[
  {
    "path": ".cirrus.yml",
    "content": "freebsd_task:\n  name: FreeBSD\n\n  matrix:\n    - name: FreeBSD 14.3\n      freebsd_instance:\n        image_family: freebsd-14-3\n\n  pkginstall_script:\n    - pkg update -f\n    - pkg install -y go125\n    - pkg install -y git\n\n  setup_script:\n    - ln -s /usr/local/bin/go125 /usr/local/bin/go\n    - pw groupadd sftpgo\n    - pw useradd sftpgo -g sftpgo -w none -m\n    - mkdir /home/sftpgo/sftpgo\n    - cp -R . /home/sftpgo/sftpgo\n    - chown -R sftpgo:sftpgo /home/sftpgo/sftpgo\n\n  compile_script:\n    - su sftpgo -c 'cd ~/sftpgo && go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo'\n    - su sftpgo -c 'cd ~/sftpgo/tests/eventsearcher && go build -trimpath -ldflags \"-s -w\" -o eventsearcher'\n    - su sftpgo -c 'cd ~/sftpgo/tests/ipfilter && go build -trimpath -ldflags \"-s -w\" -o ipfilter'\n\n  check_script:\n    - su sftpgo -c 'cd ~/sftpgo && ./sftpgo initprovider && ./sftpgo resetprovider --force'\n\n  test_script:\n    - su sftpgo -c 'cd ~/sftpgo && go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 20m ./... -coverprofile=coverage.txt -covermode=atomic'\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [drakkan] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Open Source Bug Report\ndescription: \"Submit a report and help us improve SFTPGo\"\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### 👍 Thank you for contributing to our project!\n        Before asking for help please check our [support policy](https://github.com/drakkan/sftpgo?tab=readme-ov-file#support).\n        If you are a [commercial user](https://sftpgo.com/) please contact us using the dedicated [email address](mailto:support@sftpgo.com).\n        If you'd like to contribute code, please make sure to read and understand our [Contributor License Agreement (CLA)](https://sftpgo.com/cla.html).\n        You’ll be asked to accept it when submitting a pull request.\n  - type: checkboxes\n    id: before-posting\n    attributes:\n      label: \"⚠️ This issue respects the following points: ⚠️\"\n      description: All conditions are **required**.\n      options:\n        - label: This is a **bug**, not a question or a configuration issue.\n          required: true\n        - label: This issue is **not** already reported on Github _(I've searched it)_.\n          required: true\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Bug description\n      description: |\n        Provide a description of the bug you're experiencing.\n        Don't just expect someone will guess what your specific problem is and provide full details.\n    validations:\n      required: true\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: Steps to reproduce\n      description: |\n        Describe the steps to reproduce the bug.\n        The better your description is the fastest you'll get an _(accurate)_ answer.\n      value: |\n        1.\n        2.\n        3.\n    validations:\n      required: true\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected behavior\n      description: Describe what you expected to happen instead.\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: SFTPGo version\n    validations:\n      required: true\n  - type: input\n    id: data-provider\n    attributes:\n      label: Data provider\n    validations:\n      required: true\n  - type: dropdown\n    id: install-method\n    attributes:\n      label: Installation method\n      description: |\n        Select installation method you've used.\n        _Describe the method in the \"Additional info\" section if you chose \"Other\"._\n      options:\n        - \"Community Docker image\"\n        - \"Community Deb package\"\n        - \"Community RPM package\"\n        - \"Other\"\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Configuration\n      description: \"Describe your customizations to the configuration: both config file changes and overrides via environment variables\"\n      value: config\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.\n      render: shell\n  - type: dropdown\n    id: usecase\n    attributes:\n      label: What are you using SFTPGo for?\n      description: We'd like to understand your SFTPGo usecase more\n      multiple: true\n      options:\n        - \"Private user, home usecase (home backup/VPS)\"\n        - \"Professional user, 1 person business\"\n        - \"Small business (3-person firm with file exchange?)\"\n        - \"Medium business\"\n        - \"Enterprise\"\n    validations:\n      required: true\n  - type: textarea\n    id: additional-info\n    attributes:\n      label: Additional info\n      description: Any additional information related to the issue."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Commercial Support\n    url: https://sftpgo.com/\n    about: >\n      If you need Professional support, so your reports are prioritized and resolved more quickly.\n  - name: GitHub Community Discussions\n    url: https://github.com/drakkan/sftpgo/discussions\n    about: Please ask and answer questions here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 🚀 Feature request\ndescription: Suggest an idea for SFTPGo\nlabels: [\"suggestion\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### 👍 Thank you for contributing to our project!\n        Before asking for help please check our [support policy](https://github.com/drakkan/sftpgo?tab=readme-ov-file#support).\n        If you are a [commercial user](https://sftpgo.com/) please contact us using the dedicated [email address](mailto:support@sftpgo.com).\n        If you'd like to contribute code, please make sure to read and understand our [Contributor License Agreement (CLA)](https://sftpgo.com/cla.html).\n        You’ll be asked to accept it when submitting a pull request.\n  - type: textarea\n    attributes:\n      label: Is your feature request related to a problem? Please describe.\n      description: A clear and concise description of what the problem is.\n    validations:\n      required: false\n  - type: textarea\n    attributes:\n      label: Describe the solution you'd like\n      description: A clear and concise description of what you want to happen.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe alternatives you've considered\n      description: A clear and concise description of any alternative solutions or features you've considered.\n    validations:\n      required: false\n  - type: dropdown\n    id: usecase\n    attributes:\n      label: What are you using SFTPGo for?\n      description: We'd like to understand your SFTPGo usecase more\n      multiple: true\n      options:\n        - \"Private user, home usecase (home backup/VPS)\"\n        - \"Professional user, 1 person business\"\n        - \"Small business (3-person firm with file exchange?)\"\n        - \"Medium business\"\n        - \"Enterprise\"\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots about the feature request here.\n    validations:\n      required: false"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "# Checklist for Pull Requests\n\n- [ ] Have you signed the [Contributor License Agreement](https://sftpgo.com/cla.html)?\n\n---\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n  #- package-ecosystem: \"gomod\"\n  #  directory: \"/\"\n  #  schedule:\n  #    interval: \"weekly\"\n  #  open-pull-requests-limit: 2\n\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    open-pull-requests-limit: 2\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    open-pull-requests-limit: 2\n"
  },
  {
    "path": ".github/workflows/.editorconfig",
    "content": "[*.yml]\nindent_size = 2\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "name: \"Code scanning - action\"\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: '30 1 * * 6'\n\njobs:\n  CodeQL-Build:\n    runs-on: ubuntu-latest\n\n    permissions:\n      security-events: write\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n\n    - name: Set up Go\n      uses: actions/setup-go@v6\n      with:\n        go-version: '1.25'\n\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: go\n\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v4\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4"
  },
  {
    "path": ".github/workflows/development.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n\npermissions:\n  id-token: write\n  contents: read\n\njobs:\n  test-deploy:\n    name: Test and deploy\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        go: ['1.26']\n        os: [ubuntu-latest, macos-latest]\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go }}\n\n      - name: Build for Linux/macOS x86_64\n        run: |\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo\n          cd tests/eventsearcher\n          go build -trimpath -ldflags \"-s -w\" -o eventsearcher\n          cd -\n          cd tests/ipfilter\n          go build -trimpath -ldflags \"-s -w\" -o ipfilter\n          cd -\n          ./sftpgo initprovider\n          ./sftpgo resetprovider --force\n\n      - name: Build for macOS arm64\n        if: startsWith(matrix.os, 'macos-') == true\n        run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo_arm64\n\n      - name: Run test cases using SQLite provider\n        run: go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic\n\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v5\n        with:\n          files: ./coverage.txt\n          fail_ci_if_error: false\n          token: ${{ secrets.CODECOV_TOKEN }}\n\n      - name: Run test cases using bolt provider\n        run: |\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/config -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/common -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/httpd -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 8m ./internal/sftpd -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/ftpd -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/webdavd -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/telemetry -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/mfa -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/command -covermode=atomic\n        env:\n          SFTPGO_DATA_PROVIDER__DRIVER: bolt\n          SFTPGO_DATA_PROVIDER__NAME: 'sftpgo_bolt.db'\n\n      - name: Run test cases using memory provider\n        run: go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic\n        env:\n          SFTPGO_DATA_PROVIDER__DRIVER: memory\n          SFTPGO_DATA_PROVIDER__NAME: ''\n\n      - name: Prepare build artifact for macOS\n        if: startsWith(matrix.os, 'macos-') == true\n        run: |\n          mkdir -p output/{init,bash_completion,zsh_completion}\n          cp sftpgo output/sftpgo_x86_64\n          cp sftpgo_arm64 output/\n          cp sftpgo.json output/\n          cp -r templates output/\n          cp -r static output/\n          cp -r openapi output/\n          cp init/com.github.drakkan.sftpgo.plist output/init/\n          ./sftpgo gen completion bash > output/bash_completion/sftpgo\n          ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo\n          ./sftpgo gen man -d output/man/man1\n          gzip output/man/man1/*\n\n      - name: Upload build artifact\n        if: startsWith(matrix.os, 'ubuntu-') != true\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}\n          path: output\n\n  test-deploy-windows:\n    name: Test and deploy Windows\n    runs-on: windows-latest\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '1.26'\n\n      - name: Run test cases using SQLite provider\n        run: |\n          cd tests/eventsearcher\n          go build -trimpath -ldflags \"-s -w\" -o eventsearcher.exe\n          cd ../..\n          cd tests/ipfilter\n          go build -trimpath -ldflags \"-s -w\" -o ipfilter.exe\n          cd ../..\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic\n\n      - name: Run test cases using bolt provider\n        run: |\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/config -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/common -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/httpd -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 8m ./internal/sftpd -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/ftpd -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/webdavd -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/telemetry -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/mfa -covermode=atomic\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/command -covermode=atomic\n        env:\n          SFTPGO_DATA_PROVIDER__DRIVER: bolt\n          SFTPGO_DATA_PROVIDER__NAME: 'sftpgo_bolt.db'\n\n      - name: Run test cases using memory provider\n        run: go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic\n        env:\n          SFTPGO_DATA_PROVIDER__DRIVER: memory\n          SFTPGO_DATA_PROVIDER__NAME: ''\n\n      - name: Build\n        run: |\n          $GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String\n          $DATE_TIME = ([datetime]::Now.ToUniversalTime().toString(\"yyyy-MM-ddTHH:mm:ssZ\")) | Out-String\n          $LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()\n          $REV_LIST=$LATEST_TAG+\"..HEAD\"\n          $COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()\n          $FILE_VERSION = $LATEST_TAG.substring(1)  + \".\" + $COMMITS_FROM_TAG\n          go install github.com/tc-hib/go-winres@latest\n          go-winres simply --arch amd64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version \"$FILE_VERSION\" --file-description \"SFTPGo server\" --product-name SFTPGo --copyright \"2019-2025 Nicola Murino\" --original-filename sftpgo.exe --icon  .\\windows-installer\\icon.ico\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME\" -o sftpgo.exe\n          mkdir arm64\n          $Env:CGO_ENABLED='0'\n          $Env:GOOS='windows'\n          $Env:GOARCH='arm64'\n          go-winres simply --arch arm64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version \"$FILE_VERSION\" --file-description \"SFTPGo server\" --product-name SFTPGo --copyright \"2019-2025 Nicola Murino\" --original-filename sftpgo.exe --icon  .\\windows-installer\\icon.ico\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nosqlite -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME\" -o .\\arm64\\sftpgo.exe\n          mkdir x86\n          $Env:GOARCH='386'\n          go-winres simply --arch 386 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version \"$FILE_VERSION\" --file-description \"SFTPGo server\" --product-name SFTPGo --copyright \"2019-2025 Nicola Murino\" --original-filename sftpgo.exe --icon  .\\windows-installer\\icon.ico\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nosqlite -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME\" -o .\\x86\\sftpgo.exe\n          Remove-Item Env:\\CGO_ENABLED\n          Remove-Item Env:\\GOOS\n          Remove-Item Env:\\GOARCH\n\n      - name: Initialize data provider\n        run: |\n          rm sftpgo.db\n          ./sftpgo initprovider\n        shell: bash\n\n      - name: Prepare Windows installers\n        if: ${{ github.event_name != 'pull_request' }}\n        run: |\n          choco install innosetup\n          Remove-Item -LiteralPath \"output\" -Force -Recurse -ErrorAction Ignore\n          mkdir output\n          copy .\\sftpgo.exe .\\output\n          copy .\\sftpgo.json .\\output\n          copy .\\sftpgo.db .\\output\n          copy .\\LICENSE .\\output\\LICENSE.txt\n          copy .\\NOTICE .\\output\\NOTICE.txt\n          mkdir output\\templates\n          xcopy .\\templates .\\output\\templates\\ /E\n          mkdir output\\static\n          xcopy .\\static .\\output\\static\\ /E\n          mkdir output\\openapi\n          xcopy .\\openapi .\\output\\openapi\\ /E\n          $LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()\n          $REV_LIST=$LATEST_TAG+\"..HEAD\"\n          $COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()\n          $Env:SFTPGO_ISS_DEV_VERSION = $LATEST_TAG  + \".\" + $COMMITS_FROM_TAG\n          iscc .\\windows-installer\\sftpgo.iss\n\n          rm .\\output\\sftpgo.exe\n          rm .\\output\\sftpgo.db\n          copy .\\arm64\\sftpgo.exe .\\output\n          (Get-Content .\\output\\sftpgo.json).replace('\"sqlite\"', '\"bolt\"') | Set-Content .\\output\\sftpgo.json\n          $Env:SFTPGO_DATA_PROVIDER__DRIVER='bolt'\n          $Env:SFTPGO_DATA_PROVIDER__NAME='.\\output\\sftpgo.db'\n          .\\sftpgo.exe initprovider\n          Remove-Item Env:\\SFTPGO_DATA_PROVIDER__DRIVER\n          Remove-Item Env:\\SFTPGO_DATA_PROVIDER__NAME\n          $Env:SFTPGO_ISS_ARCH='arm64'\n          iscc .\\windows-installer\\sftpgo.iss\n\n          rm .\\output\\sftpgo.exe\n          copy .\\x86\\sftpgo.exe .\\output\n          $Env:SFTPGO_ISS_ARCH='x86'\n          iscc .\\windows-installer\\sftpgo.iss\n\n      - name: Upload Windows installer x86_64 artifact\n        if: ${{ github.event_name != 'pull_request' }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_windows_installer_x86_64\n          path: ./sftpgo_windows_x86_64.exe\n\n      - name: Upload Windows installer arm64 artifact\n        if: ${{ github.event_name != 'pull_request' }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_windows_installer_arm64\n          path: ./sftpgo_windows_arm64.exe\n\n      - name: Upload Windows installer x86 artifact\n        if: ${{ github.event_name != 'pull_request' }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_windows_installer_x86\n          path: ./sftpgo_windows_x86.exe\n\n      - name: Prepare build artifact for Windows\n        run: |\n          Remove-Item -LiteralPath \"output\" -Force -Recurse -ErrorAction Ignore\n          mkdir output\n          copy .\\sftpgo.exe .\\output\n          mkdir output\\arm64\n          copy .\\arm64\\sftpgo.exe .\\output\\arm64\n          mkdir output\\x86\n          copy .\\x86\\sftpgo.exe .\\output\\x86\n          copy .\\sftpgo.json .\\output\n          (Get-Content .\\output\\sftpgo.json).replace('\"sqlite\"', '\"bolt\"') | Set-Content .\\output\\sftpgo.json\n          mkdir output\\templates\n          xcopy .\\templates .\\output\\templates\\ /E\n          mkdir output\\static\n          xcopy .\\static .\\output\\static\\ /E\n          mkdir output\\openapi\n          xcopy .\\openapi .\\output\\openapi\\ /E\n\n      - name: Upload build artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo-windows-portable\n          path: output\n\n  test-build-flags:\n    name: Test build flags\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '1.26'\n\n      - name: Build\n        run: |\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nogcs,nos3,noportable,nobolt,nomysql,nopgsql,nosqlite,nometrics,noazblob,unixcrypt -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo\n          ./sftpgo -v\n          cp -r openapi static templates internal/bundle/\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,bundle -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo\n          ./sftpgo -v\n\n  test-postgresql-mysql-crdb:\n    name: Test with PgSQL/MySQL/Cockroach\n    runs-on: ubuntu-latest\n\n    services:\n      postgres:\n        image: postgres:latest\n        env:\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: sftpgo\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - 5432:5432\n\n      mariadb:\n        image: mariadb:latest\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_DATABASE: sftpgo\n          MYSQL_USER: sftpgo\n          MYSQL_PASSWORD: sftpgo\n        options: >-\n          --health-cmd \"mariadb-admin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 6\n        ports:\n          - 3307:3306\n\n      mysql:\n        image: mysql:latest\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_DATABASE: sftpgo\n          MYSQL_USER: sftpgo\n          MYSQL_PASSWORD: sftpgo\n        options: >-\n          --health-cmd \"mysqladmin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 6\n        ports:\n          - 3308:3306\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '1.26'\n\n      - name: Build\n        run: |\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo\n          cd tests/eventsearcher\n          go build -trimpath -ldflags \"-s -w\" -o eventsearcher\n          cd -\n          cd tests/ipfilter\n          go build -trimpath -ldflags \"-s -w\" -o ipfilter\n          cd -\n\n      - name: Run tests using MySQL provider\n        run: |\n          ./sftpgo initprovider\n          ./sftpgo resetprovider --force\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic\n        env:\n          SFTPGO_DATA_PROVIDER__DRIVER: mysql\n          SFTPGO_DATA_PROVIDER__NAME: sftpgo\n          SFTPGO_DATA_PROVIDER__HOST: localhost\n          SFTPGO_DATA_PROVIDER__PORT: 3308\n          SFTPGO_DATA_PROVIDER__USERNAME: sftpgo\n          SFTPGO_DATA_PROVIDER__PASSWORD: sftpgo\n\n      - name: Run tests using PostgreSQL provider\n        run: |\n          ./sftpgo initprovider\n          ./sftpgo resetprovider --force\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic\n        env:\n          SFTPGO_DATA_PROVIDER__DRIVER: postgresql\n          SFTPGO_DATA_PROVIDER__NAME: sftpgo\n          SFTPGO_DATA_PROVIDER__HOST: localhost\n          SFTPGO_DATA_PROVIDER__PORT: 5432\n          SFTPGO_DATA_PROVIDER__USERNAME: postgres\n          SFTPGO_DATA_PROVIDER__PASSWORD: postgres\n\n      - name: Run tests using MariaDB provider\n        run: |\n          ./sftpgo initprovider\n          ./sftpgo resetprovider --force\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic\n        env:\n          SFTPGO_DATA_PROVIDER__DRIVER: mysql\n          SFTPGO_DATA_PROVIDER__NAME: sftpgo\n          SFTPGO_DATA_PROVIDER__HOST: localhost\n          SFTPGO_DATA_PROVIDER__PORT: 3307\n          SFTPGO_DATA_PROVIDER__USERNAME: sftpgo\n          SFTPGO_DATA_PROVIDER__PASSWORD: sftpgo\n          SFTPGO_DATA_PROVIDER__SQL_TABLES_PREFIX: prefix_\n\n      - name: Run tests using CockroachDB provider\n        run: |\n          docker run --rm --name crdb --health-cmd \"curl -I http://127.0.0.1:8080\" --health-interval 10s --health-timeout 5s --health-retries 6 -p 26257:26257 -d cockroachdb/cockroach:latest start-single-node --insecure --listen-addr :26257\n          sleep 10\n          docker exec crdb cockroach sql --insecure -e 'create database \"sftpgo\"'\n          ./sftpgo initprovider\n          ./sftpgo resetprovider --force\n          go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic\n          docker stop crdb\n        env:\n          SFTPGO_DATA_PROVIDER__DRIVER: cockroachdb\n          SFTPGO_DATA_PROVIDER__NAME: sftpgo\n          SFTPGO_DATA_PROVIDER__HOST: localhost\n          SFTPGO_DATA_PROVIDER__PORT: 26257\n          SFTPGO_DATA_PROVIDER__USERNAME: root\n          SFTPGO_DATA_PROVIDER__PASSWORD:\n          SFTPGO_DATA_PROVIDER__TARGET_SESSION_ATTRS: any\n          SFTPGO_DATA_PROVIDER__SQL_TABLES_PREFIX: prefix_\n\n  build-linux-packages:\n    name: Build Linux packages\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include:\n          - arch: amd64\n            distro: ubuntu:18.04\n            go: latest\n            go-arch: amd64\n          - arch: aarch64\n            distro: ubuntu18.04\n            go: latest\n            go-arch: arm64\n          - arch: ppc64le\n            distro: ubuntu18.04\n            go: latest\n            go-arch: ppc64le\n          - arch: armv7\n            distro: ubuntu18.04\n            go: latest\n            go-arch: arm7\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Get commit SHA\n        id: get_commit\n        run: echo \"COMMIT=${GITHUB_SHA::8}\" >> $GITHUB_OUTPUT\n        shell: bash\n\n      - name: Build on amd64\n        if: ${{ matrix.arch == 'amd64' }}\n        run: |\n          echo '#!/bin/bash' > build.sh\n          echo '' >> build.sh\n          echo 'set -e' >> build.sh\n          echo 'apt-get update -q -y' >> build.sh\n          echo 'apt-get install -q -y curl gcc' >> build.sh\n          if [ ${{ matrix.go }} == 'latest' ]\n          then\n            echo 'GO_VERSION=$(curl -L https://go.dev/VERSION?m=text | head -n 1)' >> build.sh\n          else\n            echo 'GO_VERSION=${{ matrix.go }}' >> build.sh\n          fi\n          echo 'GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}' >> build.sh\n          echo 'curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/${GO_VERSION}.linux-${GO_DOWNLOAD_ARCH}.tar.gz' >> build.sh\n          echo 'tar -C /usr/local -xzf go.tar.gz' >> build.sh\n          echo 'export PATH=$PATH:/usr/local/go/bin' >> build.sh\n          echo 'go version' >> build.sh\n          echo 'cd /usr/local/src' >> build.sh\n          echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo' >> build.sh\n\n          chmod 755 build.sh\n          docker run --rm --name ubuntu-build --mount type=bind,source=`pwd`,target=/usr/local/src ${{ matrix.distro }} /usr/local/src/build.sh\n          mkdir -p output/{init,bash_completion,zsh_completion}\n          cp sftpgo.json output/\n          cp -r templates output/\n          cp -r static output/\n          cp -r openapi output/\n          cp init/sftpgo.service output/init/\n          ./sftpgo gen completion bash > output/bash_completion/sftpgo\n          ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo\n          ./sftpgo gen man -d output/man/man1\n          gzip output/man/man1/*\n          cp sftpgo output/\n\n      - uses: uraimo/run-on-arch-action@v3\n        if: ${{ matrix.arch != 'amd64' }}\n        name: Build for ${{ matrix.arch }}\n        id: build\n        with:\n          arch: ${{ matrix.arch }}\n          distro: ${{ matrix.distro }}\n          setup: |\n            mkdir -p \"${PWD}/output\"\n          dockerRunArgs: |\n            --volume \"${PWD}/output:/output\"\n          shell: /bin/bash\n          install: |\n            apt-get update -q -y\n            apt-get install -q -y curl gcc\n            if [ ${{ matrix.go }} == 'latest' ]\n            then\n              GO_VERSION=$(curl -L https://go.dev/VERSION?m=text | head -n 1)\n            else\n              GO_VERSION=${{ matrix.go }}\n            fi\n            GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}\n            if [ ${{ matrix.arch}} == 'armv7' ]\n            then\n              GO_DOWNLOAD_ARCH=armv6l\n            fi\n            curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/${GO_VERSION}.linux-${GO_DOWNLOAD_ARCH}.tar.gz\n            tar -C /usr/local -xzf go.tar.gz\n          run: |\n            export PATH=$PATH:/usr/local/go/bin\n            go version\n            if [ ${{ matrix.arch}} == 'armv7' ]\n            then\n              export GOARM=7\n            fi\n            go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo\n            mkdir -p output/{init,bash_completion,zsh_completion}\n            cp sftpgo.json output/\n            cp -r templates output/\n            cp -r static output/\n            cp -r openapi output/\n            cp init/sftpgo.service output/init/\n            ./sftpgo gen completion bash > output/bash_completion/sftpgo\n            ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo\n            ./sftpgo gen man -d output/man/man1\n            gzip output/man/man1/*\n            cp sftpgo output/\n\n      - name: Upload build artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo-linux-${{ matrix.arch }}-go-${{ matrix.go }}\n          path: output\n\n      - name: Build Packages\n        id: build_linux_pkgs\n        run: |\n          export NFPM_ARCH=${{ matrix.go-arch }}\n          cd pkgs\n          ./build.sh\n          PKG_VERSION=$(cat dist/version)\n          echo \"pkg-version=${PKG_VERSION}\" >> $GITHUB_OUTPUT\n\n      - name: Upload Debian Package\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-${{ matrix.go-arch }}-deb\n          path: pkgs/dist/deb/*\n\n      - name: Upload RPM Package\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-${{ matrix.go-arch }}-rpm\n          path: pkgs/dist/rpm/*\n\n  golangci-lint:\n    name: golangci-lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '1.26'\n      - uses: actions/checkout@v6\n      - name: Run golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: latest\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Docker\n\non:\n  #schedule:\n  #  - cron: '0 4 * * *' # everyday at 4:00 AM UTC\n  push:\n    branches:\n      - main\n    tags:\n      - v*\n  pull_request:\n\njobs:\n  build:\n    name: Build\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os:\n          - ubuntu-latest\n        docker_pkg:\n          - debian\n          - alpine\n        optional_deps:\n          - true\n          - false\n        include:\n          - os: ubuntu-latest\n            docker_pkg: distroless\n            optional_deps: false\n          - os: ubuntu-latest\n            docker_pkg: debian-plugins\n            optional_deps: true\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Gather image information\n        id: info\n        run: |\n          VERSION=noop\n          DOCKERFILE=Dockerfile\n          MINOR=\"\"\n          MAJOR=\"\"\n          FEATURES=\"nopgxregisterdefaulttypes,disable_grpc_modules\"\n          if [ \"${{ github.event_name }}\" = \"schedule\" ]; then\n            VERSION=nightly\n          elif [[ $GITHUB_REF == refs/tags/* ]]; then\n            VERSION=${GITHUB_REF#refs/tags/}\n          elif [[ $GITHUB_REF == refs/heads/* ]]; then\n            VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g')\n            if [ \"${{ github.event.repository.default_branch }}\" = \"$VERSION\" ]; then\n              VERSION=edge\n            fi\n          elif [[ $GITHUB_REF == refs/pull/* ]]; then\n            VERSION=pr-${{ github.event.number }}\n          fi\n          if [[ $VERSION =~ ^v[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$ ]]; then\n            MINOR=${VERSION%.*}\n            MAJOR=${MINOR%.*}\n          fi\n          VERSION_SLIM=\"${VERSION}-slim\"\n          if [[ $DOCKER_PKG == alpine ]]; then\n            VERSION=\"${VERSION}-alpine\"\n            VERSION_SLIM=\"${VERSION}-slim\"\n            DOCKERFILE=Dockerfile.alpine\n          elif [[ $DOCKER_PKG == distroless ]]; then\n            VERSION=\"${VERSION}-distroless\"\n            VERSION_SLIM=\"${VERSION}-slim\"\n            DOCKERFILE=Dockerfile.distroless\n            FEATURES=\"${FEATURES},nosqlite\"\n          elif [[ $DOCKER_PKG == debian-plugins ]]; then\n            VERSION=\"${VERSION}-plugins\"\n            VERSION_SLIM=\"${VERSION}-slim\"\n            FEATURES=\"${FEATURES},unixcrypt\"\n          elif [[ $DOCKER_PKG == debian ]]; then\n            FEATURES=\"${FEATURES},unixcrypt\"\n          fi\n          DOCKER_IMAGES=(\"drakkan/sftpgo\" \"ghcr.io/drakkan/sftpgo\")\n          TAGS=\"${DOCKER_IMAGES[0]}:${VERSION}\"\n          TAGS_SLIM=\"${DOCKER_IMAGES[0]}:${VERSION_SLIM}\"\n\n          for DOCKER_IMAGE in ${DOCKER_IMAGES[@]}; do\n            if [[ ${DOCKER_IMAGE} != ${DOCKER_IMAGES[0]} ]]; then\n              TAGS=\"${TAGS},${DOCKER_IMAGE}:${VERSION}\"\n              TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:${VERSION_SLIM}\"\n            fi\n            if [[ $GITHUB_REF == refs/tags/* ]]; then\n              if [[ $DOCKER_PKG == debian ]]; then\n                if [[ -n $MAJOR && -n $MINOR ]]; then\n                  TAGS=\"${TAGS},${DOCKER_IMAGE}:${MINOR},${DOCKER_IMAGE}:${MAJOR}\"\n                  TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-slim,${DOCKER_IMAGE}:${MAJOR}-slim\"\n                fi\n                TAGS=\"${TAGS},${DOCKER_IMAGE}:latest\"\n                TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:slim\"\n              elif [[ $DOCKER_PKG == distroless ]]; then\n                if [[ -n $MAJOR && -n $MINOR ]]; then\n                  TAGS=\"${TAGS},${DOCKER_IMAGE}:${MINOR}-distroless,${DOCKER_IMAGE}:${MAJOR}-distroless\"\n                  TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-distroless-slim,${DOCKER_IMAGE}:${MAJOR}-distroless-slim\"\n                fi\n                TAGS=\"${TAGS},${DOCKER_IMAGE}:distroless\"\n                TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:distroless-slim\"\n              elif [[ $DOCKER_PKG == debian-plugins ]]; then\n                if [[ -n $MAJOR && -n $MINOR ]]; then\n                  TAGS=\"${TAGS},${DOCKER_IMAGE}:${MINOR}-plugins,${DOCKER_IMAGE}:${MAJOR}-plugins\"\n                  TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-plugins-slim,${DOCKER_IMAGE}:${MAJOR}-plugins-slim\"\n                fi\n                TAGS=\"${TAGS},${DOCKER_IMAGE}:plugins\"\n                TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:plugins-slim\"\n              else\n                if [[ -n $MAJOR && -n $MINOR ]]; then\n                  TAGS=\"${TAGS},${DOCKER_IMAGE}:${MINOR}-alpine,${DOCKER_IMAGE}:${MAJOR}-alpine\"\n                  TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:${MINOR}-alpine-slim,${DOCKER_IMAGE}:${MAJOR}-alpine-slim\"\n                fi\n                TAGS=\"${TAGS},${DOCKER_IMAGE}:alpine\"\n                TAGS_SLIM=\"${TAGS_SLIM},${DOCKER_IMAGE}:alpine-slim\"\n              fi\n            fi\n          done\n\n          if [[ $OPTIONAL_DEPS == true ]]; then\n            echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n            echo \"tags=${TAGS}\" >> $GITHUB_OUTPUT\n            echo \"full=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"version=${VERSION_SLIM}\" >> $GITHUB_OUTPUT\n            echo \"tags=${TAGS_SLIM}\" >> $GITHUB_OUTPUT\n            echo \"full=false\" >> $GITHUB_OUTPUT\n          fi\n          if [[ $DOCKER_PKG == debian-plugins ]]; then\n            echo \"plugins=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"plugins=false\" >> $GITHUB_OUTPUT\n          fi\n          echo \"dockerfile=${DOCKERFILE}\" >> $GITHUB_OUTPUT\n          echo \"features=${FEATURES}\" >> $GITHUB_OUTPUT\n          echo \"created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\" >> $GITHUB_OUTPUT\n          echo \"sha=${GITHUB_SHA::8}\" >> $GITHUB_OUTPUT\n        env:\n          DOCKER_PKG: ${{ matrix.docker_pkg }}\n          OPTIONAL_DEPS: ${{ matrix.optional_deps }}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v4\n\n      - name: Set up builder\n        uses: docker/setup-buildx-action@v4\n        id: builder\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v4\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n        if: ${{ github.event_name != 'pull_request' }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v4\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n        if: ${{ github.event_name != 'pull_request' }}\n\n      - name: Build and push\n        uses: docker/build-push-action@v7\n        with:\n          context: .\n          builder: ${{ steps.builder.outputs.name }}\n          file: ./${{ steps.info.outputs.dockerfile }}\n          platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/arm/v7\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.info.outputs.tags }}\n          build-args: |\n            COMMIT_SHA=${{ steps.info.outputs.sha }}\n            INSTALL_OPTIONAL_PACKAGES=${{ steps.info.outputs.full }}\n            DOWNLOAD_PLUGINS=${{ steps.info.outputs.plugins }}\n            FEATURES=${{ steps.info.outputs.features }}\n          labels: |\n            org.opencontainers.image.title=SFTPGo\n            org.opencontainers.image.description=Full-featured and highly configurable file transfer server: SFTP, HTTP/S,FTP/S, WebDAV\n            org.opencontainers.image.url=https://github.com/drakkan/sftpgo\n            org.opencontainers.image.documentation=https://github.com/drakkan/sftpgo/blob/${{ github.sha }}/docker/README.md\n            org.opencontainers.image.source=https://github.com/drakkan/sftpgo\n            org.opencontainers.image.version=${{ steps.info.outputs.version }}\n            org.opencontainers.image.created=${{ steps.info.outputs.created }}\n            org.opencontainers.image.revision=${{ github.sha }}\n            org.opencontainers.image.licenses=AGPL-3.0-only"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags: 'v*'\n\npermissions:\n  id-token: write\n  contents: write\n\nenv:\n  GO_VERSION: 1.25.8\n\njobs:\n  prepare-sources-with-deps:\n    name: Prepare sources with deps\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Get SFTPGo version\n        id: get_version\n        run: echo \"VERSION=${GITHUB_REF/refs\\/tags\\//}\" >> $GITHUB_OUTPUT\n\n      - name: Prepare release\n        run: |\n          go mod vendor\n          echo \"${SFTPGO_VERSION}\" > VERSION.txt\n          echo \"${GITHUB_SHA::8}\" >> VERSION.txt\n          tar cJvf sftpgo_${SFTPGO_VERSION}_src_with_deps.tar.xz *\n        env:\n          SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}\n\n      - name: Upload build artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.VERSION }}_src_with_deps.tar.xz\n          path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_src_with_deps.tar.xz\n          retention-days: 1\n\n  prepare-windows:\n    name: Prepare Windows binaries\n    runs-on: windows-2022\n\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Get SFTPGo version\n        id: get_version\n        run: echo \"VERSION=${GITHUB_REF/refs\\/tags\\//}\" >> $GITHUB_OUTPUT\n        shell: bash\n\n      - name: Build\n        run: |\n          $GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String\n          $DATE_TIME = ([datetime]::Now.ToUniversalTime().toString(\"yyyy-MM-ddTHH:mm:ssZ\")) | Out-String\n          $FILE_VERSION = $Env:SFTPGO_VERSION.substring(1)  + \".0\"\n          go install github.com/tc-hib/go-winres@latest\n          go-winres simply --arch amd64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version \"$FILE_VERSION\" --file-description \"SFTPGo server\" --product-name SFTPGo --copyright \"2019-2025 Nicola Murino\" --original-filename sftpgo.exe --icon  .\\windows-installer\\icon.ico\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME\" -o sftpgo.exe\n          mkdir arm64\n          $Env:CGO_ENABLED='0'\n          $Env:GOOS='windows'\n          $Env:GOARCH='arm64'\n          go-winres simply --arch arm64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version \"$FILE_VERSION\" --file-description \"SFTPGo server\" --product-name SFTPGo --copyright \"2019-2025 Nicola Murino\" --original-filename sftpgo.exe --icon  .\\windows-installer\\icon.ico\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nosqlite -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME\" -o .\\arm64\\sftpgo.exe\n          mkdir x86\n          $Env:GOARCH='386'\n          go-winres simply --arch 386 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version \"$FILE_VERSION\" --file-description \"SFTPGo server\" --product-name SFTPGo --copyright \"2019-2025 Nicola Murino\" --original-filename sftpgo.exe --icon  .\\windows-installer\\icon.ico\n          go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nosqlite -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME\" -o .\\x86\\sftpgo.exe\n          Remove-Item Env:\\CGO_ENABLED\n          Remove-Item Env:\\GOOS\n          Remove-Item Env:\\GOARCH\n        env:\n          SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}\n\n      - name: Initialize data provider\n        run: ./sftpgo initprovider\n        shell: bash\n\n      - name: Prepare Release\n        run: |\n          mkdir output\n          copy .\\sftpgo.exe .\\output\n          copy .\\sftpgo.json .\\output\n          copy .\\sftpgo.db .\\output\n          copy .\\LICENSE .\\output\\LICENSE.txt\n          copy .\\NOTICE .\\output\\NOTICE.txt\n          mkdir output\\templates\n          xcopy .\\templates .\\output\\templates\\ /E\n          mkdir output\\static\n          xcopy .\\static .\\output\\static\\ /E\n          mkdir output\\openapi\n          xcopy .\\openapi .\\output\\openapi\\ /E\n          iscc .\\windows-installer\\sftpgo.iss\n          rm .\\output\\sftpgo.exe\n          rm .\\output\\sftpgo.db\n          copy .\\arm64\\sftpgo.exe .\\output\n          (Get-Content .\\output\\sftpgo.json).replace('\"sqlite\"', '\"bolt\"') | Set-Content .\\output\\sftpgo.json\n          $Env:SFTPGO_DATA_PROVIDER__DRIVER='bolt'\n          $Env:SFTPGO_DATA_PROVIDER__NAME='.\\output\\sftpgo.db'\n          .\\sftpgo.exe initprovider\n          Remove-Item Env:\\SFTPGO_DATA_PROVIDER__DRIVER\n          Remove-Item Env:\\SFTPGO_DATA_PROVIDER__NAME\n          $Env:SFTPGO_ISS_ARCH='arm64'\n          iscc .\\windows-installer\\sftpgo.iss\n\n          rm .\\output\\sftpgo.exe\n          copy .\\x86\\sftpgo.exe .\\output\n          $Env:SFTPGO_ISS_ARCH='x86'\n          iscc .\\windows-installer\\sftpgo.iss\n        env:\n          SFTPGO_ISS_VERSION: ${{ steps.get_version.outputs.VERSION }}\n\n      - name: Prepare Portable Release\n        run: |\n          mkdir win-portable\n          copy .\\sftpgo.exe .\\win-portable\n          mkdir win-portable\\arm64\n          copy .\\arm64\\sftpgo.exe .\\win-portable\\arm64\n          mkdir win-portable\\x86\n          copy .\\x86\\sftpgo.exe .\\win-portable\\x86\n          copy .\\sftpgo.json .\\win-portable\n          (Get-Content .\\win-portable\\sftpgo.json).replace('\"sqlite\"', '\"bolt\"') | Set-Content .\\win-portable\\sftpgo.json\n          copy .\\output\\sftpgo.db .\\win-portable\n          copy .\\LICENSE .\\win-portable\\LICENSE.txt\n          copy .\\NOTICE .\\win-portable\\NOTICE.txt\n          mkdir win-portable\\templates\n          xcopy .\\templates .\\win-portable\\templates\\ /E\n          mkdir win-portable\\static\n          xcopy .\\static .\\win-portable\\static\\ /E\n          mkdir win-portable\\openapi\n          xcopy .\\openapi .\\win-portable\\openapi\\ /E\n          Compress-Archive .\\win-portable\\* sftpgo_portable.zip\n\n      - name: Upload Windows installer x86_64 artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_x86_64.exe\n          path: ./sftpgo_windows_x86_64.exe\n          retention-days: 1\n\n      - name: Upload Windows installer arm64 artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_arm64.exe\n          path: ./sftpgo_windows_arm64.exe\n          retention-days: 1\n\n      - name: Upload Windows installer x86 artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_x86.exe\n          path: ./sftpgo_windows_x86.exe\n          retention-days: 1\n\n      - name: Upload Windows portable artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_portable.zip\n          path: ./sftpgo_portable.zip\n          retention-days: 1\n\n  prepare-mac:\n    name: Prepare macOS binaries\n    runs-on: macos-14\n\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Get SFTPGo version\n        id: get_version\n        run: echo \"VERSION=${GITHUB_REF/refs\\/tags\\//}\" >> $GITHUB_OUTPUT\n        shell: bash\n\n      - name: Build for macOS x86_64\n        run: go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo\n\n      - name: Build for macOS arm64\n        run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo_arm64\n\n      - name: Initialize data provider\n        run: ./sftpgo initprovider\n        shell: bash\n\n      - name: Prepare Release\n        run: |\n          mkdir -p output/{init,sqlite,bash_completion,zsh_completion}\n          echo \"For documentation please take a look here:\" > output/README.txt\n          echo \"\" >> output/README.txt\n          echo \"https://docs.sftpgo.com\" >> output/README.txt\n          cp LICENSE output/\n          cp NOTICE output/\n          cp sftpgo output/\n          cp sftpgo.json output/\n          cp sftpgo.db output/sqlite/\n          cp -r static output/\n          cp -r openapi output/\n          cp -r templates output/\n          cp init/com.github.drakkan.sftpgo.plist output/init/\n          ./sftpgo gen completion bash > output/bash_completion/sftpgo\n          ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo\n          ./sftpgo gen man -d output/man/man1\n          gzip output/man/man1/*\n          cd output\n          tar cJvf ../sftpgo_${SFTPGO_VERSION}_macOS_x86_64.tar.xz *\n          cd ..\n          cp sftpgo_arm64 output/sftpgo\n          cd output\n          tar cJvf ../sftpgo_${SFTPGO_VERSION}_macOS_arm64.tar.xz *\n          cd ..\n        env:\n          SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}\n\n      - name: Upload macOS x86_64 artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_x86_64.tar.xz\n          path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_x86_64.tar.xz\n          retention-days: 1\n\n      - name: Upload macOS arm64 artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_arm64.tar.xz\n          path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_arm64.tar.xz\n          retention-days: 1\n\n  prepare-linux:\n    name: Prepare Linux binaries\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include:\n          - arch: amd64\n            distro: ubuntu:18.04\n            go-arch: amd64\n            deb-arch: amd64\n            rpm-arch: x86_64\n            tar-arch: x86_64\n          - arch: aarch64\n            distro: ubuntu18.04\n            go-arch: arm64\n            deb-arch: arm64\n            rpm-arch: aarch64\n            tar-arch: arm64\n          - arch: ppc64le\n            distro: ubuntu18.04\n            go-arch: ppc64le\n            deb-arch: ppc64el\n            rpm-arch: ppc64le\n            tar-arch: ppc64le\n          - arch: armv7\n            distro: ubuntu18.04\n            go-arch: arm7\n            deb-arch: armhf\n            rpm-arch: armv7hl\n            tar-arch: armv7\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Get versions\n        id: get_version\n        run: |\n          echo \"SFTPGO_VERSION=${GITHUB_REF/refs\\/tags\\//}\" >> $GITHUB_OUTPUT\n          echo \"GO_VERSION=${GO_VERSION}\" >> $GITHUB_OUTPUT\n          echo \"COMMIT=${GITHUB_SHA::8}\" >> $GITHUB_OUTPUT\n        shell: bash\n        env:\n          GO_VERSION: ${{ env.GO_VERSION }}\n\n      - name: Build on amd64\n        if: ${{ matrix.arch == 'amd64' }}\n        run: |\n          echo '#!/bin/bash' > build.sh\n          echo '' >> build.sh\n          echo 'set -e' >> build.sh\n          echo 'apt-get update -q -y' >> build.sh\n          echo 'apt-get install -q -y curl gcc' >> build.sh\n          echo 'curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/go${{ steps.get_version.outputs.GO_VERSION }}.linux-${{ matrix.go-arch }}.tar.gz' >> build.sh\n          echo 'tar -C /usr/local -xzf go.tar.gz' >> build.sh\n          echo 'export PATH=$PATH:/usr/local/go/bin' >> build.sh\n          echo 'go version' >> build.sh\n          echo 'cd /usr/local/src' >> build.sh\n          echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo' >> build.sh\n\n          chmod 755 build.sh\n          docker run --rm --name ubuntu-build --mount type=bind,source=`pwd`,target=/usr/local/src ${{ matrix.distro }} /usr/local/src/build.sh\n          mkdir -p output/{init,sqlite,bash_completion,zsh_completion}\n          echo \"For documentation please take a look here:\" > output/README.txt\n          echo \"\" >> output/README.txt\n          echo \"https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md\" >> output/README.txt\n          cp LICENSE output/\n          cp NOTICE output/\n          cp sftpgo.json output/\n          cp -r templates output/\n          cp -r static output/\n          cp -r openapi output/\n          cp init/sftpgo.service output/init/\n          ./sftpgo initprovider\n          ./sftpgo gen completion bash > output/bash_completion/sftpgo\n          ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo\n          ./sftpgo gen man -d output/man/man1\n          gzip output/man/man1/*\n          cp sftpgo output/\n          cp sftpgo.db output/sqlite/\n          cd output\n          tar cJvf sftpgo_${SFTPGO_VERSION}_linux_${{ matrix.tar-arch }}.tar.xz *\n          cd ..\n        env:\n          SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}\n\n      - uses: uraimo/run-on-arch-action@v3\n        if: ${{ matrix.arch != 'amd64' }}\n        name: Build for ${{ matrix.arch }}\n        id: build\n        with:\n          arch: ${{ matrix.arch }}\n          distro: ${{ matrix.distro }}\n          setup: |\n            mkdir -p \"${PWD}/output\"\n          dockerRunArgs: |\n            --volume \"${PWD}/output:/output\"\n          shell: /bin/bash\n          install: |\n            apt-get update -q -y\n            apt-get install -q -y curl gcc xz-utils\n            GO_DOWNLOAD_ARCH=${{ matrix.go-arch }}\n            if [ ${{ matrix.arch}} == 'armv7' ]\n            then\n              GO_DOWNLOAD_ARCH=armv6l\n            fi\n            curl --retry 5 --retry-delay 2 --connect-timeout 10 -o go.tar.gz -L https://go.dev/dl/go${{ steps.get_version.outputs.GO_VERSION }}.linux-${GO_DOWNLOAD_ARCH}.tar.gz\n            tar -C /usr/local -xzf go.tar.gz\n          run: |\n            export PATH=$PATH:/usr/local/go/bin\n            go version\n            go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -o sftpgo\n            mkdir -p output/{init,sqlite,bash_completion,zsh_completion}\n            echo \"For documentation please take a look here:\" > output/README.txt\n            echo \"\" >> output/README.txt\n            echo \"https://github.com/drakkan/sftpgo/blob/${{ steps.get_version.outputs.SFTPGO_VERSION }}/README.md\" >> output/README.txt\n            cp LICENSE output/\n            cp NOTICE output/\n            cp sftpgo.json output/\n            cp -r templates output/\n            cp -r static output/\n            cp -r openapi output/\n            cp init/sftpgo.service output/init/\n            ./sftpgo initprovider\n            ./sftpgo gen completion bash > output/bash_completion/sftpgo\n            ./sftpgo gen completion zsh > output/zsh_completion/_sftpgo\n            ./sftpgo gen man -d output/man/man1\n            gzip output/man/man1/*\n            cp sftpgo output/\n            cp sftpgo.db output/sqlite/\n            cd output\n            tar cJvf sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz *\n            cd ..\n\n      - name: Upload build artifact for ${{ matrix.arch }}\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz\n          path: ./output/sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz\n          retention-days: 1\n\n      - name: Build Packages\n        id: build_linux_pkgs\n        run: |\n          export NFPM_ARCH=${{ matrix.go-arch }}\n          cd pkgs\n          ./build.sh\n          PKG_VERSION=${SFTPGO_VERSION:1}\n          echo \"pkg-version=${PKG_VERSION}\" >> $GITHUB_OUTPUT\n        env:\n          SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}\n\n      - name: Upload Deb Package\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_${{ matrix.deb-arch}}.deb\n          path: ./pkgs/dist/deb/sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_${{ matrix.deb-arch}}.deb\n          retention-days: 1\n\n      - name: Upload RPM Package\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.${{ matrix.rpm-arch}}.rpm\n          path: ./pkgs/dist/rpm/sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.${{ matrix.rpm-arch}}.rpm\n          retention-days: 1\n\n  prepare-linux-bundle:\n    name: Prepare Linux bundle\n    needs: prepare-linux\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Get versions\n        id: get_version\n        run: |\n          echo \"SFTPGO_VERSION=${GITHUB_REF/refs\\/tags\\//}\" >> $GITHUB_OUTPUT\n        shell: bash\n\n      - name: Download amd64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz\n\n      - name: Download arm64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz\n\n      - name: Download ppc64le artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz\n\n      - name: Download armv7 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz\n\n      - name: Build bundle\n        shell: bash\n        run: |\n          mkdir -p bundle/{arm64,ppc64le,armv7}\n          cd bundle\n          tar xvf ../sftpgo_${SFTPGO_VERSION}_linux_x86_64.tar.xz\n          cd arm64\n          tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_arm64.tar.xz sftpgo\n          cd ../ppc64le\n          tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_ppc64le.tar.xz sftpgo\n          cd ../armv7\n          tar xvf ../../sftpgo_${SFTPGO_VERSION}_linux_armv7.tar.xz sftpgo\n          cd ..\n          tar cJvf sftpgo_${SFTPGO_VERSION}_linux_bundle.tar.xz *\n          cd ..\n        env:\n          SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}\n\n      - name: Upload Linux bundle\n        uses: actions/upload-artifact@v7\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz\n          path: ./bundle/sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz\n          retention-days: 1\n\n  create-release:\n    name: Release\n    needs: [prepare-linux-bundle, prepare-sources-with-deps, prepare-mac, prepare-windows]\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n      - name: Get versions\n        id: get_version\n        run: |\n          SFTPGO_VERSION=${GITHUB_REF/refs\\/tags\\//}\n          PKG_VERSION=${SFTPGO_VERSION:1}\n          echo \"SFTPGO_VERSION=${SFTPGO_VERSION}\" >> $GITHUB_OUTPUT\n          echo \"PKG_VERSION=${PKG_VERSION}\" >> $GITHUB_OUTPUT\n        shell: bash\n\n      - name: Download amd64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz\n\n      - name: Download arm64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz\n\n      - name: Download ppc64le artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz\n\n      - name: Download armv7 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz\n\n      - name: Download Linux bundle artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz\n\n      - name: Download Deb amd64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_amd64.deb\n\n      - name: Download Deb arm64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_arm64.deb\n\n      - name: Download Deb ppc64le artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_ppc64el.deb\n\n      - name: Download Deb armv7 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_armhf.deb\n\n      - name: Download RPM x86_64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.x86_64.rpm\n\n      - name: Download RPM aarch64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.aarch64.rpm\n\n      - name: Download RPM ppc64le artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.ppc64le.rpm\n\n      - name: Download RPM armv7 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.armv7hl.rpm\n\n      - name: Download macOS x86_64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_x86_64.tar.xz\n\n      - name: Download macOS arm64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_arm64.tar.xz\n\n      - name: Download Windows installer x86_64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86_64.exe\n\n      - name: Download Windows installer arm64 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_arm64.exe\n\n      - name: Download Windows installer x86 artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86.exe\n\n      - name: Download Windows portable artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_portable.zip\n\n      - name: Download source with deps artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_src_with_deps.tar.xz\n\n      - name: Create release\n        run: |\n          mv sftpgo_windows_x86_64.exe sftpgo_${SFTPGO_VERSION}_windows_x86_64.exe\n          mv sftpgo_windows_arm64.exe sftpgo_${SFTPGO_VERSION}_windows_arm64.exe\n          mv sftpgo_windows_x86.exe sftpgo_${SFTPGO_VERSION}_windows_x86.exe\n          mv sftpgo_portable.zip sftpgo_${SFTPGO_VERSION}_windows_portable.zip\n          gh release create \"${SFTPGO_VERSION}\" -t \"${SFTPGO_VERSION}\"\n          gh release upload \"${SFTPGO_VERSION}\" sftpgo_*.xz --clobber\n          gh release upload \"${SFTPGO_VERSION}\" sftpgo-*.rpm --clobber\n          gh release upload \"${SFTPGO_VERSION}\" sftpgo_*.deb --clobber\n          gh release upload \"${SFTPGO_VERSION}\" sftpgo_*.exe --clobber\n          gh release upload \"${SFTPGO_VERSION}\" sftpgo_*.zip --clobber\n          gh release view \"${SFTPGO_VERSION}\"\n        env:\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n          SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}"
  },
  {
    "path": ".gitignore",
    "content": "# compilation output\nsftpgo\nsftpgo.exe\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nrun:\n  issues-exit-code: 1\n  tests: true\nlinters:\n  enable:\n    - bodyclose\n    - dogsled\n    - dupl\n    - goconst\n    - gocyclo\n    - misspell\n    - revive\n    - rowserrcheck\n    - unconvert\n    - unparam\n    - whitespace\n  settings:\n    dupl:\n      threshold: 150\n    errcheck:\n      check-type-assertions: false\n      check-blank: false\n    goconst:\n      min-len: 3\n      min-occurrences: 3\n    gocyclo:\n      min-complexity: 15\n    # https://golangci-lint.run/usage/linters/#revive\n    revive:\n      rules:\n        - name: var-naming\n          severity: warning\n          disabled: true\n          exclude: [\"\"]\n          arguments:\n            - [\"ID\"] # AllowList\n            - [\"VM\"] # DenyList\n            - - upper-case-const: true\n            - - skip-package-name-checks: true\n  exclusions:\n    generated: lax\n    presets:\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  enable:\n    - gofmt\n    - goimports\n  settings:\n    gofmt:\n      simplify: true\n    goimports:\n      local-prefixes:\n        - github.com/drakkan/sftpgo\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "* @drakkan\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nsupport@sftpgo.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.26-trixie AS builder\n\nENV GOFLAGS=\"-mod=readonly\"\n\nRUN apt-get update && apt-get -y upgrade && rm -rf /var/lib/apt/lists/*\n\nRUN mkdir -p /workspace\nWORKDIR /workspace\n\nARG GOPROXY\n\nCOPY go.mod go.sum ./\nRUN go mod download && go mod verify\n\nARG COMMIT_SHA\n\n# This ARG allows to disable some optional features and it might be useful if you build the image yourself.\n# For example you can disable S3 and GCS support like this:\n# --build-arg FEATURES=nos3,nogcs\nARG FEATURES\n\nCOPY . .\n\nRUN set -xe && \\\n    export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \\\n    go build $(if [ -n \"${FEATURES}\" ]; then echo \"-tags ${FEATURES}\"; fi) -trimpath -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -v -o sftpgo\n\n# Set to \"true\" to download the \"official\" plugins in /usr/local/bin\nARG DOWNLOAD_PLUGINS=false\n\nRUN if [ \"${DOWNLOAD_PLUGINS}\" = \"true\" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi\n\nFROM debian:trixie-slim\n\n# Set to \"true\" to install jq\nARG INSTALL_OPTIONAL_PACKAGES=false\n\nRUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y ca-certificates media-types && rm -rf /var/lib/apt/lists/*\n\nRUN if [ \"${INSTALL_OPTIONAL_PACKAGES}\" = \"true\" ]; then apt-get update && apt-get install --no-install-recommends -y jq && rm -rf /var/lib/apt/lists/*; fi\n\nRUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups\n\nRUN groupadd --system -g 1000 sftpgo && \\\n    useradd --system --gid sftpgo --no-create-home \\\n    --home-dir /var/lib/sftpgo --shell /usr/sbin/nologin \\\n    --comment \"SFTPGo user\" --uid 1000 sftpgo\n\nCOPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json\nCOPY --from=builder /workspace/templates /usr/share/sftpgo/templates\nCOPY --from=builder /workspace/static /usr/share/sftpgo/static\nCOPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi\nCOPY --from=builder /workspace/sftpgo /usr/local/bin/sftpgo-plugin-* /usr/local/bin/\n\n# Log to the stdout so the logs will be available using docker logs\nENV SFTPGO_LOG_FILE_PATH=\"\"\n\n# Modify the default configuration file\nRUN sed -i 's|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|' /etc/sftpgo/sftpgo.json && \\\n    sed -i 's|\"backups\"|\"/srv/sftpgo/backups\"|' /etc/sftpgo/sftpgo.json\n\nRUN chown -R sftpgo:sftpgo /etc/sftpgo /srv/sftpgo && chown sftpgo:sftpgo /var/lib/sftpgo && chmod 700 /srv/sftpgo/backups\n\nWORKDIR /var/lib/sftpgo\nUSER 1000:1000\n\nCMD [\"sftpgo\", \"serve\"]\n"
  },
  {
    "path": "Dockerfile.alpine",
    "content": "FROM golang:1.26-alpine3.23 AS builder\n\nENV GOFLAGS=\"-mod=readonly\"\n\nRUN apk -U upgrade --no-cache && apk add --update --no-cache bash ca-certificates curl git gcc g++\n\nRUN mkdir -p /workspace\nWORKDIR /workspace\n\nARG GOPROXY\n\nCOPY go.mod go.sum ./\nRUN go mod download && go mod verify\n\nARG COMMIT_SHA\n\n# This ARG allows to disable some optional features and it might be useful if you build the image yourself.\n# For example you can disable S3 and GCS support like this:\n# --build-arg FEATURES=nos3,nogcs\nARG FEATURES\n\nCOPY . .\n\nRUN set -xe && \\\n    export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \\\n    go build $(if [ -n \"${FEATURES}\" ]; then echo \"-tags ${FEATURES}\"; fi) -trimpath -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -v -o sftpgo\n\nFROM alpine:3.23\n\n# Set to \"true\" to install jq\nARG INSTALL_OPTIONAL_PACKAGES=false\n\nRUN apk -U upgrade --no-cache && apk add --update --no-cache ca-certificates tzdata mailcap\n\nRUN if [ \"${INSTALL_OPTIONAL_PACKAGES}\" = \"true\" ]; then apk add --update --no-cache jq; fi\n\nRUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups\n\nRUN addgroup -g 1000 -S sftpgo && \\\n    adduser -u 1000 -h /var/lib/sftpgo -s /sbin/nologin -G sftpgo -S -D -H -g \"SFTPGo user\" sftpgo\n\nCOPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json\nCOPY --from=builder /workspace/templates /usr/share/sftpgo/templates\nCOPY --from=builder /workspace/static /usr/share/sftpgo/static\nCOPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi\nCOPY --from=builder /workspace/sftpgo /usr/local/bin/\n\n# Log to the stdout so the logs will be available using docker logs\nENV SFTPGO_LOG_FILE_PATH=\"\"\n\n# Modify the default configuration file\nRUN sed -i 's|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|' /etc/sftpgo/sftpgo.json && \\\n    sed -i 's|\"backups\"|\"/srv/sftpgo/backups\"|' /etc/sftpgo/sftpgo.json\n\nRUN chown -R sftpgo:sftpgo /etc/sftpgo /srv/sftpgo && chown sftpgo:sftpgo /var/lib/sftpgo && chmod 700 /srv/sftpgo/backups\n\nWORKDIR /var/lib/sftpgo\nUSER 1000:1000\n\nCMD [\"sftpgo\", \"serve\"]\n"
  },
  {
    "path": "Dockerfile.distroless",
    "content": "FROM golang:1.26-trixie AS builder\n\nENV CGO_ENABLED=0 GOFLAGS=\"-mod=readonly\"\n\nRUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y media-types && rm -rf /var/lib/apt/lists/*\n\nRUN mkdir -p /workspace\nWORKDIR /workspace\n\nARG GOPROXY\n\nCOPY go.mod go.sum ./\nRUN go mod download && go mod verify\n\nARG COMMIT_SHA\n\n# This ARG allows to disable some optional features and it might be useful if you build the image yourself.\n# For this variant we disable SQLite support since it requires CGO and so a C runtime which is not installed\n# in distroless/static-* images\nARG FEATURES\n\nCOPY . .\n\nRUN set -xe && \\\n    export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \\\n    go build $(if [ -n \"${FEATURES}\" ]; then echo \"-tags ${FEATURES}\"; fi) -trimpath -ldflags \"-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`\" -v -o sftpgo\n\n# Modify the default configuration file\nRUN sed -i 's|\"users_base_dir\": \"\",|\"users_base_dir\": \"/srv/sftpgo/data\",|' sftpgo.json && \\\n    sed -i 's|\"backups\"|\"/srv/sftpgo/backups\"|' sftpgo.json && \\\n    sed -i 's|\"sqlite\"|\"bolt\"|' sftpgo.json\n\nRUN mkdir /etc/sftpgo /var/lib/sftpgo /srv/sftpgo\n\nFROM gcr.io/distroless/static-debian13\n\nCOPY --from=builder --chown=1000:1000 /etc/sftpgo /etc/sftpgo\nCOPY --from=builder --chown=1000:1000 /srv/sftpgo /srv/sftpgo\nCOPY --from=builder --chown=1000:1000 /var/lib/sftpgo /var/lib/sftpgo\nCOPY --from=builder --chown=1000:1000 /workspace/sftpgo.json /etc/sftpgo/sftpgo.json\nCOPY --from=builder /workspace/templates /usr/share/sftpgo/templates\nCOPY --from=builder /workspace/static /usr/share/sftpgo/static\nCOPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi\nCOPY --from=builder /workspace/sftpgo /usr/local/bin/\nCOPY --from=builder /etc/mime.types /etc/mime.types\n\n# Log to the stdout so the logs will be available using docker logs\nENV SFTPGO_LOG_FILE_PATH=\"\"\n# These env vars are required to avoid the following error when calling user.Current():\n# unable to get the current user: user: Current requires cgo or $USER set in environment\nENV USER=sftpgo\nENV HOME=/var/lib/sftpgo\n\nWORKDIR /var/lib/sftpgo\nUSER 1000:1000\n\nCMD [\"sftpgo\", \"serve\"]"
  },
  {
    "path": "LICENSE",
    "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/>."
  },
  {
    "path": "NOTICE",
    "content": "Additional terms under GNU AGPL version 3 section 7.3(b) and 13.1:\n\nIf you have included SFTPGo so that it is offered through any network\ninteractions, including by means of an external user interface, or\nany other integration, even without modifying its source code and then\nSFTPGo is partially, fully or optionally configured via your frontend,\nyou must provide reasonable but clear attribution to the SFTPGo project\nand its author(s), not imply any endorsement by or affiliation with the\nSFTPGo project, and you must prominently offer all users interacting\nwith it remotely through a computer network an opportunity to receive\nthe Corresponding Source of the SFTPGo version you include by providing\na link to the Corresponding Source in the SFTPGo source code repository.\n"
  },
  {
    "path": "README.md",
    "content": "# SFTPGo\n\n[![CI Status](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg)](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg)\n[![License: AGPL-3.0-only](https://img.shields.io/badge/License-AGPLv3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)\n[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)\n\nFull-featured and highly configurable event-driven file transfer solution. Server protocols: SFTP, HTTP/S, FTP/S, WebDAV. Storage backends: local filesystem, encrypted local filesystem, S3 (compatible) Object Storage, Google Cloud Storage, Azure Blob Storage, other SFTP servers.\n\nWith SFTPGo you can leverage local and cloud storage backends for exchanging and storing files internally or with business partners using the same tools and processes you are already familiar with.\n\n## Project Status & Editions\n\nSFTPGo is an open-source project with a sustainable business model. We offer two editions to suit different requirements, ensuring the project remains healthy and maintained for everyone.\n\n### Open Source (Community)\n\nFree, Copyleft (AGPLv3), Community Supported. The Community edition is a fully functional, production-ready solution widely adopted worldwide. It includes all the core protocols, storage backends, and the WebAdmin/WebClient UIs. It is ideal for:\n\n- Standard file transfer needs.\n- Integrating storage backends (S3, GCS, Azure Blob) with legacy protocols.\n- Projects that are comfortable with AGPLv3 licensing.\n\n### SFTPGo Enterprise\n\nCommercial License, Professional Support, ISO 27001 Vendor. The Enterprise edition is built on the same core but extends it for mission-critical environments, compliance-heavy industries, and advanced workflows. It is a drop-in replacement (seamless upgrade).\n\n| Feature | Open Source (Community) | Enterprise Edition |\n| :--- | :--- | :--- |\n| **License Type** | AGPLv3 (Copyleft) | **Commercial License**<br/>Proprietary/No Copyleft |\n| **Vendor Compliance** | Not Applicable<br/>Community Project | **Certified Vendor**<br/>ISO 27001 & Supply Chain Validation |\n| **Support** | Community (GitHub) | **Direct from Authors** |\n| **Cloud Storage Engine** | Standard | **High Performance & Scalable**<br/>In-memory streaming (no local temp files) and up to 70% faster |\n| **High Availability (HA)** | Standard<br/>Shared DB & Storage | **Advanced**<br/>Enhanced event handling and optimized instance coordination |\n| **Automation Logic** | Simple Placeholders | **Dynamic Logic & Virtual Folders**<br/>Conditions, loops, route data across storage backends |\n| **Data Lifecycle** | Delete / Retain | **Smart Archiving**<br/>Move data to external Cloud/SFTP storage via Virtual Folders |\n| **Email Data Ingestion** | - | **Native IMAP Integration**<br/>Auto-extract attachments from email to storage |\n| **Public Sharing** | Standard Links | **Advanced & Collaborative**<br/>Email Authentication & Group Delegation |\n| **Data Protection** | - | **Encryption & Scanning**<br/>Automated PGP, Antivirus & DLP via ICAP |\n| **Advanced Identity (SSO)** | Standard | **Extended Controls**<br/>Advanced Single Sign-On parameters |\n| **Document Editing** | - | **Included**<br/>View, edit, and co-author in browser |\n\n**Note**: We are committed to keeping the Open Source edition powerful and maintained. The Enterprise edition helps fund the development of the entire SFTPGo ecosystem.\n\n## Sponsors\n\nIf you rely on SFTPGo in your projects, consider becoming a [sponsor](https://github.com/sponsors/drakkan).\n\nYour sponsorship helps cover maintenance, security updates and ongoing development of the open-source edition.\n\n### Thank you to our sponsors\n\n#### Platinum sponsors\n\n[<img src=\"./img/Aledade_logo.png\" alt=\"Aledade logo\" width=\"202\" height=\"70\">](https://www.aledade.com/)\n</br></br>\n[<img src=\"./img/jumptrading.png\" alt=\"Jump Trading logo\" width=\"362\" height=\"63\">](https://www.jumptrading.com/)\n</br></br>\n[<img src=\"./img/wpengine.png\" alt=\"WP Engine logo\" width=\"331\" height=\"63\">](https://wpengine.com/)\n\n#### Silver sponsors\n\n[<img src=\"./img/IDCS.png\" alt=\"IDCS logo\" width=\"212\" height=\"51\">](https://idcs.ip-paris.fr/)\n\n#### Bronze sponsors\n\n[<img src=\"./img/7digital.png\" alt=\"7digital logo\" width=\"178\" height=\"56\">](https://www.7digital.com/)\n</br></br>\n[<img src=\"./img/servinga.png\" alt=\"servinga logo\" width=\"258\" height=\"56\">](https://servinga.com/)\n</br></br>\n[<img src=\"./img/reui.png\" alt=\"ReUI logo\" width=\"151\" height=\"56\">](https://www.reui.io/)\n\n## Documentation\n\nYou can explore all supported features and configuration options at [docs.sftpgo.com](https://docs.sftpgo.com/latest/).\n\n**Note:** The link above refers to the **Community Edition**.\nFor details on **Enterprise Edition**, please refer to the [Enterprise Documentation](https://docs.sftpgo.com/enterprise/).\n\n## Support\n\n- **Community Support**: use [GitHub Discussions](https://github.com/drakkan/sftpgo/discussions) to ask questions, share feedback, and engage with other users.\n- **Commercial Support**: If you require guaranteed SLAs, expert guidance, or the advanced features listed above, check out [SFTPGo Enterprise](https://sftpgo.com).\n\nSFTPGo Enterprise is available as:\n\n- On-premises: Full control on your infrastructure. More details: [sftpgo.com/on-premises](https://sftpgo.com/on-premises)\n- Fully managed SaaS: We handle the infrastructure. More details: [sftpgo.com/saas](https://sftpgo.com/saas)\n\n## Internationalization\n\nThe translations are available via [Crowdin](https://crowdin.com/project/sftpgo), who have granted us an open source license.\n\nBefore translating please take a look at our contribution [guidelines](https://docs.sftpgo.com/latest/web-interfaces/#internationalization).\n\n## Release Cadence\n\nSFTPGo follows a feature-driven release cycle.\n\n- Enterprise Edition: Receives major new features first and follows a faster [release cadence](https://docs.sftpgo.com/enterprise/changelog/).\n- Community Edition: Remains maintained, receiving bug fixes, security updates, and updates to core features.\n\n## Acknowledgements\n\nSFTPGo makes use of the third party libraries listed inside [go.mod](./go.mod).\n\nWe are very grateful to all the people who contributed with ideas and/or pull requests.\n\nThank you to [ysura](https://www.ysura.com/) for granting us stable access to a test AWS S3 account.\n\nThank you to [KeenThemes](https://keenthemes.com/) for granting us a custom license to use their amazing [themes](https://keenthemes.com/bootstrap-templates) for the SFTPGo WebAdmin and WebClient user interfaces, across both the Open Source and Open Core versions.\n\nThank you to [Crowdin](https://crowdin.com/) for granting us an Open Source License.\n\nThank you to [Incode](https://www.incode.it/) for helping us to improve the UI/UX.\n\n## License\n\nSFTPGo source code is licensed under the GNU AGPL-3.0-only with [additional terms](./NOTICE).\n\nThe [theme](https://keenthemes.com/bootstrap-templates) used in WebAdmin and WebClient user interfaces is proprietary, this means:\n\n- KeenThemes HTML/CSS/JS components are allowed for use only within the SFTPGo product and restricted to be used in a resealable HTML template that can compete with KeenThemes products anyhow.\n- The SFTPGo WebAdmin and WebClient user interfaces (HTML, CSS and JS components) based on this theme are allowed for use only within the SFTPGo product and therefore cannot be used in derivative works/products without an explicit grant from the [SFTPGo Team](mailto:support@sftpgo.com).\n\nMore information about [compliance](https://sftpgo.com/compliance.html).\n\n**Note:** We do not provide legal advice. If you have questions about license compliance or whether your use case is permitted under the license terms, please consult your legal team.\n\n## Copyright\n\nCopyright (C) 2019 - 2026 Nicola Murino\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWe actively maintain the latest stable release of SFTPGo. While we strive to keep the Open Source version secure and up-to-date, maintenance is performed on a best-effort basis by the community and contributors.\n\n## Scope and Dependency Policy\n\nOur security advisories focus on vulnerabilities found within the **SFTPGo codebase itself**.\n\nTo ensure the long-term sustainability of the project, we handle upstream dependencies (like the Go standard library, external packages, or Docker base images) as follows:\n\n- Community Updates: For the Open Source version, vulnerabilities in upstream components (such as the Go standard library or third-party packages) are addressed during our **regular release cycles**. We generally do not provide immediate, out-of-band or ad-hoc releases to address dependency-only CVEs.\n- Empowering Users: One of the strengths of SFTPGo being open-source is that you have full control. If your security scanners require an immediate fix, you can always rebuild the project using the latest patched Go toolchain or updated dependencies.\n- Compatibility: We are committed to keeping SFTPGo compatible with the latest stable Go compiler. If an upstream fix breaks SFTPGo, fixing that becomes a priority for us.\n- Professional Needs: We understand that some organizations have strict compliance requirements or internal SLAs that require guaranteed, immediate response times and out-of-band patches. For these cases, we offer [SFTPGo Enterprise](https://sftpgo.com/on-premises) to cover the additional maintenance and support overhead.\n\n## Reporting a Vulnerability\n\nTo report (possible) security issues in SFTPGo, please either send a mail to the [SFTPGo Team](mailto:support@sftpgo.com) or use Github's [private reporting feature](https://github.com/drakkan/sftpgo/security/advisories/new).\n"
  },
  {
    "path": "crowdin.yml",
    "content": "project_id_env: CROWDIN_PROJECT_ID\napi_token_env: CROWDIN_PERSONAL_TOKEN\nfiles:\n  - source: /static/locales/en/translation.json\n    translation: /static/locales/%two_letters_code%/%original_file_name%\n    type: i18next_json\n"
  },
  {
    "path": "docker/scripts/download-plugins.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nARCH=$(uname -m)\n\ncase ${ARCH} in\n    x86_64)\n        SUFFIX=amd64\n        ;;\n    aarch64)\n        SUFFIX=arm64\n        ;;\n    *)\n        SUFFIX=ppc64le\n        ;;\nesac\n\necho \"Downloading plugins for arch ${SUFFIX}\"\n\nPLUGINS=(geoipfilter kms pubsub eventstore eventsearch auth)\n\nfor PLUGIN in \"${PLUGINS[@]}\"; do\n    URL=\"https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}\"\n    DEST=\"/usr/local/bin/sftpgo-plugin-${PLUGIN}\"\n\n    echo \"Downloading ${PLUGIN}...\"\n    if curl --fail --silent --show-error -L \"${URL}\" --output \"${DEST}\"; then\n        chmod 755 \"${DEST}\"\n    else\n        echo \"Error: Failed to download ${PLUGIN}\" >&2\n        exit 1\n    fi\ndone\n\necho \"All plugins downloaded successfully\"\n"
  },
  {
    "path": "examples/OTP/authy/README.md",
    "content": "# Authy\n\nThese example show how-to integrate [Twilio Authy API](https://www.twilio.com/docs/authy/api) for One-Time-Password logins.\n\nThe examples assume that the user has the free [Authy app](https://authy.com/) installed and uses it to generate offline [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) codes (soft tokens).\n\nYou first need to [create an Authy Application in the Twilio Console](https://twilio.com/console/authy/applications?_ga=2.205553366.451688189.1597667213-1526360003.1597667213), then you can create a new Authy user and store a reference to the matching SFTPGo account.\n\nVerify that your Authy application is successfully registered:\n\n```bash\nexport AUTHY_API_KEY=<your api key here>\ncurl 'https://api.authy.com/protected/json/app/details' -H \"X-Authy-API-Key: $AUTHY_API_KEY\"\n```\n\nnow create an Authy user:\n\n```bash\ncurl -XPOST \"https://api.authy.com/protected/json/users/new\" \\\n-H \"X-Authy-API-Key: $AUTHY_API_KEY\" \\\n--data-urlencode user[email]=\"user@domain.com\" \\\n--data-urlencode user[cellphone]=\"317-338-9302\" \\\n--data-urlencode user[country_code]=\"54\"\n```\n\nThe response is something like this:\n\n```json\n{\"message\":\"User created successfully.\",\"user\":{\"id\":xxxxxxxx},\"success\":true}\n```\n\nSave the user id somewhere and add a reference to the matching SFTPGo account. You could also store this ID in the `additional_info` SFTPGo user field.\n\nAfter this step you can use the Authy app installed on your phone to generate TOTP codes.\n\nNow you can verify the token using an HTTP GET request:\n\n```bash\nexport TOKEN=<TOTP you read from Authy app>\nexport AUTHY_ID=<user id>\ncurl -i \"https://api.authy.com/protected/json/verify/${TOKEN}/${AUTHY_ID}\" \\\n    -H \"X-Authy-API-Key: $AUTHY_API_KEY\"\n```\n\nSo inside your hook you need to check:\n\n- the HTTP response code for the verify request, it must be `200`\n- the JSON response body, it must contains the key `success` with the value `true` (as string)\n\nIf these conditions are met the token is valid and you allow the user to login.\n\nWe provide the following examples:\n\n- [Keyboard interactive authentication](./keyint/README.md) for 2FA using password + Authy one time token.\n- [External authentication](./extauth/README.md) using Authy one time tokens as passwords.\n- [Check password hook](./checkpwd/README.md) for 2FA using a password consisting of a fixed string and a One Time Token.\n\nPlease note that these are sample programs not intended for production use, you should write your own hook based on them and you should prefer HTTP based hooks if performance is a concern.\n\n:warning: SFTPGo has also built-in 2FA support.\n"
  },
  {
    "path": "examples/OTP/authy/checkpwd/README.md",
    "content": "# Authy 2FA via check password hook\n\nThis example shows how to use 2FA via the check password hook using a password consisting of a fixed part and an Authy TOTP token. The hook will check the TOTP token using the Authy API and SFTPGo will check the fixed part. Please read the [sample code](./main.go), it should be self explanatory.\n"
  },
  {
    "path": "examples/OTP/authy/checkpwd/go.mod",
    "content": "module github.com/drakkan/sftpgo/authy/checkpwd\n\ngo 1.22.2\n"
  },
  {
    "path": "examples/OTP/authy/checkpwd/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\ntype userMapping struct {\n\tSFTPGoUsername string\n\tAuthyID        int64\n\tAuthyAPIKey    string\n}\n\ntype checkPasswordResponse struct {\n\t// 0 KO, 1 OK, 2 partial success\n\tStatus int `json:\"status\"`\n\t// for status == 2 this is the password that SFTPGo will check against the one stored\n\t// inside the data provider\n\tToVerify string `json:\"to_verify\"`\n}\n\nvar (\n\tmapping []userMapping\n)\n\nfunc init() {\n\t// this is for demo only, you probably want to get this mapping dynamically, for example using a database query\n\tmapping = append(mapping, userMapping{\n\t\tSFTPGoUsername: \"<SFTPGo username>\",\n\t\tAuthyID:        1234567,\n\t\tAuthyAPIKey:    \"<your api key>\",\n\t})\n}\n\nfunc printResponse(status int, toVerify string) {\n\tr := checkPasswordResponse{\n\t\tStatus:   status,\n\t\tToVerify: toVerify,\n\t}\n\tresp, _ := json.Marshal(r)\n\tfmt.Printf(\"%v\\n\", string(resp))\n\tif status > 0 {\n\t\tos.Exit(0)\n\t} else {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc main() {\n\t// get credentials from env vars\n\tusername := os.Getenv(\"SFTPGO_AUTHD_USERNAME\")\n\tpassword := os.Getenv(\"SFTPGO_AUTHD_PASSWORD\")\n\n\tfor _, m := range mapping {\n\t\tif m.SFTPGoUsername == username {\n\t\t\t// Authy token len is 7, we assume that we have the password followed by the token\n\t\t\tpwdLen := len(password)\n\t\t\tif pwdLen <= 7 {\n\t\t\t\tprintResponse(0, \"\")\n\t\t\t}\n\t\t\tpwd := password[:pwdLen-7]\n\t\t\tauthyToken := password[pwdLen-7:]\n\t\t\t// now verify the authy token and instruct SFTPGo to check the password if the token is OK\n\t\t\turl := fmt.Sprintf(\"https://api.authy.com/protected/json/verify/%v/%v\", authyToken, m.AuthyID)\n\t\t\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatal(err)\n\t\t\t}\n\t\t\treq.Header.Set(\"X-Authy-API-Key\", m.AuthyAPIKey)\n\t\t\thttpClient := &http.Client{\n\t\t\t\tTimeout: 10 * time.Second,\n\t\t\t}\n\t\t\tresp, err := httpClient.Do(req)\n\t\t\tif err != nil {\n\t\t\t\tprintResponse(0, \"\")\n\t\t\t}\n\t\t\tdefer resp.Body.Close()\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\t// status code 200 is expected\n\t\t\t\tprintResponse(0, \"\")\n\t\t\t}\n\t\t\tvar authyResponse map[string]interface{}\n\t\t\trespBody, err := io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tprintResponse(0, \"\")\n\t\t\t}\n\t\t\terr = json.Unmarshal(respBody, &authyResponse)\n\t\t\tif err != nil {\n\t\t\t\tprintResponse(0, \"\")\n\t\t\t}\n\t\t\tif authyResponse[\"success\"].(string) == \"true\" {\n\t\t\t\tprintResponse(2, pwd)\n\t\t\t}\n\t\t\tprintResponse(0, \"\")\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// no mapping found\n\tprintResponse(0, \"\")\n}\n"
  },
  {
    "path": "examples/OTP/authy/extauth/README.md",
    "content": "# Authy external authentication\n\nThis example shows how to use Authy TOTP token as password for SFTPGo users. Please read the [sample code](./main.go), it should be self explanatory.\n"
  },
  {
    "path": "examples/OTP/authy/extauth/go.mod",
    "content": "module github.com/drakkan/sftpgo/authy/extauth\n\ngo 1.22.2\n"
  },
  {
    "path": "examples/OTP/authy/extauth/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\ntype userMapping struct {\n\tSFTPGoUsername string\n\tAuthyID        int64\n\tAuthyAPIKey    string\n}\n\n// we assume that the SFTPGo already exists, we only check the one time token.\n// If you need to create the SFTPGo user more fields are needed here\ntype minimalSFTPGoUser struct {\n\tStatus      int                 `json:\"status,omitempty\"`\n\tUsername    string              `json:\"username\"`\n\tHomeDir     string              `json:\"home_dir,omitempty\"`\n\tPermissions map[string][]string `json:\"permissions\"`\n}\n\nvar (\n\tmapping []userMapping\n)\n\nfunc init() {\n\t// this is for demo only, you probably want to get this mapping dynamically, for example using a database query\n\tmapping = append(mapping, userMapping{\n\t\tSFTPGoUsername: \"<SFTPGo username>\",\n\t\tAuthyID:        1234567,\n\t\tAuthyAPIKey:    \"<your api key>\",\n\t})\n}\n\nfunc printResponse(username string) {\n\tu := minimalSFTPGoUser{\n\t\tUsername: username,\n\t\tStatus:   1,\n\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t}\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{\"*\"}\n\tresp, _ := json.Marshal(u)\n\tfmt.Printf(\"%v\\n\", string(resp))\n\tif len(username) > 0 {\n\t\tos.Exit(0)\n\t} else {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc main() {\n\t// get credentials from env vars\n\tusername := os.Getenv(\"SFTPGO_AUTHD_USERNAME\")\n\tpassword := os.Getenv(\"SFTPGO_AUTHD_PASSWORD\")\n\tif len(password) == 0 {\n\t\t// login method is not password\n\t\tprintResponse(\"\")\n\t\treturn\n\t}\n\n\tfor _, m := range mapping {\n\t\tif m.SFTPGoUsername == username {\n\t\t\t// mapping found we can now verify the token\n\t\t\turl := fmt.Sprintf(\"https://api.authy.com/protected/json/verify/%v/%v\", password, m.AuthyID)\n\t\t\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatal(err)\n\t\t\t}\n\t\t\treq.Header.Set(\"X-Authy-API-Key\", m.AuthyAPIKey)\n\t\t\thttpClient := &http.Client{\n\t\t\t\tTimeout: 10 * time.Second,\n\t\t\t}\n\t\t\tresp, err := httpClient.Do(req)\n\t\t\tif err != nil {\n\t\t\t\tprintResponse(\"\")\n\t\t\t}\n\t\t\tdefer resp.Body.Close()\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\t// status code 200 is expected\n\t\t\t\tprintResponse(\"\")\n\t\t\t}\n\t\t\tvar authyResponse map[string]interface{}\n\t\t\trespBody, err := io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tprintResponse(\"\")\n\t\t\t}\n\t\t\terr = json.Unmarshal(respBody, &authyResponse)\n\t\t\tif err != nil {\n\t\t\t\tprintResponse(\"\")\n\t\t\t}\n\t\t\tif authyResponse[\"success\"].(string) == \"true\" {\n\t\t\t\tprintResponse(username)\n\t\t\t}\n\t\t\tprintResponse(\"\")\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// no mapping found\n\tprintResponse(\"\")\n}\n"
  },
  {
    "path": "examples/OTP/authy/keyint/README.md",
    "content": "# Authy 2FA using keyboard interactive authentication\n\nThis example shows how to authenticate SFTP users using 2FA (password + Authy token). Please read the [sample code](./main.go), it should be self explanatory.\n"
  },
  {
    "path": "examples/OTP/authy/keyint/go.mod",
    "content": "module github.com/drakkan/sftpgo/authy/keyint\n\ngo 1.22.2\n"
  },
  {
    "path": "examples/OTP/authy/keyint/main.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\ntype userMapping struct {\n\tSFTPGoUsername string\n\tAuthyID        int64\n\tAuthyAPIKey    string\n}\n\ntype keyboardAuthHookResponse struct {\n\tInstruction string   `json:\"instruction,omitempty\"`\n\tQuestions   []string `json:\"questions,omitempty\"`\n\tEchos       []bool   `json:\"echos,omitempty\"`\n\tAuthResult  int      `json:\"auth_result\"`\n\tCheckPwd    int      `json:\"check_password,omitempty\"`\n}\n\nvar (\n\tmapping []userMapping\n)\n\nfunc init() {\n\t// this is for demo only, you probably want to get this mapping dynamically, for example using a database query\n\tmapping = append(mapping, userMapping{\n\t\tSFTPGoUsername: \"<SFTPGo username>\",\n\t\tAuthyID:        1234567,\n\t\tAuthyAPIKey:    \"<your api key>\",\n\t})\n}\n\nfunc printAuthResponse(result int) {\n\tresp, _ := json.Marshal(keyboardAuthHookResponse{\n\t\tAuthResult: result,\n\t})\n\tfmt.Printf(\"%v\\n\", string(resp))\n\tif result == 1 {\n\t\tos.Exit(0)\n\t} else {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc main() {\n\t// get credentials from env vars\n\tusername := os.Getenv(\"SFTPGO_AUTHD_USERNAME\")\n\tvar userMap userMapping\n\tfor _, m := range mapping {\n\t\tif m.SFTPGoUsername == username {\n\t\t\tuserMap = m\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif userMap.SFTPGoUsername != username {\n\t\t// no mapping found\n\t\tos.Exit(1)\n\t}\n\n\tcheckPwdQuestion := keyboardAuthHookResponse{\n\t\tInstruction: \"This is a sample keyboard authentication program that ask for your password + Authy token\",\n\t\tQuestions:   []string{\"Your password: \"},\n\t\tEchos:       []bool{false},\n\t\tCheckPwd:    1,\n\t\tAuthResult:  0,\n\t}\n\n\tq, _ := json.Marshal(checkPwdQuestion)\n\tfmt.Printf(\"%v\\n\", string(q))\n\n\t// in a real world app you probably want to use a read timeout\n\tscanner := bufio.NewScanner(os.Stdin)\n\tscanner.Scan()\n\tif scanner.Err() != nil {\n\t\tprintAuthResponse(-1)\n\t}\n\tresponse := scanner.Text()\n\tif response != \"OK\" {\n\t\tprintAuthResponse(-1)\n\t}\n\n\tcheckTokenQuestion := keyboardAuthHookResponse{\n\t\tInstruction: \"\",\n\t\tQuestions:   []string{\"Authy token: \"},\n\t\tEchos:       []bool{false},\n\t\tCheckPwd:    0,\n\t\tAuthResult:  0,\n\t}\n\n\tq, _ = json.Marshal(checkTokenQuestion)\n\tfmt.Printf(\"%v\\n\", string(q))\n\tscanner.Scan()\n\tif scanner.Err() != nil {\n\t\tprintAuthResponse(-1)\n\t}\n\tauthyToken := scanner.Text()\n\n\turl := fmt.Sprintf(\"https://api.authy.com/protected/json/verify/%v/%v\", authyToken, userMap.AuthyID)\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\tprintAuthResponse(-1)\n\t}\n\treq.Header.Set(\"X-Authy-API-Key\", userMap.AuthyAPIKey)\n\thttpClient := &http.Client{\n\t\tTimeout: 10 * time.Second,\n\t}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\tprintAuthResponse(-1)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\t// status code 200 is expected\n\t\tprintAuthResponse(-1)\n\t}\n\tvar authyResponse map[string]interface{}\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tprintAuthResponse(-1)\n\t}\n\terr = json.Unmarshal(respBody, &authyResponse)\n\tif err != nil {\n\t\tprintAuthResponse(-1)\n\t}\n\tif authyResponse[\"success\"].(string) == \"true\" {\n\t\tprintAuthResponse(1)\n\t}\n\tprintAuthResponse(-1)\n}\n"
  },
  {
    "path": "examples/backup/README.md",
    "content": "# Data Backup\n\n:warning: Since v2.4.0 you can use the [EventManager](https://docs.sftpgo.com/latest/eventmanager/) to schedule backups.\n\nThe `backup` example script shows how to use the SFTPGo REST API to backup your data.\n\nThe script is written in Python and has the following requirements:\n\n- python3 or python2\n- python [Requests](https://requests.readthedocs.io/en/master/) module\n\nThe provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:\n\n- username: `admin`\n- password: `password`\n\nand, if you execute it daily, it saves a different backup file for each day of the week. The backups will be saved within the configured `backups_path`.\n\nPlease edit the script according to your needs.\n"
  },
  {
    "path": "examples/backup/backup",
    "content": "#!/usr/bin/env python\n\nfrom datetime import datetime\nimport sys\n\nimport requests\n\ntry:\n\timport urllib.parse as urlparse\nexcept ImportError:\n\timport urlparse\n\n# change base_url to point to your SFTPGo installation\nbase_url = \"http://127.0.0.1:8080\"\n# set to False if you want to skip TLS certificate validation\nverify_tls_cert = True\n# set the credentials for a valid admin here\nadmin_user = \"admin\"\nadmin_password = \"password\"\n\n# get a JWT token\nauth = requests.auth.HTTPBasicAuth(admin_user, admin_password)\nr = requests.get(urlparse.urljoin(base_url, \"api/v2/token\"), auth=auth, verify=verify_tls_cert, timeout=10)\nif r.status_code != 200:\n\tprint(\"error getting access token: {}\".format(r.text))\n\tsys.exit(1)\naccess_token = r.json()[\"access_token\"]\nauth_header = {\"Authorization\": \"Bearer \" + access_token}\n\nr = requests.get(urlparse.urljoin(base_url, \"api/v2/dumpdata\"),\n\t\t\t\t\tparams={\"output-file\":\"backup_{}.json\".format(datetime.today().strftime('%w'))},\n\t\t\t\t\theaders=auth_header, verify=verify_tls_cert, timeout=10)\nif r.status_code == 200:\n\tprint(\"backup OK\")\nelse:\n\tprint(\"backup error, status {}, response: {}\".format(r.status_code, r.text))\n"
  },
  {
    "path": "examples/bulkupdate/README.md",
    "content": "# Bulk user update\n\nThe `bulkuserupdate` example script shows how to use the SFTPGo REST API to easily update some common parameters for multiple users while preserving the others.\n\nThe script is written in Python and has the following requirements:\n\n- python3 or python2\n- python [Requests](https://requests.readthedocs.io/en/master/) module\n\nThe provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:\n\n- username: `admin`\n- password: `password`\n\nand it updates some fields for `user1`, `user2` and `user3`.\n\nPlease edit the script according to your needs.\n"
  },
  {
    "path": "examples/bulkupdate/bulkuserupdate",
    "content": "#!/usr/bin/env python\n\nimport posixpath\nimport sys\n\nimport requests\n\ntry:\n\timport urllib.parse as urlparse\nexcept ImportError:\n\timport urlparse\n\n# change base_url to point to your SFTPGo installation\nbase_url = \"http://127.0.0.1:8080\"\n# set to False if you want to skip TLS certificate validation\nverify_tls_cert = True\n# set the credentials for a valid admin here\nadmin_user = \"admin\"\nadmin_password = \"password\"\n# insert here the users you want to update\nusers_to_update = [\"user1\", \"user2\", \"user3\"]\n# set here the fields you need to update\nfields_to_update = {\"status\":0, \"quota_files\": 1000, \"additional_info\":\"updated using the bulkuserupdate example script\"}\n\n# get a JWT token\nauth = requests.auth.HTTPBasicAuth(admin_user, admin_password)\nr = requests.get(urlparse.urljoin(base_url, \"api/v2/token\"), auth=auth, verify=verify_tls_cert, timeout=10)\nif r.status_code != 200:\n\tprint(\"error getting access token: {}\".format(r.text))\n\tsys.exit(1)\naccess_token = r.json()[\"access_token\"]\nauth_header = {\"Authorization\": \"Bearer \" + access_token}\n\nfor username in users_to_update:\n\tr = requests.get(urlparse.urljoin(base_url, posixpath.join(\"api/v2/users\", username)),\n\t\t\t\t\theaders=auth_header, verify=verify_tls_cert, timeout=10)\n\tif r.status_code != 200:\n\t\tprint(\"error getting user {}: {}\".format(username, r.text))\n\t\tcontinue\n\tuser = r.json()\n\tuser.update(fields_to_update)\n\tr = requests.put(urlparse.urljoin(base_url, posixpath.join(\"api/v2/users\", username)),\n\t\t\t\t\theaders=auth_header, verify=verify_tls_cert, json=user, timeout=10)\n\tif r.status_code == 200:\n\t\tprint(\"user {} updated\".format(username))\n\telse:\n\t\tprint(\"error updating user {}, response code: {} response text: {}\".format(username,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tr.status_code,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tr.text))\n"
  },
  {
    "path": "examples/convertusers/README.md",
    "content": "# Import users from other stores\n\n`convertusers` is a very simple command line client, written in python, to import users from other stores. It requires `python3` or `python2`.\n\nHere is the usage:\n\n```console\nusage: convertusers [-h] [--min-uid MIN_UID] [--max-uid MAX_UID] [--usernames USERNAMES [USERNAMES ...]]\n                    [--force-uid FORCE_UID] [--force-gid FORCE_GID]\n                    input_file {unix-passwd,pure-ftpd,proftpd} output_file\n\nConvert users to a JSON format suitable to use with loadddata\n\npositional arguments:\n  input_file\n  {unix-passwd,pure-ftpd,proftpd}\n                        To import from unix-passwd format you need the permission to read /etc/shadow that is typically\n                        granted to the root user only\n  output_file\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --min-uid MIN_UID     if >= 0 only import users with UID greater or equal to this value. Default: -1\n  --max-uid MAX_UID     if >= 0 only import users with UID lesser or equal to this value. Default: -1\n  --usernames USERNAMES [USERNAMES ...]\n                        Only import users with these usernames. Default: []\n  --force-uid FORCE_UID\n                        if >= 0 the imported users will have this UID in SFTPGo. Default: -1\n  --force-gid FORCE_GID\n                        if >= 0 the imported users will have this GID in SFTPGo. Default: -1\n```\n\nLet's see some examples:\n\n```console\npython convertusers \"\" unix-passwd unix_users.json --min-uid 500 --force-uid 1000 --force-gid 1000\n```\n\n```console\npython convertusers pureftpd.passwd pure-ftpd pure_users.json --usernames \"user1\" \"user2\"\n```\n\n```console\npython convertusers proftpd.passwd proftpd pro_users.json\n```\n\nThe generated json file can be used as input for the `loaddata` REST API.\n\nPlease note that when importing Linux/Unix users the input file is not required: `/etc/passwd` and `/etc/shadow` are automatically parsed. `/etc/shadow` read permission is typically granted to the `root` user only, so you need to execute `convertusers` as `root`.\n\n:warning: SFTPGo does not currently support `yescrypt` hashed passwords.\n"
  },
  {
    "path": "examples/convertusers/convertusers",
    "content": "#!/usr/bin/env python\n\nimport argparse\nimport json\nimport sys\nimport time\n\ntry:\n\timport pwd\n\timport spwd\nexcept ImportError:\n\tpwd = None\n\n\nclass ConvertUsers:\n\n\tdef __init__(self, input_file, users_format, output_file, min_uid, max_uid, usernames, force_uid, force_gid):\n\t\tself.input_file = input_file\n\t\tself.users_format = users_format\n\t\tself.output_file = output_file\n\t\tself.min_uid = min_uid\n\t\tself.max_uid = max_uid\n\t\tself.usernames = usernames\n\t\tself.force_uid = force_uid\n\t\tself.force_gid = force_gid\n\t\tself.SFTPGoUsers = []\n\n\tdef buildUserObject(self, username, password, home_dir, uid, gid, max_sessions, quota_size, quota_files, upload_bandwidth,\n\t\t\t\t\tdownload_bandwidth, status, expiration_date, allowed_ip=[], denied_ip=[]):\n\t\treturn {'id':0, 'username':username, 'password':password, 'home_dir':home_dir, 'uid':uid, 'gid':gid,\n\t\t\t'max_sessions':max_sessions, 'quota_size':quota_size, 'quota_files':quota_files, 'permissions':{'/':[\"*\"]},\n\t\t\t'upload_bandwidth':upload_bandwidth, 'download_bandwidth':download_bandwidth,\n\t\t\t'status':status, 'expiration_date':expiration_date,\n\t\t\t'filters':{'allowed_ip':allowed_ip, 'denied_ip':denied_ip}}\n\n\tdef addUser(self, user):\n\t\tuser['id'] = len(self.SFTPGoUsers) + 1\n\t\tprint('')\n\t\tprint('New user imported: {}'.format(user))\n\t\tprint('')\n\t\tself.SFTPGoUsers.append(user)\n\n\tdef saveUsers(self):\n\t\tif self.SFTPGoUsers:\n\t\t\tdata = {'users':self.SFTPGoUsers}\n\t\t\tjsonData = json.dumps(data)\n\t\t\twith open(self.output_file, 'w') as f:\n\t\t\t\tf.write(jsonData)\n\t\t\tprint()\n\t\t\tprint('Number of users saved to \"{}\": {}. You can import them using loaddata'.format(self.output_file,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlen(self.SFTPGoUsers)))\n\t\t\tprint()\n\t\t\tsys.exit(0)\n\t\telse:\n\t\t\tprint('No user imported')\n\t\t\tsys.exit(1)\n\n\tdef convert(self):\n\t\tif self.users_format == 'unix-passwd':\n\t\t\tself.convertFromUnixPasswd()\n\t\telif self.users_format == 'pure-ftpd':\n\t\t\tself.convertFromPureFTPD()\n\t\telse:\n\t\t\tself.convertFromProFTPD()\n\t\tself.saveUsers()\n\n\tdef isUserValid(self, username, uid):\n\t\tif self.usernames and not username in self.usernames:\n\t\t\treturn False\n\t\tif self.min_uid >= 0 and uid < self.min_uid:\n\t\t\treturn False\n\t\tif self.max_uid >= 0 and uid > self.max_uid:\n\t\t\treturn False\n\t\treturn True\n\n\tdef convertFromUnixPasswd(self):\n\t\tdays_from_epoch_time = time.time() / 86400\n\t\tfor user in pwd.getpwall():\n\t\t\tusername = user.pw_name\n\t\t\tpassword = user.pw_passwd\n\t\t\tuid = user.pw_uid\n\t\t\tgid = user.pw_gid\n\t\t\thome_dir = user.pw_dir\n\t\t\tstatus = 1\n\t\t\texpiration_date = 0\n\t\t\tif not self.isUserValid(username, uid):\n\t\t\t\tcontinue\n\t\t\tif self.force_uid >= 0:\n\t\t\t\tuid = self.force_uid\n\t\t\tif self.force_gid >= 0:\n\t\t\t\tgid = self.force_gid\n\t\t\t# FIXME: if the passwords aren't in /etc/shadow they are probably DES encrypted and we don't support them\n\t\t\tif password == 'x' or password == '*':\n\t\t\t\tuser_info = spwd.getspnam(username)\n\t\t\t\tpassword = user_info.sp_pwdp\n\t\t\t\tif not password or password == '!!' or password == '!*':\n\t\t\t\t\tprint('cannot import user \"{}\" without a password'.format(username))\n\t\t\t\t\tcontinue\n\t\t\t\tif user_info.sp_inact > 0:\n\t\t\t\t\tlast_pwd_change_diff = days_from_epoch_time - user_info.sp_lstchg\n\t\t\t\t\tif last_pwd_change_diff > user_info.sp_inact:\n\t\t\t\t\t\tstatus = 0\n\t\t\t\tif user_info.sp_expire > 0:\n\t\t\t\t\texpiration_date = user_info.sp_expire * 86400\n\t\t\tself.addUser(self.buildUserObject(username, password, home_dir, uid, gid, 0, 0, 0, 0, 0, status,\n\t\t\t\t\t\t\t\t\t\t\texpiration_date))\n\n\tdef convertFromProFTPD(self):\n\t\twith open(self.input_file, 'r') as f:\n\t\t\tfor line in f:\n\t\t\t\tfields = line.split(':')\n\t\t\t\tif len(fields) > 6:\n\t\t\t\t\tusername = fields[0]\n\t\t\t\t\tpassword = fields[1]\n\t\t\t\t\tuid = int(fields[2])\n\t\t\t\t\tgid = int(fields[3])\n\t\t\t\t\thome_dir = fields[5]\n\t\t\t\t\tif not self.isUserValid(username, uid):\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tif self.force_uid >= 0:\n\t\t\t\t\t\tuid = self.force_uid\n\t\t\t\t\tif self.force_gid >= 0:\n\t\t\t\t\t\tgid = self.force_gid\n\t\t\t\t\tself.addUser(self.buildUserObject(username, password, home_dir, uid, gid, 0, 0, 0, 0, 0, 1, 0))\n\n\tdef convertPureFTPDIP(self, fields):\n\t\tresult = []\n\t\tif not fields:\n\t\t\treturn result\n\t\tfor v in fields.split(','):\n\t\t\tip_mask = v.strip()\n\t\t\tif not ip_mask:\n\t\t\t\tcontinue\n\t\t\tif ip_mask.count('.') < 3 and ip_mask.count(':') < 3:\n\t\t\t\tprint('cannot import pure-ftpd IP: {}'.format(ip_mask))\n\t\t\t\tcontinue\n\t\t\tif '/' not in ip_mask:\n\t\t\t\tip_mask += '/32'\n\t\t\tresult.append(ip_mask)\n\t\treturn result\n\n\tdef convertFromPureFTPD(self):\n\t\twith open(self.input_file, 'r') as f:\n\t\t\tfor line in f:\n\t\t\t\tfields = line.split(':')\n\t\t\t\tif len(fields) > 16:\n\t\t\t\t\tusername = fields[0]\n\t\t\t\t\tpassword = fields[1]\n\t\t\t\t\tuid = int(fields[2])\n\t\t\t\t\tgid = int(fields[3])\n\t\t\t\t\thome_dir = fields[5]\n\t\t\t\t\tupload_bandwidth = 0\n\t\t\t\t\tif fields[6]:\n\t\t\t\t\t\tupload_bandwidth = int(int(fields[6]) / 1024)\n\t\t\t\t\tdownload_bandwidth = 0\n\t\t\t\t\tif fields[7]:\n\t\t\t\t\t\tdownload_bandwidth = int(int(fields[7]) / 1024)\n\t\t\t\t\tmax_sessions = 0\n\t\t\t\t\tif fields[10]:\n\t\t\t\t\t\tmax_sessions = int(fields[10])\n\t\t\t\t\tquota_files = 0\n\t\t\t\t\tif fields[11]:\n\t\t\t\t\t\tquota_files = int(fields[11])\n\t\t\t\t\tquota_size = 0\n\t\t\t\t\tif fields[12]:\n\t\t\t\t\t\tquota_size = int(fields[12])\n\t\t\t\t\tallowed_ip = self.convertPureFTPDIP(fields[15])\n\t\t\t\t\tdenied_ip = self.convertPureFTPDIP(fields[16])\n\t\t\t\t\tif not self.isUserValid(username, uid):\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tif self.force_uid >= 0:\n\t\t\t\t\t\tuid = self.force_uid\n\t\t\t\t\tif self.force_gid >= 0:\n\t\t\t\t\t\tgid = self.force_gid\n\t\t\t\t\tself.addUser(self.buildUserObject(username, password, home_dir, uid, gid, max_sessions, quota_size,\n\t\t\t\t\t\t\t\t\t\t\t\t\t  quota_files, upload_bandwidth, download_bandwidth, 1, 0, allowed_ip,\n\t\t\t\t\t\t\t\t\t\t\t\t\t  denied_ip))\n\n\nif __name__ == '__main__':\n\tparser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\n\t\t\t\t\t\t\t\t\t'Convert users to a JSON format suitable to use with loadddata')\n\tsupportedUsersFormats = []\n\thelp_text = ''\n\tif pwd is not None:\n\t\tsupportedUsersFormats.append('unix-passwd')\n\t\thelp_text = 'To import from unix-passwd format you need the permission to read /etc/shadow that is typically granted to the root user only'\n\tsupportedUsersFormats.append('pure-ftpd')\n\tsupportedUsersFormats.append('proftpd')\n\tparser.add_argument('input_file', type=str)\n\tparser.add_argument('users_format', type=str, choices=supportedUsersFormats, help=help_text)\n\tparser.add_argument('output_file', type=str)\n\tparser.add_argument('--min-uid', type=int, default=-1, help='if >= 0 only import users with UID greater or equal ' +\n\t\t\t\t\t\t\t\t'to this value. Default: %(default)s')\n\tparser.add_argument('--max-uid', type=int, default=-1, help='if >= 0 only import users with UID lesser or equal ' +\n\t\t\t\t\t\t\t\t'to this value. Default: %(default)s')\n\tparser.add_argument('--usernames', type=str, nargs='+', default=[], help='Only import users with these usernames. ' +\n\t\t\t\t\t\t\t\t'Default: %(default)s')\n\tparser.add_argument('--force-uid', type=int, default=-1, help='if >= 0 the imported users will have this UID in ' +\n\t\t\t\t\t\t\t\t'SFTPGo. Default: %(default)s')\n\tparser.add_argument('--force-gid', type=int, default=-1, help='if >= 0 the imported users will have this GID in ' +\n\t\t\t\t\t\t\t\t'SFTPGo. Default: %(default)s')\n\n\targs = parser.parse_args()\n\n\tconvertUsers = ConvertUsers(args.input_file, args.users_format, args.output_file, args.min_uid, args.max_uid,\n\t\t\t\t\t\t\t\targs.usernames, args.force_uid, args.force_gid)\n\tconvertUsers.convert()\n"
  },
  {
    "path": "examples/ldapauth/README.md",
    "content": "# LDAPAuth\n\nThis is an example for an external authentication program. It performs authentication against an LDAP server.\nIt is tested against [389ds](https://directory.fedoraproject.org/) and can be used as starting point to authenticate using any LDAP server including Active Directory.\n\nYou need to change the LDAP connection parameters and the user search query to match your environment.\nYou can build this example using the following command:\n\n```console\ngo build -ldflags \"-s -w\" -o ldapauth\n```\n\nThis program assumes that the 389ds schema was extended to add support for public keys using the following ldif file placed in `/etc/dirsrv/schema/98openssh-ldap.ldif`:\n\n```console\ndn: cn=schema\nchangetype: modify\nadd: attributetypes\nattributetypes: ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' DESC 'MANDATORY: OpenSSH Public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )\n-\nadd: objectclasses\nobjectClasses: ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY DESC 'MANDATORY: OpenSSH LPK objectclass' MUST ( uid ) MAY ( sshPublicKey ) )\n-\n\ndn: cn=sshpublickey,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config\nchangetype: add\ncn: sshpublickey\nnsIndexType: eq\nnsIndexType: pres\nnsSystemIndex: false\nobjectClass: top\nobjectClass: nsIndex\n\ndn: cn=sshpublickey_self_manage,ou=groups,dc=example,dc=com\nchangetype: add\nobjectClass: top\nobjectClass: groupofuniquenames\ncn: sshpublickey_self_manage\ndescription: Members of this group gain the ability to edit their own sshPublicKey field\n\ndn: dc=example,dc=com\nchangetype: modify\nadd: aci\naci: (targetattr = \"sshPublicKey\") (version 3.0; acl \"Allow members of sshpublickey_self_manage to edit their keys\"; allow(write) (groupdn = \"ldap:///cn=sshpublickey_self_manage,ou=groups,dc=example,dc=com\" and userdn=\"ldap:///self\" ); )\n-\n```\n\n:warning: A plugin for LDAP/Active Directory authentication is also [available](https://github.com/sftpgo/sftpgo-plugin-auth).\n"
  },
  {
    "path": "examples/ldapauth/go.mod",
    "content": "module github.com/drakkan/ldapauth\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/go-ldap/ldap/v3 v3.4.13\n\tgolang.org/x/crypto v0.49.0\n)\n\nrequire (\n\tgithub.com/Azure/go-ntlmssp v0.1.0 // indirect\n\tgithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n)\n"
  },
  {
    "path": "examples/ldapauth/go.sum",
    "content": "github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=\ngithub.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=\ngithub.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=\ngithub.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=\ngithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=\ngithub.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=\ngithub.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "examples/ldapauth/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"log/syslog\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-ldap/ldap/v3\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nconst (\n\trootDN       = \"dc=example,dc=com\"\n\tbindUsername = \"cn=sftpgo,\" + rootDN\n\tbindURL      = \"ldap:///\" // That is, the server on the default port of localhost.\n\tpasswordFile = \"/etc/sftpgo/admin-password.txt\" // make this file readable only by the server\n\tpublicDir    = \"/var/www/webdav/public\"\n)\n\ntype userFilters struct {\n\tDeniedLoginMethods []string `json:\"denied_login_methods,omitempty\"`\n}\n\ntype minimalSFTPGoUser struct {\n\tStatus      int                 `json:\"status,omitempty\"`\n\tUsername    string              `json:\"username\"`\n\tHomeDir     string              `json:\"home_dir,omitempty\"`\n\tUID         int                 `json:\"uid,omitempty\"`\n\tGID         int                 `json:\"gid,omitempty\"`\n\tPermissions map[string][]string `json:\"permissions\"`\n\tFilters     userFilters         `json:\"filters\"`\n}\n\nfunc exitError() {\n\tlog.Printf(\"exitError\\n\")\n\tu := minimalSFTPGoUser{\n\t\tUsername: \"\",\n\t}\n\tresp, _ := json.Marshal(u)\n\tfmt.Printf(\"%v\\n\", string(resp))\n\tos.Exit(1)\n}\n\nfunc printSuccessResponse(username, homeDir string, uid, gid int, permissions []string) {\n\tu := minimalSFTPGoUser{\n\t\tUsername: username,\n\t\tHomeDir:  homeDir,\n\t\tUID:      uid,\n\t\tGID:      gid,\n\t\tStatus:   1,\n\t}\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = permissions\n\t// uncomment the next line to require publickey+password authentication\n\t//u.Filters.DeniedLoginMethods = []string{\"publickey\", \"password\", \"keyboard-interactive\", \"publickey+keyboard-interactive\"}\n\tresp, _ := json.Marshal(u)\n\tlog.Printf(\"%v\\n\", string(resp))\n\tfmt.Printf(\"%v\\n\", string(resp))\n\tos.Exit(0)\n}\n\nfunc main() {\n\tlogWriter, err := syslog.New(syslog.LOG_NOTICE, \"sftpgo\")\n\tif err == nil {\n\t\tlog.SetOutput(logWriter)\n\t}\n\t// get credentials from env vars\n\tusername := os.Getenv(\"SFTPGO_AUTHD_USERNAME\")\n\tpassword := os.Getenv(\"SFTPGO_AUTHD_PASSWORD\")\n\tpublickey := os.Getenv(\"SFTPGO_AUTHD_PUBLIC_KEY\")\n\tif strings.ToLower(username) == \"anonymous\" {\n\t\tprintSuccessResponse(\"anonymous\", publicDir, 0, 0, []string{\"list\", \"download\"})\n\t\treturn\n\t}\n\tl, err := ldap.DialURL(bindURL)\n\tif err != nil {\n\t\tlog.Printf(\"DialURL: %s\\n\", err.Error())\n\t\texitError()\n\t}\n\tdefer l.Close()\n\t// bind to the ldap server with an account that can read users\n\tbindPassword, err := os.ReadFile(passwordFile)\n\tif err != nil {\n\t\tlog.Printf(\"ReadFile(%s): %s\\n\", passwordFile, err.Error())\n\t\texitError()\n\t}\n\terr = l.Bind(bindUsername, string(bindPassword))\n\tif err != nil {\n\t\tlog.Printf(\"Bind(%s): %s\\n\", bindUsername, err.Error())\n\t\texitError()\n\t}\n\n\t// search the user trying to login and fetch some attributes, this search string is tested against 389ds using the default configuration\n\tlog.Printf(\"username=%s\\n\", username)\n\tsearchFilter := fmt.Sprintf(\"(uid=%s)\", ldap.EscapeFilter(username))\n\tsearchRequest := ldap.NewSearchRequest(\n\t\t\"ou=people,\" + rootDN,\n\t\tldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,\n\t\tsearchFilter,\n\t\t[]string{\"dn\", \"uid\", \"homeDirectory\", \"uidNumber\", \"gidNumber\", \"nsSshPublicKey\"},\n\t\tnil,\n\t)\n\n\tsr, err := l.Search(searchRequest)\n\tif err != nil {\n\t\tlog.Printf(\"Search(%s): %s\\n\", searchFilter, err.Error())\n\t\texitError()\n\t}\n\n\t// we expect exactly one user\n\tif len(sr.Entries) != 1 {\n\t\tlog.Printf(\"Search(%s): %d entries\\n\", searchFilter, len(sr.Entries))\n\t\texitError()\n\t}\n\n\tif len(publickey) > 0 {\n\t\t// check public key\n\t\tuserKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publickey))\n\t\tif err != nil {\n\t\t\tlog.Printf(\"ParseAuthorizedKey(%s): %s\\n\", publickey, err.Error())\n\t\t\texitError()\n\t\t}\n\t\tauthOk := false\n\t\tfor _, k := range sr.Entries[0].GetAttributeValues(\"nsSshPublicKey\") {\n\t\t\tkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))\n\t\t\t// we skip an invalid public key stored inside the LDAP server\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif bytes.Equal(key.Marshal(), userKey.Marshal()) {\n\t\t\t\tauthOk = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !authOk {\n\t\t\tlog.Printf(\"publickey %s !authOk\\n\", publickey)\n\t\t\texitError()\n\t\t}\n\t} else {\n\t\t// bind to the LDAP server with the user dn and the given password to check the password\n\t\tuserdn := sr.Entries[0].DN\n\t\t// log.Printf(\"password=%s\\n\", password)\n\t\terr = l.Bind(userdn, password)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Bind(%s): %s\\n\", userdn, err.Error())\n\t\t\texitError()\n\t\t}\n\t}\n\n\t// People in the LDAP directory aren't necessarily Linux users;\n\t// so they might not have a uidNumber or gidNumber.\n\tuidNumber := sr.Entries[0].GetAttributeValue(\"uidNumber\")\n\tuid, err := strconv.Atoi(uidNumber)\n\tif err != nil {\n\t\t//log.Printf(\"uid Atoi(%s) = %s\\n\", uidNumber, err.Error())\n\t\tuid = 0\n\t}\n\tgidNumber := sr.Entries[0].GetAttributeValue(\"gidNumber\")\n\tgid, err := strconv.Atoi(gidNumber)\n\tif err != nil {\n\t\t//log.Printf(\"gid Atoi(%s) = %s\\n\", gidNumber, err.Error())\n\t\tgid = 0\n\t}\n\thomeDir := sr.Entries[0].GetAttributeValue(\"homeDirectory\")\n\tif (len(homeDir) <= 0) {\n\t\thomeDir = publicDir // homeDir is a required attribute.\n\t}\n\t// return the authenticated user\n\tprintSuccessResponse(sr.Entries[0].GetAttributeValue(\"uid\"), homeDir, uid, gid, []string{\"*\"})\n}\n"
  },
  {
    "path": "examples/ldapauthserver/README.md",
    "content": "# LDAPAuthServer\n\nThis is an example for an HTTP server to use as external authentication HTTP hook. It performs authentication against an LDAP server.\nIt is tested against [389ds](https://directory.fedoraproject.org/) and can be used as starting point to authenticate using any LDAP server including Active Directory.\n\nYou can configure the server using the [ldapauth.toml](./ldapauth.toml) configuration file.\nYou can build this example using the following command:\n\n```console\ngo build -ldflags \"-s -w\" -o ldapauthserver\n```\n\n:warning: A plugin for LDAP/Active Directory authentication is also [available](https://github.com/sftpgo/sftpgo-plugin-auth).\n"
  },
  {
    "path": "examples/ldapauthserver/cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/drakkan/sftpgo/ldapauthserver/config\"\n\t\"github.com/drakkan/sftpgo/ldapauthserver/utils\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\nconst (\n\tlogSender           = \"cmd\"\n\tconfigDirFlag       = \"config-dir\"\n\tconfigDirKey        = \"config_dir\"\n\tconfigFileFlag      = \"config-file\"\n\tconfigFileKey       = \"config_file\"\n\tlogFilePathFlag     = \"log-file-path\"\n\tlogFilePathKey      = \"log_file_path\"\n\tlogMaxSizeFlag      = \"log-max-size\"\n\tlogMaxSizeKey       = \"log_max_size\"\n\tlogMaxBackupFlag    = \"log-max-backups\"\n\tlogMaxBackupKey     = \"log_max_backups\"\n\tlogMaxAgeFlag       = \"log-max-age\"\n\tlogMaxAgeKey        = \"log_max_age\"\n\tlogCompressFlag     = \"log-compress\"\n\tlogCompressKey      = \"log_compress\"\n\tlogVerboseFlag      = \"log-verbose\"\n\tlogVerboseKey       = \"log_verbose\"\n\tprofilerFlag        = \"profiler\"\n\tprofilerKey         = \"profiler\"\n\tdefaultConfigDir    = \".\"\n\tdefaultConfigName   = config.DefaultConfigName\n\tdefaultLogFile      = \"ldapauth.log\"\n\tdefaultLogMaxSize   = 10\n\tdefaultLogMaxBackup = 5\n\tdefaultLogMaxAge    = 28\n\tdefaultLogCompress  = false\n\tdefaultLogVerbose   = true\n)\n\nvar (\n\tconfigDir     string\n\tconfigFile    string\n\tlogFilePath   string\n\tlogMaxSize    int\n\tlogMaxBackups int\n\tlogMaxAge     int\n\tlogCompress   bool\n\tlogVerbose    bool\n\n\trootCmd = &cobra.Command{\n\t\tUse:   \"ldapauthserver\",\n\t\tShort: \"LDAP Authentication Server for SFTPGo\",\n\t}\n)\n\nfunc init() {\n\tversion := utils.GetAppVersion()\n\trootCmd.Flags().BoolP(\"version\", \"v\", false, \"\")\n\trootCmd.Version = version.GetVersionAsString()\n\trootCmd.SetVersionTemplate(`{{printf \"LDAP Authentication Server version: \"}}{{printf \"%s\" .Version}}\n`)\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc addConfigFlags(cmd *cobra.Command) {\n\tviper.SetDefault(configDirKey, defaultConfigDir)\n\tviper.BindEnv(configDirKey, \"LDAPAUTH_CONFIG_DIR\")\n\tcmd.Flags().StringVarP(&configDir, configDirFlag, \"c\", viper.GetString(configDirKey),\n\t\t`Location for the config dir. This directory\nshould contain the \"ldapauth\" configuration\nfile or the configured config-file. This flag\ncan be set using LDAPAUTH_CONFIG_DIR env var too.\n`)\n\tviper.BindPFlag(configDirKey, cmd.Flags().Lookup(configDirFlag))\n\n\tviper.SetDefault(configFileKey, defaultConfigName)\n\tviper.BindEnv(configFileKey, \"LDAPAUTH_CONFIG_FILE\")\n\tcmd.Flags().StringVarP(&configFile, configFileFlag, \"f\", viper.GetString(configFileKey),\n\t\t`Name for the configuration file. It must be\nthe name of a file stored in config-dir not\nthe absolute path to the configuration file.\nThe specified file name must have no extension\nwe automatically load JSON, YAML, TOML, HCL and\nJava properties. Therefore if you set \\\"ldapauth\\\"\nthen \\\"ldapauth.toml\\\", \\\"ldapauth.yaml\\\" and\nso on are searched. This flag can be set using\nLDAPAUTH_CONFIG_FILE env var too.\n`)\n\tviper.BindPFlag(configFileKey, cmd.Flags().Lookup(configFileFlag))\n}\n\nfunc addServeFlags(cmd *cobra.Command) {\n\taddConfigFlags(cmd)\n\n\tviper.SetDefault(logFilePathKey, defaultLogFile)\n\tviper.BindEnv(logFilePathKey, \"LDAPAUTH_LOG_FILE_PATH\")\n\tcmd.Flags().StringVarP(&logFilePath, logFilePathFlag, \"l\", viper.GetString(logFilePathKey),\n\t\t`Location for the log file. Leave empty to write\nlogs to the standard output. This flag can be\nset using LDAPAUTH_LOG_FILE_PATH env var too.\n`)\n\tviper.BindPFlag(logFilePathKey, cmd.Flags().Lookup(logFilePathFlag))\n\n\tviper.SetDefault(logMaxSizeKey, defaultLogMaxSize)\n\tviper.BindEnv(logMaxSizeKey, \"LDAPAUTH_LOG_MAX_SIZE\")\n\tcmd.Flags().IntVarP(&logMaxSize, logMaxSizeFlag, \"s\", viper.GetInt(logMaxSizeKey),\n\t\t`Maximum size in megabytes of the log file\nbefore it gets rotated. This flag can be set\nusing LDAPAUTH_LOG_MAX_SIZE env var too. It\nis unused if log-file-path is empty.`)\n\tviper.BindPFlag(logMaxSizeKey, cmd.Flags().Lookup(logMaxSizeFlag))\n\n\tviper.SetDefault(logMaxBackupKey, defaultLogMaxBackup)\n\tviper.BindEnv(logMaxBackupKey, \"LDAPAUTH_LOG_MAX_BACKUPS\")\n\tcmd.Flags().IntVarP(&logMaxBackups, \"log-max-backups\", \"b\", viper.GetInt(logMaxBackupKey),\n\t\t`Maximum number of old log files to retain.\nThis flag can be set using LDAPAUTH_LOG_MAX_BACKUPS\nenv var too. It is unused if log-file-path is\nempty.`)\n\tviper.BindPFlag(logMaxBackupKey, cmd.Flags().Lookup(logMaxBackupFlag))\n\n\tviper.SetDefault(logMaxAgeKey, defaultLogMaxAge)\n\tviper.BindEnv(logMaxAgeKey, \"LDAPAUTH_LOG_MAX_AGE\")\n\tcmd.Flags().IntVarP(&logMaxAge, \"log-max-age\", \"a\", viper.GetInt(logMaxAgeKey),\n\t\t`Maximum number of days to retain old log files.\nThis flag can be set using LDAPAUTH_LOG_MAX_AGE\nenv var too. It is unused if log-file-path is\nempty.`)\n\tviper.BindPFlag(logMaxAgeKey, cmd.Flags().Lookup(logMaxAgeFlag))\n\n\tviper.SetDefault(logCompressKey, defaultLogCompress)\n\tviper.BindEnv(logCompressKey, \"LDAPAUTH_LOG_COMPRESS\")\n\tcmd.Flags().BoolVarP(&logCompress, logCompressFlag, \"z\", viper.GetBool(logCompressKey),\n\t\t`Determine if the rotated log files\nshould be compressed using gzip. This flag can\nbe set using LDAPAUTH_LOG_COMPRESS env var too.\nIt is unused if log-file-path is empty.`)\n\tviper.BindPFlag(logCompressKey, cmd.Flags().Lookup(logCompressFlag))\n\n\tviper.SetDefault(logVerboseKey, defaultLogVerbose)\n\tviper.BindEnv(logVerboseKey, \"LDAPAUTH_LOG_VERBOSE\")\n\tcmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, \"v\", viper.GetBool(logVerboseKey),\n\t\t`Enable verbose logs. This flag can be set\nusing LDAPAUTH_LOG_VERBOSE env var too.\n`)\n\tviper.BindPFlag(logVerboseKey, cmd.Flags().Lookup(logVerboseFlag))\n}\n"
  },
  {
    "path": "examples/ldapauthserver/cmd/serve.go",
    "content": "package cmd\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/drakkan/sftpgo/ldapauthserver/config\"\n\t\"github.com/drakkan/sftpgo/ldapauthserver/httpd\"\n\t\"github.com/drakkan/sftpgo/ldapauthserver/logger\"\n\t\"github.com/drakkan/sftpgo/ldapauthserver/utils\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\tserveCmd = &cobra.Command{\n\t\tUse:   \"serve\",\n\t\tShort: \"Start the LDAP Authentication Server\",\n\t\tLong: `To start the server with the default values for the command line flags simply use:\n\nldapauthserver serve\n\nPlease take a look at the usage below to customize the startup options`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tstartServer()\n\t\t},\n\t}\n)\n\nfunc init() {\n\trootCmd.AddCommand(serveCmd)\n\taddServeFlags(serveCmd)\n}\n\nfunc startServer() error {\n\tlogLevel := zerolog.DebugLevel\n\tif !logVerbose {\n\t\tlogLevel = zerolog.InfoLevel\n\t}\n\tif !filepath.IsAbs(logFilePath) && utils.IsFileInputValid(logFilePath) {\n\t\tlogFilePath = filepath.Join(configDir, logFilePath)\n\t}\n\tlogger.InitLogger(logFilePath, logMaxSize, logMaxBackups, logMaxAge, logCompress, logLevel)\n\tversion := utils.GetAppVersion()\n\tlogger.Info(logSender, \"\", \"starting LDAP Auth Server %v, config dir: %v, config file: %v, log max size: %v log max backups: %v \"+\n\t\t\"log max age: %v log verbose: %v, log compress: %v\", version.GetVersionAsString(), configDir, configFile, logMaxSize,\n\t\tlogMaxBackups, logMaxAge, logVerbose, logCompress)\n\tconfig.LoadConfig(configDir, configFile)\n\treturn httpd.StartHTTPServer(configDir, config.GetHTTPDConfig())\n}\n"
  },
  {
    "path": "examples/ldapauthserver/config/config.go",
    "content": "package config\n\nimport (\n\t\"strings\"\n\n\t\"github.com/drakkan/sftpgo/ldapauthserver/logger\"\n\t\"github.com/spf13/viper\"\n)\n\nconst (\n\tlogSender = \"config\"\n\t// DefaultConfigName defines the name for the default config file.\n\t// This is the file name without extension, we use viper and so we\n\t// support all the config files format supported by viper\n\tDefaultConfigName = \"ldapauth\"\n\t// ConfigEnvPrefix defines a prefix that ENVIRONMENT variables will use\n\tconfigEnvPrefix = \"ldapauth\"\n)\n\n// HTTPDConfig defines configuration for the HTTPD server\ntype HTTPDConfig struct {\n\tBindAddress        string `mapstructure:\"bind_address\"`\n\tBindPort           int    `mapstructure:\"bind_port\"`\n\tAuthUserFile       string `mapstructure:\"auth_user_file\"`\n\tCertificateFile    string `mapstructure:\"certificate_file\"`\n\tCertificateKeyFile string `mapstructure:\"certificate_key_file\"`\n}\n\n// LDAPConfig defines the configuration parameters for LDAP connections and searches\ntype LDAPConfig struct {\n\tBaseDN             string   `mapstructure:\"basedn\"`\n\tBindURL            string   `mapstructure:\"bind_url\"`\n\tBindUsername       string   `mapstructure:\"bind_username\"`\n\tBindPassword       string   `mapstructure:\"bind_password\"`\n\tSearchFilter       string   `mapstructure:\"search_filter\"`\n\tSearchBaseAttrs    []string `mapstructure:\"search_base_attrs\"`\n\tDefaultUID         int      `mapstructure:\"default_uid\"`\n\tDefaultGID         int      `mapstructure:\"default_gid\"`\n\tForceDefaultUID    bool     `mapstructure:\"force_default_uid\"`\n\tForceDefaultGID    bool     `mapstructure:\"force_default_gid\"`\n\tInsecureSkipVerify bool     `mapstructure:\"insecure_skip_verify\"`\n\tCACertificates     []string `mapstructure:\"ca_certificates\"`\n}\n\ntype appConfig struct {\n\tHTTPD HTTPDConfig `mapstructure:\"httpd\"`\n\tLDAP  LDAPConfig  `mapstructure:\"ldap\"`\n}\n\nvar conf appConfig\n\nfunc init() {\n\tconf = appConfig{\n\t\tHTTPD: HTTPDConfig{\n\t\t\tBindAddress:        \"\",\n\t\t\tBindPort:           9000,\n\t\t\tAuthUserFile:       \"\",\n\t\t\tCertificateFile:    \"\",\n\t\t\tCertificateKeyFile: \"\",\n\t\t},\n\t\tLDAP: LDAPConfig{\n\t\t\tBaseDN:       \"dc=example,dc=com\",\n\t\t\tBindURL:      \"ldap://192.168.1.103:389\",\n\t\t\tBindUsername: \"cn=Directory Manager\",\n\t\t\tBindPassword: \"YOUR_ADMIN_PASSWORD_HERE\",\n\t\t\tSearchFilter: \"(&(objectClass=nsPerson)(uid=%s))\",\n\t\t\tSearchBaseAttrs: []string{\n\t\t\t\t\"dn\",\n\t\t\t\t\"homeDirectory\",\n\t\t\t\t\"uidNumber\",\n\t\t\t\t\"gidNumber\",\n\t\t\t\t\"nsSshPublicKey\",\n\t\t\t},\n\t\t\tDefaultUID:         0,\n\t\t\tDefaultGID:         0,\n\t\t\tForceDefaultUID:    true,\n\t\t\tForceDefaultGID:    true,\n\t\t\tInsecureSkipVerify: false,\n\t\t\tCACertificates:     nil,\n\t\t},\n\t}\n\tviper.SetEnvPrefix(configEnvPrefix)\n\treplacer := strings.NewReplacer(\".\", \"__\")\n\tviper.SetEnvKeyReplacer(replacer)\n\tviper.SetConfigName(DefaultConfigName)\n\tviper.AutomaticEnv()\n\tviper.AllowEmptyEnv(true)\n}\n\n// GetHomeDirectory returns the configured name for the LDAP field to use as home directory\nfunc (l *LDAPConfig) GetHomeDirectory() string {\n\tif len(l.SearchBaseAttrs) > 1 {\n\t\treturn l.SearchBaseAttrs[1]\n\t}\n\treturn \"homeDirectory\"\n}\n\n// GetUIDNumber returns the configured name for the LDAP field to use as UID\nfunc (l *LDAPConfig) GetUIDNumber() string {\n\tif len(l.SearchBaseAttrs) > 2 {\n\t\treturn l.SearchBaseAttrs[2]\n\t}\n\treturn \"uidNumber\"\n}\n\n// GetGIDNumber returns the configured name for the LDAP field to use as GID\nfunc (l *LDAPConfig) GetGIDNumber() string {\n\tif len(l.SearchBaseAttrs) > 3 {\n\t\treturn l.SearchBaseAttrs[3]\n\t}\n\treturn \"gidNumber\"\n}\n\n// GetPublicKey returns the configured name for the LDAP field to use as public keys\nfunc (l *LDAPConfig) GetPublicKey() string {\n\tif len(l.SearchBaseAttrs) > 4 {\n\t\treturn l.SearchBaseAttrs[4]\n\t}\n\treturn \"nsSshPublicKey\"\n}\n\n// GetHTTPDConfig returns the configuration for the HTTP server\nfunc GetHTTPDConfig() HTTPDConfig {\n\treturn conf.HTTPD\n}\n\n// GetLDAPConfig returns LDAP related settings\nfunc GetLDAPConfig() LDAPConfig {\n\treturn conf.LDAP\n}\n\nfunc getRedactedConf() appConfig {\n\tc := conf\n\treturn c\n}\n\n// LoadConfig loads the configuration\nfunc LoadConfig(configDir, configName string) error {\n\tvar err error\n\tviper.AddConfigPath(configDir)\n\tviper.AddConfigPath(\".\")\n\tviper.SetConfigName(configName)\n\tif err = viper.ReadInConfig(); err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error loading configuration file: %v. Default configuration will be used: %+v\",\n\t\t\terr, getRedactedConf())\n\t\tlogger.WarnToConsole(\"error loading configuration file: %v. Default configuration will be used.\", err)\n\t\treturn err\n\t}\n\terr = viper.Unmarshal(&conf)\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error parsing configuration file: %v. Default configuration will be used: %+v\",\n\t\t\terr, getRedactedConf())\n\t\tlogger.WarnToConsole(\"error parsing configuration file: %v. Default configuration will be used.\", err)\n\t\treturn err\n\t}\n\tlogger.Debug(logSender, \"\", \"config file used: '%q', config loaded: %+v\", viper.ConfigFileUsed(), getRedactedConf())\n\treturn err\n}\n"
  },
  {
    "path": "examples/ldapauthserver/go.mod",
    "content": "module github.com/drakkan/sftpgo/ldapauthserver\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/go-chi/chi/v5 v5.2.5\n\tgithub.com/go-chi/render v1.0.3\n\tgithub.com/go-ldap/ldap/v3 v3.4.13\n\tgithub.com/nathanaelle/password/v2 v2.0.1\n\tgithub.com/rs/zerolog v1.34.0\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/viper v1.21.0\n\tgolang.org/x/crypto v0.49.0\n\tgopkg.in/natefinch/lumberjack.v2 v2.2.1\n)\n\nrequire (\n\tgithub.com/Azure/go-ntlmssp v0.1.0 // indirect\n\tgithub.com/ajg/form v1.7.1 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect\n)\n"
  },
  {
    "path": "examples/ldapauthserver/go.sum",
    "content": "github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=\ngithub.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/ajg/form v1.7.1 h1:OsnBDzTkrWdrxvEnO68I72ZVGJGNaMwPhoAm0V+llgc=\ngithub.com/ajg/form v1.7.1/go.mod h1:HL757PzLyNkj5AIfptT6L+iGNeXTlnrr/oDePGc/y7Q=\ngithub.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=\ngithub.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=\ngithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=\ngithub.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=\ngithub.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=\ngithub.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/nathanaelle/password/v2 v2.0.1 h1:ItoCTdsuIWzilYmllQPa3DR3YoCXcpfxScWLqr8Ii2s=\ngithub.com/nathanaelle/password/v2 v2.0.1/go.mod h1:eaoT+ICQEPNtikBRIAatN8ThWwMhVG+r1jTw60BvPJk=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "examples/ldapauthserver/httpd/auth.go",
    "content": "package httpd\n\nimport (\n\t\"encoding/csv\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\n\tunixcrypt \"github.com/nathanaelle/password/v2\"\n\n\t\"github.com/drakkan/sftpgo/ldapauthserver/logger\"\n\t\"github.com/drakkan/sftpgo/ldapauthserver/utils\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nconst (\n\tauthenticationHeader = \"WWW-Authenticate\"\n\tauthenticationRealm  = \"LDAP Auth Server\"\n\tunauthResponse       = \"Unauthorized\"\n)\n\nvar (\n\tmd5CryptPwdPrefixes = []string{\"$1$\", \"$apr1$\"}\n\tbcryptPwdPrefixes   = []string{\"$2a$\", \"$2$\", \"$2x$\", \"$2y$\", \"$2b$\"}\n)\n\ntype httpAuthProvider interface {\n\tgetHashedPassword(username string) (string, bool)\n\tisEnabled() bool\n}\n\ntype basicAuthProvider struct {\n\tPath string\n\tsync.RWMutex\n\tInfo  os.FileInfo\n\tUsers map[string]string\n}\n\nfunc newBasicAuthProvider(authUserFile string) (httpAuthProvider, error) {\n\tbasicAuthProvider := basicAuthProvider{\n\t\tPath:  authUserFile,\n\t\tInfo:  nil,\n\t\tUsers: make(map[string]string),\n\t}\n\treturn &basicAuthProvider, basicAuthProvider.loadUsers()\n}\n\nfunc (p *basicAuthProvider) isEnabled() bool {\n\treturn len(p.Path) > 0\n}\n\nfunc (p *basicAuthProvider) isReloadNeeded(info os.FileInfo) bool {\n\tp.RLock()\n\tdefer p.RUnlock()\n\treturn p.Info == nil || p.Info.ModTime() != info.ModTime() || p.Info.Size() != info.Size()\n}\n\nfunc (p *basicAuthProvider) loadUsers() error {\n\tif !p.isEnabled() {\n\t\treturn nil\n\t}\n\tinfo, err := os.Stat(p.Path)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to stat basic auth users file: %v\", err)\n\t\treturn err\n\t}\n\tif p.isReloadNeeded(info) {\n\t\tr, err := os.Open(p.Path)\n\t\tif err != nil {\n\t\t\tlogger.Debug(logSender, \"\", \"unable to open basic auth users file: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tdefer r.Close()\n\t\treader := csv.NewReader(r)\n\t\treader.Comma = ':'\n\t\treader.Comment = '#'\n\t\treader.TrimLeadingSpace = true\n\t\trecords, err := reader.ReadAll()\n\t\tif err != nil {\n\t\t\tlogger.Debug(logSender, \"\", \"unable to parse basic auth users file: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tp.Lock()\n\t\tdefer p.Unlock()\n\t\tp.Users = make(map[string]string)\n\t\tfor _, record := range records {\n\t\t\tif len(record) == 2 {\n\t\t\t\tp.Users[record[0]] = record[1]\n\t\t\t}\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"number of users loaded for httpd basic auth: %v\", len(p.Users))\n\t\tp.Info = info\n\t}\n\treturn nil\n}\n\nfunc (p *basicAuthProvider) getHashedPassword(username string) (string, bool) {\n\terr := p.loadUsers()\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\tp.RLock()\n\tdefer p.RUnlock()\n\tpwd, ok := p.Users[username]\n\treturn pwd, ok\n}\n\nfunc checkAuth(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif !validateCredentials(r) {\n\t\t\tw.Header().Set(authenticationHeader, fmt.Sprintf(\"Basic realm=\\\"%v\\\"\", authenticationRealm))\n\t\t\tsendAPIResponse(w, r, errors.New(unauthResponse), \"\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc validateCredentials(r *http.Request) bool {\n\tif !httpAuth.isEnabled() {\n\t\treturn true\n\t}\n\tusername, password, ok := r.BasicAuth()\n\tif !ok {\n\t\treturn false\n\t}\n\tif hashedPwd, ok := httpAuth.getHashedPassword(username); ok {\n\t\tif utils.IsStringPrefixInSlice(hashedPwd, bcryptPwdPrefixes) {\n\t\t\terr := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(password))\n\t\t\treturn err == nil\n\t\t}\n\t\tif utils.IsStringPrefixInSlice(hashedPwd, md5CryptPwdPrefixes) {\n\t\t\tcrypter, ok := unixcrypt.MD5.CrypterFound(hashedPwd)\n\t\t\tif !ok {\n\t\t\t\terr := errors.New(\"cannot found matching MD5 crypter\")\n\t\t\t\tlogger.Debug(logSender, \"\", \"error comparing password with MD5 crypt hash: %v\", err)\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn crypter.Verify([]byte(password))\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "examples/ldapauthserver/httpd/httpd.go",
    "content": "package httpd\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/ldapauthserver/config\"\n\t\"github.com/drakkan/sftpgo/ldapauthserver/logger\"\n\t\"github.com/drakkan/sftpgo/ldapauthserver/utils\"\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/go-chi/render\"\n)\n\nconst (\n\tlogSender      = \"httpd\"\n\tversionPath    = \"/api/v1/version\"\n\tcheckAuthPath  = \"/api/v1/check_auth\"\n\tmaxRequestSize = 1 << 18 // 256KB\n)\n\nvar (\n\tldapConfig config.LDAPConfig\n\thttpAuth   httpAuthProvider\n\tcertMgr    *certManager\n\trootCAs    *x509.CertPool\n)\n\n// StartHTTPServer initializes and starts the HTTP Server\nfunc StartHTTPServer(configDir string, httpConfig config.HTTPDConfig) error {\n\tvar err error\n\tauthUserFile := getConfigPath(httpConfig.AuthUserFile, configDir)\n\thttpAuth, err = newBasicAuthProvider(authUserFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trouter := chi.NewRouter()\n\trouter.Use(middleware.RequestID)\n\trouter.Use(middleware.RealIP)\n\trouter.Use(logger.NewStructuredLogger(logger.GetLogger()))\n\trouter.Use(middleware.Recoverer)\n\n\trouter.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tsendAPIResponse(w, r, nil, \"Not Found\", http.StatusNotFound)\n\t}))\n\n\trouter.MethodNotAllowed(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tsendAPIResponse(w, r, nil, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t}))\n\n\trouter.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {\n\t\trender.JSON(w, r, utils.GetAppVersion())\n\t})\n\n\trouter.Group(func(router chi.Router) {\n\t\trouter.Use(checkAuth)\n\n\t\trouter.Post(checkAuthPath, checkSFTPGoUserAuth)\n\t})\n\n\tldapConfig = config.GetLDAPConfig()\n\tloadCACerts(configDir)\n\n\tcertificateFile := getConfigPath(httpConfig.CertificateFile, configDir)\n\tcertificateKeyFile := getConfigPath(httpConfig.CertificateKeyFile, configDir)\n\n\thttpServer := &http.Server{\n\t\tAddr:           fmt.Sprintf(\"%s:%d\", httpConfig.BindAddress, httpConfig.BindPort),\n\t\tHandler:        router,\n\t\tReadTimeout:    70 * time.Second,\n\t\tWriteTimeout:   70 * time.Second,\n\t\tIdleTimeout:    120 * time.Second,\n\t\tMaxHeaderBytes: 1 << 16, // 64KB\n\t}\n\tif len(certificateFile) > 0 && len(certificateKeyFile) > 0 {\n\t\tcertMgr, err = newCertManager(certificateFile, certificateKeyFile)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfig := &tls.Config{\n\t\t\tGetCertificate: certMgr.GetCertificateFunc(),\n\t\t\tMinVersion:     tls.VersionTLS12,\n\t\t}\n\t\thttpServer.TLSConfig = config\n\t\treturn httpServer.ListenAndServeTLS(\"\", \"\")\n\t}\n\treturn httpServer.ListenAndServe()\n}\n\nfunc sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {\n\tvar errorString string\n\tif err != nil {\n\t\terrorString = err.Error()\n\t}\n\tresp := apiResponse{\n\t\tError:      errorString,\n\t\tMessage:    message,\n\t\tHTTPStatus: code,\n\t}\n\tctx := context.WithValue(r.Context(), render.StatusCtxKey, code)\n\trender.JSON(w, r.WithContext(ctx), resp)\n}\n\nfunc loadCACerts(configDir string) error {\n\tvar err error\n\trootCAs, err = x509.SystemCertPool()\n\tif err != nil {\n\t\trootCAs = x509.NewCertPool()\n\t}\n\tfor _, ca := range ldapConfig.CACertificates {\n\t\tcaPath := getConfigPath(ca, configDir)\n\t\tcerts, err := os.ReadFile(caPath)\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"error loading ca cert %q: %v\", caPath, err)\n\t\t\treturn err\n\t\t}\n\t\tif !rootCAs.AppendCertsFromPEM(certs) {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to add ca cert %q\", caPath)\n\t\t} else {\n\t\t\tlogger.Debug(logSender, \"\", \"ca cert %q added to the trusted certificates\", caPath)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ReloadTLSCertificate reloads the TLS certificate and key from the configured paths\nfunc ReloadTLSCertificate() {\n\tif certMgr != nil {\n\t\tcertMgr.loadCertificate()\n\t}\n}\n\nfunc getConfigPath(name, configDir string) string {\n\tif !utils.IsFileInputValid(name) {\n\t\treturn \"\"\n\t}\n\tif len(name) > 0 && !filepath.IsAbs(name) {\n\t\treturn filepath.Join(configDir, name)\n\t}\n\treturn name\n}\n"
  },
  {
    "path": "examples/ldapauthserver/httpd/ldapauth.go",
    "content": "package httpd\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/drakkan/sftpgo/ldapauthserver/logger\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/go-chi/render\"\n\t\"github.com/go-ldap/ldap/v3\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc getSFTPGoUser(entry *ldap.Entry, username string) (SFTPGoUser, error) {\n\tvar err error\n\tvar user SFTPGoUser\n\tuid := ldapConfig.DefaultUID\n\tgid := ldapConfig.DefaultGID\n\tstatus := 1\n\n\tif !ldapConfig.ForceDefaultUID {\n\t\tuid, err = strconv.Atoi(entry.GetAttributeValue(ldapConfig.GetUIDNumber()))\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t}\n\n\tif !ldapConfig.ForceDefaultGID {\n\t\tuid, err = strconv.Atoi(entry.GetAttributeValue(ldapConfig.GetGIDNumber()))\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t}\n\n\tsftpgoUser := SFTPGoUser{\n\t\tUsername: username,\n\t\tHomeDir:  entry.GetAttributeValue(ldapConfig.GetHomeDirectory()),\n\t\tUID:      uid,\n\t\tGID:      gid,\n\t\tStatus:   status,\n\t}\n\tsftpgoUser.Permissions = make(map[string][]string)\n\tsftpgoUser.Permissions[\"/\"] = []string{\"*\"}\n\treturn sftpgoUser, nil\n}\n\nfunc checkSFTPGoUserAuth(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvar authReq externalAuthRequest\n\terr := render.DecodeJSON(r.Body, &authReq)\n\tif err != nil {\n\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"error decoding auth request: %v\", err)\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tl, err := ldap.DialURL(ldapConfig.BindURL, ldap.DialWithTLSConfig(&tls.Config{\n\t\tInsecureSkipVerify: ldapConfig.InsecureSkipVerify,\n\t\tRootCAs:            rootCAs,\n\t}))\n\tif err != nil {\n\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"error connecting to the LDAP server: %v\", err)\n\t\tsendAPIResponse(w, r, err, \"Error connecting to the LDAP server\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer l.Close()\n\n\terr = l.Bind(ldapConfig.BindUsername, ldapConfig.BindPassword)\n\tif err != nil {\n\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"error binding to the LDAP server: %v\", err)\n\t\tsendAPIResponse(w, r, err, \"Error binding to the LDAP server\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tsearchRequest := ldap.NewSearchRequest(\n\t\tldapConfig.BaseDN,\n\t\tldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,\n\t\tstrings.Replace(ldapConfig.SearchFilter, \"%s\", ldap.EscapeFilter(authReq.Username), 1),\n\t\tldapConfig.SearchBaseAttrs,\n\t\tnil,\n\t)\n\n\tsr, err := l.Search(searchRequest)\n\tif err != nil {\n\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"error searching LDAP user %q: %v\", authReq.Username, err)\n\t\tsendAPIResponse(w, r, err, \"Error searching LDAP user\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif len(sr.Entries) != 1 {\n\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"expected one user, found: %v\", len(sr.Entries))\n\t\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"Expected one user, found: %v\", len(sr.Entries)), http.StatusNotFound)\n\t\treturn\n\t}\n\n\tif len(authReq.PublicKey) > 0 {\n\t\tuserKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authReq.PublicKey))\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"invalid public key for user %q: %v\", authReq.Username, err)\n\t\t\tsendAPIResponse(w, r, err, \"Invalid public key\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tauthOk := false\n\t\tfor _, k := range sr.Entries[0].GetAttributeValues(ldapConfig.GetPublicKey()) {\n\t\t\tkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))\n\t\t\t// we skip an invalid public key stored inside the LDAP server\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif bytes.Equal(key.Marshal(), userKey.Marshal()) {\n\t\t\t\tauthOk = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !authOk {\n\t\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"public key authentication failed for user: %q\", authReq.Username)\n\t\t\tsendAPIResponse(w, r, nil, \"public key authentication failed\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\t// bind to the LDAP server with the user dn and the given password to check the password\n\t\tuserdn := sr.Entries[0].DN\n\t\terr = l.Bind(userdn, authReq.Password)\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"password authentication failed for user: %q\", authReq.Username)\n\t\t\tsendAPIResponse(w, r, nil, \"password authentication failed\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t}\n\n\tuser, err := getSFTPGoUser(sr.Entries[0], authReq.Username)\n\tif err != nil {\n\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"get user from LDAP entry failed for username %q: %v\",\n\t\t\tauthReq.Username, err)\n\t\tsendAPIResponse(w, r, err, \"mapping LDAP user failed\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\trender.JSON(w, r, user)\n}\n"
  },
  {
    "path": "examples/ldapauthserver/httpd/models.go",
    "content": "package httpd\n\ntype apiResponse struct {\n\tError      string `json:\"error\"`\n\tMessage    string `json:\"message\"`\n\tHTTPStatus int    `json:\"status\"`\n}\n\ntype externalAuthRequest struct {\n\tUsername  string `json:\"username\"`\n\tPassword  string `json:\"password\"`\n\tPublicKey string `json:\"public_key\"`\n}\n\n// SFTPGoExtensionsFilter defines filters based on file extensions\ntype SFTPGoExtensionsFilter struct {\n\tPath              string   `json:\"path\"`\n\tAllowedExtensions []string `json:\"allowed_extensions,omitempty\"`\n\tDeniedExtensions  []string `json:\"denied_extensions,omitempty\"`\n}\n\n// SFTPGoUserFilters defines additional restrictions for an SFTPGo user\ntype SFTPGoUserFilters struct {\n\tAllowedIP          []string                 `json:\"allowed_ip,omitempty\"`\n\tDeniedIP           []string                 `json:\"denied_ip,omitempty\"`\n\tDeniedLoginMethods []string                 `json:\"denied_login_methods,omitempty\"`\n\tFileExtensions     []SFTPGoExtensionsFilter `json:\"file_extensions,omitempty\"`\n}\n\n// S3FsConfig defines the configuration for S3 based filesystem\ntype S3FsConfig struct {\n\tBucket            string `json:\"bucket,omitempty\"`\n\tKeyPrefix         string `json:\"key_prefix,omitempty\"`\n\tRegion            string `json:\"region,omitempty\"`\n\tAccessKey         string `json:\"access_key,omitempty\"`\n\tAccessSecret      string `json:\"access_secret,omitempty\"`\n\tEndpoint          string `json:\"endpoint,omitempty\"`\n\tStorageClass      string `json:\"storage_class,omitempty\"`\n\tUploadPartSize    int64  `json:\"upload_part_size,omitempty\"`\n\tUploadConcurrency int    `json:\"upload_concurrency,omitempty\"`\n}\n\n// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem\ntype GCSFsConfig struct {\n\tBucket               string `json:\"bucket,omitempty\"`\n\tKeyPrefix            string `json:\"key_prefix,omitempty\"`\n\tCredentials          string `json:\"credentials,omitempty\"`\n\tAutomaticCredentials int    `json:\"automatic_credentials,omitempty\"`\n\tStorageClass         string `json:\"storage_class,omitempty\"`\n}\n\n// SFTPGoFilesystem defines cloud storage filesystem details\ntype SFTPGoFilesystem struct {\n\t// 0 local filesystem, 1 AWS S3 compatible, 2 Google Cloud Storage\n\tProvider  int         `json:\"provider\"`\n\tS3Config  S3FsConfig  `json:\"s3config,omitempty\"`\n\tGCSConfig GCSFsConfig `json:\"gcsconfig,omitempty\"`\n}\n\ntype virtualFolder struct {\n\tVirtualPath string `json:\"virtual_path\"`\n\tMappedPath  string `json:\"mapped_path\"`\n}\n\n// SFTPGoUser defines an SFTPGo user\ntype SFTPGoUser struct {\n\t// Database unique identifier\n\tID int64 `json:\"id\"`\n\t// 1 enabled, 0 disabled (login is not allowed)\n\tStatus int `json:\"status\"`\n\t// Username\n\tUsername string `json:\"username\"`\n\t// Account expiration date as unix timestamp in milliseconds. An expired account cannot login.\n\t// 0 means no expiration\n\tExpirationDate int64    `json:\"expiration_date\"`\n\tPassword       string   `json:\"password,omitempty\"`\n\tPublicKeys     []string `json:\"public_keys,omitempty\"`\n\tHomeDir        string   `json:\"home_dir\"`\n\t// Mapping between virtual paths and filesystem paths outside the home directory. Supported for local filesystem only\n\tVirtualFolders []virtualFolder `json:\"virtual_folders,omitempty\"`\n\t// If sftpgo runs as root system user then the created files and directories will be assigned to this system UID\n\tUID int `json:\"uid\"`\n\t// If sftpgo runs as root system user then the created files and directories will be assigned to this system GID\n\tGID int `json:\"gid\"`\n\t// Maximum concurrent sessions. 0 means unlimited\n\tMaxSessions int `json:\"max_sessions\"`\n\t// Maximum size allowed as bytes. 0 means unlimited\n\tQuotaSize int64 `json:\"quota_size\"`\n\t// Maximum number of files allowed. 0 means unlimited\n\tQuotaFiles int `json:\"quota_files\"`\n\t// List of the granted permissions\n\tPermissions map[string][]string `json:\"permissions\"`\n\t// Used quota as bytes\n\tUsedQuotaSize int64 `json:\"used_quota_size\"`\n\t// Used quota as number of files\n\tUsedQuotaFiles int `json:\"used_quota_files\"`\n\t// Last quota update as unix timestamp in milliseconds\n\tLastQuotaUpdate int64 `json:\"last_quota_update\"`\n\t// Maximum upload bandwidth as KB/s, 0 means unlimited\n\tUploadBandwidth int64 `json:\"upload_bandwidth\"`\n\t// Maximum download bandwidth as KB/s, 0 means unlimited\n\tDownloadBandwidth int64 `json:\"download_bandwidth\"`\n\t// Last login as unix timestamp in milliseconds\n\tLastLogin int64 `json:\"last_login\"`\n\t// Additional restrictions\n\tFilters SFTPGoUserFilters `json:\"filters\"`\n\t// Filesystem configuration details\n\tFsConfig SFTPGoFilesystem `json:\"filesystem\"`\n}\n"
  },
  {
    "path": "examples/ldapauthserver/httpd/tlsutils.go",
    "content": "package httpd\n\nimport (\n\t\"crypto/tls\"\n\t\"sync\"\n\n\t\"github.com/drakkan/sftpgo/ldapauthserver/logger\"\n)\n\ntype certManager struct {\n\tcertPath string\n\tkeyPath  string\n\tsync.RWMutex\n\tcert *tls.Certificate\n}\n\nfunc (m *certManager) loadCertificate() error {\n\tnewCert, err := tls.LoadX509KeyPair(m.certPath, m.keyPath)\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to load https certificate: %v\", err)\n\t\treturn err\n\t}\n\tlogger.Debug(logSender, \"\", \"https certificate successfully loaded\")\n\tm.Lock()\n\tdefer m.Unlock()\n\tm.cert = &newCert\n\treturn nil\n}\n\nfunc (m *certManager) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {\n\treturn func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\t\tm.RLock()\n\t\tdefer m.RUnlock()\n\t\treturn m.cert, nil\n\t}\n}\n\nfunc newCertManager(certificateFile, certificateKeyFile string) (*certManager, error) {\n\tmanager := &certManager{\n\t\tcert:     nil,\n\t\tcertPath: certificateFile,\n\t\tkeyPath:  certificateKeyFile,\n\t}\n\terr := manager.loadCertificate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn manager, nil\n}\n"
  },
  {
    "path": "examples/ldapauthserver/ldapauth.toml",
    "content": "[httpd]\nbind_address = \"\"\nbind_port = 9000\n# Path to a file used to store usernames and passwords for basic authentication. It can be generated using the Apache htpasswd tool\nauth_user_file = \"\"\n# If both the certificate and the private key are provided, the server will expect HTTPS connections\ncertificate_file = \"\"\ncertificate_key_file = \"\"\n\n[ldap]\nbasedn = \"dc=example,dc=com\"\nbind_url = \"ldap://127.0.0.1:389\"\nbind_username = \"cn=Directory Manager\"\nbind_password = \"YOUR_ADMIN_PASSWORD_HERE\"\nsearch_filter = \"(&(objectClass=nsPerson)(uid=%s))\"\n# you can change the name of the search base attributes to adapt them to your schema but the order must remain the same\nsearch_base_attrs = [\n    \"dn\",\n    \"homeDirectory\",\n    \"uidNumber\",\n    \"gidNumber\",\n    \"nsSshPublicKey\"\n]\ndefault_uid = 0\ndefault_gid = 0\nforce_default_uid = true\nforce_default_gid = true\n# if true, ldaps accepts any certificate presented by the LDAP server and any host name in that certificate.\n# This should be used only for testing\ninsecure_skip_verify = false\n# list of root CA to use for ldaps connections\n# If you use a self signed certificate is better to add the root CA to this list than set insecure_skip_verify to true\nca_certificates = []\n"
  },
  {
    "path": "examples/ldapauthserver/logger/logger.go",
    "content": "package logger\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/rs/zerolog\"\n\tlumberjack \"gopkg.in/natefinch/lumberjack.v2\"\n)\n\nconst (\n\tdateFormat = \"2006-01-02T15:04:05.000\" // YYYY-MM-DDTHH:MM:SS.ZZZ\n)\n\nvar (\n\tlogger        zerolog.Logger\n\tconsoleLogger zerolog.Logger\n)\n\n// GetLogger get the configured logger instance\nfunc GetLogger() *zerolog.Logger {\n\treturn &logger\n}\n\n// InitLogger initialize loggers\nfunc InitLogger(logFilePath string, logMaxSize, logMaxBackups, logMaxAge int, logCompress bool, level zerolog.Level) {\n\tzerolog.TimeFieldFormat = dateFormat\n\tif isLogFilePathValid(logFilePath) {\n\t\tlogger = zerolog.New(&lumberjack.Logger{\n\t\t\tFilename:   logFilePath,\n\t\t\tMaxSize:    logMaxSize,\n\t\t\tMaxBackups: logMaxBackups,\n\t\t\tMaxAge:     logMaxAge,\n\t\t\tCompress:   logCompress,\n\t\t})\n\t\tEnableConsoleLogger(level)\n\t} else {\n\t\tlogger = zerolog.New(&logSyncWrapper{\n\t\t\toutput: os.Stdout,\n\t\t})\n\t\tconsoleLogger = zerolog.Nop()\n\t}\n\tlogger.Level(level)\n}\n\n// DisableLogger disable the main logger.\n// ConsoleLogger will not be affected\nfunc DisableLogger() {\n\tlogger = zerolog.Nop()\n}\n\n// EnableConsoleLogger enables the console logger\nfunc EnableConsoleLogger(level zerolog.Level) {\n\tconsoleOutput := zerolog.ConsoleWriter{\n\t\tOut:        os.Stdout,\n\t\tTimeFormat: dateFormat,\n\t\tNoColor:    runtime.GOOS == \"windows\",\n\t}\n\tconsoleLogger = zerolog.New(consoleOutput).With().Timestamp().Logger().Level(level)\n}\n\n// Debug logs at debug level for the specified sender\nfunc Debug(prefix, requestID string, format string, v ...interface{}) {\n\tlogger.Debug().\n\t\tTimestamp().\n\t\tStr(\"sender\", prefix).\n\t\tStr(\"request_id\", requestID).\n\t\tMsg(fmt.Sprintf(format, v...))\n}\n\n// Info logs at info level for the specified sender\nfunc Info(prefix, requestID string, format string, v ...interface{}) {\n\tlogger.Info().\n\t\tTimestamp().\n\t\tStr(\"sender\", prefix).\n\t\tStr(\"request_id\", requestID).\n\t\tMsg(fmt.Sprintf(format, v...))\n}\n\n// Warn logs at warn level for the specified sender\nfunc Warn(prefix, requestID string, format string, v ...interface{}) {\n\tlogger.Warn().\n\t\tTimestamp().\n\t\tStr(\"sender\", prefix).\n\t\tStr(\"request_id\", requestID).\n\t\tMsg(fmt.Sprintf(format, v...))\n}\n\n// Error logs at error level for the specified sender\nfunc Error(prefix, requestID string, format string, v ...interface{}) {\n\tlogger.Error().\n\t\tTimestamp().\n\t\tStr(\"sender\", prefix).\n\t\tStr(\"request_id\", requestID).\n\t\tMsg(fmt.Sprintf(format, v...))\n}\n\n// DebugToConsole logs at debug level to stdout\nfunc DebugToConsole(format string, v ...interface{}) {\n\tconsoleLogger.Debug().Msg(fmt.Sprintf(format, v...))\n}\n\n// InfoToConsole logs at info level to stdout\nfunc InfoToConsole(format string, v ...interface{}) {\n\tconsoleLogger.Info().Msg(fmt.Sprintf(format, v...))\n}\n\n// WarnToConsole logs at info level to stdout\nfunc WarnToConsole(format string, v ...interface{}) {\n\tconsoleLogger.Warn().Msg(fmt.Sprintf(format, v...))\n}\n\n// ErrorToConsole logs at error level to stdout\nfunc ErrorToConsole(format string, v ...interface{}) {\n\tconsoleLogger.Error().Msg(fmt.Sprintf(format, v...))\n}\n\nfunc isLogFilePathValid(logFilePath string) bool {\n\tcleanInput := filepath.Clean(logFilePath)\n\tif cleanInput == \".\" || cleanInput == \"..\" {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "examples/ldapauthserver/logger/request_logger.go",
    "content": "package logger\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/rs/zerolog\"\n)\n\n// StructuredLogger defines a simple wrapper around zerolog logger.\n// It implements chi.middleware.LogFormatter interface\ntype StructuredLogger struct {\n\tLogger *zerolog.Logger\n}\n\n// StructuredLoggerEntry ...\ntype StructuredLoggerEntry struct {\n\tLogger *zerolog.Logger\n\tfields map[string]interface{}\n}\n\n// NewStructuredLogger returns a chi.middleware.RequestLogger using our StructuredLogger.\n// This structured logger is called by the chi.middleware.Logger handler to log each HTTP request\nfunc NewStructuredLogger(logger *zerolog.Logger) func(next http.Handler) http.Handler {\n\treturn middleware.RequestLogger(&StructuredLogger{logger})\n}\n\n// NewLogEntry creates a new log entry for an HTTP request\nfunc (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {\n\tscheme := \"http\"\n\tif r.TLS != nil {\n\t\tscheme = \"https\"\n\t}\n\n\tfields := map[string]interface{}{\n\t\t\"remote_addr\": r.RemoteAddr,\n\t\t\"proto\":       r.Proto,\n\t\t\"method\":      r.Method,\n\t\t\"user_agent\":  r.UserAgent(),\n\t\t\"uri\":         fmt.Sprintf(\"%s://%s%s\", scheme, r.Host, r.RequestURI)}\n\n\treqID := middleware.GetReqID(r.Context())\n\tif reqID != \"\" {\n\t\tfields[\"request_id\"] = reqID\n\t}\n\n\treturn &StructuredLoggerEntry{Logger: l.Logger, fields: fields}\n}\n\n// Write logs a new entry at the end of the HTTP request\nfunc (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {\n\tl.Logger.Info().\n\t\tTimestamp().\n\t\tStr(\"sender\", \"httpd\").\n\t\tFields(l.fields).\n\t\tInt(\"resp_status\", status).\n\t\tInt(\"resp_size\", bytes).\n\t\tInt64(\"elapsed_ms\", elapsed.Nanoseconds()/1000000).\n\t\tSend()\n}\n\n// Panic logs panics\nfunc (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {\n\tl.Logger.Error().\n\t\tTimestamp().\n\t\tStr(\"sender\", \"httpd\").\n\t\tFields(l.fields).\n\t\tStr(\"stack\", string(stack)).\n\t\tStr(\"panic\", fmt.Sprintf(\"%+v\", v)).\n\t\tSend()\n}\n"
  },
  {
    "path": "examples/ldapauthserver/logger/sync_wrapper.go",
    "content": "package logger\n\nimport (\n\t\"os\"\n\t\"sync\"\n)\n\ntype logSyncWrapper struct {\n\tsync.Mutex\n\toutput *os.File\n}\n\nfunc (l *logSyncWrapper) Write(b []byte) (n int, err error) {\n\tl.Lock()\n\tdefer l.Unlock()\n\treturn l.output.Write(b)\n}\n"
  },
  {
    "path": "examples/ldapauthserver/main.go",
    "content": "package main\n\nimport \"github.com/drakkan/sftpgo/ldapauthserver/cmd\"\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "examples/ldapauthserver/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// IsFileInputValid returns true this is a valid file name.\n// This method must be used before joining a file name, generally provided as\n// user input, with a directory\nfunc IsFileInputValid(fileInput string) bool {\n\tcleanInput := filepath.Clean(fileInput)\n\tif cleanInput == \".\" || cleanInput == \"..\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// IsStringPrefixInSlice searches a string prefix in a slice and returns true\n// if a matching prefix is found\nfunc IsStringPrefixInSlice(obj string, list []string) bool {\n\tfor _, v := range list {\n\t\tif strings.HasPrefix(obj, v) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "examples/ldapauthserver/utils/version.go",
    "content": "package utils\n\nconst version = \"0.1.0-dev\"\n\nvar (\n\tcommit      = \"\"\n\tdate        = \"\"\n\tversionInfo VersionInfo\n)\n\n// VersionInfo defines version details\ntype VersionInfo struct {\n\tVersion    string `json:\"version\"`\n\tBuildDate  string `json:\"build_date\"`\n\tCommitHash string `json:\"commit_hash\"`\n}\n\nfunc init() {\n\tversionInfo = VersionInfo{\n\t\tVersion:    version,\n\t\tCommitHash: commit,\n\t\tBuildDate:  date,\n\t}\n}\n\n// GetVersionAsString returns the string representation of the VersionInfo struct\nfunc (v *VersionInfo) GetVersionAsString() string {\n\tversionString := v.Version\n\tif len(v.CommitHash) > 0 {\n\t\tversionString += \"-\" + v.CommitHash\n\t}\n\tif len(v.BuildDate) > 0 {\n\t\tversionString += \"-\" + v.BuildDate\n\t}\n\treturn versionString\n}\n\n// GetAppVersion returns VersionInfo struct\nfunc GetAppVersion() VersionInfo {\n\treturn versionInfo\n}\n"
  },
  {
    "path": "examples/php-activedirectory-http-server/README.md",
    "content": "# SFTPGo on Windows with Active Directory Integration + Caddy Static File Server Example\n\n[![SFTPGo on Windows with Active Directory Integration + Caddy Static File Server Example](https://img.youtube.com/vi/M5UcJI8t4AI/0.jpg)](https://www.youtube.com/watch?v=M5UcJI8t4AI)\n\nThis is similar to the ldapauthserver example, but is more specific to using Active Directory along with using SFTPGo on a Windows Server.\n\nThe Youtube Walkthrough/Tutorial video above goes into considerable more detail, but in short, it walks through setting up SFTPGo on a new Windows Server, and enables the External Authentication feature within SFTPGo, along with my `sftpgo-ldap-http-server` project, to allow for user authentication into SFTPGo to occur through one or more Active Directory connections.\n\nAdditionally, I go through using the Caddy web server, to help enable serving of static files, if this is something that would be of interest for you.\n\nTo get started, you'll want to download the latest release ZIP package from the [sftpgo-ldap-http-server repository](https://github.com/orware/sftpgo-ldap-http-server).\n\nThe ZIP itself contains the `sftpgo-ldap-http-server.exe` file, along with an `OpenLDAP` folder (mainly to help if you want to use TLS for your LDAP connections), and a `Data` which contains a logs folder, a configuration.example.php file, a functions.php file, and the LICENSE and README files.\n\nThe video above goes through the whole process, but to get started you'll want to install SFTPGo on your server, and then extract the `sftpgo-ldap-http-server` ZIP file on the server as well into a separate folder. Then you'll want to copy the configuration.example.php file and name it `configuration.php` and begin customizing the settings (e.g. add in your own LDAP settings, along with how you may want to have your folders be created). At the very minimum you'll want to make sure that the home directories are set correctly to how you want the folders to be created for your environment (you don't have to use the virtual folders or really any of the other functionality if you don't need it).\n\nOnce configured, from a command prompt window, if you are already in the same folder as where you extracted the `sftpgo-ldap-http-server` ZIP, you may simply call the `sftpgo-ldap-http-server.exe` and it should start up a simple HTTP server on Port 9001 running on localhost (the port can be adjusted via the `configuration.php` file as well). Now all you have to do is point SFTPGo's `external_auth_hook` option to point to `http://localhost:9001/` and you should be able to run some authentication tests (assuming you have all of your settings correct and there are no intermediate issues).\n\nThe video above definitely goes through some troubleshooting situations you might find yourself coming across, so while it is long (at about 1 hour, 42 minutes), it may be helpful to review and avoid some issues and just to learn a bit more about SFTPGo and the integration above.\n\n## Example Virtual Folders Configuration (Allowing for Both a Public and Private Folder)\n\nThe following can be utilized if you'd like to assign your users both a Private Virtual Folder and Public Virtual Folder.\n\nBy itself, the Public Virtual Folder isn't necessarily public, so keep that in mind. Only by combining things together with the Caddy web server (and Caddyfile example configuration down below) can you be successful in making the `F:\\files\\public` folder from the example public.\n\n```php\n$virtual_folders['example'] = [\n    [\n      //\"id\" => 0,\n      \"name\" => \"private-#USERNAME#\",\n      \"mapped_path\" => 'F:\\files\\private\\#USERNAME#',\n      //\"used_quota_size\" => 0,\n      //\"used_quota_files\" => 0,\n      //\"last_quota_update\" => 0,\n      \"virtual_path\" => \"/_private\",\n      \"quota_size\" => -1,\n      \"quota_files\" => -1\n    ],\n\t[\n      //\"id\" => 0,\n      \"name\" => \"public-#USERNAME#\",\n      \"mapped_path\" => 'F:\\files\\public\\#USERNAME#',\n      //\"used_quota_size\" => 0,\n      //\"used_quota_files\" => 0,\n      //\"last_quota_update\" => 0,\n      \"virtual_path\" => \"/_public\",\n      \"quota_size\" => -1,\n      \"quota_files\" => -1\n    ]\n];\n```\n\n## Example Connection \"Output Object\" Allowing For No Files in the User's Home Directory (\"Root Directory\") but Allowing for Files in the Public/Private Virtual Folders\n\nThe magic here happens in the \"permissions\" value, by limiting the root/home directory to just the list/download permissions, and then allowing all permissions on the Public/Private virtual folders.\n\n```php\n$connection_output_objects['example'] = [\n    'status' => 1,\n    'username' => '',\n    'expiration_date' => 0,\n    'home_dir' => '',\n    'uid' => 0,\n    'gid' => 0,\n    'max_sessions' => 0,\n    'quota_size' => 0,\n    'quota_files' => 100000,\n    'permissions' => [\n        \"/\" => [\"list\", \"download\"],\n        \"/_private\" => [\"*\"],\n        \"/_public\" => [\"*\"],\n    ],\n    'upload_bandwidth' => 0,\n    'download_bandwidth' => 0,\n    'filters' => [\n        'allowed_ip' => [],\n        'denied_ip' => [],\n    ],\n    'public_keys' => [],\n];\n```\n\n## Recommended Usage of Automatic Groups Mode (Limiting by Group Prefix)\n\nThe `sftpgo-ldap-http-server` project is able to automatically create virtual folders for any groups your user is a memberof if the automatic mode is turned on. However, by having a specific set of allowed prefixes defined, you can limit things to just those groups that begin with the prefixes you've listed, which can be helpful. The prefix itself will be removed from the group name when added as a virtual folder for the user.\n\n```php\n// If automatic groups mode is disabled, then you have to manually add the allowed groups into $allowed_groups down below:\n// If enabled, then any groups you are a memberof will automatically be added in using the template below.\n$auto_groups_mode = true;\n\n$auto_groups_mode_virtual_folder_template = [\n    [\n      //\"id\" => 0,\n      \"name\" => \"groups-#GROUP#\",\n      \"mapped_path\" => 'F:\\files\\groups\\#GROUP#',\n      //\"used_quota_size\" => 0,\n      //\"used_quota_files\" => 0,\n      //\"last_quota_update\" => 0,\n      \"virtual_path\" => \"/groups/#GROUP#\",\n      \"quota_size\" => 0,\n      \"quota_files\" => 100000\n    ]\n];\n\n// Used only when auto groups mode is enabled and will help prevent all your groups from being\n// added into SFTPGo since only groups with the prefixes defined here will be automatically added\n// with prefixes automatically removed when listed as a virtual folder (e.g. a group with name\n// \"sftpgo-example\" would simply become \"example\").\n$allowed_group_prefixes = [\n    'sftpgo-'\n];\n```\n\n## Example Caddyfile Configuration You Can Adapt for Your Needs\n\n```shell\n### Re-usable snippets:\n\n(add_static_file_serving_features) {\n\n\t# Allow accessing files without requiring .html:\n\ttry_files {path} {path}.html\n\n\t# Enable Static File Server and Directory Browsing:\n\tfile_server browse\n\n\t# Enable templating functionality:\n\ttemplates\n\n\t# Enable Compression for Output:\n\tencode zstd gzip\n\n\thandle_errors {\n\t\trespond \"<pre>{http.error.status_code} {http.error.status_text}</pre>\"\n\t}\n}\n\n(add_hsts_headers) {\n\theader {\n\t\t# Enable HTTP Strict Transport Security (HSTS) to force clients to always\n\n\t\t# connect via HTTPS (do not use if only testing)\n\t\tStrict-Transport-Security \"max-age=31536000; includeSubDomains\"\n\n\t\t# Enable cross-site filter (XSS) and tell browser to block detected attacks\n\t\tX-XSS-Protection \"1; mode=block\"\n\n\t\t# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type\n\t\tX-Content-Type-Options \"nosniff\"\n\n\t\t# Disallow the site to be rendered within a frame (clickjacking protection)\n\t\tX-Frame-Options \"DENY\"\n\n\t\t# keep referrer data off of HTTP connections\n\t\tReferrer-Policy no-referrer-when-downgrade\n\t}\n}\n\n(add_logging_with_path) {\n\tlog {\n\t\toutput file \"{args.0}\" {\n\t\t\troll_size 100mb\n\t\t\troll_keep 5\n\t\t\troll_keep_for 720h\n\t\t}\n\n\t\tformat json\n\t\t#format console\n\t\t#format single_field common_log\n\t}\n}\n\n### Site Definitions:\n\npublic.example.com {\n\n\t# Site Root:\n\troot * F:\\files\\public\n\n\timport add_logging_with_path \"F:\\caddy\\logs\\public_example_com_access.log\"\n\timport add_static_file_serving_features\n\timport add_hsts_headers\n}\n\n\n### Reverse Proxy Definitions:\n\nwebdav.example.com {\n\treverse_proxy localhost:9000\n\n\timport add_logging_with_path \"F:\\caddy\\logs\\webdav_example_com_access.log\"\n}\n```\n"
  },
  {
    "path": "examples/quotascan/README.md",
    "content": "# Update user quota\n\n:warning: Since v2.4.0 you can use the [EventManager](https://docs.sftpgo.com/latest/eventmanager/) to schedule quota scans.\n\nThe `scanuserquota` example script shows how to use the SFTPGo REST API to update the users' quota.\n\nThe stored quota may be incorrect for several reasons, such as an unexpected shutdown while uploading files, temporary provider failures, files copied outside of SFTPGo, and so on.\n\nA quota scan updates the number of files and their total size for the specified user and the virtual folders, if any, included in his quota.\n\nIf you want to track quotas, a scheduled quota scan is recommended. You can use this example as a starting point.\n\nThe script is written in Python and has the following requirements:\n\n- python3 or python2\n- python [Requests](https://requests.readthedocs.io/en/master/) module\n\nThe provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:\n\n- username: `admin`\n- password: `password`\n\nPlease edit the script according to your needs.\n"
  },
  {
    "path": "examples/quotascan/scanuserquota",
    "content": "#!/usr/bin/env python\n\nfrom datetime import datetime\nimport sys\nimport time\n\nimport pytz\nimport requests\n\ntry:\n\timport urllib.parse as urlparse\nexcept ImportError:\n\timport urlparse\n\n# change base_url to point to your SFTPGo installation\nbase_url = \"http://127.0.0.1:8080\"\n# set to False if you want to skip TLS certificate validation\nverify_tls_cert = True\n# set the credentials for a valid admin here\nadmin_user = \"admin\"\nadmin_password = \"password\"\n\n\n# set your update conditions here\ndef needQuotaUpdate(user):\n\tif user[\"status\"] == 0:  # inactive user\n\t\treturn False\n\tif user[\"quota_size\"] == 0 and user[\"quota_files\"] == 0:  # no quota restrictions\n\t\treturn False\n\treturn True\n\n\nclass UpdateQuota:\n\n\tdef __init__(self):\n\t\tself.limit = 100\n\t\tself.offset = 0\n\t\tself.access_token = \"\"\n\t\tself.access_token_expiration = None\n\n\tdef printLog(self, message):\n\t\tprint(\"{} - {}\".format(datetime.now(), message))\n\n\tdef checkAccessToken(self):\n\t\tif self.access_token != \"\" and self.access_token_expiration:\n\t\t\texpire_diff = self.access_token_expiration - datetime.now(tz=pytz.UTC)\n\t\t\t# we don't use total_seconds to be python 2 compatible\n\t\t\tseconds_to_expire = expire_diff.days * 86400 + expire_diff.seconds\n\t\t\tif seconds_to_expire > 180:\n\t\t\t\treturn\n\n\t\tauth = requests.auth.HTTPBasicAuth(admin_user, admin_password)\n\t\tr = requests.get(urlparse.urljoin(base_url, \"api/v2/token\"), auth=auth, verify=verify_tls_cert, timeout=10)\n\t\tif r.status_code != 200:\n\t\t\tself.printLog(\"error getting access token: {}\".format(r.text))\n\t\t\tsys.exit(1)\n\t\tself.access_token = r.json()[\"access_token\"]\n\t\tself.access_token_expiration = pytz.timezone(\"UTC\").localize(datetime.strptime(r.json()[\"expires_at\"],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"%Y-%m-%dT%H:%M:%SZ\"))\n\n\tdef getAuthHeader(self):\n\t\tself.checkAccessToken()\n\t\treturn {\"Authorization\": \"Bearer \" + self.access_token}\n\n\tdef waitForQuotaUpdate(self, username):\n\t\twhile True:\n\t\t\tauth_header = self.getAuthHeader()\n\t\t\tr = requests.get(urlparse.urljoin(base_url, \"api/v2/quotas/users/scans\"), headers=auth_header, verify=verify_tls_cert,\n\t\t\t\t\t\t\ttimeout=10)\n\t\t\tif r.status_code != 200:\n\t\t\t\tself.printLog(\"error getting quota scans while waiting for {}: {}\".format(username, r.text))\n\t\t\t\tsys.exit(1)\n\n\t\t\tscanning = False\n\t\t\tfor scan in r.json():\n\t\t\t\tif scan[\"username\"] == username:\n\t\t\t\t\tscanning = True\n\t\t\tif not scanning:\n\t\t\t\tbreak\n\t\t\tself.printLog(\"waiting for the quota scan to complete for user {}\".format(username))\n\t\t\ttime.sleep(2)\n\n\t\tself.printLog(\"quota update for user {} finished\".format(username))\n\n\tdef updateUserQuota(self, username):\n\t\tself.printLog(\"starting quota update for user {}\".format(username))\n\t\tauth_header = self.getAuthHeader()\n\t\tr = requests.post(urlparse.urljoin(base_url, \"api/v2/quotas/users/\" + username + \"/scan\"), headers=auth_header,\n\t\t\t\t\t\t verify=verify_tls_cert, timeout=10)\n\t\tif r.status_code != 202:\n\t\t\tself.printLog(\"error starting quota scan for user {}: {}\".format(username, r.text))\n\t\t\tsys.exit(1)\n\t\tself.waitForQuotaUpdate(username)\n\n\tdef updateUsersQuota(self):\n\t\twhile True:\n\t\t\tself.printLog(\"get users, limit {} offset {}\".format(self.limit, self.offset))\n\t\t\tauth_header = self.getAuthHeader()\n\t\t\tpayload = {\"limit\":self.limit, \"offset\":self.offset}\n\t\t\tr = requests.get(urlparse.urljoin(base_url, \"api/v2/users\"), headers=auth_header, params=payload,\n\t\t\t\t\t\tverify=verify_tls_cert, timeout=10)\n\t\t\tif r.status_code != 200:\n\t\t\t\tself.printLog(\"error getting users: {}\".format(r.text))\n\t\t\t\tsys.exit(1)\n\t\t\tusers = r.json()\n\t\t\tfor user in users:\n\t\t\t\tif needQuotaUpdate(user):\n\t\t\t\t\tself.updateUserQuota(user[\"username\"])\n\t\t\t\telse:\n\t\t\t\t\tself.printLog(\"user {} does not need a quota update\".format(user[\"username\"]))\n\n\t\t\tself.offset += len(users)\n\t\t\tif len(users) < self.limit:\n\t\t\t\tbreak\n\n\nif __name__ == '__main__':\n\tq = UpdateQuota()\n\tq.updateUsersQuota()\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/drakkan/sftpgo/v2\n\ngo 1.25.0\n\nrequire (\n\tcloud.google.com/go/storage v1.60.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1\n\tgithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4\n\tgithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5\n\tgithub.com/alexedwards/argon2id v1.0.0\n\tgithub.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.4\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.12\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.12\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9\n\tgithub.com/bmatcuk/doublestar/v4 v4.10.0\n\tgithub.com/cockroachdb/cockroach-go/v2 v2.4.3\n\tgithub.com/coreos/go-oidc/v3 v3.17.0\n\tgithub.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b\n\tgithub.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b\n\tgithub.com/fclairamb/ftpserverlib v0.30.0\n\tgithub.com/go-acme/lego/v4 v4.33.0\n\tgithub.com/go-chi/chi/v5 v5.2.5\n\tgithub.com/go-chi/render v1.0.3\n\tgithub.com/go-jose/go-jose/v4 v4.1.3\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/hashicorp/go-hclog v1.6.3\n\tgithub.com/hashicorp/go-plugin v1.7.0\n\tgithub.com/hashicorp/go-retryablehttp v0.7.8\n\tgithub.com/jackc/pgx/v5 v5.8.0\n\tgithub.com/jlaffaye/ftp v0.2.0\n\tgithub.com/klauspost/compress v1.18.4\n\tgithub.com/lithammer/shortuuid/v4 v4.2.0\n\tgithub.com/mattn/go-sqlite3 v1.14.37\n\tgithub.com/mhale/smtpd v0.8.3\n\tgithub.com/minio/sio v0.4.3\n\tgithub.com/otiai10/copy v1.14.1\n\tgithub.com/pires/go-proxyproto v0.11.0\n\tgithub.com/pkg/sftp v1.13.10\n\tgithub.com/pquerna/otp v1.5.0\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/rs/cors v1.11.1\n\tgithub.com/rs/xid v1.6.0\n\tgithub.com/rs/zerolog v1.34.0\n\tgithub.com/sftpgo/sdk v0.1.9\n\tgithub.com/shirou/gopsutil/v3 v3.24.5\n\tgithub.com/spf13/afero v1.15.0\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/studio-b12/gowebdav v0.12.0\n\tgithub.com/subosito/gotenv v1.6.0\n\tgithub.com/unrolled/secure v1.17.0\n\tgithub.com/wagslane/go-password-validator v0.3.0\n\tgithub.com/wneessen/go-mail v0.7.2\n\tgithub.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a\n\tgo.etcd.io/bbolt v1.4.3\n\tgocloud.dev v0.45.0\n\tgolang.org/x/crypto v0.49.0\n\tgolang.org/x/net v0.52.0\n\tgolang.org/x/oauth2 v0.36.0\n\tgolang.org/x/sys v0.42.0\n\tgolang.org/x/term v0.41.0\n\tgolang.org/x/time v0.15.0\n\tgoogle.golang.org/api v0.272.0\n\tgopkg.in/natefinch/lumberjack.v2 v2.2.1\n)\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.18.2 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/monitoring v1.24.3 // indirect\n\tfilippo.io/edwards25519 v1.2.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect\n\tgithub.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect\n\tgithub.com/ajg/form v1.7.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect\n\tgithub.com/aws/smithy-go v1.24.2 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/boombuler/barcode v1.1.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.19.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/yamux v0.1.2 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/kr/fs v0.1.0 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/miekg/dns v1.1.72 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/oklog/run v1.2.0 // indirect\n\tgithub.com/otiai10/mint v1.6.3 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/procfs v0.20.1 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/shoenig/go-m1cpu v0.2.1 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.6.0 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect\n\tgo.opentelemetry.io/otel v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.42.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.4 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/mod v0.34.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/tools v0.43.0 // indirect\n\tgolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect\n\tgoogle.golang.org/genproto v0.0.0-20260319171110-e3a33c96fb44 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260319171110-e3a33c96fb44 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nreplace (\n\tgithub.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f\n\tgithub.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ncloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=\ncloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=\ncloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=\ncloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=\ncloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=\ncloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=\ncloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=\ncloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=\ncloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=\ncloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=\ncloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=\ncloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=\nfilippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=\ngithub.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=\ngithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=\ngithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=\ngithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=\ngithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/ajg/form v1.7.1 h1:OsnBDzTkrWdrxvEnO68I72ZVGJGNaMwPhoAm0V+llgc=\ngithub.com/ajg/form v1.7.1/go.mod h1:HL757PzLyNkj5AIfptT6L+iGNeXTlnrr/oDePGc/y7Q=\ngithub.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=\ngithub.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=\ngithub.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=\ngithub.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=\ngithub.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=\ngithub.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=\ngithub.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=\ngithub.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=\ngithub.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=\ngithub.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=\ngithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=\ngithub.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw=\ngithub.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0=\ngithub.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=\ngithub.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=\ngithub.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f h1:S9JUlrOzjK58UKoLqqb40YLyVlt0bcIFtYrvnanV3zc=\ngithub.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f/go.mod h1:4p8lUl4vQ80L598CygL+3IFtm+3nggvvW/palOlViwE=\ngithub.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b h1:Y1tLiQ8fnxM5f3wiBjAXsHzHNwiY9BR+mXZA75nZwrs=\ngithub.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE=\ngithub.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b h1:G2Mm3YhlyjkFrNnvu5E6LtNcPJtggXL1i5ekDV4hDD4=\ngithub.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b/go.mod h1:XccPiThW83W5pzeOCsJAylEUtWeH+3zQVwiO402FXXc=\ngithub.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=\ngithub.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=\ngithub.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fclairamb/ftpserverlib v0.30.0 h1:caB9sDn1Au//q0j2ev/icPn388qPuk4k1ajSvglDcMQ=\ngithub.com/fclairamb/ftpserverlib v0.30.0/go.mod h1:QmogtltTOgkihyKza0GNo37Mu4AEzbJ+sH6W9Y0MBIQ=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-acme/lego/v4 v4.33.0 h1:2KrRKieG+VczT4zvVXKAY7Tp/S3XLvh/QImofBALRAM=\ngithub.com/go-acme/lego/v4 v4.33.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=\ngithub.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=\ngithub.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=\ngithub.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=\ngithub.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=\ngithub.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=\ngithub.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=\ngithub.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=\ngithub.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=\ngithub.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=\ngithub.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=\ngithub.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=\ngithub.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=\ngithub.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=\ngithub.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=\ngithub.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=\ngithub.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=\ngithub.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/minio/sio v0.4.3 h1:JqyID1XM86KwBZox5RAdLD4MLPIDoCY2cke2CXCJCkg=\ngithub.com/minio/sio v0.4.3/go.mod h1:4ANoe4CCXqnt1FCiLM0+vlBUhhWZzVOhYCz0069KtFc=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=\ngithub.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=\ngithub.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=\ngithub.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=\ngithub.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=\ngithub.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=\ngithub.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=\ngithub.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=\ngithub.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=\ngithub.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=\ngithub.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=\ngithub.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=\ngithub.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=\ngithub.com/sftpgo/sdk v0.1.9 h1:onBWfibCt34xHeKC2KFYPZ1DBqXGl9um/cAw+AVdgzY=\ngithub.com/sftpgo/sdk v0.1.9/go.mod h1:ehimvlTP+XTEiE3t1CPwWx9n7+6A6OGvMGlZ7ouvKFk=\ngithub.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=\ngithub.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=\ngithub.com/shoenig/go-m1cpu v0.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4=\ngithub.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=\ngithub.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=\ngithub.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=\ngithub.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=\ngithub.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=\ngithub.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=\ngithub.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=\ngithub.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=\ngithub.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=\ngithub.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a h1:XfF01GyP+0eWCaVp0y6rNN+kFp7pt9Da4UUYrJ5XPWA=\ngithub.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a/go.mod h1:aXb8yZQEWo1XHGMf1qQfnb83GR/EJ2EBlwtUgAaNBoE=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=\ngo.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=\ngo.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=\ngo.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=\ngo.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=\ngo.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=\ngo.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=\ngo.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=\ngo.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=\ngo.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=\ngo.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngocloud.dev v0.45.0 h1:WknIK8IbRdmynDvara3Q7G6wQhmEiOGwpgJufbM39sY=\ngocloud.dev v0.45.0/go.mod h1:0kXKmkCLG6d31N7NyLZWzt7jDSQura9zD/mWgiB6THI=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=\ngolang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=\ngoogle.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=\ngoogle.golang.org/genproto v0.0.0-20260319171110-e3a33c96fb44 h1:5F2rCQQSavqKBEvXvkLt9Lc61ldIzythnt3+ucqJWG8=\ngoogle.golang.org/genproto v0.0.0-20260319171110-e3a33c96fb44/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260319171110-e3a33c96fb44 h1:3r0atjZtaSjEuUu8NNhKvvnFh8ad9PHWsN9VRotabFU=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260319171110-e3a33c96fb44/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44 h1:sRy++txmErSjyVWlIgQB5nB+U75+Di+AH7eEZ002B/s=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "init/com.github.drakkan.sftpgo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>Label</key>\n    <string>com.github.drakkan.sftpgo</string>\n    <key>EnvironmentVariables</key>\n    <dict>\n        <key>SFTPGO_CONFIG_DIR</key>\n        <string>/usr/local/opt/sftpgo/etc</string>\n        <key>SFTPGO_LOG_FILE_PATH</key>\n        <string>/usr/local/opt/sftpgo/var/log/sftpgo.log</string>\n        <key>SFTPGO_HTTPD__TEMPLATES_PATH</key>\n        <string>/usr/local/opt/sftpgo/usr/share/templates</string>\n        <key>SFTPGO_HTTPD__STATIC_FILES_PATH</key>\n        <string>/usr/local/opt/sftpgo/usr/share/static</string>\n        <key>SFTPGO_HTTPD__OPENAPI_PATH</key>\n        <string>/usr/local/opt/sftpgo/usr/share/openapi</string>\n        <key>SFTPGO_HTTPD__BACKUPS_PATH</key>\n        <string>/usr/local/opt/sftpgo/var/lib/backups</string>\n        <key>SFTPGO_DATA_PROVIDER__CREDENTIALS_PATH</key>\n        <string>/usr/local/opt/sftpgo/var/lib/credentials</string>\n    </dict>\n    <key>WorkingDirectory</key>\n    <string>/usr/local/opt/sftpgo/etc</string>\n    <key>ProgramArguments</key>\n    <array>\n        <string>/usr/local/opt/sftpgo/bin/sftpgo</string>\n        <string>serve</string>\n    </array>\n    <key>KeepAlive</key>\n    <true/>\n    <key>ThrottleInterval</key>\n    <integer>10</integer>\n</dict>\n</plist>\n"
  },
  {
    "path": "init/sftpgo.service",
    "content": "[Unit]\nDescription=SFTPGo Server\nAfter=network.target\n\n[Service]\nUser=sftpgo\nGroup=sftpgo\nType=simple\nWorkingDirectory=/etc/sftpgo\nRuntimeDirectory=sftpgo\nEnvironment=SFTPGO_CONFIG_DIR=/etc/sftpgo/\nEnvironment=SFTPGO_LOG_FILE_PATH=\nEnvironmentFile=-/etc/sftpgo/sftpgo.env\nExecStart=/usr/bin/sftpgo serve\nExecReload=/bin/kill -s HUP $MAINPID\nLimitNOFILE=8192\nKillMode=mixed\nPrivateTmp=true\nRestart=always\nRestartSec=10s\nNoNewPrivileges=yes\nPrivateDevices=yes\nDevicePolicy=closed\nProtectSystem=true\nRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "internal/acme/account.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage acme\n\nimport (\n\t\"crypto\"\n\n\t\"github.com/go-acme/lego/v4/registration\"\n)\n\ntype account struct {\n\tEmail        string                 `json:\"email\"`\n\tRegistration *registration.Resource `json:\"registration\"`\n\tkey          crypto.PrivateKey\n}\n\n/** Implementation of the registration.User interface **/\n\n// GetEmail returns the email address for the account.\nfunc (a *account) GetEmail() string {\n\treturn a.Email\n}\n\n// GetRegistration returns the server registration.\nfunc (a *account) GetRegistration() *registration.Resource {\n\treturn a.Registration\n}\n\n// GetPrivateKey returns the private account key.\nfunc (a *account) GetPrivateKey() crypto.PrivateKey {\n\treturn a.key\n}\n\n/** End **/\n"
  },
  {
    "path": "internal/acme/acme.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package acme provides automatic access to certificates from Let's Encrypt and any other ACME-based CA\n// The code here is largely coiped from https://github.com/go-acme/lego/tree/master/cmd\n// This package is intended to provide basic functionality for obtaining and renewing certificates\n// and implements the \"HTTP-01\" and \"TLSALPN-01\" challenge types.\n// For more advanced features use external tools such as \"lego\"\npackage acme\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/go-acme/lego/v4/challenge/tlsalpn01\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/providers/http/webroot\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\t\"github.com/hashicorp/go-retryablehttp\"\n\t\"github.com/robfig/cron/v3\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/ftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/telemetry\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\nconst (\n\tlogSender = \"acme\"\n)\n\nvar (\n\tconfig            *Configuration\n\tinitialConfig     Configuration\n\tscheduler         *cron.Cron\n\tlogMode           int\n\tsupportedKeyTypes = []string{\n\t\tstring(certcrypto.EC256),\n\t\tstring(certcrypto.EC384),\n\t\tstring(certcrypto.RSA2048),\n\t\tstring(certcrypto.RSA3072),\n\t\tstring(certcrypto.RSA4096),\n\t\tstring(certcrypto.RSA8192),\n\t}\n\tfnReloadHTTPDCerts func() error\n)\n\n// SetReloadHTTPDCertsFn set the function to call to reload HTTPD certificates\nfunc SetReloadHTTPDCertsFn(fn func() error) {\n\tfnReloadHTTPDCerts = fn\n}\n\n// GetCertificates tries to obtain the certificates using the global configuration\nfunc GetCertificates() error {\n\tif config == nil {\n\t\treturn errors.New(\"acme is disabled\")\n\t}\n\treturn config.getCertificates()\n}\n\n// GetCertificatesForConfig tries to obtain the certificates using the provided\n// configuration override. This is a NOOP if we already have certificates\nfunc GetCertificatesForConfig(c *dataprovider.ACMEConfigs, configDir string) error {\n\tif c.Domain == \"\" {\n\t\tacmeLog(logger.LevelDebug, \"no domain configured, nothing to do\")\n\t\treturn nil\n\t}\n\tconfig := mergeConfig(getConfiguration(), c)\n\tif err := config.Initialize(configDir); err != nil {\n\t\treturn err\n\t}\n\thasCerts, err := config.hasCertificates(c.Domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to check if we already have certificates for domain %q: %w\", c.Domain, err)\n\t}\n\tif hasCerts {\n\t\treturn nil\n\t}\n\treturn config.getCertificates()\n}\n\n// GetHTTP01WebRoot returns the web root for HTTP-01 challenge\nfunc GetHTTP01WebRoot() string {\n\treturn initialConfig.HTTP01Challenge.WebRoot\n}\n\nfunc mergeConfig(config Configuration, c *dataprovider.ACMEConfigs) Configuration {\n\tconfig.Domains = []string{c.Domain}\n\tconfig.Email = c.Email\n\tconfig.HTTP01Challenge.Port = c.HTTP01Challenge.Port\n\tconfig.TLSALPN01Challenge.Port = 0\n\treturn config\n}\n\n// getConfiguration returns the configuration set using config file and env vars\nfunc getConfiguration() Configuration {\n\treturn initialConfig\n}\n\nfunc loadProviderConf(c Configuration) (Configuration, error) {\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\treturn c, fmt.Errorf(\"unable to load config from provider: %w\", err)\n\t}\n\tconfigs.SetNilsToEmpty()\n\tif configs.ACME.Domain == \"\" {\n\t\treturn c, nil\n\t}\n\treturn mergeConfig(c, configs.ACME), nil\n}\n\n// Initialize validates and set the configuration\nfunc Initialize(c Configuration, configDir string, checkRenew bool) error {\n\tconfig = nil\n\tinitialConfig = c\n\tc, err := loadProviderConf(c)\n\tif err != nil {\n\t\treturn err\n\t}\n\tutil.CertsBasePath = \"\"\n\tsetLogMode(checkRenew)\n\n\tif err := c.Initialize(configDir); err != nil {\n\t\treturn err\n\t}\n\tif len(c.Domains) == 0 {\n\t\treturn nil\n\t}\n\tutil.CertsBasePath = c.CertsPath\n\tacmeLog(logger.LevelInfo, \"configured domains: %+v, certs base path %q\", c.Domains, c.CertsPath)\n\tconfig = &c\n\tif checkRenew {\n\t\treturn startScheduler()\n\t}\n\treturn nil\n}\n\n// HTTP01Challenge defines the configuration for HTTP-01 challenge type\ntype HTTP01Challenge struct {\n\tPort        int    `json:\"port\" mapstructure:\"port\"`\n\tWebRoot     string `json:\"webroot\" mapstructure:\"webroot\"`\n\tProxyHeader string `json:\"proxy_header\" mapstructure:\"proxy_header\"`\n}\n\nfunc (c *HTTP01Challenge) isEnabled() bool {\n\treturn c.Port > 0 || c.WebRoot != \"\"\n}\n\nfunc (c *HTTP01Challenge) validate() error {\n\tif !c.isEnabled() {\n\t\treturn nil\n\t}\n\tif c.WebRoot != \"\" {\n\t\tif !filepath.IsAbs(c.WebRoot) {\n\t\t\treturn fmt.Errorf(\"invalid HTTP-01 challenge web root, please set an absolute path\")\n\t\t}\n\t\t_, err := os.Stat(c.WebRoot)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid HTTP-01 challenge web root: %w\", err)\n\t\t}\n\t} else {\n\t\tif c.Port > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid HTTP-01 challenge port: %d\", c.Port)\n\t\t}\n\t}\n\treturn nil\n}\n\n// TLSALPN01Challenge defines the configuration for TLSALPN-01 challenge type\ntype TLSALPN01Challenge struct {\n\tPort int `json:\"port\" mapstructure:\"port\"`\n}\n\nfunc (c *TLSALPN01Challenge) isEnabled() bool {\n\treturn c.Port > 0\n}\n\nfunc (c *TLSALPN01Challenge) validate() error {\n\tif !c.isEnabled() {\n\t\treturn nil\n\t}\n\tif c.Port > 65535 {\n\t\treturn fmt.Errorf(\"invalid TLSALPN-01 challenge port: %d\", c.Port)\n\t}\n\treturn nil\n}\n\n// Configuration holds the ACME configuration\ntype Configuration struct {\n\tEmail      string `json:\"email\" mapstructure:\"email\"`\n\tKeyType    string `json:\"key_type\" mapstructure:\"key_type\"`\n\tCertsPath  string `json:\"certs_path\" mapstructure:\"certs_path\"`\n\tCAEndpoint string `json:\"ca_endpoint\" mapstructure:\"ca_endpoint\"`\n\t// if a certificate is to be valid for multiple domains specify the names separated by commas,\n\t// for example: example.com,www.example.com\n\tDomains            []string           `json:\"domains\" mapstructure:\"domains\"`\n\tRenewDays          int                `json:\"renew_days\" mapstructure:\"renew_days\"`\n\tHTTP01Challenge    HTTP01Challenge    `json:\"http01_challenge\" mapstructure:\"http01_challenge\"`\n\tTLSALPN01Challenge TLSALPN01Challenge `json:\"tls_alpn01_challenge\" mapstructure:\"tls_alpn01_challenge\"`\n\taccountConfigPath  string\n\taccountKeyPath     string\n\tlockPath           string\n\ttempDir            string\n}\n\n// Initialize validates and initialize the configuration\nfunc (c *Configuration) Initialize(configDir string) error {\n\tc.checkDomains()\n\tif len(c.Domains) == 0 {\n\t\tacmeLog(logger.LevelInfo, \"no domains configured, acme disabled\")\n\t\treturn nil\n\t}\n\tif c.Email == \"\" || !util.IsEmailValid(c.Email) {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"invalid email address %q\", c.Email),\n\t\t\tutil.I18nErrorInvalidEmail,\n\t\t)\n\t}\n\tif c.RenewDays < 1 {\n\t\treturn fmt.Errorf(\"invalid number of days remaining before renewal: %d\", c.RenewDays)\n\t}\n\tif !slices.Contains(supportedKeyTypes, c.KeyType) {\n\t\treturn fmt.Errorf(\"invalid key type %q\", c.KeyType)\n\t}\n\tcaURL, err := url.Parse(c.CAEndpoint)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid CA endopoint: %w\", err)\n\t}\n\tif !util.IsFileInputValid(c.CertsPath) {\n\t\treturn fmt.Errorf(\"invalid certs path %q\", c.CertsPath)\n\t}\n\tif !filepath.IsAbs(c.CertsPath) {\n\t\tc.CertsPath = filepath.Join(configDir, c.CertsPath)\n\t}\n\terr = os.MkdirAll(c.CertsPath, 0700)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create certs path %q: %w\", c.CertsPath, err)\n\t}\n\tc.tempDir = filepath.Join(c.CertsPath, \"temp\")\n\terr = os.MkdirAll(c.CertsPath, 0700)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create certs temp path %q: %w\", c.tempDir, err)\n\t}\n\tserverPath := strings.NewReplacer(\":\", \"_\", \"/\", string(os.PathSeparator)).Replace(caURL.Host)\n\taccountPath := filepath.Join(c.CertsPath, serverPath)\n\terr = os.MkdirAll(accountPath, 0700)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create account path %q: %w\", accountPath, err)\n\t}\n\tc.accountConfigPath = filepath.Join(accountPath, c.Email+\".json\")\n\tc.accountKeyPath = filepath.Join(accountPath, c.Email+\".key\")\n\tc.lockPath = filepath.Join(c.CertsPath, \"lock\")\n\n\treturn c.validateChallenges()\n}\n\nfunc (c *Configuration) validateChallenges() error {\n\tif !c.HTTP01Challenge.isEnabled() && !c.TLSALPN01Challenge.isEnabled() {\n\t\treturn fmt.Errorf(\"no challenge type defined\")\n\t}\n\tif err := c.HTTP01Challenge.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn c.TLSALPN01Challenge.validate()\n}\n\nfunc (c *Configuration) checkDomains() {\n\tvar domains []string\n\tfor _, domain := range c.Domains {\n\t\tdomain = strings.TrimSpace(domain)\n\t\tif domain == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif d, ok := isDomainValid(domain); ok {\n\t\t\tdomains = append(domains, d)\n\t\t}\n\t}\n\tc.Domains = util.RemoveDuplicates(domains, true)\n}\n\nfunc (c *Configuration) setLockTime() error {\n\tlockTime := fmt.Sprintf(\"%v\", util.GetTimeAsMsSinceEpoch(time.Now()))\n\terr := os.WriteFile(c.lockPath, []byte(lockTime), 0600)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to save lock time to %q: %v\", c.lockPath, err)\n\t\treturn fmt.Errorf(\"unable to save lock time: %w\", err)\n\t}\n\tacmeLog(logger.LevelDebug, \"lock time saved: %q\", lockTime)\n\treturn nil\n}\n\nfunc (c *Configuration) getLockTime() (time.Time, error) {\n\tcontent, err := os.ReadFile(c.lockPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tacmeLog(logger.LevelDebug, \"lock file %q not found\", c.lockPath)\n\t\t\treturn time.Time{}, nil\n\t\t}\n\t\tacmeLog(logger.LevelError, \"unable to read lock file %q: %v\", c.lockPath, err)\n\t\treturn time.Time{}, err\n\t}\n\tmsec, err := strconv.ParseInt(strings.TrimSpace(util.BytesToString(content)), 10, 64)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to parse lock time: %v\", err)\n\t\treturn time.Time{}, fmt.Errorf(\"unable to parse lock time: %w\", err)\n\t}\n\treturn util.GetTimeFromMsecSinceEpoch(msec), nil\n}\n\nfunc (c *Configuration) saveAccount(account *account) error {\n\tjsonBytes, err := json.MarshalIndent(account, \"\", \"\\t\")\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.WriteFile(c.accountConfigPath, jsonBytes, 0600)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to save account to file %q: %v\", c.accountConfigPath, err)\n\t\treturn fmt.Errorf(\"unable to save account: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (c *Configuration) getAccount(privateKey crypto.PrivateKey) (account, error) {\n\t_, err := os.Stat(c.accountConfigPath)\n\tif err != nil && os.IsNotExist(err) {\n\t\tacmeLog(logger.LevelDebug, \"account does not exist\")\n\t\treturn account{Email: c.Email, key: privateKey}, nil\n\t}\n\tvar account account\n\tfileBytes, err := os.ReadFile(c.accountConfigPath)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to read account from file %q: %v\", c.accountConfigPath, err)\n\t\treturn account, fmt.Errorf(\"unable to read account from file: %w\", err)\n\t}\n\terr = json.Unmarshal(fileBytes, &account)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"invalid account file content: %v\", err)\n\t\treturn account, fmt.Errorf(\"unable to parse account file as JSON: %w\", err)\n\t}\n\taccount.key = privateKey\n\tif account.Registration == nil || account.Registration.Body.Status == \"\" {\n\t\tacmeLog(logger.LevelInfo, \"couldn't load account but got a key. Try to look the account up\")\n\t\treg, err := c.tryRecoverRegistration(privateKey)\n\t\tif err != nil {\n\t\t\tacmeLog(logger.LevelError, \"unable to look the account up: %v\", err)\n\t\t\treturn account, fmt.Errorf(\"unable to look the account up: %w\", err)\n\t\t}\n\t\taccount.Registration = reg\n\t\terr = c.saveAccount(&account)\n\t\tif err != nil {\n\t\t\treturn account, err\n\t\t}\n\t}\n\n\treturn account, nil\n}\n\nfunc (c *Configuration) loadPrivateKey() (crypto.PrivateKey, error) {\n\tkeyBytes, err := os.ReadFile(c.accountKeyPath)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to read account key from file %q: %v\", c.accountKeyPath, err)\n\t\treturn nil, fmt.Errorf(\"unable to read account key: %w\", err)\n\t}\n\n\tkeyBlock, _ := pem.Decode(keyBytes)\n\tif keyBlock == nil {\n\t\tacmeLog(logger.LevelError, \"unable to parse private key from file %q: pem decoding failed\", c.accountKeyPath)\n\t\treturn nil, errors.New(\"pem decoding failed\")\n\t}\n\n\tvar privateKey crypto.PrivateKey\n\tswitch keyBlock.Type {\n\tcase \"RSA PRIVATE KEY\":\n\t\tprivateKey, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes)\n\tcase \"EC PRIVATE KEY\":\n\t\tprivateKey, err = x509.ParseECPrivateKey(keyBlock.Bytes)\n\tdefault:\n\t\terr = fmt.Errorf(\"unknown private key type %q\", keyBlock.Type)\n\t}\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to parse private key from file %q: %v\", c.accountKeyPath, err)\n\t\treturn privateKey, fmt.Errorf(\"unable to parse private key: %w\", err)\n\t}\n\treturn privateKey, nil\n}\n\nfunc (c *Configuration) generatePrivateKey() (crypto.PrivateKey, error) {\n\tprivateKey, err := certcrypto.GeneratePrivateKey(certcrypto.KeyType(c.KeyType))\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to generate private key: %v\", err)\n\t\treturn nil, fmt.Errorf(\"unable to generate private key: %w\", err)\n\t}\n\tcertOut, err := os.Create(c.accountKeyPath)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to save private key to file %q: %v\", c.accountKeyPath, err)\n\t\treturn nil, fmt.Errorf(\"unable to save private key: %w\", err)\n\t}\n\tdefer certOut.Close()\n\n\tpemKey := certcrypto.PEMBlock(privateKey)\n\terr = pem.Encode(certOut, pemKey)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to encode private key: %v\", err)\n\t\treturn nil, fmt.Errorf(\"unable to encode private key: %w\", err)\n\t}\n\tacmeLog(logger.LevelDebug, \"new account private key generated\")\n\n\treturn privateKey, nil\n}\n\nfunc (c *Configuration) getPrivateKey() (crypto.PrivateKey, error) {\n\t_, err := os.Stat(c.accountKeyPath)\n\tif err != nil && os.IsNotExist(err) {\n\t\tacmeLog(logger.LevelDebug, \"private key file %q does not exist, generating new private key\", c.accountKeyPath)\n\t\treturn c.generatePrivateKey()\n\t}\n\tacmeLog(logger.LevelDebug, \"loading private key from file %q, stat error: %v\", c.accountKeyPath, err)\n\treturn c.loadPrivateKey()\n}\n\nfunc (c *Configuration) loadCertificatesForDomain(domain string) ([]*x509.Certificate, error) {\n\tdomain = util.SanitizeDomain(domain)\n\tacmeLog(logger.LevelDebug, \"loading certificates for domain %q\", domain)\n\tcontent, err := os.ReadFile(filepath.Join(c.CertsPath, domain+\".crt\"))\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to load certificates for domain %q: %v\", domain, err)\n\t\treturn nil, fmt.Errorf(\"unable to load certificates for domain %q: %w\", domain, err)\n\t}\n\tcerts, err := certcrypto.ParsePEMBundle(content)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to parse certificates for domain %q: %v\", domain, err)\n\t\treturn certs, fmt.Errorf(\"unable to parse certificates for domain %q: %w\", domain, err)\n\t}\n\treturn certs, nil\n}\n\nfunc (c *Configuration) needRenewal(x509Cert *x509.Certificate, domain string) bool {\n\tif x509Cert.IsCA {\n\t\tacmeLog(logger.LevelError, \"certificate bundle starts with a CA certificate, cannot renew domain %v\", domain)\n\t\treturn false\n\t}\n\tnotAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)\n\tif notAfter > c.RenewDays {\n\t\tacmeLog(logger.LevelDebug, \"the certificate for domain %q expires in %d days, no renewal\", domain, notAfter)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (c *Configuration) setup() (*account, *lego.Client, error) {\n\tprivateKey, err := c.getPrivateKey()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\taccount, err := c.getAccount(privateKey)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tconfig := lego.NewConfig(&account)\n\tconfig.CADirURL = c.CAEndpoint\n\tconfig.Certificate.KeyType = certcrypto.KeyType(c.KeyType)\n\tconfig.Certificate.OverallRequestLimit = 6\n\tconfig.UserAgent = version.GetServerVersion(\"/\", false)\n\n\tretryClient := retryablehttp.NewClient()\n\tretryClient.Logger = &logger.LeveledLogger{Sender: \"RetryableHTTPClient\"}\n\tretryClient.RetryMax = 5\n\tretryClient.HTTPClient = config.HTTPClient\n\n\tconfig.HTTPClient = retryClient.StandardClient()\n\n\tclient, err := lego.NewClient(config)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to get ACME client: %v\", err)\n\t\treturn nil, nil, fmt.Errorf(\"unable to get ACME client: %w\", err)\n\t}\n\terr = c.setupChalleges(client)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn &account, client, nil\n}\n\nfunc (c *Configuration) setupChalleges(client *lego.Client) error {\n\tclient.Challenge.Remove(challenge.DNS01)\n\tif c.HTTP01Challenge.isEnabled() {\n\t\tif c.HTTP01Challenge.WebRoot != \"\" {\n\t\t\tacmeLog(logger.LevelDebug, \"configuring HTTP-01 web root challenge, path %q\", c.HTTP01Challenge.WebRoot)\n\t\t\tproviderServer, err := webroot.NewHTTPProvider(c.HTTP01Challenge.WebRoot)\n\t\t\tif err != nil {\n\t\t\t\tacmeLog(logger.LevelError, \"unable to create HTTP-01 web root challenge provider from path %q: %v\",\n\t\t\t\t\tc.HTTP01Challenge.WebRoot, err)\n\t\t\t\treturn fmt.Errorf(\"unable to create HTTP-01 web root challenge provider: %w\", err)\n\t\t\t}\n\t\t\terr = client.Challenge.SetHTTP01Provider(providerServer)\n\t\t\tif err != nil {\n\t\t\t\tacmeLog(logger.LevelError, \"unable to set HTTP-01 challenge provider: %v\", err)\n\t\t\t\treturn fmt.Errorf(\"unable to set HTTP-01 challenge provider: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tacmeLog(logger.LevelDebug, \"configuring HTTP-01 challenge, port %d\", c.HTTP01Challenge.Port)\n\t\t\tproviderServer := http01.NewProviderServer(\"\", fmt.Sprintf(\"%d\", c.HTTP01Challenge.Port))\n\t\t\tif c.HTTP01Challenge.ProxyHeader != \"\" {\n\t\t\t\tacmeLog(logger.LevelDebug, \"setting proxy header to \\\"%s\\\"\", c.HTTP01Challenge.ProxyHeader)\n\t\t\t\tproviderServer.SetProxyHeader(c.HTTP01Challenge.ProxyHeader)\n\t\t\t}\n\t\t\terr := client.Challenge.SetHTTP01Provider(providerServer)\n\t\t\tif err != nil {\n\t\t\t\tacmeLog(logger.LevelError, \"unable to set HTTP-01 challenge provider: %v\", err)\n\t\t\t\treturn fmt.Errorf(\"unable to set HTTP-01 challenge provider: %w\", err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tclient.Challenge.Remove(challenge.HTTP01)\n\t}\n\tif c.TLSALPN01Challenge.isEnabled() {\n\t\tacmeLog(logger.LevelDebug, \"configuring TLSALPN-01 challenge, port %d\", c.TLSALPN01Challenge.Port)\n\t\terr := client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer(\"\", fmt.Sprintf(\"%d\", c.TLSALPN01Challenge.Port)))\n\t\tif err != nil {\n\t\t\tacmeLog(logger.LevelError, \"unable to set TLSALPN-01 challenge provider: %v\", err)\n\t\t\treturn fmt.Errorf(\"unable to set TLSALPN-01 challenge provider: %w\", err)\n\t\t}\n\t} else {\n\t\tclient.Challenge.Remove(challenge.TLSALPN01)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Configuration) register(client *lego.Client) (*registration.Resource, error) {\n\treturn client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n}\n\nfunc (c *Configuration) tryRecoverRegistration(privateKey crypto.PrivateKey) (*registration.Resource, error) {\n\tconfig := lego.NewConfig(&account{key: privateKey})\n\tconfig.CADirURL = c.CAEndpoint\n\tconfig.UserAgent = version.GetServerVersion(\"/\", false)\n\n\tretryClient := retryablehttp.NewClient()\n\tretryClient.Logger = &logger.LeveledLogger{Sender: \"RetryableHTTPClient\"}\n\tretryClient.RetryMax = 5\n\tretryClient.HTTPClient = config.HTTPClient\n\n\tconfig.HTTPClient = retryClient.StandardClient()\n\n\tclient, err := lego.NewClient(config)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to get the ACME client: %v\", err)\n\t\treturn nil, err\n\t}\n\n\treturn client.Registration.ResolveAccountByKey()\n}\n\nfunc (c *Configuration) getCrtPath(domain string) string {\n\treturn filepath.Join(c.CertsPath, domain+\".crt\")\n}\n\nfunc (c *Configuration) getKeyPath(domain string) string {\n\treturn filepath.Join(c.CertsPath, domain+\".key\")\n}\n\nfunc (c *Configuration) getResourcePath(domain string) string {\n\treturn filepath.Join(c.CertsPath, domain+\".json\")\n}\n\nfunc (c *Configuration) obtainAndSaveCertificate(client *lego.Client, domain string) error {\n\tdomains := getDomains(domain)\n\tacmeLog(logger.LevelInfo, \"requesting certificates for domains %+v\", domains)\n\trequest := certificate.ObtainRequest{\n\t\tDomains:                        domains,\n\t\tBundle:                         true,\n\t\tMustStaple:                     false,\n\t\tPreferredChain:                 \"\",\n\t\tAlwaysDeactivateAuthorizations: false,\n\t}\n\tcert, err := client.Certificate.Obtain(request)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to obtain certificates for domains %+v: %v\", domains, err)\n\t\treturn fmt.Errorf(\"unable to obtain certificates: %w\", err)\n\t}\n\tdomain = util.SanitizeDomain(domain)\n\terr = os.WriteFile(c.getCrtPath(domain), cert.Certificate, 0600)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to save certificate for domain %s: %v\", domain, err)\n\t\treturn fmt.Errorf(\"unable to save certificate: %w\", err)\n\t}\n\terr = os.WriteFile(c.getKeyPath(domain), cert.PrivateKey, 0600)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to save private key for domain %s: %v\", domain, err)\n\t\treturn fmt.Errorf(\"unable to save private key: %w\", err)\n\t}\n\tjsonBytes, err := json.MarshalIndent(cert, \"\", \"\\t\")\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to marshal certificate resources for domain %v: %v\", domain, err)\n\t\treturn err\n\t}\n\terr = os.WriteFile(c.getResourcePath(domain), jsonBytes, 0600)\n\tif err != nil {\n\t\tacmeLog(logger.LevelError, \"unable to save certificate resources for domain %v: %v\", domain, err)\n\t\treturn fmt.Errorf(\"unable to save certificate resources: %w\", err)\n\t}\n\n\tacmeLog(logger.LevelInfo, \"certificates for domains %+v saved\", domains)\n\treturn nil\n}\n\n// hasCertificates returns true if certificates for the specified domain has already been issued\nfunc (c *Configuration) hasCertificates(domain string) (bool, error) {\n\tdomain = util.SanitizeDomain(domain)\n\tif _, err := os.Stat(c.getCrtPath(domain)); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\tif _, err := os.Stat(c.getKeyPath(domain)); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// getCertificates tries to obtain the certificates for the configured domains\nfunc (c *Configuration) getCertificates() error {\n\taccount, client, err := c.setup()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif account.Registration == nil {\n\t\treg, err := c.register(client)\n\t\tif err != nil {\n\t\t\tacmeLog(logger.LevelError, \"unable to register account: %v\", err)\n\t\t\treturn fmt.Errorf(\"unable to register account: %w\", err)\n\t\t}\n\t\taccount.Registration = reg\n\t\terr = c.saveAccount(account)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, domain := range c.Domains {\n\t\terr = c.obtainAndSaveCertificate(client, domain)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *Configuration) notifyCertificateRenewal(domain string, err error) {\n\tif domain == \"\" {\n\t\tdomain = strings.Join(c.Domains, \",\")\n\t}\n\tparams := common.EventParams{\n\t\tName:      domain,\n\t\tEvent:     \"Certificate renewal\",\n\t\tTimestamp: time.Now(),\n\t}\n\tif err != nil {\n\t\tparams.Status = 2\n\t\tparams.AddError(err)\n\t} else {\n\t\tparams.Status = 1\n\t}\n\tcommon.HandleCertificateEvent(params)\n}\n\nfunc (c *Configuration) renewCertificates() error {\n\tlockTime, err := c.getLockTime()\n\tif err != nil {\n\t\treturn err\n\t}\n\tacmeLog(logger.LevelDebug, \"certificate renew lock time %v\", lockTime)\n\tif lockTime.Add(-30*time.Second).Before(time.Now()) && lockTime.Add(5*time.Minute).After(time.Now()) {\n\t\tacmeLog(logger.LevelInfo, \"certificate renew skipped, lock time too close: %v\", lockTime)\n\t\treturn nil\n\t}\n\terr = c.setLockTime()\n\tif err != nil {\n\t\tc.notifyCertificateRenewal(\"\", err)\n\t\treturn err\n\t}\n\taccount, client, err := c.setup()\n\tif err != nil {\n\t\tc.notifyCertificateRenewal(\"\", err)\n\t\treturn err\n\t}\n\tif account.Registration == nil {\n\t\tacmeLog(logger.LevelError, \"cannot renew certificates, your account is not registered\")\n\t\terr = errors.New(\"cannot renew certificates, your account is not registered\")\n\t\tc.notifyCertificateRenewal(\"\", err)\n\t\treturn err\n\t}\n\tvar errRenew error\n\tneedReload := false\n\tfor _, domain := range c.Domains {\n\t\tcertificates, err := c.loadCertificatesForDomain(domain)\n\t\tif err != nil {\n\t\t\tc.notifyCertificateRenewal(domain, err)\n\t\t\terrRenew = err\n\t\t\tcontinue\n\t\t}\n\t\tcert := certificates[0]\n\t\tif !c.needRenewal(cert, domain) {\n\t\t\tcontinue\n\t\t}\n\t\terr = c.obtainAndSaveCertificate(client, domain)\n\t\tif err != nil {\n\t\t\tc.notifyCertificateRenewal(domain, err)\n\t\t\terrRenew = err\n\t\t} else {\n\t\t\tc.notifyCertificateRenewal(domain, nil)\n\t\t\tneedReload = true\n\t\t}\n\t}\n\tif needReload {\n\t\t// at least one certificate has been renewed, sends a reload to all services that may be using certificates\n\t\terr = ftpd.ReloadCertificateMgr()\n\t\tacmeLog(logger.LevelInfo, \"ftpd certificate manager reloaded , error: %v\", err)\n\t\tif fnReloadHTTPDCerts != nil {\n\t\t\terr = fnReloadHTTPDCerts()\n\t\t\tacmeLog(logger.LevelInfo, \"httpd certificates manager reloaded , error: %v\", err)\n\t\t}\n\t\terr = webdavd.ReloadCertificateMgr()\n\t\tacmeLog(logger.LevelInfo, \"webdav certificates manager reloaded , error: %v\", err)\n\t\terr = telemetry.ReloadCertificateMgr()\n\t\tacmeLog(logger.LevelInfo, \"telemetry certificates manager reloaded , error: %v\", err)\n\t}\n\n\treturn errRenew\n}\n\nfunc isDomainValid(domain string) (string, bool) {\n\tisValid := false\n\tfor d := range strings.SplitSeq(domain, \",\") {\n\t\td = strings.TrimSpace(d)\n\t\tif d != \"\" {\n\t\t\tisValid = true\n\t\t\tbreak\n\t\t}\n\t}\n\treturn domain, isValid\n}\n\nfunc getDomains(domain string) []string {\n\tvar domains []string\n\n\tdelimiter := \",\"\n\tif !strings.Contains(domain, \",\") && strings.Contains(domain, \" \") {\n\t\tdelimiter = \" \"\n\t}\n\n\tfor d := range strings.SplitSeq(domain, delimiter) {\n\t\td = strings.TrimSpace(d)\n\t\tif d != \"\" {\n\t\t\tdomains = append(domains, d)\n\t\t}\n\t}\n\treturn util.RemoveDuplicates(domains, false)\n}\n\nfunc stopScheduler() {\n\tif scheduler != nil {\n\t\tscheduler.Stop()\n\t\tscheduler = nil\n\t}\n}\n\nfunc startScheduler() error {\n\tstopScheduler()\n\n\trandSecs := rand.Intn(59)\n\tscheduler = cron.New(cron.WithLocation(time.UTC), cron.WithLogger(cron.DiscardLogger))\n\t_, err := scheduler.AddFunc(fmt.Sprintf(\"@every 12h0m%ds\", randSecs), renewCertificates)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to schedule certificates renewal: %w\", err)\n\t}\n\n\tacmeLog(logger.LevelInfo, \"starting scheduler, initial certificates check in %d seconds\", randSecs)\n\tinitialTimer := time.NewTimer(time.Duration(randSecs) * time.Second)\n\tgo func() {\n\t\t<-initialTimer.C\n\t\trenewCertificates()\n\t}()\n\n\tscheduler.Start()\n\treturn nil\n}\n\nfunc renewCertificates() {\n\tif config != nil {\n\t\tif err := config.renewCertificates(); err != nil {\n\t\t\tacmeLog(logger.LevelError, \"unable to renew certificates: %v\", err)\n\t\t}\n\t}\n}\n\nfunc setLogMode(checkRenew bool) {\n\tif checkRenew {\n\t\tlogMode = 1\n\t} else {\n\t\tlogMode = 2\n\t}\n\tlog.Logger = &logger.LegoAdapter{\n\t\tLogToConsole: logMode != 1,\n\t}\n}\n\nfunc acmeLog(level logger.LogLevel, format string, v ...any) {\n\tif logMode == 1 {\n\t\tlogger.Log(level, logSender, \"\", format, v...)\n\t} else {\n\t\tswitch level {\n\t\tcase logger.LevelDebug:\n\t\t\tlogger.DebugToConsole(format, v...)\n\t\tcase logger.LevelInfo:\n\t\t\tlogger.InfoToConsole(format, v...)\n\t\tcase logger.LevelWarn:\n\t\t\tlogger.WarnToConsole(format, v...)\n\t\tdefault:\n\t\t\tlogger.ErrorToConsole(format, v...)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/bundle/bundle.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build bundle\n\npackage bundle\n\nimport (\n\t\"embed\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"+bundle\")\n}\n\n//go:embed templates/*\nvar templatesFs embed.FS\n\n//go:embed static/*\nvar staticFs embed.FS\n\n//go:embed openapi/*\nvar openapiFs embed.FS\n\n// GetTemplatesFs returns the embedded filesystem with the SFTPGo templates\nfunc GetTemplatesFs() embed.FS {\n\treturn templatesFs\n}\n\n// GetStaticFs return the http Filesystem with the embedded static files\nfunc GetStaticFs() http.FileSystem {\n\tfsys, err := fs.Sub(staticFs, \"static\")\n\tif err != nil {\n\t\terr = fmt.Errorf(\"unable to get embedded filesystem for static files: %w\", err)\n\t\tpanic(err)\n\t}\n\treturn http.FS(fsys)\n}\n\n// GetOpenAPIFs return the http Filesystem with the embedded static files\nfunc GetOpenAPIFs() http.FileSystem {\n\tfsys, err := fs.Sub(openapiFs, \"openapi\")\n\tif err != nil {\n\t\terr = fmt.Errorf(\"unable to get embedded filesystem for OpenAPI files: %w\", err)\n\t\tpanic(err)\n\t}\n\treturn http.FS(fsys)\n}\n"
  },
  {
    "path": "internal/cmd/acme.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/acme\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tacmeCmd = &cobra.Command{\n\t\tUse:   \"acme\",\n\t\tShort: \"Obtain TLS certificates from ACME-based CAs like Let's Encrypt\",\n\t}\n\tacmeRunCmd = &cobra.Command{\n\t\tUse:   \"run\",\n\t\tShort: \"Register your account and obtain certificates\",\n\t\tLong: `This command must be run to obtain TLS certificates the first time or every\ntime you add a new domain to your configuration file.\nCertificates are saved in the configured \"certs_path\".\nAfter this initial step, the certificates are automatically checked and\nrenewed by the SFTPGo service\n`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tlogger.DisableLogger()\n\t\t\tlogger.EnableConsoleLogger(zerolog.DebugLevel)\n\t\t\tconfigDir = util.CleanDirInput(configDir)\n\t\t\terr := config.LoadConfig(configDir, configFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize ACME, config load error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tkmsConfig := config.GetKMSConfig()\n\t\t\terr = kmsConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"unable to initialize KMS: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tif config.HasKMSPlugin() {\n\t\t\t\tif err := plugin.Initialize(config.GetPluginsConfig(), \"debug\"); err != nil {\n\t\t\t\t\tlogger.ErrorToConsole(\"unable to initialize plugin system: %v\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tregisterSignals()\n\t\t\t\tdefer plugin.Handler.Cleanup()\n\t\t\t}\n\n\t\t\tmfaConfig := config.GetMFAConfig()\n\t\t\terr = mfaConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize MFA: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tproviderConf := config.GetProviderConf()\n\t\t\terr = dataprovider.Initialize(providerConf, configDir, false)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"error initializing data provider: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tacmeConfig := config.GetACMEConfig()\n\t\t\terr = acme.Initialize(acmeConfig, configDir, false)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize ACME configuration: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tif err = acme.GetCertificates(); err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Cannot get certificates: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\taddConfigFlags(acmeRunCmd)\n\tacmeCmd.AddCommand(acmeRunCmd)\n\trootCmd.AddCommand(acmeCmd)\n}\n"
  },
  {
    "path": "internal/cmd/gen.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport \"github.com/spf13/cobra\"\n\nvar genCmd = &cobra.Command{\n\tUse:   \"gen\",\n\tShort: \"A collection of useful generators\",\n}\n\nfunc init() {\n\trootCmd.AddCommand(genCmd)\n}\n"
  },
  {
    "path": "internal/cmd/gencompletion.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar genCompletionCmd = &cobra.Command{\n\tUse:   \"completion [bash|zsh|fish|powershell]\",\n\tShort: \"Generate the autocompletion script for the specified shell\",\n\tLong: `Generate the autocompletion script for sftpgo for the specified shell.\n\nSee each sub-command's help for details on how to use the generated script.\n`,\n}\n\nvar genCompletionBashCmd = &cobra.Command{\n\tUse:   \"bash\",\n\tShort: \"Generate the autocompletion script for bash\",\n\tLong: `Generate the autocompletion script for the bash shell.\n\nThis script depends on the 'bash-completion' package.\nIf it is not installed already, you can install it via your OS's package\nmanager.\n\nTo load completions in your current shell session:\n\n$ source <(sftpgo gen completion bash)\n\nTo load completions for every new session, execute once:\n\nLinux:\n  $ sudo sftpgo gen completion bash > /usr/share/bash-completion/completions/sftpgo\n\nMacOS:\n  $ sudo sftpgo gen completion bash > /usr/local/etc/bash_completion.d/sftpgo\n\nYou will need to start a new shell for this setup to take effect.\n`,\n\tDisableFlagsInUseLine: true,\n\tRunE: func(cmd *cobra.Command, _ []string) error {\n\t\treturn cmd.Root().GenBashCompletionV2(os.Stdout, true)\n\t},\n}\n\nvar genCompletionZshCmd = &cobra.Command{\n\tUse:   \"zsh\",\n\tShort: \"Generate the autocompletion script for zsh\",\n\tLong: `Generate the autocompletion script for the zsh shell.\n\nIf shell completion is not already enabled in your environment you will need\nto enable it.  You can execute the following once:\n\n$ echo \"autoload -U compinit; compinit\" >> ~/.zshrc\n\nTo load completions for every new session, execute once:\n\nLinux:\n  $ sftpgo gen completion zsh > > \"${fpath[1]}/_sftpgo\"\n\nmacOS:\n  $ sudo sftpgo gen completion zsh > /usr/local/share/zsh/site-functions/_sftpgo\n\nYou will need to start a new shell for this setup to take effect.\n`,\n\tDisableFlagsInUseLine: true,\n\tRunE: func(cmd *cobra.Command, _ []string) error {\n\t\treturn cmd.Root().GenZshCompletion(os.Stdout)\n\t},\n}\n\nvar genCompletionFishCmd = &cobra.Command{\n\tUse:   \"fish\",\n\tShort: \"Generate the autocompletion script for fish\",\n\tLong: `Generate the autocompletion script for the fish shell.\n\nTo load completions in your current shell session:\n\n$ sftpgo gen completion fish | source\n\nTo load completions for every new session, execute once:\n\n$ sftpgo gen completion fish > ~/.config/fish/completions/sftpgo.fish\n\nYou will need to start a new shell for this setup to take effect.\n`,\n\tDisableFlagsInUseLine: true,\n\tRunE: func(cmd *cobra.Command, _ []string) error {\n\t\treturn cmd.Root().GenFishCompletion(os.Stdout, true)\n\t},\n}\n\nvar genCompletionPowerShellCmd = &cobra.Command{\n\tUse:   \"powershell\",\n\tShort: \"Generate the autocompletion script for powershell\",\n\tLong: `Generate the autocompletion script for powershell.\n\nTo load completions in your current shell session:\n\nPS C:\\> sftpgo gen completion powershell | Out-String | Invoke-Expression\n\nTo load completions for every new session, add the output of the above command\nto your powershell profile.\n`,\n\tDisableFlagsInUseLine: true,\n\tRunE: func(cmd *cobra.Command, _ []string) error {\n\t\treturn cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)\n\t},\n}\n\nfunc init() {\n\tgenCompletionCmd.AddCommand(genCompletionBashCmd)\n\tgenCompletionCmd.AddCommand(genCompletionZshCmd)\n\tgenCompletionCmd.AddCommand(genCompletionFishCmd)\n\tgenCompletionCmd.AddCommand(genCompletionPowerShellCmd)\n\n\tgenCmd.AddCommand(genCompletionCmd)\n}\n"
  },
  {
    "path": "internal/cmd/genman.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/cobra/doc\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nvar (\n\tmanDir    string\n\tgenManCmd = &cobra.Command{\n\t\tUse:   \"man\",\n\t\tShort: \"Generate man pages for sftpgo\",\n\t\tLong: `This command automatically generates up-to-date man pages of SFTPGo's\ncommand-line interface.\nBy default, it creates the man page files in the \"man\" directory under the\ncurrent directory.\n`,\n\t\tRun: func(cmd *cobra.Command, _ []string) {\n\t\t\tlogger.DisableLogger()\n\t\t\tlogger.EnableConsoleLogger(zerolog.DebugLevel)\n\t\t\tif _, err := os.Stat(manDir); errors.Is(err, fs.ErrNotExist) {\n\t\t\t\terr = os.MkdirAll(manDir, os.ModePerm)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogger.WarnToConsole(\"Unable to generate man page files: %v\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t}\n\t\t\theader := &doc.GenManHeader{\n\t\t\t\tSection: \"1\",\n\t\t\t\tManual:  \"SFTPGo Manual\",\n\t\t\t\tSource:  fmt.Sprintf(\"SFTPGo %v\", version.Get().Version),\n\t\t\t}\n\t\t\tcmd.Root().DisableAutoGenTag = true\n\t\t\terr := doc.GenManTree(cmd.Root(), header, manDir)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WarnToConsole(\"Unable to generate man page files: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\tgenManCmd.Flags().StringVarP(&manDir, \"dir\", \"d\", \"man\", \"The directory to write the man pages\")\n\tgenCmd.AddCommand(genManCmd)\n}\n"
  },
  {
    "path": "internal/cmd/initprovider.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tinitProviderCmd = &cobra.Command{\n\t\tUse:   \"initprovider\",\n\t\tShort: \"Initialize and/or updates the configured data provider\",\n\t\tLong: `This command reads the data provider connection details from the specified\nconfiguration file and creates the initial structure or update the existing one,\nas needed.\n\nSome data providers such as bolt and memory does not require an initialization\nbut they could require an update to the existing data after upgrading SFTPGo.\n\nFor SQLite/bolt providers the database file will be auto-created if missing.\n\nFor PostgreSQL and MySQL providers you need to create the configured database,\nthis command will create/update the required tables as needed.\n\nTo initialize/update the data provider from the configuration directory simply use:\n\n$ sftpgo initprovider\n\nAny defined action is ignored.\nPlease take a look at the usage below to customize the options.`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tlogger.DisableLogger()\n\t\t\tlogger.EnableConsoleLogger(zerolog.DebugLevel)\n\t\t\tconfigDir = util.CleanDirInput(configDir)\n\t\t\terr := config.LoadConfig(configDir, configFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize data provider, config load error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tkmsConfig := config.GetKMSConfig()\n\t\t\terr = kmsConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize KMS: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tif config.HasKMSPlugin() {\n\t\t\t\tif err := plugin.Initialize(config.GetPluginsConfig(), \"debug\"); err != nil {\n\t\t\t\t\tlogger.ErrorToConsole(\"unable to initialize plugin system: %v\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tregisterSignals()\n\t\t\t\tdefer plugin.Handler.Cleanup()\n\t\t\t}\n\n\t\t\tmfaConfig := config.GetMFAConfig()\n\t\t\terr = mfaConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize MFA: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tproviderConf := config.GetProviderConf()\n\t\t\t// ignore actions\n\t\t\tproviderConf.Actions.Hook = \"\"\n\t\t\tproviderConf.Actions.ExecuteFor = nil\n\t\t\tproviderConf.Actions.ExecuteOn = nil\n\t\t\tlogger.InfoToConsole(\"Initializing provider: %q config file: %q\", providerConf.Driver, viper.ConfigFileUsed())\n\t\t\terr = dataprovider.InitializeDatabase(providerConf, configDir)\n\t\t\tswitch err {\n\t\t\tcase nil:\n\t\t\t\tlogger.InfoToConsole(\"Data provider successfully initialized/updated\")\n\t\t\tcase dataprovider.ErrNoInitRequired:\n\t\t\t\tlogger.InfoToConsole(\"%v\", err.Error())\n\t\t\tdefault:\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize/update the data provider: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tif providerConf.Driver != dataprovider.MemoryDataProviderName && loadDataFrom != \"\" {\n\t\t\t\tif err := common.Initialize(config.GetCommonConfig(), providerConf.GetShared()); err != nil {\n\t\t\t\t\tlogger.ErrorToConsole(\"%v\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tservice := service.Service{\n\t\t\t\t\tLoadDataFrom:      loadDataFrom,\n\t\t\t\t\tLoadDataMode:      loadDataMode,\n\t\t\t\t\tLoadDataQuotaScan: loadDataQuotaScan,\n\t\t\t\t\tLoadDataClean:     loadDataClean,\n\t\t\t\t}\n\t\t\t\tif err = service.LoadInitialData(); err != nil {\n\t\t\t\t\tlogger.ErrorToConsole(\"Cannot load initial data: %v\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\trootCmd.AddCommand(initProviderCmd)\n\taddConfigFlags(initProviderCmd)\n\taddBaseLoadDataFlags(initProviderCmd)\n}\n"
  },
  {
    "path": "internal/cmd/install_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tinstallCmd = &cobra.Command{\n\t\tUse:   \"install\",\n\t\tShort: \"Install SFTPGo as Windows Service\",\n\t\tLong: `To install the SFTPGo Windows Service with the default values for the command\nline flags simply use:\n\nsftpgo service install\n\nPlease take a look at the usage below to customize the startup options`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\ts := service.Service{\n\t\t\t\tConfigDir:     util.CleanDirInput(configDir),\n\t\t\t\tConfigFile:    configFile,\n\t\t\t\tLogFilePath:   logFilePath,\n\t\t\t\tLogMaxSize:    logMaxSize,\n\t\t\t\tLogMaxBackups: logMaxBackups,\n\t\t\t\tLogMaxAge:     logMaxAge,\n\t\t\t\tLogCompress:   logCompress,\n\t\t\t\tLogLevel:      logLevel,\n\t\t\t\tLogUTCTime:    logUTCTime,\n\t\t\t\tShutdown:      make(chan bool),\n\t\t\t}\n\t\t\twinService := service.WindowsService{\n\t\t\t\tService: s,\n\t\t\t}\n\t\t\tserviceArgs := []string{\"service\", \"start\"}\n\t\t\tcustomFlags := getCustomServeFlags()\n\t\t\tif len(customFlags) > 0 {\n\t\t\t\tserviceArgs = append(serviceArgs, customFlags...)\n\t\t\t}\n\t\t\terr := winService.Install(serviceArgs...)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error installing service: %v\\r\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Service installed!\\r\\n\")\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\tserviceCmd.AddCommand(installCmd)\n\taddServeFlags(installCmd)\n}\n\nfunc getCustomServeFlags() []string {\n\tresult := []string{}\n\tif configDir != defaultConfigDir {\n\t\tconfigDir = util.CleanDirInput(configDir)\n\t\tresult = append(result, \"--\"+configDirFlag)\n\t\tresult = append(result, configDir)\n\t}\n\tif configFile != defaultConfigFile {\n\t\tresult = append(result, \"--\"+configFileFlag)\n\t\tresult = append(result, configFile)\n\t}\n\tif logFilePath != defaultLogFile {\n\t\tresult = append(result, \"--\"+logFilePathFlag)\n\t\tresult = append(result, logFilePath)\n\t}\n\tif logMaxSize != defaultLogMaxSize {\n\t\tresult = append(result, \"--\"+logMaxSizeFlag)\n\t\tresult = append(result, strconv.Itoa(logMaxSize))\n\t}\n\tif logMaxBackups != defaultLogMaxBackup {\n\t\tresult = append(result, \"--\"+logMaxBackupFlag)\n\t\tresult = append(result, strconv.Itoa(logMaxBackups))\n\t}\n\tif logMaxAge != defaultLogMaxAge {\n\t\tresult = append(result, \"--\"+logMaxAgeFlag)\n\t\tresult = append(result, strconv.Itoa(logMaxAge))\n\t}\n\tif logLevel != defaultLogLevel {\n\t\tresult = append(result, \"--\"+logLevelFlag)\n\t\tresult = append(result, logLevel)\n\t}\n\tif logUTCTime != defaultLogUTCTime {\n\t\tresult = append(result, \"--\"+logUTCTimeFlag+\"=true\")\n\t}\n\tif logCompress != defaultLogCompress {\n\t\tresult = append(result, \"--\"+logCompressFlag+\"=true\")\n\t}\n\tif graceTime != defaultGraceTime {\n\t\tresult = append(result, \"--\"+graceTimeFlag)\n\t\tresult = append(result, strconv.Itoa(graceTime))\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "internal/cmd/ping.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getHealthzURLFromBindings(bindings []httpd.Binding) string {\n\tfor _, b := range bindings {\n\t\tif b.Port > 0 && b.IsValid() {\n\t\t\tvar url string\n\t\t\tif b.EnableHTTPS {\n\t\t\t\turl = \"https://\"\n\t\t\t} else {\n\t\t\t\turl = \"http://\"\n\t\t\t}\n\t\t\tif b.Address == \"\" {\n\t\t\t\turl += \"127.0.0.1\"\n\t\t\t} else {\n\t\t\t\turl += b.Address\n\t\t\t}\n\t\t\turl += fmt.Sprintf(\":%d\", b.Port)\n\t\t\turl += \"/healthz\"\n\t\t\treturn url\n\t\t}\n\t}\n\treturn \"\"\n}\n\nvar (\n\tpingCmd = &cobra.Command{\n\t\tUse:   \"ping\",\n\t\tShort: \"Issues an health check to SFTPGo\",\n\t\tLong: `This command is only useful in environments where system commands like\n\"curl\", \"wget\" and similar are not available.\nChecks over UNIX domain sockets are not supported`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tlogger.DisableLogger()\n\t\t\tlogger.EnableConsoleLogger(zerolog.DebugLevel)\n\t\t\tconfigDir = util.CleanDirInput(configDir)\n\t\t\terr := config.LoadConfig(configDir, configFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WarnToConsole(\"Unable to load configuration: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\thttpConfig := config.GetHTTPConfig()\n\t\t\terr = httpConfig.Initialize(configDir)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"error initializing http client: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\ttelemetryConfig := config.GetTelemetryConfig()\n\t\t\tvar url string\n\t\t\tif telemetryConfig.BindPort > 0 {\n\t\t\t\tif telemetryConfig.CertificateFile != \"\" && telemetryConfig.CertificateKeyFile != \"\" {\n\t\t\t\t\turl += \"https://\"\n\t\t\t\t} else {\n\t\t\t\t\turl += \"http://\"\n\t\t\t\t}\n\t\t\t\tif telemetryConfig.BindAddress == \"\" {\n\t\t\t\t\turl += \"127.0.0.1\"\n\t\t\t\t} else {\n\t\t\t\t\turl += telemetryConfig.BindAddress\n\t\t\t\t}\n\t\t\t\turl += fmt.Sprintf(\":%d\", telemetryConfig.BindPort)\n\t\t\t\turl += \"/healthz\"\n\t\t\t}\n\t\t\tif url == \"\" {\n\t\t\t\thttpdConfig := config.GetHTTPDConfig()\n\t\t\t\turl = getHealthzURLFromBindings(httpdConfig.Bindings)\n\t\t\t}\n\t\t\tif url == \"\" {\n\t\t\t\tlogger.ErrorToConsole(\"no suitable configuration found, please enable the telemetry server or REST API over HTTP/S\")\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\n\t\t\tlogger.DebugToConsole(\"Health Check URL %q\", url)\n\t\t\tresp, err := httpclient.RetryableGet(url)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to connect to SFTPGo: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tdefer resp.Body.Close()\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\tlogger.ErrorToConsole(\"Unexpected status code %d\", resp.StatusCode)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tlogger.InfoToConsole(\"OK\")\n\t\t},\n\t}\n)\n\nfunc init() {\n\taddConfigFlags(pingCmd)\n\trootCmd.AddCommand(pingCmd)\n}\n"
  },
  {
    "path": "internal/cmd/portable.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !noportable\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nvar (\n\tdirectoryToServe                   string\n\tportableSFTPDPort                  int\n\tportableUsername                   string\n\tportablePassword                   string\n\tportablePasswordFile               string\n\tportableStartDir                   string\n\tportableLogFile                    string\n\tportableLogLevel                   string\n\tportableLogUTCTime                 bool\n\tportablePublicKeys                 []string\n\tportablePermissions                []string\n\tportableSSHCommands                []string\n\tportableAllowedPatterns            []string\n\tportableDeniedPatterns             []string\n\tportableFsProvider                 string\n\tportableS3Bucket                   string\n\tportableS3Region                   string\n\tportableS3AccessKey                string\n\tportableS3AccessSecret             string\n\tportableS3RoleARN                  string\n\tportableS3Endpoint                 string\n\tportableS3StorageClass             string\n\tportableS3ACL                      string\n\tportableS3KeyPrefix                string\n\tportableS3ULPartSize               int\n\tportableS3ULConcurrency            int\n\tportableS3ForcePathStyle           bool\n\tportableS3SkipTLSVerify            bool\n\tportableGCSBucket                  string\n\tportableGCSCredentialsFile         string\n\tportableGCSAutoCredentials         int\n\tportableGCSStorageClass            string\n\tportableGCSKeyPrefix               string\n\tportableFTPDPort                   int\n\tportableFTPSCert                   string\n\tportableFTPSKey                    string\n\tportableWebDAVPort                 int\n\tportableWebDAVCert                 string\n\tportableWebDAVKey                  string\n\tportableHTTPPort                   int\n\tportableHTTPSCert                  string\n\tportableHTTPSKey                   string\n\tportableAzContainer                string\n\tportableAzAccountName              string\n\tportableAzAccountKey               string\n\tportableAzEndpoint                 string\n\tportableAzAccessTier               string\n\tportableAzSASURL                   string\n\tportableAzKeyPrefix                string\n\tportableAzULPartSize               int\n\tportableAzULConcurrency            int\n\tportableAzDLPartSize               int\n\tportableAzDLConcurrency            int\n\tportableAzUseEmulator              bool\n\tportableCryptPassphrase            string\n\tportableSFTPEndpoint               string\n\tportableSFTPUsername               string\n\tportableSFTPPassword               string\n\tportableSFTPPrivateKeyPath         string\n\tportableSFTPFingerprints           []string\n\tportableSFTPPrefix                 string\n\tportableSFTPDisableConcurrentReads bool\n\tportableSFTPDBufferSize            int64\n\tportableCmd                        = &cobra.Command{\n\t\tUse:   \"portable\",\n\t\tShort: \"Serve a single directory/account\",\n\t\tLong: `To serve the current working directory with auto generated credentials simply\nuse:\n\n$ sftpgo portable\n\nPlease take a look at the usage below to customize the serving parameters`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tportableDir := directoryToServe\n\t\t\tfsProvider := dataprovider.GetProviderFromValue(convertFsProvider())\n\t\t\tif !filepath.IsAbs(portableDir) {\n\t\t\t\tif fsProvider == sdk.LocalFilesystemProvider {\n\t\t\t\t\tportableDir, _ = filepath.Abs(portableDir)\n\t\t\t\t} else {\n\t\t\t\t\tportableDir = os.TempDir()\n\t\t\t\t}\n\t\t\t}\n\t\t\tpermissions := make(map[string][]string)\n\t\t\tpermissions[\"/\"] = portablePermissions\n\t\t\tportableGCSCredentials := \"\"\n\t\t\tif fsProvider == sdk.GCSFilesystemProvider && portableGCSCredentialsFile != \"\" {\n\t\t\t\tcontents, err := getFileContents(portableGCSCredentialsFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"Unable to get GCS credentials: %v\\n\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tportableGCSCredentials = contents\n\t\t\t\tportableGCSAutoCredentials = 0\n\t\t\t}\n\t\t\tportableSFTPPrivateKey := \"\"\n\t\t\tif fsProvider == sdk.SFTPFilesystemProvider && portableSFTPPrivateKeyPath != \"\" {\n\t\t\t\tcontents, err := getFileContents(portableSFTPPrivateKeyPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"Unable to get SFTP private key: %v\\n\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tportableSFTPPrivateKey = contents\n\t\t\t}\n\t\t\tif portableFTPDPort >= 0 && portableFTPSCert != \"\" && portableFTPSKey != \"\" {\n\t\t\t\tkeyPairs := []common.TLSKeyPair{\n\t\t\t\t\t{\n\t\t\t\t\t\tCert: portableFTPSCert,\n\t\t\t\t\t\tKey:  portableFTPSKey,\n\t\t\t\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\t_, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),\n\t\t\t\t\t\"FTP portable\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"Unable to load FTPS key pair, cert file %q key file %q error: %v\\n\",\n\t\t\t\t\t\tportableFTPSCert, portableFTPSKey, err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif portableWebDAVPort >= 0 && portableWebDAVCert != \"\" && portableWebDAVKey != \"\" {\n\t\t\t\tkeyPairs := []common.TLSKeyPair{\n\t\t\t\t\t{\n\t\t\t\t\t\tCert: portableWebDAVCert,\n\t\t\t\t\t\tKey:  portableWebDAVKey,\n\t\t\t\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\t_, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),\n\t\t\t\t\t\"WebDAV portable\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"Unable to load WebDAV key pair, cert file %q key file %q error: %v\\n\",\n\t\t\t\t\t\tportableWebDAVCert, portableWebDAVKey, err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif portableHTTPPort >= 0 && portableHTTPSCert != \"\" && portableHTTPSKey != \"\" {\n\t\t\t\tkeyPairs := []common.TLSKeyPair{\n\t\t\t\t\t{\n\t\t\t\t\t\tCert: portableHTTPSCert,\n\t\t\t\t\t\tKey:  portableHTTPSKey,\n\t\t\t\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\t_, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),\n\t\t\t\t\t\"HTTP portable\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"Unable to load HTTPS key pair, cert file %q key file %q error: %v\\n\",\n\t\t\t\t\t\tportableHTTPSCert, portableHTTPSKey, err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tpwd := portablePassword\n\t\t\tif portablePasswordFile != \"\" {\n\t\t\t\tcontent, err := os.ReadFile(portablePasswordFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"Unable to read password file %q: %v\", portablePasswordFile, err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tpwd = strings.TrimSpace(util.BytesToString(content))\n\t\t\t}\n\t\t\tservice.SetGraceTime(graceTime)\n\t\t\tservice := service.Service{\n\t\t\t\tConfigDir:     util.CleanDirInput(configDir),\n\t\t\t\tConfigFile:    configFile,\n\t\t\t\tLogFilePath:   portableLogFile,\n\t\t\t\tLogMaxSize:    defaultLogMaxSize,\n\t\t\t\tLogMaxBackups: defaultLogMaxBackup,\n\t\t\t\tLogMaxAge:     defaultLogMaxAge,\n\t\t\t\tLogCompress:   defaultLogCompress,\n\t\t\t\tLogLevel:      portableLogLevel,\n\t\t\t\tLogUTCTime:    portableLogUTCTime,\n\t\t\t\tShutdown:      make(chan bool),\n\t\t\t\tPortableMode:  1,\n\t\t\t\tPortableUser: dataprovider.User{\n\t\t\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\t\t\tUsername:    portableUsername,\n\t\t\t\t\t\tPassword:    pwd,\n\t\t\t\t\t\tPublicKeys:  portablePublicKeys,\n\t\t\t\t\t\tPermissions: permissions,\n\t\t\t\t\t\tHomeDir:     portableDir,\n\t\t\t\t\t\tStatus:      1,\n\t\t\t\t\t},\n\t\t\t\t\tFilters: dataprovider.UserFilters{\n\t\t\t\t\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\t\t\t\t\tFilePatterns:   parsePatternsFilesFilters(),\n\t\t\t\t\t\t\tStartDirectory: portableStartDir,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\t\t\tProvider: fsProvider,\n\t\t\t\t\t\tS3Config: vfs.S3FsConfig{\n\t\t\t\t\t\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\t\t\t\t\t\tBucket:            portableS3Bucket,\n\t\t\t\t\t\t\t\tRegion:            portableS3Region,\n\t\t\t\t\t\t\t\tAccessKey:         portableS3AccessKey,\n\t\t\t\t\t\t\t\tRoleARN:           portableS3RoleARN,\n\t\t\t\t\t\t\t\tEndpoint:          portableS3Endpoint,\n\t\t\t\t\t\t\t\tStorageClass:      portableS3StorageClass,\n\t\t\t\t\t\t\t\tACL:               portableS3ACL,\n\t\t\t\t\t\t\t\tKeyPrefix:         portableS3KeyPrefix,\n\t\t\t\t\t\t\t\tUploadPartSize:    int64(portableS3ULPartSize),\n\t\t\t\t\t\t\t\tUploadConcurrency: portableS3ULConcurrency,\n\t\t\t\t\t\t\t\tForcePathStyle:    portableS3ForcePathStyle,\n\t\t\t\t\t\t\t\tSkipTLSVerify:     portableS3SkipTLSVerify,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAccessSecret: kms.NewPlainSecret(portableS3AccessSecret),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tGCSConfig: vfs.GCSFsConfig{\n\t\t\t\t\t\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\t\t\t\t\t\tBucket:               portableGCSBucket,\n\t\t\t\t\t\t\t\tAutomaticCredentials: portableGCSAutoCredentials,\n\t\t\t\t\t\t\t\tStorageClass:         portableGCSStorageClass,\n\t\t\t\t\t\t\t\tKeyPrefix:            portableGCSKeyPrefix,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCredentials: kms.NewPlainSecret(portableGCSCredentials),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAzBlobConfig: vfs.AzBlobFsConfig{\n\t\t\t\t\t\t\tBaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{\n\t\t\t\t\t\t\t\tContainer:           portableAzContainer,\n\t\t\t\t\t\t\t\tAccountName:         portableAzAccountName,\n\t\t\t\t\t\t\t\tEndpoint:            portableAzEndpoint,\n\t\t\t\t\t\t\t\tAccessTier:          portableAzAccessTier,\n\t\t\t\t\t\t\t\tKeyPrefix:           portableAzKeyPrefix,\n\t\t\t\t\t\t\t\tUseEmulator:         portableAzUseEmulator,\n\t\t\t\t\t\t\t\tUploadPartSize:      int64(portableAzULPartSize),\n\t\t\t\t\t\t\t\tUploadConcurrency:   portableAzULConcurrency,\n\t\t\t\t\t\t\t\tDownloadPartSize:    int64(portableAzDLPartSize),\n\t\t\t\t\t\t\t\tDownloadConcurrency: portableAzDLConcurrency,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAccountKey: kms.NewPlainSecret(portableAzAccountKey),\n\t\t\t\t\t\t\tSASURL:     kms.NewPlainSecret(portableAzSASURL),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\t\t\t\tPassphrase: kms.NewPlainSecret(portableCryptPassphrase),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\t\t\t\tEndpoint:                portableSFTPEndpoint,\n\t\t\t\t\t\t\t\tUsername:                portableSFTPUsername,\n\t\t\t\t\t\t\t\tFingerprints:            portableSFTPFingerprints,\n\t\t\t\t\t\t\t\tPrefix:                  portableSFTPPrefix,\n\t\t\t\t\t\t\t\tDisableCouncurrentReads: portableSFTPDisableConcurrentReads,\n\t\t\t\t\t\t\t\tBufferSize:              portableSFTPDBufferSize,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPassword:      kms.NewPlainSecret(portableSFTPPassword),\n\t\t\t\t\t\t\tPrivateKey:    kms.NewPlainSecret(portableSFTPPrivateKey),\n\t\t\t\t\t\t\tKeyPassphrase: kms.NewEmptySecret(),\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\terr := service.StartPortableMode(portableSFTPDPort, portableFTPDPort, portableWebDAVPort, portableHTTPPort,\n\t\t\t\tportableSSHCommands, portableFTPSCert, portableFTPSKey, portableWebDAVCert, portableWebDAVKey,\n\t\t\t\tportableHTTPSCert, portableHTTPSKey)\n\t\t\tif err == nil {\n\t\t\t\tservice.Wait()\n\t\t\t\tif service.Error == nil {\n\t\t\t\t\tos.Exit(0)\n\t\t\t\t}\n\t\t\t}\n\t\t\tos.Exit(1)\n\t\t},\n\t}\n)\n\nfunc init() {\n\tversion.AddFeature(\"+portable\")\n\n\tportableCmd.Flags().StringVarP(&directoryToServe, \"directory\", \"d\", \".\", `Path to the directory to serve.\nThis can be an absolute path or a path\nrelative to the current directory\n`)\n\tportableCmd.Flags().StringVar(&portableStartDir, \"start-directory\", \"/\", `Alternate start directory.\nThis is a virtual path not a filesystem\npath`)\n\tportableCmd.Flags().IntVarP(&portableSFTPDPort, \"sftpd-port\", \"s\", 0, `0 means a random unprivileged port,\n< 0 disabled`)\n\tportableCmd.Flags().IntVar(&portableFTPDPort, \"ftpd-port\", -1, `0 means a random unprivileged port,\n< 0 disabled`)\n\tportableCmd.Flags().IntVar(&portableWebDAVPort, \"webdav-port\", -1, `0 means a random unprivileged port,\n< 0 disabled`)\n\tportableCmd.Flags().IntVar(&portableHTTPPort, \"httpd-port\", -1, `0 means a random unprivileged port,\n< 0 disabled`)\n\tportableCmd.Flags().StringSliceVar(&portableSSHCommands, \"ssh-commands\", sftpd.GetDefaultSSHCommands(),\n\t\t`SSH commands to enable.\n\"*\" means any supported SSH command\nincluding scp\n`)\n\tportableCmd.Flags().StringVarP(&portableUsername, \"username\", \"u\", \"\", `Leave empty to use an auto generated\nvalue`)\n\tportableCmd.Flags().StringVarP(&portablePassword, \"password\", \"p\", \"\", `Leave empty to use an auto generated\nvalue`)\n\tportableCmd.Flags().StringVar(&portablePasswordFile, \"password-file\", \"\", `Read the password from the specified\nfile path. Leave empty to use an auto\ngenerated value`)\n\tportableCmd.Flags().StringVarP(&portableLogFile, logFilePathFlag, \"l\", \"\", \"Leave empty to disable logging\")\n\tportableCmd.Flags().StringVar(&portableLogLevel, logLevelFlag, defaultLogLevel, `Set the log level.\nSupported values:\n\ndebug, info, warn, error.\n`)\n\tportableCmd.Flags().BoolVar(&portableLogUTCTime, logUTCTimeFlag, false, \"Use UTC time for logging\")\n\tportableCmd.Flags().StringSliceVarP(&portablePublicKeys, \"public-key\", \"k\", []string{}, \"\")\n\tportableCmd.Flags().StringSliceVarP(&portablePermissions, \"permissions\", \"g\", []string{\"list\", \"download\"},\n\t\t`User's permissions. \"*\" means any\npermission`)\n\tportableCmd.Flags().StringArrayVar(&portableAllowedPatterns, \"allowed-patterns\", []string{},\n\t\t`Allowed file patterns case insensitive.\nThe format is:\n/dir::pattern1,pattern2.\nFor example: \"/somedir::*.jpg,a*b?.png\"`)\n\tportableCmd.Flags().StringArrayVar(&portableDeniedPatterns, \"denied-patterns\", []string{},\n\t\t`Denied file patterns case insensitive.\nThe format is:\n/dir::pattern1,pattern2.\nFor example: \"/somedir::*.jpg,a*b?.png\"`)\n\tportableCmd.Flags().StringVarP(&portableFsProvider, \"fs-provider\", \"f\", \"osfs\", `osfs => local filesystem (legacy value: 0)\ns3fs => AWS S3 compatible (legacy: 1)\ngcsfs => Google Cloud Storage (legacy: 2)\nazblobfs => Azure Blob Storage (legacy: 3)\ncryptfs => Encrypted local filesystem (legacy: 4)\nsftpfs => SFTP (legacy: 5)`)\n\tportableCmd.Flags().StringVar(&portableS3Bucket, \"s3-bucket\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableS3Region, \"s3-region\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableS3AccessKey, \"s3-access-key\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableS3AccessSecret, \"s3-access-secret\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableS3RoleARN, \"s3-role-arn\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableS3Endpoint, \"s3-endpoint\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableS3StorageClass, \"s3-storage-class\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableS3ACL, \"s3-acl\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableS3KeyPrefix, \"s3-key-prefix\", \"\", `Allows to restrict access to the\nvirtual folder identified by this\nprefix and its contents`)\n\tportableCmd.Flags().IntVar(&portableS3ULPartSize, \"s3-upload-part-size\", 5, `The buffer size for multipart uploads\n(MB)`)\n\tportableCmd.Flags().IntVar(&portableS3ULConcurrency, \"s3-upload-concurrency\", 2, `How many parts are uploaded in\nparallel`)\n\tportableCmd.Flags().BoolVar(&portableS3ForcePathStyle, \"s3-force-path-style\", false, `Force path style bucket URL`)\n\tportableCmd.Flags().BoolVar(&portableS3SkipTLSVerify, \"s3-skip-tls-verify\", false, `If enabled the S3 client accepts any TLS\ncertificate presented by the server and\nany host name in that certificate.\nIn this mode, TLS is susceptible to\nman-in-the-middle attacks.\nThis should be used only for testing.\n`)\n\tportableCmd.Flags().StringVar(&portableGCSBucket, \"gcs-bucket\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableGCSStorageClass, \"gcs-storage-class\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableGCSKeyPrefix, \"gcs-key-prefix\", \"\", `Allows to restrict access to the\nvirtual folder identified by this\nprefix and its contents`)\n\tportableCmd.Flags().StringVar(&portableGCSCredentialsFile, \"gcs-credentials-file\", \"\", `Google Cloud Storage JSON credentials\nfile`)\n\tportableCmd.Flags().IntVar(&portableGCSAutoCredentials, \"gcs-automatic-credentials\", 1, `0 means explicit credentials using\na JSON credentials file, 1 automatic\n`)\n\tportableCmd.Flags().StringVar(&portableFTPSCert, \"ftpd-cert\", \"\", \"Path to the certificate file for FTPS\")\n\tportableCmd.Flags().StringVar(&portableFTPSKey, \"ftpd-key\", \"\", \"Path to the key file for FTPS\")\n\tportableCmd.Flags().StringVar(&portableWebDAVCert, \"webdav-cert\", \"\", `Path to the certificate file for WebDAV\nover HTTPS`)\n\tportableCmd.Flags().StringVar(&portableWebDAVKey, \"webdav-key\", \"\", `Path to the key file for WebDAV over\nHTTPS`)\n\tportableCmd.Flags().StringVar(&portableHTTPSCert, \"httpd-cert\", \"\", `Path to the certificate file for WebClient\nover HTTPS`)\n\tportableCmd.Flags().StringVar(&portableHTTPSKey, \"httpd-key\", \"\", `Path to the key file for WebClient over\nHTTPS`)\n\tportableCmd.Flags().StringVar(&portableAzContainer, \"az-container\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableAzAccountName, \"az-account-name\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableAzAccountKey, \"az-account-key\", \"\", \"\")\n\tportableCmd.Flags().StringVar(&portableAzSASURL, \"az-sas-url\", \"\", `Shared access signature URL`)\n\tportableCmd.Flags().StringVar(&portableAzEndpoint, \"az-endpoint\", \"\", `Leave empty to use the default:\n\"blob.core.windows.net\"`)\n\tportableCmd.Flags().StringVar(&portableAzAccessTier, \"az-access-tier\", \"\", `Leave empty to use the default\ncontainer setting`)\n\tportableCmd.Flags().StringVar(&portableAzKeyPrefix, \"az-key-prefix\", \"\", `Allows to restrict access to the\nvirtual folder identified by this\nprefix and its contents`)\n\tportableCmd.Flags().IntVar(&portableAzULPartSize, \"az-upload-part-size\", 5, `The buffer size for multipart uploads\n(MB)`)\n\tportableCmd.Flags().IntVar(&portableAzULConcurrency, \"az-upload-concurrency\", 5, `How many parts are uploaded in\nparallel`)\n\tportableCmd.Flags().IntVar(&portableAzDLPartSize, \"az-download-part-size\", 5, `The buffer size for multipart downloads\n(MB)`)\n\tportableCmd.Flags().IntVar(&portableAzDLConcurrency, \"az-download-concurrency\", 5, `How many parts are downloaded in\nparallel`)\n\tportableCmd.Flags().BoolVar(&portableAzUseEmulator, \"az-use-emulator\", false, \"\")\n\tportableCmd.Flags().StringVar(&portableCryptPassphrase, \"crypto-passphrase\", \"\", `Passphrase for encryption/decryption`)\n\tportableCmd.Flags().StringVar(&portableSFTPEndpoint, \"sftp-endpoint\", \"\", `SFTP endpoint as host:port for SFTP\nprovider`)\n\tportableCmd.Flags().StringVar(&portableSFTPUsername, \"sftp-username\", \"\", `SFTP user for SFTP provider`)\n\tportableCmd.Flags().StringVar(&portableSFTPPassword, \"sftp-password\", \"\", `SFTP password for SFTP provider`)\n\tportableCmd.Flags().StringVar(&portableSFTPPrivateKeyPath, \"sftp-key-path\", \"\", `SFTP private key path for SFTP provider`)\n\tportableCmd.Flags().StringSliceVar(&portableSFTPFingerprints, \"sftp-fingerprints\", []string{}, `SFTP fingerprints to verify remote host\nkey for SFTP provider`)\n\tportableCmd.Flags().StringVar(&portableSFTPPrefix, \"sftp-prefix\", \"\", `SFTP prefix allows restrict all\noperations to a given path within the\nremote SFTP server`)\n\tportableCmd.Flags().BoolVar(&portableSFTPDisableConcurrentReads, \"sftp-disable-concurrent-reads\", false, `Concurrent reads are safe to use and\ndisabling them will degrade performance.\nDisable for read once servers`)\n\tportableCmd.Flags().Int64Var(&portableSFTPDBufferSize, \"sftp-buffer-size\", 0, `The size of the buffer (in MB) to use\nfor transfers. By enabling buffering,\nthe reads and writes, from/to the\nremote SFTP server, are split in\nmultiple concurrent requests and this\nallows data to be transferred at a\nfaster rate, over high latency networks,\nby overlapping round-trip times`)\n\tportableCmd.Flags().IntVar(&graceTime, graceTimeFlag, 0,\n\t\t`This grace time defines the number of\nseconds allowed for existing transfers\nto get completed before shutting down.\nA graceful shutdown is triggered by an\ninterrupt signal.\n`)\n\taddConfigFlags(portableCmd)\n\trootCmd.AddCommand(portableCmd)\n}\n\nfunc parsePatternsFilesFilters() []sdk.PatternsFilter {\n\tvar patterns []sdk.PatternsFilter\n\tfor _, val := range portableAllowedPatterns {\n\t\tp, exts := getPatternsFilterValues(strings.TrimSpace(val))\n\t\tif p != \"\" {\n\t\t\tpatterns = append(patterns, sdk.PatternsFilter{\n\t\t\t\tPath:            path.Clean(p),\n\t\t\t\tAllowedPatterns: exts,\n\t\t\t\tDeniedPatterns:  []string{},\n\t\t\t})\n\t\t}\n\t}\n\tfor _, val := range portableDeniedPatterns {\n\t\tp, exts := getPatternsFilterValues(strings.TrimSpace(val))\n\t\tif p != \"\" {\n\t\t\tfound := false\n\t\t\tfor index, e := range patterns {\n\t\t\t\tif path.Clean(e.Path) == path.Clean(p) {\n\t\t\t\t\tpatterns[index].DeniedPatterns = append(patterns[index].DeniedPatterns, exts...)\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tpatterns = append(patterns, sdk.PatternsFilter{\n\t\t\t\t\tPath:            path.Clean(p),\n\t\t\t\t\tAllowedPatterns: []string{},\n\t\t\t\t\tDeniedPatterns:  exts,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\treturn patterns\n}\n\nfunc getPatternsFilterValues(value string) (string, []string) {\n\tif strings.Contains(value, \"::\") {\n\t\tdirExts := strings.Split(value, \"::\")\n\t\tif len(dirExts) > 1 {\n\t\t\tdir := strings.TrimSpace(dirExts[0])\n\t\t\texts := []string{}\n\t\t\tfor e := range strings.SplitSeq(dirExts[1], \",\") {\n\t\t\t\tcleanedExt := strings.TrimSpace(e)\n\t\t\t\tif cleanedExt != \"\" {\n\t\t\t\t\texts = append(exts, cleanedExt)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif dir != \"\" && len(exts) > 0 {\n\t\t\t\treturn dir, exts\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", nil\n}\n\nfunc getFileContents(name string) (string, error) {\n\tfi, err := os.Stat(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif fi.Size() > 1048576 {\n\t\treturn \"\", fmt.Errorf(\"%q is too big %v/1048576 bytes\", name, fi.Size())\n\t}\n\tcontents, err := os.ReadFile(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn util.BytesToString(contents), nil\n}\n\nfunc convertFsProvider() string {\n\tswitch portableFsProvider {\n\tcase \"osfs\", \"6\": // httpfs (6) is not supported in portable mode, so return the default\n\t\treturn \"0\"\n\tcase \"s3fs\":\n\t\treturn \"1\"\n\tcase \"gcsfs\":\n\t\treturn \"2\"\n\tcase \"azblobfs\":\n\t\treturn \"3\"\n\tcase \"cryptfs\":\n\t\treturn \"4\"\n\tcase \"sftpfs\":\n\t\treturn \"5\"\n\tdefault:\n\t\treturn portableFsProvider\n\t}\n}\n"
  },
  {
    "path": "internal/cmd/portable_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build noportable\n\npackage cmd\n\nimport \"github.com/drakkan/sftpgo/v2/internal/version\"\n\nfunc init() {\n\tversion.AddFeature(\"-portable\")\n}\n"
  },
  {
    "path": "internal/cmd/reload_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n)\n\nvar (\n\treloadCmd = &cobra.Command{\n\t\tUse:   \"reload\",\n\t\tShort: \"Reload the SFTPGo Windows Service sending a \\\"paramchange\\\" request\",\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\ts := service.WindowsService{\n\t\t\t\tService: service.Service{\n\t\t\t\t\tShutdown: make(chan bool),\n\t\t\t\t},\n\t\t\t}\n\t\t\terr := s.Reload()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error sending reload signal: %v\\r\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Reload signal sent!\\r\\n\")\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\tserviceCmd.AddCommand(reloadCmd)\n}\n"
  },
  {
    "path": "internal/cmd/resetprovider.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tresetProviderForce bool\n\tresetProviderCmd   = &cobra.Command{\n\t\tUse:   \"resetprovider\",\n\t\tShort: \"Reset the configured provider, any data will be lost\",\n\t\tLong: `This command reads the data provider connection details from the specified\nconfiguration file and resets the provider by deleting all data and schemas.\nThis command is not supported for the memory provider.\n\nPlease take a look at the usage below to customize the options.`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tlogger.DisableLogger()\n\t\t\tlogger.EnableConsoleLogger(zerolog.DebugLevel)\n\t\t\tconfigDir = util.CleanDirInput(configDir)\n\t\t\terr := config.LoadConfig(configDir, configFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WarnToConsole(\"Unable to load configuration: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tkmsConfig := config.GetKMSConfig()\n\t\t\terr = kmsConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"unable to initialize KMS: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tproviderConf := config.GetProviderConf()\n\t\t\tif !resetProviderForce {\n\t\t\t\tlogger.WarnToConsole(\"You are about to delete all the SFTPGo data for provider %q, config file: %q\",\n\t\t\t\t\tproviderConf.Driver, viper.ConfigFileUsed())\n\t\t\t\tlogger.WarnToConsole(\"Are you sure? (Y/n)\")\n\t\t\t\treader := bufio.NewReader(os.Stdin)\n\t\t\t\tanswer, err := reader.ReadString('\\n')\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogger.ErrorToConsole(\"unable to read your answer: %v\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tif strings.ToUpper(strings.TrimSpace(answer)) != \"Y\" {\n\t\t\t\t\tlogger.InfoToConsole(\"command aborted\")\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tlogger.InfoToConsole(\"Resetting provider: %q, config file: %q\", providerConf.Driver, viper.ConfigFileUsed())\n\t\t\terr = dataprovider.ResetDatabase(providerConf, configDir)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WarnToConsole(\"Error resetting provider: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tlogger.InfoToConsole(\"Tha data provider was successfully reset\")\n\t\t},\n\t}\n)\n\nfunc init() {\n\taddConfigFlags(resetProviderCmd)\n\tresetProviderCmd.Flags().BoolVar(&resetProviderForce, \"force\", false, `reset the provider without asking for confirmation`)\n\n\trootCmd.AddCommand(resetProviderCmd)\n}\n"
  },
  {
    "path": "internal/cmd/resetpwd.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"golang.org/x/term\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tresetPwdAdmin string\n\tresetPwdCmd   = &cobra.Command{\n\t\tUse:   \"resetpwd\",\n\t\tShort: \"Reset the password for the specified administrator\",\n\t\tLong: `This command reads the data provider connection details from the specified\nconfiguration file and resets the password for the specified administrator.\nTwo-factor authentication is also disabled.\nThis command is not supported for the memory provider.\nFor embedded providers like bolt and SQLite you should stop the running SFTPGo\ninstance to avoid database corruption.\n\nPlease take a look at the usage below to customize the options.`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tlogger.DisableLogger()\n\t\t\tlogger.EnableConsoleLogger(zerolog.DebugLevel)\n\t\t\tconfigDir = util.CleanDirInput(configDir)\n\t\t\terr := config.LoadConfig(configDir, configFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WarnToConsole(\"Unable to load configuration: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tkmsConfig := config.GetKMSConfig()\n\t\t\terr = kmsConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"unable to initialize KMS: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tif config.HasKMSPlugin() {\n\t\t\t\tif err := plugin.Initialize(config.GetPluginsConfig(), \"debug\"); err != nil {\n\t\t\t\t\tlogger.ErrorToConsole(\"unable to initialize plugin system: %v\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tregisterSignals()\n\t\t\t\tdefer plugin.Handler.Cleanup()\n\t\t\t}\n\n\t\t\tmfaConfig := config.GetMFAConfig()\n\t\t\terr = mfaConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize MFA: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tproviderConf := config.GetProviderConf()\n\t\t\tif providerConf.Driver == dataprovider.MemoryDataProviderName {\n\t\t\t\tlogger.ErrorToConsole(\"memory provider is not supported\")\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tlogger.InfoToConsole(\"Initializing provider: %q config file: %q\", providerConf.Driver, viper.ConfigFileUsed())\n\t\t\terr = dataprovider.Initialize(providerConf, configDir, false)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize data provider: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tadmin, err := dataprovider.AdminExists(resetPwdAdmin)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to get admin %q: %v\", resetPwdAdmin, err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tfmt.Printf(\"Enter Password: \")\n\t\t\tpwd, err := term.ReadPassword(int(os.Stdin.Fd()))\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to read the password: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tfmt.Println(\"\")\n\t\t\tfmt.Printf(\"Confirm Password: \")\n\t\t\tconfirmPwd, err := term.ReadPassword(int(os.Stdin.Fd()))\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to read the password: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tfmt.Println(\"\")\n\t\t\tif !bytes.Equal(pwd, confirmPwd) {\n\t\t\t\tlogger.ErrorToConsole(\"Passwords do not match\")\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tadmin.Password = string(pwd)\n\t\t\tadmin.Filters.TOTPConfig.Enabled = false\n\t\t\tif err := dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSystem, \"\", \"\"); err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to update password: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tlogger.InfoToConsole(\"Password updated for admin %q\", resetPwdAdmin)\n\t\t},\n\t}\n)\n\nfunc init() {\n\taddConfigFlags(resetPwdCmd)\n\tresetPwdCmd.Flags().StringVar(&resetPwdAdmin, \"admin\", \"\", `Administrator username whose password to reset`)\n\tresetPwdCmd.MarkFlagRequired(\"admin\") //nolint:errcheck\n\n\trootCmd.AddCommand(resetPwdCmd)\n}\n"
  },
  {
    "path": "internal/cmd/revertprovider.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\trevertProviderTargetVersion int\n\trevertProviderCmd           = &cobra.Command{\n\t\tUse:   \"revertprovider\",\n\t\tShort: \"Revert the configured data provider to a previous version\",\n\t\tLong: `This command reads the data provider connection details from the specified\nconfiguration file and restore the provider schema and/or data to a previous version.\nThis command is not supported for the memory provider.\n\nPlease take a look at the usage below to customize the options.`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tlogger.DisableLogger()\n\t\t\tlogger.EnableConsoleLogger(zerolog.DebugLevel)\n\t\t\tif revertProviderTargetVersion != 33 {\n\t\t\t\tlogger.WarnToConsole(\"Unsupported target version, 33 is the only supported one\")\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tconfigDir = util.CleanDirInput(configDir)\n\t\t\terr := config.LoadConfig(configDir, configFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WarnToConsole(\"Unable to load configuration: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tkmsConfig := config.GetKMSConfig()\n\t\t\terr = kmsConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"unable to initialize KMS: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tif config.HasKMSPlugin() {\n\t\t\t\tif err := plugin.Initialize(config.GetPluginsConfig(), \"debug\"); err != nil {\n\t\t\t\t\tlogger.ErrorToConsole(\"unable to initialize plugin system: %v\", err)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t\tregisterSignals()\n\t\t\t\tdefer plugin.Handler.Cleanup()\n\t\t\t}\n\n\t\t\tmfaConfig := config.GetMFAConfig()\n\t\t\terr = mfaConfig.Initialize()\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to initialize MFA: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tproviderConf := config.GetProviderConf()\n\t\t\tlogger.InfoToConsole(\"Reverting provider: %q config file: %q target version %d\", providerConf.Driver,\n\t\t\t\tviper.ConfigFileUsed(), revertProviderTargetVersion)\n\t\t\terr = dataprovider.RevertDatabase(providerConf, configDir, revertProviderTargetVersion)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WarnToConsole(\"Error reverting provider: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tlogger.InfoToConsole(\"Data provider successfully reverted\")\n\t\t},\n\t}\n)\n\nfunc init() {\n\taddConfigFlags(revertProviderCmd)\n\trevertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, \"to-version\", 33, `33 means the version supported in v2.7.x`)\n\n\trootCmd.AddCommand(revertProviderCmd)\n}\n"
  },
  {
    "path": "internal/cmd/root.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package cmd provides Command Line Interface support\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\tconfigDirFlag            = \"config-dir\"\n\tconfigDirKey             = \"config_dir\"\n\tconfigFileFlag           = \"config-file\"\n\tconfigFileKey            = \"config_file\"\n\tlogFilePathFlag          = \"log-file-path\"\n\tlogFilePathKey           = \"log_file_path\"\n\tlogMaxSizeFlag           = \"log-max-size\"\n\tlogMaxSizeKey            = \"log_max_size\"\n\tlogMaxBackupFlag         = \"log-max-backups\"\n\tlogMaxBackupKey          = \"log_max_backups\"\n\tlogMaxAgeFlag            = \"log-max-age\"\n\tlogMaxAgeKey             = \"log_max_age\"\n\tlogCompressFlag          = \"log-compress\"\n\tlogCompressKey           = \"log_compress\"\n\tlogLevelFlag             = \"log-level\"\n\tlogLevelKey              = \"log_level\"\n\tlogUTCTimeFlag           = \"log-utc-time\"\n\tlogUTCTimeKey            = \"log_utc_time\"\n\tloadDataFromFlag         = \"loaddata-from\"\n\tloadDataFromKey          = \"loaddata_from\"\n\tloadDataModeFlag         = \"loaddata-mode\"\n\tloadDataModeKey          = \"loaddata_mode\"\n\tloadDataQuotaScanFlag    = \"loaddata-scan\"\n\tloadDataQuotaScanKey     = \"loaddata_scan\"\n\tloadDataCleanFlag        = \"loaddata-clean\"\n\tloadDataCleanKey         = \"loaddata_clean\"\n\tgraceTimeFlag            = \"grace-time\"\n\tgraceTimeKey             = \"grace_time\"\n\tdefaultConfigDir         = \".\"\n\tdefaultConfigFile        = \"\"\n\tdefaultLogFile           = \"sftpgo.log\"\n\tdefaultLogMaxSize        = 10\n\tdefaultLogMaxBackup      = 5\n\tdefaultLogMaxAge         = 28\n\tdefaultLogCompress       = false\n\tdefaultLogLevel          = \"debug\"\n\tdefaultLogUTCTime        = false\n\tdefaultLoadDataFrom      = \"\"\n\tdefaultLoadDataMode      = 1\n\tdefaultLoadDataQuotaScan = 0\n\tdefaultLoadDataClean     = false\n\tdefaultGraceTime         = 0\n)\n\nvar (\n\tconfigDir         string\n\tconfigFile        string\n\tlogFilePath       string\n\tlogMaxSize        int\n\tlogMaxBackups     int\n\tlogMaxAge         int\n\tlogCompress       bool\n\tlogLevel          string\n\tlogUTCTime        bool\n\tloadDataFrom      string\n\tloadDataMode      int\n\tloadDataQuotaScan int\n\tloadDataClean     bool\n\tgraceTime         int\n\n\trootCmd = &cobra.Command{\n\t\tUse:   \"sftpgo\",\n\t\tShort: \"Full-featured and highly configurable file transfer server\",\n\t}\n)\n\nfunc init() {\n\trootCmd.CompletionOptions.DisableDefaultCmd = true\n\trootCmd.Flags().BoolP(\"version\", \"v\", false, \"\")\n\trootCmd.Version = version.GetAsString()\n\trootCmd.SetVersionTemplate(`{{printf \"SFTPGo \"}}{{printf \"%s\" .Version}}\n`)\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc addConfigFlags(cmd *cobra.Command) {\n\tviper.SetDefault(configDirKey, defaultConfigDir)\n\tviper.BindEnv(configDirKey, \"SFTPGO_CONFIG_DIR\") //nolint:errcheck // err is not nil only if the key to bind is missing\n\tcmd.Flags().StringVarP(&configDir, configDirFlag, \"c\", viper.GetString(configDirKey),\n\t\t`Location of the config dir. This directory\nis used as the base for files with a relative\npath, e.g. the private keys for the SFTP\nserver or the database file if you use a\nfile-based data provider.\nThe configuration file, if not explicitly set,\nis looked for in this dir. We support reading\nfrom JSON, TOML, YAML, HCL, envfile and Java\nproperties config files. The default config\nfile name is \"sftpgo\" and therefore\n\"sftpgo.json\", \"sftpgo.yaml\" and so on are\nsearched.\nThis flag can be set using SFTPGO_CONFIG_DIR\nenv var too.`)\n\tviper.BindPFlag(configDirKey, cmd.Flags().Lookup(configDirFlag)) //nolint:errcheck\n\n\tviper.SetDefault(configFileKey, defaultConfigFile)\n\tviper.BindEnv(configFileKey, \"SFTPGO_CONFIG_FILE\") //nolint:errcheck\n\tcmd.Flags().StringVar(&configFile, configFileFlag, viper.GetString(configFileKey),\n\t\t`Path to SFTPGo configuration file.\nThis flag explicitly defines the path, name\nand extension of the config file. If must be\nan absolute path or a path relative to the\nconfiguration directory. The specified file\nname must have a supported extension (JSON,\nYAML, TOML, HCL or Java properties).\nThis flag can be set using SFTPGO_CONFIG_FILE\nenv var too.`)\n\tviper.BindPFlag(configFileKey, cmd.Flags().Lookup(configFileFlag)) //nolint:errcheck\n}\n\nfunc addBaseLoadDataFlags(cmd *cobra.Command) {\n\tviper.SetDefault(loadDataFromKey, defaultLoadDataFrom)\n\tviper.BindEnv(loadDataFromKey, \"SFTPGO_LOADDATA_FROM\") //nolint:errcheck\n\tcmd.Flags().StringVar(&loadDataFrom, loadDataFromFlag, viper.GetString(loadDataFromKey),\n\t\t`Load users and folders from this file.\nThe file must be specified as absolute path\nand it must contain a backup obtained using\nthe \"dumpdata\" REST API or compatible content.\nThis flag can be set using SFTPGO_LOADDATA_FROM\nenv var too.\n`)\n\tviper.BindPFlag(loadDataFromKey, cmd.Flags().Lookup(loadDataFromFlag)) //nolint:errcheck\n\n\tviper.SetDefault(loadDataModeKey, defaultLoadDataMode)\n\tviper.BindEnv(loadDataModeKey, \"SFTPGO_LOADDATA_MODE\") //nolint:errcheck\n\tcmd.Flags().IntVar(&loadDataMode, loadDataModeFlag, viper.GetInt(loadDataModeKey),\n\t\t`Restore mode for data to load:\n  0 - new users are added, existing users are\n      updated\n  1 - New users are added, existing users are\n\t  not modified\nThis flag can be set using SFTPGO_LOADDATA_MODE\nenv var too.\n`)\n\tviper.BindPFlag(loadDataModeKey, cmd.Flags().Lookup(loadDataModeFlag)) //nolint:errcheck\n\n\tviper.SetDefault(loadDataCleanKey, defaultLoadDataClean)\n\tviper.BindEnv(loadDataCleanKey, \"SFTPGO_LOADDATA_CLEAN\") //nolint:errcheck\n\tcmd.Flags().BoolVar(&loadDataClean, loadDataCleanFlag, viper.GetBool(loadDataCleanKey),\n\t\t`Determine if the loaddata-from file should\nbe removed after a successful load. This flag\ncan be set using SFTPGO_LOADDATA_CLEAN env var\ntoo. (default \"false\")\n`)\n\tviper.BindPFlag(loadDataCleanKey, cmd.Flags().Lookup(loadDataCleanFlag)) //nolint:errcheck\n}\n\nfunc addServeFlags(cmd *cobra.Command) {\n\taddConfigFlags(cmd)\n\n\tviper.SetDefault(logFilePathKey, defaultLogFile)\n\tviper.BindEnv(logFilePathKey, \"SFTPGO_LOG_FILE_PATH\") //nolint:errcheck\n\tcmd.Flags().StringVarP(&logFilePath, logFilePathFlag, \"l\", viper.GetString(logFilePathKey),\n\t\t`Location for the log file. Leave empty to write\nlogs to the standard output. This flag can be\nset using SFTPGO_LOG_FILE_PATH env var too.\n`)\n\tviper.BindPFlag(logFilePathKey, cmd.Flags().Lookup(logFilePathFlag)) //nolint:errcheck\n\n\tviper.SetDefault(logMaxSizeKey, defaultLogMaxSize)\n\tviper.BindEnv(logMaxSizeKey, \"SFTPGO_LOG_MAX_SIZE\") //nolint:errcheck\n\tcmd.Flags().IntVarP(&logMaxSize, logMaxSizeFlag, \"s\", viper.GetInt(logMaxSizeKey),\n\t\t`Maximum size in megabytes of the log file\nbefore it gets rotated. This flag can be set\nusing SFTPGO_LOG_MAX_SIZE env var too. It is\nunused if log-file-path is empty.\n`)\n\tviper.BindPFlag(logMaxSizeKey, cmd.Flags().Lookup(logMaxSizeFlag)) //nolint:errcheck\n\n\tviper.SetDefault(logMaxBackupKey, defaultLogMaxBackup)\n\tviper.BindEnv(logMaxBackupKey, \"SFTPGO_LOG_MAX_BACKUPS\") //nolint:errcheck\n\tcmd.Flags().IntVarP(&logMaxBackups, \"log-max-backups\", \"b\", viper.GetInt(logMaxBackupKey),\n\t\t`Maximum number of old log files to retain.\nThis flag can be set using SFTPGO_LOG_MAX_BACKUPS\nenv var too. It is unused if log-file-path is\nempty.`)\n\tviper.BindPFlag(logMaxBackupKey, cmd.Flags().Lookup(logMaxBackupFlag)) //nolint:errcheck\n\n\tviper.SetDefault(logMaxAgeKey, defaultLogMaxAge)\n\tviper.BindEnv(logMaxAgeKey, \"SFTPGO_LOG_MAX_AGE\") //nolint:errcheck\n\tcmd.Flags().IntVarP(&logMaxAge, \"log-max-age\", \"a\", viper.GetInt(logMaxAgeKey),\n\t\t`Maximum number of days to retain old log files.\nThis flag can be set using SFTPGO_LOG_MAX_AGE env\nvar too. It is unused if log-file-path is empty.\n`)\n\tviper.BindPFlag(logMaxAgeKey, cmd.Flags().Lookup(logMaxAgeFlag)) //nolint:errcheck\n\n\tviper.SetDefault(logCompressKey, defaultLogCompress)\n\tviper.BindEnv(logCompressKey, \"SFTPGO_LOG_COMPRESS\") //nolint:errcheck\n\tcmd.Flags().BoolVarP(&logCompress, logCompressFlag, \"z\", viper.GetBool(logCompressKey),\n\t\t`Determine if the rotated log files\nshould be compressed using gzip. This flag can\nbe set using SFTPGO_LOG_COMPRESS env var too.\nIt is unused if log-file-path is empty.\n`)\n\tviper.BindPFlag(logCompressKey, cmd.Flags().Lookup(logCompressFlag)) //nolint:errcheck\n\n\tviper.SetDefault(logLevelKey, defaultLogLevel)\n\tviper.BindEnv(logLevelKey, \"SFTPGO_LOG_LEVEL\") //nolint:errcheck\n\tcmd.Flags().StringVar(&logLevel, logLevelFlag, viper.GetString(logLevelKey),\n\t\t`Set the log level. Supported values:\n\ndebug, info, warn, error.\n\nThis flag can be set\nusing SFTPGO_LOG_LEVEL env var too.\n`)\n\tviper.BindPFlag(logLevelKey, cmd.Flags().Lookup(logLevelFlag)) //nolint:errcheck\n\n\tviper.SetDefault(logUTCTimeKey, defaultLogUTCTime)\n\tviper.BindEnv(logUTCTimeKey, \"SFTPGO_LOG_UTC_TIME\") //nolint:errcheck\n\tcmd.Flags().BoolVar(&logUTCTime, logUTCTimeFlag, viper.GetBool(logUTCTimeKey),\n\t\t`Use UTC time for logging. This flag can be set\nusing SFTPGO_LOG_UTC_TIME env var too.\n`)\n\tviper.BindPFlag(logUTCTimeKey, cmd.Flags().Lookup(logUTCTimeFlag)) //nolint:errcheck\n\n\taddBaseLoadDataFlags(cmd)\n\n\tviper.SetDefault(loadDataQuotaScanKey, defaultLoadDataQuotaScan)\n\tviper.BindEnv(loadDataQuotaScanKey, \"SFTPGO_LOADDATA_QUOTA_SCAN\") //nolint:errcheck\n\tcmd.Flags().IntVar(&loadDataQuotaScan, loadDataQuotaScanFlag, viper.GetInt(loadDataQuotaScanKey),\n\t\t`Quota scan mode after data load:\n  0 - no quota scan\n  1 - scan quota\n  2 - scan quota if the user has quota restrictions\nThis flag can be set using SFTPGO_LOADDATA_QUOTA_SCAN\nenv var too.\n(default 0)`)\n\tviper.BindPFlag(loadDataQuotaScanKey, cmd.Flags().Lookup(loadDataQuotaScanFlag)) //nolint:errcheck\n\n\tviper.SetDefault(graceTimeKey, defaultGraceTime)\n\tviper.BindEnv(graceTimeKey, \"SFTPGO_GRACE_TIME\") //nolint:errcheck\n\tcmd.Flags().IntVar(&graceTime, graceTimeFlag, viper.GetInt(graceTimeKey),\n\t\t`Graceful shutdown is an option to initiate a\nshutdown without abrupt cancellation of the\ncurrently ongoing client-initiated transfer\nsessions.\nThis grace time defines the number of seconds\nallowed for existing transfers to get\ncompleted before shutting down.\nA graceful shutdown is triggered by an\ninterrupt signal.\nThis flag can be set using SFTPGO_GRACE_TIME env\nvar too. 0 means disabled. (default 0)`)\n\tviper.BindPFlag(graceTimeKey, cmd.Flags().Lookup(graceTimeFlag)) //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/cmd/rotatelogs_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n)\n\nvar (\n\trotateLogCmd = &cobra.Command{\n\t\tUse:   \"rotatelogs\",\n\t\tShort: \"Signal to the running service to rotate the logs\",\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\ts := service.WindowsService{\n\t\t\t\tService: service.Service{\n\t\t\t\t\tShutdown: make(chan bool),\n\t\t\t\t},\n\t\t\t}\n\t\t\terr := s.RotateLogFile()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error sending rotate log file signal to the service: %v\\r\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Rotate log file signal sent!\\r\\n\")\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\tserviceCmd.AddCommand(rotateLogCmd)\n}\n"
  },
  {
    "path": "internal/cmd/serve.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/subosito/gotenv\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\tenvFileMaxSize = 1048576\n)\n\nvar (\n\tserveCmd = &cobra.Command{\n\t\tUse:   \"serve\",\n\t\tShort: \"Start the SFTPGo service\",\n\t\tLong: `To start the SFTPGo with the default values for the command line flags simply\nuse:\n\n$ sftpgo serve\n\nPlease take a look at the usage below to customize the startup options`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tconfigDir := util.CleanDirInput(configDir)\n\t\t\tcheckServeParamsFromEnvFiles(configDir)\n\t\t\tservice.SetGraceTime(graceTime)\n\t\t\tservice := service.Service{\n\t\t\t\tConfigDir:         configDir,\n\t\t\t\tConfigFile:        configFile,\n\t\t\t\tLogFilePath:       logFilePath,\n\t\t\t\tLogMaxSize:        logMaxSize,\n\t\t\t\tLogMaxBackups:     logMaxBackups,\n\t\t\t\tLogMaxAge:         logMaxAge,\n\t\t\t\tLogCompress:       logCompress,\n\t\t\t\tLogLevel:          logLevel,\n\t\t\t\tLogUTCTime:        logUTCTime,\n\t\t\t\tLoadDataFrom:      loadDataFrom,\n\t\t\t\tLoadDataMode:      loadDataMode,\n\t\t\t\tLoadDataQuotaScan: loadDataQuotaScan,\n\t\t\t\tLoadDataClean:     loadDataClean,\n\t\t\t\tShutdown:          make(chan bool),\n\t\t\t}\n\t\t\tif err := service.Start(); err == nil {\n\t\t\t\tservice.Wait()\n\t\t\t\tif service.Error == nil {\n\t\t\t\t\tos.Exit(0)\n\t\t\t\t}\n\t\t\t}\n\t\t\tos.Exit(1)\n\t\t},\n\t}\n)\n\nfunc setIntFromEnv(receiver *int, val string) {\n\tconverted, err := strconv.Atoi(val)\n\tif err == nil {\n\t\t*receiver = converted\n\t}\n}\n\nfunc setBoolFromEnv(receiver *bool, val string) {\n\tconverted, err := strconv.ParseBool(strings.TrimSpace(val))\n\tif err == nil {\n\t\t*receiver = converted\n\t}\n}\n\nfunc checkServeParamsFromEnvFiles(configDir string) { //nolint:gocyclo\n\t// The logger is not yet initialized here, we have no way to report errors.\n\tenvd := filepath.Join(configDir, \"env.d\")\n\tentries, err := os.ReadDir(envd)\n\tif err != nil {\n\t\treturn\n\t}\n\tfor _, entry := range entries {\n\t\tinfo, err := entry.Info()\n\t\tif err == nil && info.Mode().IsRegular() {\n\t\t\tenvFile := filepath.Join(envd, entry.Name())\n\t\t\tif info.Size() > envFileMaxSize {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tenvVars, err := gotenv.Read(envFile)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor k, v := range envVars {\n\t\t\t\tif _, isSet := os.LookupEnv(k); isSet {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tswitch k {\n\t\t\t\tcase \"SFTPGO_LOG_FILE_PATH\":\n\t\t\t\t\tlogFilePath = v\n\t\t\t\tcase \"SFTPGO_LOG_MAX_SIZE\":\n\t\t\t\t\tsetIntFromEnv(&logMaxSize, v)\n\t\t\t\tcase \"SFTPGO_LOG_MAX_BACKUPS\":\n\t\t\t\t\tsetIntFromEnv(&logMaxBackups, v)\n\t\t\t\tcase \"SFTPGO_LOG_MAX_AGE\":\n\t\t\t\t\tsetIntFromEnv(&logMaxAge, v)\n\t\t\t\tcase \"SFTPGO_LOG_COMPRESS\":\n\t\t\t\t\tsetBoolFromEnv(&logCompress, v)\n\t\t\t\tcase \"SFTPGO_LOG_LEVEL\":\n\t\t\t\t\tlogLevel = v\n\t\t\t\tcase \"SFTPGO_LOG_UTC_TIME\":\n\t\t\t\t\tsetBoolFromEnv(&logUTCTime, v)\n\t\t\t\tcase \"SFTPGO_CONFIG_FILE\":\n\t\t\t\t\tconfigFile = v\n\t\t\t\tcase \"SFTPGO_LOADDATA_FROM\":\n\t\t\t\t\tloadDataFrom = v\n\t\t\t\tcase \"SFTPGO_LOADDATA_MODE\":\n\t\t\t\t\tsetIntFromEnv(&loadDataMode, v)\n\t\t\t\tcase \"SFTPGO_LOADDATA_CLEAN\":\n\t\t\t\t\tsetBoolFromEnv(&loadDataClean, v)\n\t\t\t\tcase \"SFTPGO_LOADDATA_QUOTA_SCAN\":\n\t\t\t\t\tsetIntFromEnv(&loadDataQuotaScan, v)\n\t\t\t\tcase \"SFTPGO_GRACE_TIME\":\n\t\t\t\t\tsetIntFromEnv(&graceTime, v)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc init() {\n\trootCmd.AddCommand(serveCmd)\n\taddServeFlags(serveCmd)\n}\n"
  },
  {
    "path": "internal/cmd/service_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\tserviceCmd = &cobra.Command{\n\t\tUse:   \"service\",\n\t\tShort: \"Manage the SFTPGo Windows Service\",\n\t}\n)\n\nfunc init() {\n\trootCmd.AddCommand(serviceCmd)\n}\n"
  },
  {
    "path": "internal/cmd/signals_unix.go",
    "content": "// Copyright (C) 2025 Nicola Murino\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, version 3.\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\n//go:build !windows\n\npackage cmd\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n)\n\nfunc registerSignals() {\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, syscall.SIGINT, syscall.SIGTERM)\n\tgo func() {\n\t\tfor sig := range c {\n\t\t\tswitch sig {\n\t\t\tcase syscall.SIGINT, syscall.SIGTERM:\n\t\t\t\tlogger.DebugToConsole(\"Received interrupt request\")\n\t\t\t\tplugin.Handler.Cleanup()\n\t\t\t\tos.Exit(0)\n\t\t\t}\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "internal/cmd/signals_windows.go",
    "content": "// Copyright (C) 2025 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n)\n\nfunc registerSignals() {\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt)\n\n\tgo func() {\n\t\tfor range c {\n\t\t\tlogger.DebugToConsole(\"Received interrupt request\")\n\t\t\tplugin.Handler.Cleanup()\n\t\t\tos.Exit(0)\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "internal/cmd/smtptest.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tsmtpTestRecipient string\n\tsmtpTestCmd       = &cobra.Command{\n\t\tUse:   \"smtptest\",\n\t\tShort: \"Test the SMTP configuration\",\n\t\tLong: `SFTPGo will try to send a test email to the specified recipient.\nIf the SMTP configuration is correct you should receive this email.`,\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tlogger.DisableLogger()\n\t\t\tlogger.EnableConsoleLogger(zerolog.DebugLevel)\n\t\t\tconfigDir = util.CleanDirInput(configDir)\n\t\t\terr := config.LoadConfig(configDir, configFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"Unable to load configuration: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tproviderConf := config.GetProviderConf()\n\t\t\terr = dataprovider.Initialize(providerConf, configDir, false)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"error initializing data provider: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tsmtpConfig := config.GetSMTPConfig()\n\t\t\tsmtpConfig.Debug = 1\n\t\t\terr = smtpConfig.Initialize(configDir, false)\n\t\t\tif err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"unable to initialize SMTP configuration: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\terr = smtp.SendEmail([]string{smtpTestRecipient}, nil, \"SFTPGo - Testing Email Settings\", \"It appears your SFTPGo email is setup correctly!\",\n\t\t\t\tsmtp.EmailContentTypeTextPlain)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WarnToConsole(\"Error sending email: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tlogger.InfoToConsole(\"No errors were reported while sending the test email. Please check your inbox to make sure.\")\n\t\t},\n\t}\n)\n\nfunc init() {\n\taddConfigFlags(smtpTestCmd)\n\tsmtpTestCmd.Flags().StringVar(&smtpTestRecipient, \"recipient\", \"\", `email address to send the test e-mail to`)\n\tsmtpTestCmd.MarkFlagRequired(\"recipient\") //nolint:errcheck\n\n\trootCmd.AddCommand(smtpTestCmd)\n}\n"
  },
  {
    "path": "internal/cmd/start_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tstartCmd = &cobra.Command{\n\t\tUse:   \"start\",\n\t\tShort: \"Start the SFTPGo Windows Service\",\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tconfigDir = util.CleanDirInput(configDir)\n\t\t\tcheckServeParamsFromEnvFiles(configDir)\n\t\t\tservice.SetGraceTime(graceTime)\n\t\t\ts := service.Service{\n\t\t\t\tConfigDir:         configDir,\n\t\t\t\tConfigFile:        configFile,\n\t\t\t\tLogFilePath:       logFilePath,\n\t\t\t\tLogMaxSize:        logMaxSize,\n\t\t\t\tLogMaxBackups:     logMaxBackups,\n\t\t\t\tLogMaxAge:         logMaxAge,\n\t\t\t\tLogCompress:       logCompress,\n\t\t\t\tLogLevel:          logLevel,\n\t\t\t\tLogUTCTime:        logUTCTime,\n\t\t\t\tLoadDataFrom:      loadDataFrom,\n\t\t\t\tLoadDataMode:      loadDataMode,\n\t\t\t\tLoadDataQuotaScan: loadDataQuotaScan,\n\t\t\t\tLoadDataClean:     loadDataClean,\n\t\t\t\tShutdown:          make(chan bool),\n\t\t\t}\n\t\t\twinService := service.WindowsService{\n\t\t\t\tService: s,\n\t\t\t}\n\t\t\terr := winService.RunService()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error starting service: %v\\r\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Service started!\\r\\n\")\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\tserviceCmd.AddCommand(startCmd)\n\taddServeFlags(startCmd)\n}\n"
  },
  {
    "path": "internal/cmd/status_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n)\n\nvar (\n\tstatusCmd = &cobra.Command{\n\t\tUse:   \"status\",\n\t\tShort: \"Retrieve the status for the SFTPGo Windows Service\",\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\ts := service.WindowsService{\n\t\t\t\tService: service.Service{\n\t\t\t\t\tShutdown: make(chan bool),\n\t\t\t\t},\n\t\t\t}\n\t\t\tstatus, err := s.Status()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error querying service status: %v\\r\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Service status: %q\\r\\n\", status.String())\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\tserviceCmd.AddCommand(statusCmd)\n}\n"
  },
  {
    "path": "internal/cmd/stop_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n)\n\nvar (\n\tstopCmd = &cobra.Command{\n\t\tUse:   \"stop\",\n\t\tShort: \"Stop the SFTPGo Windows Service\",\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\ts := service.WindowsService{\n\t\t\t\tService: service.Service{\n\t\t\t\t\tShutdown: make(chan bool),\n\t\t\t\t},\n\t\t\t}\n\t\t\terr := s.Stop()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error stopping service: %v\\r\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Service stopped!\\r\\n\")\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\tserviceCmd.AddCommand(stopCmd)\n}\n"
  },
  {
    "path": "internal/cmd/uninstall_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/service\"\n)\n\nvar (\n\tuninstallCmd = &cobra.Command{\n\t\tUse:   \"uninstall\",\n\t\tShort: \"Uninstall the SFTPGo Windows Service\",\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\ts := service.WindowsService{\n\t\t\t\tService: service.Service{\n\t\t\t\t\tShutdown: make(chan bool),\n\t\t\t\t},\n\t\t\t}\n\t\t\terr := s.Uninstall()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error removing service: %v\\r\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"Service uninstalled\\r\\n\")\n\t\t\t}\n\t\t},\n\t}\n)\n\nfunc init() {\n\tserviceCmd.AddCommand(uninstallCmd)\n}\n"
  },
  {
    "path": "internal/command/command.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package command provides command configuration for SFTPGo hooks\npackage command\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\tminTimeout     = 1\n\tmaxTimeout     = 300\n\tdefaultTimeout = 30\n)\n\n// Supported hook names\nconst (\n\tHookFsActions           = \"fs_actions\"\n\tHookProviderActions     = \"provider_actions\"\n\tHookStartup             = \"startup\"\n\tHookPostConnect         = \"post_connect\"\n\tHookPostDisconnect      = \"post_disconnect\"\n\tHookCheckPassword       = \"check_password\"\n\tHookPreLogin            = \"pre_login\"\n\tHookPostLogin           = \"post_login\"\n\tHookExternalAuth        = \"external_auth\"\n\tHookKeyboardInteractive = \"keyboard_interactive\"\n)\n\nvar (\n\tconfig         Config\n\tsupportedHooks = []string{HookFsActions, HookProviderActions, HookStartup, HookPostConnect, HookPostDisconnect,\n\t\tHookCheckPassword, HookPreLogin, HookPostLogin, HookExternalAuth, HookKeyboardInteractive}\n)\n\n// Command define the configuration for a specific commands\ntype Command struct {\n\t// Path is the command path as defined in the hook configuration\n\tPath string `json:\"path\" mapstructure:\"path\"`\n\t// Timeout specifies a time limit, in seconds, for the command execution.\n\t// This value overrides the global timeout if set.\n\t// Do not use variables with the SFTPGO_ prefix to avoid conflicts with env\n\t// vars that SFTPGo sets\n\tTimeout int `json:\"timeout\" mapstructure:\"timeout\"`\n\t// Env defines environment variable for the command.\n\t// Each entry is of the form \"key=value\".\n\t// These values are added to the global environment variables if any\n\tEnv []string `json:\"env\" mapstructure:\"env\"`\n\t// Args defines arguments to pass to the specified command\n\tArgs []string `json:\"args\" mapstructure:\"args\"`\n\t// if not empty both command path and hook name must match\n\tHook string `json:\"hook\" mapstructure:\"hook\"`\n}\n\n// Config defines the configuration for external commands such as\n// program based hooks\ntype Config struct {\n\t// Timeout specifies a global time limit, in seconds, for the external commands execution\n\tTimeout int `json:\"timeout\" mapstructure:\"timeout\"`\n\t// Env defines environment variable for the commands.\n\t// Each entry is of the form \"key=value\".\n\t// Do not use variables with the SFTPGO_ prefix to avoid conflicts with env\n\t// vars that SFTPGo sets\n\tEnv []string `json:\"env\" mapstructure:\"env\"`\n\t// Commands defines configuration for specific commands\n\tCommands []Command `json:\"commands\" mapstructure:\"commands\"`\n}\n\nfunc init() {\n\tconfig = Config{\n\t\tTimeout: defaultTimeout,\n\t}\n}\n\n// Initialize configures commands\nfunc (c Config) Initialize() error {\n\tif c.Timeout < minTimeout || c.Timeout > maxTimeout {\n\t\treturn fmt.Errorf(\"invalid timeout %v\", c.Timeout)\n\t}\n\tfor _, env := range c.Env {\n\t\tif len(strings.SplitN(env, \"=\", 2)) != 2 {\n\t\t\treturn fmt.Errorf(\"invalid env var %q\", env)\n\t\t}\n\t}\n\tfor idx, cmd := range c.Commands {\n\t\tif cmd.Path == \"\" {\n\t\t\treturn fmt.Errorf(\"invalid path %q\", cmd.Path)\n\t\t}\n\t\tif cmd.Timeout == 0 {\n\t\t\tc.Commands[idx].Timeout = c.Timeout\n\t\t} else {\n\t\t\tif cmd.Timeout < minTimeout || cmd.Timeout > maxTimeout {\n\t\t\t\treturn fmt.Errorf(\"invalid timeout %v for command %q\", cmd.Timeout, cmd.Path)\n\t\t\t}\n\t\t}\n\t\tfor _, env := range cmd.Env {\n\t\t\tif len(strings.SplitN(env, \"=\", 2)) != 2 {\n\t\t\t\treturn fmt.Errorf(\"invalid env var %q for command %q\", env, cmd.Path)\n\t\t\t}\n\t\t}\n\t\t// don't validate args, we allow to pass empty arguments\n\t\tif cmd.Hook != \"\" {\n\t\t\tif !slices.Contains(supportedHooks, cmd.Hook) {\n\t\t\t\treturn fmt.Errorf(\"invalid hook name %q, supported values: %+v\", cmd.Hook, supportedHooks)\n\t\t\t}\n\t\t}\n\t}\n\tconfig = c\n\treturn nil\n}\n\n// GetConfig returns the configuration for the specified command\nfunc GetConfig(command, hook string) (time.Duration, []string, []string) {\n\tenv := []string{}\n\tvar args []string\n\ttimeout := time.Duration(config.Timeout) * time.Second\n\tenv = append(env, config.Env...)\n\tfor _, cmd := range config.Commands {\n\t\tif cmd.Path == command {\n\t\t\tif cmd.Hook == \"\" || cmd.Hook == hook {\n\t\t\t\ttimeout = time.Duration(cmd.Timeout) * time.Second\n\t\t\t\tenv = append(env, cmd.Env...)\n\t\t\t\targs = cmd.Args\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn timeout, env, args\n}\n"
  },
  {
    "path": "internal/command/command_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage command\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCommandConfig(t *testing.T) {\n\trequire.Equal(t, defaultTimeout, config.Timeout)\n\tcfg := Config{\n\t\tTimeout: 10,\n\t\tEnv:     []string{\"a=b\"},\n\t}\n\terr := cfg.Initialize()\n\trequire.NoError(t, err)\n\tassert.Equal(t, cfg.Timeout, config.Timeout)\n\tassert.Equal(t, cfg.Env, config.Env)\n\tassert.Len(t, cfg.Commands, 0)\n\ttimeout, env, args := GetConfig(\"cmd\", \"\")\n\tassert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout)\n\tassert.Contains(t, env, \"a=b\")\n\tassert.Len(t, args, 0)\n\n\tcfg.Commands = []Command{\n\t\t{\n\t\t\tPath:    \"cmd1\",\n\t\t\tTimeout: 30,\n\t\t\tEnv:     []string{\"c=d\"},\n\t\t\tArgs:    []string{\"1\", \"\", \"2\"},\n\t\t},\n\t\t{\n\t\t\tPath:    \"cmd2\",\n\t\t\tTimeout: 0,\n\t\t\tEnv:     []string{\"e=f\"},\n\t\t},\n\t}\n\terr = cfg.Initialize()\n\trequire.NoError(t, err)\n\tassert.Equal(t, cfg.Timeout, config.Timeout)\n\tassert.Equal(t, cfg.Env, config.Env)\n\tif assert.Len(t, config.Commands, 2) {\n\t\tassert.Equal(t, cfg.Commands[0].Path, config.Commands[0].Path)\n\t\tassert.Equal(t, cfg.Commands[0].Timeout, config.Commands[0].Timeout)\n\t\tassert.Equal(t, cfg.Commands[0].Env, config.Commands[0].Env)\n\t\tassert.Equal(t, cfg.Commands[0].Args, config.Commands[0].Args)\n\t\tassert.Equal(t, cfg.Commands[1].Path, config.Commands[1].Path)\n\t\tassert.Equal(t, cfg.Timeout, config.Commands[1].Timeout)\n\t\tassert.Equal(t, cfg.Commands[1].Env, config.Commands[1].Env)\n\t\tassert.Equal(t, cfg.Commands[1].Args, config.Commands[1].Args)\n\t}\n\ttimeout, env, args = GetConfig(\"cmd1\", \"\")\n\tassert.Equal(t, time.Duration(config.Commands[0].Timeout)*time.Second, timeout)\n\tassert.Contains(t, env, \"a=b\")\n\tassert.Contains(t, env, \"c=d\")\n\tassert.NotContains(t, env, \"e=f\")\n\tif assert.Len(t, args, 3) {\n\t\tassert.Equal(t, \"1\", args[0])\n\t\tassert.Empty(t, args[1])\n\t\tassert.Equal(t, \"2\", args[2])\n\t}\n\ttimeout, env, args = GetConfig(\"cmd2\", \"\")\n\tassert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout)\n\tassert.Contains(t, env, \"a=b\")\n\tassert.NotContains(t, env, \"c=d\")\n\tassert.Contains(t, env, \"e=f\")\n\tassert.Len(t, args, 0)\n\n\tcfg.Commands = []Command{\n\t\t{\n\t\t\tPath:    \"cmd1\",\n\t\t\tTimeout: 30,\n\t\t\tEnv:     []string{\"c=d\"},\n\t\t\tArgs:    []string{\"1\", \"\", \"2\"},\n\t\t\tHook:    HookCheckPassword,\n\t\t},\n\t\t{\n\t\t\tPath:    \"cmd1\",\n\t\t\tTimeout: 0,\n\t\t\tEnv:     []string{\"e=f\"},\n\t\t\tHook:    HookExternalAuth,\n\t\t},\n\t}\n\terr = cfg.Initialize()\n\trequire.NoError(t, err)\n\ttimeout, env, args = GetConfig(\"cmd1\", \"\")\n\tassert.Equal(t, time.Duration(config.Timeout)*time.Second, timeout)\n\tassert.Contains(t, env, \"a=b\")\n\tassert.NotContains(t, env, \"c=d\")\n\tassert.NotContains(t, env, \"e=f\")\n\tassert.Len(t, args, 0)\n\ttimeout, env, args = GetConfig(\"cmd1\", HookCheckPassword)\n\tassert.Equal(t, time.Duration(config.Commands[0].Timeout)*time.Second, timeout)\n\tassert.Contains(t, env, \"a=b\")\n\tassert.Contains(t, env, \"c=d\")\n\tassert.NotContains(t, env, \"e=f\")\n\tif assert.Len(t, args, 3) {\n\t\tassert.Equal(t, \"1\", args[0])\n\t\tassert.Empty(t, args[1])\n\t\tassert.Equal(t, \"2\", args[2])\n\t}\n\ttimeout, env, args = GetConfig(\"cmd1\", HookExternalAuth)\n\tassert.Equal(t, time.Duration(cfg.Timeout)*time.Second, timeout)\n\tassert.Contains(t, env, \"a=b\")\n\tassert.NotContains(t, env, \"c=d\")\n\tassert.Contains(t, env, \"e=f\")\n\tassert.Len(t, args, 0)\n}\n\nfunc TestConfigErrors(t *testing.T) {\n\tc := Config{}\n\terr := c.Initialize()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid timeout\")\n\t}\n\tc.Timeout = 10\n\tc.Env = []string{\"a\"}\n\terr = c.Initialize()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid env var\")\n\t}\n\tc.Env = nil\n\tc.Commands = []Command{\n\t\t{\n\t\t\tPath: \"\",\n\t\t},\n\t}\n\terr = c.Initialize()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid path\")\n\t}\n\tc.Commands = []Command{\n\t\t{\n\t\t\tPath:    \"path\",\n\t\t\tTimeout: 10000,\n\t\t},\n\t}\n\terr = c.Initialize()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid timeout\")\n\t}\n\tc.Commands = []Command{\n\t\t{\n\t\t\tPath:    \"path\",\n\t\t\tTimeout: 30,\n\t\t\tEnv:     []string{\"b\"},\n\t\t},\n\t}\n\terr = c.Initialize()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid env var\")\n\t}\n\tc.Commands = []Command{\n\t\t{\n\t\t\tPath:    \"path\",\n\t\t\tTimeout: 30,\n\t\t\tEnv:     []string{\"a=b\"},\n\t\t\tHook:    \"invali\",\n\t\t},\n\t}\n\terr = c.Initialize()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid hook name\")\n\t}\n}\n"
  },
  {
    "path": "internal/common/actions.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/command\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n)\n\nvar (\n\terrUnexpectedHTTResponse = errors.New(\"unexpected HTTP hook response code\")\n\thooksConcurrencyGuard    = make(chan struct{}, 150)\n\tactiveHooks              atomic.Int32\n)\n\nfunc startNewHook() {\n\tactiveHooks.Add(1)\n\thooksConcurrencyGuard <- struct{}{}\n}\n\nfunc hookEnded() {\n\tactiveHooks.Add(-1)\n\t<-hooksConcurrencyGuard\n}\n\n// ProtocolActions defines the action to execute on file operations and SSH commands\ntype ProtocolActions struct {\n\t// Valid values are download, upload, pre-delete, delete, rename, ssh_cmd. Empty slice to disable\n\tExecuteOn []string `json:\"execute_on\" mapstructure:\"execute_on\"`\n\t// Actions to be performed synchronously.\n\t// The pre-delete action is always executed synchronously while the other ones are asynchronous.\n\t// Executing an action synchronously means that SFTPGo will not return a result code to the client\n\t// (which is waiting for it) until your hook have completed its execution.\n\tExecuteSync []string `json:\"execute_sync\" mapstructure:\"execute_sync\"`\n\t// Absolute path to an external program or an HTTP URL\n\tHook string `json:\"hook\" mapstructure:\"hook\"`\n}\n\nvar actionHandler ActionHandler = &defaultActionHandler{}\n\n// InitializeActionHandler lets the user choose an action handler implementation.\n//\n// Do NOT call this function after application initialization.\nfunc InitializeActionHandler(handler ActionHandler) {\n\tactionHandler = handler\n}\n\n// ExecutePreAction executes a pre-* action and returns the result.\n// The returned status has the following meaning:\n// - 0 not executed\n// - 1 executed using an external hook\n// - 2 executed using the event manager\nfunc ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) {\n\tvar event *notifier.FsEvent\n\thasNotifiersPlugin := plugin.Handler.HasNotifiers()\n\thasHook := slices.Contains(Config.Actions.ExecuteOn, operation)\n\thasRules := eventManager.hasFsRules()\n\tif !hasHook && !hasNotifiersPlugin && !hasRules {\n\t\treturn 0, nil\n\t}\n\tdateTime := time.Now()\n\tevent = newActionNotification(&conn.User, operation, filePath, virtualPath, \"\", \"\", \"\",\n\t\tconn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil), 0, dateTime, nil)\n\tif hasNotifiersPlugin {\n\t\tplugin.Handler.NotifyFsEvent(event)\n\t}\n\tif hasRules {\n\t\tparams := EventParams{\n\t\t\tName:              event.Username,\n\t\t\tGroups:            conn.User.Groups,\n\t\t\tEvent:             event.Action,\n\t\t\tStatus:            event.Status,\n\t\t\tVirtualPath:       event.VirtualPath,\n\t\t\tFsPath:            event.Path,\n\t\t\tVirtualTargetPath: event.VirtualTargetPath,\n\t\t\tFsTargetPath:      event.TargetPath,\n\t\t\tObjectName:        path.Base(event.VirtualPath),\n\t\t\tExtension:         path.Ext(event.VirtualPath),\n\t\t\tFileSize:          event.FileSize,\n\t\t\tProtocol:          event.Protocol,\n\t\t\tIP:                event.IP,\n\t\t\tRole:              event.Role,\n\t\t\tTimestamp:         dateTime,\n\t\t\tEmail:             conn.User.Email,\n\t\t\tObject:            nil,\n\t\t}\n\t\texecutedSync, err := eventManager.handleFsEvent(params)\n\t\tif executedSync {\n\t\t\treturn 2, err\n\t\t}\n\t}\n\tif !hasHook {\n\t\treturn 0, nil\n\t}\n\treturn actionHandler.Handle(event)\n}\n\n// ExecuteActionNotification executes the defined hook, if any, for the specified action\nfunc ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtualPath, target, virtualTarget, sshCmd string,\n\tfileSize int64, err error, elapsed int64, metadata map[string]string,\n) error {\n\thasNotifiersPlugin := plugin.Handler.HasNotifiers()\n\thasHook := slices.Contains(Config.Actions.ExecuteOn, operation)\n\thasRules := eventManager.hasFsRules()\n\tif !hasHook && !hasNotifiersPlugin && !hasRules {\n\t\treturn nil\n\t}\n\tdateTime := time.Now()\n\tnotification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd,\n\t\tconn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, 0, conn.getNotificationStatus(err), elapsed, dateTime, metadata)\n\tif hasNotifiersPlugin {\n\t\tplugin.Handler.NotifyFsEvent(notification)\n\t}\n\tif hasRules {\n\t\tparams := EventParams{\n\t\t\tName:              notification.Username,\n\t\t\tGroups:            conn.User.Groups,\n\t\t\tEvent:             notification.Action,\n\t\t\tStatus:            notification.Status,\n\t\t\tVirtualPath:       notification.VirtualPath,\n\t\t\tFsPath:            notification.Path,\n\t\t\tVirtualTargetPath: notification.VirtualTargetPath,\n\t\t\tFsTargetPath:      notification.TargetPath,\n\t\t\tObjectName:        path.Base(notification.VirtualPath),\n\t\t\tExtension:         path.Ext(notification.VirtualPath),\n\t\t\tFileSize:          notification.FileSize,\n\t\t\tElapsed:           notification.Elapsed,\n\t\t\tProtocol:          notification.Protocol,\n\t\t\tIP:                notification.IP,\n\t\t\tRole:              notification.Role,\n\t\t\tTimestamp:         dateTime,\n\t\t\tEmail:             conn.User.Email,\n\t\t\tObject:            nil,\n\t\t\tMetadata:          metadata,\n\t\t}\n\t\tif err != nil {\n\t\t\tparams.AddError(fmt.Errorf(\"%q failed: %w\", params.Event, err))\n\t\t}\n\t\texecutedSync, err := eventManager.handleFsEvent(params)\n\t\tif executedSync {\n\t\t\treturn err\n\t\t}\n\t}\n\tif hasHook {\n\t\tif slices.Contains(Config.Actions.ExecuteSync, operation) {\n\t\t\t_, err := actionHandler.Handle(notification)\n\t\t\treturn err\n\t\t}\n\t\tgo func() {\n\t\t\tstartNewHook()\n\t\t\tdefer hookEnded()\n\n\t\t\tactionHandler.Handle(notification) //nolint:errcheck\n\t\t}()\n\t}\n\treturn nil\n}\n\n// ActionHandler handles a notification for a Protocol Action.\ntype ActionHandler interface {\n\tHandle(notification *notifier.FsEvent) (int, error)\n}\n\nfunc newActionNotification(\n\tuser *dataprovider.User,\n\toperation, filePath, virtualPath, target, virtualTarget, sshCmd, protocol, ip, sessionID string,\n\tfileSize int64,\n\topenFlags, status int, elapsed int64,\n\tdatetime time.Time,\n\tmetadata map[string]string,\n) *notifier.FsEvent {\n\tvar bucket, endpoint string\n\n\tfsConfig := user.GetFsConfigForPath(virtualPath)\n\n\tswitch fsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tbucket = fsConfig.S3Config.Bucket\n\t\tendpoint = fsConfig.S3Config.Endpoint\n\tcase sdk.GCSFilesystemProvider:\n\t\tbucket = fsConfig.GCSConfig.Bucket\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tbucket = fsConfig.AzBlobConfig.Container\n\t\tif fsConfig.AzBlobConfig.Endpoint != \"\" {\n\t\t\tendpoint = fsConfig.AzBlobConfig.Endpoint\n\t\t}\n\tcase sdk.SFTPFilesystemProvider:\n\t\tendpoint = fsConfig.SFTPConfig.Endpoint\n\tcase sdk.HTTPFilesystemProvider:\n\t\tendpoint = fsConfig.HTTPConfig.Endpoint\n\t}\n\n\treturn &notifier.FsEvent{\n\t\tAction:            operation,\n\t\tUsername:          user.Username,\n\t\tPath:              filePath,\n\t\tTargetPath:        target,\n\t\tVirtualPath:       virtualPath,\n\t\tVirtualTargetPath: virtualTarget,\n\t\tSSHCmd:            sshCmd,\n\t\tFileSize:          fileSize,\n\t\tFsProvider:        int(fsConfig.Provider),\n\t\tBucket:            bucket,\n\t\tEndpoint:          endpoint,\n\t\tStatus:            status,\n\t\tProtocol:          protocol,\n\t\tIP:                ip,\n\t\tSessionID:         sessionID,\n\t\tOpenFlags:         openFlags,\n\t\tRole:              user.Role,\n\t\tTimestamp:         datetime.UnixNano(),\n\t\tElapsed:           elapsed,\n\t\tMetadata:          metadata,\n\t}\n}\n\ntype defaultActionHandler struct{}\n\nfunc (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) {\n\tif !slices.Contains(Config.Actions.ExecuteOn, event.Action) {\n\t\treturn 0, nil\n\t}\n\n\tif Config.Actions.Hook == \"\" {\n\t\tlogger.Warn(event.Protocol, \"\", \"Unable to send notification, no hook is defined\")\n\n\t\treturn 0, nil\n\t}\n\n\tif strings.HasPrefix(Config.Actions.Hook, \"http\") {\n\t\terr := h.handleHTTP(event)\n\t\treturn 1, err\n\t}\n\n\terr := h.handleCommand(event)\n\treturn 1, err\n}\n\nfunc (h *defaultActionHandler) handleHTTP(event *notifier.FsEvent) error {\n\tu, err := url.Parse(Config.Actions.Hook)\n\tif err != nil {\n\t\tlogger.Error(event.Protocol, \"\", \"Invalid hook %q for operation %q: %v\",\n\t\t\tConfig.Actions.Hook, event.Action, err)\n\t\treturn err\n\t}\n\n\tstartTime := time.Now()\n\trespCode := 0\n\n\tvar b bytes.Buffer\n\t_ = json.NewEncoder(&b).Encode(event)\n\n\tresp, err := httpclient.RetryablePost(Config.Actions.Hook, \"application/json\", &b)\n\tif err == nil {\n\t\trespCode = resp.StatusCode\n\t\tresp.Body.Close()\n\n\t\tif respCode != http.StatusOK {\n\t\t\terr = errUnexpectedHTTResponse\n\t\t}\n\t}\n\n\tlogger.Debug(event.Protocol, \"\", \"notified operation %q to URL: %s status code: %d, elapsed: %s err: %v\",\n\t\tevent.Action, u.Redacted(), respCode, time.Since(startTime), err)\n\n\treturn err\n}\n\nfunc (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {\n\tif !filepath.IsAbs(Config.Actions.Hook) {\n\t\terr := fmt.Errorf(\"invalid notification command %q\", Config.Actions.Hook)\n\t\tlogger.Warn(event.Protocol, \"\", \"unable to execute notification command: %v\", err)\n\n\t\treturn err\n\t}\n\n\ttimeout, env, args := command.GetConfig(Config.Actions.Hook, command.HookFsActions)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, Config.Actions.Hook, args...)\n\tcmd.Env = append(env, notificationAsEnvVars(event)...)\n\n\tstartTime := time.Now()\n\terr := cmd.Run()\n\n\tlogger.Debug(event.Protocol, \"\", \"executed command %q, elapsed: %s, error: %v\",\n\t\tConfig.Actions.Hook, time.Since(startTime), err)\n\n\treturn err\n}\n\nfunc notificationAsEnvVars(event *notifier.FsEvent) []string {\n\tresult := []string{\n\t\tfmt.Sprintf(\"SFTPGO_ACTION=%s\", event.Action),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_USERNAME=%s\", event.Username),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_PATH=%s\", event.Path),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_TARGET=%s\", event.TargetPath),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_VIRTUAL_PATH=%s\", event.VirtualPath),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_VIRTUAL_TARGET=%s\", event.VirtualTargetPath),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_SSH_CMD=%s\", event.SSHCmd),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_FILE_SIZE=%d\", event.FileSize),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_ELAPSED=%d\", event.Elapsed),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_FS_PROVIDER=%d\", event.FsProvider),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_BUCKET=%s\", event.Bucket),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_ENDPOINT=%s\", event.Endpoint),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_STATUS=%d\", event.Status),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_PROTOCOL=%s\", event.Protocol),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_IP=%s\", event.IP),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_SESSION_ID=%s\", event.SessionID),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_OPEN_FLAGS=%d\", event.OpenFlags),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_TIMESTAMP=%d\", event.Timestamp),\n\t\tfmt.Sprintf(\"SFTPGO_ACTION_ROLE=%s\", event.Role),\n\t}\n\tif len(event.Metadata) > 0 {\n\t\tdata, err := json.Marshal(event.Metadata)\n\t\tif err == nil {\n\t\t\tresult = append(result, fmt.Sprintf(\"SFTPGO_ACTION_METADATA=%s\", data))\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "internal/common/actions_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lithammer/shortuuid/v4\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nfunc TestNewActionNotification(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"username\",\n\t\t},\n\t}\n\tuser.FsConfig.Provider = sdk.LocalFilesystemProvider\n\tuser.FsConfig.S3Config = vfs.S3FsConfig{\n\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\tBucket:   \"s3bucket\",\n\t\t\tEndpoint: \"endpoint\",\n\t\t},\n\t}\n\tuser.FsConfig.GCSConfig = vfs.GCSFsConfig{\n\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\tBucket: \"gcsbucket\",\n\t\t},\n\t}\n\tuser.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{\n\t\tBaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{\n\t\t\tContainer: \"azcontainer\",\n\t\t\tEndpoint:  \"azendpoint\",\n\t\t},\n\t}\n\tuser.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: \"sftpendpoint\",\n\t\t},\n\t}\n\tuser.FsConfig.HTTPConfig = vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint: \"httpendpoint\",\n\t\t},\n\t}\n\tc := NewBaseConnection(\"id\", ProtocolSSH, \"\", \"\", user)\n\tsessionID := xid.New().String()\n\ta := newActionNotification(&user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSFTP, \"\", sessionID,\n\t\t123, 0, c.getNotificationStatus(errors.New(\"fake error\")), 0, time.Now(), nil)\n\tassert.Equal(t, user.Username, a.Username)\n\tassert.Equal(t, 0, len(a.Bucket))\n\tassert.Equal(t, 0, len(a.Endpoint))\n\tassert.Equal(t, 2, a.Status)\n\n\tuser.FsConfig.Provider = sdk.S3FilesystemProvider\n\ta = newActionNotification(&user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSSH, \"\", sessionID,\n\t\t123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)\n\tassert.Equal(t, \"s3bucket\", a.Bucket)\n\tassert.Equal(t, \"endpoint\", a.Endpoint)\n\tassert.Equal(t, 1, a.Status)\n\n\tuser.FsConfig.Provider = sdk.GCSFilesystemProvider\n\ta = newActionNotification(&user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSCP, \"\", sessionID,\n\t\t123, 0, c.getNotificationStatus(ErrQuotaExceeded), 0, time.Now(), nil)\n\tassert.Equal(t, \"gcsbucket\", a.Bucket)\n\tassert.Equal(t, 0, len(a.Endpoint))\n\tassert.Equal(t, 3, a.Status)\n\ta = newActionNotification(&user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSCP, \"\", sessionID,\n\t\t123, 0, c.getNotificationStatus(fmt.Errorf(\"wrapper quota error: %w\", ErrQuotaExceeded)), 0, time.Now(), nil)\n\tassert.Equal(t, \"gcsbucket\", a.Bucket)\n\tassert.Equal(t, 0, len(a.Endpoint))\n\tassert.Equal(t, 3, a.Status)\n\n\tuser.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\ta = newActionNotification(&user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSSH, \"\", sessionID,\n\t\t123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)\n\tassert.Equal(t, \"httpendpoint\", a.Endpoint)\n\tassert.Equal(t, 1, a.Status)\n\n\tuser.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\ta = newActionNotification(&user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSCP, \"\", sessionID,\n\t\t123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)\n\tassert.Equal(t, \"azcontainer\", a.Bucket)\n\tassert.Equal(t, \"azendpoint\", a.Endpoint)\n\tassert.Equal(t, 1, a.Status)\n\n\ta = newActionNotification(&user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSCP, \"\", sessionID,\n\t\t123, os.O_APPEND, c.getNotificationStatus(nil), 0, time.Now(), nil)\n\tassert.Equal(t, \"azcontainer\", a.Bucket)\n\tassert.Equal(t, \"azendpoint\", a.Endpoint)\n\tassert.Equal(t, 1, a.Status)\n\tassert.Equal(t, os.O_APPEND, a.OpenFlags)\n\n\tuser.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\ta = newActionNotification(&user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSFTP, \"\", sessionID,\n\t\t123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)\n\tassert.Equal(t, \"sftpendpoint\", a.Endpoint)\n}\n\nfunc TestActionHTTP(t *testing.T) {\n\tactionsCopy := Config.Actions\n\n\tConfig.Actions = ProtocolActions{\n\t\tExecuteOn: []string{operationDownload},\n\t\tHook:      fmt.Sprintf(\"http://%v\", httpAddr),\n\t}\n\tuser := &dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"username\",\n\t\t},\n\t}\n\ta := newActionNotification(user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSFTP, \"\",\n\t\txid.New().String(), 123, 0, 1, 0, time.Now(), nil)\n\tstatus, err := actionHandler.Handle(a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, status)\n\n\tConfig.Actions.Hook = \"http://invalid:1234\"\n\tstatus, err = actionHandler.Handle(a)\n\tassert.Error(t, err)\n\tassert.Equal(t, 1, status)\n\n\tConfig.Actions.Hook = fmt.Sprintf(\"http://%v/404\", httpAddr)\n\tstatus, err = actionHandler.Handle(a)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, errUnexpectedHTTResponse.Error())\n\t}\n\tassert.Equal(t, 1, status)\n\n\tConfig.Actions = actionsCopy\n}\n\nfunc TestActionCMD(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tactionsCopy := Config.Actions\n\n\thookCmd, err := exec.LookPath(\"true\")\n\tassert.NoError(t, err)\n\n\tConfig.Actions = ProtocolActions{\n\t\tExecuteOn: []string{operationDownload},\n\t\tHook:      hookCmd,\n\t}\n\tuser := &dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"username\",\n\t\t},\n\t}\n\tsessionID := shortuuid.New()\n\ta := newActionNotification(user, operationDownload, \"path\", \"vpath\", \"target\", \"\", \"\", ProtocolSFTP, \"\", sessionID,\n\t\t123, 0, 1, 0, time.Now(), map[string]string{\"key\": \"value\"})\n\tstatus, err := actionHandler.Handle(a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, status)\n\n\tc := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", *user)\n\terr = ExecuteActionNotification(c, OperationSSHCmd, \"path\", \"vpath\", \"target\", \"vtarget\", \"sha1sum\", 0, nil, 0, nil)\n\tassert.NoError(t, err)\n\n\terr = ExecuteActionNotification(c, operationDownload, \"path\", \"vpath\", \"\", \"\", \"\", 0, nil, 0, nil)\n\tassert.NoError(t, err)\n\n\tConfig.Actions = actionsCopy\n}\n\nfunc TestWrongActions(t *testing.T) {\n\tactionsCopy := Config.Actions\n\n\tbadCommand := \"/bad/command\"\n\tif runtime.GOOS == osWindows {\n\t\tbadCommand = \"C:\\\\bad\\\\command\"\n\t}\n\tConfig.Actions = ProtocolActions{\n\t\tExecuteOn: []string{operationUpload},\n\t\tHook:      badCommand,\n\t}\n\tuser := &dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"username\",\n\t\t},\n\t}\n\n\ta := newActionNotification(user, operationUpload, \"\", \"\", \"\", \"\", \"\", ProtocolSFTP, \"\", xid.New().String(),\n\t\t123, 0, 1, 0, time.Now(), nil)\n\tstatus, err := actionHandler.Handle(a)\n\tassert.Error(t, err, \"action with bad command must fail\")\n\tassert.Equal(t, 1, status)\n\n\ta.Action = operationDelete\n\tstatus, err = actionHandler.Handle(a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, status)\n\n\tConfig.Actions.Hook = \"http://foo\\x7f.com/\"\n\ta.Action = operationUpload\n\tstatus, err = actionHandler.Handle(a)\n\tassert.Error(t, err, \"action with bad url must fail\")\n\tassert.Equal(t, 1, status)\n\n\tConfig.Actions.Hook = \"\"\n\tstatus, err = actionHandler.Handle(a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, status)\n\n\tConfig.Actions.Hook = \"relative path\"\n\tstatus, err = actionHandler.Handle(a)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, fmt.Sprintf(\"invalid notification command %q\", Config.Actions.Hook))\n\t}\n\tassert.Equal(t, 1, status)\n\n\tConfig.Actions = actionsCopy\n}\n\nfunc TestPreDeleteAction(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tactionsCopy := Config.Actions\n\n\thookCmd, err := exec.LookPath(\"true\")\n\tassert.NoError(t, err)\n\tConfig.Actions = ProtocolActions{\n\t\tExecuteOn: []string{operationPreDelete},\n\t\tHook:      \"missing hook\",\n\t}\n\thomeDir := filepath.Join(os.TempDir(), \"test_user\")\n\terr = os.MkdirAll(homeDir, os.ModePerm)\n\tassert.NoError(t, err)\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"username\",\n\t\t\tHomeDir:  homeDir,\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfs := vfs.NewOsFs(\"id\", homeDir, \"\", nil)\n\tc := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", user)\n\n\ttestfile := filepath.Join(user.HomeDir, \"testfile\")\n\terr = os.WriteFile(testfile, []byte(\"test\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tinfo, err := os.Stat(testfile)\n\tassert.NoError(t, err)\n\terr = c.RemoveFile(fs, testfile, \"testfile\", info)\n\tassert.ErrorIs(t, err, c.GetPermissionDeniedError())\n\tassert.FileExists(t, testfile)\n\tConfig.Actions.Hook = hookCmd\n\terr = c.RemoveFile(fs, testfile, \"testfile\", info)\n\tassert.NoError(t, err)\n\tassert.NoFileExists(t, testfile)\n\n\tos.RemoveAll(homeDir)\n\n\tConfig.Actions = actionsCopy\n}\n\nfunc TestUnconfiguredHook(t *testing.T) {\n\tactionsCopy := Config.Actions\n\n\tConfig.Actions = ProtocolActions{\n\t\tExecuteOn: []string{operationDownload},\n\t\tHook:      \"\",\n\t}\n\tpluginsConfig := []plugin.Config{\n\t\t{\n\t\t\tType: \"notifier\",\n\t\t},\n\t}\n\terr := plugin.Initialize(pluginsConfig, \"debug\")\n\tassert.Error(t, err)\n\tassert.True(t, plugin.Handler.HasNotifiers())\n\n\tc := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", dataprovider.User{})\n\tstatus, err := ExecutePreAction(c, OperationPreDownload, \"\", \"\", 0, 0)\n\tassert.NoError(t, err)\n\tassert.Equal(t, status, 0)\n\tstatus, err = ExecutePreAction(c, operationPreDelete, \"\", \"\", 0, 0)\n\tassert.NoError(t, err)\n\tassert.Equal(t, status, 0)\n\n\terr = ExecuteActionNotification(c, operationDownload, \"\", \"\", \"\", \"\", \"\", 0, nil, 0, nil)\n\tassert.NoError(t, err)\n\n\terr = plugin.Initialize(nil, \"debug\")\n\tassert.NoError(t, err)\n\tassert.False(t, plugin.Handler.HasNotifiers())\n\n\tConfig.Actions = actionsCopy\n}\n\ntype actionHandlerStub struct {\n\tcalled bool\n}\n\nfunc (h *actionHandlerStub) Handle(_ *notifier.FsEvent) (int, error) {\n\th.called = true\n\n\treturn 1, nil\n}\n\nfunc TestInitializeActionHandler(t *testing.T) {\n\thandler := &actionHandlerStub{}\n\n\tInitializeActionHandler(handler)\n\tt.Cleanup(func() {\n\t\tInitializeActionHandler(&defaultActionHandler{})\n\t})\n\n\tstatus, err := actionHandler.Handle(&notifier.FsEvent{})\n\tassert.NoError(t, err)\n\tassert.True(t, handler.called)\n\tassert.Equal(t, 1, status)\n}\n"
  },
  {
    "path": "internal/common/clientsmap.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// clienstMap is a struct containing the map of the connected clients\ntype clientsMap struct {\n\ttotalConnections atomic.Int32\n\tmu               sync.RWMutex\n\tclients          map[string]int\n}\n\nfunc (c *clientsMap) add(source string) {\n\tc.totalConnections.Add(1)\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.clients[source]++\n}\n\nfunc (c *clientsMap) remove(source string) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif val, ok := c.clients[source]; ok {\n\t\tc.totalConnections.Add(-1)\n\t\tc.clients[source]--\n\t\tif val > 1 {\n\t\t\treturn\n\t\t}\n\t\tdelete(c.clients, source)\n\t} else {\n\t\tlogger.Warn(logSender, \"\", \"cannot remove client %v it is not mapped\", source)\n\t}\n}\n\nfunc (c *clientsMap) getTotal() int32 {\n\treturn c.totalConnections.Load()\n}\n\nfunc (c *clientsMap) getTotalFrom(source string) int {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\treturn c.clients[source]\n}\n"
  },
  {
    "path": "internal/common/clientsmap_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestClientsMap(t *testing.T) {\n\tm := clientsMap{\n\t\tclients: make(map[string]int),\n\t}\n\tip1 := \"192.168.1.1\"\n\tip2 := \"192.168.1.2\"\n\tm.add(ip1)\n\tassert.Equal(t, int32(1), m.getTotal())\n\tassert.Equal(t, 1, m.getTotalFrom(ip1))\n\tassert.Equal(t, 0, m.getTotalFrom(ip2))\n\n\tm.add(ip1)\n\tm.add(ip2)\n\tassert.Equal(t, int32(3), m.getTotal())\n\tassert.Equal(t, 2, m.getTotalFrom(ip1))\n\tassert.Equal(t, 1, m.getTotalFrom(ip2))\n\n\tm.add(ip1)\n\tm.add(ip1)\n\tm.add(ip2)\n\tassert.Equal(t, int32(6), m.getTotal())\n\tassert.Equal(t, 4, m.getTotalFrom(ip1))\n\tassert.Equal(t, 2, m.getTotalFrom(ip2))\n\n\tm.remove(ip2)\n\tassert.Equal(t, int32(5), m.getTotal())\n\tassert.Equal(t, 4, m.getTotalFrom(ip1))\n\tassert.Equal(t, 1, m.getTotalFrom(ip2))\n\n\tm.remove(\"unknown\")\n\tassert.Equal(t, int32(5), m.getTotal())\n\tassert.Equal(t, 4, m.getTotalFrom(ip1))\n\tassert.Equal(t, 1, m.getTotalFrom(ip2))\n\n\tm.remove(ip2)\n\tassert.Equal(t, int32(4), m.getTotal())\n\tassert.Equal(t, 4, m.getTotalFrom(ip1))\n\tassert.Equal(t, 0, m.getTotalFrom(ip2))\n\n\tm.remove(ip1)\n\tm.remove(ip1)\n\tm.remove(ip1)\n\tassert.Equal(t, int32(1), m.getTotal())\n\tassert.Equal(t, 1, m.getTotalFrom(ip1))\n\tassert.Equal(t, 0, m.getTotalFrom(ip2))\n\n\tm.remove(ip1)\n\tassert.Equal(t, int32(0), m.getTotal())\n\tassert.Equal(t, 0, m.getTotalFrom(ip1))\n\tassert.Equal(t, 0, m.getTotalFrom(ip2))\n}\n"
  },
  {
    "path": "internal/common/common.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package common defines code shared among file transfer packages and protocols\npackage common\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/pires/go-proxyproto\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/command\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\n// constants\nconst (\n\tlogSender              = \"common\"\n\tuploadLogSender        = \"Upload\"\n\tdownloadLogSender      = \"Download\"\n\trenameLogSender        = \"Rename\"\n\trmdirLogSender         = \"Rmdir\"\n\tmkdirLogSender         = \"Mkdir\"\n\tsymlinkLogSender       = \"Symlink\"\n\tremoveLogSender        = \"Remove\"\n\tchownLogSender         = \"Chown\"\n\tchmodLogSender         = \"Chmod\"\n\tchtimesLogSender       = \"Chtimes\"\n\tcopyLogSender          = \"Copy\"\n\ttruncateLogSender      = \"Truncate\"\n\toperationDownload      = \"download\"\n\toperationUpload        = \"upload\"\n\toperationFirstDownload = \"first-download\"\n\toperationFirstUpload   = \"first-upload\"\n\toperationDelete        = \"delete\"\n\toperationCopy          = \"copy\"\n\t// Pre-download action name\n\tOperationPreDownload = \"pre-download\"\n\t// Pre-upload action name\n\tOperationPreUpload = \"pre-upload\"\n\toperationPreDelete = \"pre-delete\"\n\toperationRename    = \"rename\"\n\toperationMkdir     = \"mkdir\"\n\toperationRmdir     = \"rmdir\"\n\t// SSH command action name\n\tOperationSSHCmd              = \"ssh_cmd\"\n\tchtimesFormat                = \"2006-01-02T15:04:05\" // YYYY-MM-DDTHH:MM:SS\n\tidleTimeoutCheckInterval     = 3 * time.Minute\n\tperiodicTimeoutCheckInterval = 1 * time.Minute\n)\n\n// Stat flags\nconst (\n\tStatAttrUIDGID = 1\n\tStatAttrPerms  = 2\n\tStatAttrTimes  = 4\n\tStatAttrSize   = 8\n)\n\n// Transfer types\nconst (\n\tTransferUpload = iota\n\tTransferDownload\n)\n\n// Supported protocols\nconst (\n\tProtocolSFTP          = \"SFTP\"\n\tProtocolSCP           = \"SCP\"\n\tProtocolSSH           = \"SSH\"\n\tProtocolFTP           = \"FTP\"\n\tProtocolWebDAV        = \"DAV\"\n\tProtocolHTTP          = \"HTTP\"\n\tProtocolHTTPShare     = \"HTTPShare\"\n\tProtocolDataRetention = \"DataRetention\"\n\tProtocolOIDC          = \"OIDC\"\n\tprotocolEventAction   = \"EventAction\"\n)\n\n// Upload modes\nconst (\n\tUploadModeStandard              = 0\n\tUploadModeAtomic                = 1\n\tUploadModeAtomicWithResume      = 2\n\tUploadModeS3StoreOnError        = 4\n\tUploadModeGCSStoreOnError       = 8\n\tUploadModeAzureBlobStoreOnError = 16\n)\n\nfunc init() {\n\tConnections.clients = clientsMap{\n\t\tclients: make(map[string]int),\n\t}\n\tConnections.transfers = clientsMap{\n\t\tclients: make(map[string]int),\n\t}\n\tConnections.perUserConns = make(map[string]int)\n\tConnections.mapping = make(map[string]int)\n\tConnections.sshMapping = make(map[string]int)\n}\n\n// errors definitions\nvar (\n\tErrPermissionDenied  = errors.New(\"permission denied\")\n\tErrNotExist          = errors.New(\"no such file or directory\")\n\tErrOpUnsupported     = errors.New(\"operation unsupported\")\n\tErrGenericFailure    = errors.New(\"failure\")\n\tErrQuotaExceeded     = errors.New(\"denying write due to space limit\")\n\tErrReadQuotaExceeded = errors.New(\"denying read due to quota limit\")\n\tErrConnectionDenied  = errors.New(\"you are not allowed to connect\")\n\tErrNoBinding         = errors.New(\"no binding configured\")\n\tErrCrtRevoked        = errors.New(\"your certificate has been revoked\")\n\tErrNoCredentials     = errors.New(\"no credential provided\")\n\tErrInternalFailure   = errors.New(\"internal failure\")\n\tErrTransferAborted   = errors.New(\"transfer aborted\")\n\tErrShuttingDown      = errors.New(\"the service is shutting down\")\n\terrNoTransfer        = errors.New(\"requested transfer not found\")\n\terrTransferMismatch  = errors.New(\"transfer mismatch\")\n)\n\nvar (\n\t// Config is the configuration for the supported protocols\n\tConfig Configuration\n\t// Connections is the list of active connections\n\tConnections ActiveConnections\n\t// QuotaScans is the list of active quota scans\n\tQuotaScans         ActiveScans\n\ttransfersChecker   TransfersChecker\n\tsupportedProtocols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP, ProtocolWebDAV,\n\t\tProtocolHTTP, ProtocolHTTPShare, ProtocolOIDC}\n\tdisconnHookProtocols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP}\n\t// the map key is the protocol, for each protocol we can have multiple rate limiters\n\trateLimiters     map[string][]*rateLimiter\n\tisShuttingDown   atomic.Bool\n\tftpLoginCommands = []string{\"PASS\", \"USER\"}\n\tfnUpdateBranding func(*dataprovider.BrandingConfigs)\n)\n\n// SetUpdateBrandingFn sets the function to call to update branding configs.\nfunc SetUpdateBrandingFn(fn func(*dataprovider.BrandingConfigs)) {\n\tfnUpdateBranding = fn\n}\n\n// Initialize sets the common configuration\nfunc Initialize(c Configuration, isShared int) error {\n\tisShuttingDown.Store(false)\n\tutil.SetUmask(c.Umask)\n\tversion.SetConfig(c.ServerVersion)\n\tdataprovider.SetTZ(c.TZ)\n\tConfig = c\n\tConfig.Actions.ExecuteOn = util.RemoveDuplicates(Config.Actions.ExecuteOn, true)\n\tConfig.Actions.ExecuteSync = util.RemoveDuplicates(Config.Actions.ExecuteSync, true)\n\tConfig.ProxyAllowed = util.RemoveDuplicates(Config.ProxyAllowed, true)\n\tConfig.idleLoginTimeout = 2 * time.Minute\n\tConfig.idleTimeoutAsDuration = time.Duration(Config.IdleTimeout) * time.Minute\n\tstartPeriodicChecks(periodicTimeoutCheckInterval, isShared)\n\tConfig.defender = nil\n\tConfig.allowList = nil\n\tConfig.rateLimitersList = nil\n\trateLimiters = make(map[string][]*rateLimiter)\n\tfor _, rlCfg := range c.RateLimitersConfig {\n\t\tif rlCfg.isEnabled() {\n\t\t\tif err := rlCfg.validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"rate limiters initialization error: %w\", err)\n\t\t\t}\n\t\t\trateLimiter := rlCfg.getLimiter()\n\t\t\tfor _, protocol := range rlCfg.Protocols {\n\t\t\t\trateLimiters[protocol] = append(rateLimiters[protocol], rateLimiter)\n\t\t\t}\n\t\t}\n\t}\n\tif len(rateLimiters) > 0 {\n\t\trateLimitersList, err := dataprovider.NewIPList(dataprovider.IPListTypeRateLimiterSafeList)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to initialize ratelimiters list: %w\", err)\n\t\t}\n\t\tConfig.rateLimitersList = rateLimitersList\n\t}\n\tif c.DefenderConfig.Enabled {\n\t\tif !slices.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {\n\t\t\treturn fmt.Errorf(\"unsupported defender driver %q\", c.DefenderConfig.Driver)\n\t\t}\n\t\tvar defender Defender\n\t\tvar err error\n\t\tswitch c.DefenderConfig.Driver {\n\t\tcase DefenderDriverProvider:\n\t\t\tdefender, err = newDBDefender(&c.DefenderConfig)\n\t\tdefault:\n\t\t\tdefender, err = newInMemoryDefender(&c.DefenderConfig)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"defender initialization error: %v\", err)\n\t\t}\n\t\tlogger.Info(logSender, \"\", \"defender initialized with config %+v\", c.DefenderConfig)\n\t\tConfig.defender = defender\n\t}\n\tif c.AllowListStatus > 0 {\n\t\tallowList, err := dataprovider.NewIPList(dataprovider.IPListTypeAllowList)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to initialize the allow list: %w\", err)\n\t\t}\n\t\tlogger.Info(logSender, \"\", \"allow list initialized\")\n\t\tConfig.allowList = allowList\n\t}\n\tif err := c.initializeProxyProtocol(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.EventManager.validate(); err != nil {\n\t\treturn err\n\t}\n\tvfs.SetTempPath(c.TempPath)\n\tdataprovider.SetTempPath(c.TempPath)\n\tvfs.SetAllowSelfConnections(c.AllowSelfConnections)\n\tvfs.SetRenameMode(c.RenameMode)\n\tvfs.SetReadMetadataMode(c.Metadata.Read)\n\tvfs.SetResumeMaxSize(c.ResumeMaxSize)\n\tvfs.SetUploadMode(c.UploadMode)\n\tdataprovider.SetAllowSelfConnections(c.AllowSelfConnections)\n\tdataprovider.EnabledActionCommands = c.EventManager.EnabledCommands\n\ttransfersChecker = getTransfersChecker(isShared)\n\treturn nil\n}\n\n// CheckClosing returns an error if the service is closing\nfunc CheckClosing() error {\n\tif isShuttingDown.Load() {\n\t\treturn ErrShuttingDown\n\t}\n\treturn nil\n}\n\n// WaitForTransfers waits, for the specified grace time, for currently ongoing\n// client-initiated transfer sessions to completes.\n// A zero graceTime means no wait\nfunc WaitForTransfers(graceTime int) {\n\tif graceTime == 0 {\n\t\treturn\n\t}\n\tif isShuttingDown.Swap(true) {\n\t\treturn\n\t}\n\n\tif activeHooks.Load() == 0 && getActiveConnections() == 0 {\n\t\treturn\n\t}\n\n\tgraceTimer := time.NewTimer(time.Duration(graceTime) * time.Second)\n\tticker := time.NewTicker(3 * time.Second)\n\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\thooks := activeHooks.Load()\n\t\t\tlogger.Info(logSender, \"\", \"active hooks: %d\", hooks)\n\t\t\tif hooks == 0 && getActiveConnections() == 0 {\n\t\t\t\tlogger.Info(logSender, \"\", \"no more active connections, graceful shutdown\")\n\t\t\t\tticker.Stop()\n\t\t\t\tgraceTimer.Stop()\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-graceTimer.C:\n\t\t\tlogger.Info(logSender, \"\", \"grace time expired, hard shutdown\")\n\t\t\tticker.Stop()\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// getActiveConnections returns the number of connections with active transfers\nfunc getActiveConnections() int {\n\tvar activeConns int\n\n\tConnections.RLock()\n\tfor _, c := range Connections.connections {\n\t\tif len(c.GetTransfers()) > 0 {\n\t\t\tactiveConns++\n\t\t}\n\t}\n\tConnections.RUnlock()\n\n\tlogger.Info(logSender, \"\", \"number of connections with active transfers: %d\", activeConns)\n\treturn activeConns\n}\n\n// LimitRate blocks until all the configured rate limiters\n// allow one event to happen.\n// It returns an error if the time to wait exceeds the max\n// allowed delay\nfunc LimitRate(protocol, ip string) (time.Duration, error) {\n\tif Config.rateLimitersList != nil {\n\t\tisListed, _, err := Config.rateLimitersList.IsListed(ip, protocol)\n\t\tif err == nil && isListed {\n\t\t\treturn 0, nil\n\t\t}\n\t}\n\tfor _, limiter := range rateLimiters[protocol] {\n\t\tif delay, err := limiter.Wait(ip, protocol); err != nil {\n\t\t\tlogger.Debug(logSender, \"\", \"protocol %s ip %s: %v\", protocol, ip, err)\n\t\t\treturn delay, err\n\t\t}\n\t}\n\treturn 0, nil\n}\n\n// Reload reloads the whitelist, the IP filter plugin and the defender's block and safe lists\nfunc Reload() error {\n\tplugin.Handler.ReloadFilter()\n\treturn nil\n}\n\n// DelayLogin applies the configured login delay\nfunc DelayLogin(err error) {\n\tif Config.defender != nil {\n\t\tConfig.defender.DelayLogin(err)\n\t}\n}\n\n// IsBanned returns true if the specified IP address is banned\nfunc IsBanned(ip, protocol string) bool {\n\tif plugin.Handler.IsIPBanned(ip, protocol) {\n\t\treturn true\n\t}\n\tif Config.defender == nil {\n\t\treturn false\n\t}\n\n\treturn Config.defender.IsBanned(ip, protocol)\n}\n\n// GetDefenderBanTime returns the ban time for the given IP\n// or nil if the IP is not banned or the defender is disabled\nfunc GetDefenderBanTime(ip string) (*time.Time, error) {\n\tif Config.defender == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn Config.defender.GetBanTime(ip)\n}\n\n// GetDefenderHosts returns hosts that are banned or for which some violations have been detected\nfunc GetDefenderHosts() ([]dataprovider.DefenderEntry, error) {\n\tif Config.defender == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn Config.defender.GetHosts()\n}\n\n// GetDefenderHost returns a defender host by ip, if any\nfunc GetDefenderHost(ip string) (dataprovider.DefenderEntry, error) {\n\tif Config.defender == nil {\n\t\treturn dataprovider.DefenderEntry{}, errors.New(\"defender is disabled\")\n\t}\n\n\treturn Config.defender.GetHost(ip)\n}\n\n// DeleteDefenderHost removes the specified IP address from the defender lists\nfunc DeleteDefenderHost(ip string) bool {\n\tif Config.defender == nil {\n\t\treturn false\n\t}\n\n\treturn Config.defender.DeleteHost(ip)\n}\n\n// GetDefenderScore returns the score for the given IP\nfunc GetDefenderScore(ip string) (int, error) {\n\tif Config.defender == nil {\n\t\treturn 0, nil\n\t}\n\n\treturn Config.defender.GetScore(ip)\n}\n\n// AddDefenderEvent adds the specified defender event for the given IP.\n// Returns true if the IP is in the defender's safe list.\nfunc AddDefenderEvent(ip, protocol string, event HostEvent) bool {\n\tif Config.defender == nil {\n\t\treturn false\n\t}\n\n\treturn Config.defender.AddEvent(ip, protocol, event)\n}\n\nfunc reloadProviderConfigs() {\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to load config from provider: %v\", err)\n\t\treturn\n\t}\n\tconfigs.SetNilsToEmpty()\n\tif fnUpdateBranding != nil {\n\t\tfnUpdateBranding(configs.Branding)\n\t}\n\tif err := configs.SMTP.TryDecrypt(); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to decrypt smtp config: %v\", err)\n\t\treturn\n\t}\n\tsmtp.Activate(configs.SMTP)\n}\n\nfunc startPeriodicChecks(duration time.Duration, isShared int) {\n\tstartEventScheduler()\n\tspec := fmt.Sprintf(\"@every %s\", duration)\n\t_, err := eventScheduler.AddFunc(spec, Connections.checkTransfers)\n\tutil.PanicOnError(err)\n\tlogger.Info(logSender, \"\", \"scheduled overquota transfers check, schedule %q\", spec)\n\tif isShared == 1 {\n\t\tlogger.Info(logSender, \"\", \"add reload configs task\")\n\t\t_, err := eventScheduler.AddFunc(\"@every 10m\", reloadProviderConfigs)\n\t\tutil.PanicOnError(err)\n\t}\n\tif Config.IdleTimeout > 0 {\n\t\tratio := idleTimeoutCheckInterval / periodicTimeoutCheckInterval\n\t\tspec = fmt.Sprintf(\"@every %s\", duration*ratio)\n\t\t_, err = eventScheduler.AddFunc(spec, Connections.checkIdles)\n\t\tutil.PanicOnError(err)\n\t\tlogger.Info(logSender, \"\", \"scheduled idle connections check, schedule %q\", spec)\n\t}\n}\n\n// ActiveTransfer defines the interface for the current active transfers\ntype ActiveTransfer interface {\n\tGetID() int64\n\tGetType() int\n\tGetSize() int64\n\tGetDownloadedSize() int64\n\tGetUploadedSize() int64\n\tGetVirtualPath() string\n\tGetFsPath() string\n\tGetStartTime() time.Time\n\tSignalClose(err error)\n\tTruncate(fsPath string, size int64) (int64, error)\n\tGetRealFsPath(fsPath string) string\n\tSetTimes(fsPath string, atime time.Time, mtime time.Time) bool\n\tGetTruncatedSize() int64\n\tHasSizeLimit() bool\n}\n\n// ActiveConnection defines the interface for the current active connections\ntype ActiveConnection interface {\n\tGetID() string\n\tGetUsername() string\n\tGetRole() string\n\tGetMaxSessions() int\n\tGetLocalAddress() string\n\tGetRemoteAddress() string\n\tGetClientVersion() string\n\tGetProtocol() string\n\tGetConnectionTime() time.Time\n\tGetLastActivity() time.Time\n\tGetCommand() string\n\tDisconnect() error\n\tAddTransfer(t ActiveTransfer)\n\tRemoveTransfer(t ActiveTransfer)\n\tGetTransfers() []ConnectionTransfer\n\tSignalTransferClose(transferID int64, err error)\n\tCloseFS() error\n\tisAccessAllowed() bool\n}\n\n// StatAttributes defines the attributes for set stat commands\ntype StatAttributes struct {\n\tMode  os.FileMode\n\tAtime time.Time\n\tMtime time.Time\n\tUID   int\n\tGID   int\n\tFlags int\n\tSize  int64\n}\n\n// ConnectionTransfer defines the trasfer details\ntype ConnectionTransfer struct {\n\tID            int64  `json:\"-\"`\n\tOperationType string `json:\"operation_type\"`\n\tStartTime     int64  `json:\"start_time\"`\n\tSize          int64  `json:\"size\"`\n\tVirtualPath   string `json:\"path\"`\n\tHasSizeLimit  bool   `json:\"-\"`\n\tULSize        int64  `json:\"-\"`\n\tDLSize        int64  `json:\"-\"`\n}\n\n// EventManagerConfig defines the configuration for the EventManager\ntype EventManagerConfig struct {\n\t// EnabledCommands defines the system commands that can be executed via EventManager,\n\t// an empty list means that any command is allowed to be executed.\n\t// Commands must be set as an absolute path\n\tEnabledCommands []string `json:\"enabled_commands\" mapstructure:\"enabled_commands\"`\n}\n\nfunc (c *EventManagerConfig) validate() error {\n\tfor _, c := range c.EnabledCommands {\n\t\tif !filepath.IsAbs(c) {\n\t\t\treturn fmt.Errorf(\"invalid command %q: it must be an absolute path\", c)\n\t\t}\n\t}\n\treturn nil\n}\n\n// MetadataConfig defines how to handle metadata for cloud storage backends\ntype MetadataConfig struct {\n\t// If not zero the metadata will be read before downloads and will be\n\t// available in notifications\n\tRead int `json:\"read\" mapstructure:\"read\"`\n}\n\n// Configuration defines configuration parameters common to all supported protocols\ntype Configuration struct {\n\t// Maximum idle timeout as minutes. If a client is idle for a time that exceeds this setting it will be disconnected.\n\t// 0 means disabled\n\tIdleTimeout int `json:\"idle_timeout\" mapstructure:\"idle_timeout\"`\n\t// UploadMode 0 means standard, the files are uploaded directly to the requested path.\n\t// 1 means atomic: the files are uploaded to a temporary path and renamed to the requested path\n\t// when the client ends the upload. Atomic mode avoid problems such as a web server that\n\t// serves partial files when the files are being uploaded.\n\t// In atomic mode if there is an upload error the temporary file is deleted and so the requested\n\t// upload path will not contain a partial file.\n\t// 2 means atomic with resume support: as atomic but if there is an upload error the temporary\n\t// file is renamed to the requested path and not deleted, this way a client can reconnect and resume\n\t// the upload.\n\t// 4 means files for S3 backend are stored even if a client-side upload error is detected.\n\t// 8 means files for Google Cloud Storage backend are stored even if a client-side upload error is detected.\n\t// 16 means files for Azure Blob backend are stored even if a client-side upload error is detected.\n\tUploadMode int `json:\"upload_mode\" mapstructure:\"upload_mode\"`\n\t// Actions to execute for SFTP file operations and SSH commands\n\tActions ProtocolActions `json:\"actions\" mapstructure:\"actions\"`\n\t// SetstatMode 0 means \"normal mode\": requests for changing permissions and owner/group are executed.\n\t// 1 means \"ignore mode\": requests for changing permissions and owner/group are silently ignored.\n\t// 2 means \"ignore mode for cloud fs\": requests for changing permissions and owner/group are\n\t// silently ignored for cloud based filesystem such as S3, GCS, Azure Blob. Requests  for changing\n\t// modification times are ignored for cloud based filesystem if they are not supported.\n\tSetstatMode int `json:\"setstat_mode\" mapstructure:\"setstat_mode\"`\n\t// RenameMode defines how to handle directory renames. By default, renaming of non-empty directories\n\t// is not allowed for cloud storage providers (S3, GCS, Azure Blob). Set to 1 to enable recursive\n\t// renames for these providers, they may be slow, there is no atomic rename API like for local\n\t// filesystem, so SFTPGo will recursively list the directory contents and do a rename for each entry\n\tRenameMode int `json:\"rename_mode\" mapstructure:\"rename_mode\"`\n\t// ResumeMaxSize defines the maximum size allowed, in bytes, to resume uploads on storage backends\n\t// with immutable objects. By default, resuming uploads is not allowed for cloud storage providers\n\t// (S3, GCS, Azure Blob) because SFTPGo must rewrite the entire file.\n\t// Set to a value greater than 0 to allow resuming uploads of files smaller than or equal to the\n\t// defined size.\n\tResumeMaxSize int64 `json:\"resume_max_size\" mapstructure:\"resume_max_size\"`\n\t// TempPath defines the path for temporary files such as those used for atomic uploads or file pipes.\n\t// If you set this option you must make sure that the defined path exists, is accessible for writing\n\t// by the user running SFTPGo, and is on the same filesystem as the users home directories otherwise\n\t// the renaming for atomic uploads will become a copy and therefore may take a long time.\n\t// The temporary files are not namespaced. The default is generally fine. Leave empty for the default.\n\tTempPath string `json:\"temp_path\" mapstructure:\"temp_path\"`\n\t// Support for HAProxy PROXY protocol.\n\t// If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable\n\t// the proxy protocol. It provides a convenient way to safely transport connection information\n\t// such as a client's address across multiple layers of NAT or TCP proxies to get the real\n\t// client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported.\n\t// - 0 means disabled\n\t// - 1 means proxy protocol enabled. Proxy header will be used and requests without proxy header will be accepted.\n\t// - 2 means proxy protocol required. Proxy header will be used and requests without proxy header will be rejected.\n\t// If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too,\n\t// for example for HAProxy add \"send-proxy\" or \"send-proxy-v2\" to each server configuration line.\n\tProxyProtocol int `json:\"proxy_protocol\" mapstructure:\"proxy_protocol\"`\n\t// List of IP addresses and IP ranges allowed to send the proxy header.\n\t// If proxy protocol is set to 1 and we receive a proxy header from an IP that is not in the list then the\n\t// connection will be accepted and the header will be ignored.\n\t// If proxy protocol is set to 2 and we receive a proxy header from an IP that is not in the list then the\n\t// connection will be rejected.\n\tProxyAllowed []string `json:\"proxy_allowed\" mapstructure:\"proxy_allowed\"`\n\t// List of IP addresses and IP ranges for which not to read the proxy header\n\tProxySkipped []string `json:\"proxy_skipped\" mapstructure:\"proxy_skipped\"`\n\t// Absolute path to an external program or an HTTP URL to invoke as soon as SFTPGo starts.\n\t// If you define an HTTP URL it will be invoked using a `GET` request.\n\t// Please note that SFTPGo services may not yet be available when this hook is run.\n\t// Leave empty do disable.\n\tStartupHook string `json:\"startup_hook\" mapstructure:\"startup_hook\"`\n\t// Absolute path to an external program or an HTTP URL to invoke after a user connects\n\t// and before he tries to login. It allows you to reject the connection based on the source\n\t// ip address. Leave empty do disable.\n\tPostConnectHook string `json:\"post_connect_hook\" mapstructure:\"post_connect_hook\"`\n\t// Absolute path to an external program or an HTTP URL to invoke after an SSH/FTP connection ends.\n\t// Leave empty do disable.\n\tPostDisconnectHook string `json:\"post_disconnect_hook\" mapstructure:\"post_disconnect_hook\"`\n\t// Maximum number of concurrent client connections. 0 means unlimited\n\tMaxTotalConnections int `json:\"max_total_connections\" mapstructure:\"max_total_connections\"`\n\t// Maximum number of concurrent client connections from the same host (IP). 0 means unlimited\n\tMaxPerHostConnections int `json:\"max_per_host_connections\" mapstructure:\"max_per_host_connections\"`\n\t// Defines the status of the global allow list. 0 means disabled, 1 enabled.\n\t// If enabled, only the listed IPs/networks can access the configured services, all other\n\t// client connections will be dropped before they even try to authenticate.\n\t// Ensure to enable this setting only after adding some allowed ip/networks from the WebAdmin/REST API\n\tAllowListStatus int `json:\"allowlist_status\" mapstructure:\"allowlist_status\"`\n\t// Allow users on this instance to use other users/virtual folders on this instance as storage backend.\n\t// Enable this setting if you know what you are doing.\n\tAllowSelfConnections int `json:\"allow_self_connections\" mapstructure:\"allow_self_connections\"`\n\t// Defender configuration\n\tDefenderConfig DefenderConfig `json:\"defender\" mapstructure:\"defender\"`\n\t// Rate limiter configurations\n\tRateLimitersConfig []RateLimiterConfig `json:\"rate_limiters\" mapstructure:\"rate_limiters\"`\n\t// Umask for new uploads. Leave blank to use the system default.\n\tUmask string `json:\"umask\" mapstructure:\"umask\"`\n\t// Defines the server version\n\tServerVersion string `json:\"server_version\" mapstructure:\"server_version\"`\n\t// TZ defines the time zone to use for the EventManager scheduler and to\n\t// control time-based access restrictions. Set to \"local\" to use the\n\t// server's local time, otherwise UTC will be used.\n\tTZ string `json:\"tz\" mapstructure:\"tz\"`\n\t// Metadata configuration\n\tMetadata MetadataConfig `json:\"metadata\" mapstructure:\"metadata\"`\n\t// EventManager configuration\n\tEventManager          EventManagerConfig `json:\"event_manager\" mapstructure:\"event_manager\"`\n\tidleTimeoutAsDuration time.Duration\n\tidleLoginTimeout      time.Duration\n\tdefender              Defender\n\tallowList             *dataprovider.IPList\n\trateLimitersList      *dataprovider.IPList\n\tproxyAllowed          []func(net.IP) bool\n\tproxySkipped          []func(net.IP) bool\n}\n\n// IsAtomicUploadEnabled returns true if atomic upload is enabled\nfunc (c *Configuration) IsAtomicUploadEnabled() bool {\n\treturn c.UploadMode&UploadModeAtomic != 0 || c.UploadMode&UploadModeAtomicWithResume != 0\n}\n\nfunc (c *Configuration) initializeProxyProtocol() error {\n\tif c.ProxyProtocol > 0 {\n\t\tallowed, err := util.ParseAllowedIPAndRanges(c.ProxyAllowed)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid proxy allowed: %w\", err)\n\t\t}\n\t\tskipped, err := util.ParseAllowedIPAndRanges(c.ProxySkipped)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid proxy skipped: %w\", err)\n\t\t}\n\t\tConfig.proxyAllowed = allowed\n\t\tConfig.proxySkipped = skipped\n\t}\n\treturn nil\n}\n\n// GetProxyListener returns a wrapper for the given listener that supports the\n// HAProxy Proxy Protocol\nfunc (c *Configuration) GetProxyListener(listener net.Listener) (net.Listener, error) {\n\tif c.ProxyProtocol > 0 {\n\t\tdefaultPolicy := proxyproto.REQUIRE\n\t\tif c.ProxyProtocol == 1 {\n\t\t\tdefaultPolicy = proxyproto.IGNORE\n\t\t}\n\n\t\treturn &proxyproto.Listener{\n\t\t\tListener:          listener,\n\t\t\tConnPolicy:        getProxyPolicy(c.proxyAllowed, c.proxySkipped, defaultPolicy),\n\t\t\tReadHeaderTimeout: 10 * time.Second,\n\t\t}, nil\n\t}\n\treturn nil, errors.New(\"proxy protocol not configured\")\n}\n\n// GetRateLimitersStatus returns the rate limiters status\nfunc (c *Configuration) GetRateLimitersStatus() (bool, []string) {\n\tenabled := false\n\tvar protocols []string\n\tfor _, rlCfg := range c.RateLimitersConfig {\n\t\tif rlCfg.isEnabled() {\n\t\t\tenabled = true\n\t\t\tprotocols = append(protocols, rlCfg.Protocols...)\n\t\t}\n\t}\n\treturn enabled, util.RemoveDuplicates(protocols, false)\n}\n\n// IsAllowListEnabled returns true if the global allow list is enabled\nfunc (c *Configuration) IsAllowListEnabled() bool {\n\treturn c.AllowListStatus > 0\n}\n\n// ExecuteStartupHook runs the startup hook if defined\nfunc (c *Configuration) ExecuteStartupHook() error {\n\tif c.StartupHook == \"\" {\n\t\treturn nil\n\t}\n\tif strings.HasPrefix(c.StartupHook, \"http\") {\n\t\tvar url *url.URL\n\t\turl, err := url.Parse(c.StartupHook)\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"Invalid startup hook %q: %v\", c.StartupHook, err)\n\t\t\treturn err\n\t\t}\n\t\tstartTime := time.Now()\n\t\tresp, err := httpclient.RetryableGet(url.String())\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"Error executing startup hook: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tlogger.Debug(logSender, \"\", \"Startup hook executed, elapsed: %v, response code: %v\", time.Since(startTime), resp.StatusCode)\n\t\treturn nil\n\t}\n\tif !filepath.IsAbs(c.StartupHook) {\n\t\terr := fmt.Errorf(\"invalid startup hook %q\", c.StartupHook)\n\t\tlogger.Warn(logSender, \"\", \"Invalid startup hook %q\", c.StartupHook)\n\t\treturn err\n\t}\n\tstartTime := time.Now()\n\ttimeout, env, args := command.GetConfig(c.StartupHook, command.HookStartup)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, c.StartupHook, args...)\n\tcmd.Env = env\n\terr := cmd.Run()\n\tlogger.Debug(logSender, \"\", \"Startup hook executed, elapsed: %s, error: %v\", time.Since(startTime), err)\n\treturn nil\n}\n\nfunc (c *Configuration) executePostDisconnectHook(remoteAddr, protocol, username, connID string, connectionTime time.Time) {\n\tstartNewHook()\n\tdefer hookEnded()\n\n\tipAddr := util.GetIPFromRemoteAddress(remoteAddr)\n\tconnDuration := int64(time.Since(connectionTime) / time.Millisecond)\n\n\tif strings.HasPrefix(c.PostDisconnectHook, \"http\") {\n\t\tvar url *url.URL\n\t\turl, err := url.Parse(c.PostDisconnectHook)\n\t\tif err != nil {\n\t\t\tlogger.Warn(protocol, connID, \"Invalid post disconnect hook %q: %v\", c.PostDisconnectHook, err)\n\t\t\treturn\n\t\t}\n\t\tq := url.Query()\n\t\tq.Add(\"ip\", ipAddr)\n\t\tq.Add(\"protocol\", protocol)\n\t\tq.Add(\"username\", username)\n\t\tq.Add(\"connection_duration\", strconv.FormatInt(connDuration, 10))\n\t\turl.RawQuery = q.Encode()\n\t\tstartTime := time.Now()\n\t\tresp, err := httpclient.RetryableGet(url.String())\n\t\trespCode := 0\n\t\tif err == nil {\n\t\t\trespCode = resp.StatusCode\n\t\t\tresp.Body.Close()\n\t\t}\n\t\tlogger.Debug(protocol, connID, \"Post disconnect hook response code: %v, elapsed: %v, err: %v\",\n\t\t\trespCode, time.Since(startTime), err)\n\t\treturn\n\t}\n\tif !filepath.IsAbs(c.PostDisconnectHook) {\n\t\tlogger.Debug(protocol, connID, \"invalid post disconnect hook %q\", c.PostDisconnectHook)\n\t\treturn\n\t}\n\ttimeout, env, args := command.GetConfig(c.PostDisconnectHook, command.HookPostDisconnect)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tstartTime := time.Now()\n\tcmd := exec.CommandContext(ctx, c.PostDisconnectHook, args...)\n\tcmd.Env = append(env,\n\t\tfmt.Sprintf(\"SFTPGO_CONNECTION_IP=%s\", ipAddr),\n\t\tfmt.Sprintf(\"SFTPGO_CONNECTION_USERNAME=%s\", username),\n\t\tfmt.Sprintf(\"SFTPGO_CONNECTION_DURATION=%d\", connDuration),\n\t\tfmt.Sprintf(\"SFTPGO_CONNECTION_PROTOCOL=%s\", protocol))\n\terr := cmd.Run()\n\tlogger.Debug(protocol, connID, \"Post disconnect hook executed, elapsed: %s error: %v\", time.Since(startTime), err)\n}\n\nfunc (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username, connID string, connectionTime time.Time) {\n\tif c.PostDisconnectHook == \"\" {\n\t\treturn\n\t}\n\tif !slices.Contains(disconnHookProtocols, protocol) {\n\t\treturn\n\t}\n\tgo c.executePostDisconnectHook(remoteAddr, protocol, username, connID, connectionTime)\n}\n\n// ExecutePostConnectHook executes the post connect hook if defined\nfunc (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {\n\tif c.PostConnectHook == \"\" {\n\t\treturn nil\n\t}\n\tif strings.HasPrefix(c.PostConnectHook, \"http\") {\n\t\tvar url *url.URL\n\t\turl, err := url.Parse(c.PostConnectHook)\n\t\tif err != nil {\n\t\t\tlogger.Warn(protocol, \"\", \"Login from ip %q denied, invalid post connect hook %q: %v\",\n\t\t\t\tipAddr, c.PostConnectHook, err)\n\t\t\treturn getPermissionDeniedError(protocol)\n\t\t}\n\t\tq := url.Query()\n\t\tq.Add(\"ip\", ipAddr)\n\t\tq.Add(\"protocol\", protocol)\n\t\turl.RawQuery = q.Encode()\n\n\t\tresp, err := httpclient.RetryableGet(url.String())\n\t\tif err != nil {\n\t\t\tlogger.Warn(protocol, \"\", \"Login from ip %q denied, error executing post connect hook: %v\", ipAddr, err)\n\t\t\treturn getPermissionDeniedError(protocol)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tlogger.Warn(protocol, \"\", \"Login from ip %q denied, post connect hook response code: %v\", ipAddr, resp.StatusCode)\n\t\t\treturn getPermissionDeniedError(protocol)\n\t\t}\n\t\treturn nil\n\t}\n\tif !filepath.IsAbs(c.PostConnectHook) {\n\t\terr := fmt.Errorf(\"invalid post connect hook %q\", c.PostConnectHook)\n\t\tlogger.Warn(protocol, \"\", \"Login from ip %q denied: %v\", ipAddr, err)\n\t\treturn getPermissionDeniedError(protocol)\n\t}\n\ttimeout, env, args := command.GetConfig(c.PostConnectHook, command.HookPostConnect)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, c.PostConnectHook, args...)\n\tcmd.Env = append(env,\n\t\tfmt.Sprintf(\"SFTPGO_CONNECTION_IP=%s\", ipAddr),\n\t\tfmt.Sprintf(\"SFTPGO_CONNECTION_PROTOCOL=%s\", protocol))\n\terr := cmd.Run()\n\tif err != nil {\n\t\tlogger.Warn(protocol, \"\", \"Login from ip %q denied, connect hook error: %v\", ipAddr, err)\n\t\treturn getPermissionDeniedError(protocol)\n\t}\n\treturn nil\n}\n\nfunc getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy) proxyproto.ConnPolicyFunc {\n\treturn func(connPolicyOptions proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {\n\t\tupstreamIP, err := util.GetIPFromNetAddr(connPolicyOptions.Upstream)\n\t\tif err != nil {\n\t\t\t// Something is wrong with the source IP, better reject the\n\t\t\t// connection.\n\t\t\tlogger.Error(logSender, \"\", \"reject connection from ip %q, err: %v\", connPolicyOptions.Upstream, err)\n\t\t\treturn proxyproto.REJECT, proxyproto.ErrInvalidUpstream\n\t\t}\n\n\t\tfor _, skippedFrom := range skipped {\n\t\t\tif skippedFrom(upstreamIP) {\n\t\t\t\treturn proxyproto.SKIP, nil\n\t\t\t}\n\t\t}\n\n\t\tfor _, allowFrom := range allowed {\n\t\t\tif allowFrom(upstreamIP) {\n\t\t\t\tif def == proxyproto.REQUIRE {\n\t\t\t\t\treturn proxyproto.REQUIRE, nil\n\t\t\t\t}\n\t\t\t\treturn proxyproto.USE, nil\n\t\t\t}\n\t\t}\n\n\t\tif def == proxyproto.REQUIRE {\n\t\t\tlogger.Debug(logSender, \"\", \"reject connection from ip %q: proxy protocol signature required and not set\",\n\t\t\t\tupstreamIP)\n\t\t\treturn proxyproto.REJECT, proxyproto.ErrInvalidUpstream\n\t\t}\n\t\treturn def, nil\n\t}\n}\n\n// SSHConnection defines an ssh connection.\n// Each SSH connection can open several channels for SFTP or SSH commands\ntype SSHConnection struct {\n\tid           string\n\tconn         io.Closer\n\tlastActivity atomic.Int64\n}\n\n// NewSSHConnection returns a new SSHConnection\nfunc NewSSHConnection(id string, conn io.Closer) *SSHConnection {\n\tc := &SSHConnection{\n\t\tid:   id,\n\t\tconn: conn,\n\t}\n\tc.lastActivity.Store(time.Now().UnixNano())\n\treturn c\n}\n\n// GetID returns the ID for this SSHConnection\nfunc (c *SSHConnection) GetID() string {\n\treturn c.id\n}\n\n// UpdateLastActivity updates last activity for this connection\nfunc (c *SSHConnection) UpdateLastActivity() {\n\tc.lastActivity.Store(time.Now().UnixNano())\n}\n\n// GetLastActivity returns the last connection activity\nfunc (c *SSHConnection) GetLastActivity() time.Time {\n\treturn time.Unix(0, c.lastActivity.Load())\n}\n\n// Close closes the underlying network connection\nfunc (c *SSHConnection) Close() error {\n\treturn c.conn.Close()\n}\n\n// ActiveConnections holds the currect active connections with the associated transfers\ntype ActiveConnections struct {\n\t// clients contains both authenticated and estabilished connections and the ones waiting\n\t// for authentication\n\tclients clientsMap\n\t// transfers contains active transfers, total and per-user\n\ttransfers            clientsMap\n\ttransfersCheckStatus atomic.Bool\n\tsync.RWMutex\n\tconnections    []ActiveConnection\n\tmapping        map[string]int\n\tsshConnections []*SSHConnection\n\tsshMapping     map[string]int\n\tperUserConns   map[string]int\n}\n\n// internal method, must be called within a locked block\nfunc (conns *ActiveConnections) addUserConnection(username string) {\n\tif username == \"\" {\n\t\treturn\n\t}\n\tconns.perUserConns[username]++\n}\n\n// internal method, must be called within a locked block\nfunc (conns *ActiveConnections) removeUserConnection(username string) {\n\tif username == \"\" {\n\t\treturn\n\t}\n\tif val, ok := conns.perUserConns[username]; ok {\n\t\tconns.perUserConns[username]--\n\t\tif val > 1 {\n\t\t\treturn\n\t\t}\n\t\tdelete(conns.perUserConns, username)\n\t}\n}\n\n// GetActiveSessions returns the number of active sessions for the given username.\n// We return the open sessions for any protocol\nfunc (conns *ActiveConnections) GetActiveSessions(username string) int {\n\tconns.RLock()\n\tdefer conns.RUnlock()\n\n\treturn conns.perUserConns[username]\n}\n\n// Add adds a new connection to the active ones\nfunc (conns *ActiveConnections) Add(c ActiveConnection) error {\n\tconns.Lock()\n\tdefer conns.Unlock()\n\n\tif username := c.GetUsername(); username != \"\" {\n\t\tif maxSessions := c.GetMaxSessions(); maxSessions > 0 {\n\t\t\tif val := conns.perUserConns[username]; val >= maxSessions {\n\t\t\t\treturn fmt.Errorf(\"too many open sessions: %d/%d\", val, maxSessions)\n\t\t\t}\n\t\t\tif val := conns.transfers.getTotalFrom(username); val >= maxSessions {\n\t\t\t\treturn fmt.Errorf(\"too many open transfers: %d/%d\", val, maxSessions)\n\t\t\t}\n\t\t}\n\t\tconns.addUserConnection(username)\n\t}\n\tconns.mapping[c.GetID()] = len(conns.connections)\n\tconns.connections = append(conns.connections, c)\n\tmetric.UpdateActiveConnectionsSize(len(conns.connections))\n\tlogger.Debug(c.GetProtocol(), c.GetID(), \"connection added, local address %q, remote address %q, num open connections: %d\",\n\t\tc.GetLocalAddress(), c.GetRemoteAddress(), len(conns.connections))\n\treturn nil\n}\n\n// Swap replaces an existing connection with the given one.\n// This method is useful if you have to change some connection details\n// for example for FTP is used to update the connection once the user\n// authenticates\nfunc (conns *ActiveConnections) Swap(c ActiveConnection) error {\n\tconns.Lock()\n\tdefer conns.Unlock()\n\n\tif idx, ok := conns.mapping[c.GetID()]; ok {\n\t\tconn := conns.connections[idx]\n\t\tconns.removeUserConnection(conn.GetUsername())\n\t\tif username := c.GetUsername(); username != \"\" {\n\t\t\tif maxSessions := c.GetMaxSessions(); maxSessions > 0 {\n\t\t\t\tif val, ok := conns.perUserConns[username]; ok && val >= maxSessions {\n\t\t\t\t\tconns.addUserConnection(conn.GetUsername())\n\t\t\t\t\treturn fmt.Errorf(\"too many open sessions: %d/%d\", val, maxSessions)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconns.addUserConnection(username)\n\t\t}\n\t\terr := conn.CloseFS()\n\t\tconns.connections[idx] = c\n\t\tlogger.Debug(logSender, c.GetID(), \"connection swapped, close fs error: %v\", err)\n\t\tconn = nil\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"connection to swap not found\")\n}\n\n// Remove removes a connection from the active ones\nfunc (conns *ActiveConnections) Remove(connectionID string) {\n\tconns.Lock()\n\tdefer conns.Unlock()\n\n\tif idx, ok := conns.mapping[connectionID]; ok {\n\t\tconn := conns.connections[idx]\n\t\terr := conn.CloseFS()\n\t\tlastIdx := len(conns.connections) - 1\n\t\tconns.connections[idx] = conns.connections[lastIdx]\n\t\tconns.connections[lastIdx] = nil\n\t\tconns.connections = conns.connections[:lastIdx]\n\t\tdelete(conns.mapping, connectionID)\n\t\tif idx != lastIdx {\n\t\t\tconns.mapping[conns.connections[idx].GetID()] = idx\n\t\t}\n\t\tconns.removeUserConnection(conn.GetUsername())\n\t\tmetric.UpdateActiveConnectionsSize(lastIdx)\n\t\tlogger.Debug(conn.GetProtocol(), conn.GetID(), \"connection removed, local address %q, remote address %q close fs error: %v, num open connections: %d\",\n\t\t\tconn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)\n\t\tif conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == \"\" && !slices.Contains(ftpLoginCommands, conn.GetCommand()) {\n\t\t\tip := util.GetIPFromRemoteAddress(conn.GetRemoteAddress())\n\t\t\tlogger.ConnectionFailedLog(\"\", ip, dataprovider.LoginMethodNoAuthTried, ProtocolFTP,\n\t\t\t\tdataprovider.ErrNoAuthTried.Error())\n\t\t\tmetric.AddNoAuthTried()\n\t\t\tAddDefenderEvent(ip, ProtocolFTP, HostEventNoLoginTried)\n\t\t\tdataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTried, ip,\n\t\t\t\tProtocolFTP, dataprovider.ErrNoAuthTried)\n\t\t\tplugin.Handler.NotifyLogEvent(notifier.LogEventTypeNoLoginTried, ProtocolFTP, \"\", ip, \"\",\n\t\t\t\tdataprovider.ErrNoAuthTried)\n\t\t}\n\t\tConfig.checkPostDisconnectHook(conn.GetRemoteAddress(), conn.GetProtocol(), conn.GetUsername(),\n\t\t\tconn.GetID(), conn.GetConnectionTime())\n\t\treturn\n\t}\n\n\tlogger.Debug(logSender, \"\", \"connection id %q to remove not found!\", connectionID)\n}\n\n// Close closes an active connection.\n// It returns true on success\nfunc (conns *ActiveConnections) Close(connectionID, role string) bool {\n\tconns.RLock()\n\n\tvar result bool\n\n\tif idx, ok := conns.mapping[connectionID]; ok {\n\t\tc := conns.connections[idx]\n\n\t\tif role == \"\" || c.GetRole() == role {\n\t\t\tdefer func(conn ActiveConnection) {\n\t\t\t\terr := conn.Disconnect()\n\t\t\t\tlogger.Debug(conn.GetProtocol(), conn.GetID(), \"close connection requested, close err: %v\", err)\n\t\t\t}(c)\n\t\t\tresult = true\n\t\t}\n\t}\n\n\tconns.RUnlock()\n\treturn result\n}\n\n// AddSSHConnection adds a new ssh connection to the active ones\nfunc (conns *ActiveConnections) AddSSHConnection(c *SSHConnection) {\n\tconns.Lock()\n\tdefer conns.Unlock()\n\n\tconns.sshMapping[c.GetID()] = len(conns.sshConnections)\n\tconns.sshConnections = append(conns.sshConnections, c)\n\tlogger.Debug(logSender, c.GetID(), \"ssh connection added, num open connections: %d\", len(conns.sshConnections))\n}\n\n// RemoveSSHConnection removes a connection from the active ones\nfunc (conns *ActiveConnections) RemoveSSHConnection(connectionID string) {\n\tconns.Lock()\n\tdefer conns.Unlock()\n\n\tif idx, ok := conns.sshMapping[connectionID]; ok {\n\t\tlastIdx := len(conns.sshConnections) - 1\n\t\tconns.sshConnections[idx] = conns.sshConnections[lastIdx]\n\t\tconns.sshConnections[lastIdx] = nil\n\t\tconns.sshConnections = conns.sshConnections[:lastIdx]\n\t\tdelete(conns.sshMapping, connectionID)\n\t\tif idx != lastIdx {\n\t\t\tconns.sshMapping[conns.sshConnections[idx].GetID()] = idx\n\t\t}\n\t\tlogger.Debug(logSender, connectionID, \"ssh connection removed, num open ssh connections: %d\", lastIdx)\n\t\treturn\n\t}\n\tlogger.Warn(logSender, \"\", \"ssh connection to remove with id %q not found!\", connectionID)\n}\n\nfunc (conns *ActiveConnections) checkIdles() {\n\tconns.RLock()\n\n\tfor _, sshConn := range conns.sshConnections {\n\t\tidleTime := time.Since(sshConn.GetLastActivity())\n\t\tif idleTime > Config.idleTimeoutAsDuration {\n\t\t\t// we close an SSH connection if it has no active connections associated\n\t\t\tidToMatch := fmt.Sprintf(\"_%s_\", sshConn.GetID())\n\t\t\ttoClose := true\n\t\t\tfor _, conn := range conns.connections {\n\t\t\t\tif strings.Contains(conn.GetID(), idToMatch) {\n\t\t\t\t\tif time.Since(conn.GetLastActivity()) <= Config.idleTimeoutAsDuration {\n\t\t\t\t\t\ttoClose = false\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\tif toClose {\n\t\t\t\tdefer func(c *SSHConnection) {\n\t\t\t\t\terr := c.Close()\n\t\t\t\t\tlogger.Debug(logSender, c.GetID(), \"close idle SSH connection, idle time: %v, close err: %v\",\n\t\t\t\t\t\ttime.Since(c.GetLastActivity()), err)\n\t\t\t\t}(sshConn)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, c := range conns.connections {\n\t\tidleTime := time.Since(c.GetLastActivity())\n\t\tisUnauthenticatedFTPUser := (c.GetProtocol() == ProtocolFTP && c.GetUsername() == \"\")\n\n\t\tif idleTime > Config.idleTimeoutAsDuration || (isUnauthenticatedFTPUser && idleTime > Config.idleLoginTimeout) {\n\t\t\tdefer func(conn ActiveConnection) {\n\t\t\t\terr := conn.Disconnect()\n\t\t\t\tlogger.Debug(conn.GetProtocol(), conn.GetID(), \"close idle connection, idle time: %s, username: %q close err: %v\",\n\t\t\t\t\ttime.Since(conn.GetLastActivity()), conn.GetUsername(), err)\n\t\t\t}(c)\n\t\t} else if !isUnauthenticatedFTPUser && !c.isAccessAllowed() {\n\t\t\tdefer func(conn ActiveConnection) {\n\t\t\t\terr := conn.Disconnect()\n\t\t\t\tlogger.Info(conn.GetProtocol(), conn.GetID(), \"access conditions not met for user: %q close connection err: %v\",\n\t\t\t\t\tconn.GetUsername(), err)\n\t\t\t}(c)\n\t\t}\n\t}\n\n\tconns.RUnlock()\n}\n\nfunc (conns *ActiveConnections) checkTransfers() {\n\tif conns.transfersCheckStatus.Load() {\n\t\tlogger.Warn(logSender, \"\", \"the previous transfer check is still running, skipping execution\")\n\t\treturn\n\t}\n\tconns.transfersCheckStatus.Store(true)\n\tdefer conns.transfersCheckStatus.Store(false)\n\n\tconns.RLock()\n\n\tif len(conns.connections) < 2 {\n\t\tconns.RUnlock()\n\t\treturn\n\t}\n\tvar wg sync.WaitGroup\n\tlogger.Debug(logSender, \"\", \"start concurrent transfers check\")\n\n\t// update the current size for transfers to monitors\n\tfor _, c := range conns.connections {\n\t\tfor _, t := range c.GetTransfers() {\n\t\t\tif t.HasSizeLimit {\n\t\t\t\twg.Add(1)\n\n\t\t\t\tgo func(transfer ConnectionTransfer, connID string) {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\ttransfersChecker.UpdateTransferCurrentSizes(transfer.ULSize, transfer.DLSize, transfer.ID, connID)\n\t\t\t\t}(t, c.GetID())\n\t\t\t}\n\t\t}\n\t}\n\n\tconns.RUnlock()\n\tlogger.Debug(logSender, \"\", \"waiting for the update of the transfers current size\")\n\twg.Wait()\n\n\tlogger.Debug(logSender, \"\", \"getting overquota transfers\")\n\toverquotaTransfers := transfersChecker.GetOverquotaTransfers()\n\tlogger.Debug(logSender, \"\", \"number of overquota transfers: %v\", len(overquotaTransfers))\n\tif len(overquotaTransfers) == 0 {\n\t\treturn\n\t}\n\n\tconns.RLock()\n\tdefer conns.RUnlock()\n\n\tfor _, c := range conns.connections {\n\t\tfor _, overquotaTransfer := range overquotaTransfers {\n\t\t\tif c.GetID() == overquotaTransfer.ConnID {\n\t\t\t\tlogger.Info(logSender, c.GetID(), \"user %q is overquota, try to close transfer id %v\",\n\t\t\t\t\tc.GetUsername(), overquotaTransfer.TransferID)\n\t\t\t\tvar err error\n\t\t\t\tif overquotaTransfer.TransferType == TransferDownload {\n\t\t\t\t\terr = getReadQuotaExceededError(c.GetProtocol())\n\t\t\t\t} else {\n\t\t\t\t\terr = getQuotaExceededError(c.GetProtocol())\n\t\t\t\t}\n\t\t\t\tc.SignalTransferClose(overquotaTransfer.TransferID, err)\n\t\t\t}\n\t\t}\n\t}\n\tlogger.Debug(logSender, \"\", \"transfers check completed\")\n}\n\n// AddClientConnection stores a new client connection\nfunc (conns *ActiveConnections) AddClientConnection(ipAddr string) {\n\tconns.clients.add(ipAddr)\n}\n\n// RemoveClientConnection removes a disconnected client from the tracked ones\nfunc (conns *ActiveConnections) RemoveClientConnection(ipAddr string) {\n\tconns.clients.remove(ipAddr)\n}\n\n// GetClientConnections returns the total number of client connections\nfunc (conns *ActiveConnections) GetClientConnections() int32 {\n\treturn conns.clients.getTotal()\n}\n\n// GetTotalTransfers returns the total number of active transfers\nfunc (conns *ActiveConnections) GetTotalTransfers() int32 {\n\treturn conns.transfers.getTotal()\n}\n\n// IsNewTransferAllowed returns an error if the maximum number of concurrent allowed\n// transfers is exceeded\nfunc (conns *ActiveConnections) IsNewTransferAllowed(username string) error {\n\tif isShuttingDown.Load() {\n\t\treturn ErrShuttingDown\n\t}\n\tif Config.MaxTotalConnections == 0 && Config.MaxPerHostConnections == 0 {\n\t\treturn nil\n\t}\n\tif Config.MaxPerHostConnections > 0 {\n\t\tif transfers := conns.transfers.getTotalFrom(username); transfers >= Config.MaxPerHostConnections {\n\t\t\tlogger.Info(logSender, \"\", \"active transfers from user %q: %d/%d\", username, transfers, Config.MaxPerHostConnections)\n\t\t\treturn ErrConnectionDenied\n\t\t}\n\t}\n\tif Config.MaxTotalConnections > 0 {\n\t\tif transfers := conns.transfers.getTotal(); transfers >= int32(Config.MaxTotalConnections) {\n\t\t\tlogger.Info(logSender, \"\", \"active transfers %d/%d\", transfers, Config.MaxTotalConnections)\n\t\t\treturn ErrConnectionDenied\n\t\t}\n\t}\n\treturn nil\n}\n\n// IsNewConnectionAllowed returns an error if the maximum number of concurrent allowed\n// connections is exceeded or a whitelist is defined and the specified ipAddr is not listed\n// or the service is shutting down\nfunc (conns *ActiveConnections) IsNewConnectionAllowed(ipAddr, protocol string) error {\n\tif isShuttingDown.Load() {\n\t\treturn ErrShuttingDown\n\t}\n\tif Config.allowList != nil {\n\t\tisListed, _, err := Config.allowList.IsListed(ipAddr, protocol)\n\t\tif err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to query allow list, connection denied, ip %q, protocol %s, err: %v\",\n\t\t\t\tipAddr, protocol, err)\n\t\t\treturn ErrConnectionDenied\n\t\t}\n\t\tif !isListed {\n\t\t\treturn ErrConnectionDenied\n\t\t}\n\t}\n\tif Config.MaxTotalConnections == 0 && Config.MaxPerHostConnections == 0 {\n\t\treturn nil\n\t}\n\n\tif Config.MaxPerHostConnections > 0 {\n\t\tif total := conns.clients.getTotalFrom(ipAddr); total > Config.MaxPerHostConnections {\n\t\t\tif !AddDefenderEvent(ipAddr, protocol, HostEventLimitExceeded) {\n\t\t\t\tlogger.Warn(logSender, \"\", \"connection denied, active connections from IP %q: %d/%d\",\n\t\t\t\t\tipAddr, total, Config.MaxPerHostConnections)\n\t\t\t\treturn ErrConnectionDenied\n\t\t\t}\n\t\t\tlogger.Info(logSender, \"\", \"active connections from safe IP %q: %d\", ipAddr, total)\n\t\t}\n\t}\n\n\tif Config.MaxTotalConnections > 0 {\n\t\tif total := conns.clients.getTotal(); total > int32(Config.MaxTotalConnections) {\n\t\t\tlogger.Info(logSender, \"\", \"active client connections %d/%d\", total, Config.MaxTotalConnections)\n\t\t\treturn ErrConnectionDenied\n\t\t}\n\n\t\t// on a single SFTP connection we could have multiple SFTP channels or commands\n\t\t// so we check the estabilished connections and active uploads too\n\t\tif transfers := conns.transfers.getTotal(); transfers >= int32(Config.MaxTotalConnections) {\n\t\t\tlogger.Info(logSender, \"\", \"active transfers %d/%d\", transfers, Config.MaxTotalConnections)\n\t\t\treturn ErrConnectionDenied\n\t\t}\n\n\t\tconns.RLock()\n\t\tdefer conns.RUnlock()\n\n\t\tif sess := len(conns.connections); sess >= Config.MaxTotalConnections {\n\t\t\tlogger.Info(logSender, \"\", \"active client sessions %d/%d\", sess, Config.MaxTotalConnections)\n\t\t\treturn ErrConnectionDenied\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// GetStats returns stats for active connections\nfunc (conns *ActiveConnections) GetStats(role string) []ConnectionStatus {\n\tconns.RLock()\n\tdefer conns.RUnlock()\n\n\tstats := make([]ConnectionStatus, 0, len(conns.connections))\n\tnode := dataprovider.GetNodeName()\n\tfor _, c := range conns.connections {\n\t\tif role == \"\" || c.GetRole() == role {\n\t\t\tstat := ConnectionStatus{\n\t\t\t\tUsername:       c.GetUsername(),\n\t\t\t\tConnectionID:   c.GetID(),\n\t\t\t\tClientVersion:  c.GetClientVersion(),\n\t\t\t\tRemoteAddress:  c.GetRemoteAddress(),\n\t\t\t\tConnectionTime: util.GetTimeAsMsSinceEpoch(c.GetConnectionTime()),\n\t\t\t\tLastActivity:   util.GetTimeAsMsSinceEpoch(c.GetLastActivity()),\n\t\t\t\tCurrentTime:    util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\t\tProtocol:       c.GetProtocol(),\n\t\t\t\tCommand:        c.GetCommand(),\n\t\t\t\tTransfers:      c.GetTransfers(),\n\t\t\t\tNode:           node,\n\t\t\t}\n\t\t\tstats = append(stats, stat)\n\t\t}\n\t}\n\treturn stats\n}\n\n// ConnectionStatus returns the status for an active connection\ntype ConnectionStatus struct {\n\t// Logged in username\n\tUsername string `json:\"username\"`\n\t// Unique identifier for the connection\n\tConnectionID string `json:\"connection_id\"`\n\t// client's version string\n\tClientVersion string `json:\"client_version,omitempty\"`\n\t// Remote address for this connection\n\tRemoteAddress string `json:\"remote_address\"`\n\t// Connection time as unix timestamp in milliseconds\n\tConnectionTime int64 `json:\"connection_time\"`\n\t// Last activity as unix timestamp in milliseconds\n\tLastActivity int64 `json:\"last_activity\"`\n\t// Current time as unix timestamp in milliseconds\n\tCurrentTime int64 `json:\"current_time\"`\n\t// Protocol for this connection\n\tProtocol string `json:\"protocol\"`\n\t// active uploads/downloads\n\tTransfers []ConnectionTransfer `json:\"active_transfers,omitempty\"`\n\t// SSH command or WebDAV method\n\tCommand string `json:\"command,omitempty\"`\n\t// Node identifier, omitted for single node installations\n\tNode string `json:\"node,omitempty\"`\n}\n\n// ActiveQuotaScan defines an active quota scan for a user\ntype ActiveQuotaScan struct {\n\t// Username to which the quota scan refers\n\tUsername string `json:\"username\"`\n\t// quota scan start time as unix timestamp in milliseconds\n\tStartTime int64  `json:\"start_time\"`\n\tRole      string `json:\"-\"`\n}\n\n// ActiveVirtualFolderQuotaScan defines an active quota scan for a virtual folder\ntype ActiveVirtualFolderQuotaScan struct {\n\t// folder name to which the quota scan refers\n\tName string `json:\"name\"`\n\t// quota scan start time as unix timestamp in milliseconds\n\tStartTime int64 `json:\"start_time\"`\n}\n\n// ActiveScans holds the active quota scans\ntype ActiveScans struct {\n\tsync.RWMutex\n\tUserScans   []ActiveQuotaScan\n\tFolderScans []ActiveVirtualFolderQuotaScan\n}\n\n// GetUsersQuotaScans returns the active users quota scans\nfunc (s *ActiveScans) GetUsersQuotaScans(role string) []ActiveQuotaScan {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\tscans := make([]ActiveQuotaScan, 0, len(s.UserScans))\n\tfor _, scan := range s.UserScans {\n\t\tif role == \"\" || role == scan.Role {\n\t\t\tscans = append(scans, ActiveQuotaScan{\n\t\t\t\tUsername:  scan.Username,\n\t\t\t\tStartTime: scan.StartTime,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn scans\n}\n\n// AddUserQuotaScan adds a user to the ones with active quota scans.\n// Returns false if the user has a quota scan already running\nfunc (s *ActiveScans) AddUserQuotaScan(username, role string) bool {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\tfor _, scan := range s.UserScans {\n\t\tif scan.Username == username {\n\t\t\treturn false\n\t\t}\n\t}\n\ts.UserScans = append(s.UserScans, ActiveQuotaScan{\n\t\tUsername:  username,\n\t\tStartTime: util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tRole:      role,\n\t})\n\treturn true\n}\n\n// RemoveUserQuotaScan removes a user from the ones with active quota scans.\n// Returns false if the user has no active quota scans\nfunc (s *ActiveScans) RemoveUserQuotaScan(username string) bool {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\tfor idx, scan := range s.UserScans {\n\t\tif scan.Username == username {\n\t\t\tlastIdx := len(s.UserScans) - 1\n\t\t\ts.UserScans[idx] = s.UserScans[lastIdx]\n\t\t\ts.UserScans = s.UserScans[:lastIdx]\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// GetVFoldersQuotaScans returns the active quota scans for virtual folders\nfunc (s *ActiveScans) GetVFoldersQuotaScans() []ActiveVirtualFolderQuotaScan {\n\ts.RLock()\n\tdefer s.RUnlock()\n\tscans := make([]ActiveVirtualFolderQuotaScan, len(s.FolderScans))\n\tcopy(scans, s.FolderScans)\n\treturn scans\n}\n\n// AddVFolderQuotaScan adds a virtual folder to the ones with active quota scans.\n// Returns false if the folder has a quota scan already running\nfunc (s *ActiveScans) AddVFolderQuotaScan(folderName string) bool {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\tfor _, scan := range s.FolderScans {\n\t\tif scan.Name == folderName {\n\t\t\treturn false\n\t\t}\n\t}\n\ts.FolderScans = append(s.FolderScans, ActiveVirtualFolderQuotaScan{\n\t\tName:      folderName,\n\t\tStartTime: util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\treturn true\n}\n\n// RemoveVFolderQuotaScan removes a folder from the ones with active quota scans.\n// Returns false if the folder has no active quota scans\nfunc (s *ActiveScans) RemoveVFolderQuotaScan(folderName string) bool {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\tfor idx, scan := range s.FolderScans {\n\t\tif scan.Name == folderName {\n\t\t\tlastIdx := len(s.FolderScans) - 1\n\t\t\ts.FolderScans[idx] = s.FolderScans[lastIdx]\n\t\t\ts.FolderScans = s.FolderScans[:lastIdx]\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/common/common_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/alexedwards/argon2id\"\n\t\"github.com/pires/go-proxyproto\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/bcrypt\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tlogSenderTest    = \"common_test\"\n\thttpAddr         = \"127.0.0.1:9999\"\n\tosWindows        = \"windows\"\n\tuserTestUsername = \"common_test_username\"\n)\n\nvar (\n\tconfigDir = filepath.Join(\".\", \"..\", \"..\")\n)\n\ntype fakeConnection struct {\n\t*BaseConnection\n\tcommand string\n}\n\nfunc (c *fakeConnection) AddUser(user dataprovider.User) error {\n\t_, err := user.GetFilesystem(c.GetID())\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.User = user\n\treturn nil\n}\n\nfunc (c *fakeConnection) Disconnect() error {\n\tConnections.Remove(c.GetID())\n\treturn nil\n}\n\nfunc (c *fakeConnection) GetClientVersion() string {\n\treturn \"\"\n}\n\nfunc (c *fakeConnection) GetCommand() string {\n\treturn c.command\n}\n\nfunc (c *fakeConnection) GetLocalAddress() string {\n\treturn \"\"\n}\n\nfunc (c *fakeConnection) GetRemoteAddress() string {\n\treturn \"\"\n}\n\ntype customNetConn struct {\n\tnet.Conn\n\tid       string\n\tisClosed bool\n}\n\nfunc (c *customNetConn) Close() error {\n\tConnections.RemoveSSHConnection(c.id)\n\tc.isClosed = true\n\treturn c.Conn.Close()\n}\n\nfunc TestConnections(t *testing.T) {\n\tc1 := &fakeConnection{\n\t\tBaseConnection: NewBaseConnection(\"id1\", ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername: userTestUsername,\n\t\t\t},\n\t\t}),\n\t}\n\tc2 := &fakeConnection{\n\t\tBaseConnection: NewBaseConnection(\"id2\", ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername: userTestUsername,\n\t\t\t},\n\t\t}),\n\t}\n\tc3 := &fakeConnection{\n\t\tBaseConnection: NewBaseConnection(\"id3\", ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername: userTestUsername,\n\t\t\t},\n\t\t}),\n\t}\n\tc4 := &fakeConnection{\n\t\tBaseConnection: NewBaseConnection(\"id4\", ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername: userTestUsername,\n\t\t\t},\n\t\t}),\n\t}\n\tassert.Equal(t, \"SFTP_id1\", c1.GetID())\n\tassert.Equal(t, \"SFTP_id2\", c2.GetID())\n\tassert.Equal(t, \"SFTP_id3\", c3.GetID())\n\tassert.Equal(t, \"SFTP_id4\", c4.GetID())\n\terr := Connections.Add(c1)\n\tassert.NoError(t, err)\n\terr = Connections.Add(c2)\n\tassert.NoError(t, err)\n\terr = Connections.Add(c3)\n\tassert.NoError(t, err)\n\terr = Connections.Add(c4)\n\tassert.NoError(t, err)\n\n\tConnections.RLock()\n\tassert.Len(t, Connections.connections, 4)\n\tassert.Len(t, Connections.mapping, 4)\n\t_, ok := Connections.mapping[c1.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.mapping[c1.GetID()])\n\tassert.Equal(t, 1, Connections.mapping[c2.GetID()])\n\tassert.Equal(t, 2, Connections.mapping[c3.GetID()])\n\tassert.Equal(t, 3, Connections.mapping[c4.GetID()])\n\tConnections.RUnlock()\n\n\tc2 = &fakeConnection{\n\t\tBaseConnection: NewBaseConnection(\"id2\", ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername: userTestUsername + \"_mod\",\n\t\t\t},\n\t\t}),\n\t}\n\terr = Connections.Swap(c2)\n\tassert.NoError(t, err)\n\n\tConnections.RLock()\n\tassert.Len(t, Connections.connections, 4)\n\tassert.Len(t, Connections.mapping, 4)\n\t_, ok = Connections.mapping[c1.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.mapping[c1.GetID()])\n\tassert.Equal(t, 1, Connections.mapping[c2.GetID()])\n\tassert.Equal(t, 2, Connections.mapping[c3.GetID()])\n\tassert.Equal(t, 3, Connections.mapping[c4.GetID()])\n\tassert.Equal(t, userTestUsername+\"_mod\", Connections.connections[1].GetUsername())\n\tConnections.RUnlock()\n\n\tConnections.Remove(c2.GetID())\n\n\tConnections.RLock()\n\tassert.Len(t, Connections.connections, 3)\n\tassert.Len(t, Connections.mapping, 3)\n\t_, ok = Connections.mapping[c1.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.mapping[c1.GetID()])\n\tassert.Equal(t, 1, Connections.mapping[c4.GetID()])\n\tassert.Equal(t, 2, Connections.mapping[c3.GetID()])\n\tConnections.RUnlock()\n\n\tConnections.Remove(c3.GetID())\n\n\tConnections.RLock()\n\tassert.Len(t, Connections.connections, 2)\n\tassert.Len(t, Connections.mapping, 2)\n\t_, ok = Connections.mapping[c1.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.mapping[c1.GetID()])\n\tassert.Equal(t, 1, Connections.mapping[c4.GetID()])\n\tConnections.RUnlock()\n\n\tConnections.Remove(c1.GetID())\n\n\tConnections.RLock()\n\tassert.Len(t, Connections.connections, 1)\n\tassert.Len(t, Connections.mapping, 1)\n\t_, ok = Connections.mapping[c4.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.mapping[c4.GetID()])\n\tConnections.RUnlock()\n\n\tConnections.Remove(c4.GetID())\n\n\tConnections.RLock()\n\tassert.Len(t, Connections.connections, 0)\n\tassert.Len(t, Connections.mapping, 0)\n\tConnections.RUnlock()\n}\n\nfunc TestEventManagerCommandsInitialization(t *testing.T) {\n\tconfigCopy := Config\n\n\tc := Configuration{\n\t\tEventManager: EventManagerConfig{\n\t\t\tEnabledCommands: []string{\"ls\"}, // not an absolute path\n\t\t},\n\t}\n\terr := Initialize(c, 0)\n\tassert.ErrorContains(t, err, \"invalid command\")\n\n\tvar commands []string\n\tif runtime.GOOS == osWindows {\n\t\tcommands = []string{\"C:\\\\command\"}\n\t} else {\n\t\tcommands = []string{\"/bin/ls\"}\n\t}\n\n\tc.EventManager.EnabledCommands = commands\n\terr = Initialize(c, 0)\n\tassert.NoError(t, err)\n\tassert.Equal(t, commands, dataprovider.EnabledActionCommands)\n\n\tdataprovider.EnabledActionCommands = configCopy.EventManager.EnabledCommands\n\tConfig = configCopy\n}\n\nfunc TestInitializationProxyErrors(t *testing.T) {\n\tconfigCopy := Config\n\n\tc := Configuration{\n\t\tProxyProtocol: 1,\n\t\tProxyAllowed:  []string{\"1.1.1.1111\"},\n\t}\n\terr := Initialize(c, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid proxy allowed\")\n\t}\n\tc.ProxyAllowed = nil\n\tc.ProxySkipped = []string{\"invalid\"}\n\terr = Initialize(c, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid proxy skipped\")\n\t}\n\tc.ProxyAllowed = []string{\"1.1.1.1\"}\n\tc.ProxySkipped = []string{\"2.2.2.2\", \"10.8.0.0/24\"}\n\terr = Initialize(c, 0)\n\tassert.NoError(t, err)\n\tassert.Len(t, Config.proxyAllowed, 1)\n\tassert.Len(t, Config.proxySkipped, 2)\n\n\tConfig = configCopy\n\tassert.Equal(t, 0, Config.ProxyProtocol)\n\tassert.Len(t, Config.proxyAllowed, 0)\n\tassert.Len(t, Config.proxySkipped, 0)\n}\n\nfunc TestInitializationClosedProvider(t *testing.T) {\n\tconfigCopy := Config\n\n\tproviderConf := dataprovider.GetProviderConfig()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\n\tconfig := Configuration{\n\t\tAllowListStatus: 1,\n\t}\n\terr = Initialize(config, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to initialize the allow list\")\n\t}\n\n\tconfig.AllowListStatus = 0\n\tconfig.RateLimitersConfig = []RateLimiterConfig{\n\t\t{\n\t\t\tAverage:   100,\n\t\t\tPeriod:    1000,\n\t\t\tBurst:     5,\n\t\t\tType:      int(rateLimiterTypeGlobal),\n\t\t\tProtocols: rateLimiterProtocolValues,\n\t\t},\n\t}\n\terr = Initialize(config, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to initialize ratelimiters list\")\n\t}\n\n\tconfig.RateLimitersConfig = nil\n\tconfig.DefenderConfig = DefenderConfig{\n\t\tEnabled:          true,\n\t\tDriver:           DefenderDriverProvider,\n\t\tBanTime:          10,\n\t\tBanTimeIncrement: 50,\n\t\tThreshold:        10,\n\t\tScoreInvalid:     2,\n\t\tScoreValid:       1,\n\t\tScoreNoAuth:      2,\n\t\tObservationTime:  15,\n\t\tEntriesSoftLimit: 100,\n\t\tEntriesHardLimit: 150,\n\t}\n\terr = Initialize(config, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"defender initialization error\")\n\t}\n\tconfig.DefenderConfig.Driver = DefenderDriverMemory\n\terr = Initialize(config, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"defender initialization error\")\n\t}\n\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tConfig = configCopy\n}\n\nfunc TestSSHConnections(t *testing.T) {\n\tconn1, conn2 := net.Pipe()\n\tnow := time.Now()\n\tsshConn1 := NewSSHConnection(\"id1\", conn1)\n\tsshConn2 := NewSSHConnection(\"id2\", conn2)\n\tsshConn3 := NewSSHConnection(\"id3\", conn2)\n\tassert.Equal(t, \"id1\", sshConn1.GetID())\n\tassert.Equal(t, \"id2\", sshConn2.GetID())\n\tassert.Equal(t, \"id3\", sshConn3.GetID())\n\tsshConn1.UpdateLastActivity()\n\tassert.GreaterOrEqual(t, sshConn1.GetLastActivity().UnixNano(), now.UnixNano())\n\tConnections.AddSSHConnection(sshConn1)\n\tConnections.AddSSHConnection(sshConn2)\n\tConnections.AddSSHConnection(sshConn3)\n\tConnections.RLock()\n\tassert.Len(t, Connections.sshConnections, 3)\n\t_, ok := Connections.sshMapping[sshConn1.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.sshMapping[sshConn1.GetID()])\n\tassert.Equal(t, 1, Connections.sshMapping[sshConn2.GetID()])\n\tassert.Equal(t, 2, Connections.sshMapping[sshConn3.GetID()])\n\tConnections.RUnlock()\n\tConnections.RemoveSSHConnection(sshConn1.id)\n\tConnections.RLock()\n\tassert.Len(t, Connections.sshConnections, 2)\n\tassert.Equal(t, sshConn3.id, Connections.sshConnections[0].id)\n\tassert.Equal(t, sshConn2.id, Connections.sshConnections[1].id)\n\t_, ok = Connections.sshMapping[sshConn3.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.sshMapping[sshConn3.GetID()])\n\tassert.Equal(t, 1, Connections.sshMapping[sshConn2.GetID()])\n\tConnections.RUnlock()\n\tConnections.RemoveSSHConnection(sshConn1.id)\n\tConnections.RLock()\n\tassert.Len(t, Connections.sshConnections, 2)\n\tassert.Equal(t, sshConn3.id, Connections.sshConnections[0].id)\n\tassert.Equal(t, sshConn2.id, Connections.sshConnections[1].id)\n\t_, ok = Connections.sshMapping[sshConn3.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.sshMapping[sshConn3.GetID()])\n\tassert.Equal(t, 1, Connections.sshMapping[sshConn2.GetID()])\n\tConnections.RUnlock()\n\tConnections.RemoveSSHConnection(sshConn2.id)\n\tConnections.RLock()\n\tassert.Len(t, Connections.sshConnections, 1)\n\tassert.Equal(t, sshConn3.id, Connections.sshConnections[0].id)\n\t_, ok = Connections.sshMapping[sshConn3.GetID()]\n\tassert.True(t, ok)\n\tassert.Equal(t, 0, Connections.sshMapping[sshConn3.GetID()])\n\tConnections.RUnlock()\n\tConnections.RemoveSSHConnection(sshConn3.id)\n\tConnections.RLock()\n\tassert.Len(t, Connections.sshConnections, 0)\n\tassert.Len(t, Connections.sshMapping, 0)\n\tConnections.RUnlock()\n\tassert.NoError(t, sshConn1.Close())\n\tassert.NoError(t, sshConn2.Close())\n\tassert.NoError(t, sshConn3.Close())\n}\n\nfunc TestDefenderIntegration(t *testing.T) {\n\t// by default defender is nil\n\tconfigCopy := Config\n\n\twdPath, err := os.Getwd()\n\trequire.NoError(t, err)\n\tpluginsConfig := []plugin.Config{\n\t\t{\n\t\t\tType:     \"ipfilter\",\n\t\t\tCmd:      filepath.Join(wdPath, \"..\", \"..\", \"tests\", \"ipfilter\", \"ipfilter\"),\n\t\t\tAutoMTLS: true,\n\t\t},\n\t}\n\tif runtime.GOOS == osWindows {\n\t\tpluginsConfig[0].Cmd += \".exe\"\n\t}\n\terr = plugin.Initialize(pluginsConfig, \"debug\")\n\trequire.NoError(t, err)\n\n\tip := \"127.1.1.1\"\n\n\tassert.Nil(t, Reload())\n\t// 192.168.1.12 is banned from the ipfilter plugin\n\tassert.True(t, IsBanned(\"192.168.1.12\", ProtocolFTP))\n\n\tAddDefenderEvent(ip, ProtocolFTP, HostEventNoLoginTried)\n\tassert.False(t, IsBanned(ip, ProtocolFTP))\n\n\tbanTime, err := GetDefenderBanTime(ip)\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\tassert.False(t, DeleteDefenderHost(ip))\n\tscore, err := GetDefenderScore(ip)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, score)\n\t_, err = GetDefenderHost(ip)\n\tassert.Error(t, err)\n\thosts, err := GetDefenderHosts()\n\tassert.NoError(t, err)\n\tassert.Nil(t, hosts)\n\n\tConfig.DefenderConfig = DefenderConfig{\n\t\tEnabled:          true,\n\t\tDriver:           DefenderDriverProvider,\n\t\tBanTime:          10,\n\t\tBanTimeIncrement: 50,\n\t\tThreshold:        0,\n\t\tScoreInvalid:     2,\n\t\tScoreValid:       1,\n\t\tScoreNoAuth:      2,\n\t\tObservationTime:  15,\n\t\tEntriesSoftLimit: 100,\n\t\tEntriesHardLimit: 150,\n\t\tLoginDelay: LoginDelay{\n\t\t\tPasswordFailed: 200,\n\t\t},\n\t}\n\terr = Initialize(Config, 0)\n\t// ScoreInvalid cannot be greater than threshold\n\tassert.Error(t, err)\n\tConfig.DefenderConfig.Driver = \"unsupported\"\n\terr = Initialize(Config, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unsupported defender driver\")\n\t}\n\tConfig.DefenderConfig.Driver = DefenderDriverMemory\n\terr = Initialize(Config, 0)\n\t// ScoreInvalid cannot be greater than threshold\n\tassert.Error(t, err)\n\tConfig.DefenderConfig.Threshold = 3\n\n\terr = Initialize(Config, 0)\n\tassert.NoError(t, err)\n\tassert.Nil(t, Reload())\n\n\tAddDefenderEvent(ip, ProtocolSSH, HostEventNoLoginTried)\n\tassert.False(t, IsBanned(ip, ProtocolSSH))\n\tscore, err = GetDefenderScore(ip)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2, score)\n\tentry, err := GetDefenderHost(ip)\n\tassert.NoError(t, err)\n\tasJSON, err := json.Marshal(&entry)\n\tassert.NoError(t, err)\n\tassert.Equal(t, `{\"id\":\"3132372e312e312e31\",\"ip\":\"127.1.1.1\",\"score\":2}`, string(asJSON), \"entry %v\", entry)\n\tassert.True(t, DeleteDefenderHost(ip))\n\tbanTime, err = GetDefenderBanTime(ip)\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\n\tAddDefenderEvent(ip, ProtocolHTTP, HostEventLoginFailed)\n\tAddDefenderEvent(ip, ProtocolHTTP, HostEventNoLoginTried)\n\tassert.True(t, IsBanned(ip, ProtocolHTTP))\n\tscore, err = GetDefenderScore(ip)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, score)\n\tbanTime, err = GetDefenderBanTime(ip)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, banTime)\n\thosts, err = GetDefenderHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 1)\n\tentry, err = GetDefenderHost(ip)\n\tassert.NoError(t, err)\n\tassert.False(t, entry.BanTime.IsZero())\n\tassert.True(t, DeleteDefenderHost(ip))\n\thosts, err = GetDefenderHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 0)\n\tbanTime, err = GetDefenderBanTime(ip)\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\tassert.False(t, DeleteDefenderHost(ip))\n\n\tstartTime := time.Now()\n\tDelayLogin(nil)\n\telapsed := time.Since(startTime)\n\tassert.Less(t, elapsed, time.Millisecond*50)\n\n\tstartTime = time.Now()\n\tDelayLogin(ErrInternalFailure)\n\telapsed = time.Since(startTime)\n\tassert.Greater(t, elapsed, time.Millisecond*150)\n\n\tConfig = configCopy\n}\n\nfunc TestRateLimitersIntegration(t *testing.T) {\n\tconfigCopy := Config\n\n\tenabled, protocols := Config.GetRateLimitersStatus()\n\tassert.False(t, enabled)\n\tassert.Len(t, protocols, 0)\n\n\tentries := []dataprovider.IPListEntry{\n\t\t{\n\t\t\tIPOrNet: \"172.16.24.7/32\",\n\t\t\tType:    dataprovider.IPListTypeRateLimiterSafeList,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"172.16.0.0/16\",\n\t\t\tType:    dataprovider.IPListTypeRateLimiterSafeList,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t}\n\n\tfor idx := range entries {\n\t\te := entries[idx]\n\t\terr := dataprovider.AddIPListEntry(&e, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\n\tConfig.RateLimitersConfig = []RateLimiterConfig{\n\t\t{\n\t\t\tAverage:   100,\n\t\t\tPeriod:    10,\n\t\t\tBurst:     5,\n\t\t\tType:      int(rateLimiterTypeGlobal),\n\t\t\tProtocols: rateLimiterProtocolValues,\n\t\t},\n\t\t{\n\t\t\tAverage:                1,\n\t\t\tPeriod:                 1000,\n\t\t\tBurst:                  1,\n\t\t\tType:                   int(rateLimiterTypeSource),\n\t\t\tProtocols:              []string{ProtocolWebDAV, ProtocolWebDAV, ProtocolFTP},\n\t\t\tGenerateDefenderEvents: true,\n\t\t\tEntriesSoftLimit:       100,\n\t\t\tEntriesHardLimit:       150,\n\t\t},\n\t}\n\terr := Initialize(Config, 0)\n\tassert.Error(t, err)\n\tConfig.RateLimitersConfig[0].Period = 1000\n\n\terr = Initialize(Config, 0)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, Config.rateLimitersList)\n\n\tassert.Len(t, rateLimiters, 4)\n\tassert.Len(t, rateLimiters[ProtocolSSH], 1)\n\tassert.Len(t, rateLimiters[ProtocolFTP], 2)\n\tassert.Len(t, rateLimiters[ProtocolWebDAV], 2)\n\tassert.Len(t, rateLimiters[ProtocolHTTP], 1)\n\n\tenabled, protocols = Config.GetRateLimitersStatus()\n\tassert.True(t, enabled)\n\tassert.Len(t, protocols, 4)\n\tassert.Contains(t, protocols, ProtocolFTP)\n\tassert.Contains(t, protocols, ProtocolSSH)\n\tassert.Contains(t, protocols, ProtocolHTTP)\n\tassert.Contains(t, protocols, ProtocolWebDAV)\n\n\tsource1 := \"127.1.1.1\"\n\tsource2 := \"127.1.1.2\"\n\tsource3 := \"172.16.24.7\" // in safelist\n\n\t_, err = LimitRate(ProtocolSSH, source1)\n\tassert.NoError(t, err)\n\t_, err = LimitRate(ProtocolFTP, source1)\n\tassert.NoError(t, err)\n\t// sleep to allow the add configured burst to the token.\n\t// This sleep is not enough to add the per-source burst\n\ttime.Sleep(20 * time.Millisecond)\n\t_, err = LimitRate(ProtocolWebDAV, source2)\n\tassert.NoError(t, err)\n\t_, err = LimitRate(ProtocolFTP, source1)\n\tassert.Error(t, err)\n\t_, err = LimitRate(ProtocolWebDAV, source2)\n\tassert.Error(t, err)\n\t_, err = LimitRate(ProtocolSSH, source1)\n\tassert.NoError(t, err)\n\t_, err = LimitRate(ProtocolSSH, source2)\n\tassert.NoError(t, err)\n\tfor i := 0; i < 10; i++ {\n\t\t_, err = LimitRate(ProtocolWebDAV, source3)\n\t\tassert.NoError(t, err)\n\t}\n\tfor _, e := range entries {\n\t\terr := dataprovider.DeleteIPListEntry(e.IPOrNet, e.Type, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\n\tassert.Nil(t, configCopy.rateLimitersList)\n\tConfig = configCopy\n}\n\nfunc TestUserMaxSessions(t *testing.T) {\n\tc := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:    userTestUsername,\n\t\t\tMaxSessions: 1,\n\t\t},\n\t})\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\terr := Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\terr = Connections.Add(fakeConn)\n\tassert.Error(t, err)\n\terr = Connections.Swap(fakeConn)\n\tassert.NoError(t, err)\n\tConnections.Remove(fakeConn.GetID())\n\tConnections.Lock()\n\tConnections.removeUserConnection(userTestUsername)\n\tConnections.Unlock()\n\tassert.Len(t, Connections.GetStats(\"\"), 0)\n}\n\nfunc TestMaxConnections(t *testing.T) {\n\toldValue := Config.MaxTotalConnections\n\tperHost := Config.MaxPerHostConnections\n\n\tConfig.MaxPerHostConnections = 0\n\n\tipAddr := \"192.168.7.8\"\n\tassert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolFTP))\n\tassert.NoError(t, Connections.IsNewTransferAllowed(userTestUsername))\n\n\tConfig.MaxTotalConnections = 1\n\tConfig.MaxPerHostConnections = perHost\n\n\tassert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolHTTP))\n\tassert.NoError(t, Connections.IsNewTransferAllowed(userTestUsername))\n\tisShuttingDown.Store(true)\n\tassert.ErrorIs(t, Connections.IsNewTransferAllowed(userTestUsername), ErrShuttingDown)\n\tisShuttingDown.Store(false)\n\n\tc := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", dataprovider.User{})\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\terr := Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\tassert.Len(t, Connections.GetStats(\"\"), 1)\n\tassert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))\n\tConnections.transfers.add(userTestUsername)\n\tassert.Error(t, Connections.IsNewTransferAllowed(userTestUsername))\n\tConnections.transfers.remove(userTestUsername)\n\tassert.Equal(t, int32(0), Connections.GetTotalTransfers())\n\n\tres := Connections.Close(fakeConn.GetID(), \"\")\n\tassert.True(t, res)\n\tassert.Eventually(t, func() bool { return len(Connections.GetStats(\"\")) == 0 }, 300*time.Millisecond, 50*time.Millisecond)\n\n\tassert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))\n\tConnections.AddClientConnection(ipAddr)\n\tConnections.AddClientConnection(ipAddr)\n\tassert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))\n\tConnections.RemoveClientConnection(ipAddr)\n\tassert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolWebDAV))\n\tConnections.transfers.add(userTestUsername)\n\tassert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))\n\tConnections.transfers.remove(userTestUsername)\n\tConnections.RemoveClientConnection(ipAddr)\n\n\tConfig.MaxTotalConnections = oldValue\n}\n\nfunc TestConnectionRoles(t *testing.T) {\n\tusername := \"testUsername\"\n\trole1 := \"testRole1\"\n\trole2 := \"testRole2\"\n\tc := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tRole:     role1,\n\t\t},\n\t})\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\terr := Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\tassert.Len(t, Connections.GetStats(\"\"), 1)\n\tassert.Len(t, Connections.GetStats(role1), 1)\n\tassert.Len(t, Connections.GetStats(role2), 0)\n\n\tres := Connections.Close(fakeConn.GetID(), role2)\n\tassert.False(t, res)\n\tassert.Len(t, Connections.GetStats(\"\"), 1)\n\tres = Connections.Close(fakeConn.GetID(), role1)\n\tassert.True(t, res)\n\tassert.Eventually(t, func() bool { return len(Connections.GetStats(\"\")) == 0 }, 300*time.Millisecond, 50*time.Millisecond)\n}\n\nfunc TestMaxConnectionPerHost(t *testing.T) {\n\tdefender, err := newInMemoryDefender(&DefenderConfig{\n\t\tEnabled:            true,\n\t\tDriver:             DefenderDriverMemory,\n\t\tBanTime:            30,\n\t\tBanTimeIncrement:   50,\n\t\tThreshold:          15,\n\t\tScoreInvalid:       2,\n\t\tScoreValid:         1,\n\t\tScoreLimitExceeded: 3,\n\t\tObservationTime:    30,\n\t\tEntriesSoftLimit:   100,\n\t\tEntriesHardLimit:   150,\n\t})\n\trequire.NoError(t, err)\n\n\toldMaxPerHostConn := Config.MaxPerHostConnections\n\toldDefender := Config.defender\n\n\tConfig.MaxPerHostConnections = 2\n\tConfig.defender = defender\n\n\tipAddr := \"192.168.9.9\"\n\tConnections.AddClientConnection(ipAddr)\n\tassert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))\n\n\tConnections.AddClientConnection(ipAddr)\n\tassert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolWebDAV))\n\n\tConnections.AddClientConnection(ipAddr)\n\tassert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolFTP))\n\tassert.Equal(t, int32(3), Connections.GetClientConnections())\n\t// Add the IP to the defender safe list\n\tentry := dataprovider.IPListEntry{\n\t\tIPOrNet: ipAddr,\n\t\tType:    dataprovider.IPListTypeDefender,\n\t\tMode:    dataprovider.ListModeAllow,\n\t}\n\terr = dataprovider.AddIPListEntry(&entry, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tConnections.AddClientConnection(ipAddr)\n\tassert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))\n\n\terr = dataprovider.DeleteIPListEntry(entry.IPOrNet, dataprovider.IPListTypeDefender, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tConnections.RemoveClientConnection(ipAddr)\n\tConnections.RemoveClientConnection(ipAddr)\n\tConnections.RemoveClientConnection(ipAddr)\n\tConnections.RemoveClientConnection(ipAddr)\n\n\tassert.Equal(t, int32(0), Connections.GetClientConnections())\n\n\tConfig.MaxPerHostConnections = oldMaxPerHostConn\n\tConfig.defender = oldDefender\n}\n\nfunc TestIdleConnections(t *testing.T) {\n\tconfigCopy := Config\n\n\tConfig.IdleTimeout = 1\n\terr := Initialize(Config, 0)\n\tassert.NoError(t, err)\n\n\tconn1, conn2 := net.Pipe()\n\tcustomConn1 := &customNetConn{\n\t\tConn: conn1,\n\t\tid:   \"id1\",\n\t}\n\tcustomConn2 := &customNetConn{\n\t\tConn: conn2,\n\t\tid:   \"id2\",\n\t}\n\tsshConn1 := NewSSHConnection(customConn1.id, customConn1)\n\tsshConn2 := NewSSHConnection(customConn2.id, customConn2)\n\n\tusername := \"test_user\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tStatus:   1,\n\t\t},\n\t}\n\tc := NewBaseConnection(sshConn1.id+\"_1\", ProtocolSFTP, \"\", \"\", user)\n\tc.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\t// both ssh connections are expired but they should get removed only\n\t// if there is no associated connection\n\tsshConn1.lastActivity.Store(c.lastActivity.Load())\n\tsshConn2.lastActivity.Store(c.lastActivity.Load())\n\tConnections.AddSSHConnection(sshConn1)\n\terr = Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\tassert.Equal(t, Connections.GetActiveSessions(username), 1)\n\tc = NewBaseConnection(sshConn2.id+\"_1\", ProtocolSSH, \"\", \"\", user)\n\tfakeConn = &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\tConnections.AddSSHConnection(sshConn2)\n\terr = Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\tassert.Equal(t, Connections.GetActiveSessions(username), 2)\n\n\tcFTP := NewBaseConnection(\"id2\", ProtocolFTP, \"\", \"\", dataprovider.User{})\n\tcFTP.lastActivity.Store(time.Now().UnixNano())\n\tfakeConn = &fakeConnection{\n\t\tBaseConnection: cFTP,\n\t}\n\terr = Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\t// the user is expired, this connection will be removed\n\tcDAV := NewBaseConnection(\"id3\", ProtocolWebDAV, \"\", \"\", dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:       username + \"_2\",\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour)),\n\t\t},\n\t})\n\tcDAV.lastActivity.Store(time.Now().UnixNano())\n\tfakeConn = &fakeConnection{\n\t\tBaseConnection: cDAV,\n\t}\n\terr = Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, 2, Connections.GetActiveSessions(username))\n\tassert.Len(t, Connections.GetStats(\"\"), 4)\n\tConnections.RLock()\n\tassert.Len(t, Connections.sshConnections, 2)\n\tConnections.RUnlock()\n\n\tstartPeriodicChecks(100*time.Millisecond, 0)\n\tassert.Eventually(t, func() bool { return Connections.GetActiveSessions(username) == 1 }, 2*time.Second, 200*time.Millisecond)\n\tassert.Eventually(t, func() bool {\n\t\tConnections.RLock()\n\t\tdefer Connections.RUnlock()\n\t\treturn len(Connections.sshConnections) == 1\n\t}, 1*time.Second, 200*time.Millisecond)\n\tstopEventScheduler()\n\tassert.Len(t, Connections.GetStats(\"\"), 2)\n\tc.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())\n\tcFTP.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())\n\tsshConn2.lastActivity.Store(c.lastActivity.Load())\n\tstartPeriodicChecks(100*time.Millisecond, 1)\n\tassert.Eventually(t, func() bool { return len(Connections.GetStats(\"\")) == 0 }, 2*time.Second, 200*time.Millisecond)\n\tassert.Eventually(t, func() bool {\n\t\tConnections.RLock()\n\t\tdefer Connections.RUnlock()\n\t\treturn len(Connections.sshConnections) == 0\n\t}, 1*time.Second, 200*time.Millisecond)\n\tassert.Equal(t, int32(0), Connections.GetClientConnections())\n\tstopEventScheduler()\n\tassert.True(t, customConn1.isClosed)\n\tassert.True(t, customConn2.isClosed)\n\n\tConfig = configCopy\n}\n\nfunc TestCloseConnection(t *testing.T) {\n\tc := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", dataprovider.User{})\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\tassert.NoError(t, Connections.IsNewConnectionAllowed(\"127.0.0.1\", ProtocolHTTP))\n\terr := Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\tassert.Len(t, Connections.GetStats(\"\"), 1)\n\tres := Connections.Close(fakeConn.GetID(), \"\")\n\tassert.True(t, res)\n\tassert.Eventually(t, func() bool { return len(Connections.GetStats(\"\")) == 0 }, 300*time.Millisecond, 50*time.Millisecond)\n\tres = Connections.Close(fakeConn.GetID(), \"\")\n\tassert.False(t, res)\n\tConnections.Remove(fakeConn.GetID())\n}\n\nfunc TestSwapConnection(t *testing.T) {\n\tc := NewBaseConnection(\"id\", ProtocolFTP, \"\", \"\", dataprovider.User{})\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\terr := Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\tif assert.Len(t, Connections.GetStats(\"\"), 1) {\n\t\tassert.Equal(t, \"\", Connections.GetStats(\"\")[0].Username)\n\t}\n\tc = NewBaseConnection(\"id\", ProtocolFTP, \"\", \"\", dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:    userTestUsername,\n\t\t\tMaxSessions: 1,\n\t\t},\n\t})\n\tfakeConn = &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\tc1 := NewBaseConnection(\"id1\", ProtocolFTP, \"\", \"\", dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: userTestUsername,\n\t\t},\n\t})\n\tfakeConn1 := &fakeConnection{\n\t\tBaseConnection: c1,\n\t}\n\terr = Connections.Add(fakeConn1)\n\tassert.NoError(t, err)\n\terr = Connections.Swap(fakeConn)\n\tassert.Error(t, err)\n\tConnections.Remove(fakeConn1.ID)\n\terr = Connections.Swap(fakeConn)\n\tassert.NoError(t, err)\n\tif assert.Len(t, Connections.GetStats(\"\"), 1) {\n\t\tassert.Equal(t, userTestUsername, Connections.GetStats(\"\")[0].Username)\n\t}\n\tres := Connections.Close(fakeConn.GetID(), \"\")\n\tassert.True(t, res)\n\tassert.Eventually(t, func() bool { return len(Connections.GetStats(\"\")) == 0 }, 300*time.Millisecond, 50*time.Millisecond)\n\terr = Connections.Swap(fakeConn)\n\tassert.Error(t, err)\n}\n\nfunc TestAtomicUpload(t *testing.T) {\n\tconfigCopy := Config\n\n\tConfig.UploadMode = UploadModeStandard\n\tassert.False(t, Config.IsAtomicUploadEnabled())\n\tConfig.UploadMode = UploadModeAtomic\n\tassert.True(t, Config.IsAtomicUploadEnabled())\n\tConfig.UploadMode = UploadModeAtomicWithResume\n\tassert.True(t, Config.IsAtomicUploadEnabled())\n\n\tConfig = configCopy\n}\n\nfunc TestConnectionStatus(t *testing.T) {\n\tusername := \"test_user\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t},\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tc1 := NewBaseConnection(\"id1\", ProtocolSFTP, \"\", \"\", user)\n\tfakeConn1 := &fakeConnection{\n\t\tBaseConnection: c1,\n\t}\n\tt1 := NewBaseTransfer(nil, c1, nil, \"/p1\", \"/p1\", \"/r1\", TransferUpload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\tt1.BytesReceived.Store(123)\n\tt2 := NewBaseTransfer(nil, c1, nil, \"/p2\", \"/p2\", \"/r2\", TransferDownload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\tt2.BytesSent.Store(456)\n\tc2 := NewBaseConnection(\"id2\", ProtocolSSH, \"\", \"\", user)\n\tfakeConn2 := &fakeConnection{\n\t\tBaseConnection: c2,\n\t\tcommand:        \"md5sum\",\n\t}\n\tc3 := NewBaseConnection(\"id3\", ProtocolWebDAV, \"\", \"\", user)\n\tfakeConn3 := &fakeConnection{\n\t\tBaseConnection: c3,\n\t\tcommand:        \"PROPFIND\",\n\t}\n\tt3 := NewBaseTransfer(nil, c3, nil, \"/p2\", \"/p2\", \"/r2\", TransferDownload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\terr := Connections.Add(fakeConn1)\n\tassert.NoError(t, err)\n\terr = Connections.Add(fakeConn2)\n\tassert.NoError(t, err)\n\terr = Connections.Add(fakeConn3)\n\tassert.NoError(t, err)\n\n\tstats := Connections.GetStats(\"\")\n\tassert.Len(t, stats, 3)\n\tfor _, stat := range stats {\n\t\tassert.Equal(t, stat.Username, username)\n\t\tswitch stat.ConnectionID {\n\t\tcase \"SFTP_id1\":\n\t\t\tassert.Len(t, stat.Transfers, 2)\n\t\tcase \"DAV_id3\":\n\t\t\tassert.Len(t, stat.Transfers, 1)\n\t\t}\n\t}\n\n\terr = t1.Close()\n\tassert.NoError(t, err)\n\terr = t2.Close()\n\tassert.NoError(t, err)\n\n\terr = fakeConn3.SignalTransfersAbort()\n\tassert.NoError(t, err)\n\tassert.True(t, t3.AbortTransfer.Load())\n\terr = t3.Close()\n\tassert.NoError(t, err)\n\terr = fakeConn3.SignalTransfersAbort()\n\tassert.Error(t, err)\n\n\tConnections.Remove(fakeConn1.GetID())\n\tstats = Connections.GetStats(\"\")\n\tassert.Len(t, stats, 2)\n\tassert.Equal(t, fakeConn3.GetID(), stats[0].ConnectionID)\n\tassert.Equal(t, fakeConn2.GetID(), stats[1].ConnectionID)\n\tConnections.Remove(fakeConn2.GetID())\n\tstats = Connections.GetStats(\"\")\n\tassert.Len(t, stats, 1)\n\tassert.Equal(t, fakeConn3.GetID(), stats[0].ConnectionID)\n\tConnections.Remove(fakeConn3.GetID())\n\tstats = Connections.GetStats(\"\")\n\tassert.Len(t, stats, 0)\n}\n\nfunc TestQuotaScans(t *testing.T) {\n\tusername := \"username\"\n\tassert.True(t, QuotaScans.AddUserQuotaScan(username, \"\"))\n\tassert.False(t, QuotaScans.AddUserQuotaScan(username, \"\"))\n\tusersScans := QuotaScans.GetUsersQuotaScans(\"\")\n\tif assert.Len(t, usersScans, 1) {\n\t\tassert.Equal(t, usersScans[0].Username, username)\n\t\tassert.Equal(t, QuotaScans.UserScans[0].StartTime, usersScans[0].StartTime)\n\t\tQuotaScans.UserScans[0].StartTime = 0\n\t\tassert.NotEqual(t, QuotaScans.UserScans[0].StartTime, usersScans[0].StartTime)\n\t}\n\n\tassert.True(t, QuotaScans.RemoveUserQuotaScan(username))\n\tassert.False(t, QuotaScans.RemoveUserQuotaScan(username))\n\tassert.Len(t, QuotaScans.GetUsersQuotaScans(\"\"), 0)\n\tassert.Len(t, usersScans, 1)\n\n\tfolderName := \"folder\"\n\tassert.True(t, QuotaScans.AddVFolderQuotaScan(folderName))\n\tassert.False(t, QuotaScans.AddVFolderQuotaScan(folderName))\n\tif assert.Len(t, QuotaScans.GetVFoldersQuotaScans(), 1) {\n\t\tassert.Equal(t, QuotaScans.GetVFoldersQuotaScans()[0].Name, folderName)\n\t}\n\n\tassert.True(t, QuotaScans.RemoveVFolderQuotaScan(folderName))\n\tassert.False(t, QuotaScans.RemoveVFolderQuotaScan(folderName))\n\tassert.Len(t, QuotaScans.GetVFoldersQuotaScans(), 0)\n}\n\nfunc TestQuotaScansRole(t *testing.T) {\n\tusername := \"u\"\n\trole1 := \"r1\"\n\trole2 := \"r2\"\n\tassert.True(t, QuotaScans.AddUserQuotaScan(username, role1))\n\tassert.False(t, QuotaScans.AddUserQuotaScan(username, \"\"))\n\tusersScans := QuotaScans.GetUsersQuotaScans(\"\")\n\tassert.Len(t, usersScans, 1)\n\tassert.Empty(t, usersScans[0].Role)\n\tusersScans = QuotaScans.GetUsersQuotaScans(role1)\n\tassert.Len(t, usersScans, 1)\n\tusersScans = QuotaScans.GetUsersQuotaScans(role2)\n\tassert.Len(t, usersScans, 0)\n\tassert.True(t, QuotaScans.RemoveUserQuotaScan(username))\n\tassert.False(t, QuotaScans.RemoveUserQuotaScan(username))\n\tassert.Len(t, QuotaScans.GetUsersQuotaScans(\"\"), 0)\n}\n\nfunc TestProxyPolicy(t *testing.T) {\n\taddr := net.TCPAddr{}\n\tdownstream := net.TCPAddr{IP: net.ParseIP(\"1.1.1.1\")}\n\tp := getProxyPolicy(nil, nil, proxyproto.IGNORE)\n\tpolicy, err := p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &addr,\n\t\tDownstream: &downstream,\n\t})\n\tassert.ErrorIs(t, err, proxyproto.ErrInvalidUpstream)\n\tassert.Equal(t, proxyproto.REJECT, policy)\n\tip1 := net.ParseIP(\"10.8.1.1\")\n\tip2 := net.ParseIP(\"10.8.1.2\")\n\tip3 := net.ParseIP(\"10.8.1.3\")\n\tallowed, err := util.ParseAllowedIPAndRanges([]string{ip1.String()})\n\tassert.NoError(t, err)\n\tskipped, err := util.ParseAllowedIPAndRanges([]string{ip2.String(), ip3.String()})\n\tassert.NoError(t, err)\n\tp = getProxyPolicy(allowed, skipped, proxyproto.IGNORE)\n\tpolicy, err = p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &net.TCPAddr{IP: ip1},\n\t\tDownstream: &downstream,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, proxyproto.USE, policy)\n\tpolicy, err = p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &net.TCPAddr{IP: ip2},\n\t\tDownstream: &downstream,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, proxyproto.SKIP, policy)\n\tpolicy, err = p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &net.TCPAddr{IP: ip3},\n\t\tDownstream: &downstream,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, proxyproto.SKIP, policy)\n\tpolicy, err = p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &net.TCPAddr{IP: net.ParseIP(\"10.8.1.4\")},\n\t\tDownstream: &downstream,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, proxyproto.IGNORE, policy)\n\tp = getProxyPolicy(allowed, skipped, proxyproto.REQUIRE)\n\tpolicy, err = p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &net.TCPAddr{IP: ip1},\n\t\tDownstream: &downstream,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, proxyproto.REQUIRE, policy)\n\tpolicy, err = p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &net.TCPAddr{IP: ip2},\n\t\tDownstream: &downstream,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, proxyproto.SKIP, policy)\n\tpolicy, err = p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &net.TCPAddr{IP: ip3},\n\t\tDownstream: &downstream,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, proxyproto.SKIP, policy)\n\tpolicy, err = p(proxyproto.ConnPolicyOptions{\n\t\tUpstream:   &net.TCPAddr{IP: net.ParseIP(\"10.8.1.5\")},\n\t\tDownstream: &downstream,\n\t})\n\tassert.ErrorIs(t, err, proxyproto.ErrInvalidUpstream)\n\tassert.Equal(t, proxyproto.REJECT, policy)\n}\n\nfunc TestProxyProtocolVersion(t *testing.T) {\n\tc := Configuration{\n\t\tProxyProtocol: 0,\n\t}\n\t_, err := c.GetProxyListener(nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"proxy protocol not configured\")\n\t}\n\tc.ProxyProtocol = 1\n\tlistener, err := c.GetProxyListener(nil)\n\tassert.NoError(t, err)\n\tproxyListener, ok := listener.(*proxyproto.Listener)\n\trequire.True(t, ok)\n\tassert.NotNil(t, proxyListener.ConnPolicy)\n\n\tc.ProxyProtocol = 2\n\tlistener, err = c.GetProxyListener(nil)\n\tassert.NoError(t, err)\n\tproxyListener, ok = listener.(*proxyproto.Listener)\n\trequire.True(t, ok)\n\tassert.NotNil(t, proxyListener.ConnPolicy)\n}\n\nfunc TestStartupHook(t *testing.T) {\n\tConfig.StartupHook = \"\"\n\n\tassert.NoError(t, Config.ExecuteStartupHook())\n\n\tConfig.StartupHook = \"http://foo\\x7f.com/startup\"\n\tassert.Error(t, Config.ExecuteStartupHook())\n\n\tConfig.StartupHook = \"http://invalid:5678/\"\n\tassert.Error(t, Config.ExecuteStartupHook())\n\n\tConfig.StartupHook = fmt.Sprintf(\"http://%v\", httpAddr)\n\tassert.NoError(t, Config.ExecuteStartupHook())\n\n\tConfig.StartupHook = \"invalidhook\"\n\tassert.Error(t, Config.ExecuteStartupHook())\n\n\tif runtime.GOOS != osWindows {\n\t\thookCmd, err := exec.LookPath(\"true\")\n\t\tassert.NoError(t, err)\n\t\tConfig.StartupHook = hookCmd\n\t\tassert.NoError(t, Config.ExecuteStartupHook())\n\t}\n\n\tConfig.StartupHook = \"\"\n}\n\nfunc TestPostDisconnectHook(t *testing.T) {\n\tConfig.PostDisconnectHook = \"http://127.0.0.1/\"\n\n\tremoteAddr := \"127.0.0.1:80\"\n\tConfig.checkPostDisconnectHook(remoteAddr, ProtocolHTTP, \"\", \"\", time.Now())\n\tConfig.checkPostDisconnectHook(remoteAddr, ProtocolSFTP, \"\", \"\", time.Now())\n\n\tConfig.PostDisconnectHook = \"http://bar\\x7f.com/\"\n\tConfig.executePostDisconnectHook(remoteAddr, ProtocolSFTP, \"\", \"\", time.Now())\n\n\tConfig.PostDisconnectHook = fmt.Sprintf(\"http://%v\", httpAddr)\n\tConfig.executePostDisconnectHook(remoteAddr, ProtocolSFTP, \"\", \"\", time.Now())\n\n\tConfig.PostDisconnectHook = \"relativePath\"\n\tConfig.executePostDisconnectHook(remoteAddr, ProtocolSFTP, \"\", \"\", time.Now())\n\n\tif runtime.GOOS == osWindows {\n\t\tConfig.PostDisconnectHook = \"C:\\\\a\\\\bad\\\\command\"\n\t\tConfig.executePostDisconnectHook(remoteAddr, ProtocolSFTP, \"\", \"\", time.Now())\n\t} else {\n\t\tConfig.PostDisconnectHook = \"/invalid/path\"\n\t\tConfig.executePostDisconnectHook(remoteAddr, ProtocolSFTP, \"\", \"\", time.Now())\n\n\t\thookCmd, err := exec.LookPath(\"true\")\n\t\tassert.NoError(t, err)\n\t\tConfig.PostDisconnectHook = hookCmd\n\t\tConfig.executePostDisconnectHook(remoteAddr, ProtocolSFTP, \"\", \"\", time.Now())\n\t}\n\tConfig.PostDisconnectHook = \"\"\n}\n\nfunc TestPostConnectHook(t *testing.T) {\n\tConfig.PostConnectHook = \"\"\n\n\tipAddr := \"127.0.0.1\"\n\n\tassert.NoError(t, Config.ExecutePostConnectHook(ipAddr, ProtocolFTP))\n\n\tConfig.PostConnectHook = \"http://foo\\x7f.com/\"\n\tassert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))\n\n\tConfig.PostConnectHook = \"http://invalid:1234/\"\n\tassert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))\n\n\tConfig.PostConnectHook = fmt.Sprintf(\"http://%v/404\", httpAddr)\n\tassert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolFTP))\n\n\tConfig.PostConnectHook = fmt.Sprintf(\"http://%v\", httpAddr)\n\tassert.NoError(t, Config.ExecutePostConnectHook(ipAddr, ProtocolFTP))\n\n\tConfig.PostConnectHook = \"invalid\"\n\tassert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolFTP))\n\n\tif runtime.GOOS == osWindows {\n\t\tConfig.PostConnectHook = \"C:\\\\bad\\\\command\"\n\t\tassert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))\n\t} else {\n\t\tConfig.PostConnectHook = \"/invalid/path\"\n\t\tassert.Error(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))\n\n\t\thookCmd, err := exec.LookPath(\"true\")\n\t\tassert.NoError(t, err)\n\t\tConfig.PostConnectHook = hookCmd\n\t\tassert.NoError(t, Config.ExecutePostConnectHook(ipAddr, ProtocolSFTP))\n\t}\n\n\tConfig.PostConnectHook = \"\"\n}\n\nfunc TestCryptoConvertFileInfo(t *testing.T) {\n\tname := \"name\"\n\tfs, err := vfs.NewCryptFs(\"connID1\", os.TempDir(), \"\", vfs.CryptFsConfig{\n\t\tPassphrase: kms.NewPlainSecret(\"secret\"),\n\t})\n\trequire.NoError(t, err)\n\tcryptFs := fs.(*vfs.CryptFs)\n\tinfo := vfs.NewFileInfo(name, true, 48, time.Now(), false)\n\tassert.Equal(t, info, cryptFs.ConvertFileInfo(info))\n\tinfo = vfs.NewFileInfo(name, false, 48, time.Now(), false)\n\tassert.NotEqual(t, info.Size(), cryptFs.ConvertFileInfo(info).Size())\n\tinfo = vfs.NewFileInfo(name, false, 33, time.Now(), false)\n\tassert.Equal(t, int64(0), cryptFs.ConvertFileInfo(info).Size())\n\tinfo = vfs.NewFileInfo(name, false, 1, time.Now(), false)\n\tassert.Equal(t, int64(0), cryptFs.ConvertFileInfo(info).Size())\n}\n\nfunc TestFolderCopy(t *testing.T) {\n\tfolder := vfs.BaseVirtualFolder{\n\t\tID:              1,\n\t\tName:            \"name\",\n\t\tMappedPath:      filepath.Clean(os.TempDir()),\n\t\tUsedQuotaSize:   4096,\n\t\tUsedQuotaFiles:  2,\n\t\tLastQuotaUpdate: util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUsers:           []string{\"user1\", \"user2\"},\n\t}\n\tfolderCopy := folder.GetACopy()\n\tfolder.ID = 2\n\tfolder.Users = []string{\"user3\"}\n\trequire.Len(t, folderCopy.Users, 2)\n\trequire.True(t, slices.Contains(folderCopy.Users, \"user1\"))\n\trequire.True(t, slices.Contains(folderCopy.Users, \"user2\"))\n\trequire.Equal(t, int64(1), folderCopy.ID)\n\trequire.Equal(t, folder.Name, folderCopy.Name)\n\trequire.Equal(t, folder.MappedPath, folderCopy.MappedPath)\n\trequire.Equal(t, folder.UsedQuotaSize, folderCopy.UsedQuotaSize)\n\trequire.Equal(t, folder.UsedQuotaFiles, folderCopy.UsedQuotaFiles)\n\trequire.Equal(t, folder.LastQuotaUpdate, folderCopy.LastQuotaUpdate)\n\n\tfolder.FsConfig = vfs.Filesystem{\n\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\tPassphrase: kms.NewPlainSecret(\"crypto secret\"),\n\t\t},\n\t}\n\tfolderCopy = folder.GetACopy()\n\tfolder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()\n\trequire.Len(t, folderCopy.Users, 1)\n\trequire.True(t, slices.Contains(folderCopy.Users, \"user3\"))\n\trequire.Equal(t, int64(2), folderCopy.ID)\n\trequire.Equal(t, folder.Name, folderCopy.Name)\n\trequire.Equal(t, folder.MappedPath, folderCopy.MappedPath)\n\trequire.Equal(t, folder.UsedQuotaSize, folderCopy.UsedQuotaSize)\n\trequire.Equal(t, folder.UsedQuotaFiles, folderCopy.UsedQuotaFiles)\n\trequire.Equal(t, folder.LastQuotaUpdate, folderCopy.LastQuotaUpdate)\n\trequire.Equal(t, \"crypto secret\", folderCopy.FsConfig.CryptConfig.Passphrase.GetPayload())\n}\n\nfunc TestCachedFs(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tconn := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", user)\n\t// changing the user should not affect the connection\n\tuser.HomeDir = filepath.Join(os.TempDir(), \"temp\")\n\terr := os.Mkdir(user.HomeDir, os.ModePerm)\n\tassert.NoError(t, err)\n\tfs, err := user.GetFilesystem(\"\")\n\tassert.NoError(t, err)\n\tp, err := fs.ResolvePath(\"/\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, user.GetHomeDir(), p)\n\n\t_, p, err = conn.GetFsAndResolvedPath(\"/\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Clean(os.TempDir()), p)\n\t// the filesystem is cached changing the provider will not affect the connection\n\tconn.User.FsConfig.Provider = sdk.S3FilesystemProvider\n\t_, p, err = conn.GetFsAndResolvedPath(\"/\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Clean(os.TempDir()), p)\n\tuser = dataprovider.User{}\n\tuser.HomeDir = filepath.Join(os.TempDir(), \"temp\")\n\tuser.FsConfig.Provider = sdk.S3FilesystemProvider\n\t_, err = user.GetFilesystem(\"\")\n\tassert.Error(t, err)\n\n\terr = os.Remove(user.HomeDir)\n\tassert.NoError(t, err)\n}\n\nfunc TestParseAllowedIPAndRanges(t *testing.T) {\n\t_, err := util.ParseAllowedIPAndRanges([]string{\"1.1.1.1\", \"not an ip\"})\n\tassert.Error(t, err)\n\t_, err = util.ParseAllowedIPAndRanges([]string{\"1.1.1.5\", \"192.168.1.0/240\"})\n\tassert.Error(t, err)\n\tallow, err := util.ParseAllowedIPAndRanges([]string{\"192.168.1.2\", \"172.16.0.0/24\"})\n\tassert.NoError(t, err)\n\tassert.True(t, allow[0](net.ParseIP(\"192.168.1.2\")))\n\tassert.False(t, allow[0](net.ParseIP(\"192.168.2.2\")))\n\tassert.True(t, allow[1](net.ParseIP(\"172.16.0.1\")))\n\tassert.False(t, allow[1](net.ParseIP(\"172.16.1.1\")))\n}\n\nfunc TestHideConfidentialData(_ *testing.T) {\n\tfor _, provider := range []sdk.FilesystemProvider{sdk.LocalFilesystemProvider,\n\t\tsdk.CryptedFilesystemProvider, sdk.S3FilesystemProvider, sdk.GCSFilesystemProvider,\n\t\tsdk.AzureBlobFilesystemProvider, sdk.SFTPFilesystemProvider,\n\t} {\n\t\tu := dataprovider.User{\n\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\tProvider: provider,\n\t\t\t},\n\t\t}\n\t\tu.PrepareForRendering()\n\t\tf := vfs.BaseVirtualFolder{\n\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\tProvider: provider,\n\t\t\t},\n\t\t}\n\t\tf.PrepareForRendering()\n\t}\n\ta := dataprovider.Admin{}\n\ta.HideConfidentialData()\n}\n\nfunc TestUserPerms(t *testing.T) {\n\tu := dataprovider.User{}\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermUpload, dataprovider.PermDelete}\n\tassert.True(t, u.HasAnyPerm([]string{dataprovider.PermRename, dataprovider.PermDelete}, \"/\"))\n\tassert.False(t, u.HasAnyPerm([]string{dataprovider.PermRename, dataprovider.PermCreateDirs}, \"/\"))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermDelete, dataprovider.PermCreateDirs}\n\tassert.True(t, u.HasPermsDeleteAll(\"/\"))\n\tassert.False(t, u.HasPermsRenameAll(\"/\"))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermDeleteDirs, dataprovider.PermDeleteFiles, dataprovider.PermRenameDirs}\n\tassert.True(t, u.HasPermsDeleteAll(\"/\"))\n\tassert.False(t, u.HasPermsRenameAll(\"/\"))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermDeleteDirs, dataprovider.PermRenameFiles, dataprovider.PermRenameDirs}\n\tassert.False(t, u.HasPermsDeleteAll(\"/\"))\n\tassert.True(t, u.HasPermsRenameAll(\"/\"))\n}\n\nfunc TestGetTLSVersion(t *testing.T) {\n\ttlsVer := util.GetTLSVersion(0)\n\tassert.Equal(t, uint16(tls.VersionTLS12), tlsVer)\n\ttlsVer = util.GetTLSVersion(12)\n\tassert.Equal(t, uint16(tls.VersionTLS12), tlsVer)\n\ttlsVer = util.GetTLSVersion(2)\n\tassert.Equal(t, uint16(tls.VersionTLS12), tlsVer)\n\ttlsVer = util.GetTLSVersion(13)\n\tassert.Equal(t, uint16(tls.VersionTLS13), tlsVer)\n}\n\nfunc TestCleanPath(t *testing.T) {\n\tassert.Equal(t, \"/\", util.CleanPath(\"/\"))\n\tassert.Equal(t, \"/\", util.CleanPath(\".\"))\n\tassert.Equal(t, \"/\", util.CleanPath(\"\"))\n\tassert.Equal(t, \"/\", util.CleanPath(\"/.\"))\n\tassert.Equal(t, \"/\", util.CleanPath(\"/a/..\"))\n\tassert.Equal(t, \"/a\", util.CleanPath(\"/a/\"))\n\tassert.Equal(t, \"/a\", util.CleanPath(\"a/\"))\n\t// filepath.ToSlash does not touch \\ as char on unix systems\n\t// so os.PathSeparator is used for windows compatible tests\n\tbslash := string(os.PathSeparator)\n\tassert.Equal(t, \"/\", util.CleanPath(bslash))\n\tassert.Equal(t, \"/\", util.CleanPath(bslash+bslash))\n\tassert.Equal(t, \"/a\", util.CleanPath(bslash+\"a\"+bslash))\n\tassert.Equal(t, \"/a\", util.CleanPath(\"a\"+bslash))\n\tassert.Equal(t, \"/a/b/c\", util.CleanPath(bslash+\"a\"+bslash+bslash+\"b\"+bslash+bslash+\"c\"+bslash))\n\tassert.Equal(t, \"/C:/a\", util.CleanPath(\"C:\"+bslash+\"a\"))\n}\n\nfunc TestUserRecentActivity(t *testing.T) {\n\tu := dataprovider.User{}\n\tres := u.HasRecentActivity()\n\tassert.False(t, res)\n\tu.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())\n\tres = u.HasRecentActivity()\n\tassert.True(t, res)\n\tu.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Minute))\n\tres = u.HasRecentActivity()\n\tassert.False(t, res)\n\tu.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Second))\n\tres = u.HasRecentActivity()\n\tassert.True(t, res)\n}\n\nfunc TestVfsSameResource(t *testing.T) {\n\tfs := vfs.Filesystem{}\n\tother := vfs.Filesystem{}\n\tres := fs.IsSameResource(other)\n\tassert.True(t, res)\n\tfs = vfs.Filesystem{\n\t\tProvider: sdk.S3FilesystemProvider,\n\t\tS3Config: vfs.S3FsConfig{\n\t\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\t\tBucket: \"a\",\n\t\t\t\tRegion: \"b\",\n\t\t\t},\n\t\t},\n\t}\n\tother = vfs.Filesystem{\n\t\tProvider: sdk.S3FilesystemProvider,\n\t\tS3Config: vfs.S3FsConfig{\n\t\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\t\tBucket: \"a\",\n\t\t\t\tRegion: \"c\",\n\t\t\t},\n\t\t},\n\t}\n\tres = fs.IsSameResource(other)\n\tassert.False(t, res)\n\tother = vfs.Filesystem{\n\t\tProvider: sdk.S3FilesystemProvider,\n\t\tS3Config: vfs.S3FsConfig{\n\t\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\t\tBucket: \"a\",\n\t\t\t\tRegion: \"b\",\n\t\t\t},\n\t\t},\n\t}\n\tres = fs.IsSameResource(other)\n\tassert.True(t, res)\n\tfs = vfs.Filesystem{\n\t\tProvider: sdk.GCSFilesystemProvider,\n\t\tGCSConfig: vfs.GCSFsConfig{\n\t\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\t\tBucket: \"b\",\n\t\t\t},\n\t\t},\n\t}\n\tother = vfs.Filesystem{\n\t\tProvider: sdk.GCSFilesystemProvider,\n\t\tGCSConfig: vfs.GCSFsConfig{\n\t\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\t\tBucket: \"c\",\n\t\t\t},\n\t\t},\n\t}\n\tres = fs.IsSameResource(other)\n\tassert.False(t, res)\n\tother = vfs.Filesystem{\n\t\tProvider: sdk.GCSFilesystemProvider,\n\t\tGCSConfig: vfs.GCSFsConfig{\n\t\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\t\tBucket: \"b\",\n\t\t\t},\n\t\t},\n\t}\n\tres = fs.IsSameResource(other)\n\tassert.True(t, res)\n\tsasURL := kms.NewPlainSecret(\"http://127.0.0.1/sasurl\")\n\tfs = vfs.Filesystem{\n\t\tProvider: sdk.AzureBlobFilesystemProvider,\n\t\tAzBlobConfig: vfs.AzBlobFsConfig{\n\t\t\tBaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{\n\t\t\t\tAccountName: \"a\",\n\t\t\t},\n\t\t\tSASURL: sasURL,\n\t\t},\n\t}\n\terr := fs.Validate(\"data1\")\n\tassert.NoError(t, err)\n\tother = vfs.Filesystem{\n\t\tProvider: sdk.AzureBlobFilesystemProvider,\n\t\tAzBlobConfig: vfs.AzBlobFsConfig{\n\t\t\tBaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{\n\t\t\t\tAccountName: \"a\",\n\t\t\t},\n\t\t\tSASURL: sasURL,\n\t\t},\n\t}\n\terr = other.Validate(\"data2\")\n\tassert.NoError(t, err)\n\terr = fs.AzBlobConfig.SASURL.TryDecrypt()\n\tassert.NoError(t, err)\n\terr = other.AzBlobConfig.SASURL.TryDecrypt()\n\tassert.NoError(t, err)\n\tres = fs.IsSameResource(other)\n\tassert.True(t, res)\n\tfs.AzBlobConfig.AccountName = \"b\"\n\tres = fs.IsSameResource(other)\n\tassert.False(t, res)\n\tfs.AzBlobConfig.AccountName = \"a\"\n\tother.AzBlobConfig.SASURL = kms.NewPlainSecret(\"http://127.1.1.1/sasurl\")\n\terr = other.Validate(\"data2\")\n\tassert.NoError(t, err)\n\terr = other.AzBlobConfig.SASURL.TryDecrypt()\n\tassert.NoError(t, err)\n\tres = fs.IsSameResource(other)\n\tassert.False(t, res)\n\tfs = vfs.Filesystem{\n\t\tProvider: sdk.HTTPFilesystemProvider,\n\t\tHTTPConfig: vfs.HTTPFsConfig{\n\t\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\t\tEndpoint: \"http://127.0.0.1/httpfs\",\n\t\t\t\tUsername: \"a\",\n\t\t\t},\n\t\t},\n\t}\n\tother = vfs.Filesystem{\n\t\tProvider: sdk.HTTPFilesystemProvider,\n\t\tHTTPConfig: vfs.HTTPFsConfig{\n\t\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\t\tEndpoint: \"http://127.0.0.1/httpfs\",\n\t\t\t\tUsername: \"b\",\n\t\t\t},\n\t\t},\n\t}\n\tres = fs.IsSameResource(other)\n\tassert.True(t, res)\n\tfs.HTTPConfig.EqualityCheckMode = 1\n\tres = fs.IsSameResource(other)\n\tassert.False(t, res)\n}\n\nfunc TestUpdateTransferTimestamps(t *testing.T) {\n\tusername := \"user_test_timestamps\"\n\tuser := &dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\terr := dataprovider.AddUser(user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), user.FirstUpload)\n\tassert.Equal(t, int64(0), user.FirstDownload)\n\n\terr = dataprovider.UpdateUserTransferTimestamps(username, true)\n\tassert.NoError(t, err)\n\tuserGet, err := dataprovider.UserExists(username, \"\")\n\tassert.NoError(t, err)\n\tassert.Greater(t, userGet.FirstUpload, int64(0))\n\tassert.Equal(t, int64(0), user.FirstDownload)\n\terr = dataprovider.UpdateUserTransferTimestamps(username, false)\n\tassert.NoError(t, err)\n\tuserGet, err = dataprovider.UserExists(username, \"\")\n\tassert.NoError(t, err)\n\tassert.Greater(t, userGet.FirstUpload, int64(0))\n\tassert.Greater(t, userGet.FirstDownload, int64(0))\n\t// updating again must fail\n\terr = dataprovider.UpdateUserTransferTimestamps(username, true)\n\tassert.Error(t, err)\n\terr = dataprovider.UpdateUserTransferTimestamps(username, false)\n\tassert.Error(t, err)\n\t// cleanup\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestIPList(t *testing.T) {\n\ttype test struct {\n\t\tip            string\n\t\tprotocol      string\n\t\texpectedMatch bool\n\t\texpectedMode  int\n\t\texpectedErr   bool\n\t}\n\n\tentries := []dataprovider.IPListEntry{\n\t\t{\n\t\t\tIPOrNet: \"192.168.0.0/25\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"192.168.0.128/25\",\n\t\t\tType:      dataprovider.IPListTypeDefender,\n\t\t\tMode:      dataprovider.ListModeDeny,\n\t\t\tProtocols: 3,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"192.168.2.128/32\",\n\t\t\tType:      dataprovider.IPListTypeDefender,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 5,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"::/0\",\n\t\t\tType:      dataprovider.IPListTypeDefender,\n\t\t\tMode:      dataprovider.ListModeDeny,\n\t\t\tProtocols: 4,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"2001:4860:4860::8888/120\",\n\t\t\tType:      dataprovider.IPListTypeDefender,\n\t\t\tMode:      dataprovider.ListModeDeny,\n\t\t\tProtocols: 1,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"2001:4860:4860::8988/120\",\n\t\t\tType:      dataprovider.IPListTypeDefender,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 3,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"::1/128\",\n\t\t\tType:      dataprovider.IPListTypeDefender,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 0,\n\t\t},\n\t}\n\tipList, err := dataprovider.NewIPList(dataprovider.IPListTypeDefender)\n\trequire.NoError(t, err)\n\tfor idx := range entries {\n\t\te := entries[idx]\n\t\terr := dataprovider.AddIPListEntry(&e, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\ttests := []test{\n\t\t{ip: \"1.1.1.1\", protocol: ProtocolSSH, expectedMatch: false, expectedMode: 0, expectedErr: false},\n\t\t{ip: \"invalid ip\", protocol: ProtocolSSH, expectedMatch: false, expectedMode: 0, expectedErr: true},\n\t\t{ip: \"192.168.0.1\", protocol: ProtocolFTP, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"192.168.0.2\", protocol: ProtocolHTTP, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"192.168.0.3\", protocol: ProtocolWebDAV, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"192.168.0.4\", protocol: ProtocolSSH, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"192.168.0.156\", protocol: ProtocolSSH, expectedMatch: true, expectedMode: dataprovider.ListModeDeny, expectedErr: false},\n\t\t{ip: \"192.168.0.158\", protocol: ProtocolFTP, expectedMatch: true, expectedMode: dataprovider.ListModeDeny, expectedErr: false},\n\t\t{ip: \"192.168.0.158\", protocol: ProtocolHTTP, expectedMatch: false, expectedMode: 0, expectedErr: false},\n\t\t{ip: \"192.168.2.128\", protocol: ProtocolHTTP, expectedMatch: false, expectedMode: 0, expectedErr: false},\n\t\t{ip: \"192.168.2.128\", protocol: ProtocolSSH, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"::2\", protocol: ProtocolSSH, expectedMatch: false, expectedMode: 0, expectedErr: false},\n\t\t{ip: \"::2\", protocol: ProtocolWebDAV, expectedMatch: true, expectedMode: dataprovider.ListModeDeny, expectedErr: false},\n\t\t{ip: \"::1\", protocol: ProtocolSSH, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"::1\", protocol: ProtocolHTTP, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"2001:4860:4860:0000:0000:0000:0000:8889\", protocol: ProtocolSSH, expectedMatch: true, expectedMode: dataprovider.ListModeDeny, expectedErr: false},\n\t\t{ip: \"2001:4860:4860:0000:0000:0000:0000:8889\", protocol: ProtocolFTP, expectedMatch: false, expectedMode: 0, expectedErr: false},\n\t\t{ip: \"2001:4860:4860:0000:0000:0000:0000:8989\", protocol: ProtocolFTP, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"2001:4860:4860:0000:0000:0000:0000:89F1\", protocol: ProtocolSSH, expectedMatch: true, expectedMode: dataprovider.ListModeAllow, expectedErr: false},\n\t\t{ip: \"2001:4860:4860:0000:0000:0000:0000:89F1\", protocol: ProtocolHTTP, expectedMatch: false, expectedMode: 0, expectedErr: false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tmatch, mode, err := ipList.IsListed(tc.ip, tc.protocol)\n\t\tif tc.expectedErr {\n\t\t\tassert.Error(t, err, \"ip %s, protocol %s\", tc.ip, tc.protocol)\n\t\t} else {\n\t\t\tassert.NoError(t, err, \"ip %s, protocol %s\", tc.ip, tc.protocol)\n\t\t}\n\t\tassert.Equal(t, tc.expectedMatch, match, \"ip %s, protocol %s\", tc.ip, tc.protocol)\n\t\tassert.Equal(t, tc.expectedMode, mode, \"ip %s, protocol %s\", tc.ip, tc.protocol)\n\t}\n\n\tipList.DisableMemoryMode()\n\n\tfor _, tc := range tests {\n\t\tmatch, mode, err := ipList.IsListed(tc.ip, tc.protocol)\n\t\tif tc.expectedErr {\n\t\t\tassert.Error(t, err, \"ip %s, protocol %s\", tc.ip, tc.protocol)\n\t\t} else {\n\t\t\tassert.NoError(t, err, \"ip %s, protocol %s\", tc.ip, tc.protocol)\n\t\t}\n\t\tassert.Equal(t, tc.expectedMatch, match, \"ip %s, protocol %s\", tc.ip, tc.protocol)\n\t\tassert.Equal(t, tc.expectedMode, mode, \"ip %s, protocol %s\", tc.ip, tc.protocol)\n\t}\n\n\tfor _, e := range entries {\n\t\terr := dataprovider.DeleteIPListEntry(e.IPOrNet, e.Type, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestSQLPlaceholderLimits(t *testing.T) {\n\tnumGroups := 120\n\tnumUsers := 120\n\tvar groupMapping []sdk.GroupMapping\n\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       \"testfolder\",\n\t\tMappedPath: filepath.Join(os.TempDir(), \"folder\"),\n\t}\n\terr := dataprovider.AddFolder(&folder, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tfor i := 0; i < numGroups; i++ {\n\t\tgroup := dataprovider.Group{\n\t\t\tBaseGroup: sdk.BaseGroup{\n\t\t\t\tName: fmt.Sprintf(\"testgroup%d\", i),\n\t\t\t},\n\t\t\tUserSettings: dataprovider.GroupUserSettings{\n\t\t\t\tBaseGroupUserSettings: sdk.BaseGroupUserSettings{\n\t\t\t\t\tPermissions: map[string][]string{\n\t\t\t\t\t\tfmt.Sprintf(\"/dir%d\", i): {dataprovider.PermAny},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tgroup.VirtualFolders = append(group.VirtualFolders, vfs.VirtualFolder{\n\t\t\tBaseVirtualFolder: folder,\n\t\t\tVirtualPath:       \"/vdir\",\n\t\t})\n\t\terr := dataprovider.AddGroup(&group, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\n\t\tgroupMapping = append(groupMapping, sdk.GroupMapping{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t})\n\t}\n\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"testusername\",\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), \"testhome\"),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t\tGroups: groupMapping,\n\t}\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tusers, err := dataprovider.GetUsersForQuotaCheck(map[string]bool{user.Username: true})\n\tassert.NoError(t, err)\n\tif assert.Len(t, users, 1) {\n\t\tfor i := 0; i < numGroups; i++ {\n\t\t\t_, ok := users[0].Permissions[fmt.Sprintf(\"/dir%d\", i)]\n\t\t\tassert.True(t, ok)\n\t\t}\n\t}\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tfor i := 0; i < numUsers; i++ {\n\t\tuser := dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername: fmt.Sprintf(\"testusername%d\", i),\n\t\t\t\tHomeDir:  filepath.Join(os.TempDir()),\n\t\t\t\tStatus:   1,\n\t\t\t\tPermissions: map[string][]string{\n\t\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroups: []sdk.GroupMapping{\n\t\t\t\t{\n\t\t\t\t\tName: \"testgroup0\",\n\t\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\terr = dataprovider.DeleteFolder(folder.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tfor i := 0; i < numUsers; i++ {\n\t\tusername := fmt.Sprintf(\"testusername%d\", i)\n\t\tuser, err := dataprovider.UserExists(username, \"\")\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.UpdatedAt, user.CreatedAt)\n\t\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\n\tfor i := 0; i < numGroups; i++ {\n\t\tgroupName := fmt.Sprintf(\"testgroup%d\", i)\n\t\terr = dataprovider.DeleteGroup(groupName, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestALPNProtocols(t *testing.T) {\n\tprotocols := util.GetALPNProtocols(nil)\n\tassert.Equal(t, []string{\"http/1.1\", \"h2\"}, protocols)\n\tprotocols = util.GetALPNProtocols([]string{\"invalid1\", \"invalid2\"})\n\tassert.Equal(t, []string{\"http/1.1\", \"h2\"}, protocols)\n\tprotocols = util.GetALPNProtocols([]string{\"invalid1\", \"h2\", \"invalid2\"})\n\tassert.Equal(t, []string{\"h2\"}, protocols)\n\tprotocols = util.GetALPNProtocols([]string{\"h2\", \"http/1.1\"})\n\tassert.Equal(t, []string{\"h2\", \"http/1.1\"}, protocols)\n}\n\nfunc TestServerVersion(t *testing.T) {\n\tappName := \"SFTPGo\"\n\tversion.SetConfig(\"\")\n\tv := version.GetServerVersion(\"_\", false)\n\tassert.Equal(t, fmt.Sprintf(\"%s_%s\", appName, version.Get().Version), v)\n\tv = version.GetServerVersion(\"-\", true)\n\tassert.Equal(t, fmt.Sprintf(\"%s-%s-\", appName, version.Get().Version), v)\n\tversion.SetConfig(\"short\")\n\tv = version.GetServerVersion(\"_\", false)\n\tassert.Equal(t, appName, v)\n\tv = version.GetServerVersion(\"_\", true)\n\tassert.Equal(t, appName+\"_\", v)\n\tversion.SetConfig(\"\")\n}\n\nfunc BenchmarkBcryptHashing(b *testing.B) {\n\tbcryptPassword := \"bcryptpassword\"\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := bcrypt.GenerateFromPassword([]byte(bcryptPassword), 10)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc BenchmarkCompareBcryptPassword(b *testing.B) {\n\tbcryptPassword := \"$2a$10$lPDdnDimJZ7d5/GwL6xDuOqoZVRXok6OHHhivCnanWUtcgN0Zafki\"\n\tfor i := 0; i < b.N; i++ {\n\t\terr := bcrypt.CompareHashAndPassword([]byte(bcryptPassword), []byte(\"password\"))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc BenchmarkArgon2Hashing(b *testing.B) {\n\targonPassword := \"argon2password\"\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := argon2id.CreateHash(argonPassword, argon2id.DefaultParams)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc BenchmarkCompareArgon2Password(b *testing.B) {\n\targon2Password := \"$argon2id$v=19$m=65536,t=1,p=2$aOoAOdAwvzhOgi7wUFjXlw$wn/y37dBWdKHtPXHR03nNaKHWKPXyNuVXOknaU+YZ+s\"\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := argon2id.ComparePasswordAndHash(\"password\", argon2Password)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc BenchmarkAddRemoveConnections(b *testing.B) {\n\tvar conns []ActiveConnection\n\tfor i := 0; i < 100; i++ {\n\t\tconns = append(conns, &fakeConnection{\n\t\t\tBaseConnection: NewBaseConnection(fmt.Sprintf(\"id%d\", i), ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\t\tUsername: userTestUsername,\n\t\t\t\t},\n\t\t\t}),\n\t\t})\n\t}\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tfor _, c := range conns {\n\t\t\tif err := Connections.Add(c); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t\tvar wg sync.WaitGroup\n\t\tfor idx := len(conns) - 1; idx >= 0; idx-- {\n\t\t\twg.Add(1)\n\t\t\tgo func(index int) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tConnections.Remove(conns[index].GetID())\n\t\t\t}(idx)\n\t\t}\n\t\twg.Wait()\n\t}\n}\n\nfunc BenchmarkAddRemoveSSHConnections(b *testing.B) {\n\tconn1, conn2 := net.Pipe()\n\tvar conns []*SSHConnection\n\tfor i := 0; i < 2000; i++ {\n\t\tconns = append(conns, NewSSHConnection(fmt.Sprintf(\"id%d\", i), conn1))\n\t}\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tfor _, c := range conns {\n\t\t\tConnections.AddSSHConnection(c)\n\t\t}\n\t\tfor idx := len(conns) - 1; idx >= 0; idx-- {\n\t\t\tConnections.RemoveSSHConnection(conns[idx].GetID())\n\t\t}\n\t}\n\tconn1.Close()\n\tconn2.Close()\n}\n"
  },
  {
    "path": "internal/common/connection.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\n// BaseConnection defines common fields for a connection using any supported protocol\ntype BaseConnection struct {\n\t// last activity for this connection.\n\t// Since this field is accessed atomically we put it as first element of the struct to achieve 64 bit alignment\n\tlastActivity atomic.Int64\n\tuploadDone   atomic.Bool\n\tdownloadDone atomic.Bool\n\t// unique ID for a transfer.\n\t// This field is accessed atomically so we put it at the beginning of the struct to achieve 64 bit alignment\n\ttransferID atomic.Int64\n\t// Unique identifier for the connection\n\tID string\n\t// user associated with this connection if any\n\tUser dataprovider.User\n\t// start time for this connection\n\tstartTime  time.Time\n\tprotocol   string\n\tremoteAddr string\n\tlocalAddr  string\n\tsync.RWMutex\n\tactiveTransfers []ActiveTransfer\n}\n\n// NewBaseConnection returns a new BaseConnection\nfunc NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprovider.User) *BaseConnection {\n\tconnID := id\n\tif slices.Contains(supportedProtocols, protocol) {\n\t\tconnID = fmt.Sprintf(\"%s_%s\", protocol, id)\n\t}\n\tuser.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)\n\tc := &BaseConnection{\n\t\tID:         connID,\n\t\tUser:       user,\n\t\tstartTime:  time.Now(),\n\t\tprotocol:   protocol,\n\t\tlocalAddr:  localAddr,\n\t\tremoteAddr: remoteAddr,\n\t}\n\tc.transferID.Store(0)\n\tc.lastActivity.Store(time.Now().UnixNano())\n\n\treturn c\n}\n\n// Log outputs a log entry to the configured logger\nfunc (c *BaseConnection) Log(level logger.LogLevel, format string, v ...any) {\n\tlogger.Log(level, c.protocol, c.ID, format, v...)\n}\n\n// GetTransferID returns an unique transfer ID for this connection\nfunc (c *BaseConnection) GetTransferID() int64 {\n\treturn c.transferID.Add(1)\n}\n\n// GetID returns the connection ID\nfunc (c *BaseConnection) GetID() string {\n\treturn c.ID\n}\n\n// GetUsername returns the authenticated username associated with this connection if any\nfunc (c *BaseConnection) GetUsername() string {\n\treturn c.User.Username\n}\n\n// GetRole returns the role for the user associated with this connection\nfunc (c *BaseConnection) GetRole() string {\n\treturn c.User.Role\n}\n\n// GetMaxSessions returns the maximum number of concurrent sessions allowed\nfunc (c *BaseConnection) GetMaxSessions() int {\n\treturn c.User.MaxSessions\n}\n\n// isAccessAllowed returns true if the user's access conditions are met\nfunc (c *BaseConnection) isAccessAllowed() bool {\n\tif err := c.User.CheckLoginConditions(); err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// GetProtocol returns the protocol for the connection\nfunc (c *BaseConnection) GetProtocol() string {\n\treturn c.protocol\n}\n\n// GetRemoteIP returns the remote ip address\nfunc (c *BaseConnection) GetRemoteIP() string {\n\treturn util.GetIPFromRemoteAddress(c.remoteAddr)\n}\n\n// SetProtocol sets the protocol for this connection\nfunc (c *BaseConnection) SetProtocol(protocol string) {\n\tc.protocol = protocol\n\tif slices.Contains(supportedProtocols, c.protocol) {\n\t\tc.ID = fmt.Sprintf(\"%v_%v\", c.protocol, c.ID)\n\t}\n}\n\n// GetConnectionTime returns the initial connection time\nfunc (c *BaseConnection) GetConnectionTime() time.Time {\n\treturn c.startTime\n}\n\n// UpdateLastActivity updates last activity for this connection\nfunc (c *BaseConnection) UpdateLastActivity() {\n\tc.lastActivity.Store(time.Now().UnixNano())\n}\n\n// GetLastActivity returns the last connection activity\nfunc (c *BaseConnection) GetLastActivity() time.Time {\n\treturn time.Unix(0, c.lastActivity.Load())\n}\n\n// CloseFS closes the underlying fs\nfunc (c *BaseConnection) CloseFS() error {\n\treturn c.User.CloseFs()\n}\n\n// AddTransfer associates a new transfer to this connection\nfunc (c *BaseConnection) AddTransfer(t ActiveTransfer) {\n\tConnections.transfers.add(c.User.Username)\n\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tc.activeTransfers = append(c.activeTransfers, t)\n\tc.Log(logger.LevelDebug, \"transfer added, id: %v, active transfers: %v\", t.GetID(), len(c.activeTransfers))\n\tif t.HasSizeLimit() {\n\t\tfolderName := \"\"\n\t\tif t.GetType() == TransferUpload {\n\t\t\tvfolder, err := c.User.GetVirtualFolderForPath(path.Dir(t.GetVirtualPath()))\n\t\t\tif err == nil {\n\t\t\t\tif !vfolder.IsIncludedInUserQuota() {\n\t\t\t\t\tfolderName = vfolder.Name\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tgo transfersChecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\t\tID:            t.GetID(),\n\t\t\tType:          t.GetType(),\n\t\t\tConnID:        c.ID,\n\t\t\tUsername:      c.GetUsername(),\n\t\t\tFolderName:    folderName,\n\t\t\tIP:            c.GetRemoteIP(),\n\t\t\tTruncatedSize: t.GetTruncatedSize(),\n\t\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t})\n\t}\n}\n\n// RemoveTransfer removes the specified transfer from the active ones\nfunc (c *BaseConnection) RemoveTransfer(t ActiveTransfer) {\n\tConnections.transfers.remove(c.User.Username)\n\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tif t.HasSizeLimit() {\n\t\tgo transfersChecker.RemoveTransfer(t.GetID(), c.ID)\n\t}\n\n\tfor idx, transfer := range c.activeTransfers {\n\t\tif transfer.GetID() == t.GetID() {\n\t\t\tlastIdx := len(c.activeTransfers) - 1\n\t\t\tc.activeTransfers[idx] = c.activeTransfers[lastIdx]\n\t\t\tc.activeTransfers[lastIdx] = nil\n\t\t\tc.activeTransfers = c.activeTransfers[:lastIdx]\n\t\t\tc.Log(logger.LevelDebug, \"transfer removed, id: %v active transfers: %v\", t.GetID(), len(c.activeTransfers))\n\t\t\treturn\n\t\t}\n\t}\n\tc.Log(logger.LevelWarn, \"transfer to remove with id %v not found!\", t.GetID())\n}\n\n// SignalTransferClose makes the transfer fail on the next read/write with the\n// specified error\nfunc (c *BaseConnection) SignalTransferClose(transferID int64, err error) {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tfor _, t := range c.activeTransfers {\n\t\tif t.GetID() == transferID {\n\t\t\tc.Log(logger.LevelInfo, \"signal transfer close for transfer id %v\", transferID)\n\t\t\tt.SignalClose(err)\n\t\t}\n\t}\n}\n\n// GetTransfers returns the active transfers\nfunc (c *BaseConnection) GetTransfers() []ConnectionTransfer {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\ttransfers := make([]ConnectionTransfer, 0, len(c.activeTransfers))\n\tfor _, t := range c.activeTransfers {\n\t\tvar operationType string\n\t\tswitch t.GetType() {\n\t\tcase TransferDownload:\n\t\t\toperationType = operationDownload\n\t\tcase TransferUpload:\n\t\t\toperationType = operationUpload\n\t\t}\n\t\ttransfers = append(transfers, ConnectionTransfer{\n\t\t\tID:            t.GetID(),\n\t\t\tOperationType: operationType,\n\t\t\tStartTime:     util.GetTimeAsMsSinceEpoch(t.GetStartTime()),\n\t\t\tSize:          t.GetSize(),\n\t\t\tVirtualPath:   t.GetVirtualPath(),\n\t\t\tHasSizeLimit:  t.HasSizeLimit(),\n\t\t\tULSize:        t.GetUploadedSize(),\n\t\t\tDLSize:        t.GetDownloadedSize(),\n\t\t})\n\t}\n\n\treturn transfers\n}\n\n// SignalTransfersAbort signals to the active transfers to exit as soon as possible\nfunc (c *BaseConnection) SignalTransfersAbort() error {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tif len(c.activeTransfers) == 0 {\n\t\treturn errors.New(\"no active transfer found\")\n\t}\n\n\tfor _, t := range c.activeTransfers {\n\t\tt.SignalClose(ErrTransferAborted)\n\t}\n\treturn nil\n}\n\nfunc (c *BaseConnection) getRealFsPath(fsPath string) string {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tfor _, t := range c.activeTransfers {\n\t\tif p := t.GetRealFsPath(fsPath); p != \"\" {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn fsPath\n}\n\nfunc (c *BaseConnection) setTimes(fsPath string, atime time.Time, mtime time.Time) bool {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tfor _, t := range c.activeTransfers {\n\t\tif t.SetTimes(fsPath, atime, mtime) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// getInfoForOngoingUpload returns upload statistics for an upload currently in\n// progress on this connection.\nfunc (c *BaseConnection) getInfoForOngoingUpload(fsPath string) (os.FileInfo, error) {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tfor _, t := range c.activeTransfers {\n\t\tif t.GetType() == TransferUpload && t.GetFsPath() == fsPath {\n\t\t\treturn vfs.NewFileInfo(t.GetVirtualPath(), false, t.GetSize(), t.GetStartTime(), false), nil\n\t\t}\n\t}\n\treturn nil, os.ErrNotExist\n}\n\nfunc (c *BaseConnection) truncateOpenHandle(fsPath string, size int64) (int64, error) {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tfor _, t := range c.activeTransfers {\n\t\tinitialSize, err := t.Truncate(fsPath, size)\n\t\tif err != errTransferMismatch {\n\t\t\treturn initialSize, err\n\t\t}\n\t}\n\n\treturn 0, errNoTransfer\n}\n\n// ListDir reads the directory matching virtualPath and returns a list of directory entries\nfunc (c *BaseConnection) ListDir(virtualPath string) (*DirListerAt, error) {\n\tif !c.User.HasPerm(dataprovider.PermListItems, virtualPath) {\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\tfs, fsPath, err := c.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlister, err := fs.ReadDir(fsPath)\n\tif err != nil {\n\t\tc.Log(logger.LevelDebug, \"error listing directory: %+v\", err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\treturn &DirListerAt{\n\t\tvirtualPath: virtualPath,\n\t\tconn:        c,\n\t\tfs:          fs,\n\t\tinfo:        c.User.GetVirtualFoldersInfo(virtualPath),\n\t\tlister:      lister,\n\t}, nil\n}\n\n// CheckParentDirs tries to create the specified directory and any missing parent dirs\nfunc (c *BaseConnection) CheckParentDirs(virtualPath string) error {\n\tfs, err := c.User.GetFilesystemForPath(virtualPath, c.GetID())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif fs.HasVirtualFolders() {\n\t\treturn nil\n\t}\n\tif _, err := c.DoStat(virtualPath, 0, false); !c.IsNotExistError(err) {\n\t\treturn err\n\t}\n\tdirs := util.GetDirsForVirtualPath(virtualPath)\n\tfor idx := len(dirs) - 1; idx >= 0; idx-- {\n\t\tfs, err = c.User.GetFilesystemForPath(dirs[idx], c.GetID())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fs.HasVirtualFolders() {\n\t\t\tcontinue\n\t\t}\n\t\tif err = c.createDirIfMissing(dirs[idx]); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to check/create missing parent dir %q for virtual path %q: %w\",\n\t\t\t\tdirs[idx], virtualPath, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetCreateChecks returns the checks for creating new files\nfunc (c *BaseConnection) GetCreateChecks(virtualPath string, isNewFile bool, isResume bool) int {\n\tresult := 0\n\tif !isNewFile {\n\t\tif isResume {\n\t\t\tresult += vfs.CheckResume\n\t\t}\n\t\treturn result\n\t}\n\tif !c.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(virtualPath)) {\n\t\tresult += vfs.CheckParentDir\n\t\treturn result\n\t}\n\treturn result\n}\n\n// CreateDir creates a new directory at the specified fsPath\nfunc (c *BaseConnection) CreateDir(virtualPath string, checkFilePatterns bool) error {\n\tif !c.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(virtualPath)) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif checkFilePatterns {\n\t\tif ok, _ := c.User.IsFileAllowed(virtualPath); !ok {\n\t\t\treturn c.GetPermissionDeniedError()\n\t\t}\n\t}\n\tif c.User.IsVirtualFolder(virtualPath) {\n\t\tc.Log(logger.LevelWarn, \"mkdir not allowed %q is a virtual folder\", virtualPath)\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tfs, fsPath, err := c.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstartTime := time.Now()\n\tif err := fs.Mkdir(fsPath); err != nil {\n\t\tc.Log(logger.LevelError, \"error creating dir: %q error: %+v\", fsPath, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\tvfs.SetPathPermissions(fs, fsPath, c.User.GetUID(), c.User.GetGID())\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\n\tlogger.CommandLog(mkdirLogSender, fsPath, \"\", c.User.Username, \"\", c.ID, c.protocol, -1, -1, \"\", \"\", \"\", -1,\n\t\tc.localAddr, c.remoteAddr, elapsed)\n\tExecuteActionNotification(c, operationMkdir, fsPath, virtualPath, \"\", \"\", \"\", 0, nil, elapsed, nil) //nolint:errcheck\n\treturn nil\n}\n\n// IsRemoveFileAllowed returns an error if removing this file is not allowed\nfunc (c *BaseConnection) IsRemoveFileAllowed(virtualPath string) error {\n\tif !c.User.HasAnyPerm([]string{dataprovider.PermDeleteFiles, dataprovider.PermDelete}, path.Dir(virtualPath)) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif ok, policy := c.User.IsFileAllowed(virtualPath); !ok {\n\t\tc.Log(logger.LevelDebug, \"removing file %q is not allowed\", virtualPath)\n\t\treturn c.GetErrorForDeniedFile(policy)\n\t}\n\treturn nil\n}\n\n// RemoveFile removes a file at the specified fsPath\nfunc (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info os.FileInfo) error {\n\tif err := c.IsRemoveFileAllowed(virtualPath); err != nil {\n\t\treturn err\n\t}\n\n\tsize := info.Size()\n\tstatus, err := ExecutePreAction(c, operationPreDelete, fsPath, virtualPath, size, 0)\n\tif err != nil {\n\t\tc.Log(logger.LevelDebug, \"delete for file %q denied by pre action: %v\", virtualPath, err)\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tupdateQuota := true\n\tstartTime := time.Now()\n\tif err := fs.Remove(fsPath, false); err != nil {\n\t\tif status > 0 && fs.IsNotExist(err) {\n\t\t\t// file removed in the pre-action, if the file was deleted from the EventManager the quota is already updated\n\t\t\tc.Log(logger.LevelDebug, \"file deleted from the hook, status: %d\", status)\n\t\t\tupdateQuota = (status == 1)\n\t\t} else {\n\t\t\tc.Log(logger.LevelError, \"failed to remove file/symlink %q: %+v\", fsPath, err)\n\t\t\treturn c.GetFsError(fs, err)\n\t\t}\n\t}\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\n\tlogger.CommandLog(removeLogSender, fsPath, \"\", c.User.Username, \"\", c.ID, c.protocol, -1, -1, \"\", \"\", \"\", -1,\n\t\tc.localAddr, c.remoteAddr, elapsed)\n\tif updateQuota && info.Mode()&os.ModeSymlink == 0 {\n\t\tvfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))\n\t\tif err == nil {\n\t\t\tdataprovider.UpdateUserFolderQuota(&vfolder, &c.User, -1, -size, false)\n\t\t} else {\n\t\t\tdataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck\n\t\t}\n\t}\n\tExecuteActionNotification(c, operationDelete, fsPath, virtualPath, \"\", \"\", \"\", size, nil, elapsed, nil) //nolint:errcheck\n\treturn nil\n}\n\n// IsRemoveDirAllowed returns an error if removing this directory is not allowed\nfunc (c *BaseConnection) IsRemoveDirAllowed(fs vfs.Fs, fsPath, virtualPath string) error {\n\tif virtualPath == \"/\" || fs.GetRelativePath(fsPath) == \"/\" {\n\t\tc.Log(logger.LevelWarn, \"removing root dir is not allowed\")\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif c.User.IsVirtualFolder(virtualPath) {\n\t\tc.Log(logger.LevelWarn, \"removing a virtual folder is not allowed: %q\", virtualPath)\n\t\treturn fmt.Errorf(\"removing virtual folders is not allowed: %w\", c.GetPermissionDeniedError())\n\t}\n\tif c.User.HasVirtualFoldersInside(virtualPath) {\n\t\tc.Log(logger.LevelWarn, \"removing a directory with a virtual folder inside is not allowed: %q\", virtualPath)\n\t\treturn fmt.Errorf(\"cannot remove directory %q with virtual folders inside: %w\", virtualPath, c.GetOpUnsupportedError())\n\t}\n\tif c.User.IsMappedPath(fsPath) {\n\t\tc.Log(logger.LevelWarn, \"removing a directory mapped as virtual folder is not allowed: %q\", fsPath)\n\t\treturn fmt.Errorf(\"removing the directory %q mapped as virtual folder is not allowed: %w\",\n\t\t\tvirtualPath, c.GetPermissionDeniedError())\n\t}\n\tif !c.User.HasAnyPerm([]string{dataprovider.PermDeleteDirs, dataprovider.PermDelete}, path.Dir(virtualPath)) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif ok, policy := c.User.IsFileAllowed(virtualPath); !ok {\n\t\tc.Log(logger.LevelDebug, \"removing directory %q is not allowed\", virtualPath)\n\t\treturn c.GetErrorForDeniedFile(policy)\n\t}\n\treturn nil\n}\n\n// RemoveDir removes a directory at the specified fsPath\nfunc (c *BaseConnection) RemoveDir(virtualPath string) error {\n\tfs, fsPath, err := c.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := c.IsRemoveDirAllowed(fs, fsPath, virtualPath); err != nil {\n\t\treturn err\n\t}\n\n\tvar fi os.FileInfo\n\tif fi, err = fs.Lstat(fsPath); err != nil {\n\t\t// see #149\n\t\tif fs.IsNotExist(err) && fs.HasVirtualFolders() {\n\t\t\treturn nil\n\t\t}\n\t\tc.Log(logger.LevelError, \"failed to remove a dir %q: stat error: %+v\", fsPath, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\tif !fi.IsDir() || fi.Mode()&os.ModeSymlink != 0 {\n\t\tc.Log(logger.LevelError, \"cannot remove %q is not a directory\", fsPath)\n\t\treturn c.GetGenericError(nil)\n\t}\n\n\tstartTime := time.Now()\n\tif err := fs.Remove(fsPath, true); err != nil {\n\t\tc.Log(logger.LevelError, \"failed to remove directory %q: %+v\", fsPath, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\n\tlogger.CommandLog(rmdirLogSender, fsPath, \"\", c.User.Username, \"\", c.ID, c.protocol, -1, -1, \"\", \"\", \"\", -1,\n\t\tc.localAddr, c.remoteAddr, elapsed)\n\tExecuteActionNotification(c, operationRmdir, fsPath, virtualPath, \"\", \"\", \"\", 0, nil, elapsed, nil) //nolint:errcheck\n\treturn nil\n}\n\nfunc (c *BaseConnection) doRecursiveRemoveDirEntry(virtualPath string, info os.FileInfo, recursion int) error {\n\tfs, fsPath, err := c.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn c.doRecursiveRemove(fs, fsPath, virtualPath, info, recursion)\n}\n\nfunc (c *BaseConnection) doRecursiveRemove(fs vfs.Fs, fsPath, virtualPath string, info os.FileInfo, recursion int) error {\n\tif info.IsDir() {\n\t\tif recursion >= util.MaxRecursion {\n\t\t\tc.Log(logger.LevelError, \"recursive rename failed, recursion too depth: %d\", recursion)\n\t\t\treturn util.ErrRecursionTooDeep\n\t\t}\n\t\trecursion++\n\t\tlister, err := c.ListDir(virtualPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to get lister for dir %q: %w\", virtualPath, err)\n\t\t}\n\t\tdefer lister.Close()\n\n\t\tfor {\n\t\t\tentries, err := lister.Next(vfs.ListerBatchSize)\n\t\t\tfinished := errors.Is(err, io.EOF)\n\t\t\tif err != nil && !finished {\n\t\t\t\treturn fmt.Errorf(\"unable to get content for dir %q: %w\", virtualPath, err)\n\t\t\t}\n\t\t\tfor _, fi := range entries {\n\t\t\t\ttargetPath := path.Join(virtualPath, fi.Name())\n\t\t\t\tif err := c.doRecursiveRemoveDirEntry(targetPath, fi, recursion); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif finished {\n\t\t\t\tlister.Close()\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn c.RemoveDir(virtualPath)\n\t}\n\treturn c.RemoveFile(fs, fsPath, virtualPath, info)\n}\n\n// RemoveAll removes the specified path and any children it contains\nfunc (c *BaseConnection) RemoveAll(virtualPath string) error {\n\tfs, fsPath, err := c.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfi, err := fs.Lstat(fsPath)\n\tif err != nil {\n\t\tc.Log(logger.LevelDebug, \"failed to remove path %q: stat error: %+v\", fsPath, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\tif fi.IsDir() && fi.Mode()&os.ModeSymlink == 0 {\n\t\tif err := c.IsRemoveDirAllowed(fs, fsPath, virtualPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.doRecursiveRemove(fs, fsPath, virtualPath, fi, 0)\n\t}\n\treturn c.RemoveFile(fs, fsPath, virtualPath, fi)\n}\n\nfunc (c *BaseConnection) checkCopy(srcInfo, dstInfo os.FileInfo, virtualSource, virtualTarget string) error {\n\t_, fsSourcePath, err := c.GetFsAndResolvedPath(virtualSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, fsTargetPath, err := c.GetFsAndResolvedPath(virtualTarget)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif srcInfo.IsDir() {\n\t\tif dstInfo != nil && !dstInfo.IsDir() {\n\t\t\treturn fmt.Errorf(\"cannot overwrite file %q with dir %q: %w\", virtualTarget, virtualSource, c.GetOpUnsupportedError())\n\t\t}\n\t\tif util.IsDirOverlapped(virtualSource, virtualTarget, true, \"/\") {\n\t\t\treturn fmt.Errorf(\"nested copy %q => %q is not supported: %w\", virtualSource, virtualTarget, c.GetOpUnsupportedError())\n\t\t}\n\t\tif util.IsDirOverlapped(fsSourcePath, fsTargetPath, true, c.User.FsConfig.GetPathSeparator()) {\n\t\t\tc.Log(logger.LevelWarn, \"nested fs copy %q => %q not allowed\", fsSourcePath, fsTargetPath)\n\t\t\treturn fmt.Errorf(\"nested fs copy is not supported: %w\", c.GetOpUnsupportedError())\n\t\t}\n\t\treturn nil\n\t}\n\tif dstInfo != nil && dstInfo.IsDir() {\n\t\treturn fmt.Errorf(\"cannot overwrite file %q with dir %q: %w\", virtualSource, virtualTarget, c.GetOpUnsupportedError())\n\t}\n\tif c.IsSameResource(virtualSource, virtualTarget) {\n\t\tif fsSourcePath == fsTargetPath {\n\t\t\treturn fmt.Errorf(\"the copy source and target cannot be the same: %w\", c.GetOpUnsupportedError())\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo) error {\n\tif !c.User.HasPerm(dataprovider.PermCopy, virtualSourcePath) || !c.User.HasPerm(dataprovider.PermCopy, virtualTargetPath) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif ok, _ := c.User.IsFileAllowed(virtualTargetPath); !ok {\n\t\treturn fmt.Errorf(\"file %q is not allowed: %w\", virtualTargetPath, c.GetPermissionDeniedError())\n\t}\n\tif c.IsSameResource(virtualSourcePath, virtualTargetPath) {\n\t\tfs, fsTargetPath, err := c.GetFsAndResolvedPath(virtualTargetPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif copier, ok := fs.(vfs.FsFileCopier); ok {\n\t\t\t_, fsSourcePath, err := c.GetFsAndResolvedPath(virtualSourcePath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tstartTime := time.Now()\n\t\t\tnumFiles, sizeDiff, err := copier.CopyFile(fsSourcePath, fsTargetPath, srcInfo)\n\t\t\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\t\t\tupdateUserQuotaAfterFileWrite(c, virtualTargetPath, numFiles, sizeDiff)\n\t\t\tlogger.CommandLog(copyLogSender, fsSourcePath, fsTargetPath, c.User.Username, \"\", c.ID, c.protocol, -1, -1,\n\t\t\t\t\"\", \"\", \"\", srcInfo.Size(), c.localAddr, c.remoteAddr, elapsed)\n\t\t\tExecuteActionNotification(c, operationCopy, fsSourcePath, virtualSourcePath, fsTargetPath, virtualTargetPath, \"\", srcInfo.Size(), err, elapsed, nil) //nolint:errcheck\n\t\t\treturn err\n\t\t}\n\t}\n\n\treader, rCancelFn, err := getFileReader(c, virtualSourcePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get reader for path %q: %w\", virtualSourcePath, err)\n\t}\n\tdefer rCancelFn()\n\tdefer reader.Close()\n\n\twriter, numFiles, truncatedSize, wCancelFn, err := getFileWriter(c, virtualTargetPath, srcInfo.Size())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get writer for path %q: %w\", virtualTargetPath, err)\n\t}\n\tdefer wCancelFn()\n\n\tstartTime := time.Now()\n\t_, err = io.Copy(writer, reader)\n\treturn closeWriterAndUpdateQuota(writer, c, virtualSourcePath, virtualTargetPath, numFiles, truncatedSize,\n\t\terr, operationCopy, startTime)\n}\n\nfunc (c *BaseConnection) doRecursiveCopy(virtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo,\n\tcreateTargetDir bool, recursion int,\n) error {\n\tif srcInfo.IsDir() {\n\t\tif recursion >= util.MaxRecursion {\n\t\t\tc.Log(logger.LevelError, \"recursive copy failed, recursion too depth: %d\", recursion)\n\t\t\treturn util.ErrRecursionTooDeep\n\t\t}\n\t\trecursion++\n\t\tif createTargetDir {\n\t\t\tif err := c.CreateDir(virtualTargetPath, false); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to create directory %q: %w\", virtualTargetPath, err)\n\t\t\t}\n\t\t}\n\t\tlister, err := c.ListDir(virtualSourcePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to get lister for dir %q: %w\", virtualSourcePath, err)\n\t\t}\n\t\tdefer lister.Close()\n\n\t\tfor {\n\t\t\tentries, err := lister.Next(vfs.ListerBatchSize)\n\t\t\tfinished := errors.Is(err, io.EOF)\n\t\t\tif err != nil && !finished {\n\t\t\t\treturn fmt.Errorf(\"unable to get contents for dir %q: %w\", virtualSourcePath, err)\n\t\t\t}\n\t\t\tif err := c.recursiveCopyEntries(virtualSourcePath, virtualTargetPath, entries, recursion); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif finished {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\tif !srcInfo.Mode().IsRegular() {\n\t\tc.Log(logger.LevelInfo, \"skipping copy for non regular file %q\", virtualSourcePath)\n\t\treturn nil\n\t}\n\n\treturn c.copyFile(virtualSourcePath, virtualTargetPath, srcInfo)\n}\n\nfunc (c *BaseConnection) recursiveCopyEntries(virtualSourcePath, virtualTargetPath string, entries []os.FileInfo, recursion int) error {\n\tfor _, info := range entries {\n\t\tsourcePath := path.Join(virtualSourcePath, info.Name())\n\t\ttargetPath := path.Join(virtualTargetPath, info.Name())\n\t\ttargetInfo, err := c.DoStat(targetPath, 1, false)\n\t\tif err == nil {\n\t\t\tif info.IsDir() && targetInfo.IsDir() {\n\t\t\t\tc.Log(logger.LevelDebug, \"target copy dir %q already exists\", targetPath)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif err != nil && !c.IsNotExistError(err) {\n\t\t\treturn err\n\t\t}\n\t\tif err := c.checkCopy(info, targetInfo, sourcePath, targetPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := c.doRecursiveCopy(sourcePath, targetPath, info, true, recursion); err != nil {\n\t\t\tif c.IsNotExistError(err) {\n\t\t\t\tc.Log(logger.LevelInfo, \"skipping copy for source path %q: %v\", sourcePath, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Copy virtualSourcePath to virtualTargetPath\nfunc (c *BaseConnection) Copy(virtualSourcePath, virtualTargetPath string) error {\n\tcopyFromSource := strings.HasSuffix(virtualSourcePath, \"/\")\n\tcopyInTarget := strings.HasSuffix(virtualTargetPath, \"/\")\n\tvirtualSourcePath = path.Clean(virtualSourcePath)\n\tvirtualTargetPath = path.Clean(virtualTargetPath)\n\tif virtualSourcePath == virtualTargetPath {\n\t\treturn fmt.Errorf(\"the copy source and target cannot be the same: %w\", c.GetOpUnsupportedError())\n\t}\n\tsrcInfo, err := c.DoStat(virtualSourcePath, 1, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif srcInfo.Mode()&os.ModeSymlink != 0 {\n\t\treturn fmt.Errorf(\"copying symlinks is not supported: %w\", c.GetOpUnsupportedError())\n\t}\n\tdstInfo, err := c.DoStat(virtualTargetPath, 1, false)\n\tif err == nil && !copyFromSource {\n\t\tcopyInTarget = dstInfo.IsDir()\n\t}\n\tif err != nil && !c.IsNotExistError(err) {\n\t\treturn err\n\t}\n\tdestPath := virtualTargetPath\n\tif copyInTarget {\n\t\tdestPath = path.Join(virtualTargetPath, path.Base(virtualSourcePath))\n\t\tdstInfo, err = c.DoStat(destPath, 1, false)\n\t\tif err != nil && !c.IsNotExistError(err) {\n\t\t\treturn err\n\t\t}\n\t}\n\tcreateTargetDir := dstInfo == nil || !dstInfo.IsDir()\n\tif err := c.checkCopy(srcInfo, dstInfo, virtualSourcePath, destPath); err != nil {\n\t\treturn err\n\t}\n\tif err := c.CheckParentDirs(path.Dir(destPath)); err != nil {\n\t\treturn err\n\t}\n\tstopKeepAlive := keepConnectionAlive(c, 2*time.Minute)\n\tdefer stopKeepAlive()\n\n\treturn c.doRecursiveCopy(virtualSourcePath, destPath, srcInfo, createTargetDir, 0)\n}\n\n// Rename renames (moves) virtualSourcePath to virtualTargetPath\nfunc (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) error {\n\treturn c.renameInternal(virtualSourcePath, virtualTargetPath, false, vfs.CheckParentDir)\n}\n\nfunc (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string, //nolint:gocyclo\n\tcheckParentDestination bool, checks int,\n) error {\n\tif virtualSourcePath == virtualTargetPath {\n\t\treturn fmt.Errorf(\"the rename source and target cannot be the same: %w\", c.GetOpUnsupportedError())\n\t}\n\tfsSrc, fsSourcePath, err := c.GetFsAndResolvedPath(virtualSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfsDst, fsTargetPath, err := c.GetFsAndResolvedPath(virtualTargetPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstartTime := time.Now()\n\tsrcInfo, err := fsSrc.Lstat(fsSourcePath)\n\tif err != nil {\n\t\treturn c.GetFsError(fsSrc, err)\n\t}\n\tif !c.isRenamePermitted(fsSrc, fsDst, fsSourcePath, fsTargetPath, virtualSourcePath, virtualTargetPath, srcInfo) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tinitialSize := int64(-1)\n\tdstInfo, err := fsDst.Lstat(fsTargetPath)\n\tif err != nil && !fsDst.IsNotExist(err) {\n\t\treturn err\n\t}\n\tif err == nil {\n\t\tcheckParentDestination = false\n\t\tif dstInfo.IsDir() {\n\t\t\tc.Log(logger.LevelWarn, \"attempted to rename %q overwriting an existing directory %q\",\n\t\t\t\tfsSourcePath, fsTargetPath)\n\t\t\treturn c.GetOpUnsupportedError()\n\t\t}\n\t\t// we are overwriting an existing file/symlink\n\t\tif dstInfo.Mode().IsRegular() {\n\t\t\tinitialSize = dstInfo.Size()\n\t\t}\n\t\tif !c.User.HasPerm(dataprovider.PermOverwrite, path.Dir(virtualTargetPath)) {\n\t\t\tc.Log(logger.LevelDebug, \"renaming %q -> %q is not allowed. Target exists but the user %q\"+\n\t\t\t\t\"has no overwrite permission\", virtualSourcePath, virtualTargetPath, c.User.Username)\n\t\t\treturn c.GetPermissionDeniedError()\n\t\t}\n\t}\n\tif srcInfo.IsDir() {\n\t\tif err := c.checkFolderRename(fsSrc, fsDst, fsSourcePath, fsTargetPath, virtualSourcePath, virtualTargetPath, srcInfo); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif !c.hasSpaceForRename(fsSrc, virtualSourcePath, virtualTargetPath, initialSize, fsSourcePath, srcInfo) {\n\t\tc.Log(logger.LevelInfo, \"denying cross rename due to space limit\")\n\t\treturn c.GetGenericError(ErrQuotaExceeded)\n\t}\n\tif checkParentDestination {\n\t\tc.CheckParentDirs(path.Dir(virtualTargetPath)) //nolint:errcheck\n\t}\n\tstopKeepAlive := keepConnectionAlive(c, 2*time.Minute)\n\tdefer stopKeepAlive()\n\n\tfiles, size, err := fsDst.Rename(fsSourcePath, fsTargetPath, checks)\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"failed to rename %q -> %q: %+v\", fsSourcePath, fsTargetPath, err)\n\t\treturn c.GetFsError(fsSrc, err)\n\t}\n\tvfs.SetPathPermissions(fsDst, fsTargetPath, c.User.GetUID(), c.User.GetGID())\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\tc.updateQuotaAfterRename(fsDst, virtualSourcePath, virtualTargetPath, fsTargetPath, initialSize, files, size) //nolint:errcheck\n\tlogger.CommandLog(renameLogSender, fsSourcePath, fsTargetPath, c.User.Username, \"\", c.ID, c.protocol, -1, -1,\n\t\t\"\", \"\", \"\", -1, c.localAddr, c.remoteAddr, elapsed)\n\tExecuteActionNotification(c, operationRename, fsSourcePath, virtualSourcePath, fsTargetPath, //nolint:errcheck\n\t\tvirtualTargetPath, \"\", 0, nil, elapsed, nil)\n\n\treturn nil\n}\n\n// CreateSymlink creates fsTargetPath as a symbolic link to fsSourcePath\nfunc (c *BaseConnection) CreateSymlink(virtualSourcePath, virtualTargetPath string) error {\n\tvar relativePath string\n\tif !path.IsAbs(virtualSourcePath) {\n\t\trelativePath = virtualSourcePath\n\t\tvirtualSourcePath = path.Join(path.Dir(virtualTargetPath), relativePath)\n\t\tc.Log(logger.LevelDebug, \"link relative path %q resolved as %q, target path %q\",\n\t\t\trelativePath, virtualSourcePath, virtualTargetPath)\n\t}\n\tif c.isCrossFoldersRequest(virtualSourcePath, virtualTargetPath) {\n\t\tc.Log(logger.LevelWarn, \"cross folder symlink is not supported, src: %v dst: %v\", virtualSourcePath, virtualTargetPath)\n\t\treturn c.GetOpUnsupportedError()\n\t}\n\t// we cannot have a cross folder request here so only one fs is enough\n\tfs, fsSourcePath, err := c.GetFsAndResolvedPath(virtualSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfsTargetPath, err := fs.ResolvePath(virtualTargetPath)\n\tif err != nil {\n\t\treturn c.GetFsError(fs, err)\n\t}\n\tif fs.GetRelativePath(fsSourcePath) == \"/\" {\n\t\tc.Log(logger.LevelError, \"symlinking root dir is not allowed\")\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif fs.GetRelativePath(fsTargetPath) == \"/\" {\n\t\tc.Log(logger.LevelError, \"symlinking to root dir is not allowed\")\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif !c.User.HasPerm(dataprovider.PermCreateSymlinks, path.Dir(virtualTargetPath)) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tok, policy := c.User.IsFileAllowed(virtualSourcePath)\n\tif !ok && policy == sdk.DenyPolicyHide {\n\t\tc.Log(logger.LevelError, \"symlink source path %q is not allowed\", virtualSourcePath)\n\t\treturn c.GetNotExistError()\n\t}\n\tif ok, _ = c.User.IsFileAllowed(virtualTargetPath); !ok {\n\t\tc.Log(logger.LevelError, \"symlink target path %q is not allowed\", virtualTargetPath)\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif relativePath != \"\" {\n\t\tfsSourcePath = relativePath\n\t}\n\tstartTime := time.Now()\n\tif err := fs.Symlink(fsSourcePath, fsTargetPath); err != nil {\n\t\tc.Log(logger.LevelError, \"failed to create symlink %q -> %q: %+v\", fsSourcePath, fsTargetPath, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\tlogger.CommandLog(symlinkLogSender, fsSourcePath, fsTargetPath, c.User.Username, \"\", c.ID, c.protocol, -1, -1, \"\",\n\t\t\"\", \"\", -1, c.localAddr, c.remoteAddr, elapsed)\n\treturn nil\n}\n\nfunc (c *BaseConnection) doStatInternal(virtualPath string, mode int, checkFilePatterns,\n\tconvertResult bool,\n) (os.FileInfo, error) {\n\t// for some vfs we don't create intermediary folders so we cannot simply check\n\t// if virtualPath is a virtual folder. Allowing stat for hidden virtual folders\n\t// is by purpose.\n\tvfolders := c.User.GetVirtualFoldersInPath(path.Dir(virtualPath))\n\tif _, ok := vfolders[virtualPath]; ok {\n\t\treturn vfs.NewFileInfo(virtualPath, true, 0, time.Unix(0, 0), false), nil\n\t}\n\tif checkFilePatterns && virtualPath != \"/\" {\n\t\tok, policy := c.User.IsFileAllowed(virtualPath)\n\t\tif !ok && policy == sdk.DenyPolicyHide {\n\t\t\treturn nil, c.GetNotExistError()\n\t\t}\n\t}\n\n\tvar info os.FileInfo\n\n\tfs, fsPath, err := c.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif mode == 1 {\n\t\tinfo, err = fs.Lstat(c.getRealFsPath(fsPath))\n\t} else {\n\t\tinfo, err = fs.Stat(c.getRealFsPath(fsPath))\n\t}\n\tif err != nil {\n\t\tisNotExist := fs.IsNotExist(err)\n\t\tif isNotExist {\n\t\t\t// This is primarily useful for atomic storage backends, where files\n\t\t\t// become visible only after they are closed. However, since we may\n\t\t\t// be proxying (for example) an SFTP server backed by atomic\n\t\t\t// storage, and this search only inspects transfers active on the\n\t\t\t// current connection (typically just one), the check is inexpensive\n\t\t\t// and safe to perform unconditionally.\n\t\t\tif info, err := c.getInfoForOngoingUpload(fsPath); err == nil {\n\t\t\t\treturn info, nil\n\t\t\t}\n\t\t}\n\t\tif !isNotExist {\n\t\t\tc.Log(logger.LevelWarn, \"stat error for path %q: %+v\", virtualPath, err)\n\t\t}\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\tif convertResult && vfs.IsCryptOsFs(fs) {\n\t\tinfo = fs.(*vfs.CryptFs).ConvertFileInfo(info)\n\t}\n\treturn info, nil\n}\n\n// DoStat execute a Stat if mode = 0, Lstat if mode = 1\nfunc (c *BaseConnection) DoStat(virtualPath string, mode int, checkFilePatterns bool) (os.FileInfo, error) {\n\treturn c.doStatInternal(virtualPath, mode, checkFilePatterns, true)\n}\n\nfunc (c *BaseConnection) createDirIfMissing(name string) error {\n\t_, err := c.DoStat(name, 0, false)\n\tif c.IsNotExistError(err) {\n\t\treturn c.CreateDir(name, false)\n\t}\n\treturn err\n}\n\nfunc (c *BaseConnection) ignoreSetStat(fs vfs.Fs) bool {\n\tif Config.SetstatMode == 1 {\n\t\treturn true\n\t}\n\tif Config.SetstatMode == 2 && !vfs.IsLocalOrSFTPFs(fs) && !vfs.IsCryptOsFs(fs) {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (c *BaseConnection) handleChmod(fs vfs.Fs, fsPath, pathForPerms string, attributes *StatAttributes) error {\n\tif !c.User.HasPerm(dataprovider.PermChmod, pathForPerms) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif c.ignoreSetStat(fs) {\n\t\treturn nil\n\t}\n\tstartTime := time.Now()\n\tif err := fs.Chmod(c.getRealFsPath(fsPath), attributes.Mode); err != nil {\n\t\tc.Log(logger.LevelError, \"failed to chmod path %q, mode: %v, err: %+v\", fsPath, attributes.Mode.String(), err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\tlogger.CommandLog(chmodLogSender, fsPath, \"\", c.User.Username, attributes.Mode.String(), c.ID, c.protocol,\n\t\t-1, -1, \"\", \"\", \"\", -1, c.localAddr, c.remoteAddr, elapsed)\n\treturn nil\n}\n\nfunc (c *BaseConnection) handleChown(fs vfs.Fs, fsPath, pathForPerms string, attributes *StatAttributes) error {\n\tif !c.User.HasPerm(dataprovider.PermChown, pathForPerms) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif c.ignoreSetStat(fs) {\n\t\treturn nil\n\t}\n\tstartTime := time.Now()\n\tif err := fs.Chown(c.getRealFsPath(fsPath), attributes.UID, attributes.GID); err != nil {\n\t\tc.Log(logger.LevelError, \"failed to chown path %q, uid: %v, gid: %v, err: %+v\", fsPath, attributes.UID,\n\t\t\tattributes.GID, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\tlogger.CommandLog(chownLogSender, fsPath, \"\", c.User.Username, \"\", c.ID, c.protocol, attributes.UID, attributes.GID,\n\t\t\"\", \"\", \"\", -1, c.localAddr, c.remoteAddr, elapsed)\n\treturn nil\n}\n\nfunc (c *BaseConnection) handleChtimes(fs vfs.Fs, fsPath, pathForPerms string, attributes *StatAttributes) error {\n\tif !c.User.HasPerm(dataprovider.PermChtimes, pathForPerms) {\n\t\treturn c.GetPermissionDeniedError()\n\t}\n\tif Config.SetstatMode == 1 {\n\t\treturn nil\n\t}\n\tstartTime := time.Now()\n\tisUploading := c.setTimes(fsPath, attributes.Atime, attributes.Mtime)\n\tif err := fs.Chtimes(c.getRealFsPath(fsPath), attributes.Atime, attributes.Mtime, isUploading); err != nil {\n\t\tc.setTimes(fsPath, time.Time{}, time.Time{})\n\t\tif errors.Is(err, vfs.ErrVfsUnsupported) && Config.SetstatMode == 2 {\n\t\t\treturn nil\n\t\t}\n\t\tc.Log(logger.LevelError, \"failed to chtimes for path %q, access time: %v, modification time: %v, err: %+v\",\n\t\t\tfsPath, attributes.Atime, attributes.Mtime, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\taccessTimeString := attributes.Atime.Format(chtimesFormat)\n\tmodificationTimeString := attributes.Mtime.Format(chtimesFormat)\n\tlogger.CommandLog(chtimesLogSender, fsPath, \"\", c.User.Username, \"\", c.ID, c.protocol, -1, -1,\n\t\taccessTimeString, modificationTimeString, \"\", -1, c.localAddr, c.remoteAddr, elapsed)\n\treturn nil\n}\n\n// SetStat set StatAttributes for the specified fsPath\nfunc (c *BaseConnection) SetStat(virtualPath string, attributes *StatAttributes) error {\n\tif ok, policy := c.User.IsFileAllowed(virtualPath); !ok {\n\t\treturn c.GetErrorForDeniedFile(policy)\n\t}\n\tfs, fsPath, err := c.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpathForPerms := path.Dir(virtualPath)\n\n\tif attributes.Flags&StatAttrTimes != 0 {\n\t\tif err = c.handleChtimes(fs, fsPath, pathForPerms, attributes); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif attributes.Flags&StatAttrPerms != 0 {\n\t\tif err = c.handleChmod(fs, fsPath, pathForPerms, attributes); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif attributes.Flags&StatAttrUIDGID != 0 {\n\t\tif err = c.handleChown(fs, fsPath, pathForPerms, attributes); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif attributes.Flags&StatAttrSize != 0 {\n\t\tif !c.User.HasPerm(dataprovider.PermOverwrite, pathForPerms) {\n\t\t\treturn c.GetPermissionDeniedError()\n\t\t}\n\t\tstartTime := time.Now()\n\t\tif err = c.truncateFile(fs, fsPath, virtualPath, attributes.Size); err != nil {\n\t\t\tc.Log(logger.LevelError, \"failed to truncate path %q, size: %v, err: %+v\", fsPath, attributes.Size, err)\n\t\t\treturn c.GetFsError(fs, err)\n\t\t}\n\t\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\t\tlogger.CommandLog(truncateLogSender, fsPath, \"\", c.User.Username, \"\", c.ID, c.protocol, -1, -1, \"\", \"\",\n\t\t\t\"\", attributes.Size, c.localAddr, c.remoteAddr, elapsed)\n\t}\n\n\treturn nil\n}\n\nfunc (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, size int64) error {\n\t// check first if we have an open transfer for the given path and try to truncate the file already opened\n\t// if we found no transfer we truncate by path.\n\tvar initialSize int64\n\tvar err error\n\tinitialSize, err = c.truncateOpenHandle(fsPath, size)\n\tif err == errNoTransfer {\n\t\tc.Log(logger.LevelDebug, \"file path %q not found in active transfers, execute trucate by path\", fsPath)\n\t\tvar info os.FileInfo\n\t\tinfo, err = fs.Stat(fsPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tinitialSize = info.Size()\n\t\terr = fs.Truncate(fsPath, size)\n\t}\n\tif err == nil && vfs.HasTruncateSupport(fs) {\n\t\tsizeDiff := initialSize - size\n\t\tvfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))\n\t\tif err == nil {\n\t\t\tdataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -sizeDiff, false)\n\t\t} else {\n\t\t\tdataprovider.UpdateUserQuota(&c.User, 0, -sizeDiff, false) //nolint:errcheck\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs, sourcePath, targetPath,\n\tvirtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo,\n) error {\n\tif !c.User.HasPermissionsInside(virtualSourcePath) &&\n\t\t!c.User.HasPermissionsInside(virtualTargetPath) {\n\t\tif !c.isRenamePermitted(fsSrc, fsDst, sourcePath, targetPath, virtualSourcePath, virtualTargetPath, srcInfo) {\n\t\t\tc.Log(logger.LevelInfo, \"rename %q -> %q is not allowed, virtual destination path: %q\",\n\t\t\t\tsourcePath, targetPath, virtualTargetPath)\n\t\t\treturn c.GetPermissionDeniedError()\n\t\t}\n\t\t// if all rename permissions are granted we have finished, otherwise we have to walk\n\t\t// because we could have the rename dir permission but not the rename file and the dir to\n\t\t// rename could contain files\n\t\tif c.User.HasPermsRenameAll(path.Dir(virtualSourcePath)) && c.User.HasPermsRenameAll(path.Dir(virtualTargetPath)) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fsSrc.Walk(sourcePath, func(walkedPath string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn c.GetFsError(fsSrc, err)\n\t\t}\n\t\tif walkedPath != sourcePath && !vfs.IsRenameAtomic(fsSrc) && Config.RenameMode == 0 {\n\t\t\tc.Log(logger.LevelInfo, \"cannot rename non empty directory %q on this filesystem\", virtualSourcePath)\n\t\t\treturn c.GetOpUnsupportedError()\n\t\t}\n\t\tdstPath := strings.Replace(walkedPath, sourcePath, targetPath, 1)\n\t\tvirtualSrcPath := fsSrc.GetRelativePath(walkedPath)\n\t\tvirtualDstPath := fsDst.GetRelativePath(dstPath)\n\t\tif !c.isRenamePermitted(fsSrc, fsDst, walkedPath, dstPath, virtualSrcPath, virtualDstPath, info) {\n\t\t\tc.Log(logger.LevelInfo, \"rename %q -> %q is not allowed, virtual destination path: %q\",\n\t\t\t\twalkedPath, dstPath, virtualDstPath)\n\t\t\treturn c.GetPermissionDeniedError()\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (c *BaseConnection) hasRenamePerms(virtualSourcePath, virtualTargetPath string, fi os.FileInfo) bool {\n\tif c.User.HasPermsRenameAll(path.Dir(virtualSourcePath)) &&\n\t\tc.User.HasPermsRenameAll(path.Dir(virtualTargetPath)) {\n\t\treturn true\n\t}\n\tif fi == nil {\n\t\t// we don't know if this is a file or a directory and we don't have all the rename perms, return false\n\t\treturn false\n\t}\n\tif fi.IsDir() {\n\t\tperms := []string{\n\t\t\tdataprovider.PermRenameDirs,\n\t\t\tdataprovider.PermRename,\n\t\t}\n\t\treturn c.User.HasAnyPerm(perms, path.Dir(virtualSourcePath)) &&\n\t\t\tc.User.HasAnyPerm(perms, path.Dir(virtualTargetPath))\n\t}\n\t// file or symlink\n\tperms := []string{\n\t\tdataprovider.PermRenameFiles,\n\t\tdataprovider.PermRename,\n\t}\n\treturn c.User.HasAnyPerm(perms, path.Dir(virtualSourcePath)) &&\n\t\tc.User.HasAnyPerm(perms, path.Dir(virtualTargetPath))\n}\n\nfunc (c *BaseConnection) checkFolderRename(fsSrc, fsDst vfs.Fs, fsSourcePath, fsTargetPath, virtualSourcePath,\n\tvirtualTargetPath string, srcInfo os.FileInfo) error {\n\tif util.IsDirOverlapped(virtualSourcePath, virtualTargetPath, true, \"/\") {\n\t\tc.Log(logger.LevelDebug, \"renaming the folder %q->%q is not supported: nested folders\",\n\t\t\tvirtualSourcePath, virtualTargetPath)\n\t\treturn fmt.Errorf(\"nested rename %q => %q is not supported: %w\",\n\t\t\tvirtualSourcePath, virtualTargetPath, c.GetOpUnsupportedError())\n\t}\n\tif util.IsDirOverlapped(fsSourcePath, fsTargetPath, true, c.User.FsConfig.GetPathSeparator()) {\n\t\tc.Log(logger.LevelDebug, \"renaming the folder %q->%q is not supported: nested fs folders\",\n\t\t\tfsSourcePath, fsTargetPath)\n\t\treturn fmt.Errorf(\"nested fs rename %q => %q is not supported: %w\",\n\t\t\tfsSourcePath, fsTargetPath, c.GetOpUnsupportedError())\n\t}\n\tif c.User.HasVirtualFoldersInside(virtualSourcePath) {\n\t\tc.Log(logger.LevelDebug, \"renaming the folder %q is not supported: it has virtual folders inside it\",\n\t\t\tvirtualSourcePath)\n\t\treturn fmt.Errorf(\"folder %q has virtual folders inside it: %w\", virtualSourcePath, c.GetOpUnsupportedError())\n\t}\n\tif c.User.HasVirtualFoldersInside(virtualTargetPath) {\n\t\tc.Log(logger.LevelDebug, \"renaming the folder %q is not supported, the target %q has virtual folders inside it\",\n\t\t\tvirtualSourcePath, virtualTargetPath)\n\t\treturn fmt.Errorf(\"folder %q has virtual folders inside it: %w\", virtualTargetPath, c.GetOpUnsupportedError())\n\t}\n\tif err := c.checkRecursiveRenameDirPermissions(fsSrc, fsDst, fsSourcePath, fsTargetPath,\n\t\tvirtualSourcePath, virtualTargetPath, srcInfo); err != nil {\n\t\tc.Log(logger.LevelDebug, \"error checking recursive permissions before renaming %q: %+v\", fsSourcePath, err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fsTargetPath, virtualSourcePath,\n\tvirtualTargetPath string, srcInfo os.FileInfo,\n) bool {\n\tif !c.IsSameResource(virtualSourcePath, virtualTargetPath) {\n\t\tc.Log(logger.LevelInfo, \"rename %q->%q is not allowed: the paths must be on the same resource\",\n\t\t\tvirtualSourcePath, virtualTargetPath)\n\t\treturn false\n\t}\n\tif c.User.IsMappedPath(fsSourcePath) && vfs.IsLocalOrCryptoFs(fsSrc) {\n\t\tc.Log(logger.LevelWarn, \"renaming a directory mapped as virtual folder is not allowed: %q\", fsSourcePath)\n\t\treturn false\n\t}\n\tif c.User.IsMappedPath(fsTargetPath) && vfs.IsLocalOrCryptoFs(fsDst) {\n\t\tc.Log(logger.LevelWarn, \"renaming to a directory mapped as virtual folder is not allowed: %q\", fsTargetPath)\n\t\treturn false\n\t}\n\tif virtualSourcePath == \"/\" || virtualTargetPath == \"/\" || fsSrc.GetRelativePath(fsSourcePath) == \"/\" {\n\t\tc.Log(logger.LevelWarn, \"renaming root dir is not allowed\")\n\t\treturn false\n\t}\n\tif c.User.IsVirtualFolder(virtualSourcePath) || c.User.IsVirtualFolder(virtualTargetPath) {\n\t\tc.Log(logger.LevelWarn, \"renaming a virtual folder is not allowed\")\n\t\treturn false\n\t}\n\tisSrcAllowed, _ := c.User.IsFileAllowed(virtualSourcePath)\n\tisDstAllowed, _ := c.User.IsFileAllowed(virtualTargetPath)\n\tif !isSrcAllowed || !isDstAllowed {\n\t\tc.Log(logger.LevelDebug, \"renaming source: %q to target: %q not allowed\", virtualSourcePath,\n\t\t\tvirtualTargetPath)\n\t\treturn false\n\t}\n\treturn c.hasRenamePerms(virtualSourcePath, virtualTargetPath, srcInfo)\n}\n\nfunc (c *BaseConnection) hasSpaceForRename(fs vfs.Fs, virtualSourcePath, virtualTargetPath string, initialSize int64,\n\tsourcePath string, srcInfo os.FileInfo) bool {\n\tif dataprovider.GetQuotaTracking() == 0 {\n\t\treturn true\n\t}\n\tsourceFolder, errSrc := c.User.GetVirtualFolderForPath(path.Dir(virtualSourcePath))\n\tdstFolder, errDst := c.User.GetVirtualFolderForPath(path.Dir(virtualTargetPath))\n\tif errSrc != nil && errDst != nil {\n\t\t// rename inside the user home dir\n\t\treturn true\n\t}\n\tif errSrc == nil && errDst == nil {\n\t\t// rename between virtual folders\n\t\tif sourceFolder.Name == dstFolder.Name {\n\t\t\t// rename inside the same virtual folder\n\t\t\treturn true\n\t\t}\n\t}\n\tif errSrc != nil && dstFolder.IsIncludedInUserQuota() {\n\t\t// rename between user root dir and a virtual folder included in user quota\n\t\treturn true\n\t}\n\tif errDst != nil && sourceFolder.IsIncludedInUserQuota() {\n\t\t// rename between a virtual folder included in user quota and the user root dir\n\t\treturn true\n\t}\n\tquotaResult, _ := c.HasSpace(true, false, virtualTargetPath)\n\tif quotaResult.HasSpace && quotaResult.QuotaSize == 0 && quotaResult.QuotaFiles == 0 {\n\t\t// no quota restrictions\n\t\treturn true\n\t}\n\treturn c.hasSpaceForCrossRename(fs, quotaResult, initialSize, sourcePath, srcInfo)\n}\n\n// hasSpaceForCrossRename checks the quota after a rename between different folders\nfunc (c *BaseConnection) hasSpaceForCrossRename(fs vfs.Fs, quotaResult vfs.QuotaCheckResult, initialSize int64,\n\tsourcePath string, srcInfo os.FileInfo,\n) bool {\n\tif !quotaResult.HasSpace && initialSize == -1 {\n\t\t// we are over quota and this is not a file replace\n\t\treturn false\n\t}\n\tvar sizeDiff int64\n\tvar filesDiff int\n\tvar err error\n\tif srcInfo.Mode().IsRegular() {\n\t\tsizeDiff = srcInfo.Size()\n\t\tfilesDiff = 1\n\t\tif initialSize != -1 {\n\t\t\tsizeDiff -= initialSize\n\t\t\tfilesDiff = 0\n\t\t}\n\t} else if srcInfo.IsDir() {\n\t\tfilesDiff, sizeDiff, err = fs.GetDirSize(sourcePath)\n\t\tif err != nil {\n\t\t\tc.Log(logger.LevelError, \"cross rename denied, error getting size for directory %q: %v\", sourcePath, err)\n\t\t\treturn false\n\t\t}\n\t}\n\tif !quotaResult.HasSpace && initialSize != -1 {\n\t\t// we are over quota but we are overwriting an existing file so we check if the quota size after the rename is ok\n\t\tif quotaResult.QuotaSize == 0 {\n\t\t\treturn true\n\t\t}\n\t\tc.Log(logger.LevelDebug, \"cross rename overwrite, source %q, used size %d, size to add %d\",\n\t\t\tsourcePath, quotaResult.UsedSize, sizeDiff)\n\t\tquotaResult.UsedSize += sizeDiff\n\t\treturn quotaResult.GetRemainingSize() >= 0\n\t}\n\tif quotaResult.QuotaFiles > 0 {\n\t\tremainingFiles := quotaResult.GetRemainingFiles()\n\t\tc.Log(logger.LevelDebug, \"cross rename, source %q remaining file %d to add %d\", sourcePath,\n\t\t\tremainingFiles, filesDiff)\n\t\tif remainingFiles < filesDiff {\n\t\t\treturn false\n\t\t}\n\t}\n\tif quotaResult.QuotaSize > 0 {\n\t\tremainingSize := quotaResult.GetRemainingSize()\n\t\tc.Log(logger.LevelDebug, \"cross rename, source %q remaining size %d to add %d\", srcInfo.Name(),\n\t\t\tremainingSize, sizeDiff)\n\t\tif remainingSize < sizeDiff {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// GetMaxWriteSize returns the allowed size for an upload or an error\n// if no enough size is available for a resume/append\nfunc (c *BaseConnection) GetMaxWriteSize(quotaResult vfs.QuotaCheckResult, isResume bool, fileSize int64,\n\tisUploadResumeSupported bool,\n) (int64, error) {\n\tmaxWriteSize := quotaResult.GetRemainingSize()\n\n\tif isResume {\n\t\tif !isUploadResumeSupported {\n\t\t\treturn 0, c.GetOpUnsupportedError()\n\t\t}\n\t\tif c.User.Filters.MaxUploadFileSize > 0 && c.User.Filters.MaxUploadFileSize <= fileSize {\n\t\t\treturn 0, c.GetQuotaExceededError()\n\t\t}\n\t\tif c.User.Filters.MaxUploadFileSize > 0 {\n\t\t\tmaxUploadSize := c.User.Filters.MaxUploadFileSize - fileSize\n\t\t\tif maxUploadSize < maxWriteSize || maxWriteSize == 0 {\n\t\t\t\tmaxWriteSize = maxUploadSize\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif maxWriteSize > 0 {\n\t\t\tmaxWriteSize += fileSize\n\t\t}\n\t\tif c.User.Filters.MaxUploadFileSize > 0 && (c.User.Filters.MaxUploadFileSize < maxWriteSize || maxWriteSize == 0) {\n\t\t\tmaxWriteSize = c.User.Filters.MaxUploadFileSize\n\t\t}\n\t}\n\n\treturn maxWriteSize, nil\n}\n\n// GetTransferQuota returns the data transfers quota\nfunc (c *BaseConnection) GetTransferQuota() dataprovider.TransferQuota {\n\tresult, _, _ := c.checkUserQuota()\n\treturn result\n}\n\nfunc (c *BaseConnection) checkUserQuota() (dataprovider.TransferQuota, int, int64) {\n\tul, dl, total := c.User.GetDataTransferLimits()\n\tresult := dataprovider.TransferQuota{\n\t\tULSize:           ul,\n\t\tDLSize:           dl,\n\t\tTotalSize:        total,\n\t\tAllowedULSize:    0,\n\t\tAllowedDLSize:    0,\n\t\tAllowedTotalSize: 0,\n\t}\n\tif !c.User.HasTransferQuotaRestrictions() {\n\t\treturn result, -1, -1\n\t}\n\tusedFiles, usedSize, usedULSize, usedDLSize, err := dataprovider.GetUsedQuota(c.User.Username)\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error getting used quota for %q: %v\", c.User.Username, err)\n\t\tresult.AllowedTotalSize = -1\n\t\treturn result, -1, -1\n\t}\n\tif result.TotalSize > 0 {\n\t\tresult.AllowedTotalSize = result.TotalSize - (usedULSize + usedDLSize)\n\t}\n\tif result.ULSize > 0 {\n\t\tresult.AllowedULSize = result.ULSize - usedULSize\n\t}\n\tif result.DLSize > 0 {\n\t\tresult.AllowedDLSize = result.DLSize - usedDLSize\n\t}\n\n\treturn result, usedFiles, usedSize\n}\n\n// HasSpace checks user's quota usage\nfunc (c *BaseConnection) HasSpace(checkFiles, getUsage bool, requestPath string) (vfs.QuotaCheckResult,\n\tdataprovider.TransferQuota,\n) {\n\tresult := vfs.QuotaCheckResult{\n\t\tHasSpace:     true,\n\t\tAllowedSize:  0,\n\t\tAllowedFiles: 0,\n\t\tUsedSize:     0,\n\t\tUsedFiles:    0,\n\t\tQuotaSize:    0,\n\t\tQuotaFiles:   0,\n\t}\n\tif dataprovider.GetQuotaTracking() == 0 {\n\t\treturn result, dataprovider.TransferQuota{}\n\t}\n\ttransferQuota, usedFiles, usedSize := c.checkUserQuota()\n\n\tvar err error\n\tvar vfolder vfs.VirtualFolder\n\tvfolder, err = c.User.GetVirtualFolderForPath(path.Dir(requestPath))\n\tif err == nil && !vfolder.IsIncludedInUserQuota() {\n\t\tif vfolder.HasNoQuotaRestrictions(checkFiles) && !getUsage {\n\t\t\treturn result, transferQuota\n\t\t}\n\t\tresult.QuotaSize = vfolder.QuotaSize\n\t\tresult.QuotaFiles = vfolder.QuotaFiles\n\t\tresult.UsedFiles, result.UsedSize, err = dataprovider.GetUsedVirtualFolderQuota(vfolder.Name)\n\t} else {\n\t\tif c.User.HasNoQuotaRestrictions(checkFiles) && !getUsage {\n\t\t\treturn result, transferQuota\n\t\t}\n\t\tresult.QuotaSize = c.User.QuotaSize\n\t\tresult.QuotaFiles = c.User.QuotaFiles\n\t\tif usedSize == -1 {\n\t\t\tresult.UsedFiles, result.UsedSize, _, _, err = dataprovider.GetUsedQuota(c.User.Username)\n\t\t} else {\n\t\t\terr = nil\n\t\t\tresult.UsedFiles = usedFiles\n\t\t\tresult.UsedSize = usedSize\n\t\t}\n\t}\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error getting used quota for %q request path %q: %v\", c.User.Username, requestPath, err)\n\t\tresult.HasSpace = false\n\t\treturn result, transferQuota\n\t}\n\tresult.AllowedFiles = result.QuotaFiles - result.UsedFiles\n\tresult.AllowedSize = result.QuotaSize - result.UsedSize\n\tif (checkFiles && result.QuotaFiles > 0 && result.UsedFiles >= result.QuotaFiles) ||\n\t\t(result.QuotaSize > 0 && result.UsedSize >= result.QuotaSize) {\n\t\tc.Log(logger.LevelDebug, \"quota exceed for user %q, request path %q, num files: %d/%d, size: %d/%d check files: %t\",\n\t\t\tc.User.Username, requestPath, result.UsedFiles, result.QuotaFiles, result.UsedSize, result.QuotaSize, checkFiles)\n\t\tresult.HasSpace = false\n\t\treturn result, transferQuota\n\t}\n\treturn result, transferQuota\n}\n\n// IsSameResource returns true if source and target paths are on the same resource\nfunc (c *BaseConnection) IsSameResource(virtualSourcePath, virtualTargetPath string) bool {\n\tsourceFolder, errSrc := c.User.GetVirtualFolderForPath(virtualSourcePath)\n\tdstFolder, errDst := c.User.GetVirtualFolderForPath(virtualTargetPath)\n\tif errSrc != nil && errDst != nil {\n\t\treturn true\n\t}\n\tif errSrc == nil && errDst == nil {\n\t\tif sourceFolder.Name == dstFolder.Name {\n\t\t\treturn true\n\t\t}\n\t\t// we have different folders, check if they point to the same resource\n\t\treturn sourceFolder.FsConfig.IsSameResource(dstFolder.FsConfig)\n\t}\n\tif errSrc == nil {\n\t\treturn sourceFolder.FsConfig.IsSameResource(c.User.FsConfig)\n\t}\n\treturn dstFolder.FsConfig.IsSameResource(c.User.FsConfig)\n}\n\nfunc (c *BaseConnection) isCrossFoldersRequest(virtualSourcePath, virtualTargetPath string) bool {\n\tsourceFolder, errSrc := c.User.GetVirtualFolderForPath(virtualSourcePath)\n\tdstFolder, errDst := c.User.GetVirtualFolderForPath(virtualTargetPath)\n\tif errSrc != nil && errDst != nil {\n\t\treturn false\n\t}\n\tif errSrc == nil && errDst == nil {\n\t\treturn sourceFolder.Name != dstFolder.Name\n\t}\n\treturn true\n}\n\nfunc (c *BaseConnection) updateQuotaMoveBetweenVFolders(sourceFolder, dstFolder *vfs.VirtualFolder, initialSize,\n\tfilesSize int64, numFiles int) {\n\tif sourceFolder.Name == dstFolder.Name {\n\t\t// both files are inside the same virtual folder\n\t\tif initialSize != -1 {\n\t\t\tdataprovider.UpdateUserFolderQuota(dstFolder, &c.User, -numFiles, -initialSize, false)\n\t\t}\n\t\treturn\n\t}\n\t// files are inside different virtual folders\n\tdataprovider.UpdateUserFolderQuota(sourceFolder, &c.User, -numFiles, -filesSize, false)\n\tif initialSize == -1 {\n\t\tdataprovider.UpdateUserFolderQuota(dstFolder, &c.User, numFiles, filesSize, false)\n\t\treturn\n\t}\n\t// we cannot have a directory here, initialSize != -1 only for files\n\tdataprovider.UpdateUserFolderQuota(dstFolder, &c.User, 0, filesSize-initialSize, false)\n}\n\nfunc (c *BaseConnection) updateQuotaMoveFromVFolder(sourceFolder *vfs.VirtualFolder, initialSize, filesSize int64, numFiles int) {\n\t// move between a virtual folder and the user home dir\n\tdataprovider.UpdateUserFolderQuota(sourceFolder, &c.User, -numFiles, -filesSize, false)\n\tif initialSize == -1 {\n\t\tdataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck\n\t\treturn\n\t}\n\t// we cannot have a directory here, initialSize != -1 only for files\n\tdataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck\n}\n\nfunc (c *BaseConnection) updateQuotaMoveToVFolder(dstFolder *vfs.VirtualFolder, initialSize, filesSize int64, numFiles int) {\n\t// move between the user home dir and a virtual folder\n\tdataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck\n\tif initialSize == -1 {\n\t\tdataprovider.UpdateUserFolderQuota(dstFolder, &c.User, numFiles, filesSize, false)\n\t\treturn\n\t}\n\t// we cannot have a directory here, initialSize != -1 only for files\n\tdataprovider.UpdateUserFolderQuota(dstFolder, &c.User, 0, filesSize-initialSize, false)\n}\n\nfunc (c *BaseConnection) updateQuotaAfterRename(fs vfs.Fs, virtualSourcePath, virtualTargetPath, targetPath string,\n\tinitialSize int64, numFiles int, filesSize int64,\n) error {\n\tif dataprovider.GetQuotaTracking() == 0 {\n\t\treturn nil\n\t}\n\t// we don't allow to overwrite an existing directory so targetPath can be:\n\t// - a new file, a symlink is as a new file here\n\t// - a file overwriting an existing one\n\t// - a new directory\n\t// initialSize != -1 only when overwriting files\n\tsourceFolder, errSrc := c.User.GetVirtualFolderForPath(path.Dir(virtualSourcePath))\n\tdstFolder, errDst := c.User.GetVirtualFolderForPath(path.Dir(virtualTargetPath))\n\tif errSrc != nil && errDst != nil {\n\t\t// both files are contained inside the user home dir\n\t\tif initialSize != -1 {\n\t\t\t// we cannot have a directory here, we are overwriting an existing file\n\t\t\t// we need to subtract the size of the overwritten file from the user quota\n\t\t\tdataprovider.UpdateUserQuota(&c.User, -1, -initialSize, false) //nolint:errcheck\n\t\t}\n\t\treturn nil\n\t}\n\n\tif filesSize == -1 {\n\t\t// fs.Rename didn't return the affected files/sizes, we need to calculate them\n\t\tnumFiles = 1\n\t\tif fi, err := fs.Stat(targetPath); err == nil {\n\t\t\tif fi.Mode().IsDir() {\n\t\t\t\tnumFiles, filesSize, err = fs.GetDirSize(targetPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tc.Log(logger.LevelError, \"failed to update quota after rename, error scanning moved folder %q: %+v\",\n\t\t\t\t\t\ttargetPath, err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfilesSize = fi.Size()\n\t\t\t}\n\t\t} else {\n\t\t\tc.Log(logger.LevelError, \"failed to update quota after renaming, file %q stat error: %+v\", targetPath, err)\n\t\t\treturn err\n\t\t}\n\t\tc.Log(logger.LevelDebug, \"calculated renamed files: %d, size: %d bytes\", numFiles, filesSize)\n\t} else {\n\t\tc.Log(logger.LevelDebug, \"returned renamed files: %d, size: %d bytes\", numFiles, filesSize)\n\t}\n\tif errSrc == nil && errDst == nil {\n\t\tc.updateQuotaMoveBetweenVFolders(&sourceFolder, &dstFolder, initialSize, filesSize, numFiles)\n\t}\n\tif errSrc == nil && errDst != nil {\n\t\tc.updateQuotaMoveFromVFolder(&sourceFolder, initialSize, filesSize, numFiles)\n\t}\n\tif errSrc != nil && errDst == nil {\n\t\tc.updateQuotaMoveToVFolder(&dstFolder, initialSize, filesSize, numFiles)\n\t}\n\treturn nil\n}\n\n// IsNotExistError returns true if the specified fs error is not exist for the connection protocol\nfunc (c *BaseConnection) IsNotExistError(err error) bool {\n\tswitch c.protocol {\n\tcase ProtocolSFTP:\n\t\treturn errors.Is(err, sftp.ErrSSHFxNoSuchFile)\n\tcase ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolOIDC, ProtocolHTTPShare, ProtocolDataRetention:\n\t\treturn errors.Is(err, os.ErrNotExist)\n\tdefault:\n\t\treturn errors.Is(err, ErrNotExist)\n\t}\n}\n\n// GetErrorForDeniedFile return permission denied or not exist error based on the specified policy\nfunc (c *BaseConnection) GetErrorForDeniedFile(policy int) error {\n\tswitch policy {\n\tcase sdk.DenyPolicyHide:\n\t\treturn c.GetNotExistError()\n\tdefault:\n\t\treturn c.GetPermissionDeniedError()\n\t}\n}\n\n// GetPermissionDeniedError returns an appropriate permission denied error for the connection protocol\nfunc (c *BaseConnection) GetPermissionDeniedError() error {\n\treturn getPermissionDeniedError(c.protocol)\n}\n\n// GetNotExistError returns an appropriate not exist error for the connection protocol\nfunc (c *BaseConnection) GetNotExistError() error {\n\tswitch c.protocol {\n\tcase ProtocolSFTP:\n\t\treturn sftp.ErrSSHFxNoSuchFile\n\tcase ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolOIDC, ProtocolHTTPShare, ProtocolDataRetention:\n\t\treturn os.ErrNotExist\n\tdefault:\n\t\treturn ErrNotExist\n\t}\n}\n\n// GetOpUnsupportedError returns an appropriate operation not supported error for the connection protocol\nfunc (c *BaseConnection) GetOpUnsupportedError() error {\n\tswitch c.protocol {\n\tcase ProtocolSFTP:\n\t\treturn sftp.ErrSSHFxOpUnsupported\n\tdefault:\n\t\treturn ErrOpUnsupported\n\t}\n}\n\nfunc getQuotaExceededError(protocol string) error {\n\tswitch protocol {\n\tcase ProtocolSFTP:\n\t\treturn fmt.Errorf(\"%w: %w\", sftp.ErrSSHFxFailure, ErrQuotaExceeded)\n\tcase ProtocolFTP:\n\t\treturn ftpserver.ErrStorageExceeded\n\tdefault:\n\t\treturn ErrQuotaExceeded\n\t}\n}\n\nfunc getReadQuotaExceededError(protocol string) error {\n\tswitch protocol {\n\tcase ProtocolSFTP:\n\t\treturn fmt.Errorf(\"%w: %w\", sftp.ErrSSHFxFailure, ErrReadQuotaExceeded)\n\tdefault:\n\t\treturn ErrReadQuotaExceeded\n\t}\n}\n\n// GetQuotaExceededError returns an appropriate storage limit exceeded error for the connection protocol\nfunc (c *BaseConnection) GetQuotaExceededError() error {\n\treturn getQuotaExceededError(c.protocol)\n}\n\n// GetReadQuotaExceededError returns an appropriate read quota limit exceeded error for the connection protocol\nfunc (c *BaseConnection) GetReadQuotaExceededError() error {\n\treturn getReadQuotaExceededError(c.protocol)\n}\n\n// IsQuotaExceededError returns true if the given error is a quota exceeded error\nfunc (c *BaseConnection) IsQuotaExceededError(err error) bool {\n\tswitch c.protocol {\n\tcase ProtocolSFTP:\n\t\tif err == nil {\n\t\t\treturn false\n\t\t}\n\t\tif errors.Is(err, ErrQuotaExceeded) {\n\t\t\treturn true\n\t\t}\n\t\treturn errors.Is(err, sftp.ErrSSHFxFailure) && strings.Contains(err.Error(), ErrQuotaExceeded.Error())\n\tcase ProtocolFTP:\n\t\treturn errors.Is(err, ftpserver.ErrStorageExceeded) || errors.Is(err, ErrQuotaExceeded)\n\tdefault:\n\t\treturn errors.Is(err, ErrQuotaExceeded)\n\t}\n}\n\nfunc isSFTPGoError(err error) bool {\n\treturn errors.Is(err, ErrPermissionDenied) || errors.Is(err, ErrNotExist) || errors.Is(err, ErrOpUnsupported) ||\n\t\terrors.Is(err, ErrQuotaExceeded) || errors.Is(err, ErrReadQuotaExceeded) ||\n\t\terrors.Is(err, vfs.ErrStorageSizeUnavailable) || errors.Is(err, ErrShuttingDown)\n}\n\n// GetGenericError returns an appropriate generic error for the connection protocol\nfunc (c *BaseConnection) GetGenericError(err error) error {\n\tswitch c.protocol {\n\tcase ProtocolSFTP:\n\t\tif errors.Is(err, vfs.ErrStorageSizeUnavailable) || errors.Is(err, ErrOpUnsupported) || errors.Is(err, sftp.ErrSSHFxOpUnsupported) {\n\t\t\treturn fmt.Errorf(\"%w: %w\", sftp.ErrSSHFxOpUnsupported, err)\n\t\t}\n\t\tif isSFTPGoError(err) {\n\t\t\treturn fmt.Errorf(\"%w: %w\", sftp.ErrSSHFxFailure, err)\n\t\t}\n\t\tif err != nil {\n\t\t\tvar pathError *fs.PathError\n\t\t\tif errors.As(err, &pathError) {\n\t\t\t\tc.Log(logger.LevelError, \"generic path error: %+v\", pathError)\n\t\t\t\treturn fmt.Errorf(\"%w: %v %v\", sftp.ErrSSHFxFailure, pathError.Op, pathError.Err.Error())\n\t\t\t}\n\t\t\tc.Log(logger.LevelError, \"generic error: %+v\", err)\n\t\t}\n\t\treturn sftp.ErrSSHFxFailure\n\tdefault:\n\t\tif isSFTPGoError(err) {\n\t\t\treturn err\n\t\t}\n\t\tc.Log(logger.LevelError, \"generic error: %+v\", err)\n\t\treturn ErrGenericFailure\n\t}\n}\n\n// GetFsError converts a filesystem error to a protocol error\nfunc (c *BaseConnection) GetFsError(fs vfs.Fs, err error) error {\n\tif fs.IsNotExist(err) {\n\t\treturn c.GetNotExistError()\n\t} else if fs.IsPermission(err) {\n\t\treturn c.GetPermissionDeniedError()\n\t} else if fs.IsNotSupported(err) {\n\t\treturn c.GetOpUnsupportedError()\n\t} else if err != nil {\n\t\treturn c.GetGenericError(err)\n\t}\n\treturn nil\n}\n\nfunc (c *BaseConnection) getNotificationStatus(err error) int {\n\tif err == nil {\n\t\treturn 1\n\t}\n\tif c.IsQuotaExceededError(err) {\n\t\treturn 3\n\t}\n\treturn 2\n}\n\n// GetFsAndResolvedPath returns the fs and the fs path matching virtualPath\nfunc (c *BaseConnection) GetFsAndResolvedPath(virtualPath string) (vfs.Fs, string, error) {\n\tfs, err := c.User.GetFilesystemForPath(virtualPath, c.ID)\n\tif err != nil {\n\t\tif c.protocol == ProtocolWebDAV && strings.Contains(err.Error(), vfs.ErrSFTPLoop.Error()) {\n\t\t\t// if there is an SFTP loop we return a permission error, for WebDAV, so the problematic folder\n\t\t\t// will not be listed\n\t\t\treturn nil, \"\", util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message)\n\t\t}\n\t\treturn nil, \"\", c.GetGenericError(err)\n\t}\n\n\tif isShuttingDown.Load() {\n\t\treturn nil, \"\", c.GetFsError(fs, ErrShuttingDown)\n\t}\n\n\tfsPath, err := fs.ResolvePath(virtualPath)\n\tif err != nil {\n\t\treturn nil, \"\", c.GetFsError(fs, err)\n\t}\n\n\treturn fs, fsPath, nil\n}\n\n// DirListerAt defines a directory lister implementing the ListAt method.\ntype DirListerAt struct {\n\tvirtualPath string\n\tconn        *BaseConnection\n\tfs          vfs.Fs\n\tinfo        []os.FileInfo\n\tmu          sync.Mutex\n\tlister      vfs.DirLister\n}\n\n// Prepend adds the given os.FileInfo as first element of the internal cache\nfunc (l *DirListerAt) Prepend(fi os.FileInfo) {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\tl.info = slices.Insert(l.info, 0, fi)\n}\n\n// ListAt implements sftp.ListerAt\nfunc (l *DirListerAt) ListAt(f []os.FileInfo, _ int64) (int, error) {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\tif len(f) == 0 {\n\t\treturn 0, errors.New(\"invalid ListAt destination, zero size\")\n\t}\n\tif len(f) <= len(l.info) {\n\t\tfiles := make([]os.FileInfo, 0, len(f))\n\t\tfor idx := range l.info {\n\t\t\tfiles = append(files, l.info[idx])\n\t\t\tif len(files) == len(f) {\n\t\t\t\tl.info = l.info[idx+1:]\n\t\t\t\tn := copy(f, files)\n\t\t\t\treturn n, nil\n\t\t\t}\n\t\t}\n\t}\n\tlimit := len(f) - len(l.info)\n\tfiles, err := l.Next(limit)\n\tn := copy(f, files)\n\treturn n, err\n}\n\n// Next reads the directory and returns a slice of up to n FileInfo values.\nfunc (l *DirListerAt) Next(limit int) ([]os.FileInfo, error) {\n\tfor {\n\t\tfiles, err := l.lister.Next(limit)\n\t\tif err != nil && !errors.Is(err, io.EOF) {\n\t\t\tl.conn.Log(logger.LevelDebug, \"error retrieving directory entries: %+v\", err)\n\t\t\treturn files, l.conn.GetFsError(l.fs, err)\n\t\t}\n\t\tfiles = l.conn.User.FilterListDir(files, l.virtualPath)\n\t\tif len(l.info) > 0 {\n\t\t\tfiles = slices.Concat(l.info, files)\n\t\t\tl.info = nil\n\t\t}\n\t\tif err != nil || len(files) > 0 {\n\t\t\treturn files, err\n\t\t}\n\t}\n}\n\n// Close closes the DirListerAt\nfunc (l *DirListerAt) Close() error {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\treturn l.lister.Close()\n}\n\nfunc (l *DirListerAt) convertError(err error) error {\n\tif errors.Is(err, io.EOF) {\n\t\treturn nil\n\t}\n\treturn err\n}\n\nfunc getPermissionDeniedError(protocol string) error {\n\tswitch protocol {\n\tcase ProtocolSFTP:\n\t\treturn sftp.ErrSSHFxPermissionDenied\n\tcase ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolOIDC, ProtocolHTTPShare, ProtocolDataRetention:\n\t\treturn os.ErrPermission\n\tdefault:\n\t\treturn ErrPermissionDenied\n\t}\n}\n\nfunc keepConnectionAlive(c *BaseConnection, interval time.Duration) func() {\n\tvar timer *time.Timer\n\tvar closed atomic.Bool\n\n\ttask := func() {\n\t\tc.UpdateLastActivity()\n\n\t\tif !closed.Load() {\n\t\t\ttimer.Reset(interval)\n\t\t}\n\t}\n\n\ttimer = time.AfterFunc(interval, task)\n\n\treturn func() {\n\t\tclosed.Store(true)\n\t\ttimer.Stop()\n\t}\n}\n"
  },
  {
    "path": "internal/common/connection_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/sftp\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nvar (\n\terrWalkDir = errors.New(\"err walk dir\")\n)\n\n// MockOsFs mockable OsFs\ntype MockOsFs struct {\n\tvfs.Fs\n\thasVirtualFolders bool\n\tname              string\n\terr               error\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *MockOsFs) Name() string {\n\tif fs.name != \"\" {\n\t\treturn fs.name\n\t}\n\treturn \"mockOsFs\"\n}\n\n// HasVirtualFolders returns true if folders are emulated\nfunc (fs *MockOsFs) HasVirtualFolders() bool {\n\treturn fs.hasVirtualFolders\n}\n\nfunc (fs *MockOsFs) IsUploadResumeSupported() bool {\n\treturn !fs.hasVirtualFolders\n}\n\nfunc (fs *MockOsFs) Chtimes(_ string, _, _ time.Time, _ bool) error {\n\treturn vfs.ErrVfsUnsupported\n}\n\nfunc (fs *MockOsFs) Lstat(name string) (os.FileInfo, error) {\n\tif fs.err != nil {\n\t\treturn nil, fs.err\n\t}\n\treturn fs.Fs.Lstat(name)\n}\n\n// Walk returns a duplicate path for testing\nfunc (fs *MockOsFs) Walk(_ string, walkFn filepath.WalkFunc) error {\n\tif fs.err == errWalkDir {\n\t\twalkFn(\"fsdpath\", vfs.NewFileInfo(\"dpath\", true, 0, time.Now(), false), nil)        //nolint:errcheck\n\t\treturn walkFn(\"fsdpath\", vfs.NewFileInfo(\"dpath\", true, 0, time.Now(), false), nil) //nolint:errcheck\n\t}\n\twalkFn(\"fsfpath\", vfs.NewFileInfo(\"fpath\", false, 0, time.Now(), false), nil) //nolint:errcheck\n\treturn fs.err\n}\n\nfunc newMockOsFs(hasVirtualFolders bool, connectionID, rootDir, name string, err error) vfs.Fs {\n\treturn &MockOsFs{\n\t\tFs:                vfs.NewOsFs(connectionID, rootDir, \"\", nil),\n\t\tname:              name,\n\t\thasVirtualFolders: hasVirtualFolders,\n\t\terr:               err,\n\t}\n}\n\nfunc TestRemoveErrors(t *testing.T) {\n\tmappedPath := filepath.Join(os.TempDir(), \"map\")\n\thomePath := filepath.Join(os.TempDir(), \"home\")\n\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"remove_errors_user\",\n\t\t\tHomeDir:  homePath,\n\t\t},\n\t\tVirtualFolders: []vfs.VirtualFolder{\n\t\t\t{\n\t\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\t\tName:       filepath.Base(mappedPath),\n\t\t\t\t\tMappedPath: mappedPath,\n\t\t\t\t},\n\t\t\t\tVirtualPath: \"/virtualpath\",\n\t\t\t},\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconn := NewBaseConnection(\"\", ProtocolFTP, \"\", \"\", user)\n\terr := conn.IsRemoveDirAllowed(fs, mappedPath, \"/virtualpath1\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"permission denied\")\n\t}\n\terr = conn.RemoveFile(fs, filepath.Join(homePath, \"missing_file\"), \"/missing_file\",\n\t\tvfs.NewFileInfo(\"info\", false, 100, time.Now(), false))\n\tassert.Error(t, err)\n}\n\nfunc TestSetStatMode(t *testing.T) {\n\toldSetStatMode := Config.SetstatMode\n\tConfig.SetstatMode = 1\n\n\tfakePath := \"fake path\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: os.TempDir(),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfs := newMockOsFs(true, \"\", user.GetHomeDir(), \"\", nil)\n\tconn := NewBaseConnection(\"\", ProtocolWebDAV, \"\", \"\", user)\n\terr := conn.handleChmod(fs, fakePath, fakePath, nil)\n\tassert.NoError(t, err)\n\terr = conn.handleChown(fs, fakePath, fakePath, nil)\n\tassert.NoError(t, err)\n\terr = conn.handleChtimes(fs, fakePath, fakePath, nil)\n\tassert.NoError(t, err)\n\n\tConfig.SetstatMode = 2\n\terr = conn.handleChmod(fs, fakePath, fakePath, nil)\n\tassert.NoError(t, err)\n\terr = conn.handleChtimes(fs, fakePath, fakePath, &StatAttributes{\n\t\tAtime: time.Now(),\n\t\tMtime: time.Now(),\n\t})\n\tassert.NoError(t, err)\n\n\tConfig.SetstatMode = oldSetStatMode\n}\n\nfunc TestRecursiveRenameWalkError(t *testing.T) {\n\tfs := vfs.NewOsFs(\"\", filepath.Clean(os.TempDir()), \"\", nil)\n\tconn := NewBaseConnection(\"\", ProtocolWebDAV, \"\", \"\", dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermListItems, dataprovider.PermUpload,\n\t\t\t\t\tdataprovider.PermDownload, dataprovider.PermRenameDirs},\n\t\t\t},\n\t\t},\n\t})\n\terr := conn.checkRecursiveRenameDirPermissions(fs, fs, filepath.Join(os.TempDir(), \"/source\"),\n\t\tfilepath.Join(os.TempDir(), \"/target\"), \"/source\", \"/target\",\n\t\tvfs.NewFileInfo(\"source\", true, 0, time.Now(), false))\n\tassert.ErrorIs(t, err, os.ErrNotExist)\n\n\tfs = newMockOsFs(false, \"mockID\", filepath.Clean(os.TempDir()), \"S3Fs\", errWalkDir)\n\terr = conn.checkRecursiveRenameDirPermissions(fs, fs, filepath.Join(os.TempDir(), \"/source\"),\n\t\tfilepath.Join(os.TempDir(), \"/target\"), \"/source\", \"/target\",\n\t\tvfs.NewFileInfo(\"source\", true, 0, time.Now(), false))\n\tif assert.Error(t, err) {\n\t\tassert.Equal(t, err.Error(), conn.GetOpUnsupportedError().Error())\n\t}\n\n\tconn.User.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload,\n\t\tdataprovider.PermDownload, dataprovider.PermRenameFiles}\n\t// no dir rename permission, the quick check path returns permission error without walking\n\terr = conn.checkRecursiveRenameDirPermissions(fs, fs, filepath.Join(os.TempDir(), \"/source\"),\n\t\tfilepath.Join(os.TempDir(), \"/target\"), \"/source\", \"/target\",\n\t\tvfs.NewFileInfo(\"source\", true, 0, time.Now(), false))\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, conn.GetPermissionDeniedError().Error())\n\t}\n}\n\nfunc TestCrossRenameFsErrors(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconn := NewBaseConnection(\"\", ProtocolWebDAV, \"\", \"\", dataprovider.User{})\n\tdirPath := filepath.Join(os.TempDir(), \"d\")\n\terr := os.Mkdir(dirPath, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.Chmod(dirPath, 0001)\n\tassert.NoError(t, err)\n\tsrcInfo := vfs.NewFileInfo(filepath.Base(dirPath), true, 0, time.Now(), false)\n\tres := conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, dirPath, srcInfo)\n\tassert.False(t, res)\n\n\terr = os.Chmod(dirPath, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.Remove(dirPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestRenameVirtualFolders(t *testing.T) {\n\tvdir := \"/avdir\"\n\tu := dataprovider.User{}\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:       \"name\",\n\t\t\tMappedPath: \"mappedPath\",\n\t\t},\n\t\tVirtualPath: vdir,\n\t})\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconn := NewBaseConnection(\"\", ProtocolFTP, \"\", \"\", u)\n\tres := conn.isRenamePermitted(fs, fs, \"source\", \"target\", vdir, \"vdirtarget\", nil)\n\tassert.False(t, res)\n}\n\nfunc TestRenamePerms(t *testing.T) {\n\tsrc := \"source\"\n\ttarget := \"target\"\n\tsub := \"/sub\"\n\tsubTarget := sub + \"/target\"\n\tu := dataprovider.User{}\n\tu.Permissions = map[string][]string{}\n\tu.Permissions[\"/\"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermCreateSymlinks,\n\t\tdataprovider.PermDeleteFiles}\n\tconn := NewBaseConnection(\"\", ProtocolSFTP, \"\", \"\", u)\n\tassert.False(t, conn.hasRenamePerms(src, target, nil))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRename}\n\tassert.True(t, conn.hasRenamePerms(src, target, nil))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDeleteFiles,\n\t\tdataprovider.PermDeleteDirs}\n\tassert.False(t, conn.hasRenamePerms(src, target, nil))\n\n\tinfo := vfs.NewFileInfo(src, true, 0, time.Now(), false)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRenameFiles}\n\tassert.False(t, conn.hasRenamePerms(src, target, info))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRenameDirs}\n\tassert.True(t, conn.hasRenamePerms(src, target, info))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRename}\n\tassert.True(t, conn.hasRenamePerms(src, target, info))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDeleteDirs}\n\tassert.False(t, conn.hasRenamePerms(src, target, info))\n\t// test with different permissions between source and target\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRename}\n\tu.Permissions[sub] = []string{dataprovider.PermRenameFiles}\n\tassert.False(t, conn.hasRenamePerms(src, subTarget, info))\n\tu.Permissions[sub] = []string{dataprovider.PermRenameDirs}\n\tassert.True(t, conn.hasRenamePerms(src, subTarget, info))\n\t// test files\n\tinfo = vfs.NewFileInfo(src, false, 0, time.Now(), false)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRenameDirs}\n\tassert.False(t, conn.hasRenamePerms(src, target, info))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRenameFiles}\n\tassert.True(t, conn.hasRenamePerms(src, target, info))\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRename}\n\tassert.True(t, conn.hasRenamePerms(src, target, info))\n\t// test with different permissions between source and target\n\tu.Permissions[\"/\"] = []string{dataprovider.PermRename}\n\tu.Permissions[sub] = []string{dataprovider.PermRenameDirs}\n\tassert.False(t, conn.hasRenamePerms(src, subTarget, info))\n\tu.Permissions[sub] = []string{dataprovider.PermRenameFiles}\n\tassert.True(t, conn.hasRenamePerms(src, subTarget, info))\n}\n\nfunc TestRenameNestedFolders(t *testing.T) {\n\tu := dataprovider.User{}\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:       \"vfolder\",\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"f\"),\n\t\t},\n\t\tVirtualPath: \"/vdirs/f\",\n\t})\n\tconn := NewBaseConnection(\"\", ProtocolSFTP, \"\", \"\", u)\n\terr := conn.checkFolderRename(nil, nil, filepath.Clean(os.TempDir()), filepath.Join(os.TempDir(), \"subdir\"), \"/src\", \"/dst\", nil)\n\tassert.Error(t, err)\n\terr = conn.checkFolderRename(nil, nil, filepath.Join(os.TempDir(), \"subdir\"), filepath.Clean(os.TempDir()), \"/src\", \"/dst\", nil)\n\tassert.Error(t, err)\n\terr = conn.checkFolderRename(nil, nil, \"\", \"\", \"/src/sub\", \"/src\", nil)\n\tassert.Error(t, err)\n\terr = conn.checkFolderRename(nil, nil, filepath.Join(os.TempDir(), \"src\"), filepath.Join(os.TempDir(), \"vdirs\"), \"/src\", \"/vdirs\", nil)\n\tassert.Error(t, err)\n}\n\nfunc TestUpdateQuotaAfterRename(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: userTestUsername,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), \"home\"),\n\t\t},\n\t}\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath,\n\t\t},\n\t\tVirtualPath: \"/vdir1\",\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\terr := os.MkdirAll(user.GetHomeDir(), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tfs, err := user.GetFilesystem(\"id\")\n\tassert.NoError(t, err)\n\tc := NewBaseConnection(\"\", ProtocolSFTP, \"\", \"\", user)\n\trequest := sftp.NewRequest(\"Rename\", \"/testfile\")\n\tif runtime.GOOS != osWindows {\n\t\trequest.Filepath = \"/dir\"\n\t\trequest.Target = path.Join(\"/vdir\", \"dir\")\n\t\ttestDirPath := filepath.Join(mappedPath, \"dir\")\n\t\terr := os.MkdirAll(testDirPath, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(testDirPath, 0001)\n\t\tassert.NoError(t, err)\n\t\terr = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, testDirPath, 0, -1, -1)\n\t\tassert.Error(t, err)\n\t\terr = os.Chmod(testDirPath, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\ttestFile1 := \"/testfile1\"\n\trequest.Target = testFile1\n\trequest.Filepath = path.Join(\"/vdir\", \"file\")\n\terr = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, \"file\"), 0, -1, -1)\n\tassert.Error(t, err)\n\terr = os.WriteFile(filepath.Join(mappedPath, \"file\"), []byte(\"test content\"), os.ModePerm)\n\tassert.NoError(t, err)\n\trequest.Filepath = testFile1\n\trequest.Target = path.Join(\"/vdir\", \"file\")\n\terr = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, \"file\"), 12, -1, -1)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), \"testfile1\"), []byte(\"test content\"), os.ModePerm)\n\tassert.NoError(t, err)\n\trequest.Target = testFile1\n\trequest.Filepath = path.Join(\"/vdir\", \"file\")\n\terr = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, \"file\"), 12, -1, -1)\n\tassert.NoError(t, err)\n\trequest.Target = path.Join(\"/vdir1\", \"file\")\n\trequest.Filepath = path.Join(\"/vdir\", \"file\")\n\terr = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, \"file\"), 12, -1, -1)\n\tassert.NoError(t, err)\n\terr = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, \"file\"), 12, 1, 100)\n\tassert.NoError(t, err)\n\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestErrorsMapping(t *testing.T) {\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconn := NewBaseConnection(\"\", ProtocolSFTP, \"\", \"\", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})\n\tosErrorsProtocols := []string{ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare,\n\t\tProtocolDataRetention, ProtocolOIDC, protocolEventAction}\n\tfor _, protocol := range supportedProtocols {\n\t\tconn.SetProtocol(protocol)\n\t\terr := conn.GetFsError(fs, os.ErrNotExist)\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)\n\t\t} else if slices.Contains(osErrorsProtocols, protocol) {\n\t\t\tassert.EqualError(t, err, os.ErrNotExist.Error())\n\t\t} else {\n\t\t\tassert.EqualError(t, err, ErrNotExist.Error())\n\t\t}\n\t\terr = conn.GetFsError(fs, os.ErrPermission)\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.EqualError(t, err, sftp.ErrSSHFxPermissionDenied.Error())\n\t\t} else {\n\t\t\tassert.EqualError(t, err, ErrPermissionDenied.Error())\n\t\t}\n\t\terr = conn.GetFsError(fs, os.ErrClosed)\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxFailure)\n\t\t} else {\n\t\t\tassert.EqualError(t, err, ErrGenericFailure.Error())\n\t\t}\n\t\terr = conn.GetFsError(fs, ErrPermissionDenied)\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxFailure)\n\t\t} else {\n\t\t\tassert.EqualError(t, err, ErrPermissionDenied.Error())\n\t\t}\n\t\terr = conn.GetFsError(fs, vfs.ErrVfsUnsupported)\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error())\n\t\t} else {\n\t\t\tassert.EqualError(t, err, ErrOpUnsupported.Error())\n\t\t}\n\t\terr = conn.GetFsError(fs, vfs.ErrStorageSizeUnavailable)\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxOpUnsupported)\n\t\t\tassert.Contains(t, err.Error(), vfs.ErrStorageSizeUnavailable.Error())\n\t\t} else {\n\t\t\tassert.EqualError(t, err, vfs.ErrStorageSizeUnavailable.Error())\n\t\t}\n\t\terr = conn.GetQuotaExceededError()\n\t\tassert.True(t, conn.IsQuotaExceededError(err))\n\t\terr = conn.GetReadQuotaExceededError()\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxFailure)\n\t\t\tassert.Contains(t, err.Error(), ErrReadQuotaExceeded.Error())\n\t\t} else {\n\t\t\tassert.ErrorIs(t, err, ErrReadQuotaExceeded)\n\t\t}\n\t\terr = conn.GetNotExistError()\n\t\tassert.True(t, conn.IsNotExistError(err))\n\t\terr = conn.GetFsError(fs, nil)\n\t\tassert.NoError(t, err)\n\t\terr = conn.GetOpUnsupportedError()\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error())\n\t\t} else {\n\t\t\tassert.EqualError(t, err, ErrOpUnsupported.Error())\n\t\t}\n\t\terr = conn.GetFsError(fs, ErrShuttingDown)\n\t\tif protocol == ProtocolSFTP {\n\t\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxFailure)\n\t\t\tassert.Contains(t, err.Error(), ErrShuttingDown.Error())\n\t\t} else {\n\t\t\tassert.EqualError(t, err, ErrShuttingDown.Error())\n\t\t}\n\t}\n}\n\nfunc TestMaxWriteSize(t *testing.T) {\n\tpermissions := make(map[string][]string)\n\tpermissions[\"/\"] = []string{dataprovider.PermAny}\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:    userTestUsername,\n\t\t\tPermissions: permissions,\n\t\t\tHomeDir:     filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tfs, err := user.GetFilesystem(\"123\")\n\tassert.NoError(t, err)\n\tconn := NewBaseConnection(\"\", ProtocolFTP, \"\", \"\", user)\n\tquotaResult := vfs.QuotaCheckResult{\n\t\tHasSpace: true,\n\t}\n\tsize, err := conn.GetMaxWriteSize(quotaResult, false, 0, fs.IsUploadResumeSupported())\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), size)\n\n\tconn.User.Filters.MaxUploadFileSize = 100\n\tsize, err = conn.GetMaxWriteSize(quotaResult, false, 0, fs.IsUploadResumeSupported())\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(100), size)\n\n\tquotaResult.QuotaSize = 1000\n\tsize, err = conn.GetMaxWriteSize(quotaResult, false, 50, fs.IsUploadResumeSupported())\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(100), size)\n\n\tquotaResult.QuotaSize = 1000\n\tquotaResult.UsedSize = 990\n\tsize, err = conn.GetMaxWriteSize(quotaResult, false, 50, fs.IsUploadResumeSupported())\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(60), size)\n\n\tquotaResult.QuotaSize = 0\n\tquotaResult.UsedSize = 0\n\tsize, err = conn.GetMaxWriteSize(quotaResult, true, 100, fs.IsUploadResumeSupported())\n\tassert.True(t, conn.IsQuotaExceededError(err))\n\tassert.Equal(t, int64(0), size)\n\n\tsize, err = conn.GetMaxWriteSize(quotaResult, true, 10, fs.IsUploadResumeSupported())\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(90), size)\n\n\tfs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), \"\", nil)\n\tsize, err = conn.GetMaxWriteSize(quotaResult, true, 100, fs.IsUploadResumeSupported())\n\tassert.EqualError(t, err, ErrOpUnsupported.Error())\n\tassert.Equal(t, int64(0), size)\n}\n\nfunc TestCheckParentDirsErrors(t *testing.T) {\n\tpermissions := make(map[string][]string)\n\tpermissions[\"/\"] = []string{dataprovider.PermAny}\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:    userTestUsername,\n\t\t\tPermissions: permissions,\n\t\t\tHomeDir:     filepath.Clean(os.TempDir()),\n\t\t},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t},\n\t}\n\tc := NewBaseConnection(xid.New().String(), ProtocolSFTP, \"\", \"\", user)\n\terr := c.CheckParentDirs(\"/a/dir\")\n\tassert.Error(t, err)\n\n\tuser.FsConfig.Provider = sdk.LocalFilesystemProvider\n\tuser.VirtualFolders = nil\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\t},\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t})\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Clean(os.TempDir()),\n\t\t},\n\t\tVirtualPath: \"/vdir/sub\",\n\t})\n\tc = NewBaseConnection(xid.New().String(), ProtocolSFTP, \"\", \"\", user)\n\terr = c.CheckParentDirs(\"/vdir/sub/dir\")\n\tassert.Error(t, err)\n\n\tuser = dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:    userTestUsername,\n\t\t\tPermissions: permissions,\n\t\t\tHomeDir:     filepath.Clean(os.TempDir()),\n\t\t},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.S3FilesystemProvider,\n\t\t\tS3Config: vfs.S3FsConfig{\n\t\t\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\t\t\tBucket:    \"buck\",\n\t\t\t\t\tRegion:    \"us-east-1\",\n\t\t\t\t\tAccessKey: \"key\",\n\t\t\t\t},\n\t\t\t\tAccessSecret: kms.NewPlainSecret(\"s3secret\"),\n\t\t\t},\n\t\t},\n\t}\n\tc = NewBaseConnection(xid.New().String(), ProtocolSFTP, \"\", \"\", user)\n\terr = c.CheckParentDirs(\"/a/dir\")\n\tassert.NoError(t, err)\n\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Clean(os.TempDir()),\n\t\t},\n\t\tVirtualPath: \"/local/dir\",\n\t})\n\n\tc = NewBaseConnection(xid.New().String(), ProtocolSFTP, \"\", \"\", user)\n\terr = c.CheckParentDirs(\"/local/dir/sub-dir\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(filepath.Join(os.TempDir(), \"sub-dir\"))\n\tassert.NoError(t, err)\n}\n\nfunc TestErrorResolvePath(t *testing.T) {\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: filepath.Join(os.TempDir(), \"u\"),\n\t\t\tStatus:  1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"test\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(\"invalid JSON for credentials\")\n\tu.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName:       \"f\",\n\t\t\t\tMappedPath: filepath.Join(os.TempDir(), \"f\"),\n\t\t\t},\n\t\t\tVirtualPath: \"/f\",\n\t\t},\n\t}\n\n\tconn := NewBaseConnection(\"\", ProtocolSFTP, \"\", \"\", u)\n\terr := conn.doRecursiveRemoveDirEntry(\"/vpath\", nil, 0)\n\tassert.Error(t, err)\n\terr = conn.doRecursiveRemove(nil, \"/fspath\", \"/vpath\", vfs.NewFileInfo(\"vpath\", true, 0, time.Now(), false), 2000)\n\tassert.Error(t, err, util.ErrRecursionTooDeep)\n\terr = conn.doRecursiveCopy(\"/src\", \"/dst\", vfs.NewFileInfo(\"src\", true, 0, time.Now(), false), false, 2000)\n\tassert.Error(t, err, util.ErrRecursionTooDeep)\n\terr = conn.checkCopy(vfs.NewFileInfo(\"name\", true, 0, time.Unix(0, 0), false), nil, \"/source\", \"/target\")\n\tassert.Error(t, err)\n\tsourceFile := filepath.Join(os.TempDir(), \"f\", \"source\")\n\terr = os.MkdirAll(filepath.Dir(sourceFile), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(sourceFile, []byte(\"\"), 0666)\n\tassert.NoError(t, err)\n\terr = conn.checkCopy(vfs.NewFileInfo(\"name\", true, 0, time.Unix(0, 0), false), nil, \"/f/source\", \"/target\")\n\tassert.Error(t, err)\n\terr = conn.checkCopy(vfs.NewFileInfo(\"source\", false, 0, time.Unix(0, 0), false), vfs.NewFileInfo(\"target\", true, 0, time.Unix(0, 0), false), \"/f/source\", \"/f/target\")\n\tassert.Error(t, err)\n\terr = os.RemoveAll(filepath.Dir(sourceFile))\n\tassert.NoError(t, err)\n}\n\nfunc TestConnectionKeepAlive(t *testing.T) {\n\tconn := NewBaseConnection(\"\", ProtocolWebDAV, \"\", \"\", dataprovider.User{})\n\tlastActivity := conn.GetLastActivity()\n\n\tstop := keepConnectionAlive(conn, 50*time.Millisecond)\n\tdefer stop()\n\n\ttime.Sleep(200 * time.Millisecond)\n\tassert.Greater(t, conn.GetLastActivity(), lastActivity)\n}\n\nfunc TestFsFileCopier(t *testing.T) {\n\tfs := vfs.Fs(&vfs.AzureBlobFs{})\n\t_, ok := fs.(vfs.FsFileCopier)\n\tassert.True(t, ok)\n\tfs = vfs.Fs(&vfs.OsFs{})\n\t_, ok = fs.(vfs.FsFileCopier)\n\tassert.False(t, ok)\n\tfs = vfs.Fs(&vfs.SFTPFs{})\n\t_, ok = fs.(vfs.FsFileCopier)\n\tassert.False(t, ok)\n\tfs = vfs.Fs(&vfs.GCSFs{})\n\t_, ok = fs.(vfs.FsFileCopier)\n\tassert.True(t, ok)\n\tfs = vfs.Fs(&vfs.S3Fs{})\n\t_, ok = fs.(vfs.FsFileCopier)\n\tassert.True(t, ok)\n}\n\nfunc TestFilePatterns(t *testing.T) {\n\tfilters := dataprovider.UserFilters{\n\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\tFilePatterns: []sdk.PatternsFilter{\n\t\t\t\t{\n\t\t\t\t\tPath:            \"/dir1\",\n\t\t\t\t\tDenyPolicy:      sdk.DenyPolicyDefault,\n\t\t\t\t\tAllowedPatterns: []string{\"*.jpg\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPath:            \"/dir2\",\n\t\t\t\t\tDenyPolicy:      sdk.DenyPolicyHide,\n\t\t\t\t\tAllowedPatterns: []string{\"*.jpg\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPath:           \"/dir3\",\n\t\t\t\t\tDenyPolicy:     sdk.DenyPolicyDefault,\n\t\t\t\t\tDeniedPatterns: []string{\"*.jpg\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPath:           \"/dir4\",\n\t\t\t\t\tDenyPolicy:     sdk.DenyPolicyHide,\n\t\t\t\t\tDeniedPatterns: []string{\"*\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tvirtualFolders := []vfs.VirtualFolder{\n\t\t{\n\t\t\tVirtualPath: \"/dir1/vdir1\",\n\t\t},\n\t\t{\n\t\t\tVirtualPath: \"/dir1/vdir2\",\n\t\t},\n\t\t{\n\t\t\tVirtualPath: \"/dir1/vdir3\",\n\t\t},\n\t\t{\n\t\t\tVirtualPath: \"/dir2/vdir1\",\n\t\t},\n\t\t{\n\t\t\tVirtualPath: \"/dir2/vdir2\",\n\t\t},\n\t\t{\n\t\t\tVirtualPath: \"/dir2/vdir3.jpg\",\n\t\t},\n\t}\n\tuser := dataprovider.User{\n\t\tFilters:        filters,\n\t\tVirtualFolders: virtualFolders,\n\t}\n\n\tgetFilteredInfo := func(dirContents []os.FileInfo, virtualPath string) []os.FileInfo {\n\t\tresult := user.FilterListDir(dirContents, virtualPath)\n\t\tresult = append(result, user.GetVirtualFoldersInfo(virtualPath)...)\n\t\treturn result\n\t}\n\n\tdirContents := []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\t// dirContents are modified in place, we need to redefine them each time\n\tfiltered := getFilteredInfo(dirContents, \"/dir1\")\n\tassert.Len(t, filtered, 5)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir1/vdir1\")\n\tassert.Len(t, filtered, 2)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir2/vdir2\")\n\trequire.Len(t, filtered, 1)\n\tassert.Equal(t, \"file1.jpg\", filtered[0].Name())\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir2/vdir2/sub\")\n\trequire.Len(t, filtered, 1)\n\tassert.Equal(t, \"file1.jpg\", filtered[0].Name())\n\n\tres, _ := user.IsFileAllowed(\"/dir1/vdir1/file.txt\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir1/vdir1/sub/file.txt\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir1/vdir1/file.jpg\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir1/vdir1/sub/file.jpg\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/file.jpg\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/dir1/file.jpg\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/dir1/sub/file.jpg\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir4/file.jpg\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir4/dir1/sub/file.jpg\")\n\tassert.False(t, res)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir4\")\n\trequire.Len(t, filtered, 0)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir4/vdir2/sub\")\n\trequire.Len(t, filtered, 0)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\n\tfiltered = getFilteredInfo(dirContents, \"/dir2\")\n\tassert.Len(t, filtered, 2)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\n\tfiltered = getFilteredInfo(dirContents, \"/dir4\")\n\tassert.Len(t, filtered, 0)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t}\n\n\tfiltered = getFilteredInfo(dirContents, \"/dir4/sub\")\n\tassert.Len(t, filtered, 0)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"vdir3.jpg\", false, 123, time.Now(), false),\n\t}\n\n\tfiltered = getFilteredInfo(dirContents, \"/dir1\")\n\tassert.Len(t, filtered, 5)\n\n\tfiltered = getFilteredInfo(dirContents, \"/dir2\")\n\tif assert.Len(t, filtered, 1) {\n\t\tassert.True(t, filtered[0].IsDir())\n\t}\n\n\tuser.VirtualFolders = nil\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"vdir3.jpg\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir1\")\n\tassert.Len(t, filtered, 2)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"vdir3.jpg\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir2\")\n\tif assert.Len(t, filtered, 1) {\n\t\tassert.False(t, filtered[0].IsDir())\n\t}\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"vdir3.jpg\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir2\")\n\tif assert.Len(t, filtered, 2) {\n\t\tassert.False(t, filtered[0].IsDir())\n\t\tassert.False(t, filtered[1].IsDir())\n\t}\n\n\tuser.VirtualFolders = virtualFolders\n\tuser.Filters = filters\n\tfiltered = getFilteredInfo(nil, \"/dir1\")\n\tassert.Len(t, filtered, 3)\n\tfiltered = getFilteredInfo(nil, \"/dir2\")\n\tassert.Len(t, filtered, 1)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jPg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"vdir3.jpg\", false, 456, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir2\")\n\tassert.Len(t, filtered, 2)\n\n\tuser = dataprovider.User{\n\t\tFilters: dataprovider.UserFilters{\n\t\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\t\tFilePatterns: []sdk.PatternsFilter{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:            \"/dir3\",\n\t\t\t\t\t\tAllowedPatterns: []string{\"ic35\"},\n\t\t\t\t\t\tDeniedPatterns:  []string{\"*\"},\n\t\t\t\t\t\tDenyPolicy:      sdk.DenyPolicyHide,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"vdir3.jpg\", false, 456, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3\")\n\tassert.Len(t, filtered, 0)\n\n\tdirContents = nil\n\tfor i := 0; i < 100; i++ {\n\t\tdirContents = append(dirContents, vfs.NewFileInfo(fmt.Sprintf(\"ic%02d\", i), i%2 == 0, int64(i), time.Now(), false))\n\t}\n\tdirContents = append(dirContents, vfs.NewFileInfo(\"ic350\", false, 123, time.Now(), false))\n\tdirContents = append(dirContents, vfs.NewFileInfo(\".ic35\", false, 123, time.Now(), false))\n\tdirContents = append(dirContents, vfs.NewFileInfo(\"ic35.\", false, 123, time.Now(), false))\n\tdirContents = append(dirContents, vfs.NewFileInfo(\"*ic35\", false, 123, time.Now(), false))\n\tdirContents = append(dirContents, vfs.NewFileInfo(\"ic35*\", false, 123, time.Now(), false))\n\tdirContents = append(dirContents, vfs.NewFileInfo(\"ic35.*\", false, 123, time.Now(), false))\n\tdirContents = append(dirContents, vfs.NewFileInfo(\"file.jpg\", false, 123, time.Now(), false))\n\n\tfiltered = getFilteredInfo(dirContents, \"/dir3\")\n\trequire.Len(t, filtered, 1)\n\tassert.Equal(t, \"ic35\", filtered[0].Name())\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic36\")\n\trequire.Len(t, filtered, 0)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic35\")\n\trequire.Len(t, filtered, 3)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic35/sub\")\n\trequire.Len(t, filtered, 3)\n\n\tres, _ = user.IsFileAllowed(\"/dir3/file.txt\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35a\")\n\tassert.False(t, res)\n\tres, policy := user.IsFileAllowed(\"/dir3/ic35a/file\")\n\tassert.False(t, res)\n\tassert.Equal(t, sdk.DenyPolicyHide, policy)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/file.jpg\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/file.txt\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub/file.txt\")\n\tassert.True(t, res)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic35/sub\")\n\trequire.Len(t, filtered, 3)\n\n\tuser.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{\n\t\tPath:            \"/dir3/ic35/sub1\",\n\t\tAllowedPatterns: []string{\"*.jpg\"},\n\t\tDenyPolicy:      sdk.DenyPolicyDefault,\n\t})\n\tuser.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{\n\t\tPath:           \"/dir3/ic35/sub2\",\n\t\tDeniedPatterns: []string{\"*.jpg\"},\n\t\tDenyPolicy:     sdk.DenyPolicyHide,\n\t})\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic35/sub1\")\n\trequire.Len(t, filtered, 3)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic35/sub2\")\n\trequire.Len(t, filtered, 2)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic35/sub2/sub1\")\n\trequire.Len(t, filtered, 2)\n\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/file.jpg\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/file.txt\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub/dir/file.txt\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub/dir/file.jpg\")\n\tassert.True(t, res)\n\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub1/file.jpg\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub1/file.txt\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub1/sub/file.jpg\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub1/sub2/file.txt\")\n\tassert.False(t, res)\n\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub2/file.jpg\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub2/file.txt\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub2/sub/file.jpg\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub2/sub1/file.txt\")\n\tassert.True(t, res)\n\n\tuser.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{\n\t\tPath:           \"/dir3/ic35\",\n\t\tDeniedPatterns: []string{\"*.txt\"},\n\t\tDenyPolicy:     sdk.DenyPolicyHide,\n\t})\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/file.jpg\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/file.txt\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/adir/sub/file.jpg\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/adir/file.txt\")\n\tassert.False(t, res)\n\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub2/file.jpg\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub2/file.txt\")\n\tassert.True(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub2/sub/file.jpg\")\n\tassert.False(t, res)\n\tres, _ = user.IsFileAllowed(\"/dir3/ic35/sub2/sub1/file.txt\")\n\tassert.True(t, res)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic35\")\n\trequire.Len(t, filtered, 1)\n\n\tdirContents = []os.FileInfo{\n\t\tvfs.NewFileInfo(\"file1.jpg\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file1.txt\", false, 123, time.Now(), false),\n\t\tvfs.NewFileInfo(\"file2.txt\", false, 123, time.Now(), false),\n\t}\n\tfiltered = getFilteredInfo(dirContents, \"/dir3/ic35/abc\")\n\trequire.Len(t, filtered, 1)\n}\n\nfunc TestStatForOngoingTransfers(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: xid.New().String(),\n\t\t\tPassword: xid.New().String(),\n\t\t\tHomeDir:  filepath.Clean(os.TempDir()),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {\"*\"},\n\t\t\t},\n\t\t},\n\t}\n\tfileName := \"file.txt\"\n\tconn := NewBaseConnection(xid.New().String(), ProtocolSFTP, \"\", \"\", user)\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\ttr := NewBaseTransfer(nil, conn, nil, filepath.Join(os.TempDir(), fileName), filepath.Join(os.TempDir(), fileName),\n\t\tfileName, TransferUpload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\t_, err := conn.DoStat(\"/file.txt\", 0, false)\n\tassert.NoError(t, err)\n\terr = tr.Close()\n\tassert.NoError(t, err)\n\ttr = NewBaseTransfer(nil, conn, nil, filepath.Join(os.TempDir(), fileName), filepath.Join(os.TempDir(), fileName),\n\t\tfileName, TransferDownload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\t_, err = conn.DoStat(\"/file.txt\", 0, false)\n\tassert.Error(t, err)\n\terr = tr.Close()\n\tassert.NoError(t, err)\n\terr = conn.CloseFS()\n\tassert.NoError(t, err)\n}\n\nfunc TestListerAt(t *testing.T) {\n\tdir := t.TempDir()\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"u\",\n\t\t\tPassword: \"p\",\n\t\t\tHomeDir:  dir,\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {\"*\"},\n\t\t\t},\n\t\t},\n\t}\n\tconn := NewBaseConnection(xid.New().String(), ProtocolSFTP, \"\", \"\", user)\n\tlister, err := conn.ListDir(\"/\")\n\trequire.NoError(t, err)\n\tfiles, err := lister.Next(1)\n\trequire.ErrorIs(t, err, io.EOF)\n\trequire.Len(t, files, 0)\n\terr = lister.Close()\n\trequire.NoError(t, err)\n\n\tconn.User.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tVirtualPath: \"p1\",\n\t\t},\n\t\t{\n\t\t\tVirtualPath: \"p2\",\n\t\t},\n\t\t{\n\t\t\tVirtualPath: \"p3\",\n\t\t},\n\t}\n\tlister, err = conn.ListDir(\"/\")\n\trequire.NoError(t, err)\n\tfiles, err = lister.Next(2)\n\t// virtual directories exceeds the limit\n\trequire.ErrorIs(t, err, io.EOF)\n\trequire.Len(t, files, 3)\n\tfiles, err = lister.Next(2)\n\trequire.ErrorIs(t, err, io.EOF)\n\trequire.Len(t, files, 0)\n\t_, err = lister.Next(-1)\n\trequire.ErrorContains(t, err, conn.GetGenericError(err).Error())\n\terr = lister.Close()\n\trequire.NoError(t, err)\n\n\tlister, err = conn.ListDir(\"/\")\n\trequire.NoError(t, err)\n\t_, err = lister.ListAt(nil, 0)\n\trequire.ErrorContains(t, err, \"zero size\")\n\terr = lister.Close()\n\trequire.NoError(t, err)\n\n\tfor i := 0; i < 100; i++ {\n\t\tf, err := os.Create(filepath.Join(dir, strconv.Itoa(i)))\n\t\trequire.NoError(t, err)\n\t\terr = f.Close()\n\t\trequire.NoError(t, err)\n\t}\n\tlister, err = conn.ListDir(\"/\")\n\trequire.NoError(t, err)\n\tfiles = make([]os.FileInfo, 18)\n\tn, err := lister.ListAt(files, 0)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 18, n)\n\tn, err = lister.ListAt(files, 0)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 18, n)\n\tfiles = make([]os.FileInfo, 100)\n\tn, err = lister.ListAt(files, 0)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 64+3, n)\n\tn, err = lister.ListAt(files, 0)\n\trequire.ErrorIs(t, err, io.EOF)\n\trequire.Equal(t, 0, n)\n\tn, err = lister.ListAt(files, 0)\n\trequire.ErrorIs(t, err, io.EOF)\n\trequire.Equal(t, 0, n)\n\terr = lister.Close()\n\trequire.NoError(t, err)\n\tn, err = lister.ListAt(files, 0)\n\tassert.Error(t, err)\n\tassert.NotErrorIs(t, err, io.EOF)\n\trequire.Equal(t, 0, n)\n\tlister, err = conn.ListDir(\"/\")\n\trequire.NoError(t, err)\n\tlister.Prepend(vfs.NewFileInfo(\"..\", true, 0, time.Unix(0, 0), false))\n\tlister.Prepend(vfs.NewFileInfo(\".\", true, 0, time.Unix(0, 0), false))\n\tfiles = make([]os.FileInfo, 1)\n\tn, err = lister.ListAt(files, 0)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, n)\n\tassert.Equal(t, \".\", files[0].Name())\n\tfiles = make([]os.FileInfo, 2)\n\tn, err = lister.ListAt(files, 0)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 2, n)\n\tassert.Equal(t, \"..\", files[0].Name())\n\tvfolders := []string{files[1].Name()}\n\tfiles = make([]os.FileInfo, 200)\n\tn, err = lister.ListAt(files, 0)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 102, n)\n\tvfolders = append(vfolders, files[0].Name())\n\tvfolders = append(vfolders, files[1].Name())\n\tassert.Contains(t, vfolders, \"p1\")\n\tassert.Contains(t, vfolders, \"p2\")\n\tassert.Contains(t, vfolders, \"p3\")\n\terr = lister.Close()\n\trequire.NoError(t, err)\n}\n\nfunc TestGetFsAndResolvedPath(t *testing.T) {\n\thomeDir := filepath.Join(os.TempDir(), \"home_test\")\n\tlocalVdir := filepath.Join(os.TempDir(), \"local_mount_test\")\n\n\terr := os.MkdirAll(homeDir, 0777)\n\trequire.NoError(t, err)\n\terr = os.MkdirAll(localVdir, 0777)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(homeDir)\n\t\tos.RemoveAll(localVdir)\n\t})\n\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: xid.New().String(),\n\t\t\tStatus:   1,\n\t\t\tHomeDir:  homeDir,\n\t\t},\n\t\tVirtualFolders: []vfs.VirtualFolder{\n\t\t\t{\n\t\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\t\tName:       \"s3\",\n\t\t\t\t\tMappedPath: \"\",\n\t\t\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\t\t\tProvider: sdk.S3FilesystemProvider,\n\t\t\t\t\t\tS3Config: vfs.S3FsConfig{\n\t\t\t\t\t\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\t\t\t\t\t\tBucket: \"my-test-bucket\",\n\t\t\t\t\t\t\t\tRegion: \"us-east-1\",\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\tVirtualPath: \"/s3\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\t\tName:       \"local\",\n\t\t\t\t\tMappedPath: localVdir,\n\t\t\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\t\t\tProvider: sdk.LocalFilesystemProvider,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tVirtualPath: \"/local\",\n\t\t\t},\n\t\t},\n\t}\n\n\tconn := NewBaseConnection(xid.New().String(), ProtocolSFTP, \"\", \"\", user)\n\n\ttests := []struct {\n\t\tname                 string\n\t\tinputVirtualPath     string\n\t\texpectedFsType       string\n\t\texpectedPhyPath      string // The resolved path on the target FS\n\t\texpectedRelativePath string\n\t}{\n\t\t{\n\t\t\tname:                 \"Root File\",\n\t\t\tinputVirtualPath:     \"/file.txt\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(homeDir, \"file.txt\"),\n\t\t\texpectedRelativePath: \"/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Standard S3 File\",\n\t\t\tinputVirtualPath:     \"/s3/image.png\",\n\t\t\texpectedFsType:       \"S3Fs\",\n\t\t\texpectedPhyPath:      \"image.png\",\n\t\t\texpectedRelativePath: \"/s3/image.png\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Standard Local Mount File\",\n\t\t\tinputVirtualPath:     \"/local/config.json\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(localVdir, \"config.json\"),\n\t\t\texpectedRelativePath: \"/local/config.json\",\n\t\t},\n\n\t\t{\n\t\t\tname:                 \"Backslash Separator -> Should hit S3\",\n\t\t\tinputVirtualPath:     \"\\\\s3\\\\doc.txt\",\n\t\t\texpectedFsType:       \"S3Fs\",\n\t\t\texpectedPhyPath:      \"doc.txt\",\n\t\t\texpectedRelativePath: \"/s3/doc.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Mixed Separators -> Should hit Local Mount\",\n\t\t\tinputVirtualPath:     \"/local\\\\subdir/test.txt\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(localVdir, \"subdir\", \"test.txt\"),\n\t\t\texpectedRelativePath: \"/local/subdir/test.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Double Slash -> Should normalize and hit S3\",\n\t\t\tinputVirtualPath:     \"//s3//dir @1/data.csv\",\n\t\t\texpectedFsType:       \"S3Fs\",\n\t\t\texpectedPhyPath:      \"dir @1/data.csv\",\n\t\t\texpectedRelativePath: \"/s3/dir @1/data.csv\",\n\t\t},\n\n\t\t{\n\t\t\tname:                 \"Local Mount Traversal (Attempt to escape)\",\n\t\t\tinputVirtualPath:     \"/local/../../etc/passwd\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(homeDir, \"/etc/passwd\"),\n\t\t\texpectedRelativePath: \"/etc/passwd\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Traversal Out of S3 (Valid)\",\n\t\t\tinputVirtualPath:     \"/s3/../../secret.txt\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(homeDir, \"secret.txt\"),\n\t\t\texpectedRelativePath: \"/secret.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Traversal Inside S3\",\n\t\t\tinputVirtualPath:     \"/s3/subdir/../image.png\",\n\t\t\texpectedFsType:       \"S3Fs\",\n\t\t\texpectedPhyPath:      \"image.png\",\n\t\t\texpectedRelativePath: \"/s3/image.png\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Mount Point Bypass -> Target Local Mount\",\n\t\t\tinputVirtualPath:     \"/s3\\\\..\\\\local\\\\secret.txt\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(localVdir, \"secret.txt\"),\n\t\t\texpectedRelativePath: \"/local/secret.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Dirty Relative Path (Your Case)\",\n\t\t\tinputVirtualPath:     \"test\\\\..\\\\..\\\\oops/file.txt\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(homeDir, \"oops\", \"file.txt\"),\n\t\t\texpectedRelativePath: \"/oops/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Relative Path targeting S3 (No leading slash)\",\n\t\t\tinputVirtualPath:     \"s3//sub/../image.png\",\n\t\t\texpectedFsType:       \"S3Fs\",\n\t\t\texpectedPhyPath:      \"image.png\",\n\t\t\texpectedRelativePath: \"/s3/image.png\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Windows Path starting with Backslash\",\n\t\t\tinputVirtualPath:     \"\\\\s3\\\\doc/dir\\\\doc.txt\",\n\t\t\texpectedFsType:       \"S3Fs\",\n\t\t\texpectedPhyPath:      \"doc/dir/doc.txt\",\n\t\t\texpectedRelativePath: \"/s3/doc/dir/doc.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Filesystem Juggling (Relative)\",\n\t\t\tinputVirtualPath:     \"local/../s3/file.txt\",\n\t\t\texpectedFsType:       \"S3Fs\",\n\t\t\texpectedPhyPath:      \"file.txt\",\n\t\t\texpectedRelativePath: \"/s3/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Triple Dot Filename (Valid Name)\",\n\t\t\tinputVirtualPath:     \"/...hidden/secret\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(homeDir, \"...hidden\", \"secret\"),\n\t\t\texpectedRelativePath: \"/...hidden/secret\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Dot Slash Prefix\",\n\t\t\tinputVirtualPath:     \"./local/file.txt\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      filepath.Join(localVdir, \"file.txt\"),\n\t\t\texpectedRelativePath: \"/local/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Root of Local Mount Exactly\",\n\t\t\tinputVirtualPath:     \"/local/\",\n\t\t\texpectedFsType:       \"osfs\",\n\t\t\texpectedPhyPath:      localVdir,\n\t\t\texpectedRelativePath: \"/local\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Root of S3 Mount Exactly\",\n\t\t\tinputVirtualPath:     \"/s3/\",\n\t\t\texpectedFsType:       \"S3Fs\",\n\t\t\texpectedPhyPath:      \"\",\n\t\t\texpectedRelativePath: \"/s3\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// The input path is sanitized by the protocol handler\n\t\t\t// implementations before reaching GetFsAndResolvedPath.\n\t\t\tcleanInput := util.CleanPath(tc.inputVirtualPath)\n\t\t\tfs, resolvedPath, err := conn.GetFsAndResolvedPath(cleanInput)\n\t\t\tif assert.NoError(t, err, \"did not expect error for path: %q, got: %v\", tc.inputVirtualPath, err) {\n\t\t\t\tassert.Contains(t, fs.Name(), tc.expectedFsType,\n\t\t\t\t\t\"routing error: input %q but expected fs %q, got %q\", tc.inputVirtualPath, tc.expectedFsType, fs.Name())\n\t\t\t\tassert.Equal(t, tc.expectedPhyPath, resolvedPath,\n\t\t\t\t\t\"resolution error: input %q resolved to %q expected %q\", tc.inputVirtualPath, resolvedPath, tc.expectedPhyPath)\n\t\t\t\trelativePath := fs.GetRelativePath(resolvedPath)\n\t\t\t\tassert.Equal(t, tc.expectedRelativePath, relativePath,\n\t\t\t\t\t\"relative path error, input %q, got %q, expected %q\", tc.inputVirtualPath, tc.expectedRelativePath, relativePath)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOsFsGetRelativePath(t *testing.T) {\n\thomeDir := filepath.Join(os.TempDir(), \"home_test\")\n\tlocalVdir := filepath.Join(os.TempDir(), \"local_mount_test\")\n\n\terr := os.MkdirAll(homeDir, 0777)\n\trequire.NoError(t, err)\n\terr = os.MkdirAll(localVdir, 0777)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(homeDir)\n\t\tos.RemoveAll(localVdir)\n\t})\n\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: xid.New().String(),\n\t\t\tStatus:   1,\n\t\t\tHomeDir:  homeDir,\n\t\t},\n\t\tVirtualFolders: []vfs.VirtualFolder{\n\t\t\t{\n\t\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\t\tName:       \"local\",\n\t\t\t\t\tMappedPath: localVdir,\n\t\t\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\t\t\tProvider: sdk.LocalFilesystemProvider,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tVirtualPath: \"/local\",\n\t\t\t},\n\t\t},\n\t}\n\n\tconnID := xid.New().String()\n\trootFs, err := user.GetFilesystemForPath(\"/\", connID)\n\trequire.NoError(t, err)\n\n\tlocalFs, err := user.GetFilesystemForPath(\"/local\", connID)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname        string\n\t\tfs          vfs.Fs\n\t\tinputPath   string // The physical path to reverse-map\n\t\texpectedRel string // The expected virtual path\n\t}{\n\t\t{\n\t\t\tname:        \"Root FS - Inside root\",\n\t\t\tfs:          rootFs,\n\t\t\tinputPath:   filepath.Join(homeDir, \"docs\", \"file.txt\"),\n\t\t\texpectedRel: \"/docs/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Root FS - Exact root directory\",\n\t\t\tfs:          rootFs,\n\t\t\tinputPath:   homeDir,\n\t\t\texpectedRel: \"/\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Root FS - External absolute path (Jail to /)\",\n\t\t\tfs:          rootFs,\n\t\t\tinputPath:   \"/etc/passwd\",\n\t\t\texpectedRel: \"/\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Root FS - Traversal escape (Jail to /)\",\n\t\t\tfs:          rootFs,\n\t\t\tinputPath:   filepath.Join(homeDir, \"..\", \"escaped.txt\"),\n\t\t\texpectedRel: \"/\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Root FS - Valid file named with triple dots\",\n\t\t\tfs:          rootFs,\n\t\t\tinputPath:   filepath.Join(homeDir, \"...\"),\n\t\t\texpectedRel: \"/...\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Local FS - Up path in dir\",\n\t\t\tfs:          rootFs,\n\t\t\tinputPath:   homeDir + \"/../\" + filepath.Base(homeDir) + \"/dir/test.txt\",\n\t\t\texpectedRel: \"/dir/test.txt\",\n\t\t},\n\n\t\t{\n\t\t\tname:        \"Local FS - Inside mount\",\n\t\t\tfs:          localFs,\n\t\t\tinputPath:   filepath.Join(localVdir, \"data\", \"config.json\"),\n\t\t\texpectedRel: \"/local/data/config.json\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Local FS - Exact mount directory\",\n\t\t\tfs:          localFs,\n\t\t\tinputPath:   localVdir,\n\t\t\texpectedRel: \"/local\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Local FS - External absolute path (Jail to /local)\",\n\t\t\tfs:          localFs,\n\t\t\tinputPath:   \"/var/log/syslog\",\n\t\t\texpectedRel: \"/local\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Local FS - Traversal escape (Jail to /local)\",\n\t\t\tfs:          localFs,\n\t\t\tinputPath:   filepath.Join(localVdir, \"..\", \"..\", \"etc\", \"passwd\"),\n\t\t\texpectedRel: \"/local\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Local FS - Partial prefix (Jail to /local)\",\n\t\t\tfs:          localFs,\n\t\t\tinputPath:   localVdir + \"_backup\",\n\t\t\texpectedRel: \"/local\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Local FS - Relative traversal matching virual dir\",\n\t\t\tfs:          localFs,\n\t\t\tinputPath:   localVdir + \"/../\" + filepath.Base(localVdir) + \"/dir/test.txt\",\n\t\t\texpectedRel: \"/local/dir/test.txt\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Local FS - Valid file starting with two dots\",\n\t\t\tfs:          localFs,\n\t\t\tinputPath:   filepath.Join(localVdir, \"..hidden_file.txt\"),\n\t\t\texpectedRel: \"/local/..hidden_file.txt\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tactualRel := tc.fs.GetRelativePath(tc.inputPath)\n\t\t\tassert.Equal(t, tc.expectedRel, actualRel,\n\t\t\t\t\"Failed mapping physical path %q on FS %q\", tc.inputPath, tc.fs.Name())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/common/dataretention.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nvar (\n\t// RetentionChecks is the list of active retention checks\n\tRetentionChecks ActiveRetentionChecks\n)\n\n// ActiveRetentionChecks holds the active retention checks\ntype ActiveRetentionChecks struct {\n\tsync.RWMutex\n\tChecks []RetentionCheck\n}\n\n// Get returns the active retention checks\nfunc (c *ActiveRetentionChecks) Get(role string) []RetentionCheck {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tchecks := make([]RetentionCheck, 0, len(c.Checks))\n\tfor _, check := range c.Checks {\n\t\tif role == \"\" || role == check.Role {\n\t\t\tfoldersCopy := make([]dataprovider.FolderRetention, len(check.Folders))\n\t\t\tcopy(foldersCopy, check.Folders)\n\t\t\tchecks = append(checks, RetentionCheck{\n\t\t\t\tUsername:  check.Username,\n\t\t\t\tStartTime: check.StartTime,\n\t\t\t\tFolders:   foldersCopy,\n\t\t\t})\n\t\t}\n\t}\n\treturn checks\n}\n\n// Add a new retention check, returns nil if a retention check for the given\n// username is already active. The returned result can be used to start the check\nfunc (c *ActiveRetentionChecks) Add(check RetentionCheck, user *dataprovider.User) *RetentionCheck {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tfor _, val := range c.Checks {\n\t\tif val.Username == user.Username {\n\t\t\treturn nil\n\t\t}\n\t}\n\t// we silently ignore file patterns\n\tuser.Filters.FilePatterns = nil\n\tconn := NewBaseConnection(\"\", \"\", \"\", \"\", *user)\n\tconn.SetProtocol(ProtocolDataRetention)\n\tconn.ID = fmt.Sprintf(\"data_retention_%v\", user.Username)\n\tcheck.Username = user.Username\n\tcheck.Role = user.Role\n\tcheck.StartTime = util.GetTimeAsMsSinceEpoch(time.Now())\n\tcheck.conn = conn\n\tcheck.updateUserPermissions()\n\tc.Checks = append(c.Checks, check)\n\n\treturn &check\n}\n\n// remove a user from the ones with active retention checks\n// and returns true if the user is removed\nfunc (c *ActiveRetentionChecks) remove(username string) bool {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tfor idx, check := range c.Checks {\n\t\tif check.Username == username {\n\t\t\tlastIdx := len(c.Checks) - 1\n\t\t\tc.Checks[idx] = c.Checks[lastIdx]\n\t\t\tc.Checks = c.Checks[:lastIdx]\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\ntype folderRetentionCheckResult struct {\n\tPath         string        `json:\"path\"`\n\tRetention    int           `json:\"retention\"`\n\tDeletedFiles int           `json:\"deleted_files\"`\n\tDeletedSize  int64         `json:\"deleted_size\"`\n\tElapsed      time.Duration `json:\"-\"`\n\tInfo         string        `json:\"info,omitempty\"`\n\tError        string        `json:\"error,omitempty\"`\n}\n\n// RetentionCheck defines an active retention check\ntype RetentionCheck struct {\n\t// Username to which the retention check refers\n\tUsername string `json:\"username\"`\n\t// retention check start time as unix timestamp in milliseconds\n\tStartTime int64 `json:\"start_time\"`\n\t// affected folders\n\tFolders []dataprovider.FolderRetention `json:\"folders\"`\n\tRole    string                         `json:\"-\"`\n\t// Cleanup results\n\tresults []folderRetentionCheckResult `json:\"-\"`\n\tconn    *BaseConnection              `json:\"-\"`\n}\n\nfunc (c *RetentionCheck) updateUserPermissions() {\n\tfor k := range c.conn.User.Permissions {\n\t\tc.conn.User.Permissions[k] = []string{dataprovider.PermAny}\n\t}\n}\n\nfunc (c *RetentionCheck) getFolderRetention(folderPath string) (dataprovider.FolderRetention, error) {\n\tdirsForPath := util.GetDirsForVirtualPath(folderPath)\n\tfor _, dirPath := range dirsForPath {\n\t\tfor _, folder := range c.Folders {\n\t\t\tif folder.Path == dirPath {\n\t\t\t\treturn folder, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn dataprovider.FolderRetention{}, fmt.Errorf(\"unable to find folder retention for %q\", folderPath)\n}\n\nfunc (c *RetentionCheck) removeFile(virtualPath string, info os.FileInfo) error {\n\tfs, fsPath, err := c.conn.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn c.conn.RemoveFile(fs, fsPath, virtualPath, info)\n}\n\nfunc (c *RetentionCheck) cleanupFolder(folderPath string, recursion int) error {\n\tstartTime := time.Now()\n\tresult := folderRetentionCheckResult{\n\t\tPath: folderPath,\n\t}\n\tdefer func() {\n\t\tc.results = append(c.results, result)\n\t}()\n\tif recursion >= util.MaxRecursion {\n\t\tresult.Elapsed = time.Since(startTime)\n\t\tresult.Info = \"data retention check skipped: recursion too deep\"\n\t\tc.conn.Log(logger.LevelError, \"data retention check skipped, recursion too depth for %q: %d\",\n\t\t\tfolderPath, recursion)\n\t\treturn util.ErrRecursionTooDeep\n\t}\n\trecursion++\n\n\tfolderRetention, err := c.getFolderRetention(folderPath)\n\tif err != nil {\n\t\tresult.Elapsed = time.Since(startTime)\n\t\tresult.Error = \"unable to get folder retention\"\n\t\tc.conn.Log(logger.LevelError, \"unable to get folder retention for path %q\", folderPath)\n\t\treturn err\n\t}\n\tresult.Retention = folderRetention.Retention\n\tif folderRetention.Retention == 0 {\n\t\tresult.Elapsed = time.Since(startTime)\n\t\tresult.Info = \"data retention check skipped: retention is set to 0\"\n\t\tc.conn.Log(logger.LevelDebug, \"retention check skipped for folder %q, retention is set to 0\", folderPath)\n\t\treturn nil\n\t}\n\tc.conn.Log(logger.LevelDebug, \"start retention check for folder %q, retention: %v hours, delete empty dirs? %v\",\n\t\tfolderPath, folderRetention.Retention, folderRetention.DeleteEmptyDirs)\n\tlister, err := c.conn.ListDir(folderPath)\n\tif err != nil {\n\t\tresult.Elapsed = time.Since(startTime)\n\t\tif err == c.conn.GetNotExistError() {\n\t\t\tresult.Info = \"data retention check skipped, folder does not exist\"\n\t\t\tc.conn.Log(logger.LevelDebug, \"folder %q does not exist, retention check skipped\", folderPath)\n\t\t\treturn nil\n\t\t}\n\t\tresult.Error = fmt.Sprintf(\"unable to get lister for directory %q\", folderPath)\n\t\tc.conn.Log(logger.LevelError, \"%s\", result.Error)\n\t\treturn err\n\t}\n\tdefer lister.Close()\n\n\tfor {\n\t\tfiles, err := lister.Next(vfs.ListerBatchSize)\n\t\tfinished := errors.Is(err, io.EOF)\n\t\tif err := lister.convertError(err); err != nil {\n\t\t\tresult.Elapsed = time.Since(startTime)\n\t\t\tresult.Error = fmt.Sprintf(\"unable to list directory %q\", folderPath)\n\t\t\tc.conn.Log(logger.LevelError, \"unable to list dir %q: %v\", folderPath, err)\n\t\t\treturn err\n\t\t}\n\t\tfor _, info := range files {\n\t\t\tvirtualPath := path.Join(folderPath, info.Name())\n\t\t\tif info.IsDir() {\n\t\t\t\tif err := c.cleanupFolder(virtualPath, recursion); err != nil {\n\t\t\t\t\tresult.Elapsed = time.Since(startTime)\n\t\t\t\t\tresult.Error = fmt.Sprintf(\"unable to check folder: %v\", err)\n\t\t\t\t\tc.conn.Log(logger.LevelError, \"unable to cleanup folder %q: %v\", virtualPath, err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tretentionTime := info.ModTime().Add(time.Duration(folderRetention.Retention) * time.Hour)\n\t\t\t\tif retentionTime.Before(time.Now()) {\n\t\t\t\t\tif err := c.removeFile(virtualPath, info); err != nil {\n\t\t\t\t\t\tresult.Elapsed = time.Since(startTime)\n\t\t\t\t\t\tresult.Error = fmt.Sprintf(\"unable to remove file %q: %v\", virtualPath, err)\n\t\t\t\t\t\tc.conn.Log(logger.LevelError, \"unable to remove file %q, retention %v: %v\",\n\t\t\t\t\t\t\tvirtualPath, retentionTime, err)\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tc.conn.Log(logger.LevelDebug, \"removed file %q, modification time: %v, retention: %v hours, retention time: %v\",\n\t\t\t\t\t\tvirtualPath, info.ModTime(), folderRetention.Retention, retentionTime)\n\t\t\t\t\tresult.DeletedFiles++\n\t\t\t\t\tresult.DeletedSize += info.Size()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif finished {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tlister.Close()\n\tc.checkEmptyDirRemoval(folderPath, folderRetention.DeleteEmptyDirs)\n\tresult.Elapsed = time.Since(startTime)\n\tc.conn.Log(logger.LevelDebug, \"retention check completed for folder %q, deleted files: %v, deleted size: %v bytes\",\n\t\tfolderPath, result.DeletedFiles, result.DeletedSize)\n\n\treturn nil\n}\n\nfunc (c *RetentionCheck) checkEmptyDirRemoval(folderPath string, checkVal bool) {\n\tif folderPath == \"/\" || !checkVal {\n\t\treturn\n\t}\n\tfor _, folder := range c.Folders {\n\t\tif folderPath == folder.Path {\n\t\t\treturn\n\t\t}\n\t}\n\tif c.conn.User.HasAnyPerm([]string{\n\t\tdataprovider.PermDelete,\n\t\tdataprovider.PermDeleteDirs,\n\t}, path.Dir(folderPath),\n\t) {\n\t\tlister, err := c.conn.ListDir(folderPath)\n\t\tif err == nil {\n\t\t\tfiles, err := lister.Next(1)\n\t\t\tlister.Close()\n\t\t\tif len(files) == 0 && errors.Is(err, io.EOF) {\n\t\t\t\terr = c.conn.RemoveDir(folderPath)\n\t\t\t\tc.conn.Log(logger.LevelDebug, \"tried to remove empty dir %q, error: %v\", folderPath, err)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Start starts the retention check\nfunc (c *RetentionCheck) Start() error {\n\tc.conn.Log(logger.LevelInfo, \"retention check started\")\n\tdefer RetentionChecks.remove(c.conn.User.Username)\n\tdefer c.conn.CloseFS() //nolint:errcheck\n\n\tstartTime := time.Now()\n\tfor _, folder := range c.Folders {\n\t\tif folder.Retention > 0 {\n\t\t\tif err := c.cleanupFolder(folder.Path, 0); err != nil {\n\t\t\t\tc.conn.Log(logger.LevelError, \"retention check failed, unable to cleanup folder %q, elapsed: %s\",\n\t\t\t\t\tfolder.Path, time.Since(startTime))\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tc.conn.Log(logger.LevelInfo, \"retention check completed, elapsed: %s\", time.Since(startTime))\n\treturn nil\n}\n"
  },
  {
    "path": "internal/common/dataretention_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc TestRetentionPermissionsAndGetFolder(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"user1\",\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDelete}\n\tuser.Permissions[\"/dir1\"] = []string{dataprovider.PermListItems}\n\tuser.Permissions[\"/dir2/sub1\"] = []string{dataprovider.PermCreateDirs}\n\tuser.Permissions[\"/dir2/sub2\"] = []string{dataprovider.PermDelete}\n\n\tcheck := RetentionCheck{\n\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:      \"/dir2\",\n\t\t\t\tRetention: 24 * 7,\n\t\t\t},\n\t\t\t{\n\t\t\t\tPath:      \"/dir3\",\n\t\t\t\tRetention: 24 * 7,\n\t\t\t},\n\t\t\t{\n\t\t\t\tPath:      \"/dir2/sub1/sub\",\n\t\t\t\tRetention: 24,\n\t\t\t},\n\t\t},\n\t}\n\n\tconn := NewBaseConnection(\"\", \"\", \"\", \"\", user)\n\tconn.SetProtocol(ProtocolDataRetention)\n\tconn.ID = fmt.Sprintf(\"data_retention_%v\", user.Username)\n\tcheck.conn = conn\n\tcheck.updateUserPermissions()\n\tassert.Equal(t, []string{dataprovider.PermAny}, conn.User.Permissions[\"/\"])\n\tassert.Equal(t, []string{dataprovider.PermAny}, conn.User.Permissions[\"/dir1\"])\n\tassert.Equal(t, []string{dataprovider.PermAny}, conn.User.Permissions[\"/dir2/sub1\"])\n\tassert.Equal(t, []string{dataprovider.PermAny}, conn.User.Permissions[\"/dir2/sub2\"])\n\n\t_, err := check.getFolderRetention(\"/\")\n\tassert.Error(t, err)\n\tfolder, err := check.getFolderRetention(\"/dir3\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/dir3\", folder.Path)\n\tfolder, err = check.getFolderRetention(\"/dir2/sub3\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/dir2\", folder.Path)\n\tfolder, err = check.getFolderRetention(\"/dir2/sub2\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/dir2\", folder.Path)\n\tfolder, err = check.getFolderRetention(\"/dir2/sub1\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/dir2\", folder.Path)\n\tfolder, err = check.getFolderRetention(\"/dir2/sub1/sub/sub\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/dir2/sub1/sub\", folder.Path)\n}\n\nfunc TestRetentionCheckAddRemove(t *testing.T) {\n\tusername := \"username\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tcheck := RetentionCheck{\n\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:      \"/\",\n\t\t\t\tRetention: 48,\n\t\t\t},\n\t\t},\n\t}\n\tassert.NotNil(t, RetentionChecks.Add(check, &user))\n\tchecks := RetentionChecks.Get(\"\")\n\trequire.Len(t, checks, 1)\n\tassert.Equal(t, username, checks[0].Username)\n\tassert.Greater(t, checks[0].StartTime, int64(0))\n\trequire.Len(t, checks[0].Folders, 1)\n\tassert.Equal(t, check.Folders[0].Path, checks[0].Folders[0].Path)\n\tassert.Equal(t, check.Folders[0].Retention, checks[0].Folders[0].Retention)\n\n\tassert.Nil(t, RetentionChecks.Add(check, &user))\n\tassert.True(t, RetentionChecks.remove(username))\n\trequire.Len(t, RetentionChecks.Get(\"\"), 0)\n\tassert.False(t, RetentionChecks.remove(username))\n}\n\nfunc TestRetentionCheckRole(t *testing.T) {\n\tusername := \"retuser\"\n\trole1 := \"retrole1\"\n\trole2 := \"retrole2\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tRole:     role1,\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tcheck := RetentionCheck{\n\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:      \"/\",\n\t\t\t\tRetention: 48,\n\t\t\t},\n\t\t},\n\t}\n\tassert.NotNil(t, RetentionChecks.Add(check, &user))\n\tchecks := RetentionChecks.Get(\"\")\n\trequire.Len(t, checks, 1)\n\tassert.Empty(t, checks[0].Role)\n\tchecks = RetentionChecks.Get(role1)\n\trequire.Len(t, checks, 1)\n\tchecks = RetentionChecks.Get(role2)\n\trequire.Len(t, checks, 0)\n\tuser.Role = \"\"\n\tassert.Nil(t, RetentionChecks.Add(check, &user))\n\tassert.True(t, RetentionChecks.remove(username))\n\trequire.Len(t, RetentionChecks.Get(\"\"), 0)\n}\n\nfunc TestCleanupErrors(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"u\",\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tcheck := &RetentionCheck{\n\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:      \"/path\",\n\t\t\t\tRetention: 48,\n\t\t\t},\n\t\t},\n\t}\n\tcheck = RetentionChecks.Add(*check, &user)\n\trequire.NotNil(t, check)\n\n\terr := check.removeFile(\"missing file\", nil)\n\tassert.Error(t, err)\n\n\terr = check.cleanupFolder(\"/\", 0)\n\tassert.Error(t, err)\n\n\terr = check.cleanupFolder(\"/\", 1000)\n\tassert.ErrorIs(t, err, util.ErrRecursionTooDeep)\n\n\tassert.True(t, RetentionChecks.remove(user.Username))\n}\n"
  },
  {
    "path": "internal/common/defender.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// HostEvent is the enumerable for the supported host events\ntype HostEvent string\n\n// Supported host events\nconst (\n\tHostEventLoginFailed   HostEvent = \"LoginFailed\"\n\tHostEventUserNotFound  HostEvent = \"UserNotFound\"\n\tHostEventNoLoginTried  HostEvent = \"NoLoginTried\"\n\tHostEventLimitExceeded HostEvent = \"LimitExceeded\"\n)\n\n// Supported defender drivers\nconst (\n\tDefenderDriverMemory   = \"memory\"\n\tDefenderDriverProvider = \"provider\"\n)\n\nvar (\n\tsupportedDefenderDrivers = []string{DefenderDriverMemory, DefenderDriverProvider}\n)\n\n// Defender defines the interface that a defender must implements\ntype Defender interface {\n\tGetHosts() ([]dataprovider.DefenderEntry, error)\n\tGetHost(ip string) (dataprovider.DefenderEntry, error)\n\tAddEvent(ip, protocol string, event HostEvent) bool\n\tIsBanned(ip, protocol string) bool\n\tIsSafe(ip, protocol string) bool\n\tGetBanTime(ip string) (*time.Time, error)\n\tGetScore(ip string) (int, error)\n\tDeleteHost(ip string) bool\n\tDelayLogin(err error)\n}\n\n// DefenderConfig defines the \"defender\" configuration\ntype DefenderConfig struct {\n\t// Set to true to enable the defender\n\tEnabled bool `json:\"enabled\" mapstructure:\"enabled\"`\n\t// Defender implementation to use, we support \"memory\" and \"provider\".\n\t// Using \"provider\" as driver you can share the defender events among\n\t// multiple SFTPGo instances. For a single instance \"memory\" provider will\n\t// be much faster\n\tDriver string `json:\"driver\" mapstructure:\"driver\"`\n\t// BanTime is the number of minutes that a host is banned\n\tBanTime int `json:\"ban_time\" mapstructure:\"ban_time\"`\n\t// Percentage increase of the ban time if a banned host tries to connect again\n\tBanTimeIncrement int `json:\"ban_time_increment\" mapstructure:\"ban_time_increment\"`\n\t// Threshold value for banning a client\n\tThreshold int `json:\"threshold\" mapstructure:\"threshold\"`\n\t// Score for invalid login attempts, eg. non-existent user accounts\n\tScoreInvalid int `json:\"score_invalid\" mapstructure:\"score_invalid\"`\n\t// Score for valid login attempts, eg. user accounts that exist\n\tScoreValid int `json:\"score_valid\" mapstructure:\"score_valid\"`\n\t// Score for limit exceeded events, generated from the rate limiters or for max connections\n\t// per-host exceeded\n\tScoreLimitExceeded int `json:\"score_limit_exceeded\" mapstructure:\"score_limit_exceeded\"`\n\t// ScoreNoAuth defines the score for clients disconnected without authentication\n\t// attempts\n\tScoreNoAuth int `json:\"score_no_auth\" mapstructure:\"score_no_auth\"`\n\t// Defines the time window, in minutes, for tracking client errors.\n\t// A host is banned if it has exceeded the defined threshold during\n\t// the last observation time minutes\n\tObservationTime int `json:\"observation_time\" mapstructure:\"observation_time\"`\n\t// The number of banned IPs and host scores kept in memory will vary between the\n\t// soft and hard limit for the \"memory\" driver. For the \"provider\" driver the\n\t// soft limit is ignored and the hard limit is used to limit the number of entries\n\t// to return when you request for the entire host list from the defender\n\tEntriesSoftLimit int `json:\"entries_soft_limit\" mapstructure:\"entries_soft_limit\"`\n\tEntriesHardLimit int `json:\"entries_hard_limit\" mapstructure:\"entries_hard_limit\"`\n\t// Configuration to impose a delay between login attempts\n\tLoginDelay LoginDelay `json:\"login_delay\" mapstructure:\"login_delay\"`\n}\n\n// LoginDelay defines the delays to impose between login attempts.\ntype LoginDelay struct {\n\t// The number of milliseconds to pause prior to allowing a successful login\n\tSuccess int `json:\"success\" mapstructure:\"success\"`\n\t// The number of milliseconds to pause prior to reporting a failed login\n\tPasswordFailed int `json:\"password_failed\" mapstructure:\"password_failed\"`\n}\n\ntype baseDefender struct {\n\tconfig *DefenderConfig\n\tipList *dataprovider.IPList\n}\n\nfunc (d *baseDefender) isBanned(ip, protocol string) bool {\n\tisListed, mode, err := d.ipList.IsListed(ip, protocol)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif isListed && mode == dataprovider.ListModeDeny {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (d *baseDefender) IsSafe(ip, protocol string) bool {\n\tisListed, mode, err := d.ipList.IsListed(ip, protocol)\n\tif err == nil && isListed && mode == dataprovider.ListModeAllow {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (d *baseDefender) getScore(event HostEvent) int {\n\tvar score int\n\n\tswitch event {\n\tcase HostEventLoginFailed:\n\t\tscore = d.config.ScoreValid\n\tcase HostEventLimitExceeded:\n\t\tscore = d.config.ScoreLimitExceeded\n\tcase HostEventUserNotFound:\n\t\tscore = d.config.ScoreInvalid\n\tcase HostEventNoLoginTried:\n\t\tscore = d.config.ScoreNoAuth\n\t}\n\treturn score\n}\n\n// logEvent logs a defender event that changes a host's score\nfunc (d *baseDefender) logEvent(ip, protocol string, event HostEvent, totalScore int) {\n\t// ignore events which do not change the host score\n\teventScore := d.getScore(event)\n\tif eventScore == 0 {\n\t\treturn\n\t}\n\n\tlogger.GetLogger().Debug().\n\t\tTimestamp().\n\t\tStr(\"sender\", \"defender\").\n\t\tStr(\"client_ip\", ip).\n\t\tStr(\"protocol\", protocol).\n\t\tStr(\"event\", string(event)).\n\t\tInt(\"increase_score_by\", eventScore).\n\t\tInt(\"score\", totalScore).\n\t\tSend()\n}\n\n// logBan logs a host's ban due to a too high host score\nfunc (d *baseDefender) logBan(ip, protocol string) {\n\tlogger.GetLogger().Info().\n\t\tTimestamp().\n\t\tStr(\"sender\", \"defender\").\n\t\tStr(\"client_ip\", ip).\n\t\tStr(\"protocol\", protocol).\n\t\tStr(\"event\", \"banned\").\n\t\tSend()\n}\n\n// DelayLogin applies the configured login delay.\nfunc (d *baseDefender) DelayLogin(err error) {\n\tif err == nil {\n\t\tif d.config.LoginDelay.Success > 0 {\n\t\t\ttime.Sleep(time.Duration(d.config.LoginDelay.Success) * time.Millisecond)\n\t\t}\n\t\treturn\n\t}\n\tif d.config.LoginDelay.PasswordFailed > 0 {\n\t\ttime.Sleep(time.Duration(d.config.LoginDelay.PasswordFailed) * time.Millisecond)\n\t}\n}\n\ntype hostEvent struct {\n\tdateTime time.Time\n\tscore    int\n}\n\ntype hostScore struct {\n\tTotalScore int\n\tEvents     []hostEvent\n}\n\nfunc (c *DefenderConfig) checkScores() error {\n\tif c.ScoreInvalid < 0 {\n\t\tc.ScoreInvalid = 0\n\t}\n\tif c.ScoreValid < 0 {\n\t\tc.ScoreValid = 0\n\t}\n\tif c.ScoreLimitExceeded < 0 {\n\t\tc.ScoreLimitExceeded = 0\n\t}\n\tif c.ScoreNoAuth < 0 {\n\t\tc.ScoreNoAuth = 0\n\t}\n\tif c.ScoreInvalid == 0 && c.ScoreValid == 0 && c.ScoreLimitExceeded == 0 && c.ScoreNoAuth == 0 {\n\t\treturn fmt.Errorf(\"invalid defender configuration: all scores are disabled\")\n\t}\n\treturn nil\n}\n\n// validate returns an error if the configuration is invalid\nfunc (c *DefenderConfig) validate() error {\n\tif !c.Enabled {\n\t\treturn nil\n\t}\n\tif err := c.checkScores(); err != nil {\n\t\treturn err\n\t}\n\tif c.ScoreInvalid >= c.Threshold {\n\t\treturn fmt.Errorf(\"score_invalid %d cannot be greater than threshold %d\", c.ScoreInvalid, c.Threshold)\n\t}\n\tif c.ScoreValid >= c.Threshold {\n\t\treturn fmt.Errorf(\"score_valid %d cannot be greater than threshold %d\", c.ScoreValid, c.Threshold)\n\t}\n\tif c.ScoreLimitExceeded >= c.Threshold {\n\t\treturn fmt.Errorf(\"score_limit_exceeded %d cannot be greater than threshold %d\", c.ScoreLimitExceeded, c.Threshold)\n\t}\n\tif c.ScoreNoAuth >= c.Threshold {\n\t\treturn fmt.Errorf(\"score_no_auth %d cannot be greater than threshold %d\", c.ScoreNoAuth, c.Threshold)\n\t}\n\tif c.BanTime <= 0 {\n\t\treturn fmt.Errorf(\"invalid ban_time %v\", c.BanTime)\n\t}\n\tif c.BanTimeIncrement <= 0 {\n\t\treturn fmt.Errorf(\"invalid ban_time_increment %v\", c.BanTimeIncrement)\n\t}\n\tif c.ObservationTime <= 0 {\n\t\treturn fmt.Errorf(\"invalid observation_time %v\", c.ObservationTime)\n\t}\n\tif c.EntriesSoftLimit <= 0 {\n\t\treturn fmt.Errorf(\"invalid entries_soft_limit %v\", c.EntriesSoftLimit)\n\t}\n\tif c.EntriesHardLimit <= c.EntriesSoftLimit {\n\t\treturn fmt.Errorf(\"invalid entries_hard_limit %v must be > %v\", c.EntriesHardLimit, c.EntriesSoftLimit)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/common/defender_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/yl2chen/cidranger\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n)\n\nfunc TestBasicDefender(t *testing.T) {\n\tentries := []dataprovider.IPListEntry{\n\t\t{\n\t\t\tIPOrNet: \"172.16.1.1/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"172.16.1.2/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"10.8.0.0/24\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"192.168.1.1/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"192.168.1.2/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"10.8.9.0/24\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"172.16.1.3/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"172.16.1.4/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"192.168.8.0/24\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"192.168.1.3/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"192.168.1.4/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"192.168.9.0/24\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t}\n\n\tfor idx := range entries {\n\t\te := entries[idx]\n\t\terr := dataprovider.AddIPListEntry(&e, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\n\tconfig := &DefenderConfig{\n\t\tEnabled:            true,\n\t\tBanTime:            10,\n\t\tBanTimeIncrement:   2,\n\t\tThreshold:          5,\n\t\tScoreInvalid:       2,\n\t\tScoreValid:         1,\n\t\tScoreNoAuth:        2,\n\t\tScoreLimitExceeded: 3,\n\t\tObservationTime:    15,\n\t\tEntriesSoftLimit:   1,\n\t\tEntriesHardLimit:   2,\n\t}\n\n\td, err := newInMemoryDefender(config)\n\tassert.NoError(t, err)\n\n\tdefender := d.(*memoryDefender)\n\tassert.True(t, defender.IsBanned(\"172.16.1.1\", ProtocolSSH))\n\tassert.True(t, defender.IsBanned(\"192.168.1.1\", ProtocolFTP))\n\tassert.False(t, defender.IsBanned(\"172.16.1.10\", ProtocolSSH))\n\tassert.False(t, defender.IsBanned(\"192.168.1.10\", ProtocolSSH))\n\tassert.False(t, defender.IsBanned(\"10.8.2.3\", ProtocolSSH))\n\tassert.False(t, defender.IsBanned(\"10.9.2.3\", ProtocolSSH))\n\tassert.True(t, defender.IsBanned(\"10.8.0.3\", ProtocolSSH))\n\tassert.True(t, defender.IsBanned(\"10.8.9.3\", ProtocolSSH))\n\tassert.False(t, defender.IsBanned(\"invalid ip\", ProtocolSSH))\n\tassert.Equal(t, 0, defender.countBanned())\n\tassert.Equal(t, 0, defender.countHosts())\n\thosts, err := defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 0)\n\t_, err = defender.GetHost(\"10.8.0.4\")\n\tassert.Error(t, err)\n\n\tdefender.AddEvent(\"172.16.1.4\", ProtocolSSH, HostEventLoginFailed)\n\tdefender.AddEvent(\"192.168.1.4\", ProtocolSSH, HostEventLoginFailed)\n\tdefender.AddEvent(\"192.168.8.4\", ProtocolSSH, HostEventUserNotFound)\n\tdefender.AddEvent(\"172.16.1.3\", ProtocolSSH, HostEventLimitExceeded)\n\tdefender.AddEvent(\"192.168.1.3\", ProtocolSSH, HostEventLimitExceeded)\n\tassert.Equal(t, 0, defender.countHosts())\n\n\ttestIP := \"12.34.56.78\"\n\tdefender.AddEvent(testIP, ProtocolSSH, HostEventLoginFailed)\n\tassert.Equal(t, 1, defender.countHosts())\n\tassert.Equal(t, 0, defender.countBanned())\n\tscore, err := defender.GetScore(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, score)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\tassert.Equal(t, 1, hosts[0].Score)\n\t\tassert.True(t, hosts[0].BanTime.IsZero())\n\t\tassert.Empty(t, hosts[0].GetBanTime())\n\t}\n\thost, err := defender.GetHost(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, host.Score)\n\tassert.Empty(t, host.GetBanTime())\n\tbanTime, err := defender.GetBanTime(testIP)\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\tdefender.AddEvent(testIP, ProtocolSSH, HostEventLimitExceeded)\n\tassert.Equal(t, 1, defender.countHosts())\n\tassert.Equal(t, 0, defender.countBanned())\n\tscore, err = defender.GetScore(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 4, score)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\tassert.Equal(t, 4, hosts[0].Score)\n\t\tassert.True(t, hosts[0].BanTime.IsZero())\n\t\tassert.Empty(t, hosts[0].GetBanTime())\n\t}\n\tdefender.AddEvent(testIP, ProtocolSSH, HostEventUserNotFound)\n\tdefender.AddEvent(testIP, ProtocolSSH, HostEventNoLoginTried)\n\tassert.Equal(t, 0, defender.countHosts())\n\tassert.Equal(t, 1, defender.countBanned())\n\tscore, err = defender.GetScore(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, score)\n\tbanTime, err = defender.GetBanTime(testIP)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, banTime)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\tassert.Equal(t, 0, hosts[0].Score)\n\t\tassert.False(t, hosts[0].BanTime.IsZero())\n\t\tassert.NotEmpty(t, hosts[0].GetBanTime())\n\t\tassert.Equal(t, hex.EncodeToString([]byte(testIP)), hosts[0].GetID())\n\t}\n\thost, err = defender.GetHost(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, host.Score)\n\tassert.NotEmpty(t, host.GetBanTime())\n\n\t// now test cleanup, testIP is already banned\n\ttestIP1 := \"12.34.56.79\"\n\ttestIP2 := \"12.34.56.80\"\n\ttestIP3 := \"12.34.56.81\"\n\n\tdefender.AddEvent(testIP1, ProtocolSSH, HostEventNoLoginTried)\n\tdefender.AddEvent(testIP2, ProtocolSSH, HostEventNoLoginTried)\n\tassert.Equal(t, 2, defender.countHosts())\n\ttime.Sleep(20 * time.Millisecond)\n\tdefender.AddEvent(testIP3, ProtocolSSH, HostEventNoLoginTried)\n\tassert.Equal(t, defender.config.EntriesSoftLimit, defender.countHosts())\n\t// testIP1 and testIP2 should be removed\n\tassert.Equal(t, defender.config.EntriesSoftLimit, defender.countHosts())\n\tscore, err = defender.GetScore(testIP1)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, score)\n\tscore, err = defender.GetScore(testIP2)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, score)\n\tscore, err = defender.GetScore(testIP3)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2, score)\n\n\tdefender.AddEvent(testIP3, ProtocolSSH, HostEventNoLoginTried)\n\tdefender.AddEvent(testIP3, ProtocolSSH, HostEventNoLoginTried)\n\t// IP3 is now banned\n\tbanTime, err = defender.GetBanTime(testIP3)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, banTime)\n\tassert.Equal(t, 0, defender.countHosts())\n\n\ttime.Sleep(20 * time.Millisecond)\n\tfor i := 0; i < 3; i++ {\n\t\tdefender.AddEvent(testIP1, ProtocolSSH, HostEventNoLoginTried)\n\t}\n\tassert.Equal(t, 0, defender.countHosts())\n\tassert.Equal(t, config.EntriesSoftLimit, defender.countBanned())\n\tbanTime, err = defender.GetBanTime(testIP)\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\tbanTime, err = defender.GetBanTime(testIP3)\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\tbanTime, err = defender.GetBanTime(testIP1)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, banTime)\n\n\tfor i := 0; i < 3; i++ {\n\t\tdefender.AddEvent(testIP, ProtocolSSH, HostEventNoLoginTried)\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\tdefender.AddEvent(testIP3, ProtocolSSH, HostEventNoLoginTried)\n\t}\n\tassert.Equal(t, 0, defender.countHosts())\n\tassert.Equal(t, defender.config.EntriesSoftLimit, defender.countBanned())\n\n\tbanTime, err = defender.GetBanTime(testIP3)\n\tassert.NoError(t, err)\n\tif assert.NotNil(t, banTime) {\n\t\tassert.True(t, defender.IsBanned(testIP3, ProtocolFTP))\n\t\t// ban time should increase\n\t\tnewBanTime, err := defender.GetBanTime(testIP3)\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, newBanTime.After(*banTime))\n\t}\n\n\tassert.True(t, defender.DeleteHost(testIP3))\n\tassert.False(t, defender.DeleteHost(testIP3))\n\n\tfor _, e := range entries {\n\t\terr := dataprovider.DeleteIPListEntry(e.IPOrNet, e.Type, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestExpiredHostBans(t *testing.T) {\n\tconfig := &DefenderConfig{\n\t\tEnabled:            true,\n\t\tBanTime:            10,\n\t\tBanTimeIncrement:   2,\n\t\tThreshold:          5,\n\t\tScoreInvalid:       2,\n\t\tScoreValid:         1,\n\t\tScoreLimitExceeded: 3,\n\t\tObservationTime:    15,\n\t\tEntriesSoftLimit:   1,\n\t\tEntriesHardLimit:   2,\n\t}\n\n\td, err := newInMemoryDefender(config)\n\tassert.NoError(t, err)\n\n\tdefender := d.(*memoryDefender)\n\n\ttestIP := \"1.2.3.4\"\n\tdefender.banned[testIP] = time.Now().Add(-24 * time.Hour)\n\n\t// the ban is expired testIP should not be listed\n\tres, err := defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, res, 0)\n\n\tassert.False(t, defender.IsBanned(testIP, ProtocolFTP))\n\t_, err = defender.GetHost(testIP)\n\tassert.Error(t, err)\n\t_, ok := defender.banned[testIP]\n\tassert.True(t, ok)\n\t// now add an event for an expired banned ip, it should be removed\n\tdefender.AddEvent(testIP, ProtocolFTP, HostEventLoginFailed)\n\tassert.False(t, defender.IsBanned(testIP, ProtocolFTP))\n\tentry, err := defender.GetHost(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, testIP, entry.IP)\n\tassert.Empty(t, entry.GetBanTime())\n\tassert.Equal(t, 1, entry.Score)\n\n\tres, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, res, 1) {\n\t\tassert.Equal(t, testIP, res[0].IP)\n\t\tassert.Empty(t, res[0].GetBanTime())\n\t\tassert.Equal(t, 1, res[0].Score)\n\t}\n\n\tevents := []hostEvent{\n\t\t{\n\t\t\tdateTime: time.Now().Add(-24 * time.Hour),\n\t\t\tscore:    2,\n\t\t},\n\t\t{\n\t\t\tdateTime: time.Now().Add(-24 * time.Hour),\n\t\t\tscore:    3,\n\t\t},\n\t}\n\n\ths := hostScore{\n\t\tEvents:     events,\n\t\tTotalScore: 5,\n\t}\n\n\tdefender.hosts[testIP] = hs\n\t// the recorded scored are too old\n\tres, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, res, 0)\n\t_, err = defender.GetHost(testIP)\n\tassert.Error(t, err)\n\t_, ok = defender.hosts[testIP]\n\tassert.True(t, ok)\n}\n\nfunc TestDefenderCleanup(t *testing.T) {\n\td := memoryDefender{\n\t\tbaseDefender: baseDefender{\n\t\t\tconfig: &DefenderConfig{\n\t\t\t\tObservationTime:  1,\n\t\t\t\tEntriesSoftLimit: 2,\n\t\t\t\tEntriesHardLimit: 3,\n\t\t\t},\n\t\t},\n\t\tbanned: make(map[string]time.Time),\n\t\thosts:  make(map[string]hostScore),\n\t}\n\n\td.banned[\"1.1.1.1\"] = time.Now().Add(-24 * time.Hour)\n\td.banned[\"1.1.1.2\"] = time.Now().Add(-24 * time.Hour)\n\td.banned[\"1.1.1.3\"] = time.Now().Add(-24 * time.Hour)\n\td.banned[\"1.1.1.4\"] = time.Now().Add(-24 * time.Hour)\n\n\td.cleanupBanned()\n\tassert.Equal(t, 0, d.countBanned())\n\n\td.banned[\"2.2.2.2\"] = time.Now().Add(2 * time.Minute)\n\td.banned[\"2.2.2.3\"] = time.Now().Add(1 * time.Minute)\n\td.banned[\"2.2.2.4\"] = time.Now().Add(3 * time.Minute)\n\td.banned[\"2.2.2.5\"] = time.Now().Add(4 * time.Minute)\n\n\td.cleanupBanned()\n\tassert.Equal(t, d.config.EntriesSoftLimit, d.countBanned())\n\tbanTime, err := d.GetBanTime(\"2.2.2.3\")\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\n\td.hosts[\"3.3.3.3\"] = hostScore{\n\t\tTotalScore: 0,\n\t\tEvents: []hostEvent{\n\t\t\t{\n\t\t\t\tdateTime: time.Now().Add(-5 * time.Minute),\n\t\t\t\tscore:    1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdateTime: time.Now().Add(-3 * time.Minute),\n\t\t\t\tscore:    1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdateTime: time.Now(),\n\t\t\t\tscore:    1,\n\t\t\t},\n\t\t},\n\t}\n\td.hosts[\"3.3.3.4\"] = hostScore{\n\t\tTotalScore: 1,\n\t\tEvents: []hostEvent{\n\t\t\t{\n\t\t\t\tdateTime: time.Now().Add(-3 * time.Minute),\n\t\t\t\tscore:    1,\n\t\t\t},\n\t\t},\n\t}\n\td.hosts[\"3.3.3.5\"] = hostScore{\n\t\tTotalScore: 1,\n\t\tEvents: []hostEvent{\n\t\t\t{\n\t\t\t\tdateTime: time.Now().Add(-2 * time.Minute),\n\t\t\t\tscore:    1,\n\t\t\t},\n\t\t},\n\t}\n\td.hosts[\"3.3.3.6\"] = hostScore{\n\t\tTotalScore: 1,\n\t\tEvents: []hostEvent{\n\t\t\t{\n\t\t\t\tdateTime: time.Now().Add(-1 * time.Minute),\n\t\t\t\tscore:    1,\n\t\t\t},\n\t\t},\n\t}\n\n\tscore, err := d.GetScore(\"3.3.3.3\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, score)\n\n\td.cleanupHosts()\n\tassert.Equal(t, d.config.EntriesSoftLimit, d.countHosts())\n\tscore, err = d.GetScore(\"3.3.3.4\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, score)\n}\n\nfunc TestDefenderDelay(t *testing.T) {\n\td := memoryDefender{\n\t\tbaseDefender: baseDefender{\n\t\t\tconfig: &DefenderConfig{\n\t\t\t\tObservationTime:  1,\n\t\t\t\tEntriesSoftLimit: 2,\n\t\t\t\tEntriesHardLimit: 3,\n\t\t\t\tLoginDelay: LoginDelay{\n\t\t\t\t\tSuccess:        50,\n\t\t\t\t\tPasswordFailed: 200,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tstartTime := time.Now()\n\td.DelayLogin(nil)\n\telapsed := time.Since(startTime)\n\tassert.Less(t, elapsed, time.Millisecond*100)\n\n\tstartTime = time.Now()\n\td.DelayLogin(ErrInternalFailure)\n\telapsed = time.Since(startTime)\n\tassert.Greater(t, elapsed, time.Millisecond*150)\n}\n\nfunc TestDefenderConfig(t *testing.T) {\n\tc := DefenderConfig{}\n\terr := c.validate()\n\trequire.NoError(t, err)\n\n\tc.Enabled = true\n\tc.Threshold = 10\n\tc.ScoreInvalid = 10\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.ScoreInvalid = 2\n\tc.ScoreLimitExceeded = 10\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.ScoreLimitExceeded = 2\n\tc.ScoreValid = 10\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.ScoreValid = 1\n\tc.ScoreNoAuth = 10\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.ScoreNoAuth = 2\n\tc.BanTime = 0\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.BanTime = 30\n\tc.BanTimeIncrement = 0\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.BanTimeIncrement = 50\n\tc.ObservationTime = 0\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.ObservationTime = 30\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.EntriesSoftLimit = 10\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.EntriesHardLimit = 10\n\terr = c.validate()\n\trequire.Error(t, err)\n\n\tc.EntriesHardLimit = 20\n\terr = c.validate()\n\trequire.NoError(t, err)\n\n\tc = DefenderConfig{\n\t\tEnabled:            true,\n\t\tScoreInvalid:       -1,\n\t\tScoreLimitExceeded: -1,\n\t\tScoreNoAuth:        -1,\n\t\tScoreValid:         -1,\n\t}\n\terr = c.validate()\n\trequire.Error(t, err)\n\tassert.Equal(t, 0, c.ScoreInvalid)\n\tassert.Equal(t, 0, c.ScoreValid)\n\tassert.Equal(t, 0, c.ScoreLimitExceeded)\n\tassert.Equal(t, 0, c.ScoreNoAuth)\n}\n\nfunc BenchmarkDefenderBannedSearch(b *testing.B) {\n\td := getDefenderForBench()\n\n\tip, ipnet, err := net.ParseCIDR(\"10.8.0.0/12\") // 1048574 ip addresses\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {\n\t\td.banned[ip.String()] = time.Now().Add(10 * time.Minute)\n\t}\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\td.IsBanned(\"192.168.1.1\", ProtocolSSH)\n\t}\n}\n\nfunc BenchmarkCleanup(b *testing.B) {\n\td := getDefenderForBench()\n\n\tip, ipnet, err := net.ParseCIDR(\"192.168.4.0/24\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tfor ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {\n\t\t\td.AddEvent(ip.String(), ProtocolSSH, HostEventLoginFailed)\n\t\t\tif d.countHosts() > d.config.EntriesHardLimit {\n\t\t\t\tpanic(\"too many hosts\")\n\t\t\t}\n\t\t\tif d.countBanned() > d.config.EntriesSoftLimit {\n\t\t\t\tpanic(\"too many ip banned\")\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc BenchmarkCIDRanger(b *testing.B) {\n\tranger := cidranger.NewPCTrieRanger()\n\tfor i := 0; i < 255; i++ {\n\t\tcidr := fmt.Sprintf(\"192.168.%d.1/24\", i)\n\t\t_, network, _ := net.ParseCIDR(cidr)\n\t\tif err := ranger.Insert(cidranger.NewBasicRangerEntry(*network)); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tipToMatch := net.ParseIP(\"192.167.1.2\")\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif _, err := ranger.Contains(ipToMatch); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc BenchmarkNetContains(b *testing.B) {\n\tvar nets []*net.IPNet\n\tfor i := 0; i < 255; i++ {\n\t\tcidr := fmt.Sprintf(\"192.168.%d.1/24\", i)\n\t\t_, network, _ := net.ParseCIDR(cidr)\n\t\tnets = append(nets, network)\n\t}\n\n\tipToMatch := net.ParseIP(\"192.167.1.1\")\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tfor _, n := range nets {\n\t\t\tn.Contains(ipToMatch)\n\t\t}\n\t}\n}\n\nfunc getDefenderForBench() *memoryDefender {\n\tconfig := &DefenderConfig{\n\t\tEnabled:          true,\n\t\tBanTime:          30,\n\t\tBanTimeIncrement: 50,\n\t\tThreshold:        10,\n\t\tScoreInvalid:     2,\n\t\tScoreValid:       2,\n\t\tObservationTime:  30,\n\t\tEntriesSoftLimit: 50,\n\t\tEntriesHardLimit: 100,\n\t}\n\treturn &memoryDefender{\n\t\tbaseDefender: baseDefender{\n\t\t\tconfig: config,\n\t\t},\n\t\thosts:  make(map[string]hostScore),\n\t\tbanned: make(map[string]time.Time),\n\t}\n}\n\nfunc inc(ip net.IP) {\n\tfor j := len(ip) - 1; j >= 0; j-- {\n\t\tip[j]++\n\t\tif ip[j] > 0 {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/common/defenderdb.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\ntype dbDefender struct {\n\tbaseDefender\n\tlastCleanup atomic.Int64\n}\n\nfunc newDBDefender(config *DefenderConfig) (Defender, error) {\n\terr := config.validate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tipList, err := dataprovider.NewIPList(dataprovider.IPListTypeDefender)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefender := &dbDefender{\n\t\tbaseDefender: baseDefender{\n\t\t\tconfig: config,\n\t\t\tipList: ipList,\n\t\t},\n\t}\n\tdefender.lastCleanup.Store(0)\n\n\treturn defender, nil\n}\n\n// GetHosts returns hosts that are banned or for which some violations have been detected\nfunc (d *dbDefender) GetHosts() ([]dataprovider.DefenderEntry, error) {\n\treturn dataprovider.GetDefenderHosts(d.getStartObservationTime(), d.config.EntriesHardLimit)\n}\n\n// GetHost returns a defender host by ip, if any\nfunc (d *dbDefender) GetHost(ip string) (dataprovider.DefenderEntry, error) {\n\treturn dataprovider.GetDefenderHostByIP(ip, d.getStartObservationTime())\n}\n\n// IsBanned returns true if the specified IP is banned\n// and increase ban time if the IP is found.\n// This method must be called as soon as the client connects\nfunc (d *dbDefender) IsBanned(ip, protocol string) bool {\n\tif d.isBanned(ip, protocol) {\n\t\treturn true\n\t}\n\n\t_, err := dataprovider.IsDefenderHostBanned(ip)\n\tif err != nil {\n\t\t// not found or another error, we allow this host\n\t\treturn false\n\t}\n\tincrement := d.config.BanTime * d.config.BanTimeIncrement / 100\n\tif increment == 0 {\n\t\tincrement++\n\t}\n\tdataprovider.UpdateDefenderBanTime(ip, increment) //nolint:errcheck\n\treturn true\n}\n\n// DeleteHost removes the specified IP from the defender lists\nfunc (d *dbDefender) DeleteHost(ip string) bool {\n\tif _, err := d.GetHost(ip); err != nil {\n\t\treturn false\n\t}\n\treturn dataprovider.DeleteDefenderHost(ip) == nil\n}\n\n// AddEvent adds an event for the given IP.\n// This method must be called for clients not yet banned.\n// Returns true if the IP is in the defender's safe list.\nfunc (d *dbDefender) AddEvent(ip, protocol string, event HostEvent) bool {\n\tif d.IsSafe(ip, protocol) {\n\t\treturn true\n\t}\n\n\tscore := d.getScore(event)\n\n\thost, err := dataprovider.AddDefenderEvent(ip, score, d.getStartObservationTime())\n\tif err != nil {\n\t\treturn false\n\t}\n\td.logEvent(ip, protocol, event, host.Score)\n\tif host.Score > d.config.Threshold {\n\t\td.logBan(ip, protocol)\n\t\tbanTime := time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)\n\t\terr = dataprovider.SetDefenderBanTime(ip, util.GetTimeAsMsSinceEpoch(banTime))\n\t\tif err == nil {\n\t\t\teventManager.handleIPBlockedEvent(EventParams{\n\t\t\t\tEvent:     ipBlockedEventName,\n\t\t\t\tIP:        ip,\n\t\t\t\tTimestamp: time.Now(),\n\t\t\t\tStatus:    1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif err == nil {\n\t\td.cleanup()\n\t}\n\treturn false\n}\n\n// GetBanTime returns the ban time for the given IP or nil if the IP is not banned\nfunc (d *dbDefender) GetBanTime(ip string) (*time.Time, error) {\n\thost, err := d.GetHost(ip)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif host.BanTime.IsZero() {\n\t\treturn nil, nil\n\t}\n\treturn &host.BanTime, nil\n}\n\n// GetScore returns the score for the given IP\nfunc (d *dbDefender) GetScore(ip string) (int, error) {\n\thost, err := d.GetHost(ip)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn host.Score, nil\n}\n\nfunc (d *dbDefender) cleanup() {\n\tlastCleanup := d.getLastCleanup()\n\tif lastCleanup.IsZero() || lastCleanup.Add(time.Duration(d.config.ObservationTime)*time.Minute*3).Before(time.Now()) {\n\t\t// FIXME: this could be racy in rare cases but it is better than acquire the lock for the cleanup duration\n\t\t// or to always acquire a read/write lock.\n\t\t// Concurrent cleanups could happen anyway from multiple SFTPGo instances and should not cause any issues\n\t\td.setLastCleanup(time.Now())\n\t\texpireTime := time.Now().Add(-time.Duration(d.config.ObservationTime+1) * time.Minute)\n\t\tlogger.Debug(logSender, \"\", \"cleanup defender hosts before %v, last cleanup %v\", expireTime, lastCleanup)\n\t\tif err := dataprovider.CleanupDefender(util.GetTimeAsMsSinceEpoch(expireTime)); err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"defender cleanup error, reset last cleanup to %v\", lastCleanup)\n\t\t\td.setLastCleanup(lastCleanup)\n\t\t}\n\t}\n}\n\nfunc (d *dbDefender) getStartObservationTime() int64 {\n\tt := time.Now().Add(-time.Duration(d.config.ObservationTime) * time.Minute)\n\treturn util.GetTimeAsMsSinceEpoch(t)\n}\n\nfunc (d *dbDefender) getLastCleanup() time.Time {\n\tval := d.lastCleanup.Load()\n\tif val == 0 {\n\t\treturn time.Time{}\n\t}\n\treturn util.GetTimeFromMsecSinceEpoch(val)\n}\n\nfunc (d *dbDefender) setLastCleanup(when time.Time) {\n\tif when.IsZero() {\n\t\td.lastCleanup.Store(0)\n\t\treturn\n\t}\n\td.lastCleanup.Store(util.GetTimeAsMsSinceEpoch(when))\n}\n"
  },
  {
    "path": "internal/common/defenderdb_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc TestBasicDbDefender(t *testing.T) {\n\tif !isDbDefenderSupported() {\n\t\tt.Skip(\"this test is not supported with the current database provider\")\n\t}\n\tentries := []dataprovider.IPListEntry{\n\t\t{\n\t\t\tIPOrNet: \"172.16.1.1/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"172.16.1.2/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"10.8.0.0/24\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeDeny,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"172.16.1.3/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"172.16.1.4/32\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t\t{\n\t\t\tIPOrNet: \"192.168.8.0/24\",\n\t\t\tType:    dataprovider.IPListTypeDefender,\n\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t},\n\t}\n\n\tfor idx := range entries {\n\t\te := entries[idx]\n\t\terr := dataprovider.AddIPListEntry(&e, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\n\tconfig := &DefenderConfig{\n\t\tEnabled:            true,\n\t\tBanTime:            10,\n\t\tBanTimeIncrement:   2,\n\t\tThreshold:          5,\n\t\tScoreInvalid:       2,\n\t\tScoreValid:         1,\n\t\tScoreNoAuth:        2,\n\t\tScoreLimitExceeded: 3,\n\t\tObservationTime:    15,\n\t\tEntriesSoftLimit:   1,\n\t\tEntriesHardLimit:   10,\n\t}\n\td, err := newDBDefender(config)\n\tassert.NoError(t, err)\n\tdefender := d.(*dbDefender)\n\tassert.True(t, defender.IsBanned(\"172.16.1.1\", ProtocolFTP))\n\tassert.False(t, defender.IsBanned(\"172.16.1.10\", ProtocolSSH))\n\tassert.False(t, defender.IsBanned(\"10.8.1.3\", ProtocolHTTP))\n\tassert.True(t, defender.IsBanned(\"10.8.0.4\", ProtocolWebDAV))\n\tassert.False(t, defender.IsBanned(\"invalid ip\", ProtocolSSH))\n\thosts, err := defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 0)\n\t_, err = defender.GetHost(\"10.8.0.3\")\n\tassert.Error(t, err)\n\n\tdefender.AddEvent(\"172.16.1.4\", ProtocolSSH, HostEventLoginFailed)\n\tdefender.AddEvent(\"192.168.8.4\", ProtocolSSH, HostEventUserNotFound)\n\tdefender.AddEvent(\"172.16.1.3\", ProtocolSSH, HostEventLimitExceeded)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 0)\n\tassert.True(t, defender.getLastCleanup().IsZero())\n\n\ttestIP := \"123.45.67.89\"\n\tdefender.AddEvent(testIP, ProtocolSSH, HostEventLoginFailed)\n\tlastCleanup := defender.getLastCleanup()\n\tassert.False(t, lastCleanup.IsZero())\n\tscore, err := defender.GetScore(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, score)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\tassert.Equal(t, 1, hosts[0].Score)\n\t\tassert.True(t, hosts[0].BanTime.IsZero())\n\t\tassert.Empty(t, hosts[0].GetBanTime())\n\t}\n\thost, err := defender.GetHost(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, host.Score)\n\tassert.Empty(t, host.GetBanTime())\n\tbanTime, err := defender.GetBanTime(testIP)\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\tdefender.AddEvent(testIP, ProtocolSSH, HostEventLimitExceeded)\n\tscore, err = defender.GetScore(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 4, score)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\tassert.Equal(t, 4, hosts[0].Score)\n\t\tassert.True(t, hosts[0].BanTime.IsZero())\n\t\tassert.Empty(t, hosts[0].GetBanTime())\n\t}\n\tdefender.AddEvent(testIP, ProtocolSSH, HostEventNoLoginTried)\n\tdefender.AddEvent(testIP, ProtocolSSH, HostEventNoLoginTried)\n\tscore, err = defender.GetScore(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, score)\n\tbanTime, err = defender.GetBanTime(testIP)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, banTime)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\tassert.Equal(t, 0, hosts[0].Score)\n\t\tassert.False(t, hosts[0].BanTime.IsZero())\n\t\tassert.NotEmpty(t, hosts[0].GetBanTime())\n\t\tassert.Equal(t, hex.EncodeToString([]byte(testIP)), hosts[0].GetID())\n\t}\n\thost, err = defender.GetHost(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, host.Score)\n\tassert.NotEmpty(t, host.GetBanTime())\n\t// ban time should increase\n\tassert.True(t, defender.IsBanned(testIP, ProtocolSSH))\n\tnewBanTime, err := defender.GetBanTime(testIP)\n\tassert.NoError(t, err)\n\tassert.True(t, newBanTime.After(*banTime))\n\n\tassert.True(t, defender.DeleteHost(testIP))\n\tassert.False(t, defender.DeleteHost(testIP))\n\t// test cleanup\n\ttestIP1 := \"123.45.67.90\"\n\ttestIP2 := \"123.45.67.91\"\n\ttestIP3 := \"123.45.67.92\"\n\tfor i := 0; i < 3; i++ {\n\t\tdefender.AddEvent(testIP, ProtocolSSH, HostEventUserNotFound)\n\t\tdefender.AddEvent(testIP1, ProtocolSSH, HostEventNoLoginTried)\n\t\tdefender.AddEvent(testIP2, ProtocolSSH, HostEventUserNotFound)\n\t}\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 3)\n\tfor _, host := range hosts {\n\t\tassert.Equal(t, 0, host.Score)\n\t\tassert.False(t, host.BanTime.IsZero())\n\t\tassert.NotEmpty(t, host.GetBanTime())\n\t}\n\tdefender.AddEvent(testIP3, ProtocolSSH, HostEventLoginFailed)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 4)\n\t// now set a ban time in the past, so the host will be cleanead up\n\tfor _, ip := range []string{testIP1, testIP2} {\n\t\terr = dataprovider.SetDefenderBanTime(ip, util.GetTimeAsMsSinceEpoch(time.Now().Add(-1*time.Minute)))\n\t\tassert.NoError(t, err)\n\t}\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 4)\n\tfor _, host := range hosts {\n\t\tswitch host.IP {\n\t\tcase testIP:\n\t\t\tassert.Equal(t, 0, host.Score)\n\t\t\tassert.False(t, host.BanTime.IsZero())\n\t\t\tassert.NotEmpty(t, host.GetBanTime())\n\t\tcase testIP3:\n\t\t\tassert.Equal(t, 1, host.Score)\n\t\t\tassert.True(t, host.BanTime.IsZero())\n\t\t\tassert.Empty(t, host.GetBanTime())\n\t\tdefault:\n\t\t\tassert.Equal(t, 6, host.Score)\n\t\t\tassert.True(t, host.BanTime.IsZero())\n\t\t\tassert.Empty(t, host.GetBanTime())\n\t\t}\n\t}\n\thost, err = defender.GetHost(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, host.Score)\n\tassert.False(t, host.BanTime.IsZero())\n\tassert.NotEmpty(t, host.GetBanTime())\n\thost, err = defender.GetHost(testIP3)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, host.Score)\n\tassert.True(t, host.BanTime.IsZero())\n\tassert.Empty(t, host.GetBanTime())\n\t// set a negative observation time so the from field in the queries will be in the future\n\t// we still should get the banned hosts\n\tdefender.config.ObservationTime = -2\n\tassert.Greater(t, defender.getStartObservationTime(), time.Now().UnixMilli())\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\tassert.Equal(t, testIP, hosts[0].IP)\n\t\tassert.Equal(t, 0, hosts[0].Score)\n\t\tassert.False(t, hosts[0].BanTime.IsZero())\n\t\tassert.NotEmpty(t, hosts[0].GetBanTime())\n\t}\n\t_, err = defender.GetHost(testIP)\n\tassert.NoError(t, err)\n\t// cleanup db\n\terr = dataprovider.CleanupDefender(util.GetTimeAsMsSinceEpoch(time.Now().Add(10 * time.Minute)))\n\tassert.NoError(t, err)\n\t// the banned host must still be there\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\tassert.Equal(t, testIP, hosts[0].IP)\n\t\tassert.Equal(t, 0, hosts[0].Score)\n\t\tassert.False(t, hosts[0].BanTime.IsZero())\n\t\tassert.NotEmpty(t, hosts[0].GetBanTime())\n\t}\n\t_, err = defender.GetHost(testIP)\n\tassert.NoError(t, err)\n\terr = dataprovider.SetDefenderBanTime(testIP, util.GetTimeAsMsSinceEpoch(time.Now().Add(-1*time.Minute)))\n\tassert.NoError(t, err)\n\terr = dataprovider.CleanupDefender(util.GetTimeAsMsSinceEpoch(time.Now().Add(10 * time.Minute)))\n\tassert.NoError(t, err)\n\thosts, err = defender.GetHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 0)\n\n\tfor _, e := range entries {\n\t\terr := dataprovider.DeleteIPListEntry(e.IPOrNet, e.Type, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestDbDefenderCleanup(t *testing.T) {\n\tif !isDbDefenderSupported() {\n\t\tt.Skip(\"this test is not supported with the current database provider\")\n\t}\n\tconfig := &DefenderConfig{\n\t\tEnabled:            true,\n\t\tBanTime:            10,\n\t\tBanTimeIncrement:   2,\n\t\tThreshold:          5,\n\t\tScoreInvalid:       2,\n\t\tScoreValid:         1,\n\t\tScoreLimitExceeded: 3,\n\t\tObservationTime:    15,\n\t\tEntriesSoftLimit:   1,\n\t\tEntriesHardLimit:   10,\n\t}\n\td, err := newDBDefender(config)\n\tassert.NoError(t, err)\n\tdefender := d.(*dbDefender)\n\tlastCleanup := defender.getLastCleanup()\n\tassert.True(t, lastCleanup.IsZero())\n\tdefender.cleanup()\n\tlastCleanup = defender.getLastCleanup()\n\tassert.False(t, lastCleanup.IsZero())\n\tdefender.cleanup()\n\tassert.Equal(t, lastCleanup, defender.getLastCleanup())\n\tdefender.setLastCleanup(time.Time{})\n\tassert.True(t, defender.getLastCleanup().IsZero())\n\tdefender.setLastCleanup(time.Now().Add(-time.Duration(config.ObservationTime) * time.Minute * 4))\n\ttime.Sleep(20 * time.Millisecond)\n\tdefender.cleanup()\n\tassert.True(t, lastCleanup.Before(defender.getLastCleanup()))\n\n\tproviderConf := dataprovider.GetProviderConfig()\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\n\tlastCleanup = util.GetTimeFromMsecSinceEpoch(time.Now().Add(-time.Duration(config.ObservationTime) * time.Minute * 4).UnixMilli())\n\tdefender.setLastCleanup(lastCleanup)\n\tdefender.cleanup()\n\t// cleanup will fail and so last cleanup should be reset to the previous value\n\tassert.Equal(t, lastCleanup, defender.getLastCleanup())\n\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc isDbDefenderSupported() bool {\n\t// SQLite shares the implementation with other SQL-based provider but it makes no sense\n\t// to use it outside test cases\n\tswitch dataprovider.GetProviderStatus().Driver {\n\tcase dataprovider.MySQLDataProviderName, dataprovider.PGSQLDataProviderName,\n\t\tdataprovider.CockroachDataProviderName, dataprovider.SQLiteDataProviderName:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/common/defendermem.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\ntype memoryDefender struct {\n\tbaseDefender\n\tsync.RWMutex\n\t// IP addresses of the clients trying to connected are stored inside hosts,\n\t// they are added to banned once the thresold is reached.\n\t// A violation from a banned host will increase the ban time\n\t// based on the configured BanTimeIncrement\n\thosts  map[string]hostScore // the key is the host IP\n\tbanned map[string]time.Time // the key is the host IP\n}\n\nfunc newInMemoryDefender(config *DefenderConfig) (Defender, error) {\n\terr := config.validate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tipList, err := dataprovider.NewIPList(dataprovider.IPListTypeDefender)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefender := &memoryDefender{\n\t\tbaseDefender: baseDefender{\n\t\t\tconfig: config,\n\t\t\tipList: ipList,\n\t\t},\n\t\thosts:  make(map[string]hostScore),\n\t\tbanned: make(map[string]time.Time),\n\t}\n\n\treturn defender, nil\n}\n\n// GetHosts returns hosts that are banned or for which some violations have been detected\nfunc (d *memoryDefender) GetHosts() ([]dataprovider.DefenderEntry, error) {\n\td.RLock()\n\tdefer d.RUnlock()\n\n\tvar result []dataprovider.DefenderEntry\n\tfor k, v := range d.banned {\n\t\tif v.After(time.Now()) {\n\t\t\tresult = append(result, dataprovider.DefenderEntry{\n\t\t\t\tIP:      k,\n\t\t\t\tBanTime: v,\n\t\t\t})\n\t\t}\n\t}\n\tfor k, v := range d.hosts {\n\t\tscore := 0\n\t\tfor _, event := range v.Events {\n\t\t\tif event.dateTime.Add(time.Duration(d.config.ObservationTime) * time.Minute).After(time.Now()) {\n\t\t\t\tscore += event.score\n\t\t\t}\n\t\t}\n\t\tif score > 0 {\n\t\t\tresult = append(result, dataprovider.DefenderEntry{\n\t\t\t\tIP:    k,\n\t\t\t\tScore: score,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// GetHost returns a defender host by ip, if any\nfunc (d *memoryDefender) GetHost(ip string) (dataprovider.DefenderEntry, error) {\n\td.RLock()\n\tdefer d.RUnlock()\n\n\tif banTime, ok := d.banned[ip]; ok {\n\t\tif banTime.After(time.Now()) {\n\t\t\treturn dataprovider.DefenderEntry{\n\t\t\t\tIP:      ip,\n\t\t\t\tBanTime: banTime,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\tif hs, ok := d.hosts[ip]; ok {\n\t\tscore := 0\n\t\tfor _, event := range hs.Events {\n\t\t\tif event.dateTime.Add(time.Duration(d.config.ObservationTime) * time.Minute).After(time.Now()) {\n\t\t\t\tscore += event.score\n\t\t\t}\n\t\t}\n\t\tif score > 0 {\n\t\t\treturn dataprovider.DefenderEntry{\n\t\t\t\tIP:    ip,\n\t\t\t\tScore: score,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn dataprovider.DefenderEntry{}, util.NewRecordNotFoundError(\"host not found\")\n}\n\n// IsBanned returns true if the specified IP is banned\n// and increase ban time if the IP is found.\n// This method must be called as soon as the client connects\nfunc (d *memoryDefender) IsBanned(ip, protocol string) bool {\n\td.RLock()\n\n\tif banTime, ok := d.banned[ip]; ok {\n\t\tif banTime.After(time.Now()) {\n\t\t\tincrement := d.config.BanTime * d.config.BanTimeIncrement / 100\n\t\t\tif increment == 0 {\n\t\t\t\tincrement++\n\t\t\t}\n\n\t\t\td.RUnlock()\n\n\t\t\t// we can save an earlier ban time if there are contemporary updates\n\t\t\t// but this should not make much difference. I prefer to hold a read lock\n\t\t\t// until possible for performance reasons, this method is called each\n\t\t\t// time a new client connects and it must be as fast as possible\n\t\t\td.Lock()\n\t\t\td.banned[ip] = banTime.Add(time.Duration(increment) * time.Minute)\n\t\t\td.Unlock()\n\n\t\t\treturn true\n\t\t}\n\t}\n\n\tdefer d.RUnlock()\n\n\treturn d.isBanned(ip, protocol)\n}\n\n// DeleteHost removes the specified IP from the defender lists\nfunc (d *memoryDefender) DeleteHost(ip string) bool {\n\td.Lock()\n\tdefer d.Unlock()\n\n\tif _, ok := d.banned[ip]; ok {\n\t\tdelete(d.banned, ip)\n\t\treturn true\n\t}\n\n\tif _, ok := d.hosts[ip]; ok {\n\t\tdelete(d.hosts, ip)\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// AddEvent adds an event for the given IP.\n// This method must be called for clients not yet banned.\n// Returns true if the IP is in the defender's safe list.\nfunc (d *memoryDefender) AddEvent(ip, protocol string, event HostEvent) bool {\n\tif d.IsSafe(ip, protocol) {\n\t\treturn true\n\t}\n\n\td.Lock()\n\tdefer d.Unlock()\n\n\t// ignore events for already banned hosts\n\tif v, ok := d.banned[ip]; ok {\n\t\tif v.After(time.Now()) {\n\t\t\treturn false\n\t\t}\n\t\tdelete(d.banned, ip)\n\t}\n\n\tscore := d.getScore(event)\n\n\tev := hostEvent{\n\t\tdateTime: time.Now(),\n\t\tscore:    score,\n\t}\n\n\tif hs, ok := d.hosts[ip]; ok {\n\t\ths.Events = append(hs.Events, ev)\n\t\ths.TotalScore = 0\n\n\t\tidx := 0\n\t\tfor _, event := range hs.Events {\n\t\t\tif event.dateTime.Add(time.Duration(d.config.ObservationTime) * time.Minute).After(time.Now()) {\n\t\t\t\ths.Events[idx] = event\n\t\t\t\ths.TotalScore += event.score\n\t\t\t\tidx++\n\t\t\t}\n\t\t}\n\t\td.logEvent(ip, protocol, event, hs.TotalScore)\n\n\t\ths.Events = hs.Events[:idx]\n\t\tif hs.TotalScore >= d.config.Threshold {\n\t\t\td.logBan(ip, protocol)\n\t\t\td.banned[ip] = time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)\n\t\t\tdelete(d.hosts, ip)\n\t\t\td.cleanupBanned()\n\t\t\teventManager.handleIPBlockedEvent(EventParams{\n\t\t\t\tEvent:     ipBlockedEventName,\n\t\t\t\tIP:        ip,\n\t\t\t\tTimestamp: time.Now(),\n\t\t\t\tStatus:    1,\n\t\t\t})\n\t\t} else {\n\t\t\td.hosts[ip] = hs\n\t\t}\n\t} else {\n\t\td.logEvent(ip, protocol, event, ev.score)\n\t\td.hosts[ip] = hostScore{\n\t\t\tTotalScore: ev.score,\n\t\t\tEvents:     []hostEvent{ev},\n\t\t}\n\t\td.cleanupHosts()\n\t}\n\treturn false\n}\n\nfunc (d *memoryDefender) countBanned() int {\n\td.RLock()\n\tdefer d.RUnlock()\n\n\treturn len(d.banned)\n}\n\nfunc (d *memoryDefender) countHosts() int {\n\td.RLock()\n\tdefer d.RUnlock()\n\n\treturn len(d.hosts)\n}\n\n// GetBanTime returns the ban time for the given IP or nil if the IP is not banned\nfunc (d *memoryDefender) GetBanTime(ip string) (*time.Time, error) {\n\td.RLock()\n\tdefer d.RUnlock()\n\n\tif banTime, ok := d.banned[ip]; ok {\n\t\treturn &banTime, nil\n\t}\n\n\treturn nil, nil\n}\n\n// GetScore returns the score for the given IP\nfunc (d *memoryDefender) GetScore(ip string) (int, error) {\n\td.RLock()\n\tdefer d.RUnlock()\n\n\tscore := 0\n\n\tif hs, ok := d.hosts[ip]; ok {\n\t\tfor _, event := range hs.Events {\n\t\t\tif event.dateTime.Add(time.Duration(d.config.ObservationTime) * time.Minute).After(time.Now()) {\n\t\t\t\tscore += event.score\n\t\t\t}\n\t\t}\n\t}\n\n\treturn score, nil\n}\n\nfunc (d *memoryDefender) cleanupBanned() {\n\tif len(d.banned) > d.config.EntriesHardLimit {\n\t\tkvList := make(kvList, 0, len(d.banned))\n\n\t\tfor k, v := range d.banned {\n\t\t\tif v.Before(time.Now()) {\n\t\t\t\tdelete(d.banned, k)\n\t\t\t}\n\n\t\t\tkvList = append(kvList, kv{\n\t\t\t\tKey:   k,\n\t\t\t\tValue: v.UnixNano(),\n\t\t\t})\n\t\t}\n\n\t\t// we removed expired ip addresses, if any, above, this could be enough\n\t\tnumToRemove := len(d.banned) - d.config.EntriesSoftLimit\n\n\t\tif numToRemove <= 0 {\n\t\t\treturn\n\t\t}\n\n\t\tsort.Sort(kvList)\n\n\t\tfor idx, kv := range kvList {\n\t\t\tif idx >= numToRemove {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tdelete(d.banned, kv.Key)\n\t\t}\n\t}\n}\n\nfunc (d *memoryDefender) cleanupHosts() {\n\tif len(d.hosts) > d.config.EntriesHardLimit {\n\t\tkvList := make(kvList, 0, len(d.hosts))\n\n\t\tfor k, v := range d.hosts {\n\t\t\tvalue := int64(0)\n\t\t\tif len(v.Events) > 0 {\n\t\t\t\tvalue = v.Events[len(v.Events)-1].dateTime.UnixNano()\n\t\t\t}\n\t\t\tkvList = append(kvList, kv{\n\t\t\t\tKey:   k,\n\t\t\t\tValue: value,\n\t\t\t})\n\t\t}\n\n\t\tsort.Sort(kvList)\n\n\t\tnumToRemove := len(d.hosts) - d.config.EntriesSoftLimit\n\n\t\tfor idx, kv := range kvList {\n\t\t\tif idx >= numToRemove {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tdelete(d.hosts, kv.Key)\n\t\t}\n\t}\n}\n\ntype kv struct {\n\tKey   string\n\tValue int64\n}\n\ntype kvList []kv\n\nfunc (p kvList) Len() int           { return len(p) }\nfunc (p kvList) Less(i, j int) bool { return p[i].Value < p[j].Value }\nfunc (p kvList) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }\n"
  },
  {
    "path": "internal/common/eventmanager.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/csv\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/bmatcuk/doublestar/v4\"\n\t\"github.com/klauspost/compress/zip\"\n\t\"github.com/robfig/cron/v3\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/wneessen/go-mail\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tipBlockedEventName       = \"IP Blocked\"\n\tmaxAttachmentsSize       = int64(10 * 1024 * 1024)\n\tobjDataPlaceholder       = \"{{.ObjectData}}\"\n\tobjDataPlaceholderString = \"{{.ObjectDataString}}\"\n\tdateTimeMillisFormat     = \"2006-01-02T15:04:05.000\"\n)\n\n// Supported IDP login events\nconst (\n\tIDPLoginUser  = \"IDP login user\"\n\tIDPLoginAdmin = \"IDP login admin\"\n)\n\nvar (\n\t// eventManager handle the supported event rules actions\n\teventManager          eventRulesContainer\n\tmultipartQuoteEscaper = strings.NewReplacer(\"\\\\\", \"\\\\\\\\\", `\"`, \"\\\\\\\"\")\n\tfsEventsWithSize      = []string{operationPreDelete, OperationPreUpload, operationDelete,\n\t\toperationCopy, operationDownload, operationFirstUpload, operationFirstDownload,\n\t\toperationUpload}\n)\n\nfunc init() {\n\teventManager = eventRulesContainer{\n\t\tschedulesMapping: make(map[string][]cron.EntryID),\n\t\t// arbitrary maximum number of concurrent asynchronous tasks,\n\t\t// each task could execute multiple actions\n\t\tconcurrencyGuard: make(chan struct{}, 200),\n\t}\n\tdataprovider.SetEventRulesCallbacks(eventManager.loadRules, eventManager.RemoveRule,\n\t\tfunc(operation, executor, ip, objectType, objectName, role string, object plugin.Renderer) {\n\t\t\tp := EventParams{\n\t\t\t\tName:       executor,\n\t\t\t\tObjectName: objectName,\n\t\t\t\tEvent:      operation,\n\t\t\t\tStatus:     1,\n\t\t\t\tObjectType: objectType,\n\t\t\t\tIP:         ip,\n\t\t\t\tRole:       role,\n\t\t\t\tTimestamp:  time.Now(),\n\t\t\t\tObject:     object,\n\t\t\t}\n\t\t\tif u, ok := object.(*dataprovider.User); ok {\n\t\t\t\tp.Email = u.Email\n\t\t\t\tp.Groups = u.Groups\n\t\t\t} else if a, ok := object.(*dataprovider.Admin); ok {\n\t\t\t\tp.Email = a.Email\n\t\t\t}\n\t\t\teventManager.handleProviderEvent(p)\n\t\t})\n}\n\n// HandleCertificateEvent checks and executes action rules for certificate events\nfunc HandleCertificateEvent(params EventParams) {\n\teventManager.handleCertificateEvent(params)\n}\n\n// HandleIDPLoginEvent executes actions defined for a successful login from an Identity Provider\nfunc HandleIDPLoginEvent(params EventParams, customFields *map[string]any) (*dataprovider.User, *dataprovider.Admin, error) {\n\treturn eventManager.handleIDPLoginEvent(params, customFields)\n}\n\n// eventRulesContainer stores event rules by trigger\ntype eventRulesContainer struct {\n\tsync.RWMutex\n\tlastLoad          atomic.Int64\n\tFsEvents          []dataprovider.EventRule\n\tProviderEvents    []dataprovider.EventRule\n\tSchedules         []dataprovider.EventRule\n\tIPBlockedEvents   []dataprovider.EventRule\n\tCertificateEvents []dataprovider.EventRule\n\tIPDLoginEvents    []dataprovider.EventRule\n\tschedulesMapping  map[string][]cron.EntryID\n\tconcurrencyGuard  chan struct{}\n}\n\nfunc (r *eventRulesContainer) addAsyncTask() {\n\tactiveHooks.Add(1)\n\tr.concurrencyGuard <- struct{}{}\n}\n\nfunc (r *eventRulesContainer) removeAsyncTask() {\n\tactiveHooks.Add(-1)\n\t<-r.concurrencyGuard\n}\n\nfunc (r *eventRulesContainer) getLastLoadTime() int64 {\n\treturn r.lastLoad.Load()\n}\n\nfunc (r *eventRulesContainer) setLastLoadTime(modTime int64) {\n\tr.lastLoad.Store(modTime)\n}\n\n// RemoveRule deletes the rule with the specified name\nfunc (r *eventRulesContainer) RemoveRule(name string) {\n\tr.Lock()\n\tdefer r.Unlock()\n\n\tr.removeRuleInternal(name)\n\teventManagerLog(logger.LevelDebug, \"event rules updated after delete, fs events: %d, provider events: %d, schedules: %d\",\n\t\tlen(r.FsEvents), len(r.ProviderEvents), len(r.Schedules))\n}\n\nfunc (r *eventRulesContainer) removeRuleInternal(name string) {\n\tfor idx := range r.FsEvents {\n\t\tif r.FsEvents[idx].Name == name {\n\t\t\tlastIdx := len(r.FsEvents) - 1\n\t\t\tr.FsEvents[idx] = r.FsEvents[lastIdx]\n\t\t\tr.FsEvents = r.FsEvents[:lastIdx]\n\t\t\teventManagerLog(logger.LevelDebug, \"removed rule %q from fs events\", name)\n\t\t\treturn\n\t\t}\n\t}\n\tfor idx := range r.ProviderEvents {\n\t\tif r.ProviderEvents[idx].Name == name {\n\t\t\tlastIdx := len(r.ProviderEvents) - 1\n\t\t\tr.ProviderEvents[idx] = r.ProviderEvents[lastIdx]\n\t\t\tr.ProviderEvents = r.ProviderEvents[:lastIdx]\n\t\t\teventManagerLog(logger.LevelDebug, \"removed rule %q from provider events\", name)\n\t\t\treturn\n\t\t}\n\t}\n\tfor idx := range r.IPBlockedEvents {\n\t\tif r.IPBlockedEvents[idx].Name == name {\n\t\t\tlastIdx := len(r.IPBlockedEvents) - 1\n\t\t\tr.IPBlockedEvents[idx] = r.IPBlockedEvents[lastIdx]\n\t\t\tr.IPBlockedEvents = r.IPBlockedEvents[:lastIdx]\n\t\t\teventManagerLog(logger.LevelDebug, \"removed rule %q from IP blocked events\", name)\n\t\t\treturn\n\t\t}\n\t}\n\tfor idx := range r.CertificateEvents {\n\t\tif r.CertificateEvents[idx].Name == name {\n\t\t\tlastIdx := len(r.CertificateEvents) - 1\n\t\t\tr.CertificateEvents[idx] = r.CertificateEvents[lastIdx]\n\t\t\tr.CertificateEvents = r.CertificateEvents[:lastIdx]\n\t\t\teventManagerLog(logger.LevelDebug, \"removed rule %q from certificate events\", name)\n\t\t\treturn\n\t\t}\n\t}\n\tfor idx := range r.IPDLoginEvents {\n\t\tif r.IPDLoginEvents[idx].Name == name {\n\t\t\tlastIdx := len(r.IPDLoginEvents) - 1\n\t\t\tr.IPDLoginEvents[idx] = r.IPDLoginEvents[lastIdx]\n\t\t\tr.IPDLoginEvents = r.IPDLoginEvents[:lastIdx]\n\t\t\teventManagerLog(logger.LevelDebug, \"removed rule %q from IDP login events\", name)\n\t\t\treturn\n\t\t}\n\t}\n\tfor idx := range r.Schedules {\n\t\tif r.Schedules[idx].Name == name {\n\t\t\tif schedules, ok := r.schedulesMapping[name]; ok {\n\t\t\t\tfor _, entryID := range schedules {\n\t\t\t\t\teventManagerLog(logger.LevelDebug, \"removing scheduled entry id %d for rule %q\", entryID, name)\n\t\t\t\t\teventScheduler.Remove(entryID)\n\t\t\t\t}\n\t\t\t\tdelete(r.schedulesMapping, name)\n\t\t\t}\n\n\t\t\tlastIdx := len(r.Schedules) - 1\n\t\t\tr.Schedules[idx] = r.Schedules[lastIdx]\n\t\t\tr.Schedules = r.Schedules[:lastIdx]\n\t\t\teventManagerLog(logger.LevelDebug, \"removed rule %q from scheduled events\", name)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (r *eventRulesContainer) addUpdateRuleInternal(rule dataprovider.EventRule) {\n\tr.removeRuleInternal(rule.Name)\n\tif rule.DeletedAt > 0 {\n\t\tdeletedAt := util.GetTimeFromMsecSinceEpoch(rule.DeletedAt)\n\t\tif deletedAt.Add(30 * time.Minute).Before(time.Now()) {\n\t\t\teventManagerLog(logger.LevelDebug, \"removing rule %q deleted at %s\", rule.Name, deletedAt)\n\t\t\tgo dataprovider.RemoveEventRule(rule) //nolint:errcheck\n\t\t}\n\t\treturn\n\t}\n\tif rule.Status != 1 || rule.Trigger == dataprovider.EventTriggerOnDemand {\n\t\treturn\n\t}\n\tswitch rule.Trigger {\n\tcase dataprovider.EventTriggerFsEvent:\n\t\tr.FsEvents = append(r.FsEvents, rule)\n\t\teventManagerLog(logger.LevelDebug, \"added rule %q to fs events\", rule.Name)\n\tcase dataprovider.EventTriggerProviderEvent:\n\t\tr.ProviderEvents = append(r.ProviderEvents, rule)\n\t\teventManagerLog(logger.LevelDebug, \"added rule %q to provider events\", rule.Name)\n\tcase dataprovider.EventTriggerIPBlocked:\n\t\tr.IPBlockedEvents = append(r.IPBlockedEvents, rule)\n\t\teventManagerLog(logger.LevelDebug, \"added rule %q to IP blocked events\", rule.Name)\n\tcase dataprovider.EventTriggerCertificate:\n\t\tr.CertificateEvents = append(r.CertificateEvents, rule)\n\t\teventManagerLog(logger.LevelDebug, \"added rule %q to certificate events\", rule.Name)\n\tcase dataprovider.EventTriggerIDPLogin:\n\t\tr.IPDLoginEvents = append(r.IPDLoginEvents, rule)\n\t\teventManagerLog(logger.LevelDebug, \"added rule %q to IDP login events\", rule.Name)\n\tcase dataprovider.EventTriggerSchedule:\n\t\tfor _, schedule := range rule.Conditions.Schedules {\n\t\t\tcronSpec := schedule.GetCronSpec()\n\t\t\tjob := &eventCronJob{\n\t\t\t\truleName: dataprovider.ConvertName(rule.Name),\n\t\t\t}\n\t\t\tentryID, err := eventScheduler.AddJob(cronSpec, job)\n\t\t\tif err != nil {\n\t\t\t\teventManagerLog(logger.LevelError, \"unable to add scheduled rule %q, cron string %q: %v\", rule.Name, cronSpec, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tr.schedulesMapping[rule.Name] = append(r.schedulesMapping[rule.Name], entryID)\n\t\t\teventManagerLog(logger.LevelDebug, \"schedule for rule %q added, id: %d, cron string %q, active scheduling rules: %d\",\n\t\t\t\trule.Name, entryID, cronSpec, len(r.schedulesMapping))\n\t\t}\n\t\tr.Schedules = append(r.Schedules, rule)\n\t\teventManagerLog(logger.LevelDebug, \"added rule %q to scheduled events\", rule.Name)\n\tdefault:\n\t\teventManagerLog(logger.LevelError, \"unsupported trigger: %d\", rule.Trigger)\n\t}\n}\n\nfunc (r *eventRulesContainer) loadRules() {\n\teventManagerLog(logger.LevelDebug, \"loading updated rules\")\n\tmodTime := util.GetTimeAsMsSinceEpoch(time.Now())\n\tlastLoadTime := r.getLastLoadTime()\n\trules, err := dataprovider.GetRecentlyUpdatedRules(lastLoadTime)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to load event rules: %v\", err)\n\t\treturn\n\t}\n\teventManagerLog(logger.LevelDebug, \"recently updated event rules loaded: %d\", len(rules))\n\n\tif len(rules) > 0 {\n\t\tr.Lock()\n\t\tdefer r.Unlock()\n\n\t\tfor _, rule := range rules {\n\t\t\tr.addUpdateRuleInternal(rule)\n\t\t}\n\t}\n\teventManagerLog(logger.LevelDebug, \"event rules updated, fs events: %d, provider events: %d, schedules: %d, ip blocked events: %d, certificate events: %d, IDP login events: %d\",\n\t\tlen(r.FsEvents), len(r.ProviderEvents), len(r.Schedules), len(r.IPBlockedEvents), len(r.CertificateEvents), len(r.IPDLoginEvents))\n\n\tr.setLastLoadTime(modTime)\n}\n\nfunc (*eventRulesContainer) checkIPDLoginEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {\n\tswitch conditions.IDPLoginEvent {\n\tcase dataprovider.IDPLoginUser:\n\t\tif params.Event != IDPLoginUser {\n\t\t\treturn false\n\t\t}\n\tcase dataprovider.IDPLoginAdmin:\n\t\tif params.Event != IDPLoginAdmin {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn checkEventConditionPatterns(params.Name, conditions.Options.Names)\n}\n\nfunc (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {\n\tif !slices.Contains(conditions.ProviderEvents, params.Event) {\n\t\treturn false\n\t}\n\tif !checkEventConditionPatterns(params.Name, conditions.Options.Names) {\n\t\treturn false\n\t}\n\tif !checkEventGroupConditionPatterns(params.Groups, conditions.Options.GroupNames) {\n\t\treturn false\n\t}\n\tif !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {\n\t\treturn false\n\t}\n\tif len(conditions.Options.ProviderObjects) > 0 && !slices.Contains(conditions.Options.ProviderObjects, params.ObjectType) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {\n\tif !slices.Contains(conditions.FsEvents, params.Event) {\n\t\treturn false\n\t}\n\tif !checkEventConditionPatterns(params.Name, conditions.Options.Names) {\n\t\treturn false\n\t}\n\tif !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {\n\t\treturn false\n\t}\n\tif !checkEventGroupConditionPatterns(params.Groups, conditions.Options.GroupNames) {\n\t\treturn false\n\t}\n\tif !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {\n\t\treturn false\n\t}\n\tif len(conditions.Options.Protocols) > 0 && !slices.Contains(conditions.Options.Protocols, params.Protocol) {\n\t\treturn false\n\t}\n\tif slices.Contains(fsEventsWithSize, params.Event) {\n\t\tif conditions.Options.MinFileSize > 0 {\n\t\t\tif params.FileSize < conditions.Options.MinFileSize {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\tif conditions.Options.MaxFileSize > 0 {\n\t\t\tif params.FileSize > conditions.Options.MaxFileSize {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n// hasFsRules returns true if there are any rules for filesystem event triggers\nfunc (r *eventRulesContainer) hasFsRules() bool {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\treturn len(r.FsEvents) > 0\n}\n\n// handleFsEvent executes the rules actions defined for the specified event.\n// The boolean parameter indicates whether a sync action was executed\nfunc (r *eventRulesContainer) handleFsEvent(params EventParams) (bool, error) {\n\tif params.Protocol == protocolEventAction {\n\t\treturn false, nil\n\t}\n\tr.RLock()\n\n\tvar rulesWithSyncActions, rulesAsync []dataprovider.EventRule\n\tfor _, rule := range r.FsEvents {\n\t\tif r.checkFsEventMatch(&rule.Conditions, &params) {\n\t\t\tif err := rule.CheckActionsConsistency(\"\"); err != nil {\n\t\t\t\teventManagerLog(logger.LevelWarn, \"rule %q skipped: %v, event %q\",\n\t\t\t\t\trule.Name, err, params.Event)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\thasSyncActions := false\n\t\t\tfor _, action := range rule.Actions {\n\t\t\t\tif action.Options.ExecuteSync {\n\t\t\t\t\thasSyncActions = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif hasSyncActions {\n\t\t\t\trulesWithSyncActions = append(rulesWithSyncActions, rule)\n\t\t\t} else {\n\t\t\t\trulesAsync = append(rulesAsync, rule)\n\t\t\t}\n\t\t}\n\t}\n\n\tr.RUnlock()\n\n\tparams.sender = params.Name\n\tparams.addUID()\n\tif len(rulesAsync) > 0 {\n\t\tgo executeAsyncRulesActions(rulesAsync, params)\n\t}\n\n\tif len(rulesWithSyncActions) > 0 {\n\t\treturn true, executeSyncRulesActions(rulesWithSyncActions, params)\n\t}\n\treturn false, nil\n}\n\nfunc (r *eventRulesContainer) handleIDPLoginEvent(params EventParams, customFields *map[string]any) (*dataprovider.User,\n\t*dataprovider.Admin, error,\n) {\n\tr.RLock()\n\n\tvar rulesWithSyncActions, rulesAsync []dataprovider.EventRule\n\tfor _, rule := range r.IPDLoginEvents {\n\t\tif r.checkIPDLoginEventMatch(&rule.Conditions, &params) {\n\t\t\tif err := rule.CheckActionsConsistency(\"\"); err != nil {\n\t\t\t\teventManagerLog(logger.LevelWarn, \"rule %q skipped: %v, event %q\",\n\t\t\t\t\trule.Name, err, params.Event)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\thasSyncActions := false\n\t\t\tfor _, action := range rule.Actions {\n\t\t\t\tif action.Options.ExecuteSync {\n\t\t\t\t\thasSyncActions = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif hasSyncActions {\n\t\t\t\trulesWithSyncActions = append(rulesWithSyncActions, rule)\n\t\t\t} else {\n\t\t\t\trulesAsync = append(rulesAsync, rule)\n\t\t\t}\n\t\t}\n\t}\n\n\tr.RUnlock()\n\n\tif len(rulesAsync) == 0 && len(rulesWithSyncActions) == 0 {\n\t\treturn nil, nil, nil\n\t}\n\n\tparams.addIDPCustomFields(customFields)\n\tif len(rulesWithSyncActions) > 1 {\n\t\tvar ruleNames []string\n\t\tfor _, r := range rulesWithSyncActions {\n\t\t\truleNames = append(ruleNames, r.Name)\n\t\t}\n\t\treturn nil, nil, fmt.Errorf(\"more than one account check action rules matches: %q\", strings.Join(ruleNames, \",\"))\n\t}\n\n\tparams.addUID()\n\tif len(rulesAsync) > 0 {\n\t\tgo executeAsyncRulesActions(rulesAsync, params)\n\t}\n\n\tif len(rulesWithSyncActions) > 0 {\n\t\treturn executeIDPAccountCheckRule(rulesWithSyncActions[0], params)\n\t}\n\treturn nil, nil, nil\n}\n\n// username is populated for user objects\nfunc (r *eventRulesContainer) handleProviderEvent(params EventParams) {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\tvar rules []dataprovider.EventRule\n\tfor _, rule := range r.ProviderEvents {\n\t\tif r.checkProviderEventMatch(&rule.Conditions, &params) {\n\t\t\tif err := rule.CheckActionsConsistency(params.ObjectType); err == nil {\n\t\t\t\trules = append(rules, rule)\n\t\t\t} else {\n\t\t\t\teventManagerLog(logger.LevelWarn, \"rule %q skipped: %v, event %q object type %q\",\n\t\t\t\t\trule.Name, err, params.Event, params.ObjectType)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(rules) > 0 {\n\t\tparams.sender = params.ObjectName\n\t\tgo executeAsyncRulesActions(rules, params)\n\t}\n}\n\nfunc (r *eventRulesContainer) handleIPBlockedEvent(params EventParams) {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\tif len(r.IPBlockedEvents) == 0 {\n\t\treturn\n\t}\n\tvar rules []dataprovider.EventRule\n\tfor _, rule := range r.IPBlockedEvents {\n\t\tif err := rule.CheckActionsConsistency(\"\"); err == nil {\n\t\t\trules = append(rules, rule)\n\t\t} else {\n\t\t\teventManagerLog(logger.LevelWarn, \"rule %q skipped: %v, event %q\",\n\t\t\t\trule.Name, err, params.Event)\n\t\t}\n\t}\n\n\tif len(rules) > 0 {\n\t\tgo executeAsyncRulesActions(rules, params)\n\t}\n}\n\nfunc (r *eventRulesContainer) handleCertificateEvent(params EventParams) {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\tif len(r.CertificateEvents) == 0 {\n\t\treturn\n\t}\n\tvar rules []dataprovider.EventRule\n\tfor _, rule := range r.CertificateEvents {\n\t\tif err := rule.CheckActionsConsistency(\"\"); err == nil {\n\t\t\trules = append(rules, rule)\n\t\t} else {\n\t\t\teventManagerLog(logger.LevelWarn, \"rule %q skipped: %v, event %q\",\n\t\t\t\trule.Name, err, params.Event)\n\t\t}\n\t}\n\n\tif len(rules) > 0 {\n\t\tgo executeAsyncRulesActions(rules, params)\n\t}\n}\n\ntype executedRetentionCheck struct {\n\tUsername   string\n\tActionName string\n\tResults    []folderRetentionCheckResult\n}\n\n// EventParams defines the supported event parameters\ntype EventParams struct {\n\tName                  string\n\tGroups                []sdk.GroupMapping\n\tEvent                 string\n\tStatus                int\n\tVirtualPath           string\n\tFsPath                string\n\tVirtualTargetPath     string\n\tFsTargetPath          string\n\tObjectName            string\n\tExtension             string\n\tObjectType            string\n\tFileSize              int64\n\tElapsed               int64\n\tProtocol              string\n\tIP                    string\n\tRole                  string\n\tEmail                 string\n\tTimestamp             time.Time\n\tUID                   string\n\tIDPCustomFields       *map[string]string\n\tObject                plugin.Renderer\n\tMetadata              map[string]string\n\tsender                string\n\tupdateStatusFromError bool\n\terrors                []string\n\tretentionChecks       []executedRetentionCheck\n}\n\nfunc (p *EventParams) getACopy() *EventParams {\n\tparams := *p\n\tparams.errors = make([]string, len(p.errors))\n\tcopy(params.errors, p.errors)\n\tretentionChecks := make([]executedRetentionCheck, 0, len(p.retentionChecks))\n\tfor _, c := range p.retentionChecks {\n\t\texecutedCheck := executedRetentionCheck{\n\t\t\tUsername:   c.Username,\n\t\t\tActionName: c.ActionName,\n\t\t}\n\t\texecutedCheck.Results = make([]folderRetentionCheckResult, len(c.Results))\n\t\tcopy(executedCheck.Results, c.Results)\n\t\tretentionChecks = append(retentionChecks, executedCheck)\n\t}\n\tparams.retentionChecks = retentionChecks\n\tif p.IDPCustomFields != nil {\n\t\tfields := make(map[string]string)\n\t\tfor k, v := range *p.IDPCustomFields {\n\t\t\tfields[k] = v\n\t\t}\n\t\tparams.IDPCustomFields = &fields\n\t}\n\tif len(params.Metadata) > 0 {\n\t\tmetadata := make(map[string]string)\n\t\tfor k, v := range p.Metadata {\n\t\t\tmetadata[k] = v\n\t\t}\n\t\tparams.Metadata = metadata\n\t}\n\n\treturn &params\n}\n\nfunc (p *EventParams) addIDPCustomFields(customFields *map[string]any) {\n\tif customFields == nil || len(*customFields) == 0 {\n\t\treturn\n\t}\n\n\tfields := make(map[string]string)\n\tfor k, v := range *customFields {\n\t\tswitch val := v.(type) {\n\t\tcase string:\n\t\t\tfields[k] = val\n\t\t}\n\t}\n\tp.IDPCustomFields = &fields\n}\n\n// AddError adds a new error to the event params and update the status if needed\nfunc (p *EventParams) AddError(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\tif p.updateStatusFromError && p.Status == 1 {\n\t\tp.Status = 2\n\t}\n\tp.errors = append(p.errors, err.Error())\n}\n\nfunc (p *EventParams) addUID() {\n\tif p.UID == \"\" {\n\t\tp.UID = util.GenerateUniqueID()\n\t}\n}\n\nfunc (p *EventParams) setBackupParams(backupPath string) {\n\tif p.sender != \"\" {\n\t\treturn\n\t}\n\tp.sender = dataprovider.ActionExecutorSystem\n\tp.FsPath = backupPath\n\tp.ObjectName = filepath.Base(backupPath)\n\tp.VirtualPath = \"/\" + p.ObjectName\n\tp.Timestamp = time.Now()\n\tinfo, err := os.Stat(backupPath)\n\tif err == nil {\n\t\tp.FileSize = info.Size()\n\t}\n}\n\nfunc (p *EventParams) getStatusString() string {\n\tswitch p.Status {\n\tcase 1:\n\t\treturn \"OK\"\n\tdefault:\n\t\treturn \"KO\"\n\t}\n}\n\n// getUsers returns users with group settings not applied\nfunc (p *EventParams) getUsers() ([]dataprovider.User, error) {\n\tif p.sender == \"\" {\n\t\tdump, err := dataprovider.DumpData([]string{dataprovider.DumpScopeUsers})\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"unable to get users: %+v\", err)\n\t\t\treturn nil, errors.New(\"unable to get users\")\n\t\t}\n\t\treturn dump.Users, nil\n\t}\n\tuser, err := p.getUserFromSender()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn []dataprovider.User{user}, nil\n}\n\nfunc (p *EventParams) getUserFromSender() (dataprovider.User, error) {\n\tif p.sender == dataprovider.ActionExecutorSystem {\n\t\treturn dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tStatus:   1,\n\t\t\t\tUsername: p.sender,\n\t\t\t\tHomeDir:  dataprovider.GetBackupsPath(),\n\t\t\t\tPermissions: map[string][]string{\n\t\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\t}\n\tuser, err := dataprovider.UserExists(p.sender, \"\")\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to get user %q: %+v\", p.sender, err)\n\t\treturn user, fmt.Errorf(\"error getting user %q\", p.sender)\n\t}\n\treturn user, nil\n}\n\nfunc (p *EventParams) getFolders() ([]vfs.BaseVirtualFolder, error) {\n\tif p.sender == \"\" {\n\t\tdump, err := dataprovider.DumpData([]string{dataprovider.DumpScopeFolders})\n\t\treturn dump.Folders, err\n\t}\n\tfolder, err := dataprovider.GetFolderByName(p.sender)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting folder %q: %w\", p.sender, err)\n\t}\n\treturn []vfs.BaseVirtualFolder{folder}, nil\n}\n\nfunc (p *EventParams) getCompressedDataRetentionReport() ([]byte, error) {\n\tif len(p.retentionChecks) == 0 {\n\t\treturn nil, errors.New(\"no data retention report available\")\n\t}\n\tvar b bytes.Buffer\n\tif _, err := p.writeCompressedDataRetentionReports(&b); err != nil {\n\t\treturn nil, err\n\t}\n\treturn b.Bytes(), nil\n}\n\nfunc (p *EventParams) writeCompressedDataRetentionReports(w io.Writer) (int64, error) {\n\tvar n int64\n\twr := zip.NewWriter(w)\n\n\tfor _, check := range p.retentionChecks {\n\t\tdata, err := getCSVRetentionReport(check.Results)\n\t\tif err != nil {\n\t\t\treturn n, fmt.Errorf(\"unable to get CSV report: %w\", err)\n\t\t}\n\t\tdataSize := int64(len(data))\n\t\tn += dataSize\n\t\t// we suppose a 3:1 compression ratio\n\t\tif n > (maxAttachmentsSize * 3) {\n\t\t\teventManagerLog(logger.LevelError, \"unable to get retention report, size too large: %s\",\n\t\t\t\tutil.ByteCountIEC(n))\n\t\t\treturn n, fmt.Errorf(\"unable to get retention report, size too large: %s\", util.ByteCountIEC(n))\n\t\t}\n\n\t\tfh := &zip.FileHeader{\n\t\t\tName:     fmt.Sprintf(\"%s-%s.csv\", check.ActionName, check.Username),\n\t\t\tMethod:   zip.Deflate,\n\t\t\tModified: time.Now().UTC(),\n\t\t}\n\t\tf, err := wr.CreateHeader(fh)\n\t\tif err != nil {\n\t\t\treturn n, fmt.Errorf(\"unable to create zip header for file %q: %w\", fh.Name, err)\n\t\t}\n\t\t_, err = io.CopyN(f, bytes.NewBuffer(data), dataSize)\n\t\tif err != nil {\n\t\t\treturn n, fmt.Errorf(\"unable to write content to zip file %q: %w\", fh.Name, err)\n\t\t}\n\t}\n\tif err := wr.Close(); err != nil {\n\t\treturn n, fmt.Errorf(\"unable to close zip writer: %w\", err)\n\t}\n\treturn n, nil\n}\n\nfunc (p *EventParams) getRetentionReportsAsMailAttachment() (*mail.File, error) {\n\tif len(p.retentionChecks) == 0 {\n\t\treturn nil, errors.New(\"no data retention report available\")\n\t}\n\treturn &mail.File{\n\t\tName:   \"retention-reports.zip\",\n\t\tHeader: make(map[string][]string),\n\t\tWriter: p.writeCompressedDataRetentionReports,\n\t}, nil\n}\n\nfunc (*EventParams) getStringReplacement(val string, escapeMode int) string {\n\tswitch escapeMode {\n\tcase 1:\n\t\treturn util.JSONEscape(val)\n\tcase 2:\n\t\treturn html.EscapeString(val)\n\tdefault:\n\t\treturn val\n\t}\n}\n\nfunc (p *EventParams) getStringReplacements(addObjectData bool, escapeMode int) []string {\n\tvar dateTimeString string\n\tif Config.TZ == \"local\" {\n\t\tdateTimeString = p.Timestamp.Local().Format(dateTimeMillisFormat)\n\t} else {\n\t\tdateTimeString = p.Timestamp.UTC().Format(dateTimeMillisFormat)\n\t}\n\tyear := dateTimeString[0:4]\n\tmonth := dateTimeString[5:7]\n\tday := dateTimeString[8:10]\n\thour := dateTimeString[11:13]\n\tminute := dateTimeString[14:16]\n\n\treplacements := []string{\n\t\t\"{{.Name}}\", p.getStringReplacement(p.Name, escapeMode),\n\t\t\"{{.Event}}\", p.Event,\n\t\t\"{{.Status}}\", fmt.Sprintf(\"%d\", p.Status),\n\t\t\"{{.VirtualPath}}\", p.getStringReplacement(p.VirtualPath, escapeMode),\n\t\t\"{{.EscapedVirtualPath}}\", p.getStringReplacement(url.QueryEscape(p.VirtualPath), escapeMode),\n\t\t\"{{.FsPath}}\", p.getStringReplacement(p.FsPath, escapeMode),\n\t\t\"{{.VirtualTargetPath}}\", p.getStringReplacement(p.VirtualTargetPath, escapeMode),\n\t\t\"{{.FsTargetPath}}\", p.getStringReplacement(p.FsTargetPath, escapeMode),\n\t\t\"{{.ObjectName}}\", p.getStringReplacement(p.ObjectName, escapeMode),\n\t\t\"{{.ObjectBaseName}}\", p.getStringReplacement(strings.TrimSuffix(p.ObjectName, p.Extension), escapeMode),\n\t\t\"{{.ObjectType}}\", p.ObjectType,\n\t\t\"{{.FileSize}}\", strconv.FormatInt(p.FileSize, 10),\n\t\t\"{{.Elapsed}}\", strconv.FormatInt(p.Elapsed, 10),\n\t\t\"{{.Protocol}}\", p.Protocol,\n\t\t\"{{.IP}}\", p.IP,\n\t\t\"{{.Role}}\", p.getStringReplacement(p.Role, escapeMode),\n\t\t\"{{.Email}}\", p.getStringReplacement(p.Email, escapeMode),\n\t\t\"{{.Timestamp}}\", strconv.FormatInt(p.Timestamp.UnixNano(), 10),\n\t\t\"{{.DateTime}}\", dateTimeString,\n\t\t\"{{.Year}}\", year,\n\t\t\"{{.Month}}\", month,\n\t\t\"{{.Day}}\", day,\n\t\t\"{{.Hour}}\", hour,\n\t\t\"{{.Minute}}\", minute,\n\t\t\"{{.StatusString}}\", p.getStatusString(),\n\t\t\"{{.UID}}\", p.getStringReplacement(p.UID, escapeMode),\n\t\t\"{{.Ext}}\", p.getStringReplacement(p.Extension, escapeMode),\n\t}\n\tif p.VirtualPath != \"\" {\n\t\treplacements = append(replacements, \"{{.VirtualDirPath}}\", p.getStringReplacement(path.Dir(p.VirtualPath), escapeMode))\n\t}\n\tif p.VirtualTargetPath != \"\" {\n\t\treplacements = append(replacements, \"{{.VirtualTargetDirPath}}\", p.getStringReplacement(path.Dir(p.VirtualTargetPath), escapeMode))\n\t\treplacements = append(replacements, \"{{.TargetName}}\", p.getStringReplacement(path.Base(p.VirtualTargetPath), escapeMode))\n\t}\n\tif len(p.errors) > 0 {\n\t\treplacements = append(replacements, \"{{.ErrorString}}\", p.getStringReplacement(strings.Join(p.errors, \", \"), escapeMode))\n\t} else {\n\t\treplacements = append(replacements, \"{{.ErrorString}}\", \"\")\n\t}\n\treplacements = append(replacements, objDataPlaceholder, \"{}\")\n\treplacements = append(replacements, objDataPlaceholderString, \"\")\n\tif addObjectData {\n\t\tdata, err := p.Object.RenderAsJSON(p.Event != operationDelete)\n\t\tif err == nil {\n\t\t\tdataString := util.BytesToString(data)\n\t\t\treplacements[len(replacements)-3] = p.getStringReplacement(dataString, 0)\n\t\t\treplacements[len(replacements)-1] = p.getStringReplacement(dataString, 1)\n\t\t}\n\t}\n\tif p.IDPCustomFields != nil {\n\t\tfor k, v := range *p.IDPCustomFields {\n\t\t\treplacements = append(replacements, fmt.Sprintf(\"{{.IDPField%s}}\", k), p.getStringReplacement(v, escapeMode))\n\t\t}\n\t}\n\treplacements = append(replacements, \"{{.Metadata}}\", \"{}\")\n\treplacements = append(replacements, \"{{.MetadataString}}\", \"\")\n\tif len(p.Metadata) > 0 {\n\t\tdata, err := json.Marshal(p.Metadata)\n\t\tif err == nil {\n\t\t\tdataString := util.BytesToString(data)\n\t\t\treplacements[len(replacements)-3] = p.getStringReplacement(dataString, 0)\n\t\t\treplacements[len(replacements)-1] = p.getStringReplacement(dataString, 1)\n\t\t}\n\t}\n\treturn replacements\n}\n\nfunc getCSVRetentionReport(results []folderRetentionCheckResult) ([]byte, error) {\n\tvar b bytes.Buffer\n\tcsvWriter := csv.NewWriter(&b)\n\terr := csvWriter.Write([]string{\"path\", \"retention (hours)\", \"deleted files\", \"deleted size (bytes)\",\n\t\t\"elapsed (ms)\", \"info\", \"error\"})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, result := range results {\n\t\terr = csvWriter.Write([]string{result.Path, strconv.Itoa(result.Retention), strconv.Itoa(result.DeletedFiles),\n\t\t\tstrconv.FormatInt(result.DeletedSize, 10), strconv.FormatInt(result.Elapsed.Milliseconds(), 10),\n\t\t\tresult.Info, result.Error})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tcsvWriter.Flush()\n\terr = csvWriter.Error()\n\treturn b.Bytes(), err\n}\n\nfunc closeWriterAndUpdateQuota(w io.WriteCloser, conn *BaseConnection, virtualSourcePath, virtualTargetPath string,\n\tnumFiles int, truncatedSize int64, errTransfer error, operation string, startTime time.Time,\n) error {\n\tvar fsDstPath string\n\tvar errDstFs error\n\terrWrite := w.Close()\n\ttargetPath := virtualSourcePath\n\tif virtualTargetPath != \"\" {\n\t\ttargetPath = virtualTargetPath\n\t\tvar fsDst vfs.Fs\n\t\tfsDst, fsDstPath, errDstFs = conn.GetFsAndResolvedPath(virtualTargetPath)\n\t\tif errTransfer != nil && errDstFs == nil {\n\t\t\t// try to remove a partial file on error. If this fails, we can't do anything\n\t\t\terrRemove := fsDst.Remove(fsDstPath, false)\n\t\t\tconn.Log(logger.LevelDebug, \"removing partial file %q after write error, result: %v\", virtualTargetPath, errRemove)\n\t\t}\n\t}\n\tinfo, err := conn.doStatInternal(targetPath, 0, false, false)\n\tif err == nil {\n\t\tupdateUserQuotaAfterFileWrite(conn, targetPath, numFiles, info.Size()-truncatedSize)\n\t\tvar fsSrcPath string\n\t\tvar errSrcFs error\n\t\tif virtualSourcePath != \"\" {\n\t\t\t_, fsSrcPath, errSrcFs = conn.GetFsAndResolvedPath(virtualSourcePath)\n\t\t}\n\t\tif errSrcFs == nil && errDstFs == nil {\n\t\t\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\t\t\tif errTransfer == nil {\n\t\t\t\terrTransfer = errWrite\n\t\t\t}\n\t\t\tif operation == operationCopy {\n\t\t\t\tlogger.CommandLog(copyLogSender, fsSrcPath, fsDstPath, conn.User.Username, \"\", conn.ID, conn.protocol, -1, -1,\n\t\t\t\t\t\"\", \"\", \"\", info.Size(), conn.localAddr, conn.remoteAddr, elapsed)\n\t\t\t}\n\t\t\tExecuteActionNotification(conn, operation, fsSrcPath, virtualSourcePath, fsDstPath, virtualTargetPath, \"\", info.Size(), errTransfer, elapsed, nil) //nolint:errcheck\n\t\t}\n\t} else {\n\t\teventManagerLog(logger.LevelWarn, \"unable to update quota after writing %q: %v\", targetPath, err)\n\t}\n\tif errTransfer != nil {\n\t\treturn errTransfer\n\t}\n\treturn errWrite\n}\n\nfunc updateUserQuotaAfterFileWrite(conn *BaseConnection, virtualPath string, numFiles int, fileSize int64) {\n\tvfolder, err := conn.User.GetVirtualFolderForPath(path.Dir(virtualPath))\n\tif err != nil {\n\t\tdataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck\n\t\treturn\n\t}\n\tdataprovider.UpdateUserFolderQuota(&vfolder, &conn.User, numFiles, fileSize, false)\n}\n\nfunc checkWriterPermsAndQuota(conn *BaseConnection, virtualPath string, numFiles int, expectedSize, truncatedSize int64) error {\n\tif numFiles == 0 {\n\t\tif !conn.User.HasPerm(dataprovider.PermOverwrite, path.Dir(virtualPath)) {\n\t\t\treturn conn.GetPermissionDeniedError()\n\t\t}\n\t} else {\n\t\tif !conn.User.HasPerm(dataprovider.PermUpload, path.Dir(virtualPath)) {\n\t\t\treturn conn.GetPermissionDeniedError()\n\t\t}\n\t}\n\tq, _ := conn.HasSpace(numFiles > 0, false, virtualPath)\n\tif !q.HasSpace {\n\t\treturn conn.GetQuotaExceededError()\n\t}\n\tif expectedSize != -1 {\n\t\tsizeDiff := expectedSize - truncatedSize\n\t\tif sizeDiff > 0 {\n\t\t\tremainingSize := q.GetRemainingSize()\n\t\t\tif remainingSize > 0 && remainingSize < sizeDiff {\n\t\t\t\treturn conn.GetQuotaExceededError()\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getFileWriter(conn *BaseConnection, virtualPath string, expectedSize int64) (io.WriteCloser, int, int64, func(), error) {\n\tfs, fsPath, err := conn.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn nil, 0, 0, nil, err\n\t}\n\tvar truncatedSize, fileSize int64\n\tnumFiles := 1\n\tisFileOverwrite := false\n\n\tinfo, err := fs.Lstat(fsPath)\n\tif err == nil {\n\t\tfileSize = info.Size()\n\t\tif info.IsDir() {\n\t\t\treturn nil, numFiles, truncatedSize, nil, fmt.Errorf(\"cannot write to a directory: %q\", virtualPath)\n\t\t}\n\t\tif info.Mode().IsRegular() {\n\t\t\tisFileOverwrite = true\n\t\t\ttruncatedSize = fileSize\n\t\t}\n\t\tnumFiles = 0\n\t}\n\tif err != nil && !fs.IsNotExist(err) {\n\t\treturn nil, numFiles, truncatedSize, nil, conn.GetFsError(fs, err)\n\t}\n\tif err := checkWriterPermsAndQuota(conn, virtualPath, numFiles, expectedSize, truncatedSize); err != nil {\n\t\treturn nil, numFiles, truncatedSize, nil, err\n\t}\n\tf, w, cancelFn, err := fs.Create(fsPath, 0, conn.GetCreateChecks(virtualPath, numFiles == 1, false))\n\tif err != nil {\n\t\treturn nil, numFiles, truncatedSize, nil, conn.GetFsError(fs, err)\n\t}\n\tvfs.SetPathPermissions(fs, fsPath, conn.User.GetUID(), conn.User.GetGID())\n\n\tif isFileOverwrite {\n\t\tif vfs.HasTruncateSupport(fs) || vfs.IsCryptOsFs(fs) {\n\t\t\tupdateUserQuotaAfterFileWrite(conn, virtualPath, numFiles, -fileSize)\n\t\t\ttruncatedSize = 0\n\t\t}\n\t}\n\tif cancelFn == nil {\n\t\tcancelFn = func() {}\n\t}\n\tif f != nil {\n\t\treturn f, numFiles, truncatedSize, cancelFn, nil\n\t}\n\treturn w, numFiles, truncatedSize, cancelFn, nil\n}\n\nfunc addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir string, info os.FileInfo, recursion int) error { //nolint:gocyclo\n\tif entryPath == wr.Name {\n\t\t// skip the archive itself\n\t\treturn nil\n\t}\n\tif recursion >= util.MaxRecursion {\n\t\teventManagerLog(logger.LevelError, \"unable to add zip entry %q, recursion too deep: %v\", entryPath, recursion)\n\t\treturn util.ErrRecursionTooDeep\n\t}\n\trecursion++\n\tvar err error\n\tif info == nil {\n\t\tinfo, err = conn.DoStat(entryPath, 1, false)\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"unable to add zip entry %q, stat error: %v\", entryPath, err)\n\t\t\treturn err\n\t\t}\n\t}\n\tentryName, err := getZipEntryName(entryPath, baseDir)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to get zip entry name: %v\", err)\n\t\treturn err\n\t}\n\tif _, ok := wr.Entries[entryName]; ok {\n\t\teventManagerLog(logger.LevelInfo, \"skipping duplicate zip entry %q, is dir %t\", entryPath, info.IsDir())\n\t\treturn nil\n\t}\n\twr.Entries[entryName] = true\n\tif info.IsDir() {\n\t\t_, err = wr.Writer.CreateHeader(&zip.FileHeader{\n\t\t\tName:     entryName + \"/\",\n\t\t\tMethod:   zip.Deflate,\n\t\t\tModified: info.ModTime(),\n\t\t})\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"unable to create zip entry %q: %v\", entryPath, err)\n\t\t\treturn fmt.Errorf(\"unable to create zip entry %q: %w\", entryPath, err)\n\t\t}\n\t\tlister, err := conn.ListDir(entryPath)\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"unable to add zip entry %q, get dir lister error: %v\", entryPath, err)\n\t\t\treturn fmt.Errorf(\"unable to add zip entry %q: %w\", entryPath, err)\n\t\t}\n\t\tdefer lister.Close()\n\n\t\tfor {\n\t\t\tcontents, err := lister.Next(vfs.ListerBatchSize)\n\t\t\tfinished := errors.Is(err, io.EOF)\n\t\t\tif err := lister.convertError(err); err != nil {\n\t\t\t\teventManagerLog(logger.LevelError, \"unable to add zip entry %q, read dir error: %v\", entryPath, err)\n\t\t\t\treturn fmt.Errorf(\"unable to add zip entry %q: %w\", entryPath, err)\n\t\t\t}\n\t\t\tfor _, info := range contents {\n\t\t\t\tfullPath := util.CleanPath(path.Join(entryPath, info.Name()))\n\t\t\t\tif err := addZipEntry(wr, conn, fullPath, baseDir, info, recursion); err != nil {\n\t\t\t\t\teventManagerLog(logger.LevelError, \"unable to add zip entry: %v\", err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif finished {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\tif !info.Mode().IsRegular() {\n\t\t// we only allow regular files\n\t\teventManagerLog(logger.LevelInfo, \"skipping zip entry for non regular file %q\", entryPath)\n\t\treturn nil\n\t}\n\n\treturn addFileToZip(wr, conn, entryPath, entryName, info.ModTime())\n}\n\nfunc addFileToZip(wr *zipWriterWrapper, conn *BaseConnection, entryPath, entryName string, modTime time.Time) error {\n\treader, cancelFn, err := getFileReader(conn, entryPath)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to add zip entry %q, cannot open file: %v\", entryPath, err)\n\t\treturn fmt.Errorf(\"unable to open %q: %w\", entryPath, err)\n\t}\n\tdefer cancelFn()\n\tdefer reader.Close()\n\n\tf, err := wr.Writer.CreateHeader(&zip.FileHeader{\n\t\tName:     entryName,\n\t\tMethod:   zip.Deflate,\n\t\tModified: modTime,\n\t})\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to create zip entry %q: %v\", entryPath, err)\n\t\treturn fmt.Errorf(\"unable to create zip entry %q: %w\", entryPath, err)\n\t}\n\t_, err = io.Copy(f, reader)\n\treturn err\n}\n\nfunc getZipEntryName(entryPath, baseDir string) (string, error) {\n\tif !strings.HasPrefix(entryPath, baseDir) {\n\t\treturn \"\", fmt.Errorf(\"entry path %q is outside base dir %q\", entryPath, baseDir)\n\t}\n\tentryPath = strings.TrimPrefix(entryPath, baseDir)\n\treturn strings.TrimPrefix(entryPath, \"/\"), nil\n}\n\nfunc getFileReader(conn *BaseConnection, virtualPath string) (io.ReadCloser, func(), error) {\n\tif !conn.User.HasPerm(dataprovider.PermDownload, path.Dir(virtualPath)) {\n\t\treturn nil, nil, conn.GetPermissionDeniedError()\n\t}\n\tfs, fsPath, err := conn.GetFsAndResolvedPath(virtualPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tf, r, cancelFn, err := fs.Open(fsPath, 0)\n\tif err != nil {\n\t\treturn nil, nil, conn.GetFsError(fs, err)\n\t}\n\tif cancelFn == nil {\n\t\tcancelFn = func() {}\n\t}\n\n\tif f != nil {\n\t\treturn f, cancelFn, nil\n\t}\n\treturn r, cancelFn, nil\n}\n\nfunc writeFileContent(conn *BaseConnection, virtualPath string, w io.Writer) error {\n\treader, cancelFn, err := getFileReader(conn, virtualPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer cancelFn()\n\tdefer reader.Close()\n\n\t_, err = io.Copy(w, reader)\n\treturn err\n}\n\nfunc getFileContentFn(conn *BaseConnection, virtualPath string, size int64) func(w io.Writer) (int64, error) {\n\treturn func(w io.Writer) (int64, error) {\n\t\treader, cancelFn, err := getFileReader(conn, virtualPath)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tdefer cancelFn()\n\t\tdefer reader.Close()\n\n\t\treturn io.CopyN(w, reader, size)\n\t}\n}\n\nfunc getMailAttachments(conn *BaseConnection, attachments []string, replacer *strings.Replacer) ([]*mail.File, error) {\n\tvar files []*mail.File\n\ttotalSize := int64(0)\n\n\tfor _, virtualPath := range replacePathsPlaceholders(attachments, replacer) {\n\t\tinfo, err := conn.DoStat(virtualPath, 0, false)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to get info for file %q, user %q: %w\", virtualPath, conn.User.Username, err)\n\t\t}\n\t\tif !info.Mode().IsRegular() {\n\t\t\treturn nil, fmt.Errorf(\"cannot attach non regular file %q\", virtualPath)\n\t\t}\n\t\ttotalSize += info.Size()\n\t\tif totalSize > maxAttachmentsSize {\n\t\t\treturn nil, fmt.Errorf(\"unable to send files as attachment, size too large: %s\", util.ByteCountIEC(totalSize))\n\t\t}\n\t\tfiles = append(files, &mail.File{\n\t\t\tName:   path.Base(virtualPath),\n\t\t\tHeader: make(map[string][]string),\n\t\t\tWriter: getFileContentFn(conn, virtualPath, info.Size()),\n\t\t})\n\t}\n\treturn files, nil\n}\n\nfunc replaceWithReplacer(input string, replacer *strings.Replacer) string {\n\tif !strings.Contains(input, \"{{.\") {\n\t\treturn input\n\t}\n\treturn replacer.Replace(input)\n}\n\nfunc checkEventConditionPattern(p dataprovider.ConditionPattern, name string) bool {\n\tvar matched bool\n\tvar err error\n\tif strings.Contains(p.Pattern, \"**\") {\n\t\tmatched, err = doublestar.Match(p.Pattern, name)\n\t} else {\n\t\tmatched, err = path.Match(p.Pattern, name)\n\t}\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"pattern matching error %q, err: %v\", p.Pattern, err)\n\t\treturn false\n\t}\n\tif p.InverseMatch {\n\t\treturn !matched\n\t}\n\treturn matched\n}\n\nfunc checkUserConditionOptions(user *dataprovider.User, conditions *dataprovider.ConditionOptions) bool {\n\tif !checkEventConditionPatterns(user.Username, conditions.Names) {\n\t\treturn false\n\t}\n\tif !checkEventConditionPatterns(user.Role, conditions.RoleNames) {\n\t\treturn false\n\t}\n\tif !checkEventGroupConditionPatterns(user.Groups, conditions.GroupNames) {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// checkEventConditionPatterns returns false if patterns are defined and no match is found\nfunc checkEventConditionPatterns(name string, patterns []dataprovider.ConditionPattern) bool {\n\tif len(patterns) == 0 {\n\t\treturn true\n\t}\n\tmatches := false\n\tfor _, p := range patterns {\n\t\t// assume, that multiple InverseMatches are set\n\t\tif p.InverseMatch {\n\t\t\tif checkEventConditionPattern(p, name) {\n\t\t\t\tmatches = true\n\t\t\t} else {\n\t\t\t\treturn false\n\t\t\t}\n\t\t} else if checkEventConditionPattern(p, name) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn matches\n}\n\nfunc checkEventGroupConditionPatterns(groups []sdk.GroupMapping, patterns []dataprovider.ConditionPattern) bool {\n\tif len(patterns) == 0 {\n\t\treturn true\n\t}\n\tmatches := false\n\tfor _, group := range groups {\n\t\tfor _, p := range patterns {\n\t\t\t// assume, that multiple InverseMatches are set\n\t\t\tif p.InverseMatch {\n\t\t\t\tif checkEventConditionPattern(p, group.Name) {\n\t\t\t\t\tmatches = true\n\t\t\t\t} else {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif checkEventConditionPattern(p, group.Name) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn matches\n}\n\nfunc getHTTPRuleActionEndpoint(c *dataprovider.EventActionHTTPConfig, replacer *strings.Replacer) (string, error) {\n\tu, err := url.Parse(c.Endpoint)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"invalid endpoint: %w\", err)\n\t}\n\tif strings.Contains(u.Path, \"{{.\") {\n\t\tpathComponents := strings.Split(u.Path, \"/\")\n\t\tfor idx := range pathComponents {\n\t\t\tpart := replaceWithReplacer(pathComponents[idx], replacer)\n\t\t\tif part != pathComponents[idx] {\n\t\t\t\tpathComponents[idx] = url.PathEscape(part)\n\t\t\t}\n\t\t}\n\t\tu.Path = \"\"\n\t\tu = u.JoinPath(pathComponents...)\n\t}\n\tif len(c.QueryParameters) > 0 {\n\t\tq := u.Query()\n\n\t\tfor _, keyVal := range c.QueryParameters {\n\t\t\tq.Add(keyVal.Key, replaceWithReplacer(keyVal.Value, replacer))\n\t\t}\n\n\t\tu.RawQuery = q.Encode()\n\t}\n\treturn u.String(), nil\n}\n\nfunc writeHTTPPart(m *multipart.Writer, part dataprovider.HTTPPart, h textproto.MIMEHeader,\n\tconn *BaseConnection, replacer *strings.Replacer, params *EventParams, addObjectData bool,\n) error {\n\tpartWriter, err := m.CreatePart(h)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to create part %q, err: %v\", part.Name, err)\n\t\treturn err\n\t}\n\tif part.Body != \"\" {\n\t\tcType := h.Get(\"Content-Type\")\n\t\tif strings.Contains(strings.ToLower(cType), \"application/json\") {\n\t\t\treplacements := params.getStringReplacements(addObjectData, 1)\n\t\t\tjsonReplacer := strings.NewReplacer(replacements...)\n\t\t\t_, err = partWriter.Write(util.StringToBytes(replaceWithReplacer(part.Body, jsonReplacer)))\n\t\t} else {\n\t\t\t_, err = partWriter.Write(util.StringToBytes(replaceWithReplacer(part.Body, replacer)))\n\t\t}\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"unable to write part %q, err: %v\", part.Name, err)\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\tif part.Filepath == dataprovider.RetentionReportPlaceHolder {\n\t\tdata, err := params.getCompressedDataRetentionReport()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = partWriter.Write(data)\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"unable to write part %q, err: %v\", part.Name, err)\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\terr = writeFileContent(conn, util.CleanPath(replacer.Replace(part.Filepath)), partWriter)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to write file part %q, err: %v\", part.Name, err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *strings.Replacer, //nolint:gocyclo\n\tcancel context.CancelFunc, user dataprovider.User, params *EventParams, addObjectData bool,\n) (io.Reader, string, error) {\n\tvar body io.Reader\n\tif c.Method == http.MethodGet {\n\t\treturn body, \"\", nil\n\t}\n\tif c.Body != \"\" {\n\t\tif c.Body == dataprovider.RetentionReportPlaceHolder {\n\t\t\tdata, err := params.getCompressedDataRetentionReport()\n\t\t\tif err != nil {\n\t\t\t\treturn body, \"\", err\n\t\t\t}\n\t\t\treturn bytes.NewBuffer(data), \"\", nil\n\t\t}\n\t\tif c.HasJSONBody() {\n\t\t\treplacements := params.getStringReplacements(addObjectData, 1)\n\t\t\tjsonReplacer := strings.NewReplacer(replacements...)\n\t\t\treturn bytes.NewBufferString(replaceWithReplacer(c.Body, jsonReplacer)), \"\", nil\n\t\t}\n\t\treturn bytes.NewBufferString(replaceWithReplacer(c.Body, replacer)), \"\", nil\n\t}\n\tif len(c.Parts) > 0 {\n\t\tr, w := io.Pipe()\n\t\tm := multipart.NewWriter(w)\n\n\t\tvar conn *BaseConnection\n\t\tif user.Username != \"\" {\n\t\t\tvar err error\n\t\t\tif err := getUserForEventAction(&user); err != nil {\n\t\t\t\treturn body, \"\", err\n\t\t\t}\n\t\t\tconnectionID := fmt.Sprintf(\"%s_%s\", protocolEventAction, xid.New().String())\n\t\t\terr = user.CheckFsRoot(connectionID)\n\t\t\tif err != nil {\n\t\t\t\tuser.CloseFs() //nolint:errcheck\n\t\t\t\treturn body, \"\", fmt.Errorf(\"error getting multipart file/s, unable to check root fs for user %q: %w\",\n\t\t\t\t\tuser.Username, err)\n\t\t\t}\n\t\t\tconn = NewBaseConnection(connectionID, protocolEventAction, \"\", \"\", user)\n\t\t}\n\n\t\tgo func() {\n\t\t\tdefer w.Close()\n\t\t\tdefer user.CloseFs() //nolint:errcheck\n\t\t\tif conn != nil {\n\t\t\t\tdefer conn.CloseFS() //nolint:errcheck\n\t\t\t}\n\n\t\t\tfor _, part := range c.Parts {\n\t\t\t\th := make(textproto.MIMEHeader)\n\t\t\t\tif part.Body != \"\" {\n\t\t\t\t\th.Set(\"Content-Disposition\", fmt.Sprintf(`form-data; name=\"%s\"`, multipartQuoteEscaper.Replace(part.Name)))\n\t\t\t\t} else {\n\t\t\t\t\th.Set(\"Content-Disposition\",\n\t\t\t\t\t\tfmt.Sprintf(`form-data; name=\"%s\"; filename=\"%s\"`,\n\t\t\t\t\t\t\tmultipartQuoteEscaper.Replace(part.Name),\n\t\t\t\t\t\t\tmultipartQuoteEscaper.Replace((path.Base(replaceWithReplacer(part.Filepath, replacer))))))\n\t\t\t\t\tcontentType := mime.TypeByExtension(path.Ext(part.Filepath))\n\t\t\t\t\tif contentType == \"\" {\n\t\t\t\t\t\tcontentType = \"application/octet-stream\"\n\t\t\t\t\t}\n\t\t\t\t\th.Set(\"Content-Type\", contentType)\n\t\t\t\t}\n\t\t\t\tfor _, keyVal := range part.Headers {\n\t\t\t\t\th.Set(keyVal.Key, replaceWithReplacer(keyVal.Value, replacer))\n\t\t\t\t}\n\t\t\t\tif err := writeHTTPPart(m, part, h, conn, replacer, params, addObjectData); err != nil {\n\t\t\t\t\tcancel()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tm.Close()\n\t\t}()\n\n\t\treturn r, m.FormDataContentType(), nil\n\t}\n\treturn body, \"\", nil\n}\n\nfunc setHTTPReqHeaders(req *http.Request, c *dataprovider.EventActionHTTPConfig, replacer *strings.Replacer,\n\tcontentType string,\n) {\n\tif contentType != \"\" {\n\t\treq.Header.Set(\"Content-Type\", contentType)\n\t}\n\tif c.Username != \"\" || c.Password.GetPayload() != \"\" {\n\t\treq.SetBasicAuth(replaceWithReplacer(c.Username, replacer), c.Password.GetPayload())\n\t}\n\tfor _, keyVal := range c.Headers {\n\t\treq.Header.Set(keyVal.Key, replaceWithReplacer(keyVal.Value, replacer))\n\t}\n}\n\nfunc executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventParams) error {\n\tif err := c.TryDecryptPassword(); err != nil {\n\t\treturn err\n\t}\n\taddObjectData := false\n\tif params.Object != nil {\n\t\taddObjectData = c.HasObjectData()\n\t}\n\n\treplacements := params.getStringReplacements(addObjectData, 0)\n\treplacer := strings.NewReplacer(replacements...)\n\tendpoint, err := getHTTPRuleActionEndpoint(&c, replacer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := c.GetContext()\n\tdefer cancel()\n\n\tvar user dataprovider.User\n\tif c.HasMultipartFiles() {\n\t\tuser, err = params.getUserFromSender()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tbody, contentType, err := getHTTPRuleActionBody(&c, replacer, cancel, user, params, addObjectData)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif body != nil {\n\t\trc, ok := body.(io.ReadCloser)\n\t\tif ok {\n\t\t\tdefer rc.Close()\n\t\t}\n\t}\n\treq, err := http.NewRequestWithContext(ctx, c.Method, endpoint, body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsetHTTPReqHeaders(req, &c, replacer, contentType)\n\n\tclient := c.GetHTTPClient()\n\tdefer client.CloseIdleConnections()\n\n\tstartTime := time.Now()\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelDebug, \"unable to send http notification, endpoint: %s, elapsed: %s, err: %v\",\n\t\t\tendpoint, time.Since(startTime), err)\n\t\treturn fmt.Errorf(\"error sending HTTP request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\teventManagerLog(logger.LevelDebug, \"http notification sent, endpoint: %s, elapsed: %s, status code: %d\",\n\t\tendpoint, time.Since(startTime), resp.StatusCode)\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent {\n\t\tif rb, err := io.ReadAll(io.LimitReader(resp.Body, 2048)); err == nil {\n\t\t\teventManagerLog(logger.LevelDebug, \"error notification response from endpoint %q: %s\",\n\t\t\t\tendpoint, rb)\n\t\t}\n\t\treturn fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\n\treturn nil\n}\n\nfunc executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *EventParams) error {\n\tif !dataprovider.IsActionCommandAllowed(c.Cmd) {\n\t\treturn fmt.Errorf(\"command %q is not allowed\", c.Cmd)\n\t}\n\taddObjectData := false\n\tif params.Object != nil {\n\t\tfor _, k := range c.EnvVars {\n\t\t\tif strings.Contains(k.Value, objDataPlaceholder) || strings.Contains(k.Value, objDataPlaceholderString) {\n\t\t\t\taddObjectData = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treplacements := params.getStringReplacements(addObjectData, 0)\n\treplacer := strings.NewReplacer(replacements...)\n\n\targs := make([]string, 0, len(c.Args))\n\tfor _, arg := range c.Args {\n\t\targs = append(args, replaceWithReplacer(arg, replacer))\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.Timeout)*time.Second)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, c.Cmd, args...)\n\tcmd.Env = []string{}\n\tfor _, keyVal := range c.EnvVars {\n\t\tif keyVal.Value == \"$\" && !strings.HasPrefix(strings.ToUpper(keyVal.Key), \"SFTPGO_\") {\n\t\t\tval := os.Getenv(keyVal.Key)\n\t\t\tif val == \"\" {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"empty value for environment variable %q\", keyVal.Key)\n\t\t\t}\n\t\t\tcmd.Env = append(cmd.Env, fmt.Sprintf(\"%s=%s\", keyVal.Key, val))\n\t\t} else {\n\t\t\tcmd.Env = append(cmd.Env, fmt.Sprintf(\"%s=%s\", keyVal.Key, replaceWithReplacer(keyVal.Value, replacer)))\n\t\t}\n\t}\n\n\tstartTime := time.Now()\n\terr := cmd.Run()\n\n\teventManagerLog(logger.LevelDebug, \"executed command %q, elapsed: %s, error: %v\",\n\t\tc.Cmd, time.Since(startTime), err)\n\n\treturn err\n}\n\nfunc getEmailAddressesWithReplacer(addrs []string, replacer *strings.Replacer) []string {\n\tif len(addrs) == 0 {\n\t\treturn nil\n\t}\n\trecipients := make([]string, 0, len(addrs))\n\tfor _, recipient := range addrs {\n\t\trcpt := replaceWithReplacer(recipient, replacer)\n\t\tif rcpt != \"\" {\n\t\t\trecipients = append(recipients, rcpt)\n\t\t}\n\t}\n\treturn recipients\n}\n\nfunc executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *EventParams) error {\n\taddObjectData := false\n\tif params.Object != nil {\n\t\tif strings.Contains(c.Body, objDataPlaceholder) || strings.Contains(c.Body, objDataPlaceholderString) {\n\t\t\taddObjectData = true\n\t\t}\n\t}\n\treplacements := params.getStringReplacements(addObjectData, 0)\n\treplacer := strings.NewReplacer(replacements...)\n\tvar body string\n\tif c.ContentType == 1 {\n\t\treplacements := params.getStringReplacements(addObjectData, 2)\n\t\tbodyReplacer := strings.NewReplacer(replacements...)\n\t\tbody = replaceWithReplacer(c.Body, bodyReplacer)\n\t} else {\n\t\tbody = replaceWithReplacer(c.Body, replacer)\n\t}\n\tsubject := replaceWithReplacer(c.Subject, replacer)\n\trecipients := getEmailAddressesWithReplacer(c.Recipients, replacer)\n\tbcc := getEmailAddressesWithReplacer(c.Bcc, replacer)\n\tstartTime := time.Now()\n\tvar files []*mail.File\n\tfileAttachments := make([]string, 0, len(c.Attachments))\n\tfor _, attachment := range c.Attachments {\n\t\tif attachment == dataprovider.RetentionReportPlaceHolder {\n\t\t\tf, err := params.getRetentionReportsAsMailAttachment()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfiles = append(files, f)\n\t\t\tcontinue\n\t\t}\n\t\tfileAttachments = append(fileAttachments, attachment)\n\t}\n\tif len(fileAttachments) > 0 {\n\t\tuser, err := params.getUserFromSender()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := getUserForEventAction(&user); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconnectionID := fmt.Sprintf(\"%s_%s\", protocolEventAction, xid.New().String())\n\t\terr = user.CheckFsRoot(connectionID)\n\t\tdefer user.CloseFs() //nolint:errcheck\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting email attachments, unable to check root fs for user %q: %w\", user.Username, err)\n\t\t}\n\t\tconn := NewBaseConnection(connectionID, protocolEventAction, \"\", \"\", user)\n\t\tdefer conn.CloseFS() //nolint:errcheck\n\n\t\tres, err := getMailAttachments(conn, fileAttachments, replacer)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfiles = append(files, res...)\n\t}\n\terr := smtp.SendEmail(recipients, bcc, subject, body, smtp.EmailContentType(c.ContentType), files...)\n\teventManagerLog(logger.LevelDebug, \"executed email notification action, elapsed: %s, error: %v\",\n\t\ttime.Since(startTime), err)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to send email: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc getUserForEventAction(user *dataprovider.User) error {\n\terr := user.LoadAndApplyGroupSettings()\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to get group for user %q: %+v\", user.Username, err)\n\t\treturn fmt.Errorf(\"unable to get groups for user %q\", user.Username)\n\t}\n\tuser.UploadDataTransfer = 0\n\tuser.UploadBandwidth = 0\n\tuser.DownloadBandwidth = 0\n\tuser.Filters.DisableFsChecks = false\n\tuser.Filters.FilePatterns = nil\n\tuser.Filters.BandwidthLimits = nil\n\tfor k := range user.Permissions {\n\t\tuser.Permissions[k] = []string{dataprovider.PermAny}\n\t}\n\treturn nil\n}\n\nfunc replacePathsPlaceholders(paths []string, replacer *strings.Replacer) []string {\n\tresults := make([]string, 0, len(paths))\n\tfor _, p := range paths {\n\t\tresults = append(results, util.CleanPath(replaceWithReplacer(p, replacer)))\n\t}\n\treturn util.RemoveDuplicates(results, false)\n}\n\nfunc executeDeleteFileFsAction(conn *BaseConnection, item string, info os.FileInfo) error {\n\tfs, fsPath, err := conn.GetFsAndResolvedPath(item)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn conn.RemoveFile(fs, fsPath, item, info)\n}\n\nfunc executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, user dataprovider.User) error {\n\tif err := getUserForEventAction(&user); err != nil {\n\t\treturn err\n\t}\n\tconnectionID := fmt.Sprintf(\"%s_%s\", protocolEventAction, xid.New().String())\n\terr := user.CheckFsRoot(connectionID)\n\tdefer user.CloseFs() //nolint:errcheck\n\tif err != nil {\n\t\treturn fmt.Errorf(\"delete error, unable to check root fs for user %q: %w\", user.Username, err)\n\t}\n\tconn := NewBaseConnection(connectionID, protocolEventAction, \"\", \"\", user)\n\tdefer conn.CloseFS() //nolint:errcheck\n\n\tfor _, item := range replacePathsPlaceholders(deletes, replacer) {\n\t\tinfo, err := conn.DoStat(item, 0, false)\n\t\tif err != nil {\n\t\t\tif conn.IsNotExistError(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"unable to check item to delete %q, user %q: %w\", item, user.Username, err)\n\t\t}\n\t\tif info.IsDir() {\n\t\t\tif err = conn.RemoveDir(item); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to remove dir %q, user %q: %w\", item, user.Username, err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err = executeDeleteFileFsAction(conn, item, info); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to remove file %q, user %q: %w\", item, user.Username, err)\n\t\t\t}\n\t\t}\n\t\teventManagerLog(logger.LevelDebug, \"item %q removed for user %q\", item, user.Username)\n\t}\n\treturn nil\n}\n\nfunc executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer,\n\tconditions dataprovider.ConditionOptions, params *EventParams,\n) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping fs delete for user %s, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif err = executeDeleteFsActionForUser(deletes, replacer, user); err != nil {\n\t\t\tparams.AddError(err)\n\t\t\tfailures = append(failures, user.Username)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"fs delete failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no delete executed\")\n\t\treturn errors.New(\"no delete executed\")\n\t}\n\treturn nil\n}\n\nfunc executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, user dataprovider.User) error {\n\tif err := getUserForEventAction(&user); err != nil {\n\t\treturn err\n\t}\n\tconnectionID := fmt.Sprintf(\"%s_%s\", protocolEventAction, xid.New().String())\n\terr := user.CheckFsRoot(connectionID)\n\tdefer user.CloseFs() //nolint:errcheck\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mkdir error, unable to check root fs for user %q: %w\", user.Username, err)\n\t}\n\tconn := NewBaseConnection(connectionID, protocolEventAction, \"\", \"\", user)\n\tdefer conn.CloseFS() //nolint:errcheck\n\n\tfor _, item := range replacePathsPlaceholders(dirs, replacer) {\n\t\tif err = conn.CheckParentDirs(path.Dir(item)); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to check parent dirs for %q, user %q: %w\", item, user.Username, err)\n\t\t}\n\t\tif err = conn.createDirIfMissing(item); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to create dir %q, user %q: %w\", item, user.Username, err)\n\t\t}\n\t\teventManagerLog(logger.LevelDebug, \"directory %q created for user %q\", item, user.Username)\n\t}\n\treturn nil\n}\n\nfunc executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,\n\tconditions dataprovider.ConditionOptions, params *EventParams,\n) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping fs mkdir for user %s, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif err = executeMkDirsFsActionForUser(dirs, replacer, user); err != nil {\n\t\t\tfailures = append(failures, user.Username)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"fs mkdir failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no mkdir executed\")\n\t\treturn errors.New(\"no mkdir executed\")\n\t}\n\treturn nil\n}\n\nfunc executeRenameFsActionForUser(renames []dataprovider.RenameConfig, replacer *strings.Replacer,\n\tuser dataprovider.User,\n) error {\n\tif err := getUserForEventAction(&user); err != nil {\n\t\treturn err\n\t}\n\tconnectionID := fmt.Sprintf(\"%s_%s\", protocolEventAction, xid.New().String())\n\terr := user.CheckFsRoot(connectionID)\n\tdefer user.CloseFs() //nolint:errcheck\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rename error, unable to check root fs for user %q: %w\", user.Username, err)\n\t}\n\tconn := NewBaseConnection(connectionID, protocolEventAction, \"\", \"\", user)\n\tdefer conn.CloseFS() //nolint:errcheck\n\n\tfor _, item := range renames {\n\t\tsource := util.CleanPath(replaceWithReplacer(item.Key, replacer))\n\t\ttarget := util.CleanPath(replaceWithReplacer(item.Value, replacer))\n\t\tchecks := 0\n\t\tif item.UpdateModTime {\n\t\t\tchecks += vfs.CheckUpdateModTime\n\t\t}\n\t\tif err = conn.renameInternal(source, target, true, checks); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to rename %q->%q, user %q: %w\", source, target, user.Username, err)\n\t\t}\n\t\teventManagerLog(logger.LevelDebug, \"rename %q->%q ok, user %q\", source, target, user.Username)\n\t}\n\treturn nil\n}\n\nfunc executeCopyFsActionForUser(keyVals []dataprovider.KeyValue, replacer *strings.Replacer,\n\tuser dataprovider.User,\n) error {\n\tif err := getUserForEventAction(&user); err != nil {\n\t\treturn err\n\t}\n\tconnectionID := fmt.Sprintf(\"%s_%s\", protocolEventAction, xid.New().String())\n\terr := user.CheckFsRoot(connectionID)\n\tdefer user.CloseFs() //nolint:errcheck\n\tif err != nil {\n\t\treturn fmt.Errorf(\"copy error, unable to check root fs for user %q: %w\", user.Username, err)\n\t}\n\tconn := NewBaseConnection(connectionID, protocolEventAction, \"\", \"\", user)\n\tdefer conn.CloseFS() //nolint:errcheck\n\n\tfor _, item := range keyVals {\n\t\tsource := util.CleanPath(replaceWithReplacer(item.Key, replacer))\n\t\ttarget := util.CleanPath(replaceWithReplacer(item.Value, replacer))\n\t\tif strings.HasSuffix(item.Key, \"/\") {\n\t\t\tsource += \"/\"\n\t\t}\n\t\tif strings.HasSuffix(item.Value, \"/\") {\n\t\t\ttarget += \"/\"\n\t\t}\n\t\tif err = conn.Copy(source, target); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to copy %q->%q, user %q: %w\", source, target, user.Username, err)\n\t\t}\n\t\teventManagerLog(logger.LevelDebug, \"copy %q->%q ok, user %q\", source, target, user.Username)\n\t}\n\treturn nil\n}\n\nfunc executeExistFsActionForUser(exist []string, replacer *strings.Replacer,\n\tuser dataprovider.User,\n) error {\n\tif err := getUserForEventAction(&user); err != nil {\n\t\treturn err\n\t}\n\tconnectionID := fmt.Sprintf(\"%s_%s\", protocolEventAction, xid.New().String())\n\terr := user.CheckFsRoot(connectionID)\n\tdefer user.CloseFs() //nolint:errcheck\n\tif err != nil {\n\t\treturn fmt.Errorf(\"existence check error, unable to check root fs for user %q: %w\", user.Username, err)\n\t}\n\tconn := NewBaseConnection(connectionID, protocolEventAction, \"\", \"\", user)\n\tdefer conn.CloseFS() //nolint:errcheck\n\n\tfor _, item := range replacePathsPlaceholders(exist, replacer) {\n\t\tif _, err = conn.DoStat(item, 0, false); err != nil {\n\t\t\treturn fmt.Errorf(\"error checking existence for path %q, user %q: %w\", item, user.Username, err)\n\t\t}\n\t\teventManagerLog(logger.LevelDebug, \"path %q exists for user %q\", item, user.Username)\n\t}\n\treturn nil\n}\n\nfunc executeRenameFsRuleAction(renames []dataprovider.RenameConfig, replacer *strings.Replacer,\n\tconditions dataprovider.ConditionOptions, params *EventParams,\n) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping fs rename for user %s, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif err = executeRenameFsActionForUser(renames, replacer, user); err != nil {\n\t\t\tfailures = append(failures, user.Username)\n\t\t\tparams.AddError(err)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"fs rename failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no rename executed\")\n\t\treturn errors.New(\"no rename executed\")\n\t}\n\treturn nil\n}\n\nfunc executeCopyFsRuleAction(keyVals []dataprovider.KeyValue, replacer *strings.Replacer,\n\tconditions dataprovider.ConditionOptions, params *EventParams,\n) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\tvar executed int\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping fs copy for user %s, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif err = executeCopyFsActionForUser(keyVals, replacer, user); err != nil {\n\t\t\tfailures = append(failures, user.Username)\n\t\t\tparams.AddError(err)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"fs copy failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no copy executed\")\n\t\treturn errors.New(\"no copy executed\")\n\t}\n\treturn nil\n}\n\nfunc getArchiveBaseDir(paths []string) string {\n\tvar parentDirs []string\n\tfor _, p := range paths {\n\t\tparentDirs = append(parentDirs, path.Dir(p))\n\t}\n\tparentDirs = util.RemoveDuplicates(parentDirs, false)\n\tbaseDir := \"/\"\n\tif len(parentDirs) == 1 {\n\t\tbaseDir = parentDirs[0]\n\t}\n\treturn baseDir\n}\n\nfunc getSizeForPath(conn *BaseConnection, p string, info os.FileInfo) (int64, error) {\n\tif info.IsDir() {\n\t\tvar dirSize int64\n\t\tlister, err := conn.ListDir(p)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tdefer lister.Close()\n\t\tfor {\n\t\t\tentries, err := lister.Next(vfs.ListerBatchSize)\n\t\t\tfinished := errors.Is(err, io.EOF)\n\t\t\tif err != nil && !finished {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tfor _, entry := range entries {\n\t\t\t\tsize, err := getSizeForPath(conn, path.Join(p, entry.Name()), entry)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn 0, err\n\t\t\t\t}\n\t\t\t\tdirSize += size\n\t\t\t}\n\t\t\tif finished {\n\t\t\t\treturn dirSize, nil\n\t\t\t}\n\t\t}\n\t}\n\tif info.Mode().IsRegular() {\n\t\treturn info.Size(), nil\n\t}\n\treturn 0, nil\n}\n\nfunc estimateZipSize(conn *BaseConnection, zipPath string, paths []string) (int64, error) {\n\tq, _ := conn.HasSpace(false, false, zipPath)\n\tif q.HasSpace && q.GetRemainingSize() > 0 {\n\t\tvar size int64\n\t\tfor _, item := range paths {\n\t\t\tinfo, err := conn.DoStat(item, 1, false)\n\t\t\tif err != nil {\n\t\t\t\treturn size, err\n\t\t\t}\n\t\t\titemSize, err := getSizeForPath(conn, item, info)\n\t\t\tif err != nil {\n\t\t\t\treturn size, err\n\t\t\t}\n\t\t\tsize += itemSize\n\t\t}\n\t\teventManagerLog(logger.LevelDebug, \"archive paths %v, archive name %q, size: %d\", paths, zipPath, size)\n\t\t// we assume the zip size will be half of the real size\n\t\treturn size / 2, nil\n\t}\n\treturn -1, nil\n}\n\nfunc executeCompressFsActionForUser(c dataprovider.EventActionFsCompress, replacer *strings.Replacer,\n\tuser dataprovider.User,\n) error {\n\tif err := getUserForEventAction(&user); err != nil {\n\t\treturn err\n\t}\n\tconnectionID := fmt.Sprintf(\"%s_%s\", protocolEventAction, xid.New().String())\n\terr := user.CheckFsRoot(connectionID)\n\tdefer user.CloseFs() //nolint:errcheck\n\tif err != nil {\n\t\treturn fmt.Errorf(\"compress error, unable to check root fs for user %q: %w\", user.Username, err)\n\t}\n\tconn := NewBaseConnection(connectionID, protocolEventAction, \"\", \"\", user)\n\tdefer conn.CloseFS() //nolint:errcheck\n\n\tname := util.CleanPath(replaceWithReplacer(c.Name, replacer))\n\tconn.CheckParentDirs(path.Dir(name)) //nolint:errcheck\n\tpaths := make([]string, 0, len(c.Paths))\n\tfor idx := range c.Paths {\n\t\tp := util.CleanPath(replaceWithReplacer(c.Paths[idx], replacer))\n\t\tif p == name {\n\t\t\treturn fmt.Errorf(\"cannot compress the archive to create: %q\", name)\n\t\t}\n\t\tpaths = append(paths, p)\n\t}\n\tpaths = util.RemoveDuplicates(paths, false)\n\testimatedSize, err := estimateZipSize(conn, name, paths)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to estimate size for archive %q: %v\", name, err)\n\t\treturn fmt.Errorf(\"unable to estimate archive size: %w\", err)\n\t}\n\twriter, numFiles, truncatedSize, cancelFn, err := getFileWriter(conn, name, estimatedSize)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to create archive %q: %v\", name, err)\n\t\treturn fmt.Errorf(\"unable to create archive: %w\", err)\n\t}\n\tdefer cancelFn()\n\n\tbaseDir := getArchiveBaseDir(paths)\n\teventManagerLog(logger.LevelDebug, \"creating archive %q for paths %+v\", name, paths)\n\n\tzipWriter := &zipWriterWrapper{\n\t\tName:    name,\n\t\tWriter:  zip.NewWriter(writer),\n\t\tEntries: make(map[string]bool),\n\t}\n\tstartTime := time.Now()\n\tfor _, item := range paths {\n\t\tif err := addZipEntry(zipWriter, conn, item, baseDir, nil, 0); err != nil {\n\t\t\tcloseWriterAndUpdateQuota(writer, conn, name, \"\", numFiles, truncatedSize, err, operationUpload, startTime) //nolint:errcheck\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := zipWriter.Writer.Close(); err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to close zip file %q: %v\", name, err)\n\t\tcloseWriterAndUpdateQuota(writer, conn, name, \"\", numFiles, truncatedSize, err, operationUpload, startTime) //nolint:errcheck\n\t\treturn fmt.Errorf(\"unable to close zip file %q: %w\", name, err)\n\t}\n\treturn closeWriterAndUpdateQuota(writer, conn, name, \"\", numFiles, truncatedSize, err, operationUpload, startTime)\n}\n\nfunc executeExistFsRuleAction(exist []string, replacer *strings.Replacer, conditions dataprovider.ConditionOptions,\n\tparams *EventParams,\n) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping fs exist for user %s, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif err = executeExistFsActionForUser(exist, replacer, user); err != nil {\n\t\t\tfailures = append(failures, user.Username)\n\t\t\tparams.AddError(err)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"fs existence check failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no existence check executed\")\n\t\treturn errors.New(\"no existence check executed\")\n\t}\n\treturn nil\n}\n\nfunc executeCompressFsRuleAction(c dataprovider.EventActionFsCompress, replacer *strings.Replacer,\n\tconditions dataprovider.ConditionOptions, params *EventParams,\n) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping fs compress for user %s, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif err = executeCompressFsActionForUser(c, replacer, user); err != nil {\n\t\t\tfailures = append(failures, user.Username)\n\t\t\tparams.AddError(err)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"fs compress failed for users: %s\", strings.Join(failures, \",\"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no file/folder compressed\")\n\t\treturn errors.New(\"no file/folder compressed\")\n\t}\n\treturn nil\n}\n\nfunc executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions dataprovider.ConditionOptions,\n\tparams *EventParams,\n) error {\n\taddObjectData := false\n\treplacements := params.getStringReplacements(addObjectData, 0)\n\treplacer := strings.NewReplacer(replacements...)\n\tswitch c.Type {\n\tcase dataprovider.FilesystemActionRename:\n\t\treturn executeRenameFsRuleAction(c.Renames, replacer, conditions, params)\n\tcase dataprovider.FilesystemActionDelete:\n\t\treturn executeDeleteFsRuleAction(c.Deletes, replacer, conditions, params)\n\tcase dataprovider.FilesystemActionMkdirs:\n\t\treturn executeMkdirFsRuleAction(c.MkDirs, replacer, conditions, params)\n\tcase dataprovider.FilesystemActionExist:\n\t\treturn executeExistFsRuleAction(c.Exist, replacer, conditions, params)\n\tcase dataprovider.FilesystemActionCompress:\n\t\treturn executeCompressFsRuleAction(c.Compress, replacer, conditions, params)\n\tcase dataprovider.FilesystemActionCopy:\n\t\treturn executeCopyFsRuleAction(c.Copy, replacer, conditions, params)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported filesystem action %d\", c.Type)\n\t}\n}\n\nfunc executeQuotaResetForUser(user *dataprovider.User) error {\n\tif err := user.LoadAndApplyGroupSettings(); err != nil {\n\t\teventManagerLog(logger.LevelError, \"skipping scheduled quota reset for user %s, cannot apply group settings: %v\",\n\t\t\tuser.Username, err)\n\t\treturn err\n\t}\n\tif !QuotaScans.AddUserQuotaScan(user.Username, user.Role) {\n\t\teventManagerLog(logger.LevelError, \"another quota scan is already in progress for user %q\", user.Username)\n\t\treturn fmt.Errorf(\"another quota scan is in progress for user %q\", user.Username)\n\t}\n\tdefer QuotaScans.RemoveUserQuotaScan(user.Username)\n\n\tnumFiles, size, err := user.ScanQuota()\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"error scanning quota for user %q: %v\", user.Username, err)\n\t\treturn fmt.Errorf(\"error scanning quota for user %q: %w\", user.Username, err)\n\t}\n\terr = dataprovider.UpdateUserQuota(user, numFiles, size, true)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"error updating quota for user %q: %v\", user.Username, err)\n\t\treturn fmt.Errorf(\"error updating quota for user %q: %w\", user.Username, err)\n\t}\n\treturn nil\n}\n\nfunc executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping quota reset for user %q, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif err = executeQuotaResetForUser(&user); err != nil {\n\t\t\tparams.AddError(err)\n\t\t\tfailures = append(failures, user.Username)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"quota reset failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no user quota reset executed\")\n\t\treturn errors.New(\"no user quota reset executed\")\n\t}\n\treturn nil\n}\n\nfunc executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error {\n\tfolders, err := params.getFolders()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get folders: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, folder := range folders {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" && !checkEventConditionPatterns(folder.Name, conditions.Names) {\n\t\t\teventManagerLog(logger.LevelDebug, \"skipping scheduled quota reset for folder %s, name conditions don't match\",\n\t\t\t\tfolder.Name)\n\t\t\tcontinue\n\t\t}\n\t\tif !QuotaScans.AddVFolderQuotaScan(folder.Name) {\n\t\t\teventManagerLog(logger.LevelError, \"another quota scan is already in progress for folder %q\", folder.Name)\n\t\t\tparams.AddError(fmt.Errorf(\"another quota scan is already in progress for folder %q\", folder.Name))\n\t\t\tfailures = append(failures, folder.Name)\n\t\t\tcontinue\n\t\t}\n\t\texecuted++\n\t\tf := vfs.VirtualFolder{\n\t\t\tBaseVirtualFolder: folder,\n\t\t\tVirtualPath:       \"/\",\n\t\t}\n\t\tnumFiles, size, err := f.ScanQuota()\n\t\tQuotaScans.RemoveVFolderQuotaScan(folder.Name)\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"error scanning quota for folder %q: %v\", folder.Name, err)\n\t\t\tparams.AddError(fmt.Errorf(\"error scanning quota for folder %q: %w\", folder.Name, err))\n\t\t\tfailures = append(failures, folder.Name)\n\t\t\tcontinue\n\t\t}\n\t\terr = dataprovider.UpdateVirtualFolderQuota(&folder, numFiles, size, true)\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"error updating quota for folder %q: %v\", folder.Name, err)\n\t\t\tparams.AddError(fmt.Errorf(\"error updating quota for folder %q: %w\", folder.Name, err))\n\t\t\tfailures = append(failures, folder.Name)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"quota reset failed for folders: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no folder quota reset executed\")\n\t\treturn errors.New(\"no folder quota reset executed\")\n\t}\n\treturn nil\n}\n\nfunc executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping scheduled transfer quota reset for user %s, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\terr = dataprovider.UpdateUserTransferQuota(&user, 0, 0, true)\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelError, \"error updating transfer quota for user %q: %v\", user.Username, err)\n\t\t\tparams.AddError(fmt.Errorf(\"error updating transfer quota for user %q: %w\", user.Username, err))\n\t\t\tfailures = append(failures, user.Username)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"transfer quota reset failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no transfer quota reset executed\")\n\t\treturn errors.New(\"no transfer quota reset executed\")\n\t}\n\treturn nil\n}\n\nfunc executeDataRetentionCheckForUser(user dataprovider.User, folders []dataprovider.FolderRetention,\n\tparams *EventParams, actionName string,\n) error {\n\tif err := user.LoadAndApplyGroupSettings(); err != nil {\n\t\teventManagerLog(logger.LevelError, \"skipping scheduled retention check for user %s, cannot apply group settings: %v\",\n\t\t\tuser.Username, err)\n\t\treturn err\n\t}\n\tcheck := RetentionCheck{\n\t\tFolders: folders,\n\t}\n\tc := RetentionChecks.Add(check, &user)\n\tif c == nil {\n\t\teventManagerLog(logger.LevelError, \"another retention check is already in progress for user %q\", user.Username)\n\t\treturn fmt.Errorf(\"another retention check is in progress for user %q\", user.Username)\n\t}\n\tdefer func() {\n\t\tparams.retentionChecks = append(params.retentionChecks, executedRetentionCheck{\n\t\t\tUsername:   user.Username,\n\t\t\tActionName: actionName,\n\t\t\tResults:    c.results,\n\t\t})\n\t}()\n\tif err := c.Start(); err != nil {\n\t\teventManagerLog(logger.LevelError, \"error checking retention for user %q: %v\", user.Username, err)\n\t\treturn fmt.Errorf(\"error checking retention for user %q: %w\", user.Username, err)\n\t}\n\treturn nil\n}\n\nfunc executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRetentionConfig,\n\tconditions dataprovider.ConditionOptions, params *EventParams, actionName string,\n) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\texecuted := 0\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping scheduled retention check for user %s, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif err = executeDataRetentionCheckForUser(user, config.Folders, params, actionName); err != nil {\n\t\t\tfailures = append(failures, user.Username)\n\t\t\tparams.AddError(err)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"retention check failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no retention check executed\")\n\t\treturn errors.New(\"no retention check executed\")\n\t}\n\treturn nil\n}\n\nfunc executeUserExpirationCheckRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\tvar executed int\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping expiration check for user %q, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\texecuted++\n\t\tif user.ExpirationDate > 0 {\n\t\t\texpDate := util.GetTimeFromMsecSinceEpoch(user.ExpirationDate)\n\t\t\tif expDate.Before(time.Now()) {\n\t\t\t\tfailures = append(failures, user.Username)\n\t\t\t}\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"expired users: %s\", strings.Join(failures, \", \"))\n\t}\n\tif executed == 0 {\n\t\teventManagerLog(logger.LevelError, \"no user expiration check executed\")\n\t\treturn errors.New(\"no user expiration check executed\")\n\t}\n\treturn nil\n}\n\nfunc executeInactivityCheckForUser(user *dataprovider.User, config dataprovider.EventActionUserInactivity, when time.Time) error {\n\tif config.DeleteThreshold > 0 && (user.Status == 0 || config.DisableThreshold == 0) {\n\t\tif inactivityDays := user.InactivityDays(when); inactivityDays > config.DeleteThreshold {\n\t\t\terr := dataprovider.DeleteUser(user.Username, dataprovider.ActionExecutorSystem, \"\", \"\")\n\t\t\teventManagerLog(logger.LevelInfo, \"deleting inactive user %q, days of inactivity: %d/%d, err: %v\",\n\t\t\t\tuser.Username, inactivityDays, config.DeleteThreshold, err)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to delete inactive user %q\", user.Username)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"inactive user %q deleted. Number of days of inactivity: %d\", user.Username, inactivityDays)\n\t\t}\n\t}\n\tif config.DisableThreshold > 0 && user.Status > 0 {\n\t\tif inactivityDays := user.InactivityDays(when); inactivityDays > config.DisableThreshold {\n\t\t\tuser.Status = 0\n\t\t\terr := dataprovider.UpdateUser(user, dataprovider.ActionExecutorSystem, \"\", \"\")\n\t\t\teventManagerLog(logger.LevelInfo, \"disabling inactive user %q, days of inactivity: %d/%d, err: %v\",\n\t\t\t\tuser.Username, inactivityDays, config.DisableThreshold, err)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to disable inactive user %q\", user.Username)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"inactive user %q disabled. Number of days of inactivity: %d\", user.Username, inactivityDays)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc executeUserInactivityCheckRuleAction(config dataprovider.EventActionUserInactivity,\n\tconditions dataprovider.ConditionOptions,\n\tparams *EventParams,\n\twhen time.Time,\n) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping inactivity check for user %q, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif err = executeInactivityCheckForUser(&user, config, when); err != nil {\n\t\t\tparams.AddError(err)\n\t\t\tfailures = append(failures, user.Username)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"executed inactivity check actions for users: %s\", strings.Join(failures, \", \"))\n\t}\n\n\treturn nil\n}\n\nfunc executePwdExpirationCheckForUser(user *dataprovider.User, config dataprovider.EventActionPasswordExpiration) error {\n\tif err := user.LoadAndApplyGroupSettings(); err != nil {\n\t\teventManagerLog(logger.LevelError, \"skipping password expiration check for user %q, cannot apply group settings: %v\",\n\t\t\tuser.Username, err)\n\t\treturn err\n\t}\n\tif user.ExpirationDate > 0 {\n\t\tif expDate := util.GetTimeFromMsecSinceEpoch(user.ExpirationDate); expDate.Before(time.Now()) {\n\t\t\teventManagerLog(logger.LevelDebug, \"skipping password expiration check for expired user %q, expiration date: %s\",\n\t\t\t\tuser.Username, expDate)\n\t\t\treturn nil\n\t\t}\n\t}\n\tif user.Filters.PasswordExpiration == 0 {\n\t\teventManagerLog(logger.LevelDebug, \"password expiration not set for user %q skipping check\", user.Username)\n\t\treturn nil\n\t}\n\tdays := user.PasswordExpiresIn()\n\tif days > config.Threshold {\n\t\teventManagerLog(logger.LevelDebug, \"password for user %q expires in %d days, threshold %d, no need to notify\",\n\t\t\tuser.Username, days, config.Threshold)\n\t\treturn nil\n\t}\n\tbody := new(bytes.Buffer)\n\tdata := make(map[string]any)\n\tdata[\"Username\"] = user.Username\n\tdata[\"Days\"] = days\n\tif err := smtp.RenderPasswordExpirationTemplate(body, data); err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to notify password expiration for user %s: %v\",\n\t\t\tuser.Username, err)\n\t\treturn err\n\t}\n\tsubject := \"SFTPGo password expiration notification\"\n\tstartTime := time.Now()\n\tif err := smtp.SendEmail(user.GetEmailAddresses(), nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to notify password expiration for user %s: %v, elapsed: %s\",\n\t\t\tuser.Username, err, time.Since(startTime))\n\t\treturn err\n\t}\n\teventManagerLog(logger.LevelDebug, \"password expiration email sent to user %s, days: %d, elapsed: %s\",\n\t\tuser.Username, days, time.Since(startTime))\n\treturn nil\n}\n\nfunc executePwdExpirationCheckRuleAction(config dataprovider.EventActionPasswordExpiration, conditions dataprovider.ConditionOptions,\n\tparams *EventParams) error {\n\tusers, err := params.getUsers()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get users: %w\", err)\n\t}\n\tvar failures []string\n\tfor _, user := range users {\n\t\t// if sender is set, the conditions have already been evaluated\n\t\tif params.sender == \"\" {\n\t\t\tif !checkUserConditionOptions(&user, &conditions) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"skipping password check for user %q, condition options don't match\",\n\t\t\t\t\tuser.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif err = executePwdExpirationCheckForUser(&user, config); err != nil {\n\t\t\tparams.AddError(err)\n\t\t\tfailures = append(failures, user.Username)\n\t\t}\n\t}\n\tif len(failures) > 0 {\n\t\treturn fmt.Errorf(\"password expiration check failed for users: %s\", strings.Join(failures, \", \"))\n\t}\n\n\treturn nil\n}\n\nfunc executeAdminCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *EventParams) (*dataprovider.Admin, error) {\n\tadmin, err := dataprovider.AdminExists(params.Name)\n\texists := err == nil\n\tif exists && c.Mode == 1 {\n\t\treturn &admin, nil\n\t}\n\tif err != nil && !errors.Is(err, util.ErrNotFound) {\n\t\treturn nil, err\n\t}\n\n\treplacements := params.getStringReplacements(false, 1)\n\treplacer := strings.NewReplacer(replacements...)\n\tdata := replaceWithReplacer(c.TemplateAdmin, replacer)\n\n\tvar newAdmin dataprovider.Admin\n\terr = json.Unmarshal(util.StringToBytes(data), &newAdmin)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exists {\n\t\teventManagerLog(logger.LevelDebug, \"updating admin %q after IDP login\", params.Name)\n\t\t// Not sure if this makes sense, but it shouldn't hurt.\n\t\tif newAdmin.Password == \"\" {\n\t\t\tnewAdmin.Password = admin.Password\n\t\t}\n\t\tnewAdmin.Filters.TOTPConfig = admin.Filters.TOTPConfig\n\t\tnewAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes\n\t\terr = dataprovider.UpdateAdmin(&newAdmin, dataprovider.ActionExecutorSystem, \"\", \"\")\n\t} else {\n\t\teventManagerLog(logger.LevelDebug, \"creating admin %q after IDP login\", params.Name)\n\t\tif newAdmin.Password == \"\" {\n\t\t\tnewAdmin.Password = util.GenerateUniqueID()\n\t\t}\n\t\terr = dataprovider.AddAdmin(&newAdmin, dataprovider.ActionExecutorSystem, \"\", \"\")\n\t}\n\treturn &newAdmin, err\n}\n\nfunc preserveUserProfile(user, newUser *dataprovider.User) {\n\tif newUser.CanChangePassword() && user.Password != \"\" {\n\t\tnewUser.Password = user.Password\n\t}\n\tif newUser.CanManagePublicKeys() && len(user.PublicKeys) > 0 {\n\t\tnewUser.PublicKeys = user.PublicKeys\n\t}\n\tif newUser.CanManageTLSCerts() {\n\t\tif len(user.Filters.TLSCerts) > 0 {\n\t\t\tnewUser.Filters.TLSCerts = user.Filters.TLSCerts\n\t\t}\n\t}\n\tif newUser.CanChangeInfo() {\n\t\tif user.Description != \"\" {\n\t\t\tnewUser.Description = user.Description\n\t\t}\n\t\tif user.Email != \"\" {\n\t\t\tnewUser.Email = user.Email\n\t\t}\n\t\tif len(user.Filters.AdditionalEmails) > 0 {\n\t\t\tnewUser.Filters.AdditionalEmails = user.Filters.AdditionalEmails\n\t\t}\n\t}\n\tif newUser.CanChangeAPIKeyAuth() {\n\t\tnewUser.Filters.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth\n\t}\n\tnewUser.Filters.RecoveryCodes = user.Filters.RecoveryCodes\n\tnewUser.Filters.TOTPConfig = user.Filters.TOTPConfig\n\tnewUser.LastPasswordChange = user.LastPasswordChange\n\tnewUser.SetEmptySecretsIfNil()\n}\n\nfunc executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *EventParams) (*dataprovider.User, error) {\n\tuser, err := dataprovider.UserExists(params.Name, \"\")\n\texists := err == nil\n\tif exists && c.Mode == 1 {\n\t\terr = user.LoadAndApplyGroupSettings()\n\t\treturn &user, err\n\t}\n\tif err != nil && !errors.Is(err, util.ErrNotFound) {\n\t\treturn nil, err\n\t}\n\treplacements := params.getStringReplacements(false, 1)\n\treplacer := strings.NewReplacer(replacements...)\n\tdata := replaceWithReplacer(c.TemplateUser, replacer)\n\n\tvar newUser dataprovider.User\n\terr = json.Unmarshal(util.StringToBytes(data), &newUser)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exists {\n\t\teventManagerLog(logger.LevelDebug, \"updating user %q after IDP login\", params.Name)\n\t\tpreserveUserProfile(&user, &newUser)\n\t\terr = dataprovider.UpdateUser(&newUser, dataprovider.ActionExecutorSystem, \"\", \"\")\n\t} else {\n\t\teventManagerLog(logger.LevelDebug, \"creating user %q after IDP login\", params.Name)\n\t\terr = dataprovider.AddUser(&newUser, dataprovider.ActionExecutorSystem, \"\", \"\")\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tu, err := dataprovider.GetUserWithGroupSettings(params.Name, \"\")\n\treturn &u, err\n}\n\nfunc executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, //nolint:gocyclo\n\tconditions dataprovider.ConditionOptions,\n) error {\n\tif len(conditions.EventStatuses) > 0 && !slices.Contains(conditions.EventStatuses, params.Status) {\n\t\teventManagerLog(logger.LevelDebug, \"skipping action %s, event status %d does not match: %v\",\n\t\t\taction.Name, params.Status, conditions.EventStatuses)\n\t\treturn nil\n\t}\n\tvar err error\n\n\tswitch action.Type {\n\tcase dataprovider.ActionTypeHTTP:\n\t\terr = executeHTTPRuleAction(action.Options.HTTPConfig, params)\n\tcase dataprovider.ActionTypeCommand:\n\t\terr = executeCommandRuleAction(action.Options.CmdConfig, params)\n\tcase dataprovider.ActionTypeEmail:\n\t\terr = executeEmailRuleAction(action.Options.EmailConfig, params)\n\tcase dataprovider.ActionTypeBackup:\n\t\tvar backupPath string\n\t\tbackupPath, err = dataprovider.ExecuteBackup()\n\t\tif err == nil {\n\t\t\tparams.setBackupParams(backupPath)\n\t\t}\n\tcase dataprovider.ActionTypeUserQuotaReset:\n\t\terr = executeUsersQuotaResetRuleAction(conditions, params)\n\tcase dataprovider.ActionTypeFolderQuotaReset:\n\t\terr = executeFoldersQuotaResetRuleAction(conditions, params)\n\tcase dataprovider.ActionTypeTransferQuotaReset:\n\t\terr = executeTransferQuotaResetRuleAction(conditions, params)\n\tcase dataprovider.ActionTypeDataRetentionCheck:\n\t\terr = executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions, params, action.Name)\n\tcase dataprovider.ActionTypeFilesystem:\n\t\terr = executeFsRuleAction(action.Options.FsConfig, conditions, params)\n\tcase dataprovider.ActionTypePasswordExpirationCheck:\n\t\terr = executePwdExpirationCheckRuleAction(action.Options.PwdExpirationConfig, conditions, params)\n\tcase dataprovider.ActionTypeUserExpirationCheck:\n\t\terr = executeUserExpirationCheckRuleAction(conditions, params)\n\tcase dataprovider.ActionTypeUserInactivityCheck:\n\t\terr = executeUserInactivityCheckRuleAction(action.Options.UserInactivityConfig, conditions, params, time.Now())\n\tcase dataprovider.ActionTypeRotateLogs:\n\t\terr = logger.RotateLogFile()\n\tdefault:\n\t\terr = fmt.Errorf(\"unsupported action type: %d\", action.Type)\n\t}\n\n\tif err != nil {\n\t\terr = fmt.Errorf(\"action %q failed: %w\", action.Name, err)\n\t}\n\tparams.AddError(err)\n\treturn err\n}\n\nfunc executeIDPAccountCheckRule(rule dataprovider.EventRule, params EventParams) (*dataprovider.User,\n\t*dataprovider.Admin, error,\n) {\n\tfor _, action := range rule.Actions {\n\t\tif action.Type == dataprovider.ActionTypeIDPAccountCheck {\n\t\t\tstartTime := time.Now()\n\t\t\tvar user *dataprovider.User\n\t\t\tvar admin *dataprovider.Admin\n\t\t\tvar err error\n\t\t\tvar failedActions []string\n\t\t\tparamsCopy := params.getACopy()\n\n\t\t\tswitch params.Event {\n\t\t\tcase IDPLoginAdmin:\n\t\t\t\tadmin, err = executeAdminCheckAction(&action.BaseEventAction.Options.IDPConfig, paramsCopy)\n\t\t\tcase IDPLoginUser:\n\t\t\t\tuser, err = executeUserCheckAction(&action.BaseEventAction.Options.IDPConfig, paramsCopy)\n\t\t\tdefault:\n\t\t\t\terr = fmt.Errorf(\"unsupported IDP login event: %q\", params.Event)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tparamsCopy.AddError(fmt.Errorf(\"unable to handle %q: %w\", params.Event, err))\n\t\t\t\teventManagerLog(logger.LevelError, \"unable to handle IDP login event %q, err: %v\", params.Event, err)\n\t\t\t\tfailedActions = append(failedActions, action.Name)\n\t\t\t} else {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"executed action %q for rule %q, elapsed %s\",\n\t\t\t\t\taction.Name, rule.Name, time.Since(startTime))\n\t\t\t}\n\t\t\t// execute async actions if any, including failure actions\n\t\t\tgo executeRuleAsyncActions(rule, paramsCopy, failedActions)\n\t\t\treturn user, admin, err\n\t\t}\n\t}\n\teventManagerLog(logger.LevelError, \"no action executed for IDP login event %q, event rule: %q\", params.Event, rule.Name)\n\treturn nil, nil, errors.New(\"no action executed\")\n}\n\nfunc executeSyncRulesActions(rules []dataprovider.EventRule, params EventParams) error {\n\tvar errRes error\n\n\tfor _, rule := range rules {\n\t\tvar failedActions []string\n\t\tparamsCopy := params.getACopy()\n\t\tfor _, action := range rule.Actions {\n\t\t\tif !action.Options.IsFailureAction && action.Options.ExecuteSync {\n\t\t\t\tstartTime := time.Now()\n\t\t\t\tif err := executeRuleAction(action.BaseEventAction, paramsCopy, rule.Conditions.Options); err != nil {\n\t\t\t\t\teventManagerLog(logger.LevelError, \"unable to execute sync action %q for rule %q, elapsed %s, err: %v\",\n\t\t\t\t\t\taction.Name, rule.Name, time.Since(startTime), err)\n\t\t\t\t\tfailedActions = append(failedActions, action.Name)\n\t\t\t\t\t// we return the last error, it is ok for now\n\t\t\t\t\terrRes = err\n\t\t\t\t\tif action.Options.StopOnFailure {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\teventManagerLog(logger.LevelDebug, \"executed sync action %q for rule %q, elapsed: %s\",\n\t\t\t\t\t\taction.Name, rule.Name, time.Since(startTime))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// execute async actions if any, including failure actions\n\t\tgo executeRuleAsyncActions(rule, paramsCopy, failedActions)\n\t}\n\n\treturn errRes\n}\n\nfunc executeAsyncRulesActions(rules []dataprovider.EventRule, params EventParams) {\n\teventManager.addAsyncTask()\n\tdefer eventManager.removeAsyncTask()\n\n\tparams.addUID()\n\tfor _, rule := range rules {\n\t\texecuteRuleAsyncActions(rule, params.getACopy(), nil)\n\t}\n}\n\nfunc executeRuleAsyncActions(rule dataprovider.EventRule, params *EventParams, failedActions []string) {\n\tfor _, action := range rule.Actions {\n\t\tif !action.Options.IsFailureAction && !action.Options.ExecuteSync {\n\t\t\tstartTime := time.Now()\n\t\t\tif err := executeRuleAction(action.BaseEventAction, params, rule.Conditions.Options); err != nil {\n\t\t\t\teventManagerLog(logger.LevelError, \"unable to execute action %q for rule %q, elapsed %s, err: %v\",\n\t\t\t\t\taction.Name, rule.Name, time.Since(startTime), err)\n\t\t\t\tfailedActions = append(failedActions, action.Name)\n\t\t\t\tif action.Options.StopOnFailure {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"executed action %q for rule %q, elapsed %s\",\n\t\t\t\t\taction.Name, rule.Name, time.Since(startTime))\n\t\t\t}\n\t\t}\n\t}\n\tif len(failedActions) > 0 {\n\t\tparams.updateStatusFromError = false\n\t\t// execute failure actions\n\t\tfor _, action := range rule.Actions {\n\t\t\tif action.Options.IsFailureAction {\n\t\t\t\tstartTime := time.Now()\n\t\t\t\tif err := executeRuleAction(action.BaseEventAction, params, rule.Conditions.Options); err != nil {\n\t\t\t\t\teventManagerLog(logger.LevelError, \"unable to execute failure action %q for rule %q, elapsed %s, err: %v\",\n\t\t\t\t\t\taction.Name, rule.Name, time.Since(startTime), err)\n\t\t\t\t\tif action.Options.StopOnFailure {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\teventManagerLog(logger.LevelDebug, \"executed failure action %q for rule %q, elapsed: %s\",\n\t\t\t\t\t\taction.Name, rule.Name, time.Since(startTime))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype eventCronJob struct {\n\truleName string\n}\n\nfunc (j *eventCronJob) getTask(rule *dataprovider.EventRule) (dataprovider.Task, error) {\n\tif rule.GuardFromConcurrentExecution() {\n\t\ttask, err := dataprovider.GetTaskByName(rule.Name)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\t\teventManagerLog(logger.LevelDebug, \"adding task for rule %q\", rule.Name)\n\t\t\t\ttask = dataprovider.Task{\n\t\t\t\t\tName:     rule.Name,\n\t\t\t\t\tUpdateAt: 0,\n\t\t\t\t\tVersion:  0,\n\t\t\t\t}\n\t\t\t\terr = dataprovider.AddTask(rule.Name)\n\t\t\t\tif err != nil {\n\t\t\t\t\teventManagerLog(logger.LevelWarn, \"unable to add task for rule %q: %v\", rule.Name, err)\n\t\t\t\t\treturn task, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\teventManagerLog(logger.LevelWarn, \"unable to get task for rule %q: %v\", rule.Name, err)\n\t\t\t}\n\t\t}\n\t\treturn task, err\n\t}\n\n\treturn dataprovider.Task{}, nil\n}\n\nfunc (j *eventCronJob) getEventParams() EventParams {\n\treturn EventParams{\n\t\tEvent:                 \"Schedule\",\n\t\tName:                  j.ruleName,\n\t\tStatus:                1,\n\t\tTimestamp:             time.Now(),\n\t\tupdateStatusFromError: true,\n\t}\n}\n\nfunc (j *eventCronJob) Run() {\n\teventManagerLog(logger.LevelDebug, \"executing scheduled rule %q\", j.ruleName)\n\trule, err := dataprovider.EventRuleExists(j.ruleName)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelError, \"unable to load rule with name %q\", j.ruleName)\n\t\treturn\n\t}\n\tif err := rule.CheckActionsConsistency(\"\"); err != nil {\n\t\teventManagerLog(logger.LevelWarn, \"scheduled rule %q skipped: %v\", rule.Name, err)\n\t\treturn\n\t}\n\ttask, err := j.getTask(&rule)\n\tif err != nil {\n\t\treturn\n\t}\n\tif task.Name != \"\" {\n\t\tupdateInterval := 5 * time.Minute\n\t\tupdatedAt := util.GetTimeFromMsecSinceEpoch(task.UpdateAt)\n\t\tif updatedAt.Add(updateInterval*2 + 1).After(time.Now()) {\n\t\t\teventManagerLog(logger.LevelDebug, \"task for rule %q too recent: %s, skip execution\", rule.Name, updatedAt)\n\t\t\treturn\n\t\t}\n\t\terr = dataprovider.UpdateTask(rule.Name, task.Version)\n\t\tif err != nil {\n\t\t\teventManagerLog(logger.LevelInfo, \"unable to update task timestamp for rule %q, skip execution, err: %v\",\n\t\t\t\trule.Name, err)\n\t\t\treturn\n\t\t}\n\t\tticker := time.NewTicker(updateInterval)\n\t\tdone := make(chan bool)\n\n\t\tdefer func() {\n\t\t\tdone <- true\n\t\t\tticker.Stop()\n\t\t}()\n\n\t\tgo func(taskName string) {\n\t\t\teventManagerLog(logger.LevelDebug, \"update task %q timestamp worker started\", taskName)\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-done:\n\t\t\t\t\teventManagerLog(logger.LevelDebug, \"update task %q timestamp worker finished\", taskName)\n\t\t\t\t\treturn\n\t\t\t\tcase <-ticker.C:\n\t\t\t\t\terr := dataprovider.UpdateTaskTimestamp(taskName)\n\t\t\t\t\teventManagerLog(logger.LevelInfo, \"updated timestamp for task %q, err: %v\", taskName, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}(task.Name)\n\n\t\texecuteAsyncRulesActions([]dataprovider.EventRule{rule}, j.getEventParams())\n\t} else {\n\t\texecuteAsyncRulesActions([]dataprovider.EventRule{rule}, j.getEventParams())\n\t}\n\teventManagerLog(logger.LevelDebug, \"execution for scheduled rule %q finished\", j.ruleName)\n}\n\n// RunOnDemandRule executes actions for a rule with on-demand trigger\nfunc RunOnDemandRule(name string) error {\n\teventManagerLog(logger.LevelDebug, \"executing on demand rule %q\", name)\n\trule, err := dataprovider.EventRuleExists(name)\n\tif err != nil {\n\t\teventManagerLog(logger.LevelDebug, \"unable to load rule with name %q\", name)\n\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"rule %q does not exist\", name))\n\t}\n\tif rule.Trigger != dataprovider.EventTriggerOnDemand {\n\t\teventManagerLog(logger.LevelDebug, \"cannot run rule %q as on demand, trigger: %d\", name, rule.Trigger)\n\t\treturn util.NewValidationError(fmt.Sprintf(\"rule %q is not defined as on-demand\", name))\n\t}\n\tif rule.Status != 1 {\n\t\teventManagerLog(logger.LevelDebug, \"on-demand rule %q is inactive\", name)\n\t\treturn util.NewValidationError(fmt.Sprintf(\"rule %q is inactive\", name))\n\t}\n\tif err := rule.CheckActionsConsistency(\"\"); err != nil {\n\t\teventManagerLog(logger.LevelError, \"on-demand rule %q has incompatible actions: %v\", name, err)\n\t\treturn util.NewValidationError(fmt.Sprintf(\"rule %q has incosistent actions\", name))\n\t}\n\teventManagerLog(logger.LevelDebug, \"on-demand rule %q started\", name)\n\tgo executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true})\n\treturn nil\n}\n\ntype zipWriterWrapper struct {\n\tName    string\n\tEntries map[string]bool\n\tWriter  *zip.Writer\n}\n\nfunc eventManagerLog(level logger.LogLevel, format string, v ...any) {\n\tlogger.Log(level, \"eventmanager\", \"\", format, v...)\n}\n"
  },
  {
    "path": "internal/common/eventmanager_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/klauspost/compress/zip\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nfunc TestEventRuleMatch(t *testing.T) {\n\trole := \"role1\"\n\tconditions := &dataprovider.EventConditions{\n\t\tProviderEvents: []string{\"add\", \"update\"},\n\t\tOptions: dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern:      \"user1\",\n\t\t\t\t\tInverseMatch: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoleNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: role,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tres := eventManager.checkProviderEventMatch(conditions, &EventParams{\n\t\tName:  \"user1\",\n\t\tRole:  role,\n\t\tEvent: \"add\",\n\t})\n\tassert.False(t, res)\n\tres = eventManager.checkProviderEventMatch(conditions, &EventParams{\n\t\tName:  \"user2\",\n\t\tRole:  role,\n\t\tEvent: \"update\",\n\t})\n\tassert.True(t, res)\n\tres = eventManager.checkProviderEventMatch(conditions, &EventParams{\n\t\tName:  \"user2\",\n\t\tRole:  role,\n\t\tEvent: \"delete\",\n\t})\n\tassert.False(t, res)\n\tconditions.Options.ProviderObjects = []string{\"api_key\"}\n\tres = eventManager.checkProviderEventMatch(conditions, &EventParams{\n\t\tName:       \"user2\",\n\t\tEvent:      \"update\",\n\t\tRole:       role,\n\t\tObjectType: \"share\",\n\t})\n\tassert.False(t, res)\n\tres = eventManager.checkProviderEventMatch(conditions, &EventParams{\n\t\tName:       \"user2\",\n\t\tEvent:      \"update\",\n\t\tRole:       role,\n\t\tObjectType: \"api_key\",\n\t})\n\tassert.True(t, res)\n\tres = eventManager.checkProviderEventMatch(conditions, &EventParams{\n\t\tName:       \"user2\",\n\t\tEvent:      \"update\",\n\t\tRole:       role + \"1\",\n\t\tObjectType: \"api_key\",\n\t})\n\tassert.False(t, res)\n\t// now test fs events\n\tconditions = &dataprovider.EventConditions{\n\t\tFsEvents: []string{operationUpload, operationDownload},\n\t\tOptions: dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: \"user*\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPattern: \"tester*\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoleNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern:      role,\n\t\t\t\t\tInverseMatch: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tFsPaths: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: \"/**/*.txt\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProtocols:   []string{ProtocolSFTP},\n\t\t\tMinFileSize: 10,\n\t\t\tMaxFileSize: 30,\n\t\t},\n\t}\n\tparams := EventParams{\n\t\tName:        \"tester4\",\n\t\tEvent:       operationDelete,\n\t\tVirtualPath: \"/path.txt\",\n\t\tProtocol:    ProtocolSFTP,\n\t\tObjectName:  \"path.txt\",\n\t\tFileSize:    20,\n\t}\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.Event = operationDownload\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.True(t, res)\n\tparams.Role = role\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.Role = \"\"\n\tparams.Name = \"name\"\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.Name = \"user5\"\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.True(t, res)\n\tparams.VirtualPath = \"/sub/f.jpg\"\n\tparams.ObjectName = path.Base(params.VirtualPath)\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.VirtualPath = \"/sub/f.txt\"\n\tparams.ObjectName = path.Base(params.VirtualPath)\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.True(t, res)\n\tparams.Protocol = ProtocolHTTP\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.Protocol = ProtocolSFTP\n\tparams.FileSize = 5\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.FileSize = 50\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.FileSize = 25\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.True(t, res)\n\t// bad pattern\n\tconditions.Options.Names = []dataprovider.ConditionPattern{\n\t\t{\n\t\t\tPattern: \"[-]\",\n\t\t},\n\t}\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\t// check fs events with group name filters\n\tconditions = &dataprovider.EventConditions{\n\t\tFsEvents: []string{operationUpload, operationDownload},\n\t\tOptions: dataprovider.ConditionOptions{\n\t\t\tGroupNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: \"group*\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPattern: \"testgroup*\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tparams = EventParams{\n\t\tName:  \"user1\",\n\t\tEvent: operationUpload,\n\t}\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: \"g1\",\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t\t{\n\t\t\tName: \"g2\",\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t}\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.False(t, res)\n\tparams.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: \"testgroup2\",\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t\t{\n\t\t\tName: \"g2\",\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t}\n\tres = eventManager.checkFsEventMatch(conditions, &params)\n\tassert.True(t, res)\n\t// check user conditions\n\tuser := dataprovider.User{}\n\tuser.Username = \"u1\"\n\tres = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{})\n\tassert.True(t, res)\n\tres = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"user\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.False(t, res)\n\tres = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{\n\t\tRoleNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: role,\n\t\t\t},\n\t\t},\n\t})\n\tassert.False(t, res)\n\tuser.Role = role\n\tres = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{\n\t\tRoleNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: role,\n\t\t\t},\n\t\t},\n\t})\n\tassert.True(t, res)\n\tres = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{\n\t\tGroupNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"group\",\n\t\t\t},\n\t\t},\n\t\tRoleNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: role,\n\t\t\t},\n\t\t},\n\t})\n\tassert.False(t, res)\n\tres = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{\n\t\tIDPLoginEvent: 0,\n\t}, &EventParams{\n\t\tEvent: IDPLoginAdmin,\n\t})\n\tassert.True(t, res)\n\tres = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{\n\t\tIDPLoginEvent: 2,\n\t}, &EventParams{\n\t\tEvent: IDPLoginAdmin,\n\t})\n\tassert.True(t, res)\n\tres = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{\n\t\tIDPLoginEvent: 1,\n\t}, &EventParams{\n\t\tEvent: IDPLoginAdmin,\n\t})\n\tassert.False(t, res)\n\tres = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{\n\t\tIDPLoginEvent: 1,\n\t}, &EventParams{\n\t\tEvent: IDPLoginUser,\n\t})\n\tassert.True(t, res)\n\tres = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{\n\t\tIDPLoginEvent: 1,\n\t}, &EventParams{\n\t\tName:  \"user\",\n\t\tEvent: IDPLoginUser,\n\t})\n\tassert.True(t, res)\n\tres = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{\n\t\tIDPLoginEvent: 1,\n\t\tOptions: dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: \"abc\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, &EventParams{\n\t\tName:  \"user\",\n\t\tEvent: IDPLoginUser,\n\t})\n\tassert.False(t, res)\n\tres = eventManager.checkIPDLoginEventMatch(&dataprovider.EventConditions{\n\t\tIDPLoginEvent: 2,\n\t}, &EventParams{\n\t\tName:  \"user\",\n\t\tEvent: IDPLoginUser,\n\t})\n\tassert.False(t, res)\n}\n\nfunc TestDoubleStarMatching(t *testing.T) {\n\tc := dataprovider.ConditionPattern{\n\t\tPattern: \"/mydir/**\",\n\t}\n\tres := checkEventConditionPattern(c, \"/mydir\")\n\tassert.True(t, res)\n\tres = checkEventConditionPattern(c, \"/mydirname\")\n\tassert.False(t, res)\n\tres = checkEventConditionPattern(c, \"/mydir/sub\")\n\tassert.True(t, res)\n\tres = checkEventConditionPattern(c, \"/mydir/sub/dir\")\n\tassert.True(t, res)\n\n\tc.Pattern = \"/**/*\"\n\tres = checkEventConditionPattern(c, \"/mydir\")\n\tassert.True(t, res)\n\tres = checkEventConditionPattern(c, \"/mydirname\")\n\tassert.True(t, res)\n\tres = checkEventConditionPattern(c, \"/mydir/sub/dir/file.txt\")\n\tassert.True(t, res)\n\n\tc.Pattern = \"/**/*.filepart\"\n\tres = checkEventConditionPattern(c, \"/file.filepart\")\n\tassert.True(t, res)\n\tres = checkEventConditionPattern(c, \"/mydir/sub/file.filepart\")\n\tassert.True(t, res)\n\tres = checkEventConditionPattern(c, \"/file.txt\")\n\tassert.False(t, res)\n\tres = checkEventConditionPattern(c, \"/mydir/file.txt\")\n\tassert.False(t, res)\n\n\tc.Pattern = \"/mydir/**/*.txt\"\n\tres = checkEventConditionPattern(c, \"/mydir\")\n\tassert.False(t, res)\n\tres = checkEventConditionPattern(c, \"/mydirname/f.txt\")\n\tassert.False(t, res)\n\tres = checkEventConditionPattern(c, \"/mydir/sub\")\n\tassert.False(t, res)\n\tres = checkEventConditionPattern(c, \"/mydir/sub/dir\")\n\tassert.False(t, res)\n\tres = checkEventConditionPattern(c, \"/mydir/sub/dir/a.txt\")\n\tassert.True(t, res)\n\n\tc.InverseMatch = true\n\tassert.True(t, checkEventConditionPattern(c, \"/mydir\"))\n\tassert.True(t, checkEventConditionPattern(c, \"/mydirname/f.txt\"))\n\tassert.True(t, checkEventConditionPattern(c, \"/mydir/sub\"))\n\tassert.True(t, checkEventConditionPattern(c, \"/mydir/sub/dir\"))\n\tassert.False(t, checkEventConditionPattern(c, \"/mydir/sub/dir/a.txt\"))\n}\n\nfunc TestMutlipleDoubleStarMatching(t *testing.T) {\n\tpatterns := []dataprovider.ConditionPattern{\n\t\t{\n\t\t\tPattern:      \"/**/*.txt\",\n\t\t\tInverseMatch: false,\n\t\t},\n\t\t{\n\t\t\tPattern:      \"/**/*.tmp\",\n\t\t\tInverseMatch: false,\n\t\t},\n\t}\n\tassert.False(t, checkEventConditionPatterns(\"/mydir\", patterns))\n\tassert.True(t, checkEventConditionPatterns(\"/mydir/test.tmp\", patterns))\n\tassert.True(t, checkEventConditionPatterns(\"/mydir/test.txt\", patterns))\n\tassert.False(t, checkEventConditionPatterns(\"/mydir/test.csv\", patterns))\n\tassert.False(t, checkEventConditionPatterns(\"/mydir/sub\", patterns))\n\tassert.True(t, checkEventConditionPatterns(\"/mydir/sub/test.tmp\", patterns))\n\tassert.True(t, checkEventConditionPatterns(\"/mydir/sub/test.txt\", patterns))\n\tassert.False(t, checkEventConditionPatterns(\"/mydir/sub/test.csv\", patterns))\n}\n\nfunc TestMultipleDoubleStarMatchingInverse(t *testing.T) {\n\tpatterns := []dataprovider.ConditionPattern{\n\t\t{\n\t\t\tPattern:      \"/**/*.txt\",\n\t\t\tInverseMatch: true,\n\t\t},\n\t\t{\n\t\t\tPattern:      \"/**/*.tmp\",\n\t\t\tInverseMatch: true,\n\t\t},\n\t}\n\tassert.True(t, checkEventConditionPatterns(\"/mydir\", patterns))\n\tassert.False(t, checkEventConditionPatterns(\"/mydir/test.tmp\", patterns))\n\tassert.False(t, checkEventConditionPatterns(\"/mydir/test.txt\", patterns))\n\tassert.True(t, checkEventConditionPatterns(\"/mydir/test.csv\", patterns))\n\tassert.True(t, checkEventConditionPatterns(\"/mydir/sub\", patterns))\n\tassert.False(t, checkEventConditionPatterns(\"/mydir/sub/test.tmp\", patterns))\n\tassert.False(t, checkEventConditionPatterns(\"/mydir/sub/test.txt\", patterns))\n\tassert.True(t, checkEventConditionPatterns(\"/mydir/sub/test.csv\", patterns))\n}\n\nfunc TestGroupConditionPatterns(t *testing.T) {\n\tgroup1 := \"group1\"\n\tgroup2 := \"group2\"\n\tpatterns := []dataprovider.ConditionPattern{\n\t\t{\n\t\t\tPattern: group1,\n\t\t},\n\t\t{\n\t\t\tPattern: group2,\n\t\t},\n\t}\n\tinversePatterns := []dataprovider.ConditionPattern{\n\t\t{\n\t\t\tPattern:      group1,\n\t\t\tInverseMatch: true,\n\t\t},\n\t\t{\n\t\t\tPattern:      group2,\n\t\t\tInverseMatch: true,\n\t\t},\n\t}\n\tgroups := []sdk.GroupMapping{\n\t\t{\n\t\t\tName: \"group3\",\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tassert.False(t, checkEventGroupConditionPatterns(groups, patterns))\n\tassert.True(t, checkEventGroupConditionPatterns(groups, inversePatterns))\n\n\tgroups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t\t{\n\t\t\tName: \"group4\",\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tassert.True(t, checkEventGroupConditionPatterns(groups, patterns))\n\tassert.False(t, checkEventGroupConditionPatterns(groups, inversePatterns))\n\tgroups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tassert.True(t, checkEventGroupConditionPatterns(groups, patterns))\n\tassert.False(t, checkEventGroupConditionPatterns(groups, inversePatterns))\n\tgroups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: \"group11\",\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tassert.False(t, checkEventGroupConditionPatterns(groups, patterns))\n\tassert.True(t, checkEventGroupConditionPatterns(groups, inversePatterns))\n}\n\nfunc TestEventManager(t *testing.T) {\n\tstartEventScheduler()\n\taction := &dataprovider.BaseEventAction{\n\t\tName: \"test_action\",\n\t\tType: dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint: \"http://localhost\",\n\t\t\t\tTimeout:  20,\n\t\t\t\tMethod:   http.MethodGet,\n\t\t\t},\n\t\t},\n\t}\n\terr := dataprovider.AddEventAction(action, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\trule := &dataprovider.EventRule{\n\t\tName:    \"rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{operationUpload},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\n\terr = dataprovider.AddEventRule(rule, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\teventManager.RLock()\n\tassert.Len(t, eventManager.FsEvents, 1)\n\tassert.Len(t, eventManager.ProviderEvents, 0)\n\tassert.Len(t, eventManager.Schedules, 0)\n\tassert.Len(t, eventManager.schedulesMapping, 0)\n\teventManager.RUnlock()\n\n\trule.Trigger = dataprovider.EventTriggerProviderEvent\n\trule.Conditions = dataprovider.EventConditions{\n\t\tProviderEvents: []string{\"add\"},\n\t}\n\terr = dataprovider.UpdateEventRule(rule, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\teventManager.RLock()\n\tassert.Len(t, eventManager.FsEvents, 0)\n\tassert.Len(t, eventManager.ProviderEvents, 1)\n\tassert.Len(t, eventManager.Schedules, 0)\n\tassert.Len(t, eventManager.schedulesMapping, 0)\n\teventManager.RUnlock()\n\n\trule.Trigger = dataprovider.EventTriggerSchedule\n\trule.Conditions = dataprovider.EventConditions{\n\t\tSchedules: []dataprovider.Schedule{\n\t\t\t{\n\t\t\t\tHours:      \"0\",\n\t\t\t\tDayOfWeek:  \"*\",\n\t\t\t\tDayOfMonth: \"*\",\n\t\t\t\tMonth:      \"*\",\n\t\t\t},\n\t\t},\n\t}\n\trule.DeletedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-12 * time.Hour))\n\teventManager.addUpdateRuleInternal(*rule)\n\n\teventManager.RLock()\n\tassert.Len(t, eventManager.FsEvents, 0)\n\tassert.Len(t, eventManager.ProviderEvents, 0)\n\tassert.Len(t, eventManager.Schedules, 0)\n\tassert.Len(t, eventManager.schedulesMapping, 0)\n\teventManager.RUnlock()\n\n\tassert.Eventually(t, func() bool {\n\t\t_, err = dataprovider.EventRuleExists(rule.Name)\n\t\tok := errors.Is(err, util.ErrNotFound)\n\t\treturn ok\n\t}, 2*time.Second, 100*time.Millisecond)\n\n\trule.DeletedAt = 0\n\terr = dataprovider.AddEventRule(rule, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\teventManager.RLock()\n\tassert.Len(t, eventManager.FsEvents, 0)\n\tassert.Len(t, eventManager.ProviderEvents, 0)\n\tassert.Len(t, eventManager.Schedules, 1)\n\tassert.Len(t, eventManager.schedulesMapping, 1)\n\teventManager.RUnlock()\n\n\terr = dataprovider.DeleteEventRule(rule.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\teventManager.RLock()\n\tassert.Len(t, eventManager.FsEvents, 0)\n\tassert.Len(t, eventManager.ProviderEvents, 0)\n\tassert.Len(t, eventManager.Schedules, 0)\n\tassert.Len(t, eventManager.schedulesMapping, 0)\n\teventManager.RUnlock()\n\n\terr = dataprovider.DeleteEventAction(action.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tstopEventScheduler()\n}\n\nfunc TestEventManagerErrors(t *testing.T) {\n\tstartEventScheduler()\n\tproviderConf := dataprovider.GetProviderConfig()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\n\tparams := EventParams{\n\t\tsender: \"sender\",\n\t}\n\t_, err = params.getUsers()\n\tassert.Error(t, err)\n\t_, err = params.getFolders()\n\tassert.Error(t, err)\n\n\terr = executeUsersQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeFoldersQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeUserExpirationCheckRuleAction(dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{},\n\t\tdataprovider.ConditionOptions{}, &EventParams{}, time.Time{})\n\tassert.Error(t, err)\n\terr = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeRenameFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeExistFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeCopyFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executeCompressFsRuleAction(dataprovider.EventActionFsCompress{}, nil, dataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\terr = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{},\n\t\tdataprovider.ConditionOptions{}, &EventParams{})\n\tassert.Error(t, err)\n\t_, err = executeAdminCheckAction(&dataprovider.EventActionIDPAccountCheck{}, &EventParams{})\n\tassert.Error(t, err)\n\t_, err = executeUserCheckAction(&dataprovider.EventActionIDPAccountCheck{}, &EventParams{})\n\tassert.Error(t, err)\n\n\tgroupName := \"agroup\"\n\terr = executeQuotaResetForUser(&dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\terr = executeDataRetentionCheckForUser(dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t}, nil, &EventParams{}, \"\")\n\tassert.Error(t, err)\n\terr = executeDeleteFsActionForUser(nil, nil, dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\terr = executeMkDirsFsActionForUser(nil, nil, dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\terr = executeRenameFsActionForUser(nil, nil, dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\terr = executeExistFsActionForUser(nil, nil, dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\terr = executeCopyFsActionForUser(nil, nil, dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\terr = executeCompressFsActionForUser(dataprovider.EventActionFsCompress{}, nil, dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\terr = executePwdExpirationCheckForUser(&dataprovider.User{\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t}}, dataprovider.EventActionPasswordExpiration{})\n\tassert.Error(t, err)\n\n\t_, _, err = getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{\n\t\tMethod: http.MethodPost,\n\t\tParts: []dataprovider.HTTPPart{\n\t\t\t{\n\t\t\t\tName: \"p1\",\n\t\t\t},\n\t\t},\n\t}, nil, nil, dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"u\",\n\t\t},\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, false)\n\tassert.Error(t, err)\n\n\tdataRetentionAction := dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeDataRetentionCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tRetentionConfig: dataprovider.EventActionDataRetentionConfig{\n\t\t\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:      \"/\",\n\t\t\t\t\t\tRetention: 24,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"username1\",\n\t\t\t},\n\t\t},\n\t})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to get users\")\n\t}\n\n\teventManager.loadRules()\n\n\teventManager.RLock()\n\tassert.Len(t, eventManager.FsEvents, 0)\n\tassert.Len(t, eventManager.ProviderEvents, 0)\n\tassert.Len(t, eventManager.Schedules, 0)\n\teventManager.RUnlock()\n\n\t// rule with invalid trigger\n\teventManager.addUpdateRuleInternal(dataprovider.EventRule{\n\t\tName:    \"test rule\",\n\t\tStatus:  1,\n\t\tTrigger: -1,\n\t})\n\n\teventManager.RLock()\n\tassert.Len(t, eventManager.FsEvents, 0)\n\tassert.Len(t, eventManager.ProviderEvents, 0)\n\tassert.Len(t, eventManager.Schedules, 0)\n\teventManager.RUnlock()\n\t// rule with invalid cronspec\n\teventManager.addUpdateRuleInternal(dataprovider.EventRule{\n\t\tName:    \"test rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerSchedule,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tSchedules: []dataprovider.Schedule{\n\t\t\t\t{\n\t\t\t\t\tHours: \"1000\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\teventManager.RLock()\n\tassert.Len(t, eventManager.FsEvents, 0)\n\tassert.Len(t, eventManager.ProviderEvents, 0)\n\tassert.Len(t, eventManager.Schedules, 0)\n\teventManager.RUnlock()\n\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tstopEventScheduler()\n}\n\nfunc TestDateTimePlaceholder(t *testing.T) {\n\toldTZ := Config.TZ\n\n\tConfig.TZ = \"\"\n\tdateTime := time.Now()\n\tparams := EventParams{\n\t\tTimestamp: dateTime,\n\t}\n\treplacements := params.getStringReplacements(false, 0)\n\tr := strings.NewReplacer(replacements...)\n\tres := r.Replace(\"{{.DateTime}}\")\n\tassert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat), res)\n\tres = r.Replace(\"{{.Year}}-{{.Month}}-{{.Day}}T{{.Hour}}:{{.Minute}}\")\n\tassert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat)[:16], res)\n\n\tConfig.TZ = \"local\"\n\treplacements = params.getStringReplacements(false, 0)\n\tr = strings.NewReplacer(replacements...)\n\tres = r.Replace(\"{{.DateTime}}\")\n\tassert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat), res)\n\tres = r.Replace(\"{{.Year}}-{{.Month}}-{{.Day}}T{{.Hour}}:{{.Minute}}\")\n\tassert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat)[:16], res)\n\n\tConfig.TZ = oldTZ\n}\n\nfunc TestEventRuleActions(t *testing.T) {\n\tactionName := \"test rule action\"\n\taction := dataprovider.BaseEventAction{\n\t\tName: actionName,\n\t\tType: dataprovider.ActionTypeBackup,\n\t}\n\terr := executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{})\n\tassert.NoError(t, err)\n\taction.Type = -1\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{})\n\tassert.Error(t, err)\n\n\taction = dataprovider.BaseEventAction{\n\t\tName: actionName,\n\t\tType: dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint:      \"http://foo\\x7f.com/\", // invalid URL\n\t\t\t\tSkipTLSVerify: true,\n\t\t\t\tBody:          `\"data\": \"{{.ObjectDataString}}\"`,\n\t\t\t\tMethod:        http.MethodPost,\n\t\t\t\tQueryParameters: []dataprovider.KeyValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   \"param\",\n\t\t\t\t\t\tValue: \"value\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tTimeout: 5,\n\t\t\t\tHeaders: []dataprovider.KeyValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   \"Content-Type\",\n\t\t\t\t\t\tValue: \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tUsername: \"httpuser\",\n\t\t\t},\n\t\t},\n\t}\n\taction.Options.SetEmptySecretsIfNil()\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid endpoint\")\n\t}\n\taction.Options.HTTPConfig.Endpoint = fmt.Sprintf(\"http://%v\", httpAddr)\n\tparams := &EventParams{\n\t\tName: \"a\",\n\t\tObject: &dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername: \"test user\",\n\t\t\t},\n\t\t},\n\t}\n\terr = executeRuleAction(action, params, dataprovider.ConditionOptions{})\n\tassert.NoError(t, err)\n\taction.Options.HTTPConfig.Method = http.MethodGet\n\terr = executeRuleAction(action, params, dataprovider.ConditionOptions{})\n\tassert.NoError(t, err)\n\taction.Options.HTTPConfig.Endpoint = fmt.Sprintf(\"http://%v/404\", httpAddr)\n\terr = executeRuleAction(action, params, dataprovider.ConditionOptions{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unexpected status code: 404\")\n\t}\n\taction.Options.HTTPConfig.Endpoint = \"http://invalid:1234\"\n\terr = executeRuleAction(action, params, dataprovider.ConditionOptions{})\n\tassert.Error(t, err)\n\taction.Options.HTTPConfig.QueryParameters = nil\n\taction.Options.HTTPConfig.Endpoint = \"http://bar\\x7f.com/\"\n\terr = executeRuleAction(action, params, dataprovider.ConditionOptions{})\n\tassert.Error(t, err)\n\taction.Options.HTTPConfig.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, \"payload\", \"key\", \"data\")\n\terr = executeRuleAction(action, params, dataprovider.ConditionOptions{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to decrypt HTTP password\")\n\t}\n\taction.Options.HTTPConfig.Endpoint = fmt.Sprintf(\"http://%v\", httpAddr)\n\taction.Options.HTTPConfig.Password = kms.NewEmptySecret()\n\taction.Options.HTTPConfig.Body = \"\"\n\taction.Options.HTTPConfig.Parts = []dataprovider.HTTPPart{\n\t\t{\n\t\t\tName:     \"p1\",\n\t\t\tFilepath: \"path\",\n\t\t},\n\t}\n\terr = executeRuleAction(action, params, dataprovider.ConditionOptions{})\n\tassert.Contains(t, getErrorString(err), \"error getting user\")\n\n\taction.Options.HTTPConfig.Parts = nil\n\taction.Options.HTTPConfig.Body = \"{{.ObjectData}}\"\n\t// test disk and transfer quota reset\n\tusername1 := \"user1\"\n\tusername2 := \"user2\"\n\tuser1 := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username1,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username1),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\tuser2 := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username2,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username2),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\tuser2.Filters.PasswordExpiration = 10\n\terr = dataprovider.AddUser(&user1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.AddUser(&user2, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{\n\t\tThreshold: 20,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user2.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{})\n\t// smtp not configured\n\tassert.Error(t, err)\n\n\taction = dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeUserQuotaReset,\n\t}\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err) // no home dir\n\t// create the home dir\n\terr = os.MkdirAll(user1.GetHomeDir(), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user1.GetHomeDir(), \"file.txt\"), []byte(\"user\"), 0666)\n\tassert.NoError(t, err)\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\tuserGet, err := dataprovider.UserExists(username1, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, userGet.UsedQuotaFiles)\n\tassert.Equal(t, int64(4), userGet.UsedQuotaSize)\n\t// simulate another quota scan in progress\n\tassert.True(t, QuotaScans.AddUserQuotaScan(username1, \"\"))\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.True(t, QuotaScans.RemoveUserQuotaScan(username1))\n\t// non matching pattern\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"don't match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no user quota reset executed\")\n\n\taction = dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeUserExpirationCheck,\n\t}\n\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"don't match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no user expiration check executed\")\n\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\n\tdataRetentionAction := dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeDataRetentionCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tRetentionConfig: dataprovider.EventActionDataRetentionConfig{\n\t\t\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:      \"\",\n\t\t\t\t\t\tRetention: 24,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err) // invalid config, no folder path specified\n\tretentionDir := \"testretention\"\n\tdataRetentionAction = dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeDataRetentionCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tRetentionConfig: dataprovider.EventActionDataRetentionConfig{\n\t\t\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:            path.Join(\"/\", retentionDir),\n\t\t\t\t\t\tRetention:       24,\n\t\t\t\t\t\tDeleteEmptyDirs: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\t// create some test files\n\tfile1 := filepath.Join(user1.GetHomeDir(), \"file1.txt\")\n\tfile2 := filepath.Join(user1.GetHomeDir(), retentionDir, \"file2.txt\")\n\tfile3 := filepath.Join(user1.GetHomeDir(), retentionDir, \"file3.txt\")\n\tfile4 := filepath.Join(user1.GetHomeDir(), retentionDir, \"sub\", \"file4.txt\")\n\n\terr = os.MkdirAll(filepath.Dir(file4), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tfor _, f := range []string{file1, file2, file3, file4} {\n\t\terr = os.WriteFile(f, []byte(\"\"), 0666)\n\t\tassert.NoError(t, err)\n\t}\n\ttimeBeforeRetention := time.Now().Add(-48 * time.Hour)\n\terr = os.Chtimes(file1, timeBeforeRetention, timeBeforeRetention)\n\tassert.NoError(t, err)\n\terr = os.Chtimes(file2, timeBeforeRetention, timeBeforeRetention)\n\tassert.NoError(t, err)\n\terr = os.Chtimes(file4, timeBeforeRetention, timeBeforeRetention)\n\tassert.NoError(t, err)\n\n\terr = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\tassert.FileExists(t, file1)\n\tassert.NoFileExists(t, file2)\n\tassert.FileExists(t, file3)\n\tassert.NoDirExists(t, filepath.Dir(file4))\n\t// simulate another check in progress\n\tc := RetentionChecks.Add(RetentionCheck{}, &user1)\n\tassert.NotNil(t, c)\n\terr = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tRetentionChecks.remove(user1.Username)\n\n\terr = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no retention check executed\")\n\n\t// test file exists action\n\taction = dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType:  dataprovider.FilesystemActionExist,\n\t\t\t\tExist: []string{\"/file1.txt\", path.Join(\"/\", retentionDir, \"file3.txt\")},\n\t\t\t},\n\t\t},\n\t}\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no existence check executed\")\n\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\taction.Options.FsConfig.Exist = []string{\"/file1.txt\", path.Join(\"/\", retentionDir, \"file2.txt\")}\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.UpdateUserTransferQuota(&user1, 100, 100, true)\n\tassert.NoError(t, err)\n\n\taction.Type = dataprovider.ActionTypeTransferQuotaReset\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\tuserGet, err = dataprovider.UserExists(username1, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), userGet.UsedDownloadDataTransfer)\n\tassert.Equal(t, int64(0), userGet.UsedUploadDataTransfer)\n\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no transfer quota reset executed\")\n\n\taction.Type = dataprovider.ActionTypeFilesystem\n\taction.Options = dataprovider.BaseEventActionOptions{\n\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\tType: dataprovider.FilesystemActionRename,\n\t\t\tRenames: []dataprovider.RenameConfig{\n\t\t\t\t{\n\t\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\t\tKey:   \"/source\",\n\t\t\t\t\t\tValue: \"/target\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no rename executed\")\n\n\taction.Options = dataprovider.BaseEventActionOptions{\n\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\tType:    dataprovider.FilesystemActionDelete,\n\t\t\tDeletes: []string{\"/dir1\"},\n\t\t},\n\t}\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no delete executed\")\n\n\taction.Options = dataprovider.BaseEventActionOptions{\n\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\tType:    dataprovider.FilesystemActionMkdirs,\n\t\t\tDeletes: []string{\"/dir1\"},\n\t\t},\n\t}\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no mkdir executed\")\n\n\taction.Options = dataprovider.BaseEventActionOptions{\n\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\tType: dataprovider.FilesystemActionCompress,\n\t\t\tCompress: dataprovider.EventActionFsCompress{\n\t\t\t\tName:  \"test.zip\",\n\t\t\t\tPaths: []string{\"/{{.VirtualPath}}\"},\n\t\t\t},\n\t\t},\n\t}\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no file/folder compressed\")\n\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tGroupNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no match\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.Contains(t, getErrorString(err), \"no file/folder compressed\")\n\n\terr = dataprovider.DeleteUser(username1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(username2, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// test folder quota reset\n\tfoldername1 := \"f1\"\n\tfoldername2 := \"f2\"\n\tfolder1 := vfs.BaseVirtualFolder{\n\t\tName:       foldername1,\n\t\tMappedPath: filepath.Join(os.TempDir(), foldername1),\n\t}\n\tfolder2 := vfs.BaseVirtualFolder{\n\t\tName:       foldername2,\n\t\tMappedPath: filepath.Join(os.TempDir(), foldername2),\n\t}\n\terr = dataprovider.AddFolder(&folder1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.AddFolder(&folder2, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\taction = dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeFolderQuotaReset,\n\t}\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: foldername1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err) // no home dir\n\terr = os.MkdirAll(folder1.MappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(folder1.MappedPath, \"file.txt\"), []byte(\"folder\"), 0666)\n\tassert.NoError(t, err)\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: foldername1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\tfolderGet, err := dataprovider.GetFolderByName(foldername1)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, folderGet.UsedQuotaFiles)\n\tassert.Equal(t, int64(6), folderGet.UsedQuotaSize)\n\t// simulate another quota scan in progress\n\tassert.True(t, QuotaScans.AddVFolderQuotaScan(foldername1))\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: foldername1,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\tassert.True(t, QuotaScans.RemoveVFolderQuotaScan(foldername1))\n\n\terr = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"no folder match\",\n\t\t\t},\n\t\t},\n\t})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no folder quota reset executed\")\n\t}\n\n\tbody, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{\n\t\tMethod: http.MethodPost,\n\t}, nil, nil, dataprovider.User{}, &EventParams{}, true)\n\tassert.NoError(t, err)\n\tassert.Nil(t, body)\n\tbody, _, err = getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{\n\t\tMethod: http.MethodPost,\n\t\tBody:   \"test body\",\n\t}, nil, nil, dataprovider.User{}, &EventParams{}, false)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, body)\n\n\terr = os.RemoveAll(folder1.MappedPath)\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteFolder(foldername1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteFolder(foldername2, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestIDPAccountCheckRule(t *testing.T) {\n\t_, _, err := executeIDPAccountCheckRule(dataprovider.EventRule{}, EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no action executed\")\n\t}\n\t_, _, err = executeIDPAccountCheckRule(dataprovider.EventRule{\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: \"n\",\n\t\t\t\t\tType: dataprovider.ActionTypeIDPAccountCheck,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, EventParams{Event: \"invalid\"})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unsupported IDP login event\")\n\t}\n\t// invalid json\n\t_, err = executeAdminCheckAction(&dataprovider.EventActionIDPAccountCheck{TemplateAdmin: \"{\"}, &EventParams{Name: \"missing admin\"})\n\tassert.Error(t, err)\n\t_, err = executeUserCheckAction(&dataprovider.EventActionIDPAccountCheck{TemplateUser: \"[\"}, &EventParams{Name: \"missing user\"})\n\tassert.Error(t, err)\n\t_, err = executeUserCheckAction(&dataprovider.EventActionIDPAccountCheck{TemplateUser: \"{}\"}, &EventParams{Name: \"invalid user template\"})\n\tassert.ErrorIs(t, err, util.ErrValidation)\n\tusername := \"u\"\n\tc := &dataprovider.EventActionIDPAccountCheck{\n\t\tMode:         1,\n\t\tTemplateUser: `{\"username\":\"` + username + `\",\"status\":1,\"home_dir\":\"` + util.JSONEscape(filepath.Join(os.TempDir())) + `\",\"permissions\":{\"/\":[\"*\"]}}`,\n\t}\n\tparams := &EventParams{\n\t\tName:  username,\n\t\tEvent: IDPLoginUser,\n\t}\n\tuser, err := executeUserCheckAction(c, params)\n\tassert.NoError(t, err)\n\tassert.Equal(t, username, user.Username)\n\tassert.Equal(t, 1, user.Status)\n\tuser.Status = 0\n\terr = dataprovider.UpdateUser(user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// the user is not changed\n\tuser, err = executeUserCheckAction(c, params)\n\tassert.NoError(t, err)\n\tassert.Equal(t, username, user.Username)\n\tassert.Equal(t, 0, user.Status)\n\t// change the mode, the user is now updated\n\tc.Mode = 0\n\tuser, err = executeUserCheckAction(c, params)\n\tassert.NoError(t, err)\n\tassert.Equal(t, username, user.Username)\n\tassert.Equal(t, 1, user.Status)\n\tassert.Empty(t, user.Password)\n\tassert.Len(t, user.PublicKeys, 0)\n\tassert.Len(t, user.Filters.TLSCerts, 0)\n\tassert.Empty(t, user.Email)\n\tassert.Empty(t, user.Description)\n\t// Update the profile attribute and make sure they are preserved\n\tuser.Password = \"secret\"\n\tuser.Email = \"example@example.com\"\n\tuser.Filters.AdditionalEmails = []string{\"alias@example.com\"}\n\tuser.Description = \"some desc\"\n\tuser.Filters.TLSCerts = []string{serverCert}\n\tuser.PublicKeys = []string{\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1\"}\n\terr = dataprovider.UpdateUser(user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tuser, err = executeUserCheckAction(c, params)\n\tassert.NoError(t, err)\n\tassert.Equal(t, username, user.Username)\n\tassert.Equal(t, 1, user.Status)\n\tassert.NotEmpty(t, user.Password)\n\tassert.Len(t, user.PublicKeys, 1)\n\tassert.Len(t, user.Filters.TLSCerts, 1)\n\tassert.NotEmpty(t, user.Email)\n\tassert.Len(t, user.Filters.AdditionalEmails, 1)\n\tassert.NotEmpty(t, user.Description)\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// check rule consistency\n\tr := dataprovider.EventRule{\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tType: dataprovider.ActionTypeIDPAccountCheck,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\terr = r.CheckActionsConsistency(\"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"IDP account check action is only supported for IDP login trigger\")\n\t}\n\tr.Trigger = dataprovider.EventTriggerIDPLogin\n\terr = r.CheckActionsConsistency(\"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"IDP account check must be a sync action\")\n\t}\n\tr.Actions[0].Options.ExecuteSync = true\n\terr = r.CheckActionsConsistency(\"\")\n\tassert.NoError(t, err)\n\tr.Actions = append(r.Actions, dataprovider.EventAction{\n\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\tType: dataprovider.ActionTypeCommand,\n\t\t},\n\t\tOptions: dataprovider.EventActionOptions{\n\t\t\tExecuteSync: true,\n\t\t},\n\t\tOrder: 2,\n\t})\n\terr = r.CheckActionsConsistency(\"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"IDP account check must be the only sync action\")\n\t}\n}\n\nfunc TestUserExpirationCheck(t *testing.T) {\n\tusername := \"test_user_expiration_check\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t\tHomeDir:        filepath.Join(os.TempDir(), username),\n\t\t\tExpirationDate: util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour)),\n\t\t},\n\t}\n\tuser.Filters.PasswordExpiration = 5\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tconditions := dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username,\n\t\t\t},\n\t\t},\n\t}\n\terr = executeUserExpirationCheckRuleAction(conditions, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"expired users\")\n\t}\n\t// the check will be skipped, the user is expired\n\terr = executePwdExpirationCheckRuleAction(dataprovider.EventActionPasswordExpiration{Threshold: 10}, conditions, &EventParams{})\n\tassert.NoError(t, err)\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestEventRuleActionsNoGroupMatching(t *testing.T) {\n\tusername := \"test_user_action_group_matching\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t\tHomeDir: filepath.Join(os.TempDir(), username),\n\t\t},\n\t}\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tconditions := dataprovider.ConditionOptions{\n\t\tGroupNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"agroup\",\n\t\t\t},\n\t\t},\n\t}\n\terr = executeDeleteFsRuleAction(nil, nil, conditions, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no delete executed\")\n\t}\n\terr = executeMkdirFsRuleAction(nil, nil, conditions, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no mkdir executed\")\n\t}\n\terr = executeRenameFsRuleAction(nil, nil, conditions, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no rename executed\")\n\t}\n\terr = executeExistFsRuleAction(nil, nil, conditions, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no existence check executed\")\n\t}\n\terr = executeCopyFsRuleAction(nil, nil, conditions, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no copy executed\")\n\t}\n\terr = executeUsersQuotaResetRuleAction(conditions, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no user quota reset executed\")\n\t}\n\terr = executeTransferQuotaResetRuleAction(conditions, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no transfer quota reset executed\")\n\t}\n\terr = executeDataRetentionCheckRuleAction(dataprovider.EventActionDataRetentionConfig{}, conditions, &EventParams{}, \"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no retention check executed\")\n\t}\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestGetFileContent(t *testing.T) {\n\tusername := \"test_user_get_file_content\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t\tHomeDir: filepath.Join(os.TempDir(), username),\n\t\t},\n\t}\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(user.GetHomeDir(), os.ModePerm)\n\tassert.NoError(t, err)\n\tfileContent := []byte(\"test file content\")\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), \"file.txt\"), fileContent, 0666)\n\tassert.NoError(t, err)\n\tconn := NewBaseConnection(xid.New().String(), protocolEventAction, \"\", \"\", user)\n\treplacer := strings.NewReplacer(\"old\", \"new\")\n\tfiles, err := getMailAttachments(conn, []string{\"/file.txt\"}, replacer)\n\tassert.NoError(t, err)\n\tif assert.Len(t, files, 1) {\n\t\tvar b bytes.Buffer\n\t\t_, err = files[0].Writer(&b)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fileContent, b.Bytes())\n\t}\n\t// missing file\n\t_, err = getMailAttachments(conn, []string{\"/file1.txt\"}, replacer)\n\tassert.Error(t, err)\n\t// directory\n\t_, err = getMailAttachments(conn, []string{\"/\"}, replacer)\n\tassert.Error(t, err)\n\t// files too large\n\tcontent := make([]byte, maxAttachmentsSize/2+1)\n\t_, err = rand.Read(content)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), \"file1.txt\"), content, 0666)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), \"file2.txt\"), content, 0666)\n\tassert.NoError(t, err)\n\tfiles, err = getMailAttachments(conn, []string{\"/file1.txt\"}, replacer)\n\tassert.NoError(t, err)\n\tif assert.Len(t, files, 1) {\n\t\tvar b bytes.Buffer\n\t\t_, err = files[0].Writer(&b)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, content, b.Bytes())\n\t}\n\t_, err = getMailAttachments(conn, []string{\"/file1.txt\", \"/file2.txt\"}, replacer)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"size too large\")\n\t}\n\t// change the filesystem provider\n\tuser.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"pwd\")\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tconn = NewBaseConnection(xid.New().String(), protocolEventAction, \"\", \"\", user)\n\t// the file is not encrypted so reading the encryption header will fail\n\tfiles, err = getMailAttachments(conn, []string{\"/file.txt\"}, replacer)\n\tassert.NoError(t, err)\n\tif assert.Len(t, files, 1) {\n\t\tvar b bytes.Buffer\n\t\t_, err = files[0].Writer(&b)\n\t\tassert.Error(t, err)\n\t}\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestFilesystemActionErrors(t *testing.T) {\n\terr := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, dataprovider.ConditionOptions{}, &EventParams{})\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unsupported filesystem action\")\n\t}\n\tusername := \"test_user_for_actions\"\n\ttestReplacer := strings.NewReplacer(\"old\", \"new\")\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t\tHomeDir: filepath.Join(os.TempDir(), username),\n\t\t},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: \"127.0.0.1:4022\",\n\t\t\t\t\tUsername: username,\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(\"pwd\"),\n\t\t\t},\n\t\t},\n\t}\n\terr = executeEmailRuleAction(dataprovider.EventActionEmailConfig{\n\t\tRecipients:  []string{\"test@example.net\"},\n\t\tSubject:     \"subject\",\n\t\tBody:        \"body\",\n\t\tAttachments: []string{\"/file.txt\"},\n\t}, &EventParams{\n\t\tsender: username,\n\t})\n\tassert.Error(t, err)\n\tconn := NewBaseConnection(\"\", protocolEventAction, \"\", \"\", user)\n\terr = executeDeleteFileFsAction(conn, \"\", nil)\n\tassert.Error(t, err)\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// check root fs fails\n\terr = executeDeleteFsActionForUser(nil, testReplacer, user)\n\tassert.Error(t, err)\n\terr = executeMkDirsFsActionForUser(nil, testReplacer, user)\n\tassert.Error(t, err)\n\terr = executeRenameFsActionForUser(nil, testReplacer, user)\n\tassert.Error(t, err)\n\terr = executeExistFsActionForUser(nil, testReplacer, user)\n\tassert.Error(t, err)\n\terr = executeCopyFsActionForUser(nil, testReplacer, user)\n\tassert.Error(t, err)\n\terr = executeCompressFsActionForUser(dataprovider.EventActionFsCompress{}, testReplacer, user)\n\tassert.Error(t, err)\n\t_, _, _, _, err = getFileWriter(conn, \"/path.txt\", -1) //nolint:dogsled\n\tassert.Error(t, err)\n\terr = executeEmailRuleAction(dataprovider.EventActionEmailConfig{\n\t\tRecipients:  []string{\"test@example.net\"},\n\t\tSubject:     \"subject\",\n\t\tBody:        \"body\",\n\t\tAttachments: []string{\"/file1.txt\"},\n\t}, &EventParams{\n\t\tsender: username,\n\t})\n\tassert.Error(t, err)\n\tfn := getFileContentFn(NewBaseConnection(\"\", protocolEventAction, \"\", \"\", user), \"/f.txt\", 1234)\n\tvar b bytes.Buffer\n\t_, err = fn(&b)\n\tassert.Error(t, err)\n\terr = executeHTTPRuleAction(dataprovider.EventActionHTTPConfig{\n\t\tEndpoint: \"http://127.0.0.1:9999/\",\n\t\tMethod:   http.MethodPost,\n\t\tParts: []dataprovider.HTTPPart{\n\t\t\t{\n\t\t\t\tName:     \"p1\",\n\t\t\t\tFilepath: \"/filepath\",\n\t\t\t},\n\t\t},\n\t}, &EventParams{\n\t\tsender: username,\n\t})\n\tassert.Error(t, err)\n\tuser.FsConfig.Provider = sdk.LocalFilesystemProvider\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermUpload}\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = executeRenameFsActionForUser([]dataprovider.RenameConfig{\n\t\t{\n\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\tKey:   \"/p1\",\n\t\t\t\tValue: \"/p1\",\n\t\t\t},\n\t\t},\n\t}, testReplacer, user)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"the rename source and target cannot be the same\")\n\t}\n\terr = executeRuleAction(dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionRename,\n\t\t\t\tRenames: []dataprovider.RenameConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\t\t\tKey:   \"/p2\",\n\t\t\t\t\t\t\tValue: \"/p2\",\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}, &EventParams{}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: username,\n\t\t\t},\n\t\t},\n\t})\n\tassert.Error(t, err)\n\n\tif runtime.GOOS != osWindows {\n\t\tdirPath := filepath.Join(user.HomeDir, \"adir\", \"sub\")\n\t\terr := os.MkdirAll(dirPath, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tfilePath := filepath.Join(dirPath, \"f.dat\")\n\t\terr = os.WriteFile(filePath, []byte(\"test file content\"), 0666)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(dirPath, 0001)\n\t\tassert.NoError(t, err)\n\n\t\terr = executeDeleteFsActionForUser([]string{\"/adir/sub\"}, testReplacer, user)\n\t\tassert.Error(t, err)\n\t\terr = executeDeleteFsActionForUser([]string{\"/adir/sub/f.dat\"}, testReplacer, user)\n\t\tassert.Error(t, err)\n\t\terr = os.Chmod(dirPath, 0555)\n\t\tassert.NoError(t, err)\n\t\terr = executeDeleteFsActionForUser([]string{\"/adir/sub/f.dat\"}, testReplacer, user)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"unable to remove file\")\n\t\t}\n\t\terr = executeRuleAction(dataprovider.BaseEventAction{\n\t\t\tType: dataprovider.ActionTypeFilesystem,\n\t\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\t\tType:    dataprovider.FilesystemActionDelete,\n\t\t\t\t\tDeletes: []string{\"/adir/sub/f.dat\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}, &EventParams{}, dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: username,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tassert.Error(t, err)\n\n\t\terr = executeMkDirsFsActionForUser([]string{\"/adir/sub/sub\"}, testReplacer, user)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"unable to create dir\")\n\t\t}\n\t\terr = executeMkDirsFsActionForUser([]string{\"/adir/sub/sub/sub\"}, testReplacer, user)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"unable to check parent dirs\")\n\t\t}\n\n\t\terr = executeRuleAction(dataprovider.BaseEventAction{\n\t\t\tType: dataprovider.ActionTypeFilesystem,\n\t\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\t\tType:   dataprovider.FilesystemActionMkdirs,\n\t\t\t\t\tMkDirs: []string{\"/adir/sub/sub1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}, &EventParams{}, dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: username,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tassert.Error(t, err)\n\n\t\terr = os.Chmod(dirPath, os.ModePerm)\n\t\tassert.NoError(t, err)\n\n\t\tconn = NewBaseConnection(\"\", protocolEventAction, \"\", \"\", user)\n\t\twr := &zipWriterWrapper{\n\t\t\tName:    \"test.zip\",\n\t\t\tWriter:  zip.NewWriter(bytes.NewBuffer(nil)),\n\t\t\tEntries: map[string]bool{},\n\t\t}\n\t\terr = addZipEntry(wr, conn, \"/adir/sub/f.dat\", \"/adir/sub/sub\", nil, 0)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, getErrorString(err), \"is outside base dir\")\n\t}\n\n\twr := &zipWriterWrapper{\n\t\tName:    xid.New().String() + \".zip\",\n\t\tWriter:  zip.NewWriter(bytes.NewBuffer(nil)),\n\t\tEntries: map[string]bool{},\n\t}\n\terr = addZipEntry(wr, conn, \"/p1\", \"/\", nil, 2000)\n\tassert.ErrorIs(t, err, util.ErrRecursionTooDeep)\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) {\n\toldProviderConf := dataprovider.GetProviderConfig()\n\tproviderConf := dataprovider.GetProviderConfig()\n\tproviderConf.TrackQuota = 0\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tusername := \"u1\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.LocalFilesystemProvider,\n\t\t},\n\t}\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = os.MkdirAll(user.GetHomeDir(), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeUserQuotaReset},\n\t\t&EventParams{}, dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: username,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tassert.Error(t, err)\n\n\terr = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeTransferQuotaReset},\n\t\t&EventParams{}, dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: username,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tassert.Error(t, err)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tfoldername := \"f1\"\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       foldername,\n\t\tMappedPath: filepath.Join(os.TempDir(), foldername),\n\t}\n\terr = dataprovider.AddFolder(&folder, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(folder.MappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\n\terr = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeFolderQuotaReset},\n\t\t&EventParams{}, dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: foldername,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tassert.Error(t, err)\n\n\terr = os.RemoveAll(folder.MappedPath)\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteFolder(foldername, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = dataprovider.Initialize(oldProviderConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestScheduledActions(t *testing.T) {\n\tstartEventScheduler()\n\tbackupsPath := filepath.Join(os.TempDir(), \"backups\")\n\terr := os.RemoveAll(backupsPath)\n\tassert.NoError(t, err)\n\tnow := time.Now().UTC().Format(dateTimeMillisFormat)\n\t// The backup action sets the home directory to the backup path.\n\texpectedDirPath := filepath.Join(backupsPath, fmt.Sprintf(\"%s_%s_%s\", now[0:4], now[5:7], now[8:10]))\n\n\taction1 := &dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeBackup,\n\t}\n\terr = dataprovider.AddEventAction(action1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\taction2 := &dataprovider.BaseEventAction{\n\t\tName: \"action2\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType:   dataprovider.FilesystemActionMkdirs,\n\t\t\t\tMkDirs: []string{\"{{.Year}}_{{.Month}}_{{.Day}}\"},\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddEventAction(action2, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\trule := &dataprovider.EventRule{\n\t\tName:    \"rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerSchedule,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tSchedules: []dataprovider.Schedule{\n\t\t\t\t{\n\t\t\t\t\tHours:      \"11\",\n\t\t\t\t\tDayOfWeek:  \"*\",\n\t\t\t\t\tDayOfMonth: \"*\",\n\t\t\t\t\tMonth:      \"*\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\n\tjob := eventCronJob{\n\t\truleName: rule.Name,\n\t}\n\tjob.Run() // rule not found\n\tassert.NoDirExists(t, backupsPath)\n\n\terr = dataprovider.AddEventRule(rule, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tjob.Run()\n\tassert.DirExists(t, backupsPath)\n\tassert.DirExists(t, expectedDirPath)\n\n\taction1.Type = dataprovider.ActionTypeEmail\n\taction1.Options = dataprovider.BaseEventActionOptions{\n\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\tRecipients:  []string{\"example@example.com\"},\n\t\t\tSubject:     \"test with attachments\",\n\t\t\tBody:        \"body\",\n\t\t\tAttachments: []string{\"/file1.txt\"},\n\t\t},\n\t}\n\terr = dataprovider.UpdateEventAction(action1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tjob.Run() // action is not compatible with a scheduled rule\n\n\terr = dataprovider.DeleteEventRule(rule.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteEventAction(action1.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteEventAction(action2.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(backupsPath)\n\tassert.NoError(t, err)\n\n\tstopEventScheduler()\n}\n\nfunc TestEventParamsCopy(t *testing.T) {\n\tparams := EventParams{\n\t\tName:            \"name\",\n\t\tEvent:           \"event\",\n\t\tExtension:       \"ext\",\n\t\tStatus:          1,\n\t\terrors:          []string{\"error1\"},\n\t\tretentionChecks: []executedRetentionCheck{},\n\t}\n\tparamsCopy := params.getACopy()\n\tassert.Equal(t, params, *paramsCopy)\n\tparams.Name = \"name mod\"\n\tparamsCopy.Event = \"event mod\"\n\tparamsCopy.Status = 2\n\tparams.errors = append(params.errors, \"error2\")\n\tparamsCopy.errors = append(paramsCopy.errors, \"error3\")\n\tassert.Equal(t, []string{\"error1\", \"error3\"}, paramsCopy.errors)\n\tassert.Equal(t, []string{\"error1\", \"error2\"}, params.errors)\n\tassert.Equal(t, \"name mod\", params.Name)\n\tassert.Equal(t, \"name\", paramsCopy.Name)\n\tassert.Equal(t, \"event\", params.Event)\n\tassert.Equal(t, \"event mod\", paramsCopy.Event)\n\tassert.Equal(t, 1, params.Status)\n\tassert.Equal(t, 2, paramsCopy.Status)\n\tparams = EventParams{\n\t\tretentionChecks: []executedRetentionCheck{\n\t\t\t{\n\t\t\t\tUsername:   \"u\",\n\t\t\t\tActionName: \"a\",\n\t\t\t\tResults: []folderRetentionCheckResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:      \"p\",\n\t\t\t\t\t\tRetention: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tparamsCopy = params.getACopy()\n\trequire.Len(t, paramsCopy.retentionChecks, 1)\n\tparamsCopy.retentionChecks[0].Username = \"u_copy\"\n\tparamsCopy.retentionChecks[0].ActionName = \"a_copy\"\n\trequire.Len(t, paramsCopy.retentionChecks[0].Results, 1)\n\tparamsCopy.retentionChecks[0].Results[0].Path = \"p_copy\"\n\tparamsCopy.retentionChecks[0].Results[0].Retention = 2\n\tassert.Equal(t, \"u\", params.retentionChecks[0].Username)\n\tassert.Equal(t, \"a\", params.retentionChecks[0].ActionName)\n\tassert.Equal(t, \"p\", params.retentionChecks[0].Results[0].Path)\n\tassert.Equal(t, 1, params.retentionChecks[0].Results[0].Retention)\n\tassert.Equal(t, \"u_copy\", paramsCopy.retentionChecks[0].Username)\n\tassert.Equal(t, \"a_copy\", paramsCopy.retentionChecks[0].ActionName)\n\tassert.Equal(t, \"p_copy\", paramsCopy.retentionChecks[0].Results[0].Path)\n\tassert.Equal(t, 2, paramsCopy.retentionChecks[0].Results[0].Retention)\n\tassert.Nil(t, params.IDPCustomFields)\n\tparams.addIDPCustomFields(nil)\n\tassert.Nil(t, params.IDPCustomFields)\n\tparams.IDPCustomFields = &map[string]string{\n\t\t\"field1\": \"val1\",\n\t}\n\tparamsCopy = params.getACopy()\n\tfor k, v := range *paramsCopy.IDPCustomFields {\n\t\tassert.Equal(t, \"field1\", k)\n\t\tassert.Equal(t, \"val1\", v)\n\t}\n\tassert.Equal(t, params.IDPCustomFields, paramsCopy.IDPCustomFields)\n\t(*paramsCopy.IDPCustomFields)[\"field1\"] = \"val2\"\n\tassert.NotEqual(t, params.IDPCustomFields, paramsCopy.IDPCustomFields)\n\tparams.Metadata = map[string]string{\"key\": \"value\"}\n\tparamsCopy = params.getACopy()\n\tparams.Metadata[\"key1\"] = \"value1\"\n\trequire.Equal(t, map[string]string{\"key\": \"value\"}, paramsCopy.Metadata)\n}\n\nfunc TestEventParamsStatusFromError(t *testing.T) {\n\tparams := EventParams{Status: 1}\n\tparams.AddError(os.ErrNotExist)\n\tassert.Equal(t, 1, params.Status)\n\n\tparams = EventParams{Status: 1, updateStatusFromError: true}\n\tparams.AddError(os.ErrNotExist)\n\tassert.Equal(t, 2, params.Status)\n}\n\ntype testWriter struct {\n\terrTest  error\n\tsentinel string\n}\n\nfunc (w *testWriter) Write(p []byte) (int, error) {\n\tif w.errTest != nil {\n\t\treturn 0, w.errTest\n\t}\n\tif w.sentinel == string(p) {\n\t\treturn 0, io.ErrUnexpectedEOF\n\t}\n\treturn len(p), nil\n}\n\nfunc TestWriteHTTPPartsError(t *testing.T) {\n\tm := multipart.NewWriter(&testWriter{\n\t\terrTest: io.ErrShortWrite,\n\t})\n\n\terr := writeHTTPPart(m, dataprovider.HTTPPart{}, nil, nil, nil, &EventParams{}, false)\n\tassert.ErrorIs(t, err, io.ErrShortWrite)\n\n\tbody := \"test body\"\n\tm = multipart.NewWriter(&testWriter{sentinel: body})\n\terr = writeHTTPPart(m, dataprovider.HTTPPart{\n\t\tBody: body,\n\t}, nil, nil, nil, &EventParams{}, false)\n\tassert.ErrorIs(t, err, io.ErrUnexpectedEOF)\n}\n\nfunc TestReplacePathsPlaceholders(t *testing.T) {\n\treplacer := strings.NewReplacer(\"{{.VirtualPath}}\", \"/path1\")\n\tpaths := []string{\"{{.VirtualPath}}\", \"/path1\"}\n\tpaths = replacePathsPlaceholders(paths, replacer)\n\tassert.Equal(t, []string{\"/path1\"}, paths)\n\tpaths = []string{\"{{.VirtualPath}}\", \"/path2\"}\n\tpaths = replacePathsPlaceholders(paths, replacer)\n\tassert.Equal(t, []string{\"/path1\", \"/path2\"}, paths)\n}\n\nfunc TestEstimateZipSizeErrors(t *testing.T) {\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"u\",\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), \"u\"),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t\tQuotaSize: 1000,\n\t\t},\n\t}\n\terr := dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(u.GetHomeDir(), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn := NewBaseConnection(\"\", ProtocolFTP, \"\", \"\", u)\n\t_, _, _, _, err = getFileWriter(conn, \"/missing/path/file.txt\", -1) //nolint:dogsled\n\tassert.Error(t, err)\n\t_, err = getSizeForPath(conn, \"/missing\", vfs.NewFileInfo(\"missing\", true, 0, time.Now(), false))\n\tassert.True(t, conn.IsNotExistError(err))\n\tif runtime.GOOS != osWindows {\n\t\terr = os.MkdirAll(filepath.Join(u.HomeDir, \"d1\", \"d2\", \"sub\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(filepath.Join(u.HomeDir, \"d1\", \"d2\", \"sub\", \"file.txt\"), []byte(\"data\"), 0666)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(filepath.Join(u.HomeDir, \"d1\", \"d2\"), 0001)\n\t\tassert.NoError(t, err)\n\t\tsize, err := estimateZipSize(conn, \"/archive.zip\", []string{\"/d1\"})\n\t\tassert.Error(t, err, \"size %d\", size)\n\t\terr = os.Chmod(filepath.Join(u.HomeDir, \"d1\", \"d2\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\terr = dataprovider.DeleteUser(u.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(u.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestOnDemandRule(t *testing.T) {\n\ta := &dataprovider.BaseEventAction{\n\t\tName:    \"a\",\n\t\tType:    dataprovider.ActionTypeBackup,\n\t\tOptions: dataprovider.BaseEventActionOptions{},\n\t}\n\terr := dataprovider.AddEventAction(a, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tr := &dataprovider.EventRule{\n\t\tName:    \"test on demand rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerOnDemand,\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: a.Name,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddEventRule(r, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = RunOnDemandRule(r.Name)\n\tassert.NoError(t, err)\n\n\tr.Status = 0\n\terr = dataprovider.UpdateEventRule(r, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = RunOnDemandRule(r.Name)\n\tassert.ErrorIs(t, err, util.ErrValidation)\n\tassert.Contains(t, err.Error(), \"is inactive\")\n\n\tr.Status = 1\n\tr.Trigger = dataprovider.EventTriggerCertificate\n\terr = dataprovider.UpdateEventRule(r, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = RunOnDemandRule(r.Name)\n\tassert.ErrorIs(t, err, util.ErrValidation)\n\tassert.Contains(t, err.Error(), \"is not defined as on-demand\")\n\n\ta1 := &dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients:  []string{\"example@example.org\"},\n\t\t\t\tSubject:     \"subject\",\n\t\t\t\tBody:        \"body\",\n\t\t\t\tAttachments: []string{\"/{{.VirtualPath}}\"},\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddEventAction(a1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tr.Trigger = dataprovider.EventTriggerOnDemand\n\tr.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: a1.Name,\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.UpdateEventRule(r, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = RunOnDemandRule(r.Name)\n\tassert.ErrorIs(t, err, util.ErrValidation)\n\tassert.Contains(t, err.Error(), \"incosistent actions\")\n\n\terr = dataprovider.DeleteEventRule(r.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteEventAction(a.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteEventAction(a1.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = RunOnDemandRule(r.Name)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n}\n\nfunc getErrorString(err error) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\treturn err.Error()\n}\n\nfunc TestHTTPEndpointWithPlaceholders(t *testing.T) {\n\tc := dataprovider.EventActionHTTPConfig{\n\t\tEndpoint: \"http://127.0.0.1:8080/base/url/{{.Name}}/{{.VirtualPath}}/upload\",\n\t\tQueryParameters: []dataprovider.KeyValue{\n\t\t\t{\n\t\t\t\tKey:   \"u\",\n\t\t\t\tValue: \"{{.Name}}\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tKey:   \"p\",\n\t\t\t\tValue: \"{{.VirtualPath}}\",\n\t\t\t},\n\t\t},\n\t}\n\tname := \"uname\"\n\tvPath := \"/a dir/@ file.txt\"\n\treplacer := strings.NewReplacer(\"{{.Name}}\", name, \"{{.VirtualPath}}\", vPath)\n\tu, err := getHTTPRuleActionEndpoint(&c, replacer)\n\tassert.NoError(t, err)\n\texpected := \"http://127.0.0.1:8080/base/url/\" + url.PathEscape(name) + \"/\" + url.PathEscape(vPath) +\n\t\t\"/upload?\" + \"p=\" + url.QueryEscape(vPath) + \"&u=\" + url.QueryEscape(name)\n\tassert.Equal(t, expected, u)\n\n\tc.Endpoint = \"http://127.0.0.1/upload\"\n\tu, err = getHTTPRuleActionEndpoint(&c, replacer)\n\tassert.NoError(t, err)\n\texpected = c.Endpoint + \"?p=\" + url.QueryEscape(vPath) + \"&u=\" + url.QueryEscape(name)\n\tassert.Equal(t, expected, u)\n}\n\nfunc TestMetadataReplacement(t *testing.T) {\n\tparams := &EventParams{\n\t\tMetadata: map[string]string{\n\t\t\t\"key\": \"value\",\n\t\t},\n\t}\n\treplacements := params.getStringReplacements(false, 0)\n\treplacer := strings.NewReplacer(replacements...)\n\treader, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{Body: \"{{.Metadata}} {{.MetadataString}}\"}, replacer, nil, dataprovider.User{}, params, false)\n\trequire.NoError(t, err)\n\tdata, err := io.ReadAll(reader)\n\trequire.NoError(t, err)\n\tassert.Equal(t, `{\"key\":\"value\"} {\\\"key\\\":\\\"value\\\"}`, string(data))\n}\n\nfunc TestUserInactivityCheck(t *testing.T) {\n\tusername1 := \"user1\"\n\tusername2 := \"user2\"\n\tuser1 := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username1,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username1),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\tuser2 := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username2,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username2),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\tdays := user1.InactivityDays(time.Now().Add(10*24*time.Hour + 5*time.Second))\n\tassert.Equal(t, 0, days)\n\n\tuser2.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())\n\terr := executeInactivityCheckForUser(&user2, dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t}, time.Now().Add(12*24*time.Hour))\n\tassert.Error(t, err)\n\tuser2.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())\n\terr = executeInactivityCheckForUser(&user2, dataprovider.EventActionUserInactivity{\n\t\tDeleteThreshold: 10,\n\t}, time.Now().Add(12*24*time.Hour))\n\tassert.Error(t, err)\n\n\terr = dataprovider.AddUser(&user1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.AddUser(&user2, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser1, err = dataprovider.UserExists(username1, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, user1.Status)\n\tdays = user1.InactivityDays(time.Now().Add(10*24*time.Hour + 5*time.Second))\n\tassert.Equal(t, 10, days)\n\tdays = user1.InactivityDays(time.Now().Add(-10*24*time.Hour + 5*time.Second))\n\tassert.Equal(t, -9, days)\n\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: \"not matching\",\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now().Add(12*24*time.Hour))\n\tassert.NoError(t, err)\n\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user1.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now())\n\tassert.NoError(t, err) // no action\n\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user1.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now().Add(-12*24*time.Hour))\n\tassert.NoError(t, err) // no action\n\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t\tDeleteThreshold:  20,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user1.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now().Add(30*24*time.Hour))\n\t// both thresholds exceeded, the user will be disabled\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"executed inactivity check actions for users\")\n\t}\n\tuser1, err = dataprovider.UserExists(username1, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, user1.Status)\n\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user1.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now().Add(30*24*time.Hour))\n\tassert.NoError(t, err) // already disabled, no action\n\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t\tDeleteThreshold:  20,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user1.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now().Add(-30*24*time.Hour))\n\tassert.NoError(t, err)\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t\tDeleteThreshold:  20,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user1.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now())\n\tassert.NoError(t, err)\n\tuser1, err = dataprovider.UserExists(username1, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, user1.Status)\n\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t\tDeleteThreshold:  20,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user1.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now().Add(30*24*time.Hour)) // the user is disabled, will be now deleted\n\tassert.Error(t, err)\n\t_, err = dataprovider.UserExists(username1, \"\")\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\n\terr = executeUserInactivityCheckRuleAction(dataprovider.EventActionUserInactivity{\n\t\tDeleteThreshold: 20,\n\t}, dataprovider.ConditionOptions{\n\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user2.Username,\n\t\t\t},\n\t\t},\n\t}, &EventParams{}, time.Now().Add(30*24*time.Hour)) // no disable threshold, user deleted\n\tassert.Error(t, err)\n\t_, err = dataprovider.UserExists(username2, \"\")\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\n\terr = dataprovider.DeleteUser(username1, \"\", \"\", \"\")\n\tassert.Error(t, err)\n\terr = dataprovider.DeleteUser(username2, \"\", \"\", \"\")\n\tassert.Error(t, err)\n}\n"
  },
  {
    "path": "internal/common/eventscheduler.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"time\"\n\n\t\"github.com/robfig/cron/v3\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\teventScheduler *cron.Cron\n)\n\nfunc stopEventScheduler() {\n\tif eventScheduler != nil {\n\t\teventScheduler.Stop()\n\t\teventScheduler = nil\n\t}\n}\n\nfunc startEventScheduler() {\n\tstopEventScheduler()\n\n\toptions := []cron.Option{\n\t\tcron.WithLogger(cron.DiscardLogger),\n\t}\n\tif !dataprovider.UseLocalTime() {\n\t\teventManagerLog(logger.LevelDebug, \"use UTC time for the scheduler\")\n\t\toptions = append(options, cron.WithLocation(time.UTC))\n\t}\n\n\teventScheduler = cron.New(options...)\n\teventManager.loadRules()\n\t_, err := eventScheduler.AddFunc(\"@every 10m\", eventManager.loadRules)\n\tutil.PanicOnError(err)\n\teventScheduler.Start()\n}\n"
  },
  {
    "path": "internal/common/httpauth.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"encoding/csv\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/GehirnInc/crypt/apr1_crypt\"\n\t\"github.com/GehirnInc/crypt/md5_crypt\"\n\t\"golang.org/x/crypto/bcrypt\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\t// HTTPAuthenticationHeader defines the HTTP authentication\n\tHTTPAuthenticationHeader = \"WWW-Authenticate\"\n\tmd5CryptPwdPrefix        = \"$1$\"\n\tapr1CryptPwdPrefix       = \"$apr1$\"\n)\n\nvar (\n\tbcryptPwdPrefixes = []string{\"$2a$\", \"$2$\", \"$2x$\", \"$2y$\", \"$2b$\"}\n)\n\n// HTTPAuthProvider defines the interface for HTTP auth providers\ntype HTTPAuthProvider interface {\n\tValidateCredentials(username, password string) bool\n\tIsEnabled() bool\n}\n\ntype basicAuthProvider struct {\n\tPath string\n\tsync.RWMutex\n\tInfo  os.FileInfo\n\tUsers map[string]string\n}\n\n// NewBasicAuthProvider returns an HTTPAuthProvider implementing Basic Auth\nfunc NewBasicAuthProvider(authUserFile string) (HTTPAuthProvider, error) {\n\tbasicAuthProvider := basicAuthProvider{\n\t\tPath:  authUserFile,\n\t\tInfo:  nil,\n\t\tUsers: make(map[string]string),\n\t}\n\treturn &basicAuthProvider, basicAuthProvider.loadUsers()\n}\n\nfunc (p *basicAuthProvider) IsEnabled() bool {\n\treturn p.Path != \"\"\n}\n\nfunc (p *basicAuthProvider) isReloadNeeded(info os.FileInfo) bool {\n\tp.RLock()\n\tdefer p.RUnlock()\n\n\treturn p.Info == nil || p.Info.ModTime() != info.ModTime() || p.Info.Size() != info.Size()\n}\n\nfunc (p *basicAuthProvider) loadUsers() error {\n\tif !p.IsEnabled() {\n\t\treturn nil\n\t}\n\tinfo, err := os.Stat(p.Path)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to stat basic auth users file: %v\", err)\n\t\treturn err\n\t}\n\tif p.isReloadNeeded(info) {\n\t\tr, err := os.Open(p.Path)\n\t\tif err != nil {\n\t\t\tlogger.Debug(logSender, \"\", \"unable to open basic auth users file: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tdefer r.Close()\n\t\treader := csv.NewReader(r)\n\t\treader.Comma = ':'\n\t\treader.Comment = '#'\n\t\treader.TrimLeadingSpace = true\n\t\trecords, err := reader.ReadAll()\n\t\tif err != nil {\n\t\t\tlogger.Debug(logSender, \"\", \"unable to parse basic auth users file: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tp.Lock()\n\t\tdefer p.Unlock()\n\n\t\tp.Users = make(map[string]string)\n\t\tfor _, record := range records {\n\t\t\tif len(record) == 2 {\n\t\t\t\tp.Users[record[0]] = record[1]\n\t\t\t}\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"number of users loaded for httpd basic auth: %v\", len(p.Users))\n\t\tp.Info = info\n\t}\n\treturn nil\n}\n\nfunc (p *basicAuthProvider) getHashedPassword(username string) (string, bool) {\n\terr := p.loadUsers()\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\tp.RLock()\n\tdefer p.RUnlock()\n\n\tpwd, ok := p.Users[username]\n\treturn pwd, ok\n}\n\n// ValidateCredentials returns true if the credentials are valid\nfunc (p *basicAuthProvider) ValidateCredentials(username, password string) bool {\n\tif hashedPwd, ok := p.getHashedPassword(username); ok {\n\t\tif util.IsStringPrefixInSlice(hashedPwd, bcryptPwdPrefixes) {\n\t\t\terr := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(password))\n\t\t\treturn err == nil\n\t\t}\n\t\tif strings.HasPrefix(hashedPwd, md5CryptPwdPrefix) {\n\t\t\tcrypter := md5_crypt.New()\n\t\t\terr := crypter.Verify(hashedPwd, []byte(password))\n\t\t\treturn err == nil\n\t\t}\n\t\tif strings.HasPrefix(hashedPwd, apr1CryptPwdPrefix) {\n\t\t\tcrypter := apr1_crypt.New()\n\t\t\terr := crypter.Verify(hashedPwd, []byte(password))\n\t\t\treturn err == nil\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/common/httpauth_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBasicAuth(t *testing.T) {\n\thttpAuth, err := NewBasicAuthProvider(\"\")\n\trequire.NoError(t, err)\n\trequire.False(t, httpAuth.IsEnabled())\n\n\t_, err = NewBasicAuthProvider(\"missing path\")\n\trequire.Error(t, err)\n\n\tauthUserFile := filepath.Join(os.TempDir(), \"http_users.txt\")\n\tauthUserData := []byte(\"test1:$2y$05$bcHSED7aO1cfLto6ZdDBOOKzlwftslVhtpIkRhAtSa4GuLmk5mola\\n\")\n\terr = os.WriteFile(authUserFile, authUserData, os.ModePerm)\n\trequire.NoError(t, err)\n\n\thttpAuth, err = NewBasicAuthProvider(authUserFile)\n\trequire.NoError(t, err)\n\trequire.True(t, httpAuth.IsEnabled())\n\trequire.False(t, httpAuth.ValidateCredentials(\"test1\", \"wrong1\"))\n\trequire.False(t, httpAuth.ValidateCredentials(\"test2\", \"password2\"))\n\trequire.True(t, httpAuth.ValidateCredentials(\"test1\", \"password1\"))\n\n\tauthUserData = append(authUserData, []byte(\"test2:$1$OtSSTL8b$bmaCqEksI1e7rnZSjsIDR1\\n\")...)\n\terr = os.WriteFile(authUserFile, authUserData, os.ModePerm)\n\trequire.NoError(t, err)\n\trequire.False(t, httpAuth.ValidateCredentials(\"test2\", \"wrong2\"))\n\trequire.True(t, httpAuth.ValidateCredentials(\"test2\", \"password2\"))\n\n\tauthUserData = append(authUserData, []byte(\"test2:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\\n\")...)\n\terr = os.WriteFile(authUserFile, authUserData, os.ModePerm)\n\trequire.NoError(t, err)\n\trequire.False(t, httpAuth.ValidateCredentials(\"test2\", \"wrong2\"))\n\trequire.True(t, httpAuth.ValidateCredentials(\"test2\", \"password2\"))\n\n\tauthUserData = append(authUserData, []byte(\"test3:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\\n\")...)\n\terr = os.WriteFile(authUserFile, authUserData, os.ModePerm)\n\trequire.NoError(t, err)\n\trequire.False(t, httpAuth.ValidateCredentials(\"test3\", \"password3\"))\n\n\tauthUserData = append(authUserData, []byte(\"test4:$invalid$gLnIkRIf$Xr/6$aJfmIr$ihP4b2N2tcs/\\n\")...)\n\terr = os.WriteFile(authUserFile, authUserData, os.ModePerm)\n\trequire.NoError(t, err)\n\trequire.False(t, httpAuth.ValidateCredentials(\"test4\", \"password3\"))\n\n\tif runtime.GOOS != \"windows\" {\n\t\tauthUserData = append(authUserData, []byte(\"test5:$apr1$gLnIkRIf$Xr/6aJfmIrihP4b2N2tcs/\\n\")...)\n\t\terr = os.WriteFile(authUserFile, authUserData, os.ModePerm)\n\t\trequire.NoError(t, err)\n\t\terr = os.Chmod(authUserFile, 0001)\n\t\trequire.NoError(t, err)\n\t\trequire.False(t, httpAuth.ValidateCredentials(\"test5\", \"password2\"))\n\t\terr = os.Chmod(authUserFile, os.ModePerm)\n\t\trequire.NoError(t, err)\n\t}\n\tauthUserData = append(authUserData, []byte(\"\\\"foo\\\"bar\\\"\\r\\n\")...)\n\terr = os.WriteFile(authUserFile, authUserData, os.ModePerm)\n\trequire.NoError(t, err)\n\trequire.False(t, httpAuth.ValidateCredentials(\"test2\", \"password2\"))\n\n\terr = os.Remove(authUserFile)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/common/protocol_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common_test\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"math\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t_ \"github.com/mattn/go-sqlite3\"\n\t\"github.com/mhale/smtpd\"\n\t\"github.com/minio/sio\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/pquerna/otp\"\n\t\"github.com/pquerna/otp/totp\"\n\t\"github.com/rs/xid\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/studio-b12/gowebdav\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpdtest\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\nconst (\n\thttpAddr              = \"127.0.0.1:9999\"\n\thttpProxyAddr         = \"127.0.0.1:7777\"\n\tsftpServerAddr        = \"127.0.0.1:4022\"\n\tsmtpServerAddr        = \"127.0.0.1:2525\"\n\twebDavServerPort      = 9191\n\thttpFsPort            = 34567\n\tdefaultUsername       = \"test_common_sftp\"\n\tdefaultPassword       = \"test_password\"\n\tdefaultSFTPUsername   = \"test_common_sftpfs_user\"\n\tdefaultHTTPFsUsername = \"httpfs_ftp_user\"\n\thttpFsWellKnowDir     = \"/wellknow\"\n\tosWindows             = \"windows\"\n\ttestFileName          = \"test_file_common_sftp.dat\"\n\ttestDir               = \"test_dir_common\"\n\ttestPubKey            = \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1\"\n\ttestPrivateKey        = `-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAYEAtN449A/nY5O6cSH/9Doa8a3ISU0WZJaHydTaCLuO+dkqtNpnV5mq\nzFbKidXAI1eSwVctw9ReVOl1uK6aZF3lbXdOD8W9PXobR9KUUT2qBx5QC4ibfAqDKWymDA\nPG9ylzz64hsYBqJr7VNk9kTFEUsDmWzLabLoH42Elnp8mF/lTkWIcpVp0ly/etS08gttXo\nXenekJ1vRuxOYWDCEzGPU7kGc920TmM14k7IDdPoOh5+3sRUKedKeOUrVDH1f0n7QjHQsZ\ncbshp8tgqzf734zu8cTqNrr+6taptdEOOij1iUL/qYGfzny/hA48tO5+UFUih5W8ftp0+E\nNBIDkkGgk2MJ92I7QAXyMVsIABXco+mJT7pQi9tqlODGIQ3AOj0gcA3X/Ib8QX77Ih3TPi\nXEh77/P1XiYZOgpp2cRmNH8QbqaL9u898hDvJwIPJPuj2lIltTElH7hjBf5LQfCzrLV7BD\n10rM7sl4jr+A2q8jl1Ikp+25kainBBZSbrDummT9AAAFgDU/VLk1P1S5AAAAB3NzaC1yc2\nEAAAGBALTeOPQP52OTunEh//Q6GvGtyElNFmSWh8nU2gi7jvnZKrTaZ1eZqsxWyonVwCNX\nksFXLcPUXlTpdbiummRd5W13Tg/FvT16G0fSlFE9qgceUAuIm3wKgylspgwDxvcpc8+uIb\nGAaia+1TZPZExRFLA5lsy2my6B+NhJZ6fJhf5U5FiHKVadJcv3rUtPILbV6F3p3pCdb0bs\nTmFgwhMxj1O5BnPdtE5jNeJOyA3T6Doeft7EVCnnSnjlK1Qx9X9J+0Ix0LGXG7IafLYKs3\n+9+M7vHE6ja6/urWqbXRDjoo9YlC/6mBn858v4QOPLTuflBVIoeVvH7adPhDQSA5JBoJNj\nCfdiO0AF8jFbCAAV3KPpiU+6UIvbapTgxiENwDo9IHAN1/yG/EF++yId0z4lxIe+/z9V4m\nGToKadnEZjR/EG6mi/bvPfIQ7ycCDyT7o9pSJbUxJR+4YwX+S0Hws6y1ewQ9dKzO7JeI6/\ngNqvI5dSJKftuZGopwQWUm6w7ppk/QAAAAMBAAEAAAGAHKnC+Nq0XtGAkIFE4N18e6SAwy\n0WSWaZqmCzFQM0S2AhJnweOIG/0ZZHjsRzKKauOTmppQk40dgVsejpytIek9R+aH172gxJ\n2n4Cx0UwduRU5x8FFQlNc/kl722B0JWfJuB/snOZXv6LJ4o5aObIkozt2w9tVFeAqjYn2S\n1UsNOfRHBXGsTYwpRDwFWP56nKo2d2wBBTHDhCy6fb2dLW1fvSi/YspueOGIlHpvlYKi2/\nCWqvs9xVrwcScMtiDoQYq0khhO0efLCxvg/o+W9CLMVM2ms4G1zoSUQKN0oYWWQJyW4+VI\nYneWO8UpN0J3ElXKi7bhgAat7dBaM1g9IrAzk153DiEFZNsPxGOgL/+YdQN7zUBx/z7EkI\njyv80RV7fpUXvcq2p+qNl6UVig3VSzRrnsaJkUWu/A0u59ha7ocv6NxDIXjxpIDJme16GF\nquiGVBQNnYJymS/vFEbGf6bgf7iRmMCRUMG4nqLA6fPYP9uAtch+CmDfVLZC/fIdC5AAAA\nwQCDissV4zH6bfqgxJSuYNk8Vbb+19cF3b7gH1rVlB3zxpCAgcRgMHC+dP1z2NRx7UW9MR\nnye6kjpkzZZ0OigLqo7TtEq8uTglD9o6W7mRXqhy5A/ySOmqPL3ernHHQhGuoNODYAHkOU\nu2Rh8HXi+VLwKZcLInPOYJvcuLG4DxN8WfeVvlMHwhAOaTNNOtL4XZDHQeIPc4qHmJymmv\nsV7GuyQ6yW5C10uoGdxRPd90Bh4z4h2bKfZFjvEBbSBVkqrlAAAADBAN/zNtNayd/dX7Cr\nNb4sZuzCh+CW4BH8GOePZWNCATwBbNXBVb5cR+dmuTqYm+Ekz0VxVQRA1TvKncluJOQpoa\nXj8r0xdIgqkehnfDPMKtYVor06B9Fl1jrXtXU0Vrr6QcBWruSVyK1ZxqcmcNK/+KolVepe\nA6vcl/iKaG4U7su166nxLST06M2EgcSVsFJHpKn5+WAXC+X0Gx8kNjWIIb3GpiChdc0xZD\nmq02xZthVJrTCVw/e7gfDoB2QRsNV8HwAAAMEAzsCghZVp+0YsYg9oOrw4tEqcbEXEMhwY\n0jW8JNL8Spr1Ibp5Dw6bRSk5azARjmJtnMJhJ3oeHfF0eoISqcNuQXGndGQbVM9YzzAzc1\nNbbCNsVroqKlChT5wyPNGS+phi2bPARBno7WSDvshTZ7dAVEP2c9MJW0XwoSevwKlhgSdt\nRLFFQ/5nclJSdzPBOmQouC0OBcMFSrYtMeknJ4VvueVvve5HcHFaEsaMc7ABAGaLYaBQOm\niixITGvaNZh/tjAAAACW5pY29sYUBwMQE=\n-----END OPENSSH PRIVATE KEY-----`\n)\n\nvar (\n\tconfigDir         = filepath.Join(\".\", \"..\", \"..\")\n\tallPerms          = []string{dataprovider.PermAny}\n\thomeBasePath      string\n\tlogFilePath       string\n\tbackupsPath       string\n\ttestFileContent   = []byte(\"test data\")\n\tlastReceivedEmail receivedEmail\n)\n\nfunc TestMain(m *testing.M) {\n\thomeBasePath = os.TempDir()\n\tlogFilePath = filepath.Join(configDir, \"common_test.log\")\n\tbackupsPath = filepath.Join(os.TempDir(), \"backups\")\n\tlogger.InitLogger(logFilePath, 5, 1, 28, false, false, zerolog.DebugLevel)\n\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN\", \"1\")\n\tos.Setenv(\"SFTPGO_COMMON__ALLOW_SELF_CONNECTIONS\", \"1\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_USERNAME\", \"admin\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_PASSWORD\", \"password\")\n\terr := config.LoadConfig(configDir, \"\")\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error loading configuration: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\tlogger.InfoToConsole(\"Starting COMMON tests, provider: %v\", providerConf.Driver)\n\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing data provider: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\terr = common.Initialize(config.GetCommonConfig(), 0)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"error initializing common: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\thttpConfig := config.GetHTTPConfig()\n\thttpConfig.Timeout = 5\n\thttpConfig.RetryMax = 0\n\thttpConfig.Initialize(configDir) //nolint:errcheck\n\tkmsConfig := config.GetKMSConfig()\n\terr = kmsConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing kms: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tmfaConfig := config.GetMFAConfig()\n\terr = mfaConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing MFA: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\tsftpdConf := config.GetSFTPDConfig()\n\tsftpdConf.Bindings[0].Port = 4022\n\tsftpdConf.EnabledSSHCommands = []string{\"*\"}\n\tsftpdConf.Bindings = append(sftpdConf.Bindings, sftpd.Binding{\n\t\tPort: 4024,\n\t})\n\tsftpdConf.KeyboardInteractiveAuthentication = true\n\n\thttpdConf := config.GetHTTPDConfig()\n\thttpdConf.Bindings[0].Port = 4080\n\thttpdtest.SetBaseURL(\"http://127.0.0.1:4080\")\n\n\twebDavConf := config.GetWebDAVDConfig()\n\twebDavConf.Bindings = []webdavd.Binding{\n\t\t{\n\t\t\tPort: webDavServerPort,\n\t\t},\n\t}\n\n\tgo func() {\n\t\tif err := sftpdConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SFTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tif err := httpdConf.Initialize(configDir, 0); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tif err := webDavConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start WebDAV server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twaitTCPListening(sftpdConf.Bindings[0].GetAddress())\n\twaitTCPListening(httpdConf.Bindings[0].GetAddress())\n\twaitTCPListening(webDavConf.Bindings[0].GetAddress())\n\tstartHTTPFs()\n\n\tgo func() {\n\t\t// start a test HTTP server to receive action notifications\n\t\thttp.HandleFunc(\"/\", func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tfmt.Fprintf(w, \"OK\\n\")\n\t\t})\n\t\thttp.HandleFunc(\"/404\", func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tfmt.Fprintf(w, \"Not found\\n\")\n\t\t})\n\t\thttp.HandleFunc(\"/multipart\", func(w http.ResponseWriter, r *http.Request) {\n\t\t\terr := r.ParseMultipartForm(1048576)\n\t\t\tif err != nil {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\tfmt.Fprintf(w, \"KO\\n\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\t\t\tfmt.Fprintf(w, \"OK\\n\")\n\t\t})\n\t\tif err := http.ListenAndServe(httpAddr, nil); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTP notification server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tcommon.Config.ProxyProtocol = 2\n\t\tlistener, err := net.Listen(\"tcp\", httpProxyAddr)\n\t\tif err != nil {\n\t\t\tlogger.ErrorToConsole(\"error creating listener for proxy protocol server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tproxyListener, err := common.Config.GetProxyListener(listener)\n\t\tif err != nil {\n\t\t\tlogger.ErrorToConsole(\"error creating proxy protocol listener: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tcommon.Config.ProxyProtocol = 0\n\n\t\ts := &http.Server{}\n\t\tif err := s.Serve(proxyListener); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTP proxy protocol server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tif err := smtpd.ListenAndServe(smtpServerAddr, func(_ net.Addr, from string, to []string, data []byte) error {\n\t\t\tlastReceivedEmail.set(from, to, data)\n\t\t\treturn nil\n\t\t}, \"SFTPGo test\", \"localhost\"); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SMTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twaitTCPListening(httpAddr)\n\twaitTCPListening(httpProxyAddr)\n\twaitTCPListening(smtpServerAddr)\n\n\texitCode := m.Run()\n\tos.Remove(logFilePath)\n\tos.RemoveAll(backupsPath)\n\tos.Exit(exitCode)\n}\n\nfunc TestBaseConnection(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\t_, err = client.ReadDir(testDir)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\terr = client.RemoveDirectory(testDir)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.Error(t, err)\n\t\tinfo, err := client.Stat(testDir)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.IsDir())\n\t\t}\n\t\terr = client.Rename(testDir, testDir)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"the rename source and target cannot be the same\")\n\t\t}\n\t\terr = client.Rename(testDir, path.Join(testDir, \"sub\"))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.RemoveDirectory(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tlinkName := testFileName + \".link\" //nolint:goconst\n\t\terr = client.Rename(testFileName, testFileName)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"the rename source and target cannot be the same\")\n\t\t}\n\t\terr = client.Symlink(testFileName, linkName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(testFileName, testFileName)\n\t\tassert.Error(t, err)\n\t\tinfo, err = client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, int64(len(testFileContent)), info.Size())\n\t\t\tassert.False(t, info.IsDir())\n\t\t}\n\t\tinfo, err = client.Lstat(linkName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.NotEqual(t, int64(7), info.Size())\n\t\t\tassert.True(t, info.Mode()&os.ModeSymlink != 0)\n\t\t\tassert.False(t, info.IsDir())\n\t\t}\n\t\terr = client.RemoveDirectory(linkName)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_FAILURE\")\n\t\t}\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(linkName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, \"test\")\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\tf, err = client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\"1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName + \"1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(\"missing\")\n\t\tassert.Error(t, err)\n\t} else {\n\t\tprintLatestLogs(10)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRemoveAll(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebDavClient := getWebDavClient(user)\n\terr = webDavClient.RemoveAll(\"/\")\n\tif assert.Error(t, err) {\n\t\tassert.True(t, gowebdav.IsErrCode(err, http.StatusForbidden))\n\t}\n\n\ttestDir := \"baseDir\"\n\terr = webDavClient.RemoveAll(testDir)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, testFileName), 1234, client)\n\t\tassert.NoError(t, err)\n\n\t\terr = webDavClient.RemoveAll(path.Join(testDir, testFileName))\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(testDir, testFileName))\n\t\tassert.Error(t, err)\n\n\t\terr = writeSFTPFile(path.Join(testDir, testFileName), 1234, client)\n\t\tassert.NoError(t, err)\n\t\terr = webDavClient.RemoveAll(testDir)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(testDir)\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRelativeSymlinks(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlinkName := testFileName + \"_link\" //nolint:goconst\n\t\terr = client.Symlink(\"non-existent-file\", linkName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(linkName)\n\t\tassert.NoError(t, err)\n\t\ttestDir := \"sub\"\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Create(path.Join(testDir, testFileName))\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(testDir, testFileName), linkName)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(linkName)\n\t\tassert.NoError(t, err)\n\t\tp, err := client.ReadLink(linkName)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, path.Join(\"/\", testDir, testFileName), p)\n\t\terr = client.Remove(linkName)\n\t\tassert.NoError(t, err)\n\n\t\terr = client.Symlink(testFileName, path.Join(testDir, linkName))\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(testDir, linkName))\n\t\tassert.NoError(t, err)\n\t\tp, err = client.ReadLink(path.Join(testDir, linkName))\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, path.Join(\"/\", testDir, testFileName), p)\n\n\t\tf, err = client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\n\t\terr = client.Symlink(testFileName, linkName)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(linkName)\n\t\tassert.NoError(t, err)\n\t\tp, err = client.ReadLink(linkName)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, path.Join(\"/\", testFileName), p)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCheckFsAfterUpdate(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\t// remove the home dir, it will not be re-created\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.Error(t, err)\n\t} else {\n\t\tprintLatestLogs(10)\n\t}\n\t// update the user and login again, this time the home dir will be created\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginAccessTime(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.AccessTime = []sdk.TimePeriod{\n\t\t{\n\t\t\tDayOfWeek: int(time.Now().Add(-25 * time.Hour).UTC().Weekday()),\n\t\t\tFrom:      \"00:00\",\n\t\t\tTo:        \"23:59\",\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, _, err = getSftpClient(user)\n\tassert.Error(t, err)\n\n\tuser.Filters.AccessTime = []sdk.TimePeriod{\n\t\t{\n\t\t\tDayOfWeek: int(time.Now().UTC().Weekday()),\n\t\t\tFrom:      \"00:00\",\n\t\t\tTo:        \"23:59\",\n\t\t},\n\t}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr := checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSetStat(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tacmodTime := time.Now().Add(36 * time.Hour)\n\t\terr = client.Chtimes(testFileName, acmodTime, acmodTime)\n\t\tassert.NoError(t, err)\n\t\tnewFi, err := client.Lstat(testFileName)\n\t\tassert.NoError(t, err)\n\t\tdiff := math.Abs(newFi.ModTime().Sub(acmodTime).Seconds())\n\t\tassert.LessOrEqual(t, diff, float64(1))\n\t\tif runtime.GOOS != osWindows {\n\t\t\terr = client.Chown(testFileName, os.Getuid(), os.Getgid())\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tnewPerm := os.FileMode(0666)\n\t\terr = client.Chmod(testFileName, newPerm)\n\t\tassert.NoError(t, err)\n\t\tnewFi, err = client.Lstat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, newPerm, newFi.Mode().Perm())\n\t\t}\n\t\terr = client.Truncate(testFileName, 2)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, int64(2), info.Size())\n\t\t}\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\n\t\terr = client.Truncate(testFileName, 0)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\terr = client.Chtimes(testFileName, acmodTime, acmodTime)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\tif runtime.GOOS != osWindows {\n\t\t\terr = client.Chown(testFileName, os.Getuid(), os.Getgid())\n\t\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t}\n\t\terr = client.Chmod(testFileName, newPerm)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCryptFsUserUploadErrorOverwrite(t *testing.T) {\n\tu := getCryptFsUser()\n\tu.QuotaSize = 6000\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tvar buf []byte\n\tfor i := 0; i < 4000; i++ {\n\t\tbuf = append(buf, []byte(\"a\")...)\n\t}\n\tbufSize := int64(len(buf))\n\treader := bytes.NewReader(buf)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tf, err := client.Create(testFileName + \"_big\")\n\t\tassert.NoError(t, err)\n\t\tn, err := io.Copy(f, reader)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, bufSize, n)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tencryptedSize, err := getEncryptedFileSize(bufSize)\n\t\tassert.NoError(t, err)\n\t\texpectedSize := encryptedSize\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedSize, user.UsedQuotaSize)\n\t\t// now write a small file\n\t\tf, err = client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tencryptedSize, err = getEncryptedFileSize(int64(len(testFileContent)))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedSize+encryptedSize, user.UsedQuotaSize)\n\t\t// try to overwrite this file with a big one, this cause an overquota error\n\t\t// the partial file is deleted and the quota updated\n\t\t_, err = reader.Seek(0, io.SeekStart)\n\t\tassert.NoError(t, err)\n\t\tf, err = client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = io.Copy(f, reader)\n\t\tassert.Error(t, err)\n\t\terr = f.Close()\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedSize, user.UsedQuotaSize)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestChtimesOpenHandle(t *testing.T) {\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getCryptFsUser()\n\tcryptFsUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfor _, user := range []dataprovider.User{localUser, sftpUser, cryptFsUser} {\n\t\tconn, client, err := getSftpClient(user)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\n\t\t\tf, err := client.Create(testFileName)\n\t\t\tassert.NoError(t, err, \"user %v\", user.Username)\n\t\t\tf1, err := client.Create(testFileName + \"1\")\n\t\t\tassert.NoError(t, err, \"user %v\", user.Username)\n\t\t\tacmodTime := time.Now().Add(36 * time.Hour)\n\t\t\terr = client.Chtimes(testFileName, acmodTime, acmodTime)\n\t\t\tassert.NoError(t, err, \"user %v\", user.Username)\n\t\t\t_, err = f.Write(testFileContent)\n\t\t\tassert.NoError(t, err, \"user %v\", user.Username)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err, \"user %v\", user.Username)\n\t\t\terr = f1.Close()\n\t\t\tassert.NoError(t, err, \"user %v\", user.Username)\n\t\t\tinfo, err := client.Lstat(testFileName)\n\t\t\tassert.NoError(t, err, \"user %v\", user.Username)\n\t\t\tdiff := math.Abs(info.ModTime().Sub(acmodTime).Seconds())\n\t\t\tassert.LessOrEqual(t, diff, float64(1), \"user %v\", user.Username)\n\t\t\tinfo1, err := client.Lstat(testFileName + \"1\")\n\t\t\tassert.NoError(t, err, \"user %v\", user.Username)\n\t\t\tdiff = math.Abs(info1.ModTime().Sub(acmodTime).Seconds())\n\t\t\tassert.Greater(t, diff, float64(86400), \"user %v\", user.Username)\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(cryptFsUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(cryptFsUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWaitForConnections(t *testing.T) {\n\tu := getTestUser()\n\tu.UploadBandwidth = 128\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFileSize := int64(524288)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = common.CheckClosing()\n\t\tassert.NoError(t, err)\n\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tcommon.WaitForTransfers(10)\n\t\t\tcommon.WaitForTransfers(0)\n\t\t\tcommon.WaitForTransfers(10)\n\t\t}()\n\n\t\terr = writeSFTPFileNoCheck(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\twg.Wait()\n\n\t\terr = common.CheckClosing()\n\t\tassert.EqualError(t, err, common.ErrShuttingDown.Error())\n\n\t\t_, err = client.Stat(testFileName)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), common.ErrShuttingDown.Error())\n\t\t}\n\t}\n\n\t_, _, err = getSftpClient(user)\n\tassert.Error(t, err)\n\n\terr = common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tinfo, err := client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, info.Size())\n\t\t}\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tcommon.WaitForTransfers(1)\n\t\t}()\n\n\t\terr = writeSFTPFileNoCheck(testFileName, testFileSize, client)\n\t\t// we don't have an error here because the service won't really stop\n\t\tassert.NoError(t, err)\n\t\twg.Wait()\n\t}\n\n\terr = common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n\n\tcommon.WaitForTransfers(1)\n\n\terr = common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCheckParentDirs(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestDir := \"/path/to/sub/dir\"\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\t_, err = client.Stat(testDir)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\tc := common.NewBaseConnection(xid.New().String(), common.ProtocolSFTP, \"\", \"\", user)\n\t\terr = c.CheckParentDirs(testDir)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = c.CheckParentDirs(testDir)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.Permissions[\"/\"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermDownload}\n\tuser, _, err = httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tc := common.NewBaseConnection(xid.New().String(), common.ProtocolSFTP, \"\", \"\", user)\n\t\terr = c.CheckParentDirs(testDir)\n\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxPermissionDenied)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermissionErrors(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestSFTPUser()\n\tsubDir := \"/sub\"\n\tu.Permissions[subDir] = nil\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.MkdirAll(path.Join(subDir, subDir))\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Create(path.Join(subDir, subDir, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\t_, err = f.Write(testFileContent)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\tconn, client, err = getSftpClient(sftpUser)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\t_, err = client.ReadDir(subDir)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Mkdir(path.Join(subDir, subDir))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.RemoveDirectory(path.Join(subDir, subDir))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Symlink(\"test\", path.Join(subDir, subDir))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Chmod(path.Join(subDir, subDir), os.ModePerm)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Chown(path.Join(subDir, subDir), os.Getuid(), os.Getgid())\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Chtimes(path.Join(subDir, subDir), time.Now(), time.Now())\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Truncate(path.Join(subDir, subDir), 0)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Remove(path.Join(subDir, subDir, testFileName))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHiddenPatternFilter(t *testing.T) {\n\tdeniedDir := \"/denied_hidden\"\n\tu := getTestUser()\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           deniedDir,\n\t\t\tDeniedPatterns: []string{\"*.txt\", \"beta*\"},\n\t\t\tDenyPolicy:     sdk.DenyPolicyHide,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tdirName := \"beta\"\n\tsubDirName := \"testDir\"\n\ttestFile := filepath.Join(u.GetHomeDir(), deniedDir, \"file.txt\")\n\ttestFile1 := filepath.Join(u.GetHomeDir(), deniedDir, \"beta.txt\")\n\ttestHiddenFile := filepath.Join(u.GetHomeDir(), deniedDir, dirName, subDirName, \"hidden.jpg\")\n\terr = os.MkdirAll(filepath.Join(u.GetHomeDir(), deniedDir), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(testFile, testFileContent, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(testFile1, testFileContent, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(filepath.Join(u.GetHomeDir(), deniedDir, dirName, subDirName), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(testHiddenFile, testFileContent, os.ModePerm)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tfiles, err := client.ReadDir(deniedDir)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, files, 0)\n\t\terr = client.Remove(path.Join(deniedDir, filepath.Base(testFile)))\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\terr = client.Chtimes(path.Join(deniedDir, filepath.Base(testFile)), time.Now(), time.Now())\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t_, err = client.Stat(path.Join(deniedDir, filepath.Base(testFile1)))\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\terr = client.RemoveDirectory(path.Join(deniedDir, dirName))\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\terr = client.Rename(path.Join(deniedDir, dirName), path.Join(deniedDir, \"newname\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Mkdir(path.Join(deniedDir, \"beta1\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(path.Join(deniedDir, \"afile.txt\"), 1024, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(path.Join(deniedDir, dirName, subDirName, \"afile.jpg\"), 1024, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t_, err = client.Open(path.Join(deniedDir, dirName, subDirName, filepath.Base(testHiddenFile)))\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\terr = client.Symlink(path.Join(deniedDir, dirName), dirName)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\terr = writeSFTPFile(path.Join(deniedDir, testFileName), 1024, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(deniedDir, testFileName), path.Join(deniedDir, \"symlink.txt\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\tfiles, err = client.ReadDir(deniedDir)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, files, 1)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           deniedDir,\n\t\t\tDeniedPatterns: []string{\"*.txt\", \"beta*\"},\n\t\t\tDenyPolicy:     sdk.DenyPolicyDefault,\n\t\t},\n\t}\n\tuser, _, err = httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tfiles, err := client.ReadDir(deniedDir)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, files, 4)\n\t\t_, err = client.Stat(path.Join(deniedDir, filepath.Base(testFile)))\n\t\tassert.NoError(t, err)\n\t\terr = client.Chtimes(path.Join(deniedDir, filepath.Base(testFile)), time.Now(), time.Now())\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Mkdir(path.Join(deniedDir, \"beta2\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(path.Join(deniedDir, \"afile2.txt\"), 1024, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Symlink(path.Join(deniedDir, testFileName), path.Join(deniedDir, \"link.txt\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(path.Join(deniedDir, dirName, subDirName, \"afile.jpg\"), 1024, client)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Open(path.Join(deniedDir, dirName, subDirName, filepath.Base(testHiddenFile)))\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHiddenRoot(t *testing.T) {\n\t// only the \"/ftp\" directory is allowed and visibile in the \"/\" path\n\t// within /ftp any file/directory is allowed and visibile\n\tu := getTestUser()\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/\",\n\t\t\tAllowedPatterns: []string{\"ftp\"},\n\t\t\tDenyPolicy:      sdk.DenyPolicyHide,\n\t\t},\n\t\t{\n\t\t\tPath:            \"/ftp\",\n\t\t\tAllowedPatterns: []string{\"*\"},\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfor i := 0; i < 10; i++ {\n\t\terr = os.MkdirAll(filepath.Join(user.HomeDir, fmt.Sprintf(\"ftp%d\", i)), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\terr = os.WriteFile(filepath.Join(user.HomeDir, testFileName), []byte(\"\"), 0666)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user.HomeDir, \"ftp.txt\"), []byte(\"\"), 0666)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.Mkdir(\"ftp\")\n\t\tassert.NoError(t, err)\n\t\tentries, err := client.ReadDir(\"/\")\n\t\tassert.NoError(t, err)\n\t\tif assert.Len(t, entries, 1) {\n\t\t\tassert.Equal(t, \"ftp\", entries[0].Name())\n\t\t}\n\t\t_, err = client.Stat(\".\")\n\t\tassert.NoError(t, err)\n\t\tfor _, name := range []string{testFileName, \"ftp.txt\"} {\n\t\t\t_, err = client.Stat(name)\n\t\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t}\n\t\tfor i := 0; i < 10; i++ {\n\t\t\t_, err = client.Stat(fmt.Sprintf(\"ftp%d\", i))\n\t\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t}\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(\"ftp123\", 4096, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(testFileName, testFileName+\"_rename\") //nolint:goconst\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(path.Join(\"/ftp\", testFileName), 4096, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(\"/ftp/dir\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(\"/ftp\", testFileName), path.Join(\"/ftp/dir\", testFileName))\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestFileNotAllowedErrors(t *testing.T) {\n\tdeniedDir := \"/denied\"\n\tu := getTestUser()\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           deniedDir,\n\t\t\tDeniedPatterns: []string{\"*.txt\"},\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFile := filepath.Join(u.GetHomeDir(), deniedDir, \"file.txt\")\n\t\terr = os.MkdirAll(filepath.Join(u.GetHomeDir(), deniedDir), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(testFile, testFileContent, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(path.Join(deniedDir, \"file.txt\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(deniedDir, \"file.txt\"), path.Join(deniedDir, \"file1.txt\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRootDirVirtualFolder(t *testing.T) {\n\tmappedPath1 := filepath.Join(os.TempDir(), \"mapped1\")\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       filepath.Base(mappedPath1),\n\t\tMappedPath: mappedPath1,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(\"cryptsecret\"),\n\t\t\t},\n\t\t},\n\t}\n\tmappedPath2 := filepath.Join(os.TempDir(), \"mapped2\")\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       filepath.Base(mappedPath2),\n\t\tMappedPath: mappedPath2,\n\t}\n\tfolder1, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfolder2, _, err := httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tu.UploadDataTransfer = 1000\n\tu.DownloadDataTransfer = 5000\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folder1.Name,\n\t\t},\n\t\tVirtualPath: \"/\",\n\t\tQuotaFiles:  1000,\n\t})\n\tvdirPath2 := \"/vmapped\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folder2.Name,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf, err := user.GetVirtualFolderForPath(\"/\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/\", f.VirtualPath)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Create(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\t_, err = f.Write(testFileContent)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tassert.NoFileExists(t, filepath.Join(user.HomeDir, testFileName))\n\t\tassert.FileExists(t, filepath.Join(mappedPath1, testFileName))\n\t\tentries, err := client.ReadDir(\".\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Len(t, entries, 2)\n\t\t}\n\n\t\tuser, _, err := httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\t\tfolder, _, err := httpdtest.GetFolderByName(folder1.Name, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, folder.UsedQuotaFiles)\n\n\t\tf, err = client.Create(path.Join(vdirPath2, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\t_, err = f.Write(testFileContent)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tfolder, _, err = httpdtest.GetFolderByName(folder1.Name, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, folder.UsedQuotaFiles)\n\n\t\terr = client.Rename(testFileName, path.Join(vdirPath2, testFileName+\"_rename\"))\n\t\tassert.Error(t, err)\n\t\terr = client.Rename(path.Join(vdirPath2, testFileName), testFileName+\"_rename\")\n\t\tassert.Error(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\"_rename\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirPath2, testFileName), path.Join(vdirPath2, testFileName+\"_rename\"))\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folder1.Name}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folder2.Name}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestTruncateQuotaLimits(t *testing.T) {\n\tmappedPath1 := filepath.Join(os.TempDir(), \"mapped1\")\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       filepath.Base(mappedPath1),\n\t\tMappedPath: mappedPath1,\n\t}\n\tfolder1, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tmappedPath2 := filepath.Join(os.TempDir(), \"mapped2\")\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       filepath.Base(mappedPath2),\n\t\tMappedPath: mappedPath2,\n\t}\n\tfolder2, _, err := httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.QuotaSize = 20\n\tu.UploadDataTransfer = 1000\n\tu.DownloadDataTransfer = 5000\n\tvdirPath1 := \"/vmapped1\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folder1.Name,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  10,\n\t})\n\tvdirPath2 := \"/vmapped2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folder2.Name,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaSize = 20\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\tf, err := client.OpenFile(testFileName, os.O_WRONLY|os.O_CREATE)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tn, err := f.Write(testFileContent)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testFileContent), n)\n\t\t\t\terr = f.Truncate(2)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\texpectedQuotaFiles := 0\n\t\t\t\texpectedQuotaSize := int64(2)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(expectedQuotaSize, io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tn, err = f.Write(testFileContent)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testFileContent), n)\n\t\t\t\terr = f.Truncate(5)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\texpectedQuotaSize = int64(5)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(expectedQuotaSize, io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tn, err = f.Write(testFileContent)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testFileContent), n)\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\texpectedQuotaFiles = 1\n\t\t\t\texpectedQuotaSize = int64(5) + int64(len(testFileContent))\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t}\n\t\t\t// now truncate by path\n\t\t\terr = client.Truncate(testFileName, 5)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, int64(5), user.UsedQuotaSize)\n\t\t\t// now open an existing file without truncate it, quota should not change\n\t\t\tf, err = client.OpenFile(testFileName, os.O_WRONLY)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(5), user.UsedQuotaSize)\n\t\t\t}\n\t\t\t// open the file truncating it\n\t\t\tf, err = client.OpenFile(testFileName, os.O_WRONLY|os.O_TRUNC)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t\t\t}\n\t\t\t// now test max write size\n\t\t\tf, err = client.OpenFile(testFileName, os.O_WRONLY)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tn, err := f.Write(testFileContent)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testFileContent), n)\n\t\t\t\terr = f.Truncate(11)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(11), user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(int64(11), io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tn, err = f.Write(testFileContent)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testFileContent), n)\n\t\t\t\terr = f.Truncate(5)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(5), user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(int64(5), io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tn, err = f.Write(testFileContent)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testFileContent), n)\n\t\t\t\terr = f.Truncate(12)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(12), user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(int64(12), io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = f.Write(testFileContent)\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t\t\t}\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.Error(t, err)\n\t\t\t\t// the file is deleted\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t\t\t}\n\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\t// basic test inside a virtual folder\n\t\t\t\tvfileName1 := path.Join(vdirPath1, testFileName)\n\t\t\t\tf, err = client.OpenFile(vfileName1, os.O_WRONLY|os.O_CREATE)\n\t\t\t\tif assert.NoError(t, err) {\n\t\t\t\t\tn, err := f.Write(testFileContent)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, len(testFileContent), n)\n\t\t\t\t\terr = f.Truncate(2)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\texpectedQuotaFiles := 0\n\t\t\t\t\texpectedQuotaSize := int64(2)\n\t\t\t\t\tfold, _, err := httpdtest.GetFolderByName(folder1.Name, http.StatusOK)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)\n\t\t\t\t\tassert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)\n\t\t\t\t\terr = f.Close()\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\texpectedQuotaFiles = 1\n\t\t\t\t\tfold, _, err = httpdtest.GetFolderByName(folder1.Name, http.StatusOK)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)\n\t\t\t\t\tassert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)\n\t\t\t\t}\n\t\t\t\terr = client.Truncate(vfileName1, 1)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tfold, _, err := httpdtest.GetFolderByName(folder1.Name, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, int64(1), fold.UsedQuotaSize)\n\t\t\t\tassert.Equal(t, 1, fold.UsedQuotaFiles)\n\t\t\t\t// now test on vdirPath2, the folder quota is included in the user's quota\n\t\t\t\tvfileName2 := path.Join(vdirPath2, testFileName)\n\t\t\t\tf, err = client.OpenFile(vfileName2, os.O_WRONLY|os.O_CREATE)\n\t\t\t\tif assert.NoError(t, err) {\n\t\t\t\t\tn, err := f.Write(testFileContent)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, len(testFileContent), n)\n\t\t\t\t\terr = f.Truncate(3)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\texpectedQuotaFiles := 0\n\t\t\t\t\texpectedQuotaSize := int64(3)\n\t\t\t\t\tfold, _, err := httpdtest.GetFolderByName(folder2.Name, http.StatusOK)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, int64(0), fold.UsedQuotaSize)\n\t\t\t\t\tassert.Equal(t, 0, fold.UsedQuotaFiles)\n\t\t\t\t\terr = f.Close()\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\texpectedQuotaFiles = 1\n\t\t\t\t\tfold, _, err = httpdtest.GetFolderByName(folder2.Name, http.StatusOK)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, int64(0), fold.UsedQuotaSize)\n\t\t\t\t\tassert.Equal(t, 0, fold.UsedQuotaFiles)\n\t\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t\t}\n\n\t\t\t\t// cleanup\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tif user.Username == defaultUsername {\n\t\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tuser.Password = defaultPassword\n\t\t\t\t\tuser.QuotaSize = 0\n\t\t\t\t\tuser.ID = 0\n\t\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder2, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {\n\ttestFileSize := int64(131072)\n\ttestFileSize1 := int64(65537)\n\ttestFileName1 := \"test_file1.dat\" //nolint:goconst\n\tu := getTestUser()\n\tu.QuotaFiles = 0\n\tu.QuotaSize = 0\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\" //nolint:goconst\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\" //nolint:goconst\n\tmappedPath3 := filepath.Join(os.TempDir(), \"vdir3\")\n\tfolderName3 := filepath.Base(mappedPath3)\n\tvdirPath3 := \"/vdir3\"\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folderName3,\n\t\tMappedPath: mappedPath3,\n\t}\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  2,\n\t\tQuotaSize:   0,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  0,\n\t\tQuotaSize:   testFileSize + testFileSize1 + 1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName3,\n\t\t},\n\t\tVirtualPath: vdirPath3,\n\t\tQuotaFiles:  2,\n\t\tQuotaSize:   testFileSize * 2,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = writeSFTPFile(path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Open(path.Join(vdirPath1, testFileName))\n\t\tassert.NoError(t, err)\n\t\tcontents, err := io.ReadAll(f)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, contents, int(testFileSize))\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath3, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath3, testFileName+\"1\"), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, path.Join(vdirPath1, testFileName+\".rename\")) //nolint:goconst\n\t\tassert.Error(t, err)\n\t\t// we overwrite an existing file and we have unlimited size\n\t\terr = client.Rename(testFileName, path.Join(vdirPath1, testFileName))\n\t\tassert.NoError(t, err)\n\t\t// we have no space and we try to overwrite a bigger file with a smaller one, this should succeed\n\t\terr = client.Rename(testFileName1, path.Join(vdirPath2, testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// we have no space and we try to overwrite a smaller file with a bigger one, this should fail\n\t\terr = client.Rename(testFileName, path.Join(vdirPath2, testFileName1))\n\t\tassert.Error(t, err)\n\t\tfi, err := client.Stat(path.Join(vdirPath1, testFileName1))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize1, fi.Size())\n\t\t}\n\t\t// we are overquota inside vdir3 size 2/2 and size 262144/262144\n\t\terr = client.Rename(path.Join(vdirPath1, testFileName1), path.Join(vdirPath3, testFileName1+\".rename\"))\n\t\tassert.Error(t, err)\n\t\t// we overwrite an existing file and we have enough size\n\t\terr = client.Rename(path.Join(vdirPath1, testFileName1), path.Join(vdirPath3, testFileName))\n\t\tassert.NoError(t, err)\n\t\ttestFileName2 := \"test_file2.dat\"\n\t\terr = writeSFTPFile(testFileName2, testFileSize+testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// we overwrite an existing file and we haven't enough size\n\t\terr = client.Rename(testFileName2, path.Join(vdirPath3, testFileName))\n\t\tassert.Error(t, err)\n\t\t// now remove a file from vdir3, create a dir with 2 files and try to rename it in vdir3\n\t\t// this will fail since the rename will result in 3 files inside vdir3 and quota limits only\n\t\t// allow 2 total files there\n\t\terr = client.Remove(path.Join(vdirPath3, testFileName+\"1\"))\n\t\tassert.NoError(t, err)\n\t\taDir := \"a dir\"\n\t\terr = client.Mkdir(aDir)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(aDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(aDir, testFileName1+\"1\"), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(aDir, path.Join(vdirPath3, aDir))\n\t\tassert.Error(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName3}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath3)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameOverwrite(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 100\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65537)\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Open(testFileName)\n\t\tassert.NoError(t, err)\n\t\tcontents := make([]byte, testFileSize)\n\t\tn, err := io.ReadFull(f, contents)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int(testFileSize), n)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), user.UsedDownloadDataTransfer)\n\t\tassert.Equal(t, int64(0), user.UsedUploadDataTransfer)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\terr = client.Rename(testFileName, testFileName1)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), user.UsedDownloadDataTransfer)\n\t\tassert.Equal(t, int64(0), user.UsedUploadDataTransfer)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\terr = client.Remove(testFileName1)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName1, testFileName)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFoldersQuotaValues(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tvdirPath1 := \"/vdir1\"\n\tfolderName1 := filepath.Base(mappedPath1)\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tvdirPath2 := \"/vdir2\"\n\tfolderName2 := filepath.Base(mappedPath2)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(131072)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// we copy the same file two times to test quota update on file overwrite\n\t\terr = writeSFTPFile(path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaFiles := 2\n\t\texpectedQuotaSize := testFileSize * 2\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\n\t\terr = client.Remove(path.Join(vdirPath1, testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(path.Join(vdirPath2, testFileName))\n\t\tassert.NoError(t, err)\n\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tvdirPath1 := \"/vdir1\"\n\tfolderName1 := filepath.Base(mappedPath1)\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tvdirPath2 := \"/vdir2\"\n\tfolderName2 := filepath.Base(mappedPath2)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65535)\n\t\tdir1 := \"dir1\" //nolint:goconst\n\t\tdir2 := \"dir2\" //nolint:goconst\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\t// initial files:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\t//\n\t\t// rename a file inside vdir1 it is included inside user quota, so we have:\n\t\t// - vdir1/dir1/testFileName.rename\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(vdirPath1, dir1, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// rename a file inside vdir2, it isn't included inside user quota, so we have:\n\t\t// - vdir1/dir1/testFileName.rename\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName.rename\n\t\t// - vdir2/dir2/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(vdirPath2, dir1, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// rename a file inside vdir2 overwriting an existing, we now have:\n\t\t// - vdir1/dir1/testFileName.rename\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName.rename (initial testFileName1)\n\t\terr = client.Rename(path.Join(vdirPath2, dir2, testFileName1), path.Join(vdirPath2, dir1, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// rename a file inside vdir1 overwriting an existing, we now have:\n\t\t// - vdir1/dir1/testFileName.rename (initial testFileName1)\n\t\t// - vdir2/dir1/testFileName.rename (initial testFileName1)\n\t\terr = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(vdirPath1, dir1, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// rename a directory inside the same virtual folder, quota should not change\n\t\terr = client.RemoveDirectory(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirPath1, dir1), path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirPath2, dir1), path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameBetweenVirtualFolder(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65535)\n\t\tdir1 := \"dir1\"\n\t\tdir2 := \"dir2\"\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// initial files:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\t//\n\t\t// rename a file from vdir1 to vdir2, vdir1 is included inside user quota, so we have:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName1.rename\n\t\terr = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(vdirPath2, dir1, testFileName1+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 3, f.UsedQuotaFiles)\n\t\t// rename a file from vdir2 to vdir1, vdir2 is not included inside user quota, so we have:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir2/testFileName.rename\n\t\t// - vdir2/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName1.rename\n\t\terr = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(vdirPath1, dir2, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*2, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1*2, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\t// rename a file from vdir1 to vdir2 overwriting an existing file, vdir1 is included inside user quota, so we have:\n\t\t// - vdir1/dir2/testFileName.rename\n\t\t// - vdir2/dir2/testFileName1 (is the initial testFileName)\n\t\t// - vdir2/dir1/testFileName1.rename\n\t\terr = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(vdirPath2, dir2, testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1+testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\t// rename a file from vdir2 to vdir1 overwriting an existing file, vdir2 is not included inside user quota, so we have:\n\t\t// - vdir1/dir2/testFileName.rename (is the initial testFileName1)\n\t\t// - vdir2/dir2/testFileName1 (is the initial testFileName)\n\t\terr = client.Rename(path.Join(vdirPath2, dir1, testFileName1+\".rename\"), path.Join(vdirPath1, dir2, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir2, testFileName), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir2, testFileName+\"1.dupl\"), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\t// - vdir1/dir2/testFileName.rename (initial testFileName1)\n\t\t// - vdir1/dir2/testFileName\n\t\t// - vdir2/dir2/testFileName1 (initial testFileName)\n\t\t// - vdir2/dir2/testFileName (initial testFileName1)\n\t\t// - vdir2/dir2/testFileName1.dupl\n\t\t// rename directories between the two virtual folders\n\t\terr = client.Rename(path.Join(vdirPath2, dir2), path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 5, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1*3+testFileSize*2, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// now move on vpath2\n\t\terr = client.Rename(path.Join(vdirPath1, dir2), path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1*2+testFileSize, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameFromVirtualFolder(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65535)\n\t\tdir1 := \"dir1\"\n\t\tdir2 := \"dir2\"\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// initial files:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\t//\n\t\t// rename a file from vdir1 to the user home dir, vdir1 is included in user quota so we have:\n\t\t// - testFileName\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(testFileName))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\t// rename a file from vdir2 to the user home dir, vdir2 is not included in user quota so we have:\n\t\t// - testFileName\n\t\t// - testFileName1\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\terr = client.Rename(path.Join(vdirPath2, dir2, testFileName1), path.Join(testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// rename a file from vdir1 to the user home dir overwriting an existing file, vdir1 is included in user quota so we have:\n\t\t// - testFileName (initial testFileName1)\n\t\t// - testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\terr = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(testFileName))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// rename a file from vdir2 to the user home dir overwriting an existing file, vdir2 is not included in user quota so we have:\n\t\t// - testFileName (initial testFileName1)\n\t\t// - testFileName1 (initial testFileName)\n\t\terr = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// dir rename\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, dir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, dir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// - testFileName (initial testFileName1)\n\t\t// - testFileName1 (initial testFileName)\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir1/testFileName1\n\t\t// - dir1/testFileName\n\t\t// - dir1/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath2, dir1), dir1)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 6, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*3+testFileSize1*3, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// - testFileName (initial testFileName1)\n\t\t// - testFileName1 (initial testFileName)\n\t\t// - dir2/testFileName\n\t\t// - dir2/testFileName1\n\t\t// - dir1/testFileName\n\t\t// - dir1/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath1, dir1), dir2)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 6, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*3+testFileSize1*3, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameToVirtualFolder(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\tu.Permissions[vdirPath1] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload,\n\t\tdataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermCreateSymlinks, dataprovider.PermCreateDirs,\n\t\tdataprovider.PermRename}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65535)\n\t\tdir1 := \"dir1\"\n\t\tdir2 := \"dir2\"\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// initial files:\n\t\t// - testFileName\n\t\t// - testFileName1\n\t\t//\n\t\t// rename a file from user home dir to vdir1, vdir1 is included in user quota so we have:\n\t\t// - testFileName\n\t\t// - /vdir1/dir1/testFileName1\n\t\terr = client.Rename(testFileName1, path.Join(vdirPath1, dir1, testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// rename a file from user home dir to vdir2, vdir2 is not included in user quota so we have:\n\t\t// - /vdir2/dir1/testFileName\n\t\t// - /vdir1/dir1/testFileName1\n\t\terr = client.Rename(testFileName, path.Join(vdirPath2, dir1, testFileName))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// upload two new files to the user home dir so we have:\n\t\t// - testFileName\n\t\t// - testFileName1\n\t\t// - /vdir1/dir1/testFileName1\n\t\t// - /vdir2/dir1/testFileName\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1+testFileSize1, user.UsedQuotaSize)\n\t\t// rename a file from user home dir to vdir1 overwriting an existing file, vdir1 is included in user quota so we have:\n\t\t// - testFileName1\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName\n\t\terr = client.Rename(testFileName, path.Join(vdirPath1, dir1, testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// rename a file from user home dir to vdir2 overwriting an existing file, vdir2 is not included in user quota so we have:\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName (initial testFileName1)\n\t\terr = client.Rename(testFileName1, path.Join(vdirPath2, dir1, testFileName))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\n\t\terr = client.Mkdir(dir1)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(dir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// - /dir1/testFileName\n\t\t// - /dir1/testFileName1\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName (initial testFileName1)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// - /vdir1/adir/testFileName\n\t\t// - /vdir1/adir/testFileName1\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName (initial testFileName1)\n\t\terr = client.Rename(dir1, path.Join(vdirPath1, \"adir\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\terr = client.Mkdir(dir1)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(dir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// - /vdir1/adir/testFileName\n\t\t// - /vdir1/adir/testFileName1\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName (initial testFileName1)\n\t\t// - /vdir2/adir/testFileName\n\t\t// - /vdir2/adir/testFileName1\n\t\terr = client.Rename(dir1, path.Join(vdirPath2, \"adir\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1*2+testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 3, f.UsedQuotaFiles)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestTransferQuotaLimits(t *testing.T) {\n\tu := getTestUser()\n\tu.TotalDataTransfer = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestFileSize := int64(524288)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Open(testFileName)\n\t\tassert.NoError(t, err)\n\t\tcontents := make([]byte, testFileSize)\n\t\tn, err := io.ReadFull(f, contents)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int(testFileSize), n)\n\t\tassert.Len(t, contents, int(testFileSize))\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Open(testFileName)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_FAILURE\")\n\t\t\tassert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error())\n\t\t}\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_FAILURE\")\n\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t}\n\t}\n\t// test the limit while uploading/downloading\n\tuser.TotalDataTransfer = 0\n\tuser.UploadDataTransfer = 1\n\tuser.DownloadDataTransfer = 1\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestFileSize := int64(450000)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Open(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\t_, err = io.Copy(io.Discard, f)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tf, err = client.Open(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\t_, err = io.Copy(io.Discard, f)\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_FAILURE\")\n\t\t\t\tassert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error())\n\t\t\t}\n\t\t\terr = f.Close()\n\t\t\tassert.Error(t, err)\n\t\t}\n\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_FAILURE\")\n\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFoldersLink(t *testing.T) {\n\tu := getTestUser()\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(131072)\n\t\ttestDir := \"adir\"\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath1, testFileName), path.Join(vdirPath1, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath1, testFileName), path.Join(vdirPath1, testDir, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath2, testFileName), path.Join(vdirPath2, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath2, testFileName), path.Join(vdirPath2, testDir, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(vdirPath1, testFileName+\".link1\")) //nolint:goconst\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(vdirPath1, testDir, testFileName+\".link1\"))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(vdirPath2, testFileName+\".link1\"))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(vdirPath2, testDir, testFileName+\".link1\"))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(path.Join(vdirPath1, testFileName), testFileName+\".link1\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(path.Join(vdirPath2, testFileName), testFileName+\".link1\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(path.Join(vdirPath1, testFileName), path.Join(vdirPath2, testDir, testFileName+\".link1\"))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(path.Join(vdirPath2, testFileName), path.Join(vdirPath1, testFileName+\".link1\"))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(\"/\", \"/roolink\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Symlink(testFileName, \"/\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Symlink(testFileName, vdirPath1)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Symlink(vdirPath1, testFileName+\".link2\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestCrossFolderRename(t *testing.T) {\n\tfolder1 := \"folder1\"\n\tfolder2 := \"folder2\"\n\tfolder3 := \"folder3\"\n\tfolder4 := \"folder4\"\n\tfolder5 := \"folder5\"\n\tfolder6 := \"folder6\"\n\tfolder7 := \"folder7\"\n\n\tbaseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folder1,\n\t\tMappedPath: filepath.Join(os.TempDir(), folder1),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folder2,\n\t\tMappedPath: filepath.Join(os.TempDir(), folder2),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folder3,\n\t\tMappedPath: filepath.Join(os.TempDir(), folder3),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword + \"mod\"),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf4 := vfs.BaseVirtualFolder{\n\t\tName:       folder4,\n\t\tMappedPath: filepath.Join(os.TempDir(), folder4),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\tUsername: baseUser.Username,\n\t\t\t\t\tPrefix:   path.Join(\"/\", folder4),\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f4, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf5 := vfs.BaseVirtualFolder{\n\t\tName:       folder5,\n\t\tMappedPath: filepath.Join(os.TempDir(), folder5),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\tUsername: baseUser.Username,\n\t\t\t\t\tPrefix:   path.Join(\"/\", folder5),\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f5, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf6 := vfs.BaseVirtualFolder{\n\t\tName:       folder6,\n\t\tMappedPath: filepath.Join(os.TempDir(), folder6),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: \"127.0.0.1:4024\",\n\t\t\t\t\tUsername: baseUser.Username,\n\t\t\t\t\tPrefix:   path.Join(\"/\", folder6),\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f6, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf7 := vfs.BaseVirtualFolder{\n\t\tName:       folder7,\n\t\tMappedPath: filepath.Join(os.TempDir(), folder7),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\tUsername: baseUser.Username,\n\t\t\t\t\tPrefix:   path.Join(\"/\", folder4),\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f7, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getCryptFsUser()\n\tu.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: folder1,\n\t\t\t},\n\t\t\tVirtualPath: path.Join(\"/\", folder1),\n\t\t\tQuotaSize:   -1,\n\t\t\tQuotaFiles:  -1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: folder2,\n\t\t\t},\n\t\t\tVirtualPath: path.Join(\"/\", folder2),\n\t\t\tQuotaSize:   -1,\n\t\t\tQuotaFiles:  -1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: folder3,\n\t\t\t},\n\t\t\tVirtualPath: path.Join(\"/\", folder3),\n\t\t\tQuotaSize:   -1,\n\t\t\tQuotaFiles:  -1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: folder4,\n\t\t\t},\n\t\t\tVirtualPath: path.Join(\"/\", folder4),\n\t\t\tQuotaSize:   -1,\n\t\t\tQuotaFiles:  -1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: folder5,\n\t\t\t},\n\t\t\tVirtualPath: path.Join(\"/\", folder5),\n\t\t\tQuotaSize:   -1,\n\t\t\tQuotaFiles:  -1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: folder6,\n\t\t\t},\n\t\t\tVirtualPath: path.Join(\"/\", folder6),\n\t\t\tQuotaSize:   -1,\n\t\t\tQuotaFiles:  -1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: folder7,\n\t\t\t},\n\t\t\tVirtualPath: path.Join(\"/\", folder7),\n\t\t\tQuotaSize:   -1,\n\t\t\tQuotaFiles:  -1,\n\t\t},\n\t}\n\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tsubDir := \"testSubDir\"\n\t\terr = client.Mkdir(subDir)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(subDir, \"afile.bin\"), 64, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(subDir, path.Join(\"/\", folder1, subDir))\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(\"/\", folder1, subDir))\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(\"/\", folder1, subDir, \"afile.bin\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(\"/\", folder1, subDir), path.Join(\"/\", folder2, subDir))\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(\"/\", folder2, subDir))\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(\"/\", folder2, subDir, \"afile.bin\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(\"/\", folder2, subDir), path.Join(\"/\", folder3, subDir))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(path.Join(\"/\", folder3, \"file.bin\"), 64, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(\"/\", folder3, \"file.bin\"), \"/renamed.bin\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(\"/\", folder3, \"file.bin\"), path.Join(\"/\", folder2, \"/renamed.bin\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(\"/\", folder3, \"file.bin\"), path.Join(\"/\", folder3, \"/renamed.bin\"))\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(\"/afile.bin\", 64, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"afile.bin\", path.Join(\"/\", folder4, \"afile_renamed.bin\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(path.Join(\"/\", folder4, \"afile.bin\"), 64, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(\"/\", folder4, \"afile.bin\"), path.Join(\"/\", folder5, \"afile_renamed.bin\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(\"/\", folder5, \"afile_renamed.bin\"), path.Join(\"/\", folder6, \"afile_renamed.bin\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = writeSFTPFile(path.Join(\"/\", folder4, \"afile.bin\"), 64, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(\"/\", folder7, \"afile.bin\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(\"/\", folder4, \"afile.bin\"), path.Join(\"/\", folder7, \"afile.bin\"))\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tfor _, folderName := range []string{folder1, folder2, folder3, folder4, folder5, folder6, folder7} {\n\t\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(filepath.Join(os.TempDir(), folderName))\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestDirs(t *testing.T) {\n\tu := getTestUser()\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tfolderName := filepath.Base(mappedPath)\n\tvdirPath := \"/path/vdir\"\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t})\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload,\n\t\tdataprovider.PermDelete, dataprovider.PermCreateDirs, dataprovider.PermRename, dataprovider.PermListItems}\n\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tinfo, err := client.ReadDir(\"/\")\n\t\tif assert.NoError(t, err) {\n\t\t\tif assert.Len(t, info, 1) {\n\t\t\t\tassert.Equal(t, \"path\", info[0].Name())\n\t\t\t}\n\t\t}\n\t\tfi, err := client.Stat(path.Dir(vdirPath))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, fi.IsDir())\n\t\t}\n\t\terr = client.RemoveDirectory(\"/\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.RemoveDirectory(vdirPath)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.RemoveDirectory(path.Dir(vdirPath))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.Mkdir(vdirPath)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Mkdir(\"adir\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"/adir\", path.Dir(vdirPath))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = client.MkdirAll(\"/subdir/adir\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"adir\", \"subdir/adir\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\terr = writeSFTPFile(\"/subdir/afile.bin\", 64, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(\"/afile.bin\", 32, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"afile.bin\", \"subdir/afile.bin\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(\"afile.bin\", \"subdir/afile1.bin\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Dir(vdirPath), \"renamed_vdir\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestCryptFsStat(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getCryptFsUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(4096)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, info.Size())\n\t\t}\n\t\tinfo, err = os.Stat(filepath.Join(user.HomeDir, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Greater(t, info.Size(), testFileSize)\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestFsPermissionErrors(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tuser, _, err := httpdtest.AddUser(getCryptFsUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestDir := \"tDir\"\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(user.GetHomeDir(), 0111)\n\t\tassert.NoError(t, err)\n\n\t\terr = client.RemoveDirectory(testDir)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(testDir, testDir+\"1\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\n\t\terr = os.Chmod(user.GetHomeDir(), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRenameErrorOutsideHomeDir(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldUploadMode := common.Config.UploadMode\n\toldTempPath := common.Config.TempPath\n\n\tcommon.Config.UploadMode = common.UploadModeAtomicWithResume\n\tcommon.Config.TempPath = filepath.Clean(os.TempDir())\n\tvfs.SetTempPath(common.Config.TempPath)\n\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = os.Chmod(user.GetHomeDir(), 0555)\n\t\tassert.NoError(t, err)\n\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\n\t\terr = os.Chmod(user.GetHomeDir(), os.ModeDir)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.UploadMode = oldUploadMode\n\tcommon.Config.TempPath = oldTempPath\n\tvfs.SetTempPath(oldTempPath)\n}\n\nfunc TestResolvePathError(t *testing.T) {\n\tu := getTestUser()\n\tu.HomeDir = \"relative_path\"\n\tconn := common.NewBaseConnection(\"\", common.ProtocolFTP, \"\", \"\", u)\n\ttestPath := \"apath\"\n\t_, err := conn.ListDir(testPath)\n\tassert.Error(t, err)\n\terr = conn.CreateDir(testPath, true)\n\tassert.Error(t, err)\n\terr = conn.RemoveDir(testPath)\n\tassert.Error(t, err)\n\terr = conn.Rename(testPath, testPath+\"1\")\n\tassert.Error(t, err)\n\terr = conn.CreateSymlink(testPath, testPath+\".sym\")\n\tassert.Error(t, err)\n\t_, err = conn.DoStat(testPath, 0, false)\n\tassert.Error(t, err)\n\terr = conn.RemoveAll(testPath)\n\tassert.Error(t, err)\n\terr = conn.SetStat(testPath, &common.StatAttributes{\n\t\tAtime: time.Now(),\n\t\tMtime: time.Now(),\n\t})\n\tassert.Error(t, err)\n\n\tu = getTestUser()\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: \"relative_mapped_path\",\n\t\t},\n\t\tVirtualPath: \"/vpath\",\n\t})\n\terr = os.MkdirAll(u.HomeDir, os.ModePerm)\n\tassert.NoError(t, err)\n\tconn.User = u\n\terr = conn.Rename(testPath, \"/vpath/subpath\")\n\tassert.Error(t, err)\n\n\toutHomePath := filepath.Join(os.TempDir(), testFileName)\n\terr = os.WriteFile(outHomePath, testFileContent, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.Symlink(outHomePath, filepath.Join(u.HomeDir, testFileName+\".link\"))\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(u.HomeDir, testFileName), testFileContent, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = conn.CreateSymlink(testFileName, testFileName+\".link\")\n\tassert.Error(t, err)\n\n\terr = os.RemoveAll(u.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.Remove(outHomePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserPasswordHashing(t *testing.T) {\n\tif config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName {\n\t\tt.Skip(\"this test is not supported with the memory provider\")\n\t}\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.PasswordHashing.Algo = dataprovider.HashingAlgoArgon2ID\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tcurrentUser, err := dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(currentUser.Password, \"$2a$\"))\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tu = getTestUser()\n\tuser, _, err = httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tcurrentUser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(currentUser.Password, \"$argon2id$\"))\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestAllowList(t *testing.T) {\n\tconfigCopy := common.Config\n\n\tentries := []dataprovider.IPListEntry{\n\t\t{\n\t\t\tIPOrNet:   \"172.18.1.1/32\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 0,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"172.18.1.2/32\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 0,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"10.8.7.0/24\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 5,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"0.0.0.0/0\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 8,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"::/0\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 8,\n\t\t},\n\t}\n\n\tfor _, e := range entries {\n\t\t_, resp, err := httpdtest.AddIPListEntry(e, http.StatusCreated)\n\t\tassert.NoError(t, err, string(resp))\n\t}\n\n\tcommon.Config.AllowListStatus = 1\n\terr := common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n\tassert.True(t, common.Config.IsAllowListEnabled())\n\n\ttestIP := \"172.18.1.1\"\n\tassert.NoError(t, common.Connections.IsNewConnectionAllowed(testIP, common.ProtocolFTP))\n\tentry := entries[0]\n\tentry.Protocols = 1\n\t_, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Error(t, common.Connections.IsNewConnectionAllowed(testIP, common.ProtocolFTP))\n\tassert.NoError(t, common.Connections.IsNewConnectionAllowed(testIP, common.ProtocolSSH))\n\t_, err = httpdtest.RemoveIPListEntry(entry, http.StatusOK)\n\tassert.NoError(t, err)\n\tentries = entries[1:]\n\tassert.Error(t, common.Connections.IsNewConnectionAllowed(testIP, common.ProtocolSSH))\n\tassert.Error(t, common.Connections.IsNewConnectionAllowed(\"172.18.1.3\", common.ProtocolSSH))\n\tassert.NoError(t, common.Connections.IsNewConnectionAllowed(\"172.18.1.3\", common.ProtocolHTTP))\n\n\tassert.NoError(t, common.Connections.IsNewConnectionAllowed(\"10.8.7.3\", common.ProtocolWebDAV))\n\tassert.NoError(t, common.Connections.IsNewConnectionAllowed(\"10.8.7.4\", common.ProtocolSSH))\n\tassert.Error(t, common.Connections.IsNewConnectionAllowed(\"10.8.7.4\", common.ProtocolFTP))\n\tassert.NoError(t, common.Connections.IsNewConnectionAllowed(\"10.8.7.4\", common.ProtocolHTTP))\n\tassert.NoError(t, common.Connections.IsNewConnectionAllowed(\"2001:0db8::1428:57ab\", common.ProtocolHTTP))\n\tassert.Error(t, common.Connections.IsNewConnectionAllowed(\"2001:0db8::1428:57ab\", common.ProtocolSSH))\n\tassert.Error(t, common.Connections.IsNewConnectionAllowed(\"10.8.8.2\", common.ProtocolWebDAV))\n\tassert.Error(t, common.Connections.IsNewConnectionAllowed(\"invalid IP\", common.ProtocolHTTP))\n\n\tcommon.Config = configCopy\n\terr = common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n\tassert.False(t, common.Config.IsAllowListEnabled())\n\n\tfor _, e := range entries {\n\t\t_, err := httpdtest.RemoveIPListEntry(e, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestDbDefenderErrors(t *testing.T) {\n\tif !isDbDefenderSupported() {\n\t\tt.Skip(\"this test is not supported with the current database provider\")\n\t}\n\tconfigCopy := common.Config\n\tcommon.Config.DefenderConfig.Enabled = true\n\tcommon.Config.DefenderConfig.Driver = common.DefenderDriverProvider\n\terr := common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n\n\ttestIP := \"127.1.1.1\"\n\thosts, err := common.GetDefenderHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 0)\n\tcommon.AddDefenderEvent(testIP, common.ProtocolSSH, common.HostEventLimitExceeded)\n\thosts, err = common.GetDefenderHosts()\n\tassert.NoError(t, err)\n\tassert.Len(t, hosts, 1)\n\tscore, err := common.GetDefenderScore(testIP)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 3, score)\n\tbanTime, err := common.GetDefenderBanTime(testIP)\n\tassert.NoError(t, err)\n\tassert.Nil(t, banTime)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\n\tcommon.AddDefenderEvent(testIP, common.ProtocolFTP, common.HostEventLimitExceeded)\n\t_, err = common.GetDefenderHosts()\n\tassert.Error(t, err)\n\t_, err = common.GetDefenderHost(testIP)\n\tassert.Error(t, err)\n\t_, err = common.GetDefenderBanTime(testIP)\n\tassert.Error(t, err)\n\t_, err = common.GetDefenderScore(testIP)\n\tassert.Error(t, err)\n\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.CleanupDefender(util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Hour)))\n\tassert.NoError(t, err)\n\n\tcommon.Config = configCopy\n\terr = common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestDelayedQuotaUpdater(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.DelayedQuotaUpdate = 120\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.QuotaFiles = 100\n\tu.TotalDataTransfer = 2000\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.UpdateUserQuota(&user, 10, 6000, false)\n\tassert.NoError(t, err)\n\terr = dataprovider.UpdateUserTransferQuota(&user, 100, 200, false)\n\tassert.NoError(t, err)\n\tfiles, size, ulSize, dlSize, err := dataprovider.GetUsedQuota(user.Username)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 10, files)\n\tassert.Equal(t, int64(6000), size)\n\tassert.Equal(t, int64(100), ulSize)\n\tassert.Equal(t, int64(200), dlSize)\n\n\tuserGet, err := dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, userGet.UsedQuotaFiles)\n\tassert.Equal(t, int64(0), userGet.UsedQuotaSize)\n\tassert.Equal(t, int64(0), userGet.UsedUploadDataTransfer)\n\tassert.Equal(t, int64(0), userGet.UsedDownloadDataTransfer)\n\n\terr = dataprovider.UpdateUserQuota(&user, 10, 6000, true)\n\tassert.NoError(t, err)\n\terr = dataprovider.UpdateUserTransferQuota(&user, 100, 200, true)\n\tassert.NoError(t, err)\n\tfiles, size, ulSize, dlSize, err = dataprovider.GetUsedQuota(user.Username)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 10, files)\n\tassert.Equal(t, int64(6000), size)\n\tassert.Equal(t, int64(100), ulSize)\n\tassert.Equal(t, int64(200), dlSize)\n\n\tuserGet, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 10, userGet.UsedQuotaFiles)\n\tassert.Equal(t, int64(6000), userGet.UsedQuotaSize)\n\tassert.Equal(t, int64(100), userGet.UsedUploadDataTransfer)\n\tassert.Equal(t, int64(200), userGet.UsedDownloadDataTransfer)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       \"folder\",\n\t\tMappedPath: filepath.Join(os.TempDir(), \"p\"),\n\t}\n\terr = dataprovider.AddFolder(&folder, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = dataprovider.UpdateVirtualFolderQuota(&folder, 10, 6000, false)\n\tassert.NoError(t, err)\n\tfiles, size, err = dataprovider.GetUsedVirtualFolderQuota(folder.Name)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 10, files)\n\tassert.Equal(t, int64(6000), size)\n\n\tfolderGet, err := dataprovider.GetFolderByName(folder.Name)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, folderGet.UsedQuotaFiles)\n\tassert.Equal(t, int64(0), folderGet.UsedQuotaSize)\n\n\terr = dataprovider.UpdateVirtualFolderQuota(&folder, 10, 6000, true)\n\tassert.NoError(t, err)\n\tfiles, size, err = dataprovider.GetUsedVirtualFolderQuota(folder.Name)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 10, files)\n\tassert.Equal(t, int64(6000), size)\n\n\tfolderGet, err = dataprovider.GetFolderByName(folder.Name)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 10, folderGet.UsedQuotaFiles)\n\tassert.Equal(t, int64(6000), folderGet.UsedQuotaSize)\n\n\terr = dataprovider.DeleteFolder(folder.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestPasswordCaching(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tdbUser, err := dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tfound, match := dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.False(t, found)\n\tassert.False(t, match)\n\n\tuser.Password = \"wrong\"\n\t_, _, err = getSftpClient(user)\n\tassert.Error(t, err)\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.False(t, found)\n\tassert.False(t, match)\n\tuser.Password = \"\"\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.True(t, found)\n\tassert.True(t, match)\n\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword+\"_\", dbUser.Password)\n\tassert.True(t, found)\n\tassert.False(t, match)\n\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username+\"_\", defaultPassword, dbUser.Password)\n\tassert.False(t, found)\n\tassert.False(t, match)\n\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t// the password was not changed\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.True(t, found)\n\tassert.True(t, match)\n\t// the password hash will change\n\tuser.Password = defaultPassword\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tdbUser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.False(t, found)\n\tassert.False(t, match)\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.True(t, found)\n\tassert.True(t, match)\n\t//change password\n\tnewPassword := defaultPassword + \"mod\"\n\tuser.Password = newPassword\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tdbUser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, newPassword, dbUser.Password)\n\tassert.False(t, found)\n\tassert.False(t, match)\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.True(t, found)\n\tassert.False(t, match)\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, newPassword, dbUser.Password)\n\tassert.True(t, found)\n\tassert.True(t, match)\n\t// update the password\n\terr = dataprovider.UpdateUserPassword(user.Username, defaultPassword, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tdbUser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\t// the stored hash does not match\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.False(t, found)\n\tassert.False(t, match)\n\n\tuser.Password = defaultPassword\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.True(t, found)\n\tassert.True(t, match)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tfound, match = dataprovider.CheckCachedUserPassword(user.Username, defaultPassword, dbUser.Password)\n\tassert.False(t, found)\n\tassert.False(t, match)\n}\n\nfunc TestEventRule(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint: \"http://localhost\",\n\t\t\t\tTimeout:  20,\n\t\t\t\tMethod:   http.MethodGet,\n\t\t\t},\n\t\t},\n\t}\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"action2\",\n\t\tType: dataprovider.ActionTypeBackup,\n\t}\n\ta3 := dataprovider.BaseEventAction{\n\t\tName: \"action3\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test1@example.com\", \"test2@example.com\"},\n\t\t\t\tBcc:        []string{\"test3@example.com\"},\n\t\t\t\tSubject:    `New \"{{.Event}}\" from \"{{.Name}}\" status {{.StatusString}}`,\n\t\t\t\tBody:       \"Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}} Data: {{.ObjectData}} {{.ErrorString}}\",\n\t\t\t},\n\t\t},\n\t}\n\ta4 := dataprovider.BaseEventAction{\n\t\tName: \"action4\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"failure@example.com\"},\n\t\t\t\tSubject:    `Failed \"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:       \"Fs path {{.FsPath}}, protocol: {{.Protocol}}, IP: {{.IP}} {{.ErrorString}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction4, _, err := httpdtest.AddEventAction(a4, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tEventStatuses: []int{1},\n\t\t\t\tFsPaths: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: \"/subdir/*.dat\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: \"/**/*.txt\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync:   true,\n\t\t\t\t\tStopOnFailure: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 3,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action4.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 4,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr2 := dataprovider.EventRule{\n\t\tName:    \"test rule2\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"download\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tFsPaths: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: \"/**/*.dat\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action4.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr3 := dataprovider.EventRule{\n\t\tName:    \"test rule3\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"delete\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule3, _, err := httpdtest.AddEventRule(r3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuploadScriptPath := filepath.Join(os.TempDir(), \"upload.sh\")\n\tu := getTestUser()\n\tu.DownloadDataTransfer = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tmovedFileName := \"moved.dat\"\n\tmovedPath := filepath.Join(user.HomeDir, movedFileName)\n\terr = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, \"\", 0), 0755)\n\tassert.NoError(t, err)\n\n\tdataprovider.EnabledActionCommands = []string{uploadScriptPath}\n\tdefer func() {\n\t\tdataprovider.EnabledActionCommands = nil\n\t}()\n\n\taction1.Type = dataprovider.ActionTypeCommand\n\taction1.Options = dataprovider.BaseEventActionOptions{\n\t\tCmdConfig: dataprovider.EventActionCommandConfig{\n\t\t\tCmd:     uploadScriptPath,\n\t\t\tTimeout: 10,\n\t\t\tEnvVars: []dataprovider.KeyValue{\n\t\t\t\t{\n\t\t\t\t\tKey:   \"SFTPGO_ACTION_PATH\",\n\t\t\t\t\tValue: \"{{.FsPath}}\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKey:   \"CUSTOM_ENV_VAR\",\n\t\t\t\t\tValue: \"value\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tdirName := \"subdir\"\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tsize := int64(32768)\n\t\t// rule conditions does not match\n\t\terr = writeSFTPFileNoCheck(testFileName, size, client)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, size, info.Size())\n\t\t}\n\t\terr = client.Mkdir(dirName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(\"subdir1\")\n\t\tassert.NoError(t, err)\n\t\t// rule conditions match\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFileNoCheck(path.Join(dirName, testFileName), size, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(dirName, testFileName))\n\t\tassert.Error(t, err)\n\t\tinfo, err = client.Stat(movedFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, size, info.Size())\n\t\t}\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 3)\n\t\tassert.True(t, slices.Contains(email.To, \"test1@example.com\"))\n\t\tassert.True(t, slices.Contains(email.To, \"test2@example.com\"))\n\t\tassert.True(t, slices.Contains(email.To, \"test3@example.com\"))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: New \"upload\" from \"%s\" status OK`, user.Username))\n\t\t// test the failure action, we download a file that exceeds the transfer quota limit\n\t\terr = writeSFTPFileNoCheck(path.Join(\"subdir1\", testFileName), 1*1024*1024+65535, client)\n\t\tassert.NoError(t, err)\n\t\tlastReceivedEmail.reset()\n\t\tf, err := client.Open(path.Join(\"subdir1\", testFileName))\n\t\tassert.NoError(t, err)\n\t\t_, err = io.ReadAll(f)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error())\n\t\t}\n\t\terr = f.Close()\n\t\tassert.Error(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 3)\n\t\tassert.True(t, slices.Contains(email.To, \"test1@example.com\"))\n\t\tassert.True(t, slices.Contains(email.To, \"test2@example.com\"))\n\t\tassert.True(t, slices.Contains(email.To, \"test3@example.com\"))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: New \"download\" from \"%s\" status KO`, user.Username))\n\t\tassert.Contains(t, email.Data, `\"download\" failed`)\n\t\tassert.Contains(t, email.Data, common.ErrReadQuotaExceeded.Error())\n\t\t_, err = httpdtest.UpdateTransferQuotaUsage(user, \"\", http.StatusOK)\n\t\tassert.NoError(t, err)\n\n\t\t// remove the upload script to test the failure action\n\t\terr = os.Remove(uploadScriptPath)\n\t\tassert.NoError(t, err)\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFileNoCheck(path.Join(dirName, testFileName), size, client)\n\t\tassert.Error(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"failure@example.com\"))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: Failed \"upload\" from \"%s\"`, user.Username))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`action %q failed`, action1.Name))\n\t\t// now test the download rule\n\t\tlastReceivedEmail.reset()\n\t\tf, err = client.Open(movedFileName)\n\t\tassert.NoError(t, err)\n\t\tcontents, err := io.ReadAll(f)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, contents, int(size))\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 3)\n\t\tassert.True(t, slices.Contains(email.To, \"test1@example.com\"))\n\t\tassert.True(t, slices.Contains(email.To, \"test2@example.com\"))\n\t\tassert.True(t, slices.Contains(email.To, \"test3@example.com\"))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: New \"download\" from \"%s\"`, user.Username))\n\t}\n\t// test upload action command with arguments\n\taction1.Options.CmdConfig.Args = []string{\"{{.Event}}\", \"{{.VirtualPath}}\", \"custom_arg\"}\n\taction1, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\tuploadLogFilePath := filepath.Join(os.TempDir(), \"upload.log\")\n\terr = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, uploadLogFilePath, 0), 0755)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = writeSFTPFileNoCheck(path.Join(dirName, testFileName), 123, client)\n\t\tassert.NoError(t, err)\n\n\t\tlogContent, err := os.ReadFile(uploadLogFilePath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fmt.Sprintf(\"upload %s custom_arg\", util.CleanPath(path.Join(dirName, testFileName))),\n\t\t\tstrings.TrimSpace(string(logContent)))\n\n\t\terr = os.Remove(uploadLogFilePath)\n\t\tassert.NoError(t, err)\n\t\tlastReceivedEmail.reset()\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t}\n\n\tlastReceivedEmail.reset()\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 3)\n\tassert.True(t, slices.Contains(email.To, \"test1@example.com\"))\n\tassert.True(t, slices.Contains(email.To, \"test2@example.com\"))\n\tassert.True(t, slices.Contains(email.To, \"test3@example.com\"))\n\tassert.Contains(t, email.Data, `Subject: New \"delete\" from \"admin\"`)\n\t_, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action4, http.StatusOK)\n\tassert.NoError(t, err)\n\tlastReceivedEmail.reset()\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleStatues(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test6@example.com\"},\n\t\t\t\tSubject:    `New \"{{.Event}}\" error`,\n\t\t\t\tBody:       \"{{.ErrorString}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr := dataprovider.EventRule{\n\t\tName:    \"rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tEventStatuses: []int{3},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule, resp, err := httpdtest.AddEventRule(r, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tu := getTestUser()\n\tu.UploadDataTransfer = 1\n\tu.DownloadDataTransfer = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestFileSize := int64(999999)\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Open(testFileName)\n\t\tassert.NoError(t, err)\n\t\tcontents := make([]byte, testFileSize)\n\t\tn, err := io.ReadFull(f, contents)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int(testFileSize), n)\n\t\tassert.Len(t, contents, int(testFileSize))\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\n\t\tlastReceivedEmail.reset()\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From == \"\"\n\t\t}, 600*time.Millisecond, 500*time.Millisecond)\n\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.Error(t, err)\n\t\tlastReceivedEmail.reset()\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test6@example.com\"))\n\t\tassert.Contains(t, email.Data, `Subject: New \"upload\" error`)\n\t\tassert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleDisabledCommand(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tsaveObjectScriptPath := filepath.Join(os.TempDir(), \"provider.sh\")\n\toutPath := filepath.Join(os.TempDir(), \"provider_out.json\")\n\terr = os.WriteFile(saveObjectScriptPath, getSaveProviderObjectScriptContent(outPath, 0), 0755)\n\tassert.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeCommand,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tCmdConfig: dataprovider.EventActionCommandConfig{\n\t\t\t\tCmd:     saveObjectScriptPath,\n\t\t\t\tTimeout: 10,\n\t\t\t\tEnvVars: []dataprovider.KeyValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   \"SFTPGO_OBJECT_DATA\",\n\t\t\t\t\t\tValue: \"{{.ObjectData}}\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test3@example.com\"},\n\t\t\t\tSubject:    `New \"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:       \"Object name: {{.ObjectName}} object type: {{.ObjectType}} Data: {{.ObjectData}}\",\n\t\t\t},\n\t\t},\n\t}\n\n\ta3 := dataprovider.BaseEventAction{\n\t\tName: \"a3\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"failure@example.com\"},\n\t\t\t\tSubject:    `Failed \"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:       \"Object name: {{.ObjectName}} object type: {{.ObjectType}}, IP: {{.IP}}\",\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddEventAction(a1, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t// Enable the command to allow saving\n\tdataprovider.EnabledActionCommands = []string{a1.Options.CmdConfig.Cmd}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr := dataprovider.EventRule{\n\t\tName:    \"rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"add\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tProviderObjects: []string{\"folder\"},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tStopOnFailure: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 3,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t\tStopOnFailure:   true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule, _, err := httpdtest.AddEventRule(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// restrict command execution\n\tdataprovider.EnabledActionCommands = nil\n\n\tlastReceivedEmail.reset()\n\t// create a folder to trigger the rule\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       \"ftest failed command\",\n\t\tMappedPath: filepath.Join(os.TempDir(), \"p\"),\n\t}\n\tfolder, _, err = httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tassert.NoFileExists(t, outPath)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"failure@example.com\"))\n\tassert.Contains(t, email.Data, `Subject: Failed \"add\" from \"admin\"`)\n\tassert.Contains(t, email.Data, fmt.Sprintf(\"Object name: %s object type: folder\", folder.Name))\n\tlastReceivedEmail.reset()\n\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestEventRuleProviderEvents(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tsaveObjectScriptPath := filepath.Join(os.TempDir(), \"provider.sh\")\n\toutPath := filepath.Join(os.TempDir(), \"provider_out.json\")\n\terr = os.WriteFile(saveObjectScriptPath, getSaveProviderObjectScriptContent(outPath, 0), 0755)\n\tassert.NoError(t, err)\n\n\tdataprovider.EnabledActionCommands = []string{saveObjectScriptPath}\n\tdefer func() {\n\t\tdataprovider.EnabledActionCommands = nil\n\t}()\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeCommand,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tCmdConfig: dataprovider.EventActionCommandConfig{\n\t\t\t\tCmd:     saveObjectScriptPath,\n\t\t\t\tTimeout: 10,\n\t\t\t\tEnvVars: []dataprovider.KeyValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   \"SFTPGO_OBJECT_DATA\",\n\t\t\t\t\t\tValue: \"{{.ObjectData}}\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test3@example.com\"},\n\t\t\t\tSubject:    `New \"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:       \"Object name: {{.ObjectName}} object type: {{.ObjectType}} Data: {{.ObjectData}}\",\n\t\t\t},\n\t\t},\n\t}\n\n\ta3 := dataprovider.BaseEventAction{\n\t\tName: \"a3\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"failure@example.com\"},\n\t\t\t\tSubject:    `Failed \"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:       \"Object name: {{.ObjectName}} object type: {{.ObjectType}}, IP: {{.IP}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr := dataprovider.EventRule{\n\t\tName:    \"rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"update\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tStopOnFailure: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 3,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t\tStopOnFailure:   true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule, _, err := httpdtest.AddEventRule(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tlastReceivedEmail.reset()\n\t// create and update a folder to trigger the rule\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       \"ftest rule\",\n\t\tMappedPath: filepath.Join(os.TempDir(), \"p\"),\n\t}\n\tfolder, _, err = httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// no action is triggered on add\n\tassert.NoFileExists(t, outPath)\n\t// update the folder\n\t_, _, err = httpdtest.UpdateFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Eventually(t, func() bool {\n\t\t_, err := os.Stat(outPath)\n\t\treturn err == nil\n\t}, 2*time.Second, 100*time.Millisecond) {\n\t\tcontent, err := os.ReadFile(outPath)\n\t\tassert.NoError(t, err)\n\t\tvar folderGet vfs.BaseVirtualFolder\n\t\terr = json.Unmarshal(content, &folderGet)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, folder, folderGet)\n\t\terr = os.Remove(outPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test3@example.com\"))\n\t\tassert.Contains(t, email.Data, `Subject: New \"update\" from \"admin\"`)\n\t}\n\t// now delete the script to generate an error\n\tlastReceivedEmail.reset()\n\terr = os.Remove(saveObjectScriptPath)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.UpdateFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NoFileExists(t, outPath)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"failure@example.com\"))\n\tassert.Contains(t, email.Data, `Subject: Failed \"update\" from \"admin\"`)\n\tassert.Contains(t, email.Data, fmt.Sprintf(\"Object name: %s object type: folder\", folder.Name))\n\tlastReceivedEmail.reset()\n\t// generate an error for the failure action\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\t_, _, err = httpdtest.UpdateFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NoFileExists(t, outPath)\n\temail = lastReceivedEmail.get()\n\tassert.Len(t, email.To, 0)\n\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestEventRuleFsActions(t *testing.T) {\n\tdirsToCreate := []string{\n\t\t\"/basedir/1\",\n\t\t\"/basedir/sub/2\",\n\t\t\"/basedir/3\",\n\t}\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType:   dataprovider.FilesystemActionMkdirs,\n\t\t\t\tMkDirs: dirsToCreate,\n\t\t\t},\n\t\t},\n\t}\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionRename,\n\t\t\t\tRenames: []dataprovider.RenameConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\t\t\tKey:   \"/{{.VirtualDirPath}}/{{.ObjectName}}\",\n\t\t\t\t\t\t\tValue: \"/{{.ObjectName}}_renamed\",\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\ta3 := dataprovider.BaseEventAction{\n\t\tName: \"a3\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType:    dataprovider.FilesystemActionDelete,\n\t\t\t\tDeletes: []string{\"/{{.ObjectName}}_renamed\"},\n\t\t\t},\n\t\t},\n\t}\n\ta4 := dataprovider.BaseEventAction{\n\t\tName: \"a4\",\n\t\tType: dataprovider.ActionTypeFolderQuotaReset,\n\t}\n\ta5 := dataprovider.BaseEventAction{\n\t\tName: \"a5\",\n\t\tType: dataprovider.ActionTypeUserQuotaReset,\n\t}\n\ta6 := dataprovider.BaseEventAction{\n\t\tName: \"a6\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType:  dataprovider.FilesystemActionExist,\n\t\t\t\tExist: []string{\"/{{.VirtualPath}}\"},\n\t\t\t},\n\t\t},\n\t}\n\taction1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\taction2, resp, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\taction3, resp, err := httpdtest.AddEventAction(a3, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\taction4, resp, err := httpdtest.AddEventAction(a4, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\taction5, resp, err := httpdtest.AddEventAction(a5, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\taction6, resp, err := httpdtest.AddEventAction(a6, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"r1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"add\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\tr2 := dataprovider.EventRule{\n\t\tName:    \"r2\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action5.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\tr3 := dataprovider.EventRule{\n\t\tName:    \"r3\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"mkdir\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action6.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\tr4 := dataprovider.EventRule{\n\t\tName:    \"r4\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"rmdir\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action4.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\tr5 := dataprovider.EventRule{\n\t\tName:    \"r5\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"add\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action4.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\trule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err)\n\trule3, _, err := httpdtest.AddEventRule(r3, http.StatusCreated)\n\tassert.NoError(t, err)\n\trule4, _, err := httpdtest.AddEventRule(r4, http.StatusCreated)\n\tassert.NoError(t, err)\n\trule5, _, err := httpdtest.AddEventRule(r5, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfolderMappedPath := filepath.Join(os.TempDir(), \"folder\")\n\terr = os.MkdirAll(folderMappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(folderMappedPath, \"file.txt\"), []byte(\"1\"), 0666)\n\tassert.NoError(t, err)\n\n\tfolder, _, err := httpdtest.AddFolder(vfs.BaseVirtualFolder{\n\t\tName:       \"test folder\",\n\t\tMappedPath: folderMappedPath,\n\t}, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\tfolderGet, _, err := httpdtest.GetFolderByName(folder.Name, http.StatusOK)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn folderGet.UsedQuotaFiles == 1 && folderGet.UsedQuotaSize == 1\n\t}, 2*time.Second, 100*time.Millisecond)\n\n\tu := getTestUser()\n\tu.Filters.DisableFsChecks = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\t// check initial directories creation\n\t\tfor _, dir := range dirsToCreate {\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\t_, err := client.Stat(dir)\n\t\t\t\treturn err == nil\n\t\t\t}, 2*time.Second, 100*time.Millisecond)\n\t\t}\n\t\t// upload a file and check the sync rename\n\t\tsize := int64(32768)\n\t\terr = writeSFTPFileNoCheck(path.Join(\"basedir\", testFileName), size, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(\"basedir\", testFileName))\n\t\tassert.Error(t, err)\n\t\tinfo, err := client.Stat(testFileName + \"_renamed\") //nolint:goconst\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, size, info.Size())\n\t\t}\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\tuserGet, _, err := httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn userGet.UsedQuotaFiles == 1 && userGet.UsedQuotaSize == size\n\t\t}, 2*time.Second, 100*time.Millisecond)\n\n\t\tfor i := 0; i < 2; i++ {\n\t\t\terr = client.Mkdir(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\t_, err = client.Stat(testFileName + \"_renamed\")\n\t\t\t\treturn err != nil\n\t\t\t}, 2*time.Second, 100*time.Millisecond)\n\t\t\terr = client.RemoveDirectory(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\terr = client.Mkdir(testFileName + \"_renamed\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testFileName)\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\t_, err = client.Stat(testFileName + \"_renamed\")\n\t\t\treturn err != nil\n\t\t}, 2*time.Second, 100*time.Millisecond)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(folderMappedPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule4, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule5, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action4, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action5, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action6, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestEventActionObjectBaseName(t *testing.T) {\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionRename,\n\t\t\t\tRenames: []dataprovider.RenameConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\t\t\tKey:   \"/{{.VirtualDirPath}}/{{.ObjectName}}\",\n\t\t\t\t\t\t\tValue: \"/{{.ObjectBaseName}}\",\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\taction1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"r2\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestDir := \"test dir name\"\n\t\terr = client.Mkdir(testDir)\n\t\tfileSize := int64(32768)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFileNoCheck(path.Join(testDir, testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\n\t\t_, err = client.Stat(path.Join(testDir, testFileName))\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\n\t\t_, err = client.Stat(strings.TrimSuffix(testFileName, path.Ext(testFileName)))\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadEventRule(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test1@example.com\"},\n\t\t\t\tSubject:    `New \"{{.Event}}\" from \"{{.Name}}\" status {{.StatusString}}`,\n\t\t\t\tBody:       \"Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}} Data: {{.ObjectData}} {{.ErrorString}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tFsPaths: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern:      \"/**/*.filepart\",\n\t\t\t\t\t\tInverseMatch: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFileNoCheck(\"/test.filepart\", 32768, client)\n\t\tassert.NoError(t, err)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Empty(t, email.From)\n\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFileNoCheck(testFileName, 32768, client)\n\t\tassert.NoError(t, err)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.Contains(t, email.Data, `Subject: New \"upload\"`)\n\t}\n\n\tr2 := dataprovider.EventRule{\n\t\tName:    \"test rule2\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"rename\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tFsPaths: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: \"/**/*.filepart\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttempName := \"file.filepart\"\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFileNoCheck(tempName, 32768, client)\n\t\tassert.NoError(t, err)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Empty(t, email.From)\n\n\t\tlastReceivedEmail.reset()\n\t\terr = client.Rename(tempName, testFileName)\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.Contains(t, email.Data, `Subject: New \"rename\"`)\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRulePreDelete(t *testing.T) {\n\tmovePath := \"recycle bin\"\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionRename,\n\t\t\t\tRenames: []dataprovider.RenameConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\t\t\tKey:   \"/{{.VirtualPath}}\",\n\t\t\t\t\t\t\tValue: fmt.Sprintf(\"/%s/{{.VirtualPath}}\", movePath),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tUpdateModTime: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"pre-delete\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tFsPaths: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern:      fmt.Sprintf(\"/%s/**\", movePath),\n\t\t\t\t\t\tInverseMatch: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       movePath,\n\t\tMappedPath: filepath.Join(os.TempDir(), movePath),\n\t}\n\t_, _, err = httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tu.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: movePath,\n\t\t\t},\n\t\t\tVirtualPath: \"/\" + movePath,\n\t\t\tQuotaFiles:  1000,\n\t\t},\n\t}\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaFiles = 1000\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\n\t\t\ttestDir := \"sub dir\"\n\t\t\terr = client.MkdirAll(testDir)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = writeSFTPFile(testFileName, 100, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = writeSFTPFile(path.Join(testDir, testFileName), 100, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tmodTime := time.Now().Add(-36 * time.Hour)\n\t\t\terr = client.Chtimes(testFileName, modTime, modTime)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Remove(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Remove(path.Join(testDir, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\t// check files\n\t\t\t_, err = client.Stat(testFileName)\n\t\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t\t_, err = client.Stat(path.Join(testDir, testFileName))\n\t\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t\tinfo, err := client.Stat(path.Join(\"/\", movePath, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\tdiff := math.Abs(time.Until(info.ModTime()).Seconds())\n\t\t\tassert.LessOrEqual(t, diff, float64(2))\n\n\t\t\t_, err = client.Stat(path.Join(\"/\", movePath, testDir, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\t// check quota\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == localUser.Username {\n\t\t\t\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t\t\t\tfolder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 2, folder.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(200), folder.UsedQuotaSize)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(100), user.UsedQuotaSize)\n\t\t\t}\n\t\t\t// pre-delete action is not executed in movePath\n\t\t\terr = client.Remove(path.Join(\"/\", movePath, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == localUser.Username {\n\t\t\t\t// check quota\n\t\t\t\tfolder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, folder.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(100), folder.UsedQuotaSize)\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: movePath}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(filepath.Join(os.TempDir(), movePath))\n\tassert.NoError(t, err)\n}\n\nfunc TestEventRulePreDownloadUpload(t *testing.T) {\n\ttestDir := \"/d\"\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType:   dataprovider.FilesystemActionMkdirs,\n\t\t\t\tMkDirs: []string{testDir},\n\t\t\t},\n\t\t},\n\t}\n\taction1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionRename,\n\t\t\t\tRenames: []dataprovider.RenameConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\t\t\tKey:   \"/missing source\",\n\t\t\t\t\t\t\tValue: \"/missing target\",\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\taction2, resp, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"pre-download\", \"pre-upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\t// the rule will always succeed, so uploads/downloads will work\n\t\terr = writeSFTPFile(testFileName, 100, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(testDir)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Open(testFileName)\n\t\tassert.NoError(t, err)\n\t\tcontents := make([]byte, 100)\n\t\tn, err := io.ReadFull(f, contents)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int(100), n)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\t// disable the rule\n\t\trule1.Status = 0\n\t\t_, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName, 100, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(testDir)\n\t\tassert.ErrorIs(t, err, fs.ErrNotExist)\n\t\t// now update the rule so that it will always fail\n\t\trule1.Status = 1\n\t\trule1.Actions = []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\t_, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Open(testFileName)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName, 100, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestEventActionCommandEnvVars(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tenvName := \"MY_ENV\"\n\tuploadScriptPath := filepath.Join(os.TempDir(), \"upload.sh\")\n\n\tdataprovider.EnabledActionCommands = []string{uploadScriptPath}\n\tdefer func() {\n\t\tdataprovider.EnabledActionCommands = nil\n\t}()\n\n\terr := os.WriteFile(uploadScriptPath, getUploadScriptEnvContent(envName), 0755)\n\tassert.NoError(t, err)\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeCommand,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tCmdConfig: dataprovider.EventActionCommandConfig{\n\t\t\t\tCmd:     uploadScriptPath,\n\t\t\t\tTimeout: 10,\n\t\t\t\tEnvVars: []dataprovider.KeyValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   envName,\n\t\t\t\t\t\tValue: \"$\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = writeSFTPFileNoCheck(testFileName, 100, client)\n\t\tassert.Error(t, err)\n\t}\n\n\tos.Setenv(envName, \"1\")\n\tdefer os.Unsetenv(envName)\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = writeSFTPFileNoCheck(testFileName, 100, client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.Remove(uploadScriptPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestFsActionCopy(t *testing.T) {\n\tdirCopy := \"/dircopy\"\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionCopy,\n\t\t\t\tCopy: []dataprovider.KeyValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   \"/{{.VirtualPath}}/\",\n\t\t\t\t\t\tValue: dirCopy + \"/\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tg1 := dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName: \"group1\",\n\t\t},\n\t\tUserSettings: dataprovider.GroupUserSettings{\n\t\t\tBaseGroupUserSettings: sdk.BaseGroupUserSettings{\n\t\t\t\tPermissions: map[string][]string{\n\t\t\t\t\t// Restrict permissions in copyPath to check that action\n\t\t\t\t\t// will have full permissions anyway.\n\t\t\t\t\tdirCopy: {dataprovider.PermListItems, dataprovider.PermDelete},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tgroup1, resp, err := httpdtest.AddGroup(g1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = writeSFTPFile(testFileName, 100, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(dirCopy, testFileName))\n\t\tassert.NoError(t, err)\n\n\t\taction1.Options.FsConfig.Copy = []dataprovider.KeyValue{\n\t\t\t{\n\t\t\t\tKey:   \"/missing path\",\n\t\t\t\tValue: \"/copied path\",\n\t\t\t},\n\t\t}\n\t\t_, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\t// copy a missing path will fail\n\t\terr = writeSFTPFile(testFileName, 100, client)\n\t\tassert.Error(t, err)\n\t}\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestEventFsActionsGroupFilters(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"example@example.net\"},\n\t\t\t\tSubject:    `New \"{{.Event}}\" from \"{{.Name}}\" status {{.StatusString}}`,\n\t\t\t\tBody:       \"Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}} {{.ErrorString}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tGroupNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: \"group*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\t// the user has no group, so the rule does not match\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFile(testFileName, 32, client)\n\t\tassert.NoError(t, err)\n\t\tassert.Empty(t, lastReceivedEmail.get().From)\n\t}\n\tg1 := dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName: \"agroup1\",\n\t\t},\n\t}\n\tgroup1, _, err := httpdtest.AddGroup(g1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tg2 := dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName: \"group2\",\n\t\t},\n\t}\n\tgroup2, _, err := httpdtest.AddGroup(g2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\t// the group does not match\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFile(testFileName, 32, client)\n\t\tassert.NoError(t, err)\n\t\tassert.Empty(t, lastReceivedEmail.get().From)\n\t}\n\tuser.Groups = append(user.Groups, sdk.GroupMapping{\n\t\tName: group2.Name,\n\t\tType: sdk.GroupTypeSecondary,\n\t})\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\t// the group matches\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFile(testFileName, 32, client)\n\t\tassert.NoError(t, err)\n\t\tassert.NotEmpty(t, lastReceivedEmail.get().From)\n\t}\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventProviderActionGroupFilters(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"example@example.net\"},\n\t\t\t\tSubject:    `New \"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:       \"IP: {{.IP}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"add\", \"update\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tGroupNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: \"group_*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tProviderObjects: []string{\"user\"},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tg1 := dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName: \"agroup_1\",\n\t\t},\n\t}\n\tgroup1, _, err := httpdtest.AddGroup(g1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tg2 := dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName: \"group_2\",\n\t\t},\n\t}\n\tgroup2, _, err := httpdtest.AddGroup(g2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group2.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\n\tlastReceivedEmail.reset()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\n\tuser.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\n\tlastReceivedEmail.reset()\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\ttime.Sleep(300 * time.Millisecond)\n\temail = lastReceivedEmail.get()\n\tassert.Len(t, email.To, 0)\n\n\tuser.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group2.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\n\tlastReceivedEmail.reset()\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\temail = lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestBackupAsAttachment(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1 with space\",\n\t\tType: dataprovider.ActionTypeBackup,\n\t}\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients:  []string{\"test@example.com\"},\n\t\t\t\tSubject:     `\"{{.Event}} {{.StatusString}}\"`,\n\t\t\t\tBody:        \"Domain: {{.Name}}\",\n\t\t\t\tAttachments: []string{\"/{{.VirtualPath}}\"},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rule certificate\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerCertificate,\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tlastReceivedEmail.reset()\n\trenewalEvent := \"Certificate renewal\"\n\n\tcommon.HandleCertificateEvent(common.EventParams{\n\t\tName:      \"example.com\",\n\t\tTimestamp: time.Now(),\n\t\tStatus:    1,\n\t\tEvent:     renewalEvent,\n\t})\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"%s OK\"`, renewalEvent))\n\tassert.Contains(t, email.Data, `Domain: example.com`)\n\tassert.Contains(t, email.Data, \"Content-Type: application/json\")\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventActionHTTPMultipart(t *testing.T) {\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint: fmt.Sprintf(\"http://%s/multipart\", httpAddr),\n\t\t\t\tMethod:   http.MethodPut,\n\t\t\t\tParts: []dataprovider.HTTPPart{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"part1\",\n\t\t\t\t\t\tHeaders: []dataprovider.KeyValue{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tKey:   \"Content-Type\",\n\t\t\t\t\t\t\t\tValue: \"application/json\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBody: `{\"FilePath\": \"{{.VirtualPath}}\"}`,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"file\",\n\t\t\t\t\t\tFilepath: \"/{{.VirtualPath}}\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test http multipart\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\t// now add an missing file to the http multipart action\n\t\taction1.Options.HTTPConfig.Parts = append(action1.Options.HTTPConfig.Parts, dataprovider.HTTPPart{\n\t\t\tName:     \"file1\",\n\t\t\tFilepath: \"/missing\",\n\t\t})\n\t\t_, resp, err = httpdtest.UpdateEventAction(action1, http.StatusOK)\n\t\tassert.NoError(t, err, string(resp))\n\n\t\tf, err = client.Create(\"testfile.txt\")\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestEventActionCompress(t *testing.T) {\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionCompress,\n\t\t\t\tCompress: dataprovider.EventActionFsCompress{\n\t\t\t\t\tName:  \"/{{.VirtualPath}}.zip\",\n\t\t\t\t\tPaths: []string{\"/{{.VirtualPath}}\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test compress\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.FsConfig.SFTPConfig.BufferSize = 1\n\tu.QuotaFiles = 1000\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getCryptFsUser()\n\tu.QuotaFiles = 1000\n\tcryptFsUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser, cryptFsUser} {\n\t\t// cleanup home dir\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t\trule1.Conditions.Options.Names = []dataprovider.ConditionPattern{\n\t\t\t{\n\t\t\t\tPattern: user.Username,\n\t\t\t},\n\t\t}\n\t\t_, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\n\t\tconn, client, err := getSftpClient(user)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\n\t\t\texpectedQuotaSize := int64(len(testFileContent))\n\t\t\texpectedQuotaFiles := 1\n\t\t\tif user.Username == cryptFsUser.Username {\n\t\t\t\tencryptedFileSize, err := getEncryptedFileSize(expectedQuotaSize)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\texpectedQuotaSize = encryptedFileSize\n\t\t\t}\n\n\t\t\tf, err := client.Create(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = f.Write(testFileContent)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tinfo, err := client.Stat(testFileName + \".zip\") //nolint:goconst\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Greater(t, info.Size(), int64(0))\n\t\t\t\t// check quota\n\t\t\t\tarchiveSize := info.Size()\n\t\t\t\tif user.Username == cryptFsUser.Username {\n\t\t\t\t\tencryptedFileSize, err := getEncryptedFileSize(archiveSize)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tarchiveSize = encryptedFileSize\n\t\t\t\t}\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedQuotaFiles+1, user.UsedQuotaFiles,\n\t\t\t\t\t\"quota file does no match for user %q\", user.Username)\n\t\t\t\tassert.Equal(t, expectedQuotaSize+archiveSize, user.UsedQuotaSize,\n\t\t\t\t\t\"quota size does no match for user %q\", user.Username)\n\t\t\t}\n\t\t\t// now overwrite the same file\n\t\t\tf, err = client.Create(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = f.Write(testFileContent)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tinfo, err = client.Stat(testFileName + \".zip\")\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Greater(t, info.Size(), int64(0))\n\t\t\t\tarchiveSize := info.Size()\n\t\t\t\tif user.Username == cryptFsUser.Username {\n\t\t\t\t\tencryptedFileSize, err := getEncryptedFileSize(archiveSize)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tarchiveSize = encryptedFileSize\n\t\t\t\t}\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedQuotaFiles+1, user.UsedQuotaFiles,\n\t\t\t\t\t\"quota file after overwrite does no match for user %q\", user.Username)\n\t\t\t\tassert.Equal(t, expectedQuotaSize+archiveSize, user.UsedQuotaSize,\n\t\t\t\t\t\"quota size after overwrite does no match for user %q\", user.Username)\n\t\t\t}\n\t\t}\n\t\tif user.Username == localUser.Username {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(cryptFsUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(cryptFsUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestEventActionCompressQuotaErrors(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notify@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ttestDir := \"archiveDir\"\n\tzipPath := \"/archive.zip\"\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionCompress,\n\t\t\t\tCompress: dataprovider.EventActionFsCompress{\n\t\t\t\t\tName:  zipPath,\n\t\t\t\t\tPaths: []string{\"/\" + testDir},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"action2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test@example.com\"},\n\t\t\t\tSubject:    `\"Compress failed\"`,\n\t\t\t\tBody:       \"Error: {{.ErrorString}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test compress\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"rename\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfileSize := int64(100)\n\tu := getTestUser()\n\tu.QuotaSize = 10 * fileSize\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.MkdirAll(path.Join(testDir, \"1\", \"1\"))\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, \"1\", testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.MkdirAll(path.Join(testDir, \"2\", \"2\"))\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, \"2\", testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(testDir, \"2\", testFileName), path.Join(testDir, \"2\", testFileName+\"_link\"))\n\t\tassert.NoError(t, err)\n\t\t// trigger the compress action\n\t\terr = client.Mkdir(\"a\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"a\", \"b\")\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\t_, err := client.Stat(zipPath)\n\t\t\treturn err == nil\n\t\t}, 3*time.Second, 100*time.Millisecond)\n\t\terr = client.Remove(zipPath)\n\t\tassert.NoError(t, err)\n\t\t// add other 6 file, the compress action should fail with a quota error\n\t\terr = writeSFTPFile(path.Join(testDir, \"1\", \"1\", testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, \"2\", \"2\", testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, \"1\", \"1\", testFileName+\"1\"), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, \"2\", \"2\", testFileName+\"2\"), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, \"1\", testFileName+\"1\"), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, \"2\", testFileName+\"2\"), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\tlastReceivedEmail.reset()\n\t\terr = client.Rename(\"b\", \"a\")\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3*time.Second, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\t\tassert.Contains(t, email.Data, `Subject: \"Compress failed\"`)\n\t\tassert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())\n\t\t// update quota size so the user is already overquota\n\t\tuser.QuotaSize = 7 * fileSize\n\t\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tlastReceivedEmail.reset()\n\t\terr = client.Rename(\"a\", \"b\")\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3*time.Second, 100*time.Millisecond)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\t\tassert.Contains(t, email.Data, `Subject: \"Compress failed\"`)\n\t\tassert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())\n\t\t// remove the path to compress to trigger an error for size estimation\n\t\tout, err := runSSHCommand(fmt.Sprintf(\"sftpgo-remove %s\", testDir), user)\n\t\tassert.NoError(t, err, string(out))\n\t\tlastReceivedEmail.reset()\n\t\terr = client.Rename(\"b\", \"a\")\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3*time.Second, 100*time.Millisecond)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\t\tassert.Contains(t, email.Data, `Subject: \"Compress failed\"`)\n\t\tassert.Contains(t, email.Data, \"unable to estimate archive size\")\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventActionCompressQuotaFolder(t *testing.T) {\n\ttestDir := \"/folder\"\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionCompress,\n\t\t\t\tCompress: dataprovider.EventActionFsCompress{\n\t\t\t\t\tName:  \"/{{.VirtualPath}}.zip\",\n\t\t\t\t\tPaths: []string{\"/{{.VirtualPath}}\", testDir},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test compress\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tmappedPath := filepath.Join(os.TempDir(), \"virtualpath\")\n\tfolderName := filepath.Base(mappedPath)\n\tvdirPath := \"/virtualpath\"\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err = httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize := int64(len(testFileContent))\n\t\texpectedQuotaFiles := 1\n\t\terr = client.Symlink(path.Join(testDir, testFileName), path.Join(testDir, testFileName+\"_link\"))\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Create(path.Join(testDir, testFileName))\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tinfo, err := client.Stat(path.Join(testDir, testFileName) + \".zip\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Greater(t, info.Size(), int64(0))\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\texpectedQuotaFiles++\n\t\t\texpectedQuotaSize += info.Size()\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t}\n\t\tvfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, vfolder.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(0), vfolder.UsedQuotaSize)\n\t\t// upload in the virtual path\n\t\tf, err = client.Create(path.Join(vdirPath, testFileName))\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tinfo, err = client.Stat(path.Join(vdirPath, testFileName) + \".zip\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Greater(t, info.Size(), int64(0))\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\texpectedQuotaFiles += 2\n\t\t\texpectedQuotaSize += info.Size() + int64(len(testFileContent))\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\tvfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 0, vfolder.UsedQuotaFiles)\n\t\t\tassert.Equal(t, int64(0), vfolder.UsedQuotaSize)\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestEventActionCompressErrors(t *testing.T) {\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionCompress,\n\t\t\t\tCompress: dataprovider.EventActionFsCompress{\n\t\t\t\t\tName:  \"/{{.VirtualPath}}.zip\",\n\t\t\t\t\tPaths: []string{\"/{{.VirtualPath}}.zip\"}, // cannot compress itself\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test compress\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.Error(t, err)\n\t}\n\t// try to compress a missing file\n\taction1.Options.FsConfig.Compress.Paths = []string{\"/missing file\"}\n\t_, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.Error(t, err)\n\t}\n\t// try to overwrite a directory\n\ttestDir := \"/adir\"\n\taction1.Options.FsConfig.Compress.Name = testDir\n\taction1.Options.FsConfig.Compress.Paths = []string{\"/{{.VirtualPath}}\"}\n\t_, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestEventActionEmailAttachments(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notify@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionCompress,\n\t\t\t\tCompress: dataprovider.EventActionFsCompress{\n\t\t\t\t\tName:  \"/archive/{{.VirtualPath}}.zip\",\n\t\t\t\t\tPaths: []string{\"/{{.VirtualPath}}\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"action2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients:  []string{\"test@example.com\"},\n\t\t\t\tSubject:     `\"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:        \"Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}} {{.EscapedVirtualPath}}\",\n\t\t\t\tAttachments: []string{\"/archive/{{.VirtualPath}}.zip\"},\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test email with attachment\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestSFTPUser()\n\tu.FsConfig.SFTPConfig.BufferSize = 1\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tcryptFsUser, _, err := httpdtest.AddUser(getCryptFsUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser, cryptFsUser} {\n\t\tconn, client, err := getSftpClient(user)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\n\t\t\tlastReceivedEmail.reset()\n\t\t\tf, err := client.Create(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = f.Write(testFileContent)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\t\temail := lastReceivedEmail.get()\n\t\t\tassert.Len(t, email.To, 1)\n\t\t\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\t\t\tassert.Contains(t, email.Data, `Subject: \"upload\" from`)\n\t\t\tassert.Contains(t, email.Data, url.QueryEscape(\"/\"+testFileName))\n\t\t\tassert.Contains(t, email.Data, \"Content-Disposition: attachment\")\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(cryptFsUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(cryptFsUser.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventActionsRetentionReports(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notify@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ttestDir := \"/d\"\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeDataRetentionCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tRetentionConfig: dataprovider.EventActionDataRetentionConfig{\n\t\t\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t\t\t{\n\t\t\t\t\t\tPath:            testDir,\n\t\t\t\t\t\tRetention:       1,\n\t\t\t\t\t\tDeleteEmptyDirs: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"action2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients:  []string{\"test@example.com\"},\n\t\t\t\tSubject:     `\"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:        \"Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}}\",\n\t\t\t\tAttachments: []string{dataprovider.RetentionReportPlaceHolder},\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta3 := dataprovider.BaseEventAction{\n\t\tName: \"action3\",\n\t\tType: dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint: fmt.Sprintf(\"http://%s/\", httpAddr),\n\t\t\t\tTimeout:  20,\n\t\t\t\tMethod:   http.MethodPost,\n\t\t\t\tBody:     dataprovider.RetentionReportPlaceHolder,\n\t\t\t},\n\t\t},\n\t}\n\taction3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta4 := dataprovider.BaseEventAction{\n\t\tName: \"action4\",\n\t\tType: dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint: fmt.Sprintf(\"http://%s/multipart\", httpAddr),\n\t\t\t\tTimeout:  20,\n\t\t\t\tMethod:   http.MethodPost,\n\t\t\t\tParts: []dataprovider.HTTPPart{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"reports.zip\",\n\t\t\t\t\t\tFilepath: dataprovider.RetentionReportPlaceHolder,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\taction4, _, err := httpdtest.AddEventAction(a4, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync:   true,\n\t\t\t\t\tStopOnFailure: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync:   true,\n\t\t\t\t\tStopOnFailure: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 3,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync:   true,\n\t\t\t\t\tStopOnFailure: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action4.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 4,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync:   true,\n\t\t\t\t\tStopOnFailure: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tsubdir := path.Join(testDir, \"sub\")\n\t\terr = client.MkdirAll(subdir)\n\t\tassert.NoError(t, err)\n\n\t\tlastReceivedEmail.reset()\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"upload\" from \"%s\"`, user.Username))\n\t\tassert.Contains(t, email.Data, \"Content-Disposition: attachment\")\n\t\t_, err = client.Stat(testDir)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(subdir)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\n\t\terr = client.Mkdir(subdir)\n\t\tassert.NoError(t, err)\n\t\tnewName := path.Join(testDir, testFileName)\n\t\terr = client.Rename(testFileName, newName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Chtimes(newName, time.Now().Add(-24*time.Hour), time.Now().Add(-24*time.Hour))\n\t\tassert.NoError(t, err)\n\n\t\tlastReceivedEmail.reset()\n\t\tf, err = client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\t_, err = client.Stat(subdir)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t_, err = client.Stat(subdir)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t}\n\t// now remove the retention check to test errors\n\trule1.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: action2.Name,\n\t\t\t},\n\t\t\tOrder: 2,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tExecuteSync:   true,\n\t\t\t\tStopOnFailure: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: action3.Name,\n\t\t\t},\n\t\t\tOrder: 3,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tExecuteSync:   true,\n\t\t\t\tStopOnFailure: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: action4.Name,\n\t\t\t},\n\t\t\tOrder: 4,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tExecuteSync:   true,\n\t\t\t\tStopOnFailure: false,\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tf, err := client.Create(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = f.Write(testFileContent)\n\t\tassert.NoError(t, err)\n\t\terr = f.Close()\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action4, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleFirstUploadDownloadActions(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notify@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test@example.com\"},\n\t\t\t\tSubject:    `\"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:       \"Fs path {{.FsPath}}, size: {{.FileSize}}, protocol: {{.Protocol}}, IP: {{.IP}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test first upload rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"first-upload\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr2 := dataprovider.EventRule{\n\t\tName:    \"test first download rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"first-download\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestFileSize := int64(32768)\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFileNoCheck(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"first-upload\" from \"%s\"`, user.Username))\n\t\tlastReceivedEmail.reset()\n\t\t// a new upload will not produce a new notification\n\t\terr = writeSFTPFileNoCheck(testFileName+\"_1\", 32768, client)\n\t\tassert.NoError(t, err)\n\t\tassert.Never(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\t\t// the same for download\n\t\tf, err := client.Open(testFileName)\n\t\tassert.NoError(t, err)\n\t\tcontents := make([]byte, testFileSize)\n\t\tn, err := io.ReadFull(f, contents)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int(testFileSize), n)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail = lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"first-download\" from \"%s\"`, user.Username))\n\t\t// download again\n\t\tlastReceivedEmail.reset()\n\t\tf, err = client.Open(testFileName)\n\t\tassert.NoError(t, err)\n\t\tcontents = make([]byte, testFileSize)\n\t\tn, err = io.ReadFull(f, contents)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int(testFileSize), n)\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\tassert.Never(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleRenameEvent(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notify@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients:  []string{\"test@example.com\"},\n\t\t\t\tSubject:     `\"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tContentType: 1,\n\t\t\t\tBody:        `<p>Fs path {{.FsPath}}, Name: {{.Name}}, Target path \"{{.VirtualTargetDirPath}}/{{.TargetName}}\", size: {{.FileSize}}</p>`,\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rename rule\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"rename\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.Username = \"test & chars\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestFileSize := int64(32768)\n\t\tlastReceivedEmail.reset()\n\t\terr = writeSFTPFileNoCheck(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(\"subdir\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, path.Join(\"/subdir\", testFileName))\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"rename\" from \"%s\"`, user.Username))\n\t\tassert.Contains(t, email.Data, \"Content-Type: text/html\")\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(\"Target path %q\", path.Join(\"/subdir\", testFileName)))\n\t\tassert.Contains(t, email.Data, \"Name: test &amp; chars,\")\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleIDPLogin(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notify@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\tlastReceivedEmail.reset()\n\n\tusername := `test_'idp_'login`\n\tcustom1 := `cust\"oa\"1`\n\tu := map[string]any{\n\t\t\"username\": \"{{.Name}}\",\n\t\t\"status\":   1,\n\t\t\"home_dir\": filepath.Join(os.TempDir(), \"{{.IDPFieldcustom1}}\"),\n\t\t\"permissions\": map[string][]string{\n\t\t\t\"/\": {dataprovider.PermAny},\n\t\t},\n\t}\n\tuserTmpl, err := json.Marshal(u)\n\trequire.NoError(t, err)\n\ta := map[string]any{\n\t\t\"username\":    \"{{.Name}}\",\n\t\t\"status\":      1,\n\t\t\"permissions\": []string{dataprovider.PermAdminAny},\n\t}\n\tadminTmpl, err := json.Marshal(a)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeIDPAccountCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tIDPConfig: dataprovider.EventActionIDPAccountCheck{\n\t\t\t\tMode:          1, // create if not exists\n\t\t\t\tTemplateUser:  string(userTmpl),\n\t\t\t\tTemplateAdmin: string(adminTmpl),\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test@example.com\"},\n\t\t\t\tSubject:    `\"{{.Event}} {{.StatusString}}\"`,\n\t\t\t\tBody:       \"{{.Name}} Custom field: {{.IDPFieldcustom1}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rule IDP login\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerIDPLogin,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tIDPLoginEvent: dataprovider.IDPLoginUser,\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name, // the rule is not sync and will be skipped\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\trule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tcustomFields := map[string]any{\n\t\t\"custom1\": custom1,\n\t}\n\tuser, admin, err := common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginUser,\n\t\tStatus: 1,\n\t}, &customFields)\n\tassert.Nil(t, user)\n\tassert.Nil(t, admin)\n\tassert.NoError(t, err)\n\n\trule1.Actions[0].Options.ExecuteSync = true\n\trule1, resp, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\tuser, admin, err = common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginUser,\n\t\tStatus: 1,\n\t}, &customFields)\n\tif assert.NotNil(t, user) {\n\t\tassert.Equal(t, filepath.Join(os.TempDir(), custom1), user.GetHomeDir())\n\t\t_, err = httpdtest.RemoveUser(*user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t}\n\tassert.Nil(t, admin)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"%s OK\"`, common.IDPLoginUser))\n\tassert.Contains(t, email.Data, username)\n\tassert.Contains(t, email.Data, custom1)\n\n\tuser, admin, err = common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginAdmin,\n\t\tStatus: 1,\n\t}, &customFields)\n\tassert.Nil(t, user)\n\tassert.Nil(t, admin)\n\tassert.NoError(t, err)\n\n\trule1.Conditions.IDPLoginEvent = dataprovider.IDPLoginAny\n\trule1.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: action1.Name,\n\t\t\t},\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tExecuteSync: true,\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t},\n\t}\n\trule1, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tr2 := dataprovider.EventRule{\n\t\tName:    \"test email on IDP login\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerIDPLogin,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tIDPLoginEvent: dataprovider.IDPLoginAdmin,\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule2, resp, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tlastReceivedEmail.reset()\n\tuser, admin, err = common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginAdmin,\n\t\tStatus: 1,\n\t}, &customFields)\n\tassert.Nil(t, user)\n\tif assert.NotNil(t, admin) {\n\t\tassert.Equal(t, 1, admin.Status)\n\t}\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail = lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"%s OK\"`, common.IDPLoginAdmin))\n\tassert.Contains(t, email.Data, username)\n\tassert.Contains(t, email.Data, custom1)\n\tadmin.Status = 0\n\t_, _, err = httpdtest.UpdateAdmin(*admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, admin, err = common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginAdmin,\n\t\tStatus: 1,\n\t}, &customFields)\n\tassert.Nil(t, user)\n\tif assert.NotNil(t, admin) {\n\t\tassert.Equal(t, 0, admin.Status)\n\t}\n\tassert.NoError(t, err)\n\taction1.Options.IDPConfig.Mode = 0\n\taction1, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, admin, err = common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginAdmin,\n\t\tStatus: 1,\n\t}, &customFields)\n\tassert.Nil(t, user)\n\tif assert.NotNil(t, admin) {\n\t\tassert.Equal(t, 1, admin.Status)\n\t}\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAdmin(*admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tr3 := dataprovider.EventRule{\n\t\tName:    \"test rule2 IDP login\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerIDPLogin,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tIDPLoginEvent: dataprovider.IDPLoginAny,\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule3, resp, err := httpdtest.AddEventRule(r3, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tuser, admin, err = common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginAdmin,\n\t\tStatus: 1,\n\t}, &customFields)\n\tassert.Nil(t, user)\n\tassert.Nil(t, admin)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"more than one account check action rules matches\")\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)\n\tassert.NoError(t, err)\n\n\taction1.Options.IDPConfig.TemplateAdmin = `{}`\n\taction1, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginAdmin,\n\t\tStatus: 1,\n\t}, &customFields)\n\tassert.ErrorIs(t, err, util.ErrValidation)\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tuser, admin, err = common.HandleIDPLoginEvent(common.EventParams{\n\t\tName:   username,\n\t\tEvent:  common.IDPLoginAdmin,\n\t\tStatus: 1,\n\t}, &customFields)\n\tassert.Nil(t, user)\n\tassert.Nil(t, admin)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleEmailField(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notify@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\tlastReceivedEmail.reset()\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"{{.Email}}\"},\n\t\t\t\tSubject:    `\"{{.Event}}\" from \"{{.Name}}\"`,\n\t\t\t\tBody:       \"Sample email body\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"action2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"failure@example.com\"},\n\t\t\t\tSubject:    `\"Failure`,\n\t\t\t\tBody:       \"{{.ErrorString}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"r1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"mkdir\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tr2 := dataprovider.EventRule{\n\t\tName:    \"test rule2\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"add\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tProviderObjects: []string{\"user\"},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\trule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.Email = \"user@example.com\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, user.Email))\n\tassert.Contains(t, email.Data, `Subject: \"add\" from \"admin\"`)\n\n\t// if we add a user without email the notification will fail\n\tlastReceivedEmail.reset()\n\tu1 := getTestUser()\n\tu1.Username += \"_1\"\n\tuser1, _, err := httpdtest.AddUser(u1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail = lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"failure@example.com\"))\n\tassert.Contains(t, email.Data, `no recipient addresses set`)\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlastReceivedEmail.reset()\n\t\terr = client.Mkdir(testFileName)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.True(t, slices.Contains(email.To, user.Email))\n\t\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"mkdir\" from \"%s\"`, user.Username))\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleCertificate(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notify@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\tlastReceivedEmail.reset()\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients:  []string{\"test@example.com\"},\n\t\t\t\tSubject:     `\"{{.Event}} {{.StatusString}}\"`,\n\t\t\t\tContentType: 0,\n\t\t\t\tBody:        \"Domain: {{.Name}} Timestamp: {{.Timestamp}} {{.ErrorString}} Date time: {{.DateTime}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"action2\",\n\t\tType: dataprovider.ActionTypeFolderQuotaReset,\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rule certificate\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerCertificate,\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr2 := dataprovider.EventRule{\n\t\tName:    \"test rule 2\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerCertificate,\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\trule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\trenewalEvent := \"Certificate renewal\"\n\n\tcommon.HandleCertificateEvent(common.EventParams{\n\t\tName:      \"example.com\",\n\t\tTimestamp: time.Now(),\n\t\tStatus:    1,\n\t\tEvent:     renewalEvent,\n\t})\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"%s OK\"`, renewalEvent))\n\tassert.Contains(t, email.Data, \"Content-Type: text/plain\")\n\tassert.Contains(t, email.Data, `Domain: example.com Timestamp`)\n\n\tlastReceivedEmail.reset()\n\tdateTime := time.Now()\n\tparams := common.EventParams{\n\t\tName:      \"example.com\",\n\t\tTimestamp: dateTime,\n\t\tStatus:    2,\n\t\tEvent:     renewalEvent,\n\t}\n\terrRenew := errors.New(\"generic renew error\")\n\tparams.AddError(errRenew)\n\tcommon.HandleCertificateEvent(params)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail = lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"test@example.com\"))\n\tassert.Contains(t, email.Data, fmt.Sprintf(`Subject: \"%s KO\"`, renewalEvent))\n\tassert.Contains(t, email.Data, `Domain: example.com Timestamp`)\n\tassert.Contains(t, email.Data, dateTime.UTC().Format(\"2006-01-02T15:04:05.000\"))\n\tassert.Contains(t, email.Data, errRenew.Error())\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t// ignored no more certificate rules\n\tcommon.HandleCertificateEvent(common.EventParams{\n\t\tName:      \"example.com\",\n\t\tTimestamp: time.Now(),\n\t\tStatus:    1,\n\t\tEvent:     renewalEvent,\n\t})\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleIPBlocked(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.DefenderConfig.Enabled = true\n\tcfg.DefenderConfig.Threshold = 3\n\tcfg.DefenderConfig.ScoreLimitExceeded = 2\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"action1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"test3@example.com\", \"test4@example.com\"},\n\t\t\t\tSubject:    `New \"{{.Event}}\"`,\n\t\t\t\tBody:       \"IP: {{.IP}} Timestamp: {{.Timestamp}}\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"action2\",\n\t\tType: dataprovider.ActionTypeFolderQuotaReset,\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"test rule ip blocked\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerIPBlocked,\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr2 := dataprovider.EventRule{\n\t\tName:    \"test rule 2\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerIPBlocked,\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\trule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlastReceivedEmail.reset()\n\ttime.Sleep(300 * time.Millisecond)\n\tassert.Empty(t, lastReceivedEmail.get().From, lastReceivedEmail.get().Data)\n\n\tfor i := 0; i < 3; i++ {\n\t\tuser.Password = \"wrong_pwd\"\n\t\t_, _, err = getSftpClient(user)\n\t\tassert.Error(t, err)\n\t}\n\t// the client is now banned\n\tuser.Password = defaultPassword\n\t_, _, err = getSftpClient(user)\n\tassert.Error(t, err)\n\t// check the email notification\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 2)\n\tassert.True(t, slices.Contains(email.To, \"test3@example.com\"))\n\tassert.True(t, slices.Contains(email.To, \"test4@example.com\"))\n\tassert.Contains(t, email.Data, `Subject: New \"IP Blocked\"`)\n\n\terr = dataprovider.DeleteEventRule(rule1.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteEventRule(rule2.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteEventAction(action1.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteEventAction(action2.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestEventRuleRotateLog(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeRotateLogs,\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"success@example.net\"},\n\t\t\t\tSubject:    `OK`,\n\t\t\t\tBody:       \"OK action\",\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"mkdir\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: user.Username,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\trule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlastReceivedEmail.reset()\n\t\terr := client.Mkdir(\"just a test dir\")\n\t\tassert.NoError(t, err)\n\t\t// just check that the action is executed\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.Contains(t, email.To, \"success@example.net\")\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRuleInactivityCheck(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeUserInactivityCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tUserInactivityConfig: dataprovider.EventActionUserInactivity{\n\t\t\t\tDisableThreshold: 10,\n\t\t\t\tDeleteThreshold:  20,\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"success@example.net\"},\n\t\t\t\tSubject:    `OK`,\n\t\t\t\tBody:       \"OK action\",\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"mkdir\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: user.Username,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t},\n\t}\n\trule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlastReceivedEmail.reset()\n\t\terr := client.Mkdir(\"just a test dir\")\n\t\tassert.NoError(t, err)\n\t\t// just check that the action is executed\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.Contains(t, email.To, \"success@example.net\")\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestEventRulePasswordExpiration(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"failure@example.net\"},\n\t\t\t\tSubject:    `Failure`,\n\t\t\t\tBody:       \"Failure action\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypePasswordExpirationCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tPwdExpirationConfig: dataprovider.EventActionPasswordExpiration{\n\t\t\t\tThreshold: 10,\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta3 := dataprovider.BaseEventAction{\n\t\tName: \"a3\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"success@example.net\"},\n\t\t\t\tSubject:    `OK`,\n\t\t\t\tBody:       \"OK action\",\n\t\t\t},\n\t\t},\n\t}\n\taction3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"mkdir\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, resp, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tdirName := \"aTestDir\"\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlastReceivedEmail.reset()\n\t\terr := client.Mkdir(dirName)\n\t\tassert.NoError(t, err)\n\t\t// the user has no password expiration, the check will be skipped and the ok action executed\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.Contains(t, email.To, \"success@example.net\")\n\t\terr = client.RemoveDirectory(dirName)\n\t\tassert.NoError(t, err)\n\t}\n\tuser.Filters.PasswordExpiration = 20\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlastReceivedEmail.reset()\n\t\terr := client.Mkdir(dirName)\n\t\tassert.NoError(t, err)\n\t\t// the passowrd is not about to expire, the check will be skipped and the ok action executed\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.Contains(t, email.To, \"success@example.net\")\n\t\terr = client.RemoveDirectory(dirName)\n\t\tassert.NoError(t, err)\n\t}\n\tuser.Filters.PasswordExpiration = 5\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlastReceivedEmail.reset()\n\t\terr := client.Mkdir(dirName)\n\t\tassert.NoError(t, err)\n\t\t// the passowrd is about to expire, the user has no email, the failure action will be executed\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 1)\n\t\tassert.Contains(t, email.To, \"failure@example.net\")\n\t\terr = client.RemoveDirectory(dirName)\n\t\tassert.NoError(t, err)\n\t}\n\t// remove the success action\n\trule1.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: action2.Name,\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t},\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: action1.Name,\n\t\t\t},\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tIsFailureAction: true,\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Email = \"user@example.net\"\n\tuser.Filters.AdditionalEmails = []string{\"additional@example.net\"}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tlastReceivedEmail.reset()\n\t\terr := client.Mkdir(dirName)\n\t\tassert.NoError(t, err)\n\t\t// the passowrd expiration will be notified\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn lastReceivedEmail.get().From != \"\"\n\t\t}, 1500*time.Millisecond, 100*time.Millisecond)\n\t\temail := lastReceivedEmail.get()\n\t\tassert.Len(t, email.To, 2)\n\t\tassert.Contains(t, email.To, user.Email)\n\t\tassert.Contains(t, email.To, user.Filters.AdditionalEmails[0])\n\t\tassert.Contains(t, email.Data, \"your SFTPGo password expires in 5 days\")\n\t\terr = client.RemoveDirectory(dirName)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestSyncUploadAction(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tuploadScriptPath := filepath.Join(os.TempDir(), \"upload.sh\")\n\tcommon.Config.Actions.ExecuteOn = []string{\"upload\"}\n\tcommon.Config.Actions.ExecuteSync = []string{\"upload\"}\n\tcommon.Config.Actions.Hook = uploadScriptPath\n\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tmovedFileName := \"moved.dat\"\n\tmovedPath := filepath.Join(user.HomeDir, movedFileName)\n\terr = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, \"\", 0), 0755)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tsize := int64(32768)\n\t\terr = writeSFTPFileNoCheck(testFileName, size, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(testFileName)\n\t\tassert.Error(t, err)\n\t\tinfo, err := client.Stat(movedFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, size, info.Size())\n\t\t}\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, size, user.UsedQuotaSize)\n\t\t// test some hook failure\n\t\t// the uploaded file is moved and the hook fails, it will be not removed from the quota\n\t\terr = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, \"\", 1), 0755)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFileNoCheck(testFileName+\"_1\", size, client)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, size*2, user.UsedQuotaSize)\n\n\t\t// the uploaded file is not moved and the hook fails, the uploaded file will be deleted\n\t\t// and removed from the quota\n\t\tmovedPath = filepath.Join(user.HomeDir, \"missing dir\", movedFileName)\n\t\terr = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, \"\", 1), 0755)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFileNoCheck(testFileName+\"_2\", size, client)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, size*2, user.UsedQuotaSize)\n\t\t// overwrite an existing file\n\t\t_, err = client.Stat(movedFileName)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFileNoCheck(movedFileName, size, client)\n\t\tassert.Error(t, err)\n\t\t_, err = client.Stat(movedFileName)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, size, user.UsedQuotaSize)\n\t}\n\n\terr = os.Remove(uploadScriptPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.ExecuteOn = nil\n\tcommon.Config.Actions.ExecuteSync = nil\n\tcommon.Config.Actions.Hook = uploadScriptPath\n}\n\nfunc TestQuotaTrackDisabled(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.TrackQuota = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = writeSFTPFile(testFileName, 32, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\"1\")\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestGetQuotaError(t *testing.T) {\n\tif dataprovider.GetProviderStatus().Driver == \"memory\" {\n\t\tt.Skip(\"this test is not available with the memory provider\")\n\t}\n\tu := getTestUser()\n\tu.TotalDataTransfer = 2000\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tfolderName := filepath.Base(mappedPath)\n\tvdirPath := \"/vpath\"\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t\tQuotaSize:   0,\n\t\tQuotaFiles:  10,\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = writeSFTPFile(testFileName, 32, client)\n\t\tassert.NoError(t, err)\n\n\t\terr = dataprovider.Close()\n\t\tassert.NoError(t, err)\n\n\t\terr = client.Rename(testFileName, path.Join(vdirPath, testFileName))\n\t\tassert.Error(t, err)\n\n\t\terr = config.LoadConfig(configDir, \"\")\n\t\tassert.NoError(t, err)\n\t\tproviderConf := config.GetProviderConf()\n\t\terr = dataprovider.Initialize(providerConf, configDir, true)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestRetentionAPI(t *testing.T) {\n\tu := getTestUser()\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload,\n\t\tdataprovider.PermOverwrite, dataprovider.PermDownload, dataprovider.PermCreateDirs,\n\t\tdataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuploadPath := path.Join(testDir, testFileName)\n\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(uploadPath, 32, client)\n\t\tassert.NoError(t, err)\n\n\t\tfolderRetention := []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:            \"/\",\n\t\t\t\tRetention:       24,\n\t\t\t\tDeleteEmptyDirs: true,\n\t\t\t},\n\t\t}\n\t\tcheck := common.RetentionCheck{\n\t\t\tFolders: folderRetention,\n\t\t}\n\t\tc := common.RetentionChecks.Add(check, &user)\n\t\tassert.NotNil(t, c)\n\t\terr = c.Start()\n\t\tassert.NoError(t, err)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\t\t_, err = client.Stat(uploadPath)\n\t\tassert.NoError(t, err)\n\n\t\terr = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))\n\t\tassert.NoError(t, err)\n\n\t\terr = c.Start()\n\t\tassert.NoError(t, err)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\t\t_, err = client.Stat(uploadPath)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\n\t\t_, err = client.Stat(testDir)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(uploadPath, 32, client)\n\t\tassert.NoError(t, err)\n\n\t\tcheck.Folders[0].DeleteEmptyDirs = false\n\t\terr = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))\n\t\tassert.NoError(t, err)\n\n\t\tc = common.RetentionChecks.Add(check, &user)\n\t\tassert.NotNil(t, c)\n\t\terr = c.Start()\n\t\tassert.NoError(t, err)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\t\t_, err = client.Stat(uploadPath)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\n\t\t_, err = client.Stat(testDir)\n\t\tassert.NoError(t, err)\n\n\t\terr = writeSFTPFile(uploadPath, 32, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))\n\t\tassert.NoError(t, err)\n\t\tconn.Close()\n\t\tclient.Close()\n\t}\n\n\t// remove delete permissions to the user, it will be automatically granted\n\tuser.Permissions[\"/\"+testDir] = []string{dataprovider.PermListItems, dataprovider.PermUpload,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermChtimes}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tinnerUploadFilePath := path.Join(\"/\"+testDir, testDir, testFileName)\n\t\terr = client.Mkdir(path.Join(testDir, testDir))\n\t\tassert.NoError(t, err)\n\n\t\terr = writeSFTPFile(innerUploadFilePath, 32, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Chtimes(innerUploadFilePath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))\n\t\tassert.NoError(t, err)\n\n\t\tfolderRetention := []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:      \"/missing\",\n\t\t\t\tRetention: 24,\n\t\t\t},\n\t\t\t{\n\t\t\t\tPath:            \"/\" + testDir,\n\t\t\t\tRetention:       24,\n\t\t\t\tDeleteEmptyDirs: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tPath:      path.Dir(innerUploadFilePath),\n\t\t\t\tRetention: 0,\n\t\t\t},\n\t\t}\n\t\tcheck := common.RetentionCheck{\n\t\t\tFolders: folderRetention,\n\t\t}\n\t\tc := common.RetentionChecks.Add(check, &user)\n\t\tassert.NotNil(t, c)\n\t\terr = c.Start()\n\t\tassert.NoError(t, err)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\t\t_, err = client.Stat(uploadPath)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t_, err = client.Stat(innerUploadFilePath)\n\t\tassert.NoError(t, err)\n\n\t\tfolderRetention = []dataprovider.FolderRetention{\n\n\t\t\t{\n\t\t\t\tPath:            \"/\" + testDir,\n\t\t\t\tRetention:       24,\n\t\t\t\tDeleteEmptyDirs: true,\n\t\t\t},\n\t\t}\n\n\t\tcheck = common.RetentionCheck{\n\t\t\tFolders: folderRetention,\n\t\t}\n\t\tc = common.RetentionChecks.Add(check, &user)\n\t\tassert.NotNil(t, c)\n\t\terr = c.Start()\n\t\tassert.NoError(t, err)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\t\t_, err = client.Stat(innerUploadFilePath)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\tconn.Close()\n\t\tclient.Close()\n\t}\n\t// finally test some errors removing files or folders\n\tif runtime.GOOS != osWindows {\n\t\tdirPath := filepath.Join(user.HomeDir, \"adir\", \"sub\")\n\t\terr := os.MkdirAll(dirPath, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tfilePath := filepath.Join(dirPath, \"f.dat\")\n\t\terr = os.WriteFile(filePath, nil, os.ModePerm)\n\t\tassert.NoError(t, err)\n\n\t\terr = os.Chtimes(filePath, time.Now().Add(-72*time.Hour), time.Now().Add(-72*time.Hour))\n\t\tassert.NoError(t, err)\n\n\t\terr = os.Chmod(dirPath, 0001)\n\t\tassert.NoError(t, err)\n\n\t\tfolderRetention := []dataprovider.FolderRetention{\n\n\t\t\t{\n\t\t\t\tPath:            \"/adir\",\n\t\t\t\tRetention:       24,\n\t\t\t\tDeleteEmptyDirs: true,\n\t\t\t},\n\t\t}\n\n\t\tcheck := common.RetentionCheck{\n\t\t\tFolders: folderRetention,\n\t\t}\n\t\tc := common.RetentionChecks.Add(check, &user)\n\t\tassert.NotNil(t, c)\n\t\terr = c.Start()\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\t\terr = os.Chmod(dirPath, 0555)\n\t\tassert.NoError(t, err)\n\n\t\tc = common.RetentionChecks.Add(check, &user)\n\t\tassert.NotNil(t, c)\n\t\terr = c.Start()\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\t\terr = os.Chmod(dirPath, os.ModePerm)\n\t\tassert.NoError(t, err)\n\n\t\tcheck = common.RetentionCheck{\n\t\t\tFolders: folderRetention,\n\t\t}\n\t\tc = common.RetentionChecks.Add(check, &user)\n\t\tassert.NotNil(t, c)\n\t\terr = c.Start()\n\t\tassert.NoError(t, err)\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\t\tassert.NoDirExists(t, dirPath)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1*time.Second, 50*time.Millisecond)\n}\n\nfunc TestPerUserTransferLimits(t *testing.T) {\n\toldMaxPerHostConns := common.Config.MaxPerHostConnections\n\n\tcommon.Config.MaxPerHostConnections = 2\n\n\tu := getTestUser()\n\tu.UploadBandwidth = 32\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tif !assert.NoError(t, err) {\n\t\tprintLatestLogs(20)\n\t}\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tvar wg sync.WaitGroup\n\t\tnumErrors := 0\n\t\tfor i := 0; i <= 2; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func(counter int) {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\ttime.Sleep(20 * time.Millisecond)\n\t\t\t\terr := writeSFTPFile(fmt.Sprintf(\"%s_%d\", testFileName, counter), 64*1024, client)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnumErrors++\n\t\t\t\t}\n\t\t\t}(i)\n\t\t}\n\t\twg.Wait()\n\n\t\tassert.Equal(t, 1, numErrors)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.MaxPerHostConnections = oldMaxPerHostConns\n}\n\nfunc TestMaxSessionsSameConnection(t *testing.T) {\n\tu := getTestUser()\n\tu.UploadBandwidth = 32\n\tu.MaxSessions = 2\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tvar wg sync.WaitGroup\n\t\tnumErrors := 0\n\t\tfor i := 0; i <= 2; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func(counter int) {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\tvar err error\n\t\t\t\tif counter < 2 {\n\t\t\t\t\terr = writeSFTPFile(fmt.Sprintf(\"%s_%d\", testFileName, counter), 64*1024, client)\n\t\t\t\t} else {\n\t\t\t\t\t// wait for the transfers to start\n\t\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\t\t\t_, _, err = getSftpClient(user)\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tnumErrors++\n\t\t\t\t}\n\t\t\t}(i)\n\t\t}\n\n\t\twg.Wait()\n\t\tassert.Equal(t, 1, numErrors)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRenameDir(t *testing.T) {\n\tu := getTestUser()\n\ttestDir := \"/dir-to-rename\"\n\tu.Permissions[testDir] = []string{dataprovider.PermListItems, dataprovider.PermUpload}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, testFileName), 32, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testDir, testDir+\"_rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestBuiltinKeyboardInteractiveAuthentication(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tauthMethods := []ssh.AuthMethod{\n\t\tssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) {\n\t\t\treturn []string{defaultPassword}, nil\n\t\t}),\n\t}\n\tconn, client, err := getCustomAuthSftpClient(user, authMethods)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t}\n\t// add multi-factor authentication\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tpasscode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)\n\tassert.NoError(t, err)\n\tpasswordAsked := false\n\tpasscodeAsked := false\n\tauthMethods = []ssh.AuthMethod{\n\t\tssh.KeyboardInteractive(func(_, _ string, questions []string, _ []bool) ([]string, error) {\n\t\t\tvar answers []string\n\t\t\tif strings.HasPrefix(questions[0], \"Password\") {\n\t\t\t\tanswers = append(answers, defaultPassword)\n\t\t\t\tpasswordAsked = true\n\t\t\t} else {\n\t\t\t\tanswers = append(answers, passcode)\n\t\t\t\tpasscodeAsked = true\n\t\t\t}\n\t\t\treturn answers, nil\n\t\t}),\n\t}\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t}\n\tassert.True(t, passwordAsked)\n\tassert.True(t, passcodeAsked)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMultiStepBuiltinKeyboardAuth(t *testing.T) {\n\tu := getTestUser()\n\tu.PublicKeys = []string{testPubKey}\n\tu.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsigner, err := ssh.ParsePrivateKey([]byte(testPrivateKey))\n\tassert.NoError(t, err)\n\t// public key + password\n\tauthMethods := []ssh.AuthMethod{\n\t\tssh.PublicKeys(signer),\n\t\tssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) {\n\t\t\treturn []string{defaultPassword}, nil\n\t\t}),\n\t}\n\tconn, client, err := getCustomAuthSftpClient(user, authMethods)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t}\n\t// add multi-factor authentication\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tpasscode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)\n\tassert.NoError(t, err)\n\t// public key + passcode\n\tauthMethods = []ssh.AuthMethod{\n\t\tssh.PublicKeys(signer),\n\t\tssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) {\n\t\t\treturn []string{passcode}, nil\n\t\t}),\n\t}\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRenameSymlink(t *testing.T) {\n\tu := getTestUser()\n\ttestDir := \"/dir-no-create-links\"\n\totherDir := \"otherdir\"\n\tu.Permissions[testDir] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete,\n\t\tdataprovider.PermCreateDirs}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(otherDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(otherDir, otherDir+\".link\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(otherDir+\".link\", path.Join(testDir, \"symlink\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(otherDir+\".link\", \"allowed_link\")\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSplittedDeletePerms(t *testing.T) {\n\tu := getTestUser()\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDeleteDirs,\n\t\tdataprovider.PermCreateDirs}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.Error(t, err)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(testDir)\n\t\tassert.NoError(t, err)\n\t}\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDeleteFiles,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermOverwrite}\n\t_, _, err = httpdtest.UpdateUser(u, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(testDir)\n\t\tassert.Error(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSplittedRenamePerms(t *testing.T) {\n\tu := getTestUser()\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermRenameDirs,\n\t\tdataprovider.PermCreateDirs}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\"_renamed\")\n\t\tassert.Error(t, err)\n\t\terr = client.Rename(testDir, testDir+\"_renamed\")\n\t\tassert.NoError(t, err)\n\t}\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermRenameFiles,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermOverwrite}\n\t_, _, err = httpdtest.UpdateUser(u, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\"_renamed\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testDir, testDir+\"_renamed\")\n\t\tassert.Error(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPLoopError(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          2525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\tuser1 := getTestUser()\n\tuser2 := getTestUser()\n\tuser1.Username += \"1\"\n\tuser2.Username += \"2\"\n\t// user1 is a local account with a virtual SFTP folder to user2\n\t// user2 has user1 as SFTP fs\n\tf := vfs.BaseVirtualFolder{\n\t\tName: \"sftp\",\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\tUsername: user2.Username,\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\tfolder, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser1.VirtualFolders = append(user1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folder.Name,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t})\n\n\tuser2.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: sftpServerAddr,\n\t\t\tUsername: user1.Username,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t}\n\n\tuser1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tuser2, resp, err = httpdtest.AddUser(user2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: \"a1\",\n\t\tType: dataprovider.ActionTypeUserQuotaReset,\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: \"a2\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"failure@example.com\"},\n\t\t\t\tSubject:    `Failed action\"`,\n\t\t\t\tBody:       \"Test body\",\n\t\t\t},\n\t\t},\n\t}\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr1 := dataprovider.EventRule{\n\t\tName:    \"rule1\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"update\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tlastReceivedEmail.reset()\n\t_, _, err = httpdtest.UpdateUser(user2, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn lastReceivedEmail.get().From != \"\"\n\t}, 3000*time.Millisecond, 100*time.Millisecond)\n\temail := lastReceivedEmail.get()\n\tassert.Len(t, email.To, 1)\n\tassert.True(t, slices.Contains(email.To, \"failure@example.com\"))\n\tassert.Contains(t, email.Data, `Subject: Failed action`)\n\n\tuser1.VirtualFolders[0].FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\tuser2.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\n\tconn := common.NewBaseConnection(\"\", common.ProtocolWebDAV, \"\", \"\", user1)\n\t_, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath)\n\tassert.ErrorIs(t, err, os.ErrPermission)\n\n\tconn = common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", user1)\n\t_, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath)\n\tassert.Error(t, err)\n\tconn = common.NewBaseConnection(\"\", common.ProtocolFTP, \"\", \"\", user1)\n\t_, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user2.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestNonLocalCrossRename(t *testing.T) {\n\tbaseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tu := getTestUser()\n\tu.HomeDir += \"_folders\"\n\tu.Username += \"_folders\"\n\tmappedPathSFTP := filepath.Join(os.TempDir(), \"sftp\")\n\tfolderNameSFTP := filepath.Base(mappedPathSFTP)\n\tvdirSFTPPath := \"/vdir/sftp\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameSFTP,\n\t\t},\n\t\tVirtualPath: vdirSFTPPath,\n\t})\n\tmappedPathCrypt := filepath.Join(os.TempDir(), \"crypt\")\n\tfolderNameCrypt := filepath.Base(mappedPathCrypt)\n\tvdirCryptPath := \"/vdir/crypt\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameCrypt,\n\t\t},\n\t\tVirtualPath: vdirCryptPath,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName: folderNameSFTP,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\tUsername: baseUser.Username,\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName: folderNameCrypt,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t\tMappedPath: mappedPathCrypt,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirSFTPPath, testFileName), 8192, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirCryptPath, testFileName), 16384, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirSFTPPath, testFileName), path.Join(vdirCryptPath, testFileName+\".rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(vdirCryptPath, testFileName), path.Join(vdirSFTPPath, testFileName+\".rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(testFileName, path.Join(vdirCryptPath, testFileName+\".rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(testFileName, path.Join(vdirSFTPPath, testFileName+\".rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(vdirSFTPPath, testFileName), testFileName+\".rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(vdirCryptPath, testFileName), testFileName+\".rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t// rename on local fs or on the same folder must work\n\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirSFTPPath, testFileName), path.Join(vdirSFTPPath, testFileName+\"_rename\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirCryptPath, testFileName), path.Join(vdirCryptPath, testFileName+\"_rename\"))\n\t\tassert.NoError(t, err)\n\t\t// renaming a virtual folder is not allowed\n\t\terr = client.Rename(vdirSFTPPath, vdirSFTPPath+\"_rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(vdirCryptPath, vdirCryptPath+\"_rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(vdirCryptPath, path.Join(vdirCryptPath, \"rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Mkdir(path.Join(vdirCryptPath, \"subcryptdir\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirCryptPath, \"subcryptdir\"), vdirCryptPath)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t// renaming root folder is not allowed\n\t\terr = client.Rename(\"/\", \"new_name\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t// renaming a path to a virtual folder is not allowed\n\t\terr = client.Rename(\"/vdir\", \"new_vdir\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameSFTP}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathCrypt)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathSFTP)\n\tassert.NoError(t, err)\n}\n\nfunc TestNonLocalCrossRenameNonLocalBaseUser(t *testing.T) {\n\tbaseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tu := getTestSFTPUser()\n\tmappedPathLocal := filepath.Join(os.TempDir(), \"local\")\n\tfolderNameLocal := filepath.Base(mappedPathLocal)\n\tvdirLocalPath := \"/vdir/local\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameLocal,\n\t\t},\n\t\tVirtualPath: vdirLocalPath,\n\t})\n\tmappedPathCrypt := filepath.Join(os.TempDir(), \"crypt\")\n\tfolderNameCrypt := filepath.Base(mappedPathCrypt)\n\tvdirCryptPath := \"/vdir/crypt\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameCrypt,\n\t\t},\n\t\tVirtualPath: vdirCryptPath,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderNameLocal,\n\t\tMappedPath: mappedPathLocal,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName: folderNameCrypt,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t\tMappedPath: mappedPathCrypt,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\terr = writeSFTPFile(testFileName, 4096, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirLocalPath, testFileName), 8192, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirCryptPath, testFileName), 16384, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirLocalPath, testFileName), path.Join(vdirCryptPath, testFileName+\".rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(vdirCryptPath, testFileName), path.Join(vdirLocalPath, testFileName+\".rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(testFileName, path.Join(vdirCryptPath, testFileName+\".rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(testFileName, path.Join(vdirLocalPath, testFileName+\".rename\"))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(vdirLocalPath, testFileName), testFileName+\".rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(path.Join(vdirCryptPath, testFileName), testFileName+\".rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t// rename on local fs or on the same folder must work\n\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirLocalPath, testFileName), path.Join(vdirLocalPath, testFileName+\"_rename\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirCryptPath, testFileName), path.Join(vdirCryptPath, testFileName+\"_rename\"))\n\t\tassert.NoError(t, err)\n\t\t// renaming a virtual folder is not allowed\n\t\terr = client.Rename(vdirLocalPath, vdirLocalPath+\"_rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Rename(vdirCryptPath, vdirCryptPath+\"_rename\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t// renaming root folder is not allowed\n\t\terr = client.Rename(\"/\", \"new_name\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t// renaming a path to a virtual folder is not allowed\n\t\terr = client.Rename(\"/vdir\", \"new_vdir\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameLocal}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathCrypt)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathLocal)\n\tassert.NoError(t, err)\n}\n\nfunc TestCopyAndRemoveSSHCommands(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tfileSize := int64(32)\n\t\terr = writeSFTPFile(testFileName, fileSize, client)\n\t\tassert.NoError(t, err)\n\n\t\ttestFileNameCopy := testFileName + \"_copy\"\n\t\tout, err := runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", testFileName, testFileNameCopy), user)\n\t\tassert.NoError(t, err, string(out))\n\t\t// the resolved destination path match the source path\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", testFileName, path.Dir(testFileName)), user)\n\t\tassert.Error(t, err, string(out))\n\n\t\tinfo, err := client.Stat(testFileNameCopy)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, fileSize, info.Size())\n\t\t}\n\n\t\ttestDir := \"test dir\"\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s '%s'`, testFileName, testDir), user)\n\t\tassert.NoError(t, err, string(out))\n\t\tinfo, err = client.Stat(path.Join(testDir, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, fileSize, info.Size())\n\t\t}\n\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3*fileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %s\", testFileNameCopy), user)\n\t\tassert.NoError(t, err, string(out))\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-remove '%s'`, testDir), user)\n\t\tassert.NoError(t, err, string(out))\n\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\n\t\t_, err = client.Stat(testFileNameCopy)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t// create a dir tree\n\t\tdir1 := \"dir1\"\n\t\tdir2 := \"dir 2\"\n\t\terr = client.MkdirAll(path.Join(dir1, dir2))\n\t\tassert.NoError(t, err)\n\t\ttoCreate := []string{\n\t\t\tpath.Join(dir1, testFileName),\n\t\t\tpath.Join(dir1, dir2, testFileName),\n\t\t}\n\t\tfor _, p := range toCreate {\n\t\t\terr = writeSFTPFile(p, fileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\t// create a symlink, copying a symlink is not supported\n\t\terr = client.Symlink(path.Join(\"/\", dir1, testFileName), path.Join(\"/\", dir1, testFileName+\"_link\"))\n\t\tassert.NoError(t, err)\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", path.Join(\"/\", dir1, testFileName+\"_link\"),\n\t\t\tpath.Join(\"/\", testFileName+\"_link\")), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// copying a dir inside itself should fail\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", path.Join(\"/\", dir1),\n\t\t\tpath.Join(\"/\", dir1, \"sub\")), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// copy source and dest must differ\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", path.Join(\"/\", dir1),\n\t\t\tpath.Join(\"/\", dir1)), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// copy a missing file/dir should fail\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", path.Join(\"/\", \"missing_entry\"),\n\t\t\tpath.Join(\"/\", dir1)), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// try to overwrite a file with a dir\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", path.Join(\"/\", dir1), testFileName), user)\n\t\tassert.Error(t, err, string(out))\n\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s \"%s\"`, dir1, dir2), user)\n\t\tassert.NoError(t, err, string(out))\n\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 5*fileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 5, user.UsedQuotaFiles)\n\n\t\t// copy again, quota must remain unchanged\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s/ \"%s\"`, dir1, dir2), user)\n\t\tassert.NoError(t, err, string(out))\n\t\t_, err = client.Stat(dir2)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 5*fileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 5, user.UsedQuotaFiles)\n\t\t// now copy inside target\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s \"%s\"`, dir1, dir2), user)\n\t\tassert.NoError(t, err, string(out))\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 7*fileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 7, user.UsedQuotaFiles)\n\n\t\tfor _, p := range []string{dir1, dir2} {\n\t\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-remove \"%s\"`, p), user)\n\t\t\tassert.NoError(t, err, string(out))\n\t\t\t_, err = client.Stat(p)\n\t\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\t}\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t// test quota errors\n\t\tuser.QuotaFiles = 1\n\t\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\t// quota files exceeded\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", testFileName, testFileNameCopy), user)\n\t\tassert.Error(t, err, string(out))\n\t\tuser.QuotaFiles = 1000\n\t\tuser.QuotaSize = fileSize + 1\n\t\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\t// quota size exceeded after the copy\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", testFileName, testFileNameCopy), user)\n\t\tassert.Error(t, err, string(out))\n\t\tuser.QuotaSize = fileSize - 1\n\t\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\t// quota size exceeded\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", testFileName, testFileNameCopy), user)\n\t\tassert.Error(t, err, string(out))\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCopyAndRemovePermissions(t *testing.T) {\n\tu := getTestUser()\n\trestrictedPath := \"/dir/path\"\n\tpatternFilterPath := \"/patterns\"\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           patternFilterPath,\n\t\t\tDeniedPatterns: []string{\"*.dat\"},\n\t\t},\n\t}\n\tu.Permissions[restrictedPath] = []string{}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.MkdirAll(restrictedPath)\n\t\tassert.NoError(t, err)\n\t\terr = client.MkdirAll(patternFilterPath)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName, 100, client)\n\t\tassert.NoError(t, err)\n\t\t// getting file writer will fail\n\t\tout, err := runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// file pattern not allowed\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, patternFilterPath), user)\n\t\tassert.Error(t, err, string(out))\n\n\t\ttestDir := path.Join(\"/\", path.Base(restrictedPath))\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, testFileName), 100, client)\n\t\tassert.NoError(t, err)\n\t\t// creating target dir will fail\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s/`, testDir, restrictedPath), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// get dir contents will fail\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s /`, restrictedPath), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// get dir contents will fail\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-remove %s`, restrictedPath), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// give list dir permissions and retry, now delete will fail\n\t\tuser.Permissions[restrictedPath] = []string{dataprovider.PermListItems, dataprovider.PermUpload}\n\t\tuser.Permissions[testDir] = []string{dataprovider.PermListItems}\n\t\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\t// no copy permission\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user)\n\t\tassert.Error(t, err, string(out))\n\t\tuser.Permissions[restrictedPath] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermCopy}\n\t\tuser.Permissions[testDir] = []string{dataprovider.PermListItems, dataprovider.PermCopy}\n\t\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user)\n\t\tassert.NoError(t, err, string(out))\n\t\t// overwrite will fail, no permission\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, testFileName, restrictedPath), user)\n\t\tassert.Error(t, err, string(out))\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-remove %s`, restrictedPath), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// try to copy a file from testDir, we have only list permissions so getFileReader will fail\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, path.Join(testDir, testFileName), testFileName+\".copy\"), user)\n\t\tassert.Error(t, err, string(out))\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCrossFoldersCopy(t *testing.T) {\n\tbaseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tu := getTestUser()\n\tu.Username += \"_1\"\n\tu.HomeDir = filepath.Join(os.TempDir(), u.Username)\n\tu.QuotaFiles = 1000\n\tmappedPath1 := filepath.Join(os.TempDir(), \"mapped1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvpath1 := \"/vdirs/vdir1\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vpath1,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tmappedPath2 := filepath.Join(os.TempDir(), \"mapped1\", \"dir\", \"mapped2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvpath2 := \"/vdirs/vdir2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vpath2,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tmappedPath3 := filepath.Join(os.TempDir(), \"mapped3\")\n\tfolderName3 := filepath.Base(mappedPath3)\n\tvpath3 := \"/vdirs/vdir3\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName3,\n\t\t},\n\t\tVirtualPath: vpath3,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tmappedPath4 := filepath.Join(os.TempDir(), \"mapped4\")\n\tfolderName4 := filepath.Base(mappedPath4)\n\tvpath4 := \"/vdirs/vdir4\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName4,\n\t\t},\n\t\tVirtualPath: vpath4,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folderName3,\n\t\tMappedPath: mappedPath3,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf4 := vfs.BaseVirtualFolder{\n\t\tName:       folderName4,\n\t\tMappedPath: mappedPath4,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\tUsername: baseUser.Username,\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f4, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tconn, client, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tbaseFileSize := int64(100)\n\t\terr = writeSFTPFile(path.Join(vpath1, testFileName), baseFileSize+1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vpath2, testFileName), baseFileSize+2, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vpath3, testFileName), baseFileSize+3, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vpath4, testFileName), baseFileSize+4, client)\n\t\tassert.NoError(t, err)\n\t\t// cannot remove a directory with virtual folders inside\n\t\tout, err := runSSHCommand(fmt.Sprintf(`sftpgo-remove %s`, path.Dir(vpath1)), user)\n\t\tassert.Error(t, err, string(out))\n\t\t// copy across virtual folders\n\t\tcopyDir := \"/copy\"\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s/`, path.Dir(vpath1), copyDir), user)\n\t\tassert.NoError(t, err, string(out))\n\t\t// check the copy\n\t\tinfo, err := client.Stat(path.Join(copyDir, vpath1, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, baseFileSize+1, info.Size())\n\t\t}\n\t\tinfo, err = client.Stat(path.Join(copyDir, vpath2, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, baseFileSize+2, info.Size())\n\t\t}\n\t\tinfo, err = client.Stat(path.Join(copyDir, vpath3, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, baseFileSize+3, info.Size())\n\t\t}\n\t\tinfo, err = client.Stat(path.Join(copyDir, vpath4, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, baseFileSize+4, info.Size())\n\t\t}\n\t\t// nested fs paths\n\t\tout, err = runSSHCommand(fmt.Sprintf(`sftpgo-copy %s %s`, vpath1, vpath2), user)\n\t\tassert.Error(t, err, string(out))\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tfor _, folderName := range []string{folderName1, folderName2, folderName3, folderName4} {\n\t\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(filepath.Join(os.TempDir(), folderName))\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestHTTPFs(t *testing.T) {\n\tu := getTestUserWithHTTPFs()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\terr = os.MkdirAll(user.GetHomeDir(), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tconn := common.NewBaseConnection(xid.New().String(), common.ProtocolFTP, \"\", \"\", user)\n\terr = conn.CreateDir(httpFsWellKnowDir, false)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(os.TempDir(), \"httpfs\", defaultHTTPFsUsername, httpFsWellKnowDir, \"file.txt\"), []byte(\"data\"), 0666)\n\tassert.NoError(t, err)\n\n\terr = conn.Copy(httpFsWellKnowDir, httpFsWellKnowDir+\"_copy\")\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestProxyProtocol(t *testing.T) {\n\tresp, err := httpclient.Get(fmt.Sprintf(\"http://%v\", httpProxyAddr))\n\tif !assert.Error(t, err) {\n\t\tresp.Body.Close()\n\t}\n}\n\nfunc TestSetProtocol(t *testing.T) {\n\tconn := common.NewBaseConnection(\"id\", \"sshd_exec\", \"\", \"\", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})\n\tconn.SetProtocol(common.ProtocolSCP)\n\trequire.Equal(t, \"SCP_id\", conn.GetID())\n}\n\nfunc TestGetFsError(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"test\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(\"invalid JSON for credentials\")\n\tconn := common.NewBaseConnection(\"\", common.ProtocolFTP, \"\", \"\", u)\n\t_, _, err := conn.GetFsAndResolvedPath(\"/vpath\")\n\tassert.Error(t, err)\n}\n\nfunc waitTCPListening(address string) {\n\tfor {\n\t\tconn, err := net.Dial(\"tcp\", address)\n\t\tif err != nil {\n\t\t\tlogger.WarnToConsole(\"tcp server %v not listening: %v\", address, err)\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\t\tlogger.InfoToConsole(\"tcp server %v now listening\", address)\n\t\tconn.Close()\n\t\tbreak\n\t}\n}\n\nfunc checkBasicSFTP(client *sftp.Client) error {\n\t_, err := client.Getwd()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = client.ReadDir(\".\")\n\treturn err\n}\n\nfunc getCustomAuthSftpClient(user dataprovider.User, authMethods []ssh.AuthMethod) (*ssh.Client, *sftp.Client, error) {\n\tvar sftpClient *sftp.Client\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tAuth:            authMethods,\n\t\tTimeout:         5 * time.Second,\n\t}\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif err != nil {\n\t\treturn conn, sftpClient, err\n\t}\n\tsftpClient, err = sftp.NewClient(conn)\n\tif err != nil {\n\t\tconn.Close()\n\t}\n\treturn conn, sftpClient, err\n}\n\nfunc getSftpClient(user dataprovider.User) (*ssh.Client, *sftp.Client, error) {\n\tvar sftpClient *sftp.Client\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         5 * time.Second,\n\t}\n\tif user.Password != \"\" {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}\n\t} else {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}\n\t}\n\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif err != nil {\n\t\treturn conn, sftpClient, err\n\t}\n\tsftpClient, err = sftp.NewClient(conn)\n\tif err != nil {\n\t\tconn.Close()\n\t}\n\treturn conn, sftpClient, err\n}\n\nfunc runSSHCommand(command string, user dataprovider.User) ([]byte, error) {\n\tvar sshSession *ssh.Session\n\tvar output []byte\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         5 * time.Second,\n\t}\n\tif user.Password != \"\" {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}\n\t} else {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}\n\t}\n\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif err != nil {\n\t\treturn output, err\n\t}\n\tdefer conn.Close()\n\tsshSession, err = conn.NewSession()\n\tif err != nil {\n\t\treturn output, err\n\t}\n\tvar stdout, stderr bytes.Buffer\n\tsshSession.Stdout = &stdout\n\tsshSession.Stderr = &stderr\n\terr = sshSession.Run(command)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run command %v: %v\", command, stderr.Bytes())\n\t}\n\treturn stdout.Bytes(), err\n}\n\nfunc getWebDavClient(user dataprovider.User) *gowebdav.Client {\n\trootPath := fmt.Sprintf(\"http://localhost:%d/\", webDavServerPort)\n\tpwd := defaultPassword\n\tif user.Password != \"\" {\n\t\tpwd = user.Password\n\t}\n\tclient := gowebdav.NewClient(rootPath, user.Username, pwd)\n\tclient.SetTimeout(10 * time.Second)\n\treturn client\n}\n\nfunc getTestUser() dataprovider.User {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:       defaultUsername,\n\t\t\tPassword:       defaultPassword,\n\t\t\tHomeDir:        filepath.Join(homeBasePath, defaultUsername),\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: 0,\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = allPerms\n\treturn user\n}\n\nfunc getTestSFTPUser() dataprovider.User {\n\tu := getTestUser()\n\tu.Username = defaultSFTPUsername\n\tu.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tu.FsConfig.SFTPConfig.Endpoint = sftpServerAddr\n\tu.FsConfig.SFTPConfig.Username = defaultUsername\n\tu.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\treturn u\n}\n\nfunc getCryptFsUser() dataprovider.User {\n\tu := getTestUser()\n\tu.Username += \"_crypt\"\n\tu.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tu.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(defaultPassword)\n\treturn u\n}\n\nfunc getTestUserWithHTTPFs() dataprovider.User {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\tu.FsConfig.HTTPConfig = vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint: fmt.Sprintf(\"http://127.0.0.1:%d/api/v1\", httpFsPort),\n\t\t\tUsername: defaultHTTPFsUsername,\n\t\t},\n\t}\n\treturn u\n}\n\nfunc writeSFTPFile(name string, size int64, client *sftp.Client) error {\n\terr := writeSFTPFileNoCheck(name, size, client)\n\tif err != nil {\n\t\treturn err\n\t}\n\tinfo, err := client.Stat(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif info.Size() != size {\n\t\treturn fmt.Errorf(\"file size mismatch, wanted %v, actual %v\", size, info.Size())\n\t}\n\treturn nil\n}\n\nfunc writeSFTPFileNoCheck(name string, size int64, client *sftp.Client) error {\n\tcontent := make([]byte, size)\n\t_, err := rand.Read(content)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf, err := client.Create(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = io.Copy(f, bytes.NewBuffer(content))\n\tif err != nil {\n\t\tf.Close()\n\t\treturn err\n\t}\n\treturn f.Close()\n}\n\nfunc getUploadScriptEnvContent(envVar string) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tcontent = append(content, []byte(fmt.Sprintf(\"if [ -z \\\"$%s\\\" ]\\n\", envVar))...)\n\tcontent = append(content, []byte(\"then\\n\")...)\n\tcontent = append(content, []byte(\"    exit 1\\n\")...)\n\tcontent = append(content, []byte(\"else\\n\")...)\n\tcontent = append(content, []byte(\"    exit 0\\n\")...)\n\tcontent = append(content, []byte(\"fi\\n\")...)\n\treturn content\n}\n\nfunc getUploadScriptContent(movedPath, logFilePath string, exitStatus int) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tcontent = append(content, []byte(\"sleep 1\\n\")...)\n\tif logFilePath != \"\" {\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"echo $@ > %v\\n\", logFilePath))...)\n\t}\n\tcontent = append(content, []byte(fmt.Sprintf(\"mv ${SFTPGO_ACTION_PATH} %v\\n\", movedPath))...)\n\tcontent = append(content, []byte(fmt.Sprintf(\"exit %d\", exitStatus))...)\n\treturn content\n}\n\nfunc getSaveProviderObjectScriptContent(outFilePath string, exitStatus int) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tcontent = append(content, []byte(fmt.Sprintf(\"echo ${SFTPGO_OBJECT_DATA} > %v\\n\", outFilePath))...)\n\tcontent = append(content, []byte(fmt.Sprintf(\"exit %d\", exitStatus))...)\n\treturn content\n}\n\nfunc generateTOTPPasscode(secret string, algo otp.Algorithm) (string, error) {\n\treturn totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{\n\t\tPeriod:    30,\n\t\tSkew:      1,\n\t\tDigits:    otp.DigitsSix,\n\t\tAlgorithm: algo,\n\t})\n}\n\nfunc isDbDefenderSupported() bool {\n\t// SQLite shares the implementation with other SQL-based provider but it makes no sense\n\t// to use it outside test cases\n\tswitch dataprovider.GetProviderStatus().Driver {\n\tcase dataprovider.MySQLDataProviderName, dataprovider.PGSQLDataProviderName,\n\t\tdataprovider.CockroachDataProviderName, dataprovider.SQLiteDataProviderName:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc getEncryptedFileSize(size int64) (int64, error) {\n\tencSize, err := sio.EncryptedSize(uint64(size))\n\treturn int64(encSize) + 33, err\n}\n\nfunc printLatestLogs(maxNumberOfLines int) {\n\tvar lines []string\n\tf, err := os.Open(logFilePath)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer f.Close()\n\tscanner := bufio.NewScanner(f)\n\tfor scanner.Scan() {\n\t\tlines = append(lines, scanner.Text()+\"\\r\\n\")\n\t\tfor len(lines) > maxNumberOfLines {\n\t\t\tlines = lines[1:]\n\t\t}\n\t}\n\tif scanner.Err() != nil {\n\t\tlogger.WarnToConsole(\"Unable to print latest logs: %v\", scanner.Err())\n\t\treturn\n\t}\n\tfor _, line := range lines {\n\t\tlogger.DebugToConsole(\"%s\", line)\n\t}\n}\n\ntype receivedEmail struct {\n\tsync.RWMutex\n\tFrom string\n\tTo   []string\n\tData string\n}\n\nfunc (e *receivedEmail) set(from string, to []string, data []byte) {\n\te.Lock()\n\tdefer e.Unlock()\n\n\te.From = from\n\te.To = to\n\te.Data = strings.ReplaceAll(string(data), \"=\\r\\n\", \"\")\n}\n\nfunc (e *receivedEmail) reset() {\n\te.Lock()\n\tdefer e.Unlock()\n\n\te.From = \"\"\n\te.To = nil\n\te.Data = \"\"\n}\n\nfunc (e *receivedEmail) get() receivedEmail {\n\te.RLock()\n\tdefer e.RUnlock()\n\n\treturn receivedEmail{\n\t\tFrom: e.From,\n\t\tTo:   e.To,\n\t\tData: e.Data,\n\t}\n}\n\nfunc startHTTPFs() {\n\tgo func() {\n\t\treaddirCallback := func(name string) []os.FileInfo {\n\t\t\tif name == httpFsWellKnowDir {\n\t\t\t\treturn []os.FileInfo{vfs.NewFileInfo(\"ghost.txt\", false, 0, time.Unix(0, 0), false)}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tcallbacks := &httpdtest.HTTPFsCallbacks{\n\t\t\tReaddir: readdirCallback,\n\t\t}\n\t\tif err := httpdtest.StartTestHTTPFs(httpFsPort, callbacks); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTPfs test server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\twaitTCPListening(fmt.Sprintf(\":%d\", httpFsPort))\n}\n"
  },
  {
    "path": "internal/common/ratelimiter.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"sort\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"golang.org/x/time/rate\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\terrNoBucket               = errors.New(\"no bucket found\")\n\terrReserve                = errors.New(\"unable to reserve token\")\n\trateLimiterProtocolValues = []string{ProtocolSSH, ProtocolFTP, ProtocolWebDAV, ProtocolHTTP}\n)\n\n// RateLimiterType defines the supported rate limiters types\ntype RateLimiterType int\n\n// Supported rate limiter types\nconst (\n\trateLimiterTypeGlobal RateLimiterType = iota + 1\n\trateLimiterTypeSource\n)\n\n// RateLimiterConfig defines the configuration for a rate limiter\ntype RateLimiterConfig struct {\n\t// Average defines the maximum rate allowed. 0 means disabled\n\tAverage int64 `json:\"average\" mapstructure:\"average\"`\n\t// Period defines the period as milliseconds. Default: 1000 (1 second).\n\t// The rate is actually defined by dividing average by period.\n\t// So for a rate below 1 req/s, one needs to define a period larger than a second.\n\tPeriod int64 `json:\"period\" mapstructure:\"period\"`\n\t// Burst is the maximum number of requests allowed to go through in the\n\t// same arbitrarily small period of time. Default: 1.\n\tBurst int `json:\"burst\" mapstructure:\"burst\"`\n\t// Type defines the rate limiter type:\n\t// - rateLimiterTypeGlobal is a global rate limiter independent from the source\n\t// - rateLimiterTypeSource is a per-source rate limiter\n\tType int `json:\"type\" mapstructure:\"type\"`\n\t// Protocols defines the protocols for this rate limiter.\n\t// Available protocols are: \"SFTP\", \"FTP\", \"DAV\".\n\t// A rate limiter with no protocols defined is disabled\n\tProtocols []string `json:\"protocols\" mapstructure:\"protocols\"`\n\t// If the rate limit is exceeded, the defender is enabled, and this is a per-source limiter,\n\t// a new defender event will be generated\n\tGenerateDefenderEvents bool `json:\"generate_defender_events\" mapstructure:\"generate_defender_events\"`\n\t// The number of per-ip rate limiters kept in memory will vary between the\n\t// soft and hard limit\n\tEntriesSoftLimit int `json:\"entries_soft_limit\" mapstructure:\"entries_soft_limit\"`\n\tEntriesHardLimit int `json:\"entries_hard_limit\" mapstructure:\"entries_hard_limit\"`\n}\n\nfunc (r *RateLimiterConfig) isEnabled() bool {\n\treturn r.Average > 0 && len(r.Protocols) > 0\n}\n\nfunc (r *RateLimiterConfig) validate() error {\n\tif r.Burst < 1 {\n\t\treturn fmt.Errorf(\"invalid burst %v. It must be >= 1\", r.Burst)\n\t}\n\tif r.Period < 100 {\n\t\treturn fmt.Errorf(\"invalid period %v. It must be >= 100\", r.Period)\n\t}\n\tif r.Type != int(rateLimiterTypeGlobal) && r.Type != int(rateLimiterTypeSource) {\n\t\treturn fmt.Errorf(\"invalid type %v\", r.Type)\n\t}\n\tif r.Type != int(rateLimiterTypeGlobal) {\n\t\tif r.EntriesSoftLimit <= 0 {\n\t\t\treturn fmt.Errorf(\"invalid entries_soft_limit %v\", r.EntriesSoftLimit)\n\t\t}\n\t\tif r.EntriesHardLimit <= r.EntriesSoftLimit {\n\t\t\treturn fmt.Errorf(\"invalid entries_hard_limit %v must be > %v\", r.EntriesHardLimit, r.EntriesSoftLimit)\n\t\t}\n\t}\n\tr.Protocols = util.RemoveDuplicates(r.Protocols, true)\n\tfor _, protocol := range r.Protocols {\n\t\tif !slices.Contains(rateLimiterProtocolValues, protocol) {\n\t\t\treturn fmt.Errorf(\"invalid protocol %q\", protocol)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *RateLimiterConfig) getLimiter() *rateLimiter {\n\tlimiter := &rateLimiter{\n\t\tburst:                  r.Burst,\n\t\tglobalBucket:           nil,\n\t\tgenerateDefenderEvents: r.GenerateDefenderEvents,\n\t}\n\tvar maxDelay time.Duration\n\tperiod := time.Duration(r.Period) * time.Millisecond\n\trtl := float64(r.Average*int64(time.Second)) / float64(period)\n\tlimiter.rate = rate.Limit(rtl)\n\tif rtl < 1 {\n\t\tmaxDelay = period / 2\n\t} else {\n\t\tmaxDelay = time.Second / (time.Duration(rtl) * 2)\n\t}\n\tif maxDelay > 10*time.Second {\n\t\tmaxDelay = 10 * time.Second\n\t}\n\tlimiter.maxDelay = maxDelay\n\tlimiter.buckets = sourceBuckets{\n\t\tbuckets:   make(map[string]sourceRateLimiter),\n\t\thardLimit: r.EntriesHardLimit,\n\t\tsoftLimit: r.EntriesSoftLimit,\n\t}\n\tif r.Type != int(rateLimiterTypeSource) {\n\t\tlimiter.globalBucket = rate.NewLimiter(limiter.rate, limiter.burst)\n\t}\n\treturn limiter\n}\n\n// RateLimiter defines a rate limiter\ntype rateLimiter struct {\n\trate                   rate.Limit\n\tburst                  int\n\tmaxDelay               time.Duration\n\tglobalBucket           *rate.Limiter\n\tbuckets                sourceBuckets\n\tgenerateDefenderEvents bool\n}\n\n// Wait blocks until the limit allows one event to happen\n// or returns an error if the time to wait exceeds the max\n// allowed delay\nfunc (rl *rateLimiter) Wait(source, protocol string) (time.Duration, error) {\n\tvar res *rate.Reservation\n\tif rl.globalBucket != nil {\n\t\tres = rl.globalBucket.Reserve()\n\t} else {\n\t\tvar err error\n\t\tres, err = rl.buckets.reserve(source)\n\t\tif err != nil {\n\t\t\trateLimiter := rate.NewLimiter(rl.rate, rl.burst)\n\t\t\tres = rl.buckets.addAndReserve(rateLimiter, source)\n\t\t}\n\t}\n\tif !res.OK() {\n\t\treturn 0, errReserve\n\t}\n\tdelay := res.Delay()\n\tif delay > rl.maxDelay {\n\t\tres.Cancel()\n\t\tif rl.generateDefenderEvents && rl.globalBucket == nil {\n\t\t\tAddDefenderEvent(source, protocol, HostEventLimitExceeded)\n\t\t}\n\t\treturn delay, fmt.Errorf(\"rate limit exceed, wait time to respect rate %v, max wait time allowed %v\", delay, rl.maxDelay)\n\t}\n\ttime.Sleep(delay)\n\treturn 0, nil\n}\n\ntype sourceRateLimiter struct {\n\tlastActivity *atomic.Int64\n\tbucket       *rate.Limiter\n}\n\nfunc (s *sourceRateLimiter) updateLastActivity() {\n\ts.lastActivity.Store(time.Now().UnixNano())\n}\n\nfunc (s *sourceRateLimiter) getLastActivity() int64 {\n\treturn s.lastActivity.Load()\n}\n\ntype sourceBuckets struct {\n\tsync.RWMutex\n\tbuckets   map[string]sourceRateLimiter\n\thardLimit int\n\tsoftLimit int\n}\n\nfunc (b *sourceBuckets) reserve(source string) (*rate.Reservation, error) {\n\tb.RLock()\n\tdefer b.RUnlock()\n\n\tif src, ok := b.buckets[source]; ok {\n\t\tsrc.updateLastActivity()\n\t\treturn src.bucket.Reserve(), nil\n\t}\n\n\treturn nil, errNoBucket\n}\n\nfunc (b *sourceBuckets) addAndReserve(r *rate.Limiter, source string) *rate.Reservation {\n\tb.Lock()\n\tdefer b.Unlock()\n\n\tb.cleanup()\n\n\tsrc := sourceRateLimiter{\n\t\tlastActivity: new(atomic.Int64),\n\t\tbucket:       r,\n\t}\n\tsrc.updateLastActivity()\n\tb.buckets[source] = src\n\treturn src.bucket.Reserve()\n}\n\nfunc (b *sourceBuckets) cleanup() {\n\tif len(b.buckets) >= b.hardLimit {\n\t\tnumToRemove := len(b.buckets) - b.softLimit\n\n\t\tkvList := make(kvList, 0, len(b.buckets))\n\n\t\tfor k, v := range b.buckets {\n\t\t\tkvList = append(kvList, kv{\n\t\t\t\tKey:   k,\n\t\t\t\tValue: v.getLastActivity(),\n\t\t\t})\n\t\t}\n\n\t\tsort.Sort(kvList)\n\n\t\tfor idx, kv := range kvList {\n\t\t\tif idx >= numToRemove {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tdelete(b.buckets, kv.Key)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/common/ratelimiter_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRateLimiterConfig(t *testing.T) {\n\tconfig := RateLimiterConfig{}\n\terr := config.validate()\n\trequire.Error(t, err)\n\tconfig.Burst = 1\n\tconfig.Period = 10\n\terr = config.validate()\n\trequire.Error(t, err)\n\tconfig.Period = 1000\n\tconfig.Type = 100\n\terr = config.validate()\n\trequire.Error(t, err)\n\tconfig.Type = int(rateLimiterTypeSource)\n\tconfig.EntriesSoftLimit = 0\n\terr = config.validate()\n\trequire.Error(t, err)\n\tconfig.EntriesSoftLimit = 150\n\tconfig.EntriesHardLimit = 0\n\terr = config.validate()\n\trequire.Error(t, err)\n\tconfig.EntriesHardLimit = 200\n\tconfig.Protocols = []string{\"unsupported protocol\"}\n\terr = config.validate()\n\trequire.Error(t, err)\n\tconfig.Protocols = rateLimiterProtocolValues\n\terr = config.validate()\n\trequire.NoError(t, err)\n\n\tlimiter := config.getLimiter()\n\trequire.Equal(t, 500*time.Millisecond, limiter.maxDelay)\n\trequire.Nil(t, limiter.globalBucket)\n\tconfig.Type = int(rateLimiterTypeGlobal)\n\tconfig.Average = 1\n\tconfig.Period = 10000\n\tlimiter = config.getLimiter()\n\trequire.Equal(t, 5*time.Second, limiter.maxDelay)\n\trequire.NotNil(t, limiter.globalBucket)\n\tconfig.Period = 100000\n\tlimiter = config.getLimiter()\n\trequire.Equal(t, 10*time.Second, limiter.maxDelay)\n\tconfig.Period = 500\n\tconfig.Average = 1\n\tlimiter = config.getLimiter()\n\trequire.Equal(t, 250*time.Millisecond, limiter.maxDelay)\n}\n\nfunc TestRateLimiter(t *testing.T) {\n\tconfig := RateLimiterConfig{\n\t\tAverage:   1,\n\t\tPeriod:    1000,\n\t\tBurst:     1,\n\t\tType:      int(rateLimiterTypeGlobal),\n\t\tProtocols: rateLimiterProtocolValues,\n\t}\n\tlimiter := config.getLimiter()\n\t_, err := limiter.Wait(\"\", ProtocolFTP)\n\trequire.NoError(t, err)\n\t_, err = limiter.Wait(\"\", ProtocolSSH)\n\trequire.Error(t, err)\n\n\tconfig.Type = int(rateLimiterTypeSource)\n\tconfig.GenerateDefenderEvents = true\n\tconfig.EntriesSoftLimit = 5\n\tconfig.EntriesHardLimit = 10\n\tlimiter = config.getLimiter()\n\n\tsource := \"192.168.1.2\"\n\t_, err = limiter.Wait(source, ProtocolSSH)\n\trequire.NoError(t, err)\n\t_, err = limiter.Wait(source, ProtocolSSH)\n\trequire.Error(t, err)\n\t// a different source should work\n\t_, err = limiter.Wait(source+\"1\", ProtocolSSH)\n\trequire.NoError(t, err)\n\n\tconfig.Burst = 0\n\tlimiter = config.getLimiter()\n\t_, err = limiter.Wait(source, ProtocolSSH)\n\trequire.ErrorIs(t, err, errReserve)\n}\n\nfunc TestLimiterCleanup(t *testing.T) {\n\tconfig := RateLimiterConfig{\n\t\tAverage:          100,\n\t\tPeriod:           1000,\n\t\tBurst:            1,\n\t\tType:             int(rateLimiterTypeSource),\n\t\tProtocols:        rateLimiterProtocolValues,\n\t\tEntriesSoftLimit: 1,\n\t\tEntriesHardLimit: 3,\n\t}\n\tlimiter := config.getLimiter()\n\tsource1 := \"10.8.0.1\"\n\tsource2 := \"10.8.0.2\"\n\tsource3 := \"10.8.0.3\"\n\tsource4 := \"10.8.0.4\"\n\t_, err := limiter.Wait(source1, ProtocolSSH)\n\tassert.NoError(t, err)\n\ttime.Sleep(20 * time.Millisecond)\n\t_, err = limiter.Wait(source2, ProtocolSSH)\n\tassert.NoError(t, err)\n\ttime.Sleep(20 * time.Millisecond)\n\tassert.Len(t, limiter.buckets.buckets, 2)\n\t_, ok := limiter.buckets.buckets[source1]\n\tassert.True(t, ok)\n\t_, ok = limiter.buckets.buckets[source2]\n\tassert.True(t, ok)\n\t_, err = limiter.Wait(source3, ProtocolSSH)\n\tassert.NoError(t, err)\n\tassert.Len(t, limiter.buckets.buckets, 3)\n\t_, ok = limiter.buckets.buckets[source1]\n\tassert.True(t, ok)\n\t_, ok = limiter.buckets.buckets[source2]\n\tassert.True(t, ok)\n\t_, ok = limiter.buckets.buckets[source3]\n\tassert.True(t, ok)\n\ttime.Sleep(20 * time.Millisecond)\n\t_, err = limiter.Wait(source4, ProtocolSSH)\n\tassert.NoError(t, err)\n\tassert.Len(t, limiter.buckets.buckets, 2)\n\t_, ok = limiter.buckets.buckets[source3]\n\tassert.True(t, ok)\n\t_, ok = limiter.buckets.buckets[source4]\n\tassert.True(t, ok)\n}\n"
  },
  {
    "path": "internal/common/tlsutils.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\t// DefaultTLSKeyPaidID defines the id to use for non-binding specific key pairs\n\tDefaultTLSKeyPaidID = \"default\"\n\tpemCRLType          = \"X509 CRL\"\n)\n\nvar (\n\tpemCRLPrefix = []byte(\"-----BEGIN X509 CRL\")\n)\n\n// TLSKeyPair defines the paths and the unique identifier for a TLS key pair\ntype TLSKeyPair struct {\n\tCert string\n\tKey  string\n\tID   string\n}\n\n// CertManager defines a TLS certificate manager\ntype CertManager struct {\n\tkeyPairs  []TLSKeyPair\n\tconfigDir string\n\tlogSender string\n\tsync.RWMutex\n\tcaCertificates    []string\n\tcaRevocationLists []string\n\tmonitorList       []string\n\tcerts             map[string]*tls.Certificate\n\tcertsInfo         map[string]fs.FileInfo\n\trootCAs           *x509.CertPool\n\tcrls              []*x509.RevocationList\n}\n\n// Reload tries to reload certificate and CRLs\nfunc (m *CertManager) Reload() error {\n\terrCrt := m.loadCertificates()\n\terrCRLs := m.LoadCRLs()\n\n\tif errCrt != nil {\n\t\treturn errCrt\n\t}\n\treturn errCRLs\n}\n\n// LoadCertificates tries to load the configured x509 key pairs\nfunc (m *CertManager) loadCertificates() error {\n\tif len(m.keyPairs) == 0 {\n\t\treturn errors.New(\"no key pairs defined\")\n\t}\n\tcerts := make(map[string]*tls.Certificate)\n\tfor _, keyPair := range m.keyPairs {\n\t\tif keyPair.ID == \"\" {\n\t\t\treturn errors.New(\"TLS certificate without ID\")\n\t\t}\n\t\tnewCert, err := tls.LoadX509KeyPair(keyPair.Cert, keyPair.Key)\n\t\tif err != nil {\n\t\t\tlogger.Error(m.logSender, \"\", \"unable to load X509 key pair, cert file %q key file %q error: %v\",\n\t\t\t\tkeyPair.Cert, keyPair.Key, err)\n\t\t\treturn err\n\t\t}\n\t\tif _, ok := certs[keyPair.ID]; ok {\n\t\t\tlogger.Error(m.logSender, \"\", \"TLS certificate with id %q is duplicated\", keyPair.ID)\n\t\t\treturn fmt.Errorf(\"TLS certificate with id %q is duplicated\", keyPair.ID)\n\t\t}\n\t\tlogger.Debug(m.logSender, \"\", \"TLS certificate %q successfully loaded, id %v\", keyPair.Cert, keyPair.ID)\n\t\tcerts[keyPair.ID] = &newCert\n\t\tif !slices.Contains(m.monitorList, keyPair.Cert) {\n\t\t\tm.monitorList = append(m.monitorList, keyPair.Cert)\n\t\t}\n\t}\n\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tm.certs = certs\n\treturn nil\n}\n\n// HasCertificate returns true if there is a certificate for the specified certID\nfunc (m *CertManager) HasCertificate(certID string) bool {\n\tm.RLock()\n\tdefer m.RUnlock()\n\n\t_, ok := m.certs[certID]\n\treturn ok\n}\n\n// GetCertificateFunc returns the loaded certificate\nfunc (m *CertManager) GetCertificateFunc(certID string) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {\n\treturn func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\t\tm.RLock()\n\t\tdefer m.RUnlock()\n\n\t\tval, ok := m.certs[certID]\n\t\tif !ok {\n\t\t\tlogger.Error(m.logSender, \"\", \"no certificate for id %s\", certID)\n\t\t\treturn nil, fmt.Errorf(\"no certificate for id %s\", certID)\n\t\t}\n\n\t\treturn val, nil\n\t}\n}\n\n// IsRevoked returns true if the specified certificate has been revoked\nfunc (m *CertManager) IsRevoked(crt *x509.Certificate, caCrt *x509.Certificate) bool {\n\tm.RLock()\n\tdefer m.RUnlock()\n\n\tif crt == nil || caCrt == nil {\n\t\tlogger.Error(m.logSender, \"\", \"unable to verify crt %v, ca crt %v\", crt, caCrt)\n\t\treturn len(m.crls) > 0\n\t}\n\n\tfor _, crl := range m.crls {\n\t\tif crl.CheckSignatureFrom(caCrt) == nil {\n\t\t\tfor _, rc := range crl.RevokedCertificateEntries {\n\t\t\t\tif rc.SerialNumber.Cmp(crt.SerialNumber) == 0 {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\n// LoadCRLs tries to load certificate revocation lists from the given paths\nfunc (m *CertManager) LoadCRLs() error {\n\tif len(m.caRevocationLists) == 0 {\n\t\treturn nil\n\t}\n\n\tvar crls []*x509.RevocationList\n\n\tfor _, revocationList := range m.caRevocationLists {\n\t\tif !util.IsFileInputValid(revocationList) {\n\t\t\treturn fmt.Errorf(\"invalid root CA revocation list %q\", revocationList)\n\t\t}\n\t\tif revocationList != \"\" && !filepath.IsAbs(revocationList) {\n\t\t\trevocationList = filepath.Join(m.configDir, revocationList)\n\t\t}\n\t\tcrlBytes, err := os.ReadFile(revocationList)\n\t\tif err != nil {\n\t\t\tlogger.Error(m.logSender, \"\", \"unable to read revocation list %q\", revocationList)\n\t\t\treturn err\n\t\t}\n\t\tif bytes.HasPrefix(crlBytes, pemCRLPrefix) {\n\t\t\tblock, _ := pem.Decode(crlBytes)\n\t\t\tif block != nil && block.Type == pemCRLType {\n\t\t\t\tcrlBytes = block.Bytes\n\t\t\t}\n\t\t}\n\t\tcrl, err := x509.ParseRevocationList(crlBytes)\n\t\tif err != nil {\n\t\t\tlogger.Error(m.logSender, \"\", \"unable to parse revocation list %q\", revocationList)\n\t\t\treturn err\n\t\t}\n\n\t\tlogger.Debug(m.logSender, \"\", \"CRL %q successfully loaded\", revocationList)\n\t\tcrls = append(crls, crl)\n\t\tif !slices.Contains(m.monitorList, revocationList) {\n\t\t\tm.monitorList = append(m.monitorList, revocationList)\n\t\t}\n\t}\n\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tm.crls = crls\n\n\treturn nil\n}\n\n// GetRootCAs returns the set of root certificate authorities that servers\n// use if required to verify a client certificate\nfunc (m *CertManager) GetRootCAs() *x509.CertPool {\n\tm.RLock()\n\tdefer m.RUnlock()\n\n\treturn m.rootCAs\n}\n\n// LoadRootCAs tries to load root CA certificate authorities from the given paths\nfunc (m *CertManager) LoadRootCAs() error {\n\tif len(m.caCertificates) == 0 {\n\t\treturn nil\n\t}\n\n\trootCAs := x509.NewCertPool()\n\n\tfor _, rootCA := range m.caCertificates {\n\t\tif !util.IsFileInputValid(rootCA) {\n\t\t\treturn fmt.Errorf(\"invalid root CA certificate %q\", rootCA)\n\t\t}\n\t\tif rootCA != \"\" && !filepath.IsAbs(rootCA) {\n\t\t\trootCA = filepath.Join(m.configDir, rootCA)\n\t\t}\n\t\tcrt, err := os.ReadFile(rootCA)\n\t\tif err != nil {\n\t\t\tlogger.Error(m.logSender, \"\", \"unable to read root CA from file %q: %v\", rootCA, err)\n\t\t\treturn err\n\t\t}\n\t\tif rootCAs.AppendCertsFromPEM(crt) {\n\t\t\tlogger.Debug(m.logSender, \"\", \"TLS certificate authority %q successfully loaded\", rootCA)\n\t\t} else {\n\t\t\terr := fmt.Errorf(\"unable to load TLS certificate authority %q\", rootCA)\n\t\t\tlogger.Error(m.logSender, \"\", \"%v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tm.rootCAs = rootCAs\n\treturn nil\n}\n\n// SetCACertificates sets the root CA authorities file paths.\n// This should not be changed at runtime\nfunc (m *CertManager) SetCACertificates(caCertificates []string) {\n\tm.caCertificates = util.RemoveDuplicates(caCertificates, true)\n}\n\n// SetCARevocationLists sets the CA revocation lists file paths.\n// This should not be changed at runtime\nfunc (m *CertManager) SetCARevocationLists(caRevocationLists []string) {\n\tm.caRevocationLists = util.RemoveDuplicates(caRevocationLists, true)\n}\n\nfunc (m *CertManager) monitor() {\n\tcertsInfo := make(map[string]fs.FileInfo)\n\n\tfor _, crt := range m.monitorList {\n\t\tinfo, err := os.Stat(crt)\n\t\tif err != nil {\n\t\t\tlogger.Warn(m.logSender, \"\", \"unable to stat certificate to monitor %q: %v\", crt, err)\n\t\t\treturn\n\t\t}\n\t\tcertsInfo[crt] = info\n\t}\n\n\tm.Lock()\n\n\tisChanged := false\n\tfor k, oldInfo := range m.certsInfo {\n\t\tnewInfo, ok := certsInfo[k]\n\t\tif ok {\n\t\t\tif newInfo.Size() != oldInfo.Size() || newInfo.ModTime() != oldInfo.ModTime() {\n\t\t\t\tlogger.Debug(m.logSender, \"\", \"change detected for certificate %q, reload required\", k)\n\t\t\t\tisChanged = true\n\t\t\t}\n\t\t}\n\t}\n\tm.certsInfo = certsInfo\n\n\tm.Unlock()\n\n\tif isChanged {\n\t\tm.Reload() //nolint:errcheck\n\t}\n}\n\n// NewCertManager creates a new certificate manager\nfunc NewCertManager(keyPairs []TLSKeyPair, configDir, logSender string) (*CertManager, error) {\n\tmanager := &CertManager{\n\t\tkeyPairs:  keyPairs,\n\t\tconfigDir: configDir,\n\t\tlogSender: logSender,\n\t\tcerts:     make(map[string]*tls.Certificate),\n\t\tcertsInfo: make(map[string]fs.FileInfo),\n\t}\n\terr := manager.loadCertificates()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trandSecs := rand.Intn(59)\n\tmanager.monitor()\n\tif eventScheduler != nil {\n\t\t_, err = eventScheduler.AddFunc(fmt.Sprintf(\"@every 8h0m%ds\", randSecs), manager.monitor)\n\t}\n\treturn manager, err\n}\n"
  },
  {
    "path": "internal/common/tlsutils_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tserverCert = `-----BEGIN CERTIFICATE-----\nMIIEIjCCAgqgAwIBAgIQfxHX0pnvRtkmtfLklgrcNzANBgkqhkiG9w0BAQsFADAT\nMREwDwYDVQQDEwhDZXJ0QXV0aDAeFw0yMzAxMDMxMDIyMDdaFw0zMzAxMDMxMDMw\nNDVaMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAKbMWjMhyjMnDsq/19J9D44Y13uPSMN26NFOCfjVgV23zcqvI8W1\ncsosYj89gSmIRxpcL2FtX7NjIT4vaqXob/en1lYy8hstacOs2cy2LcVZHfxu/hv3\n6hEKLY28tOD41L1CYZesBt3yV8vGcYIOnnAdIiG52SChnduTafBVE9Pq5P7qJ1gZ\nd4uBYxe8/Za0metKDvMN6FTK+THq56eD830iRwFOdSw3Z4NS/nQNeVW263E4CC4u\nBVxgwIHu6giqEfIoV6oVTY64y8X2YlwqvbVN/OtWNIJBLu+mN2EhR2ygpZdAyc82\n1yrk/X2/Dd3OiKSrrvXL1fOuNGlLNGD+3vUCAwEAAaNxMG8wDgYDVR0PAQH/BAQD\nAgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUabrE\n6ATHRqEf/CDQiNWI+0e/nhIwHwYDVR0jBBgwFoAUKPyWZxHuWgH3MA/996i3V4gd\naYgwDQYJKoZIhvcNAQELBQADggIBAHFtnPXxCCeeGw4RiIai3bavGtyK5qooZUia\nhN8abJp9VJKYthLwF75c0wn8W0ZMTY8z9xgmFK9afWHCBNyK+0KCpd/LdDUfwvIn\n3RwR4HRFjNG+n1UZBA4l1W6X6kCq9/x7YaKLrek9aBHfxwMnoMrOeMUybm6D+B5E\nlSkAyJRq5VHVatM7UGmdux2MXK5IMpzlIBzz1pXddnzF3f9nfS54xt6ilWst9bMi\n6mBxisJmqc51L/Fyb2SoCJoO/6kv+3V5HnRNBcZuVE8G5/Uc+WRnyy9dh996W83b\njNvSJ9UpspqMtKx7DKU4fC/3xYDjRimZvZ3akfIdkf3j5GVWMtVbx+QVSZ8aKBSM\nZx35p8aF0zppTjp2JvBpiQlGIXKfPkmmH4bLpU7Z7qLXFFnp+fs3CjcIng19gGgi\nXQldgHVsl8FtIebxgW6wc5jb2y/fXjgx9c0SKEeeA3Pp6fExH8PdQdyHHmkHKQzO\nozon1tZhQbcjkNz8kXFp3x3X/0i4TsR6vsUigSFHXT7DgusBK8eAiRVOLSpbfIyp\n7Ul/9DjhtYxcZjNI/xNJcECPGazNDdKh4TdLh35pnQHOsRXDWB873rr5xkJIUXbU\nubo+q0VpmF7OtfPO9PrPilWAUhVDRx7CCTW3YUsWrYJkr8d6F/n6y7QPKMtB9Y2P\njRJ4LDqX\n-----END CERTIFICATE-----`\n\tserverKey = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApsxaMyHKMycOyr/X0n0PjhjXe49Iw3bo0U4J+NWBXbfNyq8j\nxbVyyixiPz2BKYhHGlwvYW1fs2MhPi9qpehv96fWVjLyGy1pw6zZzLYtxVkd/G7+\nG/fqEQotjby04PjUvUJhl6wG3fJXy8Zxgg6ecB0iIbnZIKGd25Np8FUT0+rk/uon\nWBl3i4FjF7z9lrSZ60oO8w3oVMr5Mernp4PzfSJHAU51LDdng1L+dA15VbbrcTgI\nLi4FXGDAge7qCKoR8ihXqhVNjrjLxfZiXCq9tU3861Y0gkEu76Y3YSFHbKCll0DJ\nzzbXKuT9fb8N3c6IpKuu9cvV8640aUs0YP7e9QIDAQABAoIBADbD9gG/4HH3KwYr\nAyPbaBYR1f59xzhWfI7sfp2zDGzHAsy/wJETyILVG9UDzrriQeZHyk7E6J0vuSR/\n0RZ0QP8hnmBjDdcajBVxVXm/fzvCzPOrRcfNGI9LtjVJdmI/kSoq93wjQYXyIh2I\nJJC9WAwbpK9KJB5wsjH8LtZ4OLBlcdeB8jcvO6FzGij6HwyxqyPctxetlvpcmc/w\nzNJhps6t+TJ8PpNtEmTpOOmx85V6HMb3QJexwmUYygRaOoiQKBKZSNaOnGoC8w1d\nWahyyXJk4B3OUllqG1TLUgabFGqq2PeJSP8RvYFH8DUj+fdxD78qDHAygrL8ELLZ\n2O3Wi0ECgYEAyREnS/kylyIcAsyKczsKEDMIDUF9rGvm2B+QG7cLKHTu24oiNg5B\nIk5nkaYmSSrC3O2/s4v47mYzMtWbLxlogiNK6ljLPpdU5/JaeHncZC+18seBoePQ\n9nOW3AvY2A6ihzy8sKRMfl3FUx/1rcXLdNwkMQo0FWR7nqVPUme9QkkCgYEA1F5n\nlhfDptiHekagKMTf9SGw4B2UiG6SLjMWhcG2AEFeXpZlsk7Qubnuzk0krjYp+JAI\nbrlzMOkmBXBQywKLe3SG0s0McbRGWVFbEA1SA+WZV5rwJe5PO7W6ndCF2+slyZ5T\ndPwOY1RybV6R07EvjtfnE8Wtdyko4X22sTkyd00CgYA5MYnuEHqVhvxUx33yfS7F\noN5/dsuayi6l94R0fcLMxUZUaJyGp9NbQNYxFgP5+BHp6i8HkZ9DoQqbQSudYCrc\nKdHbi1p0+XMLb2LQtkk8rl2hK6LyO+1qzUJyYWRTQQZ2VY6O6I1hvKaumH636XWQ\nTjZ1RKPAGg8X94nytNOfEQKBgQC/+TL0iDjyGyykyTFAiW/WXQVSIwtBJYr5Pm9u\nrESFCJJxOM1nmT2vlrecQDoXTZk1O6aTyQqrPSeEpRoz2fISwKyb5IYKRyeM2DFU\nWmY4ZZXvjnzmHP39APNYc8Z9nZzEHF5fEvdCrXTfDy0Ny08tdlhKFFkRreBprkW3\nAPhwxQKBgDBdionnjdB9jdGbYHrsPaweMGdQNXkrTTCFfBA47F+qZswfon12yu4A\n+cBKCnQe2dQHl8AV3IeUKpmNghu4iICOASQEO9dS6OWZI5vBxZMePBm6+bjTOuf6\nozecw3yR55tKpPImt87rhrWlwp35uWuhOr9GHYBdFSwgrEkVMw++\n-----END RSA PRIVATE KEY-----`\n\tcaCRT = `-----BEGIN CERTIFICATE-----\nMIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0\nQXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT\nCENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m\nfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1\nv03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh\nyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL\nCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U\n4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb\nKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo\nNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLqR/BJTbbyXUB0imne1u00fuzb\nS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb78x7ivdyXSF5LVQJ1JvhhWu6i\nM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpBP8d2jcRZVUVrSXGc2mAGuGOY\n/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2ZxugCXULtRWJ9p4C9zUl40HEy\nOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD\nAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNoJhIvDZQrEf/VQbWuu\nXgNnt2m5MA0GCSqGSIb3DQEBCwUAA4ICAQCYhT5SRqk19hGrQ09hVSZOzynXAa5F\nsYkEWJzFyLg9azhnTPE1bFM18FScnkd+dal6mt+bQiJvdh24NaVkDghVB7GkmXki\npAiZwEDHMqtbhiPxY8LtSeCBAz5JqXVU2Q0TpAgNSH4W7FbGWNThhxcJVOoIrXKE\njbzhwl1Etcaf0DBKWliUbdlxQQs65DLy+rNBYtOeK0pzhzn1vpehUlJ4eTFzP9KX\ny2Mksuq9AspPbqnqpWW645MdTxMb5T57MCrY3GDKw63z5z3kz88LWJF3nOxZmgQy\nWFUhbLmZm7x6N5eiu6Wk8/B4yJ/n5UArD4cEP1i7nqu+mbbM/SZlq1wnGpg/sbRV\noUF+a7pRcSbfxEttle4pLFhS+ErKatjGcNEab2OlU3bX5UoBs+TYodnCWGKOuBKV\nL/CYc65QyeYZ+JiwYn9wC8YkzOnnVIQjiCEkLgSL30h9dxpnTZDLrdAA8ItelDn5\nDvjuQq58CGDsaVqpSobiSC1DMXYWot4Ets1wwovUNEq1l0MERB+2olE+JU/8E23E\neL1/aA7Kw/JibkWz1IyzClpFDKXf6kR2onJyxerdwUL+is7tqYFLysiHxZDL1bli\nSXbW8hMa5gvo0IilFP9Rznn8PplIfCsvBDVv6xsRr5nTAFtwKaMBVgznE2ghs69w\nkK8u1YiiVenmoQ==\n-----END CERTIFICATE-----`\n\tcaKey = `-----BEGIN RSA PRIVATE KEY-----\nMIIJKgIBAAKCAgEA7WHW216mfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7x\nb64rkpdzx1aWetSiCrEyc3D1v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvD\nOBUYZgtMqHZzpE6xRrqQ84zhyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKz\nn/2uEVt33qmO85WtN3RzbSqLCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj\n7B5P5MeamkkogwbExUjdHp3U4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZ\nDe67V/Q8iB2May1k7zBz1ZtbKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmOb\ncn8AIfH6smLQrn0C3cs7CYfoNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLq\nR/BJTbbyXUB0imne1u00fuzbS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb7\n8x7ivdyXSF5LVQJ1JvhhWu6iM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpB\nP8d2jcRZVUVrSXGc2mAGuGOY/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2Z\nxugCXULtRWJ9p4C9zUl40HEyOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEA\nAQKCAgEA4x0OoceG54ZrVxifqVaQd8qw3uRmUKUMIMdfuMlsdideeLO97ynmSlRY\n00kGo/I4Lp6mNEjI9gUie9+uBrcUhri4YLcujHCH+YlNnCBDbGjwbe0ds9SLCWaa\nKztZHMSlW5Q4Bqytgu+MpOnxSgqjlOk+vz9TcGFKVnUkHIkAcqKFJX8gOFxPZA/t\nOb1kJaz4kuv5W2Kur/ISKvQtvFvOtQeV0aJyZm8LqXnvS4cPI7yN4329NDU0HyDR\ny/deqS2aqV4zII3FFqbz8zix/m1xtVQzWCugZGMKrz0iuJMfNeCABb8rRGc6GsZz\n+465v/kobqgeyyneJ1s5rMFrLp2o+dwmnIVMNsFDUiN1lIZDHLvlgonaUO3IdTZc\n9asamFWKFKUMgWqM4zB1vmUO12CKowLNIIKb0L+kf1ixaLLDRGf/f9vLtSHE+oyx\nlATiS18VNA8+CGsHF6uXMRwf2auZdRI9+s6AAeyRISSbO1khyWKHo+bpOvmPAkDR\nnknTjbYgkoZOV+mrsU5oxV8s6vMkuvA3rwFhT2gie8pokuACFcCRrZi9MVs4LmUQ\nu0GYTHvp2WJUjMWBm6XX7Hk3g2HV842qpk/mdtTjNsXws81djtJPn4I/soIXSgXz\npY3SvKTuOckP9OZVF0yqKGeZXKpD288PKpC+MAg3GvEJaednagECggEBAPsfLwuP\nL1kiDjXyMcRoKlrQ6Q/zBGyBmJbZ5uVGa02+XtYtDAzLoVupPESXL0E7+r8ZpZ39\n0dV4CEJKpbVS/BBtTEkPpTK5kz778Ib04TAyj+YLhsZjsnuja3T5bIBZXFDeDVDM\n0ZaoFoKpIjTu2aO6pzngsgXs6EYbo2MTuJD3h0nkGZsICL7xvT9Mw0P1p2Ftt/hN\n+jKk3vN220wTWUsq43AePi45VwK+PNP12ZXv9HpWDxlPo3j0nXtgYXittYNAT92u\nBZbFAzldEIX9WKKZgsWtIzLaASjVRntpxDCTby/nlzQ5dw3DHU1DV3PIqxZS2+Oe\nKV+7XFWgZ44YjYECggEBAPH+VDu3QSrqSahkZLkgBtGRkiZPkZFXYvU6kL8qf5wO\nZ/uXMeqHtznAupLea8I4YZLfQim/NfC0v1cAcFa9Ckt9g3GwTSirVcN0AC1iOyv3\n/hMZCA1zIyIcuUplNr8qewoX71uPOvCNH0dix77423mKFkJmNwzy4Q+rV+qkRdLn\nv+AAgh7g5N91pxNd6LQJjoyfi1Ka6rRP2yGXM5v7QOwD16eN4JmExUxX1YQ7uNuX\npVS+HRxnBquA+3/DB1LtBX6pa2cUa+LRUmE/NCPHMvJcyuNkYpJKlNTd9vnbfo0H\nRNSJSWm+aGxDFMjuPjV3JLj2OdKMPwpnXdh2vBZCPpMCggEAM+yTvrEhmi2HgLIO\nhkz/jP2rYyfdn04ArhhqPLgd0dpuI5z24+Jq/9fzZT9ZfwSW6VK1QwDLlXcXRhXH\nQ8Hf6smev3CjuORURO61IkKaGWwrAucZPAY7ToNQ4cP9ImDXzMTNPgrLv3oMBYJR\nV16X09nxX+9NABqnQG/QjdjzDc6Qw7+NZ9f2bvzvI5qMuY2eyW91XbtJ45ThoLfP\nymAp03gPxQwL0WT7z85kJ3OrROxzwaPvxU0JQSZbNbqNDPXmFTiECxNDhpRAAWlz\n1DC5Vg2l05fkMkyPdtD6nOQWs/CYSfB5/EtxiX/xnBszhvZUIe6KFvuKFIhaJD5h\niykagQKCAQEAoBRm8k3KbTIo4ZzvyEq4V/+dF3zBRczx6FkCkYLygXBCNvsQiR2Y\nBjtI8Ijz7bnQShEoOmeDriRTAqGGrspEuiVgQ1+l2wZkKHRe/aaij/Zv+4AuhH8q\nuZEYvW7w5Uqbs9SbgQzhp2kjTNy6V8lVnjPLf8cQGZ+9Y9krwktC6T5m/i435WdN\n38h7amNP4XEE/F86Eb3rDrZYtgLIoCF4E+iCyxMehU+AGH1uABhls9XAB6vvo+8/\nSUp8lEqWWLP0U5KNOtYWfCeOAEiIHDbUq+DYUc4BKtbtV1cx3pzlPTOWw6XBi5Lq\njttdL4HyYvnasAQpwe8GcMJqIRyCVZMiwwKCAQEAhQTTS3CC8PwcoYrpBdTjW1ck\nvVFeF1YbfqPZfYxASCOtdx6wRnnEJ+bjqntagns9e88muxj9UhxSL6q9XaXQBD8+\n2AmKUxphCZQiYFZcTucjQEQEI2nN+nAKgRrUSMMGiR8Ekc2iFrcxBU0dnSohw+aB\nPbMKVypQCREu9PcDFIp9rXQTeElbaNsIg1C1w/SQjODbmN/QFHTVbRODYqLeX1J/\nVcGsykSIq7hv6bjn7JGkr2JTdANbjk9LnMjMdJFsKRYxPKkOQfYred6Hiojp5Sor\nPW5am8ejnNSPhIfqQp3uV3KhwPDKIeIpzvrB4uPfTjQWhekHCb8cKSWux3flqw==\n-----END RSA PRIVATE KEY-----`\n\tcaCRL = `-----BEGIN X509 CRL-----\nMIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN\nMjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg\nZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r\nrl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV\n+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199\nXSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE\njF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF\nDouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD\nMSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4\niYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE\nypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4\nhVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94\ncp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67\nsn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn\nw7bXJGfJcIMrrKs=\n-----END X509 CRL-----`\n\tclient1Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAJr32nHRlhyPiS7IfZ/ZWYowDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjM3WhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+kiJ3X6\nHUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi85xE\nOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLWeYl7\nQie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4ZdRf\nXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dybbhO\nc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRUh5Xo\nGzjh6iReaPSOgGatqOw9bDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEAyAK7cOTWqjyLgFM0kyyx1fNPvm2GwKep3MuU\nOrSnLuWjoxzb7WcbKNVMlnvnmSUAWuErxsY0PUJNfcuqWiGmEp4d/SWfWPigG6DC\nsDej35BlSfX8FCufYrfC74VNk4yBS2LVYmIqcpqUrfay0I2oZA8+ToLEpdUvEv2I\nl59eOhJO2jsC3JbOyZZmK2Kv7d94fR+1tg2Rq1Wbnmc9AZKq7KDReAlIJh4u2KHb\nBbtF79idusMwZyP777tqSQ4THBMa+VAEc2UrzdZqTIAwqlKQOvO2fRz2P+ARR+Tz\nMYJMdCdmPZ9qAc8U1OcFBG6qDDltO8wf/Nu/PsSI5LGCIhIuPPIuKfm0rRfTqCG7\nQPQPWjRoXtGGhwjdIuWbX9fIB+c+NpAEKHgLtV+Rxj8s5IVxqG9a5TtU9VkfVXJz\nJ20naoz/G+vDsVINpd3kH0ziNvdrKfGRM5UgtnUOPCXB22fVmkIsMH2knI10CKK+\noffI56NTkLRu00xvg98/wdukhkwIAxg6PQI/BHY5mdvoacEHHHdOhMq+GSAh7DDX\nG8+HdbABM1ExkPnZLat15q706ztiuUpQv1C2DI8YviUVkMqCslj4cD4F8EFPo4kr\nkvme0Cuc9Qlf7N5rjdV3cjwavhFx44dyXj9aesft2Q1okPiIqbGNpcjHcIRlj4Au\nMU3Bo0A=\n-----END CERTIFICATE-----`\n\tclient1Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+ki\nJ3X6HUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi\n85xEOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLW\neYl7Qie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4\nZdRfXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dy\nbbhOc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABAoIBAFjSHK7gENVZxphO\nhHg8k9ShnDo8eyDvK8l9Op3U3/yOsXKxolivvyx//7UFmz3vXDahjNHe7YScAXdw\neezbqBXa7xrvghqZzp2HhFYwMJ0210mcdncBKVFzK4ztZHxgQ0PFTqet0R19jZjl\nX3A325/eNZeuBeOied4qb/24AD6JGc6A0J55f5/QUQtdwYwrL15iC/KZXDL90PPJ\nCFJyrSzcXvOMEvOfXIFxhDVKRCppyIYXG7c80gtNC37I6rxxMNQ4mxjwUI2IVhxL\nj+nZDu0JgRZ4NaGjOq2e79QxUVm/GG3z25XgmBFBrXkEVV+sCZE1VDyj6kQfv9FU\nNhOrwGECgYEAzq47r/HwXifuGYBV/mvInFw3BNLrKry+iUZrJ4ms4g+LfOi0BAgf\nsXsWXulpBo2YgYjFdO8G66f69GlB4B7iLscpABXbRtpDZEnchQpaF36/+4g3i8gB\nZ29XHNDB8+7t4wbXvlSnLv1tZWey2fS4hPosc2YlvS87DMmnJMJqhs8CgYEA4oiB\nLGQP6VNdX0Uigmh5fL1g1k95eC8GP1ylczCcIwsb2OkAq0MT7SHRXOlg3leEq4+g\nmCHk1NdjkSYxDL2ZeTKTS/gy4p1jlcDa6Ilwi4pVvatNvu4o80EYWxRNNb1mAn67\nT8TN9lzc6mEi+LepQM3nYJ3F+ZWTKgxH8uoJwMUCgYEArpumE1vbjUBAuEyi2eGn\nRunlFW83fBCfDAxw5KM8anNlja5uvuU6GU/6s06QCxg+2lh5MPPrLdXpfukZ3UVa\nItjg+5B7gx1MSALaiY8YU7cibFdFThM3lHIM72wyH2ogkWcrh0GvSFSUQlJcWCSW\nasmMGiYXBgBL697FFZomMyMCgYEAkAnp0JcDQwHd4gDsk2zoqnckBsDb5J5J46n+\nDYNAFEww9bgZ08u/9MzG+cPu8xFE621U2MbcYLVfuuBE2ewIlPaij/COMmeO9Z59\n0tPpOuDH6eTtd1SptxqR6P+8pEn8feOlKHBj4Z1kXqdK/EiTlwAVeep4Al2oCFls\nujkz4F0CgYAe8vHnVFHlWi16zAqZx4ZZZhNuqPtgFkvPg9LfyNTA4dz7F9xgtUaY\nnXBPyCe/8NtgBfT79HkPiG3TM0xRZY9UZgsJKFtqAu5u4ManuWDnsZI9RK2QTLHe\nyEbH5r3Dg3n9k/3GbjXFIWdU9UaYsdnSKHHtMw9ZODc14LaAogEQug==\n-----END RSA PRIVATE KEY-----`\n\t// client 2 crt is revoked\n\tclient2Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAM5p4duNjikSPxYGaqBm5FEwDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjUyWhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImVjm/b\nQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TPkZua\neq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEKh4LQ\ncr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81PQAmT\nA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu4Ic0\n6tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBR5mf0f\nZjf8ZCGXqU2+45th7VkkLDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEARhFxNAouwbpEfN1M90+ao5rwyxEewerSoCCz\nPQzeUZ66MA/FkS/tFUGgGGG+wERN+WLbe1cN6q/XFr0FSMLuUxLXDNV02oUL/FnY\nxcyNLaZUZ0pP7sA+Hmx2AdTA6baIwQbyIY9RLAaz6hzo1YbI8yeis645F1bxgL2D\nEP5kXa3Obv0tqWByMZtrmJPv3p0W5GJKXVDn51GR/E5KI7pliZX2e0LmMX9mxfPB\n4sXFUggMHXxWMMSAmXPVsxC2KX6gMnajO7JUraTwuGm+6V371FzEX+UKXHI+xSvO\n78TseTIYsBGLjeiA8UjkKlD3T9qsQm2mb2PlKyqjvIm4i2ilM0E2w4JZmd45b925\n7q/QLV3NZ/zZMi6AMyULu28DWKfAx3RLKwnHWSFcR4lVkxQrbDhEUMhAhLAX+2+e\nqc7qZm3dTabi7ZJiiOvYK/yNgFHa/XtZp5uKPB5tigPIa+34hbZF7s2/ty5X3O1N\nf5Ardz7KNsxJjZIt6HvB28E/PPOvBqCKJc1Y08J9JbZi8p6QS1uarGoR7l7rT1Hv\n/ZXkNTw2bw1VpcWdzDBLLVHYNnJmS14189LVk11PcJJpSmubwCqg+ZZULdgtVr3S\nANas2dgMPVwXhnAalgkcc+lb2QqaEz06axfbRGBsgnyqR5/koKCg1Hr0+vThHSsR\nE0+r2+4=\n-----END CERTIFICATE-----`\n\tclient2Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImV\njm/bQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TP\nkZuaeq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEK\nh4LQcr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81P\nQAmTA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu\n4Ic06tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABAoIBAQCMnEeg9uXQmdvq\nop4qi6bV+ZcDWvvkLwvHikFMnYpIaheYBpF2ZMKzdmO4xgCSWeFCQ4Hah8KxfHCM\nqLuWvw2bBBE5J8yQ/JaPyeLbec7RX41GQ2YhPoxDdP0PdErREdpWo4imiFhH/Ewt\nRvq7ufRdpdLoS8dzzwnvX3r+H2MkHoC/QANW2AOuVoZK5qyCH5N8yEAAbWKaQaeL\nVBhAYEVKbAkWEtXw7bYXzxRR7WIM3f45v3ncRusDIG+Hf75ZjatoH0lF1gHQNofO\nqkCVZVzjkLFuzDic2KZqsNORglNs4J6t5Dahb9v3hnoK963YMnVSUjFvqQ+/RZZy\nVILFShilAoGBANucwZU61eJ0tLKBYEwmRY/K7Gu1MvvcYJIOoX8/BL3zNmNO0CLl\nNiABtNt9WOVwZxDsxJXdo1zvMtAegNqS6W11R1VAZbL6mQ/krScbLDE6JKA5DmA7\n4nNi1gJOW1ziAfdBAfhe4cLbQOb94xkOK5xM1YpO0xgDJLwrZbehDMmPAoGBAMAl\n/owPDAvcXz7JFynT0ieYVc64MSFiwGYJcsmxSAnbEgQ+TR5FtkHYe91OSqauZcCd\naoKXQNyrYKIhyounRPFTdYQrlx6KtEs7LU9wOxuphhpJtGjRnhmA7IqvX703wNvu\nkhrEavn86G5boH8R80371SrN0Rh9UeAlQGuNBdvfAoGAEAmokW9Ug08miwqrr6Pz\n3IZjMZJwALidTM1IufQuMnj6ddIhnQrEIx48yPKkdUz6GeBQkuk2rujA+zXfDxc/\neMDhzrX/N0zZtLFse7ieR5IJbrH7/MciyG5lVpHGVkgjAJ18uVikgAhm+vd7iC7i\nvG1YAtuyysQgAKXircBTIL0CgYAHeTLWVbt9NpwJwB6DhPaWjalAug9HIiUjktiB\nGcEYiQnBWn77X3DATOA8clAa/Yt9m2HKJIHkU1IV3ESZe+8Fh955PozJJlHu3yVb\nAp157PUHTriSnxyMF2Sb3EhX/rQkmbnbCqqygHC14iBy8MrKzLG00X6BelZV5n0D\n8d85dwKBgGWY2nsaemPH/TiTVF6kW1IKSQoIyJChkngc+Xj/2aCCkkmAEn8eqncl\nRKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA\nubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl\n-----END RSA PRIVATE KEY-----`\n)\n\nfunc TestLoadCertificate(t *testing.T) {\n\tstartEventScheduler()\n\tcaCrtPath := filepath.Join(os.TempDir(), \"testca.crt\")\n\tcaCrlPath := filepath.Join(os.TempDir(), \"testcrl.crt\")\n\tcertPath := filepath.Join(os.TempDir(), \"test.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test.key\")\n\terr := os.WriteFile(caCrtPath, []byte(caCRT), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(caCrlPath, []byte(caCRL), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(certPath, []byte(serverCert), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyPath, []byte(serverKey), os.ModePerm)\n\tassert.NoError(t, err)\n\tkeyPairs := []TLSKeyPair{\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   DefaultTLSKeyPaidID,\n\t\t},\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   DefaultTLSKeyPaidID,\n\t\t},\n\t}\n\tcertManager, err := NewCertManager(keyPairs, configDir, logSenderTest)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is duplicated\")\n\t}\n\tassert.Nil(t, certManager)\n\n\tkeyPairs = []TLSKeyPair{\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   DefaultTLSKeyPaidID,\n\t\t},\n\t}\n\n\tcertManager, err = NewCertManager(keyPairs, configDir, logSenderTest)\n\tassert.NoError(t, err)\n\tassert.True(t, certManager.HasCertificate(DefaultTLSKeyPaidID))\n\tassert.False(t, certManager.HasCertificate(\"unknownID\"))\n\tcertFunc := certManager.GetCertificateFunc(DefaultTLSKeyPaidID)\n\tif assert.NotNil(t, certFunc) {\n\t\thello := &tls.ClientHelloInfo{\n\t\t\tServerName:   \"localhost\",\n\t\t\tCipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},\n\t\t}\n\t\tcert, err := certFunc(hello)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, certManager.certs[DefaultTLSKeyPaidID], cert)\n\t}\n\tcertFunc = certManager.GetCertificateFunc(\"unknownID\")\n\tif assert.NotNil(t, certFunc) {\n\t\thello := &tls.ClientHelloInfo{\n\t\t\tServerName:   \"localhost\",\n\t\t\tCipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},\n\t\t}\n\t\t_, err = certFunc(hello)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"no certificate for id unknownID\")\n\t\t}\n\t}\n\tcertManager.SetCACertificates(nil)\n\terr = certManager.LoadRootCAs()\n\tassert.NoError(t, err)\n\n\tcertManager.SetCACertificates([]string{\"\"})\n\terr = certManager.LoadRootCAs()\n\tassert.Error(t, err)\n\n\tcertManager.SetCACertificates([]string{\"invalid\"})\n\terr = certManager.LoadRootCAs()\n\tassert.Error(t, err)\n\n\t// laoding the key as root CA must fail\n\tcertManager.SetCACertificates([]string{keyPath})\n\terr = certManager.LoadRootCAs()\n\tassert.Error(t, err)\n\n\tcertManager.SetCACertificates([]string{certPath})\n\terr = certManager.LoadRootCAs()\n\tassert.NoError(t, err)\n\n\trootCa := certManager.GetRootCAs()\n\tassert.NotNil(t, rootCa)\n\n\terr = certManager.Reload()\n\tassert.NoError(t, err)\n\n\tcertManager.SetCARevocationLists(nil)\n\terr = certManager.LoadCRLs()\n\tassert.NoError(t, err)\n\n\tcertManager.SetCARevocationLists([]string{\"\"})\n\terr = certManager.LoadCRLs()\n\tassert.Error(t, err)\n\n\tcertManager.SetCARevocationLists([]string{\"invalid crl\"})\n\terr = certManager.LoadCRLs()\n\tassert.Error(t, err)\n\n\t// this is not a crl and must fail\n\tcertManager.SetCARevocationLists([]string{caCrtPath})\n\terr = certManager.LoadCRLs()\n\tassert.Error(t, err)\n\n\tcertManager.SetCARevocationLists([]string{caCrlPath})\n\terr = certManager.LoadCRLs()\n\tassert.NoError(t, err)\n\n\tcrt, err := tls.X509KeyPair([]byte(caCRT), []byte(caKey))\n\tassert.NoError(t, err)\n\n\tx509CAcrt, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tcrt, err = tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\tx509crt, err := x509.ParseCertificate(crt.Certificate[0])\n\tif assert.NoError(t, err) {\n\t\tassert.False(t, certManager.IsRevoked(x509crt, x509CAcrt))\n\t}\n\n\tcrt, err = tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))\n\tassert.NoError(t, err)\n\tx509crt, err = x509.ParseCertificate(crt.Certificate[0])\n\tif assert.NoError(t, err) {\n\t\tassert.True(t, certManager.IsRevoked(x509crt, x509CAcrt))\n\t}\n\n\tassert.True(t, certManager.IsRevoked(nil, nil))\n\n\terr = os.Remove(caCrlPath)\n\tassert.NoError(t, err)\n\terr = certManager.Reload()\n\tassert.Error(t, err)\n\n\terr = os.Remove(certPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n\terr = certManager.Reload()\n\tassert.Error(t, err)\n\n\terr = os.Remove(caCrtPath)\n\tassert.NoError(t, err)\n\tstopEventScheduler()\n}\n\nfunc TestLoadInvalidCert(t *testing.T) {\n\tstartEventScheduler()\n\tcertManager, err := NewCertManager(nil, configDir, logSenderTest)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no key pairs defined\")\n\t}\n\tassert.Nil(t, certManager)\n\n\tkeyPairs := []TLSKeyPair{\n\t\t{\n\t\t\tCert: \"test.crt\",\n\t\t\tKey:  \"test.key\",\n\t\t\tID:   DefaultTLSKeyPaidID,\n\t\t},\n\t}\n\tcertManager, err = NewCertManager(keyPairs, configDir, logSenderTest)\n\tassert.Error(t, err)\n\tassert.Nil(t, certManager)\n\n\tkeyPairs = []TLSKeyPair{\n\t\t{\n\t\t\tCert: \"test.crt\",\n\t\t\tKey:  \"test.key\",\n\t\t},\n\t}\n\tcertManager, err = NewCertManager(keyPairs, configDir, logSenderTest)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"TLS certificate without ID\")\n\t}\n\tassert.Nil(t, certManager)\n\tstopEventScheduler()\n}\n\nfunc TestCertificateMonitor(t *testing.T) {\n\tstartEventScheduler()\n\tdefer stopEventScheduler()\n\n\tcertPath := filepath.Join(os.TempDir(), \"test.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test.key\")\n\tcaCrlPath := filepath.Join(os.TempDir(), \"testcrl.crt\")\n\terr := os.WriteFile(certPath, []byte(serverCert), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyPath, []byte(serverKey), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(caCrlPath, []byte(caCRL), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tkeyPairs := []TLSKeyPair{\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   DefaultTLSKeyPaidID,\n\t\t},\n\t}\n\tcertManager, err := NewCertManager(keyPairs, configDir, logSenderTest)\n\tassert.NoError(t, err)\n\tassert.Len(t, certManager.monitorList, 1)\n\trequire.Len(t, certManager.certsInfo, 1)\n\tinfo := certManager.certsInfo[certPath]\n\trequire.NotNil(t, info)\n\tcertManager.SetCARevocationLists([]string{caCrlPath})\n\terr = certManager.LoadCRLs()\n\tassert.NoError(t, err)\n\tassert.Len(t, certManager.monitorList, 2)\n\tcertManager.monitor()\n\trequire.Len(t, certManager.certsInfo, 2)\n\n\terr = os.Remove(certPath)\n\tassert.NoError(t, err)\n\tcertManager.monitor()\n\n\ttime.Sleep(100 * time.Millisecond)\n\terr = os.WriteFile(certPath, []byte(serverCert), os.ModePerm)\n\tassert.NoError(t, err)\n\tcertManager.monitor()\n\trequire.Len(t, certManager.certsInfo, 2)\n\tnewInfo := certManager.certsInfo[certPath]\n\trequire.NotNil(t, newInfo)\n\tassert.Equal(t, info.Size(), newInfo.Size())\n\tassert.NotEqual(t, info.ModTime(), newInfo.ModTime())\n\n\terr = os.Remove(caCrlPath)\n\tassert.NoError(t, err)\n\n\terr = os.Remove(certPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/common/transfer.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"path\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nvar (\n\t// ErrTransferClosed defines the error returned for a closed transfer\n\tErrTransferClosed = errors.New(\"transfer already closed\")\n)\n\n// BaseTransfer contains protocols common transfer details for an upload or a download.\ntype BaseTransfer struct {\n\tID              int64\n\tBytesSent       atomic.Int64\n\tBytesReceived   atomic.Int64\n\tFs              vfs.Fs\n\tFile            vfs.File\n\tConnection      *BaseConnection\n\tcancelFn        func()\n\tfsPath          string\n\teffectiveFsPath string\n\trequestPath     string\n\tftpMode         string\n\tstart           time.Time\n\tMaxWriteSize    int64\n\tMinWriteOffset  int64\n\tInitialSize     int64\n\ttruncatedSize   int64\n\tisNewFile       bool\n\ttransferType    int\n\tAbortTransfer   atomic.Bool\n\taTime           time.Time\n\tmTime           time.Time\n\ttransferQuota   dataprovider.TransferQuota\n\tmetadata        map[string]string\n\tsync.Mutex\n\terrAbort    error\n\tErrTransfer error\n}\n\n// NewBaseTransfer returns a new BaseTransfer and adds it to the given connection\nfunc NewBaseTransfer(file vfs.File, conn *BaseConnection, cancelFn func(), fsPath, effectiveFsPath, requestPath string,\n\ttransferType int, minWriteOffset, initialSize, maxWriteSize, truncatedSize int64, isNewFile bool, fs vfs.Fs,\n\ttransferQuota dataprovider.TransferQuota,\n) *BaseTransfer {\n\tt := &BaseTransfer{\n\t\tID:              conn.GetTransferID(),\n\t\tFile:            file,\n\t\tConnection:      conn,\n\t\tcancelFn:        cancelFn,\n\t\tfsPath:          fsPath,\n\t\teffectiveFsPath: effectiveFsPath,\n\t\tstart:           time.Now(),\n\t\ttransferType:    transferType,\n\t\tMinWriteOffset:  minWriteOffset,\n\t\tInitialSize:     initialSize,\n\t\tisNewFile:       isNewFile,\n\t\trequestPath:     requestPath,\n\t\tMaxWriteSize:    maxWriteSize,\n\t\ttruncatedSize:   truncatedSize,\n\t\ttransferQuota:   transferQuota,\n\t\tFs:              fs,\n\t}\n\tt.AbortTransfer.Store(false)\n\tt.BytesSent.Store(0)\n\tt.BytesReceived.Store(0)\n\n\tconn.AddTransfer(t)\n\treturn t\n}\n\n// GetTransferQuota returns data transfer quota limits\nfunc (t *BaseTransfer) GetTransferQuota() dataprovider.TransferQuota {\n\treturn t.transferQuota\n}\n\n// SetFtpMode sets the FTP mode for the current transfer\nfunc (t *BaseTransfer) SetFtpMode(mode string) {\n\tt.ftpMode = mode\n}\n\n// GetID returns the transfer ID\nfunc (t *BaseTransfer) GetID() int64 {\n\treturn t.ID\n}\n\n// GetType returns the transfer type\nfunc (t *BaseTransfer) GetType() int {\n\treturn t.transferType\n}\n\n// GetSize returns the transferred size\nfunc (t *BaseTransfer) GetSize() int64 {\n\tif t.transferType == TransferDownload {\n\t\treturn t.BytesSent.Load()\n\t}\n\treturn t.BytesReceived.Load()\n}\n\n// GetDownloadedSize returns the transferred size\nfunc (t *BaseTransfer) GetDownloadedSize() int64 {\n\treturn t.BytesSent.Load()\n}\n\n// GetUploadedSize returns the transferred size\nfunc (t *BaseTransfer) GetUploadedSize() int64 {\n\treturn t.BytesReceived.Load()\n}\n\n// GetStartTime returns the start time\nfunc (t *BaseTransfer) GetStartTime() time.Time {\n\treturn t.start\n}\n\n// GetAbortError returns the error to send to the client if the transfer was aborted\nfunc (t *BaseTransfer) GetAbortError() error {\n\tt.Lock()\n\tdefer t.Unlock()\n\n\tif t.errAbort != nil {\n\t\treturn t.errAbort\n\t}\n\treturn getQuotaExceededError(t.Connection.protocol)\n}\n\n// SignalClose signals that the transfer should be closed after the next read/write.\n// The optional error argument allow to send a specific error, otherwise a generic\n// transfer aborted error is sent\nfunc (t *BaseTransfer) SignalClose(err error) {\n\tt.Lock()\n\tt.errAbort = err\n\tt.Unlock()\n\tt.AbortTransfer.Store(true)\n}\n\n// GetTruncatedSize returns the truncated sized if this is an upload overwriting\n// an existing file\nfunc (t *BaseTransfer) GetTruncatedSize() int64 {\n\treturn t.truncatedSize\n}\n\n// HasSizeLimit returns true if there is an upload or download size limit\nfunc (t *BaseTransfer) HasSizeLimit() bool {\n\tif t.MaxWriteSize > 0 {\n\t\treturn true\n\t}\n\tif t.transferQuota.HasSizeLimits() {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// GetVirtualPath returns the transfer virtual path\nfunc (t *BaseTransfer) GetVirtualPath() string {\n\treturn t.requestPath\n}\n\n// GetFsPath returns the transfer filesystem path\nfunc (t *BaseTransfer) GetFsPath() string {\n\treturn t.fsPath\n}\n\n// SetTimes stores access and modification times if fsPath matches the current file\nfunc (t *BaseTransfer) SetTimes(fsPath string, atime time.Time, mtime time.Time) bool {\n\tif fsPath == t.GetFsPath() {\n\t\tt.aTime = atime\n\t\tt.mTime = mtime\n\t\treturn true\n\t}\n\treturn false\n}\n\n// GetRealFsPath returns the real transfer filesystem path.\n// If atomic uploads are enabled this differ from fsPath\nfunc (t *BaseTransfer) GetRealFsPath(fsPath string) string {\n\tif fsPath == t.GetFsPath() {\n\t\tif t.File != nil || vfs.IsLocalOsFs(t.Fs) {\n\t\t\treturn t.effectiveFsPath\n\t\t}\n\t\treturn t.fsPath\n\t}\n\treturn \"\"\n}\n\n// SetMetadata sets the metadata for the file\nfunc (t *BaseTransfer) SetMetadata(val map[string]string) {\n\tt.metadata = val\n}\n\n// SetCancelFn sets the cancel function for the transfer\nfunc (t *BaseTransfer) SetCancelFn(cancelFn func()) {\n\tt.cancelFn = cancelFn\n}\n\n// ConvertError accepts an error that occurs during a read or write and\n// converts it into a more understandable form for the client if it is a\n// well-known type of error\nfunc (t *BaseTransfer) ConvertError(err error) error {\n\tvar pathError *fs.PathError\n\tif errors.As(err, &pathError) {\n\t\treturn fmt.Errorf(\"%s %s: %s\", pathError.Op, t.GetVirtualPath(), pathError.Err.Error())\n\t}\n\treturn t.Connection.GetFsError(t.Fs, err)\n}\n\n// CheckRead returns an error if read if not allowed\nfunc (t *BaseTransfer) CheckRead() error {\n\tif t.transferQuota.AllowedDLSize == 0 && t.transferQuota.AllowedTotalSize == 0 {\n\t\treturn nil\n\t}\n\tif t.transferQuota.AllowedTotalSize > 0 {\n\t\tif t.BytesSent.Load()+t.BytesReceived.Load() > t.transferQuota.AllowedTotalSize {\n\t\t\treturn t.Connection.GetReadQuotaExceededError()\n\t\t}\n\t} else if t.transferQuota.AllowedDLSize > 0 {\n\t\tif t.BytesSent.Load() > t.transferQuota.AllowedDLSize {\n\t\t\treturn t.Connection.GetReadQuotaExceededError()\n\t\t}\n\t}\n\treturn nil\n}\n\n// CheckWrite returns an error if write if not allowed\nfunc (t *BaseTransfer) CheckWrite() error {\n\tif t.MaxWriteSize > 0 && t.BytesReceived.Load() > t.MaxWriteSize {\n\t\treturn t.Connection.GetQuotaExceededError()\n\t}\n\tif t.transferQuota.AllowedULSize == 0 && t.transferQuota.AllowedTotalSize == 0 {\n\t\treturn nil\n\t}\n\tif t.transferQuota.AllowedTotalSize > 0 {\n\t\tif t.BytesSent.Load()+t.BytesReceived.Load() > t.transferQuota.AllowedTotalSize {\n\t\t\treturn t.Connection.GetQuotaExceededError()\n\t\t}\n\t} else if t.transferQuota.AllowedULSize > 0 {\n\t\tif t.BytesReceived.Load() > t.transferQuota.AllowedULSize {\n\t\t\treturn t.Connection.GetQuotaExceededError()\n\t\t}\n\t}\n\treturn nil\n}\n\n// Truncate changes the size of the opened file.\n// Supported for local fs only\nfunc (t *BaseTransfer) Truncate(fsPath string, size int64) (int64, error) {\n\tif fsPath == t.GetFsPath() {\n\t\tif t.File != nil {\n\t\t\tinitialSize := t.InitialSize\n\t\t\terr := t.File.Truncate(size)\n\t\t\tif err == nil {\n\t\t\t\tt.Lock()\n\t\t\t\tt.InitialSize = size\n\t\t\t\tif t.MaxWriteSize > 0 {\n\t\t\t\t\tsizeDiff := initialSize - size\n\t\t\t\t\tt.MaxWriteSize += sizeDiff\n\t\t\t\t\tmetric.TransferCompleted(t.BytesSent.Load(), t.BytesReceived.Load(),\n\t\t\t\t\t\tt.transferType, t.ErrTransfer, vfs.IsSFTPFs(t.Fs))\n\t\t\t\t\tif t.transferQuota.HasSizeLimits() {\n\t\t\t\t\t\tgo func(ulSize, dlSize int64, user dataprovider.User) {\n\t\t\t\t\t\t\tdataprovider.UpdateUserTransferQuota(&user, ulSize, dlSize, false) //nolint:errcheck\n\t\t\t\t\t\t}(t.BytesReceived.Load(), t.BytesSent.Load(), t.Connection.User)\n\t\t\t\t\t}\n\t\t\t\t\tt.BytesReceived.Store(0)\n\t\t\t\t}\n\t\t\t\tt.Unlock()\n\t\t\t}\n\t\t\tt.Connection.Log(logger.LevelDebug, \"file %q truncated to size %v max write size %v new initial size %v err: %v\",\n\t\t\t\tfsPath, size, t.MaxWriteSize, t.InitialSize, err)\n\t\t\treturn initialSize, err\n\t\t}\n\t\tif size == 0 && t.BytesSent.Load() == 0 {\n\t\t\t// for cloud providers the file is always truncated to zero, we don't support append/resume for uploads.\n\t\t\t// For buffered SFTP and local fs we can have buffered bytes so we returns an error\n\t\t\tif !vfs.IsBufferedLocalOrSFTPFs(t.Fs) {\n\t\t\t\treturn 0, nil\n\t\t\t}\n\t\t}\n\t\treturn 0, vfs.ErrVfsUnsupported\n\t}\n\treturn 0, errTransferMismatch\n}\n\n// TransferError is called if there is an unexpected error.\n// For example network or client issues\nfunc (t *BaseTransfer) TransferError(err error) {\n\tt.Lock()\n\tdefer t.Unlock()\n\tif t.ErrTransfer != nil {\n\t\treturn\n\t}\n\tt.ErrTransfer = err\n\tif t.cancelFn != nil {\n\t\tt.cancelFn()\n\t}\n\telapsed := time.Since(t.start).Nanoseconds() / 1000000\n\tt.Connection.Log(logger.LevelError, \"Unexpected error for transfer, path: %q, error: \\\"%v\\\" bytes sent: %v, \"+\n\t\t\"bytes received: %v transfer running since %v ms\", t.fsPath, t.ErrTransfer, t.BytesSent.Load(),\n\t\tt.BytesReceived.Load(), elapsed)\n}\n\nfunc (t *BaseTransfer) getUploadFileSize() (int64, int, error) {\n\tvar fileSize int64\n\tvar deletedFiles int\n\n\tswitch dataprovider.GetQuotaTracking() {\n\tcase 0:\n\t\treturn fileSize, deletedFiles, errors.New(\"quota tracking disabled\")\n\tcase 2:\n\t\tif !t.Connection.User.HasQuotaRestrictions() {\n\t\t\tvfolder, err := t.Connection.User.GetVirtualFolderForPath(path.Dir(t.requestPath))\n\t\t\tif err != nil {\n\t\t\t\treturn fileSize, deletedFiles, errors.New(\"quota tracking disabled for this user\")\n\t\t\t}\n\t\t\tif vfolder.IsIncludedInUserQuota() {\n\t\t\t\treturn fileSize, deletedFiles, errors.New(\"quota tracking disabled for this user and folder included in user quota\")\n\t\t\t}\n\t\t}\n\t}\n\n\tinfo, err := t.Fs.Stat(t.fsPath)\n\tif err == nil {\n\t\tfileSize = info.Size()\n\t}\n\tif t.ErrTransfer != nil && vfs.IsCryptOsFs(t.Fs) {\n\t\terrDelete := t.Fs.Remove(t.fsPath, false)\n\t\tif errDelete != nil {\n\t\t\tt.Connection.Log(logger.LevelWarn, \"error removing partial crypto file %q: %v\", t.fsPath, errDelete)\n\t\t} else {\n\t\t\tfileSize = 0\n\t\t\tdeletedFiles = 1\n\t\t\tt.BytesReceived.Store(0)\n\t\t\tt.MinWriteOffset = 0\n\t\t}\n\t}\n\treturn fileSize, deletedFiles, err\n}\n\n// return 1 if the file is outside the user home dir\nfunc (t *BaseTransfer) checkUploadOutsideHomeDir(err error) int {\n\tif err == nil {\n\t\treturn 0\n\t}\n\tif t.ErrTransfer == nil {\n\t\tt.ErrTransfer = err\n\t}\n\tif Config.TempPath == \"\" {\n\t\treturn 0\n\t}\n\terr = t.Fs.Remove(t.effectiveFsPath, false)\n\tt.Connection.Log(logger.LevelWarn, \"upload in temp path cannot be renamed, delete temporary file: %q, deletion error: %v\",\n\t\tt.effectiveFsPath, err)\n\t// the file is outside the home dir so don't update the quota\n\tt.BytesReceived.Store(0)\n\tt.MinWriteOffset = 0\n\treturn 1\n}\n\n// Close it is called when the transfer is completed.\n// It logs the transfer info, updates the user quota (for uploads)\n// and executes any defined action.\n// If there is an error no action will be executed and, in atomic mode,\n// we try to delete the temporary file\nfunc (t *BaseTransfer) Close() error {\n\tdefer t.Connection.RemoveTransfer(t)\n\n\tvar err error\n\tnumFiles := t.getUploadedFiles()\n\tmetric.TransferCompleted(t.BytesSent.Load(), t.BytesReceived.Load(),\n\t\tt.transferType, t.ErrTransfer, vfs.IsSFTPFs(t.Fs))\n\tif t.transferQuota.HasSizeLimits() {\n\t\tdataprovider.UpdateUserTransferQuota(&t.Connection.User, t.BytesReceived.Load(), //nolint:errcheck\n\t\t\tt.BytesSent.Load(), false)\n\t}\n\tif (t.File != nil || vfs.IsLocalOsFs(t.Fs)) && t.Connection.IsQuotaExceededError(t.ErrTransfer) {\n\t\t// if quota is exceeded we try to remove the partial file for uploads to local filesystem\n\t\terr = t.Fs.Remove(t.effectiveFsPath, false)\n\t\tif err == nil {\n\t\t\tt.BytesReceived.Store(0)\n\t\t\tt.MinWriteOffset = 0\n\t\t}\n\t\tt.Connection.Log(logger.LevelWarn, \"upload denied due to space limit, delete temporary file: %q, deletion error: %v\",\n\t\t\tt.effectiveFsPath, err)\n\t} else if t.isAtomicUpload() {\n\t\tif t.ErrTransfer == nil || Config.UploadMode&UploadModeAtomicWithResume != 0 {\n\t\t\t_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath, 0)\n\t\t\tt.Connection.Log(logger.LevelDebug, \"atomic upload completed, rename: %q -> %q, error: %v\",\n\t\t\t\tt.effectiveFsPath, t.fsPath, err)\n\t\t\t// the file must be removed if it is uploaded to a path outside the home dir and cannot be renamed\n\t\t\tt.checkUploadOutsideHomeDir(err)\n\t\t} else {\n\t\t\terr = t.Fs.Remove(t.effectiveFsPath, false)\n\t\t\tt.Connection.Log(logger.LevelWarn, \"atomic upload completed with error: \\\"%v\\\", delete temporary file: %q, deletion error: %v\",\n\t\t\t\tt.ErrTransfer, t.effectiveFsPath, err)\n\t\t\tif err == nil {\n\t\t\t\tt.BytesReceived.Store(0)\n\t\t\t\tt.MinWriteOffset = 0\n\t\t\t}\n\t\t}\n\t}\n\telapsed := time.Since(t.start).Nanoseconds() / 1000000\n\tvar uploadFileSize int64\n\tif t.transferType == TransferDownload {\n\t\tlogger.TransferLog(downloadLogSender, t.fsPath, elapsed, t.BytesSent.Load(), t.Connection.User.Username,\n\t\t\tt.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode,\n\t\t\tt.ErrTransfer)\n\t\tExecuteActionNotification(t.Connection, operationDownload, t.fsPath, t.requestPath, \"\", \"\", \"\", //nolint:errcheck\n\t\t\tt.BytesSent.Load(), t.ErrTransfer, elapsed, t.metadata)\n\t} else {\n\t\tstatSize, deletedFiles, errStat := t.getUploadFileSize()\n\t\tif errStat == nil {\n\t\t\tuploadFileSize = statSize\n\t\t} else {\n\t\t\tuploadFileSize = t.BytesReceived.Load() + t.MinWriteOffset\n\t\t\tif t.Fs.IsNotExist(errStat) {\n\t\t\t\tuploadFileSize = 0\n\t\t\t\tnumFiles--\n\t\t\t}\n\t\t}\n\t\tnumFiles -= deletedFiles\n\t\tt.Connection.Log(logger.LevelDebug, \"upload file size %d, num files %d, deleted files %d, fs path %q\",\n\t\t\tuploadFileSize, numFiles, deletedFiles, t.fsPath)\n\t\tnumFiles, uploadFileSize = t.executeUploadHook(numFiles, uploadFileSize, elapsed)\n\t\tt.updateQuota(numFiles, uploadFileSize)\n\t\tt.updateTimes()\n\t\tlogger.TransferLog(uploadLogSender, t.fsPath, elapsed, t.BytesReceived.Load(), t.Connection.User.Username,\n\t\t\tt.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode,\n\t\t\tt.ErrTransfer)\n\t}\n\tif t.ErrTransfer != nil {\n\t\tt.Connection.Log(logger.LevelError, \"transfer error: %v, path: %q\", t.ErrTransfer, t.fsPath)\n\t\tif err == nil {\n\t\t\terr = t.ErrTransfer\n\t\t}\n\t}\n\tt.updateTransferTimestamps(uploadFileSize, elapsed)\n\treturn err\n}\n\nfunc (t *BaseTransfer) isAtomicUpload() bool {\n\treturn t.transferType == TransferUpload && t.effectiveFsPath != t.fsPath\n}\n\nfunc (t *BaseTransfer) updateTransferTimestamps(uploadFileSize, elapsed int64) {\n\tif t.ErrTransfer != nil {\n\t\treturn\n\t}\n\tif t.transferType == TransferUpload {\n\t\tif t.Connection.User.FirstUpload == 0 && !t.Connection.uploadDone.Load() {\n\t\t\tif err := dataprovider.UpdateUserTransferTimestamps(t.Connection.User.Username, true); err == nil {\n\t\t\t\tt.Connection.uploadDone.Store(true)\n\t\t\t\tExecuteActionNotification(t.Connection, operationFirstUpload, t.fsPath, t.requestPath, \"\", //nolint:errcheck\n\t\t\t\t\t\"\", \"\", uploadFileSize, t.ErrTransfer, elapsed, t.metadata)\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\tif t.Connection.User.FirstDownload == 0 && !t.Connection.downloadDone.Load() && t.BytesSent.Load() > 0 {\n\t\tif err := dataprovider.UpdateUserTransferTimestamps(t.Connection.User.Username, false); err == nil {\n\t\t\tt.Connection.downloadDone.Store(true)\n\t\t\tExecuteActionNotification(t.Connection, operationFirstDownload, t.fsPath, t.requestPath, \"\", //nolint:errcheck\n\t\t\t\t\"\", \"\", t.BytesSent.Load(), t.ErrTransfer, elapsed, t.metadata)\n\t\t}\n\t}\n}\n\nfunc (t *BaseTransfer) executeUploadHook(numFiles int, fileSize, elapsed int64) (int, int64) {\n\terr := ExecuteActionNotification(t.Connection, operationUpload, t.fsPath, t.requestPath, \"\", \"\", \"\",\n\t\tfileSize, t.ErrTransfer, elapsed, t.metadata)\n\tif err != nil {\n\t\tif t.ErrTransfer == nil {\n\t\t\tt.ErrTransfer = err\n\t\t}\n\t\t// try to remove the uploaded file\n\t\terr = t.Fs.Remove(t.fsPath, false)\n\t\tif err == nil {\n\t\t\tnumFiles--\n\t\t\tfileSize = 0\n\t\t\tt.BytesReceived.Store(0)\n\t\t\tt.MinWriteOffset = 0\n\t\t} else {\n\t\t\tt.Connection.Log(logger.LevelWarn, \"unable to remove path %q after upload hook failure: %v\", t.fsPath, err)\n\t\t}\n\t}\n\treturn numFiles, fileSize\n}\n\nfunc (t *BaseTransfer) getUploadedFiles() int {\n\tnumFiles := 0\n\tif t.isNewFile {\n\t\tnumFiles = 1\n\t}\n\treturn numFiles\n}\n\nfunc (t *BaseTransfer) updateTimes() {\n\tif !t.aTime.IsZero() && !t.mTime.IsZero() {\n\t\terr := t.Fs.Chtimes(t.fsPath, t.aTime, t.mTime, false)\n\t\tt.Connection.Log(logger.LevelDebug, \"set times for file %q, atime: %v, mtime: %v, err: %v\",\n\t\t\tt.fsPath, t.aTime, t.mTime, err)\n\t}\n}\n\nfunc (t *BaseTransfer) updateQuota(numFiles int, fileSize int64) bool {\n\t// Uploads on some filesystem (S3 and similar) are atomic, if there is an error nothing is uploaded\n\tif t.File == nil && t.ErrTransfer != nil && vfs.HasImplicitAtomicUploads(t.Fs) {\n\t\treturn false\n\t}\n\tsizeDiff := fileSize - t.InitialSize\n\tif t.transferType == TransferUpload && (numFiles != 0 || sizeDiff != 0) {\n\t\tvfolder, err := t.Connection.User.GetVirtualFolderForPath(path.Dir(t.requestPath))\n\t\tif err == nil {\n\t\t\tdataprovider.UpdateUserFolderQuota(&vfolder, &t.Connection.User, numFiles,\n\t\t\t\tsizeDiff, false)\n\t\t} else {\n\t\t\tdataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n\n// HandleThrottle manage bandwidth throttling\nfunc (t *BaseTransfer) HandleThrottle() {\n\tvar wantedBandwidth int64\n\tvar trasferredBytes int64\n\tif t.transferType == TransferDownload {\n\t\twantedBandwidth = t.Connection.User.DownloadBandwidth\n\t\ttrasferredBytes = t.BytesSent.Load()\n\t} else {\n\t\twantedBandwidth = t.Connection.User.UploadBandwidth\n\t\ttrasferredBytes = t.BytesReceived.Load()\n\t}\n\tif wantedBandwidth > 0 {\n\t\t// real and wanted elapsed as milliseconds, bytes as kilobytes\n\t\trealElapsed := time.Since(t.start).Nanoseconds() / 1000000\n\t\t// trasferredBytes / 1024 = KB/s, we multiply for 1000 to get milliseconds\n\t\twantedElapsed := 1000 * (trasferredBytes / 1024) / wantedBandwidth\n\t\tif wantedElapsed > realElapsed {\n\t\t\ttoSleep := time.Duration(wantedElapsed - realElapsed)\n\t\t\ttime.Sleep(toSleep * time.Millisecond)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/common/transfer_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/sftp\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nfunc TestTransferUpdateQuota(t *testing.T) {\n\tconn := NewBaseConnection(\"\", ProtocolSFTP, \"\", \"\", dataprovider.User{})\n\ttransfer := BaseTransfer{\n\t\tConnection:   conn,\n\t\ttransferType: TransferUpload,\n\t\tFs:           vfs.NewOsFs(\"\", os.TempDir(), \"\", nil),\n\t}\n\ttransfer.BytesReceived.Store(123)\n\terrFake := errors.New(\"fake error\")\n\ttransfer.TransferError(errFake)\n\terr := transfer.Close()\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, errFake.Error())\n\t}\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tvdirPath := \"/vdir\"\n\tconn.User.VirtualFolders = append(conn.User.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\ttransfer.ErrTransfer = nil\n\ttransfer.BytesReceived.Store(1)\n\ttransfer.requestPath = \"/vdir/file\"\n\tassert.True(t, transfer.updateQuota(1, 0))\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n\n\ttransfer.ErrTransfer = errFake\n\ttransfer.Fs = newMockOsFs(true, \"\", \"\", \"S3Fs fake\", nil)\n\tassert.False(t, transfer.updateQuota(1, 0))\n}\n\nfunc TestTransferThrottling(t *testing.T) {\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:          \"test\",\n\t\t\tUploadBandwidth:   50,\n\t\t\tDownloadBandwidth: 40,\n\t\t},\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\ttestFileSize := int64(131072)\n\twantedUploadElapsed := 1000 * (testFileSize / 1024) / u.UploadBandwidth\n\twantedDownloadElapsed := 1000 * (testFileSize / 1024) / u.DownloadBandwidth\n\t// some tolerance\n\twantedUploadElapsed -= wantedDownloadElapsed / 10\n\twantedDownloadElapsed -= wantedDownloadElapsed / 10\n\tconn := NewBaseConnection(\"id\", ProtocolSCP, \"\", \"\", u)\n\ttransfer := NewBaseTransfer(nil, conn, nil, \"\", \"\", \"\", TransferUpload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\ttransfer.BytesReceived.Store(testFileSize)\n\ttransfer.Connection.UpdateLastActivity()\n\tstartTime := transfer.Connection.GetLastActivity()\n\ttransfer.HandleThrottle()\n\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\tassert.GreaterOrEqual(t, elapsed, wantedUploadElapsed, \"upload bandwidth throttling not respected\")\n\terr := transfer.Close()\n\tassert.NoError(t, err)\n\n\ttransfer = NewBaseTransfer(nil, conn, nil, \"\", \"\", \"\", TransferDownload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\ttransfer.BytesSent.Store(testFileSize)\n\ttransfer.Connection.UpdateLastActivity()\n\tstartTime = transfer.Connection.GetLastActivity()\n\n\ttransfer.HandleThrottle()\n\telapsed = time.Since(startTime).Nanoseconds() / 1000000\n\tassert.GreaterOrEqual(t, elapsed, wantedDownloadElapsed, \"download bandwidth throttling not respected\")\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n}\n\nfunc TestRealPath(t *testing.T) {\n\ttestFile := filepath.Join(os.TempDir(), \"afile.txt\")\n\tfs := vfs.NewOsFs(\"123\", os.TempDir(), \"\", nil)\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"user\",\n\t\t\tHomeDir:  os.TempDir(),\n\t\t},\n\t}\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfile, err := os.Create(testFile)\n\trequire.NoError(t, err)\n\tconn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, \"\", \"\", u)\n\ttransfer := NewBaseTransfer(file, conn, nil, testFile, testFile, \"/transfer_test_file\",\n\t\tTransferUpload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\trPath := transfer.GetRealFsPath(testFile)\n\tassert.Equal(t, testFile, rPath)\n\trPath = conn.getRealFsPath(testFile)\n\tassert.Equal(t, testFile, rPath)\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n\terr = file.Close()\n\tassert.NoError(t, err)\n\ttransfer.File = nil\n\trPath = transfer.GetRealFsPath(testFile)\n\tassert.Equal(t, testFile, rPath)\n\trPath = transfer.GetRealFsPath(\"\")\n\tassert.Empty(t, rPath)\n\terr = os.Remove(testFile)\n\tassert.NoError(t, err)\n\tassert.Len(t, conn.GetTransfers(), 0)\n}\n\nfunc TestTruncate(t *testing.T) {\n\ttestFile := filepath.Join(os.TempDir(), \"transfer_test_file\")\n\tfs := vfs.NewOsFs(\"123\", os.TempDir(), \"\", nil)\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"user\",\n\t\t\tHomeDir:  os.TempDir(),\n\t\t},\n\t}\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfile, err := os.Create(testFile)\n\tif !assert.NoError(t, err) {\n\t\tassert.FailNow(t, \"unable to open test file\")\n\t}\n\t_, err = file.Write([]byte(\"hello\"))\n\tassert.NoError(t, err)\n\tconn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, \"\", \"\", u)\n\ttransfer := NewBaseTransfer(file, conn, nil, testFile, testFile, \"/transfer_test_file\", TransferUpload, 0, 5,\n\t\t100, 0, false, fs, dataprovider.TransferQuota{})\n\n\terr = conn.SetStat(\"/transfer_test_file\", &StatAttributes{\n\t\tSize:  2,\n\t\tFlags: StatAttrSize,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(103), transfer.MaxWriteSize)\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n\terr = file.Close()\n\tassert.NoError(t, err)\n\tfi, err := os.Stat(testFile)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, int64(2), fi.Size())\n\t}\n\n\ttransfer = NewBaseTransfer(file, conn, nil, testFile, testFile, \"/transfer_test_file\", TransferUpload, 0, 0,\n\t\t100, 0, true, fs, dataprovider.TransferQuota{})\n\t// file.Stat will fail on a closed file\n\terr = conn.SetStat(\"/transfer_test_file\", &StatAttributes{\n\t\tSize:  2,\n\t\tFlags: StatAttrSize,\n\t})\n\tassert.Error(t, err)\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n\n\ttransfer = NewBaseTransfer(nil, conn, nil, testFile, testFile, \"\", TransferUpload, 0, 0, 0, 0, true,\n\t\tfs, dataprovider.TransferQuota{})\n\t_, err = transfer.Truncate(\"mismatch\", 0)\n\tassert.EqualError(t, err, errTransferMismatch.Error())\n\t_, err = transfer.Truncate(testFile, 0)\n\tassert.NoError(t, err)\n\t_, err = transfer.Truncate(testFile, 1)\n\tassert.EqualError(t, err, vfs.ErrVfsUnsupported.Error())\n\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n\n\terr = os.Remove(testFile)\n\tassert.NoError(t, err)\n\n\tassert.Len(t, conn.GetTransfers(), 0)\n}\n\nfunc TestTransferErrors(t *testing.T) {\n\tisCancelled := false\n\tcancelFn := func() {\n\t\tisCancelled = true\n\t}\n\ttestFile := filepath.Join(os.TempDir(), \"transfer_test_file\")\n\tfs := vfs.NewOsFs(\"id\", os.TempDir(), \"\", nil)\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"test\",\n\t\t\tHomeDir:  os.TempDir(),\n\t\t},\n\t}\n\terr := os.WriteFile(testFile, []byte(\"test data\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tfile, err := os.Open(testFile)\n\tif !assert.NoError(t, err) {\n\t\tassert.FailNow(t, \"unable to open test file\")\n\t}\n\tconn := NewBaseConnection(\"id\", ProtocolSFTP, \"\", \"\", u)\n\ttransfer := NewBaseTransfer(file, conn, nil, testFile, testFile, \"/transfer_test_file\", TransferUpload,\n\t\t0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\tpathError := &os.PathError{\n\t\tOp:   \"test\",\n\t\tPath: testFile,\n\t\tErr:  os.ErrInvalid,\n\t}\n\terr = transfer.ConvertError(pathError)\n\tassert.EqualError(t, err, fmt.Sprintf(\"%s %s: %s\", pathError.Op, \"/transfer_test_file\", pathError.Err.Error()))\n\terr = transfer.ConvertError(os.ErrNotExist)\n\tassert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)\n\terr = transfer.ConvertError(os.ErrPermission)\n\tassert.ErrorIs(t, err, sftp.ErrSSHFxPermissionDenied)\n\tassert.Nil(t, transfer.cancelFn)\n\tassert.Equal(t, testFile, transfer.GetFsPath())\n\ttransfer.SetMetadata(map[string]string{\"key\": \"val\"})\n\ttransfer.SetCancelFn(cancelFn)\n\terrFake := errors.New(\"err fake\")\n\ttransfer.BytesReceived.Store(9)\n\ttransfer.TransferError(ErrQuotaExceeded)\n\tassert.True(t, isCancelled)\n\ttransfer.TransferError(errFake)\n\tassert.Error(t, transfer.ErrTransfer, ErrQuotaExceeded.Error())\n\t// the file is closed from the embedding struct before to call close\n\terr = file.Close()\n\tassert.NoError(t, err)\n\terr = transfer.Close()\n\tif assert.Error(t, err) {\n\t\tassert.Error(t, err, ErrQuotaExceeded.Error())\n\t}\n\tassert.NoFileExists(t, testFile)\n\n\terr = os.WriteFile(testFile, []byte(\"test data\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tfile, err = os.Open(testFile)\n\tif !assert.NoError(t, err) {\n\t\tassert.FailNow(t, \"unable to open test file\")\n\t}\n\tfsPath := filepath.Join(os.TempDir(), \"test_file\")\n\ttransfer = NewBaseTransfer(file, conn, nil, fsPath, file.Name(), \"/test_file\", TransferUpload, 0, 0, 0, 0, true,\n\t\tfs, dataprovider.TransferQuota{})\n\ttransfer.BytesReceived.Store(9)\n\ttransfer.TransferError(errFake)\n\tassert.Error(t, transfer.ErrTransfer, errFake.Error())\n\t// the file is closed from the embedding struct before to call close\n\terr = file.Close()\n\tassert.NoError(t, err)\n\terr = transfer.Close()\n\tif assert.Error(t, err) {\n\t\tassert.Error(t, err, errFake.Error())\n\t}\n\tassert.NoFileExists(t, testFile)\n\n\terr = os.WriteFile(testFile, []byte(\"test data\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tfile, err = os.Open(testFile)\n\tif !assert.NoError(t, err) {\n\t\tassert.FailNow(t, \"unable to open test file\")\n\t}\n\ttransfer = NewBaseTransfer(file, conn, nil, fsPath, file.Name(), \"/test_file\", TransferUpload, 0, 0, 0, 0, true,\n\t\tfs, dataprovider.TransferQuota{})\n\ttransfer.BytesReceived.Store(9)\n\t// the file is closed from the embedding struct before to call close\n\terr = file.Close()\n\tassert.NoError(t, err)\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n\tassert.NoFileExists(t, testFile)\n\tassert.FileExists(t, fsPath)\n\terr = os.Remove(fsPath)\n\tassert.NoError(t, err)\n\n\tassert.Len(t, conn.GetTransfers(), 0)\n}\n\nfunc TestRemovePartialCryptoFile(t *testing.T) {\n\ttestFile := filepath.Join(os.TempDir(), \"transfer_test_file\")\n\tfs, err := vfs.NewCryptFs(\"id\", os.TempDir(), \"\", vfs.CryptFsConfig{Passphrase: kms.NewPlainSecret(\"secret\")})\n\trequire.NoError(t, err)\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:   \"test\",\n\t\t\tHomeDir:    os.TempDir(),\n\t\t\tQuotaFiles: 1000000,\n\t\t},\n\t}\n\tconn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, \"\", \"\", u)\n\ttransfer := NewBaseTransfer(nil, conn, nil, testFile, testFile, \"/transfer_test_file\", TransferUpload,\n\t\t0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\ttransfer.ErrTransfer = errors.New(\"test error\")\n\t_, _, err = transfer.getUploadFileSize()\n\tassert.Error(t, err)\n\terr = os.WriteFile(testFile, []byte(\"test data\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tsize, deletedFiles, err := transfer.getUploadFileSize()\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), size)\n\tassert.Equal(t, 1, deletedFiles)\n\tassert.NoFileExists(t, testFile)\n\terr = transfer.Close()\n\tassert.Error(t, err)\n\tassert.Len(t, conn.GetTransfers(), 0)\n}\n\nfunc TestFTPMode(t *testing.T) {\n\tconn := NewBaseConnection(\"\", ProtocolFTP, \"\", \"\", dataprovider.User{})\n\ttransfer := BaseTransfer{\n\t\tConnection:   conn,\n\t\ttransferType: TransferUpload,\n\t\tFs:           vfs.NewOsFs(\"\", os.TempDir(), \"\", nil),\n\t}\n\ttransfer.BytesReceived.Store(123)\n\tassert.Empty(t, transfer.ftpMode)\n\ttransfer.SetFtpMode(\"active\")\n\tassert.Equal(t, \"active\", transfer.ftpMode)\n}\n\nfunc TestTransferQuota(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tTotalDataTransfer:    3,\n\t\t\tUploadDataTransfer:   2,\n\t\t\tDownloadDataTransfer: 1,\n\t\t},\n\t}\n\tul, dl, total := user.GetDataTransferLimits()\n\tassert.Equal(t, int64(2*1048576), ul)\n\tassert.Equal(t, int64(1*1048576), dl)\n\tassert.Equal(t, int64(3*1048576), total)\n\tuser.TotalDataTransfer = -1\n\tuser.UploadDataTransfer = -1\n\tuser.DownloadDataTransfer = -1\n\tul, dl, total = user.GetDataTransferLimits()\n\tassert.Equal(t, int64(0), ul)\n\tassert.Equal(t, int64(0), dl)\n\tassert.Equal(t, int64(0), total)\n\ttransferQuota := dataprovider.TransferQuota{}\n\tassert.True(t, transferQuota.HasDownloadSpace())\n\tassert.True(t, transferQuota.HasUploadSpace())\n\ttransferQuota.TotalSize = -1\n\ttransferQuota.ULSize = -1\n\ttransferQuota.DLSize = -1\n\tassert.True(t, transferQuota.HasDownloadSpace())\n\tassert.True(t, transferQuota.HasUploadSpace())\n\ttransferQuota.TotalSize = 100\n\ttransferQuota.AllowedTotalSize = 10\n\tassert.True(t, transferQuota.HasDownloadSpace())\n\tassert.True(t, transferQuota.HasUploadSpace())\n\ttransferQuota.AllowedTotalSize = 0\n\tassert.False(t, transferQuota.HasDownloadSpace())\n\tassert.False(t, transferQuota.HasUploadSpace())\n\ttransferQuota.TotalSize = 0\n\ttransferQuota.DLSize = 100\n\ttransferQuota.ULSize = 50\n\ttransferQuota.AllowedTotalSize = 0\n\tassert.False(t, transferQuota.HasDownloadSpace())\n\tassert.False(t, transferQuota.HasUploadSpace())\n\ttransferQuota.AllowedDLSize = 1\n\ttransferQuota.AllowedULSize = 1\n\tassert.True(t, transferQuota.HasDownloadSpace())\n\tassert.True(t, transferQuota.HasUploadSpace())\n\ttransferQuota.AllowedDLSize = -10\n\ttransferQuota.AllowedULSize = -1\n\tassert.False(t, transferQuota.HasDownloadSpace())\n\tassert.False(t, transferQuota.HasUploadSpace())\n\n\tconn := NewBaseConnection(\"\", ProtocolSFTP, \"\", \"\", user)\n\ttransfer := NewBaseTransfer(nil, conn, nil, \"file.txt\", \"file.txt\", \"/transfer_test_file\", TransferUpload,\n\t\t0, 0, 0, 0, true, vfs.NewOsFs(\"\", os.TempDir(), \"\", nil), dataprovider.TransferQuota{})\n\terr := transfer.CheckRead()\n\tassert.NoError(t, err)\n\terr = transfer.CheckWrite()\n\tassert.NoError(t, err)\n\n\ttransfer.transferQuota = dataprovider.TransferQuota{\n\t\tAllowedTotalSize: 10,\n\t}\n\ttransfer.BytesReceived.Store(5)\n\ttransfer.BytesSent.Store(4)\n\terr = transfer.CheckRead()\n\tassert.NoError(t, err)\n\terr = transfer.CheckWrite()\n\tassert.NoError(t, err)\n\n\ttransfer.BytesSent.Store(6)\n\terr = transfer.CheckRead()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), ErrReadQuotaExceeded.Error())\n\t}\n\terr = transfer.CheckWrite()\n\tassert.True(t, conn.IsQuotaExceededError(err))\n\n\ttransferQuota = dataprovider.TransferQuota{\n\t\tAllowedTotalSize: 0,\n\t\tAllowedULSize:    10,\n\t\tAllowedDLSize:    5,\n\t}\n\ttransfer.transferQuota = transferQuota\n\tassert.Equal(t, transferQuota, transfer.GetTransferQuota())\n\terr = transfer.CheckRead()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), ErrReadQuotaExceeded.Error())\n\t}\n\terr = transfer.CheckWrite()\n\tassert.NoError(t, err)\n\n\ttransfer.BytesReceived.Store(11)\n\terr = transfer.CheckRead()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), ErrReadQuotaExceeded.Error())\n\t}\n\terr = transfer.CheckWrite()\n\tassert.True(t, conn.IsQuotaExceededError(err))\n\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n\tassert.Len(t, conn.GetTransfers(), 0)\n\tassert.Equal(t, int32(0), Connections.GetTotalTransfers())\n}\n\nfunc TestUploadOutsideHomeRenameError(t *testing.T) {\n\toldTempPath := Config.TempPath\n\n\tconn := NewBaseConnection(\"\", ProtocolSFTP, \"\", \"\", dataprovider.User{})\n\ttransfer := BaseTransfer{\n\t\tConnection:   conn,\n\t\ttransferType: TransferUpload,\n\t\tFs:           vfs.NewOsFs(\"\", filepath.Join(os.TempDir(), \"home\"), \"\", nil),\n\t}\n\ttransfer.BytesReceived.Store(123)\n\n\tfileName := filepath.Join(os.TempDir(), \"_temp\")\n\terr := os.WriteFile(fileName, []byte(`data`), 0644)\n\tassert.NoError(t, err)\n\n\ttransfer.effectiveFsPath = fileName\n\tres := transfer.checkUploadOutsideHomeDir(os.ErrPermission)\n\tassert.Equal(t, 0, res)\n\n\tConfig.TempPath = filepath.Clean(os.TempDir())\n\tres = transfer.checkUploadOutsideHomeDir(nil)\n\tassert.Equal(t, 0, res)\n\tassert.Greater(t, transfer.BytesReceived.Load(), int64(0))\n\tres = transfer.checkUploadOutsideHomeDir(os.ErrPermission)\n\tassert.Equal(t, 1, res)\n\tassert.Equal(t, int64(0), transfer.BytesReceived.Load())\n\tassert.NoFileExists(t, fileName)\n\n\tConfig.TempPath = oldTempPath\n}\n"
  },
  {
    "path": "internal/common/transferschecker.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"errors\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\ntype overquotaTransfer struct {\n\tConnID       string\n\tTransferID   int64\n\tTransferType int\n}\n\ntype uploadAggregationKey struct {\n\tUsername   string\n\tFolderName string\n}\n\n// TransfersChecker defines the interface that transfer checkers must implement.\n// A transfer checker ensure that multiple concurrent transfers does not exceeded\n// the remaining user quota\ntype TransfersChecker interface {\n\tAddTransfer(transfer dataprovider.ActiveTransfer)\n\tRemoveTransfer(ID int64, connectionID string)\n\tUpdateTransferCurrentSizes(ulSize, dlSize, ID int64, connectionID string)\n\tGetOverquotaTransfers() []overquotaTransfer\n}\n\nfunc getTransfersChecker(isShared int) TransfersChecker {\n\tif isShared == 1 {\n\t\tlogger.Info(logSender, \"\", \"using provider transfer checker\")\n\t\treturn &transfersCheckerDB{}\n\t}\n\tlogger.Info(logSender, \"\", \"using memory transfer checker\")\n\treturn &transfersCheckerMem{}\n}\n\ntype baseTransferChecker struct {\n\ttransfers []dataprovider.ActiveTransfer\n}\n\nfunc (t *baseTransferChecker) isDataTransferExceeded(user dataprovider.User, transfer dataprovider.ActiveTransfer, ulSize,\n\tdlSize int64,\n) bool {\n\tulQuota, dlQuota, totalQuota := user.GetDataTransferLimits()\n\tif totalQuota > 0 {\n\t\tallowedSize := totalQuota - (user.UsedUploadDataTransfer + user.UsedDownloadDataTransfer)\n\t\tif ulSize+dlSize > allowedSize {\n\t\t\treturn transfer.CurrentDLSize > 0 || transfer.CurrentULSize > 0\n\t\t}\n\t}\n\tif dlQuota > 0 {\n\t\tallowedSize := dlQuota - user.UsedDownloadDataTransfer\n\t\tif dlSize > allowedSize {\n\t\t\treturn transfer.CurrentDLSize > 0\n\t\t}\n\t}\n\tif ulQuota > 0 {\n\t\tallowedSize := ulQuota - user.UsedUploadDataTransfer\n\t\tif ulSize > allowedSize {\n\t\t\treturn transfer.CurrentULSize > 0\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (t *baseTransferChecker) getRemainingDiskQuota(user dataprovider.User, folderName string) (int64, error) {\n\tvar result int64\n\n\tif folderName != \"\" {\n\t\tfor _, folder := range user.VirtualFolders {\n\t\t\tif folder.Name == folderName {\n\t\t\t\tif folder.QuotaSize > 0 {\n\t\t\t\t\treturn folder.QuotaSize - folder.UsedQuotaSize, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif user.QuotaSize > 0 {\n\t\t\treturn user.QuotaSize - user.UsedQuotaSize, nil\n\t\t}\n\t}\n\n\treturn result, errors.New(\"no quota limit defined\")\n}\n\nfunc (t *baseTransferChecker) aggregateTransfersByUser(usersToFetch map[string]bool,\n) (map[string]bool, map[string][]dataprovider.ActiveTransfer) {\n\taggregations := make(map[string][]dataprovider.ActiveTransfer)\n\tfor _, transfer := range t.transfers {\n\t\taggregations[transfer.Username] = append(aggregations[transfer.Username], transfer)\n\t\tif len(aggregations[transfer.Username]) > 1 {\n\t\t\tif _, ok := usersToFetch[transfer.Username]; !ok {\n\t\t\t\tusersToFetch[transfer.Username] = false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn usersToFetch, aggregations\n}\n\nfunc (t *baseTransferChecker) aggregateUploadTransfers() (map[string]bool, map[int][]dataprovider.ActiveTransfer) {\n\tusersToFetch := make(map[string]bool)\n\taggregations := make(map[int][]dataprovider.ActiveTransfer)\n\tvar keys []uploadAggregationKey\n\n\tfor _, transfer := range t.transfers {\n\t\tif transfer.Type != TransferUpload {\n\t\t\tcontinue\n\t\t}\n\t\tkey := -1\n\t\tfor idx, k := range keys {\n\t\t\tif k.Username == transfer.Username && k.FolderName == transfer.FolderName {\n\t\t\t\tkey = idx\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif key == -1 {\n\t\t\tkey = len(keys)\n\t\t}\n\t\tkeys = append(keys, uploadAggregationKey{\n\t\t\tUsername:   transfer.Username,\n\t\t\tFolderName: transfer.FolderName,\n\t\t})\n\n\t\taggregations[key] = append(aggregations[key], transfer)\n\t\tif len(aggregations[key]) > 1 {\n\t\t\tif transfer.FolderName != \"\" {\n\t\t\t\tusersToFetch[transfer.Username] = true\n\t\t\t} else {\n\t\t\t\tif _, ok := usersToFetch[transfer.Username]; !ok {\n\t\t\t\t\tusersToFetch[transfer.Username] = false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn usersToFetch, aggregations\n}\n\nfunc (t *baseTransferChecker) getUsersToCheck(usersToFetch map[string]bool) (map[string]dataprovider.User, error) {\n\tusers, err := dataprovider.GetUsersForQuotaCheck(usersToFetch)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tusersMap := make(map[string]dataprovider.User)\n\n\tfor _, user := range users {\n\t\tusersMap[user.Username] = user\n\t}\n\n\treturn usersMap, nil\n}\n\nfunc (t *baseTransferChecker) getOverquotaTransfers(usersToFetch map[string]bool,\n\tuploadAggregations map[int][]dataprovider.ActiveTransfer,\n\tuserAggregations map[string][]dataprovider.ActiveTransfer,\n) []overquotaTransfer {\n\tif len(usersToFetch) == 0 {\n\t\treturn nil\n\t}\n\tusersMap, err := t.getUsersToCheck(usersToFetch)\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to check transfers, error getting users quota: %v\", err)\n\t\treturn nil\n\t}\n\n\tvar overquotaTransfers []overquotaTransfer\n\n\tfor _, transfers := range uploadAggregations {\n\t\tusername := transfers[0].Username\n\t\tfolderName := transfers[0].FolderName\n\t\tremaningDiskQuota, err := t.getRemainingDiskQuota(usersMap[username], folderName)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tvar usedDiskQuota int64\n\t\tfor _, tr := range transfers {\n\t\t\t// We optimistically assume that a cloud transfer that replaces an existing\n\t\t\t// file will be successful\n\t\t\tusedDiskQuota += tr.CurrentULSize - tr.TruncatedSize\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"username %q, folder %q, concurrent transfers: %v, remaining disk quota (bytes): %v, disk quota used in ongoing transfers (bytes): %v\",\n\t\t\tusername, folderName, len(transfers), remaningDiskQuota, usedDiskQuota)\n\t\tif usedDiskQuota > remaningDiskQuota {\n\t\t\tfor _, tr := range transfers {\n\t\t\t\tif tr.CurrentULSize > tr.TruncatedSize {\n\t\t\t\t\toverquotaTransfers = append(overquotaTransfers, overquotaTransfer{\n\t\t\t\t\t\tConnID:       tr.ConnID,\n\t\t\t\t\t\tTransferID:   tr.ID,\n\t\t\t\t\t\tTransferType: tr.Type,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor username, transfers := range userAggregations {\n\t\tvar ulSize, dlSize int64\n\t\tfor _, tr := range transfers {\n\t\t\tulSize += tr.CurrentULSize\n\t\t\tdlSize += tr.CurrentDLSize\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"username %q, concurrent transfers: %v, quota (bytes) used in ongoing transfers, ul: %v, dl: %v\",\n\t\t\tusername, len(transfers), ulSize, dlSize)\n\t\tfor _, tr := range transfers {\n\t\t\tif t.isDataTransferExceeded(usersMap[username], tr, ulSize, dlSize) {\n\t\t\t\toverquotaTransfers = append(overquotaTransfers, overquotaTransfer{\n\t\t\t\t\tConnID:       tr.ConnID,\n\t\t\t\t\tTransferID:   tr.ID,\n\t\t\t\t\tTransferType: tr.Type,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn overquotaTransfers\n}\n\ntype transfersCheckerMem struct {\n\tsync.RWMutex\n\tbaseTransferChecker\n}\n\nfunc (t *transfersCheckerMem) AddTransfer(transfer dataprovider.ActiveTransfer) {\n\tt.Lock()\n\tdefer t.Unlock()\n\n\tt.transfers = append(t.transfers, transfer)\n}\n\nfunc (t *transfersCheckerMem) RemoveTransfer(ID int64, connectionID string) {\n\tt.Lock()\n\tdefer t.Unlock()\n\n\tfor idx, transfer := range t.transfers {\n\t\tif transfer.ID == ID && transfer.ConnID == connectionID {\n\t\t\tlastIdx := len(t.transfers) - 1\n\t\t\tt.transfers[idx] = t.transfers[lastIdx]\n\t\t\tt.transfers = t.transfers[:lastIdx]\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (t *transfersCheckerMem) UpdateTransferCurrentSizes(ulSize, dlSize, ID int64, connectionID string) {\n\tt.Lock()\n\tdefer t.Unlock()\n\n\tfor idx := range t.transfers {\n\t\tif t.transfers[idx].ID == ID && t.transfers[idx].ConnID == connectionID {\n\t\t\tt.transfers[idx].CurrentDLSize = dlSize\n\t\t\tt.transfers[idx].CurrentULSize = ulSize\n\t\t\tt.transfers[idx].UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (t *transfersCheckerMem) GetOverquotaTransfers() []overquotaTransfer {\n\tt.RLock()\n\n\tusersToFetch, uploadAggregations := t.aggregateUploadTransfers()\n\tusersToFetch, userAggregations := t.aggregateTransfersByUser(usersToFetch)\n\n\tt.RUnlock()\n\n\treturn t.getOverquotaTransfers(usersToFetch, uploadAggregations, userAggregations)\n}\n\ntype transfersCheckerDB struct {\n\tbaseTransferChecker\n\tlastCleanup time.Time\n}\n\nfunc (t *transfersCheckerDB) AddTransfer(transfer dataprovider.ActiveTransfer) {\n\tdataprovider.AddActiveTransfer(transfer)\n}\n\nfunc (t *transfersCheckerDB) RemoveTransfer(ID int64, connectionID string) {\n\tdataprovider.RemoveActiveTransfer(ID, connectionID)\n}\n\nfunc (t *transfersCheckerDB) UpdateTransferCurrentSizes(ulSize, dlSize, ID int64, connectionID string) {\n\tdataprovider.UpdateActiveTransferSizes(ulSize, dlSize, ID, connectionID)\n}\n\nfunc (t *transfersCheckerDB) GetOverquotaTransfers() []overquotaTransfer {\n\tif t.lastCleanup.IsZero() || t.lastCleanup.Add(periodicTimeoutCheckInterval*15).Before(time.Now()) {\n\t\tbefore := time.Now().Add(-periodicTimeoutCheckInterval * 5)\n\t\terr := dataprovider.CleanupActiveTransfers(before)\n\t\tlogger.Debug(logSender, \"\", \"cleanup active transfers completed, err: %v\", err)\n\t\tif err == nil {\n\t\t\tt.lastCleanup = time.Now()\n\t\t}\n\t}\n\tvar err error\n\tfrom := time.Now().Add(-periodicTimeoutCheckInterval * 2)\n\tt.transfers, err = dataprovider.GetActiveTransfers(from)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to check overquota transfers, error getting active transfers: %v\", err)\n\t\treturn nil\n\t}\n\n\tusersToFetch, uploadAggregations := t.aggregateUploadTransfers()\n\tusersToFetch, userAggregations := t.aggregateTransfersByUser(usersToFetch)\n\n\treturn t.getOverquotaTransfers(usersToFetch, uploadAggregations, userAggregations)\n}\n"
  },
  {
    "path": "internal/common/transferschecker_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage common\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nfunc TestTransfersCheckerDiskQuota(t *testing.T) {\n\tusername := \"transfers_check_username\"\n\tfolderName := \"test_transfers_folder\"\n\tgroupName := \"test_transfers_group\"\n\tvdirPath := \"/vdir\"\n\tgroup := dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName: groupName,\n\t\t},\n\t\tUserSettings: dataprovider.GroupUserSettings{\n\t\t\tBaseGroupUserSettings: sdk.BaseGroupUserSettings{\n\t\t\t\tQuotaSize: 120,\n\t\t\t},\n\t\t},\n\t}\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: filepath.Join(os.TempDir(), folderName),\n\t}\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:  username,\n\t\t\tPassword:  \"testpwd\",\n\t\t\tHomeDir:   filepath.Join(os.TempDir(), username),\n\t\t\tStatus:    1,\n\t\t\tQuotaSize: 0, // the quota size defined for the group is used\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t\tVirtualFolders: []vfs.VirtualFolder{\n\t\t\t{\n\t\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\t\tName: folderName,\n\t\t\t\t},\n\t\t\t\tVirtualPath: vdirPath,\n\t\t\t\tQuotaSize:   100,\n\t\t\t},\n\t\t},\n\t\tGroups: []sdk.GroupMapping{\n\t\t\t{\n\t\t\t\tName: groupName,\n\t\t\t\tType: sdk.GroupTypePrimary,\n\t\t\t},\n\t\t},\n\t}\n\terr := dataprovider.AddGroup(&group, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tgroup, err = dataprovider.GroupExists(groupName)\n\tassert.NoError(t, err)\n\terr = dataprovider.AddFolder(&folder, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(120), group.UserSettings.QuotaSize)\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser, err = dataprovider.GetUserWithGroupSettings(username, \"\")\n\tassert.NoError(t, err)\n\n\tconnID1 := xid.New().String()\n\tfsUser, err := user.GetFilesystemForPath(\"/file1\", connID1)\n\tassert.NoError(t, err)\n\tconn1 := NewBaseConnection(connID1, ProtocolSFTP, \"\", \"\", user)\n\tfakeConn1 := &fakeConnection{\n\t\tBaseConnection: conn1,\n\t}\n\ttransfer1 := NewBaseTransfer(nil, conn1, nil, filepath.Join(user.HomeDir, \"file1\"), filepath.Join(user.HomeDir, \"file1\"),\n\t\t\"/file1\", TransferUpload, 0, 0, 120, 0, true, fsUser, dataprovider.TransferQuota{})\n\ttransfer1.BytesReceived.Store(150)\n\terr = Connections.Add(fakeConn1)\n\tassert.NoError(t, err)\n\t// the transferschecker will do nothing if there is only one ongoing transfer\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\n\tconnID2 := xid.New().String()\n\tconn2 := NewBaseConnection(connID2, ProtocolSFTP, \"\", \"\", user)\n\tfakeConn2 := &fakeConnection{\n\t\tBaseConnection: conn2,\n\t}\n\ttransfer2 := NewBaseTransfer(nil, conn2, nil, filepath.Join(user.HomeDir, \"file2\"), filepath.Join(user.HomeDir, \"file2\"),\n\t\t\"/file2\", TransferUpload, 0, 0, 120, 40, true, fsUser, dataprovider.TransferQuota{})\n\ttransfer1.BytesReceived.Store(50)\n\ttransfer2.BytesReceived.Store(60)\n\terr = Connections.Add(fakeConn2)\n\tassert.NoError(t, err)\n\n\tconnID3 := xid.New().String()\n\tconn3 := NewBaseConnection(connID3, ProtocolSFTP, \"\", \"\", user)\n\tfakeConn3 := &fakeConnection{\n\t\tBaseConnection: conn3,\n\t}\n\ttransfer3 := NewBaseTransfer(nil, conn3, nil, filepath.Join(user.HomeDir, \"file3\"), filepath.Join(user.HomeDir, \"file3\"),\n\t\t\"/file3\", TransferDownload, 0, 0, 120, 0, true, fsUser, dataprovider.TransferQuota{})\n\ttransfer3.BytesReceived.Store(60) // this value will be ignored, this is a download\n\terr = Connections.Add(fakeConn3)\n\tassert.NoError(t, err)\n\n\t// the transfers are not overquota\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\tassert.Nil(t, transfer2.errAbort)\n\tassert.Nil(t, transfer3.errAbort)\n\n\ttransfer1.BytesReceived.Store(80) // truncated size will be subtracted, we are not overquota\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\tassert.Nil(t, transfer2.errAbort)\n\tassert.Nil(t, transfer3.errAbort)\n\ttransfer1.BytesReceived.Store(120)\n\t// we are now overquota\n\t// if another check is in progress nothing is done\n\tConnections.transfersCheckStatus.Store(true)\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\tassert.Nil(t, transfer2.errAbort)\n\tassert.Nil(t, transfer3.errAbort)\n\tConnections.transfersCheckStatus.Store(false)\n\n\tConnections.checkTransfers()\n\tassert.True(t, conn1.IsQuotaExceededError(transfer1.errAbort), transfer1.errAbort)\n\tassert.True(t, conn2.IsQuotaExceededError(transfer2.errAbort), transfer2.errAbort)\n\tassert.True(t, conn1.IsQuotaExceededError(transfer1.GetAbortError()))\n\tassert.Nil(t, transfer3.errAbort)\n\tassert.True(t, conn3.IsQuotaExceededError(transfer3.GetAbortError()))\n\t// update the user quota size\n\tgroup.UserSettings.QuotaSize = 1000\n\terr = dataprovider.UpdateGroup(&group, []string{username}, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\ttransfer1.errAbort = nil\n\ttransfer2.errAbort = nil\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\tassert.Nil(t, transfer2.errAbort)\n\tassert.Nil(t, transfer3.errAbort)\n\n\tgroup.UserSettings.QuotaSize = 0\n\terr = dataprovider.UpdateGroup(&group, []string{username}, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\tassert.Nil(t, transfer2.errAbort)\n\tassert.Nil(t, transfer3.errAbort)\n\t// now check a public folder\n\ttransfer1.BytesReceived.Store(0)\n\ttransfer2.BytesReceived.Store(0)\n\tconnID4 := xid.New().String()\n\tfsFolder, err := user.GetFilesystemForPath(path.Join(vdirPath, \"/file1\"), connID4)\n\tassert.NoError(t, err)\n\tconn4 := NewBaseConnection(connID4, ProtocolSFTP, \"\", \"\", user)\n\tfakeConn4 := &fakeConnection{\n\t\tBaseConnection: conn4,\n\t}\n\ttransfer4 := NewBaseTransfer(nil, conn4, nil, filepath.Join(os.TempDir(), folderName, \"file1\"),\n\t\tfilepath.Join(os.TempDir(), folderName, \"file1\"), path.Join(vdirPath, \"/file1\"), TransferUpload, 0, 0,\n\t\t100, 0, true, fsFolder, dataprovider.TransferQuota{})\n\terr = Connections.Add(fakeConn4)\n\tassert.NoError(t, err)\n\tconnID5 := xid.New().String()\n\tconn5 := NewBaseConnection(connID5, ProtocolSFTP, \"\", \"\", user)\n\tfakeConn5 := &fakeConnection{\n\t\tBaseConnection: conn5,\n\t}\n\ttransfer5 := NewBaseTransfer(nil, conn5, nil, filepath.Join(os.TempDir(), folderName, \"file2\"),\n\t\tfilepath.Join(os.TempDir(), folderName, \"file2\"), path.Join(vdirPath, \"/file2\"), TransferUpload, 0, 0,\n\t\t100, 0, true, fsFolder, dataprovider.TransferQuota{})\n\n\terr = Connections.Add(fakeConn5)\n\tassert.NoError(t, err)\n\ttransfer4.BytesReceived.Store(50)\n\ttransfer5.BytesReceived.Store(40)\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer4.errAbort)\n\tassert.Nil(t, transfer5.errAbort)\n\ttransfer5.BytesReceived.Store(60)\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\tassert.Nil(t, transfer2.errAbort)\n\tassert.Nil(t, transfer3.errAbort)\n\tassert.True(t, conn1.IsQuotaExceededError(transfer4.errAbort))\n\tassert.True(t, conn2.IsQuotaExceededError(transfer5.errAbort))\n\n\tif dataprovider.GetProviderStatus().Driver != dataprovider.MemoryDataProviderName {\n\t\tproviderConf := dataprovider.GetProviderConfig()\n\t\terr = dataprovider.Close()\n\t\tassert.NoError(t, err)\n\n\t\ttransfer4.errAbort = nil\n\t\ttransfer5.errAbort = nil\n\t\tConnections.checkTransfers()\n\t\tassert.Nil(t, transfer1.errAbort)\n\t\tassert.Nil(t, transfer2.errAbort)\n\t\tassert.Nil(t, transfer3.errAbort)\n\t\tassert.Nil(t, transfer4.errAbort)\n\t\tassert.Nil(t, transfer5.errAbort)\n\n\t\terr = dataprovider.Initialize(providerConf, configDir, true)\n\t\tassert.NoError(t, err)\n\t}\n\n\terr = transfer1.Close()\n\tassert.NoError(t, err)\n\terr = transfer2.Close()\n\tassert.NoError(t, err)\n\terr = transfer3.Close()\n\tassert.NoError(t, err)\n\terr = transfer4.Close()\n\tassert.NoError(t, err)\n\terr = transfer5.Close()\n\tassert.NoError(t, err)\n\n\tConnections.Remove(fakeConn1.GetID())\n\tConnections.Remove(fakeConn2.GetID())\n\tConnections.Remove(fakeConn3.GetID())\n\tConnections.Remove(fakeConn4.GetID())\n\tConnections.Remove(fakeConn5.GetID())\n\tstats := Connections.GetStats(\"\")\n\tassert.Len(t, stats, 0)\n\tassert.Equal(t, int32(0), Connections.GetTotalTransfers())\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.DeleteFolder(folderName, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(filepath.Join(os.TempDir(), folderName))\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteGroup(groupName, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestTransferCheckerTransferQuota(t *testing.T) {\n\tusername := \"transfers_check_username\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:          username,\n\t\t\tPassword:          \"test_pwd\",\n\t\t\tHomeDir:           filepath.Join(os.TempDir(), username),\n\t\t\tStatus:            1,\n\t\t\tTotalDataTransfer: 1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tconnID1 := xid.New().String()\n\tfsUser, err := user.GetFilesystemForPath(\"/file1\", connID1)\n\tassert.NoError(t, err)\n\tconn1 := NewBaseConnection(connID1, ProtocolSFTP, \"\", \"192.168.1.1\", user)\n\tfakeConn1 := &fakeConnection{\n\t\tBaseConnection: conn1,\n\t}\n\ttransfer1 := NewBaseTransfer(nil, conn1, nil, filepath.Join(user.HomeDir, \"file1\"), filepath.Join(user.HomeDir, \"file1\"),\n\t\t\"/file1\", TransferUpload, 0, 0, 0, 0, true, fsUser, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\ttransfer1.BytesReceived.Store(150)\n\terr = Connections.Add(fakeConn1)\n\tassert.NoError(t, err)\n\t// the transferschecker will do nothing if there is only one ongoing transfer\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\n\tconnID2 := xid.New().String()\n\tconn2 := NewBaseConnection(connID2, ProtocolSFTP, \"\", \"127.0.0.1\", user)\n\tfakeConn2 := &fakeConnection{\n\t\tBaseConnection: conn2,\n\t}\n\ttransfer2 := NewBaseTransfer(nil, conn2, nil, filepath.Join(user.HomeDir, \"file2\"), filepath.Join(user.HomeDir, \"file2\"),\n\t\t\"/file2\", TransferUpload, 0, 0, 0, 0, true, fsUser, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\ttransfer2.BytesReceived.Store(150)\n\terr = Connections.Add(fakeConn2)\n\tassert.NoError(t, err)\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer1.errAbort)\n\tassert.Nil(t, transfer2.errAbort)\n\t// now test overquota\n\ttransfer1.BytesReceived.Store(1024*1024 + 1)\n\ttransfer2.BytesReceived.Store(0)\n\tConnections.checkTransfers()\n\tassert.True(t, conn1.IsQuotaExceededError(transfer1.errAbort), transfer1.errAbort)\n\tassert.Nil(t, transfer2.errAbort)\n\ttransfer1.errAbort = nil\n\ttransfer1.BytesReceived.Store(1024*1024 + 1)\n\ttransfer2.BytesReceived.Store(1024)\n\tConnections.checkTransfers()\n\tassert.True(t, conn1.IsQuotaExceededError(transfer1.errAbort))\n\tassert.True(t, conn2.IsQuotaExceededError(transfer2.errAbort))\n\ttransfer1.BytesReceived.Store(0)\n\ttransfer2.BytesReceived.Store(0)\n\ttransfer1.errAbort = nil\n\ttransfer2.errAbort = nil\n\n\terr = transfer1.Close()\n\tassert.NoError(t, err)\n\terr = transfer2.Close()\n\tassert.NoError(t, err)\n\tConnections.Remove(fakeConn1.GetID())\n\tConnections.Remove(fakeConn2.GetID())\n\n\tconnID3 := xid.New().String()\n\tconn3 := NewBaseConnection(connID3, ProtocolSFTP, \"\", \"\", user)\n\tfakeConn3 := &fakeConnection{\n\t\tBaseConnection: conn3,\n\t}\n\ttransfer3 := NewBaseTransfer(nil, conn3, nil, filepath.Join(user.HomeDir, \"file1\"), filepath.Join(user.HomeDir, \"file1\"),\n\t\t\"/file1\", TransferDownload, 0, 0, 0, 0, true, fsUser, dataprovider.TransferQuota{AllowedDLSize: 100})\n\ttransfer3.BytesSent.Store(150)\n\terr = Connections.Add(fakeConn3)\n\tassert.NoError(t, err)\n\n\tconnID4 := xid.New().String()\n\tconn4 := NewBaseConnection(connID4, ProtocolSFTP, \"\", \"\", user)\n\tfakeConn4 := &fakeConnection{\n\t\tBaseConnection: conn4,\n\t}\n\ttransfer4 := NewBaseTransfer(nil, conn4, nil, filepath.Join(user.HomeDir, \"file2\"), filepath.Join(user.HomeDir, \"file2\"),\n\t\t\"/file2\", TransferDownload, 0, 0, 0, 0, true, fsUser, dataprovider.TransferQuota{AllowedDLSize: 100})\n\ttransfer4.BytesSent.Store(150)\n\terr = Connections.Add(fakeConn4)\n\tassert.NoError(t, err)\n\tConnections.checkTransfers()\n\tassert.Nil(t, transfer3.errAbort)\n\tassert.Nil(t, transfer4.errAbort)\n\n\ttransfer3.BytesSent.Store(512 * 1024)\n\ttransfer4.BytesSent.Store(512*1024 + 1)\n\tConnections.checkTransfers()\n\tif assert.Error(t, transfer3.errAbort) {\n\t\tassert.Contains(t, transfer3.errAbort.Error(), ErrReadQuotaExceeded.Error())\n\t}\n\tif assert.Error(t, transfer4.errAbort) {\n\t\tassert.Contains(t, transfer4.errAbort.Error(), ErrReadQuotaExceeded.Error())\n\t}\n\terr = transfer3.Close()\n\tassert.NoError(t, err)\n\terr = transfer4.Close()\n\tassert.NoError(t, err)\n\n\tConnections.Remove(fakeConn3.GetID())\n\tConnections.Remove(fakeConn4.GetID())\n\tstats := Connections.GetStats(\"\")\n\tassert.Len(t, stats, 0)\n\tassert.Equal(t, int32(0), Connections.GetTotalTransfers())\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestAggregateTransfers(t *testing.T) {\n\tchecker := transfersCheckerMem{}\n\tchecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\tID:            1,\n\t\tType:          TransferUpload,\n\t\tConnID:        \"1\",\n\t\tUsername:      \"user\",\n\t\tFolderName:    \"\",\n\t\tTruncatedSize: 0,\n\t\tCurrentULSize: 100,\n\t\tCurrentDLSize: 0,\n\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\tusersToFetch, aggregations := checker.aggregateUploadTransfers()\n\tassert.Len(t, usersToFetch, 0)\n\tassert.Len(t, aggregations, 1)\n\n\tchecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\tID:            1,\n\t\tType:          TransferDownload,\n\t\tConnID:        \"2\",\n\t\tUsername:      \"user\",\n\t\tFolderName:    \"\",\n\t\tTruncatedSize: 0,\n\t\tCurrentULSize: 0,\n\t\tCurrentDLSize: 100,\n\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\n\tusersToFetch, aggregations = checker.aggregateUploadTransfers()\n\tassert.Len(t, usersToFetch, 0)\n\tassert.Len(t, aggregations, 1)\n\n\tchecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\tID:            1,\n\t\tType:          TransferUpload,\n\t\tConnID:        \"3\",\n\t\tUsername:      \"user\",\n\t\tFolderName:    \"folder\",\n\t\tTruncatedSize: 0,\n\t\tCurrentULSize: 10,\n\t\tCurrentDLSize: 0,\n\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\n\tusersToFetch, aggregations = checker.aggregateUploadTransfers()\n\tassert.Len(t, usersToFetch, 0)\n\tassert.Len(t, aggregations, 2)\n\n\tchecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\tID:            1,\n\t\tType:          TransferUpload,\n\t\tConnID:        \"4\",\n\t\tUsername:      \"user1\",\n\t\tFolderName:    \"\",\n\t\tTruncatedSize: 0,\n\t\tCurrentULSize: 100,\n\t\tCurrentDLSize: 0,\n\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\n\tusersToFetch, aggregations = checker.aggregateUploadTransfers()\n\tassert.Len(t, usersToFetch, 0)\n\tassert.Len(t, aggregations, 3)\n\n\tchecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\tID:            1,\n\t\tType:          TransferUpload,\n\t\tConnID:        \"5\",\n\t\tUsername:      \"user\",\n\t\tFolderName:    \"\",\n\t\tTruncatedSize: 0,\n\t\tCurrentULSize: 100,\n\t\tCurrentDLSize: 0,\n\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\n\tusersToFetch, aggregations = checker.aggregateUploadTransfers()\n\tassert.Len(t, usersToFetch, 1)\n\tval, ok := usersToFetch[\"user\"]\n\tassert.True(t, ok)\n\tassert.False(t, val)\n\tassert.Len(t, aggregations, 3)\n\taggregate, ok := aggregations[0]\n\tassert.True(t, ok)\n\tassert.Len(t, aggregate, 2)\n\n\tchecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\tID:            1,\n\t\tType:          TransferUpload,\n\t\tConnID:        \"6\",\n\t\tUsername:      \"user\",\n\t\tFolderName:    \"\",\n\t\tTruncatedSize: 0,\n\t\tCurrentULSize: 100,\n\t\tCurrentDLSize: 0,\n\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\n\tusersToFetch, aggregations = checker.aggregateUploadTransfers()\n\tassert.Len(t, usersToFetch, 1)\n\tval, ok = usersToFetch[\"user\"]\n\tassert.True(t, ok)\n\tassert.False(t, val)\n\tassert.Len(t, aggregations, 3)\n\taggregate, ok = aggregations[0]\n\tassert.True(t, ok)\n\tassert.Len(t, aggregate, 3)\n\n\tchecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\tID:            1,\n\t\tType:          TransferUpload,\n\t\tConnID:        \"7\",\n\t\tUsername:      \"user\",\n\t\tFolderName:    \"folder\",\n\t\tTruncatedSize: 0,\n\t\tCurrentULSize: 10,\n\t\tCurrentDLSize: 0,\n\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\n\tusersToFetch, aggregations = checker.aggregateUploadTransfers()\n\tassert.Len(t, usersToFetch, 1)\n\tval, ok = usersToFetch[\"user\"]\n\tassert.True(t, ok)\n\tassert.True(t, val)\n\tassert.Len(t, aggregations, 3)\n\taggregate, ok = aggregations[0]\n\tassert.True(t, ok)\n\tassert.Len(t, aggregate, 3)\n\taggregate, ok = aggregations[1]\n\tassert.True(t, ok)\n\tassert.Len(t, aggregate, 2)\n\n\tchecker.AddTransfer(dataprovider.ActiveTransfer{\n\t\tID:            1,\n\t\tType:          TransferUpload,\n\t\tConnID:        \"8\",\n\t\tUsername:      \"user\",\n\t\tFolderName:    \"\",\n\t\tTruncatedSize: 0,\n\t\tCurrentULSize: 100,\n\t\tCurrentDLSize: 0,\n\t\tCreatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tUpdatedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t})\n\n\tusersToFetch, aggregations = checker.aggregateUploadTransfers()\n\tassert.Len(t, usersToFetch, 1)\n\tval, ok = usersToFetch[\"user\"]\n\tassert.True(t, ok)\n\tassert.True(t, val)\n\tassert.Len(t, aggregations, 3)\n\taggregate, ok = aggregations[0]\n\tassert.True(t, ok)\n\tassert.Len(t, aggregate, 4)\n\taggregate, ok = aggregations[1]\n\tassert.True(t, ok)\n\tassert.Len(t, aggregate, 2)\n}\n\nfunc TestDataTransferExceeded(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tTotalDataTransfer: 1,\n\t\t},\n\t}\n\ttransfer := dataprovider.ActiveTransfer{\n\t\tCurrentULSize: 0,\n\t\tCurrentDLSize: 0,\n\t}\n\tuser.UsedDownloadDataTransfer = 1024 * 1024\n\tuser.UsedUploadDataTransfer = 512 * 1024\n\tchecker := transfersCheckerMem{}\n\tres := checker.isDataTransferExceeded(user, transfer, 100, 100)\n\tassert.False(t, res)\n\ttransfer.CurrentULSize = 1\n\tres = checker.isDataTransferExceeded(user, transfer, 100, 100)\n\tassert.True(t, res)\n\tuser.UsedDownloadDataTransfer = 512*1024 - 100\n\tuser.UsedUploadDataTransfer = 512*1024 - 100\n\tres = checker.isDataTransferExceeded(user, transfer, 100, 100)\n\tassert.False(t, res)\n\tres = checker.isDataTransferExceeded(user, transfer, 101, 100)\n\tassert.True(t, res)\n\n\tuser.TotalDataTransfer = 0\n\tuser.DownloadDataTransfer = 1\n\tuser.UsedDownloadDataTransfer = 512 * 1024\n\ttransfer.CurrentULSize = 0\n\ttransfer.CurrentDLSize = 100\n\tres = checker.isDataTransferExceeded(user, transfer, 0, 512*1024)\n\tassert.False(t, res)\n\tres = checker.isDataTransferExceeded(user, transfer, 0, 512*1024+1)\n\tassert.True(t, res)\n\n\tuser.DownloadDataTransfer = 0\n\tuser.UploadDataTransfer = 1\n\tuser.UsedUploadDataTransfer = 512 * 1024\n\ttransfer.CurrentULSize = 0\n\ttransfer.CurrentDLSize = 0\n\tres = checker.isDataTransferExceeded(user, transfer, 512*1024+1, 0)\n\tassert.False(t, res)\n\ttransfer.CurrentULSize = 1\n\tres = checker.isDataTransferExceeded(user, transfer, 512*1024+1, 0)\n\tassert.True(t, res)\n}\n\nfunc TestGetUsersForQuotaCheck(t *testing.T) {\n\tusersToFetch := make(map[string]bool)\n\tfor i := 0; i < 70; i++ {\n\t\tusersToFetch[fmt.Sprintf(\"user%v\", i)] = i%2 == 0\n\t}\n\n\tusers, err := dataprovider.GetUsersForQuotaCheck(usersToFetch)\n\tassert.NoError(t, err)\n\tassert.Len(t, users, 0)\n\n\tfor i := 0; i < 60; i++ {\n\t\tfolder := vfs.BaseVirtualFolder{\n\t\t\tName:       fmt.Sprintf(\"f%v\", i),\n\t\t\tMappedPath: filepath.Join(os.TempDir(), fmt.Sprintf(\"f%v\", i)),\n\t\t}\n\t\tuser := dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername:  fmt.Sprintf(\"user%v\", i),\n\t\t\t\tPassword:  \"pwd\",\n\t\t\t\tHomeDir:   filepath.Join(os.TempDir(), fmt.Sprintf(\"user%v\", i)),\n\t\t\t\tStatus:    1,\n\t\t\t\tQuotaSize: 120,\n\t\t\t\tPermissions: map[string][]string{\n\t\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t\t},\n\t\t\t},\n\t\t\tVirtualFolders: []vfs.VirtualFolder{\n\t\t\t\t{\n\t\t\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\t\t\tName: folder.Name,\n\t\t\t\t\t},\n\t\t\t\t\tVirtualPath: \"/vfolder\",\n\t\t\t\t\tQuotaSize:   100,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\terr = dataprovider.AddFolder(&folder, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t\terr = dataprovider.UpdateVirtualFolderQuota(&vfs.BaseVirtualFolder{Name: fmt.Sprintf(\"f%v\", i)}, 1, 50, false)\n\t\tassert.NoError(t, err)\n\t}\n\n\tusers, err = dataprovider.GetUsersForQuotaCheck(usersToFetch)\n\tassert.NoError(t, err)\n\tassert.Len(t, users, 60)\n\n\tfor _, user := range users {\n\t\tuserIdxStr := strings.Replace(user.Username, \"user\", \"\", 1)\n\t\tuserIdx, err := strconv.Atoi(userIdxStr)\n\t\tassert.NoError(t, err)\n\t\tif userIdx%2 == 0 {\n\t\t\tif assert.Len(t, user.VirtualFolders, 1, user.Username) {\n\t\t\t\tassert.Equal(t, int64(100), user.VirtualFolders[0].QuotaSize)\n\t\t\t\tassert.Equal(t, int64(50), user.VirtualFolders[0].UsedQuotaSize)\n\t\t\t}\n\t\t} else {\n\t\t\tswitch dataprovider.GetProviderStatus().Driver {\n\t\t\tcase dataprovider.MySQLDataProviderName, dataprovider.PGSQLDataProviderName,\n\t\t\t\tdataprovider.CockroachDataProviderName, dataprovider.SQLiteDataProviderName:\n\t\t\t\tassert.Len(t, user.VirtualFolders, 0, user.Username)\n\t\t\t}\n\t\t}\n\t\tul, dl, total := user.GetDataTransferLimits()\n\t\tassert.Equal(t, int64(0), ul)\n\t\tassert.Equal(t, int64(0), dl)\n\t\tassert.Equal(t, int64(0), total)\n\t}\n\n\tfor i := 0; i < 60; i++ {\n\t\terr = dataprovider.DeleteUser(fmt.Sprintf(\"user%v\", i), \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t\terr = dataprovider.DeleteFolder(fmt.Sprintf(\"f%v\", i), \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\n\tusers, err = dataprovider.GetUsersForQuotaCheck(usersToFetch)\n\tassert.NoError(t, err)\n\tassert.Len(t, users, 0)\n}\n\nfunc TestDBTransferChecker(t *testing.T) {\n\tif !isDbTransferCheckerSupported() {\n\t\tt.Skip(\"this test is not supported with the current database provider\")\n\t}\n\tproviderConf := dataprovider.GetProviderConfig()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\tproviderConf.IsShared = 1\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tc := getTransfersChecker(1)\n\tchecker, ok := c.(*transfersCheckerDB)\n\tassert.True(t, ok)\n\tassert.True(t, checker.lastCleanup.IsZero())\n\ttransfer1 := dataprovider.ActiveTransfer{\n\t\tID:         1,\n\t\tType:       TransferDownload,\n\t\tConnID:     xid.New().String(),\n\t\tUsername:   \"user1\",\n\t\tFolderName: \"folder1\",\n\t\tIP:         \"127.0.0.1\",\n\t}\n\tchecker.AddTransfer(transfer1)\n\ttransfers, err := dataprovider.GetActiveTransfers(time.Now().Add(24 * time.Hour))\n\tassert.NoError(t, err)\n\tassert.Len(t, transfers, 0)\n\ttransfers, err = dataprovider.GetActiveTransfers(time.Now().Add(-periodicTimeoutCheckInterval * 2))\n\tassert.NoError(t, err)\n\tvar createdAt, updatedAt int64\n\tif assert.Len(t, transfers, 1) {\n\t\ttransfer := transfers[0]\n\t\tassert.Equal(t, transfer1.ID, transfer.ID)\n\t\tassert.Equal(t, transfer1.Type, transfer.Type)\n\t\tassert.Equal(t, transfer1.ConnID, transfer.ConnID)\n\t\tassert.Equal(t, transfer1.Username, transfer.Username)\n\t\tassert.Equal(t, transfer1.IP, transfer.IP)\n\t\tassert.Equal(t, transfer1.FolderName, transfer.FolderName)\n\t\tassert.Greater(t, transfer.CreatedAt, int64(0))\n\t\tassert.Greater(t, transfer.UpdatedAt, int64(0))\n\t\tassert.Equal(t, int64(0), transfer.CurrentDLSize)\n\t\tassert.Equal(t, int64(0), transfer.CurrentULSize)\n\t\tcreatedAt = transfer.CreatedAt\n\t\tupdatedAt = transfer.UpdatedAt\n\t}\n\ttime.Sleep(100 * time.Millisecond)\n\tchecker.UpdateTransferCurrentSizes(100, 150, transfer1.ID, transfer1.ConnID)\n\ttransfers, err = dataprovider.GetActiveTransfers(time.Now().Add(-periodicTimeoutCheckInterval * 2))\n\tassert.NoError(t, err)\n\tif assert.Len(t, transfers, 1) {\n\t\ttransfer := transfers[0]\n\t\tassert.Equal(t, int64(150), transfer.CurrentDLSize)\n\t\tassert.Equal(t, int64(100), transfer.CurrentULSize)\n\t\tassert.Equal(t, createdAt, transfer.CreatedAt)\n\t\tassert.Greater(t, transfer.UpdatedAt, updatedAt)\n\t}\n\tres := checker.GetOverquotaTransfers()\n\tassert.Len(t, res, 0)\n\n\tchecker.RemoveTransfer(transfer1.ID, transfer1.ConnID)\n\ttransfers, err = dataprovider.GetActiveTransfers(time.Now().Add(-periodicTimeoutCheckInterval * 2))\n\tassert.NoError(t, err)\n\tassert.Len(t, transfers, 0)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\tres = checker.GetOverquotaTransfers()\n\tassert.Len(t, res, 0)\n\tproviderConf.IsShared = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc isDbTransferCheckerSupported() bool {\n\t// SQLite shares the implementation with other SQL-based provider but it makes no sense\n\t// to use it outside test cases\n\tswitch dataprovider.GetProviderStatus().Driver {\n\tcase dataprovider.MySQLDataProviderName, dataprovider.PGSQLDataProviderName,\n\t\tdataprovider.CockroachDataProviderName, dataprovider.SQLiteDataProviderName:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/config/config.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package config manages the configuration\npackage config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\tkmsplugin \"github.com/sftpgo/sdk/plugin/kms\"\n\t\"github.com/spf13/viper\"\n\t\"github.com/subosito/gotenv\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/acme\"\n\t\"github.com/drakkan/sftpgo/v2/internal/command\"\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/ftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/telemetry\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\nconst (\n\tlogSender = \"config\"\n\t// configName defines the name for config file.\n\t// This name does not include the extension, viper will search for files\n\t// with supported extensions such as \"sftpgo.json\", \"sftpgo.yaml\" and so on\n\tconfigName = \"sftpgo\"\n\t// ConfigEnvPrefix defines a prefix that environment variables will use\n\tconfigEnvPrefix = \"sftpgo\"\n\tenvFileMaxSize  = 1048576\n)\n\nvar (\n\tglobalConf             globalConfig\n\tdefaultInstallCodeHint = \"Installation code\"\n\tdefaultSFTPDBinding    = sftpd.Binding{\n\t\tAddress:          \"\",\n\t\tPort:             2022,\n\t\tApplyProxyConfig: true,\n\t}\n\tdefaultFTPDBinding = ftpd.Binding{\n\t\tAddress:                    \"\",\n\t\tPort:                       0,\n\t\tApplyProxyConfig:           true,\n\t\tTLSMode:                    0,\n\t\tCertificateFile:            \"\",\n\t\tCertificateKeyFile:         \"\",\n\t\tMinTLSVersion:              12,\n\t\tForcePassiveIP:             \"\",\n\t\tPassiveIPOverrides:         nil,\n\t\tPassiveHost:                \"\",\n\t\tClientAuthType:             0,\n\t\tTLSCipherSuites:            nil,\n\t\tPassiveConnectionsSecurity: 0,\n\t\tActiveConnectionsSecurity:  0,\n\t\tDebug:                      false,\n\t}\n\tdefaultWebDAVDBinding = webdavd.Binding{\n\t\tAddress:              \"\",\n\t\tPort:                 0,\n\t\tEnableHTTPS:          false,\n\t\tCertificateFile:      \"\",\n\t\tCertificateKeyFile:   \"\",\n\t\tMinTLSVersion:        12,\n\t\tClientAuthType:       0,\n\t\tTLSCipherSuites:      nil,\n\t\tProtocols:            nil,\n\t\tPrefix:               \"\",\n\t\tProxyMode:            0,\n\t\tProxyAllowed:         nil,\n\t\tClientIPProxyHeader:  \"\",\n\t\tClientIPHeaderDepth:  0,\n\t\tDisableWWWAuthHeader: false,\n\t}\n\tdefaultHTTPDBinding = httpd.Binding{\n\t\tAddress:              \"\",\n\t\tPort:                 8080,\n\t\tEnableWebAdmin:       true,\n\t\tEnableWebClient:      true,\n\t\tEnableRESTAPI:        true,\n\t\tEnabledLoginMethods:  0,\n\t\tDisabledLoginMethods: 0,\n\t\tEnableHTTPS:          false,\n\t\tCertificateFile:      \"\",\n\t\tCertificateKeyFile:   \"\",\n\t\tMinTLSVersion:        12,\n\t\tClientAuthType:       0,\n\t\tTLSCipherSuites:      nil,\n\t\tProtocols:            nil,\n\t\tProxyMode:            0,\n\t\tProxyAllowed:         nil,\n\t\tClientIPProxyHeader:  \"\",\n\t\tClientIPHeaderDepth:  0,\n\t\tHideLoginURL:         0,\n\t\tRenderOpenAPI:        true,\n\t\tBaseURL:              \"\",\n\t\tLanguages:            []string{\"en\"},\n\t\tOIDC: httpd.OIDC{\n\t\t\tClientID:                   \"\",\n\t\t\tClientSecret:               \"\",\n\t\t\tClientSecretFile:           \"\",\n\t\t\tConfigURL:                  \"\",\n\t\t\tRedirectBaseURL:            \"\",\n\t\t\tUsernameField:              \"\",\n\t\t\tRoleField:                  \"\",\n\t\t\tImplicitRoles:              false,\n\t\t\tScopes:                     []string{\"openid\", \"profile\", \"email\"},\n\t\t\tCustomFields:               []string{},\n\t\t\tInsecureSkipSignatureCheck: false,\n\t\t\tDebug:                      false,\n\t\t},\n\t\tSecurity: httpd.SecurityConf{\n\t\t\tEnabled:                   false,\n\t\t\tAllowedHosts:              nil,\n\t\t\tAllowedHostsAreRegex:      false,\n\t\t\tHostsProxyHeaders:         nil,\n\t\t\tHTTPSRedirect:             false,\n\t\t\tHTTPSHost:                 \"\",\n\t\t\tHTTPSProxyHeaders:         nil,\n\t\t\tSTSSeconds:                0,\n\t\t\tSTSIncludeSubdomains:      false,\n\t\t\tSTSPreload:                false,\n\t\t\tContentTypeNosniff:        false,\n\t\t\tContentSecurityPolicy:     \"\",\n\t\t\tPermissionsPolicy:         \"\",\n\t\t\tCrossOriginOpenerPolicy:   \"\",\n\t\t\tCrossOriginResourcePolicy: \"\",\n\t\t\tCrossOriginEmbedderPolicy: \"\",\n\t\t\tCacheControl:              \"\",\n\t\t},\n\t\tBranding: httpd.Branding{},\n\t}\n\tdefaultRateLimiter = common.RateLimiterConfig{\n\t\tAverage:                0,\n\t\tPeriod:                 1000,\n\t\tBurst:                  1,\n\t\tType:                   2,\n\t\tProtocols:              []string{common.ProtocolSSH, common.ProtocolFTP, common.ProtocolWebDAV, common.ProtocolHTTP},\n\t\tGenerateDefenderEvents: false,\n\t\tEntriesSoftLimit:       100,\n\t\tEntriesHardLimit:       150,\n\t}\n\tdefaultTOTP = mfa.TOTPConfig{\n\t\tName:   \"Default\",\n\t\tIssuer: \"SFTPGo\",\n\t\tAlgo:   mfa.TOTPAlgoSHA1,\n\t}\n)\n\ntype globalConfig struct {\n\tCommon          common.Configuration  `json:\"common\" mapstructure:\"common\"`\n\tACME            acme.Configuration    `json:\"acme\" mapstructure:\"acme\"`\n\tSFTPD           sftpd.Configuration   `json:\"sftpd\" mapstructure:\"sftpd\"`\n\tFTPD            ftpd.Configuration    `json:\"ftpd\" mapstructure:\"ftpd\"`\n\tWebDAVD         webdavd.Configuration `json:\"webdavd\" mapstructure:\"webdavd\"`\n\tProviderConf    dataprovider.Config   `json:\"data_provider\" mapstructure:\"data_provider\"`\n\tHTTPDConfig     httpd.Conf            `json:\"httpd\" mapstructure:\"httpd\"`\n\tHTTPConfig      httpclient.Config     `json:\"http\" mapstructure:\"http\"`\n\tCommandConfig   command.Config        `json:\"command\" mapstructure:\"command\"`\n\tKMSConfig       kms.Configuration     `json:\"kms\" mapstructure:\"kms\"`\n\tMFAConfig       mfa.Config            `json:\"mfa\" mapstructure:\"mfa\"`\n\tTelemetryConfig telemetry.Conf        `json:\"telemetry\" mapstructure:\"telemetry\"`\n\tPluginsConfig   []plugin.Config       `json:\"plugins\" mapstructure:\"plugins\"`\n\tSMTPConfig      smtp.Config           `json:\"smtp\" mapstructure:\"smtp\"`\n}\n\nfunc init() {\n\tInit()\n}\n\n// Init initializes the global configuration.\n// It is not supposed to be called outside of this package.\n// It is exported to minimize refactoring efforts. Will eventually disappear.\nfunc Init() {\n\t// create a default configuration to use if no config file is provided\n\tglobalConf = globalConfig{\n\t\tCommon: common.Configuration{\n\t\t\tIdleTimeout: 15,\n\t\t\tUploadMode:  0,\n\t\t\tActions: common.ProtocolActions{\n\t\t\t\tExecuteOn:   []string{},\n\t\t\t\tExecuteSync: []string{},\n\t\t\t\tHook:        \"\",\n\t\t\t},\n\t\t\tSetstatMode:           0,\n\t\t\tRenameMode:            0,\n\t\t\tResumeMaxSize:         0,\n\t\t\tTempPath:              \"\",\n\t\t\tProxyProtocol:         0,\n\t\t\tProxyAllowed:          []string{},\n\t\t\tProxySkipped:          []string{},\n\t\t\tPostConnectHook:       \"\",\n\t\t\tPostDisconnectHook:    \"\",\n\t\t\tMaxTotalConnections:   0,\n\t\t\tMaxPerHostConnections: 20,\n\t\t\tAllowListStatus:       0,\n\t\t\tAllowSelfConnections:  0,\n\t\t\tDefenderConfig: common.DefenderConfig{\n\t\t\t\tEnabled:            false,\n\t\t\t\tDriver:             common.DefenderDriverMemory,\n\t\t\t\tBanTime:            30,\n\t\t\t\tBanTimeIncrement:   50,\n\t\t\t\tThreshold:          15,\n\t\t\t\tScoreInvalid:       2,\n\t\t\t\tScoreValid:         1,\n\t\t\t\tScoreLimitExceeded: 3,\n\t\t\t\tScoreNoAuth:        0,\n\t\t\t\tObservationTime:    30,\n\t\t\t\tEntriesSoftLimit:   100,\n\t\t\t\tEntriesHardLimit:   150,\n\t\t\t\tLoginDelay: common.LoginDelay{\n\t\t\t\t\tSuccess:        0,\n\t\t\t\t\tPasswordFailed: 1000,\n\t\t\t\t},\n\t\t\t},\n\t\t\tRateLimitersConfig: []common.RateLimiterConfig{defaultRateLimiter},\n\t\t\tUmask:              \"\",\n\t\t\tServerVersion:      \"\",\n\t\t\tTZ:                 \"\",\n\t\t\tMetadata: common.MetadataConfig{\n\t\t\t\tRead: 0,\n\t\t\t},\n\t\t\tEventManager: common.EventManagerConfig{\n\t\t\t\tEnabledCommands: []string{},\n\t\t\t},\n\t\t},\n\t\tACME: acme.Configuration{\n\t\t\tEmail:      \"\",\n\t\t\tKeyType:    \"4096\",\n\t\t\tCertsPath:  \"certs\",\n\t\t\tCAEndpoint: \"https://acme-v02.api.letsencrypt.org/directory\",\n\t\t\tDomains:    []string{},\n\t\t\tRenewDays:  30,\n\t\t\tHTTP01Challenge: acme.HTTP01Challenge{\n\t\t\t\tPort:        80,\n\t\t\t\tWebRoot:     \"\",\n\t\t\t\tProxyHeader: \"\",\n\t\t\t},\n\t\t\tTLSALPN01Challenge: acme.TLSALPN01Challenge{\n\t\t\t\tPort: 0,\n\t\t\t},\n\t\t},\n\t\tSFTPD: sftpd.Configuration{\n\t\t\tBindings:                          []sftpd.Binding{defaultSFTPDBinding},\n\t\t\tMaxAuthTries:                      0,\n\t\t\tHostKeys:                          []string{},\n\t\t\tHostCertificates:                  []string{},\n\t\t\tHostKeyAlgorithms:                 []string{},\n\t\t\tKexAlgorithms:                     []string{},\n\t\t\tCiphers:                           []string{},\n\t\t\tMACs:                              []string{},\n\t\t\tPublicKeyAlgorithms:               []string{},\n\t\t\tTrustedUserCAKeys:                 []string{},\n\t\t\tRevokedUserCertsFile:              \"\",\n\t\t\tOPKSSHPath:                        \"\",\n\t\t\tOPKSSHChecksum:                    \"\",\n\t\t\tLoginBannerFile:                   \"\",\n\t\t\tEnabledSSHCommands:                []string{},\n\t\t\tKeyboardInteractiveAuthentication: true,\n\t\t\tKeyboardInteractiveHook:           \"\",\n\t\t\tPasswordAuthentication:            true,\n\t\t},\n\t\tFTPD: ftpd.Configuration{\n\t\t\tBindings:                 []ftpd.Binding{defaultFTPDBinding},\n\t\t\tBannerFile:               \"\",\n\t\t\tActiveTransfersPortNon20: true,\n\t\t\tPassivePortRange: ftpd.PortRange{\n\t\t\t\tStart: 50000,\n\t\t\t\tEnd:   50100,\n\t\t\t},\n\t\t\tDisableActiveMode:  false,\n\t\t\tEnableSite:         false,\n\t\t\tHASHSupport:        0,\n\t\t\tCombineSupport:     0,\n\t\t\tCertificateFile:    \"\",\n\t\t\tCertificateKeyFile: \"\",\n\t\t\tCACertificates:     []string{},\n\t\t\tCARevocationLists:  []string{},\n\t\t},\n\t\tWebDAVD: webdavd.Configuration{\n\t\t\tBindings:           []webdavd.Binding{defaultWebDAVDBinding},\n\t\t\tCertificateFile:    \"\",\n\t\t\tCertificateKeyFile: \"\",\n\t\t\tCACertificates:     []string{},\n\t\t\tCARevocationLists:  []string{},\n\t\t\tCors: webdavd.CorsConfig{\n\t\t\t\tEnabled:              false,\n\t\t\t\tAllowedOrigins:       []string{},\n\t\t\t\tAllowedMethods:       []string{},\n\t\t\t\tAllowedHeaders:       []string{},\n\t\t\t\tExposedHeaders:       []string{},\n\t\t\t\tAllowCredentials:     false,\n\t\t\t\tMaxAge:               0,\n\t\t\t\tOptionsPassthrough:   false,\n\t\t\t\tOptionsSuccessStatus: 0,\n\t\t\t\tAllowPrivateNetwork:  false,\n\t\t\t},\n\t\t\tCache: webdavd.Cache{\n\t\t\t\tUsers: webdavd.UsersCacheConfig{\n\t\t\t\t\tExpirationTime: 0,\n\t\t\t\t\tMaxSize:        50,\n\t\t\t\t},\n\t\t\t\tMimeTypes: webdavd.MimeCacheConfig{\n\t\t\t\t\tEnabled:        true,\n\t\t\t\t\tMaxSize:        1000,\n\t\t\t\t\tCustomMappings: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tProviderConf: dataprovider.Config{\n\t\t\tDriver:             \"sqlite\",\n\t\t\tName:               \"sftpgo.db\",\n\t\t\tHost:               \"\",\n\t\t\tPort:               0,\n\t\t\tUsername:           \"\",\n\t\t\tPassword:           \"\",\n\t\t\tConnectionString:   \"\",\n\t\t\tSQLTablesPrefix:    \"\",\n\t\t\tSSLMode:            0,\n\t\t\tDisableSNI:         false,\n\t\t\tTargetSessionAttrs: \"\",\n\t\t\tRootCert:           \"\",\n\t\t\tClientCert:         \"\",\n\t\t\tClientKey:          \"\",\n\t\t\tTrackQuota:         2,\n\t\t\tPoolSize:           0,\n\t\t\tUsersBaseDir:       \"\",\n\t\t\tActions: dataprovider.ObjectsActions{\n\t\t\t\tExecuteOn:  []string{},\n\t\t\t\tExecuteFor: []string{},\n\t\t\t\tHook:       \"\",\n\t\t\t},\n\t\t\tExternalAuthHook:   \"\",\n\t\t\tExternalAuthScope:  0,\n\t\t\tPreLoginHook:       \"\",\n\t\t\tPostLoginHook:      \"\",\n\t\t\tPostLoginScope:     0,\n\t\t\tCheckPasswordHook:  \"\",\n\t\t\tCheckPasswordScope: 0,\n\t\t\tPasswordHashing: dataprovider.PasswordHashing{\n\t\t\t\tArgon2Options: dataprovider.Argon2Options{\n\t\t\t\t\tMemory:      65536,\n\t\t\t\t\tIterations:  1,\n\t\t\t\t\tParallelism: 2,\n\t\t\t\t},\n\t\t\t\tBcryptOptions: dataprovider.BcryptOptions{\n\t\t\t\t\tCost: 10,\n\t\t\t\t},\n\t\t\t\tAlgo: dataprovider.HashingAlgoBcrypt,\n\t\t\t},\n\t\t\tPasswordValidation: dataprovider.PasswordValidation{\n\t\t\t\tAdmins: dataprovider.PasswordValidationRules{\n\t\t\t\t\tMinEntropy: 0,\n\t\t\t\t},\n\t\t\t\tUsers: dataprovider.PasswordValidationRules{\n\t\t\t\t\tMinEntropy: 0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPasswordCaching:    true,\n\t\t\tUpdateMode:         0,\n\t\t\tDelayedQuotaUpdate: 0,\n\t\t\tCreateDefaultAdmin: false,\n\t\t\tNamingRules:        1,\n\t\t\tIsShared:           0,\n\t\t\tNode: dataprovider.NodeConfig{\n\t\t\t\tHost:  \"\",\n\t\t\t\tPort:  0,\n\t\t\t\tProto: \"http\",\n\t\t\t},\n\t\t\tBackupsPath: \"backups\",\n\t\t},\n\t\tHTTPDConfig: httpd.Conf{\n\t\t\tBindings:              []httpd.Binding{defaultHTTPDBinding},\n\t\t\tTemplatesPath:         \"templates\",\n\t\t\tStaticFilesPath:       \"static\",\n\t\t\tOpenAPIPath:           \"openapi\",\n\t\t\tWebRoot:               \"\",\n\t\t\tCertificateFile:       \"\",\n\t\t\tCertificateKeyFile:    \"\",\n\t\t\tCACertificates:        nil,\n\t\t\tCARevocationLists:     nil,\n\t\t\tSigningPassphrase:     \"\",\n\t\t\tSigningPassphraseFile: \"\",\n\t\t\tTokenValidation:       0,\n\t\t\tCookieLifetime:        20,\n\t\t\tShareCookieLifetime:   120,\n\t\t\tJWTLifetime:           20,\n\t\t\tMaxUploadFileSize:     0,\n\t\t\tCors: httpd.CorsConfig{\n\t\t\t\tEnabled:              false,\n\t\t\t\tAllowedOrigins:       []string{},\n\t\t\t\tAllowedMethods:       []string{},\n\t\t\t\tAllowedHeaders:       []string{},\n\t\t\t\tExposedHeaders:       []string{},\n\t\t\t\tAllowCredentials:     false,\n\t\t\t\tMaxAge:               0,\n\t\t\t\tOptionsPassthrough:   false,\n\t\t\t\tOptionsSuccessStatus: 0,\n\t\t\t\tAllowPrivateNetwork:  false,\n\t\t\t},\n\t\t\tSetup: httpd.SetupConfig{\n\t\t\t\tInstallationCode:     \"\",\n\t\t\t\tInstallationCodeHint: defaultInstallCodeHint,\n\t\t\t},\n\t\t\tHideSupportLink: false,\n\t\t},\n\t\tHTTPConfig: httpclient.Config{\n\t\t\tTimeout:        20,\n\t\t\tRetryWaitMin:   2,\n\t\t\tRetryWaitMax:   30,\n\t\t\tRetryMax:       3,\n\t\t\tCACertificates: nil,\n\t\t\tCertificates:   nil,\n\t\t\tSkipTLSVerify:  false,\n\t\t\tHeaders:        nil,\n\t\t},\n\t\tCommandConfig: command.Config{\n\t\t\tTimeout:  30,\n\t\t\tEnv:      nil,\n\t\t\tCommands: nil,\n\t\t},\n\t\tKMSConfig: kms.Configuration{\n\t\t\tSecrets: kms.Secrets{\n\t\t\t\tURL:             \"\",\n\t\t\t\tMasterKeyString: \"\",\n\t\t\t\tMasterKeyPath:   \"\",\n\t\t\t},\n\t\t},\n\t\tMFAConfig: mfa.Config{\n\t\t\tTOTP: []mfa.TOTPConfig{defaultTOTP},\n\t\t},\n\t\tTelemetryConfig: telemetry.Conf{\n\t\t\tBindPort:           0,\n\t\t\tBindAddress:        \"127.0.0.1\",\n\t\t\tEnableProfiler:     false,\n\t\t\tAuthUserFile:       \"\",\n\t\t\tCertificateFile:    \"\",\n\t\t\tCertificateKeyFile: \"\",\n\t\t\tMinTLSVersion:      12,\n\t\t\tTLSCipherSuites:    nil,\n\t\t\tProtocols:          nil,\n\t\t},\n\t\tSMTPConfig: smtp.Config{\n\t\t\tHost:          \"\",\n\t\t\tPort:          587,\n\t\t\tFrom:          \"\",\n\t\t\tUser:          \"\",\n\t\t\tPassword:      \"\",\n\t\t\tAuthType:      0,\n\t\t\tEncryption:    0,\n\t\t\tDomain:        \"\",\n\t\t\tTemplatesPath: \"templates\",\n\t\t},\n\t\tPluginsConfig: nil,\n\t}\n\n\tviper.SetEnvPrefix(configEnvPrefix)\n\treplacer := strings.NewReplacer(\".\", \"__\")\n\tviper.SetEnvKeyReplacer(replacer)\n\tviper.SetConfigName(configName)\n\tsetViperDefaults()\n\tviper.AutomaticEnv()\n\tviper.AllowEmptyEnv(true)\n}\n\n// GetCommonConfig returns the common protocols configuration\nfunc GetCommonConfig() common.Configuration {\n\treturn globalConf.Common\n}\n\n// SetCommonConfig sets the common protocols configuration\nfunc SetCommonConfig(config common.Configuration) {\n\tglobalConf.Common = config\n}\n\n// GetSFTPDConfig returns the configuration for the SFTP server\nfunc GetSFTPDConfig() sftpd.Configuration {\n\treturn globalConf.SFTPD\n}\n\n// SetSFTPDConfig sets the configuration for the SFTP server\nfunc SetSFTPDConfig(config sftpd.Configuration) {\n\tglobalConf.SFTPD = config\n}\n\n// GetFTPDConfig returns the configuration for the FTP server\nfunc GetFTPDConfig() ftpd.Configuration {\n\treturn globalConf.FTPD\n}\n\n// SetFTPDConfig sets the configuration for the FTP server\nfunc SetFTPDConfig(config ftpd.Configuration) {\n\tglobalConf.FTPD = config\n}\n\n// GetWebDAVDConfig returns the configuration for the WebDAV server\nfunc GetWebDAVDConfig() webdavd.Configuration {\n\treturn globalConf.WebDAVD\n}\n\n// SetWebDAVDConfig sets the configuration for the WebDAV server\nfunc SetWebDAVDConfig(config webdavd.Configuration) {\n\tglobalConf.WebDAVD = config\n}\n\n// GetHTTPDConfig returns the configuration for the HTTP server\nfunc GetHTTPDConfig() httpd.Conf {\n\treturn globalConf.HTTPDConfig\n}\n\n// SetHTTPDConfig sets the configuration for the HTTP server\nfunc SetHTTPDConfig(config httpd.Conf) {\n\tglobalConf.HTTPDConfig = config\n}\n\n// GetProviderConf returns the configuration for the data provider\nfunc GetProviderConf() dataprovider.Config {\n\treturn globalConf.ProviderConf\n}\n\n// SetProviderConf sets the configuration for the data provider\nfunc SetProviderConf(config dataprovider.Config) {\n\tglobalConf.ProviderConf = config\n}\n\n// GetHTTPConfig returns the configuration for HTTP clients\nfunc GetHTTPConfig() httpclient.Config {\n\treturn globalConf.HTTPConfig\n}\n\n// GetCommandConfig returns the configuration for external commands\nfunc GetCommandConfig() command.Config {\n\treturn globalConf.CommandConfig\n}\n\n// GetKMSConfig returns the KMS configuration\nfunc GetKMSConfig() kms.Configuration {\n\treturn globalConf.KMSConfig\n}\n\n// SetKMSConfig sets the kms configuration\nfunc SetKMSConfig(config kms.Configuration) {\n\tglobalConf.KMSConfig = config\n}\n\n// GetTelemetryConfig returns the telemetry configuration\nfunc GetTelemetryConfig() telemetry.Conf {\n\treturn globalConf.TelemetryConfig\n}\n\n// SetTelemetryConfig sets the telemetry configuration\nfunc SetTelemetryConfig(config telemetry.Conf) {\n\tglobalConf.TelemetryConfig = config\n}\n\n// GetPluginsConfig returns the plugins configuration\nfunc GetPluginsConfig() []plugin.Config {\n\treturn globalConf.PluginsConfig\n}\n\n// SetPluginsConfig sets the plugin configuration\nfunc SetPluginsConfig(config []plugin.Config) {\n\tglobalConf.PluginsConfig = config\n}\n\n// HasKMSPlugin returns true if at least one KMS plugin is configured.\nfunc HasKMSPlugin() bool {\n\tfor _, c := range globalConf.PluginsConfig {\n\t\tif c.Type == kmsplugin.PluginName {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// GetMFAConfig returns multi-factor authentication config\nfunc GetMFAConfig() mfa.Config {\n\treturn globalConf.MFAConfig\n}\n\n// GetSMTPConfig returns the SMTP configuration\nfunc GetSMTPConfig() smtp.Config {\n\treturn globalConf.SMTPConfig\n}\n\n// GetACMEConfig returns the ACME configuration\nfunc GetACMEConfig() acme.Configuration {\n\treturn globalConf.ACME\n}\n\n// HasServicesToStart returns true if the config defines at least a service to start.\n// Supported services are SFTP, FTP and WebDAV\nfunc HasServicesToStart() bool {\n\tif globalConf.SFTPD.ShouldBind() {\n\t\treturn true\n\t}\n\tif globalConf.FTPD.ShouldBind() {\n\t\treturn true\n\t}\n\tif globalConf.WebDAVD.ShouldBind() {\n\t\treturn true\n\t}\n\tif globalConf.HTTPDConfig.ShouldBind() {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc getRedactedPassword(value string) string {\n\tif value == \"\" {\n\t\treturn value\n\t}\n\treturn \"[redacted]\"\n}\n\nfunc getRedactedGlobalConf() globalConfig {\n\tconf := globalConf\n\tconf.Common.Actions.Hook = util.GetRedactedURL(conf.Common.Actions.Hook)\n\tconf.Common.StartupHook = util.GetRedactedURL(conf.Common.StartupHook)\n\tconf.Common.PostConnectHook = util.GetRedactedURL(conf.Common.PostConnectHook)\n\tconf.Common.PostDisconnectHook = util.GetRedactedURL(conf.Common.PostDisconnectHook)\n\tconf.SFTPD.KeyboardInteractiveHook = util.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook)\n\tconf.HTTPDConfig.SigningPassphrase = getRedactedPassword(conf.HTTPDConfig.SigningPassphrase)\n\tconf.HTTPDConfig.Setup.InstallationCode = getRedactedPassword(conf.HTTPDConfig.Setup.InstallationCode)\n\tconf.ProviderConf.Password = getRedactedPassword(conf.ProviderConf.Password)\n\tconf.ProviderConf.Actions.Hook = util.GetRedactedURL(conf.ProviderConf.Actions.Hook)\n\tconf.ProviderConf.ExternalAuthHook = util.GetRedactedURL(conf.ProviderConf.ExternalAuthHook)\n\tconf.ProviderConf.PreLoginHook = util.GetRedactedURL(conf.ProviderConf.PreLoginHook)\n\tconf.ProviderConf.PostLoginHook = util.GetRedactedURL(conf.ProviderConf.PostLoginHook)\n\tconf.ProviderConf.CheckPasswordHook = util.GetRedactedURL(conf.ProviderConf.CheckPasswordHook)\n\tconf.SMTPConfig.Password = getRedactedPassword(conf.SMTPConfig.Password)\n\tconf.HTTPDConfig.Bindings = nil\n\tfor _, binding := range globalConf.HTTPDConfig.Bindings {\n\t\tbinding.OIDC.ClientID = getRedactedPassword(binding.OIDC.ClientID)\n\t\tbinding.OIDC.ClientSecret = getRedactedPassword(binding.OIDC.ClientSecret)\n\t\tconf.HTTPDConfig.Bindings = append(conf.HTTPDConfig.Bindings, binding)\n\t}\n\tconf.KMSConfig.Secrets.MasterKeyString = getRedactedPassword(conf.KMSConfig.Secrets.MasterKeyString)\n\tconf.PluginsConfig = nil\n\tfor _, plugin := range globalConf.PluginsConfig {\n\t\tvar args []string\n\t\tfor _, arg := range plugin.Args {\n\t\t\targs = append(args, getRedactedPassword(arg))\n\t\t}\n\t\tplugin.Args = args\n\t\tconf.PluginsConfig = append(conf.PluginsConfig, plugin)\n\t}\n\treturn conf\n}\n\nfunc setConfigFile(configDir, configFile string) {\n\tif configFile == \"\" {\n\t\treturn\n\t}\n\tif !filepath.IsAbs(configFile) && util.IsFileInputValid(configFile) {\n\t\tconfigFile = filepath.Join(configDir, configFile)\n\t}\n\tviper.SetConfigFile(configFile)\n}\n\n// readEnvFiles reads files inside the \"env.d\" directory relative to configDir\n// and then export the valid variables into environment variables if they do\n// not exist\nfunc readEnvFiles(configDir string) {\n\tenvd := filepath.Join(configDir, \"env.d\")\n\tentries, err := os.ReadDir(envd)\n\tif err != nil {\n\t\tlogger.Info(logSender, \"\", \"unable to read env files from %q: %v\", envd, err)\n\t\treturn\n\t}\n\tfor _, entry := range entries {\n\t\tinfo, err := entry.Info()\n\t\tif err == nil && info.Mode().IsRegular() {\n\t\t\tenvFile := filepath.Join(envd, entry.Name())\n\t\t\tif info.Size() > envFileMaxSize {\n\t\t\t\tlogger.Info(logSender, \"\", \"env file %q too big: %s, skipping\", entry.Name(), util.ByteCountIEC(info.Size()))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr = gotenv.Load(envFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(logSender, \"\", \"unable to load env vars from file %q, err: %v\", envFile, err)\n\t\t\t} else {\n\t\t\t\tlogger.Info(logSender, \"\", \"set env vars from file %q\", envFile)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc checkOverrideDefaultSettings() {\n\t// for slices we need to set the defaults to nil if the key is set in the config file,\n\t// otherwise the values are merged and not replaced as expected\n\trateLimiters := viper.Get(\"common.rate_limiters\")\n\tif val, ok := rateLimiters.([]any); ok {\n\t\tif len(val) > 0 {\n\t\t\tif rl, ok := val[0].(map[string]any); ok {\n\t\t\t\tif _, ok := rl[\"protocols\"]; ok {\n\t\t\t\t\tglobalConf.Common.RateLimitersConfig[0].Protocols = nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\thttpdBindings := viper.Get(\"httpd.bindings\")\n\tif val, ok := httpdBindings.([]any); ok {\n\t\tif len(val) > 0 {\n\t\t\tif binding, ok := val[0].(map[string]any); ok {\n\t\t\t\tif val, ok := binding[\"oidc\"]; ok {\n\t\t\t\t\tif oidc, ok := val.(map[string]any); ok {\n\t\t\t\t\t\tif _, ok := oidc[\"scopes\"]; ok {\n\t\t\t\t\t\t\tglobalConf.HTTPDConfig.Bindings[0].OIDC.Scopes = nil\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\tif slices.Contains(viper.AllKeys(), \"mfa.totp\") {\n\t\tglobalConf.MFAConfig.TOTP = nil\n\t}\n}\n\n// LoadConfig loads the configuration\n// configDir will be added to the configuration search paths.\n// The search path contains by default the current directory and on linux it contains\n// $HOME/.config/sftpgo and /etc/sftpgo too.\n// configFile is an absolute or relative path (to the config dir) to the configuration file.\nfunc LoadConfig(configDir, configFile string) error {\n\tvar err error\n\treadEnvFiles(configDir)\n\tviper.AddConfigPath(configDir)\n\tsetViperAdditionalConfigPaths()\n\tviper.AddConfigPath(\".\")\n\tsetConfigFile(configDir, configFile)\n\tif err = viper.ReadInConfig(); err != nil {\n\t\t// if the user specify a configuration file we get os.ErrNotExist.\n\t\t// viper.ConfigFileNotFoundError is returned if viper is unable\n\t\t// to find sftpgo.{json,yaml, etc..} in any of the search paths\n\t\tif errors.As(err, &viper.ConfigFileNotFoundError{}) {\n\t\t\tlogger.Debug(logSender, \"\", \"no configuration file found\")\n\t\t} else {\n\t\t\tlogger.Warn(logSender, \"\", \"error loading configuration file: %v\", err)\n\t\t\tlogger.WarnToConsole(\"error loading configuration file: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\tcheckOverrideDefaultSettings()\n\terr = viper.Unmarshal(&globalConf)\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error parsing configuration file: %v\", err)\n\t\tlogger.WarnToConsole(\"error parsing configuration file: %v\", err)\n\t\treturn err\n\t}\n\t// viper only supports slice of strings from env vars, so we use our custom method\n\tloadBindingsFromEnv()\n\tloadWebDAVCacheMappingsFromEnv()\n\tresetInvalidConfigs()\n\tlogger.Debug(logSender, \"\", \"config file used: '%q', config loaded: %+v\", viper.ConfigFileUsed(), getRedactedGlobalConf())\n\treturn nil\n}\n\nfunc isProxyProtocolValid() bool {\n\treturn globalConf.Common.ProxyProtocol >= 0 && globalConf.Common.ProxyProtocol <= 2\n}\n\nfunc isExternalAuthScopeValid() bool {\n\treturn globalConf.ProviderConf.ExternalAuthScope >= 0 && globalConf.ProviderConf.ExternalAuthScope <= 15\n}\n\nfunc resetInvalidConfigs() {\n\tif strings.TrimSpace(globalConf.HTTPDConfig.Setup.InstallationCodeHint) == \"\" {\n\t\tglobalConf.HTTPDConfig.Setup.InstallationCodeHint = defaultInstallCodeHint\n\t}\n\tif globalConf.ProviderConf.UsersBaseDir != \"\" && !util.IsFileInputValid(globalConf.ProviderConf.UsersBaseDir) {\n\t\twarn := fmt.Sprintf(\"invalid users base dir %q will be ignored\", globalConf.ProviderConf.UsersBaseDir)\n\t\tglobalConf.ProviderConf.UsersBaseDir = \"\"\n\t\tlogger.Warn(logSender, \"\", \"Non-fatal configuration error: %v\", warn)\n\t\tlogger.WarnToConsole(\"Non-fatal configuration error: %v\", warn)\n\t}\n\tif !isProxyProtocolValid() {\n\t\twarn := fmt.Sprintf(\"invalid proxy_protocol 0, 1 and 2 are supported, configured: %v reset proxy_protocol to 0\",\n\t\t\tglobalConf.Common.ProxyProtocol)\n\t\tglobalConf.Common.ProxyProtocol = 0\n\t\tlogger.Warn(logSender, \"\", \"Non-fatal configuration error: %v\", warn)\n\t\tlogger.WarnToConsole(\"Non-fatal configuration error: %v\", warn)\n\t}\n\tif !isExternalAuthScopeValid() {\n\t\twarn := fmt.Sprintf(\"invalid external_auth_scope: %v reset to 0\", globalConf.ProviderConf.ExternalAuthScope)\n\t\tglobalConf.ProviderConf.ExternalAuthScope = 0\n\t\tlogger.Warn(logSender, \"\", \"Non-fatal configuration error: %v\", warn)\n\t\tlogger.WarnToConsole(\"Non-fatal configuration error: %v\", warn)\n\t}\n\tif globalConf.Common.DefenderConfig.Enabled && globalConf.Common.DefenderConfig.Driver == common.DefenderDriverProvider {\n\t\tif !globalConf.ProviderConf.IsDefenderSupported() {\n\t\t\twarn := fmt.Sprintf(\"provider based defender is not supported with data provider %q, \"+\n\t\t\t\t\"the memory defender implementation will be used. If you want to use the provider defender \"+\n\t\t\t\t\"implementation please switch to a shared/distributed data provider\",\n\t\t\t\tglobalConf.ProviderConf.Driver)\n\t\t\tglobalConf.Common.DefenderConfig.Driver = common.DefenderDriverMemory\n\t\t\tlogger.Warn(logSender, \"\", \"Non-fatal configuration error: %v\", warn)\n\t\t\tlogger.WarnToConsole(\"Non-fatal configuration error: %v\", warn)\n\t\t}\n\t}\n\tif globalConf.Common.RenameMode < 0 || globalConf.Common.RenameMode > 1 {\n\t\twarn := fmt.Sprintf(\"invalid rename mode %d, reset to 0\", globalConf.Common.RenameMode)\n\t\tglobalConf.Common.RenameMode = 0\n\t\tlogger.Warn(logSender, \"\", \"Non-fatal configuration error: %v\", warn)\n\t\tlogger.WarnToConsole(\"Non-fatal configuration error: %v\", warn)\n\t}\n}\n\nfunc loadBindingsFromEnv() {\n\tfor idx := 0; idx < 10; idx++ {\n\t\tgetTOTPFromEnv(idx)\n\t\tgetRateLimitersFromEnv(idx)\n\t\tgetPluginsFromEnv(idx)\n\t\tgetSFTPDBindindFromEnv(idx)\n\t\tgetFTPDBindingFromEnv(idx)\n\t\tgetWebDAVDBindingFromEnv(idx)\n\t\tgetHTTPDBindingFromEnv(idx)\n\t\tgetHTTPClientCertificatesFromEnv(idx)\n\t\tgetHTTPClientHeadersFromEnv(idx)\n\t\tgetCommandConfigsFromEnv(idx)\n\t}\n}\n\nfunc getTOTPFromEnv(idx int) {\n\ttotpConfig := defaultTOTP\n\tif len(globalConf.MFAConfig.TOTP) > idx {\n\t\ttotpConfig = globalConf.MFAConfig.TOTP[idx]\n\t}\n\n\tisSet := false\n\n\tname, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_MFA__TOTP__%v__NAME\", idx))\n\tif ok {\n\t\ttotpConfig.Name = name\n\t\tisSet = true\n\t}\n\n\tissuer, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_MFA__TOTP__%v__ISSUER\", idx))\n\tif ok {\n\t\ttotpConfig.Issuer = issuer\n\t\tisSet = true\n\t}\n\n\talgo, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_MFA__TOTP__%v__ALGO\", idx))\n\tif ok {\n\t\ttotpConfig.Algo = algo\n\t\tisSet = true\n\t}\n\n\tif isSet {\n\t\tif len(globalConf.MFAConfig.TOTP) > idx {\n\t\t\tglobalConf.MFAConfig.TOTP[idx] = totpConfig\n\t\t} else {\n\t\t\tglobalConf.MFAConfig.TOTP = append(globalConf.MFAConfig.TOTP, totpConfig)\n\t\t}\n\t}\n}\n\nfunc getRateLimitersFromEnv(idx int) {\n\trtlConfig := defaultRateLimiter\n\tif len(globalConf.Common.RateLimitersConfig) > idx {\n\t\trtlConfig = globalConf.Common.RateLimitersConfig[idx]\n\t}\n\n\tisSet := false\n\n\taverage, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_COMMON__RATE_LIMITERS__%v__AVERAGE\", idx), 64)\n\tif ok {\n\t\trtlConfig.Average = average\n\t\tisSet = true\n\t}\n\n\tperiod, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_COMMON__RATE_LIMITERS__%v__PERIOD\", idx), 64)\n\tif ok {\n\t\trtlConfig.Period = period\n\t\tisSet = true\n\t}\n\n\tburst, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_COMMON__RATE_LIMITERS__%v__BURST\", idx), 32)\n\tif ok {\n\t\trtlConfig.Burst = int(burst)\n\t\tisSet = true\n\t}\n\n\trtlType, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_COMMON__RATE_LIMITERS__%v__TYPE\", idx), 32)\n\tif ok {\n\t\trtlConfig.Type = int(rtlType)\n\t\tisSet = true\n\t}\n\n\tprotocols, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_COMMON__RATE_LIMITERS__%v__PROTOCOLS\", idx))\n\tif ok {\n\t\trtlConfig.Protocols = protocols\n\t\tisSet = true\n\t}\n\n\tgenerateEvents, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_COMMON__RATE_LIMITERS__%v__GENERATE_DEFENDER_EVENTS\", idx))\n\tif ok {\n\t\trtlConfig.GenerateDefenderEvents = generateEvents\n\t\tisSet = true\n\t}\n\n\tsoftLimit, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_SOFT_LIMIT\", idx), 32)\n\tif ok {\n\t\trtlConfig.EntriesSoftLimit = int(softLimit)\n\t\tisSet = true\n\t}\n\n\thardLimit, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_HARD_LIMIT\", idx), 32)\n\tif ok {\n\t\trtlConfig.EntriesHardLimit = int(hardLimit)\n\t\tisSet = true\n\t}\n\n\tif isSet {\n\t\tif len(globalConf.Common.RateLimitersConfig) > idx {\n\t\t\tglobalConf.Common.RateLimitersConfig[idx] = rtlConfig\n\t\t} else {\n\t\t\tglobalConf.Common.RateLimitersConfig = append(globalConf.Common.RateLimitersConfig, rtlConfig)\n\t\t}\n\t}\n}\n\nfunc getKMSPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {\n\tisSet := false\n\n\tkmsScheme, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__KMS_OPTIONS__SCHEME\", idx))\n\tif ok {\n\t\tpluginConfig.KMSOptions.Scheme = kmsScheme\n\t\tisSet = true\n\t}\n\n\tkmsEncStatus, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__KMS_OPTIONS__ENCRYPTED_STATUS\", idx))\n\tif ok {\n\t\tpluginConfig.KMSOptions.EncryptedStatus = kmsEncStatus\n\t\tisSet = true\n\t}\n\n\treturn isSet\n}\n\nfunc getAuthPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {\n\tisSet := false\n\n\tauthScope, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__AUTH_OPTIONS__SCOPE\", idx), 32)\n\tif ok {\n\t\tpluginConfig.AuthOptions.Scope = int(authScope)\n\t\tisSet = true\n\t}\n\n\treturn isSet\n}\n\nfunc getNotifierPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {\n\tisSet := false\n\n\tnotifierFsEvents, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__FS_EVENTS\", idx))\n\tif ok {\n\t\tpluginConfig.NotifierOptions.FsEvents = notifierFsEvents\n\t\tisSet = true\n\t}\n\n\tnotifierProviderEvents, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__PROVIDER_EVENTS\", idx))\n\tif ok {\n\t\tpluginConfig.NotifierOptions.ProviderEvents = notifierProviderEvents\n\t\tisSet = true\n\t}\n\n\tnotifierProviderObjects, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__PROVIDER_OBJECTS\", idx))\n\tif ok {\n\t\tpluginConfig.NotifierOptions.ProviderObjects = notifierProviderObjects\n\t\tisSet = true\n\t}\n\n\tnotifierLogEventsString, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__LOG_EVENTS\", idx))\n\tif ok {\n\t\tvar notifierLogEvents []int\n\t\tfor _, e := range notifierLogEventsString {\n\t\t\tev, err := strconv.Atoi(e)\n\t\t\tif err == nil {\n\t\t\t\tnotifierLogEvents = append(notifierLogEvents, ev)\n\t\t\t}\n\t\t}\n\t\tif len(notifierLogEvents) > 0 {\n\t\t\tpluginConfig.NotifierOptions.LogEvents = notifierLogEvents\n\t\t\tisSet = true\n\t\t}\n\t}\n\n\tnotifierRetryMaxTime, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_MAX_TIME\", idx), 32)\n\tif ok {\n\t\tpluginConfig.NotifierOptions.RetryMaxTime = int(notifierRetryMaxTime)\n\t\tisSet = true\n\t}\n\n\tnotifierRetryQueueMaxSize, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE\", idx), 32)\n\tif ok {\n\t\tpluginConfig.NotifierOptions.RetryQueueMaxSize = int(notifierRetryQueueMaxSize)\n\t\tisSet = true\n\t}\n\n\treturn isSet\n}\n\nfunc getPluginsFromEnv(idx int) {\n\tpluginConfig := plugin.Config{}\n\tif len(globalConf.PluginsConfig) > idx {\n\t\tpluginConfig = globalConf.PluginsConfig[idx]\n\t}\n\n\tisSet := false\n\n\tpluginType, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__TYPE\", idx))\n\tif ok {\n\t\tpluginConfig.Type = pluginType\n\t\tisSet = true\n\t}\n\n\tif getNotifierPluginFromEnv(idx, &pluginConfig) {\n\t\tisSet = true\n\t}\n\n\tif getKMSPluginFromEnv(idx, &pluginConfig) {\n\t\tisSet = true\n\t}\n\n\tif getAuthPluginFromEnv(idx, &pluginConfig) {\n\t\tisSet = true\n\t}\n\n\tcmd, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__CMD\", idx))\n\tif ok {\n\t\tpluginConfig.Cmd = cmd\n\t\tisSet = true\n\t}\n\n\tcmdArgs, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__ARGS\", idx))\n\tif ok {\n\t\tpluginConfig.Args = cmdArgs\n\t\tisSet = true\n\t}\n\n\tpluginHash, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__SHA256SUM\", idx))\n\tif ok {\n\t\tpluginConfig.SHA256Sum = pluginHash\n\t\tisSet = true\n\t}\n\n\tautoMTLS, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__AUTO_MTLS\", idx))\n\tif ok {\n\t\tpluginConfig.AutoMTLS = autoMTLS\n\t\tisSet = true\n\t}\n\n\tenvPrefix, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__ENV_PREFIX\", idx))\n\tif ok {\n\t\tpluginConfig.EnvPrefix = envPrefix\n\t\tisSet = true\n\t}\n\n\tenvVars, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_PLUGINS__%v__ENV_VARS\", idx))\n\tif ok {\n\t\tpluginConfig.EnvVars = envVars\n\t\tisSet = true\n\t}\n\n\tif isSet {\n\t\tif len(globalConf.PluginsConfig) > idx {\n\t\t\tglobalConf.PluginsConfig[idx] = pluginConfig\n\t\t} else {\n\t\t\tglobalConf.PluginsConfig = append(globalConf.PluginsConfig, pluginConfig)\n\t\t}\n\t}\n}\n\nfunc getSFTPDBindindFromEnv(idx int) {\n\tbinding := defaultSFTPDBinding\n\tif len(globalConf.SFTPD.Bindings) > idx {\n\t\tbinding = globalConf.SFTPD.Bindings[idx]\n\t}\n\n\tisSet := false\n\n\tport, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_SFTPD__BINDINGS__%v__PORT\", idx), 32)\n\tif ok {\n\t\tbinding.Port = int(port)\n\t\tisSet = true\n\t}\n\n\taddress, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_SFTPD__BINDINGS__%v__ADDRESS\", idx))\n\tif ok {\n\t\tbinding.Address = address\n\t\tisSet = true\n\t}\n\n\tapplyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_SFTPD__BINDINGS__%v__APPLY_PROXY_CONFIG\", idx))\n\tif ok {\n\t\tbinding.ApplyProxyConfig = applyProxyConfig\n\t\tisSet = true\n\t}\n\n\tif isSet {\n\t\tif len(globalConf.SFTPD.Bindings) > idx {\n\t\t\tglobalConf.SFTPD.Bindings[idx] = binding\n\t\t} else {\n\t\t\tglobalConf.SFTPD.Bindings = append(globalConf.SFTPD.Bindings, binding)\n\t\t}\n\t}\n}\n\nfunc getFTPDPassiveIPOverridesFromEnv(idx int) []ftpd.PassiveIPOverride {\n\tvar overrides []ftpd.PassiveIPOverride\n\tif len(globalConf.FTPD.Bindings) > idx {\n\t\toverrides = globalConf.FTPD.Bindings[idx].PassiveIPOverrides\n\t}\n\n\tfor subIdx := 0; subIdx < 10; subIdx++ {\n\t\tvar override ftpd.PassiveIPOverride\n\t\tvar replace bool\n\t\tif len(globalConf.FTPD.Bindings) > idx && len(globalConf.FTPD.Bindings[idx].PassiveIPOverrides) > subIdx {\n\t\t\toverride = globalConf.FTPD.Bindings[idx].PassiveIPOverrides[subIdx]\n\t\t\treplace = true\n\t\t}\n\n\t\tip, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__PASSIVE_IP_OVERRIDES__%v__IP\", idx, subIdx))\n\t\tif ok {\n\t\t\toverride.IP = ip\n\t\t}\n\n\t\tnetworks, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__PASSIVE_IP_OVERRIDES__%v__NETWORKS\",\n\t\t\tidx, subIdx))\n\t\tif ok {\n\t\t\toverride.Networks = networks\n\t\t}\n\n\t\tif len(override.Networks) > 0 {\n\t\t\tif replace {\n\t\t\t\toverrides[subIdx] = override\n\t\t\t} else {\n\t\t\t\toverrides = append(overrides, override)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn overrides\n}\n\nfunc getDefaultFTPDBinding(idx int) ftpd.Binding {\n\tbinding := defaultFTPDBinding\n\tif len(globalConf.FTPD.Bindings) > idx {\n\t\tbinding = globalConf.FTPD.Bindings[idx]\n\t}\n\treturn binding\n}\n\nfunc getFTPDBindingSecurityFromEnv(idx int, binding *ftpd.Binding) bool {\n\tisSet := false\n\n\tcertificateFile, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_FILE\", idx))\n\tif ok {\n\t\tbinding.CertificateFile = certificateFile\n\t\tisSet = true\n\t}\n\n\tcertificateKeyFile, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_KEY_FILE\", idx))\n\tif ok {\n\t\tbinding.CertificateKeyFile = certificateKeyFile\n\t\tisSet = true\n\t}\n\n\ttlsMode, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__TLS_MODE\", idx), 32)\n\tif ok {\n\t\tbinding.TLSMode = int(tlsMode)\n\t\tisSet = true\n\t}\n\n\ttlsVer, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__MIN_TLS_VERSION\", idx), 32)\n\tif ok {\n\t\tbinding.MinTLSVersion = int(tlsVer)\n\t\tisSet = true\n\t}\n\n\ttlsCiphers, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__TLS_CIPHER_SUITES\", idx))\n\tif ok {\n\t\tbinding.TLSCipherSuites = tlsCiphers\n\t\tisSet = true\n\t}\n\n\tclientAuthType, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE\", idx), 32)\n\tif ok {\n\t\tbinding.ClientAuthType = int(clientAuthType)\n\t\tisSet = true\n\t}\n\n\tpasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY\", idx), 32)\n\tif ok {\n\t\tbinding.PassiveConnectionsSecurity = int(pasvSecurity)\n\t\tisSet = true\n\t}\n\n\tactiveSecurity, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__ACTIVE_CONNECTIONS_SECURITY\", idx), 32)\n\tif ok {\n\t\tbinding.ActiveConnectionsSecurity = int(activeSecurity)\n\t\tisSet = true\n\t}\n\n\treturn isSet\n}\n\nfunc getFTPDBindingFromEnv(idx int) {\n\tbinding := getDefaultFTPDBinding(idx)\n\tisSet := false\n\n\tport, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__PORT\", idx), 32)\n\tif ok {\n\t\tbinding.Port = int(port)\n\t\tisSet = true\n\t}\n\n\taddress, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__ADDRESS\", idx))\n\tif ok {\n\t\tbinding.Address = address\n\t\tisSet = true\n\t}\n\n\tapplyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__APPLY_PROXY_CONFIG\", idx))\n\tif ok {\n\t\tbinding.ApplyProxyConfig = applyProxyConfig\n\t\tisSet = true\n\t}\n\n\tpassiveIP, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__FORCE_PASSIVE_IP\", idx))\n\tif ok {\n\t\tbinding.ForcePassiveIP = passiveIP\n\t\tisSet = true\n\t}\n\n\tpassiveIPOverrides := getFTPDPassiveIPOverridesFromEnv(idx)\n\tif len(passiveIPOverrides) > 0 {\n\t\tbinding.PassiveIPOverrides = passiveIPOverrides\n\t\tisSet = true\n\t}\n\n\tpassiveHost, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__PASSIVE_HOST\", idx))\n\tif ok {\n\t\tbinding.PassiveHost = passiveHost\n\t\tisSet = true\n\t}\n\n\tdebug, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_FTPD__BINDINGS__%v__DEBUG\", idx))\n\tif ok {\n\t\tbinding.Debug = debug\n\t\tisSet = true\n\t}\n\n\tif getFTPDBindingSecurityFromEnv(idx, &binding) {\n\t\tisSet = true\n\t}\n\n\tapplyFTPDBindingFromEnv(idx, isSet, binding)\n}\n\nfunc applyFTPDBindingFromEnv(idx int, isSet bool, binding ftpd.Binding) {\n\tif isSet {\n\t\tif len(globalConf.FTPD.Bindings) > idx {\n\t\t\tglobalConf.FTPD.Bindings[idx] = binding\n\t\t} else {\n\t\t\tglobalConf.FTPD.Bindings = append(globalConf.FTPD.Bindings, binding)\n\t\t}\n\t}\n}\n\nfunc getWebDAVBindingHTTPSConfigsFromEnv(idx int, binding *webdavd.Binding) bool {\n\tisSet := false\n\n\tenableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__ENABLE_HTTPS\", idx))\n\tif ok {\n\t\tbinding.EnableHTTPS = enableHTTPS\n\t\tisSet = true\n\t}\n\n\tcertificateFile, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__CERTIFICATE_FILE\", idx))\n\tif ok {\n\t\tbinding.CertificateFile = certificateFile\n\t\tisSet = true\n\t}\n\n\tcertificateKeyFile, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__CERTIFICATE_KEY_FILE\", idx))\n\tif ok {\n\t\tbinding.CertificateKeyFile = certificateKeyFile\n\t\tisSet = true\n\t}\n\n\ttlsVer, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__MIN_TLS_VERSION\", idx), 32)\n\tif ok {\n\t\tbinding.MinTLSVersion = int(tlsVer)\n\t\tisSet = true\n\t}\n\n\tclientAuthType, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_AUTH_TYPE\", idx), 32)\n\tif ok {\n\t\tbinding.ClientAuthType = int(clientAuthType)\n\t\tisSet = true\n\t}\n\n\ttlsCiphers, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__TLS_CIPHER_SUITES\", idx))\n\tif ok {\n\t\tbinding.TLSCipherSuites = tlsCiphers\n\t\tisSet = true\n\t}\n\n\tprotocols, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%d__TLS_PROTOCOLS\", idx))\n\tif ok {\n\t\tbinding.Protocols = protocols\n\t\tisSet = true\n\t}\n\n\treturn isSet\n}\n\nfunc getWebDAVDBindingProxyConfigsFromEnv(idx int, binding *webdavd.Binding) bool {\n\tisSet := false\n\n\tproxyMode, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_MODE\", idx), 32)\n\tif ok {\n\t\tbinding.ProxyMode = int(proxyMode)\n\t\tisSet = true\n\t}\n\n\tproxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_ALLOWED\", idx))\n\tif ok {\n\t\tbinding.ProxyAllowed = proxyAllowed\n\t\tisSet = true\n\t}\n\n\tclientIPProxyHeader, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_PROXY_HEADER\", idx))\n\tif ok {\n\t\tbinding.ClientIPProxyHeader = clientIPProxyHeader\n\t\tisSet = true\n\t}\n\n\tclientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH\", idx), 32)\n\tif ok {\n\t\tbinding.ClientIPHeaderDepth = int(clientIPHeaderDepth)\n\t\tisSet = true\n\t}\n\n\treturn isSet\n}\n\nfunc loadWebDAVCacheMappingsFromEnv() []webdavd.CustomMimeMapping {\n\tfor idx := 0; idx < 30; idx++ {\n\t\text, extOK := os.LookupEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__%d__EXT\", idx))\n\t\tmime, mimeOK := os.LookupEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__%d__MIME\", idx))\n\t\tif extOK && mimeOK {\n\t\t\tif len(globalConf.WebDAVD.Cache.MimeTypes.CustomMappings) > idx {\n\t\t\t\tglobalConf.WebDAVD.Cache.MimeTypes.CustomMappings[idx].Ext = ext\n\t\t\t\tglobalConf.WebDAVD.Cache.MimeTypes.CustomMappings[idx].Mime = mime\n\t\t\t} else {\n\t\t\t\tglobalConf.WebDAVD.Cache.MimeTypes.CustomMappings = append(globalConf.WebDAVD.Cache.MimeTypes.CustomMappings,\n\t\t\t\t\twebdavd.CustomMimeMapping{\n\t\t\t\t\t\tExt:  ext,\n\t\t\t\t\t\tMime: mime,\n\t\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn globalConf.WebDAVD.Cache.MimeTypes.CustomMappings\n}\n\nfunc getWebDAVDBindingFromEnv(idx int) {\n\tbinding := defaultWebDAVDBinding\n\tif len(globalConf.WebDAVD.Bindings) > idx {\n\t\tbinding = globalConf.WebDAVD.Bindings[idx]\n\t}\n\n\tisSet := false\n\n\tport, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__PORT\", idx), 32)\n\tif ok {\n\t\tbinding.Port = int(port)\n\t\tisSet = true\n\t}\n\n\taddress, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__ADDRESS\", idx))\n\tif ok {\n\t\tbinding.Address = address\n\t\tisSet = true\n\t}\n\n\tif getWebDAVBindingHTTPSConfigsFromEnv(idx, &binding) {\n\t\tisSet = true\n\t}\n\n\tprefix, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__PREFIX\", idx))\n\tif ok {\n\t\tbinding.Prefix = prefix\n\t\tisSet = true\n\t}\n\n\tif getWebDAVDBindingProxyConfigsFromEnv(idx, &binding) {\n\t\tisSet = true\n\t}\n\n\tdisableWWWAuth, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_WEBDAVD__BINDINGS__%v__DISABLE_WWW_AUTH_HEADER\", idx))\n\tif ok {\n\t\tbinding.DisableWWWAuthHeader = disableWWWAuth\n\t\tisSet = true\n\t}\n\n\tif isSet {\n\t\tif len(globalConf.WebDAVD.Bindings) > idx {\n\t\t\tglobalConf.WebDAVD.Bindings[idx] = binding\n\t\t} else {\n\t\t\tglobalConf.WebDAVD.Bindings = append(globalConf.WebDAVD.Bindings, binding)\n\t\t}\n\t}\n}\n\nfunc getHTTPDSecurityProxyHeadersFromEnv(idx int) []httpd.HTTPSProxyHeader {\n\tvar httpsProxyHeaders []httpd.HTTPSProxyHeader\n\tif len(globalConf.HTTPDConfig.Bindings) > idx {\n\t\thttpsProxyHeaders = globalConf.HTTPDConfig.Bindings[idx].Security.HTTPSProxyHeaders\n\t}\n\n\tfor subIdx := 0; subIdx < 10; subIdx++ {\n\t\tvar httpsProxyHeader httpd.HTTPSProxyHeader\n\t\tvar replace bool\n\t\tif len(globalConf.HTTPDConfig.Bindings) > idx &&\n\t\t\tlen(globalConf.HTTPDConfig.Bindings[idx].Security.HTTPSProxyHeaders) > subIdx {\n\t\t\thttpsProxyHeader = httpsProxyHeaders[subIdx]\n\t\t\treplace = true\n\t\t}\n\t\tproxyKey, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HTTPS_PROXY_HEADERS__%v__KEY\",\n\t\t\tidx, subIdx))\n\t\tif ok {\n\t\t\thttpsProxyHeader.Key = proxyKey\n\t\t}\n\t\tproxyVal, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HTTPS_PROXY_HEADERS__%v__VALUE\",\n\t\t\tidx, subIdx))\n\t\tif ok {\n\t\t\thttpsProxyHeader.Value = proxyVal\n\t\t}\n\t\tif httpsProxyHeader.Key != \"\" && httpsProxyHeader.Value != \"\" {\n\t\t\tif replace {\n\t\t\t\thttpsProxyHeaders[subIdx] = httpsProxyHeader\n\t\t\t} else {\n\t\t\t\thttpsProxyHeaders = append(httpsProxyHeaders, httpsProxyHeader)\n\t\t\t}\n\t\t}\n\t}\n\treturn httpsProxyHeaders\n}\n\nfunc getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { //nolint:gocyclo\n\tresult := defaultHTTPDBinding.Security\n\tif len(globalConf.HTTPDConfig.Bindings) > idx {\n\t\tresult = globalConf.HTTPDConfig.Bindings[idx].Security\n\t}\n\tisSet := false\n\n\tenabled, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__ENABLED\", idx))\n\tif ok {\n\t\tresult.Enabled = enabled\n\t\tisSet = true\n\t}\n\n\tallowedHosts, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__ALLOWED_HOSTS\", idx))\n\tif ok {\n\t\tresult.AllowedHosts = allowedHosts\n\t\tisSet = true\n\t}\n\n\tallowedHostsAreRegex, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__ALLOWED_HOSTS_ARE_REGEX\", idx))\n\tif ok {\n\t\tresult.AllowedHostsAreRegex = allowedHostsAreRegex\n\t\tisSet = true\n\t}\n\n\thostsProxyHeaders, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HOSTS_PROXY_HEADERS\", idx))\n\tif ok {\n\t\tresult.HostsProxyHeaders = hostsProxyHeaders\n\t\tisSet = true\n\t}\n\n\thttpsRedirect, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HTTPS_REDIRECT\", idx))\n\tif ok {\n\t\tresult.HTTPSRedirect = httpsRedirect\n\t\tisSet = true\n\t}\n\n\thttpsHost, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HTTPS_HOST\", idx))\n\tif ok {\n\t\tresult.HTTPSHost = httpsHost\n\t\tisSet = true\n\t}\n\n\thttpsProxyHeaders := getHTTPDSecurityProxyHeadersFromEnv(idx)\n\tif len(httpsProxyHeaders) > 0 {\n\t\tresult.HTTPSProxyHeaders = httpsProxyHeaders\n\t\tisSet = true\n\t}\n\n\tstsSeconds, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__STS_SECONDS\", idx), 64)\n\tif ok {\n\t\tresult.STSSeconds = stsSeconds\n\t\tisSet = true\n\t}\n\n\tstsIncludeSubDomains, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__STS_INCLUDE_SUBDOMAINS\", idx))\n\tif ok {\n\t\tresult.STSIncludeSubdomains = stsIncludeSubDomains\n\t\tisSet = true\n\t}\n\n\tstsPreload, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__STS_PRELOAD\", idx))\n\tif ok {\n\t\tresult.STSPreload = stsPreload\n\t\tisSet = true\n\t}\n\n\tcontentTypeNosniff, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CONTENT_TYPE_NOSNIFF\", idx))\n\tif ok {\n\t\tresult.ContentTypeNosniff = contentTypeNosniff\n\t\tisSet = true\n\t}\n\n\tcontentSecurityPolicy, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CONTENT_SECURITY_POLICY\", idx))\n\tif ok {\n\t\tresult.ContentSecurityPolicy = contentSecurityPolicy\n\t\tisSet = true\n\t}\n\n\tpermissionsPolicy, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__PERMISSIONS_POLICY\", idx))\n\tif ok {\n\t\tresult.PermissionsPolicy = permissionsPolicy\n\t\tisSet = true\n\t}\n\n\tcrossOriginOpenerPolicy, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_OPENER_POLICY\", idx))\n\tif ok {\n\t\tresult.CrossOriginOpenerPolicy = crossOriginOpenerPolicy\n\t\tisSet = true\n\t}\n\n\tcrossOriginResourcePolicy, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY\", idx))\n\tif ok {\n\t\tresult.CrossOriginResourcePolicy = crossOriginResourcePolicy\n\t\tisSet = true\n\t}\n\n\tcrossOriginEmbedderPolicy, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY\", idx))\n\tif ok {\n\t\tresult.CrossOriginEmbedderPolicy = crossOriginEmbedderPolicy\n\t\tisSet = true\n\t}\n\n\treferredPolicy, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__REFERRER_POLICY\", idx))\n\tif ok {\n\t\tresult.ReferrerPolicy = referredPolicy\n\t\tisSet = true\n\t}\n\n\tcacheControl, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CACHE_CONTROL\", idx))\n\tif ok {\n\t\tresult.CacheControl = cacheControl\n\t\tisSet = true\n\t}\n\n\treturn result, isSet\n}\n\nfunc getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) {\n\tresult := defaultHTTPDBinding.OIDC\n\tif len(globalConf.HTTPDConfig.Bindings) > idx {\n\t\tresult = globalConf.HTTPDConfig.Bindings[idx].OIDC\n\t}\n\tisSet := false\n\n\tclientID, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__CLIENT_ID\", idx))\n\tif ok {\n\t\tresult.ClientID = clientID\n\t\tisSet = true\n\t}\n\n\tclientSecret, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__CLIENT_SECRET\", idx))\n\tif ok {\n\t\tresult.ClientSecret = clientSecret\n\t\tisSet = true\n\t}\n\n\tclientSecretFile, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__CLIENT_SECRET_FILE\", idx))\n\tif ok {\n\t\tresult.ClientSecretFile = clientSecretFile\n\t\tisSet = true\n\t}\n\n\tconfigURL, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__CONFIG_URL\", idx))\n\tif ok {\n\t\tresult.ConfigURL = configURL\n\t\tisSet = true\n\t}\n\n\tredirectBaseURL, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__REDIRECT_BASE_URL\", idx))\n\tif ok {\n\t\tresult.RedirectBaseURL = redirectBaseURL\n\t\tisSet = true\n\t}\n\n\tusernameField, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__USERNAME_FIELD\", idx))\n\tif ok {\n\t\tresult.UsernameField = usernameField\n\t\tisSet = true\n\t}\n\n\tscopes, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__SCOPES\", idx))\n\tif ok {\n\t\tresult.Scopes = scopes\n\t\tisSet = true\n\t}\n\n\troleField, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__ROLE_FIELD\", idx))\n\tif ok {\n\t\tresult.RoleField = roleField\n\t\tisSet = true\n\t}\n\n\timplicitRoles, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__IMPLICIT_ROLES\", idx))\n\tif ok {\n\t\tresult.ImplicitRoles = implicitRoles\n\t\tisSet = true\n\t}\n\n\tcustomFields, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__CUSTOM_FIELDS\", idx))\n\tif ok {\n\t\tresult.CustomFields = customFields\n\t\tisSet = true\n\t}\n\n\tskipSignatureCheck, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__INSECURE_SKIP_SIGNATURE_CHECK\", idx))\n\tif ok {\n\t\tresult.InsecureSkipSignatureCheck = skipSignatureCheck\n\t\tisSet = true\n\t}\n\n\tdebug, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__OIDC__DEBUG\", idx))\n\tif ok {\n\t\tresult.Debug = debug\n\t\tisSet = true\n\t}\n\n\treturn result, isSet\n}\n\nfunc getHTTPDUIBrandingFromEnv(prefix string, branding httpd.UIBranding) (httpd.UIBranding, bool) {\n\tisSet := false\n\n\tname, ok := os.LookupEnv(fmt.Sprintf(\"%s__NAME\", prefix))\n\tif ok {\n\t\tbranding.Name = name\n\t\tisSet = true\n\t}\n\n\tshortName, ok := os.LookupEnv(fmt.Sprintf(\"%s__SHORT_NAME\", prefix))\n\tif ok {\n\t\tbranding.ShortName = shortName\n\t\tisSet = true\n\t}\n\n\tfaviconPath, ok := os.LookupEnv(fmt.Sprintf(\"%s__FAVICON_PATH\", prefix))\n\tif ok {\n\t\tbranding.FaviconPath = faviconPath\n\t\tisSet = true\n\t}\n\n\tlogoPath, ok := os.LookupEnv(fmt.Sprintf(\"%s__LOGO_PATH\", prefix))\n\tif ok {\n\t\tbranding.LogoPath = logoPath\n\t\tisSet = true\n\t}\n\n\tdisclaimerName, ok := os.LookupEnv(fmt.Sprintf(\"%s__DISCLAIMER_NAME\", prefix))\n\tif ok {\n\t\tbranding.DisclaimerName = disclaimerName\n\t\tisSet = true\n\t}\n\n\tdisclaimerPath, ok := os.LookupEnv(fmt.Sprintf(\"%s__DISCLAIMER_PATH\", prefix))\n\tif ok {\n\t\tbranding.DisclaimerPath = disclaimerPath\n\t\tisSet = true\n\t}\n\n\tdefaultCSSPath, ok := lookupStringListFromEnv(fmt.Sprintf(\"%s__DEFAULT_CSS\", prefix))\n\tif ok {\n\t\tbranding.DefaultCSS = defaultCSSPath\n\t\tisSet = true\n\t}\n\n\textraCSS, ok := lookupStringListFromEnv(fmt.Sprintf(\"%s__EXTRA_CSS\", prefix))\n\tif ok {\n\t\tbranding.ExtraCSS = extraCSS\n\t\tisSet = true\n\t}\n\n\treturn branding, isSet\n}\n\nfunc getHTTPDBrandingFromEnv(idx int) (httpd.Branding, bool) {\n\tresult := defaultHTTPDBinding.Branding\n\tif len(globalConf.HTTPDConfig.Bindings) > idx {\n\t\tresult = globalConf.HTTPDConfig.Bindings[idx].Branding\n\t}\n\tisSet := false\n\n\twebAdmin, ok := getHTTPDUIBrandingFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__BRANDING__WEB_ADMIN\", idx),\n\t\tresult.WebAdmin)\n\tif ok {\n\t\tresult.WebAdmin = webAdmin\n\t\tisSet = true\n\t}\n\n\twebClient, ok := getHTTPDUIBrandingFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__BRANDING__WEB_CLIENT\", idx),\n\t\tresult.WebClient)\n\tif ok {\n\t\tresult.WebClient = webClient\n\t\tisSet = true\n\t}\n\n\treturn result, isSet\n}\n\nfunc getDefaultHTTPBinding(idx int) httpd.Binding {\n\tbinding := defaultHTTPDBinding\n\tif len(globalConf.HTTPDConfig.Bindings) > idx {\n\t\tbinding = globalConf.HTTPDConfig.Bindings[idx]\n\t}\n\treturn binding\n}\n\nfunc getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool {\n\tisSet := false\n\n\toidc, ok := getHTTPDOIDCFromEnv(idx)\n\tif ok {\n\t\tbinding.OIDC = oidc\n\t\tisSet = true\n\t}\n\n\tsecurityConf, ok := getHTTPDSecurityConfFromEnv(idx)\n\tif ok {\n\t\tbinding.Security = securityConf\n\t\tisSet = true\n\t}\n\n\tbrandingConf, ok := getHTTPDBrandingFromEnv(idx)\n\tif ok {\n\t\tbinding.Branding = brandingConf\n\t\tisSet = true\n\t}\n\n\treturn isSet\n}\n\nfunc getHTTPDBindingProxyConfigsFromEnv(idx int, binding *httpd.Binding) bool {\n\tisSet := false\n\n\tproxyMode, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__PROXY_MODE\", idx), 32)\n\tif ok {\n\t\tbinding.ProxyMode = int(proxyMode)\n\t\tisSet = true\n\t}\n\n\tproxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__PROXY_ALLOWED\", idx))\n\tif ok {\n\t\tbinding.ProxyAllowed = proxyAllowed\n\t\tisSet = true\n\t}\n\n\tclientIPProxyHeader, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_PROXY_HEADER\", idx))\n\tif ok {\n\t\tbinding.ClientIPProxyHeader = clientIPProxyHeader\n\t\tisSet = true\n\t}\n\n\tclientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH\", idx), 32)\n\tif ok {\n\t\tbinding.ClientIPHeaderDepth = int(clientIPHeaderDepth)\n\t\tisSet = true\n\t}\n\n\treturn isSet\n}\n\nfunc getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo\n\tbinding := getDefaultHTTPBinding(idx)\n\tisSet := false\n\n\tport, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__PORT\", idx), 32)\n\tif ok {\n\t\tbinding.Port = int(port)\n\t\tisSet = true\n\t}\n\n\taddress, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__ADDRESS\", idx))\n\tif ok {\n\t\tbinding.Address = address\n\t\tisSet = true\n\t}\n\n\tcertificateFile, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__CERTIFICATE_FILE\", idx))\n\tif ok {\n\t\tbinding.CertificateFile = certificateFile\n\t\tisSet = true\n\t}\n\n\tcertificateKeyFile, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__CERTIFICATE_KEY_FILE\", idx))\n\tif ok {\n\t\tbinding.CertificateKeyFile = certificateKeyFile\n\t\tisSet = true\n\t}\n\n\tenableWebAdmin, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__ENABLE_WEB_ADMIN\", idx))\n\tif ok {\n\t\tbinding.EnableWebAdmin = enableWebAdmin\n\t\tisSet = true\n\t}\n\n\tenableWebClient, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__ENABLE_WEB_CLIENT\", idx))\n\tif ok {\n\t\tbinding.EnableWebClient = enableWebClient\n\t\tisSet = true\n\t}\n\n\tenableRESTAPI, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__ENABLE_REST_API\", idx))\n\tif ok {\n\t\tbinding.EnableRESTAPI = enableRESTAPI\n\t\tisSet = true\n\t}\n\n\tenabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__ENABLED_LOGIN_METHODS\", idx), 32)\n\tif ok {\n\t\tbinding.EnabledLoginMethods = int(enabledLoginMethods)\n\t\tisSet = true\n\t}\n\n\tdisabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__DISABLED_LOGIN_METHODS\", idx), 32)\n\tif ok {\n\t\tbinding.DisabledLoginMethods = int(disabledLoginMethods)\n\t\tisSet = true\n\t}\n\n\trenderOpenAPI, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__RENDER_OPENAPI\", idx))\n\tif ok {\n\t\tbinding.RenderOpenAPI = renderOpenAPI\n\t\tisSet = true\n\t}\n\n\tbaseURL, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%d__BASE_URL\", idx))\n\tif ok {\n\t\tbinding.BaseURL = baseURL\n\t\tisSet = true\n\t}\n\n\tlanguages, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%d__LANGUAGES\", idx))\n\tif ok {\n\t\tbinding.Languages = languages\n\t\tisSet = true\n\t}\n\n\tenableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS\", idx))\n\tif ok {\n\t\tbinding.EnableHTTPS = enableHTTPS\n\t\tisSet = true\n\t}\n\n\ttlsVer, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__MIN_TLS_VERSION\", idx), 32)\n\tif ok {\n\t\tbinding.MinTLSVersion = int(tlsVer)\n\t\tisSet = true\n\t}\n\n\tclientAuthType, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__CLIENT_AUTH_TYPE\", idx), 32)\n\tif ok {\n\t\tbinding.ClientAuthType = int(clientAuthType)\n\t\tisSet = true\n\t}\n\n\ttlsCiphers, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__TLS_CIPHER_SUITES\", idx))\n\tif ok {\n\t\tbinding.TLSCipherSuites = tlsCiphers\n\t\tisSet = true\n\t}\n\n\tprotocols, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%d__TLS_PROTOCOLS\", idx))\n\tif ok {\n\t\tbinding.Protocols = protocols\n\t\tisSet = true\n\t}\n\n\tif getHTTPDBindingProxyConfigsFromEnv(idx, &binding) {\n\t\tisSet = true\n\t}\n\n\thideLoginURL, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_HTTPD__BINDINGS__%v__HIDE_LOGIN_URL\", idx), 32)\n\tif ok {\n\t\tbinding.HideLoginURL = int(hideLoginURL)\n\t\tisSet = true\n\t}\n\n\tif getHTTPDNestedObjectsFromEnv(idx, &binding) {\n\t\tisSet = true\n\t}\n\n\tsetHTTPDBinding(isSet, binding, idx)\n}\n\nfunc setHTTPDBinding(isSet bool, binding httpd.Binding, idx int) {\n\tif isSet {\n\t\tif len(globalConf.HTTPDConfig.Bindings) > idx {\n\t\t\tglobalConf.HTTPDConfig.Bindings[idx] = binding\n\t\t} else {\n\t\t\tglobalConf.HTTPDConfig.Bindings = append(globalConf.HTTPDConfig.Bindings, binding)\n\t\t}\n\t}\n}\n\nfunc getHTTPClientCertificatesFromEnv(idx int) {\n\ttlsCert := httpclient.TLSKeyPair{}\n\tif len(globalConf.HTTPConfig.Certificates) > idx {\n\t\ttlsCert = globalConf.HTTPConfig.Certificates[idx]\n\t}\n\n\tcert, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTP__CERTIFICATES__%v__CERT\", idx))\n\tif ok {\n\t\ttlsCert.Cert = cert\n\t}\n\n\tkey, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTP__CERTIFICATES__%v__KEY\", idx))\n\tif ok {\n\t\ttlsCert.Key = key\n\t}\n\n\tif tlsCert.Cert != \"\" && tlsCert.Key != \"\" {\n\t\tif len(globalConf.HTTPConfig.Certificates) > idx {\n\t\t\tglobalConf.HTTPConfig.Certificates[idx] = tlsCert\n\t\t} else {\n\t\t\tglobalConf.HTTPConfig.Certificates = append(globalConf.HTTPConfig.Certificates, tlsCert)\n\t\t}\n\t}\n}\n\nfunc getHTTPClientHeadersFromEnv(idx int) {\n\theader := httpclient.Header{}\n\tif len(globalConf.HTTPConfig.Headers) > idx {\n\t\theader = globalConf.HTTPConfig.Headers[idx]\n\t}\n\n\tkey, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTP__HEADERS__%v__KEY\", idx))\n\tif ok {\n\t\theader.Key = key\n\t}\n\n\tvalue, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTP__HEADERS__%v__VALUE\", idx))\n\tif ok {\n\t\theader.Value = value\n\t}\n\n\turl, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_HTTP__HEADERS__%v__URL\", idx))\n\tif ok {\n\t\theader.URL = url\n\t}\n\n\tif header.Key != \"\" && header.Value != \"\" {\n\t\tif len(globalConf.HTTPConfig.Headers) > idx {\n\t\t\tglobalConf.HTTPConfig.Headers[idx] = header\n\t\t} else {\n\t\t\tglobalConf.HTTPConfig.Headers = append(globalConf.HTTPConfig.Headers, header)\n\t\t}\n\t}\n}\n\nfunc getCommandConfigsFromEnv(idx int) {\n\tcfg := command.Command{}\n\tif len(globalConf.CommandConfig.Commands) > idx {\n\t\tcfg = globalConf.CommandConfig.Commands[idx]\n\t}\n\n\tpath, ok := os.LookupEnv(fmt.Sprintf(\"SFTPGO_COMMAND__COMMANDS__%v__PATH\", idx))\n\tif ok {\n\t\tcfg.Path = path\n\t}\n\n\ttimeout, ok := lookupIntFromEnv(fmt.Sprintf(\"SFTPGO_COMMAND__COMMANDS__%v__TIMEOUT\", idx), 32)\n\tif ok {\n\t\tcfg.Timeout = int(timeout)\n\t}\n\n\tenv, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_COMMAND__COMMANDS__%v__ENV\", idx))\n\tif ok {\n\t\tcfg.Env = env\n\t}\n\n\targs, ok := lookupStringListFromEnv(fmt.Sprintf(\"SFTPGO_COMMAND__COMMANDS__%v__ARGS\", idx))\n\tif ok {\n\t\tcfg.Args = args\n\t}\n\n\tif cfg.Path != \"\" {\n\t\tif len(globalConf.CommandConfig.Commands) > idx {\n\t\t\tglobalConf.CommandConfig.Commands[idx] = cfg\n\t\t} else {\n\t\t\tglobalConf.CommandConfig.Commands = append(globalConf.CommandConfig.Commands, cfg)\n\t\t}\n\t}\n}\n\nfunc setViperDefaults() {\n\tviper.SetDefault(\"common.idle_timeout\", globalConf.Common.IdleTimeout)\n\tviper.SetDefault(\"common.upload_mode\", globalConf.Common.UploadMode)\n\tviper.SetDefault(\"common.actions.execute_on\", globalConf.Common.Actions.ExecuteOn)\n\tviper.SetDefault(\"common.actions.execute_sync\", globalConf.Common.Actions.ExecuteSync)\n\tviper.SetDefault(\"common.actions.hook\", globalConf.Common.Actions.Hook)\n\tviper.SetDefault(\"common.setstat_mode\", globalConf.Common.SetstatMode)\n\tviper.SetDefault(\"common.rename_mode\", globalConf.Common.RenameMode)\n\tviper.SetDefault(\"common.resume_max_size\", globalConf.Common.ResumeMaxSize)\n\tviper.SetDefault(\"common.temp_path\", globalConf.Common.TempPath)\n\tviper.SetDefault(\"common.proxy_protocol\", globalConf.Common.ProxyProtocol)\n\tviper.SetDefault(\"common.proxy_allowed\", globalConf.Common.ProxyAllowed)\n\tviper.SetDefault(\"common.proxy_skipped\", globalConf.Common.ProxySkipped)\n\tviper.SetDefault(\"common.post_connect_hook\", globalConf.Common.PostConnectHook)\n\tviper.SetDefault(\"common.post_disconnect_hook\", globalConf.Common.PostDisconnectHook)\n\tviper.SetDefault(\"common.max_total_connections\", globalConf.Common.MaxTotalConnections)\n\tviper.SetDefault(\"common.max_per_host_connections\", globalConf.Common.MaxPerHostConnections)\n\tviper.SetDefault(\"common.allowlist_status\", globalConf.Common.AllowListStatus)\n\tviper.SetDefault(\"common.allow_self_connections\", globalConf.Common.AllowSelfConnections)\n\tviper.SetDefault(\"common.defender.enabled\", globalConf.Common.DefenderConfig.Enabled)\n\tviper.SetDefault(\"common.defender.driver\", globalConf.Common.DefenderConfig.Driver)\n\tviper.SetDefault(\"common.defender.ban_time\", globalConf.Common.DefenderConfig.BanTime)\n\tviper.SetDefault(\"common.defender.ban_time_increment\", globalConf.Common.DefenderConfig.BanTimeIncrement)\n\tviper.SetDefault(\"common.defender.threshold\", globalConf.Common.DefenderConfig.Threshold)\n\tviper.SetDefault(\"common.defender.score_invalid\", globalConf.Common.DefenderConfig.ScoreInvalid)\n\tviper.SetDefault(\"common.defender.score_valid\", globalConf.Common.DefenderConfig.ScoreValid)\n\tviper.SetDefault(\"common.defender.score_limit_exceeded\", globalConf.Common.DefenderConfig.ScoreLimitExceeded)\n\tviper.SetDefault(\"common.defender.score_no_auth\", globalConf.Common.DefenderConfig.ScoreNoAuth)\n\tviper.SetDefault(\"common.defender.observation_time\", globalConf.Common.DefenderConfig.ObservationTime)\n\tviper.SetDefault(\"common.defender.entries_soft_limit\", globalConf.Common.DefenderConfig.EntriesSoftLimit)\n\tviper.SetDefault(\"common.defender.entries_hard_limit\", globalConf.Common.DefenderConfig.EntriesHardLimit)\n\tviper.SetDefault(\"common.defender.login_delay.success\", globalConf.Common.DefenderConfig.LoginDelay.Success)\n\tviper.SetDefault(\"common.defender.login_delay.password_failed\", globalConf.Common.DefenderConfig.LoginDelay.PasswordFailed)\n\tviper.SetDefault(\"common.umask\", globalConf.Common.Umask)\n\tviper.SetDefault(\"common.server_version\", globalConf.Common.ServerVersion)\n\tviper.SetDefault(\"common.tz\", globalConf.Common.TZ)\n\tviper.SetDefault(\"common.metadata.read\", globalConf.Common.Metadata.Read)\n\tviper.SetDefault(\"common.event_manager.enabled_commands\", globalConf.Common.EventManager.EnabledCommands)\n\tviper.SetDefault(\"acme.email\", globalConf.ACME.Email)\n\tviper.SetDefault(\"acme.key_type\", globalConf.ACME.KeyType)\n\tviper.SetDefault(\"acme.certs_path\", globalConf.ACME.CertsPath)\n\tviper.SetDefault(\"acme.ca_endpoint\", globalConf.ACME.CAEndpoint)\n\tviper.SetDefault(\"acme.domains\", globalConf.ACME.Domains)\n\tviper.SetDefault(\"acme.renew_days\", globalConf.ACME.RenewDays)\n\tviper.SetDefault(\"acme.http01_challenge.port\", globalConf.ACME.HTTP01Challenge.Port)\n\tviper.SetDefault(\"acme.http01_challenge.webroot\", globalConf.ACME.HTTP01Challenge.WebRoot)\n\tviper.SetDefault(\"acme.http01_challenge.proxy_header\", globalConf.ACME.HTTP01Challenge.ProxyHeader)\n\tviper.SetDefault(\"acme.tls_alpn01_challenge.port\", globalConf.ACME.TLSALPN01Challenge.Port)\n\tviper.SetDefault(\"sftpd.max_auth_tries\", globalConf.SFTPD.MaxAuthTries)\n\tviper.SetDefault(\"sftpd.host_keys\", globalConf.SFTPD.HostKeys)\n\tviper.SetDefault(\"sftpd.host_certificates\", globalConf.SFTPD.HostCertificates)\n\tviper.SetDefault(\"sftpd.host_key_algorithms\", globalConf.SFTPD.HostKeyAlgorithms)\n\tviper.SetDefault(\"sftpd.kex_algorithms\", globalConf.SFTPD.KexAlgorithms)\n\tviper.SetDefault(\"sftpd.ciphers\", globalConf.SFTPD.Ciphers)\n\tviper.SetDefault(\"sftpd.macs\", globalConf.SFTPD.MACs)\n\tviper.SetDefault(\"sftpd.public_key_algorithms\", globalConf.SFTPD.PublicKeyAlgorithms)\n\tviper.SetDefault(\"sftpd.trusted_user_ca_keys\", globalConf.SFTPD.TrustedUserCAKeys)\n\tviper.SetDefault(\"sftpd.revoked_user_certs_file\", globalConf.SFTPD.RevokedUserCertsFile)\n\tviper.SetDefault(\"sftpd.opkssh_path\", globalConf.SFTPD.OPKSSHPath)\n\tviper.SetDefault(\"sftpd.opkssh_checksum\", globalConf.SFTPD.OPKSSHChecksum)\n\tviper.SetDefault(\"sftpd.login_banner_file\", globalConf.SFTPD.LoginBannerFile)\n\tviper.SetDefault(\"sftpd.enabled_ssh_commands\", sftpd.GetDefaultSSHCommands())\n\tviper.SetDefault(\"sftpd.keyboard_interactive_authentication\", globalConf.SFTPD.KeyboardInteractiveAuthentication)\n\tviper.SetDefault(\"sftpd.keyboard_interactive_auth_hook\", globalConf.SFTPD.KeyboardInteractiveHook)\n\tviper.SetDefault(\"sftpd.password_authentication\", globalConf.SFTPD.PasswordAuthentication)\n\tviper.SetDefault(\"ftpd.banner_file\", globalConf.FTPD.BannerFile)\n\tviper.SetDefault(\"ftpd.active_transfers_port_non_20\", globalConf.FTPD.ActiveTransfersPortNon20)\n\tviper.SetDefault(\"ftpd.passive_port_range.start\", globalConf.FTPD.PassivePortRange.Start)\n\tviper.SetDefault(\"ftpd.passive_port_range.end\", globalConf.FTPD.PassivePortRange.End)\n\tviper.SetDefault(\"ftpd.disable_active_mode\", globalConf.FTPD.DisableActiveMode)\n\tviper.SetDefault(\"ftpd.enable_site\", globalConf.FTPD.EnableSite)\n\tviper.SetDefault(\"ftpd.hash_support\", globalConf.FTPD.HASHSupport)\n\tviper.SetDefault(\"ftpd.combine_support\", globalConf.FTPD.CombineSupport)\n\tviper.SetDefault(\"ftpd.certificate_file\", globalConf.FTPD.CertificateFile)\n\tviper.SetDefault(\"ftpd.certificate_key_file\", globalConf.FTPD.CertificateKeyFile)\n\tviper.SetDefault(\"ftpd.ca_certificates\", globalConf.FTPD.CACertificates)\n\tviper.SetDefault(\"ftpd.ca_revocation_lists\", globalConf.FTPD.CARevocationLists)\n\tviper.SetDefault(\"webdavd.certificate_file\", globalConf.WebDAVD.CertificateFile)\n\tviper.SetDefault(\"webdavd.certificate_key_file\", globalConf.WebDAVD.CertificateKeyFile)\n\tviper.SetDefault(\"webdavd.ca_certificates\", globalConf.WebDAVD.CACertificates)\n\tviper.SetDefault(\"webdavd.ca_revocation_lists\", globalConf.WebDAVD.CARevocationLists)\n\tviper.SetDefault(\"webdavd.cors.enabled\", globalConf.WebDAVD.Cors.Enabled)\n\tviper.SetDefault(\"webdavd.cors.allowed_origins\", globalConf.WebDAVD.Cors.AllowedOrigins)\n\tviper.SetDefault(\"webdavd.cors.allowed_methods\", globalConf.WebDAVD.Cors.AllowedMethods)\n\tviper.SetDefault(\"webdavd.cors.allowed_headers\", globalConf.WebDAVD.Cors.AllowedHeaders)\n\tviper.SetDefault(\"webdavd.cors.exposed_headers\", globalConf.WebDAVD.Cors.ExposedHeaders)\n\tviper.SetDefault(\"webdavd.cors.allow_credentials\", globalConf.WebDAVD.Cors.AllowCredentials)\n\tviper.SetDefault(\"webdavd.cors.options_passthrough\", globalConf.WebDAVD.Cors.OptionsPassthrough)\n\tviper.SetDefault(\"webdavd.cors.options_success_status\", globalConf.WebDAVD.Cors.OptionsSuccessStatus)\n\tviper.SetDefault(\"webdavd.cors.allow_private_network\", globalConf.WebDAVD.Cors.AllowPrivateNetwork)\n\tviper.SetDefault(\"webdavd.cors.max_age\", globalConf.WebDAVD.Cors.MaxAge)\n\tviper.SetDefault(\"webdavd.cache.users.expiration_time\", globalConf.WebDAVD.Cache.Users.ExpirationTime)\n\tviper.SetDefault(\"webdavd.cache.users.max_size\", globalConf.WebDAVD.Cache.Users.MaxSize)\n\tviper.SetDefault(\"webdavd.cache.mime_types.enabled\", globalConf.WebDAVD.Cache.MimeTypes.Enabled)\n\tviper.SetDefault(\"webdavd.cache.mime_types.max_size\", globalConf.WebDAVD.Cache.MimeTypes.MaxSize)\n\tviper.SetDefault(\"webdavd.cache.mime_types.custom_mappings\", globalConf.WebDAVD.Cache.MimeTypes.CustomMappings)\n\tviper.SetDefault(\"data_provider.driver\", globalConf.ProviderConf.Driver)\n\tviper.SetDefault(\"data_provider.name\", globalConf.ProviderConf.Name)\n\tviper.SetDefault(\"data_provider.host\", globalConf.ProviderConf.Host)\n\tviper.SetDefault(\"data_provider.port\", globalConf.ProviderConf.Port)\n\tviper.SetDefault(\"data_provider.username\", globalConf.ProviderConf.Username)\n\tviper.SetDefault(\"data_provider.password\", globalConf.ProviderConf.Password)\n\tviper.SetDefault(\"data_provider.sslmode\", globalConf.ProviderConf.SSLMode)\n\tviper.SetDefault(\"data_provider.disable_sni\", globalConf.ProviderConf.DisableSNI)\n\tviper.SetDefault(\"data_provider.target_session_attrs\", globalConf.ProviderConf.TargetSessionAttrs)\n\tviper.SetDefault(\"data_provider.root_cert\", globalConf.ProviderConf.RootCert)\n\tviper.SetDefault(\"data_provider.client_cert\", globalConf.ProviderConf.ClientCert)\n\tviper.SetDefault(\"data_provider.client_key\", globalConf.ProviderConf.ClientKey)\n\tviper.SetDefault(\"data_provider.connection_string\", globalConf.ProviderConf.ConnectionString)\n\tviper.SetDefault(\"data_provider.sql_tables_prefix\", globalConf.ProviderConf.SQLTablesPrefix)\n\tviper.SetDefault(\"data_provider.track_quota\", globalConf.ProviderConf.TrackQuota)\n\tviper.SetDefault(\"data_provider.pool_size\", globalConf.ProviderConf.PoolSize)\n\tviper.SetDefault(\"data_provider.users_base_dir\", globalConf.ProviderConf.UsersBaseDir)\n\tviper.SetDefault(\"data_provider.actions.execute_on\", globalConf.ProviderConf.Actions.ExecuteOn)\n\tviper.SetDefault(\"data_provider.actions.execute_for\", globalConf.ProviderConf.Actions.ExecuteFor)\n\tviper.SetDefault(\"data_provider.actions.hook\", globalConf.ProviderConf.Actions.Hook)\n\tviper.SetDefault(\"data_provider.external_auth_hook\", globalConf.ProviderConf.ExternalAuthHook)\n\tviper.SetDefault(\"data_provider.external_auth_scope\", globalConf.ProviderConf.ExternalAuthScope)\n\tviper.SetDefault(\"data_provider.pre_login_hook\", globalConf.ProviderConf.PreLoginHook)\n\tviper.SetDefault(\"data_provider.post_login_hook\", globalConf.ProviderConf.PostLoginHook)\n\tviper.SetDefault(\"data_provider.post_login_scope\", globalConf.ProviderConf.PostLoginScope)\n\tviper.SetDefault(\"data_provider.check_password_hook\", globalConf.ProviderConf.CheckPasswordHook)\n\tviper.SetDefault(\"data_provider.check_password_scope\", globalConf.ProviderConf.CheckPasswordScope)\n\tviper.SetDefault(\"data_provider.password_hashing.bcrypt_options.cost\", globalConf.ProviderConf.PasswordHashing.BcryptOptions.Cost)\n\tviper.SetDefault(\"data_provider.password_hashing.argon2_options.memory\", globalConf.ProviderConf.PasswordHashing.Argon2Options.Memory)\n\tviper.SetDefault(\"data_provider.password_hashing.argon2_options.iterations\", globalConf.ProviderConf.PasswordHashing.Argon2Options.Iterations)\n\tviper.SetDefault(\"data_provider.password_hashing.argon2_options.parallelism\", globalConf.ProviderConf.PasswordHashing.Argon2Options.Parallelism)\n\tviper.SetDefault(\"data_provider.password_hashing.algo\", globalConf.ProviderConf.PasswordHashing.Algo)\n\tviper.SetDefault(\"data_provider.password_validation.admins.min_entropy\", globalConf.ProviderConf.PasswordValidation.Admins.MinEntropy)\n\tviper.SetDefault(\"data_provider.password_validation.users.min_entropy\", globalConf.ProviderConf.PasswordValidation.Users.MinEntropy)\n\tviper.SetDefault(\"data_provider.password_caching\", globalConf.ProviderConf.PasswordCaching)\n\tviper.SetDefault(\"data_provider.update_mode\", globalConf.ProviderConf.UpdateMode)\n\tviper.SetDefault(\"data_provider.delayed_quota_update\", globalConf.ProviderConf.DelayedQuotaUpdate)\n\tviper.SetDefault(\"data_provider.create_default_admin\", globalConf.ProviderConf.CreateDefaultAdmin)\n\tviper.SetDefault(\"data_provider.naming_rules\", globalConf.ProviderConf.NamingRules)\n\tviper.SetDefault(\"data_provider.is_shared\", globalConf.ProviderConf.IsShared)\n\tviper.SetDefault(\"data_provider.node.host\", globalConf.ProviderConf.Node.Host)\n\tviper.SetDefault(\"data_provider.node.port\", globalConf.ProviderConf.Node.Port)\n\tviper.SetDefault(\"data_provider.node.proto\", globalConf.ProviderConf.Node.Proto)\n\tviper.SetDefault(\"data_provider.backups_path\", globalConf.ProviderConf.BackupsPath)\n\tviper.SetDefault(\"httpd.templates_path\", globalConf.HTTPDConfig.TemplatesPath)\n\tviper.SetDefault(\"httpd.static_files_path\", globalConf.HTTPDConfig.StaticFilesPath)\n\tviper.SetDefault(\"httpd.openapi_path\", globalConf.HTTPDConfig.OpenAPIPath)\n\tviper.SetDefault(\"httpd.web_root\", globalConf.HTTPDConfig.WebRoot)\n\tviper.SetDefault(\"httpd.certificate_file\", globalConf.HTTPDConfig.CertificateFile)\n\tviper.SetDefault(\"httpd.certificate_key_file\", globalConf.HTTPDConfig.CertificateKeyFile)\n\tviper.SetDefault(\"httpd.ca_certificates\", globalConf.HTTPDConfig.CACertificates)\n\tviper.SetDefault(\"httpd.ca_revocation_lists\", globalConf.HTTPDConfig.CARevocationLists)\n\tviper.SetDefault(\"httpd.signing_passphrase\", globalConf.HTTPDConfig.SigningPassphrase)\n\tviper.SetDefault(\"httpd.signing_passphrase_file\", globalConf.HTTPDConfig.SigningPassphraseFile)\n\tviper.SetDefault(\"httpd.token_validation\", globalConf.HTTPDConfig.TokenValidation)\n\tviper.SetDefault(\"httpd.cookie_lifetime\", globalConf.HTTPDConfig.CookieLifetime)\n\tviper.SetDefault(\"httpd.share_cookie_lifetime\", globalConf.HTTPDConfig.ShareCookieLifetime)\n\tviper.SetDefault(\"httpd.jwt_lifetime\", globalConf.HTTPDConfig.JWTLifetime)\n\tviper.SetDefault(\"httpd.max_upload_file_size\", globalConf.HTTPDConfig.MaxUploadFileSize)\n\tviper.SetDefault(\"httpd.cors.enabled\", globalConf.HTTPDConfig.Cors.Enabled)\n\tviper.SetDefault(\"httpd.cors.allowed_origins\", globalConf.HTTPDConfig.Cors.AllowedOrigins)\n\tviper.SetDefault(\"httpd.cors.allowed_methods\", globalConf.HTTPDConfig.Cors.AllowedMethods)\n\tviper.SetDefault(\"httpd.cors.allowed_headers\", globalConf.HTTPDConfig.Cors.AllowedHeaders)\n\tviper.SetDefault(\"httpd.cors.exposed_headers\", globalConf.HTTPDConfig.Cors.ExposedHeaders)\n\tviper.SetDefault(\"httpd.cors.allow_credentials\", globalConf.HTTPDConfig.Cors.AllowCredentials)\n\tviper.SetDefault(\"httpd.cors.max_age\", globalConf.HTTPDConfig.Cors.MaxAge)\n\tviper.SetDefault(\"httpd.cors.options_passthrough\", globalConf.HTTPDConfig.Cors.OptionsPassthrough)\n\tviper.SetDefault(\"httpd.cors.options_success_status\", globalConf.HTTPDConfig.Cors.OptionsSuccessStatus)\n\tviper.SetDefault(\"httpd.cors.allow_private_network\", globalConf.HTTPDConfig.Cors.AllowPrivateNetwork)\n\tviper.SetDefault(\"httpd.setup.installation_code\", globalConf.HTTPDConfig.Setup.InstallationCode)\n\tviper.SetDefault(\"httpd.setup.installation_code_hint\", globalConf.HTTPDConfig.Setup.InstallationCodeHint)\n\tviper.SetDefault(\"httpd.hide_support_link\", globalConf.HTTPDConfig.HideSupportLink)\n\tviper.SetDefault(\"http.timeout\", globalConf.HTTPConfig.Timeout)\n\tviper.SetDefault(\"http.retry_wait_min\", globalConf.HTTPConfig.RetryWaitMin)\n\tviper.SetDefault(\"http.retry_wait_max\", globalConf.HTTPConfig.RetryWaitMax)\n\tviper.SetDefault(\"http.retry_max\", globalConf.HTTPConfig.RetryMax)\n\tviper.SetDefault(\"http.ca_certificates\", globalConf.HTTPConfig.CACertificates)\n\tviper.SetDefault(\"http.skip_tls_verify\", globalConf.HTTPConfig.SkipTLSVerify)\n\tviper.SetDefault(\"command.timeout\", globalConf.CommandConfig.Timeout)\n\tviper.SetDefault(\"command.env\", globalConf.CommandConfig.Env)\n\tviper.SetDefault(\"kms.secrets.url\", globalConf.KMSConfig.Secrets.URL)\n\tviper.SetDefault(\"kms.secrets.master_key\", globalConf.KMSConfig.Secrets.MasterKeyString)\n\tviper.SetDefault(\"kms.secrets.master_key_path\", globalConf.KMSConfig.Secrets.MasterKeyPath)\n\tviper.SetDefault(\"telemetry.bind_port\", globalConf.TelemetryConfig.BindPort)\n\tviper.SetDefault(\"telemetry.bind_address\", globalConf.TelemetryConfig.BindAddress)\n\tviper.SetDefault(\"telemetry.enable_profiler\", globalConf.TelemetryConfig.EnableProfiler)\n\tviper.SetDefault(\"telemetry.auth_user_file\", globalConf.TelemetryConfig.AuthUserFile)\n\tviper.SetDefault(\"telemetry.certificate_file\", globalConf.TelemetryConfig.CertificateFile)\n\tviper.SetDefault(\"telemetry.certificate_key_file\", globalConf.TelemetryConfig.CertificateKeyFile)\n\tviper.SetDefault(\"telemetry.min_tls_version\", globalConf.TelemetryConfig.MinTLSVersion)\n\tviper.SetDefault(\"telemetry.tls_cipher_suites\", globalConf.TelemetryConfig.TLSCipherSuites)\n\tviper.SetDefault(\"telemetry.tls_protocols\", globalConf.TelemetryConfig.Protocols)\n\tviper.SetDefault(\"smtp.host\", globalConf.SMTPConfig.Host)\n\tviper.SetDefault(\"smtp.port\", globalConf.SMTPConfig.Port)\n\tviper.SetDefault(\"smtp.from\", globalConf.SMTPConfig.From)\n\tviper.SetDefault(\"smtp.user\", globalConf.SMTPConfig.User)\n\tviper.SetDefault(\"smtp.password\", globalConf.SMTPConfig.Password)\n\tviper.SetDefault(\"smtp.auth_type\", globalConf.SMTPConfig.AuthType)\n\tviper.SetDefault(\"smtp.encryption\", globalConf.SMTPConfig.Encryption)\n\tviper.SetDefault(\"smtp.domain\", globalConf.SMTPConfig.Domain)\n\tviper.SetDefault(\"smtp.templates_path\", globalConf.SMTPConfig.TemplatesPath)\n}\n\nfunc lookupBoolFromEnv(envName string) (bool, bool) {\n\tvalue, ok := os.LookupEnv(envName)\n\tif ok {\n\t\tconverted, err := strconv.ParseBool(strings.TrimSpace(value))\n\t\tif err == nil {\n\t\t\treturn converted, ok\n\t\t}\n\t}\n\n\treturn false, false\n}\n\nfunc lookupIntFromEnv(envName string, bitSize int) (int64, bool) {\n\tvalue, ok := os.LookupEnv(envName)\n\tif ok {\n\t\tconverted, err := strconv.ParseInt(strings.TrimSpace(value), 10, bitSize)\n\t\tif err == nil {\n\t\t\treturn converted, ok\n\t\t}\n\t}\n\n\treturn 0, false\n}\n\nfunc lookupStringListFromEnv(envName string) ([]string, bool) {\n\tvalue, ok := os.LookupEnv(envName)\n\tif ok {\n\t\tvar result []string\n\t\tfor v := range strings.SplitSeq(value, \",\") {\n\t\t\tval := strings.TrimSpace(v)\n\t\t\tif val != \"\" {\n\t\t\t\tresult = append(result, val)\n\t\t\t}\n\t\t}\n\t\treturn result, true\n\t}\n\treturn nil, false\n}\n"
  },
  {
    "path": "internal/config/config_darwin.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build darwin\n\npackage config\n\nimport \"github.com/spf13/viper\"\n\n// macOS specific config search path\nfunc setViperAdditionalConfigPaths() {\n\tviper.AddConfigPath(\"/usr/local/etc/sftpgo\")\n}\n"
  },
  {
    "path": "internal/config/config_fallback.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !linux && !darwin\n\npackage config\n\nfunc setViperAdditionalConfigPaths() {}\n"
  },
  {
    "path": "internal/config/config_linux.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build linux\n\npackage config\n\nimport \"github.com/spf13/viper\"\n\n// linux specific config search path\nfunc setViperAdditionalConfigPaths() {\n\tviper.AddConfigPath(\"$HOME/.config/sftpgo\")\n\tviper.AddConfigPath(\"/etc/sftpgo\")\n\tviper.AddConfigPath(\"/usr/local/etc/sftpgo\")\n}\n"
  },
  {
    "path": "internal/config/config_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage config_test\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/sftpgo/sdk/kms\"\n\t\"github.com/spf13/viper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/command\"\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\nconst (\n\ttempConfigName = \"temp\"\n)\n\nvar (\n\tconfigDir = filepath.Join(\".\", \"..\", \"..\")\n)\n\nfunc reset() {\n\tviper.Reset()\n\tconfig.Init()\n}\n\nfunc TestLoadConfigTest(t *testing.T) {\n\treset()\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, httpd.Conf{}, config.GetHTTPConfig())\n\tassert.NotEqual(t, dataprovider.Config{}, config.GetProviderConf())\n\tassert.NotEqual(t, sftpd.Configuration{}, config.GetSFTPDConfig())\n\tassert.NotEqual(t, httpclient.Config{}, config.GetHTTPConfig())\n\tassert.NotEqual(t, smtp.Config{}, config.GetSMTPConfig())\n\tconfName := tempConfigName + \".json\" //nolint:goconst\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.Error(t, err)\n\terr = os.WriteFile(configFilePath, []byte(\"{invalid json}\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.Error(t, err)\n\terr = os.WriteFile(configFilePath, []byte(`{\"sftpd\": {\"max_auth_tries\": \"a\"}}`), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.Error(t, err)\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoadConfigFileNotFound(t *testing.T) {\n\treset()\n\n\tviper.SetConfigName(\"configfile\")\n\terr := config.LoadConfig(os.TempDir(), \"\")\n\trequire.NoError(t, err)\n\tmfaConf := config.GetMFAConfig()\n\trequire.Len(t, mfaConf.TOTP, 1)\n\trequire.Len(t, config.GetCommonConfig().RateLimitersConfig, 1)\n\trequire.Len(t, config.GetCommonConfig().RateLimitersConfig[0].Protocols, 4)\n\trequire.Len(t, config.GetHTTPDConfig().Bindings, 1)\n\trequire.Len(t, config.GetHTTPDConfig().Bindings[0].OIDC.Scopes, 3)\n}\n\nfunc TestReadEnvFiles(t *testing.T) {\n\treset()\n\n\tenvd := filepath.Join(configDir, \"env.d\")\n\terr := os.Mkdir(envd, os.ModePerm)\n\tassert.NoError(t, err)\n\n\tcontent := make([]byte, 1048576+1)\n\t_, err = rand.Read(content)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(envd, \"env1\"), []byte(\"SFTPGO_SFTPD__MAX_AUTH_TRIES = 10\"), 0666)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(envd, \"env2\"), []byte(`{\"invalid env\": \"value\"}`), 0666)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(envd, \"env3\"), content, 0666)\n\tassert.NoError(t, err)\n\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 10, config.GetSFTPDConfig().MaxAuthTries)\n\n\t_, ok := os.LookupEnv(\"SFTPGO_SFTPD__MAX_AUTH_TRIES\")\n\tassert.True(t, ok)\n\terr = os.Unsetenv(\"SFTPGO_SFTPD__MAX_AUTH_TRIES\")\n\tassert.NoError(t, err)\n\tos.RemoveAll(envd)\n}\n\nfunc TestEnabledSSHCommands(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\n\treset()\n\n\tsftpdConf := config.GetSFTPDConfig()\n\tsftpdConf.EnabledSSHCommands = []string{\"scp\"}\n\tc := make(map[string]sftpd.Configuration)\n\tc[\"sftpd\"] = sftpdConf\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tsftpdConf = config.GetSFTPDConfig()\n\tif assert.Len(t, sftpdConf.EnabledSSHCommands, 1) {\n\t\tassert.Equal(t, \"scp\", sftpdConf.EnabledSSHCommands[0])\n\t}\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestInvalidExternalAuthScope(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.ExternalAuthScope = 100\n\tc := make(map[string]dataprovider.Config)\n\tc[\"data_provider\"] = providerConf\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, config.GetProviderConf().ExternalAuthScope)\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestInvalidProxyProtocol(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tcommonConf := config.GetCommonConfig()\n\tcommonConf.ProxyProtocol = 10\n\tc := make(map[string]common.Configuration)\n\tc[\"common\"] = commonConf\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, config.GetCommonConfig().ProxyProtocol)\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestInvalidUsersBaseDir(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.UsersBaseDir = \".\"\n\tc := make(map[string]dataprovider.Config)\n\tc[\"data_provider\"] = providerConf\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tassert.Empty(t, config.GetProviderConf().UsersBaseDir)\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestInvalidInstallationHint(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\thttpdConfig := config.GetHTTPDConfig()\n\thttpdConfig.Setup = httpd.SetupConfig{\n\t\tInstallationCode:     \"abc\",\n\t\tInstallationCodeHint: \" \",\n\t}\n\tc := make(map[string]httpd.Conf)\n\tc[\"httpd\"] = httpdConfig\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\thttpdConfig = config.GetHTTPDConfig()\n\tassert.Equal(t, \"abc\", httpdConfig.Setup.InstallationCode)\n\tassert.Equal(t, \"Installation code\", httpdConfig.Setup.InstallationCodeHint)\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestInvalidRenameMode(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\tcommonConfig := config.GetCommonConfig()\n\tcommonConfig.RenameMode = 10\n\tc := make(map[string]any)\n\tc[\"common\"] = commonConfig\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, config.GetCommonConfig().RenameMode)\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestDefenderProviderDriver(t *testing.T) {\n\tif config.GetProviderConf().Driver != dataprovider.SQLiteDataProviderName {\n\t\tt.Skip(\"this test is not supported with the current database provider\")\n\t}\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.Driver = dataprovider.BoltDataProviderName\n\tcommonConfig := config.GetCommonConfig()\n\tcommonConfig.DefenderConfig.Enabled = true\n\tcommonConfig.DefenderConfig.Driver = common.DefenderDriverProvider\n\tc := make(map[string]any)\n\tc[\"common\"] = commonConfig\n\tc[\"data_provider\"] = providerConf\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tassert.Equal(t, dataprovider.BoltDataProviderName, config.GetProviderConf().Driver)\n\tassert.Equal(t, common.DefenderDriverMemory, config.GetCommonConfig().DefenderConfig.Driver)\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestSetGetConfig(t *testing.T) {\n\treset()\n\n\tsftpdConf := config.GetSFTPDConfig()\n\tsftpdConf.MaxAuthTries = 10\n\tconfig.SetSFTPDConfig(sftpdConf)\n\tassert.Equal(t, sftpdConf.MaxAuthTries, config.GetSFTPDConfig().MaxAuthTries)\n\tdataProviderConf := config.GetProviderConf()\n\tdataProviderConf.Host = \"test host\"\n\tconfig.SetProviderConf(dataProviderConf)\n\tassert.Equal(t, dataProviderConf.Host, config.GetProviderConf().Host)\n\thttpdConf := config.GetHTTPDConfig()\n\thttpdConf.Bindings = append(httpdConf.Bindings, httpd.Binding{Address: \"0.0.0.0\"})\n\tconfig.SetHTTPDConfig(httpdConf)\n\tassert.Equal(t, httpdConf.Bindings[0].Address, config.GetHTTPDConfig().Bindings[0].Address)\n\tcommonConf := config.GetCommonConfig()\n\tcommonConf.IdleTimeout = 10\n\tconfig.SetCommonConfig(commonConf)\n\tassert.Equal(t, commonConf.IdleTimeout, config.GetCommonConfig().IdleTimeout)\n\tftpdConf := config.GetFTPDConfig()\n\tftpdConf.CertificateFile = \"cert\"\n\tftpdConf.CertificateKeyFile = \"key\"\n\tconfig.SetFTPDConfig(ftpdConf)\n\tassert.Equal(t, ftpdConf.CertificateFile, config.GetFTPDConfig().CertificateFile)\n\tassert.Equal(t, ftpdConf.CertificateKeyFile, config.GetFTPDConfig().CertificateKeyFile)\n\twebDavConf := config.GetWebDAVDConfig()\n\twebDavConf.CertificateFile = \"dav_cert\"\n\twebDavConf.CertificateKeyFile = \"dav_key\"\n\tconfig.SetWebDAVDConfig(webDavConf)\n\tassert.Equal(t, webDavConf.CertificateFile, config.GetWebDAVDConfig().CertificateFile)\n\tassert.Equal(t, webDavConf.CertificateKeyFile, config.GetWebDAVDConfig().CertificateKeyFile)\n\tkmsConf := config.GetKMSConfig()\n\tkmsConf.Secrets.MasterKeyPath = \"apath\"\n\tkmsConf.Secrets.URL = \"aurl\"\n\tconfig.SetKMSConfig(kmsConf)\n\tassert.Equal(t, kmsConf.Secrets.MasterKeyPath, config.GetKMSConfig().Secrets.MasterKeyPath)\n\tassert.Equal(t, kmsConf.Secrets.URL, config.GetKMSConfig().Secrets.URL)\n\ttelemetryConf := config.GetTelemetryConfig()\n\ttelemetryConf.BindPort = 10001\n\ttelemetryConf.BindAddress = \"0.0.0.0\"\n\tconfig.SetTelemetryConfig(telemetryConf)\n\tassert.Equal(t, telemetryConf.BindPort, config.GetTelemetryConfig().BindPort)\n\tassert.Equal(t, telemetryConf.BindAddress, config.GetTelemetryConfig().BindAddress)\n\tpluginConf := []plugin.Config{\n\t\t{\n\t\t\tType: \"eventsearcher\",\n\t\t},\n\t}\n\tconfig.SetPluginsConfig(pluginConf)\n\tif assert.Len(t, config.GetPluginsConfig(), 1) {\n\t\tassert.Equal(t, pluginConf[0].Type, config.GetPluginsConfig()[0].Type)\n\t}\n\tassert.False(t, config.HasKMSPlugin())\n\tpluginConf = []plugin.Config{\n\t\t{\n\t\t\tType: \"notifier\",\n\t\t},\n\t\t{\n\t\t\tType: \"kms\",\n\t\t},\n\t}\n\tconfig.SetPluginsConfig(pluginConf)\n\tassert.Len(t, config.GetPluginsConfig(), 2)\n\tassert.True(t, config.HasKMSPlugin())\n}\n\nfunc TestServiceToStart(t *testing.T) {\n\treset()\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tassert.True(t, config.HasServicesToStart())\n\tsftpdConf := config.GetSFTPDConfig()\n\tsftpdConf.Bindings[0].Port = 0\n\tconfig.SetSFTPDConfig(sftpdConf)\n\t// httpd service is enabled\n\tassert.True(t, config.HasServicesToStart())\n\thttpdConf := config.GetHTTPDConfig()\n\thttpdConf.Bindings[0].Port = 0\n\tassert.False(t, config.HasServicesToStart())\n\tftpdConf := config.GetFTPDConfig()\n\tftpdConf.Bindings[0].Port = 2121\n\tconfig.SetFTPDConfig(ftpdConf)\n\tassert.True(t, config.HasServicesToStart())\n\tftpdConf.Bindings[0].Port = 0\n\tconfig.SetFTPDConfig(ftpdConf)\n\twebdavdConf := config.GetWebDAVDConfig()\n\twebdavdConf.Bindings[0].Port = 9000\n\tconfig.SetWebDAVDConfig(webdavdConf)\n\tassert.True(t, config.HasServicesToStart())\n\twebdavdConf.Bindings[0].Port = 0\n\tconfig.SetWebDAVDConfig(webdavdConf)\n\tassert.False(t, config.HasServicesToStart())\n\tsftpdConf.Bindings[0].Port = 2022\n\tconfig.SetSFTPDConfig(sftpdConf)\n\tassert.True(t, config.HasServicesToStart())\n}\n\nfunc TestSSHCommandsFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_SFTPD__ENABLED_SSH_COMMANDS\", \"cd,scp\")\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_SFTPD__ENABLED_SSH_COMMANDS\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\n\tsftpdConf := config.GetSFTPDConfig()\n\tif assert.Len(t, sftpdConf.EnabledSSHCommands, 2) {\n\t\tassert.Equal(t, \"cd\", sftpdConf.EnabledSSHCommands[0])\n\t\tassert.Equal(t, \"scp\", sftpdConf.EnabledSSHCommands[1])\n\t}\n}\n\nfunc TestSMTPFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_SMTP__HOST\", \"smtp.example.com\")\n\tos.Setenv(\"SFTPGO_SMTP__PORT\", \"587\")\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_SMTP__HOST\")\n\t\tos.Unsetenv(\"SFTPGO_SMTP__PORT\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tsmtpConfig := config.GetSMTPConfig()\n\tassert.Equal(t, \"smtp.example.com\", smtpConfig.Host)\n\tassert.Equal(t, 587, smtpConfig.Port)\n}\n\nfunc TestMFAFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_MFA__TOTP__0__NAME\", \"main\")\n\tos.Setenv(\"SFTPGO_MFA__TOTP__1__NAME\", \"additional_name\")\n\tos.Setenv(\"SFTPGO_MFA__TOTP__1__ISSUER\", \"additional_issuer\")\n\tos.Setenv(\"SFTPGO_MFA__TOTP__1__ALGO\", \"sha256\")\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_MFA__TOTP__0__NAME\")\n\t\tos.Unsetenv(\"SFTPGO_MFA__TOTP__1__NAME\")\n\t\tos.Unsetenv(\"SFTPGO_MFA__TOTP__1__ISSUER\")\n\t\tos.Unsetenv(\"SFTPGO_MFA__TOTP__1__ALGO\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tmfaConf := config.GetMFAConfig()\n\trequire.Len(t, mfaConf.TOTP, 2)\n\trequire.Equal(t, \"main\", mfaConf.TOTP[0].Name)\n\trequire.Equal(t, \"SFTPGo\", mfaConf.TOTP[0].Issuer)\n\trequire.Equal(t, \"sha1\", mfaConf.TOTP[0].Algo)\n\trequire.Equal(t, \"additional_name\", mfaConf.TOTP[1].Name)\n\trequire.Equal(t, \"additional_issuer\", mfaConf.TOTP[1].Issuer)\n\trequire.Equal(t, \"sha256\", mfaConf.TOTP[1].Algo)\n}\n\nfunc TestDisabledMFAConfig(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tmfaConf := config.GetMFAConfig()\n\tassert.Len(t, mfaConf.TOTP, 1)\n\n\treset()\n\n\tc := make(map[string]mfa.Config)\n\tc[\"mfa\"] = mfa.Config{}\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tmfaConf = config.GetMFAConfig()\n\tassert.Len(t, mfaConf.TOTP, 0)\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestOverrideSliceValues(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\tc := make(map[string]any)\n\tc[\"common\"] = common.Configuration{\n\t\tRateLimitersConfig: []common.RateLimiterConfig{\n\t\t\t{\n\t\t\t\tType:      1,\n\t\t\t\tProtocols: []string{\"HTTP\"},\n\t\t\t},\n\t\t},\n\t}\n\tjsonConf, err := json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\trequire.Len(t, config.GetCommonConfig().RateLimitersConfig, 1)\n\trequire.Equal(t, []string{\"HTTP\"}, config.GetCommonConfig().RateLimitersConfig[0].Protocols)\n\n\treset()\n\n\t// empty ratelimiters, default value should be used\n\tc[\"common\"] = common.Configuration{}\n\tjsonConf, err = json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\trequire.Len(t, config.GetCommonConfig().RateLimitersConfig, 1)\n\trl := config.GetCommonConfig().RateLimitersConfig[0]\n\trequire.Equal(t, []string{\"SSH\", \"FTP\", \"DAV\", \"HTTP\"}, rl.Protocols)\n\trequire.Equal(t, int64(1000), rl.Period)\n\n\treset()\n\n\tc = make(map[string]any)\n\tc[\"httpd\"] = httpd.Conf{\n\t\tBindings: []httpd.Binding{\n\t\t\t{\n\t\t\t\tOIDC: httpd.OIDC{\n\t\t\t\t\tScopes: []string{\"scope1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tjsonConf, err = json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\trequire.Len(t, config.GetHTTPDConfig().Bindings, 1)\n\trequire.Equal(t, []string{\"scope1\"}, config.GetHTTPDConfig().Bindings[0].OIDC.Scopes)\n\n\treset()\n\n\tc = make(map[string]any)\n\tc[\"httpd\"] = httpd.Conf{\n\t\tBindings: nil,\n\t}\n\tjsonConf, err = json.Marshal(c)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\trequire.Len(t, config.GetHTTPDConfig().Bindings, 1)\n\trequire.Equal(t, []string{\"openid\", \"profile\", \"email\"}, config.GetHTTPDConfig().Bindings[0].OIDC.Scopes)\n}\n\nfunc TestFTPDOverridesFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP\", \"192.168.1.1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__NETWORKS\", \"192.168.1.0/24, 192.168.3.0/25\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__1__IP\", \"192.168.2.1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__1__NETWORKS\", \"192.168.2.0/24\")\n\tcleanup := func() {\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__NETWORKS\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__1__IP\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__1__NETWORKS\")\n\t}\n\tt.Cleanup(cleanup)\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tftpdConf := config.GetFTPDConfig()\n\trequire.Len(t, ftpdConf.Bindings, 1)\n\trequire.Len(t, ftpdConf.Bindings[0].PassiveIPOverrides, 2)\n\trequire.Equal(t, \"192.168.1.1\", ftpdConf.Bindings[0].PassiveIPOverrides[0].IP)\n\trequire.Len(t, ftpdConf.Bindings[0].PassiveIPOverrides[0].Networks, 2)\n\trequire.Equal(t, \"192.168.2.1\", ftpdConf.Bindings[0].PassiveIPOverrides[1].IP)\n\trequire.Len(t, ftpdConf.Bindings[0].PassiveIPOverrides[1].Networks, 1)\n\n\tcleanup()\n\tcfg := make(map[string]any)\n\tcfg[\"ftpd\"] = ftpdConf\n\tconfigAsJSON, err := json.Marshal(cfg)\n\trequire.NoError(t, err)\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr = os.WriteFile(configFilePath, configAsJSON, os.ModePerm)\n\tassert.NoError(t, err)\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP\", \"192.168.1.2\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__1__NETWORKS\", \"192.168.2.0/24,192.168.4.0/25\")\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tftpdConf = config.GetFTPDConfig()\n\trequire.Len(t, ftpdConf.Bindings, 1)\n\trequire.Len(t, ftpdConf.Bindings[0].PassiveIPOverrides, 2)\n\trequire.Equal(t, \"192.168.1.2\", ftpdConf.Bindings[0].PassiveIPOverrides[0].IP)\n\trequire.Len(t, ftpdConf.Bindings[0].PassiveIPOverrides[0].Networks, 2)\n\trequire.Equal(t, \"192.168.2.1\", ftpdConf.Bindings[0].PassiveIPOverrides[1].IP)\n\trequire.Len(t, ftpdConf.Bindings[0].PassiveIPOverrides[1].Networks, 2)\n\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPDSubObjectsFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__SECURITY__HTTPS_PROXY_HEADERS__0__KEY\", \"X-Forwarded-Proto\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__SECURITY__HTTPS_PROXY_HEADERS__0__VALUE\", \"https\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CLIENT_ID\", \"client_id\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CLIENT_SECRET\", \"client_secret\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CLIENT_SECRET_FILE\", \"client_secret_file\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CONFIG_URL\", \"config_url\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__REDIRECT_BASE_URL\", \"redirect_base_url\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__USERNAME_FIELD\", \"email\")\n\tcleanup := func() {\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__SECURITY__HTTPS_PROXY_HEADERS__0__KEY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__SECURITY__HTTPS_PROXY_HEADERS__0__VALUE\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CLIENT_ID\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CLIENT_SECRET\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CLIENT_SECRET_FILE\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CONFIG_URL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__REDIRECT_BASE_URL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__USERNAME_FIELD\")\n\t}\n\tt.Cleanup(cleanup)\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\thttpdConf := config.GetHTTPDConfig()\n\trequire.Len(t, httpdConf.Bindings, 1)\n\trequire.Len(t, httpdConf.Bindings[0].Security.HTTPSProxyHeaders, 1)\n\trequire.Equal(t, \"client_id\", httpdConf.Bindings[0].OIDC.ClientID)\n\trequire.Equal(t, \"client_secret\", httpdConf.Bindings[0].OIDC.ClientSecret)\n\trequire.Equal(t, \"client_secret_file\", httpdConf.Bindings[0].OIDC.ClientSecretFile)\n\trequire.Equal(t, \"config_url\", httpdConf.Bindings[0].OIDC.ConfigURL)\n\trequire.Equal(t, \"redirect_base_url\", httpdConf.Bindings[0].OIDC.RedirectBaseURL)\n\trequire.Equal(t, \"email\", httpdConf.Bindings[0].OIDC.UsernameField)\n\n\tcleanup()\n\tcfg := make(map[string]any)\n\tcfg[\"httpd\"] = httpdConf\n\tconfigAsJSON, err := json.Marshal(cfg)\n\trequire.NoError(t, err)\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr = os.WriteFile(configFilePath, configAsJSON, os.ModePerm)\n\tassert.NoError(t, err)\n\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__SECURITY__HTTPS_PROXY_HEADERS__0__VALUE\", \"http\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__OIDC__CLIENT_SECRET\", \"new_client_secret\")\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\thttpdConf = config.GetHTTPDConfig()\n\trequire.Len(t, httpdConf.Bindings, 1)\n\trequire.Len(t, httpdConf.Bindings[0].Security.HTTPSProxyHeaders, 1)\n\trequire.Equal(t, \"http\", httpdConf.Bindings[0].Security.HTTPSProxyHeaders[0].Value)\n\trequire.Equal(t, \"client_id\", httpdConf.Bindings[0].OIDC.ClientID)\n\trequire.Equal(t, \"new_client_secret\", httpdConf.Bindings[0].OIDC.ClientSecret)\n\trequire.Equal(t, \"config_url\", httpdConf.Bindings[0].OIDC.ConfigURL)\n\trequire.Equal(t, \"redirect_base_url\", httpdConf.Bindings[0].OIDC.RedirectBaseURL)\n\trequire.Equal(t, \"email\", httpdConf.Bindings[0].OIDC.UsernameField)\n\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPluginsFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_PLUGINS__0__TYPE\", \"notifier\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS\", \"upload,download\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_EVENTS\", \"add,update\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_OBJECTS\", \"user,admin\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__LOG_EVENTS\", \"a,1,2\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_MAX_TIME\", \"2\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE\", \"1000\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__CMD\", \"plugin_start_cmd\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__ARGS\", \"arg1,arg2\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__SHA256SUM\", \"0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__AUTO_MTLS\", \"1\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME\", kms.SchemeAWS)\n\tos.Setenv(\"SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS\", kms.SecretStatusAWS)\n\tos.Setenv(\"SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE\", \"14\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__ENV_PREFIX\", \"prefix_\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__ENV_VARS\", \"a, b\")\n\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__TYPE\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_EVENTS\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_OBJECTS\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__LOG_EVENTS\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_MAX_TIME\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__CMD\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__ARGS\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__SHA256SUM\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__AUTO_MTLS\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__ENV_PREFIX\")\n\t\tos.Unsetenv(\"SFTPGO_PLUGINS__0__ENV_VARS\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tpluginsConf := config.GetPluginsConfig()\n\trequire.Len(t, pluginsConf, 1)\n\tpluginConf := pluginsConf[0]\n\trequire.Equal(t, \"notifier\", pluginConf.Type)\n\trequire.Len(t, pluginConf.NotifierOptions.FsEvents, 2)\n\trequire.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, \"upload\"))\n\trequire.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, \"download\"))\n\trequire.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)\n\trequire.Equal(t, \"add\", pluginConf.NotifierOptions.ProviderEvents[0])\n\trequire.Equal(t, \"update\", pluginConf.NotifierOptions.ProviderEvents[1])\n\trequire.Len(t, pluginConf.NotifierOptions.ProviderObjects, 2)\n\trequire.Equal(t, \"user\", pluginConf.NotifierOptions.ProviderObjects[0])\n\trequire.Equal(t, \"admin\", pluginConf.NotifierOptions.ProviderObjects[1])\n\trequire.Len(t, pluginConf.NotifierOptions.LogEvents, 2)\n\trequire.Equal(t, 1, pluginConf.NotifierOptions.LogEvents[0])\n\trequire.Equal(t, 2, pluginConf.NotifierOptions.LogEvents[1])\n\trequire.Equal(t, 2, pluginConf.NotifierOptions.RetryMaxTime)\n\trequire.Equal(t, 1000, pluginConf.NotifierOptions.RetryQueueMaxSize)\n\trequire.Equal(t, \"plugin_start_cmd\", pluginConf.Cmd)\n\trequire.Len(t, pluginConf.Args, 2)\n\trequire.Equal(t, \"arg1\", pluginConf.Args[0])\n\trequire.Equal(t, \"arg2\", pluginConf.Args[1])\n\trequire.Equal(t, \"0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193\", pluginConf.SHA256Sum)\n\trequire.True(t, pluginConf.AutoMTLS)\n\trequire.Equal(t, kms.SchemeAWS, pluginConf.KMSOptions.Scheme)\n\trequire.Equal(t, kms.SecretStatusAWS, pluginConf.KMSOptions.EncryptedStatus)\n\trequire.Equal(t, 14, pluginConf.AuthOptions.Scope)\n\trequire.Equal(t, \"prefix_\", pluginConf.EnvPrefix)\n\trequire.Len(t, pluginConf.EnvVars, 2)\n\tassert.Equal(t, \"a\", pluginConf.EnvVars[0])\n\tassert.Equal(t, \"b\", pluginConf.EnvVars[1])\n\n\tcfg := make(map[string]any)\n\tcfg[\"plugins\"] = pluginConf\n\tconfigAsJSON, err := json.Marshal(cfg)\n\trequire.NoError(t, err)\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr = os.WriteFile(configFilePath, configAsJSON, os.ModePerm)\n\tassert.NoError(t, err)\n\n\tos.Setenv(\"SFTPGO_PLUGINS__0__CMD\", \"plugin_start_cmd1\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__ARGS\", \"\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__AUTO_MTLS\", \"0\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME\", kms.SchemeVaultTransit)\n\tos.Setenv(\"SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS\", kms.SecretStatusVaultTransit)\n\tos.Setenv(\"SFTPGO_PLUGINS__0__ENV_PREFIX\", \"\")\n\tos.Setenv(\"SFTPGO_PLUGINS__0__ENV_VARS\", \"\")\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tpluginsConf = config.GetPluginsConfig()\n\trequire.Len(t, pluginsConf, 1)\n\tpluginConf = pluginsConf[0]\n\trequire.Equal(t, \"notifier\", pluginConf.Type)\n\trequire.Len(t, pluginConf.NotifierOptions.FsEvents, 2)\n\trequire.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, \"upload\"))\n\trequire.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, \"download\"))\n\trequire.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)\n\trequire.Equal(t, \"add\", pluginConf.NotifierOptions.ProviderEvents[0])\n\trequire.Equal(t, \"update\", pluginConf.NotifierOptions.ProviderEvents[1])\n\trequire.Len(t, pluginConf.NotifierOptions.ProviderObjects, 2)\n\trequire.Equal(t, \"user\", pluginConf.NotifierOptions.ProviderObjects[0])\n\trequire.Equal(t, \"admin\", pluginConf.NotifierOptions.ProviderObjects[1])\n\trequire.Equal(t, 2, pluginConf.NotifierOptions.RetryMaxTime)\n\trequire.Equal(t, 1000, pluginConf.NotifierOptions.RetryQueueMaxSize)\n\trequire.Equal(t, \"plugin_start_cmd1\", pluginConf.Cmd)\n\trequire.Len(t, pluginConf.Args, 0)\n\trequire.Equal(t, \"0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193\", pluginConf.SHA256Sum)\n\trequire.False(t, pluginConf.AutoMTLS)\n\trequire.Equal(t, kms.SchemeVaultTransit, pluginConf.KMSOptions.Scheme)\n\trequire.Equal(t, kms.SecretStatusVaultTransit, pluginConf.KMSOptions.EncryptedStatus)\n\trequire.Equal(t, 14, pluginConf.AuthOptions.Scope)\n\tassert.Empty(t, pluginConf.EnvPrefix)\n\tassert.Len(t, pluginConf.EnvVars, 0)\n\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestRateLimitersFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__AVERAGE\", \"100\")\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__PERIOD\", \"2000\")\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__BURST\", \"10\")\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__TYPE\", \"2\")\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__PROTOCOLS\", \"SSH, FTP\")\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__GENERATE_DEFENDER_EVENTS\", \"1\")\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__ENTRIES_SOFT_LIMIT\", \"50\")\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__ENTRIES_HARD_LIMIT\", \"100\")\n\tos.Setenv(\"SFTPGO_COMMON__RATE_LIMITERS__8__AVERAGE\", \"50\")\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__AVERAGE\")\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__PERIOD\")\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__BURST\")\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__TYPE\")\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__PROTOCOLS\")\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__GENERATE_DEFENDER_EVENTS\")\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__ENTRIES_SOFT_LIMIT\")\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__0__ENTRIES_HARD_LIMIT\")\n\t\tos.Unsetenv(\"SFTPGO_COMMON__RATE_LIMITERS__8__AVERAGE\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tlimiters := config.GetCommonConfig().RateLimitersConfig\n\trequire.Len(t, limiters, 2)\n\trequire.Equal(t, int64(100), limiters[0].Average)\n\trequire.Equal(t, int64(2000), limiters[0].Period)\n\trequire.Equal(t, 10, limiters[0].Burst)\n\trequire.Equal(t, 2, limiters[0].Type)\n\tprotocols := limiters[0].Protocols\n\trequire.Len(t, protocols, 2)\n\trequire.True(t, slices.Contains(protocols, common.ProtocolFTP))\n\trequire.True(t, slices.Contains(protocols, common.ProtocolSSH))\n\trequire.True(t, limiters[0].GenerateDefenderEvents)\n\trequire.Equal(t, 50, limiters[0].EntriesSoftLimit)\n\trequire.Equal(t, 100, limiters[0].EntriesHardLimit)\n\trequire.Equal(t, int64(50), limiters[1].Average)\n\t// we check the default values here\n\trequire.Equal(t, int64(1000), limiters[1].Period)\n\trequire.Equal(t, 1, limiters[1].Burst)\n\trequire.Equal(t, 2, limiters[1].Type)\n\tprotocols = limiters[1].Protocols\n\trequire.Len(t, protocols, 4)\n\trequire.True(t, slices.Contains(protocols, common.ProtocolFTP))\n\trequire.True(t, slices.Contains(protocols, common.ProtocolSSH))\n\trequire.True(t, slices.Contains(protocols, common.ProtocolWebDAV))\n\trequire.True(t, slices.Contains(protocols, common.ProtocolHTTP))\n\trequire.False(t, limiters[1].GenerateDefenderEvents)\n\trequire.Equal(t, 100, limiters[1].EntriesSoftLimit)\n\trequire.Equal(t, 150, limiters[1].EntriesHardLimit)\n}\n\nfunc TestSFTPDBindingsFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_SFTPD__BINDINGS__0__ADDRESS\", \"127.0.0.1\")\n\tos.Setenv(\"SFTPGO_SFTPD__BINDINGS__0__PORT\", \"2200\")\n\tos.Setenv(\"SFTPGO_SFTPD__BINDINGS__0__APPLY_PROXY_CONFIG\", \"false\")\n\tos.Setenv(\"SFTPGO_SFTPD__BINDINGS__3__ADDRESS\", \"127.0.1.1\")\n\tos.Setenv(\"SFTPGO_SFTPD__BINDINGS__3__PORT\", \"2203\")\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_SFTPD__BINDINGS__0__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_SFTPD__BINDINGS__0__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_SFTPD__BINDINGS__0__APPLY_PROXY_CONFIG\")\n\t\tos.Unsetenv(\"SFTPGO_SFTPD__BINDINGS__3__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_SFTPD__BINDINGS__3__PORT\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tbindings := config.GetSFTPDConfig().Bindings\n\trequire.Len(t, bindings, 2)\n\trequire.Equal(t, 2200, bindings[0].Port)\n\trequire.Equal(t, \"127.0.0.1\", bindings[0].Address)\n\trequire.False(t, bindings[0].ApplyProxyConfig)\n\trequire.Equal(t, 2203, bindings[1].Port)\n\trequire.Equal(t, \"127.0.1.1\", bindings[1].Address)\n\trequire.True(t, bindings[1].ApplyProxyConfig) // default value\n}\n\nfunc TestCommandsFromEnv(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tcommandConfig := config.GetCommandConfig()\n\tcommandConfig.Commands = append(commandConfig.Commands, command.Command{\n\t\tPath:    \"cmd\",\n\t\tTimeout: 10,\n\t\tEnv:     []string{\"a=a\"},\n\t})\n\tc := make(map[string]command.Config)\n\tc[\"command\"] = commandConfig\n\tjsonConf, err := json.Marshal(c)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\trequire.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\trequire.NoError(t, err)\n\tcommandConfig = config.GetCommandConfig()\n\trequire.Equal(t, 30, commandConfig.Timeout)\n\trequire.Len(t, commandConfig.Env, 0)\n\trequire.Len(t, commandConfig.Commands, 1)\n\trequire.Equal(t, \"cmd\", commandConfig.Commands[0].Path)\n\trequire.Equal(t, 10, commandConfig.Commands[0].Timeout)\n\trequire.Equal(t, []string{\"a=a\"}, commandConfig.Commands[0].Env)\n\n\tos.Setenv(\"SFTPGO_COMMAND__TIMEOUT\", \"25\")\n\tos.Setenv(\"SFTPGO_COMMAND__ENV\", \"a=b,c=d\")\n\tos.Setenv(\"SFTPGO_COMMAND__COMMANDS__0__PATH\", \"cmd1\")\n\tos.Setenv(\"SFTPGO_COMMAND__COMMANDS__0__TIMEOUT\", \"11\")\n\tos.Setenv(\"SFTPGO_COMMAND__COMMANDS__1__PATH\", \"cmd2\")\n\tos.Setenv(\"SFTPGO_COMMAND__COMMANDS__1__TIMEOUT\", \"20\")\n\tos.Setenv(\"SFTPGO_COMMAND__COMMANDS__1__ENV\", \"e=f\")\n\tos.Setenv(\"SFTPGO_COMMAND__COMMANDS__1__ARGS\", \"arg1, arg2\")\n\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_COMMAND__TIMEOUT\")\n\t\tos.Unsetenv(\"SFTPGO_COMMAND__ENV\")\n\t\tos.Unsetenv(\"SFTPGO_COMMAND__COMMANDS__0__PATH\")\n\t\tos.Unsetenv(\"SFTPGO_COMMAND__COMMANDS__0__TIMEOUT\")\n\t\tos.Unsetenv(\"SFTPGO_COMMAND__COMMANDS__1__PATH\")\n\t\tos.Unsetenv(\"SFTPGO_COMMAND__COMMANDS__1__TIMEOUT\")\n\t\tos.Unsetenv(\"SFTPGO_COMMAND__COMMANDS__1__ENV\")\n\t\tos.Unsetenv(\"SFTPGO_COMMAND__COMMANDS__1__ARGS\")\n\t})\n\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tcommandConfig = config.GetCommandConfig()\n\trequire.Equal(t, 25, commandConfig.Timeout)\n\trequire.Equal(t, []string{\"a=b\", \"c=d\"}, commandConfig.Env)\n\trequire.Len(t, commandConfig.Commands, 2)\n\trequire.Equal(t, \"cmd1\", commandConfig.Commands[0].Path)\n\trequire.Equal(t, 11, commandConfig.Commands[0].Timeout)\n\trequire.Equal(t, []string{\"a=a\"}, commandConfig.Commands[0].Env)\n\trequire.Equal(t, \"cmd2\", commandConfig.Commands[1].Path)\n\trequire.Equal(t, 20, commandConfig.Commands[1].Timeout)\n\trequire.Equal(t, []string{\"e=f\"}, commandConfig.Commands[1].Env)\n\trequire.Equal(t, []string{\"arg1\", \"arg2\"}, commandConfig.Commands[1].Args)\n\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestFTPDBindingsFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__ADDRESS\", \"127.0.0.1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PORT\", \"2200\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG\", \"f\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__TLS_MODE\", \"2\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP\", \"127.0.1.2\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP\", \"172.16.1.1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_HOST\", \"127.0.1.3\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__TLS_CIPHER_SUITES\", \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_CONNECTIONS_SECURITY\", \"1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__ADDRESS\", \"127.0.1.1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__PORT\", \"2203\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__TLS_MODE\", \"1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__MIN_TLS_VERSION\", \"13\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP\", \"127.0.1.1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__PASSIVE_IP_OVERRIDES__3__IP\", \"192.168.1.1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__PASSIVE_IP_OVERRIDES__3__NETWORKS\", \"192.168.1.0/24, 192.168.3.0/25\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE\", \"2\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__DEBUG\", \"1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__ACTIVE_CONNECTIONS_SECURITY\", \"1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__IGNORE_ASCII_TRANSFER_TYPE\", \"1\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__CERTIFICATE_FILE\", \"cert.crt\")\n\tos.Setenv(\"SFTPGO_FTPD__BINDINGS__9__CERTIFICATE_KEY_FILE\", \"cert.key\")\n\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__TLS_MODE\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__PASSIVE_HOST\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__TLS_CIPHER_SUITES\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__0__ACTIVE_CONNECTIONS_SECURITY\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__TLS_MODE\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__MIN_TLS_VERSION\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__PASSIVE_IP_OVERRIDES__3__IP\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__PASSIVE_IP_OVERRIDES__3__NETWORKS\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__DEBUG\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__ACTIVE_CONNECTIONS_SECURITY\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__IGNORE_ASCII_TRANSFER_TYPE\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__CERTIFICATE_FILE\")\n\t\tos.Unsetenv(\"SFTPGO_FTPD__BINDINGS__9__CERTIFICATE_KEY_FILE\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tbindings := config.GetFTPDConfig().Bindings\n\trequire.Len(t, bindings, 2)\n\trequire.Equal(t, 2200, bindings[0].Port)\n\trequire.Equal(t, \"127.0.0.1\", bindings[0].Address)\n\trequire.False(t, bindings[0].ApplyProxyConfig)\n\trequire.Equal(t, 2, bindings[0].TLSMode)\n\trequire.Equal(t, 12, bindings[0].MinTLSVersion)\n\trequire.Equal(t, \"127.0.1.2\", bindings[0].ForcePassiveIP)\n\trequire.Len(t, bindings[0].PassiveIPOverrides, 0)\n\trequire.Equal(t, \"127.0.1.3\", bindings[0].PassiveHost)\n\trequire.Equal(t, 0, bindings[0].ClientAuthType)\n\trequire.Len(t, bindings[0].TLSCipherSuites, 2)\n\trequire.Equal(t, \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\", bindings[0].TLSCipherSuites[0])\n\trequire.Equal(t, \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", bindings[0].TLSCipherSuites[1])\n\trequire.False(t, bindings[0].Debug)\n\trequire.Equal(t, 1, bindings[0].PassiveConnectionsSecurity)\n\trequire.Equal(t, 0, bindings[0].ActiveConnectionsSecurity)\n\trequire.Equal(t, 2203, bindings[1].Port)\n\trequire.Equal(t, \"127.0.1.1\", bindings[1].Address)\n\trequire.True(t, bindings[1].ApplyProxyConfig) // default value\n\trequire.Equal(t, 1, bindings[1].TLSMode)\n\trequire.Equal(t, 13, bindings[1].MinTLSVersion)\n\trequire.Equal(t, \"127.0.1.1\", bindings[1].ForcePassiveIP)\n\trequire.Empty(t, bindings[1].PassiveHost)\n\trequire.Len(t, bindings[1].PassiveIPOverrides, 1)\n\trequire.Equal(t, \"192.168.1.1\", bindings[1].PassiveIPOverrides[0].IP)\n\trequire.Len(t, bindings[1].PassiveIPOverrides[0].Networks, 2)\n\trequire.Equal(t, \"192.168.1.0/24\", bindings[1].PassiveIPOverrides[0].Networks[0])\n\trequire.Equal(t, \"192.168.3.0/25\", bindings[1].PassiveIPOverrides[0].Networks[1])\n\trequire.Equal(t, 2, bindings[1].ClientAuthType)\n\trequire.Nil(t, bindings[1].TLSCipherSuites)\n\trequire.Equal(t, 0, bindings[1].PassiveConnectionsSecurity)\n\trequire.Equal(t, 1, bindings[1].ActiveConnectionsSecurity)\n\trequire.True(t, bindings[1].Debug)\n\trequire.Equal(t, \"cert.crt\", bindings[1].CertificateFile)\n\trequire.Equal(t, \"cert.key\", bindings[1].CertificateKeyFile)\n}\n\nfunc TestWebDAVMimeCache(t *testing.T) {\n\treset()\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\twebdavdConf := config.GetWebDAVDConfig()\n\twebdavdConf.Cache.MimeTypes.CustomMappings = []webdavd.CustomMimeMapping{\n\t\t{\n\t\t\tExt:  \".custom\",\n\t\t\tMime: \"application/custom\",\n\t\t},\n\t}\n\tcfg := map[string]any{\n\t\t\"webdavd\": webdavdConf,\n\t}\n\tdata, err := json.Marshal(cfg)\n\tassert.NoError(t, err)\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr = os.WriteFile(configFilePath, data, 0666)\n\tassert.NoError(t, err)\n\n\treset()\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tmappings := config.GetWebDAVDConfig().Cache.MimeTypes.CustomMappings\n\tif assert.Len(t, mappings, 1) {\n\t\tassert.Equal(t, \".custom\", mappings[0].Ext)\n\t\tassert.Equal(t, \"application/custom\", mappings[0].Mime)\n\t}\n\t// now add from env\n\tos.Setenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__1__EXT\", \".custom1\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__1__MIME\", \"application/custom1\")\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__0__EXT\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__0__MIME\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__1__EXT\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__1__MIME\")\n\t})\n\treset()\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tmappings = config.GetWebDAVDConfig().Cache.MimeTypes.CustomMappings\n\tif assert.Len(t, mappings, 2) {\n\t\tassert.Equal(t, \".custom\", mappings[0].Ext)\n\t\tassert.Equal(t, \"application/custom\", mappings[0].Mime)\n\t\tassert.Equal(t, \".custom1\", mappings[1].Ext)\n\t\tassert.Equal(t, \"application/custom1\", mappings[1].Mime)\n\t}\n\t// override from env\n\tos.Setenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__0__EXT\", \".custom0\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__0__MIME\", \"application/custom0\")\n\treset()\n\terr = config.LoadConfig(configDir, confName)\n\tassert.NoError(t, err)\n\tmappings = config.GetWebDAVDConfig().Cache.MimeTypes.CustomMappings\n\tif assert.Len(t, mappings, 2) {\n\t\tassert.Equal(t, \".custom0\", mappings[0].Ext)\n\t\tassert.Equal(t, \"application/custom0\", mappings[0].Mime)\n\t\tassert.Equal(t, \".custom1\", mappings[1].Ext)\n\t\tassert.Equal(t, \"application/custom1\", mappings[1].Mime)\n\t}\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebDAVBindingsFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS\", \"127.0.0.1\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__PORT\", \"8000\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS\", \"0\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES\", \"TLS_RSA_WITH_AES_128_CBC_SHA \")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__TLS_PROTOCOLS\", \"http/1.1 \")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__PROXY_MODE\", \"1\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED\", \"192.168.10.1\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER\", \"X-Forwarded-For\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH\", \"2\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS\", \"127.0.1.1\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__PORT\", \"9000\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS\", \"1\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__MIN_TLS_VERSION\", \"13\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE\", \"1\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__PREFIX\", \"/dav2\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__CERTIFICATE_FILE\", \"webdav.crt\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__CERTIFICATE_KEY_FILE\", \"webdav.key\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__2__DISABLE_WWW_AUTH_HEADER\", \"1\")\n\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__TLS_PROTOCOLS\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__PROXY_MODE\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__MIN_TLS_VERSION\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__PREFIX\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__CERTIFICATE_FILE\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__CERTIFICATE_KEY_FILE\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__2__DISABLE_WWW_AUTH_HEADER\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tbindings := config.GetWebDAVDConfig().Bindings\n\trequire.Len(t, bindings, 3)\n\trequire.Equal(t, 0, bindings[0].Port)\n\trequire.Empty(t, bindings[0].Address)\n\trequire.False(t, bindings[0].EnableHTTPS)\n\trequire.Equal(t, 12, bindings[0].MinTLSVersion)\n\trequire.Len(t, bindings[0].TLSCipherSuites, 0)\n\trequire.Len(t, bindings[0].Protocols, 0)\n\trequire.Equal(t, 0, bindings[0].ProxyMode)\n\trequire.Empty(t, bindings[0].Prefix)\n\trequire.Equal(t, 0, bindings[0].ClientIPHeaderDepth)\n\trequire.False(t, bindings[0].DisableWWWAuthHeader)\n\trequire.Equal(t, 8000, bindings[1].Port)\n\trequire.Equal(t, \"127.0.0.1\", bindings[1].Address)\n\trequire.False(t, bindings[1].EnableHTTPS)\n\trequire.Equal(t, 12, bindings[1].MinTLSVersion)\n\trequire.Equal(t, 0, bindings[1].ClientAuthType)\n\trequire.Len(t, bindings[1].TLSCipherSuites, 1)\n\trequire.Equal(t, \"TLS_RSA_WITH_AES_128_CBC_SHA\", bindings[1].TLSCipherSuites[0])\n\trequire.Len(t, bindings[1].Protocols, 1)\n\tassert.Equal(t, \"http/1.1\", bindings[1].Protocols[0])\n\trequire.Equal(t, 1, bindings[1].ProxyMode)\n\trequire.Equal(t, \"192.168.10.1\", bindings[1].ProxyAllowed[0])\n\trequire.Equal(t, \"X-Forwarded-For\", bindings[1].ClientIPProxyHeader)\n\trequire.Equal(t, 2, bindings[1].ClientIPHeaderDepth)\n\trequire.Empty(t, bindings[1].Prefix)\n\trequire.False(t, bindings[1].DisableWWWAuthHeader)\n\trequire.Equal(t, 9000, bindings[2].Port)\n\trequire.Equal(t, \"127.0.1.1\", bindings[2].Address)\n\trequire.True(t, bindings[2].EnableHTTPS)\n\trequire.Equal(t, 13, bindings[2].MinTLSVersion)\n\trequire.Equal(t, 1, bindings[2].ClientAuthType)\n\trequire.Equal(t, 0, bindings[2].ProxyMode)\n\trequire.Nil(t, bindings[2].TLSCipherSuites)\n\trequire.Equal(t, \"/dav2\", bindings[2].Prefix)\n\trequire.Equal(t, \"webdav.crt\", bindings[2].CertificateFile)\n\trequire.Equal(t, \"webdav.key\", bindings[2].CertificateKeyFile)\n\trequire.Equal(t, 0, bindings[2].ClientIPHeaderDepth)\n\trequire.True(t, bindings[2].DisableWWWAuthHeader)\n}\n\nfunc TestHTTPDBindingsFromEnv(t *testing.T) {\n\treset()\n\n\tsockPath := filepath.Clean(os.TempDir())\n\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__ADDRESS\", sockPath)\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__PORT\", \"0\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__0__TLS_CIPHER_SUITES\", \" TLS_AES_128_GCM_SHA256\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__1__ADDRESS\", \"127.0.0.1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__1__PORT\", \"8000\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS\", \"0\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__1__HIDE_LOGIN_URL\", \" 1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_ADMIN__NAME\", \"Web Admin\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_CLIENT__SHORT_NAME\", \"WebClient\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__ADDRESS\", \"127.0.1.1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__PORT\", \"9000\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN\", \"0\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT\", \"0\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API\", \"0\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS\", \"3\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS\", \"12\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI\", \"0\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__BASE_URL\", \"https://example.com\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__LANGUAGES\", \"en,es\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS\", \"1 \")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__MIN_TLS_VERSION\", \"13\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE\", \"1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES\", \" TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__TLS_PROTOCOLS\", \"h2, http/1.1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__PROXY_MODE\", \"1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED\", \" 192.168.9.1 , 172.16.25.0/24\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER\", \"X-Real-IP\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH\", \"2\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__HIDE_LOGIN_URL\", \"3\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__CLIENT_ID\", \"client id\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__CLIENT_SECRET\", \"client secret\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__CONFIG_URL\", \"config url\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__REDIRECT_BASE_URL\", \"redirect base url\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__USERNAME_FIELD\", \"preferred_username\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__ROLE_FIELD\", \"sftpgo_role\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__SCOPES\", \"openid\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__IMPLICIT_ROLES\", \"1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__CUSTOM_FIELDS\", \"field1,field2\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__INSECURE_SKIP_SIGNATURE_CHECK\", \"1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__DEBUG\", \"1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED\", \"true\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS\", \"*.example.com,*.example.net\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS_ARE_REGEX\", \"1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HOSTS_PROXY_HEADERS\", \"X-Forwarded-Host\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_REDIRECT\", \"1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_HOST\", \"www.example.com\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__KEY\", \"X-Forwarded-Proto\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__VALUE\", \"https\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_SECONDS\", \"31536000\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_INCLUDE_SUBDOMAINS\", \"false\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_PRELOAD\", \"0\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_TYPE_NOSNIFF\", \"t\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY\", \"script-src $NONCE\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY\", \"fullscreen=(), geolocation=()\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY\", \"same-origin\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY\", \"same-site\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY\", \"require-corp\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CACHE_CONTROL\", \"private\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__REFERRER_POLICY\", \"no-referrer\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH\", \"path1\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH\", \"path2\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH\", \"favicon.ico\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH\", \"logo.png\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME\", \"disclaimer\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH\", \"disclaimer.html\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS\", \"default.css\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS\", \"1.css,2.css\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__CERTIFICATE_FILE\", \"httpd.crt\")\n\tos.Setenv(\"SFTPGO_HTTPD__BINDINGS__2__CERTIFICATE_KEY_FILE\", \"httpd.key\")\n\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__0__TLS_CIPHER_SUITES\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__1__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__1__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__1__HIDE_LOGIN_URL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_ADMIN__NAME\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_CLIENT__SHORT_NAME\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__1__EXTRA_CSS__0__PATH\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__MIN_TLS_VERSION\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__BASE_URL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__LANGUAGES\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__TLS_PROTOCOLS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__PROXY_MODE\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__HIDE_LOGIN_URL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__CLIENT_ID\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__CLIENT_SECRET\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__CONFIG_URL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__REDIRECT_BASE_URL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__USERNAME_FIELD\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__ROLE_FIELD\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__SCOPES\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__IMPLICIT_ROLES\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__CUSTOM_FIELDS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__INSECURE_SKIP_SIGNATURE_CHECK\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__OIDC__DEBUG\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS_ARE_REGEX\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HOSTS_PROXY_HEADERS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_REDIRECT\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_HOST\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__KEY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__VALUE\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_SECONDS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_INCLUDE_SUBDOMAINS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_PRELOAD\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_TYPE_NOSNIFF\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__CACHE_CONTROL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__SECURITY__REFERRER_POLICY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__CERTIFICATE_FILE\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__BINDINGS__2__CERTIFICATE_KEY_FILE\")\n\t})\n\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tbindings := config.GetHTTPDConfig().Bindings\n\trequire.Len(t, bindings, 3)\n\trequire.Equal(t, 0, bindings[0].Port)\n\trequire.Equal(t, sockPath, bindings[0].Address)\n\trequire.False(t, bindings[0].EnableHTTPS)\n\trequire.Len(t, bindings[0].Protocols, 0)\n\trequire.Equal(t, 12, bindings[0].MinTLSVersion)\n\trequire.True(t, bindings[0].EnableWebAdmin)\n\trequire.True(t, bindings[0].EnableWebClient)\n\trequire.True(t, bindings[0].EnableRESTAPI)\n\trequire.Equal(t, 0, bindings[0].EnabledLoginMethods)\n\trequire.Equal(t, 0, bindings[0].DisabledLoginMethods)\n\trequire.True(t, bindings[0].RenderOpenAPI)\n\trequire.Empty(t, bindings[0].BaseURL)\n\trequire.Len(t, bindings[0].Languages, 1)\n\tassert.Contains(t, bindings[0].Languages, \"en\")\n\trequire.Len(t, bindings[0].TLSCipherSuites, 1)\n\trequire.Equal(t, 0, bindings[0].ProxyMode)\n\trequire.Empty(t, bindings[0].OIDC.ConfigURL)\n\trequire.Equal(t, \"TLS_AES_128_GCM_SHA256\", bindings[0].TLSCipherSuites[0])\n\trequire.Equal(t, 0, bindings[0].HideLoginURL)\n\trequire.False(t, bindings[0].Security.Enabled)\n\trequire.Equal(t, 0, bindings[0].ClientIPHeaderDepth)\n\trequire.Len(t, bindings[0].OIDC.Scopes, 3)\n\trequire.False(t, bindings[0].OIDC.InsecureSkipSignatureCheck)\n\trequire.False(t, bindings[0].OIDC.Debug)\n\trequire.Empty(t, bindings[0].Security.ReferrerPolicy)\n\trequire.Equal(t, 8000, bindings[1].Port)\n\trequire.Equal(t, \"127.0.0.1\", bindings[1].Address)\n\trequire.False(t, bindings[1].EnableHTTPS)\n\trequire.Equal(t, 12, bindings[0].MinTLSVersion)\n\trequire.True(t, bindings[1].EnableWebAdmin)\n\trequire.True(t, bindings[1].EnableWebClient)\n\trequire.True(t, bindings[1].EnableRESTAPI)\n\trequire.Equal(t, 0, bindings[1].EnabledLoginMethods)\n\trequire.Equal(t, 0, bindings[1].DisabledLoginMethods)\n\trequire.True(t, bindings[1].RenderOpenAPI)\n\trequire.Empty(t, bindings[1].BaseURL)\n\trequire.Len(t, bindings[1].Languages, 1)\n\tassert.Contains(t, bindings[1].Languages, \"en\")\n\trequire.Nil(t, bindings[1].TLSCipherSuites)\n\trequire.Equal(t, 1, bindings[1].HideLoginURL)\n\trequire.Empty(t, bindings[1].OIDC.ClientID)\n\trequire.Len(t, bindings[1].OIDC.Scopes, 3)\n\trequire.False(t, bindings[1].OIDC.InsecureSkipSignatureCheck)\n\trequire.False(t, bindings[1].OIDC.Debug)\n\trequire.False(t, bindings[1].Security.Enabled)\n\trequire.Equal(t, \"Web Admin\", bindings[1].Branding.WebAdmin.Name)\n\trequire.Equal(t, \"WebClient\", bindings[1].Branding.WebClient.ShortName)\n\trequire.Equal(t, 0, bindings[1].ProxyMode)\n\trequire.Equal(t, 0, bindings[1].ClientIPHeaderDepth)\n\trequire.Equal(t, 9000, bindings[2].Port)\n\trequire.Equal(t, \"127.0.1.1\", bindings[2].Address)\n\trequire.True(t, bindings[2].EnableHTTPS)\n\trequire.Equal(t, 13, bindings[2].MinTLSVersion)\n\trequire.False(t, bindings[2].EnableWebAdmin)\n\trequire.False(t, bindings[2].EnableWebClient)\n\trequire.False(t, bindings[2].EnableRESTAPI)\n\trequire.Equal(t, 3, bindings[2].EnabledLoginMethods)\n\trequire.Equal(t, 12, bindings[2].DisabledLoginMethods)\n\trequire.False(t, bindings[2].RenderOpenAPI)\n\trequire.Equal(t, \"https://example.com\", bindings[2].BaseURL)\n\trequire.Len(t, bindings[2].Languages, 2)\n\tassert.Contains(t, bindings[2].Languages, \"en\")\n\tassert.Contains(t, bindings[2].Languages, \"es\")\n\trequire.Equal(t, 1, bindings[2].ClientAuthType)\n\trequire.Len(t, bindings[2].TLSCipherSuites, 2)\n\trequire.Equal(t, \"TLS_AES_256_GCM_SHA384\", bindings[2].TLSCipherSuites[0])\n\trequire.Equal(t, \"TLS_CHACHA20_POLY1305_SHA256\", bindings[2].TLSCipherSuites[1])\n\trequire.Len(t, bindings[2].Protocols, 2)\n\trequire.Equal(t, \"h2\", bindings[2].Protocols[0])\n\trequire.Equal(t, \"http/1.1\", bindings[2].Protocols[1])\n\trequire.Equal(t, 1, bindings[2].ProxyMode)\n\trequire.Len(t, bindings[2].ProxyAllowed, 2)\n\trequire.Equal(t, \"192.168.9.1\", bindings[2].ProxyAllowed[0])\n\trequire.Equal(t, \"172.16.25.0/24\", bindings[2].ProxyAllowed[1])\n\trequire.Equal(t, \"X-Real-IP\", bindings[2].ClientIPProxyHeader)\n\trequire.Equal(t, 2, bindings[2].ClientIPHeaderDepth)\n\trequire.Equal(t, 3, bindings[2].HideLoginURL)\n\trequire.Equal(t, \"client id\", bindings[2].OIDC.ClientID)\n\trequire.Equal(t, \"client secret\", bindings[2].OIDC.ClientSecret)\n\trequire.Equal(t, \"config url\", bindings[2].OIDC.ConfigURL)\n\trequire.Equal(t, \"redirect base url\", bindings[2].OIDC.RedirectBaseURL)\n\trequire.Equal(t, \"preferred_username\", bindings[2].OIDC.UsernameField)\n\trequire.Equal(t, \"sftpgo_role\", bindings[2].OIDC.RoleField)\n\trequire.Len(t, bindings[2].OIDC.Scopes, 1)\n\trequire.Equal(t, \"openid\", bindings[2].OIDC.Scopes[0])\n\trequire.True(t, bindings[2].OIDC.ImplicitRoles)\n\trequire.Len(t, bindings[2].OIDC.CustomFields, 2)\n\trequire.Equal(t, \"field1\", bindings[2].OIDC.CustomFields[0])\n\trequire.Equal(t, \"field2\", bindings[2].OIDC.CustomFields[1])\n\trequire.True(t, bindings[2].OIDC.InsecureSkipSignatureCheck)\n\trequire.True(t, bindings[2].OIDC.Debug)\n\trequire.True(t, bindings[2].Security.Enabled)\n\trequire.Len(t, bindings[2].Security.AllowedHosts, 2)\n\trequire.Equal(t, \"*.example.com\", bindings[2].Security.AllowedHosts[0])\n\trequire.Equal(t, \"*.example.net\", bindings[2].Security.AllowedHosts[1])\n\trequire.True(t, bindings[2].Security.AllowedHostsAreRegex)\n\trequire.Len(t, bindings[2].Security.HostsProxyHeaders, 1)\n\trequire.Equal(t, \"X-Forwarded-Host\", bindings[2].Security.HostsProxyHeaders[0])\n\trequire.True(t, bindings[2].Security.HTTPSRedirect)\n\trequire.Equal(t, \"www.example.com\", bindings[2].Security.HTTPSHost)\n\trequire.Len(t, bindings[2].Security.HTTPSProxyHeaders, 1)\n\trequire.Equal(t, \"X-Forwarded-Proto\", bindings[2].Security.HTTPSProxyHeaders[0].Key)\n\trequire.Equal(t, \"https\", bindings[2].Security.HTTPSProxyHeaders[0].Value)\n\trequire.Equal(t, int64(31536000), bindings[2].Security.STSSeconds)\n\trequire.False(t, bindings[2].Security.STSIncludeSubdomains)\n\trequire.False(t, bindings[2].Security.STSPreload)\n\trequire.True(t, bindings[2].Security.ContentTypeNosniff)\n\trequire.Equal(t, \"script-src $NONCE\", bindings[2].Security.ContentSecurityPolicy)\n\trequire.Equal(t, \"fullscreen=(), geolocation=()\", bindings[2].Security.PermissionsPolicy)\n\trequire.Equal(t, \"same-origin\", bindings[2].Security.CrossOriginOpenerPolicy)\n\trequire.Equal(t, \"same-site\", bindings[2].Security.CrossOriginResourcePolicy)\n\trequire.Equal(t, \"require-corp\", bindings[2].Security.CrossOriginEmbedderPolicy)\n\trequire.Equal(t, \"private\", bindings[2].Security.CacheControl)\n\trequire.Equal(t, \"no-referrer\", bindings[2].Security.ReferrerPolicy)\n\trequire.Equal(t, \"favicon.ico\", bindings[2].Branding.WebAdmin.FaviconPath)\n\trequire.Equal(t, \"logo.png\", bindings[2].Branding.WebClient.LogoPath)\n\trequire.Equal(t, \"disclaimer\", bindings[2].Branding.WebClient.DisclaimerName)\n\trequire.Equal(t, \"disclaimer.html\", bindings[2].Branding.WebAdmin.DisclaimerPath)\n\trequire.Equal(t, []string{\"default.css\"}, bindings[2].Branding.WebClient.DefaultCSS)\n\trequire.Len(t, bindings[2].Branding.WebClient.ExtraCSS, 2)\n\trequire.Equal(t, \"1.css\", bindings[2].Branding.WebClient.ExtraCSS[0])\n\trequire.Equal(t, \"2.css\", bindings[2].Branding.WebClient.ExtraCSS[1])\n\trequire.Equal(t, \"httpd.crt\", bindings[2].CertificateFile)\n\trequire.Equal(t, \"httpd.key\", bindings[2].CertificateKeyFile)\n}\n\nfunc TestHTTPClientCertificatesFromEnv(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\thttpConf := config.GetHTTPConfig()\n\thttpConf.Certificates = append(httpConf.Certificates, httpclient.TLSKeyPair{\n\t\tCert: \"cert\",\n\t\tKey:  \"key\",\n\t})\n\tc := make(map[string]httpclient.Config)\n\tc[\"http\"] = httpConf\n\tjsonConf, err := json.Marshal(c)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\trequire.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\trequire.NoError(t, err)\n\trequire.Len(t, config.GetHTTPConfig().Certificates, 1)\n\trequire.Equal(t, \"cert\", config.GetHTTPConfig().Certificates[0].Cert)\n\trequire.Equal(t, \"key\", config.GetHTTPConfig().Certificates[0].Key)\n\n\tos.Setenv(\"SFTPGO_HTTP__CERTIFICATES__0__CERT\", \"cert0\")\n\tos.Setenv(\"SFTPGO_HTTP__CERTIFICATES__0__KEY\", \"key0\")\n\tos.Setenv(\"SFTPGO_HTTP__CERTIFICATES__8__CERT\", \"cert8\")\n\tos.Setenv(\"SFTPGO_HTTP__CERTIFICATES__9__CERT\", \"cert9\")\n\tos.Setenv(\"SFTPGO_HTTP__CERTIFICATES__9__KEY\", \"key9\")\n\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_HTTP__CERTIFICATES__0__CERT\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__CERTIFICATES__0__KEY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__CERTIFICATES__8__CERT\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__CERTIFICATES__9__CERT\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__CERTIFICATES__9__KEY\")\n\t})\n\n\terr = config.LoadConfig(configDir, confName)\n\trequire.NoError(t, err)\n\trequire.Len(t, config.GetHTTPConfig().Certificates, 2)\n\trequire.Equal(t, \"cert0\", config.GetHTTPConfig().Certificates[0].Cert)\n\trequire.Equal(t, \"key0\", config.GetHTTPConfig().Certificates[0].Key)\n\trequire.Equal(t, \"cert9\", config.GetHTTPConfig().Certificates[1].Cert)\n\trequire.Equal(t, \"key9\", config.GetHTTPConfig().Certificates[1].Key)\n\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n\n\tconfig.Init()\n\n\terr = config.LoadConfig(configDir, \"\")\n\trequire.NoError(t, err)\n\trequire.Len(t, config.GetHTTPConfig().Certificates, 2)\n\trequire.Equal(t, \"cert0\", config.GetHTTPConfig().Certificates[0].Cert)\n\trequire.Equal(t, \"key0\", config.GetHTTPConfig().Certificates[0].Key)\n\trequire.Equal(t, \"cert9\", config.GetHTTPConfig().Certificates[1].Cert)\n\trequire.Equal(t, \"key9\", config.GetHTTPConfig().Certificates[1].Key)\n}\n\nfunc TestHTTPClientHeadersFromEnv(t *testing.T) {\n\treset()\n\n\tconfName := tempConfigName + \".json\"\n\tconfigFilePath := filepath.Join(configDir, confName)\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\thttpConf := config.GetHTTPConfig()\n\thttpConf.Headers = append(httpConf.Headers, httpclient.Header{\n\t\tKey:   \"key\",\n\t\tValue: \"value\",\n\t\tURL:   \"url\",\n\t})\n\tc := make(map[string]httpclient.Config)\n\tc[\"http\"] = httpConf\n\tjsonConf, err := json.Marshal(c)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(configFilePath, jsonConf, os.ModePerm)\n\trequire.NoError(t, err)\n\terr = config.LoadConfig(configDir, confName)\n\trequire.NoError(t, err)\n\trequire.Len(t, config.GetHTTPConfig().Headers, 1)\n\trequire.Equal(t, \"key\", config.GetHTTPConfig().Headers[0].Key)\n\trequire.Equal(t, \"value\", config.GetHTTPConfig().Headers[0].Value)\n\trequire.Equal(t, \"url\", config.GetHTTPConfig().Headers[0].URL)\n\n\tos.Setenv(\"SFTPGO_HTTP__HEADERS__0__KEY\", \"key0\")\n\tos.Setenv(\"SFTPGO_HTTP__HEADERS__0__VALUE\", \"value0\")\n\tos.Setenv(\"SFTPGO_HTTP__HEADERS__0__URL\", \"url0\")\n\tos.Setenv(\"SFTPGO_HTTP__HEADERS__8__KEY\", \"key8\")\n\tos.Setenv(\"SFTPGO_HTTP__HEADERS__9__KEY\", \"key9\")\n\tos.Setenv(\"SFTPGO_HTTP__HEADERS__9__VALUE\", \"value9\")\n\tos.Setenv(\"SFTPGO_HTTP__HEADERS__9__URL\", \"url9\")\n\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_HTTP__HEADERS__0__KEY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__HEADERS__0__VALUE\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__HEADERS__0__URL\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__HEADERS__8__KEY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__HEADERS__9__KEY\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__HEADERS__9__VALUE\")\n\t\tos.Unsetenv(\"SFTPGO_HTTP__HEADERS__9__URL\")\n\t})\n\n\terr = config.LoadConfig(configDir, confName)\n\trequire.NoError(t, err)\n\trequire.Len(t, config.GetHTTPConfig().Headers, 2)\n\trequire.Equal(t, \"key0\", config.GetHTTPConfig().Headers[0].Key)\n\trequire.Equal(t, \"value0\", config.GetHTTPConfig().Headers[0].Value)\n\trequire.Equal(t, \"url0\", config.GetHTTPConfig().Headers[0].URL)\n\trequire.Equal(t, \"key9\", config.GetHTTPConfig().Headers[1].Key)\n\trequire.Equal(t, \"value9\", config.GetHTTPConfig().Headers[1].Value)\n\trequire.Equal(t, \"url9\", config.GetHTTPConfig().Headers[1].URL)\n\n\terr = os.Remove(configFilePath)\n\tassert.NoError(t, err)\n\n\tconfig.Init()\n\n\terr = config.LoadConfig(configDir, \"\")\n\trequire.NoError(t, err)\n\trequire.Len(t, config.GetHTTPConfig().Headers, 2)\n\trequire.Equal(t, \"key0\", config.GetHTTPConfig().Headers[0].Key)\n\trequire.Equal(t, \"value0\", config.GetHTTPConfig().Headers[0].Value)\n\trequire.Equal(t, \"url0\", config.GetHTTPConfig().Headers[0].URL)\n\trequire.Equal(t, \"key9\", config.GetHTTPConfig().Headers[1].Key)\n\trequire.Equal(t, \"value9\", config.GetHTTPConfig().Headers[1].Value)\n\trequire.Equal(t, \"url9\", config.GetHTTPConfig().Headers[1].URL)\n}\n\nfunc TestConfigFromEnv(t *testing.T) {\n\treset()\n\n\tos.Setenv(\"SFTPGO_SFTPD__BINDINGS__0__ADDRESS\", \"127.0.0.1\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__BINDINGS__0__PORT\", \"12000\")\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS\", \"41\")\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__POOL_SIZE\", \"10\")\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__IS_SHARED\", \"1\")\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON\", \"add\")\n\tos.Setenv(\"SFTPGO_KMS__SECRETS__URL\", \"local\")\n\tos.Setenv(\"SFTPGO_KMS__SECRETS__MASTER_KEY_PATH\", \"path\")\n\tos.Setenv(\"SFTPGO_TELEMETRY__TLS_CIPHER_SUITES\", \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\")\n\tos.Setenv(\"SFTPGO_TELEMETRY__TLS_PROTOCOLS\", \"h2\")\n\tos.Setenv(\"SFTPGO_HTTPD__SETUP__INSTALLATION_CODE\", \"123\")\n\tos.Setenv(\"SFTPGO_ACME__HTTP01_CHALLENGE__PORT\", \"5002\")\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(\"SFTPGO_SFTPD__BINDINGS__0__ADDRESS\")\n\t\tos.Unsetenv(\"SFTPGO_WEBDAVD__BINDINGS__0__PORT\")\n\t\tos.Unsetenv(\"SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS\")\n\t\tos.Unsetenv(\"SFTPGO_DATA_PROVIDER__POOL_SIZE\")\n\t\tos.Unsetenv(\"SFTPGO_DATA_PROVIDER__IS_SHARED\")\n\t\tos.Unsetenv(\"SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON\")\n\t\tos.Unsetenv(\"SFTPGO_KMS__SECRETS__URL\")\n\t\tos.Unsetenv(\"SFTPGO_KMS__SECRETS__MASTER_KEY_PATH\")\n\t\tos.Unsetenv(\"SFTPGO_TELEMETRY__TLS_CIPHER_SUITES\")\n\t\tos.Unsetenv(\"SFTPGO_TELEMETRY__TLS_PROTOCOLS\")\n\t\tos.Unsetenv(\"SFTPGO_HTTPD__SETUP__INSTALLATION_CODE\")\n\t\tos.Unsetenv(\"SFTPGO_ACME__HTTP01_CHALLENGE_PORT\")\n\t})\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tsftpdConfig := config.GetSFTPDConfig()\n\tassert.Equal(t, \"127.0.0.1\", sftpdConfig.Bindings[0].Address)\n\tassert.Equal(t, 12000, config.GetWebDAVDConfig().Bindings[0].Port)\n\tdataProviderConf := config.GetProviderConf()\n\tassert.Equal(t, uint32(41), dataProviderConf.PasswordHashing.Argon2Options.Iterations)\n\tassert.Equal(t, 10, dataProviderConf.PoolSize)\n\tassert.Equal(t, 1, dataProviderConf.IsShared)\n\tassert.Len(t, dataProviderConf.Actions.ExecuteOn, 1)\n\tassert.Contains(t, dataProviderConf.Actions.ExecuteOn, \"add\")\n\tkmsConfig := config.GetKMSConfig()\n\tassert.Equal(t, \"local\", kmsConfig.Secrets.URL)\n\tassert.Equal(t, \"path\", kmsConfig.Secrets.MasterKeyPath)\n\ttelemetryConfig := config.GetTelemetryConfig()\n\trequire.Len(t, telemetryConfig.TLSCipherSuites, 2)\n\tassert.Equal(t, \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", telemetryConfig.TLSCipherSuites[0])\n\tassert.Equal(t, \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", telemetryConfig.TLSCipherSuites[1])\n\trequire.Len(t, telemetryConfig.Protocols, 1)\n\tassert.Equal(t, \"h2\", telemetryConfig.Protocols[0])\n\tassert.Equal(t, \"123\", config.GetHTTPDConfig().Setup.InstallationCode)\n\tacmeConfig := config.GetACMEConfig()\n\tassert.Equal(t, 5002, acmeConfig.HTTP01Challenge.Port)\n}\n"
  },
  {
    "path": "internal/dataprovider/actions.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/command\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\t// ActionExecutorSelf is used as username for self action, for example a user/admin that updates itself\n\tActionExecutorSelf = \"__self__\"\n\t// ActionExecutorSystem is used as username for actions with no explicit executor associated, for example\n\t// adding/updating a user/admin by loading initial data\n\tActionExecutorSystem = \"__system__\"\n)\n\nconst (\n\tactionObjectUser        = \"user\"\n\tactionObjectFolder      = \"folder\"\n\tactionObjectGroup       = \"group\"\n\tactionObjectAdmin       = \"admin\"\n\tactionObjectAPIKey      = \"api_key\"\n\tactionObjectShare       = \"share\"\n\tactionObjectEventAction = \"event_action\"\n\tactionObjectEventRule   = \"event_rule\"\n\tactionObjectRole        = \"role\"\n\tactionObjectIPListEntry = \"ip_list_entry\"\n\tactionObjectConfigs     = \"configs\"\n)\n\nvar (\n\tactionsConcurrencyGuard = make(chan struct{}, 100)\n\treservedUsers           = []string{ActionExecutorSelf, ActionExecutorSystem}\n)\n\nfunc executeAction(operation, executor, ip, objectType, objectName, role string, object plugin.Renderer) {\n\tif plugin.Handler.HasNotifiers() {\n\t\tplugin.Handler.NotifyProviderEvent(&notifier.ProviderEvent{\n\t\t\tAction:     operation,\n\t\t\tUsername:   executor,\n\t\t\tObjectType: objectType,\n\t\t\tObjectName: objectName,\n\t\t\tIP:         ip,\n\t\t\tRole:       role,\n\t\t\tTimestamp:  time.Now().UnixNano(),\n\t\t}, object)\n\t}\n\tif fnHandleRuleForProviderEvent != nil {\n\t\tfnHandleRuleForProviderEvent(operation, executor, ip, objectType, objectName, role, object)\n\t}\n\tif config.Actions.Hook == \"\" {\n\t\treturn\n\t}\n\tif !slices.Contains(config.Actions.ExecuteOn, operation) ||\n\t\t!slices.Contains(config.Actions.ExecuteFor, objectType) {\n\t\treturn\n\t}\n\n\tgo func() {\n\t\tactionsConcurrencyGuard <- struct{}{}\n\t\tdefer func() {\n\t\t\t<-actionsConcurrencyGuard\n\t\t}()\n\n\t\tdataAsJSON, err := object.RenderAsJSON(operation != operationDelete)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to serialize user as JSON for operation %q: %v\", operation, err)\n\t\t\treturn\n\t\t}\n\t\tif strings.HasPrefix(config.Actions.Hook, \"http\") {\n\t\t\tvar url *url.URL\n\t\t\turl, err := url.Parse(config.Actions.Hook)\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"Invalid http_notification_url %q for operation %q: %v\",\n\t\t\t\t\tconfig.Actions.Hook, operation, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tq := url.Query()\n\t\t\tq.Add(\"action\", operation)\n\t\t\tq.Add(\"username\", executor)\n\t\t\tq.Add(\"ip\", ip)\n\t\t\tq.Add(\"object_type\", objectType)\n\t\t\tq.Add(\"object_name\", objectName)\n\t\t\tif role != \"\" {\n\t\t\t\tq.Add(\"role\", role)\n\t\t\t}\n\t\t\tq.Add(\"timestamp\", fmt.Sprintf(\"%d\", time.Now().UnixNano()))\n\t\t\turl.RawQuery = q.Encode()\n\t\t\tstartTime := time.Now()\n\t\t\tresp, err := httpclient.RetryablePost(url.String(), \"application/json\", bytes.NewBuffer(dataAsJSON))\n\t\t\trespCode := 0\n\t\t\tif err == nil {\n\t\t\t\trespCode = resp.StatusCode\n\t\t\t\tresp.Body.Close()\n\t\t\t}\n\t\t\tproviderLog(logger.LevelDebug, \"notified operation %q to URL: %s status code: %d, elapsed: %s err: %v\",\n\t\t\t\toperation, url.Redacted(), respCode, time.Since(startTime), err)\n\t\t\treturn\n\t\t}\n\t\texecuteNotificationCommand(operation, executor, ip, objectType, objectName, role, dataAsJSON) //nolint:errcheck // the error is used in test cases only\n\t}()\n}\n\nfunc executeNotificationCommand(operation, executor, ip, objectType, objectName, role string, objectAsJSON []byte) error {\n\tif !filepath.IsAbs(config.Actions.Hook) {\n\t\terr := fmt.Errorf(\"invalid notification command %q\", config.Actions.Hook)\n\t\tlogger.Warn(logSender, \"\", \"unable to execute notification command: %v\", err)\n\t\treturn err\n\t}\n\n\ttimeout, env, args := command.GetConfig(config.Actions.Hook, command.HookProviderActions)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, config.Actions.Hook, args...)\n\tcmd.Env = append(env,\n\t\tfmt.Sprintf(\"SFTPGO_PROVIDER_ACTION=%s\", operation),\n\t\tfmt.Sprintf(\"SFTPGO_PROVIDER_OBJECT_TYPE=%s\", objectType),\n\t\tfmt.Sprintf(\"SFTPGO_PROVIDER_OBJECT_NAME=%s\", objectName),\n\t\tfmt.Sprintf(\"SFTPGO_PROVIDER_USERNAME=%s\", executor),\n\t\tfmt.Sprintf(\"SFTPGO_PROVIDER_IP=%s\", ip),\n\t\tfmt.Sprintf(\"SFTPGO_PROVIDER_ROLE=%s\", role),\n\t\tfmt.Sprintf(\"SFTPGO_PROVIDER_TIMESTAMP=%d\", util.GetTimeAsMsSinceEpoch(time.Now())),\n\t\tfmt.Sprintf(\"SFTPGO_PROVIDER_OBJECT=%s\", objectAsJSON))\n\n\tstartTime := time.Now()\n\terr := cmd.Run()\n\tproviderLog(logger.LevelDebug, \"executed command %q, elapsed: %s, error: %v\", config.Actions.Hook,\n\t\ttime.Since(startTime), err)\n\treturn err\n}\n"
  },
  {
    "path": "internal/dataprovider/admin.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/alexedwards/argon2id\"\n\t\"github.com/sftpgo/sdk\"\n\tpasswordvalidator \"github.com/wagslane/go-password-validator\"\n\t\"golang.org/x/crypto/bcrypt\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// Available permissions for SFTPGo admins\nconst (\n\tPermAdminAny              = \"*\"\n\tPermAdminAddUsers         = \"add_users\"\n\tPermAdminChangeUsers      = \"edit_users\"\n\tPermAdminDeleteUsers      = \"del_users\"\n\tPermAdminViewUsers        = \"view_users\"\n\tPermAdminViewConnections  = \"view_conns\"\n\tPermAdminCloseConnections = \"close_conns\"\n\tPermAdminViewServerStatus = \"view_status\"\n\tPermAdminManageGroups     = \"manage_groups\"\n\tPermAdminManageFolders    = \"manage_folders\"\n\tPermAdminQuotaScans       = \"quota_scans\"\n\tPermAdminManageDefender   = \"manage_defender\"\n\tPermAdminViewDefender     = \"view_defender\"\n\tPermAdminViewEvents       = \"view_events\"\n\tPermAdminDisableMFA       = \"disable_mfa\"\n)\n\nconst (\n\t// GroupAddToUsersAsMembership defines that the admin's group will be added as membership group for new users\n\tGroupAddToUsersAsMembership = iota\n\t// GroupAddToUsersAsPrimary defines that the admin's group will be added as primary group for new users\n\tGroupAddToUsersAsPrimary\n\t// GroupAddToUsersAsSecondary defines that the admin's group will be added as secondary group for new users\n\tGroupAddToUsersAsSecondary\n)\n\nvar (\n\tvalidAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,\n\t\tPermAdminViewUsers, PermAdminManageFolders, PermAdminManageGroups, PermAdminViewConnections,\n\t\tPermAdminCloseConnections, PermAdminViewServerStatus, PermAdminQuotaScans,\n\t\tPermAdminManageDefender, PermAdminViewDefender, PermAdminViewEvents, PermAdminDisableMFA}\n\tforbiddenPermsForRoleAdmins = []string{PermAdminAny}\n)\n\n// AdminTOTPConfig defines the time-based one time password configuration\ntype AdminTOTPConfig struct {\n\tEnabled    bool        `json:\"enabled,omitempty\"`\n\tConfigName string      `json:\"config_name,omitempty\"`\n\tSecret     *kms.Secret `json:\"secret,omitempty\"`\n}\n\nfunc (c *AdminTOTPConfig) validate(username string) error {\n\tif !c.Enabled {\n\t\tc.ConfigName = \"\"\n\t\tc.Secret = kms.NewEmptySecret()\n\t\treturn nil\n\t}\n\tif c.ConfigName == \"\" {\n\t\treturn util.NewValidationError(\"totp: config name is mandatory\")\n\t}\n\tif !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"totp: config name %q not found\", c.ConfigName))\n\t}\n\tif c.Secret.IsEmpty() {\n\t\treturn util.NewValidationError(\"totp: secret is mandatory\")\n\t}\n\tif c.Secret.IsPlain() {\n\t\tc.Secret.SetAdditionalData(username)\n\t\tif err := c.Secret.Encrypt(); err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"totp: unable to encrypt secret: %v\", err))\n\t\t}\n\t}\n\treturn nil\n}\n\n// AdminPreferences defines the admin preferences\ntype AdminPreferences struct {\n\t// Allow to hide some sections from the user page.\n\t// These are not security settings and are not enforced server side\n\t// in any way. They are only intended to simplify the user page in\n\t// the WebAdmin UI.\n\t//\n\t// 1 means hide groups section\n\t// 2 means hide filesystem section, \"users_base_dir\" must be set in the config file otherwise this setting is ignored\n\t// 4 means hide virtual folders section\n\t// 8 means hide profile section\n\t// 16 means hide ACLs section\n\t// 32 means hide disk and bandwidth quota limits section\n\t// 64 means hide advanced settings section\n\t//\n\t// The settings can be combined\n\tHideUserPageSections int `json:\"hide_user_page_sections,omitempty\"`\n\t// Defines the default expiration for newly created users as number of days.\n\t// 0 means no expiration\n\tDefaultUsersExpiration int `json:\"default_users_expiration,omitempty\"`\n}\n\n// HideGroups returns true if the groups section should be hidden\nfunc (p *AdminPreferences) HideGroups() bool {\n\treturn p.HideUserPageSections&1 != 0\n}\n\n// HideFilesystem returns true if the filesystem section should be hidden\nfunc (p *AdminPreferences) HideFilesystem() bool {\n\treturn config.UsersBaseDir != \"\" && p.HideUserPageSections&2 != 0\n}\n\n// HideVirtualFolders returns true if the virtual folder section should be hidden\nfunc (p *AdminPreferences) HideVirtualFolders() bool {\n\treturn p.HideUserPageSections&4 != 0\n}\n\n// HideProfile returns true if the profile section should be hidden\nfunc (p *AdminPreferences) HideProfile() bool {\n\treturn p.HideUserPageSections&8 != 0\n}\n\n// HideACLs returns true if the ACLs section should be hidden\nfunc (p *AdminPreferences) HideACLs() bool {\n\treturn p.HideUserPageSections&16 != 0\n}\n\n// HideDiskQuotaAndBandwidthLimits returns true if the disk quota and bandwidth limits\n// section should be hidden\nfunc (p *AdminPreferences) HideDiskQuotaAndBandwidthLimits() bool {\n\treturn p.HideUserPageSections&32 != 0\n}\n\n// HideAdvancedSettings returns true if the advanced settings section should be hidden\nfunc (p *AdminPreferences) HideAdvancedSettings() bool {\n\treturn p.HideUserPageSections&64 != 0\n}\n\n// VisibleUserPageSections returns the number of visible sections\n// in the user page\nfunc (p *AdminPreferences) VisibleUserPageSections() int {\n\tvar result int\n\n\tif !p.HideProfile() {\n\t\tresult++\n\t}\n\tif !p.HideACLs() {\n\t\tresult++\n\t}\n\tif !p.HideDiskQuotaAndBandwidthLimits() {\n\t\tresult++\n\t}\n\tif !p.HideAdvancedSettings() {\n\t\tresult++\n\t}\n\n\treturn result\n}\n\n// AdminFilters defines additional restrictions for SFTPGo admins\n// TODO: rename to AdminOptions in v3\ntype AdminFilters struct {\n\t// only clients connecting from these IP/Mask are allowed.\n\t// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291\n\t// for example \"192.0.2.0/24\" or \"2001:db8::/32\"\n\tAllowList []string `json:\"allow_list,omitempty\"`\n\t// API key auth allows to impersonate this administrator with an API key\n\tAllowAPIKeyAuth bool `json:\"allow_api_key_auth,omitempty\"`\n\t// A password change is required at the next login\n\tRequirePasswordChange bool `json:\"require_password_change,omitempty\"`\n\t// Require two factor authentication\n\tRequireTwoFactor bool `json:\"require_two_factor\"`\n\t// Time-based one time passwords configuration\n\tTOTPConfig AdminTOTPConfig `json:\"totp_config,omitempty\"`\n\t// Recovery codes to use if the user loses access to their second factor auth device.\n\t// Each code can only be used once, you should use these codes to login and disable or\n\t// reset 2FA for your account\n\tRecoveryCodes []RecoveryCode   `json:\"recovery_codes,omitempty\"`\n\tPreferences   AdminPreferences `json:\"preferences\"`\n}\n\n// AdminGroupMappingOptions defines the options for admin/group mapping\ntype AdminGroupMappingOptions struct {\n\tAddToUsersAs int `json:\"add_to_users_as,omitempty\"`\n}\n\nfunc (o *AdminGroupMappingOptions) validate() error {\n\tif o.AddToUsersAs < GroupAddToUsersAsMembership || o.AddToUsersAs > GroupAddToUsersAsSecondary {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"Invalid mode to add groups to new users: %d\", o.AddToUsersAs))\n\t}\n\treturn nil\n}\n\n// GetUserGroupType returns the type for the matching user group\nfunc (o *AdminGroupMappingOptions) GetUserGroupType() int {\n\tswitch o.AddToUsersAs {\n\tcase GroupAddToUsersAsPrimary:\n\t\treturn sdk.GroupTypePrimary\n\tcase GroupAddToUsersAsSecondary:\n\t\treturn sdk.GroupTypeSecondary\n\tdefault:\n\t\treturn sdk.GroupTypeMembership\n\t}\n}\n\n// AdminGroupMapping defines the mapping between an SFTPGo admin and a group\ntype AdminGroupMapping struct {\n\tName    string                   `json:\"name\"`\n\tOptions AdminGroupMappingOptions `json:\"options\"`\n}\n\n// Admin defines a SFTPGo admin\ntype Admin struct {\n\t// Database unique identifier\n\tID int64 `json:\"id\"`\n\t// 1 enabled, 0 disabled (login is not allowed)\n\tStatus int `json:\"status\"`\n\t// Username\n\tUsername       string       `json:\"username\"`\n\tPassword       string       `json:\"password,omitempty\"`\n\tEmail          string       `json:\"email,omitempty\"`\n\tPermissions    []string     `json:\"permissions\"`\n\tFilters        AdminFilters `json:\"filters,omitempty\"`\n\tDescription    string       `json:\"description,omitempty\"`\n\tAdditionalInfo string       `json:\"additional_info,omitempty\"`\n\t// Groups membership\n\tGroups []AdminGroupMapping `json:\"groups,omitempty\"`\n\t// Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0\n\tCreatedAt int64 `json:\"created_at\"`\n\t// last update time as unix timestamp in milliseconds\n\tUpdatedAt int64 `json:\"updated_at\"`\n\t// Last login as unix timestamp in milliseconds\n\tLastLogin int64 `json:\"last_login\"`\n\t// Role name. If set the admin can only administer users with the same role.\n\t// Role admins cannot be super administrators\n\tRole string `json:\"role,omitempty\"`\n}\n\n// CountUnusedRecoveryCodes returns the number of unused recovery codes\nfunc (a *Admin) CountUnusedRecoveryCodes() int {\n\tunused := 0\n\tfor _, code := range a.Filters.RecoveryCodes {\n\t\tif !code.Used {\n\t\t\tunused++\n\t\t}\n\t}\n\treturn unused\n}\n\nfunc (a *Admin) hashPassword() error {\n\tif a.Password != \"\" && !util.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) {\n\t\tif config.PasswordValidation.Admins.MinEntropy > 0 {\n\t\t\tif err := passwordvalidator.Validate(a.Password, config.PasswordValidation.Admins.MinEntropy); err != nil {\n\t\t\t\treturn util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)\n\t\t\t}\n\t\t}\n\t\tif config.PasswordHashing.Algo == HashingAlgoBcrypt {\n\t\t\tpwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ta.Password = util.BytesToString(pwd)\n\t\t} else {\n\t\t\tpwd, err := argon2id.CreateHash(a.Password, argon2Params)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ta.Password = pwd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *Admin) hasRedactedSecret() bool {\n\treturn a.Filters.TOTPConfig.Secret.IsRedacted()\n}\n\nfunc (a *Admin) validateRecoveryCodes() error {\n\tfor i := 0; i < len(a.Filters.RecoveryCodes); i++ {\n\t\tcode := &a.Filters.RecoveryCodes[i]\n\t\tif code.Secret.IsEmpty() {\n\t\t\treturn util.NewValidationError(\"mfa: recovery code cannot be empty\")\n\t\t}\n\t\tif code.Secret.IsPlain() {\n\t\t\tcode.Secret.SetAdditionalData(a.Username)\n\t\t\tif err := code.Secret.Encrypt(); err != nil {\n\t\t\t\treturn util.NewValidationError(fmt.Sprintf(\"mfa: unable to encrypt recovery code: %v\", err))\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *Admin) validatePermissions() error {\n\ta.Permissions = util.RemoveDuplicates(a.Permissions, false)\n\tif len(a.Permissions) == 0 {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"please grant some permissions to this admin\"),\n\t\t\tutil.I18nErrorPermissionsRequired,\n\t\t)\n\t}\n\tif slices.Contains(a.Permissions, PermAdminAny) {\n\t\ta.Permissions = []string{PermAdminAny}\n\t}\n\tfor _, perm := range a.Permissions {\n\t\tif !slices.Contains(validAdminPerms, perm) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid permission: %q\", perm))\n\t\t}\n\t\tif a.Role != \"\" {\n\t\t\tif slices.Contains(forbiddenPermsForRoleAdmins, perm) {\n\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\tutil.NewValidationError(\"a role admin cannot be a super admin\"),\n\t\t\t\t\tutil.I18nErrorRoleAdminPerms,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *Admin) validateGroups() error {\n\thasPrimary := false\n\tfor _, g := range a.Groups {\n\t\tif g.Name == \"\" {\n\t\t\treturn util.NewValidationError(\"group name is mandatory\")\n\t\t}\n\t\tif err := g.Options.validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif g.Options.AddToUsersAs == GroupAddToUsersAsPrimary {\n\t\t\tif hasPrimary {\n\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\tutil.NewValidationError(\"only one primary group is allowed\"),\n\t\t\t\t\tutil.I18nErrorPrimaryGroup,\n\t\t\t\t)\n\t\t\t}\n\t\t\thasPrimary = true\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *Admin) applyNamingRules() {\n\ta.Username = config.convertName(a.Username)\n\ta.Role = config.convertName(a.Role)\n\tfor idx := range a.Groups {\n\t\ta.Groups[idx].Name = config.convertName(a.Groups[idx].Name)\n\t}\n}\n\nfunc (a *Admin) validate() error { //nolint:gocyclo\n\ta.SetEmptySecretsIfNil()\n\ta.applyNamingRules()\n\ta.Password = strings.TrimSpace(a.Password)\n\tif a.Username == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"username is mandatory\"), util.I18nErrorUsernameRequired)\n\t}\n\tif !util.IsNameValid(a.Username) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif err := checkReservedUsernames(a.Username); err != nil {\n\t\treturn util.NewI18nError(err, util.I18nErrorReservedUsername)\n\t}\n\tif a.Password == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"please set a password\"), util.I18nErrorPasswordRequired)\n\t}\n\tif a.hasRedactedSecret() {\n\t\treturn util.NewValidationError(\"cannot save an admin with a redacted secret\")\n\t}\n\tif err := a.Filters.TOTPConfig.validate(a.Username); err != nil {\n\t\treturn util.NewI18nError(err, util.I18nError2FAInvalid)\n\t}\n\tif err := a.validateRecoveryCodes(); err != nil {\n\t\treturn util.NewI18nError(err, util.I18nErrorRecoveryCodesInvalid)\n\t}\n\tif config.NamingRules&1 == 0 && !usernameRegex.MatchString(a.Username) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~\", a.Username)),\n\t\t\tutil.I18nErrorInvalidUser,\n\t\t)\n\t}\n\tif err := a.hashPassword(); err != nil {\n\t\treturn err\n\t}\n\tif err := a.validatePermissions(); err != nil {\n\t\treturn err\n\t}\n\tif a.Email != \"\" && !util.IsEmailValid(a.Email) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"email %q is not valid\", a.Email)),\n\t\t\tutil.I18nErrorInvalidEmail,\n\t\t)\n\t}\n\ta.Filters.AllowList = util.RemoveDuplicates(a.Filters.AllowList, false)\n\tfor _, IPMask := range a.Filters.AllowList {\n\t\t_, _, err := net.ParseCIDR(IPMask)\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not parse allow list entry %q : %v\", IPMask, err)),\n\t\t\t\tutil.I18nErrorInvalidIPMask,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn a.validateGroups()\n}\n\n// CheckPassword verifies the admin password\nfunc (a *Admin) CheckPassword(password string) (bool, error) {\n\tif config.PasswordCaching {\n\t\tfound, match := cachedAdminPasswords.Check(a.Username, password, a.Password)\n\t\tif found {\n\t\t\tif !match {\n\t\t\t\treturn false, ErrInvalidCredentials\n\t\t\t}\n\t\t\treturn match, nil\n\t\t}\n\t}\n\tif strings.HasPrefix(a.Password, bcryptPwdPrefix) {\n\t\tif err := bcrypt.CompareHashAndPassword([]byte(a.Password), []byte(password)); err != nil {\n\t\t\treturn false, ErrInvalidCredentials\n\t\t}\n\t\tcachedAdminPasswords.Add(a.Username, password, a.Password)\n\t\treturn true, nil\n\t}\n\tmatch, err := argon2id.ComparePasswordAndHash(password, a.Password)\n\tif !match || err != nil {\n\t\treturn false, ErrInvalidCredentials\n\t}\n\tif match {\n\t\tcachedAdminPasswords.Add(a.Username, password, a.Password)\n\t}\n\treturn match, err\n}\n\n// CanLoginFromIP returns true if login from the given IP is allowed\nfunc (a *Admin) CanLoginFromIP(ip string) bool {\n\tif len(a.Filters.AllowList) == 0 {\n\t\treturn true\n\t}\n\tparsedIP := net.ParseIP(ip)\n\tif parsedIP == nil {\n\t\treturn len(a.Filters.AllowList) == 0\n\t}\n\n\tfor _, ipMask := range a.Filters.AllowList {\n\t\t_, network, err := net.ParseCIDR(ipMask)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif network.Contains(parsedIP) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// CanLogin returns an error if the login is not allowed\nfunc (a *Admin) CanLogin(ip string) error {\n\tif a.Status != 1 {\n\t\treturn fmt.Errorf(\"admin %q is disabled\", a.Username)\n\t}\n\tif !a.CanLoginFromIP(ip) {\n\t\treturn fmt.Errorf(\"login from IP %v not allowed\", ip)\n\t}\n\treturn nil\n}\n\nfunc (a *Admin) checkUserAndPass(password, ip string) error {\n\tif err := a.CanLogin(ip); err != nil {\n\t\treturn err\n\t}\n\tif a.Password == \"\" || strings.TrimSpace(password) == \"\" {\n\t\treturn errors.New(\"credentials cannot be null or empty\")\n\t}\n\tmatch, err := a.CheckPassword(password)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !match {\n\t\treturn ErrInvalidCredentials\n\t}\n\treturn nil\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (a *Admin) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\tadmin, err := provider.adminExists(a.Username)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload admin before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tadmin.HideConfidentialData()\n\t\treturn json.Marshal(admin)\n\t}\n\ta.HideConfidentialData()\n\treturn json.Marshal(a)\n}\n\n// HideConfidentialData hides admin confidential data\nfunc (a *Admin) HideConfidentialData() {\n\ta.Password = \"\"\n\tif a.Filters.TOTPConfig.Secret != nil {\n\t\ta.Filters.TOTPConfig.Secret.Hide()\n\t}\n\tfor _, code := range a.Filters.RecoveryCodes {\n\t\tif code.Secret != nil {\n\t\t\tcode.Secret.Hide()\n\t\t}\n\t}\n\ta.SetNilSecretsIfEmpty()\n}\n\n// SetEmptySecretsIfNil sets the secrets to empty if nil\nfunc (a *Admin) SetEmptySecretsIfNil() {\n\tif a.Filters.TOTPConfig.Secret == nil {\n\t\ta.Filters.TOTPConfig.Secret = kms.NewEmptySecret()\n\t}\n}\n\n// SetNilSecretsIfEmpty set the secrets to nil if empty.\n// This is useful before rendering as JSON so the empty fields\n// will not be serialized.\nfunc (a *Admin) SetNilSecretsIfEmpty() {\n\tif a.Filters.TOTPConfig.Secret != nil && a.Filters.TOTPConfig.Secret.IsEmpty() {\n\t\ta.Filters.TOTPConfig.Secret = nil\n\t}\n}\n\n// HasPermission returns true if the admin has the specified permission\nfunc (a *Admin) HasPermission(perm string) bool {\n\tif slices.Contains(a.Permissions, PermAdminAny) {\n\t\treturn true\n\t}\n\treturn slices.Contains(a.Permissions, perm)\n}\n\n// HasPermissions returns true if the admin has all the specified permissions\nfunc (a *Admin) HasPermissions(perms ...string) bool {\n\tfor _, perm := range perms {\n\t\tif !a.HasPermission(perm) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn len(perms) > 0\n}\n\n// GetAllowedIPAsString returns the allowed IP as comma separated string\nfunc (a *Admin) GetAllowedIPAsString() string {\n\treturn strings.Join(a.Filters.AllowList, \",\")\n}\n\n// GetValidPerms returns the allowed admin permissions\nfunc (a *Admin) GetValidPerms() []string {\n\treturn validAdminPerms\n}\n\n// CanManageMFA returns true if the admin can add a multi-factor authentication configuration\nfunc (a *Admin) CanManageMFA() bool {\n\treturn len(mfa.GetAvailableTOTPConfigs()) > 0\n}\n\n// GetSignature returns a signature for this admin.\n// It will change after an update\nfunc (a *Admin) GetSignature() string {\n\treturn strconv.FormatInt(a.UpdatedAt, 10)\n}\n\nfunc (a *Admin) getACopy() Admin {\n\ta.SetEmptySecretsIfNil()\n\tpermissions := make([]string, len(a.Permissions))\n\tcopy(permissions, a.Permissions)\n\tfilters := AdminFilters{}\n\tfilters.AllowList = make([]string, len(a.Filters.AllowList))\n\tfilters.AllowAPIKeyAuth = a.Filters.AllowAPIKeyAuth\n\tfilters.RequirePasswordChange = a.Filters.RequirePasswordChange\n\tfilters.RequireTwoFactor = a.Filters.RequireTwoFactor\n\tfilters.TOTPConfig.Enabled = a.Filters.TOTPConfig.Enabled\n\tfilters.TOTPConfig.ConfigName = a.Filters.TOTPConfig.ConfigName\n\tfilters.TOTPConfig.Secret = a.Filters.TOTPConfig.Secret.Clone()\n\tcopy(filters.AllowList, a.Filters.AllowList)\n\tfilters.RecoveryCodes = make([]RecoveryCode, 0)\n\tfor _, code := range a.Filters.RecoveryCodes {\n\t\tif code.Secret == nil {\n\t\t\tcode.Secret = kms.NewEmptySecret()\n\t\t}\n\t\tfilters.RecoveryCodes = append(filters.RecoveryCodes, RecoveryCode{\n\t\t\tSecret: code.Secret.Clone(),\n\t\t\tUsed:   code.Used,\n\t\t})\n\t}\n\tfilters.Preferences = AdminPreferences{\n\t\tHideUserPageSections:   a.Filters.Preferences.HideUserPageSections,\n\t\tDefaultUsersExpiration: a.Filters.Preferences.DefaultUsersExpiration,\n\t}\n\tgroups := make([]AdminGroupMapping, 0, len(a.Groups))\n\tfor _, g := range a.Groups {\n\t\tgroups = append(groups, AdminGroupMapping{\n\t\t\tName: g.Name,\n\t\t\tOptions: AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: g.Options.AddToUsersAs,\n\t\t\t},\n\t\t})\n\t}\n\n\treturn Admin{\n\t\tID:             a.ID,\n\t\tStatus:         a.Status,\n\t\tUsername:       a.Username,\n\t\tPassword:       a.Password,\n\t\tEmail:          a.Email,\n\t\tPermissions:    permissions,\n\t\tGroups:         groups,\n\t\tFilters:        filters,\n\t\tAdditionalInfo: a.AdditionalInfo,\n\t\tDescription:    a.Description,\n\t\tLastLogin:      a.LastLogin,\n\t\tCreatedAt:      a.CreatedAt,\n\t\tUpdatedAt:      a.UpdatedAt,\n\t\tRole:           a.Role,\n\t}\n}\n\nfunc (a *Admin) setFromEnv() error {\n\tenvUsername := strings.TrimSpace(os.Getenv(\"SFTPGO_DEFAULT_ADMIN_USERNAME\"))\n\tenvPassword := strings.TrimSpace(os.Getenv(\"SFTPGO_DEFAULT_ADMIN_PASSWORD\"))\n\tif envUsername == \"\" || envPassword == \"\" {\n\t\treturn errors.New(`to create the default admin you need to set the env vars \"SFTPGO_DEFAULT_ADMIN_USERNAME\" and \"SFTPGO_DEFAULT_ADMIN_PASSWORD\"`)\n\t}\n\ta.Username = envUsername\n\ta.Password = envPassword\n\ta.Status = 1\n\ta.Permissions = []string{PermAdminAny}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dataprovider/apikey.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alexedwards/argon2id\"\n\t\"golang.org/x/crypto/bcrypt\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// APIKeyScope defines the supported API key scopes\ntype APIKeyScope int\n\n// Supported API key scopes\nconst (\n\t// the API key will be used for an admin\n\tAPIKeyScopeAdmin APIKeyScope = iota + 1\n\t// the API key will be used for a user\n\tAPIKeyScopeUser\n)\n\n// APIKey defines a SFTPGo API key.\n// API keys can be used as authentication alternative to short lived tokens\n// for REST API\ntype APIKey struct {\n\t// Database unique identifier\n\tID int64 `json:\"-\"`\n\t// Unique key identifier, used for key lookups.\n\t// The generated key is in the format `KeyID.hash(Key)` so we can split\n\t// and lookup by KeyID and then verify if the key matches the recorded hash\n\tKeyID string `json:\"id\"`\n\t// User friendly key name\n\tName string `json:\"name\"`\n\t// we store the hash of the key, this is just like a password\n\tKey       string      `json:\"key,omitempty\"`\n\tScope     APIKeyScope `json:\"scope\"`\n\tCreatedAt int64       `json:\"created_at\"`\n\tUpdatedAt int64       `json:\"updated_at\"`\n\t// 0 means never used\n\tLastUseAt int64 `json:\"last_use_at,omitempty\"`\n\t// 0 means never expire\n\tExpiresAt   int64  `json:\"expires_at,omitempty\"`\n\tDescription string `json:\"description,omitempty\"`\n\t// Username associated with this API key.\n\t// If empty and the scope is APIKeyScopeUser the key is valid for any user\n\tUser string `json:\"user,omitempty\"`\n\t// Admin username associated with this API key.\n\t// If empty and the scope is APIKeyScopeAdmin the key is valid for any admin\n\tAdmin string `json:\"admin,omitempty\"`\n\t// these fields are for internal use\n\tuserID   int64\n\tadminID  int64\n\tplainKey string\n}\n\nfunc (k *APIKey) getACopy() APIKey {\n\treturn APIKey{\n\t\tID:          k.ID,\n\t\tKeyID:       k.KeyID,\n\t\tName:        k.Name,\n\t\tKey:         k.Key,\n\t\tScope:       k.Scope,\n\t\tCreatedAt:   k.CreatedAt,\n\t\tUpdatedAt:   k.UpdatedAt,\n\t\tLastUseAt:   k.LastUseAt,\n\t\tExpiresAt:   k.ExpiresAt,\n\t\tDescription: k.Description,\n\t\tUser:        k.User,\n\t\tAdmin:       k.Admin,\n\t\tuserID:      k.userID,\n\t\tadminID:     k.adminID,\n\t}\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (k *APIKey) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\tapiKey, err := provider.apiKeyExists(k.KeyID)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload api key before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tapiKey.HideConfidentialData()\n\t\treturn json.Marshal(apiKey)\n\t}\n\tk.HideConfidentialData()\n\treturn json.Marshal(k)\n}\n\n// HideConfidentialData hides API key confidential data\nfunc (k *APIKey) HideConfidentialData() {\n\tk.Key = \"\"\n}\n\nfunc (k *APIKey) hashKey() error {\n\tif k.Key != \"\" && !util.IsStringPrefixInSlice(k.Key, internalHashPwdPrefixes) {\n\t\tif config.PasswordHashing.Algo == HashingAlgoBcrypt {\n\t\t\thashed, err := bcrypt.GenerateFromPassword([]byte(k.Key), config.PasswordHashing.BcryptOptions.Cost)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tk.Key = util.BytesToString(hashed)\n\t\t} else {\n\t\t\thashed, err := argon2id.CreateHash(k.Key, argon2Params)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tk.Key = hashed\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (k *APIKey) generateKey() {\n\tif k.KeyID != \"\" || k.Key != \"\" {\n\t\treturn\n\t}\n\tk.KeyID = util.GenerateUniqueID()\n\tk.Key = util.GenerateUniqueID()\n\tk.plainKey = k.Key\n}\n\n// DisplayKey returns the key to show to the user\nfunc (k *APIKey) DisplayKey() string {\n\treturn fmt.Sprintf(\"%v.%v\", k.KeyID, k.plainKey)\n}\n\nfunc (k *APIKey) validate() error {\n\tif k.Name == \"\" {\n\t\treturn util.NewValidationError(\"name is mandatory\")\n\t}\n\tif !util.IsNameValid(k.Name) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif k.Scope != APIKeyScopeAdmin && k.Scope != APIKeyScopeUser {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid scope: %v\", k.Scope))\n\t}\n\tk.generateKey()\n\tif err := k.hashKey(); err != nil {\n\t\treturn err\n\t}\n\tif k.User != \"\" && k.Admin != \"\" {\n\t\treturn util.NewValidationError(\"an API key can be related to a user or an admin, not both\")\n\t}\n\tif k.Scope == APIKeyScopeAdmin {\n\t\tk.User = \"\"\n\t}\n\tif k.Scope == APIKeyScopeUser {\n\t\tk.Admin = \"\"\n\t}\n\tif k.User != \"\" {\n\t\t_, err := provider.userExists(k.User, \"\")\n\t\tif err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unable to check API key user %v: %v\", k.User, err))\n\t\t}\n\t}\n\tif k.Admin != \"\" {\n\t\t_, err := provider.adminExists(k.Admin)\n\t\tif err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unable to check API key admin %v: %v\", k.Admin, err))\n\t\t}\n\t}\n\treturn nil\n}\n\n// Authenticate tries to authenticate the provided plain key\nfunc (k *APIKey) Authenticate(plainKey string) error {\n\tif k.ExpiresAt > 0 && k.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {\n\t\treturn fmt.Errorf(\"API key %q is expired, expiration timestamp: %v current timestamp: %v\", k.KeyID,\n\t\t\tk.ExpiresAt, util.GetTimeAsMsSinceEpoch(time.Now()))\n\t}\n\tif config.PasswordCaching {\n\t\tfound, match := cachedAPIKeys.Check(k.KeyID, plainKey, k.Key)\n\t\tif found {\n\t\t\tif !match {\n\t\t\t\treturn ErrInvalidCredentials\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\tif strings.HasPrefix(k.Key, bcryptPwdPrefix) {\n\t\tif err := bcrypt.CompareHashAndPassword([]byte(k.Key), []byte(plainKey)); err != nil {\n\t\t\treturn ErrInvalidCredentials\n\t\t}\n\t} else if strings.HasPrefix(k.Key, argonPwdPrefix) {\n\t\tmatch, err := argon2id.ComparePasswordAndHash(plainKey, k.Key)\n\t\tif err != nil || !match {\n\t\t\treturn ErrInvalidCredentials\n\t\t}\n\t}\n\n\tcachedAPIKeys.Add(k.KeyID, plainKey, k.Key)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dataprovider/bolt.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !nobolt\n\npackage dataprovider\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"time\"\n\n\tbolt \"go.etcd.io/bbolt\"\n\tbolterrors \"go.etcd.io/bbolt/errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tboltDatabaseVersion = 34\n)\n\nvar (\n\tusersBucket     = []byte(\"users\")\n\tgroupsBucket    = []byte(\"groups\")\n\tfoldersBucket   = []byte(\"folders\")\n\tadminsBucket    = []byte(\"admins\")\n\tapiKeysBucket   = []byte(\"api_keys\")\n\tsharesBucket    = []byte(\"shares\")\n\tactionsBucket   = []byte(\"events_actions\")\n\trulesBucket     = []byte(\"events_rules\")\n\trolesBucket     = []byte(\"roles\")\n\tipListsBucket   = []byte(\"ip_lists\")\n\tconfigsBucket   = []byte(\"configs\")\n\tdbVersionBucket = []byte(\"db_version\")\n\tdbVersionKey    = []byte(\"version\")\n\tconfigsKey      = []byte(\"configs\")\n\tboltBuckets     = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket,\n\t\tsharesBucket, actionsBucket, rulesBucket, rolesBucket, ipListsBucket, configsBucket, dbVersionBucket}\n)\n\n// BoltProvider defines the auth provider for bolt key/value store\ntype BoltProvider struct {\n\tdbHandle *bolt.DB\n}\n\nfunc init() {\n\tversion.AddFeature(\"+bolt\")\n}\n\nfunc initializeBoltProvider(basePath string) error {\n\tvar err error\n\n\tdbPath := config.Name\n\tif !util.IsFileInputValid(dbPath) {\n\t\treturn fmt.Errorf(\"invalid database path: %q\", dbPath)\n\t}\n\tif !filepath.IsAbs(dbPath) {\n\t\tdbPath = filepath.Join(basePath, dbPath)\n\t}\n\tdbHandle, err := bolt.Open(dbPath, 0600, &bolt.Options{\n\t\tNoGrowSync:   false,\n\t\tFreelistType: bolt.FreelistArrayType,\n\t\tTimeout:      5 * time.Second})\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"bolt key store handle created\")\n\n\t\tfor _, bucket := range boltBuckets {\n\t\t\tif err := dbHandle.Update(func(tx *bolt.Tx) error {\n\t\t\t\t_, e := tx.CreateBucketIfNotExists(bucket)\n\t\t\t\treturn e\n\t\t\t}); err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error creating bucket %q: %v\", string(bucket), err)\n\t\t\t}\n\t\t}\n\n\t\tprovider = &BoltProvider{dbHandle: dbHandle}\n\t} else {\n\t\tproviderLog(logger.LevelError, \"error creating bolt key/value store handler: %v\", err)\n\t}\n\treturn err\n}\n\nfunc (p *BoltProvider) checkAvailability() error {\n\t_, err := getBoltDatabaseVersion(p.dbHandle)\n\treturn err\n}\n\nfunc (p *BoltProvider) validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error) {\n\tvar user User\n\tif tlsCert == nil {\n\t\treturn user, errors.New(\"TLS certificate cannot be null or empty\")\n\t}\n\tuser, err := p.userExists(username, \"\")\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, err\n\t}\n\treturn checkUserAndTLSCertificate(&user, protocol, tlsCert)\n}\n\nfunc (p *BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {\n\tuser, err := p.userExists(username, \"\")\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, err\n\t}\n\treturn checkUserAndPass(&user, password, ip, protocol)\n}\n\nfunc (p *BoltProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {\n\tadmin, err := p.adminExists(username)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating admin %q: %v\", username, err)\n\t\treturn admin, err\n\t}\n\terr = admin.checkUserAndPass(password, ip)\n\treturn admin, err\n}\n\nfunc (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte, isSSHCert bool) (User, string, error) {\n\tvar user User\n\tif len(pubKey) == 0 {\n\t\treturn user, \"\", errors.New(\"credentials cannot be null or empty\")\n\t}\n\tuser, err := p.userExists(username, \"\")\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, \"\", err\n\t}\n\treturn checkUserAndPubKey(&user, pubKey, isSSHCert)\n}\n\nfunc (p *BoltProvider) updateAPIKeyLastUse(keyID string) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAPIKeysBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(keyID)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"key %q does not exist, unable to update last use\", keyID))\n\t\t}\n\t\tvar apiKey APIKey\n\t\terr = json.Unmarshal(u, &apiKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tapiKey.LastUseAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(apiKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = bucket.Put([]byte(keyID), buf)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"error updating last use for key %q: %v\", keyID, err)\n\t\t\treturn err\n\t\t}\n\t\tproviderLog(logger.LevelDebug, \"last use updated for key %q\", keyID)\n\t\treturn nil\n\t})\n}\n\nfunc (p *BoltProvider) getAdminSignature(username string) (string, error) {\n\tvar updatedAt int64\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAdminsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tu := bucket.Get([]byte(username))\n\t\tvar admin Admin\n\t\terr = json.Unmarshal(u, &admin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tupdatedAt = admin.UpdatedAt\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strconv.FormatInt(updatedAt, 10), nil\n}\n\nfunc (p *BoltProvider) getUserSignature(username string) (string, error) {\n\tvar updatedAt int64\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tu := bucket.Get([]byte(username))\n\t\tvar user User\n\t\terr = json.Unmarshal(u, &user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tupdatedAt = user.UpdatedAt\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strconv.FormatInt(updatedAt, 10), nil\n}\n\nfunc (p *BoltProvider) setUpdatedAt(username string) {\n\tp.dbHandle.Update(func(tx *bolt.Tx) error { //nolint:errcheck\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist, unable to update updated at\", username))\n\t\t}\n\t\tvar user User\n\t\terr = json.Unmarshal(u, &user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = bucket.Put([]byte(username), buf)\n\t\tif err == nil {\n\t\t\tproviderLog(logger.LevelDebug, \"updated at set for user %q\", username)\n\t\t\tsetLastUserUpdate()\n\t\t} else {\n\t\t\tproviderLog(logger.LevelWarn, \"error setting updated_at for user %q: %v\", username, err)\n\t\t}\n\t\treturn err\n\t})\n}\n\nfunc (p *BoltProvider) updateLastLogin(username string) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist, unable to update last login\", username))\n\t\t}\n\t\tvar user User\n\t\terr = json.Unmarshal(u, &user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = bucket.Put([]byte(username), buf)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"error updating last login for user %q: %v\", username, err)\n\t\t} else {\n\t\t\tproviderLog(logger.LevelDebug, \"last login updated for user %q\", username)\n\t\t}\n\t\treturn err\n\t})\n}\n\nfunc (p *BoltProvider) updateAdminLastLogin(username string) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAdminsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar a []byte\n\t\tif a = bucket.Get([]byte(username)); a == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"admin %q does not exist, unable to update last login\", username))\n\t\t}\n\t\tvar admin Admin\n\t\terr = json.Unmarshal(a, &admin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tadmin.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(admin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = bucket.Put([]byte(username), buf)\n\t\tif err == nil {\n\t\t\tproviderLog(logger.LevelDebug, \"last login updated for admin %q\", username)\n\t\t\treturn err\n\t\t}\n\t\tproviderLog(logger.LevelWarn, \"error updating last login for admin %q: %v\", username, err)\n\t\treturn err\n\t})\n}\n\nfunc (p *BoltProvider) updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist, unable to update transfer quota\",\n\t\t\t\tusername))\n\t\t}\n\t\tvar user User\n\t\terr = json.Unmarshal(u, &user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !reset {\n\t\t\tuser.UsedUploadDataTransfer += uploadSize\n\t\t\tuser.UsedDownloadDataTransfer += downloadSize\n\t\t} else {\n\t\t\tuser.UsedUploadDataTransfer = uploadSize\n\t\t\tuser.UsedDownloadDataTransfer = downloadSize\n\t\t}\n\t\tuser.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = bucket.Put([]byte(username), buf)\n\t\tproviderLog(logger.LevelDebug, \"transfer quota updated for user %q, ul increment: %v dl increment: %v is reset? %v\",\n\t\t\tusername, uploadSize, downloadSize, reset)\n\t\treturn err\n\t})\n}\n\nfunc (p *BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist, unable to update quota\", username))\n\t\t}\n\t\tvar user User\n\t\terr = json.Unmarshal(u, &user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif reset {\n\t\t\tuser.UsedQuotaSize = sizeAdd\n\t\t\tuser.UsedQuotaFiles = filesAdd\n\t\t} else {\n\t\t\tuser.UsedQuotaSize += sizeAdd\n\t\t\tuser.UsedQuotaFiles += filesAdd\n\t\t}\n\t\tuser.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = bucket.Put([]byte(username), buf)\n\t\tproviderLog(logger.LevelDebug, \"quota updated for user %q, files increment: %v size increment: %v is reset? %v\",\n\t\t\tusername, filesAdd, sizeAdd, reset)\n\t\treturn err\n\t})\n}\n\nfunc (p *BoltProvider) getUsedQuota(username string) (int, int64, int64, int64, error) {\n\tuser, err := p.userExists(username, \"\")\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get quota for user %v error: %v\", username, err)\n\t\treturn 0, 0, 0, 0, err\n\t}\n\treturn user.UsedQuotaFiles, user.UsedQuotaSize, user.UsedUploadDataTransfer, user.UsedDownloadDataTransfer, err\n}\n\nfunc (p *BoltProvider) adminExists(username string) (Admin, error) {\n\tvar admin Admin\n\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAdminsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ta := bucket.Get([]byte(username))\n\t\tif a == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"admin %v does not exist\", username))\n\t\t}\n\t\treturn json.Unmarshal(a, &admin)\n\t})\n\n\treturn admin, err\n}\n\nfunc (p *BoltProvider) addAdmin(admin *Admin) error {\n\terr := admin.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAdminsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroupBucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trolesBucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif a := bucket.Get([]byte(admin.Username)); a != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: admin %q already exists\", ErrDuplicatedKey, admin.Username),\n\t\t\t\tutil.I18nErrorDuplicatedUsername,\n\t\t\t)\n\t\t}\n\t\tid, err := bucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tadmin.ID = int64(id)\n\t\tadmin.LastLogin = 0\n\t\tadmin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tadmin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tfor idx := range admin.Groups {\n\t\t\terr = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif err = p.addAdminToRole(admin.Username, admin.Role, rolesBucket); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tbuf, err := json.Marshal(admin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(admin.Username), buf)\n\t})\n}\n\nfunc (p *BoltProvider) updateAdmin(admin *Admin) error {\n\terr := admin.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAdminsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroupBucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trolesBucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar a []byte\n\t\tif a = bucket.Get([]byte(admin.Username)); a == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"admin %v does not exist\", admin.Username))\n\t\t}\n\t\tvar oldAdmin Admin\n\t\terr = json.Unmarshal(a, &oldAdmin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err = p.removeAdminFromRole(oldAdmin.Username, oldAdmin.Role, rolesBucket); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor idx := range oldAdmin.Groups {\n\t\t\terr = p.removeAdminFromGroupMapping(oldAdmin.Username, oldAdmin.Groups[idx].Name, groupBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif err = p.addAdminToRole(admin.Username, admin.Role, rolesBucket); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor idx := range admin.Groups {\n\t\t\terr = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tadmin.ID = oldAdmin.ID\n\t\tadmin.CreatedAt = oldAdmin.CreatedAt\n\t\tadmin.LastLogin = oldAdmin.LastLogin\n\t\tadmin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(admin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(admin.Username), buf)\n\t})\n}\n\nfunc (p *BoltProvider) deleteAdmin(admin Admin) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAdminsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar a []byte\n\t\tif a = bucket.Get([]byte(admin.Username)); a == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"admin %v does not exist\", admin.Username))\n\t\t}\n\t\tvar oldAdmin Admin\n\t\terr = json.Unmarshal(a, &oldAdmin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(oldAdmin.Groups) > 0 {\n\t\t\tgroupBucket, err := p.getGroupsBucket(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor idx := range oldAdmin.Groups {\n\t\t\t\terr = p.removeAdminFromGroupMapping(oldAdmin.Username, oldAdmin.Groups[idx].Name, groupBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif oldAdmin.Role != \"\" {\n\t\t\trolesBucket, err := p.getRolesBucket(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err = p.removeAdminFromRole(oldAdmin.Username, oldAdmin.Role, rolesBucket); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif err := p.deleteRelatedAPIKey(tx, admin.Username, APIKeyScopeAdmin); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn bucket.Delete([]byte(admin.Username))\n\t})\n}\n\nfunc (p *BoltProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {\n\tadmins := make([]Admin, 0, limit)\n\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAdminsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\titNum := 0\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar admin Admin\n\t\t\t\terr = json.Unmarshal(v, &admin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tadmin.HideConfidentialData()\n\t\t\t\tadmins = append(admins, admin)\n\t\t\t\tif len(admins) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar admin Admin\n\t\t\t\terr = json.Unmarshal(v, &admin)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tadmin.HideConfidentialData()\n\t\t\t\tadmins = append(admins, admin)\n\t\t\t\tif len(admins) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn err\n\t})\n\n\treturn admins, err\n}\n\nfunc (p *BoltProvider) dumpAdmins() ([]Admin, error) {\n\tadmins := make([]Admin, 0, 30)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAdminsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar admin Admin\n\t\t\terr = json.Unmarshal(v, &admin)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tadmins = append(admins, admin)\n\t\t}\n\t\treturn err\n\t})\n\n\treturn admins, err\n}\n\nfunc (p *BoltProvider) userExists(username, role string) (User, error) {\n\tvar user User\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tu := bucket.Get([]byte(username))\n\t\tif u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser, err = p.joinUserAndFolders(u, foldersBucket)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !user.hasRole(role) {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n\t\t}\n\t\treturn nil\n\t})\n\treturn user, err\n}\n\nfunc (p *BoltProvider) addUser(user *User) error {\n\terr := ValidateUser(user)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroupBucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trolesBucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif u := bucket.Get([]byte(user.Username)); u != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: username %v already exists\", ErrDuplicatedKey, user.Username),\n\t\t\t\tutil.I18nErrorDuplicatedUsername,\n\t\t\t)\n\t\t}\n\t\tid, err := bucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser.ID = int64(id)\n\t\tuser.LastQuotaUpdate = 0\n\t\tuser.UsedQuotaSize = 0\n\t\tuser.UsedQuotaFiles = 0\n\t\tuser.UsedUploadDataTransfer = 0\n\t\tuser.UsedDownloadDataTransfer = 0\n\t\tuser.LastLogin = 0\n\t\tuser.FirstDownload = 0\n\t\tuser.FirstUpload = 0\n\t\tuser.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tif err := p.addUserToRole(user.Username, user.Role, rolesBucket); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor idx := range user.VirtualFolders {\n\t\t\terr = p.addRelationToFolderMapping(user.VirtualFolders[idx].Name, user, nil, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfor idx := range user.Groups {\n\t\t\terr = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name, groupBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(user.Username), buf)\n\t})\n}\n\nfunc (p *BoltProvider) updateUser(user *User) error {\n\terr := ValidateUser(user)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(user.Username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", user.Username))\n\t\t}\n\t\tvar oldUser User\n\t\terr = json.Unmarshal(u, &oldUser)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err = p.updateUserRelations(tx, user, oldUser); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser.ID = oldUser.ID\n\t\tuser.LastQuotaUpdate = oldUser.LastQuotaUpdate\n\t\tuser.UsedQuotaSize = oldUser.UsedQuotaSize\n\t\tuser.UsedQuotaFiles = oldUser.UsedQuotaFiles\n\t\tuser.UsedUploadDataTransfer = oldUser.UsedUploadDataTransfer\n\t\tuser.UsedDownloadDataTransfer = oldUser.UsedDownloadDataTransfer\n\t\tuser.LastLogin = oldUser.LastLogin\n\t\tuser.FirstDownload = oldUser.FirstDownload\n\t\tuser.FirstUpload = oldUser.FirstUpload\n\t\tuser.CreatedAt = oldUser.CreatedAt\n\t\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\terr = bucket.Put([]byte(user.Username), buf)\n\t\tif err == nil {\n\t\t\tsetLastUserUpdate()\n\t\t}\n\t\treturn err\n\t})\n}\n\nfunc (p *BoltProvider) deleteUser(user User, _ bool) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroupBucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trolesBucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(user.Username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", user.Username))\n\t\t}\n\t\tvar oldUser User\n\t\terr = json.Unmarshal(u, &oldUser)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := p.removeUserFromRole(oldUser.Username, oldUser.Role, rolesBucket); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor idx := range oldUser.VirtualFolders {\n\t\t\terr = p.removeRelationFromFolderMapping(oldUser.VirtualFolders[idx], oldUser.Username, \"\", foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfor idx := range oldUser.Groups {\n\t\t\terr = p.removeUserFromGroupMapping(oldUser.Username, oldUser.Groups[idx].Name, groupBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif err := p.deleteRelatedAPIKey(tx, user.Username, APIKeyScopeUser); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := p.deleteRelatedShares(tx, user.Username); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Delete([]byte(user.Username))\n\t})\n}\n\nfunc (p *BoltProvider) updateUserPassword(username, password string) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n\t\t}\n\t\tvar user User\n\t\terr = json.Unmarshal(u, &user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser.Password = password\n\t\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(username), buf)\n\t})\n}\n\nfunc (p *BoltProvider) dumpUsers() ([]User, error) {\n\tusers := make([]User, 0, 100)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tuser, err := p.joinUserAndFolders(v, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tusers = append(users, user)\n\t\t}\n\t\treturn err\n\t})\n\treturn users, err\n}\n\nfunc (p *BoltProvider) getRecentlyUpdatedUsers(after int64) ([]User, error) {\n\tif getLastUserUpdate() < after {\n\t\treturn nil, nil\n\t}\n\tusers := make([]User, 0, 10)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroupsBucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar user User\n\t\t\terr := json.Unmarshal(v, &user)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif user.UpdatedAt < after {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(user.VirtualFolders) > 0 {\n\t\t\t\tvar folders []vfs.VirtualFolder\n\t\t\t\tfor idx := range user.VirtualFolders {\n\t\t\t\t\tfolder := &user.VirtualFolders[idx]\n\t\t\t\t\tbaseFolder, err := p.folderExistsInternal(folder.Name, foldersBucket)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfolder.BaseVirtualFolder = baseFolder\n\t\t\t\t\tfolders = append(folders, *folder)\n\t\t\t\t}\n\t\t\t\tuser.VirtualFolders = folders\n\t\t\t}\n\t\t\tif len(user.Groups) > 0 {\n\t\t\t\tgroupMapping := make(map[string]Group)\n\t\t\t\tfor idx := range user.Groups {\n\t\t\t\t\tgroup, err := p.groupExistsInternal(user.Groups[idx].Name, groupsBucket)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tgroupMapping[group.Name] = group\n\t\t\t\t}\n\t\t\t\tuser.applyGroupSettings(groupMapping)\n\t\t\t}\n\t\t\tuser.SetEmptySecretsIfNil()\n\t\t\tusers = append(users, user)\n\t\t}\n\t\treturn err\n\t})\n\treturn users, err\n}\n\nfunc (p *BoltProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {\n\tusers := make([]User, 0, 10)\n\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroupsBucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar user User\n\t\t\terr := json.Unmarshal(v, &user)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif needFolders, ok := toFetch[user.Username]; ok {\n\t\t\t\tif needFolders && len(user.VirtualFolders) > 0 {\n\t\t\t\t\tvar folders []vfs.VirtualFolder\n\t\t\t\t\tfor idx := range user.VirtualFolders {\n\t\t\t\t\t\tfolder := &user.VirtualFolders[idx]\n\t\t\t\t\t\tbaseFolder, err := p.folderExistsInternal(folder.Name, foldersBucket)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfolder.BaseVirtualFolder = baseFolder\n\t\t\t\t\t\tfolders = append(folders, *folder)\n\t\t\t\t\t}\n\t\t\t\t\tuser.VirtualFolders = folders\n\t\t\t\t}\n\t\t\t\tif len(user.Groups) > 0 {\n\t\t\t\t\tgroupMapping := make(map[string]Group)\n\t\t\t\t\tfor idx := range user.Groups {\n\t\t\t\t\t\tgroup, err := p.groupExistsInternal(user.Groups[idx].Name, groupsBucket)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgroupMapping[group.Name] = group\n\t\t\t\t\t}\n\t\t\t\t\tuser.applyGroupSettings(groupMapping)\n\t\t\t\t}\n\n\t\t\t\tuser.SetEmptySecretsIfNil()\n\t\t\t\tuser.PrepareForRendering()\n\t\t\t\tusers = append(users, user)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn users, err\n}\n\nfunc (p *BoltProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {\n\tusers := make([]User, 0, limit)\n\tvar err error\n\tif limit <= 0 {\n\t\treturn users, err\n\t}\n\terr = p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\titNum := 0\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tuser, err := p.joinUserAndFolders(v, foldersBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif !user.hasRole(role) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tuser.PrepareForRendering()\n\t\t\t\tusers = append(users, user)\n\t\t\t\tif len(users) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tuser, err := p.joinUserAndFolders(v, foldersBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif !user.hasRole(role) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tuser.PrepareForRendering()\n\t\t\t\tusers = append(users, user)\n\t\t\t\tif len(users) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn err\n\t})\n\treturn users, err\n}\n\nfunc (p *BoltProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {\n\tfolders := make([]vfs.BaseVirtualFolder, 0, 50)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar folder vfs.BaseVirtualFolder\n\t\t\terr = json.Unmarshal(v, &folder)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfolders = append(folders, folder)\n\t\t}\n\t\treturn err\n\t})\n\treturn folders, err\n}\n\nfunc (p *BoltProvider) getFolders(limit, offset int, order string, _ bool) ([]vfs.BaseVirtualFolder, error) {\n\tfolders := make([]vfs.BaseVirtualFolder, 0, limit)\n\tvar err error\n\tif limit <= 0 {\n\t\treturn folders, err\n\t}\n\terr = p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\titNum := 0\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar folder vfs.BaseVirtualFolder\n\t\t\t\terr = json.Unmarshal(v, &folder)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfolder.PrepareForRendering()\n\t\t\t\tfolders = append(folders, folder)\n\t\t\t\tif len(folders) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar folder vfs.BaseVirtualFolder\n\t\t\t\terr = json.Unmarshal(v, &folder)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfolder.PrepareForRendering()\n\t\t\t\tfolders = append(folders, folder)\n\t\t\t\tif len(folders) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn err\n\t})\n\treturn folders, err\n}\n\nfunc (p *BoltProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {\n\tvar folder vfs.BaseVirtualFolder\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfolder, err = p.folderExistsInternal(name, bucket)\n\t\treturn err\n\t})\n\treturn folder, err\n}\n\nfunc (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {\n\terr := ValidateFolder(folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif f := bucket.Get([]byte(folder.Name)); f != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: folder %q already exists\", ErrDuplicatedKey, folder.Name),\n\t\t\t\tutil.I18nErrorDuplicatedUsername,\n\t\t\t)\n\t\t}\n\t\tfolder.Users = nil\n\t\tfolder.Groups = nil\n\t\treturn p.addFolderInternal(*folder, bucket)\n\t})\n}\n\nfunc (p *BoltProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {\n\terr := ValidateFolder(folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar f []byte\n\n\t\tif f = bucket.Get([]byte(folder.Name)); f == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"folder %v does not exist\", folder.Name))\n\t\t}\n\t\tvar oldFolder vfs.BaseVirtualFolder\n\t\terr = json.Unmarshal(f, &oldFolder)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfolder.ID = oldFolder.ID\n\t\tfolder.LastQuotaUpdate = oldFolder.LastQuotaUpdate\n\t\tfolder.UsedQuotaFiles = oldFolder.UsedQuotaFiles\n\t\tfolder.UsedQuotaSize = oldFolder.UsedQuotaSize\n\t\tfolder.Users = oldFolder.Users\n\t\tfolder.Groups = oldFolder.Groups\n\t\tbuf, err := json.Marshal(folder)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(folder.Name), buf)\n\t})\n}\n\nfunc (p *BoltProvider) deleteFolderMappings(folder vfs.BaseVirtualFolder, usersBucket, groupsBucket *bolt.Bucket) error {\n\tfor _, username := range folder.Users {\n\t\tvar u []byte\n\t\tif u = usersBucket.Get([]byte(username)); u == nil {\n\t\t\tcontinue\n\t\t}\n\t\tvar user User\n\t\terr := json.Unmarshal(u, &user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar folders []vfs.VirtualFolder\n\t\tfor _, userFolder := range user.VirtualFolders {\n\t\t\tif folder.Name != userFolder.Name {\n\t\t\t\tfolders = append(folders, userFolder)\n\t\t\t}\n\t\t}\n\t\tuser.VirtualFolders = folders\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = usersBucket.Put([]byte(user.Username), buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, groupname := range folder.Groups {\n\t\tvar u []byte\n\t\tif u = groupsBucket.Get([]byte(groupname)); u == nil {\n\t\t\tcontinue\n\t\t}\n\t\tvar group Group\n\t\terr := json.Unmarshal(u, &group)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar folders []vfs.VirtualFolder\n\t\tfor _, groupFolder := range group.VirtualFolders {\n\t\t\tif folder.Name != groupFolder.Name {\n\t\t\t\tfolders = append(folders, groupFolder)\n\t\t\t}\n\t\t}\n\t\tgroup.VirtualFolders = folders\n\t\tbuf, err := json.Marshal(group)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = groupsBucket.Put([]byte(group.Name), buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) deleteFolder(baseFolder vfs.BaseVirtualFolder) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tusersBucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroupsBucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar f []byte\n\t\tif f = bucket.Get([]byte(baseFolder.Name)); f == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"folder %v does not exist\", baseFolder.Name))\n\t\t}\n\t\tvar folder vfs.BaseVirtualFolder\n\t\terr = json.Unmarshal(f, &folder)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err = p.deleteFolderMappings(folder, usersBucket, groupsBucket); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn bucket.Delete([]byte(folder.Name))\n\t})\n}\n\nfunc (p *BoltProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar f []byte\n\t\tif f = bucket.Get([]byte(name)); f == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"folder %q does not exist, unable to update quota\", name))\n\t\t}\n\t\tvar folder vfs.BaseVirtualFolder\n\t\terr = json.Unmarshal(f, &folder)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif reset {\n\t\t\tfolder.UsedQuotaSize = sizeAdd\n\t\t\tfolder.UsedQuotaFiles = filesAdd\n\t\t} else {\n\t\t\tfolder.UsedQuotaSize += sizeAdd\n\t\t\tfolder.UsedQuotaFiles += filesAdd\n\t\t}\n\t\tfolder.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(folder)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(folder.Name), buf)\n\t})\n}\n\nfunc (p *BoltProvider) getUsedFolderQuota(name string) (int, int64, error) {\n\tfolder, err := p.getFolderByName(name)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get quota for folder %q error: %v\", name, err)\n\t\treturn 0, 0, err\n\t}\n\treturn folder.UsedQuotaFiles, folder.UsedQuotaSize, err\n}\n\nfunc (p *BoltProvider) getGroups(limit, offset int, order string, _ bool) ([]Group, error) {\n\tgroups := make([]Group, 0, limit)\n\tvar err error\n\tif limit <= 0 {\n\t\treturn groups, err\n\t}\n\terr = p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\titNum := 0\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar group Group\n\t\t\t\tgroup, err = p.joinGroupAndFolders(v, foldersBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tgroup.PrepareForRendering()\n\t\t\t\tgroups = append(groups, group)\n\t\t\t\tif len(groups) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar group Group\n\t\t\t\tgroup, err = p.joinGroupAndFolders(v, foldersBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tgroup.PrepareForRendering()\n\t\t\t\tgroups = append(groups, group)\n\t\t\t\tif len(groups) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn err\n\t})\n\treturn groups, err\n}\n\nfunc (p *BoltProvider) getGroupsWithNames(names []string) ([]Group, error) {\n\tvar groups []Group\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, name := range names {\n\t\t\tg := bucket.Get([]byte(name))\n\t\t\tif g == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tgroup, err := p.joinGroupAndFolders(g, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tgroups = append(groups, group)\n\t\t}\n\t\treturn nil\n\t})\n\treturn groups, err\n}\n\nfunc (p *BoltProvider) getUsersInGroups(names []string) ([]string, error) {\n\tvar usernames []string\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, name := range names {\n\t\t\tg := bucket.Get([]byte(name))\n\t\t\tif g == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar group Group\n\t\t\terr := json.Unmarshal(g, &group)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tusernames = append(usernames, group.Users...)\n\t\t}\n\t\treturn nil\n\t})\n\treturn usernames, err\n}\n\nfunc (p *BoltProvider) groupExists(name string) (Group, error) {\n\tvar group Group\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tg := bucket.Get([]byte(name))\n\t\tif g == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"group %q does not exist\", name))\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroup, err = p.joinGroupAndFolders(g, foldersBucket)\n\t\treturn err\n\t})\n\treturn group, err\n}\n\nfunc (p *BoltProvider) addGroup(group *Group) error {\n\tif err := group.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif u := bucket.Get([]byte(group.Name)); u != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: group %q already exists\", ErrDuplicatedKey, group.Name),\n\t\t\t\tutil.I18nErrorDuplicatedUsername,\n\t\t\t)\n\t\t}\n\t\tid, err := bucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroup.ID = int64(id)\n\t\tgroup.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tgroup.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tgroup.Users = nil\n\t\tgroup.Admins = nil\n\t\tfor idx := range group.VirtualFolders {\n\t\t\terr = p.addRelationToFolderMapping(group.VirtualFolders[idx].Name, nil, group, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tbuf, err := json.Marshal(group)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(group.Name), buf)\n\t})\n}\n\nfunc (p *BoltProvider) updateGroup(group *Group) error {\n\tif err := group.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar g []byte\n\t\tif g = bucket.Get([]byte(group.Name)); g == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"group %q does not exist\", group.Name))\n\t\t}\n\t\tvar oldGroup Group\n\t\terr = json.Unmarshal(g, &oldGroup)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor idx := range oldGroup.VirtualFolders {\n\t\t\terr = p.removeRelationFromFolderMapping(oldGroup.VirtualFolders[idx], \"\", oldGroup.Name, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfor idx := range group.VirtualFolders {\n\t\t\terr = p.addRelationToFolderMapping(group.VirtualFolders[idx].Name, nil, group, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tgroup.ID = oldGroup.ID\n\t\tgroup.CreatedAt = oldGroup.CreatedAt\n\t\tgroup.Users = oldGroup.Users\n\t\tgroup.Admins = oldGroup.Admins\n\t\tgroup.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(group)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(group.Name), buf)\n\t})\n}\n\nfunc (p *BoltProvider) deleteGroup(group Group) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar g []byte\n\t\tif g = bucket.Get([]byte(group.Name)); g == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"group %q does not exist\", group.Name))\n\t\t}\n\t\tvar oldGroup Group\n\t\terr = json.Unmarshal(g, &oldGroup)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(oldGroup.Users) > 0 {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"the group %q is referenced, it cannot be removed\", oldGroup.Name))\n\t\t}\n\t\tif len(oldGroup.VirtualFolders) > 0 {\n\t\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor idx := range oldGroup.VirtualFolders {\n\t\t\t\terr = p.removeRelationFromFolderMapping(oldGroup.VirtualFolders[idx], \"\", oldGroup.Name, foldersBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(oldGroup.Admins) > 0 {\n\t\t\tadminsBucket, err := p.getAdminsBucket(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor idx := range oldGroup.Admins {\n\t\t\t\terr = p.removeGroupFromAdminMapping(oldGroup.Name, oldGroup.Admins[idx], adminsBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn bucket.Delete([]byte(group.Name))\n\t})\n}\n\nfunc (p *BoltProvider) dumpGroups() ([]Group, error) {\n\tgroups := make([]Group, 0, 50)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getGroupsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfoldersBucket, err := p.getFoldersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tgroup, err := p.joinGroupAndFolders(v, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tgroups = append(groups, group)\n\t\t}\n\t\treturn err\n\t})\n\treturn groups, err\n}\n\nfunc (p *BoltProvider) apiKeyExists(keyID string) (APIKey, error) {\n\tvar apiKey APIKey\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAPIKeysBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tk := bucket.Get([]byte(keyID))\n\t\tif k == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"API key %v does not exist\", keyID))\n\t\t}\n\t\treturn json.Unmarshal(k, &apiKey)\n\t})\n\treturn apiKey, err\n}\n\nfunc (p *BoltProvider) addAPIKey(apiKey *APIKey) error {\n\terr := apiKey.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAPIKeysBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif a := bucket.Get([]byte(apiKey.KeyID)); a != nil {\n\t\t\treturn fmt.Errorf(\"API key %v already exists\", apiKey.KeyID)\n\t\t}\n\t\tid, err := bucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tapiKey.ID = int64(id)\n\t\tapiKey.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tapiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tapiKey.LastUseAt = 0\n\t\tif apiKey.User != \"\" {\n\t\t\tif err := p.userExistsInternal(tx, apiKey.User); err != nil {\n\t\t\t\treturn fmt.Errorf(\"%w: related user %q does not exists\", ErrForeignKeyViolated, apiKey.User)\n\t\t\t}\n\t\t}\n\t\tif apiKey.Admin != \"\" {\n\t\t\tif err := p.adminExistsInternal(tx, apiKey.Admin); err != nil {\n\t\t\t\treturn fmt.Errorf(\"%w: related admin %q does not exists\", ErrForeignKeyViolated, apiKey.Admin)\n\t\t\t}\n\t\t}\n\t\tbuf, err := json.Marshal(apiKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(apiKey.KeyID), buf)\n\t})\n}\n\nfunc (p *BoltProvider) updateAPIKey(apiKey *APIKey) error {\n\terr := apiKey.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAPIKeysBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar a []byte\n\n\t\tif a = bucket.Get([]byte(apiKey.KeyID)); a == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"API key %v does not exist\", apiKey.KeyID))\n\t\t}\n\t\tvar oldAPIKey APIKey\n\t\terr = json.Unmarshal(a, &oldAPIKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tapiKey.ID = oldAPIKey.ID\n\t\tapiKey.KeyID = oldAPIKey.KeyID\n\t\tapiKey.Key = oldAPIKey.Key\n\t\tapiKey.CreatedAt = oldAPIKey.CreatedAt\n\t\tapiKey.LastUseAt = oldAPIKey.LastUseAt\n\t\tapiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tif apiKey.User != \"\" {\n\t\t\tif err := p.userExistsInternal(tx, apiKey.User); err != nil {\n\t\t\t\treturn fmt.Errorf(\"%w: related user %q does not exists\", ErrForeignKeyViolated, apiKey.User)\n\t\t\t}\n\t\t}\n\t\tif apiKey.Admin != \"\" {\n\t\t\tif err := p.adminExistsInternal(tx, apiKey.Admin); err != nil {\n\t\t\t\treturn fmt.Errorf(\"%w: related admin %q does not exists\", ErrForeignKeyViolated, apiKey.Admin)\n\t\t\t}\n\t\t}\n\t\tbuf, err := json.Marshal(apiKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(apiKey.KeyID), buf)\n\t})\n}\n\nfunc (p *BoltProvider) deleteAPIKey(apiKey APIKey) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAPIKeysBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif bucket.Get([]byte(apiKey.KeyID)) == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"API key %v does not exist\", apiKey.KeyID))\n\t\t}\n\n\t\treturn bucket.Delete([]byte(apiKey.KeyID))\n\t})\n}\n\nfunc (p *BoltProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {\n\tapiKeys := make([]APIKey, 0, limit)\n\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAPIKeysBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\titNum := 0\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar apiKey APIKey\n\t\t\t\terr = json.Unmarshal(v, &apiKey)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tapiKey.HideConfidentialData()\n\t\t\t\tapiKeys = append(apiKeys, apiKey)\n\t\t\t\tif len(apiKeys) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar apiKey APIKey\n\t\t\terr = json.Unmarshal(v, &apiKey)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tapiKey.HideConfidentialData()\n\t\t\tapiKeys = append(apiKeys, apiKey)\n\t\t\tif len(apiKeys) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn apiKeys, err\n}\n\nfunc (p *BoltProvider) dumpAPIKeys() ([]APIKey, error) {\n\tapiKeys := make([]APIKey, 0, 30)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getAPIKeysBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar apiKey APIKey\n\t\t\terr = json.Unmarshal(v, &apiKey)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tapiKeys = append(apiKeys, apiKey)\n\t\t}\n\t\treturn err\n\t})\n\n\treturn apiKeys, err\n}\n\nfunc (p *BoltProvider) shareExists(shareID, username string) (Share, error) {\n\tvar share Share\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getSharesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts := bucket.Get([]byte(shareID))\n\t\tif s == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"Share %v does not exist\", shareID))\n\t\t}\n\t\tif err := json.Unmarshal(s, &share); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif username != \"\" && share.Username != username {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"Share %v does not exist\", shareID))\n\t\t}\n\t\treturn nil\n\t})\n\treturn share, err\n}\n\nfunc (p *BoltProvider) addShare(share *Share) error {\n\terr := share.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getSharesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif a := bucket.Get([]byte(share.ShareID)); a != nil {\n\t\t\treturn fmt.Errorf(\"share %q already exists\", share.ShareID)\n\t\t}\n\t\tid, err := bucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tshare.ID = int64(id)\n\t\tif !share.IsRestore {\n\t\t\tshare.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t\tshare.UpdatedAt = share.CreatedAt\n\t\t\tshare.LastUseAt = 0\n\t\t\tshare.UsedTokens = 0\n\t\t}\n\t\tif share.CreatedAt == 0 {\n\t\t\tshare.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t}\n\t\tif share.UpdatedAt == 0 {\n\t\t\tshare.UpdatedAt = share.CreatedAt\n\t\t}\n\t\tif err := p.userExistsInternal(tx, share.Username); err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"related user %q does not exists\", share.Username))\n\t\t}\n\t\tbuf, err := json.Marshal(share)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(share.ShareID), buf)\n\t})\n}\n\nfunc (p *BoltProvider) updateShare(share *Share) error {\n\tif err := share.validate(); err != nil {\n\t\treturn err\n\t}\n\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getSharesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar s []byte\n\n\t\tif s = bucket.Get([]byte(share.ShareID)); s == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"Share %v does not exist\", share.ShareID))\n\t\t}\n\t\tvar oldObject Share\n\t\tif err = json.Unmarshal(s, &oldObject); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif oldObject.Username != share.Username {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"Share %v does not exist\", share.ShareID))\n\t\t}\n\n\t\tshare.ID = oldObject.ID\n\t\tshare.ShareID = oldObject.ShareID\n\t\tif !share.IsRestore {\n\t\t\tshare.UsedTokens = oldObject.UsedTokens\n\t\t\tshare.CreatedAt = oldObject.CreatedAt\n\t\t\tshare.LastUseAt = oldObject.LastUseAt\n\t\t\tshare.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t}\n\t\tif share.CreatedAt == 0 {\n\t\t\tshare.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t}\n\t\tif share.UpdatedAt == 0 {\n\t\t\tshare.UpdatedAt = share.CreatedAt\n\t\t}\n\t\tif err := p.userExistsInternal(tx, share.Username); err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"related user %q does not exists\", share.Username))\n\t\t}\n\t\tbuf, err := json.Marshal(share)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(share.ShareID), buf)\n\t})\n}\n\nfunc (p *BoltProvider) deleteShare(share Share) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getSharesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar s []byte\n\n\t\tif s = bucket.Get([]byte(share.ShareID)); s == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"Share %v does not exist\", share.ShareID))\n\t\t}\n\t\tvar oldObject Share\n\t\tif err = json.Unmarshal(s, &oldObject); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif oldObject.Username != share.Username {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"Share %v does not exist\", share.ShareID))\n\t\t}\n\n\t\treturn bucket.Delete([]byte(share.ShareID))\n\t})\n}\n\nfunc (p *BoltProvider) getShares(limit int, offset int, order, username string) ([]Share, error) {\n\tshares := make([]Share, 0, limit)\n\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getSharesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\titNum := 0\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\tvar share Share\n\t\t\t\tif err := json.Unmarshal(v, &share); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif share.Username != username {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tshare.HideConfidentialData()\n\t\t\t\tshares = append(shares, share)\n\t\t\t\tif len(shares) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\tvar share Share\n\t\t\terr = json.Unmarshal(v, &share)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif share.Username != username {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tshare.HideConfidentialData()\n\t\t\tshares = append(shares, share)\n\t\t\tif len(shares) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn shares, err\n}\n\nfunc (p *BoltProvider) dumpShares() ([]Share, error) {\n\tshares := make([]Share, 0, 30)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getSharesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar share Share\n\t\t\terr = json.Unmarshal(v, &share)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tshares = append(shares, share)\n\t\t}\n\t\treturn err\n\t})\n\n\treturn shares, err\n}\n\nfunc (p *BoltProvider) updateShareLastUse(shareID string, numTokens int) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getSharesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(shareID)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"share %q does not exist, unable to update last use\", shareID))\n\t\t}\n\t\tvar share Share\n\t\terr = json.Unmarshal(u, &share)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tshare.LastUseAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tshare.UsedTokens += numTokens\n\t\tbuf, err := json.Marshal(share)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = bucket.Put([]byte(shareID), buf)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"error updating last use for share %q: %v\", shareID, err)\n\t\t\treturn err\n\t\t}\n\t\tproviderLog(logger.LevelDebug, \"last use updated for share %q\", shareID)\n\t\treturn nil\n\t})\n}\n\nfunc (p *BoltProvider) getDefenderHosts(_ int64, _ int) ([]DefenderEntry, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (p *BoltProvider) getDefenderHostByIP(_ string, _ int64) (DefenderEntry, error) {\n\treturn DefenderEntry{}, ErrNotImplemented\n}\n\nfunc (p *BoltProvider) isDefenderHostBanned(_ string) (DefenderEntry, error) {\n\treturn DefenderEntry{}, ErrNotImplemented\n}\n\nfunc (p *BoltProvider) updateDefenderBanTime(_ string, _ int) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) deleteDefenderHost(_ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) addDefenderEvent(_ string, _ int) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) setDefenderBanTime(_ string, _ int64) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) cleanupDefender(_ int64) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) addActiveTransfer(_ ActiveTransfer) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) updateActiveTransferSizes(_, _, _ int64, _ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) removeActiveTransfer(_ int64, _ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) cleanupActiveTransfers(_ time.Time) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) getActiveTransfers(_ time.Time) ([]ActiveTransfer, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (p *BoltProvider) addSharedSession(_ Session) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) deleteSharedSession(_ string, _ SessionType) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) getSharedSession(_ string, _ SessionType) (Session, error) {\n\treturn Session{}, ErrNotImplemented\n}\n\nfunc (p *BoltProvider) cleanupSharedSessions(_ SessionType, _ int64) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) getEventActions(limit, offset int, order string, _ bool) ([]BaseEventAction, error) {\n\tif limit <= 0 {\n\t\treturn nil, nil\n\t}\n\tactions := make([]BaseEventAction, 0, limit)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\titNum := 0\n\t\tcursor := bucket.Cursor()\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar action BaseEventAction\n\t\t\t\terr = json.Unmarshal(v, &action)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\taction.PrepareForRendering()\n\t\t\t\tactions = append(actions, action)\n\t\t\t\tif len(actions) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar action BaseEventAction\n\t\t\t\terr = json.Unmarshal(v, &action)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\taction.PrepareForRendering()\n\t\t\t\tactions = append(actions, action)\n\t\t\t\tif len(actions) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\treturn actions, err\n}\n\nfunc (p *BoltProvider) dumpEventActions() ([]BaseEventAction, error) {\n\tactions := make([]BaseEventAction, 0, 50)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar action BaseEventAction\n\t\t\terr = json.Unmarshal(v, &action)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tactions = append(actions, action)\n\t\t}\n\t\treturn nil\n\t})\n\treturn actions, err\n}\n\nfunc (p *BoltProvider) eventActionExists(name string) (BaseEventAction, error) {\n\tvar action BaseEventAction\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tk := bucket.Get([]byte(name))\n\t\tif k == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"action %q does not exist\", name))\n\t\t}\n\t\treturn json.Unmarshal(k, &action)\n\t})\n\treturn action, err\n}\n\nfunc (p *BoltProvider) addEventAction(action *BaseEventAction) error {\n\terr := action.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif a := bucket.Get([]byte(action.Name)); a != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: event action %q already exists\", ErrDuplicatedKey, action.Name),\n\t\t\t\tutil.I18nErrorDuplicatedName,\n\t\t\t)\n\t\t}\n\t\tid, err := bucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\taction.ID = int64(id)\n\t\taction.Rules = nil\n\t\tbuf, err := json.Marshal(action)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(action.Name), buf)\n\t})\n}\n\nfunc (p *BoltProvider) updateEventAction(action *BaseEventAction) error {\n\terr := action.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar a []byte\n\n\t\tif a = bucket.Get([]byte(action.Name)); a == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"event action %s does not exist\", action.Name))\n\t\t}\n\t\tvar oldAction BaseEventAction\n\t\terr = json.Unmarshal(a, &oldAction)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\taction.ID = oldAction.ID\n\t\taction.Name = oldAction.Name\n\t\taction.Rules = nil\n\t\tif len(oldAction.Rules) > 0 {\n\t\t\trulesBucket, err := p.getRulesBucket(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tvar relatedRules []string\n\t\t\tfor _, ruleName := range oldAction.Rules {\n\t\t\t\tr := rulesBucket.Get([]byte(ruleName))\n\t\t\t\tif r != nil {\n\t\t\t\t\trelatedRules = append(relatedRules, ruleName)\n\t\t\t\t\tvar rule EventRule\n\t\t\t\t\terr := json.Unmarshal(r, &rule)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\trule.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t\t\t\tbuf, err := json.Marshal(rule)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tif err = rulesBucket.Put([]byte(rule.Name), buf); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tsetLastRuleUpdate()\n\t\t\t\t}\n\t\t\t}\n\t\t\taction.Rules = relatedRules\n\t\t}\n\t\tbuf, err := json.Marshal(action)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(action.Name), buf)\n\t})\n}\n\nfunc (p *BoltProvider) deleteEventAction(action BaseEventAction) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar a []byte\n\n\t\tif a = bucket.Get([]byte(action.Name)); a == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"action %s does not exist\", action.Name))\n\t\t}\n\t\tvar oldAction BaseEventAction\n\t\terr = json.Unmarshal(a, &oldAction)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(oldAction.Rules) > 0 {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"action %s is referenced, it cannot be removed\", oldAction.Name))\n\t\t}\n\t\treturn bucket.Delete([]byte(action.Name))\n\t})\n}\n\nfunc (p *BoltProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {\n\tif limit <= 0 {\n\t\treturn nil, nil\n\t}\n\trules := make([]EventRule, 0, limit)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRulesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tactionsBucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\titNum := 0\n\t\tcursor := bucket.Cursor()\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar rule EventRule\n\t\t\t\trule, err = p.joinRuleAndActions(v, actionsBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\trule.PrepareForRendering()\n\t\t\t\trules = append(rules, rule)\n\t\t\t\tif len(rules) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar rule EventRule\n\t\t\t\trule, err = p.joinRuleAndActions(v, actionsBucket)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\trule.PrepareForRendering()\n\t\t\t\trules = append(rules, rule)\n\t\t\t\tif len(rules) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn err\n\t})\n\treturn rules, err\n}\n\nfunc (p *BoltProvider) dumpEventRules() ([]EventRule, error) {\n\trules := make([]EventRule, 0, 50)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRulesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tactionsBucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\trule, err := p.joinRuleAndActions(v, actionsBucket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trules = append(rules, rule)\n\t\t}\n\t\treturn nil\n\t})\n\treturn rules, err\n}\n\nfunc (p *BoltProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {\n\tif getLastRuleUpdate() < after {\n\t\treturn nil, nil\n\t}\n\trules := make([]EventRule, 0, 10)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRulesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tactionsBucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar rule EventRule\n\t\t\terr := json.Unmarshal(v, &rule)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif rule.UpdatedAt < after {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar actions []EventAction\n\t\t\tfor idx := range rule.Actions {\n\t\t\t\taction := &rule.Actions[idx]\n\t\t\t\tvar baseAction BaseEventAction\n\t\t\t\tk := actionsBucket.Get([]byte(action.Name))\n\t\t\t\tif k == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\terr = json.Unmarshal(k, &baseAction)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tbaseAction.Options.SetEmptySecretsIfNil()\n\t\t\t\taction.BaseEventAction = baseAction\n\t\t\t\tactions = append(actions, *action)\n\t\t\t}\n\t\t\trule.Actions = actions\n\t\t\trules = append(rules, rule)\n\t\t}\n\t\treturn nil\n\t})\n\treturn rules, err\n}\n\nfunc (p *BoltProvider) eventRuleExists(name string) (EventRule, error) {\n\tvar rule EventRule\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRulesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr := bucket.Get([]byte(name))\n\t\tif r == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"event rule %q does not exist\", name))\n\t\t}\n\t\tactionsBucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trule, err = p.joinRuleAndActions(r, actionsBucket)\n\t\treturn err\n\t})\n\treturn rule, err\n}\n\nfunc (p *BoltProvider) addEventRule(rule *EventRule) error {\n\tif err := rule.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRulesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tactionsBucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif r := bucket.Get([]byte(rule.Name)); r != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: event rule %q already exists\", ErrDuplicatedKey, rule.Name),\n\t\t\t\tutil.I18nErrorDuplicatedName,\n\t\t\t)\n\t\t}\n\t\tid, err := bucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trule.ID = int64(id)\n\t\trule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\trule.UpdatedAt = rule.CreatedAt\n\t\tfor idx := range rule.Actions {\n\t\t\tif err = p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name, actionsBucket); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tsort.Slice(rule.Actions, func(i, j int) bool {\n\t\t\treturn rule.Actions[i].Order < rule.Actions[j].Order\n\t\t})\n\t\tbuf, err := json.Marshal(rule)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = bucket.Put([]byte(rule.Name), buf)\n\t\tif err == nil {\n\t\t\tsetLastRuleUpdate()\n\t\t}\n\t\treturn err\n\t})\n}\n\nfunc (p *BoltProvider) updateEventRule(rule *EventRule) error {\n\tif err := rule.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRulesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tactionsBucket, err := p.getActionsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar r []byte\n\t\tif r = bucket.Get([]byte(rule.Name)); r == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"event rule %q does not exist\", rule.Name))\n\t\t}\n\t\tvar oldRule EventRule\n\t\tif err = json.Unmarshal(r, &oldRule); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor idx := range oldRule.Actions {\n\t\t\tif err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name, actionsBucket); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfor idx := range rule.Actions {\n\t\t\tif err = p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name, actionsBucket); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\trule.ID = oldRule.ID\n\t\trule.CreatedAt = oldRule.CreatedAt\n\t\trule.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(rule)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsort.Slice(rule.Actions, func(i, j int) bool {\n\t\t\treturn rule.Actions[i].Order < rule.Actions[j].Order\n\t\t})\n\t\terr = bucket.Put([]byte(rule.Name), buf)\n\t\tif err == nil {\n\t\t\tsetLastRuleUpdate()\n\t\t}\n\t\treturn err\n\t})\n}\n\nfunc (p *BoltProvider) deleteEventRule(rule EventRule, _ bool) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRulesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar r []byte\n\t\tif r = bucket.Get([]byte(rule.Name)); r == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"event rule %q does not exist\", rule.Name))\n\t\t}\n\t\tvar oldRule EventRule\n\t\tif err = json.Unmarshal(r, &oldRule); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(oldRule.Actions) > 0 {\n\t\t\tactionsBucket, err := p.getActionsBucket(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor idx := range oldRule.Actions {\n\t\t\t\tif err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name, actionsBucket); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn bucket.Delete([]byte(rule.Name))\n\t})\n}\n\nfunc (*BoltProvider) getTaskByName(_ string) (Task, error) {\n\treturn Task{}, ErrNotImplemented\n}\n\nfunc (*BoltProvider) addTask(_ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (*BoltProvider) updateTask(_ string, _ int64) error {\n\treturn ErrNotImplemented\n}\n\nfunc (*BoltProvider) updateTaskTimestamp(_ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (*BoltProvider) addNode() error {\n\treturn ErrNotImplemented\n}\n\nfunc (*BoltProvider) getNodeByName(_ string) (Node, error) {\n\treturn Node{}, ErrNotImplemented\n}\n\nfunc (*BoltProvider) getNodes() ([]Node, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (*BoltProvider) updateNodeTimestamp() error {\n\treturn ErrNotImplemented\n}\n\nfunc (*BoltProvider) cleanupNodes() error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *BoltProvider) roleExists(name string) (Role, error) {\n\tvar role Role\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr := bucket.Get([]byte(name))\n\t\tif r == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"role %q does not exist\", name))\n\t\t}\n\t\treturn json.Unmarshal(r, &role)\n\t})\n\treturn role, err\n}\n\nfunc (p *BoltProvider) addRole(role *Role) error {\n\tif err := role.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif r := bucket.Get([]byte(role.Name)); r != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: role %q already exists\", ErrDuplicatedKey, role.Name),\n\t\t\t\tutil.I18nErrorDuplicatedName,\n\t\t\t)\n\t\t}\n\t\tid, err := bucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trole.ID = int64(id)\n\t\trole.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\trole.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\trole.Users = nil\n\t\trole.Admins = nil\n\t\tbuf, err := json.Marshal(role)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(role.Name), buf)\n\t})\n}\n\nfunc (p *BoltProvider) updateRole(role *Role) error {\n\tif err := role.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar r []byte\n\t\tif r = bucket.Get([]byte(role.Name)); r == nil {\n\t\t\treturn fmt.Errorf(\"role %q does not exist\", role.Name)\n\t\t}\n\t\tvar oldRole Role\n\t\terr = json.Unmarshal(r, &oldRole)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trole.ID = oldRole.ID\n\t\trole.CreatedAt = oldRole.CreatedAt\n\t\trole.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\trole.Users = oldRole.Users\n\t\trole.Admins = oldRole.Admins\n\t\tbuf, err := json.Marshal(role)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(role.Name), buf)\n\t})\n}\n\nfunc (p *BoltProvider) deleteRole(role Role) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar r []byte\n\t\tif r = bucket.Get([]byte(role.Name)); r == nil {\n\t\t\treturn fmt.Errorf(\"role %q does not exist\", role.Name)\n\t\t}\n\t\tvar oldRole Role\n\t\terr = json.Unmarshal(r, &oldRole)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(oldRole.Admins) > 0 {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"the role %q is referenced, it cannot be removed\", oldRole.Name))\n\t\t}\n\t\tif len(oldRole.Users) > 0 {\n\t\t\tbucket, err := p.getUsersBucket(tx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor _, username := range oldRole.Users {\n\t\t\t\tif err := p.removeRoleFromUser(username, oldRole.Name, bucket); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn bucket.Delete([]byte(role.Name))\n\t})\n}\n\nfunc (p *BoltProvider) getRoles(limit int, offset int, order string, _ bool) ([]Role, error) {\n\troles := make([]Role, 0, limit)\n\tif limit <= 0 {\n\t\treturn roles, nil\n\t}\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\titNum := 0\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar role Role\n\t\t\t\terr = json.Unmarshal(v, &role)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\troles = append(roles, role)\n\t\t\t\tif len(roles) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {\n\t\t\t\titNum++\n\t\t\t\tif itNum <= offset {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvar role Role\n\t\t\t\terr = json.Unmarshal(v, &role)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\troles = append(roles, role)\n\t\t\t\tif len(roles) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\treturn roles, err\n}\n\nfunc (p *BoltProvider) dumpRoles() ([]Role, error) {\n\troles := make([]Role, 0, 10)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getRolesBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar role Role\n\t\t\terr = json.Unmarshal(v, &role)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\troles = append(roles, role)\n\t\t}\n\t\treturn err\n\t})\n\treturn roles, err\n}\n\nfunc (p *BoltProvider) ipListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error) {\n\tentry := IPListEntry{\n\t\tIPOrNet: ipOrNet,\n\t\tType:    listType,\n\t}\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getIPListsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\te := bucket.Get([]byte(entry.getKey()))\n\t\tif e == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"entry %q does not exist\", entry.IPOrNet))\n\t\t}\n\t\terr = json.Unmarshal(e, &entry)\n\t\tif err == nil {\n\t\t\tentry.PrepareForRendering()\n\t\t}\n\t\treturn err\n\t})\n\treturn entry, err\n}\n\nfunc (p *BoltProvider) addIPListEntry(entry *IPListEntry) error {\n\tif err := entry.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getIPListsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif e := bucket.Get([]byte(entry.getKey())); e != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: entry %q already exists\", ErrDuplicatedKey, entry.IPOrNet),\n\t\t\t\tutil.I18nErrorDuplicatedIPNet,\n\t\t\t)\n\t\t}\n\t\tentry.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tentry.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(entry)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(entry.getKey()), buf)\n\t})\n}\n\nfunc (p *BoltProvider) updateIPListEntry(entry *IPListEntry) error {\n\tif err := entry.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getIPListsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar e []byte\n\t\tif e = bucket.Get([]byte(entry.getKey())); e == nil {\n\t\t\treturn fmt.Errorf(\"entry %q does not exist\", entry.IPOrNet)\n\t\t}\n\t\tvar oldEntry IPListEntry\n\t\terr = json.Unmarshal(e, &oldEntry)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tentry.CreatedAt = oldEntry.CreatedAt\n\t\tentry.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(entry)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(entry.getKey()), buf)\n\t})\n}\n\nfunc (p *BoltProvider) deleteIPListEntry(entry IPListEntry, _ bool) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getIPListsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif e := bucket.Get([]byte(entry.getKey())); e == nil {\n\t\t\treturn fmt.Errorf(\"entry %q does not exist\", entry.IPOrNet)\n\t\t}\n\t\treturn bucket.Delete([]byte(entry.getKey()))\n\t})\n}\n\nfunc (p *BoltProvider) getIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {\n\tentries := make([]IPListEntry, 0, 15)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getIPListsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprefix := []byte(fmt.Sprintf(\"%d_\", listType))\n\t\tacceptKey := func(k []byte) bool {\n\t\t\treturn k != nil && bytes.HasPrefix(k, prefix)\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tif order == OrderASC {\n\t\t\tfor k, v := cursor.Seek(prefix); acceptKey(k); k, v = cursor.Next() {\n\t\t\t\tvar entry IPListEntry\n\t\t\t\terr = json.Unmarshal(v, &entry)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif entry.satisfySearchConstraints(filter, from, order) {\n\t\t\t\t\tentry.PrepareForRendering()\n\t\t\t\t\tentries = append(entries, entry)\n\t\t\t\t\tif limit > 0 && len(entries) >= limit {\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} else {\n\t\t\tfor k, v := cursor.Last(); acceptKey(k); k, v = cursor.Prev() {\n\t\t\t\tvar entry IPListEntry\n\t\t\t\terr = json.Unmarshal(v, &entry)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif entry.satisfySearchConstraints(filter, from, order) {\n\t\t\t\t\tentry.PrepareForRendering()\n\t\t\t\t\tentries = append(entries, entry)\n\t\t\t\t\tif limit > 0 && len(entries) >= limit {\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\t\treturn nil\n\t})\n\treturn entries, err\n}\n\nfunc (p *BoltProvider) getRecentlyUpdatedIPListEntries(_ int64) ([]IPListEntry, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (p *BoltProvider) dumpIPListEntries() ([]IPListEntry, error) {\n\tentries := make([]IPListEntry, 0, 10)\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getIPListsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif count := bucket.Stats().KeyN; count > ipListMemoryLimit {\n\t\t\tproviderLog(logger.LevelInfo, \"IP lists excluded from dump, too many entries: %d\", count)\n\t\t\treturn nil\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\t\tvar entry IPListEntry\n\t\t\terr = json.Unmarshal(v, &entry)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tentry.PrepareForRendering()\n\t\t\tentries = append(entries, entry)\n\t\t}\n\t\treturn nil\n\t})\n\treturn entries, err\n}\n\nfunc (p *BoltProvider) countIPListEntries(listType IPListType) (int64, error) {\n\tvar count int64\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getIPListsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif listType == 0 {\n\t\t\tcount = int64(bucket.Stats().KeyN)\n\t\t\treturn nil\n\t\t}\n\t\tprefix := []byte(fmt.Sprintf(\"%d_\", listType))\n\t\tcursor := bucket.Cursor()\n\t\tfor k, _ := cursor.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, _ = cursor.Next() {\n\t\t\tcount++\n\t\t}\n\t\treturn nil\n\t})\n\treturn count, err\n}\n\nfunc (p *BoltProvider) getListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error) {\n\tentries := make([]IPListEntry, 0, 3)\n\tipAddr, err := netip.ParseAddr(ip)\n\tif err != nil {\n\t\treturn entries, fmt.Errorf(\"invalid ip address %s\", ip)\n\t}\n\tvar netType int\n\tvar ipBytes []byte\n\tif ipAddr.Is4() || ipAddr.Is4In6() {\n\t\tnetType = ipTypeV4\n\t\tas4 := ipAddr.As4()\n\t\tipBytes = as4[:]\n\t} else {\n\t\tnetType = ipTypeV6\n\t\tas16 := ipAddr.As16()\n\t\tipBytes = as16[:]\n\t}\n\terr = p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getIPListsBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprefix := []byte(fmt.Sprintf(\"%d_\", listType))\n\t\tcursor := bucket.Cursor()\n\t\tfor k, v := cursor.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = cursor.Next() {\n\t\t\tvar entry IPListEntry\n\t\t\terr = json.Unmarshal(v, &entry)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif entry.IPType == netType && bytes.Compare(ipBytes, entry.First) >= 0 && bytes.Compare(ipBytes, entry.Last) <= 0 {\n\t\t\t\tentry.PrepareForRendering()\n\t\t\t\tentries = append(entries, entry)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\treturn entries, err\n}\n\nfunc (p *BoltProvider) getConfigs() (Configs, error) {\n\tvar configs Configs\n\terr := p.dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(configsBucket)\n\t\tif bucket == nil {\n\t\t\treturn fmt.Errorf(\"unable to find configs bucket\")\n\t\t}\n\t\tdata := bucket.Get(configsKey)\n\t\tif data != nil {\n\t\t\treturn json.Unmarshal(data, &configs)\n\t\t}\n\t\treturn nil\n\t})\n\treturn configs, err\n}\n\nfunc (p *BoltProvider) setConfigs(configs *Configs) error {\n\tif err := configs.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(configsBucket)\n\t\tif bucket == nil {\n\t\t\treturn fmt.Errorf(\"unable to find configs bucket\")\n\t\t}\n\t\tbuf, err := json.Marshal(configs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put(configsKey, buf)\n\t})\n}\n\nfunc (p *BoltProvider) setFirstDownloadTimestamp(username string) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist, unable to set download timestamp\",\n\t\t\t\tusername))\n\t\t}\n\t\tvar user User\n\t\terr = json.Unmarshal(u, &user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif user.FirstDownload > 0 {\n\t\t\treturn util.NewGenericError(fmt.Sprintf(\"first download already set to %v\",\n\t\t\t\tutil.GetTimeFromMsecSinceEpoch(user.FirstDownload)))\n\t\t}\n\t\tuser.FirstDownload = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(username), buf)\n\t})\n}\n\nfunc (p *BoltProvider) setFirstUploadTimestamp(username string) error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket, err := p.getUsersBucket(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar u []byte\n\t\tif u = bucket.Get([]byte(username)); u == nil {\n\t\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist, unable to set upload timestamp\",\n\t\t\t\tusername))\n\t\t}\n\t\tvar user User\n\t\tif err = json.Unmarshal(u, &user); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif user.FirstUpload > 0 {\n\t\t\treturn util.NewGenericError(fmt.Sprintf(\"first upload already set to %v\",\n\t\t\t\tutil.GetTimeFromMsecSinceEpoch(user.FirstUpload)))\n\t\t}\n\t\tuser.FirstUpload = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(username), buf)\n\t})\n}\n\nfunc (p *BoltProvider) close() error {\n\treturn p.dbHandle.Close()\n}\n\nfunc (p *BoltProvider) reloadConfig() error {\n\treturn nil\n}\n\n// initializeDatabase does nothing, no initilization is needed for bolt provider\nfunc (p *BoltProvider) initializeDatabase() error {\n\treturn ErrNoInitRequired\n}\n\nfunc (p *BoltProvider) migrateDatabase() error {\n\tdbVersion, err := getBoltDatabaseVersion(p.dbHandle)\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch version := dbVersion.Version; {\n\tcase version == boltDatabaseVersion:\n\t\tproviderLog(logger.LevelDebug, \"bolt database is up to date, current version: %d\", version)\n\t\treturn ErrNoInitRequired\n\tcase version < 33:\n\t\terr = errSchemaVersionTooOld(version)\n\t\tproviderLog(logger.LevelError, \"%v\", err)\n\t\tlogger.ErrorToConsole(\"%v\", err)\n\t\treturn err\n\tcase version == 33:\n\t\tlogger.InfoToConsole(\"updating database schema version: %d -> 34\", version)\n\t\tproviderLog(logger.LevelInfo, \"updating database schema version: %d -> 34\", version)\n\t\treturn updateBoltDatabaseVersion(p.dbHandle, 34)\n\n\tdefault:\n\t\tif version > boltDatabaseVersion {\n\t\t\tproviderLog(logger.LevelError, \"database schema version %d is newer than the supported one: %d\", version,\n\t\t\t\tboltDatabaseVersion)\n\t\t\tlogger.WarnToConsole(\"database schema version %d is newer than the supported one: %d\", version,\n\t\t\t\tboltDatabaseVersion)\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"database schema version not handled: %d\", version)\n\t}\n}\n\nfunc (p *BoltProvider) revertDatabase(targetVersion int) error {\n\tdbVersion, err := getBoltDatabaseVersion(p.dbHandle)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif dbVersion.Version == targetVersion {\n\t\treturn errors.New(\"current version match target version, nothing to do\")\n\t}\n\tswitch dbVersion.Version {\n\tcase 34:\n\t\tlogger.InfoToConsole(\"downgrading database schema version: %d -> 33\", dbVersion.Version)\n\t\tproviderLog(logger.LevelInfo, \"downgrading database schema version: %d -> 33\", dbVersion.Version)\n\t\treturn updateBoltDatabaseVersion(p.dbHandle, 33)\n\n\tdefault:\n\t\treturn fmt.Errorf(\"database schema version not handled: %v\", dbVersion.Version)\n\t}\n}\n\nfunc (p *BoltProvider) resetDatabase() error {\n\treturn p.dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tfor _, bucketName := range boltBuckets {\n\t\t\terr := tx.DeleteBucket(bucketName)\n\t\t\tif err != nil && !errors.Is(err, bolterrors.ErrBucketNotFound) {\n\t\t\t\treturn fmt.Errorf(\"unable to remove bucket %v: %w\", bucketName, err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (p *BoltProvider) joinRuleAndActions(r []byte, actionsBucket *bolt.Bucket) (EventRule, error) {\n\tvar rule EventRule\n\terr := json.Unmarshal(r, &rule)\n\tif err != nil {\n\t\treturn rule, err\n\t}\n\tvar actions []EventAction\n\tfor idx := range rule.Actions {\n\t\taction := &rule.Actions[idx]\n\t\tvar baseAction BaseEventAction\n\t\tk := actionsBucket.Get([]byte(action.Name))\n\t\tif k == nil {\n\t\t\tcontinue\n\t\t}\n\t\terr = json.Unmarshal(k, &baseAction)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tbaseAction.Options.SetEmptySecretsIfNil()\n\t\taction.BaseEventAction = baseAction\n\t\tactions = append(actions, *action)\n\t}\n\trule.Actions = actions\n\treturn rule, nil\n}\n\nfunc (p *BoltProvider) joinGroupAndFolders(g []byte, foldersBucket *bolt.Bucket) (Group, error) {\n\tvar group Group\n\terr := json.Unmarshal(g, &group)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tif len(group.VirtualFolders) > 0 {\n\t\tvar folders []vfs.VirtualFolder\n\t\tfor idx := range group.VirtualFolders {\n\t\t\tfolder := &group.VirtualFolders[idx]\n\t\t\tbaseFolder, err := p.folderExistsInternal(folder.Name, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfolder.BaseVirtualFolder = baseFolder\n\t\t\tfolders = append(folders, *folder)\n\t\t}\n\t\tgroup.VirtualFolders = folders\n\t}\n\tgroup.SetEmptySecretsIfNil()\n\treturn group, err\n}\n\nfunc (p *BoltProvider) joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) {\n\tvar user User\n\terr := json.Unmarshal(u, &user)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tif len(user.VirtualFolders) > 0 {\n\t\tvar folders []vfs.VirtualFolder\n\t\tfor idx := range user.VirtualFolders {\n\t\t\tfolder := &user.VirtualFolders[idx]\n\t\t\tbaseFolder, err := p.folderExistsInternal(folder.Name, foldersBucket)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfolder.BaseVirtualFolder = baseFolder\n\t\t\tfolders = append(folders, *folder)\n\t\t}\n\t\tuser.VirtualFolders = folders\n\t}\n\tuser.SetEmptySecretsIfNil()\n\treturn user, err\n}\n\nfunc (p *BoltProvider) groupExistsInternal(name string, bucket *bolt.Bucket) (Group, error) {\n\tvar group Group\n\tg := bucket.Get([]byte(name))\n\tif g == nil {\n\t\terr := util.NewRecordNotFoundError(fmt.Sprintf(\"group %q does not exist\", name))\n\t\treturn group, err\n\t}\n\terr := json.Unmarshal(g, &group)\n\treturn group, err\n}\n\nfunc (p *BoltProvider) folderExistsInternal(name string, bucket *bolt.Bucket) (vfs.BaseVirtualFolder, error) {\n\tvar folder vfs.BaseVirtualFolder\n\tf := bucket.Get([]byte(name))\n\tif f == nil {\n\t\terr := util.NewRecordNotFoundError(fmt.Sprintf(\"folder %q does not exist\", name))\n\t\treturn folder, err\n\t}\n\terr := json.Unmarshal(f, &folder)\n\treturn folder, err\n}\n\nfunc (p *BoltProvider) addFolderInternal(folder vfs.BaseVirtualFolder, bucket *bolt.Bucket) error {\n\tid, err := bucket.NextSequence()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfolder.ID = int64(id)\n\tbuf, err := json.Marshal(folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn bucket.Put([]byte(folder.Name), buf)\n}\n\nfunc (p *BoltProvider) removeRoleFromUser(username, role string, bucket *bolt.Bucket) error {\n\tu := bucket.Get([]byte(username))\n\tif u == nil {\n\t\tproviderLog(logger.LevelWarn, \"user %q does not exist, cannot remove role %q\", username, role)\n\t\treturn nil\n\t}\n\tvar user User\n\terr := json.Unmarshal(u, &user)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif user.Role == role {\n\t\tuser.Role = \"\"\n\t\tbuf, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(user.Username), buf)\n\t}\n\tproviderLog(logger.LevelError, \"user %q does not have the expected role %q, actual %q\", username, role, user.Role)\n\treturn nil\n}\n\nfunc (p *BoltProvider) addAdminToRole(username, roleName string, bucket *bolt.Bucket) error {\n\tif roleName == \"\" {\n\t\treturn nil\n\t}\n\tr := bucket.Get([]byte(roleName))\n\tif r == nil {\n\t\treturn fmt.Errorf(\"%w: role %q does not exist\", ErrForeignKeyViolated, roleName)\n\t}\n\tvar role Role\n\terr := json.Unmarshal(r, &role)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !slices.Contains(role.Admins, username) {\n\t\trole.Admins = append(role.Admins, username)\n\t\tbuf, err := json.Marshal(role)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(role.Name), buf)\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) removeAdminFromRole(username, roleName string, bucket *bolt.Bucket) error {\n\tif roleName == \"\" {\n\t\treturn nil\n\t}\n\tr := bucket.Get([]byte(roleName))\n\tif r == nil {\n\t\tproviderLog(logger.LevelWarn, \"role %q does not exist, cannot remove admin %q\", roleName, username)\n\t\treturn nil\n\t}\n\tvar role Role\n\terr := json.Unmarshal(r, &role)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif slices.Contains(role.Admins, username) {\n\t\tvar admins []string\n\t\tfor _, admin := range role.Admins {\n\t\t\tif admin != username {\n\t\t\t\tadmins = append(admins, admin)\n\t\t\t}\n\t\t}\n\t\trole.Admins = util.RemoveDuplicates(admins, false)\n\t\tbuf, err := json.Marshal(role)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(role.Name), buf)\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) addUserToRole(username, roleName string, bucket *bolt.Bucket) error {\n\tif roleName == \"\" {\n\t\treturn nil\n\t}\n\tr := bucket.Get([]byte(roleName))\n\tif r == nil {\n\t\treturn fmt.Errorf(\"%w: role %q does not exist\", ErrForeignKeyViolated, roleName)\n\t}\n\tvar role Role\n\terr := json.Unmarshal(r, &role)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !slices.Contains(role.Users, username) {\n\t\trole.Users = append(role.Users, username)\n\t\tbuf, err := json.Marshal(role)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(role.Name), buf)\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) removeUserFromRole(username, roleName string, bucket *bolt.Bucket) error {\n\tif roleName == \"\" {\n\t\treturn nil\n\t}\n\tr := bucket.Get([]byte(roleName))\n\tif r == nil {\n\t\tproviderLog(logger.LevelWarn, \"role %q does not exist, cannot remove admin %q\", roleName, username)\n\t\treturn nil\n\t}\n\tvar role Role\n\terr := json.Unmarshal(r, &role)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif slices.Contains(role.Users, username) {\n\t\tvar users []string\n\t\tfor _, user := range role.Users {\n\t\t\tif user != username {\n\t\t\t\tusers = append(users, user)\n\t\t\t}\n\t\t}\n\t\tusers = util.RemoveDuplicates(users, false)\n\t\trole.Users = users\n\t\tbuf, err := json.Marshal(role)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(role.Name), buf)\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucket *bolt.Bucket) error {\n\ta := bucket.Get([]byte(actionName))\n\tif a == nil {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"action %q does not exist\", actionName))\n\t}\n\tvar action BaseEventAction\n\terr := json.Unmarshal(a, &action)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !slices.Contains(action.Rules, ruleName) {\n\t\taction.Rules = append(action.Rules, ruleName)\n\t\tbuf, err := json.Marshal(action)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(action.Name), buf)\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) removeRuleFromActionMapping(ruleName, actionName string, bucket *bolt.Bucket) error {\n\ta := bucket.Get([]byte(actionName))\n\tif a == nil {\n\t\tproviderLog(logger.LevelWarn, \"action %q does not exist, cannot remove from mapping\", actionName)\n\t\treturn nil\n\t}\n\tvar action BaseEventAction\n\terr := json.Unmarshal(a, &action)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif slices.Contains(action.Rules, ruleName) {\n\t\tvar rules []string\n\t\tfor _, r := range action.Rules {\n\t\t\tif r != ruleName {\n\t\t\t\trules = append(rules, r)\n\t\t\t}\n\t\t}\n\t\taction.Rules = util.RemoveDuplicates(rules, false)\n\t\tbuf, err := json.Marshal(action)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(action.Name), buf)\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket *bolt.Bucket) error {\n\tg := bucket.Get([]byte(groupname))\n\tif g == nil {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"group %q does not exist\", groupname))\n\t}\n\tvar group Group\n\terr := json.Unmarshal(g, &group)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !slices.Contains(group.Users, username) {\n\t\tgroup.Users = append(group.Users, username)\n\t\tbuf, err := json.Marshal(group)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(group.Name), buf)\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) removeUserFromGroupMapping(username, groupname string, bucket *bolt.Bucket) error {\n\tg := bucket.Get([]byte(groupname))\n\tif g == nil {\n\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"group %q does not exist\", groupname))\n\t}\n\tvar group Group\n\terr := json.Unmarshal(g, &group)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar users []string\n\tfor _, u := range group.Users {\n\t\tif u != username {\n\t\t\tusers = append(users, u)\n\t\t}\n\t}\n\tgroup.Users = util.RemoveDuplicates(users, false)\n\tbuf, err := json.Marshal(group)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn bucket.Put([]byte(group.Name), buf)\n}\n\nfunc (p *BoltProvider) addAdminToGroupMapping(username, groupname string, bucket *bolt.Bucket) error {\n\tg := bucket.Get([]byte(groupname))\n\tif g == nil {\n\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"group %q does not exist\", groupname))\n\t}\n\tvar group Group\n\terr := json.Unmarshal(g, &group)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !slices.Contains(group.Admins, username) {\n\t\tgroup.Admins = append(group.Admins, username)\n\t\tbuf, err := json.Marshal(group)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put([]byte(group.Name), buf)\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) removeAdminFromGroupMapping(username, groupname string, bucket *bolt.Bucket) error {\n\tg := bucket.Get([]byte(groupname))\n\tif g == nil {\n\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"group %q does not exist\", groupname))\n\t}\n\tvar group Group\n\terr := json.Unmarshal(g, &group)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar admins []string\n\tfor _, a := range group.Admins {\n\t\tif a != username {\n\t\t\tadmins = append(admins, a)\n\t\t}\n\t}\n\tgroup.Admins = util.RemoveDuplicates(admins, false)\n\tbuf, err := json.Marshal(group)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn bucket.Put([]byte(group.Name), buf)\n}\n\nfunc (p *BoltProvider) removeGroupFromAdminMapping(groupName, adminName string, bucket *bolt.Bucket) error {\n\tvar a []byte\n\tif a = bucket.Get([]byte(adminName)); a == nil {\n\t\t// the admin does not exist so there is no associated group\n\t\treturn nil\n\t}\n\tvar admin Admin\n\terr := json.Unmarshal(a, &admin)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar newGroups []AdminGroupMapping\n\tfor _, g := range admin.Groups {\n\t\tif g.Name != groupName {\n\t\t\tnewGroups = append(newGroups, g)\n\t\t}\n\t}\n\tadmin.Groups = newGroups\n\tbuf, err := json.Marshal(admin)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn bucket.Put([]byte(adminName), buf)\n}\n\nfunc (p *BoltProvider) addRelationToFolderMapping(folderName string, user *User, group *Group, bucket *bolt.Bucket) error {\n\tf := bucket.Get([]byte(folderName))\n\tif f == nil {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"folder %q does not exist\", folderName))\n\t}\n\tvar folder vfs.BaseVirtualFolder\n\terr := json.Unmarshal(f, &folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\tupdated := false\n\tif user != nil && !slices.Contains(folder.Users, user.Username) {\n\t\tfolder.Users = append(folder.Users, user.Username)\n\t\tupdated = true\n\t}\n\tif group != nil && !slices.Contains(folder.Groups, group.Name) {\n\t\tfolder.Groups = append(folder.Groups, group.Name)\n\t\tupdated = true\n\t}\n\tif !updated {\n\t\treturn nil\n\t}\n\tbuf, err := json.Marshal(folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn bucket.Put([]byte(folder.Name), buf)\n}\n\nfunc (p *BoltProvider) removeRelationFromFolderMapping(folder vfs.VirtualFolder, username, groupname string,\n\tbucket *bolt.Bucket,\n) error {\n\tvar f []byte\n\tif f = bucket.Get([]byte(folder.Name)); f == nil {\n\t\t// the folder does not exist so there is no associated user/group\n\t\treturn nil\n\t}\n\tvar baseFolder vfs.BaseVirtualFolder\n\terr := json.Unmarshal(f, &baseFolder)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfound := false\n\tif username != \"\" {\n\t\tfound = true\n\t\tvar newUserMapping []string\n\t\tfor _, u := range baseFolder.Users {\n\t\t\tif u != username {\n\t\t\t\tnewUserMapping = append(newUserMapping, u)\n\t\t\t}\n\t\t}\n\t\tbaseFolder.Users = newUserMapping\n\t}\n\tif groupname != \"\" {\n\t\tfound = true\n\t\tvar newGroupMapping []string\n\t\tfor _, g := range baseFolder.Groups {\n\t\t\tif g != groupname {\n\t\t\t\tnewGroupMapping = append(newGroupMapping, g)\n\t\t\t}\n\t\t}\n\t\tbaseFolder.Groups = newGroupMapping\n\t}\n\tif !found {\n\t\treturn nil\n\t}\n\tbuf, err := json.Marshal(baseFolder)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn bucket.Put([]byte(folder.Name), buf)\n}\n\nfunc (p *BoltProvider) updateUserRelations(tx *bolt.Tx, user *User, oldUser User) error {\n\tfoldersBucket, err := p.getFoldersBucket(tx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgroupsBucket, err := p.getGroupsBucket(tx)\n\tif err != nil {\n\t\treturn err\n\t}\n\trolesBucket, err := p.getRolesBucket(tx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor idx := range oldUser.VirtualFolders {\n\t\terr = p.removeRelationFromFolderMapping(oldUser.VirtualFolders[idx], oldUser.Username, \"\", foldersBucket)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor idx := range oldUser.Groups {\n\t\terr = p.removeUserFromGroupMapping(user.Username, oldUser.Groups[idx].Name, groupsBucket)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err = p.removeUserFromRole(oldUser.Username, oldUser.Role, rolesBucket); err != nil {\n\t\treturn err\n\t}\n\tfor idx := range user.VirtualFolders {\n\t\terr = p.addRelationToFolderMapping(user.VirtualFolders[idx].Name, user, nil, foldersBucket)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor idx := range user.Groups {\n\t\terr = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name, groupsBucket)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn p.addUserToRole(user.Username, user.Role, rolesBucket)\n}\n\nfunc (p *BoltProvider) adminExistsInternal(tx *bolt.Tx, username string) error {\n\tbucket, err := p.getAdminsBucket(tx)\n\tif err != nil {\n\t\treturn err\n\t}\n\ta := bucket.Get([]byte(username))\n\tif a == nil {\n\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"admin %v does not exist\", username))\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) userExistsInternal(tx *bolt.Tx, username string) error {\n\tbucket, err := p.getUsersBucket(tx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tu := bucket.Get([]byte(username))\n\tif u == nil {\n\t\treturn util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n\t}\n\treturn nil\n}\n\nfunc (p *BoltProvider) deleteRelatedShares(tx *bolt.Tx, username string) error {\n\tbucket, err := p.getSharesBucket(tx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar toRemove []string\n\tcursor := bucket.Cursor()\n\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\tvar share Share\n\t\terr = json.Unmarshal(v, &share)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif share.Username == username {\n\t\t\ttoRemove = append(toRemove, share.ShareID)\n\t\t}\n\t}\n\n\tfor _, k := range toRemove {\n\t\tif err := bucket.Delete([]byte(k)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (p *BoltProvider) deleteRelatedAPIKey(tx *bolt.Tx, username string, scope APIKeyScope) error {\n\tbucket, err := p.getAPIKeysBucket(tx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar toRemove []string\n\tcursor := bucket.Cursor()\n\tfor k, v := cursor.First(); k != nil; k, v = cursor.Next() {\n\t\tvar apiKey APIKey\n\t\terr = json.Unmarshal(v, &apiKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif scope == APIKeyScopeUser {\n\t\t\tif apiKey.User == username {\n\t\t\t\ttoRemove = append(toRemove, apiKey.KeyID)\n\t\t\t}\n\t\t} else {\n\t\t\tif apiKey.Admin == username {\n\t\t\t\ttoRemove = append(toRemove, apiKey.KeyID)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, k := range toRemove {\n\t\tif err := bucket.Delete([]byte(k)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (p *BoltProvider) getSharesBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\n\tbucket := tx.Bucket(sharesBucket)\n\tif bucket == nil {\n\t\terr = errors.New(\"unable to find shares bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getAPIKeysBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\n\tbucket := tx.Bucket(apiKeysBucket)\n\tif bucket == nil {\n\t\terr = errors.New(\"unable to find api keys bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getAdminsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\n\tbucket := tx.Bucket(adminsBucket)\n\tif bucket == nil {\n\t\terr = errors.New(\"unable to find admins bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getUsersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\tbucket := tx.Bucket(usersBucket)\n\tif bucket == nil {\n\t\terr = errors.New(\"unable to find users bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getGroupsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\tbucket := tx.Bucket(groupsBucket)\n\tif bucket == nil {\n\t\terr = fmt.Errorf(\"unable to find groups bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getRolesBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\tbucket := tx.Bucket(rolesBucket)\n\tif bucket == nil {\n\t\terr = fmt.Errorf(\"unable to find roles bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getIPListsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\tbucket := tx.Bucket(rolesBucket)\n\tif bucket == nil {\n\t\terr = fmt.Errorf(\"unable to find IP lists bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getFoldersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\tbucket := tx.Bucket(foldersBucket)\n\tif bucket == nil {\n\t\terr = fmt.Errorf(\"unable to find folders bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getActionsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\tbucket := tx.Bucket(actionsBucket)\n\tif bucket == nil {\n\t\terr = fmt.Errorf(\"unable to find event actions bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc (p *BoltProvider) getRulesBucket(tx *bolt.Tx) (*bolt.Bucket, error) {\n\tvar err error\n\tbucket := tx.Bucket(rulesBucket)\n\tif bucket == nil {\n\t\terr = fmt.Errorf(\"unable to find event rules bucket, bolt database structure not correcly defined\")\n\t}\n\treturn bucket, err\n}\n\nfunc getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {\n\tvar dbVersion schemaVersion\n\terr := dbHandle.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(dbVersionBucket)\n\t\tif bucket == nil {\n\t\t\treturn fmt.Errorf(\"unable to find database schema version bucket\")\n\t\t}\n\t\tv := bucket.Get(dbVersionKey)\n\t\tif v == nil {\n\t\t\tdbVersion = schemaVersion{\n\t\t\t\tVersion: 33,\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\treturn json.Unmarshal(v, &dbVersion)\n\t})\n\treturn dbVersion, err\n}\n\nfunc updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {\n\terr := dbHandle.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(dbVersionBucket)\n\t\tif bucket == nil {\n\t\t\treturn fmt.Errorf(\"unable to find database schema version bucket\")\n\t\t}\n\t\tnewDbVersion := schemaVersion{\n\t\t\tVersion: version,\n\t\t}\n\t\tbuf, err := json.Marshal(newDbVersion)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn bucket.Put(dbVersionKey, buf)\n\t})\n\treturn err\n}\n"
  },
  {
    "path": "internal/dataprovider/bolt_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build nobolt\n\npackage dataprovider\n\nimport (\n\t\"errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-bolt\")\n}\n\nfunc initializeBoltProvider(_ string) error {\n\treturn errors.New(\"bolt disabled at build time\")\n}\n"
  },
  {
    "path": "internal/dataprovider/cachedpassword.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"sort\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tcachedUserPasswords  credentialsCache\n\tcachedAdminPasswords credentialsCache\n\tcachedAPIKeys        credentialsCache\n)\n\nfunc init() {\n\tcachedUserPasswords = credentialsCache{\n\t\tname:      \"users\",\n\t\tsizeLimit: 500,\n\t\tcache:     make(map[string]credentialObject),\n\t}\n\tcachedAdminPasswords = credentialsCache{\n\t\tname:      \"admins\",\n\t\tsizeLimit: 100,\n\t\tcache:     make(map[string]credentialObject),\n\t}\n\tcachedAPIKeys = credentialsCache{\n\t\tname:      \"API keys\",\n\t\tsizeLimit: 500,\n\t\tcache:     make(map[string]credentialObject),\n\t}\n}\n\n// CheckCachedUserPassword is an utility method used only in test cases\nfunc CheckCachedUserPassword(username, password, hash string) (bool, bool) {\n\treturn cachedUserPasswords.Check(username, password, hash)\n}\n\ntype credentialObject struct {\n\tkey      string\n\thash     string\n\tpassword string\n\tusedAt   *atomic.Int64\n}\n\ntype credentialsCache struct {\n\tname      string\n\tsizeLimit int\n\tsync.RWMutex\n\tcache map[string]credentialObject\n}\n\nfunc (c *credentialsCache) Add(username, password, hash string) {\n\tif !config.PasswordCaching || username == \"\" || password == \"\" || hash == \"\" {\n\t\treturn\n\t}\n\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tobj := credentialObject{\n\t\tkey:      username,\n\t\thash:     hash,\n\t\tpassword: password,\n\t\tusedAt:   &atomic.Int64{},\n\t}\n\tobj.usedAt.Store(util.GetTimeAsMsSinceEpoch(time.Now()))\n\n\tc.cache[username] = obj\n}\n\nfunc (c *credentialsCache) Remove(username string) {\n\tif !config.PasswordCaching {\n\t\treturn\n\t}\n\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tdelete(c.cache, username)\n}\n\n// Check returns if the username is found and if the password match\nfunc (c *credentialsCache) Check(username, password, hash string) (bool, bool) {\n\tif username == \"\" || password == \"\" || hash == \"\" {\n\t\treturn false, false\n\t}\n\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tcreds, ok := c.cache[username]\n\tif !ok {\n\t\treturn false, false\n\t}\n\tif creds.hash != hash {\n\t\tcreds.usedAt.Store(0)\n\t\treturn false, false\n\t}\n\tmatch := creds.password == password\n\tif match {\n\t\tcreds.usedAt.Store(util.GetTimeAsMsSinceEpoch(time.Now()))\n\t}\n\treturn true, match\n}\n\nfunc (c *credentialsCache) count() int {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\treturn len(c.cache)\n}\n\nfunc (c *credentialsCache) cleanup() {\n\tif !config.PasswordCaching {\n\t\treturn\n\t}\n\tif c.count() <= c.sizeLimit {\n\t\treturn\n\t}\n\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tfor k, v := range c.cache {\n\t\tif v.usedAt.Load() < util.GetTimeAsMsSinceEpoch(time.Now().Add(-60*time.Minute)) {\n\t\t\tdelete(c.cache, k)\n\t\t}\n\t}\n\tproviderLog(logger.LevelDebug, \"size for credentials %q after cleanup: %d\", c.name, len(c.cache))\n\n\tif len(c.cache) < c.sizeLimit*5 {\n\t\treturn\n\t}\n\tnumToRemove := len(c.cache) - c.sizeLimit\n\tproviderLog(logger.LevelDebug, \"additional item to remove from credentials %q: %d\", c.name, numToRemove)\n\tcredentials := make([]credentialObject, 0, len(c.cache))\n\tfor _, v := range c.cache {\n\t\tcredentials = append(credentials, v)\n\t}\n\tsort.Slice(credentials, func(i, j int) bool {\n\t\treturn credentials[i].usedAt.Load() < credentials[j].usedAt.Load()\n\t})\n\n\tfor idx := range credentials {\n\t\tif idx >= numToRemove {\n\t\t\tbreak\n\t\t}\n\t\tdelete(c.cache, credentials[idx].key)\n\t}\n\tproviderLog(logger.LevelDebug, \"size for credentials %q after additional cleanup: %d\", c.name, len(c.cache))\n}\n"
  },
  {
    "path": "internal/dataprovider/cacheduser.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/webdav\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\twebDAVUsersCache *usersCache\n)\n\nfunc init() {\n\twebDAVUsersCache = &usersCache{\n\t\tusers: map[string]CachedUser{},\n\t}\n}\n\n// InitializeWebDAVUserCache initializes the cache for webdav users\nfunc InitializeWebDAVUserCache(maxSize int) {\n\twebDAVUsersCache = &usersCache{\n\t\tusers:   map[string]CachedUser{},\n\t\tmaxSize: maxSize,\n\t}\n}\n\n// CachedUser adds fields useful for caching to a SFTPGo user\ntype CachedUser struct {\n\tUser       User\n\tExpiration time.Time\n\tPassword   string\n\tLockSystem webdav.LockSystem\n}\n\n// IsExpired returns true if the cached user is expired\nfunc (c *CachedUser) IsExpired() bool {\n\tif c.Expiration.IsZero() {\n\t\treturn false\n\t}\n\treturn c.Expiration.Before(time.Now())\n}\n\ntype usersCache struct {\n\tsync.RWMutex\n\tusers   map[string]CachedUser\n\tmaxSize int\n}\n\nfunc (cache *usersCache) updateLastLogin(username string) {\n\tcache.Lock()\n\tdefer cache.Unlock()\n\n\tif cachedUser, ok := cache.users[username]; ok {\n\t\tcachedUser.User.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tcache.users[username] = cachedUser\n\t}\n}\n\n// swapWebDAVUser updates an existing cached user with the specified one\n// preserving the lock fs if possible\n// FIXME: this could be racy in rare cases\nfunc (cache *usersCache) swap(userRef *User, plainPassword string) {\n\tuser := userRef.getACopy()\n\terr := user.LoadAndApplyGroupSettings()\n\n\tcache.Lock()\n\tdefer cache.Unlock()\n\n\tif cachedUser, ok := cache.users[user.Username]; ok {\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelDebug, \"unable to load group settings, for user %q, removing from cache, err :%v\",\n\t\t\t\tuser.Username, err)\n\t\t\tdelete(cache.users, user.Username)\n\t\t\treturn\n\t\t}\n\t\tif plainPassword != \"\" {\n\t\t\tcachedUser.Password = plainPassword\n\t\t} else {\n\t\t\tif cachedUser.User.Password != user.Password {\n\t\t\t\tproviderLog(logger.LevelDebug, \"current password different from the cached one for user %q, removing from cache\",\n\t\t\t\t\tuser.Username)\n\t\t\t\t// the password changed, the cached user is no longer valid\n\t\t\t\tdelete(cache.users, user.Username)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif cachedUser.User.isFsEqual(&user) {\n\t\t\t// the updated user has the same fs as the cached one, we can preserve the lock filesystem\n\t\t\tproviderLog(logger.LevelDebug, \"current password and fs unchanged for for user %q, swap cached one\",\n\t\t\t\tuser.Username)\n\t\t\tcachedUser.User = user\n\t\t\tcache.users[user.Username] = cachedUser\n\t\t} else {\n\t\t\t// filesystem changed, the cached user is no longer valid\n\t\t\tproviderLog(logger.LevelDebug, \"current fs different from the cached one for user %q, removing from cache\",\n\t\t\t\tuser.Username)\n\t\t\tdelete(cache.users, user.Username)\n\t\t}\n\t}\n}\n\nfunc (cache *usersCache) add(cachedUser *CachedUser) {\n\tcache.Lock()\n\tdefer cache.Unlock()\n\n\tif cache.maxSize > 0 && len(cache.users) >= cache.maxSize {\n\t\tvar userToRemove string\n\t\tvar expirationTime time.Time\n\n\t\tfor k, v := range cache.users {\n\t\t\tif userToRemove == \"\" {\n\t\t\t\tuserToRemove = k\n\t\t\t\texpirationTime = v.Expiration\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\texpireTime := v.Expiration\n\t\t\tif !expireTime.IsZero() && expireTime.Before(expirationTime) {\n\t\t\t\tuserToRemove = k\n\t\t\t\texpirationTime = expireTime\n\t\t\t}\n\t\t}\n\n\t\tdelete(cache.users, userToRemove)\n\t}\n\n\tif cachedUser.User.Username != \"\" {\n\t\tcache.users[cachedUser.User.Username] = *cachedUser\n\t}\n}\n\nfunc (cache *usersCache) remove(username string) {\n\tcache.Lock()\n\tdefer cache.Unlock()\n\n\tdelete(cache.users, username)\n}\n\nfunc (cache *usersCache) get(username string) (*CachedUser, bool) {\n\tcache.RLock()\n\tdefer cache.RUnlock()\n\n\tcachedUser, ok := cache.users[username]\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn &cachedUser, true\n}\n\n// CacheWebDAVUser add a user to the WebDAV cache\nfunc CacheWebDAVUser(cachedUser *CachedUser) {\n\twebDAVUsersCache.add(cachedUser)\n}\n\n// GetCachedWebDAVUser returns a previously cached WebDAV user\nfunc GetCachedWebDAVUser(username string) (*CachedUser, bool) {\n\treturn webDAVUsersCache.get(username)\n}\n\n// RemoveCachedWebDAVUser removes a cached WebDAV user\nfunc RemoveCachedWebDAVUser(username string) {\n\twebDAVUsersCache.remove(username)\n}\n"
  },
  {
    "path": "internal/dataprovider/configs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"image/png\"\n\t\"net/url\"\n\t\"slices\"\n\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// Supported values for host keys, KEXs, ciphers, MACs\nvar (\n\tsupportedHostKeyAlgos   = []string{ssh.KeyAlgoRSA}\n\tsupportedPublicKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.InsecureKeyAlgoDSA} //nolint:staticcheck\n\tsupportedKexAlgos       = []string{\n\t\tssh.KeyExchangeDH16SHA512, ssh.InsecureKeyExchangeDH14SHA1, ssh.InsecureKeyExchangeDH1SHA1,\n\t\tssh.InsecureKeyExchangeDHGEXSHA1,\n\t}\n\tsupportedCiphers = []string{\n\t\tssh.InsecureCipherAES128CBC,\n\t\tssh.InsecureCipherTripleDESCBC,\n\t}\n\tsupportedMACs = []string{\n\t\tssh.HMACSHA512ETM, ssh.HMACSHA512,\n\t\tssh.HMACSHA1, ssh.InsecureHMACSHA196,\n\t}\n)\n\n// SFTPDConfigs defines configurations for SFTPD\ntype SFTPDConfigs struct {\n\tHostKeyAlgos   []string `json:\"host_key_algos,omitempty\"`\n\tPublicKeyAlgos []string `json:\"public_key_algos,omitempty\"`\n\tKexAlgorithms  []string `json:\"kex_algorithms,omitempty\"`\n\tCiphers        []string `json:\"ciphers,omitempty\"`\n\tMACs           []string `json:\"macs,omitempty\"`\n}\n\nfunc (c *SFTPDConfigs) isEmpty() bool {\n\tif len(c.HostKeyAlgos) > 0 {\n\t\treturn false\n\t}\n\tif len(c.PublicKeyAlgos) > 0 {\n\t\treturn false\n\t}\n\tif len(c.KexAlgorithms) > 0 {\n\t\treturn false\n\t}\n\tif len(c.Ciphers) > 0 {\n\t\treturn false\n\t}\n\tif len(c.MACs) > 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// GetSupportedHostKeyAlgos returns the supported legacy host key algos\nfunc (*SFTPDConfigs) GetSupportedHostKeyAlgos() []string {\n\treturn supportedHostKeyAlgos\n}\n\n// GetSupportedPublicKeyAlgos returns the supported legacy public key algos\nfunc (*SFTPDConfigs) GetSupportedPublicKeyAlgos() []string {\n\treturn supportedPublicKeyAlgos\n}\n\n// GetSupportedKEXAlgos returns the supported KEX algos\nfunc (*SFTPDConfigs) GetSupportedKEXAlgos() []string {\n\treturn supportedKexAlgos\n}\n\n// GetSupportedCiphers returns the supported ciphers\nfunc (*SFTPDConfigs) GetSupportedCiphers() []string {\n\treturn supportedCiphers\n}\n\n// GetSupportedMACs returns the supported MACs algos\nfunc (*SFTPDConfigs) GetSupportedMACs() []string {\n\treturn supportedMACs\n}\n\nfunc (c *SFTPDConfigs) validate() error {\n\tvar hostKeyAlgos []string\n\tfor _, algo := range c.HostKeyAlgos {\n\t\tif algo == ssh.CertAlgoRSAv01 {\n\t\t\tcontinue\n\t\t}\n\t\tif !slices.Contains(supportedHostKeyAlgos, algo) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported host key algorithm %q\", algo))\n\t\t}\n\t\thostKeyAlgos = append(hostKeyAlgos, algo)\n\t}\n\tc.HostKeyAlgos = hostKeyAlgos\n\tvar kexAlgos []string\n\tfor _, algo := range c.KexAlgorithms {\n\t\tif algo == \"diffie-hellman-group18-sha512\" || algo == ssh.KeyExchangeDHGEXSHA256 {\n\t\t\tcontinue\n\t\t}\n\t\tif !slices.Contains(supportedKexAlgos, algo) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported KEX algorithm %q\", algo))\n\t\t}\n\t\tkexAlgos = append(kexAlgos, algo)\n\t}\n\tc.KexAlgorithms = kexAlgos\n\tfor _, cipher := range c.Ciphers {\n\t\tif slices.Contains([]string{\"aes192-cbc\", \"aes256-cbc\"}, cipher) {\n\t\t\tcontinue\n\t\t}\n\t\tif !slices.Contains(supportedCiphers, cipher) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported cipher %q\", cipher))\n\t\t}\n\t}\n\tfor _, mac := range c.MACs {\n\t\tif !slices.Contains(supportedMACs, mac) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported MAC algorithm %q\", mac))\n\t\t}\n\t}\n\tfor _, algo := range c.PublicKeyAlgos {\n\t\tif !slices.Contains(supportedPublicKeyAlgos, algo) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported public key algorithm %q\", algo))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *SFTPDConfigs) getACopy() *SFTPDConfigs {\n\thostKeys := make([]string, len(c.HostKeyAlgos))\n\tcopy(hostKeys, c.HostKeyAlgos)\n\tpublicKeys := make([]string, len(c.PublicKeyAlgos))\n\tcopy(publicKeys, c.PublicKeyAlgos)\n\tkexs := make([]string, len(c.KexAlgorithms))\n\tcopy(kexs, c.KexAlgorithms)\n\tciphers := make([]string, len(c.Ciphers))\n\tcopy(ciphers, c.Ciphers)\n\tmacs := make([]string, len(c.MACs))\n\tcopy(macs, c.MACs)\n\n\treturn &SFTPDConfigs{\n\t\tHostKeyAlgos:   hostKeys,\n\t\tPublicKeyAlgos: publicKeys,\n\t\tKexAlgorithms:  kexs,\n\t\tCiphers:        ciphers,\n\t\tMACs:           macs,\n\t}\n}\n\nfunc validateSMTPSecret(secret *kms.Secret, name string) error {\n\tif secret.IsRedacted() {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"cannot save a redacted smtp %s\", name))\n\t}\n\tif secret.IsEncrypted() && !secret.IsValid() {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid encrypted smtp %s\", name))\n\t}\n\tif !secret.IsEmpty() && !secret.IsValidInput() {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid smtp %s\", name))\n\t}\n\tif secret.IsPlain() {\n\t\tsecret.SetAdditionalData(\"smtp\")\n\t\tif err := secret.Encrypt(); err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"could not encrypt smtp %s: %v\", name, err))\n\t\t}\n\t}\n\treturn nil\n}\n\n// SMTPOAuth2 defines the SMTP related OAuth2 configurations\ntype SMTPOAuth2 struct {\n\tProvider     int         `json:\"provider,omitempty\"`\n\tTenant       string      `json:\"tenant,omitempty\"`\n\tClientID     string      `json:\"client_id,omitempty\"`\n\tClientSecret *kms.Secret `json:\"client_secret,omitempty\"`\n\tRefreshToken *kms.Secret `json:\"refresh_token,omitempty\"`\n}\n\nfunc (c *SMTPOAuth2) validate() error {\n\tif c.Provider < 0 || c.Provider > 1 {\n\t\treturn util.NewValidationError(\"smtp oauth2: unsupported provider\")\n\t}\n\tif c.ClientID == \"\" {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"smtp oauth2: client id is required\"),\n\t\t\tutil.I18nErrorClientIDRequired,\n\t\t)\n\t}\n\tif c.ClientSecret == nil || c.ClientSecret.IsEmpty() {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"smtp oauth2: client secret is required\"),\n\t\t\tutil.I18nErrorClientSecretRequired,\n\t\t)\n\t}\n\tif c.RefreshToken == nil || c.RefreshToken.IsEmpty() {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"smtp oauth2: refresh token is required\"),\n\t\t\tutil.I18nErrorRefreshTokenRequired,\n\t\t)\n\t}\n\tif err := validateSMTPSecret(c.ClientSecret, \"oauth2 client secret\"); err != nil {\n\t\treturn err\n\t}\n\treturn validateSMTPSecret(c.RefreshToken, \"oauth2 refresh token\")\n}\n\nfunc (c *SMTPOAuth2) getACopy() SMTPOAuth2 {\n\tvar clientSecret, refreshToken *kms.Secret\n\tif c.ClientSecret != nil {\n\t\tclientSecret = c.ClientSecret.Clone()\n\t}\n\tif c.RefreshToken != nil {\n\t\trefreshToken = c.RefreshToken.Clone()\n\t}\n\treturn SMTPOAuth2{\n\t\tProvider:     c.Provider,\n\t\tTenant:       c.Tenant,\n\t\tClientID:     c.ClientID,\n\t\tClientSecret: clientSecret,\n\t\tRefreshToken: refreshToken,\n\t}\n}\n\n// SMTPConfigs defines configuration for SMTP\ntype SMTPConfigs struct {\n\tHost       string      `json:\"host,omitempty\"`\n\tPort       int         `json:\"port,omitempty\"`\n\tFrom       string      `json:\"from,omitempty\"`\n\tUser       string      `json:\"user,omitempty\"`\n\tPassword   *kms.Secret `json:\"password,omitempty\"`\n\tAuthType   int         `json:\"auth_type,omitempty\"`\n\tEncryption int         `json:\"encryption,omitempty\"`\n\tDomain     string      `json:\"domain,omitempty\"`\n\tDebug      int         `json:\"debug,omitempty\"`\n\tOAuth2     SMTPOAuth2  `json:\"oauth2\"`\n}\n\n// IsEmpty returns true if the configuration is empty\nfunc (c *SMTPConfigs) IsEmpty() bool {\n\treturn c.Host == \"\"\n}\n\nfunc (c *SMTPConfigs) validate() error {\n\tif c.IsEmpty() {\n\t\treturn nil\n\t}\n\tif c.Port <= 0 || c.Port > 65535 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"smtp: invalid port %d\", c.Port))\n\t}\n\tif c.Password != nil && c.AuthType != 3 {\n\t\tif err := validateSMTPSecret(c.Password, \"password\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif c.User == \"\" && c.From == \"\" {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"smtp: from address and user cannot both be empty\"),\n\t\t\tutil.I18nErrorSMTPRequiredFields,\n\t\t)\n\t}\n\tif c.AuthType < 0 || c.AuthType > 3 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"smtp: invalid auth type %d\", c.AuthType))\n\t}\n\tif c.Encryption < 0 || c.Encryption > 2 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"smtp: invalid encryption %d\", c.Encryption))\n\t}\n\tif c.AuthType == 3 {\n\t\tc.Password = kms.NewEmptySecret()\n\t\treturn c.OAuth2.validate()\n\t}\n\tc.OAuth2 = SMTPOAuth2{}\n\treturn nil\n}\n\n// TryDecrypt tries to decrypt the encrypted secrets\nfunc (c *SMTPConfigs) TryDecrypt() error {\n\tif c.Password == nil {\n\t\tc.Password = kms.NewEmptySecret()\n\t}\n\tif c.OAuth2.ClientSecret == nil {\n\t\tc.OAuth2.ClientSecret = kms.NewEmptySecret()\n\t}\n\tif c.OAuth2.RefreshToken == nil {\n\t\tc.OAuth2.RefreshToken = kms.NewEmptySecret()\n\t}\n\tif err := c.Password.TryDecrypt(); err != nil {\n\t\treturn fmt.Errorf(\"unable to decrypt smtp password: %w\", err)\n\t}\n\tif err := c.OAuth2.ClientSecret.TryDecrypt(); err != nil {\n\t\treturn fmt.Errorf(\"unable to decrypt smtp oauth2 client secret: %w\", err)\n\t}\n\tif err := c.OAuth2.RefreshToken.TryDecrypt(); err != nil {\n\t\treturn fmt.Errorf(\"unable to decrypt smtp oauth2 refresh token: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (c *SMTPConfigs) prepareForRendering() {\n\tif c.Password != nil {\n\t\tc.Password.Hide()\n\t\tif c.Password.IsEmpty() {\n\t\t\tc.Password = nil\n\t\t}\n\t}\n\tif c.OAuth2.ClientSecret != nil {\n\t\tc.OAuth2.ClientSecret.Hide()\n\t\tif c.OAuth2.ClientSecret.IsEmpty() {\n\t\t\tc.OAuth2.ClientSecret = nil\n\t\t}\n\t}\n\tif c.OAuth2.RefreshToken != nil {\n\t\tc.OAuth2.RefreshToken.Hide()\n\t\tif c.OAuth2.RefreshToken.IsEmpty() {\n\t\t\tc.OAuth2.RefreshToken = nil\n\t\t}\n\t}\n}\n\nfunc (c *SMTPConfigs) getACopy() *SMTPConfigs {\n\tvar password *kms.Secret\n\tif c.Password != nil {\n\t\tpassword = c.Password.Clone()\n\t}\n\treturn &SMTPConfigs{\n\t\tHost:       c.Host,\n\t\tPort:       c.Port,\n\t\tFrom:       c.From,\n\t\tUser:       c.User,\n\t\tPassword:   password,\n\t\tAuthType:   c.AuthType,\n\t\tEncryption: c.Encryption,\n\t\tDomain:     c.Domain,\n\t\tDebug:      c.Debug,\n\t\tOAuth2:     c.OAuth2.getACopy(),\n\t}\n}\n\n// ACMEHTTP01Challenge defines the configuration for HTTP-01 challenge type\ntype ACMEHTTP01Challenge struct {\n\tPort int `json:\"port\"`\n}\n\n// ACMEConfigs defines ACME related configuration\ntype ACMEConfigs struct {\n\tDomain          string              `json:\"domain\"`\n\tEmail           string              `json:\"email\"`\n\tHTTP01Challenge ACMEHTTP01Challenge `json:\"http01_challenge\"`\n\t// apply the certificate for the specified protocols:\n\t//\n\t// 1 means HTTP\n\t// 2 means FTP\n\t// 4 means WebDAV\n\t//\n\t// Protocols can be combined\n\tProtocols int `json:\"protocols\"`\n}\n\nfunc (c *ACMEConfigs) isEmpty() bool {\n\treturn c.Domain == \"\"\n}\n\nfunc (c *ACMEConfigs) validate() error {\n\tif c.Domain == \"\" {\n\t\treturn nil\n\t}\n\tif c.Email == \"\" && !util.IsEmailValid(c.Email) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"acme: invalid email %q\", c.Email)),\n\t\t\tutil.I18nErrorInvalidEmail,\n\t\t)\n\t}\n\tif c.HTTP01Challenge.Port <= 0 || c.HTTP01Challenge.Port > 65535 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"acme: invalid HTTP-01 challenge port %d\", c.HTTP01Challenge.Port))\n\t}\n\treturn nil\n}\n\n// HasProtocol returns true if the ACME certificate must be used for the specified protocol\nfunc (c *ACMEConfigs) HasProtocol(protocol string) bool {\n\tswitch protocol {\n\tcase protocolHTTP:\n\t\treturn c.Protocols&1 != 0\n\tcase protocolFTP:\n\t\treturn c.Protocols&2 != 0\n\tcase protocolWebDAV:\n\t\treturn c.Protocols&4 != 0\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (c *ACMEConfigs) getACopy() *ACMEConfigs {\n\treturn &ACMEConfigs{\n\t\tEmail:           c.Email,\n\t\tDomain:          c.Domain,\n\t\tHTTP01Challenge: ACMEHTTP01Challenge{Port: c.HTTP01Challenge.Port},\n\t\tProtocols:       c.Protocols,\n\t}\n}\n\n// BrandingConfig defines the branding configuration\ntype BrandingConfig struct {\n\tName           string `json:\"name\"`\n\tShortName      string `json:\"short_name\"`\n\tLogo           []byte `json:\"logo\"`\n\tFavicon        []byte `json:\"favicon\"`\n\tDisclaimerName string `json:\"disclaimer_name\"`\n\tDisclaimerURL  string `json:\"disclaimer_url\"`\n}\n\nfunc (c *BrandingConfig) isEmpty() bool {\n\tif c.Name != \"\" {\n\t\treturn false\n\t}\n\tif c.ShortName != \"\" {\n\t\treturn false\n\t}\n\tif len(c.Logo) > 0 {\n\t\treturn false\n\t}\n\tif len(c.Favicon) > 0 {\n\t\treturn false\n\t}\n\tif c.DisclaimerName != \"\" && c.DisclaimerURL != \"\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (*BrandingConfig) validatePNG(b []byte, maxWidth, maxHeight int) error {\n\tif len(b) == 0 {\n\t\treturn nil\n\t}\n\t// DecodeConfig is more efficient, but I'm not sure if this would lead to\n\t// accepting invalid images in some edge cases and performance does not\n\t// matter here.\n\timg, err := png.Decode(bytes.NewBuffer(b))\n\tif err != nil {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"invalid PNG image\"),\n\t\t\tutil.I18nErrorInvalidPNG,\n\t\t)\n\t}\n\tbounds := img.Bounds()\n\tif bounds.Dx() > maxWidth || bounds.Dy() > maxHeight {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"invalid PNG image size\"),\n\t\t\tutil.I18nErrorInvalidPNGSize,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc (c *BrandingConfig) validateDisclaimerURL() error {\n\tif c.DisclaimerURL == \"\" {\n\t\treturn nil\n\t}\n\tu, err := url.Parse(c.DisclaimerURL)\n\tif err != nil {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"invalid disclaimer URL\"),\n\t\t\tutil.I18nErrorInvalidDisclaimerURL,\n\t\t)\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"invalid disclaimer URL scheme\"),\n\t\t\tutil.I18nErrorInvalidDisclaimerURL,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc (c *BrandingConfig) validate() error {\n\tif err := c.validateDisclaimerURL(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.validatePNG(c.Logo, 512, 512); err != nil {\n\t\treturn err\n\t}\n\treturn c.validatePNG(c.Favicon, 256, 256)\n}\n\nfunc (c *BrandingConfig) getACopy() BrandingConfig {\n\tlogo := make([]byte, len(c.Logo))\n\tcopy(logo, c.Logo)\n\tfavicon := make([]byte, len(c.Favicon))\n\tcopy(favicon, c.Favicon)\n\n\treturn BrandingConfig{\n\t\tName:           c.Name,\n\t\tShortName:      c.ShortName,\n\t\tLogo:           logo,\n\t\tFavicon:        favicon,\n\t\tDisclaimerName: c.DisclaimerName,\n\t\tDisclaimerURL:  c.DisclaimerURL,\n\t}\n}\n\n// BrandingConfigs defines the branding configuration for WebAdmin and WebClient UI\ntype BrandingConfigs struct {\n\tWebAdmin  BrandingConfig\n\tWebClient BrandingConfig\n}\n\nfunc (c *BrandingConfigs) isEmpty() bool {\n\treturn c.WebAdmin.isEmpty() && c.WebClient.isEmpty()\n}\n\nfunc (c *BrandingConfigs) validate() error {\n\tif err := c.WebAdmin.validate(); err != nil {\n\t\treturn err\n\t}\n\treturn c.WebClient.validate()\n}\n\nfunc (c *BrandingConfigs) getACopy() *BrandingConfigs {\n\treturn &BrandingConfigs{\n\t\tWebAdmin:  c.WebAdmin.getACopy(),\n\t\tWebClient: c.WebClient.getACopy(),\n\t}\n}\n\n// Configs allows to set configuration keys disabled by default without\n// modifying the config file or setting env vars\ntype Configs struct {\n\tSFTPD     *SFTPDConfigs    `json:\"sftpd,omitempty\"`\n\tSMTP      *SMTPConfigs     `json:\"smtp,omitempty\"`\n\tACME      *ACMEConfigs     `json:\"acme,omitempty\"`\n\tBranding  *BrandingConfigs `json:\"branding,omitempty\"`\n\tUpdatedAt int64            `json:\"updated_at,omitempty\"`\n}\n\nfunc (c *Configs) validate() error {\n\tif c.SFTPD != nil {\n\t\tif err := c.SFTPD.validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif c.SMTP != nil {\n\t\tif err := c.SMTP.validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif c.ACME != nil {\n\t\tif err := c.ACME.validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif c.Branding != nil {\n\t\tif err := c.Branding.validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// PrepareForRendering prepares configs for rendering.\n// It hides confidential data and set to nil the empty structs/secrets\n// so they are not serialized\nfunc (c *Configs) PrepareForRendering() {\n\tif c.SFTPD != nil && c.SFTPD.isEmpty() {\n\t\tc.SFTPD = nil\n\t}\n\tif c.SMTP != nil && c.SMTP.IsEmpty() {\n\t\tc.SMTP = nil\n\t}\n\tif c.ACME != nil && c.ACME.isEmpty() {\n\t\tc.ACME = nil\n\t}\n\tif c.Branding != nil && c.Branding.isEmpty() {\n\t\tc.Branding = nil\n\t}\n\tif c.SMTP != nil {\n\t\tc.SMTP.prepareForRendering()\n\t}\n}\n\n// SetNilsToEmpty sets nil fields to empty\nfunc (c *Configs) SetNilsToEmpty() {\n\tif c.SFTPD == nil {\n\t\tc.SFTPD = &SFTPDConfigs{}\n\t}\n\tif c.SMTP == nil {\n\t\tc.SMTP = &SMTPConfigs{}\n\t}\n\tif c.SMTP.Password == nil {\n\t\tc.SMTP.Password = kms.NewEmptySecret()\n\t}\n\tif c.SMTP.OAuth2.ClientSecret == nil {\n\t\tc.SMTP.OAuth2.ClientSecret = kms.NewEmptySecret()\n\t}\n\tif c.SMTP.OAuth2.RefreshToken == nil {\n\t\tc.SMTP.OAuth2.RefreshToken = kms.NewEmptySecret()\n\t}\n\tif c.ACME == nil {\n\t\tc.ACME = &ACMEConfigs{}\n\t}\n\tif c.Branding == nil {\n\t\tc.Branding = &BrandingConfigs{}\n\t}\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (c *Configs) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\tconfig, err := provider.getConfigs()\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload config overrides before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tconfig.PrepareForRendering()\n\t\treturn json.Marshal(config)\n\t}\n\tc.PrepareForRendering()\n\treturn json.Marshal(c)\n}\n\nfunc (c *Configs) getACopy() Configs {\n\tvar result Configs\n\tif c.SFTPD != nil {\n\t\tresult.SFTPD = c.SFTPD.getACopy()\n\t}\n\tif c.SMTP != nil {\n\t\tresult.SMTP = c.SMTP.getACopy()\n\t}\n\tif c.ACME != nil {\n\t\tresult.ACME = c.ACME.getACopy()\n\t}\n\tif c.Branding != nil {\n\t\tresult.Branding = c.Branding.getACopy()\n\t}\n\tresult.UpdatedAt = c.UpdatedAt\n\treturn result\n}\n"
  },
  {
    "path": "internal/dataprovider/dataprovider.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package dataprovider provides data access.\n// It abstracts different data providers using a common API.\npackage dataprovider\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/rsa\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"crypto/subtle\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/GehirnInc/crypt\"\n\t\"github.com/GehirnInc/crypt/apr1_crypt\"\n\t\"github.com/GehirnInc/crypt/md5_crypt\"\n\t\"github.com/GehirnInc/crypt/sha256_crypt\"\n\t\"github.com/GehirnInc/crypt/sha512_crypt\"\n\t\"github.com/alexedwards/argon2id\"\n\t\"github.com/go-chi/render\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\tpasswordvalidator \"github.com/wagslane/go-password-validator\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"golang.org/x/crypto/pbkdf2\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/command\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\t// SQLiteDataProviderName defines the name for SQLite database provider\n\tSQLiteDataProviderName = \"sqlite\"\n\t// PGSQLDataProviderName defines the name for PostgreSQL database provider\n\tPGSQLDataProviderName = \"postgresql\"\n\t// MySQLDataProviderName defines the name for MySQL database provider\n\tMySQLDataProviderName = \"mysql\"\n\t// BoltDataProviderName defines the name for bbolt key/value store provider\n\tBoltDataProviderName = \"bolt\"\n\t// MemoryDataProviderName defines the name for memory provider\n\tMemoryDataProviderName = \"memory\"\n\t// CockroachDataProviderName defines the for CockroachDB provider\n\tCockroachDataProviderName = \"cockroachdb\"\n\t// DumpVersion defines the version for the dump.\n\t// For restore/load we support the current version and the previous one\n\tDumpVersion = 17\n\n\targonPwdPrefix            = \"$argon2id$\"\n\tbcryptPwdPrefix           = \"$2a$\"\n\tpbkdf2SHA1Prefix          = \"$pbkdf2-sha1$\"\n\tpbkdf2SHA256Prefix        = \"$pbkdf2-sha256$\"\n\tpbkdf2SHA512Prefix        = \"$pbkdf2-sha512$\"\n\tpbkdf2SHA256B64SaltPrefix = \"$pbkdf2-b64salt-sha256$\"\n\tmd5cryptPwdPrefix         = \"$1$\"\n\tmd5cryptApr1PwdPrefix     = \"$apr1$\"\n\tsha256cryptPwdPrefix      = \"$5$\"\n\tsha512cryptPwdPrefix      = \"$6$\"\n\tyescryptPwdPrefix         = \"$y$\"\n\tmd5DigestPwdPrefix        = \"{MD5}\"\n\tsha256DigestPwdPrefix     = \"{SHA256}\"\n\tsha512DigestPwdPrefix     = \"{SHA512}\"\n\ttrackQuotaDisabledError   = \"please enable track_quota in your configuration to use this method\"\n\toperationAdd              = \"add\"\n\toperationUpdate           = \"update\"\n\toperationDelete           = \"delete\"\n\tsqlPrefixValidChars       = \"abcdefghijklmnopqrstuvwxyz_0123456789\"\n\tmaxHookResponseSize       = 1048576 // 1MB\n)\n\n// Supported algorithms for hashing passwords.\n// These algorithms can be used when SFTPGo hashes a plain text password\nconst (\n\tHashingAlgoBcrypt   = \"bcrypt\"\n\tHashingAlgoArgon2ID = \"argon2id\"\n)\n\n// ordering constants\nconst (\n\tOrderASC  = \"ASC\"\n\tOrderDESC = \"DESC\"\n)\n\nconst (\n\tprotocolSSH    = \"SSH\"\n\tprotocolFTP    = \"FTP\"\n\tprotocolWebDAV = \"DAV\"\n\tprotocolHTTP   = \"HTTP\"\n)\n\n// Dump scopes\nconst (\n\tDumpScopeUsers   = \"users\"\n\tDumpScopeFolders = \"folders\"\n\tDumpScopeGroups  = \"groups\"\n\tDumpScopeAdmins  = \"admins\"\n\tDumpScopeAPIKeys = \"api_keys\"\n\tDumpScopeShares  = \"shares\"\n\tDumpScopeActions = \"actions\"\n\tDumpScopeRules   = \"rules\"\n\tDumpScopeRoles   = \"roles\"\n\tDumpScopeIPLists = \"ip_lists\"\n\tDumpScopeConfigs = \"configs\"\n)\n\nconst (\n\tfieldUsername = 1\n\tfieldName     = 2\n\tfieldIPNet    = 3\n)\n\nvar (\n\t// SupportedProviders defines the supported data providers\n\tSupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName,\n\t\tBoltDataProviderName, MemoryDataProviderName, CockroachDataProviderName}\n\t// ValidPerms defines all the valid permissions for a user\n\tValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermCreateDirs, PermRename,\n\t\tPermRenameFiles, PermRenameDirs, PermDelete, PermDeleteFiles, PermDeleteDirs, PermCopy, PermCreateSymlinks,\n\t\tPermChmod, PermChown, PermChtimes}\n\t// ValidLoginMethods defines all the valid login methods\n\tValidLoginMethods = []string{SSHLoginMethodPublicKey, LoginMethodPassword, SSHLoginMethodPassword,\n\t\tSSHLoginMethodKeyboardInteractive, SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt,\n\t\tLoginMethodTLSCertificate, LoginMethodTLSCertificateAndPwd}\n\t// SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications\n\tSSHMultiStepsLoginMethods = []string{SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}\n\t// ErrNoAuthTried defines the error for connection closed before authentication\n\tErrNoAuthTried = errors.New(\"no auth tried\")\n\t// ErrNotImplemented defines the error for features not supported for a particular data provider\n\tErrNotImplemented = errors.New(\"feature not supported with the configured data provider\")\n\t// ValidProtocols defines all the valid protcols\n\tValidProtocols = []string{protocolSSH, protocolFTP, protocolWebDAV, protocolHTTP}\n\t// MFAProtocols defines the supported protocols for multi-factor authentication\n\tMFAProtocols = []string{protocolHTTP, protocolSSH, protocolFTP}\n\t// ErrNoInitRequired defines the error returned by InitProvider if no inizialization/update is required\n\tErrNoInitRequired = errors.New(\"the data provider is up to date\")\n\t// ErrInvalidCredentials defines the error to return if the supplied credentials are invalid\n\tErrInvalidCredentials = errors.New(\"invalid credentials\")\n\t// ErrLoginNotAllowedFromIP defines the error to return if login is denied from the current IP\n\tErrLoginNotAllowedFromIP = errors.New(\"login is not allowed from this IP\")\n\t// ErrDuplicatedKey occurs when there is a unique key constraint violation\n\tErrDuplicatedKey = errors.New(\"duplicated key not allowed\")\n\t// ErrForeignKeyViolated occurs when there is a foreign key constraint violation\n\tErrForeignKeyViolated   = errors.New(\"violates foreign key constraint\")\n\terrInvalidInput         = util.NewValidationError(\"Invalid input. Slashes (/ ), colons (:), control characters, and reserved system names are not allowed\")\n\ttz                      = \"\"\n\tisAdminCreated          atomic.Bool\n\tvalidTLSUsernames       = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)}\n\tconfig                  Config\n\tprovider                Provider\n\tsqlPlaceholders         []string\n\tinternalHashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix}\n\thashPwdPrefixes         = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,\n\t\tpbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, md5DigestPwdPrefix,\n\t\tsha256DigestPwdPrefix, sha512DigestPwdPrefix, sha256cryptPwdPrefix, sha512cryptPwdPrefix, yescryptPwdPrefix}\n\tpbkdfPwdPrefixes        = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix}\n\tpbkdfPwdB64SaltPrefixes = []string{pbkdf2SHA256B64SaltPrefix}\n\tunixPwdPrefixes         = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha256cryptPwdPrefix, sha512cryptPwdPrefix,\n\t\tyescryptPwdPrefix}\n\tdigestPwdPrefixes            = []string{md5DigestPwdPrefix, sha256DigestPwdPrefix, sha512DigestPwdPrefix}\n\tsharedProviders              = []string{PGSQLDataProviderName, MySQLDataProviderName, CockroachDataProviderName}\n\tlogSender                    = \"dataprovider\"\n\tsqlTableUsers                string\n\tsqlTableFolders              string\n\tsqlTableUsersFoldersMapping  string\n\tsqlTableAdmins               string\n\tsqlTableAPIKeys              string\n\tsqlTableShares               string\n\tsqlTableSharesGroupsMapping  string\n\tsqlTableDefenderHosts        string\n\tsqlTableDefenderEvents       string\n\tsqlTableActiveTransfers      string\n\tsqlTableGroups               string\n\tsqlTableUsersGroupsMapping   string\n\tsqlTableAdminsGroupsMapping  string\n\tsqlTableGroupsFoldersMapping string\n\tsqlTableSharedSessions       string\n\tsqlTableEventsActions        string\n\tsqlTableEventsRules          string\n\tsqlTableRulesActionsMapping  string\n\tsqlTableTasks                string\n\tsqlTableNodes                string\n\tsqlTableRoles                string\n\tsqlTableIPLists              string\n\tsqlTableConfigs              string\n\tsqlTableSchemaVersion        string\n\targon2Params                 *argon2id.Params\n\tlastLoginMinDelay            = 10 * time.Minute\n\tusernameRegex                = regexp.MustCompile(\"^[a-zA-Z0-9-_.~]+$\")\n\ttempPath                     string\n\tallowSelfConnections         int\n\tfnReloadRules                FnReloadRules\n\tfnRemoveRule                 FnRemoveRule\n\tfnHandleRuleForProviderEvent FnHandleRuleForProviderEvent\n)\n\nfunc initSQLTables() {\n\tsqlTableUsers = \"users\"\n\tsqlTableFolders = \"folders\"\n\tsqlTableUsersFoldersMapping = \"users_folders_mapping\"\n\tsqlTableAdmins = \"admins\"\n\tsqlTableAPIKeys = \"api_keys\"\n\tsqlTableShares = \"shares\"\n\tsqlTableSharesGroupsMapping = \"shares_groups_mapping\"\n\tsqlTableDefenderHosts = \"defender_hosts\"\n\tsqlTableDefenderEvents = \"defender_events\"\n\tsqlTableActiveTransfers = \"active_transfers\"\n\tsqlTableGroups = \"groups\"\n\tsqlTableUsersGroupsMapping = \"users_groups_mapping\"\n\tsqlTableGroupsFoldersMapping = \"groups_folders_mapping\"\n\tsqlTableAdminsGroupsMapping = \"admins_groups_mapping\"\n\tsqlTableSharedSessions = \"shared_sessions\"\n\tsqlTableEventsActions = \"events_actions\"\n\tsqlTableEventsRules = \"events_rules\"\n\tsqlTableRulesActionsMapping = \"rules_actions_mapping\"\n\tsqlTableTasks = \"tasks\"\n\tsqlTableNodes = \"nodes\"\n\tsqlTableRoles = \"roles\"\n\tsqlTableIPLists = \"ip_lists\"\n\tsqlTableConfigs = \"configurations\"\n\tsqlTableSchemaVersion = \"schema_version\"\n}\n\n// FnReloadRules defined the callback to reload event rules\ntype FnReloadRules func()\n\n// FnRemoveRule defines the callback to remove an event rule\ntype FnRemoveRule func(name string)\n\n// FnHandleRuleForProviderEvent define the callback to handle event rules for provider events\ntype FnHandleRuleForProviderEvent func(operation, executor, ip, objectType, objectName, role string, object plugin.Renderer)\n\n// SetEventRulesCallbacks sets the event rules callbacks\nfunc SetEventRulesCallbacks(reload FnReloadRules, remove FnRemoveRule, handle FnHandleRuleForProviderEvent) {\n\tfnReloadRules = reload\n\tfnRemoveRule = remove\n\tfnHandleRuleForProviderEvent = handle\n}\n\ntype schemaVersion struct {\n\tVersion int\n}\n\n// BcryptOptions defines the options for bcrypt password hashing\ntype BcryptOptions struct {\n\tCost int `json:\"cost\" mapstructure:\"cost\"`\n}\n\n// Argon2Options defines the options for argon2 password hashing\ntype Argon2Options struct {\n\tMemory      uint32 `json:\"memory\" mapstructure:\"memory\"`\n\tIterations  uint32 `json:\"iterations\" mapstructure:\"iterations\"`\n\tParallelism uint8  `json:\"parallelism\" mapstructure:\"parallelism\"`\n}\n\n// PasswordHashing defines the configuration for password hashing\ntype PasswordHashing struct {\n\tBcryptOptions BcryptOptions `json:\"bcrypt_options\" mapstructure:\"bcrypt_options\"`\n\tArgon2Options Argon2Options `json:\"argon2_options\" mapstructure:\"argon2_options\"`\n\t// Algorithm to use for hashing passwords. Available algorithms: argon2id, bcrypt. Default: bcrypt\n\tAlgo string `json:\"algo\" mapstructure:\"algo\"`\n}\n\n// PasswordValidationRules defines the password validation rules\ntype PasswordValidationRules struct {\n\t// MinEntropy defines the minimum password entropy.\n\t// 0 means disabled, any password will be accepted.\n\t// Take a look at the following link for more details\n\t// https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use\n\tMinEntropy float64 `json:\"min_entropy\" mapstructure:\"min_entropy\"`\n}\n\n// PasswordValidation defines the password validation rules for admins and protocol users\ntype PasswordValidation struct {\n\t// Password validation rules for SFTPGo admin users\n\tAdmins PasswordValidationRules `json:\"admins\" mapstructure:\"admins\"`\n\t// Password validation rules for SFTPGo protocol users\n\tUsers PasswordValidationRules `json:\"users\" mapstructure:\"users\"`\n}\n\ntype wrappedFolder struct {\n\tFolder vfs.BaseVirtualFolder\n}\n\nfunc (w *wrappedFolder) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\tfolder, err := provider.getFolderByName(w.Folder.Name)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload folder before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tfolder.PrepareForRendering()\n\t\treturn json.Marshal(folder)\n\t}\n\tw.Folder.PrepareForRendering()\n\treturn json.Marshal(w.Folder)\n}\n\n// ObjectsActions defines the action to execute on user create, update, delete for the specified objects\ntype ObjectsActions struct {\n\t// Valid values are add, update, delete. Empty slice to disable\n\tExecuteOn []string `json:\"execute_on\" mapstructure:\"execute_on\"`\n\t// Valid values are user, admin, api_key\n\tExecuteFor []string `json:\"execute_for\" mapstructure:\"execute_for\"`\n\t// Absolute path to an external program or an HTTP URL\n\tHook string `json:\"hook\" mapstructure:\"hook\"`\n}\n\n// ProviderStatus defines the provider status\ntype ProviderStatus struct {\n\tDriver   string `json:\"driver\"`\n\tIsActive bool   `json:\"is_active\"`\n\tError    string `json:\"error\"`\n}\n\n// Config defines the provider configuration\ntype Config struct {\n\t// Driver name, must be one of the SupportedProviders\n\tDriver string `json:\"driver\" mapstructure:\"driver\"`\n\t// Database name. For driver sqlite this can be the database name relative to the config dir\n\t// or the absolute path to the SQLite database.\n\tName string `json:\"name\" mapstructure:\"name\"`\n\t// Database host. For postgresql and cockroachdb driver you can specify multiple hosts separated by commas\n\tHost string `json:\"host\" mapstructure:\"host\"`\n\t// Database port\n\tPort int `json:\"port\" mapstructure:\"port\"`\n\t// Database username\n\tUsername string `json:\"username\" mapstructure:\"username\"`\n\t// Database password\n\tPassword string `json:\"password\" mapstructure:\"password\"`\n\t// Used for drivers mysql and postgresql.\n\t// 0 disable SSL/TLS connections.\n\t// 1 require ssl.\n\t// 2 set ssl mode to verify-ca for driver postgresql and skip-verify for driver mysql.\n\t// 3 set ssl mode to verify-full for driver postgresql and preferred for driver mysql.\n\tSSLMode int `json:\"sslmode\" mapstructure:\"sslmode\"`\n\t// Used for drivers mysql, postgresql and cockroachdb. Set to true to disable SNI\n\tDisableSNI bool `json:\"disable_sni\" mapstructure:\"disable_sni\"`\n\t// TargetSessionAttrs is a postgresql and cockroachdb specific option.\n\t// It determines whether the session must have certain properties to be acceptable.\n\t// It's typically used in combination with multiple host names to select the first\n\t// acceptable alternative among several hosts\n\tTargetSessionAttrs string `json:\"target_session_attrs\" mapstructure:\"target_session_attrs\"`\n\t// Path to the root certificate authority used to verify that the server certificate was signed by a trusted CA\n\tRootCert string `json:\"root_cert\" mapstructure:\"root_cert\"`\n\t// Path to the client certificate for two-way TLS authentication\n\tClientCert string `json:\"client_cert\" mapstructure:\"client_cert\"`\n\t// Path to the client key for two-way TLS authentication\n\tClientKey string `json:\"client_key\" mapstructure:\"client_key\"`\n\t// Custom database connection string.\n\t// If not empty this connection string will be used instead of build one using the previous parameters\n\tConnectionString string `json:\"connection_string\" mapstructure:\"connection_string\"`\n\t// prefix for SQL tables\n\tSQLTablesPrefix string `json:\"sql_tables_prefix\" mapstructure:\"sql_tables_prefix\"`\n\t// Set the preferred way to track users quota between the following choices:\n\t// 0, disable quota tracking. REST API to scan user dir and update quota will do nothing\n\t// 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions\n\t// 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions\n\t//    and for virtual folders.\n\t//    With this configuration the \"quota scan\" REST API can still be used to periodically update space usage\n\t//    for users without quota restrictions\n\tTrackQuota int `json:\"track_quota\" mapstructure:\"track_quota\"`\n\t// Sets the maximum number of open connections for mysql and postgresql driver.\n\t// Default 0 (unlimited)\n\tPoolSize int `json:\"pool_size\" mapstructure:\"pool_size\"`\n\t// Users default base directory.\n\t// If no home dir is defined while adding a new user, and this value is\n\t// a valid absolute path, then the user home dir will be automatically\n\t// defined as the path obtained joining the base dir and the username\n\tUsersBaseDir string `json:\"users_base_dir\" mapstructure:\"users_base_dir\"`\n\t// Actions to execute on objects add, update, delete.\n\t// The supported objects are user, admin, api_key.\n\t// Update action will not be fired for internal updates such as the last login or the user quota fields.\n\tActions ObjectsActions `json:\"actions\" mapstructure:\"actions\"`\n\t// Absolute path to an external program or an HTTP URL to invoke for users authentication.\n\t// Leave empty to use builtin authentication.\n\t// If the authentication succeed the user will be automatically added/updated inside the defined data provider.\n\t// Actions defined for user added/updated will not be executed in this case.\n\t// This method is slower than built-in authentication methods, but it's very flexible as anyone can\n\t// easily write his own authentication hooks.\n\tExternalAuthHook string `json:\"external_auth_hook\" mapstructure:\"external_auth_hook\"`\n\t// ExternalAuthScope defines the scope for the external authentication hook.\n\t// - 0 means all supported authentication scopes, the external hook will be executed for password,\n\t//     public key, keyboard interactive authentication and TLS certificates\n\t// - 1 means passwords only\n\t// - 2 means public keys only\n\t// - 4 means keyboard interactive only\n\t// - 8 means TLS certificates only\n\t// you can combine the scopes, for example 3 means password and public key, 5 password and keyboard\n\t// interactive and so on\n\tExternalAuthScope int `json:\"external_auth_scope\" mapstructure:\"external_auth_scope\"`\n\t// Absolute path to an external program or an HTTP URL to invoke just before the user login.\n\t// This program/URL allows to modify or create the user trying to login.\n\t// It is useful if you have users with dynamic fields to update just before the login.\n\t// Please note that if you want to create a new user, the pre-login hook response must\n\t// include all the mandatory user fields.\n\t//\n\t// The pre-login hook must finish within 30 seconds.\n\t//\n\t// If an error happens while executing the \"PreLoginHook\" then login will be denied.\n\t// PreLoginHook and ExternalAuthHook are mutally exclusive.\n\t// Leave empty to disable.\n\tPreLoginHook string `json:\"pre_login_hook\" mapstructure:\"pre_login_hook\"`\n\t// Absolute path to an external program or an HTTP URL to invoke after the user login.\n\t// Based on the configured scope you can choose if notify failed or successful logins\n\t// or both\n\tPostLoginHook string `json:\"post_login_hook\" mapstructure:\"post_login_hook\"`\n\t// PostLoginScope defines the scope for the post-login hook.\n\t// - 0 means notify both failed and successful logins\n\t// - 1 means notify failed logins\n\t// - 2 means notify successful logins\n\tPostLoginScope int `json:\"post_login_scope\" mapstructure:\"post_login_scope\"`\n\t// Absolute path to an external program or an HTTP URL to invoke just before password\n\t// authentication. This hook allows you to externally check the provided password,\n\t// its main use case is to allow to easily support things like password+OTP for protocols\n\t// without keyboard interactive support such as FTP and WebDAV. You can ask your users\n\t// to login using a string consisting of a fixed password and a One Time Token, you\n\t// can verify the token inside the hook and ask to SFTPGo to verify the fixed part.\n\tCheckPasswordHook string `json:\"check_password_hook\" mapstructure:\"check_password_hook\"`\n\t// CheckPasswordScope defines the scope for the check password hook.\n\t// - 0 means all protocols\n\t// - 1 means SSH\n\t// - 2 means FTP\n\t// - 4 means WebDAV\n\t// you can combine the scopes, for example 6 means FTP and WebDAV\n\tCheckPasswordScope int `json:\"check_password_scope\" mapstructure:\"check_password_scope\"`\n\t// Defines how the database will be initialized/updated:\n\t// - 0 means automatically\n\t// - 1 means manually using the initprovider sub-command\n\tUpdateMode int `json:\"update_mode\" mapstructure:\"update_mode\"`\n\t// PasswordHashing defines the configuration for password hashing\n\tPasswordHashing PasswordHashing `json:\"password_hashing\" mapstructure:\"password_hashing\"`\n\t// PasswordValidation defines the password validation rules\n\tPasswordValidation PasswordValidation `json:\"password_validation\" mapstructure:\"password_validation\"`\n\t// Verifying argon2 passwords has a high memory and computational cost,\n\t// by enabling, in memory, password caching you reduce this cost.\n\tPasswordCaching bool `json:\"password_caching\" mapstructure:\"password_caching\"`\n\t// DelayedQuotaUpdate defines the number of seconds to accumulate quota updates.\n\t// If there are a lot of close uploads, accumulating quota updates can save you many\n\t// queries to the data provider.\n\t// If you want to track quotas, a scheduled quota update is recommended in any case, the stored\n\t// quota size may be incorrect for several reasons, such as an unexpected shutdown, temporary provider\n\t// failures, file copied outside of SFTPGo, and so on.\n\t// 0 means immediate quota update.\n\tDelayedQuotaUpdate int `json:\"delayed_quota_update\" mapstructure:\"delayed_quota_update\"`\n\t// If enabled, a default admin user with username \"admin\" and password \"password\" will be created\n\t// on first start.\n\t// You can also create the first admin user by using the web interface or by loading initial data.\n\tCreateDefaultAdmin bool `json:\"create_default_admin\" mapstructure:\"create_default_admin\"`\n\t// Rules for usernames and folder names:\n\t// - 0 means no rules\n\t// - 1 means you can use any UTF-8 character. The names are used in URIs for REST API and Web admin.\n\t//     By default only unreserved URI characters are allowed: ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\".\n\t// - 2 means names are converted to lowercase before saving/matching and so case\n\t//     insensitive matching is possible\n\t// - 4 means trimming trailing and leading white spaces before saving/matching\n\t// Rules can be combined, for example 3 means both converting to lowercase and allowing any UTF-8 character.\n\t// Enabling these options for existing installations could be backward incompatible, some users\n\t// could be unable to login, for example existing users with mixed cases in their usernames.\n\t// You have to ensure that all existing users respect the defined rules.\n\tNamingRules int `json:\"naming_rules\" mapstructure:\"naming_rules\"`\n\t// If the data provider is shared across multiple SFTPGo instances, set this parameter to 1.\n\t// MySQL, PostgreSQL and CockroachDB can be shared, this setting is ignored for other data\n\t// providers. For shared data providers, SFTPGo periodically reloads the latest updated users,\n\t// based on the \"updated_at\" field, and updates its internal caches if users are updated from\n\t// a different instance. This check, if enabled, is executed every 10 minutes.\n\t// For shared data providers, active transfers are persisted in the database and thus\n\t// quota checks between ongoing transfers will work cross multiple instances\n\tIsShared int `json:\"is_shared\" mapstructure:\"is_shared\"`\n\t// Node defines the configuration for this cluster node.\n\t// Ignored if the provider is not shared/shareable\n\tNode NodeConfig `json:\"node\" mapstructure:\"node\"`\n\t// Path to the backup directory. This can be an absolute path or a path relative to the config dir\n\tBackupsPath string `json:\"backups_path\" mapstructure:\"backups_path\"`\n}\n\n// GetShared returns the provider share mode.\n// This method is called before the provider is initialized\nfunc (c *Config) GetShared() int {\n\tif !slices.Contains(sharedProviders, c.Driver) {\n\t\treturn 0\n\t}\n\treturn c.IsShared\n}\n\nfunc (c *Config) convertName(name string) string {\n\tif c.NamingRules <= 1 {\n\t\treturn name\n\t}\n\tif c.NamingRules&2 != 0 {\n\t\tname = strings.ToLower(name)\n\t}\n\tif c.NamingRules&4 != 0 {\n\t\tname = strings.TrimSpace(name)\n\t}\n\n\treturn name\n}\n\n// IsDefenderSupported returns true if the configured provider supports the defender\nfunc (c *Config) IsDefenderSupported() bool {\n\tswitch c.Driver {\n\tcase MySQLDataProviderName, PGSQLDataProviderName, CockroachDataProviderName:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (c *Config) requireCustomTLSForMySQL() bool {\n\tif config.DisableSNI {\n\t\treturn config.SSLMode != 0\n\t}\n\tif config.RootCert != \"\" && util.IsFileInputValid(config.RootCert) {\n\t\treturn config.SSLMode != 0\n\t}\n\tif config.ClientCert != \"\" && config.ClientKey != \"\" && util.IsFileInputValid(config.ClientCert) &&\n\t\tutil.IsFileInputValid(config.ClientKey) {\n\t\treturn config.SSLMode != 0\n\t}\n\treturn false\n}\n\nfunc (c *Config) doBackup() (string, error) {\n\tnow := time.Now().UTC()\n\toutputFile := filepath.Join(c.BackupsPath, fmt.Sprintf(\"backup_%s_%d.json\", now.Weekday(), now.Hour()))\n\tproviderLog(logger.LevelDebug, \"starting backup to file %q\", outputFile)\n\terr := os.MkdirAll(filepath.Dir(outputFile), 0700)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to create backup dir %q: %v\", outputFile, err)\n\t\treturn outputFile, fmt.Errorf(\"unable to create backup dir: %w\", err)\n\t}\n\tbackup, err := DumpData(nil)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to execute backup: %v\", err)\n\t\treturn outputFile, fmt.Errorf(\"unable to dump backup data: %w\", err)\n\t}\n\tdump, err := json.Marshal(backup)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to marshal backup as JSON: %v\", err)\n\t\treturn outputFile, fmt.Errorf(\"unable to marshal backup data as JSON: %w\", err)\n\t}\n\terr = os.WriteFile(outputFile, dump, 0600)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to save backup: %v\", err)\n\t\treturn outputFile, fmt.Errorf(\"unable to save backup: %w\", err)\n\t}\n\tproviderLog(logger.LevelDebug, \"backup saved to %q\", outputFile)\n\treturn outputFile, nil\n}\n\n// SetTZ sets the configured timezone.\nfunc SetTZ(val string) {\n\ttz = val\n}\n\n// UseLocalTime returns true if local time should be used instead of UTC.\nfunc UseLocalTime() bool {\n\treturn tz == \"local\"\n}\n\n// ExecuteBackup executes a backup\nfunc ExecuteBackup() (string, error) {\n\treturn config.doBackup()\n}\n\n// ConvertName converts the given name based on the configured rules\nfunc ConvertName(name string) string {\n\treturn config.convertName(name)\n}\n\n// ActiveTransfer defines an active protocol transfer\ntype ActiveTransfer struct {\n\tID            int64\n\tType          int\n\tConnID        string\n\tUsername      string\n\tFolderName    string\n\tIP            string\n\tTruncatedSize int64\n\tCurrentULSize int64\n\tCurrentDLSize int64\n\tCreatedAt     int64\n\tUpdatedAt     int64\n}\n\n// TransferQuota stores the allowed transfer quota fields\ntype TransferQuota struct {\n\tULSize           int64\n\tDLSize           int64\n\tTotalSize        int64\n\tAllowedULSize    int64\n\tAllowedDLSize    int64\n\tAllowedTotalSize int64\n}\n\n// HasSizeLimits returns true if any size limit is set\nfunc (q *TransferQuota) HasSizeLimits() bool {\n\treturn q.AllowedDLSize > 0 || q.AllowedULSize > 0 || q.AllowedTotalSize > 0\n}\n\n// HasUploadSpace returns true if there is transfer upload space available\nfunc (q *TransferQuota) HasUploadSpace() bool {\n\tif q.TotalSize <= 0 && q.ULSize <= 0 {\n\t\treturn true\n\t}\n\tif q.TotalSize > 0 {\n\t\treturn q.AllowedTotalSize > 0\n\t}\n\treturn q.AllowedULSize > 0\n}\n\n// HasDownloadSpace returns true if there is transfer download space available\nfunc (q *TransferQuota) HasDownloadSpace() bool {\n\tif q.TotalSize <= 0 && q.DLSize <= 0 {\n\t\treturn true\n\t}\n\tif q.TotalSize > 0 {\n\t\treturn q.AllowedTotalSize > 0\n\t}\n\treturn q.AllowedDLSize > 0\n}\n\n// DefenderEntry defines a defender entry\ntype DefenderEntry struct {\n\tID      int64     `json:\"-\"`\n\tIP      string    `json:\"ip\"`\n\tScore   int       `json:\"score,omitempty\"`\n\tBanTime time.Time `json:\"ban_time,omitempty\"`\n}\n\n// GetID returns an unique ID for a defender entry\nfunc (d *DefenderEntry) GetID() string {\n\treturn hex.EncodeToString([]byte(d.IP))\n}\n\n// GetBanTime returns the ban time for a defender entry as string\nfunc (d *DefenderEntry) GetBanTime() string {\n\tif d.BanTime.IsZero() {\n\t\treturn \"\"\n\t}\n\treturn d.BanTime.UTC().Format(time.RFC3339)\n}\n\n// MarshalJSON returns the JSON encoding of a DefenderEntry.\nfunc (d *DefenderEntry) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(&struct {\n\t\tID      string `json:\"id\"`\n\t\tIP      string `json:\"ip\"`\n\t\tScore   int    `json:\"score,omitempty\"`\n\t\tBanTime string `json:\"ban_time,omitempty\"`\n\t}{\n\t\tID:      d.GetID(),\n\t\tIP:      d.IP,\n\t\tScore:   d.Score,\n\t\tBanTime: d.GetBanTime(),\n\t})\n}\n\n// BackupData defines the structure for the backup/restore files\ntype BackupData struct {\n\tUsers        []User                  `json:\"users\"`\n\tGroups       []Group                 `json:\"groups\"`\n\tFolders      []vfs.BaseVirtualFolder `json:\"folders\"`\n\tAdmins       []Admin                 `json:\"admins\"`\n\tAPIKeys      []APIKey                `json:\"api_keys\"`\n\tShares       []Share                 `json:\"shares\"`\n\tEventActions []BaseEventAction       `json:\"event_actions\"`\n\tEventRules   []EventRule             `json:\"event_rules\"`\n\tRoles        []Role                  `json:\"roles\"`\n\tIPLists      []IPListEntry           `json:\"ip_lists\"`\n\tConfigs      *Configs                `json:\"configs\"`\n\tVersion      int                     `json:\"version\"`\n}\n\n// HasFolder returns true if the folder with the given name is included\nfunc (d *BackupData) HasFolder(name string) bool {\n\tfor _, folder := range d.Folders {\n\t\tif folder.Name == name {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\ntype checkPasswordRequest struct {\n\tUsername string `json:\"username\"`\n\tIP       string `json:\"ip\"`\n\tPassword string `json:\"password\"`\n\tProtocol string `json:\"protocol\"`\n}\n\ntype checkPasswordResponse struct {\n\t// 0 KO, 1 OK, 2 partial success, -1 not executed\n\tStatus int `json:\"status\"`\n\t// for status = 2 this is the password to check against the one stored\n\t// inside the SFTPGo data provider\n\tToVerify string `json:\"to_verify\"`\n}\n\n// GetQuotaTracking returns the configured mode for user's quota tracking\nfunc GetQuotaTracking() int {\n\treturn config.TrackQuota\n}\n\n// HasUsersBaseDir returns true if users base dir is set\nfunc HasUsersBaseDir() bool {\n\treturn config.UsersBaseDir != \"\"\n}\n\n// Provider defines the interface that data providers must implement.\ntype Provider interface {\n\tvalidateUserAndPass(username, password, ip, protocol string) (User, error)\n\tvalidateUserAndPubKey(username string, pubKey []byte, isSSHCert bool) (User, string, error)\n\tvalidateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error)\n\tupdateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error\n\tupdateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error\n\tgetUsedQuota(username string) (int, int64, int64, int64, error)\n\tuserExists(username, role string) (User, error)\n\taddUser(user *User) error\n\tupdateUser(user *User) error\n\tdeleteUser(user User, softDelete bool) error\n\tupdateUserPassword(username, password string) error // used internally when converting passwords from other hash\n\tgetUsers(limit int, offset int, order, role string) ([]User, error)\n\tdumpUsers() ([]User, error)\n\tgetRecentlyUpdatedUsers(after int64) ([]User, error)\n\tgetUsersForQuotaCheck(toFetch map[string]bool) ([]User, error)\n\tupdateLastLogin(username string) error\n\tupdateAdminLastLogin(username string) error\n\tsetUpdatedAt(username string)\n\tgetAdminSignature(username string) (string, error)\n\tgetUserSignature(username string) (string, error)\n\tgetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error)\n\tgetFolderByName(name string) (vfs.BaseVirtualFolder, error)\n\taddFolder(folder *vfs.BaseVirtualFolder) error\n\tupdateFolder(folder *vfs.BaseVirtualFolder) error\n\tdeleteFolder(folder vfs.BaseVirtualFolder) error\n\tupdateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error\n\tgetUsedFolderQuota(name string) (int, int64, error)\n\tdumpFolders() ([]vfs.BaseVirtualFolder, error)\n\tgetGroups(limit, offset int, order string, minimal bool) ([]Group, error)\n\tgetGroupsWithNames(names []string) ([]Group, error)\n\tgetUsersInGroups(names []string) ([]string, error)\n\tgroupExists(name string) (Group, error)\n\taddGroup(group *Group) error\n\tupdateGroup(group *Group) error\n\tdeleteGroup(group Group) error\n\tdumpGroups() ([]Group, error)\n\tadminExists(username string) (Admin, error)\n\taddAdmin(admin *Admin) error\n\tupdateAdmin(admin *Admin) error\n\tdeleteAdmin(admin Admin) error\n\tgetAdmins(limit int, offset int, order string) ([]Admin, error)\n\tdumpAdmins() ([]Admin, error)\n\tvalidateAdminAndPass(username, password, ip string) (Admin, error)\n\tapiKeyExists(keyID string) (APIKey, error)\n\taddAPIKey(apiKey *APIKey) error\n\tupdateAPIKey(apiKey *APIKey) error\n\tdeleteAPIKey(apiKey APIKey) error\n\tgetAPIKeys(limit int, offset int, order string) ([]APIKey, error)\n\tdumpAPIKeys() ([]APIKey, error)\n\tupdateAPIKeyLastUse(keyID string) error\n\tshareExists(shareID, username string) (Share, error)\n\taddShare(share *Share) error\n\tupdateShare(share *Share) error\n\tdeleteShare(share Share) error\n\tgetShares(limit int, offset int, order, username string) ([]Share, error)\n\tdumpShares() ([]Share, error)\n\tupdateShareLastUse(shareID string, numTokens int) error\n\tgetDefenderHosts(from int64, limit int) ([]DefenderEntry, error)\n\tgetDefenderHostByIP(ip string, from int64) (DefenderEntry, error)\n\tisDefenderHostBanned(ip string) (DefenderEntry, error)\n\tupdateDefenderBanTime(ip string, minutes int) error\n\tdeleteDefenderHost(ip string) error\n\taddDefenderEvent(ip string, score int) error\n\tsetDefenderBanTime(ip string, banTime int64) error\n\tcleanupDefender(from int64) error\n\taddActiveTransfer(transfer ActiveTransfer) error\n\tupdateActiveTransferSizes(ulSize, dlSize, transferID int64, connectionID string) error\n\tremoveActiveTransfer(transferID int64, connectionID string) error\n\tcleanupActiveTransfers(before time.Time) error\n\tgetActiveTransfers(from time.Time) ([]ActiveTransfer, error)\n\taddSharedSession(session Session) error\n\tdeleteSharedSession(key string, sessionType SessionType) error\n\tgetSharedSession(key string, sessionType SessionType) (Session, error)\n\tcleanupSharedSessions(sessionType SessionType, before int64) error\n\tgetEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error)\n\tdumpEventActions() ([]BaseEventAction, error)\n\teventActionExists(name string) (BaseEventAction, error)\n\taddEventAction(action *BaseEventAction) error\n\tupdateEventAction(action *BaseEventAction) error\n\tdeleteEventAction(action BaseEventAction) error\n\tgetEventRules(limit, offset int, order string) ([]EventRule, error)\n\tdumpEventRules() ([]EventRule, error)\n\tgetRecentlyUpdatedRules(after int64) ([]EventRule, error)\n\teventRuleExists(name string) (EventRule, error)\n\taddEventRule(rule *EventRule) error\n\tupdateEventRule(rule *EventRule) error\n\tdeleteEventRule(rule EventRule, softDelete bool) error\n\tgetTaskByName(name string) (Task, error)\n\taddTask(name string) error\n\tupdateTask(name string, version int64) error\n\tupdateTaskTimestamp(name string) error\n\tsetFirstDownloadTimestamp(username string) error\n\tsetFirstUploadTimestamp(username string) error\n\taddNode() error\n\tgetNodeByName(name string) (Node, error)\n\tgetNodes() ([]Node, error)\n\tupdateNodeTimestamp() error\n\tcleanupNodes() error\n\troleExists(name string) (Role, error)\n\taddRole(role *Role) error\n\tupdateRole(role *Role) error\n\tdeleteRole(role Role) error\n\tgetRoles(limit int, offset int, order string, minimal bool) ([]Role, error)\n\tdumpRoles() ([]Role, error)\n\tipListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error)\n\taddIPListEntry(entry *IPListEntry) error\n\tupdateIPListEntry(entry *IPListEntry) error\n\tdeleteIPListEntry(entry IPListEntry, softDelete bool) error\n\tgetIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error)\n\tgetRecentlyUpdatedIPListEntries(after int64) ([]IPListEntry, error)\n\tdumpIPListEntries() ([]IPListEntry, error)\n\tcountIPListEntries(listType IPListType) (int64, error)\n\tgetListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error)\n\tgetConfigs() (Configs, error)\n\tsetConfigs(configs *Configs) error\n\tcheckAvailability() error\n\tclose() error\n\treloadConfig() error\n\tinitializeDatabase() error\n\tmigrateDatabase() error\n\trevertDatabase(targetVersion int) error\n\tresetDatabase() error\n}\n\n// SetAllowSelfConnections sets the desired behaviour for self connections\nfunc SetAllowSelfConnections(value int) {\n\tallowSelfConnections = value\n}\n\n// SetTempPath sets the path for temporary files\nfunc SetTempPath(fsPath string) {\n\ttempPath = fsPath\n}\n\nfunc checkSharedMode() {\n\tif !slices.Contains(sharedProviders, config.Driver) {\n\t\tconfig.IsShared = 0\n\t}\n}\n\n// Initialize the data provider.\n// An error is returned if the configured driver is invalid or if the data provider cannot be initialized\nfunc Initialize(cnf Config, basePath string, checkAdmins bool) error {\n\tconfig = cnf\n\tcheckSharedMode()\n\tconfig.Actions.ExecuteOn = util.RemoveDuplicates(config.Actions.ExecuteOn, true)\n\tconfig.Actions.ExecuteFor = util.RemoveDuplicates(config.Actions.ExecuteFor, true)\n\n\tcnf.BackupsPath = getConfigPath(cnf.BackupsPath, basePath)\n\tif cnf.BackupsPath == \"\" {\n\t\treturn fmt.Errorf(\"required directory is invalid, backup path %q\", cnf.BackupsPath)\n\t}\n\tabsoluteBackupPath, err := util.GetAbsolutePath(cnf.BackupsPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get absolute backup path: %w\", err)\n\t}\n\tconfig.BackupsPath = absoluteBackupPath\n\n\tif err := initializeHashingAlgo(&cnf); err != nil {\n\t\treturn err\n\t}\n\tif err := validateHooks(); err != nil {\n\t\treturn err\n\t}\n\tif err := createProvider(basePath); err != nil {\n\t\treturn err\n\t}\n\tif err := checkDatabase(checkAdmins); err != nil {\n\t\treturn err\n\t}\n\tadmins, err := provider.getAdmins(1, 0, OrderASC)\n\tif err != nil {\n\t\treturn err\n\t}\n\tisAdminCreated.Store(len(admins) > 0)\n\tif err := config.Node.validate(); err != nil {\n\t\treturn err\n\t}\n\tdelayedQuotaUpdater.start()\n\tif currentNode != nil {\n\t\tconfig.BackupsPath = filepath.Join(config.BackupsPath, currentNode.Name)\n\t}\n\tproviderLog(logger.LevelDebug, \"absolute backup path %q\", config.BackupsPath)\n\treturn startScheduler()\n}\n\nfunc checkDatabase(checkAdmins bool) error {\n\tif config.UpdateMode == 0 {\n\t\terr := provider.initializeDatabase()\n\t\tif err != nil && err != ErrNoInitRequired {\n\t\t\tlogger.WarnToConsole(\"unable to initialize data provider: %v\", err)\n\t\t\tproviderLog(logger.LevelError, \"unable to initialize data provider: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tif err == nil {\n\t\t\tlogger.DebugToConsole(\"data provider successfully initialized\")\n\t\t\tproviderLog(logger.LevelInfo, \"data provider successfully initialized\")\n\t\t}\n\t\terr = provider.migrateDatabase()\n\t\tif err != nil && err != ErrNoInitRequired {\n\t\t\tproviderLog(logger.LevelError, \"database migration error: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\tif checkAdmins && config.CreateDefaultAdmin {\n\t\t\terr = checkDefaultAdmin()\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"erro checking the default admin: %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tproviderLog(logger.LevelInfo, \"database initialization/migration skipped, manual mode is configured\")\n\t}\n\treturn nil\n}\n\nfunc validateHooks() error {\n\tvar hooks []string\n\tif config.PreLoginHook != \"\" && !strings.HasPrefix(config.PreLoginHook, \"http\") {\n\t\thooks = append(hooks, config.PreLoginHook)\n\t}\n\tif config.ExternalAuthHook != \"\" && !strings.HasPrefix(config.ExternalAuthHook, \"http\") {\n\t\thooks = append(hooks, config.ExternalAuthHook)\n\t}\n\tif config.PostLoginHook != \"\" && !strings.HasPrefix(config.PostLoginHook, \"http\") {\n\t\thooks = append(hooks, config.PostLoginHook)\n\t}\n\tif config.CheckPasswordHook != \"\" && !strings.HasPrefix(config.CheckPasswordHook, \"http\") {\n\t\thooks = append(hooks, config.CheckPasswordHook)\n\t}\n\n\tfor _, hook := range hooks {\n\t\tif !filepath.IsAbs(hook) {\n\t\t\treturn fmt.Errorf(\"invalid hook: %q must be an absolute path\", hook)\n\t\t}\n\t\t_, err := os.Stat(hook)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"invalid hook: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// GetBackupsPath returns the normalized backups path\nfunc GetBackupsPath() string {\n\treturn config.BackupsPath\n}\n\n// GetProviderFromValue returns the FilesystemProvider matching the specified value.\n// If no match is found LocalFilesystemProvider is returned.\nfunc GetProviderFromValue(value string) sdk.FilesystemProvider {\n\tval, err := strconv.Atoi(value)\n\tif err != nil {\n\t\treturn sdk.LocalFilesystemProvider\n\t}\n\tresult := sdk.FilesystemProvider(val)\n\tif sdk.IsProviderSupported(result) {\n\t\treturn result\n\t}\n\treturn sdk.LocalFilesystemProvider\n}\n\nfunc initializeHashingAlgo(cnf *Config) error {\n\tparallelism := cnf.PasswordHashing.Argon2Options.Parallelism\n\tif parallelism == 0 {\n\t\tparallelism = uint8(runtime.NumCPU())\n\t}\n\targon2Params = &argon2id.Params{\n\t\tMemory:      cnf.PasswordHashing.Argon2Options.Memory,\n\t\tIterations:  cnf.PasswordHashing.Argon2Options.Iterations,\n\t\tParallelism: parallelism,\n\t\tSaltLength:  16,\n\t\tKeyLength:   32,\n\t}\n\n\tif config.PasswordHashing.Algo == HashingAlgoBcrypt {\n\t\tif config.PasswordHashing.BcryptOptions.Cost > bcrypt.MaxCost {\n\t\t\terr := fmt.Errorf(\"invalid bcrypt cost %v, max allowed %v\", config.PasswordHashing.BcryptOptions.Cost, bcrypt.MaxCost)\n\t\t\tlogger.WarnToConsole(\"Unable to initialize data provider: %v\", err)\n\t\t\tproviderLog(logger.LevelError, \"Unable to initialize data provider: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateSQLTablesPrefix() error {\n\tinitSQLTables()\n\tif config.SQLTablesPrefix != \"\" {\n\t\tfor _, char := range config.SQLTablesPrefix {\n\t\t\tif !strings.Contains(sqlPrefixValidChars, strings.ToLower(string(char))) {\n\t\t\t\treturn errors.New(\"invalid sql_tables_prefix only chars in range 'a..z', 'A..Z', '0-9' and '_' are allowed\")\n\t\t\t}\n\t\t}\n\t\tsqlTableUsers = config.SQLTablesPrefix + sqlTableUsers\n\t\tsqlTableFolders = config.SQLTablesPrefix + sqlTableFolders\n\t\tsqlTableUsersFoldersMapping = config.SQLTablesPrefix + sqlTableUsersFoldersMapping\n\t\tsqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins\n\t\tsqlTableAPIKeys = config.SQLTablesPrefix + sqlTableAPIKeys\n\t\tsqlTableShares = config.SQLTablesPrefix + sqlTableShares\n\t\tsqlTableSharesGroupsMapping = config.SQLTablesPrefix + sqlTableSharesGroupsMapping\n\t\tsqlTableDefenderEvents = config.SQLTablesPrefix + sqlTableDefenderEvents\n\t\tsqlTableDefenderHosts = config.SQLTablesPrefix + sqlTableDefenderHosts\n\t\tsqlTableActiveTransfers = config.SQLTablesPrefix + sqlTableActiveTransfers\n\t\tsqlTableGroups = config.SQLTablesPrefix + sqlTableGroups\n\t\tsqlTableUsersGroupsMapping = config.SQLTablesPrefix + sqlTableUsersGroupsMapping\n\t\tsqlTableAdminsGroupsMapping = config.SQLTablesPrefix + sqlTableAdminsGroupsMapping\n\t\tsqlTableGroupsFoldersMapping = config.SQLTablesPrefix + sqlTableGroupsFoldersMapping\n\t\tsqlTableSharedSessions = config.SQLTablesPrefix + sqlTableSharedSessions\n\t\tsqlTableEventsActions = config.SQLTablesPrefix + sqlTableEventsActions\n\t\tsqlTableEventsRules = config.SQLTablesPrefix + sqlTableEventsRules\n\t\tsqlTableRulesActionsMapping = config.SQLTablesPrefix + sqlTableRulesActionsMapping\n\t\tsqlTableTasks = config.SQLTablesPrefix + sqlTableTasks\n\t\tsqlTableNodes = config.SQLTablesPrefix + sqlTableNodes\n\t\tsqlTableRoles = config.SQLTablesPrefix + sqlTableRoles\n\t\tsqlTableIPLists = config.SQLTablesPrefix + sqlTableIPLists\n\t\tsqlTableConfigs = config.SQLTablesPrefix + sqlTableConfigs\n\t\tsqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion\n\t\tproviderLog(logger.LevelDebug, \"sql table for users %q, folders %q users folders mapping %q admins %q \"+\n\t\t\t\"api keys %q shares %q defender hosts %q defender events %q transfers %q  groups %q \"+\n\t\t\t\"users groups mapping %q admins groups mapping %q groups folders mapping %q shared sessions %q \"+\n\t\t\t\"schema version %q events actions %q events rules %q rules actions mapping %q tasks %q nodes %q roles %q\"+\n\t\t\t\"ip lists %q share groups mapping %q configs %q\",\n\t\t\tsqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,\n\t\t\tsqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,\n\t\t\tsqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions,\n\t\t\tsqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping,\n\t\t\tsqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists, sqlTableSharesGroupsMapping, sqlTableConfigs)\n\t}\n\treturn nil\n}\n\nfunc checkDefaultAdmin() error {\n\tadmins, err := provider.getAdmins(1, 0, OrderASC)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(admins) > 0 {\n\t\treturn nil\n\t}\n\tlogger.Debug(logSender, \"\", \"no admins found, try to create the default one\")\n\t// we need to create the default admin\n\tadmin := &Admin{}\n\tif err := admin.setFromEnv(); err != nil {\n\t\treturn err\n\t}\n\treturn provider.addAdmin(admin)\n}\n\n// InitializeDatabase creates the initial database structure\nfunc InitializeDatabase(cnf Config, basePath string) error {\n\tconfig = cnf\n\n\tif err := initializeHashingAlgo(&cnf); err != nil {\n\t\treturn err\n\t}\n\n\terr := createProvider(basePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.initializeDatabase()\n\tif err != nil && err != ErrNoInitRequired {\n\t\treturn err\n\t}\n\treturn provider.migrateDatabase()\n}\n\n// RevertDatabase restores schema and/or data to a previous version\nfunc RevertDatabase(cnf Config, basePath string, targetVersion int) error {\n\tconfig = cnf\n\n\terr := createProvider(basePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.initializeDatabase()\n\tif err != nil && err != ErrNoInitRequired {\n\t\treturn err\n\t}\n\treturn provider.revertDatabase(targetVersion)\n}\n\n// ResetDatabase restores schema and/or data to a previous version\nfunc ResetDatabase(cnf Config, basePath string) error {\n\tconfig = cnf\n\n\tif err := createProvider(basePath); err != nil {\n\t\treturn err\n\t}\n\treturn provider.resetDatabase()\n}\n\n// CheckAdminAndPass validates the given admin and password connecting from ip\nfunc CheckAdminAndPass(username, password, ip string) (Admin, error) {\n\tusername = config.convertName(username)\n\treturn provider.validateAdminAndPass(username, password, ip)\n}\n\n// CheckCachedUserCredentials checks the credentials for a cached user\nfunc CheckCachedUserCredentials(user *CachedUser, password, ip, loginMethod, protocol string, tlsCert *x509.Certificate) (*CachedUser, *User, error) {\n\tif !user.User.skipExternalAuth() && isExternalAuthConfigured(loginMethod) {\n\t\tu, _, err := CheckCompositeCredentials(user.User.Username, password, ip, loginMethod, protocol, tlsCert)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\twebDAVUsersCache.swap(&u, password)\n\t\tcu, _ := webDAVUsersCache.get(u.Username)\n\t\treturn cu, &u, nil\n\t}\n\tif err := user.User.CheckLoginConditions(); err != nil {\n\t\treturn user, nil, err\n\t}\n\tif loginMethod == LoginMethodPassword && user.User.Filters.IsAnonymous {\n\t\treturn user, nil, nil\n\t}\n\tif loginMethod != LoginMethodPassword {\n\t\t_, err := checkUserAndTLSCertificate(&user.User, protocol, tlsCert)\n\t\tif err != nil {\n\t\t\treturn user, nil, err\n\t\t}\n\t\tif loginMethod == LoginMethodTLSCertificate {\n\t\t\tif !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol) {\n\t\t\t\treturn user, nil, fmt.Errorf(\"certificate login method is not allowed for user %q\", user.User.Username)\n\t\t\t}\n\t\t\treturn user, nil, nil\n\t\t}\n\t}\n\tif password == \"\" {\n\t\treturn user, nil, ErrInvalidCredentials\n\t}\n\tif user.Password != \"\" {\n\t\tif password == user.Password {\n\t\t\treturn user, nil, nil\n\t\t}\n\t} else {\n\t\tif ok, _ := isPasswordOK(&user.User, password); ok {\n\t\t\treturn user, nil, nil\n\t\t}\n\t}\n\treturn user, nil, ErrInvalidCredentials\n}\n\n// CheckCompositeCredentials checks multiple credentials.\n// WebDAV users can send both a password and a TLS certificate within the same request\nfunc CheckCompositeCredentials(username, password, ip, loginMethod, protocol string, tlsCert *x509.Certificate) (User, string, error) {\n\tusername = config.convertName(username)\n\tif loginMethod == LoginMethodPassword {\n\t\tuser, err := CheckUserAndPass(username, password, ip, protocol)\n\t\treturn user, loginMethod, err\n\t}\n\tuser, err := CheckUserBeforeTLSAuth(username, ip, protocol, tlsCert)\n\tif err != nil {\n\t\treturn user, loginMethod, err\n\t}\n\tif !user.IsTLSVerificationEnabled() {\n\t\t// for backward compatibility with 2.0.x we only check the password and change the login method here\n\t\t// in future updates we have to return an error\n\t\tuser, err := CheckUserAndPass(username, password, ip, protocol)\n\t\treturn user, LoginMethodPassword, err\n\t}\n\tuser, err = checkUserAndTLSCertificate(&user, protocol, tlsCert)\n\tif err != nil {\n\t\treturn user, loginMethod, err\n\t}\n\tif loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, protocol) {\n\t\treturn user, loginMethod, fmt.Errorf(\"certificate login method is not allowed for user %q\", user.Username)\n\t}\n\tif loginMethod == LoginMethodTLSCertificateAndPwd {\n\t\tif plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {\n\t\t\tuser, err = doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword)\n\t\t} else if config.ExternalAuthHook != \"\" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {\n\t\t\tuser, err = doExternalAuth(username, password, nil, \"\", ip, protocol, nil)\n\t\t} else if config.PreLoginHook != \"\" {\n\t\t\tuser, err = executePreLoginHook(username, LoginMethodPassword, ip, protocol, nil)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn user, loginMethod, err\n\t\t}\n\t\tuser, err = checkUserAndPass(&user, password, ip, protocol)\n\t}\n\treturn user, loginMethod, err\n}\n\n// CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS\nfunc CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {\n\tusername = config.convertName(username)\n\tif plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) {\n\t\tuser, err := doPluginAuth(username, \"\", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\terr = user.LoadAndApplyGroupSettings()\n\t\treturn user, err\n\t}\n\tif config.ExternalAuthHook != \"\" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {\n\t\tuser, err := doExternalAuth(username, \"\", nil, \"\", ip, protocol, tlsCert)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\terr = user.LoadAndApplyGroupSettings()\n\t\treturn user, err\n\t}\n\tif config.PreLoginHook != \"\" {\n\t\tuser, err := executePreLoginHook(username, LoginMethodTLSCertificate, ip, protocol, nil)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\terr = user.LoadAndApplyGroupSettings()\n\t\treturn user, err\n\t}\n\tuser, err := UserExists(username, \"\")\n\tif err != nil {\n\t\treturn user, err\n\t}\n\terr = user.LoadAndApplyGroupSettings()\n\treturn user, err\n}\n\n// CheckUserAndTLSCert returns the SFTPGo user with the given username and check if the\n// given TLS certificate allow authentication without password\nfunc CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {\n\tusername = config.convertName(username)\n\tif plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) {\n\t\tuser, err := doPluginAuth(username, \"\", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\treturn checkUserAndTLSCertificate(&user, protocol, tlsCert)\n\t}\n\tif config.ExternalAuthHook != \"\" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {\n\t\tuser, err := doExternalAuth(username, \"\", nil, \"\", ip, protocol, tlsCert)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\treturn checkUserAndTLSCertificate(&user, protocol, tlsCert)\n\t}\n\tif config.PreLoginHook != \"\" {\n\t\tuser, err := executePreLoginHook(username, LoginMethodTLSCertificate, ip, protocol, nil)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\treturn checkUserAndTLSCertificate(&user, protocol, tlsCert)\n\t}\n\treturn provider.validateUserAndTLSCert(username, protocol, tlsCert)\n}\n\n// CheckUserAndPass retrieves the SFTPGo user with the given username and password if a match is found or an error\nfunc CheckUserAndPass(username, password, ip, protocol string) (User, error) {\n\tusername = config.convertName(username)\n\tif plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {\n\t\tuser, err := doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\treturn checkUserAndPass(&user, password, ip, protocol)\n\t}\n\tif config.ExternalAuthHook != \"\" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {\n\t\tuser, err := doExternalAuth(username, password, nil, \"\", ip, protocol, nil)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\treturn checkUserAndPass(&user, password, ip, protocol)\n\t}\n\tif config.PreLoginHook != \"\" {\n\t\tuser, err := executePreLoginHook(username, LoginMethodPassword, ip, protocol, nil)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\treturn checkUserAndPass(&user, password, ip, protocol)\n\t}\n\treturn provider.validateUserAndPass(username, password, ip, protocol)\n}\n\n// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error\nfunc CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string, isSSHCert bool) (User, string, error) {\n\tusername = config.convertName(username)\n\tif plugin.Handler.HasAuthScope(plugin.AuthScopePublicKey) {\n\t\tuser, err := doPluginAuth(username, \"\", pubKey, ip, protocol, nil, plugin.AuthScopePublicKey)\n\t\tif err != nil {\n\t\t\treturn user, \"\", err\n\t\t}\n\t\treturn checkUserAndPubKey(&user, pubKey, isSSHCert)\n\t}\n\tif config.ExternalAuthHook != \"\" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {\n\t\tuser, err := doExternalAuth(username, \"\", pubKey, \"\", ip, protocol, nil)\n\t\tif err != nil {\n\t\t\treturn user, \"\", err\n\t\t}\n\t\treturn checkUserAndPubKey(&user, pubKey, isSSHCert)\n\t}\n\tif config.PreLoginHook != \"\" {\n\t\tuser, err := executePreLoginHook(username, SSHLoginMethodPublicKey, ip, protocol, nil)\n\t\tif err != nil {\n\t\t\treturn user, \"\", err\n\t\t}\n\t\treturn checkUserAndPubKey(&user, pubKey, isSSHCert)\n\t}\n\treturn provider.validateUserAndPubKey(username, pubKey, isSSHCert)\n}\n\n// CheckKeyboardInteractiveAuth checks the keyboard interactive authentication and returns\n// the authenticated user or an error\nfunc CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge,\n\tip, protocol string, isPartialAuth bool,\n) (User, error) {\n\tvar user User\n\tvar err error\n\tusername = config.convertName(username)\n\tif plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {\n\t\tuser, err = doPluginAuth(username, \"\", nil, ip, protocol, nil, plugin.AuthScopeKeyboardInteractive)\n\t} else if config.ExternalAuthHook != \"\" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {\n\t\tuser, err = doExternalAuth(username, \"\", nil, \"1\", ip, protocol, nil)\n\t} else if config.PreLoginHook != \"\" {\n\t\tuser, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol, nil)\n\t} else {\n\t\tuser, err = provider.userExists(username, \"\")\n\t}\n\tif err != nil {\n\t\treturn user, err\n\t}\n\treturn doKeyboardInteractiveAuth(&user, authHook, client, ip, protocol, isPartialAuth)\n}\n\n// GetFTPPreAuthUser returns the SFTPGo user with the specified username\n// after receiving the FTP \"USER\" command.\n// If a pre-login hook is defined it will be executed so the SFTPGo user\n// can be created if it does not exist\nfunc GetFTPPreAuthUser(username, ip string) (User, error) {\n\tvar user User\n\tvar err error\n\tif config.PreLoginHook != \"\" {\n\t\tuser, err = executePreLoginHook(username, \"\", ip, protocolFTP, nil)\n\t} else {\n\t\tuser, err = UserExists(username, \"\")\n\t}\n\tif err != nil {\n\t\treturn user, err\n\t}\n\terr = user.LoadAndApplyGroupSettings()\n\treturn user, err\n}\n\n// GetUserAfterIDPAuth returns the SFTPGo user with the specified username\n// after a successful authentication with an external identity provider.\n// If a pre-login hook is defined it will be executed so the SFTPGo user\n// can be created if it does not exist\nfunc GetUserAfterIDPAuth(username, ip, protocol string, oidcTokenFields *map[string]any) (User, error) {\n\tvar user User\n\tvar err error\n\tif config.PreLoginHook != \"\" {\n\t\tuser, err = executePreLoginHook(username, LoginMethodIDP, ip, protocol, oidcTokenFields)\n\t\tuser.Filters.RequirePasswordChange = false\n\t} else {\n\t\tuser, err = UserExists(username, \"\")\n\t}\n\tif err != nil {\n\t\treturn user, err\n\t}\n\terr = user.LoadAndApplyGroupSettings()\n\treturn user, err\n}\n\n// GetDefenderHosts returns hosts that are banned or for which some violations have been detected\nfunc GetDefenderHosts(from int64, limit int) ([]DefenderEntry, error) {\n\treturn provider.getDefenderHosts(from, limit)\n}\n\n// GetDefenderHostByIP returns a defender host by ip, if any\nfunc GetDefenderHostByIP(ip string, from int64) (DefenderEntry, error) {\n\treturn provider.getDefenderHostByIP(ip, from)\n}\n\n// IsDefenderHostBanned returns a defender entry and no error if the specified host is banned\nfunc IsDefenderHostBanned(ip string) (DefenderEntry, error) {\n\treturn provider.isDefenderHostBanned(ip)\n}\n\n// UpdateDefenderBanTime increments ban time for the specified ip\nfunc UpdateDefenderBanTime(ip string, minutes int) error {\n\treturn provider.updateDefenderBanTime(ip, minutes)\n}\n\n// DeleteDefenderHost removes the specified IP from the defender lists\nfunc DeleteDefenderHost(ip string) error {\n\treturn provider.deleteDefenderHost(ip)\n}\n\n// AddDefenderEvent adds an event for the given IP with the given score\n// and returns the host with the updated score\nfunc AddDefenderEvent(ip string, score int, from int64) (DefenderEntry, error) {\n\tif err := provider.addDefenderEvent(ip, score); err != nil {\n\t\treturn DefenderEntry{}, err\n\t}\n\treturn provider.getDefenderHostByIP(ip, from)\n}\n\n// SetDefenderBanTime sets the ban time for the specified IP\nfunc SetDefenderBanTime(ip string, banTime int64) error {\n\treturn provider.setDefenderBanTime(ip, banTime)\n}\n\n// CleanupDefender removes events and hosts older than \"from\" from the data provider\nfunc CleanupDefender(from int64) error {\n\treturn provider.cleanupDefender(from)\n}\n\n// UpdateShareLastUse updates the LastUseAt and UsedTokens for the given share\nfunc UpdateShareLastUse(share *Share, numTokens int) error {\n\treturn provider.updateShareLastUse(share.ShareID, numTokens)\n}\n\n// UpdateAPIKeyLastUse updates the LastUseAt field for the given API key\nfunc UpdateAPIKeyLastUse(apiKey *APIKey) error {\n\tlastUse := util.GetTimeFromMsecSinceEpoch(apiKey.LastUseAt)\n\tdiff := -time.Until(lastUse)\n\tif diff < 0 || diff > lastLoginMinDelay {\n\t\treturn provider.updateAPIKeyLastUse(apiKey.KeyID)\n\t}\n\treturn nil\n}\n\n// UpdateLastLogin updates the last login field for the given SFTPGo user\nfunc UpdateLastLogin(user *User) {\n\tdelay := lastLoginMinDelay\n\tif user.Filters.ExternalAuthCacheTime > 0 {\n\t\tdelay = time.Duration(user.Filters.ExternalAuthCacheTime) * time.Second\n\t}\n\tif user.LastLogin <= user.UpdatedAt || !isLastActivityRecent(user.LastLogin, delay) {\n\t\terr := provider.updateLastLogin(user.Username)\n\t\tif err == nil {\n\t\t\twebDAVUsersCache.updateLastLogin(user.Username)\n\t\t}\n\t}\n}\n\n// UpdateAdminLastLogin updates the last login field for the given SFTPGo admin\nfunc UpdateAdminLastLogin(admin *Admin) {\n\tif !isLastActivityRecent(admin.LastLogin, lastLoginMinDelay) {\n\t\tprovider.updateAdminLastLogin(admin.Username) //nolint:errcheck\n\t}\n}\n\n// UpdateUserQuota updates the quota for the given SFTPGo user adding filesAdd and sizeAdd.\n// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.\nfunc UpdateUserQuota(user *User, filesAdd int, sizeAdd int64, reset bool) error {\n\tif config.TrackQuota == 0 {\n\t\treturn util.NewMethodDisabledError(trackQuotaDisabledError)\n\t} else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() {\n\t\treturn nil\n\t}\n\tif filesAdd == 0 && sizeAdd == 0 && !reset {\n\t\treturn nil\n\t}\n\tif config.DelayedQuotaUpdate == 0 || reset {\n\t\tif reset {\n\t\t\tdelayedQuotaUpdater.resetUserQuota(user.Username)\n\t\t}\n\t\treturn provider.updateQuota(user.Username, filesAdd, sizeAdd, reset)\n\t}\n\tdelayedQuotaUpdater.updateUserQuota(user.Username, filesAdd, sizeAdd)\n\treturn nil\n}\n\n// UpdateUserFolderQuota updates the quota for the given user and virtual folder.\nfunc UpdateUserFolderQuota(folder *vfs.VirtualFolder, user *User, filesAdd int, sizeAdd int64, reset bool) {\n\tif folder.IsIncludedInUserQuota() {\n\t\tUpdateUserQuota(user, filesAdd, sizeAdd, reset) //nolint:errcheck\n\t\treturn\n\t}\n\tUpdateVirtualFolderQuota(&folder.BaseVirtualFolder, filesAdd, sizeAdd, reset) //nolint:errcheck\n}\n\n// UpdateVirtualFolderQuota updates the quota for the given virtual folder adding filesAdd and sizeAdd.\n// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.\nfunc UpdateVirtualFolderQuota(vfolder *vfs.BaseVirtualFolder, filesAdd int, sizeAdd int64, reset bool) error {\n\tif config.TrackQuota == 0 {\n\t\treturn util.NewMethodDisabledError(trackQuotaDisabledError)\n\t}\n\tif filesAdd == 0 && sizeAdd == 0 && !reset {\n\t\treturn nil\n\t}\n\tif config.DelayedQuotaUpdate == 0 || reset {\n\t\tif reset {\n\t\t\tdelayedQuotaUpdater.resetFolderQuota(vfolder.Name)\n\t\t}\n\t\treturn provider.updateFolderQuota(vfolder.Name, filesAdd, sizeAdd, reset)\n\t}\n\tdelayedQuotaUpdater.updateFolderQuota(vfolder.Name, filesAdd, sizeAdd)\n\treturn nil\n}\n\n// UpdateUserTransferQuota updates the transfer quota for the given SFTPGo user.\n// If reset is true uploadSize and downloadSize indicates the actual sizes instead of the difference.\nfunc UpdateUserTransferQuota(user *User, uploadSize, downloadSize int64, reset bool) error {\n\tif config.TrackQuota == 0 {\n\t\treturn util.NewMethodDisabledError(trackQuotaDisabledError)\n\t} else if config.TrackQuota == 2 && !reset && !user.HasTransferQuotaRestrictions() {\n\t\treturn nil\n\t}\n\tif downloadSize == 0 && uploadSize == 0 && !reset {\n\t\treturn nil\n\t}\n\tif config.DelayedQuotaUpdate == 0 || reset {\n\t\tif reset {\n\t\t\tdelayedQuotaUpdater.resetUserTransferQuota(user.Username)\n\t\t}\n\t\treturn provider.updateTransferQuota(user.Username, uploadSize, downloadSize, reset)\n\t}\n\tdelayedQuotaUpdater.updateUserTransferQuota(user.Username, uploadSize, downloadSize)\n\treturn nil\n}\n\n// UpdateUserTransferTimestamps updates the first download/upload fields if unset\nfunc UpdateUserTransferTimestamps(username string, isUpload bool) error {\n\tif isUpload {\n\t\terr := provider.setFirstUploadTimestamp(username)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"unable to set first upload: %v\", err)\n\t\t}\n\t\treturn err\n\t}\n\terr := provider.setFirstDownloadTimestamp(username)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"unable to set first download: %v\", err)\n\t}\n\treturn err\n}\n\n// GetUsedQuota returns the used quota for the given SFTPGo user.\nfunc GetUsedQuota(username string) (int, int64, int64, int64, error) {\n\tif config.TrackQuota == 0 {\n\t\treturn 0, 0, 0, 0, util.NewMethodDisabledError(trackQuotaDisabledError)\n\t}\n\tfiles, size, ulTransferSize, dlTransferSize, err := provider.getUsedQuota(username)\n\tif err != nil {\n\t\treturn files, size, ulTransferSize, dlTransferSize, err\n\t}\n\tdelayedFiles, delayedSize := delayedQuotaUpdater.getUserPendingQuota(username)\n\tdelayedUlTransferSize, delayedDLTransferSize := delayedQuotaUpdater.getUserPendingTransferQuota(username)\n\n\treturn files + delayedFiles, size + delayedSize, ulTransferSize + delayedUlTransferSize,\n\t\tdlTransferSize + delayedDLTransferSize, err\n}\n\n// GetUsedVirtualFolderQuota returns the used quota for the given virtual folder.\nfunc GetUsedVirtualFolderQuota(name string) (int, int64, error) {\n\tif config.TrackQuota == 0 {\n\t\treturn 0, 0, util.NewMethodDisabledError(trackQuotaDisabledError)\n\t}\n\tfiles, size, err := provider.getUsedFolderQuota(name)\n\tif err != nil {\n\t\treturn files, size, err\n\t}\n\tdelayedFiles, delayedSize := delayedQuotaUpdater.getFolderPendingQuota(name)\n\treturn files + delayedFiles, size + delayedSize, err\n}\n\n// GetConfigs returns the configurations\nfunc GetConfigs() (Configs, error) {\n\treturn provider.getConfigs()\n}\n\n// UpdateConfigs updates configurations\nfunc UpdateConfigs(configs *Configs, executor, ipAddress, role string) error {\n\tif configs == nil {\n\t\tconfigs = &Configs{}\n\t} else {\n\t\tconfigs.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t}\n\terr := provider.setConfigs(configs)\n\tif err == nil {\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectConfigs, \"configs\", role, configs)\n\t}\n\treturn err\n}\n\n// AddShare adds a new share\nfunc AddShare(share *Share, executor, ipAddress, role string) error {\n\terr := provider.addShare(share)\n\tif err == nil {\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectShare, share.ShareID, role, share)\n\t}\n\treturn err\n}\n\n// UpdateShare updates an existing share\nfunc UpdateShare(share *Share, executor, ipAddress, role string) error {\n\terr := provider.updateShare(share)\n\tif err == nil {\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectShare, share.ShareID, role, share)\n\t}\n\treturn err\n}\n\n// DeleteShare deletes an existing share\nfunc DeleteShare(shareID string, executor, ipAddress, role string) error {\n\tshare, err := provider.shareExists(shareID, executor)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.deleteShare(share)\n\tif err == nil {\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectShare, shareID, role, &share)\n\t}\n\treturn err\n}\n\n// ShareExists returns the share with the given ID if it exists\nfunc ShareExists(shareID, username string) (Share, error) {\n\tif shareID == \"\" {\n\t\treturn Share{}, util.NewRecordNotFoundError(fmt.Sprintf(\"Share %q does not exist\", shareID))\n\t}\n\treturn provider.shareExists(shareID, username)\n}\n\n// AddIPListEntry adds a new IP list entry\nfunc AddIPListEntry(entry *IPListEntry, executor, ipAddress, executorRole string) error {\n\terr := provider.addIPListEntry(entry)\n\tif err == nil {\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectIPListEntry, entry.getName(), executorRole, entry)\n\t\tfor _, l := range inMemoryLists {\n\t\t\tl.addEntry(entry)\n\t\t}\n\t}\n\treturn err\n}\n\n// UpdateIPListEntry updates an existing IP list entry\nfunc UpdateIPListEntry(entry *IPListEntry, executor, ipAddress, executorRole string) error {\n\terr := provider.updateIPListEntry(entry)\n\tif err == nil {\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectIPListEntry, entry.getName(), executorRole, entry)\n\t\tfor _, l := range inMemoryLists {\n\t\t\tl.updateEntry(entry)\n\t\t}\n\t}\n\treturn err\n}\n\n// DeleteIPListEntry deletes an existing IP list entry\nfunc DeleteIPListEntry(ipOrNet string, listType IPListType, executor, ipAddress, executorRole string) error {\n\tentry, err := provider.ipListEntryExists(ipOrNet, listType)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.deleteIPListEntry(entry, config.IsShared == 1)\n\tif err == nil {\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectIPListEntry, entry.getName(), executorRole, &entry)\n\t\tfor _, l := range inMemoryLists {\n\t\t\tl.removeEntry(&entry)\n\t\t}\n\t}\n\treturn err\n}\n\n// IPListEntryExists returns the IP list entry with the given IP/net and type if it exists\nfunc IPListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error) {\n\treturn provider.ipListEntryExists(ipOrNet, listType)\n}\n\n// GetIPListEntries returns the IP list entries applying the specified criteria and search limit\nfunc GetIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {\n\tif !slices.Contains(supportedIPListType, listType) {\n\t\treturn nil, util.NewValidationError(fmt.Sprintf(\"invalid list type %d\", listType))\n\t}\n\treturn provider.getIPListEntries(listType, filter, from, order, limit)\n}\n\n// AddRole adds a new role\nfunc AddRole(role *Role, executor, ipAddress, executorRole string) error {\n\trole.Name = config.convertName(role.Name)\n\terr := provider.addRole(role)\n\tif err == nil {\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectRole, role.Name, executorRole, role)\n\t}\n\treturn err\n}\n\n// UpdateRole updates an existing Role\nfunc UpdateRole(role *Role, executor, ipAddress, executorRole string) error {\n\terr := provider.updateRole(role)\n\tif err == nil {\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectRole, role.Name, executorRole, role)\n\t}\n\treturn err\n}\n\n// DeleteRole deletes an existing Role\nfunc DeleteRole(name string, executor, ipAddress, executorRole string) error {\n\tname = config.convertName(name)\n\trole, err := provider.roleExists(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(role.Admins) > 0 {\n\t\terrorString := fmt.Sprintf(\"the role %q is referenced, it cannot be removed\", role.Name)\n\t\treturn util.NewValidationError(errorString)\n\t}\n\terr = provider.deleteRole(role)\n\tif err == nil {\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectRole, role.Name, executorRole, &role)\n\t\tfor _, user := range role.Users {\n\t\t\tprovider.setUpdatedAt(user)\n\t\t\tu, err := provider.userExists(user, \"\")\n\t\t\tif err == nil {\n\t\t\t\twebDAVUsersCache.swap(&u, \"\")\n\t\t\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, u.Role, &u)\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}\n\n// RoleExists returns the Role with the given name if it exists\nfunc RoleExists(name string) (Role, error) {\n\tname = config.convertName(name)\n\treturn provider.roleExists(name)\n}\n\n// AddGroup adds a new group\nfunc AddGroup(group *Group, executor, ipAddress, role string) error {\n\tgroup.Name = config.convertName(group.Name)\n\terr := provider.addGroup(group)\n\tif err == nil {\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectGroup, group.Name, role, group)\n\t}\n\treturn err\n}\n\n// UpdateGroup updates an existing Group\nfunc UpdateGroup(group *Group, users []string, executor, ipAddress, role string) error {\n\terr := provider.updateGroup(group)\n\tif err == nil {\n\t\tfor _, user := range users {\n\t\t\tprovider.setUpdatedAt(user)\n\t\t\tu, err := provider.userExists(user, \"\")\n\t\t\tif err == nil {\n\t\t\t\twebDAVUsersCache.swap(&u, \"\")\n\t\t\t} else {\n\t\t\t\tRemoveCachedWebDAVUser(user)\n\t\t\t}\n\t\t}\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectGroup, group.Name, role, group)\n\t}\n\treturn err\n}\n\n// DeleteGroup deletes an existing Group\nfunc DeleteGroup(name string, executor, ipAddress, role string) error {\n\tname = config.convertName(name)\n\tgroup, err := provider.groupExists(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(group.Users) > 0 {\n\t\terrorString := fmt.Sprintf(\"the group %q is referenced, it cannot be removed\", group.Name)\n\t\treturn util.NewValidationError(errorString)\n\t}\n\terr = provider.deleteGroup(group)\n\tif err == nil {\n\t\tfor _, user := range group.Users {\n\t\t\tprovider.setUpdatedAt(user)\n\t\t\tu, err := provider.userExists(user, \"\")\n\t\t\tif err == nil {\n\t\t\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, u.Role, &u)\n\t\t\t}\n\t\t\tRemoveCachedWebDAVUser(user)\n\t\t}\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectGroup, group.Name, role, &group)\n\t}\n\treturn err\n}\n\n// GroupExists returns the Group with the given name if it exists\nfunc GroupExists(name string) (Group, error) {\n\tname = config.convertName(name)\n\treturn provider.groupExists(name)\n}\n\n// AddAPIKey adds a new API key\nfunc AddAPIKey(apiKey *APIKey, executor, ipAddress, role string) error {\n\terr := provider.addAPIKey(apiKey)\n\tif err == nil {\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, role, apiKey)\n\t}\n\treturn err\n}\n\n// UpdateAPIKey updates an existing API key\nfunc UpdateAPIKey(apiKey *APIKey, executor, ipAddress, role string) error {\n\terr := provider.updateAPIKey(apiKey)\n\tif err == nil {\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, role, apiKey)\n\t}\n\treturn err\n}\n\n// DeleteAPIKey deletes an existing API key\nfunc DeleteAPIKey(keyID string, executor, ipAddress, role string) error {\n\tapiKey, err := provider.apiKeyExists(keyID)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.deleteAPIKey(apiKey)\n\tif err == nil {\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectAPIKey, apiKey.KeyID, role, &apiKey)\n\t\tcachedAPIKeys.Remove(keyID)\n\t}\n\treturn err\n}\n\n// APIKeyExists returns the API key with the given ID if it exists\nfunc APIKeyExists(keyID string) (APIKey, error) {\n\tif keyID == \"\" {\n\t\treturn APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf(\"API key %q does not exist\", keyID))\n\t}\n\treturn provider.apiKeyExists(keyID)\n}\n\n// GetEventActions returns an array of event actions respecting limit and offset\nfunc GetEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {\n\treturn provider.getEventActions(limit, offset, order, minimal)\n}\n\n// EventActionExists returns the event action with the given name if it exists\nfunc EventActionExists(name string) (BaseEventAction, error) {\n\tname = config.convertName(name)\n\treturn provider.eventActionExists(name)\n}\n\n// AddEventAction adds a new event action\nfunc AddEventAction(action *BaseEventAction, executor, ipAddress, role string) error {\n\taction.Name = config.convertName(action.Name)\n\terr := provider.addEventAction(action)\n\tif err == nil {\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectEventAction, action.Name, role, action)\n\t}\n\treturn err\n}\n\n// UpdateEventAction updates an existing event action\nfunc UpdateEventAction(action *BaseEventAction, executor, ipAddress, role string) error {\n\terr := provider.updateEventAction(action)\n\tif err == nil {\n\t\tif fnReloadRules != nil {\n\t\t\tfnReloadRules()\n\t\t}\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectEventAction, action.Name, role, action)\n\t}\n\treturn err\n}\n\n// DeleteEventAction deletes an existing event action\nfunc DeleteEventAction(name string, executor, ipAddress, role string) error {\n\tname = config.convertName(name)\n\taction, err := provider.eventActionExists(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(action.Rules) > 0 {\n\t\terrorString := fmt.Sprintf(\"the event action %#q is referenced, it cannot be removed\", action.Name)\n\t\treturn util.NewValidationError(errorString)\n\t}\n\terr = provider.deleteEventAction(action)\n\tif err == nil {\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectEventAction, action.Name, role, &action)\n\t}\n\treturn err\n}\n\n// GetEventRules returns an array of event rules respecting limit and offset\nfunc GetEventRules(limit, offset int, order string) ([]EventRule, error) {\n\treturn provider.getEventRules(limit, offset, order)\n}\n\n// GetRecentlyUpdatedRules returns the event rules updated after the specified time\nfunc GetRecentlyUpdatedRules(after int64) ([]EventRule, error) {\n\treturn provider.getRecentlyUpdatedRules(after)\n}\n\n// EventRuleExists returns the event rule with the given name if it exists\nfunc EventRuleExists(name string) (EventRule, error) {\n\tname = config.convertName(name)\n\treturn provider.eventRuleExists(name)\n}\n\n// AddEventRule adds a new event rule\nfunc AddEventRule(rule *EventRule, executor, ipAddress, role string) error {\n\trule.Name = config.convertName(rule.Name)\n\terr := provider.addEventRule(rule)\n\tif err == nil {\n\t\tif fnReloadRules != nil {\n\t\t\tfnReloadRules()\n\t\t}\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectEventRule, rule.Name, role, rule)\n\t}\n\treturn err\n}\n\n// UpdateEventRule updates an existing event rule\nfunc UpdateEventRule(rule *EventRule, executor, ipAddress, role string) error {\n\terr := provider.updateEventRule(rule)\n\tif err == nil {\n\t\tif fnReloadRules != nil {\n\t\t\tfnReloadRules()\n\t\t}\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectEventRule, rule.Name, role, rule)\n\t}\n\treturn err\n}\n\n// DeleteEventRule deletes an existing event rule\nfunc DeleteEventRule(name string, executor, ipAddress, role string) error {\n\tname = config.convertName(name)\n\trule, err := provider.eventRuleExists(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.deleteEventRule(rule, config.IsShared == 1)\n\tif err == nil {\n\t\tif fnRemoveRule != nil {\n\t\t\tfnRemoveRule(rule.Name)\n\t\t}\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectEventRule, rule.Name, role, &rule)\n\t}\n\treturn err\n}\n\n// RemoveEventRule delets an existing event rule without marking it as deleted\nfunc RemoveEventRule(rule EventRule) error {\n\treturn provider.deleteEventRule(rule, false)\n}\n\n// GetTaskByName returns the task with the specified name\nfunc GetTaskByName(name string) (Task, error) {\n\treturn provider.getTaskByName(name)\n}\n\n// AddTask add a task with the specified name\nfunc AddTask(name string) error {\n\treturn provider.addTask(name)\n}\n\n// UpdateTask updates the task with the specified name and version\nfunc UpdateTask(name string, version int64) error {\n\treturn provider.updateTask(name, version)\n}\n\n// UpdateTaskTimestamp updates the timestamp for the task with the specified name\nfunc UpdateTaskTimestamp(name string) error {\n\treturn provider.updateTaskTimestamp(name)\n}\n\n// GetNodes returns the other cluster nodes\nfunc GetNodes() ([]Node, error) {\n\tif currentNode == nil {\n\t\treturn nil, nil\n\t}\n\tnodes, err := provider.getNodes()\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get other cluster nodes %v\", err)\n\t}\n\treturn nodes, err\n}\n\n// GetNodeByName returns a node, different from the current one, by name\nfunc GetNodeByName(name string) (Node, error) {\n\tif currentNode == nil {\n\t\treturn Node{}, util.NewRecordNotFoundError(errNoClusterNodes.Error())\n\t}\n\tif name == currentNode.Name {\n\t\treturn Node{}, util.NewValidationError(fmt.Sprintf(\"%s is the current node, it must refer to other nodes\", name))\n\t}\n\treturn provider.getNodeByName(name)\n}\n\n// HasAdmin returns true if the first admin has been created\n// and so SFTPGo is ready to be used\nfunc HasAdmin() bool {\n\treturn isAdminCreated.Load()\n}\n\n// AddAdmin adds a new SFTPGo admin\nfunc AddAdmin(admin *Admin, executor, ipAddress, role string) error {\n\tadmin.Filters.RecoveryCodes = nil\n\tadmin.Filters.TOTPConfig = AdminTOTPConfig{\n\t\tEnabled: false,\n\t}\n\tadmin.Username = config.convertName(admin.Username)\n\terr := provider.addAdmin(admin)\n\tif err == nil {\n\t\tisAdminCreated.Store(true)\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectAdmin, admin.Username, role, admin)\n\t}\n\treturn err\n}\n\n// UpdateAdmin updates an existing SFTPGo admin\nfunc UpdateAdmin(admin *Admin, executor, ipAddress, role string) error {\n\terr := provider.updateAdmin(admin)\n\tif err == nil {\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectAdmin, admin.Username, role, admin)\n\t}\n\treturn err\n}\n\n// DeleteAdmin deletes an existing SFTPGo admin\nfunc DeleteAdmin(username, executor, ipAddress, role string) error {\n\tusername = config.convertName(username)\n\tadmin, err := provider.adminExists(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.deleteAdmin(admin)\n\tif err == nil {\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectAdmin, admin.Username, role, &admin)\n\t\tcachedAdminPasswords.Remove(username)\n\t}\n\treturn err\n}\n\n// AdminExists returns the admin with the given username if it exists\nfunc AdminExists(username string) (Admin, error) {\n\tusername = config.convertName(username)\n\treturn provider.adminExists(username)\n}\n\n// UserExists checks if the given SFTPGo username exists, returns an error if no match is found\nfunc UserExists(username, role string) (User, error) {\n\tusername = config.convertName(username)\n\treturn provider.userExists(username, role)\n}\n\n// GetAdminSignature returns the signature for the admin with the specified\n// username.\nfunc GetAdminSignature(username string) (string, error) {\n\tusername = config.convertName(username)\n\treturn provider.getAdminSignature(username)\n}\n\n// GetUserSignature returns the signature for the user with the specified\n// username.\nfunc GetUserSignature(username string) (string, error) {\n\tusername = config.convertName(username)\n\treturn provider.getUserSignature(username)\n}\n\n// GetUserWithGroupSettings tries to return the user with the specified username\n// loading also the group settings\nfunc GetUserWithGroupSettings(username, role string) (User, error) {\n\tusername = config.convertName(username)\n\tuser, err := provider.userExists(username, role)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\terr = user.LoadAndApplyGroupSettings()\n\treturn user, err\n}\n\n// GetUserVariants tries to return the user with the specified username with and without\n// group settings applied\nfunc GetUserVariants(username, role string) (User, User, error) {\n\tusername = config.convertName(username)\n\tuser, err := provider.userExists(username, role)\n\tif err != nil {\n\t\treturn user, User{}, err\n\t}\n\tuserWithGroupSettings := user.getACopy()\n\terr = userWithGroupSettings.LoadAndApplyGroupSettings()\n\treturn user, userWithGroupSettings, err\n}\n\n// AddUser adds a new SFTPGo user.\nfunc AddUser(user *User, executor, ipAddress, role string) error {\n\tuser.Username = config.convertName(user.Username)\n\terr := provider.addUser(user)\n\tif err == nil {\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectUser, user.Username, role, user)\n\t}\n\treturn err\n}\n\n// UpdateUserPassword updates the user password\nfunc UpdateUserPassword(username, plainPwd, executor, ipAddress, role string) error {\n\tuser, err := provider.userExists(username, role)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserCopy := user.getACopy()\n\tuserCopy.Password = plainPwd\n\tif err := createUserPasswordHash(&userCopy); err != nil {\n\t\treturn err\n\t}\n\tuser.LastPasswordChange = userCopy.LastPasswordChange\n\tuser.Password = userCopy.Password\n\tuser.Filters.RequirePasswordChange = false\n\t// the last password change is set when validating the user\n\tif err := provider.updateUser(&user); err != nil {\n\t\treturn err\n\t}\n\twebDAVUsersCache.swap(&user, plainPwd)\n\texecuteAction(operationUpdate, executor, ipAddress, actionObjectUser, username, role, &user)\n\treturn nil\n}\n\n// UpdateUser updates an existing SFTPGo user.\nfunc UpdateUser(user *User, executor, ipAddress, role string) error {\n\tif user.groupSettingsApplied {\n\t\treturn errors.New(\"cannot save a user with group settings applied\")\n\t}\n\terr := provider.updateUser(user)\n\tif err == nil {\n\t\twebDAVUsersCache.swap(user, \"\")\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectUser, user.Username, role, user)\n\t}\n\treturn err\n}\n\n// DeleteUser deletes an existing SFTPGo user.\nfunc DeleteUser(username, executor, ipAddress, role string) error {\n\tusername = config.convertName(username)\n\tuser, err := provider.userExists(username, role)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.deleteUser(user, config.IsShared == 1)\n\tif err == nil {\n\t\tRemoveCachedWebDAVUser(user.Username)\n\t\tdelayedQuotaUpdater.resetUserQuota(user.Username)\n\t\tcachedUserPasswords.Remove(username)\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectUser, user.Username, role, &user)\n\t}\n\treturn err\n}\n\n// AddActiveTransfer stores the specified transfer\nfunc AddActiveTransfer(transfer ActiveTransfer) {\n\tif err := provider.addActiveTransfer(transfer); err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to add transfer id %v, connection id %v: %v\",\n\t\t\ttransfer.ID, transfer.ConnID, err)\n\t}\n}\n\n// UpdateActiveTransferSizes updates the current upload and download sizes for the specified transfer\nfunc UpdateActiveTransferSizes(ulSize, dlSize, transferID int64, connectionID string) {\n\tif err := provider.updateActiveTransferSizes(ulSize, dlSize, transferID, connectionID); err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to update sizes for transfer id %v, connection id %v: %v\",\n\t\t\ttransferID, connectionID, err)\n\t}\n}\n\n// RemoveActiveTransfer removes the specified transfer\nfunc RemoveActiveTransfer(transferID int64, connectionID string) {\n\tif err := provider.removeActiveTransfer(transferID, connectionID); err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to delete transfer id %v, connection id %v: %v\",\n\t\t\ttransferID, connectionID, err)\n\t}\n}\n\n// CleanupActiveTransfers removes the transfer before the specified time\nfunc CleanupActiveTransfers(before time.Time) error {\n\terr := provider.cleanupActiveTransfers(before)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"deleted active transfers updated before: %v\", before)\n\t} else {\n\t\tproviderLog(logger.LevelError, \"error deleting active transfers updated before %v: %v\", before, err)\n\t}\n\treturn err\n}\n\n// GetActiveTransfers retrieves the active transfers with an update time after the specified value\nfunc GetActiveTransfers(from time.Time) ([]ActiveTransfer, error) {\n\treturn provider.getActiveTransfers(from)\n}\n\n// AddSharedSession stores a new session within the data provider\nfunc AddSharedSession(session Session) error {\n\terr := provider.addSharedSession(session)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to add shared session, key %q, type: %v, err: %v\",\n\t\t\tsession.Key, session.Type, err)\n\t}\n\treturn err\n}\n\n// DeleteSharedSession deletes the session with the specified key\nfunc DeleteSharedSession(key string, sessionType SessionType) error {\n\terr := provider.deleteSharedSession(key, sessionType)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to add shared session, key %q, err: %v\", key, err)\n\t}\n\treturn err\n}\n\n// GetSharedSession retrieves the session with the specified key\nfunc GetSharedSession(key string, sessionType SessionType) (Session, error) {\n\treturn provider.getSharedSession(key, sessionType)\n}\n\n// CleanupSharedSessions removes the shared session with the specified type and\n// before the specified time\nfunc CleanupSharedSessions(sessionType SessionType, before time.Time) error {\n\terr := provider.cleanupSharedSessions(sessionType, util.GetTimeAsMsSinceEpoch(before))\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"deleted shared sessions before: %v, type: %v\", before, sessionType)\n\t} else {\n\t\tproviderLog(logger.LevelError, \"error deleting shared session before %v, type %v: %v\", before, sessionType, err)\n\t}\n\treturn err\n}\n\n// ReloadConfig reloads provider configuration.\n// Currently only implemented for memory provider, allows to reload the users\n// from the configured file, if defined\nfunc ReloadConfig() error {\n\treturn provider.reloadConfig()\n}\n\n// GetShares returns an array of shares respecting limit and offset\nfunc GetShares(limit, offset int, order, username string) ([]Share, error) {\n\treturn provider.getShares(limit, offset, order, username)\n}\n\n// GetAPIKeys returns an array of API keys respecting limit and offset\nfunc GetAPIKeys(limit, offset int, order string) ([]APIKey, error) {\n\treturn provider.getAPIKeys(limit, offset, order)\n}\n\n// GetAdmins returns an array of admins respecting limit and offset\nfunc GetAdmins(limit, offset int, order string) ([]Admin, error) {\n\treturn provider.getAdmins(limit, offset, order)\n}\n\n// GetRoles returns an array of roles respecting limit and offset\nfunc GetRoles(limit, offset int, order string, minimal bool) ([]Role, error) {\n\treturn provider.getRoles(limit, offset, order, minimal)\n}\n\n// GetGroups returns an array of groups respecting limit and offset\nfunc GetGroups(limit, offset int, order string, minimal bool) ([]Group, error) {\n\treturn provider.getGroups(limit, offset, order, minimal)\n}\n\n// GetUsers returns an array of users respecting limit and offset\nfunc GetUsers(limit, offset int, order, role string) ([]User, error) {\n\treturn provider.getUsers(limit, offset, order, role)\n}\n\n// GetUsersForQuotaCheck returns the users with the fields required for a quota check\nfunc GetUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {\n\treturn provider.getUsersForQuotaCheck(toFetch)\n}\n\n// AddFolder adds a new virtual folder.\nfunc AddFolder(folder *vfs.BaseVirtualFolder, executor, ipAddress, role string) error {\n\tfolder.Name = config.convertName(folder.Name)\n\terr := provider.addFolder(folder)\n\tif err == nil {\n\t\texecuteAction(operationAdd, executor, ipAddress, actionObjectFolder, folder.Name, role, &wrappedFolder{Folder: *folder})\n\t}\n\treturn err\n}\n\n// UpdateFolder updates the specified virtual folder\nfunc UpdateFolder(folder *vfs.BaseVirtualFolder, users []string, groups []string, executor, ipAddress, role string) error {\n\terr := provider.updateFolder(folder)\n\tif err == nil {\n\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectFolder, folder.Name, role, &wrappedFolder{Folder: *folder})\n\t\tusersInGroups, errGrp := provider.getUsersInGroups(groups)\n\t\tif errGrp == nil {\n\t\t\tusers = append(users, usersInGroups...)\n\t\t\tusers = util.RemoveDuplicates(users, false)\n\t\t} else {\n\t\t\tproviderLog(logger.LevelWarn, \"unable to get users in groups %+v: %v\", groups, errGrp)\n\t\t}\n\t\tfor _, user := range users {\n\t\t\tprovider.setUpdatedAt(user)\n\t\t\tu, err := provider.userExists(user, \"\")\n\t\t\tif err == nil {\n\t\t\t\twebDAVUsersCache.swap(&u, \"\")\n\t\t\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, u.Role, &u)\n\t\t\t} else {\n\t\t\t\tRemoveCachedWebDAVUser(user)\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}\n\n// DeleteFolder deletes an existing folder.\nfunc DeleteFolder(folderName, executor, ipAddress, role string) error {\n\tfolderName = config.convertName(folderName)\n\tfolder, err := provider.getFolderByName(folderName)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = provider.deleteFolder(folder)\n\tif err == nil {\n\t\texecuteAction(operationDelete, executor, ipAddress, actionObjectFolder, folder.Name, role, &wrappedFolder{Folder: folder})\n\t\tusers := folder.Users\n\t\tusersInGroups, errGrp := provider.getUsersInGroups(folder.Groups)\n\t\tif errGrp == nil {\n\t\t\tusers = append(users, usersInGroups...)\n\t\t\tusers = util.RemoveDuplicates(users, false)\n\t\t} else {\n\t\t\tproviderLog(logger.LevelWarn, \"unable to get users in groups %+v: %v\", folder.Groups, errGrp)\n\t\t}\n\t\tfor _, user := range users {\n\t\t\tprovider.setUpdatedAt(user)\n\t\t\tu, err := provider.userExists(user, \"\")\n\t\t\tif err == nil {\n\t\t\t\texecuteAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, u.Role, &u)\n\t\t\t}\n\t\t\tRemoveCachedWebDAVUser(user)\n\t\t}\n\t\tdelayedQuotaUpdater.resetFolderQuota(folderName)\n\t}\n\treturn err\n}\n\n// GetFolderByName returns the folder with the specified name if any\nfunc GetFolderByName(name string) (vfs.BaseVirtualFolder, error) {\n\tname = config.convertName(name)\n\treturn provider.getFolderByName(name)\n}\n\n// GetFolders returns an array of folders respecting limit and offset\nfunc GetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error) {\n\treturn provider.getFolders(limit, offset, order, minimal)\n}\n\nfunc dumpUsers(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeUsers) {\n\t\tusers, err := provider.dumpUsers()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.Users = users\n\t}\n\treturn nil\n}\n\nfunc dumpFolders(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeFolders) {\n\t\tfolders, err := provider.dumpFolders()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.Folders = folders\n\t}\n\treturn nil\n}\n\nfunc dumpGroups(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeGroups) {\n\t\tgroups, err := provider.dumpGroups()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.Groups = groups\n\t}\n\treturn nil\n}\n\nfunc dumpAdmins(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeAdmins) {\n\t\tadmins, err := provider.dumpAdmins()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.Admins = admins\n\t}\n\treturn nil\n}\n\nfunc dumpAPIKeys(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeAPIKeys) {\n\t\tapiKeys, err := provider.dumpAPIKeys()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.APIKeys = apiKeys\n\t}\n\treturn nil\n}\n\nfunc dumpShares(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeShares) {\n\t\tshares, err := provider.dumpShares()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.Shares = shares\n\t}\n\treturn nil\n}\n\nfunc dumpActions(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeActions) {\n\t\tactions, err := provider.dumpEventActions()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.EventActions = actions\n\t}\n\treturn nil\n}\n\nfunc dumpRules(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeRules) {\n\t\trules, err := provider.dumpEventRules()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.EventRules = rules\n\t}\n\treturn nil\n}\n\nfunc dumpRoles(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeRoles) {\n\t\troles, err := provider.dumpRoles()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.Roles = roles\n\t}\n\treturn nil\n}\n\nfunc dumpIPLists(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeIPLists) {\n\t\tipLists, err := provider.dumpIPListEntries()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.IPLists = ipLists\n\t}\n\treturn nil\n}\n\nfunc dumpConfigs(data *BackupData, scopes []string) error {\n\tif len(scopes) == 0 || slices.Contains(scopes, DumpScopeConfigs) {\n\t\tconfigs, err := provider.getConfigs()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.Configs = &configs\n\t}\n\treturn nil\n}\n\n// DumpData returns a dump containing the requested scopes.\n// Empty scopes means all\nfunc DumpData(scopes []string) (BackupData, error) {\n\tdata := BackupData{\n\t\tVersion: DumpVersion,\n\t}\n\tif err := dumpGroups(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpUsers(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpFolders(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpAdmins(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpAPIKeys(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpShares(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpActions(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpRules(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpRoles(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpIPLists(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\tif err := dumpConfigs(&data, scopes); err != nil {\n\t\treturn data, err\n\t}\n\n\treturn data, nil\n}\n\n// ParseDumpData tries to parse data as BackupData\nfunc ParseDumpData(data []byte) (BackupData, error) {\n\tvar dump BackupData\n\terr := json.Unmarshal(data, &dump)\n\tif err != nil {\n\t\treturn dump, err\n\t}\n\tif dump.Version < 17 {\n\t\tproviderLog(logger.LevelInfo, \"updating placeholders for actions restored from dump version %d\", dump.Version)\n\t\teventActions, err := updateEventActionPlaceholders(dump.EventActions)\n\t\tif err != nil {\n\t\t\treturn dump, fmt.Errorf(\"unable to update event action placeholders for dump version %d: %w\", dump.Version, err)\n\t\t}\n\t\tdump.EventActions = eventActions\n\t}\n\treturn dump, err\n}\n\n// GetProviderConfig returns the current provider configuration\nfunc GetProviderConfig() Config {\n\treturn config\n}\n\n// GetProviderStatus returns an error if the provider is not available\nfunc GetProviderStatus() ProviderStatus {\n\terr := provider.checkAvailability()\n\tstatus := ProviderStatus{\n\t\tDriver: config.Driver,\n\t}\n\tif err == nil {\n\t\tstatus.IsActive = true\n\t} else {\n\t\tstatus.IsActive = false\n\t\tstatus.Error = err.Error()\n\t}\n\treturn status\n}\n\n// Close releases all provider resources.\n// This method is used in test cases.\n// Closing an uninitialized provider is not supported\nfunc Close() error {\n\tstopScheduler()\n\treturn provider.close()\n}\n\nfunc createProvider(basePath string) error {\n\tsqlPlaceholders = getSQLPlaceholders()\n\tif err := validateSQLTablesPrefix(); err != nil {\n\t\treturn err\n\t}\n\tlogSender = fmt.Sprintf(\"dataprovider_%v\", config.Driver)\n\n\tswitch config.Driver {\n\tcase SQLiteDataProviderName:\n\t\treturn initializeSQLiteProvider(basePath)\n\tcase PGSQLDataProviderName, CockroachDataProviderName:\n\t\treturn initializePGSQLProvider()\n\tcase MySQLDataProviderName:\n\t\treturn initializeMySQLProvider()\n\tcase BoltDataProviderName:\n\t\treturn initializeBoltProvider(basePath)\n\tcase MemoryDataProviderName:\n\t\tif err := initializeMemoryProvider(basePath); err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"provider initialized but data loading failed: %v\", err)\n\t\t\tlogger.WarnToConsole(\"provider initialized but data loading failed: %v\", err)\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported data provider: %v\", config.Driver)\n\t}\n}\n\nfunc copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {\n\tfilters := sdk.BaseUserFilters{}\n\tfilters.MaxUploadFileSize = in.MaxUploadFileSize\n\tfilters.TLSUsername = in.TLSUsername\n\tfilters.UserType = in.UserType\n\tfilters.AllowedIP = make([]string, len(in.AllowedIP))\n\tcopy(filters.AllowedIP, in.AllowedIP)\n\tfilters.DeniedIP = make([]string, len(in.DeniedIP))\n\tcopy(filters.DeniedIP, in.DeniedIP)\n\tfilters.DeniedLoginMethods = make([]string, len(in.DeniedLoginMethods))\n\tcopy(filters.DeniedLoginMethods, in.DeniedLoginMethods)\n\tfilters.FilePatterns = make([]sdk.PatternsFilter, len(in.FilePatterns))\n\tcopy(filters.FilePatterns, in.FilePatterns)\n\tfilters.DeniedProtocols = make([]string, len(in.DeniedProtocols))\n\tcopy(filters.DeniedProtocols, in.DeniedProtocols)\n\tfilters.TwoFactorAuthProtocols = make([]string, len(in.TwoFactorAuthProtocols))\n\tcopy(filters.TwoFactorAuthProtocols, in.TwoFactorAuthProtocols)\n\tfilters.Hooks.ExternalAuthDisabled = in.Hooks.ExternalAuthDisabled\n\tfilters.Hooks.PreLoginDisabled = in.Hooks.PreLoginDisabled\n\tfilters.Hooks.CheckPasswordDisabled = in.Hooks.CheckPasswordDisabled\n\tfilters.DisableFsChecks = in.DisableFsChecks\n\tfilters.StartDirectory = in.StartDirectory\n\tfilters.FTPSecurity = in.FTPSecurity\n\tfilters.IsAnonymous = in.IsAnonymous\n\tfilters.AllowAPIKeyAuth = in.AllowAPIKeyAuth\n\tfilters.ExternalAuthCacheTime = in.ExternalAuthCacheTime\n\tfilters.DefaultSharesExpiration = in.DefaultSharesExpiration\n\tfilters.MaxSharesExpiration = in.MaxSharesExpiration\n\tfilters.PasswordExpiration = in.PasswordExpiration\n\tfilters.PasswordStrength = in.PasswordStrength\n\tfilters.WebClient = make([]string, len(in.WebClient))\n\tcopy(filters.WebClient, in.WebClient)\n\tfilters.TLSCerts = make([]string, len(in.TLSCerts))\n\tcopy(filters.TLSCerts, in.TLSCerts)\n\tfilters.BandwidthLimits = make([]sdk.BandwidthLimit, 0, len(in.BandwidthLimits))\n\tfor _, limit := range in.BandwidthLimits {\n\t\tbwLimit := sdk.BandwidthLimit{\n\t\t\tUploadBandwidth:   limit.UploadBandwidth,\n\t\t\tDownloadBandwidth: limit.DownloadBandwidth,\n\t\t\tSources:           make([]string, 0, len(limit.Sources)),\n\t\t}\n\t\tbwLimit.Sources = make([]string, len(limit.Sources))\n\t\tcopy(bwLimit.Sources, limit.Sources)\n\t\tfilters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)\n\t}\n\tfilters.AccessTime = make([]sdk.TimePeriod, 0, len(in.AccessTime))\n\tfor _, period := range in.AccessTime {\n\t\tfilters.AccessTime = append(filters.AccessTime, sdk.TimePeriod{\n\t\t\tDayOfWeek: period.DayOfWeek,\n\t\t\tFrom:      period.From,\n\t\t\tTo:        period.To,\n\t\t})\n\t}\n\treturn filters\n}\n\nfunc buildUserHomeDir(user *User) {\n\tif user.HomeDir == \"\" {\n\t\tif config.UsersBaseDir != \"\" {\n\t\t\tuser.HomeDir = filepath.Join(config.UsersBaseDir, user.Username)\n\t\t\treturn\n\t\t}\n\t\tswitch user.FsConfig.Provider {\n\t\tcase sdk.SFTPFilesystemProvider, sdk.S3FilesystemProvider, sdk.AzureBlobFilesystemProvider, sdk.GCSFilesystemProvider, sdk.HTTPFilesystemProvider:\n\t\t\tif tempPath != \"\" {\n\t\t\t\tuser.HomeDir = filepath.Join(tempPath, user.Username)\n\t\t\t} else {\n\t\t\t\tuser.HomeDir = filepath.Join(os.TempDir(), user.Username)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tuser.HomeDir = filepath.Clean(user.HomeDir)\n\t}\n}\n\nfunc validateFolderQuotaLimits(folder vfs.VirtualFolder) error {\n\tif folder.QuotaSize < -1 {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid quota_size: %v folder path %q\", folder.QuotaSize, folder.MappedPath)),\n\t\t\tutil.I18nErrorFolderQuotaSizeInvalid,\n\t\t)\n\t}\n\tif folder.QuotaFiles < -1 {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid quota_file: %v folder path %q\", folder.QuotaFiles, folder.MappedPath)),\n\t\t\tutil.I18nErrorFolderQuotaFileInvalid,\n\t\t)\n\t}\n\tif (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v\",\n\t\t\t\tfolder.QuotaFiles, folder.QuotaSize)),\n\t\t\tutil.I18nErrorFolderQuotaInvalid,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc validateUserGroups(user *User) error {\n\tif len(user.Groups) == 0 {\n\t\treturn nil\n\t}\n\thasPrimary := false\n\tgroupNames := make(map[string]bool)\n\n\tfor _, g := range user.Groups {\n\t\tif g.Type < sdk.GroupTypePrimary || g.Type > sdk.GroupTypeMembership {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid group type: %v\", g.Type))\n\t\t}\n\t\tif g.Type == sdk.GroupTypePrimary {\n\t\t\tif hasPrimary {\n\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\tutil.NewValidationError(\"only one primary group is allowed\"),\n\t\t\t\t\tutil.I18nErrorPrimaryGroup,\n\t\t\t\t)\n\t\t\t}\n\t\t\thasPrimary = true\n\t\t}\n\t\tif groupNames[g.Name] {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"the group %q is duplicated\", g.Name)),\n\t\t\t\tutil.I18nErrorDuplicateGroup,\n\t\t\t)\n\t\t}\n\t\tgroupNames[g.Name] = true\n\t}\n\treturn nil\n}\n\nfunc validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.VirtualFolder, error) {\n\tif len(vfolders) == 0 {\n\t\treturn []vfs.VirtualFolder{}, nil\n\t}\n\tvar virtualFolders []vfs.VirtualFolder\n\tfolderNames := make(map[string]bool)\n\n\tfor _, v := range vfolders {\n\t\tv.Name = config.convertName(v.Name)\n\t\tif v.VirtualPath == \"\" {\n\t\t\treturn nil, util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"mount/virtual path is mandatory\"),\n\t\t\t\tutil.I18nErrorFolderMountPathRequired,\n\t\t\t)\n\t\t}\n\t\tcleanedVPath := util.CleanPath(v.VirtualPath)\n\t\tif err := validateFolderQuotaLimits(v); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif v.Name == \"\" {\n\t\t\treturn nil, util.NewI18nError(util.NewValidationError(\"folder name is mandatory\"), util.I18nErrorFolderNameRequired)\n\t\t}\n\t\tif folderNames[v.Name] {\n\t\t\treturn nil, util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"the folder %q is duplicated\", v.Name)),\n\t\t\t\tutil.I18nErrorDuplicatedFolders,\n\t\t\t)\n\t\t}\n\t\tfor _, vFolder := range virtualFolders {\n\t\t\tif util.IsDirOverlapped(vFolder.VirtualPath, cleanedVPath, false, \"/\") {\n\t\t\t\treturn nil, util.NewI18nError(\n\t\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid virtual folder %q, it overlaps with virtual folder %q\",\n\t\t\t\t\t\tv.VirtualPath, vFolder.VirtualPath)),\n\t\t\t\t\tutil.I18nErrorOverlappedFolders,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\tvirtualFolders = append(virtualFolders, vfs.VirtualFolder{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: v.Name,\n\t\t\t},\n\t\t\tVirtualPath: cleanedVPath,\n\t\t\tQuotaSize:   v.QuotaSize,\n\t\t\tQuotaFiles:  v.QuotaFiles,\n\t\t})\n\t\tfolderNames[v.Name] = true\n\t}\n\treturn virtualFolders, nil\n}\n\nfunc validateUserTOTPConfig(c *UserTOTPConfig, username string) error {\n\tif !c.Enabled {\n\t\tc.ConfigName = \"\"\n\t\tc.Secret = kms.NewEmptySecret()\n\t\tc.Protocols = nil\n\t\treturn nil\n\t}\n\tif c.ConfigName == \"\" {\n\t\treturn util.NewValidationError(\"totp: config name is mandatory\")\n\t}\n\tif !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"totp: config name %q not found\", c.ConfigName))\n\t}\n\tif c.Secret.IsEmpty() {\n\t\treturn util.NewValidationError(\"totp: secret is mandatory\")\n\t}\n\tif c.Secret.IsPlain() {\n\t\tc.Secret.SetAdditionalData(username)\n\t\tif err := c.Secret.Encrypt(); err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"totp: unable to encrypt secret: %v\", err))\n\t\t}\n\t}\n\tif len(c.Protocols) == 0 {\n\t\treturn util.NewValidationError(\"totp: specify at least one protocol\")\n\t}\n\tfor _, protocol := range c.Protocols {\n\t\tif !slices.Contains(MFAProtocols, protocol) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"totp: invalid protocol %q\", protocol))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateUserRecoveryCodes(user *User) error {\n\tfor i := 0; i < len(user.Filters.RecoveryCodes); i++ {\n\t\tcode := &user.Filters.RecoveryCodes[i]\n\t\tif code.Secret.IsEmpty() {\n\t\t\treturn util.NewValidationError(\"mfa: recovery code cannot be empty\")\n\t\t}\n\t\tif code.Secret.IsPlain() {\n\t\t\tcode.Secret.SetAdditionalData(user.Username)\n\t\t\tif err := code.Secret.Encrypt(); err != nil {\n\t\t\t\treturn util.NewValidationError(fmt.Sprintf(\"mfa: unable to encrypt recovery code: %v\", err))\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateUserPermissions(permsToCheck map[string][]string) (map[string][]string, error) {\n\tpermissions := make(map[string][]string)\n\tfor dir, perms := range permsToCheck {\n\t\tif len(perms) == 0 && dir == \"/\" {\n\t\t\treturn permissions, util.NewValidationError(fmt.Sprintf(\"no permissions granted for the directory: %q\", dir))\n\t\t}\n\t\tif len(perms) > len(ValidPerms) {\n\t\t\treturn permissions, util.NewValidationError(\"invalid permissions\")\n\t\t}\n\t\tfor _, p := range perms {\n\t\t\tif !slices.Contains(ValidPerms, p) {\n\t\t\t\treturn permissions, util.NewValidationError(fmt.Sprintf(\"invalid permission: %q\", p))\n\t\t\t}\n\t\t}\n\t\tcleanedDir := filepath.ToSlash(path.Clean(dir))\n\t\tif cleanedDir != \"/\" {\n\t\t\tcleanedDir = strings.TrimSuffix(cleanedDir, \"/\")\n\t\t}\n\t\tif !path.IsAbs(cleanedDir) {\n\t\t\treturn permissions, util.NewValidationError(fmt.Sprintf(\"cannot set permissions for non absolute path: %q\", dir))\n\t\t}\n\t\tif dir != cleanedDir && cleanedDir == \"/\" {\n\t\t\treturn permissions, util.NewValidationError(fmt.Sprintf(\"cannot set permissions for invalid subdirectory: %q is an alias for \\\"/\\\"\", dir))\n\t\t}\n\t\tif slices.Contains(perms, PermAny) {\n\t\t\tpermissions[cleanedDir] = []string{PermAny}\n\t\t} else {\n\t\t\tpermissions[cleanedDir] = util.RemoveDuplicates(perms, false)\n\t\t}\n\t}\n\n\treturn permissions, nil\n}\n\nfunc validatePermissions(user *User) error {\n\tif len(user.Permissions) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"please grant some permissions to this user\"), util.I18nErrorNoPermission)\n\t}\n\tif _, ok := user.Permissions[\"/\"]; !ok {\n\t\treturn util.NewI18nError(util.NewValidationError(\"permissions for the root dir \\\"/\\\" must be set\"), util.I18nErrorNoRootPermission)\n\t}\n\tpermissions, err := validateUserPermissions(user.Permissions)\n\tif err != nil {\n\t\treturn util.NewI18nError(err, util.I18nErrorGenericPermission)\n\t}\n\tuser.Permissions = permissions\n\treturn nil\n}\n\nfunc validatePublicKeys(user *User) error {\n\tif len(user.PublicKeys) == 0 {\n\t\tuser.PublicKeys = []string{}\n\t}\n\tvar validatedKeys []string\n\tfor idx, key := range user.PublicKeys {\n\t\tif key == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"error parsing public key at position %d: %v\", idx, err)),\n\t\t\t\tutil.I18nErrorPubKeyInvalid,\n\t\t\t)\n\t\t}\n\t\tif out.Type() == ssh.InsecureKeyAlgoDSA { //nolint:staticcheck\n\t\t\tproviderLog(logger.LevelError, \"dsa public key not accepted, position: %d\", idx)\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"DSA key format is insecure and it is not allowed for key at position %d\", idx)),\n\t\t\t\tutil.I18nErrorKeyInsecure,\n\t\t\t)\n\t\t}\n\t\tif k, ok := out.(ssh.CryptoPublicKey); ok {\n\t\t\tcryptoKey := k.CryptoPublicKey()\n\t\t\tif rsaKey, ok := cryptoKey.(*rsa.PublicKey); ok {\n\t\t\t\tif size := rsaKey.N.BitLen(); size < 2048 {\n\t\t\t\t\tproviderLog(logger.LevelError, \"rsa key with size %d at position %d not accepted, minimum 2048\", size, idx)\n\t\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid size %d for rsa key at position %d, minimum 2048\",\n\t\t\t\t\t\t\tsize, idx)),\n\t\t\t\t\t\tutil.I18nErrorKeySizeInvalid,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvalidatedKeys = append(validatedKeys, key)\n\t}\n\tuser.PublicKeys = util.RemoveDuplicates(validatedKeys, false)\n\treturn nil\n}\n\nfunc validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {\n\tif len(baseFilters.FilePatterns) == 0 {\n\t\tbaseFilters.FilePatterns = []sdk.PatternsFilter{}\n\t\treturn nil\n\t}\n\tfilteredPaths := []string{}\n\tvar filters []sdk.PatternsFilter\n\tfor _, f := range baseFilters.FilePatterns {\n\t\tcleanedPath := filepath.ToSlash(path.Clean(f.Path))\n\t\tif !path.IsAbs(cleanedPath) {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid path %q for file patterns filter\", f.Path)),\n\t\t\t\tutil.I18nErrorFilePatternPathInvalid,\n\t\t\t)\n\t\t}\n\t\tif slices.Contains(filteredPaths, cleanedPath) {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"duplicate file patterns filter for path %q\", f.Path)),\n\t\t\t\tutil.I18nErrorFilePatternDuplicated,\n\t\t\t)\n\t\t}\n\t\tif len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"empty file patterns filter for path %q\", f.Path))\n\t\t}\n\t\tif f.DenyPolicy < sdk.DenyPolicyDefault || f.DenyPolicy > sdk.DenyPolicyHide {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid deny policy %v for path %q\", f.DenyPolicy, f.Path))\n\t\t}\n\t\tf.Path = cleanedPath\n\t\tallowed := make([]string, 0, len(f.AllowedPatterns))\n\t\tdenied := make([]string, 0, len(f.DeniedPatterns))\n\t\tfor _, pattern := range f.AllowedPatterns {\n\t\t\t_, err := path.Match(pattern, \"abc\")\n\t\t\tif err != nil {\n\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid file pattern filter %q\", pattern)),\n\t\t\t\t\tutil.I18nErrorFilePatternInvalid,\n\t\t\t\t)\n\t\t\t}\n\t\t\tallowed = append(allowed, strings.ToLower(pattern))\n\t\t}\n\t\tfor _, pattern := range f.DeniedPatterns {\n\t\t\t_, err := path.Match(pattern, \"abc\")\n\t\t\tif err != nil {\n\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid file pattern filter %q\", pattern)),\n\t\t\t\t\tutil.I18nErrorFilePatternInvalid,\n\t\t\t\t)\n\t\t\t}\n\t\t\tdenied = append(denied, strings.ToLower(pattern))\n\t\t}\n\t\tf.AllowedPatterns = util.RemoveDuplicates(allowed, false)\n\t\tf.DeniedPatterns = util.RemoveDuplicates(denied, false)\n\t\tfilters = append(filters, f)\n\t\tfilteredPaths = append(filteredPaths, cleanedPath)\n\t}\n\tbaseFilters.FilePatterns = filters\n\treturn nil\n}\n\nfunc checkEmptyFiltersStruct(filters *sdk.BaseUserFilters) {\n\tif len(filters.AllowedIP) == 0 {\n\t\tfilters.AllowedIP = []string{}\n\t}\n\tif len(filters.DeniedIP) == 0 {\n\t\tfilters.DeniedIP = []string{}\n\t}\n\tif len(filters.DeniedLoginMethods) == 0 {\n\t\tfilters.DeniedLoginMethods = []string{}\n\t}\n\tif len(filters.DeniedProtocols) == 0 {\n\t\tfilters.DeniedProtocols = []string{}\n\t}\n}\n\nfunc validateIPFilters(filters *sdk.BaseUserFilters) error {\n\tfilters.DeniedIP = util.RemoveDuplicates(filters.DeniedIP, false)\n\tfor _, IPMask := range filters.DeniedIP {\n\t\t_, _, err := net.ParseCIDR(IPMask)\n\t\tif err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"could not parse denied IP/Mask %q: %v\", IPMask, err))\n\t\t}\n\t}\n\tfilters.AllowedIP = util.RemoveDuplicates(filters.AllowedIP, false)\n\tfor _, IPMask := range filters.AllowedIP {\n\t\t_, _, err := net.ParseCIDR(IPMask)\n\t\tif err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"could not parse allowed IP/Mask %q: %v\", IPMask, err))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateBandwidthLimit(bl sdk.BandwidthLimit) error {\n\tif len(bl.Sources) == 0 {\n\t\treturn util.NewValidationError(\"no bandwidth limit source specified\")\n\t}\n\tfor _, source := range bl.Sources {\n\t\t_, _, err := net.ParseCIDR(source)\n\t\tif err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"could not parse bandwidth limit source %q: %v\", source, err))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateBandwidthLimitsFilter(filters *sdk.BaseUserFilters) error {\n\tfor idx, bandwidthLimit := range filters.BandwidthLimits {\n\t\tif err := validateBandwidthLimit(bandwidthLimit); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif bandwidthLimit.DownloadBandwidth < 0 {\n\t\t\tfilters.BandwidthLimits[idx].DownloadBandwidth = 0\n\t\t}\n\t\tif bandwidthLimit.UploadBandwidth < 0 {\n\t\t\tfilters.BandwidthLimits[idx].UploadBandwidth = 0\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc updateFiltersValues(filters *sdk.BaseUserFilters) {\n\tif filters.StartDirectory != \"\" {\n\t\tfilters.StartDirectory = util.CleanPath(filters.StartDirectory)\n\t\tif filters.StartDirectory == \"/\" {\n\t\t\tfilters.StartDirectory = \"\"\n\t\t}\n\t}\n}\n\nfunc validateFilterProtocols(filters *sdk.BaseUserFilters) error {\n\tif len(filters.DeniedProtocols) >= len(ValidProtocols) {\n\t\treturn util.NewValidationError(\"invalid denied_protocols\")\n\t}\n\tfor _, p := range filters.DeniedProtocols {\n\t\tif !slices.Contains(ValidProtocols, p) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid denied protocol %q\", p))\n\t\t}\n\t}\n\n\tfor _, p := range filters.TwoFactorAuthProtocols {\n\t\tif !slices.Contains(MFAProtocols, p) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid two factor protocol %q\", p))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateTLSCerts(certs []string) ([]string, error) {\n\tvar validateCerts []string\n\tfor idx, cert := range certs {\n\t\tif cert == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tderBlock, _ := pem.Decode([]byte(cert))\n\t\tif derBlock == nil {\n\t\t\treturn nil, util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid TLS certificate %d\", idx)),\n\t\t\t\tutil.I18nErrorInvalidTLSCert,\n\t\t\t)\n\t\t}\n\t\tcrt, err := x509.ParseCertificate(derBlock.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"error parsing TLS certificate %d\", idx)),\n\t\t\t\tutil.I18nErrorInvalidTLSCert,\n\t\t\t)\n\t\t}\n\t\tif crt.PublicKeyAlgorithm == x509.RSA {\n\t\t\tif rsaCert, ok := crt.PublicKey.(*rsa.PublicKey); ok {\n\t\t\t\tif size := rsaCert.N.BitLen(); size < 2048 {\n\t\t\t\t\tproviderLog(logger.LevelError, \"rsa cert with size %d not accepted, minimum 2048\", size)\n\t\t\t\t\treturn nil, util.NewI18nError(\n\t\t\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid size %d for rsa cert at position %d, minimum 2048\",\n\t\t\t\t\t\t\tsize, idx)),\n\t\t\t\t\t\tutil.I18nErrorKeySizeInvalid,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tvalidateCerts = append(validateCerts, cert)\n\t}\n\treturn validateCerts, nil\n}\n\nfunc validateBaseFilters(filters *sdk.BaseUserFilters) error {\n\tcheckEmptyFiltersStruct(filters)\n\tif err := validateIPFilters(filters); err != nil {\n\t\treturn util.NewI18nError(err, util.I18nErrorIPFiltersInvalid)\n\t}\n\tif err := validateBandwidthLimitsFilter(filters); err != nil {\n\t\treturn util.NewI18nError(err, util.I18nErrorSourceBWLimitInvalid)\n\t}\n\tif len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) {\n\t\treturn util.NewValidationError(\"invalid denied_login_methods\")\n\t}\n\tfor _, loginMethod := range filters.DeniedLoginMethods {\n\t\tif !slices.Contains(ValidLoginMethods, loginMethod) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid login method: %q\", loginMethod))\n\t\t}\n\t}\n\tif err := validateFilterProtocols(filters); err != nil {\n\t\treturn err\n\t}\n\tif filters.TLSUsername != \"\" {\n\t\tif !slices.Contains(validTLSUsernames, string(filters.TLSUsername)) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid TLS username: %q\", filters.TLSUsername))\n\t\t}\n\t}\n\tcerts, err := validateTLSCerts(filters.TLSCerts)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfilters.TLSCerts = certs\n\tfor _, opts := range filters.WebClient {\n\t\tif !slices.Contains(sdk.WebClientOptions, opts) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid web client options %q\", opts))\n\t\t}\n\t}\n\tif filters.MaxSharesExpiration > 0 && filters.MaxSharesExpiration < filters.DefaultSharesExpiration {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"default shares expiration: %d must be less than or equal to max shares expiration: %d\",\n\t\t\t\tfilters.DefaultSharesExpiration, filters.MaxSharesExpiration)),\n\t\t\tutil.I18nErrorShareExpirationInvalid,\n\t\t)\n\t}\n\tupdateFiltersValues(filters)\n\n\tif err := validateAccessTimeFilters(filters); err != nil {\n\t\treturn err\n\t}\n\n\treturn validateFiltersPatternExtensions(filters)\n}\n\nfunc isTimeOfDayValid(value string) bool {\n\tif len(value) != 5 {\n\t\treturn false\n\t}\n\tparts := strings.Split(value, \":\")\n\tif len(parts) != 2 {\n\t\treturn false\n\t}\n\thour, err := strconv.Atoi(parts[0])\n\tif err != nil {\n\t\treturn false\n\t}\n\tif hour < 0 || hour > 23 {\n\t\treturn false\n\t}\n\tminute, err := strconv.Atoi(parts[1])\n\tif err != nil {\n\t\treturn false\n\t}\n\tif minute < 0 || minute > 59 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc validateAccessTimeFilters(filters *sdk.BaseUserFilters) error {\n\tfor _, period := range filters.AccessTime {\n\t\tif period.DayOfWeek < int(time.Sunday) || period.DayOfWeek > int(time.Saturday) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid day of week: %d\", period.DayOfWeek))\n\t\t}\n\t\tif !isTimeOfDayValid(period.From) || !isTimeOfDayValid(period.To) {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"invalid time of day. Supported format: HH:MM\"),\n\t\t\t\tutil.I18nErrorTimeOfDayInvalid,\n\t\t\t)\n\t\t}\n\t\tif period.To <= period.From {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"invalid time of day. The end time cannot be earlier than the start time\"),\n\t\t\t\tutil.I18nErrorTimeOfDayConflict,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc validateCombinedUserFilters(user *User) error {\n\tif user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"two-factor authentication cannot be disabled for a user with an active configuration\"),\n\t\t\tutil.I18nErrorDisableActive2FA,\n\t\t)\n\t}\n\tif user.Filters.RequirePasswordChange && slices.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"you cannot require password change and at the same time disallow it\"),\n\t\t\tutil.I18nErrorPwdChangeConflict,\n\t\t)\n\t}\n\tif len(user.Filters.TwoFactorAuthProtocols) > 0 && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"you cannot require two-factor authentication and at the same time disallow it\"),\n\t\t\tutil.I18nError2FAConflict,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc validateEmails(user *User) error {\n\tif user.Email != \"\" && !util.IsEmailValid(user.Email) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"email %q is not valid\", user.Email)),\n\t\t\tutil.I18nErrorInvalidEmail,\n\t\t)\n\t}\n\tfor _, email := range user.Filters.AdditionalEmails {\n\t\tif !util.IsEmailValid(email) {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"email %q is not valid\", email)),\n\t\t\t\tutil.I18nErrorInvalidEmail,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateBaseParams(user *User) error {\n\tif user.Username == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"username is mandatory\"), util.I18nErrorUsernameRequired)\n\t}\n\tif !util.IsNameValid(user.Username) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif err := checkReservedUsernames(user.Username); err != nil {\n\t\treturn util.NewI18nError(err, util.I18nErrorReservedUsername)\n\t}\n\tif err := validateEmails(user); err != nil {\n\t\treturn err\n\t}\n\tif config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~\", user.Username)),\n\t\t\tutil.I18nErrorInvalidUser,\n\t\t)\n\t}\n\tif user.hasRedactedSecret() {\n\t\treturn util.NewValidationError(\"cannot save a user with a redacted secret\")\n\t}\n\tif user.HomeDir == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"home_dir is mandatory\"), util.I18nErrorHomeRequired)\n\t}\n\t// we can have users with no passwords and public keys, they can authenticate via SSH user certs or OIDC\n\t/*if user.Password == \"\" && len(user.PublicKeys) == 0 {\n\t\treturn util.NewValidationError(\"please set a password or at least a public_key\")\n\t}*/\n\tif !filepath.IsAbs(user.HomeDir) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"home_dir must be an absolute path, actual value: %v\", user.HomeDir)),\n\t\t\tutil.I18nErrorHomeInvalid,\n\t\t)\n\t}\n\tif user.DownloadBandwidth < 0 {\n\t\tuser.DownloadBandwidth = 0\n\t}\n\tif user.UploadBandwidth < 0 {\n\t\tuser.UploadBandwidth = 0\n\t}\n\tif user.TotalDataTransfer > 0 {\n\t\t// if a total data transfer is defined we reset the separate upload and download limits\n\t\tuser.UploadDataTransfer = 0\n\t\tuser.DownloadDataTransfer = 0\n\t}\n\tif user.Filters.IsAnonymous {\n\t\tuser.setAnonymousSettings()\n\t}\n\terr := user.FsConfig.Validate(user.GetEncryptionAdditionalData())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc hashPlainPassword(plainPwd string) (string, error) {\n\tif config.PasswordHashing.Algo == HashingAlgoBcrypt {\n\t\tpwd, err := bcrypt.GenerateFromPassword([]byte(plainPwd), config.PasswordHashing.BcryptOptions.Cost)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"bcrypt hashing error: %w\", err)\n\t\t}\n\t\treturn util.BytesToString(pwd), nil\n\t}\n\tpwd, err := argon2id.CreateHash(plainPwd, argon2Params)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"argon2ID hashing error: %w\", err)\n\t}\n\treturn pwd, nil\n}\n\nfunc createUserPasswordHash(user *User) error {\n\tif user.Password != \"\" && !user.IsPasswordHashed() {\n\t\tfor _, g := range user.Groups {\n\t\t\tif g.Type == sdk.GroupTypePrimary {\n\t\t\t\tgroup, err := GroupExists(g.Name)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn errors.New(\"unable to load group password policies\")\n\t\t\t\t}\n\t\t\t\tif minEntropy := group.UserSettings.Filters.PasswordStrength; minEntropy > 0 {\n\t\t\t\t\tif err := passwordvalidator.Validate(user.Password, float64(minEntropy)); err != nil {\n\t\t\t\t\t\treturn util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {\n\t\t\tif err := passwordvalidator.Validate(user.Password, minEntropy); err != nil {\n\t\t\t\treturn util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)\n\t\t\t}\n\t\t}\n\t\thashedPwd, err := hashPlainPassword(user.Password)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser.Password = hashedPwd\n\t\tuser.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now())\n\t}\n\treturn nil\n}\n\n// ValidateFolder returns an error if the folder is not valid\n// FIXME: this should be defined as Folder struct method\nfunc ValidateFolder(folder *vfs.BaseVirtualFolder) error {\n\tfolder.FsConfig.SetEmptySecretsIfNil()\n\tif folder.Name == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"folder name is mandatory\"), util.I18nErrorNameRequired)\n\t}\n\tif !util.IsNameValid(folder.Name) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif config.NamingRules&1 == 0 && !usernameRegex.MatchString(folder.Name) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"folder name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~\", folder.Name)),\n\t\t\tutil.I18nErrorInvalidName,\n\t\t)\n\t}\n\tif folder.FsConfig.Provider == sdk.LocalFilesystemProvider || folder.FsConfig.Provider == sdk.CryptedFilesystemProvider ||\n\t\tfolder.MappedPath != \"\" {\n\t\tcleanedMPath := filepath.Clean(folder.MappedPath)\n\t\tif !filepath.IsAbs(cleanedMPath) {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid folder mapped path %q\", folder.MappedPath)),\n\t\t\t\tutil.I18nErrorInvalidHomeDir,\n\t\t\t)\n\t\t}\n\t\tfolder.MappedPath = cleanedMPath\n\t}\n\tif folder.HasRedactedSecret() {\n\t\treturn errors.New(\"cannot save a folder with a redacted secret\")\n\t}\n\treturn folder.FsConfig.Validate(folder.GetEncryptionAdditionalData())\n}\n\n// ValidateUser returns an error if the user is not valid\n// FIXME: this should be defined as User struct method\nfunc ValidateUser(user *User) error {\n\tuser.OIDCCustomFields = nil\n\tuser.HasPassword = false\n\tuser.SetEmptySecretsIfNil()\n\tuser.applyNamingRules()\n\tbuildUserHomeDir(user)\n\tif err := validateBaseParams(user); err != nil {\n\t\treturn err\n\t}\n\tif err := validateUserGroups(user); err != nil {\n\t\treturn err\n\t}\n\tif err := validatePermissions(user); err != nil {\n\t\treturn err\n\t}\n\tif err := validateUserTOTPConfig(&user.Filters.TOTPConfig, user.Username); err != nil {\n\t\treturn util.NewI18nError(err, util.I18nError2FAInvalid)\n\t}\n\tif err := validateUserRecoveryCodes(user); err != nil {\n\t\treturn util.NewI18nError(err, util.I18nErrorRecoveryCodesInvalid)\n\t}\n\tvfolders, err := validateAssociatedVirtualFolders(user.VirtualFolders)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuser.VirtualFolders = vfolders\n\tif user.Status < 0 || user.Status > 1 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid user status: %v\", user.Status))\n\t}\n\tif err := createUserPasswordHash(user); err != nil {\n\t\treturn err\n\t}\n\tif err := validatePublicKeys(user); err != nil {\n\t\treturn err\n\t}\n\tif err := validateBaseFilters(&user.Filters.BaseUserFilters); err != nil {\n\t\treturn err\n\t}\n\tif !user.HasExternalAuth() {\n\t\tuser.Filters.ExternalAuthCacheTime = 0\n\t}\n\treturn validateCombinedUserFilters(user)\n}\n\nfunc isPasswordOK(user *User, password string) (bool, error) {\n\tif config.PasswordCaching {\n\t\tfound, match := cachedUserPasswords.Check(user.Username, password, user.Password)\n\t\tif found {\n\t\t\treturn match, nil\n\t\t}\n\t}\n\n\tmatch := false\n\tupdatePwd := true\n\tvar err error\n\n\tswitch {\n\tcase strings.HasPrefix(user.Password, bcryptPwdPrefix):\n\t\tif err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {\n\t\t\treturn match, ErrInvalidCredentials\n\t\t}\n\t\tmatch = true\n\t\tupdatePwd = config.PasswordHashing.Algo != HashingAlgoBcrypt\n\tcase strings.HasPrefix(user.Password, argonPwdPrefix):\n\t\tmatch, err = argon2id.ComparePasswordAndHash(password, user.Password)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"error comparing password with argon hash: %v\", err)\n\t\t\treturn match, err\n\t\t}\n\t\tupdatePwd = config.PasswordHashing.Algo != HashingAlgoArgon2ID\n\tcase util.IsStringPrefixInSlice(user.Password, unixPwdPrefixes):\n\t\tmatch, err = compareUnixPasswordAndHash(user, password)\n\t\tif err != nil {\n\t\t\treturn match, err\n\t\t}\n\tcase util.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes):\n\t\tmatch, err = comparePbkdf2PasswordAndHash(password, user.Password)\n\t\tif err != nil {\n\t\t\treturn match, err\n\t\t}\n\tcase util.IsStringPrefixInSlice(user.Password, digestPwdPrefixes):\n\t\tmatch = compareDigestPasswordAndHash(user, password)\n\t}\n\n\tif err == nil && match {\n\t\tcachedUserPasswords.Add(user.Username, password, user.Password)\n\t\tif updatePwd {\n\t\t\tconvertUserPassword(user.Username, password)\n\t\t}\n\t}\n\treturn match, err\n}\n\nfunc convertUserPassword(username, plainPwd string) {\n\thashedPwd, err := hashPlainPassword(plainPwd)\n\tif err == nil {\n\t\terr = provider.updateUserPassword(username, hashedPwd)\n\t}\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"unable to convert password for user %s: %v\", username, err)\n\t} else {\n\t\tproviderLog(logger.LevelDebug, \"password converted for user %s\", username)\n\t}\n}\n\nfunc checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certificate) (User, error) {\n\terr := user.LoadAndApplyGroupSettings()\n\tif err != nil {\n\t\treturn *user, err\n\t}\n\terr = user.CheckLoginConditions()\n\tif err != nil {\n\t\treturn *user, err\n\t}\n\tswitch protocol {\n\tcase protocolFTP, protocolWebDAV:\n\t\tfor _, cert := range user.Filters.TLSCerts {\n\t\t\tderBlock, _ := pem.Decode(util.StringToBytes(cert))\n\t\t\tif derBlock != nil && bytes.Equal(derBlock.Bytes, tlsCert.Raw) {\n\t\t\t\treturn *user, nil\n\t\t\t}\n\t\t}\n\t\tif user.Filters.TLSUsername == sdk.TLSUsernameCN {\n\t\t\tif user.Username == tlsCert.Subject.CommonName {\n\t\t\t\treturn *user, nil\n\t\t\t}\n\t\t\treturn *user, fmt.Errorf(\"CN %q does not match username %q\", tlsCert.Subject.CommonName, user.Username)\n\t\t}\n\t\treturn *user, errors.New(\"TLS certificate is not valid\")\n\tdefault:\n\t\treturn *user, fmt.Errorf(\"certificate authentication is not supported for protocol %v\", protocol)\n\t}\n}\n\nfunc checkUserAndPass(user *User, password, ip, protocol string) (User, error) {\n\terr := user.LoadAndApplyGroupSettings()\n\tif err != nil {\n\t\treturn *user, err\n\t}\n\terr = user.CheckLoginConditions()\n\tif err != nil {\n\t\treturn *user, err\n\t}\n\tif protocol != protocolHTTP && user.MustChangePassword() {\n\t\treturn *user, errors.New(\"login not allowed, password change required\")\n\t}\n\tif user.Filters.IsAnonymous {\n\t\tuser.setAnonymousSettings()\n\t\treturn *user, nil\n\t}\n\tpassword, err = checkUserPasscode(user, password, protocol)\n\tif err != nil {\n\t\treturn *user, ErrInvalidCredentials\n\t}\n\tif user.Password == \"\" || strings.TrimSpace(password) == \"\" {\n\t\treturn *user, errors.New(\"credentials cannot be null or empty\")\n\t}\n\tif !user.Filters.Hooks.CheckPasswordDisabled {\n\t\thookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelDebug, \"error executing check password hook for user %q, ip %v, protocol %v: %v\",\n\t\t\t\tuser.Username, ip, protocol, err)\n\t\t\treturn *user, errors.New(\"unable to check credentials\")\n\t\t}\n\t\tswitch hookResponse.Status {\n\t\tcase -1:\n\t\t\t// no hook configured\n\t\tcase 1:\n\t\t\tproviderLog(logger.LevelDebug, \"password accepted by check password hook for user %q, ip %v, protocol %v\",\n\t\t\t\tuser.Username, ip, protocol)\n\t\t\treturn *user, nil\n\t\tcase 2:\n\t\t\tproviderLog(logger.LevelDebug, \"partial success from check password hook for user %q, ip %v, protocol %v\",\n\t\t\t\tuser.Username, ip, protocol)\n\t\t\tpassword = hookResponse.ToVerify\n\t\tdefault:\n\t\t\tproviderLog(logger.LevelDebug, \"password rejected by check password hook for user %q, ip %v, protocol %v, status: %v\",\n\t\t\t\tuser.Username, ip, protocol, hookResponse.Status)\n\t\t\treturn *user, ErrInvalidCredentials\n\t\t}\n\t}\n\n\tmatch, err := isPasswordOK(user, password)\n\tif !match {\n\t\terr = ErrInvalidCredentials\n\t}\n\treturn *user, err\n}\n\nfunc checkUserPasscode(user *User, password, protocol string) (string, error) {\n\tif user.Filters.TOTPConfig.Enabled {\n\t\tswitch protocol {\n\t\tcase protocolFTP:\n\t\t\tif slices.Contains(user.Filters.TOTPConfig.Protocols, protocol) {\n\t\t\t\t// the TOTP passcode has six digits\n\t\t\t\tpwdLen := len(password)\n\t\t\t\tif pwdLen < 7 {\n\t\t\t\t\tproviderLog(logger.LevelDebug, \"password len %v is too short to contain a passcode, user %q, protocol %v\",\n\t\t\t\t\t\tpwdLen, user.Username, protocol)\n\t\t\t\t\treturn \"\", util.NewValidationError(\"password too short, cannot contain the passcode\")\n\t\t\t\t}\n\t\t\t\terr := user.Filters.TOTPConfig.Secret.TryDecrypt()\n\t\t\t\tif err != nil {\n\t\t\t\t\tproviderLog(logger.LevelError, \"unable to decrypt TOTP secret for user %q, protocol %v, err: %v\",\n\t\t\t\t\t\tuser.Username, protocol, err)\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\tpwd := password[0:(pwdLen - 6)]\n\t\t\t\tpasscode := password[(pwdLen - 6):]\n\t\t\t\tmatch, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode,\n\t\t\t\t\tuser.Filters.TOTPConfig.Secret.GetPayload())\n\t\t\t\tif !match || err != nil {\n\t\t\t\t\tproviderLog(logger.LevelWarn, \"invalid passcode for user %q, protocol %v, err: %v\",\n\t\t\t\t\t\tuser.Username, protocol, err)\n\t\t\t\t\treturn \"\", util.NewValidationError(\"invalid passcode\")\n\t\t\t\t}\n\t\t\t\treturn pwd, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn password, nil\n}\n\nfunc checkUserAndPubKey(user *User, pubKey []byte, isSSHCert bool) (User, string, error) {\n\terr := user.LoadAndApplyGroupSettings()\n\tif err != nil {\n\t\treturn *user, \"\", err\n\t}\n\terr = user.CheckLoginConditions()\n\tif err != nil {\n\t\treturn *user, \"\", err\n\t}\n\tif isSSHCert {\n\t\treturn *user, \"\", nil\n\t}\n\tif len(user.PublicKeys) == 0 {\n\t\treturn *user, \"\", ErrInvalidCredentials\n\t}\n\tfor idx, key := range user.PublicKeys {\n\t\tstoredKey, comment, _, _, err := ssh.ParseAuthorizedKey(util.StringToBytes(key))\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"error parsing stored public key %d for user %s: %v\", idx, user.Username, err)\n\t\t\treturn *user, \"\", err\n\t\t}\n\t\tif bytes.Equal(storedKey.Marshal(), pubKey) {\n\t\t\treturn *user, fmt.Sprintf(\"%s:%s\", ssh.FingerprintSHA256(storedKey), comment), nil\n\t\t}\n\t}\n\treturn *user, \"\", ErrInvalidCredentials\n}\n\nfunc compareDigestPasswordAndHash(user *User, password string) bool {\n\tif strings.HasPrefix(user.Password, md5DigestPwdPrefix) {\n\t\th := md5.New()\n\t\th.Write([]byte(password))\n\t\treturn fmt.Sprintf(\"%s%x\", md5DigestPwdPrefix, h.Sum(nil)) == user.Password\n\t}\n\tif strings.HasPrefix(user.Password, sha256DigestPwdPrefix) {\n\t\th := sha256.New()\n\t\th.Write([]byte(password))\n\t\treturn fmt.Sprintf(\"%s%x\", sha256DigestPwdPrefix, h.Sum(nil)) == user.Password\n\t}\n\tif strings.HasPrefix(user.Password, sha512DigestPwdPrefix) {\n\t\th := sha512.New()\n\t\th.Write([]byte(password))\n\t\treturn fmt.Sprintf(\"%s%x\", sha512DigestPwdPrefix, h.Sum(nil)) == user.Password\n\t}\n\treturn false\n}\n\nfunc compareUnixPasswordAndHash(user *User, password string) (bool, error) {\n\tif strings.HasPrefix(user.Password, yescryptPwdPrefix) {\n\t\treturn compareYescryptPassword(user.Password, password)\n\t}\n\tvar crypter crypt.Crypter\n\tif strings.HasPrefix(user.Password, sha512cryptPwdPrefix) {\n\t\tcrypter = sha512_crypt.New()\n\t} else if strings.HasPrefix(user.Password, sha256cryptPwdPrefix) {\n\t\tcrypter = sha256_crypt.New()\n\t} else if strings.HasPrefix(user.Password, md5cryptPwdPrefix) {\n\t\tcrypter = md5_crypt.New()\n\t} else if strings.HasPrefix(user.Password, md5cryptApr1PwdPrefix) {\n\t\tcrypter = apr1_crypt.New()\n\t} else {\n\t\treturn false, errors.New(\"unix crypt: invalid or unsupported hash format\")\n\t}\n\tif err := crypter.Verify(user.Password, []byte(password)); err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\nfunc comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error) {\n\tvals := strings.Split(hashedPassword, \"$\")\n\tif len(vals) != 5 {\n\t\treturn false, fmt.Errorf(\"pbkdf2: hash is not in the correct format\")\n\t}\n\titerations, err := strconv.Atoi(vals[2])\n\tif err != nil {\n\t\treturn false, err\n\t}\n\texpected, err := base64.StdEncoding.DecodeString(vals[4])\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tvar salt []byte\n\tif util.IsStringPrefixInSlice(hashedPassword, pbkdfPwdB64SaltPrefixes) {\n\t\tsalt, err = base64.StdEncoding.DecodeString(vals[3])\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t} else {\n\t\tsalt = []byte(vals[3])\n\t}\n\tvar hashFunc func() hash.Hash\n\tif strings.HasPrefix(hashedPassword, pbkdf2SHA256Prefix) || strings.HasPrefix(hashedPassword, pbkdf2SHA256B64SaltPrefix) {\n\t\thashFunc = sha256.New\n\t} else if strings.HasPrefix(hashedPassword, pbkdf2SHA512Prefix) {\n\t\thashFunc = sha512.New\n\t} else if strings.HasPrefix(hashedPassword, pbkdf2SHA1Prefix) {\n\t\thashFunc = sha1.New\n\t} else {\n\t\treturn false, fmt.Errorf(\"pbkdf2: invalid or unsupported hash format %v\", vals[1])\n\t}\n\tdf := pbkdf2.Key([]byte(password), salt, iterations, len(expected), hashFunc)\n\treturn subtle.ConstantTimeCompare(df, expected) == 1, nil\n}\n\nfunc getSSLMode() string {\n\tswitch config.Driver {\n\tcase PGSQLDataProviderName, CockroachDataProviderName:\n\t\tswitch config.SSLMode {\n\t\tcase 0:\n\t\t\treturn \"disable\"\n\t\tcase 1:\n\t\t\treturn \"require\"\n\t\tcase 2:\n\t\t\treturn \"verify-ca\"\n\t\tcase 3:\n\t\t\treturn \"verify-full\"\n\t\tcase 4:\n\t\t\treturn \"prefer\"\n\t\tcase 5:\n\t\t\treturn \"allow\"\n\t\t}\n\tcase MySQLDataProviderName:\n\t\tif config.requireCustomTLSForMySQL() {\n\t\t\treturn \"custom\"\n\t\t}\n\t\tswitch config.SSLMode {\n\t\tcase 0:\n\t\t\treturn \"false\"\n\t\tcase 1:\n\t\t\treturn \"true\"\n\t\tcase 2:\n\t\t\treturn \"skip-verify\"\n\t\tcase 3:\n\t\t\treturn \"preferred\"\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) {\n\tif isFinished {\n\t\treturn\n\t}\n\tproviderLog(logger.LevelInfo, \"kill interactive auth program after an unexpected error\")\n\terr := cmd.Process.Kill()\n\tif err != nil {\n\t\tproviderLog(logger.LevelDebug, \"error killing interactive auth program: %v\", err)\n\t}\n}\n\nfunc sendKeyboardAuthHTTPReq(url string, request *plugin.KeyboardAuthRequest) (*plugin.KeyboardAuthResponse, error) {\n\treqAsJSON, err := json.Marshal(request)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error serializing keyboard interactive auth request: %v\", err)\n\t\treturn nil, err\n\t}\n\tresp, err := httpclient.Post(url, \"application/json\", bytes.NewBuffer(reqAsJSON))\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error getting keyboard interactive auth hook HTTP response: %v\", err)\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"wrong keyboard interactive auth http status code: %v, expected 200\", resp.StatusCode)\n\t}\n\tvar response plugin.KeyboardAuthResponse\n\terr = render.DecodeJSON(resp.Body, &response)\n\treturn &response, err\n}\n\nfunc doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractiveChallenge,\n\tip, protocol string, isPartialAuth bool,\n) (int, error) {\n\tif err := user.LoadAndApplyGroupSettings(); err != nil {\n\t\treturn 0, err\n\t}\n\thasSecondFactor := user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)\n\tif !isPartialAuth || !hasSecondFactor {\n\t\tanswers, err := client(\"\", \"\", []string{\"Password: \"}, []bool{false})\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif len(answers) != 1 {\n\t\t\treturn 0, fmt.Errorf(\"unexpected number of answers: %d\", len(answers))\n\t\t}\n\t\t_, err = checkUserAndPass(user, answers[0], ip, protocol)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\treturn checkKeyboardInteractiveSecondFactor(user, client, protocol)\n}\n\nfunc checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {\n\tif !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {\n\t\treturn 1, nil\n\t}\n\terr := user.Filters.TOTPConfig.Secret.TryDecrypt()\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to decrypt TOTP secret for user %q, protocol %v, err: %v\",\n\t\t\tuser.Username, protocol, err)\n\t\treturn 0, err\n\t}\n\tanswers, err := client(\"\", \"\", []string{\"Authentication code: \"}, []bool{false})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif len(answers) != 1 {\n\t\treturn 0, fmt.Errorf(\"unexpected number of answers: %v\", len(answers))\n\t}\n\tmatch, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0],\n\t\tuser.Filters.TOTPConfig.Secret.GetPayload())\n\tif !match || err != nil {\n\t\tproviderLog(logger.LevelWarn, \"invalid passcode for user %q, protocol %v, err: %v\",\n\t\t\tuser.Username, protocol, err)\n\t\treturn 0, util.NewValidationError(\"invalid passcode\")\n\t}\n\treturn 1, nil\n}\n\nfunc executeKeyboardInteractivePlugin(user *User, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {\n\tauthResult := 0\n\trequestID := xid.New().String()\n\tauthStep := 1\n\treq := &plugin.KeyboardAuthRequest{\n\t\tUsername:  user.Username,\n\t\tIP:        ip,\n\t\tPassword:  user.Password,\n\t\tRequestID: requestID,\n\t\tStep:      authStep,\n\t}\n\tvar response *plugin.KeyboardAuthResponse\n\tvar err error\n\tfor {\n\t\tresponse, err = plugin.Handler.ExecuteKeyboardInteractiveStep(req)\n\t\tif err != nil {\n\t\t\treturn authResult, err\n\t\t}\n\t\tif response.AuthResult != 0 {\n\t\t\treturn response.AuthResult, err\n\t\t}\n\t\tif err = response.Validate(); err != nil {\n\t\t\tproviderLog(logger.LevelInfo, \"invalid response from keyboard interactive plugin: %v\", err)\n\t\t\treturn authResult, err\n\t\t}\n\t\tanswers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)\n\t\tif err != nil {\n\t\t\treturn authResult, err\n\t\t}\n\t\tauthStep++\n\t\treq = &plugin.KeyboardAuthRequest{\n\t\t\tRequestID: requestID,\n\t\t\tStep:      authStep,\n\t\t\tUsername:  user.Username,\n\t\t\tPassword:  user.Password,\n\t\t\tAnswers:   answers,\n\t\t\tQuestions: response.Questions,\n\t\t}\n\t}\n}\n\nfunc executeKeyboardInteractiveHTTPHook(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {\n\tauthResult := 0\n\trequestID := xid.New().String()\n\tauthStep := 1\n\treq := &plugin.KeyboardAuthRequest{\n\t\tUsername:  user.Username,\n\t\tIP:        ip,\n\t\tPassword:  user.Password,\n\t\tRequestID: requestID,\n\t\tStep:      authStep,\n\t}\n\tvar response *plugin.KeyboardAuthResponse\n\tvar err error\n\tfor {\n\t\tresponse, err = sendKeyboardAuthHTTPReq(authHook, req)\n\t\tif err != nil {\n\t\t\treturn authResult, err\n\t\t}\n\t\tif response.AuthResult != 0 {\n\t\t\treturn response.AuthResult, err\n\t\t}\n\t\tif err = response.Validate(); err != nil {\n\t\t\tproviderLog(logger.LevelInfo, \"invalid response from keyboard interactive http hook: %v\", err)\n\t\t\treturn authResult, err\n\t\t}\n\t\tanswers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)\n\t\tif err != nil {\n\t\t\treturn authResult, err\n\t\t}\n\t\tauthStep++\n\t\treq = &plugin.KeyboardAuthRequest{\n\t\t\tRequestID: requestID,\n\t\t\tStep:      authStep,\n\t\t\tUsername:  user.Username,\n\t\t\tPassword:  user.Password,\n\t\t\tAnswers:   answers,\n\t\t\tQuestions: response.Questions,\n\t\t}\n\t}\n}\n\nfunc getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, response *plugin.KeyboardAuthResponse,\n\tuser *User, ip, protocol string,\n) ([]string, error) {\n\tquestions := response.Questions\n\tanswers, err := client(\"\", response.Instruction, questions, response.Echos)\n\tif err != nil {\n\t\tproviderLog(logger.LevelInfo, \"error getting interactive auth client response: %v\", err)\n\t\treturn answers, err\n\t}\n\tif len(answers) != len(questions) {\n\t\terr = fmt.Errorf(\"client answers does not match questions, expected: %v actual: %v\", questions, answers)\n\t\tproviderLog(logger.LevelInfo, \"keyboard interactive auth error: %v\", err)\n\t\treturn answers, err\n\t}\n\tif len(answers) == 1 && response.CheckPwd > 0 {\n\t\tif response.CheckPwd == 2 {\n\t\t\tif !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {\n\t\t\t\tproviderLog(logger.LevelInfo, \"keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q\",\n\t\t\t\t\tuser.Username)\n\t\t\t\treturn answers, errors.New(\"TOTP not enabled for SSH protocol\")\n\t\t\t}\n\t\t\terr := user.Filters.TOTPConfig.Secret.TryDecrypt()\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"unable to decrypt TOTP secret for user %q, protocol %v, err: %v\",\n\t\t\t\t\tuser.Username, protocol, err)\n\t\t\t\treturn answers, fmt.Errorf(\"unable to decrypt TOTP secret: %w\", err)\n\t\t\t}\n\t\t\tmatch, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, answers[0],\n\t\t\t\tuser.Filters.TOTPConfig.Secret.GetPayload())\n\t\t\tif !match || err != nil {\n\t\t\t\tproviderLog(logger.LevelInfo, \"keyboard interactive auth error: unable to validate passcode for user %q, match? %v, err: %v\",\n\t\t\t\t\tuser.Username, match, err)\n\t\t\t\treturn answers, errors.New(\"unable to validate TOTP passcode\")\n\t\t\t}\n\t\t} else {\n\t\t\t_, err = checkUserAndPass(user, answers[0], ip, protocol)\n\t\t\tproviderLog(logger.LevelInfo, \"interactive auth hook requested password validation for user %q, validation error: %v\",\n\t\t\t\tuser.Username, err)\n\t\t\tif err != nil {\n\t\t\t\treturn answers, err\n\t\t\t}\n\t\t}\n\t\tanswers[0] = \"OK\"\n\t}\n\treturn answers, err\n}\n\nfunc handleProgramInteractiveQuestions(client ssh.KeyboardInteractiveChallenge, response *plugin.KeyboardAuthResponse,\n\tuser *User, stdin io.WriteCloser, ip, protocol string,\n) error {\n\tanswers, err := getKeyboardInteractiveAnswers(client, response, user, ip, protocol)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, answer := range answers {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tanswer += \"\\r\"\n\t\t}\n\t\tanswer += \"\\n\"\n\t\t_, err = stdin.Write([]byte(answer))\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to write client answer to keyboard interactive program: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc executeKeyboardInteractiveProgram(user *User, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {\n\tauthResult := 0\n\ttimeout, env, args := command.GetConfig(authHook, command.HookKeyboardInteractive)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, authHook, args...)\n\tcmd.Env = append(env,\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_USERNAME=%s\", user.Username),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_IP=%s\", ip),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_PASSWORD=%s\", user.Password))\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn authResult, err\n\t}\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn authResult, err\n\t}\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn authResult, err\n\t}\n\tvar once sync.Once\n\tscanner := bufio.NewScanner(stdout)\n\tfor scanner.Scan() {\n\t\tvar response plugin.KeyboardAuthResponse\n\t\terr = json.Unmarshal(scanner.Bytes(), &response)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelInfo, \"interactive auth error parsing response: %v\", err)\n\t\t\tonce.Do(func() { terminateInteractiveAuthProgram(cmd, false) })\n\t\t\tbreak\n\t\t}\n\t\tif response.AuthResult != 0 {\n\t\t\tauthResult = response.AuthResult\n\t\t\tbreak\n\t\t}\n\t\tif err = response.Validate(); err != nil {\n\t\t\tproviderLog(logger.LevelInfo, \"invalid response from keyboard interactive program: %v\", err)\n\t\t\tonce.Do(func() { terminateInteractiveAuthProgram(cmd, false) })\n\t\t\tbreak\n\t\t}\n\t\tgo func() {\n\t\t\terr := handleProgramInteractiveQuestions(client, &response, user, stdin, ip, protocol)\n\t\t\tif err != nil {\n\t\t\t\tonce.Do(func() { terminateInteractiveAuthProgram(cmd, false) })\n\t\t\t}\n\t\t}()\n\t}\n\tstdin.Close()\n\tonce.Do(func() { terminateInteractiveAuthProgram(cmd, true) })\n\tgo func() {\n\t\t_, err := cmd.Process.Wait()\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"error waiting for %q process to exit: %v\", authHook, err)\n\t\t}\n\t}()\n\n\treturn authResult, err\n}\n\nfunc doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardInteractiveChallenge,\n\tip, protocol string, isPartialAuth bool,\n) (User, error) {\n\tif err := user.LoadAndApplyGroupSettings(); err != nil {\n\t\treturn *user, err\n\t}\n\tvar authResult int\n\tvar err error\n\tif !user.Filters.Hooks.ExternalAuthDisabled {\n\t\tif plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {\n\t\t\tauthResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)\n\t\t\tif authResult == 1 && err == nil {\n\t\t\t\tauthResult, err = checkKeyboardInteractiveSecondFactor(user, client, protocol)\n\t\t\t}\n\t\t} else if authHook != \"\" {\n\t\t\tif strings.HasPrefix(authHook, \"http\") {\n\t\t\t\tauthResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)\n\t\t\t} else {\n\t\t\t\tauthResult, err = executeKeyboardInteractiveProgram(user, authHook, client, ip, protocol)\n\t\t\t}\n\t\t} else {\n\t\t\tauthResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol, isPartialAuth)\n\t\t}\n\t} else {\n\t\tauthResult, err = doBuiltinKeyboardInteractiveAuth(user, client, ip, protocol, isPartialAuth)\n\t}\n\tif err != nil {\n\t\treturn *user, err\n\t}\n\tif authResult != 1 {\n\t\treturn *user, fmt.Errorf(\"keyboard interactive auth failed, result: %v\", authResult)\n\t}\n\terr = user.CheckLoginConditions()\n\tif err != nil {\n\t\treturn *user, err\n\t}\n\treturn *user, nil\n}\n\nfunc isCheckPasswordHookDefined(protocol string) bool {\n\tif config.CheckPasswordHook == \"\" {\n\t\treturn false\n\t}\n\tif config.CheckPasswordScope == 0 {\n\t\treturn true\n\t}\n\tswitch protocol {\n\tcase protocolSSH:\n\t\treturn config.CheckPasswordScope&1 != 0\n\tcase protocolFTP:\n\t\treturn config.CheckPasswordScope&2 != 0\n\tcase protocolWebDAV:\n\t\treturn config.CheckPasswordScope&4 != 0\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc getPasswordHookResponse(username, password, ip, protocol string) ([]byte, error) {\n\tif strings.HasPrefix(config.CheckPasswordHook, \"http\") {\n\t\tvar result []byte\n\t\treq := checkPasswordRequest{\n\t\t\tUsername: username,\n\t\t\tPassword: password,\n\t\t\tIP:       ip,\n\t\t\tProtocol: protocol,\n\t\t}\n\t\treqAsJSON, err := json.Marshal(req)\n\t\tif err != nil {\n\t\t\treturn result, err\n\t\t}\n\t\tresp, err := httpclient.Post(config.CheckPasswordHook, \"application/json\", bytes.NewBuffer(reqAsJSON))\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"error getting check password hook response: %v\", err)\n\t\t\treturn result, err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn result, fmt.Errorf(\"wrong http status code from chek password hook: %v, expected 200\", resp.StatusCode)\n\t\t}\n\t\treturn io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize))\n\t}\n\ttimeout, env, args := command.GetConfig(config.CheckPasswordHook, command.HookCheckPassword)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, config.CheckPasswordHook, args...)\n\tcmd.Env = append(env,\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_USERNAME=%s\", username),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_PASSWORD=%s\", password),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_IP=%s\", ip),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_PROTOCOL=%s\", protocol),\n\t)\n\treturn getCmdOutput(cmd, \"check_password_hook\")\n}\n\nfunc executeCheckPasswordHook(username, password, ip, protocol string) (checkPasswordResponse, error) {\n\tvar response checkPasswordResponse\n\n\tif !isCheckPasswordHookDefined(protocol) {\n\t\tresponse.Status = -1\n\t\treturn response, nil\n\t}\n\n\tstartTime := time.Now()\n\tout, err := getPasswordHookResponse(username, password, ip, protocol)\n\tproviderLog(logger.LevelDebug, \"check password hook executed, error: %v, elapsed: %v\", err, time.Since(startTime))\n\tif err != nil {\n\t\treturn response, err\n\t}\n\terr = json.Unmarshal(out, &response)\n\treturn response, err\n}\n\nfunc getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\tif strings.HasPrefix(config.PreLoginHook, \"http\") {\n\t\tvar url *url.URL\n\t\tvar result []byte\n\t\turl, err := url.Parse(config.PreLoginHook)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"invalid url for pre-login hook %q, error: %v\", config.PreLoginHook, err)\n\t\t\treturn result, err\n\t\t}\n\t\tq := url.Query()\n\t\tq.Add(\"login_method\", loginMethod)\n\t\tq.Add(\"ip\", ip)\n\t\tq.Add(\"protocol\", protocol)\n\t\turl.RawQuery = q.Encode()\n\n\t\tresp, err := httpclient.Post(url.String(), \"application/json\", bytes.NewBuffer(userAsJSON))\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"error getting pre-login hook response: %v\", err)\n\t\t\treturn result, err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.StatusCode == http.StatusNoContent {\n\t\t\treturn result, nil\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn result, fmt.Errorf(\"wrong pre-login hook http status code: %v, expected 200\", resp.StatusCode)\n\t\t}\n\t\treturn io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize))\n\t}\n\ttimeout, env, args := command.GetConfig(config.PreLoginHook, command.HookPreLogin)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, config.PreLoginHook, args...)\n\tcmd.Env = append(env,\n\t\tfmt.Sprintf(\"SFTPGO_LOGIND_USER=%s\", userAsJSON),\n\t\tfmt.Sprintf(\"SFTPGO_LOGIND_METHOD=%s\", loginMethod),\n\t\tfmt.Sprintf(\"SFTPGO_LOGIND_IP=%s\", ip),\n\t\tfmt.Sprintf(\"SFTPGO_LOGIND_PROTOCOL=%s\", protocol),\n\t)\n\treturn getCmdOutput(cmd, \"pre_login_hook\")\n}\n\nfunc executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFields *map[string]any) (User, error) {\n\tvar user User\n\n\tu, mergedUser, userAsJSON, err := getUserAndJSONForHook(username, oidcTokenFields)\n\tif err != nil {\n\t\treturn u, err\n\t}\n\tif mergedUser.Filters.Hooks.PreLoginDisabled {\n\t\treturn u, nil\n\t}\n\tstartTime := time.Now()\n\tout, err := getPreLoginHookResponse(loginMethod, ip, protocol, userAsJSON)\n\tif err != nil {\n\t\treturn u, fmt.Errorf(\"pre-login hook error: %v, username %q, ip %v, protocol %v elapsed %v\",\n\t\t\terr, username, ip, protocol, time.Since(startTime))\n\t}\n\tproviderLog(logger.LevelDebug, \"pre-login hook completed, elapsed: %s\", time.Since(startTime))\n\tif util.IsByteArrayEmpty(out) {\n\t\tproviderLog(logger.LevelDebug, \"empty response from pre-login hook, no modification requested for user %q id: %d\",\n\t\t\tusername, u.ID)\n\t\tif u.ID == 0 {\n\t\t\treturn u, util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n\t\t}\n\t\treturn u, nil\n\t}\n\terr = json.Unmarshal(out, &user)\n\tif err != nil {\n\t\treturn u, fmt.Errorf(\"invalid pre-login hook response %q, error: %v\", out, err)\n\t}\n\tif u.ID > 0 {\n\t\tuser.ID = u.ID\n\t\tuser.UsedQuotaSize = u.UsedQuotaSize\n\t\tuser.UsedQuotaFiles = u.UsedQuotaFiles\n\t\tuser.UsedUploadDataTransfer = u.UsedUploadDataTransfer\n\t\tuser.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer\n\t\tuser.LastQuotaUpdate = u.LastQuotaUpdate\n\t\tuser.LastLogin = u.LastLogin\n\t\tuser.LastPasswordChange = u.LastPasswordChange\n\t\tuser.FirstDownload = u.FirstDownload\n\t\tuser.FirstUpload = u.FirstUpload\n\t\t// preserve TOTP config and recovery codes\n\t\tuser.Filters.TOTPConfig = u.Filters.TOTPConfig\n\t\tuser.Filters.RecoveryCodes = u.Filters.RecoveryCodes\n\t\tif err := provider.updateUser(&user); err != nil {\n\t\t\treturn u, err\n\t\t}\n\t} else {\n\t\tif err := provider.addUser(&user); err != nil {\n\t\t\treturn u, err\n\t\t}\n\t}\n\tuser, err = provider.userExists(user.Username, \"\")\n\tif err != nil {\n\t\treturn u, err\n\t}\n\tproviderLog(logger.LevelDebug, \"user %q added/updated from pre-login hook response, id: %d\", username, u.ID)\n\tif u.ID > 0 {\n\t\twebDAVUsersCache.swap(&user, \"\")\n\t}\n\treturn user, nil\n}\n\n// ExecutePostLoginHook executes the post login hook if defined\nfunc ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err error) {\n\tif config.PostLoginHook == \"\" {\n\t\treturn\n\t}\n\tif config.PostLoginScope == 1 && err == nil {\n\t\treturn\n\t}\n\tif config.PostLoginScope == 2 && err != nil {\n\t\treturn\n\t}\n\n\tgo func() {\n\t\tactionsConcurrencyGuard <- struct{}{}\n\t\tdefer func() {\n\t\t\t<-actionsConcurrencyGuard\n\t\t}()\n\n\t\tstatus := \"0\"\n\t\tif err == nil {\n\t\t\tstatus = \"1\"\n\t\t}\n\n\t\tuser.PrepareForRendering()\n\t\tuserAsJSON, err := json.Marshal(user)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"error serializing user in post login hook: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tif strings.HasPrefix(config.PostLoginHook, \"http\") {\n\t\t\tvar url *url.URL\n\t\t\turl, err := url.Parse(config.PostLoginHook)\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelDebug, \"Invalid post-login hook %q\", config.PostLoginHook)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tq := url.Query()\n\t\t\tq.Add(\"login_method\", loginMethod)\n\t\t\tq.Add(\"ip\", ip)\n\t\t\tq.Add(\"protocol\", protocol)\n\t\t\tq.Add(\"status\", status)\n\t\t\turl.RawQuery = q.Encode()\n\n\t\t\tstartTime := time.Now()\n\t\t\trespCode := 0\n\t\t\tresp, err := httpclient.RetryablePost(url.String(), \"application/json\", bytes.NewBuffer(userAsJSON))\n\t\t\tif err == nil {\n\t\t\t\trespCode = resp.StatusCode\n\t\t\t\tresp.Body.Close()\n\t\t\t}\n\t\t\tproviderLog(logger.LevelDebug, \"post login hook executed for user %q, ip %v, protocol %v, response code: %v, elapsed: %v err: %v\",\n\t\t\t\tuser.Username, ip, protocol, respCode, time.Since(startTime), err)\n\t\t\treturn\n\t\t}\n\t\ttimeout, env, args := command.GetConfig(config.PostLoginHook, command.HookPostLogin)\n\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\tdefer cancel()\n\n\t\tcmd := exec.CommandContext(ctx, config.PostLoginHook, args...)\n\t\tcmd.Env = append(env,\n\t\t\tfmt.Sprintf(\"SFTPGO_LOGIND_USER=%s\", userAsJSON),\n\t\t\tfmt.Sprintf(\"SFTPGO_LOGIND_IP=%s\", ip),\n\t\t\tfmt.Sprintf(\"SFTPGO_LOGIND_METHOD=%s\", loginMethod),\n\t\t\tfmt.Sprintf(\"SFTPGO_LOGIND_STATUS=%s\", status),\n\t\t\tfmt.Sprintf(\"SFTPGO_LOGIND_PROTOCOL=%s\", protocol))\n\t\tstartTime := time.Now()\n\t\terr = cmd.Run()\n\t\tproviderLog(logger.LevelDebug, \"post login hook executed for user %q, ip %v, protocol %v, elapsed %v err: %v\",\n\t\t\tuser.Username, ip, protocol, time.Since(startTime), err)\n\t}()\n}\n\nfunc getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol string, cert *x509.Certificate,\n\tuser User,\n) ([]byte, error) {\n\tvar tlsCert string\n\tif cert != nil {\n\t\tvar err error\n\t\ttlsCert, err = util.EncodeTLSCertToPem(cert)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif strings.HasPrefix(config.ExternalAuthHook, \"http\") {\n\t\tvar result []byte\n\t\tauthRequest := make(map[string]any)\n\t\tauthRequest[\"username\"] = username\n\t\tauthRequest[\"ip\"] = ip\n\t\tauthRequest[\"password\"] = password\n\t\tauthRequest[\"public_key\"] = pkey\n\t\tauthRequest[\"protocol\"] = protocol\n\t\tauthRequest[\"keyboard_interactive\"] = keyboardInteractive\n\t\tauthRequest[\"tls_cert\"] = tlsCert\n\t\tif user.ID > 0 {\n\t\t\tauthRequest[\"user\"] = user\n\t\t}\n\t\tauthRequestAsJSON, err := json.Marshal(authRequest)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"error serializing external auth request: %v\", err)\n\t\t\treturn result, err\n\t\t}\n\t\tresp, err := httpclient.Post(config.ExternalAuthHook, \"application/json\", bytes.NewBuffer(authRequestAsJSON))\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"error getting external auth hook HTTP response: %v\", err)\n\t\t\treturn result, err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tproviderLog(logger.LevelDebug, \"external auth hook executed, response code: %v\", resp.StatusCode)\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn result, fmt.Errorf(\"wrong external auth http status code: %v, expected 200\", resp.StatusCode)\n\t\t}\n\n\t\treturn io.ReadAll(io.LimitReader(resp.Body, maxHookResponseSize))\n\t}\n\tvar userAsJSON []byte\n\tvar err error\n\tif user.ID > 0 {\n\t\tuserAsJSON, err = json.Marshal(user)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to serialize user as JSON: %w\", err)\n\t\t}\n\t}\n\ttimeout, env, args := command.GetConfig(config.ExternalAuthHook, command.HookExternalAuth)\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, config.ExternalAuthHook, args...)\n\tcmd.Env = append(env,\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_USERNAME=%s\", username),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_USER=%s\", userAsJSON),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_IP=%s\", ip),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_PASSWORD=%s\", password),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_PUBLIC_KEY=%s\", pkey),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_PROTOCOL=%s\", protocol),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_TLS_CERT=%s\", strings.ReplaceAll(tlsCert, \"\\n\", \"\\\\n\")),\n\t\tfmt.Sprintf(\"SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v\", keyboardInteractive))\n\n\treturn getCmdOutput(cmd, \"external_auth_hook\")\n}\n\nfunc updateUserFromExtAuthResponse(user *User, password, pkey string) {\n\tif password != \"\" {\n\t\tuser.Password = password\n\t}\n\tif pkey != \"\" && !util.IsStringPrefixInSlice(pkey, user.PublicKeys) {\n\t\tuser.PublicKeys = append(user.PublicKeys, pkey)\n\t}\n\tuser.LastPasswordChange = 0\n}\n\nfunc checkPasswordAfterEmptyExtAuthResponse(user *User, plainPwd, protocol string) error {\n\tif plainPwd == \"\" {\n\t\treturn nil\n\t}\n\tmatch, err := isPasswordOK(user, plainPwd)\n\tif match && err == nil {\n\t\treturn nil\n\t}\n\n\thashedPwd, err := hashPlainPassword(plainPwd)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to hash password for user %q after empty external response: %v\",\n\t\t\tuser.Username, err)\n\t\treturn err\n\t}\n\terr = provider.updateUserPassword(user.Username, hashedPwd)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to update password for user %q after empty external response: %v\",\n\t\t\tuser.Username, err)\n\t}\n\tuser.Password = hashedPwd\n\tcachedUserPasswords.Add(user.Username, plainPwd, user.Password)\n\tif protocol != protocolWebDAV {\n\t\twebDAVUsersCache.swap(user, plainPwd)\n\t}\n\tproviderLog(logger.LevelDebug, \"updated password for user %q after empty external auth response\", user.Username)\n\treturn nil\n}\n\nfunc doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string,\n\ttlsCert *x509.Certificate,\n) (User, error) {\n\tvar user User\n\n\tu, mergedUser, err := getUserForHook(username, nil)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\n\tif mergedUser.skipExternalAuth() {\n\t\treturn u, nil\n\t}\n\n\tpkey, err := util.GetSSHPublicKeyAsString(pubKey)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\n\tstartTime := time.Now()\n\tout, err := getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, protocol, tlsCert, u)\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"external auth error for user %q, elapsed: %s: %w\", username, time.Since(startTime), err)\n\t}\n\tproviderLog(logger.LevelDebug, \"external auth completed for user %q, elapsed: %s\", username, time.Since(startTime))\n\tif util.IsByteArrayEmpty(out) {\n\t\tproviderLog(logger.LevelDebug, \"empty response from external hook, no modification requested for user %q, id: %d\",\n\t\t\tusername, u.ID)\n\t\tif u.ID == 0 {\n\t\t\treturn u, util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n\t\t}\n\t\terr = checkPasswordAfterEmptyExtAuthResponse(&u, password, protocol)\n\t\treturn u, err\n\t}\n\terr = json.Unmarshal(out, &user)\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"invalid external auth response: %v\", err)\n\t}\n\t// an empty username means authentication failure\n\tif user.Username == \"\" {\n\t\treturn user, ErrInvalidCredentials\n\t}\n\tupdateUserFromExtAuthResponse(&user, password, pkey)\n\t// some users want to map multiple login usernames with a single SFTPGo account\n\t// for example an SFTP user logins using \"user1\" or \"user2\" and the external auth\n\t// returns \"user\" in both cases, so we use the username returned from\n\t// external auth and not the one used to login\n\tif user.Username != username {\n\t\tu, err = provider.userExists(user.Username, \"\")\n\t}\n\tif u.ID > 0 && err == nil {\n\t\tuser.ID = u.ID\n\t\tuser.UsedQuotaSize = u.UsedQuotaSize\n\t\tuser.UsedQuotaFiles = u.UsedQuotaFiles\n\t\tuser.UsedUploadDataTransfer = u.UsedUploadDataTransfer\n\t\tuser.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer\n\t\tuser.LastQuotaUpdate = u.LastQuotaUpdate\n\t\tuser.LastLogin = u.LastLogin\n\t\tuser.LastPasswordChange = u.LastPasswordChange\n\t\tuser.FirstDownload = u.FirstDownload\n\t\tuser.FirstUpload = u.FirstUpload\n\t\tuser.CreatedAt = u.CreatedAt\n\t\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t// preserve TOTP config and recovery codes\n\t\tuser.Filters.TOTPConfig = u.Filters.TOTPConfig\n\t\tuser.Filters.RecoveryCodes = u.Filters.RecoveryCodes\n\t\tuser, err = updateUserAfterExternalAuth(&user)\n\t\tif err == nil {\n\t\t\tif protocol != protocolWebDAV {\n\t\t\t\twebDAVUsersCache.swap(&user, password)\n\t\t\t}\n\t\t\tcachedUserPasswords.Add(user.Username, password, user.Password)\n\t\t}\n\t\treturn user, err\n\t}\n\terr = provider.addUser(&user)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\treturn provider.userExists(user.Username, \"\")\n}\n\nfunc doPluginAuth(username, password string, pubKey []byte, ip, protocol string,\n\ttlsCert *x509.Certificate, authScope int,\n) (User, error) {\n\tvar user User\n\n\tu, mergedUser, userAsJSON, err := getUserAndJSONForHook(username, nil)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\n\tif mergedUser.skipExternalAuth() {\n\t\treturn u, nil\n\t}\n\n\tpkey, err := util.GetSSHPublicKeyAsString(pubKey)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\n\tstartTime := time.Now()\n\n\tout, err := plugin.Handler.Authenticate(username, password, ip, protocol, pkey, tlsCert, authScope, userAsJSON)\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"plugin auth error for user %q: %v, elapsed: %v, auth scope: %d\",\n\t\t\tusername, err, time.Since(startTime), authScope)\n\t}\n\tproviderLog(logger.LevelDebug, \"plugin auth completed for user %q, elapsed: %v, auth scope: %d\",\n\t\tusername, time.Since(startTime), authScope)\n\tif util.IsByteArrayEmpty(out) {\n\t\tproviderLog(logger.LevelDebug, \"empty response from plugin auth, no modification requested for user %q id: %d, auth scope: %d\",\n\t\t\tusername, u.ID, authScope)\n\t\tif u.ID == 0 {\n\t\t\treturn u, util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n\t\t}\n\t\terr = checkPasswordAfterEmptyExtAuthResponse(&u, password, protocol)\n\t\treturn u, err\n\t}\n\terr = json.Unmarshal(out, &user)\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"invalid plugin auth response: %v\", err)\n\t}\n\tupdateUserFromExtAuthResponse(&user, password, pkey)\n\tif u.ID > 0 {\n\t\tuser.ID = u.ID\n\t\tuser.UsedQuotaSize = u.UsedQuotaSize\n\t\tuser.UsedQuotaFiles = u.UsedQuotaFiles\n\t\tuser.UsedUploadDataTransfer = u.UsedUploadDataTransfer\n\t\tuser.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer\n\t\tuser.LastQuotaUpdate = u.LastQuotaUpdate\n\t\tuser.LastLogin = u.LastLogin\n\t\tuser.LastPasswordChange = u.LastPasswordChange\n\t\tuser.FirstDownload = u.FirstDownload\n\t\tuser.FirstUpload = u.FirstUpload\n\t\t// preserve TOTP config and recovery codes\n\t\tuser.Filters.TOTPConfig = u.Filters.TOTPConfig\n\t\tuser.Filters.RecoveryCodes = u.Filters.RecoveryCodes\n\t\tuser, err = updateUserAfterExternalAuth(&user)\n\t\tif err == nil {\n\t\t\tif protocol != protocolWebDAV {\n\t\t\t\twebDAVUsersCache.swap(&user, password)\n\t\t\t}\n\t\t\tcachedUserPasswords.Add(user.Username, password, user.Password)\n\t\t}\n\t\treturn user, err\n\t}\n\terr = provider.addUser(&user)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\treturn provider.userExists(user.Username, \"\")\n}\n\nfunc updateUserAfterExternalAuth(user *User) (User, error) {\n\tif err := provider.updateUser(user); err != nil {\n\t\treturn *user, err\n\t}\n\treturn provider.userExists(user.Username, \"\")\n}\n\nfunc getUserForHook(username string, oidcTokenFields *map[string]any) (User, User, error) {\n\tu, err := provider.userExists(username, \"\")\n\tif err != nil {\n\t\tif !errors.Is(err, util.ErrNotFound) {\n\t\t\treturn u, u, err\n\t\t}\n\t\tu = User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tID:       0,\n\t\t\t\tUsername: username,\n\t\t\t},\n\t\t}\n\t}\n\tmergedUser := u.getACopy()\n\terr = mergedUser.LoadAndApplyGroupSettings()\n\tif err != nil {\n\t\treturn u, mergedUser, err\n\t}\n\n\tu.OIDCCustomFields = oidcTokenFields\n\treturn u, mergedUser, err\n}\n\nfunc getUserAndJSONForHook(username string, oidcTokenFields *map[string]any) (User, User, []byte, error) {\n\tu, mergedUser, err := getUserForHook(username, oidcTokenFields)\n\tif err != nil {\n\t\treturn u, mergedUser, nil, err\n\t}\n\tuserAsJSON, err := json.Marshal(u)\n\tif err != nil {\n\t\treturn u, mergedUser, userAsJSON, err\n\t}\n\treturn u, mergedUser, userAsJSON, err\n}\n\nfunc isLastActivityRecent(lastActivity int64, minDelay time.Duration) bool {\n\tlastActivityTime := util.GetTimeFromMsecSinceEpoch(lastActivity)\n\tdiff := -time.Until(lastActivityTime)\n\tif diff < -10*time.Second {\n\t\treturn false\n\t}\n\treturn diff < minDelay\n}\n\nfunc isExternalAuthConfigured(loginMethod string) bool {\n\tif config.ExternalAuthHook != \"\" {\n\t\tif config.ExternalAuthScope == 0 {\n\t\t\treturn true\n\t\t}\n\t\tswitch loginMethod {\n\t\tcase LoginMethodPassword:\n\t\t\treturn config.ExternalAuthScope&1 != 0\n\t\tcase LoginMethodTLSCertificate:\n\t\t\treturn config.ExternalAuthScope&8 != 0\n\t\tcase LoginMethodTLSCertificateAndPwd:\n\t\t\treturn config.ExternalAuthScope&1 != 0 || config.ExternalAuthScope&8 != 0\n\t\t}\n\t}\n\tswitch loginMethod {\n\tcase LoginMethodPassword:\n\t\treturn plugin.Handler.HasAuthScope(plugin.AuthScopePassword)\n\tcase LoginMethodTLSCertificate:\n\t\treturn plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate)\n\tcase LoginMethodTLSCertificateAndPwd:\n\t\treturn plugin.Handler.HasAuthScope(plugin.AuthScopePassword) ||\n\t\t\tplugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate)\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc replaceTemplateVars(input string) string {\n\tvar result strings.Builder\n\ti := 0\n\tfor i < len(input) {\n\t\tif i+2 <= len(input) && input[i:i+2] == \"{{\" {\n\t\t\tif i+2 < len(input) {\n\t\t\t\tnextChar := input[i+2]\n\t\t\t\tif nextChar == ' ' || nextChar == '.' || nextChar == '-' {\n\t\t\t\t\t// Don't replace if followed by space, dot or minus.\n\t\t\t\t\tresult.WriteString(\"{{\")\n\t\t\t\t\ti += 2\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Find the closing \"}}\"\n\t\t\tclosing := strings.Index(input[i:], \"}}\")\n\t\t\tif closing != -1 {\n\t\t\t\t// Replace with {{. only if it's a proper template variable.\n\t\t\t\tresult.WriteString(\"{{.\")\n\t\t\t\tresult.WriteString(input[i+2 : i+closing])\n\t\t\t\tresult.WriteString(\"}}\")\n\t\t\t\ti += closing + 2\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tresult.WriteByte(input[i])\n\t\ti++\n\t}\n\treturn result.String()\n}\n\nfunc updateEventActionPlaceholders(actions []BaseEventAction) ([]BaseEventAction, error) {\n\tvar result []BaseEventAction\n\n\tfor _, action := range actions {\n\t\toptions, err := json.Marshal(action.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tconvertedOptions := replaceTemplateVars(string(options))\n\t\tvar opts BaseEventActionOptions\n\t\terr = json.Unmarshal([]byte(convertedOptions), &opts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\taction.Options = opts\n\t\tresult = append(result, action)\n\t}\n\n\treturn result, nil\n}\n\nfunc getConfigPath(name, configDir string) string {\n\tif !util.IsFileInputValid(name) {\n\t\treturn \"\"\n\t}\n\tif name != \"\" && !filepath.IsAbs(name) {\n\t\treturn filepath.Join(configDir, name)\n\t}\n\treturn name\n}\n\nfunc checkReservedUsernames(username string) error {\n\tif slices.Contains(reservedUsers, username) {\n\t\treturn util.NewValidationError(\"this username is reserved\")\n\t}\n\treturn nil\n}\n\nfunc errSchemaVersionTooOld(version int) error {\n\treturn fmt.Errorf(\"database schema version %d is too old, please see the upgrading docs: https://docs.sftpgo.com/latest/data-provider/#upgrading\", version)\n}\n\nfunc getCmdOutput(cmd *exec.Cmd, sender string) ([]byte, error) {\n\tvar stdout bytes.Buffer\n\tcmd.Stdout = &stdout\n\n\tstderr, err := cmd.StderrPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tscanner := bufio.NewScanner(stderr)\n\n\tgo func() {\n\t\tfor scanner.Scan() {\n\t\t\tif out := scanner.Text(); out != \"\" {\n\t\t\t\tlogger.Log(logger.LevelWarn, sender, \"\", \"%s\", out)\n\t\t\t}\n\t\t}\n\t\tif err := scanner.Err(); err != nil {\n\t\t\tlogger.Log(logger.LevelError, sender, \"\", \"error reading stderr: %v\", err)\n\t\t}\n\t}()\n\n\terr = cmd.Wait()\n\treturn stdout.Bytes(), err\n}\n\nfunc providerLog(level logger.LogLevel, format string, v ...any) {\n\tlogger.Log(level, logSender, \"\", format, v...)\n}\n"
  },
  {
    "path": "internal/dataprovider/eventrule.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/robfig/cron/v3\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// Supported event actions\nconst (\n\tActionTypeHTTP = iota + 1\n\tActionTypeCommand\n\tActionTypeEmail\n\tActionTypeBackup\n\tActionTypeUserQuotaReset\n\tActionTypeFolderQuotaReset\n\tActionTypeTransferQuotaReset\n\tActionTypeDataRetentionCheck\n\tActionTypeFilesystem\n\tactionTypeReserved\n\tActionTypePasswordExpirationCheck\n\tActionTypeUserExpirationCheck\n\tActionTypeIDPAccountCheck\n\tActionTypeUserInactivityCheck\n\tActionTypeRotateLogs\n)\n\nvar (\n\tsupportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,\n\t\tActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,\n\t\tActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck,\n\t\tActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck, ActionTypeRotateLogs}\n\t// EnabledActionCommands defines the system commands that can be executed via EventManager,\n\t// an empty list means that no command is allowed to be executed.\n\tEnabledActionCommands []string\n)\n\nfunc isActionTypeValid(action int) bool {\n\treturn slices.Contains(supportedEventActions, action)\n}\n\nfunc getActionTypeAsString(action int) string {\n\tswitch action {\n\tcase ActionTypeHTTP:\n\t\treturn util.I18nActionTypeHTTP\n\tcase ActionTypeEmail:\n\t\treturn util.I18nActionTypeEmail\n\tcase ActionTypeBackup:\n\t\treturn util.I18nActionTypeBackup\n\tcase ActionTypeUserQuotaReset:\n\t\treturn util.I18nActionTypeUserQuotaReset\n\tcase ActionTypeFolderQuotaReset:\n\t\treturn util.I18nActionTypeFolderQuotaReset\n\tcase ActionTypeTransferQuotaReset:\n\t\treturn util.I18nActionTypeTransferQuotaReset\n\tcase ActionTypeDataRetentionCheck:\n\t\treturn util.I18nActionTypeDataRetentionCheck\n\tcase ActionTypeFilesystem:\n\t\treturn util.I18nActionTypeFilesystem\n\tcase ActionTypePasswordExpirationCheck:\n\t\treturn util.I18nActionTypePwdExpirationCheck\n\tcase ActionTypeUserExpirationCheck:\n\t\treturn util.I18nActionTypeUserExpirationCheck\n\tcase ActionTypeUserInactivityCheck:\n\t\treturn util.I18nActionTypeUserInactivityCheck\n\tcase ActionTypeIDPAccountCheck:\n\t\treturn util.I18nActionTypeIDPCheck\n\tcase ActionTypeRotateLogs:\n\t\treturn util.I18nActionTypeRotateLogs\n\tdefault:\n\t\treturn util.I18nActionTypeCommand\n\t}\n}\n\n// Supported event triggers\nconst (\n\t// Filesystem events such as upload, download, mkdir ...\n\tEventTriggerFsEvent = iota + 1\n\t// Provider events such as add, update, delete\n\tEventTriggerProviderEvent\n\tEventTriggerSchedule\n\tEventTriggerIPBlocked\n\tEventTriggerCertificate\n\tEventTriggerOnDemand\n\tEventTriggerIDPLogin\n)\n\nvar (\n\tsupportedEventTriggers = []int{EventTriggerFsEvent, EventTriggerProviderEvent, EventTriggerSchedule,\n\t\tEventTriggerIPBlocked, EventTriggerCertificate, EventTriggerIDPLogin, EventTriggerOnDemand}\n)\n\nfunc isEventTriggerValid(trigger int) bool {\n\treturn slices.Contains(supportedEventTriggers, trigger)\n}\n\nfunc getTriggerTypeAsString(trigger int) string {\n\tswitch trigger {\n\tcase EventTriggerFsEvent:\n\t\treturn util.I18nTriggerFsEvent\n\tcase EventTriggerProviderEvent:\n\t\treturn util.I18nTriggerProviderEvent\n\tcase EventTriggerIPBlocked:\n\t\treturn util.I18nTriggerIPBlockedEvent\n\tcase EventTriggerCertificate:\n\t\treturn util.I18nTriggerCertificateRenewEvent\n\tcase EventTriggerOnDemand:\n\t\treturn util.I18nTriggerOnDemandEvent\n\tcase EventTriggerIDPLogin:\n\t\treturn util.I18nTriggerIDPLoginEvent\n\tdefault:\n\t\treturn util.I18nTriggerScheduleEvent\n\t}\n}\n\n// Supported IDP login events\nconst (\n\tIDPLoginAny = iota\n\tIDPLoginUser\n\tIDPLoginAdmin\n)\n\nvar (\n\tsupportedIDPLoginEvents = []int{IDPLoginAny, IDPLoginUser, IDPLoginAdmin}\n)\n\n// Supported filesystem actions\nconst (\n\tFilesystemActionRename = iota + 1\n\tFilesystemActionDelete\n\tFilesystemActionMkdirs\n\tFilesystemActionExist\n\tFilesystemActionCompress\n\tFilesystemActionCopy\n)\n\nconst (\n\t// RetentionReportPlaceHolder defines the placeholder for data retention reports\n\tRetentionReportPlaceHolder = \"{{RetentionReports}}\"\n)\n\nvar (\n\tsupportedFsActions = []int{FilesystemActionRename, FilesystemActionDelete, FilesystemActionMkdirs,\n\t\tFilesystemActionCopy, FilesystemActionCompress, FilesystemActionExist}\n)\n\nfunc isFilesystemActionValid(value int) bool {\n\treturn slices.Contains(supportedFsActions, value)\n}\n\nfunc getFsActionTypeAsString(value int) string {\n\tswitch value {\n\tcase FilesystemActionRename:\n\t\treturn util.I18nActionFsTypeRename\n\tcase FilesystemActionDelete:\n\t\treturn util.I18nActionFsTypeDelete\n\tcase FilesystemActionExist:\n\t\treturn util.I18nActionFsTypePathExists\n\tcase FilesystemActionCompress:\n\t\treturn util.I18nActionFsTypeCompress\n\tcase FilesystemActionCopy:\n\t\treturn util.I18nActionFsTypeCopy\n\tdefault:\n\t\treturn util.I18nActionFsTypeCreateDirs\n\t}\n}\n\n// TODO: replace the copied strings with shared constants\nvar (\n\t// SupportedFsEvents defines the supported filesystem events\n\tSupportedFsEvents = []string{\"upload\", \"pre-upload\", \"first-upload\", \"download\", \"pre-download\",\n\t\t\"first-download\", \"delete\", \"pre-delete\", \"rename\", \"mkdir\", \"rmdir\", \"copy\", \"ssh_cmd\"}\n\t// SupportedProviderEvents defines the supported provider events\n\tSupportedProviderEvents = []string{operationAdd, operationUpdate, operationDelete}\n\t// SupportedRuleConditionProtocols defines the supported protcols for rule conditions\n\tSupportedRuleConditionProtocols = []string{\"SFTP\", \"SCP\", \"SSH\", \"FTP\", \"DAV\", \"HTTP\", \"HTTPShare\",\n\t\t\"OIDC\"}\n\t// SupporteRuleConditionProviderObjects defines the supported provider objects for rule conditions\n\tSupporteRuleConditionProviderObjects = []string{actionObjectUser, actionObjectFolder, actionObjectGroup,\n\t\tactionObjectAdmin, actionObjectAPIKey, actionObjectShare, actionObjectEventRule, actionObjectEventAction}\n\t// SupportedHTTPActionMethods defines the supported methods for HTTP actions\n\tSupportedHTTPActionMethods = []string{http.MethodPost, http.MethodGet, http.MethodPut, http.MethodDelete}\n\tallowedSyncFsEvents        = []string{\"upload\", \"pre-upload\", \"pre-download\", \"pre-delete\"}\n\tmandatorySyncFsEvents      = []string{\"pre-upload\", \"pre-download\", \"pre-delete\"}\n)\n\n// enum mappings\nvar (\n\tEventActionTypes  []EnumMapping\n\tEventTriggerTypes []EnumMapping\n\tFsActionTypes     []EnumMapping\n)\n\nfunc init() {\n\tfor _, t := range supportedEventActions {\n\t\tEventActionTypes = append(EventActionTypes, EnumMapping{\n\t\t\tValue: t,\n\t\t\tName:  getActionTypeAsString(t),\n\t\t})\n\t}\n\tfor _, t := range supportedEventTriggers {\n\t\tEventTriggerTypes = append(EventTriggerTypes, EnumMapping{\n\t\t\tValue: t,\n\t\t\tName:  getTriggerTypeAsString(t),\n\t\t})\n\t}\n\tfor _, t := range supportedFsActions {\n\t\tFsActionTypes = append(FsActionTypes, EnumMapping{\n\t\t\tValue: t,\n\t\t\tName:  getFsActionTypeAsString(t),\n\t\t})\n\t}\n}\n\n// EnumMapping defines a mapping between enum values and names\ntype EnumMapping struct {\n\tName  string\n\tValue int\n}\n\n// KeyValue defines a key/value pair\ntype KeyValue struct {\n\tKey   string `json:\"key\"`\n\tValue string `json:\"value\"`\n}\n\nfunc (k *KeyValue) isNotValid() bool {\n\treturn k.Key == \"\" || k.Value == \"\"\n}\n\n// HTTPPart defines a part for HTTP multipart requests\ntype HTTPPart struct {\n\tName     string     `json:\"name,omitempty\"`\n\tFilepath string     `json:\"filepath,omitempty\"`\n\tHeaders  []KeyValue `json:\"headers,omitempty\"`\n\tBody     string     `json:\"body,omitempty\"`\n\tOrder    int        `json:\"-\"`\n}\n\nfunc (p *HTTPPart) validate() error {\n\tif p.Name == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"HTTP part name is required\"), util.I18nErrorHTTPPartNameRequired)\n\t}\n\tfor _, kv := range p.Headers {\n\t\tif kv.isNotValid() {\n\t\t\treturn util.NewValidationError(\"invalid HTTP part headers\")\n\t\t}\n\t}\n\tif p.Filepath == \"\" {\n\t\tif p.Body == \"\" {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"HTTP part body is required if no file path is provided\"),\n\t\t\t\tutil.I18nErrorHTTPPartBodyRequired,\n\t\t\t)\n\t\t}\n\t} else {\n\t\tp.Body = \"\"\n\t\tif p.Filepath != RetentionReportPlaceHolder {\n\t\t\tp.Filepath = util.CleanPath(p.Filepath)\n\t\t}\n\t}\n\treturn nil\n}\n\n// EventActionHTTPConfig defines the configuration for an HTTP event target\ntype EventActionHTTPConfig struct {\n\tEndpoint        string      `json:\"endpoint,omitempty\"`\n\tUsername        string      `json:\"username,omitempty\"`\n\tPassword        *kms.Secret `json:\"password,omitempty\"`\n\tHeaders         []KeyValue  `json:\"headers,omitempty\"`\n\tTimeout         int         `json:\"timeout,omitempty\"`\n\tSkipTLSVerify   bool        `json:\"skip_tls_verify,omitempty\"`\n\tMethod          string      `json:\"method,omitempty\"`\n\tQueryParameters []KeyValue  `json:\"query_parameters,omitempty\"`\n\tBody            string      `json:\"body,omitempty\"`\n\tParts           []HTTPPart  `json:\"parts,omitempty\"`\n}\n\n// HasJSONBody returns true if the content type header indicates a JSON body\nfunc (c *EventActionHTTPConfig) HasJSONBody() bool {\n\tfor _, h := range c.Headers {\n\t\tif http.CanonicalHeaderKey(h.Key) == \"Content-Type\" {\n\t\t\treturn strings.Contains(strings.ToLower(h.Value), \"application/json\")\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (c *EventActionHTTPConfig) isTimeoutNotValid() bool {\n\tif c.HasMultipartFiles() {\n\t\treturn false\n\t}\n\treturn c.Timeout < 1 || c.Timeout > 180\n}\n\nfunc (c *EventActionHTTPConfig) validateMultiparts() error {\n\tfilePaths := make(map[string]bool)\n\tfor idx := range c.Parts {\n\t\tif err := c.Parts[idx].validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif filePath := c.Parts[idx].Filepath; filePath != \"\" {\n\t\t\tif filePaths[filePath] {\n\t\t\t\treturn util.NewI18nError(fmt.Errorf(\"filepath %q is duplicated\", filePath), util.I18nErrorPathDuplicated)\n\t\t\t}\n\t\t\tfilePaths[filePath] = true\n\t\t}\n\t}\n\tif len(c.Parts) > 0 {\n\t\tif c.Body != \"\" {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"multipart requests require no body. The request body is build from the specified parts\"),\n\t\t\t\tutil.I18nErrorMultipartBody,\n\t\t\t)\n\t\t}\n\t\tfor _, k := range c.Headers {\n\t\t\tif strings.EqualFold(k.Key, \"content-type\") {\n\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\tutil.NewValidationError(\"content type is automatically set for multipart requests\"),\n\t\t\t\t\tutil.I18nErrorMultipartCType,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *EventActionHTTPConfig) validate(additionalData string) error {\n\tif c.Endpoint == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"HTTP endpoint is required\"), util.I18nErrorURLRequired)\n\t}\n\tif !util.IsStringPrefixInSlice(c.Endpoint, []string{\"http://\", \"https://\"}) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"invalid HTTP endpoint schema: http and https are supported\"),\n\t\t\tutil.I18nErrorURLInvalid,\n\t\t)\n\t}\n\tif c.isTimeoutNotValid() {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid HTTP timeout %d\", c.Timeout))\n\t}\n\tfor _, kv := range c.Headers {\n\t\tif kv.isNotValid() {\n\t\t\treturn util.NewValidationError(\"invalid HTTP headers\")\n\t\t}\n\t}\n\tif err := c.validateMultiparts(); err != nil {\n\t\treturn err\n\t}\n\tif c.Password.IsRedacted() {\n\t\treturn util.NewValidationError(\"cannot save HTTP configuration with a redacted secret\")\n\t}\n\tif c.Password.IsPlain() {\n\t\tc.Password.SetAdditionalData(additionalData)\n\t\terr := c.Password.Encrypt()\n\t\tif err != nil {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"could not encrypt HTTP password: %v\", err))\n\t\t}\n\t}\n\tif !slices.Contains(SupportedHTTPActionMethods, c.Method) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported HTTP method: %s\", c.Method))\n\t}\n\tfor _, kv := range c.QueryParameters {\n\t\tif kv.isNotValid() {\n\t\t\treturn util.NewValidationError(\"invalid HTTP query parameters\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetContext returns the context and the cancel func to use for the HTTP request\nfunc (c *EventActionHTTPConfig) GetContext() (context.Context, context.CancelFunc) {\n\tif c.HasMultipartFiles() {\n\t\treturn context.WithCancel(context.Background())\n\t}\n\treturn context.WithTimeout(context.Background(), time.Duration(c.Timeout)*time.Second)\n}\n\n// HasObjectData returns true if the {{ObjectData}} placeholder is defined\nfunc (c *EventActionHTTPConfig) HasObjectData() bool {\n\tif strings.Contains(c.Body, \"{{ObjectData}}\") || strings.Contains(c.Body, \"{{ObjectDataString}}\") {\n\t\treturn true\n\t}\n\tfor _, part := range c.Parts {\n\t\tif strings.Contains(part.Body, \"{{ObjectData}}\") || strings.Contains(part.Body, \"{{ObjectDataString}}\") {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// HasMultipartFiles returns true if at least a file must be uploaded via a multipart request\nfunc (c *EventActionHTTPConfig) HasMultipartFiles() bool {\n\tfor _, part := range c.Parts {\n\t\tif part.Filepath != \"\" && part.Filepath != RetentionReportPlaceHolder {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// TryDecryptPassword decrypts the password if encryptet\nfunc (c *EventActionHTTPConfig) TryDecryptPassword() error {\n\tif c.Password != nil && !c.Password.IsEmpty() {\n\t\tif err := c.Password.TryDecrypt(); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to decrypt HTTP password: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetHTTPClient returns an HTTP client based on the config\nfunc (c *EventActionHTTPConfig) GetHTTPClient() *http.Client {\n\tclient := &http.Client{}\n\tif c.SkipTLSVerify {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\tif transport.TLSClientConfig != nil {\n\t\t\ttransport.TLSClientConfig.InsecureSkipVerify = true\n\t\t} else {\n\t\t\ttransport.TLSClientConfig = &tls.Config{\n\t\t\t\tInsecureSkipVerify: true,\n\t\t\t}\n\t\t}\n\t\tclient.Transport = transport\n\t}\n\treturn client\n}\n\n// IsActionCommandAllowed returns true if the specified command is allowed\nfunc IsActionCommandAllowed(cmd string) bool {\n\treturn slices.Contains(EnabledActionCommands, cmd)\n}\n\n// EventActionCommandConfig defines the configuration for a command event target\ntype EventActionCommandConfig struct {\n\tCmd     string     `json:\"cmd,omitempty\"`\n\tArgs    []string   `json:\"args,omitempty\"`\n\tTimeout int        `json:\"timeout,omitempty\"`\n\tEnvVars []KeyValue `json:\"env_vars,omitempty\"`\n}\n\nfunc (c *EventActionCommandConfig) validate() error {\n\tif c.Cmd == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"command is required\"), util.I18nErrorCommandRequired)\n\t}\n\tif !IsActionCommandAllowed(c.Cmd) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"command %q is not allowed\", c.Cmd))\n\t}\n\tif !filepath.IsAbs(c.Cmd) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"invalid command, it must be an absolute path\"),\n\t\t\tutil.I18nErrorCommandInvalid,\n\t\t)\n\t}\n\tif c.Timeout < 1 || c.Timeout > 120 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid command action timeout %d\", c.Timeout))\n\t}\n\tfor _, kv := range c.EnvVars {\n\t\tif kv.isNotValid() {\n\t\t\treturn util.NewValidationError(\"invalid command env vars\")\n\t\t}\n\t}\n\tc.Args = util.RemoveDuplicates(c.Args, true)\n\tfor _, arg := range c.Args {\n\t\tif arg == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid command args\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetArgumentsAsString returns the list of command arguments as comma separated string\nfunc (c EventActionCommandConfig) GetArgumentsAsString() string {\n\treturn strings.Join(c.Args, \",\")\n}\n\n// EventActionEmailConfig defines the configuration options for SMTP event actions\ntype EventActionEmailConfig struct {\n\tRecipients  []string `json:\"recipients,omitempty\"`\n\tBcc         []string `json:\"bcc,omitempty\"`\n\tSubject     string   `json:\"subject,omitempty\"`\n\tBody        string   `json:\"body,omitempty\"`\n\tAttachments []string `json:\"attachments,omitempty\"`\n\tContentType int      `json:\"content_type,omitempty\"`\n}\n\n// GetRecipientsAsString returns the list of recipients as comma separated string\nfunc (c EventActionEmailConfig) GetRecipientsAsString() string {\n\treturn strings.Join(c.Recipients, \",\")\n}\n\n// GetBccAsString returns the list of bcc as comma separated string\nfunc (c EventActionEmailConfig) GetBccAsString() string {\n\treturn strings.Join(c.Bcc, \",\")\n}\n\n// GetAttachmentsAsString returns the list of attachments as comma separated string\nfunc (c EventActionEmailConfig) GetAttachmentsAsString() string {\n\treturn strings.Join(c.Attachments, \",\")\n}\n\nfunc (c *EventActionEmailConfig) hasFilesAttachments() bool {\n\tfor _, a := range c.Attachments {\n\t\tif a != RetentionReportPlaceHolder {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (c *EventActionEmailConfig) validate() error {\n\tif len(c.Recipients) == 0 {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"at least one email recipient is required\"),\n\t\t\tutil.I18nErrorEmailRecipientRequired,\n\t\t)\n\t}\n\tc.Recipients = util.RemoveDuplicates(c.Recipients, false)\n\tfor _, r := range c.Recipients {\n\t\tif r == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid email recipients\")\n\t\t}\n\t}\n\tc.Bcc = util.RemoveDuplicates(c.Bcc, false)\n\tfor _, r := range c.Bcc {\n\t\tif r == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid email bcc\")\n\t\t}\n\t}\n\tif c.Subject == \"\" {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"email subject is required\"),\n\t\t\tutil.I18nErrorEmailSubjectRequired,\n\t\t)\n\t}\n\tif c.Body == \"\" {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"email body is required\"),\n\t\t\tutil.I18nErrorEmailBodyRequired,\n\t\t)\n\t}\n\tif c.ContentType < 0 || c.ContentType > 1 {\n\t\treturn util.NewValidationError(\"invalid email content type\")\n\t}\n\tfor idx, val := range c.Attachments {\n\t\tval = strings.TrimSpace(val)\n\t\tif val == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid path to attach\")\n\t\t}\n\t\tif val == RetentionReportPlaceHolder {\n\t\t\tc.Attachments[idx] = val\n\t\t} else {\n\t\t\tc.Attachments[idx] = util.CleanPath(val)\n\t\t}\n\t}\n\tc.Attachments = util.RemoveDuplicates(c.Attachments, false)\n\treturn nil\n}\n\n// FolderRetention defines a folder retention configuration\ntype FolderRetention struct {\n\t// Path is the virtual directory path, if no other specific retention is defined,\n\t// the retention applies for sub directories too. For example if retention is defined\n\t// for the paths \"/\" and \"/sub\" then the retention for \"/\" is applied for any file outside\n\t// the \"/sub\" directory\n\tPath string `json:\"path\"`\n\t// Retention time in hours. 0 means exclude this path\n\tRetention int `json:\"retention\"`\n\t// DeleteEmptyDirs defines if empty directories will be deleted.\n\t// The user need the delete permission\n\tDeleteEmptyDirs bool `json:\"delete_empty_dirs,omitempty\"`\n}\n\n// Validate returns an error if the configuration is not valid\nfunc (f *FolderRetention) Validate() error {\n\tf.Path = util.CleanPath(f.Path)\n\tif f.Retention < 0 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid folder retention %v, it must be greater or equal to zero\",\n\t\t\tf.Retention))\n\t}\n\treturn nil\n}\n\n// EventActionDataRetentionConfig defines the configuration for a data retention check\ntype EventActionDataRetentionConfig struct {\n\tFolders []FolderRetention `json:\"folders,omitempty\"`\n}\n\nfunc (c *EventActionDataRetentionConfig) validate() error {\n\tfolderPaths := make(map[string]bool)\n\tnothingToDo := true\n\tfor idx := range c.Folders {\n\t\tf := &c.Folders[idx]\n\t\tif err := f.Validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif f.Retention > 0 {\n\t\t\tnothingToDo = false\n\t\t}\n\t\tif _, ok := folderPaths[f.Path]; ok {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"duplicated folder path %q\", f.Path)),\n\t\t\t\tutil.I18nErrorPathDuplicated,\n\t\t\t)\n\t\t}\n\t\tfolderPaths[f.Path] = true\n\t}\n\tif nothingToDo {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"nothing to delete!\"),\n\t\t\tutil.I18nErrorRetentionDirRequired,\n\t\t)\n\t}\n\treturn nil\n}\n\n// EventActionFsCompress defines the configuration for the compress filesystem action\ntype EventActionFsCompress struct {\n\t// Archive path\n\tName string `json:\"name,omitempty\"`\n\t// Paths to compress\n\tPaths []string `json:\"paths,omitempty\"`\n}\n\nfunc (c *EventActionFsCompress) validate() error {\n\tif c.Name == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"archive name is mandatory\"), util.I18nErrorArchiveNameRequired)\n\t}\n\tc.Name = util.CleanPath(strings.TrimSpace(c.Name))\n\tif c.Name == \"/\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"invalid archive name\"), util.I18nErrorRootNotAllowed)\n\t}\n\tif len(c.Paths) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"no path to compress specified\"), util.I18nErrorPathRequired)\n\t}\n\tfor idx, val := range c.Paths {\n\t\tval = strings.TrimSpace(val)\n\t\tif val == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid path to compress\")\n\t\t}\n\t\tc.Paths[idx] = util.CleanPath(val)\n\t}\n\tc.Paths = util.RemoveDuplicates(c.Paths, false)\n\treturn nil\n}\n\n// RenameConfig defines the configuration for a filesystem rename\ntype RenameConfig struct {\n\t// key is the source and target the value\n\tKeyValue\n\t// This setting only applies to storage providers that support\n\t// changing modification times.\n\tUpdateModTime bool `json:\"update_modtime,omitempty\"`\n}\n\n// EventActionFilesystemConfig defines the configuration for filesystem actions\ntype EventActionFilesystemConfig struct {\n\t// Filesystem actions, see the above enum\n\tType int `json:\"type,omitempty\"`\n\t// files/dirs to rename\n\tRenames []RenameConfig `json:\"renames,omitempty\"`\n\t// directories to create\n\tMkDirs []string `json:\"mkdirs,omitempty\"`\n\t// files/dirs to delete\n\tDeletes []string `json:\"deletes,omitempty\"`\n\t// file/dirs to check for existence\n\tExist []string `json:\"exist,omitempty\"`\n\t// files/dirs to copy, key is the source and target the value\n\tCopy []KeyValue `json:\"copy,omitempty\"`\n\t// paths to compress and archive name\n\tCompress EventActionFsCompress `json:\"compress\"`\n}\n\n// GetDeletesAsString returns the list of items to delete as comma separated string.\n// Using a pointer receiver will not work in web templates\nfunc (c EventActionFilesystemConfig) GetDeletesAsString() string {\n\treturn strings.Join(c.Deletes, \",\")\n}\n\n// GetMkDirsAsString returns the list of directories to create as comma separated string.\n// Using a pointer receiver will not work in web templates\nfunc (c EventActionFilesystemConfig) GetMkDirsAsString() string {\n\treturn strings.Join(c.MkDirs, \",\")\n}\n\n// GetExistAsString returns the list of items to check for existence as comma separated string.\n// Using a pointer receiver will not work in web templates\nfunc (c EventActionFilesystemConfig) GetExistAsString() string {\n\treturn strings.Join(c.Exist, \",\")\n}\n\n// GetCompressPathsAsString returns the list of items to compress as comma separated string.\n// Using a pointer receiver will not work in web templates\nfunc (c EventActionFilesystemConfig) GetCompressPathsAsString() string {\n\treturn strings.Join(c.Compress.Paths, \",\")\n}\n\nfunc (c *EventActionFilesystemConfig) validateRenames() error {\n\tif len(c.Renames) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"no path to rename specified\"), util.I18nErrorPathRequired)\n\t}\n\tfor idx, cfg := range c.Renames {\n\t\tkey := strings.TrimSpace(cfg.Key)\n\t\tvalue := strings.TrimSpace(cfg.Value)\n\t\tif key == \"\" || value == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid paths to rename\")\n\t\t}\n\t\tkey = util.CleanPath(key)\n\t\tvalue = util.CleanPath(value)\n\t\tif key == value {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"rename source and target cannot be equal\"),\n\t\t\t\tutil.I18nErrorSourceDestMatch,\n\t\t\t)\n\t\t}\n\t\tif key == \"/\" || value == \"/\" {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"renaming the root directory is not allowed\"),\n\t\t\t\tutil.I18nErrorRootNotAllowed,\n\t\t\t)\n\t\t}\n\t\tc.Renames[idx] = RenameConfig{\n\t\t\tKeyValue: KeyValue{\n\t\t\t\tKey:   key,\n\t\t\t\tValue: value,\n\t\t\t},\n\t\t\tUpdateModTime: cfg.UpdateModTime,\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *EventActionFilesystemConfig) validateCopy() error {\n\tif len(c.Copy) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"no path to copy specified\"), util.I18nErrorPathRequired)\n\t}\n\tfor idx, kv := range c.Copy {\n\t\tkey := strings.TrimSpace(kv.Key)\n\t\tvalue := strings.TrimSpace(kv.Value)\n\t\tif key == \"\" || value == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid paths to copy\")\n\t\t}\n\t\tkey = util.CleanPath(key)\n\t\tvalue = util.CleanPath(value)\n\t\tif key == value {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"copy source and target cannot be equal\"),\n\t\t\t\tutil.I18nErrorSourceDestMatch,\n\t\t\t)\n\t\t}\n\t\tif key == \"/\" || value == \"/\" {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"copying the root directory is not allowed\"),\n\t\t\t\tutil.I18nErrorRootNotAllowed,\n\t\t\t)\n\t\t}\n\t\tif strings.HasSuffix(c.Copy[idx].Key, \"/\") {\n\t\t\tkey += \"/\"\n\t\t}\n\t\tif strings.HasSuffix(c.Copy[idx].Value, \"/\") {\n\t\t\tvalue += \"/\"\n\t\t}\n\t\tc.Copy[idx] = KeyValue{\n\t\t\tKey:   key,\n\t\t\tValue: value,\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *EventActionFilesystemConfig) validateDeletes() error {\n\tif len(c.Deletes) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"no path to delete specified\"), util.I18nErrorPathRequired)\n\t}\n\tfor idx, val := range c.Deletes {\n\t\tval = strings.TrimSpace(val)\n\t\tif val == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid path to delete\")\n\t\t}\n\t\tc.Deletes[idx] = util.CleanPath(val)\n\t}\n\tc.Deletes = util.RemoveDuplicates(c.Deletes, false)\n\treturn nil\n}\n\nfunc (c *EventActionFilesystemConfig) validateMkdirs() error {\n\tif len(c.MkDirs) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"no directory to create specified\"), util.I18nErrorPathRequired)\n\t}\n\tfor idx, val := range c.MkDirs {\n\t\tval = strings.TrimSpace(val)\n\t\tif val == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid directory to create\")\n\t\t}\n\t\tc.MkDirs[idx] = util.CleanPath(val)\n\t}\n\tc.MkDirs = util.RemoveDuplicates(c.MkDirs, false)\n\treturn nil\n}\n\nfunc (c *EventActionFilesystemConfig) validateExist() error {\n\tif len(c.Exist) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"no path to check for existence specified\"), util.I18nErrorPathRequired)\n\t}\n\tfor idx, val := range c.Exist {\n\t\tval = strings.TrimSpace(val)\n\t\tif val == \"\" {\n\t\t\treturn util.NewValidationError(\"invalid path to check for existence\")\n\t\t}\n\t\tc.Exist[idx] = util.CleanPath(val)\n\t}\n\tc.Exist = util.RemoveDuplicates(c.Exist, false)\n\treturn nil\n}\n\nfunc (c *EventActionFilesystemConfig) validate() error {\n\tif !isFilesystemActionValid(c.Type) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid filesystem action type: %d\", c.Type))\n\t}\n\tswitch c.Type {\n\tcase FilesystemActionRename:\n\t\tc.MkDirs = nil\n\t\tc.Deletes = nil\n\t\tc.Exist = nil\n\t\tc.Copy = nil\n\t\tc.Compress = EventActionFsCompress{}\n\t\tif err := c.validateRenames(); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase FilesystemActionDelete:\n\t\tc.Renames = nil\n\t\tc.MkDirs = nil\n\t\tc.Exist = nil\n\t\tc.Copy = nil\n\t\tc.Compress = EventActionFsCompress{}\n\t\tif err := c.validateDeletes(); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase FilesystemActionMkdirs:\n\t\tc.Renames = nil\n\t\tc.Deletes = nil\n\t\tc.Exist = nil\n\t\tc.Copy = nil\n\t\tc.Compress = EventActionFsCompress{}\n\t\tif err := c.validateMkdirs(); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase FilesystemActionExist:\n\t\tc.Renames = nil\n\t\tc.Deletes = nil\n\t\tc.MkDirs = nil\n\t\tc.Copy = nil\n\t\tc.Compress = EventActionFsCompress{}\n\t\tif err := c.validateExist(); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase FilesystemActionCompress:\n\t\tc.Renames = nil\n\t\tc.MkDirs = nil\n\t\tc.Deletes = nil\n\t\tc.Exist = nil\n\t\tc.Copy = nil\n\t\tif err := c.Compress.validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase FilesystemActionCopy:\n\t\tc.Renames = nil\n\t\tc.Deletes = nil\n\t\tc.MkDirs = nil\n\t\tc.Exist = nil\n\t\tc.Compress = EventActionFsCompress{}\n\t\tif err := c.validateCopy(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {\n\tmkdirs := make([]string, len(c.MkDirs))\n\tcopy(mkdirs, c.MkDirs)\n\tdeletes := make([]string, len(c.Deletes))\n\tcopy(deletes, c.Deletes)\n\texist := make([]string, len(c.Exist))\n\tcopy(exist, c.Exist)\n\tcompressPaths := make([]string, len(c.Compress.Paths))\n\tcopy(compressPaths, c.Compress.Paths)\n\n\treturn EventActionFilesystemConfig{\n\t\tType:    c.Type,\n\t\tRenames: cloneRenameConfigs(c.Renames),\n\t\tMkDirs:  mkdirs,\n\t\tDeletes: deletes,\n\t\tExist:   exist,\n\t\tCopy:    cloneKeyValues(c.Copy),\n\t\tCompress: EventActionFsCompress{\n\t\t\tPaths: compressPaths,\n\t\t\tName:  c.Compress.Name,\n\t\t},\n\t}\n}\n\n// EventActionPasswordExpiration defines the configuration for password expiration actions\ntype EventActionPasswordExpiration struct {\n\t// An email notification will be generated for users whose password expires in a number\n\t// of days less than or equal to this threshold\n\tThreshold int `json:\"threshold,omitempty\"`\n}\n\nfunc (c *EventActionPasswordExpiration) validate() error {\n\tif c.Threshold <= 0 {\n\t\treturn util.NewValidationError(\"threshold must be greater than 0\")\n\t}\n\treturn nil\n}\n\n// EventActionUserInactivity defines the configuration for user inactivity checks.\ntype EventActionUserInactivity struct {\n\t// DisableThreshold defines inactivity in days, since the last login before disabling the account\n\tDisableThreshold int `json:\"disable_threshold,omitempty\"`\n\t// DeleteThreshold defines inactivity in days, since the last login before deleting the account\n\tDeleteThreshold int `json:\"delete_threshold,omitempty\"`\n}\n\nfunc (c *EventActionUserInactivity) validate() error {\n\tif c.DeleteThreshold < 0 {\n\t\tc.DeleteThreshold = 0\n\t}\n\tif c.DisableThreshold < 0 {\n\t\tc.DisableThreshold = 0\n\t}\n\tif c.DisableThreshold == 0 && c.DeleteThreshold == 0 {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"at least a threshold must be defined\"),\n\t\t\tutil.I18nActionThresholdRequired,\n\t\t)\n\t}\n\tif c.DeleteThreshold > 0 && c.DisableThreshold > 0 {\n\t\tif c.DeleteThreshold <= c.DisableThreshold {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"deletion threshold %d must be greater than deactivation threshold: %d\", c.DeleteThreshold, c.DisableThreshold)),\n\t\t\t\tutil.I18nActionThresholdsInvalid,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\n// EventActionIDPAccountCheck defines the check to execute after a successful IDP login\ntype EventActionIDPAccountCheck struct {\n\t// 0 create/update, 1 create the account if it doesn't exist\n\tMode          int    `json:\"mode,omitempty\"`\n\tTemplateUser  string `json:\"template_user,omitempty\"`\n\tTemplateAdmin string `json:\"template_admin,omitempty\"`\n}\n\nfunc (c *EventActionIDPAccountCheck) validate() error {\n\tif c.TemplateAdmin == \"\" && c.TemplateUser == \"\" {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"at least a template must be set\"),\n\t\t\tutil.I18nErrorIDPTemplateRequired,\n\t\t)\n\t}\n\tif c.Mode < 0 || c.Mode > 1 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid account check mode: %d\", c.Mode))\n\t}\n\treturn nil\n}\n\n// BaseEventActionOptions defines the supported configuration options for a base event actions\ntype BaseEventActionOptions struct {\n\tHTTPConfig           EventActionHTTPConfig          `json:\"http_config\"`\n\tCmdConfig            EventActionCommandConfig       `json:\"cmd_config\"`\n\tEmailConfig          EventActionEmailConfig         `json:\"email_config\"`\n\tRetentionConfig      EventActionDataRetentionConfig `json:\"retention_config\"`\n\tFsConfig             EventActionFilesystemConfig    `json:\"fs_config\"`\n\tPwdExpirationConfig  EventActionPasswordExpiration  `json:\"pwd_expiration_config\"`\n\tUserInactivityConfig EventActionUserInactivity      `json:\"user_inactivity_config\"`\n\tIDPConfig            EventActionIDPAccountCheck     `json:\"idp_config\"`\n}\n\nfunc (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {\n\to.SetEmptySecretsIfNil()\n\temailRecipients := make([]string, len(o.EmailConfig.Recipients))\n\tcopy(emailRecipients, o.EmailConfig.Recipients)\n\temailBcc := make([]string, len(o.EmailConfig.Bcc))\n\tcopy(emailBcc, o.EmailConfig.Bcc)\n\temailAttachments := make([]string, len(o.EmailConfig.Attachments))\n\tcopy(emailAttachments, o.EmailConfig.Attachments)\n\tcmdArgs := make([]string, len(o.CmdConfig.Args))\n\tcopy(cmdArgs, o.CmdConfig.Args)\n\tfolders := make([]FolderRetention, 0, len(o.RetentionConfig.Folders))\n\tfor _, folder := range o.RetentionConfig.Folders {\n\t\tfolders = append(folders, FolderRetention{\n\t\t\tPath:            folder.Path,\n\t\t\tRetention:       folder.Retention,\n\t\t\tDeleteEmptyDirs: folder.DeleteEmptyDirs,\n\t\t})\n\t}\n\thttpParts := make([]HTTPPart, 0, len(o.HTTPConfig.Parts))\n\tfor _, part := range o.HTTPConfig.Parts {\n\t\thttpParts = append(httpParts, HTTPPart{\n\t\t\tName:     part.Name,\n\t\t\tFilepath: part.Filepath,\n\t\t\tHeaders:  cloneKeyValues(part.Headers),\n\t\t\tBody:     part.Body,\n\t\t})\n\t}\n\n\treturn BaseEventActionOptions{\n\t\tHTTPConfig: EventActionHTTPConfig{\n\t\t\tEndpoint:        o.HTTPConfig.Endpoint,\n\t\t\tUsername:        o.HTTPConfig.Username,\n\t\t\tPassword:        o.HTTPConfig.Password.Clone(),\n\t\t\tHeaders:         cloneKeyValues(o.HTTPConfig.Headers),\n\t\t\tTimeout:         o.HTTPConfig.Timeout,\n\t\t\tSkipTLSVerify:   o.HTTPConfig.SkipTLSVerify,\n\t\t\tMethod:          o.HTTPConfig.Method,\n\t\t\tQueryParameters: cloneKeyValues(o.HTTPConfig.QueryParameters),\n\t\t\tBody:            o.HTTPConfig.Body,\n\t\t\tParts:           httpParts,\n\t\t},\n\t\tCmdConfig: EventActionCommandConfig{\n\t\t\tCmd:     o.CmdConfig.Cmd,\n\t\t\tArgs:    cmdArgs,\n\t\t\tTimeout: o.CmdConfig.Timeout,\n\t\t\tEnvVars: cloneKeyValues(o.CmdConfig.EnvVars),\n\t\t},\n\t\tEmailConfig: EventActionEmailConfig{\n\t\t\tRecipients:  emailRecipients,\n\t\t\tBcc:         emailBcc,\n\t\t\tSubject:     o.EmailConfig.Subject,\n\t\t\tContentType: o.EmailConfig.ContentType,\n\t\t\tBody:        o.EmailConfig.Body,\n\t\t\tAttachments: emailAttachments,\n\t\t},\n\t\tRetentionConfig: EventActionDataRetentionConfig{\n\t\t\tFolders: folders,\n\t\t},\n\t\tPwdExpirationConfig: EventActionPasswordExpiration{\n\t\t\tThreshold: o.PwdExpirationConfig.Threshold,\n\t\t},\n\t\tUserInactivityConfig: EventActionUserInactivity{\n\t\t\tDisableThreshold: o.UserInactivityConfig.DisableThreshold,\n\t\t\tDeleteThreshold:  o.UserInactivityConfig.DeleteThreshold,\n\t\t},\n\t\tIDPConfig: EventActionIDPAccountCheck{\n\t\t\tMode:          o.IDPConfig.Mode,\n\t\t\tTemplateUser:  o.IDPConfig.TemplateUser,\n\t\t\tTemplateAdmin: o.IDPConfig.TemplateAdmin,\n\t\t},\n\t\tFsConfig: o.FsConfig.getACopy(),\n\t}\n}\n\n// SetEmptySecretsIfNil sets the secrets to empty if nil\nfunc (o *BaseEventActionOptions) SetEmptySecretsIfNil() {\n\tif o.HTTPConfig.Password == nil {\n\t\to.HTTPConfig.Password = kms.NewEmptySecret()\n\t}\n}\n\nfunc (o *BaseEventActionOptions) setNilSecretsIfEmpty() {\n\tif o.HTTPConfig.Password != nil && o.HTTPConfig.Password.IsEmpty() {\n\t\to.HTTPConfig.Password = nil\n\t}\n}\n\nfunc (o *BaseEventActionOptions) hideConfidentialData() {\n\tif o.HTTPConfig.Password != nil {\n\t\to.HTTPConfig.Password.Hide()\n\t}\n}\n\nfunc (o *BaseEventActionOptions) validate(action int, name string) error {\n\to.SetEmptySecretsIfNil()\n\tswitch action {\n\tcase ActionTypeHTTP:\n\t\to.CmdConfig = EventActionCommandConfig{}\n\t\to.EmailConfig = EventActionEmailConfig{}\n\t\to.RetentionConfig = EventActionDataRetentionConfig{}\n\t\to.FsConfig = EventActionFilesystemConfig{}\n\t\to.PwdExpirationConfig = EventActionPasswordExpiration{}\n\t\to.IDPConfig = EventActionIDPAccountCheck{}\n\t\to.UserInactivityConfig = EventActionUserInactivity{}\n\t\treturn o.HTTPConfig.validate(name)\n\tcase ActionTypeCommand:\n\t\to.HTTPConfig = EventActionHTTPConfig{}\n\t\to.EmailConfig = EventActionEmailConfig{}\n\t\to.RetentionConfig = EventActionDataRetentionConfig{}\n\t\to.FsConfig = EventActionFilesystemConfig{}\n\t\to.PwdExpirationConfig = EventActionPasswordExpiration{}\n\t\to.IDPConfig = EventActionIDPAccountCheck{}\n\t\to.UserInactivityConfig = EventActionUserInactivity{}\n\t\treturn o.CmdConfig.validate()\n\tcase ActionTypeEmail:\n\t\to.HTTPConfig = EventActionHTTPConfig{}\n\t\to.CmdConfig = EventActionCommandConfig{}\n\t\to.RetentionConfig = EventActionDataRetentionConfig{}\n\t\to.FsConfig = EventActionFilesystemConfig{}\n\t\to.PwdExpirationConfig = EventActionPasswordExpiration{}\n\t\to.IDPConfig = EventActionIDPAccountCheck{}\n\t\to.UserInactivityConfig = EventActionUserInactivity{}\n\t\treturn o.EmailConfig.validate()\n\tcase ActionTypeDataRetentionCheck:\n\t\to.HTTPConfig = EventActionHTTPConfig{}\n\t\to.CmdConfig = EventActionCommandConfig{}\n\t\to.EmailConfig = EventActionEmailConfig{}\n\t\to.FsConfig = EventActionFilesystemConfig{}\n\t\to.PwdExpirationConfig = EventActionPasswordExpiration{}\n\t\to.IDPConfig = EventActionIDPAccountCheck{}\n\t\to.UserInactivityConfig = EventActionUserInactivity{}\n\t\treturn o.RetentionConfig.validate()\n\tcase ActionTypeFilesystem:\n\t\to.HTTPConfig = EventActionHTTPConfig{}\n\t\to.CmdConfig = EventActionCommandConfig{}\n\t\to.EmailConfig = EventActionEmailConfig{}\n\t\to.RetentionConfig = EventActionDataRetentionConfig{}\n\t\to.PwdExpirationConfig = EventActionPasswordExpiration{}\n\t\to.IDPConfig = EventActionIDPAccountCheck{}\n\t\to.UserInactivityConfig = EventActionUserInactivity{}\n\t\treturn o.FsConfig.validate()\n\tcase ActionTypePasswordExpirationCheck:\n\t\to.HTTPConfig = EventActionHTTPConfig{}\n\t\to.CmdConfig = EventActionCommandConfig{}\n\t\to.EmailConfig = EventActionEmailConfig{}\n\t\to.RetentionConfig = EventActionDataRetentionConfig{}\n\t\to.FsConfig = EventActionFilesystemConfig{}\n\t\to.IDPConfig = EventActionIDPAccountCheck{}\n\t\to.UserInactivityConfig = EventActionUserInactivity{}\n\t\treturn o.PwdExpirationConfig.validate()\n\tcase ActionTypeUserInactivityCheck:\n\t\to.HTTPConfig = EventActionHTTPConfig{}\n\t\to.CmdConfig = EventActionCommandConfig{}\n\t\to.EmailConfig = EventActionEmailConfig{}\n\t\to.RetentionConfig = EventActionDataRetentionConfig{}\n\t\to.FsConfig = EventActionFilesystemConfig{}\n\t\to.IDPConfig = EventActionIDPAccountCheck{}\n\t\to.PwdExpirationConfig = EventActionPasswordExpiration{}\n\t\treturn o.UserInactivityConfig.validate()\n\tcase ActionTypeIDPAccountCheck:\n\t\to.HTTPConfig = EventActionHTTPConfig{}\n\t\to.CmdConfig = EventActionCommandConfig{}\n\t\to.EmailConfig = EventActionEmailConfig{}\n\t\to.RetentionConfig = EventActionDataRetentionConfig{}\n\t\to.FsConfig = EventActionFilesystemConfig{}\n\t\to.PwdExpirationConfig = EventActionPasswordExpiration{}\n\t\to.UserInactivityConfig = EventActionUserInactivity{}\n\t\treturn o.IDPConfig.validate()\n\tdefault:\n\t\to.HTTPConfig = EventActionHTTPConfig{}\n\t\to.CmdConfig = EventActionCommandConfig{}\n\t\to.EmailConfig = EventActionEmailConfig{}\n\t\to.RetentionConfig = EventActionDataRetentionConfig{}\n\t\to.FsConfig = EventActionFilesystemConfig{}\n\t\to.PwdExpirationConfig = EventActionPasswordExpiration{}\n\t\to.IDPConfig = EventActionIDPAccountCheck{}\n\t\to.UserInactivityConfig = EventActionUserInactivity{}\n\t}\n\treturn nil\n}\n\n// BaseEventAction defines the common fields for an event action\ntype BaseEventAction struct {\n\t// Data provider unique identifier\n\tID int64 `json:\"id\"`\n\t// Action name\n\tName string `json:\"name\"`\n\t// optional description\n\tDescription string `json:\"description,omitempty\"`\n\t// ActionType, see the above enum\n\tType int `json:\"type\"`\n\t// Configuration options specific for the action type\n\tOptions BaseEventActionOptions `json:\"options\"`\n\t// list of rule names associated with this event action\n\tRules []string `json:\"rules,omitempty\"`\n}\n\nfunc (a *BaseEventAction) getACopy() BaseEventAction {\n\trules := make([]string, len(a.Rules))\n\tcopy(rules, a.Rules)\n\treturn BaseEventAction{\n\t\tID:          a.ID,\n\t\tName:        a.Name,\n\t\tDescription: a.Description,\n\t\tType:        a.Type,\n\t\tOptions:     a.Options.getACopy(),\n\t\tRules:       rules,\n\t}\n}\n\n// GetTypeAsString returns the action type as string\nfunc (a *BaseEventAction) GetTypeAsString() string {\n\treturn getActionTypeAsString(a.Type)\n}\n\n// GetRulesAsString returns the list of rules as comma separated string\nfunc (a *BaseEventAction) GetRulesAsString() string {\n\treturn strings.Join(a.Rules, \",\")\n}\n\n// PrepareForRendering prepares a BaseEventAction for rendering.\n// It hides confidential data and set to nil the empty secrets\n// so they are not serialized\nfunc (a *BaseEventAction) PrepareForRendering() {\n\ta.Options.setNilSecretsIfEmpty()\n\ta.Options.hideConfidentialData()\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (a *BaseEventAction) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\taction, err := provider.eventActionExists(a.Name)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload event action before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\taction.PrepareForRendering()\n\t\treturn json.Marshal(action)\n\t}\n\ta.PrepareForRendering()\n\treturn json.Marshal(a)\n}\n\nfunc (a *BaseEventAction) validate() error {\n\tif a.Name == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"name is mandatory\"), util.I18nErrorNameRequired)\n\t}\n\tif !util.IsNameValid(a.Name) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif config.NamingRules&1 == 0 && !usernameRegex.MatchString(a.Name) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~\", a.Name)),\n\t\t\tutil.I18nErrorInvalidUser,\n\t\t)\n\t}\n\tif !isActionTypeValid(a.Type) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid action type: %d\", a.Type))\n\t}\n\treturn a.Options.validate(a.Type, a.Name)\n}\n\n// EventActionOptions defines the supported configuration options for an event action\ntype EventActionOptions struct {\n\tIsFailureAction bool `json:\"is_failure_action\"`\n\tStopOnFailure   bool `json:\"stop_on_failure\"`\n\tExecuteSync     bool `json:\"execute_sync\"`\n}\n\n// EventAction defines an event action\ntype EventAction struct {\n\tBaseEventAction\n\t// Order defines the execution order\n\tOrder   int                `json:\"order,omitempty\"`\n\tOptions EventActionOptions `json:\"relation_options\"`\n}\n\nfunc (a *EventAction) getACopy() EventAction {\n\treturn EventAction{\n\t\tBaseEventAction: a.BaseEventAction.getACopy(),\n\t\tOrder:           a.Order,\n\t\tOptions: EventActionOptions{\n\t\t\tIsFailureAction: a.Options.IsFailureAction,\n\t\t\tStopOnFailure:   a.Options.StopOnFailure,\n\t\t\tExecuteSync:     a.Options.ExecuteSync,\n\t\t},\n\t}\n}\n\nfunc (a *EventAction) validateAssociation(trigger int, fsEvents []string) error {\n\tif a.Options.IsFailureAction {\n\t\tif a.Options.ExecuteSync {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"sync execution is not supported for failure actions\"),\n\t\t\t\tutil.I18nErrorEvSyncFailureActions,\n\t\t\t)\n\t\t}\n\t}\n\tif a.Options.ExecuteSync {\n\t\tif trigger != EventTriggerFsEvent && trigger != EventTriggerIDPLogin {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"sync execution is only supported for some filesystem events and Identity Provider logins\"),\n\t\t\t\tutil.I18nErrorEvSyncUnsupported,\n\t\t\t)\n\t\t}\n\t\tif trigger == EventTriggerFsEvent {\n\t\t\tfor _, ev := range fsEvents {\n\t\t\t\tif !slices.Contains(allowedSyncFsEvents, ev) {\n\t\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\t\tutil.NewValidationError(\"sync execution is only supported for upload and pre-* events\"),\n\t\t\t\t\t\tutil.I18nErrorEvSyncUnsupportedFs,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// ConditionPattern defines a pattern for condition filters\ntype ConditionPattern struct {\n\tPattern      string `json:\"pattern,omitempty\"`\n\tInverseMatch bool   `json:\"inverse_match,omitempty\"`\n}\n\nfunc (p *ConditionPattern) validate() error {\n\tif p.Pattern == \"\" {\n\t\treturn util.NewValidationError(\"empty condition pattern not allowed\")\n\t}\n\t_, err := path.Match(p.Pattern, \"abc\")\n\tif err != nil {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid condition pattern %q\", p.Pattern))\n\t}\n\treturn nil\n}\n\n// ConditionOptions defines options for event conditions\ntype ConditionOptions struct {\n\t// Usernames or folder names\n\tNames []ConditionPattern `json:\"names,omitempty\"`\n\t// Group names\n\tGroupNames []ConditionPattern `json:\"group_names,omitempty\"`\n\t// Role names\n\tRoleNames []ConditionPattern `json:\"role_names,omitempty\"`\n\t// Virtual paths\n\tFsPaths         []ConditionPattern `json:\"fs_paths,omitempty\"`\n\tProtocols       []string           `json:\"protocols,omitempty\"`\n\tProviderObjects []string           `json:\"provider_objects,omitempty\"`\n\tMinFileSize     int64              `json:\"min_size,omitempty\"`\n\tMaxFileSize     int64              `json:\"max_size,omitempty\"`\n\tEventStatuses   []int              `json:\"event_statuses,omitempty\"`\n\t// allow to execute scheduled tasks concurrently from multiple instances\n\tConcurrentExecution bool `json:\"concurrent_execution,omitempty\"`\n}\n\nfunc (f *ConditionOptions) getACopy() ConditionOptions {\n\tprotocols := make([]string, len(f.Protocols))\n\tcopy(protocols, f.Protocols)\n\tproviderObjects := make([]string, len(f.ProviderObjects))\n\tcopy(providerObjects, f.ProviderObjects)\n\tstatuses := make([]int, len(f.EventStatuses))\n\tcopy(statuses, f.EventStatuses)\n\n\treturn ConditionOptions{\n\t\tNames:               cloneConditionPatterns(f.Names),\n\t\tGroupNames:          cloneConditionPatterns(f.GroupNames),\n\t\tRoleNames:           cloneConditionPatterns(f.RoleNames),\n\t\tFsPaths:             cloneConditionPatterns(f.FsPaths),\n\t\tProtocols:           protocols,\n\t\tProviderObjects:     providerObjects,\n\t\tMinFileSize:         f.MinFileSize,\n\t\tMaxFileSize:         f.MaxFileSize,\n\t\tEventStatuses:       statuses,\n\t\tConcurrentExecution: f.ConcurrentExecution,\n\t}\n}\n\nfunc (f *ConditionOptions) validateStatuses() error {\n\tfor _, status := range f.EventStatuses {\n\t\tif status < 0 || status > 3 {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid event_status %d\", status))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (f *ConditionOptions) validate() error {\n\tif err := validateConditionPatterns(f.Names); err != nil {\n\t\treturn err\n\t}\n\tif err := validateConditionPatterns(f.GroupNames); err != nil {\n\t\treturn err\n\t}\n\tif err := validateConditionPatterns(f.RoleNames); err != nil {\n\t\treturn err\n\t}\n\tif err := validateConditionPatterns(f.FsPaths); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, p := range f.Protocols {\n\t\tif !slices.Contains(SupportedRuleConditionProtocols, p) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported rule condition protocol: %q\", p))\n\t\t}\n\t}\n\tfor _, p := range f.ProviderObjects {\n\t\tif !slices.Contains(SupporteRuleConditionProviderObjects, p) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported provider object: %q\", p))\n\t\t}\n\t}\n\tif f.MinFileSize > 0 && f.MaxFileSize > 0 {\n\t\tif f.MaxFileSize <= f.MinFileSize {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid max file size %s, it is lesser or equal than min file size %s\",\n\t\t\t\tutil.ByteCountSI(f.MaxFileSize), util.ByteCountSI(f.MinFileSize)))\n\t\t}\n\t}\n\tif err := f.validateStatuses(); err != nil {\n\t\treturn err\n\t}\n\tif config.IsShared == 0 {\n\t\tf.ConcurrentExecution = false\n\t}\n\treturn nil\n}\n\n// Schedule defines an event schedule\ntype Schedule struct {\n\tHours      string `json:\"hour\"`\n\tDayOfWeek  string `json:\"day_of_week\"`\n\tDayOfMonth string `json:\"day_of_month\"`\n\tMonth      string `json:\"month\"`\n}\n\n// GetCronSpec returns the cron compatible schedule string\nfunc (s *Schedule) GetCronSpec() string {\n\treturn fmt.Sprintf(\"0 %s %s %s %s\", s.Hours, s.DayOfMonth, s.Month, s.DayOfWeek)\n}\n\nfunc (s *Schedule) validate() error {\n\t_, err := cron.ParseStandard(s.GetCronSpec())\n\tif err != nil {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid schedule, hour: %q, day of month: %q, month: %q, day of week: %q\",\n\t\t\ts.Hours, s.DayOfMonth, s.Month, s.DayOfWeek))\n\t}\n\treturn nil\n}\n\n// EventConditions defines the conditions for an event rule\ntype EventConditions struct {\n\t// Only one between FsEvents, ProviderEvents and Schedule is allowed\n\tFsEvents       []string   `json:\"fs_events,omitempty\"`\n\tProviderEvents []string   `json:\"provider_events,omitempty\"`\n\tSchedules      []Schedule `json:\"schedules,omitempty\"`\n\t// 0 any, 1 user, 2 admin\n\tIDPLoginEvent int              `json:\"idp_login_event,omitempty\"`\n\tOptions       ConditionOptions `json:\"options\"`\n}\n\nfunc (c *EventConditions) getACopy() EventConditions {\n\tfsEvents := make([]string, len(c.FsEvents))\n\tcopy(fsEvents, c.FsEvents)\n\tproviderEvents := make([]string, len(c.ProviderEvents))\n\tcopy(providerEvents, c.ProviderEvents)\n\tschedules := make([]Schedule, 0, len(c.Schedules))\n\tfor _, schedule := range c.Schedules {\n\t\tschedules = append(schedules, Schedule{\n\t\t\tHours:      schedule.Hours,\n\t\t\tDayOfWeek:  schedule.DayOfWeek,\n\t\t\tDayOfMonth: schedule.DayOfMonth,\n\t\t\tMonth:      schedule.Month,\n\t\t})\n\t}\n\n\treturn EventConditions{\n\t\tFsEvents:       fsEvents,\n\t\tProviderEvents: providerEvents,\n\t\tSchedules:      schedules,\n\t\tIDPLoginEvent:  c.IDPLoginEvent,\n\t\tOptions:        c.Options.getACopy(),\n\t}\n}\n\nfunc (c *EventConditions) validateSchedules() error {\n\tif len(c.Schedules) == 0 {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"at least one schedule is required\"),\n\t\t\tutil.I18nErrorRuleScheduleRequired,\n\t\t)\n\t}\n\tfor _, schedule := range c.Schedules {\n\t\tif err := schedule.validate(); err != nil {\n\t\t\treturn util.NewI18nError(err, util.I18nErrorRuleScheduleInvalid)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *EventConditions) validate(trigger int) error {\n\tswitch trigger {\n\tcase EventTriggerFsEvent:\n\t\tc.ProviderEvents = nil\n\t\tc.Schedules = nil\n\t\tc.Options.ProviderObjects = nil\n\t\tc.IDPLoginEvent = 0\n\t\tif len(c.FsEvents) == 0 {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"at least one filesystem event is required\"),\n\t\t\t\tutil.I18nErrorRuleFsEventRequired,\n\t\t\t)\n\t\t}\n\t\tfor _, ev := range c.FsEvents {\n\t\t\tif !slices.Contains(SupportedFsEvents, ev) {\n\t\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported fs event: %q\", ev))\n\t\t\t}\n\t\t}\n\tcase EventTriggerProviderEvent:\n\t\tc.FsEvents = nil\n\t\tc.Schedules = nil\n\t\tc.Options.FsPaths = nil\n\t\tc.Options.Protocols = nil\n\t\tc.Options.EventStatuses = nil\n\t\tc.Options.MinFileSize = 0\n\t\tc.Options.MaxFileSize = 0\n\t\tc.IDPLoginEvent = 0\n\t\tif len(c.ProviderEvents) == 0 {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(\"at least one provider event is required\"),\n\t\t\t\tutil.I18nErrorRuleProviderEventRequired,\n\t\t\t)\n\t\t}\n\t\tfor _, ev := range c.ProviderEvents {\n\t\t\tif !slices.Contains(SupportedProviderEvents, ev) {\n\t\t\t\treturn util.NewValidationError(fmt.Sprintf(\"unsupported provider event: %q\", ev))\n\t\t\t}\n\t\t}\n\tcase EventTriggerSchedule:\n\t\tc.FsEvents = nil\n\t\tc.ProviderEvents = nil\n\t\tc.Options.FsPaths = nil\n\t\tc.Options.Protocols = nil\n\t\tc.Options.EventStatuses = nil\n\t\tc.Options.MinFileSize = 0\n\t\tc.Options.MaxFileSize = 0\n\t\tc.Options.ProviderObjects = nil\n\t\tc.IDPLoginEvent = 0\n\t\tif err := c.validateSchedules(); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase EventTriggerIPBlocked, EventTriggerCertificate:\n\t\tc.FsEvents = nil\n\t\tc.ProviderEvents = nil\n\t\tc.Options.Names = nil\n\t\tc.Options.GroupNames = nil\n\t\tc.Options.RoleNames = nil\n\t\tc.Options.FsPaths = nil\n\t\tc.Options.Protocols = nil\n\t\tc.Options.EventStatuses = nil\n\t\tc.Options.MinFileSize = 0\n\t\tc.Options.MaxFileSize = 0\n\t\tc.Schedules = nil\n\t\tc.IDPLoginEvent = 0\n\tcase EventTriggerOnDemand:\n\t\tc.FsEvents = nil\n\t\tc.ProviderEvents = nil\n\t\tc.Options.FsPaths = nil\n\t\tc.Options.Protocols = nil\n\t\tc.Options.EventStatuses = nil\n\t\tc.Options.MinFileSize = 0\n\t\tc.Options.MaxFileSize = 0\n\t\tc.Options.ProviderObjects = nil\n\t\tc.Schedules = nil\n\t\tc.IDPLoginEvent = 0\n\t\tc.Options.ConcurrentExecution = false\n\tcase EventTriggerIDPLogin:\n\t\tc.FsEvents = nil\n\t\tc.ProviderEvents = nil\n\t\tc.Options.GroupNames = nil\n\t\tc.Options.RoleNames = nil\n\t\tc.Options.FsPaths = nil\n\t\tc.Options.Protocols = nil\n\t\tc.Options.EventStatuses = nil\n\t\tc.Options.MinFileSize = 0\n\t\tc.Options.MaxFileSize = 0\n\t\tc.Schedules = nil\n\t\tif !slices.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid Identity Provider login event %d\", c.IDPLoginEvent))\n\t\t}\n\tdefault:\n\t\tc.FsEvents = nil\n\t\tc.ProviderEvents = nil\n\t\tc.Options.GroupNames = nil\n\t\tc.Options.RoleNames = nil\n\t\tc.Options.FsPaths = nil\n\t\tc.Options.Protocols = nil\n\t\tc.Options.EventStatuses = nil\n\t\tc.Options.MinFileSize = 0\n\t\tc.Options.MaxFileSize = 0\n\t\tc.Schedules = nil\n\t\tc.IDPLoginEvent = 0\n\t}\n\n\treturn c.Options.validate()\n}\n\n// EventRule defines the trigger, conditions and actions for an event\ntype EventRule struct {\n\t// Data provider unique identifier\n\tID int64 `json:\"id\"`\n\t// Rule name\n\tName string `json:\"name\"`\n\t// 1 enabled, 0 disabled\n\tStatus int `json:\"status\"`\n\t// optional description\n\tDescription string `json:\"description,omitempty\"`\n\t// Creation time as unix timestamp in milliseconds\n\tCreatedAt int64 `json:\"created_at\"`\n\t// last update time as unix timestamp in milliseconds\n\tUpdatedAt int64 `json:\"updated_at\"`\n\t// Event trigger\n\tTrigger int `json:\"trigger\"`\n\t// Event conditions\n\tConditions EventConditions `json:\"conditions\"`\n\t// actions to execute\n\tActions []EventAction `json:\"actions\"`\n\t// in multi node setups we mark the rule as deleted to be able to update the cache\n\tDeletedAt int64 `json:\"-\"`\n}\n\nfunc (r *EventRule) getACopy() EventRule {\n\tactions := make([]EventAction, 0, len(r.Actions))\n\tfor _, action := range r.Actions {\n\t\tactions = append(actions, action.getACopy())\n\t}\n\n\treturn EventRule{\n\t\tID:          r.ID,\n\t\tName:        r.Name,\n\t\tStatus:      r.Status,\n\t\tDescription: r.Description,\n\t\tCreatedAt:   r.CreatedAt,\n\t\tUpdatedAt:   r.UpdatedAt,\n\t\tTrigger:     r.Trigger,\n\t\tConditions:  r.Conditions.getACopy(),\n\t\tActions:     actions,\n\t\tDeletedAt:   r.DeletedAt,\n\t}\n}\n\n// GuardFromConcurrentExecution returns true if the rule cannot be executed concurrently\n// from multiple instances\nfunc (r *EventRule) GuardFromConcurrentExecution() bool {\n\tif config.IsShared == 0 {\n\t\treturn false\n\t}\n\treturn !r.Conditions.Options.ConcurrentExecution\n}\n\n// GetTriggerAsString returns the rule trigger as string\nfunc (r *EventRule) GetTriggerAsString() string {\n\treturn getTriggerTypeAsString(r.Trigger)\n}\n\n// GetActionsAsString returns the list of action names as comma separated string\nfunc (r *EventRule) GetActionsAsString() string {\n\tactions := make([]string, 0, len(r.Actions))\n\tfor _, action := range r.Actions {\n\t\tactions = append(actions, action.Name)\n\t}\n\treturn strings.Join(actions, \",\")\n}\n\nfunc (r *EventRule) isStatusValid() bool {\n\treturn r.Status >= 0 && r.Status <= 1\n}\n\nfunc (r *EventRule) validate() error { //nolint:gocyclo\n\tif r.Name == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"name is mandatory\"), util.I18nErrorNameRequired)\n\t}\n\tif !util.IsNameValid(r.Name) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif config.NamingRules&1 == 0 && !usernameRegex.MatchString(r.Name) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~\", r.Name)),\n\t\t\tutil.I18nErrorInvalidUser,\n\t\t)\n\t}\n\tif !r.isStatusValid() {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid event rule status: %d\", r.Status))\n\t}\n\tif !isEventTriggerValid(r.Trigger) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid event rule trigger: %d\", r.Trigger))\n\t}\n\tif err := r.Conditions.validate(r.Trigger); err != nil {\n\t\treturn err\n\t}\n\tif len(r.Actions) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"at least one action is required\"), util.I18nErrorRuleActionRequired)\n\t}\n\tactionNames := make(map[string]bool)\n\tactionOrders := make(map[int]bool)\n\tfailureActions := 0\n\thasSyncAction := false\n\tfor idx := range r.Actions {\n\t\tif r.Actions[idx].Name == \"\" {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid action at position %d, name not specified\", idx))\n\t\t}\n\t\tif actionNames[r.Actions[idx].Name] {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"duplicated action %q\", r.Actions[idx].Name)),\n\t\t\t\tutil.I18nErrorRuleDuplicateActions,\n\t\t\t)\n\t\t}\n\t\tif actionOrders[r.Actions[idx].Order] {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"duplicated order %d for action %q\",\n\t\t\t\tr.Actions[idx].Order, r.Actions[idx].Name))\n\t\t}\n\t\tif err := r.Actions[idx].validateAssociation(r.Trigger, r.Conditions.FsEvents); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif r.Actions[idx].Options.IsFailureAction {\n\t\t\tfailureActions++\n\t\t}\n\t\tif r.Actions[idx].Options.ExecuteSync {\n\t\t\thasSyncAction = true\n\t\t}\n\t\tactionNames[r.Actions[idx].Name] = true\n\t\tactionOrders[r.Actions[idx].Order] = true\n\t}\n\tif len(r.Actions) == failureActions {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"at least a non-failure action is required\"),\n\t\t\tutil.I18nErrorRuleFailureActionsOnly,\n\t\t)\n\t}\n\tif !hasSyncAction {\n\t\treturn r.validateMandatorySyncActions()\n\t}\n\treturn nil\n}\n\nfunc (r *EventRule) validateMandatorySyncActions() error {\n\tif r.Trigger != EventTriggerFsEvent {\n\t\treturn nil\n\t}\n\tfor _, ev := range r.Conditions.FsEvents {\n\t\tif slices.Contains(mandatorySyncFsEvents, ev) {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"event %q requires at least a sync action\", ev)),\n\t\t\t\tutil.I18nErrorRuleSyncActionRequired,\n\t\t\t\tutil.I18nErrorArgs(map[string]any{\n\t\t\t\t\t\"val\": ev,\n\t\t\t\t}),\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *EventRule) checkIPBlockedAndCertificateActions() error {\n\tunavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,\n\t\tActionTypeDataRetentionCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,\n\t\tActionTypeUserExpirationCheck}\n\tfor _, action := range r.Actions {\n\t\tif slices.Contains(unavailableActions, action.Type) {\n\t\t\treturn fmt.Errorf(\"action %q, type %q is not supported for event trigger %q\",\n\t\t\t\taction.Name, getActionTypeAsString(action.Type), getTriggerTypeAsString(r.Trigger))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *EventRule) checkProviderEventActions(providerObjectType string) error {\n\t// user quota reset, transfer quota reset, data retention check and filesystem actions\n\t// can be executed only if we modify a user. They will be executed for the\n\t// affected user. Folder quota reset can be executed only for folders.\n\tuserSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,\n\t\tActionTypeDataRetentionCheck, ActionTypeFilesystem,\n\t\tActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}\n\tfor _, action := range r.Actions {\n\t\tif slices.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {\n\t\t\treturn fmt.Errorf(\"action %q, type %q is only supported for provider user events\",\n\t\t\t\taction.Name, getActionTypeAsString(action.Type))\n\t\t}\n\t\tif action.Type == ActionTypeFolderQuotaReset && providerObjectType != actionObjectFolder {\n\t\t\treturn fmt.Errorf(\"action %q, type %q is only supported for provider folder events\",\n\t\t\t\taction.Name, getActionTypeAsString(action.Type))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *EventRule) hasUserAssociated(providerObjectType string) bool {\n\tswitch r.Trigger {\n\tcase EventTriggerProviderEvent:\n\t\treturn providerObjectType == actionObjectUser\n\tcase EventTriggerFsEvent:\n\t\treturn true\n\tdefault:\n\t\tif len(r.Actions) > 0 {\n\t\t\t// should we allow schedules where backup is not the first action?\n\t\t\t// maybe we could pass the action index and check before that index\n\t\t\treturn r.Actions[0].Type == ActionTypeBackup\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (r *EventRule) checkActions(providerObjectType string) error {\n\tnumSyncAction := 0\n\thasIDPAccountCheck := false\n\tfor _, action := range r.Actions {\n\t\tif action.Options.ExecuteSync {\n\t\t\tnumSyncAction++\n\t\t}\n\t\tif action.Type == ActionTypeEmail && action.BaseEventAction.Options.EmailConfig.hasFilesAttachments() {\n\t\t\tif !r.hasUserAssociated(providerObjectType) {\n\t\t\t\treturn errors.New(\"cannot send an email with attachments for a rule with no user associated\")\n\t\t\t}\n\t\t}\n\t\tif action.Type == ActionTypeHTTP && action.BaseEventAction.Options.HTTPConfig.HasMultipartFiles() {\n\t\t\tif !r.hasUserAssociated(providerObjectType) {\n\t\t\t\treturn errors.New(\"cannot upload file/s for a rule with no user associated\")\n\t\t\t}\n\t\t}\n\t\tif action.Type == ActionTypeIDPAccountCheck {\n\t\t\tif r.Trigger != EventTriggerIDPLogin {\n\t\t\t\treturn errors.New(\"IDP account check action is only supported for IDP login trigger\")\n\t\t\t}\n\t\t\tif !action.Options.ExecuteSync {\n\t\t\t\treturn errors.New(\"IDP account check must be a sync action\")\n\t\t\t}\n\t\t\thasIDPAccountCheck = true\n\t\t}\n\t}\n\tif hasIDPAccountCheck && numSyncAction != 1 {\n\t\treturn errors.New(\"IDP account check must be the only sync action\")\n\t}\n\treturn nil\n}\n\n// CheckActionsConsistency returns an error if the actions cannot be executed\nfunc (r *EventRule) CheckActionsConsistency(providerObjectType string) error {\n\tswitch r.Trigger {\n\tcase EventTriggerProviderEvent:\n\t\tif err := r.checkProviderEventActions(providerObjectType); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase EventTriggerFsEvent:\n\t\t// folder quota reset cannot be executed\n\t\tfor _, action := range r.Actions {\n\t\t\tif action.Type == ActionTypeFolderQuotaReset {\n\t\t\t\treturn fmt.Errorf(\"action %q, type %q is not supported for filesystem events\",\n\t\t\t\t\taction.Name, getActionTypeAsString(action.Type))\n\t\t\t}\n\t\t}\n\tcase EventTriggerIPBlocked, EventTriggerCertificate:\n\t\tif err := r.checkIPBlockedAndCertificateActions(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn r.checkActions(providerObjectType)\n}\n\n// PrepareForRendering prepares an EventRule for rendering.\n// It hides confidential data and set to nil the empty secrets\n// so they are not serialized\nfunc (r *EventRule) PrepareForRendering() {\n\tfor idx := range r.Actions {\n\t\tr.Actions[idx].PrepareForRendering()\n\t}\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (r *EventRule) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\trule, err := provider.eventRuleExists(r.Name)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload event rule before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\trule.PrepareForRendering()\n\t\treturn json.Marshal(rule)\n\t}\n\tr.PrepareForRendering()\n\treturn json.Marshal(r)\n}\n\nfunc cloneRenameConfigs(renames []RenameConfig) []RenameConfig {\n\tres := make([]RenameConfig, 0, len(renames))\n\tfor _, c := range renames {\n\t\tres = append(res, RenameConfig{\n\t\t\tKeyValue: KeyValue{\n\t\t\t\tKey:   c.Key,\n\t\t\t\tValue: c.Value,\n\t\t\t},\n\t\t\tUpdateModTime: c.UpdateModTime,\n\t\t})\n\t}\n\treturn res\n}\n\nfunc cloneKeyValues(keyVals []KeyValue) []KeyValue {\n\tres := make([]KeyValue, 0, len(keyVals))\n\tfor _, kv := range keyVals {\n\t\tres = append(res, KeyValue{\n\t\t\tKey:   kv.Key,\n\t\t\tValue: kv.Value,\n\t\t})\n\t}\n\treturn res\n}\n\nfunc cloneConditionPatterns(patterns []ConditionPattern) []ConditionPattern {\n\tres := make([]ConditionPattern, 0, len(patterns))\n\tfor _, p := range patterns {\n\t\tres = append(res, ConditionPattern{\n\t\t\tPattern:      p.Pattern,\n\t\t\tInverseMatch: p.InverseMatch,\n\t\t})\n\t}\n\treturn res\n}\n\nfunc validateConditionPatterns(patterns []ConditionPattern) error {\n\tfor _, name := range patterns {\n\t\tif err := name.validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Task stores the state for a scheduled task\ntype Task struct {\n\tName     string `json:\"name\"`\n\tUpdateAt int64  `json:\"updated_at\"`\n\tVersion  int64  `json:\"version\"`\n}\n"
  },
  {
    "path": "internal/dataprovider/group.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\n// GroupUserSettings defines the settings to apply to users\ntype GroupUserSettings struct {\n\tsdk.BaseGroupUserSettings\n\t// Filesystem configuration details\n\tFsConfig vfs.Filesystem `json:\"filesystem\"`\n}\n\n// Group defines an SFTPGo group.\n// Groups are used to easily configure similar users\ntype Group struct {\n\tsdk.BaseGroup\n\t// settings to apply to users for whom this is a primary group\n\tUserSettings GroupUserSettings `json:\"user_settings,omitempty\"`\n\t// Mapping between virtual paths and virtual folders\n\tVirtualFolders []vfs.VirtualFolder `json:\"virtual_folders,omitempty\"`\n}\n\n// GetPermissions returns the permissions as list\nfunc (g *Group) GetPermissions() []sdk.DirectoryPermissions {\n\tresult := make([]sdk.DirectoryPermissions, 0, len(g.UserSettings.Permissions))\n\tfor k, v := range g.UserSettings.Permissions {\n\t\tresult = append(result, sdk.DirectoryPermissions{\n\t\t\tPath:        k,\n\t\t\tPermissions: v,\n\t\t})\n\t}\n\treturn result\n}\n\n// GetAllowedIPAsString returns the allowed IP as comma separated string\nfunc (g *Group) GetAllowedIPAsString() string {\n\treturn strings.Join(g.UserSettings.Filters.AllowedIP, \",\")\n}\n\n// GetDeniedIPAsString returns the denied IP as comma separated string\nfunc (g *Group) GetDeniedIPAsString() string {\n\treturn strings.Join(g.UserSettings.Filters.DeniedIP, \",\")\n}\n\n// HasExternalAuth returns true if the external authentication is globally enabled\n// and it is not disabled for this group\nfunc (g *Group) HasExternalAuth() bool {\n\tif g.UserSettings.Filters.Hooks.ExternalAuthDisabled {\n\t\treturn false\n\t}\n\tif config.ExternalAuthHook != \"\" {\n\t\treturn true\n\t}\n\treturn plugin.Handler.HasAuthenticators()\n}\n\n// SetEmptySecretsIfNil sets the secrets to empty if nil\nfunc (g *Group) SetEmptySecretsIfNil() {\n\tg.UserSettings.FsConfig.SetEmptySecretsIfNil()\n\tfor idx := range g.VirtualFolders {\n\t\tvfolder := &g.VirtualFolders[idx]\n\t\tvfolder.FsConfig.SetEmptySecretsIfNil()\n\t}\n}\n\n// PrepareForRendering prepares a group for rendering.\n// It hides confidential data and set to nil the empty secrets\n// so they are not serialized\nfunc (g *Group) PrepareForRendering() {\n\tg.UserSettings.FsConfig.HideConfidentialData()\n\tg.UserSettings.FsConfig.SetNilSecretsIfEmpty()\n\tfor idx := range g.VirtualFolders {\n\t\tfolder := &g.VirtualFolders[idx]\n\t\tfolder.PrepareForRendering()\n\t}\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (g *Group) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\tgroup, err := provider.groupExists(g.Name)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload group before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tgroup.PrepareForRendering()\n\t\treturn json.Marshal(group)\n\t}\n\tg.PrepareForRendering()\n\treturn json.Marshal(g)\n}\n\n// GetEncryptionAdditionalData returns the additional data to use for AEAD\nfunc (g *Group) GetEncryptionAdditionalData() string {\n\treturn fmt.Sprintf(\"group_%v\", g.Name)\n}\n\n// HasRedactedSecret returns true if the user has a redacted secret\nfunc (g *Group) hasRedactedSecret() bool {\n\tfor idx := range g.VirtualFolders {\n\t\tfolder := &g.VirtualFolders[idx]\n\t\tif folder.HasRedactedSecret() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn g.UserSettings.FsConfig.HasRedactedSecret()\n}\n\nfunc (g *Group) applyNamingRules() {\n\tg.Name = config.convertName(g.Name)\n\tfor idx := range g.VirtualFolders {\n\t\tg.VirtualFolders[idx].Name = config.convertName(g.VirtualFolders[idx].Name)\n\t}\n}\n\nfunc (g *Group) validate() error {\n\tg.SetEmptySecretsIfNil()\n\tg.applyNamingRules()\n\tif g.Name == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"name is mandatory\"), util.I18nErrorNameRequired)\n\t}\n\tif !util.IsNameValid(g.Name) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif config.NamingRules&1 == 0 && !usernameRegex.MatchString(g.Name) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~\", g.Name)),\n\t\t\tutil.I18nErrorInvalidName,\n\t\t)\n\t}\n\tif g.hasRedactedSecret() {\n\t\treturn util.NewValidationError(\"cannot save a group with a redacted secret\")\n\t}\n\tvfolders, err := validateAssociatedVirtualFolders(g.VirtualFolders)\n\tif err != nil {\n\t\treturn err\n\t}\n\tg.VirtualFolders = vfolders\n\treturn g.validateUserSettings()\n}\n\nfunc (g *Group) validateUserSettings() error {\n\tif g.UserSettings.HomeDir != \"\" {\n\t\tg.UserSettings.HomeDir = filepath.Clean(g.UserSettings.HomeDir)\n\t\tif !filepath.IsAbs(g.UserSettings.HomeDir) {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"home_dir must be an absolute path, actual value: %v\", g.UserSettings.HomeDir)),\n\t\t\t\tutil.I18nErrorInvalidHomeDir,\n\t\t\t)\n\t\t}\n\t}\n\tif err := g.UserSettings.FsConfig.Validate(g.GetEncryptionAdditionalData()); err != nil {\n\t\treturn err\n\t}\n\tif g.UserSettings.TotalDataTransfer > 0 {\n\t\t// if a total data transfer is defined we reset the separate upload and download limits\n\t\tg.UserSettings.UploadDataTransfer = 0\n\t\tg.UserSettings.DownloadDataTransfer = 0\n\t}\n\tif len(g.UserSettings.Permissions) > 0 {\n\t\tpermissions, err := validateUserPermissions(g.UserSettings.Permissions)\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(err, util.I18nErrorGenericPermission)\n\t\t}\n\t\tg.UserSettings.Permissions = permissions\n\t}\n\tg.UserSettings.Filters.TLSCerts = nil\n\tif err := validateBaseFilters(&g.UserSettings.Filters); err != nil {\n\t\treturn err\n\t}\n\tif !g.HasExternalAuth() {\n\t\tg.UserSettings.Filters.ExternalAuthCacheTime = 0\n\t}\n\tg.UserSettings.Filters.UserType = \"\"\n\treturn nil\n}\n\nfunc (g *Group) getACopy() Group {\n\tusers := make([]string, len(g.Users))\n\tcopy(users, g.Users)\n\tadmins := make([]string, len(g.Admins))\n\tcopy(admins, g.Admins)\n\tvirtualFolders := make([]vfs.VirtualFolder, 0, len(g.VirtualFolders))\n\tfor idx := range g.VirtualFolders {\n\t\tvfolder := g.VirtualFolders[idx].GetACopy()\n\t\tvirtualFolders = append(virtualFolders, vfolder)\n\t}\n\tpermissions := make(map[string][]string)\n\tfor k, v := range g.UserSettings.Permissions {\n\t\tperms := make([]string, len(v))\n\t\tcopy(perms, v)\n\t\tpermissions[k] = perms\n\t}\n\n\treturn Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tID:          g.ID,\n\t\t\tName:        g.Name,\n\t\t\tDescription: g.Description,\n\t\t\tCreatedAt:   g.CreatedAt,\n\t\t\tUpdatedAt:   g.UpdatedAt,\n\t\t\tUsers:       users,\n\t\t\tAdmins:      admins,\n\t\t},\n\t\tUserSettings: GroupUserSettings{\n\t\t\tBaseGroupUserSettings: sdk.BaseGroupUserSettings{\n\t\t\t\tHomeDir:              g.UserSettings.HomeDir,\n\t\t\t\tMaxSessions:          g.UserSettings.MaxSessions,\n\t\t\t\tQuotaSize:            g.UserSettings.QuotaSize,\n\t\t\t\tQuotaFiles:           g.UserSettings.QuotaFiles,\n\t\t\t\tPermissions:          permissions,\n\t\t\t\tUploadBandwidth:      g.UserSettings.UploadBandwidth,\n\t\t\t\tDownloadBandwidth:    g.UserSettings.DownloadBandwidth,\n\t\t\t\tUploadDataTransfer:   g.UserSettings.UploadDataTransfer,\n\t\t\t\tDownloadDataTransfer: g.UserSettings.DownloadDataTransfer,\n\t\t\t\tTotalDataTransfer:    g.UserSettings.TotalDataTransfer,\n\t\t\t\tExpiresIn:            g.UserSettings.ExpiresIn,\n\t\t\t\tFilters:              copyBaseUserFilters(g.UserSettings.Filters),\n\t\t\t},\n\t\t\tFsConfig: g.UserSettings.FsConfig.GetACopy(),\n\t\t},\n\t\tVirtualFolders: virtualFolders,\n\t}\n}\n"
  },
  {
    "path": "internal/dataprovider/iplist.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/yl2chen/cidranger\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\t// maximum number of entries to match in memory\n\t// if the list contains more elements than this limit a\n\t// database query will be executed\n\tipListMemoryLimit = 15000\n)\n\nvar (\n\tinMemoryLists map[IPListType]*IPList\n)\n\nfunc init() {\n\tinMemoryLists = map[IPListType]*IPList{}\n}\n\n// IPListType is the enumerable for the supported IP list types\ntype IPListType int\n\n// AsString returns the string representation for the list type\nfunc (t IPListType) AsString() string {\n\tswitch t {\n\tcase IPListTypeAllowList:\n\t\treturn \"Allow list\"\n\tcase IPListTypeDefender:\n\t\treturn \"Defender\"\n\tcase IPListTypeRateLimiterSafeList:\n\t\treturn \"Rate limiters safe list\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// Supported IP list types\nconst (\n\tIPListTypeAllowList IPListType = iota + 1\n\tIPListTypeDefender\n\tIPListTypeRateLimiterSafeList\n)\n\n// Supported IP list modes\nconst (\n\tListModeAllow = iota + 1\n\tListModeDeny\n)\n\nconst (\n\tipTypeV4 = iota + 1\n\tipTypeV6\n)\n\nvar (\n\tsupportedIPListType = []IPListType{IPListTypeAllowList, IPListTypeDefender, IPListTypeRateLimiterSafeList}\n)\n\n// CheckIPListType returns an error if the provided IP list type is not valid\nfunc CheckIPListType(t IPListType) error {\n\tif !slices.Contains(supportedIPListType, t) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid list type %d\", t))\n\t}\n\treturn nil\n}\n\n// IPListEntry defines an entry for the IP addresses list\ntype IPListEntry struct {\n\tIPOrNet     string     `json:\"ipornet\"`\n\tDescription string     `json:\"description,omitempty\"`\n\tType        IPListType `json:\"type\"`\n\tMode        int        `json:\"mode\"`\n\t// Defines the protocols the entry applies to\n\t// - 0 all the supported protocols\n\t// - 1 SSH\n\t// - 2 FTP\n\t// - 4 WebDAV\n\t// - 8 HTTP\n\t// Protocols can be combined\n\tProtocols int    `json:\"protocols\"`\n\tFirst     []byte `json:\"first,omitempty\"`\n\tLast      []byte `json:\"last,omitempty\"`\n\tIPType    int    `json:\"ip_type,omitempty\"`\n\t// Creation time as unix timestamp in milliseconds\n\tCreatedAt int64 `json:\"created_at\"`\n\t// last update time as unix timestamp in milliseconds\n\tUpdatedAt int64 `json:\"updated_at\"`\n\t// in multi node setups we mark the rule as deleted to be able to update the cache\n\tDeletedAt int64 `json:\"-\"`\n}\n\n// PrepareForRendering prepares an IP list entry for rendering.\n// It hides internal fields\nfunc (e *IPListEntry) PrepareForRendering() {\n\te.First = nil\n\te.Last = nil\n\te.IPType = 0\n}\n\n// HasProtocol returns true if the specified protocol is defined\nfunc (e *IPListEntry) HasProtocol(proto string) bool {\n\tswitch proto {\n\tcase protocolSSH:\n\t\treturn e.Protocols&1 != 0\n\tcase protocolFTP:\n\t\treturn e.Protocols&2 != 0\n\tcase protocolWebDAV:\n\t\treturn e.Protocols&4 != 0\n\tcase protocolHTTP:\n\t\treturn e.Protocols&8 != 0\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (e *IPListEntry) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\tentry, err := provider.ipListEntryExists(e.IPOrNet, e.Type)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload IP list entry before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tentry.PrepareForRendering()\n\t\treturn json.Marshal(entry)\n\t}\n\te.PrepareForRendering()\n\treturn json.Marshal(e)\n}\n\nfunc (e *IPListEntry) getKey() string {\n\treturn fmt.Sprintf(\"%d_%s\", e.Type, e.IPOrNet)\n}\n\nfunc (e *IPListEntry) getName() string {\n\treturn e.Type.AsString() + \"-\" + e.IPOrNet\n}\n\nfunc (e *IPListEntry) getFirst() netip.Addr {\n\tif e.IPType == ipTypeV4 {\n\t\tvar a4 [4]byte\n\t\tcopy(a4[:], e.First)\n\t\treturn netip.AddrFrom4(a4)\n\t}\n\tvar a16 [16]byte\n\tcopy(a16[:], e.First)\n\treturn netip.AddrFrom16(a16)\n}\n\nfunc (e *IPListEntry) getLast() netip.Addr {\n\tif e.IPType == ipTypeV4 {\n\t\tvar a4 [4]byte\n\t\tcopy(a4[:], e.Last)\n\t\treturn netip.AddrFrom4(a4)\n\t}\n\tvar a16 [16]byte\n\tcopy(a16[:], e.Last)\n\treturn netip.AddrFrom16(a16)\n}\n\nfunc (e *IPListEntry) checkProtocols() {\n\tfor _, proto := range ValidProtocols {\n\t\tif !e.HasProtocol(proto) {\n\t\t\treturn\n\t\t}\n\t}\n\te.Protocols = 0\n}\n\nfunc (e *IPListEntry) validate() error {\n\tif err := CheckIPListType(e.Type); err != nil {\n\t\treturn err\n\t}\n\te.checkProtocols()\n\tswitch e.Type {\n\tcase IPListTypeDefender:\n\t\tif e.Mode < ListModeAllow || e.Mode > ListModeDeny {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid list mode: %d\", e.Mode))\n\t\t}\n\tdefault:\n\t\tif e.Mode != ListModeAllow {\n\t\t\treturn util.NewValidationError(\"invalid list mode\")\n\t\t}\n\t}\n\te.PrepareForRendering()\n\tif !strings.Contains(e.IPOrNet, \"/\") {\n\t\t// parse as IP\n\t\tparsed, err := netip.ParseAddr(e.IPOrNet)\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(util.NewValidationError(fmt.Sprintf(\"invalid IP %q\", e.IPOrNet)), util.I18nErrorIPInvalid)\n\t\t}\n\t\tif parsed.Is4() {\n\t\t\te.IPOrNet += \"/32\"\n\t\t} else if parsed.Is4In6() {\n\t\t\te.IPOrNet = netip.AddrFrom4(parsed.As4()).String() + \"/32\"\n\t\t} else {\n\t\t\te.IPOrNet += \"/128\"\n\t\t}\n\t}\n\tprefix, err := netip.ParsePrefix(e.IPOrNet)\n\tif err != nil {\n\t\treturn util.NewI18nError(util.NewValidationError(fmt.Sprintf(\"invalid network %q: %v\", e.IPOrNet, err)), util.I18nErrorNetInvalid)\n\t}\n\tprefix = prefix.Masked()\n\tif prefix.Addr().Is4In6() {\n\t\te.IPOrNet = fmt.Sprintf(\"%s/%d\", netip.AddrFrom4(prefix.Addr().As4()).String(), prefix.Bits()-96)\n\t}\n\t// TODO: to remove when the in memory ranger switch to netip\n\t_, _, err = net.ParseCIDR(e.IPOrNet)\n\tif err != nil {\n\t\treturn util.NewI18nError(util.NewValidationError(fmt.Sprintf(\"invalid network: %v\", err)), util.I18nErrorNetInvalid)\n\t}\n\tif prefix.Addr().Is4() || prefix.Addr().Is4In6() {\n\t\te.IPType = ipTypeV4\n\t\tfirst := prefix.Addr().As4()\n\t\tlast := util.GetLastIPForPrefix(prefix).As4()\n\t\te.First = first[:]\n\t\te.Last = last[:]\n\t} else {\n\t\te.IPType = ipTypeV6\n\t\tfirst := prefix.Addr().As16()\n\t\tlast := util.GetLastIPForPrefix(prefix).As16()\n\t\te.First = first[:]\n\t\te.Last = last[:]\n\t}\n\treturn nil\n}\n\nfunc (e *IPListEntry) getACopy() IPListEntry {\n\tfirst := make([]byte, len(e.First))\n\tcopy(first, e.First)\n\tlast := make([]byte, len(e.Last))\n\tcopy(last, e.Last)\n\n\treturn IPListEntry{\n\t\tIPOrNet:     e.IPOrNet,\n\t\tDescription: e.Description,\n\t\tType:        e.Type,\n\t\tMode:        e.Mode,\n\t\tFirst:       first,\n\t\tLast:        last,\n\t\tIPType:      e.IPType,\n\t\tProtocols:   e.Protocols,\n\t\tCreatedAt:   e.CreatedAt,\n\t\tUpdatedAt:   e.UpdatedAt,\n\t\tDeletedAt:   e.DeletedAt,\n\t}\n}\n\n// getAsRangerEntry returns the entry as cidranger.RangerEntry\nfunc (e *IPListEntry) getAsRangerEntry() (cidranger.RangerEntry, error) {\n\t_, network, err := net.ParseCIDR(e.IPOrNet)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tentry := e.getACopy()\n\treturn &rangerEntry{\n\t\tentry:   &entry,\n\t\tnetwork: *network,\n\t}, nil\n}\n\nfunc (e IPListEntry) satisfySearchConstraints(filter, from, order string) bool {\n\tif filter != \"\" && !strings.HasPrefix(e.IPOrNet, filter) {\n\t\treturn false\n\t}\n\tif from != \"\" {\n\t\tif order == OrderASC {\n\t\t\treturn e.IPOrNet > from\n\t\t}\n\t\treturn e.IPOrNet < from\n\t}\n\treturn true\n}\n\ntype rangerEntry struct {\n\tentry   *IPListEntry\n\tnetwork net.IPNet\n}\n\nfunc (e *rangerEntry) Network() net.IPNet {\n\treturn e.network\n}\n\n// IPList defines an IP list\ntype IPList struct {\n\tisInMemory atomic.Bool\n\tlistType   IPListType\n\tmu         sync.RWMutex\n\tRanges     cidranger.Ranger\n}\n\nfunc (l *IPList) addEntry(e *IPListEntry) {\n\tif l.listType != e.Type {\n\t\treturn\n\t}\n\tif !l.isInMemory.Load() {\n\t\treturn\n\t}\n\tentry, err := e.getAsRangerEntry()\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get entry to add %q for list type %d, disabling memory mode, err: %v\",\n\t\t\te.IPOrNet, l.listType, err)\n\t\tl.isInMemory.Store(false)\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\tif err := l.Ranges.Insert(entry); err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to add entry %q for list type %d, disabling memory mode, err: %v\",\n\t\t\te.IPOrNet, l.listType, err)\n\t\tl.isInMemory.Store(false)\n\t\treturn\n\t}\n\tif l.Ranges.Len() >= ipListMemoryLimit {\n\t\tproviderLog(logger.LevelError, \"memory limit exceeded for list type %d, disabling memory mode\", l.listType)\n\t\tl.isInMemory.Store(false)\n\t}\n}\n\nfunc (l *IPList) removeEntry(e *IPListEntry) {\n\tif l.listType != e.Type {\n\t\treturn\n\t}\n\tif !l.isInMemory.Load() {\n\t\treturn\n\t}\n\tentry, err := e.getAsRangerEntry()\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get entry to remove %q for list type %d, disabling memory mode, err: %v\",\n\t\t\te.IPOrNet, l.listType, err)\n\t\tl.isInMemory.Store(false)\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\tif _, err := l.Ranges.Remove(entry.Network()); err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to remove entry %q for list type %d, disabling memory mode, err: %v\",\n\t\t\te.IPOrNet, l.listType, err)\n\t\tl.isInMemory.Store(false)\n\t}\n}\n\nfunc (l *IPList) updateEntry(e *IPListEntry) {\n\tif l.listType != e.Type {\n\t\treturn\n\t}\n\tif !l.isInMemory.Load() {\n\t\treturn\n\t}\n\tentry, err := e.getAsRangerEntry()\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get entry to update %q for list type %d, disabling memory mode, err: %v\",\n\t\t\te.IPOrNet, l.listType, err)\n\t\tl.isInMemory.Store(false)\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\tif _, err := l.Ranges.Remove(entry.Network()); err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to remove entry to update %q for list type %d, disabling memory mode, err: %v\",\n\t\t\te.IPOrNet, l.listType, err)\n\t\tl.isInMemory.Store(false)\n\t\treturn\n\t}\n\tif err := l.Ranges.Insert(entry); err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to add entry to update %q for list type %d, disabling memory mode, err: %v\",\n\t\t\te.IPOrNet, l.listType, err)\n\t\tl.isInMemory.Store(false)\n\t}\n\tif l.Ranges.Len() >= ipListMemoryLimit {\n\t\tproviderLog(logger.LevelError, \"memory limit exceeded for list type %d, disabling memory mode\", l.listType)\n\t\tl.isInMemory.Store(false)\n\t}\n}\n\n// DisableMemoryMode disables memory mode forcing database queries\nfunc (l *IPList) DisableMemoryMode() {\n\tl.isInMemory.Store(false)\n}\n\n// IsListed checks if there is a match for the specified IP and protocol.\n// If there are multiple matches, the first one is returned, in no particular order,\n// so the behavior is undefined\nfunc (l *IPList) IsListed(ip, protocol string) (bool, int, error) {\n\tif l.isInMemory.Load() {\n\t\tl.mu.RLock()\n\t\tdefer l.mu.RUnlock()\n\n\t\tif l.Ranges.Len() == 0 {\n\t\t\treturn false, 0, nil\n\t\t}\n\n\t\tparsedIP := net.ParseIP(ip)\n\t\tif parsedIP == nil {\n\t\t\treturn false, 0, fmt.Errorf(\"invalid IP %s\", ip)\n\t\t}\n\n\t\tentries, err := l.Ranges.ContainingNetworks(parsedIP)\n\t\tif err != nil {\n\t\t\treturn false, 0, fmt.Errorf(\"unable to find containing networks for ip %q: %w\", ip, err)\n\t\t}\n\t\tfor _, e := range entries {\n\t\t\tentry, ok := e.(*rangerEntry)\n\t\t\tif ok {\n\t\t\t\tif entry.entry.Protocols == 0 || entry.entry.HasProtocol(protocol) {\n\t\t\t\t\treturn true, entry.entry.Mode, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false, 0, nil\n\t}\n\n\tentries, err := provider.getListEntriesForIP(ip, l.listType)\n\tif err != nil {\n\t\treturn false, 0, err\n\t}\n\tfor _, e := range entries {\n\t\tif e.Protocols == 0 || e.HasProtocol(protocol) {\n\t\t\treturn true, e.Mode, nil\n\t\t}\n\t}\n\n\treturn false, 0, nil\n}\n\n// NewIPList returns a new IP list for the specified type\nfunc NewIPList(listType IPListType) (*IPList, error) {\n\tdelete(inMemoryLists, listType)\n\tcount, err := provider.countIPListEntries(listType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif count < ipListMemoryLimit {\n\t\tproviderLog(logger.LevelInfo, \"using in-memory matching for list type %d, num entries: %d\", listType, count)\n\t\tentries, err := provider.getIPListEntries(listType, \"\", \"\", OrderASC, 0)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tipList := &IPList{\n\t\t\tlistType: listType,\n\t\t\tRanges:   cidranger.NewPCTrieRanger(),\n\t\t}\n\t\tfor idx := range entries {\n\t\t\te := entries[idx]\n\t\t\tentry, err := e.getAsRangerEntry()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to get ranger for entry %q: %w\", e.IPOrNet, err)\n\t\t\t}\n\t\t\tif err := ipList.Ranges.Insert(entry); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to add ranger for entry %q: %w\", e.IPOrNet, err)\n\t\t\t}\n\t\t}\n\t\tipList.isInMemory.Store(true)\n\t\tinMemoryLists[listType] = ipList\n\n\t\treturn ipList, nil\n\t}\n\tproviderLog(logger.LevelInfo, \"list type %d has %d entries, in-memory matching disabled\", listType, count)\n\tipList := &IPList{\n\t\tlistType: listType,\n\t\tRanges:   nil,\n\t}\n\tipList.isInMemory.Store(false)\n\treturn ipList, nil\n}\n"
  },
  {
    "path": "internal/dataprovider/memory.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nvar (\n\terrMemoryProviderClosed = errors.New(\"memory provider is closed\")\n)\n\ntype memoryProviderHandle struct {\n\t// configuration file to use for loading users\n\tconfigFile string\n\tsync.Mutex\n\tisClosed bool\n\t// slice with ordered usernames\n\tusernames []string\n\t// map for users, username is the key\n\tusers map[string]User\n\t// slice with ordered group names\n\tgroupnames []string\n\t// map for group, group name is the key\n\tgroups map[string]Group\n\t// map for virtual folders, folder name is the key\n\tvfolders map[string]vfs.BaseVirtualFolder\n\t// slice with ordered folder names\n\tvfoldersNames []string\n\t// map for admins, username is the key\n\tadmins map[string]Admin\n\t// slice with ordered admins\n\tadminsUsernames []string\n\t// map for API keys, keyID is the key\n\tapiKeys map[string]APIKey\n\t// slice with ordered API keys KeyID\n\tapiKeysIDs []string\n\t// map for shares, shareID is the key\n\tshares map[string]Share\n\t// slice with ordered shares shareID\n\tsharesIDs []string\n\t// map for event actions, name is the key\n\tactions map[string]BaseEventAction\n\t// slice with ordered actions\n\tactionsNames []string\n\t// map for event actions, name is the key\n\trules map[string]EventRule\n\t// slice with ordered rules\n\trulesNames []string\n\t// map for roles, name is the key\n\troles map[string]Role\n\t// slice with ordered roles\n\troleNames []string\n\t// map for IP List entry\n\tipListEntries map[string]IPListEntry\n\t// slice with ordered IP list entries\n\tipListEntriesKeys []string\n\t// configurations\n\tconfigs Configs\n}\n\n// MemoryProvider defines the auth provider for a memory store\ntype MemoryProvider struct {\n\tdbHandle *memoryProviderHandle\n}\n\nfunc initializeMemoryProvider(basePath string) error {\n\tconfigFile := \"\"\n\tif util.IsFileInputValid(config.Name) {\n\t\tconfigFile = config.Name\n\t\tif !filepath.IsAbs(configFile) {\n\t\t\tconfigFile = filepath.Join(basePath, configFile)\n\t\t}\n\t}\n\tprovider = &MemoryProvider{\n\t\tdbHandle: &memoryProviderHandle{\n\t\t\tisClosed:          false,\n\t\t\tusernames:         []string{},\n\t\t\tusers:             make(map[string]User),\n\t\t\tgroupnames:        []string{},\n\t\t\tgroups:            make(map[string]Group),\n\t\t\tvfolders:          make(map[string]vfs.BaseVirtualFolder),\n\t\t\tvfoldersNames:     []string{},\n\t\t\tadmins:            make(map[string]Admin),\n\t\t\tadminsUsernames:   []string{},\n\t\t\tapiKeys:           make(map[string]APIKey),\n\t\t\tapiKeysIDs:        []string{},\n\t\t\tshares:            make(map[string]Share),\n\t\t\tsharesIDs:         []string{},\n\t\t\tactions:           make(map[string]BaseEventAction),\n\t\t\tactionsNames:      []string{},\n\t\t\trules:             make(map[string]EventRule),\n\t\t\trulesNames:        []string{},\n\t\t\troles:             map[string]Role{},\n\t\t\troleNames:         []string{},\n\t\t\tipListEntries:     map[string]IPListEntry{},\n\t\t\tipListEntriesKeys: []string{},\n\t\t\tconfigs:           Configs{},\n\t\t\tconfigFile:        configFile,\n\t\t},\n\t}\n\treturn provider.reloadConfig()\n}\n\nfunc (p *MemoryProvider) checkAvailability() error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) close() error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tp.dbHandle.isClosed = true\n\treturn nil\n}\n\nfunc (p *MemoryProvider) validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error) {\n\tvar user User\n\tif tlsCert == nil {\n\t\treturn user, errors.New(\"TLS certificate cannot be null or empty\")\n\t}\n\tuser, err := p.userExists(username, \"\")\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, err\n\t}\n\treturn checkUserAndTLSCertificate(&user, protocol, tlsCert)\n}\n\nfunc (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {\n\tuser, err := p.userExists(username, \"\")\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, err\n\t}\n\treturn checkUserAndPass(&user, password, ip, protocol)\n}\n\nfunc (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte, isSSHCert bool) (User, string, error) {\n\tvar user User\n\tif len(pubKey) == 0 {\n\t\treturn user, \"\", errors.New(\"credentials cannot be null or empty\")\n\t}\n\tuser, err := p.userExists(username, \"\")\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, \"\", err\n\t}\n\treturn checkUserAndPubKey(&user, pubKey, isSSHCert)\n}\n\nfunc (p *MemoryProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {\n\tadmin, err := p.adminExists(username)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating admin %q: %v\", username, err)\n\t\treturn admin, err\n\t}\n\terr = admin.checkUserAndPass(password, ip)\n\treturn admin, err\n}\n\nfunc (p *MemoryProvider) updateAPIKeyLastUse(keyID string) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tapiKey, err := p.apiKeyExistsInternal(keyID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tapiKey.LastUseAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.apiKeys[apiKey.KeyID] = apiKey\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getAdminSignature(username string) (string, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn \"\", errMemoryProviderClosed\n\t}\n\tadmin, err := p.adminExistsInternal(username)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strconv.FormatInt(admin.UpdatedAt, 10), nil\n}\n\nfunc (p *MemoryProvider) getUserSignature(username string) (string, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn \"\", errMemoryProviderClosed\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strconv.FormatInt(user.UpdatedAt, 10), nil\n}\n\nfunc (p *MemoryProvider) setUpdatedAt(username string) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\treturn\n\t}\n\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.users[user.Username] = user\n\tsetLastUserUpdate()\n}\n\nfunc (p *MemoryProvider) updateLastLogin(username string) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuser.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.users[user.Username] = user\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateAdminLastLogin(username string) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tadmin, err := p.adminExistsInternal(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tadmin.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.admins[admin.Username] = admin\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to update transfer quota for user %q error: %v\", username, err)\n\t\treturn err\n\t}\n\tif reset {\n\t\tuser.UsedUploadDataTransfer = uploadSize\n\t\tuser.UsedDownloadDataTransfer = downloadSize\n\t} else {\n\t\tuser.UsedUploadDataTransfer += uploadSize\n\t\tuser.UsedDownloadDataTransfer += downloadSize\n\t}\n\tuser.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())\n\tproviderLog(logger.LevelDebug, \"transfer quota updated for user %q, ul increment: %v dl increment: %v is reset? %v\",\n\t\tusername, uploadSize, downloadSize, reset)\n\tp.dbHandle.users[user.Username] = user\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to update quota for user %q error: %v\", username, err)\n\t\treturn err\n\t}\n\tif reset {\n\t\tuser.UsedQuotaSize = sizeAdd\n\t\tuser.UsedQuotaFiles = filesAdd\n\t} else {\n\t\tuser.UsedQuotaSize += sizeAdd\n\t\tuser.UsedQuotaFiles += filesAdd\n\t}\n\tuser.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())\n\tproviderLog(logger.LevelDebug, \"quota updated for user %q, files increment: %v size increment: %v is reset? %v\",\n\t\tusername, filesAdd, sizeAdd, reset)\n\tp.dbHandle.users[user.Username] = user\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getUsedQuota(username string) (int, int64, int64, int64, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn 0, 0, 0, 0, errMemoryProviderClosed\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get quota for user %q error: %v\", username, err)\n\t\treturn 0, 0, 0, 0, err\n\t}\n\treturn user.UsedQuotaFiles, user.UsedQuotaSize, user.UsedUploadDataTransfer, user.UsedDownloadDataTransfer, err\n}\n\nfunc (p *MemoryProvider) addUser(user *User) error {\n\terr := ValidateUser(user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\t_, err = p.userExistsInternal(user.Username)\n\tif err == nil {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"%w: username %v already exists\", ErrDuplicatedKey, user.Username),\n\t\t\tutil.I18nErrorDuplicatedUsername,\n\t\t)\n\t}\n\tuser.ID = p.getNextID()\n\tuser.LastQuotaUpdate = 0\n\tuser.UsedQuotaSize = 0\n\tuser.UsedQuotaFiles = 0\n\tuser.UsedUploadDataTransfer = 0\n\tuser.UsedDownloadDataTransfer = 0\n\tuser.LastLogin = 0\n\tuser.FirstUpload = 0\n\tuser.FirstDownload = 0\n\tuser.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tif err := p.addUserToRole(user.Username, user.Role); err != nil {\n\t\treturn err\n\t}\n\tvar mappedGroups []string\n\tfor idx := range user.Groups {\n\t\tif err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {\n\t\t\t// try to remove group mapping\n\t\t\tfor _, g := range mappedGroups {\n\t\t\t\tp.removeUserFromGroupMapping(user.Username, g)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tmappedGroups = append(mappedGroups, user.Groups[idx].Name)\n\t}\n\tvar mappedFolders []string\n\tfor idx := range user.VirtualFolders {\n\t\tif err = p.addUserToFolderMapping(user.Username, user.VirtualFolders[idx].Name); err != nil {\n\t\t\t// try to remove folder mapping\n\t\t\tfor _, f := range mappedFolders {\n\t\t\t\tp.removeRelationFromFolderMapping(f, user.Username, \"\")\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tmappedFolders = append(mappedFolders, user.VirtualFolders[idx].Name)\n\t}\n\tp.dbHandle.users[user.Username] = user.getACopy()\n\tp.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)\n\tsort.Strings(p.dbHandle.usernames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateUser(user *User) error { //nolint:gocyclo\n\terr := ValidateUser(user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\tu, err := p.userExistsInternal(user.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.removeUserFromRole(u.Username, u.Role)\n\tif err := p.addUserToRole(user.Username, user.Role); err != nil {\n\t\t// try ro add old role\n\t\tif errRollback := p.addUserToRole(u.Username, u.Role); errRollback != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to rollback old role %q for user %q, error: %v\",\n\t\t\t\tu.Role, u.Username, errRollback)\n\t\t}\n\t\treturn err\n\t}\n\tfor idx := range u.Groups {\n\t\tp.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)\n\t}\n\tfor idx := range user.Groups {\n\t\tif err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {\n\t\t\t// try to add old mapping\n\t\t\tfor _, g := range u.Groups {\n\t\t\t\tif errRollback := p.addUserToGroupMapping(user.Username, g.Name); errRollback != nil {\n\t\t\t\t\tproviderLog(logger.LevelError, \"unable to rollback old group mapping %q for user %q, error: %v\",\n\t\t\t\t\t\tg.Name, user.Username, errRollback)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, oldFolder := range u.VirtualFolders {\n\t\tp.removeRelationFromFolderMapping(oldFolder.Name, u.Username, \"\")\n\t}\n\tfor idx := range user.VirtualFolders {\n\t\tif err = p.addUserToFolderMapping(user.Username, user.VirtualFolders[idx].Name); err != nil {\n\t\t\t// try to add old mapping\n\t\t\tfor _, f := range u.VirtualFolders {\n\t\t\t\tif errRollback := p.addUserToFolderMapping(user.Username, f.Name); errRollback != nil {\n\t\t\t\t\tproviderLog(logger.LevelError, \"unable to rollback old folder mapping %q for user %q, error: %v\",\n\t\t\t\t\t\tf.Name, user.Username, errRollback)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\tuser.LastQuotaUpdate = u.LastQuotaUpdate\n\tuser.UsedQuotaSize = u.UsedQuotaSize\n\tuser.UsedQuotaFiles = u.UsedQuotaFiles\n\tuser.UsedUploadDataTransfer = u.UsedUploadDataTransfer\n\tuser.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer\n\tuser.LastLogin = u.LastLogin\n\tuser.FirstDownload = u.FirstDownload\n\tuser.FirstUpload = u.FirstUpload\n\tuser.CreatedAt = u.CreatedAt\n\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tuser.ID = u.ID\n\t// pre-login and external auth hook will use the passed *user so save a copy\n\tp.dbHandle.users[user.Username] = user.getACopy()\n\tsetLastUserUpdate()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteUser(user User, _ bool) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tu, err := p.userExistsInternal(user.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.removeUserFromRole(u.Username, u.Role)\n\tfor _, oldFolder := range u.VirtualFolders {\n\t\tp.removeRelationFromFolderMapping(oldFolder.Name, u.Username, \"\")\n\t}\n\tfor idx := range u.Groups {\n\t\tp.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)\n\t}\n\tdelete(p.dbHandle.users, user.Username)\n\t// this could be more efficient\n\tp.dbHandle.usernames = make([]string, 0, len(p.dbHandle.users))\n\tfor username := range p.dbHandle.users {\n\t\tp.dbHandle.usernames = append(p.dbHandle.usernames, username)\n\t}\n\tsort.Strings(p.dbHandle.usernames)\n\tp.deleteAPIKeysWithUser(user.Username)\n\tp.deleteSharesWithUser(user.Username)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateUserPassword(username, password string) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuser.Password = password\n\tuser.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.users[username] = user\n\treturn nil\n}\n\nfunc (p *MemoryProvider) dumpUsers() ([]User, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tusers := make([]User, 0, len(p.dbHandle.usernames))\n\tvar err error\n\tif p.dbHandle.isClosed {\n\t\treturn users, errMemoryProviderClosed\n\t}\n\tfor _, username := range p.dbHandle.usernames {\n\t\tu := p.dbHandle.users[username]\n\t\tuser := u.getACopy()\n\t\tp.addVirtualFoldersToUser(&user)\n\t\tusers = append(users, user)\n\t}\n\treturn users, err\n}\n\nfunc (p *MemoryProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tfolders := make([]vfs.BaseVirtualFolder, 0, len(p.dbHandle.vfoldersNames))\n\tif p.dbHandle.isClosed {\n\t\treturn folders, errMemoryProviderClosed\n\t}\n\tfor _, f := range p.dbHandle.vfolders {\n\t\tfolders = append(folders, f)\n\t}\n\treturn folders, nil\n}\n\nfunc (p *MemoryProvider) getRecentlyUpdatedUsers(after int64) ([]User, error) {\n\tif getLastUserUpdate() < after {\n\t\treturn nil, nil\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tusers := make([]User, 0, 10)\n\tfor _, username := range p.dbHandle.usernames {\n\t\tu := p.dbHandle.users[username]\n\t\tif u.UpdatedAt < after {\n\t\t\tcontinue\n\t\t}\n\t\tuser := u.getACopy()\n\t\tp.addVirtualFoldersToUser(&user)\n\t\tif len(user.Groups) > 0 {\n\t\t\tgroupMapping := make(map[string]Group)\n\t\t\tfor idx := range user.Groups {\n\t\t\t\tgroup, err := p.groupExistsInternal(user.Groups[idx].Name)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tgroupMapping[group.Name] = group\n\t\t\t}\n\t\t\tuser.applyGroupSettings(groupMapping)\n\t\t}\n\n\t\tuser.SetEmptySecretsIfNil()\n\t\tusers = append(users, user)\n\t}\n\n\treturn users, nil\n}\n\nfunc (p *MemoryProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {\n\tusers := make([]User, 0, 30)\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn users, errMemoryProviderClosed\n\t}\n\tfor _, username := range p.dbHandle.usernames {\n\t\tif needFolders, ok := toFetch[username]; ok {\n\t\t\tu := p.dbHandle.users[username]\n\t\t\tuser := u.getACopy()\n\t\t\tif needFolders {\n\t\t\t\tp.addVirtualFoldersToUser(&user)\n\t\t\t}\n\t\t\tif len(user.Groups) > 0 {\n\t\t\t\tgroupMapping := make(map[string]Group)\n\t\t\t\tfor idx := range user.Groups {\n\t\t\t\t\tgroup, err := p.groupExistsInternal(user.Groups[idx].Name)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tgroupMapping[group.Name] = group\n\t\t\t\t}\n\t\t\t\tuser.applyGroupSettings(groupMapping)\n\t\t\t}\n\t\t\tuser.SetEmptySecretsIfNil()\n\t\t\tuser.PrepareForRendering()\n\t\t\tusers = append(users, user)\n\t\t}\n\t}\n\n\treturn users, nil\n}\n\nfunc (p *MemoryProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {\n\tusers := make([]User, 0, limit)\n\tvar err error\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn users, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn users, err\n\t}\n\titNum := 0\n\tif order == OrderASC {\n\t\tfor _, username := range p.dbHandle.usernames {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tu := p.dbHandle.users[username]\n\t\t\tuser := u.getACopy()\n\t\t\tif !user.hasRole(role) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tp.addVirtualFoldersToUser(&user)\n\t\t\tuser.PrepareForRendering()\n\t\t\tusers = append(users, user)\n\t\t\tif len(users) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(p.dbHandle.usernames) - 1; i >= 0; i-- {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tusername := p.dbHandle.usernames[i]\n\t\t\tu := p.dbHandle.users[username]\n\t\t\tuser := u.getACopy()\n\t\t\tif !user.hasRole(role) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tp.addVirtualFoldersToUser(&user)\n\t\t\tuser.PrepareForRendering()\n\t\t\tusers = append(users, user)\n\t\t\tif len(users) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn users, err\n}\n\nfunc (p *MemoryProvider) userExists(username, role string) (User, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn User{}, errMemoryProviderClosed\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tif !user.hasRole(role) {\n\t\treturn User{}, util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n\t}\n\tp.addVirtualFoldersToUser(&user)\n\treturn user, nil\n}\n\nfunc (p *MemoryProvider) userExistsInternal(username string) (User, error) {\n\tif val, ok := p.dbHandle.users[username]; ok {\n\t\treturn val.getACopy(), nil\n\t}\n\treturn User{}, util.NewRecordNotFoundError(fmt.Sprintf(\"username %q does not exist\", username))\n}\n\nfunc (p *MemoryProvider) groupExistsInternal(name string) (Group, error) {\n\tif val, ok := p.dbHandle.groups[name]; ok {\n\t\treturn val.getACopy(), nil\n\t}\n\treturn Group{}, util.NewRecordNotFoundError(fmt.Sprintf(\"group %q does not exist\", name))\n}\n\nfunc (p *MemoryProvider) actionExistsInternal(name string) (BaseEventAction, error) {\n\tif val, ok := p.dbHandle.actions[name]; ok {\n\t\treturn val.getACopy(), nil\n\t}\n\treturn BaseEventAction{}, util.NewRecordNotFoundError(fmt.Sprintf(\"event action %q does not exist\", name))\n}\n\nfunc (p *MemoryProvider) ruleExistsInternal(name string) (EventRule, error) {\n\tif val, ok := p.dbHandle.rules[name]; ok {\n\t\treturn val.getACopy(), nil\n\t}\n\treturn EventRule{}, util.NewRecordNotFoundError(fmt.Sprintf(\"event rule %q does not exist\", name))\n}\n\nfunc (p *MemoryProvider) roleExistsInternal(name string) (Role, error) {\n\tif val, ok := p.dbHandle.roles[name]; ok {\n\t\treturn val.getACopy(), nil\n\t}\n\treturn Role{}, util.NewRecordNotFoundError(fmt.Sprintf(\"role %q does not exist\", name))\n}\n\nfunc (p *MemoryProvider) ipListEntryExistsInternal(entry *IPListEntry) (IPListEntry, error) {\n\tif val, ok := p.dbHandle.ipListEntries[entry.getKey()]; ok {\n\t\treturn val.getACopy(), nil\n\t}\n\treturn IPListEntry{}, util.NewRecordNotFoundError(fmt.Sprintf(\"IP list entry %q does not exist\", entry.getName()))\n}\n\nfunc (p *MemoryProvider) addAdmin(admin *Admin) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\terr := admin.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = p.adminExistsInternal(admin.Username)\n\tif err == nil {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"%w: admin %q already exists\", ErrDuplicatedKey, admin.Username),\n\t\t\tutil.I18nErrorDuplicatedUsername,\n\t\t)\n\t}\n\tadmin.ID = p.getNextAdminID()\n\tadmin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tadmin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tadmin.LastLogin = 0\n\tif err := p.addAdminToRole(admin.Username, admin.Role); err != nil {\n\t\treturn err\n\t}\n\tvar mappedAdmins []string\n\tfor idx := range admin.Groups {\n\t\tif err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {\n\t\t\t// try to remove group mapping\n\t\t\tfor _, g := range mappedAdmins {\n\t\t\t\tp.removeAdminFromGroupMapping(admin.Username, g)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tmappedAdmins = append(mappedAdmins, admin.Groups[idx].Name)\n\t}\n\tp.dbHandle.admins[admin.Username] = admin.getACopy()\n\tp.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username)\n\tsort.Strings(p.dbHandle.adminsUsernames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateAdmin(admin *Admin) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\terr := admin.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\ta, err := p.adminExistsInternal(admin.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.removeAdminFromRole(a.Username, a.Role)\n\tif err := p.addAdminToRole(admin.Username, admin.Role); err != nil {\n\t\t// try ro add old role\n\t\tif errRollback := p.addAdminToRole(a.Username, a.Role); errRollback != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to rollback old role %q for admin %q, error: %v\",\n\t\t\t\ta.Role, a.Username, errRollback)\n\t\t}\n\t\treturn err\n\t}\n\tfor idx := range a.Groups {\n\t\tp.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)\n\t}\n\tfor idx := range admin.Groups {\n\t\tif err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {\n\t\t\t// try to add old mapping\n\t\t\tfor _, oldGroup := range a.Groups {\n\t\t\t\tif errRollback := p.addAdminToGroupMapping(a.Username, oldGroup.Name); errRollback != nil {\n\t\t\t\t\tproviderLog(logger.LevelError, \"unable to rollback old group mapping %q for admin %q, error: %v\",\n\t\t\t\t\t\toldGroup.Name, a.Username, errRollback)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\tadmin.ID = a.ID\n\tadmin.CreatedAt = a.CreatedAt\n\tadmin.LastLogin = a.LastLogin\n\tadmin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.admins[admin.Username] = admin.getACopy()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteAdmin(admin Admin) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\ta, err := p.adminExistsInternal(admin.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.removeAdminFromRole(a.Username, a.Role)\n\tfor idx := range a.Groups {\n\t\tp.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)\n\t}\n\n\tdelete(p.dbHandle.admins, admin.Username)\n\t// this could be more efficient\n\tp.dbHandle.adminsUsernames = make([]string, 0, len(p.dbHandle.admins))\n\tfor username := range p.dbHandle.admins {\n\t\tp.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, username)\n\t}\n\tsort.Strings(p.dbHandle.adminsUsernames)\n\tp.deleteAPIKeysWithAdmin(admin.Username)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) adminExists(username string) (Admin, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn Admin{}, errMemoryProviderClosed\n\t}\n\treturn p.adminExistsInternal(username)\n}\n\nfunc (p *MemoryProvider) adminExistsInternal(username string) (Admin, error) {\n\tif val, ok := p.dbHandle.admins[username]; ok {\n\t\treturn val.getACopy(), nil\n\t}\n\treturn Admin{}, util.NewRecordNotFoundError(fmt.Sprintf(\"admin %q does not exist\", username))\n}\n\nfunc (p *MemoryProvider) dumpAdmins() ([]Admin, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tadmins := make([]Admin, 0, len(p.dbHandle.admins))\n\tif p.dbHandle.isClosed {\n\t\treturn admins, errMemoryProviderClosed\n\t}\n\tfor _, admin := range p.dbHandle.admins {\n\t\tadmins = append(admins, admin)\n\t}\n\treturn admins, nil\n}\n\nfunc (p *MemoryProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {\n\tadmins := make([]Admin, 0, limit)\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tif p.dbHandle.isClosed {\n\t\treturn admins, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn admins, nil\n\t}\n\titNum := 0\n\tif order == OrderASC {\n\t\tfor _, username := range p.dbHandle.adminsUsernames {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ta := p.dbHandle.admins[username]\n\t\t\tadmin := a.getACopy()\n\t\t\tadmin.HideConfidentialData()\n\t\t\tadmins = append(admins, admin)\n\t\t\tif len(admins) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(p.dbHandle.adminsUsernames) - 1; i >= 0; i-- {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tusername := p.dbHandle.adminsUsernames[i]\n\t\t\ta := p.dbHandle.admins[username]\n\t\t\tadmin := a.getACopy()\n\t\t\tadmin.HideConfidentialData()\n\t\t\tadmins = append(admins, admin)\n\t\t\tif len(admins) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn admins, nil\n}\n\nfunc (p *MemoryProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tfolder, err := p.folderExistsInternal(name)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to update quota for folder %q error: %v\", name, err)\n\t\treturn err\n\t}\n\tif reset {\n\t\tfolder.UsedQuotaSize = sizeAdd\n\t\tfolder.UsedQuotaFiles = filesAdd\n\t} else {\n\t\tfolder.UsedQuotaSize += sizeAdd\n\t\tfolder.UsedQuotaFiles += filesAdd\n\t}\n\tfolder.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.vfolders[name] = folder\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getGroups(limit, offset int, order string, _ bool) ([]Group, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn nil, nil\n\t}\n\tgroups := make([]Group, 0, limit)\n\titNum := 0\n\tif order == OrderASC {\n\t\tfor _, name := range p.dbHandle.groupnames {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tg := p.dbHandle.groups[name]\n\t\t\tgroup := g.getACopy()\n\t\t\tp.addVirtualFoldersToGroup(&group)\n\t\t\tgroup.PrepareForRendering()\n\t\t\tgroups = append(groups, group)\n\t\t\tif len(groups) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(p.dbHandle.groupnames) - 1; i >= 0; i-- {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname := p.dbHandle.groupnames[i]\n\t\t\tg := p.dbHandle.groups[name]\n\t\t\tgroup := g.getACopy()\n\t\t\tp.addVirtualFoldersToGroup(&group)\n\t\t\tgroup.PrepareForRendering()\n\t\t\tgroups = append(groups, group)\n\t\t\tif len(groups) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn groups, nil\n}\n\nfunc (p *MemoryProvider) getGroupsWithNames(names []string) ([]Group, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tgroups := make([]Group, 0, len(names))\n\tfor _, name := range names {\n\t\tif val, ok := p.dbHandle.groups[name]; ok {\n\t\t\tgroup := val.getACopy()\n\t\t\tp.addVirtualFoldersToGroup(&group)\n\t\t\tgroups = append(groups, group)\n\t\t}\n\t}\n\n\treturn groups, nil\n}\n\nfunc (p *MemoryProvider) getUsersInGroups(names []string) ([]string, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tvar users []string\n\tfor _, name := range names {\n\t\tif val, ok := p.dbHandle.groups[name]; ok {\n\t\t\tgroup := val.getACopy()\n\t\t\tusers = append(users, group.Users...)\n\t\t}\n\t}\n\n\treturn users, nil\n}\n\nfunc (p *MemoryProvider) groupExists(name string) (Group, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn Group{}, errMemoryProviderClosed\n\t}\n\tgroup, err := p.groupExistsInternal(name)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tp.addVirtualFoldersToGroup(&group)\n\treturn group, nil\n}\n\nfunc (p *MemoryProvider) addGroup(group *Group) error {\n\tif err := group.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\t_, err := p.groupExistsInternal(group.Name)\n\tif err == nil {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"%w: group %q already exists\", ErrDuplicatedKey, group.Name),\n\t\t\tutil.I18nErrorDuplicatedUsername,\n\t\t)\n\t}\n\tgroup.ID = p.getNextGroupID()\n\tgroup.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tgroup.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tgroup.Users = nil\n\tgroup.Admins = nil\n\tvar mappedFolders []string\n\tfor idx := range group.VirtualFolders {\n\t\tif err = p.addGroupToFolderMapping(group.Name, group.VirtualFolders[idx].Name); err != nil {\n\t\t\t// try to remove folder mapping\n\t\t\tfor _, f := range mappedFolders {\n\t\t\t\tp.removeRelationFromFolderMapping(f, \"\", group.Name)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tmappedFolders = append(mappedFolders, group.VirtualFolders[idx].Name)\n\t}\n\tp.dbHandle.groups[group.Name] = group.getACopy()\n\tp.dbHandle.groupnames = append(p.dbHandle.groupnames, group.Name)\n\tsort.Strings(p.dbHandle.groupnames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateGroup(group *Group) error {\n\tif err := group.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tg, err := p.groupExistsInternal(group.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, oldFolder := range g.VirtualFolders {\n\t\tp.removeRelationFromFolderMapping(oldFolder.Name, \"\", g.Name)\n\t}\n\tfor idx := range group.VirtualFolders {\n\t\tif err = p.addGroupToFolderMapping(group.Name, group.VirtualFolders[idx].Name); err != nil {\n\t\t\t// try to add old mapping\n\t\t\tfor _, f := range g.VirtualFolders {\n\t\t\t\tif errRollback := p.addGroupToFolderMapping(group.Name, f.Name); errRollback != nil {\n\t\t\t\t\tproviderLog(logger.LevelError, \"unable to rollback old folder mapping %q for group %q, error: %v\",\n\t\t\t\t\t\tf.Name, group.Name, errRollback)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\tgroup.CreatedAt = g.CreatedAt\n\tgroup.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tgroup.ID = g.ID\n\tgroup.Users = g.Users\n\tgroup.Admins = g.Admins\n\tp.dbHandle.groups[group.Name] = group.getACopy()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteGroup(group Group) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tg, err := p.groupExistsInternal(group.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(g.Users) > 0 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"the group %q is referenced, it cannot be removed\", group.Name))\n\t}\n\tfor _, oldFolder := range g.VirtualFolders {\n\t\tp.removeRelationFromFolderMapping(oldFolder.Name, \"\", g.Name)\n\t}\n\tfor _, a := range g.Admins {\n\t\tp.removeGroupFromAdminMapping(g.Name, a)\n\t}\n\tdelete(p.dbHandle.groups, group.Name)\n\t// this could be more efficient\n\tp.dbHandle.groupnames = make([]string, 0, len(p.dbHandle.groups))\n\tfor name := range p.dbHandle.groups {\n\t\tp.dbHandle.groupnames = append(p.dbHandle.groupnames, name)\n\t}\n\tsort.Strings(p.dbHandle.groupnames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) dumpGroups() ([]Group, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tgroups := make([]Group, 0, len(p.dbHandle.groups))\n\tvar err error\n\tif p.dbHandle.isClosed {\n\t\treturn groups, errMemoryProviderClosed\n\t}\n\tfor _, name := range p.dbHandle.groupnames {\n\t\tg := p.dbHandle.groups[name]\n\t\tgroup := g.getACopy()\n\t\tp.addVirtualFoldersToGroup(&group)\n\t\tgroups = append(groups, group)\n\t}\n\treturn groups, err\n}\n\nfunc (p *MemoryProvider) getUsedFolderQuota(name string) (int, int64, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn 0, 0, errMemoryProviderClosed\n\t}\n\tfolder, err := p.folderExistsInternal(name)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get quota for folder %q error: %v\", name, err)\n\t\treturn 0, 0, err\n\t}\n\treturn folder.UsedQuotaFiles, folder.UsedQuotaSize, err\n}\n\nfunc (p *MemoryProvider) addVirtualFoldersToGroup(group *Group) {\n\tif len(group.VirtualFolders) > 0 {\n\t\tvar folders []vfs.VirtualFolder\n\t\tfor idx := range group.VirtualFolders {\n\t\t\tfolder := &group.VirtualFolders[idx]\n\t\t\tbaseFolder, err := p.folderExistsInternal(folder.Name)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfolder.BaseVirtualFolder = baseFolder.GetACopy()\n\t\t\tfolders = append(folders, *folder)\n\t\t}\n\t\tgroup.VirtualFolders = folders\n\t}\n}\n\nfunc (p *MemoryProvider) addActionsToRule(rule *EventRule) {\n\tvar actions []EventAction\n\tfor idx := range rule.Actions {\n\t\taction := &rule.Actions[idx]\n\t\tbaseAction, err := p.actionExistsInternal(action.Name)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tbaseAction.Options.SetEmptySecretsIfNil()\n\t\taction.BaseEventAction = baseAction\n\t\tactions = append(actions, *action)\n\t}\n\trule.Actions = actions\n}\n\nfunc (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) error {\n\ta, err := p.actionExistsInternal(actionName)\n\tif err != nil {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"action %q does not exist\", actionName))\n\t}\n\tif !slices.Contains(a.Rules, ruleName) {\n\t\ta.Rules = append(a.Rules, ruleName)\n\t\tp.dbHandle.actions[actionName] = a\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string) {\n\ta, err := p.actionExistsInternal(actionName)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"action %q does not exist, cannot remove from mapping\", actionName)\n\t\treturn\n\t}\n\tif slices.Contains(a.Rules, ruleName) {\n\t\tvar rules []string\n\t\tfor _, r := range a.Rules {\n\t\t\tif r != ruleName {\n\t\t\t\trules = append(rules, r)\n\t\t\t}\n\t\t}\n\t\ta.Rules = rules\n\t\tp.dbHandle.actions[actionName] = a\n\t}\n}\n\nfunc (p *MemoryProvider) addAdminToGroupMapping(username, groupname string) error {\n\tg, err := p.groupExistsInternal(groupname)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !slices.Contains(g.Admins, username) {\n\t\tg.Admins = append(g.Admins, username)\n\t\tp.dbHandle.groups[groupname] = g\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) removeAdminFromGroupMapping(username, groupname string) {\n\tg, err := p.groupExistsInternal(groupname)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar admins []string\n\tfor _, a := range g.Admins {\n\t\tif a != username {\n\t\t\tadmins = append(admins, a)\n\t\t}\n\t}\n\tg.Admins = admins\n\tp.dbHandle.groups[groupname] = g\n}\n\nfunc (p *MemoryProvider) removeGroupFromAdminMapping(groupname, username string) {\n\tadmin, err := p.adminExistsInternal(username)\n\tif err != nil {\n\t\t// the admin does not exist so there is no associated group\n\t\treturn\n\t}\n\tvar newGroups []AdminGroupMapping\n\tfor _, g := range admin.Groups {\n\t\tif g.Name != groupname {\n\t\t\tnewGroups = append(newGroups, g)\n\t\t}\n\t}\n\tadmin.Groups = newGroups\n\tp.dbHandle.admins[admin.Username] = admin\n}\n\nfunc (p *MemoryProvider) addUserToGroupMapping(username, groupname string) error {\n\tg, err := p.groupExistsInternal(groupname)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !slices.Contains(g.Users, username) {\n\t\tg.Users = append(g.Users, username)\n\t\tp.dbHandle.groups[groupname] = g\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) removeUserFromGroupMapping(username, groupname string) {\n\tg, err := p.groupExistsInternal(groupname)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar users []string\n\tfor _, u := range g.Users {\n\t\tif u != username {\n\t\t\tusers = append(users, u)\n\t\t}\n\t}\n\tg.Users = users\n\tp.dbHandle.groups[groupname] = g\n}\n\nfunc (p *MemoryProvider) addAdminToRole(username, role string) error {\n\tif role == \"\" {\n\t\treturn nil\n\t}\n\tr, err := p.roleExistsInternal(role)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%w: role %q does not exist\", ErrForeignKeyViolated, role)\n\t}\n\tif !slices.Contains(r.Admins, username) {\n\t\tr.Admins = append(r.Admins, username)\n\t\tp.dbHandle.roles[role] = r\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) removeAdminFromRole(username, role string) {\n\tif role == \"\" {\n\t\treturn\n\t}\n\tr, err := p.roleExistsInternal(role)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"role %q does not exist, cannot remove admin %q\", role, username)\n\t\treturn\n\t}\n\tvar admins []string\n\tfor _, a := range r.Admins {\n\t\tif a != username {\n\t\t\tadmins = append(admins, a)\n\t\t}\n\t}\n\tr.Admins = admins\n\tp.dbHandle.roles[role] = r\n}\n\nfunc (p *MemoryProvider) addUserToRole(username, role string) error {\n\tif role == \"\" {\n\t\treturn nil\n\t}\n\tr, err := p.roleExistsInternal(role)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%w: role %q does not exist\", ErrForeignKeyViolated, role)\n\t}\n\tif !slices.Contains(r.Users, username) {\n\t\tr.Users = append(r.Users, username)\n\t\tp.dbHandle.roles[role] = r\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) removeUserFromRole(username, role string) {\n\tif role == \"\" {\n\t\treturn\n\t}\n\tr, err := p.roleExistsInternal(role)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"role %q does not exist, cannot remove user %q\", role, username)\n\t\treturn\n\t}\n\tvar users []string\n\tfor _, u := range r.Users {\n\t\tif u != username {\n\t\t\tusers = append(users, u)\n\t\t}\n\t}\n\tr.Users = users\n\tp.dbHandle.roles[role] = r\n}\n\nfunc (p *MemoryProvider) addUserToFolderMapping(username, foldername string) error {\n\tf, err := p.folderExistsInternal(foldername)\n\tif err != nil {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"unable to get folder %q: %v\", foldername, err))\n\t}\n\tif !slices.Contains(f.Users, username) {\n\t\tf.Users = append(f.Users, username)\n\t\tp.dbHandle.vfolders[foldername] = f\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) addGroupToFolderMapping(name, foldername string) error {\n\tf, err := p.folderExistsInternal(foldername)\n\tif err != nil {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"unable to get folder %q: %v\", foldername, err))\n\t}\n\tif !slices.Contains(f.Groups, name) {\n\t\tf.Groups = append(f.Groups, name)\n\t\tp.dbHandle.vfolders[foldername] = f\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) addVirtualFoldersToUser(user *User) {\n\tif len(user.VirtualFolders) > 0 {\n\t\tvar folders []vfs.VirtualFolder\n\t\tfor idx := range user.VirtualFolders {\n\t\t\tfolder := &user.VirtualFolders[idx]\n\t\t\tbaseFolder, err := p.folderExistsInternal(folder.Name)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfolder.BaseVirtualFolder = baseFolder.GetACopy()\n\t\t\tfolders = append(folders, *folder)\n\t\t}\n\t\tuser.VirtualFolders = folders\n\t}\n}\n\nfunc (p *MemoryProvider) removeRelationFromFolderMapping(folderName, username, groupname string) {\n\tfolder, err := p.folderExistsInternal(folderName)\n\tif err != nil {\n\t\treturn\n\t}\n\tif username != \"\" {\n\t\tvar usernames []string\n\t\tfor _, user := range folder.Users {\n\t\t\tif user != username {\n\t\t\t\tusernames = append(usernames, user)\n\t\t\t}\n\t\t}\n\t\tfolder.Users = usernames\n\t}\n\tif groupname != \"\" {\n\t\tvar groups []string\n\t\tfor _, group := range folder.Groups {\n\t\t\tif group != groupname {\n\t\t\t\tgroups = append(groups, group)\n\t\t\t}\n\t\t}\n\t\tfolder.Groups = groups\n\t}\n\tp.dbHandle.vfolders[folder.Name] = folder\n}\n\nfunc (p *MemoryProvider) folderExistsInternal(name string) (vfs.BaseVirtualFolder, error) {\n\tif val, ok := p.dbHandle.vfolders[name]; ok {\n\t\treturn val, nil\n\t}\n\treturn vfs.BaseVirtualFolder{}, util.NewRecordNotFoundError(fmt.Sprintf(\"folder %q does not exist\", name))\n}\n\nfunc (p *MemoryProvider) getFolders(limit, offset int, order string, _ bool) ([]vfs.BaseVirtualFolder, error) {\n\tfolders := make([]vfs.BaseVirtualFolder, 0, limit)\n\tvar err error\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn folders, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn folders, err\n\t}\n\titNum := 0\n\tif order == OrderASC {\n\t\tfor _, name := range p.dbHandle.vfoldersNames {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tf := p.dbHandle.vfolders[name]\n\t\t\tfolder := f.GetACopy()\n\t\t\tfolder.PrepareForRendering()\n\t\t\tfolders = append(folders, folder)\n\t\t\tif len(folders) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(p.dbHandle.vfoldersNames) - 1; i >= 0; i-- {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname := p.dbHandle.vfoldersNames[i]\n\t\t\tf := p.dbHandle.vfolders[name]\n\t\t\tfolder := f.GetACopy()\n\t\t\tfolder.PrepareForRendering()\n\t\t\tfolders = append(folders, folder)\n\t\t\tif len(folders) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn folders, err\n}\n\nfunc (p *MemoryProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn vfs.BaseVirtualFolder{}, errMemoryProviderClosed\n\t}\n\tfolder, err := p.folderExistsInternal(name)\n\tif err != nil {\n\t\treturn vfs.BaseVirtualFolder{}, err\n\t}\n\treturn folder.GetACopy(), nil\n}\n\nfunc (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {\n\terr := ValidateFolder(folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\t_, err = p.folderExistsInternal(folder.Name)\n\tif err == nil {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"%w: folder %q already exists\", ErrDuplicatedKey, folder.Name),\n\t\t\tutil.I18nErrorDuplicatedUsername,\n\t\t)\n\t}\n\tfolder.ID = p.getNextFolderID()\n\tfolder.Users = nil\n\tfolder.Groups = nil\n\tp.dbHandle.vfolders[folder.Name] = folder.GetACopy()\n\tp.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name)\n\tsort.Strings(p.dbHandle.vfoldersNames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {\n\terr := ValidateFolder(folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tf, err := p.folderExistsInternal(folder.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfolder.ID = f.ID\n\tfolder.LastQuotaUpdate = f.LastQuotaUpdate\n\tfolder.UsedQuotaFiles = f.UsedQuotaFiles\n\tfolder.UsedQuotaSize = f.UsedQuotaSize\n\tfolder.Users = f.Users\n\tfolder.Groups = f.Groups\n\tp.dbHandle.vfolders[folder.Name] = folder.GetACopy()\n\t// now update the related users\n\tfor _, username := range folder.Users {\n\t\tuser, err := p.userExistsInternal(username)\n\t\tif err == nil {\n\t\t\tvar folders []vfs.VirtualFolder\n\t\t\tfor idx := range user.VirtualFolders {\n\t\t\t\tuserFolder := &user.VirtualFolders[idx]\n\t\t\t\tif folder.Name == userFolder.Name {\n\t\t\t\t\tuserFolder.BaseVirtualFolder = folder.GetACopy()\n\t\t\t\t}\n\t\t\t\tfolders = append(folders, *userFolder)\n\t\t\t}\n\t\t\tuser.VirtualFolders = folders\n\t\t\tp.dbHandle.users[user.Username] = user\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteFolder(f vfs.BaseVirtualFolder) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\tfolder, err := p.folderExistsInternal(f.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, username := range folder.Users {\n\t\tuser, err := p.userExistsInternal(username)\n\t\tif err == nil {\n\t\t\tvar folders []vfs.VirtualFolder\n\t\t\tfor idx := range user.VirtualFolders {\n\t\t\t\tuserFolder := &user.VirtualFolders[idx]\n\t\t\t\tif folder.Name != userFolder.Name {\n\t\t\t\t\tfolders = append(folders, *userFolder)\n\t\t\t\t}\n\t\t\t}\n\t\t\tuser.VirtualFolders = folders\n\t\t\tp.dbHandle.users[user.Username] = user\n\t\t}\n\t}\n\tfor _, groupname := range folder.Groups {\n\t\tgroup, err := p.groupExistsInternal(groupname)\n\t\tif err == nil {\n\t\t\tvar folders []vfs.VirtualFolder\n\t\t\tfor idx := range group.VirtualFolders {\n\t\t\t\tgroupFolder := &group.VirtualFolders[idx]\n\t\t\t\tif folder.Name != groupFolder.Name {\n\t\t\t\t\tfolders = append(folders, *groupFolder)\n\t\t\t\t}\n\t\t\t}\n\t\t\tgroup.VirtualFolders = folders\n\t\t\tp.dbHandle.groups[group.Name] = group\n\t\t}\n\t}\n\tdelete(p.dbHandle.vfolders, folder.Name)\n\tp.dbHandle.vfoldersNames = []string{}\n\tfor name := range p.dbHandle.vfolders {\n\t\tp.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, name)\n\t}\n\tsort.Strings(p.dbHandle.vfoldersNames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) apiKeyExistsInternal(keyID string) (APIKey, error) {\n\tif val, ok := p.dbHandle.apiKeys[keyID]; ok {\n\t\treturn val.getACopy(), nil\n\t}\n\treturn APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf(\"API key %q does not exist\", keyID))\n}\n\nfunc (p *MemoryProvider) apiKeyExists(keyID string) (APIKey, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn APIKey{}, errMemoryProviderClosed\n\t}\n\treturn p.apiKeyExistsInternal(keyID)\n}\n\nfunc (p *MemoryProvider) addAPIKey(apiKey *APIKey) error {\n\terr := apiKey.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\t_, err = p.apiKeyExistsInternal(apiKey.KeyID)\n\tif err == nil {\n\t\treturn fmt.Errorf(\"API key %q already exists\", apiKey.KeyID)\n\t}\n\tif apiKey.User != \"\" {\n\t\tif _, err := p.userExistsInternal(apiKey.User); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: related user %q does not exists\", ErrForeignKeyViolated, apiKey.User)\n\t\t}\n\t}\n\tif apiKey.Admin != \"\" {\n\t\tif _, err := p.adminExistsInternal(apiKey.Admin); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: related admin %q does not exists\", ErrForeignKeyViolated, apiKey.Admin)\n\t\t}\n\t}\n\tapiKey.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tapiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tapiKey.LastUseAt = 0\n\tp.dbHandle.apiKeys[apiKey.KeyID] = apiKey.getACopy()\n\tp.dbHandle.apiKeysIDs = append(p.dbHandle.apiKeysIDs, apiKey.KeyID)\n\tsort.Strings(p.dbHandle.apiKeysIDs)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateAPIKey(apiKey *APIKey) error {\n\terr := apiKey.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tk, err := p.apiKeyExistsInternal(apiKey.KeyID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif apiKey.User != \"\" {\n\t\tif _, err := p.userExistsInternal(apiKey.User); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: related user %q does not exists\", ErrForeignKeyViolated, apiKey.User)\n\t\t}\n\t}\n\tif apiKey.Admin != \"\" {\n\t\tif _, err := p.adminExistsInternal(apiKey.Admin); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: related admin %q does not exists\", ErrForeignKeyViolated, apiKey.Admin)\n\t\t}\n\t}\n\tapiKey.ID = k.ID\n\tapiKey.KeyID = k.KeyID\n\tapiKey.Key = k.Key\n\tapiKey.CreatedAt = k.CreatedAt\n\tapiKey.LastUseAt = k.LastUseAt\n\tapiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.apiKeys[apiKey.KeyID] = apiKey.getACopy()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteAPIKey(apiKey APIKey) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\t_, err := p.apiKeyExistsInternal(apiKey.KeyID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdelete(p.dbHandle.apiKeys, apiKey.KeyID)\n\tp.updateAPIKeysOrdering()\n\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {\n\tapiKeys := make([]APIKey, 0, limit)\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tif p.dbHandle.isClosed {\n\t\treturn apiKeys, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn apiKeys, nil\n\t}\n\titNum := 0\n\tif order == OrderDESC {\n\t\tfor i := len(p.dbHandle.apiKeysIDs) - 1; i >= 0; i-- {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkeyID := p.dbHandle.apiKeysIDs[i]\n\t\t\tk := p.dbHandle.apiKeys[keyID]\n\t\t\tapiKey := k.getACopy()\n\t\t\tapiKey.HideConfidentialData()\n\t\t\tapiKeys = append(apiKeys, apiKey)\n\t\t\tif len(apiKeys) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, keyID := range p.dbHandle.apiKeysIDs {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tk := p.dbHandle.apiKeys[keyID]\n\t\t\tapiKey := k.getACopy()\n\t\t\tapiKey.HideConfidentialData()\n\t\t\tapiKeys = append(apiKeys, apiKey)\n\t\t\tif len(apiKeys) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn apiKeys, nil\n}\n\nfunc (p *MemoryProvider) dumpAPIKeys() ([]APIKey, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tapiKeys := make([]APIKey, 0, len(p.dbHandle.apiKeys))\n\tif p.dbHandle.isClosed {\n\t\treturn apiKeys, errMemoryProviderClosed\n\t}\n\tfor _, k := range p.dbHandle.apiKeys {\n\t\tapiKeys = append(apiKeys, k)\n\t}\n\treturn apiKeys, nil\n}\n\nfunc (p *MemoryProvider) deleteAPIKeysWithUser(username string) {\n\tfound := false\n\tfor k, v := range p.dbHandle.apiKeys {\n\t\tif v.User == username {\n\t\t\tdelete(p.dbHandle.apiKeys, k)\n\t\t\tfound = true\n\t\t}\n\t}\n\tif found {\n\t\tp.updateAPIKeysOrdering()\n\t}\n}\n\nfunc (p *MemoryProvider) deleteAPIKeysWithAdmin(username string) {\n\tfound := false\n\tfor k, v := range p.dbHandle.apiKeys {\n\t\tif v.Admin == username {\n\t\t\tdelete(p.dbHandle.apiKeys, k)\n\t\t\tfound = true\n\t\t}\n\t}\n\tif found {\n\t\tp.updateAPIKeysOrdering()\n\t}\n}\n\nfunc (p *MemoryProvider) deleteSharesWithUser(username string) {\n\tfound := false\n\tfor k, v := range p.dbHandle.shares {\n\t\tif v.Username == username {\n\t\t\tdelete(p.dbHandle.shares, k)\n\t\t\tfound = true\n\t\t}\n\t}\n\tif found {\n\t\tp.updateSharesOrdering()\n\t}\n}\n\nfunc (p *MemoryProvider) updateAPIKeysOrdering() {\n\t// this could be more efficient\n\tp.dbHandle.apiKeysIDs = make([]string, 0, len(p.dbHandle.apiKeys))\n\tfor keyID := range p.dbHandle.apiKeys {\n\t\tp.dbHandle.apiKeysIDs = append(p.dbHandle.apiKeysIDs, keyID)\n\t}\n\tsort.Strings(p.dbHandle.apiKeysIDs)\n}\n\nfunc (p *MemoryProvider) updateSharesOrdering() {\n\t// this could be more efficient\n\tp.dbHandle.sharesIDs = make([]string, 0, len(p.dbHandle.shares))\n\tfor shareID := range p.dbHandle.shares {\n\t\tp.dbHandle.sharesIDs = append(p.dbHandle.sharesIDs, shareID)\n\t}\n\tsort.Strings(p.dbHandle.sharesIDs)\n}\n\nfunc (p *MemoryProvider) shareExistsInternal(shareID, username string) (Share, error) {\n\tif val, ok := p.dbHandle.shares[shareID]; ok {\n\t\tif username != \"\" && val.Username != username {\n\t\t\treturn Share{}, util.NewRecordNotFoundError(fmt.Sprintf(\"Share %q does not exist\", shareID))\n\t\t}\n\t\treturn val.getACopy(), nil\n\t}\n\treturn Share{}, util.NewRecordNotFoundError(fmt.Sprintf(\"Share %q does not exist\", shareID))\n}\n\nfunc (p *MemoryProvider) shareExists(shareID, username string) (Share, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn Share{}, errMemoryProviderClosed\n\t}\n\treturn p.shareExistsInternal(shareID, username)\n}\n\nfunc (p *MemoryProvider) addShare(share *Share) error {\n\terr := share.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\t_, err = p.shareExistsInternal(share.ShareID, share.Username)\n\tif err == nil {\n\t\treturn fmt.Errorf(\"share %q already exists\", share.ShareID)\n\t}\n\tif _, err := p.userExistsInternal(share.Username); err != nil {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"related user %q does not exists\", share.Username))\n\t}\n\tif !share.IsRestore {\n\t\tshare.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\tshare.UpdatedAt = share.CreatedAt\n\t\tshare.LastUseAt = 0\n\t\tshare.UsedTokens = 0\n\t}\n\tif share.CreatedAt == 0 {\n\t\tshare.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t}\n\tif share.UpdatedAt == 0 {\n\t\tshare.UpdatedAt = share.CreatedAt\n\t}\n\tp.dbHandle.shares[share.ShareID] = share.getACopy()\n\tp.dbHandle.sharesIDs = append(p.dbHandle.sharesIDs, share.ShareID)\n\tsort.Strings(p.dbHandle.sharesIDs)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateShare(share *Share) error {\n\terr := share.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\ts, err := p.shareExistsInternal(share.ShareID, share.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := p.userExistsInternal(share.Username); err != nil {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"related user %q does not exists\", share.Username))\n\t}\n\tshare.ID = s.ID\n\tshare.ShareID = s.ShareID\n\tif !share.IsRestore {\n\t\tshare.UsedTokens = s.UsedTokens\n\t\tshare.CreatedAt = s.CreatedAt\n\t\tshare.LastUseAt = s.LastUseAt\n\t\tshare.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t}\n\tif share.CreatedAt == 0 {\n\t\tshare.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t}\n\tif share.UpdatedAt == 0 {\n\t\tshare.UpdatedAt = share.CreatedAt\n\t}\n\tp.dbHandle.shares[share.ShareID] = share.getACopy()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteShare(share Share) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\t_, err := p.shareExistsInternal(share.ShareID, share.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdelete(p.dbHandle.shares, share.ShareID)\n\tp.updateSharesOrdering()\n\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getShares(limit int, offset int, order, username string) ([]Share, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tif p.dbHandle.isClosed {\n\t\treturn []Share{}, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn []Share{}, nil\n\t}\n\tshares := make([]Share, 0, limit)\n\titNum := 0\n\tif order == OrderDESC {\n\t\tfor i := len(p.dbHandle.sharesIDs) - 1; i >= 0; i-- {\n\t\t\tshareID := p.dbHandle.sharesIDs[i]\n\t\t\ts := p.dbHandle.shares[shareID]\n\t\t\tif s.Username != username {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tshare := s.getACopy()\n\t\t\tshare.HideConfidentialData()\n\t\t\tshares = append(shares, share)\n\t\t\tif len(shares) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, shareID := range p.dbHandle.sharesIDs {\n\t\t\ts := p.dbHandle.shares[shareID]\n\t\t\tif s.Username != username {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tshare := s.getACopy()\n\t\t\tshare.HideConfidentialData()\n\t\t\tshares = append(shares, share)\n\t\t\tif len(shares) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn shares, nil\n}\n\nfunc (p *MemoryProvider) dumpShares() ([]Share, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tshares := make([]Share, 0, len(p.dbHandle.shares))\n\tif p.dbHandle.isClosed {\n\t\treturn shares, errMemoryProviderClosed\n\t}\n\tfor _, s := range p.dbHandle.shares {\n\t\tshares = append(shares, s)\n\t}\n\treturn shares, nil\n}\n\nfunc (p *MemoryProvider) updateShareLastUse(shareID string, numTokens int) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tshare, err := p.shareExistsInternal(shareID, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tshare.LastUseAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tshare.UsedTokens += numTokens\n\tp.dbHandle.shares[share.ShareID] = share\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getDefenderHosts(_ int64, _ int) ([]DefenderEntry, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) getDefenderHostByIP(_ string, _ int64) (DefenderEntry, error) {\n\treturn DefenderEntry{}, ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) isDefenderHostBanned(_ string) (DefenderEntry, error) {\n\treturn DefenderEntry{}, ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) updateDefenderBanTime(_ string, _ int) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) deleteDefenderHost(_ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) addDefenderEvent(_ string, _ int) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) setDefenderBanTime(_ string, _ int64) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) cleanupDefender(_ int64) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) addActiveTransfer(_ ActiveTransfer) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) updateActiveTransferSizes(_, _, _ int64, _ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) removeActiveTransfer(_ int64, _ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) cleanupActiveTransfers(_ time.Time) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) getActiveTransfers(_ time.Time) ([]ActiveTransfer, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) addSharedSession(_ Session) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) deleteSharedSession(_ string, _ SessionType) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) getSharedSession(_ string, _ SessionType) (Session, error) {\n\treturn Session{}, ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) cleanupSharedSessions(_ SessionType, _ int64) error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) getEventActions(limit, offset int, order string, _ bool) ([]BaseEventAction, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn nil, nil\n\t}\n\tactions := make([]BaseEventAction, 0, limit)\n\titNum := 0\n\tif order == OrderASC {\n\t\tfor _, name := range p.dbHandle.actionsNames {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ta := p.dbHandle.actions[name]\n\t\t\taction := a.getACopy()\n\t\t\taction.PrepareForRendering()\n\t\t\tactions = append(actions, action)\n\t\t\tif len(actions) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(p.dbHandle.actionsNames) - 1; i >= 0; i-- {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname := p.dbHandle.actionsNames[i]\n\t\t\ta := p.dbHandle.actions[name]\n\t\t\taction := a.getACopy()\n\t\t\taction.PrepareForRendering()\n\t\t\tactions = append(actions, action)\n\t\t\tif len(actions) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn actions, nil\n}\n\nfunc (p *MemoryProvider) dumpEventActions() ([]BaseEventAction, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tactions := make([]BaseEventAction, 0, len(p.dbHandle.actions))\n\tfor _, name := range p.dbHandle.actionsNames {\n\t\ta := p.dbHandle.actions[name]\n\t\taction := a.getACopy()\n\t\tactions = append(actions, action)\n\t}\n\treturn actions, nil\n}\n\nfunc (p *MemoryProvider) eventActionExists(name string) (BaseEventAction, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn BaseEventAction{}, errMemoryProviderClosed\n\t}\n\treturn p.actionExistsInternal(name)\n}\n\nfunc (p *MemoryProvider) addEventAction(action *BaseEventAction) error {\n\terr := action.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\t_, err = p.actionExistsInternal(action.Name)\n\tif err == nil {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"%w: event action %q already exists\", ErrDuplicatedKey, action.Name),\n\t\t\tutil.I18nErrorDuplicatedName,\n\t\t)\n\t}\n\taction.ID = p.getNextActionID()\n\taction.Rules = nil\n\tp.dbHandle.actions[action.Name] = action.getACopy()\n\tp.dbHandle.actionsNames = append(p.dbHandle.actionsNames, action.Name)\n\tsort.Strings(p.dbHandle.actionsNames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateEventAction(action *BaseEventAction) error {\n\terr := action.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\toldAction, err := p.actionExistsInternal(action.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"event action %s does not exist\", action.Name)\n\t}\n\taction.ID = oldAction.ID\n\taction.Name = oldAction.Name\n\taction.Rules = nil\n\tif len(oldAction.Rules) > 0 {\n\t\tvar relatedRules []string\n\t\tfor _, ruleName := range oldAction.Rules {\n\t\t\trule, err := p.ruleExistsInternal(ruleName)\n\t\t\tif err == nil {\n\t\t\t\trelatedRules = append(relatedRules, ruleName)\n\t\t\t\trule.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t\t\tp.dbHandle.rules[ruleName] = rule\n\t\t\t\tsetLastRuleUpdate()\n\t\t\t}\n\t\t}\n\t\taction.Rules = relatedRules\n\t}\n\tp.dbHandle.actions[action.Name] = action.getACopy()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteEventAction(action BaseEventAction) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\toldAction, err := p.actionExistsInternal(action.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"event action %s does not exist\", action.Name)\n\t}\n\tif len(oldAction.Rules) > 0 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"action %s is referenced, it cannot be removed\", oldAction.Name))\n\t}\n\tdelete(p.dbHandle.actions, action.Name)\n\t// this could be more efficient\n\tp.dbHandle.actionsNames = make([]string, 0, len(p.dbHandle.actions))\n\tfor name := range p.dbHandle.actions {\n\t\tp.dbHandle.actionsNames = append(p.dbHandle.actionsNames, name)\n\t}\n\tsort.Strings(p.dbHandle.actionsNames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn nil, nil\n\t}\n\titNum := 0\n\trules := make([]EventRule, 0, limit)\n\tif order == OrderASC {\n\t\tfor _, name := range p.dbHandle.rulesNames {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr := p.dbHandle.rules[name]\n\t\t\trule := r.getACopy()\n\t\t\tp.addActionsToRule(&rule)\n\t\t\trule.PrepareForRendering()\n\t\t\trules = append(rules, rule)\n\t\t\tif len(rules) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(p.dbHandle.rulesNames) - 1; i >= 0; i-- {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname := p.dbHandle.rulesNames[i]\n\t\t\tr := p.dbHandle.rules[name]\n\t\t\trule := r.getACopy()\n\t\t\tp.addActionsToRule(&rule)\n\t\t\trule.PrepareForRendering()\n\t\t\trules = append(rules, rule)\n\t\t\tif len(rules) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn rules, nil\n}\n\nfunc (p *MemoryProvider) dumpEventRules() ([]EventRule, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\trules := make([]EventRule, 0, len(p.dbHandle.rules))\n\tfor _, name := range p.dbHandle.rulesNames {\n\t\tr := p.dbHandle.rules[name]\n\t\trule := r.getACopy()\n\t\tp.addActionsToRule(&rule)\n\t\trules = append(rules, rule)\n\t}\n\treturn rules, nil\n}\n\nfunc (p *MemoryProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {\n\tif getLastRuleUpdate() < after {\n\t\treturn nil, nil\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\trules := make([]EventRule, 0, 10)\n\tfor _, name := range p.dbHandle.rulesNames {\n\t\tr := p.dbHandle.rules[name]\n\t\tif r.UpdatedAt < after {\n\t\t\tcontinue\n\t\t}\n\t\trule := r.getACopy()\n\t\tp.addActionsToRule(&rule)\n\t\trules = append(rules, rule)\n\t}\n\treturn rules, nil\n}\n\nfunc (p *MemoryProvider) eventRuleExists(name string) (EventRule, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn EventRule{}, errMemoryProviderClosed\n\t}\n\trule, err := p.ruleExistsInternal(name)\n\tif err != nil {\n\t\treturn rule, err\n\t}\n\tp.addActionsToRule(&rule)\n\treturn rule, nil\n}\n\nfunc (p *MemoryProvider) addEventRule(rule *EventRule) error {\n\tif err := rule.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\t_, err := p.ruleExistsInternal(rule.Name)\n\tif err == nil {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"%w: event rule %q already exists\", ErrDuplicatedKey, rule.Name),\n\t\t\tutil.I18nErrorDuplicatedName,\n\t\t)\n\t}\n\trule.ID = p.getNextRuleID()\n\trule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\trule.UpdatedAt = rule.CreatedAt\n\tvar mappedActions []string\n\tfor idx := range rule.Actions {\n\t\tif err := p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name); err != nil {\n\t\t\t// try to remove action mapping\n\t\t\tfor _, a := range mappedActions {\n\t\t\t\tp.removeRuleFromActionMapping(rule.Name, a)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tmappedActions = append(mappedActions, rule.Actions[idx].Name)\n\t}\n\tsort.Slice(rule.Actions, func(i, j int) bool {\n\t\treturn rule.Actions[i].Order < rule.Actions[j].Order\n\t})\n\tp.dbHandle.rules[rule.Name] = rule.getACopy()\n\tp.dbHandle.rulesNames = append(p.dbHandle.rulesNames, rule.Name)\n\tsort.Strings(p.dbHandle.rulesNames)\n\tsetLastRuleUpdate()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateEventRule(rule *EventRule) error {\n\tif err := rule.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\toldRule, err := p.ruleExistsInternal(rule.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor idx := range oldRule.Actions {\n\t\tp.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name)\n\t}\n\tfor idx := range rule.Actions {\n\t\tif err = p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name); err != nil {\n\t\t\t// try to add old mapping\n\t\t\tfor _, oldAction := range oldRule.Actions {\n\t\t\t\tif errRollback := p.addRuleToActionMapping(oldRule.Name, oldAction.Name); errRollback != nil {\n\t\t\t\t\tproviderLog(logger.LevelError, \"unable to rollback old action mapping %q for rule %q, error: %v\",\n\t\t\t\t\t\toldAction.Name, oldRule.Name, errRollback)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\trule.ID = oldRule.ID\n\trule.CreatedAt = oldRule.CreatedAt\n\trule.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tsort.Slice(rule.Actions, func(i, j int) bool {\n\t\treturn rule.Actions[i].Order < rule.Actions[j].Order\n\t})\n\tp.dbHandle.rules[rule.Name] = rule.getACopy()\n\tsetLastRuleUpdate()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteEventRule(rule EventRule, _ bool) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\toldRule, err := p.ruleExistsInternal(rule.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(oldRule.Actions) > 0 {\n\t\tfor idx := range oldRule.Actions {\n\t\t\tp.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name)\n\t\t}\n\t}\n\tdelete(p.dbHandle.rules, rule.Name)\n\tp.dbHandle.rulesNames = make([]string, 0, len(p.dbHandle.rules))\n\tfor name := range p.dbHandle.rules {\n\t\tp.dbHandle.rulesNames = append(p.dbHandle.rulesNames, name)\n\t}\n\tsort.Strings(p.dbHandle.rulesNames)\n\tsetLastRuleUpdate()\n\treturn nil\n}\n\nfunc (*MemoryProvider) getTaskByName(_ string) (Task, error) {\n\treturn Task{}, ErrNotImplemented\n}\n\nfunc (*MemoryProvider) addTask(_ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (*MemoryProvider) updateTask(_ string, _ int64) error {\n\treturn ErrNotImplemented\n}\n\nfunc (*MemoryProvider) updateTaskTimestamp(_ string) error {\n\treturn ErrNotImplemented\n}\n\nfunc (*MemoryProvider) addNode() error {\n\treturn ErrNotImplemented\n}\n\nfunc (*MemoryProvider) getNodeByName(_ string) (Node, error) {\n\treturn Node{}, ErrNotImplemented\n}\n\nfunc (*MemoryProvider) getNodes() ([]Node, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (*MemoryProvider) updateNodeTimestamp() error {\n\treturn ErrNotImplemented\n}\n\nfunc (*MemoryProvider) cleanupNodes() error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) roleExists(name string) (Role, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn Role{}, errMemoryProviderClosed\n\t}\n\trole, err := p.roleExistsInternal(name)\n\tif err != nil {\n\t\treturn role, err\n\t}\n\treturn role, nil\n}\n\nfunc (p *MemoryProvider) addRole(role *Role) error {\n\tif err := role.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\n\t_, err := p.roleExistsInternal(role.Name)\n\tif err == nil {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"%w: role %q already exists\", ErrDuplicatedKey, role.Name),\n\t\t\tutil.I18nErrorDuplicatedName,\n\t\t)\n\t}\n\trole.ID = p.getNextRoleID()\n\trole.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\trole.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\trole.Users = nil\n\trole.Admins = nil\n\tp.dbHandle.roles[role.Name] = role.getACopy()\n\tp.dbHandle.roleNames = append(p.dbHandle.roleNames, role.Name)\n\tsort.Strings(p.dbHandle.roleNames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateRole(role *Role) error {\n\tif err := role.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\toldRole, err := p.roleExistsInternal(role.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\trole.ID = oldRole.ID\n\trole.CreatedAt = oldRole.CreatedAt\n\trole.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\trole.Users = oldRole.Users\n\trole.Admins = oldRole.Admins\n\tp.dbHandle.roles[role.Name] = role.getACopy()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteRole(role Role) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\toldRole, err := p.roleExistsInternal(role.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(oldRole.Admins) > 0 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"the role %q is referenced, it cannot be removed\", oldRole.Name))\n\t}\n\tfor _, username := range oldRole.Users {\n\t\tuser, err := p.userExistsInternal(username)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif user.Role == role.Name {\n\t\t\tuser.Role = \"\"\n\t\t\tp.dbHandle.users[username] = user\n\t\t} else {\n\t\t\tproviderLog(logger.LevelError, \"user %q does not have the expected role %q, actual %q\", username, role.Name, user.Role)\n\t\t}\n\t}\n\tdelete(p.dbHandle.roles, role.Name)\n\tp.dbHandle.roleNames = make([]string, 0, len(p.dbHandle.roles))\n\tfor name := range p.dbHandle.roles {\n\t\tp.dbHandle.roleNames = append(p.dbHandle.roleNames, name)\n\t}\n\tsort.Strings(p.dbHandle.roleNames)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getRoles(limit int, offset int, order string, _ bool) ([]Role, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tif limit <= 0 {\n\t\treturn nil, nil\n\t}\n\troles := make([]Role, 0, 10)\n\titNum := 0\n\tif order == OrderASC {\n\t\tfor _, name := range p.dbHandle.roleNames {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr := p.dbHandle.roles[name]\n\t\t\trole := r.getACopy()\n\t\t\troles = append(roles, role)\n\t\t\tif len(roles) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(p.dbHandle.roleNames) - 1; i >= 0; i-- {\n\t\t\titNum++\n\t\t\tif itNum <= offset {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname := p.dbHandle.roleNames[i]\n\t\t\tr := p.dbHandle.roles[name]\n\t\t\trole := r.getACopy()\n\t\t\troles = append(roles, role)\n\t\t\tif len(roles) >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn roles, nil\n}\n\nfunc (p *MemoryProvider) dumpRoles() ([]Role, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\n\troles := make([]Role, 0, len(p.dbHandle.roles))\n\tfor _, name := range p.dbHandle.roleNames {\n\t\tr := p.dbHandle.roles[name]\n\t\troles = append(roles, r.getACopy())\n\t}\n\treturn roles, nil\n}\n\nfunc (p *MemoryProvider) ipListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn IPListEntry{}, errMemoryProviderClosed\n\t}\n\tentry, err := p.ipListEntryExistsInternal(&IPListEntry{IPOrNet: ipOrNet, Type: listType})\n\tif err != nil {\n\t\treturn entry, err\n\t}\n\tentry.PrepareForRendering()\n\treturn entry, nil\n}\n\nfunc (p *MemoryProvider) addIPListEntry(entry *IPListEntry) error {\n\tif err := entry.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\t_, err := p.ipListEntryExistsInternal(entry)\n\tif err == nil {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"%w: entry %q already exists\", ErrDuplicatedKey, entry.IPOrNet),\n\t\t\tutil.I18nErrorDuplicatedIPNet,\n\t\t)\n\t}\n\tentry.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tentry.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.ipListEntries[entry.getKey()] = entry.getACopy()\n\tp.dbHandle.ipListEntriesKeys = append(p.dbHandle.ipListEntriesKeys, entry.getKey())\n\tsort.Strings(p.dbHandle.ipListEntriesKeys)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) updateIPListEntry(entry *IPListEntry) error {\n\tif err := entry.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\toldEntry, err := p.ipListEntryExistsInternal(entry)\n\tif err != nil {\n\t\treturn err\n\t}\n\tentry.CreatedAt = oldEntry.CreatedAt\n\tentry.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.ipListEntries[entry.getKey()] = entry.getACopy()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) deleteIPListEntry(entry IPListEntry, _ bool) error {\n\tif err := entry.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\t_, err := p.ipListEntryExistsInternal(&entry)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdelete(p.dbHandle.ipListEntries, entry.getKey())\n\tp.dbHandle.ipListEntriesKeys = make([]string, 0, len(p.dbHandle.ipListEntries))\n\tfor k := range p.dbHandle.ipListEntries {\n\t\tp.dbHandle.ipListEntriesKeys = append(p.dbHandle.ipListEntriesKeys, k)\n\t}\n\tsort.Strings(p.dbHandle.ipListEntriesKeys)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tentries := make([]IPListEntry, 0, 15)\n\tif order == OrderASC {\n\t\tfor _, k := range p.dbHandle.ipListEntriesKeys {\n\t\t\te := p.dbHandle.ipListEntries[k]\n\t\t\tif e.Type == listType && e.satisfySearchConstraints(filter, from, order) {\n\t\t\t\tentry := e.getACopy()\n\t\t\t\tentry.PrepareForRendering()\n\t\t\t\tentries = append(entries, entry)\n\t\t\t\tif limit > 0 && len(entries) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := len(p.dbHandle.ipListEntriesKeys) - 1; i >= 0; i-- {\n\t\t\te := p.dbHandle.ipListEntries[p.dbHandle.ipListEntriesKeys[i]]\n\t\t\tif e.Type == listType && e.satisfySearchConstraints(filter, from, order) {\n\t\t\t\tentry := e.getACopy()\n\t\t\t\tentry.PrepareForRendering()\n\t\t\t\tentries = append(entries, entry)\n\t\t\t\tif limit > 0 && len(entries) >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn entries, nil\n}\n\nfunc (p *MemoryProvider) getRecentlyUpdatedIPListEntries(_ int64) ([]IPListEntry, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (p *MemoryProvider) dumpIPListEntries() ([]IPListEntry, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tif count := len(p.dbHandle.ipListEntriesKeys); count > ipListMemoryLimit {\n\t\tproviderLog(logger.LevelInfo, \"IP lists excluded from dump, too many entries: %d\", count)\n\t\treturn nil, nil\n\t}\n\tentries := make([]IPListEntry, 0, len(p.dbHandle.ipListEntries))\n\tfor _, k := range p.dbHandle.ipListEntriesKeys {\n\t\te := p.dbHandle.ipListEntries[k]\n\t\tentry := e.getACopy()\n\t\tentry.PrepareForRendering()\n\t\tentries = append(entries, entry)\n\t}\n\treturn entries, nil\n}\n\nfunc (p *MemoryProvider) countIPListEntries(listType IPListType) (int64, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tif p.dbHandle.isClosed {\n\t\treturn 0, errMemoryProviderClosed\n\t}\n\tif listType == 0 {\n\t\treturn int64(len(p.dbHandle.ipListEntriesKeys)), nil\n\t}\n\tvar count int64\n\tfor _, k := range p.dbHandle.ipListEntriesKeys {\n\t\te := p.dbHandle.ipListEntries[k]\n\t\tif e.Type == listType {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count, nil\n}\n\nfunc (p *MemoryProvider) getListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tif p.dbHandle.isClosed {\n\t\treturn nil, errMemoryProviderClosed\n\t}\n\tentries := make([]IPListEntry, 0, 3)\n\tipAddr, err := netip.ParseAddr(ip)\n\tif err != nil {\n\t\treturn entries, fmt.Errorf(\"invalid ip address %s\", ip)\n\t}\n\tvar netType int\n\tvar ipBytes []byte\n\tif ipAddr.Is4() || ipAddr.Is4In6() {\n\t\tnetType = ipTypeV4\n\t\tas4 := ipAddr.As4()\n\t\tipBytes = as4[:]\n\t} else {\n\t\tnetType = ipTypeV6\n\t\tas16 := ipAddr.As16()\n\t\tipBytes = as16[:]\n\t}\n\tfor _, k := range p.dbHandle.ipListEntriesKeys {\n\t\te := p.dbHandle.ipListEntries[k]\n\t\tif e.Type == listType && e.IPType == netType && bytes.Compare(ipBytes, e.First) >= 0 && bytes.Compare(ipBytes, e.Last) <= 0 {\n\t\t\tentry := e.getACopy()\n\t\t\tentry.PrepareForRendering()\n\t\t\tentries = append(entries, entry)\n\t\t}\n\t}\n\treturn entries, nil\n}\n\nfunc (p *MemoryProvider) getConfigs() (Configs, error) {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn Configs{}, errMemoryProviderClosed\n\t}\n\treturn p.dbHandle.configs.getACopy(), nil\n}\n\nfunc (p *MemoryProvider) setConfigs(configs *Configs) error {\n\tif err := configs.validate(); err != nil {\n\t\treturn err\n\t}\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tp.dbHandle.configs = configs.getACopy()\n\treturn nil\n}\n\nfunc (p *MemoryProvider) setFirstDownloadTimestamp(username string) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif user.FirstDownload > 0 {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"first download already set to %s\",\n\t\t\tutil.GetTimeFromMsecSinceEpoch(user.FirstDownload)))\n\t}\n\tuser.FirstDownload = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.users[user.Username] = user\n\treturn nil\n}\n\nfunc (p *MemoryProvider) setFirstUploadTimestamp(username string) error {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\tif p.dbHandle.isClosed {\n\t\treturn errMemoryProviderClosed\n\t}\n\tuser, err := p.userExistsInternal(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif user.FirstUpload > 0 {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"first upload already set to %s\",\n\t\t\tutil.GetTimeFromMsecSinceEpoch(user.FirstUpload)))\n\t}\n\tuser.FirstUpload = util.GetTimeAsMsSinceEpoch(time.Now())\n\tp.dbHandle.users[user.Username] = user\n\treturn nil\n}\n\nfunc (p *MemoryProvider) getNextID() int64 {\n\tnextID := int64(1)\n\tfor _, v := range p.dbHandle.users {\n\t\tif v.ID >= nextID {\n\t\t\tnextID = v.ID + 1\n\t\t}\n\t}\n\treturn nextID\n}\n\nfunc (p *MemoryProvider) getNextFolderID() int64 {\n\tnextID := int64(1)\n\tfor _, v := range p.dbHandle.vfolders {\n\t\tif v.ID >= nextID {\n\t\t\tnextID = v.ID + 1\n\t\t}\n\t}\n\treturn nextID\n}\n\nfunc (p *MemoryProvider) getNextAdminID() int64 {\n\tnextID := int64(1)\n\tfor _, a := range p.dbHandle.admins {\n\t\tif a.ID >= nextID {\n\t\t\tnextID = a.ID + 1\n\t\t}\n\t}\n\treturn nextID\n}\n\nfunc (p *MemoryProvider) getNextGroupID() int64 {\n\tnextID := int64(1)\n\tfor _, g := range p.dbHandle.groups {\n\t\tif g.ID >= nextID {\n\t\t\tnextID = g.ID + 1\n\t\t}\n\t}\n\treturn nextID\n}\n\nfunc (p *MemoryProvider) getNextActionID() int64 {\n\tnextID := int64(1)\n\tfor _, a := range p.dbHandle.actions {\n\t\tif a.ID >= nextID {\n\t\t\tnextID = a.ID + 1\n\t\t}\n\t}\n\treturn nextID\n}\n\nfunc (p *MemoryProvider) getNextRuleID() int64 {\n\tnextID := int64(1)\n\tfor _, r := range p.dbHandle.rules {\n\t\tif r.ID >= nextID {\n\t\t\tnextID = r.ID + 1\n\t\t}\n\t}\n\treturn nextID\n}\n\nfunc (p *MemoryProvider) getNextRoleID() int64 {\n\tnextID := int64(1)\n\tfor _, r := range p.dbHandle.roles {\n\t\tif r.ID >= nextID {\n\t\t\tnextID = r.ID + 1\n\t\t}\n\t}\n\treturn nextID\n}\n\nfunc (p *MemoryProvider) clear() {\n\tp.dbHandle.Lock()\n\tdefer p.dbHandle.Unlock()\n\n\tp.dbHandle.usernames = []string{}\n\tp.dbHandle.users = make(map[string]User)\n\tp.dbHandle.groupnames = []string{}\n\tp.dbHandle.groups = map[string]Group{}\n\tp.dbHandle.vfoldersNames = []string{}\n\tp.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder)\n\tp.dbHandle.admins = make(map[string]Admin)\n\tp.dbHandle.adminsUsernames = []string{}\n\tp.dbHandle.apiKeys = make(map[string]APIKey)\n\tp.dbHandle.apiKeysIDs = []string{}\n\tp.dbHandle.shares = make(map[string]Share)\n\tp.dbHandle.sharesIDs = []string{}\n\tp.dbHandle.actions = map[string]BaseEventAction{}\n\tp.dbHandle.actionsNames = []string{}\n\tp.dbHandle.rules = map[string]EventRule{}\n\tp.dbHandle.rulesNames = []string{}\n\tp.dbHandle.roles = map[string]Role{}\n\tp.dbHandle.roleNames = []string{}\n\tp.dbHandle.ipListEntries = map[string]IPListEntry{}\n\tp.dbHandle.ipListEntriesKeys = []string{}\n\tp.dbHandle.configs = Configs{}\n}\n\nfunc (p *MemoryProvider) reloadConfig() error {\n\tif p.dbHandle.configFile == \"\" {\n\t\tproviderLog(logger.LevelDebug, \"no dump configuration file defined\")\n\t\treturn nil\n\t}\n\tproviderLog(logger.LevelDebug, \"loading dump from file: %q\", p.dbHandle.configFile)\n\tfi, err := os.Stat(p.dbHandle.configFile)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error loading dump: %v\", err)\n\t\treturn err\n\t}\n\tif fi.Size() == 0 {\n\t\terr = errors.New(\"dump configuration file is invalid, its size must be > 0\")\n\t\tproviderLog(logger.LevelError, \"error loading dump: %v\", err)\n\t\treturn err\n\t}\n\tif fi.Size() > 20971520 {\n\t\terr = errors.New(\"dump configuration file is invalid, its size must be <= 20971520 bytes\")\n\t\tproviderLog(logger.LevelError, \"error loading dump: %v\", err)\n\t\treturn err\n\t}\n\tcontent, err := os.ReadFile(p.dbHandle.configFile)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error loading dump: %v\", err)\n\t\treturn err\n\t}\n\tdump, err := ParseDumpData(content)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error loading dump: %v\", err)\n\t\treturn err\n\t}\n\treturn p.restoreDump(&dump)\n}\n\nfunc (p *MemoryProvider) restoreDump(dump *BackupData) error {\n\tp.clear()\n\n\tif err := p.restoreConfigs(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreIPListEntries(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreRoles(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreFolders(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreGroups(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreUsers(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreAdmins(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreAPIKeys(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreShares(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreEventActions(dump); err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.restoreEventRules(dump); err != nil {\n\t\treturn err\n\t}\n\n\tproviderLog(logger.LevelDebug, \"config loaded from file: %q\", p.dbHandle.configFile)\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreEventActions(dump *BackupData) error {\n\tfor idx := range dump.EventActions {\n\t\taction := dump.EventActions[idx]\n\t\ta, err := p.eventActionExists(action.Name)\n\t\tif err == nil {\n\t\t\taction.ID = a.ID\n\t\t\terr = UpdateEventAction(&action, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating event action %q: %v\", action.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr = AddEventAction(&action, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding event action %q: %v\", action.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreEventRules(dump *BackupData) error {\n\tfor idx := range dump.EventRules {\n\t\trule := dump.EventRules[idx]\n\t\tr, err := p.eventRuleExists(rule.Name)\n\t\tif dump.Version < 15 {\n\t\t\trule.Status = 1\n\t\t}\n\t\tif err == nil {\n\t\t\trule.ID = r.ID\n\t\t\terr = UpdateEventRule(&rule, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating event rule %q: %v\", rule.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr = AddEventRule(&rule, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding event rule %q: %v\", rule.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreShares(dump *BackupData) error {\n\tfor idx := range dump.Shares {\n\t\tshare := dump.Shares[idx]\n\t\ts, err := p.shareExists(share.ShareID, \"\")\n\t\tshare.IsRestore = true\n\t\tif err == nil {\n\t\t\tshare.ID = s.ID\n\t\t\terr = UpdateShare(&share, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating share %q: %v\", share.ShareID, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr = AddShare(&share, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding share %q: %v\", share.ShareID, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreAPIKeys(dump *BackupData) error {\n\tfor idx := range dump.APIKeys {\n\t\tapiKey := dump.APIKeys[idx]\n\t\tif apiKey.Key == \"\" {\n\t\t\treturn fmt.Errorf(\"cannot restore an empty API key: %+v\", apiKey)\n\t\t}\n\t\tk, err := p.apiKeyExists(apiKey.KeyID)\n\t\tif err == nil {\n\t\t\tapiKey.ID = k.ID\n\t\t\terr = UpdateAPIKey(&apiKey, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating API key %q: %v\", apiKey.KeyID, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr = AddAPIKey(&apiKey, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding API key %q: %v\", apiKey.KeyID, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreAdmins(dump *BackupData) error {\n\tfor idx := range dump.Admins {\n\t\tadmin := dump.Admins[idx]\n\t\tadmin.Username = config.convertName(admin.Username)\n\t\ta, err := p.adminExists(admin.Username)\n\t\tif err == nil {\n\t\t\tadmin.ID = a.ID\n\t\t\terr = UpdateAdmin(&admin, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating admin %q: %v\", admin.Username, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr = AddAdmin(&admin, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding admin %q: %v\", admin.Username, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreConfigs(dump *BackupData) error {\n\tif dump.Configs != nil && dump.Configs.UpdatedAt > 0 {\n\t\treturn UpdateConfigs(dump.Configs, ActionExecutorSystem, \"\", \"\")\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreIPListEntries(dump *BackupData) error {\n\tfor idx := range dump.IPLists {\n\t\tentry := dump.IPLists[idx]\n\t\t_, err := p.ipListEntryExists(entry.IPOrNet, entry.Type)\n\t\tif err == nil {\n\t\t\terr = UpdateIPListEntry(&entry, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating IP list entry %q: %v\", entry.getName(), err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr = AddIPListEntry(&entry, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding IP list entry %q: %v\", entry.getName(), err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreRoles(dump *BackupData) error {\n\tfor idx := range dump.Roles {\n\t\trole := dump.Roles[idx]\n\t\trole.Name = config.convertName(role.Name)\n\t\tr, err := p.roleExists(role.Name)\n\t\tif err == nil {\n\t\t\trole.ID = r.ID\n\t\t\terr = UpdateRole(&role, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating role %q: %v\", role.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\trole.Admins = nil\n\t\t\trole.Users = nil\n\t\t\terr = AddRole(&role, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding role %q: %v\", role.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreGroups(dump *BackupData) error {\n\tfor idx := range dump.Groups {\n\t\tgroup := dump.Groups[idx]\n\t\tgroup.Name = config.convertName(group.Name)\n\t\tg, err := p.groupExists(group.Name)\n\t\tif err == nil {\n\t\t\tgroup.ID = g.ID\n\t\t\terr = UpdateGroup(&group, g.Users, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating group %q: %v\", group.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tgroup.Users = nil\n\t\t\terr = AddGroup(&group, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding group %q: %v\", group.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreFolders(dump *BackupData) error {\n\tfor idx := range dump.Folders {\n\t\tfolder := dump.Folders[idx]\n\t\tfolder.Name = config.convertName(folder.Name)\n\t\tf, err := p.getFolderByName(folder.Name)\n\t\tif err == nil {\n\t\t\tfolder.ID = f.ID\n\t\t\terr = UpdateFolder(&folder, f.Users, f.Groups, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating folder %q: %v\", folder.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tfolder.Users = nil\n\t\t\terr = AddFolder(&folder, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding folder %q: %v\", folder.Name, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *MemoryProvider) restoreUsers(dump *BackupData) error {\n\tfor idx := range dump.Users {\n\t\tuser := dump.Users[idx]\n\t\tuser.Username = config.convertName(user.Username)\n\t\tu, err := p.userExists(user.Username, \"\")\n\t\tif err == nil {\n\t\t\tuser.ID = u.ID\n\t\t\terr = UpdateUser(&user, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error updating user %q: %v\", user.Username, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\terr = AddUser(&user, ActionExecutorSystem, \"\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"error adding user %q: %v\", user.Username, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// initializeDatabase does nothing, no initilization is needed for memory provider\nfunc (p *MemoryProvider) initializeDatabase() error {\n\treturn ErrNoInitRequired\n}\n\nfunc (p *MemoryProvider) migrateDatabase() error {\n\treturn ErrNoInitRequired\n}\n\nfunc (p *MemoryProvider) revertDatabase(_ int) error {\n\treturn errors.New(\"memory provider does not store data, revert not possible\")\n}\n\nfunc (p *MemoryProvider) resetDatabase() error {\n\treturn errors.New(\"memory provider does not store data, reset not possible\")\n}\n"
  },
  {
    "path": "internal/dataprovider/mysql.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !nomysql\n\npackage dataprovider\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tmysqlResetSQL = \"DROP TABLE IF EXISTS `{{api_keys}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{users_folders_mapping}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{users_groups_mapping}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{admins_groups_mapping}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{groups_folders_mapping}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{shares_groups_mapping}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{admins}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{folders}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{shares}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{users}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{groups}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{defender_events}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{defender_hosts}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{active_transfers}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{shared_sessions}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{rules_actions_mapping}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{events_actions}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{events_rules}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{tasks}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{nodes}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{roles}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{ip_lists}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{configs}}` CASCADE;\" +\n\t\t\"DROP TABLE IF EXISTS `{{schema_version}}` CASCADE;\"\n\tmysqlInitialSQL = \"CREATE TABLE `{{schema_version}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);\" +\n\t\t\"CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, \" +\n\t\t\"`description` varchar(512) NULL, `password` varchar(255) NOT NULL, `email` varchar(255) NULL, `status` integer NOT NULL, \" +\n\t\t\"`permissions` longtext NOT NULL, `filters` longtext NULL, `additional_info` longtext NULL, `last_login` bigint NOT NULL, \" +\n\t\t\"`role_id` integer NULL, `created_at` bigint NOT NULL, `updated_at` bigint NOT NULL);\" +\n\t\t\"CREATE TABLE `{{active_transfers}}` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`connection_id` varchar(100) NOT NULL, `transfer_id` bigint NOT NULL, `transfer_type` integer NOT NULL, \" +\n\t\t\"`username` varchar(255) NOT NULL, `folder_name` varchar(255) NULL, `ip` varchar(50) NOT NULL, \" +\n\t\t\"`truncated_size` bigint NOT NULL, `current_ul_size` bigint NOT NULL, `current_dl_size` bigint NOT NULL, \" +\n\t\t\"`created_at` bigint NOT NULL, `updated_at` bigint NOT NULL);\" +\n\t\t\"CREATE TABLE `{{defender_hosts}}` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`ip` varchar(50) NOT NULL UNIQUE, `ban_time` bigint NOT NULL, `updated_at` bigint NOT NULL);\" +\n\t\t\"CREATE TABLE `{{defender_events}}` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`date_time` bigint NOT NULL, `score` integer NOT NULL, `host_id` bigint NOT NULL);\" +\n\t\t\"ALTER TABLE `{{defender_events}}` ADD CONSTRAINT `{{prefix}}defender_events_host_id_fk_defender_hosts_id` \" +\n\t\t\"FOREIGN KEY (`host_id`) REFERENCES `{{defender_hosts}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"CREATE TABLE `{{folders}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL UNIQUE, \" +\n\t\t\"`description` varchar(512) NULL, `path` longtext NULL, `used_quota_size` bigint NOT NULL, \" +\n\t\t\"`used_quota_files` integer NOT NULL, `last_quota_update` bigint NOT NULL, `filesystem` longtext NULL);\" +\n\t\t\"CREATE TABLE `{{groups}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`name` varchar(255) NOT NULL UNIQUE, `description` varchar(512) NULL, `created_at` bigint NOT NULL, \" +\n\t\t\"`updated_at` bigint NOT NULL, `user_settings` longtext NULL);\" +\n\t\t\"CREATE TABLE `{{shared_sessions}}` (`key` varchar(128) NOT NULL, `type` integer NOT NULL, `data` longtext NOT NULL, \" +\n\t\t\"`timestamp` bigint NOT NULL, PRIMARY KEY (`key`, `type`));\" +\n\t\t\"CREATE TABLE `{{users}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, \" +\n\t\t\"`status` integer NOT NULL, `expiration_date` bigint NOT NULL, `description` varchar(512) NULL, `password` longtext NULL, \" +\n\t\t\"`public_keys` longtext NULL, `home_dir` longtext NOT NULL, `uid` bigint NOT NULL, `gid` bigint NOT NULL, \" +\n\t\t\"`max_sessions` integer NOT NULL, `quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, \" +\n\t\t\"`permissions` longtext NOT NULL, `used_quota_size` bigint NOT NULL, `used_quota_files` integer NOT NULL, \" +\n\t\t\"`last_quota_update` bigint NOT NULL, `upload_bandwidth` integer NOT NULL, `download_bandwidth` integer NOT NULL, \" +\n\t\t\"`last_login` bigint NOT NULL, `filters` longtext NULL, `filesystem` longtext NULL, `additional_info` longtext NULL, \" +\n\t\t\"`created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, `email` varchar(255) NULL, \" +\n\t\t\"`upload_data_transfer` integer NOT NULL, `download_data_transfer` integer NOT NULL, \" +\n\t\t\"`total_data_transfer` integer NOT NULL, `used_upload_data_transfer` bigint NOT NULL, \" +\n\t\t\"`used_download_data_transfer` bigint NOT NULL, `deleted_at` bigint NOT NULL, `first_download` bigint NOT NULL, \" +\n\t\t\"`first_upload` bigint NOT NULL, `last_password_change` bigint NOT NULL, `role_id` integer NULL);\" +\n\t\t\"CREATE TABLE `{{groups_folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`group_id` integer NOT NULL, `folder_id` integer NOT NULL, \" +\n\t\t\"`virtual_path` longtext NOT NULL, `quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `sort_order` integer NOT NULL);\" +\n\t\t\"CREATE TABLE `{{users_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`user_id` integer NOT NULL, `group_id` integer NOT NULL, `group_type` integer NOT NULL, `sort_order` integer NOT NULL);\" +\n\t\t\"CREATE TABLE `{{users_folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `virtual_path` longtext NOT NULL, \" +\n\t\t\"`quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `folder_id` integer NOT NULL, `user_id` integer NOT NULL, `sort_order` integer NOT NULL);\" +\n\t\t\"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_user_folder_mapping` \" +\n\t\t\"UNIQUE (`user_id`, `folder_id`);\" +\n\t\t\"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}users_folders_mapping_user_id_fk_users_id` \" +\n\t\t\"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}users_folders_mapping_folder_id_fk_folders_id` \" +\n\t\t\"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"CREATE INDEX `{{prefix}}users_folders_mapping_sort_order_idx` ON `{{users_folders_mapping}}` (`sort_order`);\" +\n\t\t\"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}unique_user_group_mapping` UNIQUE (`user_id`, `group_id`);\" +\n\t\t\"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_group_folder_mapping` UNIQUE (`group_id`, `folder_id`);\" +\n\t\t\"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}users_groups_mapping_group_id_fk_groups_id` \" +\n\t\t\"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE NO ACTION;\" +\n\t\t\"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}users_groups_mapping_user_id_fk_users_id` \" +\n\t\t\"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE; \" +\n\t\t\"CREATE INDEX `{{prefix}}users_groups_mapping_sort_order_idx` ON `{{users_groups_mapping}}` (`sort_order`);\" +\n\t\t\"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}groups_folders_mapping_folder_id_fk_folders_id` \" +\n\t\t\"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}groups_folders_mapping_group_id_fk_groups_id` \" +\n\t\t\"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"CREATE INDEX `{{prefix}}groups_folders_mapping_sort_order_idx` ON `{{groups_folders_mapping}}` (`sort_order`); \" +\n\t\t\"CREATE TABLE `{{shares}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`share_id` varchar(60) NOT NULL UNIQUE, `name` varchar(255) NOT NULL, `description` varchar(512) NULL, \" +\n\t\t\"`scope` integer NOT NULL, `paths` longtext NOT NULL, `created_at` bigint NOT NULL, \" +\n\t\t\"`updated_at` bigint NOT NULL, `last_use_at` bigint NOT NULL, `expires_at` bigint NOT NULL, \" +\n\t\t\"`password` longtext NULL, `max_tokens` integer NOT NULL, `used_tokens` integer NOT NULL, \" +\n\t\t\"`allow_from` longtext NULL, `options` longtext NULL, `user_id` integer NOT NULL);\" +\n\t\t\"ALTER TABLE `{{shares}}` ADD CONSTRAINT `{{prefix}}shares_user_id_fk_users_id` \" +\n\t\t\"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"CREATE TABLE `{{api_keys}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL, `key_id` varchar(50) NOT NULL UNIQUE,\" +\n\t\t\"`api_key` varchar(255) NOT NULL UNIQUE, `scope` integer NOT NULL, `created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, `last_use_at` bigint NOT NULL, \" +\n\t\t\"`expires_at` bigint NOT NULL, `description` longtext NULL, `admin_id` integer NULL, `user_id` integer NULL);\" +\n\t\t\"ALTER TABLE `{{api_keys}}` ADD CONSTRAINT `{{prefix}}api_keys_admin_id_fk_admins_id` FOREIGN KEY (`admin_id`) REFERENCES `{{admins}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"ALTER TABLE `{{api_keys}}` ADD CONSTRAINT `{{prefix}}api_keys_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"CREATE TABLE `{{events_rules}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`name` varchar(255) NOT NULL UNIQUE, `status` integer NOT NULL, `description` varchar(512) NULL, `created_at` bigint NOT NULL, \" +\n\t\t\"`updated_at` bigint NOT NULL, `trigger` integer NOT NULL, `conditions` longtext NOT NULL, `deleted_at` bigint NOT NULL);\" +\n\t\t\"CREATE TABLE `{{events_actions}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`name` varchar(255) NOT NULL UNIQUE, `description` varchar(512) NULL, `type` integer NOT NULL, \" +\n\t\t\"`options` longtext NOT NULL);\" +\n\t\t\"CREATE TABLE `{{rules_actions_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`rule_id` integer NOT NULL, `action_id` integer NOT NULL, `order` integer NOT NULL, `options` longtext NOT NULL);\" +\n\t\t\"CREATE TABLE `{{tasks}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL UNIQUE, \" +\n\t\t\"`updated_at` bigint NOT NULL, `version` bigint NOT NULL);\" +\n\t\t\"ALTER TABLE `{{rules_actions_mapping}}` ADD CONSTRAINT `{{prefix}}unique_rule_action_mapping` UNIQUE (`rule_id`, `action_id`);\" +\n\t\t\"ALTER TABLE `{{rules_actions_mapping}}` ADD CONSTRAINT `{{prefix}}rules_actions_mapping_rule_id_fk_events_rules_id` \" +\n\t\t\"FOREIGN KEY (`rule_id`) REFERENCES `{{events_rules}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"ALTER TABLE `{{rules_actions_mapping}}` ADD CONSTRAINT `{{prefix}}rules_actions_mapping_action_id_fk_events_targets_id` \" +\n\t\t\"FOREIGN KEY (`action_id`) REFERENCES `{{events_actions}}` (`id`) ON DELETE NO ACTION;\" +\n\t\t\"CREATE TABLE `{{admins_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\" `admin_id` integer NOT NULL, `group_id` integer NOT NULL, `options` longtext NOT NULL, `sort_order` integer NOT NULL);\" +\n\t\t\"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}unique_admin_group_mapping` \" +\n\t\t\"UNIQUE (`admin_id`, `group_id`);\" +\n\t\t\"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}admins_groups_mapping_admin_id_fk_admins_id` \" +\n\t\t\"FOREIGN KEY (`admin_id`) REFERENCES `{{admins}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}admins_groups_mapping_group_id_fk_groups_id` \" +\n\t\t\"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE;\" +\n\t\t\"CREATE INDEX `{{prefix}}admins_groups_mapping_sort_order_idx` ON `{{admins_groups_mapping}}` (`sort_order`); \" +\n\t\t\"CREATE TABLE `{{nodes}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, \" +\n\t\t\"`name` varchar(255) NOT NULL UNIQUE, `data` longtext NOT NULL, `created_at` bigint NOT NULL, \" +\n\t\t\"`updated_at` bigint NOT NULL);\" +\n\t\t\"CREATE TABLE `{{roles}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL UNIQUE, \" +\n\t\t\"`description` varchar(512) NULL, `created_at` bigint NOT NULL, `updated_at` bigint NOT NULL);\" +\n\t\t\"ALTER TABLE `{{admins}}` ADD CONSTRAINT `{{prefix}}admins_role_id_fk_roles_id` FOREIGN KEY (`role_id`) \" +\n\t\t\"REFERENCES `{{roles}}`(`id`) ON DELETE NO ACTION;\" +\n\t\t\"ALTER TABLE `{{users}}` ADD CONSTRAINT `{{prefix}}users_role_id_fk_roles_id` FOREIGN KEY (`role_id`) \" +\n\t\t\"REFERENCES `{{roles}}`(`id`) ON DELETE SET NULL;\" +\n\t\t\"CREATE TABLE `{{ip_lists}}` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `type` integer NOT NULL, \" +\n\t\t\"`ipornet` varchar(50) NOT NULL, `mode` integer NOT NULL, `description` varchar(512) NULL, \" +\n\t\t\"`first` VARBINARY(16) NOT NULL, `last` VARBINARY(16) NOT NULL, `ip_type` integer NOT NULL, `protocols` integer NOT NULL, \" +\n\t\t\"`created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, `deleted_at` bigint NOT NULL);\" +\n\t\t\"ALTER TABLE `{{ip_lists}}` ADD CONSTRAINT `{{prefix}}unique_ipornet_type_mapping` UNIQUE (`type`, `ipornet`);\" +\n\t\t\"CREATE TABLE `{{configs}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `configs` longtext NOT NULL);\" +\n\t\t\"INSERT INTO {{configs}} (configs) VALUES ('{}');\" +\n\t\t\"CREATE INDEX `{{prefix}}users_updated_at_idx` ON `{{users}}` (`updated_at`);\" +\n\t\t\"CREATE INDEX `{{prefix}}users_deleted_at_idx` ON `{{users}}` (`deleted_at`);\" +\n\t\t\"CREATE INDEX `{{prefix}}defender_hosts_updated_at_idx` ON `{{defender_hosts}}` (`updated_at`);\" +\n\t\t\"CREATE INDEX `{{prefix}}defender_hosts_ban_time_idx` ON `{{defender_hosts}}` (`ban_time`);\" +\n\t\t\"CREATE INDEX `{{prefix}}defender_events_date_time_idx` ON `{{defender_events}}` (`date_time`);\" +\n\t\t\"CREATE INDEX `{{prefix}}active_transfers_connection_id_idx` ON `{{active_transfers}}` (`connection_id`);\" +\n\t\t\"CREATE INDEX `{{prefix}}active_transfers_transfer_id_idx` ON `{{active_transfers}}` (`transfer_id`);\" +\n\t\t\"CREATE INDEX `{{prefix}}active_transfers_updated_at_idx` ON `{{active_transfers}}` (`updated_at`);\" +\n\t\t\"CREATE INDEX `{{prefix}}shared_sessions_type_idx` ON `{{shared_sessions}}` (`type`);\" +\n\t\t\"CREATE INDEX `{{prefix}}shared_sessions_timestamp_idx` ON `{{shared_sessions}}` (`timestamp`);\" +\n\t\t\"CREATE INDEX `{{prefix}}events_rules_updated_at_idx` ON `{{events_rules}}` (`updated_at`);\" +\n\t\t\"CREATE INDEX `{{prefix}}events_rules_deleted_at_idx` ON `{{events_rules}}` (`deleted_at`);\" +\n\t\t\"CREATE INDEX `{{prefix}}events_rules_trigger_idx` ON `{{events_rules}}` (`trigger`);\" +\n\t\t\"CREATE INDEX `{{prefix}}rules_actions_mapping_order_idx` ON `{{rules_actions_mapping}}` (`order`);\" +\n\t\t\"CREATE INDEX `{{prefix}}ip_lists_type_idx` ON `{{ip_lists}}` (`type`);\" +\n\t\t\"CREATE INDEX `{{prefix}}ip_lists_ipornet_idx` ON `{{ip_lists}}` (`ipornet`);\" +\n\t\t\"CREATE INDEX `{{prefix}}ip_lists_ip_type_idx` ON `{{ip_lists}}` (`ip_type`);\" +\n\t\t\"CREATE INDEX `{{prefix}}ip_lists_updated_at_idx` ON `{{ip_lists}}` (`updated_at`);\" +\n\t\t\"CREATE INDEX `{{prefix}}ip_lists_deleted_at_idx` ON `{{ip_lists}}` (`deleted_at`);\" +\n\t\t\"CREATE INDEX `{{prefix}}ip_lists_first_last_idx` ON `{{ip_lists}}` (`first`, `last`);\" +\n\t\t\"INSERT INTO {{schema_version}} (version) VALUES (33);\"\n\tmysqlV34SQL = \"CREATE TABLE `{{shares_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,\" +\n\t\t\"`share_id` integer NOT NULL, `group_id` integer NOT NULL, `permissions` integer NOT NULL,\" +\n\t\t\"`sort_order` integer NOT NULL,\" +\n\t\t\"CONSTRAINT `{{prefix}}unique_share_group_mapping` UNIQUE (`share_id`, `group_id`),\" +\n\t\t\"CONSTRAINT `{{prefix}}shares_groups_mapping_share_id_fk` FOREIGN KEY (`share_id`) REFERENCES `{{shares}}` (`id`) ON DELETE CASCADE,\" +\n\t\t\"CONSTRAINT `{{prefix}}shares_groups_mapping_group_id_fk` FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE); \" +\n\t\t\"CREATE INDEX `{{prefix}}shares_groups_mapping_sort_order_idx` ON `{{shares_groups_mapping}}` (`sort_order`); \" +\n\t\t\"CREATE INDEX `{{prefix}}shares_groups_mapping_share_id_idx` ON `{{shares_groups_mapping}}` (`share_id`); \" +\n\t\t\"CREATE INDEX `{{prefix}}shares_groups_mapping_group_id_idx` ON `{{shares_groups_mapping}}` (`group_id`);\"\n\tmysqlV34DownSQL = \"DROP TABLE IF EXISTS `{{shares_groups_mapping}}`;\"\n)\n\n// MySQLProvider defines the auth provider for MySQL/MariaDB database\ntype MySQLProvider struct {\n\tdbHandle *sql.DB\n}\n\nfunc init() {\n\tversion.AddFeature(\"+mysql\")\n}\n\nfunc initializeMySQLProvider() error {\n\tconnString, err := getMySQLConnectionString(false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tredactedConnString, err := getMySQLConnectionString(true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdbHandle, err := sql.Open(\"mysql\", connString)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error creating mysql database handler, connection string: %q, error: %v\",\n\t\t\tredactedConnString, err)\n\t\treturn err\n\t}\n\tproviderLog(logger.LevelDebug, \"mysql database handle created, connection string: %q, pool size: %v\",\n\t\tredactedConnString, config.PoolSize)\n\tdbHandle.SetMaxOpenConns(config.PoolSize)\n\tif config.PoolSize > 0 {\n\t\tdbHandle.SetMaxIdleConns(config.PoolSize)\n\t} else {\n\t\tdbHandle.SetMaxIdleConns(2)\n\t}\n\tdbHandle.SetConnMaxLifetime(240 * time.Second)\n\tdbHandle.SetConnMaxIdleTime(120 * time.Second)\n\tprovider = &MySQLProvider{dbHandle: dbHandle}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn dbHandle.PingContext(ctx)\n}\nfunc getMySQLConnectionString(redactedPwd bool) (string, error) {\n\tvar connectionString string\n\tif config.ConnectionString == \"\" {\n\t\tpassword := config.Password\n\t\tif redactedPwd && password != \"\" {\n\t\t\tpassword = \"[redacted]\"\n\t\t}\n\t\tsslMode := getSSLMode()\n\t\tif sslMode == \"custom\" && !redactedPwd {\n\t\t\tif err := registerMySQLCustomTLSConfig(); err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\t\tconnectionString = fmt.Sprintf(\"%s:%s@tcp([%s]:%d)/%s?collation=utf8mb4_unicode_ci&interpolateParams=true&timeout=10s&parseTime=true&clientFoundRows=true&tls=%s&writeTimeout=60s&readTimeout=60s\",\n\t\t\tconfig.Username, password, config.Host, config.Port, config.Name, sslMode)\n\t} else {\n\t\tconnectionString = config.ConnectionString\n\t}\n\treturn connectionString, nil\n}\n\nfunc registerMySQLCustomTLSConfig() error {\n\ttlsConfig := &tls.Config{}\n\tif config.RootCert != \"\" {\n\t\trootCAs, err := x509.SystemCertPool()\n\t\tif err != nil {\n\t\t\trootCAs = x509.NewCertPool()\n\t\t}\n\t\trootCrt, err := os.ReadFile(config.RootCert)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to load root certificate %q: %v\", config.RootCert, err)\n\t\t}\n\t\tif !rootCAs.AppendCertsFromPEM(rootCrt) {\n\t\t\treturn fmt.Errorf(\"unable to parse root certificate %q\", config.RootCert)\n\t\t}\n\t\ttlsConfig.RootCAs = rootCAs\n\t}\n\tif config.ClientCert != \"\" && config.ClientKey != \"\" {\n\t\tclientCert := make([]tls.Certificate, 0, 1)\n\t\ttlsCert, err := tls.LoadX509KeyPair(config.ClientCert, config.ClientKey)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to load key pair %q, %q: %v\", config.ClientCert, config.ClientKey, err)\n\t\t}\n\t\tclientCert = append(clientCert, tlsCert)\n\t\ttlsConfig.Certificates = clientCert\n\t}\n\tif config.SSLMode == 2 || config.SSLMode == 3 {\n\t\ttlsConfig.InsecureSkipVerify = true\n\t}\n\tif !filepath.IsAbs(config.Host) && !config.DisableSNI {\n\t\ttlsConfig.ServerName = config.Host\n\t}\n\tproviderLog(logger.LevelInfo, \"registering custom TLS config, root cert %q, client cert %q, client key %q, disable SNI? %v\",\n\t\tconfig.RootCert, config.ClientCert, config.ClientKey, config.DisableSNI)\n\tif err := mysql.RegisterTLSConfig(\"custom\", tlsConfig); err != nil {\n\t\treturn fmt.Errorf(\"unable to register tls config: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (p *MySQLProvider) checkAvailability() error {\n\treturn sqlCommonCheckAvailability(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {\n\treturn sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error) {\n\treturn sqlCommonValidateUserAndTLSCertificate(username, protocol, tlsCert, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) validateUserAndPubKey(username string, publicKey []byte, isSSHCert bool) (User, string, error) {\n\treturn sqlCommonValidateUserAndPubKey(username, publicKey, isSSHCert, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error {\n\treturn sqlCommonUpdateTransferQuota(username, uploadSize, downloadSize, reset, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {\n\treturn sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getUsedQuota(username string) (int, int64, int64, int64, error) {\n\treturn sqlCommonGetUsedQuota(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getAdminSignature(username string) (string, error) {\n\treturn sqlCommonGetAdminSignature(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getUserSignature(username string) (string, error) {\n\treturn sqlCommonGetUserSignature(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) setUpdatedAt(username string) {\n\tsqlCommonSetUpdatedAt(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateLastLogin(username string) error {\n\treturn sqlCommonUpdateLastLogin(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateAdminLastLogin(username string) error {\n\treturn sqlCommonUpdateAdminLastLogin(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) userExists(username, role string) (User, error) {\n\treturn sqlCommonGetUserByUsername(username, role, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addUser(user *User) error {\n\treturn p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername)\n}\n\nfunc (p *MySQLProvider) updateUser(user *User) error {\n\treturn p.normalizeError(sqlCommonUpdateUser(user, p.dbHandle), -1)\n}\n\nfunc (p *MySQLProvider) deleteUser(user User, softDelete bool) error {\n\treturn sqlCommonDeleteUser(user, softDelete, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateUserPassword(username, password string) error {\n\treturn sqlCommonUpdateUserPassword(username, password, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpUsers() ([]User, error) {\n\treturn sqlCommonDumpUsers(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getRecentlyUpdatedUsers(after int64) ([]User, error) {\n\treturn sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {\n\treturn sqlCommonGetUsers(limit, offset, order, role, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {\n\treturn sqlCommonGetUsersForQuotaCheck(toFetch, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {\n\treturn sqlCommonDumpFolders(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error) {\n\treturn sqlCommonGetFolders(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\treturn sqlCommonGetFolderByName(ctx, name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {\n\treturn p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName)\n}\n\nfunc (p *MySQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {\n\treturn sqlCommonUpdateFolder(folder, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) deleteFolder(folder vfs.BaseVirtualFolder) error {\n\treturn sqlCommonDeleteFolder(folder, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {\n\treturn sqlCommonUpdateFolderQuota(name, filesAdd, sizeAdd, reset, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getUsedFolderQuota(name string) (int, int64, error) {\n\treturn sqlCommonGetFolderUsedQuota(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getGroups(limit, offset int, order string, minimal bool) ([]Group, error) {\n\treturn sqlCommonGetGroups(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getGroupsWithNames(names []string) ([]Group, error) {\n\treturn sqlCommonGetGroupsWithNames(names, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getUsersInGroups(names []string) ([]string, error) {\n\treturn sqlCommonGetUsersInGroups(names, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) groupExists(name string) (Group, error) {\n\treturn sqlCommonGetGroupByName(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addGroup(group *Group) error {\n\treturn p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName)\n}\n\nfunc (p *MySQLProvider) updateGroup(group *Group) error {\n\treturn sqlCommonUpdateGroup(group, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) deleteGroup(group Group) error {\n\treturn sqlCommonDeleteGroup(group, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpGroups() ([]Group, error) {\n\treturn sqlCommonDumpGroups(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) adminExists(username string) (Admin, error) {\n\treturn sqlCommonGetAdminByUsername(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addAdmin(admin *Admin) error {\n\treturn p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername)\n}\n\nfunc (p *MySQLProvider) updateAdmin(admin *Admin) error {\n\treturn p.normalizeError(sqlCommonUpdateAdmin(admin, p.dbHandle), -1)\n}\n\nfunc (p *MySQLProvider) deleteAdmin(admin Admin) error {\n\treturn sqlCommonDeleteAdmin(admin, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {\n\treturn sqlCommonGetAdmins(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpAdmins() ([]Admin, error) {\n\treturn sqlCommonDumpAdmins(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {\n\treturn sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) apiKeyExists(keyID string) (APIKey, error) {\n\treturn sqlCommonGetAPIKeyByID(keyID, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addAPIKey(apiKey *APIKey) error {\n\treturn p.normalizeError(sqlCommonAddAPIKey(apiKey, p.dbHandle), -1)\n}\n\nfunc (p *MySQLProvider) updateAPIKey(apiKey *APIKey) error {\n\treturn p.normalizeError(sqlCommonUpdateAPIKey(apiKey, p.dbHandle), -1)\n}\n\nfunc (p *MySQLProvider) deleteAPIKey(apiKey APIKey) error {\n\treturn sqlCommonDeleteAPIKey(apiKey, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {\n\treturn sqlCommonGetAPIKeys(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpAPIKeys() ([]APIKey, error) {\n\treturn sqlCommonDumpAPIKeys(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateAPIKeyLastUse(keyID string) error {\n\treturn sqlCommonUpdateAPIKeyLastUse(keyID, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) shareExists(shareID, username string) (Share, error) {\n\treturn sqlCommonGetShareByID(shareID, username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addShare(share *Share) error {\n\treturn p.normalizeError(sqlCommonAddShare(share, p.dbHandle), fieldName)\n}\n\nfunc (p *MySQLProvider) updateShare(share *Share) error {\n\treturn p.normalizeError(sqlCommonUpdateShare(share, p.dbHandle), -1)\n}\n\nfunc (p *MySQLProvider) deleteShare(share Share) error {\n\treturn sqlCommonDeleteShare(share, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getShares(limit int, offset int, order, username string) ([]Share, error) {\n\treturn sqlCommonGetShares(limit, offset, order, username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpShares() ([]Share, error) {\n\treturn sqlCommonDumpShares(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateShareLastUse(shareID string, numTokens int) error {\n\treturn sqlCommonUpdateShareLastUse(shareID, numTokens, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getDefenderHosts(from int64, limit int) ([]DefenderEntry, error) {\n\treturn sqlCommonGetDefenderHosts(from, limit, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getDefenderHostByIP(ip string, from int64) (DefenderEntry, error) {\n\treturn sqlCommonGetDefenderHostByIP(ip, from, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) isDefenderHostBanned(ip string) (DefenderEntry, error) {\n\treturn sqlCommonIsDefenderHostBanned(ip, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateDefenderBanTime(ip string, minutes int) error {\n\treturn sqlCommonDefenderIncrementBanTime(ip, minutes, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) deleteDefenderHost(ip string) error {\n\treturn sqlCommonDeleteDefenderHost(ip, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addDefenderEvent(ip string, score int) error {\n\treturn sqlCommonAddDefenderHostAndEvent(ip, score, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) setDefenderBanTime(ip string, banTime int64) error {\n\treturn sqlCommonSetDefenderBanTime(ip, banTime, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) cleanupDefender(from int64) error {\n\treturn sqlCommonDefenderCleanup(from, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addActiveTransfer(transfer ActiveTransfer) error {\n\treturn sqlCommonAddActiveTransfer(transfer, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateActiveTransferSizes(ulSize, dlSize, transferID int64, connectionID string) error {\n\treturn sqlCommonUpdateActiveTransferSizes(ulSize, dlSize, transferID, connectionID, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) removeActiveTransfer(transferID int64, connectionID string) error {\n\treturn sqlCommonRemoveActiveTransfer(transferID, connectionID, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) cleanupActiveTransfers(before time.Time) error {\n\treturn sqlCommonCleanupActiveTransfers(before, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getActiveTransfers(from time.Time) ([]ActiveTransfer, error) {\n\treturn sqlCommonGetActiveTransfers(from, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addSharedSession(session Session) error {\n\treturn sqlCommonAddSession(session, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) deleteSharedSession(key string, sessionType SessionType) error {\n\treturn sqlCommonDeleteSession(key, sessionType, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {\n\treturn sqlCommonGetSession(key, sessionType, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {\n\treturn sqlCommonCleanupSessions(sessionType, before, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {\n\treturn sqlCommonGetEventActions(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpEventActions() ([]BaseEventAction, error) {\n\treturn sqlCommonDumpEventActions(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) eventActionExists(name string) (BaseEventAction, error) {\n\treturn sqlCommonGetEventActionByName(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addEventAction(action *BaseEventAction) error {\n\treturn p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName)\n}\n\nfunc (p *MySQLProvider) updateEventAction(action *BaseEventAction) error {\n\treturn sqlCommonUpdateEventAction(action, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) deleteEventAction(action BaseEventAction) error {\n\treturn sqlCommonDeleteEventAction(action, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {\n\treturn sqlCommonGetEventRules(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpEventRules() ([]EventRule, error) {\n\treturn sqlCommonDumpEventRules(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {\n\treturn sqlCommonGetRecentlyUpdatedRules(after, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) eventRuleExists(name string) (EventRule, error) {\n\treturn sqlCommonGetEventRuleByName(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addEventRule(rule *EventRule) error {\n\treturn p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName)\n}\n\nfunc (p *MySQLProvider) updateEventRule(rule *EventRule) error {\n\treturn sqlCommonUpdateEventRule(rule, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) deleteEventRule(rule EventRule, softDelete bool) error {\n\treturn sqlCommonDeleteEventRule(rule, softDelete, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getTaskByName(name string) (Task, error) {\n\treturn sqlCommonGetTaskByName(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addTask(name string) error {\n\treturn sqlCommonAddTask(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateTask(name string, version int64) error {\n\treturn sqlCommonUpdateTask(name, version, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateTaskTimestamp(name string) error {\n\treturn sqlCommonUpdateTaskTimestamp(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addNode() error {\n\treturn sqlCommonAddNode(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getNodeByName(name string) (Node, error) {\n\treturn sqlCommonGetNodeByName(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getNodes() ([]Node, error) {\n\treturn sqlCommonGetNodes(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) updateNodeTimestamp() error {\n\treturn sqlCommonUpdateNodeTimestamp(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) cleanupNodes() error {\n\treturn sqlCommonCleanupNodes(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) roleExists(name string) (Role, error) {\n\treturn sqlCommonGetRoleByName(name, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addRole(role *Role) error {\n\treturn p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName)\n}\n\nfunc (p *MySQLProvider) updateRole(role *Role) error {\n\treturn sqlCommonUpdateRole(role, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) deleteRole(role Role) error {\n\treturn sqlCommonDeleteRole(role, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getRoles(limit int, offset int, order string, minimal bool) ([]Role, error) {\n\treturn sqlCommonGetRoles(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpRoles() ([]Role, error) {\n\treturn sqlCommonDumpRoles(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) ipListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error) {\n\treturn sqlCommonGetIPListEntry(ipOrNet, listType, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) addIPListEntry(entry *IPListEntry) error {\n\treturn p.normalizeError(sqlCommonAddIPListEntry(entry, p.dbHandle), fieldIPNet)\n}\n\nfunc (p *MySQLProvider) updateIPListEntry(entry *IPListEntry) error {\n\treturn sqlCommonUpdateIPListEntry(entry, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) deleteIPListEntry(entry IPListEntry, softDelete bool) error {\n\treturn sqlCommonDeleteIPListEntry(entry, softDelete, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {\n\treturn sqlCommonGetIPListEntries(listType, filter, from, order, limit, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getRecentlyUpdatedIPListEntries(after int64) ([]IPListEntry, error) {\n\treturn sqlCommonGetRecentlyUpdatedIPListEntries(after, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) dumpIPListEntries() ([]IPListEntry, error) {\n\treturn sqlCommonDumpIPListEntries(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) countIPListEntries(listType IPListType) (int64, error) {\n\treturn sqlCommonCountIPListEntries(listType, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error) {\n\treturn sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) getConfigs() (Configs, error) {\n\treturn sqlCommonGetConfigs(p.dbHandle)\n}\n\nfunc (p *MySQLProvider) setConfigs(configs *Configs) error {\n\treturn sqlCommonSetConfigs(configs, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) setFirstDownloadTimestamp(username string) error {\n\treturn sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) setFirstUploadTimestamp(username string) error {\n\treturn sqlCommonSetFirstUploadTimestamp(username, p.dbHandle)\n}\n\nfunc (p *MySQLProvider) close() error {\n\treturn p.dbHandle.Close()\n}\n\nfunc (p *MySQLProvider) reloadConfig() error {\n\treturn nil\n}\n\n// initializeDatabase creates the initial database structure\nfunc (p *MySQLProvider) initializeDatabase() error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)\n\tif err == nil && dbVersion.Version > 0 {\n\t\treturn ErrNoInitRequired\n\t}\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn errSchemaVersionEmpty\n\t}\n\tlogger.InfoToConsole(\"creating initial database schema, version 33\")\n\tproviderLog(logger.LevelInfo, \"creating initial database schema, version 33\")\n\tinitialSQL := sqlReplaceAll(mysqlInitialSQL)\n\n\treturn sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(initialSQL, \";\"), 33, true)\n}\n\nfunc (p *MySQLProvider) migrateDatabase() error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch version := dbVersion.Version; {\n\tcase version == sqlDatabaseVersion:\n\t\tproviderLog(logger.LevelDebug, \"sql database is up to date, current version: %d\", version)\n\t\treturn ErrNoInitRequired\n\tcase version < 33:\n\t\terr = errSchemaVersionTooOld(version)\n\t\tproviderLog(logger.LevelError, \"%v\", err)\n\t\tlogger.ErrorToConsole(\"%v\", err)\n\t\treturn err\n\tcase version == 33:\n\t\treturn updateMySQLDatabaseFromV33(p.dbHandle)\n\tdefault:\n\t\tif version > sqlDatabaseVersion {\n\t\t\tproviderLog(logger.LevelError, \"database schema version %d is newer than the supported one: %d\", version,\n\t\t\t\tsqlDatabaseVersion)\n\t\t\tlogger.WarnToConsole(\"database schema version %d is newer than the supported one: %d\", version,\n\t\t\t\tsqlDatabaseVersion)\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"database schema version not handled: %d\", version)\n\t}\n}\n\nfunc (p *MySQLProvider) revertDatabase(targetVersion int) error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif dbVersion.Version == targetVersion {\n\t\treturn errors.New(\"current version match target version, nothing to do\")\n\t}\n\n\tswitch dbVersion.Version {\n\tcase 34:\n\t\treturn downgradeMySQLDatabaseFromV34(p.dbHandle)\n\tdefault:\n\t\treturn fmt.Errorf(\"database schema version not handled: %d\", dbVersion.Version)\n\t}\n}\n\nfunc (p *MySQLProvider) resetDatabase() error {\n\tsql := sqlReplaceAll(mysqlResetSQL)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(sql, \";\"), 0, false)\n}\n\nfunc (p *MySQLProvider) normalizeError(err error, fieldType int) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\tvar mysqlErr *mysql.MySQLError\n\tif errors.As(err, &mysqlErr) {\n\t\tswitch mysqlErr.Number {\n\t\tcase 1062:\n\t\t\tvar message string\n\t\t\tswitch fieldType {\n\t\t\tcase fieldUsername:\n\t\t\t\tmessage = util.I18nErrorDuplicatedUsername\n\t\t\tcase fieldIPNet:\n\t\t\t\tmessage = util.I18nErrorDuplicatedIPNet\n\t\t\tdefault:\n\t\t\t\tmessage = util.I18nErrorDuplicatedName\n\t\t\t}\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: %s\", ErrDuplicatedKey, err.Error()),\n\t\t\t\tmessage,\n\t\t\t)\n\t\tcase 1452:\n\t\t\treturn fmt.Errorf(\"%w: %s\", ErrForeignKeyViolated, err.Error())\n\t\t}\n\t}\n\treturn err\n}\n\nfunc updateMySQLDatabaseFromV33(dbHandle *sql.DB) error {\n\treturn updateMySQLDatabaseFrom33To34(dbHandle)\n}\n\nfunc downgradeMySQLDatabaseFromV34(dbHandle *sql.DB) error {\n\treturn downgradeMySQLDatabaseFrom34To33(dbHandle)\n}\n\nfunc updateMySQLDatabaseFrom33To34(dbHandle *sql.DB) error {\n\tlogger.InfoToConsole(\"updating database schema version: 33 -> 34\")\n\tproviderLog(logger.LevelInfo, \"updating database schema version: 33 -> 34\")\n\n\tsql := strings.ReplaceAll(mysqlV34SQL, \"{{prefix}}\", config.SQLTablesPrefix)\n\tsql = strings.ReplaceAll(sql, \"{{shares}}\", sqlTableShares)\n\tsql = strings.ReplaceAll(sql, \"{{shares_groups_mapping}}\", sqlTableSharesGroupsMapping)\n\tsql = strings.ReplaceAll(sql, \"{{groups}}\", sqlTableGroups)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, \";\"), 34, true)\n}\n\nfunc downgradeMySQLDatabaseFrom34To33(dbHandle *sql.DB) error {\n\tlogger.InfoToConsole(\"downgrading database schema version: 34 -> 33\")\n\tproviderLog(logger.LevelInfo, \"downgrading database schema version: 34 -> 33\")\n\n\tsql := strings.ReplaceAll(mysqlV34DownSQL, \"{{shares_groups_mapping}}\", sqlTableSharesGroupsMapping)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, \";\"), 33, false)\n}\n"
  },
  {
    "path": "internal/dataprovider/mysql_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build nomysql\n\npackage dataprovider\n\nimport (\n\t\"errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-mysql\")\n}\n\nfunc initializeMySQLProvider() error {\n\treturn errors.New(\"MySQL disabled at build time\")\n}\n"
  },
  {
    "path": "internal/dataprovider/node.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v4\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// Supported protocols for connecting to other nodes\nconst (\n\tNodeProtoHTTP  = \"http\"\n\tNodeProtoHTTPS = \"https\"\n)\n\nconst (\n\t// NodeTokenHeader defines the header to use for the node auth token\n\tNodeTokenHeader   = \"X-SFTPGO-Node\"\n\tnodeTokenAudience = \"node\"\n)\n\nvar (\n\t// current node\n\tcurrentNode        *Node\n\terrNoClusterNodes  = errors.New(\"no cluster node defined\")\n\tactiveNodeTimeDiff = -2 * time.Minute\n\tnodeReqTimeout     = 8 * time.Second\n)\n\n// NodeConfig defines the node configuration\ntype NodeConfig struct {\n\tHost  string `json:\"host\" mapstructure:\"host\"`\n\tPort  int    `json:\"port\" mapstructure:\"port\"`\n\tProto string `json:\"proto\" mapstructure:\"proto\"`\n}\n\nfunc (n *NodeConfig) validate() error {\n\tcurrentNode = nil\n\tif config.IsShared != 1 {\n\t\treturn nil\n\t}\n\tif n.Host == \"\" {\n\t\treturn nil\n\t}\n\tcurrentNode = &Node{\n\t\tData: NodeData{\n\t\t\tHost:  n.Host,\n\t\t\tPort:  n.Port,\n\t\t\tProto: n.Proto,\n\t\t},\n\t}\n\treturn provider.addNode()\n}\n\n// NodeData defines the details to connect to a cluster node\ntype NodeData struct {\n\tHost  string      `json:\"host\"`\n\tPort  int         `json:\"port\"`\n\tProto string      `json:\"proto\"`\n\tKey   *kms.Secret `json:\"api_key\"`\n}\n\nfunc (n *NodeData) validate() error {\n\tif n.Host == \"\" {\n\t\treturn util.NewValidationError(\"node host is mandatory\")\n\t}\n\tif n.Port < 0 || n.Port > 65535 {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid node port: %d\", n.Port))\n\t}\n\tif n.Proto != NodeProtoHTTP && n.Proto != NodeProtoHTTPS {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"invalid node proto: %s\", n.Proto))\n\t}\n\tn.Key = kms.NewPlainSecret(util.GenerateOpaqueString())\n\tn.Key.SetAdditionalData(n.Host)\n\tif err := n.Key.Encrypt(); err != nil {\n\t\treturn fmt.Errorf(\"unable to encrypt node key: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (n *NodeData) getNodeName() string {\n\th := sha256.New()\n\tvar b bytes.Buffer\n\n\tfmt.Fprintf(&b, \"%s:%d\", n.Host, n.Port)\n\th.Write(b.Bytes())\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\n// Node defines a cluster node\ntype Node struct {\n\tName      string   `json:\"name\"`\n\tData      NodeData `json:\"data\"`\n\tCreatedAt int64    `json:\"created_at\"`\n\tUpdatedAt int64    `json:\"updated_at\"`\n}\n\nfunc (n *Node) validate() error {\n\tif n.Name == \"\" {\n\t\tn.Name = n.Data.getNodeName()\n\t}\n\treturn n.Data.validate()\n}\n\nfunc (n *Node) authenticate(token string) (*jwt.Claims, error) {\n\tif err := n.Data.Key.TryDecrypt(); err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to decrypt node key: %v\", err)\n\t\treturn nil, err\n\t}\n\tif token == \"\" {\n\t\treturn nil, ErrInvalidCredentials\n\t}\n\tclaims, err := jwt.VerifyTokenWithKey(token, []jose.SignatureAlgorithm{jose.HS256}, []byte(n.Data.Key.GetPayload()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse and validate token: %v\", err)\n\t}\n\tif claims.Username == \"\" {\n\t\treturn nil, errors.New(\"no admin username associated with node token\")\n\t}\n\tif !claims.Audience.Contains(nodeTokenAudience) {\n\t\treturn nil, errors.New(\"invalid node token audience\")\n\t}\n\n\treturn claims, nil\n}\n\n// getBaseURL returns the base URL for this node\nfunc (n *Node) getBaseURL() string {\n\tvar sb strings.Builder\n\tsb.WriteString(n.Data.Proto)\n\tsb.WriteString(\"://\")\n\tsb.WriteString(n.Data.Host)\n\tif n.Data.Port > 0 {\n\t\tsb.WriteString(\":\")\n\t\tsb.WriteString(strconv.Itoa(n.Data.Port))\n\t}\n\treturn sb.String()\n}\n\n// generateAuthToken generates a new auth token\nfunc (n *Node) generateAuthToken(username, role string, permissions []string) (string, error) {\n\tif err := n.Data.Key.TryDecrypt(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to decrypt node key: %w\", err)\n\t}\n\tsigner, err := jwt.NewSigner(jose.HS256, []byte(n.Data.Key.GetPayload()))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create signer: %w\", err)\n\t}\n\tclaims := &jwt.Claims{\n\t\tUsername:    username,\n\t\tRole:        role,\n\t\tPermissions: permissions,\n\t}\n\tclaims.Audience = []string{nodeTokenAudience}\n\tclaims.SetExpiry(time.Now().Add(1 * time.Minute))\n\tpayload, err := signer.Sign(claims)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to sign authentication token: %w\", err)\n\t}\n\treturn payload, nil\n}\n\nfunc (n *Node) prepareRequest(ctx context.Context, username, role, relativeURL, method string,\n\tpermissions []string, body io.Reader,\n) (*http.Request, error) {\n\turl := fmt.Sprintf(\"%s%s\", n.getBaseURL(), relativeURL)\n\treq, err := http.NewRequestWithContext(ctx, method, url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttoken, err := n.generateAuthToken(username, role, permissions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(NodeTokenHeader, fmt.Sprintf(\"Bearer %s\", token))\n\treturn req, nil\n}\n\n// SendGetRequest sends an HTTP GET request to this node.\n// The responseHolder must be a pointer\nfunc (n *Node) SendGetRequest(username, role, relativeURL string, permissions []string, responseHolder any) error {\n\tctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)\n\tdefer cancel()\n\n\treq, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodGet, permissions, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tclient := httpclient.GetHTTPClient()\n\tdefer client.CloseIdleConnections()\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to send HTTP GET to node %s: %w\", n.Name, err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent {\n\t\treturn fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\trespBody, err := io.ReadAll(io.LimitReader(resp.Body, 10485760))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to read response body: %w\", err)\n\t}\n\terr = json.Unmarshal(respBody, responseHolder)\n\tif err != nil {\n\t\treturn errors.New(\"unable to decode response as json\")\n\t}\n\treturn nil\n}\n\n// SendDeleteRequest sends an HTTP DELETE request to this node\nfunc (n *Node) SendDeleteRequest(username, role, relativeURL string, permissions []string) error {\n\tctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)\n\tdefer cancel()\n\n\treq, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodDelete, permissions, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tclient := httpclient.GetHTTPClient()\n\tdefer client.CloseIdleConnections()\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to send HTTP DELETE to node %s: %w\", n.Name, err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent {\n\t\treturn fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\treturn nil\n}\n\n// AuthenticateNodeToken check the validity of the provided token\nfunc AuthenticateNodeToken(token string) (*jwt.Claims, error) {\n\tif currentNode == nil {\n\t\treturn nil, errNoClusterNodes\n\t}\n\treturn currentNode.authenticate(token)\n}\n\n// GetNodeName returns the node name or an empty string\nfunc GetNodeName() string {\n\tif currentNode == nil {\n\t\treturn \"\"\n\t}\n\treturn currentNode.Name\n}\n"
  },
  {
    "path": "internal/dataprovider/pgsql.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !nopgsql\n\npackage dataprovider\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgconn\"\n\t\"github.com/jackc/pgx/v5/stdlib\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tpgsqlResetSQL = `DROP TABLE IF EXISTS \"{{api_keys}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{users_folders_mapping}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{users_groups_mapping}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{admins_groups_mapping}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{groups_folders_mapping}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{shares_groups_mapping}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{admins}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{folders}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{shares}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{users}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{groups}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{defender_events}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{defender_hosts}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{active_transfers}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{shared_sessions}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{rules_actions_mapping}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{events_actions}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{events_rules}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{tasks}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{nodes}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{roles}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{ip_lists}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{configs}}\" CASCADE;\nDROP TABLE IF EXISTS \"{{schema_version}}\" CASCADE;\n`\n\tpgsqlInitial = `CREATE TABLE \"{{schema_version}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"version\" integer NOT NULL);\nCREATE TABLE \"{{admins}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"username\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"password\" varchar(255) NOT NULL, \"email\" varchar(255) NULL, \"status\" integer NOT NULL,\n\"permissions\" text NOT NULL, \"filters\" text NULL, \"additional_info\" text NULL, \"last_login\" bigint NOT NULL,\n\"role_id\" integer NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL);\nCREATE TABLE \"{{active_transfers}}\" (\"id\" bigint NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"connection_id\" varchar(100) NOT NULL,\n\"transfer_id\" bigint NOT NULL, \"transfer_type\" integer NOT NULL, \"username\" varchar(255) NOT NULL,\n\"folder_name\" varchar(255) NULL, \"ip\" varchar(50) NOT NULL, \"truncated_size\" bigint NOT NULL,\n\"current_ul_size\" bigint NOT NULL, \"current_dl_size\" bigint NOT NULL, \"created_at\" bigint NOT NULL,\n\"updated_at\" bigint NOT NULL);\nCREATE TABLE \"{{defender_hosts}}\" (\"id\" bigint NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"ip\" varchar(50) NOT NULL UNIQUE,\n\"ban_time\" bigint NOT NULL, \"updated_at\" bigint NOT NULL);\nCREATE TABLE \"{{defender_events}}\" (\"id\" bigint NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"date_time\" bigint NOT NULL, \"score\" integer NOT NULL,\n\"host_id\" bigint NOT NULL);\nALTER TABLE \"{{defender_events}}\" ADD CONSTRAINT \"{{prefix}}defender_events_host_id_fk_defender_hosts_id\" FOREIGN KEY\n(\"host_id\") REFERENCES \"{{defender_hosts}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nCREATE TABLE \"{{folders}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"name\" varchar(255) NOT NULL UNIQUE, \"description\" varchar(512) NULL,\n\"path\" text NULL, \"used_quota_size\" bigint NOT NULL, \"used_quota_files\" integer NOT NULL, \"last_quota_update\" bigint NOT NULL,\n\"filesystem\" text NULL);\nCREATE TABLE \"{{groups}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL, \"user_settings\" text NULL);\nCREATE TABLE \"{{shared_sessions}}\" (\"key\" varchar(128) NOT NULL, \"type\" integer NOT NULL,\n\"data\" text NOT NULL, \"timestamp\" bigint NOT NULL, PRIMARY KEY (\"key\", \"type\"));\nCREATE TABLE \"{{users}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"username\" varchar(255) NOT NULL UNIQUE, \"status\" integer NOT NULL,\n\"expiration_date\" bigint NOT NULL, \"description\" varchar(512) NULL, \"password\" text NULL, \"public_keys\" text NULL,\n\"home_dir\" text NOT NULL, \"uid\" bigint NOT NULL, \"gid\" bigint NOT NULL, \"max_sessions\" integer NOT NULL,\n\"quota_size\" bigint NOT NULL, \"quota_files\" integer NOT NULL, \"permissions\" text NOT NULL, \"used_quota_size\" bigint NOT NULL,\n\"used_quota_files\" integer NOT NULL, \"last_quota_update\" bigint NOT NULL, \"upload_bandwidth\" integer NOT NULL,\n\"download_bandwidth\" integer NOT NULL, \"last_login\" bigint NOT NULL, \"filters\" text NULL, \"filesystem\" text NULL,\n\"additional_info\" text NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL, \"email\" varchar(255) NULL,\n\"upload_data_transfer\" integer NOT NULL, \"download_data_transfer\" integer NOT NULL, \"total_data_transfer\" integer NOT NULL,\n\"used_upload_data_transfer\" bigint NOT NULL, \"used_download_data_transfer\" bigint NOT NULL, \"deleted_at\" bigint NOT NULL,\n\"first_download\" bigint NOT NULL, \"first_upload\" bigint NOT NULL, \"last_password_change\" bigint NOT NULL, \"role_id\" integer NULL);\nCREATE TABLE \"{{groups_folders_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"group_id\" integer NOT NULL,\n\"folder_id\" integer NOT NULL, \"virtual_path\" text NOT NULL, \"quota_size\" bigint NOT NULL, \"quota_files\" integer NOT NULL, \"sort_order\" integer NOT NULL);\nCREATE TABLE \"{{users_groups_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"user_id\" integer NOT NULL,\n\"group_id\" integer NOT NULL, \"group_type\" integer NOT NULL, \"sort_order\" integer NOT NULL);\nCREATE TABLE \"{{users_folders_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"virtual_path\" text NOT NULL,\n\"quota_size\" bigint NOT NULL, \"quota_files\" integer NOT NULL, \"sort_order\" integer NOT NULL, \"folder_id\" integer NOT NULL, \"user_id\" integer NOT NULL);\nALTER TABLE \"{{users_folders_mapping}}\" ADD CONSTRAINT \"{{prefix}}unique_user_folder_mapping\" UNIQUE (\"user_id\", \"folder_id\");\nALTER TABLE \"{{users_folders_mapping}}\" ADD CONSTRAINT \"{{prefix}}users_folders_mapping_folder_id_fk_folders_id\"\nFOREIGN KEY (\"folder_id\") REFERENCES \"{{folders}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nALTER TABLE \"{{users_folders_mapping}}\" ADD CONSTRAINT \"{{prefix}}users_folders_mapping_user_id_fk_users_id\"\nFOREIGN KEY (\"user_id\") REFERENCES \"{{users}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nCREATE TABLE \"{{shares}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,\n\"share_id\" varchar(60) NOT NULL UNIQUE, \"name\" varchar(255) NOT NULL, \"description\" varchar(512) NULL,\n\"scope\" integer NOT NULL, \"paths\" text NOT NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL,\n\"last_use_at\" bigint NOT NULL, \"expires_at\" bigint NOT NULL, \"password\" text NULL,\n\"max_tokens\" integer NOT NULL, \"used_tokens\" integer NOT NULL, \"allow_from\" text NULL, \"options\" text NULL,\n\"user_id\" integer NOT NULL);\nALTER TABLE \"{{shares}}\" ADD CONSTRAINT \"{{prefix}}shares_user_id_fk_users_id\" FOREIGN KEY (\"user_id\")\nREFERENCES \"{{users}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nCREATE TABLE \"{{api_keys}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"name\" varchar(255) NOT NULL,\n\"key_id\" varchar(50) NOT NULL UNIQUE, \"api_key\" varchar(255) NOT NULL UNIQUE, \"scope\" integer NOT NULL,\n\"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL, \"last_use_at\" bigint NOT NULL,\"expires_at\" bigint NOT NULL,\n\"description\" text NULL, \"admin_id\" integer NULL, \"user_id\" integer NULL);\nALTER TABLE \"{{api_keys}}\" ADD CONSTRAINT \"{{prefix}}api_keys_admin_id_fk_admins_id\" FOREIGN KEY (\"admin_id\")\nREFERENCES \"{{admins}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nALTER TABLE \"{{api_keys}}\" ADD CONSTRAINT \"{{prefix}}api_keys_user_id_fk_users_id\" FOREIGN KEY (\"user_id\")\nREFERENCES \"{{users}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nALTER TABLE \"{{users_groups_mapping}}\" ADD CONSTRAINT \"{{prefix}}unique_user_group_mapping\" UNIQUE (\"user_id\", \"group_id\");\nALTER TABLE \"{{groups_folders_mapping}}\" ADD CONSTRAINT \"{{prefix}}unique_group_folder_mapping\" UNIQUE (\"group_id\", \"folder_id\");\nCREATE INDEX \"{{prefix}}users_groups_mapping_group_id_idx\" ON \"{{users_groups_mapping}}\" (\"group_id\");\nALTER TABLE \"{{users_groups_mapping}}\" ADD CONSTRAINT \"{{prefix}}users_groups_mapping_group_id_fk_groups_id\"\nFOREIGN KEY (\"group_id\") REFERENCES \"{{groups}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION;\nCREATE INDEX \"{{prefix}}users_groups_mapping_user_id_idx\" ON \"{{users_groups_mapping}}\" (\"user_id\");\nALTER TABLE \"{{users_groups_mapping}}\" ADD CONSTRAINT \"{{prefix}}users_groups_mapping_user_id_fk_users_id\"\nFOREIGN KEY (\"user_id\") REFERENCES \"{{users}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nCREATE INDEX \"{{prefix}}users_groups_mapping_sort_order_idx\" ON \"{{users_groups_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}groups_folders_mapping_folder_id_idx\" ON \"{{groups_folders_mapping}}\" (\"folder_id\");\nALTER TABLE \"{{groups_folders_mapping}}\" ADD CONSTRAINT \"{{prefix}}groups_folders_mapping_folder_id_fk_folders_id\"\nFOREIGN KEY (\"folder_id\") REFERENCES \"{{folders}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nCREATE INDEX \"{{prefix}}groups_folders_mapping_group_id_idx\" ON \"{{groups_folders_mapping}}\" (\"group_id\");\nALTER TABLE \"{{groups_folders_mapping}}\" ADD CONSTRAINT \"{{prefix}}groups_folders_mapping_group_id_fk_groups_id\"\nFOREIGN KEY (\"group_id\") REFERENCES \"{{groups}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nCREATE INDEX \"{{prefix}}groups_folders_mapping_sort_order_idx\" ON \"{{groups_folders_mapping}}\" (\"sort_order\");\nCREATE TABLE \"{{events_rules}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"status\" integer NOT NULL, \"description\" varchar(512) NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL,\n\"trigger\" integer NOT NULL, \"conditions\" text NOT NULL, \"deleted_at\" bigint NOT NULL);\nCREATE TABLE \"{{events_actions}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"type\" integer NOT NULL, \"options\" text NOT NULL);\nCREATE TABLE \"{{rules_actions_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"rule_id\" integer NOT NULL,\n\"action_id\" integer NOT NULL, \"order\" integer NOT NULL, \"options\" text NOT NULL);\nCREATE TABLE \"{{tasks}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"name\" varchar(255) NOT NULL UNIQUE, \"updated_at\" bigint NOT NULL,\n\"version\" bigint NOT NULL);\nALTER TABLE \"{{rules_actions_mapping}}\" ADD CONSTRAINT \"{{prefix}}unique_rule_action_mapping\" UNIQUE (\"rule_id\", \"action_id\");\nALTER TABLE \"{{rules_actions_mapping}}\" ADD CONSTRAINT \"{{prefix}}rules_actions_mapping_rule_id_fk_events_rules_id\"\nFOREIGN KEY (\"rule_id\") REFERENCES \"{{events_rules}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nALTER TABLE \"{{rules_actions_mapping}}\" ADD CONSTRAINT \"{{prefix}}rules_actions_mapping_action_id_fk_events_targets_id\"\nFOREIGN KEY (\"action_id\") REFERENCES \"{{events_actions}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION;\nCREATE TABLE \"{{admins_groups_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,\n\"admin_id\" integer NOT NULL, \"group_id\" integer NOT NULL, \"options\" text NOT NULL, \"sort_order\" integer NOT NULL);\nALTER TABLE \"{{admins_groups_mapping}}\" ADD CONSTRAINT \"{{prefix}}unique_admin_group_mapping\" UNIQUE (\"admin_id\", \"group_id\");\nALTER TABLE \"{{admins_groups_mapping}}\" ADD CONSTRAINT \"{{prefix}}admins_groups_mapping_admin_id_fk_admins_id\"\nFOREIGN KEY (\"admin_id\") REFERENCES \"{{admins}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nALTER TABLE \"{{admins_groups_mapping}}\" ADD CONSTRAINT \"{{prefix}}admins_groups_mapping_group_id_fk_groups_id\"\nFOREIGN KEY (\"group_id\") REFERENCES \"{{groups}}\" (\"id\") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;\nCREATE TABLE \"{{nodes}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"data\" text NOT NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL);\nCREATE TABLE \"{{roles}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL);\nALTER TABLE \"{{admins}}\" ADD CONSTRAINT \"{{prefix}}admins_role_id_fk_roles_id\" FOREIGN KEY (\"role_id\")\nREFERENCES \"{{roles}}\" (\"id\") ON DELETE NO ACTION;\nALTER TABLE \"{{users}}\" ADD CONSTRAINT \"{{prefix}}users_role_id_fk_roles_id\" FOREIGN KEY (\"role_id\")\nREFERENCES \"{{roles}}\" (\"id\") ON DELETE SET NULL;\nCREATE TABLE \"{{ip_lists}}\" (\"id\" bigint NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"type\" integer NOT NULL,\n\"ipornet\" varchar(50) NOT NULL, \"mode\" integer NOT NULL, \"description\" varchar(512) NULL, \"first\" inet NOT NULL,\n\"last\" inet NOT NULL, \"ip_type\" integer NOT NULL, \"protocols\" integer NOT NULL,  \"created_at\" bigint NOT NULL,\n\"updated_at\" bigint NOT NULL, \"deleted_at\" bigint NOT NULL);\nALTER TABLE \"{{ip_lists}}\" ADD CONSTRAINT \"{{prefix}}unique_ipornet_type_mapping\" UNIQUE (\"type\", \"ipornet\");\nCREATE TABLE \"{{configs}}\" (\"id\" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, \"configs\" text NOT NULL);\nINSERT INTO {{configs}} (configs) VALUES ('{}');\nCREATE INDEX \"{{prefix}}users_folders_mapping_folder_id_idx\" ON \"{{users_folders_mapping}}\" (\"folder_id\");\nCREATE INDEX \"{{prefix}}users_folders_mapping_user_id_idx\" ON \"{{users_folders_mapping}}\" (\"user_id\");\nCREATE INDEX \"{{prefix}}users_folders_mapping_sort_order_idx\" ON \"{{users_folders_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}api_keys_admin_id_idx\" ON \"{{api_keys}}\" (\"admin_id\");\nCREATE INDEX \"{{prefix}}api_keys_user_id_idx\" ON \"{{api_keys}}\" (\"user_id\");\nCREATE INDEX \"{{prefix}}users_updated_at_idx\" ON \"{{users}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}users_deleted_at_idx\" ON \"{{users}}\" (\"deleted_at\");\nCREATE INDEX \"{{prefix}}shares_user_id_idx\" ON \"{{shares}}\" (\"user_id\");\nCREATE INDEX \"{{prefix}}defender_hosts_updated_at_idx\" ON \"{{defender_hosts}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}defender_hosts_ban_time_idx\" ON \"{{defender_hosts}}\" (\"ban_time\");\nCREATE INDEX \"{{prefix}}defender_events_date_time_idx\" ON \"{{defender_events}}\" (\"date_time\");\nCREATE INDEX \"{{prefix}}defender_events_host_id_idx\" ON \"{{defender_events}}\" (\"host_id\");\nCREATE INDEX \"{{prefix}}active_transfers_connection_id_idx\" ON \"{{active_transfers}}\" (\"connection_id\");\nCREATE INDEX \"{{prefix}}active_transfers_transfer_id_idx\" ON \"{{active_transfers}}\" (\"transfer_id\");\nCREATE INDEX \"{{prefix}}active_transfers_updated_at_idx\" ON \"{{active_transfers}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}shared_sessions_type_idx\" ON \"{{shared_sessions}}\" (\"type\");\nCREATE INDEX \"{{prefix}}shared_sessions_timestamp_idx\" ON \"{{shared_sessions}}\" (\"timestamp\");\nCREATE INDEX \"{{prefix}}events_rules_updated_at_idx\" ON \"{{events_rules}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}events_rules_deleted_at_idx\" ON \"{{events_rules}}\" (\"deleted_at\");\nCREATE INDEX \"{{prefix}}events_rules_trigger_idx\" ON \"{{events_rules}}\" (\"trigger\");\nCREATE INDEX \"{{prefix}}rules_actions_mapping_rule_id_idx\" ON \"{{rules_actions_mapping}}\" (\"rule_id\");\nCREATE INDEX \"{{prefix}}rules_actions_mapping_action_id_idx\" ON \"{{rules_actions_mapping}}\" (\"action_id\");\nCREATE INDEX \"{{prefix}}rules_actions_mapping_order_idx\" ON \"{{rules_actions_mapping}}\" (\"order\");\nCREATE INDEX \"{{prefix}}admins_groups_mapping_admin_id_idx\" ON \"{{admins_groups_mapping}}\" (\"admin_id\");\nCREATE INDEX \"{{prefix}}admins_groups_mapping_group_id_idx\" ON \"{{admins_groups_mapping}}\" (\"group_id\");\nCREATE INDEX \"{{prefix}}admins_groups_mapping_sort_order_idx\" ON \"{{admins_groups_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}admins_role_id_idx\" ON \"{{admins}}\" (\"role_id\");\nCREATE INDEX \"{{prefix}}users_role_id_idx\" ON \"{{users}}\" (\"role_id\");\nCREATE INDEX \"{{prefix}}ip_lists_type_idx\" ON \"{{ip_lists}}\" (\"type\");\nCREATE INDEX \"{{prefix}}ip_lists_ipornet_idx\" ON \"{{ip_lists}}\" (\"ipornet\");\nCREATE INDEX \"{{prefix}}ip_lists_updated_at_idx\" ON \"{{ip_lists}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}ip_lists_deleted_at_idx\" ON \"{{ip_lists}}\" (\"deleted_at\");\nCREATE INDEX \"{{prefix}}ip_lists_first_last_idx\" ON \"{{ip_lists}}\" (\"first\", \"last\");\nINSERT INTO {{schema_version}} (version) VALUES (33);\n`\n\t// not supported in CockroachDB\n\tipListsLikeIndex = `CREATE INDEX \"{{prefix}}ip_lists_ipornet_like_idx\" ON \"{{ip_lists}}\" (\"ipornet\" varchar_pattern_ops);`\n\tpgsqlV34SQL      = `CREATE TABLE \"{{shares_groups_mapping}}\" (\n\"id\" integer NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,\n\"share_id\" integer NOT NULL,\n\"group_id\" integer NOT NULL,\n\"permissions\" integer NOT NULL,\n\"sort_order\" integer NOT NULL,\nCONSTRAINT \"{{prefix}}unique_share_group_mapping\" UNIQUE (\"share_id\", \"group_id\"),\nCONSTRAINT \"{{prefix}}shares_groups_mapping_share_id_fk\" FOREIGN KEY (\"share_id\") REFERENCES \"{{shares}}\"(\"id\") ON DELETE CASCADE,\nCONSTRAINT \"{{prefix}}shares_groups_mapping_group_id_fk\" FOREIGN KEY (\"group_id\") REFERENCES \"{{groups}}\"(\"id\") ON DELETE CASCADE);\nCREATE INDEX \"{{prefix}}shares_groups_mapping_sort_order_idx\" ON \"{{shares_groups_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}shares_groups_mapping_share_id_idx\" ON \"{{shares_groups_mapping}}\" (\"share_id\");\nCREATE INDEX \"{{prefix}}shares_groups_mapping_group_id_idx\" ON \"{{shares_groups_mapping}}\" (\"group_id\");\n`\n\tpgsqlV34DownSQL = `DROP TABLE IF EXISTS \"{{shares_groups_mapping}}\";`\n)\n\nvar (\n\tpgSQLTargetSessionAttrs = []string{\"any\", \"read-write\", \"read-only\", \"primary\", \"standby\", \"prefer-standby\"}\n)\n\n// PGSQLProvider defines the auth provider for PostgreSQL database\ntype PGSQLProvider struct {\n\tdbHandle *sql.DB\n}\n\nfunc init() {\n\tversion.AddFeature(\"+pgsql\")\n}\n\nfunc initializePGSQLProvider() error {\n\tvar dbHandle *sql.DB\n\tif config.TargetSessionAttrs == \"any\" {\n\t\tpgxConfig, err := pgx.ParseConfig(getPGSQLConnectionString(false))\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"error parsing postgres configuration, connection string: %q, error: %v\",\n\t\t\t\tgetPGSQLConnectionString(true), err)\n\t\t\treturn err\n\t\t}\n\t\tdbHandle = stdlib.OpenDB(*pgxConfig, stdlib.OptionBeforeConnect(stdlib.RandomizeHostOrderFunc))\n\t} else {\n\t\tvar err error\n\t\tdbHandle, err = sql.Open(\"pgx\", getPGSQLConnectionString(false))\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"error creating postgres database handler, connection string: %q, error: %v\",\n\t\t\t\tgetPGSQLConnectionString(true), err)\n\t\t\treturn err\n\t\t}\n\t}\n\tproviderLog(logger.LevelDebug, \"postgres database handle created, connection string: %q, pool size: %d\",\n\t\tgetPGSQLConnectionString(true), config.PoolSize)\n\tdbHandle.SetMaxOpenConns(config.PoolSize)\n\tif config.PoolSize > 0 {\n\t\tdbHandle.SetMaxIdleConns(config.PoolSize)\n\t} else {\n\t\tdbHandle.SetMaxIdleConns(2)\n\t}\n\tdbHandle.SetConnMaxLifetime(240 * time.Second)\n\tdbHandle.SetConnMaxIdleTime(120 * time.Second)\n\tprovider = &PGSQLProvider{dbHandle: dbHandle}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn dbHandle.PingContext(ctx)\n}\n\nfunc getPGSQLHostsAndPorts(configHost string, configPort int) (string, string) {\n\tvar hosts, ports []string\n\tdefaultPort := strconv.Itoa(configPort)\n\tif defaultPort == \"0\" {\n\t\tdefaultPort = \"5432\"\n\t}\n\n\tfor hostport := range strings.SplitSeq(configHost, \",\") {\n\t\thostport = strings.TrimSpace(hostport)\n\t\tif hostport == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\thost, port, err := net.SplitHostPort(hostport)\n\t\tif err == nil {\n\t\t\thosts = append(hosts, host)\n\t\t\tports = append(ports, port)\n\t\t} else {\n\t\t\thosts = append(hosts, hostport)\n\t\t\tports = append(ports, defaultPort)\n\t\t}\n\t}\n\n\treturn strings.Join(hosts, \",\"), strings.Join(ports, \",\")\n}\n\nfunc getPGSQLConnectionString(redactedPwd bool) string {\n\tvar connectionString string\n\tif config.ConnectionString == \"\" {\n\t\tpassword := config.Password\n\t\tif redactedPwd && password != \"\" {\n\t\t\tpassword = \"[redacted]\"\n\t\t}\n\t\thost, port := getPGSQLHostsAndPorts(config.Host, config.Port)\n\t\tconnectionString = fmt.Sprintf(\"host='%s' port='%s' dbname='%s' user='%s' password='%s' sslmode=%s connect_timeout=10\",\n\t\t\thost, port, config.Name, config.Username, password, getSSLMode())\n\t\tif config.RootCert != \"\" {\n\t\t\tconnectionString += fmt.Sprintf(\" sslrootcert='%s'\", config.RootCert)\n\t\t}\n\t\tif config.ClientCert != \"\" && config.ClientKey != \"\" {\n\t\t\tconnectionString += fmt.Sprintf(\" sslcert='%s' sslkey='%s'\", config.ClientCert, config.ClientKey)\n\t\t}\n\t\tif config.DisableSNI {\n\t\t\tconnectionString += \" sslsni=0\"\n\t\t}\n\t\tif slices.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {\n\t\t\tconnectionString += fmt.Sprintf(\" target_session_attrs='%s'\", config.TargetSessionAttrs)\n\t\t}\n\t} else {\n\t\tconnectionString = config.ConnectionString\n\t}\n\treturn connectionString\n}\n\nfunc (p *PGSQLProvider) checkAvailability() error {\n\treturn sqlCommonCheckAvailability(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {\n\treturn sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error) {\n\treturn sqlCommonValidateUserAndTLSCertificate(username, protocol, tlsCert, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte, isSSHCert bool) (User, string, error) {\n\treturn sqlCommonValidateUserAndPubKey(username, publicKey, isSSHCert, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error {\n\treturn sqlCommonUpdateTransferQuota(username, uploadSize, downloadSize, reset, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {\n\treturn sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getUsedQuota(username string) (int, int64, int64, int64, error) {\n\treturn sqlCommonGetUsedQuota(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getAdminSignature(username string) (string, error) {\n\treturn sqlCommonGetAdminSignature(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getUserSignature(username string) (string, error) {\n\treturn sqlCommonGetUserSignature(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) setUpdatedAt(username string) {\n\tsqlCommonSetUpdatedAt(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateLastLogin(username string) error {\n\treturn sqlCommonUpdateLastLogin(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateAdminLastLogin(username string) error {\n\treturn sqlCommonUpdateAdminLastLogin(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) userExists(username, role string) (User, error) {\n\treturn sqlCommonGetUserByUsername(username, role, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addUser(user *User) error {\n\treturn p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername)\n}\n\nfunc (p *PGSQLProvider) updateUser(user *User) error {\n\treturn p.normalizeError(sqlCommonUpdateUser(user, p.dbHandle), -1)\n}\n\nfunc (p *PGSQLProvider) deleteUser(user User, softDelete bool) error {\n\treturn sqlCommonDeleteUser(user, softDelete, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateUserPassword(username, password string) error {\n\treturn sqlCommonUpdateUserPassword(username, password, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpUsers() ([]User, error) {\n\treturn sqlCommonDumpUsers(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getRecentlyUpdatedUsers(after int64) ([]User, error) {\n\treturn sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {\n\treturn sqlCommonGetUsers(limit, offset, order, role, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {\n\treturn sqlCommonGetUsersForQuotaCheck(toFetch, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {\n\treturn sqlCommonDumpFolders(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error) {\n\treturn sqlCommonGetFolders(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\treturn sqlCommonGetFolderByName(ctx, name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {\n\treturn p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName)\n}\n\nfunc (p *PGSQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {\n\treturn sqlCommonUpdateFolder(folder, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) deleteFolder(folder vfs.BaseVirtualFolder) error {\n\treturn sqlCommonDeleteFolder(folder, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {\n\treturn sqlCommonUpdateFolderQuota(name, filesAdd, sizeAdd, reset, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getUsedFolderQuota(name string) (int, int64, error) {\n\treturn sqlCommonGetFolderUsedQuota(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getGroups(limit, offset int, order string, minimal bool) ([]Group, error) {\n\treturn sqlCommonGetGroups(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getGroupsWithNames(names []string) ([]Group, error) {\n\treturn sqlCommonGetGroupsWithNames(names, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getUsersInGroups(names []string) ([]string, error) {\n\treturn sqlCommonGetUsersInGroups(names, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) groupExists(name string) (Group, error) {\n\treturn sqlCommonGetGroupByName(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addGroup(group *Group) error {\n\treturn p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName)\n}\n\nfunc (p *PGSQLProvider) updateGroup(group *Group) error {\n\treturn sqlCommonUpdateGroup(group, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) deleteGroup(group Group) error {\n\treturn sqlCommonDeleteGroup(group, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpGroups() ([]Group, error) {\n\treturn sqlCommonDumpGroups(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) adminExists(username string) (Admin, error) {\n\treturn sqlCommonGetAdminByUsername(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addAdmin(admin *Admin) error {\n\treturn p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername)\n}\n\nfunc (p *PGSQLProvider) updateAdmin(admin *Admin) error {\n\treturn p.normalizeError(sqlCommonUpdateAdmin(admin, p.dbHandle), -1)\n}\n\nfunc (p *PGSQLProvider) deleteAdmin(admin Admin) error {\n\treturn sqlCommonDeleteAdmin(admin, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {\n\treturn sqlCommonGetAdmins(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpAdmins() ([]Admin, error) {\n\treturn sqlCommonDumpAdmins(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {\n\treturn sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) apiKeyExists(keyID string) (APIKey, error) {\n\treturn sqlCommonGetAPIKeyByID(keyID, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addAPIKey(apiKey *APIKey) error {\n\treturn p.normalizeError(sqlCommonAddAPIKey(apiKey, p.dbHandle), -1)\n}\n\nfunc (p *PGSQLProvider) updateAPIKey(apiKey *APIKey) error {\n\treturn p.normalizeError(sqlCommonUpdateAPIKey(apiKey, p.dbHandle), -1)\n}\n\nfunc (p *PGSQLProvider) deleteAPIKey(apiKey APIKey) error {\n\treturn sqlCommonDeleteAPIKey(apiKey, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {\n\treturn sqlCommonGetAPIKeys(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpAPIKeys() ([]APIKey, error) {\n\treturn sqlCommonDumpAPIKeys(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateAPIKeyLastUse(keyID string) error {\n\treturn sqlCommonUpdateAPIKeyLastUse(keyID, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) shareExists(shareID, username string) (Share, error) {\n\treturn sqlCommonGetShareByID(shareID, username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addShare(share *Share) error {\n\treturn p.normalizeError(sqlCommonAddShare(share, p.dbHandle), fieldName)\n}\n\nfunc (p *PGSQLProvider) updateShare(share *Share) error {\n\treturn p.normalizeError(sqlCommonUpdateShare(share, p.dbHandle), -1)\n}\n\nfunc (p *PGSQLProvider) deleteShare(share Share) error {\n\treturn sqlCommonDeleteShare(share, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getShares(limit int, offset int, order, username string) ([]Share, error) {\n\treturn sqlCommonGetShares(limit, offset, order, username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpShares() ([]Share, error) {\n\treturn sqlCommonDumpShares(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateShareLastUse(shareID string, numTokens int) error {\n\treturn sqlCommonUpdateShareLastUse(shareID, numTokens, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getDefenderHosts(from int64, limit int) ([]DefenderEntry, error) {\n\treturn sqlCommonGetDefenderHosts(from, limit, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getDefenderHostByIP(ip string, from int64) (DefenderEntry, error) {\n\treturn sqlCommonGetDefenderHostByIP(ip, from, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) isDefenderHostBanned(ip string) (DefenderEntry, error) {\n\treturn sqlCommonIsDefenderHostBanned(ip, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateDefenderBanTime(ip string, minutes int) error {\n\treturn sqlCommonDefenderIncrementBanTime(ip, minutes, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) deleteDefenderHost(ip string) error {\n\treturn sqlCommonDeleteDefenderHost(ip, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addDefenderEvent(ip string, score int) error {\n\treturn sqlCommonAddDefenderHostAndEvent(ip, score, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) setDefenderBanTime(ip string, banTime int64) error {\n\treturn sqlCommonSetDefenderBanTime(ip, banTime, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) cleanupDefender(from int64) error {\n\treturn sqlCommonDefenderCleanup(from, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addActiveTransfer(transfer ActiveTransfer) error {\n\treturn sqlCommonAddActiveTransfer(transfer, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateActiveTransferSizes(ulSize, dlSize, transferID int64, connectionID string) error {\n\treturn sqlCommonUpdateActiveTransferSizes(ulSize, dlSize, transferID, connectionID, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) removeActiveTransfer(transferID int64, connectionID string) error {\n\treturn sqlCommonRemoveActiveTransfer(transferID, connectionID, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) cleanupActiveTransfers(before time.Time) error {\n\treturn sqlCommonCleanupActiveTransfers(before, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getActiveTransfers(from time.Time) ([]ActiveTransfer, error) {\n\treturn sqlCommonGetActiveTransfers(from, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addSharedSession(session Session) error {\n\treturn sqlCommonAddSession(session, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) deleteSharedSession(key string, sessionType SessionType) error {\n\treturn sqlCommonDeleteSession(key, sessionType, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {\n\treturn sqlCommonGetSession(key, sessionType, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {\n\treturn sqlCommonCleanupSessions(sessionType, before, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {\n\treturn sqlCommonGetEventActions(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpEventActions() ([]BaseEventAction, error) {\n\treturn sqlCommonDumpEventActions(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) eventActionExists(name string) (BaseEventAction, error) {\n\treturn sqlCommonGetEventActionByName(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addEventAction(action *BaseEventAction) error {\n\treturn p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName)\n}\n\nfunc (p *PGSQLProvider) updateEventAction(action *BaseEventAction) error {\n\treturn sqlCommonUpdateEventAction(action, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) deleteEventAction(action BaseEventAction) error {\n\treturn sqlCommonDeleteEventAction(action, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {\n\treturn sqlCommonGetEventRules(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpEventRules() ([]EventRule, error) {\n\treturn sqlCommonDumpEventRules(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {\n\treturn sqlCommonGetRecentlyUpdatedRules(after, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) eventRuleExists(name string) (EventRule, error) {\n\treturn sqlCommonGetEventRuleByName(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addEventRule(rule *EventRule) error {\n\treturn p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName)\n}\n\nfunc (p *PGSQLProvider) updateEventRule(rule *EventRule) error {\n\treturn sqlCommonUpdateEventRule(rule, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) deleteEventRule(rule EventRule, softDelete bool) error {\n\treturn sqlCommonDeleteEventRule(rule, softDelete, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getTaskByName(name string) (Task, error) {\n\treturn sqlCommonGetTaskByName(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addTask(name string) error {\n\treturn sqlCommonAddTask(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateTask(name string, version int64) error {\n\treturn sqlCommonUpdateTask(name, version, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateTaskTimestamp(name string) error {\n\treturn sqlCommonUpdateTaskTimestamp(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addNode() error {\n\treturn sqlCommonAddNode(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getNodeByName(name string) (Node, error) {\n\treturn sqlCommonGetNodeByName(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getNodes() ([]Node, error) {\n\treturn sqlCommonGetNodes(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) updateNodeTimestamp() error {\n\treturn sqlCommonUpdateNodeTimestamp(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) cleanupNodes() error {\n\treturn sqlCommonCleanupNodes(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) roleExists(name string) (Role, error) {\n\treturn sqlCommonGetRoleByName(name, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addRole(role *Role) error {\n\treturn p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName)\n}\n\nfunc (p *PGSQLProvider) updateRole(role *Role) error {\n\treturn sqlCommonUpdateRole(role, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) deleteRole(role Role) error {\n\treturn sqlCommonDeleteRole(role, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getRoles(limit int, offset int, order string, minimal bool) ([]Role, error) {\n\treturn sqlCommonGetRoles(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpRoles() ([]Role, error) {\n\treturn sqlCommonDumpRoles(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) ipListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error) {\n\treturn sqlCommonGetIPListEntry(ipOrNet, listType, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) addIPListEntry(entry *IPListEntry) error {\n\treturn p.normalizeError(sqlCommonAddIPListEntry(entry, p.dbHandle), fieldIPNet)\n}\n\nfunc (p *PGSQLProvider) updateIPListEntry(entry *IPListEntry) error {\n\treturn sqlCommonUpdateIPListEntry(entry, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) deleteIPListEntry(entry IPListEntry, softDelete bool) error {\n\treturn sqlCommonDeleteIPListEntry(entry, softDelete, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {\n\treturn sqlCommonGetIPListEntries(listType, filter, from, order, limit, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getRecentlyUpdatedIPListEntries(after int64) ([]IPListEntry, error) {\n\treturn sqlCommonGetRecentlyUpdatedIPListEntries(after, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) dumpIPListEntries() ([]IPListEntry, error) {\n\treturn sqlCommonDumpIPListEntries(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) countIPListEntries(listType IPListType) (int64, error) {\n\treturn sqlCommonCountIPListEntries(listType, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error) {\n\treturn sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) getConfigs() (Configs, error) {\n\treturn sqlCommonGetConfigs(p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) setConfigs(configs *Configs) error {\n\treturn sqlCommonSetConfigs(configs, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) setFirstDownloadTimestamp(username string) error {\n\treturn sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) setFirstUploadTimestamp(username string) error {\n\treturn sqlCommonSetFirstUploadTimestamp(username, p.dbHandle)\n}\n\nfunc (p *PGSQLProvider) close() error {\n\treturn p.dbHandle.Close()\n}\n\nfunc (p *PGSQLProvider) reloadConfig() error {\n\treturn nil\n}\n\n// initializeDatabase creates the initial database structure\nfunc (p *PGSQLProvider) initializeDatabase() error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)\n\tif err == nil && dbVersion.Version > 0 {\n\t\treturn ErrNoInitRequired\n\t}\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn errSchemaVersionEmpty\n\t}\n\tlogger.InfoToConsole(\"creating initial database schema, version 33\")\n\tproviderLog(logger.LevelInfo, \"creating initial database schema, version 33\")\n\tvar initialSQL string\n\tif config.Driver == CockroachDataProviderName {\n\t\tinitialSQL = sqlReplaceAll(pgsqlInitial)\n\t\tinitialSQL = strings.ReplaceAll(initialSQL, \"GENERATED ALWAYS AS IDENTITY\", \"DEFAULT unordered_unique_rowid()\")\n\t} else {\n\t\tinitialSQL = sqlReplaceAll(pgsqlInitial + ipListsLikeIndex)\n\t}\n\n\treturn sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 33, true)\n}\n\nfunc (p *PGSQLProvider) migrateDatabase() error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch version := dbVersion.Version; {\n\tcase version == sqlDatabaseVersion:\n\t\tproviderLog(logger.LevelDebug, \"sql database is up to date, current version: %d\", version)\n\t\treturn ErrNoInitRequired\n\tcase version < 33:\n\t\terr = errSchemaVersionTooOld(version)\n\t\tproviderLog(logger.LevelError, \"%v\", err)\n\t\tlogger.ErrorToConsole(\"%v\", err)\n\t\treturn err\n\tcase version == 33:\n\t\treturn updatePGSQLDatabaseFromV33(p.dbHandle)\n\tdefault:\n\t\tif version > sqlDatabaseVersion {\n\t\t\tproviderLog(logger.LevelError, \"database schema version %d is newer than the supported one: %d\", version,\n\t\t\t\tsqlDatabaseVersion)\n\t\t\tlogger.WarnToConsole(\"database schema version %d is newer than the supported one: %d\", version,\n\t\t\t\tsqlDatabaseVersion)\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"database schema version not handled: %d\", version)\n\t}\n}\n\nfunc (p *PGSQLProvider) revertDatabase(targetVersion int) error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif dbVersion.Version == targetVersion {\n\t\treturn errors.New(\"current version match target version, nothing to do\")\n\t}\n\n\tswitch dbVersion.Version {\n\tcase 34:\n\t\treturn downgradePGSQLDatabaseFromV34(p.dbHandle)\n\tdefault:\n\t\treturn fmt.Errorf(\"database schema version not handled: %d\", dbVersion.Version)\n\t}\n}\n\nfunc (p *PGSQLProvider) resetDatabase() error {\n\tsql := sqlReplaceAll(pgsqlResetSQL)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)\n}\n\nfunc (p *PGSQLProvider) normalizeError(err error, fieldType int) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\tvar pgsqlErr *pgconn.PgError\n\tif errors.As(err, &pgsqlErr) {\n\t\tswitch pgsqlErr.Code {\n\t\tcase \"23505\":\n\t\t\tvar message string\n\t\t\tswitch fieldType {\n\t\t\tcase fieldUsername:\n\t\t\t\tmessage = util.I18nErrorDuplicatedUsername\n\t\t\tcase fieldIPNet:\n\t\t\t\tmessage = util.I18nErrorDuplicatedIPNet\n\t\t\tdefault:\n\t\t\t\tmessage = util.I18nErrorDuplicatedName\n\t\t\t}\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: %s\", ErrDuplicatedKey, err.Error()),\n\t\t\t\tmessage,\n\t\t\t)\n\t\tcase \"23503\":\n\t\t\treturn fmt.Errorf(\"%w: %s\", ErrForeignKeyViolated, err.Error())\n\t\t}\n\t}\n\treturn err\n}\n\nfunc updatePGSQLDatabaseFromV33(dbHandle *sql.DB) error {\n\treturn updatePGSQLDatabaseFrom33To34(dbHandle)\n}\n\nfunc downgradePGSQLDatabaseFromV34(dbHandle *sql.DB) error {\n\treturn downgradePGSQLDatabaseFrom34To33(dbHandle)\n}\n\nfunc updatePGSQLDatabaseFrom33To34(dbHandle *sql.DB) error {\n\tlogger.InfoToConsole(\"updating database schema version: 33 -> 34\")\n\tproviderLog(logger.LevelInfo, \"updating database schema version: 33 -> 34\")\n\n\tsql := strings.ReplaceAll(pgsqlV34SQL, \"{{prefix}}\", config.SQLTablesPrefix)\n\tsql = strings.ReplaceAll(sql, \"{{shares}}\", sqlTableShares)\n\tsql = strings.ReplaceAll(sql, \"{{shares_groups_mapping}}\", sqlTableSharesGroupsMapping)\n\tsql = strings.ReplaceAll(sql, \"{{groups}}\", sqlTableGroups)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 34, true)\n}\n\nfunc downgradePGSQLDatabaseFrom34To33(dbHandle *sql.DB) error {\n\tlogger.InfoToConsole(\"downgrading database schema version: 34 -> 33\")\n\tproviderLog(logger.LevelInfo, \"downgrading database schema version: 34 -> 33\")\n\n\tsql := strings.ReplaceAll(pgsqlV34DownSQL, \"{{shares_groups_mapping}}\", sqlTableSharesGroupsMapping)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 33, false)\n}\n"
  },
  {
    "path": "internal/dataprovider/pgsql_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build nopgsql\n\npackage dataprovider\n\nimport (\n\t\"errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-pgsql\")\n}\n\nfunc initializePGSQLProvider() error {\n\treturn errors.New(\"PostgreSQL disabled at build time\")\n}\n"
  },
  {
    "path": "internal/dataprovider/quotaupdater.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\nvar delayedQuotaUpdater quotaUpdater\n\nfunc init() {\n\tdelayedQuotaUpdater = newQuotaUpdater()\n}\n\ntype quotaObject struct {\n\tsize  int64\n\tfiles int\n}\n\ntype transferQuotaObject struct {\n\tulSize int64\n\tdlSize int64\n}\n\ntype quotaUpdater struct {\n\tparamsMutex sync.RWMutex\n\twaitTime    time.Duration\n\tsync.RWMutex\n\tpendingUserQuotaUpdates     map[string]quotaObject\n\tpendingFolderQuotaUpdates   map[string]quotaObject\n\tpendingTransferQuotaUpdates map[string]transferQuotaObject\n}\n\nfunc newQuotaUpdater() quotaUpdater {\n\treturn quotaUpdater{\n\t\tpendingUserQuotaUpdates:     make(map[string]quotaObject),\n\t\tpendingFolderQuotaUpdates:   make(map[string]quotaObject),\n\t\tpendingTransferQuotaUpdates: make(map[string]transferQuotaObject),\n\t}\n}\n\nfunc (q *quotaUpdater) start() {\n\tq.setWaitTime(config.DelayedQuotaUpdate)\n\n\tgo q.loop()\n}\n\nfunc (q *quotaUpdater) loop() {\n\twaitTime := q.getWaitTime()\n\tproviderLog(logger.LevelDebug, \"delayed quota update loop started, wait time: %v\", waitTime)\n\tfor waitTime > 0 {\n\t\t// We do this with a time.Sleep instead of a time.Ticker because we don't know\n\t\t// how long each quota processing cycle will take, and we want to make\n\t\t// sure we wait the configured seconds between each iteration\n\t\ttime.Sleep(waitTime)\n\t\tproviderLog(logger.LevelDebug, \"delayed quota update check start\")\n\t\tq.storeUsersQuota()\n\t\tq.storeFoldersQuota()\n\t\tq.storeUsersTransferQuota()\n\t\tproviderLog(logger.LevelDebug, \"delayed quota update check end\")\n\t\twaitTime = q.getWaitTime()\n\t}\n\tproviderLog(logger.LevelDebug, \"delayed quota update loop ended, wait time: %v\", waitTime)\n}\n\nfunc (q *quotaUpdater) setWaitTime(secs int) {\n\tq.paramsMutex.Lock()\n\tdefer q.paramsMutex.Unlock()\n\n\tq.waitTime = time.Duration(secs) * time.Second\n}\n\nfunc (q *quotaUpdater) getWaitTime() time.Duration {\n\tq.paramsMutex.RLock()\n\tdefer q.paramsMutex.RUnlock()\n\n\treturn q.waitTime\n}\n\nfunc (q *quotaUpdater) resetUserQuota(username string) {\n\tq.Lock()\n\tdefer q.Unlock()\n\n\tdelete(q.pendingUserQuotaUpdates, username)\n}\n\nfunc (q *quotaUpdater) updateUserQuota(username string, files int, size int64) {\n\tq.Lock()\n\tdefer q.Unlock()\n\n\tobj := q.pendingUserQuotaUpdates[username]\n\tobj.size += size\n\tobj.files += files\n\tif obj.files == 0 && obj.size == 0 {\n\t\tdelete(q.pendingUserQuotaUpdates, username)\n\t\treturn\n\t}\n\tq.pendingUserQuotaUpdates[username] = obj\n}\n\nfunc (q *quotaUpdater) getUserPendingQuota(username string) (int, int64) {\n\tq.RLock()\n\tdefer q.RUnlock()\n\n\tobj := q.pendingUserQuotaUpdates[username]\n\n\treturn obj.files, obj.size\n}\n\nfunc (q *quotaUpdater) resetFolderQuota(name string) {\n\tq.Lock()\n\tdefer q.Unlock()\n\n\tdelete(q.pendingFolderQuotaUpdates, name)\n}\n\nfunc (q *quotaUpdater) updateFolderQuota(name string, files int, size int64) {\n\tq.Lock()\n\tdefer q.Unlock()\n\n\tobj := q.pendingFolderQuotaUpdates[name]\n\tobj.size += size\n\tobj.files += files\n\tif obj.files == 0 && obj.size == 0 {\n\t\tdelete(q.pendingFolderQuotaUpdates, name)\n\t\treturn\n\t}\n\tq.pendingFolderQuotaUpdates[name] = obj\n}\n\nfunc (q *quotaUpdater) getFolderPendingQuota(name string) (int, int64) {\n\tq.RLock()\n\tdefer q.RUnlock()\n\n\tobj := q.pendingFolderQuotaUpdates[name]\n\n\treturn obj.files, obj.size\n}\n\nfunc (q *quotaUpdater) resetUserTransferQuota(username string) {\n\tq.Lock()\n\tdefer q.Unlock()\n\n\tdelete(q.pendingTransferQuotaUpdates, username)\n}\n\nfunc (q *quotaUpdater) updateUserTransferQuota(username string, ulSize, dlSize int64) {\n\tq.Lock()\n\tdefer q.Unlock()\n\n\tobj := q.pendingTransferQuotaUpdates[username]\n\tobj.ulSize += ulSize\n\tobj.dlSize += dlSize\n\tif obj.ulSize == 0 && obj.dlSize == 0 {\n\t\tdelete(q.pendingTransferQuotaUpdates, username)\n\t\treturn\n\t}\n\tq.pendingTransferQuotaUpdates[username] = obj\n}\n\nfunc (q *quotaUpdater) getUserPendingTransferQuota(username string) (int64, int64) {\n\tq.RLock()\n\tdefer q.RUnlock()\n\n\tobj := q.pendingTransferQuotaUpdates[username]\n\n\treturn obj.ulSize, obj.dlSize\n}\n\nfunc (q *quotaUpdater) getUsernames() []string {\n\tq.RLock()\n\tdefer q.RUnlock()\n\n\tresult := make([]string, 0, len(q.pendingUserQuotaUpdates))\n\tfor username := range q.pendingUserQuotaUpdates {\n\t\tresult = append(result, username)\n\t}\n\n\treturn result\n}\n\nfunc (q *quotaUpdater) getFoldernames() []string {\n\tq.RLock()\n\tdefer q.RUnlock()\n\n\tresult := make([]string, 0, len(q.pendingFolderQuotaUpdates))\n\tfor name := range q.pendingFolderQuotaUpdates {\n\t\tresult = append(result, name)\n\t}\n\n\treturn result\n}\n\nfunc (q *quotaUpdater) getTransferQuotaUsernames() []string {\n\tq.RLock()\n\tdefer q.RUnlock()\n\n\tresult := make([]string, 0, len(q.pendingTransferQuotaUpdates))\n\tfor username := range q.pendingTransferQuotaUpdates {\n\t\tresult = append(result, username)\n\t}\n\n\treturn result\n}\n\nfunc (q *quotaUpdater) storeUsersQuota() {\n\tfor _, username := range q.getUsernames() {\n\t\tfiles, size := q.getUserPendingQuota(username)\n\t\tif size != 0 || files != 0 {\n\t\t\terr := provider.updateQuota(username, files, size, false)\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelWarn, \"unable to update quota delayed for user %q: %v\", username, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tq.updateUserQuota(username, -files, -size)\n\t\t}\n\t}\n}\n\nfunc (q *quotaUpdater) storeFoldersQuota() {\n\tfor _, name := range q.getFoldernames() {\n\t\tfiles, size := q.getFolderPendingQuota(name)\n\t\tif size != 0 || files != 0 {\n\t\t\terr := provider.updateFolderQuota(name, files, size, false)\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelWarn, \"unable to update quota delayed for folder %q: %v\", name, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tq.updateFolderQuota(name, -files, -size)\n\t\t}\n\t}\n}\n\nfunc (q *quotaUpdater) storeUsersTransferQuota() {\n\tfor _, username := range q.getTransferQuotaUsernames() {\n\t\tulSize, dlSize := q.getUserPendingTransferQuota(username)\n\t\tif ulSize != 0 || dlSize != 0 {\n\t\t\terr := provider.updateTransferQuota(username, ulSize, dlSize, false)\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelWarn, \"unable to update transfer quota delayed for user %q: %v\", username, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tq.updateUserTransferQuota(username, -ulSize, -dlSize)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/dataprovider/role.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// Role defines an SFTPGo role.\ntype Role struct {\n\t// Data provider unique identifier\n\tID int64 `json:\"id\"`\n\t// Role name\n\tName string `json:\"name\"`\n\t// optional description\n\tDescription string `json:\"description,omitempty\"`\n\t// Creation time as unix timestamp in milliseconds\n\tCreatedAt int64 `json:\"created_at\"`\n\t// last update time as unix timestamp in milliseconds\n\tUpdatedAt int64 `json:\"updated_at\"`\n\t// list of admins associated with this role\n\tAdmins []string `json:\"admins,omitempty\"`\n\t// list of usernames associated with this role\n\tUsers []string `json:\"users,omitempty\"`\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (r *Role) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\trole, err := provider.roleExists(r.Name)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload role before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\treturn json.Marshal(role)\n\t}\n\treturn json.Marshal(r)\n}\n\nfunc (r *Role) validate() error {\n\tif r.Name == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"name is mandatory\"), util.I18nErrorNameRequired)\n\t}\n\tif !util.IsNameValid(r.Name) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif len(r.Name) > 255 {\n\t\treturn util.NewValidationError(\"name is too long, 255 is the maximum length allowed\")\n\t}\n\tif config.NamingRules&1 == 0 && !usernameRegex.MatchString(r.Name) {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~\", r.Name)),\n\t\t\tutil.I18nErrorInvalidName,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc (r *Role) getACopy() Role {\n\tusers := make([]string, len(r.Users))\n\tcopy(users, r.Users)\n\tadmins := make([]string, len(r.Admins))\n\tcopy(admins, r.Admins)\n\n\treturn Role{\n\t\tID:          r.ID,\n\t\tName:        r.Name,\n\t\tDescription: r.Description,\n\t\tCreatedAt:   r.CreatedAt,\n\t\tUpdatedAt:   r.UpdatedAt,\n\t\tUsers:       users,\n\t\tAdmins:      admins,\n\t}\n}\n"
  },
  {
    "path": "internal/dataprovider/scheduler.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/robfig/cron/v3\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tscheduler              *cron.Cron\n\tlastUserCacheUpdate    atomic.Int64\n\tlastIPListsCacheUpdate atomic.Int64\n\t// used for bolt and memory providers, so we avoid iterating all users/rules\n\t// to find recently modified ones\n\tlastUserUpdate atomic.Int64\n\tlastRuleUpdate atomic.Int64\n)\n\nfunc stopScheduler() {\n\tif scheduler != nil {\n\t\tscheduler.Stop()\n\t\tscheduler = nil\n\t}\n}\n\nfunc startScheduler() error {\n\tstopScheduler()\n\n\tscheduler = cron.New(cron.WithLocation(time.UTC), cron.WithLogger(cron.DiscardLogger))\n\t_, err := scheduler.AddFunc(\"@every 55s\", checkDataprovider)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to schedule dataprovider availability check: %w\", err)\n\t}\n\terr = addScheduledCacheUpdates()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif currentNode != nil {\n\t\t_, err = scheduler.AddFunc(\"@every 30m\", func() {\n\t\t\terr := provider.cleanupNodes()\n\t\t\tif err != nil {\n\t\t\t\tproviderLog(logger.LevelError, \"unable to cleanup nodes: %v\", err)\n\t\t\t} else {\n\t\t\t\tproviderLog(logger.LevelDebug, \"cleanup nodes ok\")\n\t\t\t}\n\t\t})\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to schedule nodes cleanup: %w\", err)\n\t}\n\tscheduler.Start()\n\treturn nil\n}\n\nfunc addScheduledCacheUpdates() error {\n\tlastUserCacheUpdate.Store(util.GetTimeAsMsSinceEpoch(time.Now()))\n\tlastIPListsCacheUpdate.Store(util.GetTimeAsMsSinceEpoch(time.Now()))\n\t_, err := scheduler.AddFunc(\"@every 10m\", checkCacheUpdates)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to schedule cache updates: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc checkDataprovider() {\n\tif currentNode != nil {\n\t\terr := provider.updateNodeTimestamp()\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to update node timestamp: %v\", err)\n\t\t} else {\n\t\t\tproviderLog(logger.LevelDebug, \"node timestamp updated\")\n\t\t}\n\t\tmetric.UpdateDataProviderAvailability(err)\n\t\treturn\n\t}\n\terr := provider.checkAvailability()\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"check availability error: %v\", err)\n\t}\n\tmetric.UpdateDataProviderAvailability(err)\n}\n\nfunc checkCacheUpdates() {\n\tcheckUserCache()\n\tcheckIPListEntryCache()\n\tcachedUserPasswords.cleanup()\n\tcachedAdminPasswords.cleanup()\n\tcachedAPIKeys.cleanup()\n}\n\nfunc checkUserCache() {\n\tlastCheck := lastUserCacheUpdate.Load()\n\tproviderLog(logger.LevelDebug, \"start user cache check, update time %v\", util.GetTimeFromMsecSinceEpoch(lastCheck))\n\tcheckTime := util.GetTimeAsMsSinceEpoch(time.Now())\n\tif config.IsShared == 1 {\n\t\tlastCheck -= 5000\n\t}\n\tusers, err := provider.getRecentlyUpdatedUsers(lastCheck)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get recently updated users: %v\", err)\n\t\treturn\n\t}\n\tfor idx := range users {\n\t\tuser := users[idx]\n\t\tproviderLog(logger.LevelDebug, \"invalidate caches for user %q\", user.Username)\n\t\tif user.DeletedAt > 0 {\n\t\t\tdeletedAt := util.GetTimeFromMsecSinceEpoch(user.DeletedAt)\n\t\t\tif deletedAt.Add(30 * time.Minute).Before(time.Now()) {\n\t\t\t\tproviderLog(logger.LevelDebug, \"removing user %q deleted at %s\", user.Username, deletedAt)\n\t\t\t\tgo provider.deleteUser(user, false) //nolint:errcheck\n\t\t\t}\n\t\t\twebDAVUsersCache.remove(user.Username)\n\t\t\tcachedUserPasswords.Remove(user.Username)\n\t\t\tdelayedQuotaUpdater.resetUserQuota(user.Username)\n\t\t} else {\n\t\t\twebDAVUsersCache.swap(&user, \"\")\n\t\t}\n\t}\n\tlastUserCacheUpdate.Store(checkTime)\n\tproviderLog(logger.LevelDebug, \"end user cache check, new update time %v\", util.GetTimeFromMsecSinceEpoch(lastUserCacheUpdate.Load()))\n}\n\nfunc checkIPListEntryCache() {\n\tif config.IsShared != 1 {\n\t\treturn\n\t}\n\thasMemoryLists := false\n\tfor _, l := range inMemoryLists {\n\t\tif l.isInMemory.Load() {\n\t\t\thasMemoryLists = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !hasMemoryLists {\n\t\treturn\n\t}\n\tproviderLog(logger.LevelDebug, \"start IP list cache check, update time %v\", util.GetTimeFromMsecSinceEpoch(lastIPListsCacheUpdate.Load()))\n\tcheckTime := util.GetTimeAsMsSinceEpoch(time.Now())\n\tentries, err := provider.getRecentlyUpdatedIPListEntries(lastIPListsCacheUpdate.Load() - 5000)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get recently updated IP list entries: %v\", err)\n\t\treturn\n\t}\n\tfor idx := range entries {\n\t\te := entries[idx]\n\t\tproviderLog(logger.LevelDebug, \"update cache for IP list entry %q\", e.getName())\n\t\tif e.DeletedAt > 0 {\n\t\t\tdeletedAt := util.GetTimeFromMsecSinceEpoch(e.DeletedAt)\n\t\t\tif deletedAt.Add(30 * time.Minute).Before(time.Now()) {\n\t\t\t\tproviderLog(logger.LevelDebug, \"removing IP list entry %q deleted at %s\", e.getName(), deletedAt)\n\t\t\t\tgo provider.deleteIPListEntry(e, false) //nolint:errcheck\n\t\t\t}\n\t\t\tfor _, l := range inMemoryLists {\n\t\t\t\tl.removeEntry(&e)\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, l := range inMemoryLists {\n\t\t\t\tl.updateEntry(&e)\n\t\t\t}\n\t\t}\n\t}\n\tlastIPListsCacheUpdate.Store(checkTime)\n\tproviderLog(logger.LevelDebug, \"end IP list entries cache check, new update time %v\", util.GetTimeFromMsecSinceEpoch(lastIPListsCacheUpdate.Load()))\n}\n\nfunc setLastUserUpdate() {\n\tlastUserUpdate.Store(util.GetTimeAsMsSinceEpoch(time.Now()))\n}\n\nfunc getLastUserUpdate() int64 {\n\treturn lastUserUpdate.Load()\n}\n\nfunc setLastRuleUpdate() {\n\tlastRuleUpdate.Store(util.GetTimeAsMsSinceEpoch(time.Now()))\n}\n\nfunc getLastRuleUpdate() int64 {\n\treturn lastRuleUpdate.Load()\n}\n"
  },
  {
    "path": "internal/dataprovider/session.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\n// SessionType defines the supported session types\ntype SessionType int\n\n// Supported session types\nconst (\n\tSessionTypeOIDCAuth SessionType = iota + 1\n\tSessionTypeOIDCToken\n\tSessionTypeResetCode\n\tSessionTypeOAuth2Auth\n\tSessionTypeInvalidToken\n\tSessionTypeWebTask\n)\n\n// Session defines a shared session persisted in the data provider\ntype Session struct {\n\tKey       string\n\tData      any\n\tType      SessionType\n\tTimestamp int64\n}\n\nfunc (s *Session) validate() error {\n\tif s.Key == \"\" {\n\t\treturn errors.New(\"unable to save a session with an empty key\")\n\t}\n\tif s.Type < SessionTypeOIDCAuth || s.Type > SessionTypeWebTask {\n\t\treturn fmt.Errorf(\"invalid session type: %v\", s.Type)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dataprovider/share.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alexedwards/argon2id\"\n\tpasswordvalidator \"github.com/wagslane/go-password-validator\"\n\t\"golang.org/x/crypto/bcrypt\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// ShareScope defines the supported share scopes\ntype ShareScope int\n\n// Supported share scopes\nconst (\n\tShareScopeRead ShareScope = iota + 1\n\tShareScopeWrite\n\tShareScopeReadWrite\n)\n\nconst (\n\tredactedPassword = \"[**redacted**]\"\n)\n\n// Share defines files and or directories shared with external users\ntype Share struct {\n\t// Database unique identifier\n\tID int64 `json:\"-\"`\n\t// Unique ID used to access this object\n\tShareID     string     `json:\"id\"`\n\tName        string     `json:\"name\"`\n\tDescription string     `json:\"description,omitempty\"`\n\tScope       ShareScope `json:\"scope\"`\n\t// Paths to files or directories, for ShareScopeWrite it must be exactly one directory\n\tPaths []string `json:\"paths\"`\n\t// Username who shared this object\n\tUsername  string `json:\"username\"`\n\tCreatedAt int64  `json:\"created_at\"`\n\tUpdatedAt int64  `json:\"updated_at\"`\n\t// 0 means never used\n\tLastUseAt int64 `json:\"last_use_at,omitempty\"`\n\t// ExpiresAt expiration date/time as unix timestamp in milliseconds, 0 means no expiration\n\tExpiresAt int64 `json:\"expires_at,omitempty\"`\n\t// Optional password to protect the share\n\tPassword string `json:\"password\"`\n\t// Limit the available access tokens, 0 means no limit\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\t// Used tokens\n\tUsedTokens int `json:\"used_tokens,omitempty\"`\n\t// Limit the share availability to these IPs/CIDR networks\n\tAllowFrom []string `json:\"allow_from,omitempty\"`\n\t// set for restores, we don't have to validate the expiration date\n\t// otherwise we fail to restore existing shares and we have to insert\n\t// all the previous values with no modifications\n\tIsRestore bool `json:\"-\"`\n}\n\n// IsExpired returns true if the share is expired\nfunc (s *Share) IsExpired() bool {\n\tif s.ExpiresAt > 0 {\n\t\treturn s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now())\n\t}\n\treturn false\n}\n\n// GetAllowedFromAsString returns the allowed IP as comma separated string\nfunc (s *Share) GetAllowedFromAsString() string {\n\treturn strings.Join(s.AllowFrom, \",\")\n}\n\n// IsPasswordHashed returns true if the password is hashed\nfunc (s *Share) IsPasswordHashed() bool {\n\treturn util.IsStringPrefixInSlice(s.Password, hashPwdPrefixes)\n}\n\nfunc (s *Share) getACopy() Share {\n\tallowFrom := make([]string, len(s.AllowFrom))\n\tcopy(allowFrom, s.AllowFrom)\n\n\treturn Share{\n\t\tID:          s.ID,\n\t\tShareID:     s.ShareID,\n\t\tName:        s.Name,\n\t\tDescription: s.Description,\n\t\tScope:       s.Scope,\n\t\tPaths:       s.Paths,\n\t\tUsername:    s.Username,\n\t\tCreatedAt:   s.CreatedAt,\n\t\tUpdatedAt:   s.UpdatedAt,\n\t\tLastUseAt:   s.LastUseAt,\n\t\tExpiresAt:   s.ExpiresAt,\n\t\tPassword:    s.Password,\n\t\tMaxTokens:   s.MaxTokens,\n\t\tUsedTokens:  s.UsedTokens,\n\t\tAllowFrom:   allowFrom,\n\t}\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (s *Share) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\tshare, err := provider.shareExists(s.ShareID, s.Username)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload share before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tshare.HideConfidentialData()\n\t\treturn json.Marshal(share)\n\t}\n\ts.HideConfidentialData()\n\treturn json.Marshal(s)\n}\n\n// HideConfidentialData hides share confidential data\nfunc (s *Share) HideConfidentialData() {\n\tif s.Password != \"\" {\n\t\ts.Password = redactedPassword\n\t}\n}\n\n// HasRedactedPassword returns true if this share has a redacted password\nfunc (s *Share) HasRedactedPassword() bool {\n\treturn s.Password == redactedPassword\n}\n\nfunc (s *Share) hashPassword() error {\n\tif s.Password != \"\" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {\n\t\tuser, err := GetUserWithGroupSettings(s.Username, \"\")\n\t\tif err != nil {\n\t\t\treturn util.NewGenericError(fmt.Sprintf(\"unable to validate user: %v\", err))\n\t\t}\n\t\tif minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {\n\t\t\tif err := passwordvalidator.Validate(s.Password, minEntropy); err != nil {\n\t\t\t\treturn util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)\n\t\t\t}\n\t\t}\n\t\tif config.PasswordHashing.Algo == HashingAlgoBcrypt {\n\t\t\thashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), config.PasswordHashing.BcryptOptions.Cost)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ts.Password = util.BytesToString(hashed)\n\t\t} else {\n\t\t\thashed, err := argon2id.CreateHash(s.Password, argon2Params)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ts.Password = hashed\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Share) validatePaths() error {\n\tvar paths []string\n\tfor _, p := range s.Paths {\n\t\tif strings.TrimSpace(p) != \"\" {\n\t\t\tpaths = append(paths, p)\n\t\t}\n\t}\n\ts.Paths = paths\n\tif len(s.Paths) == 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"at least a shared path is required\"), util.I18nErrorSharePathRequired)\n\t}\n\tfor idx := range s.Paths {\n\t\ts.Paths[idx] = util.CleanPath(s.Paths[idx])\n\t}\n\ts.Paths = util.RemoveDuplicates(s.Paths, false)\n\tif s.Scope >= ShareScopeWrite && len(s.Paths) != 1 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"the write share scope requires exactly one path\"), util.I18nErrorShareWriteScope)\n\t}\n\t// check nested paths\n\tif len(s.Paths) > 1 {\n\t\tfor idx := range s.Paths {\n\t\t\tfor innerIdx := range s.Paths {\n\t\t\t\tif idx == innerIdx {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif s.Paths[idx] == \"/\" || s.Paths[innerIdx] == \"/\" || util.IsDirOverlapped(s.Paths[idx], s.Paths[innerIdx], true, \"/\") {\n\t\t\t\t\treturn util.NewI18nError(util.NewGenericError(\"shared paths cannot be nested\"), util.I18nErrorShareNestedPaths)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Share) validate() error { //nolint:gocyclo\n\tif s.ShareID == \"\" {\n\t\treturn util.NewValidationError(\"share_id is mandatory\")\n\t}\n\tif s.Name == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"name is mandatory\"), util.I18nErrorNameRequired)\n\t}\n\tif !util.IsNameValid(s.Name) {\n\t\treturn util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)\n\t}\n\tif s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite {\n\t\treturn util.NewI18nError(util.NewValidationError(fmt.Sprintf(\"invalid scope: %v\", s.Scope)), util.I18nErrorShareScope)\n\t}\n\tif err := s.validatePaths(); err != nil {\n\t\treturn err\n\t}\n\tif s.ExpiresAt > 0 {\n\t\tif !s.IsRestore && s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {\n\t\t\treturn util.NewI18nError(util.NewValidationError(\"expiration must be in the future\"), util.I18nErrorShareExpirationPast)\n\t\t}\n\t} else {\n\t\ts.ExpiresAt = 0\n\t}\n\tif s.MaxTokens < 0 {\n\t\treturn util.NewI18nError(util.NewValidationError(\"invalid max tokens\"), util.I18nErrorShareMaxTokens)\n\t}\n\tif s.Username == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"username is mandatory\"), util.I18nErrorUsernameRequired)\n\t}\n\tif s.HasRedactedPassword() {\n\t\treturn util.NewValidationError(\"cannot save a share with a redacted password\")\n\t}\n\tif err := s.hashPassword(); err != nil {\n\t\treturn err\n\t}\n\ts.AllowFrom = util.RemoveDuplicates(s.AllowFrom, false)\n\tfor _, IPMask := range s.AllowFrom {\n\t\t_, _, err := net.ParseCIDR(IPMask)\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not parse allow from entry %q : %v\", IPMask, err)),\n\t\t\t\tutil.I18nErrorInvalidIPMask,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\n// CheckCredentials verifies the share credentials if a password if set\nfunc (s *Share) CheckCredentials(password string) (bool, error) {\n\tif s.Password == \"\" {\n\t\treturn true, nil\n\t}\n\tif password == \"\" {\n\t\treturn false, ErrInvalidCredentials\n\t}\n\tif strings.HasPrefix(s.Password, bcryptPwdPrefix) {\n\t\tif err := bcrypt.CompareHashAndPassword([]byte(s.Password), []byte(password)); err != nil {\n\t\t\treturn false, ErrInvalidCredentials\n\t\t}\n\t\treturn true, nil\n\t}\n\tmatch, err := argon2id.ComparePasswordAndHash(password, s.Password)\n\tif !match || err != nil {\n\t\treturn false, ErrInvalidCredentials\n\t}\n\treturn match, err\n}\n\n// GetRelativePath returns the specified absolute path as relative to the share base path\nfunc (s *Share) GetRelativePath(name string) string {\n\tif len(s.Paths) == 0 {\n\t\treturn \"\"\n\t}\n\treturn util.CleanPath(strings.TrimPrefix(name, s.Paths[0]))\n}\n\n// IsUsable checks if the share is usable from the specified IP\nfunc (s *Share) IsUsable(ip string) (bool, error) {\n\tif s.MaxTokens > 0 && s.UsedTokens >= s.MaxTokens {\n\t\treturn false, util.NewI18nError(util.NewRecordNotFoundError(\"max share usage exceeded\"), util.I18nErrorShareUsage)\n\t}\n\tif s.ExpiresAt > 0 {\n\t\tif s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {\n\t\t\treturn false, util.NewI18nError(util.NewRecordNotFoundError(\"share expired\"), util.I18nErrorShareExpired)\n\t\t}\n\t}\n\tif len(s.AllowFrom) == 0 {\n\t\treturn true, nil\n\t}\n\tparsedIP := net.ParseIP(ip)\n\tif parsedIP == nil {\n\t\treturn false, util.NewI18nError(ErrLoginNotAllowedFromIP, util.I18nErrorLoginFromIPDenied)\n\t}\n\tfor _, ipMask := range s.AllowFrom {\n\t\t_, network, err := net.ParseCIDR(ipMask)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif network.Contains(parsedIP) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, util.NewI18nError(ErrLoginNotAllowedFromIP, util.I18nErrorLoginFromIPDenied)\n}\n"
  },
  {
    "path": "internal/dataprovider/sqlcommon.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/cockroach-go/v2/crdb\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tsqlDatabaseVersion     = 34\n\tdefaultSQLQueryTimeout = 10 * time.Second\n\tlongSQLQueryTimeout    = 60 * time.Second\n)\n\nvar (\n\terrSQLFoldersAssociation = errors.New(\"unable to associate virtual folders to user\")\n\terrSQLGroupsAssociation  = errors.New(\"unable to associate groups to user\")\n\terrSQLUsersAssociation   = errors.New(\"unable to associate users to group\")\n\terrSchemaVersionEmpty    = errors.New(\"we can't determine schema version because the schema_migration table is empty. The SFTPGo database might be corrupted. Consider using the \\\"resetprovider\\\" sub-command\")\n)\n\ntype sqlQuerier interface {\n\tQueryRowContext(ctx context.Context, query string, args ...any) *sql.Row\n\tQueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)\n\tExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)\n\tPrepareContext(ctx context.Context, query string) (*sql.Stmt, error)\n}\n\ntype sqlScanner interface {\n\tScan(dest ...any) error\n}\n\nfunc sqlReplaceAll(sql string) string {\n\tsql = strings.ReplaceAll(sql, \"{{schema_version}}\", sqlTableSchemaVersion)\n\tsql = strings.ReplaceAll(sql, \"{{admins}}\", sqlTableAdmins)\n\tsql = strings.ReplaceAll(sql, \"{{folders}}\", sqlTableFolders)\n\tsql = strings.ReplaceAll(sql, \"{{users}}\", sqlTableUsers)\n\tsql = strings.ReplaceAll(sql, \"{{groups}}\", sqlTableGroups)\n\tsql = strings.ReplaceAll(sql, \"{{users_folders_mapping}}\", sqlTableUsersFoldersMapping)\n\tsql = strings.ReplaceAll(sql, \"{{users_groups_mapping}}\", sqlTableUsersGroupsMapping)\n\tsql = strings.ReplaceAll(sql, \"{{admins_groups_mapping}}\", sqlTableAdminsGroupsMapping)\n\tsql = strings.ReplaceAll(sql, \"{{groups_folders_mapping}}\", sqlTableGroupsFoldersMapping)\n\tsql = strings.ReplaceAll(sql, \"{{api_keys}}\", sqlTableAPIKeys)\n\tsql = strings.ReplaceAll(sql, \"{{shares}}\", sqlTableShares)\n\tsql = strings.ReplaceAll(sql, \"{{shares_groups_mapping}}\", sqlTableSharesGroupsMapping)\n\tsql = strings.ReplaceAll(sql, \"{{defender_events}}\", sqlTableDefenderEvents)\n\tsql = strings.ReplaceAll(sql, \"{{defender_hosts}}\", sqlTableDefenderHosts)\n\tsql = strings.ReplaceAll(sql, \"{{active_transfers}}\", sqlTableActiveTransfers)\n\tsql = strings.ReplaceAll(sql, \"{{shared_sessions}}\", sqlTableSharedSessions)\n\tsql = strings.ReplaceAll(sql, \"{{events_actions}}\", sqlTableEventsActions)\n\tsql = strings.ReplaceAll(sql, \"{{events_rules}}\", sqlTableEventsRules)\n\tsql = strings.ReplaceAll(sql, \"{{rules_actions_mapping}}\", sqlTableRulesActionsMapping)\n\tsql = strings.ReplaceAll(sql, \"{{tasks}}\", sqlTableTasks)\n\tsql = strings.ReplaceAll(sql, \"{{nodes}}\", sqlTableNodes)\n\tsql = strings.ReplaceAll(sql, \"{{roles}}\", sqlTableRoles)\n\tsql = strings.ReplaceAll(sql, \"{{ip_lists}}\", sqlTableIPLists)\n\tsql = strings.ReplaceAll(sql, \"{{configs}}\", sqlTableConfigs)\n\tsql = strings.ReplaceAll(sql, \"{{prefix}}\", config.SQLTablesPrefix)\n\treturn sql\n}\n\nfunc sqlCommonGetShareByID(shareID, username string, dbHandle sqlQuerier) (Share, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tfilterUser := username != \"\"\n\tq := getShareByIDQuery(filterUser)\n\n\tvar row *sql.Row\n\tif filterUser {\n\t\trow = dbHandle.QueryRowContext(ctx, q, shareID, username)\n\t} else {\n\t\trow = dbHandle.QueryRowContext(ctx, q, shareID)\n\t}\n\n\treturn getShareFromDbRow(row)\n}\n\nfunc sqlCommonAddShare(share *Share, dbHandle *sql.DB) error {\n\terr := share.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuser, err := provider.userExists(share.Username, \"\")\n\tif err != nil {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"unable to validate user %q\", share.Username))\n\t}\n\n\tpaths, err := json.Marshal(share.Paths)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar allowFrom []byte\n\tif len(share.AllowFrom) > 0 {\n\t\tres, err := json.Marshal(share.AllowFrom)\n\t\tif err == nil {\n\t\t\tallowFrom = res\n\t\t}\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddShareQuery()\n\tusedTokens := 0\n\tcreatedAt := util.GetTimeAsMsSinceEpoch(time.Now())\n\tupdatedAt := createdAt\n\tlastUseAt := int64(0)\n\tif share.IsRestore {\n\t\tusedTokens = share.UsedTokens\n\t\tif share.CreatedAt > 0 {\n\t\t\tcreatedAt = share.CreatedAt\n\t\t}\n\t\tif share.UpdatedAt > 0 {\n\t\t\tupdatedAt = share.UpdatedAt\n\t\t}\n\t\tlastUseAt = share.LastUseAt\n\t}\n\t_, err = dbHandle.ExecContext(ctx, q, share.ShareID, share.Name, share.Description, share.Scope,\n\t\tpaths, createdAt, updatedAt, lastUseAt, share.ExpiresAt, share.Password,\n\t\tshare.MaxTokens, usedTokens, allowFrom, user.ID)\n\treturn err\n}\n\nfunc sqlCommonUpdateShare(share *Share, dbHandle *sql.DB) error {\n\terr := share.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpaths, err := json.Marshal(share.Paths)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar allowFrom []byte\n\tif len(share.AllowFrom) > 0 {\n\t\tres, err := json.Marshal(share.AllowFrom)\n\t\tif err == nil {\n\t\t\tallowFrom = res\n\t\t}\n\t}\n\n\tuser, err := provider.userExists(share.Username, \"\")\n\tif err != nil {\n\t\treturn util.NewGenericError(fmt.Sprintf(\"unable to validate user %q\", share.Username))\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tvar q string\n\tif share.IsRestore {\n\t\tq = getUpdateShareRestoreQuery()\n\t} else {\n\t\tq = getUpdateShareQuery()\n\t}\n\n\tvar res sql.Result\n\tif share.IsRestore {\n\t\tif share.CreatedAt == 0 {\n\t\t\tshare.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t}\n\t\tif share.UpdatedAt == 0 {\n\t\t\tshare.UpdatedAt = share.CreatedAt\n\t\t}\n\t\tres, err = dbHandle.ExecContext(ctx, q, share.Name, share.Description, share.Scope, paths,\n\t\t\tshare.CreatedAt, share.UpdatedAt, share.LastUseAt, share.ExpiresAt, share.Password, share.MaxTokens,\n\t\t\tshare.UsedTokens, allowFrom, user.ID, share.ShareID)\n\t} else {\n\t\tres, err = dbHandle.ExecContext(ctx, q, share.Name, share.Description, share.Scope, paths,\n\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()), share.ExpiresAt, share.Password, share.MaxTokens,\n\t\t\tallowFrom, user.ID, share.ShareID)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonDeleteShare(share Share, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteShareQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, share.ShareID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonGetShares(limit, offset int, order, username string, dbHandle sqlQuerier) ([]Share, error) {\n\tshares := make([]Share, 0, limit)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getSharesQuery(order)\n\trows, err := dbHandle.QueryContext(ctx, q, username, limit, offset)\n\tif err != nil {\n\t\treturn shares, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\ts, err := getShareFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn shares, err\n\t\t}\n\t\ts.HideConfidentialData()\n\t\tshares = append(shares, s)\n\t}\n\n\treturn shares, rows.Err()\n}\n\nfunc sqlCommonDumpShares(dbHandle sqlQuerier) ([]Share, error) {\n\tshares := make([]Share, 0, 30)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpSharesQuery()\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn shares, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\ts, err := getShareFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn shares, err\n\t\t}\n\t\tshares = append(shares, s)\n\t}\n\n\treturn shares, rows.Err()\n}\n\nfunc sqlCommonGetAPIKeyByID(keyID string, dbHandle sqlQuerier) (APIKey, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAPIKeyByIDQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, keyID)\n\n\tapiKey, err := getAPIKeyFromDbRow(row)\n\tif err != nil {\n\t\treturn apiKey, err\n\t}\n\treturn getAPIKeyWithRelatedFields(ctx, apiKey, dbHandle)\n}\n\nfunc sqlCommonAddAPIKey(apiKey *APIKey, dbHandle *sql.DB) error {\n\terr := apiKey.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserID, adminID, err := sqlCommonGetAPIKeyRelatedIDs(apiKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddAPIKeyQuery()\n\t_, err = dbHandle.ExecContext(ctx, q, apiKey.KeyID, apiKey.Name, apiKey.Key, apiKey.Scope,\n\t\tutil.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(time.Now()), apiKey.LastUseAt,\n\t\tapiKey.ExpiresAt, apiKey.Description, userID, adminID)\n\treturn err\n}\n\nfunc sqlCommonUpdateAPIKey(apiKey *APIKey, dbHandle *sql.DB) error {\n\terr := apiKey.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserID, adminID, err := sqlCommonGetAPIKeyRelatedIDs(apiKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateAPIKeyQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, apiKey.Name, apiKey.Scope, apiKey.ExpiresAt, userID, adminID,\n\t\tapiKey.Description, util.GetTimeAsMsSinceEpoch(time.Now()), apiKey.KeyID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonDeleteAPIKey(apiKey APIKey, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteAPIKeyQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, apiKey.KeyID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonGetAPIKeys(limit, offset int, order string, dbHandle sqlQuerier) ([]APIKey, error) {\n\tapiKeys := make([]APIKey, 0, limit)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAPIKeysQuery(order)\n\trows, err := dbHandle.QueryContext(ctx, q, limit, offset)\n\tif err != nil {\n\t\treturn apiKeys, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tk, err := getAPIKeyFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn apiKeys, err\n\t\t}\n\t\tk.HideConfidentialData()\n\t\tapiKeys = append(apiKeys, k)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn apiKeys, err\n\t}\n\tapiKeys, err = getRelatedValuesForAPIKeys(ctx, apiKeys, dbHandle, APIKeyScopeAdmin)\n\tif err != nil {\n\t\treturn apiKeys, err\n\t}\n\n\treturn getRelatedValuesForAPIKeys(ctx, apiKeys, dbHandle, APIKeyScopeUser)\n}\n\nfunc sqlCommonDumpAPIKeys(dbHandle sqlQuerier) ([]APIKey, error) {\n\tapiKeys := make([]APIKey, 0, 30)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpAPIKeysQuery()\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn apiKeys, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tk, err := getAPIKeyFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn apiKeys, err\n\t\t}\n\t\tapiKeys = append(apiKeys, k)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn apiKeys, err\n\t}\n\tapiKeys, err = getRelatedValuesForAPIKeys(ctx, apiKeys, dbHandle, APIKeyScopeAdmin)\n\tif err != nil {\n\t\treturn apiKeys, err\n\t}\n\n\treturn getRelatedValuesForAPIKeys(ctx, apiKeys, dbHandle, APIKeyScopeUser)\n}\n\nfunc sqlCommonGetAdminByUsername(username string, dbHandle sqlQuerier) (Admin, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAdminByUsernameQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, username)\n\n\tadmin, err := getAdminFromDbRow(row)\n\tif err != nil {\n\t\treturn admin, err\n\t}\n\treturn getAdminWithGroups(ctx, admin, dbHandle)\n}\n\nfunc sqlCommonValidateAdminAndPass(username, password, ip string, dbHandle *sql.DB) (Admin, error) {\n\tadmin, err := sqlCommonGetAdminByUsername(username, dbHandle)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating admin %q: %v\", username, err)\n\t\treturn admin, err\n\t}\n\terr = admin.checkUserAndPass(password, ip)\n\treturn admin, err\n}\n\nfunc sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {\n\terr := admin.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tperms, err := json.Marshal(admin.Permissions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfilters, err := json.Marshal(admin.Filters)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tq := getAddAdminQuery(admin.Role)\n\t\t_, err = tx.ExecContext(ctx, q, admin.Username, admin.Password, admin.Status, admin.Email, perms,\n\t\t\tfilters, admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()), admin.Role)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn generateAdminGroupMapping(ctx, admin, tx)\n\t})\n}\n\nfunc sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {\n\terr := admin.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tperms, err := json.Marshal(admin.Permissions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfilters, err := json.Marshal(admin.Filters)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tq := getUpdateAdminQuery(admin.Role)\n\t\t_, err = tx.ExecContext(ctx, q, admin.Password, admin.Status, admin.Email, perms, filters,\n\t\t\tadmin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()), admin.Role, admin.Username)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn generateAdminGroupMapping(ctx, admin, tx)\n\t})\n}\n\nfunc sqlCommonDeleteAdmin(admin Admin, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteAdminQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, admin.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonGetAdmins(limit, offset int, order string, dbHandle sqlQuerier) ([]Admin, error) {\n\tadmins := make([]Admin, 0, limit)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAdminsQuery(order)\n\trows, err := dbHandle.QueryContext(ctx, q, limit, offset)\n\tif err != nil {\n\t\treturn admins, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\ta, err := getAdminFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn admins, err\n\t\t}\n\t\ta.HideConfidentialData()\n\t\tadmins = append(admins, a)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn admins, err\n\t}\n\treturn getAdminsWithGroups(ctx, admins, dbHandle)\n}\n\nfunc sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {\n\tadmins := make([]Admin, 0, 30)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpAdminsQuery()\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn admins, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\ta, err := getAdminFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn admins, err\n\t\t}\n\t\tadmins = append(admins, a)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn admins, err\n\t}\n\treturn getAdminsWithGroups(ctx, admins, dbHandle)\n}\n\nfunc sqlCommonGetIPListEntry(ipOrNet string, listType IPListType, dbHandle sqlQuerier) (IPListEntry, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getIPListEntryQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, listType, ipOrNet)\n\treturn getIPListEntryFromDbRow(row)\n}\n\nfunc sqlCommonDumpIPListEntries(dbHandle *sql.DB) ([]IPListEntry, error) {\n\tcount, err := sqlCommonCountIPListEntries(0, dbHandle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif count > ipListMemoryLimit {\n\t\tproviderLog(logger.LevelInfo, \"IP lists excluded from dump, too many entries: %d\", count)\n\t\treturn nil, nil\n\t}\n\tentries := make([]IPListEntry, 0, 100)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpListEntriesQuery()\n\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn entries, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tentry, err := getIPListEntryFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn entries, err\n\t\t}\n\t\tentries = append(entries, entry)\n\t}\n\treturn entries, rows.Err()\n}\n\nfunc sqlCommonCountIPListEntries(listType IPListType, dbHandle *sql.DB) (int64, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tvar q string\n\tvar args []any\n\tif listType == 0 {\n\t\tq = getCountAllIPListEntriesQuery()\n\t} else {\n\t\tq = getCountIPListEntriesQuery()\n\t\targs = append(args, listType)\n\t}\n\tvar count int64\n\terr := dbHandle.QueryRowContext(ctx, q, args...).Scan(&count)\n\treturn count, err\n}\n\nfunc sqlCommonGetIPListEntries(listType IPListType, filter, from, order string, limit int, dbHandle sqlQuerier) ([]IPListEntry, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getIPListEntriesQuery(filter, from, order, limit)\n\targs := []any{listType}\n\tif from != \"\" {\n\t\targs = append(args, from)\n\t}\n\tif filter != \"\" {\n\t\targs = append(args, filter+\"%\")\n\t}\n\tif limit > 0 {\n\t\targs = append(args, limit)\n\t}\n\tentries := make([]IPListEntry, 0, limit)\n\trows, err := dbHandle.QueryContext(ctx, q, args...)\n\tif err != nil {\n\t\treturn entries, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tentry, err := getIPListEntryFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn entries, err\n\t\t}\n\t\tentries = append(entries, entry)\n\t}\n\treturn entries, rows.Err()\n}\n\nfunc sqlCommonGetRecentlyUpdatedIPListEntries(after int64, dbHandle sqlQuerier) ([]IPListEntry, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getRecentlyUpdatedIPListQuery()\n\tentries := make([]IPListEntry, 0, 5)\n\trows, err := dbHandle.QueryContext(ctx, q, after)\n\tif err != nil {\n\t\treturn entries, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tentry, err := getIPListEntryFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn entries, err\n\t\t}\n\t\tentries = append(entries, entry)\n\t}\n\treturn entries, rows.Err()\n}\n\nfunc sqlCommonGetListEntriesForIP(ip string, listType IPListType, dbHandle sqlQuerier) ([]IPListEntry, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tvar rows *sql.Rows\n\tvar err error\n\n\tentries := make([]IPListEntry, 0, 2)\n\tif config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {\n\t\trows, err = dbHandle.QueryContext(ctx, getIPListEntriesForIPQueryPg(), listType, ip)\n\t\tif err != nil {\n\t\t\treturn entries, err\n\t\t}\n\t} else {\n\t\tipAddr, err := netip.ParseAddr(ip)\n\t\tif err != nil {\n\t\t\treturn entries, fmt.Errorf(\"invalid ip address %s\", ip)\n\t\t}\n\t\tvar netType int\n\t\tvar ipBytes []byte\n\t\tif ipAddr.Is4() || ipAddr.Is4In6() {\n\t\t\tnetType = ipTypeV4\n\t\t\tas4 := ipAddr.As4()\n\t\t\tipBytes = as4[:]\n\t\t} else {\n\t\t\tnetType = ipTypeV6\n\t\t\tas16 := ipAddr.As16()\n\t\t\tipBytes = as16[:]\n\t\t}\n\t\trows, err = dbHandle.QueryContext(ctx, getIPListEntriesForIPQueryNoPg(), listType, netType, ipBytes)\n\t\tif err != nil {\n\t\t\treturn entries, err\n\t\t}\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tentry, err := getIPListEntryFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn entries, err\n\t\t}\n\t\tentries = append(entries, entry)\n\t}\n\treturn entries, rows.Err()\n}\n\nfunc sqlCommonAddIPListEntry(entry *IPListEntry, dbHandle *sql.DB) error {\n\tif err := entry.validate(); err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tvar err error\n\tq := getAddIPListEntryQuery()\n\tfirst := entry.getFirst()\n\tlast := entry.getLast()\n\tvar netType int\n\tif first.Is4() {\n\t\tnetType = ipTypeV4\n\t} else {\n\t\tnetType = ipTypeV6\n\t}\n\tif config.IsShared == 1 {\n\t\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\t\t_, err := tx.ExecContext(ctx, getRemoveSoftDeletedIPListEntryQuery(), entry.Type, entry.IPOrNet)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {\n\t\t\t\t_, err = tx.ExecContext(ctx, q, entry.Type, entry.IPOrNet, first.String(), last.String(),\n\t\t\t\t\tnetType, entry.Protocols, entry.Description, entry.Mode, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()))\n\t\t\t} else {\n\t\t\t\t_, err = tx.ExecContext(ctx, q, entry.Type, entry.IPOrNet, entry.First, entry.Last,\n\t\t\t\t\tnetType, entry.Protocols, entry.Description, entry.Mode, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()))\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t}\n\tif config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {\n\t\t_, err = dbHandle.ExecContext(ctx, q, entry.Type, entry.IPOrNet, first.String(), last.String(),\n\t\t\tnetType, entry.Protocols, entry.Description, entry.Mode, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()))\n\t} else {\n\t\t_, err = dbHandle.ExecContext(ctx, q, entry.Type, entry.IPOrNet, entry.First, entry.Last,\n\t\t\tnetType, entry.Protocols, entry.Description, entry.Mode, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()))\n\t}\n\treturn err\n}\n\nfunc sqlCommonUpdateIPListEntry(entry *IPListEntry, dbHandle *sql.DB) error {\n\tif err := entry.validate(); err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateIPListEntryQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, entry.Mode, entry.Protocols, entry.Description,\n\t\tutil.GetTimeAsMsSinceEpoch(time.Now()), entry.Type, entry.IPOrNet)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonDeleteIPListEntry(entry IPListEntry, softDelete bool, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteIPListEntryQuery(softDelete)\n\tvar args []any\n\tif softDelete {\n\t\tts := util.GetTimeAsMsSinceEpoch(time.Now())\n\t\targs = append(args, ts, ts)\n\t}\n\targs = append(args, entry.Type, entry.IPOrNet)\n\tres, err := dbHandle.ExecContext(ctx, q, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonGetRoleByName(name string, dbHandle sqlQuerier) (Role, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getRoleByNameQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, name)\n\trole, err := getRoleFromDbRow(row)\n\tif err != nil {\n\t\treturn role, err\n\t}\n\trole, err = getRoleWithUsers(ctx, role, dbHandle)\n\tif err != nil {\n\t\treturn role, err\n\t}\n\treturn getRoleWithAdmins(ctx, role, dbHandle)\n}\n\nfunc sqlCommonDumpRoles(dbHandle sqlQuerier) ([]Role, error) {\n\troles := make([]Role, 0, 10)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpRolesQuery()\n\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn roles, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\trole, err := getRoleFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn roles, err\n\t\t}\n\t\troles = append(roles, role)\n\t}\n\treturn roles, rows.Err()\n}\n\nfunc sqlCommonGetRoles(limit int, offset int, order string, minimal bool, dbHandle sqlQuerier) ([]Role, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getRolesQuery(order, minimal)\n\n\troles := make([]Role, 0, limit)\n\trows, err := dbHandle.QueryContext(ctx, q, limit, offset)\n\tif err != nil {\n\t\treturn roles, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar role Role\n\t\tif minimal {\n\t\t\terr = rows.Scan(&role.ID, &role.Name)\n\t\t} else {\n\t\t\trole, err = getRoleFromDbRow(rows)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn roles, err\n\t\t}\n\t\troles = append(roles, role)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn roles, err\n\t}\n\tif minimal {\n\t\treturn roles, nil\n\t}\n\troles, err = getRolesWithUsers(ctx, roles, dbHandle)\n\tif err != nil {\n\t\treturn roles, err\n\t}\n\treturn getRolesWithAdmins(ctx, roles, dbHandle)\n}\n\nfunc sqlCommonAddRole(role *Role, dbHandle *sql.DB) error {\n\tif err := role.validate(); err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddRoleQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, role.Name, role.Description, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tutil.GetTimeAsMsSinceEpoch(time.Now()))\n\treturn err\n}\n\nfunc sqlCommonUpdateRole(role *Role, dbHandle *sql.DB) error {\n\tif err := role.validate(); err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateRoleQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, role.Description, util.GetTimeAsMsSinceEpoch(time.Now()), role.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonDeleteRole(role Role, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteRoleQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, role.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getGroupByNameQuery()\n\n\trow := dbHandle.QueryRowContext(ctx, q, name)\n\tgroup, err := getGroupFromDbRow(row)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tgroup, err = getGroupWithVirtualFolders(ctx, group, dbHandle)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tgroup, err = getGroupWithUsers(ctx, group, dbHandle)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\treturn getGroupWithAdmins(ctx, group, dbHandle)\n}\n\nfunc sqlCommonDumpGroups(dbHandle sqlQuerier) ([]Group, error) {\n\tgroups := make([]Group, 0, 50)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpGroupsQuery()\n\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tgroup, err := getGroupFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn groups, err\n\t\t}\n\t\tgroups = append(groups, group)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\treturn getGroupsWithVirtualFolders(ctx, groups, dbHandle)\n}\n\nfunc sqlCommonGetUsersInGroups(names []string, dbHandle sqlQuerier) ([]string, error) {\n\tif len(names) == 0 {\n\t\treturn nil, nil\n\t}\n\tmaxNames := len(sqlPlaceholders)\n\tusernames := make([]string, 0, len(names))\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tfor len(names) > 0 {\n\t\tif maxNames > len(names) {\n\t\t\tmaxNames = len(names)\n\t\t}\n\n\t\tq := getUsersInGroupsQuery(maxNames)\n\t\targs := make([]any, 0, maxNames)\n\t\tfor _, name := range names[:maxNames] {\n\t\t\targs = append(args, name)\n\t\t}\n\n\t\trows, err := dbHandle.QueryContext(ctx, q, args...)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer rows.Close()\n\n\t\tfor rows.Next() {\n\t\t\tvar username string\n\t\t\terr = rows.Scan(&username)\n\t\t\tif err != nil {\n\t\t\t\treturn usernames, err\n\t\t\t}\n\t\t\tusernames = append(usernames, username)\n\t\t}\n\t\terr = rows.Err()\n\t\tif err != nil {\n\t\t\treturn usernames, err\n\t\t}\n\t\tnames = names[maxNames:]\n\t}\n\treturn usernames, nil\n}\n\nfunc sqlCommonGetGroupsWithNames(names []string, dbHandle sqlQuerier) ([]Group, error) {\n\tif len(names) == 0 {\n\t\treturn nil, nil\n\t}\n\tmaxNames := len(sqlPlaceholders)\n\tgroups := make([]Group, 0, len(names))\n\tfor len(names) > 0 {\n\t\tif maxNames > len(names) {\n\t\t\tmaxNames = len(names)\n\t\t}\n\t\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\t\tdefer cancel()\n\n\t\tq := getGroupsWithNamesQuery(maxNames)\n\t\targs := make([]any, 0, maxNames)\n\t\tfor _, name := range names[:maxNames] {\n\t\t\targs = append(args, name)\n\t\t}\n\t\trows, err := dbHandle.QueryContext(ctx, q, args...)\n\t\tif err != nil {\n\t\t\treturn groups, err\n\t\t}\n\t\tdefer rows.Close()\n\n\t\tfor rows.Next() {\n\t\t\tgroup, err := getGroupFromDbRow(rows)\n\t\t\tif err != nil {\n\t\t\t\treturn groups, err\n\t\t\t}\n\t\t\tgroups = append(groups, group)\n\t\t}\n\t\terr = rows.Err()\n\t\tif err != nil {\n\t\t\treturn groups, err\n\t\t}\n\t\tnames = names[maxNames:]\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn getGroupsWithVirtualFolders(ctx, groups, dbHandle)\n}\n\nfunc sqlCommonGetGroups(limit int, offset int, order string, minimal bool, dbHandle sqlQuerier) ([]Group, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getGroupsQuery(order, minimal)\n\n\tgroups := make([]Group, 0, limit)\n\trows, err := dbHandle.QueryContext(ctx, q, limit, offset)\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar group Group\n\t\tif minimal {\n\t\t\terr = rows.Scan(&group.ID, &group.Name)\n\t\t} else {\n\t\t\tgroup, err = getGroupFromDbRow(rows)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn groups, err\n\t\t}\n\t\tgroups = append(groups, group)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tif minimal {\n\t\treturn groups, nil\n\t}\n\tgroups, err = getGroupsWithVirtualFolders(ctx, groups, dbHandle)\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tgroups, err = getGroupsWithUsers(ctx, groups, dbHandle)\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tgroups, err = getGroupsWithAdmins(ctx, groups, dbHandle)\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tfor idx := range groups {\n\t\tgroups[idx].PrepareForRendering()\n\t}\n\treturn groups, nil\n}\n\nfunc sqlCommonAddGroup(group *Group, dbHandle *sql.DB) error {\n\tif err := group.validate(); err != nil {\n\t\treturn err\n\t}\n\tsettings, err := json.Marshal(group.UserSettings)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tq := getAddGroupQuery()\n\t\t_, err := tx.ExecContext(ctx, q, group.Name, group.Description, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()), settings)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn generateGroupVirtualFoldersMapping(ctx, group, tx)\n\t})\n}\n\nfunc sqlCommonUpdateGroup(group *Group, dbHandle *sql.DB) error {\n\tif err := group.validate(); err != nil {\n\t\treturn err\n\t}\n\n\tsettings, err := json.Marshal(group.UserSettings)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tq := getUpdateGroupQuery()\n\t\t_, err := tx.ExecContext(ctx, q, group.Description, settings, util.GetTimeAsMsSinceEpoch(time.Now()), group.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn generateGroupVirtualFoldersMapping(ctx, group, tx)\n\t})\n}\n\nfunc sqlCommonDeleteGroup(group Group, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteGroupQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, group.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonGetUserByUsername(username, role string, dbHandle sqlQuerier) (User, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUserByUsernameQuery(role)\n\targs := []any{username}\n\tif role != \"\" {\n\t\targs = append(args, role)\n\t}\n\trow := dbHandle.QueryRowContext(ctx, q, args...)\n\tuser, err := getUserFromDbRow(row)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tuser, err = getUserWithVirtualFolders(ctx, user, dbHandle)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\treturn getUserWithGroups(ctx, user, dbHandle)\n}\n\nfunc sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHandle *sql.DB) (User, error) {\n\tuser, err := sqlCommonGetUserByUsername(username, \"\", dbHandle)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, err\n\t}\n\treturn checkUserAndPass(&user, password, ip, protocol)\n}\n\nfunc sqlCommonValidateUserAndTLSCertificate(username, protocol string, tlsCert *x509.Certificate, dbHandle *sql.DB) (User, error) {\n\tvar user User\n\tif tlsCert == nil {\n\t\treturn user, errors.New(\"TLS certificate cannot be null or empty\")\n\t}\n\tuser, err := sqlCommonGetUserByUsername(username, \"\", dbHandle)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, err\n\t}\n\treturn checkUserAndTLSCertificate(&user, protocol, tlsCert)\n}\n\nfunc sqlCommonValidateUserAndPubKey(username string, pubKey []byte, isSSHCert bool, dbHandle *sql.DB) (User, string, error) {\n\tvar user User\n\tif len(pubKey) == 0 {\n\t\treturn user, \"\", errors.New(\"credentials cannot be null or empty\")\n\t}\n\tuser, err := sqlCommonGetUserByUsername(username, \"\", dbHandle)\n\tif err != nil {\n\t\tproviderLog(logger.LevelWarn, \"error authenticating user %q: %v\", username, err)\n\t\treturn user, \"\", err\n\t}\n\treturn checkUserAndPubKey(&user, pubKey, isSSHCert)\n}\n\nfunc sqlCommonCheckAvailability(dbHandle *sql.DB) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tproviderLog(logger.LevelError, \"panic in check provider availability, stack trace: %s\", string(debug.Stack()))\n\t\t\terr = errors.New(\"unable to check provider status\")\n\t\t}\n\t}()\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\terr = dbHandle.PingContext(ctx)\n\treturn\n}\n\nfunc sqlCommonUpdateTransferQuota(username string, uploadSize, downloadSize int64, reset bool, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateTransferQuotaQuery(reset)\n\t_, err := dbHandle.ExecContext(ctx, q, uploadSize, downloadSize, util.GetTimeAsMsSinceEpoch(time.Now()), username)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"transfer quota updated for user %q, ul increment: %d dl increment: %d is reset? %t\",\n\t\t\tusername, uploadSize, downloadSize, reset)\n\t} else {\n\t\tproviderLog(logger.LevelError, \"error updating quota for user %q: %v\", username, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bool, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateQuotaQuery(reset)\n\t_, err := dbHandle.ExecContext(ctx, q, sizeAdd, filesAdd, util.GetTimeAsMsSinceEpoch(time.Now()), username)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"quota updated for user %q, files increment: %d size increment: %d is reset? %t\",\n\t\t\tusername, filesAdd, sizeAdd, reset)\n\t} else {\n\t\tproviderLog(logger.LevelError, \"error updating quota for user %q: %v\", username, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonGetAdminSignature(username string, dbHandle *sql.DB) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAdminSignatureQuery()\n\tvar updatedAt int64\n\terr := dbHandle.QueryRowContext(ctx, q, username).Scan(&updatedAt)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strconv.FormatInt(updatedAt, 10), nil\n}\n\nfunc sqlCommonGetUserSignature(username string, dbHandle *sql.DB) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUserSignatureQuery()\n\tvar updatedAt int64\n\terr := dbHandle.QueryRowContext(ctx, q, username).Scan(&updatedAt)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strconv.FormatInt(updatedAt, 10), nil\n}\n\nfunc sqlCommonGetUsedQuota(username string, dbHandle *sql.DB) (int, int64, int64, int64, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getQuotaQuery()\n\tvar usedFiles int\n\tvar usedSize, usedUploadSize, usedDownloadSize int64\n\terr := dbHandle.QueryRowContext(ctx, q, username).Scan(&usedSize, &usedFiles, &usedUploadSize, &usedDownloadSize)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error getting quota for user: %v, error: %v\", username, err)\n\t\treturn 0, 0, 0, 0, err\n\t}\n\treturn usedFiles, usedSize, usedUploadSize, usedDownloadSize, err\n}\n\nfunc sqlCommonUpdateShareLastUse(shareID string, numTokens int, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateShareLastUseQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), numTokens, shareID)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"last use updated for shared object %q\", shareID)\n\t} else {\n\t\tproviderLog(logger.LevelWarn, \"error updating last use for shared object %q: %v\", shareID, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonUpdateAPIKeyLastUse(keyID string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateAPIKeyLastUseQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), keyID)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"last use updated for key %q\", keyID)\n\t} else {\n\t\tproviderLog(logger.LevelWarn, \"error updating last use for key %q: %v\", keyID, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonUpdateAdminLastLogin(username string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateAdminLastLoginQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), username)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"last login updated for admin %q\", username)\n\t} else {\n\t\tproviderLog(logger.LevelWarn, \"error updating last login for admin %q: %v\", username, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonSetUpdatedAt(username string, dbHandle *sql.DB) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getSetUpdateAtQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), username)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"updated_at set for user %q\", username)\n\t} else {\n\t\tproviderLog(logger.LevelWarn, \"error setting updated_at for user %q: %v\", username, err)\n\t}\n}\n\nfunc sqlCommonSetFirstDownloadTimestamp(username string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getSetFirstDownloadQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), username)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonSetFirstUploadTimestamp(username string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getSetFirstUploadQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), username)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateLastLoginQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), username)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"last login updated for user %q\", username)\n\t} else {\n\t\tproviderLog(logger.LevelWarn, \"error updating last login for user %q: %v\", username, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonAddUser(user *User, dbHandle *sql.DB) error {\n\terr := ValidateUser(user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpermissions, err := user.GetPermissionsAsJSON()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpublicKeys, err := user.GetPublicKeysAsJSON()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfilters, err := user.GetFiltersAsJSON()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfsConfig, err := user.GetFsConfigAsJSON()\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tif config.IsShared == 1 {\n\t\t\t_, err := tx.ExecContext(ctx, getRemoveSoftDeletedUserQuery(), user.Username)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tq := getAddUserQuery(user.Role)\n\t\t_, err := tx.ExecContext(ctx, q, user.Username, user.Password, publicKeys, user.HomeDir, user.UID, user.GID,\n\t\t\tuser.MaxSessions, user.QuotaSize, user.QuotaFiles, permissions, user.UploadBandwidth,\n\t\t\tuser.DownloadBandwidth, user.Status, user.ExpirationDate, filters, fsConfig, user.AdditionalInfo,\n\t\t\tuser.Description, user.Email, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\tuser.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer, user.Role, user.LastPasswordChange)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := generateUserVirtualFoldersMapping(ctx, user, tx); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn generateUserGroupMapping(ctx, user, tx)\n\t})\n}\n\nfunc sqlCommonUpdateUserPassword(username, password string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateUserPasswordQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, password, util.GetTimeAsMsSinceEpoch(time.Now()), username)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {\n\terr := ValidateUser(user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpermissions, err := user.GetPermissionsAsJSON()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpublicKeys, err := user.GetPublicKeysAsJSON()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfilters, err := user.GetFiltersAsJSON()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfsConfig, err := user.GetFsConfigAsJSON()\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tq := getUpdateUserQuery(user.Role)\n\t\tres, err := tx.ExecContext(ctx, q, user.Password, publicKeys, user.HomeDir, user.UID, user.GID, user.MaxSessions,\n\t\t\tuser.QuotaSize, user.QuotaFiles, permissions, user.UploadBandwidth, user.DownloadBandwidth, user.Status,\n\t\t\tuser.ExpirationDate, filters, fsConfig, user.AdditionalInfo, user.Description, user.Email,\n\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()), user.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer,\n\t\t\tuser.Role, user.LastPasswordChange, user.Username)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := sqlCommonRequireRowAffected(res); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := generateUserVirtualFoldersMapping(ctx, user, tx); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn generateUserGroupMapping(ctx, user, tx)\n\t})\n}\n\nfunc sqlCommonDeleteUser(user User, softDelete bool, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteUserQuery(softDelete)\n\tif softDelete {\n\t\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\t\tif err := sqlCommonClearUserFolderMapping(ctx, &user, tx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := sqlCommonClearUserGroupMapping(ctx, &user, tx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tts := util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t\tres, err := tx.ExecContext(ctx, q, ts, ts, user.Username)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn sqlCommonRequireRowAffected(res)\n\t\t})\n\t}\n\tres, err := dbHandle.ExecContext(ctx, q, user.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonDumpUsers(dbHandle sqlQuerier) ([]User, error) {\n\tusers := make([]User, 0, 100)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpUsersQuery()\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tu, err := getUserFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn users, err\n\t\t}\n\t\tusers = append(users, u)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tusers, err = getUsersWithVirtualFolders(ctx, users, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\treturn getUsersWithGroups(ctx, users, dbHandle)\n}\n\nfunc sqlCommonGetRecentlyUpdatedUsers(after int64, dbHandle sqlQuerier) ([]User, error) {\n\tusers := make([]User, 0, 10)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getRecentlyUpdatedUsersQuery()\n\n\trows, err := dbHandle.QueryContext(ctx, q, after)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tu, err := getUserFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn users, err\n\t\t}\n\t\tusers = append(users, u)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tusers, err = getUsersWithVirtualFolders(ctx, users, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tusers, err = getUsersWithGroups(ctx, users, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tvar groupNames []string\n\tfor _, u := range users {\n\t\tfor _, g := range u.Groups {\n\t\t\tgroupNames = append(groupNames, g.Name)\n\t\t}\n\t}\n\tgroupNames = util.RemoveDuplicates(groupNames, false)\n\tif len(groupNames) == 0 {\n\t\treturn users, nil\n\t}\n\tgroups, err := sqlCommonGetGroupsWithNames(groupNames, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tif len(groups) == 0 {\n\t\treturn users, nil\n\t}\n\tgroupsMapping := make(map[string]Group)\n\tfor idx := range groups {\n\t\tgroupsMapping[groups[idx].Name] = groups[idx]\n\t}\n\tfor idx := range users {\n\t\tref := &users[idx]\n\t\tref.applyGroupSettings(groupsMapping)\n\t}\n\treturn users, nil\n}\n\nfunc sqlGetMaxUsersForQuotaCheckRange() int {\n\tmaxUsers := 50\n\tif maxUsers > len(sqlPlaceholders) {\n\t\tmaxUsers = len(sqlPlaceholders)\n\t}\n\treturn maxUsers\n}\n\nfunc sqlCommonGetUsersForQuotaCheck(toFetch map[string]bool, dbHandle sqlQuerier) ([]User, error) {\n\tmaxUsers := sqlGetMaxUsersForQuotaCheckRange()\n\tusers := make([]User, 0, maxUsers)\n\n\tusernames := make([]string, 0, len(toFetch))\n\tfor k := range toFetch {\n\t\tusernames = append(usernames, k)\n\t}\n\n\tfor len(usernames) > 0 {\n\t\tif maxUsers > len(usernames) {\n\t\t\tmaxUsers = len(usernames)\n\t\t}\n\t\tusersRange, err := sqlCommonGetUsersRangeForQuotaCheck(usernames[:maxUsers], dbHandle)\n\t\tif err != nil {\n\t\t\treturn users, err\n\t\t}\n\t\tusers = append(users, usersRange...)\n\t\tusernames = usernames[maxUsers:]\n\t}\n\n\tvar usersWithFolders []User\n\n\tvalidIdx := 0\n\tfor _, user := range users {\n\t\tif toFetch[user.Username] {\n\t\t\tusersWithFolders = append(usersWithFolders, user)\n\t\t} else {\n\t\t\tusers[validIdx] = user\n\t\t\tvalidIdx++\n\t\t}\n\t}\n\tusers = users[:validIdx]\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tusersWithFolders, err := getUsersWithVirtualFolders(ctx, usersWithFolders, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tusers = append(users, usersWithFolders...)\n\tusers, err = getUsersWithGroups(ctx, users, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tvar groupNames []string\n\tfor _, u := range users {\n\t\tfor _, g := range u.Groups {\n\t\t\tgroupNames = append(groupNames, g.Name)\n\t\t}\n\t}\n\tgroupNames = util.RemoveDuplicates(groupNames, false)\n\tif len(groupNames) == 0 {\n\t\treturn users, nil\n\t}\n\tgroups, err := sqlCommonGetGroupsWithNames(groupNames, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tgroupsMapping := make(map[string]Group)\n\tfor idx := range groups {\n\t\tgroupsMapping[groups[idx].Name] = groups[idx]\n\t}\n\tfor idx := range users {\n\t\tref := &users[idx]\n\t\tref.applyGroupSettings(groupsMapping)\n\t}\n\treturn users, nil\n}\n\nfunc sqlCommonGetUsersRangeForQuotaCheck(usernames []string, dbHandle sqlQuerier) ([]User, error) {\n\tusers := make([]User, 0, len(usernames))\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUsersForQuotaCheckQuery(len(usernames))\n\tqueryArgs := make([]any, 0, len(usernames))\n\tfor idx := range usernames {\n\t\tqueryArgs = append(queryArgs, usernames[idx])\n\t}\n\n\trows, err := dbHandle.QueryContext(ctx, q, queryArgs...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar user User\n\t\tvar filters []byte\n\t\terr = rows.Scan(&user.ID, &user.Username, &user.QuotaSize, &user.UsedQuotaSize, &user.TotalDataTransfer,\n\t\t\t&user.UploadDataTransfer, &user.DownloadDataTransfer, &user.UsedUploadDataTransfer,\n\t\t\t&user.UsedDownloadDataTransfer, &filters)\n\t\tif err != nil {\n\t\t\treturn users, err\n\t\t}\n\t\tvar userFilters UserFilters\n\t\terr = json.Unmarshal(filters, &userFilters)\n\t\tif err == nil {\n\t\t\tuser.Filters = userFilters\n\t\t}\n\t\tusers = append(users, user)\n\t}\n\n\treturn users, rows.Err()\n}\n\nfunc sqlCommonAddActiveTransfer(transfer ActiveTransfer, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddActiveTransferQuery()\n\tnow := util.GetTimeAsMsSinceEpoch(time.Now())\n\t_, err := dbHandle.ExecContext(ctx, q, transfer.ID, transfer.ConnID, transfer.Type, transfer.Username,\n\t\ttransfer.FolderName, transfer.IP, transfer.TruncatedSize, transfer.CurrentULSize, transfer.CurrentDLSize,\n\t\tnow, now)\n\treturn err\n}\n\nfunc sqlCommonUpdateActiveTransferSizes(ulSize, dlSize, transferID int64, connectionID string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateActiveTransferSizesQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, ulSize, dlSize, util.GetTimeAsMsSinceEpoch(time.Now()), connectionID, transferID)\n\treturn err\n}\n\nfunc sqlCommonRemoveActiveTransfer(transferID int64, connectionID string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getRemoveActiveTransferQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, connectionID, transferID)\n\treturn err\n}\n\nfunc sqlCommonCleanupActiveTransfers(before time.Time, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getCleanupActiveTransfersQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(before))\n\treturn err\n}\n\nfunc sqlCommonGetActiveTransfers(from time.Time, dbHandle sqlQuerier) ([]ActiveTransfer, error) {\n\ttransfers := make([]ActiveTransfer, 0, 30)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getActiveTransfersQuery()\n\trows, err := dbHandle.QueryContext(ctx, q, util.GetTimeAsMsSinceEpoch(from))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar transfer ActiveTransfer\n\t\tvar folderName sql.NullString\n\t\terr = rows.Scan(&transfer.ID, &transfer.ConnID, &transfer.Type, &transfer.Username, &folderName, &transfer.IP,\n\t\t\t&transfer.TruncatedSize, &transfer.CurrentULSize, &transfer.CurrentDLSize, &transfer.CreatedAt,\n\t\t\t&transfer.UpdatedAt)\n\t\tif err != nil {\n\t\t\treturn transfers, err\n\t\t}\n\t\tif folderName.Valid {\n\t\t\ttransfer.FolderName = folderName.String\n\t\t}\n\t\ttransfers = append(transfers, transfer)\n\t}\n\n\treturn transfers, rows.Err()\n}\n\nfunc sqlCommonGetUsers(limit int, offset int, order, role string, dbHandle sqlQuerier) ([]User, error) {\n\tusers := make([]User, 0, limit)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUsersQuery(order, role)\n\tvar args []any\n\tif role == \"\" {\n\t\targs = append(args, limit, offset)\n\t} else {\n\t\targs = append(args, role, limit, offset)\n\t}\n\trows, err := dbHandle.QueryContext(ctx, q, args...)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tu, err := getUserFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn users, err\n\t\t}\n\t\tusers = append(users, u)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tusers, err = getUsersWithVirtualFolders(ctx, users, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tusers, err = getUsersWithGroups(ctx, users, dbHandle)\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tfor idx := range users {\n\t\tusers[idx].PrepareForRendering()\n\t}\n\treturn users, nil\n}\n\nfunc sqlCommonGetDefenderHosts(from int64, limit int, dbHandle sqlQuerier) ([]DefenderEntry, error) {\n\thosts := make([]DefenderEntry, 0, 100)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDefenderHostsQuery()\n\trows, err := dbHandle.QueryContext(ctx, q, from, limit)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get defender hosts: %v\", err)\n\t\treturn hosts, err\n\t}\n\tdefer rows.Close()\n\n\tvar idForScores []int64\n\n\tfor rows.Next() {\n\t\tvar banTime sql.NullInt64\n\t\thost := DefenderEntry{}\n\t\terr = rows.Scan(&host.ID, &host.IP, &banTime)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to scan defender host row: %v\", err)\n\t\t\treturn hosts, err\n\t\t}\n\t\tvar hostBanTime time.Time\n\t\tif banTime.Valid && banTime.Int64 > 0 {\n\t\t\thostBanTime = util.GetTimeFromMsecSinceEpoch(banTime.Int64)\n\t\t}\n\t\tif hostBanTime.IsZero() || hostBanTime.Before(time.Now()) {\n\t\t\tidForScores = append(idForScores, host.ID)\n\t\t} else {\n\t\t\thost.BanTime = hostBanTime\n\t\t}\n\t\thosts = append(hosts, host)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to iterate over defender host rows: %v\", err)\n\t\treturn hosts, err\n\t}\n\n\treturn getDefenderHostsWithScores(ctx, hosts, from, idForScores, dbHandle)\n}\n\nfunc sqlCommonIsDefenderHostBanned(ip string, dbHandle sqlQuerier) (DefenderEntry, error) {\n\tvar host DefenderEntry\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDefenderIsHostBannedQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, ip, util.GetTimeAsMsSinceEpoch(time.Now()))\n\terr := row.Scan(&host.ID)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn host, util.NewRecordNotFoundError(\"host not found\")\n\t\t}\n\t\tproviderLog(logger.LevelError, \"unable to check ban status for host %q: %v\", ip, err)\n\t\treturn host, err\n\t}\n\n\treturn host, nil\n}\n\nfunc sqlCommonGetDefenderHostByIP(ip string, from int64, dbHandle sqlQuerier) (DefenderEntry, error) {\n\tvar host DefenderEntry\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDefenderHostQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, ip, from)\n\tvar banTime sql.NullInt64\n\terr := row.Scan(&host.ID, &host.IP, &banTime)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn host, util.NewRecordNotFoundError(\"host not found\")\n\t\t}\n\t\tproviderLog(logger.LevelError, \"unable to get host for ip %q: %v\", ip, err)\n\t\treturn host, err\n\t}\n\tif banTime.Valid && banTime.Int64 > 0 {\n\t\thostBanTime := util.GetTimeFromMsecSinceEpoch(banTime.Int64)\n\t\tif !hostBanTime.IsZero() && hostBanTime.After(time.Now()) {\n\t\t\thost.BanTime = hostBanTime\n\t\t\treturn host, nil\n\t\t}\n\t}\n\n\thosts, err := getDefenderHostsWithScores(ctx, []DefenderEntry{host}, from, []int64{host.ID}, dbHandle)\n\tif err != nil {\n\t\treturn host, err\n\t}\n\tif len(hosts) == 0 {\n\t\treturn host, util.NewRecordNotFoundError(\"host not found\")\n\t}\n\n\treturn hosts[0], nil\n}\n\nfunc sqlCommonDefenderIncrementBanTime(ip string, minutesToAdd int, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDefenderIncrementBanTimeQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, minutesToAdd*60000, ip)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"ban time updated for ip %q, increment (minutes): %v\",\n\t\t\tip, minutesToAdd)\n\t} else {\n\t\tproviderLog(logger.LevelError, \"error updating ban time for ip %q: %v\", ip, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonSetDefenderBanTime(ip string, banTime int64, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDefenderSetBanTimeQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, banTime, ip)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"ip %q banned until %v\", ip, util.GetTimeFromMsecSinceEpoch(banTime))\n\t} else {\n\t\tproviderLog(logger.LevelError, \"error setting ban time for ip %q: %v\", ip, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonDeleteDefenderHost(ip string, dbHandle sqlQuerier) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteDefenderHostQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, ip)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to delete defender host %q: %v\", ip, err)\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonAddDefenderHostAndEvent(ip string, score int, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tif err := sqlCommonAddDefenderHost(ctx, ip, tx); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn sqlCommonAddDefenderEvent(ctx, ip, score, tx)\n\t})\n}\n\nfunc sqlCommonDefenderCleanup(from int64, dbHandler *sql.DB) error {\n\tif err := sqlCommonCleanupDefenderEvents(from, dbHandler); err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonCleanupDefenderHosts(from, dbHandler)\n}\n\nfunc sqlCommonAddDefenderHost(ctx context.Context, ip string, tx *sql.Tx) error {\n\tq := getAddDefenderHostQuery()\n\t_, err := tx.ExecContext(ctx, q, ip, util.GetTimeAsMsSinceEpoch(time.Now()))\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to add defender host %q: %v\", ip, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonAddDefenderEvent(ctx context.Context, ip string, score int, tx *sql.Tx) error {\n\tq := getAddDefenderEventQuery()\n\t_, err := tx.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), score, ip)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to add defender event for %q: %v\", ip, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonCleanupDefenderHosts(from int64, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDefenderHostsCleanupQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), from)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to cleanup defender hosts: %v\", err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonCleanupDefenderEvents(from int64, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDefenderEventsCleanupQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, from)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to cleanup defender events: %v\", err)\n\t}\n\treturn err\n}\n\nfunc getShareFromDbRow(row sqlScanner) (Share, error) {\n\tvar share Share\n\tvar description, password sql.NullString\n\tvar allowFrom, paths []byte\n\n\terr := row.Scan(&share.ShareID, &share.Name, &description, &share.Scope,\n\t\t&paths, &share.Username, &share.CreatedAt, &share.UpdatedAt,\n\t\t&share.LastUseAt, &share.ExpiresAt, &password, &share.MaxTokens,\n\t\t&share.UsedTokens, &allowFrom)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn share, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn share, err\n\t}\n\tvar list []string\n\terr = json.Unmarshal(paths, &list)\n\tif err != nil {\n\t\treturn share, err\n\t}\n\tshare.Paths = list\n\tif description.Valid {\n\t\tshare.Description = description.String\n\t}\n\tif password.Valid {\n\t\tshare.Password = password.String\n\t}\n\tlist = nil\n\terr = json.Unmarshal(allowFrom, &list)\n\tif err == nil {\n\t\tshare.AllowFrom = list\n\t}\n\treturn share, nil\n}\n\nfunc getAPIKeyFromDbRow(row sqlScanner) (APIKey, error) {\n\tvar apiKey APIKey\n\tvar userID, adminID sql.NullInt64\n\tvar description sql.NullString\n\n\terr := row.Scan(&apiKey.KeyID, &apiKey.Name, &apiKey.Key, &apiKey.Scope, &apiKey.CreatedAt, &apiKey.UpdatedAt,\n\t\t&apiKey.LastUseAt, &apiKey.ExpiresAt, &description, &userID, &adminID)\n\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn apiKey, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn apiKey, err\n\t}\n\n\tif userID.Valid {\n\t\tapiKey.userID = userID.Int64\n\t}\n\tif adminID.Valid {\n\t\tapiKey.adminID = adminID.Int64\n\t}\n\tif description.Valid {\n\t\tapiKey.Description = description.String\n\t}\n\n\treturn apiKey, nil\n}\n\nfunc getAdminFromDbRow(row sqlScanner) (Admin, error) {\n\tvar admin Admin\n\tvar email, additionalInfo, description, role sql.NullString\n\tvar permissions, filters []byte\n\n\terr := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions,\n\t\t&filters, &additionalInfo, &description, &admin.CreatedAt, &admin.UpdatedAt, &admin.LastLogin, &role)\n\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn admin, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn admin, err\n\t}\n\n\tvar perms []string\n\terr = json.Unmarshal(permissions, &perms)\n\tif err != nil {\n\t\treturn admin, err\n\t}\n\tadmin.Permissions = perms\n\n\tif email.Valid {\n\t\tadmin.Email = email.String\n\t}\n\n\tvar adminFilters AdminFilters\n\terr = json.Unmarshal(filters, &adminFilters)\n\tif err == nil {\n\t\tadmin.Filters = adminFilters\n\t}\n\tif additionalInfo.Valid {\n\t\tadmin.AdditionalInfo = additionalInfo.String\n\t}\n\tif description.Valid {\n\t\tadmin.Description = description.String\n\t}\n\tif role.Valid {\n\t\tadmin.Role = role.String\n\t}\n\n\tadmin.SetEmptySecretsIfNil()\n\treturn admin, nil\n}\n\nfunc getEventActionFromDbRow(row sqlScanner) (BaseEventAction, error) {\n\tvar action BaseEventAction\n\tvar description sql.NullString\n\tvar options []byte\n\n\terr := row.Scan(&action.ID, &action.Name, &description, &action.Type, &options)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn action, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn action, err\n\t}\n\tif description.Valid {\n\t\taction.Description = description.String\n\t}\n\tvar actionOptions BaseEventActionOptions\n\terr = json.Unmarshal(options, &actionOptions)\n\tif err == nil {\n\t\taction.Options = actionOptions\n\t}\n\treturn action, nil\n}\n\nfunc getEventRuleFromDbRow(row sqlScanner) (EventRule, error) {\n\tvar rule EventRule\n\tvar description sql.NullString\n\tvar conditions []byte\n\n\terr := row.Scan(&rule.ID, &rule.Name, &description, &rule.CreatedAt, &rule.UpdatedAt, &rule.Trigger,\n\t\t&conditions, &rule.DeletedAt, &rule.Status)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn rule, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn rule, err\n\t}\n\tvar ruleConditions EventConditions\n\terr = json.Unmarshal(conditions, &ruleConditions)\n\tif err == nil {\n\t\trule.Conditions = ruleConditions\n\t}\n\n\tif description.Valid {\n\t\trule.Description = description.String\n\t}\n\treturn rule, nil\n}\n\nfunc getIPListEntryFromDbRow(row sqlScanner) (IPListEntry, error) {\n\tvar entry IPListEntry\n\tvar description sql.NullString\n\n\terr := row.Scan(&entry.Type, &entry.IPOrNet, &entry.Mode, &entry.Protocols, &description,\n\t\t&entry.CreatedAt, &entry.UpdatedAt, &entry.DeletedAt)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn entry, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn entry, err\n\t}\n\tif description.Valid {\n\t\tentry.Description = description.String\n\t}\n\treturn entry, err\n}\n\nfunc getRoleFromDbRow(row sqlScanner) (Role, error) {\n\tvar role Role\n\tvar description sql.NullString\n\n\terr := row.Scan(&role.ID, &role.Name, &description, &role.CreatedAt, &role.UpdatedAt)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn role, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn role, err\n\t}\n\tif description.Valid {\n\t\trole.Description = description.String\n\t}\n\n\treturn role, nil\n}\n\nfunc getGroupFromDbRow(row sqlScanner) (Group, error) {\n\tvar group Group\n\tvar description sql.NullString\n\tvar userSettings []byte\n\n\terr := row.Scan(&group.ID, &group.Name, &description, &group.CreatedAt, &group.UpdatedAt, &userSettings)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn group, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn group, err\n\t}\n\tif description.Valid {\n\t\tgroup.Description = description.String\n\t}\n\n\tvar settings GroupUserSettings\n\terr = json.Unmarshal(userSettings, &settings)\n\tif err == nil {\n\t\tgroup.UserSettings = settings\n\t}\n\n\treturn group, nil\n}\n\nfunc getUserFromDbRow(row sqlScanner) (User, error) {\n\tvar user User\n\tvar password sql.NullString\n\tvar permissions, publicKey, filters, fsConfig []byte\n\tvar additionalInfo, description, email, role sql.NullString\n\n\terr := row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,\n\t\t&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,\n\t\t&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,\n\t\t&additionalInfo, &description, &email, &user.CreatedAt, &user.UpdatedAt, &user.UploadDataTransfer, &user.DownloadDataTransfer,\n\t\t&user.TotalDataTransfer, &user.UsedUploadDataTransfer, &user.UsedDownloadDataTransfer, &user.DeletedAt, &user.FirstDownload,\n\t\t&user.FirstUpload, &role, &user.LastPasswordChange)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn user, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn user, err\n\t}\n\tif password.Valid {\n\t\tuser.Password = password.String\n\t}\n\tperms := make(map[string][]string)\n\terr = json.Unmarshal(permissions, &perms)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to deserialize permissions for user %q: %v\", user.Username, err)\n\t\treturn user, fmt.Errorf(\"unable to deserialize permissions for user %q: %v\", user.Username, err)\n\t}\n\tuser.Permissions = perms\n\t// we can have a empty string or an invalid json in null string\n\t// so we do a relaxed test if the field is optional, for example we\n\t// populate public keys only if unmarshal does not return an error\n\tvar pKeys []string\n\terr = json.Unmarshal(publicKey, &pKeys)\n\tif err == nil {\n\t\tuser.PublicKeys = pKeys\n\t}\n\tvar userFilters UserFilters\n\terr = json.Unmarshal(filters, &userFilters)\n\tif err == nil {\n\t\tuser.Filters = userFilters\n\t}\n\tvar fs vfs.Filesystem\n\terr = json.Unmarshal(fsConfig, &fs)\n\tif err == nil {\n\t\tuser.FsConfig = fs\n\t}\n\tif additionalInfo.Valid {\n\t\tuser.AdditionalInfo = additionalInfo.String\n\t}\n\tif description.Valid {\n\t\tuser.Description = description.String\n\t}\n\tif email.Valid {\n\t\tuser.Email = email.String\n\t}\n\tif role.Valid {\n\t\tuser.Role = role.String\n\t}\n\tuser.SetEmptySecretsIfNil()\n\treturn user, nil\n}\n\nfunc sqlCommonGetFolder(ctx context.Context, name string, dbHandle sqlQuerier) (vfs.BaseVirtualFolder, error) {\n\tvar folder vfs.BaseVirtualFolder\n\tq := getFolderByNameQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, name)\n\tvar mappedPath, description sql.NullString\n\tvar fsConfig []byte\n\terr := row.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, &folder.LastQuotaUpdate,\n\t\t&folder.Name, &description, &fsConfig)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn folder, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn folder, err\n\t}\n\tif mappedPath.Valid {\n\t\tfolder.MappedPath = mappedPath.String\n\t}\n\tif description.Valid {\n\t\tfolder.Description = description.String\n\t}\n\tvar fs vfs.Filesystem\n\terr = json.Unmarshal(fsConfig, &fs)\n\tif err == nil {\n\t\tfolder.FsConfig = fs\n\t}\n\treturn folder, err\n}\n\nfunc sqlCommonGetFolderByName(ctx context.Context, name string, dbHandle sqlQuerier) (vfs.BaseVirtualFolder, error) {\n\tfolder, err := sqlCommonGetFolder(ctx, name, dbHandle)\n\tif err != nil {\n\t\treturn folder, err\n\t}\n\tfolders, err := getVirtualFoldersWithUsers([]vfs.BaseVirtualFolder{folder}, dbHandle)\n\tif err != nil {\n\t\treturn folder, err\n\t}\n\tif len(folders) != 1 {\n\t\treturn folder, fmt.Errorf(\"unable to associate users with folder %q\", name)\n\t}\n\tfolders, err = getVirtualFoldersWithGroups([]vfs.BaseVirtualFolder{folders[0]}, dbHandle)\n\tif err != nil {\n\t\treturn folder, err\n\t}\n\tif len(folders) != 1 {\n\t\treturn folder, fmt.Errorf(\"unable to associate groups with folder %q\", name)\n\t}\n\treturn folders[0], nil\n}\n\nfunc sqlCommonAddFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) error {\n\terr := ValidateFolder(folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfsConfig, err := json.Marshal(folder.FsConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddFolderQuery()\n\t_, err = dbHandle.ExecContext(ctx, q, folder.MappedPath, folder.UsedQuotaSize, folder.UsedQuotaFiles,\n\t\tfolder.LastQuotaUpdate, folder.Name, folder.Description, fsConfig)\n\treturn err\n}\n\nfunc sqlCommonUpdateFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) error {\n\terr := ValidateFolder(folder)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfsConfig, err := json.Marshal(folder.FsConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateFolderQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, folder.MappedPath, folder.Description, fsConfig, folder.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonDeleteFolder(folder vfs.BaseVirtualFolder, dbHandle sqlQuerier) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteFolderQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, folder.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonDumpFolders(dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {\n\tfolders := make([]vfs.BaseVirtualFolder, 0, 50)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpFoldersQuery()\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn folders, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar folder vfs.BaseVirtualFolder\n\t\tvar mappedPath, description sql.NullString\n\t\tvar fsConfig []byte\n\t\terr = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,\n\t\t\t&folder.LastQuotaUpdate, &folder.Name, &description, &fsConfig)\n\t\tif err != nil {\n\t\t\treturn folders, err\n\t\t}\n\t\tif mappedPath.Valid {\n\t\t\tfolder.MappedPath = mappedPath.String\n\t\t}\n\t\tif description.Valid {\n\t\t\tfolder.Description = description.String\n\t\t}\n\t\tvar fs vfs.Filesystem\n\t\terr = json.Unmarshal(fsConfig, &fs)\n\t\tif err == nil {\n\t\t\tfolder.FsConfig = fs\n\t\t}\n\t\tfolders = append(folders, folder)\n\t}\n\treturn folders, rows.Err()\n}\n\nfunc sqlCommonGetFolders(limit, offset int, order string, minimal bool, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {\n\tfolders := make([]vfs.BaseVirtualFolder, 0, limit)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getFoldersQuery(order, minimal)\n\trows, err := dbHandle.QueryContext(ctx, q, limit, offset)\n\tif err != nil {\n\t\treturn folders, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar folder vfs.BaseVirtualFolder\n\t\tif minimal {\n\t\t\terr = rows.Scan(&folder.ID, &folder.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn folders, err\n\t\t\t}\n\t\t} else {\n\t\t\tvar mappedPath, description sql.NullString\n\t\t\tvar fsConfig []byte\n\t\t\terr = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,\n\t\t\t\t&folder.LastQuotaUpdate, &folder.Name, &description, &fsConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn folders, err\n\t\t\t}\n\t\t\tif mappedPath.Valid {\n\t\t\t\tfolder.MappedPath = mappedPath.String\n\t\t\t}\n\t\t\tif description.Valid {\n\t\t\t\tfolder.Description = description.String\n\t\t\t}\n\t\t\tvar fs vfs.Filesystem\n\t\t\terr = json.Unmarshal(fsConfig, &fs)\n\t\t\tif err == nil {\n\t\t\t\tfolder.FsConfig = fs\n\t\t\t}\n\t\t}\n\t\tfolder.PrepareForRendering()\n\t\tfolders = append(folders, folder)\n\t}\n\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn folders, err\n\t}\n\tif minimal {\n\t\treturn folders, nil\n\t}\n\tfolders, err = getVirtualFoldersWithUsers(folders, dbHandle)\n\tif err != nil {\n\t\treturn folders, err\n\t}\n\treturn getVirtualFoldersWithGroups(folders, dbHandle)\n}\n\nfunc sqlCommonClearUserFolderMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {\n\tq := getClearUserFolderMappingQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, user.Username)\n\treturn err\n}\n\nfunc sqlCommonClearGroupFolderMapping(ctx context.Context, group *Group, dbHandle sqlQuerier) error {\n\tq := getClearGroupFolderMappingQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, group.Name)\n\treturn err\n}\n\nfunc sqlCommonClearUserGroupMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {\n\tq := getClearUserGroupMappingQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, user.Username)\n\treturn err\n}\n\nfunc sqlCommonAddUserFolderMapping(ctx context.Context, user *User, folder *vfs.VirtualFolder, sortOrder int, dbHandle sqlQuerier) error {\n\tq := getAddUserFolderMappingQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, user.Username, sortOrder)\n\treturn err\n}\n\nfunc sqlCommonClearAdminGroupMapping(ctx context.Context, admin *Admin, dbHandle sqlQuerier) error {\n\tq := getClearAdminGroupMappingQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, admin.Username)\n\treturn err\n}\n\nfunc sqlCommonAddGroupFolderMapping(ctx context.Context, group *Group, folder *vfs.VirtualFolder, sortOrder int,\n\tdbHandle sqlQuerier,\n) error {\n\tq := getAddGroupFolderMappingQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, group.Name, sortOrder)\n\treturn err\n}\n\nfunc sqlCommonAddUserGroupMapping(ctx context.Context, username, groupName string, groupType, sortOrder int, dbHandle sqlQuerier) error {\n\tq := getAddUserGroupMappingQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, username, groupName, groupType, sortOrder)\n\treturn err\n}\n\nfunc sqlCommonAddAdminGroupMapping(ctx context.Context, username, groupName string, mappingOptions AdminGroupMappingOptions,\n\tsortOrder int, dbHandle sqlQuerier,\n) error {\n\toptions, err := json.Marshal(mappingOptions)\n\tif err != nil {\n\t\treturn err\n\t}\n\tq := getAddAdminGroupMappingQuery()\n\t_, err = dbHandle.ExecContext(ctx, q, username, groupName, options, sortOrder)\n\treturn err\n}\n\nfunc generateGroupVirtualFoldersMapping(ctx context.Context, group *Group, dbHandle sqlQuerier) error {\n\terr := sqlCommonClearGroupFolderMapping(ctx, group, dbHandle)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor idx := range group.VirtualFolders {\n\t\tvfolder := &group.VirtualFolders[idx]\n\t\terr = sqlCommonAddGroupFolderMapping(ctx, group, vfolder, idx, dbHandle)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn err\n}\n\nfunc generateUserVirtualFoldersMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {\n\terr := sqlCommonClearUserFolderMapping(ctx, user, dbHandle)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor idx := range user.VirtualFolders {\n\t\tvfolder := &user.VirtualFolders[idx]\n\t\terr = sqlCommonAddUserFolderMapping(ctx, user, vfolder, idx, dbHandle)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn err\n}\n\nfunc generateUserGroupMapping(ctx context.Context, user *User, dbHandle sqlQuerier) error {\n\terr := sqlCommonClearUserGroupMapping(ctx, user, dbHandle)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor idx, group := range user.Groups {\n\t\terr = sqlCommonAddUserGroupMapping(ctx, user.Username, group.Name, group.Type, idx, dbHandle)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn err\n}\n\nfunc generateAdminGroupMapping(ctx context.Context, admin *Admin, dbHandle sqlQuerier) error {\n\terr := sqlCommonClearAdminGroupMapping(ctx, admin, dbHandle)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor idx, group := range admin.Groups {\n\t\terr = sqlCommonAddAdminGroupMapping(ctx, admin.Username, group.Name, group.Options, idx, dbHandle)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn err\n}\n\nfunc getDefenderHostsWithScores(ctx context.Context, hosts []DefenderEntry, from int64, idForScores []int64,\n\tdbHandle sqlQuerier) (\n\t[]DefenderEntry,\n\terror,\n) {\n\tif len(idForScores) == 0 {\n\t\treturn hosts, nil\n\t}\n\n\thostsWithScores := make(map[int64]int)\n\tq := getDefenderEventsQuery(idForScores)\n\trows, err := dbHandle.QueryContext(ctx, q, from)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"unable to get score for hosts with id %+v: %v\", idForScores, err)\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar hostID int64\n\t\tvar score int\n\t\terr = rows.Scan(&hostID, &score)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"error scanning host score row: %v\", err)\n\t\t\treturn hosts, err\n\t\t}\n\t\tif score > 0 {\n\t\t\thostsWithScores[hostID] = score\n\t\t}\n\t}\n\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn hosts, err\n\t}\n\n\tresult := make([]DefenderEntry, 0, len(hosts))\n\n\tfor idx := range hosts {\n\t\thosts[idx].Score = hostsWithScores[hosts[idx].ID]\n\t\tif hosts[idx].Score > 0 || !hosts[idx].BanTime.IsZero() {\n\t\t\tresult = append(result, hosts[idx])\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\nfunc getAdminWithGroups(ctx context.Context, admin Admin, dbHandle sqlQuerier) (Admin, error) {\n\tadmins, err := getAdminsWithGroups(ctx, []Admin{admin}, dbHandle)\n\tif err != nil {\n\t\treturn admin, err\n\t}\n\tif len(admins) == 0 {\n\t\treturn admin, errSQLGroupsAssociation\n\t}\n\treturn admins[0], err\n}\n\nfunc getAdminsWithGroups(ctx context.Context, admins []Admin, dbHandle sqlQuerier) ([]Admin, error) {\n\tif len(admins) == 0 {\n\t\treturn admins, nil\n\t}\n\tadminsGroups := make(map[int64][]AdminGroupMapping)\n\tq := getRelatedGroupsForAdminsQuery(admins)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar group AdminGroupMapping\n\t\tvar adminID int64\n\t\tvar options []byte\n\t\terr = rows.Scan(&group.Name, &options, &adminID)\n\t\tif err != nil {\n\t\t\treturn admins, err\n\t\t}\n\t\terr = json.Unmarshal(options, &group.Options)\n\t\tif err != nil {\n\t\t\treturn admins, err\n\t\t}\n\t\tadminsGroups[adminID] = append(adminsGroups[adminID], group)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn admins, err\n\t}\n\tif len(adminsGroups) == 0 {\n\t\treturn admins, err\n\t}\n\tfor idx := range admins {\n\t\tref := &admins[idx]\n\t\tref.Groups = adminsGroups[ref.ID]\n\t}\n\treturn admins, err\n}\n\nfunc getUserWithVirtualFolders(ctx context.Context, user User, dbHandle sqlQuerier) (User, error) {\n\tusers, err := getUsersWithVirtualFolders(ctx, []User{user}, dbHandle)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tif len(users) == 0 {\n\t\treturn user, errSQLFoldersAssociation\n\t}\n\treturn users[0], err\n}\n\nfunc getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQuerier) ([]User, error) {\n\tif len(users) == 0 {\n\t\treturn users, nil\n\t}\n\n\tusersVirtualFolders := make(map[int64][]vfs.VirtualFolder)\n\tq := getRelatedFoldersForUsersQuery(users)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar folder vfs.VirtualFolder\n\t\tvar userID int64\n\t\tvar mappedPath, description sql.NullString\n\t\tvar fsConfig []byte\n\t\terr = rows.Scan(&folder.ID, &folder.Name, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,\n\t\t\t&folder.LastQuotaUpdate, &folder.VirtualPath, &folder.QuotaSize, &folder.QuotaFiles, &userID, &fsConfig,\n\t\t\t&description)\n\t\tif err != nil {\n\t\t\treturn users, err\n\t\t}\n\t\tif mappedPath.Valid {\n\t\t\tfolder.MappedPath = mappedPath.String\n\t\t}\n\t\tif description.Valid {\n\t\t\tfolder.Description = description.String\n\t\t}\n\t\tvar fs vfs.Filesystem\n\t\terr = json.Unmarshal(fsConfig, &fs)\n\t\tif err == nil {\n\t\t\tfolder.FsConfig = fs\n\t\t}\n\t\tusersVirtualFolders[userID] = append(usersVirtualFolders[userID], folder)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tif len(usersVirtualFolders) == 0 {\n\t\treturn users, err\n\t}\n\tfor idx := range users {\n\t\tref := &users[idx]\n\t\tref.VirtualFolders = usersVirtualFolders[ref.ID]\n\t}\n\treturn users, err\n}\n\nfunc getUserWithGroups(ctx context.Context, user User, dbHandle sqlQuerier) (User, error) {\n\tusers, err := getUsersWithGroups(ctx, []User{user}, dbHandle)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tif len(users) == 0 {\n\t\treturn user, errSQLGroupsAssociation\n\t}\n\treturn users[0], err\n}\n\nfunc getUsersWithGroups(ctx context.Context, users []User, dbHandle sqlQuerier) ([]User, error) {\n\tif len(users) == 0 {\n\t\treturn users, nil\n\t}\n\tusersGroups := make(map[int64][]sdk.GroupMapping)\n\tq := getRelatedGroupsForUsersQuery(users)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar group sdk.GroupMapping\n\t\tvar userID int64\n\t\terr = rows.Scan(&group.Name, &group.Type, &userID)\n\t\tif err != nil {\n\t\t\treturn users, err\n\t\t}\n\t\tusersGroups[userID] = append(usersGroups[userID], group)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn users, err\n\t}\n\tif len(usersGroups) == 0 {\n\t\treturn users, err\n\t}\n\tfor idx := range users {\n\t\tref := &users[idx]\n\t\tref.Groups = usersGroups[ref.ID]\n\t}\n\treturn users, err\n}\n\nfunc getGroupWithUsers(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {\n\tgroups, err := getGroupsWithUsers(ctx, []Group{group}, dbHandle)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tif len(groups) == 0 {\n\t\treturn group, errSQLUsersAssociation\n\t}\n\treturn groups[0], err\n}\n\nfunc getRoleWithUsers(ctx context.Context, role Role, dbHandle sqlQuerier) (Role, error) {\n\troles, err := getRolesWithUsers(ctx, []Role{role}, dbHandle)\n\tif err != nil {\n\t\treturn role, err\n\t}\n\tif len(roles) == 0 {\n\t\treturn role, errors.New(\"unable to associate users with role\")\n\t}\n\treturn roles[0], err\n}\n\nfunc getRoleWithAdmins(ctx context.Context, role Role, dbHandle sqlQuerier) (Role, error) {\n\troles, err := getRolesWithAdmins(ctx, []Role{role}, dbHandle)\n\tif err != nil {\n\t\treturn role, err\n\t}\n\tif len(roles) == 0 {\n\t\treturn role, errors.New(\"unable to associate admins with role\")\n\t}\n\treturn roles[0], err\n}\n\nfunc getGroupWithAdmins(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {\n\tgroups, err := getGroupsWithAdmins(ctx, []Group{group}, dbHandle)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tif len(groups) == 0 {\n\t\treturn group, errSQLUsersAssociation\n\t}\n\treturn groups[0], err\n}\n\nfunc getGroupWithVirtualFolders(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {\n\tgroups, err := getGroupsWithVirtualFolders(ctx, []Group{group}, dbHandle)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tif len(groups) == 0 {\n\t\treturn group, errSQLFoldersAssociation\n\t}\n\treturn groups[0], err\n}\n\nfunc getGroupsWithVirtualFolders(ctx context.Context, groups []Group, dbHandle sqlQuerier) ([]Group, error) {\n\tif len(groups) == 0 {\n\t\treturn groups, nil\n\t}\n\tq := getRelatedFoldersForGroupsQuery(groups)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tgroupsVirtualFolders := make(map[int64][]vfs.VirtualFolder)\n\n\tfor rows.Next() {\n\t\tvar groupID int64\n\t\tvar folder vfs.VirtualFolder\n\t\tvar mappedPath, description sql.NullString\n\t\tvar fsConfig []byte\n\t\terr = rows.Scan(&folder.ID, &folder.Name, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,\n\t\t\t&folder.LastQuotaUpdate, &folder.VirtualPath, &folder.QuotaSize, &folder.QuotaFiles, &groupID, &fsConfig,\n\t\t\t&description)\n\t\tif err != nil {\n\t\t\treturn groups, err\n\t\t}\n\t\tif mappedPath.Valid {\n\t\t\tfolder.MappedPath = mappedPath.String\n\t\t}\n\t\tif description.Valid {\n\t\t\tfolder.Description = description.String\n\t\t}\n\t\tvar fs vfs.Filesystem\n\t\terr = json.Unmarshal(fsConfig, &fs)\n\t\tif err == nil {\n\t\t\tfolder.FsConfig = fs\n\t\t}\n\t\tgroupsVirtualFolders[groupID] = append(groupsVirtualFolders[groupID], folder)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tif len(groupsVirtualFolders) == 0 {\n\t\treturn groups, err\n\t}\n\tfor idx := range groups {\n\t\tref := &groups[idx]\n\t\tref.VirtualFolders = groupsVirtualFolders[ref.ID]\n\t}\n\treturn groups, err\n}\n\nfunc getGroupsWithUsers(ctx context.Context, groups []Group, dbHandle sqlQuerier) ([]Group, error) {\n\tif len(groups) == 0 {\n\t\treturn groups, nil\n\t}\n\tq := getRelatedUsersForGroupsQuery(groups)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tgroupsUsers := make(map[int64][]string)\n\n\tfor rows.Next() {\n\t\tvar username string\n\t\tvar groupID int64\n\t\terr = rows.Scan(&groupID, &username)\n\t\tif err != nil {\n\t\t\treturn groups, err\n\t\t}\n\t\tgroupsUsers[groupID] = append(groupsUsers[groupID], username)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tif len(groupsUsers) == 0 {\n\t\treturn groups, err\n\t}\n\tfor idx := range groups {\n\t\tref := &groups[idx]\n\t\tref.Users = groupsUsers[ref.ID]\n\t}\n\treturn groups, err\n}\n\nfunc getRolesWithUsers(ctx context.Context, roles []Role, dbHandle sqlQuerier) ([]Role, error) {\n\tif len(roles) == 0 {\n\t\treturn roles, nil\n\t}\n\trows, err := dbHandle.QueryContext(ctx, getUsersWithRolesQuery(roles))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\trolesUsers := make(map[int64][]string)\n\tfor rows.Next() {\n\t\tvar roleID int64\n\t\tvar username string\n\t\terr = rows.Scan(&roleID, &username)\n\t\tif err != nil {\n\t\t\treturn roles, err\n\t\t}\n\t\trolesUsers[roleID] = append(rolesUsers[roleID], username)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn roles, err\n\t}\n\tif len(rolesUsers) > 0 {\n\t\tfor idx := range roles {\n\t\t\tref := &roles[idx]\n\t\t\tref.Users = rolesUsers[ref.ID]\n\t\t}\n\t}\n\treturn roles, nil\n}\n\nfunc getRolesWithAdmins(ctx context.Context, roles []Role, dbHandle sqlQuerier) ([]Role, error) {\n\tif len(roles) == 0 {\n\t\treturn roles, nil\n\t}\n\trows, err := dbHandle.QueryContext(ctx, getAdminsWithRolesQuery(roles))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\trolesAdmins := make(map[int64][]string)\n\tfor rows.Next() {\n\t\tvar roleID int64\n\t\tvar username string\n\t\terr = rows.Scan(&roleID, &username)\n\t\tif err != nil {\n\t\t\treturn roles, err\n\t\t}\n\t\trolesAdmins[roleID] = append(rolesAdmins[roleID], username)\n\t}\n\tif err = rows.Err(); err != nil {\n\t\treturn roles, err\n\t}\n\tif len(rolesAdmins) > 0 {\n\t\tfor idx := range roles {\n\t\t\tref := &roles[idx]\n\t\t\tref.Admins = rolesAdmins[ref.ID]\n\t\t}\n\t}\n\treturn roles, nil\n}\n\nfunc getGroupsWithAdmins(ctx context.Context, groups []Group, dbHandle sqlQuerier) ([]Group, error) {\n\tif len(groups) == 0 {\n\t\treturn groups, nil\n\t}\n\tq := getRelatedAdminsForGroupsQuery(groups)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tgroupsAdmins := make(map[int64][]string)\n\tfor rows.Next() {\n\t\tvar groupID int64\n\t\tvar username string\n\t\terr = rows.Scan(&groupID, &username)\n\t\tif err != nil {\n\t\t\treturn groups, err\n\t\t}\n\t\tgroupsAdmins[groupID] = append(groupsAdmins[groupID], username)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn groups, err\n\t}\n\tif len(groupsAdmins) > 0 {\n\t\tfor idx := range groups {\n\t\t\tref := &groups[idx]\n\t\t\tref.Admins = groupsAdmins[ref.ID]\n\t\t}\n\t}\n\treturn groups, nil\n}\n\nfunc getVirtualFoldersWithGroups(folders []vfs.BaseVirtualFolder, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {\n\tif len(folders) == 0 {\n\t\treturn folders, nil\n\t}\n\tvFoldersGroups := make(map[int64][]string)\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getRelatedGroupsForFoldersQuery(folders)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar name string\n\t\tvar folderID int64\n\t\terr = rows.Scan(&folderID, &name)\n\t\tif err != nil {\n\t\t\treturn folders, err\n\t\t}\n\t\tvFoldersGroups[folderID] = append(vFoldersGroups[folderID], name)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn folders, err\n\t}\n\tif len(vFoldersGroups) == 0 {\n\t\treturn folders, err\n\t}\n\tfor idx := range folders {\n\t\tref := &folders[idx]\n\t\tref.Groups = vFoldersGroups[ref.ID]\n\t}\n\treturn folders, err\n}\n\nfunc getVirtualFoldersWithUsers(folders []vfs.BaseVirtualFolder, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {\n\tif len(folders) == 0 {\n\t\treturn folders, nil\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getRelatedUsersForFoldersQuery(folders)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvFoldersUsers := make(map[int64][]string)\n\tfor rows.Next() {\n\t\tvar username string\n\t\tvar folderID int64\n\t\terr = rows.Scan(&folderID, &username)\n\t\tif err != nil {\n\t\t\treturn folders, err\n\t\t}\n\t\tvFoldersUsers[folderID] = append(vFoldersUsers[folderID], username)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn folders, err\n\t}\n\tif len(vFoldersUsers) == 0 {\n\t\treturn folders, err\n\t}\n\tfor idx := range folders {\n\t\tref := &folders[idx]\n\t\tref.Users = vFoldersUsers[ref.ID]\n\t}\n\treturn folders, err\n}\n\nfunc sqlCommonUpdateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateFolderQuotaQuery(reset)\n\t_, err := dbHandle.ExecContext(ctx, q, sizeAdd, filesAdd, util.GetTimeAsMsSinceEpoch(time.Now()), name)\n\tif err == nil {\n\t\tproviderLog(logger.LevelDebug, \"quota updated for folder %q, files increment: %d size increment: %d is reset? %t\",\n\t\t\tname, filesAdd, sizeAdd, reset)\n\t} else {\n\t\tproviderLog(logger.LevelWarn, \"error updating quota for folder %q: %v\", name, err)\n\t}\n\treturn err\n}\n\nfunc sqlCommonGetFolderUsedQuota(mappedPath string, dbHandle *sql.DB) (int, int64, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getQuotaFolderQuery()\n\tvar usedFiles int\n\tvar usedSize int64\n\terr := dbHandle.QueryRowContext(ctx, q, mappedPath).Scan(&usedSize, &usedFiles)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error getting quota for folder: %v, error: %v\", mappedPath, err)\n\t\treturn 0, 0, err\n\t}\n\treturn usedFiles, usedSize, err\n}\n\nfunc getAPIKeyWithRelatedFields(ctx context.Context, apiKey APIKey, dbHandle sqlQuerier) (APIKey, error) {\n\tvar apiKeys []APIKey\n\tvar err error\n\n\tscope := APIKeyScopeAdmin\n\tif apiKey.userID > 0 {\n\t\tscope = APIKeyScopeUser\n\t}\n\tapiKeys, err = getRelatedValuesForAPIKeys(ctx, []APIKey{apiKey}, dbHandle, scope)\n\tif err != nil {\n\t\treturn apiKey, err\n\t}\n\tif len(apiKeys) > 0 {\n\t\tapiKey = apiKeys[0]\n\t}\n\treturn apiKey, nil\n}\n\nfunc getRelatedValuesForAPIKeys(ctx context.Context, apiKeys []APIKey, dbHandle sqlQuerier, scope APIKeyScope) ([]APIKey, error) {\n\tif len(apiKeys) == 0 {\n\t\treturn apiKeys, nil\n\t}\n\tvalues := make(map[int64]string)\n\tvar q string\n\tif scope == APIKeyScopeUser {\n\t\tq = getRelatedUsersForAPIKeysQuery(apiKeys)\n\t} else {\n\t\tq = getRelatedAdminsForAPIKeysQuery(apiKeys)\n\t}\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar valueID int64\n\t\tvar valueName string\n\t\terr = rows.Scan(&valueID, &valueName)\n\t\tif err != nil {\n\t\t\treturn apiKeys, err\n\t\t}\n\t\tvalues[valueID] = valueName\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn apiKeys, err\n\t}\n\tif len(values) == 0 {\n\t\treturn apiKeys, nil\n\t}\n\tfor idx := range apiKeys {\n\t\tref := &apiKeys[idx]\n\t\tif scope == APIKeyScopeUser {\n\t\t\tref.User = values[ref.userID]\n\t\t} else {\n\t\t\tref.Admin = values[ref.adminID]\n\t\t}\n\t}\n\treturn apiKeys, nil\n}\n\nfunc sqlCommonGetAPIKeyRelatedIDs(apiKey *APIKey) (sql.NullInt64, sql.NullInt64, error) {\n\tvar userID, adminID sql.NullInt64\n\tif apiKey.User != \"\" {\n\t\tu, err := provider.userExists(apiKey.User, \"\")\n\t\tif err != nil {\n\t\t\treturn userID, adminID, util.NewGenericError(fmt.Sprintf(\"unable to validate user %v\", apiKey.User))\n\t\t}\n\t\tuserID.Valid = true\n\t\tuserID.Int64 = u.ID\n\t}\n\tif apiKey.Admin != \"\" {\n\t\ta, err := provider.adminExists(apiKey.Admin)\n\t\tif err != nil {\n\t\t\treturn userID, adminID, util.NewValidationError(fmt.Sprintf(\"unable to validate admin %v\", apiKey.Admin))\n\t\t}\n\t\tadminID.Valid = true\n\t\tadminID.Int64 = a.ID\n\t}\n\treturn userID, adminID, nil\n}\n\nfunc sqlCommonAddSession(session Session, dbHandle *sql.DB) error {\n\tif err := session.validate(); err != nil {\n\t\treturn err\n\t}\n\tdata, err := json.Marshal(session.Data)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddSessionQuery()\n\t_, err = dbHandle.ExecContext(ctx, q, session.Key, data, session.Type, session.Timestamp)\n\treturn err\n}\n\nfunc sqlCommonGetSession(key string, sessionType SessionType, dbHandle sqlQuerier) (Session, error) {\n\tvar session Session\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getSessionQuery()\n\tvar data []byte // type hint, some driver will use string instead of []byte if the type is any\n\terr := dbHandle.QueryRowContext(ctx, q, key, sessionType).Scan(&session.Key, &data, &session.Type, &session.Timestamp)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn session, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn session, err\n\t}\n\tsession.Data = data\n\treturn session, nil\n}\n\nfunc sqlCommonDeleteSession(key string, sessionType SessionType, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteSessionQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, key, sessionType)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonCleanupSessions(sessionType SessionType, before int64, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getCleanupSessionsQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, sessionType, before)\n\treturn err\n}\n\nfunc getActionsWithRuleNames(ctx context.Context, actions []BaseEventAction, dbHandle sqlQuerier,\n) ([]BaseEventAction, error) {\n\tif len(actions) == 0 {\n\t\treturn actions, nil\n\t}\n\tq := getRelatedRulesForActionsQuery(actions)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tactionsRules := make(map[int64][]string)\n\tfor rows.Next() {\n\t\tvar name string\n\t\tvar actionID int64\n\t\tif err = rows.Scan(&actionID, &name); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tactionsRules[actionID] = append(actionsRules[actionID], name)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(actionsRules) == 0 {\n\t\treturn actions, nil\n\t}\n\tfor idx := range actions {\n\t\tref := &actions[idx]\n\t\tref.Rules = actionsRules[ref.ID]\n\t}\n\treturn actions, nil\n}\n\nfunc getRulesWithActions(ctx context.Context, rules []EventRule, dbHandle sqlQuerier) ([]EventRule, error) {\n\tif len(rules) == 0 {\n\t\treturn rules, nil\n\t}\n\trulesActions := make(map[int64][]EventAction)\n\tq := getRelatedActionsForRulesQuery(rules)\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar action EventAction\n\t\tvar ruleID int64\n\t\tvar description sql.NullString\n\t\tvar baseOptions, options []byte\n\t\terr = rows.Scan(&action.ID, &action.Name, &description, &action.Type, &baseOptions, &options,\n\t\t\t&action.Order, &ruleID)\n\t\tif err != nil {\n\t\t\treturn rules, err\n\t\t}\n\t\tif len(baseOptions) > 0 {\n\t\t\terr = json.Unmarshal(baseOptions, &action.BaseEventAction.Options)\n\t\t\tif err != nil {\n\t\t\t\treturn rules, err\n\t\t\t}\n\t\t}\n\t\tif len(options) > 0 {\n\t\t\terr = json.Unmarshal(options, &action.Options)\n\t\t\tif err != nil {\n\t\t\t\treturn rules, err\n\t\t\t}\n\t\t}\n\t\taction.BaseEventAction.Options.SetEmptySecretsIfNil()\n\t\trulesActions[ruleID] = append(rulesActions[ruleID], action)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn rules, err\n\t}\n\tif len(rulesActions) == 0 {\n\t\treturn rules, nil\n\t}\n\tfor idx := range rules {\n\t\tref := &rules[idx]\n\t\tref.Actions = rulesActions[ref.ID]\n\t}\n\treturn rules, nil\n}\n\nfunc generateEventRuleActionsMapping(ctx context.Context, rule *EventRule, dbHandle sqlQuerier) error {\n\tq := getClearRuleActionMappingQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, rule.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, action := range rule.Actions {\n\t\toptions, err := json.Marshal(action.Options)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tq = getAddRuleActionMappingQuery()\n\t\t_, err = dbHandle.ExecContext(ctx, q, rule.Name, action.Name, action.Order, options)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc sqlCommonGetEventActionByName(name string, dbHandle sqlQuerier) (BaseEventAction, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getEventActionByNameQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, name)\n\n\taction, err := getEventActionFromDbRow(row)\n\tif err != nil {\n\t\treturn action, err\n\t}\n\tactions, err := getActionsWithRuleNames(ctx, []BaseEventAction{action}, dbHandle)\n\tif err != nil {\n\t\treturn action, err\n\t}\n\tif len(actions) != 1 {\n\t\treturn action, fmt.Errorf(\"unable to associate rules with action %q\", name)\n\t}\n\treturn actions[0], nil\n}\n\nfunc sqlCommonDumpEventActions(dbHandle sqlQuerier) ([]BaseEventAction, error) {\n\tactions := make([]BaseEventAction, 0, 10)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpEventActionsQuery()\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn actions, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\taction, err := getEventActionFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn actions, err\n\t\t}\n\t\tactions = append(actions, action)\n\t}\n\treturn actions, rows.Err()\n}\n\nfunc sqlCommonGetEventActions(limit int, offset int, order string, minimal bool,\n\tdbHandle sqlQuerier,\n) ([]BaseEventAction, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getEventsActionsQuery(order, minimal)\n\n\tactions := make([]BaseEventAction, 0, limit)\n\trows, err := dbHandle.QueryContext(ctx, q, limit, offset)\n\tif err != nil {\n\t\treturn actions, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar action BaseEventAction\n\t\tif minimal {\n\t\t\terr = rows.Scan(&action.ID, &action.Name)\n\t\t} else {\n\t\t\taction, err = getEventActionFromDbRow(rows)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn actions, err\n\t\t}\n\t\tactions = append(actions, action)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif minimal {\n\t\treturn actions, nil\n\t}\n\tactions, err = getActionsWithRuleNames(ctx, actions, dbHandle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor idx := range actions {\n\t\tactions[idx].PrepareForRendering()\n\t}\n\treturn actions, nil\n}\n\nfunc sqlCommonAddEventAction(action *BaseEventAction, dbHandle *sql.DB) error {\n\tif err := action.validate(); err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddEventActionQuery()\n\toptions, err := json.Marshal(action.Options)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = dbHandle.ExecContext(ctx, q, action.Name, action.Description, action.Type, options)\n\treturn err\n}\n\nfunc sqlCommonUpdateEventAction(action *BaseEventAction, dbHandle *sql.DB) error {\n\tif err := action.validate(); err != nil {\n\t\treturn err\n\t}\n\toptions, err := json.Marshal(action.Options)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tq := getUpdateEventActionQuery()\n\t\tres, err := tx.ExecContext(ctx, q, action.Description, action.Type, options, action.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := sqlCommonRequireRowAffected(res); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tq = getUpdateRulesTimestampQuery()\n\t\t_, err = tx.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), action.Name)\n\t\treturn err\n\t})\n}\n\nfunc sqlCommonDeleteEventAction(action BaseEventAction, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteEventActionQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, action.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonGetEventRuleByName(name string, dbHandle sqlQuerier) (EventRule, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getEventRulesByNameQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, name)\n\trule, err := getEventRuleFromDbRow(row)\n\tif err != nil {\n\t\treturn rule, err\n\t}\n\trules, err := getRulesWithActions(ctx, []EventRule{rule}, dbHandle)\n\tif err != nil {\n\t\treturn rule, err\n\t}\n\tif len(rules) != 1 {\n\t\treturn rule, fmt.Errorf(\"unable to associate rule %q with actions\", name)\n\t}\n\treturn rules[0], nil\n}\n\nfunc sqlCommonDumpEventRules(dbHandle sqlQuerier) ([]EventRule, error) {\n\trules := make([]EventRule, 0, 10)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDumpEventRulesQuery()\n\trows, err := dbHandle.QueryContext(ctx, q)\n\tif err != nil {\n\t\treturn rules, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\trule, err := getEventRuleFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn rules, err\n\t\t}\n\t\trules = append(rules, rule)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn rules, err\n\t}\n\treturn getRulesWithActions(ctx, rules, dbHandle)\n}\n\nfunc sqlCommonGetRecentlyUpdatedRules(after int64, dbHandle sqlQuerier) ([]EventRule, error) {\n\trules := make([]EventRule, 0, 10)\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getRecentlyUpdatedRulesQuery()\n\trows, err := dbHandle.QueryContext(ctx, q, after)\n\tif err != nil {\n\t\treturn rules, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\trule, err := getEventRuleFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn rules, err\n\t\t}\n\t\trules = append(rules, rule)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn rules, err\n\t}\n\treturn getRulesWithActions(ctx, rules, dbHandle)\n}\n\nfunc sqlCommonGetEventRules(limit int, offset int, order string, dbHandle sqlQuerier) ([]EventRule, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getEventRulesQuery(order)\n\n\trules := make([]EventRule, 0, limit)\n\trows, err := dbHandle.QueryContext(ctx, q, limit, offset)\n\tif err != nil {\n\t\treturn rules, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\trule, err := getEventRuleFromDbRow(rows)\n\t\tif err != nil {\n\t\t\treturn rules, err\n\t\t}\n\t\trules = append(rules, rule)\n\t}\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn rules, err\n\t}\n\trules, err = getRulesWithActions(ctx, rules, dbHandle)\n\tif err != nil {\n\t\treturn rules, err\n\t}\n\tfor idx := range rules {\n\t\trules[idx].PrepareForRendering()\n\t}\n\treturn rules, nil\n}\n\nfunc sqlCommonAddEventRule(rule *EventRule, dbHandle *sql.DB) error {\n\tif err := rule.validate(); err != nil {\n\t\treturn err\n\t}\n\tconditions, err := json.Marshal(rule.Conditions)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tif config.IsShared == 1 {\n\t\t\t_, err := tx.ExecContext(ctx, getRemoveSoftDeletedRuleQuery(), rule.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tq := getAddEventRuleQuery()\n\t\t_, err := tx.ExecContext(ctx, q, rule.Name, rule.Description, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\tutil.GetTimeAsMsSinceEpoch(time.Now()), rule.Trigger, conditions, rule.Status)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn generateEventRuleActionsMapping(ctx, rule, tx)\n\t})\n}\n\nfunc sqlCommonUpdateEventRule(rule *EventRule, dbHandle *sql.DB) error {\n\tif err := rule.validate(); err != nil {\n\t\treturn err\n\t}\n\tconditions, err := json.Marshal(rule.Conditions)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tq := getUpdateEventRuleQuery()\n\t\t_, err := tx.ExecContext(ctx, q, rule.Description, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\t\trule.Trigger, conditions, rule.Status, rule.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn generateEventRuleActionsMapping(ctx, rule, tx)\n\t})\n}\n\nfunc sqlCommonDeleteEventRule(rule EventRule, softDelete bool, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\treturn sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {\n\t\tif softDelete {\n\t\t\tq := getClearRuleActionMappingQuery()\n\t\t\t_, err := tx.ExecContext(ctx, q, rule.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tq := getDeleteEventRuleQuery(softDelete)\n\t\tif softDelete {\n\t\t\tts := util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t\tres, err := tx.ExecContext(ctx, q, ts, ts, rule.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn sqlCommonRequireRowAffected(res)\n\t\t}\n\t\tres, err := tx.ExecContext(ctx, q, rule.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err = sqlCommonRequireRowAffected(res); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn sqlCommonDeleteTask(rule.Name, tx)\n\t})\n}\n\nfunc sqlCommonGetTaskByName(name string, dbHandle sqlQuerier) (Task, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\ttask := Task{\n\t\tName: name,\n\t}\n\tq := getTaskByNameQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, name)\n\terr := row.Scan(&task.UpdateAt, &task.Version)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn task, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t}\n\treturn task, err\n}\n\nfunc sqlCommonAddTask(name string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddTaskQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, name, util.GetTimeAsMsSinceEpoch(time.Now()))\n\treturn err\n}\n\nfunc sqlCommonUpdateTask(name string, version int64, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateTaskQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), name, version)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonUpdateTaskTimestamp(name string, dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateTaskTimestampQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonDeleteTask(name string, dbHandle sqlQuerier) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDeleteTaskQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, name)\n\treturn err\n}\n\nfunc sqlCommonAddNode(dbHandle *sql.DB) error {\n\tif err := currentNode.validate(); err != nil {\n\t\treturn fmt.Errorf(\"unable to register cluster node: %w\", err)\n\t}\n\tdata, err := json.Marshal(currentNode.Data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getAddNodeQuery()\n\t_, err = dbHandle.ExecContext(ctx, q, currentNode.Name, data, util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tutil.GetTimeAsMsSinceEpoch(time.Now()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to register cluster node: %w\", err)\n\t}\n\tproviderLog(logger.LevelInfo, \"registered as cluster node %q, port: %d, proto: %s\",\n\t\tcurrentNode.Name, currentNode.Data.Port, currentNode.Data.Proto)\n\n\treturn nil\n}\n\nfunc sqlCommonGetNodeByName(name string, dbHandle *sql.DB) (Node, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tvar data []byte\n\tvar node Node\n\n\tq := getNodeByNameQuery()\n\trow := dbHandle.QueryRowContext(ctx, q, name, util.GetTimeAsMsSinceEpoch(time.Now().Add(activeNodeTimeDiff)))\n\terr := row.Scan(&node.Name, &data, &node.CreatedAt, &node.UpdatedAt)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn node, util.NewRecordNotFoundError(err.Error())\n\t\t}\n\t\treturn node, err\n\t}\n\terr = json.Unmarshal(data, &node.Data)\n\treturn node, err\n}\n\nfunc sqlCommonGetNodes(dbHandle *sql.DB) ([]Node, error) {\n\tvar nodes []Node\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getNodesQuery()\n\trows, err := dbHandle.QueryContext(ctx, q, currentNode.Name,\n\t\tutil.GetTimeAsMsSinceEpoch(time.Now().Add(activeNodeTimeDiff)))\n\tif err != nil {\n\t\treturn nodes, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar node Node\n\t\tvar data []byte\n\n\t\terr = rows.Scan(&node.Name, &data, &node.CreatedAt, &node.UpdatedAt)\n\t\tif err != nil {\n\t\t\treturn nodes, err\n\t\t}\n\t\terr = json.Unmarshal(data, &node.Data)\n\t\tif err != nil {\n\t\t\treturn nodes, err\n\t\t}\n\t\tnodes = append(nodes, node)\n\t}\n\n\treturn nodes, rows.Err()\n}\n\nfunc sqlCommonUpdateNodeTimestamp(dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateNodeTimestampQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), currentNode.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonCleanupNodes(dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getCleanupNodesQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now().Add(10*activeNodeTimeDiff)))\n\treturn err\n}\n\nfunc sqlCommonGetConfigs(dbHandle sqlQuerier) (Configs, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tvar result Configs\n\tvar configs []byte\n\tq := getConfigsQuery()\n\terr := dbHandle.QueryRowContext(ctx, q).Scan(&configs)\n\tif err != nil {\n\t\treturn result, err\n\t}\n\terr = json.Unmarshal(configs, &result)\n\treturn result, err\n}\n\nfunc sqlCommonSetConfigs(configs *Configs, dbHandle *sql.DB) error {\n\tif err := configs.validate(); err != nil {\n\t\treturn err\n\t}\n\tasJSON, err := json.Marshal(configs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getUpdateConfigsQuery()\n\tres, err := dbHandle.ExecContext(ctx, q, asJSON)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn sqlCommonRequireRowAffected(res)\n}\n\nfunc sqlCommonGetDatabaseVersion(dbHandle sqlQuerier, showInitWarn bool) (schemaVersion, error) {\n\tvar result schemaVersion\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tq := getDatabaseVersionQuery()\n\tstmt, err := dbHandle.PrepareContext(ctx, q)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error preparing database query %q: %v\", q, err)\n\t\tif showInitWarn && strings.Contains(err.Error(), sqlTableSchemaVersion) {\n\t\t\tlogger.WarnToConsole(\"database query error, did you forgot to run the \\\"initprovider\\\" command?\")\n\t\t}\n\t\treturn result, err\n\t}\n\tdefer stmt.Close()\n\trow := stmt.QueryRowContext(ctx)\n\terr = row.Scan(&result.Version)\n\treturn result, err\n}\n\nfunc sqlCommonRequireRowAffected(res sql.Result) error {\n\taffected, err := res.RowsAffected()\n\tif err == nil && affected == 0 {\n\t\treturn util.NewRecordNotFoundError(sql.ErrNoRows.Error())\n\t}\n\treturn nil\n}\n\nfunc sqlCommonUpdateDatabaseVersion(ctx context.Context, dbHandle sqlQuerier, version int) error {\n\tq := getUpdateDBVersionQuery()\n\t_, err := dbHandle.ExecContext(ctx, q, version)\n\treturn err\n}\n\nfunc sqlCommonExecSQLAndUpdateDBVersion(dbHandle *sql.DB, sqlQueries []string, newVersion int, isUp bool) error {\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tconn, err := dbHandle.Conn(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get connection from pool: %w\", err)\n\t}\n\tdefer conn.Close()\n\n\tif err := sqlAcquireLock(conn); err != nil {\n\t\treturn err\n\t}\n\tdefer sqlReleaseLock(conn)\n\n\tif newVersion > 0 {\n\t\tcurrentVersion, err := sqlCommonGetDatabaseVersion(conn, false)\n\t\tif err == nil {\n\t\t\tif (isUp && currentVersion.Version >= newVersion) || (!isUp && currentVersion.Version <= newVersion) {\n\t\t\t\tproviderLog(logger.LevelInfo, \"current schema version: %v, requested: %v, did you execute simultaneous migrations?\",\n\t\t\t\t\tcurrentVersion.Version, newVersion)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn sqlCommonExecuteTxOnConn(ctx, conn, func(tx *sql.Tx) error {\n\t\tfor _, q := range sqlQueries {\n\t\t\tif strings.TrimSpace(q) == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t_, err := tx.ExecContext(ctx, q)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif newVersion == 0 {\n\t\t\treturn nil\n\t\t}\n\t\treturn sqlCommonUpdateDatabaseVersion(ctx, tx, newVersion)\n\t})\n}\n\nfunc sqlAcquireLock(dbHandle *sql.Conn) error {\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tswitch config.Driver {\n\tcase PGSQLDataProviderName:\n\t\t_, err := dbHandle.ExecContext(ctx, `SELECT pg_advisory_lock(101,1)`)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to get advisory lock: %w\", err)\n\t\t}\n\t\tproviderLog(logger.LevelInfo, \"acquired database lock\")\n\tcase MySQLDataProviderName:\n\t\tvar lockResult sql.NullInt64\n\t\terr := dbHandle.QueryRowContext(ctx, `SELECT GET_LOCK('sftpgo.migration',30)`).Scan(&lockResult)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to get lock: %w\", err)\n\t\t}\n\t\tif !lockResult.Valid {\n\t\t\treturn errors.New(\"unable to get lock: null value returned\")\n\t\t}\n\t\tif lockResult.Int64 != 1 {\n\t\t\treturn fmt.Errorf(\"unable to get lock, result: %d\", lockResult.Int64)\n\t\t}\n\t\tproviderLog(logger.LevelInfo, \"acquired database lock\")\n\t}\n\n\treturn nil\n}\n\nfunc sqlReleaseLock(dbHandle *sql.Conn) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\tswitch config.Driver {\n\tcase PGSQLDataProviderName:\n\t\t_, err := dbHandle.ExecContext(ctx, `SELECT pg_advisory_unlock(101,1)`)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"unable to release lock: %v\", err)\n\t\t} else {\n\t\t\tproviderLog(logger.LevelInfo, \"released database lock\")\n\t\t}\n\tcase MySQLDataProviderName:\n\t\t_, err := dbHandle.ExecContext(ctx, `SELECT RELEASE_LOCK('sftpgo.migration')`)\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelWarn, \"unable to release lock: %v\", err)\n\t\t} else {\n\t\t\tproviderLog(logger.LevelInfo, \"released database lock\")\n\t\t}\n\t}\n}\n\nfunc sqlCommonExecuteTxOnConn(ctx context.Context, conn *sql.Conn, txFn func(*sql.Tx) error) error {\n\ttx, err := conn.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = txFn(tx)\n\tif err != nil {\n\t\ttx.Rollback() //nolint:errcheck\n\t\treturn err\n\t}\n\treturn tx.Commit()\n}\n\nfunc sqlCommonExecuteTx(ctx context.Context, dbHandle *sql.DB, txFn func(*sql.Tx) error) error {\n\tif config.Driver == CockroachDataProviderName {\n\t\treturn crdb.ExecuteTx(ctx, dbHandle, nil, txFn)\n\t}\n\n\ttx, err := dbHandle.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = txFn(tx)\n\tif err != nil {\n\t\t// we don't change the returned error\n\t\ttx.Rollback() //nolint:errcheck\n\t\treturn err\n\t}\n\treturn tx.Commit()\n}\n"
  },
  {
    "path": "internal/dataprovider/sqlite.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !nosqlite && cgo\n\npackage dataprovider\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mattn/go-sqlite3\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tsqliteResetSQL = `DROP TABLE IF EXISTS \"{{api_keys}}\";\nDROP TABLE IF EXISTS \"{{users_folders_mapping}}\";\nDROP TABLE IF EXISTS \"{{users_groups_mapping}}\";\nDROP TABLE IF EXISTS \"{{admins_groups_mapping}}\";\nDROP TABLE IF EXISTS \"{{groups_folders_mapping}}\";\nDROP TABLE IF EXISTS \"{{shares_groups_mapping}}\";\nDROP TABLE IF EXISTS \"{{admins}}\";\nDROP TABLE IF EXISTS \"{{folders}}\";\nDROP TABLE IF EXISTS \"{{shares}}\";\nDROP TABLE IF EXISTS \"{{users}}\";\nDROP TABLE IF EXISTS \"{{groups}}\";\nDROP TABLE IF EXISTS \"{{defender_events}}\";\nDROP TABLE IF EXISTS \"{{defender_hosts}}\";\nDROP TABLE IF EXISTS \"{{active_transfers}}\";\nDROP TABLE IF EXISTS \"{{shared_sessions}}\";\nDROP TABLE IF EXISTS \"{{rules_actions_mapping}}\";\nDROP TABLE IF EXISTS \"{{events_rules}}\";\nDROP TABLE IF EXISTS \"{{events_actions}}\";\nDROP TABLE IF EXISTS \"{{tasks}}\";\nDROP TABLE IF EXISTS \"{{roles}}\";\nDROP TABLE IF EXISTS \"{{ip_lists}}\";\nDROP TABLE IF EXISTS \"{{configs}}\";\nDROP TABLE IF EXISTS \"{{schema_version}}\";\n`\n\tsqliteInitialSQL = `CREATE TABLE \"{{schema_version}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"version\" integer NOT NULL);\nCREATE TABLE \"{{roles}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL);\nCREATE TABLE \"{{admins}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"username\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"password\" varchar(255) NOT NULL, \"email\" varchar(255) NULL, \"status\" integer NOT NULL,\n\"permissions\" text NOT NULL, \"filters\" text NULL, \"additional_info\" text NULL, \"last_login\" bigint NOT NULL,\n\"role_id\" integer NULL REFERENCES \"{{roles}}\" (\"id\") ON DELETE NO ACTION, \"created_at\" bigint NOT NULL,\n\"updated_at\" bigint NOT NULL);\nCREATE TABLE \"{{active_transfers}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"connection_id\" varchar(100) NOT NULL,\n\"transfer_id\" bigint NOT NULL, \"transfer_type\" integer NOT NULL, \"username\" varchar(255) NOT NULL,\n\"folder_name\" varchar(255) NULL, \"ip\" varchar(50) NOT NULL, \"truncated_size\" bigint NOT NULL,\n\"current_ul_size\" bigint NOT NULL, \"current_dl_size\" bigint NOT NULL, \"created_at\" bigint NOT NULL,\n\"updated_at\" bigint NOT NULL);\nCREATE TABLE \"{{defender_hosts}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"ip\" varchar(50) NOT NULL UNIQUE,\n\"ban_time\" bigint NOT NULL, \"updated_at\" bigint NOT NULL);\nCREATE TABLE \"{{defender_events}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"date_time\" bigint NOT NULL,\n\"score\" integer NOT NULL, \"host_id\" integer NOT NULL REFERENCES \"{{defender_hosts}}\" (\"id\") ON DELETE CASCADE\nDEFERRABLE INITIALLY DEFERRED);\nCREATE TABLE \"{{folders}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"path\" text NULL, \"used_quota_size\" bigint NOT NULL, \"used_quota_files\" integer NOT NULL,\n\"last_quota_update\" bigint NOT NULL, \"filesystem\" text NULL);\nCREATE TABLE \"{{groups}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL, \"user_settings\" text NULL);\nCREATE TABLE \"{{shared_sessions}}\" (\"key\" varchar(128) NOT NULL, \"type\" integer NOT NULL,\n\"data\" text NOT NULL, \"timestamp\" bigint NOT NULL, PRIMARY KEY (\"key\", \"type\"));\nCREATE TABLE \"{{users}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"username\" varchar(255) NOT NULL UNIQUE,\n\"status\" integer NOT NULL, \"expiration_date\" bigint NOT NULL, \"description\" varchar(512) NULL, \"password\" text NULL,\n\"public_keys\" text NULL, \"home_dir\" text NOT NULL, \"uid\" bigint NOT NULL, \"gid\" bigint NOT NULL,\n\"max_sessions\" integer NOT NULL, \"quota_size\" bigint NOT NULL, \"quota_files\" integer NOT NULL, \"permissions\" text NOT NULL,\n\"used_quota_size\" bigint NOT NULL, \"used_quota_files\" integer NOT NULL, \"last_quota_update\" bigint NOT NULL,\n\"upload_bandwidth\" integer NOT NULL, \"download_bandwidth\" integer NOT NULL, \"last_login\" bigint NOT NULL,\n\"filters\" text NULL, \"filesystem\" text NULL, \"additional_info\" text NULL, \"created_at\" bigint NOT NULL,\n\"updated_at\" bigint NOT NULL, \"email\" varchar(255) NULL, \"upload_data_transfer\" integer NOT NULL,\n\"download_data_transfer\" integer NOT NULL, \"total_data_transfer\" integer NOT NULL, \"used_upload_data_transfer\" bigint NOT NULL,\n\"used_download_data_transfer\" bigint NOT NULL, \"deleted_at\" bigint NOT NULL, \"first_download\" bigint NOT NULL,\n\"first_upload\" bigint NOT NULL, \"last_password_change\" bigint NOT NULL, \"role_id\" integer NULL REFERENCES \"{{roles}}\" (\"id\") ON DELETE SET NULL);\nCREATE TABLE \"{{groups_folders_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY,\n\"folder_id\" integer NOT NULL REFERENCES \"{{folders}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"group_id\" integer NOT NULL REFERENCES \"{{groups}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"virtual_path\" text NOT NULL, \"quota_size\" bigint NOT NULL, \"quota_files\" integer NOT NULL, \"sort_order\" integer NOT NULL,\nCONSTRAINT \"{{prefix}}unique_group_folder_mapping\" UNIQUE (\"group_id\", \"folder_id\"));\nCREATE TABLE \"{{users_groups_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY,\n\"user_id\" integer NOT NULL REFERENCES \"{{users}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"group_id\" integer NOT NULL REFERENCES \"{{groups}}\" (\"id\") ON DELETE NO ACTION,\n\"group_type\" integer NOT NULL, \"sort_order\" integer NOT NULL, CONSTRAINT \"{{prefix}}unique_user_group_mapping\" UNIQUE (\"user_id\", \"group_id\"));\nCREATE TABLE \"{{users_folders_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY,\n\"user_id\" integer NOT NULL REFERENCES \"{{users}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"folder_id\" integer NOT NULL REFERENCES \"{{folders}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"virtual_path\" text NOT NULL, \"quota_size\" bigint NOT NULL, \"quota_files\" integer NOT NULL, \"sort_order\" integer NOT NULL,\nCONSTRAINT \"{{prefix}}unique_user_folder_mapping\" UNIQUE (\"user_id\", \"folder_id\"));\nCREATE TABLE \"{{shares}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"share_id\" varchar(60) NOT NULL UNIQUE,\n\"name\" varchar(255) NOT NULL, \"description\" varchar(512) NULL, \"scope\" integer NOT NULL, \"paths\" text NOT NULL,\n\"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL, \"last_use_at\" bigint NOT NULL, \"expires_at\" bigint NOT NULL,\n\"password\" text NULL, \"max_tokens\" integer NOT NULL, \"used_tokens\" integer NOT NULL, \"allow_from\" text NULL, \"options\" text NULL,\n\"user_id\" integer NOT NULL REFERENCES \"{{users}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED);\nCREATE TABLE \"{{api_keys}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"name\" varchar(255) NOT NULL,\n\"key_id\" varchar(50) NOT NULL UNIQUE, \"api_key\" varchar(255) NOT NULL UNIQUE, \"scope\" integer NOT NULL,\n\"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL, \"last_use_at\" bigint NOT NULL, \"expires_at\" bigint NOT NULL,\n\"description\" text NULL, \"admin_id\" integer NULL REFERENCES \"{{admins}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"user_id\" integer NULL REFERENCES \"{{users}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED);\nCREATE TABLE \"{{events_rules}}\" (\"id\" integer NOT NULL PRIMARY KEY,\n\"name\" varchar(255) NOT NULL UNIQUE, \"status\" integer NOT NULL, \"description\" varchar(512) NULL, \"created_at\" bigint NOT NULL,\n\"updated_at\" bigint NOT NULL, \"trigger\" integer NOT NULL, \"conditions\" text NOT NULL, \"deleted_at\" bigint NOT NULL);\nCREATE TABLE \"{{events_actions}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"description\" varchar(512) NULL, \"type\" integer NOT NULL, \"options\" text NOT NULL);\nCREATE TABLE \"{{rules_actions_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY,\n\"rule_id\" integer NOT NULL REFERENCES \"{{events_rules}}\" (\"id\")  ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"action_id\" integer NOT NULL REFERENCES \"{{events_actions}}\" (\"id\")  ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,\n\"order\" integer NOT NULL, \"options\" text NOT NULL,\nCONSTRAINT \"{{prefix}}unique_rule_action_mapping\" UNIQUE (\"rule_id\", \"action_id\"));\nCREATE TABLE \"{{tasks}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"name\" varchar(255) NOT NULL UNIQUE,\n\"updated_at\" bigint NOT NULL, \"version\" bigint NOT NULL);\nCREATE TABLE \"{{admins_groups_mapping}}\" (\"id\" integer NOT NULL PRIMARY KEY,\n\"admin_id\" integer NOT NULL REFERENCES \"{{admins}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"group_id\" integer NOT NULL REFERENCES \"{{groups}}\" (\"id\") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,\n\"options\" text NOT NULL, \"sort_order\" integer NOT NULL, CONSTRAINT \"{{prefix}}unique_admin_group_mapping\" UNIQUE (\"admin_id\", \"group_id\"));\nCREATE TABLE \"{{ip_lists}}\" (\"id\" integer NOT NULL PRIMARY KEY,\n\"type\" integer NOT NULL, \"ipornet\" varchar(50) NOT NULL, \"mode\" integer NOT NULL, \"description\" varchar(512) NULL,\n\"first\" BLOB NOT NULL, \"last\" BLOB NOT NULL, \"ip_type\" integer NOT NULL, \"protocols\" integer NOT NULL,\n\"created_at\" bigint NOT NULL, \"updated_at\" bigint NOT NULL, \"deleted_at\" bigint NOT NULL,\nCONSTRAINT \"{{prefix}}unique_ipornet_type_mapping\" UNIQUE (\"type\", \"ipornet\"));\nCREATE TABLE \"{{configs}}\" (\"id\" integer NOT NULL PRIMARY KEY, \"configs\" text NOT NULL);\nINSERT INTO {{configs}} (configs) VALUES ('{}');\nCREATE INDEX \"{{prefix}}users_folders_mapping_folder_id_idx\" ON \"{{users_folders_mapping}}\" (\"folder_id\");\nCREATE INDEX \"{{prefix}}users_folders_mapping_user_id_idx\" ON \"{{users_folders_mapping}}\" (\"user_id\");\nCREATE INDEX \"{{prefix}}users_folders_mapping_sort_order_idx\" ON \"{{users_folders_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}users_groups_mapping_group_id_idx\" ON \"{{users_groups_mapping}}\" (\"group_id\");\nCREATE INDEX \"{{prefix}}users_groups_mapping_user_id_idx\" ON \"{{users_groups_mapping}}\" (\"user_id\");\nCREATE INDEX \"{{prefix}}users_groups_mapping_sort_order_idx\" ON \"{{users_groups_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}groups_folders_mapping_folder_id_idx\" ON \"{{groups_folders_mapping}}\" (\"folder_id\");\nCREATE INDEX \"{{prefix}}groups_folders_mapping_group_id_idx\" ON \"{{groups_folders_mapping}}\" (\"group_id\");\nCREATE INDEX \"{{prefix}}groups_folders_mapping_sort_order_idx\" ON \"{{groups_folders_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}api_keys_admin_id_idx\" ON \"{{api_keys}}\" (\"admin_id\");\nCREATE INDEX \"{{prefix}}api_keys_user_id_idx\" ON \"{{api_keys}}\" (\"user_id\");\nCREATE INDEX \"{{prefix}}users_updated_at_idx\" ON \"{{users}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}users_deleted_at_idx\" ON \"{{users}}\" (\"deleted_at\");\nCREATE INDEX \"{{prefix}}shares_user_id_idx\" ON \"{{shares}}\" (\"user_id\");\nCREATE INDEX \"{{prefix}}defender_hosts_updated_at_idx\" ON \"{{defender_hosts}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}defender_hosts_ban_time_idx\" ON \"{{defender_hosts}}\" (\"ban_time\");\nCREATE INDEX \"{{prefix}}defender_events_date_time_idx\" ON \"{{defender_events}}\" (\"date_time\");\nCREATE INDEX \"{{prefix}}defender_events_host_id_idx\" ON \"{{defender_events}}\" (\"host_id\");\nCREATE INDEX \"{{prefix}}active_transfers_connection_id_idx\" ON \"{{active_transfers}}\" (\"connection_id\");\nCREATE INDEX \"{{prefix}}active_transfers_transfer_id_idx\" ON \"{{active_transfers}}\" (\"transfer_id\");\nCREATE INDEX \"{{prefix}}active_transfers_updated_at_idx\" ON \"{{active_transfers}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}shared_sessions_type_idx\" ON \"{{shared_sessions}}\" (\"type\");\nCREATE INDEX \"{{prefix}}shared_sessions_timestamp_idx\" ON \"{{shared_sessions}}\" (\"timestamp\");\nCREATE INDEX \"{{prefix}}events_rules_updated_at_idx\" ON \"{{events_rules}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}events_rules_deleted_at_idx\" ON \"{{events_rules}}\" (\"deleted_at\");\nCREATE INDEX \"{{prefix}}events_rules_trigger_idx\" ON \"{{events_rules}}\" (\"trigger\");\nCREATE INDEX \"{{prefix}}rules_actions_mapping_rule_id_idx\" ON \"{{rules_actions_mapping}}\" (\"rule_id\");\nCREATE INDEX \"{{prefix}}rules_actions_mapping_action_id_idx\" ON \"{{rules_actions_mapping}}\" (\"action_id\");\nCREATE INDEX \"{{prefix}}rules_actions_mapping_order_idx\" ON \"{{rules_actions_mapping}}\" (\"order\");\nCREATE INDEX \"{{prefix}}admins_groups_mapping_admin_id_idx\" ON \"{{admins_groups_mapping}}\" (\"admin_id\");\nCREATE INDEX \"{{prefix}}admins_groups_mapping_group_id_idx\" ON \"{{admins_groups_mapping}}\" (\"group_id\");\nCREATE INDEX \"{{prefix}}admins_groups_mapping_sort_order_idx\" ON \"{{admins_groups_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}users_role_id_idx\" ON \"{{users}}\" (\"role_id\");\nCREATE INDEX \"{{prefix}}admins_role_id_idx\" ON \"{{admins}}\" (\"role_id\");\nCREATE INDEX \"{{prefix}}ip_lists_type_idx\" ON \"{{ip_lists}}\" (\"type\");\nCREATE INDEX \"{{prefix}}ip_lists_ipornet_idx\" ON \"{{ip_lists}}\" (\"ipornet\");\nCREATE INDEX \"{{prefix}}ip_lists_ip_type_idx\" ON \"{{ip_lists}}\" (\"ip_type\");\nCREATE INDEX \"{{prefix}}ip_lists_ip_updated_at_idx\" ON \"{{ip_lists}}\" (\"updated_at\");\nCREATE INDEX \"{{prefix}}ip_lists_ip_deleted_at_idx\" ON \"{{ip_lists}}\" (\"deleted_at\");\nCREATE INDEX \"{{prefix}}ip_lists_first_last_idx\" ON \"{{ip_lists}}\" (\"first\", \"last\");\nINSERT INTO {{schema_version}} (version) VALUES (33);\n`\n\tsqliteV34SQL = `\nCREATE TABLE \"{{shares_groups_mapping}}\" (\n    \"id\" integer NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"share_id\" integer NOT NULL REFERENCES \"{{shares}}\" (\"id\") ON DELETE CASCADE,\n    \"group_id\" integer NOT NULL REFERENCES \"{{groups}}\" (\"id\") ON DELETE CASCADE,\n    \"permissions\" integer NOT NULL,\n    \"sort_order\" integer NOT NULL,\n    CONSTRAINT \"{{prefix}}unique_share_group_mapping\" UNIQUE (\"share_id\", \"group_id\")\n);\nCREATE INDEX \"{{prefix}}shares_groups_mapping_sort_order_idx\" ON \"{{shares_groups_mapping}}\" (\"sort_order\");\nCREATE INDEX \"{{prefix}}shares_groups_mapping_group_id_idx\" ON \"{{shares_groups_mapping}}\" (\"group_id\");\nCREATE INDEX \"{{prefix}}shares_groups_mapping_share_id_idx\" ON \"{{shares_groups_mapping}}\" (\"share_id\");\n`\n\tsqliteV34DownSQL = `DROP TABLE IF EXISTS \"{{shares_groups_mapping}}\";`\n)\n\n// SQLiteProvider defines the auth provider for SQLite database\ntype SQLiteProvider struct {\n\tdbHandle *sql.DB\n}\n\nfunc init() {\n\tversion.AddFeature(\"+sqlite\")\n}\n\nfunc initializeSQLiteProvider(basePath string) error {\n\tvar connectionString string\n\n\tif config.ConnectionString == \"\" {\n\t\tdbPath := config.Name\n\t\tif !util.IsFileInputValid(dbPath) {\n\t\t\treturn fmt.Errorf(\"invalid database path: %q\", dbPath)\n\t\t}\n\t\tif !filepath.IsAbs(dbPath) {\n\t\t\tdbPath = filepath.Join(basePath, dbPath)\n\t\t}\n\t\tconnectionString = fmt.Sprintf(\"file:%s?cache=shared&_foreign_keys=1\", dbPath)\n\t} else {\n\t\tconnectionString = config.ConnectionString\n\t}\n\tdbHandle, err := sql.Open(\"sqlite3\", connectionString)\n\tif err != nil {\n\t\tproviderLog(logger.LevelError, \"error creating sqlite database handler, connection string: %q, error: %v\",\n\t\t\tconnectionString, err)\n\t\treturn err\n\t}\n\tproviderLog(logger.LevelDebug, \"sqlite database handle created, connection string: %q\", connectionString)\n\tdbHandle.SetMaxOpenConns(1)\n\tprovider = &SQLiteProvider{dbHandle: dbHandle}\n\treturn executePragmaOptimize(dbHandle)\n}\n\nfunc (p *SQLiteProvider) checkAvailability() error {\n\treturn sqlCommonCheckAvailability(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {\n\treturn sqlCommonValidateUserAndPass(username, password, ip, protocol, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error) {\n\treturn sqlCommonValidateUserAndTLSCertificate(username, protocol, tlsCert, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte, isSSHCert bool) (User, string, error) {\n\treturn sqlCommonValidateUserAndPubKey(username, publicKey, isSSHCert, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error {\n\treturn sqlCommonUpdateTransferQuota(username, uploadSize, downloadSize, reset, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {\n\treturn sqlCommonUpdateQuota(username, filesAdd, sizeAdd, reset, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getUsedQuota(username string) (int, int64, int64, int64, error) {\n\treturn sqlCommonGetUsedQuota(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getAdminSignature(username string) (string, error) {\n\treturn sqlCommonGetAdminSignature(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getUserSignature(username string) (string, error) {\n\treturn sqlCommonGetUserSignature(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) setUpdatedAt(username string) {\n\tsqlCommonSetUpdatedAt(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateLastLogin(username string) error {\n\treturn sqlCommonUpdateLastLogin(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateAdminLastLogin(username string) error {\n\treturn sqlCommonUpdateAdminLastLogin(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) userExists(username, role string) (User, error) {\n\treturn sqlCommonGetUserByUsername(username, role, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addUser(user *User) error {\n\treturn p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername)\n}\n\nfunc (p *SQLiteProvider) updateUser(user *User) error {\n\treturn p.normalizeError(sqlCommonUpdateUser(user, p.dbHandle), -1)\n}\n\nfunc (p *SQLiteProvider) deleteUser(user User, softDelete bool) error {\n\treturn sqlCommonDeleteUser(user, softDelete, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateUserPassword(username, password string) error {\n\treturn sqlCommonUpdateUserPassword(username, password, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpUsers() ([]User, error) {\n\treturn sqlCommonDumpUsers(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getRecentlyUpdatedUsers(after int64) ([]User, error) {\n\treturn sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {\n\treturn sqlCommonGetUsers(limit, offset, order, role, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {\n\treturn sqlCommonGetUsersForQuotaCheck(toFetch, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpFolders() ([]vfs.BaseVirtualFolder, error) {\n\treturn sqlCommonDumpFolders(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error) {\n\treturn sqlCommonGetFolders(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\treturn sqlCommonGetFolderByName(ctx, name, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error {\n\treturn p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName)\n}\n\nfunc (p *SQLiteProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {\n\treturn sqlCommonUpdateFolder(folder, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) deleteFolder(folder vfs.BaseVirtualFolder) error {\n\treturn sqlCommonDeleteFolder(folder, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error {\n\treturn sqlCommonUpdateFolderQuota(name, filesAdd, sizeAdd, reset, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getUsedFolderQuota(name string) (int, int64, error) {\n\treturn sqlCommonGetFolderUsedQuota(name, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getGroups(limit, offset int, order string, minimal bool) ([]Group, error) {\n\treturn sqlCommonGetGroups(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getGroupsWithNames(names []string) ([]Group, error) {\n\treturn sqlCommonGetGroupsWithNames(names, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getUsersInGroups(names []string) ([]string, error) {\n\treturn sqlCommonGetUsersInGroups(names, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) groupExists(name string) (Group, error) {\n\treturn sqlCommonGetGroupByName(name, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addGroup(group *Group) error {\n\treturn p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName)\n}\n\nfunc (p *SQLiteProvider) updateGroup(group *Group) error {\n\treturn sqlCommonUpdateGroup(group, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) deleteGroup(group Group) error {\n\treturn sqlCommonDeleteGroup(group, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpGroups() ([]Group, error) {\n\treturn sqlCommonDumpGroups(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) adminExists(username string) (Admin, error) {\n\treturn sqlCommonGetAdminByUsername(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addAdmin(admin *Admin) error {\n\treturn p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername)\n}\n\nfunc (p *SQLiteProvider) updateAdmin(admin *Admin) error {\n\treturn p.normalizeError(sqlCommonUpdateAdmin(admin, p.dbHandle), -1)\n}\n\nfunc (p *SQLiteProvider) deleteAdmin(admin Admin) error {\n\treturn sqlCommonDeleteAdmin(admin, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getAdmins(limit int, offset int, order string) ([]Admin, error) {\n\treturn sqlCommonGetAdmins(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpAdmins() ([]Admin, error) {\n\treturn sqlCommonDumpAdmins(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {\n\treturn sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) apiKeyExists(keyID string) (APIKey, error) {\n\treturn sqlCommonGetAPIKeyByID(keyID, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addAPIKey(apiKey *APIKey) error {\n\treturn p.normalizeError(sqlCommonAddAPIKey(apiKey, p.dbHandle), -1)\n}\n\nfunc (p *SQLiteProvider) updateAPIKey(apiKey *APIKey) error {\n\treturn p.normalizeError(sqlCommonUpdateAPIKey(apiKey, p.dbHandle), -1)\n}\n\nfunc (p *SQLiteProvider) deleteAPIKey(apiKey APIKey) error {\n\treturn sqlCommonDeleteAPIKey(apiKey, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {\n\treturn sqlCommonGetAPIKeys(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpAPIKeys() ([]APIKey, error) {\n\treturn sqlCommonDumpAPIKeys(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateAPIKeyLastUse(keyID string) error {\n\treturn sqlCommonUpdateAPIKeyLastUse(keyID, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) shareExists(shareID, username string) (Share, error) {\n\treturn sqlCommonGetShareByID(shareID, username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addShare(share *Share) error {\n\treturn p.normalizeError(sqlCommonAddShare(share, p.dbHandle), fieldName)\n}\n\nfunc (p *SQLiteProvider) updateShare(share *Share) error {\n\treturn p.normalizeError(sqlCommonUpdateShare(share, p.dbHandle), -1)\n}\n\nfunc (p *SQLiteProvider) deleteShare(share Share) error {\n\treturn sqlCommonDeleteShare(share, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getShares(limit int, offset int, order, username string) ([]Share, error) {\n\treturn sqlCommonGetShares(limit, offset, order, username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpShares() ([]Share, error) {\n\treturn sqlCommonDumpShares(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateShareLastUse(shareID string, numTokens int) error {\n\treturn sqlCommonUpdateShareLastUse(shareID, numTokens, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getDefenderHosts(from int64, limit int) ([]DefenderEntry, error) {\n\treturn sqlCommonGetDefenderHosts(from, limit, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getDefenderHostByIP(ip string, from int64) (DefenderEntry, error) {\n\treturn sqlCommonGetDefenderHostByIP(ip, from, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) isDefenderHostBanned(ip string) (DefenderEntry, error) {\n\treturn sqlCommonIsDefenderHostBanned(ip, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateDefenderBanTime(ip string, minutes int) error {\n\treturn sqlCommonDefenderIncrementBanTime(ip, minutes, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) deleteDefenderHost(ip string) error {\n\treturn sqlCommonDeleteDefenderHost(ip, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addDefenderEvent(ip string, score int) error {\n\treturn sqlCommonAddDefenderHostAndEvent(ip, score, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) setDefenderBanTime(ip string, banTime int64) error {\n\treturn sqlCommonSetDefenderBanTime(ip, banTime, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) cleanupDefender(from int64) error {\n\treturn sqlCommonDefenderCleanup(from, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addActiveTransfer(transfer ActiveTransfer) error {\n\treturn sqlCommonAddActiveTransfer(transfer, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateActiveTransferSizes(ulSize, dlSize, transferID int64, connectionID string) error {\n\treturn sqlCommonUpdateActiveTransferSizes(ulSize, dlSize, transferID, connectionID, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) removeActiveTransfer(transferID int64, connectionID string) error {\n\treturn sqlCommonRemoveActiveTransfer(transferID, connectionID, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) cleanupActiveTransfers(before time.Time) error {\n\treturn sqlCommonCleanupActiveTransfers(before, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getActiveTransfers(from time.Time) ([]ActiveTransfer, error) {\n\treturn sqlCommonGetActiveTransfers(from, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addSharedSession(session Session) error {\n\treturn sqlCommonAddSession(session, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) deleteSharedSession(key string, sessionType SessionType) error {\n\treturn sqlCommonDeleteSession(key, sessionType, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {\n\treturn sqlCommonGetSession(key, sessionType, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {\n\treturn sqlCommonCleanupSessions(sessionType, before, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {\n\treturn sqlCommonGetEventActions(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpEventActions() ([]BaseEventAction, error) {\n\treturn sqlCommonDumpEventActions(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) eventActionExists(name string) (BaseEventAction, error) {\n\treturn sqlCommonGetEventActionByName(name, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addEventAction(action *BaseEventAction) error {\n\treturn p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName)\n}\n\nfunc (p *SQLiteProvider) updateEventAction(action *BaseEventAction) error {\n\treturn sqlCommonUpdateEventAction(action, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) deleteEventAction(action BaseEventAction) error {\n\treturn sqlCommonDeleteEventAction(action, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {\n\treturn sqlCommonGetEventRules(limit, offset, order, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpEventRules() ([]EventRule, error) {\n\treturn sqlCommonDumpEventRules(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {\n\treturn sqlCommonGetRecentlyUpdatedRules(after, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) eventRuleExists(name string) (EventRule, error) {\n\treturn sqlCommonGetEventRuleByName(name, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addEventRule(rule *EventRule) error {\n\treturn p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName)\n}\n\nfunc (p *SQLiteProvider) updateEventRule(rule *EventRule) error {\n\treturn sqlCommonUpdateEventRule(rule, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) deleteEventRule(rule EventRule, softDelete bool) error {\n\treturn sqlCommonDeleteEventRule(rule, softDelete, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getTaskByName(name string) (Task, error) {\n\treturn sqlCommonGetTaskByName(name, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addTask(name string) error {\n\treturn sqlCommonAddTask(name, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateTask(name string, version int64) error {\n\treturn sqlCommonUpdateTask(name, version, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) updateTaskTimestamp(name string) error {\n\treturn sqlCommonUpdateTaskTimestamp(name, p.dbHandle)\n}\n\nfunc (*SQLiteProvider) addNode() error {\n\treturn ErrNotImplemented\n}\n\nfunc (*SQLiteProvider) getNodeByName(_ string) (Node, error) {\n\treturn Node{}, ErrNotImplemented\n}\n\nfunc (*SQLiteProvider) getNodes() ([]Node, error) {\n\treturn nil, ErrNotImplemented\n}\n\nfunc (*SQLiteProvider) updateNodeTimestamp() error {\n\treturn ErrNotImplemented\n}\n\nfunc (*SQLiteProvider) cleanupNodes() error {\n\treturn ErrNotImplemented\n}\n\nfunc (p *SQLiteProvider) roleExists(name string) (Role, error) {\n\treturn sqlCommonGetRoleByName(name, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addRole(role *Role) error {\n\treturn p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName)\n}\n\nfunc (p *SQLiteProvider) updateRole(role *Role) error {\n\treturn sqlCommonUpdateRole(role, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) deleteRole(role Role) error {\n\treturn sqlCommonDeleteRole(role, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getRoles(limit int, offset int, order string, minimal bool) ([]Role, error) {\n\treturn sqlCommonGetRoles(limit, offset, order, minimal, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpRoles() ([]Role, error) {\n\treturn sqlCommonDumpRoles(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) ipListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error) {\n\treturn sqlCommonGetIPListEntry(ipOrNet, listType, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) addIPListEntry(entry *IPListEntry) error {\n\treturn p.normalizeError(sqlCommonAddIPListEntry(entry, p.dbHandle), fieldIPNet)\n}\n\nfunc (p *SQLiteProvider) updateIPListEntry(entry *IPListEntry) error {\n\treturn sqlCommonUpdateIPListEntry(entry, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) deleteIPListEntry(entry IPListEntry, softDelete bool) error {\n\treturn sqlCommonDeleteIPListEntry(entry, softDelete, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {\n\treturn sqlCommonGetIPListEntries(listType, filter, from, order, limit, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getRecentlyUpdatedIPListEntries(after int64) ([]IPListEntry, error) {\n\treturn sqlCommonGetRecentlyUpdatedIPListEntries(after, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) dumpIPListEntries() ([]IPListEntry, error) {\n\treturn sqlCommonDumpIPListEntries(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) countIPListEntries(listType IPListType) (int64, error) {\n\treturn sqlCommonCountIPListEntries(listType, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getListEntriesForIP(ip string, listType IPListType) ([]IPListEntry, error) {\n\treturn sqlCommonGetListEntriesForIP(ip, listType, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) getConfigs() (Configs, error) {\n\treturn sqlCommonGetConfigs(p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) setConfigs(configs *Configs) error {\n\treturn sqlCommonSetConfigs(configs, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) setFirstDownloadTimestamp(username string) error {\n\treturn sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) setFirstUploadTimestamp(username string) error {\n\treturn sqlCommonSetFirstUploadTimestamp(username, p.dbHandle)\n}\n\nfunc (p *SQLiteProvider) close() error {\n\treturn p.dbHandle.Close()\n}\n\nfunc (p *SQLiteProvider) reloadConfig() error {\n\treturn nil\n}\n\n// initializeDatabase creates the initial database structure\nfunc (p *SQLiteProvider) initializeDatabase() error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, false)\n\tif err == nil && dbVersion.Version > 0 {\n\t\treturn ErrNoInitRequired\n\t}\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn errSchemaVersionEmpty\n\t}\n\tlogger.InfoToConsole(\"creating initial database schema, version 33\")\n\tproviderLog(logger.LevelInfo, \"creating initial database schema, version 33\")\n\tsql := sqlReplaceAll(sqliteInitialSQL)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 33, true)\n}\n\nfunc (p *SQLiteProvider) migrateDatabase() error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch version := dbVersion.Version; {\n\tcase version == sqlDatabaseVersion:\n\t\tproviderLog(logger.LevelDebug, \"sql database is up to date, current version: %d\", version)\n\t\treturn ErrNoInitRequired\n\tcase version < 33:\n\t\terr = errSchemaVersionTooOld(version)\n\t\tproviderLog(logger.LevelError, \"%v\", err)\n\t\tlogger.ErrorToConsole(\"%v\", err)\n\t\treturn err\n\tcase version == 33:\n\t\treturn updateSQLiteDatabaseFromV33(p.dbHandle)\n\tdefault:\n\t\tif version > sqlDatabaseVersion {\n\t\t\tproviderLog(logger.LevelError, \"database schema version %d is newer than the supported one: %d\", version,\n\t\t\t\tsqlDatabaseVersion)\n\t\t\tlogger.WarnToConsole(\"database schema version %d is newer than the supported one: %d\", version,\n\t\t\t\tsqlDatabaseVersion)\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"database schema version not handled: %d\", version)\n\t}\n}\n\nfunc (p *SQLiteProvider) revertDatabase(targetVersion int) error {\n\tdbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif dbVersion.Version == targetVersion {\n\t\treturn errors.New(\"current version match target version, nothing to do\")\n\t}\n\n\tswitch dbVersion.Version {\n\tcase 34:\n\t\treturn downgradeSQLiteDatabaseFromV34(p.dbHandle)\n\tdefault:\n\t\treturn fmt.Errorf(\"database schema version not handled: %d\", dbVersion.Version)\n\t}\n}\n\nfunc (p *SQLiteProvider) resetDatabase() error {\n\tsql := sqlReplaceAll(sqliteResetSQL)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)\n}\n\nfunc (p *SQLiteProvider) normalizeError(err error, fieldType int) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif e, ok := err.(sqlite3.Error); ok {\n\t\tswitch e.ExtendedCode {\n\t\tcase 1555, 2067:\n\t\t\tvar message string\n\t\t\tswitch fieldType {\n\t\t\tcase fieldUsername:\n\t\t\t\tmessage = util.I18nErrorDuplicatedUsername\n\t\t\tcase fieldIPNet:\n\t\t\t\tmessage = util.I18nErrorDuplicatedIPNet\n\t\t\tdefault:\n\t\t\t\tmessage = util.I18nErrorDuplicatedName\n\t\t\t}\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"%w: %s\", ErrDuplicatedKey, err.Error()),\n\t\t\t\tmessage,\n\t\t\t)\n\t\tcase 787:\n\t\t\treturn fmt.Errorf(\"%w: %s\", ErrForeignKeyViolated, err.Error())\n\t\t}\n\t}\n\treturn err\n}\n\nfunc executePragmaOptimize(dbHandle *sql.DB) error {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)\n\tdefer cancel()\n\n\t_, err := dbHandle.ExecContext(ctx, \"PRAGMA optimize;\")\n\treturn err\n}\n\nfunc updateSQLiteDatabaseFromV33(dbHandle *sql.DB) error {\n\treturn updateSQLiteDatabaseFrom33To34(dbHandle)\n}\n\nfunc downgradeSQLiteDatabaseFromV34(dbHandle *sql.DB) error {\n\treturn downgradeSQLiteDatabaseFrom34To33(dbHandle)\n}\n\nfunc updateSQLiteDatabaseFrom33To34(dbHandle *sql.DB) error {\n\tlogger.InfoToConsole(\"updating database schema version: 33 -> 34\")\n\tproviderLog(logger.LevelInfo, \"updating database schema version: 33 -> 34\")\n\n\tsql := strings.ReplaceAll(sqliteV34SQL, \"{{prefix}}\", config.SQLTablesPrefix)\n\tsql = strings.ReplaceAll(sql, \"{{shares}}\", sqlTableShares)\n\tsql = strings.ReplaceAll(sql, \"{{shares_groups_mapping}}\", sqlTableSharesGroupsMapping)\n\tsql = strings.ReplaceAll(sql, \"{{groups}}\", sqlTableGroups)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 34, true)\n}\n\nfunc downgradeSQLiteDatabaseFrom34To33(dbHandle *sql.DB) error {\n\tlogger.InfoToConsole(\"downgrading database schema version: 34 -> 33\")\n\tproviderLog(logger.LevelInfo, \"downgrading database schema version: 34 -> 33\")\n\n\tsql := strings.ReplaceAll(sqliteV34DownSQL, \"{{shares_groups_mapping}}\", sqlTableSharesGroupsMapping)\n\treturn sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 33, false)\n}\n\n/*func setPragmaFK(dbHandle *sql.DB, value string) error {\n\tctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)\n\tdefer cancel()\n\n\tsql := fmt.Sprintf(\"PRAGMA foreign_keys=%v;\", value)\n\n\t_, err := dbHandle.ExecContext(ctx, sql)\n\treturn err\n}*/\n"
  },
  {
    "path": "internal/dataprovider/sqlite_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build nosqlite || !cgo\n\npackage dataprovider\n\nimport (\n\t\"errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-sqlite\")\n}\n\nfunc initializeSQLiteProvider(_ string) error {\n\treturn errors.New(\"SQLite disabled at build time\")\n}\n"
  },
  {
    "path": "internal/dataprovider/sqlqueries.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tselectUserFields = \"u.id,u.username,u.password,u.public_keys,u.home_dir,u.uid,u.gid,u.max_sessions,u.quota_size,u.quota_files,\" +\n\t\t\"u.permissions,u.used_quota_size,u.used_quota_files,u.last_quota_update,u.upload_bandwidth,u.download_bandwidth,\" +\n\t\t\"u.expiration_date,u.last_login,u.status,u.filters,u.filesystem,u.additional_info,u.description,u.email,u.created_at,\" +\n\t\t\"u.updated_at,u.upload_data_transfer,u.download_data_transfer,u.total_data_transfer,\" +\n\t\t\"u.used_upload_data_transfer,u.used_download_data_transfer,u.deleted_at,u.first_download,u.first_upload,r.name,u.last_password_change\"\n\tselectFolderFields = \"id,path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem\"\n\tselectAdminFields  = \"a.id,a.username,a.password,a.status,a.email,a.permissions,a.filters,a.additional_info,a.description,a.created_at,a.updated_at,a.last_login,r.name\"\n\tselectAPIKeyFields = \"key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id\"\n\tselectShareFields  = \"s.share_id,s.name,s.description,s.scope,s.paths,u.username,s.created_at,s.updated_at,s.last_use_at,\" +\n\t\t\"s.expires_at,s.password,s.max_tokens,s.used_tokens,s.allow_from\"\n\tselectGroupFields       = \"id,name,description,created_at,updated_at,user_settings\"\n\tselectEventActionFields = \"id,name,description,type,options\"\n\tselectRoleFields        = \"id,name,description,created_at,updated_at\"\n\tselectIPListEntryFields = \"type,ipornet,mode,protocols,description,created_at,updated_at,deleted_at\"\n\tselectMinimalFields     = \"id,name\"\n)\n\nfunc getSQLPlaceholders() []string {\n\tvar placeholders []string\n\tfor i := 1; i <= 100; i++ {\n\t\tif config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {\n\t\t\tplaceholders = append(placeholders, fmt.Sprintf(\"$%d\", i))\n\t\t} else {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t}\n\t}\n\treturn placeholders\n}\n\nfunc getSQLQuotedName(name string) string {\n\tif config.Driver == MySQLDataProviderName {\n\t\treturn fmt.Sprintf(\"`%s`\", name)\n\t}\n\n\treturn fmt.Sprintf(`\"%s\"`, name)\n}\n\nfunc getSelectEventRuleFields() string {\n\tif config.Driver == MySQLDataProviderName {\n\t\treturn \"id,name,description,created_at,updated_at,`trigger`,conditions,deleted_at,status\"\n\t}\n\n\treturn `id,name,description,created_at,updated_at,\"trigger\",conditions,deleted_at,status`\n}\n\nfunc getCoalesceDefaultForRole(role string) string {\n\tif role != \"\" {\n\t\treturn \"0\"\n\t}\n\treturn \"NULL\"\n}\n\nfunc getAddSessionQuery() string {\n\tif config.Driver == MySQLDataProviderName {\n\t\treturn fmt.Sprintf(\"INSERT INTO %s (`key`,`data`,`type`,`timestamp`) VALUES (%s,%s,%s,%s) \"+\n\t\t\t\"ON DUPLICATE KEY UPDATE `data`=VALUES(`data`), `timestamp`=VALUES(`timestamp`)\",\n\t\t\tsqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n\t}\n\treturn fmt.Sprintf(`INSERT INTO %s (key,data,type,timestamp) VALUES (%s,%s,%s,%s) ON CONFLICT(key,type) DO UPDATE SET data=\n\t\tEXCLUDED.data, timestamp=EXCLUDED.timestamp`,\n\t\tsqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getDeleteSessionQuery() string {\n\tif config.Driver == MySQLDataProviderName {\n\t\treturn fmt.Sprintf(\"DELETE FROM %s WHERE `key` = %s AND `type` = %s\",\n\t\t\tsqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])\n\t}\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE key = %s AND type = %s`,\n\t\tsqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getSessionQuery() string {\n\tif config.Driver == MySQLDataProviderName {\n\t\treturn fmt.Sprintf(\"SELECT `key`,`data`,`type`,`timestamp` FROM %s WHERE `key` = %s AND `type` = %s\",\n\t\t\tsqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])\n\t}\n\treturn fmt.Sprintf(`SELECT key,data,type,timestamp FROM %s WHERE key = %s AND type = %s`,\n\t\tsqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getCleanupSessionsQuery() string {\n\treturn fmt.Sprintf(`DELETE from %s WHERE type = %s AND timestamp < %s`,\n\t\tsqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getAddDefenderHostQuery() string {\n\tif config.Driver == MySQLDataProviderName {\n\t\treturn fmt.Sprintf(\"INSERT INTO %s (`ip`,`updated_at`,`ban_time`) VALUES (%s,%s,0) ON DUPLICATE KEY UPDATE `updated_at`=VALUES(`updated_at`)\",\n\t\t\tsqlTableDefenderHosts, sqlPlaceholders[0], sqlPlaceholders[1])\n\t}\n\treturn fmt.Sprintf(`INSERT INTO %s (ip,updated_at,ban_time) VALUES (%s,%s,0) ON CONFLICT (ip) DO UPDATE SET updated_at = EXCLUDED.updated_at RETURNING id`,\n\t\tsqlTableDefenderHosts, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getAddDefenderEventQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (date_time,score,host_id) VALUES (%s,%s,(SELECT id from %s WHERE ip = %s))`,\n\t\tsqlTableDefenderEvents, sqlPlaceholders[0], sqlPlaceholders[1], sqlTableDefenderHosts, sqlPlaceholders[2])\n}\n\nfunc getDefenderHostsQuery() string {\n\treturn fmt.Sprintf(`SELECT id,ip,ban_time FROM %s WHERE updated_at >= %s OR ban_time > 0 ORDER BY updated_at DESC LIMIT %s`,\n\t\tsqlTableDefenderHosts, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDefenderHostQuery() string {\n\treturn fmt.Sprintf(`SELECT id,ip,ban_time FROM %s WHERE ip = %s AND (updated_at >= %s OR ban_time > 0)`,\n\t\tsqlTableDefenderHosts, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDefenderEventsQuery(hostIDs []int64) string {\n\tvar sb strings.Builder\n\tfor _, hID := range hostIDs {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(hID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t} else {\n\t\tsb.WriteString(\"(0)\")\n\t}\n\treturn fmt.Sprintf(`SELECT host_id,SUM(score) FROM %s WHERE date_time >= %s AND host_id IN %s GROUP BY host_id`,\n\t\tsqlTableDefenderEvents, sqlPlaceholders[0], sb.String())\n}\n\nfunc getDefenderIsHostBannedQuery() string {\n\treturn fmt.Sprintf(`SELECT id FROM %s WHERE ip = %s AND ban_time >= %s`,\n\t\tsqlTableDefenderHosts, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDefenderIncrementBanTimeQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET ban_time = ban_time + %s WHERE ip = %s`,\n\t\tsqlTableDefenderHosts, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDefenderSetBanTimeQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET ban_time = %s WHERE ip = %s`,\n\t\tsqlTableDefenderHosts, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDeleteDefenderHostQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE ip = %s`, sqlTableDefenderHosts, sqlPlaceholders[0])\n}\n\nfunc getDefenderHostsCleanupQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE ban_time < %s AND NOT EXISTS (\n\t\tSELECT id FROM %s WHERE %s.host_id = %s.id AND %s.date_time > %s)`,\n\t\tsqlTableDefenderHosts, sqlPlaceholders[0], sqlTableDefenderEvents, sqlTableDefenderEvents, sqlTableDefenderHosts,\n\t\tsqlTableDefenderEvents, sqlPlaceholders[1])\n}\n\nfunc getDefenderEventsCleanupQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE date_time < %s`, sqlTableDefenderEvents, sqlPlaceholders[0])\n}\n\nfunc getIPListEntryQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE type = %s AND ipornet = %s AND deleted_at = 0`,\n\t\tselectIPListEntryFields, sqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getIPListEntriesQuery(filter, from, order string, limit int) string {\n\tvar sb strings.Builder\n\tvar idx int\n\n\tsb.WriteString(\"SELECT \")\n\tsb.WriteString(selectIPListEntryFields)\n\tsb.WriteString(\" FROM \")\n\tsb.WriteString(sqlTableIPLists)\n\tsb.WriteString(\" WHERE type = \")\n\tsb.WriteString(sqlPlaceholders[idx])\n\tidx++\n\tif from != \"\" {\n\t\tif order == OrderASC {\n\t\t\tsb.WriteString(\" AND ipornet > \")\n\t\t} else {\n\t\t\tsb.WriteString(\" AND ipornet < \")\n\t\t}\n\t\tsb.WriteString(sqlPlaceholders[idx])\n\t\tidx++\n\t}\n\tif filter != \"\" {\n\t\tsb.WriteString(\" AND ipornet LIKE \")\n\t\tsb.WriteString(sqlPlaceholders[idx])\n\t\tidx++\n\t}\n\tsb.WriteString(\" AND deleted_at = 0 \")\n\tsb.WriteString(\" ORDER BY ipornet \")\n\tsb.WriteString(order)\n\tif limit > 0 {\n\t\tsb.WriteString(\" LIMIT \")\n\t\tsb.WriteString(sqlPlaceholders[idx])\n\t}\n\treturn sb.String()\n}\n\nfunc getCountIPListEntriesQuery() string {\n\treturn fmt.Sprintf(`SELECT count(ipornet) FROM %s WHERE type = %s AND deleted_at = 0`, sqlTableIPLists, sqlPlaceholders[0])\n}\n\nfunc getCountAllIPListEntriesQuery() string {\n\treturn fmt.Sprintf(`SELECT count(ipornet) FROM %s WHERE deleted_at = 0`, sqlTableIPLists)\n}\n\nfunc getIPListEntriesForIPQueryPg() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE type = %s AND deleted_at = 0 AND %s::inet BETWEEN first AND last`,\n\t\tselectIPListEntryFields, sqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getIPListEntriesForIPQueryNoPg() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE type = %s AND deleted_at = 0 AND ip_type = %s AND %s BETWEEN first AND last`,\n\t\tselectIPListEntryFields, sqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])\n}\n\nfunc getRecentlyUpdatedIPListQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE updated_at >= %s OR deleted_at > 0`,\n\t\tselectIPListEntryFields, sqlTableIPLists, sqlPlaceholders[0])\n}\n\nfunc getDumpListEntriesQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0`, selectIPListEntryFields, sqlTableIPLists)\n}\n\nfunc getAddIPListEntryQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (type,ipornet,first,last,ip_type,protocols,description,mode,created_at,updated_at,deleted_at)\n\t\tVALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0)`, sqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1],\n\t\tsqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5],\n\t\tsqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9])\n}\n\nfunc getUpdateIPListEntryQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET mode=%s,protocols=%s,description=%s,updated_at=%s WHERE type = %s AND ipornet = %s`,\n\t\tsqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3],\n\t\tsqlPlaceholders[4], sqlPlaceholders[5])\n}\n\nfunc getDeleteIPListEntryQuery(softDelete bool) string {\n\tif softDelete {\n\t\treturn fmt.Sprintf(`UPDATE %s SET updated_at=%s,deleted_at=%s WHERE type = %s AND ipornet = %s`,\n\t\t\tsqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n\t}\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE type = %s AND ipornet = %s`,\n\t\tsqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getRemoveSoftDeletedIPListEntryQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE type = %s AND ipornet = %s AND deleted_at > 0`,\n\t\tsqlTableIPLists, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getConfigsQuery() string {\n\treturn fmt.Sprintf(`SELECT configs FROM %s LIMIT 1`, sqlTableConfigs)\n}\n\nfunc getUpdateConfigsQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET configs = %s`, sqlTableConfigs, sqlPlaceholders[0])\n}\n\nfunc getRoleByNameQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectRoleFields, sqlTableRoles,\n\t\tsqlPlaceholders[0])\n}\n\nfunc getRolesQuery(order string, minimal bool) string {\n\tvar fieldSelection string\n\tif minimal {\n\t\tfieldSelection = selectMinimalFields\n\t} else {\n\t\tfieldSelection = selectRoleFields\n\t}\n\treturn fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection,\n\t\tsqlTableRoles, order, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getUsersWithRolesQuery(roles []Role) string {\n\tvar sb strings.Builder\n\tfor _, r := range roles {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(r.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT r.id, u.username FROM %s u INNER JOIN %s r ON u.role_id = r.id WHERE u.role_id IN %s`,\n\t\tsqlTableUsers, sqlTableRoles, sb.String())\n}\n\nfunc getAdminsWithRolesQuery(roles []Role) string {\n\tvar sb strings.Builder\n\tfor _, r := range roles {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(r.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT r.id, a.username FROM %s a INNER JOIN %s r ON a.role_id = r.id WHERE a.role_id IN %s`,\n\t\tsqlTableAdmins, sqlTableRoles, sb.String())\n}\n\nfunc getDumpRolesQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s`, selectRoleFields, sqlTableRoles)\n}\n\nfunc getAddRoleQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (name,description,created_at,updated_at)\n\t\tVALUES (%s,%s,%s,%s)`, sqlTableRoles, sqlPlaceholders[0], sqlPlaceholders[1],\n\t\tsqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getUpdateRoleQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET description=%s,updated_at=%s\n\t\tWHERE name = %s`, sqlTableRoles, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])\n}\n\nfunc getDeleteRoleQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableRoles, sqlPlaceholders[0])\n}\n\nfunc getGroupByNameQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectGroupFields, getSQLQuotedName(sqlTableGroups),\n\t\tsqlPlaceholders[0])\n}\n\nfunc getGroupsQuery(order string, minimal bool) string {\n\tvar fieldSelection string\n\tif minimal {\n\t\tfieldSelection = selectMinimalFields\n\t} else {\n\t\tfieldSelection = selectGroupFields\n\t}\n\treturn fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection,\n\t\tgetSQLQuotedName(sqlTableGroups), order, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getGroupsWithNamesQuery(numArgs int) string {\n\tvar sb strings.Builder\n\tfor idx := 0; idx < numArgs; idx++ {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(sqlPlaceholders[idx])\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t} else {\n\t\tsb.WriteString(\"('')\")\n\t}\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE name in %s`, selectGroupFields, getSQLQuotedName(sqlTableGroups), sb.String())\n}\n\nfunc getUsersInGroupsQuery(numArgs int) string {\n\tvar sb strings.Builder\n\tfor idx := 0; idx < numArgs; idx++ {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(sqlPlaceholders[idx])\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t} else {\n\t\tsb.WriteString(\"('')\")\n\t}\n\treturn fmt.Sprintf(`SELECT username FROM %s WHERE id IN (SELECT user_id from %s WHERE group_id IN (SELECT id FROM %s WHERE name IN %s))`,\n\t\tsqlTableUsers, sqlTableUsersGroupsMapping, getSQLQuotedName(sqlTableGroups), sb.String())\n}\n\nfunc getDumpGroupsQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s`, selectGroupFields, getSQLQuotedName(sqlTableGroups))\n}\n\nfunc getAddGroupQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (name,description,created_at,updated_at,user_settings)\n\t\tVALUES (%s,%s,%s,%s,%s)`, getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0], sqlPlaceholders[1],\n\t\tsqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4])\n}\n\nfunc getUpdateGroupQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET description=%s,user_settings=%s,updated_at=%s\n\t\tWHERE name = %s`, getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],\n\t\tsqlPlaceholders[3])\n}\n\nfunc getDeleteGroupQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0])\n}\n\nfunc getAdminByUsernameQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s a LEFT JOIN %s r on r.id = a.role_id WHERE a.username = %s`,\n\t\tselectAdminFields, sqlTableAdmins, sqlTableRoles, sqlPlaceholders[0])\n}\n\nfunc getAdminsQuery(order string) string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s a LEFT JOIN %s r on r.id = a.role_id ORDER BY a.username %s LIMIT %s OFFSET %s`,\n\t\tselectAdminFields, sqlTableAdmins, sqlTableRoles, order, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDumpAdminsQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s a LEFT JOIN %s r on r.id = a.role_id`,\n\t\tselectAdminFields, sqlTableAdmins, sqlTableRoles)\n}\n\nfunc getAddAdminQuery(role string) string {\n\treturn fmt.Sprintf(`INSERT INTO %s (username,password,status,email,permissions,filters,additional_info,description,created_at,updated_at,last_login,role_id)\n\t\tVALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,COALESCE((SELECT id from %s WHERE name = %s),%s))`,\n\t\tsqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],\n\t\tsqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],\n\t\tsqlTableRoles, sqlPlaceholders[10], getCoalesceDefaultForRole(role))\n}\n\nfunc getUpdateAdminQuery(role string) string {\n\treturn fmt.Sprintf(`UPDATE %s SET password=%s,status=%s,email=%s,permissions=%s,filters=%s,additional_info=%s,description=%s,updated_at=%s,\n\t\trole_id=COALESCE((SELECT id from %s WHERE name = %s),%s) WHERE username = %s`, sqlTableAdmins, sqlPlaceholders[0],\n\t\tsqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6],\n\t\tsqlPlaceholders[7], sqlTableRoles, sqlPlaceholders[8], getCoalesceDefaultForRole(role), sqlPlaceholders[9])\n}\n\nfunc getDeleteAdminQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE username = %s`, sqlTableAdmins, sqlPlaceholders[0])\n}\n\nfunc getShareByIDQuery(filterUser bool) string {\n\tif filterUser {\n\t\treturn fmt.Sprintf(`SELECT %s FROM %s s INNER JOIN %s u ON s.user_id = u.id WHERE s.share_id = %s AND u.username = %s`,\n\t\t\tselectShareFields, sqlTableShares, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])\n\t}\n\treturn fmt.Sprintf(`SELECT %s FROM %s s INNER JOIN %s u ON s.user_id = u.id WHERE s.share_id = %s`,\n\t\tselectShareFields, sqlTableShares, sqlTableUsers, sqlPlaceholders[0])\n}\n\nfunc getSharesQuery(order string) string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s s INNER JOIN %s u ON s.user_id = u.id WHERE u.username = %s ORDER BY s.share_id %s LIMIT %s OFFSET %s`,\n\t\tselectShareFields, sqlTableShares, sqlTableUsers, sqlPlaceholders[0], order, sqlPlaceholders[1], sqlPlaceholders[2])\n}\n\nfunc getDumpSharesQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s s INNER JOIN %s u ON s.user_id = u.id`,\n\t\tselectShareFields, sqlTableShares, sqlTableUsers)\n}\n\nfunc getAddShareQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (share_id,name,description,scope,paths,created_at,updated_at,last_use_at,\n\t\texpires_at,password,max_tokens,used_tokens,allow_from,user_id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)`,\n\t\tsqlTableShares, sqlPlaceholders[0], sqlPlaceholders[1],\n\t\tsqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6],\n\t\tsqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10], sqlPlaceholders[11],\n\t\tsqlPlaceholders[12], sqlPlaceholders[13])\n}\n\nfunc getUpdateShareRestoreQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET name=%s,description=%s,scope=%s,paths=%s,created_at=%s,updated_at=%s,\n\t\tlast_use_at=%s,expires_at=%s,password=%s,max_tokens=%s,used_tokens=%s,allow_from=%s,user_id=%s WHERE share_id = %s`, sqlTableShares,\n\t\tsqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],\n\t\tsqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],\n\t\tsqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13])\n}\n\nfunc getUpdateShareQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET name=%s,description=%s,scope=%s,paths=%s,updated_at=%s,expires_at=%s,\n\t\tpassword=%s,max_tokens=%s,allow_from=%s,user_id=%s WHERE share_id = %s`, sqlTableShares,\n\t\tsqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],\n\t\tsqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],\n\t\tsqlPlaceholders[10])\n}\n\nfunc getDeleteShareQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE share_id = %s`, sqlTableShares, sqlPlaceholders[0])\n}\n\nfunc getAPIKeyByIDQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE key_id = %s`, selectAPIKeyFields, sqlTableAPIKeys, sqlPlaceholders[0])\n}\n\nfunc getAPIKeysQuery(order string) string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s ORDER BY key_id %s LIMIT %s OFFSET %s`, selectAPIKeyFields, sqlTableAPIKeys,\n\t\torder, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDumpAPIKeysQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s`, selectAPIKeyFields, sqlTableAPIKeys)\n}\n\nfunc getAddAPIKeyQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id)\n\t\tVALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)`, sqlTableAPIKeys, sqlPlaceholders[0], sqlPlaceholders[1],\n\t\tsqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6],\n\t\tsqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10])\n}\n\nfunc getUpdateAPIKeyQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET name=%s,scope=%s,expires_at=%s,user_id=%s,admin_id=%s,description=%s,updated_at=%s\n\t\tWHERE key_id = %s`, sqlTableAPIKeys, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],\n\t\tsqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7])\n}\n\nfunc getDeleteAPIKeyQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE key_id = %s`, sqlTableAPIKeys, sqlPlaceholders[0])\n}\n\nfunc getRelatedUsersForAPIKeysQuery(apiKeys []APIKey) string {\n\tvar sb strings.Builder\n\tfor _, k := range apiKeys {\n\t\tif k.userID == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(k.userID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t} else {\n\t\tsb.WriteString(\"(0)\")\n\t}\n\treturn fmt.Sprintf(`SELECT id,username FROM %s WHERE id IN %s ORDER BY username`, sqlTableUsers, sb.String())\n}\n\nfunc getRelatedAdminsForAPIKeysQuery(apiKeys []APIKey) string {\n\tvar sb strings.Builder\n\tfor _, k := range apiKeys {\n\t\tif k.adminID == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(k.adminID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t} else {\n\t\tsb.WriteString(\"(0)\")\n\t}\n\treturn fmt.Sprintf(`SELECT id,username FROM %s WHERE id IN %s ORDER BY username`, sqlTableAdmins, sb.String())\n}\n\nfunc getUserByUsernameQuery(role string) string {\n\tif role == \"\" {\n\t\treturn fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE u.username = %s AND u.deleted_at = 0`,\n\t\t\tselectUserFields, sqlTableUsers, sqlTableRoles, sqlPlaceholders[0])\n\t}\n\treturn fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE u.username = %s AND u.deleted_at = 0\n\t\tAND u.role_id is NOT NULL AND r.name = %s`,\n\t\tselectUserFields, sqlTableUsers, sqlTableRoles, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getUsersQuery(order, role string) string {\n\tif role == \"\" {\n\t\treturn fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE\n\t\t\tu.deleted_at = 0 ORDER BY u.username %s LIMIT %s OFFSET %s`,\n\t\t\tselectUserFields, sqlTableUsers, sqlTableRoles, order, sqlPlaceholders[0], sqlPlaceholders[1])\n\t}\n\treturn fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE\n\t\tu.deleted_at = 0 AND u.role_id is NOT NULL AND r.name = %s ORDER BY u.username %s LIMIT %s OFFSET %s`,\n\t\tselectUserFields, sqlTableUsers, sqlTableRoles, sqlPlaceholders[0], order, sqlPlaceholders[1], sqlPlaceholders[2])\n}\n\nfunc getUsersForQuotaCheckQuery(numArgs int) string {\n\tvar sb strings.Builder\n\tfor idx := 0; idx < numArgs; idx++ {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(sqlPlaceholders[idx])\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT id,username,quota_size,used_quota_size,total_data_transfer,upload_data_transfer,\n\t\tdownload_data_transfer,used_upload_data_transfer,used_download_data_transfer,filters FROM %s WHERE username IN %s`,\n\t\tsqlTableUsers, sb.String())\n}\n\nfunc getRecentlyUpdatedUsersQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE u.updated_at >= %s OR u.deleted_at > 0`,\n\t\tselectUserFields, sqlTableUsers, sqlTableRoles, sqlPlaceholders[0])\n}\n\nfunc getDumpUsersQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE u.deleted_at = 0`,\n\t\tselectUserFields, sqlTableUsers, sqlTableRoles)\n}\n\nfunc getDumpFoldersQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s`, selectFolderFields, sqlTableFolders)\n}\n\nfunc getUpdateTransferQuotaQuery(reset bool) string {\n\tif reset {\n\t\treturn fmt.Sprintf(`UPDATE %s SET used_upload_data_transfer = %s,used_download_data_transfer = %s,last_quota_update = %s\n\t\t\tWHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n\t}\n\treturn fmt.Sprintf(`UPDATE %s SET used_upload_data_transfer = used_upload_data_transfer + %s,\n\t\tused_download_data_transfer = used_download_data_transfer + %s,last_quota_update = %s\n\t\tWHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getUpdateQuotaQuery(reset bool) string {\n\tif reset {\n\t\treturn fmt.Sprintf(`UPDATE %s SET used_quota_size = %s,used_quota_files = %s,last_quota_update = %s\n\t\t\tWHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n\t}\n\treturn fmt.Sprintf(`UPDATE %s SET used_quota_size = used_quota_size + %s,used_quota_files = used_quota_files + %s,last_quota_update = %s\n\t\tWHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getAdminSignatureQuery() string {\n\treturn fmt.Sprintf(`SELECT updated_at FROM %s WHERE username = %s`, sqlTableAdmins, sqlPlaceholders[0])\n}\n\nfunc getUserSignatureQuery() string {\n\treturn fmt.Sprintf(`SELECT updated_at FROM %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0])\n}\n\nfunc getSetUpdateAtQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET updated_at = %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getSetFirstUploadQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET first_upload = %s WHERE username = %s AND first_upload = 0`,\n\t\tsqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getSetFirstDownloadQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET first_download = %s WHERE username = %s AND first_download = 0`,\n\t\tsqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getUpdateLastLoginQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET last_login = %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getUpdateAdminLastLoginQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET last_login = %s WHERE username = %s`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getUpdateAPIKeyLastUseQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET last_use_at = %s WHERE key_id = %s`, sqlTableAPIKeys, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getUpdateShareLastUseQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET last_use_at = %s, used_tokens = used_tokens +%s WHERE share_id = %s`,\n\t\tsqlTableShares, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])\n}\n\nfunc getQuotaQuery() string {\n\treturn fmt.Sprintf(`SELECT used_quota_size,used_quota_files,used_upload_data_transfer,\n\t\tused_download_data_transfer FROM %s WHERE username = %s`,\n\t\tsqlTableUsers, sqlPlaceholders[0])\n}\n\nfunc getAddUserQuery(role string) string {\n\treturn fmt.Sprintf(`INSERT INTO %s (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,\n\t\tused_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,status,last_login,expiration_date,filters,\n\t\tfilesystem,additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer,\n\t\tused_upload_data_transfer,used_download_data_transfer,deleted_at,first_download,first_upload,role_id,last_password_change)\n\t\tVALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,%s,%s,%s,0,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,0,0,\n\t\tCOALESCE((SELECT id from %s WHERE name=%s),%s),%s)`,\n\t\tsqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],\n\t\tsqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],\n\t\tsqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],\n\t\tsqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17], sqlPlaceholders[18], sqlPlaceholders[19],\n\t\tsqlPlaceholders[20], sqlPlaceholders[21], sqlPlaceholders[22], sqlPlaceholders[23], sqlTableRoles,\n\t\tsqlPlaceholders[24], getCoalesceDefaultForRole(role), sqlPlaceholders[25])\n}\n\nfunc getUpdateUserQuery(role string) string {\n\treturn fmt.Sprintf(`UPDATE %s SET password=%s,public_keys=%s,home_dir=%s,uid=%s,gid=%s,max_sessions=%s,quota_size=%s,\n\t\tquota_files=%s,permissions=%s,upload_bandwidth=%s,download_bandwidth=%s,status=%s,expiration_date=%s,filters=%s,filesystem=%s,\n\t\tadditional_info=%s,description=%s,email=%s,updated_at=%s,upload_data_transfer=%s,download_data_transfer=%s,\n\t\ttotal_data_transfer=%s,role_id=COALESCE((SELECT id from %s WHERE name=%s),%s),last_password_change=%s WHERE username = %s`,\n\t\tsqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],\n\t\tsqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],\n\t\tsqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],\n\t\tsqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17], sqlPlaceholders[18], sqlPlaceholders[19],\n\t\tsqlPlaceholders[20], sqlPlaceholders[21], sqlTableRoles, sqlPlaceholders[22], getCoalesceDefaultForRole(role),\n\t\tsqlPlaceholders[23], sqlPlaceholders[24])\n}\n\nfunc getUpdateUserPasswordQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET password=%s,updated_at=%s WHERE username = %s`,\n\t\tsqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])\n}\n\nfunc getDeleteUserQuery(softDelete bool) string {\n\tif softDelete {\n\t\treturn fmt.Sprintf(`UPDATE %s SET updated_at=%s,deleted_at=%s WHERE username = %s`,\n\t\t\tsqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])\n\t}\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0])\n}\n\nfunc getRemoveSoftDeletedUserQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE username = %s AND deleted_at > 0`, sqlTableUsers, sqlPlaceholders[0])\n}\n\nfunc getFolderByNameQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectFolderFields, sqlTableFolders, sqlPlaceholders[0])\n}\n\nfunc getAddFolderQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem)\n\t\tVALUES (%s,%s,%s,%s,%s,%s,%s)`, sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],\n\t\tsqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])\n}\n\nfunc getUpdateFolderQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET path=%s,description=%s,filesystem=%s WHERE name = %s`, sqlTableFolders, sqlPlaceholders[0],\n\t\tsqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getDeleteFolderQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableFolders, sqlPlaceholders[0])\n}\n\nfunc getClearUserGroupMappingQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE user_id = (SELECT id FROM %s WHERE username = %s)`, sqlTableUsersGroupsMapping,\n\t\tsqlTableUsers, sqlPlaceholders[0])\n}\n\nfunc getAddUserGroupMappingQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (user_id,group_id,group_type,sort_order) VALUES ((SELECT id FROM %s WHERE username = %s),\n\t\t(SELECT id FROM %s WHERE name = %s),%s,%s)`,\n\t\tsqlTableUsersGroupsMapping, sqlTableUsers, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),\n\t\tsqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getClearAdminGroupMappingQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE admin_id = (SELECT id FROM %s WHERE username = %s)`, sqlTableAdminsGroupsMapping,\n\t\tsqlTableAdmins, sqlPlaceholders[0])\n}\n\nfunc getAddAdminGroupMappingQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (admin_id,group_id,options,sort_order) VALUES ((SELECT id FROM %s WHERE username = %s),\n\t\t(SELECT id FROM %s WHERE name = %s),%s,%s)`,\n\t\tsqlTableAdminsGroupsMapping, sqlTableAdmins, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),\n\t\tsqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getClearGroupFolderMappingQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE group_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableGroupsFoldersMapping,\n\t\tgetSQLQuotedName(sqlTableGroups), sqlPlaceholders[0])\n}\n\nfunc getAddGroupFolderMappingQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,group_id,sort_order)\n\t\tVALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE name = %s),%s)`,\n\t\tsqlTableGroupsFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,\n\t\tsqlPlaceholders[3], getSQLQuotedName(sqlTableGroups), sqlPlaceholders[4], sqlPlaceholders[5])\n}\n\nfunc getClearUserFolderMappingQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE user_id = (SELECT id FROM %s WHERE username = %s)`, sqlTableUsersFoldersMapping,\n\t\tsqlTableUsers, sqlPlaceholders[0])\n}\n\nfunc getAddUserFolderMappingQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,user_id,sort_order)\n\t\tVALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE username = %s),%s)`,\n\t\tsqlTableUsersFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,\n\t\tsqlPlaceholders[3], sqlTableUsers, sqlPlaceholders[4], sqlPlaceholders[5])\n}\n\nfunc getFoldersQuery(order string, minimal bool) string {\n\tvar fieldSelection string\n\tif minimal {\n\t\tfieldSelection = selectMinimalFields\n\t} else {\n\t\tfieldSelection = selectFolderFields\n\t}\n\treturn fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection, sqlTableFolders,\n\t\torder, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getUpdateFolderQuotaQuery(reset bool) string {\n\tif reset {\n\t\treturn fmt.Sprintf(`UPDATE %s SET used_quota_size = %s,used_quota_files = %s,last_quota_update = %s\n\t\t\tWHERE name = %s`, sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n\t}\n\treturn fmt.Sprintf(`UPDATE %s SET used_quota_size = used_quota_size + %s,used_quota_files = used_quota_files + %s,last_quota_update = %s\n\t\tWHERE name = %s`, sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getQuotaFolderQuery() string {\n\treturn fmt.Sprintf(`SELECT used_quota_size,used_quota_files FROM %s WHERE name = %s`, sqlTableFolders,\n\t\tsqlPlaceholders[0])\n}\n\nfunc getRelatedGroupsForUsersQuery(users []User) string {\n\tvar sb strings.Builder\n\tfor _, u := range users {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(u.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT g.name,ug.group_type,ug.user_id FROM %s g INNER JOIN %s ug ON g.id = ug.group_id WHERE\n\t\tug.user_id IN %s ORDER BY ug.sort_order`, getSQLQuotedName(sqlTableGroups), sqlTableUsersGroupsMapping, sb.String())\n}\n\nfunc getRelatedGroupsForAdminsQuery(admins []Admin) string {\n\tvar sb strings.Builder\n\tfor _, a := range admins {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(a.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT g.name,ag.options,ag.admin_id FROM %s g INNER JOIN %s ag ON g.id = ag.group_id WHERE\n\t\tag.admin_id IN %s ORDER BY ag.sort_order`, getSQLQuotedName(sqlTableGroups), sqlTableAdminsGroupsMapping, sb.String())\n}\n\nfunc getRelatedFoldersForUsersQuery(users []User) string {\n\tvar sb strings.Builder\n\tfor _, u := range users {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(u.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT f.id,f.name,f.path,f.used_quota_size,f.used_quota_files,f.last_quota_update,fm.virtual_path,\n\t\tfm.quota_size,fm.quota_files,fm.user_id,f.filesystem,f.description FROM %s f INNER JOIN %s fm ON f.id = fm.folder_id WHERE\n\t\tfm.user_id IN %s ORDER BY fm.sort_order`, sqlTableFolders, sqlTableUsersFoldersMapping, sb.String())\n}\n\nfunc getRelatedUsersForFoldersQuery(folders []vfs.BaseVirtualFolder) string {\n\tvar sb strings.Builder\n\tfor _, f := range folders {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(f.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT fm.folder_id,u.username FROM %s fm INNER JOIN %s u ON fm.user_id = u.id\n\t\tWHERE fm.folder_id IN %s ORDER BY u.username`, sqlTableUsersFoldersMapping, sqlTableUsers, sb.String())\n}\n\nfunc getRelatedGroupsForFoldersQuery(folders []vfs.BaseVirtualFolder) string {\n\tvar sb strings.Builder\n\tfor _, f := range folders {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(f.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT fm.folder_id,g.name FROM %s fm INNER JOIN %s g ON fm.group_id = g.id\n\t\tWHERE fm.folder_id IN %s ORDER BY g.name`, sqlTableGroupsFoldersMapping, getSQLQuotedName(sqlTableGroups),\n\t\tsb.String())\n}\n\nfunc getRelatedUsersForGroupsQuery(groups []Group) string {\n\tvar sb strings.Builder\n\tfor _, g := range groups {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(g.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT um.group_id,u.username FROM %s um INNER JOIN %s u ON um.user_id = u.id\n\t\tWHERE um.group_id IN %s ORDER BY u.username`, sqlTableUsersGroupsMapping, sqlTableUsers, sb.String())\n}\n\nfunc getRelatedAdminsForGroupsQuery(groups []Group) string {\n\tvar sb strings.Builder\n\tfor _, g := range groups {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(g.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT am.group_id,a.username FROM %s am INNER JOIN %s a ON am.admin_id = a.id\n\t\tWHERE am.group_id IN %s ORDER BY a.username`, sqlTableAdminsGroupsMapping, sqlTableAdmins, sb.String())\n}\n\nfunc getRelatedFoldersForGroupsQuery(groups []Group) string {\n\tvar sb strings.Builder\n\tfor _, g := range groups {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(g.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT f.id,f.name,f.path,f.used_quota_size,f.used_quota_files,f.last_quota_update,fm.virtual_path,\n\t\tfm.quota_size,fm.quota_files,fm.group_id,f.filesystem,f.description FROM %s f INNER JOIN %s fm ON f.id = fm.folder_id WHERE\n\t\tfm.group_id IN %s ORDER BY fm.sort_order`, sqlTableFolders, sqlTableGroupsFoldersMapping, sb.String())\n}\n\nfunc getActiveTransfersQuery() string {\n\treturn fmt.Sprintf(`SELECT transfer_id,connection_id,transfer_type,username,folder_name,ip,truncated_size,\n\t\tcurrent_ul_size,current_dl_size,created_at,updated_at FROM %s WHERE updated_at > %s`,\n\t\tsqlTableActiveTransfers, sqlPlaceholders[0])\n}\n\nfunc getAddActiveTransferQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (transfer_id,connection_id,transfer_type,username,folder_name,ip,truncated_size,\n\t\tcurrent_ul_size,current_dl_size,created_at,updated_at) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)`,\n\t\tsqlTableActiveTransfers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3],\n\t\tsqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8],\n\t\tsqlPlaceholders[9], sqlPlaceholders[10])\n}\n\nfunc getUpdateActiveTransferSizesQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET current_ul_size=%s,current_dl_size=%s,updated_at=%s WHERE connection_id = %s AND transfer_id = %s`,\n\t\tsqlTableActiveTransfers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4])\n}\n\nfunc getRemoveActiveTransferQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE connection_id = %s AND transfer_id = %s`,\n\t\tsqlTableActiveTransfers, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getCleanupActiveTransfersQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE updated_at < %s`, sqlTableActiveTransfers, sqlPlaceholders[0])\n}\n\nfunc getRelatedRulesForActionsQuery(actions []BaseEventAction) string {\n\tvar sb strings.Builder\n\tfor _, a := range actions {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(a.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT am.action_id,r.name FROM %s am INNER JOIN %s r ON am.rule_id = r.id\n\t\tWHERE am.action_id IN %s ORDER BY r.name ASC`, sqlTableRulesActionsMapping, sqlTableEventsRules, sb.String())\n}\n\nfunc getEventsActionsQuery(order string, minimal bool) string {\n\tvar fieldSelection string\n\tif minimal {\n\t\tfieldSelection = selectMinimalFields\n\t} else {\n\t\tfieldSelection = selectEventActionFields\n\t}\n\treturn fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection,\n\t\tsqlTableEventsActions, order, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDumpEventActionsQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s`, selectEventActionFields, sqlTableEventsActions)\n}\n\nfunc getEventActionByNameQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectEventActionFields, sqlTableEventsActions,\n\t\tsqlPlaceholders[0])\n}\n\nfunc getAddEventActionQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (name,description,type,options) VALUES (%s,%s,%s,%s)`,\n\t\tsqlTableEventsActions, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getUpdateEventActionQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET description=%s,type=%s,options=%s WHERE name = %s`, sqlTableEventsActions,\n\t\tsqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getDeleteEventActionQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableEventsActions, sqlPlaceholders[0])\n}\n\nfunc getEventRulesQuery(order string) string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0 ORDER BY name %s LIMIT %s OFFSET %s`,\n\t\tgetSelectEventRuleFields(), sqlTableEventsRules, order, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDumpEventRulesQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0`, getSelectEventRuleFields(), sqlTableEventsRules)\n}\n\nfunc getRecentlyUpdatedRulesQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE updated_at >= %s OR deleted_at > 0`, getSelectEventRuleFields(),\n\t\tsqlTableEventsRules, sqlPlaceholders[0])\n}\n\nfunc getEventRulesByNameQuery() string {\n\treturn fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s AND deleted_at = 0`, getSelectEventRuleFields(), sqlTableEventsRules,\n\t\tsqlPlaceholders[0])\n}\n\nfunc getAddEventRuleQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (name,description,created_at,updated_at,%s,conditions,deleted_at,status)\n\t\tVALUES (%s,%s,%s,%s,%s,%s,0,%s)`,\n\t\tsqlTableEventsRules, getSQLQuotedName(\"trigger\"), sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],\n\t\tsqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])\n}\n\nfunc getUpdateEventRuleQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET description=%s,updated_at=%s,%s=%s,conditions=%s,status=%s WHERE name = %s`,\n\t\tsqlTableEventsRules, sqlPlaceholders[0], sqlPlaceholders[1], getSQLQuotedName(\"trigger\"), sqlPlaceholders[2],\n\t\tsqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5])\n}\n\nfunc getDeleteEventRuleQuery(softDelete bool) string {\n\tif softDelete {\n\t\treturn fmt.Sprintf(`UPDATE %s SET updated_at=%s,deleted_at=%s WHERE name = %s`,\n\t\t\tsqlTableEventsRules, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])\n\t}\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableEventsRules, sqlPlaceholders[0])\n}\n\nfunc getRemoveSoftDeletedRuleQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE name = %s AND deleted_at > 0`, sqlTableEventsRules, sqlPlaceholders[0])\n}\n\nfunc getClearRuleActionMappingQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE rule_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableRulesActionsMapping,\n\t\tsqlTableEventsRules, sqlPlaceholders[0])\n}\n\nfunc getUpdateRulesTimestampQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET updated_at=%s WHERE id IN (SELECT rule_id FROM %s WHERE action_id = (SELECT id from %s WHERE name = %s))`,\n\t\tsqlTableEventsRules, sqlPlaceholders[0], sqlTableRulesActionsMapping, sqlTableEventsActions, sqlPlaceholders[1])\n}\n\nfunc getRelatedActionsForRulesQuery(rules []EventRule) string {\n\tvar sb strings.Builder\n\tfor _, r := range rules {\n\t\tif sb.Len() == 0 {\n\t\t\tsb.WriteString(\"(\")\n\t\t} else {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\t\tsb.WriteString(strconv.FormatInt(r.ID, 10))\n\t}\n\tif sb.Len() > 0 {\n\t\tsb.WriteString(\")\")\n\t}\n\treturn fmt.Sprintf(`SELECT a.id,a.name,a.description,a.type,a.options,am.options,am.%s,\n\t\tam.rule_id FROM %s a INNER JOIN %s am ON a.id = am.action_id WHERE am.rule_id IN %s ORDER BY am.%s ASC`,\n\t\tgetSQLQuotedName(\"order\"), sqlTableEventsActions, sqlTableRulesActionsMapping, sb.String(),\n\t\tgetSQLQuotedName(\"order\"))\n}\n\nfunc getAddRuleActionMappingQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (rule_id,action_id,%s,options) VALUES ((SELECT id FROM %s WHERE name = %s),\n\t\t(SELECT id FROM %s WHERE name = %s),%s,%s)`,\n\t\tsqlTableRulesActionsMapping, getSQLQuotedName(\"order\"), sqlTableEventsRules, sqlPlaceholders[0],\n\t\tsqlTableEventsActions, sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getTaskByNameQuery() string {\n\treturn fmt.Sprintf(`SELECT updated_at,version FROM %s WHERE name = %s`, sqlTableTasks, sqlPlaceholders[0])\n}\n\nfunc getAddTaskQuery() string {\n\treturn fmt.Sprintf(`INSERT INTO %s (name,updated_at,version) VALUES (%s,%s,0)`,\n\t\tsqlTableTasks, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getUpdateTaskQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET updated_at=%s,version = version + 1 WHERE name = %s AND version = %s`,\n\t\tsqlTableTasks, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])\n}\n\nfunc getUpdateTaskTimestampQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET updated_at=%s WHERE name = %s`,\n\t\tsqlTableTasks, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getDeleteTaskQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableTasks, sqlPlaceholders[0])\n}\n\nfunc getAddNodeQuery() string {\n\tif config.Driver == MySQLDataProviderName {\n\t\treturn fmt.Sprintf(\"INSERT INTO %s (`name`,`data`,created_at,`updated_at`) VALUES (%s,%s,%s,%s) ON DUPLICATE KEY UPDATE \"+\n\t\t\t\"`data`=VALUES(`data`), `created_at`=VALUES(`created_at`), `updated_at`=VALUES(`updated_at`)\",\n\t\t\tsqlTableNodes, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n\t}\n\treturn fmt.Sprintf(`INSERT INTO %s (name,data,created_at,updated_at) VALUES (%s,%s,%s,%s) ON CONFLICT(name)\n\t\tDO UPDATE SET data=EXCLUDED.data, created_at=EXCLUDED.created_at, updated_at=EXCLUDED.updated_at`,\n\t\tsqlTableNodes, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])\n}\n\nfunc getUpdateNodeTimestampQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET updated_at=%s WHERE name = %s`,\n\t\tsqlTableNodes, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getNodeByNameQuery() string {\n\treturn fmt.Sprintf(`SELECT name,data,created_at,updated_at FROM %s WHERE name = %s AND updated_at > %s`,\n\t\tsqlTableNodes, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getNodesQuery() string {\n\treturn fmt.Sprintf(`SELECT name,data,created_at,updated_at FROM %s WHERE name != %s AND updated_at > %s`,\n\t\tsqlTableNodes, sqlPlaceholders[0], sqlPlaceholders[1])\n}\n\nfunc getCleanupNodesQuery() string {\n\treturn fmt.Sprintf(`DELETE FROM %s WHERE updated_at < %s`, sqlTableNodes, sqlPlaceholders[0])\n}\n\nfunc getDatabaseVersionQuery() string {\n\treturn fmt.Sprintf(\"SELECT version from %s LIMIT 1\", sqlTableSchemaVersion)\n}\n\nfunc getUpdateDBVersionQuery() string {\n\treturn fmt.Sprintf(`UPDATE %s SET version=%s`, sqlTableSchemaVersion, sqlPlaceholders[0])\n}\n"
  },
  {
    "path": "internal/dataprovider/unixcrypt.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build unixcrypt && cgo\n\npackage dataprovider\n\nimport (\n\t\"strings\"\n\n\t\"github.com/amoghe/go-crypt\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"+unixcrypt\")\n}\n\nfunc compareYescryptPassword(hashedPwd, plainPwd string) (bool, error) {\n\tlastIdx := strings.LastIndex(hashedPwd, \"$\")\n\tpwd, err := crypt.Crypt(plainPwd, hashedPwd[:lastIdx+1])\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn pwd == hashedPwd, nil\n}\n"
  },
  {
    "path": "internal/dataprovider/unixcrypt_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !unixcrypt || !cgo\n\npackage dataprovider\n\nimport (\n\t\"errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-unixcrypt\")\n}\n\nfunc compareYescryptPassword(_, _ string) (bool, error) {\n\treturn false, errors.New(\"yescrypt hash format is not supported or disabled\")\n}\n"
  },
  {
    "path": "internal/dataprovider/user.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage dataprovider\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\n// Available permissions for SFTPGo users\nconst (\n\t// All permissions are granted\n\tPermAny = \"*\"\n\t// List items such as files and directories is allowed\n\tPermListItems = \"list\"\n\t// download files is allowed\n\tPermDownload = \"download\"\n\t// upload files is allowed\n\tPermUpload = \"upload\"\n\t// overwrite an existing file, while uploading, is allowed\n\t// upload permission is required to allow file overwrite\n\tPermOverwrite = \"overwrite\"\n\t// delete files or directories is allowed\n\tPermDelete = \"delete\"\n\t// delete files is allowed\n\tPermDeleteFiles = \"delete_files\"\n\t// delete directories is allowed\n\tPermDeleteDirs = \"delete_dirs\"\n\t// rename files or directories is allowed\n\tPermRename = \"rename\"\n\t// rename files is allowed\n\tPermRenameFiles = \"rename_files\"\n\t// rename directories is allowed\n\tPermRenameDirs = \"rename_dirs\"\n\t// create directories is allowed\n\tPermCreateDirs = \"create_dirs\"\n\t// create symbolic links is allowed\n\tPermCreateSymlinks = \"create_symlinks\"\n\t// changing file or directory permissions is allowed\n\tPermChmod = \"chmod\"\n\t// changing file or directory owner and group is allowed\n\tPermChown = \"chown\"\n\t// changing file or directory access and modification time is allowed\n\tPermChtimes = \"chtimes\"\n\t// copying files or directories is allowed\n\tPermCopy = \"copy\"\n)\n\n// Available login methods\nconst (\n\tLoginMethodNoAuthTried            = \"no_auth_tried\"\n\tLoginMethodPassword               = \"password\"\n\tSSHLoginMethodPassword            = \"password-over-SSH\"\n\tSSHLoginMethodPublicKey           = \"publickey\"\n\tSSHLoginMethodKeyboardInteractive = \"keyboard-interactive\"\n\tSSHLoginMethodKeyAndPassword      = \"publickey+password\"\n\tSSHLoginMethodKeyAndKeyboardInt   = \"publickey+keyboard-interactive\"\n\tLoginMethodTLSCertificate         = \"TLSCertificate\"\n\tLoginMethodTLSCertificateAndPwd   = \"TLSCertificate+password\"\n\tLoginMethodIDP                    = \"IDP\"\n)\n\nvar (\n\terrNoMatchingVirtualFolder = errors.New(\"no matching virtual folder found\")\n\tpermsRenameAny             = []string{PermRename, PermRenameDirs, PermRenameFiles}\n\tpermsDeleteAny             = []string{PermDelete, PermDeleteDirs, PermDeleteFiles}\n)\n\n// RecoveryCode defines a 2FA recovery code\ntype RecoveryCode struct {\n\tSecret *kms.Secret `json:\"secret\"`\n\tUsed   bool        `json:\"used,omitempty\"`\n}\n\n// UserTOTPConfig defines the time-based one time password configuration\ntype UserTOTPConfig struct {\n\tEnabled    bool        `json:\"enabled,omitempty\"`\n\tConfigName string      `json:\"config_name,omitempty\"`\n\tSecret     *kms.Secret `json:\"secret,omitempty\"`\n\t// TOTP will be required for the specified protocols.\n\t// SSH protocol (SFTP/SCP/SSH commands) will ask for the TOTP passcode if the client uses keyboard interactive\n\t// authentication.\n\t// FTP have no standard way to support two factor authentication, if you\n\t// enable the support for this protocol you have to add the TOTP passcode after the password.\n\t// For example if your password is \"password\" and your one time passcode is\n\t// \"123456\" you have to use \"password123456\" as password.\n\tProtocols []string `json:\"protocols,omitempty\"`\n}\n\n// UserFilters defines additional restrictions for a user\n// TODO: rename to UserOptions in v3\ntype UserFilters struct {\n\tsdk.BaseUserFilters\n\t// User must change password from WebClient/REST API at next login.\n\tRequirePasswordChange bool `json:\"require_password_change,omitempty\"`\n\t// AdditionalEmails defines additional email addresses\n\tAdditionalEmails []string `json:\"additional_emails,omitempty\"`\n\t// Time-based one time passwords configuration\n\tTOTPConfig UserTOTPConfig `json:\"totp_config,omitempty\"`\n\t// Recovery codes to use if the user loses access to their second factor auth device.\n\t// Each code can only be used once, you should use these codes to login and disable or\n\t// reset 2FA for your account\n\tRecoveryCodes []RecoveryCode `json:\"recovery_codes,omitempty\"`\n}\n\n// User defines a SFTPGo user\ntype User struct {\n\tsdk.BaseUser\n\t// Additional restrictions\n\tFilters UserFilters `json:\"filters\"`\n\t// Mapping between virtual paths and virtual folders\n\tVirtualFolders []vfs.VirtualFolder `json:\"virtual_folders,omitempty\"`\n\t// Filesystem configuration details\n\tFsConfig vfs.Filesystem `json:\"filesystem\"`\n\t// groups associated with this user\n\tGroups []sdk.GroupMapping `json:\"groups,omitempty\"`\n\t// we store the filesystem here using the base path as key.\n\tfsCache map[string]vfs.Fs `json:\"-\"`\n\t// true if group settings are already applied for this user\n\tgroupSettingsApplied bool `json:\"-\"`\n\t// in multi node setups we mark the user as deleted to be able to update the webdav cache\n\tDeletedAt int64 `json:\"-\"`\n}\n\n// GetFilesystem returns the base filesystem for this user\nfunc (u *User) GetFilesystem(connectionID string) (fs vfs.Fs, err error) {\n\treturn u.GetFilesystemForPath(\"/\", connectionID)\n}\n\nfunc (u *User) getRootFs(connectionID string) (fs vfs.Fs, err error) {\n\tswitch u.FsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\treturn vfs.NewS3Fs(connectionID, u.GetHomeDir(), \"\", u.FsConfig.S3Config)\n\tcase sdk.GCSFilesystemProvider:\n\t\treturn vfs.NewGCSFs(connectionID, u.GetHomeDir(), \"\", u.FsConfig.GCSConfig)\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\treturn vfs.NewAzBlobFs(connectionID, u.GetHomeDir(), \"\", u.FsConfig.AzBlobConfig)\n\tcase sdk.CryptedFilesystemProvider:\n\t\treturn vfs.NewCryptFs(connectionID, u.GetHomeDir(), \"\", u.FsConfig.CryptConfig)\n\tcase sdk.SFTPFilesystemProvider:\n\t\tforbiddenSelfUsers, err := u.getForbiddenSFTPSelfUsers(u.FsConfig.SFTPConfig.Username)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tforbiddenSelfUsers = append(forbiddenSelfUsers, u.Username)\n\t\treturn vfs.NewSFTPFs(connectionID, \"\", u.GetHomeDir(), forbiddenSelfUsers, u.FsConfig.SFTPConfig)\n\tcase sdk.HTTPFilesystemProvider:\n\t\treturn vfs.NewHTTPFs(connectionID, u.GetHomeDir(), \"\", u.FsConfig.HTTPConfig)\n\tdefault:\n\t\treturn vfs.NewOsFs(connectionID, u.GetHomeDir(), \"\", &u.FsConfig.OSConfig), nil\n\t}\n}\n\nfunc (u *User) checkDirWithParents(virtualDirPath, connectionID string) error {\n\tdirs := util.GetDirsForVirtualPath(virtualDirPath)\n\tfor idx := len(dirs) - 1; idx >= 0; idx-- {\n\t\tvPath := dirs[idx]\n\t\tif vPath == \"/\" {\n\t\t\tcontinue\n\t\t}\n\t\tfs, err := u.GetFilesystemForPath(vPath, connectionID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to get fs for path %q: %w\", vPath, err)\n\t\t}\n\t\tif fs.HasVirtualFolders() {\n\t\t\tcontinue\n\t\t}\n\t\tfsPath, err := fs.ResolvePath(vPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to resolve path %q: %w\", vPath, err)\n\t\t}\n\t\t_, err = fs.Stat(fsPath)\n\t\tif err == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif fs.IsNotExist(err) {\n\t\t\terr = fs.Mkdir(fsPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tvfs.SetPathPermissions(fs, fsPath, u.GetUID(), u.GetGID())\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"unable to stat path %q: %w\", vPath, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (u *User) checkLocalHomeDir(connectionID string) {\n\tswitch u.FsConfig.Provider {\n\tcase sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider:\n\t\treturn\n\tdefault:\n\t\tosFs := vfs.NewOsFs(connectionID, u.GetHomeDir(), \"\", nil)\n\t\tosFs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())\n\t}\n}\n\nfunc (u *User) checkRootPath(connectionID string) error {\n\tfs, err := u.GetFilesystemForPath(\"/\", connectionID)\n\tif err != nil {\n\t\tlogger.Warn(logSender, connectionID, \"could not create main filesystem for user %q err: %v\", u.Username, err)\n\t\treturn fmt.Errorf(\"could not create root filesystem: %w\", err)\n\t}\n\tfs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())\n\treturn nil\n}\n\n// CheckFsRoot check the root directory for the main fs and the virtual folders.\n// It returns an error if the main filesystem cannot be created\nfunc (u *User) CheckFsRoot(connectionID string) error {\n\tif u.Filters.DisableFsChecks {\n\t\treturn nil\n\t}\n\tdelay := lastLoginMinDelay\n\tif u.Filters.ExternalAuthCacheTime > 0 {\n\t\tcacheTime := time.Duration(u.Filters.ExternalAuthCacheTime) * time.Second\n\t\tif cacheTime > delay {\n\t\t\tdelay = cacheTime\n\t\t}\n\t}\n\tif isLastActivityRecent(u.LastLogin, delay) {\n\t\tif u.LastLogin > u.UpdatedAt {\n\t\t\tif config.IsShared == 1 {\n\t\t\t\tu.checkLocalHomeDir(connectionID)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\terr := u.checkRootPath(connectionID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif u.Filters.StartDirectory != \"\" {\n\t\terr = u.checkDirWithParents(u.Filters.StartDirectory, connectionID)\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, connectionID, \"could not create start directory %q, err: %v\",\n\t\t\t\tu.Filters.StartDirectory, err)\n\t\t}\n\t}\n\tfor idx := range u.VirtualFolders {\n\t\tv := &u.VirtualFolders[idx]\n\t\tfs, err := u.GetFilesystemForPath(v.VirtualPath, connectionID)\n\t\tif err == nil {\n\t\t\tfs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())\n\t\t}\n\t\t// now check intermediary folders\n\t\terr = u.checkDirWithParents(path.Dir(v.VirtualPath), connectionID)\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, connectionID, \"could not create intermediary dir to %q, err: %v\", v.VirtualPath, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetCleanedPath returns a clean POSIX absolute path using the user start directory as base\n// if the provided rawVirtualPath is relative\nfunc (u *User) GetCleanedPath(rawVirtualPath string) string {\n\tif u.Filters.StartDirectory != \"\" {\n\t\tif !path.IsAbs(rawVirtualPath) {\n\t\t\tvar b strings.Builder\n\n\t\t\tb.Grow(len(u.Filters.StartDirectory) + 1 + len(rawVirtualPath))\n\t\t\tb.WriteString(u.Filters.StartDirectory)\n\t\t\tb.WriteString(\"/\")\n\t\t\tb.WriteString(rawVirtualPath)\n\t\t\treturn util.CleanPath(b.String())\n\t\t}\n\t}\n\treturn util.CleanPath(rawVirtualPath)\n}\n\n// isFsEqual returns true if the filesystem configurations are the same\nfunc (u *User) isFsEqual(other *User) bool {\n\tif u.FsConfig.Provider == sdk.LocalFilesystemProvider && u.GetHomeDir() != other.GetHomeDir() {\n\t\treturn false\n\t}\n\tif !u.FsConfig.IsEqual(other.FsConfig) {\n\t\treturn false\n\t}\n\tif u.Filters.StartDirectory != other.Filters.StartDirectory {\n\t\treturn false\n\t}\n\tif len(u.VirtualFolders) != len(other.VirtualFolders) {\n\t\treturn false\n\t}\n\tfor idx := range u.VirtualFolders {\n\t\tf := &u.VirtualFolders[idx]\n\t\tfound := false\n\t\tfor idx1 := range other.VirtualFolders {\n\t\t\tf1 := &other.VirtualFolders[idx1]\n\t\t\tif f.VirtualPath == f1.VirtualPath {\n\t\t\t\tfound = true\n\t\t\t\tif f.FsConfig.Provider == sdk.LocalFilesystemProvider && f.MappedPath != f1.MappedPath {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif !f.FsConfig.IsEqual(f1.FsConfig) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (u *User) isTimeBasedAccessAllowed(when time.Time) bool {\n\tif len(u.Filters.AccessTime) == 0 {\n\t\treturn true\n\t}\n\tif when.IsZero() {\n\t\twhen = time.Now()\n\t}\n\tif UseLocalTime() {\n\t\twhen = when.Local()\n\t} else {\n\t\twhen = when.UTC()\n\t}\n\tweekDay := when.Weekday()\n\thhMM := when.Format(\"15:04\")\n\tfor _, p := range u.Filters.AccessTime {\n\t\tif p.DayOfWeek == int(weekDay) {\n\t\t\tif hhMM >= p.From && hhMM <= p.To {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// CheckLoginConditions checks user access restrictions\nfunc (u *User) CheckLoginConditions() error {\n\tif u.Status < 1 {\n\t\treturn fmt.Errorf(\"user %q is disabled\", u.Username)\n\t}\n\tif u.ExpirationDate > 0 && u.ExpirationDate < util.GetTimeAsMsSinceEpoch(time.Now()) {\n\t\treturn fmt.Errorf(\"user %q is expired, expiration timestamp: %v current timestamp: %v\", u.Username,\n\t\t\tu.ExpirationDate, util.GetTimeAsMsSinceEpoch(time.Now()))\n\t}\n\tif u.isTimeBasedAccessAllowed(time.Now()) {\n\t\treturn nil\n\t}\n\treturn errors.New(\"access is not allowed at this time\")\n}\n\n// hideConfidentialData hides user confidential data\nfunc (u *User) hideConfidentialData() {\n\tu.Password = \"\"\n\tu.FsConfig.HideConfidentialData()\n\tif u.Filters.TOTPConfig.Secret != nil {\n\t\tu.Filters.TOTPConfig.Secret.Hide()\n\t}\n\tfor _, code := range u.Filters.RecoveryCodes {\n\t\tif code.Secret != nil {\n\t\t\tcode.Secret.Hide()\n\t\t}\n\t}\n}\n\n// CheckMaxShareExpiration returns an error if the share expiration exceed the\n// maximum allowed date.\nfunc (u *User) CheckMaxShareExpiration(expiresAt time.Time) error {\n\tif u.Filters.MaxSharesExpiration == 0 {\n\t\treturn nil\n\t}\n\tmaxAllowedExpiration := time.Now().Add(24 * time.Hour * time.Duration(u.Filters.MaxSharesExpiration+1))\n\tmaxAllowedExpiration = time.Date(maxAllowedExpiration.Year(), maxAllowedExpiration.Month(),\n\t\tmaxAllowedExpiration.Day(), 0, 0, 0, 0, maxAllowedExpiration.Location())\n\tif util.GetTimeAsMsSinceEpoch(expiresAt) == 0 || expiresAt.After(maxAllowedExpiration) {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"the share must expire before %s\", maxAllowedExpiration.Format(time.DateOnly)))\n\t}\n\treturn nil\n}\n\n// GetEmailAddresses returns all the email addresses.\nfunc (u *User) GetEmailAddresses() []string {\n\tvar res []string\n\tif u.Email != \"\" {\n\t\tres = append(res, u.Email)\n\t}\n\treturn slices.Concat(res, u.Filters.AdditionalEmails)\n}\n\n// GetSubDirPermissions returns permissions for sub directories\nfunc (u *User) GetSubDirPermissions() []sdk.DirectoryPermissions {\n\tvar result []sdk.DirectoryPermissions\n\tfor k, v := range u.Permissions {\n\t\tif k == \"/\" {\n\t\t\tcontinue\n\t\t}\n\t\tdirPerms := sdk.DirectoryPermissions{\n\t\t\tPath:        k,\n\t\t\tPermissions: v,\n\t\t}\n\t\tresult = append(result, dirPerms)\n\t}\n\treturn result\n}\n\nfunc (u *User) setAnonymousSettings() {\n\tfor k := range u.Permissions {\n\t\tu.Permissions[k] = []string{PermListItems, PermDownload}\n\t}\n\tu.Filters.DeniedProtocols = append(u.Filters.DeniedProtocols, protocolSSH, protocolHTTP)\n\tu.Filters.DeniedProtocols = util.RemoveDuplicates(u.Filters.DeniedProtocols, false)\n\tfor _, method := range ValidLoginMethods {\n\t\tif method != LoginMethodPassword {\n\t\t\tu.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, method)\n\t\t}\n\t}\n\tu.Filters.DeniedLoginMethods = util.RemoveDuplicates(u.Filters.DeniedLoginMethods, false)\n}\n\n// RenderAsJSON implements the renderer interface used within plugins\nfunc (u *User) RenderAsJSON(reload bool) ([]byte, error) {\n\tif reload {\n\t\tuser, err := provider.userExists(u.Username, \"\")\n\t\tif err != nil {\n\t\t\tproviderLog(logger.LevelError, \"unable to reload user before rendering as json: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tuser.PrepareForRendering()\n\t\treturn json.Marshal(user)\n\t}\n\tu.PrepareForRendering()\n\treturn json.Marshal(u)\n}\n\n// PrepareForRendering prepares a user for rendering.\n// It hides confidential data and set to nil the empty secrets\n// so they are not serialized\nfunc (u *User) PrepareForRendering() {\n\tu.hideConfidentialData()\n\tu.FsConfig.SetNilSecretsIfEmpty()\n\tfor idx := range u.VirtualFolders {\n\t\tfolder := &u.VirtualFolders[idx]\n\t\tfolder.PrepareForRendering()\n\t}\n}\n\n// HasRedactedSecret returns true if the user has a redacted secret\nfunc (u *User) hasRedactedSecret() bool {\n\tif u.FsConfig.HasRedactedSecret() {\n\t\treturn true\n\t}\n\n\tfor idx := range u.VirtualFolders {\n\t\tfolder := &u.VirtualFolders[idx]\n\t\tif folder.HasRedactedSecret() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn u.Filters.TOTPConfig.Secret.IsRedacted()\n}\n\n// CloseFs closes the underlying filesystems\nfunc (u *User) CloseFs() error {\n\tif u.fsCache == nil {\n\t\treturn nil\n\t}\n\n\tvar err error\n\tfor _, fs := range u.fsCache {\n\t\terrClose := fs.Close()\n\t\tif err == nil {\n\t\t\terr = errClose\n\t\t}\n\t}\n\treturn err\n}\n\n// IsPasswordHashed returns true if the password is hashed\nfunc (u *User) IsPasswordHashed() bool {\n\treturn util.IsStringPrefixInSlice(u.Password, hashPwdPrefixes)\n}\n\n// IsTLSVerificationEnabled returns true if we need to check the TLS authentication\nfunc (u *User) IsTLSVerificationEnabled() bool {\n\tif len(u.Filters.TLSCerts) > 0 {\n\t\treturn true\n\t}\n\tif u.Filters.TLSUsername != \"\" {\n\t\treturn u.Filters.TLSUsername != sdk.TLSUsernameNone\n\t}\n\treturn false\n}\n\n// SetEmptySecrets sets to empty any user secret\nfunc (u *User) SetEmptySecrets() {\n\tu.FsConfig.SetEmptySecrets()\n\tfor idx := range u.VirtualFolders {\n\t\tfolder := &u.VirtualFolders[idx]\n\t\tfolder.FsConfig.SetEmptySecrets()\n\t}\n\tu.Filters.TOTPConfig.Secret = kms.NewEmptySecret()\n}\n\n// GetPermissionsForPath returns the permissions for the given path.\n// The path must be a SFTPGo virtual path\nfunc (u *User) GetPermissionsForPath(p string) []string {\n\tpermissions := []string{}\n\tif perms, ok := u.Permissions[\"/\"]; ok {\n\t\t// if only root permissions are defined returns them unconditionally\n\t\tif len(u.Permissions) == 1 {\n\t\t\treturn perms\n\t\t}\n\t\t// fallback permissions\n\t\tpermissions = perms\n\t}\n\tdirsForPath := util.GetDirsForVirtualPath(p)\n\t// dirsForPath contains all the dirs for a given path in reverse order\n\t// for example if the path is: /1/2/3/4 it contains:\n\t// [ \"/1/2/3/4\", \"/1/2/3\", \"/1/2\", \"/1\", \"/\" ]\n\t// so the first match is the one we are interested to\n\tfor idx := range dirsForPath {\n\t\tif perms, ok := u.Permissions[dirsForPath[idx]]; ok {\n\t\t\treturn perms\n\t\t}\n\t\tfor dir, perms := range u.Permissions {\n\t\t\tif match, err := path.Match(dir, dirsForPath[idx]); err == nil && match {\n\t\t\t\treturn perms\n\t\t\t}\n\t\t}\n\t}\n\treturn permissions\n}\n\nfunc (u *User) getForbiddenSFTPSelfUsers(username string) ([]string, error) {\n\tif allowSelfConnections == 0 {\n\t\treturn nil, nil\n\t}\n\tsftpUser, err := UserExists(username, \"\")\n\tif err == nil {\n\t\terr = sftpUser.LoadAndApplyGroupSettings()\n\t}\n\tif err == nil {\n\t\t// we don't allow local nested SFTP folders\n\t\tvar forbiddens []string\n\t\tif sftpUser.FsConfig.Provider == sdk.SFTPFilesystemProvider {\n\t\t\tforbiddens = append(forbiddens, sftpUser.Username)\n\t\t\treturn forbiddens, nil\n\t\t}\n\t\tfor idx := range sftpUser.VirtualFolders {\n\t\t\tv := &sftpUser.VirtualFolders[idx]\n\t\t\tif v.FsConfig.Provider == sdk.SFTPFilesystemProvider {\n\t\t\t\tforbiddens = append(forbiddens, sftpUser.Username)\n\t\t\t\treturn forbiddens, nil\n\t\t\t}\n\t\t}\n\t\treturn forbiddens, nil\n\t}\n\tif !errors.Is(err, util.ErrNotFound) {\n\t\treturn nil, err\n\t}\n\n\treturn nil, nil\n}\n\n// GetFsConfigForPath returns the file system configuration for the specified virtual path\nfunc (u *User) GetFsConfigForPath(virtualPath string) vfs.Filesystem {\n\tif virtualPath != \"\" && virtualPath != \"/\" && len(u.VirtualFolders) > 0 {\n\t\tfolder, err := u.GetVirtualFolderForPath(virtualPath)\n\t\tif err == nil {\n\t\t\treturn folder.FsConfig\n\t\t}\n\t}\n\n\treturn u.FsConfig\n}\n\n// GetFilesystemForPath returns the filesystem for the given path\nfunc (u *User) GetFilesystemForPath(virtualPath, connectionID string) (vfs.Fs, error) {\n\tif u.fsCache == nil {\n\t\tu.fsCache = make(map[string]vfs.Fs)\n\t}\n\t// allow to override the `/` path with a virtual folder\n\tif len(u.VirtualFolders) > 0 {\n\t\tfolder, err := u.GetVirtualFolderForPath(virtualPath)\n\t\tif err == nil {\n\t\t\tif fs, ok := u.fsCache[folder.VirtualPath]; ok {\n\t\t\t\treturn fs, nil\n\t\t\t}\n\t\t\tforbiddenSelfUsers := []string{u.Username}\n\t\t\tif folder.FsConfig.Provider == sdk.SFTPFilesystemProvider {\n\t\t\t\tforbiddens, err := u.getForbiddenSFTPSelfUsers(folder.FsConfig.SFTPConfig.Username)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tforbiddenSelfUsers = append(forbiddenSelfUsers, forbiddens...)\n\t\t\t}\n\t\t\tfs, err := folder.GetFilesystem(connectionID, forbiddenSelfUsers)\n\t\t\tif err == nil {\n\t\t\t\tu.fsCache[folder.VirtualPath] = fs\n\t\t\t}\n\t\t\treturn fs, err\n\t\t}\n\t}\n\n\tif val, ok := u.fsCache[\"/\"]; ok {\n\t\treturn val, nil\n\t}\n\tfs, err := u.getRootFs(connectionID)\n\tif err != nil {\n\t\treturn fs, err\n\t}\n\tu.fsCache[\"/\"] = fs\n\treturn fs, err\n}\n\n// GetVirtualFolderForPath returns the virtual folder containing the specified virtual path.\n// If the path is not inside a virtual folder an error is returned\nfunc (u *User) GetVirtualFolderForPath(virtualPath string) (vfs.VirtualFolder, error) {\n\tvar folder vfs.VirtualFolder\n\tif len(u.VirtualFolders) == 0 {\n\t\treturn folder, errNoMatchingVirtualFolder\n\t}\n\tdirsForPath := util.GetDirsForVirtualPath(virtualPath)\n\tfor index := range dirsForPath {\n\t\tfor idx := range u.VirtualFolders {\n\t\t\tv := &u.VirtualFolders[idx]\n\t\t\tif v.VirtualPath == dirsForPath[index] {\n\t\t\t\treturn *v, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn folder, errNoMatchingVirtualFolder\n}\n\n// ScanQuota scans the user home dir and virtual folders, included in its quota,\n// and returns the number of files and their size\nfunc (u *User) ScanQuota() (int, int64, error) {\n\tfs, err := u.getRootFs(xid.New().String())\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\tdefer fs.Close()\n\n\tnumFiles, size, err := fs.ScanRootDirContents()\n\tif err != nil {\n\t\treturn numFiles, size, err\n\t}\n\tfor idx := range u.VirtualFolders {\n\t\tv := &u.VirtualFolders[idx]\n\t\tif !v.IsIncludedInUserQuota() {\n\t\t\tcontinue\n\t\t}\n\t\tnum, s, err := v.ScanQuota()\n\t\tif err != nil {\n\t\t\treturn numFiles, size, err\n\t\t}\n\t\tnumFiles += num\n\t\tsize += s\n\t}\n\n\treturn numFiles, size, nil\n}\n\n// GetVirtualFoldersInPath returns the virtual folders inside virtualPath including\n// any parents\nfunc (u *User) GetVirtualFoldersInPath(virtualPath string) map[string]bool {\n\tresult := make(map[string]bool)\n\n\tfor idx := range u.VirtualFolders {\n\t\tdirsForPath := util.GetDirsForVirtualPath(u.VirtualFolders[idx].VirtualPath)\n\t\tfor index := range dirsForPath {\n\t\t\td := dirsForPath[index]\n\t\t\tif d == \"/\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif path.Dir(d) == virtualPath {\n\t\t\t\tresult[d] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif u.Filters.StartDirectory != \"\" {\n\t\tdirsForPath := util.GetDirsForVirtualPath(u.Filters.StartDirectory)\n\t\tfor index := range dirsForPath {\n\t\t\td := dirsForPath[index]\n\t\t\tif d == \"/\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif path.Dir(d) == virtualPath {\n\t\t\t\tresult[d] = true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc (u *User) hasVirtualDirs() bool {\n\tif u.Filters.StartDirectory != \"\" {\n\t\treturn true\n\t}\n\tnumFolders := len(u.VirtualFolders)\n\tif numFolders == 1 {\n\t\treturn u.VirtualFolders[0].VirtualPath != \"/\"\n\t}\n\treturn numFolders > 0\n}\n\n// GetVirtualFoldersInfo returns []os.FileInfo for virtual folders\nfunc (u *User) GetVirtualFoldersInfo(virtualPath string) []os.FileInfo {\n\tfilter := u.getPatternsFilterForPath(virtualPath)\n\tif !u.hasVirtualDirs() && filter.DenyPolicy != sdk.DenyPolicyHide {\n\t\treturn nil\n\t}\n\tvdirs := u.GetVirtualFoldersInPath(virtualPath)\n\tresult := make([]os.FileInfo, 0, len(vdirs))\n\n\tfor dir := range u.GetVirtualFoldersInPath(virtualPath) {\n\t\tdirName := path.Base(dir)\n\t\tif filter.DenyPolicy == sdk.DenyPolicyHide {\n\t\t\tif !filter.CheckAllowed(dirName) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tresult = append(result, vfs.NewFileInfo(dirName, true, 0, time.Unix(0, 0), false))\n\t}\n\n\treturn result\n}\n\n// FilterListDir removes hidden items from the given files list\nfunc (u *User) FilterListDir(dirContents []os.FileInfo, virtualPath string) []os.FileInfo {\n\tfilter := u.getPatternsFilterForPath(virtualPath)\n\tif !u.hasVirtualDirs() && filter.DenyPolicy != sdk.DenyPolicyHide {\n\t\treturn dirContents\n\t}\n\tvdirs := make(map[string]bool)\n\tfor dir := range u.GetVirtualFoldersInPath(virtualPath) {\n\t\tdirName := path.Base(dir)\n\t\tif filter.DenyPolicy == sdk.DenyPolicyHide {\n\t\t\tif !filter.CheckAllowed(dirName) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tvdirs[dirName] = true\n\t}\n\n\tvalidIdx := 0\n\tfor idx := range dirContents {\n\t\tfi := dirContents[idx]\n\n\t\tif fi.Name() != \".\" && fi.Name() != \"..\" {\n\t\t\tif _, ok := vdirs[fi.Name()]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif filter.DenyPolicy == sdk.DenyPolicyHide {\n\t\t\t\tif !filter.CheckAllowed(fi.Name()) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdirContents[validIdx] = fi\n\t\tvalidIdx++\n\t}\n\n\treturn dirContents[:validIdx]\n}\n\n// IsMappedPath returns true if the specified filesystem path has a virtual folder mapping.\n// The filesystem path must be cleaned before calling this method\nfunc (u *User) IsMappedPath(fsPath string) bool {\n\tfor idx := range u.VirtualFolders {\n\t\tv := &u.VirtualFolders[idx]\n\t\tif fsPath == v.MappedPath {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// IsVirtualFolder returns true if the specified virtual path is a virtual folder\nfunc (u *User) IsVirtualFolder(virtualPath string) bool {\n\tfor idx := range u.VirtualFolders {\n\t\tv := &u.VirtualFolders[idx]\n\t\tif virtualPath == v.VirtualPath {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// HasVirtualFoldersInside returns true if there are virtual folders inside the\n// specified virtual path. We assume that path are cleaned\nfunc (u *User) HasVirtualFoldersInside(virtualPath string) bool {\n\tif virtualPath == \"/\" && len(u.VirtualFolders) > 0 {\n\t\treturn true\n\t}\n\tfor idx := range u.VirtualFolders {\n\t\tv := &u.VirtualFolders[idx]\n\t\tif len(v.VirtualPath) > len(virtualPath) {\n\t\t\tif strings.HasPrefix(v.VirtualPath, virtualPath+\"/\") {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// HasPermissionsInside returns true if the specified virtualPath has no permissions itself and\n// no subdirs with defined permissions\nfunc (u *User) HasPermissionsInside(virtualPath string) bool {\n\tfor dir, perms := range u.Permissions {\n\t\tif len(perms) == 1 && perms[0] == PermAny {\n\t\t\tcontinue\n\t\t}\n\t\tif dir == virtualPath {\n\t\t\treturn true\n\t\t} else if len(dir) > len(virtualPath) {\n\t\t\tif strings.HasPrefix(dir, virtualPath+\"/\") {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// HasPerm returns true if the user has the given permission or any permission\nfunc (u *User) HasPerm(permission, path string) bool {\n\tperms := u.GetPermissionsForPath(path)\n\tif slices.Contains(perms, PermAny) {\n\t\treturn true\n\t}\n\treturn slices.Contains(perms, permission)\n}\n\n// HasAnyPerm returns true if the user has at least one of the given permissions\nfunc (u *User) HasAnyPerm(permissions []string, path string) bool {\n\tperms := u.GetPermissionsForPath(path)\n\tif slices.Contains(perms, PermAny) {\n\t\treturn true\n\t}\n\tfor _, permission := range permissions {\n\t\tif slices.Contains(perms, permission) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// HasPerms returns true if the user has all the given permissions\nfunc (u *User) HasPerms(permissions []string, path string) bool {\n\tperms := u.GetPermissionsForPath(path)\n\tif slices.Contains(perms, PermAny) {\n\t\treturn true\n\t}\n\tfor _, permission := range permissions {\n\t\tif !slices.Contains(perms, permission) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// HasPermsDeleteAll returns true if the user can delete both files and directories\n// for the given path\nfunc (u *User) HasPermsDeleteAll(path string) bool {\n\tperms := u.GetPermissionsForPath(path)\n\tcanDeleteFiles := false\n\tcanDeleteDirs := false\n\tfor _, permission := range perms {\n\t\tif permission == PermAny || permission == PermDelete {\n\t\t\treturn true\n\t\t}\n\t\tif permission == PermDeleteFiles {\n\t\t\tcanDeleteFiles = true\n\t\t}\n\t\tif permission == PermDeleteDirs {\n\t\t\tcanDeleteDirs = true\n\t\t}\n\t}\n\treturn canDeleteFiles && canDeleteDirs\n}\n\n// HasPermsRenameAll returns true if the user can rename both files and directories\n// for the given path\nfunc (u *User) HasPermsRenameAll(path string) bool {\n\tperms := u.GetPermissionsForPath(path)\n\tcanRenameFiles := false\n\tcanRenameDirs := false\n\tfor _, permission := range perms {\n\t\tif permission == PermAny || permission == PermRename {\n\t\t\treturn true\n\t\t}\n\t\tif permission == PermRenameFiles {\n\t\t\tcanRenameFiles = true\n\t\t}\n\t\tif permission == PermRenameDirs {\n\t\t\tcanRenameDirs = true\n\t\t}\n\t}\n\treturn canRenameFiles && canRenameDirs\n}\n\n// HasNoQuotaRestrictions returns true if no quota restrictions need to be applyed\nfunc (u *User) HasNoQuotaRestrictions(checkFiles bool) bool {\n\tif u.QuotaSize == 0 && (!checkFiles || u.QuotaFiles == 0) {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// IsLoginMethodAllowed returns true if the specified login method is allowed\nfunc (u *User) IsLoginMethodAllowed(loginMethod, protocol string) bool {\n\tif len(u.Filters.DeniedLoginMethods) == 0 {\n\t\treturn true\n\t}\n\tif slices.Contains(u.Filters.DeniedLoginMethods, loginMethod) {\n\t\treturn false\n\t}\n\tif protocol == protocolSSH && loginMethod == LoginMethodPassword {\n\t\tif slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// GetNextAuthMethods returns the list of authentications methods that can\n// continue for multi-step authentication. We call this method after a\n// successful public key authentication.\nfunc (u *User) GetNextAuthMethods() []string {\n\tvar methods []string\n\tfor _, method := range u.GetAllowedLoginMethods() {\n\t\tif method == SSHLoginMethodKeyAndPassword {\n\t\t\tmethods = append(methods, LoginMethodPassword)\n\t\t}\n\t\tif method == SSHLoginMethodKeyAndKeyboardInt {\n\t\t\tmethods = append(methods, SSHLoginMethodKeyboardInteractive)\n\t\t}\n\t}\n\treturn methods\n}\n\n// IsPartialAuth returns true if the specified login method is a step for\n// a multi-step Authentication.\n// We support publickey+password and publickey+keyboard-interactive, so\n// only publickey can returns partial success.\n// We can have partial success if only multi-step Auth methods are enabled\nfunc (u *User) IsPartialAuth() bool {\n\tfor _, method := range u.GetAllowedLoginMethods() {\n\t\tif method == LoginMethodTLSCertificate || method == LoginMethodTLSCertificateAndPwd ||\n\t\t\tmethod == SSHLoginMethodPassword {\n\t\t\tcontinue\n\t\t}\n\t\tif method == LoginMethodPassword && slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {\n\t\t\tcontinue\n\t\t}\n\t\tif !slices.Contains(SSHMultiStepsLoginMethods, method) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// GetAllowedLoginMethods returns the allowed login methods\nfunc (u *User) GetAllowedLoginMethods() []string {\n\tvar allowedMethods []string\n\tfor _, method := range ValidLoginMethods {\n\t\tif method == SSHLoginMethodPassword {\n\t\t\tcontinue\n\t\t}\n\t\tif !slices.Contains(u.Filters.DeniedLoginMethods, method) {\n\t\t\tallowedMethods = append(allowedMethods, method)\n\t\t}\n\t}\n\treturn allowedMethods\n}\n\nfunc (u *User) getPatternsFilterForPath(virtualPath string) sdk.PatternsFilter {\n\tvar filter sdk.PatternsFilter\n\tif len(u.Filters.FilePatterns) == 0 {\n\t\treturn filter\n\t}\n\tdirsForPath := util.GetDirsForVirtualPath(virtualPath)\n\tfor idx, dir := range dirsForPath {\n\t\tfor _, f := range u.Filters.FilePatterns {\n\t\t\tif f.Path == dir {\n\t\t\t\tif idx > 0 && len(f.AllowedPatterns) > 0 && len(f.DeniedPatterns) > 0 && f.DeniedPatterns[0] == \"*\" {\n\t\t\t\t\tif f.CheckAllowed(path.Base(dirsForPath[idx-1])) {\n\t\t\t\t\t\treturn filter\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfilter = f\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif filter.Path != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn filter\n}\n\nfunc (u *User) isDirHidden(virtualPath string) bool {\n\tif len(u.Filters.FilePatterns) == 0 {\n\t\treturn false\n\t}\n\tfor _, dirPath := range util.GetDirsForVirtualPath(virtualPath) {\n\t\tif dirPath == \"/\" {\n\t\t\treturn false\n\t\t}\n\t\tfilter := u.getPatternsFilterForPath(dirPath)\n\t\tif filter.DenyPolicy == sdk.DenyPolicyHide && filter.Path != dirPath {\n\t\t\tif !filter.CheckAllowed(path.Base(dirPath)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (u *User) getMinPasswordEntropy() float64 {\n\tif u.Filters.PasswordStrength > 0 {\n\t\treturn float64(u.Filters.PasswordStrength)\n\t}\n\treturn config.PasswordValidation.Users.MinEntropy\n}\n\n// IsFileAllowed returns true if the specified file is allowed by the file restrictions filters.\n// The second parameter returned is the deny policy\nfunc (u *User) IsFileAllowed(virtualPath string) (bool, int) {\n\tdirPath := path.Dir(virtualPath)\n\tif u.isDirHidden(dirPath) {\n\t\treturn false, sdk.DenyPolicyHide\n\t}\n\tfilter := u.getPatternsFilterForPath(dirPath)\n\treturn filter.CheckAllowed(path.Base(virtualPath)), filter.DenyPolicy\n}\n\n// CanManageMFA returns true if the user can add a multi-factor authentication configuration\nfunc (u *User) CanManageMFA() bool {\n\tif slices.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {\n\t\treturn false\n\t}\n\treturn len(mfa.GetAvailableTOTPConfigs()) > 0\n}\n\nfunc (u *User) skipExternalAuth() bool {\n\tif u.Filters.Hooks.ExternalAuthDisabled {\n\t\treturn true\n\t}\n\tif u.ID <= 0 {\n\t\treturn false\n\t}\n\tif u.Filters.ExternalAuthCacheTime <= 0 {\n\t\treturn false\n\t}\n\treturn isLastActivityRecent(u.LastLogin, time.Duration(u.Filters.ExternalAuthCacheTime)*time.Second)\n}\n\n// CanManageShares returns true if the user can add, update and list shares\nfunc (u *User) CanManageShares() bool {\n\treturn !slices.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)\n}\n\n// CanResetPassword returns true if this user is allowed to reset its password\nfunc (u *User) CanResetPassword() bool {\n\treturn !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)\n}\n\n// CanChangePassword returns true if this user is allowed to change its password\nfunc (u *User) CanChangePassword() bool {\n\treturn !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)\n}\n\n// CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication\nfunc (u *User) CanChangeAPIKeyAuth() bool {\n\treturn !slices.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)\n}\n\n// CanChangeInfo returns true if this user is allowed to change its info such as email and description\nfunc (u *User) CanChangeInfo() bool {\n\treturn !slices.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)\n}\n\n// CanManagePublicKeys returns true if this user is allowed to manage public keys\n// from the WebClient. Used in WebClient UI\nfunc (u *User) CanManagePublicKeys() bool {\n\treturn !slices.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)\n}\n\n// CanManageTLSCerts returns true if this user is allowed to manage TLS certificates\n// from the WebClient. Used in WebClient UI\nfunc (u *User) CanManageTLSCerts() bool {\n\treturn !slices.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)\n}\n\n// CanUpdateProfile returns true if the user is allowed to update the profile.\n// Used in WebClient UI\nfunc (u *User) CanUpdateProfile() bool {\n\treturn u.CanManagePublicKeys() || u.CanChangeAPIKeyAuth() || u.CanChangeInfo() || u.CanManageTLSCerts()\n}\n\n// CanAddFilesFromWeb returns true if the client can add files from the web UI.\n// The specified target is the directory where the files must be uploaded\nfunc (u *User) CanAddFilesFromWeb(target string) bool {\n\tif slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {\n\t\treturn false\n\t}\n\treturn u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target)\n}\n\n// CanAddDirsFromWeb returns true if the client can add directories from the web UI.\n// The specified target is the directory where the new directory must be created\nfunc (u *User) CanAddDirsFromWeb(target string) bool {\n\tif slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {\n\t\treturn false\n\t}\n\treturn u.HasPerm(PermCreateDirs, target)\n}\n\n// CanRenameFromWeb returns true if the client can rename objects from the web UI.\n// The specified src and dest are the source and target directories for the rename.\nfunc (u *User) CanRenameFromWeb(src, dest string) bool {\n\tif slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {\n\t\treturn false\n\t}\n\treturn u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest)\n}\n\n// CanDeleteFromWeb returns true if the client can delete objects from the web UI.\n// The specified target is the parent directory for the object to delete\nfunc (u *User) CanDeleteFromWeb(target string) bool {\n\tif slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {\n\t\treturn false\n\t}\n\treturn u.HasAnyPerm(permsDeleteAny, target)\n}\n\n// CanCopyFromWeb returns true if the client can copy objects from the web UI.\n// The specified src and dest are the source and target directories for the copy.\nfunc (u *User) CanCopyFromWeb(src, dest string) bool {\n\tif slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {\n\t\treturn false\n\t}\n\tif !u.HasPerm(PermListItems, src) {\n\t\treturn false\n\t}\n\tif !u.HasPerm(PermDownload, src) {\n\t\treturn false\n\t}\n\treturn u.HasPerm(PermCopy, src) && u.HasPerm(PermCopy, dest)\n}\n\n// InactivityDays returns the number of days of inactivity\nfunc (u *User) InactivityDays(when time.Time) int {\n\tif when.IsZero() {\n\t\twhen = time.Now()\n\t}\n\tlastActivity := u.LastLogin\n\tif lastActivity == 0 {\n\t\tlastActivity = u.CreatedAt\n\t}\n\tif lastActivity == 0 {\n\t\t// unable to determine inactivity\n\t\treturn 0\n\t}\n\treturn int(float64(when.Sub(util.GetTimeFromMsecSinceEpoch(lastActivity))) / float64(24*time.Hour))\n}\n\n// PasswordExpiresIn returns the number of days before the password expires.\n// The returned value is negative if the password is expired.\n// The caller must ensure that a PasswordExpiration is set\nfunc (u *User) PasswordExpiresIn() int {\n\tlastPwdChange := util.GetTimeFromMsecSinceEpoch(u.LastPasswordChange)\n\tpwdExpiration := lastPwdChange.Add(time.Duration(u.Filters.PasswordExpiration) * 24 * time.Hour)\n\tres := int(math.Round(float64(time.Until(pwdExpiration)) / float64(24*time.Hour)))\n\tif res == 0 && pwdExpiration.After(time.Now()) {\n\t\tres = 1\n\t}\n\treturn res\n}\n\n// MustChangePassword returns true if the user must change the password\nfunc (u *User) MustChangePassword() bool {\n\tif u.Filters.RequirePasswordChange {\n\t\treturn true\n\t}\n\tif u.Filters.PasswordExpiration == 0 {\n\t\treturn false\n\t}\n\tlastPwdChange := util.GetTimeFromMsecSinceEpoch(u.LastPasswordChange)\n\treturn lastPwdChange.Add(time.Duration(u.Filters.PasswordExpiration) * 24 * time.Hour).Before(time.Now())\n}\n\n// MustSetSecondFactor returns true if the user must set a second factor authentication\nfunc (u *User) MustSetSecondFactor() bool {\n\tif len(u.Filters.TwoFactorAuthProtocols) > 0 {\n\t\tif !u.Filters.TOTPConfig.Enabled {\n\t\t\treturn true\n\t\t}\n\t\tfor _, p := range u.Filters.TwoFactorAuthProtocols {\n\t\t\tif !slices.Contains(u.Filters.TOTPConfig.Protocols, p) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication\n// for the specified protocol\nfunc (u *User) MustSetSecondFactorForProtocol(protocol string) bool {\n\tif slices.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {\n\t\tif !u.Filters.TOTPConfig.Enabled {\n\t\t\treturn true\n\t\t}\n\t\tif !slices.Contains(u.Filters.TOTPConfig.Protocols, protocol) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// GetSignature returns a signature for this user.\n// It will change after an update\nfunc (u *User) GetSignature() string {\n\treturn strconv.FormatInt(u.UpdatedAt, 10)\n}\n\n// GetBandwidthForIP returns the upload and download bandwidth for the specified IP\nfunc (u *User) GetBandwidthForIP(clientIP, connectionID string) (int64, int64) {\n\tif len(u.Filters.BandwidthLimits) > 0 {\n\t\tip := net.ParseIP(clientIP)\n\t\tif ip != nil {\n\t\t\tfor _, bwLimit := range u.Filters.BandwidthLimits {\n\t\t\t\tfor _, source := range bwLimit.Sources {\n\t\t\t\t\t_, ipNet, err := net.ParseCIDR(source)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif ipNet.Contains(ip) {\n\t\t\t\t\t\t\tlogger.Debug(logSender, connectionID, \"override bandwidth limit for ip %q, upload limit: %v KB/s, download limit: %v KB/s\",\n\t\t\t\t\t\t\t\tclientIP, bwLimit.UploadBandwidth, bwLimit.DownloadBandwidth)\n\t\t\t\t\t\t\treturn bwLimit.UploadBandwidth, bwLimit.DownloadBandwidth\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\treturn u.UploadBandwidth, u.DownloadBandwidth\n}\n\n// IsLoginFromAddrAllowed returns true if the login is allowed from the specified remoteAddr.\n// If AllowedIP is defined only the specified IP/Mask can login.\n// If DeniedIP is defined the specified IP/Mask cannot login.\n// If an IP is both allowed and denied then login will be allowed\nfunc (u *User) IsLoginFromAddrAllowed(remoteAddr string) bool {\n\tif len(u.Filters.AllowedIP) == 0 && len(u.Filters.DeniedIP) == 0 {\n\t\treturn true\n\t}\n\tremoteIP := net.ParseIP(util.GetIPFromRemoteAddress(remoteAddr))\n\t// if remoteIP is invalid we allow login, this should never happen\n\tif remoteIP == nil {\n\t\tlogger.Warn(logSender, \"\", \"login allowed for invalid IP. remote address: %q\", remoteAddr)\n\t\treturn true\n\t}\n\tfor _, IPMask := range u.Filters.AllowedIP {\n\t\t_, IPNet, err := net.ParseCIDR(IPMask)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\tif IPNet.Contains(remoteIP) {\n\t\t\treturn true\n\t\t}\n\t}\n\tfor _, IPMask := range u.Filters.DeniedIP {\n\t\t_, IPNet, err := net.ParseCIDR(IPMask)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\tif IPNet.Contains(remoteIP) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn len(u.Filters.AllowedIP) == 0\n}\n\n// GetPermissionsAsJSON returns the permissions as json byte array\nfunc (u *User) GetPermissionsAsJSON() ([]byte, error) {\n\treturn json.Marshal(u.Permissions)\n}\n\n// GetPublicKeysAsJSON returns the public keys as json byte array\nfunc (u *User) GetPublicKeysAsJSON() ([]byte, error) {\n\treturn json.Marshal(u.PublicKeys)\n}\n\n// GetFiltersAsJSON returns the filters as json byte array\nfunc (u *User) GetFiltersAsJSON() ([]byte, error) {\n\treturn json.Marshal(u.Filters)\n}\n\n// GetFsConfigAsJSON returns the filesystem config as json byte array\nfunc (u *User) GetFsConfigAsJSON() ([]byte, error) {\n\treturn json.Marshal(u.FsConfig)\n}\n\n// GetUID returns a validate uid, suitable for use with os.Chown\nfunc (u *User) GetUID() int {\n\tif u.UID <= 0 || u.UID > math.MaxInt32 {\n\t\treturn -1\n\t}\n\treturn u.UID\n}\n\n// GetGID returns a validate gid, suitable for use with os.Chown\nfunc (u *User) GetGID() int {\n\tif u.GID <= 0 || u.GID > math.MaxInt32 {\n\t\treturn -1\n\t}\n\treturn u.GID\n}\n\n// GetHomeDir returns the shortest path name equivalent to the user's home directory\nfunc (u *User) GetHomeDir() string {\n\treturn u.HomeDir\n}\n\n// HasRecentActivity returns true if the last user login is recent and so we can skip some expensive checks\nfunc (u *User) HasRecentActivity() bool {\n\treturn isLastActivityRecent(u.LastLogin, lastLoginMinDelay)\n}\n\n// HasQuotaRestrictions returns true if there are any disk quota restrictions\nfunc (u *User) HasQuotaRestrictions() bool {\n\treturn u.QuotaFiles > 0 || u.QuotaSize > 0\n}\n\n// HasTransferQuotaRestrictions returns true if there are any data transfer restrictions\nfunc (u *User) HasTransferQuotaRestrictions() bool {\n\treturn u.UploadDataTransfer > 0 || u.TotalDataTransfer > 0 || u.DownloadDataTransfer > 0\n}\n\n// GetDataTransferLimits returns upload, download and total data transfer limits\nfunc (u *User) GetDataTransferLimits() (int64, int64, int64) {\n\tvar total, ul, dl int64\n\tif u.TotalDataTransfer > 0 {\n\t\ttotal = u.TotalDataTransfer * 1048576\n\t}\n\tif u.DownloadDataTransfer > 0 {\n\t\tdl = u.DownloadDataTransfer * 1048576\n\t}\n\tif u.UploadDataTransfer > 0 {\n\t\tul = u.UploadDataTransfer * 1048576\n\t}\n\treturn ul, dl, total\n}\n\n// GetAllowedIPAsString returns the allowed IP as comma separated string\nfunc (u *User) GetAllowedIPAsString() string {\n\treturn strings.Join(u.Filters.AllowedIP, \",\")\n}\n\n// GetDeniedIPAsString returns the denied IP as comma separated string\nfunc (u *User) GetDeniedIPAsString() string {\n\treturn strings.Join(u.Filters.DeniedIP, \",\")\n}\n\n// HasExternalAuth returns true if the external authentication is globally enabled\n// and it is not disabled for this user\nfunc (u *User) HasExternalAuth() bool {\n\tif u.Filters.Hooks.ExternalAuthDisabled {\n\t\treturn false\n\t}\n\tif config.ExternalAuthHook != \"\" {\n\t\treturn true\n\t}\n\treturn plugin.Handler.HasAuthenticators()\n}\n\n// CountUnusedRecoveryCodes returns the number of unused recovery codes\nfunc (u *User) CountUnusedRecoveryCodes() int {\n\tunused := 0\n\tfor _, code := range u.Filters.RecoveryCodes {\n\t\tif !code.Used {\n\t\t\tunused++\n\t\t}\n\t}\n\treturn unused\n}\n\n// SetEmptySecretsIfNil sets the secrets to empty if nil\nfunc (u *User) SetEmptySecretsIfNil() {\n\tu.HasPassword = u.Password != \"\"\n\tu.FsConfig.SetEmptySecretsIfNil()\n\tfor idx := range u.VirtualFolders {\n\t\tvfolder := &u.VirtualFolders[idx]\n\t\tvfolder.FsConfig.SetEmptySecretsIfNil()\n\t}\n\tif u.Filters.TOTPConfig.Secret == nil {\n\t\tu.Filters.TOTPConfig.Secret = kms.NewEmptySecret()\n\t}\n}\n\nfunc (u *User) hasMainDataTransferLimits() bool {\n\treturn u.UploadDataTransfer > 0 || u.DownloadDataTransfer > 0 || u.TotalDataTransfer > 0\n}\n\n// HasPrimaryGroup returns true if the user has the specified primary group\nfunc (u *User) HasPrimaryGroup(name string) bool {\n\tfor _, g := range u.Groups {\n\t\tif g.Name == name {\n\t\t\treturn g.Type == sdk.GroupTypePrimary\n\t\t}\n\t}\n\treturn false\n}\n\n// HasSecondaryGroup returns true if the user has the specified secondary group\nfunc (u *User) HasSecondaryGroup(name string) bool {\n\tfor _, g := range u.Groups {\n\t\tif g.Name == name {\n\t\t\treturn g.Type == sdk.GroupTypeSecondary\n\t\t}\n\t}\n\treturn false\n}\n\n// HasMembershipGroup returns true if the user has the specified membership group\nfunc (u *User) HasMembershipGroup(name string) bool {\n\tfor _, g := range u.Groups {\n\t\tif g.Name == name {\n\t\t\treturn g.Type == sdk.GroupTypeMembership\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (u *User) hasSettingsFromGroups() bool {\n\tfor _, g := range u.Groups {\n\t\tif g.Type != sdk.GroupTypeMembership {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (u *User) applyGroupSettings(groupsMapping map[string]Group) {\n\tif !u.hasSettingsFromGroups() {\n\t\treturn\n\t}\n\tif u.groupSettingsApplied {\n\t\treturn\n\t}\n\treplacer := u.getGroupPlacehodersReplacer()\n\tfor _, g := range u.Groups {\n\t\tif g.Type == sdk.GroupTypePrimary {\n\t\t\tif group, ok := groupsMapping[g.Name]; ok {\n\t\t\t\tu.mergeWithPrimaryGroup(&group, replacer)\n\t\t\t} else {\n\t\t\t\tproviderLog(logger.LevelError, \"mapping not found for user %s, group %s\", u.Username, g.Name)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\tfor _, g := range u.Groups {\n\t\tif g.Type == sdk.GroupTypeSecondary {\n\t\t\tif group, ok := groupsMapping[g.Name]; ok {\n\t\t\t\tu.mergeAdditiveProperties(&group, sdk.GroupTypeSecondary, replacer)\n\t\t\t} else {\n\t\t\t\tproviderLog(logger.LevelError, \"mapping not found for user %s, group %s\", u.Username, g.Name)\n\t\t\t}\n\t\t}\n\t}\n\tu.removeDuplicatesAfterGroupMerge()\n}\n\n// LoadAndApplyGroupSettings update the user by loading and applying the group settings\nfunc (u *User) LoadAndApplyGroupSettings() error {\n\tif !u.hasSettingsFromGroups() {\n\t\treturn nil\n\t}\n\tif u.groupSettingsApplied {\n\t\treturn nil\n\t}\n\tnames := make([]string, 0, len(u.Groups))\n\tvar primaryGroupName string\n\tfor _, g := range u.Groups {\n\t\tif g.Type == sdk.GroupTypePrimary {\n\t\t\tprimaryGroupName = g.Name\n\t\t}\n\t\tif g.Type != sdk.GroupTypeMembership {\n\t\t\tnames = append(names, g.Name)\n\t\t}\n\t}\n\tgroups, err := provider.getGroupsWithNames(names)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get groups: %w\", err)\n\t}\n\treplacer := u.getGroupPlacehodersReplacer()\n\t// make sure to always merge with the primary group first\n\tfor idx := range groups {\n\t\tg := groups[idx]\n\t\tif g.Name == primaryGroupName {\n\t\t\tu.mergeWithPrimaryGroup(&g, replacer)\n\t\t\tlastIdx := len(groups) - 1\n\t\t\tgroups[idx] = groups[lastIdx]\n\t\t\tgroups = groups[:lastIdx]\n\t\t\tbreak\n\t\t}\n\t}\n\tfor idx := range groups {\n\t\tg := groups[idx]\n\t\tu.mergeAdditiveProperties(&g, sdk.GroupTypeSecondary, replacer)\n\t}\n\tu.removeDuplicatesAfterGroupMerge()\n\treturn nil\n}\n\nfunc (u *User) getGroupPlacehodersReplacer() *strings.Replacer {\n\treturn strings.NewReplacer(\"%username%\", u.Username, \"%role%\", u.Role)\n}\n\nfunc (u *User) replacePlaceholder(value string, replacer *strings.Replacer) string {\n\tif value == \"\" {\n\t\treturn value\n\t}\n\treturn replacer.Replace(value)\n}\n\nfunc (u *User) replaceFsConfigPlaceholders(fsConfig vfs.Filesystem, replacer *strings.Replacer) vfs.Filesystem {\n\tswitch fsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tfsConfig.S3Config.KeyPrefix = u.replacePlaceholder(fsConfig.S3Config.KeyPrefix, replacer)\n\tcase sdk.GCSFilesystemProvider:\n\t\tfsConfig.GCSConfig.KeyPrefix = u.replacePlaceholder(fsConfig.GCSConfig.KeyPrefix, replacer)\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tfsConfig.AzBlobConfig.KeyPrefix = u.replacePlaceholder(fsConfig.AzBlobConfig.KeyPrefix, replacer)\n\tcase sdk.SFTPFilesystemProvider:\n\t\tfsConfig.SFTPConfig.Username = u.replacePlaceholder(fsConfig.SFTPConfig.Username, replacer)\n\t\tfsConfig.SFTPConfig.Prefix = u.replacePlaceholder(fsConfig.SFTPConfig.Prefix, replacer)\n\tcase sdk.HTTPFilesystemProvider:\n\t\tfsConfig.HTTPConfig.Username = u.replacePlaceholder(fsConfig.HTTPConfig.Username, replacer)\n\t}\n\treturn fsConfig\n}\n\nfunc (u *User) mergeCryptFsConfig(group *Group) {\n\tif group.UserSettings.FsConfig.Provider == sdk.CryptedFilesystemProvider {\n\t\tif u.FsConfig.CryptConfig.ReadBufferSize == 0 {\n\t\t\tu.FsConfig.CryptConfig.ReadBufferSize = group.UserSettings.FsConfig.CryptConfig.ReadBufferSize\n\t\t}\n\t\tif u.FsConfig.CryptConfig.WriteBufferSize == 0 {\n\t\t\tu.FsConfig.CryptConfig.WriteBufferSize = group.UserSettings.FsConfig.CryptConfig.WriteBufferSize\n\t\t}\n\t}\n}\n\nfunc (u *User) mergeWithPrimaryGroup(group *Group, replacer *strings.Replacer) {\n\tif group.UserSettings.HomeDir != \"\" {\n\t\tu.HomeDir = filepath.Clean(u.replacePlaceholder(group.UserSettings.HomeDir, replacer))\n\t}\n\tif group.UserSettings.FsConfig.Provider != 0 {\n\t\tu.FsConfig = u.replaceFsConfigPlaceholders(group.UserSettings.FsConfig, replacer)\n\t\tu.mergeCryptFsConfig(group)\n\t} else {\n\t\tif u.FsConfig.OSConfig.ReadBufferSize == 0 {\n\t\t\tu.FsConfig.OSConfig.ReadBufferSize = group.UserSettings.FsConfig.OSConfig.ReadBufferSize\n\t\t}\n\t\tif u.FsConfig.OSConfig.WriteBufferSize == 0 {\n\t\t\tu.FsConfig.OSConfig.WriteBufferSize = group.UserSettings.FsConfig.OSConfig.WriteBufferSize\n\t\t}\n\t}\n\tif u.MaxSessions == 0 {\n\t\tu.MaxSessions = group.UserSettings.MaxSessions\n\t}\n\tif u.QuotaSize == 0 {\n\t\tu.QuotaSize = group.UserSettings.QuotaSize\n\t}\n\tif u.QuotaFiles == 0 {\n\t\tu.QuotaFiles = group.UserSettings.QuotaFiles\n\t}\n\tif u.UploadBandwidth == 0 {\n\t\tu.UploadBandwidth = group.UserSettings.UploadBandwidth\n\t}\n\tif u.DownloadBandwidth == 0 {\n\t\tu.DownloadBandwidth = group.UserSettings.DownloadBandwidth\n\t}\n\tif !u.hasMainDataTransferLimits() {\n\t\tu.UploadDataTransfer = group.UserSettings.UploadDataTransfer\n\t\tu.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer\n\t\tu.TotalDataTransfer = group.UserSettings.TotalDataTransfer\n\t}\n\tif u.ExpirationDate == 0 && group.UserSettings.ExpiresIn > 0 {\n\t\tu.ExpirationDate = u.CreatedAt + int64(group.UserSettings.ExpiresIn)*86400000\n\t}\n\tu.mergePrimaryGroupFilters(&group.UserSettings.Filters, replacer)\n\tu.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer)\n}\n\nfunc (u *User) mergePrimaryGroupFilters(filters *sdk.BaseUserFilters, replacer *strings.Replacer) { //nolint:gocyclo\n\tif u.Filters.MaxUploadFileSize == 0 {\n\t\tu.Filters.MaxUploadFileSize = filters.MaxUploadFileSize\n\t}\n\tif !u.IsTLSVerificationEnabled() {\n\t\tu.Filters.TLSUsername = filters.TLSUsername\n\t}\n\tif !u.Filters.Hooks.CheckPasswordDisabled {\n\t\tu.Filters.Hooks.CheckPasswordDisabled = filters.Hooks.CheckPasswordDisabled\n\t}\n\tif !u.Filters.Hooks.PreLoginDisabled {\n\t\tu.Filters.Hooks.PreLoginDisabled = filters.Hooks.PreLoginDisabled\n\t}\n\tif !u.Filters.Hooks.ExternalAuthDisabled {\n\t\tu.Filters.Hooks.ExternalAuthDisabled = filters.Hooks.ExternalAuthDisabled\n\t}\n\tif !u.Filters.DisableFsChecks {\n\t\tu.Filters.DisableFsChecks = filters.DisableFsChecks\n\t}\n\tif !u.Filters.AllowAPIKeyAuth {\n\t\tu.Filters.AllowAPIKeyAuth = filters.AllowAPIKeyAuth\n\t}\n\tif !u.Filters.IsAnonymous {\n\t\tu.Filters.IsAnonymous = filters.IsAnonymous\n\t}\n\tif u.Filters.ExternalAuthCacheTime == 0 {\n\t\tu.Filters.ExternalAuthCacheTime = filters.ExternalAuthCacheTime\n\t}\n\tif u.Filters.FTPSecurity == 0 {\n\t\tu.Filters.FTPSecurity = filters.FTPSecurity\n\t}\n\tif u.Filters.StartDirectory == \"\" {\n\t\tu.Filters.StartDirectory = u.replacePlaceholder(filters.StartDirectory, replacer)\n\t}\n\tif u.Filters.DefaultSharesExpiration == 0 {\n\t\tu.Filters.DefaultSharesExpiration = filters.DefaultSharesExpiration\n\t}\n\tif u.Filters.MaxSharesExpiration == 0 {\n\t\tu.Filters.MaxSharesExpiration = filters.MaxSharesExpiration\n\t}\n\tif u.Filters.PasswordExpiration == 0 {\n\t\tu.Filters.PasswordExpiration = filters.PasswordExpiration\n\t}\n\tif u.Filters.PasswordStrength == 0 {\n\t\tu.Filters.PasswordStrength = filters.PasswordStrength\n\t}\n}\n\nfunc (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *strings.Replacer) {\n\tu.mergeVirtualFolders(group, groupType, replacer)\n\tu.mergePermissions(group, groupType, replacer)\n\tu.mergeFilePatterns(group, groupType, replacer)\n\tu.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)\n\tu.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...)\n\tu.Filters.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...)\n\tu.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, group.UserSettings.Filters.DeniedLoginMethods...)\n\tu.Filters.DeniedProtocols = append(u.Filters.DeniedProtocols, group.UserSettings.Filters.DeniedProtocols...)\n\tu.Filters.WebClient = append(u.Filters.WebClient, group.UserSettings.Filters.WebClient...)\n\tu.Filters.TwoFactorAuthProtocols = append(u.Filters.TwoFactorAuthProtocols, group.UserSettings.Filters.TwoFactorAuthProtocols...)\n\tu.Filters.AccessTime = append(u.Filters.AccessTime, group.UserSettings.Filters.AccessTime...)\n}\n\nfunc (u *User) mergeVirtualFolders(group *Group, groupType int, replacer *strings.Replacer) {\n\tif len(group.VirtualFolders) > 0 {\n\t\tfolderPaths := make(map[string]bool)\n\t\tfor _, folder := range u.VirtualFolders {\n\t\t\tfolderPaths[folder.VirtualPath] = true\n\t\t}\n\t\tfor _, folder := range group.VirtualFolders {\n\t\t\tif folder.VirtualPath == \"/\" && groupType != sdk.GroupTypePrimary {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfolder.VirtualPath = u.replacePlaceholder(folder.VirtualPath, replacer)\n\t\t\tif _, ok := folderPaths[folder.VirtualPath]; !ok {\n\t\t\t\tfolder.MappedPath = u.replacePlaceholder(folder.MappedPath, replacer)\n\t\t\t\tfolder.FsConfig = u.replaceFsConfigPlaceholders(folder.FsConfig, replacer)\n\t\t\t\tu.VirtualFolders = append(u.VirtualFolders, folder)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (u *User) mergePermissions(group *Group, groupType int, replacer *strings.Replacer) {\n\tif u.Permissions == nil {\n\t\tu.Permissions = make(map[string][]string)\n\t}\n\tfor k, v := range group.UserSettings.Permissions {\n\t\tif k == \"/\" {\n\t\t\tif groupType == sdk.GroupTypePrimary {\n\t\t\t\tu.Permissions[k] = v\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tk = u.replacePlaceholder(k, replacer)\n\t\tif _, ok := u.Permissions[k]; !ok {\n\t\t\tu.Permissions[k] = v\n\t\t}\n\t}\n}\n\nfunc (u *User) mergeFilePatterns(group *Group, groupType int, replacer *strings.Replacer) {\n\tif len(group.UserSettings.Filters.FilePatterns) > 0 {\n\t\tpatternPaths := make(map[string]bool)\n\t\tfor _, pattern := range u.Filters.FilePatterns {\n\t\t\tpatternPaths[pattern.Path] = true\n\t\t}\n\t\tfor _, pattern := range group.UserSettings.Filters.FilePatterns {\n\t\t\tif pattern.Path == \"/\" && groupType != sdk.GroupTypePrimary {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpattern.Path = u.replacePlaceholder(pattern.Path, replacer)\n\t\t\tif _, ok := patternPaths[pattern.Path]; !ok {\n\t\t\t\tu.Filters.FilePatterns = append(u.Filters.FilePatterns, pattern)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (u *User) removeDuplicatesAfterGroupMerge() {\n\tu.Filters.AllowedIP = util.RemoveDuplicates(u.Filters.AllowedIP, false)\n\tu.Filters.DeniedIP = util.RemoveDuplicates(u.Filters.DeniedIP, false)\n\tu.Filters.DeniedLoginMethods = util.RemoveDuplicates(u.Filters.DeniedLoginMethods, false)\n\tu.Filters.DeniedProtocols = util.RemoveDuplicates(u.Filters.DeniedProtocols, false)\n\tu.Filters.WebClient = util.RemoveDuplicates(u.Filters.WebClient, false)\n\tu.Filters.TwoFactorAuthProtocols = util.RemoveDuplicates(u.Filters.TwoFactorAuthProtocols, false)\n\tu.SetEmptySecretsIfNil()\n\tu.groupSettingsApplied = true\n}\n\nfunc (u *User) hasRole(role string) bool {\n\tif role == \"\" {\n\t\treturn true\n\t}\n\treturn role == u.Role\n}\n\nfunc (u *User) applyNamingRules() {\n\tu.Username = config.convertName(u.Username)\n\tu.Role = config.convertName(u.Role)\n\tfor idx := range u.Groups {\n\t\tu.Groups[idx].Name = config.convertName(u.Groups[idx].Name)\n\t}\n\tfor idx := range u.VirtualFolders {\n\t\tu.VirtualFolders[idx].Name = config.convertName(u.VirtualFolders[idx].Name)\n\t}\n}\n\nfunc (u *User) getACopy() User {\n\tu.SetEmptySecretsIfNil()\n\tpubKeys := make([]string, len(u.PublicKeys))\n\tcopy(pubKeys, u.PublicKeys)\n\tvirtualFolders := make([]vfs.VirtualFolder, 0, len(u.VirtualFolders))\n\tfor idx := range u.VirtualFolders {\n\t\tvfolder := u.VirtualFolders[idx].GetACopy()\n\t\tvirtualFolders = append(virtualFolders, vfolder)\n\t}\n\tgroups := make([]sdk.GroupMapping, 0, len(u.Groups))\n\tfor _, g := range u.Groups {\n\t\tgroups = append(groups, sdk.GroupMapping{\n\t\t\tName: g.Name,\n\t\t\tType: g.Type,\n\t\t})\n\t}\n\tpermissions := make(map[string][]string)\n\tfor k, v := range u.Permissions {\n\t\tperms := make([]string, len(v))\n\t\tcopy(perms, v)\n\t\tpermissions[k] = perms\n\t}\n\tfilters := UserFilters{\n\t\tBaseUserFilters: copyBaseUserFilters(u.Filters.BaseUserFilters),\n\t}\n\tfilters.RequirePasswordChange = u.Filters.RequirePasswordChange\n\tfilters.TOTPConfig.Enabled = u.Filters.TOTPConfig.Enabled\n\tfilters.TOTPConfig.ConfigName = u.Filters.TOTPConfig.ConfigName\n\tfilters.TOTPConfig.Secret = u.Filters.TOTPConfig.Secret.Clone()\n\tfilters.TOTPConfig.Protocols = make([]string, len(u.Filters.TOTPConfig.Protocols))\n\tcopy(filters.TOTPConfig.Protocols, u.Filters.TOTPConfig.Protocols)\n\tfilters.AdditionalEmails = make([]string, len(u.Filters.AdditionalEmails))\n\tcopy(filters.AdditionalEmails, u.Filters.AdditionalEmails)\n\tfilters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes))\n\tfor _, code := range u.Filters.RecoveryCodes {\n\t\tif code.Secret == nil {\n\t\t\tcode.Secret = kms.NewEmptySecret()\n\t\t}\n\t\tfilters.RecoveryCodes = append(filters.RecoveryCodes, RecoveryCode{\n\t\t\tSecret: code.Secret.Clone(),\n\t\t\tUsed:   code.Used,\n\t\t})\n\t}\n\n\treturn User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tID:                       u.ID,\n\t\t\tUsername:                 u.Username,\n\t\t\tEmail:                    u.Email,\n\t\t\tPassword:                 u.Password,\n\t\t\tPublicKeys:               pubKeys,\n\t\t\tHasPassword:              u.HasPassword,\n\t\t\tHomeDir:                  u.HomeDir,\n\t\t\tUID:                      u.UID,\n\t\t\tGID:                      u.GID,\n\t\t\tMaxSessions:              u.MaxSessions,\n\t\t\tQuotaSize:                u.QuotaSize,\n\t\t\tQuotaFiles:               u.QuotaFiles,\n\t\t\tPermissions:              permissions,\n\t\t\tUsedQuotaSize:            u.UsedQuotaSize,\n\t\t\tUsedQuotaFiles:           u.UsedQuotaFiles,\n\t\t\tLastQuotaUpdate:          u.LastQuotaUpdate,\n\t\t\tUploadBandwidth:          u.UploadBandwidth,\n\t\t\tDownloadBandwidth:        u.DownloadBandwidth,\n\t\t\tUploadDataTransfer:       u.UploadDataTransfer,\n\t\t\tDownloadDataTransfer:     u.DownloadDataTransfer,\n\t\t\tTotalDataTransfer:        u.TotalDataTransfer,\n\t\t\tUsedUploadDataTransfer:   u.UsedUploadDataTransfer,\n\t\t\tUsedDownloadDataTransfer: u.UsedDownloadDataTransfer,\n\t\t\tStatus:                   u.Status,\n\t\t\tExpirationDate:           u.ExpirationDate,\n\t\t\tLastLogin:                u.LastLogin,\n\t\t\tFirstDownload:            u.FirstDownload,\n\t\t\tFirstUpload:              u.FirstUpload,\n\t\t\tLastPasswordChange:       u.LastPasswordChange,\n\t\t\tAdditionalInfo:           u.AdditionalInfo,\n\t\t\tDescription:              u.Description,\n\t\t\tCreatedAt:                u.CreatedAt,\n\t\t\tUpdatedAt:                u.UpdatedAt,\n\t\t\tRole:                     u.Role,\n\t\t},\n\t\tFilters:              filters,\n\t\tVirtualFolders:       virtualFolders,\n\t\tGroups:               groups,\n\t\tFsConfig:             u.FsConfig.GetACopy(),\n\t\tgroupSettingsApplied: u.groupSettingsApplied,\n\t}\n}\n\n// GetEncryptionAdditionalData returns the additional data to use for AEAD\nfunc (u *User) GetEncryptionAdditionalData() string {\n\treturn u.Username\n}\n"
  },
  {
    "path": "internal/ftpd/cryptfs_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage ftpd_test\n\nimport (\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/minio/sio\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpdtest\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n)\n\nfunc TestBasicFTPHandlingCryptFs(t *testing.T) {\n\tu := getTestUserWithCryptFs()\n\tu.QuotaSize = 6553600\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\tassert.Len(t, common.Connections.GetStats(\"\"), 1)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\tencryptedFileSize, err := getEncryptedFileSize(testFileSize)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize := encryptedFileSize\n\t\texpectedQuotaFiles := 1\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, path.Join(\"/missing_dir\", testFileName), testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\t// overwrite an existing file\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := os.Stat(localDownloadPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, info.Size())\n\t\t}\n\t\tlist, err := client.List(\".\")\n\t\tif assert.NoError(t, err) {\n\t\t\tif assert.Len(t, list, 1) {\n\t\t\t\tassert.Equal(t, testFileSize, int64(list[0].Size))\n\t\t\t}\n\t\t}\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\terr = client.Rename(testFileName, testFileName+\"1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Delete(testFileName)\n\t\tassert.Error(t, err)\n\t\terr = client.Delete(testFileName + \"1\")\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)\n\t\tcurDir, err := client.CurrentDir()\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"/\", curDir)\n\t\t}\n\t\ttestDir := \"testDir\"\n\t\terr = client.MakeDir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.ChangeDir(testDir)\n\t\tassert.NoError(t, err)\n\t\tcurDir, err = client.CurrentDir()\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, path.Join(\"/\", testDir), curDir)\n\t\t}\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tsize, err := client.FileSize(path.Join(\"/\", testDir, testFileName))\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, size)\n\t\terr = client.ChangeDirToParent()\n\t\tassert.NoError(t, err)\n\t\tcurDir, err = client.CurrentDir()\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"/\", curDir)\n\t\t}\n\t\terr = client.Delete(path.Join(\"/\", testDir, testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = client.Delete(testDir)\n\t\tassert.Error(t, err)\n\t\terr = client.RemoveDir(testDir)\n\t\tassert.NoError(t, err)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 }, 1*time.Second, 50*time.Millisecond)\n\tassert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,\n\t\t50*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestBufferedCryptFs(t *testing.T) {\n\tu := getTestUserWithCryptFs()\n\tu.FsConfig.CryptConfig.OSFsConfig = sdk.OSFsConfig{\n\t\tReadBufferSize:  1,\n\t\tWriteBufferSize: 1,\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\t// overwrite an existing file\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := os.Stat(localDownloadPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, info.Size())\n\t\t}\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 }, 1*time.Second, 50*time.Millisecond)\n\tassert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,\n\t\t50*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestZeroBytesTransfersCryptFs(t *testing.T) {\n\tu := getTestUserWithCryptFs()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFileName := \"testfilename\"\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, \"emptydownload\")\n\t\terr = os.WriteFile(localDownloadPath, []byte(\"\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(localDownloadPath, testFileName, 0, client, 0)\n\t\tassert.NoError(t, err)\n\t\tsize, err := client.FileSize(testFileName)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), size)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.NoFileExists(t, localDownloadPath)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, 0, client, 0)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := os.Stat(localDownloadPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, int64(0), info.Size())\n\t\t}\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestResumeCryptFs(t *testing.T) {\n\tu := getTestUserWithCryptFs()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\tdata := []byte(\"test data\")\n\t\terr = os.WriteFile(testFilePath, data, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0)\n\t\tassert.NoError(t, err)\n\t\t// resuming uploads is not supported\n\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)+5), client, 5)\n\t\tassert.Error(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(4), client, 5)\n\t\tassert.NoError(t, err)\n\t\treaded, err := os.ReadFile(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, data[5:], readed)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(8), client, 1)\n\t\tassert.NoError(t, err)\n\t\treaded, err = os.ReadFile(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, data[1:], readed)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(0), client, 9)\n\t\tassert.NoError(t, err)\n\t\terr = client.Delete(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0)\n\t\tassert.NoError(t, err)\n\t\t// now append to a file\n\t\tsrcFile, err := os.Open(testFilePath)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = client.Append(testFileName, srcFile)\n\t\t\tassert.Error(t, err)\n\t\t\terr = srcFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tsize, err := client.FileSize(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, int64(len(data)), size)\n\t\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(len(data)), client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\treaded, err = os.ReadFile(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, data, readed)\n\t\t}\n\t\t// now test a download resume using a bigger file\n\t\ttestFileSize := int64(655352)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tinitialHash, err := computeHashForFile(sha256.New(), testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tdownloadHash, err := computeHashForFile(sha256.New(), localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, initialHash, downloadHash)\n\t\terr = os.Truncate(localDownloadPath, 32767)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath+\"_partial\", testFileSize-32767, client, 32767) //nolint:goconst\n\t\tassert.NoError(t, err)\n\t\tfile, err := os.OpenFile(localDownloadPath, os.O_APPEND|os.O_WRONLY, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tfile1, err := os.Open(localDownloadPath + \"_partial\") //nolint:goconst\n\t\tassert.NoError(t, err)\n\t\t_, err = io.Copy(file, file1)\n\t\tassert.NoError(t, err)\n\t\terr = file.Close()\n\t\tassert.NoError(t, err)\n\t\terr = file1.Close()\n\t\tassert.NoError(t, err)\n\t\tdownloadHash, err = computeHashForFile(sha256.New(), localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, initialHash, downloadHash)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath + \"_partial\")\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc getTestUserWithCryptFs() dataprovider.User {\n\tuser := getTestUser()\n\tuser.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"testPassphrase\")\n\treturn user\n}\n\nfunc getEncryptedFileSize(size int64) (int64, error) {\n\tencSize, err := sio.EncryptedSize(uint64(size))\n\treturn int64(encSize) + 33, err\n}\n\nfunc computeHashForFile(hasher hash.Hash, path string) (string, error) {\n\thash := \"\"\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn hash, err\n\t}\n\tdefer f.Close()\n\t_, err = io.Copy(hasher, f)\n\tif err == nil {\n\t\thash = fmt.Sprintf(\"%x\", hasher.Sum(nil))\n\t}\n\treturn hash, err\n}\n"
  },
  {
    "path": "internal/ftpd/ftpd.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package ftpd implements the FTP protocol\npackage ftpd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\tlogSender = \"ftpd\"\n)\n\nvar (\n\tcertMgr       *common.CertManager\n\tserviceStatus ServiceStatus\n)\n\n// PassiveIPOverride defines an exception for the configured passive IP\ntype PassiveIPOverride struct {\n\tNetworks []string `json:\"networks\" mapstructure:\"networks\"`\n\t// if empty the local address will be returned\n\tIP             string `json:\"ip\" mapstructure:\"ip\"`\n\tparsedNetworks []func(net.IP) bool\n}\n\n// GetNetworksAsString returns the configured networks as string\nfunc (p *PassiveIPOverride) GetNetworksAsString() string {\n\treturn strings.Join(p.Networks, \", \")\n}\n\n// Binding defines the configuration for a network listener\ntype Binding struct {\n\t// The address to listen on. A blank value means listen on all available network interfaces.\n\tAddress string `json:\"address\" mapstructure:\"address\"`\n\t// The port used for serving requests\n\tPort int `json:\"port\" mapstructure:\"port\"`\n\t// Apply the proxy configuration, if any, for this binding\n\tApplyProxyConfig bool `json:\"apply_proxy_config\" mapstructure:\"apply_proxy_config\"`\n\t// Set to 1 to require TLS for both data and control connection.\n\t// Set to 2 to enable implicit TLS\n\tTLSMode int `json:\"tls_mode\" mapstructure:\"tls_mode\"`\n\t// Certificate and matching private key for this specific binding, if empty the global\n\t// ones will be used, if any\n\tCertificateFile    string `json:\"certificate_file\" mapstructure:\"certificate_file\"`\n\tCertificateKeyFile string `json:\"certificate_key_file\" mapstructure:\"certificate_key_file\"`\n\t// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2\n\tMinTLSVersion int `json:\"min_tls_version\" mapstructure:\"min_tls_version\"`\n\t// External IP address for passive connections.\n\tForcePassiveIP string `json:\"force_passive_ip\" mapstructure:\"force_passive_ip\"`\n\t// PassiveIPOverrides allows to define different IP addresses for passive connections\n\t// based on the client IP address\n\tPassiveIPOverrides []PassiveIPOverride `json:\"passive_ip_overrides\" mapstructure:\"passive_ip_overrides\"`\n\t// Hostname for passive connections. This hostname will be resolved each time a passive\n\t// connection is requested and this can, depending on the DNS configuration, take a noticeable\n\t// amount of time. Enable this setting only if you have a dynamic IP address\n\tPassiveHost string `json:\"passive_host\" mapstructure:\"passive_host\"`\n\t// Set to 1 to require client certificate authentication.\n\t// Set to 2 to require a client certificate and verfify it if given. In this mode\n\t// the client is allowed not to send a certificate.\n\t// You need to define at least a certificate authority for this to work\n\tClientAuthType int `json:\"client_auth_type\" mapstructure:\"client_auth_type\"`\n\t// TLSCipherSuites is a list of supported cipher suites for TLS version 1.2.\n\t// If CipherSuites is nil/empty, a default list of secure cipher suites\n\t// is used, with a preference order based on hardware performance.\n\t// Note that TLS 1.3 ciphersuites are not configurable.\n\t// The supported ciphersuites names are defined here:\n\t//\n\t// https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53\n\t//\n\t// any invalid name will be silently ignored.\n\t// The order matters, the ciphers listed first will be the preferred ones.\n\tTLSCipherSuites []string `json:\"tls_cipher_suites\" mapstructure:\"tls_cipher_suites\"`\n\t// PassiveConnectionsSecurity defines the security checks for passive data connections.\n\t// Supported values:\n\t// - 0 require matching peer IP addresses of control and data connection. This is the default\n\t// - 1 disable any checks\n\tPassiveConnectionsSecurity int `json:\"passive_connections_security\" mapstructure:\"passive_connections_security\"`\n\t// ActiveConnectionsSecurity defines the security checks for active data connections.\n\t// The supported values are the same as described for PassiveConnectionsSecurity.\n\t// Please note that disabling the security checks you will make the FTP service vulnerable to bounce attacks\n\t// on active data connections, so change the default value only if you are on a trusted/internal network\n\tActiveConnectionsSecurity int `json:\"active_connections_security\" mapstructure:\"active_connections_security\"`\n\t// Debug enables the FTP debug mode. In debug mode, every FTP command will be logged\n\tDebug   bool `json:\"debug\" mapstructure:\"debug\"`\n\tciphers []uint16\n}\n\nfunc (b *Binding) setCiphers() {\n\tb.ciphers = util.GetTLSCiphersFromNames(b.TLSCipherSuites)\n}\n\nfunc (b *Binding) isMutualTLSEnabled() bool {\n\treturn b.ClientAuthType == 1 || b.ClientAuthType == 2\n}\n\n// GetAddress returns the binding address\nfunc (b *Binding) GetAddress() string {\n\treturn fmt.Sprintf(\"%s:%d\", b.Address, b.Port)\n}\n\n// IsValid returns true if the binding port is > 0\nfunc (b *Binding) IsValid() bool {\n\treturn b.Port > 0\n}\n\nfunc (b *Binding) isTLSModeValid() bool {\n\treturn b.TLSMode >= 0 && b.TLSMode <= 2\n}\n\nfunc (b *Binding) checkSecuritySettings() error {\n\tif b.PassiveConnectionsSecurity < 0 || b.PassiveConnectionsSecurity > 1 {\n\t\treturn fmt.Errorf(\"invalid passive_connections_security: %v\", b.PassiveConnectionsSecurity)\n\t}\n\tif b.ActiveConnectionsSecurity < 0 || b.ActiveConnectionsSecurity > 1 {\n\t\treturn fmt.Errorf(\"invalid active_connections_security: %v\", b.ActiveConnectionsSecurity)\n\t}\n\treturn nil\n}\n\nfunc (b *Binding) checkPassiveIP() error {\n\tif b.ForcePassiveIP != \"\" {\n\t\tip, err := parsePassiveIP(b.ForcePassiveIP)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tb.ForcePassiveIP = ip\n\t}\n\tfor idx, passiveOverride := range b.PassiveIPOverrides {\n\t\tvar ip string\n\n\t\tif passiveOverride.IP != \"\" {\n\t\t\tvar err error\n\t\t\tip, err = parsePassiveIP(passiveOverride.IP)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif len(passiveOverride.Networks) == 0 {\n\t\t\treturn errors.New(\"passive IP networks override cannot be empty\")\n\t\t}\n\t\tcheckFuncs, err := util.ParseAllowedIPAndRanges(passiveOverride.Networks)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid passive IP networks override %+v: %w\", passiveOverride.Networks, err)\n\t\t}\n\t\tb.PassiveIPOverrides[idx].IP = ip\n\t\tb.PassiveIPOverrides[idx].parsedNetworks = checkFuncs\n\t}\n\treturn nil\n}\n\nfunc (b *Binding) getPassiveIP(cc ftpserver.ClientContext) (string, error) {\n\tif b.ForcePassiveIP != \"\" {\n\t\treturn b.ForcePassiveIP, nil\n\t}\n\tif b.PassiveHost != \"\" {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\n\t\taddrs, err := net.DefaultResolver.LookupIP(ctx, \"ip4\", b.PassiveHost)\n\t\tif err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to resolve hostname %q: %v\", b.PassiveHost, err)\n\t\t\treturn \"\", fmt.Errorf(\"unable to resolve hostname %q: %w\", b.PassiveHost, err)\n\t\t}\n\t\tif len(addrs) > 0 {\n\t\t\treturn addrs[0].String(), nil\n\t\t}\n\t}\n\treturn strings.Split(cc.LocalAddr().String(), \":\")[0], nil\n}\n\nfunc (b *Binding) passiveIPResolver(cc ftpserver.ClientContext) (string, error) {\n\tif len(b.PassiveIPOverrides) > 0 {\n\t\tclientIP := net.ParseIP(util.GetIPFromRemoteAddress(cc.RemoteAddr().String()))\n\t\tif clientIP != nil {\n\t\t\tfor _, override := range b.PassiveIPOverrides {\n\t\t\t\tfor _, fn := range override.parsedNetworks {\n\t\t\t\t\tif fn(clientIP) {\n\t\t\t\t\t\tif override.IP == \"\" {\n\t\t\t\t\t\t\treturn strings.Split(cc.LocalAddr().String(), \":\")[0], nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn override.IP, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn b.getPassiveIP(cc)\n}\n\n// HasProxy returns true if the proxy protocol is active for this binding\nfunc (b *Binding) HasProxy() bool {\n\treturn b.ApplyProxyConfig && common.Config.ProxyProtocol > 0\n}\n\n// GetTLSDescription returns the TLS mode as string\nfunc (b *Binding) GetTLSDescription() string {\n\tif certMgr == nil {\n\t\treturn util.I18nFTPTLSDisabled\n\t}\n\tswitch b.TLSMode {\n\tcase 1:\n\t\treturn util.I18nFTPTLSExplicit\n\tcase 2:\n\t\treturn util.I18nFTPTLSImplicit\n\t}\n\n\tif certMgr.HasCertificate(common.DefaultTLSKeyPaidID) || certMgr.HasCertificate(b.GetAddress()) {\n\t\treturn util.I18nFTPTLSMixed\n\t}\n\treturn util.I18nFTPTLSDisabled\n}\n\n// PortRange defines a port range\ntype PortRange struct {\n\t// Range start\n\tStart int `json:\"start\" mapstructure:\"start\"`\n\t// Range end\n\tEnd int `json:\"end\" mapstructure:\"end\"`\n}\n\n// ServiceStatus defines the service status\ntype ServiceStatus struct {\n\tIsActive         bool      `json:\"is_active\"`\n\tBindings         []Binding `json:\"bindings\"`\n\tPassivePortRange PortRange `json:\"passive_port_range\"`\n}\n\n// Configuration defines the configuration for the ftp server\ntype Configuration struct {\n\t// Addresses and ports to bind to\n\tBindings []Binding `json:\"bindings\" mapstructure:\"bindings\"`\n\t// The contents of the specified file, if any, are diplayed when someone connects to the server.\n\tBannerFile string `json:\"banner_file\" mapstructure:\"banner_file\"`\n\t// If files containing a certificate and matching private key for the server are provided the server will accept\n\t// both plain FTP an explicit FTP over TLS.\n\t// Certificate and key files can be reloaded on demand sending a \"SIGHUP\" signal on Unix based systems and a\n\t// \"paramchange\" request to the running service on Windows.\n\tCertificateFile    string `json:\"certificate_file\" mapstructure:\"certificate_file\"`\n\tCertificateKeyFile string `json:\"certificate_key_file\" mapstructure:\"certificate_key_file\"`\n\t// CACertificates defines the set of root certificate authorities to be used to verify client certificates.\n\tCACertificates []string `json:\"ca_certificates\" mapstructure:\"ca_certificates\"`\n\t// CARevocationLists defines a set a revocation lists, one for each root CA, to be used to check\n\t// if a client certificate has been revoked\n\tCARevocationLists []string `json:\"ca_revocation_lists\" mapstructure:\"ca_revocation_lists\"`\n\t// Do not impose the port 20 for active data transfer. Enabling this option allows to run SFTPGo with less privilege\n\tActiveTransfersPortNon20 bool `json:\"active_transfers_port_non_20\" mapstructure:\"active_transfers_port_non_20\"`\n\t// Set to true to disable active FTP\n\tDisableActiveMode bool `json:\"disable_active_mode\" mapstructure:\"disable_active_mode\"`\n\t// Set to true to enable the FTP SITE command.\n\t// We support chmod and symlink if SITE support is enabled\n\tEnableSite bool `json:\"enable_site\" mapstructure:\"enable_site\"`\n\t// Set to 1 to enable FTP commands that allow to calculate the hash value of files.\n\t// These FTP commands will be enabled: HASH, XCRC, MD5/XMD5, XSHA/XSHA1, XSHA256, XSHA512.\n\t// Please keep in mind that to calculate the hash we need to read the whole file, for\n\t// remote backends this means downloading the file, for the encrypted backend this means\n\t// decrypting the file\n\tHASHSupport int `json:\"hash_support\" mapstructure:\"hash_support\"`\n\t// Set to 1 to enable support for the non standard \"COMB\" FTP command.\n\t// Combine is only supported for local filesystem, for cloud backends it has\n\t// no advantage as it will download the partial files and will upload the\n\t// combined one. Cloud backends natively support multipart uploads.\n\tCombineSupport int `json:\"combine_support\" mapstructure:\"combine_support\"`\n\t// Port Range for data connections. Random if not specified\n\tPassivePortRange PortRange `json:\"passive_port_range\" mapstructure:\"passive_port_range\"`\n\tacmeDomain       string\n}\n\n// ShouldBind returns true if there is at least a valid binding\nfunc (c *Configuration) ShouldBind() bool {\n\tfor _, binding := range c.Bindings {\n\t\tif binding.IsValid() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *Configuration) getKeyPairs(configDir string) []common.TLSKeyPair {\n\tvar keyPairs []common.TLSKeyPair\n\n\tfor _, binding := range c.Bindings {\n\t\tcertificateFile := getConfigPath(binding.CertificateFile, configDir)\n\t\tcertificateKeyFile := getConfigPath(binding.CertificateKeyFile, configDir)\n\t\tif certificateFile != \"\" && certificateKeyFile != \"\" {\n\t\t\tkeyPairs = append(keyPairs, common.TLSKeyPair{\n\t\t\t\tCert: certificateFile,\n\t\t\t\tKey:  certificateKeyFile,\n\t\t\t\tID:   binding.GetAddress(),\n\t\t\t})\n\t\t}\n\t}\n\tvar certificateFile, certificateKeyFile string\n\tif c.acmeDomain != \"\" {\n\t\tcertificateFile, certificateKeyFile = util.GetACMECertificateKeyPair(c.acmeDomain)\n\t} else {\n\t\tcertificateFile = getConfigPath(c.CertificateFile, configDir)\n\t\tcertificateKeyFile = getConfigPath(c.CertificateKeyFile, configDir)\n\t}\n\tif certificateFile != \"\" && certificateKeyFile != \"\" {\n\t\tkeyPairs = append(keyPairs, common.TLSKeyPair{\n\t\t\tCert: certificateFile,\n\t\t\tKey:  certificateKeyFile,\n\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t})\n\t}\n\treturn keyPairs\n}\n\nfunc (c *Configuration) loadFromProvider() error {\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to load config from provider: %w\", err)\n\t}\n\tconfigs.SetNilsToEmpty()\n\tif configs.ACME.Domain == \"\" || !configs.ACME.HasProtocol(common.ProtocolFTP) {\n\t\treturn nil\n\t}\n\tcrt, key := util.GetACMECertificateKeyPair(configs.ACME.Domain)\n\tif crt != \"\" && key != \"\" {\n\t\tif _, err := os.Stat(crt); err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to load acme cert file %q: %v\", crt, err)\n\t\t\treturn nil\n\t\t}\n\t\tif _, err := os.Stat(key); err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to load acme key file %q: %v\", key, err)\n\t\t\treturn nil\n\t\t}\n\t\tc.acmeDomain = configs.ACME.Domain\n\t\tlogger.Info(logSender, \"\", \"acme domain set to %q\", c.acmeDomain)\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// Initialize configures and starts the FTP server\nfunc (c *Configuration) Initialize(configDir string) error {\n\tif err := c.loadFromProvider(); err != nil {\n\t\treturn err\n\t}\n\tlogger.Info(logSender, \"\", \"initializing FTP server with config %+v\", *c)\n\tif !c.ShouldBind() {\n\t\treturn common.ErrNoBinding\n\t}\n\n\tkeyPairs := c.getKeyPairs(configDir)\n\tif len(keyPairs) > 0 {\n\t\tmgr, err := common.NewCertManager(keyPairs, configDir, logSender)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmgr.SetCACertificates(c.CACertificates)\n\t\tif err := mgr.LoadRootCAs(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmgr.SetCARevocationLists(c.CARevocationLists)\n\t\tif err := mgr.LoadCRLs(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcertMgr = mgr\n\t}\n\tserviceStatus = ServiceStatus{\n\t\tBindings:         nil,\n\t\tPassivePortRange: c.PassivePortRange,\n\t}\n\n\texitChannel := make(chan error, 1)\n\n\tfor idx, binding := range c.Bindings {\n\t\tif !binding.IsValid() {\n\t\t\tcontinue\n\t\t}\n\n\t\tserver := NewServer(c, configDir, binding, idx)\n\n\t\tgo func(s *Server) {\n\t\t\tftpLogger := logger.NewSlogAdapter(\"ftpserverlib\", []slog.Attr{\n\t\t\t\t{\n\t\t\t\t\tKey:   \"server_id\",\n\t\t\t\t\tValue: slog.StringValue(fmt.Sprintf(\"FTP_%d\", s.ID)),\n\t\t\t\t},\n\t\t\t})\n\t\t\tftpServer := ftpserver.NewFtpServer(s)\n\t\t\tftpServer.Logger = slog.New(ftpLogger)\n\t\t\tlogger.Info(logSender, \"\", \"starting FTP serving, binding: %v\", s.binding.GetAddress())\n\t\t\tutil.CheckTCP4Port(s.binding.Port)\n\t\t\texitChannel <- ftpServer.ListenAndServe()\n\t\t}(server)\n\n\t\tserviceStatus.Bindings = append(serviceStatus.Bindings, binding)\n\t}\n\n\tserviceStatus.IsActive = true\n\n\treturn <-exitChannel\n}\n\n// ReloadCertificateMgr reloads the certificate manager\nfunc ReloadCertificateMgr() error {\n\tif certMgr != nil {\n\t\treturn certMgr.Reload()\n\t}\n\treturn nil\n}\n\n// GetStatus returns the server status\nfunc GetStatus() ServiceStatus {\n\treturn serviceStatus\n}\n\nfunc parsePassiveIP(passiveIP string) (string, error) {\n\tip := net.ParseIP(passiveIP)\n\tif ip == nil {\n\t\treturn \"\", fmt.Errorf(\"the provided passive IP %q is not valid\", passiveIP)\n\t}\n\tip = ip.To4()\n\tif ip == nil {\n\t\treturn \"\", fmt.Errorf(\"the provided passive IP %q is not a valid IPv4 address\", passiveIP)\n\t}\n\treturn ip.String(), nil\n}\n\nfunc getConfigPath(name, configDir string) string {\n\tif !util.IsFileInputValid(name) {\n\t\treturn \"\"\n\t}\n\tif name != \"\" && !filepath.IsAbs(name) {\n\t\treturn filepath.Join(configDir, name)\n\t}\n\treturn name\n}\n"
  },
  {
    "path": "internal/ftpd/ftpd_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage ftpd_test\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\t\"github.com/jlaffaye/ftp\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/pquerna/otp\"\n\t\"github.com/pquerna/otp/totp\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/sftpgo/sdk\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/ftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpdtest\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tlogSender               = \"ftpdTesting\"\n\tftpServerAddr           = \"127.0.0.1:2121\"\n\tsftpServerAddr          = \"127.0.0.1:2122\"\n\tftpSrvAddrTLS           = \"127.0.0.1:2124\" // ftp server with implicit tls\n\tftpSrvAddrTLSResumption = \"127.0.0.1:2126\" // ftp server with implicit tls\n\tdefaultUsername         = \"test_user_ftp\"\n\tdefaultPassword         = \"test_password\"\n\tosWindows               = \"windows\"\n\tftpsCert                = `-----BEGIN CERTIFICATE-----\nMIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw\nRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw\nOTUzMDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD\nVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVqWvrJ51t5OxV0v25NsOgR82CA\nNXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIVCzgWkxiz7XE4lgUwX44FCXZM\n3+JeUbKjUzBRMB0GA1UdDgQWBBRhLw+/o3+Z02MI/d4tmaMui9W16jAfBgNVHSME\nGDAWgBRhLw+/o3+Z02MI/d4tmaMui9W16jAPBgNVHRMBAf8EBTADAQH/MAoGCCqG\nSM49BAMCA2kAMGYCMQDqLt2lm8mE+tGgtjDmtFgdOcI72HSbRQ74D5rYTzgST1rY\n/8wTi5xl8TiFUyLMUsICMQC5ViVxdXbhuG7gX6yEqSkMKZICHpO8hqFwOD/uaFVI\ndV4vKmHUzwK/eIx+8Ay3neE=\n-----END CERTIFICATE-----`\n\tftpsKey = `-----BEGIN EC PARAMETERS-----\nBgUrgQQAIg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDCfMNsN6miEE3rVyUPwElfiJSWaR5huPCzUenZOfJT04GAcQdWvEju3\nUM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq\nWvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV\nCzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=\n-----END EC PRIVATE KEY-----`\n\tcaCRT = `-----BEGIN CERTIFICATE-----\nMIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0\nQXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT\nCENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m\nfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1\nv03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh\nyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL\nCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U\n4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb\nKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo\nNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLqR/BJTbbyXUB0imne1u00fuzb\nS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb78x7ivdyXSF5LVQJ1JvhhWu6i\nM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpBP8d2jcRZVUVrSXGc2mAGuGOY\n/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2ZxugCXULtRWJ9p4C9zUl40HEy\nOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD\nAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNoJhIvDZQrEf/VQbWuu\nXgNnt2m5MA0GCSqGSIb3DQEBCwUAA4ICAQCYhT5SRqk19hGrQ09hVSZOzynXAa5F\nsYkEWJzFyLg9azhnTPE1bFM18FScnkd+dal6mt+bQiJvdh24NaVkDghVB7GkmXki\npAiZwEDHMqtbhiPxY8LtSeCBAz5JqXVU2Q0TpAgNSH4W7FbGWNThhxcJVOoIrXKE\njbzhwl1Etcaf0DBKWliUbdlxQQs65DLy+rNBYtOeK0pzhzn1vpehUlJ4eTFzP9KX\ny2Mksuq9AspPbqnqpWW645MdTxMb5T57MCrY3GDKw63z5z3kz88LWJF3nOxZmgQy\nWFUhbLmZm7x6N5eiu6Wk8/B4yJ/n5UArD4cEP1i7nqu+mbbM/SZlq1wnGpg/sbRV\noUF+a7pRcSbfxEttle4pLFhS+ErKatjGcNEab2OlU3bX5UoBs+TYodnCWGKOuBKV\nL/CYc65QyeYZ+JiwYn9wC8YkzOnnVIQjiCEkLgSL30h9dxpnTZDLrdAA8ItelDn5\nDvjuQq58CGDsaVqpSobiSC1DMXYWot4Ets1wwovUNEq1l0MERB+2olE+JU/8E23E\neL1/aA7Kw/JibkWz1IyzClpFDKXf6kR2onJyxerdwUL+is7tqYFLysiHxZDL1bli\nSXbW8hMa5gvo0IilFP9Rznn8PplIfCsvBDVv6xsRr5nTAFtwKaMBVgznE2ghs69w\nkK8u1YiiVenmoQ==\n-----END CERTIFICATE-----`\n\tcaCRL = `-----BEGIN X509 CRL-----\nMIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN\nMjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg\nZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r\nrl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV\n+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199\nXSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE\njF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF\nDouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD\nMSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4\niYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE\nypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4\nhVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94\ncp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67\nsn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn\nw7bXJGfJcIMrrKs=\n-----END X509 CRL-----`\n\tclient1Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAJr32nHRlhyPiS7IfZ/ZWYowDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjM3WhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+kiJ3X6\nHUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi85xE\nOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLWeYl7\nQie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4ZdRf\nXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dybbhO\nc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRUh5Xo\nGzjh6iReaPSOgGatqOw9bDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEAyAK7cOTWqjyLgFM0kyyx1fNPvm2GwKep3MuU\nOrSnLuWjoxzb7WcbKNVMlnvnmSUAWuErxsY0PUJNfcuqWiGmEp4d/SWfWPigG6DC\nsDej35BlSfX8FCufYrfC74VNk4yBS2LVYmIqcpqUrfay0I2oZA8+ToLEpdUvEv2I\nl59eOhJO2jsC3JbOyZZmK2Kv7d94fR+1tg2Rq1Wbnmc9AZKq7KDReAlIJh4u2KHb\nBbtF79idusMwZyP777tqSQ4THBMa+VAEc2UrzdZqTIAwqlKQOvO2fRz2P+ARR+Tz\nMYJMdCdmPZ9qAc8U1OcFBG6qDDltO8wf/Nu/PsSI5LGCIhIuPPIuKfm0rRfTqCG7\nQPQPWjRoXtGGhwjdIuWbX9fIB+c+NpAEKHgLtV+Rxj8s5IVxqG9a5TtU9VkfVXJz\nJ20naoz/G+vDsVINpd3kH0ziNvdrKfGRM5UgtnUOPCXB22fVmkIsMH2knI10CKK+\noffI56NTkLRu00xvg98/wdukhkwIAxg6PQI/BHY5mdvoacEHHHdOhMq+GSAh7DDX\nG8+HdbABM1ExkPnZLat15q706ztiuUpQv1C2DI8YviUVkMqCslj4cD4F8EFPo4kr\nkvme0Cuc9Qlf7N5rjdV3cjwavhFx44dyXj9aesft2Q1okPiIqbGNpcjHcIRlj4Au\nMU3Bo0A=\n-----END CERTIFICATE-----`\n\tclient1Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+ki\nJ3X6HUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi\n85xEOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLW\neYl7Qie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4\nZdRfXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dy\nbbhOc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABAoIBAFjSHK7gENVZxphO\nhHg8k9ShnDo8eyDvK8l9Op3U3/yOsXKxolivvyx//7UFmz3vXDahjNHe7YScAXdw\neezbqBXa7xrvghqZzp2HhFYwMJ0210mcdncBKVFzK4ztZHxgQ0PFTqet0R19jZjl\nX3A325/eNZeuBeOied4qb/24AD6JGc6A0J55f5/QUQtdwYwrL15iC/KZXDL90PPJ\nCFJyrSzcXvOMEvOfXIFxhDVKRCppyIYXG7c80gtNC37I6rxxMNQ4mxjwUI2IVhxL\nj+nZDu0JgRZ4NaGjOq2e79QxUVm/GG3z25XgmBFBrXkEVV+sCZE1VDyj6kQfv9FU\nNhOrwGECgYEAzq47r/HwXifuGYBV/mvInFw3BNLrKry+iUZrJ4ms4g+LfOi0BAgf\nsXsWXulpBo2YgYjFdO8G66f69GlB4B7iLscpABXbRtpDZEnchQpaF36/+4g3i8gB\nZ29XHNDB8+7t4wbXvlSnLv1tZWey2fS4hPosc2YlvS87DMmnJMJqhs8CgYEA4oiB\nLGQP6VNdX0Uigmh5fL1g1k95eC8GP1ylczCcIwsb2OkAq0MT7SHRXOlg3leEq4+g\nmCHk1NdjkSYxDL2ZeTKTS/gy4p1jlcDa6Ilwi4pVvatNvu4o80EYWxRNNb1mAn67\nT8TN9lzc6mEi+LepQM3nYJ3F+ZWTKgxH8uoJwMUCgYEArpumE1vbjUBAuEyi2eGn\nRunlFW83fBCfDAxw5KM8anNlja5uvuU6GU/6s06QCxg+2lh5MPPrLdXpfukZ3UVa\nItjg+5B7gx1MSALaiY8YU7cibFdFThM3lHIM72wyH2ogkWcrh0GvSFSUQlJcWCSW\nasmMGiYXBgBL697FFZomMyMCgYEAkAnp0JcDQwHd4gDsk2zoqnckBsDb5J5J46n+\nDYNAFEww9bgZ08u/9MzG+cPu8xFE621U2MbcYLVfuuBE2ewIlPaij/COMmeO9Z59\n0tPpOuDH6eTtd1SptxqR6P+8pEn8feOlKHBj4Z1kXqdK/EiTlwAVeep4Al2oCFls\nujkz4F0CgYAe8vHnVFHlWi16zAqZx4ZZZhNuqPtgFkvPg9LfyNTA4dz7F9xgtUaY\nnXBPyCe/8NtgBfT79HkPiG3TM0xRZY9UZgsJKFtqAu5u4ManuWDnsZI9RK2QTLHe\nyEbH5r3Dg3n9k/3GbjXFIWdU9UaYsdnSKHHtMw9ZODc14LaAogEQug==\n-----END RSA PRIVATE KEY-----`\n\t// client 2 crt is revoked\n\tclient2Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAM5p4duNjikSPxYGaqBm5FEwDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjUyWhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImVjm/b\nQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TPkZua\neq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEKh4LQ\ncr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81PQAmT\nA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu4Ic0\n6tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBR5mf0f\nZjf8ZCGXqU2+45th7VkkLDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEARhFxNAouwbpEfN1M90+ao5rwyxEewerSoCCz\nPQzeUZ66MA/FkS/tFUGgGGG+wERN+WLbe1cN6q/XFr0FSMLuUxLXDNV02oUL/FnY\nxcyNLaZUZ0pP7sA+Hmx2AdTA6baIwQbyIY9RLAaz6hzo1YbI8yeis645F1bxgL2D\nEP5kXa3Obv0tqWByMZtrmJPv3p0W5GJKXVDn51GR/E5KI7pliZX2e0LmMX9mxfPB\n4sXFUggMHXxWMMSAmXPVsxC2KX6gMnajO7JUraTwuGm+6V371FzEX+UKXHI+xSvO\n78TseTIYsBGLjeiA8UjkKlD3T9qsQm2mb2PlKyqjvIm4i2ilM0E2w4JZmd45b925\n7q/QLV3NZ/zZMi6AMyULu28DWKfAx3RLKwnHWSFcR4lVkxQrbDhEUMhAhLAX+2+e\nqc7qZm3dTabi7ZJiiOvYK/yNgFHa/XtZp5uKPB5tigPIa+34hbZF7s2/ty5X3O1N\nf5Ardz7KNsxJjZIt6HvB28E/PPOvBqCKJc1Y08J9JbZi8p6QS1uarGoR7l7rT1Hv\n/ZXkNTw2bw1VpcWdzDBLLVHYNnJmS14189LVk11PcJJpSmubwCqg+ZZULdgtVr3S\nANas2dgMPVwXhnAalgkcc+lb2QqaEz06axfbRGBsgnyqR5/koKCg1Hr0+vThHSsR\nE0+r2+4=\n-----END CERTIFICATE-----`\n\tclient2Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImV\njm/bQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TP\nkZuaeq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEK\nh4LQcr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81P\nQAmTA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu\n4Ic06tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABAoIBAQCMnEeg9uXQmdvq\nop4qi6bV+ZcDWvvkLwvHikFMnYpIaheYBpF2ZMKzdmO4xgCSWeFCQ4Hah8KxfHCM\nqLuWvw2bBBE5J8yQ/JaPyeLbec7RX41GQ2YhPoxDdP0PdErREdpWo4imiFhH/Ewt\nRvq7ufRdpdLoS8dzzwnvX3r+H2MkHoC/QANW2AOuVoZK5qyCH5N8yEAAbWKaQaeL\nVBhAYEVKbAkWEtXw7bYXzxRR7WIM3f45v3ncRusDIG+Hf75ZjatoH0lF1gHQNofO\nqkCVZVzjkLFuzDic2KZqsNORglNs4J6t5Dahb9v3hnoK963YMnVSUjFvqQ+/RZZy\nVILFShilAoGBANucwZU61eJ0tLKBYEwmRY/K7Gu1MvvcYJIOoX8/BL3zNmNO0CLl\nNiABtNt9WOVwZxDsxJXdo1zvMtAegNqS6W11R1VAZbL6mQ/krScbLDE6JKA5DmA7\n4nNi1gJOW1ziAfdBAfhe4cLbQOb94xkOK5xM1YpO0xgDJLwrZbehDMmPAoGBAMAl\n/owPDAvcXz7JFynT0ieYVc64MSFiwGYJcsmxSAnbEgQ+TR5FtkHYe91OSqauZcCd\naoKXQNyrYKIhyounRPFTdYQrlx6KtEs7LU9wOxuphhpJtGjRnhmA7IqvX703wNvu\nkhrEavn86G5boH8R80371SrN0Rh9UeAlQGuNBdvfAoGAEAmokW9Ug08miwqrr6Pz\n3IZjMZJwALidTM1IufQuMnj6ddIhnQrEIx48yPKkdUz6GeBQkuk2rujA+zXfDxc/\neMDhzrX/N0zZtLFse7ieR5IJbrH7/MciyG5lVpHGVkgjAJ18uVikgAhm+vd7iC7i\nvG1YAtuyysQgAKXircBTIL0CgYAHeTLWVbt9NpwJwB6DhPaWjalAug9HIiUjktiB\nGcEYiQnBWn77X3DATOA8clAa/Yt9m2HKJIHkU1IV3ESZe+8Fh955PozJJlHu3yVb\nAp157PUHTriSnxyMF2Sb3EhX/rQkmbnbCqqygHC14iBy8MrKzLG00X6BelZV5n0D\n8d85dwKBgGWY2nsaemPH/TiTVF6kW1IKSQoIyJChkngc+Xj/2aCCkkmAEn8eqncl\nRKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA\nubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl\n-----END RSA PRIVATE KEY-----`\n\ttestFileName          = \"test_file_ftp.dat\"\n\ttestDLFileName        = \"test_download_ftp.dat\"\n\ttlsClient1Username    = \"client1\"\n\ttlsClient2Username    = \"client2\"\n\thttpFsPort            = 23456\n\tdefaultHTTPFsUsername = \"httpfs_ftp_user\"\n\temptyPwdPlaceholder   = \"empty\"\n)\n\nvar (\n\tconfigDir       = filepath.Join(\".\", \"..\", \"..\")\n\tallPerms        = []string{dataprovider.PermAny}\n\thomeBasePath    string\n\thookCmdPath     string\n\textAuthPath     string\n\tpreLoginPath    string\n\tpostConnectPath string\n\tpreDownloadPath string\n\tpreUploadPath   string\n\tlogFilePath     string\n\tcaCrtPath       string\n\tcaCRLPath       string\n)\n\nfunc TestMain(m *testing.M) { //nolint:gocyclo\n\tlogFilePath = filepath.Join(configDir, \"sftpgo_ftpd_test.log\")\n\tbannerFileName := \"banner_file\"\n\tbannerFile := filepath.Join(configDir, bannerFileName)\n\tlogger.InitLogger(logFilePath, 5, 1, 28, false, false, zerolog.DebugLevel)\n\terr := os.WriteFile(bannerFile, []byte(\"SFTPGo test ready\\nsimple banner line\\n\"), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error creating banner file: %v\", err)\n\t\tos.Exit(1)\n\t}\n\t// we run the test cases with UploadMode atomic and resume support. The non atomic code path\n\t// simply does not execute some code so if it works in atomic mode will\n\t// work in non atomic mode too\n\tos.Setenv(\"SFTPGO_COMMON__UPLOAD_MODE\", \"2\")\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN\", \"1\")\n\tos.Setenv(\"SFTPGO_COMMON__ALLOW_SELF_CONNECTIONS\", \"1\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_USERNAME\", \"admin\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_PASSWORD\", \"password\")\n\terr = config.LoadConfig(configDir, \"\")\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error loading configuration: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tproviderConf := config.GetProviderConf()\n\tlogger.InfoToConsole(\"Starting FTPD tests, provider: %v\", providerConf.Driver)\n\n\tcommonConf := config.GetCommonConfig()\n\thomeBasePath = os.TempDir()\n\tif runtime.GOOS != osWindows {\n\t\tcommonConf.Actions.ExecuteOn = []string{\"download\", \"upload\", \"rename\", \"delete\"}\n\t\tcommonConf.Actions.Hook = hookCmdPath\n\t\thookCmdPath, err = exec.LookPath(\"true\")\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to get hook command: %v\", err)\n\t\t\tlogger.WarnToConsole(\"unable to get hook command: %v\", err)\n\t\t}\n\t}\n\n\tcertPath := filepath.Join(os.TempDir(), \"test_ftpd.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test_ftpd.key\")\n\tcaCrtPath = filepath.Join(os.TempDir(), \"test_ftpd_ca.crt\")\n\tcaCRLPath = filepath.Join(os.TempDir(), \"test_ftpd_crl.crt\")\n\terr = writeCerts(certPath, keyPath, caCrtPath, caCRLPath)\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing data provider: %v\", err)\n\t\tos.Exit(1)\n\t}\n\terr = common.Initialize(commonConf, 0)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"error initializing common: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\thttpConfig := config.GetHTTPConfig()\n\thttpConfig.Initialize(configDir) //nolint:errcheck\n\n\tkmsConfig := config.GetKMSConfig()\n\terr = kmsConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing kms: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tmfaConfig := config.GetMFAConfig()\n\terr = mfaConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing MFA: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\thttpdConf := config.GetHTTPDConfig()\n\thttpdConf.Bindings[0].Port = 8079\n\thttpdtest.SetBaseURL(\"http://127.0.0.1:8079\")\n\n\tftpdConf := config.GetFTPDConfig()\n\tftpdConf.Bindings = []ftpd.Binding{\n\t\t{\n\t\t\tPort:               2121,\n\t\t\tClientAuthType:     2,\n\t\t\tCertificateFile:    certPath,\n\t\t\tCertificateKeyFile: keyPath,\n\t\t},\n\t}\n\tftpdConf.PassivePortRange.Start = 0\n\tftpdConf.PassivePortRange.End = 0\n\tftpdConf.BannerFile = bannerFileName\n\tftpdConf.CACertificates = []string{caCrtPath}\n\tftpdConf.CARevocationLists = []string{caCRLPath}\n\tftpdConf.EnableSite = true\n\n\t// required to test sftpfs\n\tsftpdConf := config.GetSFTPDConfig()\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort: 2122,\n\t\t},\n\t}\n\thostKeyPath := filepath.Join(os.TempDir(), \"id_ed25519\")\n\tsftpdConf.HostKeys = []string{hostKeyPath}\n\n\textAuthPath = filepath.Join(homeBasePath, \"extauth.sh\")\n\tpreLoginPath = filepath.Join(homeBasePath, \"prelogin.sh\")\n\tpostConnectPath = filepath.Join(homeBasePath, \"postconnect.sh\")\n\tpreDownloadPath = filepath.Join(homeBasePath, \"predownload.sh\")\n\tpreUploadPath = filepath.Join(homeBasePath, \"preupload.sh\")\n\n\tstatus := ftpd.GetStatus()\n\tif status.IsActive {\n\t\tlogger.ErrorToConsole(\"ftpd is already active\")\n\t\tos.Exit(1)\n\t}\n\n\tgo func() {\n\t\tlogger.Debug(logSender, \"\", \"initializing FTP server with config %+v\", ftpdConf)\n\t\tif err := ftpdConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start FTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tlogger.Debug(logSender, \"\", \"initializing SFTP server with config %+v\", sftpdConf)\n\t\tif err := sftpdConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SFTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tif err := httpdConf.Initialize(configDir, 0); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twaitTCPListening(ftpdConf.Bindings[0].GetAddress())\n\twaitTCPListening(httpdConf.Bindings[0].GetAddress())\n\twaitTCPListening(sftpdConf.Bindings[0].GetAddress())\n\tftpd.ReloadCertificateMgr() //nolint:errcheck\n\n\tftpdConf = config.GetFTPDConfig()\n\tftpdConf.Bindings = []ftpd.Binding{\n\t\t{\n\t\t\tPort:    2124,\n\t\t\tTLSMode: 2,\n\t\t},\n\t}\n\tftpdConf.CertificateFile = certPath\n\tftpdConf.CertificateKeyFile = keyPath\n\tftpdConf.CACertificates = []string{caCrtPath}\n\tftpdConf.CARevocationLists = []string{caCRLPath}\n\tftpdConf.EnableSite = false\n\tftpdConf.DisableActiveMode = true\n\tftpdConf.CombineSupport = 1\n\tftpdConf.HASHSupport = 1\n\n\tgo func() {\n\t\tlogger.Debug(logSender, \"\", \"initializing FTP server with config %+v\", ftpdConf)\n\t\tif err := ftpdConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start FTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twaitTCPListening(ftpdConf.Bindings[0].GetAddress())\n\n\tftpdConf = config.GetFTPDConfig()\n\tftpdConf.Bindings = []ftpd.Binding{\n\t\t{\n\t\t\tPort:               2126,\n\t\t\tCertificateFile:    certPath,\n\t\t\tCertificateKeyFile: keyPath,\n\t\t\tTLSMode:            1,\n\t\t\tClientAuthType:     2,\n\t\t},\n\t}\n\tftpdConf.CACertificates = []string{caCrtPath}\n\tftpdConf.CARevocationLists = []string{caCRLPath}\n\n\tgo func() {\n\t\tlogger.Debug(logSender, \"\", \"initializing FTP server with config %+v\", ftpdConf)\n\t\tif err := ftpdConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start FTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twaitTCPListening(ftpdConf.Bindings[0].GetAddress())\n\n\twaitNoConnections()\n\tstartHTTPFs()\n\n\texitCode := m.Run()\n\tos.Remove(logFilePath)\n\tos.Remove(bannerFile)\n\tos.Remove(extAuthPath)\n\tos.Remove(preLoginPath)\n\tos.Remove(postConnectPath)\n\tos.Remove(preDownloadPath)\n\tos.Remove(preUploadPath)\n\tos.Remove(certPath)\n\tos.Remove(keyPath)\n\tos.Remove(caCrtPath)\n\tos.Remove(caCRLPath)\n\tos.Remove(hostKeyPath)\n\tos.Remove(hostKeyPath + \".pub\")\n\tos.Exit(exitCode)\n}\n\nfunc TestInitializationFailure(t *testing.T) {\n\tftpdConf := config.GetFTPDConfig()\n\tftpdConf.Bindings = []ftpd.Binding{}\n\tftpdConf.CertificateFile = filepath.Join(os.TempDir(), \"test_ftpd.crt\")\n\tftpdConf.CertificateKeyFile = filepath.Join(os.TempDir(), \"test_ftpd.key\")\n\terr := ftpdConf.Initialize(configDir)\n\trequire.EqualError(t, err, common.ErrNoBinding.Error())\n\tftpdConf.Bindings = []ftpd.Binding{\n\t\t{\n\t\t\tPort: 0,\n\t\t},\n\t\t{\n\t\t\tPort: 2121,\n\t\t},\n\t}\n\tftpdConf.BannerFile = \"a-missing-file\"\n\terr = ftpdConf.Initialize(configDir)\n\trequire.Error(t, err)\n\n\tftpdConf.BannerFile = \"\"\n\tftpdConf.Bindings[1].TLSMode = 10\n\terr = ftpdConf.Initialize(configDir)\n\trequire.Error(t, err)\n\n\tftpdConf.CertificateFile = \"\"\n\tftpdConf.CertificateKeyFile = \"\"\n\tftpdConf.Bindings[1].TLSMode = 1\n\terr = ftpdConf.Initialize(configDir)\n\trequire.Error(t, err)\n\n\tcertPath := filepath.Join(os.TempDir(), \"test_ftpd.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test_ftpd.key\")\n\tftpdConf.CertificateFile = certPath\n\tftpdConf.CertificateKeyFile = keyPath\n\tftpdConf.CACertificates = []string{\"invalid ca cert\"}\n\terr = ftpdConf.Initialize(configDir)\n\trequire.Error(t, err)\n\n\tftpdConf.CACertificates = nil\n\tftpdConf.CARevocationLists = []string{\"\"}\n\terr = ftpdConf.Initialize(configDir)\n\trequire.Error(t, err)\n\n\tftpdConf.CACertificates = []string{caCrtPath}\n\tftpdConf.CARevocationLists = []string{caCRLPath}\n\tftpdConf.Bindings[1].ForcePassiveIP = \"127001\"\n\terr = ftpdConf.Initialize(configDir)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"the provided passive IP \\\"127001\\\" is not valid\")\n\tftpdConf.Bindings[1].ForcePassiveIP = \"\"\n\terr = ftpdConf.Initialize(configDir)\n\trequire.Error(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = ftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to load config from provider\")\n\t}\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicFTPHandling(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaSize = 6553600\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaSize = 6553600\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient, err := getFTPClient(user, true, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\tassert.Len(t, common.Connections.GetStats(\"\"), 1)\n\t\t\t} else {\n\t\t\t\tassert.Len(t, common.Connections.GetStats(\"\"), 2)\n\t\t\t}\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\texpectedQuotaSize := testFileSize\n\t\t\texpectedQuotaFiles := 1\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, path.Join(\"/missing_dir\", testFileName), testFileSize, client, 0)\n\t\t\tassert.Error(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, int64(0), user.FirstUpload)\n\t\t\tassert.Equal(t, int64(0), user.FirstDownload)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Greater(t, user.FirstUpload, int64(0))\n\t\t\tassert.Equal(t, int64(0), user.FirstDownload)\n\t\t\t// overwrite an existing file\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\tassert.Greater(t, user.FirstUpload, int64(0))\n\t\t\tassert.Greater(t, user.FirstDownload, int64(0))\n\t\t\terr = client.Rename(testFileName, testFileName+\"1\")\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Delete(testFileName)\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Delete(testFileName + \"1\")\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)\n\t\t\tcurDir, err := client.CurrentDir()\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, \"/\", curDir)\n\t\t\t}\n\t\t\ttestDir := \"testDir\"\n\t\t\terr = client.MakeDir(testDir)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.ChangeDir(testDir)\n\t\t\tassert.NoError(t, err)\n\t\t\tcurDir, err = client.CurrentDir()\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, path.Join(\"/\", testDir), curDir)\n\t\t\t}\n\t\t\tres, err := client.List(path.Join(\"/\", testDir))\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Len(t, res, 0)\n\t\t\tres, err = client.List(path.Join(\"/\"))\n\t\t\tassert.NoError(t, err)\n\t\t\tif assert.Len(t, res, 1) {\n\t\t\t\tassert.Equal(t, testDir, res[0].Name)\n\t\t\t}\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = client.FileSize(path.Join(\"/\", testDir))\n\t\t\tassert.Error(t, err)\n\t\t\tsize, err := client.FileSize(path.Join(\"/\", testDir, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testFileSize, size)\n\t\t\terr = client.ChangeDirToParent()\n\t\t\tassert.NoError(t, err)\n\t\t\tcurDir, err = client.CurrentDir()\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, \"/\", curDir)\n\t\t\t}\n\t\t\terr = client.Delete(path.Join(\"/\", testDir, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Delete(testDir)\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.RemoveDir(testDir)\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 }, 1*time.Second, 50*time.Millisecond)\n\tassert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,\n\t\t50*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestHTTPFs(t *testing.T) {\n\tu := getTestUserWithHTTPFs()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\t// test a download resume\n\t\tdata := []byte(\"test data\")\n\t\terr = os.WriteFile(testFilePath, data, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(len(data)-5), client, 5)\n\t\tassert.NoError(t, err)\n\t\treaded, err := os.ReadFile(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"data\"), readed, \"readed data mismatch: %q\", string(readed))\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 }, 1*time.Second, 50*time.Millisecond)\n\tassert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,\n\t\t50*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestListDirWithWildcards(t *testing.T) {\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tdefer func() {\n\t\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(localUser.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t}()\n\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient, err := getFTPClient(user, true, nil, ftp.DialWithDisabledMLSD(true))\n\t\tif assert.NoError(t, err) {\n\t\t\tdir1 := \"test.dir\"\n\t\t\tdir2 := \"test.dir1\"\n\t\t\terr = client.MakeDir(dir1)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.MakeDir(dir2)\n\t\t\tassert.NoError(t, err)\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\tfileName := \"file[a-z]e.dat\"\n\t\t\terr = ftpUploadFile(testFilePath, fileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\t\terr = ftpDownloadFile(fileName, localDownloadPath, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tentries, err := client.List(fileName)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Equal(t, fileName, entries[0].Name)\n\t\t\tnListEntries, err := client.NameList(fileName)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Contains(t, nListEntries, fileName)\n\t\t\tentries, err = client.List(\".\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 3)\n\t\t\tnListEntries, err = client.NameList(\".\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, nListEntries, 3)\n\t\t\tentries, err = client.List(\"/test.*\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 2)\n\t\t\tfound := 0\n\t\t\tfor _, e := range entries {\n\t\t\t\tswitch e.Name {\n\t\t\t\tcase dir1, dir2:\n\t\t\t\t\tfound++\n\t\t\t\t}\n\t\t\t}\n\t\t\tassert.Equal(t, 2, found)\n\t\t\tnListEntries, err = client.NameList(\"/test.*\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 2)\n\t\t\tassert.Contains(t, nListEntries, dir1)\n\t\t\tassert.Contains(t, nListEntries, dir2)\n\t\t\tentries, err = client.List(\"/*.dir?\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, entries, 1)\n\t\t\tassert.Equal(t, dir2, entries[0].Name)\n\t\t\tnListEntries, err = client.NameList(\"/*.dir?\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Contains(t, nListEntries, dir2)\n\t\t\tentries, err = client.List(\"/test.???\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Equal(t, dir1, entries[0].Name)\n\t\t\tnListEntries, err = client.NameList(\"/test.???\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Contains(t, nListEntries, dir1)\n\t\t\t_, err = client.NameList(\"/missingdir/test.*\")\n\t\t\tassert.Error(t, err)\n\t\t\t_, err = client.List(\"/missingdir/test.*\")\n\t\t\tassert.Error(t, err)\n\t\t\t_, err = client.NameList(\"test[-]\")\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), path.ErrBadPattern.Error())\n\t\t\t}\n\t\t\t_, err = client.List(\"test[-]\")\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), path.ErrBadPattern.Error())\n\t\t\t}\n\t\t\tsubDir := path.Join(dir1, \"sub.d\")\n\t\t\terr = client.MakeDir(subDir)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.ChangeDir(path.Dir(subDir))\n\t\t\tassert.NoError(t, err)\n\t\t\tentries, err = client.List(\"sub.?\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Contains(t, path.Base(subDir), entries[0].Name)\n\t\t\tnListEntries, err = client.NameList(\"sub.?\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Contains(t, nListEntries, path.Base(subDir))\n\t\t\tentries, err = client.List(\"../*.dir?\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Equal(t, path.Join(\"../\", dir2), entries[0].Name)\n\t\t\tnListEntries, err = client.NameList(\"../*.dir?\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Contains(t, nListEntries, path.Join(\"../\", dir2))\n\n\t\t\terr = client.ChangeDir(\"/\")\n\t\t\tassert.NoError(t, err)\n\t\t\tentries, err = client.List(path.Join(dir1, \"sub.*\"))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Equal(t, path.Join(dir1, \"sub.d\"), entries[0].Name)\n\t\t\tnListEntries, err = client.NameList(path.Join(dir1, \"sub.*\"))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1)\n\t\t\tassert.Contains(t, nListEntries, path.Join(dir1, \"sub.d\"))\n\t\t\terr = client.RemoveDir(subDir)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.RemoveDir(dir1)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.RemoveDir(dir2)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n}\n\nfunc TestStartDirectory(t *testing.T) {\n\tstartDir := \"/start/dir\"\n\tu := getTestUser()\n\tu.Filters.StartDirectory = startDir\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.Filters.StartDirectory = startDir\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient, err := getFTPClient(user, true, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\tcurrentDir, err := client.CurrentDir()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, startDir, currentDir)\n\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tentries, err := client.List(\".\")\n\t\t\tassert.NoError(t, err)\n\t\t\tif assert.Len(t, entries, 1) {\n\t\t\t\tassert.Equal(t, testFileName, entries[0].Name)\n\t\t\t}\n\t\t\tentries, err = client.List(\"/\")\n\t\t\tassert.NoError(t, err)\n\t\t\tif assert.Len(t, entries, 1) {\n\t\t\t\tassert.Equal(t, \"start\", entries[0].Name)\n\t\t\t}\n\t\t\terr = client.ChangeDirToParent()\n\t\t\tassert.NoError(t, err)\n\t\t\tcurrentDir, err = client.CurrentDir()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, path.Dir(startDir), currentDir)\n\t\t\terr = client.ChangeDirToParent()\n\t\t\tassert.NoError(t, err)\n\t\t\tcurrentDir, err = client.CurrentDir()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, \"/\", currentDir)\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginEmptyPassword(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Password = emptyPwdPlaceholder\n\n\t_, err = getFTPClient(user, true, nil)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestAnonymousUser(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tu.Filters.IsAnonymous = true\n\t_, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.Error(t, err)\n\tuser, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.IsAnonymous)\n\tassert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions[\"/\"])\n\tassert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)\n\tassert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,\n\t\tdataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)\n\n\tuser.Password = emptyPwdPlaceholder\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission\")\n\t\t}\n\t\terr = os.Rename(testFilePath, filepath.Join(user.GetHomeDir(), testFileName))\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = client.MakeDir(\"adir\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission\")\n\t\t}\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestAnonymousGroupInheritance(t *testing.T) {\n\tg := getTestGroup()\n\tg.UserSettings.Filters.IsAnonymous = true\n\tg.UserSettings.Permissions = make(map[string][]string)\n\tg.UserSettings.Permissions[\"/\"] = allPerms\n\tg.UserSettings.Permissions[\"/testsub\"] = allPerms\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser.Password = emptyPwdPlaceholder\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission\")\n\t\t}\n\t\terr = client.MakeDir(\"adir\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission\")\n\t\t}\n\t\terr = client.MakeDir(\"/testsub/adir\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission\")\n\t\t}\n\t\terr = os.Rename(testFilePath, filepath.Join(user.GetHomeDir(), testFileName))\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\tuser.Password = defaultPassword\n\tclient, err = getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestMultiFactorAuth(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolFTP},\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tuser.Password = defaultPassword\n\t_, err = getFTPClient(user, true, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), dataprovider.ErrInvalidCredentials.Error())\n\t}\n\tpasscode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword + passcode\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t// reusing the same passcode should not work\n\t_, err = getFTPClient(user, true, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), dataprovider.ErrInvalidCredentials.Error())\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMustChangePasswordRequirement(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.RequirePasswordChange = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(user, true, nil)\n\tassert.Error(t, err)\n\n\terr = dataprovider.UpdateUserPassword(user.Username, defaultPassword, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSecondFactorRequirement(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.TwoFactorAuthProtocols = []string{common.ProtocolFTP}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(user, true, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"second factor authentication is not set\")\n\t}\n\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolFTP},\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tpasscode, err := generateTOTPPasscode(key.Secret(), otp.AlgorithmSHA1)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword + passcode\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginInvalidCredentials(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Username = \"wrong username\"\n\t_, err = getFTPClient(user, false, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), dataprovider.ErrInvalidCredentials.Error())\n\t}\n\tuser.Username = u.Username\n\tuser.Password = \"wrong pwd\"\n\t_, err = getFTPClient(user, false, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), dataprovider.ErrInvalidCredentials.Error())\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginNonExistentUser(t *testing.T) {\n\tuser := getTestUser()\n\t_, err := getFTPClient(user, false, nil)\n\tassert.Error(t, err)\n}\n\nfunc TestFTPSecurity(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.FTPSecurity = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = getFTPClient(user, false, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"TLS is required\")\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestGroupFTPSecurity(t *testing.T) {\n\tg := getTestGroup()\n\tg.UserSettings.Filters.FTPSecurity = 1\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = getFTPClient(user, false, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"TLS is required\")\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginExternalAuth(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tg := getTestGroup()\n\tg.UserSettings.Filters.DeniedProtocols = []string{common.ProtocolFTP}\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient, err := getFTPClient(u, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm)\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(u, true, nil)\n\tif !assert.Error(t, err) {\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t} else {\n\t\tassert.Contains(t, err.Error(), \"protocol FTP is not allowed\")\n\t}\n\n\tu.Groups = nil\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm)\n\tassert.NoError(t, err)\n\tu.Username = defaultUsername + \"1\"\n\tclient, err = getFTPClient(u, true, nil)\n\tif !assert.Error(t, err) {\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t} else {\n\t\tassert.Contains(t, err.Error(), \"invalid credentials\")\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, defaultUsername, user.Username)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreLoginHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(u, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t// test login with an existing user\n\tclient, err = getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(u, false, nil)\n\tif !assert.Error(t, err) {\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\tuser.Status = 0\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(u, false, nil)\n\tif !assert.Error(t, err, \"pre-login script returned a disabled user, login must fail\") {\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\tuser.Status = 0\n\tuser.Filters.FTPSecurity = 1\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(u, true, nil)\n\tif !assert.Error(t, err) {\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = getFTPClient(user, false, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"TLS is required\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreLoginHookReturningAnonymousUser(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\tu.Filters.IsAnonymous = true\n\tu.Filters.DeniedProtocols = []string{common.ProtocolSSH}\n\tu.Password = \"\"\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t// the pre-login hook create the anonymous user\n\tclient, err := getFTPClient(u, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\n\t\terr = client.MakeDir(\"tdiranonymous\")\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission\")\n\t\t}\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission\")\n\t\t}\n\t\terr = os.Rename(testFilePath, filepath.Join(u.GetHomeDir(), testFileName))\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.IsAnonymous)\n\tassert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions[\"/\"])\n\tassert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)\n\tassert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,\n\t\tdataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)\n\t// now the same with an existing user\n\tclient, err = getFTPClient(u, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission\")\n\t\t}\n\t\terr = os.Rename(testFilePath, filepath.Join(u.GetHomeDir(), testFileName))\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreDownloadHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldExecuteOn := common.Config.Actions.ExecuteOn\n\toldHook := common.Config.Actions.Hook\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}\n\tcommon.Config.Actions.Hook = preDownloadPath\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preDownloadPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t// now return an error from the pre-download hook\n\terr = os.WriteFile(preDownloadPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"permission denied\")\n\t\t}\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.ExecuteOn = oldExecuteOn\n\tcommon.Config.Actions.Hook = oldHook\n}\n\nfunc TestPreUploadHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldExecuteOn := common.Config.Actions.ExecuteOn\n\toldHook := common.Config.Actions.Hook\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreUpload}\n\tcommon.Config.Actions.Hook = preUploadPath\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preUploadPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t// now return an error from the pre-upload hook\n\terr = os.WriteFile(preUploadPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), ftpserver.ErrFileNameNotAllowed.Error())\n\t\t}\n\t\terr = ftpUploadFile(testFilePath, testFileName+\"1\", testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), ftpserver.ErrFileNameNotAllowed.Error())\n\t\t}\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.ExecuteOn = oldExecuteOn\n\tcommon.Config.Actions.Hook = oldHook\n}\n\nfunc TestPostConnectHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tcommon.Config.PostConnectHook = postConnectPath\n\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\terr = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, true, nil)\n\tif !assert.Error(t, err) {\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tcommon.Config.PostConnectHook = \"http://127.0.0.1:8079/healthz\"\n\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tcommon.Config.PostConnectHook = \"http://127.0.0.1:8079/notfound\"\n\n\tclient, err = getFTPClient(user, true, nil)\n\tif !assert.Error(t, err) {\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.PostConnectHook = \"\"\n}\n\n//nolint:dupl\nfunc TestMaxConnections(t *testing.T) {\n\toldValue := common.Config.MaxTotalConnections\n\tcommon.Config.MaxTotalConnections = 1\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tuser := getTestUser()\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser.Password = \"\"\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\t_, err = getFTPClient(user, false, nil)\n\t\tassert.Error(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.MaxTotalConnections = oldValue\n}\n\n//nolint:dupl\nfunc TestMaxPerHostConnections(t *testing.T) {\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 1\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tuser := getTestUser()\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser.Password = \"\"\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\t_, err = getFTPClient(user, false, nil)\n\t\tassert.Error(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\nfunc TestMaxTransfers(t *testing.T) {\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 2\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tuser := getTestUser()\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser.Password = \"\"\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tconn, sftpClient, err := getSftpClient(user)\n\tassert.NoError(t, err)\n\tdefer conn.Close()\n\tdefer sftpClient.Close()\n\n\tf1, err := sftpClient.Create(\"file1\")\n\tassert.NoError(t, err)\n\tf2, err := sftpClient.Create(\"file2\")\n\tassert.NoError(t, err)\n\t_, err = f1.Write([]byte(\" \"))\n\tassert.NoError(t, err)\n\t_, err = f2.Write([]byte(\" \"))\n\tassert.NoError(t, err)\n\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\n\terr = f1.Close()\n\tassert.NoError(t, err)\n\terr = f2.Close()\n\tassert.NoError(t, err)\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\nfunc TestRateLimiter(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.DefenderConfig.Enabled = true\n\tcfg.DefenderConfig.Threshold = 5\n\tcfg.DefenderConfig.ScoreLimitExceeded = 3\n\tcfg.RateLimitersConfig = []common.RateLimiterConfig{\n\t\t{\n\t\t\tAverage:                1,\n\t\t\tPeriod:                 1000,\n\t\t\tBurst:                  1,\n\t\t\tType:                   2,\n\t\t\tProtocols:              []string{common.ProtocolFTP},\n\t\t\tGenerateDefenderEvents: true,\n\t\t\tEntriesSoftLimit:       100,\n\t\t\tEntriesHardLimit:       150,\n\t\t},\n\t}\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient, err := getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = getFTPClient(user, true, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"rate limit exceed\")\n\t}\n\n\t_, err = getFTPClient(user, false, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"rate limit exceed\")\n\t}\n\n\t_, err = getFTPClient(user, true, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"banned client IP\")\n\t}\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestDefender(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.DefenderConfig.Enabled = true\n\tcfg.DefenderConfig.Threshold = 4\n\tcfg.DefenderConfig.ScoreLimitExceeded = 2\n\tcfg.DefenderConfig.ScoreNoAuth = 1\n\tcfg.DefenderConfig.ScoreValid = 1\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t// just dial without login\n\tftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}\n\tclient, err = ftp.Dial(ftpServerAddr, ftpOptions...)\n\tassert.NoError(t, err)\n\terr = client.Quit()\n\tassert.NoError(t, err)\n\thosts, _, err := httpdtest.GetDefenderHosts(http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\thost := hosts[0]\n\t\tassert.Empty(t, host.GetBanTime())\n\t\tassert.Equal(t, 1, host.Score)\n\t}\n\tuser.Password = \"wrong_pwd\"\n\t_, err = getFTPClient(user, false, nil)\n\tassert.Error(t, err)\n\thosts, _, err = httpdtest.GetDefenderHosts(http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\thost := hosts[0]\n\t\tassert.Empty(t, host.GetBanTime())\n\t\tassert.Equal(t, 2, host.Score)\n\t}\n\n\tfor i := 0; i < 2; i++ {\n\t\t_, err = getFTPClient(user, false, nil)\n\t\tassert.Error(t, err)\n\t}\n\n\tuser.Password = defaultPassword\n\t_, err = getFTPClient(user, false, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"banned client IP\")\n\t}\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestMaxSessions(t *testing.T) {\n\tu := getTestUser()\n\tu.MaxSessions = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\t_, err = getFTPClient(user, false, nil)\n\t\tassert.Error(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestZeroBytesTransfers(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, useTLS := range []bool{true, false} {\n\t\tclient, err := getFTPClient(user, useTLS, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestFileName := \"testfilename\"\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\t\t\tlocalDownloadPath := filepath.Join(homeBasePath, \"empty_download\")\n\t\t\terr = os.WriteFile(localDownloadPath, []byte(\"\"), os.ModePerm)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(localDownloadPath, testFileName, 0, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tsize, err := client.FileSize(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, int64(0), size)\n\t\t\terr = os.Remove(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoFileExists(t, localDownloadPath)\n\t\t\terr = ftpDownloadFile(testFileName, localDownloadPath, 0, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.FileExists(t, localDownloadPath)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDownloadErrors(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 1\n\tsubDir1 := \"sub1\"\n\tsubDir2 := \"sub2\"\n\tu.Permissions[path.Join(\"/\", subDir1)] = []string{dataprovider.PermListItems}\n\tu.Permissions[path.Join(\"/\", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload,\n\t\tdataprovider.PermDelete, dataprovider.PermDownload}\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/sub2\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{\"*.jpg\", \"*.zip\"},\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath1 := filepath.Join(user.HomeDir, subDir1, \"file.zip\")\n\t\ttestFilePath2 := filepath.Join(user.HomeDir, subDir2, \"file.zip\")\n\t\ttestFilePath3 := filepath.Join(user.HomeDir, subDir2, \"file.jpg\")\n\t\terr = os.MkdirAll(filepath.Dir(testFilePath1), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.MkdirAll(filepath.Dir(testFilePath2), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(testFilePath1, []byte(\"file1\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(testFilePath2, []byte(\"file2\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(testFilePath3, []byte(\"file3\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(path.Join(\"/\", subDir1, \"file.zip\"), localDownloadPath, 5, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = ftpDownloadFile(path.Join(\"/\", subDir2, \"file.zip\"), localDownloadPath, 5, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = ftpDownloadFile(path.Join(\"/\", subDir2, \"file.jpg\"), localDownloadPath, 5, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = ftpDownloadFile(\"/missing.zip\", localDownloadPath, 5, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadErrors(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaSize = 65535\n\tsubDir1 := \"sub1\"\n\tsubDir2 := \"sub2\"\n\tu.Permissions[path.Join(\"/\", subDir1)] = []string{dataprovider.PermListItems}\n\tu.Permissions[path.Join(\"/\", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload,\n\t\tdataprovider.PermDelete}\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/sub2\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{\"*.zip\"},\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := user.QuotaSize\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = client.MakeDir(subDir1)\n\t\tassert.NoError(t, err)\n\t\terr = client.MakeDir(subDir2)\n\t\tassert.NoError(t, err)\n\t\terr = client.ChangeDir(subDir1)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = client.ChangeDirToParent()\n\t\tassert.NoError(t, err)\n\t\terr = client.ChangeDir(subDir2)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName+\".zip\", testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = client.ChangeDir(\"/\")\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, subDir1, testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\t// overquota\n\t\terr = ftpUploadFile(testFilePath, testFileName+\"1\", testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = client.Delete(path.Join(\"/\", subDir2, testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPBuffered(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaFiles = 100\n\tu.FsConfig.SFTPConfig.BufferSize = 2\n\tu.HomeDir = filepath.Join(os.TempDir(), u.Username)\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(sftpUser, true, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\texpectedQuotaSize := testFileSize\n\t\texpectedQuotaFiles := 1\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\t// overwrite an existing file\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err := httpdtest.GetUserByUsername(sftpUser.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\n\t\tdata := []byte(\"test data\")\n\t\terr = os.WriteFile(testFilePath, data, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)+5), client, 5)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"operation unsupported\")\n\t\t}\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(4), client, 5)\n\t\tassert.NoError(t, err)\n\t\treaded, err := os.ReadFile(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"data\"), readed)\n\t\t// try to append to a file, it should fail\n\t\t// now append to a file\n\t\tsrcFile, err := os.Open(testFilePath)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = client.Append(testFileName, srcFile)\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), \"operation unsupported\")\n\t\t\t}\n\t\t\terr = srcFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tsize, err := client.FileSize(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, int64(len(data)), size)\n\t\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(len(data)), client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(sftpUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestResume(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestUser()\n\tu.FsConfig.OSConfig = sdk.OSFsConfig{\n\t\tReadBufferSize:  1,\n\t\tWriteBufferSize: 1,\n\t}\n\tu.Username += \"_buf\"\n\tu.HomeDir += \"_buf\"\n\tbufferedUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser, bufferedUser} {\n\t\tclient, err := getFTPClient(user, true, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\tdata := []byte(\"test data\")\n\t\t\terr = os.WriteFile(testFilePath, data, os.ModePerm)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)+5), client, 5)\n\t\t\tassert.NoError(t, err)\n\t\t\treaded, err := os.ReadFile(filepath.Join(user.GetHomeDir(), testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, \"test test data\", string(readed))\n\t\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(len(data)), client, 5)\n\t\t\tassert.NoError(t, err)\n\t\t\treaded, err = os.ReadFile(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, data, readed)\n\t\t\terr = client.Delete(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\t// now append to a file\n\t\t\tsrcFile, err := os.Open(testFilePath)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\terr = client.Append(testFileName, srcFile)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\terr = srcFile.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tsize, err := client.FileSize(testFileName)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, int64(2*len(data)), size)\n\t\t\t\terr = ftpDownloadFile(testFileName, localDownloadPath, int64(2*len(data)), client, 0)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treaded, err = os.ReadFile(localDownloadPath)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\texpected := append(data, data...)\n\t\t\t\tassert.Equal(t, expected, readed)\n\t\t\t}\n\t\t\t// append to a new file\n\t\t\tsrcFile, err = os.Open(testFilePath)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tnewFileName := testFileName + \"_new\"\n\t\t\t\terr = client.Append(newFileName, srcFile)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\terr = srcFile.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tsize, err := client.FileSize(newFileName)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, int64(len(data)), size)\n\t\t\t\terr = ftpDownloadFile(newFileName, localDownloadPath, int64(len(data)), client, 0)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treaded, err = os.ReadFile(localDownloadPath)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, data, readed)\n\t\t\t}\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(bufferedUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(bufferedUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\n//nolint:dupl\nfunc TestDeniedLoginMethod(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(user, false, nil)\n\tassert.Error(t, err)\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndPassword}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\tassert.NoError(t, checkBasicFTP(client))\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\n//nolint:dupl\nfunc TestDeniedProtocols(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.DeniedProtocols = []string{common.ProtocolFTP}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(user, false, nil)\n\tassert.Error(t, err)\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolWebDAV}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\tassert.NoError(t, checkBasicFTP(client))\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaLimits(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 1\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaFiles = 1\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\ttestFileSize := int64(65535)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\ttestFileSize1 := int64(131072)\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\ttestFileSize2 := int64(32768)\n\t\ttestFileName2 := \"test_file2.dat\"\n\t\ttestFilePath2 := filepath.Join(homeBasePath, testFileName2)\n\t\terr = createTestFile(testFilePath2, testFileSize2)\n\t\tassert.NoError(t, err)\n\t\t// test quota files\n\t\tclient, err := getFTPClient(user, false, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = ftpUploadFile(testFilePath, testFileName+\".quota\", testFileSize, client, 0) //nolint:goconst\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName+\".quota1\", testFileSize, client, 0)\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Rename(testFileName+\".quota\", testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\t// test quota size\n\t\tuser.QuotaSize = testFileSize - 1\n\t\tuser.QuotaFiles = 0\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tclient, err = getFTPClient(user, true, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = ftpUploadFile(testFilePath, testFileName+\".quota\", testFileSize, client, 0)\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Rename(testFileName, testFileName+\".quota\")\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\t// now test quota limits while uploading the current file, we have 1 bytes remaining\n\t\tuser.QuotaSize = testFileSize + 1\n\t\tuser.QuotaFiles = 0\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tclient, err = getFTPClient(user, false, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = ftpUploadFile(testFilePath1, testFileName1, testFileSize1, client, 0)\n\t\t\tassert.Error(t, err)\n\t\t\t_, err = client.FileSize(testFileName1)\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Rename(testFileName+\".quota\", testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\t// overwriting an existing file will work if the resulting size is lesser or equal than the current one\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath2, testFileName, testFileSize2, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath1, testFileName, testFileSize1, client, 0)\n\t\t\tassert.Error(t, err)\n\t\t\terr = ftpUploadFile(testFilePath1, testFileName, testFileSize1, client, 10)\n\t\t\tassert.Error(t, err)\n\t\t\terr = ftpUploadFile(testFilePath2, testFileName, testFileSize2, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath2)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.QuotaFiles = 0\n\t\t\tuser.QuotaSize = 0\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.QuotaSize = 0\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadMaxSize(t *testing.T) {\n\ttestFileSize := int64(65535)\n\tu := getTestUser()\n\tu.Filters.MaxUploadFileSize = testFileSize + 1\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.Filters.MaxUploadFileSize = testFileSize + 1\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\ttestFileSize1 := int64(131072)\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\tclient, err := getFTPClient(user, false, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = ftpUploadFile(testFilePath1, testFileName1, testFileSize1, client, 0)\n\t\t\tassert.Error(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\t// now test overwrite an existing file with a size bigger than the allowed one\n\t\t\terr = createTestFile(filepath.Join(user.GetHomeDir(), testFileName1), testFileSize1)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath1, testFileName1, testFileSize1, client, 0)\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.Filters.MaxUploadFileSize = 65536000\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginWithIPilters(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.DeniedIP = []string{\"192.167.0.0/24\", \"172.18.0.0/16\"}\n\tu.Filters.AllowedIP = []string{\"172.19.0.0/16\"}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif !assert.Error(t, err) {\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginWithDatabaseCredentials(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"test\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ \"type\": \"service_account\", \"private_key\": \" \", \"client_email\": \"example@iam.gserviceaccount.com\" }`)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())\n\tassert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())\n\tassert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey())\n\n\tclient, err := getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginInvalidFs(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"test\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(\"invalid JSON for credentials\")\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient, err := getFTPClient(user, false, nil)\n\tif !assert.Error(t, err) {\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestClientClose(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\tstats := common.Connections.GetStats(\"\")\n\t\tif assert.Len(t, stats, 1) {\n\t\t\tcommon.Connections.Close(stats[0].ConnectionID, \"\")\n\t\t\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t\t\t1*time.Second, 50*time.Millisecond)\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRename(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\ttestDir := \"adir\"\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tclient, err := getFTPClient(user, false, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.MakeDir(testDir)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(testFileName, path.Join(\"missing\", testFileName))\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Rename(testFileName, path.Join(testDir, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\tsize, err := client.FileSize(path.Join(testDir, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testFileSize, size)\n\t\t\tif runtime.GOOS != osWindows {\n\t\t\t\totherDir := \"dir\"\n\t\t\t\terr = client.MakeDir(otherDir)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\terr = client.MakeDir(path.Join(otherDir, testDir))\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tcode, response, err := client.SendCommand(\"SITE CHMOD 0001 %v\", otherDir)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\t\t\tassert.Equal(t, \"SITE CHMOD command successful\", response)\n\t\t\t\terr = client.Rename(testDir, path.Join(otherDir, testDir))\n\t\t\t\tassert.Error(t, err)\n\n\t\t\t\tcode, response, err = client.SendCommand(\"SITE CHMOD 755 %v\", otherDir)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\t\t\tassert.Equal(t, \"SITE CHMOD command successful\", response)\n\t\t\t}\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tuser.Permissions[path.Join(\"/\", testDir)] = []string{dataprovider.PermListItems}\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tclient, err = getFTPClient(user, false, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = client.Rename(path.Join(testDir, testFileName), testFileName)\n\t\t\tassert.Error(t, err)\n\t\t\terr := client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Permissions = make(map[string][]string)\n\t\t\tuser.Permissions[\"/\"] = allPerms\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSymlink(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tclient, err := getFTPClient(user, false, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tcode, _, err := client.SendCommand(\"SITE SYMLINK %v %v\", testFileName, testFileName+\".link\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\n\t\t\tif runtime.GOOS != osWindows {\n\t\t\t\ttestDir := \"adir\"\n\t\t\t\totherDir := \"dir\"\n\t\t\t\terr = client.MakeDir(otherDir)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\terr = client.MakeDir(path.Join(otherDir, testDir))\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tcode, response, err := client.SendCommand(\"SITE CHMOD 0001 %v\", otherDir)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\t\t\tassert.Equal(t, \"SITE CHMOD command successful\", response)\n\t\t\t\tcode, _, err = client.SendCommand(\"SITE SYMLINK %v %v\", testDir, path.Join(otherDir, testDir))\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, ftp.StatusFileUnavailable, code)\n\n\t\t\t\tcode, response, err = client.SendCommand(\"SITE CHMOD 755 %v\", otherDir)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\t\t\tassert.Equal(t, \"SITE CHMOD command successful\", response)\n\t\t\t}\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestStat(t *testing.T) {\n\tu := getTestUser()\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermUpload}\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient, err := getFTPClient(user, false, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\tsubDir := \"subdir\"\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.MakeDir(subDir)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, path.Join(\"/\", subDir, testFileName), testFileSize, client, 0)\n\t\t\tassert.Error(t, err)\n\t\t\tsize, err := client.FileSize(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testFileSize, size)\n\t\t\t_, err = client.FileSize(path.Join(\"/\", subDir, testFileName))\n\t\t\tassert.Error(t, err)\n\t\t\t_, err = client.FileSize(\"missing file\")\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadOverwriteVfolder(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tvdir := \"/vdir\"\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tfolderName := filepath.Base(mappedPath)\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdir,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\terr = os.MkdirAll(mappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), folder.UsedQuotaSize)\n\t\tassert.Equal(t, 0, folder.UsedQuotaFiles)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\n\t\terr = ftpUploadFile(testFilePath, path.Join(vdir, testFileName), testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tfolder, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), folder.UsedQuotaSize)\n\t\tassert.Equal(t, 0, folder.UsedQuotaFiles)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestTransferQuotaLimits(t *testing.T) {\n\tu := getTestUser()\n\tu.DownloadDataTransfer = 1\n\tu.UploadDataTransfer = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(524288)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), ftpserver.ErrStorageExceeded.Error())\n\t\t}\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error())\n\t\t}\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\ttestFileSize = int64(600000)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tuser.DownloadDataTransfer = 2\n\tuser.UploadDataTransfer = 2\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.Error(t, err)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestAllocateAvailable(t *testing.T) {\n\tu := getTestUser()\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tfolderName := filepath.Base(mappedPath)\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t\tQuotaSize:   110,\n\t})\n\terr = os.MkdirAll(mappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"allo 2000000\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\tassert.Equal(t, \"Done !\", response)\n\n\t\tcode, response, err = client.SendCommand(\"AVBL /vdir\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tassert.Equal(t, \"110\", response)\n\n\t\tcode, _, err = client.SendCommand(\"AVBL\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\tuser.QuotaSize = 100\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := user.QuotaSize - 1\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tcode, response, err := client.SendCommand(\"allo 1000\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\tassert.Equal(t, \"Done !\", response)\n\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\n\t\tcode, response, err = client.SendCommand(\"AVBL\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tassert.Equal(t, \"1\", response)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\tuser.TotalDataTransfer = 1\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"AVBL\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tassert.Equal(t, \"1\", response)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser.TotalDataTransfer = 0\n\tuser.UploadDataTransfer = 5\n\tuser.QuotaSize = 6 * 1024 * 1024\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"AVBL\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tassert.Equal(t, \"5242880\", response)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser.TotalDataTransfer = 0\n\tuser.UploadDataTransfer = 5\n\tuser.QuotaSize = 0\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"AVBL\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tassert.Equal(t, \"5242880\", response)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser.Filters.MaxUploadFileSize = 100\n\tuser.QuotaSize = 0\n\tuser.TotalDataTransfer = 0\n\tuser.UploadDataTransfer = 0\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"allo 10000\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\tassert.Equal(t, \"Done !\", response)\n\n\t\tcode, response, err = client.SendCommand(\"AVBL\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tassert.Equal(t, \"100\", response)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser.QuotaSize = 50\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"AVBL\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tassert.Equal(t, \"0\", response)\n\t}\n\n\tuser.QuotaSize = 1000\n\tuser.Filters.MaxUploadFileSize = 1\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"AVBL\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tassert.Equal(t, \"1\", response)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestAvailableSFTPFs(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(sftpUser, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"AVBL /\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\tavblSize, err := strconv.ParseInt(response, 10, 64)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, avblSize, int64(0))\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestChtimes(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient, err := getFTPClient(user, false, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tmtime := time.Now().Format(\"20060102150405\")\n\t\t\tcode, response, err := client.SendCommand(\"MFMT %v %v\", mtime, testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\t\tassert.Equal(t, fmt.Sprintf(\"Modify=%v; %v\", mtime, testFileName), response)\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMODEType(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"MODE s\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusNotImplementedParameter, code)\n\t\tassert.Equal(t, \"Unsupported mode\", response)\n\t\tcode, response, err = client.SendCommand(\"MODE S\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\tassert.Equal(t, \"Using stream mode\", response)\n\n\t\tcode, _, err = client.SendCommand(\"MODE Z\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusNotImplementedParameter, code)\n\n\t\tcode, _, err = client.SendCommand(\"MODE SS\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusNotImplementedParameter, code)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSTAT(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(131072)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\ttestDir := \"testdir\"\n\t\terr = client.MakeDir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, path.Join(testDir, testFileName), testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, path.Join(testDir, testFileName+\"_1\"), testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tcode, response, err := client.SendCommand(\"STAT %s\", testDir)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusDirectory, code)\n\t\tassert.Contains(t, response, fmt.Sprintf(\"STAT %s\", testDir))\n\t\tassert.Contains(t, response, testFileName)\n\t\tassert.Contains(t, response, testFileName+\"_1\")\n\t\tassert.Contains(t, response, \"End\")\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestChown(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"chown is not supported on Windows\")\n\t}\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, nil)\n\tif assert.NoError(t, err) {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(131072)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\tcode, response, err := client.SendCommand(\"SITE CHOWN 1000:1000 %v\", testFileName)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusFileUnavailable, code)\n\t\tassert.Equal(t, \"Couldn't chown: operation unsupported\", response)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestChmod(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"chmod is partially supported on Windows\")\n\t}\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient, err := getFTPClient(user, true, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(131072)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tcode, response, err := client.SendCommand(\"SITE CHMOD 600 %v\", testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, ftp.StatusCommandOK, code)\n\t\t\tassert.Equal(t, \"SITE CHMOD command successful\", response)\n\n\t\t\tfi, err := os.Stat(filepath.Join(user.HomeDir, testFileName))\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, os.FileMode(0600), fi.Mode().Perm())\n\t\t\t}\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCombineDisabled(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient, err := getFTPClient(user, true, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tcode, response, err := client.SendCommand(\"COMB file file.1 file.2\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, ftp.StatusNotImplemented, code)\n\t\t\tassert.Equal(t, \"COMB support is disabled\", response)\n\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestActiveModeDisabled(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClientImplicitTLS(user)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\tcode, response, err := client.SendCommand(\"PORT 10,2,0,2,4,31\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusNotAvailable, code)\n\t\tassert.Equal(t, \"PORT command is disabled\", response)\n\n\t\tcode, response, err = client.SendCommand(\"EPRT |1|132.235.1.2|6275|\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusNotAvailable, code)\n\t\tassert.Equal(t, \"EPRT command is disabled\", response)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tclient, err = getFTPClient(user, false, nil)\n\tif assert.NoError(t, err) {\n\t\tcode, response, err := client.SendCommand(\"PORT 10,2,0,2,4,31\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusBadArguments, code)\n\t\tassert.Equal(t, \"Your request does not meet the configured security requirements\", response)\n\n\t\tcode, response, err = client.SendCommand(\"EPRT |1|132.235.1.2|6275|\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusBadArguments, code)\n\t\tassert.Equal(t, \"Your request does not meet the configured security requirements\", response)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSITEDisabled(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClientImplicitTLS(user)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\n\t\tcode, response, err := client.SendCommand(\"SITE CHMOD 600 afile.txt\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, ftp.StatusBadCommand, code)\n\t\tassert.Equal(t, \"SITE support is disabled\", response)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHASH(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestUserWithCryptFs()\n\tu.Username += \"_crypt\"\n\tcryptUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser, cryptUser} {\n\t\tclient, err := getFTPClientImplicitTLS(user)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(131072)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\n\t\t\th := sha256.New()\n\t\t\tf, err := os.Open(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = io.Copy(h, f)\n\t\t\tassert.NoError(t, err)\n\t\t\thash := hex.EncodeToString(h.Sum(nil))\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\n\t\t\tcode, response, err := client.SendCommand(\"XSHA256 %v\", testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, ftp.StatusRequestedFileActionOK, code)\n\t\t\tassert.Contains(t, response, hash)\n\n\t\t\tcode, response, err = client.SendCommand(\"HASH %v\", testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, ftp.StatusFile, code)\n\t\t\tassert.Contains(t, response, hash)\n\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(cryptUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(cryptUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCombine(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient, err := getFTPClientImplicitTLS(user)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(131072)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = checkBasicFTP(client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName+\".1\", testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = ftpUploadFile(testFilePath, testFileName+\".2\", testFileSize, client, 0)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tcode, response, err := client.SendCommand(\"COMB %v %v %v\", testFileName, testFileName+\".1\", testFileName+\".2\")\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\tassert.Equal(t, ftp.StatusRequestedFileActionOK, code)\n\t\t\t\tassert.Equal(t, \"COMB succeeded!\", response)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, ftp.StatusFileUnavailable, code)\n\t\t\t\tassert.Contains(t, response, \"COMB is not supported for this filesystem\")\n\t\t\t}\n\n\t\t\terr = client.Quit()\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestClientCertificateAuthRevokedCert(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = tlsClient2Username\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t\tClientSessionCache: tls.NewLRUClientSessionCache(0),\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\t_, err = getFTPClientWithSessionReuse(user, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"bad certificate\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestClientCertificateAuth(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\t// TLS username is not enabled, mutual TLS should fail\n\t_, err = getFTPClient(user, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"login method password is not allowed\")\n\t}\n\n\tuser.Filters.TLSUsername = sdk.TLSUsernameCN\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(user, true, tlsConfig)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t// now use a valid certificate with a CN different from username\n\tu = getTestUser()\n\tu.Username = tlsClient2Username\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}\n\tuser2, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(user2, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"does not match username\")\n\t}\n\t// add the certs to the user\n\tuser2.Filters.TLSUsername = sdk.TLSUsernameNone\n\tuser2.Filters.TLSCerts = []string{client2Crt, client1Crt}\n\tuser2, _, err = httpdtest.UpdateUser(user2, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient, err = getFTPClient(user2, true, tlsConfig)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\tuser2.Filters.TLSCerts = []string{client2Crt}\n\tuser2, _, err = httpdtest.UpdateUser(user2, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(user2, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"TLS certificate is not valid\")\n\t}\n\n\t// now disable certificate authentication\n\tuser.Filters.DeniedLoginMethods = append(user.Filters.DeniedLoginMethods, dataprovider.LoginMethodTLSCertificate,\n\t\tdataprovider.LoginMethodTLSCertificateAndPwd)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(user, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"login method TLSCertificate+password is not allowed\")\n\t}\n\n\t// disable FTP protocol\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}\n\tuser.Filters.DeniedProtocols = append(user.Filters.DeniedProtocols, common.ProtocolFTP)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(user, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"protocol FTP is not allowed\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user2.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t_, err = getFTPClient(user, true, tlsConfig)\n\tassert.Error(t, err)\n}\n\nfunc TestClientCertificateAndPwdAuth(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\tclient, err := getFTPClient(user, true, tlsConfig)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = getFTPClient(user, true, nil)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"login method password is not allowed\")\n\t}\n\tuser.Password = defaultPassword + \"1\"\n\t_, err = getFTPClient(user, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid credentials\")\n\t}\n\n\ttlsCert, err = tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = []tls.Certificate{tlsCert}\n\t_, err = getFTPClient(user, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"bad certificate\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthWithClientCert(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, dataprovider.LoginMethodPassword)\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 8\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\t// external auth not called, auth scope is 8\n\t_, err = getFTPClient(u, true, nil)\n\tassert.Error(t, err)\n\t_, _, err = httpdtest.GetUserByUsername(u.Username, http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\tclient, err := getFTPClient(u, true, tlsConfig)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, u.Username, user.Username)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tu.Username = tlsClient2Username\n\t_, err = getFTPClient(u, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid credentials\")\n\t}\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreLoginHookWithClientCert(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, dataprovider.LoginMethodPassword)\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetUserByUsername(tlsClient1Username, http.StatusNotFound)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\tclient, err := getFTPClient(u, true, tlsConfig)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(tlsClient1Username, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t// test login with an existing user\n\tclient, err = getFTPClient(user, true, tlsConfig)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\terr := client.Quit()\n\t\tassert.NoError(t, err)\n\t}\n\n\tu.Username = tlsClient2Username\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\t_, err = getFTPClient(u, true, tlsConfig)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"does not match username\")\n\t}\n\n\tuser2, _, err := httpdtest.GetUserByUsername(tlsClient2Username, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user2.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestNestedVirtualFolders(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tmappedPathCrypt := filepath.Join(os.TempDir(), \"crypt\")\n\tfolderNameCrypt := filepath.Base(mappedPathCrypt)\n\tvdirCryptPath := \"/vdir/crypt\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameCrypt,\n\t\t},\n\t\tVirtualPath: vdirCryptPath,\n\t})\n\tmappedPath := filepath.Join(os.TempDir(), \"local\")\n\tfolderName := filepath.Base(mappedPath)\n\tvdirPath := \"/vdir/local\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t})\n\tmappedPathNested := filepath.Join(os.TempDir(), \"nested\")\n\tfolderNameNested := filepath.Base(mappedPathNested)\n\tvdirNestedPath := \"/vdir/crypt/nested\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameNested,\n\t\t},\n\t\tVirtualPath: vdirNestedPath,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName: folderNameCrypt,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t\tMappedPath: mappedPathCrypt,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folderNameNested,\n\t\tMappedPath: mappedPathNested,\n\t}\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient, err := getFTPClient(sftpUser, false, nil)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicFTP(client)\n\t\tassert.NoError(t, err)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\n\t\terr = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, path.Join(\"/vdir\", testFileName), testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(path.Join(\"/vdir\", testFileName), localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, path.Join(vdirPath, testFileName), testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, path.Join(vdirCryptPath, testFileName), testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(path.Join(vdirCryptPath, testFileName), localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpUploadFile(testFilePath, path.Join(vdirNestedPath, testFileName), testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\t\terr = ftpDownloadFile(path.Join(vdirNestedPath, testFileName), localDownloadPath, testFileSize, client, 0)\n\t\tassert.NoError(t, err)\n\n\t\terr = client.Quit()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameNested}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathCrypt)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathNested)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 }, 1*time.Second, 50*time.Millisecond)\n\tassert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,\n\t\t50*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc checkBasicFTP(client *ftp.ServerConn) error {\n\t_, err := client.CurrentDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = client.NoOp()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = client.List(\".\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc ftpUploadFile(localSourcePath string, remoteDestPath string, expectedSize int64, client *ftp.ServerConn, offset uint64) error {\n\tsrcFile, err := os.Open(localSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer srcFile.Close()\n\tif offset > 0 {\n\t\terr = client.StorFrom(remoteDestPath, srcFile, offset)\n\t} else {\n\t\terr = client.Stor(remoteDestPath, srcFile)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tif expectedSize > 0 {\n\t\tsize, err := client.FileSize(remoteDestPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif size != expectedSize {\n\t\t\treturn fmt.Errorf(\"uploaded file size does not match, actual: %v, expected: %v\", size, expectedSize)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ftpDownloadFile(remoteSourcePath string, localDestPath string, expectedSize int64, client *ftp.ServerConn, offset uint64) error {\n\tdownloadDest, err := os.Create(localDestPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer downloadDest.Close()\n\tvar r *ftp.Response\n\tif offset > 0 {\n\t\tr, err = client.RetrFrom(remoteSourcePath, offset)\n\t} else {\n\t\tr, err = client.Retr(remoteSourcePath)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer r.Close()\n\n\twritten, err := io.Copy(downloadDest, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif written != expectedSize {\n\t\treturn fmt.Errorf(\"downloaded file size does not match, actual: %v, expected: %v\", written, expectedSize)\n\t}\n\treturn nil\n}\n\nfunc getFTPClientImplicitTLS(user dataprovider.User) (*ftp.ServerConn, error) {\n\tftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\tftpOptions = append(ftpOptions, ftp.DialWithTLS(tlsConfig))\n\tftpOptions = append(ftpOptions, ftp.DialWithDisabledEPSV(true))\n\tclient, err := ftp.Dial(ftpSrvAddrTLS, ftpOptions...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpwd := defaultPassword\n\tif user.Password != \"\" {\n\t\tpwd = user.Password\n\t}\n\terr = client.Login(user.Username, pwd)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, err\n}\n\nfunc getFTPClientWithSessionReuse(user dataprovider.User, tlsConfig *tls.Config, dialOptions ...ftp.DialOption,\n) (*ftp.ServerConn, error) {\n\tftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}\n\tftpOptions = append(ftpOptions, dialOptions...)\n\tif tlsConfig == nil {\n\t\ttlsConfig = &tls.Config{\n\t\t\tServerName:         \"localhost\",\n\t\t\tInsecureSkipVerify: true, // use this for tests only\n\t\t\tMinVersion:         tls.VersionTLS12,\n\t\t\tClientSessionCache: tls.NewLRUClientSessionCache(0),\n\t\t}\n\t}\n\tftpOptions = append(ftpOptions, ftp.DialWithExplicitTLS(tlsConfig))\n\tclient, err := ftp.Dial(ftpSrvAddrTLSResumption, ftpOptions...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpwd := defaultPassword\n\tif user.Password != \"\" {\n\t\tif user.Password == emptyPwdPlaceholder {\n\t\t\tpwd = \"\"\n\t\t} else {\n\t\t\tpwd = user.Password\n\t\t}\n\t}\n\terr = client.Login(user.Username, pwd)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, err\n}\n\nfunc getFTPClient(user dataprovider.User, useTLS bool, tlsConfig *tls.Config, dialOptions ...ftp.DialOption,\n) (*ftp.ServerConn, error) {\n\tftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}\n\tftpOptions = append(ftpOptions, dialOptions...)\n\tif useTLS {\n\t\tif tlsConfig == nil {\n\t\t\ttlsConfig = &tls.Config{\n\t\t\t\tServerName:         \"localhost\",\n\t\t\t\tInsecureSkipVerify: true, // use this for tests only\n\t\t\t\tMinVersion:         tls.VersionTLS12,\n\t\t\t}\n\t\t}\n\t\tftpOptions = append(ftpOptions, ftp.DialWithExplicitTLS(tlsConfig))\n\t}\n\tclient, err := ftp.Dial(ftpServerAddr, ftpOptions...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpwd := defaultPassword\n\tif user.Password != \"\" {\n\t\tif user.Password == emptyPwdPlaceholder {\n\t\t\tpwd = \"\"\n\t\t} else {\n\t\t\tpwd = user.Password\n\t\t}\n\t}\n\terr = client.Login(user.Username, pwd)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, err\n}\n\nfunc waitTCPListening(address string) {\n\tfor {\n\t\tconn, err := net.Dial(\"tcp\", address)\n\t\tif err != nil {\n\t\t\tlogger.WarnToConsole(\"tcp server %v not listening: %v\", address, err)\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\t\tlogger.InfoToConsole(\"tcp server %v now listening\", address)\n\t\tconn.Close()\n\t\tbreak\n\t}\n}\n\nfunc waitNoConnections() {\n\ttime.Sleep(50 * time.Millisecond)\n\tfor len(common.Connections.GetStats(\"\")) > 0 {\n\t\ttime.Sleep(50 * time.Millisecond)\n\t}\n}\n\nfunc getTestGroup() dataprovider.Group {\n\treturn dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName:        \"test_group\",\n\t\t\tDescription: \"test group description\",\n\t\t},\n\t}\n}\n\nfunc getTestUser() dataprovider.User {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:       defaultUsername,\n\t\t\tPassword:       defaultPassword,\n\t\t\tHomeDir:        filepath.Join(homeBasePath, defaultUsername),\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: 0,\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = allPerms\n\treturn user\n}\n\nfunc getTestSFTPUser() dataprovider.User {\n\tu := getTestUser()\n\tu.Username = u.Username + \"_sftp\"\n\tu.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tu.FsConfig.SFTPConfig.Endpoint = sftpServerAddr\n\tu.FsConfig.SFTPConfig.Username = defaultUsername\n\tu.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\treturn u\n}\n\nfunc getTestUserWithHTTPFs() dataprovider.User {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\tu.FsConfig.HTTPConfig = vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint: fmt.Sprintf(\"http://127.0.0.1:%d/api/v1\", httpFsPort),\n\t\t\tUsername: defaultHTTPFsUsername,\n\t\t},\n\t}\n\treturn u\n}\n\nfunc getExtAuthScriptContent(user dataprovider.User) []byte {\n\textAuthContent := []byte(\"#!/bin/sh\\n\\n\")\n\textAuthContent = append(extAuthContent, []byte(fmt.Sprintf(\"if test \\\"$SFTPGO_AUTHD_USERNAME\\\" = \\\"%v\\\"; then\\n\", user.Username))...)\n\tu, _ := json.Marshal(user)\n\textAuthContent = append(extAuthContent, []byte(fmt.Sprintf(\"echo '%v'\\n\", string(u)))...)\n\textAuthContent = append(extAuthContent, []byte(\"else\\n\")...)\n\textAuthContent = append(extAuthContent, []byte(\"echo '{\\\"username\\\":\\\"\\\"}'\\n\")...)\n\textAuthContent = append(extAuthContent, []byte(\"fi\\n\")...)\n\treturn extAuthContent\n}\n\nfunc getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tif nonJSONResponse {\n\t\tcontent = append(content, []byte(\"echo 'text response'\\n\")...)\n\t\treturn content\n\t}\n\tif len(user.Username) > 0 {\n\t\tu, _ := json.Marshal(user)\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"echo '%v'\\n\", string(u)))...)\n\t}\n\treturn content\n}\n\nfunc getSftpClient(user dataprovider.User) (*ssh.Client, *sftp.Client, error) {\n\tvar sftpClient *sftp.Client\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         5 * time.Second,\n\t}\n\tif user.Password != \"\" {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}\n\t} else {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}\n\t}\n\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif err != nil {\n\t\treturn conn, sftpClient, err\n\t}\n\tsftpClient, err = sftp.NewClient(conn)\n\tif err != nil {\n\t\tconn.Close()\n\t}\n\treturn conn, sftpClient, err\n}\n\nfunc getExitCodeScriptContent(exitCode int) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tcontent = append(content, []byte(fmt.Sprintf(\"exit %v\", exitCode))...)\n\treturn content\n}\n\nfunc createTestFile(path string, size int64) error {\n\tbaseDir := filepath.Dir(path)\n\tif _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) {\n\t\terr = os.MkdirAll(baseDir, os.ModePerm)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tcontent := make([]byte, size)\n\t_, err := rand.Read(content)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, content, os.ModePerm)\n}\n\nfunc writeCerts(certPath, keyPath, caCrtPath, caCRLPath string) error {\n\terr := os.WriteFile(certPath, []byte(ftpsCert), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing FTPS certificate: %v\", err)\n\t\treturn err\n\t}\n\terr = os.WriteFile(keyPath, []byte(ftpsKey), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing FTPS private key: %v\", err)\n\t\treturn err\n\t}\n\terr = os.WriteFile(caCrtPath, []byte(caCRT), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing FTPS CA crt: %v\", err)\n\t\treturn err\n\t}\n\terr = os.WriteFile(caCRLPath, []byte(caCRL), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing FTPS CRL: %v\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc generateTOTPPasscode(secret string, algo otp.Algorithm) (string, error) {\n\treturn totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{\n\t\tPeriod:    30,\n\t\tSkew:      1,\n\t\tDigits:    otp.DigitsSix,\n\t\tAlgorithm: algo,\n\t})\n}\n\nfunc startHTTPFs() {\n\tgo func() {\n\t\tif err := httpdtest.StartTestHTTPFs(httpFsPort, nil); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTPfs test server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\twaitTCPListening(fmt.Sprintf(\":%d\", httpFsPort))\n}\n"
  },
  {
    "path": "internal/ftpd/handler.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage ftpd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nvar (\n\terrNotImplemented   = errors.New(\"not implemented\")\n\terrCOMBNotSupported = errors.New(\"COMB is not supported for this filesystem\")\n)\n\n// Connection details for an FTP connection.\n// It implements common.ActiveConnection and ftpserver.ClientDriver interfaces\ntype Connection struct {\n\t*common.BaseConnection\n\tclientContext     ftpserver.ClientContext\n\tdoWildcardListDir bool\n}\n\nfunc (c *Connection) getFTPMode() string {\n\tif c.clientContext == nil {\n\t\treturn \"\"\n\t}\n\tswitch c.clientContext.GetLastDataChannel() {\n\tcase ftpserver.DataChannelActive:\n\t\treturn \"active\"\n\tcase ftpserver.DataChannelPassive:\n\t\treturn \"passive\"\n\t}\n\treturn \"\"\n}\n\n// GetClientVersion returns the connected client's version.\n// It returns \"Unknown\" if the client does not advertise its\n// version\nfunc (c *Connection) GetClientVersion() string {\n\tversion := c.clientContext.GetClientVersion()\n\tif len(version) > 0 {\n\t\treturn version\n\t}\n\treturn \"Unknown\"\n}\n\n// GetLocalAddress returns local connection address\nfunc (c *Connection) GetLocalAddress() string {\n\treturn c.clientContext.LocalAddr().String()\n}\n\n// GetRemoteAddress returns the connected client's address\nfunc (c *Connection) GetRemoteAddress() string {\n\treturn c.clientContext.RemoteAddr().String()\n}\n\n// Disconnect disconnects the client\nfunc (c *Connection) Disconnect() error {\n\treturn c.clientContext.Close()\n}\n\n// GetCommand returns the last received FTP command\nfunc (c *Connection) GetCommand() string {\n\treturn c.clientContext.GetLastCommand()\n}\n\n// Create is not implemented we use ClientDriverExtentionFileTransfer\nfunc (c *Connection) Create(_ string) (afero.File, error) {\n\treturn nil, errNotImplemented\n}\n\n// Mkdir creates a directory using the connection filesystem\nfunc (c *Connection) Mkdir(name string, _ os.FileMode) error {\n\tc.UpdateLastActivity()\n\tname = util.CleanPath(name)\n\n\treturn c.CreateDir(name, true)\n}\n\n// MkdirAll is not implemented, we don't need it\nfunc (c *Connection) MkdirAll(_ string, _ os.FileMode) error {\n\treturn errNotImplemented\n}\n\n// Open is not implemented we use ClientDriverExtentionFileTransfer and ClientDriverExtensionFileList\nfunc (c *Connection) Open(_ string) (afero.File, error) {\n\treturn nil, errNotImplemented\n}\n\n// OpenFile is not implemented we use ClientDriverExtentionFileTransfer\nfunc (c *Connection) OpenFile(_ string, _ int, _ os.FileMode) (afero.File, error) {\n\treturn nil, errNotImplemented\n}\n\n// Remove removes a file.\n// We implements ClientDriverExtensionRemoveDir for directories\nfunc (c *Connection) Remove(name string) error {\n\tc.UpdateLastActivity()\n\tname = util.CleanPath(name)\n\n\tfs, p, err := c.GetFsAndResolvedPath(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar fi os.FileInfo\n\tif fi, err = fs.Lstat(p); err != nil {\n\t\tc.Log(logger.LevelError, \"failed to remove file %q: stat error: %+v\", p, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\n\tif fi.IsDir() && fi.Mode()&os.ModeSymlink == 0 {\n\t\tc.Log(logger.LevelError, \"cannot remove %q is not a file/symlink\", p)\n\t\treturn c.GetGenericError(nil)\n\t}\n\treturn c.RemoveFile(fs, p, name, fi)\n}\n\n// RemoveAll is not implemented, we don't need it\nfunc (c *Connection) RemoveAll(_ string) error {\n\treturn errNotImplemented\n}\n\n// Rename renames a file or a directory\nfunc (c *Connection) Rename(oldname, newname string) error {\n\tc.UpdateLastActivity()\n\toldname = util.CleanPath(oldname)\n\tnewname = util.CleanPath(newname)\n\n\treturn c.BaseConnection.Rename(oldname, newname)\n}\n\n// Stat returns a FileInfo describing the named file/directory, or an error,\n// if any happens\nfunc (c *Connection) Stat(name string) (os.FileInfo, error) {\n\tc.UpdateLastActivity()\n\tname = util.CleanPath(name)\n\tc.doWildcardListDir = false\n\n\tif !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tfi, err := c.DoStat(name, 0, true)\n\tif err != nil {\n\t\tif c.isListDirWithWildcards(path.Base(name)) {\n\t\t\tc.doWildcardListDir = true\n\t\t\treturn vfs.NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn fi, nil\n}\n\n// Name returns the name of this connection\nfunc (c *Connection) Name() string {\n\treturn c.GetID()\n}\n\n// Chown changes the uid and gid of the named file\nfunc (c *Connection) Chown(_ string, _, _ int) error {\n\tc.UpdateLastActivity()\n\n\treturn common.ErrOpUnsupported\n\t/*p, err := c.Fs.ResolvePath(name)\n\tif err != nil {\n\t\treturn c.GetFsError(err)\n\t}\n\tattrs := common.StatAttributes{\n\t\tFlags: common.StatAttrUIDGID,\n\t\tUID:   uid,\n\t\tGID:   gid,\n\t}\n\n\treturn c.SetStat(p, name, &attrs)*/\n}\n\n// Chmod changes the mode of the named file/directory\nfunc (c *Connection) Chmod(name string, mode os.FileMode) error {\n\tc.UpdateLastActivity()\n\tname = util.CleanPath(name)\n\n\tattrs := common.StatAttributes{\n\t\tFlags: common.StatAttrPerms,\n\t\tMode:  mode,\n\t}\n\treturn c.SetStat(name, &attrs)\n}\n\n// Chtimes changes the access and modification times of the named file\nfunc (c *Connection) Chtimes(name string, atime time.Time, mtime time.Time) error {\n\tc.UpdateLastActivity()\n\tname = util.CleanPath(name)\n\n\tattrs := common.StatAttributes{\n\t\tFlags: common.StatAttrTimes,\n\t\tAtime: atime,\n\t\tMtime: mtime,\n\t}\n\treturn c.SetStat(name, &attrs)\n}\n\n// GetAvailableSpace implements ClientDriverExtensionAvailableSpace interface\nfunc (c *Connection) GetAvailableSpace(dirName string) (int64, error) {\n\tc.UpdateLastActivity()\n\tdirName = util.CleanPath(dirName)\n\n\tdiskQuota, transferQuota := c.HasSpace(false, false, path.Join(dirName, \"fakefile.txt\"))\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\treturn 0, nil\n\t}\n\n\tif diskQuota.AllowedSize == 0 && transferQuota.AllowedULSize == 0 && transferQuota.AllowedTotalSize == 0 {\n\t\t// no quota restrictions\n\t\tif c.User.Filters.MaxUploadFileSize > 0 {\n\t\t\treturn c.User.Filters.MaxUploadFileSize, nil\n\t\t}\n\n\t\tfs, p, err := c.GetFsAndResolvedPath(dirName)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tstatVFS, err := fs.GetAvailableDiskSize(p)\n\t\tif err != nil {\n\t\t\treturn 0, c.GetFsError(fs, err)\n\t\t}\n\t\treturn int64(statVFS.FreeSpace()), nil\n\t}\n\n\tallowedDiskSize := diskQuota.AllowedSize\n\tallowedUploadSize := transferQuota.AllowedULSize\n\tif transferQuota.AllowedTotalSize > 0 {\n\t\tallowedUploadSize = transferQuota.AllowedTotalSize\n\t}\n\tallowedSize := allowedDiskSize\n\tif allowedSize == 0 {\n\t\tallowedSize = allowedUploadSize\n\t} else {\n\t\tif allowedUploadSize > 0 && allowedUploadSize < allowedSize {\n\t\t\tallowedSize = allowedUploadSize\n\t\t}\n\t}\n\t// the available space is the minimum between MaxUploadFileSize, if setted,\n\t// and quota allowed size\n\tif c.User.Filters.MaxUploadFileSize > 0 {\n\t\tif c.User.Filters.MaxUploadFileSize < allowedSize {\n\t\t\treturn c.User.Filters.MaxUploadFileSize, nil\n\t\t}\n\t}\n\n\treturn allowedSize, nil\n}\n\n// AllocateSpace implements ClientDriverExtensionAllocate interface\nfunc (c *Connection) AllocateSpace(_ int) error {\n\tc.UpdateLastActivity()\n\t// we treat ALLO as NOOP see RFC 959\n\treturn nil\n}\n\n// RemoveDir implements ClientDriverExtensionRemoveDir\nfunc (c *Connection) RemoveDir(name string) error {\n\tc.UpdateLastActivity()\n\tname = util.CleanPath(name)\n\n\treturn c.BaseConnection.RemoveDir(name)\n}\n\n// Symlink implements ClientDriverExtensionSymlink\nfunc (c *Connection) Symlink(oldname, newname string) error {\n\tc.UpdateLastActivity()\n\toldname = util.CleanPath(oldname)\n\tnewname = util.CleanPath(newname)\n\n\treturn c.CreateSymlink(oldname, newname)\n}\n\n// ReadDir implements ClientDriverExtensionFilelist\nfunc (c *Connection) ReadDir(name string) ([]os.FileInfo, error) {\n\tc.UpdateLastActivity()\n\tname = util.CleanPath(name)\n\n\tif c.doWildcardListDir {\n\t\tc.doWildcardListDir = false\n\t\tbaseName := path.Base(name)\n\t\t// we only support wildcards for the last path level, for example:\n\t\t// - *.xml is supported\n\t\t// - dir*/*.xml is not supported\n\t\tname = path.Dir(name)\n\t\tc.clientContext.SetListPath(name)\n\t\tlister, err := c.ListDir(name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpatternLister := &patternDirLister{\n\t\t\tDirLister:      lister,\n\t\t\tpattern:        baseName,\n\t\t\tlastCommand:    c.clientContext.GetLastCommand(),\n\t\t\tdirName:        name,\n\t\t\tconnectionPath: util.CleanPath(c.clientContext.Path()),\n\t\t}\n\t\treturn consumeDirLister(patternLister)\n\t}\n\n\tlister, err := c.ListDir(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn consumeDirLister(lister)\n}\n\n// GetHandle implements ClientDriverExtentionFileTransfer\nfunc (c *Connection) GetHandle(name string, flags int, offset int64) (ftpserver.FileTransfer, error) {\n\tc.UpdateLastActivity()\n\tname = util.CleanPath(name)\n\n\tfs, p, err := c.GetFsAndResolvedPath(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif c.GetCommand() == \"COMB\" && !vfs.IsLocalOsFs(fs) {\n\t\treturn nil, errCOMBNotSupported\n\t}\n\n\tif err := common.Connections.IsNewTransferAllowed(c.User.Username); err != nil {\n\t\tc.Log(logger.LevelInfo, \"denying transfer due to count limits\")\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tif flags&os.O_WRONLY != 0 {\n\t\treturn c.uploadFile(fs, p, name, flags)\n\t}\n\treturn c.downloadFile(fs, p, name, offset)\n}\n\nfunc (c *Connection) downloadFile(fs vfs.Fs, fsPath, ftpPath string, offset int64) (ftpserver.FileTransfer, error) {\n\tif !c.User.HasPerm(dataprovider.PermDownload, path.Dir(ftpPath)) {\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\ttransferQuota := c.GetTransferQuota()\n\tif !transferQuota.HasDownloadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file read due to quota limits\")\n\t\treturn nil, c.GetReadQuotaExceededError()\n\t}\n\n\tif ok, policy := c.User.IsFileAllowed(ftpPath); !ok {\n\t\tc.Log(logger.LevelWarn, \"reading file %q is not allowed\", ftpPath)\n\t\treturn nil, c.GetErrorForDeniedFile(policy)\n\t}\n\n\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, fsPath, ftpPath, 0, 0); err != nil {\n\t\tc.Log(logger.LevelDebug, \"download for file %q denied by pre action: %v\", ftpPath, err)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tfile, r, cancelFn, err := fs.Open(fsPath, offset)\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"could not open file %q for reading: %+v\", fsPath, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, fsPath, fsPath, ftpPath,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, transferQuota)\n\tbaseTransfer.SetFtpMode(c.getFTPMode())\n\tt := newTransfer(baseTransfer, nil, r, offset)\n\n\treturn t, nil\n}\n\nfunc (c *Connection) uploadFile(fs vfs.Fs, fsPath, ftpPath string, flags int) (ftpserver.FileTransfer, error) {\n\tif ok, _ := c.User.IsFileAllowed(ftpPath); !ok {\n\t\tc.Log(logger.LevelWarn, \"writing file %q is not allowed\", ftpPath)\n\t\treturn nil, ftpserver.ErrFileNameNotAllowed\n\t}\n\n\tfilePath := fsPath\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\tfilePath = fs.GetAtomicUploadPath(fsPath)\n\t}\n\n\tstat, statErr := fs.Lstat(fsPath)\n\tif (statErr == nil && stat.Mode()&os.ModeSymlink != 0) || fs.IsNotExist(statErr) {\n\t\tif !c.User.HasPerm(dataprovider.PermUpload, path.Dir(ftpPath)) {\n\t\t\treturn nil, fmt.Errorf(\"%w, no upload permission\", ftpserver.ErrFileNameNotAllowed)\n\t\t}\n\t\treturn c.handleFTPUploadToNewFile(fs, flags, fsPath, filePath, ftpPath)\n\t}\n\n\tif statErr != nil {\n\t\tc.Log(logger.LevelError, \"error performing file stat %q: %+v\", fsPath, statErr)\n\t\treturn nil, c.GetFsError(fs, statErr)\n\t}\n\n\t// This happen if we upload a file that has the same name of an existing directory\n\tif stat.IsDir() {\n\t\tc.Log(logger.LevelError, \"attempted to open a directory for writing to: %q\", fsPath)\n\t\treturn nil, c.GetOpUnsupportedError()\n\t}\n\n\tif !c.User.HasPerm(dataprovider.PermOverwrite, path.Dir(ftpPath)) {\n\t\treturn nil, fmt.Errorf(\"%w, no overwrite permission\", ftpserver.ErrFileNameNotAllowed)\n\t}\n\n\treturn c.handleFTPUploadToExistingFile(fs, flags, fsPath, filePath, stat.Size(), ftpPath)\n}\n\nfunc (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, flags int, resolvedPath, filePath, requestPath string) (ftpserver.FileTransfer, error) {\n\tdiskQuota, transferQuota := c.HasSpace(true, false, requestPath)\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to quota limits\")\n\t\treturn nil, ftpserver.ErrStorageExceeded\n\t}\n\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {\n\t\tc.Log(logger.LevelDebug, \"upload for file %q denied by pre action: %v\", requestPath, err)\n\t\treturn nil, ftpserver.ErrFileNameNotAllowed\n\t}\n\tfile, w, cancelFn, err := fs.Create(filePath, flags, c.GetCreateChecks(requestPath, true, false))\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error creating file %q, flags %v: %+v\", resolvedPath, flags, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tvfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())\n\n\t// we can get an error only for resume\n\tmaxWriteSize, _ := c.GetMaxWriteSize(diskQuota, false, 0, fs.IsUploadResumeSupported())\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,\n\t\tcommon.TransferUpload, 0, 0, maxWriteSize, 0, true, fs, transferQuota)\n\tbaseTransfer.SetFtpMode(c.getFTPMode())\n\tt := newTransfer(baseTransfer, w, nil, 0)\n\n\treturn t, nil\n}\n\nfunc (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolvedPath, filePath string, fileSize int64,\n\trequestPath string) (ftpserver.FileTransfer, error) {\n\tvar err error\n\tdiskQuota, transferQuota := c.HasSpace(false, false, requestPath)\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to quota limits\")\n\t\treturn nil, ftpserver.ErrStorageExceeded\n\t}\n\tminWriteOffset := int64(0)\n\t// ftpserverlib sets:\n\t// - os.O_WRONLY | os.O_APPEND for APPE and COMB\n\t// - os.O_WRONLY | os.O_CREATE for REST.\n\t// - os.O_WRONLY | os.O_CREATE | os.O_TRUNC if the command is not APPE and REST = 0\n\t// so if we don't have O_TRUNC is a resume.\n\tisResume := flags&os.O_TRUNC == 0\n\t// if there is a size limit remaining size cannot be 0 here, since quotaResult.HasSpace\n\t// will return false in this case and we deny the upload before\n\tmaxWriteSize, err := c.GetMaxWriteSize(diskQuota, isResume, fileSize, vfs.IsUploadResumeSupported(fs, fileSize))\n\tif err != nil {\n\t\tc.Log(logger.LevelDebug, \"unable to get max write size: %v\", err)\n\t\treturn nil, err\n\t}\n\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, flags); err != nil {\n\t\tc.Log(logger.LevelDebug, \"upload for file %q denied by pre action: %v\", requestPath, err)\n\t\treturn nil, ftpserver.ErrFileNameNotAllowed\n\t}\n\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\t_, _, err = fs.Rename(resolvedPath, filePath, 0)\n\t\tif err != nil {\n\t\t\tc.Log(logger.LevelError, \"error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v\",\n\t\t\t\tresolvedPath, filePath, err)\n\t\t\treturn nil, c.GetFsError(fs, err)\n\t\t}\n\t}\n\n\tfile, w, cancelFn, err := fs.Create(filePath, flags, c.GetCreateChecks(requestPath, false, isResume))\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error opening existing file, flags: %v, source: %q, err: %+v\", flags, filePath, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tinitialSize := int64(0)\n\ttruncatedSize := int64(0) // bytes truncated and not included in quota\n\tif isResume {\n\t\tc.Log(logger.LevelDebug, \"resuming upload requested, file path: %q initial size: %v\", filePath, fileSize)\n\t\tminWriteOffset = fileSize\n\t\tinitialSize = fileSize\n\t\tif vfs.IsSFTPFs(fs) && fs.IsUploadResumeSupported() {\n\t\t\t// we need this since we don't allow resume with wrong offset, we should fix this in pkg/sftp\n\t\t\tfile.Seek(initialSize, io.SeekStart) //nolint:errcheck // for sftp seek simply set the offset\n\t\t}\n\t} else {\n\t\tif vfs.HasTruncateSupport(fs) {\n\t\t\tvfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))\n\t\t\tif err == nil {\n\t\t\t\tdataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -fileSize, false)\n\t\t\t} else {\n\t\t\t\tdataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck\n\t\t\t}\n\t\t} else {\n\t\t\tinitialSize = fileSize\n\t\t\ttruncatedSize = fileSize\n\t\t}\n\t}\n\n\tvfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,\n\t\tcommon.TransferUpload, minWriteOffset, initialSize, maxWriteSize, truncatedSize, false, fs, transferQuota)\n\tbaseTransfer.SetFtpMode(c.getFTPMode())\n\tt := newTransfer(baseTransfer, w, nil, minWriteOffset)\n\n\treturn t, nil\n}\n\nfunc (c *Connection) isListDirWithWildcards(name string) bool {\n\tif strings.ContainsAny(name, \"*?[]^\") {\n\t\tlastCommand := c.clientContext.GetLastCommand()\n\t\treturn lastCommand == \"LIST\" || lastCommand == \"NLST\"\n\t}\n\treturn false\n}\n\nfunc getPathRelativeTo(base, target string) string {\n\tvar sb strings.Builder\n\tfor {\n\t\tif base == target {\n\t\t\treturn sb.String()\n\t\t}\n\t\tif !strings.HasSuffix(base, \"/\") {\n\t\t\tbase += \"/\"\n\t\t}\n\t\tif strings.HasPrefix(target, base) {\n\t\t\tsb.WriteString(strings.TrimPrefix(target, base))\n\t\t\treturn sb.String()\n\t\t}\n\t\tif base == \"/\" || base == \"./\" {\n\t\t\treturn target\n\t\t}\n\t\tsb.WriteString(\"../\")\n\t\tbase = path.Dir(path.Clean(base))\n\t}\n}\n\ntype patternDirLister struct {\n\tvfs.DirLister\n\tpattern        string\n\tlastCommand    string\n\tdirName        string\n\tconnectionPath string\n}\n\nfunc (l *patternDirLister) Next(limit int) ([]os.FileInfo, error) {\n\tfor {\n\t\tfiles, err := l.DirLister.Next(limit)\n\t\tif len(files) == 0 {\n\t\t\treturn files, err\n\t\t}\n\t\tvalidIdx := 0\n\t\tvar relativeBase string\n\t\tif l.lastCommand != \"NLST\" {\n\t\t\trelativeBase = getPathRelativeTo(l.connectionPath, l.dirName)\n\t\t}\n\t\tfor _, fi := range files {\n\t\t\tmatch, errMatch := path.Match(l.pattern, fi.Name())\n\t\t\tif errMatch != nil {\n\t\t\t\treturn nil, errMatch\n\t\t\t}\n\t\t\tif match {\n\t\t\t\tfiles[validIdx] = vfs.NewFileInfo(path.Join(relativeBase, fi.Name()), fi.IsDir(), fi.Size(),\n\t\t\t\t\tfi.ModTime(), true)\n\t\t\t\tvalidIdx++\n\t\t\t}\n\t\t}\n\t\tfiles = files[:validIdx]\n\t\tif err != nil || len(files) > 0 {\n\t\t\treturn files, err\n\t\t}\n\t}\n}\n\nfunc consumeDirLister(lister vfs.DirLister) ([]os.FileInfo, error) {\n\tdefer lister.Close()\n\n\tvar results []os.FileInfo\n\n\tfor {\n\t\tfiles, err := lister.Next(vfs.ListerBatchSize)\n\t\tfinished := errors.Is(err, io.EOF)\n\t\tresults = append(results, files...)\n\t\tif err != nil && !finished {\n\t\t\treturn results, err\n\t\t}\n\t\tif finished {\n\t\t\tlister.Close()\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "internal/ftpd/internal_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage ftpd\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/eikenb/pipeat\"\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\t\"github.com/pires/go-proxyproto\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tftpsCert = `-----BEGIN CERTIFICATE-----\nMIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw\nRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw\nOTUzMDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD\nVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVqWvrJ51t5OxV0v25NsOgR82CA\nNXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIVCzgWkxiz7XE4lgUwX44FCXZM\n3+JeUbKjUzBRMB0GA1UdDgQWBBRhLw+/o3+Z02MI/d4tmaMui9W16jAfBgNVHSME\nGDAWgBRhLw+/o3+Z02MI/d4tmaMui9W16jAPBgNVHRMBAf8EBTADAQH/MAoGCCqG\nSM49BAMCA2kAMGYCMQDqLt2lm8mE+tGgtjDmtFgdOcI72HSbRQ74D5rYTzgST1rY\n/8wTi5xl8TiFUyLMUsICMQC5ViVxdXbhuG7gX6yEqSkMKZICHpO8hqFwOD/uaFVI\ndV4vKmHUzwK/eIx+8Ay3neE=\n-----END CERTIFICATE-----`\n\tftpsKey = `-----BEGIN EC PARAMETERS-----\nBgUrgQQAIg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDCfMNsN6miEE3rVyUPwElfiJSWaR5huPCzUenZOfJT04GAcQdWvEju3\nUM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq\nWvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV\nCzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=\n-----END EC PRIVATE KEY-----`\n\tcaCRT = `-----BEGIN CERTIFICATE-----\nMIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0\nQXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT\nCENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m\nfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1\nv03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh\nyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL\nCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U\n4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb\nKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo\nNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLqR/BJTbbyXUB0imne1u00fuzb\nS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb78x7ivdyXSF5LVQJ1JvhhWu6i\nM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpBP8d2jcRZVUVrSXGc2mAGuGOY\n/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2ZxugCXULtRWJ9p4C9zUl40HEy\nOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD\nAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNoJhIvDZQrEf/VQbWuu\nXgNnt2m5MA0GCSqGSIb3DQEBCwUAA4ICAQCYhT5SRqk19hGrQ09hVSZOzynXAa5F\nsYkEWJzFyLg9azhnTPE1bFM18FScnkd+dal6mt+bQiJvdh24NaVkDghVB7GkmXki\npAiZwEDHMqtbhiPxY8LtSeCBAz5JqXVU2Q0TpAgNSH4W7FbGWNThhxcJVOoIrXKE\njbzhwl1Etcaf0DBKWliUbdlxQQs65DLy+rNBYtOeK0pzhzn1vpehUlJ4eTFzP9KX\ny2Mksuq9AspPbqnqpWW645MdTxMb5T57MCrY3GDKw63z5z3kz88LWJF3nOxZmgQy\nWFUhbLmZm7x6N5eiu6Wk8/B4yJ/n5UArD4cEP1i7nqu+mbbM/SZlq1wnGpg/sbRV\noUF+a7pRcSbfxEttle4pLFhS+ErKatjGcNEab2OlU3bX5UoBs+TYodnCWGKOuBKV\nL/CYc65QyeYZ+JiwYn9wC8YkzOnnVIQjiCEkLgSL30h9dxpnTZDLrdAA8ItelDn5\nDvjuQq58CGDsaVqpSobiSC1DMXYWot4Ets1wwovUNEq1l0MERB+2olE+JU/8E23E\neL1/aA7Kw/JibkWz1IyzClpFDKXf6kR2onJyxerdwUL+is7tqYFLysiHxZDL1bli\nSXbW8hMa5gvo0IilFP9Rznn8PplIfCsvBDVv6xsRr5nTAFtwKaMBVgznE2ghs69w\nkK8u1YiiVenmoQ==\n-----END CERTIFICATE-----`\n\tcaKey = `-----BEGIN RSA PRIVATE KEY-----\nMIIJKgIBAAKCAgEA7WHW216mfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7x\nb64rkpdzx1aWetSiCrEyc3D1v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvD\nOBUYZgtMqHZzpE6xRrqQ84zhyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKz\nn/2uEVt33qmO85WtN3RzbSqLCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj\n7B5P5MeamkkogwbExUjdHp3U4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZ\nDe67V/Q8iB2May1k7zBz1ZtbKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmOb\ncn8AIfH6smLQrn0C3cs7CYfoNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLq\nR/BJTbbyXUB0imne1u00fuzbS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb7\n8x7ivdyXSF5LVQJ1JvhhWu6iM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpB\nP8d2jcRZVUVrSXGc2mAGuGOY/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2Z\nxugCXULtRWJ9p4C9zUl40HEyOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEA\nAQKCAgEA4x0OoceG54ZrVxifqVaQd8qw3uRmUKUMIMdfuMlsdideeLO97ynmSlRY\n00kGo/I4Lp6mNEjI9gUie9+uBrcUhri4YLcujHCH+YlNnCBDbGjwbe0ds9SLCWaa\nKztZHMSlW5Q4Bqytgu+MpOnxSgqjlOk+vz9TcGFKVnUkHIkAcqKFJX8gOFxPZA/t\nOb1kJaz4kuv5W2Kur/ISKvQtvFvOtQeV0aJyZm8LqXnvS4cPI7yN4329NDU0HyDR\ny/deqS2aqV4zII3FFqbz8zix/m1xtVQzWCugZGMKrz0iuJMfNeCABb8rRGc6GsZz\n+465v/kobqgeyyneJ1s5rMFrLp2o+dwmnIVMNsFDUiN1lIZDHLvlgonaUO3IdTZc\n9asamFWKFKUMgWqM4zB1vmUO12CKowLNIIKb0L+kf1ixaLLDRGf/f9vLtSHE+oyx\nlATiS18VNA8+CGsHF6uXMRwf2auZdRI9+s6AAeyRISSbO1khyWKHo+bpOvmPAkDR\nnknTjbYgkoZOV+mrsU5oxV8s6vMkuvA3rwFhT2gie8pokuACFcCRrZi9MVs4LmUQ\nu0GYTHvp2WJUjMWBm6XX7Hk3g2HV842qpk/mdtTjNsXws81djtJPn4I/soIXSgXz\npY3SvKTuOckP9OZVF0yqKGeZXKpD288PKpC+MAg3GvEJaednagECggEBAPsfLwuP\nL1kiDjXyMcRoKlrQ6Q/zBGyBmJbZ5uVGa02+XtYtDAzLoVupPESXL0E7+r8ZpZ39\n0dV4CEJKpbVS/BBtTEkPpTK5kz778Ib04TAyj+YLhsZjsnuja3T5bIBZXFDeDVDM\n0ZaoFoKpIjTu2aO6pzngsgXs6EYbo2MTuJD3h0nkGZsICL7xvT9Mw0P1p2Ftt/hN\n+jKk3vN220wTWUsq43AePi45VwK+PNP12ZXv9HpWDxlPo3j0nXtgYXittYNAT92u\nBZbFAzldEIX9WKKZgsWtIzLaASjVRntpxDCTby/nlzQ5dw3DHU1DV3PIqxZS2+Oe\nKV+7XFWgZ44YjYECggEBAPH+VDu3QSrqSahkZLkgBtGRkiZPkZFXYvU6kL8qf5wO\nZ/uXMeqHtznAupLea8I4YZLfQim/NfC0v1cAcFa9Ckt9g3GwTSirVcN0AC1iOyv3\n/hMZCA1zIyIcuUplNr8qewoX71uPOvCNH0dix77423mKFkJmNwzy4Q+rV+qkRdLn\nv+AAgh7g5N91pxNd6LQJjoyfi1Ka6rRP2yGXM5v7QOwD16eN4JmExUxX1YQ7uNuX\npVS+HRxnBquA+3/DB1LtBX6pa2cUa+LRUmE/NCPHMvJcyuNkYpJKlNTd9vnbfo0H\nRNSJSWm+aGxDFMjuPjV3JLj2OdKMPwpnXdh2vBZCPpMCggEAM+yTvrEhmi2HgLIO\nhkz/jP2rYyfdn04ArhhqPLgd0dpuI5z24+Jq/9fzZT9ZfwSW6VK1QwDLlXcXRhXH\nQ8Hf6smev3CjuORURO61IkKaGWwrAucZPAY7ToNQ4cP9ImDXzMTNPgrLv3oMBYJR\nV16X09nxX+9NABqnQG/QjdjzDc6Qw7+NZ9f2bvzvI5qMuY2eyW91XbtJ45ThoLfP\nymAp03gPxQwL0WT7z85kJ3OrROxzwaPvxU0JQSZbNbqNDPXmFTiECxNDhpRAAWlz\n1DC5Vg2l05fkMkyPdtD6nOQWs/CYSfB5/EtxiX/xnBszhvZUIe6KFvuKFIhaJD5h\niykagQKCAQEAoBRm8k3KbTIo4ZzvyEq4V/+dF3zBRczx6FkCkYLygXBCNvsQiR2Y\nBjtI8Ijz7bnQShEoOmeDriRTAqGGrspEuiVgQ1+l2wZkKHRe/aaij/Zv+4AuhH8q\nuZEYvW7w5Uqbs9SbgQzhp2kjTNy6V8lVnjPLf8cQGZ+9Y9krwktC6T5m/i435WdN\n38h7amNP4XEE/F86Eb3rDrZYtgLIoCF4E+iCyxMehU+AGH1uABhls9XAB6vvo+8/\nSUp8lEqWWLP0U5KNOtYWfCeOAEiIHDbUq+DYUc4BKtbtV1cx3pzlPTOWw6XBi5Lq\njttdL4HyYvnasAQpwe8GcMJqIRyCVZMiwwKCAQEAhQTTS3CC8PwcoYrpBdTjW1ck\nvVFeF1YbfqPZfYxASCOtdx6wRnnEJ+bjqntagns9e88muxj9UhxSL6q9XaXQBD8+\n2AmKUxphCZQiYFZcTucjQEQEI2nN+nAKgRrUSMMGiR8Ekc2iFrcxBU0dnSohw+aB\nPbMKVypQCREu9PcDFIp9rXQTeElbaNsIg1C1w/SQjODbmN/QFHTVbRODYqLeX1J/\nVcGsykSIq7hv6bjn7JGkr2JTdANbjk9LnMjMdJFsKRYxPKkOQfYred6Hiojp5Sor\nPW5am8ejnNSPhIfqQp3uV3KhwPDKIeIpzvrB4uPfTjQWhekHCb8cKSWux3flqw==\n-----END RSA PRIVATE KEY-----`\n\tcaCRL = `-----BEGIN X509 CRL-----\nMIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN\nMjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg\nZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r\nrl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV\n+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199\nXSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE\njF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF\nDouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD\nMSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4\niYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE\nypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4\nhVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94\ncp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67\nsn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn\nw7bXJGfJcIMrrKs=\n-----END X509 CRL-----`\n\tclient1Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAJr32nHRlhyPiS7IfZ/ZWYowDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjM3WhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+kiJ3X6\nHUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi85xE\nOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLWeYl7\nQie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4ZdRf\nXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dybbhO\nc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRUh5Xo\nGzjh6iReaPSOgGatqOw9bDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEAyAK7cOTWqjyLgFM0kyyx1fNPvm2GwKep3MuU\nOrSnLuWjoxzb7WcbKNVMlnvnmSUAWuErxsY0PUJNfcuqWiGmEp4d/SWfWPigG6DC\nsDej35BlSfX8FCufYrfC74VNk4yBS2LVYmIqcpqUrfay0I2oZA8+ToLEpdUvEv2I\nl59eOhJO2jsC3JbOyZZmK2Kv7d94fR+1tg2Rq1Wbnmc9AZKq7KDReAlIJh4u2KHb\nBbtF79idusMwZyP777tqSQ4THBMa+VAEc2UrzdZqTIAwqlKQOvO2fRz2P+ARR+Tz\nMYJMdCdmPZ9qAc8U1OcFBG6qDDltO8wf/Nu/PsSI5LGCIhIuPPIuKfm0rRfTqCG7\nQPQPWjRoXtGGhwjdIuWbX9fIB+c+NpAEKHgLtV+Rxj8s5IVxqG9a5TtU9VkfVXJz\nJ20naoz/G+vDsVINpd3kH0ziNvdrKfGRM5UgtnUOPCXB22fVmkIsMH2knI10CKK+\noffI56NTkLRu00xvg98/wdukhkwIAxg6PQI/BHY5mdvoacEHHHdOhMq+GSAh7DDX\nG8+HdbABM1ExkPnZLat15q706ztiuUpQv1C2DI8YviUVkMqCslj4cD4F8EFPo4kr\nkvme0Cuc9Qlf7N5rjdV3cjwavhFx44dyXj9aesft2Q1okPiIqbGNpcjHcIRlj4Au\nMU3Bo0A=\n-----END CERTIFICATE-----`\n\tclient1Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+ki\nJ3X6HUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi\n85xEOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLW\neYl7Qie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4\nZdRfXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dy\nbbhOc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABAoIBAFjSHK7gENVZxphO\nhHg8k9ShnDo8eyDvK8l9Op3U3/yOsXKxolivvyx//7UFmz3vXDahjNHe7YScAXdw\neezbqBXa7xrvghqZzp2HhFYwMJ0210mcdncBKVFzK4ztZHxgQ0PFTqet0R19jZjl\nX3A325/eNZeuBeOied4qb/24AD6JGc6A0J55f5/QUQtdwYwrL15iC/KZXDL90PPJ\nCFJyrSzcXvOMEvOfXIFxhDVKRCppyIYXG7c80gtNC37I6rxxMNQ4mxjwUI2IVhxL\nj+nZDu0JgRZ4NaGjOq2e79QxUVm/GG3z25XgmBFBrXkEVV+sCZE1VDyj6kQfv9FU\nNhOrwGECgYEAzq47r/HwXifuGYBV/mvInFw3BNLrKry+iUZrJ4ms4g+LfOi0BAgf\nsXsWXulpBo2YgYjFdO8G66f69GlB4B7iLscpABXbRtpDZEnchQpaF36/+4g3i8gB\nZ29XHNDB8+7t4wbXvlSnLv1tZWey2fS4hPosc2YlvS87DMmnJMJqhs8CgYEA4oiB\nLGQP6VNdX0Uigmh5fL1g1k95eC8GP1ylczCcIwsb2OkAq0MT7SHRXOlg3leEq4+g\nmCHk1NdjkSYxDL2ZeTKTS/gy4p1jlcDa6Ilwi4pVvatNvu4o80EYWxRNNb1mAn67\nT8TN9lzc6mEi+LepQM3nYJ3F+ZWTKgxH8uoJwMUCgYEArpumE1vbjUBAuEyi2eGn\nRunlFW83fBCfDAxw5KM8anNlja5uvuU6GU/6s06QCxg+2lh5MPPrLdXpfukZ3UVa\nItjg+5B7gx1MSALaiY8YU7cibFdFThM3lHIM72wyH2ogkWcrh0GvSFSUQlJcWCSW\nasmMGiYXBgBL697FFZomMyMCgYEAkAnp0JcDQwHd4gDsk2zoqnckBsDb5J5J46n+\nDYNAFEww9bgZ08u/9MzG+cPu8xFE621U2MbcYLVfuuBE2ewIlPaij/COMmeO9Z59\n0tPpOuDH6eTtd1SptxqR6P+8pEn8feOlKHBj4Z1kXqdK/EiTlwAVeep4Al2oCFls\nujkz4F0CgYAe8vHnVFHlWi16zAqZx4ZZZhNuqPtgFkvPg9LfyNTA4dz7F9xgtUaY\nnXBPyCe/8NtgBfT79HkPiG3TM0xRZY9UZgsJKFtqAu5u4ManuWDnsZI9RK2QTLHe\nyEbH5r3Dg3n9k/3GbjXFIWdU9UaYsdnSKHHtMw9ZODc14LaAogEQug==\n-----END RSA PRIVATE KEY-----`\n\t// client 2 crt is revoked\n\tclient2Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAM5p4duNjikSPxYGaqBm5FEwDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjUyWhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImVjm/b\nQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TPkZua\neq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEKh4LQ\ncr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81PQAmT\nA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu4Ic0\n6tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBR5mf0f\nZjf8ZCGXqU2+45th7VkkLDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEARhFxNAouwbpEfN1M90+ao5rwyxEewerSoCCz\nPQzeUZ66MA/FkS/tFUGgGGG+wERN+WLbe1cN6q/XFr0FSMLuUxLXDNV02oUL/FnY\nxcyNLaZUZ0pP7sA+Hmx2AdTA6baIwQbyIY9RLAaz6hzo1YbI8yeis645F1bxgL2D\nEP5kXa3Obv0tqWByMZtrmJPv3p0W5GJKXVDn51GR/E5KI7pliZX2e0LmMX9mxfPB\n4sXFUggMHXxWMMSAmXPVsxC2KX6gMnajO7JUraTwuGm+6V371FzEX+UKXHI+xSvO\n78TseTIYsBGLjeiA8UjkKlD3T9qsQm2mb2PlKyqjvIm4i2ilM0E2w4JZmd45b925\n7q/QLV3NZ/zZMi6AMyULu28DWKfAx3RLKwnHWSFcR4lVkxQrbDhEUMhAhLAX+2+e\nqc7qZm3dTabi7ZJiiOvYK/yNgFHa/XtZp5uKPB5tigPIa+34hbZF7s2/ty5X3O1N\nf5Ardz7KNsxJjZIt6HvB28E/PPOvBqCKJc1Y08J9JbZi8p6QS1uarGoR7l7rT1Hv\n/ZXkNTw2bw1VpcWdzDBLLVHYNnJmS14189LVk11PcJJpSmubwCqg+ZZULdgtVr3S\nANas2dgMPVwXhnAalgkcc+lb2QqaEz06axfbRGBsgnyqR5/koKCg1Hr0+vThHSsR\nE0+r2+4=\n-----END CERTIFICATE-----`\n\tclient2Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImV\njm/bQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TP\nkZuaeq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEK\nh4LQcr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81P\nQAmTA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu\n4Ic06tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABAoIBAQCMnEeg9uXQmdvq\nop4qi6bV+ZcDWvvkLwvHikFMnYpIaheYBpF2ZMKzdmO4xgCSWeFCQ4Hah8KxfHCM\nqLuWvw2bBBE5J8yQ/JaPyeLbec7RX41GQ2YhPoxDdP0PdErREdpWo4imiFhH/Ewt\nRvq7ufRdpdLoS8dzzwnvX3r+H2MkHoC/QANW2AOuVoZK5qyCH5N8yEAAbWKaQaeL\nVBhAYEVKbAkWEtXw7bYXzxRR7WIM3f45v3ncRusDIG+Hf75ZjatoH0lF1gHQNofO\nqkCVZVzjkLFuzDic2KZqsNORglNs4J6t5Dahb9v3hnoK963YMnVSUjFvqQ+/RZZy\nVILFShilAoGBANucwZU61eJ0tLKBYEwmRY/K7Gu1MvvcYJIOoX8/BL3zNmNO0CLl\nNiABtNt9WOVwZxDsxJXdo1zvMtAegNqS6W11R1VAZbL6mQ/krScbLDE6JKA5DmA7\n4nNi1gJOW1ziAfdBAfhe4cLbQOb94xkOK5xM1YpO0xgDJLwrZbehDMmPAoGBAMAl\n/owPDAvcXz7JFynT0ieYVc64MSFiwGYJcsmxSAnbEgQ+TR5FtkHYe91OSqauZcCd\naoKXQNyrYKIhyounRPFTdYQrlx6KtEs7LU9wOxuphhpJtGjRnhmA7IqvX703wNvu\nkhrEavn86G5boH8R80371SrN0Rh9UeAlQGuNBdvfAoGAEAmokW9Ug08miwqrr6Pz\n3IZjMZJwALidTM1IufQuMnj6ddIhnQrEIx48yPKkdUz6GeBQkuk2rujA+zXfDxc/\neMDhzrX/N0zZtLFse7ieR5IJbrH7/MciyG5lVpHGVkgjAJ18uVikgAhm+vd7iC7i\nvG1YAtuyysQgAKXircBTIL0CgYAHeTLWVbt9NpwJwB6DhPaWjalAug9HIiUjktiB\nGcEYiQnBWn77X3DATOA8clAa/Yt9m2HKJIHkU1IV3ESZe+8Fh955PozJJlHu3yVb\nAp157PUHTriSnxyMF2Sb3EhX/rQkmbnbCqqygHC14iBy8MrKzLG00X6BelZV5n0D\n8d85dwKBgGWY2nsaemPH/TiTVF6kW1IKSQoIyJChkngc+Xj/2aCCkkmAEn8eqncl\nRKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA\nubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl\n-----END RSA PRIVATE KEY-----`\n)\n\nvar (\n\tconfigDir = filepath.Join(\".\", \"..\", \"..\")\n)\n\ntype mockFTPClientContext struct {\n\tlastDataChannel ftpserver.DataChannel\n\tremoteIP        string\n\tlocalIP         string\n\textra           any\n}\n\nfunc (cc *mockFTPClientContext) Path() string {\n\treturn \"\"\n}\n\nfunc (cc *mockFTPClientContext) SetPath(_ string) {}\n\nfunc (cc *mockFTPClientContext) SetListPath(_ string) {}\n\nfunc (cc *mockFTPClientContext) SetDebug(_ bool) {}\n\nfunc (cc *mockFTPClientContext) Debug() bool {\n\treturn false\n}\n\nfunc (cc *mockFTPClientContext) ID() uint32 {\n\treturn 1\n}\n\nfunc (cc *mockFTPClientContext) RemoteAddr() net.Addr {\n\tip := \"127.0.0.1\"\n\tif cc.remoteIP != \"\" {\n\t\tip = cc.remoteIP\n\t}\n\treturn &net.IPAddr{IP: net.ParseIP(ip)}\n}\n\nfunc (cc *mockFTPClientContext) LocalAddr() net.Addr {\n\tip := \"127.0.0.1\"\n\tif cc.localIP != \"\" {\n\t\tip = cc.localIP\n\t}\n\treturn &net.IPAddr{IP: net.ParseIP(ip)}\n}\n\nfunc (cc *mockFTPClientContext) GetClientVersion() string {\n\treturn \"mock version\"\n}\n\nfunc (cc *mockFTPClientContext) Close() error {\n\treturn nil\n}\n\nfunc (cc *mockFTPClientContext) HasTLSForControl() bool {\n\treturn false\n}\n\nfunc (cc *mockFTPClientContext) HasTLSForTransfers() bool {\n\treturn false\n}\n\nfunc (cc *mockFTPClientContext) SetTLSRequirement(_ ftpserver.TLSRequirement) error {\n\treturn nil\n}\n\nfunc (cc *mockFTPClientContext) GetLastCommand() string {\n\treturn \"\"\n}\n\nfunc (cc *mockFTPClientContext) GetLastDataChannel() ftpserver.DataChannel {\n\treturn cc.lastDataChannel\n}\n\nfunc (cc *mockFTPClientContext) SetExtra(extra any) {\n\tcc.extra = extra\n}\n\nfunc (cc *mockFTPClientContext) Extra() any {\n\treturn cc.extra\n}\n\n// MockOsFs mockable OsFs\ntype MockOsFs struct {\n\tvfs.Fs\n\terr                     error\n\tstatErr                 error\n\tisAtomicUploadSupported bool\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs MockOsFs) Name() string {\n\treturn \"mockOsFs\"\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported\nfunc (MockOsFs) IsUploadResumeSupported() bool {\n\treturn false\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (MockOsFs) IsConditionalUploadResumeSupported(_ int64) bool {\n\treturn false\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported\nfunc (fs MockOsFs) IsAtomicUploadSupported() bool {\n\treturn fs.isAtomicUploadSupported\n}\n\n// Stat returns a FileInfo describing the named file\nfunc (fs MockOsFs) Stat(name string) (os.FileInfo, error) {\n\tif fs.statErr != nil {\n\t\treturn nil, fs.statErr\n\t}\n\treturn os.Stat(name)\n}\n\n// Lstat returns a FileInfo describing the named file\nfunc (fs MockOsFs) Lstat(name string) (os.FileInfo, error) {\n\tif fs.statErr != nil {\n\t\treturn nil, fs.statErr\n\t}\n\treturn os.Lstat(name)\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (fs MockOsFs) Remove(name string, _ bool) error {\n\tif fs.err != nil {\n\t\treturn fs.err\n\t}\n\treturn os.Remove(name)\n}\n\n// Rename renames (moves) source to target\nfunc (fs MockOsFs) Rename(source, target string, _ int) (int, int64, error) {\n\tif fs.err != nil {\n\t\treturn -1, -1, fs.err\n\t}\n\terr := os.Rename(source, target)\n\treturn -1, -1, err\n}\n\nfunc newMockOsFs(err, statErr error, atomicUpload bool, connectionID, rootDir string) vfs.Fs {\n\treturn &MockOsFs{\n\t\tFs:                      vfs.NewOsFs(connectionID, rootDir, \"\", nil),\n\t\terr:                     err,\n\t\tstatErr:                 statErr,\n\t\tisAtomicUploadSupported: atomicUpload,\n\t}\n}\n\nfunc TestInitialization(t *testing.T) {\n\toldMgr := certMgr\n\tcertMgr = nil\n\n\tbinding := Binding{\n\t\tPort: 2121,\n\t}\n\tc := &Configuration{\n\t\tBindings:           []Binding{binding},\n\t\tCertificateFile:    \"acert\",\n\t\tCertificateKeyFile: \"akey\",\n\t}\n\tassert.False(t, binding.HasProxy())\n\tassert.Equal(t, util.I18nFTPTLSDisabled, binding.GetTLSDescription())\n\terr := c.Initialize(configDir)\n\tassert.Error(t, err)\n\tc.CertificateFile = \"\"\n\tc.CertificateKeyFile = \"\"\n\tc.BannerFile = \"afile\"\n\tserver := NewServer(c, configDir, binding, 0)\n\tassert.Equal(t, version.GetServerVersion(\"_\", false), server.initialMsg)\n\t_, err = server.GetTLSConfig()\n\tassert.Error(t, err)\n\n\tbinding.TLSMode = 1\n\tserver = NewServer(c, configDir, binding, 0)\n\t_, err = server.GetSettings()\n\tassert.Error(t, err)\n\n\tbinding.PassiveConnectionsSecurity = 100\n\tbinding.ActiveConnectionsSecurity = 100\n\tserver = NewServer(c, configDir, binding, 0)\n\t_, err = server.GetSettings()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid passive_connections_security\")\n\t}\n\tbinding.PassiveConnectionsSecurity = 1\n\tserver = NewServer(c, configDir, binding, 0)\n\t_, err = server.GetSettings()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid active_connections_security\")\n\t}\n\tbinding = Binding{\n\t\tPort:           2121,\n\t\tForcePassiveIP: \"192.168.1\",\n\t}\n\tserver = NewServer(c, configDir, binding, 0)\n\t_, err = server.GetSettings()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is not valid\")\n\t}\n\n\tbinding.ForcePassiveIP = \"::ffff:192.168.89.9\"\n\terr = binding.checkPassiveIP()\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"192.168.89.9\", binding.ForcePassiveIP)\n\n\tbinding.ForcePassiveIP = \"::1\"\n\terr = binding.checkPassiveIP()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is not a valid IPv4 address\")\n\t}\n\n\terr = ReloadCertificateMgr()\n\tassert.NoError(t, err)\n\n\tbinding = Binding{\n\t\tPort:           2121,\n\t\tClientAuthType: 1,\n\t}\n\tassert.Equal(t, util.I18nFTPTLSDisabled, binding.GetTLSDescription())\n\tcertPath := filepath.Join(os.TempDir(), \"test_ftpd.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test_ftpd.key\")\n\tbinding.CertificateFile = certPath\n\tbinding.CertificateKeyFile = keyPath\n\tkeyPairs := []common.TLSKeyPair{\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   binding.GetAddress(),\n\t\t},\n\t}\n\tcertMgr, err = common.NewCertManager(keyPairs, configDir, \"\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, util.I18nFTPTLSMixed, binding.GetTLSDescription())\n\tserver = NewServer(c, configDir, binding, 0)\n\tcfg, err := server.GetTLSConfig()\n\trequire.NoError(t, err)\n\tassert.Equal(t, tls.RequireAndVerifyClientCert, cfg.ClientAuth)\n\n\tcertMgr = oldMgr\n}\n\nfunc TestServerGetSettings(t *testing.T) {\n\toldConfig := common.Config\n\toldMgr := certMgr\n\n\tbinding := Binding{\n\t\tPort:             2121,\n\t\tApplyProxyConfig: true,\n\t}\n\tc := &Configuration{\n\t\tBindings: []Binding{binding},\n\t\tPassivePortRange: PortRange{\n\t\t\tStart: 10000,\n\t\t\tEnd:   10000,\n\t\t},\n\t}\n\tassert.False(t, binding.HasProxy())\n\tserver := NewServer(c, configDir, binding, 0)\n\tsettings, err := server.GetSettings()\n\tassert.NoError(t, err)\n\tif ranger, ok := settings.PassiveTransferPortRange.(*ftpserver.PortRange); ok {\n\t\tassert.Equal(t, 10000, ranger.Start)\n\t\tassert.Equal(t, 10000, ranger.End)\n\t}\n\tc.PassivePortRange.End = 11000\n\tsettings, err = server.GetSettings()\n\tassert.NoError(t, err)\n\tif ranger, ok := settings.PassiveTransferPortRange.(*ftpserver.PortRange); ok {\n\t\tassert.Equal(t, 10000, ranger.Start)\n\t\tassert.Equal(t, 11000, ranger.End)\n\t}\n\n\tcommon.Config.ProxyProtocol = 1\n\t_, err = server.GetSettings()\n\tassert.Error(t, err)\n\tserver.binding.Port = 8021\n\n\tassert.Equal(t, util.I18nFTPTLSDisabled, binding.GetTLSDescription())\n\t_, err = server.GetTLSConfig()\n\tassert.Error(t, err) // TLS configured but cert manager has no certificate\n\n\tbinding.TLSMode = 1\n\tassert.Equal(t, util.I18nFTPTLSExplicit, binding.GetTLSDescription())\n\n\tbinding.TLSMode = 2\n\tassert.Equal(t, util.I18nFTPTLSImplicit, binding.GetTLSDescription())\n\n\tcertPath := filepath.Join(os.TempDir(), \"test_ftpd.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test_ftpd.key\")\n\terr = os.WriteFile(certPath, []byte(ftpsCert), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyPath, []byte(ftpsKey), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tkeyPairs := []common.TLSKeyPair{\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t},\n\t}\n\tcertMgr, err = common.NewCertManager(keyPairs, configDir, \"\")\n\trequire.NoError(t, err)\n\tcommon.Config.ProxyAllowed = nil\n\tc.CertificateFile = certPath\n\tc.CertificateKeyFile = keyPath\n\tserver = NewServer(c, configDir, binding, 0)\n\tserver.binding.Port = 9021\n\tsettings, err = server.GetSettings()\n\tassert.NoError(t, err)\n\tassert.NotNil(t, settings.Listener)\n\n\tlistener, err := net.Listen(\"tcp\", \":0\")\n\tassert.NoError(t, err)\n\tlistener, err = server.WrapPassiveListener(listener)\n\tassert.NoError(t, err)\n\n\t_, ok := listener.(*proxyproto.Listener)\n\tassert.True(t, ok)\n\n\tcommon.Config = oldConfig\n\tcertMgr = oldMgr\n}\n\nfunc TestUserInvalidParams(t *testing.T) {\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: \"invalid\",\n\t\t},\n\t}\n\tbinding := Binding{\n\t\tPort: 2121,\n\t}\n\tc := &Configuration{\n\t\tBindings: []Binding{binding},\n\t\tPassivePortRange: PortRange{\n\t\t\tStart: 10000,\n\t\t\tEnd:   11000,\n\t\t},\n\t}\n\tserver := NewServer(c, configDir, binding, 3)\n\t_, err := server.validateUser(u, &mockFTPClientContext{}, dataprovider.LoginMethodPassword)\n\tassert.Error(t, err)\n\n\tu.Username = \"a\"\n\tu.HomeDir = filepath.Clean(os.TempDir())\n\tsubDir := \"subdir\"\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir1\", subDir)\n\tvdirPath2 := \"/vdir2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t})\n\t_, err = server.validateUser(u, &mockFTPClientContext{}, dataprovider.LoginMethodPassword)\n\tassert.Error(t, err)\n\tu.VirtualFolders = nil\n\t_, err = server.validateUser(u, &mockFTPClientContext{}, dataprovider.LoginMethodPassword)\n\tassert.Error(t, err)\n}\n\nfunc TestFTPMode(t *testing.T) {\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolFTP, \"\", \"\", dataprovider.User{}),\n\t}\n\tassert.Empty(t, connection.getFTPMode())\n\tconnection.clientContext = &mockFTPClientContext{lastDataChannel: ftpserver.DataChannelActive}\n\tassert.Equal(t, \"active\", connection.getFTPMode())\n\tconnection.clientContext = &mockFTPClientContext{lastDataChannel: ftpserver.DataChannelPassive}\n\tassert.Equal(t, \"passive\", connection.getFTPMode())\n\tconnection.clientContext = &mockFTPClientContext{lastDataChannel: 0}\n\tassert.Empty(t, connection.getFTPMode())\n}\n\nfunc TestClientVersion(t *testing.T) {\n\tmockCC := &mockFTPClientContext{}\n\tconnID := fmt.Sprintf(\"2_%v\", mockCC.ID())\n\tuser := dataprovider.User{}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, \"\", \"\", user),\n\t\tclientContext:  mockCC,\n\t}\n\terr := common.Connections.Add(connection)\n\tassert.NoError(t, err)\n\tstats := common.Connections.GetStats(\"\")\n\tif assert.Len(t, stats, 1) {\n\t\tassert.Equal(t, \"mock version\", stats[0].ClientVersion)\n\t\tcommon.Connections.Remove(connection.GetID())\n\t}\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestDriverMethodsNotImplemented(t *testing.T) {\n\tmockCC := &mockFTPClientContext{}\n\tconnID := fmt.Sprintf(\"2_%v\", mockCC.ID())\n\tuser := dataprovider.User{}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, \"\", \"\", user),\n\t\tclientContext:  mockCC,\n\t}\n\t_, err := connection.Create(\"\")\n\tassert.EqualError(t, err, errNotImplemented.Error())\n\terr = connection.MkdirAll(\"\", os.ModePerm)\n\tassert.EqualError(t, err, errNotImplemented.Error())\n\t_, err = connection.Open(\"\")\n\tassert.EqualError(t, err, errNotImplemented.Error())\n\t_, err = connection.OpenFile(\"\", 0, os.ModePerm)\n\tassert.EqualError(t, err, errNotImplemented.Error())\n\terr = connection.RemoveAll(\"\")\n\tassert.EqualError(t, err, errNotImplemented.Error())\n\tassert.Equal(t, connection.GetID(), connection.Name())\n}\n\nfunc TestExtraData(t *testing.T) {\n\tmockCC := mockFTPClientContext{}\n\t_, ok := mockCC.Extra().(*tlsState)\n\trequire.False(t, ok)\n\tmockCC.SetExtra(&tlsState{\n\t\tLoginWithMutualTLS: false,\n\t\tVersion:            tls.VersionName(tls.VersionTLS13),\n\t\tCipher:             tls.CipherSuiteName(tls.TLS_AES_128_GCM_SHA256),\n\t\tKEX:                tls.X25519MLKEM768.String(),\n\t})\n\tstate, ok := mockCC.Extra().(*tlsState)\n\trequire.True(t, ok)\n\trequire.False(t, state.LoginWithMutualTLS)\n\trequire.Equal(t, tls.VersionName(tls.VersionTLS13), state.Version)\n\trequire.Equal(t, tls.CipherSuiteName(tls.TLS_AES_128_GCM_SHA256), state.Cipher)\n\trequire.Equal(t, tls.X25519MLKEM768.String(), state.KEX)\n\tmockCC.SetExtra(&tlsState{\n\t\tLoginWithMutualTLS: true,\n\t})\n\tstate, ok = mockCC.Extra().(*tlsState)\n\trequire.True(t, ok)\n\trequire.True(t, state.LoginWithMutualTLS)\n}\n\nfunc TestResolvePathErrors(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: \"invalid\",\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tmockCC := &mockFTPClientContext{}\n\tconnID := fmt.Sprintf(\"%v\", mockCC.ID())\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, \"\", \"\", user),\n\t\tclientContext:  mockCC,\n\t}\n\terr := connection.Mkdir(\"\", os.ModePerm)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\terr = connection.Remove(\"\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\terr = connection.RemoveDir(\"\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\terr = connection.Rename(\"\", \"\")\n\tassert.ErrorIs(t, err, common.ErrOpUnsupported)\n\terr = connection.Symlink(\"\", \"\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\t_, err = connection.Stat(\"\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\terr = connection.Chmod(\"\", os.ModePerm)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\terr = connection.Chtimes(\"\", time.Now(), time.Now())\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\t_, err = connection.ReadDir(\"\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\t_, err = connection.GetHandle(\"\", 0, 0)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\t_, err = connection.GetAvailableSpace(\"\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n}\n\nfunc TestUploadFileStatError(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"user\",\n\t\t\tHomeDir:  filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tmockCC := &mockFTPClientContext{}\n\tconnID := fmt.Sprintf(\"%v\", mockCC.ID())\n\tfs := vfs.NewOsFs(connID, user.HomeDir, \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, \"\", \"\", user),\n\t\tclientContext:  mockCC,\n\t}\n\ttestFile := filepath.Join(user.HomeDir, \"test\", \"testfile\")\n\terr := os.MkdirAll(filepath.Dir(testFile), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(testFile, []byte(\"data\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.Chmod(filepath.Dir(testFile), 0001)\n\tassert.NoError(t, err)\n\t_, err = connection.uploadFile(fs, testFile, \"test\", 0)\n\tassert.Error(t, err)\n\terr = os.Chmod(filepath.Dir(testFile), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(filepath.Dir(testFile))\n\tassert.NoError(t, err)\n}\n\nfunc TestAVBLErrors(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"user\",\n\t\t\tHomeDir:  filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tmockCC := &mockFTPClientContext{}\n\tconnID := fmt.Sprintf(\"%v\", mockCC.ID())\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, \"\", \"\", user),\n\t\tclientContext:  mockCC,\n\t}\n\t_, err := connection.GetAvailableSpace(\"/\")\n\tassert.NoError(t, err)\n\t_, err = connection.GetAvailableSpace(\"/missing-path\")\n\tassert.Error(t, err)\n\tassert.True(t, errors.Is(err, fs.ErrNotExist))\n}\n\nfunc TestUploadOverwriteErrors(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"user\",\n\t\t\tHomeDir:  filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tmockCC := &mockFTPClientContext{}\n\tconnID := fmt.Sprintf(\"%v\", mockCC.ID())\n\tfs := newMockOsFs(nil, nil, false, connID, user.GetHomeDir())\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, \"\", \"\", user),\n\t\tclientContext:  mockCC,\n\t}\n\tflags := 0\n\tflags |= os.O_APPEND\n\t_, err := connection.handleFTPUploadToExistingFile(fs, flags, \"\", \"\", 0, \"\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrOpUnsupported.Error())\n\t}\n\n\tf, err := os.CreateTemp(\"\", \"temp\")\n\tassert.NoError(t, err)\n\terr = f.Close()\n\tassert.NoError(t, err)\n\tflags = 0\n\tflags |= os.O_CREATE\n\tflags |= os.O_TRUNC\n\ttr, err := connection.handleFTPUploadToExistingFile(fs, flags, f.Name(), f.Name(), 123, f.Name())\n\tif assert.NoError(t, err) {\n\t\ttransfer := tr.(*transfer)\n\t\ttransfers := connection.GetTransfers()\n\t\tif assert.Equal(t, 1, len(transfers)) {\n\t\t\tassert.Equal(t, transfers[0].ID, transfer.GetID())\n\t\t\tassert.Equal(t, int64(123), transfer.InitialSize)\n\t\t\terr = transfer.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 0, len(connection.GetTransfers()))\n\t\t}\n\t}\n\terr = os.Remove(f.Name())\n\tassert.NoError(t, err)\n\n\t_, err = connection.handleFTPUploadToExistingFile(fs, os.O_TRUNC, filepath.Join(os.TempDir(), \"sub\", \"file\"),\n\t\tfilepath.Join(os.TempDir(), \"sub\", \"file1\"), 0, \"/sub/file1\")\n\tassert.Error(t, err)\n\tfs = vfs.NewOsFs(connID, user.GetHomeDir(), \"\", nil)\n\t_, err = connection.handleFTPUploadToExistingFile(fs, 0, \"missing1\", \"missing2\", 0, \"missing\")\n\tassert.Error(t, err)\n}\n\nfunc TestTransferErrors(t *testing.T) {\n\ttestfile := \"testfile\"\n\tfile, err := os.Create(testfile)\n\tassert.NoError(t, err)\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"user\",\n\t\t\tHomeDir:  filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tmockCC := &mockFTPClientContext{}\n\tconnID := fmt.Sprintf(\"%v\", mockCC.ID())\n\tfs := newMockOsFs(nil, nil, false, connID, user.GetHomeDir())\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, \"\", \"\", user),\n\t}\n\tbaseTransfer := common.NewBaseTransfer(file, connection.BaseConnection, nil, file.Name(), file.Name(), testfile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\ttr := newTransfer(baseTransfer, nil, nil, 0)\n\terr = tr.Close()\n\tassert.NoError(t, err)\n\t_, err = tr.Seek(10, 0)\n\tassert.Error(t, err)\n\tbuf := make([]byte, 64)\n\t_, err = tr.Read(buf)\n\tassert.Error(t, err)\n\terr = tr.Close()\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrTransferClosed.Error())\n\t}\n\tassert.Len(t, connection.GetTransfers(), 0)\n\n\tr, _, err := pipeat.Pipe()\n\tassert.NoError(t, err)\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testfile, testfile, testfile,\n\t\tcommon.TransferUpload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\ttr = newTransfer(baseTransfer, nil, vfs.NewPipeReader(r), 10)\n\tpos, err := tr.Seek(10, 0)\n\tassert.NoError(t, err)\n\tassert.Equal(t, pos, tr.expectedOffset)\n\terr = tr.closeIO()\n\tassert.NoError(t, err)\n\n\tr, w, err := pipeat.Pipe()\n\tassert.NoError(t, err)\n\tpipeWriter := vfs.NewPipeWriter(w)\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testfile, testfile, testfile,\n\t\tcommon.TransferUpload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\ttr.Connection.RemoveTransfer(tr)\n\ttr = newTransfer(baseTransfer, pipeWriter, nil, 0)\n\n\terr = r.Close()\n\tassert.NoError(t, err)\n\terrFake := fmt.Errorf(\"fake upload error\")\n\tgo func() {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tpipeWriter.Done(errFake)\n\t}()\n\terr = tr.closeIO()\n\tassert.EqualError(t, err, errFake.Error())\n\t_, err = tr.Seek(1, 0)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrOpUnsupported.Error())\n\t}\n\ttr.Connection.RemoveTransfer(tr)\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n}\n\nfunc TestVerifyTLSConnection(t *testing.T) {\n\toldCertMgr := certMgr\n\n\tcaCrlPath := filepath.Join(os.TempDir(), \"testcrl.crt\")\n\tcertPath := filepath.Join(os.TempDir(), \"test.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test.key\")\n\terr := os.WriteFile(caCrlPath, []byte(caCRL), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(certPath, []byte(ftpsCert), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyPath, []byte(ftpsKey), os.ModePerm)\n\tassert.NoError(t, err)\n\tkeyPairs := []common.TLSKeyPair{\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t},\n\t}\n\tcertMgr, err = common.NewCertManager(keyPairs, \"\", \"ftp_test\")\n\tassert.NoError(t, err)\n\n\tcertMgr.SetCARevocationLists([]string{caCrlPath})\n\terr = certMgr.LoadCRLs()\n\tassert.NoError(t, err)\n\n\tcrt, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\tx509crt, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tserver := Server{}\n\tstate := tls.ConnectionState{\n\t\tPeerCertificates: []*x509.Certificate{x509crt},\n\t}\n\n\terr = server.verifyTLSConnection(state)\n\tassert.Error(t, err) // no verified certification chain\n\terr = server.VerifyTLSConnectionState(nil, state)\n\tassert.NoError(t, err)\n\tserver.binding.ClientAuthType = 1\n\terr = server.VerifyTLSConnectionState(nil, state)\n\tassert.Error(t, err)\n\n\tcrt, err = tls.X509KeyPair([]byte(caCRT), []byte(caKey))\n\tassert.NoError(t, err)\n\n\tx509CAcrt, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tstate.VerifiedChains = append(state.VerifiedChains, []*x509.Certificate{x509crt, x509CAcrt})\n\terr = server.verifyTLSConnection(state)\n\tassert.NoError(t, err)\n\n\tcrt, err = tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))\n\tassert.NoError(t, err)\n\tx509crtRevoked, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tstate.VerifiedChains = append(state.VerifiedChains, []*x509.Certificate{x509crtRevoked, x509CAcrt})\n\tstate.PeerCertificates = []*x509.Certificate{x509crtRevoked}\n\terr = server.verifyTLSConnection(state)\n\tassert.EqualError(t, err, common.ErrCrtRevoked.Error())\n\n\terr = os.Remove(caCrlPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(certPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n\n\tcertMgr = oldCertMgr\n}\n\nfunc TestCiphers(t *testing.T) {\n\tb := Binding{\n\t\tTLSCipherSuites: []string{},\n\t}\n\tb.setCiphers()\n\trequire.Equal(t, util.GetTLSCiphersFromNames(nil), b.ciphers)\n\tb.TLSCipherSuites = []string{\"TLS_AES_128_GCM_SHA256\", \"TLS_AES_256_GCM_SHA384\"}\n\tb.setCiphers()\n\trequire.Len(t, b.ciphers, 2)\n\trequire.Equal(t, []uint16{tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384}, b.ciphers)\n}\n\nfunc TestPassiveIPResolver(t *testing.T) {\n\tb := Binding{\n\t\tPassiveIPOverrides: []PassiveIPOverride{\n\t\t\t{},\n\t\t},\n\t}\n\terr := b.checkPassiveIP()\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"passive IP networks override cannot be empty\")\n\tb = Binding{\n\t\tPassiveIPOverrides: []PassiveIPOverride{\n\t\t\t{\n\t\t\t\tIP: \"invalid ip\",\n\t\t\t},\n\t\t},\n\t}\n\terr = b.checkPassiveIP()\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"is not valid\")\n\n\tb = Binding{\n\t\tPassiveIPOverrides: []PassiveIPOverride{\n\t\t\t{\n\t\t\t\tIP:       \"192.168.1.1\",\n\t\t\t\tNetworks: []string{\"192.168.1.0/24\", \"invalid cidr\"},\n\t\t\t},\n\t\t},\n\t}\n\terr = b.checkPassiveIP()\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"invalid passive IP networks override\")\n\tb = Binding{\n\t\tForcePassiveIP: \"192.168.2.1\",\n\t\tPassiveIPOverrides: []PassiveIPOverride{\n\t\t\t{\n\t\t\t\tIP:       \"::ffff:192.168.1.1\",\n\t\t\t\tNetworks: []string{\"192.168.1.0/24\"},\n\t\t\t},\n\t\t},\n\t}\n\terr = b.checkPassiveIP()\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, b.PassiveIPOverrides[0].GetNetworksAsString())\n\tassert.Equal(t, \"192.168.1.1\", b.PassiveIPOverrides[0].IP)\n\trequire.Len(t, b.PassiveIPOverrides[0].parsedNetworks, 1)\n\tip := net.ParseIP(\"192.168.1.2\")\n\tassert.True(t, b.PassiveIPOverrides[0].parsedNetworks[0](ip))\n\tip = net.ParseIP(\"192.168.0.2\")\n\tassert.False(t, b.PassiveIPOverrides[0].parsedNetworks[0](ip))\n\n\tmockCC := &mockFTPClientContext{\n\t\tremoteIP: \"192.168.1.10\",\n\t\tlocalIP:  \"192.168.1.3\",\n\t}\n\tpassiveIP, err := b.passiveIPResolver(mockCC)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"192.168.1.1\", passiveIP)\n\tb.PassiveIPOverrides[0].IP = \"\"\n\tpassiveIP, err = b.passiveIPResolver(mockCC)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"192.168.1.3\", passiveIP)\n\tmockCC.remoteIP = \"172.16.2.3\"\n\tpassiveIP, err = b.passiveIPResolver(mockCC)\n\tassert.NoError(t, err)\n\tassert.Equal(t, b.ForcePassiveIP, passiveIP)\n}\n\nfunc TestRelativePath(t *testing.T) {\n\trel := getPathRelativeTo(\"/testpath\", \"/testpath\")\n\tassert.Empty(t, rel)\n\trel = getPathRelativeTo(\"/\", \"/\")\n\tassert.Empty(t, rel)\n\trel = getPathRelativeTo(\"/\", \"/dir/sub\")\n\tassert.Equal(t, \"dir/sub\", rel)\n\trel = getPathRelativeTo(\"./\", \"/dir/sub\")\n\tassert.Equal(t, \"/dir/sub\", rel)\n\trel = getPathRelativeTo(\"/sub\", \"/dir/sub\")\n\tassert.Equal(t, \"../dir/sub\", rel)\n\trel = getPathRelativeTo(\"/dir\", \"/dir/sub\")\n\tassert.Equal(t, \"sub\", rel)\n\trel = getPathRelativeTo(\"/dir/sub\", \"/dir\")\n\tassert.Equal(t, \"../\", rel)\n\trel = getPathRelativeTo(\"dir\", \"/dir1\")\n\tassert.Equal(t, \"/dir1\", rel)\n\trel = getPathRelativeTo(\"\", \"/dir2\")\n\tassert.Equal(t, \"dir2\", rel)\n\trel = getPathRelativeTo(\".\", \"/dir2\")\n\tassert.Equal(t, \"/dir2\", rel)\n\trel = getPathRelativeTo(\"/dir3\", \"dir3\")\n\tassert.Equal(t, \"dir3\", rel)\n}\n\nfunc TestConfigsFromProvider(t *testing.T) {\n\terr := dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tc := Configuration{}\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tconfigs := dataprovider.Configs{\n\t\tACME: &dataprovider.ACMEConfigs{\n\t\t\tDomain:          \"domain.com\",\n\t\t\tEmail:           \"info@domain.com\",\n\t\t\tHTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: 80},\n\t\t\tProtocols:       2,\n\t\t},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tutil.CertsBasePath = \"\"\n\t// crt and key empty\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tutil.CertsBasePath = filepath.Clean(os.TempDir())\n\t// crt not found\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs := c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\tcrtPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+\".crt\")\n\terr = os.WriteFile(crtPath, nil, 0666)\n\tassert.NoError(t, err)\n\t// key not found\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\tkeyPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+\".key\")\n\terr = os.WriteFile(keyPath, nil, 0666)\n\tassert.NoError(t, err)\n\t// acme cert used\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Equal(t, configs.ACME.Domain, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 1)\n\t// protocols does not match\n\tconfigs.ACME.Protocols = 5\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tc.acmeDomain = \"\"\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\n\terr = os.Remove(crtPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n\tutil.CertsBasePath = \"\"\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestPassiveHost(t *testing.T) {\n\tb := Binding{\n\t\tPassiveHost: \"invalid hostname\",\n\t}\n\t_, err := b.getPassiveIP(nil)\n\tassert.Error(t, err)\n\tb.PassiveHost = \"localhost\"\n\tip, err := b.getPassiveIP(nil)\n\tassert.NoError(t, err, ip)\n\tassert.Equal(t, \"127.0.0.1\", ip)\n}\n"
  },
  {
    "path": "internal/ftpd/server.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage ftpd\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\tftpserver \"github.com/fclairamb/ftpserverlib\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\n// tlsState tracks TLS connection state for a client\ntype tlsState struct {\n\t// LoginWithMutualTLS indicates whether the user logged in using TLS certificate authentication\n\tLoginWithMutualTLS bool\n\tVersion            string\n\tCipher             string\n\tKEX                string\n}\n\n// Server implements the ftpserverlib MainDriver interface\ntype Server struct {\n\tID           int\n\tconfig       *Configuration\n\tinitialMsg   string\n\tstatusBanner string\n\tbinding      Binding\n\ttlsConfig    *tls.Config\n}\n\n// NewServer returns a new FTP server driver\nfunc NewServer(config *Configuration, configDir string, binding Binding, id int) *Server {\n\tbinding.setCiphers()\n\tvers := version.GetServerVersion(\"_\", false)\n\tserver := &Server{\n\t\tconfig:       config,\n\t\tinitialMsg:   vers,\n\t\tstatusBanner: fmt.Sprintf(\"%s FTP Server\", vers),\n\t\tbinding:      binding,\n\t\tID:           id,\n\t}\n\tif config.BannerFile != \"\" {\n\t\tbannerFilePath := config.BannerFile\n\t\tif !filepath.IsAbs(bannerFilePath) {\n\t\t\tbannerFilePath = filepath.Join(configDir, bannerFilePath)\n\t\t}\n\t\tbannerContent, err := os.ReadFile(bannerFilePath)\n\t\tif err == nil {\n\t\t\tserver.initialMsg = util.BytesToString(bannerContent)\n\t\t} else {\n\t\t\tlogger.WarnToConsole(\"unable to read FTPD banner file: %v\", err)\n\t\t\tlogger.Warn(logSender, \"\", \"unable to read banner file: %v\", err)\n\t\t}\n\t}\n\tserver.buildTLSConfig()\n\treturn server\n}\n\n// GetSettings returns FTP server settings\nfunc (s *Server) GetSettings() (*ftpserver.Settings, error) {\n\tif err := s.binding.checkPassiveIP(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := s.binding.checkSecuritySettings(); err != nil {\n\t\treturn nil, err\n\t}\n\tvar portRange *ftpserver.PortRange\n\tif s.config.PassivePortRange.Start > 0 && s.config.PassivePortRange.End >= s.config.PassivePortRange.Start {\n\t\tportRange = &ftpserver.PortRange{\n\t\t\tStart: s.config.PassivePortRange.Start,\n\t\t\tEnd:   s.config.PassivePortRange.End,\n\t\t}\n\t}\n\tvar ftpListener net.Listener\n\tif s.binding.HasProxy() {\n\t\tlistener, err := net.Listen(\"tcp\", s.binding.GetAddress())\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"error starting listener on address %v: %v\", s.binding.GetAddress(), err)\n\t\t\treturn nil, err\n\t\t}\n\t\tftpListener, err = common.Config.GetProxyListener(listener)\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"error enabling proxy listener: %v\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tif s.binding.TLSMode == 2 && s.tlsConfig != nil {\n\t\t\tftpListener = tls.NewListener(ftpListener, s.tlsConfig)\n\t\t}\n\t}\n\n\tif !s.binding.isTLSModeValid() {\n\t\treturn nil, fmt.Errorf(\"unsupported TLS mode: %d\", s.binding.TLSMode)\n\t}\n\n\tif s.binding.TLSMode > 0 && certMgr == nil {\n\t\treturn nil, errors.New(\"to enable TLS you need to provide a certificate\")\n\t}\n\n\tsettings := &ftpserver.Settings{\n\t\tListener:                ftpListener,\n\t\tListenAddr:              s.binding.GetAddress(),\n\t\tPublicIPResolver:        s.binding.passiveIPResolver,\n\t\tActiveTransferPortNon20: s.config.ActiveTransfersPortNon20,\n\t\tIdleTimeout:             -1,\n\t\tConnectionTimeout:       20,\n\t\tBanner:                  s.statusBanner,\n\t\tTLSRequired:             ftpserver.TLSRequirement(s.binding.TLSMode),\n\t\tDisableSite:             !s.config.EnableSite,\n\t\tDisableActiveMode:       s.config.DisableActiveMode,\n\t\tEnableHASH:              s.config.HASHSupport > 0,\n\t\tEnableCOMB:              s.config.CombineSupport > 0,\n\t\tDefaultTransferType:     ftpserver.TransferTypeBinary,\n\t\tActiveConnectionsCheck:  ftpserver.DataConnectionRequirement(s.binding.ActiveConnectionsSecurity),\n\t\tPasvConnectionsCheck:    ftpserver.DataConnectionRequirement(s.binding.PassiveConnectionsSecurity),\n\t}\n\tif portRange != nil {\n\t\tsettings.PassiveTransferPortRange = portRange\n\t}\n\treturn settings, nil\n}\n\n// ClientConnected is called to send the very first welcome message\nfunc (s *Server) ClientConnected(cc ftpserver.ClientContext) (string, error) {\n\tcc.SetDebug(s.binding.Debug)\n\tipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())\n\tcommon.Connections.AddClientConnection(ipAddr)\n\tif common.IsBanned(ipAddr, common.ProtocolFTP) {\n\t\tlogger.Log(logger.LevelDebug, common.ProtocolFTP, \"\", \"connection refused, ip %q is banned\", ipAddr)\n\t\treturn \"Access denied: banned client IP\", common.ErrConnectionDenied\n\t}\n\tif err := common.Connections.IsNewConnectionAllowed(ipAddr, common.ProtocolFTP); err != nil {\n\t\tlogger.Log(logger.LevelDebug, common.ProtocolFTP, \"\", \"connection not allowed from ip %q: %v\", ipAddr, err)\n\t\treturn \"Access denied\", err\n\t}\n\t_, err := common.LimitRate(common.ProtocolFTP, ipAddr)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"Access denied: %v\", err.Error()), err\n\t}\n\tif err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolFTP); err != nil {\n\t\treturn \"Access denied\", err\n\t}\n\tconnID := fmt.Sprintf(\"%v_%v\", s.ID, cc.ID())\n\tuser := dataprovider.User{}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, cc.LocalAddr().String(),\n\t\t\tcc.RemoteAddr().String(), user),\n\t\tclientContext: cc,\n\t}\n\terr = common.Connections.Add(connection)\n\treturn s.initialMsg, err\n}\n\n// ClientDisconnected is called when the user disconnects, even if he never authenticated\nfunc (s *Server) ClientDisconnected(cc ftpserver.ClientContext) {\n\tconnID := fmt.Sprintf(\"%v_%v_%v\", common.ProtocolFTP, s.ID, cc.ID())\n\tcommon.Connections.Remove(connID)\n\tcommon.Connections.RemoveClientConnection(util.GetIPFromRemoteAddress(cc.RemoteAddr().String()))\n}\n\n// AuthUser authenticates the user and selects an handling driver\nfunc (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string) (ftpserver.ClientDriver, error) {\n\tloginMethod := dataprovider.LoginMethodPassword\n\ttlsState, ok := cc.Extra().(*tlsState)\n\tif ok && tlsState != nil && tlsState.LoginWithMutualTLS {\n\t\tloginMethod = dataprovider.LoginMethodTLSCertificateAndPwd\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())\n\tuser, err := dataprovider.CheckUserAndPass(username, password, ipAddr, common.ProtocolFTP)\n\tif err != nil {\n\t\tuser.Username = username\n\t\tupdateLoginMetrics(&user, ipAddr, loginMethod, err, nil)\n\t\treturn nil, dataprovider.ErrInvalidCredentials\n\t}\n\n\tconnection, err := s.validateUser(user, cc, loginMethod)\n\n\tdefer updateLoginMetrics(&user, ipAddr, loginMethod, err, connection)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsetStartDirectory(user.Filters.StartDirectory, cc)\n\tdataprovider.UpdateLastLogin(&user)\n\treturn connection, nil\n}\n\n// PreAuthUser implements the MainDriverExtensionUserVerifier interface\nfunc (s *Server) PreAuthUser(cc ftpserver.ClientContext, username string) error {\n\tif s.binding.TLSMode == 0 && s.tlsConfig != nil {\n\t\tuser, err := dataprovider.GetFTPPreAuthUser(username, util.GetIPFromRemoteAddress(cc.RemoteAddr().String()))\n\t\tif err == nil {\n\t\t\tif user.Filters.FTPSecurity == 1 {\n\t\t\t\treturn cc.SetTLSRequirement(ftpserver.MandatoryEncryption)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tif !errors.Is(err, util.ErrNotFound) {\n\t\t\tlogger.Error(logSender, fmt.Sprintf(\"%v_%v_%v\", common.ProtocolFTP, s.ID, cc.ID()),\n\t\t\t\t\"unable to get user on pre auth: %v\", err)\n\t\t\treturn common.ErrInternalFailure\n\t\t}\n\t}\n\treturn nil\n}\n\n// WrapPassiveListener implements the MainDriverExtensionPassiveWrapper interface\nfunc (s *Server) WrapPassiveListener(listener net.Listener) (net.Listener, error) {\n\tif s.binding.HasProxy() {\n\t\treturn common.Config.GetProxyListener(listener)\n\t}\n\treturn listener, nil\n}\n\n// VerifyConnection checks whether a user should be authenticated using a client certificate without prompting for a password\nfunc (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsConn *tls.Conn) (ftpserver.ClientDriver, error) {\n\tif tlsConn == nil {\n\t\treturn nil, nil\n\t}\n\tstate := tlsConn.ConnectionState()\n\tcc.SetExtra(&tlsState{\n\t\tLoginWithMutualTLS: false,\n\t\tCipher:             tls.CipherSuiteName(state.CipherSuite),\n\t\tVersion:            tls.VersionName(state.Version),\n\t\tKEX:                state.CurveID.String(),\n\t})\n\tif !s.binding.isMutualTLSEnabled() {\n\t\treturn nil, nil\n\t}\n\n\tif len(state.PeerCertificates) > 0 {\n\t\tipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())\n\t\tdbUser, err := dataprovider.CheckUserBeforeTLSAuth(user, ipAddr, common.ProtocolFTP, state.PeerCertificates[0])\n\t\tif err != nil {\n\t\t\tdbUser.Username = user\n\t\t\tupdateLoginMetrics(&dbUser, ipAddr, dataprovider.LoginMethodTLSCertificate, err, nil)\n\t\t\treturn nil, dataprovider.ErrInvalidCredentials\n\t\t}\n\t\tif dbUser.IsTLSVerificationEnabled() {\n\t\t\tdbUser, err = dataprovider.CheckUserAndTLSCert(user, ipAddr, common.ProtocolFTP, state.PeerCertificates[0])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tcc.SetExtra(&tlsState{\n\t\t\t\tLoginWithMutualTLS: true,\n\t\t\t\tCipher:             tls.CipherSuiteName(state.CipherSuite),\n\t\t\t\tVersion:            tls.VersionName(state.Version),\n\t\t\t\tKEX:                state.CurveID.String(),\n\t\t\t})\n\n\t\t\tif dbUser.IsLoginMethodAllowed(dataprovider.LoginMethodTLSCertificate, common.ProtocolFTP) {\n\t\t\t\tconnection, err := s.validateUser(dbUser, cc, dataprovider.LoginMethodTLSCertificate)\n\n\t\t\t\tdefer updateLoginMetrics(&dbUser, ipAddr, dataprovider.LoginMethodTLSCertificate, err, connection)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tsetStartDirectory(dbUser.Filters.StartDirectory, cc)\n\t\t\t\tdataprovider.UpdateLastLogin(&dbUser)\n\t\t\t\treturn connection, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\nfunc (s *Server) buildTLSConfig() {\n\tif certMgr != nil {\n\t\tcertID := common.DefaultTLSKeyPaidID\n\t\tif getConfigPath(s.binding.CertificateFile, \"\") != \"\" && getConfigPath(s.binding.CertificateKeyFile, \"\") != \"\" {\n\t\t\tcertID = s.binding.GetAddress()\n\t\t}\n\t\tif !certMgr.HasCertificate(certID) {\n\t\t\treturn\n\t\t}\n\t\ts.tlsConfig = &tls.Config{\n\t\t\tGetCertificate: certMgr.GetCertificateFunc(certID),\n\t\t\tMinVersion:     util.GetTLSVersion(s.binding.MinTLSVersion),\n\t\t\tCipherSuites:   s.binding.ciphers,\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"configured TLS cipher suites for binding %q: %v, certID: %v\",\n\t\t\ts.binding.GetAddress(), s.binding.ciphers, certID)\n\t\tif s.binding.isMutualTLSEnabled() {\n\t\t\ts.tlsConfig.ClientCAs = certMgr.GetRootCAs()\n\t\t\ts.tlsConfig.VerifyConnection = s.verifyTLSConnection\n\t\t\tswitch s.binding.ClientAuthType {\n\t\t\tcase 1:\n\t\t\t\ts.tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert\n\t\t\tcase 2:\n\t\t\t\ts.tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven\n\t\t\t}\n\t\t}\n\t}\n}\n\n// GetTLSConfig returns the TLS configuration for this server\nfunc (s *Server) GetTLSConfig() (*tls.Config, error) {\n\tif s.tlsConfig != nil {\n\t\treturn s.tlsConfig, nil\n\t}\n\treturn nil, errors.New(\"no TLS certificate configured\")\n}\n\n// VerifyTLSConnectionState implements the MainDriverExtensionTLSConnectionStateVerifier extension\nfunc (s *Server) VerifyTLSConnectionState(_ ftpserver.ClientContext, cs tls.ConnectionState) error {\n\tif !s.binding.isMutualTLSEnabled() {\n\t\treturn nil\n\t}\n\treturn s.verifyTLSConnection(cs)\n}\n\nfunc (s *Server) verifyTLSConnection(state tls.ConnectionState) error {\n\tif certMgr != nil {\n\t\tvar clientCrt *x509.Certificate\n\t\tvar clientCrtName string\n\t\tif len(state.PeerCertificates) > 0 {\n\t\t\tclientCrt = state.PeerCertificates[0]\n\t\t\tclientCrtName = clientCrt.Subject.String()\n\t\t}\n\t\tif len(state.VerifiedChains) == 0 {\n\t\t\tif s.binding.ClientAuthType == 2 {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tlogger.Warn(logSender, \"\", \"TLS connection cannot be verified: unable to get verification chain\")\n\t\t\treturn errors.New(\"TLS connection cannot be verified: unable to get verification chain\")\n\t\t}\n\t\tfor _, verifiedChain := range state.VerifiedChains {\n\t\t\tvar caCrt *x509.Certificate\n\t\t\tif len(verifiedChain) > 0 {\n\t\t\t\tcaCrt = verifiedChain[len(verifiedChain)-1]\n\t\t\t}\n\t\t\tif certMgr.IsRevoked(clientCrt, caCrt) {\n\t\t\t\tlogger.Debug(logSender, \"\", \"tls handshake error, client certificate %q has beed revoked\", clientCrtName)\n\t\t\t\treturn common.ErrCrtRevoked\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext, loginMethod string) (*Connection, error) {\n\tconnectionID := fmt.Sprintf(\"%v_%v_%v\", common.ProtocolFTP, s.ID, cc.ID())\n\tif !filepath.IsAbs(user.HomeDir) {\n\t\tlogger.Warn(logSender, connectionID, \"user %q has an invalid home dir: %q. Home dir must be an absolute path, login not allowed\",\n\t\t\tuser.Username, user.HomeDir)\n\t\treturn nil, fmt.Errorf(\"cannot login user with invalid home dir: %q\", user.HomeDir)\n\t}\n\tif slices.Contains(user.Filters.DeniedProtocols, common.ProtocolFTP) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, protocol FTP is not allowed\", user.Username)\n\t\treturn nil, fmt.Errorf(\"protocol FTP is not allowed for user %q\", user.Username)\n\t}\n\tif !user.IsLoginMethodAllowed(loginMethod, common.ProtocolFTP) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, %v login method is not allowed\",\n\t\t\tuser.Username, loginMethod)\n\t\treturn nil, fmt.Errorf(\"login method %v is not allowed for user %q\", loginMethod, user.Username)\n\t}\n\tif user.MustSetSecondFactorForProtocol(common.ProtocolFTP) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, second factor authentication is not set\",\n\t\t\tuser.Username)\n\t\treturn nil, fmt.Errorf(\"second factor authentication is not set for user %q\", user.Username)\n\t}\n\tif user.MaxSessions > 0 {\n\t\tactiveSessions := common.Connections.GetActiveSessions(user.Username)\n\t\tif activeSessions >= user.MaxSessions {\n\t\t\tlogger.Info(logSender, connectionID, \"authentication refused for user: %q, too many open sessions: %v/%v\",\n\t\t\t\tuser.Username, activeSessions, user.MaxSessions)\n\t\t\treturn nil, fmt.Errorf(\"too many open sessions: %v\", activeSessions)\n\t\t}\n\t}\n\tremoteAddr := cc.RemoteAddr().String()\n\tif !user.IsLoginFromAddrAllowed(remoteAddr) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, remote address is not allowed: %v\",\n\t\t\tuser.Username, remoteAddr)\n\t\treturn nil, fmt.Errorf(\"login for user %q is not allowed from this address: %v\", user.Username, remoteAddr)\n\t}\n\terr := user.CheckFsRoot(connectionID)\n\tif err != nil {\n\t\terrClose := user.CloseFs()\n\t\tlogger.Warn(logSender, connectionID, \"unable to check fs root: %v close fs error: %v\", err, errClose)\n\t\treturn nil, common.ErrInternalFailure\n\t}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(fmt.Sprintf(\"%v_%v\", s.ID, cc.ID()), common.ProtocolFTP,\n\t\t\tcc.LocalAddr().String(), remoteAddr, user),\n\t\tclientContext: cc,\n\t}\n\terr = common.Connections.Swap(connection)\n\tif err != nil {\n\t\terrClose := user.CloseFs()\n\t\tlogger.Warn(logSender, connectionID, \"unable to swap connection: %v, close fs error: %v\", err, errClose)\n\t\treturn nil, err\n\t}\n\treturn connection, nil\n}\n\nfunc setStartDirectory(startDirectory string, cc ftpserver.ClientContext) {\n\tif startDirectory == \"\" {\n\t\treturn\n\t}\n\tcc.SetPath(startDirectory)\n}\n\nfunc updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error, c *Connection) {\n\tmetric.AddLoginAttempt(loginMethod)\n\tif err == nil {\n\t\tinfo := \"\"\n\t\tif tlsState, ok := c.clientContext.Extra().(*tlsState); ok && tlsState != nil {\n\t\t\tinfo = fmt.Sprintf(\"%s - %s - %s\", tlsState.Version, tlsState.Cipher, tlsState.KEX)\n\t\t}\n\t\tlogger.LoginLog(user.Username, ip, loginMethod, common.ProtocolFTP, c.ID, c.GetClientVersion(),\n\t\t\tc.clientContext.HasTLSForControl(), info)\n\t\tplugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolFTP, user.Username, ip, \"\", nil)\n\t\tcommon.DelayLogin(nil)\n\t} else if err != common.ErrInternalFailure {\n\t\tlogger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolFTP, err.Error())\n\t\tevent := common.HostEventLoginFailed\n\t\tlogEv := notifier.LogEventTypeLoginFailed\n\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\tevent = common.HostEventUserNotFound\n\t\t\tlogEv = notifier.LogEventTypeLoginNoUser\n\t\t}\n\t\tcommon.AddDefenderEvent(ip, common.ProtocolFTP, event)\n\t\tplugin.Handler.NotifyLogEvent(logEv, common.ProtocolFTP, user.Username, ip, \"\", err)\n\t\tif loginMethod != dataprovider.LoginMethodTLSCertificate {\n\t\t\tcommon.DelayLogin(err)\n\t\t}\n\t}\n\tmetric.AddLoginResult(loginMethod, err)\n\tdataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolFTP, err)\n}\n"
  },
  {
    "path": "internal/ftpd/transfer.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage ftpd\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\n// transfer contains the transfer details for an upload or a download.\n// It implements the ftpserver.FileTransfer interface to handle files downloads and uploads\ntype transfer struct {\n\t*common.BaseTransfer\n\twriter         io.WriteCloser\n\treader         io.ReadCloser\n\tisFinished     bool\n\texpectedOffset int64\n}\n\nfunc newTransfer(baseTransfer *common.BaseTransfer, pipeWriter vfs.PipeWriter, pipeReader vfs.PipeReader,\n\texpectedOffset int64) *transfer {\n\tvar writer io.WriteCloser\n\tvar reader io.ReadCloser\n\tif baseTransfer.File != nil {\n\t\twriter = baseTransfer.File\n\t\treader = baseTransfer.File\n\t} else if pipeWriter != nil {\n\t\twriter = pipeWriter\n\t} else if pipeReader != nil {\n\t\treader = pipeReader\n\t}\n\treturn &transfer{\n\t\tBaseTransfer:   baseTransfer,\n\t\twriter:         writer,\n\t\treader:         reader,\n\t\tisFinished:     false,\n\t\texpectedOffset: expectedOffset,\n\t}\n}\n\n// Read reads the contents to downloads.\nfunc (t *transfer) Read(p []byte) (n int, err error) {\n\tt.Connection.UpdateLastActivity()\n\n\tn, err = t.reader.Read(p)\n\tt.BytesSent.Add(int64(n))\n\n\tif err == nil {\n\t\terr = t.CheckRead()\n\t}\n\tif err != nil && err != io.EOF {\n\t\tt.TransferError(err)\n\t\terr = t.ConvertError(err)\n\t\treturn\n\t}\n\tt.HandleThrottle()\n\treturn\n}\n\n// Write writes the uploaded contents.\nfunc (t *transfer) Write(p []byte) (n int, err error) {\n\tt.Connection.UpdateLastActivity()\n\n\tn, err = t.writer.Write(p)\n\tt.BytesReceived.Add(int64(n))\n\n\tif err == nil {\n\t\terr = t.CheckWrite()\n\t}\n\tif err != nil {\n\t\tt.TransferError(err)\n\t\terr = t.ConvertError(err)\n\t\treturn\n\t}\n\tt.HandleThrottle()\n\treturn\n}\n\n// Seek sets the offset to resume an upload or a download\nfunc (t *transfer) Seek(offset int64, whence int) (int64, error) {\n\tt.Connection.UpdateLastActivity()\n\tif t.File != nil {\n\t\tret, err := t.File.Seek(offset, whence)\n\t\tif err != nil {\n\t\t\tt.TransferError(err)\n\t\t}\n\t\treturn ret, err\n\t}\n\tif (t.reader != nil || t.writer != nil) && t.expectedOffset == offset && whence == io.SeekStart {\n\t\treturn offset, nil\n\t}\n\tt.TransferError(errors.New(\"seek is unsupported for this transfer\"))\n\treturn 0, common.ErrOpUnsupported\n}\n\n// Close it is called when the transfer is completed.\nfunc (t *transfer) Close() error {\n\tif err := t.setFinished(); err != nil {\n\t\treturn err\n\t}\n\terr := t.closeIO()\n\terrBaseClose := t.BaseTransfer.Close()\n\tif errBaseClose != nil {\n\t\terr = errBaseClose\n\t}\n\treturn t.Connection.GetFsError(t.Fs, err)\n}\n\nfunc (t *transfer) closeIO() error {\n\tvar err error\n\tif t.File != nil {\n\t\terr = t.File.Close()\n\t} else if t.writer != nil {\n\t\terr = t.writer.Close()\n\t\tt.Lock()\n\t\t// we set ErrTransfer here so quota is not updated, in this case the uploads are atomic\n\t\tif err != nil && t.ErrTransfer == nil {\n\t\t\tt.ErrTransfer = err\n\t\t}\n\t\tt.Unlock()\n\t} else if t.reader != nil {\n\t\terr = t.reader.Close()\n\t\tif metadater, ok := t.reader.(vfs.Metadater); ok {\n\t\t\tt.SetMetadata(metadater.Metadata())\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (t *transfer) setFinished() error {\n\tt.Lock()\n\tdefer t.Unlock()\n\tif t.isFinished {\n\t\treturn common.ErrTransferClosed\n\t}\n\tt.isFinished = true\n\treturn nil\n}\n"
  },
  {
    "path": "internal/httpclient/httpclient.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package httpclient provides HTTP client configuration for SFTPGo hooks\npackage httpclient\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-retryablehttp\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// TLSKeyPair defines the paths for a TLS key pair\ntype TLSKeyPair struct {\n\tCert string `json:\"cert\" mapstructure:\"cert\"`\n\tKey  string `json:\"key\" mapstructure:\"key\"`\n}\n\n// Header defines an HTTP header.\n// If the URL is not empty, the header is added only if the\n// requested URL starts with the one specified\ntype Header struct {\n\tKey   string `json:\"key\" mapstructure:\"key\"`\n\tValue string `json:\"value\" mapstructure:\"value\"`\n\tURL   string `json:\"url\" mapstructure:\"url\"`\n}\n\n// Config defines the configuration for HTTP clients.\n// HTTP clients are used for executing hooks such as the ones used for\n// custom actions, external authentication and pre-login user modifications\ntype Config struct {\n\t// Timeout specifies a time limit, in seconds, for a request\n\tTimeout float64 `json:\"timeout\" mapstructure:\"timeout\"`\n\t// RetryWaitMin defines the minimum waiting time between attempts in seconds\n\tRetryWaitMin int `json:\"retry_wait_min\" mapstructure:\"retry_wait_min\"`\n\t// RetryWaitMax defines the minimum waiting time between attempts in seconds\n\tRetryWaitMax int `json:\"retry_wait_max\" mapstructure:\"retry_wait_max\"`\n\t// RetryMax defines the maximum number of attempts\n\tRetryMax int `json:\"retry_max\" mapstructure:\"retry_max\"`\n\t// CACertificates defines extra CA certificates to trust.\n\t// The paths can be absolute or relative to the config dir.\n\t// Adding trusted CA certificates is a convenient way to use self-signed\n\t// certificates without defeating the purpose of using TLS\n\tCACertificates []string `json:\"ca_certificates\" mapstructure:\"ca_certificates\"`\n\t// Certificates defines the certificates to use for mutual TLS\n\tCertificates []TLSKeyPair `json:\"certificates\" mapstructure:\"certificates\"`\n\t// if enabled the HTTP client accepts any TLS certificate presented by\n\t// the server and any host name in that certificate.\n\t// In this mode, TLS is susceptible to man-in-the-middle attacks.\n\t// This should be used only for testing.\n\tSkipTLSVerify bool `json:\"skip_tls_verify\" mapstructure:\"skip_tls_verify\"`\n\t// Headers defines a list of http headers to add to each request\n\tHeaders         []Header `json:\"headers\" mapstructure:\"headers\"`\n\tcustomTransport *http.Transport\n}\n\nconst logSender = \"httpclient\"\n\nvar httpConfig Config\n\n// Initialize configures HTTP clients\nfunc (c *Config) Initialize(configDir string) error {\n\tif c.Timeout <= 0 {\n\t\treturn fmt.Errorf(\"invalid timeout: %v\", c.Timeout)\n\t}\n\trootCAs, err := c.loadCACerts(configDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcustomTransport := http.DefaultTransport.(*http.Transport).Clone()\n\tif customTransport.TLSClientConfig != nil {\n\t\tcustomTransport.TLSClientConfig.RootCAs = rootCAs\n\t} else {\n\t\tcustomTransport.TLSClientConfig = &tls.Config{\n\t\t\tRootCAs: rootCAs,\n\t\t}\n\t}\n\tcustomTransport.TLSClientConfig.InsecureSkipVerify = c.SkipTLSVerify\n\tc.customTransport = customTransport\n\n\terr = c.loadCertificates(configDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar headers []Header\n\tfor _, h := range c.Headers {\n\t\tif h.Key != \"\" && h.Value != \"\" {\n\t\t\theaders = append(headers, h)\n\t\t}\n\t}\n\tc.Headers = headers\n\thttpConfig = *c\n\treturn nil\n}\n\n// loadCACerts returns system cert pools and try to add the configured\n// CA certificates to it\nfunc (c *Config) loadCACerts(configDir string) (*x509.CertPool, error) {\n\tif len(c.CACertificates) == 0 {\n\t\treturn nil, nil\n\t}\n\trootCAs, err := x509.SystemCertPool()\n\tif err != nil {\n\t\trootCAs = x509.NewCertPool()\n\t}\n\n\tfor _, ca := range c.CACertificates {\n\t\tif !util.IsFileInputValid(ca) {\n\t\t\treturn nil, fmt.Errorf(\"unable to load invalid CA certificate: %q\", ca)\n\t\t}\n\t\tif !filepath.IsAbs(ca) {\n\t\t\tca = filepath.Join(configDir, ca)\n\t\t}\n\t\tcerts, err := os.ReadFile(ca)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to load CA certificate: %v\", err)\n\t\t}\n\t\tif rootCAs.AppendCertsFromPEM(certs) {\n\t\t\tlogger.Debug(logSender, \"\", \"CA certificate %q added to the trusted certificates\", ca)\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"unable to add CA certificate %q to the trusted cetificates\", ca)\n\t\t}\n\t}\n\treturn rootCAs, nil\n}\n\nfunc (c *Config) loadCertificates(configDir string) error {\n\tif len(c.Certificates) == 0 {\n\t\treturn nil\n\t}\n\n\tfor _, keyPair := range c.Certificates {\n\t\tcert := keyPair.Cert\n\t\tkey := keyPair.Key\n\t\tif !util.IsFileInputValid(cert) {\n\t\t\treturn fmt.Errorf(\"unable to load invalid certificate: %q\", cert)\n\t\t}\n\t\tif !util.IsFileInputValid(key) {\n\t\t\treturn fmt.Errorf(\"unable to load invalid key: %q\", key)\n\t\t}\n\t\tif !filepath.IsAbs(cert) {\n\t\t\tcert = filepath.Join(configDir, cert)\n\t\t}\n\t\tif !filepath.IsAbs(key) {\n\t\t\tkey = filepath.Join(configDir, key)\n\t\t}\n\t\ttlsCert, err := tls.LoadX509KeyPair(cert, key)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to load key pair %q, %q: %v\", cert, key, err)\n\t\t}\n\t\tx509Cert, err := x509.ParseCertificate(tlsCert.Certificate[0])\n\t\tif err == nil {\n\t\t\tlogger.Debug(logSender, \"\", \"adding leaf certificate for key pair %q, %q\", cert, key)\n\t\t\ttlsCert.Leaf = x509Cert\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"client certificate %q and key %q successfully loaded\", cert, key)\n\t\tc.customTransport.TLSClientConfig.Certificates = append(c.customTransport.TLSClientConfig.Certificates, tlsCert)\n\t}\n\treturn nil\n}\n\n// GetHTTPClient returns a new HTTP client with the configured parameters\nfunc GetHTTPClient() *http.Client {\n\treturn &http.Client{\n\t\tTimeout:   time.Duration(httpConfig.Timeout * float64(time.Second)),\n\t\tTransport: httpConfig.customTransport,\n\t}\n}\n\n// GetRetraybleHTTPClient returns an HTTP client that retry a request on error.\n// It uses the configured retry parameters\nfunc GetRetraybleHTTPClient() *retryablehttp.Client {\n\tclient := retryablehttp.NewClient()\n\tclient.HTTPClient.Timeout = time.Duration(httpConfig.Timeout * float64(time.Second))\n\tclient.HTTPClient.Transport.(*http.Transport).TLSClientConfig = httpConfig.customTransport.TLSClientConfig\n\tclient.Logger = &logger.LeveledLogger{Sender: \"RetryableHTTPClient\"}\n\tclient.RetryWaitMin = time.Duration(httpConfig.RetryWaitMin) * time.Second\n\tclient.RetryWaitMax = time.Duration(httpConfig.RetryWaitMax) * time.Second\n\tclient.RetryMax = httpConfig.RetryMax\n\n\treturn client\n}\n\n// Get issues a GET to the specified URL\nfunc Get(url string) (*http.Response, error) {\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taddHeaders(req, url)\n\tclient := GetHTTPClient()\n\tdefer client.CloseIdleConnections()\n\n\treturn client.Do(req)\n}\n\n// Post issues a POST to the specified URL\nfunc Post(url string, contentType string, body io.Reader) (*http.Response, error) {\n\treq, err := http.NewRequest(http.MethodPost, url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", contentType)\n\taddHeaders(req, url)\n\tclient := GetHTTPClient()\n\tdefer client.CloseIdleConnections()\n\n\treturn client.Do(req)\n}\n\n// RetryableGet issues a GET to the specified URL using the retryable client\nfunc RetryableGet(url string) (*http.Response, error) {\n\treq, err := retryablehttp.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taddHeadersToRetryableReq(req, url)\n\tclient := GetRetraybleHTTPClient()\n\tdefer client.HTTPClient.CloseIdleConnections()\n\n\treturn client.Do(req)\n}\n\n// RetryablePost issues a POST to the specified URL using the retryable client\nfunc RetryablePost(url string, contentType string, body io.Reader) (*http.Response, error) {\n\treq, err := retryablehttp.NewRequest(http.MethodPost, url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", contentType)\n\taddHeadersToRetryableReq(req, url)\n\tclient := GetRetraybleHTTPClient()\n\tdefer client.HTTPClient.CloseIdleConnections()\n\n\treturn client.Do(req)\n}\n\nfunc addHeaders(req *http.Request, url string) {\n\tfor idx := range httpConfig.Headers {\n\t\th := &httpConfig.Headers[idx]\n\t\tif h.URL == \"\" || strings.HasPrefix(url, h.URL) {\n\t\t\treq.Header.Set(h.Key, h.Value)\n\t\t}\n\t}\n}\n\nfunc addHeadersToRetryableReq(req *retryablehttp.Request, url string) {\n\tfor idx := range httpConfig.Headers {\n\t\th := &httpConfig.Headers[idx]\n\t\tif h.URL == \"\" || strings.HasPrefix(url, h.URL) {\n\t\t\treq.Header.Set(h.Key, h.Value)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/api_admin.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getAdmins(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tadmins, err := dataprovider.GetAdmins(limit, offset, order)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, admins)\n}\n\nfunc getAdminByUsername(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tusername := getURLParam(r, \"username\")\n\trenderAdmin(w, r, username, http.StatusOK)\n}\n\nfunc renderAdmin(w http.ResponseWriter, r *http.Request, username string, status int) {\n\tadmin, err := dataprovider.AdminExists(username)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tadmin.HideConfidentialData()\n\tif status != http.StatusOK {\n\t\tctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)\n\t\trender.JSON(w, r.WithContext(ctx), admin)\n\t} else {\n\t\trender.JSON(w, r, admin)\n\t}\n}\n\nfunc addAdmin(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tadmin := dataprovider.Admin{}\n\terr = render.DecodeJSON(r.Body, &admin)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = dataprovider.AddAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", adminPath, url.PathEscape(admin.Username)))\n\trenderAdmin(w, r, admin.Username, http.StatusCreated)\n}\n\nfunc disableAdmin2FA(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tadmin, err := dataprovider.AdminExists(getURLParam(r, \"username\"))\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif !admin.Filters.TOTPConfig.Enabled {\n\t\tsendAPIResponse(w, r, nil, \"two-factor authentication is not enabled\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif admin.Username == claims.Username {\n\t\tif admin.Filters.RequireTwoFactor {\n\t\t\terr := util.NewValidationError(\"two-factor authentication must be enabled\")\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t}\n\tadmin.Filters.RecoveryCodes = nil\n\tadmin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled: false,\n\t}\n\tif err := dataprovider.UpdateAdmin(&admin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"2FA disabled\", http.StatusOK)\n}\n\nfunc updateAdmin(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tusername := getURLParam(r, \"username\")\n\tadmin, err := dataprovider.AdminExists(username)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedAdmin dataprovider.Admin\n\terr = render.DecodeJSON(r.Body, &updatedAdmin)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif username == claims.Username {\n\t\tif claims.APIKeyID != \"\" {\n\t\t\tsendAPIResponse(w, r, errors.New(\"updating the admin impersonated with an API key is not allowed\"), \"\",\n\t\t\t\thttp.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tif !util.SlicesEqual(admin.Permissions, updatedAdmin.Permissions) {\n\t\t\tsendAPIResponse(w, r, errors.New(\"you cannot change your permissions\"), \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tif updatedAdmin.Status == 0 {\n\t\t\tsendAPIResponse(w, r, errors.New(\"you cannot disable yourself\"), \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tif updatedAdmin.Role != claims.Role {\n\t\t\tsendAPIResponse(w, r, errors.New(\"you cannot add/change your role\"), \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tupdatedAdmin.Filters.RequirePasswordChange = admin.Filters.RequirePasswordChange\n\t\tupdatedAdmin.Filters.RequireTwoFactor = admin.Filters.RequireTwoFactor\n\t}\n\tupdatedAdmin.ID = admin.ID\n\tupdatedAdmin.Username = admin.Username\n\tif updatedAdmin.Password == \"\" {\n\t\tupdatedAdmin.Password = admin.Password\n\t}\n\tupdatedAdmin.Filters.TOTPConfig = admin.Filters.TOTPConfig\n\tupdatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes\n\terr = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Admin updated\", http.StatusOK)\n}\n\nfunc deleteAdmin(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tusername := getURLParam(r, \"username\")\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif username == claims.Username {\n\t\tsendAPIResponse(w, r, errors.New(\"you cannot delete yourself\"), \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr = dataprovider.DeleteAdmin(username, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Admin deleted\", http.StatusOK)\n}\n\nfunc getAdminProfile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tadmin, err := dataprovider.AdminExists(claims.Username)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tresp := adminProfile{\n\t\tbaseProfile: baseProfile{\n\t\t\tEmail:           admin.Email,\n\t\t\tDescription:     admin.Description,\n\t\t\tAllowAPIKeyAuth: admin.Filters.AllowAPIKeyAuth,\n\t\t},\n\t}\n\trender.JSON(w, r, resp)\n}\n\nfunc updateAdminProfile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tadmin, err := dataprovider.AdminExists(claims.Username)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tvar req adminProfile\n\terr = render.DecodeJSON(r.Body, &req)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tadmin.Email = req.Email\n\tadmin.Description = req.Description\n\tadmin.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth\n\tif err := dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Profile updated\", http.StatusOK)\n}\n\nfunc forgotAdminPassword(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tif !smtp.IsEnabled() {\n\t\tsendAPIResponse(w, r, nil, \"No SMTP configuration\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr := handleForgotPassword(r, getURLParam(r, \"username\"), true)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tsendAPIResponse(w, r, err, \"Check your email for the confirmation code\", http.StatusOK)\n}\n\nfunc resetAdminPassword(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tvar req pwdReset\n\terr := render.DecodeJSON(r.Body, &req)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\t_, _, err = handleResetPassword(r, req.Code, req.Password, req.Password, true)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Password reset successful\", http.StatusOK)\n}\n\nfunc changeAdminPassword(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tvar pwd pwdChange\n\terr := render.DecodeJSON(r.Body, &pwd)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = doChangeAdminPassword(r, pwd.CurrentPassword, pwd.NewPassword, pwd.NewPassword)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tinvalidateToken(r)\n\tsendAPIResponse(w, r, err, \"Password updated\", http.StatusOK)\n}\n\nfunc doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error {\n\tif currentPassword == \"\" || newPassword == \"\" || confirmNewPassword == \"\" {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"please provide the current password and the new one two times\"),\n\t\t\tutil.I18nErrorChangePwdRequiredFields,\n\t\t)\n\t}\n\tif newPassword != confirmNewPassword {\n\t\treturn util.NewI18nError(util.NewValidationError(\"the two password fields do not match\"), util.I18nErrorChangePwdNoMatch)\n\t}\n\tif currentPassword == newPassword {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"the new password must be different from the current one\"),\n\t\t\tutil.I18nErrorChangePwdNoDifferent,\n\t\t)\n\t}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\treturn util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)\n\t}\n\tadmin, err := dataprovider.AdminExists(claims.Username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmatch, err := admin.CheckPassword(currentPassword)\n\tif !match || err != nil {\n\t\treturn util.NewI18nError(util.NewValidationError(\"current password does not match\"), util.I18nErrorChangePwdCurrentNoMatch)\n\t}\n\n\tadmin.Password = newPassword\n\tadmin.Filters.RequirePasswordChange = false\n\n\treturn dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n}\n"
  },
  {
    "path": "internal/httpd/api_configs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/render\"\n\t\"github.com/rs/xid\"\n\t\"golang.org/x/oauth2\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\ntype smtpTestRequest struct {\n\tsmtp.Config\n\tRecipient string `json:\"recipient\"`\n}\n\nfunc (r *smtpTestRequest) hasRedactedSecret() bool {\n\treturn r.Password == redactedSecret || r.OAuth2.ClientSecret == redactedSecret || r.OAuth2.RefreshToken == redactedSecret\n}\n\nfunc testSMTPConfig(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tvar req smtpTestRequest\n\terr := render.DecodeJSON(r.Body, &req)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif req.hasRedactedSecret() {\n\t\tconfigs, err := dataprovider.GetConfigs()\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tconfigs.SetNilsToEmpty()\n\t\tif err := configs.SMTP.TryDecrypt(); err == nil {\n\t\t\tif req.Password == redactedSecret {\n\t\t\t\treq.Password = configs.SMTP.Password.GetPayload()\n\t\t\t}\n\t\t\tif req.OAuth2.ClientSecret == redactedSecret {\n\t\t\t\treq.OAuth2.ClientSecret = configs.SMTP.OAuth2.ClientSecret.GetPayload()\n\t\t\t}\n\t\t\tif req.OAuth2.RefreshToken == redactedSecret {\n\t\t\t\treq.OAuth2.RefreshToken = configs.SMTP.OAuth2.RefreshToken.GetPayload()\n\t\t\t}\n\t\t}\n\t}\n\tif req.AuthType == 3 {\n\t\tif err := req.OAuth2.Validate(); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t}\n\tif err := req.SendEmail([]string{req.Recipient}, nil, \"SFTPGo - Testing Email Settings\",\n\t\t\"It appears your SFTPGo email is setup correctly!\", smtp.EmailContentTypeTextPlain); err != nil {\n\t\tlogger.Info(logSender, \"\", \"unable to send test email: %v\", err)\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"SMTP connection OK\", http.StatusOK)\n}\n\ntype oauth2TokenRequest struct {\n\tsmtp.OAuth2Config\n\tBaseRedirectURL string `json:\"base_redirect_url\"`\n}\n\nfunc (s *httpdServer) handleSMTPOAuth2TokenRequestPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tvar req oauth2TokenRequest\n\terr := render.DecodeJSON(r.Body, &req)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif req.BaseRedirectURL == \"\" {\n\t\tsendAPIResponse(w, r, nil, \"base redirect url is required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif req.ClientSecret == redactedSecret {\n\t\tconfigs, err := dataprovider.GetConfigs()\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tconfigs.SetNilsToEmpty()\n\t\tif err := configs.SMTP.TryDecrypt(); err == nil {\n\t\t\treq.ClientSecret = configs.SMTP.OAuth2.ClientSecret.GetPayload()\n\t\t}\n\t}\n\tcfg := req.GetOAuth2()\n\tcfg.RedirectURL = req.BaseRedirectURL + webOAuth2RedirectPath\n\tclientSecret := kms.NewPlainSecret(cfg.ClientSecret)\n\tclientSecret.SetAdditionalData(xid.New().String())\n\tpendingAuth := newOAuth2PendingAuth(req.Provider, cfg.RedirectURL, cfg.ClientID, clientSecret)\n\toauth2Mgr.addPendingAuth(pendingAuth)\n\tstateToken := createOAuth2Token(s.csrfTokenAuth, pendingAuth.State, util.GetIPFromRemoteAddress(r.RemoteAddr))\n\tif stateToken == \"\" {\n\t\tsendAPIResponse(w, r, nil, \"unable to create state token\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tu := cfg.AuthCodeURL(stateToken, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(pendingAuth.Verifier))\n\tsendAPIResponse(w, r, nil, u, http.StatusOK)\n}\n"
  },
  {
    "path": "internal/httpd/api_defender.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getDefenderHosts(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\thosts, err := common.GetDefenderHosts()\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif hosts == nil {\n\t\trender.JSON(w, r, make([]dataprovider.DefenderEntry, 0))\n\t\treturn\n\t}\n\trender.JSON(w, r, hosts)\n}\n\nfunc getDefenderHostByID(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tip, err := getIPFromID(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\thost, err := common.GetDefenderHost(ip)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, host)\n}\n\nfunc deleteDefenderHostByID(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tip, err := getIPFromID(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif !common.DeleteDefenderHost(ip) {\n\t\tsendAPIResponse(w, r, nil, \"Not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tsendAPIResponse(w, r, nil, \"OK\", http.StatusOK)\n}\n\nfunc getIPFromID(r *http.Request) (string, error) {\n\tdecoded, err := hex.DecodeString(getURLParam(r, \"id\"))\n\tif err != nil {\n\t\treturn \"\", errors.New(\"invalid host id\")\n\t}\n\tip := util.BytesToString(decoded)\n\terr = validateIPAddress(ip)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn ip, nil\n}\n\nfunc validateIPAddress(ip string) error {\n\tif net.ParseIP(ip) == nil {\n\t\treturn fmt.Errorf(\"ip address %q is not valid\", ip)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/httpd/api_eventrule.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getEventActions(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tactions, err := dataprovider.GetEventActions(limit, offset, order, false)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\trender.JSON(w, r, actions)\n}\n\nfunc renderEventAction(w http.ResponseWriter, r *http.Request, name string, claims *jwt.Claims, status int) {\n\taction, err := dataprovider.EventActionExists(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif hideConfidentialData(claims, r) {\n\t\taction.PrepareForRendering()\n\t}\n\tif status != http.StatusOK {\n\t\tctx := context.WithValue(r.Context(), render.StatusCtxKey, status)\n\t\trender.JSON(w, r.WithContext(ctx), action)\n\t} else {\n\t\trender.JSON(w, r, action)\n\t}\n}\n\nfunc getEventActionByName(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\trenderEventAction(w, r, name, claims, http.StatusOK)\n}\n\nfunc addEventAction(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tvar action dataprovider.BaseEventAction\n\terr = render.DecodeJSON(r.Body, &action)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\terr = dataprovider.AddEventAction(&action, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", eventActionsPath, url.PathEscape(action.Name)))\n\trenderEventAction(w, r, action.Name, claims, http.StatusCreated)\n}\n\nfunc updateEventAction(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tname := getURLParam(r, \"name\")\n\taction, err := dataprovider.EventActionExists(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedAction dataprovider.BaseEventAction\n\terr = render.DecodeJSON(r.Body, &updatedAction)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tupdatedAction.ID = action.ID\n\tupdatedAction.Name = action.Name\n\tupdatedAction.Options.SetEmptySecretsIfNil()\n\n\tswitch updatedAction.Type {\n\tcase dataprovider.ActionTypeHTTP:\n\t\tif updatedAction.Options.HTTPConfig.Password.IsNotPlainAndNotEmpty() {\n\t\t\tupdatedAction.Options.HTTPConfig.Password = action.Options.HTTPConfig.Password\n\t\t}\n\t}\n\n\terr = dataprovider.UpdateEventAction(&updatedAction, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Event action updated\", http.StatusOK)\n}\n\nfunc deleteEventAction(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\terr = dataprovider.DeleteEventAction(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Event action deleted\", http.StatusOK)\n}\n\nfunc getEventRules(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\trules, err := dataprovider.GetEventRules(limit, offset, order)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\trender.JSON(w, r, rules)\n}\n\nfunc renderEventRule(w http.ResponseWriter, r *http.Request, name string, claims *jwt.Claims, status int) {\n\trule, err := dataprovider.EventRuleExists(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif hideConfidentialData(claims, r) {\n\t\trule.PrepareForRendering()\n\t}\n\tif status != http.StatusOK {\n\t\tctx := context.WithValue(r.Context(), render.StatusCtxKey, status)\n\t\trender.JSON(w, r.WithContext(ctx), rule)\n\t} else {\n\t\trender.JSON(w, r, rule)\n\t}\n}\n\nfunc getEventRuleByName(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\trenderEventRule(w, r, name, claims, http.StatusOK)\n}\n\nfunc addEventRule(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tvar rule dataprovider.EventRule\n\terr = render.DecodeJSON(r.Body, &rule)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := dataprovider.AddEventRule(&rule, claims.Username, ipAddr, claims.Role); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", eventRulesPath, url.PathEscape(rule.Name)))\n\trenderEventRule(w, r, rule.Name, claims, http.StatusCreated)\n}\n\nfunc updateEventRule(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\trule, err := dataprovider.EventRuleExists(getURLParam(r, \"name\"))\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedRule dataprovider.EventRule\n\terr = render.DecodeJSON(r.Body, &updatedRule)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tupdatedRule.ID = rule.ID\n\tupdatedRule.Name = rule.Name\n\n\terr = dataprovider.UpdateEventRule(&updatedRule, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Event rules updated\", http.StatusOK)\n}\n\nfunc deleteEventRule(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\terr = dataprovider.DeleteEventRule(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Event rule deleted\", http.StatusOK)\n}\n\nfunc runOnDemandRule(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tname := getURLParam(r, \"name\")\n\tif err := common.RunOnDemandRule(name); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Event rule started\", http.StatusAccepted)\n}\n"
  },
  {
    "path": "internal/httpd/api_events.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/csv\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sftpgo/sdk/plugin/eventsearcher\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getCommonSearchParamsFromRequest(r *http.Request) (eventsearcher.CommonSearchParams, error) {\n\tc := eventsearcher.CommonSearchParams{}\n\tc.Limit = 100\n\n\tif _, ok := r.URL.Query()[\"limit\"]; ok {\n\t\tlimit, err := strconv.Atoi(r.URL.Query().Get(\"limit\"))\n\t\tif err != nil {\n\t\t\treturn c, util.NewValidationError(fmt.Sprintf(\"invalid limit: %v\", err))\n\t\t}\n\t\tif limit < 1 || limit > 1000 {\n\t\t\treturn c, util.NewValidationError(fmt.Sprintf(\"limit is out of the 1-1000 range: %v\", limit))\n\t\t}\n\t\tc.Limit = limit\n\t}\n\tif _, ok := r.URL.Query()[\"order\"]; ok {\n\t\torder := r.URL.Query().Get(\"order\")\n\t\tif order != dataprovider.OrderASC && order != dataprovider.OrderDESC {\n\t\t\treturn c, util.NewValidationError(fmt.Sprintf(\"invalid order %q\", order))\n\t\t}\n\t\tif order == dataprovider.OrderASC {\n\t\t\tc.Order = 1\n\t\t}\n\t}\n\tif _, ok := r.URL.Query()[\"start_timestamp\"]; ok {\n\t\tts, err := strconv.ParseInt(r.URL.Query().Get(\"start_timestamp\"), 10, 64)\n\t\tif err != nil {\n\t\t\treturn c, util.NewValidationError(fmt.Sprintf(\"invalid start_timestamp: %v\", err))\n\t\t}\n\t\tc.StartTimestamp = ts\n\t}\n\tif _, ok := r.URL.Query()[\"end_timestamp\"]; ok {\n\t\tts, err := strconv.ParseInt(r.URL.Query().Get(\"end_timestamp\"), 10, 64)\n\t\tif err != nil {\n\t\t\treturn c, util.NewValidationError(fmt.Sprintf(\"invalid end_timestamp: %v\", err))\n\t\t}\n\t\tc.EndTimestamp = ts\n\t}\n\tc.Username = strings.TrimSpace(r.URL.Query().Get(\"username\"))\n\tc.IP = strings.TrimSpace(r.URL.Query().Get(\"ip\"))\n\tc.InstanceIDs = getCommaSeparatedQueryParam(r, \"instance_ids\")\n\tc.FromID = r.URL.Query().Get(\"from_id\")\n\n\treturn c, nil\n}\n\nfunc getFsSearchParamsFromRequest(r *http.Request) (eventsearcher.FsEventSearch, error) {\n\tvar err error\n\ts := eventsearcher.FsEventSearch{}\n\ts.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)\n\tif err != nil {\n\t\treturn s, err\n\t}\n\ts.FsProvider = -1\n\tif _, ok := r.URL.Query()[\"fs_provider\"]; ok {\n\t\tprovider := r.URL.Query().Get(\"fs_provider\")\n\t\tval, err := strconv.Atoi(provider)\n\t\tif err != nil {\n\t\t\treturn s, util.NewValidationError(fmt.Sprintf(\"invalid fs_provider: %v\", provider))\n\t\t}\n\t\ts.FsProvider = val\n\t}\n\ts.Actions = getCommaSeparatedQueryParam(r, \"actions\")\n\ts.SSHCmd = strings.TrimSpace(r.URL.Query().Get(\"ssh_cmd\"))\n\ts.Bucket = strings.TrimSpace(r.URL.Query().Get(\"bucket\"))\n\ts.Endpoint = strings.TrimSpace(r.URL.Query().Get(\"endpoint\"))\n\ts.Protocols = getCommaSeparatedQueryParam(r, \"protocols\")\n\tstatuses := getCommaSeparatedQueryParam(r, \"statuses\")\n\tfor _, status := range statuses {\n\t\tval, err := strconv.ParseInt(status, 10, 32)\n\t\tif err != nil {\n\t\t\treturn s, util.NewValidationError(fmt.Sprintf(\"invalid status: %v\", status))\n\t\t}\n\t\ts.Statuses = append(s.Statuses, int32(val))\n\t}\n\n\treturn s, nil\n}\n\nfunc getProviderSearchParamsFromRequest(r *http.Request) (eventsearcher.ProviderEventSearch, error) {\n\tvar err error\n\ts := eventsearcher.ProviderEventSearch{}\n\ts.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)\n\tif err != nil {\n\t\treturn s, err\n\t}\n\ts.Actions = getCommaSeparatedQueryParam(r, \"actions\")\n\ts.ObjectName = strings.TrimSpace(r.URL.Query().Get(\"object_name\"))\n\ts.ObjectTypes = getCommaSeparatedQueryParam(r, \"object_types\")\n\treturn s, nil\n}\n\nfunc getLogSearchParamsFromRequest(r *http.Request) (eventsearcher.LogEventSearch, error) {\n\tvar err error\n\ts := eventsearcher.LogEventSearch{}\n\ts.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)\n\tif err != nil {\n\t\treturn s, err\n\t}\n\ts.Protocols = getCommaSeparatedQueryParam(r, \"protocols\")\n\tevents := getCommaSeparatedQueryParam(r, \"events\")\n\tfor _, ev := range events {\n\t\tevType, err := strconv.ParseInt(ev, 10, 32)\n\t\tif err == nil {\n\t\t\ts.Events = append(s.Events, int32(evType))\n\t\t}\n\t}\n\n\treturn s, nil\n}\n\nfunc searchFsEvents(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tfilters, err := getFsSearchParamsFromRequest(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tfilters.Role = getRoleFilterForEventSearch(r, claims.Role)\n\n\tif getBoolQueryParam(r, \"csv_export\") {\n\t\tfilters.Limit = 100\n\t\tif err := exportFsEvents(w, &filters); err != nil {\n\t\t\tpanic(http.ErrAbortHandler)\n\t\t}\n\t\treturn\n\t}\n\n\tdata, err := plugin.Handler.SearchFsEvents(&filters)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Write(data) //nolint:errcheck\n}\n\nfunc searchProviderEvents(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar filters eventsearcher.ProviderEventSearch\n\tif filters, err = getProviderSearchParamsFromRequest(r); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tfilters.Role = getRoleFilterForEventSearch(r, claims.Role)\n\tfilters.OmitObjectData = getBoolQueryParam(r, \"omit_object_data\")\n\n\tif getBoolQueryParam(r, \"csv_export\") {\n\t\tfilters.Limit = 100\n\t\tfilters.OmitObjectData = true\n\t\tif err := exportProviderEvents(w, &filters); err != nil {\n\t\t\tpanic(http.ErrAbortHandler)\n\t\t}\n\t\treturn\n\t}\n\n\tdata, err := plugin.Handler.SearchProviderEvents(&filters)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Write(data) //nolint:errcheck\n}\n\nfunc searchLogEvents(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar filters eventsearcher.LogEventSearch\n\tif filters, err = getLogSearchParamsFromRequest(r); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tfilters.Role = getRoleFilterForEventSearch(r, claims.Role)\n\n\tif getBoolQueryParam(r, \"csv_export\") {\n\t\tfilters.Limit = 100\n\t\tif err := exportLogEvents(w, &filters); err != nil {\n\t\t\tpanic(http.ErrAbortHandler)\n\t\t}\n\t\treturn\n\t}\n\n\tdata, err := plugin.Handler.SearchLogEvents(&filters)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Write(data) //nolint:errcheck\n}\n\nfunc exportFsEvents(w http.ResponseWriter, filters *eventsearcher.FsEventSearch) error {\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=fslogs-%s.csv\", time.Now().Format(\"2006-01-02T15-04-05\")))\n\tw.Header().Set(\"Content-Type\", \"text/csv\")\n\tw.Header().Set(\"Accept-Ranges\", \"none\")\n\tw.WriteHeader(http.StatusOK)\n\n\tcsvWriter := csv.NewWriter(w)\n\tev := fsEvent{}\n\terr := csvWriter.Write(ev.getCSVHeader())\n\tif err != nil {\n\t\treturn err\n\t}\n\tresults := make([]fsEvent, 0, filters.Limit)\n\tfor {\n\t\tdata, err := plugin.Handler.SearchFsEvents(filters)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := json.Unmarshal(data, &results); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, event := range results {\n\t\t\tif err := csvWriter.Write(event.getCSVData()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif len(results) == 0 || len(results) < filters.Limit {\n\t\t\tbreak\n\t\t}\n\t\tfilters.StartTimestamp = results[len(results)-1].Timestamp\n\t\tfilters.FromID = results[len(results)-1].ID\n\t\tresults = nil\n\t}\n\tcsvWriter.Flush()\n\treturn csvWriter.Error()\n}\n\nfunc exportProviderEvents(w http.ResponseWriter, filters *eventsearcher.ProviderEventSearch) error {\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=providerlogs-%s.csv\", time.Now().Format(\"2006-01-02T15-04-05\")))\n\tw.Header().Set(\"Content-Type\", \"text/csv\")\n\tw.Header().Set(\"Accept-Ranges\", \"none\")\n\tw.WriteHeader(http.StatusOK)\n\n\tev := providerEvent{}\n\tcsvWriter := csv.NewWriter(w)\n\terr := csvWriter.Write(ev.getCSVHeader())\n\tif err != nil {\n\t\treturn err\n\t}\n\tresults := make([]providerEvent, 0, filters.Limit)\n\tfor {\n\t\tdata, err := plugin.Handler.SearchProviderEvents(filters)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := json.Unmarshal(data, &results); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, event := range results {\n\t\t\tif err := csvWriter.Write(event.getCSVData()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif len(results) < filters.Limit || len(results) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tfilters.FromID = results[len(results)-1].ID\n\t\tfilters.StartTimestamp = results[len(results)-1].Timestamp\n\t\tresults = nil\n\t}\n\tcsvWriter.Flush()\n\treturn csvWriter.Error()\n}\n\nfunc exportLogEvents(w http.ResponseWriter, filters *eventsearcher.LogEventSearch) error {\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=logs-%s.csv\", time.Now().Format(\"2006-01-02T15-04-05\")))\n\tw.Header().Set(\"Content-Type\", \"text/csv\")\n\tw.Header().Set(\"Accept-Ranges\", \"none\")\n\tw.WriteHeader(http.StatusOK)\n\n\tev := logEvent{}\n\tcsvWriter := csv.NewWriter(w)\n\terr := csvWriter.Write(ev.getCSVHeader())\n\tif err != nil {\n\t\treturn err\n\t}\n\tresults := make([]logEvent, 0, filters.Limit)\n\tfor {\n\t\tdata, err := plugin.Handler.SearchLogEvents(filters)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := json.Unmarshal(data, &results); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, event := range results {\n\t\t\tif err := csvWriter.Write(event.getCSVData()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif len(results) == 0 || len(results) < filters.Limit {\n\t\t\tbreak\n\t\t}\n\t\tfilters.StartTimestamp = results[len(results)-1].Timestamp\n\t\tfilters.FromID = results[len(results)-1].ID\n\t\tresults = nil\n\t}\n\tcsvWriter.Flush()\n\treturn csvWriter.Error()\n}\n\nfunc getRoleFilterForEventSearch(r *http.Request, defaultValue string) string {\n\tif defaultValue != \"\" {\n\t\treturn defaultValue\n\t}\n\treturn r.URL.Query().Get(\"role\")\n}\n\ntype fsEvent struct {\n\tID                string `json:\"id\"`\n\tTimestamp         int64  `json:\"timestamp\"`\n\tAction            string `json:\"action\"`\n\tUsername          string `json:\"username\"`\n\tFsPath            string `json:\"fs_path\"`\n\tFsTargetPath      string `json:\"fs_target_path,omitempty\"`\n\tVirtualPath       string `json:\"virtual_path\"`\n\tVirtualTargetPath string `json:\"virtual_target_path,omitempty\"`\n\tSSHCmd            string `json:\"ssh_cmd,omitempty\"`\n\tFileSize          int64  `json:\"file_size,omitempty\"`\n\tElapsed           int64  `json:\"elapsed,omitempty\"`\n\tStatus            int    `json:\"status\"`\n\tProtocol          string `json:\"protocol\"`\n\tIP                string `json:\"ip,omitempty\"`\n\tSessionID         string `json:\"session_id\"`\n\tFsProvider        int    `json:\"fs_provider\"`\n\tBucket            string `json:\"bucket,omitempty\"`\n\tEndpoint          string `json:\"endpoint,omitempty\"`\n\tOpenFlags         int    `json:\"open_flags,omitempty\"`\n\tRole              string `json:\"role,omitempty\"`\n\tInstanceID        string `json:\"instance_id,omitempty\"`\n}\n\nfunc (e *fsEvent) getCSVHeader() []string {\n\treturn []string{\"Time\", \"Action\", \"Path\", \"Size\", \"Elapsed\", \"Status\", \"User\", \"Protocol\",\n\t\t\"IP\", \"SSH command\"}\n}\n\nfunc (e *fsEvent) getCSVData() []string {\n\ttimestamp := time.Unix(0, e.Timestamp).UTC()\n\tvar pathInfo strings.Builder\n\tpathInfo.Write([]byte(e.VirtualPath))\n\tif e.VirtualTargetPath != \"\" {\n\t\tpathInfo.WriteString(\" => \")\n\t\tpathInfo.WriteString(e.VirtualTargetPath)\n\t}\n\tvar status string\n\tswitch e.Status {\n\tcase 1:\n\t\tstatus = \"OK\"\n\tcase 2:\n\t\tstatus = \"KO\"\n\tcase 3:\n\t\tstatus = \"Quota exceeded\"\n\t}\n\tvar fileSize string\n\tif e.FileSize > 0 {\n\t\tfileSize = util.ByteCountIEC(e.FileSize)\n\t}\n\tvar elapsed string\n\tif e.Elapsed > 0 {\n\t\telapsed = (time.Duration(e.Elapsed) * time.Millisecond).String()\n\t}\n\treturn []string{timestamp.Format(time.RFC3339Nano), e.Action, pathInfo.String(),\n\t\tfileSize, elapsed, status, e.Username, e.Protocol, e.IP, e.SSHCmd}\n}\n\ntype providerEvent struct {\n\tID         string `json:\"id\"`\n\tTimestamp  int64  `json:\"timestamp\"`\n\tAction     string `json:\"action\"`\n\tUsername   string `json:\"username\"`\n\tIP         string `json:\"ip,omitempty\"`\n\tObjectType string `json:\"object_type\"`\n\tObjectName string `json:\"object_name\"`\n\tObjectData []byte `json:\"object_data\"`\n\tRole       string `json:\"role,omitempty\"`\n\tInstanceID string `json:\"instance_id,omitempty\"`\n}\n\nfunc (e *providerEvent) getCSVHeader() []string {\n\treturn []string{\"Time\", \"Action\", \"Object Type\", \"Object Name\", \"User\", \"IP\"}\n}\n\nfunc (e *providerEvent) getCSVData() []string {\n\ttimestamp := time.Unix(0, e.Timestamp).UTC()\n\treturn []string{timestamp.Format(time.RFC3339Nano), e.Action, e.ObjectType, e.ObjectName,\n\t\te.Username, e.IP}\n}\n\ntype logEvent struct {\n\tID        string `json:\"id\"`\n\tTimestamp int64  `json:\"timestamp\"`\n\tEvent     int    `json:\"event\"`\n\tProtocol  string `json:\"protocol\"`\n\tUsername  string `json:\"username,omitempty\"`\n\tIP        string `json:\"ip,omitempty\"`\n\tMessage   string `json:\"message,omitempty\"`\n\tRole      string `json:\"role,omitempty\"`\n}\n\nfunc (e *logEvent) getCSVHeader() []string {\n\treturn []string{\"Time\", \"Event\", \"Protocol\", \"User\", \"IP\", \"Message\"}\n}\n\nfunc (e *logEvent) getCSVData() []string {\n\ttimestamp := time.Unix(0, e.Timestamp).UTC()\n\treturn []string{timestamp.Format(time.RFC3339Nano), getLogEventString(notifier.LogEventType(e.Event)),\n\t\te.Protocol, e.Username, e.IP, e.Message}\n}\n\nfunc getLogEventString(event notifier.LogEventType) string {\n\tswitch event {\n\tcase notifier.LogEventTypeLoginFailed:\n\t\treturn \"Login failed\"\n\tcase notifier.LogEventTypeLoginNoUser:\n\t\treturn \"Login with non-existent user\"\n\tcase notifier.LogEventTypeNoLoginTried:\n\t\treturn \"No login tried\"\n\tcase notifier.LogEventTypeNotNegotiated:\n\t\treturn \"Algorithm negotiation failed\"\n\tcase notifier.LogEventTypeLoginOK:\n\t\treturn \"Login succeeded\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/api_folder.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nfunc getFolders(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfolders, err := dataprovider.GetFolders(limit, offset, order, false)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\trender.JSON(w, r, folders)\n}\n\nfunc addFolder(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar folder vfs.BaseVirtualFolder\n\terr = render.DecodeJSON(r.Body, &folder)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif err := dataprovider.AddFolder(&folder, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", folderPath, url.PathEscape(folder.Name)))\n\trenderFolder(w, r, folder.Name, claims, http.StatusCreated)\n}\n\nfunc updateFolder(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tname := getURLParam(r, \"name\")\n\tfolder, err := dataprovider.GetFolderByName(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedFolder vfs.BaseVirtualFolder\n\terr = render.DecodeJSON(r.Body, &updatedFolder)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tupdatedFolder.ID = folder.ID\n\tupdatedFolder.Name = folder.Name\n\tupdatedFolder.FsConfig.SetEmptySecretsIfNil()\n\tupdateEncryptedSecrets(&updatedFolder.FsConfig, &folder.FsConfig)\n\n\terr = dataprovider.UpdateFolder(&updatedFolder, folder.Users, folder.Groups, claims.Username,\n\t\tutil.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Folder updated\", http.StatusOK)\n}\n\nfunc renderFolder(w http.ResponseWriter, r *http.Request, name string, claims *jwt.Claims, status int) {\n\tfolder, err := dataprovider.GetFolderByName(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif hideConfidentialData(claims, r) {\n\t\tfolder.PrepareForRendering()\n\t}\n\tif status != http.StatusOK {\n\t\tctx := context.WithValue(r.Context(), render.StatusCtxKey, status)\n\t\trender.JSON(w, r.WithContext(ctx), folder)\n\t} else {\n\t\trender.JSON(w, r, folder)\n\t}\n}\n\nfunc getFolderByName(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\trenderFolder(w, r, name, claims, http.StatusOK)\n}\n\nfunc deleteFolder(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\terr = dataprovider.DeleteFolder(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Folder deleted\", http.StatusOK)\n}\n"
  },
  {
    "path": "internal/httpd/api_group.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getGroups(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tgroups, err := dataprovider.GetGroups(limit, offset, order, false)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\trender.JSON(w, r, groups)\n}\n\nfunc addGroup(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tvar group dataprovider.Group\n\terr = render.DecodeJSON(r.Body, &group)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = dataprovider.AddGroup(&group, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", groupPath, url.PathEscape(group.Name)))\n\trenderGroup(w, r, group.Name, claims, http.StatusCreated)\n}\n\nfunc updateGroup(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tname := getURLParam(r, \"name\")\n\tgroup, err := dataprovider.GroupExists(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedGroup dataprovider.Group\n\terr = render.DecodeJSON(r.Body, &updatedGroup)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tupdatedGroup.ID = group.ID\n\tupdatedGroup.Name = group.Name\n\tupdatedGroup.UserSettings.FsConfig.SetEmptySecretsIfNil()\n\tupdateEncryptedSecrets(&updatedGroup.UserSettings.FsConfig, &group.UserSettings.FsConfig)\n\terr = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr),\n\t\tclaims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Group updated\", http.StatusOK)\n}\n\nfunc renderGroup(w http.ResponseWriter, r *http.Request, name string, claims *jwt.Claims, status int) {\n\tgroup, err := dataprovider.GroupExists(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif hideConfidentialData(claims, r) {\n\t\tgroup.PrepareForRendering()\n\t}\n\tif status != http.StatusOK {\n\t\tctx := context.WithValue(r.Context(), render.StatusCtxKey, status)\n\t\trender.JSON(w, r.WithContext(ctx), group)\n\t} else {\n\t\trender.JSON(w, r, group)\n\t}\n}\n\nfunc getGroupByName(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\trenderGroup(w, r, name, claims, http.StatusOK)\n}\n\nfunc deleteGroup(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\terr = dataprovider.DeleteGroup(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Group deleted\", http.StatusOK)\n}\n"
  },
  {
    "path": "internal/httpd/api_http_user.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-chi/render\"\n\t\"github.com/rs/xid\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getUserConnection(w http.ResponseWriter, r *http.Request) (*Connection, error) {\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn nil, fmt.Errorf(\"invalid token claims %w\", err)\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\tsendAPIResponse(w, r, nil, \"Unable to retrieve your user\", getRespStatus(err))\n\t\treturn nil, err\n\t}\n\tconnID := xid.New().String()\n\tprotocol := getProtocolFromRequest(r)\n\tconnectionID := fmt.Sprintf(\"%v_%v\", protocol, connID)\n\tif err := checkHTTPClientUser(&user, r, connectionID, false, false); err != nil {\n\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\treturn nil, err\n\t}\n\tbaseConn := common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r), r.RemoteAddr, user)\n\tconnection := newConnection(baseConn, w, r)\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to add connection\", http.StatusTooManyRequests)\n\t\treturn connection, err\n\t}\n\treturn connection, nil\n}\n\nfunc readUserFolder(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tlister, err := connection.ReadDir(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to get directory lister\", getMappedStatusCode(err))\n\t\treturn\n\t}\n\trenderAPIDirContents(w, lister, false)\n}\n\nfunc createUserDir(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tif getBoolQueryParam(r, \"mkdir_parents\") {\n\t\tif err = connection.CheckParentDirs(path.Dir(name)); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"Error checking parent directories\", getMappedStatusCode(err))\n\t\t\treturn\n\t\t}\n\t}\n\terr = connection.CreateDir(name, true)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to create directory %q\", name), getMappedStatusCode(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"Directory %q created\", name), http.StatusCreated)\n}\n\nfunc deleteUserDir(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\terr = connection.RemoveAll(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to delete directory %q\", name), getMappedStatusCode(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"Directory %q deleted\", name), http.StatusOK)\n}\n\nfunc renameUserFsEntry(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\toldName := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tnewName := connection.User.GetCleanedPath(r.URL.Query().Get(\"target\"))\n\tif !connection.IsSameResource(oldName, newName) {\n\t\tif err := connection.Copy(oldName, newName); err != nil {\n\t\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Cannot perform copy step to rename %q -> %q\", oldName, newName),\n\t\t\t\tgetMappedStatusCode(err))\n\t\t\treturn\n\t\t}\n\t\tif err := connection.RemoveAll(oldName); err != nil {\n\t\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Cannot perform remove step to rename %q -> %q\", oldName, newName),\n\t\t\t\tgetMappedStatusCode(err))\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif err := connection.Rename(oldName, newName); err != nil {\n\t\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to rename %q => %q\", oldName, newName),\n\t\t\t\tgetMappedStatusCode(err))\n\t\t\treturn\n\t\t}\n\t}\n\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"%q renamed to %q\", oldName, newName), http.StatusOK)\n}\n\nfunc copyUserFsEntry(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tsource := r.URL.Query().Get(\"path\")\n\ttarget := r.URL.Query().Get(\"target\")\n\tcopyFromSource := strings.HasSuffix(source, \"/\")\n\tcopyInTarget := strings.HasSuffix(target, \"/\")\n\tsource = connection.User.GetCleanedPath(source)\n\ttarget = connection.User.GetCleanedPath(target)\n\tif copyFromSource {\n\t\tsource += \"/\"\n\t}\n\tif copyInTarget {\n\t\ttarget += \"/\"\n\t}\n\terr = connection.Copy(source, target)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to copy %q => %q\", source, target),\n\t\t\tgetMappedStatusCode(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"%q copied to %q\", source, target), http.StatusOK)\n}\n\nfunc getUserFile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tif name == \"/\" {\n\t\tsendAPIResponse(w, r, nil, \"Please set the path to a valid file\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tinfo, err := connection.Stat(name, 0)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to stat the requested file\", getMappedStatusCode(err))\n\t\treturn\n\t}\n\tif info.IsDir() {\n\t\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"Please set the path to a valid file, %q is a directory\", name), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tinline := r.URL.Query().Get(\"inline\") != \"\"\n\tif status, err := downloadFile(w, r, connection, name, info, inline, nil); err != nil {\n\t\tresp := apiResponse{\n\t\t\tError:   err.Error(),\n\t\t\tMessage: http.StatusText(status),\n\t\t}\n\t\tctx := r.Context()\n\t\tif status != 0 {\n\t\t\tctx = context.WithValue(ctx, render.StatusCtxKey, status)\n\t\t}\n\t\trender.JSON(w, r.WithContext(ctx), resp)\n\t}\n}\n\nfunc setFileDirMetadata(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tmetadata := make(map[string]int64)\n\terr := render.DecodeJSON(r.Body, &metadata)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tmTime, ok := metadata[\"modification_time\"]\n\tif !ok || !r.URL.Query().Has(\"path\") {\n\t\tsendAPIResponse(w, r, errors.New(\"please set a modification_time and a path\"), \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tattrs := common.StatAttributes{\n\t\tFlags: common.StatAttrTimes,\n\t\tAtime: util.GetTimeFromMsecSinceEpoch(mTime),\n\t\tMtime: util.GetTimeFromMsecSinceEpoch(mTime),\n\t}\n\terr = connection.SetStat(name, &attrs)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to set metadata for path %q\", name), getMappedStatusCode(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"OK\", http.StatusOK)\n}\n\nfunc uploadUserFile(w http.ResponseWriter, r *http.Request) {\n\tif maxUploadFileSize > 0 {\n\t\tr.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)\n\t}\n\n\tif !r.URL.Query().Has(\"path\") {\n\t\tsendAPIResponse(w, r, errors.New(\"please set a file path\"), \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\tfilePath := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tif getBoolQueryParam(r, \"mkdir_parents\") {\n\t\tif err = connection.CheckParentDirs(path.Dir(filePath)); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"Error checking parent directories\", getMappedStatusCode(err))\n\t\t\treturn\n\t\t}\n\t}\n\tdoUploadFile(w, r, connection, filePath) //nolint:errcheck\n}\n\nfunc doUploadFile(w http.ResponseWriter, r *http.Request, connection *Connection, filePath string) error {\n\twriter, err := connection.getFileWriter(filePath)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to write file %q\", filePath), getMappedStatusCode(err))\n\t\treturn err\n\t}\n\t_, err = io.Copy(writer, r.Body)\n\tif err != nil {\n\t\twriter.Close() //nolint:errcheck\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Error saving file %q\", filePath), getMappedStatusCode(err))\n\t\treturn err\n\t}\n\terr = writer.Close()\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Error closing file %q\", filePath), getMappedStatusCode(err))\n\t\treturn err\n\t}\n\tsetModificationTimeFromHeader(r, connection, filePath)\n\tsendAPIResponse(w, r, nil, \"Upload completed\", http.StatusCreated)\n\treturn nil\n}\n\nfunc uploadUserFiles(w http.ResponseWriter, r *http.Request) {\n\tif maxUploadFileSize > 0 {\n\t\tr.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)\n\t}\n\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tif err := common.Connections.IsNewTransferAllowed(connection.User.Username); err != nil {\n\t\tconnection.Log(logger.LevelInfo, \"denying file write due to number of transfer limits\")\n\t\tsendAPIResponse(w, r, err, \"Denying file write due to transfer count limits\",\n\t\t\thttp.StatusConflict)\n\t\treturn\n\t}\n\n\ttransferQuota := connection.GetTransferQuota()\n\tif !transferQuota.HasUploadSpace() {\n\t\tconnection.Log(logger.LevelInfo, \"denying file write due to transfer quota limits\")\n\t\tsendAPIResponse(w, r, common.ErrQuotaExceeded, \"Denying file write due to transfer quota limits\",\n\t\t\thttp.StatusRequestEntityTooLarge)\n\t\treturn\n\t}\n\n\tt := newThrottledReader(r.Body, connection.User.UploadBandwidth, connection)\n\tr.Body = t\n\terr = r.ParseMultipartForm(maxMultipartMem)\n\tif err != nil {\n\t\tconnection.RemoveTransfer(t)\n\t\tsendAPIResponse(w, r, err, \"Unable to parse multipart form\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tconnection.RemoveTransfer(t)\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tparentDir := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tfiles := r.MultipartForm.File[\"filenames\"]\n\tif len(files) == 0 {\n\t\tsendAPIResponse(w, r, nil, \"No files uploaded!\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\tif getBoolQueryParam(r, \"mkdir_parents\") {\n\t\tif err = connection.CheckParentDirs(parentDir); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"Error checking parent directories\", getMappedStatusCode(err))\n\t\t\treturn\n\t\t}\n\t}\n\tdoUploadFiles(w, r, connection, parentDir, files)\n}\n\nfunc doUploadFiles(w http.ResponseWriter, r *http.Request, connection *Connection, parentDir string,\n\tfiles []*multipart.FileHeader,\n) int {\n\tuploaded := 0\n\tconnection.User.UploadBandwidth = 0\n\tfor _, f := range files {\n\t\tfile, err := f.Open()\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to read uploaded file %q\", f.Filename), getMappedStatusCode(err))\n\t\t\treturn uploaded\n\t\t}\n\t\tdefer file.Close()\n\n\t\tfilePath := path.Join(parentDir, path.Base(util.CleanPath(f.Filename)))\n\t\twriter, err := connection.getFileWriter(filePath)\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to write file %q\", f.Filename), getMappedStatusCode(err))\n\t\t\treturn uploaded\n\t\t}\n\t\t_, err = io.Copy(writer, file)\n\t\tif err != nil {\n\t\t\twriter.Close() //nolint:errcheck\n\t\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Error saving file %q\", f.Filename), getMappedStatusCode(err))\n\t\t\treturn uploaded\n\t\t}\n\t\terr = writer.Close()\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Error closing file %q\", f.Filename), getMappedStatusCode(err))\n\t\t\treturn uploaded\n\t\t}\n\t\tuploaded++\n\t}\n\tsendAPIResponse(w, r, nil, \"Upload completed\", http.StatusCreated)\n\treturn uploaded\n}\n\nfunc deleteUserFile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tfs, p, err := connection.GetFsAndResolvedPath(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to delete file %q\", name), getMappedStatusCode(err))\n\t\treturn\n\t}\n\n\tvar fi os.FileInfo\n\tif fi, err = fs.Lstat(p); err != nil {\n\t\tconnection.Log(logger.LevelError, \"failed to remove file %q: stat error: %+v\", p, err)\n\t\terr = connection.GetFsError(fs, err)\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to delete file %q\", name), getMappedStatusCode(err))\n\t\treturn\n\t}\n\n\tif fi.IsDir() && fi.Mode()&os.ModeSymlink == 0 {\n\t\tconnection.Log(logger.LevelDebug, \"cannot remove %q is not a file/symlink\", p)\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable delete %q, it is not a file/symlink\", name), http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = connection.RemoveFile(fs, p, name, fi)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to delete file %q\", name), getMappedStatusCode(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"File %q deleted\", name), http.StatusOK)\n}\n\nfunc getUserFilesAsZipStream(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tvar filesList []string\n\terr = render.DecodeJSON(r.Body, &filesList)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tbaseDir := \"/\"\n\tfor idx := range filesList {\n\t\tfilesList[idx] = util.CleanPath(filesList[idx])\n\t}\n\n\tfilesList = util.RemoveDuplicates(filesList, false)\n\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\",\n\t\tgetCompressedFileName(connection.GetUsername(), filesList)))\n\trenderCompressedFiles(w, connection, baseDir, filesList, nil)\n}\n\nfunc getUserProfile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tuser, err := dataprovider.UserExists(claims.Username, \"\")\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tresp := userProfile{\n\t\tbaseProfile: baseProfile{\n\t\t\tEmail:           user.Email,\n\t\t\tDescription:     user.Description,\n\t\t\tAllowAPIKeyAuth: user.Filters.AllowAPIKeyAuth,\n\t\t},\n\t\tAdditionalEmails: user.Filters.AdditionalEmails,\n\t\tPublicKeys:       user.PublicKeys,\n\t\tTLSCerts:         user.Filters.TLSCerts,\n\t}\n\trender.JSON(w, r, resp)\n}\n\nfunc updateUserProfile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tvar req userProfile\n\terr = render.DecodeJSON(r.Body, &req)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tuser, userMerged, err := dataprovider.GetUserVariants(claims.Username, \"\")\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif !userMerged.CanUpdateProfile() {\n\t\tsendAPIResponse(w, r, nil, \"You are not allowed to change anything\", http.StatusForbidden)\n\t\treturn\n\t}\n\tif userMerged.CanManagePublicKeys() {\n\t\tuser.PublicKeys = req.PublicKeys\n\t}\n\tif userMerged.CanManageTLSCerts() {\n\t\tuser.Filters.TLSCerts = req.TLSCerts\n\t}\n\tif userMerged.CanChangeAPIKeyAuth() {\n\t\tuser.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth\n\t}\n\tif userMerged.CanChangeInfo() {\n\t\tuser.Email = req.Email\n\t\tuser.Filters.AdditionalEmails = req.AdditionalEmails\n\t\tuser.Description = req.Description\n\t}\n\tif err := dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), user.Role); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Profile updated\", http.StatusOK)\n}\n\nfunc changeUserPassword(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tvar pwd pwdChange\n\terr := render.DecodeJSON(r.Body, &pwd)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = doChangeUserPassword(r, pwd.CurrentPassword, pwd.NewPassword, pwd.NewPassword)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tinvalidateToken(r)\n\tsendAPIResponse(w, r, err, \"Password updated\", http.StatusOK)\n}\n\nfunc doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error {\n\tif currentPassword == \"\" || newPassword == \"\" || confirmNewPassword == \"\" {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"please provide the current password and the new one two times\"),\n\t\t\tutil.I18nErrorChangePwdRequiredFields,\n\t\t)\n\t}\n\tif newPassword != confirmNewPassword {\n\t\treturn util.NewI18nError(util.NewValidationError(\"the two password fields do not match\"), util.I18nErrorChangePwdNoMatch)\n\t}\n\tif currentPassword == newPassword {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"the new password must be different from the current one\"),\n\t\t\tutil.I18nErrorChangePwdNoDifferent,\n\t\t)\n\t}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\treturn util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)\n\t}\n\t_, err = dataprovider.CheckUserAndPass(claims.Username, currentPassword, util.GetIPFromRemoteAddress(r.RemoteAddr),\n\t\tgetProtocolFromRequest(r))\n\tif err != nil {\n\t\treturn util.NewI18nError(util.NewValidationError(\"current password does not match\"), util.I18nErrorChangePwdCurrentNoMatch)\n\t}\n\n\treturn dataprovider.UpdateUserPassword(claims.Username, newPassword, dataprovider.ActionExecutorSelf,\n\t\tutil.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n}\n\nfunc setModificationTimeFromHeader(r *http.Request, c *Connection, filePath string) {\n\tmTimeString := r.Header.Get(mTimeHeader)\n\tif mTimeString != \"\" {\n\t\t// we don't return an error here if we fail to set the modification time\n\t\tmTime, err := strconv.ParseInt(mTimeString, 10, 64)\n\t\tif err == nil {\n\t\t\tattrs := common.StatAttributes{\n\t\t\t\tFlags: common.StatAttrTimes,\n\t\t\t\tAtime: util.GetTimeFromMsecSinceEpoch(mTime),\n\t\t\t\tMtime: util.GetTimeFromMsecSinceEpoch(mTime),\n\t\t\t}\n\t\t\terr = c.SetStat(filePath, &attrs)\n\t\t\tc.Log(logger.LevelDebug, \"requested modification time %v for file %q, error: %v\",\n\t\t\t\tattrs.Mtime, filePath, err)\n\t\t} else {\n\t\t\tc.Log(logger.LevelInfo, \"invalid modification time header was ignored: %v\", mTimeString)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/api_iplist.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getIPListEntries(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, _, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tlistType, _, err := getIPListPathParams(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tentries, err := dataprovider.GetIPListEntries(listType, r.URL.Query().Get(\"filter\"), r.URL.Query().Get(\"from\"),\n\t\torder, limit)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, entries)\n}\n\nfunc getIPListEntry(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tlistType, ipOrNet, err := getIPListPathParams(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tentry, err := dataprovider.IPListEntryExists(ipOrNet, listType)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, entry)\n}\n\nfunc addIPListEntry(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tvar entry dataprovider.IPListEntry\n\terr = render.DecodeJSON(r.Body, &entry)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = dataprovider.AddIPListEntry(&entry, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%d/%s\", ipListsPath, entry.Type, url.PathEscape(entry.IPOrNet)))\n\tsendAPIResponse(w, r, nil, \"Entry added\", http.StatusCreated)\n}\n\nfunc updateIPListEntry(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tlistType, ipOrNet, err := getIPListPathParams(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tentry, err := dataprovider.IPListEntryExists(ipOrNet, listType)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tvar updatedEntry dataprovider.IPListEntry\n\terr = render.DecodeJSON(r.Body, &updatedEntry)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tupdatedEntry.Type = entry.Type\n\tupdatedEntry.IPOrNet = entry.IPOrNet\n\terr = dataprovider.UpdateIPListEntry(&updatedEntry, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Entry updated\", http.StatusOK)\n}\n\nfunc deleteIPListEntry(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tlistType, ipOrNet, err := getIPListPathParams(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = dataprovider.DeleteIPListEntry(ipOrNet, listType, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr),\n\t\tclaims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Entry deleted\", http.StatusOK)\n}\n\nfunc getIPListPathParams(r *http.Request) (dataprovider.IPListType, string, error) {\n\tlistTypeString := chi.URLParam(r, \"type\")\n\tlistType, err := strconv.Atoi(listTypeString)\n\tif err != nil {\n\t\treturn dataprovider.IPListType(listType), \"\", errors.New(\"invalid list type\")\n\t}\n\tif err := dataprovider.CheckIPListType(dataprovider.IPListType(listType)); err != nil {\n\t\treturn dataprovider.IPListType(listType), \"\", err\n\t}\n\treturn dataprovider.IPListType(listType), getURLParam(r, \"ipornet\"), nil\n}\n"
  },
  {
    "path": "internal/httpd/api_keys.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getAPIKeys(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tapiKeys, err := dataprovider.GetAPIKeys(limit, offset, order)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, apiKeys)\n}\n\nfunc getAPIKeyByID(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tkeyID := getURLParam(r, \"id\")\n\tapiKey, err := dataprovider.APIKeyExists(keyID)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tapiKey.HideConfidentialData()\n\n\trender.JSON(w, r, apiKey)\n}\n\nfunc addAPIKey(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tvar apiKey dataprovider.APIKey\n\terr = render.DecodeJSON(r.Body, &apiKey)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tapiKey.ID = 0\n\tapiKey.KeyID = \"\"\n\tapiKey.Key = \"\"\n\tapiKey.LastUseAt = 0\n\terr = dataprovider.AddAPIKey(&apiKey, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tresponse := make(map[string]string)\n\tresponse[\"message\"] = \"API key created. This is the only time the API key is visible, please save it.\"\n\tresponse[\"key\"] = apiKey.DisplayKey()\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", apiKeysPath, url.PathEscape(apiKey.KeyID)))\n\tw.Header().Add(\"X-Object-ID\", apiKey.KeyID)\n\tctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)\n\trender.JSON(w, r.WithContext(ctx), response)\n}\n\nfunc updateAPIKey(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tkeyID := getURLParam(r, \"id\")\n\tapiKey, err := dataprovider.APIKeyExists(keyID)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedAPIKey dataprovider.APIKey\n\terr = render.DecodeJSON(r.Body, &updatedAPIKey)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tupdatedAPIKey.KeyID = keyID\n\tupdatedAPIKey.Key = apiKey.Key\n\terr = dataprovider.UpdateAPIKey(&updatedAPIKey, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"API key updated\", http.StatusOK)\n}\n\nfunc deleteAPIKey(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tkeyID := getURLParam(r, \"id\")\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr = dataprovider.DeleteAPIKey(keyID, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"API key deleted\", http.StatusOK)\n}\n"
  },
  {
    "path": "internal/httpd/api_maintenance.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nfunc validateBackupFile(outputFile string) (string, error) {\n\tif outputFile == \"\" {\n\t\treturn \"\", errors.New(\"invalid or missing output-file\")\n\t}\n\tif filepath.IsAbs(outputFile) {\n\t\treturn \"\", fmt.Errorf(\"invalid output-file %q: it must be a relative path\", outputFile)\n\t}\n\tif strings.Contains(outputFile, \"..\") {\n\t\treturn \"\", fmt.Errorf(\"invalid output-file %q\", outputFile)\n\t}\n\toutputFile = filepath.Join(dataprovider.GetBackupsPath(), outputFile)\n\treturn outputFile, nil\n}\n\nfunc dumpData(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvar outputFile, outputData, indent string\n\tvar scopes []string\n\tif _, ok := r.URL.Query()[\"output-file\"]; ok {\n\t\toutputFile = strings.TrimSpace(r.URL.Query().Get(\"output-file\"))\n\t}\n\tif _, ok := r.URL.Query()[\"output-data\"]; ok {\n\t\toutputData = strings.TrimSpace(r.URL.Query().Get(\"output-data\"))\n\t}\n\tif _, ok := r.URL.Query()[\"indent\"]; ok {\n\t\tindent = strings.TrimSpace(r.URL.Query().Get(\"indent\"))\n\t}\n\tif _, ok := r.URL.Query()[\"scopes\"]; ok {\n\t\tscopes = getCommaSeparatedQueryParam(r, \"scopes\")\n\t}\n\n\tif outputData != \"1\" {\n\t\tvar err error\n\t\toutputFile, err = validateBackupFile(outputFile)\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\terr = os.MkdirAll(filepath.Dir(outputFile), 0700)\n\t\tif err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"dumping data error: %v, output file: %q\", err, outputFile)\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"dumping data to: %q\", outputFile)\n\t}\n\n\tbackup, err := dataprovider.DumpData(scopes)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"dumping data error: %v, output file: %q\", err, outputFile)\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tif outputData == \"1\" {\n\t\tw.Header().Set(\"Content-Disposition\", \"attachment; filename=\\\"sftpgo-backup.json\\\"\")\n\t\trender.JSON(w, r, backup)\n\t\treturn\n\t}\n\n\tvar dump []byte\n\tif indent == \"1\" {\n\t\tdump, err = json.MarshalIndent(backup, \"\", \"  \")\n\t} else {\n\t\tdump, err = json.Marshal(backup)\n\t}\n\tif err == nil {\n\t\terr = os.WriteFile(outputFile, dump, 0600)\n\t}\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"dumping data error: %v, output file: %q\", err, outputFile)\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tlogger.Debug(logSender, \"\", \"dumping data completed, output file: %q, error: %v\", outputFile, err)\n\tsendAPIResponse(w, r, err, \"Data saved\", http.StatusOK)\n}\n\nfunc loadDataFromRequest(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\t_, scanQuota, mode, err := getLoaddataOptions(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcontent, err := io.ReadAll(r.Body)\n\tif err != nil || len(content) == 0 {\n\t\tif len(content) == 0 {\n\t\t\terr = util.NewValidationError(\"request body is required\")\n\t\t}\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif err := restoreBackup(content, \"\", scanQuota, mode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Data restored\", http.StatusOK)\n}\n\nfunc loadData(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tinputFile, scanQuota, mode, err := getLoaddataOptions(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif !filepath.IsAbs(inputFile) {\n\t\tsendAPIResponse(w, r, fmt.Errorf(\"invalid input_file %q: it must be an absolute path\", inputFile), \"\",\n\t\t\thttp.StatusBadRequest)\n\t\treturn\n\t}\n\tfi, err := os.Stat(inputFile)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, fmt.Errorf(\"invalid input_file %q\", inputFile), \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif fi.Size() > MaxRestoreSize {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Unable to restore input file: %q size too big: %d/%d bytes\",\n\t\t\tinputFile, fi.Size(), MaxRestoreSize), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcontent, err := os.ReadFile(inputFile)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, fmt.Errorf(\"invalid input_file %q\", inputFile), \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif err := restoreBackup(content, inputFile, scanQuota, mode, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Data restored\", http.StatusOK)\n}\n\nfunc restoreBackup(content []byte, inputFile string, scanQuota, mode int, executor, ipAddress, role string) error {\n\tdump, err := dataprovider.ParseDumpData(content)\n\tif err != nil {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"invalid input_file %q\", inputFile)),\n\t\t\tutil.I18nErrorBackupFile,\n\t\t)\n\t}\n\n\tif err = RestoreConfigs(dump.Configs, mode, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreIPListEntries(dump.IPLists, inputFile, mode, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreRoles(dump.Roles, inputFile, mode, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreFolders(dump.Folders, inputFile, mode, scanQuota, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreGroups(dump.Groups, inputFile, mode, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreUsers(dump.Users, inputFile, mode, scanQuota, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreAdmins(dump.Admins, inputFile, mode, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreAPIKeys(dump.APIKeys, inputFile, mode, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreShares(dump.Shares, inputFile, mode, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreEventActions(dump.EventActions, inputFile, mode, executor, ipAddress, role); err != nil {\n\t\treturn err\n\t}\n\n\tif err = RestoreEventRules(dump.EventRules, inputFile, mode, executor, ipAddress, role, dump.Version); err != nil {\n\t\treturn err\n\t}\n\tlogger.Debug(logSender, \"\", \"backup restored\")\n\n\treturn nil\n}\n\nfunc getLoaddataOptions(r *http.Request) (string, int, int, error) {\n\tvar inputFile string\n\tvar err error\n\tscanQuota := 0\n\trestoreMode := 0\n\tif _, ok := r.URL.Query()[\"input-file\"]; ok {\n\t\tinputFile = strings.TrimSpace(r.URL.Query().Get(\"input-file\"))\n\t}\n\tif _, ok := r.URL.Query()[\"scan-quota\"]; ok {\n\t\tscanQuota, err = strconv.Atoi(r.URL.Query().Get(\"scan-quota\"))\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"invalid scan_quota: %v\", err)\n\t\t\treturn inputFile, scanQuota, restoreMode, err\n\t\t}\n\t}\n\tif _, ok := r.URL.Query()[\"mode\"]; ok {\n\t\trestoreMode, err = strconv.Atoi(r.URL.Query().Get(\"mode\"))\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"invalid mode: %v\", err)\n\t\t\treturn inputFile, scanQuota, restoreMode, err\n\t\t}\n\t}\n\treturn inputFile, scanQuota, restoreMode, err\n}\n\n// RestoreFolders restores the specified folders\nfunc RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, scanQuota int, executor, ipAddress, role string) error {\n\tfor idx := range folders {\n\t\tfolder := folders[idx]\n\t\tf, err := dataprovider.GetFolderByName(folder.Name)\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing folder %q not updated\", folder.Name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfolder.ID = f.ID\n\t\t\tfolder.Name = f.Name\n\t\t\terr = dataprovider.UpdateFolder(&folder, f.Users, f.Groups, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring existing folder %q, dump file: %q, error: %v\", folder.Name, inputFile, err)\n\t\t} else {\n\t\t\tfolder.Users = nil\n\t\t\terr = dataprovider.AddFolder(&folder, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new folder %q, dump file: %q, error: %v\", folder.Name, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore folder %q: %w\", folder.Name, err)\n\t\t}\n\t\tif scanQuota >= 1 {\n\t\t\tif common.QuotaScans.AddVFolderQuotaScan(folder.Name) {\n\t\t\t\tlogger.Debug(logSender, \"\", \"starting quota scan for restored folder: %q\", folder.Name)\n\t\t\t\tgo doFolderQuotaScan(folder) //nolint:errcheck\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// RestoreShares restores the specified shares\nfunc RestoreShares(shares []dataprovider.Share, inputFile string, mode int, executor,\n\tipAddress, role string,\n) error {\n\tfor idx := range shares {\n\t\tshare := shares[idx]\n\t\tshare.IsRestore = true\n\t\ts, err := dataprovider.ShareExists(share.ShareID, \"\")\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing share %q not updated\", share.ShareID)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tshare.ID = s.ID\n\t\t\terr = dataprovider.UpdateShare(&share, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring existing share %q, dump file: %q, error: %v\", share.ShareID, inputFile, err)\n\t\t} else {\n\t\t\terr = dataprovider.AddShare(&share, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new share %q, dump file: %q, error: %v\", share.ShareID, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore share %q: %w\", share.ShareID, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// RestoreEventActions restores the specified event actions\nfunc RestoreEventActions(actions []dataprovider.BaseEventAction, inputFile string, mode int, executor, ipAddress, role string) error {\n\tfor idx := range actions {\n\t\taction := actions[idx]\n\t\ta, err := dataprovider.EventActionExists(action.Name)\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing event action %q not updated\", a.Name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\taction.ID = a.ID\n\t\t\terr = dataprovider.UpdateEventAction(&action, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring event action %q, dump file: %q, error: %v\", action.Name, inputFile, err)\n\t\t} else {\n\t\t\terr = dataprovider.AddEventAction(&action, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new event action %q, dump file: %q, error: %v\", action.Name, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore event action %q: %w\", action.Name, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// RestoreEventRules restores the specified event rules\nfunc RestoreEventRules(rules []dataprovider.EventRule, inputFile string, mode int, executor, ipAddress,\n\trole string, dumpVersion int,\n) error {\n\tfor idx := range rules {\n\t\trule := rules[idx]\n\t\tif dumpVersion < 15 {\n\t\t\trule.Status = 1\n\t\t}\n\t\tr, err := dataprovider.EventRuleExists(rule.Name)\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing event rule %q not updated\", r.Name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trule.ID = r.ID\n\t\t\terr = dataprovider.UpdateEventRule(&rule, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring event rule %q, dump file: %q, error: %v\", rule.Name, inputFile, err)\n\t\t} else {\n\t\t\terr = dataprovider.AddEventRule(&rule, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new event rule %q, dump file: %q, error: %v\", rule.Name, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore event rule %q: %w\", rule.Name, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// RestoreAPIKeys restores the specified API keys\nfunc RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int, executor, ipAddress, role string) error {\n\tfor idx := range apiKeys {\n\t\tapiKey := apiKeys[idx]\n\t\tif apiKey.Key == \"\" {\n\t\t\tlogger.Warn(logSender, \"\", \"cannot restore empty API key\")\n\t\t\treturn fmt.Errorf(\"cannot restore an empty API key: %+v\", apiKey)\n\t\t}\n\t\tk, err := dataprovider.APIKeyExists(apiKey.KeyID)\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing API key %q not updated\", apiKey.KeyID)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tapiKey.ID = k.ID\n\t\t\terr = dataprovider.UpdateAPIKey(&apiKey, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring existing API key %q, dump file: %q, error: %v\", apiKey.KeyID, inputFile, err)\n\t\t} else {\n\t\t\terr = dataprovider.AddAPIKey(&apiKey, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new API key %q, dump file: %q, error: %v\", apiKey.KeyID, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore API key %q: %w\", apiKey.KeyID, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// RestoreAdmins restores the specified admins\nfunc RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int, executor, ipAddress, role string) error {\n\tfor idx := range admins {\n\t\tadmin := admins[idx]\n\t\ta, err := dataprovider.AdminExists(admin.Username)\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing admin %q not updated\", a.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tadmin.ID = a.ID\n\t\t\tadmin.Username = a.Username\n\t\t\terr = dataprovider.UpdateAdmin(&admin, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring existing admin %q, dump file: %q, error: %v\", admin.Username, inputFile, err)\n\t\t} else {\n\t\t\terr = dataprovider.AddAdmin(&admin, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new admin %q, dump file: %q, error: %v\", admin.Username, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore admin %q: %w\", admin.Username, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// RestoreConfigs restores the specified provider configs\nfunc RestoreConfigs(configs *dataprovider.Configs, mode int, executor, ipAddress,\n\texecutorRole string,\n) error {\n\tif configs == nil {\n\t\treturn nil\n\t}\n\tc, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore configs, error loading existing from db: %w\", err)\n\t}\n\tif c.UpdatedAt > 0 {\n\t\tif mode == 1 {\n\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing configs not updated\")\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn dataprovider.UpdateConfigs(configs, executor, ipAddress, executorRole)\n}\n\n// RestoreIPListEntries restores the specified IP list entries\nfunc RestoreIPListEntries(entries []dataprovider.IPListEntry, inputFile string, mode int, executor, ipAddress,\n\texecutorRole string,\n) error {\n\tfor idx := range entries {\n\t\tentry := entries[idx]\n\t\te, err := dataprovider.IPListEntryExists(entry.IPOrNet, entry.Type)\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing IP list entry %s-%s not updated\",\n\t\t\t\t\te.Type.AsString(), e.IPOrNet)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr = dataprovider.UpdateIPListEntry(&entry, executor, ipAddress, executorRole)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring existing IP list entry: %s-%s, dump file: %q, error: %v\",\n\t\t\t\tentry.Type.AsString(), entry.IPOrNet, inputFile, err)\n\t\t} else {\n\t\t\terr = dataprovider.AddIPListEntry(&entry, executor, ipAddress, executorRole)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new IP list entry %s-%s, dump file: %q, error: %v\",\n\t\t\t\tentry.Type.AsString(), entry.IPOrNet, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore IP list entry %s-%s: %w\", entry.Type.AsString(), entry.IPOrNet, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// RestoreRoles restores the specified roles\nfunc RestoreRoles(roles []dataprovider.Role, inputFile string, mode int, executor, ipAddress, executorRole string) error {\n\tfor idx := range roles {\n\t\trole := roles[idx]\n\t\tr, err := dataprovider.RoleExists(role.Name)\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing role %q not updated\", r.Name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trole.ID = r.ID\n\t\t\terr = dataprovider.UpdateRole(&role, executor, ipAddress, executorRole)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring existing role: %q, dump file: %q, error: %v\", role.Name, inputFile, err)\n\t\t} else {\n\t\t\terr = dataprovider.AddRole(&role, executor, ipAddress, executorRole)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new role: %q, dump file: %q, error: %v\", role.Name, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore role %q: %w\", role.Name, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// RestoreGroups restores the specified groups\nfunc RestoreGroups(groups []dataprovider.Group, inputFile string, mode int, executor, ipAddress, role string) error {\n\tfor idx := range groups {\n\t\tgroup := groups[idx]\n\t\tg, err := dataprovider.GroupExists(group.Name)\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing group %q not updated\", g.Name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tgroup.ID = g.ID\n\t\t\tgroup.Name = g.Name\n\t\t\terr = dataprovider.UpdateGroup(&group, g.Users, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring existing group: %q, dump file: %q, error: %v\", group.Name, inputFile, err)\n\t\t} else {\n\t\t\terr = dataprovider.AddGroup(&group, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new group: %q, dump file: %q, error: %v\", group.Name, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore group %q: %w\", group.Name, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// RestoreUsers restores the specified users\nfunc RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int, executor, ipAddress, role string) error {\n\tfor idx := range users {\n\t\tuser := users[idx]\n\t\tu, err := dataprovider.UserExists(user.Username, \"\")\n\t\tif err == nil {\n\t\t\tif mode == 1 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"loaddata mode 1, existing user %q not updated\", u.Username)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tuser.ID = u.ID\n\t\t\tuser.Username = u.Username\n\t\t\terr = dataprovider.UpdateUser(&user, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"restoring existing user: %q, dump file: %q, error: %v\", user.Username, inputFile, err)\n\t\t\tif mode == 2 && err == nil {\n\t\t\t\tdisconnectUser(user.Username, executor, role)\n\t\t\t}\n\t\t} else {\n\t\t\terr = dataprovider.AddUser(&user, executor, ipAddress, role)\n\t\t\tlogger.Debug(logSender, \"\", \"adding new user: %q, dump file: %q, error: %v\", user.Username, inputFile, err)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to restore user %q: %w\", user.Username, err)\n\t\t}\n\t\tif scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) {\n\t\t\tuser, err = dataprovider.GetUserWithGroupSettings(user.Username, \"\")\n\t\t\tif err == nil && common.QuotaScans.AddUserQuotaScan(user.Username, user.Role) {\n\t\t\t\tlogger.Debug(logSender, \"\", \"starting quota scan for restored user: %q\", user.Username)\n\t\t\t\tgo doUserQuotaScan(&user) //nolint:errcheck\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/httpd/api_mfa.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\terrRecoveryCodeForbidden = errors.New(\"recovery codes are not available with two-factor authentication disabled\")\n)\n\ntype generateTOTPRequest struct {\n\tConfigName string `json:\"config_name\"`\n}\n\ntype generateTOTPResponse struct {\n\tConfigName string `json:\"config_name\"`\n\tIssuer     string `json:\"issuer\"`\n\tSecret     string `json:\"secret\"`\n\tURL        string `json:\"url\"`\n\tQRCode     []byte `json:\"qr_code\"`\n}\n\ntype validateTOTPRequest struct {\n\tConfigName string `json:\"config_name\"`\n\tPasscode   string `json:\"passcode\"`\n\tSecret     string `json:\"secret\"`\n}\n\ntype recoveryCode struct {\n\tCode string `json:\"code\"`\n\tUsed bool   `json:\"used\"`\n}\n\nfunc getTOTPConfigs(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\trender.JSON(w, r, mfa.GetAvailableTOTPConfigs())\n}\n\nfunc generateTOTPSecret(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tvar accountName string\n\tif hasUserAudience(claims) {\n\t\taccountName = fmt.Sprintf(\"User %q\", claims.Username)\n\t} else {\n\t\taccountName = fmt.Sprintf(\"Admin %q\", claims.Username)\n\t}\n\n\tvar req generateTOTPRequest\n\terr = render.DecodeJSON(r.Body, &req)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tconfigName, key, qrCode, err := mfa.GenerateTOTPSecret(req.ConfigName, accountName)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\trender.JSON(w, r, generateTOTPResponse{\n\t\tConfigName: configName,\n\t\tIssuer:     key.Issuer(),\n\t\tSecret:     key.Secret(),\n\t\tURL:        key.URL(),\n\t\tQRCode:     qrCode,\n\t})\n}\n\nfunc getQRCode(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\timg, err := mfa.GenerateQRCodeFromURL(r.URL.Query().Get(\"url\"), 400, 400)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, nil, \"unable to generate qr code\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\timgSize := int64(len(img))\n\tw.Header().Set(\"Content-Length\", strconv.FormatInt(imgSize, 10))\n\tw.Header().Set(\"Content-Type\", \"image/png\")\n\tio.CopyN(w, bytes.NewBuffer(img), imgSize) //nolint:errcheck\n}\n\nfunc saveTOTPConfig(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\trecoveryCodes := make([]dataprovider.RecoveryCode, 0, 12)\n\tfor i := 0; i < 12; i++ {\n\t\tcode := getNewRecoveryCode()\n\t\trecoveryCodes = append(recoveryCodes, dataprovider.RecoveryCode{Secret: kms.NewPlainSecret(code)})\n\t}\n\tbaseURL := webBaseClientPath\n\tif hasUserAudience(claims) {\n\t\tif err := saveUserTOTPConfig(claims.Username, r, recoveryCodes); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif err := saveAdminTOTPConfig(claims.Username, r, recoveryCodes); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\tbaseURL = webBasePath\n\t}\n\tif claims.MustSetTwoFactorAuth {\n\t\t// force logout\n\t\tdefer func() {\n\t\t\tremoveCookie(w, r, baseURL)\n\t\t}()\n\t}\n\n\tsendAPIResponse(w, r, nil, \"TOTP configuration saved\", http.StatusOK)\n}\n\nfunc validateTOTPPasscode(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvar req validateTOTPRequest\n\terr := render.DecodeJSON(r.Body, &req)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tmatch, err := mfa.ValidateTOTPPasscode(req.ConfigName, req.Passcode, req.Secret)\n\tif !match || err != nil {\n\t\tsendAPIResponse(w, r, err, \"Invalid passcode\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Passcode successfully validated\", http.StatusOK)\n}\n\nfunc getRecoveryCodes(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\trecoveryCodes := make([]recoveryCode, 0, 12)\n\tvar accountRecoveryCodes []dataprovider.RecoveryCode\n\tif hasUserAudience(claims) {\n\t\tuser, err := dataprovider.UserExists(claims.Username, \"\")\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\tif !user.Filters.TOTPConfig.Enabled {\n\t\t\tsendAPIResponse(w, r, errRecoveryCodeForbidden, \"\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t\taccountRecoveryCodes = user.Filters.RecoveryCodes\n\t} else {\n\t\tadmin, err := dataprovider.AdminExists(claims.Username)\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\tif !admin.Filters.TOTPConfig.Enabled {\n\t\t\tsendAPIResponse(w, r, errRecoveryCodeForbidden, \"\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t\taccountRecoveryCodes = admin.Filters.RecoveryCodes\n\t}\n\n\tfor _, code := range accountRecoveryCodes {\n\t\tif err := code.Secret.Decrypt(); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"Unable to decrypt recovery codes\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\trecoveryCodes = append(recoveryCodes, recoveryCode{\n\t\t\tCode: code.Secret.GetPayload(),\n\t\t\tUsed: code.Used,\n\t\t})\n\t}\n\trender.JSON(w, r, recoveryCodes)\n}\n\nfunc generateRecoveryCodes(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\trecoveryCodes := make([]string, 0, 12)\n\taccountRecoveryCodes := make([]dataprovider.RecoveryCode, 0, 12)\n\tfor i := 0; i < 12; i++ {\n\t\tcode := getNewRecoveryCode()\n\t\trecoveryCodes = append(recoveryCodes, code)\n\t\taccountRecoveryCodes = append(accountRecoveryCodes, dataprovider.RecoveryCode{Secret: kms.NewPlainSecret(code)})\n\t}\n\tif hasUserAudience(claims) {\n\t\tuser, err := dataprovider.UserExists(claims.Username, \"\")\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\tif !user.Filters.TOTPConfig.Enabled {\n\t\t\tsendAPIResponse(w, r, errRecoveryCodeForbidden, \"\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t\tuser.Filters.RecoveryCodes = accountRecoveryCodes\n\t\tif err := dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), user.Role); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tadmin, err := dataprovider.AdminExists(claims.Username)\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\tif !admin.Filters.TOTPConfig.Enabled {\n\t\t\tsendAPIResponse(w, r, errRecoveryCodeForbidden, \"\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t\tadmin.Filters.RecoveryCodes = accountRecoveryCodes\n\t\tif err := dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), admin.Role); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t}\n\n\trender.JSON(w, r, recoveryCodes)\n}\n\nfunc getNewRecoveryCode() string {\n\treturn fmt.Sprintf(\"RC-%v\", strings.ToUpper(util.GenerateUniqueID()))\n}\n\nfunc saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []dataprovider.RecoveryCode) error {\n\tuser, userMerged, err := dataprovider.GetUserVariants(username, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tcurrentTOTPSecret := user.Filters.TOTPConfig.Secret\n\tuser.Filters.TOTPConfig.Secret = nil\n\terr = render.DecodeJSON(r.Body, &user.Filters.TOTPConfig)\n\tif err != nil {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"unable to decode JSON body: %v\", err))\n\t}\n\tif !user.Filters.TOTPConfig.Enabled && len(userMerged.Filters.TwoFactorAuthProtocols) > 0 {\n\t\treturn util.NewValidationError(\"two-factor authentication must be enabled\")\n\t}\n\tfor _, p := range userMerged.Filters.TwoFactorAuthProtocols {\n\t\tif !slices.Contains(user.Filters.TOTPConfig.Protocols, p) {\n\t\t\treturn util.NewValidationError(fmt.Sprintf(\"totp: the following protocols are required: %q\",\n\t\t\t\tstrings.Join(userMerged.Filters.TwoFactorAuthProtocols, \", \")))\n\t\t}\n\t}\n\tif user.Filters.TOTPConfig.Secret == nil || !user.Filters.TOTPConfig.Secret.IsPlain() {\n\t\tuser.Filters.TOTPConfig.Secret = currentTOTPSecret\n\t}\n\tif user.Filters.TOTPConfig.Enabled {\n\t\tif user.CountUnusedRecoveryCodes() < 5 && user.Filters.TOTPConfig.Enabled {\n\t\t\tuser.Filters.RecoveryCodes = recoveryCodes\n\t\t}\n\t} else {\n\t\tuser.Filters.RecoveryCodes = nil\n\t}\n\treturn dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), user.Role)\n}\n\nfunc saveAdminTOTPConfig(username string, r *http.Request, recoveryCodes []dataprovider.RecoveryCode) error {\n\tadmin, err := dataprovider.AdminExists(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcurrentTOTPSecret := admin.Filters.TOTPConfig.Secret\n\tadmin.Filters.TOTPConfig.Secret = nil\n\terr = render.DecodeJSON(r.Body, &admin.Filters.TOTPConfig)\n\tif err != nil {\n\t\treturn util.NewValidationError(fmt.Sprintf(\"unable to decode JSON body: %v\", err))\n\t}\n\tif !admin.Filters.TOTPConfig.Enabled && admin.Filters.RequireTwoFactor {\n\t\treturn util.NewValidationError(\"two-factor authentication must be enabled\")\n\t}\n\tif admin.Filters.TOTPConfig.Enabled {\n\t\tif admin.CountUnusedRecoveryCodes() < 5 && admin.Filters.TOTPConfig.Enabled {\n\t\t\tadmin.Filters.RecoveryCodes = recoveryCodes\n\t\t}\n\t} else {\n\t\tadmin.Filters.RecoveryCodes = nil\n\t}\n\tif admin.Filters.TOTPConfig.Secret == nil || !admin.Filters.TOTPConfig.Secret.IsPlain() {\n\t\tadmin.Filters.TOTPConfig.Secret = currentTOTPSecret\n\t}\n\treturn dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), admin.Role)\n}\n"
  },
  {
    "path": "internal/httpd/api_quota.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tquotaUpdateModeAdd   = \"add\"\n\tquotaUpdateModeReset = \"reset\"\n)\n\ntype quotaUsage struct {\n\tUsedQuotaSize  int64 `json:\"used_quota_size\"`\n\tUsedQuotaFiles int   `json:\"used_quota_files\"`\n}\n\ntype transferQuotaUsage struct {\n\tUsedUploadDataTransfer   int64 `json:\"used_upload_data_transfer\"`\n\tUsedDownloadDataTransfer int64 `json:\"used_download_data_transfer\"`\n}\n\nfunc getUsersQuotaScans(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\trender.JSON(w, r, common.QuotaScans.GetUsersQuotaScans(claims.Role))\n}\n\nfunc getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\trender.JSON(w, r, common.QuotaScans.GetVFoldersQuotaScans())\n}\n\nfunc updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvar usage quotaUsage\n\terr := render.DecodeJSON(r.Body, &usage)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tdoUpdateUserQuotaUsage(w, r, getURLParam(r, \"username\"), usage)\n}\n\nfunc updateFolderQuotaUsage(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvar usage quotaUsage\n\terr := render.DecodeJSON(r.Body, &usage)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tdoUpdateFolderQuotaUsage(w, r, getURLParam(r, \"name\"), usage)\n}\n\nfunc startUserQuotaScan(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tdoStartUserQuotaScan(w, r, getURLParam(r, \"username\"))\n}\n\nfunc startFolderQuotaScan(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tdoStartFolderQuotaScan(w, r, getURLParam(r, \"name\"))\n}\n\nfunc updateUserTransferQuotaUsage(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tvar usage transferQuotaUsage\n\terr = render.DecodeJSON(r.Body, &usage)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif usage.UsedUploadDataTransfer < 0 || usage.UsedDownloadDataTransfer < 0 {\n\t\tsendAPIResponse(w, r, errors.New(\"invalid used transfer quota parameters, negative values are not allowed\"),\n\t\t\t\"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tmode, err := getQuotaUpdateMode(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(getURLParam(r, \"username\"), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif mode == quotaUpdateModeAdd && !user.HasTransferQuotaRestrictions() && dataprovider.GetQuotaTracking() == 2 {\n\t\tsendAPIResponse(w, r, errors.New(\"this user has no transfer quota restrictions, only reset mode is supported\"),\n\t\t\t\"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = dataprovider.UpdateUserTransferQuota(&user, usage.UsedUploadDataTransfer, usage.UsedDownloadDataTransfer,\n\t\tmode == quotaUpdateModeReset)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Quota updated\", http.StatusOK)\n}\n\nfunc doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username string, usage quotaUsage) {\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif usage.UsedQuotaFiles < 0 || usage.UsedQuotaSize < 0 {\n\t\tsendAPIResponse(w, r, errors.New(\"invalid used quota parameters, negative values are not allowed\"),\n\t\t\t\"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tmode, err := getQuotaUpdateMode(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(username, claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif mode == quotaUpdateModeAdd && !user.HasQuotaRestrictions() && dataprovider.GetQuotaTracking() == 2 {\n\t\tsendAPIResponse(w, r, errors.New(\"this user has no quota restrictions, only reset mode is supported\"),\n\t\t\t\"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif !common.QuotaScans.AddUserQuotaScan(user.Username, user.Role) {\n\t\tsendAPIResponse(w, r, err, \"A quota scan is in progress for this user\", http.StatusConflict)\n\t\treturn\n\t}\n\tdefer common.QuotaScans.RemoveUserQuotaScan(user.Username)\n\terr = dataprovider.UpdateUserQuota(&user, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Quota updated\", http.StatusOK)\n}\n\nfunc doUpdateFolderQuotaUsage(w http.ResponseWriter, r *http.Request, name string, usage quotaUsage) {\n\tif usage.UsedQuotaFiles < 0 || usage.UsedQuotaSize < 0 {\n\t\tsendAPIResponse(w, r, errors.New(\"invalid used quota parameters, negative values are not allowed\"),\n\t\t\t\"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tmode, err := getQuotaUpdateMode(r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tfolder, err := dataprovider.GetFolderByName(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif !common.QuotaScans.AddVFolderQuotaScan(folder.Name) {\n\t\tsendAPIResponse(w, r, err, \"A quota scan is in progress for this folder\", http.StatusConflict)\n\t\treturn\n\t}\n\tdefer common.QuotaScans.RemoveVFolderQuotaScan(folder.Name)\n\terr = dataprovider.UpdateVirtualFolderQuota(&folder, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t} else {\n\t\tsendAPIResponse(w, r, err, \"Quota updated\", http.StatusOK)\n\t}\n}\n\nfunc doStartUserQuotaScan(w http.ResponseWriter, r *http.Request, username string) {\n\tif dataprovider.GetQuotaTracking() == 0 {\n\t\tsendAPIResponse(w, r, nil, \"Quota tracking is disabled!\", http.StatusForbidden)\n\t\treturn\n\t}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(username, claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif !common.QuotaScans.AddUserQuotaScan(user.Username, user.Role) {\n\t\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"Another scan is already in progress for user %q\", username),\n\t\t\thttp.StatusConflict)\n\t\treturn\n\t}\n\tgo doUserQuotaScan(&user) //nolint:errcheck\n\tsendAPIResponse(w, r, err, \"Scan started\", http.StatusAccepted)\n}\n\nfunc doStartFolderQuotaScan(w http.ResponseWriter, r *http.Request, name string) {\n\tif dataprovider.GetQuotaTracking() == 0 {\n\t\tsendAPIResponse(w, r, nil, \"Quota tracking is disabled!\", http.StatusForbidden)\n\t\treturn\n\t}\n\tfolder, err := dataprovider.GetFolderByName(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif !common.QuotaScans.AddVFolderQuotaScan(folder.Name) {\n\t\tsendAPIResponse(w, r, err, fmt.Sprintf(\"Another scan is already in progress for folder %q\", name),\n\t\t\thttp.StatusConflict)\n\t\treturn\n\t}\n\tgo doFolderQuotaScan(folder) //nolint:errcheck\n\tsendAPIResponse(w, r, err, \"Scan started\", http.StatusAccepted)\n}\n\nfunc doUserQuotaScan(user *dataprovider.User) error {\n\tdefer common.QuotaScans.RemoveUserQuotaScan(user.Username)\n\tnumFiles, size, err := user.ScanQuota()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error scanning user quota %q: %v\", user.Username, err)\n\t\treturn err\n\t}\n\terr = dataprovider.UpdateUserQuota(user, numFiles, size, true)\n\tlogger.Debug(logSender, \"\", \"user quota scanned, user: %q, error: %v\", user.Username, err)\n\treturn err\n}\n\nfunc doFolderQuotaScan(folder vfs.BaseVirtualFolder) error {\n\tdefer common.QuotaScans.RemoveVFolderQuotaScan(folder.Name)\n\tf := vfs.VirtualFolder{\n\t\tBaseVirtualFolder: folder,\n\t\tVirtualPath:       \"/\",\n\t}\n\tnumFiles, size, err := f.ScanQuota()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error scanning folder %q: %v\", folder.Name, err)\n\t\treturn err\n\t}\n\terr = dataprovider.UpdateVirtualFolderQuota(&folder, numFiles, size, true)\n\tlogger.Debug(logSender, \"\", \"virtual folder %q scanned, error: %v\", folder.Name, err)\n\treturn err\n}\n\nfunc getQuotaUpdateMode(r *http.Request) (string, error) {\n\tmode := quotaUpdateModeReset\n\tif _, ok := r.URL.Query()[\"mode\"]; ok {\n\t\tmode = r.URL.Query().Get(\"mode\")\n\t\tif mode != quotaUpdateModeReset && mode != quotaUpdateModeAdd {\n\t\t\treturn \"\", errors.New(\"invalid mode\")\n\t\t}\n\t}\n\treturn mode, nil\n}\n"
  },
  {
    "path": "internal/httpd/api_retention.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n)\n\nfunc getRetentionChecks(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\trender.JSON(w, r, common.RetentionChecks.Get(claims.Role))\n}\n"
  },
  {
    "path": "internal/httpd/api_role.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getRoles(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\troles, err := dataprovider.GetRoles(limit, offset, order, false)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\trender.JSON(w, r, roles)\n}\n\nfunc addRole(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar role dataprovider.Role\n\terr = render.DecodeJSON(r.Body, &role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\terr = dataprovider.AddRole(&role, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t} else {\n\t\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", rolesPath, url.PathEscape(role.Name)))\n\t\trenderRole(w, r, role.Name, http.StatusCreated)\n\t}\n}\n\nfunc updateRole(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tname := getURLParam(r, \"name\")\n\trole, err := dataprovider.RoleExists(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedRole dataprovider.Role\n\terr = render.DecodeJSON(r.Body, &updatedRole)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tupdatedRole.ID = role.ID\n\tupdatedRole.Name = role.Name\n\terr = dataprovider.UpdateRole(&updatedRole, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Role updated\", http.StatusOK)\n}\n\nfunc renderRole(w http.ResponseWriter, r *http.Request, name string, status int) {\n\trole, err := dataprovider.RoleExists(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif status != http.StatusOK {\n\t\tctx := context.WithValue(r.Context(), render.StatusCtxKey, status)\n\t\trender.JSON(w, r.WithContext(ctx), role)\n\t} else {\n\t\trender.JSON(w, r, role)\n\t}\n}\n\nfunc getRoleByName(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tname := getURLParam(r, \"name\")\n\trenderRole(w, r, name, http.StatusOK)\n}\n\nfunc deleteRole(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\terr = dataprovider.DeleteRole(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Role deleted\", http.StatusOK)\n}\n"
  },
  {
    "path": "internal/httpd/api_shares.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-chi/render\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc getShares(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tshares, err := dataprovider.GetShares(limit, offset, order, claims.Username)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, shares)\n}\n\nfunc getShareByID(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tshareID := getURLParam(r, \"id\")\n\tshare, err := dataprovider.ShareExists(shareID, claims.Username)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tshare.HideConfidentialData()\n\n\trender.JSON(w, r, share)\n}\n\nfunc addShare(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to retrieve your user\", getRespStatus(err))\n\t\treturn\n\t}\n\tvar share dataprovider.Share\n\tif user.Filters.DefaultSharesExpiration > 0 {\n\t\tshare.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(user.Filters.DefaultSharesExpiration)))\n\t}\n\terr = render.DecodeJSON(r.Body, &share)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(share.ExpiresAt)); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tshare.ID = 0\n\tshare.ShareID = util.GenerateUniqueID()\n\tshare.LastUseAt = 0\n\tshare.Username = claims.Username\n\tif share.Name == \"\" {\n\t\tshare.Name = share.ShareID\n\t}\n\tif share.Password == \"\" {\n\t\tif slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {\n\t\t\tsendAPIResponse(w, r, nil, \"You are not authorized to share files/folders without a password\",\n\t\t\t\thttp.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t}\n\terr = dataprovider.AddShare(&share, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", userSharesPath, url.PathEscape(share.ShareID)))\n\tw.Header().Add(\"X-Object-ID\", share.ShareID)\n\tsendAPIResponse(w, r, nil, \"Share created\", http.StatusCreated)\n}\n\nfunc updateShare(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to retrieve your user\", getRespStatus(err))\n\t\treturn\n\t}\n\tshareID := getURLParam(r, \"id\")\n\tshare, err := dataprovider.ShareExists(shareID, claims.Username)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedShare dataprovider.Share\n\terr = render.DecodeJSON(r.Body, &updatedShare)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tupdatedShare.ShareID = shareID\n\tupdatedShare.Username = claims.Username\n\tif updatedShare.Password == redactedSecret {\n\t\tupdatedShare.Password = share.Password\n\t}\n\tif updatedShare.Password == \"\" {\n\t\tif slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {\n\t\t\tsendAPIResponse(w, r, nil, \"You are not authorized to share files/folders without a password\",\n\t\t\t\thttp.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t}\n\tif err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(updatedShare.ExpiresAt)); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\terr = dataprovider.UpdateShare(&updatedShare, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Share updated\", http.StatusOK)\n}\n\nfunc deleteShare(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tshareID := getURLParam(r, \"id\")\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr = dataprovider.DeleteShare(shareID, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Share deleted\", http.StatusOK)\n}\n\nfunc (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := validateBrowsableShare(share, connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tname, err := getBrowsableSharedPath(share.Paths[0], r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to add connection\", http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tlister, err := connection.ReadDir(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to get directory lister\", getMappedStatusCode(err))\n\t\treturn\n\t}\n\trenderAPIDirContents(w, lister, true)\n}\n\nfunc (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := validateBrowsableShare(share, connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tname, err := getBrowsableSharedPath(share.Paths[0], r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to add connection\", http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tinfo, err := connection.Stat(name, 1)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to stat the requested file\", getMappedStatusCode(err))\n\t\treturn\n\t}\n\tif info.IsDir() {\n\t\tsendAPIResponse(w, r, nil, fmt.Sprintf(\"Please set the path to a valid file, %q is a directory\", name),\n\t\t\thttp.StatusBadRequest)\n\t\treturn\n\t}\n\n\tinline := r.URL.Query().Get(\"inline\") != \"\"\n\tdataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck\n\tif status, err := downloadFile(w, r, connection, name, info, inline, &share); err != nil {\n\t\tdataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck\n\t\tresp := apiResponse{\n\t\t\tError:   err.Error(),\n\t\t\tMessage: http.StatusText(status),\n\t\t}\n\t\tctx := r.Context()\n\t\tif status != 0 {\n\t\t\tctx = context.WithValue(ctx, render.StatusCtxKey, status)\n\t\t}\n\t\trender.JSON(w, r.WithContext(ctx), resp)\n\t}\n}\n\nfunc (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to add connection\", http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tcompress := true\n\tvar info os.FileInfo\n\tif len(share.Paths) == 1 {\n\t\tinfo, err = connection.Stat(share.Paths[0], 1)\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\tif info.Mode().IsRegular() && r.URL.Query().Get(\"compress\") == \"false\" {\n\t\t\tcompress = false\n\t\t}\n\t}\n\n\tdataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck\n\tif compress {\n\t\ttransferQuota := connection.GetTransferQuota()\n\t\tif !transferQuota.HasDownloadSpace() {\n\t\t\terr = connection.GetReadQuotaExceededError()\n\t\t\tconnection.Log(logger.LevelInfo, \"denying share read due to quota limits\")\n\t\t\tsendAPIResponse(w, r, err, \"\", getMappedStatusCode(err))\n\t\t\tdataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck\n\t\t\treturn\n\t\t}\n\t\tbaseDir := \"/\"\n\t\tif info != nil && info.IsDir() {\n\t\t\tbaseDir = share.Paths[0]\n\t\t\tshare.Paths[0] = \"/\"\n\t\t}\n\t\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"share-%v.zip\\\"\", share.Name))\n\t\trenderCompressedFiles(w, connection, baseDir, share.Paths, &share)\n\t\treturn\n\t}\n\tif status, err := downloadFile(w, r, connection, share.Paths[0], info, false, &share); err != nil {\n\t\tdataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck\n\t\tresp := apiResponse{\n\t\t\tError:   err.Error(),\n\t\t\tMessage: http.StatusText(status),\n\t\t}\n\t\tctx := r.Context()\n\t\tif status != 0 {\n\t\t\tctx = context.WithValue(ctx, render.StatusCtxKey, status)\n\t\t}\n\t\trender.JSON(w, r.WithContext(ctx), resp)\n\t}\n}\n\nfunc (s *httpdServer) uploadFileToShare(w http.ResponseWriter, r *http.Request) {\n\tif maxUploadFileSize > 0 {\n\t\tr.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)\n\t}\n\tname := getURLParam(r, \"name\")\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tfilePath := util.CleanPath(path.Join(share.Paths[0], name))\n\texpectedPrefix := share.Paths[0]\n\tif !strings.HasSuffix(expectedPrefix, \"/\") {\n\t\texpectedPrefix += \"/\"\n\t}\n\tif !strings.HasPrefix(filePath, expectedPrefix) {\n\t\tsendAPIResponse(w, r, err, \"Uploading outside the share is not allowed\", http.StatusForbidden)\n\t\treturn\n\t}\n\tdataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck\n\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to add connection\", http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\tif getBoolQueryParam(r, \"mkdir_parents\") {\n\t\tif err = connection.CheckParentDirs(path.Dir(filePath)); err != nil {\n\t\t\tsendAPIResponse(w, r, err, \"Error checking parent directories\", getMappedStatusCode(err))\n\t\t\treturn\n\t\t}\n\t}\n\tif err := doUploadFile(w, r, connection, filePath); err != nil {\n\t\tdataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck\n\t}\n}\n\nfunc (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request) {\n\tif maxUploadFileSize > 0 {\n\t\tr.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)\n\t}\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := common.Connections.IsNewTransferAllowed(connection.User.Username); err != nil {\n\t\tconnection.Log(logger.LevelInfo, \"denying file write due to number of transfer limits\")\n\t\tsendAPIResponse(w, r, err, \"Denying file write due to transfer count limits\",\n\t\t\thttp.StatusConflict)\n\t\treturn\n\t}\n\n\ttransferQuota := connection.GetTransferQuota()\n\tif !transferQuota.HasUploadSpace() {\n\t\tconnection.Log(logger.LevelInfo, \"denying file write due to transfer quota limits\")\n\t\tsendAPIResponse(w, r, common.ErrQuotaExceeded, \"Denying file write due to transfer quota limits\",\n\t\t\thttp.StatusRequestEntityTooLarge)\n\t\treturn\n\t}\n\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to add connection\", http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tt := newThrottledReader(r.Body, connection.User.UploadBandwidth, connection)\n\tr.Body = t\n\terr = r.ParseMultipartForm(maxMultipartMem)\n\tif err != nil {\n\t\tconnection.RemoveTransfer(t)\n\t\tsendAPIResponse(w, r, err, \"Unable to parse multipart form\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tconnection.RemoveTransfer(t)\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tfiles := r.MultipartForm.File[\"filenames\"]\n\tif len(files) == 0 {\n\t\tsendAPIResponse(w, r, nil, \"No files uploaded!\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif share.MaxTokens > 0 {\n\t\tif len(files) > (share.MaxTokens - share.UsedTokens) {\n\t\t\tsendAPIResponse(w, r, nil, \"Allowed usage exceeded\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t}\n\tdataprovider.UpdateShareLastUse(&share, len(files)) //nolint:errcheck\n\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\tnumUploads := doUploadFiles(w, r, connection, share.Paths[0], files)\n\tif numUploads != len(files) {\n\t\tdataprovider.UpdateShareLastUse(&share, numUploads-len(files)) //nolint:errcheck\n\t}\n}\n\nfunc (s *httpdServer) getShareClaims(r *http.Request, shareID string) (context.Context, *jwt.Claims, error) {\n\ttoken, err := jwt.VerifyRequest(s.tokenAuth, r, jwt.TokenFromCookie)\n\tif err != nil || token == nil {\n\t\treturn nil, nil, errInvalidToken\n\t}\n\ttokenString := jwt.TokenFromCookie(r)\n\tif tokenString == \"\" || invalidatedJWTTokens.Get(tokenString) {\n\t\treturn nil, nil, errInvalidToken\n\t}\n\tif !token.Audience.Contains(tokenAudienceWebShare) {\n\t\tlogger.Debug(logSender, \"\", \"invalid token audience for share %q\", shareID)\n\t\treturn nil, nil, errInvalidToken\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := validateIPForToken(token, ipAddr); err != nil {\n\t\tlogger.Debug(logSender, \"\", \"token for share %q is not valid for the ip address %q\", shareID, ipAddr)\n\t\treturn nil, nil, err\n\t}\n\tif token.Username != shareID {\n\t\tlogger.Debug(logSender, \"\", \"token not valid for share %q\", shareID)\n\t\treturn nil, nil, errInvalidToken\n\t}\n\tctx := jwt.NewContext(r.Context(), token, nil)\n\treturn ctx, token, nil\n}\n\nfunc (s *httpdServer) checkWebClientShareCredentials(w http.ResponseWriter, r *http.Request, share *dataprovider.Share) error {\n\tdoRedirect := func() {\n\t\tredirectURL := path.Join(webClientPubSharesPath, share.ShareID, fmt.Sprintf(\"login?next=%s\", url.QueryEscape(r.RequestURI)))\n\t\thttp.Redirect(w, r, redirectURL, http.StatusFound)\n\t}\n\n\tif _, _, err := s.getShareClaims(r, share.ShareID); err != nil {\n\t\tdoRedirect()\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, validScopes []dataprovider.ShareScope,\n) (dataprovider.Share, *Connection, error) {\n\tisWebClient := isWebClientRequest(r)\n\trenderError := func(err error, message string, statusCode int) {\n\t\tif isWebClient {\n\t\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, statusCode, err, message)\n\t\t} else {\n\t\t\tsendAPIResponse(w, r, err, message, statusCode)\n\t\t}\n\t}\n\n\tshareID := getURLParam(r, \"id\")\n\tshare, err := dataprovider.ShareExists(shareID, \"\")\n\tif err != nil {\n\t\tstatusCode := getRespStatus(err)\n\t\tif statusCode == http.StatusNotFound {\n\t\t\terr = util.NewI18nError(errors.New(\"share does not exist\"), util.I18nError404Message)\n\t\t}\n\t\trenderError(err, \"\", statusCode)\n\t\treturn share, nil, err\n\t}\n\tif !slices.Contains(validScopes, share.Scope) {\n\t\terr := errors.New(\"invalid share scope\")\n\t\trenderError(util.NewI18nError(err, util.I18nErrorShareScope), \"\", http.StatusForbidden)\n\t\treturn share, nil, err\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tok, err := share.IsUsable(ipAddr)\n\tif !ok || err != nil {\n\t\trenderError(err, \"\", getRespStatus(err))\n\t\treturn share, nil, err\n\t}\n\tif share.Password != \"\" {\n\t\tif isWebClient {\n\t\t\tif err := s.checkWebClientShareCredentials(w, r, &share); err != nil {\n\t\t\t\treturn share, nil, dataprovider.ErrInvalidCredentials\n\t\t\t}\n\t\t} else {\n\t\t\t_, password, ok := r.BasicAuth()\n\t\t\tif !ok {\n\t\t\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\t\t\trenderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\t\t\t\treturn share, nil, dataprovider.ErrInvalidCredentials\n\t\t\t}\n\t\t\tmatch, err := share.CheckCredentials(password)\n\t\t\tif !match || err != nil {\n\t\t\t\thandleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck\n\t\t\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\t\t\trenderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\t\t\t\treturn share, nil, dataprovider.ErrInvalidCredentials\n\t\t\t}\n\t\t}\n\t\tcommon.DelayLogin(nil)\n\t}\n\tuser, err := getUserForShare(share)\n\tif err != nil {\n\t\trenderError(err, \"\", getRespStatus(err))\n\t\treturn share, nil, err\n\t}\n\tconnID := xid.New().String()\n\tbaseConn := common.NewBaseConnection(connID, common.ProtocolHTTPShare, util.GetHTTPLocalAddress(r), r.RemoteAddr, user)\n\tconnection := newConnection(baseConn, w, r)\n\n\treturn share, connection, nil\n}\n\nfunc getUserForShare(share dataprovider.Share) (dataprovider.User, error) {\n\tuser, err := dataprovider.GetUserWithGroupSettings(share.Username, \"\")\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tif !user.CanManageShares() {\n\t\treturn user, util.NewI18nError(util.NewRecordNotFoundError(\"this share does not exist\"), util.I18nError404Message)\n\t}\n\tif share.Password == \"\" && slices.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {\n\t\treturn user, util.NewI18nError(\n\t\t\tfmt.Errorf(\"sharing without a password was disabled: %w\", os.ErrPermission),\n\t\t\tutil.I18nError403Message,\n\t\t)\n\t}\n\tif user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) {\n\t\treturn user, util.NewI18nError(\n\t\t\tutil.NewMethodDisabledError(\"two-factor authentication requirements not met\"),\n\t\t\tutil.I18nError403Message,\n\t\t)\n\t}\n\treturn user, nil\n}\n\nfunc validateBrowsableShare(share dataprovider.Share, connection *Connection) error {\n\tif len(share.Paths) != 1 {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"a share with multiple paths is not browsable\"),\n\t\t\tutil.I18nErrorShareBrowsePaths,\n\t\t)\n\t}\n\tbasePath := share.Paths[0]\n\tinfo, err := connection.Stat(basePath, 0)\n\tif err != nil {\n\t\tconnection.CloseFS() //nolint:errcheck\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"unable to check the share directory: %w\", err),\n\t\t\tutil.I18nErrorShareInvalidPath,\n\t\t)\n\t}\n\tif !info.IsDir() {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"the shared object is not a directory and so it is not browsable\"),\n\t\t\tutil.I18nErrorShareBrowseNoDir,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc getBrowsableSharedPath(shareBasePath string, r *http.Request) (string, error) {\n\tname := util.CleanPath(path.Join(shareBasePath, r.URL.Query().Get(\"path\")))\n\tif shareBasePath == \"/\" {\n\t\treturn name, nil\n\t}\n\tif name != shareBasePath && !strings.HasPrefix(name, shareBasePath+\"/\") {\n\t\treturn \"\", util.NewI18nError(\n\t\t\tutil.NewValidationError(fmt.Sprintf(\"Invalid path %q\", r.URL.Query().Get(\"path\"))),\n\t\t\tutil.I18nErrorPathInvalid,\n\t\t)\n\t}\n\treturn name, nil\n}\n"
  },
  {
    "path": "internal/httpd/api_user.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-chi/render\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nfunc getUsers(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlimit, offset, order, err := getSearchFilters(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tusers, err := dataprovider.GetUsers(limit, offset, order, claims.Role)\n\tif err == nil {\n\t\trender.JSON(w, r, users)\n\t} else {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusInternalServerError)\n\t}\n}\n\nfunc getUserByUsername(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tusername := getURLParam(r, \"username\")\n\trenderUser(w, r, username, claims, http.StatusOK)\n}\n\nfunc renderUser(w http.ResponseWriter, r *http.Request, username string, claims *jwt.Claims, status int) {\n\tuser, err := dataprovider.UserExists(username, claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif hideConfidentialData(claims, r) {\n\t\tuser.PrepareForRendering()\n\t}\n\tif status != http.StatusOK {\n\t\tctx := context.WithValue(r.Context(), render.StatusCtxKey, status)\n\t\trender.JSON(w, r.WithContext(ctx), user)\n\t} else {\n\t\trender.JSON(w, r, user)\n\t}\n}\n\nfunc addUser(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tadmin, err := dataprovider.AdminExists(claims.Username)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tvar user dataprovider.User\n\tif admin.Filters.Preferences.DefaultUsersExpiration > 0 {\n\t\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(admin.Filters.Preferences.DefaultUsersExpiration)))\n\t}\n\terr = render.DecodeJSON(r.Body, &user)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif claims.Role != \"\" {\n\t\tuser.Role = claims.Role\n\t}\n\tuser.LastPasswordChange = 0\n\tuser.Filters.RecoveryCodes = nil\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled: false,\n\t}\n\terr = dataprovider.AddUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tw.Header().Add(\"Location\", fmt.Sprintf(\"%s/%s\", userPath, url.PathEscape(user.Username)))\n\trenderUser(w, r, user.Username, claims, http.StatusCreated)\n}\n\nfunc disableUser2FA(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tusername := getURLParam(r, \"username\")\n\tuser, err := dataprovider.UserExists(username, claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tif !user.Filters.TOTPConfig.Enabled {\n\t\tsendAPIResponse(w, r, nil, \"two-factor authentication is not enabled\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tuser.Filters.RecoveryCodes = nil\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled: false,\n\t}\n\tif err := dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"2FA disabled\", http.StatusOK)\n}\n\nfunc updateUser(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tusername := getURLParam(r, \"username\")\n\tdisconnect := 0\n\tif _, ok := r.URL.Query()[\"disconnect\"]; ok {\n\t\tdisconnect, err = strconv.Atoi(r.URL.Query().Get(\"disconnect\"))\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"invalid disconnect parameter: %v\", err)\n\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t}\n\tuser, err := dataprovider.UserExists(username, claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tvar updatedUser dataprovider.User\n\tupdatedUser.Password = user.Password\n\terr = render.DecodeJSON(r.Body, &updatedUser)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tupdatedUser.ID = user.ID\n\tupdatedUser.Username = user.Username\n\tupdatedUser.Filters.RecoveryCodes = user.Filters.RecoveryCodes\n\tupdatedUser.Filters.TOTPConfig = user.Filters.TOTPConfig\n\tupdatedUser.LastPasswordChange = user.LastPasswordChange\n\tupdatedUser.SetEmptySecretsIfNil()\n\tupdateEncryptedSecrets(&updatedUser.FsConfig, &user.FsConfig)\n\tif claims.Role != \"\" {\n\t\tupdatedUser.Role = claims.Role\n\t}\n\terr = dataprovider.UpdateUser(&updatedUser, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"User updated\", http.StatusOK)\n\tif disconnect == 1 {\n\t\tdisconnectUser(user.Username, claims.Username, claims.Role)\n\t}\n}\n\nfunc deleteUser(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tusername := getURLParam(r, \"username\")\n\terr = dataprovider.DeleteUser(username, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"User deleted\", http.StatusOK)\n\tdisconnectUser(dataprovider.ConvertName(username), claims.Username, claims.Role)\n}\n\nfunc forgotUserPassword(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tif !smtp.IsEnabled() {\n\t\tsendAPIResponse(w, r, nil, \"No SMTP configuration\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr := handleForgotPassword(r, getURLParam(r, \"username\"), false)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\n\tsendAPIResponse(w, r, err, \"Check your email for the confirmation code\", http.StatusOK)\n}\n\nfunc resetUserPassword(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tvar req pwdReset\n\terr := render.DecodeJSON(r.Body, &req)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\t_, _, err = handleResetPassword(r, req.Code, req.Password, req.Password, false)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"Password reset successful\", http.StatusOK)\n}\n\nfunc disconnectUser(username, admin, role string) {\n\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\tif stat.Username == username {\n\t\t\tcommon.Connections.Close(stat.ConnectionID, \"\")\n\t\t}\n\t}\n\tfor _, stat := range getNodesConnections(admin, role) {\n\t\tif stat.Username == username {\n\t\t\tn, err := dataprovider.GetNodeByName(stat.Node)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"unable to disconnect user %q, error getting node %q: %v\", username, stat.Node, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tperms := []string{dataprovider.PermAdminCloseConnections}\n\t\t\turi := fmt.Sprintf(\"%s/%s\", activeConnectionsPath, stat.ConnectionID)\n\t\t\tif err := n.SendDeleteRequest(admin, role, uri, perms); err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"unable to disconnect user %q from node %q, error: %v\", username, n.Name, err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentFsConfig *vfs.Filesystem) {\n\t// we use the new access secret if plain or empty, otherwise the old value\n\tswitch fsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tif fsConfig.S3Config.AccessSecret.IsNotPlainAndNotEmpty() {\n\t\t\tfsConfig.S3Config.AccessSecret = currentFsConfig.S3Config.AccessSecret\n\t\t}\n\t\tif fsConfig.S3Config.SSECustomerKey.IsNotPlainAndNotEmpty() {\n\t\t\tfsConfig.S3Config.SSECustomerKey = currentFsConfig.S3Config.SSECustomerKey\n\t\t}\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tif fsConfig.AzBlobConfig.AccountKey.IsNotPlainAndNotEmpty() {\n\t\t\tfsConfig.AzBlobConfig.AccountKey = currentFsConfig.AzBlobConfig.AccountKey\n\t\t}\n\t\tif fsConfig.AzBlobConfig.SASURL.IsNotPlainAndNotEmpty() {\n\t\t\tfsConfig.AzBlobConfig.SASURL = currentFsConfig.AzBlobConfig.SASURL\n\t\t}\n\tcase sdk.GCSFilesystemProvider:\n\t\t// for GCS credentials will be cleared if we enable automatic credentials\n\t\t// so keep the old credentials here if no new credentials are provided\n\t\tif !fsConfig.GCSConfig.Credentials.IsPlain() {\n\t\t\tfsConfig.GCSConfig.Credentials = currentFsConfig.GCSConfig.Credentials\n\t\t}\n\tcase sdk.CryptedFilesystemProvider:\n\t\tif fsConfig.CryptConfig.Passphrase.IsNotPlainAndNotEmpty() {\n\t\t\tfsConfig.CryptConfig.Passphrase = currentFsConfig.CryptConfig.Passphrase\n\t\t}\n\tcase sdk.SFTPFilesystemProvider:\n\t\tupdateSFTPFsEncryptedSecrets(fsConfig, currentFsConfig)\n\tcase sdk.HTTPFilesystemProvider:\n\t\tupdateHTTPFsEncryptedSecrets(fsConfig, currentFsConfig)\n\t}\n}\n\nfunc updateSFTPFsEncryptedSecrets(fsConfig *vfs.Filesystem, currentFsConfig *vfs.Filesystem) {\n\tif fsConfig.SFTPConfig.Password.IsNotPlainAndNotEmpty() {\n\t\tfsConfig.SFTPConfig.Password = currentFsConfig.SFTPConfig.Password\n\t}\n\tif fsConfig.SFTPConfig.PrivateKey.IsNotPlainAndNotEmpty() {\n\t\tfsConfig.SFTPConfig.PrivateKey = currentFsConfig.SFTPConfig.PrivateKey\n\t}\n\tif fsConfig.SFTPConfig.KeyPassphrase.IsNotPlainAndNotEmpty() {\n\t\tfsConfig.SFTPConfig.KeyPassphrase = currentFsConfig.SFTPConfig.KeyPassphrase\n\t}\n}\n\nfunc updateHTTPFsEncryptedSecrets(fsConfig *vfs.Filesystem, currentFsConfig *vfs.Filesystem) {\n\tif fsConfig.HTTPConfig.Password.IsNotPlainAndNotEmpty() {\n\t\tfsConfig.HTTPConfig.Password = currentFsConfig.HTTPConfig.Password\n\t}\n\tif fsConfig.HTTPConfig.APIKey.IsNotPlainAndNotEmpty() {\n\t\tfsConfig.HTTPConfig.APIKey = currentFsConfig.HTTPConfig.APIKey\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/api_utils.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"mime\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/go-chi/render\"\n\t\"github.com/klauspost/compress/zip\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\ntype pwdChange struct {\n\tCurrentPassword string `json:\"current_password\"`\n\tNewPassword     string `json:\"new_password\"`\n}\n\ntype pwdReset struct {\n\tCode     string `json:\"code\"`\n\tPassword string `json:\"password\"`\n}\n\ntype baseProfile struct {\n\tEmail           string `json:\"email,omitempty\"`\n\tDescription     string `json:\"description,omitempty\"`\n\tAllowAPIKeyAuth bool   `json:\"allow_api_key_auth\"`\n}\n\ntype adminProfile struct {\n\tbaseProfile\n}\n\ntype userProfile struct {\n\tbaseProfile\n\tAdditionalEmails []string `json:\"additional_emails,omitempty\"`\n\tPublicKeys       []string `json:\"public_keys,omitempty\"`\n\tTLSCerts         []string `json:\"tls_certs,omitempty\"`\n}\n\nfunc sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {\n\tvar errorString string\n\tif errors.Is(err, util.ErrNotFound) {\n\t\terrorString = http.StatusText(http.StatusNotFound)\n\t} else if err != nil {\n\t\terrorString = err.Error()\n\t}\n\tresp := apiResponse{\n\t\tError:   errorString,\n\t\tMessage: message,\n\t}\n\tctx := context.WithValue(r.Context(), render.StatusCtxKey, code)\n\trender.JSON(w, r.WithContext(ctx), resp)\n}\n\nfunc getRespStatus(err error) int {\n\tif errors.Is(err, util.ErrValidation) {\n\t\treturn http.StatusBadRequest\n\t}\n\tif errors.Is(err, util.ErrMethodDisabled) {\n\t\treturn http.StatusForbidden\n\t}\n\tif errors.Is(err, util.ErrNotFound) {\n\t\treturn http.StatusNotFound\n\t}\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn http.StatusBadRequest\n\t}\n\tif errors.Is(err, fs.ErrPermission) || errors.Is(err, dataprovider.ErrLoginNotAllowedFromIP) {\n\t\treturn http.StatusForbidden\n\t}\n\tif errors.Is(err, plugin.ErrNoSearcher) || errors.Is(err, dataprovider.ErrNotImplemented) {\n\t\treturn http.StatusNotImplemented\n\t}\n\tif errors.Is(err, dataprovider.ErrDuplicatedKey) || errors.Is(err, dataprovider.ErrForeignKeyViolated) {\n\t\treturn http.StatusConflict\n\t}\n\treturn http.StatusInternalServerError\n}\n\n// mappig between fs errors for HTTP protocol and HTTP response status codes\nfunc getMappedStatusCode(err error) int {\n\tvar statusCode int\n\tswitch {\n\tcase errors.Is(err, fs.ErrPermission):\n\t\tstatusCode = http.StatusForbidden\n\tcase errors.Is(err, common.ErrReadQuotaExceeded):\n\t\tstatusCode = http.StatusForbidden\n\tcase errors.Is(err, fs.ErrNotExist):\n\t\tstatusCode = http.StatusNotFound\n\tcase errors.Is(err, common.ErrQuotaExceeded):\n\t\tstatusCode = http.StatusRequestEntityTooLarge\n\tcase errors.Is(err, common.ErrOpUnsupported):\n\t\tstatusCode = http.StatusBadRequest\n\tdefault:\n\t\tif _, ok := err.(*http.MaxBytesError); ok {\n\t\t\tstatusCode = http.StatusRequestEntityTooLarge\n\t\t} else {\n\t\t\tstatusCode = http.StatusInternalServerError\n\t\t}\n\t}\n\treturn statusCode\n}\n\nfunc getURLParam(r *http.Request, key string) string {\n\tv := chi.URLParam(r, key)\n\tunescaped, err := url.PathUnescape(v)\n\tif err != nil {\n\t\treturn v\n\t}\n\treturn unescaped\n}\n\nfunc getURLPath(r *http.Request) string {\n\trctx := chi.RouteContext(r.Context())\n\tif rctx != nil && rctx.RoutePath != \"\" {\n\t\treturn rctx.RoutePath\n\t}\n\treturn r.URL.Path\n}\n\nfunc getCommaSeparatedQueryParam(r *http.Request, key string) []string {\n\tvar result []string\n\n\tfor val := range strings.SplitSeq(r.URL.Query().Get(key), \",\") {\n\t\tval = strings.TrimSpace(val)\n\t\tif val != \"\" {\n\t\t\tresult = append(result, val)\n\t\t}\n\t}\n\n\treturn util.RemoveDuplicates(result, false)\n}\n\nfunc getBoolQueryParam(r *http.Request, param string) bool {\n\treturn r.URL.Query().Get(param) == \"true\"\n}\n\nfunc getActiveConnections(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tstats := common.Connections.GetStats(claims.Role)\n\tif claims.NodeID == \"\" {\n\t\tstats = append(stats, getNodesConnections(claims.Username, claims.Role)...)\n\t}\n\trender.JSON(w, r, stats)\n}\n\nfunc handleCloseConnection(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tconnectionID := getURLParam(r, \"connectionID\")\n\tif connectionID == \"\" {\n\t\tsendAPIResponse(w, r, nil, \"connectionID is mandatory\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tnode := r.URL.Query().Get(\"node\")\n\tif node == \"\" || node == dataprovider.GetNodeName() {\n\t\tif common.Connections.Close(connectionID, claims.Role) {\n\t\t\tsendAPIResponse(w, r, nil, \"Connection closed\", http.StatusOK)\n\t\t} else {\n\t\t\tsendAPIResponse(w, r, nil, \"Not Found\", http.StatusNotFound)\n\t\t}\n\t\treturn\n\t}\n\tn, err := dataprovider.GetNodeByName(node)\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to get node with name %q: %v\", node, err)\n\t\tstatus := getRespStatus(err)\n\t\tsendAPIResponse(w, r, nil, http.StatusText(status), status)\n\t\treturn\n\t}\n\tperms := []string{dataprovider.PermAdminCloseConnections}\n\turi := fmt.Sprintf(\"%s/%s\", activeConnectionsPath, connectionID)\n\tif err := n.SendDeleteRequest(claims.Username, claims.Role, uri, perms); err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to delete connection id %q from node %q: %v\", connectionID, n.Name, err)\n\t\tsendAPIResponse(w, r, nil, \"Not Found\", http.StatusNotFound)\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, \"Connection closed\", http.StatusOK)\n}\n\n// getNodesConnections returns the active connections from other nodes.\n// Errors are silently ignored\nfunc getNodesConnections(admin, role string) []common.ConnectionStatus {\n\tnodes, err := dataprovider.GetNodes()\n\tif err != nil || len(nodes) == 0 {\n\t\treturn nil\n\t}\n\tvar results []common.ConnectionStatus\n\tvar mu sync.Mutex\n\tvar wg sync.WaitGroup\n\n\tfor _, n := range nodes {\n\t\twg.Add(1)\n\n\t\tgo func(node dataprovider.Node) {\n\t\t\tdefer wg.Done()\n\n\t\t\tvar stats []common.ConnectionStatus\n\t\t\tperms := []string{dataprovider.PermAdminViewConnections}\n\t\t\tif err := node.SendGetRequest(admin, role, activeConnectionsPath, perms, &stats); err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"unable to get connections from node %s: %v\", node.Name, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmu.Lock()\n\t\t\tresults = append(results, stats...)\n\t\t\tmu.Unlock()\n\t\t}(n)\n\t}\n\twg.Wait()\n\n\treturn results\n}\n\nfunc getSearchFilters(w http.ResponseWriter, r *http.Request) (int, int, string, error) {\n\tvar err error\n\tlimit := 100\n\toffset := 0\n\torder := dataprovider.OrderASC\n\tif _, ok := r.URL.Query()[\"limit\"]; ok {\n\t\tlimit, err = strconv.Atoi(r.URL.Query().Get(\"limit\"))\n\t\tif err != nil {\n\t\t\terr = errors.New(\"invalid limit\")\n\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\t\treturn limit, offset, order, err\n\t\t}\n\t\tif limit > 500 {\n\t\t\tlimit = 500\n\t\t}\n\t}\n\tif _, ok := r.URL.Query()[\"offset\"]; ok {\n\t\toffset, err = strconv.Atoi(r.URL.Query().Get(\"offset\"))\n\t\tif err != nil {\n\t\t\terr = errors.New(\"invalid offset\")\n\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\t\treturn limit, offset, order, err\n\t\t}\n\t}\n\tif _, ok := r.URL.Query()[\"order\"]; ok {\n\t\torder = r.URL.Query().Get(\"order\")\n\t\tif order != dataprovider.OrderASC && order != dataprovider.OrderDESC {\n\t\t\terr = errors.New(\"invalid order\")\n\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\t\treturn limit, offset, order, err\n\t\t}\n\t}\n\n\treturn limit, offset, order, err\n}\n\nfunc renderAPIDirContents(w http.ResponseWriter, lister vfs.DirLister, omitNonRegularFiles bool) {\n\tdefer lister.Close()\n\n\tdataGetter := func(limit, _ int) ([]byte, int, error) {\n\t\tcontents, err := lister.Next(limit)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\terr = nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tresults := make([]map[string]any, 0, len(contents))\n\t\tfor _, info := range contents {\n\t\t\tif omitNonRegularFiles && !info.Mode().IsDir() && !info.Mode().IsRegular() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tres := make(map[string]any)\n\t\t\tres[\"name\"] = info.Name()\n\t\t\tif info.Mode().IsRegular() {\n\t\t\t\tres[\"size\"] = info.Size()\n\t\t\t}\n\t\t\tres[\"mode\"] = info.Mode()\n\t\t\tres[\"last_modified\"] = info.ModTime().UTC().Format(time.RFC3339)\n\t\t\tresults = append(results, res)\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\tcount := limit\n\t\tif len(results) == 0 {\n\t\t\tcount = 0\n\t\t}\n\t\treturn data, count, err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc streamData(w io.Writer, data []byte) {\n\tb := bytes.NewBuffer(data)\n\t_, err := io.CopyN(w, b, int64(len(data)))\n\tif err != nil {\n\t\tpanic(http.ErrAbortHandler)\n\t}\n}\n\nfunc streamJSONArray(w http.ResponseWriter, chunkSize int, dataGetter func(limit, offset int) ([]byte, int, error)) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Header().Set(\"Accept-Ranges\", \"none\")\n\tw.WriteHeader(http.StatusOK)\n\n\tstreamData(w, []byte(\"[\"))\n\toffset := 0\n\tfor {\n\t\tdata, count, err := dataGetter(chunkSize, offset)\n\t\tif err != nil {\n\t\t\tpanic(http.ErrAbortHandler)\n\t\t}\n\t\tif count == 0 {\n\t\t\tbreak\n\t\t}\n\t\tif offset > 0 {\n\t\t\tstreamData(w, []byte(\",\"))\n\t\t}\n\t\tstreamData(w, data[1:len(data)-1])\n\t\tif count < chunkSize {\n\t\t\tbreak\n\t\t}\n\t\toffset += count\n\t}\n\tstreamData(w, []byte(\"]\"))\n}\n\nfunc renderPNGImage(w http.ResponseWriter, r *http.Request, b []byte) {\n\tif len(b) == 0 {\n\t\tctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusNotFound)\n\t\trender.PlainText(w, r.WithContext(ctx), http.StatusText(http.StatusNotFound))\n\t\treturn\n\t}\n\tw.Header().Set(\"Content-Type\", \"image/png\")\n\tstreamData(w, b)\n}\n\nfunc getCompressedFileName(username string, files []string) string {\n\tif len(files) == 1 {\n\t\tname := path.Base(files[0])\n\t\treturn fmt.Sprintf(\"%s-%s.zip\", username, strings.TrimSuffix(name, path.Ext(name)))\n\t}\n\treturn fmt.Sprintf(\"%s-download.zip\", username)\n}\n\nfunc renderCompressedFiles(w http.ResponseWriter, conn *Connection, baseDir string, files []string,\n\tshare *dataprovider.Share,\n) {\n\tconn.User.CheckFsRoot(conn.ID) //nolint:errcheck\n\tw.Header().Set(\"Content-Type\", \"application/zip\")\n\tw.Header().Set(\"Accept-Ranges\", \"none\")\n\tw.Header().Set(\"Content-Transfer-Encoding\", \"binary\")\n\tw.WriteHeader(http.StatusOK)\n\n\twr := zip.NewWriter(w)\n\n\tfor _, file := range files {\n\t\tfullPath := util.CleanPath(path.Join(baseDir, file))\n\t\tif err := addZipEntry(wr, conn, fullPath, baseDir, nil, 0); err != nil {\n\t\t\tif share != nil {\n\t\t\t\tdataprovider.UpdateShareLastUse(share, -1) //nolint:errcheck\n\t\t\t}\n\t\t\tpanic(http.ErrAbortHandler)\n\t\t}\n\t}\n\tif err := wr.Close(); err != nil {\n\t\tconn.Log(logger.LevelError, \"unable to close zip file: %v\", err)\n\t\tif share != nil {\n\t\t\tdataprovider.UpdateShareLastUse(share, -1) //nolint:errcheck\n\t\t}\n\t\tpanic(http.ErrAbortHandler)\n\t}\n}\n\nfunc addZipEntry(wr *zip.Writer, conn *Connection, entryPath, baseDir string, info os.FileInfo, recursion int) error {\n\tif recursion >= util.MaxRecursion {\n\t\tconn.Log(logger.LevelDebug, \"unable to add zip entry %q, recursion too depth: %d\", entryPath, recursion)\n\t\treturn util.ErrRecursionTooDeep\n\t}\n\trecursion++\n\tvar err error\n\tif info == nil {\n\t\tinfo, err = conn.Stat(entryPath, 1)\n\t\tif err != nil {\n\t\t\tconn.Log(logger.LevelDebug, \"unable to add zip entry %q, stat error: %v\", entryPath, err)\n\t\t\treturn err\n\t\t}\n\t}\n\tentryName, err := getZipEntryName(entryPath, baseDir)\n\tif err != nil {\n\t\tconn.Log(logger.LevelError, \"unable to get zip entry name: %v\", err)\n\t\treturn err\n\t}\n\tif info.IsDir() {\n\t\t_, err = wr.CreateHeader(&zip.FileHeader{\n\t\t\tName:     entryName + \"/\",\n\t\t\tMethod:   zip.Deflate,\n\t\t\tModified: info.ModTime(),\n\t\t})\n\t\tif err != nil {\n\t\t\tconn.Log(logger.LevelError, \"unable to create zip entry %q: %v\", entryPath, err)\n\t\t\treturn err\n\t\t}\n\t\tlister, err := conn.ReadDir(entryPath)\n\t\tif err != nil {\n\t\t\tconn.Log(logger.LevelDebug, \"unable to add zip entry %q, get list dir error: %v\", entryPath, err)\n\t\t\treturn err\n\t\t}\n\t\tdefer lister.Close()\n\n\t\tfor {\n\t\t\tcontents, err := lister.Next(vfs.ListerBatchSize)\n\t\t\tfinished := errors.Is(err, io.EOF)\n\t\t\tif err != nil && !finished {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor _, info := range contents {\n\t\t\t\tfullPath := util.CleanPath(path.Join(entryPath, info.Name()))\n\t\t\t\tif err := addZipEntry(wr, conn, fullPath, baseDir, info, recursion); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif finished {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\tif !info.Mode().IsRegular() {\n\t\t// we only allow regular files\n\t\tconn.Log(logger.LevelInfo, \"skipping zip entry for non regular file %q\", entryPath)\n\t\treturn nil\n\t}\n\treturn addFileToZipEntry(wr, conn, entryPath, entryName, info)\n}\n\nfunc addFileToZipEntry(wr *zip.Writer, conn *Connection, entryPath, entryName string, info os.FileInfo) error {\n\treader, err := conn.getFileReader(entryPath, 0, http.MethodGet)\n\tif err != nil {\n\t\tconn.Log(logger.LevelDebug, \"unable to add zip entry %q, cannot open file: %v\", entryPath, err)\n\t\treturn err\n\t}\n\tdefer reader.Close()\n\n\tf, err := wr.CreateHeader(&zip.FileHeader{\n\t\tName:     entryName,\n\t\tMethod:   zip.Deflate,\n\t\tModified: info.ModTime(),\n\t})\n\tif err != nil {\n\t\tconn.Log(logger.LevelError, \"unable to create zip entry %q: %v\", entryPath, err)\n\t\treturn err\n\t}\n\t_, err = io.Copy(f, reader)\n\treturn err\n}\n\nfunc getZipEntryName(entryPath, baseDir string) (string, error) {\n\tif !strings.HasPrefix(entryPath, baseDir) {\n\t\treturn \"\", fmt.Errorf(\"entry path %q is outside base dir %q\", entryPath, baseDir)\n\t}\n\tentryPath = strings.TrimPrefix(entryPath, baseDir)\n\treturn strings.TrimPrefix(entryPath, \"/\"), nil\n}\n\nfunc checkDownloadFileFromShare(share *dataprovider.Share, info os.FileInfo) error {\n\tif share != nil && !info.Mode().IsRegular() {\n\t\treturn util.NewValidationError(\"non regular files are not supported for shares\")\n\t}\n\treturn nil\n}\n\nfunc downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection, name string,\n\tinfo os.FileInfo, inline bool, share *dataprovider.Share,\n) (int, error) {\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\terr := checkDownloadFileFromShare(share, info)\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\trangeHeader := r.Header.Get(\"Range\")\n\tif rangeHeader != \"\" && checkIfRange(r, info.ModTime()) == condFalse {\n\t\trangeHeader = \"\"\n\t}\n\toffset := int64(0)\n\tsize := info.Size()\n\tresponseStatus := http.StatusOK\n\tif strings.HasPrefix(rangeHeader, \"bytes=\") {\n\t\tif strings.Contains(rangeHeader, \",\") {\n\t\t\treturn http.StatusRequestedRangeNotSatisfiable, fmt.Errorf(\"unsupported range %q\", rangeHeader)\n\t\t}\n\t\toffset, size, err = parseRangeRequest(rangeHeader[6:], size)\n\t\tif err != nil {\n\t\t\treturn http.StatusRequestedRangeNotSatisfiable, err\n\t\t}\n\t\tresponseStatus = http.StatusPartialContent\n\t}\n\treader, err := connection.getFileReader(name, offset, r.Method)\n\tif err != nil {\n\t\treturn getMappedStatusCode(err), fmt.Errorf(\"unable to read file %q: %v\", name, err)\n\t}\n\tdefer reader.Close()\n\n\tw.Header().Set(\"Last-Modified\", info.ModTime().UTC().Format(http.TimeFormat))\n\tif checkPreconditions(w, r, info.ModTime()) {\n\t\treturn 0, fmt.Errorf(\"%v\", http.StatusText(http.StatusPreconditionFailed))\n\t}\n\tctype := mime.TypeByExtension(path.Ext(name))\n\tif ctype == \"\" {\n\t\tctype = \"application/octet-stream\"\n\t}\n\tif responseStatus == http.StatusPartialContent {\n\t\tw.Header().Set(\"Content-Range\", fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, info.Size()))\n\t}\n\tw.Header().Set(\"Content-Length\", strconv.FormatInt(size, 10))\n\tw.Header().Set(\"Content-Type\", ctype)\n\tif !inline {\n\t\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=%q\", path.Base(name)))\n\t}\n\tw.Header().Set(\"Accept-Ranges\", \"bytes\")\n\tw.WriteHeader(responseStatus)\n\tif r.Method != http.MethodHead {\n\t\t_, err = io.CopyN(w, reader, size)\n\t\tif err != nil {\n\t\t\tif share != nil {\n\t\t\t\tdataprovider.UpdateShareLastUse(share, -1) //nolint:errcheck\n\t\t\t}\n\t\t\tconnection.Log(logger.LevelDebug, \"error reading file to download: %v\", err)\n\t\t\tpanic(http.ErrAbortHandler)\n\t\t}\n\t}\n\treturn http.StatusOK, nil\n}\n\nfunc checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) bool {\n\tif checkIfUnmodifiedSince(r, modtime) == condFalse {\n\t\tw.WriteHeader(http.StatusPreconditionFailed)\n\t\treturn true\n\t}\n\tif checkIfModifiedSince(r, modtime) == condFalse {\n\t\tw.WriteHeader(http.StatusNotModified)\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult {\n\tius := r.Header.Get(\"If-Unmodified-Since\")\n\tif ius == \"\" || isZeroTime(modtime) {\n\t\treturn condNone\n\t}\n\tt, err := http.ParseTime(ius)\n\tif err != nil {\n\t\treturn condNone\n\t}\n\n\t// The Last-Modified header truncates sub-second precision so\n\t// the modtime needs to be truncated too.\n\tmodtime = modtime.Truncate(time.Second)\n\tif modtime.Before(t) || modtime.Equal(t) {\n\t\treturn condTrue\n\t}\n\treturn condFalse\n}\n\nfunc checkIfModifiedSince(r *http.Request, modtime time.Time) condResult {\n\tif r.Method != http.MethodGet && r.Method != http.MethodHead {\n\t\treturn condNone\n\t}\n\tims := r.Header.Get(\"If-Modified-Since\")\n\tif ims == \"\" || isZeroTime(modtime) {\n\t\treturn condNone\n\t}\n\tt, err := http.ParseTime(ims)\n\tif err != nil {\n\t\treturn condNone\n\t}\n\t// The Last-Modified header truncates sub-second precision so\n\t// the modtime needs to be truncated too.\n\tmodtime = modtime.Truncate(time.Second)\n\tif modtime.Before(t) || modtime.Equal(t) {\n\t\treturn condFalse\n\t}\n\treturn condTrue\n}\n\nfunc checkIfRange(r *http.Request, modtime time.Time) condResult {\n\tif r.Method != http.MethodGet && r.Method != http.MethodHead {\n\t\treturn condNone\n\t}\n\tir := r.Header.Get(\"If-Range\")\n\tif ir == \"\" {\n\t\treturn condNone\n\t}\n\tif modtime.IsZero() {\n\t\treturn condFalse\n\t}\n\tt, err := http.ParseTime(ir)\n\tif err != nil {\n\t\treturn condFalse\n\t}\n\tif modtime.Unix() == t.Unix() {\n\t\treturn condTrue\n\t}\n\treturn condFalse\n}\n\nfunc parseRangeRequest(bytesRange string, size int64) (int64, int64, error) {\n\tvar start, end int64\n\tvar err error\n\n\tvalues := strings.Split(bytesRange, \"-\")\n\tif values[0] == \"\" {\n\t\tstart = -1\n\t} else {\n\t\tstart, err = strconv.ParseInt(values[0], 10, 64)\n\t\tif err != nil {\n\t\t\treturn start, size, err\n\t\t}\n\t}\n\tif len(values) >= 2 {\n\t\tif values[1] != \"\" {\n\t\t\tend, err = strconv.ParseInt(values[1], 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn start, size, err\n\t\t\t}\n\t\t\tif end >= size {\n\t\t\t\tend = size - 1\n\t\t\t}\n\t\t}\n\t}\n\tif start == -1 && end == 0 {\n\t\treturn 0, 0, fmt.Errorf(\"unsupported range %q\", bytesRange)\n\t}\n\n\tif end > 0 {\n\t\tif start == -1 {\n\t\t\t// we have something like -500\n\t\t\tstart = size - end\n\t\t\tsize = end\n\t\t\t// start cannot be < 0 here, we did end = size -1 above\n\t\t} else {\n\t\t\t// we have something like 500-600\n\t\t\tsize = end - start + 1\n\t\t\tif size < 0 {\n\t\t\t\treturn 0, 0, fmt.Errorf(\"unacceptable range %q\", bytesRange)\n\t\t\t}\n\t\t}\n\t\treturn start, size, nil\n\t}\n\t// we have something like 500-\n\tsize -= start\n\tif size < 0 {\n\t\treturn 0, 0, fmt.Errorf(\"unacceptable range %q\", bytesRange)\n\t}\n\treturn start, size, err\n}\n\nfunc handleDefenderEventLoginFailed(ipAddr string, err error) error {\n\tevent := common.HostEventLoginFailed\n\tif errors.Is(err, util.ErrNotFound) {\n\t\tevent = common.HostEventUserNotFound\n\t\terr = dataprovider.ErrInvalidCredentials\n\t}\n\tcommon.AddDefenderEvent(ipAddr, common.ProtocolHTTP, event)\n\tcommon.DelayLogin(err)\n\treturn err\n}\n\nfunc updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err error, r *http.Request) {\n\tmetric.AddLoginAttempt(loginMethod)\n\tvar protocol string\n\tswitch loginMethod {\n\tcase dataprovider.LoginMethodIDP:\n\t\tprotocol = common.ProtocolOIDC\n\tdefault:\n\t\tprotocol = common.ProtocolHTTP\n\t}\n\tif err == nil {\n\t\tlogger.LoginLog(user.Username, ip, loginMethod, protocol, \"\", r.UserAgent(), r.TLS != nil, \"\")\n\t\tplugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, protocol, user.Username, ip, \"\", nil)\n\t\tcommon.DelayLogin(nil)\n\t} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials {\n\t\tlogger.ConnectionFailedLog(user.Username, ip, loginMethod, protocol, err.Error())\n\t\terr = handleDefenderEventLoginFailed(ip, err)\n\t\tlogEv := notifier.LogEventTypeLoginFailed\n\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\tlogEv = notifier.LogEventTypeLoginNoUser\n\t\t}\n\t\tplugin.Handler.NotifyLogEvent(logEv, protocol, user.Username, ip, \"\", err)\n\t}\n\tmetric.AddLoginResult(loginMethod, err)\n\tdataprovider.ExecutePostLoginHook(user, loginMethod, ip, protocol, err)\n}\n\nfunc checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string, checkSessions, isOIDCLogin bool) error {\n\tif slices.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, protocol HTTP is not allowed\", user.Username)\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"protocol HTTP is not allowed for user %q\", user.Username),\n\t\t\tutil.I18nErrorProtocolForbidden,\n\t\t)\n\t}\n\tif !isLoggedInWithOIDC(r) && !isOIDCLogin && !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, password login method is not allowed\", user.Username)\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"login method password is not allowed for user %q\", user.Username),\n\t\t\tutil.I18nErrorPwdLoginForbidden,\n\t\t)\n\t}\n\tif checkSessions && user.MaxSessions > 0 {\n\t\tactiveSessions := common.Connections.GetActiveSessions(user.Username)\n\t\tif activeSessions >= user.MaxSessions {\n\t\t\tlogger.Info(logSender, connectionID, \"authentication refused for user: %q, too many open sessions: %v/%v\", user.Username,\n\t\t\t\tactiveSessions, user.MaxSessions)\n\t\t\treturn util.NewI18nError(fmt.Errorf(\"too many open sessions: %v\", activeSessions), util.I18nError429Message)\n\t\t}\n\t}\n\tif !user.IsLoginFromAddrAllowed(r.RemoteAddr) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, remote address is not allowed: %v\", user.Username, r.RemoteAddr)\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"login for user %q is not allowed from this address: %v\", user.Username, r.RemoteAddr),\n\t\t\tutil.I18nErrorIPForbidden,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc getActiveAdmin(username, ipAddr string) (dataprovider.Admin, error) {\n\tadmin, err := dataprovider.AdminExists(username)\n\tif err != nil {\n\t\treturn admin, err\n\t}\n\tif err := admin.CanLogin(ipAddr); err != nil {\n\t\treturn admin, util.NewRecordNotFoundError(fmt.Sprintf(\"admin %q cannot login: %v\", username, err))\n\t}\n\treturn admin, nil\n}\n\nfunc getActiveUser(username string, r *http.Request) (dataprovider.User, error) {\n\tuser, err := dataprovider.GetUserWithGroupSettings(username, \"\")\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tif err := user.CheckLoginConditions(); err != nil {\n\t\treturn user, util.NewRecordNotFoundError(fmt.Sprintf(\"user %q cannot login: %v\", username, err))\n\t}\n\tif err := checkHTTPClientUser(&user, r, xid.New().String(), false, false); err != nil {\n\t\treturn user, util.NewRecordNotFoundError(fmt.Sprintf(\"user %q cannot login: %v\", username, err))\n\t}\n\treturn user, nil\n}\n\nfunc handleForgotPassword(r *http.Request, username string, isAdmin bool) error {\n\tvar emails []string\n\tvar subject string\n\tvar err error\n\tvar admin dataprovider.Admin\n\tvar user dataprovider.User\n\n\tif username == \"\" {\n\t\treturn util.NewI18nError(util.NewValidationError(\"username is mandatory\"), util.I18nErrorUsernameRequired)\n\t}\n\tif isAdmin {\n\t\tadmin, err = getActiveAdmin(username, util.GetIPFromRemoteAddress(r.RemoteAddr))\n\t\tif admin.Email != \"\" {\n\t\t\temails = []string{admin.Email}\n\t\t}\n\t\tsubject = fmt.Sprintf(\"Email Verification Code for admin %q\", username)\n\t} else {\n\t\tuser, err = getActiveUser(username, r)\n\t\temails = user.GetEmailAddresses()\n\t\tsubject = fmt.Sprintf(\"Email Verification Code for user %q\", username)\n\t\tif err == nil {\n\t\t\tif !isUserAllowedToResetPassword(r, &user) {\n\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\tutil.NewValidationError(\"you are not allowed to reset your password\"),\n\t\t\t\t\tutil.I18nErrorPwdResetForbidded,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\tif err != nil {\n\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\thandleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), err) //nolint:errcheck\n\t\t\tlogger.Debug(logSender, middleware.GetReqID(r.Context()),\n\t\t\t\t\"username %q does not exists or cannot login, reset password request silently ignored, is admin? %t, err: %v\",\n\t\t\t\tusername, isAdmin, err)\n\t\t\treturn nil\n\t\t}\n\t\treturn util.NewI18nError(util.NewGenericError(\"Error retrieving your account, please try again later\"), util.I18nErrorGetUser)\n\t}\n\tif len(emails) == 0 {\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"Your account does not have an email address, it is not possible to reset your password by sending an email verification code\"),\n\t\t\tutil.I18nErrorPwdResetNoEmail,\n\t\t)\n\t}\n\tc := newResetCode(username, isAdmin)\n\tbody := new(bytes.Buffer)\n\tdata := make(map[string]string)\n\tdata[\"Code\"] = c.Code\n\tif err := smtp.RenderPasswordResetTemplate(body, data); err != nil {\n\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"unable to render password reset template: %v\", err)\n\t\treturn util.NewGenericError(\"Unable to render password reset template\")\n\t}\n\tstartTime := time.Now()\n\tif err := smtp.SendEmail(emails, nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {\n\t\tlogger.Warn(logSender, middleware.GetReqID(r.Context()), \"unable to send password reset code via email: %v, elapsed: %v\",\n\t\t\terr, time.Since(startTime))\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewGenericError(fmt.Sprintf(\"Error sending confirmation code via email: %v\", err)),\n\t\t\tutil.I18nErrorPwdResetSendEmail,\n\t\t)\n\t}\n\tlogger.Debug(logSender, middleware.GetReqID(r.Context()), \"reset code sent via email to %q, emails: %+v, is admin? %v, elapsed: %v\",\n\t\tusername, emails, isAdmin, time.Since(startTime))\n\treturn resetCodesMgr.Add(c)\n}\n\nfunc handleResetPassword(r *http.Request, code, newPassword, confirmPassword string, isAdmin bool) (\n\t*dataprovider.Admin, *dataprovider.User, error,\n) {\n\tvar admin dataprovider.Admin\n\tvar user dataprovider.User\n\tvar err error\n\n\tif newPassword == \"\" {\n\t\treturn &admin, &user, util.NewValidationError(\"please set a password\")\n\t}\n\tif code == \"\" {\n\t\treturn &admin, &user, util.NewValidationError(\"please set a confirmation code\")\n\t}\n\tif newPassword != confirmPassword {\n\t\treturn &admin, &user, util.NewI18nError(errors.New(\"the two password fields do not match\"), util.I18nErrorChangePwdNoMatch)\n\t}\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tresetCode, err := resetCodesMgr.Get(code)\n\tif err != nil {\n\t\thandleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck\n\t\treturn &admin, &user, util.NewValidationError(\"confirmation code not found\")\n\t}\n\tif resetCode.IsAdmin != isAdmin {\n\t\treturn &admin, &user, util.NewValidationError(\"invalid confirmation code\")\n\t}\n\tif isAdmin {\n\t\tadmin, err = getActiveAdmin(resetCode.Username, ipAddr)\n\t\tif err != nil {\n\t\t\treturn &admin, &user, util.NewValidationError(\"unable to associate the confirmation code with an existing admin\")\n\t\t}\n\t\tadmin.Password = newPassword\n\t\tadmin.Filters.RequirePasswordChange = false\n\t\terr = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr, admin.Role)\n\t\tif err != nil {\n\t\t\treturn &admin, &user, util.NewGenericError(fmt.Sprintf(\"unable to set the new password: %v\", err))\n\t\t}\n\t\terr = resetCodesMgr.Delete(code)\n\t\treturn &admin, &user, err\n\t}\n\tuser, err = getActiveUser(resetCode.Username, r)\n\tif err != nil {\n\t\treturn &admin, &user, util.NewValidationError(\"Unable to associate the confirmation code with an existing user\")\n\t}\n\tif !isUserAllowedToResetPassword(r, &user) {\n\t\treturn &admin, &user, util.NewI18nError(\n\t\t\tutil.NewValidationError(\"you are not allowed to reset your password\"),\n\t\t\tutil.I18nErrorPwdResetForbidded,\n\t\t)\n\t}\n\terr = dataprovider.UpdateUserPassword(user.Username, newPassword, dataprovider.ActionExecutorSelf,\n\t\tutil.GetIPFromRemoteAddress(r.RemoteAddr), user.Role)\n\tif err == nil {\n\t\terr = resetCodesMgr.Delete(code)\n\t}\n\tuser.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now())\n\tuser.Filters.RequirePasswordChange = false\n\treturn &admin, &user, err\n}\n\nfunc isUserAllowedToResetPassword(r *http.Request, user *dataprovider.User) bool {\n\tif !user.CanResetPassword() {\n\t\treturn false\n\t}\n\tif slices.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {\n\t\treturn false\n\t}\n\tif !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) {\n\t\treturn false\n\t}\n\tif !user.IsLoginFromAddrAllowed(r.RemoteAddr) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc getProtocolFromRequest(r *http.Request) string {\n\tif isLoggedInWithOIDC(r) {\n\t\treturn common.ProtocolOIDC\n\t}\n\treturn common.ProtocolHTTP\n}\n\nfunc hideConfidentialData(claims *jwt.Claims, r *http.Request) bool {\n\tif !claims.HasPerm(dataprovider.PermAdminAny) {\n\t\treturn true\n\t}\n\treturn r.URL.Query().Get(\"confidential_data\") != \"1\"\n}\n\nfunc responseControllerDeadlines(rc *http.ResponseController, read, write time.Time) {\n\tif err := rc.SetReadDeadline(read); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to set read timeout to %s: %v\", read, err)\n\t}\n\tif err := rc.SetWriteDeadline(write); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to set write timeout to %s: %v\", write, err)\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/auth_utils.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\ntype tokenAudience = string\n\nconst (\n\ttokenAudienceWebAdmin         tokenAudience = \"WebAdmin\"\n\ttokenAudienceWebClient        tokenAudience = \"WebClient\"\n\ttokenAudienceWebShare         tokenAudience = \"WebShare\"\n\ttokenAudienceWebAdminPartial  tokenAudience = \"WebAdminPartial\"\n\ttokenAudienceWebClientPartial tokenAudience = \"WebClientPartial\"\n\ttokenAudienceAPI              tokenAudience = \"API\"\n\ttokenAudienceAPIUser          tokenAudience = \"APIUser\"\n\ttokenAudienceCSRF             tokenAudience = \"CSRF\"\n\ttokenAudienceOAuth2           tokenAudience = \"OAuth2\"\n\ttokenAudienceWebLogin         tokenAudience = \"WebLogin\"\n)\n\nconst (\n\ttokenValidationModeDefault       = 0\n\ttokenValidationModeNoIPMatch     = 1\n\ttokenValidationModeUserSignature = 2\n)\n\nconst (\n\tbasicRealm = \"Basic realm=\\\"SFTPGo\\\"\"\n)\n\nvar (\n\tapiTokenDuration    = 20 * time.Minute\n\tcookieTokenDuration = 20 * time.Minute\n\tshareTokenDuration  = 2 * time.Hour\n\t// csrf token duration is greater than normal token duration to reduce issues\n\t// with the login form\n\tcsrfTokenDuration      = 4 * time.Hour\n\tcookieRefreshThreshold = 10 * time.Minute\n\tmaxTokenDuration       = 12 * time.Hour\n\ttokenValidationMode    = tokenValidationModeDefault\n)\n\nfunc isTokenDurationValid(minutes int) bool {\n\treturn minutes >= 1 && minutes <= 720\n}\n\nfunc updateTokensDuration(api, cookie, share int) {\n\tif isTokenDurationValid(api) {\n\t\tapiTokenDuration = time.Duration(api) * time.Minute\n\t}\n\tif isTokenDurationValid(cookie) {\n\t\tcookieTokenDuration = time.Duration(cookie) * time.Minute\n\t\tcookieRefreshThreshold = cookieTokenDuration / 2\n\t\tif cookieTokenDuration > csrfTokenDuration {\n\t\t\tcsrfTokenDuration = cookieTokenDuration\n\t\t}\n\t}\n\tif isTokenDurationValid(share) {\n\t\tshareTokenDuration = time.Duration(share) * time.Minute\n\t}\n\tlogger.Debug(logSender, \"\", \"API token duration %s, cookie token duration %s, cookie refresh threshold %s, share token duration %s, csrf token duration %s\",\n\t\tapiTokenDuration, cookieTokenDuration, cookieRefreshThreshold, shareTokenDuration, csrfTokenDuration)\n}\n\nfunc getTokenDuration(audience tokenAudience) time.Duration {\n\tswitch audience {\n\tcase tokenAudienceWebShare:\n\t\treturn shareTokenDuration\n\tcase tokenAudienceWebLogin, tokenAudienceCSRF:\n\t\treturn csrfTokenDuration\n\tcase tokenAudienceAPI, tokenAudienceAPIUser:\n\t\treturn apiTokenDuration\n\tcase tokenAudienceWebAdmin, tokenAudienceWebClient:\n\t\treturn cookieTokenDuration\n\tcase tokenAudienceWebAdminPartial, tokenAudienceWebClientPartial, tokenAudienceOAuth2:\n\t\treturn 5 * time.Minute\n\tdefault:\n\t\tlogger.Error(logSender, \"\", \"token duration not handled for audience: %q\", audience)\n\t\treturn 20 * time.Minute\n\t}\n}\n\nfunc getMaxCookieDuration() time.Duration {\n\tresult := csrfTokenDuration\n\tif shareTokenDuration > result {\n\t\tresult = shareTokenDuration\n\t}\n\tif cookieTokenDuration > result {\n\t\tresult = cookieTokenDuration\n\t}\n\treturn result\n}\n\nfunc hasUserAudience(claims *jwt.Claims) bool {\n\treturn claims.HasAnyAudience([]string{tokenAudienceWebClient, tokenAudienceAPIUser})\n}\n\nfunc createAndSetCookie(w http.ResponseWriter, r *http.Request, claims *jwt.Claims, tokenAuth *jwt.Signer,\n\taudience tokenAudience, ip string,\n) error {\n\tduration := getTokenDuration(audience)\n\ttoken, err := tokenAuth.SignWithParams(claims, audience, ip, duration)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp := claims.BuildTokenResponse(token)\n\tvar basePath string\n\tif audience == tokenAudienceWebAdmin || audience == tokenAudienceWebAdminPartial {\n\t\tbasePath = webBaseAdminPath\n\t} else {\n\t\tbasePath = webBaseClientPath\n\t}\n\tsetCookie(w, r, basePath, resp.Token, duration)\n\n\treturn nil\n}\n\nfunc setCookie(w http.ResponseWriter, r *http.Request, cookiePath, cookieValue string, duration time.Duration) {\n\thttp.SetCookie(w, &http.Cookie{\n\t\tName:     jwt.CookieKey,\n\t\tValue:    cookieValue,\n\t\tPath:     cookiePath,\n\t\tExpires:  time.Now().Add(duration),\n\t\tMaxAge:   int(duration / time.Second),\n\t\tHttpOnly: true,\n\t\tSecure:   isTLS(r),\n\t\tSameSite: http.SameSiteStrictMode,\n\t})\n}\n\nfunc removeCookie(w http.ResponseWriter, r *http.Request, cookiePath string) {\n\tinvalidateToken(r)\n\thttp.SetCookie(w, &http.Cookie{\n\t\tName:     jwt.CookieKey,\n\t\tValue:    \"\",\n\t\tPath:     cookiePath,\n\t\tExpires:  time.Unix(0, 0),\n\t\tMaxAge:   -1,\n\t\tHttpOnly: true,\n\t\tSecure:   isTLS(r),\n\t\tSameSite: http.SameSiteStrictMode,\n\t})\n\tw.Header().Add(\"Cache-Control\", `no-cache=\"Set-Cookie\"`)\n}\n\nfunc oidcTokenFromContext(r *http.Request) string {\n\tif token, ok := r.Context().Value(oidcGeneratedToken).(string); ok {\n\t\treturn token\n\t}\n\treturn \"\"\n}\n\nfunc isTLS(r *http.Request) bool {\n\tif r.TLS != nil {\n\t\treturn true\n\t}\n\tif proto, ok := r.Context().Value(forwardedProtoKey).(string); ok {\n\t\treturn proto == \"https\" //nolint:goconst\n\t}\n\treturn false\n}\n\nfunc isTokenInvalidated(r *http.Request) bool {\n\tvar findTokenFns []func(r *http.Request) string\n\tfindTokenFns = append(findTokenFns, jwt.TokenFromHeader)\n\tfindTokenFns = append(findTokenFns, jwt.TokenFromCookie)\n\tfindTokenFns = append(findTokenFns, oidcTokenFromContext)\n\n\tisTokenFound := false\n\tfor _, fn := range findTokenFns {\n\t\ttoken := fn(r)\n\t\tif token != \"\" {\n\t\t\tisTokenFound = true\n\t\t\tif invalidatedJWTTokens.Get(token) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn !isTokenFound\n}\n\nfunc invalidateToken(r *http.Request) {\n\ttokenString := jwt.TokenFromHeader(r)\n\tif tokenString != \"\" {\n\t\tinvalidateTokenString(r, tokenString, apiTokenDuration)\n\t}\n\ttokenString = jwt.TokenFromCookie(r)\n\tif tokenString != \"\" {\n\t\tinvalidateTokenString(r, tokenString, getMaxCookieDuration())\n\t}\n}\n\nfunc invalidateTokenString(r *http.Request, tokenString string, fallbackDuration time.Duration) {\n\ttoken, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\tinvalidatedJWTTokens.Add(tokenString, time.Now().Add(fallbackDuration).UTC())\n\t\treturn\n\t}\n\tinvalidatedJWTTokens.Add(tokenString, token.Expiry.Time().Add(1*time.Minute).UTC())\n}\n\nfunc getUserFromToken(r *http.Request) *dataprovider.User {\n\tuser := &dataprovider.User{}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\treturn user\n\t}\n\tuser.Username = claims.Username\n\tuser.Filters.WebClient = claims.Permissions\n\tuser.Role = claims.Role\n\treturn user\n}\n\nfunc getAdminFromToken(r *http.Request) *dataprovider.Admin {\n\tadmin := &dataprovider.Admin{}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\treturn admin\n\t}\n\tadmin.Username = claims.Username\n\tadmin.Permissions = claims.Permissions\n\tadmin.Filters.Preferences.HideUserPageSections = claims.HideUserPageSections\n\tadmin.Role = claims.Role\n\treturn admin\n}\n\nfunc createLoginCookie(w http.ResponseWriter, r *http.Request, csrfTokenAuth *jwt.Signer, tokenID, basePath, ip string,\n) {\n\tc := jwt.NewClaims(tokenAudienceWebLogin, ip, getTokenDuration(tokenAudienceWebLogin))\n\tc.ID = tokenID\n\tresp, err := c.GenerateTokenResponse(csrfTokenAuth)\n\tif err != nil {\n\t\treturn\n\t}\n\tsetCookie(w, r, basePath, resp.Token, csrfTokenDuration)\n}\n\nfunc createCSRFToken(w http.ResponseWriter, r *http.Request, csrfTokenAuth *jwt.Signer, tokenID,\n\tbasePath string,\n) string {\n\tip := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tclaims := jwt.NewClaims(tokenAudienceCSRF, ip, csrfTokenDuration)\n\tclaims.ID = rand.Text()\n\tif tokenID != \"\" {\n\t\tcreateLoginCookie(w, r, csrfTokenAuth, tokenID, basePath, ip)\n\t\tclaims.Ref = tokenID\n\t} else {\n\t\tif c, err := jwt.FromContext(r.Context()); err == nil {\n\t\t\tclaims.Ref = c.ID\n\t\t} else {\n\t\t\tlogger.Error(logSender, \"\", \"unable to add reference to CSRF token: %v\", err)\n\t\t}\n\t}\n\ttokenString, err := csrfTokenAuth.Sign(claims)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to create CSRF token: %v\", err)\n\t\treturn \"\"\n\t}\n\treturn tokenString\n}\n\nfunc verifyCSRFToken(r *http.Request, csrfTokenAuth *jwt.Signer) error {\n\ttokenString := r.Form.Get(csrfFormToken)\n\ttoken, err := jwt.VerifyToken(csrfTokenAuth, tokenString)\n\tif err != nil || token == nil {\n\t\tlogger.Debug(logSender, \"\", \"error validating CSRF token %q: %v\", tokenString, err)\n\t\treturn fmt.Errorf(\"unable to verify form token: %v\", err)\n\t}\n\n\tif !token.Audience.Contains(tokenAudienceCSRF) {\n\t\tlogger.Debug(logSender, \"\", \"error validating CSRF token audience\")\n\t\treturn errors.New(\"the form token is not valid\")\n\t}\n\n\tif err := validateIPForToken(token, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {\n\t\tlogger.Debug(logSender, \"\", \"error validating CSRF token IP audience\")\n\t\treturn errors.New(\"the form token is not valid\")\n\t}\n\treturn checkCSRFTokenRef(r, token)\n}\n\nfunc checkCSRFTokenRef(r *http.Request, token *jwt.Claims) error {\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"error getting token claims for CSRF validation: %v\", err)\n\t\treturn err\n\t}\n\tif token.ID == \"\" {\n\t\tlogger.Debug(logSender, \"\", \"error validating CSRF token, missing reference\")\n\t\treturn errors.New(\"the form token is not valid\")\n\t}\n\tif claims.ID != token.Ref {\n\t\tlogger.Debug(logSender, \"\", \"error validating CSRF reference, id %q, reference %q\", claims.ID, token.ID)\n\t\treturn errors.New(\"unexpected form token\")\n\t}\n\n\treturn nil\n}\n\nfunc verifyLoginCookie(r *http.Request) error {\n\ttoken, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"error getting login token: %v\", err)\n\t\treturn errInvalidToken\n\t}\n\tif isTokenInvalidated(r) {\n\t\tlogger.Debug(logSender, \"\", \"the login token has been invalidated\")\n\t\treturn errInvalidToken\n\t}\n\tif !token.Audience.Contains(tokenAudienceWebLogin) {\n\t\tlogger.Debug(logSender, \"\", \"the token with id %q is not valid for audience %q\", token.ID, tokenAudienceWebLogin)\n\t\treturn errInvalidToken\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := validateIPForToken(token, ipAddr); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc verifyLoginCookieAndCSRFToken(r *http.Request, csrfTokenAuth *jwt.Signer) error {\n\tif err := verifyLoginCookie(r); err != nil {\n\t\treturn err\n\t}\n\tif err := verifyCSRFToken(r, csrfTokenAuth); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc createOAuth2Token(csrfTokenAuth *jwt.Signer, state, ip string) string {\n\tclaims := jwt.NewClaims(tokenAudienceOAuth2, ip, getTokenDuration(tokenAudienceOAuth2))\n\tclaims.ID = state\n\n\ttokenString, err := csrfTokenAuth.Sign(claims)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to create OAuth2 token: %v\", err)\n\t\treturn \"\"\n\t}\n\treturn tokenString\n}\n\nfunc verifyOAuth2Token(csrfTokenAuth *jwt.Signer, tokenString, ip string) (string, error) {\n\ttoken, err := jwt.VerifyToken(csrfTokenAuth, tokenString)\n\tif err != nil || token == nil {\n\t\tlogger.Debug(logSender, \"\", \"error validating OAuth2 token %q: %v\", tokenString, err)\n\t\treturn \"\", util.NewI18nError(\n\t\t\tfmt.Errorf(\"unable to verify OAuth2 state: %v\", err),\n\t\t\tutil.I18nOAuth2ErrorVerifyState,\n\t\t)\n\t}\n\n\tif !token.Audience.Contains(tokenAudienceOAuth2) {\n\t\tlogger.Debug(logSender, \"\", \"error validating OAuth2 token audience\")\n\t\treturn \"\", util.NewI18nError(errors.New(\"invalid OAuth2 state\"), util.I18nOAuth2InvalidState)\n\t}\n\n\tif err := validateIPForToken(token, ip); err != nil {\n\t\tlogger.Debug(logSender, \"\", \"error validating OAuth2 token IP audience\")\n\t\treturn \"\", util.NewI18nError(errors.New(\"invalid OAuth2 state\"), util.I18nOAuth2InvalidState)\n\t}\n\tif token.ID != \"\" {\n\t\treturn token.ID, nil\n\t}\n\tlogger.Debug(logSender, \"\", \"jti not found in OAuth2 token\")\n\treturn \"\", util.NewI18nError(errors.New(\"invalid OAuth2 state\"), util.I18nOAuth2InvalidState)\n}\n\nfunc validateIPForToken(token *jwt.Claims, ip string) error {\n\tif tokenValidationMode&tokenValidationModeNoIPMatch == 0 {\n\t\tif !token.Audience.Contains(ip) {\n\t\t\treturn errInvalidToken\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkTokenSignature(r *http.Request, token *jwt.Claims) error {\n\tif _, ok := r.Context().Value(oidcTokenKey).(string); ok {\n\t\treturn nil\n\t}\n\tvar err error\n\tif tokenValidationMode&tokenValidationModeUserSignature != 0 {\n\t\tfor _, audience := range token.Audience {\n\t\t\tswitch audience {\n\t\t\tcase tokenAudienceAPI, tokenAudienceWebAdmin:\n\t\t\t\terr = validateSignatureForToken(token, dataprovider.GetAdminSignature)\n\t\t\tcase tokenAudienceAPIUser, tokenAudienceWebClient:\n\t\t\t\terr = validateSignatureForToken(token, dataprovider.GetUserSignature)\n\t\t\t}\n\t\t}\n\t}\n\tif err != nil {\n\t\tinvalidateToken(r)\n\t}\n\treturn err\n}\n\nfunc validateSignatureForToken(token *jwt.Claims, getter func(string) (string, error)) error {\n\tsignature, err := getter(token.Username)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get signature for username %q: %v\", token.Username, err)\n\t\treturn errInvalidToken\n\t}\n\tif signature != \"\" && signature == token.Subject {\n\t\treturn nil\n\t}\n\tlogger.Debug(logSender, \"\", \"signature mismatch for username %q, signature %q, token signature %q\",\n\t\ttoken.Username, signature, token.Subject)\n\treturn errInvalidToken\n}\n"
  },
  {
    "path": "internal/httpd/file.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"io\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\ntype httpdFile struct {\n\t*common.BaseTransfer\n\twriter     io.WriteCloser\n\treader     io.ReadCloser\n\tisFinished bool\n}\n\nfunc newHTTPDFile(baseTransfer *common.BaseTransfer, pipeWriter vfs.PipeWriter, pipeReader vfs.PipeReader) *httpdFile {\n\tvar writer io.WriteCloser\n\tvar reader io.ReadCloser\n\tif baseTransfer.File != nil {\n\t\twriter = baseTransfer.File\n\t\treader = baseTransfer.File\n\t} else if pipeWriter != nil {\n\t\twriter = pipeWriter\n\t} else if pipeReader != nil {\n\t\treader = pipeReader\n\t}\n\treturn &httpdFile{\n\t\tBaseTransfer: baseTransfer,\n\t\twriter:       writer,\n\t\treader:       reader,\n\t\tisFinished:   false,\n\t}\n}\n\n// Read reads the contents to downloads.\nfunc (f *httpdFile) Read(p []byte) (n int, err error) {\n\tif f.AbortTransfer.Load() {\n\t\terr := f.GetAbortError()\n\t\tf.TransferError(err)\n\t\treturn 0, err\n\t}\n\n\tf.Connection.UpdateLastActivity()\n\n\tn, err = f.reader.Read(p)\n\tf.BytesSent.Add(int64(n))\n\n\tif err == nil {\n\t\terr = f.CheckRead()\n\t}\n\tif err != nil && err != io.EOF {\n\t\tf.TransferError(err)\n\t\terr = f.ConvertError(err)\n\t\treturn\n\t}\n\tf.HandleThrottle()\n\treturn\n}\n\n// Write writes the contents to upload\nfunc (f *httpdFile) Write(p []byte) (n int, err error) {\n\tif f.AbortTransfer.Load() {\n\t\terr := f.GetAbortError()\n\t\tf.TransferError(err)\n\t\treturn 0, err\n\t}\n\n\tf.Connection.UpdateLastActivity()\n\n\tn, err = f.writer.Write(p)\n\tf.BytesReceived.Add(int64(n))\n\n\tif err == nil {\n\t\terr = f.CheckWrite()\n\t}\n\tif err != nil {\n\t\tf.TransferError(err)\n\t\terr = f.ConvertError(err)\n\t\treturn\n\t}\n\tf.HandleThrottle()\n\treturn\n}\n\n// Close closes the current transfer\nfunc (f *httpdFile) Close() error {\n\tif err := f.setFinished(); err != nil {\n\t\treturn err\n\t}\n\terr := f.closeIO()\n\terrBaseClose := f.BaseTransfer.Close()\n\tif errBaseClose != nil {\n\t\terr = errBaseClose\n\t}\n\n\treturn f.Connection.GetFsError(f.Fs, err)\n}\n\nfunc (f *httpdFile) closeIO() error {\n\tvar err error\n\tif f.File != nil {\n\t\terr = f.File.Close()\n\t} else if f.writer != nil {\n\t\terr = f.writer.Close()\n\t\tf.Lock()\n\t\t// we set ErrTransfer here so quota is not updated, in this case the uploads are atomic\n\t\tif err != nil && f.ErrTransfer == nil {\n\t\t\tf.ErrTransfer = err\n\t\t}\n\t\tf.Unlock()\n\t} else if f.reader != nil {\n\t\terr = f.reader.Close()\n\t\tif metadater, ok := f.reader.(vfs.Metadater); ok {\n\t\t\tf.SetMetadata(metadater.Metadata())\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (f *httpdFile) setFinished() error {\n\tf.Lock()\n\tdefer f.Unlock()\n\n\tif f.isFinished {\n\t\treturn common.ErrTransferClosed\n\t}\n\tf.isFinished = true\n\treturn nil\n}\n"
  },
  {
    "path": "internal/httpd/flash.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\tflashCookieName = \"message\"\n)\n\nfunc newFlashMessage(errorStrig, i18nMessage string) flashMessage {\n\treturn flashMessage{\n\t\tErrorString: errorStrig,\n\t\tI18nMessage: i18nMessage,\n\t}\n}\n\ntype flashMessage struct {\n\tErrorString string `json:\"error\"`\n\tI18nMessage string `json:\"message\"`\n}\n\nfunc (m *flashMessage) getI18nError() *util.I18nError {\n\tif m.ErrorString == \"\" && m.I18nMessage == \"\" {\n\t\treturn nil\n\t}\n\treturn util.NewI18nError(\n\t\tutil.NewGenericError(m.ErrorString),\n\t\tm.I18nMessage,\n\t)\n}\n\nfunc setFlashMessage(w http.ResponseWriter, r *http.Request, message flashMessage) {\n\tvalue, err := json.Marshal(message)\n\tif err != nil {\n\t\treturn\n\t}\n\thttp.SetCookie(w, &http.Cookie{\n\t\tName:     flashCookieName,\n\t\tValue:    base64.URLEncoding.EncodeToString(value),\n\t\tPath:     \"/\",\n\t\tExpires:  time.Now().Add(60 * time.Second),\n\t\tMaxAge:   60,\n\t\tHttpOnly: true,\n\t\tSecure:   isTLS(r),\n\t\tSameSite: http.SameSiteLaxMode,\n\t})\n\tw.Header().Add(\"Cache-Control\", `no-cache=\"Set-Cookie\"`)\n}\n\nfunc getFlashMessage(w http.ResponseWriter, r *http.Request) flashMessage {\n\tvar msg flashMessage\n\tcookie, err := r.Cookie(flashCookieName)\n\tif err != nil {\n\t\treturn msg\n\t}\n\thttp.SetCookie(w, &http.Cookie{\n\t\tName:     flashCookieName,\n\t\tValue:    \"\",\n\t\tPath:     \"/\",\n\t\tExpires:  time.Unix(0, 0),\n\t\tMaxAge:   -1,\n\t\tHttpOnly: true,\n\t\tSecure:   isTLS(r),\n\t\tSameSite: http.SameSiteLaxMode,\n\t})\n\tvalue, err := base64.URLEncoding.DecodeString(cookie.Value)\n\tif err != nil {\n\t\treturn msg\n\t}\n\terr = json.Unmarshal(value, &msg)\n\tif err != nil {\n\t\treturn flashMessage{}\n\t}\n\treturn msg\n}\n"
  },
  {
    "path": "internal/httpd/flash_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc TestFlashMessages(t *testing.T) {\n\trr := httptest.NewRecorder()\n\treq, err := http.NewRequest(http.MethodGet, \"/url\", nil)\n\trequire.NoError(t, err)\n\tmessage := flashMessage{\n\t\tErrorString: \"error\",\n\t\tI18nMessage: util.I18nChangePwdTitle,\n\t}\n\tsetFlashMessage(rr, req, message)\n\tvalue, err := json.Marshal(message)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", flashCookieName, base64.URLEncoding.EncodeToString(value)))\n\tmsg := getFlashMessage(rr, req)\n\tassert.Equal(t, message, msg)\n\tassert.Equal(t, util.I18nChangePwdTitle, msg.getI18nError().Message)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", flashCookieName, \"a\"))\n\tmsg = getFlashMessage(rr, req)\n\tassert.Empty(t, msg)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", flashCookieName, \"YQ==\"))\n\tmsg = getFlashMessage(rr, req)\n\tassert.Empty(t, msg)\n}\n"
  },
  {
    "path": "internal/httpd/handler.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\n// Connection details for a HTTP connection used to inteact with an SFTPGo filesystem\ntype Connection struct {\n\t*common.BaseConnection\n\trequest *http.Request\n\trc      *http.ResponseController\n}\n\nfunc newConnection(conn *common.BaseConnection, w http.ResponseWriter, r *http.Request) *Connection {\n\trc := http.NewResponseController(w)\n\tresponseControllerDeadlines(rc, time.Time{}, time.Time{})\n\treturn &Connection{\n\t\tBaseConnection: conn,\n\t\trequest:        r,\n\t\trc:             rc,\n\t}\n}\n\n// GetClientVersion returns the connected client's version.\nfunc (c *Connection) GetClientVersion() string {\n\tif c.request != nil {\n\t\treturn c.request.UserAgent()\n\t}\n\treturn \"\"\n}\n\n// GetLocalAddress returns local connection address\nfunc (c *Connection) GetLocalAddress() string {\n\treturn util.GetHTTPLocalAddress(c.request)\n}\n\n// GetRemoteAddress returns the connected client's address\nfunc (c *Connection) GetRemoteAddress() string {\n\tif c.request != nil {\n\t\treturn c.request.RemoteAddr\n\t}\n\treturn \"\"\n}\n\n// Disconnect closes the active transfer\nfunc (c *Connection) Disconnect() (err error) {\n\tif c.rc != nil {\n\t\tresponseControllerDeadlines(c.rc, time.Now().Add(5*time.Second), time.Now().Add(5*time.Second))\n\t}\n\treturn c.SignalTransfersAbort()\n}\n\n// GetCommand returns the request method\nfunc (c *Connection) GetCommand() string {\n\tif c.request != nil {\n\t\treturn strings.ToUpper(c.request.Method)\n\t}\n\treturn \"\"\n}\n\n// Stat returns a FileInfo describing the named file/directory, or an error,\n// if any happens\nfunc (c *Connection) Stat(name string, mode int) (os.FileInfo, error) {\n\tc.UpdateLastActivity()\n\n\tif !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tfi, err := c.DoStat(name, mode, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn fi, err\n}\n\n// ReadDir returns a list of directory entries\nfunc (c *Connection) ReadDir(name string) (vfs.DirLister, error) {\n\tc.UpdateLastActivity()\n\n\treturn c.ListDir(name)\n}\n\nfunc (c *Connection) getFileReader(name string, offset int64, method string) (io.ReadCloser, error) {\n\tc.UpdateLastActivity()\n\n\tif err := common.Connections.IsNewTransferAllowed(c.User.Username); err != nil {\n\t\tc.Log(logger.LevelInfo, \"denying file read due to transfer count limits\")\n\t\treturn nil, util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message)\n\t}\n\n\ttransferQuota := c.GetTransferQuota()\n\tif !transferQuota.HasDownloadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file read due to quota limits\")\n\t\treturn nil, util.NewI18nError(c.GetReadQuotaExceededError(), util.I18nErrorQuotaRead)\n\t}\n\n\tif !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) {\n\t\treturn nil, util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message)\n\t}\n\n\tif ok, policy := c.User.IsFileAllowed(name); !ok {\n\t\tc.Log(logger.LevelWarn, \"reading file %q is not allowed\", name)\n\t\treturn nil, util.NewI18nError(c.GetErrorForDeniedFile(policy), util.I18nError403Message)\n\t}\n\n\tfs, p, err := c.GetFsAndResolvedPath(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif method != http.MethodHead {\n\t\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, name, 0, 0); err != nil {\n\t\t\tc.Log(logger.LevelDebug, \"download for file %q denied by pre action: %v\", name, err)\n\t\t\treturn nil, util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message)\n\t\t}\n\t}\n\n\tfile, r, cancelFn, err := fs.Open(p, offset)\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"could not open file %q for reading: %+v\", p, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, p, p, name, common.TransferDownload,\n\t\t0, 0, 0, 0, false, fs, transferQuota)\n\treturn newHTTPDFile(baseTransfer, nil, r), nil\n}\n\nfunc (c *Connection) getFileWriter(name string) (io.WriteCloser, error) {\n\tc.UpdateLastActivity()\n\n\tif ok, _ := c.User.IsFileAllowed(name); !ok {\n\t\tc.Log(logger.LevelWarn, \"writing file %q is not allowed\", name)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tfs, p, err := c.GetFsAndResolvedPath(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfilePath := p\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\tfilePath = fs.GetAtomicUploadPath(p)\n\t}\n\n\tstat, statErr := fs.Lstat(p)\n\tif (statErr == nil && stat.Mode()&os.ModeSymlink != 0) || fs.IsNotExist(statErr) {\n\t\tif !c.User.HasPerm(dataprovider.PermUpload, path.Dir(name)) {\n\t\t\treturn nil, c.GetPermissionDeniedError()\n\t\t}\n\t\treturn c.handleUploadFile(fs, p, filePath, name, true, 0)\n\t}\n\n\tif statErr != nil {\n\t\tc.Log(logger.LevelError, \"error performing file stat %q: %+v\", p, statErr)\n\t\treturn nil, c.GetFsError(fs, statErr)\n\t}\n\n\t// This happen if we upload a file that has the same name of an existing directory\n\tif stat.IsDir() {\n\t\tc.Log(logger.LevelError, \"attempted to open a directory for writing to: %q\", p)\n\t\treturn nil, c.GetOpUnsupportedError()\n\t}\n\n\tif !c.User.HasPerm(dataprovider.PermOverwrite, path.Dir(name)) {\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\t_, _, err = fs.Rename(p, filePath, 0)\n\t\tif err != nil {\n\t\t\tc.Log(logger.LevelError, \"error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v\",\n\t\t\t\tp, filePath, err)\n\t\t\treturn nil, c.GetFsError(fs, err)\n\t\t}\n\t}\n\n\treturn c.handleUploadFile(fs, p, filePath, name, false, stat.Size())\n}\n\nfunc (c *Connection) handleUploadFile(fs vfs.Fs, resolvedPath, filePath, requestPath string, isNewFile bool, fileSize int64) (io.WriteCloser, error) {\n\tif err := common.Connections.IsNewTransferAllowed(c.User.Username); err != nil {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to transfer count limits\")\n\t\treturn nil, util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message)\n\t}\n\tdiskQuota, transferQuota := c.HasSpace(isNewFile, false, requestPath)\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to quota limits\")\n\t\treturn nil, common.ErrQuotaExceeded\n\t}\n\t_, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, os.O_TRUNC)\n\tif err != nil {\n\t\tc.Log(logger.LevelDebug, \"upload for file %q denied by pre action: %v\", requestPath, err)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tmaxWriteSize, _ := c.GetMaxWriteSize(diskQuota, false, fileSize, fs.IsUploadResumeSupported())\n\n\tfile, w, cancelFn, err := fs.Create(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, c.GetCreateChecks(requestPath, isNewFile, false))\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error opening existing file, source: %q, err: %+v\", filePath, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tinitialSize := int64(0)\n\ttruncatedSize := int64(0) // bytes truncated and not included in quota\n\tif !isNewFile {\n\t\tif vfs.HasTruncateSupport(fs) {\n\t\t\tvfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))\n\t\t\tif err == nil {\n\t\t\t\tdataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -fileSize, false)\n\t\t\t} else {\n\t\t\t\tdataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck\n\t\t\t}\n\t\t} else {\n\t\t\tinitialSize = fileSize\n\t\t\ttruncatedSize = fileSize\n\t\t}\n\t\tif maxWriteSize > 0 {\n\t\t\tmaxWriteSize += fileSize\n\t\t}\n\t}\n\n\tvfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,\n\t\tcommon.TransferUpload, 0, initialSize, maxWriteSize, truncatedSize, isNewFile, fs, transferQuota)\n\treturn newHTTPDFile(baseTransfer, w, nil), nil\n}\n\nfunc newThrottledReader(r io.ReadCloser, limit int64, conn *Connection) *throttledReader {\n\tt := &throttledReader{\n\t\tid:    conn.GetTransferID(),\n\t\tlimit: limit,\n\t\tr:     r,\n\t\tstart: time.Now(),\n\t\tconn:  conn,\n\t}\n\tt.bytesRead.Store(0)\n\tt.abortTransfer.Store(false)\n\tconn.AddTransfer(t)\n\treturn t\n}\n\ntype throttledReader struct {\n\tbytesRead     atomic.Int64\n\tid            int64\n\tlimit         int64\n\tr             io.ReadCloser\n\tabortTransfer atomic.Bool\n\tstart         time.Time\n\tconn          *Connection\n\tmu            sync.Mutex\n\terrAbort      error\n}\n\nfunc (t *throttledReader) GetID() int64 {\n\treturn t.id\n}\n\nfunc (t *throttledReader) GetType() int {\n\treturn common.TransferUpload\n}\n\nfunc (t *throttledReader) GetSize() int64 {\n\treturn t.bytesRead.Load()\n}\n\nfunc (t *throttledReader) GetDownloadedSize() int64 {\n\treturn 0\n}\n\nfunc (t *throttledReader) GetUploadedSize() int64 {\n\treturn t.bytesRead.Load()\n}\n\nfunc (t *throttledReader) GetVirtualPath() string {\n\treturn \"**reading request body**\"\n}\n\nfunc (t *throttledReader) GetStartTime() time.Time {\n\treturn t.start\n}\n\nfunc (t *throttledReader) GetAbortError() error {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\n\tif t.errAbort != nil {\n\t\treturn t.errAbort\n\t}\n\treturn common.ErrTransferAborted\n}\n\nfunc (t *throttledReader) SignalClose(err error) {\n\tt.mu.Lock()\n\tt.errAbort = err\n\tt.mu.Unlock()\n\tt.abortTransfer.Store(true)\n}\n\nfunc (t *throttledReader) GetTruncatedSize() int64 {\n\treturn 0\n}\n\nfunc (t *throttledReader) HasSizeLimit() bool {\n\treturn false\n}\n\nfunc (t *throttledReader) Truncate(_ string, _ int64) (int64, error) {\n\treturn 0, vfs.ErrVfsUnsupported\n}\n\nfunc (t *throttledReader) GetRealFsPath(_ string) string {\n\treturn \"\"\n}\n\nfunc (t *throttledReader) GetFsPath() string {\n\treturn \"\"\n}\n\nfunc (t *throttledReader) SetTimes(_ string, _ time.Time, _ time.Time) bool {\n\treturn false\n}\n\nfunc (t *throttledReader) Read(p []byte) (n int, err error) {\n\tif t.abortTransfer.Load() {\n\t\treturn 0, t.GetAbortError()\n\t}\n\n\tt.conn.UpdateLastActivity()\n\tn, err = t.r.Read(p)\n\tif t.limit > 0 {\n\t\tt.bytesRead.Add(int64(n))\n\t\ttrasferredBytes := t.bytesRead.Load()\n\t\telapsed := time.Since(t.start).Nanoseconds() / 1000000\n\t\twantedElapsed := 1000 * (trasferredBytes / 1024) / t.limit\n\t\tif wantedElapsed > elapsed {\n\t\t\ttoSleep := time.Duration(wantedElapsed - elapsed)\n\t\t\ttime.Sleep(toSleep * time.Millisecond)\n\t\t}\n\t}\n\treturn\n}\n\nfunc (t *throttledReader) Close() error {\n\treturn t.r.Close()\n}\n"
  },
  {
    "path": "internal/httpd/httpd.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package httpd implements REST API and Web interface for SFTPGo.\n// The OpenAPI 3 schema for the supported API can be found inside the source tree:\n// https://github.com/drakkan/sftpgo/blob/main/openapi/openapi.yaml\npackage httpd\n\nimport (\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/acme\"\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/ftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\nconst (\n\tlogSender                             = \"httpd\"\n\ttokenPath                             = \"/api/v2/token\"\n\tlogoutPath                            = \"/api/v2/logout\"\n\tuserTokenPath                         = \"/api/v2/user/token\"\n\tuserLogoutPath                        = \"/api/v2/user/logout\"\n\tactiveConnectionsPath                 = \"/api/v2/connections\"\n\tquotasBasePath                        = \"/api/v2/quotas\"\n\tuserPath                              = \"/api/v2/users\"\n\tversionPath                           = \"/api/v2/version\"\n\tfolderPath                            = \"/api/v2/folders\"\n\tgroupPath                             = \"/api/v2/groups\"\n\tserverStatusPath                      = \"/api/v2/status\"\n\tdumpDataPath                          = \"/api/v2/dumpdata\"\n\tloadDataPath                          = \"/api/v2/loaddata\"\n\tdefenderHosts                         = \"/api/v2/defender/hosts\"\n\tadminPath                             = \"/api/v2/admins\"\n\tadminPwdPath                          = \"/api/v2/admin/changepwd\"\n\tadminProfilePath                      = \"/api/v2/admin/profile\"\n\tuserPwdPath                           = \"/api/v2/user/changepwd\"\n\tuserDirsPath                          = \"/api/v2/user/dirs\"\n\tuserFilesPath                         = \"/api/v2/user/files\"\n\tuserFileActionsPath                   = \"/api/v2/user/file-actions\"\n\tuserStreamZipPath                     = \"/api/v2/user/streamzip\"\n\tuserUploadFilePath                    = \"/api/v2/user/files/upload\"\n\tuserFilesDirsMetadataPath             = \"/api/v2/user/files/metadata\"\n\tapiKeysPath                           = \"/api/v2/apikeys\"\n\tadminTOTPConfigsPath                  = \"/api/v2/admin/totp/configs\"\n\tadminTOTPGeneratePath                 = \"/api/v2/admin/totp/generate\"\n\tadminTOTPValidatePath                 = \"/api/v2/admin/totp/validate\"\n\tadminTOTPSavePath                     = \"/api/v2/admin/totp/save\"\n\tadmin2FARecoveryCodesPath             = \"/api/v2/admin/2fa/recoverycodes\"\n\tuserTOTPConfigsPath                   = \"/api/v2/user/totp/configs\"\n\tuserTOTPGeneratePath                  = \"/api/v2/user/totp/generate\"\n\tuserTOTPValidatePath                  = \"/api/v2/user/totp/validate\"\n\tuserTOTPSavePath                      = \"/api/v2/user/totp/save\"\n\tuser2FARecoveryCodesPath              = \"/api/v2/user/2fa/recoverycodes\"\n\tuserProfilePath                       = \"/api/v2/user/profile\"\n\tuserSharesPath                        = \"/api/v2/user/shares\"\n\tretentionChecksPath                   = \"/api/v2/retention/users/checks\"\n\tfsEventsPath                          = \"/api/v2/events/fs\"\n\tproviderEventsPath                    = \"/api/v2/events/provider\"\n\tlogEventsPath                         = \"/api/v2/events/logs\"\n\tsharesPath                            = \"/api/v2/shares\"\n\teventActionsPath                      = \"/api/v2/eventactions\"\n\teventRulesPath                        = \"/api/v2/eventrules\"\n\trolesPath                             = \"/api/v2/roles\"\n\tipListsPath                           = \"/api/v2/iplists\"\n\thealthzPath                           = \"/healthz\"\n\twebRootPathDefault                    = \"/\"\n\twebBasePathDefault                    = \"/web\"\n\twebBasePathAdminDefault               = \"/web/admin\"\n\twebBasePathClientDefault              = \"/web/client\"\n\twebAdminSetupPathDefault              = \"/web/admin/setup\"\n\twebAdminLoginPathDefault              = \"/web/admin/login\"\n\twebAdminOIDCLoginPathDefault          = \"/web/admin/oidclogin\"\n\twebOIDCRedirectPathDefault            = \"/web/oidc/redirect\"\n\twebOAuth2RedirectPathDefault          = \"/web/oauth2/redirect\"\n\twebOAuth2TokenPathDefault             = \"/web/admin/oauth2/token\"\n\twebAdminTwoFactorPathDefault          = \"/web/admin/twofactor\"\n\twebAdminTwoFactorRecoveryPathDefault  = \"/web/admin/twofactor-recovery\"\n\twebLogoutPathDefault                  = \"/web/admin/logout\"\n\twebUsersPathDefault                   = \"/web/admin/users\"\n\twebUserPathDefault                    = \"/web/admin/user\"\n\twebConnectionsPathDefault             = \"/web/admin/connections\"\n\twebFoldersPathDefault                 = \"/web/admin/folders\"\n\twebFolderPathDefault                  = \"/web/admin/folder\"\n\twebGroupsPathDefault                  = \"/web/admin/groups\"\n\twebGroupPathDefault                   = \"/web/admin/group\"\n\twebStatusPathDefault                  = \"/web/admin/status\"\n\twebAdminsPathDefault                  = \"/web/admin/managers\"\n\twebAdminPathDefault                   = \"/web/admin/manager\"\n\twebMaintenancePathDefault             = \"/web/admin/maintenance\"\n\twebBackupPathDefault                  = \"/web/admin/backup\"\n\twebRestorePathDefault                 = \"/web/admin/restore\"\n\twebScanVFolderPathDefault             = \"/web/admin/quotas/scanfolder\"\n\twebQuotaScanPathDefault               = \"/web/admin/quotas/scanuser\"\n\twebChangeAdminPwdPathDefault          = \"/web/admin/changepwd\"\n\twebAdminForgotPwdPathDefault          = \"/web/admin/forgot-password\"\n\twebAdminResetPwdPathDefault           = \"/web/admin/reset-password\"\n\twebAdminProfilePathDefault            = \"/web/admin/profile\"\n\twebAdminMFAPathDefault                = \"/web/admin/mfa\"\n\twebAdminEventRulesPathDefault         = \"/web/admin/eventrules\"\n\twebAdminEventRulePathDefault          = \"/web/admin/eventrule\"\n\twebAdminEventActionsPathDefault       = \"/web/admin/eventactions\"\n\twebAdminEventActionPathDefault        = \"/web/admin/eventaction\"\n\twebAdminRolesPathDefault              = \"/web/admin/roles\"\n\twebAdminRolePathDefault               = \"/web/admin/role\"\n\twebAdminTOTPGeneratePathDefault       = \"/web/admin/totp/generate\"\n\twebAdminTOTPValidatePathDefault       = \"/web/admin/totp/validate\"\n\twebAdminTOTPSavePathDefault           = \"/web/admin/totp/save\"\n\twebAdminRecoveryCodesPathDefault      = \"/web/admin/recoverycodes\"\n\twebTemplateUserDefault                = \"/web/admin/template/user\"\n\twebTemplateFolderDefault              = \"/web/admin/template/folder\"\n\twebDefenderPathDefault                = \"/web/admin/defender\"\n\twebIPListsPathDefault                 = \"/web/admin/ip-lists\"\n\twebIPListPathDefault                  = \"/web/admin/ip-list\"\n\twebDefenderHostsPathDefault           = \"/web/admin/defender/hosts\"\n\twebEventsPathDefault                  = \"/web/admin/events\"\n\twebEventsFsSearchPathDefault          = \"/web/admin/events/fs\"\n\twebEventsProviderSearchPathDefault    = \"/web/admin/events/provider\"\n\twebEventsLogSearchPathDefault         = \"/web/admin/events/logs\"\n\twebConfigsPathDefault                 = \"/web/admin/configs\"\n\twebClientLoginPathDefault             = \"/web/client/login\"\n\twebClientOIDCLoginPathDefault         = \"/web/client/oidclogin\"\n\twebClientTwoFactorPathDefault         = \"/web/client/twofactor\"\n\twebClientTwoFactorRecoveryPathDefault = \"/web/client/twofactor-recovery\"\n\twebClientFilesPathDefault             = \"/web/client/files\"\n\twebClientFilePathDefault              = \"/web/client/file\"\n\twebClientFileActionsPathDefault       = \"/web/client/file-actions\"\n\twebClientSharesPathDefault            = \"/web/client/shares\"\n\twebClientSharePathDefault             = \"/web/client/share\"\n\twebClientEditFilePathDefault          = \"/web/client/editfile\"\n\twebClientDirsPathDefault              = \"/web/client/dirs\"\n\twebClientDownloadZipPathDefault       = \"/web/client/downloadzip\"\n\twebClientProfilePathDefault           = \"/web/client/profile\"\n\twebClientPingPathDefault              = \"/web/client/ping\"\n\twebClientMFAPathDefault               = \"/web/client/mfa\"\n\twebClientTOTPGeneratePathDefault      = \"/web/client/totp/generate\"\n\twebClientTOTPValidatePathDefault      = \"/web/client/totp/validate\"\n\twebClientTOTPSavePathDefault          = \"/web/client/totp/save\"\n\twebClientRecoveryCodesPathDefault     = \"/web/client/recoverycodes\"\n\twebChangeClientPwdPathDefault         = \"/web/client/changepwd\"\n\twebClientLogoutPathDefault            = \"/web/client/logout\"\n\twebClientPubSharesPathDefault         = \"/web/client/pubshares\"\n\twebClientForgotPwdPathDefault         = \"/web/client/forgot-password\"\n\twebClientResetPwdPathDefault          = \"/web/client/reset-password\"\n\twebClientViewPDFPathDefault           = \"/web/client/viewpdf\"\n\twebClientGetPDFPathDefault            = \"/web/client/getpdf\"\n\twebClientExistPathDefault             = \"/web/client/exist\"\n\twebClientTasksPathDefault             = \"/web/client/tasks\"\n\twebStaticFilesPathDefault             = \"/static\"\n\twebOpenAPIPathDefault                 = \"/openapi\"\n\t// MaxRestoreSize defines the max size for the loaddata input file\n\tMaxRestoreSize       = 20 * 1048576 // 20 MB\n\tmaxRequestSize       = 1048576      // 1MB\n\tmaxLoginBodySize     = 262144       // 256 KB\n\thttpdMaxEditFileSize = 2 * 1048576  // 2 MB\n\tmaxMultipartMem      = 10 * 1048576 // 10 MB\n\tosWindows            = \"windows\"\n\totpHeaderCode        = \"X-SFTPGO-OTP\"\n\tmTimeHeader          = \"X-SFTPGO-MTIME\"\n\tacmeChallengeURI     = \"/.well-known/acme-challenge/\"\n)\n\nvar (\n\tcertMgr                        *common.CertManager\n\tcleanupTicker                  *time.Ticker\n\tcleanupDone                    chan bool\n\tinvalidatedJWTTokens           tokenManager\n\twebRootPath                    string\n\twebBasePath                    string\n\twebBaseAdminPath               string\n\twebBaseClientPath              string\n\twebOIDCRedirectPath            string\n\twebOAuth2RedirectPath          string\n\twebOAuth2TokenPath             string\n\twebAdminSetupPath              string\n\twebAdminOIDCLoginPath          string\n\twebAdminLoginPath              string\n\twebAdminTwoFactorPath          string\n\twebAdminTwoFactorRecoveryPath  string\n\twebLogoutPath                  string\n\twebUsersPath                   string\n\twebUserPath                    string\n\twebConnectionsPath             string\n\twebFoldersPath                 string\n\twebFolderPath                  string\n\twebGroupsPath                  string\n\twebGroupPath                   string\n\twebStatusPath                  string\n\twebAdminsPath                  string\n\twebAdminPath                   string\n\twebMaintenancePath             string\n\twebBackupPath                  string\n\twebRestorePath                 string\n\twebScanVFolderPath             string\n\twebQuotaScanPath               string\n\twebAdminProfilePath            string\n\twebAdminMFAPath                string\n\twebAdminEventRulesPath         string\n\twebAdminEventRulePath          string\n\twebAdminEventActionsPath       string\n\twebAdminEventActionPath        string\n\twebAdminRolesPath              string\n\twebAdminRolePath               string\n\twebAdminTOTPGeneratePath       string\n\twebAdminTOTPValidatePath       string\n\twebAdminTOTPSavePath           string\n\twebAdminRecoveryCodesPath      string\n\twebChangeAdminPwdPath          string\n\twebAdminForgotPwdPath          string\n\twebAdminResetPwdPath           string\n\twebTemplateUser                string\n\twebTemplateFolder              string\n\twebDefenderPath                string\n\twebIPListPath                  string\n\twebIPListsPath                 string\n\twebEventsPath                  string\n\twebEventsFsSearchPath          string\n\twebEventsProviderSearchPath    string\n\twebEventsLogSearchPath         string\n\twebConfigsPath                 string\n\twebDefenderHostsPath           string\n\twebClientLoginPath             string\n\twebClientOIDCLoginPath         string\n\twebClientTwoFactorPath         string\n\twebClientTwoFactorRecoveryPath string\n\twebClientFilesPath             string\n\twebClientFilePath              string\n\twebClientFileActionsPath       string\n\twebClientSharesPath            string\n\twebClientSharePath             string\n\twebClientEditFilePath          string\n\twebClientDirsPath              string\n\twebClientDownloadZipPath       string\n\twebClientProfilePath           string\n\twebClientPingPath              string\n\twebChangeClientPwdPath         string\n\twebClientMFAPath               string\n\twebClientTOTPGeneratePath      string\n\twebClientTOTPValidatePath      string\n\twebClientTOTPSavePath          string\n\twebClientRecoveryCodesPath     string\n\twebClientPubSharesPath         string\n\twebClientLogoutPath            string\n\twebClientForgotPwdPath         string\n\twebClientResetPwdPath          string\n\twebClientViewPDFPath           string\n\twebClientGetPDFPath            string\n\twebClientExistPath             string\n\twebClientTasksPath             string\n\twebStaticFilesPath             string\n\twebOpenAPIPath                 string\n\t// max upload size for http clients, 1GB by default\n\tmaxUploadFileSize          = int64(1048576000)\n\thideSupportLink            bool\n\tinstallationCode           string\n\tinstallationCodeHint       string\n\tfnInstallationCodeResolver FnInstallationCodeResolver\n\tconfigurationDir           string\n\tdbBrandingConfig           brandingCache\n)\n\nfunc init() {\n\tupdateWebAdminURLs(\"\")\n\tupdateWebClientURLs(\"\")\n\tacme.SetReloadHTTPDCertsFn(ReloadCertificateMgr)\n\tcommon.SetUpdateBrandingFn(dbBrandingConfig.Set)\n}\n\ntype brandingCache struct {\n\tmu      sync.RWMutex\n\tconfigs *dataprovider.BrandingConfigs\n}\n\nfunc (b *brandingCache) Set(configs *dataprovider.BrandingConfigs) {\n\tb.mu.Lock()\n\tdefer b.mu.Unlock()\n\n\tb.configs = configs\n}\n\nfunc (b *brandingCache) getWebAdminLogo() []byte {\n\tb.mu.RLock()\n\tdefer b.mu.RUnlock()\n\n\treturn b.configs.WebAdmin.Logo\n}\n\nfunc (b *brandingCache) getWebAdminFavicon() []byte {\n\tb.mu.RLock()\n\tdefer b.mu.RUnlock()\n\n\treturn b.configs.WebAdmin.Favicon\n}\n\nfunc (b *brandingCache) getWebClientLogo() []byte {\n\tb.mu.RLock()\n\tdefer b.mu.RUnlock()\n\n\treturn b.configs.WebClient.Logo\n}\n\nfunc (b *brandingCache) getWebClientFavicon() []byte {\n\tb.mu.RLock()\n\tdefer b.mu.RUnlock()\n\n\treturn b.configs.WebClient.Favicon\n}\n\nfunc (b *brandingCache) mergeBrandingConfig(branding UIBranding, isWebClient bool) UIBranding {\n\tb.mu.RLock()\n\tdefer b.mu.RUnlock()\n\n\tvar urlPrefix string\n\tvar cfg dataprovider.BrandingConfig\n\tif isWebClient {\n\t\tcfg = b.configs.WebClient\n\t\turlPrefix = \"webclient\"\n\t} else {\n\t\tcfg = b.configs.WebAdmin\n\t\turlPrefix = \"webadmin\"\n\t}\n\tif cfg.Name != \"\" {\n\t\tbranding.Name = cfg.Name\n\t}\n\tif cfg.ShortName != \"\" {\n\t\tbranding.ShortName = cfg.ShortName\n\t}\n\tif cfg.DisclaimerName != \"\" {\n\t\tbranding.DisclaimerName = cfg.DisclaimerName\n\t}\n\tif cfg.DisclaimerURL != \"\" {\n\t\tbranding.DisclaimerPath = cfg.DisclaimerURL\n\t}\n\tif len(cfg.Logo) > 0 {\n\t\tbranding.LogoPath = path.Join(\"/\", \"branding\", urlPrefix, \"logo.png\")\n\t}\n\tif len(cfg.Favicon) > 0 {\n\t\tbranding.FaviconPath = path.Join(\"/\", \"branding\", urlPrefix, \"favicon.png\")\n\t}\n\treturn branding\n}\n\n// FnInstallationCodeResolver defines a method to get the installation code.\n// If the installation code cannot be resolved the provided default must be returned\ntype FnInstallationCodeResolver func(defaultInstallationCode string) string\n\n// HTTPSProxyHeader defines an HTTPS proxy header as key/value.\n// For example Key could be \"X-Forwarded-Proto\" and Value \"https\"\ntype HTTPSProxyHeader struct {\n\tKey   string\n\tValue string\n}\n\n// SecurityConf allows to add some security related headers to HTTP responses and to restrict allowed hosts\ntype SecurityConf struct {\n\t// Set to true to enable the security configurations\n\tEnabled bool `json:\"enabled\" mapstructure:\"enabled\"`\n\t// AllowedHosts is a list of fully qualified domain names that are allowed.\n\t// Default is empty list, which allows any and all host names.\n\tAllowedHosts []string `json:\"allowed_hosts\" mapstructure:\"allowed_hosts\"`\n\t// AllowedHostsAreRegex determines if the provided allowed hosts contains valid regular expressions\n\tAllowedHostsAreRegex bool `json:\"allowed_hosts_are_regex\" mapstructure:\"allowed_hosts_are_regex\"`\n\t// HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request.\n\tHostsProxyHeaders []string `json:\"hosts_proxy_headers\" mapstructure:\"hosts_proxy_headers\"`\n\t// Set to true to redirect HTTP requests to HTTPS\n\tHTTPSRedirect bool `json:\"https_redirect\" mapstructure:\"https_redirect\"`\n\t// HTTPSHost defines the host name that is used to redirect HTTP requests to HTTPS.\n\t// Default is \"\", which indicates to use the same host.\n\tHTTPSHost string `json:\"https_host\" mapstructure:\"https_host\"`\n\t// HTTPSProxyHeaders is a list of header keys with associated values that would indicate a valid https request.\n\tHTTPSProxyHeaders []HTTPSProxyHeader `json:\"https_proxy_headers\" mapstructure:\"https_proxy_headers\"`\n\t// STSSeconds is the max-age of the Strict-Transport-Security header.\n\t// Default is 0, which would NOT include the header.\n\tSTSSeconds int64 `json:\"sts_seconds\" mapstructure:\"sts_seconds\"`\n\t// If STSIncludeSubdomains is set to true, the \"includeSubdomains\" will be appended to the\n\t// Strict-Transport-Security header. Default is false.\n\tSTSIncludeSubdomains bool `json:\"sts_include_subdomains\" mapstructure:\"sts_include_subdomains\"`\n\t// If STSPreload is set to true, the `preload` flag will be appended to the\n\t// Strict-Transport-Security header. Default is false.\n\tSTSPreload bool `json:\"sts_preload\" mapstructure:\"sts_preload\"`\n\t// If ContentTypeNosniff is true, adds the X-Content-Type-Options header with the value \"nosniff\". Default is false.\n\tContentTypeNosniff bool `json:\"content_type_nosniff\" mapstructure:\"content_type_nosniff\"`\n\t// ContentSecurityPolicy allows to set the Content-Security-Policy header value. Default is \"\".\n\tContentSecurityPolicy string `json:\"content_security_policy\" mapstructure:\"content_security_policy\"`\n\t// PermissionsPolicy allows to set the Permissions-Policy header value. Default is \"\".\n\tPermissionsPolicy string `json:\"permissions_policy\" mapstructure:\"permissions_policy\"`\n\t// CrossOriginOpenerPolicy allows to set the Cross-Origin-Opener-Policy header value. Default is \"\".\n\tCrossOriginOpenerPolicy string `json:\"cross_origin_opener_policy\" mapstructure:\"cross_origin_opener_policy\"`\n\t// CrossOriginResourcePolicy allows to set the Cross-Origin-Resource-Policy header value. Default is \"\".\n\tCrossOriginResourcePolicy string `json:\"cross_origin_resource_policy\" mapstructure:\"cross_origin_resource_policy\"`\n\t// CrossOriginEmbedderPolicy allows to set the Cross-Origin-Embedder-Policy header value. Default is \"\".\n\tCrossOriginEmbedderPolicy string `json:\"cross_origin_embedder_policy\" mapstructure:\"cross_origin_embedder_policy\"`\n\t// CacheControl allows to set the Cache-Control header value.\n\tCacheControl string `json:\"cache_control\" mapstructure:\"cache_control\"`\n\t// ReferrerPolicy allows to set the Referrer-Policy header values.\n\tReferrerPolicy string `json:\"referrer_policy\" mapstructure:\"referrer_policy\"`\n\tproxyHeaders   []string\n}\n\nfunc (s *SecurityConf) updateProxyHeaders() {\n\tif !s.Enabled {\n\t\ts.proxyHeaders = nil\n\t\treturn\n\t}\n\ts.proxyHeaders = s.HostsProxyHeaders\n\tfor _, httpsProxyHeader := range s.HTTPSProxyHeaders {\n\t\ts.proxyHeaders = append(s.proxyHeaders, httpsProxyHeader.Key)\n\t}\n}\n\nfunc (s *SecurityConf) getHTTPSProxyHeaders() map[string]string {\n\theaders := make(map[string]string)\n\tfor _, httpsProxyHeader := range s.HTTPSProxyHeaders {\n\t\theaders[httpsProxyHeader.Key] = httpsProxyHeader.Value\n\t}\n\treturn headers\n}\n\nfunc (s *SecurityConf) redirectHandler(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif !isTLS(r) && !strings.HasPrefix(r.RequestURI, acmeChallengeURI) {\n\t\t\turl := r.URL\n\t\t\turl.Scheme = \"https\"\n\t\t\tif s.HTTPSHost != \"\" {\n\t\t\t\turl.Host = s.HTTPSHost\n\t\t\t} else {\n\t\t\t\thost := r.Host\n\t\t\t\tfor _, header := range s.HostsProxyHeaders {\n\t\t\t\t\tif h := r.Header.Get(header); h != \"\" {\n\t\t\t\t\t\thost = h\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\turl.Host = host\n\t\t\t}\n\t\t\thttp.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\n// UIBranding defines the supported customizations for the web UIs\ntype UIBranding struct {\n\t// Name defines the text to show at the login page and as HTML title\n\tName string `json:\"name\" mapstructure:\"name\"`\n\t// ShortName defines the name to show next to the logo image\n\tShortName string `json:\"short_name\" mapstructure:\"short_name\"`\n\t// Path to your logo relative to \"static_files_path\".\n\t// For example, if you create a directory named \"branding\" inside the static dir and\n\t// put the \"mylogo.png\" file in it, you must set \"/branding/mylogo.png\" as logo path.\n\tLogoPath string `json:\"logo_path\" mapstructure:\"logo_path\"`\n\t// Path to your favicon relative to \"static_files_path\"\n\tFaviconPath string `json:\"favicon_path\" mapstructure:\"favicon_path\"`\n\t// DisclaimerName defines the name for the link to your optional disclaimer\n\tDisclaimerName string `json:\"disclaimer_name\" mapstructure:\"disclaimer_name\"`\n\t// Path to the HTML page for your disclaimer relative to \"static_files_path\"\n\t// or an absolute http/https URL.\n\tDisclaimerPath string `json:\"disclaimer_path\" mapstructure:\"disclaimer_path\"`\n\t// Path to custom CSS files, relative to \"static_files_path\", which replaces\n\t// the default CSS files\n\tDefaultCSS []string `json:\"default_css\" mapstructure:\"default_css\"`\n\t// Additional CSS file paths, relative to \"static_files_path\", to include\n\tExtraCSS           []string `json:\"extra_css\" mapstructure:\"extra_css\"`\n\tDefaultLogoPath    string   `json:\"-\" mapstructure:\"-\"`\n\tDefaultFaviconPath string   `json:\"-\" mapstructure:\"-\"`\n}\n\nfunc (b *UIBranding) check() {\n\tb.DefaultLogoPath = \"/img/logo.png\"\n\tb.DefaultFaviconPath = \"/favicon.png\"\n\tif b.LogoPath != \"\" {\n\t\tb.LogoPath = util.CleanPath(b.LogoPath)\n\t} else {\n\t\tb.LogoPath = b.DefaultLogoPath\n\t}\n\tif b.FaviconPath != \"\" {\n\t\tb.FaviconPath = util.CleanPath(b.FaviconPath)\n\t} else {\n\t\tb.FaviconPath = b.DefaultFaviconPath\n\t}\n\tif b.DisclaimerPath != \"\" {\n\t\tif !strings.HasPrefix(b.DisclaimerPath, \"https://\") && !strings.HasPrefix(b.DisclaimerPath, \"http://\") {\n\t\t\tb.DisclaimerPath = path.Join(webStaticFilesPath, util.CleanPath(b.DisclaimerPath))\n\t\t}\n\t}\n\tif len(b.DefaultCSS) > 0 {\n\t\tfor idx := range b.DefaultCSS {\n\t\t\tb.DefaultCSS[idx] = util.CleanPath(b.DefaultCSS[idx])\n\t\t}\n\t} else {\n\t\tb.DefaultCSS = []string{\n\t\t\t\"/assets/plugins/global/plugins.bundle.css\",\n\t\t\t\"/assets/css/style.bundle.css\",\n\t\t}\n\t}\n\tfor idx := range b.ExtraCSS {\n\t\tb.ExtraCSS[idx] = util.CleanPath(b.ExtraCSS[idx])\n\t}\n}\n\n// Branding defines the branding-related customizations supported\ntype Branding struct {\n\tWebAdmin  UIBranding `json:\"web_admin\" mapstructure:\"web_admin\"`\n\tWebClient UIBranding `json:\"web_client\" mapstructure:\"web_client\"`\n}\n\n// WebClientIntegration defines the configuration for an external Web Client integration\ntype WebClientIntegration struct {\n\t// Files with these extensions can be sent to the configured URL\n\tFileExtensions []string `json:\"file_extensions\" mapstructure:\"file_extensions\"`\n\t// URL that will receive the files\n\tURL string `json:\"url\" mapstructure:\"url\"`\n}\n\n// Binding defines the configuration for a network listener\ntype Binding struct {\n\t// The address to listen on. A blank value means listen on all available network interfaces.\n\tAddress string `json:\"address\" mapstructure:\"address\"`\n\t// The port used for serving requests\n\tPort int `json:\"port\" mapstructure:\"port\"`\n\t// Enable the built-in admin interface.\n\t// You have to define TemplatesPath and StaticFilesPath for this to work\n\tEnableWebAdmin bool `json:\"enable_web_admin\" mapstructure:\"enable_web_admin\"`\n\t// Enable the built-in client interface.\n\t// You have to define TemplatesPath and StaticFilesPath for this to work\n\tEnableWebClient bool `json:\"enable_web_client\" mapstructure:\"enable_web_client\"`\n\t// Enable REST API\n\tEnableRESTAPI bool `json:\"enable_rest_api\" mapstructure:\"enable_rest_api\"`\n\t// Defines the login methods available for the WebAdmin and WebClient UIs:\n\t//\n\t// - 0 means any configured method: username/password login form and OIDC, if enabled\n\t// - 1 means OIDC for the WebAdmin UI\n\t// - 2 means OIDC for the WebClient UI\n\t// - 4 means login form for the WebAdmin UI\n\t// - 8 means login form for the WebClient UI\n\t//\n\t// You can combine the values. For example 3 means that you can only login using OIDC on\n\t// both WebClient and WebAdmin UI.\n\t// Deprecated because it is not extensible, use DisabledLoginMethods\n\tEnabledLoginMethods int `json:\"enabled_login_methods\" mapstructure:\"enabled_login_methods\"`\n\t// Defines the login methods disabled for the WebAdmin and WebClient UIs:\n\t//\n\t// - 1 means OIDC for the WebAdmin UI\n\t// - 2 means OIDC for the WebClient UI\n\t// - 4 means login form for the WebAdmin UI\n\t// - 8 means login form for the WebClient UI\n\t// - 16 means basic auth for admin REST API\n\t// - 32 means basic auth for user REST API\n\t// - 64 means API key auth for admins\n\t// - 128 means API key auth for users\n\t// You can combine the values. For example 12 means that you can only login using OIDC on\n\t// both WebClient and WebAdmin UI.\n\tDisabledLoginMethods int `json:\"disabled_login_methods\" mapstructure:\"disabled_login_methods\"`\n\t// you also need to provide a certificate for enabling HTTPS\n\tEnableHTTPS bool `json:\"enable_https\" mapstructure:\"enable_https\"`\n\t// Certificate and matching private key for this specific binding, if empty the global\n\t// ones will be used, if any\n\tCertificateFile    string `json:\"certificate_file\" mapstructure:\"certificate_file\"`\n\tCertificateKeyFile string `json:\"certificate_key_file\" mapstructure:\"certificate_key_file\"`\n\t// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2\n\tMinTLSVersion int `json:\"min_tls_version\" mapstructure:\"min_tls_version\"`\n\t// set to 1 to require client certificate authentication in addition to basic auth.\n\t// You need to define at least a certificate authority for this to work\n\tClientAuthType int `json:\"client_auth_type\" mapstructure:\"client_auth_type\"`\n\t// TLSCipherSuites is a list of supported cipher suites for TLS version 1.2.\n\t// If CipherSuites is nil/empty, a default list of secure cipher suites\n\t// is used, with a preference order based on hardware performance.\n\t// Note that TLS 1.3 ciphersuites are not configurable.\n\t// The supported ciphersuites names are defined here:\n\t//\n\t// https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53\n\t//\n\t// any invalid name will be silently ignored.\n\t// The order matters, the ciphers listed first will be the preferred ones.\n\tTLSCipherSuites []string `json:\"tls_cipher_suites\" mapstructure:\"tls_cipher_suites\"`\n\t// HTTP protocols in preference order. Supported values: http/1.1, h2\n\tProtocols []string `json:\"tls_protocols\" mapstructure:\"tls_protocols\"`\n\t// Defines whether to use the common proxy protocol configuration or the\n\t// binding-specific proxy header configuration.\n\tProxyMode int `json:\"proxy_mode\" mapstructure:\"proxy_mode\"`\n\t// List of IP addresses and IP ranges allowed to set client IP proxy headers and\n\t// X-Forwarded-Proto header.\n\tProxyAllowed []string `json:\"proxy_allowed\" mapstructure:\"proxy_allowed\"`\n\t// Allowed client IP proxy header such as \"X-Forwarded-For\", \"X-Real-IP\"\n\tClientIPProxyHeader string `json:\"client_ip_proxy_header\" mapstructure:\"client_ip_proxy_header\"`\n\t// Some client IP headers such as \"X-Forwarded-For\" can contain multiple IP address, this setting\n\t// define the position to trust starting from the right. For example if we have:\n\t// \"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1\" and the depth is 0, SFTPGo will use \"13.0.0.1\"\n\t// as client IP, if depth is 1, \"12.0.0.1\" will be used and so on\n\tClientIPHeaderDepth int `json:\"client_ip_header_depth\" mapstructure:\"client_ip_header_depth\"`\n\t// If both web admin and web client are enabled each login page will show a link\n\t// to the other one. This setting allows to hide this link:\n\t// - 0 login links are displayed on both admin and client login page. This is the default\n\t// - 1 the login link to the web client login page is hidden on admin login page\n\t// - 2 the login link to the web admin login page is hidden on client login page\n\t// The flags can be combined, for example 3 will disable both login links.\n\tHideLoginURL int `json:\"hide_login_url\" mapstructure:\"hide_login_url\"`\n\t// Enable the built-in OpenAPI renderer\n\tRenderOpenAPI bool `json:\"render_openapi\" mapstructure:\"render_openapi\"`\n\t// BaseURL defines the external base URL for generating public links\n\t// (currently share access link), bypassing the default browser-based\n\t// detection.\n\tBaseURL string `json:\"base_url\" mapstructure:\"base_url\"`\n\t// Languages defines the list of enabled translations for the WebAdmin and WebClient UI.\n\tLanguages []string `json:\"languages\" mapstructure:\"languages\"`\n\t// Defining an OIDC configuration the web admin and web client UI will use OpenID to authenticate users.\n\tOIDC OIDC `json:\"oidc\" mapstructure:\"oidc\"`\n\t// Security defines security headers to add to HTTP responses and allows to restrict allowed hosts\n\tSecurity SecurityConf `json:\"security\" mapstructure:\"security\"`\n\t// Branding defines customizations to suit your brand\n\tBranding         Branding `json:\"branding\" mapstructure:\"branding\"`\n\tallowHeadersFrom []func(net.IP) bool\n}\n\nfunc (b *Binding) checkBranding() {\n\tb.Branding.WebAdmin.check()\n\tb.Branding.WebClient.check()\n\tif b.Branding.WebAdmin.Name == \"\" {\n\t\tb.Branding.WebAdmin.Name = \"SFTPGo WebAdmin\"\n\t}\n\tif b.Branding.WebAdmin.ShortName == \"\" {\n\t\tb.Branding.WebAdmin.ShortName = \"WebAdmin\"\n\t}\n\tif b.Branding.WebClient.Name == \"\" {\n\t\tb.Branding.WebClient.Name = \"SFTPGo WebClient\"\n\t}\n\tif b.Branding.WebClient.ShortName == \"\" {\n\t\tb.Branding.WebClient.ShortName = \"WebClient\"\n\t}\n}\n\nfunc (b *Binding) webAdminBranding() UIBranding {\n\treturn dbBrandingConfig.mergeBrandingConfig(b.Branding.WebAdmin, false)\n}\n\nfunc (b *Binding) webClientBranding() UIBranding {\n\treturn dbBrandingConfig.mergeBrandingConfig(b.Branding.WebClient, true)\n}\n\nfunc (b *Binding) languages() []string {\n\treturn b.Languages\n}\n\nfunc (b *Binding) validateBaseURL() error {\n\tif b.BaseURL == \"\" {\n\t\treturn nil\n\t}\n\tu, err := url.ParseRequestURI(b.BaseURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn fmt.Errorf(\"invalid base URL schema %s\", b.BaseURL)\n\t}\n\tif u.Host == \"\" {\n\t\treturn fmt.Errorf(\"invalid base URL host %s\", b.BaseURL)\n\t}\n\tb.BaseURL = strings.TrimRight(u.String(), \"/\")\n\treturn nil\n}\n\nfunc (b *Binding) parseAllowedProxy() error {\n\tif filepath.IsAbs(b.Address) && len(b.ProxyAllowed) > 0 {\n\t\t// unix domain socket\n\t\tb.allowHeadersFrom = []func(net.IP) bool{func(_ net.IP) bool { return true }}\n\t\treturn nil\n\t}\n\tallowedFuncs, err := util.ParseAllowedIPAndRanges(b.ProxyAllowed)\n\tif err != nil {\n\t\treturn err\n\t}\n\tb.allowHeadersFrom = allowedFuncs\n\treturn nil\n}\n\n// GetAddress returns the binding address\nfunc (b *Binding) GetAddress() string {\n\treturn fmt.Sprintf(\"%s:%d\", b.Address, b.Port)\n}\n\n// IsValid returns true if the binding is valid\nfunc (b *Binding) IsValid() bool {\n\tif !b.EnableRESTAPI && !b.EnableWebAdmin && !b.EnableWebClient {\n\t\treturn false\n\t}\n\tif b.Port > 0 {\n\t\treturn true\n\t}\n\tif filepath.IsAbs(b.Address) && runtime.GOOS != osWindows {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (b *Binding) check() error {\n\tif err := b.parseAllowedProxy(); err != nil {\n\t\treturn err\n\t}\n\tif err := b.validateBaseURL(); err != nil {\n\t\treturn err\n\t}\n\tb.checkBranding()\n\tb.Security.updateProxyHeaders()\n\treturn nil\n}\n\nfunc (b *Binding) isWebAdminOIDCLoginDisabled() bool {\n\tif b.EnableWebAdmin {\n\t\treturn b.DisabledLoginMethods&1 != 0\n\t}\n\treturn false\n}\n\nfunc (b *Binding) isWebClientOIDCLoginDisabled() bool {\n\tif b.EnableWebClient {\n\t\treturn b.DisabledLoginMethods&2 != 0\n\t}\n\treturn false\n}\n\nfunc (b *Binding) isWebAdminLoginFormDisabled() bool {\n\tif b.EnableWebAdmin {\n\t\treturn b.DisabledLoginMethods&4 != 0\n\t}\n\treturn false\n}\n\nfunc (b *Binding) isWebClientLoginFormDisabled() bool {\n\tif b.EnableWebClient {\n\t\treturn b.DisabledLoginMethods&8 != 0\n\t}\n\treturn false\n}\n\nfunc (b *Binding) isAdminTokenEndpointDisabled() bool {\n\treturn b.DisabledLoginMethods&16 != 0\n}\n\nfunc (b *Binding) isUserTokenEndpointDisabled() bool {\n\treturn b.DisabledLoginMethods&32 != 0\n}\n\nfunc (b *Binding) isAdminAPIKeyAuthDisabled() bool {\n\treturn b.DisabledLoginMethods&64 != 0\n}\n\nfunc (b *Binding) isUserAPIKeyAuthDisabled() bool {\n\treturn b.DisabledLoginMethods&128 != 0\n}\n\nfunc (b *Binding) hasLoginForAPI() bool {\n\treturn !b.isAdminTokenEndpointDisabled() || !b.isUserTokenEndpointDisabled() ||\n\t\t!b.isAdminAPIKeyAuthDisabled() || !b.isUserAPIKeyAuthDisabled()\n}\n\n// convertLoginMethods checks if the deprecated EnabledLoginMethods is set and\n// convert the value to DisabledLoginMethods.\nfunc (b *Binding) convertLoginMethods() {\n\tif b.DisabledLoginMethods > 0 || b.EnabledLoginMethods == 0 {\n\t\t// DisabledLoginMethods already in use or EnabledLoginMethods not set.\n\t\treturn\n\t}\n\tif b.EnabledLoginMethods&1 == 0 {\n\t\tb.DisabledLoginMethods++\n\t}\n\tif b.EnabledLoginMethods&2 == 0 {\n\t\tb.DisabledLoginMethods += 2\n\t}\n\tif b.EnabledLoginMethods&4 == 0 {\n\t\tb.DisabledLoginMethods += 4\n\t}\n\tif b.EnabledLoginMethods&8 == 0 {\n\t\tb.DisabledLoginMethods += 8\n\t}\n}\n\nfunc (b *Binding) checkLoginMethods() error {\n\tb.convertLoginMethods()\n\tif b.isWebAdminLoginFormDisabled() && b.isWebAdminOIDCLoginDisabled() {\n\t\treturn errors.New(\"no login method available for WebAdmin UI\")\n\t}\n\tif !b.isWebAdminOIDCLoginDisabled() {\n\t\tif b.isWebAdminLoginFormDisabled() && !b.OIDC.hasRoles() {\n\t\t\treturn errors.New(\"no login method available for WebAdmin UI\")\n\t\t}\n\t}\n\tif b.isWebClientLoginFormDisabled() && b.isWebClientOIDCLoginDisabled() {\n\t\treturn errors.New(\"no login method available for WebClient UI\")\n\t}\n\tif !b.isWebClientOIDCLoginDisabled() {\n\t\tif b.isWebClientLoginFormDisabled() && !b.OIDC.isEnabled() {\n\t\t\treturn errors.New(\"no login method available for WebClient UI\")\n\t\t}\n\t}\n\tif b.EnableRESTAPI && !b.hasLoginForAPI() {\n\t\treturn errors.New(\"no login method available for REST API\")\n\t}\n\treturn nil\n}\n\nfunc (b *Binding) showAdminLoginURL() bool {\n\tif !b.EnableWebAdmin {\n\t\treturn false\n\t}\n\tif b.HideLoginURL&2 != 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (b *Binding) showClientLoginURL() bool {\n\tif !b.EnableWebClient {\n\t\treturn false\n\t}\n\tif b.HideLoginURL&1 != 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (b *Binding) isMutualTLSEnabled() bool {\n\treturn b.ClientAuthType == 1\n}\n\nfunc (b *Binding) listenerWrapper() func(net.Listener) (net.Listener, error) {\n\tif b.ProxyMode == 1 {\n\t\treturn common.Config.GetProxyListener\n\t}\n\treturn nil\n}\n\ntype defenderStatus struct {\n\tIsActive bool `json:\"is_active\"`\n}\n\ntype allowListStatus struct {\n\tIsActive bool `json:\"is_active\"`\n}\n\ntype rateLimiters struct {\n\tIsActive  bool     `json:\"is_active\"`\n\tProtocols []string `json:\"protocols\"`\n}\n\n// GetProtocolsAsString returns the enabled protocols as comma separated string\nfunc (r *rateLimiters) GetProtocolsAsString() string {\n\treturn strings.Join(r.Protocols, \", \")\n}\n\n// ServicesStatus keep the state of the running services\ntype ServicesStatus struct {\n\tSSH          sftpd.ServiceStatus         `json:\"ssh\"`\n\tFTP          ftpd.ServiceStatus          `json:\"ftp\"`\n\tWebDAV       webdavd.ServiceStatus       `json:\"webdav\"`\n\tDataProvider dataprovider.ProviderStatus `json:\"data_provider\"`\n\tDefender     defenderStatus              `json:\"defender\"`\n\tMFA          mfa.ServiceStatus           `json:\"mfa\"`\n\tAllowList    allowListStatus             `json:\"allow_list\"`\n\tRateLimiters rateLimiters                `json:\"rate_limiters\"`\n}\n\n// SetupConfig defines the configuration parameters for the initial web admin setup\ntype SetupConfig struct {\n\t// Installation code to require when creating the first admin account.\n\t// As for the other configurations, this value is read at SFTPGo startup and not at runtime\n\t// even if set using an environment variable.\n\t// This is not a license key or similar, the purpose here is to prevent anyone who can access\n\t// to the initial setup screen from creating an admin user\n\tInstallationCode string `json:\"installation_code\" mapstructure:\"installation_code\"`\n\t// Description for the installation code input field\n\tInstallationCodeHint string `json:\"installation_code_hint\" mapstructure:\"installation_code_hint\"`\n}\n\n// CorsConfig defines the CORS configuration\ntype CorsConfig struct {\n\tAllowedOrigins       []string `json:\"allowed_origins\" mapstructure:\"allowed_origins\"`\n\tAllowedMethods       []string `json:\"allowed_methods\" mapstructure:\"allowed_methods\"`\n\tAllowedHeaders       []string `json:\"allowed_headers\" mapstructure:\"allowed_headers\"`\n\tExposedHeaders       []string `json:\"exposed_headers\" mapstructure:\"exposed_headers\"`\n\tAllowCredentials     bool     `json:\"allow_credentials\" mapstructure:\"allow_credentials\"`\n\tEnabled              bool     `json:\"enabled\" mapstructure:\"enabled\"`\n\tMaxAge               int      `json:\"max_age\" mapstructure:\"max_age\"`\n\tOptionsPassthrough   bool     `json:\"options_passthrough\" mapstructure:\"options_passthrough\"`\n\tOptionsSuccessStatus int      `json:\"options_success_status\" mapstructure:\"options_success_status\"`\n\tAllowPrivateNetwork  bool     `json:\"allow_private_network\" mapstructure:\"allow_private_network\"`\n}\n\n// Conf httpd daemon configuration\ntype Conf struct {\n\t// Addresses and ports to bind to\n\tBindings []Binding `json:\"bindings\" mapstructure:\"bindings\"`\n\t// Path to the HTML web templates. This can be an absolute path or a path relative to the config dir\n\tTemplatesPath string `json:\"templates_path\" mapstructure:\"templates_path\"`\n\t// Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir.\n\t// If both TemplatesPath and StaticFilesPath are empty the built-in web interface will be disabled\n\tStaticFilesPath string `json:\"static_files_path\" mapstructure:\"static_files_path\"`\n\t// Path to the backup directory. This can be an absolute path or a path relative to the config dir\n\t//BackupsPath string `json:\"backups_path\" mapstructure:\"backups_path\"`\n\t// Path to the directory that contains the OpenAPI schema and the default renderer.\n\t// This can be an absolute path or a path relative to the config dir\n\tOpenAPIPath string `json:\"openapi_path\" mapstructure:\"openapi_path\"`\n\t// Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will\n\t// be available at the root (\"/\") URI. If defined it must be an absolute URI or it will be ignored.\n\tWebRoot string `json:\"web_root\" mapstructure:\"web_root\"`\n\t// If files containing a certificate and matching private key for the server are provided you can enable\n\t// HTTPS connections for the configured bindings.\n\t// Certificate and key files can be reloaded on demand sending a \"SIGHUP\" signal on Unix based systems and a\n\t// \"paramchange\" request to the running service on Windows.\n\tCertificateFile    string `json:\"certificate_file\" mapstructure:\"certificate_file\"`\n\tCertificateKeyFile string `json:\"certificate_key_file\" mapstructure:\"certificate_key_file\"`\n\t// CACertificates defines the set of root certificate authorities to be used to verify client certificates.\n\tCACertificates []string `json:\"ca_certificates\" mapstructure:\"ca_certificates\"`\n\t// CARevocationLists defines a set a revocation lists, one for each root CA, to be used to check\n\t// if a client certificate has been revoked\n\tCARevocationLists []string `json:\"ca_revocation_lists\" mapstructure:\"ca_revocation_lists\"`\n\t// SigningPassphrase defines the passphrase to use to derive the signing key for JWT and CSRF tokens.\n\t// If empty a random signing key will be generated each time SFTPGo starts. If you set a\n\t// signing passphrase you should consider rotating it periodically for added security\n\tSigningPassphrase     string `json:\"signing_passphrase\" mapstructure:\"signing_passphrase\"`\n\tSigningPassphraseFile string `json:\"signing_passphrase_file\" mapstructure:\"signing_passphrase_file\"`\n\t// TokenValidation allows to define how to validate JWT tokens, cookies and CSRF tokens.\n\t// By default all the available security checks are enabled. Set to 1 to disable the requirement\n\t// that a token must be used by the same IP for which it was issued.\n\tTokenValidation int `json:\"token_validation\" mapstructure:\"token_validation\"`\n\t// CookieLifetime defines the duration of cookies for WebAdmin and WebClient\n\tCookieLifetime int `json:\"cookie_lifetime\" mapstructure:\"cookie_lifetime\"`\n\t// ShareCookieLifetime defines the duration of cookies for public shares\n\tShareCookieLifetime int `json:\"share_cookie_lifetime\" mapstructure:\"share_cookie_lifetime\"`\n\t// JWTLifetime defines the duration of JWT tokens used in REST API\n\tJWTLifetime int `json:\"jwt_lifetime\" mapstructure:\"jwt_lifetime\"`\n\t// MaxUploadFileSize Defines the maximum request body size, in bytes, for Web Client/API HTTP upload requests.\n\t// 0 means no limit\n\tMaxUploadFileSize int64 `json:\"max_upload_file_size\" mapstructure:\"max_upload_file_size\"`\n\t// CORS configuration\n\tCors CorsConfig `json:\"cors\" mapstructure:\"cors\"`\n\t// Initial setup configuration\n\tSetup SetupConfig `json:\"setup\" mapstructure:\"setup\"`\n\t// If enabled, the link to the sponsors section will not appear on the setup screen page\n\tHideSupportLink bool `json:\"hide_support_link\" mapstructure:\"hide_support_link\"`\n\tacmeDomain      string\n}\n\ntype apiResponse struct {\n\tError   string `json:\"error,omitempty\"`\n\tMessage string `json:\"message\"`\n}\n\n// ShouldBind returns true if there is at least a valid binding\nfunc (c *Conf) ShouldBind() bool {\n\tfor _, binding := range c.Bindings {\n\t\tif binding.IsValid() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *Conf) isWebAdminEnabled() bool {\n\tfor _, binding := range c.Bindings {\n\t\tif binding.EnableWebAdmin {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (c *Conf) isWebClientEnabled() bool {\n\tfor _, binding := range c.Bindings {\n\t\tif binding.EnableWebClient {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (c *Conf) checkRequiredDirs(staticFilesPath, templatesPath string) error {\n\tif (c.isWebAdminEnabled() || c.isWebClientEnabled()) && (staticFilesPath == \"\" || templatesPath == \"\") {\n\t\treturn fmt.Errorf(\"required directory is invalid, static file path: %q template path: %q\",\n\t\t\tstaticFilesPath, templatesPath)\n\t}\n\treturn nil\n}\n\nfunc (c *Conf) getRedacted() Conf {\n\tredacted := \"[redacted]\"\n\tconf := *c\n\tif conf.SigningPassphrase != \"\" {\n\t\tconf.SigningPassphrase = redacted\n\t}\n\tif conf.Setup.InstallationCode != \"\" {\n\t\tconf.Setup.InstallationCode = redacted\n\t}\n\tconf.Bindings = nil\n\tfor _, binding := range c.Bindings {\n\t\tif binding.OIDC.ClientID != \"\" {\n\t\t\tbinding.OIDC.ClientID = redacted\n\t\t}\n\t\tif binding.OIDC.ClientSecret != \"\" {\n\t\t\tbinding.OIDC.ClientSecret = redacted\n\t\t}\n\t\tconf.Bindings = append(conf.Bindings, binding)\n\t}\n\treturn conf\n}\n\nfunc (c *Conf) getKeyPairs(configDir string) []common.TLSKeyPair {\n\tvar keyPairs []common.TLSKeyPair\n\n\tfor _, binding := range c.Bindings {\n\t\tcertificateFile := getConfigPath(binding.CertificateFile, configDir)\n\t\tcertificateKeyFile := getConfigPath(binding.CertificateKeyFile, configDir)\n\t\tif certificateFile != \"\" && certificateKeyFile != \"\" {\n\t\t\tkeyPairs = append(keyPairs, common.TLSKeyPair{\n\t\t\t\tCert: certificateFile,\n\t\t\t\tKey:  certificateKeyFile,\n\t\t\t\tID:   binding.GetAddress(),\n\t\t\t})\n\t\t}\n\t}\n\tvar certificateFile, certificateKeyFile string\n\tif c.acmeDomain != \"\" {\n\t\tcertificateFile, certificateKeyFile = util.GetACMECertificateKeyPair(c.acmeDomain)\n\t} else {\n\t\tcertificateFile = getConfigPath(c.CertificateFile, configDir)\n\t\tcertificateKeyFile = getConfigPath(c.CertificateKeyFile, configDir)\n\t}\n\tif certificateFile != \"\" && certificateKeyFile != \"\" {\n\t\tkeyPairs = append(keyPairs, common.TLSKeyPair{\n\t\t\tCert: certificateFile,\n\t\t\tKey:  certificateKeyFile,\n\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t})\n\t}\n\treturn keyPairs\n}\n\nfunc (c *Conf) setTokenValidationMode() {\n\ttokenValidationMode = c.TokenValidation\n}\n\nfunc (c *Conf) loadFromProvider() error {\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to load config from provider: %w\", err)\n\t}\n\tconfigs.SetNilsToEmpty()\n\tdbBrandingConfig.Set(configs.Branding)\n\tif configs.ACME.Domain == \"\" || !configs.ACME.HasProtocol(common.ProtocolHTTP) {\n\t\treturn nil\n\t}\n\tcrt, key := util.GetACMECertificateKeyPair(configs.ACME.Domain)\n\tif crt != \"\" && key != \"\" {\n\t\tif _, err := os.Stat(crt); err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to load acme cert file %q: %v\", crt, err)\n\t\t\treturn nil\n\t\t}\n\t\tif _, err := os.Stat(key); err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to load acme key file %q: %v\", key, err)\n\t\t\treturn nil\n\t\t}\n\t\tfor idx := range c.Bindings {\n\t\t\tif c.Bindings[idx].Security.Enabled && c.Bindings[idx].Security.HTTPSRedirect {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tc.Bindings[idx].EnableHTTPS = true\n\t\t}\n\t\tc.acmeDomain = configs.ACME.Domain\n\t\tlogger.Info(logSender, \"\", \"acme domain set to %q\", c.acmeDomain)\n\t\treturn nil\n\t}\n\treturn nil\n}\n\nfunc (c *Conf) loadTemplates(templatesPath string) {\n\tif c.isWebAdminEnabled() {\n\t\tupdateWebAdminURLs(c.WebRoot)\n\t\tloadAdminTemplates(templatesPath)\n\t} else {\n\t\tlogger.Info(logSender, \"\", \"built-in web admin interface disabled\")\n\t}\n\tif c.isWebClientEnabled() {\n\t\tupdateWebClientURLs(c.WebRoot)\n\t\tloadClientTemplates(templatesPath)\n\t} else {\n\t\tlogger.Info(logSender, \"\", \"built-in web client interface disabled\")\n\t}\n}\n\n// Initialize configures and starts the HTTP server\nfunc (c *Conf) Initialize(configDir string, isShared int) error {\n\tif err := c.loadFromProvider(); err != nil {\n\t\treturn err\n\t}\n\tlogger.Info(logSender, \"\", \"initializing HTTP server with config %+v\", c.getRedacted())\n\tconfigurationDir = configDir\n\tinvalidatedJWTTokens = newTokenManager(isShared)\n\tresetCodesMgr = newResetCodeManager(isShared)\n\toidcMgr = newOIDCManager(isShared)\n\toauth2Mgr = newOAuth2Manager(isShared)\n\twebTaskMgr = newWebTaskManager(isShared)\n\tstaticFilesPath := util.FindSharedDataPath(c.StaticFilesPath, configDir)\n\ttemplatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir)\n\topenAPIPath := util.FindSharedDataPath(c.OpenAPIPath, configDir)\n\tif err := c.checkRequiredDirs(staticFilesPath, templatesPath); err != nil {\n\t\treturn err\n\t}\n\tc.loadTemplates(templatesPath)\n\tkeyPairs := c.getKeyPairs(configDir)\n\tif len(keyPairs) > 0 {\n\t\tmgr, err := common.NewCertManager(keyPairs, configDir, logSender)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmgr.SetCACertificates(c.CACertificates)\n\t\tif err := mgr.LoadRootCAs(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmgr.SetCARevocationLists(c.CARevocationLists)\n\t\tif err := mgr.LoadCRLs(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcertMgr = mgr\n\t}\n\n\tif c.SigningPassphraseFile != \"\" {\n\t\tpassphrase, err := util.ReadConfigFromFile(c.SigningPassphraseFile, configDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.SigningPassphrase = passphrase\n\t}\n\n\thideSupportLink = c.HideSupportLink\n\n\texitChannel := make(chan error, 1)\n\n\tfor _, binding := range c.Bindings {\n\t\tif !binding.IsValid() {\n\t\t\tcontinue\n\t\t}\n\t\tif err := binding.check(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tgo func(b Binding) {\n\t\t\tif err := b.OIDC.initialize(); err != nil {\n\t\t\t\texitChannel <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := b.checkLoginMethods(); err != nil {\n\t\t\t\texitChannel <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tserver := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors, openAPIPath)\n\t\t\tserver.setShared(isShared)\n\n\t\t\texitChannel <- server.listenAndServe()\n\t\t}(binding)\n\t}\n\n\tmaxUploadFileSize = c.MaxUploadFileSize\n\tinstallationCode = c.Setup.InstallationCode\n\tinstallationCodeHint = c.Setup.InstallationCodeHint\n\tupdateTokensDuration(c.JWTLifetime, c.CookieLifetime, c.ShareCookieLifetime)\n\tstartCleanupTicker(10 * time.Minute)\n\tc.setTokenValidationMode()\n\treturn <-exitChannel\n}\n\nfunc isWebRequest(r *http.Request) bool {\n\treturn strings.HasPrefix(r.RequestURI, webBasePath+\"/\")\n}\n\nfunc isWebClientRequest(r *http.Request) bool {\n\treturn strings.HasPrefix(r.RequestURI, webBaseClientPath+\"/\")\n}\n\n// ReloadCertificateMgr reloads the certificate manager\nfunc ReloadCertificateMgr() error {\n\tif certMgr != nil {\n\t\treturn certMgr.Reload()\n\t}\n\treturn nil\n}\n\nfunc getConfigPath(name, configDir string) string {\n\tif !util.IsFileInputValid(name) {\n\t\treturn \"\"\n\t}\n\tif name != \"\" && !filepath.IsAbs(name) {\n\t\treturn filepath.Join(configDir, name)\n\t}\n\treturn name\n}\n\nfunc getServicesStatus() *ServicesStatus {\n\trtlEnabled, rtlProtocols := common.Config.GetRateLimitersStatus()\n\tstatus := &ServicesStatus{\n\t\tSSH:          sftpd.GetStatus(),\n\t\tFTP:          ftpd.GetStatus(),\n\t\tWebDAV:       webdavd.GetStatus(),\n\t\tDataProvider: dataprovider.GetProviderStatus(),\n\t\tDefender: defenderStatus{\n\t\t\tIsActive: common.Config.DefenderConfig.Enabled,\n\t\t},\n\t\tMFA: mfa.GetStatus(),\n\t\tAllowList: allowListStatus{\n\t\t\tIsActive: common.Config.IsAllowListEnabled(),\n\t\t},\n\t\tRateLimiters: rateLimiters{\n\t\t\tIsActive:  rtlEnabled,\n\t\t\tProtocols: rtlProtocols,\n\t\t},\n\t}\n\treturn status\n}\n\nfunc fileServer(r chi.Router, path string, root http.FileSystem, disableDirectoryIndex bool) {\n\tif path != \"/\" && path[len(path)-1] != '/' {\n\t\tr.Get(path, http.RedirectHandler(path+\"/\", http.StatusMovedPermanently).ServeHTTP)\n\t\tpath += \"/\"\n\t}\n\tpath += \"*\"\n\n\tr.Get(path, func(w http.ResponseWriter, r *http.Request) {\n\t\trctx := chi.RouteContext(r.Context())\n\t\tpathPrefix := strings.TrimSuffix(rctx.RoutePattern(), \"/*\")\n\t\tif disableDirectoryIndex {\n\t\t\troot = neuteredFileSystem{root}\n\t\t}\n\t\tfs := http.StripPrefix(pathPrefix, http.FileServer(root))\n\t\tfs.ServeHTTP(w, r)\n\t})\n}\n\nfunc updateWebClientURLs(baseURL string) {\n\tif !path.IsAbs(baseURL) {\n\t\tbaseURL = \"/\"\n\t}\n\twebRootPath = path.Join(baseURL, webRootPathDefault)\n\twebBasePath = path.Join(baseURL, webBasePathDefault)\n\twebBaseClientPath = path.Join(baseURL, webBasePathClientDefault)\n\twebOIDCRedirectPath = path.Join(baseURL, webOIDCRedirectPathDefault)\n\twebClientLoginPath = path.Join(baseURL, webClientLoginPathDefault)\n\twebClientOIDCLoginPath = path.Join(baseURL, webClientOIDCLoginPathDefault)\n\twebClientTwoFactorPath = path.Join(baseURL, webClientTwoFactorPathDefault)\n\twebClientTwoFactorRecoveryPath = path.Join(baseURL, webClientTwoFactorRecoveryPathDefault)\n\twebClientFilesPath = path.Join(baseURL, webClientFilesPathDefault)\n\twebClientFilePath = path.Join(baseURL, webClientFilePathDefault)\n\twebClientFileActionsPath = path.Join(baseURL, webClientFileActionsPathDefault)\n\twebClientSharesPath = path.Join(baseURL, webClientSharesPathDefault)\n\twebClientPubSharesPath = path.Join(baseURL, webClientPubSharesPathDefault)\n\twebClientSharePath = path.Join(baseURL, webClientSharePathDefault)\n\twebClientEditFilePath = path.Join(baseURL, webClientEditFilePathDefault)\n\twebClientDirsPath = path.Join(baseURL, webClientDirsPathDefault)\n\twebClientDownloadZipPath = path.Join(baseURL, webClientDownloadZipPathDefault)\n\twebClientProfilePath = path.Join(baseURL, webClientProfilePathDefault)\n\twebClientPingPath = path.Join(baseURL, webClientPingPathDefault)\n\twebChangeClientPwdPath = path.Join(baseURL, webChangeClientPwdPathDefault)\n\twebClientLogoutPath = path.Join(baseURL, webClientLogoutPathDefault)\n\twebClientMFAPath = path.Join(baseURL, webClientMFAPathDefault)\n\twebClientTOTPGeneratePath = path.Join(baseURL, webClientTOTPGeneratePathDefault)\n\twebClientTOTPValidatePath = path.Join(baseURL, webClientTOTPValidatePathDefault)\n\twebClientTOTPSavePath = path.Join(baseURL, webClientTOTPSavePathDefault)\n\twebClientRecoveryCodesPath = path.Join(baseURL, webClientRecoveryCodesPathDefault)\n\twebClientForgotPwdPath = path.Join(baseURL, webClientForgotPwdPathDefault)\n\twebClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault)\n\twebClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault)\n\twebClientGetPDFPath = path.Join(baseURL, webClientGetPDFPathDefault)\n\twebClientExistPath = path.Join(baseURL, webClientExistPathDefault)\n\twebClientTasksPath = path.Join(baseURL, webClientTasksPathDefault)\n\twebStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)\n\twebOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault)\n}\n\nfunc updateWebAdminURLs(baseURL string) {\n\tif !path.IsAbs(baseURL) {\n\t\tbaseURL = \"/\"\n\t}\n\twebRootPath = path.Join(baseURL, webRootPathDefault)\n\twebBasePath = path.Join(baseURL, webBasePathDefault)\n\twebBaseAdminPath = path.Join(baseURL, webBasePathAdminDefault)\n\twebOIDCRedirectPath = path.Join(baseURL, webOIDCRedirectPathDefault)\n\twebOAuth2RedirectPath = path.Join(baseURL, webOAuth2RedirectPathDefault)\n\twebOAuth2TokenPath = path.Join(baseURL, webOAuth2TokenPathDefault)\n\twebAdminSetupPath = path.Join(baseURL, webAdminSetupPathDefault)\n\twebAdminLoginPath = path.Join(baseURL, webAdminLoginPathDefault)\n\twebAdminOIDCLoginPath = path.Join(baseURL, webAdminOIDCLoginPathDefault)\n\twebAdminTwoFactorPath = path.Join(baseURL, webAdminTwoFactorPathDefault)\n\twebAdminTwoFactorRecoveryPath = path.Join(baseURL, webAdminTwoFactorRecoveryPathDefault)\n\twebLogoutPath = path.Join(baseURL, webLogoutPathDefault)\n\twebUsersPath = path.Join(baseURL, webUsersPathDefault)\n\twebUserPath = path.Join(baseURL, webUserPathDefault)\n\twebConnectionsPath = path.Join(baseURL, webConnectionsPathDefault)\n\twebFoldersPath = path.Join(baseURL, webFoldersPathDefault)\n\twebFolderPath = path.Join(baseURL, webFolderPathDefault)\n\twebGroupsPath = path.Join(baseURL, webGroupsPathDefault)\n\twebGroupPath = path.Join(baseURL, webGroupPathDefault)\n\twebStatusPath = path.Join(baseURL, webStatusPathDefault)\n\twebAdminsPath = path.Join(baseURL, webAdminsPathDefault)\n\twebAdminPath = path.Join(baseURL, webAdminPathDefault)\n\twebMaintenancePath = path.Join(baseURL, webMaintenancePathDefault)\n\twebBackupPath = path.Join(baseURL, webBackupPathDefault)\n\twebRestorePath = path.Join(baseURL, webRestorePathDefault)\n\twebScanVFolderPath = path.Join(baseURL, webScanVFolderPathDefault)\n\twebQuotaScanPath = path.Join(baseURL, webQuotaScanPathDefault)\n\twebChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault)\n\twebAdminForgotPwdPath = path.Join(baseURL, webAdminForgotPwdPathDefault)\n\twebAdminResetPwdPath = path.Join(baseURL, webAdminResetPwdPathDefault)\n\twebAdminProfilePath = path.Join(baseURL, webAdminProfilePathDefault)\n\twebAdminMFAPath = path.Join(baseURL, webAdminMFAPathDefault)\n\twebAdminEventRulesPath = path.Join(baseURL, webAdminEventRulesPathDefault)\n\twebAdminEventRulePath = path.Join(baseURL, webAdminEventRulePathDefault)\n\twebAdminEventActionsPath = path.Join(baseURL, webAdminEventActionsPathDefault)\n\twebAdminEventActionPath = path.Join(baseURL, webAdminEventActionPathDefault)\n\twebAdminRolesPath = path.Join(baseURL, webAdminRolesPathDefault)\n\twebAdminRolePath = path.Join(baseURL, webAdminRolePathDefault)\n\twebAdminTOTPGeneratePath = path.Join(baseURL, webAdminTOTPGeneratePathDefault)\n\twebAdminTOTPValidatePath = path.Join(baseURL, webAdminTOTPValidatePathDefault)\n\twebAdminTOTPSavePath = path.Join(baseURL, webAdminTOTPSavePathDefault)\n\twebAdminRecoveryCodesPath = path.Join(baseURL, webAdminRecoveryCodesPathDefault)\n\twebTemplateUser = path.Join(baseURL, webTemplateUserDefault)\n\twebTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)\n\twebDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault)\n\twebDefenderPath = path.Join(baseURL, webDefenderPathDefault)\n\twebIPListPath = path.Join(baseURL, webIPListPathDefault)\n\twebIPListsPath = path.Join(baseURL, webIPListsPathDefault)\n\twebEventsPath = path.Join(baseURL, webEventsPathDefault)\n\twebEventsFsSearchPath = path.Join(baseURL, webEventsFsSearchPathDefault)\n\twebEventsProviderSearchPath = path.Join(baseURL, webEventsProviderSearchPathDefault)\n\twebEventsLogSearchPath = path.Join(baseURL, webEventsLogSearchPathDefault)\n\twebConfigsPath = path.Join(baseURL, webConfigsPathDefault)\n\twebStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)\n\twebOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault)\n}\n\n// GetHTTPRouter returns an HTTP handler suitable to use for test cases\nfunc GetHTTPRouter(b Binding) (http.Handler, error) {\n\tserver := newHttpdServer(b, filepath.Join(\"..\", \"..\", \"static\"), \"\", CorsConfig{}, filepath.Join(\"..\", \"..\", \"openapi\"))\n\tif err := server.initializeRouter(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn server.router, nil\n}\n\n// the ticker cannot be started/stopped from multiple goroutines\nfunc startCleanupTicker(duration time.Duration) {\n\tstopCleanupTicker()\n\tcleanupTicker = time.NewTicker(duration)\n\tcleanupDone = make(chan bool)\n\n\tgo func() {\n\t\tcounter := int64(0)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-cleanupDone:\n\t\t\t\treturn\n\t\t\tcase <-cleanupTicker.C:\n\t\t\t\tcounter++\n\t\t\t\tinvalidatedJWTTokens.Cleanup()\n\t\t\t\tresetCodesMgr.Cleanup()\n\t\t\t\twebTaskMgr.Cleanup()\n\t\t\t\tif counter%2 == 0 {\n\t\t\t\t\toidcMgr.cleanup()\n\t\t\t\t\toauth2Mgr.cleanup()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc stopCleanupTicker() {\n\tif cleanupTicker != nil {\n\t\tcleanupTicker.Stop()\n\t\tcleanupDone <- true\n\t\tcleanupTicker = nil\n\t}\n}\n\nfunc getSigningKey(signingPassphrase string) []byte {\n\tvar key []byte\n\tif signingPassphrase != \"\" {\n\t\tkey = []byte(signingPassphrase)\n\t} else {\n\t\tkey = util.GenerateRandomBytes(32)\n\t}\n\tsk := sha256.Sum256(key)\n\treturn sk[:]\n}\n\n// SetInstallationCodeResolver sets a function to call to resolve the installation code\nfunc SetInstallationCodeResolver(fn FnInstallationCodeResolver) {\n\tfnInstallationCodeResolver = fn\n}\n\nfunc resolveInstallationCode() string {\n\tif fnInstallationCodeResolver != nil {\n\t\treturn fnInstallationCodeResolver(installationCode)\n\t}\n\treturn installationCode\n}\n\ntype neuteredFileSystem struct {\n\tfs http.FileSystem\n}\n\nfunc (nfs neuteredFileSystem) Open(name string) (http.File, error) {\n\tf, err := nfs.fs.Open(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts, err := f.Stat()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif s.IsDir() {\n\t\tindex := path.Join(name, \"index.html\")\n\t\tif _, err := nfs.fs.Open(index); err != nil {\n\t\t\tdefer f.Close()\n\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn f, nil\n}\n"
  },
  {
    "path": "internal/httpd/httpd_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/png\"\n\t\"io\"\n\t\"io/fs\"\n\t\"math\"\n\t\"mime/multipart\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/render\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t\"github.com/lithammer/shortuuid/v4\"\n\t_ \"github.com/mattn/go-sqlite3\"\n\t\"github.com/mhale/smtpd\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/pquerna/otp\"\n\t\"github.com/pquerna/otp/totp\"\n\t\"github.com/rs/xid\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/sftpgo/sdk\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"golang.org/x/net/html\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/acme\"\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpdtest\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tdefaultUsername                = \"test_user\"\n\tdefaultPassword                = \"test_password\"\n\ttestPubKey                     = \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1\"\n\ttestPubKey1                    = \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCd60+/j+y8f0tLftihWV1YN9RSahMI9btQMDIMqts/jeNbD8jgoogM3nhF7KxfcaMKURuD47KC4Ey6iAJUJ0sWkSNNxOcIYuvA+5MlspfZDsa8Ag76Fe1vyz72WeHMHMeh/hwFo2TeIeIXg480T1VI6mzfDrVp2GzUx0SS0dMsQBjftXkuVR8YOiOwMCAH2a//M1OrvV7d/NBk6kBN0WnuIBb2jKm15PAA7+jQQG7tzwk2HedNH3jeL5GH31xkSRwlBczRK0xsCQXehAlx6cT/e/s44iJcJTHfpPKoSk6UAhPJYe7Z1QnuoawY9P9jQaxpyeImBZxxUEowhjpj2avBxKdRGBVK8R7EL8tSOeLbhdyWe5Mwc1+foEbq9Zz5j5Kd+hn3Wm1UnsGCrXUUUoZp1jnlNl0NakCto+5KmqnT9cHxaY+ix2RLUWAZyVFlRq71OYux1UHJnEJPiEI1/tr4jFBSL46qhQZv/TfpkfVW8FLz0lErfqu0gQEZnNHr3Fc= nicola@p1\"\n\tdefaultTokenAuthUser           = \"admin\"\n\tdefaultTokenAuthPass           = \"password\"\n\taltAdminUsername               = \"newTestAdmin\"\n\taltAdminPassword               = \"password1\"\n\tcsrfFormToken                  = \"_form_token\"\n\ttokenPath                      = \"/api/v2/token\"\n\tuserTokenPath                  = \"/api/v2/user/token\"\n\tuserLogoutPath                 = \"/api/v2/user/logout\"\n\tuserPath                       = \"/api/v2/users\"\n\tadminPath                      = \"/api/v2/admins\"\n\tadminPwdPath                   = \"/api/v2/admin/changepwd\"\n\tfolderPath                     = \"/api/v2/folders\"\n\tgroupPath                      = \"/api/v2/groups\"\n\tactiveConnectionsPath          = \"/api/v2/connections\"\n\tserverStatusPath               = \"/api/v2/status\"\n\tquotasBasePath                 = \"/api/v2/quotas\"\n\tquotaScanPath                  = \"/api/v2/quotas/users/scans\"\n\tquotaScanVFolderPath           = \"/api/v2/quotas/folders/scans\"\n\tdefenderHosts                  = \"/api/v2/defender/hosts\"\n\tversionPath                    = \"/api/v2/version\"\n\tlogoutPath                     = \"/api/v2/logout\"\n\tuserPwdPath                    = \"/api/v2/user/changepwd\"\n\tuserDirsPath                   = \"/api/v2/user/dirs\"\n\tuserFilesPath                  = \"/api/v2/user/files\"\n\tuserFileActionsPath            = \"/api/v2/user/file-actions\"\n\tuserStreamZipPath              = \"/api/v2/user/streamzip\"\n\tuserUploadFilePath             = \"/api/v2/user/files/upload\"\n\tuserFilesDirsMetadataPath      = \"/api/v2/user/files/metadata\"\n\tapiKeysPath                    = \"/api/v2/apikeys\"\n\tadminTOTPConfigsPath           = \"/api/v2/admin/totp/configs\"\n\tadminTOTPGeneratePath          = \"/api/v2/admin/totp/generate\"\n\tadminTOTPValidatePath          = \"/api/v2/admin/totp/validate\"\n\tadminTOTPSavePath              = \"/api/v2/admin/totp/save\"\n\tadmin2FARecoveryCodesPath      = \"/api/v2/admin/2fa/recoverycodes\"\n\tadminProfilePath               = \"/api/v2/admin/profile\"\n\tuserTOTPConfigsPath            = \"/api/v2/user/totp/configs\"\n\tuserTOTPGeneratePath           = \"/api/v2/user/totp/generate\"\n\tuserTOTPValidatePath           = \"/api/v2/user/totp/validate\"\n\tuserTOTPSavePath               = \"/api/v2/user/totp/save\"\n\tuser2FARecoveryCodesPath       = \"/api/v2/user/2fa/recoverycodes\"\n\tuserProfilePath                = \"/api/v2/user/profile\"\n\tuserSharesPath                 = \"/api/v2/user/shares\"\n\tfsEventsPath                   = \"/api/v2/events/fs\"\n\tproviderEventsPath             = \"/api/v2/events/provider\"\n\tlogEventsPath                  = \"/api/v2/events/logs\"\n\tsharesPath                     = \"/api/v2/shares\"\n\teventActionsPath               = \"/api/v2/eventactions\"\n\teventRulesPath                 = \"/api/v2/eventrules\"\n\trolesPath                      = \"/api/v2/roles\"\n\tipListsPath                    = \"/api/v2/iplists\"\n\thealthzPath                    = \"/healthz\"\n\twebBasePath                    = \"/web\"\n\twebBasePathAdmin               = \"/web/admin\"\n\twebAdminSetupPath              = \"/web/admin/setup\"\n\twebLoginPath                   = \"/web/admin/login\"\n\twebLogoutPath                  = \"/web/admin/logout\"\n\twebUsersPath                   = \"/web/admin/users\"\n\twebUserPath                    = \"/web/admin/user\"\n\twebGroupsPath                  = \"/web/admin/groups\"\n\twebGroupPath                   = \"/web/admin/group\"\n\twebFoldersPath                 = \"/web/admin/folders\"\n\twebFolderPath                  = \"/web/admin/folder\"\n\twebConnectionsPath             = \"/web/admin/connections\"\n\twebStatusPath                  = \"/web/admin/status\"\n\twebAdminsPath                  = \"/web/admin/managers\"\n\twebAdminPath                   = \"/web/admin/manager\"\n\twebMaintenancePath             = \"/web/admin/maintenance\"\n\twebRestorePath                 = \"/web/admin/restore\"\n\twebChangeAdminPwdPath          = \"/web/admin/changepwd\"\n\twebAdminProfilePath            = \"/web/admin/profile\"\n\twebTemplateUser                = \"/web/admin/template/user\"\n\twebTemplateFolder              = \"/web/admin/template/folder\"\n\twebDefenderPath                = \"/web/admin/defender\"\n\twebIPListsPath                 = \"/web/admin/ip-lists\"\n\twebIPListPath                  = \"/web/admin/ip-list\"\n\twebAdminTwoFactorPath          = \"/web/admin/twofactor\"\n\twebAdminTwoFactorRecoveryPath  = \"/web/admin/twofactor-recovery\"\n\twebAdminMFAPath                = \"/web/admin/mfa\"\n\twebAdminTOTPSavePath           = \"/web/admin/totp/save\"\n\twebAdminForgotPwdPath          = \"/web/admin/forgot-password\"\n\twebAdminResetPwdPath           = \"/web/admin/reset-password\"\n\twebAdminEventRulesPath         = \"/web/admin/eventrules\"\n\twebAdminEventRulePath          = \"/web/admin/eventrule\"\n\twebAdminEventActionsPath       = \"/web/admin/eventactions\"\n\twebAdminEventActionPath        = \"/web/admin/eventaction\"\n\twebAdminRolesPath              = \"/web/admin/roles\"\n\twebAdminRolePath               = \"/web/admin/role\"\n\twebEventsPath                  = \"/web/admin/events\"\n\twebConfigsPath                 = \"/web/admin/configs\"\n\twebOAuth2TokenPath             = \"/web/admin/oauth2/token\"\n\twebBasePathClient              = \"/web/client\"\n\twebClientLoginPath             = \"/web/client/login\"\n\twebClientFilesPath             = \"/web/client/files\"\n\twebClientEditFilePath          = \"/web/client/editfile\"\n\twebClientDirsPath              = \"/web/client/dirs\"\n\twebClientDownloadZipPath       = \"/web/client/downloadzip\"\n\twebChangeClientPwdPath         = \"/web/client/changepwd\"\n\twebClientProfilePath           = \"/web/client/profile\"\n\twebClientPingPath              = \"/web/client/ping\"\n\twebClientTwoFactorPath         = \"/web/client/twofactor\"\n\twebClientTwoFactorRecoveryPath = \"/web/client/twofactor-recovery\"\n\twebClientLogoutPath            = \"/web/client/logout\"\n\twebClientMFAPath               = \"/web/client/mfa\"\n\twebClientTOTPSavePath          = \"/web/client/totp/save\"\n\twebClientSharesPath            = \"/web/client/shares\"\n\twebClientSharePath             = \"/web/client/share\"\n\twebClientPubSharesPath         = \"/web/client/pubshares\"\n\twebClientForgotPwdPath         = \"/web/client/forgot-password\"\n\twebClientResetPwdPath          = \"/web/client/reset-password\"\n\twebClientViewPDFPath           = \"/web/client/viewpdf\"\n\twebClientGetPDFPath            = \"/web/client/getpdf\"\n\twebClientExistPath             = \"/web/client/exist\"\n\twebClientTasksPath             = \"/web/client/tasks\"\n\twebClientFileMovePath          = \"/web/client/file-actions/move\"\n\twebClientFileCopyPath          = \"/web/client/file-actions/copy\"\n\tjsonAPISuffix                  = \"/json\"\n\thttpBaseURL                    = \"http://127.0.0.1:8081\"\n\tdefaultRemoteAddr              = \"127.0.0.1:1234\"\n\tsftpServerAddr                 = \"127.0.0.1:8022\"\n\tsmtpServerAddr                 = \"127.0.0.1:3525\"\n\thttpsCert                      = `-----BEGIN CERTIFICATE-----\nMIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw\nRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw\nOTUzMDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD\nVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVqWvrJ51t5OxV0v25NsOgR82CA\nNXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIVCzgWkxiz7XE4lgUwX44FCXZM\n3+JeUbKjUzBRMB0GA1UdDgQWBBRhLw+/o3+Z02MI/d4tmaMui9W16jAfBgNVHSME\nGDAWgBRhLw+/o3+Z02MI/d4tmaMui9W16jAPBgNVHRMBAf8EBTADAQH/MAoGCCqG\nSM49BAMCA2kAMGYCMQDqLt2lm8mE+tGgtjDmtFgdOcI72HSbRQ74D5rYTzgST1rY\n/8wTi5xl8TiFUyLMUsICMQC5ViVxdXbhuG7gX6yEqSkMKZICHpO8hqFwOD/uaFVI\ndV4vKmHUzwK/eIx+8Ay3neE=\n-----END CERTIFICATE-----`\n\thttpsKey = `-----BEGIN EC PARAMETERS-----\nBgUrgQQAIg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDCfMNsN6miEE3rVyUPwElfiJSWaR5huPCzUenZOfJT04GAcQdWvEju3\nUM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq\nWvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV\nCzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=\n-----END EC PRIVATE KEY-----`\n\tsftpPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACB+RB4yNTZz9mHOkawwUibNdemijVV3ErMeLxWUBlCN/gAAAJA7DjpfOw46\nXwAAAAtzc2gtZWQyNTUxOQAAACB+RB4yNTZz9mHOkawwUibNdemijVV3ErMeLxWUBlCN/g\nAAAEA0E24gi8ab/XRSvJ85TGZJMe6HVmwxSG4ExPfTMwwe2n5EHjI1NnP2Yc6RrDBSJs11\n6aKNVXcSsx4vFZQGUI3+AAAACW5pY29sYUBwMQECAwQ=\n-----END OPENSSH PRIVATE KEY-----`\n\tsftpPkeyFingerprint = \"SHA256:QVQ06XHZZbYZzqfrsZcf3Yozy2WTnqQPeLOkcJCdbP0\"\n\t// password protected private key\n\ttestPrivateKeyPwd = `-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAvfwQQcs\n+PyMsCLTNFcKiQAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAILqltfCL7IPuIQ2q\n+8w23flfgskjIlKViEwMfjJR4mrbAAAAkHp5xgG8J1XW90M/fT59ZUQht8sZzzP17rEKlX\nwaYKvLzDxkPK6LFIYs55W1EX1eVt/2Maq+zQ7k2SOUmhPNknsUOlPV2gytX3uIYvXF7u2F\nFTBIJuzZ+UQ14wFbraunliE9yye9DajVG1kz2cz2wVgXUbee+gp5NyFVvln+TcTxXwMsWD\nqwlk5iw/jQekxThg==\n-----END OPENSSH PRIVATE KEY-----\n`\n\ttestPubKeyPwd  = \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILqltfCL7IPuIQ2q+8w23flfgskjIlKViEwMfjJR4mrb\"\n\tprivateKeyPwd  = \"password\"\n\trsa1024PrivKey = `-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAIEAxgrZ84gJyU7Qz8JbYuYh0fgTN29h4qVkqDkEE0lWZe7L4QRcQHrB\nvycJO5vjfitY5JTojV3nbDNHN6XGVX8QNurwXmxv0EmEbqPoNO/rTf1t7qqwMBBAfSJJ5H\nTXsO37vqcWSOt1Ki5yjRm232UfPo3AYXaZdOKDWKpzI12FfqkAAAIAondFqKJ3RagAAAAH\nc3NoLXJzYQAAAIEAxgrZ84gJyU7Qz8JbYuYh0fgTN29h4qVkqDkEE0lWZe7L4QRcQHrBvy\ncJO5vjfitY5JTojV3nbDNHN6XGVX8QNurwXmxv0EmEbqPoNO/rTf1t7qqwMBBAfSJJ5HTX\nsO37vqcWSOt1Ki5yjRm232UfPo3AYXaZdOKDWKpzI12FfqkAAAADAQABAAAAgC7V5COG+a\nGFJTbtJQWnnTn17D2A9upN6RcrnL4e6vLiXY8So+qP3YAicDmLrWpqP/SXDsRX/+ID4oTT\njKstiJy5jTvXAozwBbFCvNDk1qifs8p/HKzel3t0172j6gLOa2h9+clJ4BYyCk6ue4f8fV\nyKTIc9chdJSpeINNY60CJxAAAAQQDhYpGXljD2Xy/CzqRXyoF+iMtOImLlbgQYswTXegk3\n7JoCNvwqg8xP+JxGpvUGpX23VWh0nBhzcAKHGlssiYQuAAAAQQDwB6s7s1WIRZ2Jsz8f6l\n7/ebpPrAMyKmWkXc7KyvR53zuMkMIdvujM5NkOWh1ON8jtNumArey2dWuGVh+pXbdVAAAA\nQQDTOAaMcyTfXMH/oSMsp+5obvT/RuewaRLHdBiCy0y1Jw0ykOcOCkswr/btDL26hImaHF\nSheorO+2We7dnFuUIFAAAACW5pY29sYUBwMQE=\n-----END OPENSSH PRIVATE KEY-----`\n\trsa1024PubKey  = \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDGCtnziAnJTtDPwlti5iHR+BM3b2HipWSoOQQTSVZl7svhBFxAesG/Jwk7m+N+K1jklOiNXedsM0c3pcZVfxA26vBebG/QSYRuo+g07+tN/W3uqrAwEEB9IknkdNew7fu+pxZI63UqLnKNGbbfZR8+jcBhdpl04oNYqnMjXYV+qQ==\"\n\tredactedSecret = \"[**redacted**]\"\n\tosWindows      = \"windows\"\n\toidcMockAddr   = \"127.0.0.1:11111\"\n)\n\nvar (\n\tconfigDir       = filepath.Join(\".\", \"..\", \"..\")\n\tdefaultPerms    = []string{dataprovider.PermAny}\n\thomeBasePath    string\n\tbackupsPath     string\n\ttestServer      *httptest.Server\n\tpostConnectPath string\n\tpreActionPath   string\n\tlastResetCode   string\n)\n\ntype fakeConnection struct {\n\t*common.BaseConnection\n\tcommand string\n}\n\nfunc (c *fakeConnection) Disconnect() error {\n\tcommon.Connections.Remove(c.GetID())\n\treturn nil\n}\n\nfunc (c *fakeConnection) GetClientVersion() string {\n\treturn \"\"\n}\n\nfunc (c *fakeConnection) GetCommand() string {\n\treturn c.command\n}\n\nfunc (c *fakeConnection) GetLocalAddress() string {\n\treturn \"\"\n}\n\nfunc (c *fakeConnection) GetRemoteAddress() string {\n\treturn \"\"\n}\n\ntype generateTOTPRequest struct {\n\tConfigName string `json:\"config_name\"`\n}\n\ntype generateTOTPResponse struct {\n\tConfigName string `json:\"config_name\"`\n\tIssuer     string `json:\"issuer\"`\n\tSecret     string `json:\"secret\"`\n\tQRCode     []byte `json:\"qr_code\"`\n}\n\ntype validateTOTPRequest struct {\n\tConfigName string `json:\"config_name\"`\n\tPasscode   string `json:\"passcode\"`\n\tSecret     string `json:\"secret\"`\n}\n\ntype recoveryCode struct {\n\tCode string `json:\"code\"`\n\tUsed bool   `json:\"used\"`\n}\n\nfunc TestMain(m *testing.M) { //nolint:gocyclo\n\thomeBasePath = os.TempDir()\n\tlogfilePath := filepath.Join(configDir, \"sftpgo_api_test.log\")\n\tlogger.InitLogger(logfilePath, 5, 1, 28, false, false, zerolog.DebugLevel)\n\tos.Setenv(\"SFTPGO_COMMON__UPLOAD_MODE\", \"2\")\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN\", \"1\")\n\tos.Setenv(\"SFTPGO_COMMON__ALLOW_SELF_CONNECTIONS\", \"1\")\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__NAMING_RULES\", \"0\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_USERNAME\", \"admin\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_PASSWORD\", \"password\")\n\tos.Setenv(\"SFTPGO_HTTPD__MAX_UPLOAD_FILE_SIZE\", \"1048576000\")\n\terr := config.LoadConfig(configDir, \"\")\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"error loading configuration: %v\", err)\n\t\tos.Exit(1)\n\t}\n\twdPath, err := os.Getwd()\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"error getting exe path: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tpluginsConfig := []plugin.Config{\n\t\t{\n\t\t\tType:     \"eventsearcher\",\n\t\t\tCmd:      filepath.Join(wdPath, \"..\", \"..\", \"tests\", \"eventsearcher\", \"eventsearcher\"),\n\t\t\tAutoMTLS: true,\n\t\t},\n\t}\n\tif runtime.GOOS == osWindows {\n\t\tpluginsConfig[0].Cmd += \".exe\"\n\t}\n\tproviderConf := config.GetProviderConf()\n\tlogger.InfoToConsole(\"Starting HTTPD tests, provider: %v\", providerConf.Driver)\n\n\tbackupsPath = filepath.Join(os.TempDir(), \"test_backups\")\n\tproviderConf.BackupsPath = backupsPath\n\terr = os.MkdirAll(backupsPath, os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error creating backups path: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\tkmsConfig := config.GetKMSConfig()\n\terr = kmsConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing kms: %v\", err)\n\t\tos.Exit(1)\n\t}\n\terr = plugin.Initialize(pluginsConfig, \"debug\")\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing plugin: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tmfaConfig := config.GetMFAConfig()\n\terr = mfaConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing MFA: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"error initializing data provider: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\terr = common.Initialize(config.GetCommonConfig(), 0)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"error initializing common: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\tpostConnectPath = filepath.Join(homeBasePath, \"postconnect.sh\")\n\tpreActionPath = filepath.Join(homeBasePath, \"preaction.sh\")\n\n\thttpConfig := config.GetHTTPConfig()\n\thttpConfig.RetryMax = 1\n\thttpConfig.Timeout = 5\n\thttpConfig.Initialize(configDir) //nolint:errcheck\n\n\thttpdConf := config.GetHTTPDConfig()\n\n\thttpdConf.Bindings[0].Port = 8081\n\thttpdConf.Bindings[0].Security = httpd.SecurityConf{\n\t\tEnabled: true,\n\t\tHTTPSProxyHeaders: []httpd.HTTPSProxyHeader{\n\t\t\t{\n\t\t\t\tKey:   \"X-Forwarded-Proto\",\n\t\t\t\tValue: \"https\",\n\t\t\t},\n\t\t},\n\t\tCacheControl: \"private\",\n\t}\n\thttpdtest.SetBaseURL(httpBaseURL)\n\t// required to test sftpfs\n\tsftpdConf := config.GetSFTPDConfig()\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort: 8022,\n\t\t},\n\t}\n\thostKeyPath := filepath.Join(os.TempDir(), \"id_rsa\")\n\tsftpdConf.HostKeys = []string{hostKeyPath}\n\n\tgo func() {\n\t\tif err := httpdConf.Initialize(configDir, 0); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tif err := sftpdConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SFTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tstartSMTPServer()\n\tstartOIDCMockServer()\n\n\twaitTCPListening(httpdConf.Bindings[0].GetAddress())\n\twaitTCPListening(sftpdConf.Bindings[0].GetAddress())\n\thttpd.ReloadCertificateMgr() //nolint:errcheck\n\t// now start an https server\n\tcertPath := filepath.Join(os.TempDir(), \"test.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test.key\")\n\terr = os.WriteFile(certPath, []byte(httpsCert), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing HTTPS certificate: %v\", err)\n\t\tos.Exit(1)\n\t}\n\terr = os.WriteFile(keyPath, []byte(httpsKey), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing HTTPS private key: %v\", err)\n\t\tos.Exit(1)\n\t}\n\thttpdConf.Bindings[0].Port = 8443\n\thttpdConf.Bindings[0].EnableHTTPS = true\n\thttpdConf.Bindings[0].CertificateFile = certPath\n\thttpdConf.Bindings[0].CertificateKeyFile = keyPath\n\thttpdConf.Bindings = append(httpdConf.Bindings, httpd.Binding{})\n\n\tgo func() {\n\t\tif err := httpdConf.Initialize(configDir, 0); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTPS server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\twaitTCPListening(httpdConf.Bindings[0].GetAddress())\n\thttpd.ReloadCertificateMgr() //nolint:errcheck\n\n\thandler, err := httpd.GetHTTPRouter(httpdConf.Bindings[0])\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"unable to get http test handler: %v\", err)\n\t\tos.Exit(1)\n\t}\n\ttestServer = httptest.NewServer(handler)\n\tdefer testServer.Close()\n\n\texitCode := m.Run()\n\tos.Remove(logfilePath)\n\tos.RemoveAll(backupsPath)\n\tos.Remove(certPath)\n\tos.Remove(keyPath)\n\tos.Remove(hostKeyPath)\n\tos.Remove(hostKeyPath + \".pub\")\n\tos.Remove(postConnectPath)\n\tos.Remove(preActionPath)\n\tos.Exit(exitCode)\n}\n\nfunc TestInitialization(t *testing.T) {\n\tisShared := 0\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tinvalidFile := \"invalid file\"\n\tpassphraseFile := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\terr = os.WriteFile(passphraseFile, []byte(\"my secret\"), 0600)\n\tassert.NoError(t, err)\n\tdefer os.Remove(passphraseFile)\n\thttpdConf := config.GetHTTPDConfig()\n\thttpdConf.SigningPassphraseFile = invalidFile\n\terr = httpdConf.Initialize(configDir, isShared)\n\tassert.ErrorIs(t, err, fs.ErrNotExist)\n\thttpdConf.SigningPassphraseFile = passphraseFile\n\tdefaultTemplatesPath := httpdConf.TemplatesPath\n\tdefaultStaticPath := httpdConf.StaticFilesPath\n\thttpdConf.CertificateFile = invalidFile\n\thttpdConf.CertificateKeyFile = invalidFile\n\terr = httpdConf.Initialize(configDir, isShared)\n\tassert.Error(t, err)\n\thttpdConf.CertificateFile = \"\"\n\thttpdConf.CertificateKeyFile = \"\"\n\thttpdConf.TemplatesPath = \".\"\n\terr = httpdConf.Initialize(configDir, isShared)\n\tassert.Error(t, err)\n\thttpdConf = config.GetHTTPDConfig()\n\thttpdConf.TemplatesPath = defaultTemplatesPath\n\thttpdConf.CertificateFile = invalidFile\n\thttpdConf.CertificateKeyFile = invalidFile\n\thttpdConf.StaticFilesPath = \"\"\n\thttpdConf.TemplatesPath = \"\"\n\terr = httpdConf.Initialize(configDir, isShared)\n\tassert.Error(t, err)\n\thttpdConf.StaticFilesPath = defaultStaticPath\n\thttpdConf.TemplatesPath = defaultTemplatesPath\n\thttpdConf.CertificateFile = filepath.Join(os.TempDir(), \"test.crt\")\n\thttpdConf.CertificateKeyFile = filepath.Join(os.TempDir(), \"test.key\")\n\thttpdConf.CACertificates = append(httpdConf.CACertificates, invalidFile)\n\terr = httpdConf.Initialize(configDir, isShared)\n\tassert.Error(t, err)\n\thttpdConf.CACertificates = nil\n\thttpdConf.CARevocationLists = append(httpdConf.CARevocationLists, invalidFile)\n\terr = httpdConf.Initialize(configDir, isShared)\n\tassert.Error(t, err)\n\thttpdConf.CARevocationLists = nil\n\thttpdConf.SigningPassphraseFile = passphraseFile\n\thttpdConf.Bindings[0].ProxyAllowed = []string{\"invalid ip/network\"}\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is not a valid IP range\")\n\t}\n\tassert.Equal(t, \"my secret\", httpdConf.SigningPassphrase)\n\thttpdConf.Bindings[0].ProxyAllowed = nil\n\thttpdConf.Bindings[0].EnableWebAdmin = false\n\thttpdConf.Bindings[0].EnableWebClient = false\n\thttpdConf.Bindings[0].Port = 8081\n\thttpdConf.Bindings[0].EnableHTTPS = true\n\thttpdConf.Bindings[0].ClientAuthType = 1\n\thttpdConf.TokenValidation = 1\n\terr = httpdConf.Initialize(configDir, 0)\n\tassert.Error(t, err)\n\thttpdConf.TokenValidation = 0\n\terr = httpdConf.Initialize(configDir, 0)\n\tassert.Error(t, err)\n\n\thttpdConf.Bindings[0].OIDC = httpd.OIDC{\n\t\tClientID:     \"123\",\n\t\tClientSecret: \"secret\",\n\t\tConfigURL:    \"http://127.0.0.1:11111\",\n\t}\n\terr = httpdConf.Initialize(configDir, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"oidc\")\n\t}\n\thttpdConf.Bindings[0].OIDC.UsernameField = \"preferred_username\"\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"oidc\")\n\t}\n\thttpdConf.Bindings[0].OIDC = httpd.OIDC{}\n\thttpdConf.Bindings[0].BaseURL = \"ftp://127.0.0.1\"\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"URL schema\")\n\t}\n\thttpdConf.Bindings[0].BaseURL = \"\"\n\thttpdConf.Bindings[0].EnableWebClient = true\n\thttpdConf.Bindings[0].EnableWebAdmin = true\n\thttpdConf.Bindings[0].EnableRESTAPI = true\n\thttpdConf.Bindings[0].DisabledLoginMethods = 14\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no login method available for WebAdmin UI\")\n\t}\n\thttpdConf.Bindings[0].DisabledLoginMethods = 13\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no login method available for WebAdmin UI\")\n\t}\n\thttpdConf.Bindings[0].DisabledLoginMethods = 9\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no login method available for WebClient UI\")\n\t}\n\thttpdConf.Bindings[0].DisabledLoginMethods = 11\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no login method available for WebClient UI\")\n\t}\n\thttpdConf.Bindings[0].DisabledLoginMethods = 12\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no login method available for WebAdmin UI\")\n\t}\n\thttpdConf.Bindings[0].EnableWebAdmin = false\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no login method available for WebClient UI\")\n\t}\n\thttpdConf.Bindings[0].EnableWebClient = false\n\thttpdConf.Bindings[0].DisabledLoginMethods = 240\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"no login method available for REST API\")\n\t}\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = httpdConf.Initialize(configDir, isShared)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to load config from provider\")\n\t}\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicUserHandling(t *testing.T) {\n\tu := getTestUser()\n\tu.Email = \"user@user.com\"\n\tu.Filters.AdditionalEmails = []string{\"email1@user.com\", \"email2@user.com\"}\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\t_, resp, err = httpdtest.AddUser(u, http.StatusConflict)\n\tassert.NoError(t, err, string(resp))\n\tlastPwdChange := user.LastPasswordChange\n\tassert.Greater(t, lastPwdChange, int64(0))\n\tuser.MaxSessions = 10\n\tuser.QuotaSize = 4096\n\tuser.QuotaFiles = 2\n\tuser.UploadBandwidth = 128\n\tuser.DownloadBandwidth = 64\n\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now())\n\tuser.AdditionalInfo = \"some free text\"\n\tuser.Filters.TLSUsername = sdk.TLSUsernameCN\n\tuser.Email = \"user@example.net\"\n\tuser.OIDCCustomFields = &map[string]any{\n\t\t\"field1\": \"value1\",\n\t}\n\tuser.Filters.WebClient = append(user.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled,\n\t\tsdk.WebClientWriteDisabled)\n\toriginalUser := user\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, originalUser.ID, user.ID)\n\tassert.Equal(t, lastPwdChange, user.LastPasswordChange)\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Nil(t, user.OIDCCustomFields)\n\tassert.True(t, user.HasPassword)\n\n\tuser.Email = \"invalid@email\"\n\tuser.FsConfig.OSConfig = sdk.OSFsConfig{\n\t\tReadBufferSize:  1,\n\t\tWriteBufferSize: 2,\n\t}\n\t_, body, err := httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(body), \"Validation error: email\")\n\n\tuser.Email = \"\"\n\tuser.Filters.AdditionalEmails = []string{\"invalid@email\"}\n\t_, body, err = httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(body), \"Validation error: email\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicRoleHandling(t *testing.T) {\n\tr := getTestRole()\n\trole, resp, err := httpdtest.AddRole(r, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Greater(t, role.CreatedAt, int64(0))\n\tassert.Greater(t, role.UpdatedAt, int64(0))\n\troleGet, _, err := httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role, roleGet)\n\n\troles, _, err := httpdtest.GetRoles(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.GreaterOrEqual(t, len(roles), 1) {\n\t\tfound := false\n\t\tfor _, ro := range roles {\n\t\t\tif ro.Name == r.Name {\n\t\t\t\tassert.Equal(t, role, ro)\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\t\tassert.True(t, found)\n\t}\n\troles, _, err = httpdtest.GetRoles(0, int64(len(roles)), http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, roles, 0)\n\n\trole.Description = \"updated desc\"\n\t_, _, err = httpdtest.UpdateRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n\troleGet, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Description, roleGet.Description)\n\n\t_, _, err = httpdtest.GetRoleByName(role.Name+\"_\", http.StatusNotFound)\n\tassert.NoError(t, err)\n\t// adding the same role again should fail\n\t_, _, err = httpdtest.AddRole(r, http.StatusConflict)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestRoleRelations(t *testing.T) {\n\tr := getTestRole()\n\trole, resp, err := httpdtest.AddRole(r, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\ta.Role = role.Name\n\t_, resp, err = httpdtest.AddAdmin(a, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"a role admin cannot be a super admin\")\n\n\ta.Permissions = []string{dataprovider.PermAdminAddUsers, dataprovider.PermAdminChangeUsers,\n\t\tdataprovider.PermAdminDeleteUsers, dataprovider.PermAdminViewUsers}\n\ta.Role = \"missing admin role\"\n\t_, _, err = httpdtest.AddAdmin(a, http.StatusConflict)\n\tassert.NoError(t, err)\n\ta.Role = role.Name\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\tadmin.Role = \"invalid role\"\n\t_, resp, err = httpdtest.UpdateAdmin(admin, http.StatusConflict)\n\tassert.NoError(t, err, string(resp))\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, admin.Role)\n\n\tresp, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.Error(t, err, \"removing a referenced role should fail\")\n\tassert.Contains(t, string(resp), \"is referenced\")\n\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, role.Admins, 1) {\n\t\tassert.Equal(t, admin.Username, role.Admins[0])\n\t}\n\n\tu1 := getTestUser()\n\tu1.Username = defaultUsername + \"1\"\n\tu1.Role = \"missing role\"\n\t_, _, err = httpdtest.AddUser(u1, http.StatusConflict)\n\tassert.NoError(t, err)\n\tu1.Role = role.Name\n\tuser1, _, err := httpdtest.AddUser(u1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, user1.Role)\n\tuser1.Role = \"missing\"\n\t_, _, err = httpdtest.UpdateUser(user1, http.StatusConflict, \"\")\n\tassert.NoError(t, err)\n\tuser1, _, err = httpdtest.GetUserByUsername(user1.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, user1.Role)\n\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, role.Admins, 1) {\n\t\tassert.Equal(t, admin.Username, role.Admins[0])\n\t}\n\tif assert.Len(t, role.Users, 1) {\n\t\tassert.Equal(t, user1.Username, role.Users[0])\n\t}\n\troles, _, err := httpdtest.GetRoles(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tfor _, r := range roles {\n\t\tif r.Name == role.Name {\n\t\t\tif assert.Len(t, role.Admins, 1) {\n\t\t\t\tassert.Equal(t, admin.Username, role.Admins[0])\n\t\t\t}\n\t\t\tif assert.Len(t, role.Users, 1) {\n\t\t\t\tassert.Equal(t, user1.Username, role.Users[0])\n\t\t\t}\n\t\t}\n\t}\n\n\tu2 := getTestUser()\n\tuser2, _, err := httpdtest.AddUser(u2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\t// the global admin can list all users\n\tusers, _, err := httpdtest.GetUsers(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(users), 2)\n\t_, _, err = httpdtest.GetUserByUsername(user1.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\t// the role admin can only list users with its role\n\ttoken, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\thttpdtest.SetJWTToken(token)\n\tusers, _, err = httpdtest.GetUsers(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, users, 1)\n\t_, _, err = httpdtest.GetUserByUsername(user1.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t// the role admin can only update/delete users with its role\n\t_, _, err = httpdtest.UpdateUser(user1, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.UpdateUser(user2, http.StatusNotFound, \"\")\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t// new users created by a role admin have the same role\n\tu3 := getTestUser()\n\tu3.Username = defaultUsername + \"3\"\n\t_, _, err = httpdtest.AddUser(u3, http.StatusCreated)\n\tif assert.Error(t, err) {\n\t\tassert.Equal(t, err.Error(), \"role mismatch\")\n\t}\n\n\tuser3, _, err := httpdtest.GetUserByUsername(u3.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, user3.Role)\n\n\t_, err = httpdtest.RemoveUser(user3, http.StatusOK)\n\tassert.NoError(t, err)\n\n\thttpdtest.SetJWTToken(\"\")\n\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Admins, []string{altAdminUsername})\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser1, _, err = httpdtest.GetUserByUsername(user1.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Empty(t, user1.Role)\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestRSAKeyInvalidSize(t *testing.T) {\n\tu := getTestUser()\n\tu.PublicKeys = append(u.PublicKeys, rsa1024PubKey)\n\t_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"invalid size\")\n\tu = getTestSFTPUser()\n\tu.FsConfig.SFTPConfig.Password = nil\n\tu.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(rsa1024PrivKey)\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"rsa key with size 1024 not accepted\")\n}\n\nfunc TestTLSCert(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.TLSCerts = []string{\"not a cert\"}\n\t_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"invalid TLS certificate\")\n\n\tu.Filters.TLSCerts = []string{httpsCert}\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tif assert.Len(t, user.Filters.TLSCerts, 1) {\n\t\tassert.Equal(t, httpsCert, user.Filters.TLSCerts[0])\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSortRelatedFolders(t *testing.T) {\n\tfolder1 := util.GenerateUniqueID()\n\tfolder2 := util.GenerateUniqueID()\n\tfolder3 := util.GenerateUniqueID()\n\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folder1,\n\t\tMappedPath: filepath.Clean(os.TempDir()),\n\t}\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folder2,\n\t\tMappedPath: filepath.Clean(os.TempDir()),\n\t}\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folder3,\n\t\tMappedPath: filepath.Clean(os.TempDir()),\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: f1,\n\t\t\tVirtualPath:       \"/\" + folder1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: f2,\n\t\t\tVirtualPath:       \"/\" + folder2,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: f3,\n\t\t\tVirtualPath:       \"/\" + folder3,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user.VirtualFolders, 3) {\n\t\tassert.Equal(t, folder1, user.VirtualFolders[0].Name)\n\t\tassert.Equal(t, folder2, user.VirtualFolders[1].Name)\n\t\tassert.Equal(t, folder3, user.VirtualFolders[2].Name)\n\t}\n\t// Update\n\tuser.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: f2,\n\t\t\tVirtualPath:       \"/\" + folder2,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: f1,\n\t\t\tVirtualPath:       \"/\" + folder1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: f3,\n\t\t\tVirtualPath:       \"/\" + folder3,\n\t\t},\n\t}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user.VirtualFolders, 3) {\n\t\tassert.Equal(t, folder2, user.VirtualFolders[0].Name)\n\t\tassert.Equal(t, folder1, user.VirtualFolders[1].Name)\n\t\tassert.Equal(t, folder3, user.VirtualFolders[2].Name)\n\t}\n\n\tg := getTestGroup()\n\tg.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: f1,\n\t\t\tVirtualPath:       \"/\" + folder1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: f2,\n\t\t\tVirtualPath:       \"/\" + folder2,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: f3,\n\t\t\tVirtualPath:       \"/\" + folder3,\n\t\t},\n\t}\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, group.VirtualFolders, 3) {\n\t\tassert.Equal(t, folder1, group.VirtualFolders[0].Name)\n\t\tassert.Equal(t, folder2, group.VirtualFolders[1].Name)\n\t\tassert.Equal(t, folder3, group.VirtualFolders[2].Name)\n\t}\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tgroup.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: f3,\n\t\t\tVirtualPath:       \"/\" + folder3,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: f1,\n\t\t\tVirtualPath:       \"/\" + folder1,\n\t\t},\n\t\t{\n\t\t\tBaseVirtualFolder: f2,\n\t\t\tVirtualPath:       \"/\" + folder2,\n\t\t},\n\t}\n\tgroup, _, err = httpdtest.UpdateGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, group.VirtualFolders, 3) {\n\t\tassert.Equal(t, folder3, group.VirtualFolders[0].Name)\n\t\tassert.Equal(t, folder1, group.VirtualFolders[1].Name)\n\t\tassert.Equal(t, folder2, group.VirtualFolders[2].Name)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveFolder(f1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(f2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(f3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSortRelatedGroups(t *testing.T) {\n\tname1 := util.GenerateUniqueID()\n\tname2 := util.GenerateUniqueID()\n\tname3 := util.GenerateUniqueID()\n\n\tg1 := getTestGroup()\n\tg1.Name = name1\n\tg2 := getTestGroup()\n\tg2.Name = name2\n\tg3 := getTestGroup()\n\tg3.Name = name3\n\n\tgroup1, _, err := httpdtest.AddGroup(g1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup2, _, err := httpdtest.AddGroup(g2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup3, _, err := httpdtest.AddGroup(g3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: name1,\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: name1,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t\t{\n\t\t\tName: name2,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t\t{\n\t\t\tName: name3,\n\t\t\tType: sdk.GroupTypeMembership,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user.Groups, 3) {\n\t\tassert.Equal(t, name1, user.Groups[0].Name)\n\t\tassert.Equal(t, name2, user.Groups[1].Name)\n\t\tassert.Equal(t, name3, user.Groups[2].Name)\n\t}\n\tuser.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: name2,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t\t{\n\t\t\tName: name3,\n\t\t\tType: sdk.GroupTypeMembership,\n\t\t},\n\t\t{\n\t\t\tName: name1,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user.Groups, 3) {\n\t\tassert.Equal(t, name2, user.Groups[0].Name)\n\t\tassert.Equal(t, name3, user.Groups[1].Name)\n\t\tassert.Equal(t, name1, user.Groups[2].Name)\n\t}\n\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Groups = []dataprovider.AdminGroupMapping{\n\t\t{\n\t\t\tName: name3,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsSecondary,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: name2,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsPrimary,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: name1,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsMembership,\n\t\t\t},\n\t\t},\n\t}\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, admin.Groups, 3) {\n\t\tassert.Equal(t, name3, admin.Groups[0].Name)\n\t\tassert.Equal(t, name2, admin.Groups[1].Name)\n\t\tassert.Equal(t, name1, admin.Groups[2].Name)\n\t}\n\tadmin.Groups = []dataprovider.AdminGroupMapping{\n\t\t{\n\t\t\tName: name1,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsPrimary,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: name3,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsMembership,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: name2,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsSecondary,\n\t\t\t},\n\t\t},\n\t}\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, admin.Groups, 3) {\n\t\tassert.Equal(t, name1, admin.Groups[0].Name)\n\t\tassert.Equal(t, name3, admin.Groups[1].Name)\n\t\tassert.Equal(t, name2, admin.Groups[2].Name)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicGroupHandling(t *testing.T) {\n\tg := getTestGroup()\n\tg.UserSettings.Filters.TLSCerts = []string{\"invalid cert\"} // ignored for groups\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Greater(t, group.CreatedAt, int64(0))\n\tassert.Greater(t, group.UpdatedAt, int64(0))\n\tassert.Len(t, group.UserSettings.Filters.TLSCerts, 0)\n\tgroupGet, _, err := httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, group, groupGet)\n\tgroups, _, err := httpdtest.GetGroups(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, groups, 1) {\n\t\tassert.Equal(t, group, groups[0])\n\t}\n\tgroups, _, err = httpdtest.GetGroups(0, 1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, groups, 0)\n\t_, _, err = httpdtest.GetGroupByName(group.Name+\"_\", http.StatusNotFound)\n\tassert.NoError(t, err)\n\t// adding the same group again should fail\n\t_, _, err = httpdtest.AddGroup(g, http.StatusConflict)\n\tassert.NoError(t, err)\n\n\tgroup.UserSettings.HomeDir = filepath.Join(os.TempDir(), \"%username%\")\n\tgroup.UserSettings.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tgroup.UserSettings.FsConfig.SFTPConfig.Endpoint = sftpServerAddr\n\tgroup.UserSettings.FsConfig.SFTPConfig.Username = defaultUsername\n\tgroup.UserSettings.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\tgroup.UserSettings.Permissions = map[string][]string{\n\t\t\"/\": {dataprovider.PermAny},\n\t}\n\tgroup.UserSettings.Filters.AllowedIP = []string{\"10.0.0.0/8\"}\n\tgroup, _, err = httpdtest.UpdateGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\tgroupGet, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, groupGet.UserSettings.Permissions, 1)\n\tassert.Len(t, groupGet.UserSettings.Filters.AllowedIP, 1)\n\n\t// update again and check that the password was preserved\n\tdbGroup, err := dataprovider.GroupExists(group.Name)\n\tassert.NoError(t, err)\n\tgroup.UserSettings.FsConfig.SFTPConfig.Password = kms.NewSecret(\n\t\tdbGroup.UserSettings.FsConfig.SFTPConfig.Password.GetStatus(),\n\t\tdbGroup.UserSettings.FsConfig.SFTPConfig.Password.GetPayload(), \"\", \"\")\n\tgroup.UserSettings.Permissions = nil\n\tgroup.UserSettings.Filters.AllowedIP = nil\n\tgroup, _, err = httpdtest.UpdateGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group.UserSettings.Permissions, 0)\n\tassert.Len(t, group.UserSettings.Filters.AllowedIP, 0)\n\tdbGroup, err = dataprovider.GroupExists(group.Name)\n\tassert.NoError(t, err)\n\terr = dbGroup.UserSettings.FsConfig.SFTPConfig.Password.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, defaultPassword, dbGroup.UserSettings.FsConfig.SFTPConfig.Password.GetPayload())\n\t// check the group permissions\n\tgroupGet, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, groupGet.UserSettings.Permissions, 0)\n\n\tgroup.UserSettings.HomeDir = \"relative path\"\n\t_, _, err = httpdtest.UpdateGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.UpdateGroup(group, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusNotFound)\n\tassert.NoError(t, err)\n}\n\nfunc TestGroupRelations(t *testing.T) {\n\tmappedPath1 := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tfolderName1 := filepath.Base(mappedPath1)\n\tmappedPath2 := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tfolderName2 := filepath.Base(mappedPath2)\n\t_, _, err := httpdtest.AddFolder(vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tOSConfig: sdk.OSFsConfig{\n\t\t\t\tReadBufferSize:  3,\n\t\t\t\tWriteBufferSize: 5,\n\t\t\t},\n\t\t},\n\t}, http.StatusCreated)\n\tassert.NoError(t, err)\n\tg1 := getTestGroup()\n\tg1.Name += \"_1\"\n\tg1.VirtualFolders = append(g1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: \"/vdir1\",\n\t})\n\tg2 := getTestGroup()\n\tg2.Name += \"_2\"\n\tg2.VirtualFolders = append(g2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: \"/vdir2\",\n\t})\n\tg3 := getTestGroup()\n\tg3.Name += \"_3\"\n\tg3.VirtualFolders = append(g3.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: \"/vdir3\",\n\t})\n\t_, _, err = httpdtest.AddGroup(g1, http.StatusCreated)\n\tassert.Error(t, err, \"adding a group with a missing folder must fail\")\n\t_, _, err = httpdtest.AddFolder(vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tgroup1, resp, err := httpdtest.AddGroup(g1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Len(t, group1.VirtualFolders, 1)\n\tgroup2, resp, err := httpdtest.AddGroup(g2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Len(t, group2.VirtualFolders, 1)\n\tgroup3, resp, err := httpdtest.AddGroup(g3, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Len(t, group3.VirtualFolders, 1)\n\n\tfolder1, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder1.Groups, 3)\n\tfolder2, _, err := httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder2.Groups, 0)\n\n\tgroup1.VirtualFolders = append(group1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: folder2,\n\t\tVirtualPath:       \"/vfolder2\",\n\t})\n\tgroup1, _, err = httpdtest.UpdateGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group1.VirtualFolders, 2)\n\n\tfolder2, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder2.Groups, 1)\n\n\tgroup1.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName:       folder1.Name,\n\t\t\t\tMappedPath: folder1.MappedPath,\n\t\t\t},\n\t\t\tVirtualPath: \"/vpathmod\",\n\t\t},\n\t}\n\tgroup1, _, err = httpdtest.UpdateGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group1.VirtualFolders, 1)\n\n\tfolder2, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder2.Groups, 0)\n\n\tgroup1.VirtualFolders = append(group1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: folder2,\n\t\tVirtualPath:       \"/vfolder2mod\",\n\t})\n\tgroup1, _, err = httpdtest.UpdateGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group1.VirtualFolders, 2)\n\n\tfolder2, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder2.Groups, 1)\n\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t\t{\n\t\t\tName: group2.Name,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t\t{\n\t\t\tName: group3.Name,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user.Groups, 3) {\n\t\tfor _, g := range user.Groups {\n\t\t\tif g.Name == group1.Name {\n\t\t\t\tassert.Equal(t, sdk.GroupTypePrimary, g.Type)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, sdk.GroupTypeSecondary, g.Type)\n\t\t\t}\n\t\t}\n\t}\n\tgroup1, _, err = httpdtest.GetGroupByName(group1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group1.Users, 1)\n\tgroup2, _, err = httpdtest.GetGroupByName(group2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group2.Users, 1)\n\tgroup3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group3.Users, 1)\n\n\tuser.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group3.Name,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Groups, 1)\n\n\tgroup1, _, err = httpdtest.GetGroupByName(group1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group1.Users, 0)\n\tgroup2, _, err = httpdtest.GetGroupByName(group2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group2.Users, 0)\n\tgroup3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group3.Users, 1)\n\n\tuser.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t\t{\n\t\t\tName: group2.Name,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t\t{\n\t\t\tName: group3.Name,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Groups, 3)\n\n\tgroup1, _, err = httpdtest.GetGroupByName(group1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group1.Users, 1)\n\tgroup2, _, err = httpdtest.GetGroupByName(group2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group2.Users, 1)\n\tgroup3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group3.Users, 1)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder1, http.StatusOK)\n\tassert.NoError(t, err)\n\tgroup1, _, err = httpdtest.GetGroupByName(group1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group1.Users, 0)\n\tassert.Len(t, group1.VirtualFolders, 1)\n\tgroup2, _, err = httpdtest.GetGroupByName(group2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group2.Users, 0)\n\tassert.Len(t, group2.VirtualFolders, 0)\n\tgroup3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group3.Users, 0)\n\tassert.Len(t, group3.VirtualFolders, 0)\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group3, http.StatusOK)\n\tassert.NoError(t, err)\n\tfolder2, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder2.Groups, 0)\n\t_, err = httpdtest.RemoveFolder(folder2, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestGroupValidation(t *testing.T) {\n\tgroup := getTestGroup()\n\tgroup.VirtualFolders = []vfs.VirtualFolder{\n\t\t{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName:       util.GenerateUniqueID(),\n\t\t\t\tMappedPath: filepath.Join(os.TempDir(), util.GenerateUniqueID()),\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err := httpdtest.AddGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"virtual path is mandatory\")\n\tgroup.VirtualFolders = nil\n\tgroup.UserSettings.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\t_, resp, err = httpdtest.AddGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"endpoint cannot be empty\")\n\tgroup.UserSettings.FsConfig.Provider = sdk.LocalFilesystemProvider\n\tgroup.UserSettings.Permissions = map[string][]string{\n\t\t\"a\": nil,\n\t}\n\t_, resp, err = httpdtest.AddGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"cannot set permissions for non absolute path\")\n\tgroup.UserSettings.Permissions = map[string][]string{\n\t\t\"/\": nil,\n\t}\n\t_, resp, err = httpdtest.AddGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"no permissions granted\")\n\tgroup.UserSettings.Permissions = map[string][]string{\n\t\t\"/..\": nil,\n\t}\n\t_, resp, err = httpdtest.AddGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"cannot set permissions for invalid subdirectory\")\n\tgroup.UserSettings.Permissions = map[string][]string{\n\t\t\"/\": {dataprovider.PermAny},\n\t}\n\tgroup.UserSettings.HomeDir = \"relative\"\n\t_, resp, err = httpdtest.AddGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"home_dir must be an absolute path\")\n\tgroup.UserSettings.HomeDir = \"\"\n\tgroup.UserSettings.Filters.WebClient = []string{\"invalid permission\"}\n\t_, resp, err = httpdtest.AddGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid web client options\")\n}\n\nfunc TestGroupSettingsOverride(t *testing.T) {\n\tmappedPath1 := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tfolderName1 := filepath.Base(mappedPath1)\n\tmappedPath2 := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tfolderName2 := filepath.Base(mappedPath2)\n\tmappedPath3 := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tfolderName3 := filepath.Base(mappedPath3)\n\tg1 := getTestGroup()\n\tg1.Name += \"_1\"\n\tg1.VirtualFolders = append(g1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: \"/vdir1\",\n\t})\n\tg1.UserSettings.Permissions = map[string][]string{\n\t\t\"/dir1\": {dataprovider.PermUpload},\n\t\t\"/dir2\": {dataprovider.PermDownload, dataprovider.PermListItems},\n\t}\n\tg1.UserSettings.FsConfig.OSConfig = sdk.OSFsConfig{\n\t\tReadBufferSize:  6,\n\t\tWriteBufferSize: 2,\n\t}\n\tg2 := getTestGroup()\n\tg2.Name += \"_2\"\n\tg2.UserSettings.Permissions = map[string][]string{\n\t\t\"/dir1\": {dataprovider.PermAny},\n\t\t\"/dir3\": {dataprovider.PermDownload, dataprovider.PermListItems, dataprovider.PermChtimes},\n\t}\n\tg2.VirtualFolders = append(g2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: \"/vdir2\",\n\t})\n\tg2.VirtualFolders = append(g2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: \"/vdir3\",\n\t})\n\tg2.VirtualFolders = append(g2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName3,\n\t\t},\n\t\tVirtualPath: \"/vdir4\",\n\t})\n\tg2.UserSettings.Filters.AccessTime = []sdk.TimePeriod{\n\t\t{\n\t\t\tDayOfWeek: int(time.Now().UTC().Weekday()),\n\t\t\tFrom:      \"00:00\",\n\t\t\tTo:        \"23:59\",\n\t\t},\n\t}\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tOSConfig: sdk.OSFsConfig{\n\t\t\t\tReadBufferSize:  3,\n\t\t\t\tWriteBufferSize: 5,\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folderName3,\n\t\tMappedPath: mappedPath3,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tOSConfig: sdk.OSFsConfig{\n\t\t\t\tReadBufferSize:  1,\n\t\t\t\tWriteBufferSize: 2,\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tgroup1, resp, err := httpdtest.AddGroup(g1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tgroup2, resp, err := httpdtest.AddGroup(g2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t\t{\n\t\t\tName: group2.Name,\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t},\n\t}\n\n\tr := getTestRole()\n\trole, _, err := httpdtest.AddRole(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.Role = role.Name\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Len(t, user.VirtualFolders, 0)\n\tassert.Len(t, user.Permissions, 1)\n\n\tuser, err = dataprovider.CheckUserAndPass(defaultUsername, defaultPassword, \"\", common.ProtocolHTTP)\n\tassert.NoError(t, err)\n\n\tvar folderNames []string\n\tif assert.Len(t, user.VirtualFolders, 4) {\n\t\tfor _, f := range user.VirtualFolders {\n\t\t\tif !slices.Contains(folderNames, f.Name) {\n\t\t\t\tfolderNames = append(folderNames, f.Name)\n\t\t\t}\n\t\t\tswitch f.Name {\n\t\t\tcase folderName1:\n\t\t\t\tassert.Equal(t, mappedPath1, f.MappedPath)\n\t\t\t\tassert.Equal(t, 3, f.FsConfig.OSConfig.ReadBufferSize)\n\t\t\t\tassert.Equal(t, 5, f.FsConfig.OSConfig.WriteBufferSize)\n\t\t\t\tassert.True(t, slices.Contains([]string{\"/vdir1\", \"/vdir2\"}, f.VirtualPath))\n\t\t\tcase folderName2:\n\t\t\t\tassert.Equal(t, mappedPath2, f.MappedPath)\n\t\t\t\tassert.Equal(t, \"/vdir3\", f.VirtualPath)\n\t\t\t\tassert.Equal(t, 0, f.FsConfig.OSConfig.ReadBufferSize)\n\t\t\t\tassert.Equal(t, 0, f.FsConfig.OSConfig.WriteBufferSize)\n\t\t\tcase folderName3:\n\t\t\t\tassert.Equal(t, mappedPath3, f.MappedPath)\n\t\t\t\tassert.Equal(t, \"/vdir4\", f.VirtualPath)\n\t\t\t\tassert.Equal(t, 1, f.FsConfig.OSConfig.ReadBufferSize)\n\t\t\t\tassert.Equal(t, 2, f.FsConfig.OSConfig.WriteBufferSize)\n\t\t\t}\n\t\t}\n\t}\n\tassert.Len(t, folderNames, 3)\n\tassert.Contains(t, folderNames, folderName1)\n\tassert.Contains(t, folderNames, folderName2)\n\tassert.Contains(t, folderNames, folderName3)\n\tassert.Len(t, user.Permissions, 4)\n\tassert.Equal(t, g1.UserSettings.Permissions[\"/dir1\"], user.Permissions[\"/dir1\"])\n\tassert.Equal(t, g1.UserSettings.Permissions[\"/dir2\"], user.Permissions[\"/dir2\"])\n\tassert.Equal(t, g2.UserSettings.Permissions[\"/dir3\"], user.Permissions[\"/dir3\"])\n\tassert.Equal(t, g1.UserSettings.FsConfig.OSConfig.ReadBufferSize, user.FsConfig.OSConfig.ReadBufferSize)\n\tassert.Equal(t, g1.UserSettings.FsConfig.OSConfig.WriteBufferSize, user.FsConfig.OSConfig.WriteBufferSize)\n\tassert.Len(t, user.Filters.AccessTime, 1)\n\n\tuser, err = dataprovider.GetUserAfterIDPAuth(defaultUsername, \"\", common.ProtocolOIDC, nil)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.VirtualFolders, 4)\n\tassert.Len(t, user.Filters.AccessTime, 1)\n\n\tuser1, user2, err := dataprovider.GetUserVariants(defaultUsername, \"\")\n\tassert.NoError(t, err)\n\tassert.Len(t, user1.VirtualFolders, 0)\n\tassert.Len(t, user2.VirtualFolders, 4)\n\tassert.Equal(t, int64(0), user1.ExpirationDate)\n\tassert.Equal(t, int64(0), user2.ExpirationDate)\n\tassert.Len(t, user1.Filters.AccessTime, 0)\n\tassert.Len(t, user2.Filters.AccessTime, 1)\n\n\tgroup2.UserSettings.FsConfig = vfs.Filesystem{\n\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\tUsername: defaultUsername,\n\t\t\t},\n\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t},\n\t}\n\tgroup2.UserSettings.Permissions = map[string][]string{\n\t\t\"/\":           {dataprovider.PermListItems, dataprovider.PermDownload},\n\t\t\"/%username%\": {dataprovider.PermListItems},\n\t}\n\tgroup2.UserSettings.DownloadBandwidth = 128\n\tgroup2.UserSettings.UploadBandwidth = 256\n\tgroup2.UserSettings.Filters.PasswordStrength = 70\n\tgroup2.UserSettings.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled, sdk.WebClientMFADisabled}\n\t_, _, err = httpdtest.UpdateGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, err = dataprovider.CheckUserAndPass(defaultUsername, defaultPassword, \"\", common.ProtocolHTTP)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.VirtualFolders, 4)\n\tassert.Equal(t, sdk.LocalFilesystemProvider, user.FsConfig.Provider)\n\tassert.Equal(t, int64(0), user.DownloadBandwidth)\n\tassert.Equal(t, int64(0), user.UploadBandwidth)\n\tassert.Equal(t, 0, user.Filters.PasswordStrength)\n\tassert.Equal(t, []string{dataprovider.PermAny}, user.GetPermissionsForPath(\"/\"))\n\tassert.Equal(t, []string{dataprovider.PermListItems}, user.GetPermissionsForPath(\"/\"+defaultUsername))\n\tassert.Len(t, user.Filters.WebClient, 2)\n\n\tgroup1.UserSettings.FsConfig = vfs.Filesystem{\n\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\tUsername: altAdminUsername,\n\t\t\t\tPrefix:   \"/dirs/%role%/%username%\",\n\t\t\t},\n\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t},\n\t}\n\tgroup1.UserSettings.MaxSessions = 2\n\tgroup1.UserSettings.QuotaFiles = 1000\n\tgroup1.UserSettings.UploadBandwidth = 512\n\tgroup1.UserSettings.DownloadBandwidth = 1024\n\tgroup1.UserSettings.TotalDataTransfer = 2048\n\tgroup1.UserSettings.ExpiresIn = 15\n\tgroup1.UserSettings.Filters.MaxUploadFileSize = 1024 * 1024\n\tgroup1.UserSettings.Filters.StartDirectory = \"/startdir/%username%\"\n\tgroup1.UserSettings.Filters.PasswordStrength = 70\n\tgroup1.UserSettings.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled}\n\tgroup1.UserSettings.Permissions = map[string][]string{\n\t\t\"/\":                  {dataprovider.PermListItems, dataprovider.PermUpload},\n\t\t\"/sub/%username%\":    {dataprovider.PermRename},\n\t\t\"/%role%/%username%\": {dataprovider.PermDelete},\n\t}\n\tgroup1.UserSettings.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/sub2/%role%/%username%test\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{\"*.jpg\", \"*.zip\"},\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, err = dataprovider.CheckUserAndPass(defaultUsername, defaultPassword, \"\", common.ProtocolHTTP)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.VirtualFolders, 4)\n\tassert.Equal(t, user.CreatedAt+int64(group1.UserSettings.ExpiresIn)*86400000, user.ExpirationDate)\n\tassert.Equal(t, group1.UserSettings.Filters.PasswordStrength, user.Filters.PasswordStrength)\n\tassert.Equal(t, sdk.SFTPFilesystemProvider, user.FsConfig.Provider)\n\tassert.Equal(t, altAdminUsername, user.FsConfig.SFTPConfig.Username)\n\tassert.Equal(t, \"/dirs/\"+role.Name+\"/\"+defaultUsername, user.FsConfig.SFTPConfig.Prefix)\n\tassert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermUpload}, user.GetPermissionsForPath(\"/\"))\n\tassert.Equal(t, []string{dataprovider.PermDelete}, user.GetPermissionsForPath(path.Join(\"/\", role.Name, defaultUsername)))\n\tassert.Equal(t, []string{dataprovider.PermRename}, user.GetPermissionsForPath(path.Join(\"/sub\", defaultUsername)))\n\tassert.Equal(t, group1.UserSettings.MaxSessions, user.MaxSessions)\n\tassert.Equal(t, group1.UserSettings.QuotaFiles, user.QuotaFiles)\n\tassert.Equal(t, group1.UserSettings.UploadBandwidth, user.UploadBandwidth)\n\tassert.Equal(t, group1.UserSettings.TotalDataTransfer, user.TotalDataTransfer)\n\tassert.Equal(t, group1.UserSettings.Filters.MaxUploadFileSize, user.Filters.MaxUploadFileSize)\n\tassert.Equal(t, \"/startdir/\"+defaultUsername, user.Filters.StartDirectory)\n\tif assert.Len(t, user.Filters.FilePatterns, 1) {\n\t\tassert.Equal(t, \"/sub2/\"+role.Name+\"/\"+defaultUsername+\"test\", user.Filters.FilePatterns[0].Path) //nolint:goconst\n\t}\n\tif assert.Len(t, user.Filters.WebClient, 2) {\n\t\tassert.Contains(t, user.Filters.WebClient, sdk.WebClientInfoChangeDisabled)\n\t\tassert.Contains(t, user.Filters.WebClient, sdk.WebClientMFADisabled)\n\t}\n\t// Attempt to create a user with a weak password and group1 as the primary group: this should fail\n\tu = getTestUser()\n\tu.Username = rand.Text()\n\tu.Password = defaultPassword\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"insecure password\")\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName3}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestConfigs(t *testing.T) {\n\terr := dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tconfigs, err := dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), configs.UpdatedAt)\n\tassert.Nil(t, configs.SFTPD)\n\tassert.Nil(t, configs.SMTP)\n\tconfigs = dataprovider.Configs{\n\t\tSFTPD: &dataprovider.SFTPDConfigs{},\n\t\tSMTP:  &dataprovider.SMTPConfigs{},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Greater(t, configs.UpdatedAt, int64(0))\n\n\tconfigs = dataprovider.Configs{\n\t\tSFTPD: &dataprovider.SFTPDConfigs{\n\t\t\tCiphers: []string{\"unknown\"},\n\t\t},\n\t\tSMTP: &dataprovider.SMTPConfigs{},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.ErrorIs(t, err, util.ErrValidation)\n\tconfigs = dataprovider.Configs{\n\t\tSFTPD: &dataprovider.SFTPDConfigs{},\n\t\tSMTP: &dataprovider.SMTPConfigs{\n\t\t\tHost: \"smtp.example.com\",\n\t\t\tPort: -1,\n\t\t},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.ErrorIs(t, err, util.ErrValidation)\n\n\tconfigs = dataprovider.Configs{\n\t\tSMTP: &dataprovider.SMTPConfigs{\n\t\t\tHost:       \"mail.example.com\",\n\t\t\tPort:       587,\n\t\t\tUser:       \"test@example.com\",\n\t\t\tAuthType:   3,\n\t\t\tEncryption: 2,\n\t\t\tOAuth2: dataprovider.SMTPOAuth2{\n\t\t\t\tProvider: 1,\n\t\t\t\tTenant:   \"\",\n\t\t\t\tClientID: \"\",\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tif assert.ErrorIs(t, err, util.ErrValidation) {\n\t\tassert.Contains(t, err.Error(), \"smtp oauth2: client id is required\")\n\t}\n\tconfigs.SMTP.OAuth2 = dataprovider.SMTPOAuth2{\n\t\tProvider:     1,\n\t\tClientID:     \"client id\",\n\t\tClientSecret: kms.NewPlainSecret(\"client secret\"),\n\t\tRefreshToken: kms.NewPlainSecret(\"refresh token\"),\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 3, configs.SMTP.AuthType)\n\tassert.Equal(t, 1, configs.SMTP.OAuth2.Provider)\n\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicIPListEntriesHandling(t *testing.T) {\n\tentry := dataprovider.IPListEntry{\n\t\tIPOrNet:     \"::ffff:12.34.56.78\",\n\t\tType:        dataprovider.IPListTypeAllowList,\n\t\tMode:        dataprovider.ListModeAllow,\n\t\tDescription: \"test desc\",\n\t}\n\t_, _, err := httpdtest.GetIPListEntry(entry.IPOrNet, -1, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.AddIPListEntry(entry, http.StatusCreated)\n\tassert.Error(t, err)\n\t// IPv4 address in IPv6 will be converted to standard IPv4\n\tentry1, _, err := httpdtest.GetIPListEntry(\"12.34.56.78/32\", dataprovider.IPListTypeAllowList, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tentry = dataprovider.IPListEntry{\n\t\tIPOrNet: \"192.168.0.0/24\",\n\t\tType:    dataprovider.IPListTypeDefender,\n\t\tMode:    dataprovider.ListModeDeny,\n\t}\n\tentry2, _, err := httpdtest.AddIPListEntry(entry, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// adding the same entry again should fail\n\t_, _, err = httpdtest.AddIPListEntry(entry, http.StatusConflict)\n\tassert.NoError(t, err)\n\t// adding an entry with an invalid IP should fail\n\tentry.IPOrNet = \"not valid\"\n\t_, _, err = httpdtest.AddIPListEntry(entry, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t// adding an entry with an incompatible mode should fail\n\tentry.IPOrNet = entry2.IPOrNet\n\tentry.Mode = -1\n\t_, _, err = httpdtest.AddIPListEntry(entry, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tentry.Type = -1\n\t_, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tentry = dataprovider.IPListEntry{\n\t\tIPOrNet: \"2001:4860:4860::8888/120\",\n\t\tType:    dataprovider.IPListTypeRateLimiterSafeList,\n\t\tMode:    dataprovider.ListModeDeny,\n\t}\n\t_, _, err = httpdtest.AddIPListEntry(entry, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tentry.Mode = dataprovider.ListModeAllow\n\t_, _, err = httpdtest.AddIPListEntry(entry, http.StatusCreated)\n\tassert.NoError(t, err)\n\tentry.Protocols = 3\n\tentry3, _, err := httpdtest.UpdateIPListEntry(entry, http.StatusOK)\n\tassert.NoError(t, err)\n\tentry.Mode = dataprovider.ListModeDeny\n\t_, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\n\tfor _, tt := range []dataprovider.IPListType{dataprovider.IPListTypeAllowList, dataprovider.IPListTypeDefender, dataprovider.IPListTypeRateLimiterSafeList} {\n\t\tentries, _, err := httpdtest.GetIPListEntries(tt, \"\", \"\", dataprovider.OrderASC, 0, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tif assert.Len(t, entries, 1) {\n\t\t\tswitch tt {\n\t\t\tcase dataprovider.IPListTypeAllowList:\n\t\t\t\tassert.Equal(t, entry1, entries[0])\n\t\t\tcase dataprovider.IPListTypeDefender:\n\t\t\t\tassert.Equal(t, entry2, entries[0])\n\t\t\tcase dataprovider.IPListTypeRateLimiterSafeList:\n\t\t\t\tassert.Equal(t, entry3, entries[0])\n\t\t\t}\n\t\t}\n\t}\n\n\t_, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"\", \"\", \"invalid order\", 0, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetIPListEntries(-1, \"\", \"\", dataprovider.OrderASC, 0, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveIPListEntry(entry1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveIPListEntry(entry1, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveIPListEntry(entry2, http.StatusOK)\n\tassert.NoError(t, err)\n\tentry2.Type = -1\n\t_, err = httpdtest.RemoveIPListEntry(entry2, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveIPListEntry(entry3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSearchIPListEntries(t *testing.T) {\n\tentries := []dataprovider.IPListEntry{\n\t\t{\n\t\t\tIPOrNet:   \"192.168.0.0/24\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 0,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"192.168.0.1/24\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 0,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"192.168.0.2/24\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 5,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"192.168.0.3/24\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 8,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"10.8.0.0/24\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 3,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"10.8.1.0/24\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 8,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"10.8.2.0/24\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 1,\n\t\t},\n\t}\n\n\tfor _, e := range entries {\n\t\t_, _, err := httpdtest.AddIPListEntry(e, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\t}\n\n\tresults, _, err := httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"\", \"\", dataprovider.OrderASC, 20, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Equal(t, len(entries), len(results)) {\n\t\tassert.Equal(t, \"10.8.0.0/24\", results[0].IPOrNet)\n\t}\n\tresults, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"\", \"\", dataprovider.OrderDESC, 20, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Equal(t, len(entries), len(results)) {\n\t\tassert.Equal(t, \"192.168.0.3/24\", results[0].IPOrNet)\n\t}\n\tresults, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"\", \"192.168.0.1/24\", dataprovider.OrderASC, 1, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Equal(t, 1, len(results), results) {\n\t\tassert.Equal(t, \"192.168.0.2/24\", results[0].IPOrNet)\n\t}\n\tresults, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"\", \"10.8.2.0/24\", dataprovider.OrderDESC, 1, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Equal(t, 1, len(results), results) {\n\t\tassert.Equal(t, \"10.8.1.0/24\", results[0].IPOrNet)\n\t}\n\tresults, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"10.\", \"\", dataprovider.OrderASC, 20, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 3, len(results))\n\tresults, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"192\", \"\", dataprovider.OrderASC, 20, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 4, len(results))\n\tresults, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"1\", \"\", dataprovider.OrderASC, 20, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 7, len(results))\n\tresults, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeAllowList, \"108\", \"\", dataprovider.OrderASC, 20, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, len(results))\n\n\tfor _, e := range entries {\n\t\t_, err := httpdtest.RemoveIPListEntry(e, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestIPListEntriesValidation(t *testing.T) {\n\tentry := dataprovider.IPListEntry{\n\t\tIPOrNet: \"::ffff:34.56.78.90/120\",\n\t\tType:    -1,\n\t\tMode:    dataprovider.ListModeDeny,\n\t}\n\t_, resp, err := httpdtest.AddIPListEntry(entry, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid list type\")\n\tentry.Type = dataprovider.IPListTypeRateLimiterSafeList\n\t_, resp, err = httpdtest.AddIPListEntry(entry, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid list mode\")\n\tentry.Type = dataprovider.IPListTypeDefender\n\t_, _, err = httpdtest.AddIPListEntry(entry, http.StatusCreated)\n\tassert.Error(t, err)\n\tentry.IPOrNet = \"34.56.78.0/24\"\n\t_, err = httpdtest.RemoveIPListEntry(entry, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicActionRulesHandling(t *testing.T) {\n\tactionName := \"test_action\"\n\ta := dataprovider.BaseEventAction{\n\t\tName:        actionName,\n\t\tDescription: \"test description\",\n\t\tType:        dataprovider.ActionTypeBackup,\n\t\tOptions:     dataprovider.BaseEventActionOptions{},\n\t}\n\taction, _, err := httpdtest.AddEventAction(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// adding the same action should fail\n\t_, _, err = httpdtest.AddEventAction(a, http.StatusConflict)\n\tassert.NoError(t, err)\n\tactionGet, _, err := httpdtest.GetEventActionByName(actionName, http.StatusOK)\n\tassert.NoError(t, err)\n\tactions, _, err := httpdtest.GetEventActions(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, len(actions), 0)\n\tfound := false\n\tfor _, ac := range actions {\n\t\tif ac.Name == actionName {\n\t\t\tassert.Equal(t, actionGet, ac)\n\t\t\tfound = true\n\t\t}\n\t}\n\tassert.True(t, found)\n\ta.Description = \"new description\"\n\ta.Type = dataprovider.ActionTypeDataRetentionCheck\n\ta.Options = dataprovider.BaseEventActionOptions{\n\t\tRetentionConfig: dataprovider.EventActionDataRetentionConfig{\n\t\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t\t{\n\t\t\t\t\tPath:      \"/\",\n\t\t\t\t\tRetention: 144,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPath:      \"/p1\",\n\t\t\t\t\tRetention: 0,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPath:      \"/p2\",\n\t\t\t\t\tRetention: 12,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)\n\tassert.NoError(t, err)\n\ta.Type = dataprovider.ActionTypeCommand\n\ta.Options = dataprovider.BaseEventActionOptions{\n\t\tCmdConfig: dataprovider.EventActionCommandConfig{\n\t\t\tCmd:     filepath.Join(os.TempDir(), \"test_cmd\"),\n\t\t\tTimeout: 20,\n\t\t\tEnvVars: []dataprovider.KeyValue{\n\t\t\t\t{\n\t\t\t\t\tKey:   \"NAME\",\n\t\t\t\t\tValue: \"VALUE\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tdataprovider.EnabledActionCommands = []string{a.Options.CmdConfig.Cmd}\n\tdefer func() {\n\t\tdataprovider.EnabledActionCommands = nil\n\t}()\n\t_, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)\n\tassert.NoError(t, err)\n\t// invalid type\n\ta.Type = 1000\n\t_, _, err = httpdtest.UpdateEventAction(a, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\n\ta.Type = dataprovider.ActionTypeEmail\n\ta.Options = dataprovider.BaseEventActionOptions{\n\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\tRecipients:  []string{\"email@example.com\"},\n\t\t\tBcc:         []string{\"bcc@example.com\"},\n\t\t\tSubject:     \"Event: {{.Event}}\",\n\t\t\tBody:        \"test mail body\",\n\t\t\tAttachments: []string{\"/{{.VirtualPath}}\"},\n\t\t},\n\t}\n\n\t_, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)\n\tassert.NoError(t, err)\n\n\ta.Type = dataprovider.ActionTypeUserInactivityCheck\n\ta.Options = dataprovider.BaseEventActionOptions{\n\t\tUserInactivityConfig: dataprovider.EventActionUserInactivity{\n\t\t\tDisableThreshold: 10,\n\t\t\tDeleteThreshold:  20,\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)\n\tassert.NoError(t, err)\n\n\ta.Type = dataprovider.ActionTypeHTTP\n\ta.Options = dataprovider.BaseEventActionOptions{\n\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\tEndpoint: \"https://localhost:1234\",\n\t\t\tUsername: defaultUsername,\n\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\tHeaders: []dataprovider.KeyValue{\n\t\t\t\t{\n\t\t\t\t\tKey:   \"Content-Type\",\n\t\t\t\t\tValue: \"application/json\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tTimeout:       10,\n\t\t\tSkipTLSVerify: true,\n\t\t\tMethod:        http.MethodPost,\n\t\t\tQueryParameters: []dataprovider.KeyValue{\n\t\t\t\t{\n\t\t\t\t\tKey:   \"a\",\n\t\t\t\t\tValue: \"b\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tBody: `{\"event\":\"{{.Event}}\",\"name\":\"{{.Name}}\"}`,\n\t\t},\n\t}\n\taction, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, action.Options.HTTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, action.Options.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, action.Options.HTTPConfig.Password.GetKey())\n\tassert.Empty(t, action.Options.HTTPConfig.Password.GetAdditionalData())\n\t// update again and check that the password was preserved\n\tdbAction, err := dataprovider.EventActionExists(actionName)\n\tassert.NoError(t, err)\n\taction.Options.HTTPConfig.Password = kms.NewSecret(\n\t\tdbAction.Options.HTTPConfig.Password.GetStatus(),\n\t\tdbAction.Options.HTTPConfig.Password.GetPayload(), \"\", \"\")\n\taction, _, err = httpdtest.UpdateEventAction(action, http.StatusOK)\n\tassert.NoError(t, err)\n\tdbAction, err = dataprovider.EventActionExists(actionName)\n\tassert.NoError(t, err)\n\terr = dbAction.Options.HTTPConfig.Password.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, defaultPassword, dbAction.Options.HTTPConfig.Password.GetPayload())\n\n\tr := dataprovider.EventRule{\n\t\tName:        \"test_rule_name\",\n\t\tStatus:      1,\n\t\tDescription: \"\",\n\t\tTrigger:     dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"upload\"},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tEventStatuses: []int{2, 3},\n\t\t\t\tMinFileSize:   1024 * 1024,\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: actionName,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: false,\n\t\t\t\t\tStopOnFailure:   true,\n\t\t\t\t\tExecuteSync:     true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule, _, err := httpdtest.AddEventRule(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// adding the same rule should fail\n\t_, _, err = httpdtest.AddEventRule(r, http.StatusConflict)\n\tassert.NoError(t, err)\n\n\trule.Description = \"new rule desc\"\n\trule.Trigger = 1000\n\t_, _, err = httpdtest.UpdateEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\trule.Trigger = dataprovider.EventTriggerFsEvent\n\trule, _, err = httpdtest.UpdateEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\n\truleGet, _, err := httpdtest.GetEventRuleByName(rule.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, ruleGet.Actions, 1) {\n\t\tif assert.NotNil(t, ruleGet.Actions[0].BaseEventAction.Options.HTTPConfig.Password) {\n\t\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, ruleGet.Actions[0].BaseEventAction.Options.HTTPConfig.Password.GetStatus())\n\t\t\tassert.NotEmpty(t, ruleGet.Actions[0].BaseEventAction.Options.HTTPConfig.Password.GetPayload())\n\t\t\tassert.Empty(t, ruleGet.Actions[0].BaseEventAction.Options.HTTPConfig.Password.GetKey())\n\t\t\tassert.Empty(t, ruleGet.Actions[0].BaseEventAction.Options.HTTPConfig.Password.GetAdditionalData())\n\t\t}\n\t}\n\trules, _, err := httpdtest.GetEventRules(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, len(rules), 0)\n\tfound = false\n\tfor _, ru := range rules {\n\t\tif ru.Name == rule.Name {\n\t\t\tassert.Equal(t, ruleGet, ru)\n\t\t\tfound = true\n\t\t}\n\t}\n\tassert.True(t, found)\n\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.UpdateEventRule(rule, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetEventRuleByName(rule.Name, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveEventAction(action, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.UpdateEventAction(action, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetEventActionByName(actionName, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action, http.StatusNotFound)\n\tassert.NoError(t, err)\n}\n\nfunc TestActionRuleRelations(t *testing.T) {\n\ta1 := dataprovider.BaseEventAction{\n\t\tName:        \"action1\",\n\t\tDescription: \"test description\",\n\t\tType:        dataprovider.ActionTypeBackup,\n\t\tOptions:     dataprovider.BaseEventActionOptions{},\n\t}\n\ta2 := dataprovider.BaseEventAction{\n\t\tName:    \"action2\",\n\t\tType:    dataprovider.ActionTypeTransferQuotaReset,\n\t\tOptions: dataprovider.BaseEventActionOptions{},\n\t}\n\ta3 := dataprovider.BaseEventAction{\n\t\tName: \"action3\",\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients:  []string{\"test@example.net\"},\n\t\t\t\tContentType: 1,\n\t\t\t\tSubject:     \"test subject\",\n\t\t\t\tBody:        \"test body\",\n\t\t\t},\n\t\t},\n\t}\n\taction1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)\n\tassert.NoError(t, err)\n\taction3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tr1 := dataprovider.EventRule{\n\t\tName:        \"rule1\",\n\t\tDescription: \"\",\n\t\tTrigger:     dataprovider.EventTriggerProviderEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tProviderEvents: []string{\"add\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action1.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tif assert.Len(t, rule1.Actions, 2) {\n\t\tassert.Equal(t, action1.Name, rule1.Actions[0].Name)\n\t\tassert.Equal(t, 1, rule1.Actions[0].Order)\n\t\tassert.Equal(t, action3.Name, rule1.Actions[1].Name)\n\t\tassert.Equal(t, 2, rule1.Actions[1].Order)\n\t\tassert.True(t, rule1.Actions[1].Options.IsFailureAction)\n\t}\n\n\tr2 := dataprovider.EventRule{\n\t\tName:        \"rule2\",\n\t\tDescription: \"\",\n\t\tTrigger:     dataprovider.EventTriggerSchedule,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tSchedules: []dataprovider.Schedule{\n\t\t\t\t{\n\t\t\t\t\tHours:      \"1\",\n\t\t\t\t\tDayOfWeek:  \"*\",\n\t\t\t\t\tDayOfMonth: \"*\",\n\t\t\t\t\tMonth:      \"*\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tRoleNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern:      \"g*\",\n\t\t\t\t\t\tInverseMatch: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action3.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 2,\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tIsFailureAction: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action2.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tif assert.Len(t, rule1.Actions, 2) {\n\t\tassert.Equal(t, action2.Name, rule2.Actions[0].Name)\n\t\tassert.Equal(t, 1, rule2.Actions[0].Order)\n\t\tassert.Equal(t, action3.Name, rule2.Actions[1].Name)\n\t\tassert.Equal(t, 2, rule2.Actions[1].Order)\n\t\tassert.True(t, rule2.Actions[1].Options.IsFailureAction)\n\t}\n\t// check the references\n\taction1, _, err = httpdtest.GetEventActionByName(action1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, action1.Rules, 1)\n\tassert.True(t, slices.Contains(action1.Rules, rule1.Name))\n\taction2, _, err = httpdtest.GetEventActionByName(action2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, action2.Rules, 1)\n\tassert.True(t, slices.Contains(action2.Rules, rule2.Name))\n\taction3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, action3.Rules, 2)\n\tassert.True(t, slices.Contains(action3.Rules, rule1.Name))\n\tassert.True(t, slices.Contains(action3.Rules, rule2.Name))\n\t// referenced actions cannot be removed\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action3, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t// remove action3 from rule2\n\tr2.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: action2.Name,\n\t\t\t},\n\t\t\tOrder: 10,\n\t\t},\n\t}\n\trule2.Status = 1\n\trule2, _, err = httpdtest.UpdateEventRule(r2, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, rule2.Actions, 1) {\n\t\tassert.Equal(t, action2.Name, rule2.Actions[0].Name)\n\t\tassert.Equal(t, 10, rule2.Actions[0].Order)\n\t}\n\t// check the updated relation\n\taction3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, action3.Rules, 1)\n\tassert.True(t, slices.Contains(action3.Rules, rule1.Name))\n\n\t_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)\n\tassert.NoError(t, err)\n\t// no relations anymore\n\taction1, _, err = httpdtest.GetEventActionByName(action1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, action1.Rules, 0)\n\taction2, _, err = httpdtest.GetEventActionByName(action2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, action2.Rules, 0)\n\taction3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, action3.Rules, 0)\n\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestOnDemandEventRules(t *testing.T) {\n\truleName := \"test_on_demand_rule\"\n\ta := dataprovider.BaseEventAction{\n\t\tName:    \"a\",\n\t\tType:    dataprovider.ActionTypeBackup,\n\t\tOptions: dataprovider.BaseEventActionOptions{},\n\t}\n\taction, _, err := httpdtest.AddEventAction(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\tr := dataprovider.EventRule{\n\t\tName:    ruleName,\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerOnDemand,\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: a.Name,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule, _, err := httpdtest.AddEventRule(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RunOnDemandRule(ruleName, http.StatusAccepted)\n\tassert.NoError(t, err)\n\trule.Status = 0\n\t_, _, err = httpdtest.UpdateEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\tresp, err := httpdtest.RunOnDemandRule(ruleName, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"is inactive\")\n\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RunOnDemandRule(ruleName, http.StatusNotFound)\n\tassert.NoError(t, err)\n}\n\nfunc TestIDPLoginEventRule(t *testing.T) {\n\truleName := \"test_IDP_login_rule\"\n\ta := dataprovider.BaseEventAction{\n\t\tName: \"a\",\n\t\tType: dataprovider.ActionTypeIDPAccountCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tIDPConfig: dataprovider.EventActionIDPAccountCheck{\n\t\t\t\tMode:          1,\n\t\t\t\tTemplateUser:  `{\"username\": \"user\"}`,\n\t\t\t\tTemplateAdmin: `{\"username\": \"admin\"}`,\n\t\t\t},\n\t\t},\n\t}\n\taction, resp, err := httpdtest.AddEventAction(a, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tr := dataprovider.EventRule{\n\t\tName:    ruleName,\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerIDPLogin,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tIDPLoginEvent: 1,\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern: \"username\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: a.Name,\n\t\t\t\t},\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\trule, _, err := httpdtest.AddEventRule(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\trule.Status = 0\n\t_, _, err = httpdtest.UpdateEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestEventActionValidation(t *testing.T) {\n\taction := dataprovider.BaseEventAction{\n\t\tName: \"\",\n\t}\n\t_, resp, err := httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"name is mandatory\")\n\taction = dataprovider.BaseEventAction{\n\t\tName: \"n\",\n\t\tType: -1,\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid action type\")\n\taction.Type = dataprovider.ActionTypeHTTP\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"HTTP endpoint is required\")\n\taction.Options.HTTPConfig.Endpoint = \"abc\"\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid HTTP endpoint schema\")\n\taction.Options.HTTPConfig.Endpoint = \"http://localhost\"\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid HTTP timeout\")\n\taction.Options.HTTPConfig.Timeout = 20\n\taction.Options.HTTPConfig.Headers = []dataprovider.KeyValue{\n\t\t{\n\t\t\tKey:   \"\",\n\t\t\tValue: \"\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid HTTP headers\")\n\taction.Options.HTTPConfig.Headers = []dataprovider.KeyValue{\n\t\t{\n\t\t\tKey:   \"Content-Type\",\n\t\t\tValue: \"application/json\",\n\t\t},\n\t}\n\taction.Options.HTTPConfig.Password = kms.NewSecret(sdkkms.SecretStatusRedacted, \"payload\", \"\", \"\")\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"cannot save HTTP configuration with a redacted secret\")\n\taction.Options.HTTPConfig.Password = nil\n\taction.Options.HTTPConfig.Method = http.MethodTrace\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"unsupported HTTP method\")\n\taction.Options.HTTPConfig.Method = http.MethodGet\n\taction.Options.HTTPConfig.QueryParameters = []dataprovider.KeyValue{\n\t\t{\n\t\t\tKey:   \"a\",\n\t\t\tValue: \"\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid HTTP query parameters\")\n\taction.Options.HTTPConfig.QueryParameters = nil\n\taction.Options.HTTPConfig.Parts = []dataprovider.HTTPPart{\n\t\t{\n\t\t\tName: \"\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"HTTP part name is required\")\n\taction.Options.HTTPConfig.Parts = []dataprovider.HTTPPart{\n\t\t{\n\t\t\tName: \"p1\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"HTTP part body is required if no file path is provided\")\n\taction.Options.HTTPConfig.Parts = []dataprovider.HTTPPart{\n\t\t{\n\t\t\tName:     \"p1\",\n\t\t\tFilepath: \"p\",\n\t\t},\n\t}\n\taction.Options.HTTPConfig.Body = \"b\"\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"multipart requests require no body\")\n\taction.Options.HTTPConfig.Body = \"\"\n\taction.Options.HTTPConfig.Headers = []dataprovider.KeyValue{\n\t\t{\n\t\t\tKey:   \"Content-Type\",\n\t\t\tValue: \"application/json\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"content type is automatically set for multipart requests\")\n\n\taction.Type = dataprovider.ActionTypeCommand\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"command is required\")\n\taction.Options.CmdConfig.Cmd = \"relative\"\n\tdataprovider.EnabledActionCommands = []string{action.Options.CmdConfig.Cmd}\n\tdefer func() {\n\t\tdataprovider.EnabledActionCommands = nil\n\t}()\n\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid command, it must be an absolute path\")\n\taction.Options.CmdConfig.Cmd = filepath.Join(os.TempDir(), \"cmd\")\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"is not allowed\")\n\n\tdataprovider.EnabledActionCommands = []string{action.Options.CmdConfig.Cmd}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid command action timeout\")\n\n\taction.Options.CmdConfig.Timeout = 30\n\taction.Options.CmdConfig.EnvVars = []dataprovider.KeyValue{\n\t\t{\n\t\t\tKey: \"k\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid command env vars\")\n\taction.Options.CmdConfig.EnvVars = nil\n\taction.Options.CmdConfig.Args = []string{\"arg1\", \"\"}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid command args\")\n\taction.Options.CmdConfig.Args = nil\n\t// restrict commands\n\tif runtime.GOOS == osWindows {\n\t\tdataprovider.EnabledActionCommands = []string{\"C:\\\\cmd.exe\"}\n\t} else {\n\t\tdataprovider.EnabledActionCommands = []string{\"/bin/sh\"}\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"is not allowed\")\n\tdataprovider.EnabledActionCommands = nil\n\n\taction.Type = dataprovider.ActionTypeEmail\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"at least one email recipient is required\")\n\taction.Options.EmailConfig.Recipients = []string{\"\"}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid email recipients\")\n\taction.Options.EmailConfig.Recipients = []string{\"a@a.com\"}\n\taction.Options.EmailConfig.Bcc = []string{\"\"}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid email bcc\")\n\taction.Options.EmailConfig.Bcc = nil\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"email subject is required\")\n\taction.Options.EmailConfig.Subject = \"subject\"\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"email body is required\")\n\n\taction.Type = dataprovider.ActionTypeDataRetentionCheck\n\taction.Options.RetentionConfig = dataprovider.EventActionDataRetentionConfig{\n\t\tFolders: nil,\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"nothing to delete\")\n\taction.Options.RetentionConfig = dataprovider.EventActionDataRetentionConfig{\n\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:      \"/\",\n\t\t\t\tRetention: 0,\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"nothing to delete\")\n\taction.Options.RetentionConfig = dataprovider.EventActionDataRetentionConfig{\n\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:      \"../path\",\n\t\t\t\tRetention: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tPath:      \"/path\",\n\t\t\t\tRetention: 10,\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"duplicated folder path\")\n\taction.Options.RetentionConfig = dataprovider.EventActionDataRetentionConfig{\n\t\tFolders: []dataprovider.FolderRetention{\n\t\t\t{\n\t\t\t\tPath:      \"p\",\n\t\t\t\tRetention: -1,\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid folder retention\")\n\taction.Type = dataprovider.ActionTypeFilesystem\n\taction.Options.FsConfig = dataprovider.EventActionFilesystemConfig{\n\t\tType: dataprovider.FilesystemActionRename,\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"no path to rename specified\")\n\taction.Options.FsConfig.Renames = []dataprovider.RenameConfig{\n\t\t{\n\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\tKey:   \"\",\n\t\t\t\tValue: \"/adir\",\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid paths to rename\")\n\taction.Options.FsConfig.Renames = []dataprovider.RenameConfig{\n\t\t{\n\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\tKey:   \"adir\",\n\t\t\t\tValue: \"/adir\",\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"rename source and target cannot be equal\")\n\taction.Options.FsConfig.Renames = []dataprovider.RenameConfig{\n\t\t{\n\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\tKey:   \"/\",\n\t\t\t\tValue: \"/dir\",\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"renaming the root directory is not allowed\")\n\taction.Options.FsConfig.Type = dataprovider.FilesystemActionMkdirs\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"no directory to create specified\")\n\taction.Options.FsConfig.MkDirs = []string{\"dir1\", \"\"}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid directory to create\")\n\taction.Options.FsConfig.Type = dataprovider.FilesystemActionDelete\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"no path to delete specified\")\n\taction.Options.FsConfig.Deletes = []string{\"item1\", \"\"}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid path to delete\")\n\taction.Options.FsConfig.Type = dataprovider.FilesystemActionExist\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"no path to check for existence specified\")\n\taction.Options.FsConfig.Exist = []string{\"item1\", \"\"}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid path to check for existence\")\n\taction.Options.FsConfig.Type = dataprovider.FilesystemActionCompress\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"archive name is mandatory\")\n\taction.Options.FsConfig.Compress.Name = \"archive.zip\"\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"no path to compress specified\")\n\taction.Options.FsConfig.Compress.Paths = []string{\"item1\", \"\"}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid path to compress\")\n\taction.Type = dataprovider.ActionTypePasswordExpirationCheck\n\taction.Options.PwdExpirationConfig.Threshold = 0\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"threshold must be greater than 0\")\n\taction.Type = dataprovider.ActionTypeIDPAccountCheck\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"at least a template must be set\")\n\taction.Options.IDPConfig.TemplateAdmin = \"{}\"\n\taction.Options.IDPConfig.Mode = 100\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid account check mode\")\n\taction.Type = dataprovider.ActionTypeUserInactivityCheck\n\taction.Options = dataprovider.BaseEventActionOptions{\n\t\tUserInactivityConfig: dataprovider.EventActionUserInactivity{\n\t\t\tDisableThreshold: 0,\n\t\t\tDeleteThreshold:  0,\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"at least a threshold must be defined\")\n\taction.Options = dataprovider.BaseEventActionOptions{\n\t\tUserInactivityConfig: dataprovider.EventActionUserInactivity{\n\t\t\tDisableThreshold: 10,\n\t\t\tDeleteThreshold:  10,\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"must be greater than deactivation threshold\")\n}\n\nfunc TestEventRuleValidation(t *testing.T) {\n\trule := dataprovider.EventRule{\n\t\tName: \"\",\n\t}\n\t_, resp, err := httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"name is mandatory\")\n\trule.Name = \"r\"\n\trule.Status = 100\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid event rule status\")\n\trule.Status = 1\n\trule.Trigger = 1000\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid event rule trigger\")\n\trule.Trigger = dataprovider.EventTriggerFsEvent\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"at least one filesystem event is required\")\n\trule.Conditions.FsEvents = []string{\"\"}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"unsupported fs event\")\n\trule.Conditions.FsEvents = []string{\"upload\"}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"at least one action is required\")\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action1\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t},\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"\",\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"name not specified\")\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t},\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action\",\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"duplicated action\")\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action11\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t},\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action12\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"duplicated order\")\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action111\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tIsFailureAction: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action112\",\n\t\t\t},\n\t\t\tOrder: 2,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tIsFailureAction: true,\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"at least a non-failure action is required\")\n\trule.Conditions.FsEvents = []string{\"upload\", \"download\"}\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action111\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tExecuteSync: true,\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"sync execution is only supported for upload and pre-* events\")\n\trule.Conditions.FsEvents = []string{\"pre-upload\", \"download\"}\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tExecuteSync: false,\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"requires at least a sync action\")\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tExecuteSync: true,\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"sync execution is only supported for upload and pre-* events\")\n\n\trule.Conditions.FsEvents = []string{\"download\"}\n\trule.Conditions.Options.EventStatuses = []int{3, 2, 8}\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid event_status\")\n\n\trule.Trigger = dataprovider.EventTriggerProviderEvent\n\trule.Actions = []dataprovider.EventAction{\n\t\t{\n\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\tName: \"action1234\",\n\t\t\t},\n\t\t\tOrder: 1,\n\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\tIsFailureAction: false,\n\t\t\t},\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"at least one provider event is required\")\n\trule.Conditions.ProviderEvents = []string{\"\"}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"unsupported provider event\")\n\trule.Conditions.ProviderEvents = []string{\"add\"}\n\trule.Conditions.Options.RoleNames = []dataprovider.ConditionPattern{\n\t\t{\n\t\t\tPattern: \"\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"empty condition pattern not allowed\")\n\trule.Conditions.Options.RoleNames = nil\n\trule.Trigger = dataprovider.EventTriggerSchedule\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"at least one schedule is required\")\n\trule.Conditions.Schedules = []dataprovider.Schedule{\n\t\t{},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid schedule\")\n\trule.Conditions.Schedules = []dataprovider.Schedule{\n\t\t{\n\t\t\tHours:      \"3\",\n\t\t\tDayOfWeek:  \"*\",\n\t\t\tDayOfMonth: \"*\",\n\t\t\tMonth:      \"*\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusInternalServerError)\n\tassert.NoError(t, err, string(resp))\n\trule.Trigger = dataprovider.EventTriggerIDPLogin\n\trule.Conditions.IDPLoginEvent = 100\n\t_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid Identity Provider login event\")\n}\n\nfunc TestUserBandwidthLimits(t *testing.T) {\n\tu := getTestUser()\n\tu.UploadBandwidth = 128\n\tu.DownloadBandwidth = 96\n\tu.Filters.BandwidthLimits = []sdk.BandwidthLimit{\n\t\t{\n\t\t\tSources: []string{\"1\"},\n\t\t},\n\t}\n\t_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"Validation error: could not parse bandwidth limit source\")\n\tu.Filters.BandwidthLimits = []sdk.BandwidthLimit{\n\t\t{\n\t\t\tSources: nil,\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"Validation error: no bandwidth limit source specified\")\n\tu.Filters.BandwidthLimits = []sdk.BandwidthLimit{\n\t\t{\n\t\t\tSources:         []string{\"127.0.0.0/8\", \"::1/128\"},\n\t\t\tUploadBandwidth: 256,\n\t\t},\n\t\t{\n\t\t\tSources:           []string{\"10.0.0.0/8\"},\n\t\t\tUploadBandwidth:   512,\n\t\t\tDownloadBandwidth: 256,\n\t\t},\n\t}\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Len(t, user.Filters.BandwidthLimits, 2)\n\tassert.Equal(t, u.Filters.BandwidthLimits, user.Filters.BandwidthLimits)\n\n\tconnID := xid.New().String()\n\tlocalAddr := \"127.0.0.1\"\n\tup, down := user.GetBandwidthForIP(\"127.0.1.1\", connID)\n\tassert.Equal(t, int64(256), up)\n\tassert.Equal(t, int64(0), down)\n\tconn := common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, \"127.0.1.1\", user)\n\tassert.Equal(t, int64(256), conn.User.UploadBandwidth)\n\tassert.Equal(t, int64(0), conn.User.DownloadBandwidth)\n\tup, down = user.GetBandwidthForIP(\"10.1.2.3\", connID)\n\tassert.Equal(t, int64(512), up)\n\tassert.Equal(t, int64(256), down)\n\tconn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, \"10.2.1.4:1234\", user)\n\tassert.Equal(t, int64(512), conn.User.UploadBandwidth)\n\tassert.Equal(t, int64(256), conn.User.DownloadBandwidth)\n\tup, down = user.GetBandwidthForIP(\"192.168.1.2\", connID)\n\tassert.Equal(t, int64(128), up)\n\tassert.Equal(t, int64(96), down)\n\tconn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, \"172.16.0.1\", user)\n\tassert.Equal(t, int64(128), conn.User.UploadBandwidth)\n\tassert.Equal(t, int64(96), conn.User.DownloadBandwidth)\n\tup, down = user.GetBandwidthForIP(\"invalid\", connID)\n\tassert.Equal(t, int64(128), up)\n\tassert.Equal(t, int64(96), down)\n\tconn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, \"172.16.0\", user)\n\tassert.Equal(t, int64(128), conn.User.UploadBandwidth)\n\tassert.Equal(t, int64(96), conn.User.DownloadBandwidth)\n\n\tuser.Filters.BandwidthLimits = []sdk.BandwidthLimit{\n\t\t{\n\t\t\tSources:           []string{\"10.0.0.0/24\"},\n\t\t\tUploadBandwidth:   256,\n\t\t\tDownloadBandwidth: 512,\n\t\t},\n\t}\n\tuser, resp, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(resp))\n\tif assert.Len(t, user.Filters.BandwidthLimits, 1) {\n\t\tbwLimit := user.Filters.BandwidthLimits[0]\n\t\tassert.Equal(t, []string{\"10.0.0.0/24\"}, bwLimit.Sources)\n\t\tassert.Equal(t, int64(256), bwLimit.UploadBandwidth)\n\t\tassert.Equal(t, int64(512), bwLimit.DownloadBandwidth)\n\t}\n\tup, down = user.GetBandwidthForIP(\"10.1.2.3\", connID)\n\tassert.Equal(t, int64(128), up)\n\tassert.Equal(t, int64(96), down)\n\tconn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, \"172.16.0.2\", user)\n\tassert.Equal(t, int64(128), conn.User.UploadBandwidth)\n\tassert.Equal(t, int64(96), conn.User.DownloadBandwidth)\n\tup, down = user.GetBandwidthForIP(\"10.0.0.26\", connID)\n\tassert.Equal(t, int64(256), up)\n\tassert.Equal(t, int64(512), down)\n\tconn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, \"10.0.0.28\", user)\n\tassert.Equal(t, int64(256), conn.User.UploadBandwidth)\n\tassert.Equal(t, int64(512), conn.User.DownloadBandwidth)\n\n\t// this works if we remove the omitempty tag from BandwidthLimits\n\t/*user.Filters.BandwidthLimits = nil\n\tuser, resp, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(resp))\n\tassert.Len(t, user.Filters.BandwidthLimits, 0)*/\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAccessTimeValidation(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.AccessTime = []sdk.TimePeriod{\n\t\t{\n\t\t\tDayOfWeek: 8,\n\t\t\tFrom:      \"10:00\",\n\t\t\tTo:        \"18:00\",\n\t\t},\n\t}\n\t_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"invalid day of week\")\n\tu.Filters.AccessTime = []sdk.TimePeriod{\n\t\t{\n\t\t\tDayOfWeek: 6,\n\t\t\tFrom:      \"10:00\",\n\t\t\tTo:        \"18\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"invalid time of day\")\n\tu.Filters.AccessTime = []sdk.TimePeriod{\n\t\t{\n\t\t\tDayOfWeek: 6,\n\t\t\tFrom:      \"11:00\",\n\t\t\tTo:        \"10:58\",\n\t\t},\n\t}\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"The end time cannot be earlier than the start time\")\n}\n\nfunc TestUserTimestamps(t *testing.T) {\n\tuser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tcreatedAt := user.CreatedAt\n\tupdatedAt := user.UpdatedAt\n\tassert.Equal(t, int64(0), user.LastLogin)\n\tassert.Equal(t, int64(0), user.FirstDownload)\n\tassert.Equal(t, int64(0), user.FirstUpload)\n\tassert.Greater(t, createdAt, int64(0))\n\tassert.Greater(t, updatedAt, int64(0))\n\tmappedPath := filepath.Join(os.TempDir(), \"mapped_dir\")\n\tfolderName := filepath.Base(mappedPath)\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:       folderName,\n\t\t\tMappedPath: mappedPath,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err = httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttime.Sleep(10 * time.Millisecond)\n\tuser, resp, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, int64(0), user.LastLogin)\n\tassert.Equal(t, int64(0), user.FirstDownload)\n\tassert.Equal(t, int64(0), user.FirstUpload)\n\tassert.Equal(t, createdAt, user.CreatedAt)\n\tassert.Greater(t, user.UpdatedAt, updatedAt)\n\tupdatedAt = user.UpdatedAt\n\t// after a folder update or delete the user updated_at field should change\n\tfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\ttime.Sleep(10 * time.Millisecond)\n\t_, _, err = httpdtest.UpdateFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), user.LastLogin)\n\tassert.Equal(t, int64(0), user.FirstDownload)\n\tassert.Equal(t, int64(0), user.FirstUpload)\n\tassert.Equal(t, createdAt, user.CreatedAt)\n\tassert.Greater(t, user.UpdatedAt, updatedAt)\n\tupdatedAt = user.UpdatedAt\n\ttime.Sleep(10 * time.Millisecond)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), user.LastLogin)\n\tassert.Equal(t, int64(0), user.FirstDownload)\n\tassert.Equal(t, int64(0), user.FirstUpload)\n\tassert.Equal(t, createdAt, user.CreatedAt)\n\tassert.Greater(t, user.UpdatedAt, updatedAt)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminTimestamps(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\tcreatedAt := admin.CreatedAt\n\tupdatedAt := admin.UpdatedAt\n\tassert.Equal(t, int64(0), admin.LastLogin)\n\tassert.Greater(t, createdAt, int64(0))\n\tassert.Greater(t, updatedAt, int64(0))\n\ttime.Sleep(10 * time.Millisecond)\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), admin.LastLogin)\n\tassert.Equal(t, createdAt, admin.CreatedAt)\n\tassert.Greater(t, admin.UpdatedAt, updatedAt)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPUserAuthEmptyPassword(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, \"\")\n\tc := httpclient.GetHTTPClient()\n\tresp, err := c.Do(req)\n\tc.CloseIdleConnections()\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, \"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unexpected status code 401\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPAnonymousUser(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.IsAnonymous = true\n\t_, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.Error(t, err)\n\tuser, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.IsAnonymous)\n\tassert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions[\"/\"])\n\tassert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)\n\tassert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,\n\t\tdataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)\n\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tc := httpclient.GetHTTPClient()\n\tresp, err := c.Do(req)\n\tc.CloseIdleConnections()\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unexpected status code 403\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPUserAuthentication(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tc := httpclient.GetHTTPClient()\n\tresp, err := c.Do(req)\n\tc.CloseIdleConnections()\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\tuserToken := responseHolder[\"access_token\"].(string)\n\tassert.NotEmpty(t, userToken)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// login with wrong credentials\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, \"\")\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, \"wrong pwd\")\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\trespBody, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(respBody), \"invalid credentials\")\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(\"wrong username\", defaultPassword)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\trespBody, err = io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(respBody), \"invalid credentials\")\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, tokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultTokenAuthUser, defaultTokenAuthPass)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponseHolder = make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\tadminToken := responseHolder[\"access_token\"].(string)\n\tassert.NotEmpty(t, adminToken)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, versionPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %v\", adminToken))\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// using the user token should not work\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, versionPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %v\", userToken))\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userLogoutPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %v\", adminToken))\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userLogoutPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %v\", userToken))\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermMFADisabled(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.WebClient = []string{sdk.WebClientMFADisabled}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH}\n\t_, resp, err := httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"you cannot require two-factor authentication and at the same time disallow it\")\n\tuser.Filters.TwoFactorAuthProtocols = nil\n\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tasJSON, err := json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr) // MFA is disabled for this user\n\n\tuser.Filters.WebClient = []string{sdk.WebClientWriteDisabled}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\ttoken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// now we cannot disable MFA for this user\n\tuser.Filters.WebClient = []string{sdk.WebClientMFADisabled}\n\t_, resp, err = httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"two-factor authentication cannot be disabled for a user with an active configuration\")\n\n\tsaveReq := make(map[string]bool)\n\tsaveReq[\"enabled\"] = false\n\tasJSON, err = json.Marshal(saveReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateUserPassword(t *testing.T) {\n\tg := getTestGroup()\n\tg.UserSettings.Filters.PasswordStrength = 20\n\tg.UserSettings.MaxSessions = 10\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.Filters.RequirePasswordChange = true\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlastPwdChange := user.LastPasswordChange\n\ttime.Sleep(100 * time.Millisecond)\n\tnewPwd := \"uaCooGh3pheiShooghah\"\n\terr = dataprovider.UpdateUserPassword(user.Username, newPwd, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t_, err = dataprovider.CheckUserAndPass(user.Username, newPwd, \"\", common.ProtocolHTTP)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, user.Filters.RequirePasswordChange)\n\tassert.NotEqual(t, lastPwdChange, user.LastPasswordChange)\n\t// check that we don't save group overrides\n\tassert.Equal(t, 0, user.MaxSessions)\n\tassert.Equal(t, 0, user.Filters.PasswordStrength)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginRedirectNext(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\turi := webClientFilesPath + \"?path=%2F\" //nolint:goconst\n\treq, err := http.NewRequest(http.MethodGet, uri, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tredirectURI := rr.Header().Get(\"Location\")\n\tassert.Equal(t, webClientLoginPath+\"?next=\"+url.QueryEscape(uri), redirectURI) //nolint:goconst\n\t// render the login page\n\treq, err = http.NewRequest(http.MethodGet, redirectURI, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), fmt.Sprintf(\"action=%q\", redirectURI))\n\t// now login the user and check the redirect\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, loginCookie)\n\tform := getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, redirectURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.RequestURI = redirectURI\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, uri, rr.Header().Get(\"Location\"))\n\t// unsafe URI\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, loginCookie)\n\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\tunsafeURI := webClientLoginPath + \"?next=\" + url.QueryEscape(\"http://example.net\")\n\treq, err = http.NewRequest(http.MethodPost, unsafeURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.RequestURI = unsafeURI\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientFilesPath, rr.Header().Get(\"Location\"))\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, loginCookie)\n\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\tunsupportedURI := webClientLoginPath + \"?next=\" + url.QueryEscape(webClientProfilePath)\n\treq, err = http.NewRequest(http.MethodPost, unsupportedURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.RequestURI = unsupportedURI\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientFilesPath, rr.Header().Get(\"Location\"))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMustChangePasswordRequirement(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.RequirePasswordChange = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.RequirePasswordChange)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, userFilesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Password change required. Please set a new password to continue to use your account\")\n\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdRequired)\n\t// change pwd\n\tpwd := make(map[string]string)\n\tpwd[\"current_password\"] = defaultPassword\n\tpwd[\"new_password\"] = altAdminPassword\n\tasJSON, err := json.Marshal(pwd)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userPwdPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// check that the change pwd bool is changed\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, user.Filters.RequirePasswordChange)\n\t// get a new token\n\ttoken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\twebToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\t// the new token should work\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// check the same as above but changing password from the WebClient UI\n\tuser.Filters.RequirePasswordChange = true\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\twebToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webChangeClientPwdPath, webToken)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"current_password\", altAdminPassword)\n\tform.Set(\"new_password1\", defaultPassword)\n\tform.Set(\"new_password2\", defaultPassword)\n\treq, err = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\n\ttoken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestTwoFactorRequirements(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolFTP}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols\")\n\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError2FARequired)\n\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolHTTP},\n\t}\n\tasJSON, err := json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the following protocols are required\")\n\n\tuserTOTPConfig.Protocols = []string{common.ProtocolHTTP, common.ProtocolFTP}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// now get new tokens and check that the two factor requirements are now met\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", passcode)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\tuserToken := responseHolder[\"access_token\"].(string)\n\tassert.NotEmpty(t, userToken)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userDirsPath), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestTwoFactorRequirementsGroupLevel(t *testing.T) {\n\tg := getTestGroup()\n\tg.UserSettings.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolFTP}\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError2FARequired)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols\")\n\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolHTTP},\n\t}\n\tasJSON, err := json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the following protocols are required\")\n\n\tuserTOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolFTP, common.ProtocolHTTP},\n\t}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// now get new tokens and check that the two factor requirements are now met\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", passcode)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\tuserToken := responseHolder[\"access_token\"].(string)\n\tassert.NotEmpty(t, userToken)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userDirsPath), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminMustChangePasswordRequirement(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin.Filters.RequirePasswordChange = true\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\thttpdtest.SetJWTToken(token)\n\n\t_, _, err = httpdtest.GetUsers(0, 0, http.StatusForbidden)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetStatus(http.StatusForbidden)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.ChangeAdminPassword(altAdminPassword, defaultTokenAuthPass, http.StatusOK)\n\tassert.NoError(t, err)\n\n\thttpdtest.SetJWTToken(\"\")\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.RequirePasswordChange)\n\n\t// get a new token\n\ttoken, _, err = httpdtest.GetToken(altAdminUsername, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\thttpdtest.SetJWTToken(token)\n\n\t_, _, err = httpdtest.GetUsers(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tdesc := xid.New().String()\n\tadmin.Filters.RequirePasswordChange = true\n\tadmin.Filters.RequireTwoFactor = true\n\tadmin.Description = desc\n\t_, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tif assert.Error(t, err) {\n\t\tassert.ErrorContains(t, err, \"require password change mismatch\")\n\t}\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.RequirePasswordChange)\n\tassert.False(t, admin.Filters.RequireTwoFactor)\n\tassert.Equal(t, desc, admin.Description)\n\n\thttpdtest.SetJWTToken(\"\")\n\n\tadmin.Filters.RequirePasswordChange = true\n\t_, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t// test the same for the WebAdmin\n\twebToken, err := getJWTWebTokenFromTestServer(altAdminUsername, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webUsersPath\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t// The change password page should be accessible, we get the CSRF from it.\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webChangeAdminPwdPath, webToken)\n\tassert.NoError(t, err)\n\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"current_password\", defaultTokenAuthPass)\n\tform.Set(\"new_password1\", altAdminPassword)\n\tform.Set(\"new_password2\", altAdminPassword)\n\treq, err = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.RequirePasswordChange)\n\n\twebToken, err = getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webUsersPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminTwoFactorRequirements(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin.Filters.RequireTwoFactor = true\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Two-factor authentication requirements not met\")\n\n\twebToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webFoldersPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webFoldersPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError2FARequiredGeneric)\n\t// add TOTP config\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], altAdminUsername)\n\tassert.NoError(t, err)\n\tadminTOTPConfig := dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t}\n\tasJSON, err := json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, tokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", passcode)\n\treq.SetBasicAuth(altAdminUsername, altAdminPassword)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\ttoken = responseHolder[\"access_token\"].(string)\n\tassert.NotEmpty(t, token)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, serverStatusPath), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// try to disable 2FA\n\tdisableReq := map[string]any{\n\t\t\"enabled\": false,\n\t}\n\tasJSON, err = json.Marshal(disableReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, fmt.Sprintf(\"%v%v\", httpBaseURL, adminTOTPSavePath), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusBadRequest, resp.StatusCode)\n\tbodyResp, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(bodyResp), \"two-factor authentication must be enabled\")\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// try to disable 2FA using the dedicated API\n\treq, err = http.NewRequest(http.MethodPut, fmt.Sprintf(\"%v%v\", httpBaseURL, path.Join(adminPath, altAdminUsername, \"2fa\", \"disable\")), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusBadRequest, resp.StatusCode)\n\tbodyResp, err = io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(bodyResp), \"two-factor authentication must be enabled\")\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// disabling 2FA using another admin should work\n\ttoken, err = getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, path.Join(adminPath, altAdminUsername, \"2fa\", \"disable\"), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// check\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.TOTPConfig.Enabled)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginUserAPITOTP(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolHTTP},\n\t}\n\tasJSON, err := json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// now require HTTP and SSH for TOTP\n\tuser.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolSSH}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t// two factor auth cannot be disabled\n\tconfig := make(map[string]any)\n\tconfig[\"enabled\"] = false\n\tasJSON, err = json.Marshal(config)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"two-factor authentication must be enabled\")\n\t// all the required protocols must be enabled\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the following protocols are required\")\n\t// setting all the required protocols should work\n\tuserTOTPConfig.Protocols = []string{common.ProtocolHTTP, common.ProtocolSSH}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", passcode)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\tuserToken := responseHolder[\"access_token\"].(string)\n\tassert.NotEmpty(t, userToken)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", passcode)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginAdminAPITOTP(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)\n\tassert.NoError(t, err)\n\taltToken, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\tadminTOTPConfig := dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t}\n\tasJSON, err := json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, admin.Filters.RecoveryCodes, 12)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, tokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(altAdminUsername, altAdminPassword)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, tokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", \"passcode\")\n\treq.SetBasicAuth(altAdminUsername, altAdminPassword)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, tokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", passcode)\n\treq.SetBasicAuth(altAdminUsername, altAdminPassword)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\tadminToken := responseHolder[\"access_token\"].(string)\n\tassert.NotEmpty(t, adminToken)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, versionPath), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// get/set recovery codes\n\treq, err = http.NewRequest(http.MethodGet, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// disable two-factor auth\n\tsaveReq := make(map[string]bool)\n\tsaveReq[\"enabled\"] = false\n\tasJSON, err = json.Marshal(saveReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, admin.Filters.RecoveryCodes, 0)\n\t// get/set recovery codes will not work\n\treq, err = http.NewRequest(http.MethodGet, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPStreamZipError(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\tuserToken := responseHolder[\"access_token\"].(string)\n\tassert.NotEmpty(t, userToken)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\tfilesList := []string{\"missing\"}\n\tasJSON, err := json.Marshal(filesList)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, fmt.Sprintf(\"%v%v\", httpBaseURL, userStreamZipPath), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %v\", userToken))\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tif !assert.Error(t, err) { // the connection will be closed\n\t\terr = resp.Body.Close()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicAdminHandling(t *testing.T) {\n\t// we have one admin by default\n\tadmins, _, err := httpdtest.GetAdmins(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(admins), 1)\n\tadmin := getTestAdmin()\n\t// the default admin already exists\n\t_, _, err = httpdtest.AddAdmin(admin, http.StatusConflict)\n\tassert.NoError(t, err)\n\n\tadmin.Username = altAdminUsername\n\tadmin.Filters.Preferences.HideUserPageSections = 1 + 4 + 8\n\tadmin.Filters.Preferences.DefaultUsersExpiration = 30\n\tadmin, _, err = httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.Preferences.HideGroups())\n\tassert.False(t, admin.Filters.Preferences.HideFilesystem())\n\tassert.True(t, admin.Filters.Preferences.HideVirtualFolders())\n\tassert.True(t, admin.Filters.Preferences.HideProfile())\n\tassert.False(t, admin.Filters.Preferences.HideACLs())\n\tassert.False(t, admin.Filters.Preferences.HideDiskQuotaAndBandwidthLimits())\n\tassert.False(t, admin.Filters.Preferences.HideAdvancedSettings())\n\n\tadmin.AdditionalInfo = \"test info\"\n\tadmin.Filters.Preferences.HideUserPageSections = 16 + 32 + 64\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test info\", admin.AdditionalInfo)\n\tassert.False(t, admin.Filters.Preferences.HideGroups())\n\tassert.False(t, admin.Filters.Preferences.HideFilesystem())\n\tassert.False(t, admin.Filters.Preferences.HideVirtualFolders())\n\tassert.False(t, admin.Filters.Preferences.HideProfile())\n\tassert.True(t, admin.Filters.Preferences.HideACLs())\n\tassert.True(t, admin.Filters.Preferences.HideDiskQuotaAndBandwidthLimits())\n\tassert.True(t, admin.Filters.Preferences.HideAdvancedSettings())\n\n\tadmins, _, err = httpdtest.GetAdmins(1, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, admins, 1)\n\tassert.NotEqual(t, admin.Username, admins[0].Username)\n\n\tadmins, _, err = httpdtest.GetAdmins(1, 1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, admins, 1)\n\tassert.Equal(t, admin.Username, admins[0].Username)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username+\"123\", http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\tadmin.Username = defaultTokenAuthUser\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusBadRequest)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminGroups(t *testing.T) {\n\tgroup1 := getTestGroup()\n\tgroup1.Name += \"_1\"\n\tgroup1, _, err := httpdtest.AddGroup(group1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup2 := getTestGroup()\n\tgroup2.Name += \"_2\"\n\tgroup2, _, err = httpdtest.AddGroup(group2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup3 := getTestGroup()\n\tgroup3.Name += \"_3\"\n\tgroup3, _, err = httpdtest.AddGroup(group3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Groups = []dataprovider.AdminGroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsPrimary,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: group2.Name,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsSecondary,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: group3.Name,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsMembership,\n\t\t\t},\n\t\t},\n\t}\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Len(t, admin.Groups, 3)\n\n\tgroups, _, err := httpdtest.GetGroups(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, groups, 3)\n\tfor _, g := range groups {\n\t\tif assert.Len(t, g.Admins, 1) {\n\t\t\tassert.Equal(t, admin.Username, g.Admins[0])\n\t\t}\n\t}\n\n\tadmin, _, err = httpdtest.UpdateAdmin(a, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, admin.Groups, 1)\n\n\t// try to add a missing group\n\tadmin.Groups = []dataprovider.AdminGroupMapping{\n\t\t{\n\t\t\tName: group1.Name,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsPrimary,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: group2.Name,\n\t\t\tOptions: dataprovider.AdminGroupMappingOptions{\n\t\t\t\tAddToUsersAs: dataprovider.GroupAddToUsersAsSecondary,\n\t\t\t},\n\t\t},\n\t}\n\n\tgroup3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group3.Admins, 1)\n\n\t_, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.Error(t, err)\n\tgroup3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group3.Admins, 1)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tgroup3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group3.Admins, 0)\n\n\t_, err = httpdtest.RemoveGroup(group3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestChangeAdminPassword(t *testing.T) {\n\t_, err := httpdtest.ChangeAdminPassword(\"wrong\", defaultTokenAuthPass, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.ChangeAdminPassword(defaultTokenAuthPass, defaultTokenAuthPass, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.ChangeAdminPassword(defaultTokenAuthPass, defaultTokenAuthPass+\"1\", http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.ChangeAdminPassword(defaultTokenAuthPass+\"1\", defaultTokenAuthPass, http.StatusUnauthorized)\n\tassert.NoError(t, err)\n\tadmin, err := dataprovider.AdminExists(defaultTokenAuthUser)\n\tassert.NoError(t, err)\n\tadmin.Password = defaultTokenAuthPass\n\terr = dataprovider.UpdateAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestPasswordValidations(t *testing.T) {\n\tif config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName {\n\t\tt.Skip(\"this test is not supported with the memory provider\")\n\t}\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tproviderConf := config.GetProviderConf()\n\tassert.NoError(t, err)\n\tproviderConf.PasswordValidation.Admins.MinEntropy = 50\n\tproviderConf.PasswordValidation.Users.MinEntropy = 70\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\n\t_, resp, err := httpdtest.AddAdmin(a, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"insecure password\")\n\n\t_, resp, err = httpdtest.AddUser(getTestUser(), http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"insecure password\")\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminPasswordHashing(t *testing.T) {\n\tif config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName {\n\t\tt.Skip(\"this test is not supported with the memory provider\")\n\t}\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tproviderConf := config.GetProviderConf()\n\tassert.NoError(t, err)\n\tproviderConf.PasswordHashing.Algo = dataprovider.HashingAlgoArgon2ID\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tcurrentAdmin, err := dataprovider.AdminExists(defaultTokenAuthUser)\n\tassert.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(currentAdmin.Password, \"$2a$\"))\n\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tnewAdmin, err := dataprovider.AdminExists(altAdminUsername)\n\tassert.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(newAdmin.Password, \"$argon2id$\"))\n\n\ttoken, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\thttpdtest.SetJWTToken(token)\n\t_, _, err = httpdtest.GetStatus(http.StatusOK)\n\tassert.NoError(t, err)\n\n\thttpdtest.SetJWTToken(\"\")\n\t_, _, err = httpdtest.GetStatus(http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\tassert.NoError(t, err)\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestDefaultUsersExpiration(t *testing.T) {\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\ta.Filters.Preferences.DefaultUsersExpiration = 30\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\thttpdtest.SetJWTToken(token)\n\n\t_, _, err = httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.Error(t, err)\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, user.ExpirationDate, int64(0))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Minute))\n\n\t_, _, err = httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, u.ExpirationDate, user.ExpirationDate)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\n\thttpdtest.SetJWTToken(\"\")\n\t_, _, err = httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\t// render the user template page\n\twebToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webTemplateUser, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webTemplateUser+fmt.Sprintf(\"?from=%s\", user.Username), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\thttpdtest.SetJWTToken(token)\n\t_, _, err = httpdtest.AddUser(u, http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\thttpdtest.SetJWTToken(\"\")\n}\n\nfunc TestAdminInvalidCredentials(t *testing.T) {\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, tokenPath), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultTokenAuthUser, defaultTokenAuthPass)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// wrong password\n\treq.SetBasicAuth(defaultTokenAuthUser, \"wrong pwd\")\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, dataprovider.ErrInvalidCredentials.Error(), responseHolder[\"error\"].(string))\n\t// wrong username\n\treq.SetBasicAuth(\"wrong username\", defaultTokenAuthPass)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode)\n\tresponseHolder = make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tassert.NoError(t, err)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, dataprovider.ErrInvalidCredentials.Error(), responseHolder[\"error\"].(string))\n}\n\nfunc TestAdminLastLogin(t *testing.T) {\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), admin.LastLogin)\n\n\t_, _, err = httpdtest.GetToken(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, admin.LastLogin, int64(0))\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminAllowList(t *testing.T) {\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\thttpdtest.SetJWTToken(token)\n\t_, _, err = httpdtest.GetStatus(http.StatusOK)\n\tassert.NoError(t, err)\n\n\thttpdtest.SetJWTToken(\"\")\n\n\tadmin.Password = altAdminPassword\n\tadmin.Filters.AllowList = []string{\"10.6.6.0/32\"}\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, _, err = httpdtest.GetToken(altAdminUsername, altAdminPassword)\n\tassert.EqualError(t, err, \"wrong status code: got 401 want 200\")\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserStatus(t *testing.T) {\n\tu := getTestUser()\n\tu.Status = 3\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Status = 0\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Status = 2\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\tuser.Status = 1\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUidGidLimits(t *testing.T) {\n\tu := getTestUser()\n\tu.UID = math.MaxInt32\n\tu.GID = math.MaxInt32\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, math.MaxInt32, user.GetUID())\n\tassert.Equal(t, math.MaxInt32, user.GetGID())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserNoCredentials(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tu.PublicKeys = []string{}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// this user cannot login with an empty password but it still can use an SSH cert\n\t_, err = getJWTAPITokenFromTestServer(defaultTokenAuthUser, \"\")\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserNoUsername(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = \"\"\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserNoHomeDir(t *testing.T) {\n\tu := getTestUser()\n\tu.HomeDir = \"\"\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserInvalidHomeDir(t *testing.T) {\n\tu := getTestUser()\n\tu.HomeDir = \"relative_path\" //nolint:goconst\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserNoPerms(t *testing.T) {\n\tu := getTestUser()\n\tu.Permissions = make(map[string][]string)\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Permissions[\"/\"] = []string{}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserInvalidEmail(t *testing.T) {\n\tu := getTestUser()\n\tu.Email = \"invalid_email\"\n\t_, body, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(body), \"Validation error: email\")\n}\n\nfunc TestAddUserInvalidPerms(t *testing.T) {\n\tu := getTestUser()\n\tu.Permissions[\"/\"] = []string{\"invalidPerm\"}\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t// permissions for root dir are mandatory\n\tu.Permissions[\"/\"] = []string{}\n\tu.Permissions[\"/somedir\"] = []string{dataprovider.PermAny}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.Permissions[\"/subdir/..\"] = []string{dataprovider.PermAny}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserInvalidFilters(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.AllowedIP = []string{\"192.168.1.0/24\", \"192.168.2.0\"}\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.AllowedIP = []string{}\n\tu.Filters.DeniedIP = []string{\"192.168.3.0/16\", \"invalid\"}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.DeniedIP = []string{}\n\tu.Filters.DeniedLoginMethods = []string{\"invalid\"}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.DeniedLoginMethods = dataprovider.ValidLoginMethods\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificateAndPwd}\n\tu.Filters.DeniedProtocols = dataprovider.ValidProtocols\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.DeniedProtocols = []string{common.ProtocolFTP}\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"relative\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/subdir\",\n\t\t\tAllowedPatterns: []string{\"*.zip\"},\n\t\t\tDeniedPatterns:  []string{},\n\t\t},\n\t\t{\n\t\t\tPath:            \"/subdir\",\n\t\t\tAllowedPatterns: []string{\"*.rar\"},\n\t\t\tDeniedPatterns:  []string{\"*.jpg\"},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"relative\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/subdir\",\n\t\t\tAllowedPatterns: []string{\"*.zip\"},\n\t\t},\n\t\t{\n\t\t\tPath:            \"/subdir\",\n\t\t\tAllowedPatterns: []string{\"*.rar\"},\n\t\t\tDeniedPatterns:  []string{\"*.jpg\"},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/subdir\",\n\t\t\tAllowedPatterns: []string{\"a\\\\\"},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/subdir\",\n\t\t\tAllowedPatterns: []string{\"*.*\"},\n\t\t\tDenyPolicy:      100,\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.DeniedProtocols = []string{\"invalid\"}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.DeniedProtocols = dataprovider.ValidProtocols\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.DeniedProtocols = nil\n\tu.Filters.TLSUsername = \"not a supported attribute\"\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.Filters.TLSUsername = \"\"\n\tu.Filters.WebClient = []string{\"not a valid web client options\"}\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserInvalidFsConfig(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.S3FilesystemProvider\n\tu.FsConfig.S3Config.Bucket = \"\"\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.S3Config.Bucket = \"testbucket\"\n\tu.FsConfig.S3Config.Region = \"eu-west-1\"     //nolint:goconst\n\tu.FsConfig.S3Config.AccessKey = \"access-key\" //nolint:goconst\n\tu.FsConfig.S3Config.AccessSecret = kms.NewSecret(sdkkms.SecretStatusRedacted, \"access-secret\", \"\", \"\")\n\tu.FsConfig.S3Config.Endpoint = \"http://127.0.0.1:9000/path?a=b\"\n\tu.FsConfig.S3Config.StorageClass = \"Standard\" //nolint:goconst\n\tu.FsConfig.S3Config.KeyPrefix = \"/adir/subdir/\"\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.S3Config.AccessSecret.SetStatus(sdkkms.SecretStatusPlain)\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.S3Config.KeyPrefix = \"\"\n\tu.FsConfig.S3Config.UploadPartSize = 3\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.S3Config.UploadPartSize = 5001\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.S3Config.UploadPartSize = 0\n\tu.FsConfig.S3Config.UploadConcurrency = -1\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.S3Config.UploadConcurrency = 0\n\tu.FsConfig.S3Config.DownloadPartSize = -1\n\t_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"download_part_size cannot be\")\n\t}\n\tu.FsConfig.S3Config.DownloadPartSize = 5001\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"download_part_size cannot be\")\n\t}\n\tu.FsConfig.S3Config.DownloadPartSize = 0\n\tu.FsConfig.S3Config.DownloadConcurrency = 100\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid download concurrency\")\n\t}\n\tu.FsConfig.S3Config.DownloadConcurrency = -1\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid download concurrency\")\n\t}\n\tu.FsConfig.S3Config.DownloadConcurrency = 0\n\tu.FsConfig.S3Config.Endpoint = \"\"\n\tu.FsConfig.S3Config.Region = \"\"\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"region cannot be empty\")\n\t}\n\tu = getTestUser()\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"\"\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.GCSConfig.Bucket = \"abucket\"\n\tu.FsConfig.GCSConfig.StorageClass = \"Standard\"\n\tu.FsConfig.GCSConfig.KeyPrefix = \"/somedir/subdir/\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewSecret(sdkkms.SecretStatusRedacted, \"test\", \"\", \"\") //nolint:goconst\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.GCSConfig.Credentials.SetStatus(sdkkms.SecretStatusPlain)\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.GCSConfig.KeyPrefix = \"somedir/subdir/\" //nolint:goconst\n\tu.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()\n\tu.FsConfig.GCSConfig.AutomaticCredentials = 0\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.GCSConfig.Credentials = kms.NewSecret(sdkkms.SecretStatusSecretBox, \"invalid\", \"\", \"\")\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\n\tu = getTestUser()\n\tu.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tu.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret(\"http://foo\\x7f.com/\")\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.AzBlobConfig.SASURL = kms.NewSecret(sdkkms.SecretStatusRedacted, \"key\", \"\", \"\")\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.AzBlobConfig.SASURL = kms.NewEmptySecret()\n\tu.FsConfig.AzBlobConfig.AccountName = \"name\"\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(sdkkms.SecretStatusRedacted, \"key\", \"\", \"\")\n\tu.FsConfig.AzBlobConfig.KeyPrefix = \"/amedir/subdir/\"\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.AzBlobConfig.AccountKey.SetStatus(sdkkms.SecretStatusPlain)\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.AzBlobConfig.KeyPrefix = \"amedir/subdir/\"\n\tu.FsConfig.AzBlobConfig.UploadPartSize = -1\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.AzBlobConfig.UploadPartSize = 101\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\n\tu = getTestUser()\n\tu.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.CryptConfig.Passphrase = kms.NewSecret(sdkkms.SecretStatusRedacted, \"akey\", \"\", \"\")\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu = getTestUser()\n\tu.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.SFTPConfig.Password = kms.NewSecret(sdkkms.SecretStatusRedacted, \"randompkey\", \"\", \"\")\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()\n\tu.FsConfig.SFTPConfig.PrivateKey = kms.NewSecret(sdkkms.SecretStatusRedacted, \"keyforpkey\", \"\", \"\")\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(\"pk\")\n\tu.FsConfig.SFTPConfig.Endpoint = \"127.1.1.1:22\"\n\tu.FsConfig.SFTPConfig.Username = defaultUsername\n\tu.FsConfig.SFTPConfig.BufferSize = -1\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid buffer_size\")\n\t}\n\tu.FsConfig.SFTPConfig.BufferSize = 1000\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid buffer_size\")\n\t}\n\n\tu = getTestUser()\n\tu.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\tu.FsConfig.HTTPConfig.Endpoint = \"http://foo\\x7f.com/\"\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid endpoint\")\n\t}\n\tu.FsConfig.HTTPConfig.Endpoint = \"http://127.0.0.1:9999/api/v1\"\n\tu.FsConfig.HTTPConfig.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, \"\", \"\", \"\")\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid encrypted password\")\n\t}\n\tu.FsConfig.HTTPConfig.Password = nil\n\tu.FsConfig.HTTPConfig.APIKey = kms.NewSecret(sdkkms.SecretStatusRedacted, redactedSecret, \"\", \"\")\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"cannot save a user with a redacted secret\")\n\t}\n\tu.FsConfig.HTTPConfig.APIKey = nil\n\tu.FsConfig.HTTPConfig.Endpoint = \"/api/v1\"\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid endpoint schema\")\n\t}\n\tu.FsConfig.HTTPConfig.Endpoint = \"http://unix?api_prefix=v1\"\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid unix domain socket path\")\n\t}\n\tu.FsConfig.HTTPConfig.Endpoint = \"http://unix?socket_path=test.sock\"\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, string(resp), \"invalid unix domain socket path\")\n\t}\n}\n\nfunc TestUserRedactedPassword(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.S3FilesystemProvider\n\tu.FsConfig.S3Config.Bucket = \"b\"\n\tu.FsConfig.S3Config.Region = \"eu-west-1\"\n\tu.FsConfig.S3Config.AccessKey = \"access-key\"\n\tu.FsConfig.S3Config.RoleARN = \"myRoleARN\"\n\tu.FsConfig.S3Config.AccessSecret = kms.NewSecret(sdkkms.SecretStatusRedacted, \"access-secret\", \"\", \"\")\n\tu.FsConfig.S3Config.Endpoint = \"http://127.0.0.1:9000/path?k=m\"\n\tu.FsConfig.S3Config.StorageClass = \"Standard\"\n\tu.FsConfig.S3Config.ACL = \"bucket-owner-full-control\"\n\t_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"cannot save a user with a redacted secret\")\n\terr = dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"cannot save a user with a redacted secret\")\n\t}\n\tu.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(\"secret\")\n\tu.FsConfig.S3Config.SSECustomerKey = kms.NewSecret(sdkkms.SecretStatusRedacted, \"mysecretkey\", \"\", \"\")\n\t_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err, string(resp))\n\tassert.Contains(t, string(resp), \"cannot save a user with a redacted secret\")\n\n\tu.FsConfig.S3Config.SSECustomerKey = kms.NewPlainSecret(\"key\")\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfolderName := \"folderName\"\n\tvfolder := vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:       folderName,\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"crypted\"),\n\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\t\tPassphrase: kms.NewSecret(sdkkms.SecretStatusRedacted, \"crypted-secret\", \"\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tVirtualPath: \"/avpath\",\n\t}\n\n\tuser.Password = defaultPassword\n\tuser.VirtualFolders = append(user.VirtualFolders, vfolder)\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"cannot save a user with a redacted secret\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserType(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.UserType = string(sdk.UserTypeLDAP)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, string(sdk.UserTypeLDAP), user.Filters.UserType)\n\tuser.Filters.UserType = string(sdk.UserTypeOS)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, string(sdk.UserTypeOS), user.Filters.UserType)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestRetentionAPI(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t})\n\n\tchecks, _, err := httpdtest.GetRetentionChecks(http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, checks, 0)\n\n\tlocalFilePath := filepath.Join(user.HomeDir, \"testdir\", \"testfile\")\n\terr = os.MkdirAll(filepath.Dir(localFilePath), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(localFilePath, []byte(\"test data\"), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tfolderRetention := []dataprovider.FolderRetention{\n\t\t{\n\t\t\tPath:            \"/\",\n\t\t\tRetention:       24,\n\t\t\tDeleteEmptyDirs: true,\n\t\t},\n\t}\n\n\tcheck := common.RetentionCheck{\n\t\tFolders: folderRetention,\n\t}\n\tc := common.RetentionChecks.Add(check, &user)\n\trequire.NotNil(t, c)\n\n\terr = c.Start()\n\trequire.NoError(t, err)\n\n\tassert.Eventually(t, func() bool {\n\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tassert.FileExists(t, localFilePath)\n\n\terr = os.Chtimes(localFilePath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))\n\tassert.NoError(t, err)\n\n\terr = c.Start()\n\trequire.NoError(t, err)\n\n\tassert.Eventually(t, func() bool {\n\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tassert.NoFileExists(t, localFilePath)\n\tassert.NoDirExists(t, filepath.Dir(localFilePath))\n\n\tc = common.RetentionChecks.Add(check, &user)\n\tassert.NotNil(t, c)\n\n\tassert.Nil(t, common.RetentionChecks.Add(check, &user)) // a check for this user is already in progress\n\n\tchecks, _, err = httpdtest.GetRetentionChecks(http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, checks, 1)\n\n\terr = c.Start()\n\tassert.NoError(t, err)\n\n\tassert.Eventually(t, func() bool {\n\t\treturn len(common.RetentionChecks.Get(\"\")) == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tchecks, _, err = httpdtest.GetRetentionChecks(http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, checks, 0)\n}\n\nfunc TestAddUserInvalidVirtualFolders(t *testing.T) {\n\tu := getTestUser()\n\tfolderName := \"fname\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir\"),\n\t\t\tName:       folderName,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir1\"),\n\t\t\tName:       folderName + \"1\",\n\t\t},\n\t\tVirtualPath: \"/vdir\", // invalid, already defined\n\t})\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = nil\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir\"),\n\t\t\tName:       folderName,\n\t\t},\n\t\tVirtualPath: \"/vdir1\",\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir\"),\n\t\t\tName:       folderName, // invalid, unique constraint (user.id, folder.id) violated\n\t\t},\n\t\tVirtualPath: \"/vdir2\",\n\t})\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = nil\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir1\"),\n\t\t\tName:       folderName + \"1\",\n\t\t},\n\t\tVirtualPath: \"/vdir1/\",\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  1, // invvalid, we cannot have -1 and > 0\n\t})\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = nil\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir1\"),\n\t\t\tName:       folderName + \"1\",\n\t\t},\n\t\tVirtualPath: \"/vdir1/\",\n\t\tQuotaSize:   1,\n\t\tQuotaFiles:  -1,\n\t})\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = nil\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir1\"),\n\t\t\tName:       folderName + \"1\",\n\t\t},\n\t\tVirtualPath: \"/vdir1/\",\n\t\tQuotaSize:   -2, // invalid\n\t\tQuotaFiles:  0,\n\t})\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = nil\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir1\"),\n\t\t\tName:       folderName + \"1\",\n\t\t},\n\t\tVirtualPath: \"/vdir1/\",\n\t\tQuotaSize:   0,\n\t\tQuotaFiles:  -2, // invalid\n\t})\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = nil\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped_dir\"),\n\t\t},\n\t\tVirtualPath: \"/vdir1\",\n\t})\n\t// folder name is mandatory\n\t_, _, err = httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserPublicKey(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tinvalidPubKey := \"invalid\"\n\tu.PublicKeys = []string{invalidPubKey}\n\t_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.PublicKeys = []string{testPubKey}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tdbUser, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.Empty(t, dbUser.Password)\n\tassert.False(t, dbUser.IsPasswordHashed())\n\n\tuser.PublicKeys = []string{testPubKey, invalidPubKey}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\tuser.PublicKeys = []string{testPubKey, testPubKey, testPubKey}\n\tuser.Password = defaultPassword\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tdbUser, err = dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, dbUser.Password)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t// DSA keys are not accepted\n\tu = getTestUser()\n\tu.Password = \"\"\n\tu.PublicKeys = []string{\"ssh-dss AAAAB3NzaC1kc3MAAACBAK+BKLZs1Vd0cWYOquKfp++0ml9hkzB7UDRozT3nhRcyHcuwASsXiVTqsg96oGjBcUUy076CXlsfJEXE2P0dF6tt1wvABPMwKpOn+kIrfJ0j93X2c2KIZNlD4YuNUJjLHu1DvgQHw8NMps6l5D0M5NFCRdD3NYhI5zFVJJ4CzikrAAAAFQCRBagw7gEbs0gd8So7OLMcSVzs/wAAAIBjuo7U9q8npchQ3otgCvj0xIwsQ+Fi9bH0SBceqbCcVzFYY6JXSQ0XmwHs+0AuvRCPIGaBdfcm+w+9YOxREtdEVjcmkYlfJpTaVljjWcWFWTQddbiamZhQ/xLU9CNLK4oYLwIGLZjCcG7nRDdLtLQdBFuzP/faEi3TD2BK114QmAAAAIEAj1n34pH2WKwbSZhzmz/OG0VzqJICFWboiM44LZl2AqcRBvEEycdHlGe2IKaj5lEtLgBKJt9NSFhBIzWh7gcEzSMlkiDecdYSFlDc4snmTiXaoiIehV59nTY6gc8GLWCzuem+WdHxvJ4yOSWF9k+a+Y+/v/35shNLkfokViOlN7k=\"}\n\t_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"DSA key format is insecure and it is not allowed\")\n}\n\nfunc TestUpdateUserEmptyPassword(t *testing.T) {\n\tu := getTestUser()\n\tu.PublicKeys = []string{testPubKey}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\t// the password is not empty\n\tdbUser, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, dbUser.Password)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\t// now update the user and set an empty password\n\tdata, err := json.Marshal(dbUser)\n\tassert.NoError(t, err)\n\tvar customUser map[string]any\n\terr = json.Unmarshal(data, &customUser)\n\tassert.NoError(t, err)\n\tcustomUser[\"password\"] = \"\"\n\tasJSON, err := json.Marshal(customUser)\n\tassert.NoError(t, err)\n\tuserNoPwd, _, err := httpdtest.UpdateUserWithJSON(user, http.StatusOK, \"\", asJSON)\n\tassert.NoError(t, err)\n\tassert.Equal(t, user.Password, userNoPwd.Password) // the password is hidden\n\t// check the password within the data provider\n\tdbUser, err = dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.Empty(t, dbUser.Password)\n\tassert.False(t, dbUser.IsPasswordHashed())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateUserNoPassword(t *testing.T) {\n\tu := getTestUser()\n\tu.PublicKeys = []string{testPubKey}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// the password is not empty\n\tdbUser, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, dbUser.Password)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\t// now update the user and remove the password field, old password should be preserved\n\tuser.Password = \"\" // password has the omitempty tag\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t// the password is preserved\n\tdbUser, err = dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, dbUser.Password)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateUser(t *testing.T) {\n\tu := getTestUser()\n\tu.UsedQuotaFiles = 1\n\tu.UsedQuotaSize = 2\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tu.Filters.Hooks.CheckPasswordDisabled = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\tuser.HomeDir = filepath.Join(homeBasePath, \"testmod\")\n\tuser.UID = 33\n\tuser.GID = 101\n\tuser.MaxSessions = 10\n\tuser.QuotaSize = 4096\n\tuser.QuotaFiles = 2\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}\n\tuser.Permissions[\"/subdir\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload}\n\tuser.Filters.AllowedIP = []string{\"192.168.1.0/24\", \"192.168.2.0/24\"}\n\tuser.Filters.DeniedIP = []string{\"192.168.3.0/24\", \"192.168.4.0/24\"}\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}\n\tuser.Filters.TLSUsername = sdk.TLSUsernameNone\n\tuser.Filters.Hooks.ExternalAuthDisabled = true\n\tuser.Filters.Hooks.PreLoginDisabled = true\n\tuser.Filters.Hooks.CheckPasswordDisabled = false\n\tuser.Filters.DisableFsChecks = true\n\tuser.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{\n\t\tPath:            \"/subdir\",\n\t\tAllowedPatterns: []string{\"*.zip\", \"*.rar\"},\n\t\tDeniedPatterns:  []string{\"*.jpg\", \"*.png\"},\n\t\tDenyPolicy:      sdk.DenyPolicyHide,\n\t})\n\tuser.Filters.MaxUploadFileSize = 4096\n\tuser.UploadBandwidth = 1024\n\tuser.DownloadBandwidth = 512\n\tuser.VirtualFolders = nil\n\tmappedPath1 := filepath.Join(os.TempDir(), \"mapped_dir1\")\n\tmappedPath2 := filepath.Join(os.TempDir(), \"mapped_dir2\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tfolderName2 := filepath.Base(mappedPath2)\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: \"/vdir1\",\n\t})\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: \"/vdir12/subdir\",\n\t\tQuotaSize:   123,\n\t\tQuotaFiles:  2,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, \"invalid\")\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"0\")\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"1\")\n\tassert.NoError(t, err)\n\tuser.Permissions[\"/subdir\"] = []string{}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Permissions[\"/subdir\"], 0)\n\tassert.Len(t, user.VirtualFolders, 2)\n\tfor _, folder := range user.VirtualFolders {\n\t\tassert.Greater(t, folder.ID, int64(0))\n\t\tif folder.VirtualPath == \"/vdir12/subdir\" {\n\t\t\tassert.Equal(t, int64(123), folder.QuotaSize)\n\t\t\tassert.Equal(t, 2, folder.QuotaFiles)\n\t\t}\n\t}\n\tfolder, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Contains(t, folder.Users, user.Username)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t// removing the user must remove folder mapping\n\tfolder, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 0)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 0)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateUserTransferQuotaUsage(t *testing.T) {\n\tu := getTestUser()\n\tusedDownloadDataTransfer := int64(2 * 1024 * 1024)\n\tusedUploadDataTransfer := int64(1024 * 1024)\n\tu.UsedDownloadDataTransfer = usedDownloadDataTransfer\n\tu.UsedUploadDataTransfer = usedUploadDataTransfer\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), user.UsedUploadDataTransfer)\n\tassert.Equal(t, int64(0), user.UsedDownloadDataTransfer)\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"invalid_mode\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"\", http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedUploadDataTransfer, user.UsedUploadDataTransfer)\n\tassert.Equal(t, usedDownloadDataTransfer, user.UsedDownloadDataTransfer)\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"add\", http.StatusBadRequest)\n\tassert.NoError(t, err, \"user has no transfer quota restrictions add mode should fail\")\n\tuser.TotalDataTransfer = 100\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"add\", http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2*usedUploadDataTransfer, user.UsedUploadDataTransfer)\n\tassert.Equal(t, 2*usedDownloadDataTransfer, user.UsedDownloadDataTransfer)\n\tu.UsedDownloadDataTransfer = -1\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"add\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.UsedDownloadDataTransfer = usedDownloadDataTransfer\n\tu.Username += \"1\"\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"\", http.StatusNotFound)\n\tassert.NoError(t, err)\n\tu.Username = defaultUsername\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"\", http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedUploadDataTransfer, user.UsedUploadDataTransfer)\n\tassert.Equal(t, usedDownloadDataTransfer, user.UsedDownloadDataTransfer)\n\tu.UsedDownloadDataTransfer = 0\n\tu.UsedUploadDataTransfer = 1\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"add\", http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedUploadDataTransfer+1, user.UsedUploadDataTransfer)\n\tassert.Equal(t, usedDownloadDataTransfer, user.UsedDownloadDataTransfer)\n\tu.UsedDownloadDataTransfer = 1\n\tu.UsedUploadDataTransfer = 0\n\t_, err = httpdtest.UpdateTransferQuotaUsage(u, \"add\", http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedUploadDataTransfer+1, user.UsedUploadDataTransfer)\n\tassert.Equal(t, usedDownloadDataTransfer+1, user.UsedDownloadDataTransfer)\n\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"users\", u.Username, \"transfer-usage\"),\n\t\tbytes.NewBuffer([]byte(`not a json`)))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateUserQuotaUsage(t *testing.T) {\n\tu := getTestUser()\n\tusedQuotaFiles := 1\n\tusedQuotaSize := int64(65535)\n\tu.UsedQuotaFiles = usedQuotaFiles\n\tu.UsedQuotaSize = usedQuotaSize\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t_, err = httpdtest.UpdateQuotaUsage(u, \"invalid_mode\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.UpdateQuotaUsage(u, \"\", http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize, user.UsedQuotaSize)\n\t_, err = httpdtest.UpdateQuotaUsage(u, \"add\", http.StatusBadRequest)\n\tassert.NoError(t, err, \"user has no quota restrictions add mode should fail\")\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize, user.UsedQuotaSize)\n\tuser.QuotaFiles = 100\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = httpdtest.UpdateQuotaUsage(u, \"add\", http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2*usedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, 2*usedQuotaSize, user.UsedQuotaSize)\n\tu.UsedQuotaFiles = -1\n\t_, err = httpdtest.UpdateQuotaUsage(u, \"\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tu.UsedQuotaFiles = usedQuotaFiles\n\tu.Username = u.Username + \"1\"\n\t_, err = httpdtest.UpdateQuotaUsage(u, \"\", http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserFolderMapping(t *testing.T) {\n\tmappedPath1 := filepath.Join(os.TempDir(), \"mapped_dir1\")\n\tmappedPath2 := filepath.Join(os.TempDir(), \"mapped_dir2\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tfolderName2 := filepath.Base(mappedPath2)\n\tu1 := getTestUser()\n\tu1.VirtualFolders = append(u1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:            folderName1,\n\t\tMappedPath:      mappedPath1,\n\t\tUsedQuotaFiles:  2,\n\t\tUsedQuotaSize:   123,\n\t\tLastQuotaUpdate: 456,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser1, _, err := httpdtest.AddUser(u1, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// virtual folder must be auto created\n\tfolder, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Contains(t, folder.Users, user1.Username)\n\tassert.Equal(t, 2, folder.UsedQuotaFiles)\n\tassert.Equal(t, int64(123), folder.UsedQuotaSize)\n\tassert.Equal(t, int64(456), folder.LastQuotaUpdate)\n\tassert.Equal(t, 2, user1.VirtualFolders[0].UsedQuotaFiles)\n\tassert.Equal(t, int64(123), user1.VirtualFolders[0].UsedQuotaSize)\n\tassert.Equal(t, int64(456), user1.VirtualFolders[0].LastQuotaUpdate)\n\n\tu2 := getTestUser()\n\tu2.Username = defaultUsername + \"2\"\n\tu2.VirtualFolders = append(u2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:       folderName1,\n\t\t\tMappedPath: mappedPath1,\n\t\t},\n\t\tVirtualPath: \"/vdir1\",\n\t\tQuotaSize:   0,\n\t\tQuotaFiles:  0,\n\t})\n\tu2.VirtualFolders = append(u2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:       folderName2,\n\t\t\tMappedPath: mappedPath2,\n\t\t},\n\t\tVirtualPath: \"/vdir2\",\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tuser2, _, err := httpdtest.AddUser(u2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Contains(t, folder.Users, user2.Username)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 2)\n\tassert.Contains(t, folder.Users, user1.Username)\n\tassert.Contains(t, folder.Users, user2.Username)\n\t// now update user2 removing mappedPath1\n\tuser2.VirtualFolders = nil\n\tuser2.VirtualFolders = append(user2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:           folderName2,\n\t\t\tMappedPath:     mappedPath2,\n\t\t\tUsedQuotaFiles: 2,\n\t\t\tUsedQuotaSize:  123,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t\tQuotaSize:   0,\n\t\tQuotaFiles:  0,\n\t})\n\tuser2, _, err = httpdtest.UpdateUser(user2, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Contains(t, folder.Users, user2.Username)\n\tassert.Equal(t, 0, folder.UsedQuotaFiles)\n\tassert.Equal(t, int64(0), folder.UsedQuotaSize)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Contains(t, folder.Users, user1.Username)\n\t// add mappedPath1 again to user2\n\tuser2.VirtualFolders = append(user2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:       folderName1,\n\t\t\tMappedPath: mappedPath1,\n\t\t},\n\t\tVirtualPath: \"/vdir1\",\n\t})\n\tuser2, _, err = httpdtest.UpdateUser(user2, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Contains(t, folder.Users, user2.Username)\n\t// removing virtual folders should clear relations on both side\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser2, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user2.VirtualFolders, 1) {\n\t\tfolder := user2.VirtualFolders[0]\n\t\tassert.Equal(t, mappedPath1, folder.MappedPath)\n\t\tassert.Equal(t, folderName1, folder.Name)\n\t}\n\tuser1, _, err = httpdtest.GetUserByUsername(user1.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user2.VirtualFolders, 1) {\n\t\tfolder := user2.VirtualFolders[0]\n\t\tassert.Equal(t, mappedPath1, folder.MappedPath)\n\t}\n\n\tfolder, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 2)\n\t// removing a user should clear virtual folder mapping\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Contains(t, folder.Users, user2.Username)\n\t// removing a folder should clear mapping on the user side too\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser2, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user2.VirtualFolders, 0)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserS3Config(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.S3FilesystemProvider\n\tuser.FsConfig.S3Config.Bucket = \"test\" //nolint:goconst\n\tuser.FsConfig.S3Config.AccessKey = \"Server-Access-Key\"\n\tuser.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(\"Server-Access-Secret\")\n\tuser.FsConfig.S3Config.SSECustomerKey = kms.NewPlainSecret(\"SSE-encryption-key\")\n\tuser.FsConfig.S3Config.RoleARN = \"myRoleARN\"\n\tuser.FsConfig.S3Config.Endpoint = \"http://127.0.0.1:9000\"\n\tuser.FsConfig.S3Config.UploadPartSize = 8\n\tuser.FsConfig.S3Config.DownloadPartMaxTime = 60\n\tuser.FsConfig.S3Config.UploadPartMaxTime = 40\n\tuser.FsConfig.S3Config.ForcePathStyle = true\n\tuser.FsConfig.S3Config.SkipTLSVerify = true\n\tuser.FsConfig.S3Config.DownloadPartSize = 6\n\tfolderName := \"vfolderName\"\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: \"/folderPath\",\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: filepath.Join(os.TempDir(), \"folderName\"),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(\"Crypted-Secret\"),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, body, err := httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(body))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus())\n\tassert.NotEmpty(t, user.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.S3Config.SSECustomerKey.GetStatus())\n\tassert.NotEmpty(t, user.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\tassert.Empty(t, user.FsConfig.S3Config.SSECustomerKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.S3Config.SSECustomerKey.GetKey())\n\tassert.Equal(t, 60, user.FsConfig.S3Config.DownloadPartMaxTime)\n\tassert.Equal(t, 40, user.FsConfig.S3Config.UploadPartMaxTime)\n\tassert.True(t, user.FsConfig.S3Config.SkipTLSVerify)\n\tif assert.Len(t, user.VirtualFolders, 1) {\n\t\tfolder := user.VirtualFolders[0]\n\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.CryptConfig.Passphrase.GetStatus())\n\t\tassert.NotEmpty(t, folder.FsConfig.CryptConfig.Passphrase.GetPayload())\n\t\tassert.Empty(t, folder.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\t\tassert.Empty(t, folder.FsConfig.CryptConfig.Passphrase.GetKey())\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, folder.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tassert.Empty(t, folder.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.Empty(t, folder.FsConfig.CryptConfig.Passphrase.GetKey())\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.ID = 0\n\tuser.CreatedAt = 0\n\tuser.VirtualFolders = nil\n\tuser.FsConfig.S3Config.SSECustomerKey = kms.NewEmptySecret()\n\tsecret := kms.NewSecret(sdkkms.SecretStatusSecretBox, \"Server-Access-Secret\", \"\", \"\")\n\tuser.FsConfig.S3Config.AccessSecret = secret\n\t_, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.Error(t, err)\n\tuser.FsConfig.S3Config.AccessSecret.SetStatus(sdkkms.SecretStatusPlain)\n\tuser, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tinitialSecretPayload := user.FsConfig.S3Config.AccessSecret.GetPayload()\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus())\n\tassert.NotEmpty(t, initialSecretPayload)\n\tassert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey())\n\tuser.FsConfig.Provider = sdk.S3FilesystemProvider\n\tuser.FsConfig.S3Config.Bucket = \"test-bucket\"\n\tuser.FsConfig.S3Config.Region = \"us-east-1\" //nolint:goconst\n\tuser.FsConfig.S3Config.AccessKey = \"Server-Access-Key1\"\n\tuser.FsConfig.S3Config.Endpoint = \"http://localhost:9000\"\n\tuser.FsConfig.S3Config.KeyPrefix = \"somedir/subdir\" //nolint:goconst\n\tuser.FsConfig.S3Config.UploadConcurrency = 5\n\tuser.FsConfig.S3Config.DownloadConcurrency = 4\n\tuser, bb, err := httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(bb))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus())\n\tassert.Equal(t, initialSecretPayload, user.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey())\n\t// test user without access key and access secret (shared config state)\n\tuser.FsConfig.Provider = sdk.S3FilesystemProvider\n\tuser.FsConfig.S3Config.Bucket = \"testbucket\"\n\tuser.FsConfig.S3Config.Region = \"us-east-1\"\n\tuser.FsConfig.S3Config.AccessKey = \"\"\n\tuser.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()\n\tuser.FsConfig.S3Config.Endpoint = \"\"\n\tuser.FsConfig.S3Config.KeyPrefix = \"somedir/subdir\"\n\tuser.FsConfig.S3Config.UploadPartSize = 6\n\tuser.FsConfig.S3Config.UploadConcurrency = 4\n\tuser, body, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(body))\n\tassert.Nil(t, user.FsConfig.S3Config.AccessSecret)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.ID = 0\n\tuser.CreatedAt = 0\n\t// shared credential test for add instead of update\n\tuser, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Nil(t, user.FsConfig.S3Config.AccessSecret)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPFsConfig(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\tuser.FsConfig.HTTPConfig = vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint: \"http://127.0.0.1/httpfs\",\n\t\t\tUsername: defaultUsername,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\tAPIKey:   kms.NewPlainSecret(defaultTokenAuthUser),\n\t}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tinitialPwdPayload := user.FsConfig.HTTPConfig.Password.GetPayload()\n\tinitialAPIKeyPayload := user.FsConfig.HTTPConfig.APIKey.GetPayload()\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, initialPwdPayload)\n\tassert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())\n\tassert.NotEmpty(t, initialAPIKeyPayload)\n\tassert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())\n\tuser.FsConfig.HTTPConfig.Password.SetStatus(sdkkms.SecretStatusSecretBox)\n\tuser.FsConfig.HTTPConfig.Password.SetAdditionalData(util.GenerateUniqueID())\n\tuser.FsConfig.HTTPConfig.Password.SetKey(util.GenerateUniqueID())\n\tuser.FsConfig.HTTPConfig.APIKey.SetStatus(sdkkms.SecretStatusSecretBox)\n\tuser.FsConfig.HTTPConfig.APIKey.SetAdditionalData(util.GenerateUniqueID())\n\tuser.FsConfig.HTTPConfig.APIKey.SetKey(util.GenerateUniqueID())\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.Equal(t, initialPwdPayload, user.FsConfig.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())\n\tassert.Equal(t, initialAPIKeyPayload, user.FsConfig.HTTPConfig.APIKey.GetPayload())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t// also test AddUser\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\tu.FsConfig.HTTPConfig = vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint: \"http://127.0.0.1/httpfs\",\n\t\t\tUsername: defaultUsername,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\tAPIKey:   kms.NewPlainSecret(defaultTokenAuthUser),\n\t}\n\tuser, _, err = httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, user.FsConfig.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())\n\tassert.NotEmpty(t, user.FsConfig.HTTPConfig.APIKey.GetPayload())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserAzureBlobConfig(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tuser.FsConfig.AzBlobConfig.Container = \"test\"\n\tuser.FsConfig.AzBlobConfig.AccountName = \"Server-Account-Name\"\n\tuser.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(\"Server-Account-Key\")\n\tuser.FsConfig.AzBlobConfig.Endpoint = \"http://127.0.0.1:9000\"\n\tuser.FsConfig.AzBlobConfig.UploadPartSize = 8\n\tuser.FsConfig.AzBlobConfig.DownloadPartSize = 6\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tinitialPayload := user.FsConfig.AzBlobConfig.AccountKey.GetPayload()\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.NotEmpty(t, initialPayload)\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\tuser.FsConfig.AzBlobConfig.AccountKey.SetStatus(sdkkms.SecretStatusSecretBox)\n\tuser.FsConfig.AzBlobConfig.AccountKey.SetAdditionalData(\"data\")\n\tuser.FsConfig.AzBlobConfig.AccountKey.SetKey(\"fake key\")\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.Equal(t, initialPayload, user.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.ID = 0\n\tuser.CreatedAt = 0\n\tsecret := kms.NewSecret(sdkkms.SecretStatusSecretBox, \"Server-Account-Key\", \"\", \"\")\n\tuser.FsConfig.AzBlobConfig.AccountKey = secret\n\t_, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.Error(t, err)\n\tuser.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(\"Server-Account-Key-Test\")\n\tuser, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.NoError(t, err)\n\tinitialPayload = user.FsConfig.AzBlobConfig.AccountKey.GetPayload()\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.NotEmpty(t, initialPayload)\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\tuser.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tuser.FsConfig.AzBlobConfig.Container = \"test-container\"\n\tuser.FsConfig.AzBlobConfig.Endpoint = \"http://localhost:9001\"\n\tuser.FsConfig.AzBlobConfig.KeyPrefix = \"somedir/subdir\"\n\tuser.FsConfig.AzBlobConfig.UploadConcurrency = 5\n\tuser.FsConfig.AzBlobConfig.DownloadConcurrency = 4\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.NotEmpty(t, initialPayload)\n\tassert.Equal(t, initialPayload, user.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\t// test user without access key and access secret (SAS)\n\tuser.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tuser.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret(\"https://myaccount.blob.core.windows.net/pictures/profile.jpg?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d\")\n\tuser.FsConfig.AzBlobConfig.KeyPrefix = \"somedir/subdir\"\n\tuser.FsConfig.AzBlobConfig.AccountName = \"\"\n\tuser.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()\n\tuser.FsConfig.AzBlobConfig.UploadPartSize = 6\n\tuser.FsConfig.AzBlobConfig.UploadConcurrency = 4\n\tuser.FsConfig.AzBlobConfig.DownloadPartSize = 3\n\tuser.FsConfig.AzBlobConfig.DownloadConcurrency = 5\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Nil(t, user.FsConfig.AzBlobConfig.AccountKey)\n\tassert.NotNil(t, user.FsConfig.AzBlobConfig.SASURL)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.ID = 0\n\tuser.CreatedAt = 0\n\t// sas test for add instead of update\n\tuser.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{\n\t\tBaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{\n\t\t\tContainer: user.FsConfig.AzBlobConfig.Container,\n\t\t},\n\t\tSASURL: kms.NewPlainSecret(\"http://127.0.0.1/fake/sass/url\"),\n\t}\n\tuser, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Nil(t, user.FsConfig.AzBlobConfig.AccountKey)\n\tinitialPayload = user.FsConfig.AzBlobConfig.SASURL.GetPayload()\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.SASURL.GetStatus())\n\tassert.NotEmpty(t, initialPayload)\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.SASURL.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.SASURL.GetKey())\n\tuser.FsConfig.AzBlobConfig.SASURL.SetStatus(sdkkms.SecretStatusSecretBox)\n\tuser.FsConfig.AzBlobConfig.SASURL.SetAdditionalData(\"data\")\n\tuser.FsConfig.AzBlobConfig.SASURL.SetKey(\"fake key\")\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.SASURL.GetStatus())\n\tassert.Equal(t, initialPayload, user.FsConfig.AzBlobConfig.SASURL.GetPayload())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.SASURL.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.AzBlobConfig.SASURL.GetKey())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserCryptFs(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"crypt passphrase\")\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tinitialPayload := user.FsConfig.CryptConfig.Passphrase.GetPayload()\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, initialPayload)\n\tassert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey())\n\tuser.FsConfig.CryptConfig.Passphrase.SetStatus(sdkkms.SecretStatusSecretBox)\n\tuser.FsConfig.CryptConfig.Passphrase.SetAdditionalData(\"data\")\n\tuser.FsConfig.CryptConfig.Passphrase.SetKey(\"fake pass key\")\n\tuser, bb, err := httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(bb))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.Equal(t, initialPayload, user.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tassert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.ID = 0\n\tuser.CreatedAt = 0\n\tsecret := kms.NewSecret(sdkkms.SecretStatusSecretBox, \"invalid encrypted payload\", \"\", \"\")\n\tuser.FsConfig.CryptConfig.Passphrase = secret\n\t_, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.Error(t, err)\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"passphrase test\")\n\tuser, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.NoError(t, err)\n\tinitialPayload = user.FsConfig.CryptConfig.Passphrase.GetPayload()\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, initialPayload)\n\tassert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey())\n\tuser.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase.SetKey(\"pass\")\n\tuser, bb, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(bb))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, initialPayload)\n\tassert.Equal(t, initialPayload, user.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tassert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserSFTPFs(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser.FsConfig.SFTPConfig.Endpoint = \"[::1]:22:22\" // invalid endpoint\n\tuser.FsConfig.SFTPConfig.Username = \"sftp_user\"\n\tuser.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(\"sftp_pwd\")\n\tuser.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey)\n\tuser.FsConfig.SFTPConfig.Fingerprints = []string{sftpPkeyFingerprint}\n\tuser.FsConfig.SFTPConfig.BufferSize = 2\n\tuser.FsConfig.SFTPConfig.EqualityCheckMode = 1\n\t_, resp, err := httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"invalid endpoint\")\n\n\tuser.FsConfig.SFTPConfig.Endpoint = \"127.0.0.1\"\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.Error(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"127.0.0.1:22\", user.FsConfig.SFTPConfig.Endpoint)\n\n\tuser.FsConfig.SFTPConfig.Endpoint = \"127.0.0.1:2022\"\n\tuser.FsConfig.SFTPConfig.DisableCouncurrentReads = true\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/\", user.FsConfig.SFTPConfig.Prefix)\n\tassert.True(t, user.FsConfig.SFTPConfig.DisableCouncurrentReads)\n\tassert.Equal(t, int64(2), user.FsConfig.SFTPConfig.BufferSize)\n\tinitialPwdPayload := user.FsConfig.SFTPConfig.Password.GetPayload()\n\tinitialPkeyPayload := user.FsConfig.SFTPConfig.PrivateKey.GetPayload()\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.SFTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, initialPwdPayload)\n\tassert.Empty(t, user.FsConfig.SFTPConfig.Password.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.Password.GetKey())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.NotEmpty(t, initialPkeyPayload)\n\tassert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\tuser.FsConfig.SFTPConfig.Password.SetStatus(sdkkms.SecretStatusSecretBox)\n\tuser.FsConfig.SFTPConfig.Password.SetAdditionalData(\"adata\")\n\tuser.FsConfig.SFTPConfig.Password.SetKey(\"fake pwd key\")\n\tuser.FsConfig.SFTPConfig.PrivateKey.SetStatus(sdkkms.SecretStatusSecretBox)\n\tuser.FsConfig.SFTPConfig.PrivateKey.SetAdditionalData(\"adata\")\n\tuser.FsConfig.SFTPConfig.PrivateKey.SetKey(\"fake key\")\n\tuser.FsConfig.SFTPConfig.DisableCouncurrentReads = false\n\tuser, bb, err := httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(bb))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.SFTPConfig.Password.GetStatus())\n\tassert.Equal(t, initialPwdPayload, user.FsConfig.SFTPConfig.Password.GetPayload())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.Password.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.Password.GetKey())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.Equal(t, initialPkeyPayload, user.FsConfig.SFTPConfig.PrivateKey.GetPayload())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\tassert.False(t, user.FsConfig.SFTPConfig.DisableCouncurrentReads)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.ID = 0\n\tuser.CreatedAt = 0\n\tsecret := kms.NewSecret(sdkkms.SecretStatusSecretBox, \"invalid encrypted payload\", \"\", \"\")\n\tuser.FsConfig.SFTPConfig.Password = secret\n\t_, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.Error(t, err)\n\tuser.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()\n\tuser.FsConfig.SFTPConfig.PrivateKey = secret\n\t_, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.Error(t, err)\n\n\tuser.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey)\n\tuser, _, err = httpdtest.AddUser(user, http.StatusCreated)\n\tassert.NoError(t, err)\n\tinitialPkeyPayload = user.FsConfig.SFTPConfig.PrivateKey.GetPayload()\n\tassert.Nil(t, user.FsConfig.SFTPConfig.Password)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.NotEmpty(t, initialPkeyPayload)\n\tassert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\tuser.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser.FsConfig.SFTPConfig.PrivateKey.SetKey(\"k\")\n\tuser, bb, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(bb))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.NotEmpty(t, initialPkeyPayload)\n\tassert.Equal(t, initialPkeyPayload, user.FsConfig.SFTPConfig.PrivateKey.GetPayload())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserHiddenFields(t *testing.T) {\n\t// sensitive data must be hidden but not deleted from the dataprovider\n\tusernames := []string{\"user1\", \"user2\", \"user3\", \"user4\", \"user5\", \"user6\"}\n\tu1 := getTestUser()\n\tu1.Username = usernames[0]\n\tu1.FsConfig.Provider = sdk.S3FilesystemProvider\n\tu1.FsConfig.S3Config.Bucket = \"test\"\n\tu1.FsConfig.S3Config.Region = \"us-east-1\"\n\tu1.FsConfig.S3Config.AccessKey = \"S3-Access-Key\"\n\tu1.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(\"S3-Access-Secret\")\n\tu1.FsConfig.S3Config.SSECustomerKey = kms.NewPlainSecret(\"SSE-secret-key\")\n\tuser1, _, err := httpdtest.AddUser(u1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu2 := getTestUser()\n\tu2.Username = usernames[1]\n\tu2.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu2.FsConfig.GCSConfig.Bucket = \"test\"\n\tu2.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(\"fake credentials\")\n\tu2.FsConfig.GCSConfig.ACL = \"bucketOwnerRead\"\n\tu2.FsConfig.GCSConfig.UploadPartSize = 5\n\tu2.FsConfig.GCSConfig.UploadPartMaxTime = 20\n\tuser2, _, err := httpdtest.AddUser(u2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu3 := getTestUser()\n\tu3.Username = usernames[2]\n\tu3.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tu3.FsConfig.AzBlobConfig.Container = \"test\"\n\tu3.FsConfig.AzBlobConfig.AccountName = \"Server-Account-Name\"\n\tu3.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(\"Server-Account-Key\")\n\tuser3, _, err := httpdtest.AddUser(u3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu4 := getTestUser()\n\tu4.Username = usernames[3]\n\tu4.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tu4.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"test passphrase\")\n\tuser4, _, err := httpdtest.AddUser(u4, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu5 := getTestUser()\n\tu5.Username = usernames[4]\n\tu5.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tu5.FsConfig.SFTPConfig.Endpoint = \"127.0.0.1:2022\"\n\tu5.FsConfig.SFTPConfig.Username = \"sftp_user\"\n\tu5.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(\"apassword\")\n\tu5.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey)\n\tu5.FsConfig.SFTPConfig.Fingerprints = []string{sftpPkeyFingerprint}\n\tu5.FsConfig.SFTPConfig.Prefix = \"/prefix\"\n\tuser5, _, err := httpdtest.AddUser(u5, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu6 := getTestUser()\n\tu6.Username = usernames[5]\n\tu6.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\tu6.FsConfig.HTTPConfig = vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint: \"http://127.0.0.1/api/v1\",\n\t\t\tUsername: defaultUsername,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\tAPIKey:   kms.NewPlainSecret(defaultTokenAuthUser),\n\t}\n\tuser6, _, err := httpdtest.AddUser(u6, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tusers, _, err := httpdtest.GetUsers(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(users), 6)\n\tfor _, username := range usernames {\n\t\tuser, _, err := httpdtest.GetUserByUsername(username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Empty(t, user.Password)\n\t\tassert.True(t, user.HasPassword)\n\t}\n\tuser1, _, err = httpdtest.GetUserByUsername(user1.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Empty(t, user1.Password)\n\tassert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetKey())\n\tassert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetStatus())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.Empty(t, user1.FsConfig.S3Config.SSECustomerKey.GetKey())\n\tassert.Empty(t, user1.FsConfig.S3Config.SSECustomerKey.GetAdditionalData())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.SSECustomerKey.GetStatus())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\n\tuser2, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Empty(t, user2.Password)\n\tassert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey())\n\tassert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData())\n\tassert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetStatus())\n\tassert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetPayload())\n\n\tuser3, _, err = httpdtest.GetUserByUsername(user3.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Empty(t, user3.Password)\n\tassert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\tassert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\tassert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\n\tuser4, _, err = httpdtest.GetUserByUsername(user4.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Empty(t, user4.Password)\n\tassert.Empty(t, user4.FsConfig.CryptConfig.Passphrase.GetKey())\n\tassert.Empty(t, user4.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetPayload())\n\n\tuser5, _, err = httpdtest.GetUserByUsername(user5.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Empty(t, user5.Password)\n\tassert.Empty(t, user5.FsConfig.SFTPConfig.Password.GetKey())\n\tassert.Empty(t, user5.FsConfig.SFTPConfig.Password.GetAdditionalData())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.Password.GetPayload())\n\tassert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\tassert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetPayload())\n\tassert.Equal(t, \"/prefix\", user5.FsConfig.SFTPConfig.Prefix)\n\n\tuser6, _, err = httpdtest.GetUserByUsername(user6.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Empty(t, user6.Password)\n\tassert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.NotEmpty(t, user6.FsConfig.HTTPConfig.APIKey.GetStatus())\n\tassert.NotEmpty(t, user6.FsConfig.HTTPConfig.APIKey.GetPayload())\n\n\t// finally check that we have all the data inside the data provider\n\tuser1, err = dataprovider.UserExists(user1.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, user1.Password)\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetKey())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetStatus())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.SSECustomerKey.GetKey())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.SSECustomerKey.GetAdditionalData())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.SSECustomerKey.GetStatus())\n\tassert.NotEmpty(t, user1.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\terr = user1.FsConfig.S3Config.AccessSecret.Decrypt()\n\tassert.NoError(t, err)\n\terr = user1.FsConfig.S3Config.SSECustomerKey.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, user1.FsConfig.S3Config.AccessSecret.GetStatus())\n\tassert.Equal(t, u1.FsConfig.S3Config.AccessSecret.GetPayload(), user1.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetKey())\n\tassert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusPlain, user1.FsConfig.S3Config.SSECustomerKey.GetStatus())\n\tassert.Equal(t, u1.FsConfig.S3Config.SSECustomerKey.GetPayload(), user1.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\tassert.Empty(t, user1.FsConfig.S3Config.SSECustomerKey.GetKey())\n\tassert.Empty(t, user1.FsConfig.S3Config.SSECustomerKey.GetAdditionalData())\n\n\tuser2, err = dataprovider.UserExists(user2.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, user2.Password)\n\tassert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetKey())\n\tassert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData())\n\tassert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetStatus())\n\tassert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetPayload())\n\terr = user2.FsConfig.GCSConfig.Credentials.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, user2.FsConfig.GCSConfig.Credentials.GetStatus())\n\tassert.Equal(t, u2.FsConfig.GCSConfig.Credentials.GetPayload(), user2.FsConfig.GCSConfig.Credentials.GetPayload())\n\tassert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey())\n\tassert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData())\n\n\tuser3, err = dataprovider.UserExists(user3.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, user3.Password)\n\tassert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\tassert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\tassert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\terr = user3.FsConfig.AzBlobConfig.AccountKey.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.Equal(t, u3.FsConfig.AzBlobConfig.AccountKey.GetPayload(), user3.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\tassert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\tassert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\n\tuser4, err = dataprovider.UserExists(user4.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, user4.Password)\n\tassert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetKey())\n\tassert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, user4.FsConfig.CryptConfig.Passphrase.GetPayload())\n\terr = user4.FsConfig.CryptConfig.Passphrase.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, user4.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.Equal(t, u4.FsConfig.CryptConfig.Passphrase.GetPayload(), user4.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tassert.Empty(t, user4.FsConfig.CryptConfig.Passphrase.GetKey())\n\tassert.Empty(t, user4.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\n\tuser5, err = dataprovider.UserExists(user5.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, user5.Password)\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.Password.GetKey())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.Password.GetAdditionalData())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.Password.GetPayload())\n\terr = user5.FsConfig.SFTPConfig.Password.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, user5.FsConfig.SFTPConfig.Password.GetStatus())\n\tassert.Equal(t, u5.FsConfig.SFTPConfig.Password.GetPayload(), user5.FsConfig.SFTPConfig.Password.GetPayload())\n\tassert.Empty(t, user5.FsConfig.SFTPConfig.Password.GetKey())\n\tassert.Empty(t, user5.FsConfig.SFTPConfig.Password.GetAdditionalData())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.NotEmpty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetPayload())\n\terr = user5.FsConfig.SFTPConfig.PrivateKey.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, user5.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.Equal(t, u5.FsConfig.SFTPConfig.PrivateKey.GetPayload(), user5.FsConfig.SFTPConfig.PrivateKey.GetPayload())\n\tassert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\tassert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\n\tuser6, err = dataprovider.UserExists(user6.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, user6.Password)\n\tassert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetPayload())\n\terr = user6.FsConfig.HTTPConfig.Password.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, user6.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.Equal(t, u6.FsConfig.HTTPConfig.Password.GetPayload(), user6.FsConfig.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\n\t// update the GCS user and check that the credentials are preserved\n\tuser2.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()\n\tuser2.FsConfig.GCSConfig.ACL = \"private\"\n\t_, _, err = httpdtest.UpdateUser(user2, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tuser2, _, err = httpdtest.GetUserByUsername(user2.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Empty(t, user2.Password)\n\tassert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey())\n\tassert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData())\n\tassert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetStatus())\n\tassert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetPayload())\n\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user3, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user4, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user5, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user6, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSecretObject(t *testing.T) {\n\ts := kms.NewPlainSecret(\"test data\")\n\ts.SetAdditionalData(\"username\")\n\trequire.True(t, s.IsValid())\n\terr := s.Encrypt()\n\trequire.NoError(t, err)\n\trequire.Equal(t, sdkkms.SecretStatusSecretBox, s.GetStatus())\n\trequire.NotEmpty(t, s.GetPayload())\n\trequire.NotEmpty(t, s.GetKey())\n\trequire.True(t, s.IsValid())\n\terr = s.Decrypt()\n\trequire.NoError(t, err)\n\trequire.Equal(t, sdkkms.SecretStatusPlain, s.GetStatus())\n\trequire.Equal(t, \"test data\", s.GetPayload())\n\trequire.Empty(t, s.GetKey())\n}\n\nfunc TestSecretObjectCompatibility(t *testing.T) {\n\t// this is manually tested against vault too\n\ttestPayload := \"test payload\"\n\ts := kms.NewPlainSecret(testPayload)\n\trequire.True(t, s.IsValid())\n\terr := s.Encrypt()\n\trequire.NoError(t, err)\n\tlocalAsJSON, err := json.Marshal(s)\n\tassert.NoError(t, err)\n\n\tfor _, secretStatus := range []string{sdkkms.SecretStatusSecretBox} {\n\t\tkmsConfig := config.GetKMSConfig()\n\t\tassert.Empty(t, kmsConfig.Secrets.MasterKeyPath)\n\t\tif secretStatus == sdkkms.SecretStatusVaultTransit {\n\t\t\tos.Setenv(\"VAULT_SERVER_URL\", \"http://127.0.0.1:8200\")\n\t\t\tos.Setenv(\"VAULT_SERVER_TOKEN\", \"s.9lYGq83MbgG5KR5kfebXVyhJ\")\n\t\t\tkmsConfig.Secrets.URL = \"hashivault://mykey\"\n\t\t}\n\t\terr := kmsConfig.Initialize()\n\t\tassert.NoError(t, err)\n\t\t// encrypt without a master key\n\t\tsecret := kms.NewPlainSecret(testPayload)\n\t\tsecret.SetAdditionalData(\"add data\")\n\t\terr = secret.Encrypt()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, secret.GetMode())\n\t\tsecretClone := secret.Clone()\n\t\terr = secretClone.Decrypt()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testPayload, secretClone.GetPayload())\n\t\tif secretStatus == sdkkms.SecretStatusVaultTransit {\n\t\t\t// decrypt the local secret now that the provider is vault\n\t\t\tsecretLocal := kms.NewEmptySecret()\n\t\t\terr = json.Unmarshal(localAsJSON, secretLocal)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, secretLocal.GetStatus())\n\t\t\tassert.Equal(t, 0, secretLocal.GetMode())\n\t\t\terr = secretLocal.Decrypt()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testPayload, secretLocal.GetPayload())\n\t\t\tassert.Equal(t, sdkkms.SecretStatusPlain, secretLocal.GetStatus())\n\t\t\terr = secretLocal.Encrypt()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, secretLocal.GetStatus())\n\t\t\tassert.Equal(t, 0, secretLocal.GetMode())\n\t\t}\n\n\t\tasJSON, err := json.Marshal(secret)\n\t\tassert.NoError(t, err)\n\n\t\tmasterKeyPath := filepath.Join(os.TempDir(), \"mkey\")\n\t\terr = os.WriteFile(masterKeyPath, []byte(\"test key\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tconfig := kms.Configuration{\n\t\t\tSecrets: kms.Secrets{\n\t\t\t\tMasterKeyPath: masterKeyPath,\n\t\t\t},\n\t\t}\n\t\tif secretStatus == sdkkms.SecretStatusVaultTransit {\n\t\t\tconfig.Secrets.URL = \"hashivault://mykey\"\n\t\t}\n\t\terr = config.Initialize()\n\t\tassert.NoError(t, err)\n\n\t\t// now build the secret from JSON\n\t\tsecret = kms.NewEmptySecret()\n\t\terr = json.Unmarshal(asJSON, secret)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, secret.GetMode())\n\t\terr = secret.Decrypt()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testPayload, secret.GetPayload())\n\t\terr = secret.Encrypt()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, secret.GetMode())\n\t\terr = secret.Decrypt()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testPayload, secret.GetPayload())\n\t\tif secretStatus == sdkkms.SecretStatusVaultTransit {\n\t\t\t// decrypt the local secret encryped without a master key now that\n\t\t\t// the provider is vault and a master key is set.\n\t\t\t// The provider will not change, the master key will be used\n\t\t\tsecretLocal := kms.NewEmptySecret()\n\t\t\terr = json.Unmarshal(localAsJSON, secretLocal)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, secretLocal.GetStatus())\n\t\t\tassert.Equal(t, 0, secretLocal.GetMode())\n\t\t\terr = secretLocal.Decrypt()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testPayload, secretLocal.GetPayload())\n\t\t\tassert.Equal(t, sdkkms.SecretStatusPlain, secretLocal.GetStatus())\n\t\t\terr = secretLocal.Encrypt()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, secretLocal.GetStatus())\n\t\t\tassert.Equal(t, 1, secretLocal.GetMode())\n\t\t}\n\n\t\terr = kmsConfig.Initialize()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(masterKeyPath)\n\t\tassert.NoError(t, err)\n\t\tif secretStatus == sdkkms.SecretStatusVaultTransit {\n\t\t\tos.Unsetenv(\"VAULT_SERVER_URL\")\n\t\t\tos.Unsetenv(\"VAULT_SERVER_TOKEN\")\n\t\t}\n\t}\n}\n\nfunc TestUpdateUserNoCredentials(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Password = \"\"\n\tuser.PublicKeys = []string{}\n\t// password and public key will be omitted from json serialization if empty and so they will remain unchanged\n\t// and no validation error will be raised\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateUserEmptyHomeDir(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.HomeDir = \"\"\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateUserInvalidHomeDir(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.HomeDir = \"relative_path\"\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, \"\")\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateNonExistentUser(t *testing.T) {\n\t_, _, err := httpdtest.UpdateUser(getTestUser(), http.StatusNotFound, \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestGetNonExistentUser(t *testing.T) {\n\t_, _, err := httpdtest.GetUserByUsername(\"na\", http.StatusNotFound)\n\tassert.NoError(t, err)\n}\n\nfunc TestDeleteNonExistentUser(t *testing.T) {\n\t_, err := httpdtest.RemoveUser(getTestUser(), http.StatusNotFound)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddDuplicateUser(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.AddUser(getTestUser(), http.StatusConflict)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.Error(t, err, \"adding a duplicate user must fail\")\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestGetUsers(t *testing.T) {\n\tuser1, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.Username = defaultUsername + \"1\"\n\tuser2, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tusers, _, err := httpdtest.GetUsers(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(users), 2)\n\tfor _, user := range users {\n\t\tif u.Username == user.Username {\n\t\t\tassert.True(t, user.HasPassword)\n\t\t}\n\t}\n\tusers, _, err = httpdtest.GetUsers(1, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, len(users))\n\tusers, _, err = httpdtest.GetUsers(1, 1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, len(users))\n\t_, _, err = httpdtest.GetUsers(1, 1, http.StatusInternalServerError)\n\tassert.Error(t, err)\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestGetQuotaScans(t *testing.T) {\n\t_, _, err := httpdtest.GetQuotaScans(http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetQuotaScans(http.StatusInternalServerError)\n\tassert.Error(t, err)\n\t_, _, err = httpdtest.GetFoldersQuotaScans(http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetFoldersQuotaScans(http.StatusInternalServerError)\n\tassert.Error(t, err)\n}\n\nfunc TestStartQuotaScan(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:        \"vfolder\",\n\t\tMappedPath:  filepath.Join(os.TempDir(), \"folder\"),\n\t\tDescription: \"virtual folder\",\n\t}\n\t_, _, err = httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.StartFolderQuotaScan(folder, http.StatusAccepted)\n\tassert.NoError(t, err)\n\tfor {\n\t\tquotaScan, _, err := httpdtest.GetFoldersQuotaScans(http.StatusOK)\n\t\tif !assert.NoError(t, err, \"Error getting active scans\") {\n\t\t\tbreak\n\t\t}\n\t\tif len(quotaScan) == 0 {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateFolderQuotaUsage(t *testing.T) {\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       \"vdir\",\n\t\tMappedPath: filepath.Join(os.TempDir(), \"folder\"),\n\t}\n\tusedQuotaFiles := 1\n\tusedQuotaSize := int64(65535)\n\tf.UsedQuotaFiles = usedQuotaFiles\n\tf.UsedQuotaSize = usedQuotaSize\n\tfolder, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, usedQuotaFiles, folder.UsedQuotaFiles)\n\t\tassert.Equal(t, usedQuotaSize, folder.UsedQuotaSize)\n\t}\n\t_, err = httpdtest.UpdateFolderQuotaUsage(folder, \"invalid mode\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.UpdateFolderQuotaUsage(f, \"reset\", http.StatusOK)\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(f.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles, folder.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize, folder.UsedQuotaSize)\n\t_, err = httpdtest.UpdateFolderQuotaUsage(f, \"add\", http.StatusOK)\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(f.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2*usedQuotaFiles, folder.UsedQuotaFiles)\n\tassert.Equal(t, 2*usedQuotaSize, folder.UsedQuotaSize)\n\tf.UsedQuotaSize = -1\n\t_, err = httpdtest.UpdateFolderQuotaUsage(f, \"\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tf.UsedQuotaSize = usedQuotaSize\n\tf.Name = f.Name + \"1\"\n\t_, err = httpdtest.UpdateFolderQuotaUsage(f, \"\", http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestGetVersion(t *testing.T) {\n\t_, _, err := httpdtest.GetVersion(http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetVersion(http.StatusInternalServerError)\n\tassert.Error(t, err, \"get version request must succeed, we requested to check a wrong status code\")\n}\n\nfunc TestGetStatus(t *testing.T) {\n\t_, _, err := httpdtest.GetStatus(http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetStatus(http.StatusBadRequest)\n\tassert.Error(t, err, \"get provider status request must succeed, we requested to check a wrong status code\")\n}\n\nfunc TestGetConnections(t *testing.T) {\n\t_, _, err := httpdtest.GetConnections(http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetConnections(http.StatusInternalServerError)\n\tassert.Error(t, err, \"get sftp connections request must succeed, we requested to check a wrong status code\")\n}\n\nfunc TestCloseActiveConnection(t *testing.T) {\n\t_, err := httpdtest.CloseConnection(\"non_existent_id\", http.StatusNotFound)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tc := common.NewBaseConnection(\"connID\", common.ProtocolSFTP, \"\", \"\", user)\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\terr = common.Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.CloseConnection(c.GetID(), http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestCloseConnectionAfterUserUpdateDelete(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tc := common.NewBaseConnection(\"connID\", common.ProtocolFTP, \"\", \"\", user)\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\terr = common.Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\tc1 := common.NewBaseConnection(\"connID1\", common.ProtocolSFTP, \"\", \"\", user)\n\tfakeConn1 := &fakeConnection{\n\t\tBaseConnection: c1,\n\t}\n\terr = common.Connections.Add(fakeConn1)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"0\")\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 2)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"1\")\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\n\terr = common.Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\terr = common.Connections.Add(fakeConn1)\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 2)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestAdminGenerateRecoveryCodesSaveError(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.NamingRules = 7\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\ta := getTestAdmin()\n\ta.Username = \"adMiN@example.com \"\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)\n\tassert.NoError(t, err)\n\tadmin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t}\n\tadmin.Password = defaultTokenAuthPass\n\terr = dataprovider.UpdateAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tadmin, _, err = httpdtest.GetAdminByUsername(a.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\tadminAPIToken, err := getJWTAPITokenFromTestServerWithPasscode(a.Username, defaultTokenAuthPass, passcode)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, adminAPIToken)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tif config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName {\n\t\treturn\n\t}\n\treq, err := http.NewRequest(http.MethodPost, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the following characters are allowed\")\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminCredentialsWithSpaces(t *testing.T) {\n\ta := getTestAdmin()\n\ta.Username = xid.New().String()\n\ta.Password = \" \" + xid.New().String() + \" \"\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// For admins the password is always trimmed.\n\t_, err = getJWTAPITokenFromTestServer(a.Username, a.Password)\n\tassert.Error(t, err)\n\t_, err = getJWTAPITokenFromTestServer(a.Username, strings.TrimSpace(a.Password))\n\tassert.NoError(t, err)\n\t// The password sent from the WebAdmin UI is automatically trimmed\n\t_, err = getJWTWebToken(a.Username, a.Password)\n\tassert.NoError(t, err)\n\t_, err = getJWTWebToken(a.Username, strings.TrimSpace(a.Password))\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserCredentialsWithSpaces(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \" \" + xid.New().String() + \" \"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// For users the password is not trimmed\n\t_, err = getJWTAPIUserTokenFromTestServer(u.Username, u.Password)\n\tassert.NoError(t, err)\n\t_, err = getJWTAPIUserTokenFromTestServer(u.Username, strings.TrimSpace(u.Password))\n\tassert.Error(t, err)\n\n\t_, err = getJWTWebClientTokenFromTestServer(u.Username, u.Password)\n\tassert.NoError(t, err)\n\t_, err = getJWTWebClientTokenFromTestServer(u.Username, strings.TrimSpace(u.Password))\n\tassert.Error(t, err)\n\n\tuser.Password = u.Password\n\tconn, sftpClient, err := getSftpClient(user)\n\tif assert.NoError(t, err) {\n\t\tconn.Close()\n\t\tsftpClient.Close()\n\t}\n\tuser.Password = strings.TrimSpace(u.Password)\n\t_, _, err = getSftpClient(user)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestNamingRules(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          3525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.NamingRules = 7\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.Username = \" uSeR@user.me \"\n\tu.Email = dataprovider.ConvertName(u.Username)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"user@user.me\", user.Username)\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tuser.Password = u.Password\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser.Username = u.Username\n\tuser.AdditionalInfo = \"info\"\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(u.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\n\tr := getTestRole()\n\tr.Name = \"role@mycompany\"\n\trole, _, err := httpdtest.AddRole(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ta := getTestAdmin()\n\ta.Username = \"admiN@example.com \"\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"admin@example.com\", admin.Username)\n\tadmin.Email = dataprovider.ConvertName(a.Username)\n\tadmin.Username = a.Username\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tadmin, _, err = httpdtest.GetAdminByUsername(a.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       \"文件夹AB\",\n\t\tMappedPath: filepath.Clean(os.TempDir()),\n\t}\n\tfolder, resp, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, \"文件夹ab\", folder.Name)\n\tfolder.Name = f.Name\n\tfolder.Description = folder.Name\n\t_, resp, err = httpdtest.UpdateFolder(folder, http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\tfolder, resp, err = httpdtest.GetFolderByName(f.Name, http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, \"文件夹AB\", folder.Description)\n\t_, err = httpdtest.RemoveFolder(f, http.StatusOK)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTWebClientTokenFromTestServer(u.Username, defaultPassword)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, token)\n\tadminAPIToken, err := getJWTAPITokenFromTestServer(a.Username, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, adminAPIToken)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tif config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName {\n\t\treturn\n\t}\n\n\ttoken, err = getJWTWebClientTokenFromTestServer(user.Username, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, token)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidUser)\n\t// test user reset password. Setting the new password will fail because the username is not valid\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"username\", user.Username)\n\tform.Set(csrfFormToken, csrfToken)\n\tlastResetCode = \"\"\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"code\", lastResetCode)\n\tform.Set(\"password\", defaultPassword)\n\tform.Set(\"confirm_password\", defaultPassword)\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidUser)\n\n\tadminAPIToken, err = getJWTAPITokenFromTestServer(admin.Username, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuserAPIToken, err := getJWTAPIUserTokenFromTestServer(user.Username, defaultPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userPath+\"/\"+user.Username+\"/2fa/disable\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the following characters are allowed\")\n\n\treq, err = http.NewRequest(http.MethodPost, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the following characters are allowed\")\n\n\tapiKeyAuthReq := make(map[string]bool)\n\tapiKeyAuthReq[\"allow_api_key_auth\"] = true\n\tasJSON, err := json.Marshal(apiKeyAuthReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the following characters are allowed\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\ttoken, err = getJWTWebTokenFromTestServer(admin.Username, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminProfilePath, token)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidUser)\n\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminRolePath, role.Name), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidName)\n\n\tapiKeyAuthReq = make(map[string]bool)\n\tapiKeyAuthReq[\"allow_api_key_auth\"] = true\n\tasJSON, err = json.Marshal(apiKeyAuthReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the following characters are allowed\")\n\t// test admin reset password\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"username\", admin.Username)\n\tform.Set(csrfFormToken, csrfToken)\n\tlastResetCode = \"\"\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"code\", lastResetCode)\n\tform.Set(\"password\", defaultPassword)\n\tform.Set(\"confirm_password\", defaultPassword)\n\treq, err = http.NewRequest(http.MethodPost, webAdminResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestUserPassword(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.False(t, user.HasPassword)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.False(t, user.HasPassword)\n\n\tuser.Password = defaultPassword\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.True(t, user.HasPassword)\n\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\trawUser := map[string]any{\n\t\t\"username\": user.Username,\n\t\t\"home_dir\": filepath.Join(homeBasePath, defaultUsername),\n\t\t\"permissions\": map[string][]string{\n\t\t\t\"/\": {\"*\"},\n\t\t},\n\t}\n\tuserAsJSON, err := json.Marshal(rawUser)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// the previous password must be preserved\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.HasPassword)\n\t// update the user with an empty password field, the password will be unset\n\trawUser[\"password\"] = \"\"\n\tuserAsJSON, err = json.Marshal(rawUser)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, user.HasPassword)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSaveErrors(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.NamingRules = 1\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\trecCode := \"recovery code\"\n\trecoveryCodes := []dataprovider.RecoveryCode{\n\t\t{\n\t\t\tSecret: kms.NewPlainSecret(recCode),\n\t\t\tUsed:   false,\n\t\t},\n\t}\n\n\tu := getTestUser()\n\tu.Username = \"user@example.com\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = u.Password\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH, common.ProtocolHTTP},\n\t}\n\tuser.Filters.RecoveryCodes = recoveryCodes\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, user.Filters.RecoveryCodes, 1)\n\n\ta := getTestAdmin()\n\ta.Username = \"admin@example.com\"\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\tadmin.Email = admin.Username\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tadmin.Password = a.Password\n\tadmin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t}\n\tadmin.Filters.RecoveryCodes = recoveryCodes\n\terr = dataprovider.UpdateAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, admin.Filters.RecoveryCodes, 1)\n\n\tr := getTestRole()\n\tr.Name = \"role@mycompany\"\n\trole, _, err := httpdtest.AddRole(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tif config.GetProviderConf().Driver == dataprovider.MemoryDataProviderName {\n\t\treturn\n\t}\n\n\t_, resp, err := httpdtest.UpdateRole(role, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"the following characters are allowed\")\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform := getLoginForm(a.Username, a.Password, csrfToken)\n\treq, err := http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr := executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err := getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"recovery_code\", recCode)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(u.Username, u.Password, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err = getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"recovery_code\", recCode)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserBaseDir(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.UsersBaseDir = homeBasePath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.HomeDir = \"\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, \"home dir mismatch\")\n\t}\n\tassert.Equal(t, filepath.Join(providerConf.UsersBaseDir, u.Username), user.HomeDir)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaTrackingDisabled(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.TrackQuota = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t// user quota scan must fail\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.StartQuotaScan(user, http.StatusForbidden)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.UpdateQuotaUsage(user, \"\", http.StatusForbidden)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.UpdateTransferQuotaUsage(user, \"\", http.StatusForbidden)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t// folder quota scan must fail\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       \"folder_quota_test\",\n\t\tMappedPath: filepath.Clean(os.TempDir()),\n\t}\n\tfolder, resp, err := httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\t_, err = httpdtest.StartFolderQuotaScan(folder, http.StatusForbidden)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.UpdateFolderQuotaUsage(folder, \"\", http.StatusForbidden)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestProviderErrors(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuserAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tuserWebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\ttoken, _, err := httpdtest.GetToken(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\ttestServerToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\thttpdtest.SetJWTToken(token)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetUserByUsername(\"na\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetUsers(1, 0, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetGroups(1, 0, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetAdmins(1, 0, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetAPIKeys(1, 0, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetEventActions(1, 0, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetEventRules(1, 0, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetIPListEntries(dataprovider.IPListTypeDefender, \"\", \"\", dataprovider.OrderASC, 10, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetRoles(1, 0, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.UpdateRole(getTestRole(), http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, userSharesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\t// password reset errors\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"username\", \"username\")\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)\n\n\tgetJSONShares := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, err := http.NewRequest(http.MethodGet, webClientSharesPath+jsonAPISuffix, nil)\n\t\tassert.NoError(t, err)\n\t\tsetJWTCookieForReq(req, userWebToken)\n\t\texecuteRequest(req)\n\t}\n\tgetJSONShares()\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharePath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, userWebToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharePath+\"/shareID\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, userWebToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath+\"/shareID\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, userWebToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\t_, _, err = httpdtest.UpdateUser(dataprovider.User{BaseUser: sdk.BaseUser{Username: \"auser\"}}, http.StatusInternalServerError, \"\")\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(dataprovider.User{BaseUser: sdk.BaseUser{Username: \"auser\"}}, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: \"aname\"}, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tstatus, _, err := httpdtest.GetStatus(http.StatusOK)\n\tif assert.NoError(t, err) {\n\t\tassert.False(t, status.DataProvider.IsActive)\n\t}\n\t_, _, err = httpdtest.Dumpdata(\"backup.json\", \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetFolders(0, 0, http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tuser = getTestUser()\n\tuser.ID = 1\n\tbackupData := dataprovider.BackupData{\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupData.Configs = &dataprovider.Configs{}\n\tbackupData.Users = append(backupData.Users, user)\n\tbackupContent, err := json.Marshal(backupData)\n\tassert.NoError(t, err)\n\tbackupFilePath := filepath.Join(backupsPath, \"backup.json\")\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData.Configs = nil\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData.Folders = append(backupData.Folders, vfs.BaseVirtualFolder{Name: \"testFolder\", MappedPath: filepath.Clean(os.TempDir())})\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData.Users = nil\n\tbackupData.Folders = nil\n\tbackupData.Groups = append(backupData.Groups, getTestGroup())\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData.Groups = nil\n\tbackupData.Admins = append(backupData.Admins, getTestAdmin())\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData.Users = nil\n\tbackupData.Folders = nil\n\tbackupData.Admins = nil\n\tbackupData.APIKeys = append(backupData.APIKeys, dataprovider.APIKey{\n\t\tName:  \"name\",\n\t\tKeyID: util.GenerateUniqueID(),\n\t\tKey:   fmt.Sprintf(\"%v.%v\", util.GenerateUniqueID(), util.GenerateUniqueID()),\n\t\tScope: dataprovider.APIKeyScopeUser,\n\t})\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData.APIKeys = nil\n\tbackupData.Shares = append(backupData.Shares, dataprovider.Share{\n\t\tName:     util.GenerateUniqueID(),\n\t\tShareID:  util.GenerateUniqueID(),\n\t\tScope:    dataprovider.ShareScopeRead,\n\t\tPaths:    []string{\"/\"},\n\t\tUsername: defaultUsername,\n\t})\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, resp, err := httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err, string(resp))\n\tbackupData = dataprovider.BackupData{\n\t\tEventActions: []dataprovider.BaseEventAction{\n\t\t\t{\n\t\t\t\tName: \"quota_reset\",\n\t\t\t\tType: dataprovider.ActionTypeFolderQuotaReset,\n\t\t\t},\n\t\t},\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData = dataprovider.BackupData{\n\t\tEventRules: []dataprovider.EventRule{\n\t\t\t{\n\t\t\t\tName:    \"quota_reset\",\n\t\t\t\tTrigger: dataprovider.EventTriggerSchedule,\n\t\t\t\tConditions: dataprovider.EventConditions{\n\t\t\t\t\tSchedules: []dataprovider.Schedule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tHours:      \"2\",\n\t\t\t\t\t\t\tDayOfWeek:  \"1\",\n\t\t\t\t\t\t\tDayOfMonth: \"2\",\n\t\t\t\t\t\t\tMonth:      \"3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tActions: []dataprovider.EventAction{\n\t\t\t\t\t{\n\t\t\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\t\t\tName: \"unknown action\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOrder: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData = dataprovider.BackupData{\n\t\tRoles: []dataprovider.Role{\n\t\t\t{\n\t\t\t\tName: \"role1\",\n\t\t\t},\n\t\t},\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\tbackupData = dataprovider.BackupData{\n\t\tIPLists: []dataprovider.IPListEntry{\n\t\t\t{\n\t\t\t\tIPOrNet: \"192.168.1.1/24\",\n\t\t\t\tType:    dataprovider.IPListTypeRateLimiterSafeList,\n\t\t\t\tMode:    dataprovider.ListModeAllow,\n\t\t\t},\n\t\t},\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\n\terr = os.Remove(backupFilePath)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webUserPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodGet, webTemplateUser+\"?from=auser\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodGet, webGroupPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodGet, webAdminPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodGet, webTemplateUser, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webGroupPath, \"groupname\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webGroupPath, \"grpname\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodGet, webTemplateFolder+\"?from=afolder\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminEventActionPath, \"actionname\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, \"actionname\"), bytes.NewBuffer(nil))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\tgetJSONActions := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, err := http.NewRequest(http.MethodGet, webAdminEventActionsPath+jsonAPISuffix, nil)\n\t\tassert.NoError(t, err)\n\t\tsetJWTCookieForReq(req, testServerToken)\n\t\texecuteRequest(req)\n\t}\n\tgetJSONActions()\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminEventRulePath, \"rulename\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventRulePath, \"rulename\"), bytes.NewBuffer(nil))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\tgetJSONRules := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, err := http.NewRequest(http.MethodGet, webAdminEventRulesPath+jsonAPISuffix, nil)\n\t\tassert.NoError(t, err)\n\t\tsetJWTCookieForReq(req, testServerToken)\n\t\texecuteRequest(req)\n\t}\n\tgetJSONRules()\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminEventRulePath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, testServerToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\thttpdtest.SetJWTToken(\"\")\n}\n\nfunc TestFolders(t *testing.T) {\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       \"name\",\n\t\tMappedPath: \"relative path\",\n\t\tUsers:      []string{\"1\", \"2\", \"3\"},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(\"asecret\"),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(folder, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tfolder.MappedPath = filepath.Clean(os.TempDir())\n\tfolder1, resp, err := httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, folder.Name, folder1.Name)\n\tassert.Equal(t, folder.MappedPath, folder1.MappedPath)\n\tassert.Equal(t, 0, folder1.UsedQuotaFiles)\n\tassert.Equal(t, int64(0), folder1.UsedQuotaSize)\n\tassert.Equal(t, int64(0), folder1.LastQuotaUpdate)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, folder1.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, folder1.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tassert.Empty(t, folder1.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.Empty(t, folder1.FsConfig.CryptConfig.Passphrase.GetKey())\n\tassert.Len(t, folder1.Users, 0)\n\t// adding a duplicate folder must fail\n\t_, _, err = httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.Error(t, err)\n\tfolder.MappedPath = filepath.Join(os.TempDir(), \"vfolder\")\n\tfolder.Name = filepath.Base(folder.MappedPath)\n\tfolder.UsedQuotaFiles = 1\n\tfolder.UsedQuotaSize = 345\n\tfolder.LastQuotaUpdate = 10\n\tfolder2, _, err := httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, 1, folder2.UsedQuotaFiles)\n\tassert.Equal(t, int64(345), folder2.UsedQuotaSize)\n\tassert.Equal(t, int64(10), folder2.LastQuotaUpdate)\n\tassert.Len(t, folder2.Users, 0)\n\tfolders, _, err := httpdtest.GetFolders(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tnumResults := len(folders)\n\tassert.GreaterOrEqual(t, numResults, 2)\n\tfound := false\n\tfor _, f := range folders {\n\t\tif f.Name == folder1.Name {\n\t\t\tfound = true\n\t\t\tassert.Equal(t, folder1.MappedPath, f.MappedPath)\n\t\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, f.FsConfig.CryptConfig.Passphrase.GetStatus())\n\t\t\tassert.NotEmpty(t, f.FsConfig.CryptConfig.Passphrase.GetPayload())\n\t\t\tassert.Empty(t, f.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\t\t\tassert.Empty(t, f.FsConfig.CryptConfig.Passphrase.GetKey())\n\t\t\tassert.Len(t, f.Users, 0)\n\t\t}\n\t}\n\tassert.True(t, found)\n\tfolders, _, err = httpdtest.GetFolders(0, 1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folders, numResults-1)\n\tfolders, _, err = httpdtest.GetFolders(1, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folders, 1)\n\tf, _, err := httpdtest.GetFolderByName(folder1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, folder1.Name, f.Name)\n\tassert.Equal(t, folder1.MappedPath, f.MappedPath)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, f.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, f.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tassert.Empty(t, f.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.Empty(t, f.FsConfig.CryptConfig.Passphrase.GetKey())\n\tassert.Len(t, f.Users, 0)\n\tf, _, err = httpdtest.GetFolderByName(folder2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, folder2.Name, f.Name)\n\tassert.Equal(t, folder2.MappedPath, f.MappedPath)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{\n\t\tName: \"invalid\",\n\t}, http.StatusNotFound)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.UpdateFolder(vfs.BaseVirtualFolder{Name: \"notfound\"}, http.StatusNotFound)\n\tassert.NoError(t, err)\n\tfolder1.MappedPath = \"a/relative/path\"\n\t_, _, err = httpdtest.UpdateFolder(folder1, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tfolder1.MappedPath = filepath.Join(os.TempDir(), \"updated\")\n\tfolder1.Description = \"updated folder description\"\n\tf, resp, err = httpdtest.UpdateFolder(folder1, http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, folder1.MappedPath, f.MappedPath)\n\tassert.Equal(t, folder1.Description, f.Description)\n\n\t_, err = httpdtest.RemoveFolder(folder1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder2, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestFolderRelations(t *testing.T) {\n\tmappedPath := filepath.Join(os.TempDir(), \"mapped_path\")\n\tname := filepath.Base(mappedPath)\n\tu := getTestUser()\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: name,\n\t\t},\n\t\tVirtualPath: \"/mountu\",\n\t})\n\t_, resp, err := httpdtest.AddUser(u, http.StatusInternalServerError)\n\tassert.NoError(t, err, string(resp))\n\tg := getTestGroup()\n\tg.VirtualFolders = append(g.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: name,\n\t\t},\n\t\tVirtualPath: \"/mountg\",\n\t})\n\t_, resp, err = httpdtest.AddGroup(g, http.StatusInternalServerError)\n\tassert.NoError(t, err, string(resp))\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       name,\n\t\tMappedPath: mappedPath,\n\t}\n\tfolder, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 0)\n\tassert.Len(t, folder.Groups, 0)\n\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tgroup, resp, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tfolder, _, err = httpdtest.GetFolderByName(folder.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Len(t, folder.Groups, 1)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user.VirtualFolders, 1) {\n\t\tassert.Equal(t, mappedPath, user.VirtualFolders[0].MappedPath)\n\t}\n\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, group.VirtualFolders, 1) {\n\t\tassert.Equal(t, mappedPath, group.VirtualFolders[0].MappedPath)\n\t}\n\t// update the folder and check the modified field on user and group\n\tmappedPath = filepath.Join(os.TempDir(), \"mapped_path\")\n\tfolder.MappedPath = mappedPath\n\t_, _, err = httpdtest.UpdateFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, user.VirtualFolders, 1) {\n\t\tassert.Equal(t, mappedPath, user.VirtualFolders[0].MappedPath)\n\t}\n\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, group.VirtualFolders, 1) {\n\t\tassert.Equal(t, mappedPath, group.VirtualFolders[0].MappedPath)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(folder.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 0)\n\tassert.Len(t, folder.Groups, 0)\n\n\tuser, resp, err = httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Len(t, user.VirtualFolders, 1)\n\tgroup, resp, err = httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Len(t, group.VirtualFolders, 1)\n\n\tfolder, _, err = httpdtest.GetFolderByName(folder.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, folder.Users, 1)\n\tassert.Len(t, folder.Groups, 1)\n\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.VirtualFolders, 0)\n\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group.VirtualFolders, 0)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestDumpdata(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t_, rawResp, err := httpdtest.Dumpdata(\"\", \"\", \"\", http.StatusBadRequest)\n\tassert.NoError(t, err, string(rawResp))\n\t_, _, err = httpdtest.Dumpdata(filepath.Join(backupsPath, \"backup.json\"), \"\", \"\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, rawResp, err = httpdtest.Dumpdata(\"../backup.json\", \"\", \"\", http.StatusBadRequest)\n\tassert.NoError(t, err, string(rawResp))\n\t_, rawResp, err = httpdtest.Dumpdata(\"backup.json\", \"\", \"0\", http.StatusOK)\n\tassert.NoError(t, err, string(rawResp))\n\tresponse, _, err := httpdtest.Dumpdata(\"\", \"1\", \"0\", http.StatusOK)\n\tassert.NoError(t, err)\n\t_, ok := response[\"admins\"]\n\tassert.True(t, ok)\n\t_, ok = response[\"users\"]\n\tassert.True(t, ok)\n\t_, ok = response[\"groups\"]\n\tassert.True(t, ok)\n\t_, ok = response[\"folders\"]\n\tassert.True(t, ok)\n\t_, ok = response[\"api_keys\"]\n\tassert.True(t, ok)\n\t_, ok = response[\"shares\"]\n\tassert.True(t, ok)\n\t_, ok = response[\"version\"]\n\tassert.True(t, ok)\n\t_, rawResp, err = httpdtest.Dumpdata(\"backup.json\", \"\", \"1\", http.StatusOK)\n\tassert.NoError(t, err, string(rawResp))\n\terr = os.Remove(filepath.Join(backupsPath, \"backup.json\"))\n\tassert.NoError(t, err)\n\tif runtime.GOOS != osWindows {\n\t\terr = os.Chmod(backupsPath, 0001)\n\t\tassert.NoError(t, err)\n\t\t_, _, err = httpdtest.Dumpdata(\"bck.json\", \"\", \"\", http.StatusForbidden)\n\t\tassert.NoError(t, err)\n\t\t// subdir cannot be created\n\t\t_, _, err = httpdtest.Dumpdata(filepath.Join(\"subdir\", \"bck.json\"), \"\", \"\", http.StatusForbidden)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(backupsPath, 0755)\n\t\tassert.NoError(t, err)\n\t}\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestDefenderAPI(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tdrivers := []string{common.DefenderDriverMemory}\n\tif isDbDefenderSupported() {\n\t\tdrivers = append(drivers, common.DefenderDriverProvider)\n\t}\n\n\tfor _, driver := range drivers {\n\t\tcfg := config.GetCommonConfig()\n\t\tcfg.DefenderConfig.Enabled = true\n\t\tcfg.DefenderConfig.Driver = driver\n\t\tcfg.DefenderConfig.Threshold = 3\n\t\tcfg.DefenderConfig.ScoreLimitExceeded = 2\n\t\tcfg.DefenderConfig.ScoreNoAuth = 0\n\n\t\terr := common.Initialize(cfg, 0)\n\t\tassert.NoError(t, err)\n\n\t\tip := \"::1\"\n\n\t\thosts, _, err := httpdtest.GetDefenderHosts(http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, hosts, 0)\n\n\t\t_, err = httpdtest.RemoveDefenderHostByIP(ip, http.StatusNotFound)\n\t\tassert.NoError(t, err)\n\n\t\tcommon.AddDefenderEvent(ip, common.ProtocolHTTP, common.HostEventNoLoginTried)\n\t\thosts, _, err = httpdtest.GetDefenderHosts(http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, hosts, 0)\n\t\tcommon.AddDefenderEvent(ip, common.ProtocolHTTP, common.HostEventUserNotFound)\n\t\thosts, _, err = httpdtest.GetDefenderHosts(http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tif assert.Len(t, hosts, 1) {\n\t\t\thost := hosts[0]\n\t\t\tassert.Empty(t, host.GetBanTime())\n\t\t\tassert.Equal(t, 2, host.Score)\n\t\t\tassert.Equal(t, ip, host.IP)\n\t\t}\n\t\thost, _, err := httpdtest.GetDefenderHostByIP(ip, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Empty(t, host.GetBanTime())\n\t\tassert.Equal(t, 2, host.Score)\n\n\t\tcommon.AddDefenderEvent(ip, common.ProtocolHTTP, common.HostEventUserNotFound)\n\t\thosts, _, err = httpdtest.GetDefenderHosts(http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tif assert.Len(t, hosts, 1) {\n\t\t\thost := hosts[0]\n\t\t\tassert.NotEmpty(t, host.GetBanTime())\n\t\t\tassert.Equal(t, 0, host.Score)\n\t\t\tassert.Equal(t, ip, host.IP)\n\t\t}\n\t\thost, _, err = httpdtest.GetDefenderHostByIP(ip, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.NotEmpty(t, host.GetBanTime())\n\t\tassert.Equal(t, 0, host.Score)\n\n\t\t_, err = httpdtest.RemoveDefenderHostByIP(ip, http.StatusOK)\n\t\tassert.NoError(t, err)\n\n\t\t_, _, err = httpdtest.GetDefenderHostByIP(ip, http.StatusNotFound)\n\t\tassert.NoError(t, err)\n\n\t\tcommon.AddDefenderEvent(ip, common.ProtocolHTTP, common.HostEventUserNotFound)\n\t\tcommon.AddDefenderEvent(ip, common.ProtocolHTTP, common.HostEventUserNotFound)\n\t\thosts, _, err = httpdtest.GetDefenderHosts(http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, hosts, 1)\n\n\t\t_, err = httpdtest.RemoveDefenderHostByIP(ip, http.StatusOK)\n\t\tassert.NoError(t, err)\n\n\t\thost, _, err = httpdtest.GetDefenderHostByIP(ip, http.StatusNotFound)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.RemoveDefenderHostByIP(ip, http.StatusNotFound)\n\t\tassert.NoError(t, err)\n\n\t\thost, _, err = httpdtest.GetDefenderHostByIP(\"invalid_ip\", http.StatusBadRequest)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.RemoveDefenderHostByIP(\"invalid_ip\", http.StatusBadRequest)\n\t\tassert.NoError(t, err)\n\t\tif driver == common.DefenderDriverProvider {\n\t\t\terr = dataprovider.CleanupDefender(util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Hour)))\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\terr := common.Initialize(oldConfig, 0)\n\trequire.NoError(t, err)\n}\n\nfunc TestDefenderAPIErrors(t *testing.T) {\n\tif isDbDefenderSupported() {\n\t\toldConfig := config.GetCommonConfig()\n\n\t\tcfg := config.GetCommonConfig()\n\t\tcfg.DefenderConfig.Enabled = true\n\t\tcfg.DefenderConfig.Driver = common.DefenderDriverProvider\n\t\terr := common.Initialize(cfg, 0)\n\t\trequire.NoError(t, err)\n\n\t\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\t\tassert.NoError(t, err)\n\n\t\terr = dataprovider.Close()\n\t\tassert.NoError(t, err)\n\n\t\treq, err := http.NewRequest(http.MethodGet, defenderHosts, nil)\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, token)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\t\terr = config.LoadConfig(configDir, \"\")\n\t\tassert.NoError(t, err)\n\t\tproviderConf := config.GetProviderConf()\n\t\tproviderConf.BackupsPath = backupsPath\n\t\terr = dataprovider.Initialize(providerConf, configDir, true)\n\t\tassert.NoError(t, err)\n\n\t\terr = common.Initialize(oldConfig, 0)\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc TestRestoreShares(t *testing.T) {\n\t// shares should be restored preserving the UsedTokens, CreatedAt, LastUseAt, UpdatedAt,\n\t// and ExpiresAt, so an expired share can be restored while we cannot create an already\n\t// expired share\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tshare := dataprovider.Share{\n\t\tShareID:     shortuuid.New(),\n\t\tName:        \"share name\",\n\t\tDescription: \"share description\",\n\t\tScope:       dataprovider.ShareScopeRead,\n\t\tPaths:       []string{\"/\"},\n\t\tUsername:    user.Username,\n\t\tCreatedAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(-144 * time.Hour)),\n\t\tUpdatedAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(-96 * time.Hour)),\n\t\tLastUseAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(-64 * time.Hour)),\n\t\tExpiresAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(-48 * time.Hour)),\n\t\tMaxTokens:   10,\n\t\tUsedTokens:  8,\n\t\tAllowFrom:   []string{\"127.0.0.0/8\"},\n\t}\n\tbackupData := dataprovider.BackupData{\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupData.Shares = append(backupData.Shares, share)\n\tbackupContent, err := json.Marshal(backupData)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.LoaddataFromPostBody(backupContent, \"0\", \"0\", http.StatusOK)\n\tassert.NoError(t, err)\n\tshareGet, err := dataprovider.ShareExists(share.ShareID, user.Username)\n\tassert.NoError(t, err)\n\tassert.Equal(t, share, shareGet)\n\n\tshare.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-142 * time.Hour))\n\tshare.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-92 * time.Hour))\n\tshare.LastUseAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-62 * time.Hour))\n\tshare.UsedTokens = 6\n\tbackupData.Shares = []dataprovider.Share{share}\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.LoaddataFromPostBody(backupContent, \"0\", \"0\", http.StatusOK)\n\tassert.NoError(t, err)\n\tshareGet, err = dataprovider.ShareExists(share.ShareID, user.Username)\n\tassert.NoError(t, err)\n\tassert.Equal(t, share, shareGet)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoaddataFromPostBody(t *testing.T) {\n\tmappedPath := filepath.Join(os.TempDir(), \"restored_folder\")\n\tfolderName := filepath.Base(mappedPath)\n\trole := getTestRole()\n\trole.ID = 1\n\trole.Name = \"test_restored_role\"\n\tgroup := getTestGroup()\n\tgroup.ID = 1\n\tgroup.Name = \"test_group_restored\"\n\tuser := getTestUser()\n\tuser.ID = 1\n\tuser.Username = \"test_user_restored\"\n\tuser.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser.Role = role.Name\n\tadmin := getTestAdmin()\n\tadmin.ID = 1\n\tadmin.Username = \"test_admin_restored\"\n\tadmin.Permissions = []string{dataprovider.PermAdminAddUsers, dataprovider.PermAdminChangeUsers,\n\t\tdataprovider.PermAdminDeleteUsers, dataprovider.PermAdminViewUsers}\n\tadmin.Role = role.Name\n\tbackupData := dataprovider.BackupData{\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupData.Users = append(backupData.Users, user)\n\tbackupData.Groups = append(backupData.Groups, group)\n\tbackupData.Admins = append(backupData.Admins, admin)\n\tbackupData.Roles = append(backupData.Roles, role)\n\tbackupData.Folders = []vfs.BaseVirtualFolder{\n\t\t{\n\t\t\tName:            folderName,\n\t\t\tMappedPath:      mappedPath,\n\t\t\tUsedQuotaSize:   123,\n\t\t\tUsedQuotaFiles:  456,\n\t\t\tLastQuotaUpdate: 789,\n\t\t\tUsers:           []string{\"user\"},\n\t\t},\n\t\t{\n\t\t\tName:       folderName,\n\t\t\tMappedPath: mappedPath + \"1\",\n\t\t},\n\t}\n\tbackupData.APIKeys = append(backupData.APIKeys, dataprovider.APIKey{})\n\tbackupData.Shares = append(backupData.Shares, dataprovider.Share{})\n\tbackupContent, err := json.Marshal(backupData)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.LoaddataFromPostBody(nil, \"0\", \"0\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.LoaddataFromPostBody(backupContent, \"a\", \"0\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.LoaddataFromPostBody([]byte(\"invalid content\"), \"0\", \"0\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.LoaddataFromPostBody(backupContent, \"0\", \"0\", http.StatusInternalServerError)\n\tassert.NoError(t, err)\n\n\tkeyID := util.GenerateUniqueID()\n\tbackupData.APIKeys = []dataprovider.APIKey{\n\t\t{\n\t\t\tName:  \"test key\",\n\t\t\tScope: dataprovider.APIKeyScopeAdmin,\n\t\t\tKeyID: keyID,\n\t\t\tKey:   fmt.Sprintf(\"%v.%v\", util.GenerateUniqueID(), util.GenerateUniqueID()),\n\t\t},\n\t}\n\tbackupData.Shares = []dataprovider.Share{\n\t\t{\n\t\t\tShareID:  keyID,\n\t\t\tName:     keyID,\n\t\t\tScope:    dataprovider.ShareScopeWrite,\n\t\t\tPaths:    []string{\"/\"},\n\t\t\tUsername: user.Username,\n\t\t},\n\t}\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\t_, resp, err := httpdtest.LoaddataFromPostBody(backupContent, \"0\", \"0\", http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, user.Role)\n\tif assert.Len(t, user.Groups, 1) {\n\t\tassert.Equal(t, sdk.GroupTypePrimary, user.Groups[0].Type)\n\t\tassert.Equal(t, group.Name, user.Groups[0].Name)\n\t}\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, role.Admins, 1)\n\tassert.Len(t, role.Users, 1)\n\t_, err = dataprovider.ShareExists(keyID, user.Username)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, admin.Role)\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, role.Admins, 0)\n\tassert.Len(t, role.Users, 0)\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n\tapiKey, _, err := httpdtest.GetAPIKeyByID(keyID, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAPIKey(apiKey, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath+\"1\", folder.MappedPath)\n\tassert.Equal(t, int64(123), folder.UsedQuotaSize)\n\tassert.Equal(t, 456, folder.UsedQuotaFiles)\n\tassert.Equal(t, int64(789), folder.LastQuotaUpdate)\n\tassert.Len(t, folder.Users, 0)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoaddata(t *testing.T) {\n\terr := dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tmappedPath := filepath.Join(os.TempDir(), \"restored_folder\")\n\tfolderName := filepath.Base(mappedPath)\n\tfolderDesc := \"restored folder desc\"\n\tuser := getTestUser()\n\tuser.ID = 1\n\tuser.Username = \"test_user_restore\"\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: \"/vuserpath\",\n\t})\n\tgroup := getTestGroup()\n\tgroup.ID = 1\n\tgroup.Name = \"test_group_restore\"\n\tgroup.VirtualFolders = append(group.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: \"/vgrouppath\",\n\t})\n\trole := getTestRole()\n\trole.ID = 1\n\trole.Name = \"test_role_restore\"\n\tuser.Groups = append(user.Groups, sdk.GroupMapping{\n\t\tName: group.Name,\n\t\tType: sdk.GroupTypePrimary,\n\t})\n\tadmin := getTestAdmin()\n\tadmin.ID = 1\n\tadmin.Username = \"test_admin_restore\"\n\tadmin.Groups = []dataprovider.AdminGroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t},\n\t}\n\tipListEntry := dataprovider.IPListEntry{\n\t\tIPOrNet:     \"172.16.2.4/32\",\n\t\tDescription: \"entry desc\",\n\t\tType:        dataprovider.IPListTypeDefender,\n\t\tMode:        dataprovider.ListModeDeny,\n\t\tProtocols:   3,\n\t}\n\tapiKey := dataprovider.APIKey{\n\t\tName:  util.GenerateUniqueID(),\n\t\tScope: dataprovider.APIKeyScopeAdmin,\n\t\tKeyID: util.GenerateUniqueID(),\n\t\tKey:   fmt.Sprintf(\"%v.%v\", util.GenerateUniqueID(), util.GenerateUniqueID()),\n\t}\n\tshare := dataprovider.Share{\n\t\tShareID:  util.GenerateUniqueID(),\n\t\tName:     util.GenerateUniqueID(),\n\t\tScope:    dataprovider.ShareScopeRead,\n\t\tPaths:    []string{\"/\"},\n\t\tUsername: user.Username,\n\t}\n\taction := dataprovider.BaseEventAction{\n\t\tID:   81,\n\t\tName: \"test_restore_action\",\n\t\tType: dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint:      \"https://localhost:4567/action\",\n\t\t\t\tUsername:      defaultUsername,\n\t\t\t\tPassword:      kms.NewPlainSecret(defaultPassword),\n\t\t\t\tTimeout:       10,\n\t\t\t\tSkipTLSVerify: true,\n\t\t\t\tMethod:        http.MethodPost,\n\t\t\t\tBody:          `{\"event\":\"{{.Event}}\",\"name\":\"{{.Name}}\"}`,\n\t\t\t},\n\t\t},\n\t}\n\trule := dataprovider.EventRule{\n\t\tID:          100,\n\t\tName:        \"test_rule_restore\",\n\t\tDescription: \"\",\n\t\tTrigger:     dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"download\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\tconfigs := dataprovider.Configs{\n\t\tSFTPD: &dataprovider.SFTPDConfigs{\n\t\t\tHostKeyAlgos:   []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01},\n\t\t\tPublicKeyAlgos: []string{ssh.InsecureKeyAlgoDSA}, //nolint:staticcheck\n\t\t},\n\t\tSMTP: &dataprovider.SMTPConfigs{\n\t\t\tHost: \"mail.example.com\",\n\t\t\tPort: 587,\n\t\t\tFrom: \"from@example.net\",\n\t\t},\n\t}\n\tbackupData := dataprovider.BackupData{\n\t\tVersion: 14,\n\t}\n\tbackupData.Configs = &configs\n\tbackupData.Users = append(backupData.Users, user)\n\tbackupData.Roles = append(backupData.Roles, role)\n\tbackupData.Groups = append(backupData.Groups, group)\n\tbackupData.Admins = append(backupData.Admins, admin)\n\tbackupData.Folders = []vfs.BaseVirtualFolder{\n\t\t{\n\t\t\tName:            folderName,\n\t\t\tMappedPath:      mappedPath + \"1\",\n\t\t\tUsedQuotaSize:   123,\n\t\t\tUsedQuotaFiles:  456,\n\t\t\tLastQuotaUpdate: 789,\n\t\t\tUsers:           []string{\"user\"},\n\t\t},\n\t\t{\n\t\t\tMappedPath:  mappedPath,\n\t\t\tName:        folderName,\n\t\t\tDescription: folderDesc,\n\t\t},\n\t}\n\tbackupData.APIKeys = append(backupData.APIKeys, apiKey)\n\tbackupData.Shares = append(backupData.Shares, share)\n\tbackupData.EventActions = append(backupData.EventActions, action)\n\tbackupData.EventRules = append(backupData.EventRules, rule)\n\tbackupData.IPLists = append(backupData.IPLists, ipListEntry)\n\tbackupContent, err := json.Marshal(backupData)\n\tassert.NoError(t, err)\n\tbackupFilePath := filepath.Join(backupsPath, \"backup.json\")\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"a\", \"\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"\", \"a\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(\"backup.json\", \"1\", \"\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath+\"a\", \"1\", \"\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tif runtime.GOOS != osWindows {\n\t\terr = os.Chmod(backupFilePath, 0111)\n\t\tassert.NoError(t, err)\n\t\t_, _, err = httpdtest.Loaddata(backupFilePath, \"1\", \"\", http.StatusBadRequest)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(backupFilePath, 0644)\n\t\tassert.NoError(t, err)\n\t}\n\t// add objects from backup\n\t_, resp, err := httpdtest.Loaddata(backupFilePath, \"1\", \"\", http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\t// update from backup\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"2\", \"\", http.StatusOK)\n\tassert.NoError(t, err)\n\tconfigsGet, err := dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Equal(t, configs.SMTP, configsGet.SMTP)\n\tassert.Equal(t, []string{ssh.KeyAlgoRSA}, configsGet.SFTPD.HostKeyAlgos)\n\tassert.Equal(t, []string{ssh.InsecureKeyAlgoDSA}, configsGet.SFTPD.PublicKeyAlgos) //nolint:staticcheck\n\tassert.Len(t, configsGet.SFTPD.KexAlgorithms, 0)\n\tassert.Len(t, configsGet.SFTPD.Ciphers, 0)\n\tassert.Len(t, configsGet.SFTPD.MACs, 0)\n\tassert.Greater(t, configsGet.UpdatedAt, int64(0))\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.VirtualFolders, 1)\n\tassert.Len(t, user.Groups, 1)\n\t_, err = dataprovider.ShareExists(share.ShareID, user.Username)\n\tassert.NoError(t, err)\n\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group.VirtualFolders, 1)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, admin.Groups, 1)\n\n\tapiKey, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusOK)\n\tassert.NoError(t, err)\n\n\taction, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tentry, _, err := httpdtest.GetIPListEntry(ipListEntry.IPOrNet, ipListEntry.Type, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, entry.CreatedAt, int64(0))\n\tassert.Greater(t, entry.UpdatedAt, int64(0))\n\tassert.Equal(t, ipListEntry.Description, entry.Description)\n\tassert.Equal(t, ipListEntry.Protocols, entry.Protocols)\n\tassert.Equal(t, ipListEntry.Mode, entry.Mode)\n\n\trule, _, err = httpdtest.GetEventRuleByName(rule.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, rule.Status)\n\tif assert.Len(t, rule.Actions, 1) {\n\t\tif assert.NotNil(t, rule.Actions[0].BaseEventAction.Options.HTTPConfig.Password) {\n\t\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, rule.Actions[0].BaseEventAction.Options.HTTPConfig.Password.GetStatus())\n\t\t\tassert.NotEmpty(t, rule.Actions[0].BaseEventAction.Options.HTTPConfig.Password.GetPayload())\n\t\t\tassert.Empty(t, rule.Actions[0].BaseEventAction.Options.HTTPConfig.Password.GetKey())\n\t\t\tassert.Empty(t, rule.Actions[0].BaseEventAction.Options.HTTPConfig.Password.GetAdditionalData())\n\t\t}\n\t}\n\n\tresponse, _, err := httpdtest.Dumpdata(\"\", \"1\", \"0\", http.StatusOK)\n\tassert.NoError(t, err)\n\tvar dumpedData dataprovider.BackupData\n\tdata, err := json.Marshal(response)\n\tassert.NoError(t, err)\n\terr = json.Unmarshal(data, &dumpedData)\n\tassert.NoError(t, err)\n\tfound := false\n\tif assert.GreaterOrEqual(t, len(dumpedData.Users), 1) {\n\t\tfor _, u := range dumpedData.Users {\n\t\t\tif u.Username == user.Username {\n\t\t\t\tfound = true\n\t\t\t\tassert.Equal(t, len(user.VirtualFolders), len(u.VirtualFolders))\n\t\t\t\tassert.Equal(t, len(user.Groups), len(u.Groups))\n\t\t\t}\n\t\t}\n\t}\n\tassert.True(t, found)\n\tfound = false\n\tif assert.GreaterOrEqual(t, len(dumpedData.Admins), 1) {\n\t\tfor _, a := range dumpedData.Admins {\n\t\t\tif a.Username == admin.Username {\n\t\t\t\tfound = true\n\t\t\t\tassert.Equal(t, len(admin.Groups), len(a.Groups))\n\t\t\t}\n\t\t}\n\t}\n\tassert.True(t, found)\n\tif assert.Len(t, dumpedData.Groups, 1) {\n\t\tassert.Equal(t, len(group.VirtualFolders), len(dumpedData.Groups[0].VirtualFolders))\n\t}\n\tif assert.Len(t, dumpedData.EventActions, 1) {\n\t\tassert.Equal(t, action.Name, dumpedData.EventActions[0].Name)\n\t}\n\tif assert.Len(t, dumpedData.EventRules, 1) {\n\t\tassert.Equal(t, rule.Name, dumpedData.EventRules[0].Name)\n\t\tassert.Len(t, dumpedData.EventRules[0].Actions, 1)\n\t}\n\tfound = false\n\tfor _, r := range dumpedData.Roles {\n\t\tif r.Name == role.Name {\n\t\t\tfound = true\n\t\t}\n\t}\n\tassert.True(t, found)\n\tfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath, folder.MappedPath)\n\tassert.Equal(t, int64(123), folder.UsedQuotaSize)\n\tassert.Equal(t, 456, folder.UsedQuotaFiles)\n\tassert.Equal(t, int64(789), folder.LastQuotaUpdate)\n\tassert.Equal(t, folderDesc, folder.Description)\n\tassert.Len(t, folder.Users, 1)\n\tresponse, _, err = httpdtest.Dumpdata(\"\", \"1\", \"0\", http.StatusOK, dataprovider.DumpScopeUsers)\n\tassert.NoError(t, err)\n\tdumpedData = dataprovider.BackupData{}\n\tdata, err = json.Marshal(response)\n\tassert.NoError(t, err)\n\terr = json.Unmarshal(data, &dumpedData)\n\tassert.NoError(t, err)\n\tassert.Greater(t, len(dumpedData.Users), 0)\n\tassert.Len(t, dumpedData.Admins, 0)\n\tassert.Len(t, dumpedData.Folders, 0)\n\tassert.Len(t, dumpedData.Groups, 0)\n\tassert.Len(t, dumpedData.Roles, 0)\n\tassert.Len(t, dumpedData.EventRules, 0)\n\tassert.Len(t, dumpedData.EventActions, 0)\n\tassert.Len(t, dumpedData.IPLists, 0)\n\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAPIKey(apiKey, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveIPListEntry(entry, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = os.Remove(backupFilePath)\n\tassert.NoError(t, err)\n\terr = createTestFile(backupFilePath, 20*1048576+1)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"1\", \"0\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\terr = os.Remove(backupFilePath)\n\tassert.NoError(t, err)\n\terr = createTestFile(backupFilePath, 65535)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"1\", \"0\", http.StatusBadRequest)\n\tassert.NoError(t, err)\n\terr = os.Remove(backupFilePath)\n\tassert.NoError(t, err)\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestLoaddataConvertActions(t *testing.T) {\n\ta1 := dataprovider.BaseEventAction{\n\t\tName: xid.New().String(),\n\t\tType: dataprovider.ActionTypeEmail,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\t\tRecipients: []string{\"failure@example.com\"},\n\t\t\t\tSubject:    `Failed \"{{Event}}\" from \"{{Name}}\"`,\n\t\t\t\tBody:       \"Object name: {{ObjectName}} object type: {{ObjectType}}, IP: {{IP}}\",\n\t\t\t},\n\t\t},\n\t}\n\ta2 := dataprovider.BaseEventAction{\n\t\tName: xid.New().String(),\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType: dataprovider.FilesystemActionRename,\n\t\t\t\tRenames: []dataprovider.RenameConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\t\t\tKey:   \"/{{VirtualDirPath}}/{{ObjectName}}\",\n\t\t\t\t\t\t\tValue: \"/{{ObjectName}}_renamed\",\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\tbackupData := dataprovider.BackupData{\n\t\tEventActions: []dataprovider.BaseEventAction{a1, a2},\n\t\tVersion:      16,\n\t}\n\tbackupContent, err := json.Marshal(backupData)\n\tassert.NoError(t, err)\n\tbackupFilePath := filepath.Join(backupsPath, \"backup.json\")\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, resp, err := httpdtest.Loaddata(backupFilePath, \"1\", \"2\", http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\t// Check that actions are migrated.\n\taction1, _, err := httpdtest.GetEventActionByName(a1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\taction2, _, err := httpdtest.GetEventActionByName(a2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, `Failed \"{{.Event}}\" from \"{{.Name}}\"`, action1.Options.EmailConfig.Subject)\n\tassert.Equal(t, `Object name: {{.ObjectName}} object type: {{.ObjectType}}, IP: {{.IP}}`, action1.Options.EmailConfig.Body)\n\tassert.Equal(t, `/{{.VirtualDirPath}}/{{.ObjectName}}`, action2.Options.FsConfig.Renames[0].Key)\n\tassert.Equal(t, `/{{.ObjectName}}_renamed`, action2.Options.FsConfig.Renames[0].Value)\n\t// If we restore a backup from the current version actions are not migrated.\n\tbackupData = dataprovider.BackupData{\n\t\tEventActions: []dataprovider.BaseEventAction{a1, a2},\n\t\tVersion:      dataprovider.DumpVersion,\n\t}\n\tbackupContent, err = json.Marshal(backupData)\n\tassert.NoError(t, err)\n\tbackupFilePath = filepath.Join(backupsPath, \"backup.json\")\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, resp, err = httpdtest.Loaddata(backupFilePath, \"1\", \"2\", http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\taction1, _, err = httpdtest.GetEventActionByName(a1.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\taction2, _, err = httpdtest.GetEventActionByName(a2.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, `Failed \"{{Event}}\" from \"{{Name}}\"`, action1.Options.EmailConfig.Subject)\n\tassert.Equal(t, `Object name: {{ObjectName}} object type: {{ObjectType}}, IP: {{IP}}`, action1.Options.EmailConfig.Body)\n\tassert.Equal(t, `/{{VirtualDirPath}}/{{ObjectName}}`, action2.Options.FsConfig.Renames[0].Key)\n\tassert.Equal(t, `/{{ObjectName}}_renamed`, action2.Options.FsConfig.Renames[0].Value)\n\t// Cleanup.\n\t_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)\n\tassert.NoError(t, err)\n\tactions, _, err := httpdtest.GetEventActions(0, 0, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, actions, 0)\n}\n\nfunc TestLoaddataMode(t *testing.T) {\n\terr := dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tmappedPath := filepath.Join(os.TempDir(), \"restored_fold\")\n\tfolderName := filepath.Base(mappedPath)\n\tconfigs := dataprovider.Configs{\n\t\tSFTPD: &dataprovider.SFTPDConfigs{\n\t\t\tPublicKeyAlgos: []string{ssh.KeyAlgoRSA},\n\t\t},\n\t}\n\trole := getTestRole()\n\trole.ID = 1\n\trole.Name = \"test_role_load\"\n\trole.Description = \"\"\n\tuser := getTestUser()\n\tuser.ID = 1\n\tuser.Username = \"test_user_restore\"\n\tuser.Role = role.Name\n\tgroup := getTestGroup()\n\tgroup.ID = 1\n\tgroup.Name = \"test_group_restore\"\n\tuser.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tadmin := getTestAdmin()\n\tadmin.ID = 1\n\tadmin.Username = \"test_admin_restore\"\n\tapiKey := dataprovider.APIKey{\n\t\tName:        util.GenerateUniqueID(),\n\t\tScope:       dataprovider.APIKeyScopeAdmin,\n\t\tKeyID:       util.GenerateUniqueID(),\n\t\tKey:         fmt.Sprintf(\"%v.%v\", util.GenerateUniqueID(), util.GenerateUniqueID()),\n\t\tDescription: \"desc\",\n\t}\n\tshare := dataprovider.Share{\n\t\tShareID:  util.GenerateUniqueID(),\n\t\tName:     util.GenerateUniqueID(),\n\t\tScope:    dataprovider.ShareScopeRead,\n\t\tPaths:    []string{\"/\"},\n\t\tUsername: user.Username,\n\t}\n\taction := dataprovider.BaseEventAction{\n\t\tID:          81,\n\t\tName:        \"test_restore_action_data_mode\",\n\t\tDescription: \"action desc\",\n\t\tType:        dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint:      \"https://localhost:4567/mode\",\n\t\t\t\tUsername:      defaultUsername,\n\t\t\t\tPassword:      kms.NewPlainSecret(defaultPassword),\n\t\t\t\tTimeout:       10,\n\t\t\t\tSkipTLSVerify: true,\n\t\t\t\tMethod:        http.MethodPost,\n\t\t\t\tBody:          `{\"event\":\"{{.Event}}\",\"name\":\"{{.Name}}\"}`,\n\t\t\t},\n\t\t},\n\t}\n\trule := dataprovider.EventRule{\n\t\tID:          100,\n\t\tName:        \"test_rule_restore_data_mode\",\n\t\tDescription: \"rule desc\",\n\t\tTrigger:     dataprovider.EventTriggerFsEvent,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tFsEvents: []string{\"mkdir\"},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\tipListEntry := dataprovider.IPListEntry{\n\t\tIPOrNet:     \"10.8.3.9/32\",\n\t\tDescription: \"note\",\n\t\tType:        dataprovider.IPListTypeDefender,\n\t\tMode:        dataprovider.ListModeDeny,\n\t\tProtocols:   7,\n\t}\n\tbackupData := dataprovider.BackupData{\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupData.Configs = &configs\n\tbackupData.Users = append(backupData.Users, user)\n\tbackupData.Groups = append(backupData.Groups, group)\n\tbackupData.Admins = append(backupData.Admins, admin)\n\tbackupData.EventActions = append(backupData.EventActions, action)\n\tbackupData.EventRules = append(backupData.EventRules, rule)\n\tbackupData.Roles = append(backupData.Roles, role)\n\tbackupData.Folders = []vfs.BaseVirtualFolder{\n\t\t{\n\t\t\tName:            folderName,\n\t\t\tMappedPath:      mappedPath,\n\t\t\tUsedQuotaSize:   123,\n\t\t\tUsedQuotaFiles:  456,\n\t\t\tLastQuotaUpdate: 789,\n\t\t\tUsers:           []string{\"user\"},\n\t\t},\n\t\t{\n\t\t\tMappedPath: mappedPath + \"1\",\n\t\t\tName:       folderName,\n\t\t},\n\t}\n\tbackupData.APIKeys = append(backupData.APIKeys, apiKey)\n\tbackupData.Shares = append(backupData.Shares, share)\n\tbackupData.IPLists = append(backupData.IPLists, ipListEntry)\n\tbackupContent, _ := json.Marshal(backupData)\n\tbackupFilePath := filepath.Join(backupsPath, \"backup.json\")\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"0\", \"0\", http.StatusOK)\n\tassert.NoError(t, err)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)\n\tfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath+\"1\", folder.MappedPath)\n\tassert.Equal(t, int64(123), folder.UsedQuotaSize)\n\tassert.Equal(t, 456, folder.UsedQuotaFiles)\n\tassert.Equal(t, int64(789), folder.LastQuotaUpdate)\n\tassert.Len(t, folder.Users, 0)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, user.Role)\n\toldUploadBandwidth := user.UploadBandwidth\n\tuser.UploadBandwidth = oldUploadBandwidth + 128\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, role.Users, 1)\n\tassert.Len(t, role.Admins, 0)\n\tassert.Empty(t, role.Description)\n\trole.Description = \"role desc\"\n\t_, _, err = httpdtest.UpdateRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n\trole.Description = \"\"\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, group.Users, 1)\n\toldGroupDesc := group.Description\n\tgroup.Description = \"new group description\"\n\tgroup, _, err = httpdtest.UpdateGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\toldInfo := admin.AdditionalInfo\n\toldDesc := admin.Description\n\tadmin.AdditionalInfo = \"newInfo\"\n\tadmin.Description = \"newDesc\"\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tapiKey, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusOK)\n\tassert.NoError(t, err)\n\toldAPIKeyDesc := apiKey.Description\n\tapiKey.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tapiKey.Description = \"new desc\"\n\tapiKey, _, err = httpdtest.UpdateAPIKey(apiKey, http.StatusOK)\n\tassert.NoError(t, err)\n\tshare.Description = \"test desc\"\n\terr = dataprovider.UpdateShare(&share, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\taction, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\toldActionDesc := action.Description\n\taction.Description = \"new action description\"\n\taction, _, err = httpdtest.UpdateEventAction(action, http.StatusOK)\n\tassert.NoError(t, err)\n\n\trule, _, err = httpdtest.GetEventRuleByName(rule.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, rule.Status)\n\toldRuleDesc := rule.Description\n\trule.Description = \"new rule description\"\n\trule, _, err = httpdtest.UpdateEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tentry, _, err := httpdtest.GetIPListEntry(ipListEntry.IPOrNet, ipListEntry.Type, http.StatusOK)\n\tassert.NoError(t, err)\n\toldEntryDesc := entry.Description\n\tentry.Description = \"new note\"\n\tentry, _, err = httpdtest.UpdateIPListEntry(entry, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tconfigs.SFTPD.PublicKeyAlgos = append(configs.SFTPD.PublicKeyAlgos, ssh.InsecureKeyAlgoDSA) //nolint:staticcheck\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tbackupData.Configs = &configs\n\tbackupData.Folders = []vfs.BaseVirtualFolder{\n\t\t{\n\t\t\tMappedPath: mappedPath,\n\t\t\tName:       folderName,\n\t\t},\n\t}\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"0\", \"1\", http.StatusOK)\n\tassert.NoError(t, err)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Len(t, configs.SFTPD.PublicKeyAlgos, 2)\n\tgroup, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, oldGroupDesc, group.Description)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath+\"1\", folder.MappedPath)\n\tassert.Equal(t, int64(123), folder.UsedQuotaSize)\n\tassert.Equal(t, 456, folder.UsedQuotaFiles)\n\tassert.Equal(t, int64(789), folder.LastQuotaUpdate)\n\tassert.Len(t, folder.Users, 0)\n\taction, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, oldActionDesc, action.Description)\n\trule, _, err = httpdtest.GetEventRuleByName(rule.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, oldRuleDesc, rule.Description)\n\tentry, _, err = httpdtest.GetIPListEntry(ipListEntry.IPOrNet, ipListEntry.Type, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, oldEntryDesc, entry.Description)\n\n\tc := common.NewBaseConnection(\"connID\", common.ProtocolFTP, \"\", \"\", user)\n\tfakeConn := &fakeConnection{\n\t\tBaseConnection: c,\n\t}\n\terr = common.Connections.Add(fakeConn)\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 1)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, oldUploadBandwidth, user.UploadBandwidth)\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, oldInfo, admin.AdditionalInfo)\n\tassert.NotEqual(t, oldDesc, admin.Description)\n\n\tapiKey, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, int64(0), apiKey.ExpiresAt)\n\tassert.NotEqual(t, oldAPIKeyDesc, apiKey.Description)\n\n\tshare, err = dataprovider.ShareExists(share.ShareID, user.Username)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, share.Description)\n\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, role.Users, 1)\n\tassert.Len(t, role.Admins, 0)\n\tassert.NotEmpty(t, role.Description)\n\n\t_, _, err = httpdtest.Loaddata(backupFilePath, \"0\", \"2\", http.StatusOK)\n\tassert.NoError(t, err)\n\t// mode 2 will update the user and close the previous connection\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, oldUploadBandwidth, user.UploadBandwidth)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)\n\t// the group is referenced\n\t_, err = httpdtest.RemoveGroup(group, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAPIKey(apiKey, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveIPListEntry(entry, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(backupFilePath)\n\tassert.NoError(t, err)\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestRateLimiter(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.RateLimitersConfig = []common.RateLimiterConfig{\n\t\t{\n\t\t\tAverage:   1,\n\t\t\tPeriod:    1000,\n\t\t\tBurst:     1,\n\t\t\tType:      1,\n\t\t\tProtocols: []string{common.ProtocolHTTP},\n\t\t},\n\t}\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tclient := &http.Client{\n\t\tTimeout: 5 * time.Second,\n\t}\n\tresp, err := client.Get(httpBaseURL + healthzPath)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\tresp, err = client.Get(httpBaseURL + healthzPath)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusTooManyRequests, resp.StatusCode)\n\tassert.Equal(t, \"1\", resp.Header.Get(\"Retry-After\"))\n\tassert.NotEmpty(t, resp.Header.Get(\"X-Retry-In\"))\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\tresp, err = client.Get(httpBaseURL + webLoginPath)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusTooManyRequests, resp.StatusCode)\n\tassert.Equal(t, \"1\", resp.Header.Get(\"Retry-After\"))\n\tassert.NotEmpty(t, resp.Header.Get(\"X-Retry-In\"))\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\tresp, err = client.Get(httpBaseURL + webClientLoginPath)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusTooManyRequests, resp.StatusCode)\n\tassert.Equal(t, \"1\", resp.Header.Get(\"Retry-After\"))\n\tassert.NotEmpty(t, resp.Header.Get(\"X-Retry-In\"))\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPSConnection(t *testing.T) {\n\tclient := &http.Client{\n\t\tTimeout: 5 * time.Second,\n\t}\n\tresp, err := client.Get(\"https://localhost:8443\" + healthzPath)\n\tif assert.Error(t, err) {\n\t\tif !strings.Contains(err.Error(), \"certificate is not valid\") &&\n\t\t\t!strings.Contains(err.Error(), \"certificate signed by unknown authority\") &&\n\t\t\t!strings.Contains(err.Error(), \"certificate is not standards compliant\") {\n\t\t\tassert.Fail(t, err.Error())\n\t\t}\n\t} else {\n\t\tresp.Body.Close()\n\t}\n}\n\n// test using mock http server\n\nfunc TestBasicUserHandlingMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusConflict, rr)\n\tuser.MaxSessions = 10\n\tuser.UploadBandwidth = 128\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny, dataprovider.PermDelete, dataprovider.PermDownload}\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPut, userPath+\"/\"+user.Username, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, userPath+\"/\"+user.Username, nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tvar updatedUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updatedUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, user.MaxSessions, updatedUser.MaxSessions)\n\tassert.Equal(t, user.UploadBandwidth, updatedUser.UploadBandwidth)\n\tassert.Equal(t, 1, len(updatedUser.Permissions[\"/\"]))\n\tassert.True(t, slices.Contains(updatedUser.Permissions[\"/\"], dataprovider.PermAny))\n\treq, _ = http.NewRequest(http.MethodDelete, userPath+\"/\"+user.Username, nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestAddUserNoUsernameMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuser.Username = \"\"\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestAddUserInvalidHomeDirMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuser.HomeDir = \"relative_path\"\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestAddUserInvalidPermsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuser.Permissions[\"/\"] = []string{}\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestAddFolderInvalidJsonMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer([]byte(\"invalid json\")))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestAddEventRuleInvalidJsonMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, eventActionsPath, bytes.NewBuffer([]byte(\"invalid json\")))\n\trequire.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\treq, err = http.NewRequest(http.MethodPost, eventRulesPath, bytes.NewBuffer([]byte(\"invalid json\")))\n\trequire.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestAddRoleInvalidJsonMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, rolesPath, bytes.NewBuffer([]byte(\"{\")))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestIPListEntriesErrorsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, ipListsPath+\"/a/b\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"invalid list type\")\n\treq, err = http.NewRequest(http.MethodGet, ipListsPath+\"/invalid\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"invalid list type\")\n\n\treqBody := bytes.NewBuffer([]byte(\"{\"))\n\treq, err = http.NewRequest(http.MethodPost, ipListsPath+\"/2\", reqBody)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tentry := dataprovider.IPListEntry{\n\t\tIPOrNet:   \"172.120.1.1/32\",\n\t\tType:      dataprovider.IPListTypeAllowList,\n\t\tMode:      dataprovider.ListModeAllow,\n\t\tProtocols: 0,\n\t}\n\t_, _, err = httpdtest.AddIPListEntry(entry, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPut, path.Join(ipListsPath, \"1\", url.PathEscape(entry.IPOrNet)), reqBody)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\t_, err = httpdtest.RemoveIPListEntry(entry, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestRoleErrorsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treqBody := bytes.NewBuffer([]byte(\"{\"))\n\treq, err := http.NewRequest(http.MethodGet, rolesPath+\"?limit=a\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\trole, _, err := httpdtest.AddRole(getTestRole(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPut, path.Join(rolesPath, role.Name), reqBody)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, path.Join(rolesPath, \"missing_role\"), reqBody)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role, http.StatusNotFound)\n\tassert.NoError(t, err)\n}\n\nfunc TestEventRuleErrorsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treqBody := bytes.NewBuffer([]byte(\"invalid json body\"))\n\n\treq, err := http.NewRequest(http.MethodGet, eventActionsPath+\"?limit=b\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, eventRulesPath+\"?limit=c\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\ta := dataprovider.BaseEventAction{\n\t\tName:        \"action_name\",\n\t\tDescription: \"test description\",\n\t\tType:        dataprovider.ActionTypeBackup,\n\t\tOptions:     dataprovider.BaseEventActionOptions{},\n\t}\n\taction, _, err := httpdtest.AddEventAction(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPut, path.Join(eventActionsPath, action.Name), reqBody)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tr := dataprovider.EventRule{\n\t\tName:    \"test_event_rule\",\n\t\tTrigger: dataprovider.EventTriggerSchedule,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tSchedules: []dataprovider.Schedule{\n\t\t\t\t{\n\t\t\t\t\tHours:      \"2\",\n\t\t\t\t\tDayOfWeek:  \"*\",\n\t\t\t\t\tDayOfMonth: \"*\",\n\t\t\t\t\tMonth:      \"*\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\trule, _, err := httpdtest.AddEventRule(r, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPut, path.Join(eventRulesPath, rule.Name), reqBody)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\trule.Actions[0].Name = \"misssing action name\"\n\tasJSON, err := json.Marshal(rule)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, path.Join(eventRulesPath, rule.Name), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\t_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveEventAction(action, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestGroupErrorsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treqBody := bytes.NewBuffer([]byte(\"not a json string\"))\n\n\treq, err := http.NewRequest(http.MethodPost, groupPath, reqBody)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, groupPath+\"?limit=d\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tgroup, _, err := httpdtest.AddGroup(getTestGroup(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPut, path.Join(groupPath, group.Name), reqBody)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateFolderInvalidJsonMock(t *testing.T) {\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       \"name\",\n\t\tMappedPath: filepath.Clean(os.TempDir()),\n\t}\n\tfolder, resp, err := httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPut, path.Join(folderPath, folder.Name), bytes.NewBuffer([]byte(\"not a json\")))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddUserInvalidJsonMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer([]byte(\"invalid json\")))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestAddAdminInvalidJsonMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer([]byte(\"...\")))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestAddAdminNoPasswordMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tadmin := getTestAdmin()\n\tadmin.Password = \"\"\n\tasJSON, err := json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"please set a password\")\n}\n\nfunc TestAdminTwoFactorLogin(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\tadmin1 := getTestAdmin()\n\tadmin1.Username = altAdminUsername + \"1\"\n\tadmin1.Password = altAdminPassword\n\tvar permissions []string\n\tfor _, p := range admin1.GetValidPerms() {\n\t\tif p != dataprovider.PermAdminAny && p != dataprovider.PermAdminDisableMFA {\n\t\t\tpermissions = append(permissions, p)\n\t\t}\n\t}\n\tadmin1.Permissions = permissions\n\tadmin1, _, err = httpdtest.AddAdmin(admin1, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// enable two factor authentication\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)\n\tassert.NoError(t, err)\n\taltToken, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\tadminTOTPConfig := dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t}\n\tasJSON, err := json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\n\treq, err = http.NewRequest(http.MethodGet, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar recCodes []recoveryCode\n\terr = json.Unmarshal(rr.Body.Bytes(), &recCodes)\n\tassert.NoError(t, err)\n\tassert.Len(t, recCodes, 12)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, admin.Filters.RecoveryCodes, 12)\n\tfor _, c := range admin.Filters.RecoveryCodes {\n\t\tassert.Empty(t, c.Secret.GetAdditionalData())\n\t\tassert.Empty(t, c.Secret.GetKey())\n\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, c.Secret.GetStatus())\n\t\tassert.NotEmpty(t, c.Secret.GetPayload())\n\t}\n\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webAdminTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminTwoFactorRecoveryPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform := getLoginForm(altAdminUsername, altAdminPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err := getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\t// without a cookie\n\treq, err = http.NewRequest(http.MethodGet, webAdminTwoFactorRecoveryPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminTwoFactorRecoveryPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// any other page will be redirected to the two factor auth page\n\treq, err = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webAdminTwoFactorPath, rr.Header().Get(\"Location\"))\n\t// a partial token cannot be used for user pages\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"passcode\", passcode)\n\t// no csrf\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"passcode\", \"invalid_passcode\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform.Set(\"passcode\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform.Set(\"passcode\", passcode)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webUsersPath, rr.Header().Get(\"Location\"))\n\t// the same cookie cannot be reused\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\t// get a new cookie and login using a recovery code\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(altAdminUsername, altAdminPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err = getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\tform = make(url.Values)\n\trecoveryCode := recCodes[0].Code\n\tform.Set(\"recovery_code\", recoveryCode)\n\t// no csrf\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"recovery_code\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform.Set(\"recovery_code\", recoveryCode)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webUsersPath, rr.Header().Get(\"Location\"))\n\tauthenticatedCookie, err := getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\t//render MFA page\n\treq, err = http.NewRequest(http.MethodGet, webAdminMFAPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, authenticatedCookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// check that the recovery code was marked as used\n\treq, err = http.NewRequest(http.MethodGet, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\trecCodes = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &recCodes)\n\tassert.NoError(t, err)\n\tassert.Len(t, recCodes, 12)\n\tfound := false\n\tfor _, rc := range recCodes {\n\t\tif rc.Code == recoveryCode {\n\t\t\tfound = true\n\t\t\tassert.True(t, rc.Used)\n\t\t} else {\n\t\t\tassert.False(t, rc.Used)\n\t\t}\n\t}\n\tassert.True(t, found)\n\t// the same recovery code cannot be reused\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(altAdminUsername, altAdminPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err = getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"recovery_code\", recoveryCode)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform.Set(\"recovery_code\", \"invalid_recovery_code\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(altAdminUsername, altAdminPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\treq, err = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err = getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\t// disable TOTP\n\taltToken1, err := getJWTAPITokenFromTestServer(admin1.Username, altAdminPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, adminPath+\"/\"+altAdminUsername+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken1)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, adminPath+\"/\"+altAdminUsername+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, adminPath+\"/\"+altAdminUsername+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"two-factor authentication is not enabled\")\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"recovery_code\", recoveryCode)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18n2FADisabled)\n\n\tform.Set(\"passcode\", passcode)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18n2FADisabled)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAdmin(admin1, http.StatusOK)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminMFAPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, authenticatedCookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n}\n\nfunc TestAdminTOTP(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\t// TOTPConfig will be ignored on add\n\tadmin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: \"config\",\n\t\tSecret:     kms.NewEmptySecret(),\n\t}\n\tasJSON, err := json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, admin.Filters.RecoveryCodes, 0)\n\n\taltToken, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, adminTOTPConfigsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar configs []mfa.TOTPConfig\n\terr = json.Unmarshal(rr.Body.Bytes(), &configs)\n\tassert.NoError(t, err, rr.Body.String())\n\tassert.Len(t, configs, len(mfa.GetAvailableTOTPConfigs()))\n\ttotpConfig := configs[0]\n\ttotpReq := generateTOTPRequest{\n\t\tConfigName: totpConfig.Name,\n\t}\n\tasJSON, err = json.Marshal(totpReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPGeneratePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar totpGenResp generateTOTPResponse\n\terr = json.Unmarshal(rr.Body.Bytes(), &totpGenResp)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, totpGenResp.Secret)\n\tassert.NotEmpty(t, totpGenResp.QRCode)\n\n\tpasscode, err := generateTOTPPasscode(totpGenResp.Secret)\n\tassert.NoError(t, err)\n\tvalidateReq := validateTOTPRequest{\n\t\tConfigName: totpGenResp.ConfigName,\n\t\tPasscode:   passcode,\n\t\tSecret:     totpGenResp.Secret,\n\t}\n\tasJSON, err = json.Marshal(validateReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPValidatePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// the same passcode cannot be reused\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPValidatePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"this passcode was already used\")\n\n\tadminTOTPConfig := dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: totpGenResp.ConfigName,\n\t\tSecret:     kms.NewPlainSecret(totpGenResp.Secret),\n\t}\n\tasJSON, err = json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, totpGenResp.ConfigName, admin.Filters.TOTPConfig.ConfigName)\n\tassert.Empty(t, admin.Filters.TOTPConfig.Secret.GetKey())\n\tassert.Empty(t, admin.Filters.TOTPConfig.Secret.GetAdditionalData())\n\tassert.NotEmpty(t, admin.Filters.TOTPConfig.Secret.GetPayload())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, admin.Filters.TOTPConfig.Secret.GetStatus())\n\tadmin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled:    false,\n\t\tConfigName: util.GenerateUniqueID(),\n\t\tSecret:     kms.NewEmptySecret(),\n\t}\n\tadmin.Filters.RecoveryCodes = []dataprovider.RecoveryCode{\n\t\t{\n\t\t\tSecret: kms.NewEmptySecret(),\n\t\t},\n\t}\n\tadmin, resp, err := httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err, string(resp))\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, admin.Filters.RecoveryCodes, 12)\n\t// if we use token we cannot get or generate recovery codes\n\treq, err = http.NewRequest(http.MethodGet, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\treq, err = http.NewRequest(http.MethodPost, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t// now the same but with altToken\n\treq, err = http.NewRequest(http.MethodGet, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar recCodes []recoveryCode\n\terr = json.Unmarshal(rr.Body.Bytes(), &recCodes)\n\tassert.NoError(t, err)\n\tassert.Len(t, recCodes, 12)\n\t// regenerate recovery codes\n\treq, err = http.NewRequest(http.MethodPost, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// check that recovery codes are different\n\treq, err = http.NewRequest(http.MethodGet, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar newRecCodes []recoveryCode\n\terr = json.Unmarshal(rr.Body.Bytes(), &newRecCodes)\n\tassert.NoError(t, err)\n\tassert.Len(t, newRecCodes, 12)\n\tassert.NotEqual(t, recCodes, newRecCodes)\n\t// disable 2FA, the update admin API should not work\n\tadmin.Filters.TOTPConfig.Enabled = false\n\tadmin.Filters.RecoveryCodes = nil\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, altAdminUsername, admin.Username)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, admin.Filters.RecoveryCodes, 12)\n\t// use the dedicated API\n\treq, err = http.NewRequest(http.MethodPut, adminPath+\"/\"+altAdminUsername+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, admin.Filters.RecoveryCodes, 0)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(adminPath, altAdminUsername), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, adminPath+\"/\"+altAdminUsername+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, admin2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestChangeAdminPwdInvalidJsonMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPut, adminPwdPath, bytes.NewBuffer([]byte(\"{\")))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n}\n\nfunc TestSMTPConfig(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          3525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tsmtpTestURL := path.Join(webConfigsPath, \"smtp\", \"test\")\n\ttokenHeader := \"X-CSRF-TOKEN\"\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webConfigsPath, webToken)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer([]byte(\"{\")))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq.Header.Set(tokenHeader, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\ttestReq := make(map[string]any)\n\ttestReq[\"host\"] = smtpCfg.Host\n\ttestReq[\"port\"] = 3525\n\ttestReq[\"from\"] = \"from@example.com\"\n\tasJSON, err := json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(tokenHeader, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\ttestReq[\"recipient\"] = \"example@example.com\"\n\tasJSON, err = json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(tokenHeader, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tconfigs := dataprovider.Configs{\n\t\tSMTP: &dataprovider.SMTPConfigs{\n\t\t\tHost:     \"127.0.0.1\",\n\t\t\tPort:     3535,\n\t\t\tUser:     \"user@example.com\",\n\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\ttestReq[\"password\"] = redactedSecret\n\tasJSON, err = json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(tokenHeader, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\tassert.Contains(t, rr.Body.String(), \"server does not support SMTP AUTH\")\n\n\ttestReq[\"password\"] = \"\"\n\ttestReq[\"auth_type\"] = 3\n\ttestReq[\"oauth2\"] = smtp.OAuth2Config{\n\t\tClientSecret: redactedSecret,\n\t\tRefreshToken: redactedSecret,\n\t}\n\tasJSON, err = json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, smtpTestURL, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(tokenHeader, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"smtp oauth2: client id is required\")\n\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n}\n\nfunc TestOAuth2TokenRequest(t *testing.T) {\n\ttokenHeader := \"X-CSRF-TOKEN\"\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webConfigsPath, webToken)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, webOAuth2TokenPath, bytes.NewBuffer([]byte(\"{\")))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq.Header.Set(tokenHeader, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\ttestReq := make(map[string]any)\n\ttestReq[\"client_secret\"] = redactedSecret\n\tasJSON, err := json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webOAuth2TokenPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(tokenHeader, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"base redirect url is required\")\n\n\ttestReq[\"base_redirect_url\"] = \"http://localhost:8081\"\n\tasJSON, err = json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webOAuth2TokenPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(tokenHeader, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestMFAPermission(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webClientMFAPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientMFAPath\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser.Filters.WebClient = []string{sdk.WebClientMFADisabled}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\twebToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientMFAPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientMFAPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUserTwoFactorLogin(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// enable two factor authentication\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tadminToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolHTTP},\n\t}\n\tasJSON, err := json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar recCodes []recoveryCode\n\terr = json.Unmarshal(rr.Body.Bytes(), &recCodes)\n\tassert.NoError(t, err)\n\tassert.Len(t, recCodes, 12)\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.RecoveryCodes, 12)\n\tfor _, c := range user.Filters.RecoveryCodes {\n\t\tassert.Empty(t, c.Secret.GetAdditionalData())\n\t\tassert.Empty(t, c.Secret.GetKey())\n\t\tassert.Equal(t, sdkkms.SecretStatusSecretBox, c.Secret.GetStatus())\n\t\tassert.NotEmpty(t, c.Secret.GetPayload())\n\t}\n\n\treq, err = http.NewRequest(http.MethodGet, webClientTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientTwoFactorRecoveryPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform := getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\t// CSRF verification fails if there is no cookie\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err := getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// without a cookie\n\treq, err = http.NewRequest(http.MethodGet, webClientTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// invalid IP address\n\treq, err = http.NewRequest(http.MethodGet, webClientTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = \"6.7.8.9:4567\"\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientTwoFactorRecoveryPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// any other page will be redirected to the two factor auth page\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\t// a partial token cannot be used for admin pages\n\treq, err = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"passcode\", passcode)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientTwoFactorPath, cookie)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"passcode\", \"invalid_user_passcode\")\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform.Set(\"passcode\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform.Set(\"passcode\", passcode)\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientFilesPath, rr.Header().Get(\"Location\"))\n\t// the same cookie cannot be reused\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\t// get a new cookie and login using a recovery code\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err = getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\tform = make(url.Values)\n\trecoveryCode := recCodes[0].Code\n\tform.Set(\"recovery_code\", recoveryCode)\n\t// no csrf\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"recovery_code\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform.Set(\"recovery_code\", recoveryCode)\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientFilesPath, rr.Header().Get(\"Location\"))\n\tauthenticatedCookie, err := getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\t//render MFA page\n\treq, err = http.NewRequest(http.MethodGet, webClientMFAPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, authenticatedCookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// get MFA qrcode\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientMFAPath, \"qrcode?url=\"+url.QueryEscape(key.URL())), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, authenticatedCookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"image/png\", rr.Header().Get(\"Content-Type\"))\n\t// invalid MFA url\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientMFAPath, \"qrcode?url=\"+url.QueryEscape(\"http://foo\\x7f.eu\")), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, authenticatedCookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\t// check that the recovery code was marked as used\n\treq, err = http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\trecCodes = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &recCodes)\n\tassert.NoError(t, err)\n\tassert.Len(t, recCodes, 12)\n\tfound := false\n\tfor _, rc := range recCodes {\n\t\tif rc.Code == recoveryCode {\n\t\t\tfound = true\n\t\t\tassert.True(t, rc.Used)\n\t\t} else {\n\t\t\tassert.False(t, rc.Used)\n\t\t}\n\t}\n\tassert.True(t, found)\n\t// the same recovery code cannot be reused\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err = getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"recovery_code\", recoveryCode)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform.Set(\"recovery_code\", \"invalid_user_recovery_code\")\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err = getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\t// disable TOTP\n\treq, err = http.NewRequest(http.MethodPut, userPath+\"/\"+user.Username+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, userPath+\"/\"+user.Username+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"two-factor authentication is not enabled\")\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"recovery_code\", recoveryCode)\n\tform.Set(\"passcode\", passcode)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18n2FADisabled)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18n2FADisabled)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientMFAPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, authenticatedCookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n}\n\nfunc TestWebUserTwoFactoryLoginRedirect(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolHTTP},\n\t}\n\tasJSON, err := json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform := getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\turi := webClientFilesPath + \"?path=%2F\"\n\tloginURI := webClientLoginPath + \"?next=\" + url.QueryEscape(uri)\n\texpectedURI := webClientTwoFactorPath + \"?next=\" + url.QueryEscape(uri)\n\treq, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.RequestURI = loginURI\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, expectedURI, rr.Header().Get(\"Location\"))\n\tcookie, err := getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\t// test unsafe redirects\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\texternalURI := webClientLoginPath + \"?next=\" + url.QueryEscape(\"https://example.com\")\n\treq, err = http.NewRequest(http.MethodPost, externalURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.RequestURI = externalURI\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\tinternalURI := webClientLoginPath + \"?next=\" + url.QueryEscape(webClientMFAPath)\n\treq, err = http.NewRequest(http.MethodPost, internalURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.RequestURI = internalURI\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\t// render two factor page\n\treq, err = http.NewRequest(http.MethodGet, expectedURI, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = expectedURI\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), fmt.Sprintf(\"action=%q\", expectedURI))\n\t// login with the passcode\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(expectedURI, cookie)\n\tassert.NoError(t, err)\n\tpasscode, err := generateTOTPPasscode(key.Secret())\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(\"passcode\", passcode)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, expectedURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.RequestURI = expectedURI\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, uri, rr.Header().Get(\"Location\"))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSearchEvents(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, fsEventsPath+\"?limit=10&order=ASC&fs_provider=0\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tevents := make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &events)\n\tassert.NoError(t, err)\n\tif assert.Len(t, events, 1) {\n\t\tev := events[0]\n\t\tfor _, field := range []string{\"id\", \"timestamp\", \"action\", \"username\", \"fs_path\", \"status\", \"protocol\",\n\t\t\t\"ip\", \"session_id\", \"fs_provider\", \"bucket\", \"endpoint\", \"open_flags\", \"role\", \"instance_id\"} {\n\t\t\t_, ok := ev[field]\n\t\t\tassert.True(t, ok, field)\n\t\t}\n\t}\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?limit=10&order=ASC&role=role1\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tevents = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &events)\n\tassert.NoError(t, err)\n\tassert.Len(t, events, 1)\n\t// CSV export\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?limit=10&order=ASC&csv_export=true\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"text/csv\", rr.Header().Get(\"Content-Type\"))\n\t// the test eventsearcher plugin returns error if start_timestamp < 0\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?start_timestamp=-1&end_timestamp=123456&statuses=1,2\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\t// CSV export with error\n\texportFunc := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\n\t\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?start_timestamp=-2&csv_export=true\", nil)\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, token)\n\t\trr = executeRequest(req)\n\t}\n\texportFunc()\n\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?limit=e\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, providerEventsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tevents = make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &events)\n\tassert.NoError(t, err)\n\tif assert.Len(t, events, 1) {\n\t\tev := events[0]\n\t\tfor _, field := range []string{\"id\", \"timestamp\", \"action\", \"username\", \"object_type\", \"object_name\",\n\t\t\t\"object_data\", \"role\", \"instance_id\"} {\n\t\t\t_, ok := ev[field]\n\t\t\tassert.True(t, ok, field)\n\t\t}\n\t}\n\treq, err = http.NewRequest(http.MethodGet, providerEventsPath+\"?omit_object_data=true\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tevents = make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &events)\n\tassert.NoError(t, err)\n\tif assert.Len(t, events, 1) {\n\t\tev := events[0]\n\t\tfield := \"object_data\"\n\t\t_, ok := ev[field]\n\t\tassert.False(t, ok, field)\n\t}\n\t// CSV export\n\treq, err = http.NewRequest(http.MethodGet, providerEventsPath+\"?csv_export=true\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"text/csv\", rr.Header().Get(\"Content-Type\"))\n\n\t// the test eventsearcher plugin returns error if start_timestamp < 0\n\treq, err = http.NewRequest(http.MethodGet, providerEventsPath+\"?start_timestamp=-1\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\t// CSV export with error\n\texportFunc = func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\n\t\treq, err = http.NewRequest(http.MethodGet, providerEventsPath+\"?start_timestamp=-1&csv_export=true\", nil)\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, token)\n\t\trr = executeRequest(req)\n\t}\n\texportFunc()\n\n\treq, err = http.NewRequest(http.MethodGet, logEventsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tevents = make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &events)\n\tassert.NoError(t, err)\n\tif assert.Len(t, events, 1) {\n\t\tev := events[0]\n\t\tfor _, field := range []string{\"id\", \"timestamp\", \"event\", \"ip\", \"message\", \"role\", \"instance_id\"} {\n\t\t\t_, ok := ev[field]\n\t\t\tassert.True(t, ok, field)\n\t\t}\n\t}\n\treq, err = http.NewRequest(http.MethodGet, logEventsPath+\"?events=a,1\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// CSV export\n\treq, err = http.NewRequest(http.MethodGet, logEventsPath+\"?csv_export=true\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"text/csv\", rr.Header().Get(\"Content-Type\"))\n\t// the test eventsearcher plugin returns error if start_timestamp < 0\n\treq, err = http.NewRequest(http.MethodGet, logEventsPath+\"?start_timestamp=-1\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\t// CSV export with error\n\texportFunc = func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\n\t\treq, err = http.NewRequest(http.MethodGet, logEventsPath+\"?start_timestamp=-1&csv_export=true\", nil)\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, token)\n\t\trr = executeRequest(req)\n\t}\n\texportFunc()\n\n\treq, err = http.NewRequest(http.MethodGet, providerEventsPath+\"?limit=2000\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, logEventsPath+\"?limit=2000\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?start_timestamp=a\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?end_timestamp=a\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?order=ASSC\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?statuses=a,b\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, fsEventsPath+\"?fs_provider=a\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webEventsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestMFAErrors(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.False(t, user.Filters.TOTPConfig.Enabled)\n\tuserToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tadminToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\t// invalid config name\n\ttotpReq := generateTOTPRequest{\n\t\tConfigName: \"invalid config name\",\n\t}\n\tasJSON, err := json.Marshal(totpReq)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userTOTPGeneratePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// invalid JSON\n\tinvalidJSON := []byte(\"not a JSON\")\n\treq, err = http.NewRequest(http.MethodPost, userTOTPGeneratePath, bytes.NewBuffer(invalidJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(invalidJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(invalidJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPValidatePath, bytes.NewBuffer(invalidJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// invalid TOTP config name\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: \"missing name\",\n\t\tSecret:     kms.NewPlainSecret(xid.New().String()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"totp: config name\")\n\t// invalid TOTP secret\n\tuserTOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: mfa.GetAvailableTOTPConfigNames()[0],\n\t\tSecret:     nil,\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"totp: secret is mandatory\")\n\t// no protocol\n\tuserTOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: mfa.GetAvailableTOTPConfigNames()[0],\n\t\tSecret:     kms.NewPlainSecret(xid.New().String()),\n\t\tProtocols:  nil,\n\t}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"totp: specify at least one protocol\")\n\t// invalid protocol\n\tuserTOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: mfa.GetAvailableTOTPConfigNames()[0],\n\t\tSecret:     kms.NewPlainSecret(xid.New().String()),\n\t\tProtocols:  []string{common.ProtocolWebDAV},\n\t}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"totp: invalid protocol\")\n\n\tadminTOTPConfig := dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: \"\",\n\t\tSecret:     kms.NewPlainSecret(\"secret\"),\n\t}\n\tasJSON, err = json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"totp: config name is mandatory\")\n\n\tadminTOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: mfa.GetAvailableTOTPConfigNames()[0],\n\t\tSecret:     nil,\n\t}\n\tasJSON, err = json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"totp: secret is mandatory\")\n\n\t// invalid TOTP secret status\n\tuserTOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: mfa.GetAvailableTOTPConfigNames()[0],\n\t\tSecret:     kms.NewSecret(sdkkms.SecretStatusRedacted, \"\", \"\", \"\"),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// previous secret will be preserved and we have no secret saved\n\tassert.Contains(t, rr.Body.String(), \"totp: secret is mandatory\")\n\n\treq, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"totp: secret is mandatory\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMFAInvalidSecret(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuserToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: mfa.GetAvailableTOTPConfigNames()[0],\n\t\tSecret:     kms.NewSecret(sdkkms.SecretStatusSecretBox, \"payload\", \"key\", user.Username),\n\t\tProtocols:  []string{common.ProtocolSSH, common.ProtocolHTTP},\n\t}\n\tuser.Filters.RecoveryCodes = append(user.Filters.RecoveryCodes, dataprovider.RecoveryCode{\n\t\tUsed:   false,\n\t\tSecret: kms.NewSecret(sdkkms.SecretStatusSecretBox, \"payload\", \"key\", user.Username),\n\t})\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to decrypt recovery codes\")\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform := getLoginForm(defaultUsername, defaultPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err := getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientTwoFactorPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"passcode\", \"123456\")\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"recovery_code\", \"RC-123456\")\n\treq, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, userTokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", \"authcode\")\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusInternalServerError, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin, _, err = httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tadmin.Password = altAdminPassword\n\tadmin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: mfa.GetAvailableTOTPConfigNames()[0],\n\t\tSecret:     kms.NewSecret(sdkkms.SecretStatusSecretBox, \"payload\", \"key\", user.Username),\n\t}\n\tadmin.Filters.RecoveryCodes = append(user.Filters.RecoveryCodes, dataprovider.RecoveryCode{\n\t\tUsed:   false,\n\t\tSecret: kms.NewSecret(sdkkms.SecretStatusSecretBox, \"payload\", \"key\", user.Username),\n\t})\n\terr = dataprovider.UpdateAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = getLoginForm(altAdminUsername, altAdminPassword, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminTwoFactorPath, rr.Header().Get(\"Location\"))\n\tcookie, err = getCookieFromResponse(rr)\n\tassert.NoError(t, err)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"passcode\", \"123456\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminTwoFactorRecoveryPath, cookie)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"recovery_code\", \"RC-123456\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v%v\", httpBaseURL, tokenPath), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-SFTPGO-OTP\", \"auth-code\")\n\treq.SetBasicAuth(altAdminUsername, altAdminPassword)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusInternalServerError, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUserTOTP(t *testing.T) {\n\tu := getTestUser()\n\t// TOTPConfig will be ignored on add\n\tu.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: \"\",\n\t\tSecret:     kms.NewEmptySecret(),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.False(t, user.Filters.TOTPConfig.Enabled)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, userTOTPConfigsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar configs []mfa.TOTPConfig\n\terr = json.Unmarshal(rr.Body.Bytes(), &configs)\n\tassert.NoError(t, err, rr.Body.String())\n\tassert.Len(t, configs, len(mfa.GetAvailableTOTPConfigs()))\n\ttotpConfig := configs[0]\n\ttotpReq := generateTOTPRequest{\n\t\tConfigName: totpConfig.Name,\n\t}\n\tasJSON, err := json.Marshal(totpReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPGeneratePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar totpGenResp generateTOTPResponse\n\terr = json.Unmarshal(rr.Body.Bytes(), &totpGenResp)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, totpGenResp.Secret)\n\tassert.NotEmpty(t, totpGenResp.QRCode)\n\n\tpasscode, err := generateTOTPPasscode(totpGenResp.Secret)\n\tassert.NoError(t, err)\n\tvalidateReq := validateTOTPRequest{\n\t\tConfigName: totpGenResp.ConfigName,\n\t\tPasscode:   passcode,\n\t\tSecret:     totpGenResp.Secret,\n\t}\n\tasJSON, err = json.Marshal(validateReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPValidatePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// the same passcode cannot be reused\n\treq, err = http.NewRequest(http.MethodPost, userTOTPValidatePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"this passcode was already used\")\n\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: totpGenResp.ConfigName,\n\t\tSecret:     kms.NewPlainSecret(totpGenResp.Secret),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\ttotpCfg := user.Filters.TOTPConfig\n\tassert.True(t, totpCfg.Enabled)\n\tsecretPayload := totpCfg.Secret.GetPayload()\n\tassert.Equal(t, totpGenResp.ConfigName, totpCfg.ConfigName)\n\tassert.Empty(t, totpCfg.Secret.GetKey())\n\tassert.Empty(t, totpCfg.Secret.GetAdditionalData())\n\tassert.NotEmpty(t, secretPayload)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, totpCfg.Secret.GetStatus())\n\tassert.Len(t, totpCfg.Protocols, 1)\n\tassert.Contains(t, totpCfg.Protocols, common.ProtocolSSH)\n\t// update protocols only\n\tuserTOTPConfig = dataprovider.UserTOTPConfig{\n\t\tProtocols: []string{common.ProtocolSSH, common.ProtocolFTP},\n\t\tSecret:    kms.NewEmptySecret(),\n\t}\n\tasJSON, err = json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// update the user, TOTP should not be affected\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled: false,\n\t\tSecret:  kms.NewEmptySecret(),\n\t}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, totpCfg.ConfigName, user.Filters.TOTPConfig.ConfigName)\n\tassert.Empty(t, user.Filters.TOTPConfig.Secret.GetKey())\n\tassert.Empty(t, user.Filters.TOTPConfig.Secret.GetAdditionalData())\n\tassert.Equal(t, secretPayload, user.Filters.TOTPConfig.Secret.GetPayload())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())\n\tassert.Len(t, user.Filters.TOTPConfig.Protocols, 2)\n\tassert.Contains(t, user.Filters.TOTPConfig.Protocols, common.ProtocolSSH)\n\tassert.Contains(t, user.Filters.TOTPConfig.Protocols, common.ProtocolFTP)\n\n\treq, err = http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar recCodes []recoveryCode\n\terr = json.Unmarshal(rr.Body.Bytes(), &recCodes)\n\tassert.NoError(t, err)\n\tassert.Len(t, recCodes, 12)\n\t// regenerate recovery codes\n\treq, err = http.NewRequest(http.MethodPost, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// check that recovery codes are different\n\treq, err = http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar newRecCodes []recoveryCode\n\terr = json.Unmarshal(rr.Body.Bytes(), &newRecCodes)\n\tassert.NoError(t, err)\n\tassert.Len(t, newRecCodes, 12)\n\tassert.NotEqual(t, recCodes, newRecCodes)\n\t// disable 2FA, the update user API should not work\n\tadminToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser.Filters.TOTPConfig.Enabled = false\n\tuser.Filters.RecoveryCodes = nil\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, defaultUsername, user.Username)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, user.Filters.RecoveryCodes, 12)\n\t// use the dedicated API\n\treq, err = http.NewRequest(http.MethodPut, userPath+\"/\"+defaultUsername+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Len(t, user.Filters.RecoveryCodes, 0)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPut, userPath+\"/\"+defaultUsername+\"/2fa/disable\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, user2FARecoveryCodesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestWebAPIChangeUserProfileMock(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.False(t, user.Filters.AllowAPIKeyAuth)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\t// invalid json\n\treq, err := http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer([]byte(\"{\")))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\temail := \"userapi@example.com\"\n\tadditionalEmails := []string{\"userapi1@example.com\"}\n\tdescription := \"user API description\"\n\tprofileReq := make(map[string]any)\n\tprofileReq[\"allow_api_key_auth\"] = true\n\tprofileReq[\"email\"] = email\n\tprofileReq[\"description\"] = description\n\tprofileReq[\"public_keys\"] = []string{testPubKey, testPubKey1}\n\tprofileReq[\"tls_certs\"] = []string{httpsCert}\n\tprofileReq[\"additional_emails\"] = additionalEmails\n\tasJSON, err := json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tprofileReq = make(map[string]any)\n\treq, err = http.NewRequest(http.MethodGet, userProfilePath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = json.Unmarshal(rr.Body.Bytes(), &profileReq)\n\tassert.NoError(t, err)\n\tassert.Equal(t, email, profileReq[\"email\"].(string))\n\tassert.Len(t, profileReq[\"additional_emails\"].([]interface{}), 1)\n\tassert.Equal(t, description, profileReq[\"description\"].(string))\n\tassert.True(t, profileReq[\"allow_api_key_auth\"].(bool))\n\tval, ok := profileReq[\"public_keys\"].([]any)\n\tif assert.True(t, ok, profileReq) {\n\t\tassert.Len(t, val, 2)\n\t}\n\tval, ok = profileReq[\"tls_certs\"].([]any)\n\tif assert.True(t, ok, profileReq) {\n\t\tassert.Len(t, val, 1)\n\t}\n\t// set an invalid email\n\tprofileReq = make(map[string]any)\n\tprofileReq[\"email\"] = \"notavalidemail\"\n\tasJSON, err = json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Validation error: email\")\n\t// set an invalid additional email\n\tprofileReq = make(map[string]any)\n\tprofileReq[\"additional_emails\"] = []string{\"not an email\"}\n\tasJSON, err = json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Validation error: email\")\n\t// set an invalid public key\n\tprofileReq = make(map[string]any)\n\tprofileReq[\"public_keys\"] = []string{\"not a public key\"}\n\tasJSON, err = json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Validation error: error parsing public key\")\n\t// set an invalid TLS certificate\n\tprofileReq = make(map[string]any)\n\tprofileReq[\"tls_certs\"] = []string{\"not a TLS cert\"}\n\tasJSON, err = json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Validation error: invalid TLS certificate\")\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientPubKeyChangeDisabled,\n\t\tsdk.WebClientTLSCertChangeDisabled}\n\tuser.Email = email\n\tuser.Description = description\n\tuser.Filters.AllowAPIKeyAuth = true\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\ttoken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tprofileReq = make(map[string]any)\n\tprofileReq[\"allow_api_key_auth\"] = false\n\tprofileReq[\"email\"] = email\n\tprofileReq[\"description\"] = description + \"_mod\" //nolint:goconst\n\tprofileReq[\"public_keys\"] = []string{testPubKey}\n\tprofileReq[\"tls_certs\"] = []string{}\n\tasJSON, err = json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), \"Profile updated\")\n\t// check that api key auth and public keys were not changed\n\tprofileReq = make(map[string]any)\n\treq, err = http.NewRequest(http.MethodGet, userProfilePath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = json.Unmarshal(rr.Body.Bytes(), &profileReq)\n\tassert.NoError(t, err)\n\tassert.Equal(t, email, profileReq[\"email\"].(string))\n\tassert.Equal(t, description+\"_mod\", profileReq[\"description\"].(string))\n\tassert.True(t, profileReq[\"allow_api_key_auth\"].(bool))\n\tval, ok = profileReq[\"public_keys\"].([]any)\n\tif assert.True(t, ok, profileReq) {\n\t\tassert.Len(t, val, 2)\n\t}\n\tval, ok = profileReq[\"tls_certs\"].([]any)\n\tif assert.True(t, ok, profileReq) {\n\t\tassert.Len(t, val, 1)\n\t}\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled}\n\tuser.Description = description + \"_mod\"\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\ttoken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tprofileReq = make(map[string]any)\n\tprofileReq[\"allow_api_key_auth\"] = false\n\tprofileReq[\"email\"] = \"newemail@apiuser.com\"\n\tprofileReq[\"description\"] = description\n\tprofileReq[\"public_keys\"] = []string{testPubKey}\n\tasJSON, err = json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tprofileReq = make(map[string]any)\n\treq, err = http.NewRequest(http.MethodGet, userProfilePath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = json.Unmarshal(rr.Body.Bytes(), &profileReq)\n\tassert.NoError(t, err)\n\tassert.Equal(t, email, profileReq[\"email\"].(string))\n\tassert.Equal(t, description+\"_mod\", profileReq[\"description\"].(string))\n\tassert.True(t, profileReq[\"allow_api_key_auth\"].(bool))\n\tassert.Len(t, profileReq[\"public_keys\"].([]any), 1)\n\t// finally disable all profile permissions\n\tuser.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled,\n\t\tsdk.WebClientPubKeyChangeDisabled, sdk.WebClientTLSCertChangeDisabled}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"You are not allowed to change anything\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, userProfilePath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestPermGroupOverride(t *testing.T) {\n\tg := getTestGroup()\n\tg.UserSettings.Filters.WebClient = []string{sdk.WebClientPasswordChangeDisabled}\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tpwd := make(map[string]string)\n\tpwd[\"current_password\"] = defaultPassword\n\tpwd[\"new_password\"] = altAdminPassword\n\tasJSON, err := json.Marshal(pwd)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPut, userPwdPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tgroup.UserSettings.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP}\n\tgroup, _, err = httpdtest.UpdateGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\n\ttoken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols\")\n\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError2FARequired)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebAPIChangeUserPwdMock(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, userProfilePath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// invalid json\n\treq, err = http.NewRequest(http.MethodPut, userPwdPath, bytes.NewBuffer([]byte(\"{\")))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tpwd := make(map[string]string)\n\tpwd[\"current_password\"] = defaultPassword\n\tpwd[\"new_password\"] = defaultPassword\n\tasJSON, err := json.Marshal(pwd)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userPwdPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the new password must be different from the current one\")\n\n\tpwd[\"new_password\"] = altAdminPassword\n\tasJSON, err = json.Marshal(pwd)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userPwdPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userProfilePath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\ttoken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, token)\n\n\t// remove the change password permission\n\tuser.Filters.WebClient = []string{sdk.WebClientPasswordChangeDisabled}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.WebClient, 1)\n\tassert.Contains(t, user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)\n\n\ttoken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, token)\n\n\tpwd[\"current_password\"] = altAdminPassword\n\tpwd[\"new_password\"] = defaultPassword\n\tasJSON, err = json.Marshal(pwd)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userPwdPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginInvalidPasswordMock(t *testing.T) {\n\t_, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass+\"1\")\n\tassert.Error(t, err)\n\t// now a login with no credentials\n\treq, _ := http.NewRequest(http.MethodGet, \"/api/v2/token\", nil)\n\trr := executeRequest(req)\n\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n}\n\nfunc TestWebAPIChangeAdminProfileMock(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.AllowAPIKeyAuth)\n\n\ttoken, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\t// invalid json\n\treq, err := http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer([]byte(\"{\")))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\temail := \"adminapi@example.com\"\n\tdescription := \"admin API description\"\n\tprofileReq := make(map[string]any)\n\tprofileReq[\"allow_api_key_auth\"] = true\n\tprofileReq[\"email\"] = email\n\tprofileReq[\"description\"] = description\n\tasJSON, err := json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), \"Profile updated\")\n\n\tprofileReq = make(map[string]any)\n\treq, err = http.NewRequest(http.MethodGet, adminProfilePath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = json.Unmarshal(rr.Body.Bytes(), &profileReq)\n\tassert.NoError(t, err)\n\tassert.Equal(t, email, profileReq[\"email\"].(string))\n\tassert.Equal(t, description, profileReq[\"description\"].(string))\n\tassert.True(t, profileReq[\"allow_api_key_auth\"].(bool))\n\t// set an invalid email\n\tprofileReq[\"email\"] = \"admin_invalid_email\"\n\tasJSON, err = json.Marshal(profileReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Validation error: email\")\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, adminProfilePath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestChangeAdminPwdMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin.Permissions = []string{dataprovider.PermAdminAddUsers, dataprovider.PermAdminDeleteUsers}\n\tasJSON, err := json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\taltToken, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tpwd := make(map[string]string)\n\tpwd[\"current_password\"] = altAdminPassword\n\tpwd[\"new_password\"] = defaultTokenAuthPass\n\tasJSON, err = json.Marshal(pwd)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, adminPwdPath, bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// try using the old token\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\t_, err = getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.Error(t, err)\n\n\taltToken, err = getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, adminPwdPath, bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr) // current password does not match\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(adminPath, altAdminUsername), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestUpdateAdminMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\t_, err = getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass)\n\tassert.Error(t, err)\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Permissions = []string{dataprovider.PermAdminAny}\n\tasJSON, err := json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t_, err = getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, \"abc\"), bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, altAdminUsername), bytes.NewBuffer([]byte(\"no json\")))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tadmin.Permissions = nil\n\tasJSON, err = json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, altAdminUsername), bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tadmin = getTestAdmin()\n\tadmin.Status = 0\n\tasJSON, err = json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, defaultTokenAuthUser), bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"you cannot disable yourself\")\n\tadmin.Status = 1\n\tadmin.Permissions = []string{dataprovider.PermAdminAddUsers}\n\tasJSON, err = json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, defaultTokenAuthUser), bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"you cannot change your permissions\")\n\tadmin.Permissions = []string{dataprovider.PermAdminAny}\n\tadmin.Role = \"missing role\"\n\tasJSON, err = json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, defaultTokenAuthUser), bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"you cannot add/change your role\")\n\tadmin.Role = \"\"\n\n\taltToken, err := getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tadmin.Password = \"\" // it must remain unchanged\n\tadmin.Permissions = []string{dataprovider.PermAdminAny}\n\tasJSON, err = json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, altAdminUsername), bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(adminPath, altAdminUsername), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestAdminLastLoginWithAPIKey(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Filters.AllowAPIKeyAuth = true\n\tadmin, resp, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, int64(0), admin.LastLogin)\n\n\tapiKey := dataprovider.APIKey{\n\t\tName:      \"admin API key\",\n\t\tScope:     dataprovider.APIKeyScopeAdmin,\n\t\tAdmin:     altAdminUsername,\n\t\tLastUseAt: 123,\n\t}\n\n\tapiKey, resp, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, int64(0), apiKey.LastUseAt)\n\n\treq, err := http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, admin.Username)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, admin.LastLogin, int64(0))\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserLastLoginWithAPIKey(t *testing.T) {\n\tuser := getTestUser()\n\tuser.Filters.AllowAPIKeyAuth = true\n\tuser, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tassert.Equal(t, int64(0), user.LastLogin)\n\n\tapiKey := dataprovider.APIKey{\n\t\tName:  \"user API key\",\n\t\tScope: dataprovider.APIKeyScopeUser,\n\t\tUser:  user.Username,\n\t}\n\n\tapiKey, resp, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\treq, err := http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, user.LastLogin, int64(0))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminHandlingWithAPIKeys(t *testing.T) {\n\tsysAdmin, _, err := httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK)\n\tassert.NoError(t, err)\n\tsysAdmin.Filters.AllowAPIKeyAuth = true\n\tsysAdmin, _, err = httpdtest.UpdateAdmin(sysAdmin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tapiKey := dataprovider.APIKey{\n\t\tName:  \"test admin API key\",\n\t\tScope: dataprovider.APIKeyScopeAdmin,\n\t\tAdmin: defaultTokenAuthUser,\n\t}\n\n\tapiKey, resp, err := httpdtest.AddAPIKey(apiKey, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tasJSON, err := json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t_, err = getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\tadmin.Filters.AllowAPIKeyAuth = true\n\tasJSON, err = json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, path.Join(adminPath, altAdminUsername), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(adminPath, altAdminUsername), nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar adminGet dataprovider.Admin\n\terr = json.Unmarshal(rr.Body.Bytes(), &adminGet)\n\tassert.NoError(t, err)\n\tassert.True(t, adminGet.Filters.AllowAPIKeyAuth)\n\n\treq, err = http.NewRequest(http.MethodPut, path.Join(adminPath, defaultTokenAuthUser), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"updating the admin impersonated with an API key is not allowed\")\n\t// changing the password for the impersonated admin is not allowed\n\tpwd := make(map[string]string)\n\tpwd[\"current_password\"] = defaultTokenAuthPass\n\tpwd[\"new_password\"] = altAdminPassword\n\tasJSON, err = json.Marshal(pwd)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, adminPwdPath, bytes.NewBuffer(asJSON))\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"API key authentication is not allowed\")\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(adminPath, defaultTokenAuthUser), nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"you cannot delete yourself\")\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(adminPath, altAdminUsername), nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = httpdtest.RemoveAPIKey(apiKey, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tdbAdmin, err := dataprovider.AdminExists(defaultTokenAuthUser)\n\tassert.NoError(t, err)\n\tdbAdmin.Filters.AllowAPIKeyAuth = false\n\terr = dataprovider.UpdateAdmin(&dbAdmin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tsysAdmin, _, err = httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, sysAdmin.Filters.AllowAPIKeyAuth)\n}\n\nfunc TestUserHandlingWithAPIKey(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Filters.AllowAPIKeyAuth = true\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tapiKey := dataprovider.APIKey{\n\t\tName:  \"test admin API key\",\n\t\tScope: dataprovider.APIKeyScopeAdmin,\n\t\tAdmin: admin.Username,\n\t}\n\n\tapiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tuser.Filters.DisableFsChecks = true\n\tuser.Description = \"desc\"\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, err = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updatedUser dataprovider.User\n\terr = json.Unmarshal(rr.Body.Bytes(), &updatedUser)\n\tassert.NoError(t, err)\n\tassert.True(t, updatedUser.Filters.DisableFsChecks)\n\tassert.Equal(t, user.Description, updatedUser.Description)\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusNotFound)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateUserQuotaUsageMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tvar user dataprovider.User\n\tu := getTestUser()\n\tusedQuotaFiles := 1\n\tusedQuotaSize := int64(65535)\n\tu.UsedQuotaFiles = usedQuotaFiles\n\tu.UsedQuotaSize = usedQuotaSize\n\tu.QuotaFiles = 100\n\tuserAsJSON := getUserAsJSON(t, u)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"users\", u.Username, \"usage\"), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize, user.UsedQuotaSize)\n\t// now update only quota size\n\tu.UsedQuotaFiles = 0\n\tuserAsJSON = getUserAsJSON(t, u)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"users\", u.Username, \"usage\")+\"?mode=add\", bytes.NewBuffer(userAsJSON)) //nolint:goconst\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize*2, user.UsedQuotaSize)\n\t// only quota files\n\tu.UsedQuotaFiles = usedQuotaFiles\n\tu.UsedQuotaSize = 0\n\tuserAsJSON = getUserAsJSON(t, u)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"users\", u.Username, \"usage\")+\"?mode=add\", bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles*2, user.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize*2, user.UsedQuotaSize)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"users\", u.Username, \"usage\"), bytes.NewBuffer([]byte(\"string\")))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.True(t, common.QuotaScans.AddUserQuotaScan(user.Username, \"\"))\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"users\", u.Username, \"usage\"), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusConflict, rr)\n\tassert.True(t, common.QuotaScans.RemoveUserQuotaScan(user.Username))\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestUserPermissionsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/somedir\"] = []string{dataprovider.PermAny}\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tuser.Permissions[\"..\"] = []string{dataprovider.PermAny}\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tuser.Permissions[\"/somedir\"] = []string{\"invalid\"}\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tdelete(user.Permissions, \"/somedir\")\n\tuser.Permissions[\"/somedir/..\"] = []string{dataprovider.PermAny}\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tdelete(user.Permissions, \"/somedir/..\")\n\tuser.Permissions[\"not_abs_path\"] = []string{dataprovider.PermAny}\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tdelete(user.Permissions, \"not_abs_path\")\n\tuser.Permissions[\"/somedir/../otherdir/\"] = []string{dataprovider.PermListItems}\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updatedUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updatedUser)\n\tassert.NoError(t, err)\n\tif val, ok := updatedUser.Permissions[\"/otherdir\"]; ok {\n\t\tassert.True(t, slices.Contains(val, dataprovider.PermListItems))\n\t\tassert.Equal(t, 1, len(val))\n\t} else {\n\t\tassert.Fail(t, \"expected dir not found in permissions\")\n\t}\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestUpdateUserInvalidJsonMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer([]byte(\"Invalid json\")))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestUpdateUserInvalidParamsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tuser.HomeDir = \"\"\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tuserID := user.ID\n\tuser.ID = 0\n\tuser.CreatedAt = 0\n\tuserAsJSON = getUserAsJSON(t, user)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(userPath, user.Username), bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tuser.ID = userID\n\treq, _ = http.NewRequest(http.MethodPut, userPath+\"/0\", bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestGetAdminsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tasJSON, err := json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, adminPath, bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, adminPath+\"?limit=510&offset=0&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar admins []dataprovider.Admin\n\terr = render.DecodeJSON(rr.Body, &admins)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(admins), 1)\n\tfirtAdmin := admins[0].Username\n\treq, _ = http.NewRequest(http.MethodGet, adminPath+\"?limit=510&offset=0&order=DESC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tadmins = nil\n\terr = render.DecodeJSON(rr.Body, &admins)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(admins), 1)\n\tassert.NotEqual(t, firtAdmin, admins[0].Username)\n\n\treq, _ = http.NewRequest(http.MethodGet, adminPath+\"?limit=510&offset=1&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tadmins = nil\n\terr = render.DecodeJSON(rr.Body, &admins)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(admins), 1)\n\tassert.NotEqual(t, firtAdmin, admins[0].Username)\n\n\treq, _ = http.NewRequest(http.MethodGet, adminPath+\"?limit=a&offset=0&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\treq, _ = http.NewRequest(http.MethodGet, adminPath+\"?limit=1&offset=aa&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\treq, _ = http.NewRequest(http.MethodGet, adminPath+\"?limit=1&offset=0&order=ASCa\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(adminPath, admin.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestGetUsersMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodGet, userPath+\"?limit=510&offset=0&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar users []dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &users)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(users), 1)\n\treq, _ = http.NewRequest(http.MethodGet, userPath+\"?limit=aa&offset=0&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\treq, _ = http.NewRequest(http.MethodGet, userPath+\"?limit=1&offset=a&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\treq, _ = http.NewRequest(http.MethodGet, userPath+\"?limit=1&offset=0&order=ASCc\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestDeleteUserInvalidParamsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodDelete, userPath+\"/0\", nil)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestGetQuotaScansMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, quotaScanPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestStartQuotaScanMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\t_, err = os.Stat(user.HomeDir)\n\tif err == nil {\n\t\terr = os.Remove(user.HomeDir)\n\t\tassert.NoError(t, err)\n\t}\n\t// simulate a duplicate quota scan\n\tcommon.QuotaScans.AddUserQuotaScan(user.Username, \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, \"users\", user.Username, \"scan\"), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusConflict, rr)\n\tassert.True(t, common.QuotaScans.RemoveUserQuotaScan(user.Username))\n\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, \"users\", user.Username, \"scan\"), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\n\twaitForUsersQuotaScan(t, token)\n\n\t_, err = os.Stat(user.HomeDir)\n\tif err != nil && errors.Is(err, fs.ErrNotExist) {\n\t\terr = os.MkdirAll(user.HomeDir, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, \"users\", user.Username, \"scan\"), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\n\twaitForUsersQuotaScan(t, token)\n\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, \"users\", user.Username, \"scan\"), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\n\twaitForUsersQuotaScan(t, token)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateFolderQuotaUsageMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tmappedPath := filepath.Join(os.TempDir(), \"vfolder\")\n\tfolderName := filepath.Base(mappedPath)\n\tf := vfs.BaseVirtualFolder{\n\t\tMappedPath: mappedPath,\n\t\tName:       folderName,\n\t}\n\tusedQuotaFiles := 1\n\tusedQuotaSize := int64(65535)\n\tf.UsedQuotaFiles = usedQuotaFiles\n\tf.UsedQuotaSize = usedQuotaSize\n\tvar folder vfs.BaseVirtualFolder\n\tfolderAsJSON, err := json.Marshal(f)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &folder)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"folders\", folder.Name, \"usage\"), bytes.NewBuffer(folderAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar folderGet vfs.BaseVirtualFolder\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folderGet)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles, folderGet.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize, folderGet.UsedQuotaSize)\n\t// now update only quota size\n\tf.UsedQuotaFiles = 0\n\tfolderAsJSON, err = json.Marshal(f)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"folders\", folder.Name, \"usage\")+\"?mode=add\",\n\t\tbytes.NewBuffer(folderAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tfolderGet = vfs.BaseVirtualFolder{}\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folderGet)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles, folderGet.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize*2, folderGet.UsedQuotaSize)\n\t// now update only quota files\n\tf.UsedQuotaSize = 0\n\tf.UsedQuotaFiles = 1\n\tfolderAsJSON, err = json.Marshal(f)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"folders\", folder.Name, \"usage\")+\"?mode=add\",\n\t\tbytes.NewBuffer(folderAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tfolderGet = vfs.BaseVirtualFolder{}\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folderGet)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedQuotaFiles*2, folderGet.UsedQuotaFiles)\n\tassert.Equal(t, usedQuotaSize*2, folderGet.UsedQuotaSize)\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"folders\", folder.Name, \"usage\"),\n\t\tbytes.NewBuffer([]byte(\"not a json\")))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tassert.True(t, common.QuotaScans.AddVFolderQuotaScan(folderName))\n\treq, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, \"folders\", folder.Name, \"usage\"),\n\t\tbytes.NewBuffer(folderAsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusConflict, rr)\n\tassert.True(t, common.QuotaScans.RemoveVFolderQuotaScan(folderName))\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestStartFolderQuotaScanMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tmappedPath := filepath.Join(os.TempDir(), \"vfolder\")\n\tfolderName := filepath.Base(mappedPath)\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\tfolderAsJSON, err := json.Marshal(folder)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t_, err = os.Stat(mappedPath)\n\tif err == nil {\n\t\terr = os.Remove(mappedPath)\n\t\tassert.NoError(t, err)\n\t}\n\t// simulate a duplicate quota scan\n\tcommon.QuotaScans.AddVFolderQuotaScan(folderName)\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, \"folders\", folder.Name, \"scan\"), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusConflict, rr)\n\tassert.True(t, common.QuotaScans.RemoveVFolderQuotaScan(folderName))\n\t// and now a real quota scan\n\t_, err = os.Stat(mappedPath)\n\tif err != nil && errors.Is(err, fs.ErrNotExist) {\n\t\terr = os.MkdirAll(mappedPath, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, \"folders\", folder.Name, \"scan\"), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\twaitForFoldersQuotaScanPath(t, token)\n\t// cleanup\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = os.RemoveAll(folderPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestStartQuotaScanNonExistentUserMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\n\treq, _ := http.NewRequest(http.MethodPost, path.Join(quotasBasePath, \"users\", user.Username, \"scan\"), nil)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestStartQuotaScanNonExistentFolderMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName: \"afolder\",\n\t}\n\treq, _ := http.NewRequest(http.MethodPost, path.Join(quotasBasePath, \"folders\", folder.Name, \"scan\"), nil)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestGetFoldersMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tmappedPath := filepath.Join(os.TempDir(), \"vfolder\")\n\tfolderName := filepath.Base(mappedPath)\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\tfolderAsJSON, err := json.Marshal(folder)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &folder)\n\tassert.NoError(t, err)\n\n\tvar folders []vfs.BaseVirtualFolder\n\turl, err := url.Parse(folderPath + \"?limit=510&offset=0&order=DESC\")\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodGet, url.String(), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folders)\n\tassert.NoError(t, err)\n\tassert.GreaterOrEqual(t, len(folders), 1)\n\treq, _ = http.NewRequest(http.MethodGet, folderPath+\"?limit=a&offset=0&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\treq, _ = http.NewRequest(http.MethodGet, folderPath+\"?limit=1&offset=a&order=ASC\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\treq, _ = http.NewRequest(http.MethodGet, folderPath+\"?limit=1&offset=0&order=ASCV\", nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestGetVersionMock(t *testing.T) {\n\treq, _ := http.NewRequest(http.MethodGet, versionPath, nil)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodGet, versionPath, nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, versionPath, nil)\n\tsetBearerForReq(req, \"abcde\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n}\n\nfunc TestGetConnectionsMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, activeConnectionsPath, nil)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestGetStatusMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestDeleteActiveConnectionMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodDelete, activeConnectionsPath+\"/connectionID\", nil)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\treq.Header.Set(dataprovider.NodeTokenHeader, \"Bearer abc\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"the provided token cannot be authenticated\")\n\treq, err = http.NewRequest(http.MethodDelete, activeConnectionsPath+\"/connectionID?node=node1\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestNotFoundMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, \"/non/existing/path\", nil)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestMethodNotAllowedMock(t *testing.T) {\n\treq, _ := http.NewRequest(http.MethodPost, activeConnectionsPath, nil)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusMethodNotAllowed, rr)\n}\n\nfunc TestHealthCheck(t *testing.T) {\n\treq, _ := http.NewRequest(http.MethodGet, healthzPath, nil)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"ok\", rr.Body.String())\n}\n\nfunc TestGetWebRootMock(t *testing.T) {\n\treq, _ := http.NewRequest(http.MethodGet, \"/\", nil)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\treq, _ = http.NewRequest(http.MethodGet, webBasePath, nil)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\treq, _ = http.NewRequest(http.MethodGet, webBasePathAdmin, nil)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\treq, _ = http.NewRequest(http.MethodGet, webBasePathClient, nil)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n}\n\nfunc TestWebNotFoundURI(t *testing.T) {\n\turlString := httpBaseURL + webBasePath + \"/a\"\n\treq, err := http.NewRequest(http.MethodGet, urlString, nil)\n\tassert.NoError(t, err)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusNotFound, resp.StatusCode)\n\n\treq, err = http.NewRequest(http.MethodGet, urlString, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, \"invalid token\")\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusNotFound, resp.StatusCode)\n\n\turlString = httpBaseURL + webBasePathClient + \"/a\"\n\treq, err = http.NewRequest(http.MethodGet, urlString, nil)\n\tassert.NoError(t, err)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusNotFound, resp.StatusCode)\n\n\treq, err = http.NewRequest(http.MethodGet, urlString, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, \"invalid client token\")\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusNotFound, resp.StatusCode)\n}\n\nfunc TestLogout(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, logoutPath, nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"Your token is no longer valid\")\n}\n\nfunc TestDefenderAPIInvalidIDMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, path.Join(defenderHosts, \"abc\"), nil) // not hex id\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"invalid host id\")\n}\n\nfunc TestTokenHeaderCookie(t *testing.T) {\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\treq, _ := http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetJWTCookieForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"no token found\")\n\n\treq, _ = http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath, nil)\n\tsetBearerForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestTokenAudience(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\treq, _ := http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"Your token audience is not valid\")\n\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath, nil)\n\tsetJWTCookieForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n}\n\nfunc TestWebAPILoginMock(t *testing.T) {\n\t_, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername+\"1\", defaultPassword)\n\tassert.Error(t, err)\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword+\"1\")\n\tassert.Error(t, err)\n\tapiToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\t// a web token is not valid for API usage\n\treq, err := http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"Your token audience is not valid\")\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath+\"/?path=%2F\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// API token is not valid for web usage\n\treq, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\tsetJWTCookieForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebClientLoginMock(t *testing.T) {\n\t_, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\t// a web token is not valid for API or WebAdmin usage\n\treq, _ := http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"Your token audience is not valid\")\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\t// bearer should not work\n\treq, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\tsetBearerForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\treq, _ = http.NewRequest(http.MethodGet, webClientPingPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetBearerForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\t// now try to render client pages\n\treq, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, webClientPingPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// now logout\n\treq, _ = http.NewRequest(http.MethodGet, webClientLogoutPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\treq, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\treq, _ = http.NewRequest(http.MethodGet, webClientPingPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\t// get a new token and use it after removing the user\n\twebToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tapiUserToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, webToken)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\treq.RemoteAddr = defaultRemoteAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorDirListUser)\n\n\tform := make(url.Values)\n\tform.Set(\"files\", `[]`)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientDownloadZipPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)\n\n\treq, _ = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tsetBearerForReq(req, apiUserToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to retrieve your user\")\n\n\treq, _ = http.NewRequest(http.MethodGet, userFilesPath, nil)\n\tsetBearerForReq(req, apiUserToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to retrieve your user\")\n\n\treq, _ = http.NewRequest(http.MethodPost, userStreamZipPath, bytes.NewBuffer([]byte(`{}`)))\n\tsetBearerForReq(req, apiUserToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to retrieve your user\")\n\n\tform = make(url.Values)\n\tform.Set(\"public_keys\", testPubKey)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n}\n\nfunc TestWebClientLoginErrorsMock(t *testing.T) {\n\tform := getLoginForm(\"\", \"\", \"\")\n\treq, _ := http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr := executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\tform = getLoginForm(defaultUsername, defaultPassword, \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n}\n\nfunc TestWebClientMaxConnections(t *testing.T) {\n\toldValue := common.Config.MaxTotalConnections\n\tcommon.Config.MaxTotalConnections = 1\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// now add a fake connection\n\tfs := vfs.NewOsFs(\"id\", os.TempDir(), \"\", nil)\n\tconnection := &httpd.Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolHTTP, \"\", \"\", user),\n\t}\n\terr = common.Connections.Add(connection)\n\tassert.NoError(t, err)\n\n\t_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), common.ErrConnectionDenied.Error())\n\n\tcommon.Connections.Remove(connection.GetID())\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\tcommon.Config.MaxTotalConnections = oldValue\n}\n\nfunc TestTokenInvalidIPAddress(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\treq.RemoteAddr = \"1.1.1.2\"\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\n\tapiToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath+\"/?path=%2F\", nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = \"2.2.2.2\"\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"Your token is not valid\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDefender(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.DefenderConfig.Enabled = true\n\tcfg.DefenderConfig.Threshold = 3\n\tcfg.DefenderConfig.ScoreLimitExceeded = 2\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tremoteAddr := \"172.16.5.6:9876\"\n\n\twebAdminToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServerWithAddr(defaultUsername, defaultPassword, remoteAddr)\n\tassert.NoError(t, err)\n\n\treq, _ := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\treq.RemoteAddr = remoteAddr\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tfor i := 0; i < 3; i++ {\n\t\t_, err = getJWTWebClientTokenFromTestServerWithAddr(defaultUsername, \"wrong pwd\", remoteAddr)\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = getJWTWebClientTokenFromTestServerWithAddr(defaultUsername, defaultPassword, remoteAddr)\n\tassert.Error(t, err)\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.RequestURI = webClientFilesPath\n\tsetJWTCookieForReq(req, webToken)\n\treq.RemoteAddr = remoteAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorIPForbidden)\n\n\treq, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\treq.RequestURI = webUsersPath\n\tsetJWTCookieForReq(req, webAdminToken)\n\treq.RemoteAddr = remoteAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorIPForbidden)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.Header.Set(\"X-Real-IP\", \"127.0.0.1:2345\")\n\tsetJWTCookieForReq(req, webToken)\n\treq.RemoteAddr = remoteAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"your IP address is blocked\")\n\t// requests for static files should be always allowed\n\treq, err = http.NewRequest(http.MethodGet, \"/static/favicon.png\", nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = remoteAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Empty(t, rr.Header().Get(\"Cache-Control\"))\n\n\treq, err = http.NewRequest(http.MethodGet, \"/.well-known/acme-challenge/foo\", nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = remoteAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Equal(t, \"no-cache, no-store, max-age=0, must-revalidate, private\", rr.Header().Get(\"Cache-Control\"))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestPostConnectHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tcommon.Config.PostConnectHook = postConnectPath\n\n\tu := getTestUser()\n\tu.Filters.AllowAPIKeyAuth = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tapiKey, _, err := httpdtest.AddAPIKey(dataprovider.APIKey{\n\t\tName:  \"name\",\n\t\tScope: dataprovider.APIKeyScopeUser,\n\t\tUser:  user.Username,\n\t}, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\n\t_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\terr = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\n\t_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.PostConnectHook = \"\"\n}\n\nfunc TestMaxSessions(t *testing.T) {\n\tu := getTestUser()\n\tu.MaxSessions = 1\n\tu.Email = \"user@session.com\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\t// now add a fake connection\n\tfs := vfs.NewOsFs(\"id\", os.TempDir(), \"\", nil)\n\tconnection := &httpd.Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolHTTP, \"\", \"\", user),\n\t}\n\terr = common.Connections.Add(connection)\n\tassert.NoError(t, err)\n\t_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\t// try an user API call\n\treq, err := http.NewRequest(http.MethodGet, userDirsPath+\"/?path=%2F\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), \"too many open sessions\")\n\t// web client requests\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, webToken)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"files\", `[]`)\n\treq, err = http.NewRequest(http.MethodPost, webClientDownloadZipPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError429Message)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorDirList429)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=p\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError429Message)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=file\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError429Message)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=file\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError429Message)\n\n\t// test reset password\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          3525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr = smtpCfg.Initialize(configDir, true)\n\tassert.NoError(t, err)\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tlastResetCode = \"\"\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"password\", defaultPassword)\n\tform.Set(\"confirm_password\", defaultPassword)\n\tform.Set(\"code\", lastResetCode)\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nError429Message)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tcommon.Connections.Remove(connection.GetID())\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestMaxTransfers(t *testing.T) {\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 2\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:     \"test share\",\n\t\tScope:    dataprovider.ShareScopeReadWrite,\n\t\tPaths:    []string{\"/\"},\n\t\tPassword: defaultPassword,\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\tfileName := \"testfile.txt\"\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=\"+fileName, bytes.NewBuffer([]byte(\" \")))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tconn, sftpClient, err := getSftpClient(user)\n\tassert.NoError(t, err)\n\tdefer conn.Close()\n\tdefer sftpClient.Close()\n\n\tf1, err := sftpClient.Create(\"file1\")\n\tassert.NoError(t, err)\n\tf2, err := sftpClient.Create(\"file2\")\n\tassert.NoError(t, err)\n\t_, err = f1.Write([]byte(\" \"))\n\tassert.NoError(t, err)\n\t_, err = f2.Write([]byte(\" \"))\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", \"filepre\")\n\tassert.NoError(t, err)\n\t_, err = part.Write([]byte(\"file content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusConflict, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+fileName, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError403Message)\n\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=\"+fileName, bytes.NewBuffer([]byte(\" \")))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tbody = new(bytes.Buffer)\n\twriter = multipart.NewWriter(body)\n\tpart1, err := writer.CreateFormFile(\"filenames\", \"file11.txt\")\n\tassert.NoError(t, err)\n\t_, err = part1.Write([]byte(\"file11 content\"))\n\tassert.NoError(t, err)\n\tpart2, err := writer.CreateFormFile(\"filenames\", \"file22.txt\")\n\tassert.NoError(t, err)\n\t_, err = part2.Write([]byte(\"file22 content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader = bytes.NewReader(body.Bytes())\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusConflict, rr)\n\n\terr = f1.Close()\n\tassert.NoError(t, err)\n\terr = f2.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn len(common.Connections.GetStats(\"\")) == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetTotalTransfers() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\nfunc TestWebConfigsMock(t *testing.T) {\n\tacmeConfig := config.GetACMEConfig()\n\tacmeConfig.CertsPath = filepath.Clean(os.TempDir())\n\terr := acme.Initialize(acmeConfig, configDir, true)\n\trequire.NoError(t, err)\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webConfigsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform := make(url.Values)\n\tb, contentType, err := getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t// parse form error\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webConfigsPath, webToken)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath+\"?p=p%C3%AO%GH\", bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// save SFTP configs\n\tform.Set(\"sftp_host_key_algos\", ssh.KeyAlgoRSA)\n\tform.Add(\"sftp_host_key_algos\", ssh.InsecureCertAlgoDSAv01) //nolint:staticcheck\n\tform.Set(\"sftp_pub_key_algos\", ssh.InsecureKeyAlgoDSA)      //nolint:staticcheck\n\tform.Set(\"form_action\", \"sftp_submit\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message) // invalid algo\n\tform.Set(\"sftp_host_key_algos\", ssh.KeyAlgoRSA)\n\tform.Add(\"sftp_host_key_algos\", ssh.CertAlgoRSAv01)\n\tform.Set(\"sftp_pub_key_algos\", ssh.InsecureKeyAlgoDSA) //nolint:staticcheck\n\tform.Set(\"sftp_kex_algos\", \"diffie-hellman-group18-sha512\")\n\tform.Add(\"sftp_kex_algos\", ssh.KeyExchangeDH16SHA512)\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\t// check SFTP configs\n\tconfigs, err := dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Len(t, configs.SFTPD.HostKeyAlgos, 1)\n\tassert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)\n\tassert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)\n\tassert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.InsecureKeyAlgoDSA) //nolint:staticcheck\n\tassert.Len(t, configs.SFTPD.KexAlgorithms, 1)\n\tassert.Contains(t, configs.SFTPD.KexAlgorithms, ssh.KeyExchangeDH16SHA512)\n\t// invalid form action\n\tform.Set(\"form_action\", \"\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError400Message)\n\t// test SMTP configs\n\tform.Set(\"form_action\", \"smtp_submit\")\n\tform.Set(\"smtp_host\", \"mail.example.net\")\n\tform.Set(\"smtp_from\", \"Example <info@example.net>\")\n\tform.Set(\"smtp_username\", defaultUsername)\n\tform.Set(\"smtp_password\", defaultPassword)\n\tform.Set(\"smtp_domain\", \"localdomain\")\n\tform.Set(\"smtp_auth\", \"100\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message) // invalid smtp_auth\n\t// set valid parameters\n\tform.Set(\"smtp_port\", \"a\") // converted to 587\n\tform.Set(\"smtp_auth\", \"1\")\n\tform.Set(\"smtp_encryption\", \"2\")\n\tform.Set(\"smtp_debug\", \"checked\")\n\tform.Set(\"smtp_oauth2_provider\", \"1\")\n\tform.Set(\"smtp_oauth2_client_id\", \"123\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\t// check\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Len(t, configs.SFTPD.HostKeyAlgos, 1)\n\tassert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)\n\tassert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)\n\tassert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.InsecureKeyAlgoDSA) //nolint:staticcheck\n\tassert.Equal(t, \"mail.example.net\", configs.SMTP.Host)\n\tassert.Equal(t, 587, configs.SMTP.Port)\n\tassert.Equal(t, \"Example <info@example.net>\", configs.SMTP.From)\n\tassert.Equal(t, defaultUsername, configs.SMTP.User)\n\tassert.Equal(t, 1, configs.SMTP.Debug)\n\tassert.Equal(t, \"\", configs.SMTP.OAuth2.ClientID)\n\terr = configs.SMTP.Password.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, defaultPassword, configs.SMTP.Password.GetPayload())\n\tassert.Equal(t, 1, configs.SMTP.AuthType)\n\tassert.Equal(t, 2, configs.SMTP.Encryption)\n\tassert.Equal(t, \"localdomain\", configs.SMTP.Domain)\n\t// set a redacted password, the current password must be preserved\n\tform.Set(\"smtp_password\", redactedSecret)\n\tform.Set(\"smtp_auth\", \"\")\n\tconfigs.SMTP.AuthType = 0 // empty will be converted to 0\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\tupdatedConfigs, err := dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tencryptedPayload := updatedConfigs.SMTP.Password.GetPayload()\n\tsecretKey := updatedConfigs.SMTP.Password.GetKey()\n\terr = updatedConfigs.SMTP.Password.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, configs.SFTPD, updatedConfigs.SFTPD)\n\tassert.Equal(t, configs.SMTP, updatedConfigs.SMTP)\n\t// now set an undecryptable password\n\tupdatedConfigs.SMTP.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, encryptedPayload, secretKey, \"\")\n\terr = dataprovider.UpdateConfigs(&updatedConfigs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\tform.Set(\"form_action\", \"acme_submit\")\n\tform.Set(\"acme_port\", \"\") // on error will be set to 80\n\tform.Set(\"acme_protocols\", \"1\")\n\tform.Add(\"acme_protocols\", \"2\")\n\tform.Add(\"acme_protocols\", \"3\")\n\tform.Set(\"acme_domain\", \"example.com\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\t// no email set, validation will fail\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidEmail)\n\tform.Set(\"acme_domain\", \"\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\t// check\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Len(t, configs.SFTPD.HostKeyAlgos, 1)\n\tassert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)\n\tassert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)\n\tassert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.InsecureKeyAlgoDSA) //nolint:staticcheck\n\tassert.Equal(t, 80, configs.ACME.HTTP01Challenge.Port)\n\tassert.Equal(t, 7, configs.ACME.Protocols)\n\tassert.Empty(t, configs.ACME.Domain)\n\tassert.Empty(t, configs.ACME.Email)\n\tassert.True(t, configs.ACME.HasProtocol(common.ProtocolFTP))\n\tassert.True(t, configs.ACME.HasProtocol(common.ProtocolWebDAV))\n\tassert.True(t, configs.ACME.HasProtocol(common.ProtocolHTTP))\n\t// create certificate files, so no real ACME call is done\n\tdomain := \"acme.example.com\"\n\tcrtPath := filepath.Join(acmeConfig.CertsPath, util.SanitizeDomain(domain)+\".crt\")\n\tkeyPath := filepath.Join(acmeConfig.CertsPath, util.SanitizeDomain(domain)+\".key\")\n\terr = os.WriteFile(crtPath, nil, 0666)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyPath, nil, 0666)\n\tassert.NoError(t, err)\n\tform.Set(\"acme_port\", \"402\")\n\tform.Set(\"acme_protocols\", \"1\")\n\tform.Add(\"acme_protocols\", \"1000\")\n\tform.Set(\"acme_domain\", domain)\n\tform.Set(\"acme_email\", \"email@example.com\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Len(t, configs.SFTPD.HostKeyAlgos, 1)\n\tassert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)\n\tassert.Equal(t, 402, configs.ACME.HTTP01Challenge.Port)\n\tassert.Equal(t, 1, configs.ACME.Protocols)\n\tassert.Equal(t, domain, configs.ACME.Domain)\n\tassert.Equal(t, \"email@example.com\", configs.ACME.Email)\n\tassert.False(t, configs.ACME.HasProtocol(common.ProtocolFTP))\n\tassert.False(t, configs.ACME.HasProtocol(common.ProtocolWebDAV))\n\tassert.True(t, configs.ACME.HasProtocol(common.ProtocolHTTP))\n\n\terr = os.Remove(crtPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestBrandingConfigMock(t *testing.T) {\n\terr := dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\twebClientLogoPath := \"/static/branding/webclient/logo.png\"\n\twebClientFaviconPath := \"/static/branding/webclient/favicon.png\"\n\twebAdminLogoPath := \"/static/branding/webadmin/logo.png\"\n\twebAdminFaviconPath := \"/static/branding/webadmin/favicon.png\"\n\t// no custom log or favicon was set\n\tfor _, p := range []string{webClientLogoPath, webClientFaviconPath, webAdminLogoPath, webAdminFaviconPath} {\n\t\treq, err := http.NewRequest(http.MethodGet, p, nil)\n\t\tassert.NoError(t, err)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t}\n\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webConfigsPath, webToken)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"form_action\", \"branding_submit\")\n\tform.Set(\"branding_webadmin_name\", \"Custom WebAdmin\")\n\tform.Set(\"branding_webadmin_short_name\", \"WebAdmin\")\n\tform.Set(\"branding_webadmin_disclaimer_name\", \"Admin disclaimer\")\n\tform.Set(\"branding_webadmin_disclaimer_url\", \"invalid, not a URL\")\n\tform.Set(\"branding_webclient_name\", \"Custom WebClient\")\n\tform.Set(\"branding_webclient_short_name\", \"WebClient\")\n\tform.Set(\"branding_webclient_disclaimer_name\", \"Client disclaimer\")\n\tform.Set(\"branding_webclient_disclaimer_url\", \"https://example.com\")\n\tb, contentType, err := getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidDisclaimerURL)\n\n\tform.Set(\"branding_webadmin_disclaimer_url\", \"https://example.net\")\n\ttmpFile := filepath.Join(os.TempDir(), util.GenerateUniqueID()+\".png\")\n\terr = createTestPNG(tmpFile, 512, 512, color.RGBA{100, 200, 200, 0xff})\n\tassert.NoError(t, err)\n\n\tb, contentType, err = getMultipartFormData(form, \"branding_webadmin_logo\", tmpFile)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\t// check\n\tconfigs, err := dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"Custom WebAdmin\", configs.Branding.WebAdmin.Name)\n\tassert.Equal(t, \"WebAdmin\", configs.Branding.WebAdmin.ShortName)\n\tassert.Equal(t, \"Admin disclaimer\", configs.Branding.WebAdmin.DisclaimerName)\n\tassert.Equal(t, \"https://example.net\", configs.Branding.WebAdmin.DisclaimerURL)\n\tassert.Equal(t, \"Custom WebClient\", configs.Branding.WebClient.Name)\n\tassert.Equal(t, \"WebClient\", configs.Branding.WebClient.ShortName)\n\tassert.Equal(t, \"Client disclaimer\", configs.Branding.WebClient.DisclaimerName)\n\tassert.Equal(t, \"https://example.com\", configs.Branding.WebClient.DisclaimerURL)\n\tassert.Greater(t, len(configs.Branding.WebAdmin.Logo), 0)\n\tassert.Len(t, configs.Branding.WebAdmin.Favicon, 0)\n\tassert.Len(t, configs.Branding.WebClient.Logo, 0)\n\tassert.Len(t, configs.Branding.WebClient.Favicon, 0)\n\n\terr = createTestPNG(tmpFile, 256, 256, color.RGBA{120, 220, 220, 0xff})\n\tassert.NoError(t, err)\n\tform.Set(\"branding_webadmin_logo_remove\", \"0\") // 0 preserves WebAdmin logo\n\tb, contentType, err = getMultipartFormData(form, \"branding_webadmin_favicon\", tmpFile)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"Custom WebAdmin\", configs.Branding.WebAdmin.Name)\n\tassert.Equal(t, \"WebAdmin\", configs.Branding.WebAdmin.ShortName)\n\tassert.Equal(t, \"Admin disclaimer\", configs.Branding.WebAdmin.DisclaimerName)\n\tassert.Equal(t, \"https://example.net\", configs.Branding.WebAdmin.DisclaimerURL)\n\tassert.Equal(t, \"Custom WebClient\", configs.Branding.WebClient.Name)\n\tassert.Equal(t, \"WebClient\", configs.Branding.WebClient.ShortName)\n\tassert.Equal(t, \"Client disclaimer\", configs.Branding.WebClient.DisclaimerName)\n\tassert.Equal(t, \"https://example.com\", configs.Branding.WebClient.DisclaimerURL)\n\tassert.Greater(t, len(configs.Branding.WebAdmin.Logo), 0)\n\tassert.Greater(t, len(configs.Branding.WebAdmin.Favicon), 0)\n\tassert.Len(t, configs.Branding.WebClient.Logo, 0)\n\tassert.Len(t, configs.Branding.WebClient.Favicon, 0)\n\n\terr = createTestPNG(tmpFile, 256, 256, color.RGBA{80, 90, 110, 0xff})\n\tassert.NoError(t, err)\n\tb, contentType, err = getMultipartFormData(form, \"branding_webclient_logo\", tmpFile)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Greater(t, len(configs.Branding.WebClient.Logo), 0)\n\n\terr = createTestPNG(tmpFile, 256, 256, color.RGBA{120, 50, 120, 0xff})\n\tassert.NoError(t, err)\n\tb, contentType, err = getMultipartFormData(form, \"branding_webclient_favicon\", tmpFile)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Greater(t, len(configs.Branding.WebClient.Favicon), 0)\n\n\tfor _, p := range []string{webClientLogoPath, webClientFaviconPath, webAdminLogoPath, webAdminFaviconPath} {\n\t\treq, err := http.NewRequest(http.MethodGet, p, nil)\n\t\tassert.NoError(t, err)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t}\n\t// remove images\n\tform.Set(\"branding_webadmin_logo_remove\", \"1\")\n\tform.Set(\"branding_webclient_logo_remove\", \"1\")\n\tform.Set(\"branding_webadmin_favicon_remove\", \"1\")\n\tform.Set(\"branding_webclient_favicon_remove\", \"1\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nConfigsOK)\n\tconfigs, err = dataprovider.GetConfigs()\n\tassert.NoError(t, err)\n\tassert.Len(t, configs.Branding.WebAdmin.Logo, 0)\n\tassert.Len(t, configs.Branding.WebAdmin.Favicon, 0)\n\tassert.Len(t, configs.Branding.WebClient.Logo, 0)\n\tassert.Len(t, configs.Branding.WebClient.Favicon, 0)\n\tfor _, p := range []string{webClientLogoPath, webClientFaviconPath, webAdminLogoPath, webAdminFaviconPath} {\n\t\treq, err := http.NewRequest(http.MethodGet, p, nil)\n\t\tassert.NoError(t, err)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t}\n\tform.Del(\"branding_webadmin_logo_remove\")\n\tform.Del(\"branding_webclient_logo_remove\")\n\tform.Del(\"branding_webadmin_favicon_remove\")\n\tform.Del(\"branding_webclient_favicon_remove\")\n\t// image too large\n\terr = createTestPNG(tmpFile, 768, 512, color.RGBA{120, 50, 120, 0xff})\n\tassert.NoError(t, err)\n\tb, contentType, err = getMultipartFormData(form, \"branding_webclient_logo\", tmpFile)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidPNGSize)\n\t// not a png image\n\terr = createTestFile(tmpFile, 128)\n\tassert.NoError(t, err)\n\tb, contentType, err = getMultipartFormData(form, \"branding_webclient_logo\", tmpFile)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidPNG)\n\n\terr = os.Remove(tmpFile)\n\tassert.NoError(t, err)\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPLoopError(t *testing.T) {\n\tuser1 := getTestUser()\n\tuser2 := getTestUser()\n\tuser1.Username += \"1\"\n\tuser1.Email = \"user1@test.com\"\n\tuser2.Username += \"2\"\n\tuser1.FsConfig = vfs.Filesystem{\n\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\tUsername: user2.Username,\n\t\t\t},\n\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t},\n\t}\n\n\tuser2.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: sftpServerAddr,\n\t\t\tUsername: user1.Username,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t}\n\n\tuser1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tuser2, resp, err = httpdtest.AddUser(user2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\t// test reset password\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          3525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr = smtpCfg.Initialize(configDir, true)\n\tassert.NoError(t, err)\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user1.Username)\n\tlastResetCode = \"\"\n\treq, err := http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr := executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"password\", defaultPassword)\n\tform.Set(\"confirm_password\", defaultPassword)\n\tform.Set(\"code\", lastResetCode)\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorLoginAfterReset)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user2.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginInvalidFs(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.AllowAPIKeyAuth = true\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"test\"\n\tu.FsConfig.GCSConfig.UploadPartSize = 1\n\tu.FsConfig.GCSConfig.UploadPartMaxTime = 10\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(\"invalid JSON for credentials\")\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tapiKey, _, err := httpdtest.AddAPIKey(dataprovider.APIKey{\n\t\tName:  \"testk\",\n\t\tScope: dataprovider.APIKeyScopeUser,\n\t\tUser:  u.Username,\n\t}, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\t_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\n\t_, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebClientChangePwd(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webChangeClientPwdPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform := make(url.Values)\n\tform.Set(\"current_password\", defaultPassword)\n\tform.Set(\"new_password1\", defaultPassword)\n\tform.Set(\"new_password2\", defaultPassword)\n\t// no csrf token\n\treq, err = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webChangeClientPwdPath, webToken)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdNoDifferent)\n\n\tform.Set(\"current_password\", defaultPassword+\"2\")\n\tform.Set(\"new_password1\", defaultPassword+\"1\")\n\tform.Set(\"new_password2\", defaultPassword+\"1\")\n\treq, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdCurrentNoMatch)\n\n\tform.Set(\"current_password\", defaultPassword)\n\tform.Set(\"new_password1\", defaultPassword+\"1\")\n\tform.Set(\"new_password2\", defaultPassword+\"1\")\n\treq, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPingPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\n\t_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.Error(t, err)\n\t_, err = getJWTWebClientTokenFromTestServer(defaultUsername+\"1\", defaultPassword+\"1\")\n\tassert.Error(t, err)\n\t_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword+\"1\")\n\tassert.NoError(t, err)\n\n\t// remove the change password permission\n\tuser.Filters.WebClient = []string{sdk.WebClientPasswordChangeDisabled}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.WebClient, 1)\n\tassert.Contains(t, user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)\n\n\twebToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword+\"1\")\n\tassert.NoError(t, err)\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientProfilePath, webToken)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"current_password\", defaultPassword+\"1\")\n\tform.Set(\"new_password1\", defaultPassword)\n\tform.Set(\"new_password2\", defaultPassword)\n\treq, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPreDownloadHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldExecuteOn := common.Config.Actions.ExecuteOn\n\toldHook := common.Config.Actions.Hook\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}\n\tcommon.Config.Actions.Hook = preActionPath\n\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preActionPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\n\ttestFileName := \"testfile\"\n\ttestFileContents := []byte(\"file contents\")\n\terr = os.MkdirAll(filepath.Join(user.GetHomeDir()), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), testFileName), testFileContents, os.ModePerm)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, testFileContents, rr.Body.Bytes())\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, testFileContents, rr.Body.Bytes())\n\n\terr = os.WriteFile(preActionPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError403Message)\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"permission denied\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.ExecuteOn = oldExecuteOn\n\tcommon.Config.Actions.Hook = oldHook\n}\n\nfunc TestPreUploadHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldExecuteOn := common.Config.Actions.ExecuteOn\n\toldHook := common.Config.Actions.Hook\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreUpload}\n\tcommon.Config.Actions.Hook = preActionPath\n\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preActionPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", \"filepre\")\n\tassert.NoError(t, err)\n\t_, err = part.Write([]byte(\"file content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=filepre\",\n\t\tbytes.NewBuffer([]byte(\"single upload content\")))\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\terr = os.WriteFile(preActionPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=filepre\",\n\t\tbytes.NewBuffer([]byte(\"single upload content\")))\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.ExecuteOn = oldExecuteOn\n\tcommon.Config.Actions.Hook = oldHook\n}\n\nfunc TestShareUsage(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFileName := \"testfile.dat\"\n\ttestFileSize := int64(65536)\n\ttestFilePath := filepath.Join(user.GetHomeDir(), testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:      \"test share\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{\"/\"},\n\t\tPassword:  defaultPassword,\n\t\tMaxTokens: 2,\n\t\tExpiresAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Second)),\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodGet, sharesPath+\"/unknownid\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, sharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\treq.SetBasicAuth(defaultUsername, \"wrong password\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\ttime.Sleep(2 * time.Second)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID+\"_mod\", nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webClientPubSharesPath + \"/\" + objectID + \"_mod\"\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tshare.ExpiresAt = 0\n\tjsonReq := make(map[string]any)\n\tjsonReq[\"name\"] = share.Name\n\tjsonReq[\"scope\"] = share.Scope\n\tjsonReq[\"paths\"] = share.Paths\n\tjsonReq[\"password\"] = share.Password\n\tjsonReq[\"max_tokens\"] = share.MaxTokens\n\tjsonReq[\"expires_at\"] = share.ExpiresAt\n\tasJSON, err = json.Marshal(jsonReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userSharesPath+\"/\"+objectID, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"invalid share scope\")\n\n\tshare.MaxTokens = 3\n\tshare.Scope = dataprovider.ShareScopeWrite\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userSharesPath+\"/\"+objectID, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart1, err := writer.CreateFormFile(\"filenames\", \"file1.txt\")\n\tassert.NoError(t, err)\n\t_, err = part1.Write([]byte(\"file1 content\"))\n\tassert.NoError(t, err)\n\tpart2, err := writer.CreateFormFile(\"filenames\", \"file2.txt\")\n\tassert.NoError(t, err)\n\t_, err = part2.Write([]byte(\"file2 content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to parse multipart form\")\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\t// set the proper content type\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Allowed usage exceeded\")\n\n\tshare.MaxTokens = 6\n\tshare.Scope = dataprovider.ShareScopeWrite\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, userSharesPath+\"/\"+objectID, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientPubSharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tshare, err = dataprovider.ShareExists(objectID, user.Username)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 6, share.UsedTokens)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tshare.MaxTokens = 0\n\terr = dataprovider.UpdateShare(&share, user.Username, \"\", \"\")\n\tassert.NoError(t, err)\n\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"permission denied\")\n\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tbody = new(bytes.Buffer)\n\twriter = multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filename\", \"file1.txt\")\n\tassert.NoError(t, err)\n\t_, err = part.Write([]byte(\"file content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader = bytes.NewReader(body.Bytes())\n\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"No files uploaded!\")\n\n\tuser.Filters.WebClient = []string{sdk.WebClientSharesDisabled}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tuser.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tshare.Password = \"\"\n\terr = dataprovider.UpdateShare(&share, user.Username, \"\", \"\")\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"sharing without a password was disabled\")\n\n\tuser.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tshare.Scope = dataprovider.ShareScopeRead\n\tshare.Paths = []string{\"/missing1\", \"/missing2\"}\n\terr = dataprovider.UpdateShare(&share, user.Username, \"\", \"\")\n\tassert.NoError(t, err)\n\n\tdefer func() {\n\t\trcv := recover()\n\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\n\t\tshare, err = dataprovider.ShareExists(objectID, user.Username)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 6, share.UsedTokens)\n\n\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t}()\n\n\treq, err = http.NewRequest(http.MethodGet, sharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\texecuteRequest(req)\n}\n\nfunc TestSharePasswordPolicy(t *testing.T) {\n\tg := getTestGroup()\n\tg.UserSettings.Filters.PasswordStrength = 70\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: g.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tu.Password = rand.Text()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, u.Password)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:     util.GenerateUniqueID(),\n\t\tScope:    dataprovider.ShareScopeRead,\n\t\tPaths:    []string{\"/\"},\n\t\tPassword: defaultPassword,\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"insecure password\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestShareMaxExpiration(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.MaxSharesExpiration = 5\n\tu.Filters.DefaultSharesExpiration = 10\n\t_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(resp), \"must be less than or equal to max shares expiration\")\n\n\tu.Filters.DefaultSharesExpiration = 0\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebClientToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\ts := dataprovider.Share{\n\t\tName:      \"test share\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPassword:  defaultPassword,\n\t\tPaths:     []string{\"/\"},\n\t\tExpiresAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(u.Filters.MaxSharesExpiration+2))),\n\t}\n\tasJSON, err := json.Marshal(s)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"share must expire before\")\n\n\treq, err = http.NewRequest(http.MethodPut, path.Join(userSharesPath, \"shareID\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// expiresAt is mandatory\n\ts.ExpiresAt = 0\n\tasJSON, err = json.Marshal(s)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"share must expire before\")\n\n\ts.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(2 * time.Hour))\n\tasJSON, err = json.Marshal(s)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tshareID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, shareID)\n\n\ts.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(u.Filters.MaxSharesExpiration+2)))\n\tasJSON, err = json.Marshal(s)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, path.Join(userSharesPath, shareID), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"share must expire before\")\n\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientSharePath, webClientToken)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"name\", s.Name)\n\tform.Set(\"scope\", strconv.Itoa(int(s.Scope)))\n\tform.Set(\"max_tokens\", \"0\")\n\tform.Set(\"paths[0][path]\", \"/\")\n\tform.Set(\"expiration_date\", time.Now().Add(24*time.Hour*time.Duration(u.Filters.MaxSharesExpiration+2)).Format(\"2006-01-02 15:04:05\"))\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webClientToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareExpirationOutOfRange)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientSharePath, shareID), bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webClientToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareExpirationOutOfRange)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webClientToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)\n}\n\nfunc TestWebClientShareCredentials(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshareRead := dataprovider.Share{\n\t\tName:     \"test share read\",\n\t\tScope:    dataprovider.ShareScopeRead,\n\t\tPassword: defaultPassword,\n\t\tPaths:    []string{\"/\"},\n\t}\n\n\tshareWrite := dataprovider.Share{\n\t\tName:     \"test share write\",\n\t\tScope:    dataprovider.ShareScopeReadWrite,\n\t\tPassword: defaultPassword,\n\t\tPaths:    []string{\"/\"},\n\t}\n\n\tasJSON, err := json.Marshal(shareRead)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tshareReadID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, shareReadID)\n\n\tasJSON, err = json.Marshal(shareWrite)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tshareWriteID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, shareWriteID)\n\n\turi := path.Join(webClientPubSharesPath, shareReadID, \"browse\")\n\treq, err = http.NewRequest(http.MethodGet, uri, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tlocation := rr.Header().Get(\"Location\")\n\tassert.Contains(t, location, url.QueryEscape(uri))\n\t// get the login form\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// now set the user token, it is not valid for the share\n\treq, err = http.NewRequest(http.MethodGet, uri, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\t// get a share token\n\tform := make(url.Values)\n\tform.Set(\"share_password\", defaultPassword)\n\tloginURI := path.Join(webClientPubSharesPath, shareReadID, \"login\")\n\treq, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\t// set the CSRF token\n\tloginCookie, csrfToken, err := getCSRFTokenMock(loginURI, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nShareLoginOK)\n\tcookie := rr.Header().Get(\"Set-Cookie\")\n\tcookie = strings.TrimPrefix(cookie, \"jwt=\")\n\tassert.NotEmpty(t, cookie)\n\treq, err = http.NewRequest(http.MethodGet, uri, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// get the download page\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareReadID, \"download?a=b\"), nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// get the download page for a missing share\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, \"invalidshareid\", \"download\"), nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// the same cookie will not work for the other share\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareWriteID, \"browse\"), nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\t// IP address does not match\n\treq, err = http.NewRequest(http.MethodGet, uri, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, cookie)\n\treq.RemoteAddr = \"1.2.3.4:1234\"\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\t// logout to a different share, the cookie is not valid.\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareWriteID, \"logout\"), nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\t// logout\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareReadID, \"logout\"), nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\t// the cookie is no longer valid\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, shareReadID, \"download?b=c\"), nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = uri\n\tsetJWTCookieForReq(req, cookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Contains(t, rr.Header().Get(\"Location\"), \"/login\")\n\n\t// try to login with invalid credentials\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"share_password\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\t// login with the next param set\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"share_password\", defaultPassword)\n\tnextURI := path.Join(webClientPubSharesPath, shareReadID, \"browse\")\n\tloginURI = path.Join(webClientPubSharesPath, shareReadID, fmt.Sprintf(\"login?next=%s\", url.QueryEscape(nextURI)))\n\treq, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, nextURI, rr.Header().Get(\"Location\"))\n\t// try to login to a missing share\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tloginURI = path.Join(webClientPubSharesPath, \"missing\", \"login\")\n\treq, err = http.NewRequest(http.MethodPost, loginURI, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestShareMaxSessions(t *testing.T) {\n\tu := getTestUser()\n\tu.MaxSessions = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:  \"test share max sessions read\",\n\t\tScope: dataprovider.ShareScopeRead,\n\t\tPaths: []string{\"/\"},\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodGet, sharesPath+\"/\"+objectID+\"/dirs\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// add a fake connection\n\tfs := vfs.NewOsFs(\"id\", os.TempDir(), \"\", nil)\n\tconnection := &httpd.Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolHTTP, \"\", \"\", user),\n\t}\n\terr = common.Connections.Add(connection)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, sharesPath+\"/\"+objectID+\"/dirs\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), \"too many open sessions\")\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID+\"/dirs\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), \"too many open sessions\")\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=file.pdf\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError429Message)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID+\"/browse\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError429Message)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientPubSharesPath+\"/\"+objectID+\"/browse/exist\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"invalid share scope\")\n\n\treq, err = http.NewRequest(http.MethodGet, sharesPath+\"/\"+objectID+\"/files?path=afile\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), \"too many open sessions\")\n\n\tform := make(url.Values)\n\tform.Set(\"files\", `[]`)\n\treq, err = http.NewRequest(http.MethodPost, webClientPubSharesPath+\"/\"+objectID+\"/partial\",\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError429Message)\n\n\treq, err = http.NewRequest(http.MethodGet, sharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), \"too many open sessions\")\n\n\treq, err = http.NewRequest(http.MethodDelete, userSharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// now test a write share\n\tshare = dataprovider.Share{\n\t\tName:  \"test share max sessions write\",\n\t\tScope: dataprovider.ShareScopeWrite,\n\t\tPaths: []string{\"/\"},\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, \"file.txt\"), bytes.NewBuffer([]byte(\"content\")))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), \"too many open sessions\")\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart1, err := writer.CreateFormFile(\"filenames\", \"file1.txt\")\n\tassert.NoError(t, err)\n\t_, err = part1.Write([]byte(\"file1 content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), \"too many open sessions\")\n\n\tshare = dataprovider.Share{\n\t\tName:  \"test share max sessions read&write\",\n\t\tScope: dataprovider.ShareScopeReadWrite,\n\t\tPaths: []string{\"/\"},\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientPubSharesPath+\"/\"+objectID+\"/browse/exist\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusTooManyRequests, rr)\n\tassert.Contains(t, rr.Body.String(), \"too many open sessions\")\n\n\tcommon.Connections.Remove(connection.GetID())\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestShareUploadSingle(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:      \"test share\",\n\t\tScope:     dataprovider.ShareScopeWrite,\n\t\tPaths:     []string{\"/\"},\n\t\tPassword:  defaultPassword,\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\tcontent := []byte(\"shared file content\")\n\tmodTime := time.Now().Add(-12 * time.Hour)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, \"file.txt\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\treq.Header.Set(\"X-SFTPGO-MTIME\", strconv.FormatInt(util.GetTimeAsMsSinceEpoch(modTime), 10))\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tinfo, err := os.Stat(filepath.Join(user.GetHomeDir(), \"file.txt\"))\n\tif assert.NoError(t, err) {\n\t\tassert.InDelta(t, util.GetTimeAsMsSinceEpoch(modTime), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(1000))\n\t}\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"upload\"), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"file.txt\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tinfo, err = os.Stat(filepath.Join(user.GetHomeDir(), \"file.txt\"))\n\tif assert.NoError(t, err) {\n\t\tassert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000))\n\t}\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, \"dir\", \"file.dat\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, \"%2F\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"operation unsupported\")\n\n\terr = os.MkdirAll(filepath.Join(user.GetHomeDir(), \"dir\"), os.ModePerm)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, \"dir\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"operation unsupported\")\n\n\t// only uploads to the share root dir are allowed\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, \"dir\", \"file.dat\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tshare, err = dataprovider.ShareExists(objectID, user.Username)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2, share.UsedTokens)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, \"file1.txt\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestShareReadWrite(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.StartDirectory = path.Join(\"/start\", \"dir\")\n\tu.Permissions[\"/start/dir/limited\"] = []string{dataprovider.PermListItems}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\ttestFileName := \"test.txt\"\n\ttestSubDirs := \"/sub/dir\"\n\n\tshare := dataprovider.Share{\n\t\tName:      \"test share rw\",\n\t\tScope:     dataprovider.ShareScopeReadWrite,\n\t\tPaths:     []string{user.Filters.StartDirectory},\n\t\tPassword:  defaultPassword,\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\tfilesToCheck := make(map[string]any)\n\tfilesToCheck[\"files\"] = []string{testFileName}\n\tasJSON, err = json.Marshal(filesToCheck)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"/browse/exist?path=%2F\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar fileList []any\n\terr = json.Unmarshal(rr.Body.Bytes(), &fileList)\n\tassert.NoError(t, err)\n\tassert.Len(t, fileList, 0)\n\n\tcontent := []byte(\"shared rw content\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, testFileName), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tassert.FileExists(t, filepath.Join(user.GetHomeDir(), user.Filters.StartDirectory, testFileName))\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+\"/\"+url.PathEscape(path.Join(testSubDirs, testFileName)), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+\"/\"+url.PathEscape(path.Join(testSubDirs, testFileName))+\"?mkdir_parents=true\",\n\t\tbytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tassert.FileExists(t, filepath.Join(user.GetHomeDir(), user.Filters.StartDirectory, testSubDirs, testFileName))\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+\"/\"+url.PathEscape(path.Join(\"limited\", \"sub\", testFileName))+\"?mkdir_parents=true\",\n\t\tbytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"/browse/exist?path=%2F\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tfileList = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &fileList)\n\tassert.NoError(t, err)\n\tassert.Len(t, fileList, 1)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=\"+testFileName), nil) //nolint:goconst\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontentDisposition := rr.Header().Get(\"Content-Disposition\")\n\tassert.NotEmpty(t, contentDisposition)\n\n\tform := make(url.Values)\n\tform.Set(\"files\", fmt.Sprintf(`[\"%s\"]`, testFileName))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"partial\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontentDisposition = rr.Header().Get(\"Content-Disposition\")\n\tassert.NotEmpty(t, contentDisposition)\n\tassert.Equal(t, \"application/zip\", rr.Header().Get(\"Content-Type\"))\n\t// parse form error\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"partial?path=p%C3%AO%GK\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// invalid files list\n\tform.Set(\"files\", fmt.Sprintf(`[%s]`, testFileName))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"partial\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError400Message)\n\t// missing directory\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"partial?path=missing\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError400Message)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+\"/\"+url.PathEscape(\"../\"+testFileName),\n\t\tbytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Uploading outside the share is not allowed\")\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+\"/\"+url.PathEscape(\"/../../\"+testFileName),\n\t\tbytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Uploading outside the share is not allowed\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestShareUncompressed(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFileName := \"testfile.dat\"\n\ttestFileSize := int64(65536)\n\ttestFilePath := filepath.Join(user.GetHomeDir(), testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:      \"test share\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{\"/\"},\n\t\tPassword:  defaultPassword,\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\ts, err := dataprovider.ShareExists(objectID, defaultUsername)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), s.ExpiresAt)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"application/zip\", rr.Header().Get(\"Content-Type\"))\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID+\"?compress=false\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"application/zip\", rr.Header().Get(\"Content-Type\"))\n\n\tshare = dataprovider.Share{\n\t\tName:      \"test share1\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{testFileName},\n\t\tPassword:  defaultPassword,\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"application/zip\", rr.Header().Get(\"Content-Type\"))\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID+\"?compress=false\", nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"application/octet-stream\", rr.Header().Get(\"Content-Type\"))\n\n\tshare, err = dataprovider.ShareExists(objectID, user.Username)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2, share.UsedTokens)\n\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID+\"?compress=false\", nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tshare, err = dataprovider.ShareExists(objectID, user.Username)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2, share.UsedTokens)\n\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID+\"?compress=false\", nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDownloadFromShareError(t *testing.T) {\n\tu := getTestUser()\n\tu.DownloadDataTransfer = 1\n\tu.Filters.DefaultSharesExpiration = 10\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.UsedDownloadDataTransfer = 1024*1024 - 32768\n\t_, err = httpdtest.UpdateTransferQuotaUsage(user, \"add\", http.StatusOK)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1024*1024-32768), user.UsedDownloadDataTransfer)\n\ttestFileName := \"test_share_file.dat\"\n\ttestFileSize := int64(524288)\n\ttestFilePath := filepath.Join(user.GetHomeDir(), testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tshare := dataprovider.Share{\n\t\tName:      \"test share root browse\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{\"/\"},\n\t\tMaxTokens: 2,\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\ts, err := dataprovider.ShareExists(objectID, defaultUsername)\n\tassert.NoError(t, err)\n\tassert.Greater(t, s.ExpiresAt, int64(0))\n\n\tdefer func() {\n\t\trcv := recover()\n\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\n\t\tshare, err = dataprovider.ShareExists(objectID, user.Username)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, share.UsedTokens)\n\n\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t}()\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=\"+testFileName), nil)\n\tassert.NoError(t, err)\n\texecuteRequest(req)\n}\n\nfunc TestBrowseShares(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFileName := \"testsharefile.dat\"\n\ttestFileNameLink := \"testsharefile.link\"\n\tshareDir := \"share\"\n\tsubDir := \"sub\"\n\ttestFileSize := int64(65536)\n\ttestFilePath := filepath.Join(user.GetHomeDir(), shareDir, testFileName)\n\ttestLinkPath := filepath.Join(user.GetHomeDir(), shareDir, testFileNameLink)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = createTestFile(filepath.Join(user.GetHomeDir(), shareDir, subDir, testFileName), testFileSize)\n\tassert.NoError(t, err)\n\terr = os.Symlink(testFilePath, testLinkPath)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:      \"test share browse\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{shareDir},\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"upload\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"invalid share scope\")\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"files?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Please set the path to a valid file\")\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"dirs?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents := make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 2)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 2)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs?path=%2F\"+subDir), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 1)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=%2F..\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"dirs?path=%2F..\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs?path=%2F..\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"files?path=%2F..%2F\"+testFileName), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=\"+testFileName), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontentDisposition := rr.Header().Get(\"Content-Disposition\")\n\tassert.NotEmpty(t, contentDisposition)\n\n\tform := make(url.Values)\n\tform.Set(\"files\", `[]`)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"partial?path=%2F..\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"files?path=\"+testFileName), nil) //nolint:goconst\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontentDisposition = rr.Header().Get(\"Content-Disposition\")\n\tassert.NotEmpty(t, contentDisposition)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=\"+subDir+\"%2F\"+testFileName), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontentDisposition = rr.Header().Get(\"Content-Disposition\")\n\tassert.NotEmpty(t, contentDisposition)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=missing\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"files?path=missing\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"dirs?path=missing\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs?path=missing\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=\"+testFileNameLink), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"files?path=\"+testFileNameLink), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"non regular files are not supported for shares\")\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=\"+testFileName), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=missing\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=%2F..\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid)\n\n\tfakePDF := []byte(`%PDF-1.6`)\n\tfor i := 0; i < 128; i++ {\n\t\tfakePDF = append(fakePDF, []byte(fmt.Sprintf(\"%d\", i))...)\n\t}\n\tpdfPath := filepath.Join(user.GetHomeDir(), shareDir, \"test.pdf\")\n\tpdfLinkPath := filepath.Join(user.GetHomeDir(), shareDir, \"link.pdf\")\n\terr = os.WriteFile(pdfPath, fakePDF, 0666)\n\tassert.NoError(t, err)\n\terr = os.Symlink(pdfPath, pdfLinkPath)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"viewpdf?path=test.pdf\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=test.pdf\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\ts, err := dataprovider.ShareExists(objectID, defaultUsername)\n\tassert.NoError(t, err)\n\tusedTokens := s.UsedTokens\n\tassert.Greater(t, usedTokens, 0)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=link.pdf\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// downloading a symlink will fail, usage should not change\n\ts, err = dataprovider.ShareExists(objectID, defaultUsername)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedTokens, s.UsedTokens)\n\n\t// share a symlink\n\tshare = dataprovider.Share{\n\t\tName:      \"test share browse\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{path.Join(shareDir, testFileNameLink)},\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\t// uncompressed download should not work\n\treq, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+\"/\"+objectID+\"?compress=false\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"application/zip\", rr.Header().Get(\"Content-Type\"))\n\t// this share is not browsable, it does not contains a directory\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tform = make(url.Values)\n\tform.Set(\"files\", `[]`)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"partial\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=\"+testFileName), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"files?path=\"+testFileName), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"dirs?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the shared object is not a directory and so it is not browsable\")\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir)\n\n\t// now test a missing shareID\n\tobjectID = \"123456\"\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"files?path=\"+testFileName), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"dirs?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tform = make(url.Values)\n\tform.Set(\"files\", `[]`)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"partial?path=%2F\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"viewpdf?path=p\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"getpdf?path=p\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// share a missing base path\n\tshare = dataprovider.Share{\n\t\tName:      \"test share\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{path.Join(shareDir, \"missingdir\")},\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"unable to check the share directory\")\n\t// share multiple paths\n\tshare = dataprovider.Share{\n\t\tName:      \"test share\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{shareDir, \"/anotherdir\"},\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowsePaths)\n\n\tshare = dataprovider.Share{\n\t\tName:      \"test share rw\",\n\t\tScope:     dataprovider.ShareScopeReadWrite,\n\t\tPaths:     []string{\"/missingdir\"},\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"/browse/exist?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"unable to check the share directory\")\n\n\tshare = dataprovider.Share{\n\t\tName:      \"test share rw\",\n\t\tScope:     dataprovider.ShareScopeReadWrite,\n\t\tPaths:     []string{shareDir},\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"/browse/exist?path=%2F..\"), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Invalid path\")\n\t// share the root path\n\tshare = dataprovider.Share{\n\t\tName:      \"test share root\",\n\t\tScope:     dataprovider.ShareScopeRead,\n\t\tPaths:     []string{\"/\"},\n\t\tMaxTokens: 0,\n\t}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 1)\n\t// if we require two-factor auth for HTTP protocol the share should not work anymore\n\tuser.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tuser.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH, common.ProtocolHTTP}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, \"dirs?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"two-factor authentication requirements not met\")\n\tuser.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t// share read/write\n\tshare.Scope = dataprovider.ShareScopeReadWrite\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"browse?path=%2F\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// on upload we should be redirected\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, \"upload\"), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(defaultUsername, defaultPassword)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tlocation := rr.Header().Get(\"Location\")\n\tassert.Equal(t, path.Join(webClientPubSharesPath, objectID, \"browse\"), location)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUserAPIShareErrors(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tScope: 1000,\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"invalid scope\")\n\t// invalid json\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer([]byte(\"{\")))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tshare.Scope = dataprovider.ShareScopeWrite\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"at least a shared path is required\")\n\n\tshare.Paths = []string{\"path1\", \"../path1\", \"/path2\"}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the write share scope requires exactly one path\")\n\n\tshare.Paths = []string{\"\", \"\"}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"at least a shared path is required\")\n\n\tshare.Paths = []string{\"path1\", \"../path1\", \"/path1\"}\n\tshare.Password = redactedSecret\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"cannot save a share with a redacted password\")\n\n\tshare.Password = \"newpass\"\n\tshare.AllowFrom = []string{\"not valid\"}\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"could not parse allow from entry\")\n\n\tshare.AllowFrom = []string{\"127.0.0.1/8\"}\n\tshare.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-12 * time.Hour))\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"expiration must be in the future\")\n\n\tshare.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(12 * time.Hour))\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tlocation := rr.Header().Get(\"Location\")\n\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"name is mandatory\")\n\t// invalid json\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer([]byte(\"}\")))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userSharesPath+\"?limit=a\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUserAPIShares(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tu := getTestUser()\n\tu.Username = altAdminUsername\n\tuser1, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken1, err := getJWTAPIUserTokenFromTestServer(user1.Username, defaultPassword)\n\tassert.NoError(t, err)\n\n\t// the share username will be set from the claims\n\tshare := dataprovider.Share{\n\t\tName:        \"share1\",\n\t\tDescription: \"description1\",\n\t\tScope:       dataprovider.ShareScopeRead,\n\t\tPaths:       []string{\"/\"},\n\t\tCreatedAt:   1,\n\t\tUpdatedAt:   2,\n\t\tLastUseAt:   3,\n\t\tExpiresAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(2 * time.Hour)),\n\t\tPassword:    defaultPassword,\n\t\tMaxTokens:   10,\n\t\tUsedTokens:  2,\n\t\tAllowFrom:   []string{\"192.168.1.0/24\"},\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tlocation := rr.Header().Get(\"Location\")\n\tassert.NotEmpty(t, location)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\tassert.Equal(t, fmt.Sprintf(\"%v/%v\", userSharesPath, objectID), location)\n\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar shareGet dataprovider.Share\n\terr = json.Unmarshal(rr.Body.Bytes(), &shareGet)\n\tassert.NoError(t, err)\n\tassert.Equal(t, objectID, shareGet.ShareID)\n\tassert.Equal(t, share.Name, shareGet.Name)\n\tassert.Equal(t, share.Description, shareGet.Description)\n\tassert.Equal(t, share.Scope, shareGet.Scope)\n\tassert.Equal(t, share.Paths, shareGet.Paths)\n\tassert.Equal(t, int64(0), shareGet.LastUseAt)\n\tassert.Greater(t, shareGet.CreatedAt, share.CreatedAt)\n\tassert.Greater(t, shareGet.UpdatedAt, share.UpdatedAt)\n\tassert.Equal(t, share.ExpiresAt, shareGet.ExpiresAt)\n\tassert.Equal(t, share.MaxTokens, shareGet.MaxTokens)\n\tassert.Equal(t, 0, shareGet.UsedTokens)\n\tassert.Equal(t, share.Paths, shareGet.Paths)\n\tassert.Equal(t, redactedSecret, shareGet.Password)\n\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token1)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\ts, err := dataprovider.ShareExists(objectID, defaultUsername)\n\tassert.NoError(t, err)\n\tmatch, err := s.CheckCredentials(defaultPassword)\n\tassert.True(t, match)\n\tassert.NoError(t, err)\n\tmatch, err = s.CheckCredentials(defaultPassword + \"mod\")\n\tassert.False(t, match)\n\tassert.Error(t, err)\n\n\tshareGet.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(3 * time.Hour))\n\tasJSON, err = json.Marshal(shareGet)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\ts, err = dataprovider.ShareExists(objectID, defaultUsername)\n\tassert.NoError(t, err)\n\tmatch, err = s.CheckCredentials(defaultPassword)\n\tassert.True(t, match)\n\tassert.NoError(t, err)\n\tmatch, err = s.CheckCredentials(defaultPassword + \"mod\")\n\tassert.False(t, match)\n\tassert.Error(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar shareGetNew dataprovider.Share\n\terr = json.Unmarshal(rr.Body.Bytes(), &shareGetNew)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, shareGet.UpdatedAt, shareGetNew.UpdatedAt)\n\tshareGet.UpdatedAt = shareGetNew.UpdatedAt\n\tassert.Equal(t, shareGet, shareGetNew)\n\n\treq, err = http.NewRequest(http.MethodGet, userSharesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar shares []dataprovider.Share\n\terr = json.Unmarshal(rr.Body.Bytes(), &shares)\n\tassert.NoError(t, err)\n\tif assert.Len(t, shares, 1) {\n\t\tassert.Equal(t, shareGetNew, shares[0])\n\t}\n\n\terr = dataprovider.UpdateShareLastUse(&shareGetNew, 2)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tshareGetNew = dataprovider.Share{}\n\terr = json.Unmarshal(rr.Body.Bytes(), &shareGetNew)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 2, shareGetNew.UsedTokens, \"share: %v\", shareGetNew)\n\tassert.Greater(t, shareGetNew.LastUseAt, int64(0), \"share: %v\", shareGetNew)\n\n\treq, err = http.NewRequest(http.MethodGet, userSharesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token1)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tshares = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &shares)\n\tassert.NoError(t, err)\n\tassert.Len(t, shares, 0)\n\n\t// set an empty password\n\tshareGet.Password = \"\"\n\tasJSON, err = json.Marshal(shareGet)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tshareGetNew = dataprovider.Share{}\n\terr = json.Unmarshal(rr.Body.Bytes(), &shareGetNew)\n\tassert.NoError(t, err)\n\tassert.Empty(t, shareGetNew.Password)\n\n\treq, err = http.NewRequest(http.MethodDelete, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tshare.Name = \"\"\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tlocation = rr.Header().Get(\"Location\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t// the share should be deleted with the associated user\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUsersAPISharesNoPasswordDisabled(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled}\n\tu.Filters.PasswordStrength = 70\n\tu.Password = \"ahpoo8baa6EeZieshies\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, u.Password)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:  \"s\",\n\t\tScope: dataprovider.ShareScopeRead,\n\t\tPaths: []string{\"/\"},\n\t}\n\tasJSON, err := json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"You are not authorized to share files/folders without a password\")\n\n\tshare.Password = defaultPassword\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tshare.Password = \"vi5eiJoovee5ya9yahpi\"\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tlocation := rr.Header().Get(\"Location\")\n\tassert.NotEmpty(t, location)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\tassert.Equal(t, fmt.Sprintf(\"%v/%v\", userSharesPath, objectID), location)\n\n\tshare.Password = \"\"\n\tasJSON, err = json.Marshal(share)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"You are not authorized to share files/folders without a password\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUserAPIKey(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.AllowAPIKeyAuth = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tapiKey := dataprovider.APIKey{\n\t\tName:  \"testkey\",\n\t\tUser:  user.Username + \"1\",\n\t\tScope: dataprovider.APIKeyScopeUser,\n\t}\n\t_, _, err = httpdtest.AddAPIKey(apiKey, http.StatusBadRequest)\n\tassert.NoError(t, err)\n\tapiKey.User = user.Username\n\tapiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tadminAPIKey := dataprovider.APIKey{\n\t\tName:  \"testadminkey\",\n\t\tScope: dataprovider.APIKeyScopeAdmin,\n\t}\n\tadminAPIKey, _, err = httpdtest.AddAPIKey(adminAPIKey, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", \"filenametest\")\n\tassert.NoError(t, err)\n\t_, err = part.Write([]byte(\"test file content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar dirEntries []map[string]any\n\terr = json.Unmarshal(rr.Body.Bytes(), &dirEntries)\n\tassert.NoError(t, err)\n\tassert.Len(t, dirEntries, 1)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, adminAPIKey.Key, user.Username)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tuser.Status = 0\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\tuser.Status = 1\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolHTTP}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolFTP}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tapiKeyNew := dataprovider.APIKey{\n\t\tName:  apiKey.Name,\n\t\tScope: dataprovider.APIKeyScopeUser,\n\t}\n\n\tapiKeyNew, _, err = httpdtest.AddAPIKey(apiKeyNew, http.StatusCreated)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKeyNew.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\t// now associate a user\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKeyNew.Key, user.Username)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// now with a missing user\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKeyNew.Key, user.Username+\"1\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\t// empty user and key not associated to any user\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKeyNew.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tapiKeyNew.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour))\n\t_, _, err = httpdtest.UpdateAPIKey(apiKeyNew, http.StatusOK)\n\tassert.NoError(t, err)\n\t// expired API key\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKeyNew.Key, user.Username)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAPIKey(apiKeyNew, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAPIKey(adminAPIKey, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebClientExistenceCheck(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, webToken)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPost, webClientExistPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr) // no CSRF header\n\n\treq, err = http.NewRequest(http.MethodPost, webClientExistPath, bytes.NewBuffer([]byte(`[]`)))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tfilesToCheck := make(map[string]any)\n\tfilesToCheck[\"files\"] = nil\n\tasJSON, err := json.Marshal(filesToCheck)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientExistPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"files to be checked are mandatory\")\n\n\ttestFileName := \"file.dat\"\n\ttestDirName := \"adirname\"\n\tfilesToCheck[\"files\"] = []string{testFileName}\n\tasJSON, err = json.Marshal(filesToCheck)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientExistPath+\"?path=%2Fmissingdir\", bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientExistPath+\"?path=%2F\", bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar fileList []any\n\terr = json.Unmarshal(rr.Body.Bytes(), &fileList)\n\tassert.NoError(t, err)\n\tassert.Len(t, fileList, 0)\n\n\terr = createTestFile(filepath.Join(user.GetHomeDir(), testFileName), 100)\n\tassert.NoError(t, err)\n\terr = os.Mkdir(filepath.Join(user.GetHomeDir(), testDirName), 0755)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientExistPath+\"?path=%2F\", bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tfileList = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &fileList)\n\tassert.NoError(t, err)\n\tassert.Len(t, fileList, 1)\n\n\tfilesToCheck[\"files\"] = []string{testFileName, testDirName}\n\tasJSON, err = json.Marshal(filesToCheck)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientExistPath+\"?path=%2F\", bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tfileList = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &fileList)\n\tassert.NoError(t, err)\n\tassert.Len(t, fileList, 2)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientExistPath+\"?path=%2F\"+testDirName, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tfileList = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &fileList)\n\tassert.NoError(t, err)\n\tassert.Len(t, fileList, 0)\n\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolHTTP}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientExistPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebClientViewPDF(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webClientViewPDFPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientViewPDFPath+\"?path=test.pdf\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=test.pdf\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=%2F\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)\n\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), \"test.pdf\"), []byte(\"some text data\"), 0666)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=%2Ftest.pdf\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)\n\n\terr = createTestFile(filepath.Join(user.GetHomeDir(), \"test.pdf\"), 1024)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=%2Ftest.pdf\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)\n\n\tfakePDF := []byte(`%PDF-1.6`)\n\tfor i := 0; i < 128; i++ {\n\t\tfakePDF = append(fakePDF, []byte(fmt.Sprintf(\"%d\", i))...)\n\t}\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), \"test.pdf\"), fakePDF, 0666)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=%2Ftest.pdf\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           \"/\",\n\t\t\tDeniedPatterns: []string{\"*.pdf\"},\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=%2Ftest.pdf\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError403Message)\n\n\tuser.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           \"/\",\n\t\t\tDeniedPatterns: []string{\"*.txt\"},\n\t\t},\n\t}\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolHTTP}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=%2Ftest.pdf\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+\"?path=%2Ftest.pdf\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestWebEditFile(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFile1 := \"testfile1.txt\"\n\ttestFile2 := \"testfile2\"\n\tfile1Size := int64(65536)\n\tfile2Size := int64(1048576 * 5)\n\terr = createTestFile(filepath.Join(user.GetHomeDir(), testFile1), file1Size)\n\tassert.NoError(t, err)\n\terr = createTestFile(filepath.Join(user.GetHomeDir(), testFile2), file2Size)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=\"+testFile1, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=\"+testFile2, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorEditSize)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=missing\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=%2F\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorEditDir)\n\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolHTTP}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=\"+testFile1, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolFTP}\n\tuser.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           \"/\",\n\t\t\tDeniedPatterns: []string{\"*.txt\"},\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=\"+testFile1, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError403Message)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=\"+testFile1, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestWebGetFiles(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileName := \"testfile\"\n\ttestDir := \"testdir\"\n\ttestFileContents := []byte(\"file contents\")\n\terr = os.MkdirAll(filepath.Join(user.GetHomeDir(), testDir), os.ModePerm)\n\tassert.NoError(t, err)\n\textensions := []string{\"\", \".doc\", \".ppt\", \".xls\", \".pdf\", \".mkv\", \".png\", \".go\", \".zip\", \".txt\"}\n\tfor _, ext := range extensions {\n\t\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), testFileName+ext), testFileContents, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\terr = os.Symlink(filepath.Join(user.GetHomeDir(), testFileName+\".doc\"), filepath.Join(user.GetHomeDir(), testDir, testFileName+\".link\"))\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testDir, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientDirsPath+\"?path=\"+testDir, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar dirContents []map[string]any\n\terr = json.Unmarshal(rr.Body.Bytes(), &dirContents)\n\tassert.NoError(t, err)\n\tassert.Len(t, dirContents, 1)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientDirsPath+\"?dirtree=1&path=\"+testDir, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tdirContents = make([]map[string]any, 0)\n\terr = json.Unmarshal(rr.Body.Bytes(), &dirContents)\n\tassert.NoError(t, err)\n\tassert.Len(t, dirContents, 0)\n\n\treq, _ = http.NewRequest(http.MethodGet, userDirsPath+\"?path=\"+testDir, nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar dirEntries []map[string]any\n\terr = json.Unmarshal(rr.Body.Bytes(), &dirEntries)\n\tassert.NoError(t, err)\n\tassert.Len(t, dirEntries, 1)\n\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, webToken)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"files\", fmt.Sprintf(`[\"%s\",\"%s\",\"%s\"]`, testFileName, testDir, testFileName+extensions[2]))\n\treq, _ = http.NewRequest(http.MethodPost, webClientDownloadZipPath+\"?path=\"+url.QueryEscape(\"/\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t// add csrf token\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientDownloadZipPath+\"?path=\"+url.QueryEscape(\"/\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// parse form error\n\treq, _ = http.NewRequest(http.MethodPost, webClientDownloadZipPath+\"?path=p%C3%AO%GK\",\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tfilesList := []string{testFileName, testDir, testFileName + extensions[2]}\n\tasJSON, err := json.Marshal(filesList)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPost, userStreamZipPath, bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPost, userStreamZipPath, bytes.NewBuffer([]byte(`file`)))\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"files\", fmt.Sprintf(`[\"%v\"]`, testDir))\n\treq, _ = http.NewRequest(http.MethodPost, webClientDownloadZipPath+\"?path=\"+url.QueryEscape(\"/\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"files\", \"notalist\")\n\treq, _ = http.NewRequest(http.MethodPost, webClientDownloadZipPath+\"?path=\"+url.QueryEscape(\"/\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError400Message)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientDirsPath+\"?path=/\", nil) //nolint:goconst\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tdirContents = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &dirContents)\n\tassert.NoError(t, err)\n\tassert.Len(t, dirContents, len(extensions)+1)\n\n\treq, _ = http.NewRequest(http.MethodGet, userDirsPath+\"?path=/\", nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tdirEntries = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &dirEntries)\n\tassert.NoError(t, err)\n\tassert.Len(t, dirEntries, len(extensions)+1)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientDirsPath+\"?path=/missing\", nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorDirListGeneric)\n\n\treq, _ = http.NewRequest(http.MethodGet, userDirsPath+\"?path=missing\", nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to get directory lister\")\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, testFileContents, rr.Body.Bytes())\n\n\treq, _ = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, testFileContents, rr.Body.Bytes())\n\n\treq, _ = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\", nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Please set the path to a valid file\")\n\n\treq, _ = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testDir, nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"is a directory\")\n\n\treq, _ = http.NewRequest(http.MethodGet, userFilesPath+\"?path=notafile\", nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to stat the requested file\")\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"Range\", \"bytes=2-\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusPartialContent, rr)\n\tassert.Equal(t, testFileContents[2:], rr.Body.Bytes())\n\tlastModified, err := http.ParseTime(rr.Header().Get(\"Last-Modified\"))\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"Range\", \"bytes=2-\")\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusPartialContent, rr)\n\tassert.Equal(t, testFileContents[2:], rr.Body.Bytes())\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"Range\", \"bytes=-2\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusPartialContent, rr)\n\tassert.Equal(t, testFileContents[11:], rr.Body.Bytes())\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"Range\", \"bytes=-2,\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestedRangeNotSatisfiable, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"Range\", \"bytes=1a-\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestedRangeNotSatisfiable, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"Range\", \"bytes=2b-\")\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestedRangeNotSatisfiable, rr)\n\n\treq, _ = http.NewRequest(http.MethodHead, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"Range\", \"bytes=2-\")\n\treq.Header.Set(\"If-Range\", lastModified.UTC().Format(http.TimeFormat))\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusPartialContent, rr)\n\n\treq, _ = http.NewRequest(http.MethodHead, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"Range\", \"bytes=2-\")\n\treq.Header.Set(\"If-Range\", lastModified.UTC().Add(-120*time.Second).Format(http.TimeFormat))\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodHead, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"If-Modified-Since\", lastModified.UTC().Add(-120*time.Second).Format(http.TimeFormat))\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodHead, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"If-Modified-Since\", lastModified.UTC().Add(120*time.Second).Format(http.TimeFormat))\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotModified, rr)\n\n\treq, _ = http.NewRequest(http.MethodHead, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"If-Unmodified-Since\", lastModified.UTC().Add(-120*time.Second).Format(http.TimeFormat))\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusPreconditionFailed, rr)\n\n\treq, _ = http.NewRequest(http.MethodHead, userFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"If-Unmodified-Since\", lastModified.UTC().Add(-120*time.Second).Format(http.TimeFormat))\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusPreconditionFailed, rr)\n\n\treq, _ = http.NewRequest(http.MethodHead, webClientFilesPath+\"?path=\"+testFileName, nil)\n\treq.Header.Set(\"If-Unmodified-Since\", lastModified.UTC().Add(120*time.Second).Format(http.TimeFormat))\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolHTTP}\n\t_, resp, err := httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(resp))\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientDirsPath+\"?path=/\", nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, userDirsPath+\"?path=\"+testDir, nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tfilesList = []string{testDir}\n\tasJSON, err = json.Marshal(filesList)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPost, userStreamZipPath, bytes.NewBuffer(asJSON))\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolFTP}\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}\n\t_, resp, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(resp))\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tform = make(url.Values)\n\tform.Set(\"files\", `[]`)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientDownloadZipPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, userDirsPath+\"?path=\"+testDir, nil)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRenameDifferentResource(t *testing.T) {\n\tfolderName := \"foldercryptfs\"\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: filepath.Join(os.TempDir(), \"folderName\"),\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(\"super secret\"),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: \"/folderPath\",\n\t})\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFileName := \"file.txt\"\n\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, webToken)\n\tassert.NoError(t, err)\n\n\tgetStatusResponse := func(taskID string) int {\n\t\treq, _ := http.NewRequest(http.MethodGet, webClientTasksPath+\"/\"+url.PathEscape(taskID), nil)\n\t\treq.RemoteAddr = defaultRemoteAddr\n\t\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr := executeRequest(req)\n\t\tif rr.Code != http.StatusOK {\n\t\t\treturn -1\n\t\t}\n\t\tresp := make(map[string]any)\n\t\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\t\tif err != nil {\n\t\t\treturn -1\n\t\t}\n\t\treturn int(resp[\"status\"].(float64))\n\t}\n\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testFileName+\"&target=\"+url.QueryEscape(path.Join(\"/\", \"folderPath\", testFileName)), nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Cannot perform copy step\")\n\n\treq, err = http.NewRequest(http.MethodPost, webClientFileMovePath+\"?path=\"+testFileName+\"&target=\"+url.QueryEscape(path.Join(\"/\", \"folderPath\", testFileName)), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\ttaskResp := make(map[string]any)\n\terr = json.Unmarshal(rr.Body.Bytes(), &taskResp)\n\tassert.NoError(t, err)\n\ttaskID := taskResp[\"message\"].(string)\n\tassert.NotEmpty(t, taskID)\n\n\tassert.Eventually(t, func() bool {\n\t\tstatus := getStatusResponse(taskID)\n\t\treturn status == http.StatusNotFound\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), testFileName), []byte(\"just a test\"), os.ModePerm)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testFileName+\"&target=\"+url.QueryEscape(path.Join(\"/\", \"folderPath\", testFileName)), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// recreate the file and remove the delete permission\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), testFileName), []byte(\"just another test\"), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tu.Permissions = map[string][]string{\n\t\t\"/\": {dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermCreateDirs,\n\t\t\tdataprovider.PermDownload, dataprovider.PermOverwrite},\n\t}\n\t_, resp, err := httpdtest.UpdateUser(u, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(resp))\n\twebAPIToken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testFileName+\"&target=\"+url.QueryEscape(path.Join(\"/\", \"folderPath\", testFileName)), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Cannot perform copy step\")\n\n\tu.Permissions = map[string][]string{\n\t\t\"/\": {dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermCreateDirs,\n\t\t\tdataprovider.PermDownload, dataprovider.PermOverwrite, dataprovider.PermCopy},\n\t}\n\t_, resp, err = httpdtest.UpdateUser(u, http.StatusOK, \"\")\n\tassert.NoError(t, err, string(resp))\n\twebAPIToken, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testFileName+\"&target=\"+url.QueryEscape(path.Join(\"/\", \"folderPath\", testFileName)), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Cannot perform remove step\")\n\n\treq, err = http.NewRequest(http.MethodPost, webClientFileMovePath+\"?path=\"+testFileName+\"&target=\"+url.QueryEscape(path.Join(\"/\", \"folderPath\", testFileName)), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\ttaskResp = make(map[string]any)\n\terr = json.Unmarshal(rr.Body.Bytes(), &taskResp)\n\tassert.NoError(t, err)\n\ttaskID = taskResp[\"message\"].(string)\n\tassert.NotEmpty(t, taskID)\n\n\tassert.Eventually(t, func() bool {\n\t\tstatus := getStatusResponse(taskID)\n\t\treturn status == http.StatusForbidden\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebDirsAPI(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\ttestDir := \"testdir\"\n\n\treq, err := http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar contents []map[string]any\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 0)\n\n\t// rename a missing folder\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testDir+\"&target=\"+testDir+\"new\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// copy a missing folder\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/copy?path=\"+testDir+\"%2F&target=\"+testDir+\"new%2F\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// delete a missing folder\n\treq, err = http.NewRequest(http.MethodDelete, userDirsPath+\"?path=\"+testDir, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// create a dir\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=\"+testDir, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t// check the dir was created\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = nil\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tif assert.Len(t, contents, 1) {\n\t\tassert.Equal(t, testDir, contents[0][\"name\"])\n\t}\n\t// rename a dir with the same source and target name\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testDir+\"&target=\"+testDir, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"operation unsupported\")\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testDir+\"&target=%2F\"+testDir+\"%2F\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"operation unsupported\")\n\t// copy a dir with the same source and target name\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/copy?path=\"+testDir+\"&target=\"+testDir, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"operation unsupported\")\n\t// create a dir with missing parents\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=\"+url.QueryEscape(path.Join(\"/sub/dir\", testDir)), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// setting the mkdir_parents param will work\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?mkdir_parents=true&path=\"+url.QueryEscape(path.Join(\"/sub/dir\", testDir)), nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t// copy the dir\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/copy?path=\"+testDir+\"&target=\"+testDir+\"copy\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// rename the dir\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testDir+\"&target=\"+testDir+\"new\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// delete the dir\n\treq, err = http.NewRequest(http.MethodDelete, userDirsPath+\"?path=\"+testDir+\"new\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodDelete, userDirsPath+\"?path=\"+testDir+\"copy\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// the root dir cannot be created\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermListItems}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t// the user has no more the permission to create the directory\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=\"+testDir, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t// the user is deleted, any API call should fail\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=\"+testDir, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=\"+testDir+\"&target=\"+testDir+\"new\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/copy?path=\"+testDir+\"&target=\"+testDir+\"new\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, userDirsPath+\"?path=\"+testDir+\"new\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestWebUploadSingleFile(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tcontent := []byte(\"test content\")\n\n\treq, err := http.NewRequest(http.MethodPost, userUploadFilePath, bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"please set a file path\")\n\n\tmodTime := time.Now().Add(-24 * time.Hour)\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=file.txt\", bytes.NewBuffer(content)) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\treq.Header.Set(\"X-SFTPGO-MTIME\", strconv.FormatInt(util.GetTimeAsMsSinceEpoch(modTime), 10))\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tinfo, err := os.Stat(filepath.Join(user.GetHomeDir(), \"file.txt\"))\n\tif assert.NoError(t, err) {\n\t\tassert.InDelta(t, util.GetTimeAsMsSinceEpoch(modTime), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(1000))\n\t}\n\t// invalid modification time will be ignored\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=file.txt\", bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\treq.Header.Set(\"X-SFTPGO-MTIME\", \"123abc\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tinfo, err = os.Stat(filepath.Join(user.GetHomeDir(), \"file.txt\"))\n\tif assert.NoError(t, err) {\n\t\tassert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000))\n\t}\n\t// upload to a missing dir will fail without the mkdir_parents param\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=\"+url.QueryEscape(\"/subdir/file.txt\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?mkdir_parents=true&path=\"+url.QueryEscape(\"/subdir/file.txt\"), bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tmetadataReq := make(map[string]int64)\n\tmetadataReq[\"modification_time\"] = util.GetTimeAsMsSinceEpoch(modTime)\n\tasJSON, err := json.Marshal(metadataReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+\"?path=file.txt\", bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tinfo, err = os.Stat(filepath.Join(user.GetHomeDir(), \"file.txt\"))\n\tif assert.NoError(t, err) {\n\t\tassert.InDelta(t, util.GetTimeAsMsSinceEpoch(modTime), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(1000))\n\t}\n\t// missing file\n\treq, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+\"?path=file2.txt\", bytes.NewBuffer(asJSON)) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to set metadata for path\")\n\t// invalid JSON\n\treq, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+\"?path=file.txt\", bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// missing mandatory parameter\n\treq, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"please set a modification_time and a path\")\n\n\tmetadataReq = make(map[string]int64)\n\tasJSON, err = json.Marshal(metadataReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+\"?path=file.txt\", bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"please set a modification_time and a path\")\n\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=%2Fdir%2Ffile.txt\", bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to write file\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=file.txt\", bytes.NewBuffer(content))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to retrieve your user\")\n\n\tmetadataReq[\"modification_time\"] = util.GetTimeAsMsSinceEpoch(modTime)\n\tasJSON, err = json.Marshal(metadataReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+\"?path=file.txt\", bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to retrieve your user\")\n}\n\nfunc TestWebFilesAPI(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart1, err := writer.CreateFormFile(\"filenames\", \"file1.txt\")\n\tassert.NoError(t, err)\n\t_, err = part1.Write([]byte(\"file1 content\"))\n\tassert.NoError(t, err)\n\tpart2, err := writer.CreateFormFile(\"filenames\", \"file2.txt\")\n\tassert.NoError(t, err)\n\t_, err = part2.Write([]byte(\"file2 content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to parse multipart form\")\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), user.FirstUpload)\n\tassert.Equal(t, int64(0), user.FirstDownload)\n\t// set the proper content type\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, user.FirstUpload, int64(0))\n\tassert.Equal(t, int64(0), user.FirstDownload)\n\t// check we have 2 files\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar contents []map[string]any\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 2)\n\t// download a file\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=file1.txt\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"file1 content\", rr.Body.String())\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, user.FirstUpload, int64(0))\n\tassert.Greater(t, user.FirstDownload, int64(0))\n\t// overwrite the existing files\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = nil\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 2)\n\t// now create a dir and upload to that dir\n\ttestDir := \"tdir\"\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=\"+testDir, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?path=\"+testDir, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t// upload to a missing subdir will fail without the mkdir_parents param\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?path=\"+url.QueryEscape(\"/sub/\"+testDir), reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?mkdir_parents=true&path=\"+url.QueryEscape(\"/sub/\"+testDir), reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = nil\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 4)\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath+\"?path=\"+testDir, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = nil\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 2)\n\t// copy a file\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/copy?path=file1.txt&target=%2Ftdir%2Ffile_copy.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// rename a file\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?target=%2Ftdir%2Ffile3.txt&path=file1.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// rename a missing file\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=file1.txt&target=%2Ftdir%2Ffile3.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// rename a file with target name equal to source name\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=file1.txt&target=file1.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"operation unsupported\")\n\t// delete a file\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=file2.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// delete a missing file\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=file2.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// delete a directory\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=tdir\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// make a symlink outside the home dir and then try to delete it\n\textPath := filepath.Join(os.TempDir(), \"file\")\n\terr = os.WriteFile(extPath, []byte(\"contents\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.Symlink(extPath, filepath.Join(user.GetHomeDir(), \"file\"))\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=file\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\terr = os.Remove(extPath)\n\tassert.NoError(t, err)\n\t// remove delete and overwrite permissions\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?path=tdir\", reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=%2Ftdir%2Ffile1.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t// the user is deleted, any API call should fail\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=file1.txt&target=%2Ftdir%2Ffile3.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=file2.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestBufferedWebFilesAPI(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.OSConfig = sdk.OSFsConfig{\n\t\tReadBufferSize:  1,\n\t\tWriteBufferSize: 1,\n\t}\n\tvdirPath := \"/crypted\"\n\tmappedPath := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tfolderName := filepath.Base(mappedPath)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tOSFsConfig: sdk.OSFsConfig{\n\t\t\t\t\tWriteBufferSize: 3,\n\t\t\t\t\tReadBufferSize:  2,\n\t\t\t\t},\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart1, err := writer.CreateFormFile(\"filenames\", \"file1.txt\")\n\tassert.NoError(t, err)\n\t_, err = part1.Write([]byte(\"file1 content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?path=\"+url.QueryEscape(vdirPath), reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=file1.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"file1 content\", rr.Body.String())\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+url.QueryEscape(vdirPath+\"/file1.txt\"), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, \"file1 content\", rr.Body.String())\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=file1.txt\", nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Range\", \"bytes=2-\")\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusPartialContent, rr)\n\tassert.Equal(t, \"le1 content\", rr.Body.String())\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+url.QueryEscape(vdirPath+\"/file1.txt\"), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Range\", \"bytes=3-6\")\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusPartialContent, rr)\n\tassert.Equal(t, \"e1 c\", rr.Body.String())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebClientTasksAPI(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu1 := getTestUser()\n\tu1.Username = xid.New().String()\n\tuser1, _, err := httpdtest.AddUser(u1, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestDir := \"subdir\"\n\ttestFileData := []byte(\"data\")\n\ttestFilePath := filepath.Join(user.GetHomeDir(), testDir, \"file.txt\")\n\ttestFileName := filepath.Base(testFilePath)\n\terr = os.MkdirAll(filepath.Dir(testFilePath), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(testFilePath, testFileData, 0666)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, webToken)\n\tassert.NoError(t, err)\n\twebToken1, err := getJWTWebClientTokenFromTestServer(user1.Username, defaultPassword)\n\tassert.NoError(t, err)\n\n\tgetStatusResponse := func(taskID string) int {\n\t\treq, _ := http.NewRequest(http.MethodGet, webClientTasksPath+\"/\"+url.PathEscape(taskID), nil)\n\t\treq.RemoteAddr = defaultRemoteAddr\n\t\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr := executeRequest(req)\n\t\tif rr.Code != http.StatusOK {\n\t\t\treturn -1\n\t\t}\n\t\tresp := make(map[string]any)\n\t\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\t\tif err != nil {\n\t\t\treturn -1\n\t\t}\n\t\treturn int(resp[\"status\"].(float64))\n\t}\n\t// missing task\n\tassert.Equal(t, -1, getStatusResponse(\"missing\"))\n\n\treq, err := http.NewRequest(http.MethodPost, webClientFileCopyPath+\"?path=\"+\n\t\turl.QueryEscape(path.Join(testDir, testFileName))+\"&target=\"+url.QueryEscape(testFileName), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\tresp := make(map[string]any)\n\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\tassert.NoError(t, err)\n\ttaskID := resp[\"message\"].(string)\n\tassert.NotEmpty(t, taskID)\n\n\tassert.Eventually(t, func() bool {\n\t\tstatus := getStatusResponse(taskID)\n\t\treturn status == http.StatusOK\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\n\t// cannot get the task with a different user\n\treq, err = http.NewRequest(http.MethodGet, webClientTasksPath+\"/\"+url.PathEscape(taskID), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken1)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientFileMovePath+\"?path=\"+\n\t\turl.QueryEscape(path.Join(testDir, testFileName))+\"&target=\"+url.QueryEscape(testFileName), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\tresp = make(map[string]any)\n\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\tassert.NoError(t, err)\n\ttaskID = resp[\"message\"].(string)\n\tassert.NotEmpty(t, taskID)\n\n\tassert.Eventually(t, func() bool {\n\t\tstatus := getStatusResponse(taskID)\n\t\treturn status == http.StatusOK\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\n\treq, err = http.NewRequest(http.MethodDelete, webClientDirsPath+\"?path=\"+\n\t\turl.QueryEscape(testDir), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\tresp = make(map[string]any)\n\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\tassert.NoError(t, err)\n\ttaskID = resp[\"message\"].(string)\n\tassert.NotEmpty(t, taskID)\n\n\tassert.Eventually(t, func() bool {\n\t\tstatus := getStatusResponse(taskID)\n\t\treturn status == http.StatusOK\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\n\treq, err = http.NewRequest(http.MethodDelete, webClientDirsPath+\"?path=\"+\n\t\turl.QueryEscape(testDir), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\tresp = make(map[string]any)\n\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\tassert.NoError(t, err)\n\ttaskID = resp[\"message\"].(string)\n\tassert.NotEmpty(t, taskID)\n\n\tassert.Eventually(t, func() bool {\n\t\tstatus := getStatusResponse(taskID)\n\t\treturn status == http.StatusNotFound\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientFileMovePath+\"?path=\"+\n\t\turl.QueryEscape(path.Join(testDir, testFileName))+\"&target=\"+url.QueryEscape(testFileName), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\tresp = make(map[string]any)\n\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\tassert.NoError(t, err)\n\ttaskID = resp[\"message\"].(string)\n\tassert.NotEmpty(t, taskID)\n\n\tassert.Eventually(t, func() bool {\n\t\tstatus := getStatusResponse(taskID)\n\t\treturn status == http.StatusNotFound\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientFileCopyPath+\"?path=\"+\n\t\turl.QueryEscape(path.Join(testDir, testFileName)+\"/\")+\"&target=\"+url.QueryEscape(testFileName+\"/\"), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusAccepted, rr)\n\tresp = make(map[string]any)\n\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\tassert.NoError(t, err)\n\ttaskID = resp[\"message\"].(string)\n\tassert.NotEmpty(t, taskID)\n\n\tassert.Eventually(t, func() bool {\n\t\tstatus := getStatusResponse(taskID)\n\t\treturn status == http.StatusNotFound\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n\t// user deleted\n\treq, err = http.NewRequest(http.MethodDelete, webClientDirsPath+\"?path=\"+\n\t\turl.QueryEscape(testDir), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientFileMovePath+\"?path=\"+\n\t\turl.QueryEscape(path.Join(testDir, testFileName))+\"&target=\"+url.QueryEscape(testFileName), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientFileCopyPath+\"?path=\"+\n\t\turl.QueryEscape(path.Join(testDir, testFileName))+\"&target=\"+url.QueryEscape(testFileName), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestStartDirectory(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.StartDirectory = \"/start/dir\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tfilename := \"file1.txt\"\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart1, err := writer.CreateFormFile(\"filenames\", filename)\n\tassert.NoError(t, err)\n\t_, err = part1.Write([]byte(\"test content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t// check we have 2 files in the defined start dir\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar contents []map[string]any\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tif assert.Len(t, contents, 1) {\n\t\tassert.Equal(t, filename, contents[0][\"name\"].(string))\n\t}\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=file2.txt\",\n\t\tbytes.NewBuffer([]byte(\"single upload content\")))\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=testdir\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=testdir&target=testdir1\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=%2Ftestdirroot\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath+\"?path=\"+url.QueryEscape(u.Filters.StartDirectory), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = nil\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 3)\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+filename, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=%2F\"+filename, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPatch, userFilesPath+\"?path=\"+filename+\"&target=\"+filename+\"_rename\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, userDirsPath+\"?path=testdir1\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = nil\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 2)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = nil\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 2)\n\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=\"+filename+\"_rename\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath+\"?path=\"+url.QueryEscape(u.Filters.StartDirectory), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcontents = nil\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 1)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebFilesTransferQuotaLimits(t *testing.T) {\n\tu := getTestUser()\n\tu.UploadDataTransfer = 1\n\tu.DownloadDataTransfer = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\ttestFileName := \"file.data\"\n\ttestFileSize := 550000\n\ttestFileContents := make([]byte, testFileSize)\n\tn, err := io.ReadFull(rand.Reader, testFileContents)\n\tassert.NoError(t, err)\n\tassert.Equal(t, testFileSize, n)\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", testFileName)\n\tassert.NoError(t, err)\n\t_, err = part.Write(testFileContents)\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, testFileContents, rr.Body.Bytes())\n\t// error while download is active\n\tdownloadFunc := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\n\t\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, webAPIToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t}\n\tdownloadFunc()\n\t// error before starting the download\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=\"+testFileName, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t// error while upload is active\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestEntityTooLarge, rr)\n\t// error before starting the upload\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestEntityTooLarge, rr)\n\t// now test upload/download to/from shares\n\tshare1 := dataprovider.Share{\n\t\tName:  \"share1\",\n\t\tScope: dataprovider.ShareScopeRead,\n\t\tPaths: []string{\"/\"},\n\t}\n\tasJSON, err := json.Marshal(share1)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\treq, err = http.NewRequest(http.MethodGet, sharesPath+\"/\"+objectID, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tform := make(url.Values)\n\tform.Set(\"files\", `[]`)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, \"/partial\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorQuotaRead)\n\n\tshare2 := dataprovider.Share{\n\t\tName:  \"share2\",\n\t\tScope: dataprovider.ShareScopeWrite,\n\t\tPaths: []string{\"/\"},\n\t}\n\tasJSON, err = json.Marshal(share2)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tobjectID = rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, sharesPath+\"/\"+objectID, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestEntityTooLarge, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUploadErrors(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaSize = 65535\n\tsubDir1 := \"sub1\"\n\tsubDir2 := \"sub2\"\n\tu.Permissions[path.Join(\"/\", subDir1)] = []string{dataprovider.PermListItems}\n\tu.Permissions[path.Join(\"/\", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload,\n\t\tdataprovider.PermDelete}\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/sub2\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{\"*.zip\"},\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", \"file.zip\")\n\tassert.NoError(t, err)\n\t_, err = part.Write([]byte(\"file content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\t// zip file are not allowed within sub2\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath+\"?path=sub2\", reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\t// we have no upload permissions within sub1\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?path=sub1\", reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t// we cannot create dirs in sub2\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?mkdir_parents=true&path=\"+url.QueryEscape(\"/sub2/dir\"), reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"unable to check/create missing parent dir\")\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?mkdir_parents=true&path=\"+url.QueryEscape(\"/sub2/dir/test\"), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Error checking parent directories\")\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?mkdir_parents=true&path=\"+url.QueryEscape(\"/sub2/dir1/file.txt\"), bytes.NewBuffer([]byte(\"\")))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Error checking parent directories\")\n\t// create a dir and try to overwrite it with a file\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=file.zip\", nil) //nolint:goconst\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"operation unsupported\")\n\t// try to upload to a missing parent directory\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?path=missingdir\", reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, userDirsPath+\"?path=file.zip\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// upload will work now\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\t// overwrite the file\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tvfs.SetTempPath(filepath.Join(os.TempDir(), \"missingpath\"))\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tif runtime.GOOS != osWindows {\n\t\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=file.zip\", nil)\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, webAPIToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t\tvfs.SetTempPath(filepath.Clean(os.TempDir()))\n\t\terr = os.Chmod(user.GetHomeDir(), 0555)\n\t\tassert.NoError(t, err)\n\n\t\t_, err = reader.Seek(0, io.SeekStart)\n\t\tassert.NoError(t, err)\n\t\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\t\tassert.NoError(t, err)\n\t\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\t\tsetBearerForReq(req, webAPIToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t\tassert.Contains(t, rr.Body.String(), \"Error closing file\")\n\n\t\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=file.zip\", bytes.NewBuffer(nil))\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, webAPIToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t\tassert.Contains(t, rr.Body.String(), \"Error closing file\")\n\n\t\terr = os.Chmod(user.GetHomeDir(), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\n\tvfs.SetTempPath(\"\")\n\n\t// upload a multipart form with no files\n\tbody = new(bytes.Buffer)\n\twriter = multipart.NewWriter(body)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader = bytes.NewReader(body.Bytes())\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?path=sub2\", reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"No files uploaded!\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebAPIVFolder(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaSize = 65535\n\tvdir := \"/vdir\"\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tfolderName := filepath.Base(mappedPath)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdir,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(user.Username, defaultPassword)\n\tassert.NoError(t, err)\n\n\tfileContents := []byte(\"test contents\")\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", \"file.txt\")\n\tassert.NoError(t, err)\n\t_, err = part.Write(fileContents)\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath+\"?path=vdir\", reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(len(fileContents)), user.UsedQuotaSize)\n\n\tfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), folder.UsedQuotaSize)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath+\"?path=vdir\", reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(len(fileContents)), user.UsedQuotaSize)\n\n\tfolder, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), folder.UsedQuotaSize)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebAPIWritePermission(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.WebClient = append(u.Filters.WebClient, sdk.WebClientWriteDisabled)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", \"file.txt\")\n\tassert.NoError(t, err)\n\t_, err = part.Write([]byte(\"\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=a&target=b\", nil)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=a\", nil)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userFilesPath+\"?path=a.txt\", nil)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userDirsPath+\"?path=dir\", nil)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, userFileActionsPath+\"/move?path=dir&target=dir1\", nil)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, userDirsPath+\"?path=dir\", nil)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebAPICryptFs(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaSize = 65535\n\tu.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tu.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(defaultPassword)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", \"file.txt\")\n\tassert.NoError(t, err)\n\t_, err = part.Write([]byte(\"content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUploadSFTP(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaFiles = 100\n\tu.FsConfig.SFTPConfig.BufferSize = 2\n\tu.HomeDir = filepath.Join(os.TempDir(), u.Username)\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(sftpUser.Username, defaultPassword)\n\tassert.NoError(t, err)\n\n\tbody := new(bytes.Buffer)\n\twriter := multipart.NewWriter(body)\n\tpart, err := writer.CreateFormFile(\"filenames\", \"file.txt\")\n\tassert.NoError(t, err)\n\t_, err = part.Write([]byte(\"test file content\"))\n\tassert.NoError(t, err)\n\terr = writer.Close()\n\tassert.NoError(t, err)\n\treader := bytes.NewReader(body.Bytes())\n\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\texpectedQuotaSize := int64(17)\n\texpectedQuotaFiles := 1\n\tuser, _, err := httpdtest.GetUserByUsername(sftpUser.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\n\tuser.QuotaSize = 10\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t// we are now overquota on overwrite\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestEntityTooLarge, rr)\n\tassert.Contains(t, rr.Body.String(), \"denying write due to space limit\")\n\tassert.Contains(t, rr.Body.String(), \"Unable to write file\")\n\n\t// delete the file\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=file.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(sftpUser.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\n\treq, err = http.NewRequest(http.MethodPost, userUploadFilePath+\"?path=file.txt\",\n\t\tbytes.NewBuffer([]byte(\"test upload single file content\")))\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestEntityTooLarge, rr)\n\tassert.Contains(t, rr.Body.String(), \"denying write due to space limit\")\n\tassert.Contains(t, rr.Body.String(), \"Error saving file\")\n\n\t// delete the file\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=file.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = reader.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, userFilesPath, reader)\n\tassert.NoError(t, err)\n\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusRequestEntityTooLarge, rr)\n\tassert.Contains(t, rr.Body.String(), \"denying write due to space limit\")\n\tassert.Contains(t, rr.Body.String(), \"Error saving file\")\n\n\t// delete the file\n\treq, err = http.NewRequest(http.MethodDelete, userFilesPath+\"?path=file.txt\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, webAPIToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(sftpUser.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(sftpUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebAPISFTPPasswordProtectedPrivateKey(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tu.PublicKeys = []string{testPubKeyPwd}\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()\n\tu.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(testPrivateKeyPwd)\n\tu.FsConfig.SFTPConfig.KeyPassphrase = kms.NewPlainSecret(privateKeyPwd)\n\tu.HomeDir = filepath.Join(os.TempDir(), u.Username)\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(sftpUser.Username, defaultPassword)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// update the user, the key must be preserved\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, sftpUser.FsConfig.SFTPConfig.KeyPassphrase.GetStatus())\n\t_, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// using a wrong passphrase or no passphrase should fail\n\tsftpUser.FsConfig.SFTPConfig.KeyPassphrase = kms.NewPlainSecret(\"wrong\")\n\t_, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = getJWTWebClientTokenFromTestServer(sftpUser.Username, defaultPassword)\n\tassert.Error(t, err)\n\tsftpUser.FsConfig.SFTPConfig.KeyPassphrase = kms.NewEmptySecret()\n\t_, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = getJWTWebClientTokenFromTestServer(sftpUser.Username, defaultPassword)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(sftpUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUploadMultipartFormReadError(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPost, userFilesPath, nil)\n\tassert.NoError(t, err)\n\n\tmpartForm := &multipart.Form{\n\t\tFile: make(map[string][]*multipart.FileHeader),\n\t}\n\tmpartForm.File[\"filenames\"] = append(mpartForm.File[\"filenames\"], &multipart.FileHeader{Filename: \"missing\"})\n\treq.MultipartForm = mpartForm\n\treq.Header.Add(\"Content-Type\", \"multipart/form-data\")\n\tsetBearerForReq(req, webAPIToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tassert.Contains(t, rr.Body.String(), \"Unable to read uploaded file\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCompressionErrorMock(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tdefer func() {\n\t\trcv := recover()\n\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t_, err := httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t}()\n\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, webToken)\n\tassert.NoError(t, err)\n\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"files\", `[\"missing\"]`)\n\treq, _ := http.NewRequest(http.MethodPost, webClientDownloadZipPath+\"?path=%2F\",\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\texecuteRequest(req)\n}\n\nfunc TestGetFilesSFTPBackend(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestSFTPUser()\n\tu.HomeDir = filepath.Clean(os.TempDir())\n\tu.FsConfig.SFTPConfig.BufferSize = 2\n\tu.Permissions[\"/adir\"] = nil\n\tu.Permissions[\"/adir1\"] = []string{dataprovider.PermListItems}\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           \"/adir2\",\n\t\t\tDeniedPatterns: []string{\"*.txt\"},\n\t\t},\n\t}\n\tsftpUserBuffered, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.Username += \"_unbuffered\"\n\tu.FsConfig.SFTPConfig.BufferSize = 0\n\tsftpUserUnbuffered, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFileName := \"testsftpfile\"\n\ttestDir := \"testsftpdir\"\n\ttestFileContents := []byte(\"sftp file contents\")\n\terr = os.MkdirAll(filepath.Join(user.GetHomeDir(), testDir, \"sub\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(filepath.Join(user.GetHomeDir(), \"adir1\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(filepath.Join(user.GetHomeDir(), \"adir2\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), testFileName), testFileContents, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), \"adir1\", \"afile\"), testFileContents, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(filepath.Join(user.GetHomeDir(), \"adir2\", \"afile.txt\"), testFileContents, os.ModePerm)\n\tassert.NoError(t, err)\n\tfor _, sftpUser := range []dataprovider.User{sftpUserBuffered, sftpUserUnbuffered} {\n\t\twebToken, err := getJWTWebClientTokenFromTestServer(sftpUser.Username, defaultPassword)\n\t\tassert.NoError(t, err)\n\t\treq, _ := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+path.Join(testDir, \"sub\"), nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t\thtmlErrFrag := `div id=\"errorMsg\" class=\"rounded border-warning border border-dashed bg-light-warning`\n\t\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+path.Join(testDir, \"missing\"), nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\tassert.Contains(t, rr.Body.String(), htmlErrFrag)\n\t\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=adir/sub\", nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\tassert.Contains(t, rr.Body.String(), htmlErrFrag)\n\n\t\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=adir1/afile\", nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\tassert.Contains(t, rr.Body.String(), htmlErrFrag)\n\n\t\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=adir2/afile.txt\", nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\tassert.Contains(t, rr.Body.String(), htmlErrFrag)\n\n\t\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\tassert.Equal(t, testFileContents, rr.Body.Bytes())\n\n\t\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\t\treq.Header.Set(\"Range\", \"bytes=2-\")\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusPartialContent, rr)\n\t\tassert.Equal(t, testFileContents[2:], rr.Body.Bytes())\n\n\t\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestClientUserClose(t *testing.T) {\n\tu := getTestUser()\n\tu.UploadBandwidth = 32\n\tu.DownloadBandwidth = 32\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileName := \"file.dat\"\n\ttestFileSize := int64(524288)\n\ttestFilePath := filepath.Join(user.GetHomeDir(), testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tuploadContent := make([]byte, testFileSize)\n\t_, err = rand.Read(uploadContent)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\twebAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, _ := http.NewRequest(http.MethodGet, webClientFilesPath+\"?path=\"+testFileName, nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t}()\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\treq, _ := http.NewRequest(http.MethodGet, webClientEditFilePath+\"?path=\"+testFileName, nil)\n\t\tsetJWTCookieForReq(req, webToken)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\t\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\t}()\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tbody := new(bytes.Buffer)\n\t\twriter := multipart.NewWriter(body)\n\t\tpart, err := writer.CreateFormFile(\"filenames\", \"upload.dat\")\n\t\tassert.NoError(t, err)\n\t\tn, err := part.Write(uploadContent)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, int64(n))\n\t\terr = writer.Close()\n\t\tassert.NoError(t, err)\n\t\treader := bytes.NewReader(body.Bytes())\n\t\treq, err := http.NewRequest(http.MethodPost, userFilesPath, reader)\n\t\tassert.NoError(t, err)\n\t\treq.Header.Add(\"Content-Type\", writer.FormDataContentType())\n\t\tsetBearerForReq(req, webAPIToken)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t\tassert.Contains(t, rr.Body.String(), \"transfer aborted\")\n\t}()\n\t// wait for the transfers\n\tassert.Eventually(t, func() bool {\n\t\tstats := common.Connections.GetStats(\"\")\n\t\tif len(stats) == 3 {\n\t\t\tif len(stats[0].Transfers) > 0 && len(stats[1].Transfers) > 0 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}, 1*time.Second, 50*time.Millisecond)\n\n\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\t// close all the active transfers\n\t\tcommon.Connections.Close(stat.ConnectionID, \"\")\n\t}\n\twg.Wait()\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWebAdminSetupMock(t *testing.T) {\n\treq, err := http.NewRequest(http.MethodGet, webAdminSetupPath, nil)\n\tassert.NoError(t, err)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\t// now delete all the admins\n\tadmins, err := dataprovider.GetAdmins(100, 0, dataprovider.OrderASC)\n\tassert.NoError(t, err)\n\tfor _, admin := range admins {\n\t\terr = dataprovider.DeleteAdmin(admin.Username, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\t// close the provider and initializes it without creating the default admin\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN\", \"0\")\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t// now the setup page must be rendered\n\treq, err = http.NewRequest(http.MethodGet, webAdminSetupPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// check redirects to the setup page\n\treq, err = http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webAdminSetupPath, rr.Header().Get(\"Location\"))\n\treq, err = http.NewRequest(http.MethodGet, webBasePath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webAdminSetupPath, rr.Header().Get(\"Location\"))\n\treq, err = http.NewRequest(http.MethodGet, webBasePathAdmin, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webAdminSetupPath, rr.Header().Get(\"Location\"))\n\treq, err = http.NewRequest(http.MethodGet, webLoginPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webAdminSetupPath, rr.Header().Get(\"Location\"))\n\treq, err = http.NewRequest(http.MethodGet, webClientLoginPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webAdminSetupPath, rr.Header().Get(\"Location\"))\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webAdminSetupPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\treq, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"username\", defaultTokenAuthUser)\n\treq, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"password\", defaultTokenAuthPass)\n\treq, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdNoMatch)\n\tform.Set(\"confirm_password\", defaultTokenAuthPass)\n\t// test a parse form error\n\treq, err = http.NewRequest(http.MethodPost, webAdminSetupPath+\"?param=p%C3%AO%GH\", bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test a dataprovider error\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// finally initialize the provider and create the default admin\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webAdminMFAPath, rr.Header().Get(\"Location\"))\n\t// if we resubmit the form we get a bad request, an admin already exists\n\treq, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError400Message)\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN\", \"1\")\n}\n\nfunc TestAllowList(t *testing.T) {\n\tconfigCopy := common.Config\n\n\tentries := []dataprovider.IPListEntry{\n\t\t{\n\t\t\tIPOrNet:   \"172.120.1.1/32\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 0,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"172.120.1.2/32\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 0,\n\t\t},\n\t\t{\n\t\t\tIPOrNet:   \"192.8.7.0/22\",\n\t\t\tType:      dataprovider.IPListTypeAllowList,\n\t\t\tMode:      dataprovider.ListModeAllow,\n\t\t\tProtocols: 8,\n\t\t},\n\t}\n\n\tfor _, e := range entries {\n\t\t_, _, err := httpdtest.AddIPListEntry(e, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\t}\n\n\tcommon.Config.MaxTotalConnections = 1\n\tcommon.Config.AllowListStatus = 1\n\terr := common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n\n\treq, _ := http.NewRequest(http.MethodGet, webLoginPath, nil)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), common.ErrConnectionDenied.Error())\n\n\treq.RemoteAddr = \"172.120.1.1\"\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\ttestIP := \"172.120.1.3\"\n\treq.RemoteAddr = testIP\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), common.ErrConnectionDenied.Error())\n\n\treq.RemoteAddr = \"192.8.7.1\"\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tentry := dataprovider.IPListEntry{\n\t\tIPOrNet:   \"172.120.1.3/32\",\n\t\tType:      dataprovider.IPListTypeAllowList,\n\t\tMode:      dataprovider.ListModeAllow,\n\t\tProtocols: 8,\n\t}\n\terr = dataprovider.AddIPListEntry(&entry, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\treq.RemoteAddr = testIP\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\terr = dataprovider.DeleteIPListEntry(entry.IPOrNet, entry.Type, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\treq.RemoteAddr = testIP\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), common.ErrConnectionDenied.Error())\n\n\tcommon.Config = configCopy\n\terr = common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n\n\tfor _, e := range entries {\n\t\t_, err := httpdtest.RemoveIPListEntry(e, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestWebAdminLoginMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\n\treq, _ := http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath+\"notfound\", nil)\n\treq.RequestURI = webStatusPath + \"notfound\"\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webLogoutPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tcookie := rr.Header().Get(\"Cookie\")\n\tassert.Empty(t, cookie)\n\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, logoutPath, nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, serverStatusPath, nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"Your token is no longer valid\")\n\n\treq, _ = http.NewRequest(http.MethodGet, webStatusPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\t// now try using wrong password\n\tform := getLoginForm(defaultTokenAuthUser, \"wrong pwd\", csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\t// wrong username\n\tform = getLoginForm(\"wrong username\", defaultTokenAuthPass, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\t// try from an ip not allowed\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\ta.Filters.AllowList = []string{\"10.0.0.0/8\"}\n\n\t_, _, err = httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\trAddr := \"127.1.1.1:1234\"\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, rAddr)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, loginCookie)\n\tform = getLoginForm(altAdminUsername, altAdminPassword, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.RemoteAddr = rAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\trAddr = \"10.9.9.9:1234\"\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, rAddr)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, loginCookie)\n\tform = getLoginForm(altAdminUsername, altAdminPassword, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.RemoteAddr = rAddr\n\tsetLoginCookie(req, loginCookie)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\n\trAddr = \"127.0.1.1:4567\"\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, rAddr)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, loginCookie)\n\tform = getLoginForm(altAdminUsername, altAdminPassword, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.RemoteAddr = rAddr\n\treq.Header.Set(\"X-Forwarded-For\", \"10.9.9.9\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\t// invalid csrf token\n\tform = getLoginForm(altAdminUsername, altAdminPassword, \"invalid csrf\")\n\treq, _ = http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.RemoteAddr = \"10.9.9.8:1234\"\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\treq, _ = http.NewRequest(http.MethodGet, webLoginPath, nil)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = httpdtest.RemoveAdmin(a, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminNoToken(t *testing.T) {\n\treq, _ := http.NewRequest(http.MethodGet, webAdminProfilePath, nil)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\n\treq, _ = http.NewRequest(http.MethodGet, webUserPath, nil)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\n\treq, _ = http.NewRequest(http.MethodGet, userPath, nil)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, activeConnectionsPath, nil)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n}\n\nfunc TestWebUserShare(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, token)\n\tassert.NoError(t, err)\n\tuserAPItoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:        \"test share\",\n\t\tDescription: \"test share desc\",\n\t\tScope:       dataprovider.ShareScopeRead,\n\t\tPaths:       []string{\"/\"},\n\t\tExpiresAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour)),\n\t\tMaxTokens:   100,\n\t\tAllowFrom:   []string{\"127.0.0.0/8\", \"172.16.0.0/16\"},\n\t\tPassword:    defaultPassword,\n\t}\n\tform := make(url.Values)\n\tform.Set(\"name\", share.Name)\n\tform.Set(\"scope\", strconv.Itoa(int(share.Scope)))\n\tform.Set(\"paths[0][path]\", \"/\")\n\tform.Set(\"max_tokens\", strconv.Itoa(share.MaxTokens))\n\tform.Set(\"allowed_ip\", strings.Join(share.AllowFrom, \",\"))\n\tform.Set(\"description\", share.Description)\n\tform.Set(\"password\", share.Password)\n\tform.Set(\"expiration_date\", \"123\")\n\t// invalid expiration date\n\treq, err := http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareExpiration)\n\tform.Set(\"expiration_date\", util.GetTimeFromMsecSinceEpoch(share.ExpiresAt).UTC().Format(\"2006-01-02 15:04:05\"))\n\tform.Set(\"scope\", \"\")\n\t// invalid scope\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareScope)\n\tform.Set(\"scope\", strconv.Itoa(int(share.Scope)))\n\t// invalid max tokens\n\tform.Set(\"max_tokens\", \"t\")\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareMaxTokens)\n\tform.Set(\"max_tokens\", strconv.Itoa(share.MaxTokens))\n\t// no csrf token\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"scope\", \"100\")\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareScope)\n\n\tform.Set(\"scope\", strconv.Itoa(int(share.Scope)))\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userSharesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userAPItoken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar shares []dataprovider.Share\n\terr = json.Unmarshal(rr.Body.Bytes(), &shares)\n\tassert.NoError(t, err)\n\tif assert.Len(t, shares, 1) {\n\t\ts := shares[0]\n\t\tassert.Equal(t, share.Name, s.Name)\n\t\tassert.Equal(t, share.Description, s.Description)\n\t\tassert.Equal(t, share.Scope, s.Scope)\n\t\tassert.Equal(t, share.Paths, s.Paths)\n\t\tassert.InDelta(t, share.ExpiresAt, s.ExpiresAt, 999)\n\t\tassert.Equal(t, share.MaxTokens, s.MaxTokens)\n\t\tassert.Equal(t, share.AllowFrom, s.AllowFrom)\n\t\tassert.Equal(t, redactedSecret, s.Password)\n\t\tshare.ShareID = s.ShareID\n\t}\n\tform.Set(\"password\", redactedSecret)\n\tform.Set(\"expiration_date\", \"123\")\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath+\"/unknowid\", bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath+\"/\"+share.ShareID, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareExpiration)\n\n\tform.Set(\"expiration_date\", \"\")\n\tform.Set(csrfFormToken, \"\")\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath+\"/\"+share.ShareID, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"allowed_ip\", \"1.1.1\")\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath+\"/\"+share.ShareID, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidIPMask)\n\n\tform.Set(\"allowed_ip\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath+\"/\"+share.ShareID, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userSharesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userAPItoken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tshares = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &shares)\n\tassert.NoError(t, err)\n\tif assert.Len(t, shares, 1) {\n\t\ts := shares[0]\n\t\tassert.Equal(t, share.Name, s.Name)\n\t\tassert.Equal(t, share.Description, s.Description)\n\t\tassert.Equal(t, share.Scope, s.Scope)\n\t\tassert.Equal(t, share.Paths, s.Paths)\n\t\tassert.Equal(t, int64(0), s.ExpiresAt)\n\t\tassert.Equal(t, share.MaxTokens, s.MaxTokens)\n\t\tassert.Empty(t, s.AllowFrom)\n\t}\n\t// check the password\n\ts, err := dataprovider.ShareExists(share.ShareID, user.Username)\n\tassert.NoError(t, err)\n\tmatch, err := s.CheckCredentials(defaultPassword)\n\tassert.NoError(t, err)\n\tassert.True(t, match)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharePath+\"?path=%2F&files=a\", nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError400Message)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharePath+\"?path=%2F&files=%5B\\\"adir\\\"%5D\", nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharePath+\"/unknown\", nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharePath+\"/\"+share.ShareID, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharesPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharesPath+jsonAPISuffix, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUserShareNoPasswordDisabled(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled}\n\tu.Filters.DefaultSharesExpiration = 15\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Filters.DefaultSharesExpiration = 30\n\tuser, _, err = httpdtest.UpdateUser(u, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\ttoken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientSharePath, token)\n\tassert.NoError(t, err)\n\tuserAPItoken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\n\tshare := dataprovider.Share{\n\t\tName:  \"s\",\n\t\tScope: dataprovider.ShareScopeRead,\n\t\tPaths: []string{\"/\"},\n\t}\n\tform := make(url.Values)\n\tform.Set(\"name\", share.Name)\n\tform.Set(\"scope\", strconv.Itoa(int(share.Scope)))\n\tform.Set(\"paths[0][path]\", \"/\")\n\tform.Set(\"max_tokens\", \"0\")\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err := http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareNoPwd)\n\n\tform.Set(\"password\", defaultPassword)\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientSharePath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, userSharesPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, userAPItoken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar shares []dataprovider.Share\n\terr = json.Unmarshal(rr.Body.Bytes(), &shares)\n\tassert.NoError(t, err)\n\tif assert.Len(t, shares, 1) {\n\t\ts := shares[0]\n\t\tassert.Equal(t, share.Name, s.Name)\n\t\tassert.Equal(t, share.Scope, s.Scope)\n\t\tassert.Equal(t, share.Paths, s.Paths)\n\t\tshare.ShareID = s.ShareID\n\t}\n\tassert.NotEmpty(t, share.ShareID)\n\tform.Set(\"password\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webClientSharePath+\"/\"+share.ShareID, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorShareNoPwd)\n\n\tuser.Filters.DefaultSharesExpiration = 0\n\tuser.Filters.MaxSharesExpiration = 30\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webClientSharePath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestInvalidCSRF(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfor _, loginURL := range []string{webClientLoginPath, webLoginPath} {\n\t\t// try using an invalid CSRF token\n\t\tloginCookie1, csrfToken1, err := getCSRFTokenMock(loginURL, defaultRemoteAddr)\n\t\tassert.NoError(t, err)\n\t\tassert.NotEmpty(t, loginCookie1)\n\t\tassert.NotEmpty(t, csrfToken1)\n\t\tloginCookie2, csrfToken2, err := getCSRFTokenMock(loginURL, defaultRemoteAddr)\n\t\tassert.NoError(t, err)\n\t\tassert.NotEmpty(t, loginCookie2)\n\t\tassert.NotEmpty(t, csrfToken2)\n\t\trAddr := \"1.2.3.4\"\n\t\tloginCookie3, csrfToken3, err := getCSRFTokenMock(loginURL, rAddr)\n\t\tassert.NoError(t, err)\n\t\tassert.NotEmpty(t, loginCookie3)\n\t\tassert.NotEmpty(t, csrfToken3)\n\n\t\tform := getLoginForm(defaultUsername, defaultPassword, csrfToken1)\n\t\treq, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer([]byte(form.Encode())))\n\t\tassert.NoError(t, err)\n\t\treq.RemoteAddr = defaultRemoteAddr\n\t\treq.RequestURI = loginURL\n\t\tsetLoginCookie(req, loginCookie2)\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\t\t// use a CSRF token as login cookie (invalid audience)\n\t\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken1)\n\t\treq, err = http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer([]byte(form.Encode())))\n\t\tassert.NoError(t, err)\n\t\treq.RemoteAddr = defaultRemoteAddr\n\t\treq.RequestURI = loginURL\n\t\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", csrfToken1))\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\t\t// invalid IP\n\t\tform = getLoginForm(defaultUsername, defaultPassword, csrfToken3)\n\t\treq, err = http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer([]byte(form.Encode())))\n\t\tassert.NoError(t, err)\n\t\treq.RemoteAddr = defaultRemoteAddr\n\t\treq.RequestURI = loginURL\n\t\tsetLoginCookie(req, loginCookie3)\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\trr = executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\t}\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUserProfile(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, token)\n\tassert.NoError(t, err)\n\n\temail := \"user@user.com\"\n\tdescription := \"User\"\n\n\tform := make(url.Values)\n\tform.Set(\"allow_api_key_auth\", \"1\")\n\tform.Set(\"email\", email)\n\tform.Set(\"description\", description)\n\tform.Set(\"public_keys[0][public_key]\", testPubKey)\n\tform.Set(\"public_keys[1][public_key]\", testPubKey1)\n\tform.Set(\"tls_certs[0][tls_cert]\", httpsCert)\n\tform.Set(\"additional_emails[0][additional_email]\", \"email1@user.com\")\n\t// no csrf token\n\treq, err := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.AllowAPIKeyAuth)\n\tassert.Len(t, user.PublicKeys, 2)\n\tassert.Len(t, user.Filters.TLSCerts, 1)\n\tassert.Equal(t, email, user.Email)\n\tassert.Equal(t, description, user.Description)\n\tif assert.Len(t, user.Filters.AdditionalEmails, 1) {\n\t\tassert.Equal(t, \"email1@user.com\", user.Filters.AdditionalEmails[0])\n\t}\n\n\t// set an invalid email\n\tform.Set(\"email\", \"not an email\")\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidEmail)\n\t// invalid tls cert\n\tform.Set(\"email\", email)\n\tform.Set(\"tls_certs[0][tls_cert]\", \"not a TLS cert\")\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidTLSCert)\n\t// invalid public key\n\tform.Set(\"tls_certs[0][tls_cert]\", httpsCert)\n\tform.Set(\"public_keys[0][public_key]\", \"invalid\")\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPubKeyInvalid)\n\t// now remove permissions\n\tform.Set(\"public_keys[0][public_key]\", testPubKey)\n\tform.Del(\"public_keys[1][public_key]\")\n\tuser.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\ttoken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientProfilePath, token)\n\tassert.NoError(t, err)\n\n\tform.Set(\"allow_api_key_auth\", \"0\")\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.AllowAPIKeyAuth)\n\tassert.Len(t, user.PublicKeys, 1)\n\tassert.Len(t, user.Filters.TLSCerts, 1)\n\tassert.Equal(t, email, user.Email)\n\tassert.Equal(t, description, user.Description)\n\n\tuser.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled,\n\t\tsdk.WebClientPubKeyChangeDisabled, sdk.WebClientTLSCertChangeDisabled}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\ttoken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientProfilePath, token)\n\tassert.NoError(t, err)\n\tform.Set(\"public_keys[0][public_key]\", testPubKey)\n\tform.Set(\"public_keys[1][public_key]\", testPubKey1)\n\tform.Set(\"tls_certs[0][tls_cert]\", \"\")\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.AllowAPIKeyAuth)\n\tassert.Len(t, user.PublicKeys, 1)\n\tassert.Len(t, user.Filters.TLSCerts, 1)\n\tassert.Equal(t, email, user.Email)\n\tassert.Equal(t, description, user.Description)\n\n\tuser.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\ttoken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webClientProfilePath, token)\n\tassert.NoError(t, err)\n\tform.Set(\"email\", \"newemail@user.com\")\n\tform.Set(\"description\", \"new description\")\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.AllowAPIKeyAuth)\n\tassert.Len(t, user.PublicKeys, 2)\n\tassert.Len(t, user.Filters.TLSCerts, 0)\n\tassert.Equal(t, email, user.Email)\n\tassert.Equal(t, description, user.Description)\n\t// finally disable all profile permissions\n\tuser.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled,\n\t\tsdk.WebClientPubKeyChangeDisabled, sdk.WebClientTLSCertChangeDisabled}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\ttoken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webChangeClientPwdPath, token)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n}\n\nfunc TestWebAdminProfile(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTWebTokenFromTestServer(admin.Username, altAdminPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webAdminProfilePath, token)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, webAdminProfilePath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform := make(url.Values)\n\tform.Set(\"allow_api_key_auth\", \"1\")\n\tform.Set(\"email\", \"admin@example.com\")\n\tform.Set(\"description\", \"admin desc\")\n\t// no csrf token\n\treq, err = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.AllowAPIKeyAuth)\n\tassert.Equal(t, \"admin@example.com\", admin.Email)\n\tassert.Equal(t, \"admin desc\", admin.Description)\n\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.AllowAPIKeyAuth)\n\tassert.Empty(t, admin.Email)\n\tassert.Empty(t, admin.Description)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n}\n\nfunc TestWebAdminPwdChange(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin.Filters.Preferences.HideUserPageSections = 16 + 32\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTWebTokenFromTestServer(admin.Username, altAdminPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webChangeAdminPwdPath, token)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, webChangeAdminPwdPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform := make(url.Values)\n\tform.Set(\"current_password\", altAdminPassword)\n\tform.Set(\"new_password1\", altAdminPassword)\n\tform.Set(\"new_password2\", altAdminPassword)\n\t// no csrf token\n\treq, _ = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdNoDifferent)\n\n\tform.Set(\"new_password1\", altAdminPassword+\"1\")\n\tform.Set(\"new_password2\", altAdminPassword+\"1\")\n\treq, _ = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAPIKeysManagement(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiKey := dataprovider.APIKey{\n\t\tName:  \"test key\",\n\t\tScope: dataprovider.APIKeyScopeAdmin,\n\t}\n\tasJSON, err := json.Marshal(apiKey)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, apiKeysPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tlocation := rr.Header().Get(\"Location\")\n\tassert.NotEmpty(t, location)\n\tobjectID := rr.Header().Get(\"X-Object-ID\")\n\tassert.NotEmpty(t, objectID)\n\tassert.Equal(t, fmt.Sprintf(\"%v/%v\", apiKeysPath, objectID), location)\n\tapiKey.KeyID = objectID\n\tresponse := make(map[string]string)\n\terr = json.Unmarshal(rr.Body.Bytes(), &response)\n\tassert.NoError(t, err)\n\tkey := response[\"key\"]\n\tassert.NotEmpty(t, key)\n\tassert.True(t, strings.HasPrefix(key, apiKey.KeyID+\".\"))\n\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar keyGet dataprovider.APIKey\n\terr = json.Unmarshal(rr.Body.Bytes(), &keyGet)\n\tassert.NoError(t, err)\n\tassert.Empty(t, keyGet.Key)\n\tassert.Equal(t, apiKey.KeyID, keyGet.KeyID)\n\tassert.Equal(t, apiKey.Scope, keyGet.Scope)\n\tassert.Equal(t, apiKey.Name, keyGet.Name)\n\tassert.Equal(t, int64(0), keyGet.ExpiresAt)\n\tassert.Equal(t, int64(0), keyGet.LastUseAt)\n\tassert.Greater(t, keyGet.CreatedAt, int64(0))\n\tassert.Greater(t, keyGet.UpdatedAt, int64(0))\n\tassert.Empty(t, keyGet.Description)\n\tassert.Empty(t, keyGet.User)\n\tassert.Empty(t, keyGet.Admin)\n\n\t// API key is not enabled for the admin user so this request should fail\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, admin.Username)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"the admin associated with the provided api key cannot be authenticated\")\n\n\tadmin.Filters.AllowAPIKeyAuth = true\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, admin.Username)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, admin.Username+\"1\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\t// now associate the key directly to the admin\n\tapiKey.Admin = admin.Username\n\tapiKey.Description = \"test description\"\n\tasJSON, err = json.Marshal(apiKey)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, apiKeysPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar keys []dataprovider.APIKey\n\terr = json.Unmarshal(rr.Body.Bytes(), &keys)\n\tassert.NoError(t, err)\n\tif assert.GreaterOrEqual(t, len(keys), 1) {\n\t\tfound := false\n\t\tfor _, k := range keys {\n\t\t\tif k.KeyID == apiKey.KeyID {\n\t\t\t\tfound = true\n\t\t\t\tassert.Empty(t, k.Key)\n\t\t\t\tassert.Equal(t, apiKey.Scope, k.Scope)\n\t\t\t\tassert.Equal(t, apiKey.Name, k.Name)\n\t\t\t\tassert.Equal(t, int64(0), k.ExpiresAt)\n\t\t\t\tassert.Greater(t, k.LastUseAt, int64(0))\n\t\t\t\tassert.Equal(t, k.CreatedAt, keyGet.CreatedAt)\n\t\t\t\tassert.Greater(t, k.UpdatedAt, keyGet.UpdatedAt)\n\t\t\t\tassert.Equal(t, apiKey.Description, k.Description)\n\t\t\t\tassert.Empty(t, k.User)\n\t\t\t\tassert.Equal(t, admin.Username, k.Admin)\n\t\t\t}\n\t\t}\n\t\tassert.True(t, found)\n\t}\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// invalid API keys\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key+\"invalid\", \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\tassert.Contains(t, rr.Body.String(), \"the provided api key cannot be authenticated\")\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, \"invalid\", \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// using an API key we cannot modify/get API keys\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tadmin.Filters.AllowList = []string{\"172.16.18.0/24\"}\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"the provided api key is not valid\")\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAPIKeySearch(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiKey := dataprovider.APIKey{\n\t\tScope: dataprovider.APIKeyScopeAdmin,\n\t}\n\tfor i := 1; i < 5; i++ {\n\t\tapiKey.Name = fmt.Sprintf(\"testapikey%v\", i)\n\t\tasJSON, err := json.Marshal(apiKey)\n\t\tassert.NoError(t, err)\n\t\treq, err := http.NewRequest(http.MethodPost, apiKeysPath, bytes.NewBuffer(asJSON))\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, token)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusCreated, rr)\n\t}\n\n\treq, err := http.NewRequest(http.MethodGet, apiKeysPath+\"?limit=1&order=ASC\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar keys []dataprovider.APIKey\n\terr = json.Unmarshal(rr.Body.Bytes(), &keys)\n\tassert.NoError(t, err)\n\tassert.Len(t, keys, 1)\n\tfirstKey := keys[0]\n\n\treq, err = http.NewRequest(http.MethodGet, apiKeysPath+\"?limit=1&order=DESC\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tkeys = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &keys)\n\tassert.NoError(t, err)\n\tif assert.Len(t, keys, 1) {\n\t\tassert.NotEqual(t, firstKey.KeyID, keys[0].KeyID)\n\t}\n\n\treq, err = http.NewRequest(http.MethodGet, apiKeysPath+\"?limit=1&offset=100\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tkeys = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &keys)\n\tassert.NoError(t, err)\n\tassert.Len(t, keys, 0)\n\n\treq, err = http.NewRequest(http.MethodGet, apiKeysPath+\"?limit=f\", nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"%v/%v\", apiKeysPath, \"missingid\"), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, apiKeysPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tkeys = nil\n\terr = json.Unmarshal(rr.Body.Bytes(), &keys)\n\tassert.NoError(t, err)\n\tcounter := 0\n\tfor _, k := range keys {\n\t\tif strings.HasPrefix(k.Name, \"testapikey\") {\n\t\t\treq, err = http.NewRequest(http.MethodDelete, fmt.Sprintf(\"%v/%v\", apiKeysPath, k.KeyID), nil)\n\t\t\tassert.NoError(t, err)\n\t\t\tsetBearerForReq(req, token)\n\t\t\trr = executeRequest(req)\n\t\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\t\tcounter++\n\t\t}\n\t}\n\tassert.Equal(t, 4, counter)\n}\n\nfunc TestAPIKeyErrors(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiKey := dataprovider.APIKey{\n\t\tName:  \"testkey\",\n\t\tScope: dataprovider.APIKeyScopeUser,\n\t}\n\tasJSON, err := json.Marshal(apiKey)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, apiKeysPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tlocation := rr.Header().Get(\"Location\")\n\tassert.NotEmpty(t, location)\n\n\t// invalid API scope\n\tapiKey.Scope = 1000\n\tasJSON, err = json.Marshal(apiKey)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, apiKeysPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\t// invalid JSON\n\treq, err = http.NewRequest(http.MethodPost, apiKeysPath, bytes.NewBuffer([]byte(`invalid JSON`)))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer([]byte(`invalid JSON`)))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, location, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodPut, location, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestAPIKeyOnDeleteCascade(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin, _, err = httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tapiKey := dataprovider.APIKey{\n\t\tName:  \"user api key\",\n\t\tScope: dataprovider.APIKeyScopeUser,\n\t\tUser:  user.Username,\n\t}\n\n\tapiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusUnauthorized, rr)\n\n\tuser.Filters.AllowAPIKeyAuth = true\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, userDirsPath, nil)\n\tassert.NoError(t, err)\n\tsetAPIKeyForReq(req, apiKey.Key, \"\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar contents []map[string]any\n\terr = json.NewDecoder(rr.Body).Decode(&contents)\n\tassert.NoError(t, err)\n\tassert.Len(t, contents, 0)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t_, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\tapiKey.User = \"\"\n\tapiKey.Admin = admin.Username\n\tapiKey.Scope = dataprovider.APIKeyScopeAdmin\n\n\tapiKey, _, err = httpdtest.AddAPIKey(apiKey, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusNotFound)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicWebUsersMock(t *testing.T) {\n\ttoken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tuser1 := getTestUser()\n\tuser1.Username += \"1\"\n\tuser1AsJSON := getUserAsJSON(t, user1)\n\treq, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(user1AsJSON))\n\tsetBearerForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user1)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, webUsersPath+jsonAPISuffix, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, webUserPath, nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(webUserPath, user.Username), nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, webUserPath+\"/0\", nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"username\", user.Username)\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath+\"/0\", &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath+\"/aaa\", &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webUserPath, user.Username), nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token\")\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webUserPath, user.Username), nil)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webUserPath, user1.Username), nil)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestRenderDefenderPageMock(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, webDefenderPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nDefenderTitle)\n}\n\nfunc TestWebAdminBasicMock(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webAdminPath, token)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"username\", admin.Username)\n\tform.Set(\"password\", \"\")\n\tform.Set(\"status\", \"1\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"description\", admin.Description)\n\tform.Set(\"user_page_hidden_sections\", \"1\")\n\tform.Add(\"user_page_hidden_sections\", \"2\")\n\tform.Add(\"user_page_hidden_sections\", \"3\")\n\tform.Add(\"user_page_hidden_sections\", \"4\")\n\tform.Add(\"user_page_hidden_sections\", \"5\")\n\tform.Add(\"user_page_hidden_sections\", \"6\")\n\tform.Add(\"user_page_hidden_sections\", \"7\")\n\tform.Set(\"default_users_expiration\", \"10\")\n\treq, _ := http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"status\", \"a\")\n\treq, _ = http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"status\", \"1\")\n\tform.Set(\"default_users_expiration\", \"a\")\n\treq, _ = http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\tform.Set(\"default_users_expiration\", \"10\")\n\tform.Set(\"password\", admin.Password)\n\treq, _ = http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\t// add TOTP config\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], altAdminUsername)\n\tassert.NoError(t, err)\n\taltToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\tadminTOTPConfig := dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t}\n\tasJSON, err := json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\t// no CSRF token\n\treq, err = http.NewRequest(http.MethodPost, webAdminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token\")\n\n\treq, err = http.NewRequest(http.MethodPost, webAdminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, altToken)\n\tsetCSRFHeaderForReq(req, csrfToken) // invalid CSRF token\n\treq.RemoteAddr = defaultRemoteAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"the token is not valid\")\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminPath, altToken)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, altToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\treq.RemoteAddr = defaultRemoteAddr\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\tsecretPayload := admin.Filters.TOTPConfig.Secret.GetPayload()\n\tassert.NotEmpty(t, secretPayload)\n\tassert.Equal(t, 1+2+4+8+16+32+64, admin.Filters.Preferences.HideUserPageSections)\n\tassert.Equal(t, 10, admin.Filters.Preferences.DefaultUsersExpiration)\n\n\tadminTOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewEmptySecret(),\n\t}\n\tasJSON, err = json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, altToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, secretPayload, admin.Filters.TOTPConfig.Secret.GetPayload())\n\n\tadminTOTPConfig = dataprovider.AdminTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     nil,\n\t}\n\tasJSON, err = json.Marshal(adminTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webAdminTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, altToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webAdminsPath+jsonAPISuffix, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, webAdminsPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webAdminPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"password\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tform.Set(csrfFormToken, csrfToken) // associated to altToken\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webAdminPath, token)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"email\", \"not-an-email\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"email\", \"\")\n\tform.Set(\"status\", \"b\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"email\", \"admin@example.com\")\n\tform.Set(\"status\", \"0\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, admin.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, \"admin@example.com\", admin.Email)\n\tassert.Equal(t, 0, admin.Status)\n\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, altAdminUsername+\"1\"), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(webAdminPath, altAdminUsername), nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(webAdminPath, altAdminUsername+\"1\"), nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webUserPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webAdminPath, altAdminUsername), nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, webUserPath, nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webAdminPath, defaultTokenAuthUser), nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"you cannot delete yourself\")\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webAdminPath, defaultTokenAuthUser), nil)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token\")\n}\n\nfunc TestWebAdminGroupsMock(t *testing.T) {\n\tgroup1 := getTestGroup()\n\tgroup1.Name += \"_1\"\n\tgroup1, _, err := httpdtest.AddGroup(group1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup2 := getTestGroup()\n\tgroup2.Name += \"_2\"\n\tgroup2, _, err = httpdtest.AddGroup(group2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup3 := getTestGroup()\n\tgroup3.Name += \"_3\"\n\tgroup3, _, err = httpdtest.AddGroup(group3, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webAdminPath, token)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", admin.Username)\n\tform.Set(\"password\", \"\")\n\tform.Set(\"status\", \"1\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"description\", admin.Description)\n\tform.Set(\"password\", admin.Password)\n\tform.Set(\"groups[0][group]\", group1.Name)\n\tform.Set(\"groups[0][group_type]\", \"1\")\n\tform.Set(\"groups[1][group]\", group2.Name)\n\tform.Set(\"groups[1][group_type]\", \"2\")\n\tform.Set(\"groups[2][group]\", group3.Name)\n\treq, err := http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, admin.Groups, 3) {\n\t\tfor _, g := range admin.Groups {\n\t\t\tswitch g.Name {\n\t\t\tcase group1.Name:\n\t\t\t\tassert.Equal(t, dataprovider.GroupAddToUsersAsPrimary, g.Options.AddToUsersAs)\n\t\t\tcase group2.Name:\n\t\t\t\tassert.Equal(t, dataprovider.GroupAddToUsersAsSecondary, g.Options.AddToUsersAs)\n\t\t\tcase group3.Name:\n\t\t\t\tassert.Equal(t, dataprovider.GroupAddToUsersAsMembership, g.Options.AddToUsersAs)\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"unexpected group %q\", g.Name)\n\t\t\t}\n\t\t}\n\t}\n\tadminToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webUserPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodGet, webUserPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, adminToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebAdminPermissions(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin.Permissions = []string{dataprovider.PermAdminAddUsers}\n\t_, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTWebToken(altAdminUsername, altAdminPassword)\n\trequire.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, httpBaseURL+webUserPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\n\treq, err = http.NewRequest(http.MethodGet, httpBaseURL+path.Join(webUserPath, \"auser\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\n\treq, err = http.NewRequest(http.MethodGet, httpBaseURL+webFolderPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\n\treq, err = http.NewRequest(http.MethodGet, httpBaseURL+webStatusPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\n\treq, err = http.NewRequest(http.MethodGet, httpBaseURL+webConnectionsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\n\treq, err = http.NewRequest(http.MethodGet, httpBaseURL+webAdminPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\n\treq, err = http.NewRequest(http.MethodGet, httpBaseURL+path.Join(webAdminPath, \"a\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\tresp, err = httpclient.GetHTTPClient().Do(req)\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close()\n\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAdminUpdateSelfMock(t *testing.T) {\n\tadmin, _, err := httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK)\n\tassert.NoError(t, err)\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webAdminPath, token)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"username\", admin.Username)\n\tform.Set(\"password\", admin.Password)\n\tform.Set(\"status\", \"0\")\n\tform.Set(\"permissions\", dataprovider.PermAdminAddUsers)\n\tform.Set(\"permissions\", dataprovider.PermAdminCloseConnections)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, _ := http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorAdminSelfPerms)\n\n\tform.Set(\"permissions\", dataprovider.PermAdminAny)\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorAdminSelfDisable)\n\n\tform.Set(\"status\", \"1\")\n\tform.Set(\"require_two_factor\", \"1\")\n\tform.Set(\"require_password_change\", \"1\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.RequirePasswordChange)\n\tassert.False(t, admin.Filters.RequireTwoFactor)\n\n\tform.Set(\"role\", \"my role\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorAdminSelfRole)\n}\n\nfunc TestWebMaintenanceMock(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, webMaintenancePath, nil)\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webMaintenancePath, token)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"mode\", \"a\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"mode\", \"0\")\n\tform.Set(\"quota\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"quota\", \"0\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodPost, webRestorePath+\"?a=%3\", &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tbackupFilePath := filepath.Join(os.TempDir(), \"backup.json\")\n\terr = createTestFile(backupFilePath, 0)\n\tassert.NoError(t, err)\n\n\tb, contentType, _ = getMultipartFormData(form, \"backup_file\", backupFilePath)\n\treq, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\terr = createTestFile(backupFilePath, 10)\n\tassert.NoError(t, err)\n\n\tb, contentType, _ = getMultipartFormData(form, \"backup_file\", backupFilePath)\n\treq, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser := getTestUser()\n\tuser.ID = 1\n\tuser.Username = \"test_user_web_restore\"\n\tadmin := getTestAdmin()\n\tadmin.ID = 1\n\tadmin.Username = \"test_admin_web_restore\"\n\n\tapiKey := dataprovider.APIKey{\n\t\tName:  \"key name\",\n\t\tKeyID: util.GenerateUniqueID(),\n\t\tKey:   fmt.Sprintf(\"%v.%v\", util.GenerateUniqueID(), util.GenerateUniqueID()),\n\t\tScope: dataprovider.APIKeyScopeAdmin,\n\t}\n\tbackupData := dataprovider.BackupData{\n\t\tVersion: dataprovider.DumpVersion,\n\t}\n\tbackupData.Users = append(backupData.Users, user)\n\tbackupData.Admins = append(backupData.Admins, admin)\n\tbackupData.APIKeys = append(backupData.APIKeys, apiKey)\n\tbackupContent, err := json.Marshal(backupData)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(backupFilePath, backupContent, os.ModePerm)\n\tassert.NoError(t, err)\n\n\tb, contentType, _ = getMultipartFormData(form, \"backup_file\", backupFilePath)\n\treq, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nBackupOK)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAPIKey(apiKey, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = os.Remove(backupFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUserAddMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tgroup1 := getTestGroup()\n\tgroup1.Name += \"_1\"\n\tgroup1, _, err = httpdtest.AddGroup(group1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup2 := getTestGroup()\n\tgroup2.Name += \"_2\"\n\tgroup2, _, err = httpdtest.AddGroup(group2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tgroup3 := getTestGroup()\n\tgroup3.Name += \"_3\"\n\tgroup3, _, err = httpdtest.AddGroup(group3, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuser.UploadBandwidth = 32\n\tuser.DownloadBandwidth = 64\n\tuser.UploadDataTransfer = 1000\n\tuser.DownloadDataTransfer = 2000\n\tuser.UID = 1000\n\tuser.AdditionalInfo = \"info\"\n\tuser.Description = \"user dsc\"\n\tuser.Email = \"test@test.com\"\n\tuser.Filters.AdditionalEmails = []string{\"example1@test.com\", \"example2@test.com\"}\n\tmappedDir := filepath.Join(os.TempDir(), \"mapped\")\n\tfolderName := filepath.Base(mappedDir)\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedDir,\n\t}\n\tfolderAsJSON, err := json.Marshal(f)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON))\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"email\", user.Email)\n\tform.Set(\"additional_emails[0][additional_email]\", user.Filters.AdditionalEmails[0])\n\tform.Set(\"additional_emails[1][additional_email]\", user.Filters.AdditionalEmails[1])\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"osfs_read_buffer_size\", \"2\")\n\tform.Set(\"osfs_write_buffer_size\", \"3\")\n\tform.Set(\"password\", user.Password)\n\tform.Set(\"primary_group\", group1.Name)\n\tform.Set(\"secondary_groups\", group2.Name)\n\tform.Set(\"membership_groups\", group3.Name)\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"directory_permissions[0][sub_perm_path]\", \"/subdir\")\n\tform.Set(\"directory_permissions[0][sub_perm_permissions][]\", \"list\")\n\tform.Add(\"directory_permissions[0][sub_perm_permissions][]\", \"download\")\n\tform.Set(\"virtual_folders[0][vfolder_path]\", \" /vdir\")\n\tform.Set(\"virtual_folders[0][vfolder_name]\", folderName)\n\tform.Set(\"virtual_folders[0][vfolder_quota_files]\", \"2\")\n\tform.Set(\"virtual_folders[0][vfolder_quota_size]\", \"1024\")\n\tform.Set(\"directory_patterns[0][pattern_path]\", \"/dir2\")\n\tform.Set(\"directory_patterns[0][patterns]\", \"*.jpg,*.png\")\n\tform.Set(\"directory_patterns[0][pattern_type]\", \"allowed\")\n\tform.Set(\"directory_patterns[0][pattern_policy]\", \"1\")\n\tform.Set(\"directory_patterns[1][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[1][patterns]\", \"*.png\")\n\tform.Set(\"directory_patterns[1][pattern_type]\", \"allowed\")\n\tform.Set(\"directory_patterns[2][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[2][patterns]\", \"*.zip\")\n\tform.Set(\"directory_patterns[2][pattern_type]\", \"denied\")\n\tform.Set(\"directory_patterns[3][pattern_path]\", \"/dir3\")\n\tform.Set(\"directory_patterns[3][patterns]\", \"*.rar\")\n\tform.Set(\"directory_patterns[3][pattern_type]\", \"denied\")\n\tform.Set(\"directory_patterns[4][pattern_path]\", \"/dir2\")\n\tform.Set(\"directory_patterns[4][patterns]\", \"*.mkv\")\n\tform.Set(\"directory_patterns[4][pattern_type]\", \"denied\")\n\tform.Set(\"access_time_restrictions[0][access_time_day_of_week]\", \"2\")\n\tform.Set(\"access_time_restrictions[0][access_time_start]\", \"10\") // invalid and no end, ignored\n\tform.Set(\"access_time_restrictions[1][access_time_day_of_week]\", \"3\")\n\tform.Set(\"access_time_restrictions[1][access_time_start]\", \"12:00\")\n\tform.Set(\"access_time_restrictions[1][access_time_end]\", \"14:09\")\n\tform.Set(\"additional_info\", user.AdditionalInfo)\n\tform.Set(\"description\", user.Description)\n\tform.Add(\"hooks\", \"external_auth_disabled\")\n\tform.Set(\"disable_fs_checks\", \"checked\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"start_directory\", \"start/dir\")\n\tform.Set(\"require_password_change\", \"1\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\t// test invalid url escape\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath+\"?a=%2\", &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"public_keys\", testPubKey)\n\tform.Add(\"public_keys\", testPubKey1)\n\tform.Set(\"uid\", strconv.FormatInt(int64(user.UID), 10))\n\tform.Set(\"gid\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid gid\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"gid\", \"0\")\n\tform.Set(\"max_sessions\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid max sessions\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"max_sessions\", \"0\")\n\tform.Set(\"quota_size\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid quota size\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"quota_size\", \"0\")\n\tform.Set(\"quota_files\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid quota files\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"quota_files\", \"0\")\n\tform.Set(\"upload_bandwidth\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid upload bandwidth\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"upload_bandwidth\", strconv.FormatInt(user.UploadBandwidth, 10))\n\tform.Set(\"download_bandwidth\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid download bandwidth\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"download_bandwidth\", strconv.FormatInt(user.DownloadBandwidth, 10))\n\tform.Set(\"upload_data_transfer\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid upload data transfer\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"upload_data_transfer\", strconv.FormatInt(user.UploadDataTransfer, 10))\n\tform.Set(\"download_data_transfer\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid download data transfer\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"download_data_transfer\", strconv.FormatInt(user.DownloadDataTransfer, 10))\n\tform.Set(\"total_data_transfer\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid total data transfer\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"total_data_transfer\", strconv.FormatInt(user.TotalDataTransfer, 10))\n\tform.Set(\"status\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid status\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"123\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid expiration date\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"expiration_date\", \"\")\n\tform.Set(\"allowed_ip\", \"invalid,ip\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid allowed_ip\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"allowed_ip\", \"\")\n\tform.Set(\"denied_ip\", \"192.168.1.2\") // it should be 192.168.1.2/32\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\t// test invalid denied_ip\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"denied_ip\", \"\")\n\t// test invalid max file upload size\n\tform.Set(\"max_upload_file_size\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"max_upload_file_size\", \"1 KB\")\n\t// test invalid default shares expiration\n\tform.Set(\"default_shares_expiration\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"default_shares_expiration\", \"10\")\n\t// test invalid max shares expiration\n\tform.Set(\"max_shares_expiration\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"max_shares_expiration\", \"30\")\n\t// test invalid password expiration\n\tform.Set(\"password_expiration\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"password_expiration\", \"90\")\n\t// test invalid password strength\n\tform.Set(\"password_strength\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"password_strength\", \"60\")\n\t// test invalid tls username\n\tform.Set(\"tls_username\", \"username\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"tls_username\", string(sdk.TLSUsernameNone))\n\t// invalid upload bandwidth source\n\tform.Set(\"src_bandwidth_limits[0][bandwidth_limit_sources]\", \"192.168.1.0/24, 192.168.2.0/25\")\n\tform.Set(\"src_bandwidth_limits[0][upload_bandwidth_source]\", \"a\")\n\tform.Set(\"src_bandwidth_limits[0][download_bandwidth_source]\", \"0\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\t// invalid download bandwidth source\n\tform.Set(\"src_bandwidth_limits[0][upload_bandwidth_source]\", \"256\")\n\tform.Set(\"src_bandwidth_limits[0][download_bandwidth_source]\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"src_bandwidth_limits[0][download_bandwidth_source]\", \"512\")\n\tform.Set(\"src_bandwidth_limits[1][download_bandwidth_source]\", \"1024\")\n\tform.Set(\"src_bandwidth_limits[1][bandwidth_limit_sources]\", \"1.1.1\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorSourceBWLimitInvalid)\n\tform.Set(\"src_bandwidth_limits[1][bandwidth_limit_sources]\", \"127.0.0.1/32\")\n\tform.Set(\"src_bandwidth_limits[1][upload_bandwidth_source]\", \"-1\")\n\t// invalid external auth cache size\n\tform.Set(\"external_auth_cache_time\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(csrfFormToken, \"invalid form token\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tdbUser, err := dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, dbUser.Password)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\t// the user already exists, was created with the above request\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tnewUser := dataprovider.User{}\n\terr = render.DecodeJSON(rr.Body, &newUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, user.UID, newUser.UID)\n\tassert.Equal(t, 2, newUser.FsConfig.OSConfig.ReadBufferSize)\n\tassert.Equal(t, 3, newUser.FsConfig.OSConfig.WriteBufferSize)\n\tassert.Equal(t, user.UploadBandwidth, newUser.UploadBandwidth)\n\tassert.Equal(t, user.DownloadBandwidth, newUser.DownloadBandwidth)\n\tassert.Equal(t, user.UploadDataTransfer, newUser.UploadDataTransfer)\n\tassert.Equal(t, user.DownloadDataTransfer, newUser.DownloadDataTransfer)\n\tassert.Equal(t, user.TotalDataTransfer, newUser.TotalDataTransfer)\n\tassert.Equal(t, int64(1000), newUser.Filters.MaxUploadFileSize)\n\tassert.Equal(t, user.AdditionalInfo, newUser.AdditionalInfo)\n\tassert.Equal(t, user.Description, newUser.Description)\n\tassert.True(t, newUser.Filters.Hooks.ExternalAuthDisabled)\n\tassert.False(t, newUser.Filters.Hooks.PreLoginDisabled)\n\tassert.False(t, newUser.Filters.Hooks.CheckPasswordDisabled)\n\tassert.True(t, newUser.Filters.DisableFsChecks)\n\tassert.False(t, newUser.Filters.AllowAPIKeyAuth)\n\tassert.Equal(t, user.Email, newUser.Email)\n\tassert.Equal(t, len(user.Filters.AdditionalEmails), len(newUser.Filters.AdditionalEmails))\n\tassert.Equal(t, \"/start/dir\", newUser.Filters.StartDirectory)\n\tassert.Equal(t, 0, newUser.Filters.FTPSecurity)\n\tassert.Equal(t, 10, newUser.Filters.DefaultSharesExpiration)\n\tassert.Equal(t, 30, newUser.Filters.MaxSharesExpiration)\n\tassert.Equal(t, 90, newUser.Filters.PasswordExpiration)\n\tassert.Equal(t, 60, newUser.Filters.PasswordStrength)\n\tassert.Greater(t, newUser.LastPasswordChange, int64(0))\n\tassert.True(t, newUser.Filters.RequirePasswordChange)\n\tassert.True(t, slices.Contains(newUser.PublicKeys, testPubKey))\n\tif val, ok := newUser.Permissions[\"/subdir\"]; ok {\n\t\tassert.True(t, slices.Contains(val, dataprovider.PermListItems))\n\t\tassert.True(t, slices.Contains(val, dataprovider.PermDownload))\n\t} else {\n\t\tassert.Fail(t, \"user permissions must contain /somedir\", \"actual: %v\", newUser.Permissions)\n\t}\n\tassert.Len(t, newUser.PublicKeys, 2)\n\tassert.Len(t, newUser.VirtualFolders, 1)\n\tfor _, v := range newUser.VirtualFolders {\n\t\tassert.Equal(t, v.VirtualPath, \"/vdir\")\n\t\tassert.Equal(t, v.Name, folderName)\n\t\tassert.Equal(t, v.MappedPath, mappedDir)\n\t\tassert.Equal(t, v.QuotaFiles, 2)\n\t\tassert.Equal(t, v.QuotaSize, int64(1024))\n\t}\n\tassert.Len(t, newUser.Filters.FilePatterns, 3)\n\tfor _, filter := range newUser.Filters.FilePatterns {\n\t\tswitch filter.Path {\n\t\tcase \"/dir1\":\n\t\t\tassert.Len(t, filter.DeniedPatterns, 1)\n\t\t\tassert.Len(t, filter.AllowedPatterns, 1)\n\t\t\tassert.True(t, slices.Contains(filter.AllowedPatterns, \"*.png\"))\n\t\t\tassert.True(t, slices.Contains(filter.DeniedPatterns, \"*.zip\"))\n\t\t\tassert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)\n\t\tcase \"/dir2\":\n\t\t\tassert.Len(t, filter.DeniedPatterns, 1)\n\t\t\tassert.Len(t, filter.AllowedPatterns, 2)\n\t\t\tassert.True(t, slices.Contains(filter.AllowedPatterns, \"*.jpg\"))\n\t\t\tassert.True(t, slices.Contains(filter.AllowedPatterns, \"*.png\"))\n\t\t\tassert.True(t, slices.Contains(filter.DeniedPatterns, \"*.mkv\"))\n\t\t\tassert.Equal(t, sdk.DenyPolicyHide, filter.DenyPolicy)\n\t\tcase \"/dir3\":\n\t\t\tassert.Len(t, filter.DeniedPatterns, 1)\n\t\t\tassert.Len(t, filter.AllowedPatterns, 0)\n\t\t\tassert.True(t, slices.Contains(filter.DeniedPatterns, \"*.rar\"))\n\t\t\tassert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)\n\t\t}\n\t}\n\tif assert.Len(t, newUser.Filters.BandwidthLimits, 2) {\n\t\tfor _, bwLimit := range newUser.Filters.BandwidthLimits {\n\t\t\tif len(bwLimit.Sources) == 2 {\n\t\t\t\tassert.Equal(t, \"192.168.1.0/24\", bwLimit.Sources[0])\n\t\t\t\tassert.Equal(t, \"192.168.2.0/25\", bwLimit.Sources[1])\n\t\t\t\tassert.Equal(t, int64(256), bwLimit.UploadBandwidth)\n\t\t\t\tassert.Equal(t, int64(512), bwLimit.DownloadBandwidth)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, []string{\"127.0.0.1/32\"}, bwLimit.Sources)\n\t\t\t\tassert.Equal(t, int64(0), bwLimit.UploadBandwidth)\n\t\t\t\tassert.Equal(t, int64(1024), bwLimit.DownloadBandwidth)\n\t\t\t}\n\t\t}\n\t}\n\tif assert.Len(t, newUser.Filters.AccessTime, 1) {\n\t\tassert.Equal(t, 3, newUser.Filters.AccessTime[0].DayOfWeek)\n\t\tassert.Equal(t, \"12:00\", newUser.Filters.AccessTime[0].From)\n\t\tassert.Equal(t, \"14:09\", newUser.Filters.AccessTime[0].To)\n\t}\n\tassert.Len(t, newUser.Groups, 3)\n\tassert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t_, err = httpdtest.RemoveGroup(group1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group3, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUserUpdateMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuser.Filters.BandwidthLimits = []sdk.BandwidthLimit{\n\t\t{\n\t\t\tSources:           []string{\"10.8.0.0/16\", \"192.168.1.0/25\"},\n\t\t\tUploadBandwidth:   256,\n\t\t\tDownloadBandwidth: 512,\n\t\t},\n\t}\n\tuser.TotalDataTransfer = 4000\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tlastPwdChange := user.LastPasswordChange\n\tassert.Greater(t, lastPwdChange, int64(0))\n\t// add TOTP config\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuserToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)\n\tassert.NoError(t, err)\n\tuserTOTPConfig := dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH, common.ProtocolFTP},\n\t}\n\tasJSON, err := json.Marshal(userTOTPConfig)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, userToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token\")\n\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webClientProfilePath, userToken)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientTOTPSavePath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, userToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, int64(4000), user.TotalDataTransfer)\n\tassert.Equal(t, lastPwdChange, user.LastPasswordChange)\n\tif assert.Len(t, user.Filters.BandwidthLimits, 1) {\n\t\tif assert.Len(t, user.Filters.BandwidthLimits[0].Sources, 2) {\n\t\t\tassert.Equal(t, \"10.8.0.0/16\", user.Filters.BandwidthLimits[0].Sources[0])\n\t\t\tassert.Equal(t, \"192.168.1.0/25\", user.Filters.BandwidthLimits[0].Sources[1])\n\t\t}\n\t\tassert.Equal(t, int64(256), user.Filters.BandwidthLimits[0].UploadBandwidth)\n\t\tassert.Equal(t, int64(512), user.Filters.BandwidthLimits[0].DownloadBandwidth)\n\t}\n\n\tdbUser, err := dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, dbUser.Password)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tuser.MaxSessions = 1\n\tuser.QuotaFiles = 2\n\tuser.QuotaSize = 1000 * 1000 * 1000\n\tuser.GID = 1000\n\tuser.Filters.AllowAPIKeyAuth = true\n\tuser.AdditionalInfo = \"new additional info\"\n\tuser.Email = \"user@example.com\"\n\tform := make(url.Values)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"email\", user.Email)\n\tform.Set(\"password\", \"\")\n\tform.Set(\"public_keys[0][public_key]\", testPubKey)\n\tform.Set(\"tls_certs[0][tls_cert]\", httpsCert)\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", strconv.FormatInt(int64(user.GID), 10))\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(user.MaxSessions), 10))\n\tform.Set(\"quota_size\", \"1 GB\")\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(user.QuotaFiles), 10))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"directory_permissions[0][sub_perm_path]\", \"/otherdir\")\n\tform.Set(\"directory_permissions[0][sub_perm_permissions][]\", \"list\")\n\tform.Add(\"directory_permissions[0][sub_perm_permissions][]\", \"upload\")\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"2020-01-01 00:00:00\")\n\tform.Set(\"allowed_ip\", \" 192.168.1.3/32, 192.168.2.0/24 \")\n\tform.Set(\"denied_ip\", \" 10.0.0.2/32 \")\n\tform.Set(\"directory_patterns[0][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[0][patterns]\", \"*.zip\")\n\tform.Set(\"directory_patterns[0][pattern_type]\", \"denied\")\n\tform.Set(\"denied_login_methods\", dataprovider.SSHLoginMethodKeyboardInteractive)\n\tform.Set(\"denied_protocols\", common.ProtocolFTP)\n\tform.Set(\"max_upload_file_size\", \"100\")\n\tform.Set(\"default_shares_expiration\", \"30\")\n\tform.Set(\"max_shares_expiration\", \"60\")\n\tform.Set(\"password_expiration\", \"60\")\n\tform.Set(\"password_strength\", \"40\")\n\tform.Set(\"disconnect\", \"1\")\n\tform.Set(\"additional_info\", user.AdditionalInfo)\n\tform.Set(\"description\", user.Description)\n\tform.Set(\"tls_username\", string(sdk.TLSUsernameCN))\n\tform.Set(\"allow_api_key_auth\", \"1\")\n\tform.Set(\"require_password_change\", \"1\")\n\tform.Set(\"external_auth_cache_time\", \"120\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tdbUser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.Empty(t, dbUser.Password)\n\tassert.False(t, dbUser.IsPasswordHashed())\n\n\tform.Set(\"password\", defaultPassword)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tdbUser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, dbUser.Password)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\tprevPwd := dbUser.Password\n\n\tform.Set(\"password\", redactedSecret)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tdbUser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, dbUser.Password)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\tassert.Equal(t, prevPwd, dbUser.Password)\n\tassert.True(t, dbUser.Filters.TOTPConfig.Enabled)\n\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updateUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, user.Email, updateUser.Email)\n\tassert.Equal(t, user.HomeDir, updateUser.HomeDir)\n\tassert.Equal(t, user.MaxSessions, updateUser.MaxSessions)\n\tassert.Equal(t, user.QuotaFiles, updateUser.QuotaFiles)\n\tassert.Equal(t, user.QuotaSize, updateUser.QuotaSize)\n\tassert.Equal(t, user.UID, updateUser.UID)\n\tassert.Equal(t, user.GID, updateUser.GID)\n\tassert.Equal(t, user.AdditionalInfo, updateUser.AdditionalInfo)\n\tassert.Equal(t, user.Description, updateUser.Description)\n\tassert.Equal(t, int64(100), updateUser.Filters.MaxUploadFileSize)\n\tassert.Equal(t, sdk.TLSUsernameCN, updateUser.Filters.TLSUsername)\n\tassert.True(t, updateUser.Filters.AllowAPIKeyAuth)\n\tassert.True(t, updateUser.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, int64(0), updateUser.TotalDataTransfer)\n\tassert.Equal(t, int64(0), updateUser.DownloadDataTransfer)\n\tassert.Equal(t, int64(0), updateUser.UploadDataTransfer)\n\tassert.Equal(t, int64(0), updateUser.Filters.ExternalAuthCacheTime)\n\tassert.Equal(t, 30, updateUser.Filters.DefaultSharesExpiration)\n\tassert.Equal(t, 60, updateUser.Filters.MaxSharesExpiration)\n\tassert.Equal(t, 60, updateUser.Filters.PasswordExpiration)\n\tassert.Equal(t, 40, updateUser.Filters.PasswordStrength)\n\tassert.True(t, updateUser.Filters.RequirePasswordChange)\n\tif val, ok := updateUser.Permissions[\"/otherdir\"]; ok {\n\t\tassert.True(t, slices.Contains(val, dataprovider.PermListItems))\n\t\tassert.True(t, slices.Contains(val, dataprovider.PermUpload))\n\t} else {\n\t\tassert.Fail(t, \"user permissions must contains /otherdir\", \"actual: %v\", updateUser.Permissions)\n\t}\n\tassert.True(t, slices.Contains(updateUser.Filters.AllowedIP, \"192.168.1.3/32\"))\n\tassert.True(t, slices.Contains(updateUser.Filters.DeniedIP, \"10.0.0.2/32\"))\n\tassert.True(t, slices.Contains(updateUser.Filters.DeniedLoginMethods, dataprovider.SSHLoginMethodKeyboardInteractive))\n\tassert.True(t, slices.Contains(updateUser.Filters.DeniedProtocols, common.ProtocolFTP))\n\tassert.True(t, slices.Contains(updateUser.Filters.FilePatterns[0].DeniedPatterns, \"*.zip\"))\n\tassert.Len(t, updateUser.Filters.BandwidthLimits, 0)\n\tassert.Len(t, updateUser.Filters.TLSCerts, 1)\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestRenderFolderTemplateMock(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, webTemplateFolder, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:        \"templatefolder\",\n\t\tMappedPath:  filepath.Join(os.TempDir(), \"mapped\"),\n\t\tDescription: \"template folder desc\",\n\t}\n\tfolder, _, err = httpdtest.AddFolder(folder, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webTemplateFolder+fmt.Sprintf(\"?from=%v\", folder.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webTemplateFolder+\"?from=unknown-folder\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestRenderUserTemplateMock(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, webTemplateUser, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webTemplateUser+fmt.Sprintf(\"?from=%v\", user.Username), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webTemplateUser+\"?from=unknown\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserSaveFromTemplateMock(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webTemplateFolder, token)\n\tassert.NoError(t, err)\n\tuser1 := \"u1\"\n\tuser2 := \"u2\"\n\tform := make(url.Values)\n\tform.Set(\"username\", \"\")\n\tform.Set(\"home_dir\", filepath.Join(os.TempDir(), \"%username%\"))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", \"0\")\n\tform.Set(\"max_sessions\", \"0\")\n\tform.Set(\"quota_size\", \"0\")\n\tform.Set(\"quota_files\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", \"1\")\n\tform.Set(\"expiration_date\", \"\")\n\tform.Set(\"fs_provider\", \"0\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Add(\"template_users[0][tpl_username]\", user1)\n\tform.Add(\"template_users[0][tpl_password]\", \"password1\")\n\tform.Add(\"template_users[0][tpl_public_keys]\", \" \")\n\tform.Add(\"template_users[1][tpl_username]\", user2)\n\tform.Add(\"template_users[1][tpl_public_keys]\", testPubKey)\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ := http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tu1, _, err := httpdtest.GetUserByUsername(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, u1.Filters.RequirePasswordChange)\n\tu2, _, err := httpdtest.GetUserByUsername(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, u2.Filters.RequirePasswordChange)\n\n\t_, err = httpdtest.RemoveUser(u1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(u2, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tform.Add(\"tpl_require_password_change\", \"checked\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tu1, _, err = httpdtest.GetUserByUsername(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, u1.Filters.RequirePasswordChange)\n\tu2, _, err = httpdtest.GetUserByUsername(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, u2.Filters.RequirePasswordChange)\n\n\t_, err = httpdtest.RemoveUser(u1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(u2, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserTemplateErrors(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webTemplateFolder, token)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuser.FsConfig.Provider = sdk.S3FilesystemProvider\n\tuser.FsConfig.S3Config.Bucket = \"test\"\n\tuser.FsConfig.S3Config.Region = \"eu-central-1\"\n\tuser.FsConfig.S3Config.AccessKey = \"%username%\"\n\tuser.FsConfig.S3Config.KeyPrefix = \"somedir/subdir/\"\n\tuser.FsConfig.S3Config.UploadPartSize = 5\n\tuser.FsConfig.S3Config.UploadConcurrency = 4\n\tuser.FsConfig.S3Config.DownloadPartSize = 6\n\tuser.FsConfig.S3Config.DownloadConcurrency = 3\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"home_dir\", filepath.Join(os.TempDir(), \"%username%\"))\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", strconv.FormatInt(int64(user.GID), 10))\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(user.MaxSessions), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(user.QuotaSize, 10))\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(user.QuotaFiles), 10))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"2020-01-01 00:00:00\")\n\tform.Set(\"allowed_ip\", \"\")\n\tform.Set(\"denied_ip\", \"\")\n\tform.Set(\"fs_provider\", \"1\")\n\tform.Set(\"s3_bucket\", user.FsConfig.S3Config.Bucket)\n\tform.Set(\"s3_region\", user.FsConfig.S3Config.Region)\n\tform.Set(\"s3_access_key\", \"%username%\")\n\tform.Set(\"s3_access_secret\", \"%password%\")\n\tform.Set(\"s3_sse_customer_key\", \"%password%\")\n\tform.Set(\"s3_key_prefix\", \"base/%username%\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tform.Add(\"hooks\", \"external_auth_disabled\")\n\tform.Add(\"hooks\", \"check_password_disabled\")\n\tform.Set(\"disable_fs_checks\", \"checked\")\n\tform.Set(\"s3_download_part_max_time\", \"0\")\n\tform.Set(\"s3_upload_part_max_time\", \"0\")\n\t// test invalid s3_upload_part_size\n\tform.Set(\"s3_upload_part_size\", \"a\")\n\tform.Set(\"form_action\", \"export_from_template\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ := http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tform.Set(\"s3_upload_part_size\", strconv.FormatInt(user.FsConfig.S3Config.UploadPartSize, 10))\n\tform.Set(\"s3_upload_concurrency\", strconv.Itoa(user.FsConfig.S3Config.UploadConcurrency))\n\tform.Set(\"s3_download_part_size\", strconv.FormatInt(user.FsConfig.S3Config.DownloadPartSize, 10))\n\tform.Set(\"s3_download_concurrency\", strconv.Itoa(user.FsConfig.S3Config.DownloadConcurrency))\n\t// no user defined\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorUserTemplate)\n\n\tform.Set(\"template_users[0][tpl_username]\", \"user1\")\n\tform.Set(\"template_users[0][tpl_password]\", \"password1\")\n\tform.Set(\"template_users[0][tpl_public_keys]\", \"invalid-pkey\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\trequire.Contains(t, rr.Body.String(), util.I18nErrorPubKeyInvalid)\n\n\tform.Set(\"template_users[0][tpl_username]\", \" \")\n\tform.Set(\"template_users[0][tpl_password]\", \"pwd\")\n\tform.Set(\"template_users[0][tpl_public_keys]\", testPubKey)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\trequire.Contains(t, rr.Body.String(), util.I18nErrorUserTemplate)\n}\n\nfunc TestUserTemplateRoleAndPermissions(t *testing.T) {\n\tr1 := getTestRole()\n\tr2 := getTestRole()\n\tr2.Name += \"_mod\"\n\trole1, resp, err := httpdtest.AddRole(r1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\trole2, resp, err := httpdtest.AddRole(r2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin.Role = role1.Name\n\tadmin.Permissions = []string{dataprovider.PermAdminManageFolders, dataprovider.PermAdminChangeUsers,\n\t\tdataprovider.PermAdminViewUsers}\n\tadmin, _, err = httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\n\treq, _ := http.NewRequest(http.MethodGet, webTemplateUser, nil)\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webTemplateFolder, token)\n\tassert.NoError(t, err)\n\tuser1 := \"u1\"\n\tuser2 := \"u2\"\n\tform := make(url.Values)\n\tform.Set(\"username\", \"\")\n\tform.Set(\"role\", role2.Name)\n\tform.Set(\"home_dir\", filepath.Join(os.TempDir(), \"%username%\"))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", \"0\")\n\tform.Set(\"max_sessions\", \"0\")\n\tform.Set(\"quota_size\", \"0\")\n\tform.Set(\"quota_files\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", \"1\")\n\tform.Set(\"expiration_date\", \"\")\n\tform.Set(\"fs_provider\", \"0\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Add(\"template_users[0][tpl_username]\", user1)\n\tform.Add(\"template_users[0][tpl_password]\", \"password1\")\n\tform.Add(\"template_users[0][tpl_public_keys]\", \" \")\n\tform.Add(\"template_users[1][tpl_username]\", user2)\n\tform.Add(\"template_users[1][tpl_public_keys]\", testPubKey)\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\t// Add the required permissions\n\tadmin.Permissions = append(admin.Permissions, dataprovider.PermAdminAddUsers)\n\t_, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\ttoken, err = getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodGet, webTemplateUser, nil)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tcsrfToken, err = getCSRFTokenFromInternalPageMock(webTemplateUser, token)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tu1, _, err := httpdtest.GetUserByUsername(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, admin.Role, u1.Role)\n\tu2, _, err := httpdtest.GetUserByUsername(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, admin.Role, u2.Role)\n\n\t_, err = httpdtest.RemoveUser(u1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(u2, http.StatusOK)\n\tassert.NoError(t, err)\n\t// Set an empty role\n\tform.Set(\"role\", \"\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tu1, _, err = httpdtest.GetUserByUsername(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, admin.Role, u1.Role)\n\tu2, _, err = httpdtest.GetUserByUsername(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, admin.Role, u2.Role)\n\n\t_, err = httpdtest.RemoveUser(u1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(u2, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role2, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserPlaceholders(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, token)\n\tassert.NoError(t, err)\n\tu := getTestUser()\n\tu.HomeDir = filepath.Join(os.TempDir(), \"%username%_%password%\")\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", u.Username)\n\tform.Set(\"home_dir\", u.HomeDir)\n\tform.Set(\"password\", u.Password)\n\tform.Set(\"status\", strconv.Itoa(u.Status))\n\tform.Set(\"expiration_date\", \"\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"public_keys[0][public_key]\", testPubKey)\n\tform.Set(\"public_keys[1][public_key]\", testPubKey1)\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", \"0\")\n\tform.Set(\"max_sessions\", \"0\")\n\tform.Set(\"quota_size\", \"0\")\n\tform.Set(\"quota_files\", \"0\")\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ := http.NewRequest(http.MethodPost, webUserPath, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Join(os.TempDir(), fmt.Sprintf(\"%v_%v\", defaultUsername, defaultPassword)), user.HomeDir)\n\n\tdbUser, err := dataprovider.UserExists(defaultUsername, \"\")\n\tassert.NoError(t, err)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\thashedPwd := dbUser.Password\n\n\tform.Set(\"password\", redactedSecret)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webUserPath, defaultUsername), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Join(os.TempDir(), defaultUsername+\"_%password%\"), user.HomeDir)\n\t// check that the password was unchanged\n\tdbUser, err = dataprovider.UserExists(defaultUsername, \"\")\n\tassert.NoError(t, err)\n\tassert.True(t, dbUser.IsPasswordHashed())\n\tassert.Equal(t, hashedPwd, dbUser.Password)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestFolderPlaceholders(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webFolderPath, token)\n\tassert.NoError(t, err)\n\tfolderName := \"folderName\"\n\tform := make(url.Values)\n\tform.Set(\"name\", folderName)\n\tform.Set(\"mapped_path\", filepath.Join(os.TempDir(), \"%name%\"))\n\tform.Set(\"description\", \"desc folder %name%\")\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, err := http.NewRequest(http.MethodPost, webFolderPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tfolderGet, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Join(os.TempDir(), folderName), folderGet.MappedPath)\n\tassert.Equal(t, fmt.Sprintf(\"desc folder %v\", folderName), folderGet.Description)\n\n\tform.Set(\"mapped_path\", filepath.Join(os.TempDir(), \"%name%_%name%\"))\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tfolderGet, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Join(os.TempDir(), fmt.Sprintf(\"%v_%v\", folderName, folderName)), folderGet.MappedPath)\n\tassert.Equal(t, fmt.Sprintf(\"desc folder %v\", folderName), folderGet.Description)\n\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestFolderSaveFromTemplateMock(t *testing.T) {\n\tfolder1 := \"f1\"\n\tfolder2 := \"f2\"\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webTemplateFolder, token)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"name\", \"name\")\n\tform.Set(\"mapped_path\", filepath.Join(os.TempDir(), \"%name%\"))\n\tform.Set(\"description\", \"desc folder %name%\")\n\tform.Set(\"template_folders[0][tpl_foldername]\", folder1)\n\tform.Set(\"template_folders[1][tpl_foldername]\", folder2)\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, err := http.NewRequest(http.MethodPost, webTemplateFolder, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\t_, _, err = httpdtest.GetFolderByName(folder1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetFolderByName(folder2, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folder1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folder2}, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webTemplateFolder, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestFolderTemplateErrors(t *testing.T) {\n\tfolderName := \"vfolder-template\"\n\tmappedPath := filepath.Join(os.TempDir(), \"%name%mapped%name%path\")\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webTemplateFolder, token)\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"name\", folderName)\n\tform.Set(\"mapped_path\", mappedPath)\n\tform.Set(\"description\", \"desc folder %name%\")\n\tform.Set(\"template_folders[0][tpl_foldername]\", \"folder1\")\n\tform.Set(\"template_folders[1][tpl_foldername]\", \"folder2\")\n\tform.Set(\"template_folders[2][tpl_foldername]\", \"folder3\")\n\tform.Set(\"template_folders[3][tpl_foldername]\", \"folder1 \")\n\tform.Add(\"template_folders[3][tpl_foldername]\", \" \")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ := http.NewRequest(http.MethodPost, webTemplateFolder, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateFolder+\"?param=p%C3%AO%GG\", &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\tform.Set(\"fs_provider\", \"1\")\n\tform.Set(\"s3_bucket\", \"bucket\")\n\tform.Set(\"s3_region\", \"us-east-1\")\n\tform.Set(\"s3_access_key\", \"%name%\")\n\tform.Set(\"s3_access_secret\", \"pwd%name%\")\n\tform.Set(\"s3_sse_customer_key\", \"key%name%\")\n\tform.Set(\"s3_key_prefix\", \"base/%name%\")\n\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateFolder, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\tform.Set(\"s3_upload_part_size\", \"5\")\n\tform.Set(\"s3_upload_concurrency\", \"4\")\n\tform.Set(\"s3_download_part_max_time\", \"0\")\n\tform.Set(\"s3_upload_part_max_time\", \"0\")\n\tform.Set(\"s3_download_part_size\", \"6\")\n\tform.Set(\"s3_download_concurrency\", \"2\")\n\n\tform.Set(\"template_folders[0][tpl_foldername]\", \" \")\n\tform.Set(\"template_folders[1][tpl_foldername]\", \"\")\n\tform.Set(\"template_folders[2][tpl_foldername]\", \"\")\n\tform.Set(\"template_folders[3][tpl_foldername]\", \" \")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateFolder, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorFolderTemplate)\n\n\tform.Set(\"template_folders[0][tpl_foldername]\", \"name\")\n\tform.Set(\"mapped_path\", \"relative-path\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, webTemplateFolder, &b)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidHomeDir)\n}\n\nfunc TestFolderTemplatePermission(t *testing.T) {\n\tadmin := getTestAdmin()\n\tadmin.Username = altAdminUsername\n\tadmin.Password = altAdminPassword\n\tadmin.Permissions = []string{dataprovider.PermAdminChangeUsers, dataprovider.PermAdminAddUsers, dataprovider.PermAdminViewUsers}\n\tadmin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// no permission to view or add folders from templates\n\ttoken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webTemplateUser, token)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webTemplateFolder, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webTemplateFolder\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tform := make(url.Values)\n\tform.Set(\"name\", \"name\")\n\tform.Set(\"mapped_path\", filepath.Join(os.TempDir(), \"%name%\"))\n\tform.Set(\"description\", \"desc folder %name%\")\n\tform.Set(\"template_folders[0][tpl_foldername]\", \"folder1\")\n\tform.Set(\"template_folders[1][tpl_foldername]\", \"folder2\")\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webTemplateFolder, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\n\tadmin.Permissions = append(admin.Permissions, dataprovider.PermAdminManageFolders)\n\t_, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\ttoken, err = getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\t_, err = getCSRFTokenFromInternalPageMock(webTemplateUser, token)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webTemplateFolder, nil)\n\tassert.NoError(t, err)\n\treq.RequestURI = webTemplateFolder\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUserS3Mock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tlastPwdChange := user.LastPasswordChange\n\tassert.Greater(t, lastPwdChange, int64(0))\n\tuser.FsConfig.Provider = sdk.S3FilesystemProvider\n\tuser.FsConfig.S3Config.Bucket = \"test\"\n\tuser.FsConfig.S3Config.Region = \"eu-west-1\"\n\tuser.FsConfig.S3Config.AccessKey = \"access-key\"\n\tuser.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(\"access-secret\")\n\tuser.FsConfig.S3Config.SSECustomerKey = kms.NewPlainSecret(\"enc-key\")\n\tuser.FsConfig.S3Config.RoleARN = \"arn:aws:iam::123456789012:user/Development/product_1234/*\"\n\tuser.FsConfig.S3Config.Endpoint = \"http://127.0.0.1:9000/path?a=b\"\n\tuser.FsConfig.S3Config.StorageClass = \"Standard\"\n\tuser.FsConfig.S3Config.KeyPrefix = \"somedir/subdir/\"\n\tuser.FsConfig.S3Config.UploadPartSize = 5\n\tuser.FsConfig.S3Config.UploadConcurrency = 4\n\tuser.FsConfig.S3Config.DownloadPartMaxTime = 60\n\tuser.FsConfig.S3Config.UploadPartMaxTime = 120\n\tuser.FsConfig.S3Config.DownloadPartSize = 6\n\tuser.FsConfig.S3Config.DownloadConcurrency = 3\n\tuser.FsConfig.S3Config.ForcePathStyle = true\n\tuser.FsConfig.S3Config.SkipTLSVerify = true\n\tuser.FsConfig.S3Config.ACL = \"public-read\"\n\tuser.Description = \"s3 tèst user\"\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"password\", redactedSecret)\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", strconv.FormatInt(int64(user.GID), 10))\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(user.MaxSessions), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(user.QuotaSize, 10))\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(user.QuotaFiles), 10))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"2020-01-01 00:00:00\")\n\tform.Set(\"allowed_ip\", \"\")\n\tform.Set(\"denied_ip\", \"\")\n\tform.Set(\"fs_provider\", \"1\")\n\tform.Set(\"s3_bucket\", user.FsConfig.S3Config.Bucket)\n\tform.Set(\"s3_region\", user.FsConfig.S3Config.Region)\n\tform.Set(\"s3_access_key\", user.FsConfig.S3Config.AccessKey)\n\tform.Set(\"s3_access_secret\", user.FsConfig.S3Config.AccessSecret.GetPayload())\n\tform.Set(\"s3_sse_customer_key\", user.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\tform.Set(\"s3_role_arn\", user.FsConfig.S3Config.RoleARN)\n\tform.Set(\"s3_storage_class\", user.FsConfig.S3Config.StorageClass)\n\tform.Set(\"s3_acl\", user.FsConfig.S3Config.ACL)\n\tform.Set(\"s3_endpoint\", user.FsConfig.S3Config.Endpoint)\n\tform.Set(\"s3_key_prefix\", user.FsConfig.S3Config.KeyPrefix)\n\tform.Set(\"directory_patterns[0][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[0][patterns]\", \"*.jpg,*.png\")\n\tform.Set(\"directory_patterns[0][pattern_type]\", \"allowed\")\n\tform.Set(\"directory_patterns[0][pattern_policy]\", \"0\")\n\tform.Set(\"directory_patterns[1][pattern_path]\", \"/dir2\")\n\tform.Set(\"directory_patterns[1][patterns]\", \"*.zip\")\n\tform.Set(\"directory_patterns[1][pattern_type]\", \"denied\")\n\tform.Set(\"directory_patterns[1][pattern_policy]\", \"1\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tform.Set(\"ftp_security\", \"1\")\n\tform.Set(\"s3_force_path_style\", \"checked\")\n\tform.Set(\"s3_skip_tls_verify\", \"checked\")\n\tform.Set(\"description\", user.Description)\n\tform.Add(\"hooks\", \"pre_login_disabled\")\n\tform.Add(\"allow_api_key_auth\", \"1\")\n\t// test invalid s3_upload_part_size\n\tform.Set(\"s3_upload_part_size\", \"a\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test invalid s3_upload_concurrency\n\tform.Set(\"s3_upload_part_size\", strconv.FormatInt(user.FsConfig.S3Config.UploadPartSize, 10))\n\tform.Set(\"s3_upload_concurrency\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test invalid s3_download_part_size\n\tform.Set(\"s3_upload_concurrency\", strconv.Itoa(user.FsConfig.S3Config.UploadConcurrency))\n\tform.Set(\"s3_download_part_size\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test invalid s3_download_concurrency\n\tform.Set(\"s3_download_part_size\", strconv.FormatInt(user.FsConfig.S3Config.DownloadPartSize, 10))\n\tform.Set(\"s3_download_concurrency\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test invalid s3_download_part_max_time\n\tform.Set(\"s3_download_concurrency\", strconv.Itoa(user.FsConfig.S3Config.DownloadConcurrency))\n\tform.Set(\"s3_download_part_max_time\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test invalid s3_upload_part_max_time\n\tform.Set(\"s3_download_part_max_time\", strconv.Itoa(user.FsConfig.S3Config.DownloadPartMaxTime))\n\tform.Set(\"s3_upload_part_max_time\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// now add the user\n\tform.Set(\"s3_upload_part_max_time\", strconv.Itoa(user.FsConfig.S3Config.UploadPartMaxTime))\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updateUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1577836800000), updateUser.ExpirationDate)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.Bucket, user.FsConfig.S3Config.Bucket)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.Region, user.FsConfig.S3Config.Region)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.AccessKey, user.FsConfig.S3Config.AccessKey)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.RoleARN, user.FsConfig.S3Config.RoleARN)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.StorageClass, user.FsConfig.S3Config.StorageClass)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.ACL, user.FsConfig.S3Config.ACL)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.Endpoint, user.FsConfig.S3Config.Endpoint)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.KeyPrefix, user.FsConfig.S3Config.KeyPrefix)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.UploadPartSize, user.FsConfig.S3Config.UploadPartSize)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.UploadConcurrency, user.FsConfig.S3Config.UploadConcurrency)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.DownloadPartMaxTime, user.FsConfig.S3Config.DownloadPartMaxTime)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.UploadPartMaxTime, user.FsConfig.S3Config.UploadPartMaxTime)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.DownloadPartSize, user.FsConfig.S3Config.DownloadPartSize)\n\tassert.Equal(t, updateUser.FsConfig.S3Config.DownloadConcurrency, user.FsConfig.S3Config.DownloadConcurrency)\n\tassert.Equal(t, lastPwdChange, updateUser.LastPasswordChange)\n\tassert.True(t, updateUser.FsConfig.S3Config.ForcePathStyle)\n\tassert.True(t, updateUser.FsConfig.S3Config.SkipTLSVerify)\n\tif assert.Equal(t, 2, len(updateUser.Filters.FilePatterns)) {\n\t\tfor _, filter := range updateUser.Filters.FilePatterns {\n\t\t\tswitch filter.Path {\n\t\t\tcase \"/dir1\":\n\t\t\t\tassert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)\n\t\t\tcase \"/dir2\":\n\t\t\t\tassert.Equal(t, sdk.DenyPolicyHide, filter.DenyPolicy)\n\t\t\t}\n\t\t}\n\t}\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.S3Config.AccessSecret.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.S3Config.SSECustomerKey.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.S3Config.SSECustomerKey.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.S3Config.SSECustomerKey.GetAdditionalData())\n\tassert.Equal(t, user.Description, updateUser.Description)\n\tassert.True(t, updateUser.Filters.Hooks.PreLoginDisabled)\n\tassert.False(t, updateUser.Filters.Hooks.ExternalAuthDisabled)\n\tassert.False(t, updateUser.Filters.Hooks.CheckPasswordDisabled)\n\tassert.False(t, updateUser.Filters.DisableFsChecks)\n\tassert.True(t, updateUser.Filters.AllowAPIKeyAuth)\n\tassert.Equal(t, 1, updateUser.Filters.FTPSecurity)\n\t// now check that a redacted password is not saved\n\tform.Set(\"s3_access_secret\", redactedSecret)\n\tform.Set(\"s3_sse_customer_key\", redactedSecret)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tvar lastUpdatedUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &lastUpdatedUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload(), lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.S3Config.SSECustomerKey.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.S3Config.SSECustomerKey.GetPayload(), lastUpdatedUser.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.S3Config.SSECustomerKey.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.S3Config.SSECustomerKey.GetAdditionalData())\n\tassert.Equal(t, lastPwdChange, lastUpdatedUser.LastPasswordChange)\n\t// now clear credentials\n\tform.Set(\"s3_access_key\", \"\")\n\tform.Set(\"s3_access_secret\", \"\")\n\tform.Set(\"s3_sse_customer_key\", \"\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar userGet dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &userGet)\n\tassert.NoError(t, err)\n\tassert.Nil(t, userGet.FsConfig.S3Config.AccessSecret)\n\tassert.Nil(t, userGet.FsConfig.S3Config.SSECustomerKey)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestWebUserGCSMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tcredentialsFilePath := filepath.Join(os.TempDir(), \"gcs.json\")\n\terr = createTestFile(credentialsFilePath, 0)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tuser.FsConfig.GCSConfig.Bucket = \"test\"\n\tuser.FsConfig.GCSConfig.KeyPrefix = \"somedir/subdir/\"\n\tuser.FsConfig.GCSConfig.StorageClass = \"standard\"\n\tuser.FsConfig.GCSConfig.ACL = \"publicReadWrite\"\n\tuser.FsConfig.GCSConfig.UploadPartSize = 16\n\tuser.FsConfig.GCSConfig.UploadPartMaxTime = 32\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"password\", redactedSecret)\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", strconv.FormatInt(int64(user.GID), 10))\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(user.MaxSessions), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(user.QuotaSize, 10))\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(user.QuotaFiles), 10))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"2020-01-01 00:00:00\")\n\tform.Set(\"allowed_ip\", \"\")\n\tform.Set(\"denied_ip\", \"\")\n\tform.Set(\"fs_provider\", \"2\")\n\tform.Set(\"gcs_bucket\", user.FsConfig.GCSConfig.Bucket)\n\tform.Set(\"gcs_storage_class\", user.FsConfig.GCSConfig.StorageClass)\n\tform.Set(\"gcs_acl\", user.FsConfig.GCSConfig.ACL)\n\tform.Set(\"gcs_key_prefix\", user.FsConfig.GCSConfig.KeyPrefix)\n\tform.Set(\"gcs_upload_part_size\", strconv.FormatInt(user.FsConfig.GCSConfig.UploadPartSize, 10))\n\tform.Set(\"gcs_upload_part_max_time\", strconv.FormatInt(int64(user.FsConfig.GCSConfig.UploadPartMaxTime), 10))\n\tform.Set(\"directory_patterns[0][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[0][patterns]\", \"*.jpg,*.png\")\n\tform.Set(\"directory_patterns[0][pattern_type]\", \"allowed\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tform.Set(\"ftp_security\", \"1\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tb, contentType, _ = getMultipartFormData(form, \"gcs_credential_file\", credentialsFilePath)\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = createTestFile(credentialsFilePath, 4096)\n\tassert.NoError(t, err)\n\tb, contentType, _ = getMultipartFormData(form, \"gcs_credential_file\", credentialsFilePath)\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updateUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1577836800000), updateUser.ExpirationDate)\n\tassert.Equal(t, user.FsConfig.Provider, updateUser.FsConfig.Provider)\n\tassert.Equal(t, user.FsConfig.GCSConfig.Bucket, updateUser.FsConfig.GCSConfig.Bucket)\n\tassert.Equal(t, user.FsConfig.GCSConfig.StorageClass, updateUser.FsConfig.GCSConfig.StorageClass)\n\tassert.Equal(t, user.FsConfig.GCSConfig.ACL, updateUser.FsConfig.GCSConfig.ACL)\n\tassert.Equal(t, user.FsConfig.GCSConfig.KeyPrefix, updateUser.FsConfig.GCSConfig.KeyPrefix)\n\tassert.Equal(t, user.FsConfig.GCSConfig.UploadPartSize, updateUser.FsConfig.GCSConfig.UploadPartSize)\n\tassert.Equal(t, user.FsConfig.GCSConfig.UploadPartMaxTime, updateUser.FsConfig.GCSConfig.UploadPartMaxTime)\n\tif assert.Len(t, updateUser.Filters.FilePatterns, 1) {\n\t\tassert.Equal(t, \"/dir1\", updateUser.Filters.FilePatterns[0].Path)\n\t\tassert.Len(t, updateUser.Filters.FilePatterns[0].AllowedPatterns, 2)\n\t\tassert.Contains(t, updateUser.Filters.FilePatterns[0].AllowedPatterns, \"*.png\")\n\t\tassert.Contains(t, updateUser.Filters.FilePatterns[0].AllowedPatterns, \"*.jpg\")\n\t}\n\tassert.Equal(t, 1, updateUser.Filters.FTPSecurity)\n\tform.Set(\"gcs_auto_credentials\", \"on\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tupdateUser = dataprovider.User{}\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, updateUser.FsConfig.GCSConfig.AutomaticCredentials)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = os.Remove(credentialsFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebUserHTTPFsMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\tuser.FsConfig.HTTPConfig = vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint:      \"https://127.0.0.1:9999/api/v1\",\n\t\t\tUsername:      defaultUsername,\n\t\t\tSkipTLSVerify: true,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\tAPIKey:   kms.NewPlainSecret(defaultTokenAuthPass),\n\t}\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"password\", redactedSecret)\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", strconv.FormatInt(int64(user.GID), 10))\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(user.MaxSessions), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(user.QuotaSize, 10))\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(user.QuotaFiles), 10))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"2020-01-01 00:00:00\")\n\tform.Set(\"allowed_ip\", \"\")\n\tform.Set(\"denied_ip\", \"\")\n\tform.Set(\"fs_provider\", \"6\")\n\tform.Set(\"http_endpoint\", user.FsConfig.HTTPConfig.Endpoint)\n\tform.Set(\"http_username\", user.FsConfig.HTTPConfig.Username)\n\tform.Set(\"http_password\", user.FsConfig.HTTPConfig.Password.GetPayload())\n\tform.Set(\"http_api_key\", user.FsConfig.HTTPConfig.APIKey.GetPayload())\n\tform.Set(\"http_skip_tls_verify\", \"checked\")\n\tform.Set(\"directory_patterns[0][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[0][patterns]\", \"*.jpg,*.png\")\n\tform.Set(\"directory_patterns[0][pattern_type]\", \"allowed\")\n\tform.Set(\"directory_patterns[1][pattern_path]\", \"/dir2\")\n\tform.Set(\"directory_patterns[1][patterns]\", \"*.zip\")\n\tform.Set(\"directory_patterns[1][pattern_type]\", \"denied\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tform.Set(\"http_equality_check_mode\", \"true\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the updated user\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updateUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1577836800000), updateUser.ExpirationDate)\n\tassert.Equal(t, 2, len(updateUser.Filters.FilePatterns))\n\tassert.Equal(t, user.FsConfig.HTTPConfig.Endpoint, updateUser.FsConfig.HTTPConfig.Endpoint)\n\tassert.Equal(t, user.FsConfig.HTTPConfig.Username, updateUser.FsConfig.HTTPConfig.Username)\n\tassert.Equal(t, user.FsConfig.HTTPConfig.SkipTLSVerify, updateUser.FsConfig.HTTPConfig.SkipTLSVerify)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.HTTPConfig.APIKey.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetAdditionalData())\n\tassert.Equal(t, 1, updateUser.FsConfig.HTTPConfig.EqualityCheckMode)\n\t// now check that a redacted password is not saved\n\tform.Set(\"http_equality_check_mode\", \"\")\n\tform.Set(\"http_password\", \" \"+redactedSecret+\" \")\n\tform.Set(\"http_api_key\", redactedSecret)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar lastUpdatedUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &lastUpdatedUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.HTTPConfig.Password.GetPayload(), lastUpdatedUser.FsConfig.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.HTTPConfig.APIKey.GetPayload(), lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetAdditionalData())\n\tassert.Equal(t, 0, lastUpdatedUser.FsConfig.HTTPConfig.EqualityCheckMode)\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestWebUserAzureBlobMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tuser.FsConfig.AzBlobConfig.Container = \"container\"\n\tuser.FsConfig.AzBlobConfig.AccountName = \"aname\"\n\tuser.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(\"access-skey\")\n\tuser.FsConfig.AzBlobConfig.Endpoint = \"http://127.0.0.1:9000/path?b=c\"\n\tuser.FsConfig.AzBlobConfig.KeyPrefix = \"somedir/subdir/\"\n\tuser.FsConfig.AzBlobConfig.UploadPartSize = 5\n\tuser.FsConfig.AzBlobConfig.UploadConcurrency = 4\n\tuser.FsConfig.AzBlobConfig.DownloadPartSize = 3\n\tuser.FsConfig.AzBlobConfig.DownloadConcurrency = 6\n\tuser.FsConfig.AzBlobConfig.UseEmulator = true\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"password\", redactedSecret)\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", strconv.FormatInt(int64(user.GID), 10))\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(user.MaxSessions), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(user.QuotaSize, 10))\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(user.QuotaFiles), 10))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"2020-01-01 00:00:00\")\n\tform.Set(\"allowed_ip\", \"\")\n\tform.Set(\"denied_ip\", \"\")\n\tform.Set(\"fs_provider\", \"3\")\n\tform.Set(\"az_container\", user.FsConfig.AzBlobConfig.Container)\n\tform.Set(\"az_account_name\", user.FsConfig.AzBlobConfig.AccountName)\n\tform.Set(\"az_account_key\", user.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\tform.Set(\"az_endpoint\", user.FsConfig.AzBlobConfig.Endpoint)\n\tform.Set(\"az_key_prefix\", user.FsConfig.AzBlobConfig.KeyPrefix)\n\tform.Set(\"az_use_emulator\", \"checked\")\n\tform.Set(\"directory_patterns[0][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[0][patterns]\", \"*.jpg,*.png\")\n\tform.Set(\"directory_patterns[0][pattern_type]\", \"allowed\")\n\tform.Set(\"directory_patterns[1][pattern_path]\", \"/dir2\")\n\tform.Set(\"directory_patterns[1][patterns]\", \"*.zip\")\n\tform.Set(\"directory_patterns[1][pattern_type]\", \"denied\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\t// test invalid az_upload_part_size\n\tform.Set(\"az_upload_part_size\", \"a\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test invalid az_upload_concurrency\n\tform.Set(\"az_upload_part_size\", strconv.FormatInt(user.FsConfig.AzBlobConfig.UploadPartSize, 10))\n\tform.Set(\"az_upload_concurrency\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test invalid az_download_part_size\n\tform.Set(\"az_upload_concurrency\", strconv.Itoa(user.FsConfig.AzBlobConfig.UploadConcurrency))\n\tform.Set(\"az_download_part_size\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// test invalid az_download_concurrency\n\tform.Set(\"az_download_part_size\", strconv.FormatInt(user.FsConfig.AzBlobConfig.DownloadPartSize, 10))\n\tform.Set(\"az_download_concurrency\", \"a\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// now add the user\n\tform.Set(\"az_download_concurrency\", strconv.Itoa(user.FsConfig.AzBlobConfig.DownloadConcurrency))\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updateUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1577836800000), updateUser.ExpirationDate)\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.Container, user.FsConfig.AzBlobConfig.Container)\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.AccountName, user.FsConfig.AzBlobConfig.AccountName)\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.Endpoint, user.FsConfig.AzBlobConfig.Endpoint)\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.KeyPrefix, user.FsConfig.AzBlobConfig.KeyPrefix)\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.UploadPartSize, user.FsConfig.AzBlobConfig.UploadPartSize)\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.UploadConcurrency, user.FsConfig.AzBlobConfig.UploadConcurrency)\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.DownloadPartSize, user.FsConfig.AzBlobConfig.DownloadPartSize)\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.DownloadConcurrency, user.FsConfig.AzBlobConfig.DownloadConcurrency)\n\tassert.Equal(t, 2, len(updateUser.Filters.FilePatterns))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\t// now check that a redacted password is not saved\n\tform.Set(\"az_account_key\", redactedSecret+\" \")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar lastUpdatedUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &lastUpdatedUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetPayload(), lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())\n\t// test SAS url\n\tuser.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret(\"sasurl\")\n\tform.Set(\"az_account_name\", \"\")\n\tform.Set(\"az_account_key\", \"\")\n\tform.Set(\"az_container\", \"\")\n\tform.Set(\"az_sas_url\", user.FsConfig.AzBlobConfig.SASURL.GetPayload())\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tupdateUser = dataprovider.User{}\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.AzBlobConfig.SASURL.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.AzBlobConfig.SASURL.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.AzBlobConfig.SASURL.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.AzBlobConfig.SASURL.GetAdditionalData())\n\t// now check that a redacted sas url is not saved\n\tform.Set(\"az_sas_url\", redactedSecret)\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tlastUpdatedUser = dataprovider.User{}\n\terr = render.DecodeJSON(rr.Body, &lastUpdatedUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.AzBlobConfig.SASURL.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.AzBlobConfig.SASURL.GetPayload(), lastUpdatedUser.FsConfig.AzBlobConfig.SASURL.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.SASURL.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.SASURL.GetAdditionalData())\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestWebUserCryptMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"crypted passphrase\")\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"password\", redactedSecret)\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", strconv.FormatInt(int64(user.GID), 10))\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(user.MaxSessions), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(user.QuotaSize, 10))\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(user.QuotaFiles), 10))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"2020-01-01 00:00:00\")\n\tform.Set(\"allowed_ip\", \"\")\n\tform.Set(\"denied_ip\", \"\")\n\tform.Set(\"fs_provider\", \"4\")\n\tform.Set(\"crypt_passphrase\", \"\")\n\tform.Set(\"cryptfs_read_buffer_size\", \"1\")\n\tform.Set(\"cryptfs_write_buffer_size\", \"2\")\n\tform.Set(\"directory_patterns[0][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[0][patterns]\", \"*.jpg,*.png\")\n\tform.Set(\"directory_patterns[0][pattern_type]\", \"allowed\")\n\tform.Set(\"directory_patterns[1][pattern_path]\", \"/dir2\")\n\tform.Set(\"directory_patterns[1][patterns]\", \"*.zip\")\n\tform.Set(\"directory_patterns[1][pattern_type]\", \"denied\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\t// passphrase cannot be empty\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"crypt_passphrase\", user.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updateUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1577836800000), updateUser.ExpirationDate)\n\tassert.Equal(t, 2, len(updateUser.Filters.FilePatterns))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.CryptConfig.Passphrase.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\tassert.Equal(t, 1, updateUser.FsConfig.CryptConfig.ReadBufferSize)\n\tassert.Equal(t, 2, updateUser.FsConfig.CryptConfig.WriteBufferSize)\n\t// now check that a redacted password is not saved\n\tform.Set(\"crypt_passphrase\", redactedSecret+\" \")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar lastUpdatedUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &lastUpdatedUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.CryptConfig.Passphrase.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.CryptConfig.Passphrase.GetPayload(), lastUpdatedUser.FsConfig.CryptConfig.Passphrase.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.CryptConfig.Passphrase.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.CryptConfig.Passphrase.GetAdditionalData())\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestWebUserSFTPFsMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tuserAsJSON := getUserAsJSON(t, user)\n\treq, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusCreated, rr)\n\terr = render.DecodeJSON(rr.Body, &user)\n\tassert.NoError(t, err)\n\tuser.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser.FsConfig.SFTPConfig.Endpoint = \"127.0.0.1:22\"\n\tuser.FsConfig.SFTPConfig.Username = \"sftpuser\"\n\tuser.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(\"pwd\")\n\tuser.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(testPrivateKeyPwd)\n\tuser.FsConfig.SFTPConfig.KeyPassphrase = kms.NewPlainSecret(privateKeyPwd)\n\tuser.FsConfig.SFTPConfig.Fingerprints = []string{sftpPkeyFingerprint}\n\tuser.FsConfig.SFTPConfig.Prefix = \"/home/sftpuser\"\n\tuser.FsConfig.SFTPConfig.DisableCouncurrentReads = true\n\tuser.FsConfig.SFTPConfig.BufferSize = 5\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"password\", redactedSecret)\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", strconv.FormatInt(int64(user.GID), 10))\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(user.MaxSessions), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(user.QuotaSize, 10))\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(user.QuotaFiles), 10))\n\tform.Set(\"upload_bandwidth\", \"0\")\n\tform.Set(\"download_bandwidth\", \"0\")\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"expiration_date\", \"2020-01-01 00:00:00\")\n\tform.Set(\"allowed_ip\", \"\")\n\tform.Set(\"denied_ip\", \"\")\n\tform.Set(\"fs_provider\", \"5\")\n\tform.Set(\"crypt_passphrase\", \"\")\n\tform.Set(\"directory_patterns[0][pattern_path]\", \"/dir1\")\n\tform.Set(\"directory_patterns[0][patterns]\", \"*.jpg,*.png\")\n\tform.Set(\"directory_patterns[0][pattern_type]\", \"allowed\")\n\tform.Set(\"directory_patterns[1][pattern_path]\", \"/dir2\")\n\tform.Set(\"directory_patterns[1][patterns]\", \"*.zip\")\n\tform.Set(\"directory_patterns[1][pattern_type]\", \"denied\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\t// empty sftpconfig\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tform.Set(\"sftp_endpoint\", user.FsConfig.SFTPConfig.Endpoint)\n\tform.Set(\"sftp_username\", user.FsConfig.SFTPConfig.Username)\n\tform.Set(\"sftp_password\", user.FsConfig.SFTPConfig.Password.GetPayload())\n\tform.Set(\"sftp_private_key\", user.FsConfig.SFTPConfig.PrivateKey.GetPayload())\n\tform.Set(\"sftp_key_passphrase\", user.FsConfig.SFTPConfig.KeyPassphrase.GetPayload())\n\tform.Set(\"sftp_fingerprints\", user.FsConfig.SFTPConfig.Fingerprints[0])\n\tform.Set(\"sftp_prefix\", user.FsConfig.SFTPConfig.Prefix)\n\tform.Set(\"sftp_disable_concurrent_reads\", \"true\")\n\tform.Set(\"sftp_equality_check_mode\", \"true\")\n\tform.Set(\"sftp_buffer_size\", strconv.FormatInt(user.FsConfig.SFTPConfig.BufferSize, 10))\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar updateUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &updateUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1577836800000), updateUser.ExpirationDate)\n\tassert.Equal(t, 2, len(updateUser.Filters.FilePatterns))\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.SFTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.SFTPConfig.Password.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.SFTPConfig.Password.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.SFTPConfig.Password.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.SFTPConfig.PrivateKey.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.SFTPConfig.KeyPassphrase.GetStatus())\n\tassert.NotEmpty(t, updateUser.FsConfig.SFTPConfig.KeyPassphrase.GetPayload())\n\tassert.Empty(t, updateUser.FsConfig.SFTPConfig.KeyPassphrase.GetKey())\n\tassert.Empty(t, updateUser.FsConfig.SFTPConfig.KeyPassphrase.GetAdditionalData())\n\tassert.Equal(t, updateUser.FsConfig.SFTPConfig.Prefix, user.FsConfig.SFTPConfig.Prefix)\n\tassert.Equal(t, updateUser.FsConfig.SFTPConfig.Username, user.FsConfig.SFTPConfig.Username)\n\tassert.Equal(t, updateUser.FsConfig.SFTPConfig.Endpoint, user.FsConfig.SFTPConfig.Endpoint)\n\tassert.True(t, updateUser.FsConfig.SFTPConfig.DisableCouncurrentReads)\n\tassert.Len(t, updateUser.FsConfig.SFTPConfig.Fingerprints, 1)\n\tassert.Equal(t, user.FsConfig.SFTPConfig.BufferSize, updateUser.FsConfig.SFTPConfig.BufferSize)\n\tassert.Contains(t, updateUser.FsConfig.SFTPConfig.Fingerprints, sftpPkeyFingerprint)\n\tassert.Equal(t, 1, updateUser.FsConfig.SFTPConfig.EqualityCheckMode)\n\t// now check that a redacted credentials are not saved\n\tform.Set(\"sftp_password\", redactedSecret+\" \")\n\tform.Set(\"sftp_private_key\", redactedSecret)\n\tform.Set(\"sftp_key_passphrase\", redactedSecret)\n\tform.Set(\"sftp_equality_check_mode\", \"\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar lastUpdatedUser dataprovider.User\n\terr = render.DecodeJSON(rr.Body, &lastUpdatedUser)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.SFTPConfig.Password.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.SFTPConfig.Password.GetPayload(), lastUpdatedUser.FsConfig.SFTPConfig.Password.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.Password.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.Password.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.SFTPConfig.PrivateKey.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.SFTPConfig.PrivateKey.GetPayload(), lastUpdatedUser.FsConfig.SFTPConfig.PrivateKey.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.PrivateKey.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.SFTPConfig.KeyPassphrase.GetStatus())\n\tassert.Equal(t, updateUser.FsConfig.SFTPConfig.KeyPassphrase.GetPayload(), lastUpdatedUser.FsConfig.SFTPConfig.KeyPassphrase.GetPayload())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.KeyPassphrase.GetKey())\n\tassert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.KeyPassphrase.GetAdditionalData())\n\tassert.Equal(t, 0, lastUpdatedUser.FsConfig.SFTPConfig.EqualityCheckMode)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestWebUserRole(t *testing.T) {\n\trole, resp, err := httpdtest.AddRole(getTestRole(), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\ta.Role = role.Name\n\ta.Permissions = []string{dataprovider.PermAdminAddUsers, dataprovider.PermAdminChangeUsers,\n\t\tdataprovider.PermAdminDeleteUsers, dataprovider.PermAdminViewUsers}\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\twebToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\tuser := getTestUser()\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"home_dir\", user.HomeDir)\n\tform.Set(\"password\", user.Password)\n\tform.Set(\"status\", strconv.Itoa(user.Status))\n\tform.Set(\"permissions\", \"*\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"uid\", \"0\")\n\tform.Set(\"gid\", \"0\")\n\tform.Set(\"max_sessions\", \"0\")\n\tform.Set(\"quota_size\", \"0\")\n\tform.Set(\"quota_files\", \"0\")\n\tform.Set(\"upload_bandwidth\", strconv.FormatInt(user.UploadBandwidth, 10))\n\tform.Set(\"download_bandwidth\", strconv.FormatInt(user.DownloadBandwidth, 10))\n\tform.Set(\"upload_data_transfer\", strconv.FormatInt(user.UploadDataTransfer, 10))\n\tform.Set(\"download_data_transfer\", strconv.FormatInt(user.DownloadDataTransfer, 10))\n\tform.Set(\"total_data_transfer\", strconv.FormatInt(user.TotalDataTransfer, 10))\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"10\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tb, contentType, _ := getMultipartFormData(form, \"\", \"\")\n\treq, err := http.NewRequest(http.MethodPost, webUserPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, user.Role)\n\n\tform.Set(\"role\", \"\")\n\tb, contentType, _ = getMultipartFormData(form, \"\", \"\")\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, role.Name, user.Role)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebEventAction(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webAdminEventActionPath, webToken)\n\tassert.NoError(t, err)\n\taction := dataprovider.BaseEventAction{\n\t\tName:        \"web_action_http\",\n\t\tDescription: \"http web action\",\n\t\tType:        dataprovider.ActionTypeHTTP,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\t\tEndpoint: \"https://localhost:4567/action\",\n\t\t\t\tUsername: defaultUsername,\n\t\t\t\tHeaders: []dataprovider.KeyValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   \"Content-Type\",\n\t\t\t\t\t\tValue: \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPassword:      kms.NewPlainSecret(defaultPassword),\n\t\t\t\tTimeout:       10,\n\t\t\t\tSkipTLSVerify: true,\n\t\t\t\tMethod:        http.MethodPost,\n\t\t\t\tQueryParameters: []dataprovider.KeyValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   \"param1\",\n\t\t\t\t\t\tValue: \"value1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tBody: `{\"event\":\"{{.Event}}\",\"name\":\"{{.Name}}\"}`,\n\t\t\t},\n\t\t},\n\t}\n\tform := make(url.Values)\n\tform.Set(\"name\", action.Name)\n\tform.Set(\"description\", action.Description)\n\tform.Set(\"fs_action_type\", \"0\")\n\tform.Set(\"type\", \"a\")\n\treq, err := http.NewRequest(http.MethodPost, webAdminEventActionPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"type\", fmt.Sprintf(\"%d\", action.Type))\n\tform.Set(\"http_timeout\", \"b\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventActionPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"cmd_timeout\", \"20\")\n\tform.Set(\"pwd_expiration_threshold\", \"10\")\n\tform.Set(\"http_timeout\", fmt.Sprintf(\"%d\", action.Options.HTTPConfig.Timeout))\n\tform.Set(\"http_headers[0][http_header_key]\", action.Options.HTTPConfig.Headers[0].Key)\n\tform.Set(\"http_headers[0][http_header_value]\", action.Options.HTTPConfig.Headers[0].Value)\n\tform.Set(\"http_headers[1][http_header_key]\", action.Options.HTTPConfig.Headers[0].Key) // ignored\n\tform.Set(\"query_parameters[0][http_query_key]\", action.Options.HTTPConfig.QueryParameters[0].Key)\n\tform.Set(\"query_parameters[0][http_query_value]\", action.Options.HTTPConfig.QueryParameters[0].Value)\n\tform.Set(\"http_body\", action.Options.HTTPConfig.Body)\n\tform.Set(\"http_skip_tls_verify\", \"1\")\n\tform.Set(\"http_username\", action.Options.HTTPConfig.Username)\n\tform.Set(\"http_password\", action.Options.HTTPConfig.Password.GetPayload())\n\tform.Set(\"http_method\", action.Options.HTTPConfig.Method)\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventActionPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventActionPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorURLRequired)\n\tform.Set(\"http_endpoint\", action.Options.HTTPConfig.Endpoint)\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventActionPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// a new add will fail\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventActionPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// list actions\n\treq, err = http.NewRequest(http.MethodGet, webAdminEventActionsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, webAdminEventActionsPath+jsonAPISuffix, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// render add page\n\treq, err = http.NewRequest(http.MethodGet, webAdminEventActionPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// render action page\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminEventActionPath, action.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// missing action\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminEventActionPath, action.Name+\"1\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// check the action\n\tactionGet, _, err := httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tassert.Equal(t, action.Description, actionGet.Description)\n\tassert.Equal(t, action.Options.HTTPConfig.Body, actionGet.Options.HTTPConfig.Body)\n\tassert.Equal(t, action.Options.HTTPConfig.Endpoint, actionGet.Options.HTTPConfig.Endpoint)\n\tassert.Equal(t, action.Options.HTTPConfig.Headers, actionGet.Options.HTTPConfig.Headers)\n\tassert.Equal(t, action.Options.HTTPConfig.Method, actionGet.Options.HTTPConfig.Method)\n\tassert.Equal(t, action.Options.HTTPConfig.SkipTLSVerify, actionGet.Options.HTTPConfig.SkipTLSVerify)\n\tassert.Equal(t, action.Options.HTTPConfig.Timeout, actionGet.Options.HTTPConfig.Timeout)\n\tassert.Equal(t, action.Options.HTTPConfig.Username, actionGet.Options.HTTPConfig.Username)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, actionGet.Options.HTTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, actionGet.Options.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, actionGet.Options.HTTPConfig.Password.GetKey())\n\tassert.Empty(t, actionGet.Options.HTTPConfig.Password.GetAdditionalData())\n\t// update and check that the password is preserved and the multipart fields\n\tform.Set(\"http_password\", redactedSecret)\n\tform.Set(\"http_body\", \"\")\n\tform.Set(\"http_timeout\", \"0\")\n\tform.Del(\"http_headers[0][http_header_key]\")\n\tform.Del(\"http_headers[0][http_header_val]\")\n\tform.Set(\"multipart_body[0][http_part_name]\", \"part1\")\n\tform.Set(\"multipart_body[0][http_part_file]\", \"{{.VirtualPath}}\")\n\tform.Set(\"multipart_body[0][http_part_body]\", \"\")\n\tform.Set(\"multipart_body[0][http_part_headers]\", \"X-MyHeader: a:b,c\")\n\tform.Set(\"multipart_body[12][http_part_name]\", \"part2\")\n\tform.Set(\"multipart_body[12][http_part_headers]\", \"Content-Type:application/json \\r\\n\")\n\tform.Set(\"multipart_body[12][http_part_body]\", \"{{.ObjectData}}\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tdbAction, err := dataprovider.EventActionExists(action.Name)\n\tassert.NoError(t, err)\n\terr = dbAction.Options.HTTPConfig.Password.Decrypt()\n\tassert.NoError(t, err)\n\tassert.Equal(t, defaultPassword, dbAction.Options.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, dbAction.Options.HTTPConfig.Body)\n\tassert.Equal(t, 0, dbAction.Options.HTTPConfig.Timeout)\n\tif assert.Len(t, dbAction.Options.HTTPConfig.Parts, 2) {\n\t\tassert.Equal(t, \"part1\", dbAction.Options.HTTPConfig.Parts[0].Name)\n\t\tassert.Equal(t, \"/{{.VirtualPath}}\", dbAction.Options.HTTPConfig.Parts[0].Filepath)\n\t\tassert.Empty(t, dbAction.Options.HTTPConfig.Parts[0].Body)\n\t\tassert.Equal(t, \"X-MyHeader\", dbAction.Options.HTTPConfig.Parts[0].Headers[0].Key)\n\t\tassert.Equal(t, \"a:b,c\", dbAction.Options.HTTPConfig.Parts[0].Headers[0].Value)\n\t\tassert.Equal(t, \"part2\", dbAction.Options.HTTPConfig.Parts[1].Name)\n\t\tassert.Equal(t, \"{{.ObjectData}}\", dbAction.Options.HTTPConfig.Parts[1].Body)\n\t\tassert.Empty(t, dbAction.Options.HTTPConfig.Parts[1].Filepath)\n\t\tassert.Equal(t, \"Content-Type\", dbAction.Options.HTTPConfig.Parts[1].Headers[0].Key)\n\t\tassert.Equal(t, \"application/json\", dbAction.Options.HTTPConfig.Parts[1].Headers[0].Value)\n\t}\n\t// change action type\n\taction.Type = dataprovider.ActionTypeCommand\n\taction.Options.CmdConfig = dataprovider.EventActionCommandConfig{\n\t\tCmd:     filepath.Join(os.TempDir(), \"cmd\"),\n\t\tArgs:    []string{\"arg1\", \"arg2\"},\n\t\tTimeout: 20,\n\t\tEnvVars: []dataprovider.KeyValue{\n\t\t\t{\n\t\t\t\tKey:   \"key\",\n\t\t\t\tValue: \"val\",\n\t\t\t},\n\t\t},\n\t}\n\tdataprovider.EnabledActionCommands = []string{action.Options.CmdConfig.Cmd}\n\tdefer func() {\n\t\tdataprovider.EnabledActionCommands = nil\n\t}()\n\tform.Set(\"type\", fmt.Sprintf(\"%d\", action.Type))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorCommandRequired)\n\tform.Set(\"cmd_path\", action.Options.CmdConfig.Cmd)\n\tform.Set(\"cmd_timeout\", \"a\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"cmd_timeout\", fmt.Sprintf(\"%d\", action.Options.CmdConfig.Timeout))\n\tform.Set(\"env_vars[0][cmd_env_key]\", action.Options.CmdConfig.EnvVars[0].Key)\n\tform.Set(\"env_vars[0][cmd_env_value]\", action.Options.CmdConfig.EnvVars[0].Value)\n\tform.Set(\"cmd_arguments\", \"arg1  ,arg2  \")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// update a missing action\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name+\"1\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// update with no csrf token\n\tform.Del(csrfFormToken)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\tform.Set(csrfFormToken, csrfToken)\n\t// check the update\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tassert.Equal(t, action.Options.CmdConfig.Cmd, actionGet.Options.CmdConfig.Cmd)\n\tassert.Equal(t, action.Options.CmdConfig.Args, actionGet.Options.CmdConfig.Args)\n\tassert.Equal(t, action.Options.CmdConfig.Timeout, actionGet.Options.CmdConfig.Timeout)\n\tassert.Equal(t, action.Options.CmdConfig.EnvVars, actionGet.Options.CmdConfig.EnvVars)\n\tassert.Equal(t, dataprovider.EventActionHTTPConfig{}, actionGet.Options.HTTPConfig)\n\tassert.Equal(t, dataprovider.EventActionPasswordExpiration{}, actionGet.Options.PwdExpirationConfig)\n\t// change action type again\n\taction.Type = dataprovider.ActionTypeEmail\n\taction.Options.EmailConfig = dataprovider.EventActionEmailConfig{\n\t\tRecipients:  []string{\"address1@example.com\", \"address2@example.com\"},\n\t\tBcc:         []string{\"address3@example.com\"},\n\t\tSubject:     \"subject\",\n\t\tContentType: 1,\n\t\tBody:        \"body\",\n\t\tAttachments: []string{\"/file1.txt\", \"/file2.txt\"},\n\t}\n\tform.Set(\"type\", fmt.Sprintf(\"%d\", action.Type))\n\tform.Set(\"email_recipients\", \"address1@example.com,  address2@example.com\")\n\tform.Set(\"email_bcc\", \"address3@example.com\")\n\tform.Set(\"email_subject\", action.Options.EmailConfig.Subject)\n\tform.Set(\"email_content_type\", fmt.Sprintf(\"%d\", action.Options.EmailConfig.ContentType))\n\tform.Set(\"email_body\", action.Options.EmailConfig.Body)\n\tform.Set(\"email_attachments\", \"file1.txt, file2.txt\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the update\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tassert.Equal(t, action.Options.EmailConfig.Recipients, actionGet.Options.EmailConfig.Recipients)\n\tassert.Equal(t, action.Options.EmailConfig.Bcc, actionGet.Options.EmailConfig.Bcc)\n\tassert.Equal(t, action.Options.EmailConfig.Subject, actionGet.Options.EmailConfig.Subject)\n\tassert.Equal(t, action.Options.EmailConfig.ContentType, actionGet.Options.EmailConfig.ContentType)\n\tassert.Equal(t, action.Options.EmailConfig.Body, actionGet.Options.EmailConfig.Body)\n\tassert.Equal(t, action.Options.EmailConfig.Attachments, actionGet.Options.EmailConfig.Attachments)\n\tassert.Equal(t, dataprovider.EventActionHTTPConfig{}, actionGet.Options.HTTPConfig)\n\tassert.Empty(t, actionGet.Options.CmdConfig.Cmd)\n\tassert.Equal(t, 0, actionGet.Options.CmdConfig.Timeout)\n\tassert.Len(t, actionGet.Options.CmdConfig.EnvVars, 0)\n\t// change action type to data retention check\n\taction.Type = dataprovider.ActionTypeDataRetentionCheck\n\tform.Set(\"type\", fmt.Sprintf(\"%d\", action.Type))\n\tform.Set(\"data_retention[10][folder_retention_path]\", \"p1\")\n\tform.Set(\"data_retention[10][folder_retention_val]\", \"a\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"data_retention[10][folder_retention_val]\", \"24\")\n\tform.Set(\"data_retention[10][folder_retention_options][]\", \"1\")\n\tform.Set(\"data_retention[11][folder_retention_path]\", \"../p2\")\n\tform.Set(\"data_retention[11][folder_retention_val]\", \"48\")\n\tform.Set(\"data_retention[11][folder_retention_options][]\", \"1\")\n\tform.Set(\"data_retention[13][folder_retention_options][]\", \"1\") // ignored\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the update\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tif assert.Len(t, actionGet.Options.RetentionConfig.Folders, 2) {\n\t\tfor _, folder := range actionGet.Options.RetentionConfig.Folders {\n\t\t\tswitch folder.Path {\n\t\t\tcase \"/p1\":\n\t\t\t\tassert.Equal(t, 24, folder.Retention)\n\t\t\t\tassert.True(t, folder.DeleteEmptyDirs)\n\t\t\tcase \"/p2\":\n\t\t\t\tassert.Equal(t, 48, folder.Retention)\n\t\t\t\tassert.True(t, folder.DeleteEmptyDirs)\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"unexpected folder path %v\", folder.Path)\n\t\t\t}\n\t\t}\n\t}\n\taction.Type = dataprovider.ActionTypeFilesystem\n\taction.Options.FsConfig = dataprovider.EventActionFilesystemConfig{\n\t\tType:   dataprovider.FilesystemActionMkdirs,\n\t\tMkDirs: []string{\"a \", \" a/b\"},\n\t}\n\tform.Set(\"type\", fmt.Sprintf(\"%d\", action.Type))\n\tform.Set(\"fs_mkdir_paths\", strings.Join(action.Options.FsConfig.MkDirs, \",\"))\n\tform.Set(\"fs_action_type\", \"invalid\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\tform.Set(\"fs_action_type\", fmt.Sprintf(\"%d\", action.Options.FsConfig.Type))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the update\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tif assert.Len(t, actionGet.Options.FsConfig.MkDirs, 2) {\n\t\tfor _, dir := range actionGet.Options.FsConfig.MkDirs {\n\t\t\tswitch dir {\n\t\t\tcase \"/a\":\n\t\t\tcase \"/a/b\":\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"unexpected dir path %v\", dir)\n\t\t\t}\n\t\t}\n\t}\n\n\taction.Options.FsConfig = dataprovider.EventActionFilesystemConfig{\n\t\tType:  dataprovider.FilesystemActionExist,\n\t\tExist: []string{\"b \", \" c/d\"},\n\t}\n\tform.Set(\"fs_action_type\", fmt.Sprintf(\"%d\", action.Options.FsConfig.Type))\n\tform.Set(\"fs_exist_paths\", strings.Join(action.Options.FsConfig.Exist, \",\"))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the update\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tif assert.Len(t, actionGet.Options.FsConfig.Exist, 2) {\n\t\tfor _, p := range actionGet.Options.FsConfig.Exist {\n\t\t\tswitch p {\n\t\t\tcase \"/b\":\n\t\t\tcase \"/c/d\":\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"unexpected path %v\", p)\n\t\t\t}\n\t\t}\n\t}\n\n\taction.Options.FsConfig = dataprovider.EventActionFilesystemConfig{\n\t\tType: dataprovider.FilesystemActionRename,\n\t\tRenames: []dataprovider.RenameConfig{\n\t\t\t{\n\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\tKey:   \"/src\",\n\t\t\t\t\tValue: \"/target\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tform.Set(\"fs_action_type\", fmt.Sprintf(\"%d\", action.Options.FsConfig.Type))\n\tform.Set(\"fs_rename[0][fs_rename_source]\", action.Options.FsConfig.Renames[0].Key)\n\tform.Set(\"fs_rename[0][fs_rename_target]\", action.Options.FsConfig.Renames[0].Value)\n\tform.Set(\"fs_rename[0][fs_rename_options][]\", \"1\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the update\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tif assert.Len(t, actionGet.Options.FsConfig.Renames, 1) {\n\t\tassert.True(t, actionGet.Options.FsConfig.Renames[0].UpdateModTime)\n\t}\n\n\taction.Options.FsConfig = dataprovider.EventActionFilesystemConfig{\n\t\tType: dataprovider.FilesystemActionCopy,\n\t\tCopy: []dataprovider.KeyValue{\n\t\t\t{\n\t\t\t\tKey:   \"/copy_src\",\n\t\t\t\tValue: \"/copy_target\",\n\t\t\t},\n\t\t},\n\t}\n\tform.Set(\"fs_action_type\", fmt.Sprintf(\"%d\", action.Options.FsConfig.Type))\n\tform.Set(\"fs_copy[0][fs_copy_source]\", action.Options.FsConfig.Copy[0].Key)\n\tform.Set(\"fs_copy[0][fs_copy_target]\", action.Options.FsConfig.Copy[0].Value)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the update\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tassert.Len(t, actionGet.Options.FsConfig.Copy, 1)\n\n\taction.Type = dataprovider.ActionTypePasswordExpirationCheck\n\taction.Options.PwdExpirationConfig.Threshold = 15\n\tform.Set(\"type\", fmt.Sprintf(\"%d\", action.Type))\n\tform.Set(\"pwd_expiration_threshold\", \"a\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"pwd_expiration_threshold\", strconv.Itoa(action.Options.PwdExpirationConfig.Threshold))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tassert.Equal(t, action.Options.PwdExpirationConfig.Threshold, actionGet.Options.PwdExpirationConfig.Threshold)\n\tassert.Equal(t, 0, actionGet.Options.CmdConfig.Timeout)\n\tassert.Len(t, actionGet.Options.CmdConfig.EnvVars, 0)\n\n\taction.Type = dataprovider.ActionTypeUserInactivityCheck\n\taction.Options.UserInactivityConfig = dataprovider.EventActionUserInactivity{\n\t\tDisableThreshold: 10,\n\t\tDeleteThreshold:  15,\n\t}\n\tform.Set(\"type\", fmt.Sprintf(\"%d\", action.Type))\n\tform.Set(\"inactivity_disable_threshold\", strconv.Itoa(action.Options.UserInactivityConfig.DisableThreshold))\n\tform.Set(\"inactivity_delete_threshold\", strconv.Itoa(action.Options.UserInactivityConfig.DeleteThreshold))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tassert.Equal(t, 0, actionGet.Options.PwdExpirationConfig.Threshold)\n\tassert.Equal(t, action.Options.UserInactivityConfig.DisableThreshold, actionGet.Options.UserInactivityConfig.DisableThreshold)\n\tassert.Equal(t, action.Options.UserInactivityConfig.DeleteThreshold, actionGet.Options.UserInactivityConfig.DeleteThreshold)\n\n\taction.Type = dataprovider.ActionTypeIDPAccountCheck\n\tform.Set(\"type\", fmt.Sprintf(\"%d\", action.Type))\n\tform.Set(\"idp_mode\", \"1\")\n\tform.Set(\"idp_user\", `{\"username\":\"user\"}`)\n\tform.Set(\"idp_admin\", `{\"username\":\"admin\"}`)\n\tform.Set(\"pwd_expiration_threshold\", strconv.Itoa(action.Options.PwdExpirationConfig.Threshold))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tactionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, action.Type, actionGet.Type)\n\tassert.Equal(t, 1, actionGet.Options.IDPConfig.Mode)\n\tassert.Contains(t, actionGet.Options.IDPConfig.TemplateUser, `\"user\"`)\n\tassert.Contains(t, actionGet.Options.IDPConfig.TemplateAdmin, `\"admin\"`)\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventActionPath, action.Name), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventActionPath, action.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminEventActionsPath+jsonAPISuffix, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Equal(t, `[]`, rr.Body.String())\n}\n\nfunc TestWebEventRule(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webAdminEventRulePath, webToken)\n\tassert.NoError(t, err)\n\ta := dataprovider.BaseEventAction{\n\t\tName: \"web_action\",\n\t\tType: dataprovider.ActionTypeFilesystem,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\t\tType:  dataprovider.FilesystemActionExist,\n\t\t\t\tExist: []string{\"/dir1\"},\n\t\t\t},\n\t\t},\n\t}\n\taction, _, err := httpdtest.AddEventAction(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\trule := dataprovider.EventRule{\n\t\tName:        \"test_web_rule\",\n\t\tStatus:      1,\n\t\tDescription: \"rule added using web API\",\n\t\tTrigger:     dataprovider.EventTriggerSchedule,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tSchedules: []dataprovider.Schedule{\n\t\t\t\t{\n\t\t\t\t\tHours:      \"0\",\n\t\t\t\t\tDayOfWeek:  \"*\",\n\t\t\t\t\tDayOfMonth: \"*\",\n\t\t\t\t\tMonth:      \"*\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tOptions: dataprovider.ConditionOptions{\n\t\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern:      \"u*\",\n\t\t\t\t\t\tInverseMatch: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tGroupNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern:      \"g*\",\n\t\t\t\t\t\tInverseMatch: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRoleNames: []dataprovider.ConditionPattern{\n\t\t\t\t\t{\n\t\t\t\t\t\tPattern:      \"r*\",\n\t\t\t\t\t\tInverseMatch: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action.Name,\n\t\t\t\t},\n\t\t\t\tOrder: 1,\n\t\t\t},\n\t\t},\n\t}\n\tform := make(url.Values)\n\tform.Set(\"name\", rule.Name)\n\tform.Set(\"description\", rule.Description)\n\tform.Set(\"status\", \"a\")\n\treq, err := http.NewRequest(http.MethodPost, webAdminEventRulePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"status\", fmt.Sprintf(\"%d\", rule.Status))\n\tform.Set(\"trigger\", \"a\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventRulePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"trigger\", fmt.Sprintf(\"%d\", rule.Trigger))\n\tform.Set(\"schedules[0][schedule_hour]\", rule.Conditions.Schedules[0].Hours)\n\tform.Set(\"schedules[0][schedule_day_of_week]\", rule.Conditions.Schedules[0].DayOfWeek)\n\tform.Set(\"schedules[0][schedule_day_of_month]\", rule.Conditions.Schedules[0].DayOfMonth)\n\tform.Set(\"schedules[0][schedule_month]\", rule.Conditions.Schedules[0].Month)\n\tform.Set(\"name_filters[0][name_pattern]\", rule.Conditions.Options.Names[0].Pattern)\n\tform.Set(\"name_filters[0][type_name_pattern]\", \"inverse\")\n\tform.Set(\"group_name_filters[0][group_name_pattern]\", rule.Conditions.Options.GroupNames[0].Pattern)\n\tform.Set(\"group_name_filters[0][type_group_name_pattern]\", \"inverse\")\n\tform.Set(\"role_name_filters[0][role_name_pattern]\", rule.Conditions.Options.RoleNames[0].Pattern)\n\tform.Set(\"role_name_filters[0][type_role_name_pattern]\", \"inverse\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventRulePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidMinSize)\n\tform.Set(\"fs_min_size\", \"0\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventRulePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidMaxSize)\n\tform.Set(\"fs_max_size\", \"0\")\n\tform.Set(\"actions[0][action_name]\", action.Name)\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventRulePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventRulePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// a new add will fail\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventRulePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// list rules\n\treq, err = http.NewRequest(http.MethodGet, webAdminEventRulesPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, webAdminEventRulesPath+jsonAPISuffix, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// render add page\n\treq, err = http.NewRequest(http.MethodGet, webAdminEventRulePath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// render rule page\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminEventRulePath, rule.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// missing rule\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminEventRulePath, rule.Name+\"1\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// check the rule\n\truleGet, _, err := httpdtest.GetEventRuleByName(rule.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, rule.Trigger, ruleGet.Trigger)\n\tassert.Equal(t, rule.Status, ruleGet.Status)\n\tassert.Equal(t, rule.Description, ruleGet.Description)\n\tassert.Equal(t, rule.Conditions, ruleGet.Conditions)\n\tif assert.Len(t, ruleGet.Actions, 1) {\n\t\tassert.Equal(t, rule.Actions[0].Name, ruleGet.Actions[0].Name)\n\t\tassert.Equal(t, rule.Actions[0].Order, ruleGet.Actions[0].Order)\n\t}\n\t// change rule trigger and status\n\trule.Status = 0\n\trule.Trigger = dataprovider.EventTriggerFsEvent\n\trule.Conditions = dataprovider.EventConditions{\n\t\tFsEvents: []string{\"upload\", \"download\"},\n\t\tOptions: dataprovider.ConditionOptions{\n\t\t\tNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern:      \"u*\",\n\t\t\t\t\tInverseMatch: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tGroupNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern:      \"g*\",\n\t\t\t\t\tInverseMatch: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tRoleNames: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern:      \"r*\",\n\t\t\t\t\tInverseMatch: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tFsPaths: []dataprovider.ConditionPattern{\n\t\t\t\t{\n\t\t\t\t\tPattern: \"/subdir/*.txt\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProtocols:   []string{common.ProtocolSFTP, common.ProtocolHTTP},\n\t\t\tMinFileSize: 1024 * 1024,\n\t\t\tMaxFileSize: 5 * 1024 * 1024,\n\t\t},\n\t}\n\tform.Set(\"status\", fmt.Sprintf(\"%d\", rule.Status))\n\tform.Set(\"trigger\", fmt.Sprintf(\"%d\", rule.Trigger))\n\tfor _, event := range rule.Conditions.FsEvents {\n\t\tform.Add(\"fs_events\", event)\n\t}\n\tform.Set(\"path_filters[0][fs_path_pattern]\", rule.Conditions.Options.FsPaths[0].Pattern)\n\tfor _, protocol := range rule.Conditions.Options.Protocols {\n\t\tform.Add(\"fs_protocols\", protocol)\n\t}\n\tform.Set(\"fs_min_size\", fmt.Sprintf(\"%d\", rule.Conditions.Options.MinFileSize))\n\tform.Set(\"fs_max_size\", fmt.Sprintf(\"%d\", rule.Conditions.Options.MaxFileSize))\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventRulePath, rule.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the rule\n\truleGet, _, err = httpdtest.GetEventRuleByName(rule.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, rule.Status, ruleGet.Status)\n\tassert.Equal(t, rule.Trigger, ruleGet.Trigger)\n\tassert.Equal(t, rule.Description, ruleGet.Description)\n\tassert.Equal(t, rule.Conditions, ruleGet.Conditions)\n\tif assert.Len(t, ruleGet.Actions, 1) {\n\t\tassert.Equal(t, rule.Actions[0].Name, ruleGet.Actions[0].Name)\n\t\tassert.Equal(t, rule.Actions[0].Order, ruleGet.Actions[0].Order)\n\t}\n\trule.Trigger = dataprovider.EventTriggerIDPLogin\n\tform.Set(\"trigger\", fmt.Sprintf(\"%d\", rule.Trigger))\n\tform.Set(\"idp_login_event\", \"1\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventRulePath, rule.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the rule\n\truleGet, _, err = httpdtest.GetEventRuleByName(rule.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, rule.Trigger, ruleGet.Trigger)\n\tassert.Equal(t, 1, ruleGet.Conditions.IDPLoginEvent)\n\n\tform.Set(\"idp_login_event\", \"2\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventRulePath, rule.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the rule\n\truleGet, _, err = httpdtest.GetEventRuleByName(rule.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, rule.Trigger, ruleGet.Trigger)\n\tassert.Equal(t, 2, ruleGet.Conditions.IDPLoginEvent)\n\n\t// update a missing rule\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventRulePath, rule.Name+\"1\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// update with no csrf token\n\tform.Del(csrfFormToken)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventRulePath, rule.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\tform.Set(csrfFormToken, csrfToken)\n\t// update with no action defined\n\tform.Del(\"actions[0][action_name]\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventRulePath, rule.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorRuleActionRequired)\n\t// invalid trigger\n\tform.Set(\"trigger\", \"a\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventRulePath, rule.Name),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventRulePath, rule.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventActionPath, action.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestWebIPListEntries(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, webToken)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webIPListPath+\"/mode\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webIPListPath+\"/mode/a\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webIPListPath, \"/1/a\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webIPListPath+\"/1\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webIPListsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tentry := dataprovider.IPListEntry{\n\t\tIPOrNet:     \"12.34.56.78/20\",\n\t\tType:        dataprovider.IPListTypeDefender,\n\t\tMode:        dataprovider.ListModeDeny,\n\t\tDescription: \"note\",\n\t\tProtocols:   5,\n\t}\n\tform := make(url.Values)\n\tform.Set(\"ipornet\", entry.IPOrNet)\n\tform.Set(\"description\", entry.Description)\n\tform.Set(\"mode\", \"a\")\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/mode\", bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError400Message)\n\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/1\", bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/2\", bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\tform.Set(\"mode\", \"2\")\n\tform.Set(\"protocols\", \"a\")\n\tform.Add(\"protocols\", \"1\")\n\tform.Add(\"protocols\", \"4\")\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/\"+strconv.Itoa(int(entry.Type)),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tentry1, _, err := httpdtest.GetIPListEntry(entry.IPOrNet, dataprovider.IPListTypeDefender, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, entry.Description, entry1.Description)\n\tassert.Equal(t, entry.Mode, entry1.Mode)\n\tassert.Equal(t, entry.Protocols, entry1.Protocols)\n\n\tform.Set(\"ipornet\", \"1111.11.11.11\")\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/1\", bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorIPInvalid)\n\n\tform.Set(\"ipornet\", entry.IPOrNet)\n\tform.Set(\"mode\", \"invalid\") // ignored for list type 1\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/1\", bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tentry2, _, err := httpdtest.GetIPListEntry(entry.IPOrNet, dataprovider.IPListTypeAllowList, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, entry.Description, entry2.Description)\n\tassert.Equal(t, dataprovider.ListModeAllow, entry2.Mode)\n\tassert.Equal(t, entry.Protocols, entry2.Protocols)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webIPListPath, \"1\", url.PathEscape(entry2.IPOrNet)), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"protocols\", \"1\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webIPListPath, \"1\", url.PathEscape(entry.IPOrNet)),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\tentry2, _, err = httpdtest.GetIPListEntry(entry.IPOrNet, dataprovider.IPListTypeAllowList, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, entry.Description, entry2.Description)\n\tassert.Equal(t, dataprovider.ListModeAllow, entry2.Mode)\n\tassert.Equal(t, 1, entry2.Protocols)\n\n\tform.Del(csrfFormToken)\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/1/\"+url.PathEscape(entry.IPOrNet),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/a/\"+url.PathEscape(entry.IPOrNet),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/1/\"+url.PathEscape(entry.IPOrNet)+\"a\",\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tform.Set(\"mode\", \"a\")\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/2/\"+url.PathEscape(entry.IPOrNet),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\tform.Set(\"mode\", \"100\")\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/2/\"+url.PathEscape(entry.IPOrNet),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\t_, err = httpdtest.RemoveIPListEntry(entry1, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveIPListEntry(entry2, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebRole(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webAdminRolePath, webToken)\n\tassert.NoError(t, err)\n\trole := getTestRole()\n\tform := make(url.Values)\n\tform.Set(\"name\", \"\")\n\tform.Set(\"description\", role.Description)\n\treq, err := http.NewRequest(http.MethodPost, webAdminRolePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminRolePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorNameRequired)\n\tform.Set(\"name\", role.Name)\n\treq, err = http.NewRequest(http.MethodPost, webAdminRolePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// a new add will fail\n\treq, err = http.NewRequest(http.MethodPost, webAdminRolePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// list roles\n\treq, err = http.NewRequest(http.MethodGet, webAdminRolesPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, webAdminRolesPath+jsonAPISuffix, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// render the new role page\n\treq, err = http.NewRequest(http.MethodGet, webAdminRolePath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminRolePath, role.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminRolePath, \"missing_role\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\t// parse form error\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminRolePath, role.Name)+\"?param=p%C4%AO%GH\",\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\t// update role\n\tform.Set(\"description\", \"new desc\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminRolePath, role.Name), bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check the changes\n\trole, _, err = httpdtest.GetRoleByName(role.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"new desc\", role.Description)\n\t// no CSRF token\n\tform.Set(csrfFormToken, \"\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminRolePath, role.Name), bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\t// missing role\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminRolePath, \"missing\"), bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestAddWebGroup(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webGroupPath, webToken)\n\tassert.NoError(t, err)\n\tgroup := getTestGroup()\n\tgroup.UserSettings = dataprovider.GroupUserSettings{\n\t\tBaseGroupUserSettings: sdk.BaseGroupUserSettings{\n\t\t\tHomeDir:           filepath.Join(os.TempDir(), util.GenerateUniqueID()),\n\t\t\tPermissions:       make(map[string][]string),\n\t\t\tMaxSessions:       2,\n\t\t\tQuotaSize:         123,\n\t\t\tQuotaFiles:        10,\n\t\t\tUploadBandwidth:   128,\n\t\t\tDownloadBandwidth: 256,\n\t\t\tExpiresIn:         10,\n\t\t},\n\t}\n\tform := make(url.Values)\n\tform.Set(\"name\", group.Name)\n\tform.Set(\"description\", group.Description)\n\tform.Set(\"home_dir\", group.UserSettings.HomeDir)\n\tb, contentType, err := getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(group.UserSettings.MaxSessions), 10))\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidQuotaSize)\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(group.UserSettings.QuotaFiles), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(group.UserSettings.QuotaSize, 10))\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"upload_bandwidth\", strconv.FormatInt(group.UserSettings.UploadBandwidth, 10))\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"download_bandwidth\", strconv.FormatInt(group.UserSettings.DownloadBandwidth, 10))\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"expires_in\", strconv.Itoa(group.UserSettings.ExpiresIn))\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidMaxFilesize)\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath+\"?b=%2\", &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr) // error parsing the multipart form\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// a new add will fail\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webGroupPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// list groups\n\treq, err = http.NewRequest(http.MethodGet, webGroupsPath, nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, webGroupsPath+jsonAPISuffix, nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// render the new group page\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webGroupPath, group.Name), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// check the added group\n\tgroupGet, _, err := httpdtest.GetGroupByName(group.Name, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, group.UserSettings, groupGet.UserSettings)\n\tassert.Equal(t, group.Name, groupGet.Name)\n\tassert.Equal(t, group.Description, groupGet.Description)\n\t// cleanup\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(groupPath, group.Name), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webGroupPath, group.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestAddWebFoldersMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webFolderPath, webToken)\n\tassert.NoError(t, err)\n\tmappedPath := filepath.Clean(os.TempDir())\n\tfolderName := filepath.Base(mappedPath)\n\tfolderDesc := \"a simple desc\"\n\tform := make(url.Values)\n\tform.Set(\"mapped_path\", mappedPath)\n\tform.Set(\"name\", folderName)\n\tform.Set(\"description\", folderDesc)\n\tform.Set(\"osfs_read_buffer_size\", \"3\")\n\tform.Set(\"osfs_write_buffer_size\", \"4\")\n\tb, contentType, err := getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, webFolderPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webFolderPath, &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// adding the same folder will fail since the name must be unique\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webFolderPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// invalid form\n\treq, err = http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode()))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", \"text/plain; boundary=\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// now render the add folder page\n\treq, err = http.NewRequest(http.MethodGet, webFolderPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tvar folder vfs.BaseVirtualFolder\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folder)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath, folder.MappedPath)\n\tassert.Equal(t, folderName, folder.Name)\n\tassert.Equal(t, folderDesc, folder.Description)\n\tassert.Equal(t, 3, folder.FsConfig.OSConfig.ReadBufferSize)\n\tassert.Equal(t, 4, folder.FsConfig.OSConfig.WriteBufferSize)\n\t// cleanup\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestHTTPFsWebFolderMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webFolderPath, webToken)\n\tassert.NoError(t, err)\n\tmappedPath := filepath.Clean(os.TempDir())\n\tfolderName := filepath.Base(mappedPath)\n\thttpfsConfig := vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint:      \"https://127.0.0.1:9998/api/v1\",\n\t\t\tUsername:      folderName,\n\t\t\tSkipTLSVerify: true,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\tAPIKey:   kms.NewPlainSecret(defaultTokenAuthPass),\n\t}\n\tform := make(url.Values)\n\tform.Set(\"mapped_path\", mappedPath)\n\tform.Set(\"name\", folderName)\n\tform.Set(\"fs_provider\", \"6\")\n\tform.Set(\"http_endpoint\", httpfsConfig.Endpoint)\n\tform.Set(\"http_username\", \"%name%\")\n\tform.Set(\"http_password\", httpfsConfig.Password.GetPayload())\n\tform.Set(\"http_api_key\", httpfsConfig.APIKey.GetPayload())\n\tform.Set(\"http_skip_tls_verify\", \"checked\")\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, err := getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, webFolderPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check\n\tvar folder vfs.BaseVirtualFolder\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folder)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath, folder.MappedPath)\n\tassert.Equal(t, folderName, folder.Name)\n\tassert.Equal(t, sdk.HTTPFilesystemProvider, folder.FsConfig.Provider)\n\tassert.Equal(t, httpfsConfig.Endpoint, folder.FsConfig.HTTPConfig.Endpoint)\n\tassert.Equal(t, httpfsConfig.Username, folder.FsConfig.HTTPConfig.Username)\n\tassert.Equal(t, httpfsConfig.SkipTLSVerify, folder.FsConfig.HTTPConfig.SkipTLSVerify)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.NotEmpty(t, folder.FsConfig.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, folder.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Empty(t, folder.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.HTTPConfig.APIKey.GetStatus())\n\tassert.NotEmpty(t, folder.FsConfig.HTTPConfig.APIKey.GetPayload())\n\tassert.Empty(t, folder.FsConfig.HTTPConfig.APIKey.GetKey())\n\tassert.Empty(t, folder.FsConfig.HTTPConfig.APIKey.GetAdditionalData())\n\t// update\n\tform.Set(\"http_password\", redactedSecret)\n\tform.Set(\"http_api_key\", redactedSecret)\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\t// check\n\tvar updateFolder vfs.BaseVirtualFolder\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &updateFolder)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath, updateFolder.MappedPath)\n\tassert.Equal(t, folderName, updateFolder.Name)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateFolder.FsConfig.HTTPConfig.Password.GetStatus())\n\tassert.Equal(t, folder.FsConfig.HTTPConfig.Password.GetPayload(), updateFolder.FsConfig.HTTPConfig.Password.GetPayload())\n\tassert.Empty(t, updateFolder.FsConfig.HTTPConfig.Password.GetKey())\n\tassert.Empty(t, updateFolder.FsConfig.HTTPConfig.Password.GetAdditionalData())\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, updateFolder.FsConfig.HTTPConfig.APIKey.GetStatus())\n\tassert.Equal(t, folder.FsConfig.HTTPConfig.APIKey.GetPayload(), updateFolder.FsConfig.HTTPConfig.APIKey.GetPayload())\n\tassert.Empty(t, updateFolder.FsConfig.HTTPConfig.APIKey.GetKey())\n\tassert.Empty(t, updateFolder.FsConfig.HTTPConfig.APIKey.GetAdditionalData())\n\n\t// cleanup\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestS3WebFolderMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webFolderPath, webToken)\n\tassert.NoError(t, err)\n\tmappedPath := filepath.Clean(os.TempDir())\n\tfolderName := filepath.Base(mappedPath)\n\tfolderDesc := \"a simple desc\"\n\tS3Bucket := \"test\"\n\tS3Region := \"eu-west-1\"\n\tS3AccessKey := \"access-key\"\n\tS3AccessSecret := kms.NewPlainSecret(\"folder-access-secret\")\n\tS3SSEKey := kms.NewPlainSecret(\"folder-sse-key\")\n\tS3SessionToken := \"fake session token\"\n\tS3RoleARN := \"arn:aws:iam::123456789012:user/Development/product_1234/*\"\n\tS3Endpoint := \"http://127.0.0.1:9000/path?b=c\"\n\tS3StorageClass := \"Standard\"\n\tS3ACL := \"public-read-write\"\n\tS3KeyPrefix := \"somedir/subdir/\"\n\tS3UploadPartSize := 5\n\tS3UploadConcurrency := 4\n\tS3MaxPartDownloadTime := 120\n\tS3MaxPartUploadTime := 60\n\tS3DownloadPartSize := 6\n\tS3DownloadConcurrency := 3\n\tform := make(url.Values)\n\tform.Set(\"mapped_path\", mappedPath)\n\tform.Set(\"name\", folderName)\n\tform.Set(\"description\", folderDesc)\n\tform.Set(\"fs_provider\", \"1\")\n\tform.Set(\"s3_bucket\", S3Bucket)\n\tform.Set(\"s3_region\", S3Region)\n\tform.Set(\"s3_access_key\", S3AccessKey)\n\tform.Set(\"s3_access_secret\", S3AccessSecret.GetPayload())\n\tform.Set(\"s3_sse_customer_key\", S3SSEKey.GetPayload())\n\tform.Set(\"s3_session_token\", S3SessionToken)\n\tform.Set(\"s3_role_arn\", S3RoleARN)\n\tform.Set(\"s3_storage_class\", S3StorageClass)\n\tform.Set(\"s3_acl\", S3ACL)\n\tform.Set(\"s3_endpoint\", S3Endpoint)\n\tform.Set(\"s3_key_prefix\", S3KeyPrefix)\n\tform.Set(\"s3_upload_part_size\", strconv.Itoa(S3UploadPartSize))\n\tform.Set(\"s3_download_part_max_time\", strconv.Itoa(S3MaxPartDownloadTime))\n\tform.Set(\"s3_download_part_size\", strconv.Itoa(S3DownloadPartSize))\n\tform.Set(\"s3_download_concurrency\", strconv.Itoa(S3DownloadConcurrency))\n\tform.Set(\"s3_upload_part_max_time\", strconv.Itoa(S3MaxPartUploadTime))\n\tform.Set(\"s3_upload_concurrency\", \"a\")\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, err := getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, webFolderPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"s3_upload_concurrency\", strconv.Itoa(S3UploadConcurrency))\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webFolderPath, &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tvar folder vfs.BaseVirtualFolder\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folder)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath, folder.MappedPath)\n\tassert.Equal(t, folderName, folder.Name)\n\tassert.Equal(t, folderDesc, folder.Description)\n\tassert.Equal(t, sdk.S3FilesystemProvider, folder.FsConfig.Provider)\n\tassert.Equal(t, S3Bucket, folder.FsConfig.S3Config.Bucket)\n\tassert.Equal(t, S3Region, folder.FsConfig.S3Config.Region)\n\tassert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey)\n\tassert.NotEmpty(t, folder.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.NotEmpty(t, folder.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\tassert.Equal(t, S3Endpoint, folder.FsConfig.S3Config.Endpoint)\n\tassert.Equal(t, S3StorageClass, folder.FsConfig.S3Config.StorageClass)\n\tassert.Equal(t, S3ACL, folder.FsConfig.S3Config.ACL)\n\tassert.Equal(t, S3KeyPrefix, folder.FsConfig.S3Config.KeyPrefix)\n\tassert.Equal(t, S3UploadConcurrency, folder.FsConfig.S3Config.UploadConcurrency)\n\tassert.Equal(t, int64(S3UploadPartSize), folder.FsConfig.S3Config.UploadPartSize)\n\tassert.Equal(t, S3MaxPartDownloadTime, folder.FsConfig.S3Config.DownloadPartMaxTime)\n\tassert.Equal(t, S3MaxPartUploadTime, folder.FsConfig.S3Config.UploadPartMaxTime)\n\tassert.Equal(t, S3DownloadConcurrency, folder.FsConfig.S3Config.DownloadConcurrency)\n\tassert.Equal(t, int64(S3DownloadPartSize), folder.FsConfig.S3Config.DownloadPartSize)\n\tassert.False(t, folder.FsConfig.S3Config.ForcePathStyle)\n\tassert.False(t, folder.FsConfig.S3Config.SkipTLSVerify)\n\t// update\n\tS3UploadConcurrency = 10\n\tform.Set(\"s3_upload_concurrency\", \"b\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tform.Set(\"s3_upload_concurrency\", strconv.Itoa(S3UploadConcurrency))\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\tfolder = vfs.BaseVirtualFolder{}\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folder)\n\tassert.NoError(t, err)\n\tassert.Equal(t, mappedPath, folder.MappedPath)\n\tassert.Equal(t, folderName, folder.Name)\n\tassert.Equal(t, folderDesc, folder.Description)\n\tassert.Equal(t, sdk.S3FilesystemProvider, folder.FsConfig.Provider)\n\tassert.Equal(t, S3Bucket, folder.FsConfig.S3Config.Bucket)\n\tassert.Equal(t, S3Region, folder.FsConfig.S3Config.Region)\n\tassert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey)\n\tassert.Equal(t, S3RoleARN, folder.FsConfig.S3Config.RoleARN)\n\tassert.NotEmpty(t, folder.FsConfig.S3Config.AccessSecret.GetPayload())\n\tassert.NotEmpty(t, folder.FsConfig.S3Config.SSECustomerKey.GetPayload())\n\tassert.Equal(t, S3Endpoint, folder.FsConfig.S3Config.Endpoint)\n\tassert.Equal(t, S3StorageClass, folder.FsConfig.S3Config.StorageClass)\n\tassert.Equal(t, S3KeyPrefix, folder.FsConfig.S3Config.KeyPrefix)\n\tassert.Equal(t, S3UploadConcurrency, folder.FsConfig.S3Config.UploadConcurrency)\n\tassert.Equal(t, int64(S3UploadPartSize), folder.FsConfig.S3Config.UploadPartSize)\n\n\t// cleanup\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestUpdateWebGroupMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webGroupPath, webToken)\n\tassert.NoError(t, err)\n\tgroup, _, err := httpdtest.AddGroup(getTestGroup(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tgroup.UserSettings = dataprovider.GroupUserSettings{\n\t\tBaseGroupUserSettings: sdk.BaseGroupUserSettings{\n\t\t\tHomeDir:     filepath.Join(os.TempDir(), util.GenerateUniqueID()),\n\t\t\tPermissions: make(map[string][]string),\n\t\t},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint:   sftpServerAddr,\n\t\t\t\t\tUsername:   defaultUsername,\n\t\t\t\t\tBufferSize: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tform := make(url.Values)\n\tform.Set(\"name\", group.Name)\n\tform.Set(\"description\", group.Description)\n\tform.Set(\"home_dir\", group.UserSettings.HomeDir)\n\tform.Set(\"max_sessions\", strconv.FormatInt(int64(group.UserSettings.MaxSessions), 10))\n\tform.Set(\"quota_files\", strconv.FormatInt(int64(group.UserSettings.QuotaFiles), 10))\n\tform.Set(\"quota_size\", strconv.FormatInt(group.UserSettings.QuotaSize, 10))\n\tform.Set(\"upload_bandwidth\", strconv.FormatInt(group.UserSettings.UploadBandwidth, 10))\n\tform.Set(\"download_bandwidth\", strconv.FormatInt(group.UserSettings.DownloadBandwidth, 10))\n\tform.Set(\"upload_data_transfer\", \"0\")\n\tform.Set(\"download_data_transfer\", \"0\")\n\tform.Set(\"total_data_transfer\", \"0\")\n\tform.Set(\"max_upload_file_size\", \"0\")\n\tform.Set(\"default_shares_expiration\", \"0\")\n\tform.Set(\"max_shares_expiration\", \"0\")\n\tform.Set(\"expires_in\", \"0\")\n\tform.Set(\"password_expiration\", \"0\")\n\tform.Set(\"password_strength\", \"0\")\n\tform.Set(\"external_auth_cache_time\", \"0\")\n\tform.Set(\"fs_provider\", strconv.FormatInt(int64(group.UserSettings.FsConfig.Provider), 10))\n\tform.Set(\"sftp_endpoint\", group.UserSettings.FsConfig.SFTPConfig.Endpoint)\n\tform.Set(\"sftp_username\", group.UserSettings.FsConfig.SFTPConfig.Username)\n\tb, contentType, err := getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, path.Join(webGroupPath, group.Name), &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\tform.Set(\"sftp_buffer_size\", strconv.FormatInt(group.UserSettings.FsConfig.SFTPConfig.BufferSize, 10))\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webGroupPath, group.Name), &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webGroupPath, group.Name), &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorFsCredentialsRequired)\n\n\tform.Set(\"sftp_password\", defaultPassword)\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webGroupPath, group.Name), &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\treq, err = http.NewRequest(http.MethodDelete, path.Join(groupPath, group.Name), nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webGroupPath, group.Name), &b)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", contentType)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestUpdateWebFolderMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webFolderPath, webToken)\n\tassert.NoError(t, err)\n\tfolderName := \"vfolderupdate\"\n\tfolderDesc := \"updated desc\"\n\tfolder := vfs.BaseVirtualFolder{\n\t\tName:        folderName,\n\t\tMappedPath:  filepath.Join(os.TempDir(), \"folderupdate\"),\n\t\tDescription: \"dsc\",\n\t}\n\t_, _, err = httpdtest.AddFolder(folder, http.StatusCreated)\n\tnewMappedPath := folder.MappedPath + \"1\"\n\tassert.NoError(t, err)\n\tform := make(url.Values)\n\tform.Set(\"mapped_path\", newMappedPath)\n\tform.Set(\"name\", folderName)\n\tform.Set(\"description\", folderDesc)\n\tform.Set(csrfFormToken, \"\")\n\tb, contentType, err := getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\tform.Set(csrfFormToken, csrfToken)\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusSeeOther, rr)\n\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)\n\tsetBearerForReq(req, apiToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\terr = render.DecodeJSON(rr.Body, &folder)\n\tassert.NoError(t, err)\n\tassert.Equal(t, newMappedPath, folder.MappedPath)\n\tassert.Equal(t, folderName, folder.Name)\n\tassert.Equal(t, folderDesc, folder.Description)\n\n\t// parse form error\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName)+\"??a=a%B3%A2%G3\", &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName+\"1\"), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tform.Set(\"mapped_path\", \"arelative/path\")\n\tb, contentType, err = getMultipartFormData(form, \"\", \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\treq.Header.Set(\"Content-Type\", contentType)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// render update folder page\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webFolderPath, folderName), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webFolderPath, folderName+\"1\"), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webFolderPath, folderName), nil)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token\")\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webFolderPath, folderName), nil)\n\tsetJWTCookieForReq(req, apiToken) // api token is not accepted\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusFound, rr)\n\tassert.Equal(t, webLoginPath, rr.Header().Get(\"Location\"))\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webFolderPath, folderName), nil)\n\tsetJWTCookieForReq(req, webToken)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestWebFoldersMock(t *testing.T) {\n\twebToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tapiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vfolder1\")\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vfolder2\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tfolderName2 := filepath.Base(mappedPath2)\n\tfolderDesc1 := \"vfolder1 desc\"\n\tfolderDesc2 := \"vfolder2 desc\"\n\tfolders := []vfs.BaseVirtualFolder{\n\t\t{\n\t\t\tName:        folderName1,\n\t\t\tMappedPath:  mappedPath1,\n\t\t\tDescription: folderDesc1,\n\t\t},\n\t\t{\n\t\t\tName:        folderName2,\n\t\t\tMappedPath:  mappedPath2,\n\t\t\tDescription: folderDesc2,\n\t\t},\n\t}\n\tfor _, folder := range folders {\n\t\tfolderAsJSON, err := json.Marshal(folder)\n\t\tassert.NoError(t, err)\n\t\treq, err := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON))\n\t\tassert.NoError(t, err)\n\t\tsetBearerForReq(req, apiToken)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusCreated, rr)\n\t}\n\n\treq, err := http.NewRequest(http.MethodGet, folderPath, nil)\n\tassert.NoError(t, err)\n\tsetBearerForReq(req, apiToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tvar foldersGet []vfs.BaseVirtualFolder\n\terr = render.DecodeJSON(rr.Body, &foldersGet)\n\tassert.NoError(t, err)\n\tnumFound := 0\n\tfor _, f := range foldersGet {\n\t\tif f.Name == folderName1 {\n\t\t\tassert.Equal(t, mappedPath1, f.MappedPath)\n\t\t\tassert.Equal(t, folderDesc1, f.Description)\n\t\t\tnumFound++\n\t\t}\n\t\tif f.Name == folderName2 {\n\t\t\tassert.Equal(t, mappedPath2, f.MappedPath)\n\t\t\tassert.Equal(t, folderDesc2, f.Description)\n\t\t\tnumFound++\n\t\t}\n\t}\n\tassert.Equal(t, 2, numFound)\n\n\treq, err = http.NewRequest(http.MethodGet, webFoldersPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\treq, err = http.NewRequest(http.MethodGet, webFoldersPath+jsonAPISuffix, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, webToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tfor _, folder := range folders {\n\t\treq, _ := http.NewRequest(http.MethodDelete, path.Join(folderPath, folder.Name), nil)\n\t\tsetBearerForReq(req, apiToken)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t}\n}\n\nfunc TestAdminForgotPassword(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          3525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\ta.Filters.RequirePasswordChange = true\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webAdminForgotPwdPath, nil)\n\tassert.NoError(t, err)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminResetPwdPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webLoginPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\n\tform := make(url.Values)\n\tform.Set(\"username\", \"\")\n\t// no csrf token\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\t// empty username\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorUsernameRequired)\n\n\tlastResetCode = \"\"\n\tform.Set(\"username\", altAdminUsername)\n\t// disable the admin\n\tadmin.Status = 0\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Len(t, lastResetCode, 0)\n\n\tadmin.Status = 1\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\n\tform = make(url.Values)\n\treq, err = http.NewRequest(http.MethodPost, webAdminResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\t// no password\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)\n\t// no code\n\tform.Set(\"password\", defaultPassword)\n\tform.Set(\"confirm_password\", defaultPassword)\n\treq, err = http.NewRequest(http.MethodPost, webAdminResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)\n\t// disable the admin\n\tadmin.Status = 0\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\tform.Set(\"code\", lastResetCode)\n\treq, err = http.NewRequest(http.MethodPost, webAdminResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)\n\n\tadmin.Status = 1\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t// ok\n\treq, err = http.NewRequest(http.MethodPost, webAdminResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", altAdminUsername)\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\n\t// not working smtp server\n\tsmtpCfg = smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          3526,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tform = make(url.Values)\n\tform.Set(\"username\", altAdminUsername)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPwdResetSendEmail)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tform.Set(\"username\", altAdminUsername)\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPwdResetGeneric)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminForgotPwdPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminResetPwdPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\tadmin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, admin.Filters.RequirePasswordChange)\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserForgotPassword(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          3525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\tu := getTestUser()\n\tu.Email = \"user@test.com\"\n\tu.Filters.WebClient = []string{sdk.WebClientPasswordResetDisabled}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webClientForgotPwdPath, nil)\n\tassert.NoError(t, err)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientResetPwdPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientLoginPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\n\tform := make(url.Values)\n\tform.Set(\"username\", \"\")\n\t// no csrf token\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\t// empty username\n\tform.Set(csrfFormToken, csrfToken)\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorUsernameRequired)\n\t// user cannot reset the password\n\tform.Set(\"username\", user.Username)\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorPwdResetForbidded)\n\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(-1 * time.Hour))\n\tuser.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t// user is expired\n\tlastResetCode = \"\"\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Len(t, lastResetCode, 0)\n\n\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour))\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\t// no login token\n\tform = make(url.Values)\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\t// no password\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"password\", \"\")\n\tform.Set(\"confirm_password\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)\n\t// passwords mismatch\n\tform.Set(\"password\", altAdminPassword)\n\tform.Set(\"code\", lastResetCode)\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdNoMatch)\n\t// no code\n\tform.Del(\"code\")\n\tform.Set(\"confirm_password\", altAdminPassword)\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)\n\t// Invalid login condition\n\tform.Set(\"code\", lastResetCode)\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolHTTP}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)\n\t// ok\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolFTP}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\n\tloginCookie, csrfToken, err = getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tassert.NoError(t, err)\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", user.Username)\n\tlastResetCode = \"\"\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientForgotPwdPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientResetPwdPath, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t// user does not exist anymore\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"code\", lastResetCode)\n\tform.Set(\"password\", \"pwd\")\n\tform.Set(\"confirm_password\", \"pwd\")\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)\n}\n\nfunc TestAPIForgotPassword(t *testing.T) {\n\tsmtpCfg := smtp.Config{\n\t\tHost:          \"127.0.0.1\",\n\t\tPort:          3525,\n\t\tFrom:          \"notification@example.com\",\n\t\tTemplatesPath: \"templates\",\n\t}\n\terr := smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\ta.Email = \"\"\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// no email, forgot pwd will not work\n\tlastResetCode = \"\"\n\treq, err := http.NewRequest(http.MethodPost, path.Join(adminPath, altAdminUsername, \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Your account does not have an email address\")\n\n\tadmin.Email = \"admin@test.com\"\n\tadmin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, altAdminUsername, \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\n\t// invalid JSON\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, altAdminUsername, \"/reset-password\"), bytes.NewBuffer([]byte(`{`)))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\n\tresetReq := make(map[string]string)\n\tresetReq[\"code\"] = lastResetCode\n\tresetReq[\"password\"] = defaultPassword\n\tasJSON, err := json.Marshal(resetReq)\n\tassert.NoError(t, err)\n\n\t// a user cannot use an admin code\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, defaultUsername, \"/reset-password\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"invalid confirmation code\")\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, altAdminUsername, \"/reset-password\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\t// the same code cannot be reused\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, altAdminUsername, \"/reset-password\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"confirmation code not found\")\n\n\tadmin, err = dataprovider.AdminExists(altAdminUsername)\n\tassert.NoError(t, err)\n\n\tmatch, err := admin.CheckPassword(defaultPassword)\n\tassert.NoError(t, err)\n\tassert.True(t, match)\n\tlastResetCode = \"\"\n\t// now the same for a user\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, defaultUsername, \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"Your account does not have an email address\")\n\n\tuser.Email = \"user@test.com\"\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, defaultUsername, \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\n\t// invalid JSON\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, defaultUsername, \"/reset-password\"), bytes.NewBuffer([]byte(`{`)))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\t// remove the reset password permission\n\tuser.Filters.WebClient = []string{sdk.WebClientPasswordResetDisabled}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tresetReq[\"code\"] = lastResetCode\n\tresetReq[\"password\"] = altAdminPassword\n\tasJSON, err = json.Marshal(resetReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, defaultUsername, \"/reset-password\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"you are not allowed to reset your password\")\n\n\tuser.Filters.WebClient = []string{sdk.WebClientSharesDisabled}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, defaultUsername, \"/reset-password\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\t// the same code cannot be reused\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, defaultUsername, \"/reset-password\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"confirmation code not found\")\n\n\tuser, err = dataprovider.UserExists(defaultUsername, \"\")\n\tassert.NoError(t, err)\n\terr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(altAdminPassword))\n\tassert.NoError(t, err)\n\n\tlastResetCode = \"\"\n\t// a request for a missing admin/user will be silently ignored\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, \"missing-admin\", \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Empty(t, lastResetCode)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, \"missing-user\", \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.Empty(t, lastResetCode)\n\n\tlastResetCode = \"\"\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, altAdminUsername, \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\tassert.GreaterOrEqual(t, len(lastResetCode), 20)\n\n\tsmtpCfg = smtp.Config{}\n\terr = smtpCfg.Initialize(configDir, true)\n\trequire.NoError(t, err)\n\n\t// without an smtp configuration reset password is not available\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, altAdminUsername, \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"No SMTP configuration\")\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(userPath, defaultUsername, \"/forgot-password\"), nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"No SMTP configuration\")\n\n\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\tassert.NoError(t, err)\n\t// the admin does not exist anymore\n\tresetReq[\"code\"] = lastResetCode\n\tresetReq[\"password\"] = altAdminPassword\n\tasJSON, err = json.Marshal(resetReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, altAdminUsername, \"/reset-password\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusBadRequest, rr)\n\tassert.Contains(t, rr.Body.String(), \"unable to associate the confirmation code with an existing admin\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestProviderClosedMock(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webConfigsPath, token)\n\tassert.NoError(t, err)\n\t// create a role admin\n\trole, resp, err := httpdtest.AddRole(getTestRole(), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\ta := getTestAdmin()\n\ta.Username = altAdminUsername\n\ta.Password = altAdminPassword\n\ta.Role = role.Name\n\ta.Permissions = []string{dataprovider.PermAdminAddUsers, dataprovider.PermAdminChangeUsers,\n\t\tdataprovider.PermAdminDeleteUsers, dataprovider.PermAdminViewUsers}\n\tadmin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)\n\tassert.NoError(t, err)\n\taltToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)\n\tassert.NoError(t, err)\n\n\tdataprovider.Close()\n\n\ttestReq := make(map[string]any)\n\ttestReq[\"password\"] = redactedSecret\n\tasJSON, err := json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, path.Join(webConfigsPath, \"smtp\", \"test\"), bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\ttestReq[\"base_redirect_url\"] = \"http://localhost\"\n\ttestReq[\"client_secret\"] = redactedSecret\n\tasJSON, err = json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webOAuth2TokenPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webConfigsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webConfigsPath, nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\tgetJSONFolders := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, _ := http.NewRequest(http.MethodGet, webFoldersPath+jsonAPISuffix, nil)\n\t\tsetJWTCookieForReq(req, token)\n\t\texecuteRequest(req)\n\t}\n\tgetJSONFolders()\n\n\tgetJSONGroups := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, _ := http.NewRequest(http.MethodGet, webGroupsPath+jsonAPISuffix, nil)\n\t\tsetJWTCookieForReq(req, token)\n\t\texecuteRequest(req)\n\t}\n\tgetJSONGroups()\n\n\tgetJSONUsers := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, _ := http.NewRequest(http.MethodGet, webUsersPath+jsonAPISuffix, nil)\n\t\tsetJWTCookieForReq(req, token)\n\t\texecuteRequest(req)\n\t}\n\tgetJSONUsers()\n\n\treq, _ = http.NewRequest(http.MethodGet, webUserPath+\"/0\", nil)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", \"test\")\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath+\"/0\", strings.NewReader(form.Encode()))\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(webAdminPath, defaultTokenAuthUser), nil)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), strings.NewReader(form.Encode()))\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\tgetJSONAdmins := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, _ := http.NewRequest(http.MethodGet, webAdminsPath+jsonAPISuffix, nil)\n\t\tsetJWTCookieForReq(req, token)\n\t\texecuteRequest(req)\n\t}\n\tgetJSONAdmins()\n\n\treq, _ = http.NewRequest(http.MethodGet, path.Join(webFolderPath, defaultTokenAuthUser), nil)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webFolderPath, defaultTokenAuthUser), strings.NewReader(form.Encode()))\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))\n\tsetJWTCookieForReq(req, altToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, webIPListPath+\"/1/a\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, webIPListPath+\"/1/a\", nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\tgetJSONRoles := func() {\n\t\tdefer func() {\n\t\t\trcv := recover()\n\t\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t\t}()\n\t\treq, err := http.NewRequest(http.MethodGet, webAdminRolesPath+jsonAPISuffix, nil)\n\t\tassert.NoError(t, err)\n\t\tsetJWTCookieForReq(req, token)\n\t\texecuteRequest(req)\n\t}\n\tgetJSONRoles()\n\n\treq, err = http.NewRequest(http.MethodGet, path.Join(webAdminRolePath, role.Name), nil)\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminRolePath, role.Name), strings.NewReader(form.Encode()))\n\tassert.NoError(t, err)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusInternalServerError, rr)\n\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.BackupsPath = backupsPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tif config.GetProviderConf().Driver != dataprovider.MemoryDataProviderName {\n\t\t_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.RemoveRole(role, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestWebConnectionsMock(t *testing.T) {\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, webConnectionsPath, nil)\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webConnectionsPath, \"id\"), nil)\n\tsetJWTCookieForReq(req, token)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token\")\n\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webConnectionsPath, \"id\"), nil)\n\tsetJWTCookieForReq(req, token)\n\tsetCSRFHeaderForReq(req, \"csrfToken\")\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusForbidden, rr)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token\")\n\n\tcsrfToken, err := getCSRFTokenFromInternalPageMock(webUserPath, token)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(webConnectionsPath, \"id\"), nil)\n\tsetJWTCookieForReq(req, token)\n\tsetCSRFHeaderForReq(req, csrfToken)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n}\n\nfunc TestGetWebStatusMock(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.RateLimitersConfig = []common.RateLimiterConfig{\n\t\t{\n\t\t\tAverage:   1,\n\t\t\tPeriod:    1000,\n\t\t\tBurst:     1,\n\t\t\tType:      1,\n\t\t\tProtocols: []string{common.ProtocolFTP},\n\t\t},\n\t}\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\ttoken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodGet, webStatusPath, nil)\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestStaticFilesMock(t *testing.T) {\n\treq, err := http.NewRequest(http.MethodGet, \"/static/favicon.png\", nil)\n\tassert.NoError(t, err)\n\trr := executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, \"/openapi/openapi.yaml\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, \"/static\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusMovedPermanently, rr)\n\tlocation := rr.Header().Get(\"Location\")\n\tassert.Equal(t, \"/static/\", location)\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusNotFound, rr)\n\n\treq, err = http.NewRequest(http.MethodGet, \"/openapi\", nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusMovedPermanently, rr)\n\tlocation = rr.Header().Get(\"Location\")\n\tassert.Equal(t, \"/openapi/\", location)\n\treq, err = http.NewRequest(http.MethodGet, location, nil)\n\tassert.NoError(t, err)\n\trr = executeRequest(req)\n\tcheckResponseCode(t, http.StatusOK, rr)\n}\n\nfunc TestPasswordChangeRequired(t *testing.T) {\n\tuser := getTestUser()\n\tassert.False(t, user.MustChangePassword())\n\tuser.Filters.RequirePasswordChange = true\n\tassert.True(t, user.MustChangePassword())\n\tuser.Filters.RequirePasswordChange = false\n\tassert.False(t, user.MustChangePassword())\n\tuser.Filters.PasswordExpiration = 2\n\tuser.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now())\n\tassert.False(t, user.MustChangePassword())\n\tuser.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now().Add(49 * time.Hour))\n\tassert.False(t, user.MustChangePassword())\n\tuser.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now().Add(-49 * time.Hour))\n\tassert.True(t, user.MustChangePassword())\n}\n\nfunc TestPasswordExpiresIn(t *testing.T) {\n\tuser := getTestUser()\n\tuser.Filters.PasswordExpiration = 30\n\tuser.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now().Add(-15*24*time.Hour + 1*time.Hour))\n\tres := user.PasswordExpiresIn()\n\tassert.Equal(t, 15, res)\n\tuser.Filters.PasswordExpiration = 15\n\tres = user.PasswordExpiresIn()\n\tassert.Equal(t, 1, res)\n\tuser.LastPasswordChange = util.GetTimeAsMsSinceEpoch(time.Now().Add(-15*24*time.Hour - 1*time.Hour))\n\tres = user.PasswordExpiresIn()\n\tassert.Equal(t, 0, res)\n\tuser.Filters.PasswordExpiration = 5\n\tres = user.PasswordExpiresIn()\n\tassert.Equal(t, -10, res)\n}\n\nfunc TestSecondFactorRequirements(t *testing.T) {\n\tuser := getTestUser()\n\tuser.Filters.TwoFactorAuthProtocols = []string{common.ProtocolHTTP, common.ProtocolSSH}\n\tassert.True(t, user.MustSetSecondFactor())\n\tassert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolFTP))\n\tassert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolHTTP))\n\tassert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolSSH))\n\n\tuser.Filters.TOTPConfig.Enabled = true\n\tassert.True(t, user.MustSetSecondFactor())\n\tassert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolFTP))\n\tassert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolHTTP))\n\tassert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolSSH))\n\n\tuser.Filters.TOTPConfig.Protocols = []string{common.ProtocolHTTP}\n\tassert.True(t, user.MustSetSecondFactor())\n\tassert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolFTP))\n\tassert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolHTTP))\n\tassert.True(t, user.MustSetSecondFactorForProtocol(common.ProtocolSSH))\n\n\tuser.Filters.TOTPConfig.Protocols = []string{common.ProtocolHTTP, common.ProtocolSSH}\n\tassert.False(t, user.MustSetSecondFactor())\n\tassert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolFTP))\n\tassert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolHTTP))\n\tassert.False(t, user.MustSetSecondFactorForProtocol(common.ProtocolSSH))\n}\n\nfunc TestIsNameValid(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected bool\n\t}{\n\t\t{\"simple name\", \"user\", true},\n\t\t{\"alphanumeric\", \"User123\", true},\n\t\t{\"unicode allowed\", \"你好\", true},\n\t\t{\"emoji allowed\", \"user😊\", true},\n\t\t{\"name with dot\", \"file.txt\", true},\n\t\t{\"name with multiple dots\", \"archive.tar.gz\", true},\n\t\t{\"control char\", \"abc\\u0001\", false},\n\t\t{\"newline\", \"abc\\n\", false},\n\t\t{\"tab\", \"abc\\t\", false},\n\t\t{\"slash\", \"user/name\", false},\n\t\t{\"backslash\", \"user\\\\name\", false},\n\t\t{\"colon\", \"user:name\", false},\n\t\t{\"single dot\", \".\", false},\n\t\t{\"double dot\", \"..\", false},\n\t\t{\"dot with suffix allowed\", \".hidden\", true},\n\t\t{\"name ending with dot\", \"file.\", false},\n\t\t{\"name ending with space\", \"file \", false},\n\t\t{\"CON\", \"CON\", false},\n\t\t{\"con lowercase\", \"con\", false},\n\t\t{\"con with extension\", \"con.txt\", false},\n\t\t{\"LPT1\", \"LPT1\", false},\n\t\t{\"lpt1 lowercase\", \"lpt1\", false},\n\t\t{\"COM5 uppercase\", \"COM5\", false},\n\t\t{\"com9 with extension\", \"com9.log\", false},\n\t\t{\"NUL\", \"NUL\", false},\n\t\t{\"Valid because suffix changes base\", \"con123\", true},\n\t\t{\"base name split\", \"aux.pdf\", false},\n\t\t{\"valid long name\", \"auxiliary\", true},\n\t\t{\"space only\", \" \", false},\n\t\t{\"dot inside\", \"ab.cd.ef\", true},\n\t\t{\"unicode that ends with dot\", \"你好.\", false},\n\t\t{\"unicode that ends with space\", \"你好 \", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := util.IsNameValid(tt.input)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"IsNameValid(%q) = %v, expected %v\", tt.input, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc startOIDCMockServer() {\n\tgo func() {\n\t\thttp.HandleFunc(\"/\", func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tfmt.Fprintf(w, \"OK\\n\")\n\t\t})\n\t\thttp.HandleFunc(\"/auth/realms/sftpgo/.well-known/openid-configuration\", func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tfmt.Fprintf(w, `{\"issuer\":\"http://127.0.0.1:11111/auth/realms/sftpgo\",\"authorization_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/auth\",\"token_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/token\",\"introspection_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/token/introspect\",\"userinfo_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/userinfo\",\"end_session_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/logout\",\"frontchannel_logout_session_supported\":true,\"frontchannel_logout_supported\":true,\"jwks_uri\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/certs\",\"check_session_iframe\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/login-status-iframe.html\",\"grant_types_supported\":[\"authorization_code\",\"implicit\",\"refresh_token\",\"password\",\"client_credentials\",\"urn:ietf:params:oauth:grant-type:device_code\",\"urn:openid:params:grant-type:ciba\"],\"response_types_supported\":[\"code\",\"none\",\"id_token\",\"token\",\"id_token token\",\"code id_token\",\"code token\",\"code id_token token\"],\"subject_types_supported\":[\"public\",\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"id_token_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"id_token_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"userinfo_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\",\"none\"],\"request_object_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\",\"none\"],\"request_object_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"request_object_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\",\"query.jwt\",\"fragment.jwt\",\"form_post.jwt\",\"jwt\"],\"registration_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/clients-registrations/openid-connect\",\"token_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_secret_basic\",\"client_secret_post\",\"tls_client_auth\",\"client_secret_jwt\"],\"token_endpoint_auth_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"introspection_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_secret_basic\",\"client_secret_post\",\"tls_client_auth\",\"client_secret_jwt\"],\"introspection_endpoint_auth_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"authorization_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"authorization_encryption_alg_values_supported\":[\"RSA-OAEP\",\"RSA-OAEP-256\",\"RSA1_5\"],\"authorization_encryption_enc_values_supported\":[\"A256GCM\",\"A192GCM\",\"A128GCM\",\"A128CBC-HS256\",\"A192CBC-HS384\",\"A256CBC-HS512\"],\"claims_supported\":[\"aud\",\"sub\",\"iss\",\"auth_time\",\"name\",\"given_name\",\"family_name\",\"preferred_username\",\"email\",\"acr\"],\"claim_types_supported\":[\"normal\"],\"claims_parameter_supported\":true,\"scopes_supported\":[\"openid\",\"phone\",\"email\",\"web-origins\",\"offline_access\",\"microprofile-jwt\",\"profile\",\"address\",\"roles\"],\"request_parameter_supported\":true,\"request_uri_parameter_supported\":true,\"require_request_uri_registration\":true,\"code_challenge_methods_supported\":[\"plain\",\"S256\"],\"tls_client_certificate_bound_access_tokens\":true,\"revocation_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/revoke\",\"revocation_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_secret_basic\",\"client_secret_post\",\"tls_client_auth\",\"client_secret_jwt\"],\"revocation_endpoint_auth_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"HS256\",\"HS512\",\"ES256\",\"RS256\",\"HS384\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"backchannel_logout_supported\":true,\"backchannel_logout_session_supported\":true,\"device_authorization_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/auth/device\",\"backchannel_token_delivery_modes_supported\":[\"poll\",\"ping\"],\"backchannel_authentication_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/ext/ciba/auth\",\"backchannel_authentication_request_signing_alg_values_supported\":[\"PS384\",\"ES384\",\"RS384\",\"ES256\",\"RS256\",\"ES512\",\"PS256\",\"PS512\",\"RS512\"],\"require_pushed_authorization_requests\":false,\"pushed_authorization_request_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/ext/par/request\",\"mtls_endpoint_aliases\":{\"token_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/token\",\"revocation_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/revoke\",\"introspection_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/token/introspect\",\"device_authorization_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/auth/device\",\"registration_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/clients-registrations/openid-connect\",\"userinfo_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/userinfo\",\"pushed_authorization_request_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/ext/par/request\",\"backchannel_authentication_endpoint\":\"http://127.0.0.1:11111/auth/realms/sftpgo/protocol/openid-connect/ext/ciba/auth\"}}`)\n\t\t})\n\t\thttp.HandleFunc(\"/404\", func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tfmt.Fprintf(w, \"Not found\\n\")\n\t\t})\n\t\tif err := http.ListenAndServe(oidcMockAddr, nil); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTP notification server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\twaitTCPListening(oidcMockAddr)\n}\n\nfunc waitForUsersQuotaScan(t *testing.T, token string) {\n\tfor {\n\t\tvar scans []common.ActiveQuotaScan\n\t\treq, _ := http.NewRequest(http.MethodGet, quotaScanPath, nil)\n\t\tsetBearerForReq(req, token)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\terr := render.DecodeJSON(rr.Body, &scans)\n\n\t\tif !assert.NoError(t, err, \"Error getting active scans\") {\n\t\t\tbreak\n\t\t}\n\t\tif len(scans) == 0 {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n}\n\nfunc waitForFoldersQuotaScanPath(t *testing.T, token string) {\n\tvar scans []common.ActiveVirtualFolderQuotaScan\n\tfor {\n\t\treq, _ := http.NewRequest(http.MethodGet, quotaScanVFolderPath, nil)\n\t\tsetBearerForReq(req, token)\n\t\trr := executeRequest(req)\n\t\tcheckResponseCode(t, http.StatusOK, rr)\n\t\terr := render.DecodeJSON(rr.Body, &scans)\n\t\tif !assert.NoError(t, err, \"Error getting active folders scans\") {\n\t\t\tbreak\n\t\t}\n\t\tif len(scans) == 0 {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n}\n\nfunc waitTCPListening(address string) {\n\tfor {\n\t\tconn, err := net.Dial(\"tcp\", address)\n\t\tif err != nil {\n\t\t\tlogger.WarnToConsole(\"tcp server %v not listening: %v\", address, err)\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\t\tlogger.InfoToConsole(\"tcp server %v now listening\", address)\n\t\tconn.Close()\n\t\tbreak\n\t}\n}\n\nfunc startSMTPServer() {\n\tgo func() {\n\t\tif err := smtpd.ListenAndServe(smtpServerAddr, func(_ net.Addr, _ string, _ []string, data []byte) error {\n\t\t\tre := regexp.MustCompile(`code is \".*?\"`)\n\t\t\tcode := strings.TrimPrefix(string(re.Find(data)), \"code is \")\n\t\t\tlastResetCode = strings.ReplaceAll(code, \"\\\"\", \"\")\n\t\t\treturn nil\n\t\t}, \"SFTPGo test\", \"localhost\"); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SMTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\twaitTCPListening(smtpServerAddr)\n}\n\nfunc getTestAdmin() dataprovider.Admin {\n\treturn dataprovider.Admin{\n\t\tUsername:    defaultTokenAuthUser,\n\t\tPassword:    defaultTokenAuthPass,\n\t\tStatus:      1,\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t\tEmail:       \"admin@example.com\",\n\t\tDescription: \"test admin\",\n\t}\n}\n\nfunc getTestGroup() dataprovider.Group {\n\treturn dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName:        \"test_group\",\n\t\t\tDescription: \"test group description\",\n\t\t},\n\t}\n}\n\nfunc getTestRole() dataprovider.Role {\n\treturn dataprovider.Role{\n\t\tName:        \"test_role\",\n\t\tDescription: \"test role description\",\n\t}\n}\n\nfunc getTestUser() dataprovider.User {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:    defaultUsername,\n\t\t\tPassword:    defaultPassword,\n\t\t\tHomeDir:     filepath.Join(homeBasePath, defaultUsername),\n\t\t\tStatus:      1,\n\t\t\tDescription: \"test user\",\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = defaultPerms\n\treturn user\n}\n\nfunc getTestSFTPUser() dataprovider.User {\n\tu := getTestUser()\n\tu.Username = u.Username + \"_sftp\"\n\tu.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tu.FsConfig.SFTPConfig.Endpoint = sftpServerAddr\n\tu.FsConfig.SFTPConfig.Username = defaultUsername\n\tu.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\treturn u\n}\n\nfunc getUserAsJSON(t *testing.T, user dataprovider.User) []byte {\n\tjson, err := json.Marshal(user)\n\tassert.NoError(t, err)\n\treturn json\n}\n\nfunc getCSRFTokenFromInternalPageMock(urlPath, token string) (string, error) {\n\treq, err := http.NewRequest(http.MethodGet, urlPath, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.RequestURI = urlPath\n\tsetJWTCookieForReq(req, token)\n\trr := executeRequest(req)\n\tif rr.Code != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"unexpected status code: %d\", rr.Code)\n\t}\n\treturn getCSRFTokenFromBody(rr.Body)\n}\n\nfunc getCSRFTokenMock(loginURLPath, remoteAddr string) (string, string, error) {\n\treq, err := http.NewRequest(http.MethodGet, loginURLPath, nil)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\treq.RemoteAddr = remoteAddr\n\trr := executeRequest(req)\n\tcookie := rr.Header().Get(\"Set-Cookie\")\n\tif cookie == \"\" {\n\t\treturn \"\", \"\", errors.New(\"unable to get login cookie\")\n\t}\n\ttoken, err := getCSRFTokenFromBody(bytes.NewBuffer(rr.Body.Bytes()))\n\treturn cookie, token, err\n}\n\nfunc getCSRFToken(url string) (string, string, error) {\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tcookie := resp.Header.Get(\"Set-Cookie\")\n\tif cookie == \"\" {\n\t\treturn \"\", \"\", errors.New(\"no login cookie\")\n\t}\n\n\tdefer resp.Body.Close()\n\n\ttoken, err := getCSRFTokenFromBody(resp.Body)\n\treturn cookie, token, err\n}\n\nfunc getCSRFTokenFromBody(body io.Reader) (string, error) {\n\tdoc, err := html.Parse(body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar csrfToken string\n\tvar f func(*html.Node)\n\n\tf = func(n *html.Node) {\n\t\tif n.Type == html.ElementNode && n.Data == \"input\" {\n\t\t\tvar name, value string\n\t\t\tfor _, attr := range n.Attr {\n\t\t\t\tif attr.Key == \"value\" {\n\t\t\t\t\tvalue = attr.Val\n\t\t\t\t}\n\t\t\t\tif attr.Key == \"name\" {\n\t\t\t\t\tname = attr.Val\n\t\t\t\t}\n\t\t\t}\n\t\t\tif name == csrfFormToken {\n\t\t\t\tcsrfToken = value\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\t\tf(c)\n\t\t}\n\t}\n\n\tf(doc)\n\n\tif csrfToken == \"\" {\n\t\treturn \"\", errors.New(\"CSRF token not found\")\n\t}\n\n\treturn csrfToken, nil\n}\n\nfunc getLoginForm(username, password, csrfToken string) url.Values {\n\tform := make(url.Values)\n\tform.Set(\"username\", username)\n\tform.Set(\"password\", password)\n\tform.Set(csrfFormToken, csrfToken)\n\treturn form\n}\n\nfunc setCSRFHeaderForReq(req *http.Request, csrfToken string) {\n\treq.Header.Set(\"X-CSRF-TOKEN\", csrfToken)\n}\n\nfunc setBearerForReq(req *http.Request, jwtToken string) {\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %v\", jwtToken))\n}\n\nfunc setAPIKeyForReq(req *http.Request, apiKey, username string) {\n\tif username != \"\" {\n\t\tapiKey += \".\" + username\n\t}\n\treq.Header.Set(\"X-SFTPGO-API-KEY\", apiKey)\n}\n\nfunc setLoginCookie(req *http.Request, cookie string) {\n\treq.Header.Set(\"Cookie\", cookie)\n}\n\nfunc setJWTCookieForReq(req *http.Request, jwtToken string) {\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%v\", jwtToken))\n}\n\nfunc getJWTAPITokenFromTestServer(username, password string) (string, error) {\n\treturn getJWTAPITokenFromTestServerWithPasscode(username, password, \"\")\n}\n\nfunc getJWTAPITokenFromTestServerWithPasscode(username, password, passcode string) (string, error) {\n\treq, _ := http.NewRequest(http.MethodGet, tokenPath, nil)\n\treq.SetBasicAuth(username, password)\n\tif passcode != \"\" {\n\t\treq.Header.Set(\"X-SFTPGO-OTP\", passcode)\n\t}\n\trr := executeRequest(req)\n\tif rr.Code != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"unexpected  status code %v\", rr.Code)\n\t}\n\tresponseHolder := make(map[string]any)\n\terr := render.DecodeJSON(rr.Body, &responseHolder)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn responseHolder[\"access_token\"].(string), nil\n}\n\nfunc getJWTAPIUserTokenFromTestServer(username, password string) (string, error) {\n\treq, _ := http.NewRequest(http.MethodGet, userTokenPath, nil)\n\treq.SetBasicAuth(username, password)\n\trr := executeRequest(req)\n\tif rr.Code != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"unexpected status code %v\", rr.Code)\n\t}\n\tresponseHolder := make(map[string]any)\n\terr := render.DecodeJSON(rr.Body, &responseHolder)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn responseHolder[\"access_token\"].(string), nil\n}\n\nfunc getJWTWebToken(username, password string) (string, error) {\n\tloginCookie, csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tform := getLoginForm(username, password, csrfToken)\n\treq, _ := http.NewRequest(http.MethodPost, httpBaseURL+webLoginPath,\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tclient := &http.Client{\n\t\tTimeout: 10 * time.Second,\n\t\tCheckRedirect: func(_ *http.Request, _ []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusFound {\n\t\treturn \"\", fmt.Errorf(\"unexpected  status code %v\", resp.StatusCode)\n\t}\n\tcookie := resp.Header.Get(\"Set-Cookie\")\n\tif strings.HasPrefix(cookie, \"jwt=\") {\n\t\treturn cookie[4:], nil\n\t}\n\treturn \"\", errors.New(\"no cookie found\")\n}\n\nfunc getCookieFromResponse(rr *httptest.ResponseRecorder) (string, error) {\n\tcookie := strings.Split(rr.Header().Get(\"Set-Cookie\"), \";\")\n\tif strings.HasPrefix(cookie[0], \"jwt=\") {\n\t\treturn cookie[0][4:], nil\n\t}\n\treturn \"\", errors.New(\"no cookie found\")\n}\n\nfunc getJWTWebClientTokenFromTestServerWithAddr(username, password, remoteAddr string) (string, error) {\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webClientLoginPath, remoteAddr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tform := getLoginForm(username, password, csrfToken)\n\treq, _ := http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = remoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr := executeRequest(req)\n\tif rr.Code != http.StatusFound {\n\t\treturn \"\", fmt.Errorf(\"unexpected  status code %v\", rr)\n\t}\n\treturn getCookieFromResponse(rr)\n}\n\nfunc getJWTWebClientTokenFromTestServer(username, password string) (string, error) {\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webClientLoginPath, defaultRemoteAddr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tform := getLoginForm(username, password, csrfToken)\n\treq, _ := http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\treq.Header.Set(\"Cookie\", loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr := executeRequest(req)\n\tif rr.Code != http.StatusFound {\n\t\treturn \"\", fmt.Errorf(\"unexpected  status code %v\", rr)\n\t}\n\treturn getCookieFromResponse(rr)\n}\n\nfunc getJWTWebTokenFromTestServer(username, password string) (string, error) {\n\tloginCookie, csrfToken, err := getCSRFTokenMock(webLoginPath, defaultRemoteAddr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tform := getLoginForm(username, password, csrfToken)\n\treq, _ := http.NewRequest(http.MethodPost, webLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.RemoteAddr = defaultRemoteAddr\n\tsetLoginCookie(req, loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr := executeRequest(req)\n\tif rr.Code != http.StatusFound {\n\t\treturn \"\", fmt.Errorf(\"unexpected  status code %v\", rr)\n\t}\n\treturn getCookieFromResponse(rr)\n}\n\nfunc executeRequest(req *http.Request) *httptest.ResponseRecorder {\n\trr := httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\treturn rr\n}\n\nfunc checkResponseCode(t *testing.T, expected int, rr *httptest.ResponseRecorder) {\n\tassert.Equal(t, expected, rr.Code, rr.Body.String())\n}\n\nfunc getSftpClient(user dataprovider.User) (*ssh.Client, *sftp.Client, error) {\n\tvar sftpClient *sftp.Client\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         5 * time.Second,\n\t}\n\tif user.Password != \"\" {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}\n\t} else {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}\n\t}\n\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif err != nil {\n\t\treturn conn, sftpClient, err\n\t}\n\tsftpClient, err = sftp.NewClient(conn)\n\tif err != nil {\n\t\tconn.Close()\n\t}\n\treturn conn, sftpClient, err\n}\n\nfunc createTestFile(path string, size int64) error {\n\tbaseDir := filepath.Dir(path)\n\tif _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) {\n\t\terr = os.MkdirAll(baseDir, os.ModePerm)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tcontent := make([]byte, size)\n\tif size > 0 {\n\t\t_, err := rand.Read(content)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn os.WriteFile(path, content, os.ModePerm)\n}\n\nfunc getExitCodeScriptContent(exitCode int) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tcontent = append(content, []byte(fmt.Sprintf(\"exit %v\", exitCode))...)\n\treturn content\n}\n\nfunc getMultipartFormData(values url.Values, fileFieldName, filePath string) (bytes.Buffer, string, error) {\n\tvar b bytes.Buffer\n\tw := multipart.NewWriter(&b)\n\tfor k, v := range values {\n\t\tfor _, s := range v {\n\t\t\tif err := w.WriteField(k, s); err != nil {\n\t\t\t\treturn b, \"\", err\n\t\t\t}\n\t\t}\n\t}\n\tif len(fileFieldName) > 0 && len(filePath) > 0 {\n\t\tfw, err := w.CreateFormFile(fileFieldName, filepath.Base(filePath))\n\t\tif err != nil {\n\t\t\treturn b, \"\", err\n\t\t}\n\t\tf, err := os.Open(filePath)\n\t\tif err != nil {\n\t\t\treturn b, \"\", err\n\t\t}\n\t\tdefer f.Close()\n\t\tif _, err = io.Copy(fw, f); err != nil {\n\t\t\treturn b, \"\", err\n\t\t}\n\t}\n\terr := w.Close()\n\treturn b, w.FormDataContentType(), err\n}\n\nfunc generateTOTPPasscode(secret string) (string, error) {\n\treturn totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{\n\t\tPeriod:    30,\n\t\tSkew:      1,\n\t\tDigits:    otp.DigitsSix,\n\t\tAlgorithm: otp.AlgorithmSHA1,\n\t})\n}\n\nfunc isDbDefenderSupported() bool {\n\t// SQLite shares the implementation with other SQL-based provider but it makes no sense\n\t// to use it outside test cases\n\tswitch dataprovider.GetProviderStatus().Driver {\n\tcase dataprovider.MySQLDataProviderName, dataprovider.PGSQLDataProviderName,\n\t\tdataprovider.CockroachDataProviderName, dataprovider.SQLiteDataProviderName:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc createTestPNG(name string, width, height int, imgColor color.Color) error {\n\tupLeft := image.Point{0, 0}\n\tlowRight := image.Point{width, height}\n\timg := image.NewRGBA(image.Rectangle{upLeft, lowRight})\n\tfor x := 0; x < width; x++ {\n\t\tfor y := 0; y < height; y++ {\n\t\t\timg.Set(x, y, imgColor)\n\t\t}\n\t}\n\tf, err := os.Create(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\treturn png.Encode(f, img)\n}\n\nfunc BenchmarkSecretDecryption(b *testing.B) {\n\ts := kms.NewPlainSecret(\"test data\")\n\ts.SetAdditionalData(\"username\")\n\terr := s.Encrypt()\n\trequire.NoError(b, err)\n\tfor i := 0; i < b.N; i++ {\n\t\terr = s.Clone().Decrypt()\n\t\trequire.NoError(b, err)\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/internal_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/go-jose/go-jose/v4\"\n\tjosejwt \"github.com/go-jose/go-jose/v4/jwt\"\n\t\"github.com/klauspost/compress/zip\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/net/html\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/acme\"\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\thttpdCert = `-----BEGIN CERTIFICATE-----\nMIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw\nRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw\nOTUzMDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD\nVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVqWvrJ51t5OxV0v25NsOgR82CA\nNXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIVCzgWkxiz7XE4lgUwX44FCXZM\n3+JeUbKjUzBRMB0GA1UdDgQWBBRhLw+/o3+Z02MI/d4tmaMui9W16jAfBgNVHSME\nGDAWgBRhLw+/o3+Z02MI/d4tmaMui9W16jAPBgNVHRMBAf8EBTADAQH/MAoGCCqG\nSM49BAMCA2kAMGYCMQDqLt2lm8mE+tGgtjDmtFgdOcI72HSbRQ74D5rYTzgST1rY\n/8wTi5xl8TiFUyLMUsICMQC5ViVxdXbhuG7gX6yEqSkMKZICHpO8hqFwOD/uaFVI\ndV4vKmHUzwK/eIx+8Ay3neE=\n-----END CERTIFICATE-----`\n\thttpdKey = `-----BEGIN EC PARAMETERS-----\nBgUrgQQAIg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDCfMNsN6miEE3rVyUPwElfiJSWaR5huPCzUenZOfJT04GAcQdWvEju3\nUM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq\nWvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV\nCzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=\n-----END EC PRIVATE KEY-----`\n\tcaCRT = `-----BEGIN CERTIFICATE-----\nMIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0\nQXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT\nCENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m\nfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1\nv03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh\nyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL\nCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U\n4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb\nKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo\nNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLqR/BJTbbyXUB0imne1u00fuzb\nS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb78x7ivdyXSF5LVQJ1JvhhWu6i\nM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpBP8d2jcRZVUVrSXGc2mAGuGOY\n/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2ZxugCXULtRWJ9p4C9zUl40HEy\nOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD\nAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNoJhIvDZQrEf/VQbWuu\nXgNnt2m5MA0GCSqGSIb3DQEBCwUAA4ICAQCYhT5SRqk19hGrQ09hVSZOzynXAa5F\nsYkEWJzFyLg9azhnTPE1bFM18FScnkd+dal6mt+bQiJvdh24NaVkDghVB7GkmXki\npAiZwEDHMqtbhiPxY8LtSeCBAz5JqXVU2Q0TpAgNSH4W7FbGWNThhxcJVOoIrXKE\njbzhwl1Etcaf0DBKWliUbdlxQQs65DLy+rNBYtOeK0pzhzn1vpehUlJ4eTFzP9KX\ny2Mksuq9AspPbqnqpWW645MdTxMb5T57MCrY3GDKw63z5z3kz88LWJF3nOxZmgQy\nWFUhbLmZm7x6N5eiu6Wk8/B4yJ/n5UArD4cEP1i7nqu+mbbM/SZlq1wnGpg/sbRV\noUF+a7pRcSbfxEttle4pLFhS+ErKatjGcNEab2OlU3bX5UoBs+TYodnCWGKOuBKV\nL/CYc65QyeYZ+JiwYn9wC8YkzOnnVIQjiCEkLgSL30h9dxpnTZDLrdAA8ItelDn5\nDvjuQq58CGDsaVqpSobiSC1DMXYWot4Ets1wwovUNEq1l0MERB+2olE+JU/8E23E\neL1/aA7Kw/JibkWz1IyzClpFDKXf6kR2onJyxerdwUL+is7tqYFLysiHxZDL1bli\nSXbW8hMa5gvo0IilFP9Rznn8PplIfCsvBDVv6xsRr5nTAFtwKaMBVgznE2ghs69w\nkK8u1YiiVenmoQ==\n-----END CERTIFICATE-----`\n\tcaKey = `-----BEGIN RSA PRIVATE KEY-----\nMIIJKgIBAAKCAgEA7WHW216mfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7x\nb64rkpdzx1aWetSiCrEyc3D1v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvD\nOBUYZgtMqHZzpE6xRrqQ84zhyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKz\nn/2uEVt33qmO85WtN3RzbSqLCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj\n7B5P5MeamkkogwbExUjdHp3U4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZ\nDe67V/Q8iB2May1k7zBz1ZtbKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmOb\ncn8AIfH6smLQrn0C3cs7CYfoNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLq\nR/BJTbbyXUB0imne1u00fuzbS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb7\n8x7ivdyXSF5LVQJ1JvhhWu6iM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpB\nP8d2jcRZVUVrSXGc2mAGuGOY/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2Z\nxugCXULtRWJ9p4C9zUl40HEyOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEA\nAQKCAgEA4x0OoceG54ZrVxifqVaQd8qw3uRmUKUMIMdfuMlsdideeLO97ynmSlRY\n00kGo/I4Lp6mNEjI9gUie9+uBrcUhri4YLcujHCH+YlNnCBDbGjwbe0ds9SLCWaa\nKztZHMSlW5Q4Bqytgu+MpOnxSgqjlOk+vz9TcGFKVnUkHIkAcqKFJX8gOFxPZA/t\nOb1kJaz4kuv5W2Kur/ISKvQtvFvOtQeV0aJyZm8LqXnvS4cPI7yN4329NDU0HyDR\ny/deqS2aqV4zII3FFqbz8zix/m1xtVQzWCugZGMKrz0iuJMfNeCABb8rRGc6GsZz\n+465v/kobqgeyyneJ1s5rMFrLp2o+dwmnIVMNsFDUiN1lIZDHLvlgonaUO3IdTZc\n9asamFWKFKUMgWqM4zB1vmUO12CKowLNIIKb0L+kf1ixaLLDRGf/f9vLtSHE+oyx\nlATiS18VNA8+CGsHF6uXMRwf2auZdRI9+s6AAeyRISSbO1khyWKHo+bpOvmPAkDR\nnknTjbYgkoZOV+mrsU5oxV8s6vMkuvA3rwFhT2gie8pokuACFcCRrZi9MVs4LmUQ\nu0GYTHvp2WJUjMWBm6XX7Hk3g2HV842qpk/mdtTjNsXws81djtJPn4I/soIXSgXz\npY3SvKTuOckP9OZVF0yqKGeZXKpD288PKpC+MAg3GvEJaednagECggEBAPsfLwuP\nL1kiDjXyMcRoKlrQ6Q/zBGyBmJbZ5uVGa02+XtYtDAzLoVupPESXL0E7+r8ZpZ39\n0dV4CEJKpbVS/BBtTEkPpTK5kz778Ib04TAyj+YLhsZjsnuja3T5bIBZXFDeDVDM\n0ZaoFoKpIjTu2aO6pzngsgXs6EYbo2MTuJD3h0nkGZsICL7xvT9Mw0P1p2Ftt/hN\n+jKk3vN220wTWUsq43AePi45VwK+PNP12ZXv9HpWDxlPo3j0nXtgYXittYNAT92u\nBZbFAzldEIX9WKKZgsWtIzLaASjVRntpxDCTby/nlzQ5dw3DHU1DV3PIqxZS2+Oe\nKV+7XFWgZ44YjYECggEBAPH+VDu3QSrqSahkZLkgBtGRkiZPkZFXYvU6kL8qf5wO\nZ/uXMeqHtznAupLea8I4YZLfQim/NfC0v1cAcFa9Ckt9g3GwTSirVcN0AC1iOyv3\n/hMZCA1zIyIcuUplNr8qewoX71uPOvCNH0dix77423mKFkJmNwzy4Q+rV+qkRdLn\nv+AAgh7g5N91pxNd6LQJjoyfi1Ka6rRP2yGXM5v7QOwD16eN4JmExUxX1YQ7uNuX\npVS+HRxnBquA+3/DB1LtBX6pa2cUa+LRUmE/NCPHMvJcyuNkYpJKlNTd9vnbfo0H\nRNSJSWm+aGxDFMjuPjV3JLj2OdKMPwpnXdh2vBZCPpMCggEAM+yTvrEhmi2HgLIO\nhkz/jP2rYyfdn04ArhhqPLgd0dpuI5z24+Jq/9fzZT9ZfwSW6VK1QwDLlXcXRhXH\nQ8Hf6smev3CjuORURO61IkKaGWwrAucZPAY7ToNQ4cP9ImDXzMTNPgrLv3oMBYJR\nV16X09nxX+9NABqnQG/QjdjzDc6Qw7+NZ9f2bvzvI5qMuY2eyW91XbtJ45ThoLfP\nymAp03gPxQwL0WT7z85kJ3OrROxzwaPvxU0JQSZbNbqNDPXmFTiECxNDhpRAAWlz\n1DC5Vg2l05fkMkyPdtD6nOQWs/CYSfB5/EtxiX/xnBszhvZUIe6KFvuKFIhaJD5h\niykagQKCAQEAoBRm8k3KbTIo4ZzvyEq4V/+dF3zBRczx6FkCkYLygXBCNvsQiR2Y\nBjtI8Ijz7bnQShEoOmeDriRTAqGGrspEuiVgQ1+l2wZkKHRe/aaij/Zv+4AuhH8q\nuZEYvW7w5Uqbs9SbgQzhp2kjTNy6V8lVnjPLf8cQGZ+9Y9krwktC6T5m/i435WdN\n38h7amNP4XEE/F86Eb3rDrZYtgLIoCF4E+iCyxMehU+AGH1uABhls9XAB6vvo+8/\nSUp8lEqWWLP0U5KNOtYWfCeOAEiIHDbUq+DYUc4BKtbtV1cx3pzlPTOWw6XBi5Lq\njttdL4HyYvnasAQpwe8GcMJqIRyCVZMiwwKCAQEAhQTTS3CC8PwcoYrpBdTjW1ck\nvVFeF1YbfqPZfYxASCOtdx6wRnnEJ+bjqntagns9e88muxj9UhxSL6q9XaXQBD8+\n2AmKUxphCZQiYFZcTucjQEQEI2nN+nAKgRrUSMMGiR8Ekc2iFrcxBU0dnSohw+aB\nPbMKVypQCREu9PcDFIp9rXQTeElbaNsIg1C1w/SQjODbmN/QFHTVbRODYqLeX1J/\nVcGsykSIq7hv6bjn7JGkr2JTdANbjk9LnMjMdJFsKRYxPKkOQfYred6Hiojp5Sor\nPW5am8ejnNSPhIfqQp3uV3KhwPDKIeIpzvrB4uPfTjQWhekHCb8cKSWux3flqw==\n-----END RSA PRIVATE KEY-----`\n\tcaCRL = `-----BEGIN X509 CRL-----\nMIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN\nMjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg\nZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r\nrl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV\n+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199\nXSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE\njF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF\nDouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD\nMSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4\niYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE\nypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4\nhVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94\ncp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67\nsn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn\nw7bXJGfJcIMrrKs=\n-----END X509 CRL-----`\n\tclient1Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAJr32nHRlhyPiS7IfZ/ZWYowDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjM3WhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+kiJ3X6\nHUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi85xE\nOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLWeYl7\nQie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4ZdRf\nXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dybbhO\nc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRUh5Xo\nGzjh6iReaPSOgGatqOw9bDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEAyAK7cOTWqjyLgFM0kyyx1fNPvm2GwKep3MuU\nOrSnLuWjoxzb7WcbKNVMlnvnmSUAWuErxsY0PUJNfcuqWiGmEp4d/SWfWPigG6DC\nsDej35BlSfX8FCufYrfC74VNk4yBS2LVYmIqcpqUrfay0I2oZA8+ToLEpdUvEv2I\nl59eOhJO2jsC3JbOyZZmK2Kv7d94fR+1tg2Rq1Wbnmc9AZKq7KDReAlIJh4u2KHb\nBbtF79idusMwZyP777tqSQ4THBMa+VAEc2UrzdZqTIAwqlKQOvO2fRz2P+ARR+Tz\nMYJMdCdmPZ9qAc8U1OcFBG6qDDltO8wf/Nu/PsSI5LGCIhIuPPIuKfm0rRfTqCG7\nQPQPWjRoXtGGhwjdIuWbX9fIB+c+NpAEKHgLtV+Rxj8s5IVxqG9a5TtU9VkfVXJz\nJ20naoz/G+vDsVINpd3kH0ziNvdrKfGRM5UgtnUOPCXB22fVmkIsMH2knI10CKK+\noffI56NTkLRu00xvg98/wdukhkwIAxg6PQI/BHY5mdvoacEHHHdOhMq+GSAh7DDX\nG8+HdbABM1ExkPnZLat15q706ztiuUpQv1C2DI8YviUVkMqCslj4cD4F8EFPo4kr\nkvme0Cuc9Qlf7N5rjdV3cjwavhFx44dyXj9aesft2Q1okPiIqbGNpcjHcIRlj4Au\nMU3Bo0A=\n-----END CERTIFICATE-----`\n\tclient1Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+ki\nJ3X6HUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi\n85xEOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLW\neYl7Qie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4\nZdRfXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dy\nbbhOc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABAoIBAFjSHK7gENVZxphO\nhHg8k9ShnDo8eyDvK8l9Op3U3/yOsXKxolivvyx//7UFmz3vXDahjNHe7YScAXdw\neezbqBXa7xrvghqZzp2HhFYwMJ0210mcdncBKVFzK4ztZHxgQ0PFTqet0R19jZjl\nX3A325/eNZeuBeOied4qb/24AD6JGc6A0J55f5/QUQtdwYwrL15iC/KZXDL90PPJ\nCFJyrSzcXvOMEvOfXIFxhDVKRCppyIYXG7c80gtNC37I6rxxMNQ4mxjwUI2IVhxL\nj+nZDu0JgRZ4NaGjOq2e79QxUVm/GG3z25XgmBFBrXkEVV+sCZE1VDyj6kQfv9FU\nNhOrwGECgYEAzq47r/HwXifuGYBV/mvInFw3BNLrKry+iUZrJ4ms4g+LfOi0BAgf\nsXsWXulpBo2YgYjFdO8G66f69GlB4B7iLscpABXbRtpDZEnchQpaF36/+4g3i8gB\nZ29XHNDB8+7t4wbXvlSnLv1tZWey2fS4hPosc2YlvS87DMmnJMJqhs8CgYEA4oiB\nLGQP6VNdX0Uigmh5fL1g1k95eC8GP1ylczCcIwsb2OkAq0MT7SHRXOlg3leEq4+g\nmCHk1NdjkSYxDL2ZeTKTS/gy4p1jlcDa6Ilwi4pVvatNvu4o80EYWxRNNb1mAn67\nT8TN9lzc6mEi+LepQM3nYJ3F+ZWTKgxH8uoJwMUCgYEArpumE1vbjUBAuEyi2eGn\nRunlFW83fBCfDAxw5KM8anNlja5uvuU6GU/6s06QCxg+2lh5MPPrLdXpfukZ3UVa\nItjg+5B7gx1MSALaiY8YU7cibFdFThM3lHIM72wyH2ogkWcrh0GvSFSUQlJcWCSW\nasmMGiYXBgBL697FFZomMyMCgYEAkAnp0JcDQwHd4gDsk2zoqnckBsDb5J5J46n+\nDYNAFEww9bgZ08u/9MzG+cPu8xFE621U2MbcYLVfuuBE2ewIlPaij/COMmeO9Z59\n0tPpOuDH6eTtd1SptxqR6P+8pEn8feOlKHBj4Z1kXqdK/EiTlwAVeep4Al2oCFls\nujkz4F0CgYAe8vHnVFHlWi16zAqZx4ZZZhNuqPtgFkvPg9LfyNTA4dz7F9xgtUaY\nnXBPyCe/8NtgBfT79HkPiG3TM0xRZY9UZgsJKFtqAu5u4ManuWDnsZI9RK2QTLHe\nyEbH5r3Dg3n9k/3GbjXFIWdU9UaYsdnSKHHtMw9ZODc14LaAogEQug==\n-----END RSA PRIVATE KEY-----`\n\t// client 2 crt is revoked\n\tclient2Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAM5p4duNjikSPxYGaqBm5FEwDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjUyWhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImVjm/b\nQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TPkZua\neq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEKh4LQ\ncr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81PQAmT\nA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu4Ic0\n6tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBR5mf0f\nZjf8ZCGXqU2+45th7VkkLDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEARhFxNAouwbpEfN1M90+ao5rwyxEewerSoCCz\nPQzeUZ66MA/FkS/tFUGgGGG+wERN+WLbe1cN6q/XFr0FSMLuUxLXDNV02oUL/FnY\nxcyNLaZUZ0pP7sA+Hmx2AdTA6baIwQbyIY9RLAaz6hzo1YbI8yeis645F1bxgL2D\nEP5kXa3Obv0tqWByMZtrmJPv3p0W5GJKXVDn51GR/E5KI7pliZX2e0LmMX9mxfPB\n4sXFUggMHXxWMMSAmXPVsxC2KX6gMnajO7JUraTwuGm+6V371FzEX+UKXHI+xSvO\n78TseTIYsBGLjeiA8UjkKlD3T9qsQm2mb2PlKyqjvIm4i2ilM0E2w4JZmd45b925\n7q/QLV3NZ/zZMi6AMyULu28DWKfAx3RLKwnHWSFcR4lVkxQrbDhEUMhAhLAX+2+e\nqc7qZm3dTabi7ZJiiOvYK/yNgFHa/XtZp5uKPB5tigPIa+34hbZF7s2/ty5X3O1N\nf5Ardz7KNsxJjZIt6HvB28E/PPOvBqCKJc1Y08J9JbZi8p6QS1uarGoR7l7rT1Hv\n/ZXkNTw2bw1VpcWdzDBLLVHYNnJmS14189LVk11PcJJpSmubwCqg+ZZULdgtVr3S\nANas2dgMPVwXhnAalgkcc+lb2QqaEz06axfbRGBsgnyqR5/koKCg1Hr0+vThHSsR\nE0+r2+4=\n-----END CERTIFICATE-----`\n\tclient2Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImV\njm/bQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TP\nkZuaeq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEK\nh4LQcr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81P\nQAmTA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu\n4Ic06tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABAoIBAQCMnEeg9uXQmdvq\nop4qi6bV+ZcDWvvkLwvHikFMnYpIaheYBpF2ZMKzdmO4xgCSWeFCQ4Hah8KxfHCM\nqLuWvw2bBBE5J8yQ/JaPyeLbec7RX41GQ2YhPoxDdP0PdErREdpWo4imiFhH/Ewt\nRvq7ufRdpdLoS8dzzwnvX3r+H2MkHoC/QANW2AOuVoZK5qyCH5N8yEAAbWKaQaeL\nVBhAYEVKbAkWEtXw7bYXzxRR7WIM3f45v3ncRusDIG+Hf75ZjatoH0lF1gHQNofO\nqkCVZVzjkLFuzDic2KZqsNORglNs4J6t5Dahb9v3hnoK963YMnVSUjFvqQ+/RZZy\nVILFShilAoGBANucwZU61eJ0tLKBYEwmRY/K7Gu1MvvcYJIOoX8/BL3zNmNO0CLl\nNiABtNt9WOVwZxDsxJXdo1zvMtAegNqS6W11R1VAZbL6mQ/krScbLDE6JKA5DmA7\n4nNi1gJOW1ziAfdBAfhe4cLbQOb94xkOK5xM1YpO0xgDJLwrZbehDMmPAoGBAMAl\n/owPDAvcXz7JFynT0ieYVc64MSFiwGYJcsmxSAnbEgQ+TR5FtkHYe91OSqauZcCd\naoKXQNyrYKIhyounRPFTdYQrlx6KtEs7LU9wOxuphhpJtGjRnhmA7IqvX703wNvu\nkhrEavn86G5boH8R80371SrN0Rh9UeAlQGuNBdvfAoGAEAmokW9Ug08miwqrr6Pz\n3IZjMZJwALidTM1IufQuMnj6ddIhnQrEIx48yPKkdUz6GeBQkuk2rujA+zXfDxc/\neMDhzrX/N0zZtLFse7ieR5IJbrH7/MciyG5lVpHGVkgjAJ18uVikgAhm+vd7iC7i\nvG1YAtuyysQgAKXircBTIL0CgYAHeTLWVbt9NpwJwB6DhPaWjalAug9HIiUjktiB\nGcEYiQnBWn77X3DATOA8clAa/Yt9m2HKJIHkU1IV3ESZe+8Fh955PozJJlHu3yVb\nAp157PUHTriSnxyMF2Sb3EhX/rQkmbnbCqqygHC14iBy8MrKzLG00X6BelZV5n0D\n8d85dwKBgGWY2nsaemPH/TiTVF6kW1IKSQoIyJChkngc+Xj/2aCCkkmAEn8eqncl\nRKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA\nubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl\n-----END RSA PRIVATE KEY-----`\n\tdefaultAdminUsername = \"admin\"\n\tdefaultAdminPass     = \"password\"\n\tdefeaultUsername     = \"test_user\"\n)\n\nvar (\n\tconfigDir = filepath.Join(\".\", \"..\", \"..\")\n)\n\ntype failingWriter struct {\n}\n\nfunc (r *failingWriter) Write(_ []byte) (n int, err error) {\n\treturn 0, errors.New(\"write error\")\n}\n\nfunc (r *failingWriter) WriteHeader(_ int) {}\n\nfunc (r *failingWriter) Header() http.Header {\n\treturn make(http.Header)\n}\n\ntype failingJoseSigner struct{}\n\nfunc (s *failingJoseSigner) Sign(payload []byte) (*jose.JSONWebSignature, error) {\n\treturn nil, errors.New(\"sign test error\")\n}\n\nfunc (s *failingJoseSigner) Options() jose.SignerOptions {\n\treturn jose.SignerOptions{}\n}\n\nfunc TestShouldBind(t *testing.T) {\n\tc := Conf{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort: 10000,\n\t\t\t},\n\t\t},\n\t}\n\trequire.False(t, c.ShouldBind())\n\tc.Bindings[0].EnableRESTAPI = true\n\trequire.True(t, c.ShouldBind())\n\n\tc.Bindings[0].Port = 0\n\trequire.False(t, c.ShouldBind())\n\n\tif runtime.GOOS != osWindows {\n\t\tc.Bindings[0].Address = \"/absolute/path\"\n\t\trequire.True(t, c.ShouldBind())\n\t}\n}\n\nfunc TestBrandingValidation(t *testing.T) {\n\tb := Binding{\n\t\tBranding: Branding{\n\t\t\tWebAdmin: UIBranding{\n\t\t\t\tLogoPath:   \"path1\",\n\t\t\t\tDefaultCSS: []string{\"my.css\"},\n\t\t\t},\n\t\t\tWebClient: UIBranding{\n\t\t\t\tFaviconPath:    \"favicon1.ico\",\n\t\t\t\tDisclaimerPath: \"../path2\",\n\t\t\t\tExtraCSS:       []string{\"1.css\"},\n\t\t\t},\n\t\t},\n\t}\n\tb.checkBranding()\n\tassert.Equal(t, \"/favicon.png\", b.Branding.WebAdmin.FaviconPath)\n\tassert.Equal(t, \"/path1\", b.Branding.WebAdmin.LogoPath)\n\tassert.Equal(t, []string{\"/my.css\"}, b.Branding.WebAdmin.DefaultCSS)\n\tassert.Len(t, b.Branding.WebAdmin.ExtraCSS, 0)\n\tassert.Equal(t, \"/favicon1.ico\", b.Branding.WebClient.FaviconPath)\n\tassert.Equal(t, path.Join(webStaticFilesPath, \"/path2\"), b.Branding.WebClient.DisclaimerPath)\n\tif assert.Len(t, b.Branding.WebClient.ExtraCSS, 1) {\n\t\tassert.Equal(t, \"/1.css\", b.Branding.WebClient.ExtraCSS[0])\n\t}\n\tb.Branding.WebAdmin.DisclaimerPath = \"https://example.com\"\n\tb.checkBranding()\n\tassert.Equal(t, \"https://example.com\", b.Branding.WebAdmin.DisclaimerPath)\n}\n\nfunc TestRedactedConf(t *testing.T) {\n\tc := Conf{\n\t\tSigningPassphrase: \"passphrase\",\n\t\tSetup: SetupConfig{\n\t\t\tInstallationCode: \"123\",\n\t\t},\n\t}\n\tredactedField := \"[redacted]\"\n\tredactedConf := c.getRedacted()\n\tassert.Equal(t, redactedField, redactedConf.SigningPassphrase)\n\tassert.Equal(t, redactedField, redactedConf.Setup.InstallationCode)\n\tassert.NotEqual(t, c.SigningPassphrase, redactedConf.SigningPassphrase)\n\tassert.NotEqual(t, c.Setup.InstallationCode, redactedConf.Setup.InstallationCode)\n}\n\nfunc TestGetRespStatus(t *testing.T) {\n\tvar err error\n\terr = util.NewMethodDisabledError(\"\")\n\trespStatus := getRespStatus(err)\n\tassert.Equal(t, http.StatusForbidden, respStatus)\n\terr = fmt.Errorf(\"generic error\")\n\trespStatus = getRespStatus(err)\n\tassert.Equal(t, http.StatusInternalServerError, respStatus)\n\trespStatus = getRespStatus(plugin.ErrNoSearcher)\n\tassert.Equal(t, http.StatusNotImplemented, respStatus)\n}\n\nfunc TestMappedStatusCode(t *testing.T) {\n\terr := os.ErrPermission\n\tcode := getMappedStatusCode(err)\n\tassert.Equal(t, http.StatusForbidden, code)\n\terr = os.ErrNotExist\n\tcode = getMappedStatusCode(err)\n\tassert.Equal(t, http.StatusNotFound, code)\n\terr = common.ErrQuotaExceeded\n\tcode = getMappedStatusCode(err)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, code)\n\terr = os.ErrClosed\n\tcode = getMappedStatusCode(err)\n\tassert.Equal(t, http.StatusInternalServerError, code)\n\terr = &http.MaxBytesError{}\n\tcode = getMappedStatusCode(err)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, code)\n}\n\nfunc TestGCSWebInvalidFormFile(t *testing.T) {\n\tform := make(url.Values)\n\tform.Set(\"username\", \"test_username\")\n\tform.Set(\"fs_provider\", \"2\")\n\treq, _ := http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\terr := req.ParseForm()\n\tassert.NoError(t, err)\n\t_, err = getFsConfigFromPostFields(req)\n\tassert.EqualError(t, err, http.ErrNotMultipart.Error())\n}\n\nfunc TestBrandingInvalidFormFile(t *testing.T) {\n\tform := make(url.Values)\n\treq, _ := http.NewRequest(http.MethodPost, webConfigsPath, strings.NewReader(form.Encode()))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\terr := req.ParseForm()\n\tassert.NoError(t, err)\n\t_, err = getBrandingConfigFromPostFields(req, &dataprovider.BrandingConfigs{})\n\tassert.EqualError(t, err, http.ErrNotMultipart.Error())\n}\n\nfunc TestTokenDuration(t *testing.T) {\n\tassert.Equal(t, shareTokenDuration, getTokenDuration(tokenAudienceWebShare))\n\tassert.Equal(t, apiTokenDuration, getTokenDuration(tokenAudienceAPI))\n\tassert.Equal(t, apiTokenDuration, getTokenDuration(tokenAudienceAPIUser))\n\tassert.Equal(t, cookieTokenDuration, getTokenDuration(tokenAudienceWebAdmin))\n\tassert.Equal(t, csrfTokenDuration, getTokenDuration(tokenAudienceCSRF))\n\tassert.Equal(t, 20*time.Minute, getTokenDuration(\"\"))\n\n\tupdateTokensDuration(30, 660, 360)\n\tassert.Equal(t, 30*time.Minute, apiTokenDuration)\n\tassert.Equal(t, 11*time.Hour, cookieTokenDuration)\n\tassert.Equal(t, 11*time.Hour, csrfTokenDuration)\n\tassert.Equal(t, 6*time.Hour, shareTokenDuration)\n\tassert.Equal(t, 11*time.Hour, getMaxCookieDuration())\n\n\tcsrfTokenDuration = 1 * time.Hour\n\tassert.Equal(t, 11*time.Hour, getMaxCookieDuration())\n}\n\nfunc TestVerifyCSRFToken(t *testing.T) {\n\tserver := httpdServer{}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, webAdminEventActionPath, nil)\n\trequire.NoError(t, err)\n\treq = req.WithContext(jwt.NewContext(req.Context(), &jwt.Claims{}, fs.ErrPermission))\n\n\trr := httptest.NewRecorder()\n\ttokenString := createCSRFToken(rr, req, server.csrfTokenAuth, \"\", webBaseAdminPath)\n\tassert.NotEmpty(t, tokenString)\n\n\tclaims, err := jwt.VerifyToken(server.csrfTokenAuth, tokenString)\n\trequire.NoError(t, err)\n\tassert.Empty(t, claims.Ref)\n\n\treq.Form = url.Values{}\n\treq.Form.Set(csrfFormToken, tokenString)\n\terr = verifyCSRFToken(req, server.csrfTokenAuth)\n\tassert.ErrorIs(t, err, fs.ErrPermission)\n\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventActionPath, nil)\n\trequire.NoError(t, err)\n\treq = req.WithContext(jwt.NewContext(req.Context(), &jwt.Claims{Claims: josejwt.Claims{ID: xid.New().String()}}, nil))\n\treq.Form = url.Values{}\n\treq.Form.Set(csrfFormToken, tokenString)\n\terr = verifyCSRFToken(req, server.csrfTokenAuth)\n\tassert.ErrorContains(t, err, \"unexpected form token\")\n\n\tclaims = jwt.NewClaims(tokenAudienceCSRF, \"\", getTokenDuration(tokenAudienceCSRF))\n\ttokenString, err = josejwt.Signed(server.csrfTokenAuth.Signer()).Claims(claims).Serialize()\n\tassert.NoError(t, err)\n\treq, err = http.NewRequest(http.MethodPost, webAdminEventActionPath, nil)\n\trequire.NoError(t, err)\n\treq = req.WithContext(jwt.NewContext(req.Context(), &jwt.Claims{Claims: josejwt.Claims{ID: xid.New().String()}}, nil))\n\treq.Form = url.Values{}\n\treq.Form.Set(csrfFormToken, tokenString)\n\terr = verifyCSRFToken(req, server.csrfTokenAuth)\n\tassert.ErrorContains(t, err, \"the form token is not valid\")\n}\n\nfunc TestInvalidToken(t *testing.T) {\n\tserver := httpdServer{}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\tadmin := dataprovider.Admin{\n\t\tUsername: \"admin\",\n\t}\n\terrFake := errors.New(\"fake error\")\n\tasJSON, err := json.Marshal(admin)\n\tassert.NoError(t, err)\n\treq, _ := http.NewRequest(http.MethodPut, path.Join(adminPath, admin.Username), bytes.NewBuffer(asJSON))\n\trctx := chi.NewRouteContext()\n\trctx.URLParams.Add(\"username\", admin.Username)\n\treq = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))\n\treq = req.WithContext(context.WithValue(req.Context(), jwt.ErrorCtxKey, errFake))\n\trr := httptest.NewRecorder()\n\tupdateAdmin(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\trr = httptest.NewRecorder()\n\tdeleteAdmin(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\tadminPwd := pwdChange{\n\t\tCurrentPassword: \"old\",\n\t\tNewPassword:     \"new\",\n\t}\n\tasJSON, err = json.Marshal(adminPwd)\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodPut, \"\", bytes.NewBuffer(asJSON))\n\treq = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))\n\treq = req.WithContext(context.WithValue(req.Context(), jwt.ErrorCtxKey, errFake))\n\trr = httptest.NewRecorder()\n\tchangeAdminPassword(rr, req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\tadm := getAdminFromToken(req)\n\tassert.Empty(t, adm.Username)\n\n\trr = httptest.NewRecorder()\n\treadUserFolder(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetUserFile(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetUserFilesAsZipStream(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetShares(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetShareByID(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\taddShare(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateShare(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteShare(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgenerateTOTPSecret(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tsaveTOTPConfig(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetRecoveryCodes(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgenerateRecoveryCodes(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetUserProfile(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateUserProfile(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetWebTask(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetAdminProfile(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateAdminProfile(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tloadData(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tloadDataFromRequest(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\taddUser(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdisableUser2FA(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateUser(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteUser(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetActiveConnections(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\thandleCloseConnection(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebRestore(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddUserPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateUserPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebTemplateFolderPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebTemplateUserPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tgetAllAdmins(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tgetAllUsers(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\taddFolder(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateFolder(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetFolderByName(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteFolder(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddFolderPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateFolderPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebGetConnections(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebConfigsPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\taddAdmin(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdisableAdmin2FA(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\taddAPIKey(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateAPIKey(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteAPIKey(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\taddGroup(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateGroup(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetGroupByName(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteGroup(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\taddEventAction(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetEventActionByName(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateEventAction(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteEventAction(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetEventRuleByName(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\taddEventRule(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateEventRule(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteEventRule(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetUsersQuotaScans(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateUserTransferQuotaUsage(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdoUpdateUserQuotaUsage(rr, req, \"\", quotaUsage{})\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdoStartUserQuotaScan(rr, req, \"\")\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetRetentionChecks(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\taddRole(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateRole(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteRole(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetUsers(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tgetUserByUsername(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tsearchFsEvents(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tsearchProviderEvents(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tsearchLogEvents(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\taddIPListEntry(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tupdateIPListEntry(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tdeleteIPListEntry(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n\n\trr = httptest.NewRecorder()\n\tserver.handleGetWebUsers(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateUserGet(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateRolePost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddRolePost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddAdminPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddGroupPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateGroupPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddEventActionPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateEventActionPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddEventRulePost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateEventRulePost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateIPListEntryPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientTwoFactorRecoveryPost(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientTwoFactorPost(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminTwoFactorRecoveryPost(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminTwoFactorPost(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\tserver.handleWebUpdateIPListEntryPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\tform := make(url.Values)\n\treq, _ = http.NewRequest(http.MethodPost, webIPListPath+\"/1\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trctx = chi.NewRouteContext()\n\trctx.URLParams.Add(\"type\", \"1\")\n\treq = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddIPListEntryPost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n}\n\nfunc TestTokenSignatureValidation(t *testing.T) {\n\ttokenValidationMode = 0\n\tserver := httpdServer{\n\t\tbinding: Binding{\n\t\t\tAddress:         \"\",\n\t\t\tPort:            8080,\n\t\t\tEnableWebAdmin:  true,\n\t\t\tEnableWebClient: true,\n\t\t\tEnableRESTAPI:   true,\n\t\t},\n\t\tenableWebAdmin:  true,\n\t\tenableWebClient: true,\n\t\tenableRESTAPI:   true,\n\t}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\ttestServer := httptest.NewServer(server.router)\n\tdefer testServer.Close()\n\n\trr := httptest.NewRecorder()\n\treq, err := http.NewRequest(http.MethodGet, tokenPath, nil)\n\trequire.NoError(t, err)\n\treq.SetBasicAuth(defaultAdminUsername, defaultAdminPass)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tvar resp map[string]any\n\terr = json.Unmarshal(rr.Body.Bytes(), &resp)\n\tassert.NoError(t, err)\n\taccessToken := resp[\"access_token\"]\n\trequire.NotEmpty(t, accessToken)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", accessToken))\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\t// change the token validation mode\n\ttokenValidationMode = 2\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", accessToken))\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\t// Now update the admin\n\tadmin, err := dataprovider.AdminExists(defaultAdminUsername)\n\tassert.NoError(t, err)\n\terr = dataprovider.UpdateAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// token validation mode is 0, the old token is still valid\n\ttokenValidationMode = 0\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", accessToken))\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\t// change the token validation mode\n\ttokenValidationMode = 2\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", accessToken))\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t// the token is invalidated, changing the validation mode has no effect\n\ttokenValidationMode = 0\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, versionPath, nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", accessToken))\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\tuserPwd := \"pwd\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: defeaultUsername,\n\t\t\tPassword: userPwd,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), defeaultUsername),\n\t\t\tStatus:   1,\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tdefer func() {\n\t\tdataprovider.DeleteUser(defeaultUsername, \"\", \"\", \"\") //nolint:errcheck\n\t}()\n\n\ttokenValidationMode = 2\n\treq, err = http.NewRequest(http.MethodGet, webClientLoginPath, nil)\n\trequire.NoError(t, err)\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tloginCookie := strings.Split(rr.Header().Get(\"Set-Cookie\"), \";\")[0]\n\tassert.NotEmpty(t, loginCookie)\n\tcsrfToken, err := getCSRFTokenFromBody(rr.Body)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, csrfToken)\n\t// Now login\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"username\", defeaultUsername)\n\tform.Set(\"password\", userPwd)\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Cookie\", loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tuserCookie := strings.Split(rr.Header().Get(\"Set-Cookie\"), \";\")[0]\n\tassert.NotEmpty(t, userCookie)\n\t// Test a WebClient page and a JSON API\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Cookie\", userCookie)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Cookie\", userCookie)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tcsrfToken, err = getCSRFTokenFromBody(rr.Body)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, csrfToken)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, webClientFilePath+\"?path=missing.txt\", nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Cookie\", userCookie)\n\treq.Header.Set(csrfHeaderToken, csrfToken)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\ttokenValidationMode = 0\n\terr = dataprovider.DeleteUser(defeaultUsername, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, webClientFilePath+\"?path=missing.txt\", nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Cookie\", userCookie)\n\treq.Header.Set(csrfHeaderToken, csrfToken)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\ttokenValidationMode = 2\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, webClientFilePath+\"?path=missing.txt\", nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Cookie\", userCookie)\n\treq.Header.Set(csrfHeaderToken, csrfToken)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\n\ttokenValidationMode = 0\n}\n\nfunc TestUpdateWebAdminInvalidClaims(t *testing.T) {\n\tserver := httpdServer{}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\n\trr := httptest.NewRecorder()\n\tadmin := dataprovider.Admin{\n\t\tUsername: \"\",\n\t\tPassword: \"password\",\n\t}\n\tc := &jwt.Claims{\n\t\tUsername:    admin.Username,\n\t\tPermissions: admin.Permissions,\n\t}\n\tc.Subject = admin.GetSignature()\n\ttoken, err := server.tokenAuth.SignWithParams(c, tokenAudienceWebAdmin, \"\", 10*time.Minute)\n\tassert.NoError(t, err)\n\tresp := c.BuildTokenResponse(token)\n\n\treq, err := http.NewRequest(http.MethodGet, webAdminPath, nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", resp.Token))\n\tparsedToken, err := jwt.VerifyRequest(server.tokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx := req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, \"\", webBaseAdminPath))\n\tform.Set(\"status\", \"1\")\n\tform.Set(\"default_users_expiration\", \"30\")\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webAdminPath, \"admin\"), bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\trctx := chi.NewRouteContext()\n\trctx.URLParams.Add(\"username\", \"admin\")\n\treq = req.WithContext(ctx)\n\treq = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", resp.Token))\n\tserver.handleWebUpdateAdminPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n}\n\nfunc TestUpdateSMTPSecrets(t *testing.T) {\n\tcurrentConfigs := &dataprovider.SMTPConfigs{\n\t\tOAuth2: dataprovider.SMTPOAuth2{\n\t\t\tClientSecret: kms.NewPlainSecret(\"client secret\"),\n\t\t\tRefreshToken: kms.NewPlainSecret(\"refresh token\"),\n\t\t},\n\t}\n\tredactedClientSecret := kms.NewPlainSecret(\"secret\")\n\tredactedRefreshToken := kms.NewPlainSecret(\"token\")\n\tredactedClientSecret.SetStatus(sdkkms.SecretStatusRedacted)\n\tredactedRefreshToken.SetStatus(sdkkms.SecretStatusRedacted)\n\tnewConfigs := &dataprovider.SMTPConfigs{\n\t\tPassword: kms.NewPlainSecret(\"pwd\"),\n\t\tOAuth2: dataprovider.SMTPOAuth2{\n\t\t\tClientSecret: redactedClientSecret,\n\t\t\tRefreshToken: redactedRefreshToken,\n\t\t},\n\t}\n\tupdateSMTPSecrets(newConfigs, currentConfigs)\n\tassert.Nil(t, currentConfigs.Password)\n\tassert.NotNil(t, newConfigs.Password)\n\tassert.Equal(t, currentConfigs.OAuth2.ClientSecret, newConfigs.OAuth2.ClientSecret)\n\tassert.Equal(t, currentConfigs.OAuth2.RefreshToken, newConfigs.OAuth2.RefreshToken)\n\n\tclientSecret := kms.NewPlainSecret(\"plain secret\")\n\trefreshToken := kms.NewPlainSecret(\"plain token\")\n\tnewConfigs = &dataprovider.SMTPConfigs{\n\t\tPassword: kms.NewPlainSecret(\"pwd\"),\n\t\tOAuth2: dataprovider.SMTPOAuth2{\n\t\t\tClientSecret: clientSecret,\n\t\t\tRefreshToken: refreshToken,\n\t\t},\n\t}\n\tupdateSMTPSecrets(newConfigs, currentConfigs)\n\tassert.Equal(t, clientSecret, newConfigs.OAuth2.ClientSecret)\n\tassert.Equal(t, refreshToken, newConfigs.OAuth2.RefreshToken)\n}\n\nfunc TestOAuth2Redirect(t *testing.T) {\n\tserver := httpdServer{}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\n\trr := httptest.NewRecorder()\n\treq, err := http.NewRequest(http.MethodGet, webOAuth2RedirectPath+\"?state=invalid\", nil)\n\tassert.NoError(t, err)\n\tserver.handleOAuth2TokenRedirect(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nOAuth2ErrorTitle)\n\n\tip := \"127.1.1.4\"\n\ttokenString := createOAuth2Token(server.csrfTokenAuth, xid.New().String(), ip)\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodGet, webOAuth2RedirectPath+\"?state=\"+tokenString, nil) //nolint:goconst\n\tassert.NoError(t, err)\n\treq.RemoteAddr = ip\n\tserver.handleOAuth2TokenRedirect(rr, req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nOAuth2ErrorValidateState)\n}\n\nfunc TestOAuth2Token(t *testing.T) {\n\tserver := httpdServer{}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\t// invalid token\n\t_, err = verifyOAuth2Token(server.csrfTokenAuth, \"token\", \"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to verify OAuth2 state\")\n\t}\n\t// bad audience\n\tclaims := jwt.NewClaims(tokenAudienceAPI, \"\", getTokenDuration(tokenAudienceAPI))\n\n\ttokenString, err := server.csrfTokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\t_, err = verifyOAuth2Token(server.csrfTokenAuth, tokenString, \"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid OAuth2 state\")\n\t}\n\t// bad IP\n\ttokenString = createOAuth2Token(server.csrfTokenAuth, \"state\", \"127.1.1.1\")\n\t_, err = verifyOAuth2Token(server.csrfTokenAuth, tokenString, \"127.1.1.2\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid OAuth2 state\")\n\t}\n\t// ok\n\tstate := xid.New().String()\n\ttokenString = createOAuth2Token(server.csrfTokenAuth, state, \"127.1.1.3\")\n\ts, err := verifyOAuth2Token(server.csrfTokenAuth, tokenString, \"127.1.1.3\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, state, s)\n\t// no jti\n\tclaims = jwt.NewClaims(tokenAudienceOAuth2, \"127.1.1.4\", getTokenDuration(tokenAudienceOAuth2))\n\ttokenString, err = josejwt.Signed(server.csrfTokenAuth.Signer()).Claims(claims).Serialize()\n\tassert.NoError(t, err)\n\t_, err = verifyOAuth2Token(server.csrfTokenAuth, tokenString, \"127.1.1.4\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid OAuth2 state\")\n\t}\n\t// encode error\n\tserver.csrfTokenAuth.SetSigner(&failingJoseSigner{})\n\ttokenString = createOAuth2Token(server.csrfTokenAuth, xid.New().String(), \"\")\n\tassert.Empty(t, tokenString)\n\n\trr := httptest.NewRecorder()\n\ttestReq := make(map[string]any)\n\ttestReq[\"base_redirect_url\"] = \"http://localhost:8082\"\n\tasJSON, err := json.Marshal(testReq)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodPost, webOAuth2TokenPath, bytes.NewBuffer(asJSON))\n\tassert.NoError(t, err)\n\tserver.handleSMTPOAuth2TokenRequestPost(rr, req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"unable to create state token\")\n}\n\nfunc TestCSRFToken(t *testing.T) {\n\tserver := httpdServer{}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\t// invalid token\n\treq := &http.Request{}\n\terr = verifyCSRFToken(req, server.csrfTokenAuth)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to verify form token\")\n\t}\n\t// bad audience\n\tclaims := jwt.NewClaims(tokenAudienceAPI, \"\", getTokenDuration(tokenAudienceAPI))\n\ttokenString, err := server.csrfTokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\tvalues := url.Values{}\n\tvalues.Set(csrfFormToken, tokenString)\n\treq.Form = values\n\terr = verifyCSRFToken(req, server.csrfTokenAuth)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"form token is not valid\")\n\t}\n\n\t// bad IP\n\treq.RemoteAddr = \"127.1.1.1\"\n\ttokenString = createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, \"\", webBaseAdminPath)\n\tvalues.Set(csrfFormToken, tokenString)\n\treq.Form = values\n\treq.RemoteAddr = \"127.1.1.2\"\n\terr = verifyCSRFToken(req, server.csrfTokenAuth)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"form token is not valid\")\n\t}\n\n\tclaims = jwt.NewClaims(tokenAudienceAPI, \"\", getTokenDuration(tokenAudienceAPI))\n\ttokenString, err = server.csrfTokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, tokenString)\n\n\tr, err := GetHTTPRouter(Binding{\n\t\tAddress:         \"\",\n\t\tPort:            8080,\n\t\tEnableWebAdmin:  true,\n\t\tEnableWebClient: true,\n\t\tEnableRESTAPI:   true,\n\t\tRenderOpenAPI:   true,\n\t})\n\tassert.NoError(t, err)\n\tfn := server.verifyCSRFHeader(r)\n\trr := httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, \"username\"), nil)\n\tfn.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token\")\n\n\t// invalid audience\n\treq.Header.Set(csrfHeaderToken, tokenString)\n\trr = httptest.NewRecorder()\n\tfn.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"the token is not valid\")\n\n\t// invalid IP\n\ttokenString = createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, \"\", webBaseAdminPath)\n\treq.Header.Set(csrfHeaderToken, tokenString)\n\treq.RemoteAddr = \"172.16.1.2\"\n\trr = httptest.NewRecorder()\n\tfn.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"the token is not valid\")\n\n\tcsrfTokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\tcsrfTokenAuth.SetSigner(&failingJoseSigner{})\n\ttokenString = createCSRFToken(httptest.NewRecorder(), req, csrfTokenAuth, \"\", webBaseAdminPath)\n\tassert.Empty(t, tokenString)\n\trr = httptest.NewRecorder()\n\tcreateLoginCookie(rr, req, csrfTokenAuth, \"\", webBaseAdminPath, req.RemoteAddr)\n\tassert.Empty(t, rr.Header().Get(\"Set-Cookie\"))\n}\n\nfunc TestCreateShareCookieError(t *testing.T) {\n\tusername := \"share_user\"\n\tpwd := util.GenerateUniqueID()\n\tuser := &dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPassword: pwd,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\terr := dataprovider.AddUser(user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tshare := &dataprovider.Share{\n\t\tName:     \"test_share_cookie_error\",\n\t\tShareID:  util.GenerateUniqueID(),\n\t\tScope:    dataprovider.ShareScopeRead,\n\t\tPassword: pwd,\n\t\tPaths:    []string{\"/\"},\n\t\tUsername: username,\n\t}\n\terr = dataprovider.AddShare(share, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\ttokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\ttokenAuth.SetSigner(&failingJoseSigner{})\n\tcsrfTokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\n\tserver := httpdServer{\n\t\ttokenAuth:     tokenAuth,\n\t\tcsrfTokenAuth: csrfTokenAuth,\n\t}\n\n\tc := jwt.NewClaims(tokenAudienceWebLogin, \"127.0.0.1\", getTokenDuration(tokenAudienceWebLogin))\n\ttoken, err := server.csrfTokenAuth.Sign(c)\n\tassert.NoError(t, err)\n\tresp := c.BuildTokenResponse(token)\n\tparsedToken, err := jwt.VerifyToken(server.csrfTokenAuth, resp.Token)\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, share.ShareID, \"login\"), nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = \"127.0.0.1:4567\"\n\tctx := req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\tform := make(url.Values)\n\tform.Set(\"share_password\", pwd)\n\tform.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, \"\", webBaseClientPath))\n\trctx := chi.NewRouteContext()\n\trctx.URLParams.Add(\"id\", share.ShareID)\n\trr := httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, share.ShareID, \"login\"),\n\t\tbytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = \"127.0.0.1:2345\"\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", resp.Token))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq = req.WithContext(ctx)\n\treq = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))\n\tserver.handleClientShareLoginPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nError500Message)\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestCreateTokenError(t *testing.T) {\n\ttokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\ttokenAuth.SetSigner(&failingJoseSigner{})\n\tcsrfTokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\n\tserver := httpdServer{\n\t\ttokenAuth:     tokenAuth,\n\t\tcsrfTokenAuth: csrfTokenAuth,\n\t}\n\trr := httptest.NewRecorder()\n\tadmin := dataprovider.Admin{\n\t\tUsername: defaultAdminUsername,\n\t\tPassword: \"password\",\n\t}\n\treq, _ := http.NewRequest(http.MethodGet, tokenPath, nil)\n\n\tserver.generateAndSendToken(rr, req, admin, \"\")\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\n\trr = httptest.NewRecorder()\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"u\",\n\t\t\tPassword: util.GenerateUniqueID(),\n\t\t},\n\t}\n\treq, _ = http.NewRequest(http.MethodGet, userTokenPath, nil)\n\n\tserver.generateAndSendUserToken(rr, req, \"\", user)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\n\tc := &jwt.Claims{}\n\tc.ID = xid.New().String()\n\tc.SetExpiry(time.Now().Add(1 * time.Minute))\n\ttokenString, err := server.csrfTokenAuth.SignWithParams(c, tokenAudienceAPI, \"\", getTokenDuration(tokenAudienceAPI))\n\tassert.NoError(t, err)\n\ttoken := c.BuildTokenResponse(tokenString)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token.Token))\n\tparsedToken, err := jwt.VerifyRequest(server.csrfTokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx := req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\trr = httptest.NewRecorder()\n\tform := make(url.Values)\n\tform.Set(\"username\", admin.Username)\n\tform.Set(\"password\", admin.Password)\n\tform.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, xid.New().String(), webBaseAdminPath))\n\tcookie := rr.Header().Get(\"Set-Cookie\")\n\tassert.NotEmpty(t, cookie)\n\treq, _ = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Cookie\", cookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tparsedToken, err = jwt.VerifyRequest(server.csrfTokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx = req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\tserver.handleWebAdminLoginPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\t// req with no content type\n\treq, _ = http.NewRequest(http.MethodPost, webAdminLoginPath, nil)\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminLoginPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\treq, _ = http.NewRequest(http.MethodPost, webAdminSetupPath, nil)\n\trr = httptest.NewRecorder()\n\tserver.loginAdmin(rr, req, &admin, false, nil, \"\")\n\t// req with no POST body\n\treq, _ = http.NewRequest(http.MethodGet, webAdminLoginPath+\"?a=a%C3%AO%GG\", nil)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminLoginPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\n\treq, _ = http.NewRequest(http.MethodGet, webAdminLoginPath+\"?a=a%C3%A1%G2\", nil)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminChangePwdPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodGet, webAdminLoginPath+\"?a=a%C3%A2%G3\", nil)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t_, err = getAdminFromPostFields(req)\n\tassert.Error(t, err)\n\n\treq, _ = http.NewRequest(http.MethodPost, webAdminEventActionPath+\"?a=a%C3%A2%GG\", nil)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t_, err = getEventActionFromPostFields(req)\n\tassert.Error(t, err)\n\n\treq, _ = http.NewRequest(http.MethodPost, webAdminEventRulePath+\"?a=a%C3%A3%GG\", nil)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t_, err = getEventRuleFromPostFields(req)\n\tassert.Error(t, err)\n\n\treq, _ = http.NewRequest(http.MethodPost, webIPListPath+\"/1?a=a%C3%AO%GG\", nil)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t_, err = getIPListEntryFromPostFields(req, dataprovider.IPListTypeAllowList)\n\tassert.Error(t, err)\n\n\treq, _ = http.NewRequest(http.MethodPost, path.Join(webClientSharePath, \"shareID\", \"login?a=a%C3%AO%GG\"), bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleClientShareLoginPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\n\treq, _ = http.NewRequest(http.MethodPost, webClientLoginPath+\"?a=a%C3%AO%GG\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientLoginPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\n\treq, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath+\"?a=a%C3%AO%GA\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientChangePwdPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath+\"?a=a%C3%AO%GB\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientProfilePost(rr, req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())\n\n\treq, _ = http.NewRequest(http.MethodPost, webAdminProfilePath+\"?a=a%C3%AO%GB\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminProfilePost(rr, req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())\n\n\treq, _ = http.NewRequest(http.MethodPost, webAdminTwoFactorPath+\"?a=a%C3%AO%GC\", bytes.NewBuffer([]byte(form.Encode())))\n\treq = req.WithContext(jwt.NewContext(req.Context(), &jwt.Claims{}, nil))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminTwoFactorPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath+\"?a=a%C3%AO%GD\", bytes.NewBuffer([]byte(form.Encode())))\n\treq = req.WithContext(jwt.NewContext(req.Context(), &jwt.Claims{}, nil))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminTwoFactorRecoveryPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webClientTwoFactorPath+\"?a=a%C3%AO%GC\", bytes.NewBuffer([]byte(form.Encode())))\n\treq = req.WithContext(jwt.NewContext(req.Context(), &jwt.Claims{}, nil))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientTwoFactorPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath+\"?a=a%C3%AO%GD\", bytes.NewBuffer([]byte(form.Encode())))\n\treq = req.WithContext(jwt.NewContext(req.Context(), &jwt.Claims{}, nil))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientTwoFactorRecoveryPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webAdminForgotPwdPath+\"?a=a%C3%A1%GD\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminForgotPwdPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webClientForgotPwdPath+\"?a=a%C2%A1%GD\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientForgotPwdPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webAdminResetPwdPath+\"?a=a%C3%AO%JD\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebAdminPasswordResetPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webAdminRolePath+\"?a=a%C3%AO%JE\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebAddRolePost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webClientResetPwdPath+\"?a=a%C3%AO%JD\", bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\tserver.handleWebClientPasswordResetPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)\n\n\treq, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath+\"?a=a%K3%AO%GA\", bytes.NewBuffer([]byte(form.Encode())))\n\t_, err = getShareFromPostFields(req)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid URL escape\")\n\t}\n\n\tusername := \"webclientuser\"\n\tuser = dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:    username,\n\t\t\tPassword:    \"clientpwd\",\n\t\t\tHomeDir:     filepath.Join(os.TempDir(), username),\n\t\t\tStatus:      1,\n\t\t\tDescription: \"test user\",\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tuser.Filters.AllowAPIKeyAuth = true\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webClientLoginPath, nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token.Token))\n\tparsedToken, err = jwt.VerifyRequest(server.csrfTokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx = req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\trr = httptest.NewRecorder()\n\tform = make(url.Values)\n\tform.Set(\"username\", user.Username)\n\tform.Set(\"password\", \"clientpwd\")\n\tform.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, \"\", webBaseClientPath))\n\treq, _ = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tserver.handleWebClientLoginPost(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\n\terr = authenticateUserWithAPIKey(username, \"\", server.tokenAuth, req)\n\tassert.Error(t, err)\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.HomeDir)\n\tassert.NoError(t, err)\n\n\tadmin.Username += \"1\"\n\tadmin.Status = 1\n\tadmin.Filters.AllowAPIKeyAuth = true\n\tadmin.Permissions = []string{dataprovider.PermAdminAny}\n\terr = dataprovider.AddAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = authenticateAdminWithAPIKey(admin.Username, \"\", server.tokenAuth, req)\n\tassert.Error(t, err)\n\n\terr = dataprovider.DeleteAdmin(admin.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestAPIKeyAuthForbidden(t *testing.T) {\n\tr, err := GetHTTPRouter(Binding{\n\t\tAddress:         \"\",\n\t\tPort:            8080,\n\t\tEnableWebAdmin:  true,\n\t\tEnableWebClient: true,\n\t\tEnableRESTAPI:   true,\n\t\tRenderOpenAPI:   true,\n\t})\n\trequire.NoError(t, err)\n\tfn := forbidAPIKeyAuthentication(r)\n\trr := httptest.NewRecorder()\n\treq, _ := http.NewRequest(http.MethodGet, versionPath, nil)\n\tfn.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"Invalid token claims\")\n}\n\nfunc TestJWTTokenValidation(t *testing.T) {\n\ttokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\tclaims := &jwt.Claims{\n\t\tUsername: defaultAdminUsername,\n\t}\n\tclaims.SetExpiry(time.Now().UTC().Add(-1 * time.Hour))\n\t_, err = tokenAuth.SignWithParams(claims, tokenAudienceWebAdmin, \"\", getTokenDuration(tokenAudienceWebAdmin))\n\trequire.NoError(t, err)\n\n\tserver := httpdServer{\n\t\tbinding: Binding{\n\t\t\tAddress:         \"\",\n\t\t\tPort:            8080,\n\t\t\tEnableWebAdmin:  true,\n\t\t\tEnableWebClient: true,\n\t\t\tEnableRESTAPI:   true,\n\t\t\tRenderOpenAPI:   true,\n\t\t},\n\t}\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\tr := server.router\n\tfn := jwtAuthenticatorAPI(r)\n\trr := httptest.NewRecorder()\n\treq, _ := http.NewRequest(http.MethodGet, userPath, nil)\n\tctx := jwt.NewContext(req.Context(), claims, nil)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\tfn = jwtAuthenticatorWebAdmin(r)\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webUserPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\n\tfn = jwtAuthenticatorWebClient(r)\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\n\terrTest := errors.New(\"test error\")\n\tpermFn := server.checkPerms(dataprovider.PermAdminAny)\n\tfn = permFn(r)\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, userPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, errTest)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\tpermFn = server.checkPerms(dataprovider.PermAdminAny)\n\tfn = permFn(r)\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webUserPath, nil)\n\treq.RequestURI = webUserPath\n\tctx = jwt.NewContext(req.Context(), claims, errTest)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\tpermClientFn := server.checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)\n\tfn = permClientFn(r)\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)\n\treq.RequestURI = webClientProfilePath\n\tctx = jwt.NewContext(req.Context(), claims, errTest)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodPost, userProfilePath, nil)\n\treq.RequestURI = userProfilePath\n\tctx = jwt.NewContext(req.Context(), claims, errTest)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\tfn = server.checkAuthRequirements(r)\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)\n\treq.RequestURI = webClientProfilePath\n\tctx = jwt.NewContext(req.Context(), claims, errTest)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\tfn = server.checkAuthRequirements(r)\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodPost, webGroupsPath, nil)\n\treq.RequestURI = webGroupsPath\n\tctx = jwt.NewContext(req.Context(), claims, errTest)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodPost, userSharesPath, nil)\n\treq.RequestURI = userSharesPath\n\tctx = jwt.NewContext(req.Context(), claims, errTest)\n\tfn.ServeHTTP(rr, req.WithContext(ctx))\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n}\n\nfunc TestUpdateContextFromCookie(t *testing.T) {\n\ttokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\tserver := httpdServer{\n\t\ttokenAuth: tokenAuth,\n\t}\n\treq, _ := http.NewRequest(http.MethodGet, tokenPath, nil)\n\tclaims := jwt.NewClaims(tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\t_, err = server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\n\tctx := jwt.NewContext(req.Context(), claims, nil)\n\treq = server.updateContextFromCookie(req.WithContext(ctx))\n\ttoken, err := jwt.FromContext(req.Context())\n\trequire.NoError(t, err)\n\trequire.True(t, token.Audience.Contains(tokenAudienceWebClient))\n\trequire.NotEmpty(t, token.ID)\n}\n\nfunc TestCookieExpiration(t *testing.T) {\n\ttokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\tserver := httpdServer{\n\t\ttokenAuth: tokenAuth,\n\t}\n\terr = errors.New(\"test error\")\n\trr := httptest.NewRecorder()\n\treq, _ := http.NewRequest(http.MethodGet, tokenPath, nil)\n\tctx := jwt.NewContext(req.Context(), nil, err)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie := rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\treq, _ = http.NewRequest(http.MethodGet, tokenPath, nil)\n\tclaims := jwt.NewClaims(tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\t_, err = server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\tadmin := dataprovider.Admin{\n\t\tUsername:    \"newtestadmin\",\n\t\tPassword:    \"password\",\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t}\n\tclaims = jwt.NewClaims(tokenAudienceAPI, \"\", getTokenDuration(tokenAudienceAPI))\n\tclaims.Username = admin.Username\n\tclaims.Permissions = admin.Permissions\n\tclaims.Subject = admin.GetSignature()\n\tclaims.SetExpiry(time.Now().Add(1 * time.Minute))\n\t_, err = server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodGet, tokenPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\tadmin.Status = 0\n\terr = dataprovider.AddAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodGet, tokenPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\tadmin.Status = 1\n\tadmin.Filters.AllowList = []string{\"172.16.1.0/24\"}\n\terr = dataprovider.UpdateAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodGet, tokenPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\tadmin, err = dataprovider.AdminExists(admin.Username)\n\tassert.NoError(t, err)\n\ttokenID := xid.New().String()\n\tclaims = jwt.NewClaims(tokenAudienceAPI, \"\", getTokenDuration(tokenAudienceAPI))\n\tclaims.ID = tokenID\n\tclaims.Username = admin.Username\n\tclaims.Permissions = admin.Permissions\n\tclaims.Subject = admin.GetSignature()\n\tclaims.SetExpiry(time.Now().Add(1 * time.Minute))\n\t_, err = server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodGet, tokenPath, nil)\n\treq.RemoteAddr = \"192.168.8.1:1234\"\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\treq, _ = http.NewRequest(http.MethodGet, tokenPath, nil)\n\treq.RemoteAddr = \"172.16.1.12:4567\"\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.True(t, strings.HasPrefix(cookie, \"jwt=\"))\n\treq.Header.Set(\"Cookie\", cookie)\n\tc, err := jwt.VerifyRequest(server.tokenAuth, req, jwt.TokenFromCookie)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, tokenID, c.ID)\n\t}\n\n\terr = dataprovider.DeleteAdmin(admin.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// now check client cookie expiration\n\tusername := \"client\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:    username,\n\t\t\tPassword:    \"clientpwd\",\n\t\t\tHomeDir:     filepath.Join(os.TempDir(), username),\n\t\t\tStatus:      1,\n\t\t\tDescription: \"test user\",\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{\"*\"}\n\n\tclaims = jwt.NewClaims(tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\tclaims.ID = tokenID\n\tclaims.Username = user.Username\n\tclaims.Permissions = user.Filters.WebClient\n\tclaims.Subject = user.GetSignature()\n\tclaims.SetExpiry(time.Now().Add(1 * time.Minute))\n\t_, err = server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\t// the password will be hashed and so the signature will change\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\tuser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tuser.Filters.AllowedIP = []string{\"172.16.4.0/24\"}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tuser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\tissuedAt := time.Now().Add(-1 * time.Minute)\n\texpiresAt := time.Now().Add(1 * time.Minute)\n\n\tclaims = jwt.NewClaims(tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\tclaims.ID = tokenID\n\tclaims.Username = user.Username\n\tclaims.Permissions = user.Filters.WebClient\n\tclaims.Subject = user.GetSignature()\n\tclaims.SetExpiry(expiresAt)\n\tclaims.SetIssuedAt(issuedAt)\n\t_, err = server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.RemoteAddr = \"172.16.3.12:4567\"\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.RemoteAddr = \"172.16.4.16:4567\"\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.NotEmpty(t, cookie)\n\treq.Header.Set(\"Cookie\", cookie)\n\tc, err = jwt.VerifyRequest(server.tokenAuth, req, jwt.TokenFromCookie)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, tokenID, c.ID)\n\t\tassert.Equal(t, issuedAt.Unix(), c.IssuedAt.Time().Unix())\n\t\tassert.NotEqual(t, expiresAt.Unix(), c.Expiry.Time().Unix())\n\t}\n\t// test a cookie issued more that 12 hours ago\n\tclaims = jwt.NewClaims(tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\tclaims.ID = tokenID\n\tclaims.Username = user.Username\n\tclaims.Permissions = user.Filters.WebClient\n\tclaims.Subject = user.GetSignature()\n\tclaims.SetExpiry(expiresAt)\n\tclaims.SetIssuedAt(time.Now().Add(-24 * time.Hour))\n\t_, err = server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.RemoteAddr = \"172.16.4.16:6789\"\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\t// test a disabled user\n\tuser.Status = 0\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser, err = dataprovider.UserExists(user.Username, \"\")\n\tassert.NoError(t, err)\n\n\tclaims = jwt.NewClaims(tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\tclaims.ID = tokenID\n\tclaims.Username = user.Username\n\tclaims.Permissions = user.Filters.WebClient\n\tclaims.Subject = user.GetSignature()\n\tclaims.SetExpiry(time.Now().Add(1 * time.Minute))\n\tclaims.SetIssuedAt(issuedAt)\n\t_, err = server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tctx = jwt.NewContext(req.Context(), claims, nil)\n\tserver.checkCookieExpiration(rr, req.WithContext(ctx))\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Empty(t, cookie)\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestGetURLParam(t *testing.T) {\n\treq, _ := http.NewRequest(http.MethodGet, adminPwdPath, nil)\n\trctx := chi.NewRouteContext()\n\trctx.URLParams.Add(\"val\", \"testuser%C3%A0\")\n\trctx.URLParams.Add(\"inval\", \"testuser%C3%AO%GG\")\n\treq = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))\n\tescaped := getURLParam(req, \"val\")\n\tassert.Equal(t, \"testuserà\", escaped)\n\tescaped = getURLParam(req, \"inval\")\n\tassert.Equal(t, \"testuser%C3%AO%GG\", escaped)\n}\n\nfunc TestChangePwdValidationErrors(t *testing.T) {\n\terr := doChangeAdminPassword(nil, \"\", \"\", \"\")\n\trequire.Error(t, err)\n\terr = doChangeAdminPassword(nil, \"a\", \"b\", \"c\")\n\trequire.Error(t, err)\n\terr = doChangeAdminPassword(nil, \"a\", \"a\", \"a\")\n\trequire.Error(t, err)\n\n\treq, _ := http.NewRequest(http.MethodPut, adminPwdPath, nil)\n\treq = req.WithContext(jwt.NewContext(req.Context(), &jwt.Claims{Claims: josejwt.Claims{ID: xid.New().String()}}, nil))\n\terr = doChangeAdminPassword(req, \"currentpwd\", \"newpwd\", \"newpwd\")\n\tassert.Error(t, err)\n}\n\nfunc TestRenderUnexistingFolder(t *testing.T) {\n\trr := httptest.NewRecorder()\n\treq, _ := http.NewRequest(http.MethodPost, folderPath, nil)\n\trenderFolder(rr, req, \"path not mapped\", &jwt.Claims{}, http.StatusOK)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n}\n\nfunc TestCloseConnectionHandler(t *testing.T) {\n\ttokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\tclaims := jwt.NewClaims(tokenAudienceAPI, \"\", getTokenDuration(tokenAudienceAPI))\n\tclaims.Username = defaultAdminUsername\n\tclaims.SetExpiry(time.Now().UTC().Add(1 * time.Hour))\n\t_, err = tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodDelete, activeConnectionsPath+\"/connectionID\", nil)\n\tassert.NoError(t, err)\n\trctx := chi.NewRouteContext()\n\trctx.URLParams.Add(\"connectionID\", \"\")\n\treq = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))\n\treq = req.WithContext(context.WithValue(req.Context(), jwt.TokenCtxKey, claims))\n\trr := httptest.NewRecorder()\n\thandleCloseConnection(rr, req)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), \"connectionID is mandatory\")\n}\n\nfunc TestRenderInvalidTemplate(t *testing.T) {\n\ttmpl, err := template.New(\"test\").Parse(\"{{.Count}}\")\n\tif assert.NoError(t, err) {\n\t\tnoMatchTmpl := \"no_match\"\n\t\tadminTemplates[noMatchTmpl] = tmpl\n\t\trw := httptest.NewRecorder()\n\t\trenderAdminTemplate(rw, noMatchTmpl, map[string]string{})\n\t\tassert.Equal(t, http.StatusInternalServerError, rw.Code)\n\t\tclientTemplates[noMatchTmpl] = tmpl\n\t\trenderClientTemplate(rw, noMatchTmpl, map[string]string{})\n\t\tassert.Equal(t, http.StatusInternalServerError, rw.Code)\n\t}\n}\n\nfunc TestQuotaScanInvalidFs(t *testing.T) {\n\tuser := &dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"test\",\n\t\t\tHomeDir:  os.TempDir(),\n\t\t},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.S3FilesystemProvider,\n\t\t},\n\t}\n\tcommon.QuotaScans.AddUserQuotaScan(user.Username, \"\")\n\terr := doUserQuotaScan(user)\n\tassert.Error(t, err)\n}\n\nfunc TestVerifyTLSConnection(t *testing.T) {\n\toldCertMgr := certMgr\n\n\tcaCrlPath := filepath.Join(os.TempDir(), \"testcrl.crt\")\n\tcertPath := filepath.Join(os.TempDir(), \"testh.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"testh.key\")\n\terr := os.WriteFile(caCrlPath, []byte(caCRL), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(certPath, []byte(httpdCert), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyPath, []byte(httpdKey), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tkeyPairs := []common.TLSKeyPair{\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t},\n\t}\n\tcertMgr, err = common.NewCertManager(keyPairs, \"\", \"httpd_test\")\n\tassert.NoError(t, err)\n\n\tcertMgr.SetCARevocationLists([]string{caCrlPath})\n\terr = certMgr.LoadCRLs()\n\tassert.NoError(t, err)\n\n\tcrt, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\tx509crt, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tserver := httpdServer{}\n\tstate := tls.ConnectionState{\n\t\tPeerCertificates: []*x509.Certificate{x509crt},\n\t}\n\n\terr = server.verifyTLSConnection(state)\n\tassert.Error(t, err) // no verified certification chain\n\n\tcrt, err = tls.X509KeyPair([]byte(caCRT), []byte(caKey))\n\tassert.NoError(t, err)\n\n\tx509CAcrt, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tstate.VerifiedChains = append(state.VerifiedChains, []*x509.Certificate{x509crt, x509CAcrt})\n\terr = server.verifyTLSConnection(state)\n\tassert.NoError(t, err)\n\n\tcrt, err = tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))\n\tassert.NoError(t, err)\n\tx509crtRevoked, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tstate.VerifiedChains = append(state.VerifiedChains, []*x509.Certificate{x509crtRevoked, x509CAcrt})\n\tstate.PeerCertificates = []*x509.Certificate{x509crtRevoked}\n\terr = server.verifyTLSConnection(state)\n\tassert.EqualError(t, err, common.ErrCrtRevoked.Error())\n\n\terr = os.Remove(caCrlPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(certPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n\n\tcertMgr = oldCertMgr\n}\n\nfunc TestGetFolderFromTemplate(t *testing.T) {\n\tfolder := vfs.BaseVirtualFolder{\n\t\tMappedPath:  \"Folder%name%\",\n\t\tDescription: \"Folder %name% desc\",\n\t}\n\tfolderName := \"folderTemplate\"\n\tfolderTemplate := getFolderFromTemplate(folder, folderName)\n\trequire.Equal(t, folderName, folderTemplate.Name)\n\trequire.Equal(t, fmt.Sprintf(\"Folder%v\", folderName), folderTemplate.MappedPath)\n\trequire.Equal(t, fmt.Sprintf(\"Folder %v desc\", folderName), folderTemplate.Description)\n\n\tfolder.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tfolder.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"%name%\")\n\tfolderTemplate = getFolderFromTemplate(folder, folderName)\n\trequire.Equal(t, folderName, folderTemplate.FsConfig.CryptConfig.Passphrase.GetPayload())\n\n\tfolder.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tfolder.FsConfig.GCSConfig.KeyPrefix = \"prefix%name%/\"\n\tfolderTemplate = getFolderFromTemplate(folder, folderName)\n\trequire.Equal(t, fmt.Sprintf(\"prefix%v/\", folderName), folderTemplate.FsConfig.GCSConfig.KeyPrefix)\n\n\tfolder.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tfolder.FsConfig.AzBlobConfig.KeyPrefix = \"a%name%\"\n\tfolder.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(\"pwd%name%\")\n\tfolderTemplate = getFolderFromTemplate(folder, folderName)\n\trequire.Equal(t, \"a\"+folderName, folderTemplate.FsConfig.AzBlobConfig.KeyPrefix)\n\trequire.Equal(t, \"pwd\"+folderName, folderTemplate.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\n\tfolder.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tfolder.FsConfig.SFTPConfig.Prefix = \"%name%\"\n\tfolder.FsConfig.SFTPConfig.Username = \"sftp_%name%\"\n\tfolder.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(\"sftp%name%\")\n\tfolderTemplate = getFolderFromTemplate(folder, folderName)\n\trequire.Equal(t, folderName, folderTemplate.FsConfig.SFTPConfig.Prefix)\n\trequire.Equal(t, \"sftp_\"+folderName, folderTemplate.FsConfig.SFTPConfig.Username)\n\trequire.Equal(t, \"sftp\"+folderName, folderTemplate.FsConfig.SFTPConfig.Password.GetPayload())\n}\n\nfunc TestGetUserFromTemplate(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tStatus: 1,\n\t\t},\n\t}\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: \"Folder%username%\",\n\t\t},\n\t})\n\n\tusername := \"userTemplate\"\n\tpassword := \"pwdTemplate\"\n\ttemplateFields := userTemplateFields{\n\t\tUsername: username,\n\t\tPassword: password,\n\t}\n\n\tuserTemplate := getUserFromTemplate(user, templateFields)\n\trequire.Len(t, userTemplate.VirtualFolders, 1)\n\trequire.Equal(t, \"Folder\"+username, userTemplate.VirtualFolders[0].Name)\n\n\tuser.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"%password%\")\n\tuserTemplate = getUserFromTemplate(user, templateFields)\n\trequire.Equal(t, password, userTemplate.FsConfig.CryptConfig.Passphrase.GetPayload())\n\n\tuser.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tuser.FsConfig.GCSConfig.KeyPrefix = \"%username%%password%\"\n\tuserTemplate = getUserFromTemplate(user, templateFields)\n\trequire.Equal(t, username+password, userTemplate.FsConfig.GCSConfig.KeyPrefix)\n\n\tuser.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tuser.FsConfig.AzBlobConfig.KeyPrefix = \"a%username%\"\n\tuser.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(\"pwd%password%%username%\")\n\tuserTemplate = getUserFromTemplate(user, templateFields)\n\trequire.Equal(t, \"a\"+username, userTemplate.FsConfig.AzBlobConfig.KeyPrefix)\n\trequire.Equal(t, \"pwd\"+password+username, userTemplate.FsConfig.AzBlobConfig.AccountKey.GetPayload())\n\n\tuser.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser.FsConfig.SFTPConfig.Prefix = \"%username%\"\n\tuser.FsConfig.SFTPConfig.Username = \"sftp_%username%\"\n\tuser.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(\"sftp%password%\")\n\tuserTemplate = getUserFromTemplate(user, templateFields)\n\trequire.Equal(t, username, userTemplate.FsConfig.SFTPConfig.Prefix)\n\trequire.Equal(t, \"sftp_\"+username, userTemplate.FsConfig.SFTPConfig.Username)\n\trequire.Equal(t, \"sftp\"+password, userTemplate.FsConfig.SFTPConfig.Password.GetPayload())\n}\n\nfunc TestJWTTokenCleanup(t *testing.T) {\n\ttokenAuth, err := jwt.NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\tserver := httpdServer{\n\t\ttokenAuth: tokenAuth,\n\t}\n\tadmin := dataprovider.Admin{\n\t\tUsername:    \"newtestadmin\",\n\t\tPassword:    \"password\",\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t}\n\tclaims := jwt.NewClaims(tokenAudienceAPI, \"\", getTokenDuration(tokenAudienceAPI))\n\tclaims.Username = admin.Username\n\tclaims.Permissions = admin.Permissions\n\tclaims.Subject = admin.GetSignature()\n\tclaims.SetExpiry(time.Now().Add(1 * time.Minute))\n\ttoken, err := server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\n\treq, _ := http.NewRequest(http.MethodGet, versionPath, nil)\n\tassert.True(t, isTokenInvalidated(req))\n\n\tfakeToken := \"abc\"\n\tinvalidateTokenString(req, fakeToken, -100*time.Millisecond)\n\tassert.True(t, invalidatedJWTTokens.Get(fakeToken))\n\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\n\tinvalidatedJWTTokens.Add(token, time.Now().Add(-getTokenDuration(tokenAudienceWebAdmin)).UTC())\n\trequire.True(t, isTokenInvalidated(req))\n\tstartCleanupTicker(100 * time.Millisecond)\n\tassert.Eventually(t, func() bool { return !isTokenInvalidated(req) }, 1*time.Second, 200*time.Millisecond)\n\tassert.False(t, invalidatedJWTTokens.Get(fakeToken))\n\tstopCleanupTicker()\n}\n\nfunc TestDbTokenManager(t *testing.T) {\n\tif !isSharedProviderSupported() {\n\t\tt.Skip(\"this test it is not available with this provider\")\n\t}\n\tmgr := newTokenManager(1)\n\tdbTokenManager := mgr.(*dbTokenManager)\n\ttestToken := \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiV2ViQWRtaW4iLCI6OjEiXSwiZXhwIjoxNjk4NjYwMDM4LCJqdGkiOiJja3ZuazVrYjF1aHUzZXRmZmhyZyIsIm5iZiI6MTY5ODY1ODgwOCwicGVybWlzc2lvbnMiOlsiKiJdLCJzdWIiOiIxNjk3ODIwNDM3NTMyIiwidXNlcm5hbWUiOiJhZG1pbiJ9.LXuFFksvnSuzHqHat6r70yR0jEulNRju7m7SaWrOfy8; csrftoken=mP0C7DqjwpAXsptO2gGCaYBkYw3oNMWB\"\n\tkey := dbTokenManager.getKey(testToken)\n\trequire.Len(t, key, 64)\n\tdbTokenManager.Add(testToken, time.Now().Add(-getTokenDuration(tokenAudienceWebClient)).UTC())\n\tisInvalidated := dbTokenManager.Get(testToken)\n\tassert.True(t, isInvalidated)\n\tdbTokenManager.Cleanup()\n\tisInvalidated = dbTokenManager.Get(testToken)\n\tassert.False(t, isInvalidated)\n\tdbTokenManager.Add(testToken, time.Now().Add(getTokenDuration(tokenAudienceWebAdmin)).UTC())\n\tisInvalidated = dbTokenManager.Get(testToken)\n\tassert.True(t, isInvalidated)\n\tdbTokenManager.Cleanup()\n\tisInvalidated = dbTokenManager.Get(testToken)\n\tassert.True(t, isInvalidated)\n\terr := dataprovider.DeleteSharedSession(key, dataprovider.SessionTypeInvalidToken)\n\tassert.NoError(t, err)\n}\n\nfunc TestDatabaseSharedSessions(t *testing.T) {\n\tif !isSharedProviderSupported() {\n\t\tt.Skip(\"this test it is not available with this provider\")\n\t}\n\tsession1 := dataprovider.Session{\n\t\tKey:       \"1\",\n\t\tData:      map[string]string{\"a\": \"b\"},\n\t\tType:      dataprovider.SessionTypeOIDCAuth,\n\t\tTimestamp: 10,\n\t}\n\terr := dataprovider.AddSharedSession(session1)\n\tassert.NoError(t, err)\n\t// Adding another session with the same key but a different type should work\n\tsession2 := session1\n\tsession2.Type = dataprovider.SessionTypeOIDCToken\n\terr = dataprovider.AddSharedSession(session2)\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteSharedSession(session1.Key, dataprovider.SessionTypeInvalidToken)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\t_, err = dataprovider.GetSharedSession(session1.Key, dataprovider.SessionTypeResetCode)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\tsession1Get, err := dataprovider.GetSharedSession(session1.Key, dataprovider.SessionTypeOIDCAuth)\n\tassert.NoError(t, err)\n\tassert.Equal(t, session1.Timestamp, session1Get.Timestamp)\n\tvar stored map[string]string\n\terr = json.Unmarshal(session1Get.Data.([]byte), &stored)\n\tassert.NoError(t, err)\n\tassert.Equal(t, session1.Data, stored)\n\tsession1.Timestamp = 20\n\tsession1.Data = map[string]string{\"c\": \"d\"}\n\terr = dataprovider.AddSharedSession(session1)\n\tassert.NoError(t, err)\n\tsession1Get, err = dataprovider.GetSharedSession(session1.Key, dataprovider.SessionTypeOIDCAuth)\n\tassert.NoError(t, err)\n\tassert.Equal(t, session1.Timestamp, session1Get.Timestamp)\n\tstored = make(map[string]string)\n\terr = json.Unmarshal(session1Get.Data.([]byte), &stored)\n\tassert.NoError(t, err)\n\tassert.Equal(t, session1.Data, stored)\n\terr = dataprovider.DeleteSharedSession(session1.Key, dataprovider.SessionTypeOIDCAuth)\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteSharedSession(session2.Key, dataprovider.SessionTypeOIDCToken)\n\tassert.NoError(t, err)\n\t_, err = dataprovider.GetSharedSession(session1.Key, dataprovider.SessionTypeOIDCAuth)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\t_, err = dataprovider.GetSharedSession(session2.Key, dataprovider.SessionTypeOIDCToken)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n}\n\nfunc TestAllowedProxyUnixDomainSocket(t *testing.T) {\n\tb := Binding{\n\t\tAddress:      filepath.Join(os.TempDir(), \"sock\"),\n\t\tProxyAllowed: []string{\"127.0.0.1\", \"127.0.1.1\"},\n\t}\n\terr := b.parseAllowedProxy()\n\tassert.NoError(t, err)\n\tif assert.Len(t, b.allowHeadersFrom, 1) {\n\t\tassert.True(t, b.allowHeadersFrom[0](nil))\n\t}\n}\n\nfunc TestProxyListenerWrapper(t *testing.T) {\n\tb := Binding{\n\t\tProxyMode: 0,\n\t}\n\trequire.Nil(t, b.listenerWrapper())\n\tb.ProxyMode = 1\n\trequire.NotNil(t, b.listenerWrapper())\n}\n\nfunc TestProxyHeaders(t *testing.T) {\n\tusername := \"adminTest\"\n\tpassword := \"testPwd\"\n\tadmin := dataprovider.Admin{\n\t\tUsername:    username,\n\t\tPassword:    password,\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t\tStatus:      1,\n\t\tFilters: dataprovider.AdminFilters{\n\t\t\tAllowList: []string{\"172.19.2.0/24\"},\n\t\t},\n\t}\n\n\terr := dataprovider.AddAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\ttestIP := \"10.29.1.9\"\n\tvalidForwardedFor := \"172.19.2.6\"\n\tb := Binding{\n\t\tAddress:             \"\",\n\t\tPort:                8080,\n\t\tEnableWebAdmin:      true,\n\t\tEnableWebClient:     false,\n\t\tEnableRESTAPI:       true,\n\t\tProxyAllowed:        []string{testIP, \"10.8.0.0/30\"},\n\t\tClientIPProxyHeader: \"x-forwarded-for\",\n\t}\n\terr = b.parseAllowedProxy()\n\tassert.NoError(t, err)\n\tserver := newHttpdServer(b, \"\", \"\", CorsConfig{Enabled: true}, \"\")\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\ttestServer := httptest.NewServer(server.router)\n\tdefer testServer.Close()\n\n\treq, err := http.NewRequest(http.MethodGet, tokenPath, nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"X-Forwarded-For\", validForwardedFor)\n\treq.Header.Set(xForwardedProto, \"https\")\n\treq.RemoteAddr = \"127.0.0.1:123\"\n\treq.SetBasicAuth(username, password)\n\trr := httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\tassert.NotContains(t, rr.Body.String(), \"login from IP 127.0.0.1 not allowed\")\n\n\treq.RemoteAddr = testIP\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\n\treq.RemoteAddr = \"10.8.0.2\"\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = testIP\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tcookie := rr.Header().Get(\"Set-Cookie\")\n\tassert.NotEmpty(t, cookie)\n\treq.Header.Set(\"Cookie\", cookie)\n\tparsedToken, err := jwt.VerifyRequest(server.csrfTokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx := req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\tform := make(url.Values)\n\tform.Set(\"username\", username)\n\tform.Set(\"password\", password)\n\tform.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, \"\", webBaseAdminPath))\n\treq, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = testIP\n\treq.Header.Set(\"Cookie\", cookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = validForwardedFor\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tloginCookie := rr.Header().Get(\"Set-Cookie\")\n\tassert.NotEmpty(t, loginCookie)\n\treq.Header.Set(\"Cookie\", loginCookie)\n\tparsedToken, err = jwt.VerifyRequest(server.csrfTokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx = req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\tform.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, \"\", webBaseAdminPath))\n\treq, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = testIP\n\treq.Header.Set(\"Cookie\", loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"X-Forwarded-For\", validForwardedFor)\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusFound, rr.Code, rr.Body.String())\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.NotContains(t, cookie, \"Secure\")\n\n\t// The login cookie is invalidated after a successful login, the same request will fail\n\treq, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = testIP\n\treq.Header.Set(\"Cookie\", loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"X-Forwarded-For\", validForwardedFor)\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = validForwardedFor\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tloginCookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.NotEmpty(t, loginCookie)\n\treq.Header.Set(\"Cookie\", loginCookie)\n\tparsedToken, err = jwt.VerifyRequest(server.csrfTokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx = req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\tform.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, \"\", webBaseAdminPath))\n\treq, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = testIP\n\treq.Header.Set(\"Cookie\", loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"X-Forwarded-For\", validForwardedFor)\n\treq.Header.Set(xForwardedProto, \"https\")\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusFound, rr.Code, rr.Body.String())\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.Contains(t, cookie, \"Secure\")\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = validForwardedFor\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\tloginCookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.NotEmpty(t, loginCookie)\n\treq.Header.Set(\"Cookie\", loginCookie)\n\tparsedToken, err = jwt.VerifyRequest(server.csrfTokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx = req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\tform.Set(csrfFormToken, createCSRFToken(httptest.NewRecorder(), req, server.csrfTokenAuth, \"\", webBaseAdminPath))\n\treq, err = http.NewRequest(http.MethodPost, webAdminLoginPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq.RemoteAddr = testIP\n\treq.Header.Set(\"Cookie\", loginCookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"X-Forwarded-For\", validForwardedFor)\n\treq.Header.Set(xForwardedProto, \"http\")\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusFound, rr.Code, rr.Body.String())\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tassert.NotContains(t, cookie, \"Secure\")\n\n\terr = dataprovider.DeleteAdmin(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestRecoverer(t *testing.T) {\n\trecoveryPath := \"/recovery\"\n\tb := Binding{\n\t\tAddress:         \"\",\n\t\tPort:            8080,\n\t\tEnableWebAdmin:  true,\n\t\tEnableWebClient: false,\n\t\tEnableRESTAPI:   true,\n\t}\n\tserver := newHttpdServer(b, \"../static\", \"\", CorsConfig{}, \"../openapi\")\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\tserver.router.Get(recoveryPath, func(_ http.ResponseWriter, _ *http.Request) {\n\t\tpanic(\"panic\")\n\t})\n\ttestServer := httptest.NewServer(server.router)\n\tdefer testServer.Close()\n\n\treq, err := http.NewRequest(http.MethodGet, recoveryPath, nil)\n\tassert.NoError(t, err)\n\trr := httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())\n\n\tserver.router = chi.NewRouter()\n\tserver.router.Use(middleware.Recoverer)\n\tserver.router.Get(recoveryPath, func(_ http.ResponseWriter, _ *http.Request) {\n\t\tpanic(\"panic\")\n\t})\n\ttestServer = httptest.NewServer(server.router)\n\tdefer testServer.Close()\n\n\treq, err = http.NewRequest(http.MethodGet, recoveryPath, nil)\n\tassert.NoError(t, err)\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())\n}\n\nfunc TestStreamJSONArray(t *testing.T) {\n\tdataGetter := func(_, _ int) ([]byte, int, error) {\n\t\treturn nil, 0, nil\n\t}\n\trr := httptest.NewRecorder()\n\tstreamJSONArray(rr, 10, dataGetter)\n\tassert.Equal(t, `[]`, rr.Body.String())\n\n\tdata := []int{}\n\tfor i := 0; i < 10; i++ {\n\t\tdata = append(data, i)\n\t}\n\n\tdataGetter = func(_, offset int) ([]byte, int, error) {\n\t\tif offset >= len(data) {\n\t\t\treturn nil, 0, nil\n\t\t}\n\t\tval := data[offset]\n\t\tdata, err := json.Marshal([]int{val})\n\t\treturn data, 1, err\n\t}\n\n\trr = httptest.NewRecorder()\n\tstreamJSONArray(rr, 1, dataGetter)\n\tassert.Equal(t, `[0,1,2,3,4,5,6,7,8,9]`, rr.Body.String())\n}\n\nfunc TestCompressorAbortHandler(t *testing.T) {\n\tdefer func() {\n\t\trcv := recover()\n\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t}()\n\n\tconnection := newConnection(\n\t\tcommon.NewBaseConnection(xid.New().String(), common.ProtocolHTTP, \"\", \"\", dataprovider.User{}),\n\t\tnil,\n\t\tnil,\n\t)\n\tshare := &dataprovider.Share{}\n\trenderCompressedFiles(&failingWriter{}, connection, \"\", nil, share)\n}\n\nfunc TestStreamDataAbortHandler(t *testing.T) {\n\tdefer func() {\n\t\trcv := recover()\n\t\tassert.Equal(t, http.ErrAbortHandler, rcv)\n\t}()\n\n\tstreamData(&failingWriter{}, []byte(`[\"a\":\"b\"]`))\n}\n\nfunc TestZipErrors(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tconnection := newConnection(\n\t\tcommon.NewBaseConnection(xid.New().String(), common.ProtocolHTTP, \"\", \"\", user),\n\t\tnil,\n\t\tnil,\n\t)\n\n\ttestDir := filepath.Join(os.TempDir(), \"testDir\")\n\terr := os.MkdirAll(testDir, os.ModePerm)\n\tassert.NoError(t, err)\n\n\twr := zip.NewWriter(&failingWriter{})\n\terr = wr.Close()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"write error\")\n\t}\n\n\terr = addZipEntry(wr, connection, \"/\"+filepath.Base(testDir), \"/\", nil, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"write error\")\n\t}\n\terr = addZipEntry(wr, connection, \"/\"+filepath.Base(testDir), \"/\", nil, 2000)\n\tassert.ErrorIs(t, err, util.ErrRecursionTooDeep)\n\n\terr = addZipEntry(wr, connection, \"/\"+filepath.Base(testDir), path.Join(\"/\", filepath.Base(testDir), \"dir\"), nil, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is outside base dir\")\n\t}\n\n\ttestFilePath := filepath.Join(testDir, \"ziptest.zip\")\n\terr = os.WriteFile(testFilePath, util.GenerateRandomBytes(65535), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = addZipEntry(wr, connection, path.Join(\"/\", filepath.Base(testDir), filepath.Base(testFilePath)),\n\t\t\"/\"+filepath.Base(testDir), nil, 0)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"write error\")\n\t}\n\n\tconnection.User.Permissions[\"/\"] = []string{dataprovider.PermListItems}\n\terr = addZipEntry(wr, connection, path.Join(\"/\", filepath.Base(testDir), filepath.Base(testFilePath)),\n\t\t\"/\"+filepath.Base(testDir), nil, 0)\n\tassert.ErrorIs(t, err, os.ErrPermission)\n\n\t// creating a virtual folder to a missing path stat is ok but readdir fails\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped\"),\n\t\t},\n\t\tVirtualPath: \"/vpath\",\n\t})\n\tconnection.User = user\n\twr = zip.NewWriter(bytes.NewBuffer(make([]byte, 0)))\n\terr = addZipEntry(wr, connection, user.VirtualFolders[0].VirtualPath, \"/\", nil, 0)\n\tassert.Error(t, err)\n\n\tuser.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{\n\t\tPath:           \"/\",\n\t\tDeniedPatterns: []string{\"*.zip\"},\n\t})\n\terr = addZipEntry(wr, connection, \"/\"+filepath.Base(testDir), \"/\", nil, 0)\n\tassert.ErrorIs(t, err, os.ErrPermission)\n\n\terr = os.RemoveAll(testDir)\n\tassert.NoError(t, err)\n}\n\nfunc TestWebAdminRedirect(t *testing.T) {\n\tb := Binding{\n\t\tAddress:         \"\",\n\t\tPort:            8080,\n\t\tEnableWebAdmin:  true,\n\t\tEnableWebClient: false,\n\t\tEnableRESTAPI:   true,\n\t}\n\tserver := newHttpdServer(b, \"../static\", \"\", CorsConfig{}, \"../openapi\")\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\ttestServer := httptest.NewServer(server.router)\n\tdefer testServer.Close()\n\n\treq, err := http.NewRequest(http.MethodGet, webRootPath, nil)\n\tassert.NoError(t, err)\n\trr := httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusFound, rr.Code, rr.Body.String())\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\n\treq, err = http.NewRequest(http.MethodGet, webBasePath, nil)\n\tassert.NoError(t, err)\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusFound, rr.Code, rr.Body.String())\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n}\n\nfunc TestParseRangeRequests(t *testing.T) {\n\t// curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=24-24\"\n\tfileSize := int64(169740)\n\trangeHeader := \"bytes=24-24\"\n\toffset, size, err := parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp := fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 24-24/169740\", resp)\n\trequire.Equal(t, int64(1), size)\n\t// curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=24-\"\n\trangeHeader = \"bytes=24-\"\n\toffset, size, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp = fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 24-169739/169740\", resp)\n\trequire.Equal(t, int64(169716), size)\n\t// curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=-1\"\n\trangeHeader = \"bytes=-1\"\n\toffset, size, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp = fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 169739-169739/169740\", resp)\n\trequire.Equal(t, int64(1), size)\n\t// curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=-100\"\n\trangeHeader = \"bytes=-100\"\n\toffset, size, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp = fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 169640-169739/169740\", resp)\n\trequire.Equal(t, int64(100), size)\n\t// curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=20-30\"\n\trangeHeader = \"bytes=20-30\"\n\toffset, size, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp = fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 20-30/169740\", resp)\n\trequire.Equal(t, int64(11), size)\n\t// curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=20-169739\"\n\trangeHeader = \"bytes=20-169739\"\n\toffset, size, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp = fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 20-169739/169740\", resp)\n\trequire.Equal(t, int64(169720), size)\n\t// curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=20-169740\"\n\trangeHeader = \"bytes=20-169740\"\n\toffset, size, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp = fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 20-169739/169740\", resp)\n\trequire.Equal(t, int64(169720), size)\n\t// curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=20-169741\"\n\trangeHeader = \"bytes=20-169741\"\n\toffset, size, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp = fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 20-169739/169740\", resp)\n\trequire.Equal(t, int64(169720), size)\n\t//curl --verbose  \"http://127.0.0.1:8080/static/css/sb-admin-2.min.css\" -H \"Range: bytes=0-\" > /dev/null\n\trangeHeader = \"bytes=0-\"\n\toffset, size, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.NoError(t, err)\n\tresp = fmt.Sprintf(\"bytes %d-%d/%d\", offset, offset+size-1, fileSize)\n\tassert.Equal(t, \"bytes 0-169739/169740\", resp)\n\trequire.Equal(t, int64(169740), size)\n\t// now test errors\n\trangeHeader = \"bytes=0-a\"\n\t_, _, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.Error(t, err)\n\trangeHeader = \"bytes=\"\n\t_, _, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.Error(t, err)\n\trangeHeader = \"bytes=-\"\n\t_, _, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.Error(t, err)\n\trangeHeader = \"bytes=500-300\"\n\t_, _, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.Error(t, err)\n\trangeHeader = \"bytes=5000000\"\n\t_, _, err = parseRangeRequest(rangeHeader[6:], fileSize)\n\trequire.Error(t, err)\n}\n\nfunc TestRequestHeaderErrors(t *testing.T) {\n\treq, _ := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.Header.Set(\"If-Unmodified-Since\", \"not a date\")\n\tres := checkIfUnmodifiedSince(req, time.Now())\n\tassert.Equal(t, condNone, res)\n\n\treq, _ = http.NewRequest(http.MethodPost, webClientFilesPath, nil)\n\tres = checkIfModifiedSince(req, time.Now())\n\tassert.Equal(t, condNone, res)\n\n\treq, _ = http.NewRequest(http.MethodPost, webClientFilesPath, nil)\n\tres = checkIfRange(req, time.Now())\n\tassert.Equal(t, condNone, res)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.Header.Set(\"If-Modified-Since\", \"not a date\")\n\tres = checkIfModifiedSince(req, time.Now())\n\tassert.Equal(t, condNone, res)\n\n\treq, _ = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.Header.Set(\"If-Range\", time.Now().Format(http.TimeFormat))\n\tres = checkIfRange(req, time.Time{})\n\tassert.Equal(t, condFalse, res)\n\n\treq.Header.Set(\"If-Range\", \"invalid if range date\")\n\tres = checkIfRange(req, time.Now())\n\tassert.Equal(t, condFalse, res)\n\tmodTime := getFileObjectModTime(time.Time{})\n\tassert.Empty(t, modTime)\n}\n\nfunc TestConnection(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"test_httpd_user\",\n\t\t\tHomeDir:  filepath.Clean(os.TempDir()),\n\t\t},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.GCSFilesystemProvider,\n\t\t\tGCSConfig: vfs.GCSFsConfig{\n\t\t\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\t\t\tBucket: \"test_bucket_name\",\n\t\t\t\t},\n\t\t\t\tCredentials: kms.NewPlainSecret(\"invalid JSON payload\"),\n\t\t\t},\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tconnection := newConnection(\n\t\tcommon.NewBaseConnection(xid.New().String(), common.ProtocolHTTP, \"\", \"\", user),\n\t\tnil,\n\t\tnil,\n\t)\n\tassert.Empty(t, connection.GetClientVersion())\n\tassert.Empty(t, connection.GetRemoteAddress())\n\tassert.Empty(t, connection.GetCommand())\n\tname := \"missing file name\"\n\t_, err := connection.getFileReader(name, 0, http.MethodGet)\n\tassert.Error(t, err)\n\tconnection.User.FsConfig.Provider = sdk.LocalFilesystemProvider\n\t_, err = connection.getFileReader(name, 0, http.MethodGet)\n\tassert.ErrorIs(t, err, os.ErrNotExist)\n}\n\nfunc TestGetFileWriterErrors(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"test_httpd_user\",\n\t\t\tHomeDir:  \"invalid\",\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tconnection := newConnection(\n\t\tcommon.NewBaseConnection(xid.New().String(), common.ProtocolHTTP, \"\", \"\", user),\n\t\tnil,\n\t\tnil,\n\t)\n\t_, err := connection.getFileWriter(\"name\")\n\tassert.Error(t, err)\n\n\tuser.FsConfig.Provider = sdk.S3FilesystemProvider\n\tuser.FsConfig.S3Config = vfs.S3FsConfig{\n\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\tBucket:    \"b\",\n\t\t\tRegion:    \"us-west-1\",\n\t\t\tAccessKey: \"key\",\n\t\t},\n\t\tAccessSecret: kms.NewPlainSecret(\"secret\"),\n\t}\n\tconnection = newConnection(\n\t\tcommon.NewBaseConnection(xid.New().String(), common.ProtocolHTTP, \"\", \"\", user),\n\t\tnil,\n\t\tnil,\n\t)\n\t_, err = connection.getFileWriter(\"/path\")\n\tassert.Error(t, err)\n}\n\nfunc TestThrottledHandler(t *testing.T) {\n\ttr := &throttledReader{\n\t\tr: io.NopCloser(bytes.NewBuffer(nil)),\n\t}\n\tassert.Equal(t, int64(0), tr.GetTruncatedSize())\n\terr := tr.Close()\n\tassert.NoError(t, err)\n\tassert.Empty(t, tr.GetRealFsPath(\"real path\"))\n\tassert.False(t, tr.SetTimes(\"p\", time.Now(), time.Now()))\n\t_, err = tr.Truncate(\"\", 0)\n\tassert.ErrorIs(t, err, vfs.ErrVfsUnsupported)\n\terr = tr.GetAbortError()\n\tassert.ErrorIs(t, err, common.ErrTransferAborted)\n}\n\nfunc TestHTTPDFile(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"test_httpd_user\",\n\t\t\tHomeDir:  filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tconnection := newConnection(\n\t\tcommon.NewBaseConnection(xid.New().String(), common.ProtocolHTTP, \"\", \"\", user),\n\t\tnil,\n\t\tnil,\n\t)\n\n\tfs, err := user.GetFilesystem(\"\")\n\tassert.NoError(t, err)\n\n\tname := \"fileName\"\n\tp := filepath.Join(os.TempDir(), name)\n\terr = os.WriteFile(p, []byte(\"contents\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tfile, err := os.Open(p)\n\tassert.NoError(t, err)\n\terr = file.Close()\n\tassert.NoError(t, err)\n\n\tbaseTransfer := common.NewBaseTransfer(file, connection.BaseConnection, nil, p, p, name, common.TransferDownload,\n\t\t0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\thttpdFile := newHTTPDFile(baseTransfer, nil, nil)\n\t// the file is closed, read should fail\n\tbuf := make([]byte, 100)\n\t_, err = httpdFile.Read(buf)\n\tassert.Error(t, err)\n\terr = httpdFile.Close()\n\tassert.Error(t, err)\n\terr = httpdFile.Close()\n\tassert.ErrorIs(t, err, common.ErrTransferClosed)\n\terr = os.Remove(p)\n\tassert.NoError(t, err)\n\n\thttpdFile.writer = file\n\thttpdFile.File = nil\n\thttpdFile.ErrTransfer = nil\n\terr = httpdFile.closeIO()\n\tassert.Error(t, err)\n\tassert.Error(t, httpdFile.ErrTransfer)\n\tassert.Equal(t, err, httpdFile.ErrTransfer)\n\thttpdFile.SignalClose(nil)\n\t_, err = httpdFile.Write(nil)\n\tassert.ErrorIs(t, err, common.ErrQuotaExceeded)\n}\n\nfunc TestChangeUserPwd(t *testing.T) {\n\treq, _ := http.NewRequest(http.MethodPost, webChangeClientPwdPath, nil)\n\terr := doChangeUserPassword(req, \"\", \"\", \"\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"please provide the current password and the new one two times\")\n\t}\n\terr = doChangeUserPassword(req, \"a\", \"b\", \"c\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"the two password fields do not match\")\n\t}\n\terr = doChangeUserPassword(req, \"a\", \"b\", \"b\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), errInvalidTokenClaims.Error())\n\t}\n}\n\nfunc TestWebUserInvalidClaims(t *testing.T) {\n\tserver := httpdServer{}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\n\trr := httptest.NewRecorder()\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"\",\n\t\t\tPassword: \"pwd\",\n\t\t},\n\t}\n\tc := &jwt.Claims{\n\t\tUsername:    user.Username,\n\t\tPermissions: nil,\n\t}\n\tc.Subject = user.GetSignature()\n\tc.SetExpiry(time.Now().Add(10 * time.Minute))\n\tc.Audience = []string{tokenAudienceAPI}\n\ttoken, err := server.tokenAuth.Sign(c)\n\tassert.NoError(t, err)\n\n\treq, _ := http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleClientGetFiles(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleClientGetDirContents(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorDirList403)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientDownloadZipPath, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleWebClientDownloadZip(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientEditFilePath, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleClientEditFile(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleClientAddShareGet(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleClientUpdateShareGet(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodPost, webClientSharePath, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleClientAddSharePost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodPost, webClientSharePath+\"/id\", nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleClientUpdateSharePost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientSharesPath+jsonAPISuffix, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tgetAllShares(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n\n\trr = httptest.NewRecorder()\n\treq, _ = http.NewRequest(http.MethodGet, webClientViewPDFPath, nil)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleClientGetPDF(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n}\n\nfunc TestInvalidClaims(t *testing.T) {\n\tserver := httpdServer{}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\n\trr := httptest.NewRecorder()\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"\",\n\t\t\tPassword: \"pwd\",\n\t\t},\n\t}\n\tc := &jwt.Claims{\n\t\tUsername:    user.Username,\n\t\tPermissions: nil,\n\t}\n\tc.Subject = user.GetSignature()\n\ttoken, err := server.tokenAuth.SignWithParams(c, tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tparsedToken, err := jwt.VerifyRequest(server.tokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx := req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, \"\", webBaseClientPath))\n\tform.Set(\"public_keys\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq = req.WithContext(ctx)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleWebClientProfilePost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\n\tadmin := dataprovider.Admin{\n\t\tUsername: \"\",\n\t\tPassword: user.Password,\n\t}\n\tc = &jwt.Claims{\n\t\tUsername:    admin.Username,\n\t\tPermissions: nil,\n\t}\n\tc.Subject = admin.GetSignature()\n\ttoken, err = server.tokenAuth.SignWithParams(c, tokenAudienceWebAdmin, \"\", getTokenDuration(tokenAudienceWebAdmin))\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, webAdminProfilePath, nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tparsedToken, err = jwt.VerifyRequest(server.tokenAuth, req, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx = req.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\treq = req.WithContext(ctx)\n\n\tform = make(url.Values)\n\tform.Set(csrfFormToken, createCSRFToken(rr, req, server.csrfTokenAuth, \"\", webBaseAdminPath))\n\tform.Set(\"allow_api_key_auth\", \"\")\n\treq, err = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\treq = req.WithContext(ctx)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tserver.handleWebAdminProfilePost(rr, req)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)\n}\n\nfunc TestTLSReq(t *testing.T) {\n\treq, err := http.NewRequest(http.MethodGet, webClientLoginPath, nil)\n\tassert.NoError(t, err)\n\treq.TLS = &tls.ConnectionState{}\n\tassert.True(t, isTLS(req))\n\treq.TLS = nil\n\tctx := context.WithValue(req.Context(), forwardedProtoKey, \"https\")\n\tassert.True(t, isTLS(req.WithContext(ctx)))\n\tctx = context.WithValue(req.Context(), forwardedProtoKey, \"http\")\n\tassert.False(t, isTLS(req.WithContext(ctx)))\n\tassert.Equal(t, \"context value forwarded proto\", forwardedProtoKey.String())\n}\n\nfunc TestSigningKey(t *testing.T) {\n\tsigningPassphrase := \"test\"\n\tserver1 := httpdServer{\n\t\tsigningPassphrase: signingPassphrase,\n\t}\n\terr := server1.initializeRouter()\n\trequire.NoError(t, err)\n\n\tserver2 := httpdServer{\n\t\tsigningPassphrase: signingPassphrase,\n\t}\n\terr = server2.initializeRouter()\n\trequire.NoError(t, err)\n\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"\",\n\t\t\tPassword: \"pwd\",\n\t\t},\n\t}\n\tc := &jwt.Claims{\n\t\tUsername:    user.Username,\n\t\tPermissions: nil,\n\t}\n\tc.Subject = user.GetSignature()\n\ttoken, err := server1.tokenAuth.SignWithParams(c, tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, token)\n\t_, err = jwt.VerifyToken(server1.tokenAuth, token)\n\tassert.NoError(t, err)\n\t_, err = jwt.VerifyToken(server2.tokenAuth, token)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginLinks(t *testing.T) {\n\tb := Binding{\n\t\tEnableWebAdmin:  true,\n\t\tEnableWebClient: false,\n\t\tEnableRESTAPI:   true,\n\t}\n\tassert.False(t, b.showClientLoginURL())\n\tb = Binding{\n\t\tEnableWebAdmin:  false,\n\t\tEnableWebClient: true,\n\t\tEnableRESTAPI:   true,\n\t}\n\tassert.False(t, b.showAdminLoginURL())\n\tb = Binding{\n\t\tEnableWebAdmin:  true,\n\t\tEnableWebClient: true,\n\t\tEnableRESTAPI:   true,\n\t}\n\tassert.True(t, b.showAdminLoginURL())\n\tassert.True(t, b.showClientLoginURL())\n\tb.HideLoginURL = 3\n\tassert.False(t, b.showAdminLoginURL())\n\tassert.False(t, b.showClientLoginURL())\n\tb.HideLoginURL = 1\n\tassert.True(t, b.showAdminLoginURL())\n\tassert.False(t, b.showClientLoginURL())\n\tb.HideLoginURL = 2\n\tassert.False(t, b.showAdminLoginURL())\n\tassert.True(t, b.showClientLoginURL())\n}\n\nfunc TestResetCodesCleanup(t *testing.T) {\n\tresetCode := newResetCode(util.GenerateUniqueID(), false)\n\tresetCode.ExpiresAt = time.Now().Add(-1 * time.Minute).UTC()\n\terr := resetCodesMgr.Add(resetCode)\n\tassert.NoError(t, err)\n\tresetCodesMgr.Cleanup()\n\t_, err = resetCodesMgr.Get(resetCode.Code)\n\tassert.Error(t, err)\n}\n\nfunc TestUserCanResetPassword(t *testing.T) {\n\treq, err := http.NewRequest(http.MethodGet, webClientLoginPath, nil)\n\tassert.NoError(t, err)\n\treq.RemoteAddr = \"172.16.9.2:55080\"\n\n\tu := dataprovider.User{}\n\tassert.True(t, isUserAllowedToResetPassword(req, &u))\n\tu.Filters.DeniedProtocols = []string{common.ProtocolHTTP}\n\tassert.False(t, isUserAllowedToResetPassword(req, &u))\n\tu.Filters.DeniedProtocols = nil\n\tu.Filters.WebClient = []string{sdk.WebClientPasswordResetDisabled}\n\tassert.False(t, isUserAllowedToResetPassword(req, &u))\n\tu.Filters.WebClient = nil\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}\n\tassert.False(t, isUserAllowedToResetPassword(req, &u))\n\tu.Filters.DeniedLoginMethods = nil\n\tu.Filters.AllowedIP = []string{\"127.0.0.1/8\"}\n\tassert.False(t, isUserAllowedToResetPassword(req, &u))\n}\n\nfunc TestBrowsableSharePaths(t *testing.T) {\n\tshare := dataprovider.Share{\n\t\tPaths:    []string{\"/\"},\n\t\tUsername: defaultAdminUsername,\n\t}\n\t_, err := getUserForShare(share)\n\tif assert.Error(t, err) {\n\t\tassert.ErrorIs(t, err, util.ErrNotFound)\n\t}\n\treq, err := http.NewRequest(http.MethodGet, \"/share\", nil)\n\trequire.NoError(t, err)\n\tname, err := getBrowsableSharedPath(share.Paths[0], req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/\", name)\n\treq, err = http.NewRequest(http.MethodGet, \"/share?path=abc\", nil)\n\trequire.NoError(t, err)\n\tname, err = getBrowsableSharedPath(share.Paths[0], req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/abc\", name)\n\n\tshare.Paths = []string{\"/a/b/c\"}\n\treq, err = http.NewRequest(http.MethodGet, \"/share?path=abc\", nil)\n\trequire.NoError(t, err)\n\tname, err = getBrowsableSharedPath(share.Paths[0], req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/a/b/c/abc\", name)\n\treq, err = http.NewRequest(http.MethodGet, \"/share?path=%2Fabc/d\", nil)\n\trequire.NoError(t, err)\n\tname, err = getBrowsableSharedPath(share.Paths[0], req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/a/b/c/abc/d\", name)\n\n\treq, err = http.NewRequest(http.MethodGet, \"/share?path=%2Fabc%2F..%2F..\", nil)\n\trequire.NoError(t, err)\n\t_, err = getBrowsableSharedPath(share.Paths[0], req)\n\tassert.Error(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, \"/share?path=%2Fabc%2F..\", nil)\n\trequire.NoError(t, err)\n\tname, err = getBrowsableSharedPath(share.Paths[0], req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"/a/b/c\", name)\n\n\tshare = dataprovider.Share{\n\t\tPaths: []string{\"/a\", \"/b\"},\n\t}\n}\n\nfunc TestSecureMiddlewareIntegration(t *testing.T) {\n\tforwardedHostHeader := \"X-Forwarded-Host\"\n\tserver := httpdServer{\n\t\tbinding: Binding{\n\t\t\tProxyAllowed: []string{\"192.168.1.0/24\"},\n\t\t\tSecurity: SecurityConf{\n\t\t\t\tEnabled:              true,\n\t\t\t\tAllowedHosts:         []string{\"*.sftpgo.com\"},\n\t\t\t\tAllowedHostsAreRegex: true,\n\t\t\t\tHostsProxyHeaders:    []string{forwardedHostHeader},\n\t\t\t\tHTTPSProxyHeaders: []HTTPSProxyHeader{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey:   xForwardedProto,\n\t\t\t\t\t\tValue: \"https\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSTSSeconds:                31536000,\n\t\t\t\tSTSIncludeSubdomains:      true,\n\t\t\t\tSTSPreload:                true,\n\t\t\t\tContentTypeNosniff:        true,\n\t\t\t\tCacheControl:              \"private\",\n\t\t\t\tCrossOriginOpenerPolicy:   \"same-origin\",\n\t\t\t\tCrossOriginResourcePolicy: \"same-site\",\n\t\t\t\tCrossOriginEmbedderPolicy: \"require-corp\",\n\t\t\t\tReferrerPolicy:            \"no-referrer\",\n\t\t\t},\n\t\t},\n\t\tenableWebAdmin:  true,\n\t\tenableWebClient: true,\n\t\tenableRESTAPI:   true,\n\t}\n\tserver.binding.Security.updateProxyHeaders()\n\terr := server.binding.parseAllowedProxy()\n\tassert.NoError(t, err)\n\tassert.Equal(t, []string{forwardedHostHeader, xForwardedProto}, server.binding.Security.proxyHeaders)\n\tassert.Equal(t, map[string]string{xForwardedProto: \"https\"}, server.binding.Security.getHTTPSProxyHeaders())\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webClientLoginPath, nil)\n\tassert.NoError(t, err)\n\tr.Host = \"127.0.0.1\"\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.Equal(t, \"no-cache, no-store, max-age=0, must-revalidate, private\", rr.Header().Get(\"Cache-Control\"))\n\n\trr = httptest.NewRecorder()\n\tr.Header.Set(forwardedHostHeader, \"www.sftpgo.com\")\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\t// the header should be removed\n\tassert.Empty(t, r.Header.Get(forwardedHostHeader))\n\n\trr = httptest.NewRecorder()\n\tr.Host = \"test.sftpgo.com\"\n\tr.Header.Set(forwardedHostHeader, \"test.example.com\")\n\tr.RemoteAddr = \"192.168.1.1\"\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\tassert.NotEmpty(t, r.Header.Get(forwardedHostHeader))\n\n\trr = httptest.NewRecorder()\n\tr.Header.Set(forwardedHostHeader, \"www.sftpgo.com\")\n\tr.RemoteAddr = \"192.168.1.1\"\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.NotEmpty(t, r.Header.Get(forwardedHostHeader))\n\tassert.Empty(t, rr.Header().Get(\"Strict-Transport-Security\"))\n\tassert.Equal(t, \"nosniff\", rr.Header().Get(\"X-Content-Type-Options\"))\n\t// now set the X-Forwarded-Proto to https, we should get the Strict-Transport-Security header\n\trr = httptest.NewRecorder()\n\tr.Host = \"test.sftpgo.com\"\n\tr.Header.Set(xForwardedProto, \"https\")\n\tr.RemoteAddr = \"192.168.1.3\"\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.NotEmpty(t, r.Header.Get(forwardedHostHeader))\n\tassert.Equal(t, \"max-age=31536000; includeSubDomains; preload\", rr.Header().Get(\"Strict-Transport-Security\"))\n\tassert.Equal(t, \"nosniff\", rr.Header().Get(\"X-Content-Type-Options\"))\n\tassert.Equal(t, \"require-corp\", rr.Header().Get(\"Cross-Origin-Embedder-Policy\"))\n\tassert.Equal(t, \"same-origin\", rr.Header().Get(\"Cross-Origin-Opener-Policy\"))\n\tassert.Equal(t, \"same-site\", rr.Header().Get(\"Cross-Origin-Resource-Policy\"))\n\tassert.Equal(t, \"no-referrer\", rr.Header().Get(\"Referrer-Policy\"))\n\n\tserver.binding.Security.Enabled = false\n\tserver.binding.Security.updateProxyHeaders()\n\tassert.Len(t, server.binding.Security.proxyHeaders, 0)\n}\n\nfunc TestGetCompressedFileName(t *testing.T) {\n\tusername := \"test\"\n\tres := getCompressedFileName(username, []string{\"single dir\"})\n\trequire.Equal(t, fmt.Sprintf(\"%s-single dir.zip\", username), res)\n\tres = getCompressedFileName(username, []string{\"file1\", \"file2\"})\n\trequire.Equal(t, fmt.Sprintf(\"%s-download.zip\", username), res)\n\tres = getCompressedFileName(username, []string{\"file1.txt\"})\n\trequire.Equal(t, fmt.Sprintf(\"%s-file1.zip\", username), res)\n\t// now files with full paths\n\tres = getCompressedFileName(username, []string{\"/dir/single dir\"})\n\trequire.Equal(t, fmt.Sprintf(\"%s-single dir.zip\", username), res)\n\tres = getCompressedFileName(username, []string{\"/adir/file1\", \"/adir/file2\"})\n\trequire.Equal(t, fmt.Sprintf(\"%s-download.zip\", username), res)\n\tres = getCompressedFileName(username, []string{\"/sub/dir/file1.txt\"})\n\trequire.Equal(t, fmt.Sprintf(\"%s-file1.zip\", username), res)\n}\n\nfunc TestRESTAPIDisabled(t *testing.T) {\n\tserver := httpdServer{\n\t\tenableWebAdmin:  true,\n\t\tenableWebClient: true,\n\t\tenableRESTAPI:   false,\n\t}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\tassert.False(t, server.enableRESTAPI)\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, healthzPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, tokenPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n}\n\nfunc TestWebAdminSetupWithInstallCode(t *testing.T) {\n\tinstallationCode = \"1234\"\n\t// delete all the admins\n\tadmins, err := dataprovider.GetAdmins(100, 0, dataprovider.OrderASC)\n\tassert.NoError(t, err)\n\tfor _, admin := range admins {\n\t\terr = dataprovider.DeleteAdmin(admin.Username, \"\", \"\", \"\")\n\t\tassert.NoError(t, err)\n\t}\n\t// close the provider and initializes it without creating the default admin\n\tproviderConf := dataprovider.GetProviderConfig()\n\tproviderConf.CreateDefaultAdmin = false\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tserver := httpdServer{\n\t\tenableWebAdmin:  true,\n\t\tenableWebClient: true,\n\t\tenableRESTAPI:   true,\n\t}\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\n\tfor _, webURL := range []string{\"/\", webBasePath, webBaseAdminPath, webAdminLoginPath, webClientLoginPath} {\n\t\trr := httptest.NewRecorder()\n\t\tr, err := http.NewRequest(http.MethodGet, webURL, nil)\n\t\tassert.NoError(t, err)\n\t\tserver.router.ServeHTTP(rr, r)\n\t\tassert.Equal(t, http.StatusFound, rr.Code)\n\t\tassert.Equal(t, webAdminSetupPath, rr.Header().Get(\"Location\"))\n\t}\n\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webAdminSetupPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tcookie := rr.Header().Get(\"Set-Cookie\")\n\tr.Header.Set(\"Cookie\", cookie)\n\tparsedToken, err := jwt.VerifyRequest(server.csrfTokenAuth, r, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx := r.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\tr = r.WithContext(ctx)\n\n\tform := make(url.Values)\n\tcsrfToken := createCSRFToken(rr, r, server.csrfTokenAuth, \"\", webBaseAdminPath)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"install_code\", installationCode+\"5\")\n\tform.Set(\"username\", defaultAdminUsername)\n\tform.Set(\"password\", \"password\")\n\tform.Set(\"confirm_password\", \"password\")\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tr = r.WithContext(ctx)\n\tr.Header.Set(\"Cookie\", cookie)\n\tr.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorSetupInstallCode)\n\n\t_, err = dataprovider.AdminExists(defaultAdminUsername)\n\tassert.Error(t, err)\n\tform.Set(\"install_code\", installationCode)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tr = r.WithContext(ctx)\n\tr.Header.Set(\"Cookie\", cookie)\n\tr.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminMFAPath, rr.Header().Get(\"Location\"))\n\n\t_, err = dataprovider.AdminExists(defaultAdminUsername)\n\tassert.NoError(t, err)\n\n\t// delete the admin and test the installation code resolver\n\terr = dataprovider.DeleteAdmin(defaultAdminUsername, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tSetInstallationCodeResolver(func(_ string) string {\n\t\treturn \"5678\"\n\t})\n\n\tfor _, webURL := range []string{\"/\", webBasePath, webBaseAdminPath, webAdminLoginPath, webClientLoginPath} {\n\t\trr = httptest.NewRecorder()\n\t\tr, err = http.NewRequest(http.MethodGet, webURL, nil)\n\t\tassert.NoError(t, err)\n\t\tserver.router.ServeHTTP(rr, r)\n\t\tassert.Equal(t, http.StatusFound, rr.Code)\n\t\tassert.Equal(t, webAdminSetupPath, rr.Header().Get(\"Location\"))\n\t}\n\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webAdminSetupPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tcookie = rr.Header().Get(\"Set-Cookie\")\n\tr.Header.Set(\"Cookie\", cookie)\n\tparsedToken, err = jwt.VerifyRequest(server.csrfTokenAuth, r, jwt.TokenFromCookie)\n\tassert.NoError(t, err)\n\tctx = r.Context()\n\tctx = jwt.NewContext(ctx, parsedToken, err)\n\tr = r.WithContext(ctx)\n\n\tform = make(url.Values)\n\tcsrfToken = createCSRFToken(rr, r, server.csrfTokenAuth, \"\", webBaseAdminPath)\n\tform.Set(csrfFormToken, csrfToken)\n\tform.Set(\"install_code\", installationCode)\n\tform.Set(\"username\", defaultAdminUsername)\n\tform.Set(\"password\", \"password\")\n\tform.Set(\"confirm_password\", \"password\")\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tr = r.WithContext(ctx)\n\tr.Header.Set(\"Cookie\", cookie)\n\tr.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nErrorSetupInstallCode)\n\n\t_, err = dataprovider.AdminExists(defaultAdminUsername)\n\tassert.Error(t, err)\n\tform.Set(\"install_code\", \"5678\")\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tr = r.WithContext(ctx)\n\tr.Header.Set(\"Cookie\", cookie)\n\tr.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminMFAPath, rr.Header().Get(\"Location\"))\n\n\t_, err = dataprovider.AdminExists(defaultAdminUsername)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\tproviderConf.CreateDefaultAdmin = true\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tinstallationCode = \"\"\n\tSetInstallationCodeResolver(nil)\n}\n\nfunc TestDbResetCodeManager(t *testing.T) {\n\tif !isSharedProviderSupported() {\n\t\tt.Skip(\"this test it is not available with this provider\")\n\t}\n\tmgr := newResetCodeManager(1)\n\tresetCode := newResetCode(\"admin\", true)\n\terr := mgr.Add(resetCode)\n\tassert.NoError(t, err)\n\tcodeGet, err := mgr.Get(resetCode.Code)\n\tassert.NoError(t, err)\n\tassert.Equal(t, resetCode, codeGet)\n\terr = mgr.Delete(resetCode.Code)\n\tassert.NoError(t, err)\n\terr = mgr.Delete(resetCode.Code)\n\tif assert.Error(t, err) {\n\t\tassert.ErrorIs(t, err, util.ErrNotFound)\n\t}\n\t_, err = mgr.Get(resetCode.Code)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\t// add an expired reset code\n\tresetCode = newResetCode(\"user\", false)\n\tresetCode.ExpiresAt = time.Now().Add(-24 * time.Hour)\n\terr = mgr.Add(resetCode)\n\tassert.NoError(t, err)\n\t_, err = mgr.Get(resetCode.Code)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"reset code expired\")\n\t}\n\tmgr.Cleanup()\n\t_, err = mgr.Get(resetCode.Code)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\n\tdbMgr, ok := mgr.(*dbResetCodeManager)\n\tif assert.True(t, ok) {\n\t\t_, err = dbMgr.decodeData(\"astring\")\n\t\tassert.Error(t, err)\n\t}\n}\n\nfunc TestEventRoleFilter(t *testing.T) {\n\tdefaultVal := \"default\"\n\treq, err := http.NewRequest(http.MethodGet, fsEventsPath+\"?role=role1\", nil)\n\trequire.NoError(t, err)\n\trole := getRoleFilterForEventSearch(req, defaultVal)\n\tassert.Equal(t, defaultVal, role)\n\trole = getRoleFilterForEventSearch(req, \"\")\n\tassert.Equal(t, \"role1\", role)\n}\n\nfunc TestEventsCSV(t *testing.T) {\n\te := fsEvent{\n\t\tStatus: 1,\n\t}\n\tdata := e.getCSVData()\n\tassert.Equal(t, \"OK\", data[5])\n\te.Status = 2\n\tdata = e.getCSVData()\n\tassert.Equal(t, \"KO\", data[5])\n\te.Status = 3\n\tdata = e.getCSVData()\n\tassert.Equal(t, \"Quota exceeded\", data[5])\n}\n\nfunc TestConfigsFromProvider(t *testing.T) {\n\terr := dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tc := Conf{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort: 1234,\n\t\t\t},\n\t\t\t{\n\t\t\t\tPort: 80,\n\t\t\t\tSecurity: SecurityConf{\n\t\t\t\t\tEnabled:       true,\n\t\t\t\t\tHTTPSRedirect: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tconfigs := dataprovider.Configs{\n\t\tACME: &dataprovider.ACMEConfigs{\n\t\t\tDomain:          \"domain.com\",\n\t\t\tEmail:           \"info@domain.com\",\n\t\t\tHTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: 80},\n\t\t\tProtocols:       1,\n\t\t},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tutil.CertsBasePath = \"\"\n\t// crt and key empty\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tutil.CertsBasePath = filepath.Clean(os.TempDir())\n\t// crt not found\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs := c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\tcrtPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+\".crt\")\n\terr = os.WriteFile(crtPath, nil, 0666)\n\tassert.NoError(t, err)\n\t// key not found\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\tkeyPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+\".key\")\n\terr = os.WriteFile(keyPath, nil, 0666)\n\tassert.NoError(t, err)\n\t// acme cert used\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Equal(t, configs.ACME.Domain, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 1)\n\tassert.True(t, c.Bindings[0].EnableHTTPS)\n\tassert.False(t, c.Bindings[1].EnableHTTPS)\n\t// protocols does not match\n\tconfigs.ACME.Protocols = 6\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tc.acmeDomain = \"\"\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\n\terr = os.Remove(crtPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n\tutil.CertsBasePath = \"\"\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPSRedirect(t *testing.T) {\n\tacmeWebRoot := filepath.Join(os.TempDir(), \"acme\")\n\terr := os.MkdirAll(acmeWebRoot, os.ModePerm)\n\tassert.NoError(t, err)\n\ttokenName := \"token\"\n\terr = os.WriteFile(filepath.Join(acmeWebRoot, tokenName), []byte(\"val\"), 0666)\n\tassert.NoError(t, err)\n\n\tacmeConfig := acme.Configuration{\n\t\tHTTP01Challenge: acme.HTTP01Challenge{WebRoot: acmeWebRoot},\n\t}\n\terr = acme.Initialize(acmeConfig, configDir, true)\n\trequire.NoError(t, err)\n\n\tforwardedHostHeader := \"X-Forwarded-Host\"\n\tserver := httpdServer{\n\t\tbinding: Binding{\n\t\t\tSecurity: SecurityConf{\n\t\t\t\tEnabled:           true,\n\t\t\t\tHTTPSRedirect:     true,\n\t\t\t\tHostsProxyHeaders: []string{forwardedHostHeader},\n\t\t\t},\n\t\t},\n\t}\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, path.Join(acmeChallengeURI, tokenName), nil)\n\tassert.NoError(t, err)\n\tr.Host = \"localhost\"\n\tr.RequestURI = path.Join(acmeChallengeURI, tokenName)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())\n\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\tr.RequestURI = webAdminLoginPath\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusTemporaryRedirect, rr.Code, rr.Body.String())\n\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\tr.RequestURI = webAdminLoginPath\n\tr.Header.Set(forwardedHostHeader, \"sftpgo.com\")\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusTemporaryRedirect, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), \"https://sftpgo.com\")\n\n\tserver.binding.Security.HTTPSHost = \"myhost:1044\"\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\tr.RequestURI = webAdminLoginPath\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusTemporaryRedirect, rr.Code, rr.Body.String())\n\tassert.Contains(t, rr.Body.String(), \"https://myhost:1044\")\n\n\terr = os.RemoveAll(acmeWebRoot)\n\tassert.NoError(t, err)\n}\n\nfunc TestDisabledAdminLoginMethods(t *testing.T) {\n\tserver := httpdServer{\n\t\tbinding: Binding{\n\t\t\tAddress:              \"\",\n\t\t\tPort:                 8080,\n\t\t\tEnableWebAdmin:       true,\n\t\t\tEnableWebClient:      true,\n\t\t\tEnableRESTAPI:        true,\n\t\t\tDisabledLoginMethods: 20,\n\t\t},\n\t\tenableWebAdmin:  true,\n\t\tenableWebClient: true,\n\t\tenableRESTAPI:   true,\n\t}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\ttestServer := httptest.NewServer(server.router)\n\tdefer testServer.Close()\n\n\trr := httptest.NewRecorder()\n\treq, err := http.NewRequest(http.MethodGet, tokenPath, nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, defaultAdminUsername, \"forgot-password\"), nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, path.Join(adminPath, defaultAdminUsername, \"reset-password\"), nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, webAdminLoginPath, nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusMethodNotAllowed, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, webAdminResetPwdPath, nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n}\n\nfunc TestDisabledUserLoginMethods(t *testing.T) {\n\tserver := httpdServer{\n\t\tbinding: Binding{\n\t\t\tAddress:              \"\",\n\t\t\tPort:                 8080,\n\t\t\tEnableWebAdmin:       true,\n\t\t\tEnableWebClient:      true,\n\t\t\tEnableRESTAPI:        true,\n\t\t\tDisabledLoginMethods: 40,\n\t\t},\n\t\tenableWebAdmin:  true,\n\t\tenableWebClient: true,\n\t\tenableRESTAPI:   true,\n\t}\n\terr := server.initializeRouter()\n\trequire.NoError(t, err)\n\ttestServer := httptest.NewServer(server.router)\n\tdefer testServer.Close()\n\n\trr := httptest.NewRecorder()\n\treq, err := http.NewRequest(http.MethodGet, userTokenPath, nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, userPath+\"/user/forgot-password\", nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, userPath+\"/user/reset-password\", nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, webClientLoginPath, nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusMethodNotAllowed, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\n\trr = httptest.NewRecorder()\n\treq, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, nil)\n\trequire.NoError(t, err)\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n}\n\nfunc TestGetLogEventString(t *testing.T) {\n\tassert.Equal(t, \"Login failed\", getLogEventString(notifier.LogEventTypeLoginFailed))\n\tassert.Equal(t, \"Login with non-existent user\", getLogEventString(notifier.LogEventTypeLoginNoUser))\n\tassert.Equal(t, \"No login tried\", getLogEventString(notifier.LogEventTypeNoLoginTried))\n\tassert.Equal(t, \"Algorithm negotiation failed\", getLogEventString(notifier.LogEventTypeNotNegotiated))\n\tassert.Equal(t, \"Login succeeded\", getLogEventString(notifier.LogEventTypeLoginOK))\n\tassert.Empty(t, getLogEventString(0))\n}\n\nfunc TestUserQuotaUsage(t *testing.T) {\n\tusage := userQuotaUsage{\n\t\tQuotaSize: 100,\n\t}\n\trequire.True(t, usage.HasQuotaInfo())\n\trequire.NotEmpty(t, usage.GetQuotaSize())\n\tproviderConf := dataprovider.GetProviderConfig()\n\tquotaTracking := dataprovider.GetQuotaTracking()\n\tproviderConf.TrackQuota = 0\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\tassert.False(t, usage.HasQuotaInfo())\n\tproviderConf.TrackQuota = quotaTracking\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tusage.QuotaSize = 0\n\tassert.False(t, usage.HasQuotaInfo())\n\tassert.Empty(t, usage.GetQuotaSize())\n\tassert.Equal(t, 0, usage.GetQuotaSizePercentage())\n\tassert.False(t, usage.IsQuotaSizeLow())\n\tassert.False(t, usage.IsDiskQuotaLow())\n\tassert.False(t, usage.IsQuotaLow())\n\tusage.UsedQuotaSize = 9\n\tassert.NotEmpty(t, usage.GetQuotaSize())\n\tusage.QuotaSize = 10\n\tassert.True(t, usage.IsQuotaSizeLow())\n\tassert.True(t, usage.IsDiskQuotaLow())\n\tassert.True(t, usage.IsQuotaLow())\n\tusage.DownloadDataTransfer = 1\n\tassert.True(t, usage.HasQuotaInfo())\n\tassert.True(t, usage.HasTranferQuota())\n\tassert.Empty(t, usage.GetQuotaFiles())\n\tassert.Equal(t, 0, usage.GetQuotaFilesPercentage())\n\tusage.QuotaFiles = 1\n\tassert.NotEmpty(t, usage.GetQuotaFiles())\n\tusage.QuotaFiles = 0\n\tusage.UsedQuotaFiles = 9\n\tassert.NotEmpty(t, usage.GetQuotaFiles())\n\tusage.QuotaFiles = 10\n\tusage.DownloadDataTransfer = 0\n\tassert.True(t, usage.IsQuotaFilesLow())\n\tassert.True(t, usage.IsDiskQuotaLow())\n\tassert.False(t, usage.IsTotalTransferQuotaLow())\n\tassert.False(t, usage.IsUploadTransferQuotaLow())\n\tassert.False(t, usage.IsDownloadTransferQuotaLow())\n\tassert.Equal(t, 0, usage.GetTotalTransferQuotaPercentage())\n\tassert.Equal(t, 0, usage.GetUploadTransferQuotaPercentage())\n\tassert.Equal(t, 0, usage.GetDownloadTransferQuotaPercentage())\n\tassert.Empty(t, usage.GetTotalTransferQuota())\n\tassert.Empty(t, usage.GetUploadTransferQuota())\n\tassert.Empty(t, usage.GetDownloadTransferQuota())\n\tusage.TotalDataTransfer = 3\n\tusage.UsedUploadDataTransfer = 1 * 1048576\n\tassert.NotEmpty(t, usage.GetTotalTransferQuota())\n\tusage.TotalDataTransfer = 0\n\tassert.NotEmpty(t, usage.GetTotalTransferQuota())\n\tassert.NotEmpty(t, usage.GetUploadTransferQuota())\n\tusage.UploadDataTransfer = 2\n\tassert.NotEmpty(t, usage.GetUploadTransferQuota())\n\tusage.UsedDownloadDataTransfer = 1 * 1048576\n\tassert.NotEmpty(t, usage.GetDownloadTransferQuota())\n\tusage.DownloadDataTransfer = 2\n\tassert.NotEmpty(t, usage.GetDownloadTransferQuota())\n\tassert.False(t, usage.IsTransferQuotaLow())\n\tusage.UsedDownloadDataTransfer = 8 * 1048576\n\tusage.TotalDataTransfer = 10\n\tassert.True(t, usage.IsTotalTransferQuotaLow())\n\tassert.True(t, usage.IsTransferQuotaLow())\n\tusage.TotalDataTransfer = 0\n\tusage.UploadDataTransfer = 0\n\tusage.DownloadDataTransfer = 0\n\tassert.False(t, usage.IsTransferQuotaLow())\n\tusage.UploadDataTransfer = 10\n\tusage.UsedUploadDataTransfer = 9 * 1048576\n\tassert.True(t, usage.IsUploadTransferQuotaLow())\n\tassert.True(t, usage.IsTransferQuotaLow())\n\tusage.DownloadDataTransfer = 10\n\tusage.UsedDownloadDataTransfer = 9 * 1048576\n\tassert.True(t, usage.IsDownloadTransferQuotaLow())\n\tassert.True(t, usage.IsTransferQuotaLow())\n}\n\nfunc TestShareRedirectURL(t *testing.T) {\n\tshareID := util.GenerateUniqueID()\n\tbase := path.Join(webClientPubSharesPath, shareID)\n\tnext := path.Join(webClientPubSharesPath, shareID, \"browse\")\n\tok, res := checkShareRedirectURL(next, base)\n\tassert.True(t, ok)\n\tassert.Equal(t, next, res)\n\tnext = path.Join(webClientPubSharesPath, shareID, \"browse\") + \"?a=b\"\n\tok, res = checkShareRedirectURL(next, base)\n\tassert.True(t, ok)\n\tassert.Equal(t, next, res)\n\tnext = path.Join(webClientPubSharesPath, shareID)\n\tok, res = checkShareRedirectURL(next, base)\n\tassert.True(t, ok)\n\tassert.Equal(t, path.Join(base, \"download\"), res)\n\tnext = path.Join(webClientEditFilePath, shareID)\n\tok, res = checkShareRedirectURL(next, base)\n\tassert.False(t, ok)\n\tassert.Empty(t, res)\n\tnext = path.Join(webClientPubSharesPath, shareID) + \"?compress=false&a=b\"\n\tok, res = checkShareRedirectURL(next, base)\n\tassert.True(t, ok)\n\tassert.Equal(t, path.Join(base, \"download?compress=false&a=b\"), res)\n\tnext = path.Join(webClientPubSharesPath, shareID) + \"?compress=true&b=c\"\n\tok, res = checkShareRedirectURL(next, base)\n\tassert.True(t, ok)\n\tassert.Equal(t, path.Join(base, \"download?compress=true&b=c\"), res)\n\tok, res = checkShareRedirectURL(\"http://foo\\x7f.com/ab\", \"http://foo\\x7f.com/\")\n\tassert.False(t, ok)\n\tassert.Empty(t, res)\n\tok, res = checkShareRedirectURL(\"http://foo.com/?foo\\nbar\", \"http://foo.com\")\n\tassert.False(t, ok)\n\tassert.Empty(t, res)\n}\n\nfunc TestI18NMessages(t *testing.T) {\n\tmsg := i18nListDirMsg(http.StatusForbidden)\n\trequire.Equal(t, util.I18nErrorDirList403, msg)\n\tmsg = i18nListDirMsg(http.StatusInternalServerError)\n\trequire.Equal(t, util.I18nErrorDirListGeneric, msg)\n\tmsg = i18nFsMsg(http.StatusForbidden)\n\trequire.Equal(t, util.I18nError403Message, msg)\n\tmsg = i18nFsMsg(http.StatusInternalServerError)\n\trequire.Equal(t, util.I18nErrorFsGeneric, msg)\n}\n\nfunc TestI18NErrors(t *testing.T) {\n\terr := util.NewValidationError(\"error text\")\n\terrI18n := util.NewI18nError(err, util.I18nError500Message)\n\tassert.ErrorIs(t, errI18n, util.ErrValidation)\n\tassert.Equal(t, err.Error(), errI18n.Error())\n\tassert.Equal(t, util.I18nError500Message, getI18NErrorString(errI18n, \"\"))\n\tassert.Equal(t, util.I18nError500Message, errI18n.Message)\n\tassert.Equal(t, \"{}\", errI18n.Args())\n\tvar e1 *util.ValidationError\n\tassert.ErrorAs(t, errI18n, &e1)\n\tvar e2 *util.I18nError\n\tassert.ErrorAs(t, errI18n, &e2)\n\terr2 := util.NewI18nError(fs.ErrNotExist, util.I18nError500Message)\n\tassert.ErrorIs(t, err2, &util.I18nError{})\n\tassert.ErrorIs(t, err2, fs.ErrNotExist)\n\tassert.NotErrorIs(t, err2, fs.ErrExist)\n\tassert.Equal(t, util.I18nError403Message, getI18NErrorString(fs.ErrClosed, util.I18nError403Message))\n\terrorString := getI18NErrorString(nil, util.I18nError500Message)\n\tassert.Equal(t, util.I18nError500Message, errorString)\n\terrI18nWrap := util.NewI18nError(errI18n, util.I18nError404Message)\n\tassert.Equal(t, util.I18nError500Message, errI18nWrap.Message)\n\terrI18n = util.NewI18nError(err, util.I18nError500Message, util.I18nErrorArgs(map[string]any{\"a\": \"b\"}))\n\tassert.Equal(t, util.I18nError500Message, errI18n.Message)\n\tassert.Equal(t, `{\"a\":\"b\"}`, errI18n.Args())\n}\n\nfunc TestConvertEnabledLoginMethods(t *testing.T) {\n\tb := Binding{\n\t\tEnabledLoginMethods:  0,\n\t\tDisabledLoginMethods: 1,\n\t}\n\tb.convertLoginMethods()\n\tassert.Equal(t, 1, b.DisabledLoginMethods)\n\tb.DisabledLoginMethods = 0\n\tb.EnabledLoginMethods = 1\n\tb.convertLoginMethods()\n\tassert.Equal(t, 14, b.DisabledLoginMethods)\n\tb.DisabledLoginMethods = 0\n\tb.EnabledLoginMethods = 2\n\tb.convertLoginMethods()\n\tassert.Equal(t, 13, b.DisabledLoginMethods)\n\tb.DisabledLoginMethods = 0\n\tb.EnabledLoginMethods = 3\n\tb.convertLoginMethods()\n\tassert.Equal(t, 12, b.DisabledLoginMethods)\n\tb.DisabledLoginMethods = 0\n\tb.EnabledLoginMethods = 4\n\tb.convertLoginMethods()\n\tassert.Equal(t, 11, b.DisabledLoginMethods)\n\tb.DisabledLoginMethods = 0\n\tb.EnabledLoginMethods = 7\n\tb.convertLoginMethods()\n\tassert.Equal(t, 8, b.DisabledLoginMethods)\n\tb.DisabledLoginMethods = 0\n\tb.EnabledLoginMethods = 15\n\tb.convertLoginMethods()\n\tassert.Equal(t, 0, b.DisabledLoginMethods)\n}\n\nfunc TestValidateBaseURL(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinputURL    string\n\t\texpectedURL string\n\t\texpectErr   bool\n\t}{\n\t\t{\n\t\t\tname:        \"Valid HTTPS URL\",\n\t\t\tinputURL:    \"https://sftp.example.com\",\n\t\t\texpectedURL: \"https://sftp.example.com\",\n\t\t\texpectErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:        \"Remove trailing slash\",\n\t\t\tinputURL:    \"https://sftp.example.com/\",\n\t\t\texpectedURL: \"https://sftp.example.com\",\n\t\t\texpectErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:        \"Remove multiple trailing slashes\",\n\t\t\tinputURL:    \"http://192.168.1.100:8080///\",\n\t\t\texpectedURL: \"http://192.168.1.100:8080\",\n\t\t\texpectErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:        \"Empty BaseURL (optional case)\",\n\t\t\tinputURL:    \"\",\n\t\t\texpectedURL: \"\",\n\t\t\texpectErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"Unsupported scheme (FTP)\",\n\t\t\tinputURL:  \"ftp://files.example.com\",\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"Malformed URL string\",\n\t\t\tinputURL:  \"not-a-url\",\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"Missing Host\",\n\t\t\tinputURL:  \"https://\",\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Preserve path without trailing slash\",\n\t\t\tinputURL:    \"https://example.com/sftp/\",\n\t\t\texpectedURL: \"https://example.com/sftp\",\n\t\t\texpectErr:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := &Binding{\n\t\t\t\tBaseURL: tt.inputURL,\n\t\t\t}\n\n\t\t\terr := b.validateBaseURL()\n\n\t\t\tif (err != nil) != tt.expectErr {\n\t\t\t\tt.Errorf(\"validateBaseURL() error = %v, expectErr %v\", err, tt.expectErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !tt.expectErr && b.BaseURL != tt.expectedURL {\n\t\t\t\tt.Errorf(\"validateBaseURL() got = %v, want %v\", b.BaseURL, tt.expectedURL)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc getCSRFTokenFromBody(body io.Reader) (string, error) {\n\tdoc, err := html.Parse(body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar csrfToken string\n\tvar f func(*html.Node)\n\n\tf = func(n *html.Node) {\n\t\tif n.Type == html.ElementNode && n.Data == \"input\" {\n\t\t\tvar name, value string\n\t\t\tfor _, attr := range n.Attr {\n\t\t\t\tif attr.Key == \"value\" {\n\t\t\t\t\tvalue = attr.Val\n\t\t\t\t}\n\t\t\t\tif attr.Key == \"name\" {\n\t\t\t\t\tname = attr.Val\n\t\t\t\t}\n\t\t\t}\n\t\t\tif name == csrfFormToken {\n\t\t\t\tcsrfToken = value\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\t\tf(c)\n\t\t}\n\t}\n\n\tf(doc)\n\n\tif csrfToken == \"\" {\n\t\treturn \"\", errors.New(\"CSRF token not found\")\n\t}\n\n\treturn csrfToken, nil\n}\n\nfunc isSharedProviderSupported() bool {\n\t// SQLite shares the implementation with other SQL-based provider but it makes no sense\n\t// to use it outside test cases\n\tswitch dataprovider.GetProviderStatus().Driver {\n\tcase dataprovider.MySQLDataProviderName, dataprovider.PGSQLDataProviderName,\n\t\tdataprovider.CockroachDataProviderName, dataprovider.SQLiteDataProviderName:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/middleware.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tforwardedProtoKey = &contextKey{\"forwarded proto\"}\n\terrInvalidToken   = errors.New(\"invalid JWT token\")\n)\n\ntype contextKey struct {\n\tname string\n}\n\nfunc (k *contextKey) String() string {\n\treturn \"context value \" + k.name\n}\n\nfunc validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudience) error {\n\ttoken, err := jwt.FromContext(r.Context())\n\n\tvar redirectPath string\n\tif audience == tokenAudienceWebAdmin {\n\t\tredirectPath = webAdminLoginPath\n\t} else {\n\t\tredirectPath = webClientLoginPath\n\t\tif uri := r.RequestURI; strings.HasPrefix(uri, webClientFilesPath) {\n\t\t\tredirectPath += \"?next=\" + url.QueryEscape(uri) //nolint:goconst\n\t\t}\n\t}\n\n\tisAPIToken := (audience == tokenAudienceAPI || audience == tokenAudienceAPIUser)\n\n\tdoRedirect := func(message string, err error) {\n\t\tif isAPIToken {\n\t\t\tsendAPIResponse(w, r, err, message, http.StatusUnauthorized)\n\t\t} else {\n\t\t\thttp.Redirect(w, r, redirectPath, http.StatusFound)\n\t\t}\n\t}\n\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"error getting jwt token: %v\", err)\n\t\tdoRedirect(http.StatusText(http.StatusUnauthorized), err)\n\t\treturn errInvalidToken\n\t}\n\n\tif isTokenInvalidated(r) {\n\t\tlogger.Debug(logSender, \"\", \"the token has been invalidated\")\n\t\tdoRedirect(\"Your token is no longer valid\", nil)\n\t\treturn errInvalidToken\n\t}\n\t// a user with a partial token will be always redirected to the appropriate two factor auth page\n\tif err := checkPartialAuth(w, r, audience, token.Audience); err != nil {\n\t\treturn err\n\t}\n\tif !token.Audience.Contains(audience) {\n\t\tlogger.Debug(logSender, \"\", \"the token is not valid for audience %q\", audience)\n\t\tdoRedirect(\"Your token audience is not valid\", nil)\n\t\treturn errInvalidToken\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := validateIPForToken(token, ipAddr); err != nil {\n\t\tlogger.Debug(logSender, \"\", \"the token with id %q is not valid for the ip address %q\", token.ID, ipAddr)\n\t\tdoRedirect(\"Your token is not valid\", nil)\n\t\treturn err\n\t}\n\tif err := checkTokenSignature(r, token); err != nil {\n\t\tdoRedirect(\"Your token is no longer valid\", nil)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *httpdServer) validateJWTPartialToken(w http.ResponseWriter, r *http.Request, audience tokenAudience) error {\n\ttoken, err := jwt.FromContext(r.Context())\n\tvar notFoundFunc func(w http.ResponseWriter, r *http.Request, err error)\n\tif audience == tokenAudienceWebAdminPartial {\n\t\tnotFoundFunc = s.renderNotFoundPage\n\t} else {\n\t\tnotFoundFunc = s.renderClientNotFoundPage\n\t}\n\tif err != nil {\n\t\tnotFoundFunc(w, r, nil)\n\t\treturn errInvalidToken\n\t}\n\tif isTokenInvalidated(r) {\n\t\tnotFoundFunc(w, r, nil)\n\t\treturn errInvalidToken\n\t}\n\tif !token.Audience.Contains(audience) {\n\t\tlogger.Debug(logSender, \"\", \"the partial token with id %q is not valid for audience %q\", token.ID, audience)\n\t\tnotFoundFunc(w, r, nil)\n\t\treturn errInvalidToken\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := validateIPForToken(token, ipAddr); err != nil {\n\t\tlogger.Debug(logSender, \"\", \"the partial token with id %q is not valid for the ip address %q\", token.ID, ipAddr)\n\t\tnotFoundFunc(w, r, nil)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *httpdServer) jwtAuthenticatorPartial(audience tokenAudience) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif err := s.validateJWTPartialToken(w, r, audience); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Token is authenticated, pass it through\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n}\n\nfunc jwtAuthenticatorAPI(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif err := validateJWTToken(w, r, tokenAudienceAPI); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// Token is authenticated, pass it through\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc jwtAuthenticatorAPIUser(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif err := validateJWTToken(w, r, tokenAudienceAPIUser); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// Token is authenticated, pass it through\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc jwtAuthenticatorWebAdmin(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif err := validateJWTToken(w, r, tokenAudienceWebAdmin); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// Token is authenticated, pass it through\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc jwtAuthenticatorWebClient(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif err := validateJWTToken(w, r, tokenAudienceWebClient); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// Token is authenticated, pass it through\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc (s *httpdServer) checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tclaims, err := jwt.FromContext(r.Context())\n\t\t\tif err != nil {\n\t\t\t\tif isWebRequest(r) {\n\t\t\t\t\ts.renderClientBadRequestPage(w, r, err)\n\t\t\t\t} else {\n\t\t\t\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// for web client perms are negated and not granted\n\t\t\tif claims.HasPerm(perm) {\n\t\t\t\tif isWebRequest(r) {\n\t\t\t\t\ts.renderClientForbiddenPage(w, r, errors.New(\"you don't have permission for this action\"))\n\t\t\t\t} else {\n\t\t\t\t\tsendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n}\n\n// checkAuthRequirements checks if the user must set a second factor auth or change the password\nfunc (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tclaims, err := jwt.FromContext(r.Context())\n\t\tif err != nil {\n\t\t\tif isWebRequest(r) {\n\t\t\t\tif isWebClientRequest(r) {\n\t\t\t\t\ts.renderClientBadRequestPage(w, r, err)\n\t\t\t\t} else {\n\t\t\t\t\ts.renderBadRequestPage(w, r, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif claims.MustSetTwoFactorAuth || claims.MustChangePassword {\n\t\t\tvar err error\n\t\t\tif claims.MustSetTwoFactorAuth {\n\t\t\t\tif len(claims.RequiredTwoFactorProtocols) > 0 {\n\t\t\t\t\tprotocols := strings.Join(claims.RequiredTwoFactorProtocols, \", \")\n\t\t\t\t\terr = util.NewI18nError(\n\t\t\t\t\t\tutil.NewGenericError(\n\t\t\t\t\t\t\tfmt.Sprintf(\"Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v\",\n\t\t\t\t\t\t\t\tprotocols)),\n\t\t\t\t\t\tutil.I18nError2FARequired,\n\t\t\t\t\t\tutil.I18nErrorArgs(map[string]any{\n\t\t\t\t\t\t\t\"val\": protocols,\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\terr = util.NewI18nError(\n\t\t\t\t\t\tutil.NewGenericError(\"Two-factor authentication requirements not met, please configure two-factor authentication\"),\n\t\t\t\t\t\tutil.I18nError2FARequiredGeneric,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\terr = util.NewI18nError(\n\t\t\t\t\tutil.NewGenericError(\"Password change required. Please set a new password to continue to use your account\"),\n\t\t\t\t\tutil.I18nErrorChangePwdRequired,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif isWebRequest(r) {\n\t\t\t\tif isWebClientRequest(r) {\n\t\t\t\t\ts.renderClientForbiddenPage(w, r, err)\n\t\t\t\t} else {\n\t\t\t\t\ts.renderForbiddenPage(w, r, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsendAPIResponse(w, r, err, \"\", http.StatusForbidden)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc (s *httpdServer) requireBuiltinLogin(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif isLoggedInWithOIDC(r) {\n\t\t\terr := util.NewI18nError(\n\t\t\t\tutil.NewGenericError(\"This feature is not available if you are logged in with OpenID\"),\n\t\t\t\tutil.I18nErrorNoOIDCFeature,\n\t\t\t)\n\t\t\tif isWebClientRequest(r) {\n\t\t\t\ts.renderClientForbiddenPage(w, r, err)\n\t\t\t} else {\n\t\t\t\ts.renderForbiddenPage(w, r, err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc (s *httpdServer) checkPerms(perms ...string) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tclaims, err := jwt.FromContext(r.Context())\n\t\t\tif err != nil {\n\t\t\t\tif isWebRequest(r) {\n\t\t\t\t\ts.renderBadRequestPage(w, r, err)\n\t\t\t\t} else {\n\t\t\t\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, perm := range perms {\n\t\t\t\tif !claims.HasPerm(perm) {\n\t\t\t\t\tif isWebRequest(r) {\n\t\t\t\t\t\ts.renderForbiddenPage(w, r, util.NewI18nError(fs.ErrPermission, util.I18nError403Message))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n}\n\nfunc (s *httpdServer) verifyCSRFHeader(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\ttokenString := r.Header.Get(csrfHeaderToken)\n\t\ttoken, err := jwt.VerifyToken(s.csrfTokenAuth, tokenString)\n\t\tif err != nil || token == nil {\n\t\t\tlogger.Debug(logSender, \"\", \"error validating CSRF header: %v\", err)\n\t\t\tsendAPIResponse(w, r, err, \"Invalid token\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\n\t\tif !token.Audience.Contains(tokenAudienceCSRF) {\n\t\t\tlogger.Debug(logSender, \"\", \"error validating CSRF header token audience\")\n\t\t\tsendAPIResponse(w, r, errors.New(\"the token is not valid\"), \"\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\n\t\tif err := validateIPForToken(token, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {\n\t\t\tlogger.Debug(logSender, \"\", \"error validating CSRF header IP audience\")\n\t\t\tsendAPIResponse(w, r, errors.New(\"the token is not valid\"), \"\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t\tif err := checkCSRFTokenRef(r, token); err != nil {\n\t\t\tsendAPIResponse(w, r, errors.New(\"the token is not valid\"), \"\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc checkNodeToken(tokenAuth *jwt.Signer) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tbearer := r.Header.Get(dataprovider.NodeTokenHeader)\n\t\t\tif bearer == \"\" {\n\t\t\t\tnext.ServeHTTP(w, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst prefix = \"Bearer \"\n\t\t\tif len(bearer) >= len(prefix) && strings.EqualFold(bearer[:len(prefix)], prefix) {\n\t\t\t\tbearer = bearer[len(prefix):]\n\t\t\t}\n\t\t\tif invalidatedJWTTokens.Get(bearer) {\n\t\t\t\tlogger.Debug(logSender, \"\", \"the node token has been invalidated\")\n\t\t\t\tsendAPIResponse(w, r, fmt.Errorf(\"the provided token is not valid\"), \"\", http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tclaims, err := dataprovider.AuthenticateNodeToken(bearer)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Debug(logSender, \"\", \"unable to authenticate node token %q: %v\", bearer, err)\n\t\t\t\tsendAPIResponse(w, r, fmt.Errorf(\"the provided token cannot be authenticated\"), \"\", http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer invalidatedJWTTokens.Add(bearer, time.Now().Add(2*time.Minute).UTC())\n\n\t\t\tc := &jwt.Claims{\n\t\t\t\tUsername:    claims.Username,\n\t\t\t\tPermissions: claims.Permissions,\n\t\t\t\tNodeID:      dataprovider.GetNodeName(),\n\t\t\t\tRole:        claims.Role,\n\t\t\t}\n\n\t\t\ttoken, err := tokenAuth.SignWithParams(c, tokenAudienceAPI, util.GetIPFromRemoteAddress(r.RemoteAddr), getTokenDuration(tokenAudienceAPI))\n\t\t\tif err != nil {\n\t\t\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresp := c.BuildTokenResponse(token)\n\t\t\tr.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", resp.Token))\n\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n}\n\nfunc checkAPIKeyAuth(tokenAuth *jwt.Signer, scope dataprovider.APIKeyScope) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tapiKey := r.Header.Get(\"X-SFTPGO-API-KEY\")\n\t\t\tif apiKey == \"\" {\n\t\t\t\tnext.ServeHTTP(w, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tkeyParams := strings.SplitN(apiKey, \".\", 3)\n\t\t\tif len(keyParams) < 2 {\n\t\t\t\tlogger.Debug(logSender, \"\", \"invalid api key %q\", apiKey)\n\t\t\t\tsendAPIResponse(w, r, errors.New(\"the provided api key is not valid\"), \"\", http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tkeyID := keyParams[0]\n\t\t\tkey := keyParams[1]\n\t\t\tapiUser := \"\"\n\t\t\tif len(keyParams) > 2 {\n\t\t\t\tapiUser = keyParams[2]\n\t\t\t}\n\n\t\t\tk, err := dataprovider.APIKeyExists(keyID)\n\t\t\tif err != nil {\n\t\t\t\thandleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), util.NewRecordNotFoundError(\"invalid api key\")) //nolint:errcheck\n\t\t\t\tlogger.Debug(logSender, \"\", \"invalid api key %q: %v\", apiKey, err)\n\t\t\t\tsendAPIResponse(w, r, errors.New(\"the provided api key is not valid\"), \"\", http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif k.Scope != scope {\n\t\t\t\thandleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck\n\t\t\t\tlogger.Debug(logSender, \"\", \"unable to authenticate api key %q: invalid scope: got %d, wanted: %d\",\n\t\t\t\t\tapiKey, k.Scope, scope)\n\t\t\t\tsendAPIResponse(w, r, fmt.Errorf(\"the provided api key is invalid for this request\"), \"\", http.StatusForbidden)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := k.Authenticate(key); err != nil {\n\t\t\t\thandleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), dataprovider.ErrInvalidCredentials) //nolint:errcheck\n\t\t\t\tlogger.Debug(logSender, \"\", \"unable to authenticate api key %q: %v\", apiKey, err)\n\t\t\t\tsendAPIResponse(w, r, fmt.Errorf(\"the provided api key cannot be authenticated\"), \"\", http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif scope == dataprovider.APIKeyScopeAdmin {\n\t\t\t\tif k.Admin != \"\" {\n\t\t\t\t\tapiUser = k.Admin\n\t\t\t\t}\n\t\t\t\tif err := authenticateAdminWithAPIKey(apiUser, keyID, tokenAuth, r); err != nil {\n\t\t\t\t\thandleDefenderEventLoginFailed(util.GetIPFromRemoteAddress(r.RemoteAddr), err) //nolint:errcheck\n\t\t\t\t\tlogger.Debug(logSender, \"\", \"unable to authenticate admin %q associated with api key %q: %v\",\n\t\t\t\t\t\tapiUser, apiKey, err)\n\t\t\t\t\tsendAPIResponse(w, r, fmt.Errorf(\"the admin associated with the provided api key cannot be authenticated\"),\n\t\t\t\t\t\t\"\", http.StatusUnauthorized)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcommon.DelayLogin(nil)\n\t\t\t} else {\n\t\t\t\tif k.User != \"\" {\n\t\t\t\t\tapiUser = k.User\n\t\t\t\t}\n\t\t\t\tif err := authenticateUserWithAPIKey(apiUser, keyID, tokenAuth, r); err != nil {\n\t\t\t\t\tlogger.Debug(logSender, \"\", \"unable to authenticate user %q associated with api key %q: %v\",\n\t\t\t\t\t\tapiUser, apiKey, err)\n\t\t\t\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: apiUser}},\n\t\t\t\t\t\tdataprovider.LoginMethodPassword, util.GetIPFromRemoteAddress(r.RemoteAddr), err, r)\n\t\t\t\t\tcode := http.StatusUnauthorized\n\t\t\t\t\tif errors.Is(err, common.ErrInternalFailure) {\n\t\t\t\t\t\tcode = http.StatusInternalServerError\n\t\t\t\t\t}\n\t\t\t\t\tsendAPIResponse(w, r, errors.New(\"the user associated with the provided api key cannot be authenticated\"),\n\t\t\t\t\t\t\"\", code)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: apiUser}},\n\t\t\t\t\tdataprovider.LoginMethodPassword, util.GetIPFromRemoteAddress(r.RemoteAddr), nil, r)\n\t\t\t}\n\t\t\tdataprovider.UpdateAPIKeyLastUse(&k) //nolint:errcheck\n\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n}\n\nfunc forbidAPIKeyAuthentication(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tclaims, err := jwt.FromContext(r.Context())\n\t\tif err != nil || claims.Username == \"\" {\n\t\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tif claims.APIKeyID != \"\" {\n\t\t\tsendAPIResponse(w, r, nil, \"API key authentication is not allowed\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc authenticateAdminWithAPIKey(username, keyID string, tokenAuth *jwt.Signer, r *http.Request) error {\n\tif username == \"\" {\n\t\treturn errors.New(\"the provided key is not associated with any admin and no username was provided\")\n\t}\n\tadmin, err := dataprovider.AdminExists(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !admin.Filters.AllowAPIKeyAuth {\n\t\treturn fmt.Errorf(\"API key authentication disabled for admin %q\", admin.Username)\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := admin.CanLogin(ipAddr); err != nil {\n\t\treturn err\n\t}\n\tc := &jwt.Claims{\n\t\tUsername:    admin.Username,\n\t\tPermissions: admin.Permissions,\n\t\tRole:        admin.Role,\n\t\tAPIKeyID:    keyID,\n\t}\n\tc.Subject = admin.GetSignature()\n\n\ttoken, err := tokenAuth.SignWithParams(c, tokenAudienceAPI, ipAddr, getTokenDuration(tokenAudienceAPI))\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp := c.BuildTokenResponse(token)\n\tr.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", resp.Token))\n\tdataprovider.UpdateAdminLastLogin(&admin)\n\tcommon.DelayLogin(nil)\n\treturn nil\n}\n\nfunc authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwt.Signer, r *http.Request) error {\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tprotocol := common.ProtocolHTTP\n\tif username == \"\" {\n\t\terr := errors.New(\"the provided key is not associated with any user and no username was provided\")\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\treturn err\n\t}\n\tif err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {\n\t\treturn err\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(username, \"\")\n\tif err != nil {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\treturn err\n\t}\n\tif !user.Filters.AllowAPIKeyAuth {\n\t\terr := fmt.Errorf(\"API key authentication disabled for user %q\", user.Username)\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\treturn err\n\t}\n\tif err := user.CheckLoginConditions(); err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\treturn err\n\t}\n\tconnectionID := fmt.Sprintf(\"%v_%v\", protocol, xid.New().String())\n\tif err := checkHTTPClientUser(&user, r, connectionID, true, false); err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\treturn err\n\t}\n\tdefer user.CloseFs() //nolint:errcheck\n\terr = user.CheckFsRoot(connectionID)\n\tif err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\treturn common.ErrInternalFailure\n\t}\n\tc := &jwt.Claims{\n\t\tUsername:    user.Username,\n\t\tPermissions: user.Filters.WebClient,\n\t\tRole:        user.Role,\n\t\tAPIKeyID:    keyID,\n\t}\n\tc.Subject = user.GetSignature()\n\n\ttoken, err := tokenAuth.SignWithParams(c, tokenAudienceAPIUser, ipAddr, getTokenDuration(tokenAudienceAPIUser))\n\tif err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\treturn err\n\t}\n\tresp := c.BuildTokenResponse(token)\n\tr.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", resp.Token))\n\tdataprovider.UpdateLastLogin(&user)\n\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, nil, r)\n\n\treturn nil\n}\n\nfunc checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, tokenAudience []string) error {\n\tif audience == tokenAudienceWebAdmin && slices.Contains(tokenAudience, tokenAudienceWebAdminPartial) {\n\t\thttp.Redirect(w, r, webAdminTwoFactorPath, http.StatusFound)\n\t\treturn errInvalidToken\n\t}\n\tif audience == tokenAudienceWebClient && slices.Contains(tokenAudience, tokenAudienceWebClientPartial) {\n\t\thttp.Redirect(w, r, webClientTwoFactorPath, http.StatusFound)\n\t\treturn errInvalidToken\n\t}\n\treturn nil\n}\n\nfunc cacheControlMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Cache-Control\", \"no-cache, no-store, max-age=0, must-revalidate, private\")\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc cleanCacheControlMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Del(\"Cache-Control\")\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n"
  },
  {
    "path": "internal/httpd/oauth2.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/oauth2\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\toauth2Mgr oauth2Manager\n)\n\nfunc newOAuth2Manager(isShared int) oauth2Manager {\n\tif isShared == 1 {\n\t\tlogger.Info(logSender, \"\", \"using provider OAuth2 manager\")\n\t\treturn &dbOAuth2Manager{}\n\t}\n\tlogger.Info(logSender, \"\", \"using memory OAuth2 manager\")\n\treturn &memoryOAuth2Manager{\n\t\tpendingAuths: make(map[string]oauth2PendingAuth),\n\t}\n}\n\ntype oauth2PendingAuth struct {\n\tState        string      `json:\"state\"`\n\tProvider     int         `json:\"provider\"`\n\tClientID     string      `json:\"client_id\"`\n\tClientSecret *kms.Secret `json:\"client_secret\"`\n\tRedirectURL  string      `json:\"redirect_url\"`\n\tIssuedAt     int64       `json:\"issued_at\"`\n\tVerifier     string      `json:\"verifier\"`\n}\n\nfunc newOAuth2PendingAuth(provider int, redirectURL, clientID string, clientSecret *kms.Secret) oauth2PendingAuth {\n\treturn oauth2PendingAuth{\n\t\tState:        util.GenerateOpaqueString(),\n\t\tProvider:     provider,\n\t\tClientID:     clientID,\n\t\tClientSecret: clientSecret,\n\t\tRedirectURL:  redirectURL,\n\t\tIssuedAt:     util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tVerifier:     oauth2.GenerateVerifier(),\n\t}\n}\n\ntype oauth2Manager interface {\n\taddPendingAuth(pendingAuth oauth2PendingAuth)\n\tremovePendingAuth(state string)\n\tgetPendingAuth(state string) (oauth2PendingAuth, error)\n\tcleanup()\n}\n\ntype memoryOAuth2Manager struct {\n\tmu           sync.RWMutex\n\tpendingAuths map[string]oauth2PendingAuth\n}\n\nfunc (o *memoryOAuth2Manager) addPendingAuth(pendingAuth oauth2PendingAuth) {\n\to.mu.Lock()\n\tdefer o.mu.Unlock()\n\n\to.pendingAuths[pendingAuth.State] = pendingAuth\n}\n\nfunc (o *memoryOAuth2Manager) removePendingAuth(state string) {\n\to.mu.Lock()\n\tdefer o.mu.Unlock()\n\n\tdelete(o.pendingAuths, state)\n}\n\nfunc (o *memoryOAuth2Manager) getPendingAuth(state string) (oauth2PendingAuth, error) {\n\to.mu.RLock()\n\tdefer o.mu.RUnlock()\n\n\tauthReq, ok := o.pendingAuths[state]\n\tif !ok {\n\t\treturn oauth2PendingAuth{}, errors.New(\"oauth2: no auth request found for the specified state\")\n\t}\n\tdiff := util.GetTimeAsMsSinceEpoch(time.Now()) - authReq.IssuedAt\n\tif diff > authStateValidity {\n\t\treturn oauth2PendingAuth{}, errors.New(\"oauth2: auth request is too old\")\n\t}\n\treturn authReq, nil\n}\n\nfunc (o *memoryOAuth2Manager) cleanup() {\n\to.mu.Lock()\n\tdefer o.mu.Unlock()\n\n\tfor k, auth := range o.pendingAuths {\n\t\tdiff := util.GetTimeAsMsSinceEpoch(time.Now()) - auth.IssuedAt\n\t\t// remove old pending auth requests\n\t\tif diff < 0 || diff > authStateValidity {\n\t\t\tdelete(o.pendingAuths, k)\n\t\t}\n\t}\n}\n\ntype dbOAuth2Manager struct{}\n\nfunc (o *dbOAuth2Manager) addPendingAuth(pendingAuth oauth2PendingAuth) {\n\tif err := pendingAuth.ClientSecret.Encrypt(); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to encrypt oauth2 secret: %v\", err)\n\t\treturn\n\t}\n\tsession := dataprovider.Session{\n\t\tKey:       pendingAuth.State,\n\t\tData:      pendingAuth,\n\t\tType:      dataprovider.SessionTypeOAuth2Auth,\n\t\tTimestamp: pendingAuth.IssuedAt + authStateValidity,\n\t}\n\tdataprovider.AddSharedSession(session) //nolint:errcheck\n}\n\nfunc (o *dbOAuth2Manager) removePendingAuth(state string) {\n\tdataprovider.DeleteSharedSession(state, dataprovider.SessionTypeOAuth2Auth) //nolint:errcheck\n}\n\nfunc (o *dbOAuth2Manager) getPendingAuth(state string) (oauth2PendingAuth, error) {\n\tsession, err := dataprovider.GetSharedSession(state, dataprovider.SessionTypeOAuth2Auth)\n\tif err != nil {\n\t\treturn oauth2PendingAuth{}, errors.New(\"oauth2: unable to get the auth request for the specified state\")\n\t}\n\tif session.Timestamp < util.GetTimeAsMsSinceEpoch(time.Now()) {\n\t\t// expired\n\t\treturn oauth2PendingAuth{}, errors.New(\"oauth2: auth request is too old\")\n\t}\n\treturn o.decodePendingAuthData(session.Data)\n}\n\nfunc (o *dbOAuth2Manager) decodePendingAuthData(data any) (oauth2PendingAuth, error) {\n\tif val, ok := data.([]byte); ok {\n\t\tauthReq := oauth2PendingAuth{}\n\t\terr := json.Unmarshal(val, &authReq)\n\t\tif err != nil {\n\t\t\treturn authReq, err\n\t\t}\n\t\terr = authReq.ClientSecret.TryDecrypt()\n\t\treturn authReq, err\n\t}\n\tlogger.Error(logSender, \"\", \"invalid oauth2 auth request data type %T\", data)\n\treturn oauth2PendingAuth{}, errors.New(\"oauth2: invalid auth request data\")\n}\n\nfunc (o *dbOAuth2Manager) cleanup() {\n\tdataprovider.CleanupSharedSessions(dataprovider.SessionTypeOAuth2Auth, time.Now()) //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/httpd/oauth2_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/rs/xid\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc TestMemoryOAuth2Manager(t *testing.T) {\n\tmgr := newOAuth2Manager(0)\n\tm, ok := mgr.(*memoryOAuth2Manager)\n\trequire.True(t, ok)\n\trequire.Len(t, m.pendingAuths, 0)\n\t_, err := m.getPendingAuth(xid.New().String())\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"no auth request found\")\n\tauth := newOAuth2PendingAuth(1, \"https://...\", \"cid\", kms.NewPlainSecret(\"mysecret\"))\n\tm.addPendingAuth(auth)\n\trequire.Len(t, m.pendingAuths, 1)\n\ta, err := m.getPendingAuth(auth.State)\n\tassert.NoError(t, err)\n\tassert.Equal(t, auth.State, a.State)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, a.ClientSecret.GetStatus())\n\tm.removePendingAuth(auth.State)\n\t_, err = m.getPendingAuth(auth.State)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"no auth request found\")\n\trequire.Len(t, m.pendingAuths, 0)\n\tstate := xid.New().String()\n\tauth = oauth2PendingAuth{\n\t\tState:    state,\n\t\tProvider: 1,\n\t\tIssuedAt: util.GetTimeAsMsSinceEpoch(time.Now()),\n\t}\n\tm.addPendingAuth(auth)\n\tauth = oauth2PendingAuth{\n\t\tState:    xid.New().String(),\n\t\tProvider: 1,\n\t\tIssuedAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(-10 * time.Minute)),\n\t}\n\tm.addPendingAuth(auth)\n\trequire.Len(t, m.pendingAuths, 2)\n\t_, err = m.getPendingAuth(auth.State)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"auth request is too old\")\n\tm.cleanup()\n\trequire.Len(t, m.pendingAuths, 1)\n\tm.removePendingAuth(state)\n\trequire.Len(t, m.pendingAuths, 0)\n}\n\nfunc TestDbOAuth2Manager(t *testing.T) {\n\tif !isSharedProviderSupported() {\n\t\tt.Skip(\"this test it is not available with this provider\")\n\t}\n\tmgr := newOAuth2Manager(1)\n\tm, ok := mgr.(*dbOAuth2Manager)\n\trequire.True(t, ok)\n\t_, err := m.getPendingAuth(xid.New().String())\n\trequire.Error(t, err)\n\tauth := newOAuth2PendingAuth(1, \"https://...\", \"client_id\", kms.NewPlainSecret(\"my db secret\"))\n\tm.addPendingAuth(auth)\n\ta, err := m.getPendingAuth(auth.State)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusPlain, a.ClientSecret.GetStatus())\n\tsession, err := dataprovider.GetSharedSession(auth.State, dataprovider.SessionTypeOAuth2Auth)\n\tassert.NoError(t, err)\n\tauthReq := oauth2PendingAuth{}\n\terr = json.Unmarshal(session.Data.([]byte), &authReq)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, authReq.ClientSecret.GetStatus())\n\tm.cleanup()\n\t_, err = m.getPendingAuth(auth.State)\n\tassert.NoError(t, err)\n\tm.removePendingAuth(auth.State)\n\t_, err = m.getPendingAuth(auth.State)\n\tassert.Error(t, err)\n\tauth = oauth2PendingAuth{\n\t\tState:        xid.New().String(),\n\t\tProvider:     1,\n\t\tIssuedAt:     util.GetTimeAsMsSinceEpoch(time.Now().Add(-10 * time.Minute)),\n\t\tClientSecret: kms.NewPlainSecret(\"db secret\"),\n\t}\n\tm.addPendingAuth(auth)\n\t_, err = m.getPendingAuth(auth.State)\n\tassert.Error(t, err)\n\t_, err = dataprovider.GetSharedSession(auth.State, dataprovider.SessionTypeOAuth2Auth)\n\tassert.NoError(t, err)\n\tm.cleanup()\n\t_, err = dataprovider.GetSharedSession(auth.State, dataprovider.SessionTypeOAuth2Auth)\n\tassert.Error(t, err)\n\t_, err = m.decodePendingAuthData(\"not a byte array\")\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"invalid auth request data\")\n\t_, err = m.decodePendingAuthData([]byte(\"{not a json\"))\n\trequire.Error(t, err)\n\t// adding a request with a non plain secret will fail\n\tauth = oauth2PendingAuth{\n\t\tState:        xid.New().String(),\n\t\tProvider:     1,\n\t\tIssuedAt:     util.GetTimeAsMsSinceEpoch(time.Now().Add(-10 * time.Minute)),\n\t\tClientSecret: kms.NewPlainSecret(\"db secret\"),\n\t}\n\tauth.ClientSecret.SetStatus(sdkkms.SecretStatusSecretBox)\n\tm.addPendingAuth(auth)\n\t_, err = dataprovider.GetSharedSession(auth.State, dataprovider.SessionTypeOAuth2Auth)\n\tassert.Error(t, err)\n\tasJSON, err := json.Marshal(auth)\n\tassert.NoError(t, err)\n\t_, err = m.decodePendingAuthData(asJSON)\n\tassert.Error(t, err)\n}\n"
  },
  {
    "path": "internal/httpd/oidc.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/coreos/go-oidc/v3/oidc\"\n\t\"github.com/rs/xid\"\n\t\"golang.org/x/oauth2\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\toidcCookieKey       = \"oidc\"\n\tadminRoleFieldValue = \"admin\"\n\tauthStateValidity   = 2 * 60 * 1000   // 2 minutes\n\ttokenUpdateInterval = 3 * 60 * 1000   // 3 minutes\n\ttokenDeleteInterval = 2 * 3600 * 1000 // 2 hours\n)\n\nvar (\n\toidcTokenKey       = &contextKey{\"OIDC token key\"}\n\toidcGeneratedToken = &contextKey{\"OIDC generated token\"}\n)\n\n// OAuth2Config defines an interface for OAuth2 methods, so we can mock them\ntype OAuth2Config interface {\n\tAuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string\n\tExchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)\n\tTokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource\n}\n\n// OIDCTokenVerifier defines an interface for OpenID token verifier, so we can mock them\ntype OIDCTokenVerifier interface {\n\tVerify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error)\n}\n\n// OIDC defines the OpenID Connect configuration\ntype OIDC struct {\n\t// ClientID is the application's ID\n\tClientID string `json:\"client_id\" mapstructure:\"client_id\"`\n\t// ClientSecret is the application's secret\n\tClientSecret     string `json:\"client_secret\" mapstructure:\"client_secret\"`\n\tClientSecretFile string `json:\"client_secret_file\" mapstructure:\"client_secret_file\"`\n\t// ConfigURL is the identifier for the service.\n\t// SFTPGo will try to retrieve the provider configuration on startup and then\n\t// will refuse to start if it fails to connect to the specified URL\n\tConfigURL string `json:\"config_url\" mapstructure:\"config_url\"`\n\t// RedirectBaseURL is the base URL to redirect to after OpenID authentication.\n\t// The suffix \"/web/oidc/redirect\" will be added to this base URL, adding also the\n\t// \"web_root\" if configured\n\tRedirectBaseURL string `json:\"redirect_base_url\" mapstructure:\"redirect_base_url\"`\n\t// ID token claims field to map to the SFTPGo username\n\tUsernameField string `json:\"username_field\" mapstructure:\"username_field\"`\n\t// Optional ID token claims field to map to a SFTPGo role.\n\t// If the defined ID token claims field is set to \"admin\" the authenticated user\n\t// is mapped to an SFTPGo admin.\n\t// You don't need to specify this field if you want to use OpenID only for the\n\t// Web Client UI\n\tRoleField string `json:\"role_field\" mapstructure:\"role_field\"`\n\t// If set, the `RoleField` is ignored and the SFTPGo role is assumed based on\n\t// the login link used\n\tImplicitRoles bool `json:\"implicit_roles\" mapstructure:\"implicit_roles\"`\n\t// Scopes required by the OAuth provider to retrieve information about the authenticated user.\n\t// The \"openid\" scope is required.\n\t// Refer to your OAuth provider documentation for more information about this\n\tScopes []string `json:\"scopes\" mapstructure:\"scopes\"`\n\t// Custom token claims fields to pass to the pre-login hook\n\tCustomFields []string `json:\"custom_fields\" mapstructure:\"custom_fields\"`\n\t// InsecureSkipSignatureCheck causes SFTPGo to skip JWT signature validation.\n\t// It's intended for special cases where providers, such as Azure, use the \"none\"\n\t// algorithm. Skipping the signature validation can cause security issues\n\tInsecureSkipSignatureCheck bool `json:\"insecure_skip_signature_check\" mapstructure:\"insecure_skip_signature_check\"`\n\t// Debug enables the OIDC debug mode. In debug mode, the received id_token will be logged\n\t// at the debug level\n\tDebug             bool `json:\"debug\" mapstructure:\"debug\"`\n\tprovider          *oidc.Provider\n\tverifier          OIDCTokenVerifier\n\tproviderLogoutURL string\n\toauth2Config      OAuth2Config\n}\n\nfunc (o *OIDC) isEnabled() bool {\n\treturn o.provider != nil\n}\n\nfunc (o *OIDC) hasRoles() bool {\n\treturn o.isEnabled() && (o.RoleField != \"\" || o.ImplicitRoles)\n}\n\nfunc (o *OIDC) getForcedRole(audience string) string {\n\tif !o.ImplicitRoles {\n\t\treturn \"\"\n\t}\n\tif audience == tokenAudienceWebAdmin {\n\t\treturn adminRoleFieldValue\n\t}\n\treturn \"\"\n}\n\nfunc (o *OIDC) getRedirectURL() string {\n\turl := o.RedirectBaseURL\n\tif strings.HasSuffix(o.RedirectBaseURL, \"/\") {\n\t\turl = strings.TrimSuffix(o.RedirectBaseURL, \"/\")\n\t}\n\turl += webOIDCRedirectPath\n\tlogger.Debug(logSender, \"\", \"oidc redirect URL: %q\", url)\n\treturn url\n}\n\nfunc (o *OIDC) initialize() error {\n\tif o.ConfigURL == \"\" {\n\t\treturn nil\n\t}\n\tif o.UsernameField == \"\" {\n\t\treturn errors.New(\"oidc: username field cannot be empty\")\n\t}\n\tif o.RedirectBaseURL == \"\" {\n\t\treturn errors.New(\"oidc: redirect base URL cannot be empty\")\n\t}\n\tif !slices.Contains(o.Scopes, oidc.ScopeOpenID) {\n\t\treturn fmt.Errorf(\"oidc: required scope %q is not set\", oidc.ScopeOpenID)\n\t}\n\tif o.ClientSecretFile != \"\" {\n\t\tsecret, err := util.ReadConfigFromFile(o.ClientSecretFile, configurationDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\to.ClientSecret = secret\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\tprovider, err := oidc.NewProvider(ctx, o.ConfigURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"oidc: unable to initialize provider for URL %q: %w\", o.ConfigURL, err)\n\t}\n\tclaims := make(map[string]any)\n\t// we cannot get an error here because the response body was already parsed as JSON\n\t// on provider creation\n\tprovider.Claims(&claims) //nolint:errcheck\n\tendSessionEndPoint, ok := claims[\"end_session_endpoint\"]\n\tif ok {\n\t\tif val, ok := endSessionEndPoint.(string); ok {\n\t\t\to.providerLogoutURL = val\n\t\t\tlogger.Debug(logSender, \"\", \"oidc end session endpoint %q\", o.providerLogoutURL)\n\t\t}\n\t}\n\to.provider = provider\n\to.verifier = nil\n\to.oauth2Config = &oauth2.Config{\n\t\tClientID:     o.ClientID,\n\t\tClientSecret: o.ClientSecret,\n\t\tEndpoint:     o.provider.Endpoint(),\n\t\tRedirectURL:  o.getRedirectURL(),\n\t\tScopes:       o.Scopes,\n\t}\n\n\treturn nil\n}\n\nfunc (o *OIDC) getVerifier(ctx context.Context) OIDCTokenVerifier {\n\tif o.verifier != nil {\n\t\treturn o.verifier\n\t}\n\treturn o.provider.VerifierContext(ctx, &oidc.Config{\n\t\tClientID:                   o.ClientID,\n\t\tInsecureSkipSignatureCheck: o.InsecureSkipSignatureCheck,\n\t})\n}\n\ntype oidcPendingAuth struct {\n\tState    string        `json:\"state\"`\n\tNonce    string        `json:\"nonce\"`\n\tAudience tokenAudience `json:\"audience\"`\n\tIssuedAt int64         `json:\"issued_at\"`\n\tVerifier string        `json:\"verifier\"`\n}\n\nfunc newOIDCPendingAuth(audience tokenAudience) oidcPendingAuth {\n\treturn oidcPendingAuth{\n\t\tState:    util.GenerateOpaqueString(),\n\t\tNonce:    util.GenerateOpaqueString(),\n\t\tAudience: audience,\n\t\tIssuedAt: util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tVerifier: oauth2.GenerateVerifier(),\n\t}\n}\n\ntype oidcToken struct {\n\tAccessToken                string          `json:\"access_token\"`\n\tTokenType                  string          `json:\"token_type,omitempty\"`\n\tRefreshToken               string          `json:\"refresh_token,omitempty\"`\n\tExpiresAt                  int64           `json:\"expires_at,omitempty\"`\n\tSessionID                  string          `json:\"session_id\"`\n\tIDToken                    string          `json:\"id_token\"`\n\tNonce                      string          `json:\"nonce\"`\n\tUsername                   string          `json:\"username\"`\n\tPermissions                []string        `json:\"permissions\"`\n\tHideUserPageSections       int             `json:\"hide_user_page_sections,omitempty\"`\n\tMustSetTwoFactorAuth       bool            `json:\"must_set_2fa,omitempty\"`\n\tMustChangePassword         bool            `json:\"must_change_password,omitempty\"`\n\tRequiredTwoFactorProtocols []string        `json:\"required_two_factor_protocols,omitempty\"`\n\tTokenRole                  string          `json:\"token_role,omitempty\"` // SFTPGo role name\n\tRole                       any             `json:\"role\"`                 // oidc user role: SFTPGo user or admin\n\tCustomFields               *map[string]any `json:\"custom_fields,omitempty\"`\n\tCookie                     string          `json:\"cookie\"`\n\tUsedAt                     int64           `json:\"used_at\"`\n}\n\nfunc (t *oidcToken) parseClaims(claims map[string]any, usernameField, roleField string, customFields []string,\n\tforcedRole string,\n) error {\n\tgetClaimsFields := func() []string {\n\t\tkeys := make([]string, 0, len(claims))\n\t\tfor k := range claims {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\treturn keys\n\t}\n\n\tvar username string\n\tval, ok := getOIDCFieldFromClaims(claims, usernameField)\n\tif ok {\n\t\tusername, ok = val.(string)\n\t}\n\tif !ok || username == \"\" {\n\t\tlogger.Warn(logSender, \"\", \"username field %q not found, empty or not a string, claims fields: %+v\",\n\t\t\tusernameField, getClaimsFields())\n\t\treturn errors.New(\"no username field\")\n\t}\n\tt.Username = username\n\tif forcedRole != \"\" {\n\t\tt.Role = forcedRole\n\t} else {\n\t\tt.getRoleFromField(claims, roleField)\n\t}\n\tt.CustomFields = nil\n\tif len(customFields) > 0 {\n\t\tfor _, field := range customFields {\n\t\t\tif val, ok := getOIDCFieldFromClaims(claims, field); ok {\n\t\t\t\tif t.CustomFields == nil {\n\t\t\t\t\tcustomFields := make(map[string]any)\n\t\t\t\t\tt.CustomFields = &customFields\n\t\t\t\t}\n\t\t\t\tlogger.Debug(logSender, \"\", \"custom field %q found in token claims\", field)\n\t\t\t\t(*t.CustomFields)[field] = val\n\t\t\t} else {\n\t\t\t\tlogger.Info(logSender, \"\", \"custom field %q not found in token claims\", field)\n\t\t\t}\n\t\t}\n\t}\n\tsid, ok := claims[\"sid\"].(string)\n\tif ok {\n\t\tt.SessionID = sid\n\t}\n\treturn nil\n}\n\nfunc (t *oidcToken) getRoleFromField(claims map[string]any, roleField string) {\n\trole, ok := getOIDCFieldFromClaims(claims, roleField)\n\tif ok {\n\t\tt.Role = role\n\t}\n}\n\nfunc (t *oidcToken) isAdmin() bool {\n\tswitch v := t.Role.(type) {\n\tcase string:\n\t\treturn v == adminRoleFieldValue\n\tcase []any:\n\t\tfor _, s := range v {\n\t\t\tif val, ok := s.(string); ok && val == adminRoleFieldValue {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (t *oidcToken) isExpired() bool {\n\tif t.ExpiresAt == 0 {\n\t\treturn false\n\t}\n\treturn t.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now())\n}\n\nfunc (t *oidcToken) refresh(ctx context.Context, config OAuth2Config, verifier OIDCTokenVerifier, r *http.Request) error {\n\tif t.RefreshToken == \"\" {\n\t\tlogger.Debug(logSender, \"\", \"refresh token not set, unable to refresh cookie %q\", t.Cookie)\n\t\treturn errors.New(\"refresh token not set\")\n\t}\n\toauth2Token := oauth2.Token{\n\t\tAccessToken:  t.AccessToken,\n\t\tTokenType:    t.TokenType,\n\t\tRefreshToken: t.RefreshToken,\n\t}\n\tif t.ExpiresAt > 0 {\n\t\toauth2Token.Expiry = util.GetTimeFromMsecSinceEpoch(t.ExpiresAt)\n\t}\n\n\tnewToken, err := config.TokenSource(ctx, &oauth2Token).Token()\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to refresh token for cookie %q: %v\", t.Cookie, err)\n\t\treturn err\n\t}\n\trawIDToken, ok := newToken.Extra(\"id_token\").(string)\n\tif !ok {\n\t\tlogger.Debug(logSender, \"\", \"the refreshed token has no id token, cookie %q\", t.Cookie)\n\t\treturn errors.New(\"the refreshed token has no id token\")\n\t}\n\n\tt.AccessToken = newToken.AccessToken\n\tt.TokenType = newToken.TokenType\n\tt.RefreshToken = newToken.RefreshToken\n\tt.IDToken = rawIDToken\n\tif !newToken.Expiry.IsZero() {\n\t\tt.ExpiresAt = util.GetTimeAsMsSinceEpoch(newToken.Expiry)\n\t} else {\n\t\tt.ExpiresAt = 0\n\t}\n\tidToken, err := verifier.Verify(ctx, rawIDToken)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to verify refreshed id token for cookie %q: %v\", t.Cookie, err)\n\t\treturn err\n\t}\n\tif idToken.Nonce != \"\" && idToken.Nonce != t.Nonce {\n\t\tlogger.Warn(logSender, \"\", \"unable to verify refreshed id token for cookie %q: nonce mismatch, expected: %q, actual: %q\",\n\t\t\tt.Cookie, t.Nonce, idToken.Nonce)\n\t\treturn errors.New(\"the refreshed token nonce mismatch\")\n\t}\n\tclaims := make(map[string]any)\n\terr = idToken.Claims(&claims)\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to get refreshed id token claims for cookie %q: %v\", t.Cookie, err)\n\t\treturn err\n\t}\n\tsid, ok := claims[\"sid\"].(string)\n\tif ok {\n\t\tt.SessionID = sid\n\t}\n\terr = t.refreshUser(r)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to refresh user after token refresh for cookie %q: %v\", t.Cookie, err)\n\t\treturn err\n\t}\n\tlogger.Debug(logSender, \"\", \"oidc token refreshed for user %q, cookie %q\", t.Username, t.Cookie)\n\toidcMgr.addToken(*t)\n\n\treturn nil\n}\n\nfunc (t *oidcToken) refreshUser(r *http.Request) error {\n\tif t.isAdmin() {\n\t\tadmin, err := dataprovider.AdminExists(t.Username)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := admin.CanLogin(util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tt.Permissions = admin.Permissions\n\t\tt.TokenRole = admin.Role\n\t\tt.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections\n\t\treturn nil\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(t.Username, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := user.CheckLoginConditions(); err != nil {\n\t\treturn err\n\t}\n\tif err := checkHTTPClientUser(&user, r, xid.New().String(), true, false); err != nil {\n\t\treturn err\n\t}\n\tt.Permissions = user.Filters.WebClient\n\tt.TokenRole = user.Role\n\tt.MustSetTwoFactorAuth = user.MustSetSecondFactor()\n\tt.MustChangePassword = user.MustChangePassword()\n\tt.RequiredTwoFactorProtocols = user.Filters.TwoFactorAuthProtocols\n\treturn nil\n}\n\nfunc (t *oidcToken) getUser(r *http.Request) error {\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tparams := common.EventParams{\n\t\tName:      t.Username,\n\t\tIP:        ipAddr,\n\t\tProtocol:  common.ProtocolOIDC,\n\t\tTimestamp: time.Now(),\n\t\tStatus:    1,\n\t}\n\tif t.isAdmin() {\n\t\tparams.Event = common.IDPLoginAdmin\n\t\t_, admin, err := common.HandleIDPLoginEvent(params, t.CustomFields)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif admin == nil {\n\t\t\ta, err := dataprovider.AdminExists(t.Username)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tadmin = &a\n\t\t}\n\t\tif err := admin.CanLogin(ipAddr); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tt.Permissions = admin.Permissions\n\t\tt.TokenRole = admin.Role\n\t\tt.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections\n\t\tdataprovider.UpdateAdminLastLogin(admin)\n\t\tcommon.DelayLogin(nil)\n\t\treturn nil\n\t}\n\tparams.Event = common.IDPLoginUser\n\tuser, _, err := common.HandleIDPLoginEvent(params, t.CustomFields)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif user == nil {\n\t\tu, err := dataprovider.GetUserAfterIDPAuth(t.Username, ipAddr, common.ProtocolOIDC, t.CustomFields)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser = &u\n\t}\n\tif err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolOIDC); err != nil {\n\t\tupdateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, err, r)\n\t\treturn fmt.Errorf(\"access denied: %w\", err)\n\t}\n\tif err := user.CheckLoginConditions(); err != nil {\n\t\tupdateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, err, r)\n\t\treturn err\n\t}\n\tconnectionID := fmt.Sprintf(\"%s_%s\", common.ProtocolOIDC, xid.New().String())\n\tif err := checkHTTPClientUser(user, r, connectionID, true, true); err != nil {\n\t\tupdateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, err, r)\n\t\treturn err\n\t}\n\tdefer user.CloseFs() //nolint:errcheck\n\terr = user.CheckFsRoot(connectionID)\n\tif err != nil {\n\t\tlogger.Warn(logSender, connectionID, \"unable to check fs root: %v\", err)\n\t\tupdateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, common.ErrInternalFailure, r)\n\t\treturn err\n\t}\n\tupdateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, nil, r)\n\tdataprovider.UpdateLastLogin(user)\n\tt.Permissions = user.Filters.WebClient\n\tt.TokenRole = user.Role\n\tt.MustSetTwoFactorAuth = user.MustSetSecondFactor()\n\tt.MustChangePassword = user.MustChangePassword()\n\tt.RequiredTwoFactorProtocols = user.Filters.TwoFactorAuthProtocols\n\treturn nil\n}\n\nfunc (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request, isAdmin bool) (oidcToken, error) {\n\tdoRedirect := func() {\n\t\tremoveOIDCCookie(w, r)\n\t\tif isAdmin {\n\t\t\thttp.Redirect(w, r, webAdminLoginPath, http.StatusFound)\n\t\t\treturn\n\t\t}\n\t\thttp.Redirect(w, r, webClientLoginPath, http.StatusFound)\n\t}\n\n\tcookie, err := r.Cookie(oidcCookieKey)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"no oidc cookie, redirecting to login page\")\n\t\tdoRedirect()\n\t\treturn oidcToken{}, errInvalidToken\n\t}\n\ttoken, err := oidcMgr.getToken(cookie.Value)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"error getting oidc token associated with cookie %q: %v\", cookie.Value, err)\n\t\tdoRedirect()\n\t\treturn oidcToken{}, errInvalidToken\n\t}\n\tif token.isExpired() {\n\t\tlogger.Debug(logSender, \"\", \"oidc token associated with cookie %q is expired\", token.Cookie)\n\t\tctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)\n\t\tdefer cancel()\n\n\t\tif err = token.refresh(ctx, s.binding.OIDC.oauth2Config, s.binding.OIDC.getVerifier(ctx), r); err != nil {\n\t\t\tsetFlashMessage(w, r, newFlashMessage(\"Your OpenID token is expired, please log-in again\", util.I18nOIDCTokenExpired))\n\t\t\tdoRedirect()\n\t\t\treturn oidcToken{}, errInvalidToken\n\t\t}\n\t} else {\n\t\toidcMgr.updateTokenUsage(token)\n\t}\n\tif isAdmin {\n\t\tif !token.isAdmin() {\n\t\t\tlogger.Debug(logSender, \"\", \"oidc token associated with cookie %q is not valid for admin users\", token.Cookie)\n\t\t\tsetFlashMessage(w, r, newFlashMessage(\n\t\t\t\t\"Your OpenID token is not valid for the SFTPGo Web Admin UI. Please logout from your OpenID server and log-in as an SFTPGo admin\",\n\t\t\t\tutil.I18nOIDCTokenInvalidAdmin,\n\t\t\t))\n\t\t\tdoRedirect()\n\t\t\treturn oidcToken{}, errInvalidToken\n\t\t}\n\t\treturn token, nil\n\t}\n\tif token.isAdmin() {\n\t\tlogger.Debug(logSender, \"\", \"oidc token associated with cookie %q is valid for admin users\", token.Cookie)\n\t\tsetFlashMessage(w, r, newFlashMessage(\n\t\t\t\"Your OpenID token is not valid for the SFTPGo Web Client UI. Please logout from your OpenID server and log-in as an SFTPGo user\",\n\t\t\tutil.I18nOIDCTokenInvalidUser,\n\t\t))\n\t\tdoRedirect()\n\t\treturn oidcToken{}, errInvalidToken\n\t}\n\treturn token, nil\n}\n\nfunc (s *httpdServer) oidcTokenAuthenticator(audience tokenAudience) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif canSkipOIDCValidation(r) {\n\t\t\t\tnext.ServeHTTP(w, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttoken, err := s.validateOIDCToken(w, r, audience == tokenAudienceWebAdmin)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tclaims := jwt.Claims{\n\t\t\t\tUsername:             dataprovider.ConvertName(token.Username),\n\t\t\t\tPermissions:          token.Permissions,\n\t\t\t\tRole:                 token.TokenRole,\n\t\t\t\tHideUserPageSections: token.HideUserPageSections,\n\t\t\t}\n\t\t\tclaims.ID = token.Cookie\n\t\t\tif audience == tokenAudienceWebClient {\n\t\t\t\tclaims.MustSetTwoFactorAuth = token.MustSetTwoFactorAuth\n\t\t\t\tclaims.MustChangePassword = token.MustChangePassword\n\t\t\t\tclaims.RequiredTwoFactorProtocols = token.RequiredTwoFactorProtocols\n\t\t\t}\n\t\t\ttokenString, err := s.tokenAuth.SignWithParams(&claims, audience, util.GetIPFromRemoteAddress(r.RemoteAddr),\n\t\t\t\tgetTokenDuration(audience))\n\t\t\tif err != nil {\n\t\t\t\tsetFlashMessage(w, r, newFlashMessage(\"Unable to create cookie\", util.I18nError500Message))\n\t\t\t\tif audience == tokenAudienceWebAdmin {\n\t\t\t\t\thttp.Redirect(w, r, webAdminLoginPath, http.StatusFound)\n\t\t\t\t} else {\n\t\t\t\t\thttp.Redirect(w, r, webClientLoginPath, http.StatusFound)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tctx := context.WithValue(r.Context(), oidcTokenKey, token.Cookie)\n\t\t\tctx = context.WithValue(ctx, oidcGeneratedToken, tokenString)\n\n\t\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t\t})\n\t}\n}\n\nfunc (s *httpdServer) handleWebAdminOIDCLogin(w http.ResponseWriter, r *http.Request) {\n\ts.oidcLoginRedirect(w, r, tokenAudienceWebAdmin)\n}\n\nfunc (s *httpdServer) handleWebClientOIDCLogin(w http.ResponseWriter, r *http.Request) {\n\ts.oidcLoginRedirect(w, r, tokenAudienceWebClient)\n}\n\nfunc (s *httpdServer) oidcLoginRedirect(w http.ResponseWriter, r *http.Request, audience tokenAudience) {\n\tpendingAuth := newOIDCPendingAuth(audience)\n\toidcMgr.addPendingAuth(pendingAuth)\n\thttp.Redirect(w, r, s.binding.OIDC.oauth2Config.AuthCodeURL(pendingAuth.State,\n\t\toidc.Nonce(pendingAuth.Nonce), oauth2.S256ChallengeOption(pendingAuth.Verifier)), http.StatusFound)\n}\n\nfunc (s *httpdServer) debugTokenClaims(claims map[string]any, rawIDToken string) {\n\tif s.binding.OIDC.Debug {\n\t\tif claims == nil {\n\t\t\tlogger.Debug(logSender, \"\", \"raw id token %q\", rawIDToken)\n\t\t} else {\n\t\t\tlogger.Debug(logSender, \"\", \"raw id token %q, parsed claims %+v\", rawIDToken, claims)\n\t\t}\n\t}\n}\n\nfunc (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request) {\n\tstate := r.URL.Query().Get(\"state\")\n\tauthReq, err := oidcMgr.getPendingAuth(state)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"oidc authentication state did not match\")\n\t\toidcMgr.removePendingAuth(state)\n\t\ts.renderClientMessagePage(w, r, util.I18nInvalidAuthReqTitle, http.StatusBadRequest,\n\t\t\tutil.NewI18nError(err, util.I18nInvalidAuth), \"\")\n\t\treturn\n\t}\n\toidcMgr.removePendingAuth(state)\n\n\tdoRedirect := func() {\n\t\tif authReq.Audience == tokenAudienceWebAdmin {\n\t\t\thttp.Redirect(w, r, webAdminLoginPath, http.StatusFound)\n\t\t\treturn\n\t\t}\n\t\thttp.Redirect(w, r, webClientLoginPath, http.StatusFound)\n\t}\n\tdoLogout := func(rawIDToken string) {\n\t\ts.logoutFromOIDCOP(rawIDToken)\n\t}\n\n\tctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)\n\tdefer cancel()\n\n\toauth2Token, err := s.binding.OIDC.oauth2Config.Exchange(ctx, r.URL.Query().Get(\"code\"),\n\t\toauth2.VerifierOption(authReq.Verifier))\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"failed to exchange oidc token: %v\", err)\n\t\tsetFlashMessage(w, r, newFlashMessage(\"Failed to exchange OpenID token\", util.I18nOIDCErrTokenExchange))\n\t\tdoRedirect()\n\t\treturn\n\t}\n\trawIDToken, ok := oauth2Token.Extra(\"id_token\").(string)\n\tif !ok {\n\t\tlogger.Debug(logSender, \"\", \"no id_token field in OAuth2 OpenID token\")\n\t\tsetFlashMessage(w, r, newFlashMessage(\"No id_token field in OAuth2 OpenID token\", util.I18nOIDCTokenInvalid))\n\t\tdoRedirect()\n\t\treturn\n\t}\n\ts.debugTokenClaims(nil, rawIDToken)\n\tidToken, err := s.binding.OIDC.getVerifier(ctx).Verify(ctx, rawIDToken)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"failed to verify oidc token: %v\", err)\n\t\tsetFlashMessage(w, r, newFlashMessage(\"Failed to verify OpenID token\", util.I18nOIDCTokenInvalid))\n\t\tdoRedirect()\n\t\tdoLogout(rawIDToken)\n\t\treturn\n\t}\n\tif idToken.Nonce != authReq.Nonce {\n\t\tlogger.Debug(logSender, \"\", \"oidc authentication nonce did not match\")\n\t\tsetFlashMessage(w, r, newFlashMessage(\"OpenID authentication nonce did not match\", util.I18nOIDCTokenInvalid))\n\t\tdoRedirect()\n\t\tdoLogout(rawIDToken)\n\t\treturn\n\t}\n\n\tclaims := make(map[string]any)\n\terr = idToken.Claims(&claims)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get oidc token claims: %v\", err)\n\t\tsetFlashMessage(w, r, newFlashMessage(\"Unable to get OpenID token claims\", util.I18nOIDCTokenInvalid))\n\t\tdoRedirect()\n\t\tdoLogout(rawIDToken)\n\t\treturn\n\t}\n\ts.debugTokenClaims(claims, rawIDToken)\n\ttoken := oidcToken{\n\t\tAccessToken:  oauth2Token.AccessToken,\n\t\tTokenType:    oauth2Token.TokenType,\n\t\tRefreshToken: oauth2Token.RefreshToken,\n\t\tIDToken:      rawIDToken,\n\t\tNonce:        idToken.Nonce,\n\t\tCookie:       util.GenerateOpaqueString(),\n\t}\n\tif !oauth2Token.Expiry.IsZero() {\n\t\ttoken.ExpiresAt = util.GetTimeAsMsSinceEpoch(oauth2Token.Expiry)\n\t}\n\terr = token.parseClaims(claims, s.binding.OIDC.UsernameField, s.binding.OIDC.RoleField,\n\t\ts.binding.OIDC.CustomFields, s.binding.OIDC.getForcedRole(authReq.Audience))\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to parse oidc token claims: %v\", err)\n\t\tsetFlashMessage(w, r, newFlashMessage(fmt.Sprintf(\"Unable to parse OpenID token claims: %v\", err), util.I18nOIDCTokenInvalid))\n\t\tdoRedirect()\n\t\tdoLogout(rawIDToken)\n\t\treturn\n\t}\n\tswitch authReq.Audience {\n\tcase tokenAudienceWebAdmin:\n\t\tif !token.isAdmin() {\n\t\t\tlogger.Debug(logSender, \"\", \"wrong oidc token role, the mapped user is not an SFTPGo admin\")\n\t\t\tsetFlashMessage(w, r, newFlashMessage(\n\t\t\t\t\"Wrong OpenID role, the logged in user is not an SFTPGo admin\",\n\t\t\t\tutil.I18nOIDCTokenInvalidRoleAdmin))\n\t\t\tdoRedirect()\n\t\t\tdoLogout(rawIDToken)\n\t\t\treturn\n\t\t}\n\tcase tokenAudienceWebClient:\n\t\tif token.isAdmin() {\n\t\t\tlogger.Debug(logSender, \"\", \"wrong oidc token role, the mapped user is an SFTPGo admin\")\n\t\t\tsetFlashMessage(w, r, newFlashMessage(\n\t\t\t\t\"Wrong OpenID role, the logged in user is an SFTPGo admin\",\n\t\t\t\tutil.I18nOIDCTokenInvalidRoleUser,\n\t\t\t))\n\t\t\tdoRedirect()\n\t\t\tdoLogout(rawIDToken)\n\t\t\treturn\n\t\t}\n\t}\n\terr = token.getUser(r)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get the sftpgo user associated with oidc token: %v\", err)\n\t\tsetFlashMessage(w, r, newFlashMessage(\"Unable to get the user associated with the OpenID token\", util.I18nOIDCErrGetUser))\n\t\tdoRedirect()\n\t\tdoLogout(rawIDToken)\n\t\treturn\n\t}\n\n\tloginOIDCUser(w, r, token)\n}\n\nfunc loginOIDCUser(w http.ResponseWriter, r *http.Request, token oidcToken) {\n\toidcMgr.addToken(token)\n\n\tcookie := http.Cookie{\n\t\tName:     oidcCookieKey,\n\t\tValue:    token.Cookie,\n\t\tPath:     \"/\",\n\t\tHttpOnly: true,\n\t\tSecure:   isTLS(r),\n\t\tSameSite: http.SameSiteLaxMode,\n\t}\n\t// we don't set a cookie expiration so we can refresh the token without setting a new cookie\n\t// the cookie will be invalidated on browser close\n\thttp.SetCookie(w, &cookie)\n\tw.Header().Add(\"Cache-Control\", `no-cache=\"Set-Cookie\"`)\n\tif token.isAdmin() {\n\t\thttp.Redirect(w, r, webUsersPath, http.StatusFound)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webClientFilesPath, http.StatusFound)\n}\n\nfunc (s *httpdServer) logoutOIDCUser(w http.ResponseWriter, r *http.Request) {\n\tif oidcKey, ok := r.Context().Value(oidcTokenKey).(string); ok {\n\t\tremoveOIDCCookie(w, r)\n\t\ttoken, err := oidcMgr.getToken(oidcKey)\n\t\tif err == nil {\n\t\t\ts.logoutFromOIDCOP(token.IDToken)\n\t\t}\n\t\toidcMgr.removeToken(oidcKey)\n\t}\n}\n\nfunc (s *httpdServer) logoutFromOIDCOP(idToken string) {\n\tif s.binding.OIDC.providerLogoutURL == \"\" {\n\t\tlogger.Debug(logSender, \"\", \"oidc: provider logout URL not set, unable to logout from the OP\")\n\t\treturn\n\t}\n\tgo s.doOIDCFromLogout(idToken)\n}\n\nfunc (s *httpdServer) doOIDCFromLogout(idToken string) {\n\tlogoutURL, err := url.Parse(s.binding.OIDC.providerLogoutURL)\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"oidc: unable to parse logout URL: %v\", err)\n\t\treturn\n\t}\n\tquery := logoutURL.Query()\n\tif idToken != \"\" {\n\t\tquery.Set(\"id_token_hint\", idToken)\n\t}\n\tlogoutURL.RawQuery = query.Encode()\n\tresp, err := httpclient.RetryableGet(logoutURL.String())\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"oidc: error calling logout URL %q: %v\", logoutURL.String(), err)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tlogger.Debug(logSender, \"\", \"oidc: logout url response code %v\", resp.StatusCode)\n}\n\nfunc removeOIDCCookie(w http.ResponseWriter, r *http.Request) {\n\thttp.SetCookie(w, &http.Cookie{\n\t\tName:     oidcCookieKey,\n\t\tValue:    \"\",\n\t\tPath:     \"/\",\n\t\tExpires:  time.Unix(0, 0),\n\t\tMaxAge:   -1,\n\t\tHttpOnly: true,\n\t\tSecure:   isTLS(r),\n\t\tSameSite: http.SameSiteLaxMode,\n\t})\n}\n\n// canSkipOIDCValidation returns true if there is no OIDC cookie but a jwt cookie is set\n// and so we check if the user is logged in using a built-in user\nfunc canSkipOIDCValidation(r *http.Request) bool {\n\t_, err := r.Cookie(oidcCookieKey)\n\tif err != nil {\n\t\t_, err = r.Cookie(jwt.CookieKey)\n\t\treturn err == nil\n\t}\n\treturn false\n}\n\nfunc isLoggedInWithOIDC(r *http.Request) bool {\n\t_, ok := r.Context().Value(oidcTokenKey).(string)\n\treturn ok\n}\n\nfunc getOIDCFieldFromClaims(claims map[string]any, fieldName string) (any, bool) {\n\tif fieldName == \"\" {\n\t\treturn nil, false\n\t}\n\tval, ok := claims[fieldName]\n\tif ok {\n\t\treturn val, true\n\t}\n\tif !strings.Contains(fieldName, \".\") {\n\t\treturn nil, false\n\t}\n\n\tgetStructValue := func(outer any, field string) (any, bool) {\n\t\tswitch v := outer.(type) {\n\t\tcase map[string]any:\n\t\t\tres, ok := v[field]\n\t\t\treturn res, ok\n\t\t}\n\t\treturn nil, false\n\t}\n\n\tfor idx, field := range strings.Split(fieldName, \".\") {\n\t\tif idx == 0 {\n\t\t\tval, ok = getStructValue(claims, field)\n\t\t} else {\n\t\t\tval, ok = getStructValue(val, field)\n\t\t}\n\t\tif !ok {\n\t\t\treturn nil, false\n\t\t}\n\t}\n\n\treturn val, ok\n}\n"
  },
  {
    "path": "internal/httpd/oidc_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"github.com/coreos/go-oidc/v3/oidc\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/oauth2\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\toidcMockAddr = \"127.0.0.1:11111\"\n)\n\ntype mockTokenSource struct {\n\ttoken *oauth2.Token\n\terr   error\n}\n\nfunc (t *mockTokenSource) Token() (*oauth2.Token, error) {\n\treturn t.token, t.err\n}\n\ntype mockOAuth2Config struct {\n\ttokenSource *mockTokenSource\n\tauthCodeURL string\n\ttoken       *oauth2.Token\n\terr         error\n}\n\nfunc (c *mockOAuth2Config) AuthCodeURL(_ string, _ ...oauth2.AuthCodeOption) string {\n\treturn c.authCodeURL\n}\n\nfunc (c *mockOAuth2Config) Exchange(_ context.Context, _ string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) {\n\treturn c.token, c.err\n}\n\nfunc (c *mockOAuth2Config) TokenSource(_ context.Context, _ *oauth2.Token) oauth2.TokenSource {\n\treturn c.tokenSource\n}\n\ntype mockOIDCVerifier struct {\n\ttoken *oidc.IDToken\n\terr   error\n}\n\nfunc (v *mockOIDCVerifier) Verify(_ context.Context, _ string) (*oidc.IDToken, error) {\n\treturn v.token, v.err\n}\n\n// hack because the field is unexported\nfunc setIDTokenClaims(idToken *oidc.IDToken, claims []byte) {\n\tpointerVal := reflect.ValueOf(idToken)\n\tval := reflect.Indirect(pointerVal)\n\tmember := val.FieldByName(\"claims\")\n\tptr := unsafe.Pointer(member.UnsafeAddr())\n\trealPtr := (*[]byte)(ptr)\n\t*realPtr = claims\n}\n\nfunc TestOIDCInitialization(t *testing.T) {\n\tconfig := OIDC{}\n\terr := config.initialize()\n\tassert.NoError(t, err)\n\tsecret := \"jRsmE0SWnuZjP7djBqNq0mrf8QN77j2c\"\n\tconfig = OIDC{\n\t\tClientID:        \"sftpgo-client\",\n\t\tClientSecret:    util.GenerateUniqueID(),\n\t\tConfigURL:       fmt.Sprintf(\"http://%v/\", oidcMockAddr),\n\t\tRedirectBaseURL: \"http://127.0.0.1:8081/\",\n\t\tUsernameField:   \"preferred_username\",\n\t\tRoleField:       \"sftpgo_role\",\n\t}\n\terr = config.initialize()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"oidc: required scope \\\"openid\\\" is not set\")\n\t}\n\tconfig.Scopes = []string{oidc.ScopeOpenID}\n\tconfig.ClientSecretFile = \"missing file\"\n\terr = config.initialize()\n\tassert.ErrorIs(t, err, fs.ErrNotExist)\n\tsecretFile := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tdefer os.Remove(secretFile)\n\terr = os.WriteFile(secretFile, []byte(secret), 0600)\n\tassert.NoError(t, err)\n\tconfig.ClientSecretFile = secretFile\n\terr = config.initialize()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"oidc: unable to initialize provider\")\n\t}\n\tassert.Equal(t, secret, config.ClientSecret)\n\tconfig.ConfigURL = fmt.Sprintf(\"http://%v/auth/realms/sftpgo\", oidcMockAddr)\n\terr = config.initialize()\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"http://127.0.0.1:8081\"+webOIDCRedirectPath, config.getRedirectURL())\n}\n\nfunc TestOIDCLoginLogout(t *testing.T) {\n\ttokenValidationMode = 2\n\n\toidcMgr, ok := oidcMgr.(*memoryOIDCManager)\n\trequire.True(t, ok)\n\tserver := getTestOIDCServer()\n\terr := server.binding.OIDC.initialize()\n\tassert.NoError(t, err)\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webOIDCRedirectPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nInvalidAuth)\n\n\texpiredAuthReq := oidcPendingAuth{\n\t\tState:    util.GenerateOpaqueString(),\n\t\tNonce:    util.GenerateOpaqueString(),\n\t\tAudience: tokenAudienceWebClient,\n\t\tIssuedAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(-10 * time.Minute)),\n\t}\n\toidcMgr.addPendingAuth(expiredAuthReq)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+expiredAuthReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\tassert.Contains(t, rr.Body.String(), util.I18nInvalidAuth)\n\toidcMgr.removePendingAuth(expiredAuthReq.State)\n\n\tserver.binding.OIDC.oauth2Config = &mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{},\n\t\tauthCodeURL: webOIDCRedirectPath,\n\t\terr:         common.ErrGenericFailure,\n\t}\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr: common.ErrGenericFailure,\n\t}\n\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webAdminOIDCLoginPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webOIDCRedirectPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 1)\n\tvar state string\n\tfor k := range oidcMgr.pendingAuths {\n\t\tstate = k\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+state, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\t// now the same for the web client\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientOIDCLoginPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webOIDCRedirectPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 1)\n\tfor k := range oidcMgr.pendingAuths {\n\t\tstate = k\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+state, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientLoginPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\t// now return an OAuth2 token without the id_token\n\tserver.binding.OIDC.oauth2Config = &mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{},\n\t\tauthCodeURL: webOIDCRedirectPath,\n\t\ttoken: &oauth2.Token{\n\t\t\tAccessToken: \"123\",\n\t\t\tExpiry:      time.Now().Add(5 * time.Minute),\n\t\t},\n\t\terr: nil,\n\t}\n\tauthReq := newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// now fail to verify the id token\n\ttoken := &oauth2.Token{\n\t\tAccessToken: \"123\",\n\t\tExpiry:      time.Now().Add(5 * time.Minute),\n\t}\n\ttoken = token.WithExtra(map[string]any{\n\t\t\"id_token\": \"id_token_val\",\n\t})\n\tserver.binding.OIDC.oauth2Config = &mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{},\n\t\tauthCodeURL: webOIDCRedirectPath,\n\t\ttoken:       token,\n\t\terr:         nil,\n\t}\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// id token nonce does not match\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: &oidc.IDToken{},\n\t}\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// null id token claims\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr: nil,\n\t\ttoken: &oidc.IDToken{\n\t\t\tNonce: authReq.Nonce,\n\t\t},\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// invalid id token claims: no username\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken := &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"aud\": \"my_client_id\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// invalid id token clamims: username not a string\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"aud\": \"my_client_id\",\"preferred_username\": 1}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// invalid audience\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"test\",\"sftpgo_role\":\"admin\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// invalid audience\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebAdmin)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"test\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// mapped user not found\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebAdmin)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"test\",\"sftpgo_role\":\"admin\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\t// admin login ok\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebAdmin)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"admin\",\"sftpgo_role\":\"admin\",\"sid\":\"sid123\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webUsersPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 1)\n\t// admin profile is not available\n\tvar tokenCookie string\n\tfor k := range oidcMgr.tokens {\n\t\ttokenCookie = k\n\t}\n\toidcToken, err := oidcMgr.getToken(tokenCookie)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"sid123\", oidcToken.SessionID)\n\tassert.True(t, oidcToken.isAdmin())\n\tassert.False(t, oidcToken.isExpired())\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webAdminProfilePath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusForbidden, rr.Code)\n\t// the admin can access the allowed pages\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\t// try with an invalid cookie\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, xid.New().String()))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\t// Web Client is not available with an admin token\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\t// logout the admin user\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webLogoutPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 0)\n\t// now login and logout a user\n\tusername := \"test_oidc_user\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPassword: \"pwd\",\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t\tFilters: dataprovider.UserFilters{\n\t\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\t\tWebClient: []string{sdk.WebClientSharesDisabled},\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"test_oidc_user\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientFilesPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 1)\n\t// user profile is not available\n\tfor k := range oidcMgr.tokens {\n\t\ttokenCookie = k\n\t}\n\toidcToken, err = oidcMgr.getToken(tokenCookie)\n\tassert.NoError(t, err)\n\tassert.Empty(t, oidcToken.SessionID)\n\tassert.False(t, oidcToken.isAdmin())\n\tassert.False(t, oidcToken.isExpired())\n\tif assert.Len(t, oidcToken.Permissions, 1) {\n\t\tassert.Equal(t, sdk.WebClientSharesDisabled, oidcToken.Permissions[0])\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientProfilePath, nil)\n\tassert.NoError(t, err)\n\tr.RequestURI = webClientProfilePath\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\t// the user can access the allowed pages\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\t// try with an invalid cookie\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, xid.New().String()))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\t// Web Admin is not available with a client cookie\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\t// logout the user\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientLogoutPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 0)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\ttokenValidationMode = 0\n}\n\nfunc TestOIDCRefreshToken(t *testing.T) {\n\toidcMgr, ok := oidcMgr.(*memoryOIDCManager)\n\trequire.True(t, ok)\n\tr, err := http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\ttoken := oidcToken{\n\t\tCookie:      util.GenerateOpaqueString(),\n\t\tAccessToken: xid.New().String(),\n\t\tTokenType:   \"Bearer\",\n\t\tExpiresAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(-1 * time.Minute)),\n\t\tNonce:       xid.New().String(),\n\t\tRole:        adminRoleFieldValue,\n\t\tUsername:    defaultAdminUsername,\n\t}\n\tconfig := mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{\n\t\t\terr: common.ErrGenericFailure,\n\t\t},\n\t}\n\tverifier := mockOIDCVerifier{\n\t\terr: common.ErrGenericFailure,\n\t}\n\terr = token.refresh(context.Background(), &config, &verifier, r)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"refresh token not set\")\n\t}\n\ttoken.RefreshToken = xid.New().String()\n\terr = token.refresh(context.Background(), &config, &verifier, r)\n\tassert.ErrorIs(t, err, common.ErrGenericFailure)\n\n\tnewToken := &oauth2.Token{\n\t\tAccessToken:  xid.New().String(),\n\t\tRefreshToken: xid.New().String(),\n\t\tExpiry:       time.Now().Add(5 * time.Minute),\n\t}\n\tconfig = mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{\n\t\t\ttoken: newToken,\n\t\t},\n\t}\n\tverifier = mockOIDCVerifier{\n\t\ttoken: &oidc.IDToken{},\n\t}\n\terr = token.refresh(context.Background(), &config, &verifier, r)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"the refreshed token has no id token\")\n\t}\n\tnewToken = newToken.WithExtra(map[string]any{\n\t\t\"id_token\": \"id_token_val\",\n\t})\n\tnewToken.Expiry = time.Time{}\n\tconfig = mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{\n\t\t\ttoken: newToken,\n\t\t},\n\t}\n\tverifier = mockOIDCVerifier{\n\t\terr: common.ErrGenericFailure,\n\t}\n\terr = token.refresh(context.Background(), &config, &verifier, r)\n\tassert.ErrorIs(t, err, common.ErrGenericFailure)\n\n\tnewToken = newToken.WithExtra(map[string]any{\n\t\t\"id_token\": \"id_token_val\",\n\t})\n\tnewToken.Expiry = time.Now().Add(5 * time.Minute)\n\tconfig = mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{\n\t\t\ttoken: newToken,\n\t\t},\n\t}\n\tverifier = mockOIDCVerifier{\n\t\ttoken: &oidc.IDToken{\n\t\t\tNonce: xid.New().String(), // nonce is different from the expected one\n\t\t},\n\t}\n\terr = token.refresh(context.Background(), &config, &verifier, r)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"the refreshed token nonce mismatch\")\n\t}\n\tverifier = mockOIDCVerifier{\n\t\ttoken: &oidc.IDToken{\n\t\t\tNonce: \"\", // empty token is fine on refresh but claims are not set\n\t\t},\n\t}\n\terr = token.refresh(context.Background(), &config, &verifier, r)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"oidc: claims not set\")\n\t}\n\tidToken := &oidc.IDToken{\n\t\tNonce: token.Nonce,\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"sid\":\"id_token_sid\"}`))\n\tverifier = mockOIDCVerifier{\n\t\ttoken: idToken,\n\t}\n\terr = token.refresh(context.Background(), &config, &verifier, r)\n\tassert.NoError(t, err)\n\tassert.Len(t, token.Permissions, 1)\n\ttoken.Role = nil\n\t// user does not exist\n\terr = token.refresh(context.Background(), &config, &verifier, r)\n\tassert.Error(t, err)\n\trequire.Len(t, oidcMgr.tokens, 1)\n\toidcMgr.removeToken(token.Cookie)\n\trequire.Len(t, oidcMgr.tokens, 0)\n}\n\nfunc TestOIDCRefreshUser(t *testing.T) {\n\ttoken := oidcToken{\n\t\tCookie:      util.GenerateOpaqueString(),\n\t\tAccessToken: xid.New().String(),\n\t\tTokenType:   \"Bearer\",\n\t\tExpiresAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Minute)),\n\t\tNonce:       xid.New().String(),\n\t\tRole:        adminRoleFieldValue,\n\t\tUsername:    \"missing username\",\n\t}\n\tr, err := http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\terr = token.refreshUser(r)\n\tassert.Error(t, err)\n\tadmin := dataprovider.Admin{\n\t\tUsername:    \"test_oidc_admin_refresh\",\n\t\tPassword:    \"p\",\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t\tStatus:      0,\n\t\tFilters: dataprovider.AdminFilters{\n\t\t\tPreferences: dataprovider.AdminPreferences{\n\t\t\t\tHideUserPageSections: 1 + 2 + 4,\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\ttoken.Username = admin.Username\n\terr = token.refreshUser(r)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is disabled\")\n\t}\n\n\tadmin.Status = 1\n\terr = dataprovider.UpdateAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = token.refreshUser(r)\n\tassert.NoError(t, err)\n\tassert.Equal(t, admin.Permissions, token.Permissions)\n\tassert.Equal(t, admin.Filters.Preferences.HideUserPageSections, token.HideUserPageSections)\n\n\terr = dataprovider.DeleteAdmin(admin.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tusername := \"test_oidc_user_refresh_token\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPassword: \"p\",\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t\t\tStatus:   0,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t\tFilters: dataprovider.UserFilters{\n\t\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\t\tDeniedProtocols: []string{common.ProtocolHTTP},\n\t\t\t\tWebClient:       []string{sdk.WebClientSharesDisabled, sdk.WebClientWriteDisabled},\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tr, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\ttoken.Role = nil\n\ttoken.Username = username\n\tassert.False(t, token.isAdmin())\n\terr = token.refreshUser(r)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is disabled\")\n\t}\n\tuser, err = dataprovider.UserExists(username, \"\")\n\tassert.NoError(t, err)\n\tuser.Status = 1\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = token.refreshUser(r)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"protocol HTTP is not allowed\")\n\t}\n\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolFTP}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = token.refreshUser(r)\n\tassert.NoError(t, err)\n\tassert.Equal(t, user.Filters.WebClient, token.Permissions)\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestValidateOIDCToken(t *testing.T) {\n\toidcMgr, ok := oidcMgr.(*memoryOIDCManager)\n\trequire.True(t, ok)\n\tserver := getTestOIDCServer()\n\terr := server.binding.OIDC.initialize()\n\tassert.NoError(t, err)\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webClientLogoutPath, nil)\n\tassert.NoError(t, err)\n\t_, err = server.validateOIDCToken(rr, r, false)\n\tassert.ErrorIs(t, err, errInvalidToken)\n\t// expired token and refresh error\n\tserver.binding.OIDC.oauth2Config = &mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{\n\t\t\terr: common.ErrGenericFailure,\n\t\t},\n\t}\n\ttoken := oidcToken{\n\t\tCookie:      util.GenerateOpaqueString(),\n\t\tAccessToken: xid.New().String(),\n\t\tExpiresAt:   util.GetTimeAsMsSinceEpoch(time.Now().Add(-2 * time.Minute)),\n\t}\n\toidcMgr.addToken(token)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientLogoutPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, token.Cookie))\n\t_, err = server.validateOIDCToken(rr, r, false)\n\tassert.ErrorIs(t, err, errInvalidToken)\n\toidcMgr.removeToken(token.Cookie)\n\tassert.Len(t, oidcMgr.tokens, 0)\n\n\tserver.tokenAuth.SetSigner(&failingJoseSigner{})\n\ttoken = oidcToken{\n\t\tCookie:      util.GenerateOpaqueString(),\n\t\tAccessToken: util.GenerateUniqueID(),\n\t}\n\toidcMgr.addToken(token)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientLogoutPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, token.Cookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\toidcMgr.removeToken(token.Cookie)\n\tassert.Len(t, oidcMgr.tokens, 0)\n\n\ttoken = oidcToken{\n\t\tCookie:      util.GenerateOpaqueString(),\n\t\tAccessToken: xid.New().String(),\n\t\tRole:        \"admin\",\n\t}\n\toidcMgr.addToken(token)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webLogoutPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, token.Cookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\toidcMgr.removeToken(token.Cookie)\n\tassert.Len(t, oidcMgr.tokens, 0)\n}\n\nfunc TestSkipOIDCAuth(t *testing.T) {\n\tserver := getTestOIDCServer()\n\terr := server.binding.OIDC.initialize()\n\tassert.NoError(t, err)\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\n\tclaims := jwt.NewClaims(tokenAudienceWebClient, \"\", getTokenDuration(tokenAudienceWebClient))\n\tclaims.Username = \"user\"\n\ttokenString, err := server.tokenAuth.Sign(claims)\n\tassert.NoError(t, err)\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webClientLogoutPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", jwt.CookieKey, tokenString))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n}\n\nfunc TestOIDCLogoutErrors(t *testing.T) {\n\tserver := getTestOIDCServer()\n\tassert.Empty(t, server.binding.OIDC.providerLogoutURL)\n\tserver.logoutFromOIDCOP(\"\")\n\tserver.binding.OIDC.providerLogoutURL = \"http://foo\\x7f.com/\"\n\tserver.doOIDCFromLogout(\"\")\n\tserver.binding.OIDC.providerLogoutURL = \"http://127.0.0.1:11234\"\n\tserver.doOIDCFromLogout(\"\")\n}\n\nfunc TestOIDCToken(t *testing.T) {\n\tadmin := dataprovider.Admin{\n\t\tUsername:    \"test_oidc_admin\",\n\t\tPassword:    \"p\",\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t\tStatus:      0,\n\t}\n\terr := dataprovider.AddAdmin(&admin, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\ttoken := oidcToken{\n\t\tUsername: admin.Username,\n\t}\n\t// role not initialized, user with the specified username does not exist\n\treq, err := http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\terr = token.getUser(req)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\ttoken.Role = \"admin\"\n\treq, err = http.NewRequest(http.MethodGet, webUsersPath, nil)\n\tassert.NoError(t, err)\n\terr = token.getUser(req)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is disabled\")\n\t}\n\terr = dataprovider.DeleteAdmin(admin.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tusername := \"test_oidc_user\"\n\ttoken.Username = username\n\ttoken.Role = \"\"\n\terr = token.getUser(req)\n\tif assert.Error(t, err) {\n\t\tassert.ErrorIs(t, err, util.ErrNotFound)\n\t}\n\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPassword: \"p\",\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t\t\tStatus:   0,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t\tFilters: dataprovider.UserFilters{\n\t\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\t\tDeniedProtocols:    []string{common.ProtocolHTTP},\n\t\t\t\tDeniedLoginMethods: []string{dataprovider.LoginMethodPassword},\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = token.getUser(req)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is disabled\")\n\t}\n\tuser, err = dataprovider.UserExists(username, \"\")\n\tassert.NoError(t, err)\n\tuser.Status = 1\n\tuser.Password = \"np\"\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = token.getUser(req)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"protocol HTTP is not allowed\")\n\t}\n\n\tuser.Filters.DeniedProtocols = nil\n\tuser.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: \"127.0.0.1:8022\",\n\t\t\tUsername: username,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(\"np\"),\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = token.getUser(req)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"SFTP loop\")\n\t}\n\n\tcommon.Config.PostConnectHook = fmt.Sprintf(\"http://%v/404\", oidcMockAddr)\n\n\terr = token.getUser(req)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"access denied\")\n\t}\n\n\tcommon.Config.PostConnectHook = \"\"\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestOIDCImplicitRoles(t *testing.T) {\n\toidcMgr, ok := oidcMgr.(*memoryOIDCManager)\n\trequire.True(t, ok)\n\n\tserver := getTestOIDCServer()\n\tserver.binding.OIDC.ImplicitRoles = true\n\terr := server.binding.OIDC.initialize()\n\tassert.NoError(t, err)\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\n\tauthReq := newOIDCPendingAuth(tokenAudienceWebAdmin)\n\toidcMgr.addPendingAuth(authReq)\n\ttoken := &oauth2.Token{\n\t\tAccessToken: \"1234\",\n\t\tExpiry:      time.Now().Add(5 * time.Minute),\n\t}\n\ttoken = token.WithExtra(map[string]any{\n\t\t\"id_token\": \"id_token_val\",\n\t})\n\tserver.binding.OIDC.oauth2Config = &mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{},\n\t\tauthCodeURL: webOIDCRedirectPath,\n\t\ttoken:       token,\n\t}\n\tidToken := &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"admin\",\"sid\":\"sid456\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webUsersPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 1)\n\tvar tokenCookie string\n\tfor k := range oidcMgr.tokens {\n\t\ttokenCookie = k\n\t}\n\t// Web Client is not available with an admin token\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\t// logout the admin user\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webLogoutPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webAdminLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 0)\n\t// now login and logout a user\n\tusername := \"test_oidc_implicit_user\"\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tPassword: \"pwd\",\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t\tFilters: dataprovider.UserFilters{\n\t\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\t\tWebClient: []string{sdk.WebClientSharesDisabled},\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"test_oidc_implicit_user\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientFilesPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 1)\n\tfor k := range oidcMgr.tokens {\n\t\ttokenCookie = k\n\t}\n\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webClientLogoutPath, nil)\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 0)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestMemoryOIDCManager(t *testing.T) {\n\toidcMgr, ok := oidcMgr.(*memoryOIDCManager)\n\trequire.True(t, ok)\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\tauthReq := newOIDCPendingAuth(tokenAudienceWebAdmin)\n\toidcMgr.addPendingAuth(authReq)\n\trequire.Len(t, oidcMgr.pendingAuths, 1)\n\t_, err := oidcMgr.getPendingAuth(authReq.State)\n\tassert.NoError(t, err)\n\toidcMgr.removePendingAuth(authReq.State)\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\tauthReq.IssuedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-600 * time.Second))\n\toidcMgr.addPendingAuth(authReq)\n\trequire.Len(t, oidcMgr.pendingAuths, 1)\n\t_, err = oidcMgr.getPendingAuth(authReq.State)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"too old\")\n\t}\n\toidcMgr.cleanup()\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\n\ttoken := oidcToken{\n\t\tAccessToken: xid.New().String(),\n\t\tNonce:       xid.New().String(),\n\t\tSessionID:   xid.New().String(),\n\t\tCookie:      util.GenerateOpaqueString(),\n\t\tUsername:    xid.New().String(),\n\t\tRole:        \"admin\",\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t}\n\trequire.Len(t, oidcMgr.tokens, 0)\n\toidcMgr.addToken(token)\n\trequire.Len(t, oidcMgr.tokens, 1)\n\t_, err = oidcMgr.getToken(xid.New().String())\n\tassert.Error(t, err)\n\tstoredToken, err := oidcMgr.getToken(token.Cookie)\n\tassert.NoError(t, err)\n\ttoken.UsedAt = 0 // ensure we don't modify the stored token\n\tassert.Greater(t, storedToken.UsedAt, int64(0))\n\ttoken.UsedAt = storedToken.UsedAt\n\tassert.Equal(t, token, storedToken)\n\t// the usage will not be updated, it is recent\n\toidcMgr.updateTokenUsage(storedToken)\n\tstoredToken, err = oidcMgr.getToken(token.Cookie)\n\tassert.NoError(t, err)\n\tassert.Equal(t, token, storedToken)\n\tusedAt := util.GetTimeAsMsSinceEpoch(time.Now().Add(-5 * time.Minute))\n\tstoredToken.UsedAt = usedAt\n\toidcMgr.tokens[token.Cookie] = storedToken\n\tstoredToken, err = oidcMgr.getToken(token.Cookie)\n\tassert.NoError(t, err)\n\tassert.Equal(t, usedAt, storedToken.UsedAt)\n\ttoken.UsedAt = storedToken.UsedAt\n\tassert.Equal(t, token, storedToken)\n\toidcMgr.updateTokenUsage(storedToken)\n\tstoredToken, err = oidcMgr.getToken(token.Cookie)\n\tassert.NoError(t, err)\n\tassert.Greater(t, storedToken.UsedAt, usedAt)\n\ttoken.UsedAt = storedToken.UsedAt\n\tassert.Equal(t, token, storedToken)\n\tstoredToken.UsedAt = util.GetTimeAsMsSinceEpoch(time.Now()) - tokenDeleteInterval - 1\n\toidcMgr.tokens[token.Cookie] = storedToken\n\tstoredToken, err = oidcMgr.getToken(token.Cookie)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"token is too old\")\n\t}\n\toidcMgr.removeToken(xid.New().String())\n\trequire.Len(t, oidcMgr.tokens, 1)\n\toidcMgr.removeToken(token.Cookie)\n\trequire.Len(t, oidcMgr.tokens, 0)\n\toidcMgr.addToken(token)\n\tusedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-6 * time.Hour))\n\ttoken.UsedAt = usedAt\n\toidcMgr.tokens[token.Cookie] = token\n\tnewToken := oidcToken{\n\t\tCookie: util.GenerateOpaqueString(),\n\t}\n\toidcMgr.addToken(newToken)\n\toidcMgr.cleanup()\n\trequire.Len(t, oidcMgr.tokens, 1)\n\t_, err = oidcMgr.getToken(token.Cookie)\n\tassert.Error(t, err)\n\t_, err = oidcMgr.getToken(newToken.Cookie)\n\tassert.NoError(t, err)\n\toidcMgr.removeToken(newToken.Cookie)\n\trequire.Len(t, oidcMgr.tokens, 0)\n}\n\nfunc TestOIDCEvMgrIntegration(t *testing.T) {\n\tproviderConf := dataprovider.GetProviderConfig()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\tnewProviderConf := providerConf\n\tnewProviderConf.NamingRules = 5\n\terr = dataprovider.Initialize(newProviderConf, configDir, true)\n\tassert.NoError(t, err)\n\t// add a special chars to check json replacer\n\tusername := `test_'oidc_eventmanager`\n\tu := map[string]any{\n\t\t\"username\": \"{{.Name}}\",\n\t\t\"status\":   1,\n\t\t\"home_dir\": filepath.Join(os.TempDir(), \"{{.IDPFieldcustom1.sub}}\"),\n\t\t\"permissions\": map[string][]string{\n\t\t\t\"/\": {dataprovider.PermAny},\n\t\t},\n\t\t\"description\": \"{{.IDPFieldcustom2}}\",\n\t}\n\tuserTmpl, err := json.Marshal(u)\n\trequire.NoError(t, err)\n\ta := map[string]any{\n\t\t\"username\":    \"{{.Name}}\",\n\t\t\"status\":      1,\n\t\t\"permissions\": []string{dataprovider.PermAdminAny},\n\t}\n\tadminTmpl, err := json.Marshal(a)\n\trequire.NoError(t, err)\n\n\taction := &dataprovider.BaseEventAction{\n\t\tName: \"a\",\n\t\tType: dataprovider.ActionTypeIDPAccountCheck,\n\t\tOptions: dataprovider.BaseEventActionOptions{\n\t\t\tIDPConfig: dataprovider.EventActionIDPAccountCheck{\n\t\t\t\tMode:          0,\n\t\t\t\tTemplateUser:  string(userTmpl),\n\t\t\t\tTemplateAdmin: string(adminTmpl),\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddEventAction(action, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\trule := &dataprovider.EventRule{\n\t\tName:    \"r\",\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerIDPLogin,\n\t\tConditions: dataprovider.EventConditions{\n\t\t\tIDPLoginEvent: 0,\n\t\t},\n\t\tActions: []dataprovider.EventAction{\n\t\t\t{\n\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\tName: action.Name,\n\t\t\t\t},\n\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\tExecuteSync: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = dataprovider.AddEventRule(rule, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\toidcMgr, ok := oidcMgr.(*memoryOIDCManager)\n\trequire.True(t, ok)\n\tserver := getTestOIDCServer()\n\tserver.binding.OIDC.ImplicitRoles = true\n\tserver.binding.OIDC.CustomFields = []string{\"custom1.sub\", \"custom2\"}\n\terr = server.binding.OIDC.initialize()\n\tassert.NoError(t, err)\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\t// login a user with OIDC\n\t_, err = dataprovider.UserExists(username, \"\")\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\tauthReq := newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\ttoken := &oauth2.Token{\n\t\tAccessToken: \"1234\",\n\t\tExpiry:      time.Now().Add(5 * time.Minute),\n\t}\n\ttoken = token.WithExtra(map[string]any{\n\t\t\"id_token\": \"id_token_val\",\n\t})\n\tserver.binding.OIDC.oauth2Config = &mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{},\n\t\tauthCodeURL: webOIDCRedirectPath,\n\t\ttoken:       token,\n\t}\n\tidToken := &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"`+util.JSONEscape(username)+`\",\"custom1\":{\"sub\":\"val1\"},\"custom2\":\"desc\"}`)) //nolint:goconst\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientFilesPath, rr.Header().Get(\"Location\"))\n\tuser, err := dataprovider.UserExists(username, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Join(os.TempDir(), \"val1\"), user.GetHomeDir())\n\tassert.Equal(t, \"desc\", user.Description)\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t// login an admin with OIDC\n\t_, err = dataprovider.AdminExists(username)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebAdmin)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"`+util.JSONEscape(username)+`\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webUsersPath, rr.Header().Get(\"Location\"))\n\n\t_, err = dataprovider.AdminExists(username)\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteAdmin(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// set invalid templates and try again\n\taction.Options.IDPConfig.TemplateUser = `{}`\n\taction.Options.IDPConfig.TemplateAdmin = `{}`\n\terr = dataprovider.UpdateEventAction(action, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tfor _, audience := range []string{tokenAudienceWebAdmin, tokenAudienceWebClient} {\n\t\tauthReq = newOIDCPendingAuth(audience)\n\t\toidcMgr.addPendingAuth(authReq)\n\t\tidToken = &oidc.IDToken{\n\t\t\tNonce:  authReq.Nonce,\n\t\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t\t}\n\t\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"`+util.JSONEscape(username)+`\"}`))\n\t\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\t\terr:   nil,\n\t\t\ttoken: idToken,\n\t\t}\n\t\trr = httptest.NewRecorder()\n\t\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\t\tassert.NoError(t, err)\n\t\tserver.router.ServeHTTP(rr, r)\n\t\tassert.Equal(t, http.StatusFound, rr.Code)\n\t}\n\tfor k := range oidcMgr.tokens {\n\t\toidcMgr.removeToken(k)\n\t}\n\n\terr = dataprovider.DeleteEventRule(rule.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteEventAction(action.Name, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestOIDCPreLoginHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toidcMgr, ok := oidcMgr.(*memoryOIDCManager)\n\trequire.True(t, ok)\n\tusername := \"test_oidc_user_prelogin\"\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: username,\n\t\t\tHomeDir:  filepath.Join(os.TempDir(), username),\n\t\t\tStatus:   1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\tpreLoginPath := filepath.Join(os.TempDir(), \"prelogin.sh\")\n\tproviderConf := dataprovider.GetProviderConfig()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tnewProviderConf := providerConf\n\tnewProviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(newProviderConf, configDir, true)\n\tassert.NoError(t, err)\n\tserver := getTestOIDCServer()\n\tserver.binding.OIDC.CustomFields = []string{\"field1\", \"field2\"}\n\terr = server.binding.OIDC.initialize()\n\tassert.NoError(t, err)\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\n\t_, err = dataprovider.UserExists(username, \"\")\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\t// now login with OIDC\n\tauthReq := newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\ttoken := &oauth2.Token{\n\t\tAccessToken: \"1234\",\n\t\tExpiry:      time.Now().Add(5 * time.Minute),\n\t}\n\ttoken = token.WithExtra(map[string]any{\n\t\t\"id_token\": \"id_token_val\",\n\t})\n\tserver.binding.OIDC.oauth2Config = &mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{},\n\t\tauthCodeURL: webOIDCRedirectPath,\n\t\ttoken:       token,\n\t}\n\tidToken := &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"`+username+`\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientFilesPath, rr.Header().Get(\"Location\"))\n\t_, err = dataprovider.UserExists(username, \"\")\n\tassert.NoError(t, err)\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(u.HomeDir)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, true), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tauthReq = newOIDCPendingAuth(tokenAudienceWebClient)\n\toidcMgr.addPendingAuth(authReq)\n\tidToken = &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"`+username+`\",\"field1\":\"value1\",\"field2\":\"value2\",\"field3\":\"value3\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webClientLoginPath, rr.Header().Get(\"Location\"))\n\t_, err = dataprovider.UserExists(username, \"\")\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\tif assert.Len(t, oidcMgr.tokens, 1) {\n\t\tfor k := range oidcMgr.tokens {\n\t\t\toidcMgr.removeToken(k)\n\t\t}\n\t}\n\trequire.Len(t, oidcMgr.pendingAuths, 0)\n\trequire.Len(t, oidcMgr.tokens, 0)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestOIDCIsAdmin(t *testing.T) {\n\ttype test struct {\n\t\tinput any\n\t\twant  bool\n\t}\n\n\temptySlice := make([]any, 0)\n\n\ttests := []test{\n\t\t{input: \"admin\", want: true},\n\t\t{input: append(emptySlice, \"admin\"), want: true},\n\t\t{input: append(emptySlice, \"user\", \"admin\"), want: true},\n\t\t{input: \"user\", want: false},\n\t\t{input: emptySlice, want: false},\n\t\t{input: append(emptySlice, 1), want: false},\n\t\t{input: 1, want: false},\n\t\t{input: nil, want: false},\n\t\t{input: map[string]string{\"admin\": \"admin\"}, want: false},\n\t}\n\tfor _, tc := range tests {\n\t\ttoken := oidcToken{\n\t\t\tRole: tc.input,\n\t\t}\n\t\tassert.Equal(t, tc.want, token.isAdmin(), \"%v should return %t\", tc.input, tc.want)\n\t}\n}\n\nfunc TestParseAdminRole(t *testing.T) {\n\tclaims := make(map[string]any)\n\trawClaims := []byte(`{\n\t\t\"sub\": \"35666371\",\n\t\t\"email\": \"example@example.com\",\n\t\t\"preferred_username\": \"Sally\",\n\t\t\"name\": \"Sally Tyler\",\n\t\t\"updated_at\": \"2018-04-13T22:08:45Z\",\n\t\t\"given_name\": \"Sally\",\n\t\t\"family_name\": \"Tyler\",\n\t\t\"params\": {\n\t\t  \"sftpgo_role\": \"admin\",\n\t\t  \"subparams\": {\n\t\t\t\"sftpgo_role\": \"admin\",\n\t\t\t\"inner\": {\n\t\t\t\t\"sftpgo_role\": [\"user\",\"admin\"]\n\t\t\t}\n\t\t  }\n\t\t},\n\t\t\"at_hash\": \"lPLhxI2wjEndc-WfyroDZA\",\n\t\t\"rt_hash\": \"mCmxPtA04N-55AxlEUbq-A\",\n\t\t\"aud\": \"78d1d040-20c9-0136-5146-067351775fae92920\",\n\t\t\"exp\": 1523664997,\n\t\t\"iat\": 1523657797\n\t  }`)\n\terr := json.Unmarshal(rawClaims, &claims)\n\tassert.NoError(t, err)\n\n\ttype test struct {\n\t\tinput string\n\t\twant  bool\n\t\tval   any\n\t}\n\n\ttests := []test{\n\t\t{input: \"\", want: false},\n\t\t{input: \"sftpgo_role\", want: false},\n\t\t{input: \"params.sftpgo_role\", want: true, val: \"admin\"},\n\t\t{input: \"params.subparams.sftpgo_role\", want: true, val: \"admin\"},\n\t\t{input: \"params.subparams.inner.sftpgo_role\", want: true, val: []any{\"user\", \"admin\"}},\n\t\t{input: \"email\", want: false},\n\t\t{input: \"missing\", want: false},\n\t\t{input: \"params.email\", want: false},\n\t\t{input: \"missing.sftpgo_role\", want: false},\n\t\t{input: \"params\", want: false},\n\t\t{input: \"params.subparams.inner.sftpgo_role.missing\", want: false},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttoken := oidcToken{}\n\t\ttoken.getRoleFromField(claims, tc.input)\n\t\tassert.Equal(t, tc.want, token.isAdmin(), \"%q should return %t\", tc.input, tc.want)\n\t\tif tc.want {\n\t\t\tassert.Equal(t, tc.val, token.Role)\n\t\t}\n\t}\n}\n\nfunc TestOIDCWithLoginFormsDisabled(t *testing.T) {\n\toidcMgr, ok := oidcMgr.(*memoryOIDCManager)\n\trequire.True(t, ok)\n\n\tserver := getTestOIDCServer()\n\tserver.binding.OIDC.ImplicitRoles = true\n\tserver.binding.DisabledLoginMethods = 12\n\tserver.binding.EnableWebAdmin = true\n\tserver.binding.EnableWebClient = true\n\terr := server.binding.OIDC.initialize()\n\tassert.NoError(t, err)\n\terr = server.initializeRouter()\n\trequire.NoError(t, err)\n\t// login with an admin user\n\tauthReq := newOIDCPendingAuth(tokenAudienceWebAdmin)\n\toidcMgr.addPendingAuth(authReq)\n\ttoken := &oauth2.Token{\n\t\tAccessToken: \"1234\",\n\t\tExpiry:      time.Now().Add(5 * time.Minute),\n\t}\n\ttoken = token.WithExtra(map[string]any{\n\t\t\"id_token\": \"id_token_val\",\n\t})\n\tserver.binding.OIDC.oauth2Config = &mockOAuth2Config{\n\t\ttokenSource: &mockTokenSource{},\n\t\tauthCodeURL: webOIDCRedirectPath,\n\t\ttoken:       token,\n\t}\n\tidToken := &oidc.IDToken{\n\t\tNonce:  authReq.Nonce,\n\t\tExpiry: time.Now().Add(5 * time.Minute),\n\t}\n\tsetIDTokenClaims(idToken, []byte(`{\"preferred_username\":\"admin\",\"sid\":\"sid456\"}`))\n\tserver.binding.OIDC.verifier = &mockOIDCVerifier{\n\t\terr:   nil,\n\t\ttoken: idToken,\n\t}\n\trr := httptest.NewRecorder()\n\tr, err := http.NewRequest(http.MethodGet, webOIDCRedirectPath+\"?state=\"+authReq.State, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusFound, rr.Code)\n\tassert.Equal(t, webUsersPath, rr.Header().Get(\"Location\"))\n\tvar tokenCookie string\n\tfor k := range oidcMgr.tokens {\n\t\ttokenCookie = k\n\t}\n\t// we should be able to create admins without setting a password\n\tadminUsername := \"testAdmin\"\n\tform := make(url.Values)\n\tform.Set(csrfFormToken, createCSRFToken(rr, r, server.csrfTokenAuth, tokenCookie, webBaseAdminPath))\n\tform.Set(\"username\", adminUsername)\n\tform.Set(\"password\", \"\")\n\tform.Set(\"status\", \"1\")\n\tform.Set(\"permissions\", \"*\")\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))\n\tassert.NoError(t, err)\n\tr.Header.Set(\"Cookie\", fmt.Sprintf(\"%v=%v\", oidcCookieKey, tokenCookie))\n\tr.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusSeeOther, rr.Code)\n\t_, err = dataprovider.AdminExists(adminUsername)\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteAdmin(adminUsername, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// login and password related routes are disabled\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webAdminLoginPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusMethodNotAllowed, rr.Code)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webAdminTwoFactorPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webClientLoginPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusMethodNotAllowed, rr.Code)\n\trr = httptest.NewRecorder()\n\tr, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, nil)\n\tassert.NoError(t, err)\n\tserver.router.ServeHTTP(rr, r)\n\tassert.Equal(t, http.StatusNotFound, rr.Code)\n}\n\nfunc TestDbOIDCManager(t *testing.T) {\n\tif !isSharedProviderSupported() {\n\t\tt.Skip(\"this test it is not available with this provider\")\n\t}\n\tmgr := newOIDCManager(1)\n\tpendingAuth := newOIDCPendingAuth(tokenAudienceWebAdmin)\n\tmgr.addPendingAuth(pendingAuth)\n\tauthReq, err := mgr.getPendingAuth(pendingAuth.State)\n\tassert.NoError(t, err)\n\tassert.Equal(t, pendingAuth, authReq)\n\tpendingAuth.IssuedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour))\n\tmgr.addPendingAuth(pendingAuth)\n\t_, err = mgr.getPendingAuth(pendingAuth.State)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"auth request is too old\")\n\t}\n\tmgr.removePendingAuth(pendingAuth.State)\n\t_, err = mgr.getPendingAuth(pendingAuth.State)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to get the auth request for the specified state\")\n\t}\n\tmgr.addPendingAuth(pendingAuth)\n\t_, err = mgr.getPendingAuth(pendingAuth.State)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"auth request is too old\")\n\t}\n\tmgr.cleanup()\n\t_, err = mgr.getPendingAuth(pendingAuth.State)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to get the auth request for the specified state\")\n\t}\n\n\ttoken := oidcToken{\n\t\tCookie:       util.GenerateOpaqueString(),\n\t\tAccessToken:  xid.New().String(),\n\t\tTokenType:    \"Bearer\",\n\t\tRefreshToken: xid.New().String(),\n\t\tExpiresAt:    util.GetTimeAsMsSinceEpoch(time.Now().Add(-2 * time.Minute)),\n\t\tSessionID:    xid.New().String(),\n\t\tIDToken:      xid.New().String(),\n\t\tNonce:        xid.New().String(),\n\t\tUsername:     xid.New().String(),\n\t\tPermissions:  []string{dataprovider.PermAdminAny},\n\t\tRole:         \"admin\",\n\t}\n\tmgr.addToken(token)\n\ttokenGet, err := mgr.getToken(token.Cookie)\n\tassert.NoError(t, err)\n\tassert.Greater(t, tokenGet.UsedAt, int64(0))\n\ttoken.UsedAt = tokenGet.UsedAt\n\tassert.Equal(t, token, tokenGet)\n\ttime.Sleep(100 * time.Millisecond)\n\tmgr.updateTokenUsage(token)\n\t// no change\n\ttokenGet, err = mgr.getToken(token.Cookie)\n\tassert.NoError(t, err)\n\tassert.Equal(t, token.UsedAt, tokenGet.UsedAt)\n\ttokenGet.UsedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour))\n\ttokenGet.RefreshToken = xid.New().String()\n\tmgr.updateTokenUsage(tokenGet)\n\ttokenGet, err = mgr.getToken(token.Cookie)\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, tokenGet.RefreshToken)\n\tassert.NotEqual(t, token.RefreshToken, tokenGet.RefreshToken)\n\tassert.Greater(t, tokenGet.UsedAt, token.UsedAt)\n\tmgr.removeToken(token.Cookie)\n\ttokenGet, err = mgr.getToken(token.Cookie)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to get the token for the specified session\")\n\t}\n\t// add an expired token\n\ttoken.UsedAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour))\n\tsession := dataprovider.Session{\n\t\tKey:       token.Cookie,\n\t\tData:      token,\n\t\tType:      dataprovider.SessionTypeOIDCToken,\n\t\tTimestamp: token.UsedAt + tokenDeleteInterval,\n\t}\n\terr = dataprovider.AddSharedSession(session)\n\tassert.NoError(t, err)\n\t_, err = mgr.getToken(token.Cookie)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"token is too old\")\n\t}\n\tmgr.cleanup()\n\t_, err = mgr.getToken(token.Cookie)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to get the token for the specified session\")\n\t}\n\t// adding a session without a key should fail\n\tsession.Key = \"\"\n\terr = dataprovider.AddSharedSession(session)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to save a session with an empty key\")\n\t}\n\tsession.Key = xid.New().String()\n\tsession.Type = 1000\n\terr = dataprovider.AddSharedSession(session)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"invalid session type\")\n\t}\n\n\tdbMgr, ok := mgr.(*dbOIDCManager)\n\tif assert.True(t, ok) {\n\t\t_, err = dbMgr.decodePendingAuthData(2)\n\t\tassert.Error(t, err)\n\t\t_, err = dbMgr.decodeTokenData(true)\n\t\tassert.Error(t, err)\n\t}\n}\n\nfunc getTestOIDCServer() *httpdServer {\n\treturn &httpdServer{\n\t\tbinding: Binding{\n\t\t\tOIDC: OIDC{\n\t\t\t\tClientID:        \"sftpgo-client\",\n\t\t\t\tClientSecret:    \"jRsmE0SWnuZjP7djBqNq0mrf8QN77j2c\",\n\t\t\t\tConfigURL:       fmt.Sprintf(\"http://%v/auth/realms/sftpgo\", oidcMockAddr),\n\t\t\t\tRedirectBaseURL: \"http://127.0.0.1:8081/\",\n\t\t\t\tUsernameField:   \"preferred_username\",\n\t\t\t\tRoleField:       \"sftpgo_role\",\n\t\t\t\tImplicitRoles:   false,\n\t\t\t\tScopes:          []string{oidc.ScopeOpenID, \"profile\", \"email\"},\n\t\t\t\tCustomFields:    nil,\n\t\t\t\tDebug:           true,\n\t\t\t},\n\t\t},\n\t\tenableWebAdmin:  true,\n\t\tenableWebClient: true,\n\t}\n}\n\nfunc getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tif nonJSONResponse {\n\t\tcontent = append(content, []byte(\"echo 'text response'\\n\")...)\n\t\treturn content\n\t}\n\tif len(user.Username) > 0 {\n\t\tu, _ := json.Marshal(user)\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"echo '%v'\\n\", string(u)))...)\n\t}\n\treturn content\n}\n"
  },
  {
    "path": "internal/httpd/oidcmanager.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\toidcMgr oidcManager\n)\n\nfunc newOIDCManager(isShared int) oidcManager {\n\tif isShared == 1 {\n\t\tlogger.Info(logSender, \"\", \"using provider OIDC manager\")\n\t\treturn &dbOIDCManager{}\n\t}\n\tlogger.Info(logSender, \"\", \"using memory OIDC manager\")\n\treturn &memoryOIDCManager{\n\t\tpendingAuths: make(map[string]oidcPendingAuth),\n\t\ttokens:       make(map[string]oidcToken),\n\t}\n}\n\ntype oidcManager interface {\n\taddPendingAuth(pendingAuth oidcPendingAuth)\n\tremovePendingAuth(state string)\n\tgetPendingAuth(state string) (oidcPendingAuth, error)\n\taddToken(token oidcToken)\n\tgetToken(cookie string) (oidcToken, error)\n\tremoveToken(cookie string)\n\tupdateTokenUsage(token oidcToken)\n\tcleanup()\n}\n\ntype memoryOIDCManager struct {\n\tauthMutex    sync.RWMutex\n\tpendingAuths map[string]oidcPendingAuth\n\ttokenMutex   sync.RWMutex\n\ttokens       map[string]oidcToken\n}\n\nfunc (o *memoryOIDCManager) addPendingAuth(pendingAuth oidcPendingAuth) {\n\to.authMutex.Lock()\n\to.pendingAuths[pendingAuth.State] = pendingAuth\n\to.authMutex.Unlock()\n}\n\nfunc (o *memoryOIDCManager) removePendingAuth(state string) {\n\to.authMutex.Lock()\n\tdefer o.authMutex.Unlock()\n\n\tdelete(o.pendingAuths, state)\n}\n\nfunc (o *memoryOIDCManager) getPendingAuth(state string) (oidcPendingAuth, error) {\n\to.authMutex.RLock()\n\tdefer o.authMutex.RUnlock()\n\n\tauthReq, ok := o.pendingAuths[state]\n\tif !ok {\n\t\treturn oidcPendingAuth{}, errors.New(\"oidc: no auth request found for the specified state\")\n\t}\n\tdiff := util.GetTimeAsMsSinceEpoch(time.Now()) - authReq.IssuedAt\n\tif diff > authStateValidity {\n\t\treturn oidcPendingAuth{}, errors.New(\"oidc: auth request is too old\")\n\t}\n\treturn authReq, nil\n}\n\nfunc (o *memoryOIDCManager) addToken(token oidcToken) {\n\to.tokenMutex.Lock()\n\ttoken.UsedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\to.tokens[token.Cookie] = token\n\to.tokenMutex.Unlock()\n}\n\nfunc (o *memoryOIDCManager) getToken(cookie string) (oidcToken, error) {\n\to.tokenMutex.RLock()\n\tdefer o.tokenMutex.RUnlock()\n\n\ttoken, ok := o.tokens[cookie]\n\tif !ok {\n\t\treturn oidcToken{}, errors.New(\"oidc: no token found for the specified session\")\n\t}\n\tdiff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt\n\tif diff > tokenDeleteInterval {\n\t\treturn oidcToken{}, errors.New(\"oidc: token is too old\")\n\t}\n\treturn token, nil\n}\n\nfunc (o *memoryOIDCManager) removeToken(cookie string) {\n\to.tokenMutex.Lock()\n\tdefer o.tokenMutex.Unlock()\n\n\tdelete(o.tokens, cookie)\n}\n\nfunc (o *memoryOIDCManager) updateTokenUsage(token oidcToken) {\n\tdiff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt\n\tif diff > tokenUpdateInterval {\n\t\to.addToken(token)\n\t}\n}\n\nfunc (o *memoryOIDCManager) cleanup() {\n\to.cleanupAuthRequests()\n\to.cleanupTokens()\n}\n\nfunc (o *memoryOIDCManager) cleanupAuthRequests() {\n\to.authMutex.Lock()\n\tdefer o.authMutex.Unlock()\n\n\tfor k, auth := range o.pendingAuths {\n\t\tdiff := util.GetTimeAsMsSinceEpoch(time.Now()) - auth.IssuedAt\n\t\t// remove old pending auth requests\n\t\tif diff < 0 || diff > authStateValidity {\n\t\t\tdelete(o.pendingAuths, k)\n\t\t}\n\t}\n}\n\nfunc (o *memoryOIDCManager) cleanupTokens() {\n\to.tokenMutex.Lock()\n\tdefer o.tokenMutex.Unlock()\n\n\tfor k, token := range o.tokens {\n\t\tdiff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt\n\t\t// remove tokens unused from more than tokenDeleteInterval\n\t\tif diff > tokenDeleteInterval {\n\t\t\tdelete(o.tokens, k)\n\t\t}\n\t}\n}\n\ntype dbOIDCManager struct{}\n\nfunc (o *dbOIDCManager) addPendingAuth(pendingAuth oidcPendingAuth) {\n\tsession := dataprovider.Session{\n\t\tKey:       pendingAuth.State,\n\t\tData:      pendingAuth,\n\t\tType:      dataprovider.SessionTypeOIDCAuth,\n\t\tTimestamp: pendingAuth.IssuedAt + authStateValidity,\n\t}\n\tdataprovider.AddSharedSession(session) //nolint:errcheck\n}\n\nfunc (o *dbOIDCManager) removePendingAuth(state string) {\n\tdataprovider.DeleteSharedSession(state, dataprovider.SessionTypeOIDCAuth) //nolint:errcheck\n}\n\nfunc (o *dbOIDCManager) getPendingAuth(state string) (oidcPendingAuth, error) {\n\tsession, err := dataprovider.GetSharedSession(state, dataprovider.SessionTypeOIDCAuth)\n\tif err != nil {\n\t\treturn oidcPendingAuth{}, errors.New(\"oidc: unable to get the auth request for the specified state\")\n\t}\n\tif session.Timestamp < util.GetTimeAsMsSinceEpoch(time.Now()) {\n\t\t// expired\n\t\treturn oidcPendingAuth{}, errors.New(\"oidc: auth request is too old\")\n\t}\n\treturn o.decodePendingAuthData(session.Data)\n}\n\nfunc (o *dbOIDCManager) decodePendingAuthData(data any) (oidcPendingAuth, error) {\n\tif val, ok := data.([]byte); ok {\n\t\tauthReq := oidcPendingAuth{}\n\t\terr := json.Unmarshal(val, &authReq)\n\t\treturn authReq, err\n\t}\n\tlogger.Error(logSender, \"\", \"invalid oidc auth request data type %T\", data)\n\treturn oidcPendingAuth{}, errors.New(\"oidc: invalid auth request data\")\n}\n\nfunc (o *dbOIDCManager) addToken(token oidcToken) {\n\ttoken.UsedAt = util.GetTimeAsMsSinceEpoch(time.Now())\n\tsession := dataprovider.Session{\n\t\tKey:       token.Cookie,\n\t\tData:      token,\n\t\tType:      dataprovider.SessionTypeOIDCToken,\n\t\tTimestamp: token.UsedAt + tokenDeleteInterval,\n\t}\n\tdataprovider.AddSharedSession(session) //nolint:errcheck\n}\n\nfunc (o *dbOIDCManager) removeToken(cookie string) {\n\tdataprovider.DeleteSharedSession(cookie, dataprovider.SessionTypeOIDCToken) //nolint:errcheck\n}\n\nfunc (o *dbOIDCManager) updateTokenUsage(token oidcToken) {\n\tdiff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt\n\tif diff > tokenUpdateInterval {\n\t\to.addToken(token)\n\t}\n}\n\nfunc (o *dbOIDCManager) getToken(cookie string) (oidcToken, error) {\n\tsession, err := dataprovider.GetSharedSession(cookie, dataprovider.SessionTypeOIDCToken)\n\tif err != nil {\n\t\treturn oidcToken{}, errors.New(\"oidc: unable to get the token for the specified session\")\n\t}\n\tif session.Timestamp < util.GetTimeAsMsSinceEpoch(time.Now()) {\n\t\t// expired\n\t\treturn oidcToken{}, errors.New(\"oidc: token is too old\")\n\t}\n\treturn o.decodeTokenData(session.Data)\n}\n\nfunc (o *dbOIDCManager) decodeTokenData(data any) (oidcToken, error) {\n\tif val, ok := data.([]byte); ok {\n\t\ttoken := oidcToken{}\n\t\terr := json.Unmarshal(val, &token)\n\t\treturn token, err\n\t}\n\tlogger.Error(logSender, \"\", \"invalid oidc token data type %T\", data)\n\treturn oidcToken{}, errors.New(\"oidc: invalid token data\")\n}\n\nfunc (o *dbOIDCManager) cleanup() {\n\tdataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCAuth, time.Now())  //nolint:errcheck\n\tdataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCToken, time.Now()) //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/httpd/resetcode.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/json\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\tresetCodeLifespan = 10 * time.Minute\n\tresetCodesMgr     resetCodeManager\n)\n\ntype resetCodeManager interface {\n\tAdd(code *resetCode) error\n\tGet(code string) (*resetCode, error)\n\tDelete(code string) error\n\tCleanup()\n}\n\nfunc newResetCodeManager(isShared int) resetCodeManager {\n\tif isShared == 1 {\n\t\tlogger.Info(logSender, \"\", \"using provider reset code manager\")\n\t\treturn &dbResetCodeManager{}\n\t}\n\tlogger.Info(logSender, \"\", \"using memory reset code manager\")\n\treturn &memoryResetCodeManager{}\n}\n\ntype resetCode struct {\n\tCode      string    `json:\"code\"`\n\tUsername  string    `json:\"username\"`\n\tIsAdmin   bool      `json:\"is_admin\"`\n\tExpiresAt time.Time `json:\"expires_at\"`\n}\n\nfunc newResetCode(username string, isAdmin bool) *resetCode {\n\treturn &resetCode{\n\t\tCode:      util.GenerateUniqueID(),\n\t\tUsername:  username,\n\t\tIsAdmin:   isAdmin,\n\t\tExpiresAt: time.Now().Add(resetCodeLifespan).UTC(),\n\t}\n}\n\nfunc (c *resetCode) isExpired() bool {\n\treturn c.ExpiresAt.Before(time.Now().UTC())\n}\n\ntype memoryResetCodeManager struct {\n\tresetCodes sync.Map\n}\n\nfunc (m *memoryResetCodeManager) Add(code *resetCode) error {\n\tm.resetCodes.Store(code.Code, code)\n\treturn nil\n}\n\nfunc (m *memoryResetCodeManager) Get(code string) (*resetCode, error) {\n\tc, ok := m.resetCodes.Load(code)\n\tif !ok {\n\t\treturn nil, util.NewRecordNotFoundError(\"reset code not found\")\n\t}\n\treturn c.(*resetCode), nil\n}\n\nfunc (m *memoryResetCodeManager) Delete(code string) error {\n\tm.resetCodes.Delete(code)\n\treturn nil\n}\n\nfunc (m *memoryResetCodeManager) Cleanup() {\n\tm.resetCodes.Range(func(key, value any) bool {\n\t\tc, ok := value.(*resetCode)\n\t\tif !ok || c.isExpired() {\n\t\t\tm.resetCodes.Delete(key)\n\t\t}\n\t\treturn true\n\t})\n}\n\ntype dbResetCodeManager struct{}\n\nfunc (m *dbResetCodeManager) Add(code *resetCode) error {\n\tsession := dataprovider.Session{\n\t\tKey:       code.Code,\n\t\tData:      code,\n\t\tType:      dataprovider.SessionTypeResetCode,\n\t\tTimestamp: util.GetTimeAsMsSinceEpoch(code.ExpiresAt),\n\t}\n\treturn dataprovider.AddSharedSession(session)\n}\n\nfunc (m *dbResetCodeManager) Get(code string) (*resetCode, error) {\n\tsession, err := dataprovider.GetSharedSession(code, dataprovider.SessionTypeResetCode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif session.Timestamp < util.GetTimeAsMsSinceEpoch(time.Now()) {\n\t\t// expired\n\t\treturn nil, util.NewRecordNotFoundError(\"reset code expired\")\n\t}\n\treturn m.decodeData(session.Data)\n}\n\nfunc (m *dbResetCodeManager) decodeData(data any) (*resetCode, error) {\n\tif val, ok := data.([]byte); ok {\n\t\tc := &resetCode{}\n\t\terr := json.Unmarshal(val, c)\n\t\treturn c, err\n\t}\n\tlogger.Error(logSender, \"\", \"invalid reset code data type %T\", data)\n\treturn nil, util.NewRecordNotFoundError(\"invalid reset code\")\n}\n\nfunc (m *dbResetCodeManager) Delete(code string) error {\n\treturn dataprovider.DeleteSharedSession(code, dataprovider.SessionTypeResetCode)\n}\n\nfunc (m *dbResetCodeManager) Cleanup() {\n\tdataprovider.CleanupSharedSessions(dataprovider.SessionTypeResetCode, time.Now()) //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/httpd/resources.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !bundle\n\npackage httpd\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n)\n\nfunc serveStaticDir(router chi.Router, path, fsDirPath string, disableDirectoryIndex bool) {\n\tfileServer(router, path, http.Dir(fsDirPath), disableDirectoryIndex)\n}\n"
  },
  {
    "path": "internal/httpd/resources_embedded.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build bundle\n\npackage httpd\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/bundle\"\n)\n\nfunc serveStaticDir(router chi.Router, path, fsDirPath string, disableDirectoryIndex bool) {\n\tswitch path {\n\tcase webStaticFilesPath:\n\t\tfileServer(router, path, bundle.GetStaticFs(), disableDirectoryIndex)\n\tcase webOpenAPIPath:\n\t\tfileServer(router, path, bundle.GetOpenAPIFs(), disableDirectoryIndex)\n\tdefault:\n\t\tfileServer(router, path, http.Dir(fsDirPath), disableDirectoryIndex)\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/server.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/go-chi/render\"\n\t\"github.com/go-jose/go-jose/v4\"\n\t\"github.com/rs/cors\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/unrolled/secure\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/acme\"\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\tjsonAPISuffix = \"/json\"\n)\n\nvar (\n\tcompressor      = middleware.NewCompressor(5)\n\txForwardedProto = http.CanonicalHeaderKey(\"X-Forwarded-Proto\")\n)\n\ntype httpdServer struct {\n\tbinding           Binding\n\tstaticFilesPath   string\n\topenAPIPath       string\n\tenableWebAdmin    bool\n\tenableWebClient   bool\n\tenableRESTAPI     bool\n\trenderOpenAPI     bool\n\tisShared          int\n\trouter            *chi.Mux\n\ttokenAuth         *jwt.Signer\n\tcsrfTokenAuth     *jwt.Signer\n\tsigningPassphrase string\n\tcors              CorsConfig\n}\n\nfunc newHttpdServer(b Binding, staticFilesPath, signingPassphrase string, cors CorsConfig,\n\topenAPIPath string,\n) *httpdServer {\n\tif openAPIPath == \"\" {\n\t\tb.RenderOpenAPI = false\n\t}\n\treturn &httpdServer{\n\t\tbinding:           b,\n\t\tstaticFilesPath:   staticFilesPath,\n\t\topenAPIPath:       openAPIPath,\n\t\tenableWebAdmin:    b.EnableWebAdmin,\n\t\tenableWebClient:   b.EnableWebClient,\n\t\tenableRESTAPI:     b.EnableRESTAPI,\n\t\trenderOpenAPI:     b.RenderOpenAPI,\n\t\tsigningPassphrase: signingPassphrase,\n\t\tcors:              cors,\n\t}\n}\n\nfunc (s *httpdServer) setShared(value int) {\n\ts.isShared = value\n}\n\nfunc (s *httpdServer) listenAndServe() error {\n\tif err := s.initializeRouter(); err != nil {\n\t\treturn err\n\t}\n\thttpServer := &http.Server{\n\t\tHandler:           s.router,\n\t\tReadHeaderTimeout: 30 * time.Second,\n\t\tIdleTimeout:       60 * time.Second,\n\t\tMaxHeaderBytes:    1 << 16, // 64KB\n\t\tErrorLog:          log.New(&logger.StdLoggerWrapper{Sender: logSender}, \"\", 0),\n\t}\n\tif certMgr != nil && s.binding.EnableHTTPS {\n\t\tcertID := common.DefaultTLSKeyPaidID\n\t\tif getConfigPath(s.binding.CertificateFile, \"\") != \"\" && getConfigPath(s.binding.CertificateKeyFile, \"\") != \"\" {\n\t\t\tcertID = s.binding.GetAddress()\n\t\t}\n\t\tconfig := &tls.Config{\n\t\t\tGetCertificate: certMgr.GetCertificateFunc(certID),\n\t\t\tMinVersion:     util.GetTLSVersion(s.binding.MinTLSVersion),\n\t\t\tNextProtos:     util.GetALPNProtocols(s.binding.Protocols),\n\t\t\tCipherSuites:   util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),\n\t\t}\n\t\thttpServer.TLSConfig = config\n\t\tlogger.Debug(logSender, \"\", \"configured TLS cipher suites for binding %q: %v, certID: %v\",\n\t\t\ts.binding.GetAddress(), httpServer.TLSConfig.CipherSuites, certID)\n\t\tif s.binding.isMutualTLSEnabled() {\n\t\t\thttpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()\n\t\t\thttpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert\n\t\t\thttpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection\n\t\t}\n\t\treturn util.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, true,\n\t\t\ts.binding.listenerWrapper(), logSender)\n\t}\n\treturn util.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, false,\n\t\ts.binding.listenerWrapper(), logSender)\n}\n\nfunc (s *httpdServer) verifyTLSConnection(state tls.ConnectionState) error {\n\tif certMgr != nil {\n\t\tvar clientCrt *x509.Certificate\n\t\tvar clientCrtName string\n\t\tif len(state.PeerCertificates) > 0 {\n\t\t\tclientCrt = state.PeerCertificates[0]\n\t\t\tclientCrtName = clientCrt.Subject.String()\n\t\t}\n\t\tif len(state.VerifiedChains) == 0 {\n\t\t\tlogger.Warn(logSender, \"\", \"TLS connection cannot be verified: unable to get verification chain\")\n\t\t\treturn errors.New(\"TLS connection cannot be verified: unable to get verification chain\")\n\t\t}\n\t\tfor _, verifiedChain := range state.VerifiedChains {\n\t\t\tvar caCrt *x509.Certificate\n\t\t\tif len(verifiedChain) > 0 {\n\t\t\t\tcaCrt = verifiedChain[len(verifiedChain)-1]\n\t\t\t}\n\t\t\tif certMgr.IsRevoked(clientCrt, caCrt) {\n\t\t\t\tlogger.Debug(logSender, \"\", \"tls handshake error, client certificate %q has been revoked\", clientCrtName)\n\t\t\t\treturn common.ErrCrtRevoked\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *httpdServer) refreshCookie(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\ts.checkCookieExpiration(w, r)\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := loginPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          util.I18nLoginTitle,\n\t\tCurrentURL:     webClientLoginPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, rand.Text(), webBaseClientPath),\n\t\tBranding:       s.binding.webClientBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t\tFormDisabled:   s.binding.isWebClientLoginFormDisabled(),\n\t\tCheckRedirect:  true,\n\t}\n\tif next := r.URL.Query().Get(\"next\"); strings.HasPrefix(next, webClientFilesPath) {\n\t\tdata.CurrentURL += \"?next=\" + url.QueryEscape(next)\n\t}\n\tif s.binding.showAdminLoginURL() {\n\t\tdata.AltLoginURL = webAdminLoginPath\n\t\tdata.AltLoginName = s.binding.webAdminBranding().ShortName\n\t}\n\tif smtp.IsEnabled() && !data.FormDisabled {\n\t\tdata.ForgotPwdURL = webClientForgotPwdPath\n\t}\n\tif s.binding.OIDC.isEnabled() && !s.binding.isWebClientOIDCLoginDisabled() {\n\t\tdata.OpenIDLoginURL = webClientOIDCLoginPath\n\t}\n\trenderClientTemplate(w, templateCommonLogin, data)\n}\n\nfunc (s *httpdServer) handleWebClientLogout(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tremoveCookie(w, r, webBaseClientPath)\n\ts.logoutOIDCUser(w, r)\n\n\thttp.Redirect(w, r, webClientLoginPath, http.StatusFound)\n}\n\nfunc (s *httpdServer) handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderClientChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\terr := doChangeUserPassword(r, strings.TrimSpace(r.Form.Get(\"current_password\")),\n\t\tstrings.TrimSpace(r.Form.Get(\"new_password1\")), strings.TrimSpace(r.Form.Get(\"new_password2\")))\n\tif err != nil {\n\t\ts.renderClientChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric))\n\t\treturn\n\t}\n\ts.handleWebClientLogout(w, r)\n}\n\nfunc (s *httpdServer) handleClientWebLogin(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tif !dataprovider.HasAdmin() {\n\t\thttp.Redirect(w, r, webAdminSetupPath, http.StatusFound)\n\t\treturn\n\t}\n\tmsg := getFlashMessage(w, r)\n\ts.renderClientLoginPage(w, r, msg.getI18nError())\n}\n\nfunc (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tprotocol := common.ProtocolHTTP\n\tusername := strings.TrimSpace(r.Form.Get(\"username\"))\n\tpassword := r.Form.Get(\"password\")\n\tif username == \"\" || password == \"\" {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials, r)\n\t\ts.renderClientLoginPage(w, r,\n\t\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\ts.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t}\n\n\tif err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\ts.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nError403Message))\n\t\treturn\n\t}\n\n\tuser, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol)\n\tif err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\ts.renderClientLoginPage(w, r,\n\t\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tconnectionID := fmt.Sprintf(\"%v_%v\", protocol, xid.New().String())\n\tif err := checkHTTPClientUser(&user, r, connectionID, true, false); err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\ts.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nError403Message))\n\t\treturn\n\t}\n\n\tdefer user.CloseFs() //nolint:errcheck\n\terr = user.CheckFsRoot(connectionID)\n\tif err != nil {\n\t\tlogger.Warn(logSender, connectionID, \"unable to check fs root: %v\", err)\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\ts.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorFsGeneric))\n\t\treturn\n\t}\n\ts.loginUser(w, r, &user, connectionID, ipAddr, false, s.renderClientLoginPage)\n}\n\nfunc (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\terr := r.ParseForm()\n\tif err != nil {\n\t\ts.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tnewPassword := strings.TrimSpace(r.Form.Get(\"password\"))\n\tconfirmPassword := strings.TrimSpace(r.Form.Get(\"confirm_password\"))\n\t_, user, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get(\"code\")),\n\t\tnewPassword, confirmPassword, false)\n\tif err != nil {\n\t\ts.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric))\n\t\treturn\n\t}\n\tconnectionID := fmt.Sprintf(\"%v_%v\", getProtocolFromRequest(r), xid.New().String())\n\tif err := checkHTTPClientUser(user, r, connectionID, true, false); err != nil {\n\t\ts.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorLoginAfterReset))\n\t\treturn\n\t}\n\n\tdefer user.CloseFs() //nolint:errcheck\n\terr = user.CheckFsRoot(connectionID)\n\tif err != nil {\n\t\tlogger.Warn(logSender, connectionID, \"unable to check fs root: %v\", err)\n\t\ts.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorLoginAfterReset))\n\t\treturn\n\t}\n\ts.loginUser(w, r, user, connectionID, ipAddr, false, s.renderClientResetPwdPage)\n}\n\nfunc (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\ts.renderNotFoundPage(w, r, nil)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tusername := claims.Username\n\trecoveryCode := strings.TrimSpace(r.Form.Get(\"recovery_code\"))\n\tif username == \"\" || recoveryCode == \"\" {\n\t\ts.renderClientTwoFactorRecoveryPage(w, r,\n\t\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tuser, userMerged, err := dataprovider.GetUserVariants(username, \"\")\n\tif err != nil {\n\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\thandleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck\n\t\t}\n\t\ts.renderClientTwoFactorRecoveryPage(w, r,\n\t\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif !userMerged.Filters.TOTPConfig.Enabled || !slices.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {\n\t\ts.renderClientTwoFactorPage(w, r, util.NewI18nError(\n\t\t\tutil.NewValidationError(\"two factory authentication is not enabled\"), util.I18n2FADisabled))\n\t\treturn\n\t}\n\tfor idx, code := range user.Filters.RecoveryCodes {\n\t\tif err := code.Secret.Decrypt(); err != nil {\n\t\t\ts.renderClientInternalServerErrorPage(w, r, fmt.Errorf(\"unable to decrypt recovery code: %w\", err))\n\t\t\treturn\n\t\t}\n\t\tif code.Secret.GetPayload() == recoveryCode {\n\t\t\tif code.Used {\n\t\t\t\ts.renderClientTwoFactorRecoveryPage(w, r,\n\t\t\t\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tuser.Filters.RecoveryCodes[idx].Used = true\n\t\t\terr = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, ipAddr, user.Role)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"unable to set the recovery code %q as used: %v\", recoveryCode, err)\n\t\t\t\ts.renderClientInternalServerErrorPage(w, r, errors.New(\"unable to set the recovery code as used\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconnectionID := fmt.Sprintf(\"%v_%v\", getProtocolFromRequest(r), xid.New().String())\n\t\t\ts.loginUser(w, r, &userMerged, connectionID, ipAddr, true,\n\t\t\t\ts.renderClientTwoFactorRecoveryPage)\n\t\t\treturn\n\t\t}\n\t}\n\thandleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck\n\ts.renderClientTwoFactorRecoveryPage(w, r,\n\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n}\n\nfunc (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\ts.renderNotFoundPage(w, r, nil)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tusername := claims.Username\n\tpasscode := strings.TrimSpace(r.Form.Get(\"passcode\"))\n\tif username == \"\" || passcode == \"\" {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials, r)\n\t\ts.renderClientTwoFactorPage(w, r,\n\t\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\ts.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(username, \"\")\n\tif err != nil {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\ts.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\ts.renderClientTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled))\n\t\treturn\n\t}\n\terr = user.Filters.TOTPConfig.Secret.Decrypt()\n\tif err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\ts.renderClientInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tmatch, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode,\n\t\tuser.Filters.TOTPConfig.Secret.GetPayload())\n\tif !match || err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials, r)\n\t\ts.renderClientTwoFactorPage(w, r,\n\t\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tconnectionID := fmt.Sprintf(\"%s_%s\", getProtocolFromRequest(r), xid.New().String())\n\ts.loginUser(w, r, &user, connectionID, ipAddr, true, s.renderClientTwoFactorPage)\n}\n\nfunc (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\ts.renderNotFoundPage(w, r, nil)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tusername := claims.Username\n\trecoveryCode := strings.TrimSpace(r.Form.Get(\"recovery_code\"))\n\tif username == \"\" || recoveryCode == \"\" {\n\t\ts.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tadmin, err := dataprovider.AdminExists(username)\n\tif err != nil {\n\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\thandleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck\n\t\t}\n\t\ts.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif !admin.Filters.TOTPConfig.Enabled {\n\t\ts.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(util.NewValidationError(\"two factory authentication is not enabled\"), util.I18n2FADisabled))\n\t\treturn\n\t}\n\tfor idx, code := range admin.Filters.RecoveryCodes {\n\t\tif err := code.Secret.Decrypt(); err != nil {\n\t\t\ts.renderInternalServerErrorPage(w, r, fmt.Errorf(\"unable to decrypt recovery code: %w\", err))\n\t\t\treturn\n\t\t}\n\t\tif code.Secret.GetPayload() == recoveryCode {\n\t\t\tif code.Used {\n\t\t\t\ts.renderTwoFactorRecoveryPage(w, r,\n\t\t\t\t\tutil.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tadmin.Filters.RecoveryCodes[idx].Used = true\n\t\t\terr = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr, admin.Role)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"unable to set the recovery code %q as used: %v\", recoveryCode, err)\n\t\t\t\ts.renderInternalServerErrorPage(w, r, errors.New(\"unable to set the recovery code as used\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\ts.loginAdmin(w, r, &admin, true, s.renderTwoFactorRecoveryPage, ipAddr)\n\t\t\treturn\n\t\t}\n\t}\n\thandleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck\n\ts.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n}\n\nfunc (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\ts.renderNotFoundPage(w, r, nil)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tusername := claims.Username\n\tpasscode := strings.TrimSpace(r.Form.Get(\"passcode\"))\n\tif username == \"\" || passcode == \"\" {\n\t\ts.renderTwoFactorPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\thandleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck\n\t\ts.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tadmin, err := dataprovider.AdminExists(username)\n\tif err != nil {\n\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\thandleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck\n\t\t}\n\t\ts.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif !admin.Filters.TOTPConfig.Enabled {\n\t\ts.renderTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled))\n\t\treturn\n\t}\n\terr = admin.Filters.TOTPConfig.Secret.Decrypt()\n\tif err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tmatch, err := mfa.ValidateTOTPPasscode(admin.Filters.TOTPConfig.ConfigName, passcode,\n\t\tadmin.Filters.TOTPConfig.Secret.GetPayload())\n\tif !match || err != nil {\n\t\thandleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck\n\t\ts.renderTwoFactorPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\ts.loginAdmin(w, r, &admin, true, s.renderTwoFactorPage, ipAddr)\n}\n\nfunc (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderAdminLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tusername := strings.TrimSpace(r.Form.Get(\"username\"))\n\tpassword := strings.TrimSpace(r.Form.Get(\"password\"))\n\tif username == \"\" || password == \"\" {\n\t\ts.renderAdminLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tif err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderAdminLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tadmin, err := dataprovider.CheckAdminAndPass(username, password, ipAddr)\n\tif err != nil {\n\t\thandleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck\n\t\ts.renderAdminLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\ts.loginAdmin(w, r, &admin, false, s.renderAdminLoginPage, ipAddr)\n}\n\nfunc (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := loginPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          util.I18nLoginTitle,\n\t\tCurrentURL:     webAdminLoginPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, rand.Text(), webBaseAdminPath),\n\t\tBranding:       s.binding.webAdminBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t\tFormDisabled:   s.binding.isWebAdminLoginFormDisabled(),\n\t\tCheckRedirect:  false,\n\t}\n\tif s.binding.showClientLoginURL() {\n\t\tdata.AltLoginURL = webClientLoginPath\n\t\tdata.AltLoginName = s.binding.webClientBranding().ShortName\n\t}\n\tif smtp.IsEnabled() && !data.FormDisabled {\n\t\tdata.ForgotPwdURL = webAdminForgotPwdPath\n\t}\n\tif s.binding.OIDC.hasRoles() && !s.binding.isWebAdminOIDCLoginDisabled() {\n\t\tdata.OpenIDLoginURL = webAdminOIDCLoginPath\n\t}\n\trenderAdminTemplate(w, templateCommonLogin, data)\n}\n\nfunc (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tif !dataprovider.HasAdmin() {\n\t\thttp.Redirect(w, r, webAdminSetupPath, http.StatusFound)\n\t\treturn\n\t}\n\tmsg := getFlashMessage(w, r)\n\ts.renderAdminLoginPage(w, r, msg.getI18nError())\n}\n\nfunc (s *httpdServer) handleWebAdminLogout(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tremoveCookie(w, r, webBaseAdminPath)\n\ts.logoutOIDCUser(w, r)\n\n\thttp.Redirect(w, r, webAdminLoginPath, http.StatusFound)\n}\n\nfunc (s *httpdServer) handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\terr := r.ParseForm()\n\tif err != nil {\n\t\ts.renderChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\terr = doChangeAdminPassword(r, strings.TrimSpace(r.Form.Get(\"current_password\")),\n\t\tstrings.TrimSpace(r.Form.Get(\"new_password1\")), strings.TrimSpace(r.Form.Get(\"new_password2\")))\n\tif err != nil {\n\t\ts.renderChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric))\n\t\treturn\n\t}\n\ts.handleWebAdminLogout(w, r)\n}\n\nfunc (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\terr := r.ParseForm()\n\tif err != nil {\n\t\ts.renderResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tnewPassword := strings.TrimSpace(r.Form.Get(\"password\"))\n\tconfirmPassword := strings.TrimSpace(r.Form.Get(\"confirm_password\"))\n\tadmin, _, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get(\"code\")),\n\t\tnewPassword, confirmPassword, true)\n\tif err != nil {\n\t\ts.renderResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric))\n\t\treturn\n\t}\n\n\ts.loginAdmin(w, r, admin, false, s.renderResetPwdPage, ipAddr)\n}\n\nfunc (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tif dataprovider.HasAdmin() {\n\t\ts.renderBadRequestPage(w, r, errors.New(\"an admin user already exists\"))\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\terr := r.ParseForm()\n\tif err != nil {\n\t\ts.renderAdminSetupPage(w, r, \"\", util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tusername := strings.TrimSpace(r.Form.Get(\"username\"))\n\tpassword := strings.TrimSpace(r.Form.Get(\"password\"))\n\tconfirmPassword := strings.TrimSpace(r.Form.Get(\"confirm_password\"))\n\tinstallCode := strings.TrimSpace(r.Form.Get(\"install_code\"))\n\tif installationCode != \"\" && installCode != resolveInstallationCode() {\n\t\ts.renderAdminSetupPage(w, r, username,\n\t\t\tutil.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"%v mismatch\", installationCodeHint)),\n\t\t\t\tutil.I18nErrorSetupInstallCode),\n\t\t)\n\t\treturn\n\t}\n\tif username == \"\" {\n\t\ts.renderAdminSetupPage(w, r, username,\n\t\t\tutil.NewI18nError(util.NewValidationError(\"please set a username\"), util.I18nError500Message))\n\t\treturn\n\t}\n\tif password == \"\" {\n\t\ts.renderAdminSetupPage(w, r, username,\n\t\t\tutil.NewI18nError(util.NewValidationError(\"please set a password\"), util.I18nError500Message))\n\t\treturn\n\t}\n\tif password != confirmPassword {\n\t\ts.renderAdminSetupPage(w, r, username,\n\t\t\tutil.NewI18nError(errors.New(\"the two password fields do not match\"), util.I18nErrorChangePwdNoMatch))\n\t\treturn\n\t}\n\tadmin := dataprovider.Admin{\n\t\tUsername:    username,\n\t\tPassword:    password,\n\t\tStatus:      1,\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t}\n\terr = dataprovider.AddAdmin(&admin, username, ipAddr, \"\")\n\tif err != nil {\n\t\ts.renderAdminSetupPage(w, r, username, util.NewI18nError(err, util.I18nError500Message))\n\t\treturn\n\t}\n\ts.loginAdmin(w, r, &admin, false, nil, ipAddr)\n}\n\nfunc (s *httpdServer) loginUser(\n\tw http.ResponseWriter, r *http.Request, user *dataprovider.User, connectionID, ipAddr string,\n\tisSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, err *util.I18nError),\n) {\n\tc := &jwt.Claims{\n\t\tUsername:                   user.Username,\n\t\tPermissions:                user.Filters.WebClient,\n\t\tRole:                       user.Role,\n\t\tMustSetTwoFactorAuth:       user.MustSetSecondFactor(),\n\t\tMustChangePassword:         user.MustChangePassword(),\n\t\tRequiredTwoFactorProtocols: user.Filters.TwoFactorAuthProtocols,\n\t}\n\tc.Subject = user.GetSignature()\n\n\taudience := tokenAudienceWebClient\n\tif user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) &&\n\t\tuser.CanManageMFA() && !isSecondFactorAuth {\n\t\taudience = tokenAudienceWebClientPartial\n\t}\n\n\terr := createAndSetCookie(w, r, c, s.tokenAuth, audience, ipAddr)\n\tif err != nil {\n\t\tlogger.Warn(logSender, connectionID, \"unable to set user login cookie %v\", err)\n\t\tupdateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\terrorFunc(w, r, util.NewI18nError(err, util.I18nError500Message))\n\t\treturn\n\t}\n\tinvalidateToken(r)\n\tif audience == tokenAudienceWebClientPartial {\n\t\tredirectPath := webClientTwoFactorPath\n\t\tif next := r.URL.Query().Get(\"next\"); strings.HasPrefix(next, webClientFilesPath) {\n\t\t\tredirectPath += \"?next=\" + url.QueryEscape(next)\n\t\t}\n\t\thttp.Redirect(w, r, redirectPath, http.StatusFound)\n\t\treturn\n\t}\n\tupdateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\tdataprovider.UpdateLastLogin(user)\n\tif next := r.URL.Query().Get(\"next\"); strings.HasPrefix(next, webClientFilesPath) {\n\t\thttp.Redirect(w, r, next, http.StatusFound)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webClientFilesPath, http.StatusFound)\n}\n\nfunc (s *httpdServer) loginAdmin(\n\tw http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,\n\tisSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, err *util.I18nError),\n\tipAddr string,\n) {\n\tc := &jwt.Claims{\n\t\tUsername:             admin.Username,\n\t\tPermissions:          admin.Permissions,\n\t\tRole:                 admin.Role,\n\t\tHideUserPageSections: admin.Filters.Preferences.HideUserPageSections,\n\t\tMustSetTwoFactorAuth: admin.Filters.RequireTwoFactor && !admin.Filters.TOTPConfig.Enabled,\n\t\tMustChangePassword:   admin.Filters.RequirePasswordChange,\n\t}\n\tc.Subject = admin.GetSignature()\n\n\taudience := tokenAudienceWebAdmin\n\tif admin.Filters.TOTPConfig.Enabled && admin.CanManageMFA() && !isSecondFactorAuth {\n\t\taudience = tokenAudienceWebAdminPartial\n\t}\n\n\terr := createAndSetCookie(w, r, c, s.tokenAuth, audience, ipAddr)\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to set admin login cookie %v\", err)\n\t\tif errorFunc == nil {\n\t\t\ts.renderAdminSetupPage(w, r, admin.Username, util.NewI18nError(err, util.I18nError500Message))\n\t\t\treturn\n\t\t}\n\t\terrorFunc(w, r, util.NewI18nError(err, util.I18nError500Message))\n\t\treturn\n\t}\n\tinvalidateToken(r)\n\tif audience == tokenAudienceWebAdminPartial {\n\t\thttp.Redirect(w, r, webAdminTwoFactorPath, http.StatusFound)\n\t\treturn\n\t}\n\tdataprovider.UpdateAdminLastLogin(admin)\n\tcommon.DelayLogin(nil)\n\tredirectURL := webUsersPath\n\tif errorFunc == nil {\n\t\tredirectURL = webAdminMFAPath\n\t}\n\thttp.Redirect(w, r, redirectURL, http.StatusFound)\n}\n\nfunc (s *httpdServer) logout(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tinvalidateToken(r)\n\tsendAPIResponse(w, r, nil, \"Your token has been invalidated\", http.StatusOK)\n}\n\nfunc (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tusername, password, ok := r.BasicAuth()\n\tprotocol := common.ProtocolHTTP\n\tif !ok {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials, r)\n\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\tsendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\t\treturn\n\t}\n\tif username == \"\" || strings.TrimSpace(password) == \"\" {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials, r)\n\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\tsendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\t\treturn\n\t}\n\tif err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {\n\t\tupdateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},\n\t\t\tdataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\treturn\n\t}\n\tuser, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol)\n\tif err != nil {\n\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\tsendAPIResponse(w, r, dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized),\n\t\t\thttp.StatusUnauthorized)\n\t\treturn\n\t}\n\tconnectionID := fmt.Sprintf(\"%v_%v\", protocol, xid.New().String())\n\tif err := checkHTTPClientUser(&user, r, connectionID, true, false); err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\treturn\n\t}\n\n\tif user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {\n\t\tpasscode := r.Header.Get(otpHeaderCode)\n\t\tif passcode == \"\" {\n\t\t\tlogger.Debug(logSender, \"\", \"TOTP enabled for user %q and not passcode provided, authentication refused\", user.Username)\n\t\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials, r)\n\t\t\tsendAPIResponse(w, r, dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized),\n\t\t\t\thttp.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\terr = user.Filters.TOTPConfig.Secret.Decrypt()\n\t\tif err != nil {\n\t\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\t\tsendAPIResponse(w, r, fmt.Errorf(\"unable to decrypt TOTP secret: %w\", err), http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tmatch, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode,\n\t\t\tuser.Filters.TOTPConfig.Secret.GetPayload())\n\t\tif !match || err != nil {\n\t\t\tlogger.Debug(logSender, \"invalid passcode for user %q, match? %v, err: %v\", user.Username, match, err)\n\t\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials, r)\n\t\t\tsendAPIResponse(w, r, dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized),\n\t\t\t\thttp.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t}\n\n\tdefer user.CloseFs() //nolint:errcheck\n\terr = user.CheckFsRoot(connectionID)\n\tif err != nil {\n\t\tlogger.Warn(logSender, connectionID, \"unable to check fs root: %v\", err)\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\ts.generateAndSendUserToken(w, r, ipAddr, user)\n}\n\nfunc (s *httpdServer) generateAndSendUserToken(w http.ResponseWriter, r *http.Request, ipAddr string, user dataprovider.User) {\n\tc := &jwt.Claims{\n\t\tUsername:                   user.Username,\n\t\tPermissions:                user.Filters.WebClient,\n\t\tRole:                       user.Role,\n\t\tMustSetTwoFactorAuth:       user.MustSetSecondFactor(),\n\t\tMustChangePassword:         user.MustChangePassword(),\n\t\tRequiredTwoFactorProtocols: user.Filters.TwoFactorAuthProtocols,\n\t}\n\tc.Subject = user.GetSignature()\n\n\ttoken, err := s.tokenAuth.SignWithParams(c, tokenAudienceAPIUser, ipAddr, getTokenDuration(tokenAudienceAPIUser))\n\tif err != nil {\n\t\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure, r)\n\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tupdateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err, r)\n\tdataprovider.UpdateLastLogin(&user)\n\n\trender.JSON(w, r, c.BuildTokenResponse(token))\n}\n\nfunc (s *httpdServer) getToken(w http.ResponseWriter, r *http.Request) {\n\tusername, password, ok := r.BasicAuth()\n\tif !ok {\n\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\tsendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tadmin, err := dataprovider.CheckAdminAndPass(username, password, ipAddr)\n\tif err != nil {\n\t\thandleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck\n\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\tsendAPIResponse(w, r, dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized),\n\t\t\thttp.StatusUnauthorized)\n\t\treturn\n\t}\n\tif admin.Filters.TOTPConfig.Enabled {\n\t\tpasscode := r.Header.Get(otpHeaderCode)\n\t\tif passcode == \"\" {\n\t\t\tlogger.Debug(logSender, \"\", \"TOTP enabled for admin %q and not passcode provided, authentication refused\", admin.Username)\n\t\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\t\terr = handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials)\n\t\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\terr = admin.Filters.TOTPConfig.Secret.Decrypt()\n\t\tif err != nil {\n\t\t\tsendAPIResponse(w, r, fmt.Errorf(\"unable to decrypt TOTP secret: %w\", err),\n\t\t\t\thttp.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tmatch, err := mfa.ValidateTOTPPasscode(admin.Filters.TOTPConfig.ConfigName, passcode,\n\t\t\tadmin.Filters.TOTPConfig.Secret.GetPayload())\n\t\tif !match || err != nil {\n\t\t\tlogger.Debug(logSender, \"invalid passcode for admin %q, match? %v, err: %v\", admin.Username, match, err)\n\t\t\tw.Header().Set(common.HTTPAuthenticationHeader, basicRealm)\n\t\t\terr = handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials)\n\t\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized),\n\t\t\t\thttp.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t}\n\n\ts.generateAndSendToken(w, r, admin, ipAddr)\n}\n\nfunc (s *httpdServer) generateAndSendToken(w http.ResponseWriter, r *http.Request, admin dataprovider.Admin, ip string) {\n\tc := &jwt.Claims{\n\t\tUsername:             admin.Username,\n\t\tPermissions:          admin.Permissions,\n\t\tRole:                 admin.Role,\n\t\tMustSetTwoFactorAuth: admin.Filters.RequireTwoFactor && !admin.Filters.TOTPConfig.Enabled,\n\t\tMustChangePassword:   admin.Filters.RequirePasswordChange,\n\t}\n\tc.Subject = admin.GetSignature()\n\n\ttoken, err := s.tokenAuth.SignWithParams(c, tokenAudienceAPI, ip, getTokenDuration(tokenAudienceAPI))\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdataprovider.UpdateAdminLastLogin(&admin)\n\tcommon.DelayLogin(nil)\n\trender.JSON(w, r, c.BuildTokenResponse(token))\n}\n\nfunc (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Request) {\n\tif _, ok := r.Context().Value(oidcTokenKey).(string); ok {\n\t\treturn\n\t}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\treturn\n\t}\n\tif claims.Username == \"\" || claims.Subject == \"\" {\n\t\treturn\n\t}\n\tif time.Until(claims.Expiry.Time()) > cookieRefreshThreshold {\n\t\treturn\n\t}\n\tif (time.Since(claims.IssuedAt.Time()) + cookieTokenDuration) > maxTokenDuration {\n\t\treturn\n\t}\n\tif claims.Audience.Contains(tokenAudienceWebClient) {\n\t\ts.refreshClientToken(w, r, claims)\n\t} else {\n\t\ts.refreshAdminToken(w, r, claims)\n\t}\n}\n\nfunc (s *httpdServer) refreshClientToken(w http.ResponseWriter, r *http.Request, tokenClaims *jwt.Claims) {\n\tuser, err := dataprovider.GetUserWithGroupSettings(tokenClaims.Username, \"\")\n\tif err != nil {\n\t\treturn\n\t}\n\tif user.GetSignature() != tokenClaims.Subject {\n\t\tlogger.Debug(logSender, \"\", \"signature mismatch for user %q, unable to refresh cookie\", user.Username)\n\t\treturn\n\t}\n\tif err := user.CheckLoginConditions(); err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to refresh cookie for user %q: %v\", user.Username, err)\n\t\treturn\n\t}\n\tif err := checkHTTPClientUser(&user, r, xid.New().String(), true, false); err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to refresh cookie for user %q: %v\", user.Username, err)\n\t\treturn\n\t}\n\n\ttokenClaims.Permissions = user.Filters.WebClient\n\ttokenClaims.Role = user.Role\n\tlogger.Debug(logSender, \"\", \"cookie refreshed for user %q\", user.Username)\n\tcreateAndSetCookie(w, r, tokenClaims, s.tokenAuth, tokenAudienceWebClient, util.GetIPFromRemoteAddress(r.RemoteAddr)) //nolint:errcheck\n}\n\nfunc (s *httpdServer) refreshAdminToken(w http.ResponseWriter, r *http.Request, tokenClaims *jwt.Claims) {\n\tadmin, err := dataprovider.AdminExists(tokenClaims.Username)\n\tif err != nil {\n\t\treturn\n\t}\n\tif admin.GetSignature() != tokenClaims.Subject {\n\t\tlogger.Debug(logSender, \"\", \"signature mismatch for admin %q, unable to refresh cookie\", admin.Username)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := admin.CanLogin(ipAddr); err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to refresh cookie for admin %q, err: %v\", admin.Username, err)\n\t\treturn\n\t}\n\ttokenClaims.Permissions = admin.Permissions\n\ttokenClaims.Role = admin.Role\n\ttokenClaims.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections\n\tlogger.Debug(logSender, \"\", \"cookie refreshed for admin %q\", admin.Username)\n\tcreateAndSetCookie(w, r, tokenClaims, s.tokenAuth, tokenAudienceWebAdmin, ipAddr) //nolint:errcheck\n}\n\nfunc (s *httpdServer) updateContextFromCookie(r *http.Request) *http.Request {\n\t_, err := jwt.FromContext(r.Context())\n\tif err != nil {\n\t\t_, err = r.Cookie(jwt.CookieKey)\n\t\tif err != nil {\n\t\t\treturn r\n\t\t}\n\t\ttoken, err := jwt.VerifyRequest(s.tokenAuth, r, jwt.TokenFromCookie)\n\t\tctx := jwt.NewContext(r.Context(), token, err)\n\t\treturn r.WithContext(ctx)\n\t}\n\treturn r\n}\n\nfunc (s *httpdServer) parseHeaders(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tresponseControllerDeadlines(\n\t\t\thttp.NewResponseController(w),\n\t\t\ttime.Now().Add(60*time.Second),\n\t\t\ttime.Now().Add(60*time.Second),\n\t\t)\n\t\tw.Header().Set(\"Server\", version.GetServerVersion(\"/\", false))\n\t\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\t\tvar ip net.IP\n\t\tisUnixSocket := filepath.IsAbs(s.binding.Address)\n\t\tif !isUnixSocket {\n\t\t\tip = net.ParseIP(ipAddr)\n\t\t}\n\t\tareHeadersAllowed := false\n\t\tif isUnixSocket || ip != nil {\n\t\t\tfor _, allow := range s.binding.allowHeadersFrom {\n\t\t\t\tif allow(ip) {\n\t\t\t\t\tparsedIP := util.GetRealIP(r, s.binding.ClientIPProxyHeader, s.binding.ClientIPHeaderDepth)\n\t\t\t\t\tif parsedIP != \"\" {\n\t\t\t\t\t\tipAddr = parsedIP\n\t\t\t\t\t\tr.RemoteAddr = ipAddr\n\t\t\t\t\t}\n\t\t\t\t\tif forwardedProto := r.Header.Get(xForwardedProto); forwardedProto != \"\" {\n\t\t\t\t\t\tctx := context.WithValue(r.Context(), forwardedProtoKey, forwardedProto)\n\t\t\t\t\t\tr = r.WithContext(ctx)\n\t\t\t\t\t}\n\t\t\t\t\tareHeadersAllowed = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !areHeadersAllowed {\n\t\t\tfor idx := range s.binding.Security.proxyHeaders {\n\t\t\t\tr.Header.Del(s.binding.Security.proxyHeaders[idx])\n\t\t\t}\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc (s *httpdServer) checkConnection(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\t\tcommon.Connections.AddClientConnection(ipAddr)\n\t\tdefer common.Connections.RemoveClientConnection(ipAddr)\n\n\t\tif err := common.Connections.IsNewConnectionAllowed(ipAddr, common.ProtocolHTTP); err != nil {\n\t\t\tlogger.Log(logger.LevelDebug, common.ProtocolHTTP, \"\", \"connection not allowed from ip %q: %v\", ipAddr, err)\n\t\t\ts.sendForbiddenResponse(w, r, util.NewI18nError(err, util.I18nErrorConnectionForbidden))\n\t\t\treturn\n\t\t}\n\t\tif common.IsBanned(ipAddr, common.ProtocolHTTP) {\n\t\t\ts.sendForbiddenResponse(w, r, util.NewI18nError(\n\t\t\t\tutil.NewGenericError(\"your IP address is blocked\"),\n\t\t\t\tutil.I18nErrorIPForbidden),\n\t\t\t)\n\t\t\treturn\n\t\t}\n\t\tif delay, err := common.LimitRate(common.ProtocolHTTP, ipAddr); err != nil {\n\t\t\tdelay += 499999999 * time.Nanosecond\n\t\t\tw.Header().Set(\"Retry-After\", fmt.Sprintf(\"%.0f\", delay.Seconds()))\n\t\t\tw.Header().Set(\"X-Retry-In\", delay.String())\n\t\t\ts.sendTooManyRequestResponse(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc (s *httpdServer) sendTooManyRequestResponse(w http.ResponseWriter, r *http.Request, err error) {\n\tif (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {\n\t\tr = s.updateContextFromCookie(r)\n\t\tif s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {\n\t\t\ts.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,\n\t\t\t\tutil.NewI18nError(errors.New(http.StatusText(http.StatusTooManyRequests)), util.I18nError429Message), \"\")\n\t\t\treturn\n\t\t}\n\t\ts.renderMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,\n\t\t\tutil.NewI18nError(errors.New(http.StatusText(http.StatusTooManyRequests)), util.I18nError429Message), \"\")\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)\n}\n\nfunc (s *httpdServer) sendForbiddenResponse(w http.ResponseWriter, r *http.Request, err error) {\n\tif (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {\n\t\tr = s.updateContextFromCookie(r)\n\t\tif s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {\n\t\t\ts.renderClientForbiddenPage(w, r, err)\n\t\t\treturn\n\t\t}\n\t\ts.renderForbiddenPage(w, r, err)\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, err, \"\", http.StatusForbidden)\n}\n\nfunc (s *httpdServer) badHostHandler(w http.ResponseWriter, r *http.Request) {\n\thost := r.Host\n\tfor _, header := range s.binding.Security.HostsProxyHeaders {\n\t\tif h := r.Header.Get(header); h != \"\" {\n\t\t\thost = h\n\t\t\tbreak\n\t\t}\n\t}\n\tlogger.Debug(logSender, \"\", \"the host %q is not allowed\", host)\n\ts.sendForbiddenResponse(w, r, util.NewI18nError(\n\t\tutil.NewGenericError(http.StatusText(http.StatusForbidden)),\n\t\tutil.I18nErrorConnectionForbidden,\n\t))\n}\n\nfunc (s *httpdServer) notFoundHandler(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tif (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {\n\t\tr = s.updateContextFromCookie(r)\n\t\tif s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {\n\t\t\ts.renderClientNotFoundPage(w, r, nil)\n\t\t\treturn\n\t\t}\n\t\ts.renderNotFoundPage(w, r, nil)\n\t\treturn\n\t}\n\tsendAPIResponse(w, r, nil, http.StatusText(http.StatusNotFound), http.StatusNotFound)\n}\n\nfunc (s *httpdServer) redirectToWebPath(w http.ResponseWriter, r *http.Request, webPath string) {\n\tif dataprovider.HasAdmin() {\n\t\thttp.Redirect(w, r, webPath, http.StatusFound)\n\t\treturn\n\t}\n\tif s.enableWebAdmin {\n\t\thttp.Redirect(w, r, webAdminSetupPath, http.StatusFound)\n\t}\n}\n\n// The StripSlashes causes infinite redirects at the root path if used with http.FileServer.\n// We also don't strip paths with more than one trailing slash, see #1434\nfunc (s *httpdServer) mustStripSlash(r *http.Request) bool {\n\turlPath := getURLPath(r)\n\treturn !strings.HasSuffix(urlPath, \"//\") && !strings.HasPrefix(urlPath, webOpenAPIPath) &&\n\t\t!strings.HasPrefix(urlPath, webStaticFilesPath) && !strings.HasPrefix(urlPath, acmeChallengeURI)\n}\n\nfunc (s *httpdServer) mustCheckPath(r *http.Request) bool {\n\turlPath := getURLPath(r)\n\treturn !strings.HasPrefix(urlPath, webStaticFilesPath) && !strings.HasPrefix(urlPath, acmeChallengeURI)\n}\n\nfunc (s *httpdServer) initializeRouter() error {\n\tsigner, err := jwt.NewSigner(jose.HS256, getSigningKey(s.signingPassphrase))\n\tif err != nil {\n\t\treturn err\n\t}\n\tcsrfSigner, err := jwt.NewSigner(jose.HS256, getSigningKey(s.signingPassphrase))\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar hasHTTPSRedirect bool\n\ts.tokenAuth = signer\n\ts.csrfTokenAuth = csrfSigner\n\ts.router = chi.NewRouter()\n\n\ts.router.Use(middleware.RequestID)\n\ts.router.Use(s.parseHeaders)\n\ts.router.Use(logger.NewStructuredLogger(logger.GetLogger()))\n\ts.router.Use(middleware.Recoverer)\n\tif s.binding.Security.Enabled {\n\t\tsecureMiddleware := secure.New(secure.Options{\n\t\t\tAllowedHosts:              s.binding.Security.AllowedHosts,\n\t\t\tAllowedHostsAreRegex:      s.binding.Security.AllowedHostsAreRegex,\n\t\t\tHostsProxyHeaders:         s.binding.Security.HostsProxyHeaders,\n\t\t\tSSLProxyHeaders:           s.binding.Security.getHTTPSProxyHeaders(),\n\t\t\tSTSSeconds:                s.binding.Security.STSSeconds,\n\t\t\tSTSIncludeSubdomains:      s.binding.Security.STSIncludeSubdomains,\n\t\t\tSTSPreload:                s.binding.Security.STSPreload,\n\t\t\tContentTypeNosniff:        s.binding.Security.ContentTypeNosniff,\n\t\t\tContentSecurityPolicy:     s.binding.Security.ContentSecurityPolicy,\n\t\t\tPermissionsPolicy:         s.binding.Security.PermissionsPolicy,\n\t\t\tCrossOriginOpenerPolicy:   s.binding.Security.CrossOriginOpenerPolicy,\n\t\t\tCrossOriginResourcePolicy: s.binding.Security.CrossOriginResourcePolicy,\n\t\t\tCrossOriginEmbedderPolicy: s.binding.Security.CrossOriginEmbedderPolicy,\n\t\t\tReferrerPolicy:            s.binding.Security.ReferrerPolicy,\n\t\t})\n\t\tsecureMiddleware.SetBadHostHandler(http.HandlerFunc(s.badHostHandler))\n\t\tif s.binding.Security.CacheControl == \"private\" {\n\t\t\ts.router.Use(cacheControlMiddleware)\n\t\t}\n\t\ts.router.Use(secureMiddleware.Handler)\n\t\tif s.binding.Security.HTTPSRedirect {\n\t\t\ts.router.Use(s.binding.Security.redirectHandler)\n\t\t\thasHTTPSRedirect = true\n\t\t}\n\t}\n\tif s.cors.Enabled {\n\t\tc := cors.New(cors.Options{\n\t\t\tAllowedOrigins:       util.RemoveDuplicates(s.cors.AllowedOrigins, true),\n\t\t\tAllowedMethods:       util.RemoveDuplicates(s.cors.AllowedMethods, true),\n\t\t\tAllowedHeaders:       util.RemoveDuplicates(s.cors.AllowedHeaders, true),\n\t\t\tExposedHeaders:       util.RemoveDuplicates(s.cors.ExposedHeaders, true),\n\t\t\tMaxAge:               s.cors.MaxAge,\n\t\t\tAllowCredentials:     s.cors.AllowCredentials,\n\t\t\tOptionsPassthrough:   s.cors.OptionsPassthrough,\n\t\t\tOptionsSuccessStatus: s.cors.OptionsSuccessStatus,\n\t\t\tAllowPrivateNetwork:  s.cors.AllowPrivateNetwork,\n\t\t})\n\t\ts.router.Use(c.Handler)\n\t}\n\ts.router.Use(middleware.Maybe(s.checkConnection, s.mustCheckPath))\n\ts.router.Use(middleware.GetHead)\n\ts.router.Use(middleware.Maybe(middleware.StripSlashes, s.mustStripSlash))\n\n\ts.router.NotFound(s.notFoundHandler)\n\n\ts.router.Get(healthzPath, func(w http.ResponseWriter, r *http.Request) {\n\t\trender.PlainText(w, r, \"ok\")\n\t})\n\n\tif hasHTTPSRedirect {\n\t\tif p := acme.GetHTTP01WebRoot(); p != \"\" {\n\t\t\tserveStaticDir(s.router, acmeChallengeURI, p, true)\n\t\t}\n\t}\n\n\ts.setupRESTAPIRoutes()\n\n\tif s.enableWebAdmin || s.enableWebClient {\n\t\ts.router.Group(func(router chi.Router) {\n\t\t\trouter.Use(cleanCacheControlMiddleware)\n\t\t\trouter.Use(compressor.Handler)\n\t\t\tserveStaticDir(router, webStaticFilesPath, s.staticFilesPath, true)\n\t\t})\n\t\tif s.binding.OIDC.isEnabled() {\n\t\t\ts.router.Get(webOIDCRedirectPath, s.handleOIDCRedirect)\n\t\t}\n\t\tif s.enableWebClient {\n\t\t\ts.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\ts.redirectToWebPath(w, r, webClientLoginPath)\n\t\t\t})\n\t\t\ts.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\ts.redirectToWebPath(w, r, webClientLoginPath)\n\t\t\t})\n\t\t} else {\n\t\t\ts.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\ts.redirectToWebPath(w, r, webAdminLoginPath)\n\t\t\t})\n\t\t\ts.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\ts.redirectToWebPath(w, r, webAdminLoginPath)\n\t\t\t})\n\t\t}\n\t}\n\n\ts.setupWebClientRoutes()\n\ts.setupWebAdminRoutes()\n\treturn nil\n}\n\nfunc (s *httpdServer) setupRESTAPIRoutes() {\n\tif s.enableRESTAPI {\n\t\tif !s.binding.isAdminTokenEndpointDisabled() {\n\t\t\ts.router.Get(tokenPath, s.getToken)\n\t\t\ts.router.Post(adminPath+\"/{username}/forgot-password\", forgotAdminPassword)\n\t\t\ts.router.Post(adminPath+\"/{username}/reset-password\", resetAdminPassword)\n\t\t}\n\n\t\ts.router.Group(func(router chi.Router) {\n\t\t\trouter.Use(checkNodeToken(s.tokenAuth))\n\t\t\tif !s.binding.isAdminAPIKeyAuthDisabled() {\n\t\t\t\trouter.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeAdmin))\n\t\t\t}\n\t\t\trouter.Use(jwt.Verify(s.tokenAuth, jwt.TokenFromHeader))\n\t\t\trouter.Use(jwtAuthenticatorAPI)\n\n\t\t\trouter.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\trender.JSON(w, r, version.Get())\n\t\t\t})\n\n\t\t\trouter.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout)\n\t\t\trouter.With(forbidAPIKeyAuthentication).Get(adminProfilePath, getAdminProfile)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkAuthRequirements).Put(adminProfilePath, updateAdminProfile)\n\t\t\trouter.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword)\n\t\t\t// admin TOTP APIs\n\t\t\trouter.With(forbidAPIKeyAuthentication).Get(adminTOTPConfigsPath, getTOTPConfigs)\n\t\t\trouter.With(forbidAPIKeyAuthentication).Post(adminTOTPGeneratePath, generateTOTPSecret)\n\t\t\trouter.With(forbidAPIKeyAuthentication).Post(adminTOTPValidatePath, validateTOTPPasscode)\n\t\t\trouter.With(forbidAPIKeyAuthentication).Post(adminTOTPSavePath, saveTOTPConfig)\n\t\t\trouter.With(forbidAPIKeyAuthentication).Get(admin2FARecoveryCodesPath, getRecoveryCodes)\n\t\t\trouter.With(forbidAPIKeyAuthentication).Post(admin2FARecoveryCodesPath, generateRecoveryCodes)\n\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkPerms(dataprovider.PermAdminAny)).\n\t\t\t\tGet(apiKeysPath, getAPIKeys)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkPerms(dataprovider.PermAdminAny)).\n\t\t\t\tPost(apiKeysPath, addAPIKey)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkPerms(dataprovider.PermAdminAny)).\n\t\t\t\tGet(apiKeysPath+\"/{id}\", getAPIKeyByID)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkPerms(dataprovider.PermAdminAny)).\n\t\t\t\tPut(apiKeysPath+\"/{id}\", updateAPIKey)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkPerms(dataprovider.PermAdminAny)).\n\t\t\t\tDelete(apiKeysPath+\"/{id}\", deleteAPIKey)\n\n\t\t\trouter.Group(func(router chi.Router) {\n\t\t\t\trouter.Use(s.checkAuthRequirements)\n\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewServerStatus)).\n\t\t\t\t\tGet(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\t\t\trender.JSON(w, r, getServicesStatus())\n\t\t\t\t\t})\n\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewConnections)).Get(activeConnectionsPath, getActiveConnections)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminCloseConnections)).\n\t\t\t\t\tDelete(activeConnectionsPath+\"/{connectionID}\", handleCloseConnection)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+\"/users/scans\", getUsersQuotaScans)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+\"/users/{username}/scan\", startUserQuotaScan)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+\"/folders/scans\", getFoldersQuotaScans)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+\"/folders/{name}/scan\", startFolderQuotaScan)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewUsers)).Get(userPath+\"/{username}\", getUserByUsername) //nolint:goconst\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminChangeUsers)).Put(userPath+\"/{username}\", updateUser)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminDeleteUsers)).Delete(userPath+\"/{username}\", deleteUser)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminDisableMFA)).Put(userPath+\"/{username}/2fa/disable\", disableUser2FA) //nolint:goconst\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders)).Get(folderPath, getFolders)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders)).Get(folderPath+\"/{name}\", getFolderByName) //nolint:goconst\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders)).Post(folderPath, addFolder)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders)).Put(folderPath+\"/{name}\", updateFolder)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders)).Delete(folderPath+\"/{name}\", deleteFolder)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups)).Get(groupPath+\"/{name}\", getGroupByName)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups)).Put(groupPath+\"/{name}\", updateGroup)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups)).Delete(groupPath+\"/{name}\", deleteGroup)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(dumpDataPath, dumpData)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(loadDataPath, loadData)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(loadDataPath, loadDataFromRequest)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+\"/users/{username}/usage\",\n\t\t\t\t\tupdateUserQuotaUsage)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+\"/users/{username}/transfer-usage\",\n\t\t\t\t\tupdateUserTransferQuotaUsage)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+\"/folders/{name}/usage\",\n\t\t\t\t\tupdateFolderQuotaUsage)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewDefender)).Get(defenderHosts+\"/{id}\", getDefenderHostByID)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+\"/{id}\", deleteDefenderHostByID)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(adminPath, getAdmins)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(adminPath, addAdmin)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(adminPath+\"/{username}\", getAdminByUsername)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Put(adminPath+\"/{username}\", updateAdmin)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Delete(adminPath+\"/{username}\", deleteAdmin)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminDisableMFA)).Put(adminPath+\"/{username}/2fa/disable\", disableAdmin2FA)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(retentionChecksPath, getRetentionChecks)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewEvents), compressor.Handler).\n\t\t\t\t\tGet(fsEventsPath, searchFsEvents)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewEvents), compressor.Handler).\n\t\t\t\t\tGet(providerEventsPath, searchProviderEvents)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewEvents), compressor.Handler).\n\t\t\t\t\tGet(logEventsPath, searchLogEvents)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(eventActionsPath, getEventActions)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(eventActionsPath+\"/{name}\", getEventActionByName)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(eventActionsPath, addEventAction)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Put(eventActionsPath+\"/{name}\", updateEventAction)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Delete(eventActionsPath+\"/{name}\", deleteEventAction)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(eventRulesPath, getEventRules)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(eventRulesPath+\"/{name}\", getEventRuleByName)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(eventRulesPath, addEventRule)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Put(eventRulesPath+\"/{name}\", updateEventRule)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Delete(eventRulesPath+\"/{name}\", deleteEventRule)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(eventRulesPath+\"/run/{name}\", runOnDemandRule)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(rolesPath, getRoles)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(rolesPath, addRole)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(rolesPath+\"/{name}\", getRoleByName)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Put(rolesPath+\"/{name}\", updateRole)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Delete(rolesPath+\"/{name}\", deleteRole)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), compressor.Handler).Get(ipListsPath+\"/{type}\", getIPListEntries) //nolint:goconst\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(ipListsPath+\"/{type}\", addIPListEntry)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(ipListsPath+\"/{type}/{ipornet}\", getIPListEntry) //nolint:goconst\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Put(ipListsPath+\"/{type}/{ipornet}\", updateIPListEntry)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Delete(ipListsPath+\"/{type}/{ipornet}\", deleteIPListEntry)\n\t\t\t})\n\t\t})\n\n\t\t// share API available to external users\n\t\ts.router.Get(sharesPath+\"/{id}\", s.downloadFromShare)\n\t\ts.router.Post(sharesPath+\"/{id}\", s.uploadFilesToShare)\n\t\ts.router.Post(sharesPath+\"/{id}/{name}\", s.uploadFileToShare)\n\t\ts.router.With(compressor.Handler).Get(sharesPath+\"/{id}/dirs\", s.readBrowsableShareContents)\n\t\ts.router.Get(sharesPath+\"/{id}/files\", s.downloadBrowsableSharedFile)\n\n\t\tif !s.binding.isUserTokenEndpointDisabled() {\n\t\t\ts.router.Get(userTokenPath, s.getUserToken)\n\t\t\ts.router.Post(userPath+\"/{username}/forgot-password\", forgotUserPassword)\n\t\t\ts.router.Post(userPath+\"/{username}/reset-password\", resetUserPassword)\n\t\t}\n\n\t\ts.router.Group(func(router chi.Router) {\n\t\t\tif !s.binding.isUserAPIKeyAuthDisabled() {\n\t\t\t\trouter.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeUser))\n\t\t\t}\n\t\t\trouter.Use(jwt.Verify(s.tokenAuth, jwt.TokenFromHeader))\n\t\t\trouter.Use(jwtAuthenticatorAPIUser)\n\n\t\t\trouter.With(forbidAPIKeyAuthentication).Get(userLogoutPath, s.logout)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).\n\t\t\t\tPut(userPwdPath, changeUserPassword)\n\t\t\trouter.With(forbidAPIKeyAuthentication).Get(userProfilePath, getUserProfile)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkAuthRequirements).Put(userProfilePath, updateUserProfile)\n\t\t\t// user TOTP APIs\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).\n\t\t\t\tGet(userTOTPConfigsPath, getTOTPConfigs)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).\n\t\t\t\tPost(userTOTPGeneratePath, generateTOTPSecret)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).\n\t\t\t\tPost(userTOTPValidatePath, validateTOTPPasscode)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).\n\t\t\t\tPost(userTOTPSavePath, saveTOTPConfig)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).\n\t\t\t\tGet(user2FARecoveryCodesPath, getRecoveryCodes)\n\t\t\trouter.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).\n\t\t\t\tPost(user2FARecoveryCodesPath, generateRecoveryCodes)\n\n\t\t\trouter.With(s.checkAuthRequirements, compressor.Handler).Get(userDirsPath, readUserFolder)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tPost(userDirsPath, createUserDir)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tPatch(userDirsPath, renameUserFsEntry)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tDelete(userDirsPath, deleteUserDir)\n\t\t\trouter.With(s.checkAuthRequirements).Get(userFilesPath, getUserFile)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tPost(userFilesPath, uploadUserFiles)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tPatch(userFilesPath, renameUserFsEntry)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tDelete(userFilesPath, deleteUserFile)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tPost(userFileActionsPath+\"/move\", renameUserFsEntry)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tPost(userFileActionsPath+\"/copy\", copyUserFsEntry)\n\t\t\trouter.With(s.checkAuthRequirements).Post(userStreamZipPath, getUserFilesAsZipStream)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).\n\t\t\t\tGet(userSharesPath, getShares)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).\n\t\t\t\tPost(userSharesPath, addShare)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).\n\t\t\t\tGet(userSharesPath+\"/{id}\", getShareByID)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).\n\t\t\t\tPut(userSharesPath+\"/{id}\", updateShare)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).\n\t\t\t\tDelete(userSharesPath+\"/{id}\", deleteShare)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tPost(userUploadFilePath, uploadUserFile)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).\n\t\t\t\tPatch(userFilesDirsMetadataPath, setFileDirMetadata)\n\t\t})\n\n\t\tif s.renderOpenAPI {\n\t\t\ts.router.Group(func(router chi.Router) {\n\t\t\t\trouter.Use(cleanCacheControlMiddleware)\n\t\t\t\trouter.Use(compressor.Handler)\n\t\t\t\tserveStaticDir(router, webOpenAPIPath, s.openAPIPath, false)\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc (s *httpdServer) setupWebClientRoutes() {\n\tif s.enableWebClient {\n\t\ts.router.Get(webBaseClientPath, func(w http.ResponseWriter, r *http.Request) {\n\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\thttp.Redirect(w, r, webClientLoginPath, http.StatusFound)\n\t\t})\n\t\ts.router.With(cleanCacheControlMiddleware).Get(path.Join(webStaticFilesPath, \"branding/webclient/logo.png\"),\n\t\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\trenderPNGImage(w, r, dbBrandingConfig.getWebClientLogo())\n\t\t\t})\n\t\ts.router.With(cleanCacheControlMiddleware).Get(path.Join(webStaticFilesPath, \"branding/webclient/favicon.png\"),\n\t\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\trenderPNGImage(w, r, dbBrandingConfig.getWebClientFavicon())\n\t\t\t})\n\t\ts.router.Get(webClientLoginPath, s.handleClientWebLogin)\n\t\tif s.binding.OIDC.isEnabled() && !s.binding.isWebClientOIDCLoginDisabled() {\n\t\t\ts.router.Get(webClientOIDCLoginPath, s.handleWebClientOIDCLogin)\n\t\t}\n\t\tif !s.binding.isWebClientLoginFormDisabled() {\n\t\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\t\tPost(webClientLoginPath, s.handleWebClientLoginPost)\n\t\t\ts.router.Get(webClientForgotPwdPath, s.handleWebClientForgotPwd)\n\t\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\t\tPost(webClientForgotPwdPath, s.handleWebClientForgotPwdPost)\n\t\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\t\tGet(webClientResetPwdPath, s.handleWebClientPasswordReset)\n\t\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\t\tPost(webClientResetPwdPath, s.handleWebClientPasswordResetPost)\n\t\t\ts.router.With(jwt.Verify(s.tokenAuth, jwt.TokenFromCookie),\n\t\t\t\ts.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).\n\t\t\t\tGet(webClientTwoFactorPath, s.handleWebClientTwoFactor)\n\t\t\ts.router.With(jwt.Verify(s.tokenAuth, jwt.TokenFromCookie),\n\t\t\t\ts.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).\n\t\t\t\tPost(webClientTwoFactorPath, s.handleWebClientTwoFactorPost)\n\t\t\ts.router.With(jwt.Verify(s.tokenAuth, jwt.TokenFromCookie),\n\t\t\t\ts.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).\n\t\t\t\tGet(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecovery)\n\t\t\ts.router.With(jwt.Verify(s.tokenAuth, jwt.TokenFromCookie),\n\t\t\t\ts.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).\n\t\t\t\tPost(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)\n\t\t}\n\t\t// share routes available to external users\n\t\ts.router.Get(webClientPubSharesPath+\"/{id}/login\", s.handleClientShareLoginGet)\n\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\tPost(webClientPubSharesPath+\"/{id}/login\", s.handleClientShareLoginPost)\n\t\ts.router.Get(webClientPubSharesPath+\"/{id}/logout\", s.handleClientShareLogout)\n\t\ts.router.Get(webClientPubSharesPath+\"/{id}\", s.downloadFromShare)\n\t\ts.router.Post(webClientPubSharesPath+\"/{id}/partial\", s.handleClientSharePartialDownload)\n\t\ts.router.Get(webClientPubSharesPath+\"/{id}/browse\", s.handleShareGetFiles)\n\t\ts.router.Post(webClientPubSharesPath+\"/{id}/browse/exist\", s.handleClientShareCheckExist)\n\t\ts.router.Get(webClientPubSharesPath+\"/{id}/download\", s.handleClientSharedFile)\n\t\ts.router.Get(webClientPubSharesPath+\"/{id}/upload\", s.handleClientUploadToShare)\n\t\ts.router.With(compressor.Handler).Get(webClientPubSharesPath+\"/{id}/dirs\", s.handleShareGetDirContents)\n\t\ts.router.Post(webClientPubSharesPath+\"/{id}\", s.uploadFilesToShare)\n\t\ts.router.Post(webClientPubSharesPath+\"/{id}/{name}\", s.uploadFileToShare)\n\t\ts.router.Get(webClientPubSharesPath+\"/{id}/viewpdf\", s.handleShareViewPDF)\n\t\ts.router.Get(webClientPubSharesPath+\"/{id}/getpdf\", s.handleShareGetPDF)\n\n\t\ts.router.Group(func(router chi.Router) {\n\t\t\tif s.binding.OIDC.isEnabled() {\n\t\t\t\trouter.Use(s.oidcTokenAuthenticator(tokenAudienceWebClient))\n\t\t\t}\n\t\t\trouter.Use(jwt.Verify(s.tokenAuth, oidcTokenFromContext, jwt.TokenFromCookie))\n\t\t\trouter.Use(jwtAuthenticatorWebClient)\n\n\t\t\trouter.Get(webClientLogoutPath, s.handleWebClientLogout)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientViewPDFPath, s.handleClientViewPDF)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientGetPDFPath, s.handleClientGetPDF)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie, s.verifyCSRFHeader).Get(webClientFilePath, getUserFile)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie, s.verifyCSRFHeader).Get(webClientTasksPath+\"/{id}\",\n\t\t\t\tgetWebTask)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientFilePath, uploadUserFile)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientExistPath, s.handleClientCheckExist)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientEditFilePath, s.handleClientEditFile)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).\n\t\t\t\tDelete(webClientFilesPath, deleteUserFile)\n\t\t\trouter.With(s.checkAuthRequirements, compressor.Handler, s.refreshCookie).\n\t\t\t\tGet(webClientDirsPath, s.handleClientGetDirContents)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientDirsPath, createUserDir)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).\n\t\t\t\tDelete(webClientDirsPath, taskDeleteDir)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientFileActionsPath+\"/move\", taskRenameFsEntry)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientFileActionsPath+\"/copy\", taskCopyFsEntry)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie).\n\t\t\t\tPost(webClientDownloadZipPath, s.handleWebClientDownloadZip)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientPingPath, handlePingRequest)\n\t\t\trouter.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientProfilePath,\n\t\t\t\ts.handleClientGetProfile)\n\t\t\trouter.With(s.checkAuthRequirements).Post(webClientProfilePath, s.handleWebClientProfilePost)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).\n\t\t\t\tGet(webChangeClientPwdPath, s.handleWebClientChangePwd)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).\n\t\t\t\tPost(webChangeClientPwdPath, s.handleWebClientChangePwdPost)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).\n\t\t\t\tGet(webClientMFAPath, s.handleWebClientMFA)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).\n\t\t\t\tGet(webClientMFAPath+\"/qrcode\", getQRCode)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientTOTPGeneratePath, generateTOTPSecret)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientTOTPValidatePath, validateTOTPPasscode)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientTOTPSavePath, saveTOTPConfig)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader, s.refreshCookie).\n\t\t\t\tGet(webClientRecoveryCodesPath, getRecoveryCodes)\n\t\t\trouter.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.verifyCSRFHeader).\n\t\t\t\tPost(webClientRecoveryCodesPath, generateRecoveryCodes)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), compressor.Handler, s.refreshCookie).\n\t\t\t\tGet(webClientSharesPath+jsonAPISuffix, getAllShares)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).\n\t\t\t\tGet(webClientSharesPath, s.handleClientGetShares)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).\n\t\t\t\tGet(webClientSharePath, s.handleClientAddShareGet)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).\n\t\t\t\tPost(webClientSharePath, s.handleClientAddSharePost)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).\n\t\t\t\tGet(webClientSharePath+\"/{id}\", s.handleClientUpdateShareGet)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).\n\t\t\t\tPost(webClientSharePath+\"/{id}\", s.handleClientUpdateSharePost)\n\t\t\trouter.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.verifyCSRFHeader).\n\t\t\t\tDelete(webClientSharePath+\"/{id}\", deleteShare)\n\t\t})\n\t}\n}\n\nfunc (s *httpdServer) setupWebAdminRoutes() {\n\tif s.enableWebAdmin {\n\t\ts.router.Get(webBaseAdminPath, func(w http.ResponseWriter, r *http.Request) {\n\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\t\t\ts.redirectToWebPath(w, r, webAdminLoginPath)\n\t\t})\n\t\ts.router.With(cleanCacheControlMiddleware).Get(path.Join(webStaticFilesPath, \"branding/webadmin/logo.png\"),\n\t\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\trenderPNGImage(w, r, dbBrandingConfig.getWebAdminLogo())\n\t\t\t})\n\t\ts.router.With(cleanCacheControlMiddleware).Get(path.Join(webStaticFilesPath, \"branding/webadmin/favicon.png\"),\n\t\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\t\t\t\trenderPNGImage(w, r, dbBrandingConfig.getWebAdminFavicon())\n\t\t\t})\n\t\ts.router.Get(webAdminLoginPath, s.handleWebAdminLogin)\n\t\tif s.binding.OIDC.hasRoles() && !s.binding.isWebAdminOIDCLoginDisabled() {\n\t\t\ts.router.Get(webAdminOIDCLoginPath, s.handleWebAdminOIDCLogin)\n\t\t}\n\t\ts.router.Get(webOAuth2RedirectPath, s.handleOAuth2TokenRedirect)\n\t\ts.router.Get(webAdminSetupPath, s.handleWebAdminSetupGet)\n\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\tPost(webAdminSetupPath, s.handleWebAdminSetupPost)\n\t\tif !s.binding.isWebAdminLoginFormDisabled() {\n\t\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\t\tPost(webAdminLoginPath, s.handleWebAdminLoginPost)\n\t\t\ts.router.With(jwt.Verify(s.tokenAuth, jwt.TokenFromCookie),\n\t\t\t\ts.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).\n\t\t\t\tGet(webAdminTwoFactorPath, s.handleWebAdminTwoFactor)\n\t\t\ts.router.With(jwt.Verify(s.tokenAuth, jwt.TokenFromCookie),\n\t\t\t\ts.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).\n\t\t\t\tPost(webAdminTwoFactorPath, s.handleWebAdminTwoFactorPost)\n\t\t\ts.router.With(jwt.Verify(s.tokenAuth, jwt.TokenFromCookie),\n\t\t\t\ts.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).\n\t\t\t\tGet(webAdminTwoFactorRecoveryPath, s.handleWebAdminTwoFactorRecovery)\n\t\t\ts.router.With(jwt.Verify(s.tokenAuth, jwt.TokenFromCookie),\n\t\t\t\ts.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).\n\t\t\t\tPost(webAdminTwoFactorRecoveryPath, s.handleWebAdminTwoFactorRecoveryPost)\n\t\t\ts.router.Get(webAdminForgotPwdPath, s.handleWebAdminForgotPwd)\n\t\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\t\tPost(webAdminForgotPwdPath, s.handleWebAdminForgotPwdPost)\n\t\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\t\tGet(webAdminResetPwdPath, s.handleWebAdminPasswordReset)\n\t\t\ts.router.With(jwt.Verify(s.csrfTokenAuth, jwt.TokenFromCookie)).\n\t\t\t\tPost(webAdminResetPwdPath, s.handleWebAdminPasswordResetPost)\n\t\t}\n\n\t\ts.router.Group(func(router chi.Router) {\n\t\t\tif s.binding.OIDC.isEnabled() {\n\t\t\t\trouter.Use(s.oidcTokenAuthenticator(tokenAudienceWebAdmin))\n\t\t\t}\n\t\t\trouter.Use(jwt.Verify(s.tokenAuth, oidcTokenFromContext, jwt.TokenFromCookie))\n\t\t\trouter.Use(jwtAuthenticatorWebAdmin)\n\n\t\t\trouter.Get(webLogoutPath, s.handleWebAdminLogout)\n\t\t\trouter.With(s.refreshCookie, s.checkAuthRequirements, s.requireBuiltinLogin).Get(\n\t\t\t\twebAdminProfilePath, s.handleWebAdminProfile)\n\t\t\trouter.With(s.checkAuthRequirements, s.requireBuiltinLogin).Post(webAdminProfilePath, s.handleWebAdminProfilePost)\n\t\t\trouter.With(s.refreshCookie, s.requireBuiltinLogin).Get(webChangeAdminPwdPath, s.handleWebAdminChangePwd)\n\t\t\trouter.With(s.requireBuiltinLogin).Post(webChangeAdminPwdPath, s.handleWebAdminChangePwdPost)\n\n\t\t\trouter.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminMFAPath, s.handleWebAdminMFA)\n\t\t\trouter.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminMFAPath+\"/qrcode\", getQRCode)\n\t\t\trouter.With(s.verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret)\n\t\t\trouter.With(s.verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode)\n\t\t\trouter.With(s.verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPSavePath, saveTOTPConfig)\n\t\t\trouter.With(s.verifyCSRFHeader, s.requireBuiltinLogin, s.refreshCookie).Get(webAdminRecoveryCodesPath,\n\t\t\t\tgetRecoveryCodes)\n\t\t\trouter.With(s.verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes)\n\n\t\t\trouter.Group(func(router chi.Router) {\n\t\t\t\trouter.Use(s.checkAuthRequirements)\n\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewUsers), s.refreshCookie).\n\t\t\t\t\tGet(webUsersPath, s.handleGetWebUsers)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewUsers), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webUsersPath+jsonAPISuffix, getAllUsers)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAddUsers), s.refreshCookie).\n\t\t\t\t\tGet(webUserPath, s.handleWebAddUserGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminChangeUsers), s.refreshCookie).\n\t\t\t\t\tGet(webUserPath+\"/{username}\", s.handleWebUpdateUserGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAddUsers)).Post(webUserPath, s.handleWebAddUserPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminChangeUsers)).Post(webUserPath+\"/{username}\",\n\t\t\t\t\ts.handleWebUpdateUserPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups), s.refreshCookie).\n\t\t\t\t\tGet(webGroupsPath, s.handleWebGetGroups)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webGroupsPath+jsonAPISuffix, getAllGroups)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups), s.refreshCookie).\n\t\t\t\t\tGet(webGroupPath, s.handleWebAddGroupGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups)).Post(webGroupPath, s.handleWebAddGroupPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups), s.refreshCookie).\n\t\t\t\t\tGet(webGroupPath+\"/{name}\", s.handleWebUpdateGroupGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups)).Post(webGroupPath+\"/{name}\",\n\t\t\t\t\ts.handleWebUpdateGroupPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageGroups), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webGroupPath+\"/{name}\", deleteGroup)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewConnections), s.refreshCookie).\n\t\t\t\t\tGet(webConnectionsPath, s.handleWebGetConnections)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewConnections), s.refreshCookie).\n\t\t\t\t\tGet(webConnectionsPath+jsonAPISuffix, getActiveConnections)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders), s.refreshCookie).\n\t\t\t\t\tGet(webFoldersPath, s.handleWebGetFolders)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webFoldersPath+jsonAPISuffix, getAllFolders)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders), s.refreshCookie).\n\t\t\t\t\tGet(webFolderPath, s.handleWebAddFolderGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders)).Post(webFolderPath, s.handleWebAddFolderPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewServerStatus), s.refreshCookie).\n\t\t\t\t\tGet(webStatusPath, s.handleWebGetStatus)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminsPath, s.handleGetWebAdmins)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webAdminsPath+jsonAPISuffix, getAllAdmins)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminPath, s.handleWebAddAdminGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminPath+\"/{username}\", s.handleWebUpdateAdminGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webAdminPath, s.handleWebAddAdminPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webAdminPath+\"/{username}\",\n\t\t\t\t\ts.handleWebUpdateAdminPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webAdminPath+\"/{username}\", deleteAdmin)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminDisableMFA), s.verifyCSRFHeader).\n\t\t\t\t\tPut(webAdminPath+\"/{username}/2fa/disable\", disableAdmin2FA)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminCloseConnections), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webConnectionsPath+\"/{connectionID}\", handleCloseConnection)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders), s.refreshCookie).\n\t\t\t\t\tGet(webFolderPath+\"/{name}\", s.handleWebUpdateFolderGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders)).Post(webFolderPath+\"/{name}\",\n\t\t\t\t\ts.handleWebUpdateFolderPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webFolderPath+\"/{name}\", deleteFolder)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminQuotaScans), s.verifyCSRFHeader).\n\t\t\t\t\tPost(webScanVFolderPath+\"/{name}\", startFolderQuotaScan)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminDeleteUsers), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webUserPath+\"/{username}\", deleteUser)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminDisableMFA), s.verifyCSRFHeader).\n\t\t\t\t\tPut(webUserPath+\"/{username}/2fa/disable\", disableUser2FA)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminQuotaScans), s.verifyCSRFHeader).\n\t\t\t\t\tPost(webQuotaScanPath+\"/{username}\", startUserQuotaScan)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(webMaintenancePath, s.handleWebMaintenance)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(webBackupPath, dumpData)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webRestorePath, s.handleWebRestore)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAddUsers, dataprovider.PermAdminChangeUsers), s.refreshCookie).\n\t\t\t\t\tGet(webTemplateUser, s.handleWebTemplateUserGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAddUsers, dataprovider.PermAdminChangeUsers)).\n\t\t\t\t\tPost(webTemplateUser, s.handleWebTemplateUserPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders), s.refreshCookie).\n\t\t\t\t\tGet(webTemplateFolder, s.handleWebTemplateFolderGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageFolders)).Post(webTemplateFolder, s.handleWebTemplateFolderPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, s.handleWebDefenderPage)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminManageDefender), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webDefenderHostsPath+\"/{id}\", deleteDefenderHostByID)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webAdminEventActionsPath+jsonAPISuffix, getAllActions)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminEventActionsPath, s.handleWebGetEventActions)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminEventActionPath, s.handleWebAddEventActionGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webAdminEventActionPath,\n\t\t\t\t\ts.handleWebAddEventActionPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminEventActionPath+\"/{name}\", s.handleWebUpdateEventActionGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webAdminEventActionPath+\"/{name}\",\n\t\t\t\t\ts.handleWebUpdateEventActionPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webAdminEventActionPath+\"/{name}\", deleteEventAction)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webAdminEventRulesPath+jsonAPISuffix, getAllRules)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminEventRulesPath, s.handleWebGetEventRules)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminEventRulePath, s.handleWebAddEventRuleGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webAdminEventRulePath,\n\t\t\t\t\ts.handleWebAddEventRulePost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminEventRulePath+\"/{name}\", s.handleWebUpdateEventRuleGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webAdminEventRulePath+\"/{name}\",\n\t\t\t\t\ts.handleWebUpdateEventRulePost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webAdminEventRulePath+\"/{name}\", deleteEventRule)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.verifyCSRFHeader).\n\t\t\t\t\tPost(webAdminEventRulePath+\"/run/{name}\", runOnDemandRule)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminRolesPath, s.handleWebGetRoles)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webAdminRolesPath+jsonAPISuffix, getAllRoles)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminRolePath, s.handleWebAddRoleGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webAdminRolePath, s.handleWebAddRolePost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).\n\t\t\t\t\tGet(webAdminRolePath+\"/{name}\", s.handleWebUpdateRoleGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webAdminRolePath+\"/{name}\",\n\t\t\t\t\ts.handleWebUpdateRolePost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webAdminRolePath+\"/{name}\", deleteRole)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewEvents), s.refreshCookie).Get(webEventsPath,\n\t\t\t\t\ts.handleWebGetEvents)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webEventsFsSearchPath, searchFsEvents)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webEventsProviderSearchPath, searchProviderEvents)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webEventsLogSearchPath, searchLogEvents)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Get(webIPListsPath, s.handleWebIPListsPage)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), compressor.Handler, s.refreshCookie).\n\t\t\t\t\tGet(webIPListsPath+\"/{type}\", getIPListEntries)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).Get(webIPListPath+\"/{type}\",\n\t\t\t\t\ts.handleWebAddIPListEntryGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webIPListPath+\"/{type}\",\n\t\t\t\t\ts.handleWebAddIPListEntryPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).Get(webIPListPath+\"/{type}/{ipornet}\",\n\t\t\t\t\ts.handleWebUpdateIPListEntryGet)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webIPListPath+\"/{type}/{ipornet}\",\n\t\t\t\t\ts.handleWebUpdateIPListEntryPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.verifyCSRFHeader).\n\t\t\t\t\tDelete(webIPListPath+\"/{type}/{ipornet}\", deleteIPListEntry)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.refreshCookie).Get(webConfigsPath, s.handleWebConfigs)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny)).Post(webConfigsPath, s.handleWebConfigsPost)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.verifyCSRFHeader, s.refreshCookie).\n\t\t\t\t\tPost(webConfigsPath+\"/smtp/test\", testSMTPConfig)\n\t\t\t\trouter.With(s.checkPerms(dataprovider.PermAdminAny), s.verifyCSRFHeader, s.refreshCookie).\n\t\t\t\t\tPost(webOAuth2TokenPath, s.handleSMTPOAuth2TokenRequestPost)\n\t\t\t})\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/token.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc newTokenManager(isShared int) tokenManager {\n\tif isShared == 1 {\n\t\tlogger.Info(logSender, \"\", \"using provider token manager\")\n\t\treturn &dbTokenManager{}\n\t}\n\tlogger.Info(logSender, \"\", \"using memory token manager\")\n\treturn &memoryTokenManager{}\n}\n\ntype tokenManager interface {\n\tAdd(token string, expiresAt time.Time)\n\tGet(token string) bool\n\tCleanup()\n}\n\ntype memoryTokenManager struct {\n\tinvalidatedJWTTokens sync.Map\n}\n\nfunc (m *memoryTokenManager) Add(token string, expiresAt time.Time) {\n\tm.invalidatedJWTTokens.Store(token, expiresAt)\n}\n\nfunc (m *memoryTokenManager) Get(token string) bool {\n\t_, ok := m.invalidatedJWTTokens.Load(token)\n\treturn ok\n}\n\nfunc (m *memoryTokenManager) Cleanup() {\n\tm.invalidatedJWTTokens.Range(func(key, value any) bool {\n\t\texp, ok := value.(time.Time)\n\t\tif !ok || exp.Before(time.Now().UTC()) {\n\t\t\tm.invalidatedJWTTokens.Delete(key)\n\t\t}\n\t\treturn true\n\t})\n}\n\ntype dbTokenManager struct{}\n\nfunc (m *dbTokenManager) getKey(token string) string {\n\tdigest := sha256.Sum256([]byte(token))\n\treturn hex.EncodeToString(digest[:])\n}\n\nfunc (m *dbTokenManager) Add(token string, expiresAt time.Time) {\n\tkey := m.getKey(token)\n\tdata := map[string]string{\n\t\t\"jwt\": token,\n\t}\n\tsession := dataprovider.Session{\n\t\tKey:       key,\n\t\tData:      data,\n\t\tType:      dataprovider.SessionTypeInvalidToken,\n\t\tTimestamp: util.GetTimeAsMsSinceEpoch(expiresAt),\n\t}\n\tdataprovider.AddSharedSession(session) //nolint:errcheck\n}\n\nfunc (m *dbTokenManager) Get(token string) bool {\n\tkey := m.getKey(token)\n\t_, err := dataprovider.GetSharedSession(key, dataprovider.SessionTypeInvalidToken)\n\treturn err == nil\n}\n\nfunc (m *dbTokenManager) Cleanup() {\n\tdataprovider.CleanupSharedSessions(dataprovider.SessionTypeInvalidToken, time.Now()) //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/httpd/web.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-chi/render\"\n\t\"github.com/unrolled/secure\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\twebDateTimeFormat         = \"2006-01-02 15:04:05\" // YYYY-MM-DD HH:MM:SS\n\tredactedSecret            = \"[**redacted**]\"\n\tcsrfFormToken             = \"_form_token\"\n\tcsrfHeaderToken           = \"X-CSRF-TOKEN\"\n\ttemplateCommonDir         = \"common\"\n\ttemplateTwoFactor         = \"twofactor.html\"\n\ttemplateTwoFactorRecovery = \"twofactor-recovery.html\"\n\ttemplateForgotPassword    = \"forgot-password.html\"\n\ttemplateResetPassword     = \"reset-password.html\"\n\ttemplateChangePwd         = \"changepassword.html\"\n\ttemplateMessage           = \"message.html\"\n\ttemplateCommonBase        = \"base.html\"\n\ttemplateCommonBaseLogin   = \"baselogin.html\"\n\ttemplateCommonLogin       = \"login.html\"\n)\n\nvar (\n\terrInvalidTokenClaims = errors.New(\"invalid token claims\")\n)\n\ntype commonBasePage struct {\n\tCSPNonce  string\n\tStaticURL string\n\tVersion   string\n}\n\ntype loginPage struct {\n\tcommonBasePage\n\tCurrentURL     string\n\tError          *util.I18nError\n\tCSRFToken      string\n\tAltLoginURL    string\n\tAltLoginName   string\n\tForgotPwdURL   string\n\tOpenIDLoginURL string\n\tTitle          string\n\tBranding       UIBranding\n\tLanguages      []string\n\tFormDisabled   bool\n\tCheckRedirect  bool\n}\n\ntype twoFactorPage struct {\n\tcommonBasePage\n\tCurrentURL    string\n\tError         *util.I18nError\n\tCSRFToken     string\n\tRecoveryURL   string\n\tTitle         string\n\tBranding      UIBranding\n\tLanguages     []string\n\tCheckRedirect bool\n}\n\ntype forgotPwdPage struct {\n\tcommonBasePage\n\tCurrentURL    string\n\tError         *util.I18nError\n\tCSRFToken     string\n\tLoginURL      string\n\tTitle         string\n\tBranding      UIBranding\n\tLanguages     []string\n\tCheckRedirect bool\n}\n\ntype resetPwdPage struct {\n\tcommonBasePage\n\tCurrentURL    string\n\tError         *util.I18nError\n\tCSRFToken     string\n\tLoginURL      string\n\tTitle         string\n\tBranding      UIBranding\n\tLanguages     []string\n\tCheckRedirect bool\n}\n\nfunc getSliceFromDelimitedValues(values, delimiter string) []string {\n\tresult := []string{}\n\tfor v := range strings.SplitSeq(values, delimiter) {\n\t\tcleaned := strings.TrimSpace(v)\n\t\tif cleaned != \"\" {\n\t\t\tresult = append(result, cleaned)\n\t\t}\n\t}\n\treturn result\n}\n\nfunc hasPrefixAndSuffix(key, prefix, suffix string) bool {\n\treturn strings.HasPrefix(key, prefix) && strings.HasSuffix(key, suffix)\n}\n\nfunc getCommonBasePage(r *http.Request) commonBasePage {\n\treturn commonBasePage{\n\t\tCSPNonce:  secure.CSPNonce(r.Context()),\n\t\tStaticURL: webStaticFilesPath,\n\t\tVersion:   version.GetServerVersion(\" \", true),\n\t}\n}\n\nfunc i18nListDirMsg(status int) string {\n\tif status == http.StatusForbidden {\n\t\treturn util.I18nErrorDirList403\n\t}\n\treturn util.I18nErrorDirListGeneric\n}\n\nfunc i18nFsMsg(status int) string {\n\tif status == http.StatusForbidden {\n\t\treturn util.I18nError403Message\n\t}\n\treturn util.I18nErrorFsGeneric\n}\n\nfunc getI18NErrorString(err error, fallback string) string {\n\tvar errI18n *util.I18nError\n\tif errors.As(err, &errI18n) {\n\t\treturn errI18n.Message\n\t}\n\treturn fallback\n}\n\nfunc getI18nError(err error) *util.I18nError {\n\tvar errI18n *util.I18nError\n\tif err != nil {\n\t\terrI18n = util.NewI18nError(err, util.I18nError500Message)\n\t}\n\treturn errI18n\n}\n\nfunc handlePingRequest(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\trender.PlainText(w, r, \"PONG\")\n}\n"
  },
  {
    "path": "internal/httpd/webadmin.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sftpgo/sdk\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"golang.org/x/oauth2\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/acme\"\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/ftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\ntype userPageMode int\n\nconst (\n\tuserPageModeAdd userPageMode = iota + 1\n\tuserPageModeUpdate\n\tuserPageModeTemplate\n)\n\ntype folderPageMode int\n\nconst (\n\tfolderPageModeAdd folderPageMode = iota + 1\n\tfolderPageModeUpdate\n\tfolderPageModeTemplate\n)\n\ntype genericPageMode int\n\nconst (\n\tgenericPageModeAdd genericPageMode = iota + 1\n\tgenericPageModeUpdate\n)\n\nconst (\n\ttemplateAdminDir     = \"webadmin\"\n\ttemplateBase         = \"base.html\"\n\ttemplateFsConfig     = \"fsconfig.html\"\n\ttemplateUsers        = \"users.html\"\n\ttemplateUser         = \"user.html\"\n\ttemplateAdmins       = \"admins.html\"\n\ttemplateAdmin        = \"admin.html\"\n\ttemplateConnections  = \"connections.html\"\n\ttemplateGroups       = \"groups.html\"\n\ttemplateGroup        = \"group.html\"\n\ttemplateFolders      = \"folders.html\"\n\ttemplateFolder       = \"folder.html\"\n\ttemplateEventRules   = \"eventrules.html\"\n\ttemplateEventRule    = \"eventrule.html\"\n\ttemplateEventActions = \"eventactions.html\"\n\ttemplateEventAction  = \"eventaction.html\"\n\ttemplateRoles        = \"roles.html\"\n\ttemplateRole         = \"role.html\"\n\ttemplateEvents       = \"events.html\"\n\ttemplateStatus       = \"status.html\"\n\ttemplateDefender     = \"defender.html\"\n\ttemplateIPLists      = \"iplists.html\"\n\ttemplateIPList       = \"iplist.html\"\n\ttemplateConfigs      = \"configs.html\"\n\ttemplateProfile      = \"profile.html\"\n\ttemplateMaintenance  = \"maintenance.html\"\n\ttemplateMFA          = \"mfa.html\"\n\ttemplateSetup        = \"adminsetup.html\"\n\tdefaultQueryLimit    = 1000\n\tinversePatternType   = \"inverse\"\n)\n\nvar (\n\tadminTemplates = make(map[string]*template.Template)\n)\n\ntype basePage struct {\n\tcommonBasePage\n\tTitle               string\n\tCurrentURL          string\n\tUsersURL            string\n\tUserURL             string\n\tUserTemplateURL     string\n\tAdminsURL           string\n\tAdminURL            string\n\tQuotaScanURL        string\n\tConnectionsURL      string\n\tGroupsURL           string\n\tGroupURL            string\n\tFoldersURL          string\n\tFolderURL           string\n\tFolderTemplateURL   string\n\tDefenderURL         string\n\tIPListsURL          string\n\tIPListURL           string\n\tEventsURL           string\n\tConfigsURL          string\n\tLogoutURL           string\n\tLoginURL            string\n\tProfileURL          string\n\tChangePwdURL        string\n\tMFAURL              string\n\tEventRulesURL       string\n\tEventRuleURL        string\n\tEventActionsURL     string\n\tEventActionURL      string\n\tRolesURL            string\n\tRoleURL             string\n\tFolderQuotaScanURL  string\n\tStatusURL           string\n\tMaintenanceURL      string\n\tCSRFToken           string\n\tIsEventManagerPage  bool\n\tIsIPManagerPage     bool\n\tIsServerManagerPage bool\n\tHasDefender         bool\n\tHasSearcher         bool\n\tHasExternalLogin    bool\n\tLoggedUser          *dataprovider.Admin\n\tIsLoggedToShare     bool\n\tBranding            UIBranding\n\tLanguages           []string\n}\n\ntype statusPage struct {\n\tbasePage\n\tStatus *ServicesStatus\n}\n\ntype fsWrapper struct {\n\tvfs.Filesystem\n\tIsUserPage      bool\n\tIsGroupPage     bool\n\tIsHidden        bool\n\tHasUsersBaseDir bool\n\tDirPath         string\n}\n\ntype userPage struct {\n\tbasePage\n\tUser               *dataprovider.User\n\tRootPerms          []string\n\tError              *util.I18nError\n\tValidPerms         []string\n\tValidLoginMethods  []string\n\tValidProtocols     []string\n\tTwoFactorProtocols []string\n\tWebClientOptions   []string\n\tRootDirPerms       []string\n\tMode               userPageMode\n\tVirtualFolders     []vfs.BaseVirtualFolder\n\tGroups             []dataprovider.Group\n\tRoles              []dataprovider.Role\n\tCanImpersonate     bool\n\tFsWrapper          fsWrapper\n\tCanUseTLSCerts     bool\n}\n\ntype adminPage struct {\n\tbasePage\n\tAdmin  *dataprovider.Admin\n\tGroups []dataprovider.Group\n\tRoles  []dataprovider.Role\n\tError  *util.I18nError\n\tIsAdd  bool\n}\n\ntype profilePage struct {\n\tbasePage\n\tError           *util.I18nError\n\tAllowAPIKeyAuth bool\n\tEmail           string\n\tDescription     string\n}\n\ntype changePasswordPage struct {\n\tbasePage\n\tError *util.I18nError\n}\n\ntype mfaPage struct {\n\tbasePage\n\tTOTPConfigs      []string\n\tTOTPConfig       dataprovider.AdminTOTPConfig\n\tGenerateTOTPURL  string\n\tValidateTOTPURL  string\n\tSaveTOTPURL      string\n\tRecCodesURL      string\n\tRequireTwoFactor bool\n}\n\ntype maintenancePage struct {\n\tbasePage\n\tBackupPath  string\n\tRestorePath string\n\tError       *util.I18nError\n}\n\ntype defenderHostsPage struct {\n\tbasePage\n\tDefenderHostsURL string\n}\n\ntype ipListsPage struct {\n\tbasePage\n\tIPListsSearchURL      string\n\tRateLimitersStatus    bool\n\tRateLimitersProtocols string\n\tIsAllowListEnabled    bool\n}\n\ntype ipListPage struct {\n\tbasePage\n\tEntry *dataprovider.IPListEntry\n\tError *util.I18nError\n\tMode  genericPageMode\n}\n\ntype setupPage struct {\n\tcommonBasePage\n\tCurrentURL           string\n\tError                *util.I18nError\n\tCSRFToken            string\n\tUsername             string\n\tHasInstallationCode  bool\n\tInstallationCodeHint string\n\tHideSupportLink      bool\n\tTitle                string\n\tBranding             UIBranding\n\tLanguages            []string\n\tCheckRedirect        bool\n}\n\ntype folderPage struct {\n\tbasePage\n\tFolder    vfs.BaseVirtualFolder\n\tError     *util.I18nError\n\tMode      folderPageMode\n\tFsWrapper fsWrapper\n}\n\ntype groupPage struct {\n\tbasePage\n\tGroup              *dataprovider.Group\n\tError              *util.I18nError\n\tMode               genericPageMode\n\tValidPerms         []string\n\tValidLoginMethods  []string\n\tValidProtocols     []string\n\tTwoFactorProtocols []string\n\tWebClientOptions   []string\n\tVirtualFolders     []vfs.BaseVirtualFolder\n\tFsWrapper          fsWrapper\n}\n\ntype rolePage struct {\n\tbasePage\n\tRole  *dataprovider.Role\n\tError *util.I18nError\n\tMode  genericPageMode\n}\n\ntype eventActionPage struct {\n\tbasePage\n\tAction          dataprovider.BaseEventAction\n\tActionTypes     []dataprovider.EnumMapping\n\tFsActions       []dataprovider.EnumMapping\n\tHTTPMethods     []string\n\tEnabledCommands []string\n\tRedactedSecret  string\n\tError           *util.I18nError\n\tMode            genericPageMode\n}\n\ntype eventRulePage struct {\n\tbasePage\n\tRule            dataprovider.EventRule\n\tTriggerTypes    []dataprovider.EnumMapping\n\tActions         []dataprovider.BaseEventAction\n\tFsEvents        []string\n\tProtocols       []string\n\tProviderEvents  []string\n\tProviderObjects []string\n\tError           *util.I18nError\n\tMode            genericPageMode\n\tIsShared        bool\n}\n\ntype eventsPage struct {\n\tbasePage\n\tFsEventsSearchURL       string\n\tProviderEventsSearchURL string\n\tLogEventsSearchURL      string\n}\n\ntype configsPage struct {\n\tbasePage\n\tConfigs           dataprovider.Configs\n\tConfigSection     int\n\tRedactedSecret    string\n\tOAuth2TokenURL    string\n\tOAuth2RedirectURL string\n\tWebClientBranding UIBranding\n\tError             *util.I18nError\n}\n\ntype messagePage struct {\n\tbasePage\n\tError   *util.I18nError\n\tSuccess string\n\tText    string\n}\n\ntype userTemplateFields struct {\n\tUsername         string\n\tPassword         string\n\tPublicKeys       []string\n\tRequirePwdChange bool\n}\n\nfunc loadAdminTemplates(templatesPath string) {\n\tusersPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateUsers),\n\t}\n\tuserPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateFsConfig),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateUser),\n\t}\n\tadminsPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateAdmins),\n\t}\n\tadminPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateAdmin),\n\t}\n\tprofilePaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateProfile),\n\t}\n\tchangePwdPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateChangePwd),\n\t}\n\tconnectionsPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateConnections),\n\t}\n\tmessagePaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateMessage),\n\t}\n\tfoldersPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateFolders),\n\t}\n\tfolderPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateFsConfig),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateFolder),\n\t}\n\tgroupsPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateGroups),\n\t}\n\tgroupPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateFsConfig),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateGroup),\n\t}\n\teventRulesPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateEventRules),\n\t}\n\teventRulePaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateEventRule),\n\t}\n\teventActionsPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateEventActions),\n\t}\n\teventActionPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateEventAction),\n\t}\n\tstatusPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateStatus),\n\t}\n\tloginPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonLogin),\n\t}\n\tmaintenancePaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateMaintenance),\n\t}\n\tdefenderPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateDefender),\n\t}\n\tipListsPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateIPLists),\n\t}\n\tipListPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateIPList),\n\t}\n\tmfaPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateMFA),\n\t}\n\ttwoFactorPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateTwoFactor),\n\t}\n\ttwoFactorRecoveryPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateTwoFactorRecovery),\n\t}\n\tsetupPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateSetup),\n\t}\n\tforgotPwdPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateForgotPassword),\n\t}\n\tresetPwdPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateResetPassword),\n\t}\n\trolesPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateRoles),\n\t}\n\trolePaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateRole),\n\t}\n\teventsPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateEvents),\n\t}\n\tconfigsPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateBase),\n\t\tfilepath.Join(templatesPath, templateAdminDir, templateConfigs),\n\t}\n\n\tfsBaseTpl := template.New(\"fsBaseTemplate\").Funcs(template.FuncMap{\n\t\t\"HumanizeBytes\": util.ByteCountSI,\n\t})\n\tusersTmpl := util.LoadTemplate(nil, usersPaths...)\n\tuserTmpl := util.LoadTemplate(fsBaseTpl, userPaths...)\n\tadminsTmpl := util.LoadTemplate(nil, adminsPaths...)\n\tadminTmpl := util.LoadTemplate(nil, adminPaths...)\n\tconnectionsTmpl := util.LoadTemplate(nil, connectionsPaths...)\n\tmessageTmpl := util.LoadTemplate(nil, messagePaths...)\n\tgroupsTmpl := util.LoadTemplate(nil, groupsPaths...)\n\tgroupTmpl := util.LoadTemplate(fsBaseTpl, groupPaths...)\n\tfoldersTmpl := util.LoadTemplate(nil, foldersPaths...)\n\tfolderTmpl := util.LoadTemplate(fsBaseTpl, folderPaths...)\n\teventRulesTmpl := util.LoadTemplate(nil, eventRulesPaths...)\n\teventRuleTmpl := util.LoadTemplate(fsBaseTpl, eventRulePaths...)\n\teventActionsTmpl := util.LoadTemplate(nil, eventActionsPaths...)\n\teventActionTmpl := util.LoadTemplate(nil, eventActionPaths...)\n\tstatusTmpl := util.LoadTemplate(nil, statusPaths...)\n\tloginTmpl := util.LoadTemplate(nil, loginPaths...)\n\tprofileTmpl := util.LoadTemplate(nil, profilePaths...)\n\tchangePwdTmpl := util.LoadTemplate(nil, changePwdPaths...)\n\tmaintenanceTmpl := util.LoadTemplate(nil, maintenancePaths...)\n\tdefenderTmpl := util.LoadTemplate(nil, defenderPaths...)\n\tipListsTmpl := util.LoadTemplate(nil, ipListsPaths...)\n\tipListTmpl := util.LoadTemplate(nil, ipListPaths...)\n\tmfaTmpl := util.LoadTemplate(nil, mfaPaths...)\n\ttwoFactorTmpl := util.LoadTemplate(nil, twoFactorPaths...)\n\ttwoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPaths...)\n\tsetupTmpl := util.LoadTemplate(nil, setupPaths...)\n\tforgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)\n\tresetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)\n\trolesTmpl := util.LoadTemplate(nil, rolesPaths...)\n\troleTmpl := util.LoadTemplate(nil, rolePaths...)\n\teventsTmpl := util.LoadTemplate(nil, eventsPaths...)\n\tconfigsTmpl := util.LoadTemplate(nil, configsPaths...)\n\n\tadminTemplates[templateUsers] = usersTmpl\n\tadminTemplates[templateUser] = userTmpl\n\tadminTemplates[templateAdmins] = adminsTmpl\n\tadminTemplates[templateAdmin] = adminTmpl\n\tadminTemplates[templateConnections] = connectionsTmpl\n\tadminTemplates[templateMessage] = messageTmpl\n\tadminTemplates[templateGroups] = groupsTmpl\n\tadminTemplates[templateGroup] = groupTmpl\n\tadminTemplates[templateFolders] = foldersTmpl\n\tadminTemplates[templateFolder] = folderTmpl\n\tadminTemplates[templateEventRules] = eventRulesTmpl\n\tadminTemplates[templateEventRule] = eventRuleTmpl\n\tadminTemplates[templateEventActions] = eventActionsTmpl\n\tadminTemplates[templateEventAction] = eventActionTmpl\n\tadminTemplates[templateStatus] = statusTmpl\n\tadminTemplates[templateCommonLogin] = loginTmpl\n\tadminTemplates[templateProfile] = profileTmpl\n\tadminTemplates[templateChangePwd] = changePwdTmpl\n\tadminTemplates[templateMaintenance] = maintenanceTmpl\n\tadminTemplates[templateDefender] = defenderTmpl\n\tadminTemplates[templateIPLists] = ipListsTmpl\n\tadminTemplates[templateIPList] = ipListTmpl\n\tadminTemplates[templateMFA] = mfaTmpl\n\tadminTemplates[templateTwoFactor] = twoFactorTmpl\n\tadminTemplates[templateTwoFactorRecovery] = twoFactorRecoveryTmpl\n\tadminTemplates[templateSetup] = setupTmpl\n\tadminTemplates[templateForgotPassword] = forgotPwdTmpl\n\tadminTemplates[templateResetPassword] = resetPwdTmpl\n\tadminTemplates[templateRoles] = rolesTmpl\n\tadminTemplates[templateRole] = roleTmpl\n\tadminTemplates[templateEvents] = eventsTmpl\n\tadminTemplates[templateConfigs] = configsTmpl\n}\n\nfunc isEventManagerResource(currentURL string) bool {\n\tif currentURL == webAdminEventRulesPath {\n\t\treturn true\n\t}\n\tif currentURL == webAdminEventActionsPath {\n\t\treturn true\n\t}\n\tif currentURL == webAdminEventRulePath || strings.HasPrefix(currentURL, webAdminEventRulePath+\"/\") {\n\t\treturn true\n\t}\n\tif currentURL == webAdminEventActionPath || strings.HasPrefix(currentURL, webAdminEventActionPath+\"/\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc isIPListsResource(currentURL string) bool {\n\tif currentURL == webDefenderPath {\n\t\treturn true\n\t}\n\tif currentURL == webIPListsPath {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(currentURL, webIPListPath+\"/\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc isServerManagerResource(currentURL string) bool {\n\treturn currentURL == webEventsPath || currentURL == webStatusPath || currentURL == webMaintenancePath ||\n\t\tcurrentURL == webConfigsPath\n}\n\nfunc (s *httpdServer) getBasePageData(title, currentURL string, w http.ResponseWriter, r *http.Request) basePage {\n\tvar csrfToken string\n\tif currentURL != \"\" {\n\t\tcsrfToken = createCSRFToken(w, r, s.csrfTokenAuth, \"\", webBaseAdminPath)\n\t}\n\treturn basePage{\n\t\tcommonBasePage:      getCommonBasePage(r),\n\t\tTitle:               title,\n\t\tCurrentURL:          currentURL,\n\t\tUsersURL:            webUsersPath,\n\t\tUserURL:             webUserPath,\n\t\tUserTemplateURL:     webTemplateUser,\n\t\tAdminsURL:           webAdminsPath,\n\t\tAdminURL:            webAdminPath,\n\t\tGroupsURL:           webGroupsPath,\n\t\tGroupURL:            webGroupPath,\n\t\tFoldersURL:          webFoldersPath,\n\t\tFolderURL:           webFolderPath,\n\t\tFolderTemplateURL:   webTemplateFolder,\n\t\tDefenderURL:         webDefenderPath,\n\t\tIPListsURL:          webIPListsPath,\n\t\tIPListURL:           webIPListPath,\n\t\tEventsURL:           webEventsPath,\n\t\tConfigsURL:          webConfigsPath,\n\t\tLogoutURL:           webLogoutPath,\n\t\tLoginURL:            webAdminLoginPath,\n\t\tProfileURL:          webAdminProfilePath,\n\t\tChangePwdURL:        webChangeAdminPwdPath,\n\t\tMFAURL:              webAdminMFAPath,\n\t\tEventRulesURL:       webAdminEventRulesPath,\n\t\tEventRuleURL:        webAdminEventRulePath,\n\t\tEventActionsURL:     webAdminEventActionsPath,\n\t\tEventActionURL:      webAdminEventActionPath,\n\t\tRolesURL:            webAdminRolesPath,\n\t\tRoleURL:             webAdminRolePath,\n\t\tQuotaScanURL:        webQuotaScanPath,\n\t\tConnectionsURL:      webConnectionsPath,\n\t\tStatusURL:           webStatusPath,\n\t\tFolderQuotaScanURL:  webScanVFolderPath,\n\t\tMaintenanceURL:      webMaintenancePath,\n\t\tLoggedUser:          getAdminFromToken(r),\n\t\tIsEventManagerPage:  isEventManagerResource(currentURL),\n\t\tIsIPManagerPage:     isIPListsResource(currentURL),\n\t\tIsServerManagerPage: isServerManagerResource(currentURL),\n\t\tHasDefender:         common.Config.DefenderConfig.Enabled,\n\t\tHasSearcher:         plugin.Handler.HasSearcher(),\n\t\tHasExternalLogin:    isLoggedInWithOIDC(r),\n\t\tCSRFToken:           csrfToken,\n\t\tBranding:            s.binding.webAdminBranding(),\n\t\tLanguages:           s.binding.languages(),\n\t}\n}\n\nfunc renderAdminTemplate(w http.ResponseWriter, tmplName string, data any) {\n\terr := adminTemplates[tmplName].ExecuteTemplate(w, tmplName, data)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n\nfunc (s *httpdServer) renderMessagePageWithString(w http.ResponseWriter, r *http.Request, title string, statusCode int,\n\terr error, message, text string,\n) {\n\tdata := messagePage{\n\t\tbasePage: s.getBasePageData(title, \"\", w, r),\n\t\tError:    getI18nError(err),\n\t\tSuccess:  message,\n\t\tText:     text,\n\t}\n\tw.WriteHeader(statusCode)\n\trenderAdminTemplate(w, templateMessage, data)\n}\n\nfunc (s *httpdServer) renderMessagePage(w http.ResponseWriter, r *http.Request, title string, statusCode int,\n\terr error, message string,\n) {\n\ts.renderMessagePageWithString(w, r, title, statusCode, err, message, \"\")\n}\n\nfunc (s *httpdServer) renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {\n\ts.renderMessagePage(w, r, util.I18nError500Title, http.StatusInternalServerError,\n\t\tutil.NewI18nError(err, util.I18nError500Message), \"\")\n}\n\nfunc (s *httpdServer) renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {\n\ts.renderMessagePage(w, r, util.I18nError400Title, http.StatusBadRequest,\n\t\tutil.NewI18nError(err, util.I18nError400Message), \"\")\n}\n\nfunc (s *httpdServer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, err error) {\n\ts.renderMessagePage(w, r, util.I18nError403Title, http.StatusForbidden,\n\t\tutil.NewI18nError(err, util.I18nError403Message), \"\")\n}\n\nfunc (s *httpdServer) renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {\n\ts.renderMessagePage(w, r, util.I18nError404Title, http.StatusNotFound,\n\t\tutil.NewI18nError(err, util.I18nError404Message), \"\")\n}\n\nfunc (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := forgotPwdPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tCurrentURL:     webAdminForgotPwdPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, rand.Text(), webBaseAdminPath),\n\t\tLoginURL:       webAdminLoginPath,\n\t\tTitle:          util.I18nForgotPwdTitle,\n\t\tBranding:       s.binding.webAdminBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\trenderAdminTemplate(w, templateForgotPassword, data)\n}\n\nfunc (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := resetPwdPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tCurrentURL:     webAdminResetPwdPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, \"\", webBaseAdminPath),\n\t\tLoginURL:       webAdminLoginPath,\n\t\tTitle:          util.I18nResetPwdTitle,\n\t\tBranding:       s.binding.webAdminBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\trenderAdminTemplate(w, templateResetPassword, data)\n}\n\nfunc (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := twoFactorPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          util.I18n2FATitle,\n\t\tCurrentURL:     webAdminTwoFactorPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, \"\", webBaseAdminPath),\n\t\tRecoveryURL:    webAdminTwoFactorRecoveryPath,\n\t\tBranding:       s.binding.webAdminBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\trenderAdminTemplate(w, templateTwoFactor, data)\n}\n\nfunc (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := twoFactorPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          util.I18n2FATitle,\n\t\tCurrentURL:     webAdminTwoFactorRecoveryPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, \"\", webBaseAdminPath),\n\t\tBranding:       s.binding.webAdminBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\trenderAdminTemplate(w, templateTwoFactorRecovery, data)\n}\n\nfunc (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) {\n\tdata := mfaPage{\n\t\tbasePage:        s.getBasePageData(util.I18n2FATitle, webAdminMFAPath, w, r),\n\t\tTOTPConfigs:     mfa.GetAvailableTOTPConfigNames(),\n\t\tGenerateTOTPURL: webAdminTOTPGeneratePath,\n\t\tValidateTOTPURL: webAdminTOTPValidatePath,\n\t\tSaveTOTPURL:     webAdminTOTPSavePath,\n\t\tRecCodesURL:     webAdminRecoveryCodesPath,\n\t}\n\tadmin, err := dataprovider.AdminExists(data.LoggedUser.Username)\n\tif err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tdata.TOTPConfig = admin.Filters.TOTPConfig\n\tdata.RequireTwoFactor = admin.Filters.RequireTwoFactor\n\trenderAdminTemplate(w, templateMFA, data)\n}\n\nfunc (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request, err error) {\n\tdata := profilePage{\n\t\tbasePage: s.getBasePageData(util.I18nProfileTitle, webAdminProfilePath, w, r),\n\t\tError:    getI18nError(err),\n\t}\n\tadmin, err := dataprovider.AdminExists(data.LoggedUser.Username)\n\tif err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tdata.AllowAPIKeyAuth = admin.Filters.AllowAPIKeyAuth\n\tdata.Email = admin.Email\n\tdata.Description = admin.Description\n\n\trenderAdminTemplate(w, templateProfile, data)\n}\n\nfunc (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := changePasswordPage{\n\t\tbasePage: s.getBasePageData(util.I18nChangePwdTitle, webChangeAdminPwdPath, w, r),\n\t\tError:    err,\n\t}\n\n\trenderAdminTemplate(w, templateChangePwd, data)\n}\n\nfunc (s *httpdServer) renderMaintenancePage(w http.ResponseWriter, r *http.Request, err error) {\n\tdata := maintenancePage{\n\t\tbasePage:    s.getBasePageData(util.I18nMaintenanceTitle, webMaintenancePath, w, r),\n\t\tBackupPath:  webBackupPath,\n\t\tRestorePath: webRestorePath,\n\t\tError:       getI18nError(err),\n\t}\n\n\trenderAdminTemplate(w, templateMaintenance, data)\n}\n\nfunc (s *httpdServer) renderConfigsPage(w http.ResponseWriter, r *http.Request, configs dataprovider.Configs,\n\terr error, section int,\n) {\n\tconfigs.SetNilsToEmpty()\n\tif configs.SMTP.Port == 0 {\n\t\tconfigs.SMTP.Port = 587\n\t\tconfigs.SMTP.AuthType = 1\n\t\tconfigs.SMTP.Encryption = 2\n\t}\n\tif configs.ACME.HTTP01Challenge.Port == 0 {\n\t\tconfigs.ACME.HTTP01Challenge.Port = 80\n\t}\n\tdata := configsPage{\n\t\tbasePage:          s.getBasePageData(util.I18nConfigsTitle, webConfigsPath, w, r),\n\t\tConfigs:           configs,\n\t\tConfigSection:     section,\n\t\tRedactedSecret:    redactedSecret,\n\t\tOAuth2TokenURL:    webOAuth2TokenPath,\n\t\tOAuth2RedirectURL: webOAuth2RedirectPath,\n\t\tWebClientBranding: s.binding.webClientBranding(),\n\t\tError:             getI18nError(err),\n\t}\n\n\trenderAdminTemplate(w, templateConfigs, data)\n}\n\nfunc (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username string, err *util.I18nError) {\n\tdata := setupPage{\n\t\tcommonBasePage:       getCommonBasePage(r),\n\t\tTitle:                util.I18nSetupTitle,\n\t\tCurrentURL:           webAdminSetupPath,\n\t\tCSRFToken:            createCSRFToken(w, r, s.csrfTokenAuth, rand.Text(), webBaseAdminPath),\n\t\tUsername:             username,\n\t\tHasInstallationCode:  installationCode != \"\",\n\t\tInstallationCodeHint: installationCodeHint,\n\t\tHideSupportLink:      hideSupportLink,\n\t\tError:                err,\n\t\tBranding:             s.binding.webAdminBranding(),\n\t\tLanguages:            s.binding.languages(),\n\t}\n\n\trenderAdminTemplate(w, templateSetup, data)\n}\n\nfunc (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,\n\terr error, isAdd bool) {\n\tgroups, errGroups := s.getWebGroups(w, r, defaultQueryLimit, true)\n\tif errGroups != nil {\n\t\treturn\n\t}\n\troles, errRoles := s.getWebRoles(w, r, 10, true)\n\tif errRoles != nil {\n\t\treturn\n\t}\n\tcurrentURL := webAdminPath\n\ttitle := util.I18nAddAdminTitle\n\tif !isAdd {\n\t\tcurrentURL = fmt.Sprintf(\"%v/%v\", webAdminPath, url.PathEscape(admin.Username))\n\t\ttitle = util.I18nUpdateAdminTitle\n\t}\n\tdata := adminPage{\n\t\tbasePage: s.getBasePageData(title, currentURL, w, r),\n\t\tAdmin:    admin,\n\t\tGroups:   groups,\n\t\tRoles:    roles,\n\t\tError:    getI18nError(err),\n\t\tIsAdd:    isAdd,\n\t}\n\n\trenderAdminTemplate(w, templateAdmin, data)\n}\n\nfunc (s *httpdServer) getUserPageTitleAndURL(mode userPageMode, username string) (string, string) {\n\tvar title, currentURL string\n\tswitch mode {\n\tcase userPageModeAdd:\n\t\ttitle = util.I18nAddUserTitle\n\t\tcurrentURL = webUserPath\n\tcase userPageModeUpdate:\n\t\ttitle = util.I18nUpdateUserTitle\n\t\tcurrentURL = fmt.Sprintf(\"%v/%v\", webUserPath, url.PathEscape(username))\n\tcase userPageModeTemplate:\n\t\ttitle = util.I18nTemplateUserTitle\n\t\tcurrentURL = webTemplateUser\n\t}\n\treturn title, currentURL\n}\n\nfunc (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User,\n\tmode userPageMode, err error, admin *dataprovider.Admin,\n) {\n\tuser.SetEmptySecretsIfNil()\n\ttitle, currentURL := s.getUserPageTitleAndURL(mode, user.Username)\n\tif user.Password != \"\" && user.IsPasswordHashed() {\n\t\tswitch mode {\n\t\tcase userPageModeUpdate:\n\t\t\tuser.Password = redactedSecret\n\t\tdefault:\n\t\t\tuser.Password = \"\"\n\t\t}\n\t}\n\tuser.FsConfig.RedactedSecret = redactedSecret\n\tbasePage := s.getBasePageData(title, currentURL, w, r)\n\tif (mode == userPageModeAdd || mode == userPageModeTemplate) && len(user.Groups) == 0 && admin != nil {\n\t\tfor _, group := range admin.Groups {\n\t\t\tuser.Groups = append(user.Groups, sdk.GroupMapping{\n\t\t\t\tName: group.Name,\n\t\t\t\tType: group.Options.GetUserGroupType(),\n\t\t\t})\n\t\t}\n\t}\n\tvar roles []dataprovider.Role\n\tif basePage.LoggedUser.Role == \"\" {\n\t\tvar errRoles error\n\t\troles, errRoles = s.getWebRoles(w, r, 10, true)\n\t\tif errRoles != nil {\n\t\t\treturn\n\t\t}\n\t}\n\tfolders, errFolders := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)\n\tif errFolders != nil {\n\t\treturn\n\t}\n\tgroups, errGroups := s.getWebGroups(w, r, defaultQueryLimit, true)\n\tif errGroups != nil {\n\t\treturn\n\t}\n\tdata := userPage{\n\t\tbasePage:           basePage,\n\t\tMode:               mode,\n\t\tError:              getI18nError(err),\n\t\tUser:               user,\n\t\tValidPerms:         dataprovider.ValidPerms,\n\t\tValidLoginMethods:  dataprovider.ValidLoginMethods,\n\t\tValidProtocols:     dataprovider.ValidProtocols,\n\t\tTwoFactorProtocols: dataprovider.MFAProtocols,\n\t\tWebClientOptions:   sdk.WebClientOptions,\n\t\tRootDirPerms:       user.GetPermissionsForPath(\"/\"),\n\t\tVirtualFolders:     folders,\n\t\tGroups:             groups,\n\t\tRoles:              roles,\n\t\tCanImpersonate:     os.Getuid() == 0,\n\t\tCanUseTLSCerts:     ftpd.GetStatus().IsActive || webdavd.GetStatus().IsActive,\n\t\tFsWrapper: fsWrapper{\n\t\t\tFilesystem:      user.FsConfig,\n\t\t\tIsUserPage:      true,\n\t\t\tIsGroupPage:     false,\n\t\t\tIsHidden:        basePage.LoggedUser.Filters.Preferences.HideFilesystem(),\n\t\t\tHasUsersBaseDir: dataprovider.HasUsersBaseDir(),\n\t\t\tDirPath:         user.HomeDir,\n\t\t},\n\t}\n\trenderAdminTemplate(w, templateUser, data)\n}\n\nfunc (s *httpdServer) renderIPListPage(w http.ResponseWriter, r *http.Request, entry dataprovider.IPListEntry,\n\tmode genericPageMode, err error,\n) {\n\tvar title, currentURL string\n\tswitch mode {\n\tcase genericPageModeAdd:\n\t\ttitle = util.I18nAddIPListTitle\n\t\tcurrentURL = fmt.Sprintf(\"%s/%d\", webIPListPath, entry.Type)\n\tcase genericPageModeUpdate:\n\t\ttitle = util.I18nUpdateIPListTitle\n\t\tcurrentURL = fmt.Sprintf(\"%s/%d/%s\", webIPListPath, entry.Type, url.PathEscape(entry.IPOrNet))\n\t}\n\tdata := ipListPage{\n\t\tbasePage: s.getBasePageData(title, currentURL, w, r),\n\t\tError:    getI18nError(err),\n\t\tEntry:    &entry,\n\t\tMode:     mode,\n\t}\n\trenderAdminTemplate(w, templateIPList, data)\n}\n\nfunc (s *httpdServer) renderRolePage(w http.ResponseWriter, r *http.Request, role dataprovider.Role,\n\tmode genericPageMode, err error,\n) {\n\tvar title, currentURL string\n\tswitch mode {\n\tcase genericPageModeAdd:\n\t\ttitle = util.I18nRoleAddTitle\n\t\tcurrentURL = webAdminRolePath\n\tcase genericPageModeUpdate:\n\t\ttitle = util.I18nRoleUpdateTitle\n\t\tcurrentURL = fmt.Sprintf(\"%s/%s\", webAdminRolePath, url.PathEscape(role.Name))\n\t}\n\tdata := rolePage{\n\t\tbasePage: s.getBasePageData(title, currentURL, w, r),\n\t\tError:    getI18nError(err),\n\t\tRole:     &role,\n\t\tMode:     mode,\n\t}\n\trenderAdminTemplate(w, templateRole, data)\n}\n\nfunc (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group,\n\tmode genericPageMode, err error,\n) {\n\tfolders, errFolders := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)\n\tif errFolders != nil {\n\t\treturn\n\t}\n\tgroup.SetEmptySecretsIfNil()\n\tgroup.UserSettings.FsConfig.RedactedSecret = redactedSecret\n\tvar title, currentURL string\n\tswitch mode {\n\tcase genericPageModeAdd:\n\t\ttitle = util.I18nAddGroupTitle\n\t\tcurrentURL = webGroupPath\n\tcase genericPageModeUpdate:\n\t\ttitle = util.I18nUpdateGroupTitle\n\t\tcurrentURL = fmt.Sprintf(\"%v/%v\", webGroupPath, url.PathEscape(group.Name))\n\t}\n\tgroup.UserSettings.FsConfig.RedactedSecret = redactedSecret\n\tgroup.UserSettings.FsConfig.SetEmptySecretsIfNil()\n\n\tdata := groupPage{\n\t\tbasePage:           s.getBasePageData(title, currentURL, w, r),\n\t\tError:              getI18nError(err),\n\t\tGroup:              &group,\n\t\tMode:               mode,\n\t\tValidPerms:         dataprovider.ValidPerms,\n\t\tValidLoginMethods:  dataprovider.ValidLoginMethods,\n\t\tValidProtocols:     dataprovider.ValidProtocols,\n\t\tTwoFactorProtocols: dataprovider.MFAProtocols,\n\t\tWebClientOptions:   sdk.WebClientOptions,\n\t\tVirtualFolders:     folders,\n\t\tFsWrapper: fsWrapper{\n\t\t\tFilesystem:      group.UserSettings.FsConfig,\n\t\t\tIsUserPage:      false,\n\t\t\tIsGroupPage:     true,\n\t\t\tHasUsersBaseDir: false,\n\t\t\tDirPath:         group.UserSettings.HomeDir,\n\t\t},\n\t}\n\trenderAdminTemplate(w, templateGroup, data)\n}\n\nfunc (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Request, action dataprovider.BaseEventAction,\n\tmode genericPageMode, err error,\n) {\n\taction.Options.SetEmptySecretsIfNil()\n\tvar title, currentURL string\n\tswitch mode {\n\tcase genericPageModeAdd:\n\t\ttitle = util.I18nAddActionTitle\n\t\tcurrentURL = webAdminEventActionPath\n\tcase genericPageModeUpdate:\n\t\ttitle = util.I18nUpdateActionTitle\n\t\tcurrentURL = fmt.Sprintf(\"%s/%s\", webAdminEventActionPath, url.PathEscape(action.Name))\n\t}\n\tif action.Options.HTTPConfig.Timeout == 0 {\n\t\taction.Options.HTTPConfig.Timeout = 20\n\t}\n\tif action.Options.CmdConfig.Timeout == 0 {\n\t\taction.Options.CmdConfig.Timeout = 20\n\t}\n\tif action.Options.PwdExpirationConfig.Threshold == 0 {\n\t\taction.Options.PwdExpirationConfig.Threshold = 10\n\t}\n\n\tdata := eventActionPage{\n\t\tbasePage:        s.getBasePageData(title, currentURL, w, r),\n\t\tAction:          action,\n\t\tActionTypes:     dataprovider.EventActionTypes,\n\t\tFsActions:       dataprovider.FsActionTypes,\n\t\tHTTPMethods:     dataprovider.SupportedHTTPActionMethods,\n\t\tEnabledCommands: dataprovider.EnabledActionCommands,\n\t\tRedactedSecret:  redactedSecret,\n\t\tError:           getI18nError(err),\n\t\tMode:            mode,\n\t}\n\trenderAdminTemplate(w, templateEventAction, data)\n}\n\nfunc (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request, rule dataprovider.EventRule,\n\tmode genericPageMode, err error,\n) {\n\tactions, errActions := s.getWebEventActions(w, r, defaultQueryLimit, true)\n\tif errActions != nil {\n\t\treturn\n\t}\n\tvar title, currentURL string\n\tswitch mode {\n\tcase genericPageModeAdd:\n\t\ttitle = util.I18nAddRuleTitle\n\t\tcurrentURL = webAdminEventRulePath\n\tcase genericPageModeUpdate:\n\t\ttitle = util.I18nUpdateRuleTitle\n\t\tcurrentURL = fmt.Sprintf(\"%v/%v\", webAdminEventRulePath, url.PathEscape(rule.Name))\n\t}\n\n\tdata := eventRulePage{\n\t\tbasePage:        s.getBasePageData(title, currentURL, w, r),\n\t\tRule:            rule,\n\t\tTriggerTypes:    dataprovider.EventTriggerTypes,\n\t\tActions:         actions,\n\t\tFsEvents:        dataprovider.SupportedFsEvents,\n\t\tProtocols:       dataprovider.SupportedRuleConditionProtocols,\n\t\tProviderEvents:  dataprovider.SupportedProviderEvents,\n\t\tProviderObjects: dataprovider.SupporteRuleConditionProviderObjects,\n\t\tError:           getI18nError(err),\n\t\tMode:            mode,\n\t\tIsShared:        s.isShared > 0,\n\t}\n\trenderAdminTemplate(w, templateEventRule, data)\n}\n\nfunc (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder,\n\tmode folderPageMode, err error,\n) {\n\tvar title, currentURL string\n\tswitch mode {\n\tcase folderPageModeAdd:\n\t\ttitle = util.I18nAddFolderTitle\n\t\tcurrentURL = webFolderPath\n\tcase folderPageModeUpdate:\n\t\ttitle = util.I18nUpdateFolderTitle\n\t\tcurrentURL = fmt.Sprintf(\"%v/%v\", webFolderPath, url.PathEscape(folder.Name))\n\tcase folderPageModeTemplate:\n\t\ttitle = util.I18nTemplateFolderTitle\n\t\tcurrentURL = webTemplateFolder\n\t}\n\tfolder.FsConfig.RedactedSecret = redactedSecret\n\tfolder.FsConfig.SetEmptySecretsIfNil()\n\n\tdata := folderPage{\n\t\tbasePage: s.getBasePageData(title, currentURL, w, r),\n\t\tError:    getI18nError(err),\n\t\tFolder:   folder,\n\t\tMode:     mode,\n\t\tFsWrapper: fsWrapper{\n\t\t\tFilesystem:      folder.FsConfig,\n\t\t\tIsUserPage:      false,\n\t\t\tIsGroupPage:     false,\n\t\t\tHasUsersBaseDir: false,\n\t\t\tDirPath:         folder.MappedPath,\n\t\t},\n\t}\n\trenderAdminTemplate(w, templateFolder, data)\n}\n\nfunc getFoldersForTemplate(r *http.Request) []string {\n\tvar res []string\n\tfor k := range r.Form {\n\t\tif hasPrefixAndSuffix(k, \"template_folders[\", \"][tpl_foldername]\") {\n\t\t\tr.Form.Add(\"tpl_foldername\", r.Form.Get(k))\n\t\t}\n\t}\n\tfolderNames := r.Form[\"tpl_foldername\"]\n\tfolders := make(map[string]bool)\n\tfor _, name := range folderNames {\n\t\tname = strings.TrimSpace(name)\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := folders[name]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tfolders[name] = true\n\t\tres = append(res, name)\n\t}\n\treturn res\n}\n\nfunc getUsersForTemplate(r *http.Request) []userTemplateFields {\n\tvar res []userTemplateFields\n\ttplUsernames := r.Form[\"tpl_username\"]\n\ttplPasswords := r.Form[\"tpl_password\"]\n\ttplPublicKeys := r.Form[\"tpl_public_keys\"]\n\n\tusers := make(map[string]bool)\n\tfor idx := range tplUsernames {\n\t\tusername := tplUsernames[idx]\n\t\tpassword := \"\"\n\t\tpublicKey := \"\"\n\t\tif len(tplPasswords) > idx {\n\t\t\tpassword = strings.TrimSpace(tplPasswords[idx])\n\t\t}\n\t\tif len(tplPublicKeys) > idx {\n\t\t\tpublicKey = strings.TrimSpace(tplPublicKeys[idx])\n\t\t}\n\t\tif username == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := users[username]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tusers[username] = true\n\t\tres = append(res, userTemplateFields{\n\t\t\tUsername:         username,\n\t\t\tPassword:         password,\n\t\t\tPublicKeys:       []string{publicKey},\n\t\t\tRequirePwdChange: r.Form.Get(\"tpl_require_password_change\") != \"\",\n\t\t})\n\t}\n\n\treturn res\n}\n\nfunc getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {\n\tvar virtualFolders []vfs.VirtualFolder\n\tfolderPaths := r.Form[\"vfolder_path\"]\n\tfolderNames := r.Form[\"vfolder_name\"]\n\tfolderQuotaSizes := r.Form[\"vfolder_quota_size\"]\n\tfolderQuotaFiles := r.Form[\"vfolder_quota_files\"]\n\tfor idx, p := range folderPaths {\n\t\tname := \"\"\n\t\tif len(folderNames) > idx {\n\t\t\tname = folderNames[idx]\n\t\t}\n\t\tif p != \"\" && name != \"\" {\n\t\t\tvfolder := vfs.VirtualFolder{\n\t\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\t\tName: name,\n\t\t\t\t},\n\t\t\t\tVirtualPath: p,\n\t\t\t\tQuotaFiles:  -1,\n\t\t\t\tQuotaSize:   -1,\n\t\t\t}\n\t\t\tif len(folderQuotaSizes) > idx {\n\t\t\t\tquotaSize, err := util.ParseBytes(folderQuotaSizes[idx])\n\t\t\t\tif err == nil {\n\t\t\t\t\tvfolder.QuotaSize = quotaSize\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(folderQuotaFiles) > idx {\n\t\t\t\tquotaFiles, err := strconv.Atoi(folderQuotaFiles[idx])\n\t\t\t\tif err == nil {\n\t\t\t\t\tvfolder.QuotaFiles = quotaFiles\n\t\t\t\t}\n\t\t\t}\n\t\t\tvirtualFolders = append(virtualFolders, vfolder)\n\t\t}\n\t}\n\n\treturn virtualFolders\n}\n\nfunc getSubDirPermissionsFromPostFields(r *http.Request) map[string][]string {\n\tpermissions := make(map[string][]string)\n\n\tfor idx, p := range r.Form[\"sub_perm_path\"] {\n\t\tif p != \"\" {\n\t\t\tpermissions[p] = r.Form[\"sub_perm_permissions\"+strconv.Itoa(idx)]\n\t\t}\n\t}\n\n\treturn permissions\n}\n\nfunc getUserPermissionsFromPostFields(r *http.Request) map[string][]string {\n\tpermissions := getSubDirPermissionsFromPostFields(r)\n\tpermissions[\"/\"] = r.Form[\"permissions\"]\n\n\treturn permissions\n}\n\nfunc getAccessTimeRestrictionsFromPostFields(r *http.Request) []sdk.TimePeriod {\n\tvar result []sdk.TimePeriod\n\n\tdayOfWeeks := r.Form[\"access_time_day_of_week\"]\n\tstarts := r.Form[\"access_time_start\"]\n\tends := r.Form[\"access_time_end\"]\n\n\tfor idx, dayOfWeek := range dayOfWeeks {\n\t\tdayOfWeek = strings.TrimSpace(dayOfWeek)\n\t\tstart := \"\"\n\t\tif len(starts) > idx {\n\t\t\tstart = strings.TrimSpace(starts[idx])\n\t\t}\n\t\tend := \"\"\n\t\tif len(ends) > idx {\n\t\t\tend = strings.TrimSpace(ends[idx])\n\t\t}\n\t\tdayNumber, err := strconv.Atoi(dayOfWeek)\n\t\tif err == nil && start != \"\" && end != \"\" {\n\t\t\tresult = append(result, sdk.TimePeriod{\n\t\t\t\tDayOfWeek: dayNumber,\n\t\t\t\tFrom:      start,\n\t\t\t\tTo:        end,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {\n\tvar result []sdk.BandwidthLimit\n\tbwSources := r.Form[\"bandwidth_limit_sources\"]\n\tuploadSources := r.Form[\"upload_bandwidth_source\"]\n\tdownloadSources := r.Form[\"download_bandwidth_source\"]\n\n\tfor idx, bwSource := range bwSources {\n\t\tsources := getSliceFromDelimitedValues(bwSource, \",\")\n\t\tif len(sources) > 0 {\n\t\t\tbwLimit := sdk.BandwidthLimit{\n\t\t\t\tSources: sources,\n\t\t\t}\n\t\t\tul := \"\"\n\t\t\tdl := \"\"\n\t\t\tif len(uploadSources) > idx {\n\t\t\t\tul = uploadSources[idx]\n\t\t\t}\n\t\t\tif len(downloadSources) > idx {\n\t\t\t\tdl = downloadSources[idx]\n\t\t\t}\n\t\t\tif ul != \"\" {\n\t\t\t\tbandwidthUL, err := strconv.ParseInt(ul, 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn result, fmt.Errorf(\"invalid upload_bandwidth_source%v %q: %w\", idx, ul, err)\n\t\t\t\t}\n\t\t\t\tbwLimit.UploadBandwidth = bandwidthUL\n\t\t\t}\n\t\t\tif dl != \"\" {\n\t\t\t\tbandwidthDL, err := strconv.ParseInt(dl, 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn result, fmt.Errorf(\"invalid download_bandwidth_source%v %q: %w\", idx, ul, err)\n\t\t\t\t}\n\t\t\t\tbwLimit.DownloadBandwidth = bandwidthDL\n\t\t\t}\n\t\t\tresult = append(result, bwLimit)\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\nfunc getPatterDenyPolicyFromString(policy string) int {\n\tdenyPolicy := sdk.DenyPolicyDefault\n\tif policy == \"1\" {\n\t\tdenyPolicy = sdk.DenyPolicyHide\n\t}\n\treturn denyPolicy\n}\n\nfunc getFilePatternsFromPostField(r *http.Request) []sdk.PatternsFilter {\n\tvar result []sdk.PatternsFilter\n\tpatternPaths := r.Form[\"pattern_path\"]\n\tpatterns := r.Form[\"patterns\"]\n\tpatternTypes := r.Form[\"pattern_type\"]\n\tpolicies := r.Form[\"pattern_policy\"]\n\n\tallowedPatterns := make(map[string][]string)\n\tdeniedPatterns := make(map[string][]string)\n\tpatternPolicies := make(map[string]string)\n\n\tfor idx := range patternPaths {\n\t\tp := patternPaths[idx]\n\t\tfilters := strings.ReplaceAll(patterns[idx], \" \", \"\")\n\t\tpatternType := patternTypes[idx]\n\t\tpatternPolicy := policies[idx]\n\t\tif p != \"\" && filters != \"\" {\n\t\t\tif patternType == \"allowed\" {\n\t\t\t\tallowedPatterns[p] = append(allowedPatterns[p], strings.Split(filters, \",\")...)\n\t\t\t} else {\n\t\t\t\tdeniedPatterns[p] = append(deniedPatterns[p], strings.Split(filters, \",\")...)\n\t\t\t}\n\t\t\tif patternPolicy != \"\" && patternPolicy != \"0\" {\n\t\t\t\tpatternPolicies[p] = patternPolicy\n\t\t\t}\n\t\t}\n\t}\n\n\tfor dirAllowed, allowPatterns := range allowedPatterns {\n\t\tfilter := sdk.PatternsFilter{\n\t\t\tPath:            dirAllowed,\n\t\t\tAllowedPatterns: allowPatterns,\n\t\t\tDenyPolicy:      getPatterDenyPolicyFromString(patternPolicies[dirAllowed]),\n\t\t}\n\t\tfor dirDenied, denPatterns := range deniedPatterns {\n\t\t\tif dirAllowed == dirDenied {\n\t\t\t\tfilter.DeniedPatterns = denPatterns\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tresult = append(result, filter)\n\t}\n\tfor dirDenied, denPatterns := range deniedPatterns {\n\t\tfound := false\n\t\tfor _, res := range result {\n\t\t\tif res.Path == dirDenied {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tresult = append(result, sdk.PatternsFilter{\n\t\t\t\tPath:           dirDenied,\n\t\t\t\tDeniedPatterns: denPatterns,\n\t\t\t\tDenyPolicy:     getPatterDenyPolicyFromString(patternPolicies[dirDenied]),\n\t\t\t})\n\t\t}\n\t}\n\treturn result\n}\n\nfunc getGroupsFromUserPostFields(r *http.Request) []sdk.GroupMapping {\n\tvar groups []sdk.GroupMapping\n\n\tprimaryGroup := strings.TrimSpace(r.Form.Get(\"primary_group\"))\n\tif primaryGroup != \"\" {\n\t\tgroups = append(groups, sdk.GroupMapping{\n\t\t\tName: primaryGroup,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t})\n\t}\n\tsecondaryGroups := r.Form[\"secondary_groups\"]\n\tfor _, name := range secondaryGroups {\n\t\tgroups = append(groups, sdk.GroupMapping{\n\t\t\tName: strings.TrimSpace(name),\n\t\t\tType: sdk.GroupTypeSecondary,\n\t\t})\n\t}\n\tmembershipGroups := r.Form[\"membership_groups\"]\n\tfor _, name := range membershipGroups {\n\t\tgroups = append(groups, sdk.GroupMapping{\n\t\t\tName: strings.TrimSpace(name),\n\t\t\tType: sdk.GroupTypeMembership,\n\t\t})\n\t}\n\treturn groups\n}\n\nfunc getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error) {\n\tvar filters sdk.BaseUserFilters\n\tbwLimits, err := getBandwidthLimitsFromPostFields(r)\n\tif err != nil {\n\t\treturn filters, err\n\t}\n\tmaxFileSize, err := util.ParseBytes(r.Form.Get(\"max_upload_file_size\"))\n\tif err != nil {\n\t\treturn filters, util.NewI18nError(fmt.Errorf(\"invalid max upload file size: %w\", err), util.I18nErrorInvalidMaxFilesize)\n\t}\n\tdefaultSharesExpiration, err := strconv.Atoi(r.Form.Get(\"default_shares_expiration\"))\n\tif err != nil {\n\t\treturn filters, fmt.Errorf(\"invalid default shares expiration: %w\", err)\n\t}\n\tmaxSharesExpiration, err := strconv.Atoi(r.Form.Get(\"max_shares_expiration\"))\n\tif err != nil {\n\t\treturn filters, fmt.Errorf(\"invalid max shares expiration: %w\", err)\n\t}\n\tpasswordExpiration, err := strconv.Atoi(r.Form.Get(\"password_expiration\"))\n\tif err != nil {\n\t\treturn filters, fmt.Errorf(\"invalid password expiration: %w\", err)\n\t}\n\tpasswordStrength, err := strconv.Atoi(r.Form.Get(\"password_strength\"))\n\tif err != nil {\n\t\treturn filters, fmt.Errorf(\"invalid password strength: %w\", err)\n\t}\n\tif r.Form.Get(\"ftp_security\") == \"1\" {\n\t\tfilters.FTPSecurity = 1\n\t}\n\tfilters.BandwidthLimits = bwLimits\n\tfilters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get(\"allowed_ip\"), \",\")\n\tfilters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get(\"denied_ip\"), \",\")\n\tfilters.DeniedLoginMethods = r.Form[\"denied_login_methods\"]\n\tfilters.DeniedProtocols = r.Form[\"denied_protocols\"]\n\tfilters.TwoFactorAuthProtocols = r.Form[\"required_two_factor_protocols\"]\n\tfilters.FilePatterns = getFilePatternsFromPostField(r)\n\tfilters.TLSUsername = sdk.TLSUsername(strings.TrimSpace(r.Form.Get(\"tls_username\")))\n\tfilters.WebClient = r.Form[\"web_client_options\"]\n\tfilters.DefaultSharesExpiration = defaultSharesExpiration\n\tfilters.MaxSharesExpiration = maxSharesExpiration\n\tfilters.PasswordExpiration = passwordExpiration\n\tfilters.PasswordStrength = passwordStrength\n\tfilters.AccessTime = getAccessTimeRestrictionsFromPostFields(r)\n\thooks := r.Form[\"hooks\"]\n\tif slices.Contains(hooks, \"external_auth_disabled\") {\n\t\tfilters.Hooks.ExternalAuthDisabled = true\n\t}\n\tif slices.Contains(hooks, \"pre_login_disabled\") {\n\t\tfilters.Hooks.PreLoginDisabled = true\n\t}\n\tif slices.Contains(hooks, \"check_password_disabled\") {\n\t\tfilters.Hooks.CheckPasswordDisabled = true\n\t}\n\tfilters.IsAnonymous = r.Form.Get(\"is_anonymous\") != \"\"\n\tfilters.DisableFsChecks = r.Form.Get(\"disable_fs_checks\") != \"\"\n\tfilters.AllowAPIKeyAuth = r.Form.Get(\"allow_api_key_auth\") != \"\"\n\tfilters.StartDirectory = strings.TrimSpace(r.Form.Get(\"start_directory\"))\n\tfilters.MaxUploadFileSize = maxFileSize\n\tfilters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get(\"external_auth_cache_time\"), 10, 64)\n\tif err != nil {\n\t\treturn filters, fmt.Errorf(\"invalid external auth cache time: %w\", err)\n\t}\n\treturn filters, nil\n}\n\nfunc getSecretFromFormField(r *http.Request, field string) *kms.Secret {\n\tsecret := kms.NewPlainSecret(r.Form.Get(field))\n\tif strings.TrimSpace(secret.GetPayload()) == redactedSecret {\n\t\tsecret.SetStatus(sdkkms.SecretStatusRedacted)\n\t}\n\tif strings.TrimSpace(secret.GetPayload()) == \"\" {\n\t\tsecret.SetStatus(\"\")\n\t}\n\treturn secret\n}\n\nfunc getS3Config(r *http.Request) (vfs.S3FsConfig, error) {\n\tvar err error\n\tconfig := vfs.S3FsConfig{}\n\tconfig.Bucket = strings.TrimSpace(r.Form.Get(\"s3_bucket\"))\n\tconfig.Region = strings.TrimSpace(r.Form.Get(\"s3_region\"))\n\tconfig.AccessKey = strings.TrimSpace(r.Form.Get(\"s3_access_key\"))\n\tconfig.RoleARN = strings.TrimSpace(r.Form.Get(\"s3_role_arn\"))\n\tconfig.AccessSecret = getSecretFromFormField(r, \"s3_access_secret\")\n\tconfig.SSECustomerKey = getSecretFromFormField(r, \"s3_sse_customer_key\")\n\tconfig.Endpoint = strings.TrimSpace(r.Form.Get(\"s3_endpoint\"))\n\tconfig.StorageClass = strings.TrimSpace(r.Form.Get(\"s3_storage_class\"))\n\tconfig.ACL = strings.TrimSpace(r.Form.Get(\"s3_acl\"))\n\tconfig.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get(\"s3_key_prefix\"), \"/\"))\n\tconfig.UploadPartSize, err = strconv.ParseInt(r.Form.Get(\"s3_upload_part_size\"), 10, 64)\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid s3 upload part size: %w\", err)\n\t}\n\tconfig.UploadConcurrency, err = strconv.Atoi(r.Form.Get(\"s3_upload_concurrency\"))\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid s3 upload concurrency: %w\", err)\n\t}\n\tconfig.DownloadPartSize, err = strconv.ParseInt(r.Form.Get(\"s3_download_part_size\"), 10, 64)\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid s3 download part size: %w\", err)\n\t}\n\tconfig.DownloadConcurrency, err = strconv.Atoi(r.Form.Get(\"s3_download_concurrency\"))\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid s3 download concurrency: %w\", err)\n\t}\n\tconfig.ForcePathStyle = r.Form.Get(\"s3_force_path_style\") != \"\"\n\tconfig.SkipTLSVerify = r.Form.Get(\"s3_skip_tls_verify\") != \"\"\n\tconfig.DownloadPartMaxTime, err = strconv.Atoi(r.Form.Get(\"s3_download_part_max_time\"))\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid s3 download part max time: %w\", err)\n\t}\n\tconfig.UploadPartMaxTime, err = strconv.Atoi(r.Form.Get(\"s3_upload_part_max_time\"))\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid s3 upload part max time: %w\", err)\n\t}\n\treturn config, nil\n}\n\nfunc getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {\n\tvar err error\n\tconfig := vfs.GCSFsConfig{}\n\n\tconfig.Bucket = strings.TrimSpace(r.Form.Get(\"gcs_bucket\"))\n\tconfig.StorageClass = strings.TrimSpace(r.Form.Get(\"gcs_storage_class\"))\n\tconfig.ACL = strings.TrimSpace(r.Form.Get(\"gcs_acl\"))\n\tconfig.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get(\"gcs_key_prefix\"), \"/\"))\n\tuploadPartSize, err := strconv.ParseInt(r.Form.Get(\"gcs_upload_part_size\"), 10, 64)\n\tif err == nil {\n\t\tconfig.UploadPartSize = uploadPartSize\n\t}\n\tuploadPartMaxTime, err := strconv.Atoi(r.Form.Get(\"gcs_upload_part_max_time\"))\n\tif err == nil {\n\t\tconfig.UploadPartMaxTime = uploadPartMaxTime\n\t}\n\tautoCredentials := r.Form.Get(\"gcs_auto_credentials\")\n\tif autoCredentials != \"\" {\n\t\tconfig.AutomaticCredentials = 1\n\t} else {\n\t\tconfig.AutomaticCredentials = 0\n\t}\n\tcredentials, _, err := r.FormFile(\"gcs_credential_file\")\n\tif errors.Is(err, http.ErrMissingFile) {\n\t\treturn config, nil\n\t}\n\tif err != nil {\n\t\treturn config, err\n\t}\n\tdefer credentials.Close()\n\tfileBytes, err := io.ReadAll(credentials)\n\tif err != nil || len(fileBytes) == 0 {\n\t\tif len(fileBytes) == 0 {\n\t\t\terr = errors.New(\"credentials file size must be greater than 0\")\n\t\t}\n\t\treturn config, err\n\t}\n\tconfig.Credentials = kms.NewPlainSecret(util.BytesToString(fileBytes))\n\tconfig.AutomaticCredentials = 0\n\treturn config, err\n}\n\nfunc getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {\n\tvar err error\n\tconfig := vfs.SFTPFsConfig{}\n\tconfig.Endpoint = strings.TrimSpace(r.Form.Get(\"sftp_endpoint\"))\n\tconfig.Username = strings.TrimSpace(r.Form.Get(\"sftp_username\"))\n\tconfig.Password = getSecretFromFormField(r, \"sftp_password\")\n\tconfig.PrivateKey = getSecretFromFormField(r, \"sftp_private_key\")\n\tconfig.KeyPassphrase = getSecretFromFormField(r, \"sftp_key_passphrase\")\n\tfingerprintsFormValue := r.Form.Get(\"sftp_fingerprints\")\n\tconfig.Fingerprints = getSliceFromDelimitedValues(fingerprintsFormValue, \"\\n\")\n\tconfig.Prefix = strings.TrimSpace(r.Form.Get(\"sftp_prefix\"))\n\tconfig.DisableCouncurrentReads = r.Form.Get(\"sftp_disable_concurrent_reads\") != \"\"\n\tconfig.BufferSize, err = strconv.ParseInt(r.Form.Get(\"sftp_buffer_size\"), 10, 64)\n\tif r.Form.Get(\"sftp_equality_check_mode\") != \"\" {\n\t\tconfig.EqualityCheckMode = 1\n\t} else {\n\t\tconfig.EqualityCheckMode = 0\n\t}\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid SFTP buffer size: %w\", err)\n\t}\n\treturn config, nil\n}\n\nfunc getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {\n\tconfig := vfs.HTTPFsConfig{}\n\tconfig.Endpoint = strings.TrimSpace(r.Form.Get(\"http_endpoint\"))\n\tconfig.Username = strings.TrimSpace(r.Form.Get(\"http_username\"))\n\tconfig.SkipTLSVerify = r.Form.Get(\"http_skip_tls_verify\") != \"\"\n\tconfig.Password = getSecretFromFormField(r, \"http_password\")\n\tconfig.APIKey = getSecretFromFormField(r, \"http_api_key\")\n\tif r.Form.Get(\"http_equality_check_mode\") != \"\" {\n\t\tconfig.EqualityCheckMode = 1\n\t} else {\n\t\tconfig.EqualityCheckMode = 0\n\t}\n\treturn config\n}\n\nfunc getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {\n\tvar err error\n\tconfig := vfs.AzBlobFsConfig{}\n\tconfig.Container = strings.TrimSpace(r.Form.Get(\"az_container\"))\n\tconfig.AccountName = strings.TrimSpace(r.Form.Get(\"az_account_name\"))\n\tconfig.AccountKey = getSecretFromFormField(r, \"az_account_key\")\n\tconfig.SASURL = getSecretFromFormField(r, \"az_sas_url\")\n\tconfig.Endpoint = strings.TrimSpace(r.Form.Get(\"az_endpoint\"))\n\tconfig.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get(\"az_key_prefix\"), \"/\"))\n\tconfig.AccessTier = strings.TrimSpace(r.Form.Get(\"az_access_tier\"))\n\tconfig.UseEmulator = r.Form.Get(\"az_use_emulator\") != \"\"\n\tconfig.UploadPartSize, err = strconv.ParseInt(r.Form.Get(\"az_upload_part_size\"), 10, 64)\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid azure upload part size: %w\", err)\n\t}\n\tconfig.UploadConcurrency, err = strconv.Atoi(r.Form.Get(\"az_upload_concurrency\"))\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid azure upload concurrency: %w\", err)\n\t}\n\tconfig.DownloadPartSize, err = strconv.ParseInt(r.Form.Get(\"az_download_part_size\"), 10, 64)\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid azure download part size: %w\", err)\n\t}\n\tconfig.DownloadConcurrency, err = strconv.Atoi(r.Form.Get(\"az_download_concurrency\"))\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"invalid azure download concurrency: %w\", err)\n\t}\n\treturn config, nil\n}\n\nfunc getOsConfigFromPostFields(r *http.Request, readBufferField, writeBufferField string) sdk.OSFsConfig {\n\tconfig := sdk.OSFsConfig{}\n\treadBuffer, err := strconv.Atoi(r.Form.Get(readBufferField))\n\tif err == nil {\n\t\tconfig.ReadBufferSize = readBuffer\n\t}\n\twriteBuffer, err := strconv.Atoi(r.Form.Get(writeBufferField))\n\tif err == nil {\n\t\tconfig.WriteBufferSize = writeBuffer\n\t}\n\treturn config\n}\n\nfunc getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) {\n\tvar fs vfs.Filesystem\n\tfs.Provider = dataprovider.GetProviderFromValue(r.Form.Get(\"fs_provider\"))\n\tswitch fs.Provider {\n\tcase sdk.LocalFilesystemProvider:\n\t\tfs.OSConfig = getOsConfigFromPostFields(r, \"osfs_read_buffer_size\", \"osfs_write_buffer_size\")\n\tcase sdk.S3FilesystemProvider:\n\t\tconfig, err := getS3Config(r)\n\t\tif err != nil {\n\t\t\treturn fs, err\n\t\t}\n\t\tfs.S3Config = config\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tconfig, err := getAzureConfig(r)\n\t\tif err != nil {\n\t\t\treturn fs, err\n\t\t}\n\t\tfs.AzBlobConfig = config\n\tcase sdk.GCSFilesystemProvider:\n\t\tconfig, err := getGCSConfig(r)\n\t\tif err != nil {\n\t\t\treturn fs, err\n\t\t}\n\t\tfs.GCSConfig = config\n\tcase sdk.CryptedFilesystemProvider:\n\t\tfs.CryptConfig.Passphrase = getSecretFromFormField(r, \"crypt_passphrase\")\n\t\tfs.CryptConfig.OSFsConfig = getOsConfigFromPostFields(r, \"cryptfs_read_buffer_size\", \"cryptfs_write_buffer_size\")\n\tcase sdk.SFTPFilesystemProvider:\n\t\tconfig, err := getSFTPConfig(r)\n\t\tif err != nil {\n\t\t\treturn fs, err\n\t\t}\n\t\tfs.SFTPConfig = config\n\tcase sdk.HTTPFilesystemProvider:\n\t\tfs.HTTPConfig = getHTTPFsConfig(r)\n\t}\n\treturn fs, nil\n}\n\nfunc getAdminHiddenUserPageSections(r *http.Request) int {\n\tvar result int\n\n\tfor _, val := range r.Form[\"user_page_hidden_sections\"] {\n\t\tswitch val {\n\t\tcase \"1\":\n\t\t\tresult++\n\t\tcase \"2\":\n\t\t\tresult += 2\n\t\tcase \"3\":\n\t\t\tresult += 4\n\t\tcase \"4\":\n\t\t\tresult += 8\n\t\tcase \"5\":\n\t\t\tresult += 16\n\t\tcase \"6\":\n\t\t\tresult += 32\n\t\tcase \"7\":\n\t\t\tresult += 64\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {\n\tvar admin dataprovider.Admin\n\terr := r.ParseForm()\n\tif err != nil {\n\t\treturn admin, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tstatus, err := strconv.Atoi(r.Form.Get(\"status\"))\n\tif err != nil {\n\t\treturn admin, fmt.Errorf(\"invalid status: %w\", err)\n\t}\n\tadmin.Username = strings.TrimSpace(r.Form.Get(\"username\"))\n\tadmin.Password = strings.TrimSpace(r.Form.Get(\"password\"))\n\tadmin.Permissions = r.Form[\"permissions\"]\n\tadmin.Email = strings.TrimSpace(r.Form.Get(\"email\"))\n\tadmin.Status = status\n\tadmin.Role = strings.TrimSpace(r.Form.Get(\"role\"))\n\tadmin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get(\"allowed_ip\"), \",\")\n\tadmin.Filters.AllowAPIKeyAuth = r.Form.Get(\"allow_api_key_auth\") != \"\"\n\tadmin.Filters.RequireTwoFactor = r.Form.Get(\"require_two_factor\") != \"\"\n\tadmin.Filters.RequirePasswordChange = r.Form.Get(\"require_password_change\") != \"\"\n\tadmin.AdditionalInfo = r.Form.Get(\"additional_info\")\n\tadmin.Description = r.Form.Get(\"description\")\n\tadmin.Filters.Preferences.HideUserPageSections = getAdminHiddenUserPageSections(r)\n\tadmin.Filters.Preferences.DefaultUsersExpiration = 0\n\tif val := r.Form.Get(\"default_users_expiration\"); val != \"\" {\n\t\tdefaultUsersExpiration, err := strconv.Atoi(r.Form.Get(\"default_users_expiration\"))\n\t\tif err != nil {\n\t\t\treturn admin, fmt.Errorf(\"invalid default users expiration: %w\", err)\n\t\t}\n\t\tadmin.Filters.Preferences.DefaultUsersExpiration = defaultUsersExpiration\n\t}\n\tfor k := range r.Form {\n\t\tif hasPrefixAndSuffix(k, \"groups[\", \"][group]\") {\n\t\t\tgroupName := strings.TrimSpace(r.Form.Get(k))\n\t\t\tif groupName != \"\" {\n\t\t\t\tgroup := dataprovider.AdminGroupMapping{\n\t\t\t\t\tName: groupName,\n\t\t\t\t}\n\t\t\t\tbase, _ := strings.CutSuffix(k, \"[group]\")\n\t\t\t\taddAsGroupType := strings.TrimSpace(r.Form.Get(base + \"[group_type]\"))\n\t\t\t\tswitch addAsGroupType {\n\t\t\t\tcase \"1\":\n\t\t\t\t\tgroup.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsPrimary\n\t\t\t\tcase \"2\":\n\t\t\t\t\tgroup.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsSecondary\n\t\t\t\tdefault:\n\t\t\t\t\tgroup.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsMembership\n\t\t\t\t}\n\t\t\t\tadmin.Groups = append(admin.Groups, group)\n\t\t\t}\n\t\t}\n\t}\n\treturn admin, nil\n}\n\nfunc replacePlaceholders(field string, replacements map[string]string) string {\n\tfor k, v := range replacements {\n\t\tfield = strings.ReplaceAll(field, k, v)\n\t}\n\treturn field\n}\n\nfunc getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVirtualFolder {\n\tfolder.Name = name\n\treplacements := make(map[string]string)\n\treplacements[\"%name%\"] = folder.Name\n\n\tfolder.MappedPath = replacePlaceholders(folder.MappedPath, replacements)\n\tfolder.Description = replacePlaceholders(folder.Description, replacements)\n\tswitch folder.FsConfig.Provider {\n\tcase sdk.CryptedFilesystemProvider:\n\t\tfolder.FsConfig.CryptConfig = getCryptFsFromTemplate(folder.FsConfig.CryptConfig, replacements)\n\tcase sdk.S3FilesystemProvider:\n\t\tfolder.FsConfig.S3Config = getS3FsFromTemplate(folder.FsConfig.S3Config, replacements)\n\tcase sdk.GCSFilesystemProvider:\n\t\tfolder.FsConfig.GCSConfig = getGCSFsFromTemplate(folder.FsConfig.GCSConfig, replacements)\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tfolder.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(folder.FsConfig.AzBlobConfig, replacements)\n\tcase sdk.SFTPFilesystemProvider:\n\t\tfolder.FsConfig.SFTPConfig = getSFTPFsFromTemplate(folder.FsConfig.SFTPConfig, replacements)\n\tcase sdk.HTTPFilesystemProvider:\n\t\tfolder.FsConfig.HTTPConfig = getHTTPFsFromTemplate(folder.FsConfig.HTTPConfig, replacements)\n\t}\n\n\treturn folder\n}\n\nfunc getCryptFsFromTemplate(fsConfig vfs.CryptFsConfig, replacements map[string]string) vfs.CryptFsConfig {\n\tif fsConfig.Passphrase != nil {\n\t\tif fsConfig.Passphrase.IsPlain() {\n\t\t\tpayload := replacePlaceholders(fsConfig.Passphrase.GetPayload(), replacements)\n\t\t\tfsConfig.Passphrase = kms.NewPlainSecret(payload)\n\t\t}\n\t}\n\treturn fsConfig\n}\n\nfunc getS3FsFromTemplate(fsConfig vfs.S3FsConfig, replacements map[string]string) vfs.S3FsConfig {\n\tfsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)\n\tfsConfig.AccessKey = replacePlaceholders(fsConfig.AccessKey, replacements)\n\tif fsConfig.AccessSecret != nil && fsConfig.AccessSecret.IsPlain() {\n\t\tpayload := replacePlaceholders(fsConfig.AccessSecret.GetPayload(), replacements)\n\t\tfsConfig.AccessSecret = kms.NewPlainSecret(payload)\n\t}\n\tif fsConfig.SSECustomerKey != nil && fsConfig.SSECustomerKey.IsPlain() {\n\t\tpayload := replacePlaceholders(fsConfig.SSECustomerKey.GetPayload(), replacements)\n\t\tfsConfig.SSECustomerKey = kms.NewPlainSecret(payload)\n\t}\n\treturn fsConfig\n}\n\nfunc getGCSFsFromTemplate(fsConfig vfs.GCSFsConfig, replacements map[string]string) vfs.GCSFsConfig {\n\tfsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)\n\treturn fsConfig\n}\n\nfunc getAzBlobFsFromTemplate(fsConfig vfs.AzBlobFsConfig, replacements map[string]string) vfs.AzBlobFsConfig {\n\tfsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)\n\tfsConfig.AccountName = replacePlaceholders(fsConfig.AccountName, replacements)\n\tif fsConfig.AccountKey != nil && fsConfig.AccountKey.IsPlain() {\n\t\tpayload := replacePlaceholders(fsConfig.AccountKey.GetPayload(), replacements)\n\t\tfsConfig.AccountKey = kms.NewPlainSecret(payload)\n\t}\n\treturn fsConfig\n}\n\nfunc getSFTPFsFromTemplate(fsConfig vfs.SFTPFsConfig, replacements map[string]string) vfs.SFTPFsConfig {\n\tfsConfig.Prefix = replacePlaceholders(fsConfig.Prefix, replacements)\n\tfsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)\n\tif fsConfig.Password != nil && fsConfig.Password.IsPlain() {\n\t\tpayload := replacePlaceholders(fsConfig.Password.GetPayload(), replacements)\n\t\tfsConfig.Password = kms.NewPlainSecret(payload)\n\t}\n\treturn fsConfig\n}\n\nfunc getHTTPFsFromTemplate(fsConfig vfs.HTTPFsConfig, replacements map[string]string) vfs.HTTPFsConfig {\n\tfsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)\n\treturn fsConfig\n}\n\nfunc getUserFromTemplate(user dataprovider.User, template userTemplateFields) dataprovider.User {\n\tuser.Username = template.Username\n\tuser.Password = template.Password\n\tuser.PublicKeys = template.PublicKeys\n\tuser.Filters.RequirePasswordChange = template.RequirePwdChange\n\treplacements := make(map[string]string)\n\treplacements[\"%username%\"] = user.Username\n\tif user.Password != \"\" && !user.IsPasswordHashed() {\n\t\tuser.Password = replacePlaceholders(user.Password, replacements)\n\t\treplacements[\"%password%\"] = user.Password\n\t}\n\n\tuser.HomeDir = replacePlaceholders(user.HomeDir, replacements)\n\tvar vfolders []vfs.VirtualFolder\n\tfor _, vfolder := range user.VirtualFolders {\n\t\tvfolder.Name = replacePlaceholders(vfolder.Name, replacements)\n\t\tvfolder.VirtualPath = replacePlaceholders(vfolder.VirtualPath, replacements)\n\t\tvfolders = append(vfolders, vfolder)\n\t}\n\tuser.VirtualFolders = vfolders\n\tuser.Description = replacePlaceholders(user.Description, replacements)\n\tuser.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements)\n\tuser.Filters.StartDirectory = replacePlaceholders(user.Filters.StartDirectory, replacements)\n\n\tswitch user.FsConfig.Provider {\n\tcase sdk.CryptedFilesystemProvider:\n\t\tuser.FsConfig.CryptConfig = getCryptFsFromTemplate(user.FsConfig.CryptConfig, replacements)\n\tcase sdk.S3FilesystemProvider:\n\t\tuser.FsConfig.S3Config = getS3FsFromTemplate(user.FsConfig.S3Config, replacements)\n\tcase sdk.GCSFilesystemProvider:\n\t\tuser.FsConfig.GCSConfig = getGCSFsFromTemplate(user.FsConfig.GCSConfig, replacements)\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tuser.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements)\n\tcase sdk.SFTPFilesystemProvider:\n\t\tuser.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements)\n\tcase sdk.HTTPFilesystemProvider:\n\t\tuser.FsConfig.HTTPConfig = getHTTPFsFromTemplate(user.FsConfig.HTTPConfig, replacements)\n\t}\n\n\treturn user\n}\n\nfunc getTransferLimits(r *http.Request) (int64, int64, int64, error) {\n\tdataTransferUL, err := strconv.ParseInt(r.Form.Get(\"upload_data_transfer\"), 10, 64)\n\tif err != nil {\n\t\treturn 0, 0, 0, fmt.Errorf(\"invalid upload data transfer: %w\", err)\n\t}\n\tdataTransferDL, err := strconv.ParseInt(r.Form.Get(\"download_data_transfer\"), 10, 64)\n\tif err != nil {\n\t\treturn 0, 0, 0, fmt.Errorf(\"invalid download data transfer: %w\", err)\n\t}\n\tdataTransferTotal, err := strconv.ParseInt(r.Form.Get(\"total_data_transfer\"), 10, 64)\n\tif err != nil {\n\t\treturn 0, 0, 0, fmt.Errorf(\"invalid total data transfer: %w\", err)\n\t}\n\treturn dataTransferUL, dataTransferDL, dataTransferTotal, nil\n}\n\nfunc getQuotaLimits(r *http.Request) (int64, int, error) {\n\tquotaSize, err := util.ParseBytes(r.Form.Get(\"quota_size\"))\n\tif err != nil {\n\t\treturn 0, 0, util.NewI18nError(fmt.Errorf(\"invalid quota size: %w\", err), util.I18nErrorInvalidQuotaSize)\n\t}\n\tquotaFiles, err := strconv.Atoi(r.Form.Get(\"quota_files\"))\n\tif err != nil {\n\t\treturn 0, 0, fmt.Errorf(\"invalid quota files: %w\", err)\n\t}\n\treturn quotaSize, quotaFiles, nil\n}\n\nfunc updateRepeaterFormFields(r *http.Request) {\n\tfor k := range r.Form {\n\t\tif hasPrefixAndSuffix(k, \"public_keys[\", \"][public_key]\") {\n\t\t\tkey := r.Form.Get(k)\n\t\t\tif strings.TrimSpace(key) != \"\" {\n\t\t\t\tr.Form.Add(\"public_keys\", key)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"tls_certs[\", \"][tls_cert]\") {\n\t\t\tcert := strings.TrimSpace(r.Form.Get(k))\n\t\t\tif cert != \"\" {\n\t\t\t\tr.Form.Add(\"tls_certs\", cert)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"additional_emails[\", \"][additional_email]\") {\n\t\t\temail := strings.TrimSpace(r.Form.Get(k))\n\t\t\tif email != \"\" {\n\t\t\t\tr.Form.Add(\"additional_emails\", email)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"virtual_folders[\", \"][vfolder_path]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[vfolder_path]\")\n\t\t\tr.Form.Add(\"vfolder_path\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"vfolder_name\", strings.TrimSpace(r.Form.Get(base+\"[vfolder_name]\")))\n\t\t\tr.Form.Add(\"vfolder_quota_files\", strings.TrimSpace(r.Form.Get(base+\"[vfolder_quota_files]\")))\n\t\t\tr.Form.Add(\"vfolder_quota_size\", strings.TrimSpace(r.Form.Get(base+\"[vfolder_quota_size]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"directory_permissions[\", \"][sub_perm_path]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[sub_perm_path]\")\n\t\t\tr.Form.Add(\"sub_perm_path\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form[\"sub_perm_permissions\"+strconv.Itoa(len(r.Form[\"sub_perm_path\"])-1)] = r.Form[base+\"[sub_perm_permissions][]\"]\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"directory_patterns[\", \"][pattern_path]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[pattern_path]\")\n\t\t\tr.Form.Add(\"pattern_path\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"patterns\", strings.TrimSpace(r.Form.Get(base+\"[patterns]\")))\n\t\t\tr.Form.Add(\"pattern_type\", strings.TrimSpace(r.Form.Get(base+\"[pattern_type]\")))\n\t\t\tr.Form.Add(\"pattern_policy\", strings.TrimSpace(r.Form.Get(base+\"[pattern_policy]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"access_time_restrictions[\", \"][access_time_day_of_week]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[access_time_day_of_week]\")\n\t\t\tr.Form.Add(\"access_time_day_of_week\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"access_time_start\", strings.TrimSpace(r.Form.Get(base+\"[access_time_start]\")))\n\t\t\tr.Form.Add(\"access_time_end\", strings.TrimSpace(r.Form.Get(base+\"[access_time_end]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"src_bandwidth_limits[\", \"][bandwidth_limit_sources]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[bandwidth_limit_sources]\")\n\t\t\tr.Form.Add(\"bandwidth_limit_sources\", r.Form.Get(k))\n\t\t\tr.Form.Add(\"upload_bandwidth_source\", strings.TrimSpace(r.Form.Get(base+\"[upload_bandwidth_source]\")))\n\t\t\tr.Form.Add(\"download_bandwidth_source\", strings.TrimSpace(r.Form.Get(base+\"[download_bandwidth_source]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"template_users[\", \"][tpl_username]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[tpl_username]\")\n\t\t\tr.Form.Add(\"tpl_username\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"tpl_password\", strings.TrimSpace(r.Form.Get(base+\"[tpl_password]\")))\n\t\t\tr.Form.Add(\"tpl_public_keys\", strings.TrimSpace(r.Form.Get(base+\"[tpl_public_keys]\")))\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc getUserFromPostFields(r *http.Request) (dataprovider.User, error) {\n\tuser := dataprovider.User{}\n\terr := r.ParseMultipartForm(maxRequestSize)\n\tif err != nil {\n\t\treturn user, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tupdateRepeaterFormFields(r)\n\n\tuid, err := strconv.Atoi(r.Form.Get(\"uid\"))\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"invalid uid: %w\", err)\n\t}\n\tgid, err := strconv.Atoi(r.Form.Get(\"gid\"))\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"invalid uid: %w\", err)\n\t}\n\tmaxSessions, err := strconv.Atoi(r.Form.Get(\"max_sessions\"))\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"invalid max sessions: %w\", err)\n\t}\n\tquotaSize, quotaFiles, err := getQuotaLimits(r)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tbandwidthUL, err := strconv.ParseInt(r.Form.Get(\"upload_bandwidth\"), 10, 64)\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"invalid upload bandwidth: %w\", err)\n\t}\n\tbandwidthDL, err := strconv.ParseInt(r.Form.Get(\"download_bandwidth\"), 10, 64)\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"invalid download bandwidth: %w\", err)\n\t}\n\tdataTransferUL, dataTransferDL, dataTransferTotal, err := getTransferLimits(r)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tstatus, err := strconv.Atoi(r.Form.Get(\"status\"))\n\tif err != nil {\n\t\treturn user, fmt.Errorf(\"invalid status: %w\", err)\n\t}\n\texpirationDateMillis := int64(0)\n\texpirationDateString := r.Form.Get(\"expiration_date\")\n\tif strings.TrimSpace(expirationDateString) != \"\" {\n\t\texpirationDate, err := time.Parse(webDateTimeFormat, expirationDateString)\n\t\tif err != nil {\n\t\t\treturn user, err\n\t\t}\n\t\texpirationDateMillis = util.GetTimeAsMsSinceEpoch(expirationDate)\n\t}\n\tfsConfig, err := getFsConfigFromPostFields(r)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tfilters, err := getFiltersFromUserPostFields(r)\n\tif err != nil {\n\t\treturn user, err\n\t}\n\tfilters.TLSCerts = r.Form[\"tls_certs\"]\n\tuser = dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:             strings.TrimSpace(r.Form.Get(\"username\")),\n\t\t\tEmail:                strings.TrimSpace(r.Form.Get(\"email\")),\n\t\t\tPassword:             strings.TrimSpace(r.Form.Get(\"password\")),\n\t\t\tPublicKeys:           r.Form[\"public_keys\"],\n\t\t\tHomeDir:              strings.TrimSpace(r.Form.Get(\"home_dir\")),\n\t\t\tUID:                  uid,\n\t\t\tGID:                  gid,\n\t\t\tPermissions:          getUserPermissionsFromPostFields(r),\n\t\t\tMaxSessions:          maxSessions,\n\t\t\tQuotaSize:            quotaSize,\n\t\t\tQuotaFiles:           quotaFiles,\n\t\t\tUploadBandwidth:      bandwidthUL,\n\t\t\tDownloadBandwidth:    bandwidthDL,\n\t\t\tUploadDataTransfer:   dataTransferUL,\n\t\t\tDownloadDataTransfer: dataTransferDL,\n\t\t\tTotalDataTransfer:    dataTransferTotal,\n\t\t\tStatus:               status,\n\t\t\tExpirationDate:       expirationDateMillis,\n\t\t\tAdditionalInfo:       r.Form.Get(\"additional_info\"),\n\t\t\tDescription:          r.Form.Get(\"description\"),\n\t\t\tRole:                 strings.TrimSpace(r.Form.Get(\"role\")),\n\t\t},\n\t\tFilters: dataprovider.UserFilters{\n\t\t\tBaseUserFilters:       filters,\n\t\t\tRequirePasswordChange: r.Form.Get(\"require_password_change\") != \"\",\n\t\t\tAdditionalEmails:      r.Form[\"additional_emails\"],\n\t\t},\n\t\tVirtualFolders: getVirtualFoldersFromPostFields(r),\n\t\tFsConfig:       fsConfig,\n\t\tGroups:         getGroupsFromUserPostFields(r),\n\t}\n\treturn user, nil\n}\n\nfunc getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {\n\tgroup := dataprovider.Group{}\n\terr := r.ParseMultipartForm(maxRequestSize)\n\tif err != nil {\n\t\treturn group, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tupdateRepeaterFormFields(r)\n\n\tmaxSessions, err := strconv.Atoi(r.Form.Get(\"max_sessions\"))\n\tif err != nil {\n\t\treturn group, fmt.Errorf(\"invalid max sessions: %w\", err)\n\t}\n\tquotaSize, quotaFiles, err := getQuotaLimits(r)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tbandwidthUL, err := strconv.ParseInt(r.Form.Get(\"upload_bandwidth\"), 10, 64)\n\tif err != nil {\n\t\treturn group, fmt.Errorf(\"invalid upload bandwidth: %w\", err)\n\t}\n\tbandwidthDL, err := strconv.ParseInt(r.Form.Get(\"download_bandwidth\"), 10, 64)\n\tif err != nil {\n\t\treturn group, fmt.Errorf(\"invalid download bandwidth: %w\", err)\n\t}\n\tdataTransferUL, dataTransferDL, dataTransferTotal, err := getTransferLimits(r)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\texpiresIn, err := strconv.Atoi(r.Form.Get(\"expires_in\"))\n\tif err != nil {\n\t\treturn group, fmt.Errorf(\"invalid expires in: %w\", err)\n\t}\n\tfsConfig, err := getFsConfigFromPostFields(r)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tfilters, err := getFiltersFromUserPostFields(r)\n\tif err != nil {\n\t\treturn group, err\n\t}\n\tgroup = dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName:        strings.TrimSpace(r.Form.Get(\"name\")),\n\t\t\tDescription: r.Form.Get(\"description\"),\n\t\t},\n\t\tUserSettings: dataprovider.GroupUserSettings{\n\t\t\tBaseGroupUserSettings: sdk.BaseGroupUserSettings{\n\t\t\t\tHomeDir:              strings.TrimSpace(r.Form.Get(\"home_dir\")),\n\t\t\t\tMaxSessions:          maxSessions,\n\t\t\t\tQuotaSize:            quotaSize,\n\t\t\t\tQuotaFiles:           quotaFiles,\n\t\t\t\tPermissions:          getSubDirPermissionsFromPostFields(r),\n\t\t\t\tUploadBandwidth:      bandwidthUL,\n\t\t\t\tDownloadBandwidth:    bandwidthDL,\n\t\t\t\tUploadDataTransfer:   dataTransferUL,\n\t\t\t\tDownloadDataTransfer: dataTransferDL,\n\t\t\t\tTotalDataTransfer:    dataTransferTotal,\n\t\t\t\tExpiresIn:            expiresIn,\n\t\t\t\tFilters:              filters,\n\t\t\t},\n\t\t\tFsConfig: fsConfig,\n\t\t},\n\t\tVirtualFolders: getVirtualFoldersFromPostFields(r),\n\t}\n\treturn group, nil\n}\n\nfunc getKeyValsFromPostFields(r *http.Request, key, val string) []dataprovider.KeyValue {\n\tvar res []dataprovider.KeyValue\n\n\tkeys := r.Form[key]\n\tvalues := r.Form[val]\n\n\tfor idx, k := range keys {\n\t\tv := values[idx]\n\t\tif k != \"\" && v != \"\" {\n\t\t\tres = append(res, dataprovider.KeyValue{\n\t\t\t\tKey:   k,\n\t\t\t\tValue: v,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn res\n}\n\nfunc getRenameConfigsFromPostFields(r *http.Request) []dataprovider.RenameConfig {\n\tvar res []dataprovider.RenameConfig\n\tkeys := r.Form[\"fs_rename_source\"]\n\tvalues := r.Form[\"fs_rename_target\"]\n\n\tfor idx, k := range keys {\n\t\tv := values[idx]\n\t\tif k != \"\" && v != \"\" {\n\t\t\topts := r.Form[\"fs_rename_options\"+strconv.Itoa(idx)]\n\t\t\tres = append(res, dataprovider.RenameConfig{\n\t\t\t\tKeyValue: dataprovider.KeyValue{\n\t\t\t\t\tKey:   k,\n\t\t\t\t\tValue: v,\n\t\t\t\t},\n\t\t\t\tUpdateModTime: slices.Contains(opts, \"1\"),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn res\n}\n\nfunc getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRetention, error) {\n\tvar res []dataprovider.FolderRetention\n\tpaths := r.Form[\"folder_retention_path\"]\n\tvalues := r.Form[\"folder_retention_val\"]\n\n\tfor idx, p := range paths {\n\t\tif p != \"\" {\n\t\t\tretention, err := strconv.Atoi(values[idx])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid retention for path %q: %w\", p, err)\n\t\t\t}\n\t\t\topts := r.Form[\"folder_retention_options\"+strconv.Itoa(idx)]\n\t\t\tres = append(res, dataprovider.FolderRetention{\n\t\t\t\tPath:            p,\n\t\t\t\tRetention:       retention,\n\t\t\t\tDeleteEmptyDirs: slices.Contains(opts, \"1\"),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\nfunc getHTTPPartsFromPostFields(r *http.Request) []dataprovider.HTTPPart {\n\tvar result []dataprovider.HTTPPart\n\n\tnames := r.Form[\"http_part_name\"]\n\tfiles := r.Form[\"http_part_file\"]\n\theaders := r.Form[\"http_part_headers\"]\n\tbodies := r.Form[\"http_part_body\"]\n\torders := r.Form[\"http_part_order\"]\n\n\tfor idx, partName := range names {\n\t\tif partName != \"\" {\n\t\t\torder, err := strconv.Atoi(orders[idx])\n\t\t\tif err == nil {\n\t\t\t\tfilePath := files[idx]\n\t\t\t\tbody := bodies[idx]\n\t\t\t\tconcatHeaders := getSliceFromDelimitedValues(headers[idx], \"\\n\")\n\t\t\t\tvar headers []dataprovider.KeyValue\n\t\t\t\tfor _, h := range concatHeaders {\n\t\t\t\t\tvalues := strings.SplitN(h, \":\", 2)\n\t\t\t\t\tif len(values) > 1 {\n\t\t\t\t\t\theaders = append(headers, dataprovider.KeyValue{\n\t\t\t\t\t\t\tKey:   strings.TrimSpace(values[0]),\n\t\t\t\t\t\t\tValue: strings.TrimSpace(values[1]),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tresult = append(result, dataprovider.HTTPPart{\n\t\t\t\t\tName:     partName,\n\t\t\t\t\tFilepath: filePath,\n\t\t\t\t\tHeaders:  headers,\n\t\t\t\t\tBody:     body,\n\t\t\t\t\tOrder:    order,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(result, func(i, j int) bool {\n\t\treturn result[i].Order < result[j].Order\n\t})\n\treturn result\n}\n\nfunc updateRepeaterFormActionFields(r *http.Request) {\n\tfor k := range r.Form {\n\t\tif hasPrefixAndSuffix(k, \"http_headers[\", \"][http_header_key]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[http_header_key]\")\n\t\t\tr.Form.Add(\"http_header_key\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"http_header_value\", strings.TrimSpace(r.Form.Get(base+\"[http_header_value]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"query_parameters[\", \"][http_query_key]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[http_query_key]\")\n\t\t\tr.Form.Add(\"http_query_key\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"http_query_value\", strings.TrimSpace(r.Form.Get(base+\"[http_query_value]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"multipart_body[\", \"][http_part_name]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[http_part_name]\")\n\t\t\torder, _ := strings.CutPrefix(k, \"multipart_body[\")\n\t\t\torder, _ = strings.CutSuffix(order, \"][http_part_name]\")\n\t\t\tr.Form.Add(\"http_part_name\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"http_part_file\", strings.TrimSpace(r.Form.Get(base+\"[http_part_file]\")))\n\t\t\tr.Form.Add(\"http_part_headers\", strings.TrimSpace(r.Form.Get(base+\"[http_part_headers]\")))\n\t\t\tr.Form.Add(\"http_part_body\", strings.TrimSpace(r.Form.Get(base+\"[http_part_body]\")))\n\t\t\tr.Form.Add(\"http_part_order\", order)\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"env_vars[\", \"][cmd_env_key]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[cmd_env_key]\")\n\t\t\tr.Form.Add(\"cmd_env_key\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"cmd_env_value\", strings.TrimSpace(r.Form.Get(base+\"[cmd_env_value]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"data_retention[\", \"][folder_retention_path]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[folder_retention_path]\")\n\t\t\tr.Form.Add(\"folder_retention_path\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"folder_retention_val\", strings.TrimSpace(r.Form.Get(base+\"[folder_retention_val]\")))\n\t\t\tr.Form[\"folder_retention_options\"+strconv.Itoa(len(r.Form[\"folder_retention_path\"])-1)] =\n\t\t\t\tr.Form[base+\"[folder_retention_options][]\"]\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"fs_rename[\", \"][fs_rename_source]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[fs_rename_source]\")\n\t\t\tr.Form.Add(\"fs_rename_source\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"fs_rename_target\", strings.TrimSpace(r.Form.Get(base+\"[fs_rename_target]\")))\n\t\t\tr.Form[\"fs_rename_options\"+strconv.Itoa(len(r.Form[\"fs_rename_source\"])-1)] =\n\t\t\t\tr.Form[base+\"[fs_rename_options][]\"]\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"fs_copy[\", \"][fs_copy_source]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[fs_copy_source]\")\n\t\t\tr.Form.Add(\"fs_copy_source\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"fs_copy_target\", strings.TrimSpace(r.Form.Get(base+\"[fs_copy_target]\")))\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEventActionOptions, error) {\n\tupdateRepeaterFormActionFields(r)\n\thttpTimeout, err := strconv.Atoi(r.Form.Get(\"http_timeout\"))\n\tif err != nil {\n\t\treturn dataprovider.BaseEventActionOptions{}, fmt.Errorf(\"invalid http timeout: %w\", err)\n\t}\n\tcmdTimeout, err := strconv.Atoi(r.Form.Get(\"cmd_timeout\"))\n\tif err != nil {\n\t\treturn dataprovider.BaseEventActionOptions{}, fmt.Errorf(\"invalid command timeout: %w\", err)\n\t}\n\tfoldersRetention, err := getFoldersRetentionFromPostFields(r)\n\tif err != nil {\n\t\treturn dataprovider.BaseEventActionOptions{}, err\n\t}\n\tfsActionType, err := strconv.Atoi(r.Form.Get(\"fs_action_type\"))\n\tif err != nil {\n\t\treturn dataprovider.BaseEventActionOptions{}, fmt.Errorf(\"invalid fs action type: %w\", err)\n\t}\n\tpwdExpirationThreshold, err := strconv.Atoi(r.Form.Get(\"pwd_expiration_threshold\"))\n\tif err != nil {\n\t\treturn dataprovider.BaseEventActionOptions{}, fmt.Errorf(\"invalid password expiration threshold: %w\", err)\n\t}\n\tvar disableThreshold, deleteThreshold int\n\tif val, err := strconv.Atoi(r.Form.Get(\"inactivity_disable_threshold\")); err == nil {\n\t\tdisableThreshold = val\n\t}\n\tif val, err := strconv.Atoi(r.Form.Get(\"inactivity_delete_threshold\")); err == nil {\n\t\tdeleteThreshold = val\n\t}\n\tvar emailAttachments []string\n\tif r.Form.Get(\"email_attachments\") != \"\" {\n\t\temailAttachments = getSliceFromDelimitedValues(r.Form.Get(\"email_attachments\"), \",\")\n\t}\n\tvar cmdArgs []string\n\tif r.Form.Get(\"cmd_arguments\") != \"\" {\n\t\tcmdArgs = getSliceFromDelimitedValues(r.Form.Get(\"cmd_arguments\"), \",\")\n\t}\n\tidpMode := 0\n\tif r.Form.Get(\"idp_mode\") == \"1\" {\n\t\tidpMode = 1\n\t}\n\temailContentType := 0\n\tif r.Form.Get(\"email_content_type\") == \"1\" {\n\t\temailContentType = 1\n\t}\n\toptions := dataprovider.BaseEventActionOptions{\n\t\tHTTPConfig: dataprovider.EventActionHTTPConfig{\n\t\t\tEndpoint:        strings.TrimSpace(r.Form.Get(\"http_endpoint\")),\n\t\t\tUsername:        strings.TrimSpace(r.Form.Get(\"http_username\")),\n\t\t\tPassword:        getSecretFromFormField(r, \"http_password\"),\n\t\t\tHeaders:         getKeyValsFromPostFields(r, \"http_header_key\", \"http_header_value\"),\n\t\t\tTimeout:         httpTimeout,\n\t\t\tSkipTLSVerify:   r.Form.Get(\"http_skip_tls_verify\") != \"\",\n\t\t\tMethod:          r.Form.Get(\"http_method\"),\n\t\t\tQueryParameters: getKeyValsFromPostFields(r, \"http_query_key\", \"http_query_value\"),\n\t\t\tBody:            r.Form.Get(\"http_body\"),\n\t\t\tParts:           getHTTPPartsFromPostFields(r),\n\t\t},\n\t\tCmdConfig: dataprovider.EventActionCommandConfig{\n\t\t\tCmd:     strings.TrimSpace(r.Form.Get(\"cmd_path\")),\n\t\t\tArgs:    cmdArgs,\n\t\t\tTimeout: cmdTimeout,\n\t\t\tEnvVars: getKeyValsFromPostFields(r, \"cmd_env_key\", \"cmd_env_value\"),\n\t\t},\n\t\tEmailConfig: dataprovider.EventActionEmailConfig{\n\t\t\tRecipients:  getSliceFromDelimitedValues(r.Form.Get(\"email_recipients\"), \",\"),\n\t\t\tBcc:         getSliceFromDelimitedValues(r.Form.Get(\"email_bcc\"), \",\"),\n\t\t\tSubject:     r.Form.Get(\"email_subject\"),\n\t\t\tContentType: emailContentType,\n\t\t\tBody:        r.Form.Get(\"email_body\"),\n\t\t\tAttachments: emailAttachments,\n\t\t},\n\t\tRetentionConfig: dataprovider.EventActionDataRetentionConfig{\n\t\t\tFolders: foldersRetention,\n\t\t},\n\t\tFsConfig: dataprovider.EventActionFilesystemConfig{\n\t\t\tType:    fsActionType,\n\t\t\tRenames: getRenameConfigsFromPostFields(r),\n\t\t\tDeletes: getSliceFromDelimitedValues(r.Form.Get(\"fs_delete_paths\"), \",\"),\n\t\t\tMkDirs:  getSliceFromDelimitedValues(r.Form.Get(\"fs_mkdir_paths\"), \",\"),\n\t\t\tExist:   getSliceFromDelimitedValues(r.Form.Get(\"fs_exist_paths\"), \",\"),\n\t\t\tCopy:    getKeyValsFromPostFields(r, \"fs_copy_source\", \"fs_copy_target\"),\n\t\t\tCompress: dataprovider.EventActionFsCompress{\n\t\t\t\tName:  strings.TrimSpace(r.Form.Get(\"fs_compress_name\")),\n\t\t\t\tPaths: getSliceFromDelimitedValues(r.Form.Get(\"fs_compress_paths\"), \",\"),\n\t\t\t},\n\t\t},\n\t\tPwdExpirationConfig: dataprovider.EventActionPasswordExpiration{\n\t\t\tThreshold: pwdExpirationThreshold,\n\t\t},\n\t\tUserInactivityConfig: dataprovider.EventActionUserInactivity{\n\t\t\tDisableThreshold: disableThreshold,\n\t\t\tDeleteThreshold:  deleteThreshold,\n\t\t},\n\t\tIDPConfig: dataprovider.EventActionIDPAccountCheck{\n\t\t\tMode:          idpMode,\n\t\t\tTemplateUser:  strings.TrimSpace(r.Form.Get(\"idp_user\")),\n\t\t\tTemplateAdmin: strings.TrimSpace(r.Form.Get(\"idp_admin\")),\n\t\t},\n\t}\n\treturn options, nil\n}\n\nfunc getEventActionFromPostFields(r *http.Request) (dataprovider.BaseEventAction, error) {\n\terr := r.ParseForm()\n\tif err != nil {\n\t\treturn dataprovider.BaseEventAction{}, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tactionType, err := strconv.Atoi(r.Form.Get(\"type\"))\n\tif err != nil {\n\t\treturn dataprovider.BaseEventAction{}, fmt.Errorf(\"invalid action type: %w\", err)\n\t}\n\toptions, err := getEventActionOptionsFromPostFields(r)\n\tif err != nil {\n\t\treturn dataprovider.BaseEventAction{}, err\n\t}\n\taction := dataprovider.BaseEventAction{\n\t\tName:        strings.TrimSpace(r.Form.Get(\"name\")),\n\t\tDescription: r.Form.Get(\"description\"),\n\t\tType:        actionType,\n\t\tOptions:     options,\n\t}\n\treturn action, nil\n}\n\nfunc getIDPLoginEventFromPostField(r *http.Request) int {\n\tswitch r.Form.Get(\"idp_login_event\") {\n\tcase \"1\":\n\t\treturn 1\n\tcase \"2\":\n\t\treturn 2\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {\n\tvar schedules []dataprovider.Schedule\n\tvar names, groupNames, roleNames, fsPaths []dataprovider.ConditionPattern\n\n\tscheduleHours := r.Form[\"schedule_hour\"]\n\tscheduleDayOfWeeks := r.Form[\"schedule_day_of_week\"]\n\tscheduleDayOfMonths := r.Form[\"schedule_day_of_month\"]\n\tscheduleMonths := r.Form[\"schedule_month\"]\n\n\tfor idx, hour := range scheduleHours {\n\t\tif hour != \"\" {\n\t\t\tschedules = append(schedules, dataprovider.Schedule{\n\t\t\t\tHours:      hour,\n\t\t\t\tDayOfWeek:  scheduleDayOfWeeks[idx],\n\t\t\t\tDayOfMonth: scheduleDayOfMonths[idx],\n\t\t\t\tMonth:      scheduleMonths[idx],\n\t\t\t})\n\t\t}\n\t}\n\n\tfor idx, name := range r.Form[\"name_pattern\"] {\n\t\tif name != \"\" {\n\t\t\tnames = append(names, dataprovider.ConditionPattern{\n\t\t\t\tPattern:      name,\n\t\t\t\tInverseMatch: r.Form[\"type_name_pattern\"][idx] == inversePatternType,\n\t\t\t})\n\t\t}\n\t}\n\n\tfor idx, name := range r.Form[\"group_name_pattern\"] {\n\t\tif name != \"\" {\n\t\t\tgroupNames = append(groupNames, dataprovider.ConditionPattern{\n\t\t\t\tPattern:      name,\n\t\t\t\tInverseMatch: r.Form[\"type_group_name_pattern\"][idx] == inversePatternType,\n\t\t\t})\n\t\t}\n\t}\n\n\tfor idx, name := range r.Form[\"role_name_pattern\"] {\n\t\tif name != \"\" {\n\t\t\troleNames = append(roleNames, dataprovider.ConditionPattern{\n\t\t\t\tPattern:      name,\n\t\t\t\tInverseMatch: r.Form[\"type_role_name_pattern\"][idx] == inversePatternType,\n\t\t\t})\n\t\t}\n\t}\n\n\tfor idx, name := range r.Form[\"fs_path_pattern\"] {\n\t\tif name != \"\" {\n\t\t\tfsPaths = append(fsPaths, dataprovider.ConditionPattern{\n\t\t\t\tPattern:      name,\n\t\t\t\tInverseMatch: r.Form[\"type_fs_path_pattern\"][idx] == inversePatternType,\n\t\t\t})\n\t\t}\n\t}\n\n\tminFileSize, err := util.ParseBytes(r.Form.Get(\"fs_min_size\"))\n\tif err != nil {\n\t\treturn dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf(\"invalid min file size: %w\", err), util.I18nErrorInvalidMinSize)\n\t}\n\tmaxFileSize, err := util.ParseBytes(r.Form.Get(\"fs_max_size\"))\n\tif err != nil {\n\t\treturn dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf(\"invalid max file size: %w\", err), util.I18nErrorInvalidMaxSize)\n\t}\n\tvar eventStatuses []int\n\tfor _, s := range r.Form[\"fs_statuses\"] {\n\t\tstatus, err := strconv.ParseInt(s, 10, 32)\n\t\tif err == nil {\n\t\t\teventStatuses = append(eventStatuses, int(status))\n\t\t}\n\t}\n\tconditions := dataprovider.EventConditions{\n\t\tFsEvents:       r.Form[\"fs_events\"],\n\t\tProviderEvents: r.Form[\"provider_events\"],\n\t\tIDPLoginEvent:  getIDPLoginEventFromPostField(r),\n\t\tSchedules:      schedules,\n\t\tOptions: dataprovider.ConditionOptions{\n\t\t\tNames:               names,\n\t\t\tGroupNames:          groupNames,\n\t\t\tRoleNames:           roleNames,\n\t\t\tFsPaths:             fsPaths,\n\t\t\tProtocols:           r.Form[\"fs_protocols\"],\n\t\t\tEventStatuses:       eventStatuses,\n\t\t\tProviderObjects:     r.Form[\"provider_objects\"],\n\t\t\tMinFileSize:         minFileSize,\n\t\t\tMaxFileSize:         maxFileSize,\n\t\t\tConcurrentExecution: r.Form.Get(\"concurrent_execution\") != \"\",\n\t\t},\n\t}\n\treturn conditions, nil\n}\n\nfunc getEventRuleActionsFromPostFields(r *http.Request) []dataprovider.EventAction {\n\tvar actions []dataprovider.EventAction\n\n\tnames := r.Form[\"action_name\"]\n\torders := r.Form[\"action_order\"]\n\n\tfor idx, name := range names {\n\t\tif name != \"\" {\n\t\t\torder, err := strconv.Atoi(orders[idx])\n\t\t\tif err == nil {\n\t\t\t\toptions := r.Form[\"action_options\"+strconv.Itoa(idx)]\n\t\t\t\tactions = append(actions, dataprovider.EventAction{\n\t\t\t\t\tBaseEventAction: dataprovider.BaseEventAction{\n\t\t\t\t\t\tName: name,\n\t\t\t\t\t},\n\t\t\t\t\tOrder: order + 1,\n\t\t\t\t\tOptions: dataprovider.EventActionOptions{\n\t\t\t\t\t\tIsFailureAction: slices.Contains(options, \"1\"),\n\t\t\t\t\t\tStopOnFailure:   slices.Contains(options, \"2\"),\n\t\t\t\t\t\tExecuteSync:     slices.Contains(options, \"3\"),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn actions\n}\n\nfunc updateRepeaterFormRuleFields(r *http.Request) {\n\tfor k := range r.Form {\n\t\tif hasPrefixAndSuffix(k, \"schedules[\", \"][schedule_hour]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[schedule_hour]\")\n\t\t\tr.Form.Add(\"schedule_hour\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"schedule_day_of_week\", strings.TrimSpace(r.Form.Get(base+\"[schedule_day_of_week]\")))\n\t\t\tr.Form.Add(\"schedule_day_of_month\", strings.TrimSpace(r.Form.Get(base+\"[schedule_day_of_month]\")))\n\t\t\tr.Form.Add(\"schedule_month\", strings.TrimSpace(r.Form.Get(base+\"[schedule_month]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"name_filters[\", \"][name_pattern]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[name_pattern]\")\n\t\t\tr.Form.Add(\"name_pattern\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"type_name_pattern\", strings.TrimSpace(r.Form.Get(base+\"[type_name_pattern]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"group_name_filters[\", \"][group_name_pattern]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[group_name_pattern]\")\n\t\t\tr.Form.Add(\"group_name_pattern\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"type_group_name_pattern\", strings.TrimSpace(r.Form.Get(base+\"[type_group_name_pattern]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"role_name_filters[\", \"][role_name_pattern]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[role_name_pattern]\")\n\t\t\tr.Form.Add(\"role_name_pattern\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"type_role_name_pattern\", strings.TrimSpace(r.Form.Get(base+\"[type_role_name_pattern]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"path_filters[\", \"][fs_path_pattern]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[fs_path_pattern]\")\n\t\t\tr.Form.Add(\"fs_path_pattern\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form.Add(\"type_fs_path_pattern\", strings.TrimSpace(r.Form.Get(base+\"[type_fs_path_pattern]\")))\n\t\t\tcontinue\n\t\t}\n\t\tif hasPrefixAndSuffix(k, \"actions[\", \"][action_name]\") {\n\t\t\tbase, _ := strings.CutSuffix(k, \"[action_name]\")\n\t\t\torder, _ := strings.CutPrefix(k, \"actions[\")\n\t\t\torder, _ = strings.CutSuffix(order, \"][action_name]\")\n\t\t\tr.Form.Add(\"action_name\", strings.TrimSpace(r.Form.Get(k)))\n\t\t\tr.Form[\"action_options\"+strconv.Itoa(len(r.Form[\"action_name\"])-1)] = r.Form[base+\"[action_options][]\"]\n\t\t\tr.Form.Add(\"action_order\", order)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) {\n\terr := r.ParseForm()\n\tif err != nil {\n\t\treturn dataprovider.EventRule{}, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tupdateRepeaterFormRuleFields(r)\n\tstatus, err := strconv.Atoi(r.Form.Get(\"status\"))\n\tif err != nil {\n\t\treturn dataprovider.EventRule{}, fmt.Errorf(\"invalid status: %w\", err)\n\t}\n\ttrigger, err := strconv.Atoi(r.Form.Get(\"trigger\"))\n\tif err != nil {\n\t\treturn dataprovider.EventRule{}, fmt.Errorf(\"invalid trigger: %w\", err)\n\t}\n\tconditions, err := getEventRuleConditionsFromPostFields(r)\n\tif err != nil {\n\t\treturn dataprovider.EventRule{}, err\n\t}\n\trule := dataprovider.EventRule{\n\t\tName:        strings.TrimSpace(r.Form.Get(\"name\")),\n\t\tStatus:      status,\n\t\tDescription: r.Form.Get(\"description\"),\n\t\tTrigger:     trigger,\n\t\tConditions:  conditions,\n\t\tActions:     getEventRuleActionsFromPostFields(r),\n\t}\n\treturn rule, nil\n}\n\nfunc getRoleFromPostFields(r *http.Request) (dataprovider.Role, error) {\n\terr := r.ParseForm()\n\tif err != nil {\n\t\treturn dataprovider.Role{}, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\n\treturn dataprovider.Role{\n\t\tName:        strings.TrimSpace(r.Form.Get(\"name\")),\n\t\tDescription: r.Form.Get(\"description\"),\n\t}, nil\n}\n\nfunc getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListType) (dataprovider.IPListEntry, error) {\n\terr := r.ParseForm()\n\tif err != nil {\n\t\treturn dataprovider.IPListEntry{}, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tvar mode int\n\tif listType == dataprovider.IPListTypeDefender {\n\t\tmode, err = strconv.Atoi(r.Form.Get(\"mode\"))\n\t\tif err != nil {\n\t\t\treturn dataprovider.IPListEntry{}, fmt.Errorf(\"invalid mode: %w\", err)\n\t\t}\n\t} else {\n\t\tmode = 1\n\t}\n\tprotocols := 0\n\tfor _, proto := range r.Form[\"protocols\"] {\n\t\tp, err := strconv.Atoi(proto)\n\t\tif err == nil {\n\t\t\tprotocols += p\n\t\t}\n\t}\n\n\treturn dataprovider.IPListEntry{\n\t\tIPOrNet:     strings.TrimSpace(r.Form.Get(\"ipornet\")),\n\t\tMode:        mode,\n\t\tProtocols:   protocols,\n\t\tDescription: r.Form.Get(\"description\"),\n\t}, nil\n}\n\nfunc getSFTPConfigsFromPostFields(r *http.Request) *dataprovider.SFTPDConfigs {\n\treturn &dataprovider.SFTPDConfigs{\n\t\tHostKeyAlgos:   r.Form[\"sftp_host_key_algos\"],\n\t\tPublicKeyAlgos: r.Form[\"sftp_pub_key_algos\"],\n\t\tKexAlgorithms:  r.Form[\"sftp_kex_algos\"],\n\t\tCiphers:        r.Form[\"sftp_ciphers\"],\n\t\tMACs:           r.Form[\"sftp_macs\"],\n\t}\n}\n\nfunc getACMEConfigsFromPostFields(r *http.Request) *dataprovider.ACMEConfigs {\n\tport, err := strconv.Atoi(r.Form.Get(\"acme_port\"))\n\tif err != nil {\n\t\tport = 80\n\t}\n\tvar protocols int\n\tfor _, val := range r.Form[\"acme_protocols\"] {\n\t\tswitch val {\n\t\tcase \"1\":\n\t\t\tprotocols++\n\t\tcase \"2\":\n\t\t\tprotocols += 2\n\t\tcase \"3\":\n\t\t\tprotocols += 4\n\t\t}\n\t}\n\n\treturn &dataprovider.ACMEConfigs{\n\t\tDomain:          strings.TrimSpace(r.Form.Get(\"acme_domain\")),\n\t\tEmail:           strings.TrimSpace(r.Form.Get(\"acme_email\")),\n\t\tHTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: port},\n\t\tProtocols:       protocols,\n\t}\n}\n\nfunc getSMTPConfigsFromPostFields(r *http.Request) *dataprovider.SMTPConfigs {\n\tport, err := strconv.Atoi(r.Form.Get(\"smtp_port\"))\n\tif err != nil {\n\t\tport = 587\n\t}\n\tauthType, err := strconv.Atoi(r.Form.Get(\"smtp_auth\"))\n\tif err != nil {\n\t\tauthType = 0\n\t}\n\tencryption, err := strconv.Atoi(r.Form.Get(\"smtp_encryption\"))\n\tif err != nil {\n\t\tencryption = 0\n\t}\n\tdebug := 0\n\tif r.Form.Get(\"smtp_debug\") != \"\" {\n\t\tdebug = 1\n\t}\n\toauth2Provider := 0\n\tif r.Form.Get(\"smtp_oauth2_provider\") == \"1\" {\n\t\toauth2Provider = 1\n\t}\n\treturn &dataprovider.SMTPConfigs{\n\t\tHost:       strings.TrimSpace(r.Form.Get(\"smtp_host\")),\n\t\tPort:       port,\n\t\tFrom:       strings.TrimSpace(r.Form.Get(\"smtp_from\")),\n\t\tUser:       strings.TrimSpace(r.Form.Get(\"smtp_username\")),\n\t\tPassword:   getSecretFromFormField(r, \"smtp_password\"),\n\t\tAuthType:   authType,\n\t\tEncryption: encryption,\n\t\tDomain:     strings.TrimSpace(r.Form.Get(\"smtp_domain\")),\n\t\tDebug:      debug,\n\t\tOAuth2: dataprovider.SMTPOAuth2{\n\t\t\tProvider:     oauth2Provider,\n\t\t\tTenant:       strings.TrimSpace(r.Form.Get(\"smtp_oauth2_tenant\")),\n\t\t\tClientID:     strings.TrimSpace(r.Form.Get(\"smtp_oauth2_client_id\")),\n\t\t\tClientSecret: getSecretFromFormField(r, \"smtp_oauth2_client_secret\"),\n\t\t\tRefreshToken: getSecretFromFormField(r, \"smtp_oauth2_refresh_token\"),\n\t\t},\n\t}\n}\n\nfunc getImageInputBytes(r *http.Request, fieldName, removeFieldName string, defaultVal []byte) ([]byte, error) {\n\tvar result []byte\n\tremove := r.Form.Get(removeFieldName)\n\tif remove == \"\" || remove == \"0\" {\n\t\tresult = defaultVal\n\t}\n\tf, _, err := r.FormFile(fieldName)\n\tif err != nil {\n\t\tif errors.Is(err, http.ErrMissingFile) {\n\t\t\treturn result, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\n\treturn io.ReadAll(f)\n}\n\nfunc getBrandingConfigFromPostFields(r *http.Request, config *dataprovider.BrandingConfigs) (\n\t*dataprovider.BrandingConfigs, error,\n) {\n\tif config == nil {\n\t\tconfig = &dataprovider.BrandingConfigs{}\n\t}\n\tadminLogo, err := getImageInputBytes(r, \"branding_webadmin_logo\", \"branding_webadmin_logo_remove\", config.WebAdmin.Logo)\n\tif err != nil {\n\t\treturn nil, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tadminFavicon, err := getImageInputBytes(r, \"branding_webadmin_favicon\", \"branding_webadmin_favicon_remove\",\n\t\tconfig.WebAdmin.Favicon)\n\tif err != nil {\n\t\treturn nil, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tclientLogo, err := getImageInputBytes(r, \"branding_webclient_logo\", \"branding_webclient_logo_remove\",\n\t\tconfig.WebClient.Logo)\n\tif err != nil {\n\t\treturn nil, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tclientFavicon, err := getImageInputBytes(r, \"branding_webclient_favicon\", \"branding_webclient_favicon_remove\",\n\t\tconfig.WebClient.Favicon)\n\tif err != nil {\n\t\treturn nil, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\n\tbranding := &dataprovider.BrandingConfigs{\n\t\tWebAdmin: dataprovider.BrandingConfig{\n\t\t\tName:           strings.TrimSpace(r.Form.Get(\"branding_webadmin_name\")),\n\t\t\tShortName:      strings.TrimSpace(r.Form.Get(\"branding_webadmin_short_name\")),\n\t\t\tLogo:           adminLogo,\n\t\t\tFavicon:        adminFavicon,\n\t\t\tDisclaimerName: strings.TrimSpace(r.Form.Get(\"branding_webadmin_disclaimer_name\")),\n\t\t\tDisclaimerURL:  strings.TrimSpace(r.Form.Get(\"branding_webadmin_disclaimer_url\")),\n\t\t},\n\t\tWebClient: dataprovider.BrandingConfig{\n\t\t\tName:           strings.TrimSpace(r.Form.Get(\"branding_webclient_name\")),\n\t\t\tShortName:      strings.TrimSpace(r.Form.Get(\"branding_webclient_short_name\")),\n\t\t\tLogo:           clientLogo,\n\t\t\tFavicon:        clientFavicon,\n\t\t\tDisclaimerName: strings.TrimSpace(r.Form.Get(\"branding_webclient_disclaimer_name\")),\n\t\t\tDisclaimerURL:  strings.TrimSpace(r.Form.Get(\"branding_webclient_disclaimer_url\")),\n\t\t},\n\t}\n\treturn branding, nil\n}\n\nfunc (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tif !smtp.IsEnabled() {\n\t\ts.renderNotFoundPage(w, r, errors.New(\"this page does not exist\"))\n\t\treturn\n\t}\n\ts.renderForgotPwdPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\terr := r.ParseForm()\n\tif err != nil {\n\t\ts.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\terr = handleForgotPassword(r, r.Form.Get(\"username\"), true)\n\tif err != nil {\n\t\ts.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric))\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminResetPwdPath, http.StatusFound)\n}\n\nfunc (s *httpdServer) handleWebAdminPasswordReset(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tif !smtp.IsEnabled() {\n\t\ts.renderNotFoundPage(w, r, errors.New(\"this page does not exist\"))\n\t\treturn\n\t}\n\ts.renderResetPwdPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebAdminTwoFactor(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderTwoFactorPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebAdminTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderTwoFactorRecoveryPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebAdminMFA(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderMFAPage(w, r)\n}\n\nfunc (s *httpdServer) handleWebAdminProfile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderProfilePage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderChangePasswordPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\terr := r.ParseForm()\n\tif err != nil {\n\t\ts.renderProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tadmin, err := dataprovider.AdminExists(claims.Username)\n\tif err != nil {\n\t\ts.renderProfilePage(w, r, err)\n\t\treturn\n\t}\n\tadmin.Filters.AllowAPIKeyAuth = r.Form.Get(\"allow_api_key_auth\") != \"\"\n\tadmin.Email = r.Form.Get(\"email\")\n\tadmin.Description = r.Form.Get(\"description\")\n\terr = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr, admin.Role)\n\tif err != nil {\n\t\ts.renderProfilePage(w, r, err)\n\t\treturn\n\t}\n\ts.renderMessagePage(w, r, util.I18nProfileTitle, http.StatusOK, nil, util.I18nProfileUpdated)\n}\n\nfunc (s *httpdServer) handleWebMaintenance(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderMaintenancePage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\terr = r.ParseMultipartForm(MaxRestoreSize)\n\tif err != nil {\n\t\ts.renderMaintenancePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\trestoreMode, err := strconv.Atoi(r.Form.Get(\"mode\"))\n\tif err != nil {\n\t\ts.renderMaintenancePage(w, r, err)\n\t\treturn\n\t}\n\tscanQuota, err := strconv.Atoi(r.Form.Get(\"quota\"))\n\tif err != nil {\n\t\ts.renderMaintenancePage(w, r, err)\n\t\treturn\n\t}\n\tbackupFile, _, err := r.FormFile(\"backup_file\")\n\tif err != nil {\n\t\ts.renderMaintenancePage(w, r, util.NewI18nError(err, util.I18nErrorBackupFile))\n\t\treturn\n\t}\n\tdefer backupFile.Close()\n\n\tbackupContent, err := io.ReadAll(backupFile)\n\tif err != nil || len(backupContent) == 0 {\n\t\tif len(backupContent) == 0 {\n\t\t\terr = errors.New(\"backup file size must be greater than 0\")\n\t\t}\n\t\ts.renderMaintenancePage(w, r, util.NewI18nError(err, util.I18nErrorBackupFile))\n\t\treturn\n\t}\n\n\tif err := restoreBackup(backupContent, \"\", scanQuota, restoreMode, claims.Username, ipAddr, claims.Role); err != nil {\n\t\ts.renderMaintenancePage(w, r, util.NewI18nError(err, util.I18nErrorRestore))\n\t\treturn\n\t}\n\n\ts.renderMessagePage(w, r, util.I18nMaintenanceTitle, http.StatusOK, nil, util.I18nBackupOK)\n}\n\nfunc getAllAdmins(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, nil, util.I18nErrorInvalidToken, http.StatusForbidden)\n\t\treturn\n\t}\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tresults, err := dataprovider.GetAdmins(limit, offset, dataprovider.OrderASC)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\treturn data, len(results), err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdata := s.getBasePageData(util.I18nAdminsTitle, webAdminsPath, w, r)\n\trenderAdminTemplate(w, templateAdmins, data)\n}\n\nfunc (s *httpdServer) handleWebAdminSetupGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tif dataprovider.HasAdmin() {\n\t\thttp.Redirect(w, r, webAdminLoginPath, http.StatusFound)\n\t\treturn\n\t}\n\ts.renderAdminSetupPage(w, r, \"\", nil)\n}\n\nfunc (s *httpdServer) handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tadmin := &dataprovider.Admin{\n\t\tStatus:      1,\n\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t}\n\ts.renderAddUpdateAdminPage(w, r, admin, nil, true)\n}\n\nfunc (s *httpdServer) handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tusername := getURLParam(r, \"username\")\n\tadmin, err := dataprovider.AdminExists(username)\n\tif err == nil {\n\t\ts.renderAddUpdateAdminPage(w, r, &admin, nil, false)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tadmin, err := getAdminFromPostFields(r)\n\tif err != nil {\n\t\ts.renderAddUpdateAdminPage(w, r, &admin, err, true)\n\t\treturn\n\t}\n\tif admin.Password == \"\" {\n\t\t// Administrators can be used with OpenID Connect or for authentication\n\t\t// via API key, in these cases the password is not necessary, we create\n\t\t// a non-usable one. This feature is only useful for WebAdmin, in REST\n\t\t// API you can create an unusable password externally.\n\t\tadmin.Password = util.GenerateUniqueID()\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\terr = dataprovider.AddAdmin(&admin, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderAddUpdateAdminPage(w, r, &admin, err, true)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminsPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tusername := getURLParam(r, \"username\")\n\tadmin, err := dataprovider.AdminExists(username)\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\n\tupdatedAdmin, err := getAdminFromPostFields(r)\n\tif err != nil {\n\t\ts.renderAddUpdateAdminPage(w, r, &updatedAdmin, err, false)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tupdatedAdmin.ID = admin.ID\n\tupdatedAdmin.Username = admin.Username\n\tif updatedAdmin.Password == \"\" {\n\t\tupdatedAdmin.Password = admin.Password\n\t}\n\tupdatedAdmin.Filters.TOTPConfig = admin.Filters.TOTPConfig\n\tupdatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderAddUpdateAdminPage(w, r, &updatedAdmin, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken), false)\n\t\treturn\n\t}\n\tif username == claims.Username {\n\t\tif !util.SlicesEqual(admin.Permissions, updatedAdmin.Permissions) {\n\t\t\ts.renderAddUpdateAdminPage(w, r, &updatedAdmin,\n\t\t\t\tutil.NewI18nError(errors.New(\"you cannot change your permissions\"),\n\t\t\t\t\tutil.I18nErrorAdminSelfPerms,\n\t\t\t\t), false)\n\t\t\treturn\n\t\t}\n\t\tif updatedAdmin.Status == 0 {\n\t\t\ts.renderAddUpdateAdminPage(w, r, &updatedAdmin,\n\t\t\t\tutil.NewI18nError(errors.New(\"you cannot disable yourself\"),\n\t\t\t\t\tutil.I18nErrorAdminSelfDisable,\n\t\t\t\t), false)\n\t\t\treturn\n\t\t}\n\t\tif updatedAdmin.Role != claims.Role {\n\t\t\ts.renderAddUpdateAdminPage(w, r, &updatedAdmin,\n\t\t\t\tutil.NewI18nError(\n\t\t\t\t\terrors.New(\"you cannot add/change your role\"),\n\t\t\t\t\tutil.I18nErrorAdminSelfRole,\n\t\t\t\t), false)\n\t\t\treturn\n\t\t}\n\t\tupdatedAdmin.Filters.RequirePasswordChange = admin.Filters.RequirePasswordChange\n\t\tupdatedAdmin.Filters.RequireTwoFactor = admin.Filters.RequireTwoFactor\n\t}\n\terr = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderAddUpdateAdminPage(w, r, &updatedAdmin, err, false)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminsPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebDefenderPage(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tdata := defenderHostsPage{\n\t\tbasePage:         s.getBasePageData(util.I18nDefenderTitle, webDefenderPath, w, r),\n\t\tDefenderHostsURL: webDefenderHostsPath,\n\t}\n\n\trenderAdminTemplate(w, templateDefender, data)\n}\n\nfunc getAllUsers(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, nil, util.I18nErrorInvalidToken, http.StatusForbidden)\n\t\treturn\n\t}\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tresults, err := dataprovider.GetUsers(limit, offset, dataprovider.OrderASC, claims.Role)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\treturn data, len(results), err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleGetWebUsers(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tdata := s.getBasePageData(util.I18nUsersTitle, webUsersPath, w, r)\n\trenderAdminTemplate(w, templateUsers, data)\n}\n\nfunc (s *httpdServer) handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tif r.URL.Query().Get(\"from\") != \"\" {\n\t\tname := r.URL.Query().Get(\"from\")\n\t\tfolder, err := dataprovider.GetFolderByName(name)\n\t\tif err == nil {\n\t\t\tfolder.FsConfig.SetEmptySecrets()\n\t\t\ts.renderFolderPage(w, r, folder, folderPageModeTemplate, nil)\n\t\t} else if errors.Is(err, util.ErrNotFound) {\n\t\t\ts.renderNotFoundPage(w, r, err)\n\t\t} else {\n\t\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\t}\n\t} else {\n\t\tfolder := vfs.BaseVirtualFolder{}\n\t\ts.renderFolderPage(w, r, folder, folderPageModeTemplate, nil)\n\t}\n}\n\nfunc (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\ttemplateFolder := vfs.BaseVirtualFolder{}\n\terr = r.ParseMultipartForm(maxRequestSize)\n\tif err != nil {\n\t\ts.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, util.NewI18nError(err, util.I18nErrorInvalidForm), \"\")\n\t\treturn\n\t}\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\n\ttemplateFolder.MappedPath = r.Form.Get(\"mapped_path\")\n\ttemplateFolder.Description = r.Form.Get(\"description\")\n\tfsConfig, err := getFsConfigFromPostFields(r)\n\tif err != nil {\n\t\ts.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, err, \"\")\n\t\treturn\n\t}\n\ttemplateFolder.FsConfig = fsConfig\n\n\tvar dump dataprovider.BackupData\n\n\tfoldersFields := getFoldersForTemplate(r)\n\tfor _, tmpl := range foldersFields {\n\t\tf := getFolderFromTemplate(templateFolder, tmpl)\n\t\tif err := dataprovider.ValidateFolder(&f); err != nil {\n\t\t\ts.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, err, \"\")\n\t\t\treturn\n\t\t}\n\t\tdump.Folders = append(dump.Folders, f)\n\t}\n\n\tif len(dump.Folders) == 0 {\n\t\ts.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest,\n\t\t\tutil.NewI18nError(\n\t\t\t\terrors.New(\"no valid folder defined, unable to complete the requested action\"),\n\t\t\t\tutil.I18nErrorFolderTemplate,\n\t\t\t), \"\")\n\t\treturn\n\t}\n\tif err = RestoreFolders(dump.Folders, \"\", 1, 0, claims.Username, ipAddr, claims.Role); err != nil {\n\t\ts.renderMessagePage(w, r, util.I18nTemplateFolderTitle, getRespStatus(err), err, \"\")\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webFoldersPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ttokenAdmin := getAdminFromToken(r)\n\tadmin, err := dataprovider.AdminExists(tokenAdmin.Username)\n\tif err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, fmt.Errorf(\"unable to get the admin %q: %w\", tokenAdmin.Username, err))\n\t\treturn\n\t}\n\tif r.URL.Query().Get(\"from\") != \"\" {\n\t\tusername := r.URL.Query().Get(\"from\")\n\t\tuser, err := dataprovider.UserExists(username, admin.Role)\n\t\tif err == nil {\n\t\t\tuser.SetEmptySecrets()\n\t\t\tuser.PublicKeys = nil\n\t\t\tuser.Email = \"\"\n\t\t\tuser.Filters.AdditionalEmails = nil\n\t\t\tuser.Description = \"\"\n\t\t\tif user.ExpirationDate == 0 && admin.Filters.Preferences.DefaultUsersExpiration > 0 {\n\t\t\t\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(admin.Filters.Preferences.DefaultUsersExpiration)))\n\t\t\t}\n\t\t\ts.renderUserPage(w, r, &user, userPageModeTemplate, nil, &admin)\n\t\t} else if errors.Is(err, util.ErrNotFound) {\n\t\t\ts.renderNotFoundPage(w, r, err)\n\t\t} else {\n\t\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\t}\n\t} else {\n\t\tuser := dataprovider.User{BaseUser: sdk.BaseUser{\n\t\t\tStatus: 1,\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t}}\n\t\tif admin.Filters.Preferences.DefaultUsersExpiration > 0 {\n\t\t\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(admin.Filters.Preferences.DefaultUsersExpiration)))\n\t\t}\n\t\ts.renderUserPage(w, r, &user, userPageModeTemplate, nil, &admin)\n\t}\n}\n\nfunc (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\ttemplateUser, err := getUserFromPostFields(r)\n\tif err != nil {\n\t\ts.renderMessagePage(w, r, util.I18nTemplateUserTitle, http.StatusBadRequest, err, \"\")\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\n\tvar dump dataprovider.BackupData\n\n\tuserTmplFields := getUsersForTemplate(r)\n\tfor _, tmpl := range userTmplFields {\n\t\tu := getUserFromTemplate(templateUser, tmpl)\n\t\tif err := dataprovider.ValidateUser(&u); err != nil {\n\t\t\ts.renderMessagePage(w, r, util.I18nTemplateUserTitle, http.StatusBadRequest, err, \"\")\n\t\t\treturn\n\t\t}\n\t\tif claims.Role != \"\" {\n\t\t\tu.Role = claims.Role\n\t\t}\n\t\tdump.Users = append(dump.Users, u)\n\t}\n\n\tif len(dump.Users) == 0 {\n\t\ts.renderMessagePage(w, r, util.I18nTemplateUserTitle,\n\t\t\thttp.StatusBadRequest, util.NewI18nError(\n\t\t\t\terrors.New(\"no valid user defined, unable to complete the requested action\"),\n\t\t\t\tutil.I18nErrorUserTemplate,\n\t\t\t), \"\")\n\t\treturn\n\t}\n\tif err = RestoreUsers(dump.Users, \"\", 1, 0, claims.Username, ipAddr, claims.Role); err != nil {\n\t\ts.renderMessagePage(w, r, util.I18nTemplateUserTitle, getRespStatus(err), err, \"\")\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webUsersPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ttokenAdmin := getAdminFromToken(r)\n\tadmin, err := dataprovider.AdminExists(tokenAdmin.Username)\n\tif err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, fmt.Errorf(\"unable to get the admin %q: %w\", tokenAdmin.Username, err))\n\t\treturn\n\t}\n\tuser := dataprovider.User{BaseUser: sdk.BaseUser{\n\t\tStatus: 1,\n\t\tPermissions: map[string][]string{\n\t\t\t\"/\": {dataprovider.PermAny},\n\t\t}},\n\t}\n\tif admin.Filters.Preferences.DefaultUsersExpiration > 0 {\n\t\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(admin.Filters.Preferences.DefaultUsersExpiration)))\n\t}\n\ts.renderUserPage(w, r, &user, userPageModeAdd, nil, &admin)\n}\n\nfunc (s *httpdServer) handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tusername := getURLParam(r, \"username\")\n\tuser, err := dataprovider.UserExists(username, claims.Role)\n\tif err == nil {\n\t\ts.renderUserPage(w, r, &user, userPageModeUpdate, nil, nil)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tuser, err := getUserFromPostFields(r)\n\tif err != nil {\n\t\ts.renderUserPage(w, r, &user, userPageModeAdd, err, nil)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tuser = getUserFromTemplate(user, userTemplateFields{\n\t\tUsername:         user.Username,\n\t\tPassword:         user.Password,\n\t\tPublicKeys:       user.PublicKeys,\n\t\tRequirePwdChange: user.Filters.RequirePasswordChange,\n\t})\n\tif claims.Role != \"\" {\n\t\tuser.Role = claims.Role\n\t}\n\tuser.Filters.RecoveryCodes = nil\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled: false,\n\t}\n\terr = dataprovider.AddUser(&user, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderUserPage(w, r, &user, userPageModeAdd, err, nil)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webUsersPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tusername := getURLParam(r, \"username\")\n\tuser, err := dataprovider.UserExists(username, claims.Role)\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tupdatedUser, err := getUserFromPostFields(r)\n\tif err != nil {\n\t\ts.renderUserPage(w, r, &user, userPageModeUpdate, err, nil)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tupdatedUser.ID = user.ID\n\tupdatedUser.Username = user.Username\n\tupdatedUser.Filters.RecoveryCodes = user.Filters.RecoveryCodes\n\tupdatedUser.Filters.TOTPConfig = user.Filters.TOTPConfig\n\tupdatedUser.LastPasswordChange = user.LastPasswordChange\n\tupdatedUser.SetEmptySecretsIfNil()\n\tif updatedUser.Password == redactedSecret {\n\t\tupdatedUser.Password = user.Password\n\t}\n\tupdateEncryptedSecrets(&updatedUser.FsConfig, &user.FsConfig)\n\n\tupdatedUser = getUserFromTemplate(updatedUser, userTemplateFields{\n\t\tUsername:         updatedUser.Username,\n\t\tPassword:         updatedUser.Password,\n\t\tPublicKeys:       updatedUser.PublicKeys,\n\t\tRequirePwdChange: updatedUser.Filters.RequirePasswordChange,\n\t})\n\tif claims.Role != \"\" {\n\t\tupdatedUser.Role = claims.Role\n\t}\n\n\terr = dataprovider.UpdateUser(&updatedUser, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderUserPage(w, r, &updatedUser, userPageModeUpdate, err, nil)\n\t\treturn\n\t}\n\tif r.Form.Get(\"disconnect\") != \"\" {\n\t\tdisconnectUser(user.Username, claims.Username, claims.Role)\n\t}\n\thttp.Redirect(w, r, webUsersPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebGetStatus(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tdata := statusPage{\n\t\tbasePage: s.getBasePageData(util.I18nStatusTitle, webStatusPath, w, r),\n\t\tStatus:   getServicesStatus(),\n\t}\n\trenderAdminTemplate(w, templateStatus, data)\n}\n\nfunc (s *httpdServer) handleWebGetConnections(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\n\tdata := s.getBasePageData(util.I18nSessionsTitle, webConnectionsPath, w, r)\n\trenderAdminTemplate(w, templateConnections, data)\n}\n\nfunc (s *httpdServer) handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderFolderPage(w, r, vfs.BaseVirtualFolder{}, folderPageModeAdd, nil)\n}\n\nfunc (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tfolder := vfs.BaseVirtualFolder{}\n\terr = r.ParseMultipartForm(maxRequestSize)\n\tif err != nil {\n\t\ts.renderFolderPage(w, r, folder, folderPageModeAdd, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tfolder.MappedPath = strings.TrimSpace(r.Form.Get(\"mapped_path\"))\n\tfolder.Name = strings.TrimSpace(r.Form.Get(\"name\"))\n\tfolder.Description = r.Form.Get(\"description\")\n\tfsConfig, err := getFsConfigFromPostFields(r)\n\tif err != nil {\n\t\ts.renderFolderPage(w, r, folder, folderPageModeAdd, err)\n\t\treturn\n\t}\n\tfolder.FsConfig = fsConfig\n\tfolder = getFolderFromTemplate(folder, folder.Name)\n\n\terr = dataprovider.AddFolder(&folder, claims.Username, ipAddr, claims.Role)\n\tif err == nil {\n\t\thttp.Redirect(w, r, webFoldersPath, http.StatusSeeOther)\n\t} else {\n\t\ts.renderFolderPage(w, r, folder, folderPageModeAdd, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tname := getURLParam(r, \"name\")\n\tfolder, err := dataprovider.GetFolderByName(name)\n\tif err == nil {\n\t\ts.renderFolderPage(w, r, folder, folderPageModeUpdate, nil)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\tfolder, err := dataprovider.GetFolderByName(name)\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\n\terr = r.ParseMultipartForm(maxRequestSize)\n\tif err != nil {\n\t\ts.renderFolderPage(w, r, folder, folderPageModeUpdate, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tfsConfig, err := getFsConfigFromPostFields(r)\n\tif err != nil {\n\t\ts.renderFolderPage(w, r, folder, folderPageModeUpdate, err)\n\t\treturn\n\t}\n\tupdatedFolder := vfs.BaseVirtualFolder{\n\t\tMappedPath:  strings.TrimSpace(r.Form.Get(\"mapped_path\")),\n\t\tDescription: r.Form.Get(\"description\"),\n\t}\n\tupdatedFolder.ID = folder.ID\n\tupdatedFolder.Name = folder.Name\n\tupdatedFolder.FsConfig = fsConfig\n\tupdatedFolder.FsConfig.SetEmptySecretsIfNil()\n\tupdateEncryptedSecrets(&updatedFolder.FsConfig, &folder.FsConfig)\n\n\tupdatedFolder = getFolderFromTemplate(updatedFolder, updatedFolder.Name)\n\n\terr = dataprovider.UpdateFolder(&updatedFolder, folder.Users, folder.Groups, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderFolderPage(w, r, updatedFolder, folderPageModeUpdate, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webFoldersPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]vfs.BaseVirtualFolder, error) {\n\tfolders := make([]vfs.BaseVirtualFolder, 0, 50)\n\tfor {\n\t\tf, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, minimal)\n\t\tif err != nil {\n\t\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\t\treturn folders, err\n\t\t}\n\t\tfolders = append(folders, f...)\n\t\tif len(f) < limit {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn folders, nil\n}\n\nfunc getAllFolders(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tresults, err := dataprovider.GetFolders(limit, offset, dataprovider.OrderASC, false)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\treturn data, len(results), err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdata := s.getBasePageData(util.I18nFoldersTitle, webFoldersPath, w, r)\n\trenderAdminTemplate(w, templateFolders, data)\n}\n\nfunc (s *httpdServer) getWebGroups(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Group, error) {\n\tgroups := make([]dataprovider.Group, 0, 50)\n\tfor {\n\t\tf, err := dataprovider.GetGroups(limit, len(groups), dataprovider.OrderASC, minimal)\n\t\tif err != nil {\n\t\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\t\treturn groups, err\n\t\t}\n\t\tgroups = append(groups, f...)\n\t\tif len(f) < limit {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn groups, nil\n}\n\nfunc getAllGroups(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tresults, err := dataprovider.GetGroups(limit, offset, dataprovider.OrderASC, false)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\treturn data, len(results), err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdata := s.getBasePageData(util.I18nGroupsTitle, webGroupsPath, w, r)\n\trenderAdminTemplate(w, templateGroups, data)\n}\n\nfunc (s *httpdServer) handleWebAddGroupGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderGroupPage(w, r, dataprovider.Group{}, genericPageModeAdd, nil)\n}\n\nfunc (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tgroup, err := getGroupFromPostFields(r)\n\tif err != nil {\n\t\ts.renderGroupPage(w, r, group, genericPageModeAdd, err)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\terr = dataprovider.AddGroup(&group, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderGroupPage(w, r, group, genericPageModeAdd, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webGroupsPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebUpdateGroupGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tname := getURLParam(r, \"name\")\n\tgroup, err := dataprovider.GroupExists(name)\n\tif err == nil {\n\t\ts.renderGroupPage(w, r, group, genericPageModeUpdate, nil)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\tgroup, err := dataprovider.GroupExists(name)\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tupdatedGroup, err := getGroupFromPostFields(r)\n\tif err != nil {\n\t\ts.renderGroupPage(w, r, group, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tupdatedGroup.ID = group.ID\n\tupdatedGroup.Name = group.Name\n\tupdatedGroup.SetEmptySecretsIfNil()\n\n\tupdateEncryptedSecrets(&updatedGroup.UserSettings.FsConfig, &group.UserSettings.FsConfig)\n\n\terr = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderGroupPage(w, r, updatedGroup, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webGroupsPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) getWebEventActions(w http.ResponseWriter, r *http.Request, limit int, minimal bool,\n) ([]dataprovider.BaseEventAction, error) {\n\tactions := make([]dataprovider.BaseEventAction, 0, limit)\n\tfor {\n\t\tres, err := dataprovider.GetEventActions(limit, len(actions), dataprovider.OrderASC, minimal)\n\t\tif err != nil {\n\t\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\t\treturn actions, err\n\t\t}\n\t\tactions = append(actions, res...)\n\t\tif len(res) < limit {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn actions, nil\n}\n\nfunc getAllActions(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tresults, err := dataprovider.GetEventActions(limit, offset, dataprovider.OrderASC, false)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\treturn data, len(results), err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleWebGetEventActions(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdata := s.getBasePageData(util.I18nActionsTitle, webAdminEventActionsPath, w, r)\n\trenderAdminTemplate(w, templateEventActions, data)\n}\n\nfunc (s *httpdServer) handleWebAddEventActionGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\taction := dataprovider.BaseEventAction{\n\t\tType: dataprovider.ActionTypeHTTP,\n\t}\n\ts.renderEventActionPage(w, r, action, genericPageModeAdd, nil)\n}\n\nfunc (s *httpdServer) handleWebAddEventActionPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\taction, err := getEventActionFromPostFields(r)\n\tif err != nil {\n\t\ts.renderEventActionPage(w, r, action, genericPageModeAdd, err)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tif err = dataprovider.AddEventAction(&action, claims.Username, ipAddr, claims.Role); err != nil {\n\t\ts.renderEventActionPage(w, r, action, genericPageModeAdd, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebUpdateEventActionGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tname := getURLParam(r, \"name\")\n\taction, err := dataprovider.EventActionExists(name)\n\tif err == nil {\n\t\ts.renderEventActionPage(w, r, action, genericPageModeUpdate, nil)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\taction, err := dataprovider.EventActionExists(name)\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tupdatedAction, err := getEventActionFromPostFields(r)\n\tif err != nil {\n\t\ts.renderEventActionPage(w, r, updatedAction, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tupdatedAction.ID = action.ID\n\tupdatedAction.Name = action.Name\n\tupdatedAction.Options.SetEmptySecretsIfNil()\n\tswitch updatedAction.Type {\n\tcase dataprovider.ActionTypeHTTP:\n\t\tif updatedAction.Options.HTTPConfig.Password.IsNotPlainAndNotEmpty() {\n\t\t\tupdatedAction.Options.HTTPConfig.Password = action.Options.HTTPConfig.Password\n\t\t}\n\t}\n\terr = dataprovider.UpdateEventAction(&updatedAction, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderEventActionPage(w, r, updatedAction, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)\n}\n\nfunc getAllRules(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tresults, err := dataprovider.GetEventRules(limit, offset, dataprovider.OrderASC)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\treturn data, len(results), err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdata := s.getBasePageData(util.I18nRulesTitle, webAdminEventRulesPath, w, r)\n\trenderAdminTemplate(w, templateEventRules, data)\n}\n\nfunc (s *httpdServer) handleWebAddEventRuleGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\trule := dataprovider.EventRule{\n\t\tStatus:  1,\n\t\tTrigger: dataprovider.EventTriggerFsEvent,\n\t}\n\ts.renderEventRulePage(w, r, rule, genericPageModeAdd, nil)\n}\n\nfunc (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\trule, err := getEventRuleFromPostFields(r)\n\tif err != nil {\n\t\ts.renderEventRulePage(w, r, rule, genericPageModeAdd, err)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\terr = verifyCSRFToken(r, s.csrfTokenAuth)\n\tif err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tif err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr, claims.Role); err != nil {\n\t\ts.renderEventRulePage(w, r, rule, genericPageModeAdd, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebUpdateEventRuleGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tname := getURLParam(r, \"name\")\n\trule, err := dataprovider.EventRuleExists(name)\n\tif err == nil {\n\t\ts.renderEventRulePage(w, r, rule, genericPageModeUpdate, nil)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tname := getURLParam(r, \"name\")\n\trule, err := dataprovider.EventRuleExists(name)\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tupdatedRule, err := getEventRuleFromPostFields(r)\n\tif err != nil {\n\t\ts.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tupdatedRule.ID = rule.ID\n\tupdatedRule.Name = rule.Name\n\terr = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) getWebRoles(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Role, error) {\n\troles := make([]dataprovider.Role, 0, 10)\n\tfor {\n\t\tres, err := dataprovider.GetRoles(limit, len(roles), dataprovider.OrderASC, minimal)\n\t\tif err != nil {\n\t\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\t\treturn roles, err\n\t\t}\n\t\troles = append(roles, res...)\n\t\tif len(res) < limit {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn roles, nil\n}\n\nfunc getAllRoles(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tresults, err := dataprovider.GetRoles(limit, offset, dataprovider.OrderASC, false)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\treturn data, len(results), err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleWebGetRoles(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tdata := s.getBasePageData(util.I18nRolesTitle, webAdminRolesPath, w, r)\n\n\trenderAdminTemplate(w, templateRoles, data)\n}\n\nfunc (s *httpdServer) handleWebAddRoleGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderRolePage(w, r, dataprovider.Role{}, genericPageModeAdd, nil)\n}\n\nfunc (s *httpdServer) handleWebAddRolePost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\trole, err := getRoleFromPostFields(r)\n\tif err != nil {\n\t\ts.renderRolePage(w, r, role, genericPageModeAdd, err)\n\t\treturn\n\t}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\terr = dataprovider.AddRole(&role, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderRolePage(w, r, role, genericPageModeAdd, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebUpdateRoleGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\trole, err := dataprovider.RoleExists(getURLParam(r, \"name\"))\n\tif err == nil {\n\t\ts.renderRolePage(w, r, role, genericPageModeUpdate, nil)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\trole, err := dataprovider.RoleExists(getURLParam(r, \"name\"))\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\n\tupdatedRole, err := getRoleFromPostFields(r)\n\tif err != nil {\n\t\ts.renderRolePage(w, r, role, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tupdatedRole.ID = role.ID\n\tupdatedRole.Name = role.Name\n\terr = dataprovider.UpdateRole(&updatedRole, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderRolePage(w, r, updatedRole, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebGetEvents(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdata := eventsPage{\n\t\tbasePage:                s.getBasePageData(util.I18nEventsTitle, webEventsPath, w, r),\n\t\tFsEventsSearchURL:       webEventsFsSearchPath,\n\t\tProviderEventsSearchURL: webEventsProviderSearchPath,\n\t\tLogEventsSearchURL:      webEventsLogSearchPath,\n\t}\n\trenderAdminTemplate(w, templateEvents, data)\n}\n\nfunc (s *httpdServer) handleWebIPListsPage(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\trtlStatus, rtlProtocols := common.Config.GetRateLimitersStatus()\n\tdata := ipListsPage{\n\t\tbasePage:              s.getBasePageData(util.I18nIPListsTitle, webIPListsPath, w, r),\n\t\tRateLimitersStatus:    rtlStatus,\n\t\tRateLimitersProtocols: strings.Join(rtlProtocols, \", \"),\n\t\tIsAllowListEnabled:    common.Config.IsAllowListEnabled(),\n\t}\n\n\trenderAdminTemplate(w, templateIPLists, data)\n}\n\nfunc (s *httpdServer) handleWebAddIPListEntryGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlistType, _, err := getIPListPathParams(r)\n\tif err != nil {\n\t\ts.renderBadRequestPage(w, r, err)\n\t\treturn\n\t}\n\ts.renderIPListPage(w, r, dataprovider.IPListEntry{Type: listType}, genericPageModeAdd, nil)\n}\n\nfunc (s *httpdServer) handleWebAddIPListEntryPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlistType, _, err := getIPListPathParams(r)\n\tif err != nil {\n\t\ts.renderBadRequestPage(w, r, err)\n\t\treturn\n\t}\n\tentry, err := getIPListEntryFromPostFields(r, listType)\n\tif err != nil {\n\t\ts.renderIPListPage(w, r, entry, genericPageModeAdd, err)\n\t\treturn\n\t}\n\tentry.Type = listType\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\terr = dataprovider.AddIPListEntry(&entry, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderIPListPage(w, r, entry, genericPageModeAdd, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webIPListsPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebUpdateIPListEntryGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tlistType, ipOrNet, err := getIPListPathParams(r)\n\tif err != nil {\n\t\ts.renderBadRequestPage(w, r, err)\n\t\treturn\n\t}\n\tentry, err := dataprovider.IPListEntryExists(ipOrNet, listType)\n\tif err == nil {\n\t\ts.renderIPListPage(w, r, entry, genericPageModeUpdate, nil)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tlistType, ipOrNet, err := getIPListPathParams(r)\n\tif err != nil {\n\t\ts.renderBadRequestPage(w, r, err)\n\t\treturn\n\t}\n\tentry, err := dataprovider.IPListEntryExists(ipOrNet, listType)\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tupdatedEntry, err := getIPListEntryFromPostFields(r, listType)\n\tif err != nil {\n\t\ts.renderIPListPage(w, r, entry, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tupdatedEntry.Type = listType\n\tupdatedEntry.IPOrNet = ipOrNet\n\terr = dataprovider.UpdateIPListEntry(&updatedEntry, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderIPListPage(w, r, entry, genericPageModeUpdate, err)\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webIPListsPath, http.StatusSeeOther)\n}\n\nfunc (s *httpdServer) handleWebConfigs(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\ts.renderConfigsPage(w, r, configs, nil, 0)\n}\n\nfunc (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\ts.renderInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\terr = r.ParseMultipartForm(maxRequestSize)\n\tif err != nil {\n\t\ts.renderBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tdefer r.MultipartForm.RemoveAll() //nolint:errcheck\n\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tvar configSection int\n\tswitch r.Form.Get(\"form_action\") {\n\tcase \"sftp_submit\":\n\t\tconfigSection = 1\n\t\tsftpConfigs := getSFTPConfigsFromPostFields(r)\n\t\tconfigs.SFTPD = sftpConfigs\n\tcase \"acme_submit\":\n\t\tconfigSection = 2\n\t\tacmeConfigs := getACMEConfigsFromPostFields(r)\n\t\tconfigs.ACME = acmeConfigs\n\t\tif err := acme.GetCertificatesForConfig(acmeConfigs, configurationDir); err != nil {\n\t\t\tlogger.Info(logSender, \"\", \"unable to get ACME certificates: %v\", err)\n\t\t\ts.renderConfigsPage(w, r, configs, util.NewI18nError(err, util.I18nErrorACMEGeneric), configSection)\n\t\t\treturn\n\t\t}\n\tcase \"smtp_submit\":\n\t\tconfigSection = 3\n\t\tsmtpConfigs := getSMTPConfigsFromPostFields(r)\n\t\tupdateSMTPSecrets(smtpConfigs, configs.SMTP)\n\t\tconfigs.SMTP = smtpConfigs\n\tcase \"branding_submit\":\n\t\tconfigSection = 4\n\t\tbrandingConfigs, err := getBrandingConfigFromPostFields(r, configs.Branding)\n\t\tconfigs.Branding = brandingConfigs\n\t\tif err != nil {\n\t\t\tlogger.Info(logSender, \"\", \"unable to get branding config: %v\", err)\n\t\t\ts.renderConfigsPage(w, r, configs, err, configSection)\n\t\t\treturn\n\t\t}\n\tdefault:\n\t\ts.renderBadRequestPage(w, r, errors.New(\"unsupported form action\"))\n\t\treturn\n\t}\n\n\terr = dataprovider.UpdateConfigs(&configs, claims.Username, ipAddr, claims.Role)\n\tif err != nil {\n\t\ts.renderConfigsPage(w, r, configs, err, configSection)\n\t\treturn\n\t}\n\tpostConfigsUpdate(configSection, configs)\n\ts.renderMessagePage(w, r, util.I18nConfigsTitle, http.StatusOK, nil, util.I18nConfigsOK)\n}\n\nfunc postConfigsUpdate(section int, configs dataprovider.Configs) {\n\tswitch section {\n\tcase 3:\n\t\terr := configs.SMTP.TryDecrypt()\n\t\tif err == nil {\n\t\t\tsmtp.Activate(configs.SMTP)\n\t\t} else {\n\t\t\tlogger.Error(logSender, \"\", \"unable to decrypt SMTP configuration, cannot activate configuration: %v\", err)\n\t\t}\n\tcase 4:\n\t\tdbBrandingConfig.Set(configs.Branding)\n\t}\n}\n\nfunc (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tstateToken := r.URL.Query().Get(\"state\")\n\n\tstate, err := verifyOAuth2Token(s.csrfTokenAuth, stateToken, util.GetIPFromRemoteAddress(r.RemoteAddr))\n\tif err != nil {\n\t\ts.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusBadRequest, err, \"\")\n\t\treturn\n\t}\n\n\tpendingAuth, err := oauth2Mgr.getPendingAuth(state)\n\tif err != nil {\n\t\toauth2Mgr.removePendingAuth(state)\n\t\ts.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusInternalServerError,\n\t\t\tutil.NewI18nError(err, util.I18nOAuth2ErrorValidateState), \"\")\n\t\treturn\n\t}\n\toauth2Mgr.removePendingAuth(state)\n\n\toauth2Config := smtp.OAuth2Config{\n\t\tProvider:     pendingAuth.Provider,\n\t\tClientID:     pendingAuth.ClientID,\n\t\tClientSecret: pendingAuth.ClientSecret.GetPayload(),\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tcfg := oauth2Config.GetOAuth2()\n\tcfg.RedirectURL = pendingAuth.RedirectURL\n\ttoken, err := cfg.Exchange(ctx, r.URL.Query().Get(\"code\"), oauth2.VerifierOption(pendingAuth.Verifier))\n\tif err != nil {\n\t\ts.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusInternalServerError,\n\t\t\tutil.NewI18nError(err, util.I18nOAuth2ErrTokenExchange), \"\")\n\t\treturn\n\t}\n\tif token.RefreshToken == \"\" {\n\t\terrTxt := \"the OAuth2 provider returned an empty token. \" +\n\t\t\t\"Some providers only return the token when the user first authorizes. \" +\n\t\t\t\"If you have already registered SFTPGo with this user in the past, revoke access and try again. \" +\n\t\t\t\"This way you will invalidate the previous token\"\n\t\ts.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusBadRequest,\n\t\t\tutil.NewI18nError(errors.New(errTxt), util.I18nOAuth2ErrNoRefreshToken), \"\")\n\t\treturn\n\t}\n\ts.renderMessagePageWithString(w, r, util.I18nOAuth2Title, http.StatusOK, nil, util.I18nOAuth2OK,\n\t\tfmt.Sprintf(\"%q\", token.RefreshToken))\n}\n\nfunc updateSMTPSecrets(newConfigs, currentConfigs *dataprovider.SMTPConfigs) {\n\tif currentConfigs == nil {\n\t\tcurrentConfigs = &dataprovider.SMTPConfigs{}\n\t}\n\tif newConfigs.Password.IsNotPlainAndNotEmpty() {\n\t\tnewConfigs.Password = currentConfigs.Password\n\t}\n\tif newConfigs.OAuth2.ClientSecret.IsNotPlainAndNotEmpty() {\n\t\tnewConfigs.OAuth2.ClientSecret = currentConfigs.OAuth2.ClientSecret\n\t}\n\tif newConfigs.OAuth2.RefreshToken.IsNotPlainAndNotEmpty() {\n\t\tnewConfigs.OAuth2.RefreshToken = currentConfigs.OAuth2.RefreshToken\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/webclient.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-chi/render\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/jwt\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/smtp\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\ttemplateClientDir      = \"webclient\"\n\ttemplateClientBase     = \"base.html\"\n\ttemplateClientFiles    = \"files.html\"\n\ttemplateClientProfile  = \"profile.html\"\n\ttemplateClientMFA      = \"mfa.html\"\n\ttemplateClientEditFile = \"editfile.html\"\n\ttemplateClientShare    = \"share.html\"\n\ttemplateClientShares   = \"shares.html\"\n\ttemplateClientViewPDF  = \"viewpdf.html\"\n\ttemplateShareLogin     = \"sharelogin.html\"\n\ttemplateShareDownload  = \"sharedownload.html\"\n\ttemplateUploadToShare  = \"shareupload.html\"\n)\n\n// condResult is the result of an HTTP request precondition check.\n// See https://tools.ietf.org/html/rfc7232 section 3.\ntype condResult int\n\nconst (\n\tcondNone condResult = iota\n\tcondTrue\n\tcondFalse\n)\n\nvar (\n\tclientTemplates = make(map[string]*template.Template)\n\tunixEpochTime   = time.Unix(0, 0)\n)\n\n// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).\nfunc isZeroTime(t time.Time) bool {\n\treturn t.IsZero() || t.Equal(unixEpochTime)\n}\n\ntype baseClientPage struct {\n\tcommonBasePage\n\tTitle           string\n\tCurrentURL      string\n\tFilesURL        string\n\tSharesURL       string\n\tShareURL        string\n\tProfileURL      string\n\tPingURL         string\n\tChangePwdURL    string\n\tLogoutURL       string\n\tLoginURL        string\n\tEditURL         string\n\tMFAURL          string\n\tCSRFToken       string\n\tLoggedUser      *dataprovider.User\n\tIsLoggedToShare bool\n\tBranding        UIBranding\n\tLanguages       []string\n}\n\ntype dirMapping struct {\n\tDirName string\n\tHref    string\n}\n\ntype viewPDFPage struct {\n\tcommonBasePage\n\tTitle     string\n\tURL       string\n\tBranding  UIBranding\n\tLanguages []string\n}\n\ntype editFilePage struct {\n\tbaseClientPage\n\tCurrentDir string\n\tFileURL    string\n\tPath       string\n\tName       string\n\tReadOnly   bool\n\tData       string\n}\n\ntype filesPage struct {\n\tbaseClientPage\n\tCurrentDir         string\n\tDirsURL            string\n\tFileActionsURL     string\n\tCheckExistURL      string\n\tDownloadURL        string\n\tViewPDFURL         string\n\tFileURL            string\n\tTasksURL           string\n\tCanAddFiles        bool\n\tCanCreateDirs      bool\n\tCanRename          bool\n\tCanDelete          bool\n\tCanDownload        bool\n\tCanShare           bool\n\tCanCopy            bool\n\tShareUploadBaseURL string\n\tError              *util.I18nError\n\tPaths              []dirMapping\n\tQuotaUsage         *userQuotaUsage\n\tKeepAliveInterval  int\n}\n\ntype shareLoginPage struct {\n\tcommonBasePage\n\tCurrentURL    string\n\tError         *util.I18nError\n\tCSRFToken     string\n\tTitle         string\n\tBranding      UIBranding\n\tLanguages     []string\n\tCheckRedirect bool\n}\n\ntype shareDownloadPage struct {\n\tbaseClientPage\n\tDownloadLink string\n}\n\ntype shareUploadPage struct {\n\tbaseClientPage\n\tShare          *dataprovider.Share\n\tUploadBasePath string\n}\n\ntype clientMessagePage struct {\n\tbaseClientPage\n\tError   *util.I18nError\n\tSuccess string\n\tText    string\n}\n\ntype clientProfilePage struct {\n\tbaseClientPage\n\tPublicKeys             []string\n\tTLSCerts               []string\n\tCanSubmit              bool\n\tAllowAPIKeyAuth        bool\n\tEmail                  string\n\tAdditionalEmails       []string\n\tAdditionalEmailsString string\n\tDescription            string\n\tError                  *util.I18nError\n}\n\ntype changeClientPasswordPage struct {\n\tbaseClientPage\n\tError *util.I18nError\n}\n\ntype clientMFAPage struct {\n\tbaseClientPage\n\tTOTPConfigs       []string\n\tTOTPConfig        dataprovider.UserTOTPConfig\n\tGenerateTOTPURL   string\n\tValidateTOTPURL   string\n\tSaveTOTPURL       string\n\tRecCodesURL       string\n\tProtocols         []string\n\tRequiredProtocols []string\n}\n\ntype clientSharesPage struct {\n\tbaseClientPage\n\tBasePublicSharesURL string\n\tBaseURL             string\n}\n\ntype clientSharePage struct {\n\tbaseClientPage\n\tShare *dataprovider.Share\n\tError *util.I18nError\n\tIsAdd bool\n}\n\ntype userQuotaUsage struct {\n\tQuotaSize                int64\n\tQuotaFiles               int\n\tUsedQuotaSize            int64\n\tUsedQuotaFiles           int\n\tUploadDataTransfer       int64\n\tDownloadDataTransfer     int64\n\tTotalDataTransfer        int64\n\tUsedUploadDataTransfer   int64\n\tUsedDownloadDataTransfer int64\n}\n\nfunc (u *userQuotaUsage) HasQuotaInfo() bool {\n\tif dataprovider.GetQuotaTracking() == 0 {\n\t\treturn false\n\t}\n\tif u.HasDiskQuota() {\n\t\treturn true\n\t}\n\treturn u.HasTranferQuota()\n}\n\nfunc (u *userQuotaUsage) HasDiskQuota() bool {\n\tif u.QuotaSize > 0 || u.UsedQuotaSize > 0 {\n\t\treturn true\n\t}\n\treturn u.QuotaFiles > 0 || u.UsedQuotaFiles > 0\n}\n\nfunc (u *userQuotaUsage) HasTranferQuota() bool {\n\tif u.TotalDataTransfer > 0 || u.UploadDataTransfer > 0 || u.DownloadDataTransfer > 0 {\n\t\treturn true\n\t}\n\treturn u.UsedDownloadDataTransfer > 0 || u.UsedUploadDataTransfer > 0\n}\n\nfunc (u *userQuotaUsage) GetQuotaSize() string {\n\tif u.QuotaSize > 0 {\n\t\treturn fmt.Sprintf(\"%s/%s\", util.ByteCountIEC(u.UsedQuotaSize), util.ByteCountIEC(u.QuotaSize))\n\t}\n\tif u.UsedQuotaSize > 0 {\n\t\treturn util.ByteCountIEC(u.UsedQuotaSize)\n\t}\n\treturn \"\"\n}\n\nfunc (u *userQuotaUsage) GetQuotaFiles() string {\n\tif u.QuotaFiles > 0 {\n\t\treturn fmt.Sprintf(\"%d/%d\", u.UsedQuotaFiles, u.QuotaFiles)\n\t}\n\tif u.UsedQuotaFiles > 0 {\n\t\treturn strconv.FormatInt(int64(u.UsedQuotaFiles), 10)\n\t}\n\treturn \"\"\n}\n\nfunc (u *userQuotaUsage) GetQuotaSizePercentage() int {\n\tif u.QuotaSize > 0 {\n\t\treturn int(math.Round(100 * float64(u.UsedQuotaSize) / float64(u.QuotaSize)))\n\t}\n\treturn 0\n}\n\nfunc (u *userQuotaUsage) GetQuotaFilesPercentage() int {\n\tif u.QuotaFiles > 0 {\n\t\treturn int(math.Round(100 * float64(u.UsedQuotaFiles) / float64(u.QuotaFiles)))\n\t}\n\treturn 0\n}\n\nfunc (u *userQuotaUsage) IsQuotaSizeLow() bool {\n\treturn u.GetQuotaSizePercentage() > 85\n}\n\nfunc (u *userQuotaUsage) IsQuotaFilesLow() bool {\n\treturn u.GetQuotaFilesPercentage() > 85\n}\n\nfunc (u *userQuotaUsage) IsDiskQuotaLow() bool {\n\treturn u.IsQuotaSizeLow() || u.IsQuotaFilesLow()\n}\n\nfunc (u *userQuotaUsage) GetTotalTransferQuota() string {\n\ttotal := u.UsedUploadDataTransfer + u.UsedDownloadDataTransfer\n\tif u.TotalDataTransfer > 0 {\n\t\treturn fmt.Sprintf(\"%s/%s\", util.ByteCountIEC(total), util.ByteCountIEC(u.TotalDataTransfer*1048576))\n\t}\n\tif total > 0 {\n\t\treturn util.ByteCountIEC(total)\n\t}\n\treturn \"\"\n}\n\nfunc (u *userQuotaUsage) GetUploadTransferQuota() string {\n\tif u.UploadDataTransfer > 0 {\n\t\treturn fmt.Sprintf(\"%s/%s\", util.ByteCountIEC(u.UsedUploadDataTransfer),\n\t\t\tutil.ByteCountIEC(u.UploadDataTransfer*1048576))\n\t}\n\tif u.UsedUploadDataTransfer > 0 {\n\t\treturn util.ByteCountIEC(u.UsedUploadDataTransfer)\n\t}\n\treturn \"\"\n}\n\nfunc (u *userQuotaUsage) GetDownloadTransferQuota() string {\n\tif u.DownloadDataTransfer > 0 {\n\t\treturn fmt.Sprintf(\"%s/%s\", util.ByteCountIEC(u.UsedDownloadDataTransfer),\n\t\t\tutil.ByteCountIEC(u.DownloadDataTransfer*1048576))\n\t}\n\tif u.UsedDownloadDataTransfer > 0 {\n\t\treturn util.ByteCountIEC(u.UsedDownloadDataTransfer)\n\t}\n\treturn \"\"\n}\n\nfunc (u *userQuotaUsage) GetTotalTransferQuotaPercentage() int {\n\tif u.TotalDataTransfer > 0 {\n\t\treturn int(math.Round(100 * float64(u.UsedDownloadDataTransfer+u.UsedUploadDataTransfer) / float64(u.TotalDataTransfer*1048576)))\n\t}\n\treturn 0\n}\n\nfunc (u *userQuotaUsage) GetUploadTransferQuotaPercentage() int {\n\tif u.UploadDataTransfer > 0 {\n\t\treturn int(math.Round(100 * float64(u.UsedUploadDataTransfer) / float64(u.UploadDataTransfer*1048576)))\n\t}\n\treturn 0\n}\n\nfunc (u *userQuotaUsage) GetDownloadTransferQuotaPercentage() int {\n\tif u.DownloadDataTransfer > 0 {\n\t\treturn int(math.Round(100 * float64(u.UsedDownloadDataTransfer) / float64(u.DownloadDataTransfer*1048576)))\n\t}\n\treturn 0\n}\n\nfunc (u *userQuotaUsage) IsTotalTransferQuotaLow() bool {\n\tif u.TotalDataTransfer > 0 {\n\t\treturn u.GetTotalTransferQuotaPercentage() > 85\n\t}\n\treturn false\n}\n\nfunc (u *userQuotaUsage) IsUploadTransferQuotaLow() bool {\n\tif u.UploadDataTransfer > 0 {\n\t\treturn u.GetUploadTransferQuotaPercentage() > 85\n\t}\n\treturn false\n}\n\nfunc (u *userQuotaUsage) IsDownloadTransferQuotaLow() bool {\n\tif u.DownloadDataTransfer > 0 {\n\t\treturn u.GetDownloadTransferQuotaPercentage() > 85\n\t}\n\treturn false\n}\n\nfunc (u *userQuotaUsage) IsTransferQuotaLow() bool {\n\treturn u.IsTotalTransferQuotaLow() || u.IsUploadTransferQuotaLow() || u.IsDownloadTransferQuotaLow()\n}\n\nfunc (u *userQuotaUsage) IsQuotaLow() bool {\n\treturn u.IsDiskQuotaLow() || u.IsTransferQuotaLow()\n}\n\nfunc newUserQuotaUsage(u *dataprovider.User) *userQuotaUsage {\n\treturn &userQuotaUsage{\n\t\tQuotaSize:                u.QuotaSize,\n\t\tQuotaFiles:               u.QuotaFiles,\n\t\tUsedQuotaSize:            u.UsedQuotaSize,\n\t\tUsedQuotaFiles:           u.UsedQuotaFiles,\n\t\tTotalDataTransfer:        u.TotalDataTransfer,\n\t\tUploadDataTransfer:       u.UploadDataTransfer,\n\t\tDownloadDataTransfer:     u.DownloadDataTransfer,\n\t\tUsedUploadDataTransfer:   u.UsedUploadDataTransfer,\n\t\tUsedDownloadDataTransfer: u.UsedDownloadDataTransfer,\n\t}\n}\n\nfunc getFileObjectURL(baseDir, name, baseWebPath string) string {\n\treturn fmt.Sprintf(\"%v?path=%v&_=%v\", baseWebPath, url.QueryEscape(path.Join(baseDir, name)), time.Now().UTC().Unix())\n}\n\nfunc getFileObjectModTime(t time.Time) int64 {\n\tif isZeroTime(t) {\n\t\treturn 0\n\t}\n\treturn t.UnixMilli()\n}\n\nfunc loadClientTemplates(templatesPath string) {\n\tfilesPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientFiles),\n\t}\n\teditFilePath := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientEditFile),\n\t}\n\tsharesPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientShares),\n\t}\n\tsharePaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientShare),\n\t}\n\tprofilePaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientProfile),\n\t}\n\tchangePwdPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateChangePwd),\n\t}\n\tloginPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonLogin),\n\t}\n\tmessagePaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateMessage),\n\t}\n\tmfaPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientMFA),\n\t}\n\ttwoFactorPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateTwoFactor),\n\t}\n\ttwoFactorRecoveryPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateTwoFactorRecovery),\n\t}\n\tforgotPwdPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateForgotPassword),\n\t}\n\tresetPwdPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateResetPassword),\n\t}\n\tviewPDFPaths := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientViewPDF),\n\t}\n\tshareLoginPath := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateShareLogin),\n\t}\n\tshareUploadPath := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateUploadToShare),\n\t}\n\tshareDownloadPath := []string{\n\t\tfilepath.Join(templatesPath, templateCommonDir, templateCommonBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateClientBase),\n\t\tfilepath.Join(templatesPath, templateClientDir, templateShareDownload),\n\t}\n\n\tfilesTmpl := util.LoadTemplate(nil, filesPaths...)\n\tprofileTmpl := util.LoadTemplate(nil, profilePaths...)\n\tchangePwdTmpl := util.LoadTemplate(nil, changePwdPaths...)\n\tloginTmpl := util.LoadTemplate(nil, loginPaths...)\n\tmessageTmpl := util.LoadTemplate(nil, messagePaths...)\n\tmfaTmpl := util.LoadTemplate(nil, mfaPaths...)\n\ttwoFactorTmpl := util.LoadTemplate(nil, twoFactorPaths...)\n\ttwoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPaths...)\n\teditFileTmpl := util.LoadTemplate(nil, editFilePath...)\n\tshareLoginTmpl := util.LoadTemplate(nil, shareLoginPath...)\n\tsharesTmpl := util.LoadTemplate(nil, sharesPaths...)\n\tshareTmpl := util.LoadTemplate(nil, sharePaths...)\n\tforgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)\n\tresetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)\n\tviewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...)\n\tshareUploadTmpl := util.LoadTemplate(nil, shareUploadPath...)\n\tshareDownloadTmpl := util.LoadTemplate(nil, shareDownloadPath...)\n\n\tclientTemplates[templateClientFiles] = filesTmpl\n\tclientTemplates[templateClientProfile] = profileTmpl\n\tclientTemplates[templateChangePwd] = changePwdTmpl\n\tclientTemplates[templateCommonLogin] = loginTmpl\n\tclientTemplates[templateMessage] = messageTmpl\n\tclientTemplates[templateClientMFA] = mfaTmpl\n\tclientTemplates[templateTwoFactor] = twoFactorTmpl\n\tclientTemplates[templateTwoFactorRecovery] = twoFactorRecoveryTmpl\n\tclientTemplates[templateClientEditFile] = editFileTmpl\n\tclientTemplates[templateClientShares] = sharesTmpl\n\tclientTemplates[templateClientShare] = shareTmpl\n\tclientTemplates[templateForgotPassword] = forgotPwdTmpl\n\tclientTemplates[templateResetPassword] = resetPwdTmpl\n\tclientTemplates[templateClientViewPDF] = viewPDFTmpl\n\tclientTemplates[templateShareLogin] = shareLoginTmpl\n\tclientTemplates[templateUploadToShare] = shareUploadTmpl\n\tclientTemplates[templateShareDownload] = shareDownloadTmpl\n}\n\nfunc (s *httpdServer) getBaseClientPageData(title, currentURL string, w http.ResponseWriter, r *http.Request) baseClientPage {\n\tvar csrfToken string\n\tif currentURL != \"\" {\n\t\tcsrfToken = createCSRFToken(w, r, s.csrfTokenAuth, \"\", webBaseClientPath)\n\t}\n\n\tdata := baseClientPage{\n\t\tcommonBasePage:  getCommonBasePage(r),\n\t\tTitle:           title,\n\t\tCurrentURL:      currentURL,\n\t\tFilesURL:        webClientFilesPath,\n\t\tSharesURL:       webClientSharesPath,\n\t\tShareURL:        webClientSharePath,\n\t\tProfileURL:      webClientProfilePath,\n\t\tPingURL:         webClientPingPath,\n\t\tChangePwdURL:    webChangeClientPwdPath,\n\t\tLogoutURL:       webClientLogoutPath,\n\t\tEditURL:         webClientEditFilePath,\n\t\tMFAURL:          webClientMFAPath,\n\t\tCSRFToken:       csrfToken,\n\t\tLoggedUser:      getUserFromToken(r),\n\t\tIsLoggedToShare: false,\n\t\tBranding:        s.binding.webClientBranding(),\n\t\tLanguages:       s.binding.languages(),\n\t}\n\tif !strings.HasPrefix(r.RequestURI, webClientPubSharesPath) {\n\t\tdata.LoginURL = webClientLoginPath\n\t}\n\treturn data\n}\n\nfunc (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := forgotPwdPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tCurrentURL:     webClientForgotPwdPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, rand.Text(), webBaseClientPath),\n\t\tLoginURL:       webClientLoginPath,\n\t\tTitle:          util.I18nForgotPwdTitle,\n\t\tBranding:       s.binding.webClientBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\trenderClientTemplate(w, templateForgotPassword, data)\n}\n\nfunc (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := resetPwdPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tCurrentURL:     webClientResetPwdPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, \"\", webBaseClientPath),\n\t\tLoginURL:       webClientLoginPath,\n\t\tTitle:          util.I18nResetPwdTitle,\n\t\tBranding:       s.binding.webClientBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\trenderClientTemplate(w, templateResetPassword, data)\n}\n\nfunc (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := shareLoginPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          util.I18nShareLoginTitle,\n\t\tCurrentURL:     r.RequestURI,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, rand.Text(), webBaseClientPath),\n\t\tBranding:       s.binding.webClientBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t\tCheckRedirect:  false,\n\t}\n\trenderClientTemplate(w, templateShareLogin, data)\n}\n\nfunc renderClientTemplate(w http.ResponseWriter, tmplName string, data any) {\n\terr := clientTemplates[tmplName].ExecuteTemplate(w, tmplName, data)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n\nfunc (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Request, title string, statusCode int, err error, message string) {\n\tdata := clientMessagePage{\n\t\tbaseClientPage: s.getBaseClientPageData(title, \"\", w, r),\n\t\tError:          getI18nError(err),\n\t\tSuccess:        message,\n\t}\n\tw.WriteHeader(statusCode)\n\trenderClientTemplate(w, templateMessage, data)\n}\n\nfunc (s *httpdServer) renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {\n\ts.renderClientMessagePage(w, r, util.I18nError500Title, http.StatusInternalServerError,\n\t\tutil.NewI18nError(err, util.I18nError500Message), \"\")\n}\n\nfunc (s *httpdServer) renderClientBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {\n\ts.renderClientMessagePage(w, r, util.I18nError400Title, http.StatusBadRequest,\n\t\tutil.NewI18nError(err, util.I18nError400Message), \"\")\n}\n\nfunc (s *httpdServer) renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, err error) {\n\ts.renderClientMessagePage(w, r, util.I18nError403Title, http.StatusForbidden,\n\t\tutil.NewI18nError(err, util.I18nError403Message), \"\")\n}\n\nfunc (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {\n\ts.renderClientMessagePage(w, r, util.I18nError404Title, http.StatusNotFound,\n\t\tutil.NewI18nError(err, util.I18nError404Message), \"\")\n}\n\nfunc (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := twoFactorPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          util.I18n2FATitle,\n\t\tCurrentURL:     webClientTwoFactorPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, \"\", webBaseClientPath),\n\t\tRecoveryURL:    webClientTwoFactorRecoveryPath,\n\t\tBranding:       s.binding.webClientBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\tif next := r.URL.Query().Get(\"next\"); strings.HasPrefix(next, webClientFilesPath) {\n\t\tdata.CurrentURL += \"?next=\" + url.QueryEscape(next)\n\t}\n\trenderClientTemplate(w, templateTwoFactor, data)\n}\n\nfunc (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := twoFactorPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          util.I18n2FATitle,\n\t\tCurrentURL:     webClientTwoFactorRecoveryPath,\n\t\tError:          err,\n\t\tCSRFToken:      createCSRFToken(w, r, s.csrfTokenAuth, \"\", webBaseClientPath),\n\t\tBranding:       s.binding.webClientBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\trenderClientTemplate(w, templateTwoFactorRecovery, data)\n}\n\nfunc (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request) {\n\tdata := clientMFAPage{\n\t\tbaseClientPage:  s.getBaseClientPageData(util.I18n2FATitle, webClientMFAPath, w, r),\n\t\tTOTPConfigs:     mfa.GetAvailableTOTPConfigNames(),\n\t\tGenerateTOTPURL: webClientTOTPGeneratePath,\n\t\tValidateTOTPURL: webClientTOTPValidatePath,\n\t\tSaveTOTPURL:     webClientTOTPSavePath,\n\t\tRecCodesURL:     webClientRecoveryCodesPath,\n\t\tProtocols:       dataprovider.MFAProtocols,\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(data.LoggedUser.Username, \"\")\n\tif err != nil {\n\t\ts.renderClientInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tdata.TOTPConfig = user.Filters.TOTPConfig\n\tdata.RequiredProtocols = user.Filters.TwoFactorAuthProtocols\n\trenderClientTemplate(w, templateClientMFA, data)\n}\n\nfunc (s *httpdServer) renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) {\n\ttitle := util.I18nViewFileTitle\n\tif !readOnly {\n\t\ttitle = util.I18nEditFileTitle\n\t}\n\tdata := editFilePage{\n\t\tbaseClientPage: s.getBaseClientPageData(title, webClientEditFilePath, w, r),\n\t\tPath:           fileName,\n\t\tName:           path.Base(fileName),\n\t\tCurrentDir:     path.Dir(fileName),\n\t\tFileURL:        webClientFilePath,\n\t\tReadOnly:       readOnly,\n\t\tData:           fileData,\n\t}\n\n\trenderClientTemplate(w, templateClientEditFile, data)\n}\n\nfunc (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share,\n\terr *util.I18nError, isAdd bool) {\n\tcurrentURL := webClientSharePath\n\ttitle := util.I18nShareAddTitle\n\tif !isAdd {\n\t\tcurrentURL = fmt.Sprintf(\"%v/%v\", webClientSharePath, url.PathEscape(share.ShareID))\n\t\ttitle = util.I18nShareUpdateTitle\n\t}\n\tif share.IsPasswordHashed() {\n\t\tshare.Password = redactedSecret\n\t}\n\tdata := clientSharePage{\n\t\tbaseClientPage: s.getBaseClientPageData(title, currentURL, w, r),\n\t\tShare:          share,\n\t\tError:          err,\n\t\tIsAdd:          isAdd,\n\t}\n\n\trenderClientTemplate(w, templateClientShare, data)\n}\n\nfunc getDirMapping(dirName, baseWebPath string) []dirMapping {\n\tpaths := []dirMapping{}\n\tif dirName != \"/\" {\n\t\tpaths = append(paths, dirMapping{\n\t\t\tDirName: path.Base(dirName),\n\t\t\tHref:    getFileObjectURL(\"/\", dirName, baseWebPath),\n\t\t})\n\t\tfor {\n\t\t\tdirName = path.Dir(dirName)\n\t\t\tif dirName == \"/\" || dirName == \".\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tpaths = append([]dirMapping{{\n\t\t\t\tDirName: path.Base(dirName),\n\t\t\t\tHref:    getFileObjectURL(\"/\", dirName, baseWebPath)},\n\t\t\t}, paths...)\n\t\t}\n\t}\n\treturn paths\n}\n\nfunc (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName string,\n\terr *util.I18nError, share dataprovider.Share,\n) {\n\tcurrentURL := path.Join(webClientPubSharesPath, share.ShareID, \"browse\")\n\tbaseData := s.getBaseClientPageData(util.I18nSharedFilesTitle, currentURL, w, r)\n\tbaseData.FilesURL = currentURL\n\tbaseSharePath := path.Join(webClientPubSharesPath, share.ShareID)\n\tbaseData.LogoutURL = path.Join(webClientPubSharesPath, share.ShareID, \"logout\")\n\tbaseData.IsLoggedToShare = share.Password != \"\"\n\n\tdata := filesPage{\n\t\tbaseClientPage: baseData,\n\t\tError:          err,\n\t\tCurrentDir:     url.QueryEscape(dirName),\n\t\tDownloadURL:    path.Join(baseSharePath, \"partial\"),\n\t\t// dirName must be escaped because the router expects the full path as single argument\n\t\tShareUploadBaseURL: path.Join(baseSharePath, url.PathEscape(dirName)),\n\t\tViewPDFURL:         path.Join(baseSharePath, \"viewpdf\"),\n\t\tDirsURL:            path.Join(baseSharePath, \"dirs\"),\n\t\tFileURL:            \"\",\n\t\tFileActionsURL:     \"\",\n\t\tCheckExistURL:      path.Join(baseSharePath, \"browse\", \"exist\"),\n\t\tTasksURL:           \"\",\n\t\tCanAddFiles:        share.Scope == dataprovider.ShareScopeReadWrite,\n\t\tCanCreateDirs:      false,\n\t\tCanRename:          false,\n\t\tCanDelete:          false,\n\t\tCanDownload:        share.Scope != dataprovider.ShareScopeWrite,\n\t\tCanShare:           false,\n\t\tCanCopy:            false,\n\t\tPaths:              getDirMapping(dirName, currentURL),\n\t\tQuotaUsage:         newUserQuotaUsage(&dataprovider.User{}),\n\t\tKeepAliveInterval:  int(cookieRefreshThreshold / time.Millisecond),\n\t}\n\trenderClientTemplate(w, templateClientFiles, data)\n}\n\nfunc (s *httpdServer) renderShareDownloadPage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share,\n\tdownloadLink string,\n) {\n\tdata := shareDownloadPage{\n\t\tbaseClientPage: s.getBaseClientPageData(util.I18nShareDownloadTitle, \"\", w, r),\n\t\tDownloadLink:   downloadLink,\n\t}\n\tdata.LogoutURL = \"\"\n\tif share.Password != \"\" {\n\t\tdata.LogoutURL = path.Join(webClientPubSharesPath, share.ShareID, \"logout\")\n\t}\n\n\trenderClientTemplate(w, templateShareDownload, data)\n}\n\nfunc (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share) {\n\tcurrentURL := path.Join(webClientPubSharesPath, share.ShareID, \"upload\")\n\tdata := shareUploadPage{\n\t\tbaseClientPage: s.getBaseClientPageData(util.I18nShareUploadTitle, currentURL, w, r),\n\t\tShare:          share,\n\t\tUploadBasePath: path.Join(webClientPubSharesPath, share.ShareID),\n\t}\n\tdata.LogoutURL = \"\"\n\tif share.Password != \"\" {\n\t\tdata.LogoutURL = path.Join(webClientPubSharesPath, share.ShareID, \"logout\")\n\t}\n\trenderClientTemplate(w, templateUploadToShare, data)\n}\n\nfunc (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName string,\n\terr *util.I18nError, user *dataprovider.User) {\n\tdata := filesPage{\n\t\tbaseClientPage:     s.getBaseClientPageData(util.I18nFilesTitle, webClientFilesPath, w, r),\n\t\tError:              err,\n\t\tCurrentDir:         url.QueryEscape(dirName),\n\t\tDownloadURL:        webClientDownloadZipPath,\n\t\tViewPDFURL:         webClientViewPDFPath,\n\t\tDirsURL:            webClientDirsPath,\n\t\tFileURL:            webClientFilePath,\n\t\tFileActionsURL:     webClientFileActionsPath,\n\t\tCheckExistURL:      webClientExistPath,\n\t\tTasksURL:           webClientTasksPath,\n\t\tCanAddFiles:        user.CanAddFilesFromWeb(dirName),\n\t\tCanCreateDirs:      user.CanAddDirsFromWeb(dirName),\n\t\tCanRename:          user.CanRenameFromWeb(dirName, dirName),\n\t\tCanDelete:          user.CanDeleteFromWeb(dirName),\n\t\tCanDownload:        user.HasPerm(dataprovider.PermDownload, dirName),\n\t\tCanShare:           user.CanManageShares(),\n\t\tCanCopy:            user.CanCopyFromWeb(dirName, dirName),\n\t\tShareUploadBaseURL: \"\",\n\t\tPaths:              getDirMapping(dirName, webClientFilesPath),\n\t\tQuotaUsage:         newUserQuotaUsage(user),\n\t\tKeepAliveInterval:  int(cookieRefreshThreshold / time.Millisecond),\n\t}\n\trenderClientTemplate(w, templateClientFiles, data)\n}\n\nfunc (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := clientProfilePage{\n\t\tbaseClientPage: s.getBaseClientPageData(util.I18nProfileTitle, webClientProfilePath, w, r),\n\t\tError:          err,\n\t}\n\tuser, userMerged, errUser := dataprovider.GetUserVariants(data.LoggedUser.Username, \"\")\n\tif errUser != nil {\n\t\ts.renderClientInternalServerErrorPage(w, r, errUser)\n\t\treturn\n\t}\n\tdata.PublicKeys = user.PublicKeys\n\tdata.TLSCerts = user.Filters.TLSCerts\n\tdata.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth\n\tdata.Email = user.Email\n\tdata.AdditionalEmails = user.Filters.AdditionalEmails\n\tdata.AdditionalEmailsString = strings.Join(data.AdditionalEmails, \", \")\n\tdata.Description = user.Description\n\tdata.CanSubmit = userMerged.CanUpdateProfile()\n\trenderClientTemplate(w, templateClientProfile, data)\n}\n\nfunc (s *httpdServer) renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {\n\tdata := changeClientPasswordPage{\n\t\tbaseClientPage: s.getBaseClientPageData(util.I18nChangePwdTitle, webChangeClientPwdPath, w, r),\n\t\tError:          err,\n\t}\n\n\trenderClientTemplate(w, templateChangePwd, data)\n}\n\nfunc (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxMultipartMem)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderClientBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err),\n\t\t\tutil.NewI18nError(err, util.I18nErrorGetUser), \"\")\n\t\treturn\n\t}\n\n\tconnID := xid.New().String()\n\tprotocol := getProtocolFromRequest(r)\n\tconnectionID := fmt.Sprintf(\"%v_%v\", protocol, connID)\n\tif err := checkHTTPClientUser(&user, r, connectionID, false, false); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, err)\n\t\treturn\n\t}\n\tbaseConn := common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r), r.RemoteAddr, user)\n\tconnection := newConnection(baseConn, w, r)\n\tif err = common.Connections.Add(connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,\n\t\t\tutil.NewI18nError(err, util.I18nError429Message), \"\")\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tfiles := r.Form.Get(\"files\")\n\tvar filesList []string\n\terr = json.Unmarshal(util.StringToBytes(files), &filesList)\n\tif err != nil {\n\t\ts.renderClientBadRequestPage(w, r, err)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\",\n\t\tgetCompressedFileName(connection.GetUsername(), filesList)))\n\trenderCompressedFiles(w, connection, name, filesList, nil)\n}\n\nfunc (s *httpdServer) handleClientSharePartialDownload(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxMultipartMem)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderClientBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := validateBrowsableShare(share, connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, \"\")\n\t\treturn\n\t}\n\tname, err := getBrowsableSharedPath(share.Paths[0], r)\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, \"\")\n\t\treturn\n\t}\n\tif err = common.Connections.Add(connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,\n\t\t\tutil.NewI18nError(err, util.I18nError429Message), \"\")\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\ttransferQuota := connection.GetTransferQuota()\n\tif !transferQuota.HasDownloadSpace() {\n\t\terr = util.NewI18nError(connection.GetReadQuotaExceededError(), util.I18nErrorQuotaRead)\n\t\tconnection.Log(logger.LevelInfo, \"denying share read due to quota limits\")\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getMappedStatusCode(err), err, \"\")\n\t\treturn\n\t}\n\tfiles := r.Form.Get(\"files\")\n\tvar filesList []string\n\terr = json.Unmarshal(util.StringToBytes(files), &filesList)\n\tif err != nil {\n\t\ts.renderClientBadRequestPage(w, r, err)\n\t\treturn\n\t}\n\n\tdataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck\n\tw.Header().Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\",\n\t\tgetCompressedFileName(fmt.Sprintf(\"share-%s\", share.Name), filesList)))\n\trenderCompressedFiles(w, connection, name, filesList, &share)\n}\n\nfunc (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := validateBrowsableShare(share, connection); err != nil {\n\t\tsendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), getRespStatus(err))\n\t\treturn\n\t}\n\tname, err := getBrowsableSharedPath(share.Paths[0], r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), getRespStatus(err))\n\t\treturn\n\t}\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError429Message), http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tlister, err := connection.ReadDir(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nErrorDirListGeneric), getMappedStatusCode(err))\n\t\treturn\n\t}\n\tdefer lister.Close()\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tcontents, err := lister.Next(limit)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\terr = nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tresults := make([]map[string]any, 0, len(contents))\n\t\tfor idx, info := range contents {\n\t\t\tif !info.Mode().IsDir() && !info.Mode().IsRegular() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tres := make(map[string]any)\n\t\t\tres[\"id\"] = offset + idx + 1\n\t\t\tif info.IsDir() {\n\t\t\t\tres[\"type\"] = \"1\"\n\t\t\t\tres[\"size\"] = \"\"\n\t\t\t} else {\n\t\t\t\tres[\"type\"] = \"2\"\n\t\t\t\tres[\"size\"] = info.Size()\n\t\t\t}\n\t\t\tres[\"meta\"] = fmt.Sprintf(\"%v_%v\", res[\"type\"], info.Name())\n\t\t\tres[\"name\"] = info.Name()\n\t\t\tres[\"url\"] = getFileObjectURL(share.GetRelativePath(name), info.Name(),\n\t\t\t\tpath.Join(webClientPubSharesPath, share.ShareID, \"browse\"))\n\t\t\tres[\"last_modified\"] = getFileObjectModTime(info.ModTime())\n\t\t\tresults = append(results, res)\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\tcount := limit\n\t\tif len(results) == 0 {\n\t\t\tcount = 0\n\t\t}\n\t\treturn data, count, err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}\n\tshare, _, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif share.Scope == dataprovider.ShareScopeReadWrite {\n\t\thttp.Redirect(w, r, path.Join(webClientPubSharesPath, share.ShareID, \"browse\"), http.StatusFound)\n\t\treturn\n\t}\n\ts.renderUploadToSharePage(w, r, &share)\n}\n\nfunc (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := validateBrowsableShare(share, connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, \"\")\n\t\treturn\n\t}\n\tname, err := getBrowsableSharedPath(share.Paths[0], r)\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, \"\")\n\t\treturn\n\t}\n\n\tif err = common.Connections.Add(connection); err != nil {\n\t\ts.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)),\n\t\t\tutil.NewI18nError(err, util.I18nError429Message), share)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tvar info os.FileInfo\n\tif name == \"/\" {\n\t\tinfo = vfs.NewFileInfo(name, true, 0, time.Unix(0, 0), false)\n\t} else {\n\t\tinfo, err = connection.Stat(name, 1)\n\t}\n\tif err != nil {\n\t\ts.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)),\n\t\t\tutil.NewI18nError(err, i18nFsMsg(getRespStatus(err))), share)\n\t\treturn\n\t}\n\tif info.IsDir() {\n\t\ts.renderSharedFilesPage(w, r, share.GetRelativePath(name), nil, share)\n\t\treturn\n\t}\n\tdataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck\n\tif status, err := downloadFile(w, r, connection, name, info, false, &share); err != nil {\n\t\tdataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck\n\t\tif status > 0 {\n\t\t\ts.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)),\n\t\t\t\tutil.NewI18nError(err, i18nFsMsg(getRespStatus(err))), share)\n\t\t}\n\t}\n}\n\nfunc (s *httpdServer) handleShareViewPDF(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}\n\tshare, _, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tname := util.CleanPath(r.URL.Query().Get(\"path\"))\n\tdata := viewPDFPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          path.Base(name),\n\t\tURL: fmt.Sprintf(\"%s?path=%s&_=%d\", path.Join(webClientPubSharesPath, share.ShareID, \"getpdf\"),\n\t\t\turl.QueryEscape(name), time.Now().UTC().Unix()),\n\t\tBranding:  s.binding.webClientBranding(),\n\t\tLanguages: s.binding.languages(),\n\t}\n\trenderClientTemplate(w, templateClientViewPDF, data)\n}\n\nfunc (s *httpdServer) handleShareGetPDF(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := validateBrowsableShare(share, connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, \"\")\n\t\treturn\n\t}\n\tname, err := getBrowsableSharedPath(share.Paths[0], r)\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, \"\")\n\t\treturn\n\t}\n\n\tif err = common.Connections.Add(connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,\n\t\t\tutil.NewI18nError(err, util.I18nError429Message), \"\")\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tinfo, err := connection.Stat(name, 1)\n\tif err != nil {\n\t\tstatus := getRespStatus(err)\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, status,\n\t\t\tutil.NewI18nError(err, i18nFsMsg(status)), \"\")\n\t\treturn\n\t}\n\tif info.IsDir() {\n\t\ts.renderClientBadRequestPage(w, r, util.NewI18nError(fmt.Errorf(\"%q is not a file\", name), util.I18nErrorPDFMessage))\n\t\treturn\n\t}\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\tif err := s.ensurePDF(w, r, name, connection); err != nil {\n\t\treturn\n\t}\n\tdataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck\n\tif _, err := downloadFile(w, r, connection, name, info, true, &share); err != nil {\n\t\tdataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck\n\t}\n}\n\nfunc (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, nil, util.I18nErrorDirList403, http.StatusForbidden)\n\t\treturn\n\t}\n\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\tsendAPIResponse(w, r, nil, util.I18nErrorDirListUser, getRespStatus(err))\n\t\treturn\n\t}\n\n\tconnID := xid.New().String()\n\tprotocol := getProtocolFromRequest(r)\n\tconnectionID := fmt.Sprintf(\"%s_%s\", protocol, connID)\n\tif err := checkHTTPClientUser(&user, r, connectionID, false, false); err != nil {\n\t\tsendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nErrorDirList403), http.StatusForbidden)\n\t\treturn\n\t}\n\tbaseConn := common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r), r.RemoteAddr, user)\n\tconnection := newConnection(baseConn, w, r)\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, util.I18nErrorDirList429, http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tlister, err := connection.ReadDir(name)\n\tif err != nil {\n\t\tstatusCode := getMappedStatusCode(err)\n\t\tsendAPIResponse(w, r, err, i18nListDirMsg(statusCode), statusCode)\n\t\treturn\n\t}\n\tdefer lister.Close()\n\n\tdirTree := r.URL.Query().Get(\"dirtree\") == \"1\"\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tcontents, err := lister.Next(limit)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\terr = nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tresults := make([]map[string]any, 0, len(contents))\n\t\tfor idx, info := range contents {\n\t\t\tres := make(map[string]any)\n\t\t\tres[\"id\"] = offset + idx + 1\n\t\t\tres[\"url\"] = getFileObjectURL(name, info.Name(), webClientFilesPath)\n\t\t\tif info.IsDir() {\n\t\t\t\tres[\"type\"] = \"1\"\n\t\t\t\tres[\"size\"] = \"\"\n\t\t\t\tres[\"dir_path\"] = url.QueryEscape(path.Join(name, info.Name()))\n\t\t\t} else {\n\t\t\t\tif dirTree {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tres[\"type\"] = \"2\"\n\t\t\t\tif info.Mode()&os.ModeSymlink != 0 {\n\t\t\t\t\tres[\"size\"] = \"\"\n\t\t\t\t} else {\n\t\t\t\t\tres[\"size\"] = info.Size()\n\t\t\t\t\tif info.Size() < httpdMaxEditFileSize {\n\t\t\t\t\t\tres[\"edit_url\"] = strings.Replace(res[\"url\"].(string), webClientFilesPath, webClientEditFilePath, 1)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tres[\"meta\"] = fmt.Sprintf(\"%v_%v\", res[\"type\"], info.Name())\n\t\t\tres[\"name\"] = info.Name()\n\t\t\tres[\"last_modified\"] = getFileObjectModTime(info.ModTime())\n\t\t\tresults = append(results, res)\n\t\t}\n\t\tdata, err := json.Marshal(results)\n\t\tcount := limit\n\t\tif len(results) == 0 {\n\t\t\tcount = 0\n\t\t}\n\t\treturn data, count, err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err),\n\t\t\tutil.NewI18nError(err, util.I18nErrorGetUser), \"\")\n\t\treturn\n\t}\n\n\tconnID := xid.New().String()\n\tprotocol := getProtocolFromRequest(r)\n\tconnectionID := fmt.Sprintf(\"%v_%v\", protocol, connID)\n\tif err := checkHTTPClientUser(&user, r, connectionID, false, false); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, err)\n\t\treturn\n\t}\n\tbaseConn := common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r), r.RemoteAddr, user)\n\tconnection := newConnection(baseConn, w, r)\n\tif err = common.Connections.Add(connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,\n\t\t\tutil.NewI18nError(err, util.I18nError429Message), \"\")\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tvar info os.FileInfo\n\tif name == \"/\" {\n\t\tinfo = vfs.NewFileInfo(name, true, 0, time.Unix(0, 0), false)\n\t} else {\n\t\tinfo, err = connection.Stat(name, 0)\n\t}\n\tif err != nil {\n\t\ts.renderFilesPage(w, r, path.Dir(name), util.NewI18nError(err, i18nFsMsg(getRespStatus(err))), &user)\n\t\treturn\n\t}\n\tif info.IsDir() {\n\t\ts.renderFilesPage(w, r, name, nil, &user)\n\t\treturn\n\t}\n\tif status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 {\n\t\tif status > 0 {\n\t\t\tif status == http.StatusRequestedRangeNotSatisfiable {\n\t\t\t\ts.renderClientMessagePage(w, r, util.I18nError416Title, status,\n\t\t\t\t\tutil.NewI18nError(err, util.I18nError416Message), \"\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\ts.renderFilesPage(w, r, path.Dir(name), util.NewI18nError(err, i18nFsMsg(status)), &user)\n\t\t}\n\t}\n}\n\nfunc (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err),\n\t\t\tutil.NewI18nError(err, util.I18nErrorGetUser), \"\")\n\t\treturn\n\t}\n\n\tconnID := xid.New().String()\n\tprotocol := getProtocolFromRequest(r)\n\tconnectionID := fmt.Sprintf(\"%v_%v\", protocol, connID)\n\tif err := checkHTTPClientUser(&user, r, connectionID, false, false); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, err)\n\t\treturn\n\t}\n\tbaseConn := common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r), r.RemoteAddr, user)\n\tconnection := newConnection(baseConn, w, r)\n\tif err = common.Connections.Add(connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,\n\t\t\tutil.NewI18nError(err, util.I18nError429Message), \"\")\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tinfo, err := connection.Stat(name, 0)\n\tif err != nil {\n\t\tstatus := getRespStatus(err)\n\t\ts.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, status, util.NewI18nError(err, i18nFsMsg(status)), \"\")\n\t\treturn\n\t}\n\tif info.IsDir() {\n\t\ts.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, http.StatusBadRequest,\n\t\t\tutil.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"The path %q does not point to a file\", name)),\n\t\t\t\tutil.I18nErrorEditDir,\n\t\t\t), \"\")\n\t\treturn\n\t}\n\tif info.Size() > httpdMaxEditFileSize {\n\t\ts.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, http.StatusBadRequest,\n\t\t\tutil.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"The file size %v for %q exceeds the maximum allowed size\",\n\t\t\t\t\tutil.ByteCountIEC(info.Size()), name)),\n\t\t\t\tutil.I18nErrorEditSize,\n\t\t\t), \"\")\n\t\treturn\n\t}\n\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\treader, err := connection.getFileReader(name, 0, r.Method)\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, getRespStatus(err),\n\t\t\tutil.NewI18nError(err, util.I18nError500Message), \"\")\n\t\treturn\n\t}\n\tdefer reader.Close()\n\n\tvar b bytes.Buffer\n\t_, err = io.Copy(&b, reader)\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, getRespStatus(err),\n\t\t\tutil.NewI18nError(err, util.I18nError500Message), \"\")\n\t\treturn\n\t}\n\n\ts.renderEditFilePage(w, r, name, b.String(), !user.CanAddFilesFromWeb(path.Dir(name)))\n}\n\nfunc (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err),\n\t\t\tutil.NewI18nError(err, util.I18nErrorGetUser), \"\")\n\t\treturn\n\t}\n\tshare := &dataprovider.Share{Scope: dataprovider.ShareScopeRead}\n\tif user.Filters.DefaultSharesExpiration > 0 {\n\t\tshare.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(user.Filters.DefaultSharesExpiration)))\n\t} else if user.Filters.MaxSharesExpiration > 0 {\n\t\tshare.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(user.Filters.MaxSharesExpiration)))\n\t}\n\tdirName := \"/\"\n\tif _, ok := r.URL.Query()[\"path\"]; ok {\n\t\tdirName = util.CleanPath(r.URL.Query().Get(\"path\"))\n\t}\n\n\tif _, ok := r.URL.Query()[\"files\"]; ok {\n\t\tfiles := r.URL.Query().Get(\"files\")\n\t\tvar filesList []string\n\t\terr := json.Unmarshal(util.StringToBytes(files), &filesList)\n\t\tif err != nil {\n\t\t\ts.renderClientBadRequestPage(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tfor _, f := range filesList {\n\t\t\tif f != \"\" {\n\t\t\t\tshare.Paths = append(share.Paths, path.Join(dirName, f))\n\t\t\t}\n\t\t}\n\t}\n\n\ts.renderAddUpdateSharePage(w, r, share, nil, true)\n}\n\nfunc (s *httpdServer) handleClientUpdateShareGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tshareID := getURLParam(r, \"id\")\n\tshare, err := dataprovider.ShareExists(shareID, claims.Username)\n\tif err == nil {\n\t\ts.renderAddUpdateSharePage(w, r, &share, nil, false)\n\t} else if errors.Is(err, util.ErrNotFound) {\n\t\ts.renderClientNotFoundPage(w, r, err)\n\t} else {\n\t\ts.renderClientInternalServerErrorPage(w, r, err)\n\t}\n}\n\nfunc (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tshare, err := getShareFromPostFields(r)\n\tif err != nil {\n\t\ts.renderAddUpdateSharePage(w, r, share, util.NewI18nError(err, util.I18nError500Message), true)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tshare.ID = 0\n\tshare.ShareID = util.GenerateUniqueID()\n\tshare.LastUseAt = 0\n\tshare.Username = claims.Username\n\tif share.Password == \"\" {\n\t\tif slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {\n\t\t\ts.renderAddUpdateSharePage(w, r, share,\n\t\t\t\tutil.NewI18nError(util.NewValidationError(\"You are not allowed to share files/folders without password\"), util.I18nErrorShareNoPwd),\n\t\t\t\ttrue)\n\t\t\treturn\n\t\t}\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\ts.renderAddUpdateSharePage(w, r, share, util.NewI18nError(err, util.I18nErrorGetUser), true)\n\t\treturn\n\t}\n\tif err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(share.ExpiresAt)); err != nil {\n\t\ts.renderAddUpdateSharePage(w, r, share, util.NewI18nError(\n\t\t\terr,\n\t\t\tutil.I18nErrorShareExpirationOutOfRange,\n\t\t\tutil.I18nErrorArgs(\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"val\": time.Now().Add(24 * time.Hour * time.Duration(user.Filters.MaxSharesExpiration+1)).UnixMilli(),\n\t\t\t\t\t\"formatParams\": map[string]string{\n\t\t\t\t\t\t\"year\":  \"numeric\",\n\t\t\t\t\t\t\"month\": \"numeric\",\n\t\t\t\t\t\t\"day\":   \"numeric\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t), true)\n\t\treturn\n\t}\n\terr = dataprovider.AddShare(share, claims.Username, ipAddr, claims.Role)\n\tif err == nil {\n\t\thttp.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)\n\t} else {\n\t\ts.renderAddUpdateSharePage(w, r, share, util.NewI18nError(err, util.I18nErrorShareGeneric), true)\n\t}\n}\n\nfunc (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tshareID := getURLParam(r, \"id\")\n\tshare, err := dataprovider.ShareExists(shareID, claims.Username)\n\tif errors.Is(err, util.ErrNotFound) {\n\t\ts.renderClientNotFoundPage(w, r, err)\n\t\treturn\n\t} else if err != nil {\n\t\ts.renderClientInternalServerErrorPage(w, r, err)\n\t\treturn\n\t}\n\tupdatedShare, err := getShareFromPostFields(r)\n\tif err != nil {\n\t\ts.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(err, util.I18nError500Message), false)\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tupdatedShare.ShareID = shareID\n\tupdatedShare.Username = claims.Username\n\tif updatedShare.Password == redactedSecret {\n\t\tupdatedShare.Password = share.Password\n\t}\n\tif updatedShare.Password == \"\" {\n\t\tif slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {\n\t\t\ts.renderAddUpdateSharePage(w, r, updatedShare,\n\t\t\t\tutil.NewI18nError(util.NewValidationError(\"You are not allowed to share files/folders without password\"), util.I18nErrorShareNoPwd),\n\t\t\t\tfalse)\n\t\t\treturn\n\t\t}\n\t}\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\ts.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(err, util.I18nErrorGetUser), false)\n\t\treturn\n\t}\n\tif err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(updatedShare.ExpiresAt)); err != nil {\n\t\ts.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(\n\t\t\terr,\n\t\t\tutil.I18nErrorShareExpirationOutOfRange,\n\t\t\tutil.I18nErrorArgs(\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"val\": time.Now().Add(24 * time.Hour * time.Duration(user.Filters.MaxSharesExpiration+1)).UnixMilli(),\n\t\t\t\t\t\"formatParams\": map[string]string{\n\t\t\t\t\t\t\"year\":  \"numeric\",\n\t\t\t\t\t\t\"month\": \"numeric\",\n\t\t\t\t\t\t\"day\":   \"numeric\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t), false)\n\t\treturn\n\t}\n\terr = dataprovider.UpdateShare(updatedShare, claims.Username, ipAddr, claims.Role)\n\tif err == nil {\n\t\thttp.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)\n\t} else {\n\t\ts.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(err, util.I18nErrorShareGeneric), false)\n\t}\n}\n\nfunc getAllShares(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, nil, util.I18nErrorInvalidToken, http.StatusForbidden)\n\t\treturn\n\t}\n\n\tdataGetter := func(limit, offset int) ([]byte, int, error) {\n\t\tshares, err := dataprovider.GetShares(limit, offset, dataprovider.OrderASC, claims.Username)\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\tdata, err := json.Marshal(shares)\n\t\treturn data, len(shares), err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\tdata := clientSharesPage{\n\t\tbaseClientPage:      s.getBaseClientPageData(util.I18nSharesTitle, webClientSharesPath, w, r),\n\t\tBasePublicSharesURL: webClientPubSharesPath,\n\t\tBaseURL:             s.binding.BaseURL,\n\t}\n\trenderClientTemplate(w, templateClientShares, data)\n}\n\nfunc (s *httpdServer) handleClientGetProfile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderClientProfilePage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderClientChangePasswordPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) { //nolint:gocyclo\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\terr := r.ParseForm()\n\tif err != nil {\n\t\ts.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tuser, userMerged, err := dataprovider.GetUserVariants(claims.Username, \"\")\n\tif err != nil {\n\t\ts.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nErrorGetUser))\n\t\treturn\n\t}\n\tif !userMerged.CanUpdateProfile() {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(\n\t\t\terrors.New(\"you are not allowed to change anything\"),\n\t\t\tutil.I18nErrorNoPermissions,\n\t\t))\n\t\treturn\n\t}\n\tif userMerged.CanManagePublicKeys() {\n\t\tfor k := range r.Form {\n\t\t\tif hasPrefixAndSuffix(k, \"public_keys[\", \"][public_key]\") {\n\t\t\t\tr.Form.Add(\"public_keys\", r.Form.Get(k))\n\t\t\t}\n\t\t}\n\t\tuser.PublicKeys = r.Form[\"public_keys\"]\n\t}\n\tif userMerged.CanManageTLSCerts() {\n\t\tfor k := range r.Form {\n\t\t\tif hasPrefixAndSuffix(k, \"tls_certs[\", \"][tls_cert]\") {\n\t\t\t\tr.Form.Add(\"tls_certs\", r.Form.Get(k))\n\t\t\t}\n\t\t}\n\t\tuser.Filters.TLSCerts = r.Form[\"tls_certs\"]\n\t}\n\tif userMerged.CanChangeAPIKeyAuth() {\n\t\tuser.Filters.AllowAPIKeyAuth = r.Form.Get(\"allow_api_key_auth\") != \"\"\n\t}\n\tif userMerged.CanChangeInfo() {\n\t\tuser.Email = strings.TrimSpace(r.Form.Get(\"email\"))\n\t\tuser.Description = r.Form.Get(\"description\")\n\t\tfor k := range r.Form {\n\t\t\tif hasPrefixAndSuffix(k, \"additional_emails[\", \"][additional_email]\") {\n\t\t\t\temail := strings.TrimSpace(r.Form.Get(k))\n\t\t\t\tif email != \"\" {\n\t\t\t\t\tr.Form.Add(\"additional_emails\", email)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tuser.Filters.AdditionalEmails = r.Form[\"additional_emails\"]\n\t}\n\terr = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, ipAddr, user.Role)\n\tif err != nil {\n\t\ts.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nError500Message))\n\t\treturn\n\t}\n\ts.renderClientMessagePage(w, r, util.I18nProfileTitle, http.StatusOK, nil, util.I18nProfileUpdated)\n}\n\nfunc (s *httpdServer) handleWebClientMFA(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderClientMFAPage(w, r)\n}\n\nfunc (s *httpdServer) handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderClientTwoFactorPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\ts.renderClientTwoFactorRecoveryPage(w, r, nil)\n}\n\nfunc getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {\n\tshare := &dataprovider.Share{}\n\tif err := r.ParseForm(); err != nil {\n\t\treturn share, util.NewI18nError(err, util.I18nErrorInvalidForm)\n\t}\n\tfor k := range r.Form {\n\t\tif hasPrefixAndSuffix(k, \"paths[\", \"][path]\") {\n\t\t\tr.Form.Add(\"paths\", r.Form.Get(k))\n\t\t}\n\t}\n\n\tshare.Name = strings.TrimSpace(r.Form.Get(\"name\"))\n\tshare.Description = r.Form.Get(\"description\")\n\tfor _, p := range r.Form[\"paths\"] {\n\t\tif strings.TrimSpace(p) != \"\" {\n\t\t\tshare.Paths = append(share.Paths, p)\n\t\t}\n\t}\n\tshare.Password = strings.TrimSpace(r.Form.Get(\"password\"))\n\tshare.AllowFrom = getSliceFromDelimitedValues(r.Form.Get(\"allowed_ip\"), \",\")\n\tscope, err := strconv.Atoi(r.Form.Get(\"scope\"))\n\tif err != nil {\n\t\treturn share, util.NewI18nError(err, util.I18nErrorShareScope)\n\t}\n\tshare.Scope = dataprovider.ShareScope(scope)\n\tmaxTokens, err := strconv.Atoi(r.Form.Get(\"max_tokens\"))\n\tif err != nil {\n\t\treturn share, util.NewI18nError(err, util.I18nErrorShareMaxTokens)\n\t}\n\tshare.MaxTokens = maxTokens\n\texpirationDateMillis := int64(0)\n\texpirationDateString := strings.TrimSpace(r.Form.Get(\"expiration_date\"))\n\tif expirationDateString != \"\" {\n\t\texpirationDate, err := time.Parse(webDateTimeFormat, expirationDateString)\n\t\tif err != nil {\n\t\t\treturn share, util.NewI18nError(err, util.I18nErrorShareExpiration)\n\t\t}\n\t\texpirationDateMillis = util.GetTimeAsMsSinceEpoch(expirationDate)\n\t}\n\tshare.ExpiresAt = expirationDateMillis\n\treturn share, nil\n}\n\nfunc (s *httpdServer) handleWebClientForgotPwd(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tif !smtp.IsEnabled() {\n\t\ts.renderClientNotFoundPage(w, r, errors.New(\"this page does not exist\"))\n\t\treturn\n\t}\n\ts.renderClientForgotPwdPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\n\terr := r.ParseForm()\n\tif err != nil {\n\t\ts.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tusername := strings.TrimSpace(r.Form.Get(\"username\"))\n\terr = handleForgotPassword(r, username, false)\n\tif err != nil {\n\t\ts.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric))\n\t\treturn\n\t}\n\thttp.Redirect(w, r, webClientResetPwdPath, http.StatusFound)\n}\n\nfunc (s *httpdServer) handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tif !smtp.IsEnabled() {\n\t\ts.renderClientNotFoundPage(w, r, errors.New(\"this page does not exist\"))\n\t\treturn\n\t}\n\ts.renderClientResetPwdPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tname := r.URL.Query().Get(\"path\")\n\tif name == \"\" {\n\t\ts.renderClientBadRequestPage(w, r, errors.New(\"no file specified\"))\n\t\treturn\n\t}\n\tname = util.CleanPath(name)\n\tdata := viewPDFPage{\n\t\tcommonBasePage: getCommonBasePage(r),\n\t\tTitle:          path.Base(name),\n\t\tURL:            fmt.Sprintf(\"%s?path=%s&_=%d\", webClientGetPDFPath, url.QueryEscape(name), time.Now().UTC().Unix()),\n\t\tBranding:       s.binding.webClientBranding(),\n\t\tLanguages:      s.binding.languages(),\n\t}\n\trenderClientTemplate(w, templateClientViewPDF, data)\n}\n\nfunc (s *httpdServer) handleClientGetPDF(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\ts.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))\n\t\treturn\n\t}\n\tname := r.URL.Query().Get(\"path\")\n\tif name == \"\" {\n\t\ts.renderClientBadRequestPage(w, r, util.NewI18nError(errors.New(\"no file specified\"), util.I18nError400Message))\n\t\treturn\n\t}\n\tname = util.CleanPath(name)\n\tuser, err := dataprovider.GetUserWithGroupSettings(claims.Username, \"\")\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err),\n\t\t\tutil.NewI18nError(err, util.I18nErrorGetUser), \"\")\n\t\treturn\n\t}\n\n\tconnID := xid.New().String()\n\tprotocol := getProtocolFromRequest(r)\n\tconnectionID := fmt.Sprintf(\"%v_%v\", protocol, connID)\n\tif err := checkHTTPClientUser(&user, r, connectionID, false, false); err != nil {\n\t\ts.renderClientForbiddenPage(w, r, err)\n\t\treturn\n\t}\n\tbaseConn := common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r), r.RemoteAddr, user)\n\tconnection := newConnection(baseConn, w, r)\n\tif err = common.Connections.Add(connection); err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,\n\t\t\tutil.NewI18nError(err, util.I18nError429Message), \"\")\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tinfo, err := connection.Stat(name, 0)\n\tif err != nil {\n\t\tstatus := getRespStatus(err)\n\t\ts.renderClientMessagePage(w, r, util.I18nErrorPDFTitle, status, util.NewI18nError(err, i18nFsMsg(status)), \"\")\n\t\treturn\n\t}\n\tif info.IsDir() {\n\t\ts.renderClientBadRequestPage(w, r, util.NewI18nError(fmt.Errorf(\"%q is not a file\", name), util.I18nErrorPDFMessage))\n\t\treturn\n\t}\n\tconnection.User.CheckFsRoot(connection.ID) //nolint:errcheck\n\tif err := s.ensurePDF(w, r, name, connection); err != nil {\n\t\treturn\n\t}\n\tdownloadFile(w, r, connection, name, info, true, nil) //nolint:errcheck\n}\n\nfunc (s *httpdServer) ensurePDF(w http.ResponseWriter, r *http.Request, name string, connection *Connection) error {\n\treader, err := connection.getFileReader(name, 0, r.Method)\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nErrorPDFTitle,\n\t\t\tgetRespStatus(err), util.NewI18nError(err, util.I18nError500Message), \"\")\n\t\treturn err\n\t}\n\tdefer reader.Close()\n\n\tvar b bytes.Buffer\n\t_, err = io.CopyN(&b, reader, 128)\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nErrorPDFTitle, getRespStatus(err),\n\t\t\tutil.NewI18nError(err, util.I18nErrorPDFMessage), \"\")\n\t\treturn err\n\t}\n\tif ctype := http.DetectContentType(b.Bytes()); ctype != \"application/pdf\" {\n\t\tconnection.Log(logger.LevelDebug, \"detected %q content type, expected PDF, file %q\", ctype, name)\n\t\terr := fmt.Errorf(\"the file %q does not look like a PDF\", name)\n\t\ts.renderClientBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorPDFMessage))\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *httpdServer) handleClientShareLoginGet(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\ts.renderShareLoginPage(w, r, nil)\n}\n\nfunc (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tif err := r.ParseForm(); err != nil {\n\t\ts.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))\n\t\treturn\n\t}\n\tif err := verifyLoginCookieAndCSRFToken(r, s.csrfTokenAuth); err != nil {\n\t\ts.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))\n\t\treturn\n\t}\n\tinvalidateToken(r)\n\tshareID := getURLParam(r, \"id\")\n\tshare, err := dataprovider.ShareExists(shareID, \"\")\n\tif err != nil {\n\t\ts.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tmatch, err := share.CheckCredentials(strings.TrimSpace(r.Form.Get(\"share_password\")))\n\tif !match || err != nil {\n\t\ts.renderShareLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))\n\t\treturn\n\t}\n\tnext := path.Clean(r.URL.Query().Get(\"next\"))\n\tbaseShareURL := path.Join(webClientPubSharesPath, share.ShareID)\n\tisRedirect, redirectTo := checkShareRedirectURL(next, baseShareURL)\n\tc := &jwt.Claims{\n\t\tUsername: shareID,\n\t}\n\tif isRedirect {\n\t\tc.Ref = next\n\t}\n\terr = createAndSetCookie(w, r, c, s.tokenAuth, tokenAudienceWebShare, ipAddr)\n\tif err != nil {\n\t\ts.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nError500Message))\n\t\treturn\n\t}\n\tif isRedirect {\n\t\thttp.Redirect(w, r, redirectTo, http.StatusFound)\n\t\treturn\n\t}\n\ts.renderClientMessagePage(w, r, util.I18nSharedFilesTitle, http.StatusOK, nil, util.I18nShareLoginOK)\n}\n\nfunc (s *httpdServer) handleClientShareLogout(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\n\tshareID := getURLParam(r, \"id\")\n\tctx, claims, err := s.getShareClaims(r, shareID)\n\tif err != nil {\n\t\ts.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, http.StatusForbidden,\n\t\t\tutil.NewI18nError(err, util.I18nErrorInvalidToken), \"\")\n\t\treturn\n\t}\n\tremoveCookie(w, r.WithContext(ctx), webBaseClientPath)\n\n\tredirectURL := path.Join(webClientPubSharesPath, shareID, fmt.Sprintf(\"login?next=%s\", url.QueryEscape(claims.Ref)))\n\thttp.Redirect(w, r, redirectURL, http.StatusFound)\n}\n\nfunc (s *httpdServer) handleClientSharedFile(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead}\n\tshare, _, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tquery := \"\"\n\tif r.URL.RawQuery != \"\" {\n\t\tquery = \"?\" + r.URL.RawQuery\n\t}\n\ts.renderShareDownloadPage(w, r, &share, path.Join(webClientPubSharesPath, share.ShareID)+query)\n}\n\nfunc (s *httpdServer) handleClientCheckExist(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\n\tdoCheckExist(w, r, connection, name)\n}\n\nfunc (s *httpdServer) handleClientShareCheckExist(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tvalidScopes := []dataprovider.ShareScope{dataprovider.ShareScopeReadWrite}\n\tshare, connection, err := s.checkPublicShare(w, r, validScopes)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := validateBrowsableShare(share, connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", getRespStatus(err))\n\t\treturn\n\t}\n\tname, err := getBrowsableSharedPath(share.Paths[0], r)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif err = common.Connections.Add(connection); err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to add connection\", http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tdoCheckExist(w, r, connection, name)\n}\n\ntype filesToCheck struct {\n\tFiles []string `json:\"files\"`\n}\n\nfunc doCheckExist(w http.ResponseWriter, r *http.Request, connection *Connection, name string) {\n\tvar filesList filesToCheck\n\terr := render.DecodeJSON(r.Body, &filesList)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif len(filesList.Files) == 0 {\n\t\tsendAPIResponse(w, r, errors.New(\"files to be checked are mandatory\"), \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tlister, err := connection.ListDir(name)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to get directory contents\", getMappedStatusCode(err))\n\t\treturn\n\t}\n\tdefer lister.Close()\n\n\tdataGetter := func(limit, _ int) ([]byte, int, error) {\n\t\tcontents, err := lister.Next(limit)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\terr = nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\t\texisting := make([]map[string]any, 0)\n\t\tfor _, info := range contents {\n\t\t\tif slices.Contains(filesList.Files, info.Name()) {\n\t\t\t\tres := make(map[string]any)\n\t\t\t\tres[\"name\"] = info.Name()\n\t\t\t\tif info.IsDir() {\n\t\t\t\t\tres[\"type\"] = \"1\"\n\t\t\t\t\tres[\"size\"] = \"\"\n\t\t\t\t} else {\n\t\t\t\t\tres[\"type\"] = \"2\"\n\t\t\t\t\tres[\"size\"] = info.Size()\n\t\t\t\t}\n\t\t\t\texisting = append(existing, res)\n\t\t\t}\n\t\t}\n\t\tdata, err := json.Marshal(existing)\n\t\tcount := limit\n\t\tif len(existing) == 0 {\n\t\t\tcount = 0\n\t\t}\n\t\treturn data, count, err\n\t}\n\n\tstreamJSONArray(w, defaultQueryLimit, dataGetter)\n}\n\nfunc checkShareRedirectURL(next, base string) (bool, string) {\n\tif !strings.HasPrefix(next, base) {\n\t\treturn false, \"\"\n\t}\n\tif next == base {\n\t\treturn true, path.Join(next, \"download\")\n\t}\n\tbaseURL, err := url.Parse(base)\n\tif err != nil {\n\t\treturn false, \"\"\n\t}\n\tnextURL, err := url.Parse(next)\n\tif err != nil {\n\t\treturn false, \"\"\n\t}\n\tif nextURL.Path == baseURL.Path {\n\t\tredirectURL := nextURL.JoinPath(\"download\")\n\t\treturn true, redirectURL.String()\n\t}\n\treturn true, next\n}\n\nfunc getWebTask(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)\n\tclaims, err := jwt.FromContext(r.Context())\n\tif err != nil || claims.Username == \"\" {\n\t\tsendAPIResponse(w, r, err, \"Invalid token claims\", http.StatusBadRequest)\n\t\treturn\n\t}\n\ttaskID := getURLParam(r, \"id\")\n\n\ttask, err := webTaskMgr.Get(taskID)\n\tif err != nil {\n\t\tsendAPIResponse(w, r, err, \"Unable to get task\", getMappedStatusCode(err))\n\t\treturn\n\t}\n\tif task.User != claims.Username {\n\t\tsendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\treturn\n\t}\n\trender.JSON(w, r, task)\n}\n\nfunc taskDeleteDir(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tname := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\ttask := webTaskData{\n\t\tID:        connection.GetID(),\n\t\tUser:      connection.GetUsername(),\n\t\tPath:      name,\n\t\tTimestamp: util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tStatus:    0,\n\t}\n\tif err := webTaskMgr.Add(task); err != nil {\n\t\tcommon.Connections.Remove(connection.GetID())\n\t\tsendAPIResponse(w, r, nil, \"Unable to create task\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tgo executeDeleteTask(connection, task)\n\tsendAPIResponse(w, r, nil, task.ID, http.StatusAccepted)\n}\n\nfunc taskRenameFsEntry(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\toldName := connection.User.GetCleanedPath(r.URL.Query().Get(\"path\"))\n\tnewName := connection.User.GetCleanedPath(r.URL.Query().Get(\"target\"))\n\ttask := webTaskData{\n\t\tID:        connection.GetID(),\n\t\tUser:      connection.GetUsername(),\n\t\tPath:      oldName,\n\t\tTarget:    newName,\n\t\tTimestamp: util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tStatus:    0,\n\t}\n\tif err := webTaskMgr.Add(task); err != nil {\n\t\tcommon.Connections.Remove(connection.GetID())\n\t\tsendAPIResponse(w, r, nil, \"Unable to create task\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tgo executeRenameTask(connection, task)\n\tsendAPIResponse(w, r, nil, task.ID, http.StatusAccepted)\n}\n\nfunc taskCopyFsEntry(w http.ResponseWriter, r *http.Request) {\n\tr.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)\n\tconnection, err := getUserConnection(w, r)\n\tif err != nil {\n\t\treturn\n\t}\n\tsource := r.URL.Query().Get(\"path\")\n\ttarget := r.URL.Query().Get(\"target\")\n\tcopyFromSource := strings.HasSuffix(source, \"/\")\n\tcopyInTarget := strings.HasSuffix(target, \"/\")\n\tsource = connection.User.GetCleanedPath(source)\n\ttarget = connection.User.GetCleanedPath(target)\n\tif copyFromSource {\n\t\tsource += \"/\"\n\t}\n\tif copyInTarget {\n\t\ttarget += \"/\"\n\t}\n\ttask := webTaskData{\n\t\tID:        connection.GetID(),\n\t\tUser:      connection.GetUsername(),\n\t\tPath:      source,\n\t\tTarget:    target,\n\t\tTimestamp: util.GetTimeAsMsSinceEpoch(time.Now()),\n\t\tStatus:    0,\n\t}\n\tif err := webTaskMgr.Add(task); err != nil {\n\t\tcommon.Connections.Remove(connection.GetID())\n\t\tsendAPIResponse(w, r, nil, \"Unable to create task\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tgo executeCopyTask(connection, task)\n\tsendAPIResponse(w, r, nil, task.ID, http.StatusAccepted)\n}\n\nfunc executeDeleteTask(conn *Connection, task webTaskData) {\n\tdone := make(chan bool)\n\n\tdefer func() {\n\t\tclose(done)\n\t\tcommon.Connections.Remove(conn.GetID())\n\t}()\n\n\tgo keepAliveTask(task, done, 2*time.Minute)\n\n\tstatus := http.StatusOK\n\tif err := conn.RemoveAll(task.Path); err != nil {\n\t\tstatus = getMappedStatusCode(err)\n\t}\n\n\ttask.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())\n\ttask.Status = status\n\terr := webTaskMgr.Add(task)\n\tconn.Log(logger.LevelDebug, \"delete task finished, status: %d, update task err: %v\", status, err)\n}\n\nfunc executeRenameTask(conn *Connection, task webTaskData) {\n\tdone := make(chan bool)\n\n\tdefer func() {\n\t\tclose(done)\n\t\tcommon.Connections.Remove(conn.GetID())\n\t}()\n\n\tgo keepAliveTask(task, done, 2*time.Minute)\n\n\tstatus := http.StatusOK\n\n\tif !conn.IsSameResource(task.Path, task.Target) {\n\t\tif err := conn.Copy(task.Path, task.Target); err != nil {\n\t\t\tstatus = getMappedStatusCode(err)\n\t\t\ttask.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t\ttask.Status = status\n\t\t\terr = webTaskMgr.Add(task)\n\t\t\tconn.Log(logger.LevelDebug, \"copy step for rename task finished, status: %d, update task err: %v\", status, err)\n\t\t\treturn\n\t\t}\n\t\tif err := conn.RemoveAll(task.Path); err != nil {\n\t\t\tstatus = getMappedStatusCode(err)\n\t\t}\n\t} else {\n\t\tif err := conn.Rename(task.Path, task.Target); err != nil {\n\t\t\tstatus = getMappedStatusCode(err)\n\t\t}\n\t}\n\n\ttask.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())\n\ttask.Status = status\n\terr := webTaskMgr.Add(task)\n\tconn.Log(logger.LevelDebug, \"rename task finished, status: %d, update task err: %v\", status, err)\n}\n\nfunc executeCopyTask(conn *Connection, task webTaskData) {\n\tdone := make(chan bool)\n\n\tdefer func() {\n\t\tclose(done)\n\t\tcommon.Connections.Remove(conn.GetID())\n\t}()\n\n\tgo keepAliveTask(task, done, 2*time.Minute)\n\n\tstatus := http.StatusOK\n\tif err := conn.Copy(task.Path, task.Target); err != nil {\n\t\tstatus = getMappedStatusCode(err)\n\t}\n\n\ttask.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())\n\ttask.Status = status\n\terr := webTaskMgr.Add(task)\n\tconn.Log(logger.LevelDebug, \"copy task finished, status: %d, update task err: %v\", status, err)\n}\n\nfunc keepAliveTask(task webTaskData, done chan bool, interval time.Duration) {\n\tticker := time.NewTicker(interval)\n\tdefer func() {\n\t\tticker.Stop()\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase <-done:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\ttask.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())\n\t\t\terr := webTaskMgr.Add(task)\n\t\t\tlogger.Debug(logSender, task.ID, \"task timestamp updated, err: %v\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/httpd/webtask.go",
    "content": "// Copyright (C) 2024 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\twebTaskMgr webTaskManager\n)\n\nfunc newWebTaskManager(isShared int) webTaskManager {\n\tif isShared == 1 {\n\t\tlogger.Info(logSender, \"\", \"using provider task manager\")\n\t\treturn &dbTaskManager{}\n\t}\n\tlogger.Info(logSender, \"\", \"using memory task manager\")\n\treturn &memoryTaskManager{}\n}\n\ntype webTaskManager interface {\n\tAdd(data webTaskData) error\n\tGet(ID string) (webTaskData, error)\n\tCleanup()\n}\n\ntype webTaskData struct {\n\tID        string `json:\"id\"`\n\tUser      string `json:\"user\"`\n\tPath      string `json:\"path\"`\n\tTarget    string `json:\"target\"`\n\tTimestamp int64  `json:\"ts\"`\n\tStatus    int    `json:\"status\"` // 0 in progress or http status code (200 ok, 403 and so on)\n}\n\ntype memoryTaskManager struct {\n\ttasks sync.Map\n}\n\nfunc (m *memoryTaskManager) Add(data webTaskData) error {\n\tm.tasks.Store(data.ID, &data)\n\treturn nil\n}\n\nfunc (m *memoryTaskManager) Get(ID string) (webTaskData, error) {\n\tdata, ok := m.tasks.Load(ID)\n\tif !ok {\n\t\treturn webTaskData{}, util.NewRecordNotFoundError(fmt.Sprintf(\"task for ID %q not found\", ID))\n\t}\n\treturn *data.(*webTaskData), nil\n}\n\nfunc (m *memoryTaskManager) Cleanup() {\n\tm.tasks.Range(func(key, value any) bool {\n\t\tdata := value.(*webTaskData)\n\t\tif data.Timestamp < util.GetTimeAsMsSinceEpoch(time.Now().Add(-5*time.Minute)) {\n\t\t\tm.tasks.Delete(key)\n\t\t}\n\t\treturn true\n\t})\n}\n\ntype dbTaskManager struct{}\n\nfunc (m *dbTaskManager) Add(data webTaskData) error {\n\tsession := dataprovider.Session{\n\t\tKey:       data.ID,\n\t\tData:      data,\n\t\tType:      dataprovider.SessionTypeWebTask,\n\t\tTimestamp: data.Timestamp,\n\t}\n\treturn dataprovider.AddSharedSession(session)\n}\n\nfunc (m *dbTaskManager) Get(ID string) (webTaskData, error) {\n\tsess, err := dataprovider.GetSharedSession(ID, dataprovider.SessionTypeWebTask)\n\tif err != nil {\n\t\treturn webTaskData{}, err\n\t}\n\td := sess.Data.([]byte)\n\tvar data webTaskData\n\terr = json.Unmarshal(d, &data)\n\treturn data, err\n}\n\nfunc (m *dbTaskManager) Cleanup() {\n\tdataprovider.CleanupSharedSessions(dataprovider.SessionTypeWebTask, time.Now().Add(-5*time.Minute)) //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/httpd/webtask_test.go",
    "content": "// Copyright (C) 2024 Nicola Murino\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, version 3.\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\npackage httpd\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/rs/xid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc TestMemoryWebTaskManager(t *testing.T) {\n\tmgr := newWebTaskManager(0)\n\tm, ok := mgr.(*memoryTaskManager)\n\trequire.True(t, ok)\n\ttask := webTaskData{\n\t\tID:        xid.New().String(),\n\t\tUser:      defeaultUsername,\n\t\tTimestamp: time.Now().Add(-1 * time.Hour).UnixMilli(),\n\t\tStatus:    0,\n\t}\n\ttask1 := webTaskData{\n\t\tID:        xid.New().String(),\n\t\tUser:      defeaultUsername,\n\t\tTimestamp: time.Now().UnixMilli(),\n\t\tStatus:    0,\n\t}\n\terr := m.Add(task)\n\trequire.NoError(t, err)\n\terr = m.Add(task1)\n\trequire.NoError(t, err)\n\ttaskGet, err := m.Get(task.ID)\n\trequire.NoError(t, err)\n\trequire.Equal(t, task, taskGet)\n\tm.Cleanup()\n\t_, err = m.Get(task.ID)\n\trequire.ErrorIs(t, err, util.ErrNotFound)\n\ttaskGet, err = m.Get(task1.ID)\n\trequire.NoError(t, err)\n\trequire.Equal(t, task1, taskGet)\n\ttask1.Timestamp = time.Now().Add(-1 * time.Hour).UnixMilli()\n\terr = m.Add(task1)\n\trequire.NoError(t, err)\n\tm.Cleanup()\n\t_, err = m.Get(task.ID)\n\trequire.ErrorIs(t, err, util.ErrNotFound)\n\t// test keep alive task\n\toldMgr := webTaskMgr\n\twebTaskMgr = mgr\n\n\tdone := make(chan bool)\n\tgo keepAliveTask(task, done, 50*time.Millisecond)\n\n\ttime.Sleep(120 * time.Millisecond)\n\tclose(done)\n\ttaskGet, err = m.Get(task.ID)\n\trequire.NoError(t, err)\n\tassert.Greater(t, taskGet.Timestamp, task.Timestamp)\n\tm.Cleanup()\n\t_, err = m.Get(task.ID)\n\trequire.NoError(t, err)\n\terr = m.Add(task)\n\trequire.NoError(t, err)\n\tm.Cleanup()\n\t_, err = m.Get(task.ID)\n\trequire.ErrorIs(t, err, util.ErrNotFound)\n\n\twebTaskMgr = oldMgr\n}\n\nfunc TestDbWebTaskManager(t *testing.T) {\n\tif !isSharedProviderSupported() {\n\t\tt.Skip(\"this test it is not available with this provider\")\n\t}\n\tmgr := newWebTaskManager(1)\n\tm, ok := mgr.(*dbTaskManager)\n\trequire.True(t, ok)\n\n\ttask := webTaskData{\n\t\tID:        xid.New().String(),\n\t\tUser:      defeaultUsername,\n\t\tTimestamp: time.Now().Add(-1 * time.Hour).UnixMilli(),\n\t\tStatus:    0,\n\t}\n\terr := m.Add(task)\n\trequire.NoError(t, err)\n\ttaskGet, err := m.Get(task.ID)\n\trequire.NoError(t, err)\n\trequire.Equal(t, task, taskGet)\n\tm.Cleanup()\n\t_, err = m.Get(task.ID)\n\trequire.ErrorIs(t, err, util.ErrNotFound)\n\terr = m.Add(task)\n\trequire.NoError(t, err)\n\t// test keep alive task\n\toldMgr := webTaskMgr\n\twebTaskMgr = mgr\n\n\tdone := make(chan bool)\n\tgo keepAliveTask(task, done, 50*time.Millisecond)\n\n\ttime.Sleep(120 * time.Millisecond)\n\tclose(done)\n\ttaskGet, err = m.Get(task.ID)\n\trequire.NoError(t, err)\n\tassert.Greater(t, taskGet.Timestamp, task.Timestamp)\n\tm.Cleanup()\n\t_, err = m.Get(task.ID)\n\trequire.NoError(t, err)\n\terr = m.Add(task)\n\trequire.NoError(t, err)\n\tm.Cleanup()\n\t_, err = m.Get(task.ID)\n\trequire.ErrorIs(t, err, util.ErrNotFound)\n\n\twebTaskMgr = oldMgr\n}\n"
  },
  {
    "path": "internal/httpdtest/httpdtest.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package httpdtest provides utilities for testing the supported REST API.\npackage httpdtest\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-chi/render\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\ttokenPath             = \"/api/v2/token\"\n\tactiveConnectionsPath = \"/api/v2/connections\"\n\tquotasBasePath        = \"/api/v2/quotas\"\n\tquotaScanPath         = \"/api/v2/quotas/users/scans\"\n\tquotaScanVFolderPath  = \"/api/v2/quotas/folders/scans\"\n\tuserPath              = \"/api/v2/users\"\n\tgroupPath             = \"/api/v2/groups\"\n\tversionPath           = \"/api/v2/version\"\n\tfolderPath            = \"/api/v2/folders\"\n\tserverStatusPath      = \"/api/v2/status\"\n\tdumpDataPath          = \"/api/v2/dumpdata\"\n\tloadDataPath          = \"/api/v2/loaddata\"\n\tdefenderHosts         = \"/api/v2/defender/hosts\"\n\tadminPath             = \"/api/v2/admins\"\n\tadminPwdPath          = \"/api/v2/admin/changepwd\"\n\tapiKeysPath           = \"/api/v2/apikeys\"\n\tretentionChecksPath   = \"/api/v2/retention/users/checks\"\n\teventActionsPath      = \"/api/v2/eventactions\"\n\teventRulesPath        = \"/api/v2/eventrules\"\n\trolesPath             = \"/api/v2/roles\"\n\tipListsPath           = \"/api/v2/iplists\"\n)\n\nconst (\n\tdefaultTokenAuthUser = \"admin\"\n\tdefaultTokenAuthPass = \"password\"\n)\n\nvar (\n\thttpBaseURL = \"http://127.0.0.1:8080\"\n\tjwtToken    = \"\"\n)\n\n// SetBaseURL sets the base url to use for HTTP requests.\n// Default URL is \"http://127.0.0.1:8080\"\nfunc SetBaseURL(url string) {\n\thttpBaseURL = url\n}\n\n// SetJWTToken sets the JWT token to use\nfunc SetJWTToken(token string) {\n\tjwtToken = token\n}\n\nfunc sendHTTPRequest(method, url string, body io.Reader, contentType, token string) (*http.Response, error) {\n\treq, err := http.NewRequest(method, url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif contentType != \"\" {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\tif token != \"\" {\n\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %v\", token))\n\t}\n\treturn httpclient.GetHTTPClient().Do(req)\n}\n\nfunc buildURLRelativeToBase(paths ...string) string {\n\t// we need to use path.Join and not filepath.Join\n\t// since filepath.Join will use backslash separator on Windows\n\tp := path.Join(paths...)\n\treturn fmt.Sprintf(\"%s/%s\", strings.TrimRight(httpBaseURL, \"/\"), strings.TrimLeft(p, \"/\"))\n}\n\n// GetToken tries to return a JWT token\nfunc GetToken(username, password string) (string, map[string]any, error) {\n\treq, err := http.NewRequest(http.MethodGet, buildURLRelativeToBase(tokenPath), nil)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treq.SetBasicAuth(username, password)\n\tresp, err := httpclient.GetHTTPClient().Do(req)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\terr = checkResponse(resp.StatusCode, http.StatusOK)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tresponseHolder := make(map[string]any)\n\terr = render.DecodeJSON(resp.Body, &responseHolder)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn responseHolder[\"access_token\"].(string), responseHolder, nil\n}\n\nfunc getDefaultToken() string {\n\tif jwtToken != \"\" {\n\t\treturn jwtToken\n\t}\n\ttoken, _, err := GetToken(defaultTokenAuthUser, defaultTokenAuthPass)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn token\n}\n\n// AddUser adds a new user and checks the received HTTP Status code against expectedStatusCode.\nfunc AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) {\n\tvar newUser dataprovider.User\n\tvar body []byte\n\tuserAsJSON, _ := json.Marshal(user)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(userPath), bytes.NewBuffer(userAsJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newUser, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newUser, body, err\n\t}\n\tif err == nil {\n\t\terr = render.DecodeJSON(resp.Body, &newUser)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\tif err == nil {\n\t\terr = checkUser(&user, &newUser)\n\t}\n\treturn newUser, body, err\n}\n\n// UpdateUserWithJSON update a user using the provided JSON as POST body\nfunc UpdateUserWithJSON(user dataprovider.User, expectedStatusCode int, disconnect string, userAsJSON []byte) (dataprovider.User, []byte, error) {\n\tvar newUser dataprovider.User\n\tvar body []byte\n\turl, err := addUpdateUserQueryParams(buildURLRelativeToBase(userPath, url.PathEscape(user.Username)), disconnect)\n\tif err != nil {\n\t\treturn user, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), \"application/json\",\n\t\tgetDefaultToken())\n\tif err != nil {\n\t\treturn user, body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn newUser, body, err\n\t}\n\tif err == nil {\n\t\tnewUser, body, err = GetUserByUsername(user.Username, expectedStatusCode)\n\t}\n\tif err == nil {\n\t\terr = checkUser(&user, &newUser)\n\t}\n\treturn newUser, body, err\n}\n\n// UpdateUser updates an existing user and checks the received HTTP Status code against expectedStatusCode.\nfunc UpdateUser(user dataprovider.User, expectedStatusCode int, disconnect string) (dataprovider.User, []byte, error) {\n\tuserAsJSON, _ := json.Marshal(user)\n\treturn UpdateUserWithJSON(user, expectedStatusCode, disconnect, userAsJSON)\n}\n\n// RemoveUser removes an existing user and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveUser(user dataprovider.User, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(userPath, url.PathEscape(user.Username)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetUserByUsername gets a user by username and checks the received HTTP Status code against expectedStatusCode.\nfunc GetUserByUsername(username string, expectedStatusCode int) (dataprovider.User, []byte, error) {\n\tvar user dataprovider.User\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(userPath, url.PathEscape(username)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn user, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &user)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn user, body, err\n}\n\n// GetUsers returns a list of users and checks the received HTTP Status code against expectedStatusCode.\n// The number of results can be limited specifying a limit.\n// Some results can be skipped specifying an offset.\nfunc GetUsers(limit, offset int64, expectedStatusCode int) ([]dataprovider.User, []byte, error) {\n\tvar users []dataprovider.User\n\tvar body []byte\n\turl, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(userPath), limit, offset)\n\tif err != nil {\n\t\treturn users, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn users, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &users)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn users, body, err\n}\n\n// AddGroup adds a new group and checks the received HTTP Status code against expectedStatusCode.\nfunc AddGroup(group dataprovider.Group, expectedStatusCode int) (dataprovider.Group, []byte, error) {\n\tvar newGroup dataprovider.Group\n\tvar body []byte\n\tasJSON, _ := json.Marshal(group)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(groupPath), bytes.NewBuffer(asJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newGroup, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newGroup, body, err\n\t}\n\tif err == nil {\n\t\terr = render.DecodeJSON(resp.Body, &newGroup)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\tif err == nil {\n\t\tgroup.UserSettings.Filters.TLSCerts = nil\n\t\terr = checkGroup(group, newGroup)\n\t}\n\treturn newGroup, body, err\n}\n\n// UpdateGroup updates an existing group and checks the received HTTP Status code against expectedStatusCode\nfunc UpdateGroup(group dataprovider.Group, expectedStatusCode int) (dataprovider.Group, []byte, error) {\n\tvar newGroup dataprovider.Group\n\tvar body []byte\n\n\tasJSON, _ := json.Marshal(group)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(groupPath, url.PathEscape(group.Name)),\n\t\tbytes.NewBuffer(asJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newGroup, body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn newGroup, body, err\n\t}\n\tif err == nil {\n\t\tnewGroup, body, err = GetGroupByName(group.Name, expectedStatusCode)\n\t}\n\tif err == nil {\n\t\terr = checkGroup(group, newGroup)\n\t}\n\treturn newGroup, body, err\n}\n\n// RemoveGroup removes an existing group and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveGroup(group dataprovider.Group, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(groupPath, url.PathEscape(group.Name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetGroupByName gets a group by name and checks the received HTTP Status code against expectedStatusCode.\nfunc GetGroupByName(name string, expectedStatusCode int) (dataprovider.Group, []byte, error) {\n\tvar group dataprovider.Group\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(groupPath, url.PathEscape(name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn group, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &group)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn group, body, err\n}\n\n// GetGroups returns a list of groups and checks the received HTTP Status code against expectedStatusCode.\n// The number of results can be limited specifying a limit.\n// Some results can be skipped specifying an offset.\nfunc GetGroups(limit, offset int64, expectedStatusCode int) ([]dataprovider.Group, []byte, error) {\n\tvar groups []dataprovider.Group\n\tvar body []byte\n\turl, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(groupPath), limit, offset)\n\tif err != nil {\n\t\treturn groups, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn groups, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &groups)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn groups, body, err\n}\n\n// AddRole adds a new role and checks the received HTTP Status code against expectedStatusCode.\nfunc AddRole(role dataprovider.Role, expectedStatusCode int) (dataprovider.Role, []byte, error) {\n\tvar newRole dataprovider.Role\n\tvar body []byte\n\tasJSON, _ := json.Marshal(role)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(rolesPath), bytes.NewBuffer(asJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newRole, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newRole, body, err\n\t}\n\tif err == nil {\n\t\terr = render.DecodeJSON(resp.Body, &newRole)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\tif err == nil {\n\t\terr = checkRole(role, newRole)\n\t}\n\treturn newRole, body, err\n}\n\n// UpdateRole updates an existing role and checks the received HTTP Status code against expectedStatusCode\nfunc UpdateRole(role dataprovider.Role, expectedStatusCode int) (dataprovider.Role, []byte, error) {\n\tvar newRole dataprovider.Role\n\tvar body []byte\n\n\tasJSON, _ := json.Marshal(role)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(rolesPath, url.PathEscape(role.Name)),\n\t\tbytes.NewBuffer(asJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newRole, body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn newRole, body, err\n\t}\n\tif err == nil {\n\t\tnewRole, body, err = GetRoleByName(role.Name, expectedStatusCode)\n\t}\n\tif err == nil {\n\t\terr = checkRole(role, newRole)\n\t}\n\treturn newRole, body, err\n}\n\n// RemoveRole removes an existing role and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveRole(role dataprovider.Role, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(rolesPath, url.PathEscape(role.Name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetRoleByName gets a role by name and checks the received HTTP Status code against expectedStatusCode.\nfunc GetRoleByName(name string, expectedStatusCode int) (dataprovider.Role, []byte, error) {\n\tvar role dataprovider.Role\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(rolesPath, url.PathEscape(name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn role, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &role)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn role, body, err\n}\n\n// GetRoles returns a list of roles and checks the received HTTP Status code against expectedStatusCode.\n// The number of results can be limited specifying a limit.\n// Some results can be skipped specifying an offset.\nfunc GetRoles(limit, offset int64, expectedStatusCode int) ([]dataprovider.Role, []byte, error) {\n\tvar roles []dataprovider.Role\n\tvar body []byte\n\turl, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(rolesPath), limit, offset)\n\tif err != nil {\n\t\treturn roles, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn roles, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &roles)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn roles, body, err\n}\n\n// AddIPListEntry adds a new IP list entry and checks the received HTTP Status code against expectedStatusCode.\nfunc AddIPListEntry(entry dataprovider.IPListEntry, expectedStatusCode int) (dataprovider.IPListEntry, []byte, error) {\n\tvar newEntry dataprovider.IPListEntry\n\tvar body []byte\n\n\tasJSON, _ := json.Marshal(entry)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(ipListsPath, strconv.Itoa(int(entry.Type))),\n\t\tbytes.NewBuffer(asJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newEntry, body, err\n\t}\n\tdefer resp.Body.Close()\n\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newEntry, body, err\n\t}\n\tif err == nil {\n\t\tnewEntry, body, err = GetIPListEntry(entry.IPOrNet, entry.Type, http.StatusOK)\n\t}\n\tif err == nil {\n\t\terr = checkIPListEntry(entry, newEntry)\n\t}\n\treturn newEntry, body, err\n}\n\n// UpdateIPListEntry updates an existing IP list entry and checks the received HTTP Status code against expectedStatusCode\nfunc UpdateIPListEntry(entry dataprovider.IPListEntry, expectedStatusCode int) (dataprovider.IPListEntry, []byte, error) {\n\tvar newEntry dataprovider.IPListEntry\n\tvar body []byte\n\n\tasJSON, _ := json.Marshal(entry)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(ipListsPath, fmt.Sprintf(\"%d\", entry.Type),\n\t\turl.PathEscape(entry.IPOrNet)), bytes.NewBuffer(asJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newEntry, body, err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ = getResponseBody(resp)\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn newEntry, body, err\n\t}\n\tif err == nil {\n\t\tnewEntry, body, err = GetIPListEntry(entry.IPOrNet, entry.Type, http.StatusOK)\n\t}\n\tif err == nil {\n\t\terr = checkIPListEntry(entry, newEntry)\n\t}\n\treturn newEntry, body, err\n}\n\n// RemoveIPListEntry removes an existing IP list entry and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveIPListEntry(entry dataprovider.IPListEntry, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(ipListsPath, fmt.Sprintf(\"%d\", entry.Type),\n\t\turl.PathEscape(entry.IPOrNet)), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetIPListEntry returns an IP list entry matching the specified parameters, if exists,\n// and checks the received HTTP Status code against expectedStatusCode.\nfunc GetIPListEntry(ipOrNet string, listType dataprovider.IPListType, expectedStatusCode int,\n) (dataprovider.IPListEntry, []byte, error) {\n\tvar entry dataprovider.IPListEntry\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(ipListsPath, fmt.Sprintf(\"%d\", listType), url.PathEscape(ipOrNet)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn entry, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &entry)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn entry, body, err\n}\n\n// GetIPListEntries returns a list of IP list entries and checks the received HTTP Status code against expectedStatusCode.\nfunc GetIPListEntries(listType dataprovider.IPListType, filter, from, order string, limit int64,\n\texpectedStatusCode int,\n) ([]dataprovider.IPListEntry, []byte, error) {\n\tvar entries []dataprovider.IPListEntry\n\tvar body []byte\n\n\turl, err := url.Parse(buildURLRelativeToBase(ipListsPath, strconv.Itoa(int(listType))))\n\tif err != nil {\n\t\treturn entries, body, err\n\t}\n\tq := url.Query()\n\tq.Add(\"filter\", filter)\n\tq.Add(\"from\", from)\n\tq.Add(\"order\", order)\n\tif limit > 0 {\n\t\tq.Add(\"limit\", strconv.FormatInt(limit, 10))\n\t}\n\turl.RawQuery = q.Encode()\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn entries, body, err\n\t}\n\tdefer resp.Body.Close()\n\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &entries)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn entries, body, err\n}\n\n// AddAdmin adds a new admin and checks the received HTTP Status code against expectedStatusCode.\nfunc AddAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) {\n\tvar newAdmin dataprovider.Admin\n\tvar body []byte\n\tasJSON, _ := json.Marshal(admin)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(adminPath), bytes.NewBuffer(asJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newAdmin, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newAdmin, body, err\n\t}\n\tif err == nil {\n\t\terr = render.DecodeJSON(resp.Body, &newAdmin)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\tif err == nil {\n\t\terr = checkAdmin(&admin, &newAdmin)\n\t}\n\treturn newAdmin, body, err\n}\n\n// UpdateAdmin updates an existing admin and checks the received HTTP Status code against expectedStatusCode\nfunc UpdateAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) {\n\tvar newAdmin dataprovider.Admin\n\tvar body []byte\n\n\tasJSON, _ := json.Marshal(admin)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(adminPath, url.PathEscape(admin.Username)),\n\t\tbytes.NewBuffer(asJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newAdmin, body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn newAdmin, body, err\n\t}\n\tif err == nil {\n\t\tnewAdmin, body, err = GetAdminByUsername(admin.Username, expectedStatusCode)\n\t}\n\tif err == nil {\n\t\terr = checkAdmin(&admin, &newAdmin)\n\t}\n\treturn newAdmin, body, err\n}\n\n// RemoveAdmin removes an existing admin and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveAdmin(admin dataprovider.Admin, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(adminPath, url.PathEscape(admin.Username)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetAdminByUsername gets an admin by username and checks the received HTTP Status code against expectedStatusCode.\nfunc GetAdminByUsername(username string, expectedStatusCode int) (dataprovider.Admin, []byte, error) {\n\tvar admin dataprovider.Admin\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(adminPath, url.PathEscape(username)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn admin, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &admin)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn admin, body, err\n}\n\n// GetAdmins returns a list of admins and checks the received HTTP Status code against expectedStatusCode.\n// The number of results can be limited specifying a limit.\n// Some results can be skipped specifying an offset.\nfunc GetAdmins(limit, offset int64, expectedStatusCode int) ([]dataprovider.Admin, []byte, error) {\n\tvar admins []dataprovider.Admin\n\tvar body []byte\n\turl, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(adminPath), limit, offset)\n\tif err != nil {\n\t\treturn admins, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn admins, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &admins)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn admins, body, err\n}\n\n// ChangeAdminPassword changes the password for an existing admin\nfunc ChangeAdminPassword(currentPassword, newPassword string, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\n\tpwdChange := make(map[string]string)\n\tpwdChange[\"current_password\"] = currentPassword\n\tpwdChange[\"new_password\"] = newPassword\n\n\tasJSON, _ := json.Marshal(&pwdChange)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(adminPwdPath),\n\t\tbytes.NewBuffer(asJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tbody, _ = getResponseBody(resp)\n\n\treturn body, err\n}\n\n// GetAPIKeys returns a list of API keys and checks the received HTTP Status code against expectedStatusCode.\n// The number of results can be limited specifying a limit.\n// Some results can be skipped specifying an offset.\nfunc GetAPIKeys(limit, offset int64, expectedStatusCode int) ([]dataprovider.APIKey, []byte, error) {\n\tvar apiKeys []dataprovider.APIKey\n\tvar body []byte\n\turl, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(apiKeysPath), limit, offset)\n\tif err != nil {\n\t\treturn apiKeys, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn apiKeys, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &apiKeys)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn apiKeys, body, err\n}\n\n// AddAPIKey adds a new API key and checks the received HTTP Status code against expectedStatusCode.\nfunc AddAPIKey(apiKey dataprovider.APIKey, expectedStatusCode int) (dataprovider.APIKey, []byte, error) {\n\tvar newAPIKey dataprovider.APIKey\n\tvar body []byte\n\tasJSON, _ := json.Marshal(apiKey)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(apiKeysPath), bytes.NewBuffer(asJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newAPIKey, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newAPIKey, body, err\n\t}\n\tif err != nil {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newAPIKey, body, err\n\t}\n\tresponse := make(map[string]string)\n\terr = render.DecodeJSON(resp.Body, &response)\n\tif err == nil {\n\t\tnewAPIKey, body, err = GetAPIKeyByID(resp.Header.Get(\"X-Object-ID\"), http.StatusOK)\n\t}\n\tif err == nil {\n\t\terr = checkAPIKey(&apiKey, &newAPIKey)\n\t}\n\tnewAPIKey.Key = response[\"key\"]\n\n\treturn newAPIKey, body, err\n}\n\n// UpdateAPIKey updates an existing API key and checks the received HTTP Status code against expectedStatusCode\nfunc UpdateAPIKey(apiKey dataprovider.APIKey, expectedStatusCode int) (dataprovider.APIKey, []byte, error) {\n\tvar newAPIKey dataprovider.APIKey\n\tvar body []byte\n\n\tasJSON, _ := json.Marshal(apiKey)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(apiKeysPath, url.PathEscape(apiKey.KeyID)),\n\t\tbytes.NewBuffer(asJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newAPIKey, body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn newAPIKey, body, err\n\t}\n\tif err == nil {\n\t\tnewAPIKey, body, err = GetAPIKeyByID(apiKey.KeyID, expectedStatusCode)\n\t}\n\tif err == nil {\n\t\terr = checkAPIKey(&apiKey, &newAPIKey)\n\t}\n\treturn newAPIKey, body, err\n}\n\n// RemoveAPIKey removes an existing API key and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveAPIKey(apiKey dataprovider.APIKey, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(apiKeysPath, url.PathEscape(apiKey.KeyID)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetAPIKeyByID gets a API key by ID and checks the received HTTP Status code against expectedStatusCode.\nfunc GetAPIKeyByID(keyID string, expectedStatusCode int) (dataprovider.APIKey, []byte, error) {\n\tvar apiKey dataprovider.APIKey\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(apiKeysPath, url.PathEscape(keyID)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn apiKey, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &apiKey)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn apiKey, body, err\n}\n\n// AddEventAction adds a new event action\nfunc AddEventAction(action dataprovider.BaseEventAction, expectedStatusCode int) (dataprovider.BaseEventAction, []byte, error) {\n\tvar newAction dataprovider.BaseEventAction\n\tvar body []byte\n\tasJSON, _ := json.Marshal(action)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(eventActionsPath), bytes.NewBuffer(asJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newAction, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newAction, body, err\n\t}\n\tif err == nil {\n\t\terr = render.DecodeJSON(resp.Body, &newAction)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\tif err == nil {\n\t\terr = checkEventAction(action, newAction)\n\t}\n\treturn newAction, body, err\n}\n\n// UpdateEventAction updates an existing event action\nfunc UpdateEventAction(action dataprovider.BaseEventAction, expectedStatusCode int) (dataprovider.BaseEventAction, []byte, error) {\n\tvar newAction dataprovider.BaseEventAction\n\tvar body []byte\n\n\tasJSON, _ := json.Marshal(action)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(eventActionsPath, url.PathEscape(action.Name)),\n\t\tbytes.NewBuffer(asJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newAction, body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn newAction, body, err\n\t}\n\tif err == nil {\n\t\tnewAction, body, err = GetEventActionByName(action.Name, expectedStatusCode)\n\t}\n\tif err == nil {\n\t\terr = checkEventAction(action, newAction)\n\t}\n\treturn newAction, body, err\n}\n\n// RemoveEventAction removes an existing action and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveEventAction(action dataprovider.BaseEventAction, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(eventActionsPath, url.PathEscape(action.Name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetEventActionByName gets an event action by name and checks the received HTTP Status code against expectedStatusCode.\nfunc GetEventActionByName(name string, expectedStatusCode int) (dataprovider.BaseEventAction, []byte, error) {\n\tvar action dataprovider.BaseEventAction\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(eventActionsPath, url.PathEscape(name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn action, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &action)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn action, body, err\n}\n\n// GetEventActions returns a list of event actions and checks the received HTTP Status code against expectedStatusCode.\n// The number of results can be limited specifying a limit.\n// Some results can be skipped specifying an offset.\nfunc GetEventActions(limit, offset int64, expectedStatusCode int) ([]dataprovider.BaseEventAction, []byte, error) {\n\tvar actions []dataprovider.BaseEventAction\n\tvar body []byte\n\turl, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(eventActionsPath), limit, offset)\n\tif err != nil {\n\t\treturn actions, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn actions, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &actions)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn actions, body, err\n}\n\n// AddEventRule adds a new event rule\nfunc AddEventRule(rule dataprovider.EventRule, expectedStatusCode int) (dataprovider.EventRule, []byte, error) {\n\tvar newRule dataprovider.EventRule\n\tvar body []byte\n\tasJSON, _ := json.Marshal(rule)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(eventRulesPath), bytes.NewBuffer(asJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newRule, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newRule, body, err\n\t}\n\tif err == nil {\n\t\terr = render.DecodeJSON(resp.Body, &newRule)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\tif err == nil {\n\t\terr = checkEventRule(rule, newRule)\n\t}\n\treturn newRule, body, err\n}\n\n// UpdateEventRule updates an existing event rule\nfunc UpdateEventRule(rule dataprovider.EventRule, expectedStatusCode int) (dataprovider.EventRule, []byte, error) {\n\tvar newRule dataprovider.EventRule\n\tvar body []byte\n\n\tasJSON, _ := json.Marshal(rule)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(eventRulesPath, url.PathEscape(rule.Name)),\n\t\tbytes.NewBuffer(asJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newRule, body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn newRule, body, err\n\t}\n\tif err == nil {\n\t\tnewRule, body, err = GetEventRuleByName(rule.Name, expectedStatusCode)\n\t}\n\tif err == nil {\n\t\terr = checkEventRule(rule, newRule)\n\t}\n\treturn newRule, body, err\n}\n\n// RemoveEventRule removes an existing rule and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveEventRule(rule dataprovider.EventRule, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(eventRulesPath, url.PathEscape(rule.Name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetEventRuleByName gets an event rule by name and checks the received HTTP Status code against expectedStatusCode.\nfunc GetEventRuleByName(name string, expectedStatusCode int) (dataprovider.EventRule, []byte, error) {\n\tvar rule dataprovider.EventRule\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(eventRulesPath, url.PathEscape(name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn rule, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &rule)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn rule, body, err\n}\n\n// GetEventRules returns a list of event rules and checks the received HTTP Status code against expectedStatusCode.\n// The number of results can be limited specifying a limit.\n// Some results can be skipped specifying an offset.\nfunc GetEventRules(limit, offset int64, expectedStatusCode int) ([]dataprovider.EventRule, []byte, error) {\n\tvar rules []dataprovider.EventRule\n\tvar body []byte\n\turl, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(eventRulesPath), limit, offset)\n\tif err != nil {\n\t\treturn rules, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn rules, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &rules)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn rules, body, err\n}\n\n// RunOnDemandRule executes the specified on demand rule\nfunc RunOnDemandRule(name string, expectedStatusCode int) ([]byte, error) {\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(eventRulesPath, \"run\", url.PathEscape(name)),\n\t\tnil, \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tb, err := getResponseBody(resp)\n\tif err != nil {\n\t\treturn b, err\n\t}\n\tif err := checkResponse(resp.StatusCode, expectedStatusCode); err != nil {\n\t\treturn b, err\n\t}\n\treturn b, nil\n}\n\n// GetQuotaScans gets active quota scans for users and checks the received HTTP Status code against expectedStatusCode.\nfunc GetQuotaScans(expectedStatusCode int) ([]common.ActiveQuotaScan, []byte, error) {\n\tvar quotaScans []common.ActiveQuotaScan\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanPath), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn quotaScans, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &quotaScans)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn quotaScans, body, err\n}\n\n// StartQuotaScan starts a new quota scan for the given user and checks the received HTTP Status code against expectedStatusCode.\nfunc StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotasBasePath, \"users\", user.Username, \"scan\"),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// UpdateQuotaUsage updates the user used quota limits and checks the received\n// HTTP Status code against expectedStatusCode.\nfunc UpdateQuotaUsage(user dataprovider.User, mode string, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tuserAsJSON, _ := json.Marshal(user)\n\turl, err := addModeQueryParam(buildURLRelativeToBase(quotasBasePath, \"users\", user.Username, \"usage\"), mode)\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), \"application/json\",\n\t\tgetDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// UpdateTransferQuotaUsage updates the user used transfer quota limits and checks the received\n// HTTP Status code against expectedStatusCode.\nfunc UpdateTransferQuotaUsage(user dataprovider.User, mode string, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tuserAsJSON, _ := json.Marshal(user)\n\turl, err := addModeQueryParam(buildURLRelativeToBase(quotasBasePath, \"users\", user.Username, \"transfer-usage\"), mode)\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), \"application/json\",\n\t\tgetDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetRetentionChecks returns the active retention checks\nfunc GetRetentionChecks(expectedStatusCode int) ([]common.ActiveRetentionChecks, []byte, error) {\n\tvar checks []common.ActiveRetentionChecks\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(retentionChecksPath), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn checks, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &checks)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn checks, body, err\n}\n\n// GetConnections returns status and stats for active SFTP/SCP connections\nfunc GetConnections(expectedStatusCode int) ([]common.ConnectionStatus, []byte, error) {\n\tvar connections []common.ConnectionStatus\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(activeConnectionsPath), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn connections, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &connections)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn connections, body, err\n}\n\n// CloseConnection closes an active  connection identified by connectionID\nfunc CloseConnection(connectionID string, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(activeConnectionsPath, connectionID),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tbody, _ = getResponseBody(resp)\n\treturn body, err\n}\n\n// AddFolder adds a new folder and checks the received HTTP Status code against expectedStatusCode\nfunc AddFolder(folder vfs.BaseVirtualFolder, expectedStatusCode int) (vfs.BaseVirtualFolder, []byte, error) {\n\tvar newFolder vfs.BaseVirtualFolder\n\tvar body []byte\n\tfolderAsJSON, _ := json.Marshal(folder)\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(folderPath), bytes.NewBuffer(folderAsJSON),\n\t\t\"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn newFolder, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusCreated {\n\t\tbody, _ = getResponseBody(resp)\n\t\treturn newFolder, body, err\n\t}\n\tif err == nil {\n\t\terr = render.DecodeJSON(resp.Body, &newFolder)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\tif err == nil {\n\t\terr = checkFolder(&folder, &newFolder)\n\t}\n\treturn newFolder, body, err\n}\n\n// UpdateFolder updates an existing folder and checks the received HTTP Status code against expectedStatusCode.\nfunc UpdateFolder(folder vfs.BaseVirtualFolder, expectedStatusCode int) (vfs.BaseVirtualFolder, []byte, error) {\n\tvar updatedFolder vfs.BaseVirtualFolder\n\tvar body []byte\n\n\tfolderAsJSON, _ := json.Marshal(folder)\n\tresp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(folderPath, url.PathEscape(folder.Name)),\n\t\tbytes.NewBuffer(folderAsJSON), \"application/json\", getDefaultToken())\n\tif err != nil {\n\t\treturn updatedFolder, body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif expectedStatusCode != http.StatusOK {\n\t\treturn updatedFolder, body, err\n\t}\n\tif err == nil {\n\t\tupdatedFolder, body, err = GetFolderByName(folder.Name, expectedStatusCode)\n\t}\n\tif err == nil {\n\t\terr = checkFolder(&folder, &updatedFolder)\n\t}\n\treturn updatedFolder, body, err\n}\n\n// RemoveFolder removes an existing user and checks the received HTTP Status code against expectedStatusCode.\nfunc RemoveFolder(folder vfs.BaseVirtualFolder, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(folderPath, url.PathEscape(folder.Name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetFolderByName gets a folder by name and checks the received HTTP Status code against expectedStatusCode.\nfunc GetFolderByName(name string, expectedStatusCode int) (vfs.BaseVirtualFolder, []byte, error) {\n\tvar folder vfs.BaseVirtualFolder\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(folderPath, url.PathEscape(name)),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn folder, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &folder)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn folder, body, err\n}\n\n// GetFolders returns a list of folders and checks the received HTTP Status code against expectedStatusCode.\n// The number of results can be limited specifying a limit.\n// Some results can be skipped specifying an offset.\n// The results can be filtered specifying a folder path, the folder path filter is an exact match\nfunc GetFolders(limit int64, offset int64, expectedStatusCode int) ([]vfs.BaseVirtualFolder, []byte, error) {\n\tvar folders []vfs.BaseVirtualFolder\n\tvar body []byte\n\turl, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(folderPath), limit, offset)\n\tif err != nil {\n\t\treturn folders, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn folders, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &folders)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn folders, body, err\n}\n\n// GetFoldersQuotaScans gets active quota scans for folders and checks the received HTTP Status code against expectedStatusCode.\nfunc GetFoldersQuotaScans(expectedStatusCode int) ([]common.ActiveVirtualFolderQuotaScan, []byte, error) {\n\tvar quotaScans []common.ActiveVirtualFolderQuotaScan\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanVFolderPath), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn quotaScans, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &quotaScans)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn quotaScans, body, err\n}\n\n// StartFolderQuotaScan start a new quota scan for the given folder and checks the received HTTP Status code against expectedStatusCode.\nfunc StartFolderQuotaScan(folder vfs.BaseVirtualFolder, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotasBasePath, \"folders\", folder.Name, \"scan\"),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// UpdateFolderQuotaUsage updates the folder used quota limits and checks the received HTTP Status code against expectedStatusCode.\nfunc UpdateFolderQuotaUsage(folder vfs.BaseVirtualFolder, mode string, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tfolderAsJSON, _ := json.Marshal(folder)\n\turl, err := addModeQueryParam(buildURLRelativeToBase(quotasBasePath, \"folders\", folder.Name, \"usage\"), mode)\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(folderAsJSON), \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// GetVersion returns version details\nfunc GetVersion(expectedStatusCode int) (version.Info, []byte, error) {\n\tvar appVersion version.Info\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(versionPath), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn appVersion, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &appVersion)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn appVersion, body, err\n}\n\n// GetStatus returns the server status\nfunc GetStatus(expectedStatusCode int) (httpd.ServicesStatus, []byte, error) {\n\tvar response httpd.ServicesStatus\n\tvar body []byte\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(serverStatusPath), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && (expectedStatusCode == http.StatusOK) {\n\t\terr = render.DecodeJSON(resp.Body, &response)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn response, body, err\n}\n\n// GetDefenderHosts returns hosts that are banned or for which some violations have been detected\nfunc GetDefenderHosts(expectedStatusCode int) ([]dataprovider.DefenderEntry, []byte, error) {\n\tvar response []dataprovider.DefenderEntry\n\tvar body []byte\n\turl, err := url.Parse(buildURLRelativeToBase(defenderHosts))\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &response)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn response, body, err\n}\n\n// GetDefenderHostByIP returns the host with the given IP, if it exists\nfunc GetDefenderHostByIP(ip string, expectedStatusCode int) (dataprovider.DefenderEntry, []byte, error) {\n\tvar host dataprovider.DefenderEntry\n\tvar body []byte\n\tid := hex.EncodeToString([]byte(ip))\n\tresp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(defenderHosts, id),\n\t\tnil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn host, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &host)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn host, body, err\n}\n\n// RemoveDefenderHostByIP removes the host with the given IP from the defender list\nfunc RemoveDefenderHostByIP(ip string, expectedStatusCode int) ([]byte, error) {\n\tvar body []byte\n\tid := hex.EncodeToString([]byte(ip))\n\tresp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(defenderHosts, id), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn body, err\n\t}\n\tdefer resp.Body.Close()\n\tbody, _ = getResponseBody(resp)\n\treturn body, checkResponse(resp.StatusCode, expectedStatusCode)\n}\n\n// Dumpdata requests a backup to outputFile.\n// outputFile is relative to the configured backups_path\nfunc Dumpdata(outputFile, outputData, indent string, expectedStatusCode int, scopes ...string) (map[string]any, []byte, error) {\n\tvar response map[string]any\n\tvar body []byte\n\turl, err := url.Parse(buildURLRelativeToBase(dumpDataPath))\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tq := url.Query()\n\tif outputData != \"\" {\n\t\tq.Add(\"output-data\", outputData)\n\t}\n\tif outputFile != \"\" {\n\t\tq.Add(\"output-file\", outputFile)\n\t}\n\tif indent != \"\" {\n\t\tq.Add(\"indent\", indent)\n\t}\n\tif len(scopes) > 0 {\n\t\tq.Add(\"scopes\", strings.Join(scopes, \",\"))\n\t}\n\turl.RawQuery = q.Encode()\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &response)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn response, body, err\n}\n\n// Loaddata restores a backup.\nfunc Loaddata(inputFile, scanQuota, mode string, expectedStatusCode int) (map[string]any, []byte, error) {\n\tvar response map[string]any\n\tvar body []byte\n\turl, err := url.Parse(buildURLRelativeToBase(loadDataPath))\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tq := url.Query()\n\tq.Add(\"input-file\", inputFile)\n\tif scanQuota != \"\" {\n\t\tq.Add(\"scan-quota\", scanQuota)\n\t}\n\tif mode != \"\" {\n\t\tq.Add(\"mode\", mode)\n\t}\n\turl.RawQuery = q.Encode()\n\tresp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &response)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn response, body, err\n}\n\n// LoaddataFromPostBody restores a backup\nfunc LoaddataFromPostBody(data []byte, scanQuota, mode string, expectedStatusCode int) (map[string]any, []byte, error) {\n\tvar response map[string]any\n\tvar body []byte\n\turl, err := url.Parse(buildURLRelativeToBase(loadDataPath))\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tq := url.Query()\n\tif scanQuota != \"\" {\n\t\tq.Add(\"scan-quota\", scanQuota)\n\t}\n\tif mode != \"\" {\n\t\tq.Add(\"mode\", mode)\n\t}\n\turl.RawQuery = q.Encode()\n\tresp, err := sendHTTPRequest(http.MethodPost, url.String(), bytes.NewReader(data), \"\", getDefaultToken())\n\tif err != nil {\n\t\treturn response, body, err\n\t}\n\tdefer resp.Body.Close()\n\terr = checkResponse(resp.StatusCode, expectedStatusCode)\n\tif err == nil && expectedStatusCode == http.StatusOK {\n\t\terr = render.DecodeJSON(resp.Body, &response)\n\t} else {\n\t\tbody, _ = getResponseBody(resp)\n\t}\n\treturn response, body, err\n}\n\nfunc checkResponse(actual int, expected int) error {\n\tif expected != actual {\n\t\treturn fmt.Errorf(\"wrong status code: got %v want %v\", actual, expected)\n\t}\n\treturn nil\n}\n\nfunc getResponseBody(resp *http.Response) ([]byte, error) {\n\treturn io.ReadAll(resp.Body)\n}\n\nfunc checkEventAction(expected, actual dataprovider.BaseEventAction) error {\n\tif expected.ID <= 0 {\n\t\tif actual.ID <= 0 {\n\t\t\treturn errors.New(\"actual action ID must be > 0\")\n\t\t}\n\t} else {\n\t\tif actual.ID != expected.ID {\n\t\t\treturn errors.New(\"action ID mismatch\")\n\t\t}\n\t}\n\tif dataprovider.ConvertName(expected.Name) != actual.Name {\n\t\treturn errors.New(\"name mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\tif expected.Type != actual.Type {\n\t\treturn errors.New(\"type mismatch\")\n\t}\n\tif expected.Options.PwdExpirationConfig.Threshold != actual.Options.PwdExpirationConfig.Threshold {\n\t\treturn errors.New(\"password expiration threshold mismatch\")\n\t}\n\tif expected.Options.UserInactivityConfig.DisableThreshold != actual.Options.UserInactivityConfig.DisableThreshold {\n\t\treturn errors.New(\"user inactivity disable threshold mismatch\")\n\t}\n\tif expected.Options.UserInactivityConfig.DeleteThreshold != actual.Options.UserInactivityConfig.DeleteThreshold {\n\t\treturn errors.New(\"user inactivity delete threshold mismatch\")\n\t}\n\tif err := compareEventActionIDPConfigFields(expected.Options.IDPConfig, actual.Options.IDPConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := compareEventActionCmdConfigFields(expected.Options.CmdConfig, actual.Options.CmdConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := compareEventActionEmailConfigFields(expected.Options.EmailConfig, actual.Options.EmailConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := compareEventActionDataRetentionFields(expected.Options.RetentionConfig, actual.Options.RetentionConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := compareEventActionFsConfigFields(expected.Options.FsConfig, actual.Options.FsConfig); err != nil {\n\t\treturn err\n\t}\n\treturn compareEventActionHTTPConfigFields(expected.Options.HTTPConfig, actual.Options.HTTPConfig)\n}\n\nfunc checkEventSchedules(expected, actual []dataprovider.Schedule) error {\n\tif len(expected) != len(actual) {\n\t\treturn errors.New(\"schedules mismatch\")\n\t}\n\tfor _, ex := range expected {\n\t\tfound := false\n\t\tfor _, ac := range actual {\n\t\t\tif ac.DayOfMonth == ex.DayOfMonth && ac.DayOfWeek == ex.DayOfWeek && ac.Hours == ex.Hours && ac.Month == ex.Month {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"schedules content mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareConditionPatternOptions(expected, actual []dataprovider.ConditionPattern) error {\n\tif len(expected) != len(actual) {\n\t\treturn errors.New(\"condition pattern mismatch\")\n\t}\n\tfor _, ex := range expected {\n\t\tfound := false\n\t\tfor _, ac := range actual {\n\t\t\tif ac.Pattern == ex.Pattern && ac.InverseMatch == ex.InverseMatch {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"condition pattern content mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkEventConditionOptions(expected, actual dataprovider.ConditionOptions) error { //nolint:gocyclo\n\tif err := compareConditionPatternOptions(expected.Names, actual.Names); err != nil {\n\t\treturn errors.New(\"condition names mismatch\")\n\t}\n\tif err := compareConditionPatternOptions(expected.GroupNames, actual.GroupNames); err != nil {\n\t\treturn errors.New(\"condition group names mismatch\")\n\t}\n\tif err := compareConditionPatternOptions(expected.RoleNames, actual.RoleNames); err != nil {\n\t\treturn errors.New(\"condition role names mismatch\")\n\t}\n\tif err := compareConditionPatternOptions(expected.FsPaths, actual.FsPaths); err != nil {\n\t\treturn errors.New(\"condition fs_paths mismatch\")\n\t}\n\tif len(expected.Protocols) != len(actual.Protocols) {\n\t\treturn errors.New(\"condition protocols mismatch\")\n\t}\n\tfor _, v := range expected.Protocols {\n\t\tif !slices.Contains(actual.Protocols, v) {\n\t\t\treturn errors.New(\"condition protocols content mismatch\")\n\t\t}\n\t}\n\tif len(expected.EventStatuses) != len(actual.EventStatuses) {\n\t\treturn errors.New(\"condition statuses mismatch\")\n\t}\n\tfor _, v := range expected.EventStatuses {\n\t\tif !slices.Contains(actual.EventStatuses, v) {\n\t\t\treturn errors.New(\"condition statuses content mismatch\")\n\t\t}\n\t}\n\tif len(expected.ProviderObjects) != len(actual.ProviderObjects) {\n\t\treturn errors.New(\"condition provider objects mismatch\")\n\t}\n\tfor _, v := range expected.ProviderObjects {\n\t\tif !slices.Contains(actual.ProviderObjects, v) {\n\t\t\treturn errors.New(\"condition provider objects content mismatch\")\n\t\t}\n\t}\n\tif expected.MinFileSize != actual.MinFileSize {\n\t\treturn errors.New(\"condition min file size mismatch\")\n\t}\n\tif expected.MaxFileSize != actual.MaxFileSize {\n\t\treturn errors.New(\"condition max file size mismatch\")\n\t}\n\treturn nil\n}\n\nfunc checkEventConditions(expected, actual dataprovider.EventConditions) error {\n\tif len(expected.FsEvents) != len(actual.FsEvents) {\n\t\treturn errors.New(\"fs events mismatch\")\n\t}\n\tfor _, v := range expected.FsEvents {\n\t\tif !slices.Contains(actual.FsEvents, v) {\n\t\t\treturn errors.New(\"fs events content mismatch\")\n\t\t}\n\t}\n\tif len(expected.ProviderEvents) != len(actual.ProviderEvents) {\n\t\treturn errors.New(\"provider events mismatch\")\n\t}\n\tfor _, v := range expected.ProviderEvents {\n\t\tif !slices.Contains(actual.ProviderEvents, v) {\n\t\t\treturn errors.New(\"provider events content mismatch\")\n\t\t}\n\t}\n\tif err := checkEventConditionOptions(expected.Options, actual.Options); err != nil {\n\t\treturn err\n\t}\n\tif expected.IDPLoginEvent != actual.IDPLoginEvent {\n\t\treturn errors.New(\"IDP login event mismatch\")\n\t}\n\n\treturn checkEventSchedules(expected.Schedules, actual.Schedules)\n}\n\nfunc checkEventRuleActions(expected, actual []dataprovider.EventAction) error {\n\tif len(expected) != len(actual) {\n\t\treturn errors.New(\"actions mismatch\")\n\t}\n\tfor _, ex := range expected {\n\t\tfound := false\n\t\tfor _, ac := range actual {\n\t\t\tif ex.Name == ac.Name && ex.Order == ac.Order && ex.Options.ExecuteSync == ac.Options.ExecuteSync &&\n\t\t\t\tex.Options.IsFailureAction == ac.Options.IsFailureAction && ex.Options.StopOnFailure == ac.Options.StopOnFailure {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"actions contents mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkEventRule(expected, actual dataprovider.EventRule) error {\n\tif expected.ID <= 0 {\n\t\tif actual.ID <= 0 {\n\t\t\treturn errors.New(\"actual group ID must be > 0\")\n\t\t}\n\t} else {\n\t\tif actual.ID != expected.ID {\n\t\t\treturn errors.New(\"group ID mismatch\")\n\t\t}\n\t}\n\tif dataprovider.ConvertName(expected.Name) != actual.Name {\n\t\treturn errors.New(\"name mismatch\")\n\t}\n\tif expected.Status != actual.Status {\n\t\treturn errors.New(\"status mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\tif actual.CreatedAt == 0 {\n\t\treturn errors.New(\"created_at unset\")\n\t}\n\tif actual.UpdatedAt == 0 {\n\t\treturn errors.New(\"updated_at unset\")\n\t}\n\tif expected.Trigger != actual.Trigger {\n\t\treturn errors.New(\"trigger mismatch\")\n\t}\n\tif err := checkEventConditions(expected.Conditions, actual.Conditions); err != nil {\n\t\treturn err\n\t}\n\treturn checkEventRuleActions(expected.Actions, actual.Actions)\n}\n\nfunc checkIPListEntry(expected, actual dataprovider.IPListEntry) error {\n\tif expected.IPOrNet != actual.IPOrNet {\n\t\treturn errors.New(\"ipornet mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\tif expected.Type != actual.Type {\n\t\treturn errors.New(\"type mismatch\")\n\t}\n\tif expected.Mode != actual.Mode {\n\t\treturn errors.New(\"mode mismatch\")\n\t}\n\tif expected.Protocols != actual.Protocols {\n\t\treturn errors.New(\"protocols mismatch\")\n\t}\n\tif actual.CreatedAt == 0 {\n\t\treturn errors.New(\"created_at unset\")\n\t}\n\tif actual.UpdatedAt == 0 {\n\t\treturn errors.New(\"updated_at unset\")\n\t}\n\treturn nil\n}\n\nfunc checkRole(expected, actual dataprovider.Role) error {\n\tif expected.ID <= 0 {\n\t\tif actual.ID <= 0 {\n\t\t\treturn errors.New(\"actual role ID must be > 0\")\n\t\t}\n\t} else {\n\t\tif actual.ID != expected.ID {\n\t\t\treturn errors.New(\"role ID mismatch\")\n\t\t}\n\t}\n\tif dataprovider.ConvertName(expected.Name) != actual.Name {\n\t\treturn errors.New(\"name mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\tif actual.CreatedAt == 0 {\n\t\treturn errors.New(\"created_at unset\")\n\t}\n\tif actual.UpdatedAt == 0 {\n\t\treturn errors.New(\"updated_at unset\")\n\t}\n\treturn nil\n}\n\nfunc checkGroup(expected, actual dataprovider.Group) error {\n\tif expected.ID <= 0 {\n\t\tif actual.ID <= 0 {\n\t\t\treturn errors.New(\"actual group ID must be > 0\")\n\t\t}\n\t} else {\n\t\tif actual.ID != expected.ID {\n\t\t\treturn errors.New(\"group ID mismatch\")\n\t\t}\n\t}\n\tif dataprovider.ConvertName(expected.Name) != actual.Name {\n\t\treturn errors.New(\"name mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\tif actual.CreatedAt == 0 {\n\t\treturn errors.New(\"created_at unset\")\n\t}\n\tif actual.UpdatedAt == 0 {\n\t\treturn errors.New(\"updated_at unset\")\n\t}\n\tif err := compareEqualGroupSettingsFields(expected.UserSettings.BaseGroupUserSettings,\n\t\tactual.UserSettings.BaseGroupUserSettings); err != nil {\n\t\treturn err\n\t}\n\tif err := compareVirtualFolders(expected.VirtualFolders, actual.VirtualFolders); err != nil {\n\t\treturn err\n\t}\n\tif err := compareUserFilters(expected.UserSettings.Filters, actual.UserSettings.Filters); err != nil {\n\t\treturn err\n\t}\n\treturn compareFsConfig(&expected.UserSettings.FsConfig, &actual.UserSettings.FsConfig)\n}\n\nfunc checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder) error {\n\tif expected.ID <= 0 {\n\t\tif actual.ID <= 0 {\n\t\t\treturn errors.New(\"actual folder ID must be > 0\")\n\t\t}\n\t} else {\n\t\tif actual.ID != expected.ID {\n\t\t\treturn errors.New(\"folder ID mismatch\")\n\t\t}\n\t}\n\tif dataprovider.ConvertName(expected.Name) != actual.Name {\n\t\treturn errors.New(\"name mismatch\")\n\t}\n\tif expected.MappedPath != actual.MappedPath {\n\t\treturn errors.New(\"mapped path mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\treturn compareFsConfig(&expected.FsConfig, &actual.FsConfig)\n}\n\nfunc checkAPIKey(expected, actual *dataprovider.APIKey) error {\n\tif actual.Key != \"\" {\n\t\treturn errors.New(\"key must not be visible\")\n\t}\n\tif actual.KeyID == \"\" {\n\t\treturn errors.New(\"actual key_id cannot be empty\")\n\t}\n\tif expected.Name != actual.Name {\n\t\treturn errors.New(\"name mismatch\")\n\t}\n\tif expected.Scope != actual.Scope {\n\t\treturn errors.New(\"scope mismatch\")\n\t}\n\tif actual.CreatedAt == 0 {\n\t\treturn errors.New(\"created_at cannot be 0\")\n\t}\n\tif actual.UpdatedAt == 0 {\n\t\treturn errors.New(\"updated_at cannot be 0\")\n\t}\n\tif expected.ExpiresAt != actual.ExpiresAt {\n\t\treturn errors.New(\"expires_at mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\tif expected.User != actual.User {\n\t\treturn errors.New(\"user mismatch\")\n\t}\n\tif expected.Admin != actual.Admin {\n\t\treturn errors.New(\"admin mismatch\")\n\t}\n\n\treturn nil\n}\n\nfunc checkAdmin(expected, actual *dataprovider.Admin) error {\n\tif actual.Password != \"\" {\n\t\treturn errors.New(\"admin password must not be visible\")\n\t}\n\tif expected.ID <= 0 {\n\t\tif actual.ID <= 0 {\n\t\t\treturn errors.New(\"actual admin ID must be > 0\")\n\t\t}\n\t} else {\n\t\tif actual.ID != expected.ID {\n\t\t\treturn errors.New(\"admin ID mismatch\")\n\t\t}\n\t}\n\tif expected.CreatedAt > 0 {\n\t\tif expected.CreatedAt != actual.CreatedAt {\n\t\t\treturn fmt.Errorf(\"created_at mismatch %v != %v\", expected.CreatedAt, actual.CreatedAt)\n\t\t}\n\t}\n\tif err := compareAdminEqualFields(expected, actual); err != nil {\n\t\treturn err\n\t}\n\tif len(expected.Permissions) != len(actual.Permissions) {\n\t\treturn errors.New(\"permissions mismatch\")\n\t}\n\tfor _, p := range expected.Permissions {\n\t\tif !slices.Contains(actual.Permissions, p) {\n\t\t\treturn errors.New(\"permissions content mismatch\")\n\t\t}\n\t}\n\tif err := compareAdminFilters(expected.Filters, actual.Filters); err != nil {\n\t\treturn err\n\t}\n\treturn compareAdminGroups(expected, actual)\n}\n\nfunc compareAdminFilters(expected, actual dataprovider.AdminFilters) error {\n\tif expected.AllowAPIKeyAuth != actual.AllowAPIKeyAuth {\n\t\treturn errors.New(\"allow_api_key_auth mismatch\")\n\t}\n\tif len(expected.AllowList) != len(actual.AllowList) {\n\t\treturn errors.New(\"allow list mismatch\")\n\t}\n\tfor _, v := range expected.AllowList {\n\t\tif !slices.Contains(actual.AllowList, v) {\n\t\t\treturn errors.New(\"allow list content mismatch\")\n\t\t}\n\t}\n\tif expected.Preferences.HideUserPageSections != actual.Preferences.HideUserPageSections {\n\t\treturn errors.New(\"hide user page sections mismatch\")\n\t}\n\tif expected.Preferences.DefaultUsersExpiration != actual.Preferences.DefaultUsersExpiration {\n\t\treturn errors.New(\"default users expiration mismatch\")\n\t}\n\tif expected.RequirePasswordChange != actual.RequirePasswordChange {\n\t\treturn errors.New(\"require password change mismatch\")\n\t}\n\tif expected.RequireTwoFactor != actual.RequireTwoFactor {\n\t\treturn errors.New(\"require two factor mismatch\")\n\t}\n\treturn nil\n}\n\nfunc compareAdminEqualFields(expected *dataprovider.Admin, actual *dataprovider.Admin) error {\n\tif dataprovider.ConvertName(expected.Username) != actual.Username {\n\t\treturn errors.New(\"sername mismatch\")\n\t}\n\tif expected.Email != actual.Email {\n\t\treturn errors.New(\"email mismatch\")\n\t}\n\tif expected.Status != actual.Status {\n\t\treturn errors.New(\"status mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\tif expected.AdditionalInfo != actual.AdditionalInfo {\n\t\treturn errors.New(\"additional info mismatch\")\n\t}\n\tif expected.Role != actual.Role {\n\t\treturn errors.New(\"role mismatch\")\n\t}\n\treturn nil\n}\n\nfunc checkUser(expected *dataprovider.User, actual *dataprovider.User) error {\n\tif actual.Password != \"\" {\n\t\treturn errors.New(\"user password must not be visible\")\n\t}\n\tif expected.ID <= 0 {\n\t\tif actual.ID <= 0 {\n\t\t\treturn errors.New(\"actual user ID must be > 0\")\n\t\t}\n\t} else {\n\t\tif actual.ID != expected.ID {\n\t\t\treturn errors.New(\"user ID mismatch\")\n\t\t}\n\t}\n\tif expected.CreatedAt > 0 {\n\t\tif expected.CreatedAt != actual.CreatedAt {\n\t\t\treturn fmt.Errorf(\"created_at mismatch %v != %v\", expected.CreatedAt, actual.CreatedAt)\n\t\t}\n\t}\n\n\tif expected.Email != actual.Email {\n\t\treturn errors.New(\"email mismatch\")\n\t}\n\tif !slices.Equal(expected.Filters.AdditionalEmails, actual.Filters.AdditionalEmails) {\n\t\treturn errors.New(\"additional emails mismatch\")\n\t}\n\tif expected.Filters.RequirePasswordChange != actual.Filters.RequirePasswordChange {\n\t\treturn errors.New(\"require_password_change mismatch\")\n\t}\n\tif err := compareUserPermissions(expected.Permissions, actual.Permissions); err != nil {\n\t\treturn err\n\t}\n\tif err := compareUserFilters(expected.Filters.BaseUserFilters, actual.Filters.BaseUserFilters); err != nil {\n\t\treturn err\n\t}\n\tif err := compareFsConfig(&expected.FsConfig, &actual.FsConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := compareUserGroups(expected, actual); err != nil {\n\t\treturn err\n\t}\n\tif err := compareVirtualFolders(expected.VirtualFolders, actual.VirtualFolders); err != nil {\n\t\treturn err\n\t}\n\treturn compareEqualsUserFields(expected, actual)\n}\n\nfunc compareUserPermissions(expected map[string][]string, actual map[string][]string) error {\n\tif len(expected) != len(actual) {\n\t\treturn errors.New(\"permissions mismatch\")\n\t}\n\tfor dir, perms := range expected {\n\t\tif actualPerms, ok := actual[dir]; ok {\n\t\t\tfor _, v := range actualPerms {\n\t\t\t\tif !slices.Contains(perms, v) {\n\t\t\t\t\treturn errors.New(\"permissions contents mismatch\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\treturn errors.New(\"permissions directories mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareAdminGroups(expected *dataprovider.Admin, actual *dataprovider.Admin) error {\n\tif len(actual.Groups) != len(expected.Groups) {\n\t\treturn errors.New(\"groups len mismatch\")\n\t}\n\tfor _, g := range actual.Groups {\n\t\tfound := false\n\t\tfor _, g1 := range expected.Groups {\n\t\t\tif g1.Name == g.Name {\n\t\t\t\tfound = true\n\t\t\t\tif g1.Options.AddToUsersAs != g.Options.AddToUsersAs {\n\t\t\t\t\treturn fmt.Errorf(\"add to users as field mismatch for group %s\", g.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"groups mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareUserGroups(expected *dataprovider.User, actual *dataprovider.User) error {\n\tif len(actual.Groups) != len(expected.Groups) {\n\t\treturn errors.New(\"groups len mismatch\")\n\t}\n\tfor _, g := range actual.Groups {\n\t\tfound := false\n\t\tfor _, g1 := range expected.Groups {\n\t\t\tif g1.Name == g.Name {\n\t\t\t\tfound = true\n\t\t\t\tif g1.Type != g.Type {\n\t\t\t\t\treturn fmt.Errorf(\"type mismatch for group %s\", g.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"groups mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareVirtualFolders(expected []vfs.VirtualFolder, actual []vfs.VirtualFolder) error {\n\tif len(actual) != len(expected) {\n\t\treturn errors.New(\"virtual folders len mismatch\")\n\t}\n\tfor _, v := range actual {\n\t\tfound := false\n\t\tfor _, v1 := range expected {\n\t\t\tif path.Clean(v.VirtualPath) == path.Clean(v1.VirtualPath) {\n\t\t\t\tif dataprovider.ConvertName(v1.Name) != v.Name {\n\t\t\t\t\treturn errors.New(\"virtual folder name mismatch\")\n\t\t\t\t}\n\t\t\t\tif v.QuotaSize != v1.QuotaSize {\n\t\t\t\t\treturn errors.New(\"vfolder quota size mismatch\")\n\t\t\t\t}\n\t\t\t\tif (v.QuotaFiles) != (v1.QuotaFiles) {\n\t\t\t\t\treturn errors.New(\"vfolder quota files mismatch\")\n\t\t\t\t}\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"virtual folders mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {\n\tif expected.Provider != actual.Provider {\n\t\treturn errors.New(\"fs provider mismatch\")\n\t}\n\tif expected.OSConfig.ReadBufferSize != actual.OSConfig.ReadBufferSize {\n\t\treturn fmt.Errorf(\"read buffer size mismatch\")\n\t}\n\tif expected.OSConfig.WriteBufferSize != actual.OSConfig.WriteBufferSize {\n\t\treturn fmt.Errorf(\"write buffer size mismatch\")\n\t}\n\tif err := compareS3Config(expected, actual); err != nil {\n\t\treturn err\n\t}\n\tif err := compareGCSConfig(expected, actual); err != nil {\n\t\treturn err\n\t}\n\tif err := compareAzBlobConfig(expected, actual); err != nil {\n\t\treturn err\n\t}\n\tif err := checkEncryptedSecret(expected.CryptConfig.Passphrase, actual.CryptConfig.Passphrase); err != nil {\n\t\treturn err\n\t}\n\tif expected.CryptConfig.ReadBufferSize != actual.CryptConfig.ReadBufferSize {\n\t\treturn fmt.Errorf(\"crypt read buffer size mismatch\")\n\t}\n\tif expected.CryptConfig.WriteBufferSize != actual.CryptConfig.WriteBufferSize {\n\t\treturn fmt.Errorf(\"crypt write buffer size mismatch\")\n\t}\n\tif err := compareSFTPFsConfig(expected, actual); err != nil {\n\t\treturn err\n\t}\n\treturn compareHTTPFsConfig(expected, actual)\n}\n\nfunc compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error { //nolint:gocyclo\n\tif expected.S3Config.Bucket != actual.S3Config.Bucket {\n\t\treturn errors.New(\"fs S3 bucket mismatch\")\n\t}\n\tif expected.S3Config.Region != actual.S3Config.Region {\n\t\treturn errors.New(\"fs S3 region mismatch\")\n\t}\n\tif expected.S3Config.AccessKey != actual.S3Config.AccessKey {\n\t\treturn errors.New(\"fs S3 access key mismatch\")\n\t}\n\tif expected.S3Config.RoleARN != actual.S3Config.RoleARN {\n\t\treturn errors.New(\"fs S3 role ARN mismatch\")\n\t}\n\tif err := checkEncryptedSecret(expected.S3Config.AccessSecret, actual.S3Config.AccessSecret); err != nil {\n\t\treturn fmt.Errorf(\"fs S3 access secret mismatch: %v\", err)\n\t}\n\tif err := checkEncryptedSecret(expected.S3Config.SSECustomerKey, actual.S3Config.SSECustomerKey); err != nil {\n\t\treturn fmt.Errorf(\"fs S3 SSE customer key mismatch: %v\", err)\n\t}\n\tif expected.S3Config.Endpoint != actual.S3Config.Endpoint {\n\t\treturn errors.New(\"fs S3 endpoint mismatch\")\n\t}\n\tif expected.S3Config.StorageClass != actual.S3Config.StorageClass {\n\t\treturn errors.New(\"fs S3 storage class mismatch\")\n\t}\n\tif expected.S3Config.ACL != actual.S3Config.ACL {\n\t\treturn errors.New(\"fs S3 ACL mismatch\")\n\t}\n\tif expected.S3Config.UploadPartSize != actual.S3Config.UploadPartSize {\n\t\treturn errors.New(\"fs S3 upload part size mismatch\")\n\t}\n\tif expected.S3Config.UploadConcurrency != actual.S3Config.UploadConcurrency {\n\t\treturn errors.New(\"fs S3 upload concurrency mismatch\")\n\t}\n\tif expected.S3Config.DownloadPartSize != actual.S3Config.DownloadPartSize {\n\t\treturn errors.New(\"fs S3 download part size mismatch\")\n\t}\n\tif expected.S3Config.DownloadConcurrency != actual.S3Config.DownloadConcurrency {\n\t\treturn errors.New(\"fs S3 download concurrency mismatch\")\n\t}\n\tif expected.S3Config.ForcePathStyle != actual.S3Config.ForcePathStyle {\n\t\treturn errors.New(\"fs S3 force path style mismatch\")\n\t}\n\tif expected.S3Config.SkipTLSVerify != actual.S3Config.SkipTLSVerify {\n\t\treturn errors.New(\"fs S3 skip TLS verify mismatch\")\n\t}\n\tif expected.S3Config.DownloadPartMaxTime != actual.S3Config.DownloadPartMaxTime {\n\t\treturn errors.New(\"fs S3 download part max time mismatch\")\n\t}\n\tif expected.S3Config.UploadPartMaxTime != actual.S3Config.UploadPartMaxTime {\n\t\treturn errors.New(\"fs S3 upload part max time mismatch\")\n\t}\n\tif expected.S3Config.KeyPrefix != actual.S3Config.KeyPrefix &&\n\t\texpected.S3Config.KeyPrefix+\"/\" != actual.S3Config.KeyPrefix {\n\t\treturn errors.New(\"fs S3 key prefix mismatch\")\n\t}\n\treturn nil\n}\n\nfunc compareGCSConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {\n\tif expected.GCSConfig.Bucket != actual.GCSConfig.Bucket {\n\t\treturn errors.New(\"GCS bucket mismatch\")\n\t}\n\tif expected.GCSConfig.StorageClass != actual.GCSConfig.StorageClass {\n\t\treturn errors.New(\"GCS storage class mismatch\")\n\t}\n\tif expected.GCSConfig.ACL != actual.GCSConfig.ACL {\n\t\treturn errors.New(\"GCS ACL mismatch\")\n\t}\n\tif expected.GCSConfig.KeyPrefix != actual.GCSConfig.KeyPrefix &&\n\t\texpected.GCSConfig.KeyPrefix+\"/\" != actual.GCSConfig.KeyPrefix {\n\t\treturn errors.New(\"GCS key prefix mismatch\")\n\t}\n\tif expected.GCSConfig.AutomaticCredentials != actual.GCSConfig.AutomaticCredentials {\n\t\treturn errors.New(\"GCS automatic credentials mismatch\")\n\t}\n\tif expected.GCSConfig.UploadPartSize != actual.GCSConfig.UploadPartSize {\n\t\treturn errors.New(\"GCS upload part size mismatch\")\n\t}\n\tif expected.GCSConfig.UploadPartMaxTime != actual.GCSConfig.UploadPartMaxTime {\n\t\treturn errors.New(\"GCS upload part max time mismatch\")\n\t}\n\treturn nil\n}\n\nfunc compareHTTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {\n\tif expected.HTTPConfig.Endpoint != actual.HTTPConfig.Endpoint {\n\t\treturn errors.New(\"HTTPFs endpoint mismatch\")\n\t}\n\tif expected.HTTPConfig.Username != actual.HTTPConfig.Username {\n\t\treturn errors.New(\"HTTPFs username mismatch\")\n\t}\n\tif expected.HTTPConfig.SkipTLSVerify != actual.HTTPConfig.SkipTLSVerify {\n\t\treturn errors.New(\"HTTPFs skip_tls_verify mismatch\")\n\t}\n\tif expected.SFTPConfig.EqualityCheckMode != actual.SFTPConfig.EqualityCheckMode {\n\t\treturn errors.New(\"HTTPFs equality_check_mode mismatch\")\n\t}\n\tif err := checkEncryptedSecret(expected.HTTPConfig.Password, actual.HTTPConfig.Password); err != nil {\n\t\treturn fmt.Errorf(\"HTTPFs password mismatch: %v\", err)\n\t}\n\tif err := checkEncryptedSecret(expected.HTTPConfig.APIKey, actual.HTTPConfig.APIKey); err != nil {\n\t\treturn fmt.Errorf(\"HTTPFs API key mismatch: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc compareSFTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {\n\tif expected.SFTPConfig.Endpoint != actual.SFTPConfig.Endpoint {\n\t\treturn errors.New(\"SFTPFs endpoint mismatch\")\n\t}\n\tif expected.SFTPConfig.Username != actual.SFTPConfig.Username {\n\t\treturn errors.New(\"SFTPFs username mismatch\")\n\t}\n\tif expected.SFTPConfig.DisableCouncurrentReads != actual.SFTPConfig.DisableCouncurrentReads {\n\t\treturn errors.New(\"SFTPFs disable_concurrent_reads mismatch\")\n\t}\n\tif expected.SFTPConfig.BufferSize != actual.SFTPConfig.BufferSize {\n\t\treturn errors.New(\"SFTPFs buffer_size mismatch\")\n\t}\n\tif expected.SFTPConfig.EqualityCheckMode != actual.SFTPConfig.EqualityCheckMode {\n\t\treturn errors.New(\"SFTPFs equality_check_mode mismatch\")\n\t}\n\tif err := checkEncryptedSecret(expected.SFTPConfig.Password, actual.SFTPConfig.Password); err != nil {\n\t\treturn fmt.Errorf(\"SFTPFs password mismatch: %v\", err)\n\t}\n\tif err := checkEncryptedSecret(expected.SFTPConfig.PrivateKey, actual.SFTPConfig.PrivateKey); err != nil {\n\t\treturn fmt.Errorf(\"SFTPFs private key mismatch: %v\", err)\n\t}\n\tif err := checkEncryptedSecret(expected.SFTPConfig.KeyPassphrase, actual.SFTPConfig.KeyPassphrase); err != nil {\n\t\treturn fmt.Errorf(\"SFTPFs private key passphrase mismatch: %v\", err)\n\t}\n\tif expected.SFTPConfig.Prefix != actual.SFTPConfig.Prefix {\n\t\tif expected.SFTPConfig.Prefix != \"\" && actual.SFTPConfig.Prefix != \"/\" {\n\t\t\treturn errors.New(\"SFTPFs prefix mismatch\")\n\t\t}\n\t}\n\tif len(expected.SFTPConfig.Fingerprints) != len(actual.SFTPConfig.Fingerprints) {\n\t\treturn errors.New(\"SFTPFs fingerprints mismatch\")\n\t}\n\tfor _, value := range actual.SFTPConfig.Fingerprints {\n\t\tif !slices.Contains(expected.SFTPConfig.Fingerprints, value) {\n\t\t\treturn errors.New(\"SFTPFs fingerprints mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareAzBlobConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {\n\tif expected.AzBlobConfig.Container != actual.AzBlobConfig.Container {\n\t\treturn errors.New(\"azure Blob container mismatch\")\n\t}\n\tif expected.AzBlobConfig.AccountName != actual.AzBlobConfig.AccountName {\n\t\treturn errors.New(\"azure Blob account name mismatch\")\n\t}\n\tif err := checkEncryptedSecret(expected.AzBlobConfig.AccountKey, actual.AzBlobConfig.AccountKey); err != nil {\n\t\treturn fmt.Errorf(\"azure Blob account key mismatch: %v\", err)\n\t}\n\tif expected.AzBlobConfig.Endpoint != actual.AzBlobConfig.Endpoint {\n\t\treturn errors.New(\"azure Blob endpoint mismatch\")\n\t}\n\tif err := checkEncryptedSecret(expected.AzBlobConfig.SASURL, actual.AzBlobConfig.SASURL); err != nil {\n\t\treturn fmt.Errorf(\"azure Blob SAS URL mismatch: %v\", err)\n\t}\n\tif expected.AzBlobConfig.UploadPartSize != actual.AzBlobConfig.UploadPartSize {\n\t\treturn errors.New(\"azure Blob upload part size mismatch\")\n\t}\n\tif expected.AzBlobConfig.UploadConcurrency != actual.AzBlobConfig.UploadConcurrency {\n\t\treturn errors.New(\"azure Blob upload concurrency mismatch\")\n\t}\n\tif expected.AzBlobConfig.DownloadPartSize != actual.AzBlobConfig.DownloadPartSize {\n\t\treturn errors.New(\"azure Blob download part size mismatch\")\n\t}\n\tif expected.AzBlobConfig.DownloadConcurrency != actual.AzBlobConfig.DownloadConcurrency {\n\t\treturn errors.New(\"azure Blob download concurrency mismatch\")\n\t}\n\tif expected.AzBlobConfig.KeyPrefix != actual.AzBlobConfig.KeyPrefix &&\n\t\texpected.AzBlobConfig.KeyPrefix+\"/\" != actual.AzBlobConfig.KeyPrefix {\n\t\treturn errors.New(\"azure Blob key prefix mismatch\")\n\t}\n\tif expected.AzBlobConfig.UseEmulator != actual.AzBlobConfig.UseEmulator {\n\t\treturn errors.New(\"azure Blob use emulator mismatch\")\n\t}\n\tif expected.AzBlobConfig.AccessTier != actual.AzBlobConfig.AccessTier {\n\t\treturn errors.New(\"azure Blob access tier mismatch\")\n\t}\n\treturn nil\n}\n\nfunc areSecretEquals(expected, actual *kms.Secret) bool {\n\tif expected == nil && actual == nil {\n\t\treturn true\n\t}\n\tif expected != nil && expected.IsEmpty() && actual == nil {\n\t\treturn true\n\t}\n\tif actual != nil && actual.IsEmpty() && expected == nil {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc checkEncryptedSecret(expected, actual *kms.Secret) error {\n\tif areSecretEquals(expected, actual) {\n\t\treturn nil\n\t}\n\tif expected == nil && actual != nil && !actual.IsEmpty() {\n\t\treturn errors.New(\"secret mismatch\")\n\t}\n\tif actual == nil && expected != nil && !expected.IsEmpty() {\n\t\treturn errors.New(\"secret mismatch\")\n\t}\n\tif expected.IsPlain() && actual.IsEncrypted() {\n\t\tif actual.GetPayload() == \"\" {\n\t\t\treturn errors.New(\"invalid secret payload\")\n\t\t}\n\t\tif actual.GetAdditionalData() != \"\" {\n\t\t\treturn errors.New(\"invalid secret additional data\")\n\t\t}\n\t\tif actual.GetKey() != \"\" {\n\t\t\treturn errors.New(\"invalid secret key\")\n\t\t}\n\t} else {\n\t\tif expected.GetStatus() != actual.GetStatus() || expected.GetPayload() != actual.GetPayload() {\n\t\t\treturn errors.New(\"secret mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {\n\tfor _, IPMask := range expected.AllowedIP {\n\t\tif !slices.Contains(actual.AllowedIP, IPMask) {\n\t\t\treturn errors.New(\"allowed IP contents mismatch\")\n\t\t}\n\t}\n\tfor _, IPMask := range expected.DeniedIP {\n\t\tif !slices.Contains(actual.DeniedIP, IPMask) {\n\t\t\treturn errors.New(\"denied IP contents mismatch\")\n\t\t}\n\t}\n\tfor _, method := range expected.DeniedLoginMethods {\n\t\tif !slices.Contains(actual.DeniedLoginMethods, method) {\n\t\t\treturn errors.New(\"denied login methods contents mismatch\")\n\t\t}\n\t}\n\tfor _, protocol := range expected.DeniedProtocols {\n\t\tif !slices.Contains(actual.DeniedProtocols, protocol) {\n\t\t\treturn errors.New(\"denied protocols contents mismatch\")\n\t\t}\n\t}\n\tfor _, options := range expected.WebClient {\n\t\tif !slices.Contains(actual.WebClient, options) {\n\t\t\treturn errors.New(\"web client options contents mismatch\")\n\t\t}\n\t}\n\n\tif len(expected.TLSCerts) != len(actual.TLSCerts) {\n\t\treturn errors.New(\"TLS certs mismatch\")\n\t}\n\tfor _, cert := range expected.TLSCerts {\n\t\tif !slices.Contains(actual.TLSCerts, cert) {\n\t\t\treturn errors.New(\"TLS certs content mismatch\")\n\t\t}\n\t}\n\n\treturn compareUserFiltersEqualFields(expected, actual)\n}\n\nfunc compareUserFiltersEqualFields(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {\n\tif expected.Hooks.ExternalAuthDisabled != actual.Hooks.ExternalAuthDisabled {\n\t\treturn errors.New(\"external_auth_disabled hook mismatch\")\n\t}\n\tif expected.Hooks.PreLoginDisabled != actual.Hooks.PreLoginDisabled {\n\t\treturn errors.New(\"pre_login_disabled hook mismatch\")\n\t}\n\tif expected.Hooks.CheckPasswordDisabled != actual.Hooks.CheckPasswordDisabled {\n\t\treturn errors.New(\"check_password_disabled hook mismatch\")\n\t}\n\tif expected.DisableFsChecks != actual.DisableFsChecks {\n\t\treturn errors.New(\"disable_fs_checks mismatch\")\n\t}\n\tif expected.StartDirectory != actual.StartDirectory {\n\t\treturn errors.New(\"start_directory mismatch\")\n\t}\n\treturn nil\n}\n\nfunc compareBaseUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error { //nolint:gocyclo\n\tif len(expected.AllowedIP) != len(actual.AllowedIP) {\n\t\treturn errors.New(\"allowed IP mismatch\")\n\t}\n\tif len(expected.DeniedIP) != len(actual.DeniedIP) {\n\t\treturn errors.New(\"denied IP mismatch\")\n\t}\n\tif len(expected.DeniedLoginMethods) != len(actual.DeniedLoginMethods) {\n\t\treturn errors.New(\"denied login methods mismatch\")\n\t}\n\tif len(expected.DeniedProtocols) != len(actual.DeniedProtocols) {\n\t\treturn errors.New(\"denied protocols mismatch\")\n\t}\n\tif expected.MaxUploadFileSize != actual.MaxUploadFileSize {\n\t\treturn errors.New(\"max upload file size mismatch\")\n\t}\n\tif expected.TLSUsername != actual.TLSUsername {\n\t\treturn errors.New(\"TLSUsername mismatch\")\n\t}\n\tif len(expected.WebClient) != len(actual.WebClient) {\n\t\treturn errors.New(\"WebClient filter mismatch\")\n\t}\n\tif expected.AllowAPIKeyAuth != actual.AllowAPIKeyAuth {\n\t\treturn errors.New(\"allow_api_key_auth mismatch\")\n\t}\n\tif expected.ExternalAuthCacheTime != actual.ExternalAuthCacheTime {\n\t\treturn errors.New(\"external_auth_cache_time mismatch\")\n\t}\n\tif expected.FTPSecurity != actual.FTPSecurity {\n\t\treturn errors.New(\"ftp_security mismatch\")\n\t}\n\tif expected.IsAnonymous != actual.IsAnonymous {\n\t\treturn errors.New(\"is_anonymous mismatch\")\n\t}\n\tif expected.DefaultSharesExpiration != actual.DefaultSharesExpiration {\n\t\treturn errors.New(\"default_shares_expiration mismatch\")\n\t}\n\tif expected.MaxSharesExpiration != actual.MaxSharesExpiration {\n\t\treturn errors.New(\"max_shares_expiration mismatch\")\n\t}\n\tif expected.PasswordExpiration != actual.PasswordExpiration {\n\t\treturn errors.New(\"password_expiration mismatch\")\n\t}\n\tif expected.PasswordStrength != actual.PasswordStrength {\n\t\treturn errors.New(\"password_strength mismatch\")\n\t}\n\treturn nil\n}\n\nfunc compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {\n\tif err := compareBaseUserFilters(expected, actual); err != nil {\n\t\treturn err\n\t}\n\tif err := compareUserFilterSubStructs(expected, actual); err != nil {\n\t\treturn err\n\t}\n\tif err := compareUserBandwidthLimitFilters(expected, actual); err != nil {\n\t\treturn err\n\t}\n\tif err := compareAccessTimeFilters(expected, actual); err != nil {\n\t\treturn err\n\t}\n\treturn compareUserFilePatternsFilters(expected, actual)\n}\n\nfunc checkFilterMatch(expected []string, actual []string) bool {\n\tif len(expected) != len(actual) {\n\t\treturn false\n\t}\n\tfor _, e := range expected {\n\t\tif !slices.Contains(actual, strings.ToLower(e)) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc compareAccessTimeFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {\n\tif len(expected.AccessTime) != len(actual.AccessTime) {\n\t\treturn errors.New(\"access time filters mismatch\")\n\t}\n\n\tfor idx, p := range expected.AccessTime {\n\t\tif actual.AccessTime[idx].DayOfWeek != p.DayOfWeek {\n\t\t\treturn errors.New(\"access time day of week mismatch\")\n\t\t}\n\t\tif actual.AccessTime[idx].From != p.From {\n\t\t\treturn errors.New(\"access time from mismatch\")\n\t\t}\n\t\tif actual.AccessTime[idx].To != p.To {\n\t\t\treturn errors.New(\"access time to mismatch\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {\n\tif len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {\n\t\treturn errors.New(\"bandwidth limits filters mismatch\")\n\t}\n\n\tfor idx, l := range expected.BandwidthLimits {\n\t\tif actual.BandwidthLimits[idx].UploadBandwidth != l.UploadBandwidth {\n\t\t\treturn errors.New(\"bandwidth filters upload_bandwidth mismatch\")\n\t\t}\n\t\tif actual.BandwidthLimits[idx].DownloadBandwidth != l.DownloadBandwidth {\n\t\t\treturn errors.New(\"bandwidth filters download_bandwidth mismatch\")\n\t\t}\n\t\tif len(actual.BandwidthLimits[idx].Sources) != len(l.Sources) {\n\t\t\treturn errors.New(\"bandwidth filters sources mismatch\")\n\t\t}\n\t\tfor _, source := range actual.BandwidthLimits[idx].Sources {\n\t\t\tif !slices.Contains(l.Sources, source) {\n\t\t\t\treturn errors.New(\"bandwidth filters source mismatch\")\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc compareUserFilePatternsFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {\n\tif len(expected.FilePatterns) != len(actual.FilePatterns) {\n\t\treturn errors.New(\"file patterns mismatch\")\n\t}\n\tfor _, f := range expected.FilePatterns {\n\t\tfound := false\n\t\tfor _, f1 := range actual.FilePatterns {\n\t\t\tif path.Clean(f.Path) == path.Clean(f1.Path) && f.DenyPolicy == f1.DenyPolicy {\n\t\t\t\tif !checkFilterMatch(f.AllowedPatterns, f1.AllowedPatterns) ||\n\t\t\t\t\t!checkFilterMatch(f.DeniedPatterns, f1.DeniedPatterns) {\n\t\t\t\t\treturn errors.New(\"file patterns contents mismatch\")\n\t\t\t\t}\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"file patterns contents mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareRenameConfigs(expected, actual []dataprovider.RenameConfig) error {\n\tif len(expected) != len(actual) {\n\t\treturn errors.New(\"rename configs mismatch\")\n\t}\n\tfor _, ex := range expected {\n\t\tfound := false\n\t\tfor _, ac := range actual {\n\t\t\tif ac.Key == ex.Key && ac.Value == ex.Value && ac.UpdateModTime == ex.UpdateModTime {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"rename configs mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareKeyValues(expected, actual []dataprovider.KeyValue) error {\n\tif len(expected) != len(actual) {\n\t\treturn errors.New(\"key values mismatch\")\n\t}\n\tfor _, ex := range expected {\n\t\tfound := false\n\t\tfor _, ac := range actual {\n\t\t\tif ac.Key == ex.Key && ac.Value == ex.Value {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"key values mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareHTTPparts(expected, actual []dataprovider.HTTPPart) error {\n\tfor _, p1 := range expected {\n\t\tfound := false\n\t\tfor _, p2 := range actual {\n\t\t\tif p1.Name == p2.Name {\n\t\t\t\tfound = true\n\t\t\t\tif err := compareKeyValues(p1.Headers, p2.Headers); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"http headers mismatch for part %q\", p1.Name)\n\t\t\t\t}\n\t\t\t\tif p1.Body != p2.Body || p1.Filepath != p2.Filepath {\n\t\t\t\t\treturn fmt.Errorf(\"http part %q mismatch\", p1.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn fmt.Errorf(\"expected http part %q not found\", p1.Name)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareEventActionHTTPConfigFields(expected, actual dataprovider.EventActionHTTPConfig) error {\n\tif expected.Endpoint != actual.Endpoint {\n\t\treturn errors.New(\"http endpoint mismatch\")\n\t}\n\tif expected.Username != actual.Username {\n\t\treturn errors.New(\"http username mismatch\")\n\t}\n\tif err := checkEncryptedSecret(expected.Password, actual.Password); err != nil {\n\t\treturn err\n\t}\n\tif err := compareKeyValues(expected.Headers, actual.Headers); err != nil {\n\t\treturn errors.New(\"http headers mismatch\")\n\t}\n\tif expected.Timeout != actual.Timeout {\n\t\treturn errors.New(\"http timeout mismatch\")\n\t}\n\tif expected.SkipTLSVerify != actual.SkipTLSVerify {\n\t\treturn errors.New(\"http skip TLS verify mismatch\")\n\t}\n\tif expected.Method != actual.Method {\n\t\treturn errors.New(\"http method mismatch\")\n\t}\n\tif err := compareKeyValues(expected.QueryParameters, actual.QueryParameters); err != nil {\n\t\treturn errors.New(\"http query parameters mismatch\")\n\t}\n\tif expected.Body != actual.Body {\n\t\treturn errors.New(\"http body mismatch\")\n\t}\n\tif len(expected.Parts) != len(actual.Parts) {\n\t\treturn errors.New(\"http parts mismatch\")\n\t}\n\treturn compareHTTPparts(expected.Parts, actual.Parts)\n}\n\nfunc compareEventActionEmailConfigFields(expected, actual dataprovider.EventActionEmailConfig) error {\n\tif len(expected.Recipients) != len(actual.Recipients) {\n\t\treturn errors.New(\"email recipients mismatch\")\n\t}\n\tfor _, v := range expected.Recipients {\n\t\tif !slices.Contains(actual.Recipients, v) {\n\t\t\treturn errors.New(\"email recipients content mismatch\")\n\t\t}\n\t}\n\tif len(expected.Bcc) != len(actual.Bcc) {\n\t\treturn errors.New(\"email bcc mismatch\")\n\t}\n\tfor _, v := range expected.Bcc {\n\t\tif !slices.Contains(actual.Bcc, v) {\n\t\t\treturn errors.New(\"email bcc content mismatch\")\n\t\t}\n\t}\n\tif expected.Subject != actual.Subject {\n\t\treturn errors.New(\"email subject mismatch\")\n\t}\n\tif expected.ContentType != actual.ContentType {\n\t\treturn errors.New(\"email content type mismatch\")\n\t}\n\tif expected.Body != actual.Body {\n\t\treturn errors.New(\"email body mismatch\")\n\t}\n\tif len(expected.Attachments) != len(actual.Attachments) {\n\t\treturn errors.New(\"email attachments mismatch\")\n\t}\n\tfor _, v := range expected.Attachments {\n\t\tif !slices.Contains(actual.Attachments, v) {\n\t\t\treturn errors.New(\"email attachments content mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareEventActionFsCompressFields(expected, actual dataprovider.EventActionFsCompress) error {\n\tif expected.Name != actual.Name {\n\t\treturn errors.New(\"fs compress name mismatch\")\n\t}\n\tif len(expected.Paths) != len(actual.Paths) {\n\t\treturn errors.New(\"fs compress paths mismatch\")\n\t}\n\tfor _, v := range expected.Paths {\n\t\tif !slices.Contains(actual.Paths, v) {\n\t\t\treturn errors.New(\"fs compress paths content mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareEventActionFsConfigFields(expected, actual dataprovider.EventActionFilesystemConfig) error {\n\tif expected.Type != actual.Type {\n\t\treturn errors.New(\"fs type mismatch\")\n\t}\n\tif err := compareRenameConfigs(expected.Renames, actual.Renames); err != nil {\n\t\treturn errors.New(\"fs renames mismatch\")\n\t}\n\tif err := compareKeyValues(expected.Copy, actual.Copy); err != nil {\n\t\treturn errors.New(\"fs copy mismatch\")\n\t}\n\tif len(expected.Deletes) != len(actual.Deletes) {\n\t\treturn errors.New(\"fs deletes mismatch\")\n\t}\n\tfor _, v := range expected.Deletes {\n\t\tif !slices.Contains(actual.Deletes, v) {\n\t\t\treturn errors.New(\"fs deletes content mismatch\")\n\t\t}\n\t}\n\tif len(expected.MkDirs) != len(actual.MkDirs) {\n\t\treturn errors.New(\"fs mkdirs mismatch\")\n\t}\n\tfor _, v := range expected.MkDirs {\n\t\tif !slices.Contains(actual.MkDirs, v) {\n\t\t\treturn errors.New(\"fs mkdir content mismatch\")\n\t\t}\n\t}\n\tif len(expected.Exist) != len(actual.Exist) {\n\t\treturn errors.New(\"fs exist mismatch\")\n\t}\n\tfor _, v := range expected.Exist {\n\t\tif !slices.Contains(actual.Exist, v) {\n\t\t\treturn errors.New(\"fs exist content mismatch\")\n\t\t}\n\t}\n\treturn compareEventActionFsCompressFields(expected.Compress, actual.Compress)\n}\n\nfunc compareEventActionIDPConfigFields(expected, actual dataprovider.EventActionIDPAccountCheck) error {\n\tif expected.Mode != actual.Mode {\n\t\treturn errors.New(\"mode mismatch\")\n\t}\n\tif expected.TemplateAdmin != actual.TemplateAdmin {\n\t\treturn errors.New(\"admin template mismatch\")\n\t}\n\tif expected.TemplateUser != actual.TemplateUser {\n\t\treturn errors.New(\"user template mismatch\")\n\t}\n\treturn nil\n}\n\nfunc compareEventActionCmdConfigFields(expected, actual dataprovider.EventActionCommandConfig) error {\n\tif expected.Cmd != actual.Cmd {\n\t\treturn errors.New(\"command mismatch\")\n\t}\n\tif expected.Timeout != actual.Timeout {\n\t\treturn errors.New(\"cmd timeout mismatch\")\n\t}\n\tif len(expected.Args) != len(actual.Args) {\n\t\treturn errors.New(\"cmd args mismatch\")\n\t}\n\tfor _, v := range expected.Args {\n\t\tif !slices.Contains(actual.Args, v) {\n\t\t\treturn errors.New(\"cmd args content mismatch\")\n\t\t}\n\t}\n\tif err := compareKeyValues(expected.EnvVars, actual.EnvVars); err != nil {\n\t\treturn errors.New(\"cmd env vars mismatch\")\n\t}\n\treturn nil\n}\n\nfunc compareEventActionDataRetentionFields(expected, actual dataprovider.EventActionDataRetentionConfig) error {\n\tif len(expected.Folders) != len(actual.Folders) {\n\t\treturn errors.New(\"retention folders mismatch\")\n\t}\n\tfor _, f1 := range expected.Folders {\n\t\tfound := false\n\t\tfor _, f2 := range actual.Folders {\n\t\t\tif f1.Path == f2.Path {\n\t\t\t\tfound = true\n\t\t\t\tif f1.Retention != f2.Retention {\n\t\t\t\t\treturn fmt.Errorf(\"retention mismatch for folder %s\", f1.Path)\n\t\t\t\t}\n\t\t\t\tif f1.DeleteEmptyDirs != f2.DeleteEmptyDirs {\n\t\t\t\t\treturn fmt.Errorf(\"delete_empty_dirs mismatch for folder %s\", f1.Path)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errors.New(\"retention folders mismatch\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc compareEqualGroupSettingsFields(expected sdk.BaseGroupUserSettings, actual sdk.BaseGroupUserSettings) error {\n\tif expected.HomeDir != actual.HomeDir {\n\t\treturn errors.New(\"home dir mismatch\")\n\t}\n\tif expected.MaxSessions != actual.MaxSessions {\n\t\treturn errors.New(\"MaxSessions mismatch\")\n\t}\n\tif expected.QuotaSize != actual.QuotaSize {\n\t\treturn errors.New(\"QuotaSize mismatch\")\n\t}\n\tif expected.QuotaFiles != actual.QuotaFiles {\n\t\treturn errors.New(\"QuotaFiles mismatch\")\n\t}\n\tif expected.UploadBandwidth != actual.UploadBandwidth {\n\t\treturn errors.New(\"UploadBandwidth mismatch\")\n\t}\n\tif expected.DownloadBandwidth != actual.DownloadBandwidth {\n\t\treturn errors.New(\"DownloadBandwidth mismatch\")\n\t}\n\tif expected.UploadDataTransfer != actual.UploadDataTransfer {\n\t\treturn errors.New(\"upload_data_transfer mismatch\")\n\t}\n\tif expected.DownloadDataTransfer != actual.DownloadDataTransfer {\n\t\treturn errors.New(\"download_data_transfer mismatch\")\n\t}\n\tif expected.TotalDataTransfer != actual.TotalDataTransfer {\n\t\treturn errors.New(\"total_data_transfer mismatch\")\n\t}\n\tif expected.ExpiresIn != actual.ExpiresIn {\n\t\treturn errors.New(\"expires_in mismatch\")\n\t}\n\treturn compareUserPermissions(expected.Permissions, actual.Permissions)\n}\n\nfunc compareEqualsUserFields(expected *dataprovider.User, actual *dataprovider.User) error {\n\tif dataprovider.ConvertName(expected.Username) != actual.Username {\n\t\treturn errors.New(\"username mismatch\")\n\t}\n\tif expected.HomeDir != actual.HomeDir {\n\t\treturn errors.New(\"home dir mismatch\")\n\t}\n\tif expected.UID != actual.UID {\n\t\treturn errors.New(\"UID mismatch\")\n\t}\n\tif expected.GID != actual.GID {\n\t\treturn errors.New(\"GID mismatch\")\n\t}\n\tif expected.MaxSessions != actual.MaxSessions {\n\t\treturn errors.New(\"MaxSessions mismatch\")\n\t}\n\tif len(expected.Permissions) != len(actual.Permissions) {\n\t\treturn errors.New(\"permissions mismatch\")\n\t}\n\tif expected.UploadBandwidth != actual.UploadBandwidth {\n\t\treturn errors.New(\"UploadBandwidth mismatch\")\n\t}\n\tif expected.DownloadBandwidth != actual.DownloadBandwidth {\n\t\treturn errors.New(\"DownloadBandwidth mismatch\")\n\t}\n\tif expected.Status != actual.Status {\n\t\treturn errors.New(\"status mismatch\")\n\t}\n\tif expected.ExpirationDate != actual.ExpirationDate {\n\t\treturn errors.New(\"ExpirationDate mismatch\")\n\t}\n\tif expected.AdditionalInfo != actual.AdditionalInfo {\n\t\treturn errors.New(\"AdditionalInfo mismatch\")\n\t}\n\tif expected.Description != actual.Description {\n\t\treturn errors.New(\"description mismatch\")\n\t}\n\tif expected.Role != actual.Role {\n\t\treturn errors.New(\"role mismatch\")\n\t}\n\treturn compareQuotaUserFields(expected, actual)\n}\n\nfunc compareQuotaUserFields(expected *dataprovider.User, actual *dataprovider.User) error {\n\tif expected.QuotaSize != actual.QuotaSize {\n\t\treturn errors.New(\"QuotaSize mismatch\")\n\t}\n\tif expected.QuotaFiles != actual.QuotaFiles {\n\t\treturn errors.New(\"QuotaFiles mismatch\")\n\t}\n\tif expected.UploadDataTransfer != actual.UploadDataTransfer {\n\t\treturn errors.New(\"upload_data_transfer mismatch\")\n\t}\n\tif expected.DownloadDataTransfer != actual.DownloadDataTransfer {\n\t\treturn errors.New(\"download_data_transfer mismatch\")\n\t}\n\tif expected.TotalDataTransfer != actual.TotalDataTransfer {\n\t\treturn errors.New(\"total_data_transfer mismatch\")\n\t}\n\treturn nil\n}\n\nfunc addLimitAndOffsetQueryParams(rawurl string, limit, offset int64) (*url.URL, error) {\n\turl, err := url.Parse(rawurl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tq := url.Query()\n\tif limit > 0 {\n\t\tq.Add(\"limit\", strconv.FormatInt(limit, 10))\n\t}\n\tif offset > 0 {\n\t\tq.Add(\"offset\", strconv.FormatInt(offset, 10))\n\t}\n\turl.RawQuery = q.Encode()\n\treturn url, err\n}\n\nfunc addModeQueryParam(rawurl, mode string) (*url.URL, error) {\n\turl, err := url.Parse(rawurl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tq := url.Query()\n\tif len(mode) > 0 {\n\t\tq.Add(\"mode\", mode)\n\t}\n\turl.RawQuery = q.Encode()\n\treturn url, err\n}\n\nfunc addUpdateUserQueryParams(rawurl, disconnect string) (*url.URL, error) {\n\turl, err := url.Parse(rawurl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tq := url.Query()\n\tif disconnect != \"\" {\n\t\tq.Add(\"disconnect\", disconnect)\n\t}\n\turl.RawQuery = q.Encode()\n\treturn url, err\n}\n"
  },
  {
    "path": "internal/httpdtest/httpfsimpl.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage httpdtest\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/go-chi/render\"\n\t\"github.com/shirou/gopsutil/v3/disk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\tstatPath     = \"/api/v1/stat\"\n\topenPath     = \"/api/v1/open\"\n\tcreatePath   = \"/api/v1/create\"\n\trenamePath   = \"/api/v1/rename\"\n\tremovePath   = \"/api/v1/remove\"\n\tmkdirPath    = \"/api/v1/mkdir\"\n\tchmodPath    = \"/api/v1/chmod\"\n\tchtimesPath  = \"/api/v1/chtimes\"\n\ttruncatePath = \"/api/v1/truncate\"\n\treaddirPath  = \"/api/v1/readdir\"\n\tdirsizePath  = \"/api/v1/dirsize\"\n\tmimetypePath = \"/api/v1/mimetype\"\n\tstatvfsPath  = \"/api/v1/statvfs\"\n)\n\n// HTTPFsCallbacks defines additional callbacks to customize the HTTPfs responses\ntype HTTPFsCallbacks struct {\n\tReaddir func(string) []os.FileInfo\n}\n\n// StartTestHTTPFs starts a test HTTP service that implements httpfs\n// and listens on the specified port\nfunc StartTestHTTPFs(port int, callbacks *HTTPFsCallbacks) error {\n\tfs := httpFsImpl{\n\t\tport:      port,\n\t\tcallbacks: callbacks,\n\t}\n\n\treturn fs.Run()\n}\n\n// StartTestHTTPFsOverUnixSocket starts a test HTTP service that implements httpfs\n// and listens on the specified UNIX domain socket path\nfunc StartTestHTTPFsOverUnixSocket(socketPath string) error {\n\tfs := httpFsImpl{\n\t\tunixSocketPath: socketPath,\n\t}\n\treturn fs.Run()\n}\n\ntype httpFsImpl struct {\n\trouter         *chi.Mux\n\tbasePath       string\n\tport           int\n\tunixSocketPath string\n\tcallbacks      *HTTPFsCallbacks\n}\n\ntype apiResponse struct {\n\tError   string `json:\"error,omitempty\"`\n\tMessage string `json:\"message,omitempty\"`\n}\n\nfunc (fs *httpFsImpl) sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {\n\tvar errorString string\n\tif err != nil {\n\t\terrorString = err.Error()\n\t}\n\tresp := apiResponse{\n\t\tError:   errorString,\n\t\tMessage: message,\n\t}\n\tctx := context.WithValue(r.Context(), render.StatusCtxKey, code)\n\trender.JSON(w, r.WithContext(ctx), resp)\n}\n\nfunc (fs *httpFsImpl) getUsername(r *http.Request) (string, error) {\n\tusername, _, ok := r.BasicAuth()\n\tif !ok || username == \"\" {\n\t\treturn \"\", os.ErrPermission\n\t}\n\trootPath := filepath.Join(fs.basePath, username)\n\t_, err := os.Stat(rootPath)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\terr = os.MkdirAll(rootPath, os.ModePerm)\n\t\tif err != nil {\n\t\t\treturn username, err\n\t\t}\n\t}\n\treturn username, nil\n}\n\nfunc (fs *httpFsImpl) getRespStatus(err error) int {\n\tif errors.Is(err, os.ErrPermission) {\n\t\treturn http.StatusForbidden\n\t}\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn http.StatusNotFound\n\t}\n\n\treturn http.StatusInternalServerError\n}\n\nfunc (fs *httpFsImpl) stat(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\tinfo, err := os.Stat(fsPath)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, getStatFromInfo(info))\n}\n\nfunc (fs *httpFsImpl) open(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tvar offset int64\n\tif r.URL.Query().Has(\"offset\") {\n\t\toffset, err = strconv.ParseInt(r.URL.Query().Get(\"offset\"), 10, 64)\n\t\tif err != nil {\n\t\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\tf, err := os.Open(fsPath)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tdefer f.Close()\n\n\tif offset > 0 {\n\t\t_, err = f.Seek(offset, io.SeekStart)\n\t\tif err != nil {\n\t\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t}\n\tctype := mime.TypeByExtension(filepath.Ext(name))\n\tif ctype != \"\" {\n\t\tctype = \"application/octet-stream\"\n\t}\n\tw.Header().Set(\"Content-Type\", ctype)\n\t_, err = io.Copy(w, f)\n\tif err != nil {\n\t\tpanic(http.ErrAbortHandler)\n\t}\n}\n\nfunc (fs *httpFsImpl) create(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tflags := os.O_RDWR | os.O_CREATE | os.O_TRUNC\n\tif r.URL.Query().Has(\"flags\") {\n\t\topenFlags, err := strconv.ParseInt(r.URL.Query().Get(\"flags\"), 10, 32)\n\t\tif err != nil {\n\t\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t\tif openFlags > 0 {\n\t\t\tflags = int(openFlags)\n\t\t}\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\tf, err := os.OpenFile(fsPath, flags, 0666)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tdefer f.Close()\n\n\t_, err = io.Copy(f, r.Body)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tfs.sendAPIResponse(w, r, nil, \"upload OK\", http.StatusOK)\n}\n\nfunc (fs *httpFsImpl) rename(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\ttarget := r.URL.Query().Get(\"target\")\n\tif target == \"\" {\n\t\tfs.sendAPIResponse(w, r, nil, \"target path cannot be empty\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tsourcePath := filepath.Join(fs.basePath, username, name)\n\ttargetPath := filepath.Join(fs.basePath, username, target)\n\terr = os.Rename(sourcePath, targetPath)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tfs.sendAPIResponse(w, r, nil, \"rename OK\", http.StatusOK)\n}\n\nfunc (fs *httpFsImpl) remove(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\terr = os.Remove(fsPath)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tfs.sendAPIResponse(w, r, nil, \"remove OK\", http.StatusOK)\n}\n\nfunc (fs *httpFsImpl) mkdir(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\terr = os.Mkdir(fsPath, os.ModePerm)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tfs.sendAPIResponse(w, r, nil, \"mkdir OK\", http.StatusOK)\n}\n\nfunc (fs *httpFsImpl) chmod(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tmode, err := strconv.ParseUint(r.URL.Query().Get(\"mode\"), 10, 32)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\terr = os.Chmod(fsPath, os.FileMode(mode))\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tfs.sendAPIResponse(w, r, nil, \"chmod OK\", http.StatusOK)\n}\n\nfunc (fs *httpFsImpl) chtimes(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tatime, err := time.Parse(time.RFC3339, r.URL.Query().Get(\"access_time\"))\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tmtime, err := time.Parse(time.RFC3339, r.URL.Query().Get(\"modification_time\"))\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\terr = os.Chtimes(fsPath, atime, mtime)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tfs.sendAPIResponse(w, r, nil, \"chtimes OK\", http.StatusOK)\n}\n\nfunc (fs *httpFsImpl) truncate(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tsize, err := strconv.ParseInt(r.URL.Query().Get(\"size\"), 10, 64)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\terr = os.Truncate(fsPath, size)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tfs.sendAPIResponse(w, r, nil, \"chmod OK\", http.StatusOK)\n}\n\nfunc (fs *httpFsImpl) readdir(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\tf, err := os.Open(fsPath)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tlist, err := f.Readdir(-1)\n\tf.Close()\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tresult := make([]map[string]any, 0, len(list))\n\tfor _, fi := range list {\n\t\tresult = append(result, getStatFromInfo(fi))\n\t}\n\tif fs.callbacks != nil && fs.callbacks.Readdir != nil {\n\t\tfor _, fi := range fs.callbacks.Readdir(name) {\n\t\t\tresult = append(result, getStatFromInfo(fi))\n\t\t}\n\t}\n\trender.JSON(w, r, result)\n}\n\nfunc (fs *httpFsImpl) dirsize(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\tinfo, err := os.Stat(fsPath)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tnumFiles := 0\n\tsize := int64(0)\n\tif info.IsDir() {\n\t\terr = filepath.Walk(fsPath, func(_ string, info os.FileInfo, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif info != nil && info.Mode().IsRegular() {\n\t\t\t\tsize += info.Size()\n\t\t\t\tnumFiles++\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t\tif err != nil {\n\t\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\t\treturn\n\t\t}\n\t}\n\trender.JSON(w, r, map[string]any{\n\t\t\"files\": numFiles,\n\t\t\"size\":  size,\n\t})\n}\n\nfunc (fs *httpFsImpl) mimetype(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\tf, err := os.OpenFile(fsPath, os.O_RDONLY, 0)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tdefer f.Close()\n\tvar buf [512]byte\n\tn, err := io.ReadFull(f, buf[:])\n\tif err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tctype := http.DetectContentType(buf[:n])\n\trender.JSON(w, r, map[string]any{\n\t\t\"mime\": ctype,\n\t})\n}\n\nfunc (fs *httpFsImpl) statvfs(w http.ResponseWriter, r *http.Request) {\n\tusername, err := fs.getUsername(r)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\tname := getNameURLParam(r)\n\tfsPath := filepath.Join(fs.basePath, username, name)\n\tusage, err := disk.Usage(fsPath)\n\tif err != nil {\n\t\tfs.sendAPIResponse(w, r, err, \"\", fs.getRespStatus(err))\n\t\treturn\n\t}\n\t// we assume block size = 4096\n\tbsize := uint64(4096)\n\tblocks := usage.Total / bsize\n\tbfree := usage.Free / bsize\n\tfiles := usage.InodesTotal\n\tffree := usage.InodesFree\n\tif files == 0 {\n\t\t// these assumptions are wrong but still better than returning 0\n\t\tfiles = blocks / 4\n\t\tffree = bfree / 4\n\t}\n\trender.JSON(w, r, map[string]any{\n\t\t\"bsize\":   bsize,\n\t\t\"frsize\":  bsize,\n\t\t\"blocks\":  blocks,\n\t\t\"bfree\":   bfree,\n\t\t\"bavail\":  bfree,\n\t\t\"files\":   files,\n\t\t\"ffree\":   ffree,\n\t\t\"favail\":  ffree,\n\t\t\"namemax\": 255,\n\t})\n}\n\nfunc (fs *httpFsImpl) configureRouter() {\n\tfs.router = chi.NewRouter()\n\tfs.router.Use(middleware.Recoverer)\n\n\tfs.router.Get(statPath+\"/{name}\", fs.stat) //nolint:goconst\n\tfs.router.Get(openPath+\"/{name}\", fs.open)\n\tfs.router.Post(createPath+\"/{name}\", fs.create)\n\tfs.router.Patch(renamePath+\"/{name}\", fs.rename)\n\tfs.router.Delete(removePath+\"/{name}\", fs.remove)\n\tfs.router.Post(mkdirPath+\"/{name}\", fs.mkdir)\n\tfs.router.Patch(chmodPath+\"/{name}\", fs.chmod)\n\tfs.router.Patch(chtimesPath+\"/{name}\", fs.chtimes)\n\tfs.router.Patch(truncatePath+\"/{name}\", fs.truncate)\n\tfs.router.Get(readdirPath+\"/{name}\", fs.readdir)\n\tfs.router.Get(dirsizePath+\"/{name}\", fs.dirsize)\n\tfs.router.Get(mimetypePath+\"/{name}\", fs.mimetype)\n\tfs.router.Get(statvfsPath+\"/{name}\", fs.statvfs)\n}\n\nfunc (fs *httpFsImpl) Run() error {\n\tfs.basePath = filepath.Join(os.TempDir(), \"httpfs\")\n\tif err := os.RemoveAll(fs.basePath); err != nil {\n\t\treturn err\n\t}\n\tif err := os.MkdirAll(fs.basePath, os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\tfs.configureRouter()\n\n\thttpServer := http.Server{\n\t\tAddr:           fmt.Sprintf(\":%d\", fs.port),\n\t\tHandler:        fs.router,\n\t\tReadTimeout:    60 * time.Second,\n\t\tWriteTimeout:   60 * time.Second,\n\t\tIdleTimeout:    120 * time.Second,\n\t\tMaxHeaderBytes: 1 << 16, // 64KB\n\t}\n\n\tif fs.unixSocketPath == \"\" {\n\t\treturn httpServer.ListenAndServe()\n\t}\n\terr := os.Remove(fs.unixSocketPath)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn err\n\t}\n\tlistener, err := net.Listen(\"unix\", fs.unixSocketPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn httpServer.Serve(listener)\n}\n\nfunc getStatFromInfo(info os.FileInfo) map[string]any {\n\treturn map[string]any{\n\t\t\"name\":          info.Name(),\n\t\t\"size\":          info.Size(),\n\t\t\"mode\":          info.Mode(),\n\t\t\"last_modified\": info.ModTime(),\n\t}\n}\n\nfunc getNameURLParam(r *http.Request) string {\n\tv := chi.URLParam(r, \"name\")\n\tunescaped, err := url.PathUnescape(v)\n\tif err != nil {\n\t\treturn util.CleanPath(v)\n\t}\n\treturn util.CleanPath(unescaped)\n}\n"
  },
  {
    "path": "internal/jwt/jwt.go",
    "content": "// Copyright (C) 2025 Nicola Murino\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, version 3.\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\n// Package jwt provides functionality for creating, parsing, and validating\n// JSON Web Tokens (JWT) used in authentication and authorization workflows.\npackage jwt\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v4\"\n\t\"github.com/go-jose/go-jose/v4/jwt\"\n\t\"github.com/rs/xid\"\n)\n\nconst (\n\tCookieKey = \"jwt\"\n)\n\nvar (\n\tTokenCtxKey = &contextKey{\"Token\"}\n\tErrorCtxKey = &contextKey{\"Error\"}\n)\n\n// contextKey is a value for use with context.WithValue. It's used as\n// a pointer so it fits in an interface{} without allocation. This technique\n// for defining context keys was copied from Go 1.7's new use of context in net/http.\ntype contextKey struct {\n\tname string\n}\n\nfunc (k *contextKey) String() string {\n\treturn \"jwt context value \" + k.name\n}\n\nfunc NewClaims(audience, ip string, duration time.Duration) *Claims {\n\tnow := time.Now()\n\tclaims := &Claims{}\n\tclaims.IssuedAt = jwt.NewNumericDate(now)\n\tclaims.NotBefore = jwt.NewNumericDate(now.Add(-10 * time.Second))\n\tclaims.Expiry = jwt.NewNumericDate(now.Add(duration))\n\tclaims.Audience = []string{audience, ip}\n\treturn claims\n}\n\ntype Claims struct {\n\tjwt.Claims\n\tUsername                   string   `json:\"username,omitempty\"`\n\tPermissions                []string `json:\"permissions,omitempty\"`\n\tRole                       string   `json:\"role,omitempty\"`\n\tAPIKeyID                   string   `json:\"api_key,omitempty\"`\n\tNodeID                     string   `json:\"node_id,omitempty\"`\n\tMustSetTwoFactorAuth       bool     `json:\"2fa_required,omitempty\"`\n\tMustChangePassword         bool     `json:\"chpwd,omitempty\"`\n\tRequiredTwoFactorProtocols []string `json:\"2fa_protos,omitempty\"`\n\tHideUserPageSections       int      `json:\"hus,omitempty\"`\n\tRef                        string   `json:\"ref,omitempty\"`\n}\n\nfunc (c *Claims) SetIssuedAt(t time.Time) {\n\tc.IssuedAt = jwt.NewNumericDate(t)\n}\n\nfunc (c *Claims) SetNotBefore(t time.Time) {\n\tc.NotBefore = jwt.NewNumericDate(t)\n}\n\nfunc (c *Claims) SetExpiry(t time.Time) {\n\tc.Expiry = jwt.NewNumericDate(t)\n}\n\nfunc (c *Claims) HasPerm(perm string) bool {\n\tfor _, p := range c.Permissions {\n\t\tif p == \"*\" || p == perm {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (c *Claims) HasAnyAudience(audiences []string) bool {\n\tfor _, a := range c.Audience {\n\t\tif slices.Contains(audiences, a) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (c *Claims) GenerateTokenResponse(signer *Signer) (TokenResponse, error) {\n\ttoken, err := signer.Sign(c)\n\tif err != nil {\n\t\treturn TokenResponse{}, err\n\t}\n\treturn c.BuildTokenResponse(token), nil\n}\n\nfunc (c *Claims) BuildTokenResponse(token string) TokenResponse {\n\treturn TokenResponse{Token: token, Expiry: c.Expiry.Time().UTC().Format(time.RFC3339)}\n}\n\ntype TokenResponse struct {\n\tToken  string `json:\"access_token\"`\n\tExpiry string `json:\"expires_at\"`\n}\n\nfunc NewSigner(algo jose.SignatureAlgorithm, key any) (*Signer, error) {\n\topts := (&jose.SignerOptions{}).WithType(\"JWT\")\n\tsigner, err := jose.NewSigner(jose.SigningKey{Algorithm: algo, Key: key}, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Signer{\n\t\tsigner: signer,\n\t\talgo:   []jose.SignatureAlgorithm{algo},\n\t\tkey:    key,\n\t}, nil\n}\n\ntype Signer struct {\n\talgo   []jose.SignatureAlgorithm\n\tsigner jose.Signer\n\tkey    any\n}\n\nfunc (s *Signer) Sign(claims *Claims) (string, error) {\n\tif claims.ID == \"\" {\n\t\tclaims.ID = xid.New().String()\n\t}\n\tif claims.IssuedAt == nil {\n\t\tclaims.IssuedAt = jwt.NewNumericDate(time.Now())\n\t}\n\tif claims.NotBefore == nil {\n\t\tclaims.NotBefore = jwt.NewNumericDate(time.Now().Add(-10 * time.Second))\n\t}\n\tif claims.Expiry == nil {\n\t\treturn \"\", errors.New(\"expiration must be set\")\n\t}\n\tif len(claims.Audience) == 0 {\n\t\treturn \"\", errors.New(\"audience must be set\")\n\t}\n\n\treturn jwt.Signed(s.signer).Claims(claims).Serialize()\n}\n\nfunc (s *Signer) Signer() jose.Signer {\n\treturn s.signer\n}\n\nfunc (s *Signer) SetSigner(signer jose.Signer) {\n\ts.signer = signer\n}\n\nfunc (s *Signer) SignWithParams(claims *Claims, audience, ip string, duration time.Duration) (string, error) {\n\tclaims.Expiry = jwt.NewNumericDate(time.Now().Add(duration))\n\tclaims.Audience = []string{audience, ip}\n\treturn s.Sign(claims)\n}\n\nfunc NewContext(ctx context.Context, claims *Claims, err error) context.Context {\n\tctx = context.WithValue(ctx, TokenCtxKey, claims)\n\tctx = context.WithValue(ctx, ErrorCtxKey, err)\n\treturn ctx\n}\n\nfunc FromContext(ctx context.Context) (*Claims, error) {\n\tval := ctx.Value(TokenCtxKey)\n\ttoken, ok := val.(*Claims)\n\tif !ok && val != nil {\n\t\treturn nil, fmt.Errorf(\"invalid type for TokenCtxKey: %T\", val)\n\t}\n\n\tvalErr := ctx.Value(ErrorCtxKey)\n\terr, ok := valErr.(error)\n\tif !ok && valErr != nil {\n\t\treturn nil, fmt.Errorf(\"invalid type for ErrorCtxKey: %T\", valErr)\n\t}\n\tif token == nil {\n\t\treturn nil, errors.New(\"no token found\")\n\t}\n\n\treturn token, err\n}\n\nfunc Verify(s *Signer, findTokenFns ...func(r *http.Request) string) func(http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\thfn := func(w http.ResponseWriter, r *http.Request) {\n\t\t\tctx := r.Context()\n\t\t\ttoken, err := VerifyRequest(s, r, findTokenFns...)\n\t\t\tctx = NewContext(ctx, token, err)\n\t\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t\t}\n\t\treturn http.HandlerFunc(hfn)\n\t}\n}\n\nfunc VerifyRequest(s *Signer, r *http.Request, findTokenFns ...func(r *http.Request) string) (*Claims, error) {\n\tvar tokenString string\n\tfor _, fn := range findTokenFns {\n\t\ttokenString = fn(r)\n\t\tif tokenString != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\tif tokenString == \"\" {\n\t\treturn nil, errors.New(\"no token found\")\n\t}\n\treturn VerifyToken(s, tokenString)\n}\n\nfunc VerifyToken(s *Signer, payload string) (*Claims, error) {\n\treturn VerifyTokenWithKey(payload, s.algo, s.key)\n}\n\nfunc VerifyTokenWithKey(payload string, algo []jose.SignatureAlgorithm, key any) (*Claims, error) {\n\ttoken, err := jwt.ParseSigned(payload, algo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar claims Claims\n\terr = token.Claims(key, &claims)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := claims.ValidateWithLeeway(jwt.Expected{Time: time.Now()}, 30*time.Second); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &claims, nil\n}\n\n// TokenFromCookie tries to retrieve the token string from a cookie named\n// \"jwt\".\nfunc TokenFromCookie(r *http.Request) string {\n\tcookie, err := r.Cookie(CookieKey)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn cookie.Value\n}\n\n// TokenFromHeader tries to retrieve the token string from the\n// \"Authorization\" request header: \"Authorization: BEARER T\".\nfunc TokenFromHeader(r *http.Request) string {\n\t// Get token from authorization header.\n\tbearer := r.Header.Get(\"Authorization\")\n\tconst prefix = \"Bearer \"\n\tif len(bearer) >= len(prefix) && strings.EqualFold(bearer[:len(prefix)], prefix) {\n\t\treturn bearer[len(prefix):]\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/jwt/jwt_test.go",
    "content": "package jwt\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v4\"\n\t\"github.com/go-jose/go-jose/v4/jwt\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\ntype failingJoseSigner struct{}\n\nfunc (s *failingJoseSigner) Sign(payload []byte) (*jose.JSONWebSignature, error) {\n\treturn nil, errors.New(\"sign test error\")\n}\n\nfunc (s *failingJoseSigner) Options() jose.SignerOptions {\n\treturn jose.SignerOptions{}\n}\n\nfunc TestJWTToken(t *testing.T) {\n\ts, err := NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\tusername := util.GenerateUniqueID()\n\tclaims := Claims{\n\t\tUsername: username,\n\t\tClaims: jwt.Claims{\n\t\t\tAudience:  jwt.Audience{\"test\"},\n\t\t\tExpiry:    jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),\n\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t},\n\t}\n\ttoken, err := s.Sign(&claims)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, token)\n\n\tparsed, err := VerifyToken(s, token)\n\trequire.NoError(t, err)\n\trequire.Equal(t, username, parsed.Username)\n\n\tja1, err := NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\n\ttoken, err = ja1.Sign(&claims)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, token)\n\t_, err = VerifyToken(s, token)\n\trequire.Error(t, err)\n\t_, err = VerifyToken(ja1, token)\n\trequire.NoError(t, err)\n}\n\nfunc TestClaims(t *testing.T) {\n\tclaims := NewClaims(util.GenerateUniqueID(), \"\", 10*time.Minute)\n\ts, err := NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\ttoken, err := s.Sign(claims)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, token)\n\tassert.NotNil(t, claims.Expiry)\n\tassert.NotNil(t, claims.IssuedAt)\n\tassert.NotNil(t, claims.NotBefore)\n\n\tclaims = &Claims{\n\t\tPermissions: []string{\"myperm\"},\n\t}\n\tclaims.SetExpiry(time.Now().Add(1 * time.Minute))\n\tclaims.Audience = []string{\"testaudience\"}\n\t_, err = s.Sign(claims)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, claims.IssuedAt)\n\tassert.NotNil(t, claims.NotBefore)\n\tassert.True(t, claims.HasAnyAudience([]string{util.GenerateUniqueID(), util.GenerateUniqueID(), \"testaudience\"}))\n\tassert.False(t, claims.HasAnyAudience([]string{util.GenerateUniqueID()}))\n\tassert.True(t, claims.HasPerm(\"myperm\"))\n\tassert.False(t, claims.HasPerm(util.GenerateUniqueID()))\n\tresp, err := claims.GenerateTokenResponse(s)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, resp.Token)\n\tassert.Equal(t, claims.Expiry.Time().UTC().Format(time.RFC3339), resp.Expiry)\n\tclaims.SetIssuedAt(time.Now())\n\tclaims.SetNotBefore(time.Now().Add(10 * time.Minute))\n\ttoken, err = s.SignWithParams(claims, util.GenerateUniqueID(), \"127.0.0.1\", time.Minute)\n\tassert.NoError(t, err)\n\t_, err = VerifyToken(s, token)\n\tassert.ErrorContains(t, err, \"nbf\")\n\tclaims = &Claims{}\n\t_, err = s.Sign(claims)\n\tassert.ErrorContains(t, err, \"expiration must be set\")\n\tclaims.SetExpiry(time.Now())\n\t_, err = s.Sign(claims)\n\tassert.ErrorContains(t, err, \"audience must be set\")\n\tclaims = &Claims{}\n\t_, err = s.SignWithParams(claims, util.GenerateUniqueID(), \"\", time.Minute)\n\tassert.NoError(t, err)\n}\n\nfunc TestClaimsPermissions(t *testing.T) {\n\tc := Claims{\n\t\tPermissions: []string{\"*\"},\n\t}\n\tassert.True(t, c.HasPerm(util.GenerateUniqueID()))\n\tc.Permissions = []string{\"list\"}\n\tassert.False(t, c.HasPerm(util.GenerateUniqueID()))\n\tassert.True(t, c.HasPerm(\"list\"))\n}\n\nfunc TestErrors(t *testing.T) {\n\ts, err := NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\t_, err = VerifyToken(s, util.GenerateUniqueID())\n\tassert.Error(t, err)\n\tclaims := &Claims{}\n\tclaims.SetExpiry(time.Now().Add(-1 * time.Minute))\n\ttoken, err := jwt.Signed(s.Signer()).Claims(claims).Serialize()\n\tassert.NoError(t, err)\n\t_, err = VerifyToken(s, token)\n\tassert.ErrorContains(t, err, \"exp\")\n\tclaims.SetExpiry(time.Now().Add(2 * time.Minute))\n\tclaims.SetIssuedAt(time.Now().Add(1 * time.Minute))\n\ttoken, err = jwt.Signed(s.Signer()).Claims(claims).Serialize()\n\tassert.NoError(t, err)\n\t_, err = VerifyToken(s, token)\n\tassert.ErrorContains(t, err, \"iat\")\n\tclaims.SetIssuedAt(time.Now())\n\tclaims.SetNotBefore(time.Now().Add(1 * time.Minute))\n\ttoken, err = jwt.Signed(s.Signer()).Claims(claims).Serialize()\n\tassert.NoError(t, err)\n\t_, err = VerifyToken(s, token)\n\tassert.ErrorContains(t, err, \"nbf\")\n\n\ts.SetSigner(&failingJoseSigner{})\n\tclaims = NewClaims(util.GenerateUniqueID(), \"\", time.Minute)\n\t_, err = s.Sign(claims)\n\tassert.Error(t, err)\n\t_, err = claims.GenerateTokenResponse(s)\n\tassert.Error(t, err)\n\t// Wrong algorithm\n\t_, err = NewSigner(\"PS256\", util.GenerateRandomBytes(32))\n\tassert.Error(t, err)\n}\n\nfunc TestTokenFromRequest(t *testing.T) {\n\tclaims := NewClaims(util.GenerateUniqueID(), \"\", 10*time.Minute)\n\ts, err := NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\ttoken, err := s.Sign(claims)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, token)\n\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"jwt=%s\", token))\n\tcookie := TokenFromCookie(req)\n\tassert.Equal(t, token, cookie)\n\treq, err = http.NewRequest(http.MethodGet, \"/\", nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\t_, err = VerifyRequest(s, req, TokenFromHeader)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Authorization\", token)\n\tassert.Empty(t, TokenFromHeader(req))\n\tassert.Empty(t, TokenFromCookie(req))\n\t_, err = VerifyRequest(s, req, TokenFromCookie)\n\tassert.ErrorContains(t, err, \"no token found\")\n}\n\nfunc TestContext(t *testing.T) {\n\tclaims := &Claims{\n\t\tUsername: util.GenerateUniqueID(),\n\t}\n\ts, err := NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\ttoken, err := s.SignWithParams(claims, util.GenerateUniqueID(), \"\", time.Minute)\n\trequire.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\trequire.NoError(t, err)\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\n\th := Verify(s, TokenFromHeader)\n\twrapped := h(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\ttoken, err := FromContext(r.Context())\n\t\tassert.Nil(t, err)\n\t\tassert.Equal(t, claims.Username, token.Username)\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\trr := httptest.NewRecorder()\n\twrapped.ServeHTTP(rr, req)\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\n\t_, err = FromContext(context.Background())\n\tassert.ErrorContains(t, err, \"no token found\")\n\n\tctx := NewContext(context.Background(), &Claims{}, fs.ErrClosed)\n\t_, err = FromContext(ctx)\n\tassert.Equal(t, fs.ErrClosed, err)\n\n\tctx = context.WithValue(context.Background(), TokenCtxKey, \"1\")\n\t_, err = FromContext(ctx)\n\tassert.ErrorContains(t, err, \"invalid type for TokenCtxKey\")\n\n\tctx = context.WithValue(context.Background(), ErrorCtxKey, 2)\n\t_, err = FromContext(ctx)\n\tassert.ErrorContains(t, err, \"invalid type for ErrorCtxKey\")\n\tclaims = NewClaims(util.GenerateUniqueID(), \"127.1.1.1\", time.Minute)\n\t_, err = s.Sign(claims)\n\trequire.NoError(t, err)\n\tctx = context.WithValue(context.Background(), TokenCtxKey, claims)\n\tclaimsFromContext, err := FromContext(ctx)\n\tassert.NoError(t, err)\n\tassert.Equal(t, claims, claimsFromContext)\n\n\tassert.Equal(t, \"jwt context value Token\", TokenCtxKey.String())\n}\n\nfunc TestValidationLeeway(t *testing.T) {\n\ts, err := NewSigner(jose.HS256, util.GenerateRandomBytes(32))\n\trequire.NoError(t, err)\n\tclaims := &Claims{}\n\tclaims.Audience = []string{util.GenerateUniqueID()}\n\tclaims.SetIssuedAt(time.Now().Add(10 * time.Second)) // issued at in the future\n\tclaims.SetExpiry(time.Now().Add(10 * time.Second))\n\ttoken, err := s.Sign(claims)\n\trequire.NoError(t, err)\n\t_, err = VerifyToken(s, token)\n\tassert.NoError(t, err)\n\n\tclaims = &Claims{}\n\tclaims.Audience = []string{util.GenerateUniqueID()}\n\tclaims.SetExpiry(time.Now().Add(-10 * time.Second)) // expired\n\ttoken, err = s.Sign(claims)\n\trequire.NoError(t, err)\n\t_, err = VerifyToken(s, token)\n\tassert.NoError(t, err)\n\n\tclaims = &Claims{}\n\tclaims.Audience = []string{util.GenerateUniqueID()}\n\tclaims.SetExpiry(time.Now().Add(30 * time.Second))\n\tclaims.SetNotBefore(time.Now().Add(10 * time.Second)) // not before in the future\n\ttoken, err = s.Sign(claims)\n\trequire.NoError(t, err)\n\t_, err = VerifyToken(s, token)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/kms/basesecret.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage kms\n\nimport (\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n)\n\n// BaseSecret defines the base struct shared among all the secret providers\ntype BaseSecret struct {\n\tStatus         sdkkms.SecretStatus `json:\"status,omitempty\"`\n\tPayload        string              `json:\"payload,omitempty\"`\n\tKey            string              `json:\"key,omitempty\"`\n\tAdditionalData string              `json:\"additional_data,omitempty\"`\n\t// 1 means encrypted using a master key\n\tMode int `json:\"mode,omitempty\"`\n}\n\n// GetStatus returns the secret's status\nfunc (s *BaseSecret) GetStatus() sdkkms.SecretStatus {\n\treturn s.Status\n}\n\n// GetPayload returns the secret's payload\nfunc (s *BaseSecret) GetPayload() string {\n\treturn s.Payload\n}\n\n// GetKey returns the secret's key\nfunc (s *BaseSecret) GetKey() string {\n\treturn s.Key\n}\n\n// GetMode returns the encryption mode\nfunc (s *BaseSecret) GetMode() int {\n\treturn s.Mode\n}\n\n// GetAdditionalData returns the secret's additional data\nfunc (s *BaseSecret) GetAdditionalData() string {\n\treturn s.AdditionalData\n}\n\n// SetKey sets the secret's key\nfunc (s *BaseSecret) SetKey(value string) {\n\ts.Key = value\n}\n\n// SetAdditionalData sets the secret's additional data\nfunc (s *BaseSecret) SetAdditionalData(value string) {\n\ts.AdditionalData = value\n}\n\n// SetStatus sets the secret's status\nfunc (s *BaseSecret) SetStatus(value sdkkms.SecretStatus) {\n\ts.Status = value\n}\n\nfunc (s *BaseSecret) isEmpty() bool {\n\tif s.Status != \"\" {\n\t\treturn false\n\t}\n\tif s.Payload != \"\" {\n\t\treturn false\n\t}\n\tif s.Key != \"\" {\n\t\treturn false\n\t}\n\tif s.AdditionalData != \"\" {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "internal/kms/builtin.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage kms\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nvar (\n\terrMalformedCiphertext = errors.New(\"malformed ciphertext\")\n)\n\ntype builtinSecret struct {\n\tBaseSecret\n}\n\nfunc init() {\n\tRegisterSecretProvider(sdkkms.SchemeBuiltin, sdkkms.SecretStatusAES256GCM, newBuiltinSecret)\n}\n\nfunc newBuiltinSecret(base BaseSecret, _, _ string) SecretProvider {\n\treturn &builtinSecret{\n\t\tBaseSecret: base,\n\t}\n}\n\nfunc (s *builtinSecret) Name() string {\n\treturn \"Builtin\"\n}\n\nfunc (s *builtinSecret) IsEncrypted() bool {\n\treturn s.Status == sdkkms.SecretStatusAES256GCM\n}\n\nfunc (s *builtinSecret) deriveKey(key []byte) []byte {\n\tvar combined []byte\n\tcombined = append(combined, key...)\n\tif s.AdditionalData != \"\" {\n\t\tcombined = append(combined, []byte(s.AdditionalData)...)\n\t}\n\tcombined = append(combined, key...)\n\thash := sha256.Sum256(combined)\n\treturn hash[:]\n}\n\nfunc (s *builtinSecret) Encrypt() error {\n\tif s.Payload == \"\" {\n\t\treturn ErrInvalidSecret\n\t}\n\tswitch s.Status {\n\tcase sdkkms.SecretStatusPlain:\n\t\tkey := make([]byte, 32)\n\t\tif _, err := io.ReadFull(rand.Reader, key); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tblock, err := aes.NewCipher(s.deriveKey(key))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgcm, err := cipher.NewGCM(block)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnonce := make([]byte, gcm.NonceSize())\n\t\tif _, err = io.ReadFull(rand.Reader, nonce); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar aad []byte\n\t\tif s.AdditionalData != \"\" {\n\t\t\taad = []byte(s.AdditionalData)\n\t\t}\n\t\tciphertext := gcm.Seal(nonce, nonce, []byte(s.Payload), aad)\n\t\ts.Key = hex.EncodeToString(key)\n\t\ts.Payload = hex.EncodeToString(ciphertext)\n\t\ts.Status = sdkkms.SecretStatusAES256GCM\n\t\treturn nil\n\tdefault:\n\t\treturn ErrWrongSecretStatus\n\t}\n}\n\nfunc (s *builtinSecret) Decrypt() error {\n\tswitch s.Status {\n\tcase sdkkms.SecretStatusAES256GCM:\n\t\tencrypted, err := hex.DecodeString(s.Payload)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tkey, err := hex.DecodeString(s.Key)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tblock, err := aes.NewCipher(s.deriveKey(key))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgcm, err := cipher.NewGCM(block)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnonceSize := gcm.NonceSize()\n\t\tif len(encrypted) < nonceSize {\n\t\t\treturn errMalformedCiphertext\n\t\t}\n\t\tnonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:]\n\t\tvar aad []byte\n\t\tif s.AdditionalData != \"\" {\n\t\t\taad = []byte(s.AdditionalData)\n\t\t}\n\t\tplaintext, err := gcm.Open(nil, nonce, ciphertext, aad)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.Status = sdkkms.SecretStatusPlain\n\t\ts.Payload = util.BytesToString(plaintext)\n\t\ts.Key = \"\"\n\t\ts.AdditionalData = \"\"\n\t\treturn nil\n\tdefault:\n\t\treturn ErrWrongSecretStatus\n\t}\n}\n\nfunc (s *builtinSecret) Clone() SecretProvider {\n\tbaseSecret := BaseSecret{\n\t\tStatus:         s.Status,\n\t\tPayload:        s.Payload,\n\t\tKey:            s.Key,\n\t\tAdditionalData: s.AdditionalData,\n\t\tMode:           s.Mode,\n\t}\n\treturn newBuiltinSecret(baseSecret, \"\", \"\")\n}\n"
  },
  {
    "path": "internal/kms/kms.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package kms provides Key Management Services support\npackage kms\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strings\"\n\t\"sync\"\n\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// SecretProvider defines the interface for a KMS secrets provider\ntype SecretProvider interface {\n\tName() string\n\tEncrypt() error\n\tDecrypt() error\n\tIsEncrypted() bool\n\tGetStatus() sdkkms.SecretStatus\n\tGetPayload() string\n\tGetKey() string\n\tGetAdditionalData() string\n\tGetMode() int\n\tSetKey(string)\n\tSetAdditionalData(string)\n\tSetStatus(sdkkms.SecretStatus)\n\tClone() SecretProvider\n}\n\nconst (\n\tlogSender = \"kms\"\n)\n\n// Configuration defines the KMS configuration\ntype Configuration struct {\n\tSecrets Secrets `json:\"secrets\" mapstructure:\"secrets\"`\n}\n\n// Secrets define the KMS configuration for encryption/decryption\ntype Secrets struct {\n\tURL             string `json:\"url\" mapstructure:\"url\"`\n\tMasterKeyPath   string `json:\"master_key_path\" mapstructure:\"master_key_path\"`\n\tMasterKeyString string `json:\"master_key\" mapstructure:\"master_key\"`\n\tmasterKey       string\n}\n\ntype registeredSecretProvider struct {\n\tencryptedStatus sdkkms.SecretStatus\n\tnewFn           func(base BaseSecret, url, masterKey string) SecretProvider\n}\n\nvar (\n\t// ErrWrongSecretStatus defines the error to return if the secret status is not appropriate\n\t// for the request operation\n\tErrWrongSecretStatus = errors.New(\"wrong secret status\")\n\t// ErrInvalidSecret defines the error to return if a secret is not valid\n\tErrInvalidSecret    = errors.New(\"invalid secret\")\n\tvalidSecretStatuses = []string{sdkkms.SecretStatusPlain, sdkkms.SecretStatusAES256GCM, sdkkms.SecretStatusSecretBox,\n\t\tsdkkms.SecretStatusVaultTransit, sdkkms.SecretStatusAWS, sdkkms.SecretStatusGCP, sdkkms.SecretStatusAzureKeyVault,\n\t\tsdkkms.SecretStatusOracleKeyVault, sdkkms.SecretStatusRedacted}\n\tconfig          Configuration\n\tsecretProviders = make(map[string]registeredSecretProvider)\n)\n\n// RegisterSecretProvider register a new secret provider\nfunc RegisterSecretProvider(scheme string, encryptedStatus sdkkms.SecretStatus,\n\tfn func(base BaseSecret, url, masterKey string) SecretProvider,\n) {\n\tsecretProviders[scheme] = registeredSecretProvider{\n\t\tencryptedStatus: encryptedStatus,\n\t\tnewFn:           fn,\n\t}\n}\n\n// NewSecret builds a new Secret using the provided arguments\nfunc NewSecret(status sdkkms.SecretStatus, payload, key, data string) *Secret {\n\treturn config.newSecret(status, payload, key, data)\n}\n\n// NewEmptySecret returns an empty secret\nfunc NewEmptySecret() *Secret {\n\treturn NewSecret(\"\", \"\", \"\", \"\")\n}\n\n// NewPlainSecret stores the give payload in a plain text secret\nfunc NewPlainSecret(payload string) *Secret {\n\treturn NewSecret(sdkkms.SecretStatusPlain, strings.TrimSpace(payload), \"\", \"\")\n}\n\n// Initialize configures the KMS support\nfunc (c *Configuration) Initialize() error {\n\tif c.Secrets.MasterKeyPath != \"\" {\n\t\tmKey, err := util.ReadConfigFromFile(c.Secrets.MasterKeyPath, \"\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.Secrets.masterKey = mKey\n\t} else if c.Secrets.MasterKeyString != \"\" {\n\t\tc.Secrets.masterKey = c.Secrets.MasterKeyString\n\t}\n\tconfig = *c\n\tif config.Secrets.URL == \"\" {\n\t\tconfig.Secrets.URL = sdkkms.SchemeLocal + \"://\"\n\t}\n\tfor k, v := range secretProviders {\n\t\tlogger.Info(logSender, \"\", \"secret provider registered for scheme: %q, encrypted status: %q\",\n\t\t\tk, v.encryptedStatus)\n\t}\n\treturn nil\n}\n\nfunc (c *Configuration) newSecret(status sdkkms.SecretStatus, payload, key, data string) *Secret {\n\tbase := BaseSecret{\n\t\tStatus:         status,\n\t\tKey:            key,\n\t\tPayload:        payload,\n\t\tAdditionalData: data,\n\t}\n\treturn &Secret{\n\t\tprovider: c.getSecretProvider(base),\n\t}\n}\n\nfunc (c *Configuration) getSecretProvider(base BaseSecret) SecretProvider {\n\tfor k, v := range secretProviders {\n\t\tif strings.HasPrefix(c.Secrets.URL, k) {\n\t\t\treturn v.newFn(base, c.Secrets.URL, c.Secrets.masterKey)\n\t\t}\n\t}\n\tlogger.Warn(logSender, \"\", \"no secret provider registered for URL %v, fallback to local provider\", c.Secrets.URL)\n\treturn NewLocalSecret(base, c.Secrets.URL, c.Secrets.masterKey)\n}\n\n// Secret defines the struct used to store confidential data\ntype Secret struct {\n\tsync.RWMutex\n\tprovider SecretProvider\n}\n\n// MarshalJSON return the JSON encoding of the Secret object\nfunc (s *Secret) MarshalJSON() ([]byte, error) {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn json.Marshal(&BaseSecret{\n\t\tStatus:         s.provider.GetStatus(),\n\t\tPayload:        s.provider.GetPayload(),\n\t\tKey:            s.provider.GetKey(),\n\t\tAdditionalData: s.provider.GetAdditionalData(),\n\t\tMode:           s.provider.GetMode(),\n\t})\n}\n\n// UnmarshalJSON parses the JSON-encoded data and stores the result\n// in the Secret object\nfunc (s *Secret) UnmarshalJSON(data []byte) error {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\tbaseSecret := BaseSecret{}\n\terr := json.Unmarshal(data, &baseSecret)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif baseSecret.isEmpty() {\n\t\ts.provider = config.getSecretProvider(baseSecret)\n\t\treturn nil\n\t}\n\n\tif baseSecret.Status == sdkkms.SecretStatusPlain || baseSecret.Status == sdkkms.SecretStatusRedacted {\n\t\ts.provider = config.getSecretProvider(baseSecret)\n\t\treturn nil\n\t}\n\n\tfor _, v := range secretProviders {\n\t\tif v.encryptedStatus == baseSecret.Status {\n\t\t\ts.provider = v.newFn(baseSecret, config.Secrets.URL, config.Secrets.masterKey)\n\t\t\treturn nil\n\t\t}\n\t}\n\tlogger.Error(logSender, \"\", \"no provider registered for status %q\", baseSecret.Status)\n\treturn ErrInvalidSecret\n}\n\n// IsEqual returns true if all the secrets fields are equal\nfunc (s *Secret) IsEqual(other *Secret) bool {\n\tif s.GetStatus() != other.GetStatus() {\n\t\treturn false\n\t}\n\tif s.GetPayload() != other.GetPayload() {\n\t\treturn false\n\t}\n\tif s.GetKey() != other.GetKey() {\n\t\treturn false\n\t}\n\tif s.GetAdditionalData() != other.GetAdditionalData() {\n\t\treturn false\n\t}\n\tif s.GetMode() != other.GetMode() {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// Clone returns a copy of the secret object\nfunc (s *Secret) Clone() *Secret {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn &Secret{\n\t\tprovider: s.provider.Clone(),\n\t}\n}\n\n// IsEncrypted returns true if the secret is encrypted\n// This isn't a pointer receiver because we don't want to pass\n// a pointer to html template\nfunc (s *Secret) IsEncrypted() bool {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn s.provider.IsEncrypted()\n}\n\n// IsPlain returns true if the secret is in plain text\nfunc (s *Secret) IsPlain() bool {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn s.provider.GetStatus() == sdkkms.SecretStatusPlain\n}\n\n// IsNotPlainAndNotEmpty returns true if the secret is not plain and not empty.\n// This is an utility method, we update the secret for an existing user\n// if it is empty or plain\nfunc (s *Secret) IsNotPlainAndNotEmpty() bool {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn !s.IsPlain() && !s.IsEmpty()\n}\n\n// IsRedacted returns true if the secret is redacted\nfunc (s *Secret) IsRedacted() bool {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn s.provider.GetStatus() == sdkkms.SecretStatusRedacted\n}\n\n// GetPayload returns the secret payload\nfunc (s *Secret) GetPayload() string {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn s.provider.GetPayload()\n}\n\n// GetAdditionalData returns the secret additional data\nfunc (s *Secret) GetAdditionalData() string {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn s.provider.GetAdditionalData()\n}\n\n// GetStatus returns the secret status\nfunc (s *Secret) GetStatus() sdkkms.SecretStatus {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn s.provider.GetStatus()\n}\n\n// GetKey returns the secret key\nfunc (s *Secret) GetKey() string {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn s.provider.GetKey()\n}\n\n// GetMode returns the secret mode\nfunc (s *Secret) GetMode() int {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\treturn s.provider.GetMode()\n}\n\n// SetAdditionalData sets the given additional data\nfunc (s *Secret) SetAdditionalData(value string) {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\ts.provider.SetAdditionalData(value)\n}\n\n// SetStatus sets the status for this secret\nfunc (s *Secret) SetStatus(value sdkkms.SecretStatus) {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\ts.provider.SetStatus(value)\n}\n\n// SetKey sets the key for this secret\nfunc (s *Secret) SetKey(value string) {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\ts.provider.SetKey(value)\n}\n\n// IsEmpty returns true if all fields are empty\nfunc (s *Secret) IsEmpty() bool {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\tif s.provider.GetStatus() != \"\" {\n\t\treturn false\n\t}\n\tif s.provider.GetPayload() != \"\" {\n\t\treturn false\n\t}\n\tif s.provider.GetKey() != \"\" {\n\t\treturn false\n\t}\n\tif s.provider.GetAdditionalData() != \"\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// IsValid returns true if the secret is not empty and valid\nfunc (s *Secret) IsValid() bool {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\tif !s.IsValidInput() {\n\t\treturn false\n\t}\n\tswitch s.provider.GetStatus() {\n\tcase sdkkms.SecretStatusAES256GCM, sdkkms.SecretStatusSecretBox:\n\t\tif len(s.provider.GetKey()) != 64 {\n\t\t\treturn false\n\t\t}\n\tcase sdkkms.SecretStatusAWS, sdkkms.SecretStatusGCP, sdkkms.SecretStatusVaultTransit:\n\t\tkey := s.provider.GetKey()\n\t\tif key != \"\" && len(key) != 64 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// IsValidInput returns true if the secret is a valid user input\nfunc (s *Secret) IsValidInput() bool {\n\ts.RLock()\n\tdefer s.RUnlock()\n\n\tif !isSecretStatusValid(s.provider.GetStatus()) {\n\t\treturn false\n\t}\n\tif s.provider.GetPayload() == \"\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// Hide hides info to decrypt data\nfunc (s *Secret) Hide() {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\ts.provider.SetKey(\"\")\n\ts.provider.SetAdditionalData(\"\")\n}\n\n// Encrypt encrypts a plain text Secret object\nfunc (s *Secret) Encrypt() error {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\treturn s.provider.Encrypt()\n}\n\n// Decrypt decrypts a Secret object\nfunc (s *Secret) Decrypt() error {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\treturn s.provider.Decrypt()\n}\n\n// TryDecrypt decrypts a Secret object if encrypted.\n// It returns a nil error if the object is not encrypted\nfunc (s *Secret) TryDecrypt() error {\n\ts.Lock()\n\tdefer s.Unlock()\n\n\tif s.provider.IsEncrypted() {\n\t\treturn s.provider.Decrypt()\n\t}\n\treturn nil\n}\n\nfunc isSecretStatusValid(status string) bool {\n\tfor idx := range validSecretStatuses {\n\t\tif validSecretStatuses[idx] == status {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/kms/local.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage kms\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"io\"\n\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"gocloud.dev/secrets/localsecrets\"\n\t\"golang.org/x/crypto/hkdf\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nfunc init() {\n\tRegisterSecretProvider(sdkkms.SchemeLocal, sdkkms.SecretStatusSecretBox, NewLocalSecret)\n}\n\ntype localSecret struct {\n\tBaseSecret\n\tmasterKey string\n}\n\n// NewLocalSecret returns a SecretProvider that use a locally provided symmetric key\nfunc NewLocalSecret(base BaseSecret, _, masterKey string) SecretProvider {\n\treturn &localSecret{\n\t\tBaseSecret: base,\n\t\tmasterKey:  masterKey,\n\t}\n}\n\nfunc (s *localSecret) Name() string {\n\treturn \"Local\"\n}\n\nfunc (s *localSecret) IsEncrypted() bool {\n\treturn s.Status == sdkkms.SecretStatusSecretBox\n}\n\nfunc (s *localSecret) Encrypt() error {\n\tif s.Status != sdkkms.SecretStatusPlain {\n\t\treturn ErrWrongSecretStatus\n\t}\n\tif s.Payload == \"\" {\n\t\treturn ErrInvalidSecret\n\t}\n\tsecretKey, err := localsecrets.NewRandomKey()\n\tif err != nil {\n\t\treturn err\n\t}\n\tkey, err := s.deriveKey(secretKey[:], false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tkeeper := localsecrets.NewKeeper(key)\n\tdefer keeper.Close()\n\n\tciphertext, err := keeper.Encrypt(context.Background(), []byte(s.Payload))\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.Key = hex.EncodeToString(secretKey[:])\n\ts.Payload = base64.StdEncoding.EncodeToString(ciphertext)\n\ts.Status = sdkkms.SecretStatusSecretBox\n\ts.Mode = s.getEncryptionMode()\n\treturn nil\n}\n\nfunc (s *localSecret) Decrypt() error {\n\tif !s.IsEncrypted() {\n\t\treturn ErrWrongSecretStatus\n\t}\n\tencrypted, err := base64.StdEncoding.DecodeString(s.Payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsecretKey, err := hex.DecodeString(s.Key)\n\tif err != nil {\n\t\treturn err\n\t}\n\tkey, err := s.deriveKey(secretKey[:], true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tkeeper := localsecrets.NewKeeper(key)\n\tdefer keeper.Close()\n\n\tplaintext, err := keeper.Decrypt(context.Background(), encrypted)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.Status = sdkkms.SecretStatusPlain\n\ts.Payload = util.BytesToString(plaintext)\n\ts.Key = \"\"\n\ts.AdditionalData = \"\"\n\ts.Mode = 0\n\treturn nil\n}\n\nfunc (s *localSecret) deriveKey(key []byte, isForDecryption bool) ([32]byte, error) {\n\tvar masterKey []byte\n\tif s.masterKey == \"\" || (isForDecryption && s.Mode == 0) {\n\t\tvar combined []byte\n\t\tcombined = append(combined, key...)\n\t\tif s.AdditionalData != \"\" {\n\t\t\tcombined = append(combined, []byte(s.AdditionalData)...)\n\t\t}\n\t\tcombined = append(combined, key...)\n\t\thash := sha256.Sum256(combined)\n\t\tmasterKey = hash[:]\n\t} else {\n\t\tmasterKey = []byte(s.masterKey)\n\t}\n\tvar derivedKey [32]byte\n\tvar info []byte\n\tif s.AdditionalData != \"\" {\n\t\tinfo = []byte(s.AdditionalData)\n\t}\n\tkdf := hkdf.New(sha256.New, masterKey, key, info)\n\tif _, err := io.ReadFull(kdf, derivedKey[:]); err != nil {\n\t\treturn derivedKey, err\n\t}\n\treturn derivedKey, nil\n}\n\nfunc (s *localSecret) getEncryptionMode() int {\n\tif s.masterKey == \"\" {\n\t\treturn 0\n\t}\n\treturn 1\n}\n\nfunc (s *localSecret) Clone() SecretProvider {\n\tbaseSecret := BaseSecret{\n\t\tStatus:         s.Status,\n\t\tPayload:        s.Payload,\n\t\tKey:            s.Key,\n\t\tAdditionalData: s.AdditionalData,\n\t\tMode:           s.Mode,\n\t}\n\treturn NewLocalSecret(baseSecret, \"\", s.masterKey)\n}\n"
  },
  {
    "path": "internal/logger/hclog.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage logger\n\nimport (\n\t\"io\"\n\t\"log\"\n\n\t\"github.com/hashicorp/go-hclog\"\n\t\"github.com/rs/zerolog\"\n)\n\n// HCLogAdapter is an adapter for hclog.Logger\ntype HCLogAdapter struct {\n\thclog.Logger\n}\n\n// Log emits a message and key/value pairs at a provided log level\nfunc (l *HCLogAdapter) Log(level hclog.Level, msg string, args ...any) {\n\t// Workaround to avoid logging plugin arguments that may contain sensitive data.\n\t// Check everytime we update go-plugin library.\n\tif msg == \"starting plugin\" {\n\t\treturn\n\t}\n\tvar ev *zerolog.Event\n\tswitch level {\n\tcase hclog.Info:\n\t\tev = logger.Info()\n\tcase hclog.Warn:\n\t\tev = logger.Warn()\n\tcase hclog.Error:\n\t\tev = logger.Error()\n\tdefault:\n\t\tev = logger.Debug()\n\t}\n\tev.Timestamp().Str(\"sender\", l.Name())\n\taddKeysAndValues(ev, args...)\n\tev.Msg(msg)\n}\n\n// Trace emits a message and key/value pairs at the TRACE level\nfunc (l *HCLogAdapter) Trace(msg string, args ...any) {\n\tl.Log(hclog.Debug, msg, args...)\n}\n\n// Debug emits a message and key/value pairs at the DEBUG level\nfunc (l *HCLogAdapter) Debug(msg string, args ...any) {\n\tl.Log(hclog.Debug, msg, args...)\n}\n\n// Info emits a message and key/value pairs at the INFO level\nfunc (l *HCLogAdapter) Info(msg string, args ...any) {\n\tl.Log(hclog.Info, msg, args...)\n}\n\n// Warn emits a message and key/value pairs at the WARN level\nfunc (l *HCLogAdapter) Warn(msg string, args ...any) {\n\tl.Log(hclog.Warn, msg, args...)\n}\n\n// Error emits a message and key/value pairs at the ERROR level\nfunc (l *HCLogAdapter) Error(msg string, args ...any) {\n\tl.Log(hclog.Error, msg, args...)\n}\n\n// With creates a sub-logger\nfunc (l *HCLogAdapter) With(args ...any) hclog.Logger {\n\treturn &HCLogAdapter{Logger: l.Logger.With(args...)}\n}\n\n// Named creates a logger that will prepend the name string on the front of all messages\nfunc (l *HCLogAdapter) Named(name string) hclog.Logger {\n\treturn &HCLogAdapter{Logger: l.Logger.Named(name)}\n}\n\n// StandardLogger returns a value that conforms to the stdlib log.Logger interface\nfunc (l *HCLogAdapter) StandardLogger(_ *hclog.StandardLoggerOptions) *log.Logger {\n\treturn log.New(&StdLoggerWrapper{Sender: l.Name()}, \"\", 0)\n}\n\n// StandardWriter returns a value that conforms to io.Writer, which can be passed into log.SetOutput()\nfunc (l *HCLogAdapter) StandardWriter(_ *hclog.StandardLoggerOptions) io.Writer {\n\treturn &StdLoggerWrapper{Sender: l.Name()}\n}\n"
  },
  {
    "path": "internal/logger/lego.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage logger\n\nimport \"fmt\"\n\nconst (\n\tlegoLogSender = \"lego\"\n)\n\n// LegoAdapter is an adapter for lego.StdLogger\ntype LegoAdapter struct {\n\tLogToConsole bool\n}\n\n// Fatal emits a log at Error level\nfunc (l *LegoAdapter) Fatal(args ...any) {\n\tif l.LogToConsole {\n\t\tErrorToConsole(\"%s\", fmt.Sprint(args...))\n\t\treturn\n\t}\n\tLog(LevelError, legoLogSender, \"\", \"%s\", fmt.Sprint(args...))\n}\n\n// Fatalln is the same as Fatal\nfunc (l *LegoAdapter) Fatalln(args ...any) {\n\tl.Fatal(args...)\n}\n\n// Fatalf emits a log at Error level\nfunc (l *LegoAdapter) Fatalf(format string, args ...any) {\n\tif l.LogToConsole {\n\t\tErrorToConsole(format, args...)\n\t\treturn\n\t}\n\tLog(LevelError, legoLogSender, \"\", format, args...)\n}\n\n// Print emits a log at Info level\nfunc (l *LegoAdapter) Print(args ...any) {\n\tif l.LogToConsole {\n\t\tInfoToConsole(\"%s\", fmt.Sprint(args...))\n\t\treturn\n\t}\n\tLog(LevelInfo, legoLogSender, \"\", \"%s\", fmt.Sprint(args...))\n}\n\n// Println is the same as Print\nfunc (l *LegoAdapter) Println(args ...any) {\n\tl.Print(args...)\n}\n\n// Printf emits a log at Info level\nfunc (l *LegoAdapter) Printf(format string, args ...any) {\n\tif l.LogToConsole {\n\t\tInfoToConsole(format, args...)\n\t\treturn\n\t}\n\tLog(LevelInfo, legoLogSender, \"\", format, args...)\n}\n"
  },
  {
    "path": "internal/logger/logger.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package logger provides logging capabilities.\n// It is a wrapper around zerolog for logging and lumberjack for log rotation.\n// Logs are written to the specified log file.\n// Logging on the console is provided to print initialization info, errors and warnings.\n// The package provides a request logger to log the HTTP requests for REST API too.\n// The request logger uses chi.middleware.RequestLogger,\n// chi.middleware.LogFormatter and chi.middleware.LogEntry to build a structured\n// logger using zerolog\npackage logger\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\tlumberjack \"gopkg.in/natefinch/lumberjack.v2\"\n)\n\nconst (\n\tdateFormat = \"2006-01-02T15:04:05.000\" // YYYY-MM-DDTHH:MM:SS.ZZZ\n)\n\n// LogLevel defines log levels.\ntype LogLevel uint8\n\n// defines our own log levels, just in case we'll change logger in future\nconst (\n\tLevelDebug LogLevel = iota\n\tLevelInfo\n\tLevelWarn\n\tLevelError\n)\n\nvar (\n\tlogger        zerolog.Logger\n\tconsoleLogger zerolog.Logger\n\trollingLogger *lumberjack.Logger\n)\n\nfunc init() {\n\tzerolog.TimeFieldFormat = dateFormat\n}\n\n// GetLogger get the configured logger instance\nfunc GetLogger() *zerolog.Logger {\n\treturn &logger\n}\n\n// InitLogger configures the logger using the given parameters\nfunc InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge int, logCompress, logUTCTime bool,\n\tlevel zerolog.Level,\n) {\n\tSetLogTime(logUTCTime)\n\tif isLogFilePathValid(logFilePath) {\n\t\tlogDir := filepath.Dir(logFilePath)\n\t\tif _, err := os.Stat(logDir); errors.Is(err, fs.ErrNotExist) {\n\t\t\terr = os.MkdirAll(logDir, os.ModePerm)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"unable to create log dir %q: %v\", logDir, err)\n\t\t\t}\n\t\t}\n\t\trollingLogger = &lumberjack.Logger{\n\t\t\tFilename:   logFilePath,\n\t\t\tMaxSize:    logMaxSize,\n\t\t\tMaxBackups: logMaxBackups,\n\t\t\tMaxAge:     logMaxAge,\n\t\t\tCompress:   logCompress,\n\t\t\tLocalTime:  !logUTCTime,\n\t\t}\n\t\tlogger = zerolog.New(rollingLogger)\n\t\tEnableConsoleLogger(level)\n\t} else {\n\t\tlogger = zerolog.New(&logSyncWrapper{\n\t\t\toutput: os.Stdout,\n\t\t})\n\t\tconsoleLogger = zerolog.Nop()\n\t}\n\tlogger = logger.Level(level)\n}\n\n// InitStdErrLogger configures the logger to write to stderr\nfunc InitStdErrLogger(level zerolog.Level) {\n\tlogger = zerolog.New(&logSyncWrapper{\n\t\toutput: os.Stderr,\n\t}).Level(level)\n\tconsoleLogger = zerolog.Nop()\n}\n\n// DisableLogger disable the main logger.\n// ConsoleLogger will not be affected\nfunc DisableLogger() {\n\tlogger = zerolog.Nop()\n\trollingLogger = nil\n}\n\n// EnableConsoleLogger enables the console logger\nfunc EnableConsoleLogger(level zerolog.Level) {\n\tconsoleOutput := zerolog.ConsoleWriter{\n\t\tOut:        os.Stdout,\n\t\tTimeFormat: dateFormat,\n\t}\n\tconsoleLogger = zerolog.New(consoleOutput).With().Timestamp().Logger().Level(level)\n}\n\n// RotateLogFile closes the existing log file and immediately create a new one\nfunc RotateLogFile() error {\n\tif rollingLogger != nil {\n\t\treturn rollingLogger.Rotate()\n\t}\n\treturn errors.New(\"logging to file is disabled\")\n}\n\n// SetLogTime sets logging time related setting\nfunc SetLogTime(utc bool) {\n\tif utc {\n\t\tzerolog.TimestampFunc = func() time.Time {\n\t\t\treturn time.Now().UTC()\n\t\t}\n\t} else {\n\t\tzerolog.TimestampFunc = time.Now\n\t}\n}\n\n// Log logs at the specified level for the specified sender\nfunc Log(level LogLevel, sender string, connectionID string, format string, v ...any) {\n\tvar ev *zerolog.Event\n\tswitch level {\n\tcase LevelDebug:\n\t\tev = logger.Debug()\n\tcase LevelInfo:\n\t\tev = logger.Info()\n\tcase LevelWarn:\n\t\tev = logger.Warn()\n\tdefault:\n\t\tev = logger.Error()\n\t}\n\tev.Timestamp().Str(\"sender\", sender)\n\tif connectionID != \"\" {\n\t\tev.Str(\"connection_id\", connectionID)\n\t}\n\tev.Msg(fmt.Sprintf(format, v...))\n}\n\n// Debug logs at debug level for the specified sender\nfunc Debug(sender, connectionID, format string, v ...any) {\n\tLog(LevelDebug, sender, connectionID, format, v...)\n}\n\n// Info logs at info level for the specified sender\nfunc Info(sender, connectionID, format string, v ...any) {\n\tLog(LevelInfo, sender, connectionID, format, v...)\n}\n\n// Warn logs at warn level for the specified sender\nfunc Warn(sender, connectionID, format string, v ...any) {\n\tLog(LevelWarn, sender, connectionID, format, v...)\n}\n\n// Error logs at error level for the specified sender\nfunc Error(sender, connectionID, format string, v ...any) {\n\tLog(LevelError, sender, connectionID, format, v...)\n}\n\n// DebugToConsole logs at debug level to stdout\nfunc DebugToConsole(format string, v ...any) {\n\tconsoleLogger.Debug().Msg(fmt.Sprintf(format, v...))\n}\n\n// InfoToConsole logs at info level to stdout\nfunc InfoToConsole(format string, v ...any) {\n\tconsoleLogger.Info().Msg(fmt.Sprintf(format, v...))\n}\n\n// WarnToConsole logs at info level to stdout\nfunc WarnToConsole(format string, v ...any) {\n\tconsoleLogger.Warn().Msg(fmt.Sprintf(format, v...))\n}\n\n// ErrorToConsole logs at error level to stdout\nfunc ErrorToConsole(format string, v ...any) {\n\tconsoleLogger.Error().Msg(fmt.Sprintf(format, v...))\n}\n\n// TransferLog logs uploads or downloads\nfunc TransferLog(operation, path string, elapsed int64, size int64, user, connectionID, protocol, localAddr,\n\tremoteAddr, ftpMode string, err error,\n) {\n\tvar ev *zerolog.Event\n\tif err != nil {\n\t\tev = logger.Error()\n\t} else {\n\t\tev = logger.Info()\n\t}\n\tev.\n\t\tTimestamp().\n\t\tStr(\"sender\", operation).\n\t\tStr(\"local_addr\", localAddr).\n\t\tStr(\"remote_addr\", remoteAddr).\n\t\tInt64(\"elapsed_ms\", elapsed).\n\t\tInt64(\"size_bytes\", size).\n\t\tStr(\"username\", user).\n\t\tStr(\"file_path\", path).\n\t\tStr(\"connection_id\", connectionID).\n\t\tStr(\"protocol\", protocol)\n\tif ftpMode != \"\" {\n\t\tev.Str(\"ftp_mode\", ftpMode)\n\t}\n\tev.AnErr(\"error\", err).Send()\n}\n\n// CommandLog logs an SFTP/SCP/SSH command\nfunc CommandLog(command, path, target, user, fileMode, connectionID, protocol string, uid, gid int, atime, mtime,\n\tsshCommand string, size int64, localAddr, remoteAddr string, elapsed int64) {\n\tlogger.Info().\n\t\tTimestamp().\n\t\tStr(\"sender\", command).\n\t\tStr(\"local_addr\", localAddr).\n\t\tStr(\"remote_addr\", remoteAddr).\n\t\tStr(\"username\", user).\n\t\tStr(\"file_path\", path).\n\t\tStr(\"target_path\", target).\n\t\tStr(\"filemode\", fileMode).\n\t\tInt(\"uid\", uid).\n\t\tInt(\"gid\", gid).\n\t\tStr(\"access_time\", atime).\n\t\tStr(\"modification_time\", mtime).\n\t\tInt64(\"size\", size).\n\t\tInt64(\"elapsed\", elapsed).\n\t\tStr(\"ssh_command\", sshCommand).\n\t\tStr(\"connection_id\", connectionID).\n\t\tStr(\"protocol\", protocol).\n\t\tSend()\n}\n\n// ConnectionFailedLog logs failed attempts to initialize a connection.\n// A connection can fail for an authentication error or other errors such as\n// a client abort or a time out if the login does not happen in two minutes.\n// These logs are useful for better integration with Fail2ban and similar tools.\nfunc ConnectionFailedLog(user, ip, loginType, protocol, errorString string) {\n\tlogger.Debug().\n\t\tTimestamp().\n\t\tStr(\"sender\", \"connection_failed\").\n\t\tStr(\"client_ip\", ip).\n\t\tStr(\"username\", user).\n\t\tStr(\"login_type\", loginType).\n\t\tStr(\"protocol\", protocol).\n\t\tStr(\"error\", errorString).\n\t\tSend()\n}\n\n// LoginLog logs successful logins.\nfunc LoginLog(user, ip, loginMethod, protocol, connectionID, clientVersion string, encrypted bool, info string) {\n\tev := logger.Info()\n\tev.Timestamp().\n\t\tStr(\"sender\", \"login\").\n\t\tStr(\"ip\", ip).\n\t\tStr(\"username\", user).\n\t\tStr(\"method\", loginMethod).\n\t\tStr(\"protocol\", protocol)\n\tif connectionID != \"\" {\n\t\tev.Str(\"connection_id\", connectionID)\n\t}\n\tev.Str(\"client\", clientVersion).\n\t\tBool(\"encrypted\", encrypted)\n\tif info != \"\" {\n\t\tev.Str(\"info\", info)\n\t}\n\tev.Send()\n}\n\nfunc isLogFilePathValid(logFilePath string) bool {\n\tcleanInput := filepath.Clean(logFilePath)\n\tif cleanInput == \".\" || cleanInput == \"..\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// StdLoggerWrapper is a wrapper for standard logger compatibility\ntype StdLoggerWrapper struct {\n\tSender string\n}\n\n// Write implements the io.Writer interface. This is useful to set as a writer\n// for the standard library log.\nfunc (l *StdLoggerWrapper) Write(p []byte) (n int, err error) {\n\tn = len(p)\n\tif n > 0 && p[n-1] == '\\n' {\n\t\t// Trim CR added by stdlog.\n\t\tp = p[0 : n-1]\n\t}\n\n\tLog(LevelError, l.Sender, \"\", \"%s\", p)\n\treturn\n}\n\n// LeveledLogger is a logger that accepts a message string and a variadic number of key-value pairs\ntype LeveledLogger struct {\n\tSender            string\n\tadditionalKeyVals []any\n}\n\nfunc addKeysAndValues(ev *zerolog.Event, keysAndValues ...any) {\n\tkvLen := len(keysAndValues)\n\tif kvLen%2 != 0 {\n\t\textra := keysAndValues[kvLen-1]\n\t\tkeysAndValues = append(keysAndValues[:kvLen-1], \"EXTRA_VALUE_AT_END\", extra)\n\t}\n\tfor i := 0; i < len(keysAndValues); i += 2 {\n\t\tkey, val := keysAndValues[i], keysAndValues[i+1]\n\t\tif keyStr, ok := key.(string); ok && keyStr != \"timestamp\" {\n\t\t\tev.Str(keyStr, fmt.Sprintf(\"%v\", val))\n\t\t}\n\t}\n}\n\n// Error logs at error level for the specified sender\nfunc (l *LeveledLogger) Error(msg string, keysAndValues ...any) {\n\tev := logger.Error()\n\tev.Timestamp().Str(\"sender\", l.Sender)\n\tif len(l.additionalKeyVals) > 0 {\n\t\taddKeysAndValues(ev, l.additionalKeyVals...)\n\t}\n\taddKeysAndValues(ev, keysAndValues...)\n\tev.Msg(msg)\n}\n\n// Info logs at info level for the specified sender\nfunc (l *LeveledLogger) Info(msg string, keysAndValues ...any) {\n\tev := logger.Info()\n\tev.Timestamp().Str(\"sender\", l.Sender)\n\tif len(l.additionalKeyVals) > 0 {\n\t\taddKeysAndValues(ev, l.additionalKeyVals...)\n\t}\n\taddKeysAndValues(ev, keysAndValues...)\n\tev.Msg(msg)\n}\n\n// Debug logs at debug level for the specified sender\nfunc (l *LeveledLogger) Debug(msg string, keysAndValues ...any) {\n\tev := logger.Debug()\n\tev.Timestamp().Str(\"sender\", l.Sender)\n\tif len(l.additionalKeyVals) > 0 {\n\t\taddKeysAndValues(ev, l.additionalKeyVals...)\n\t}\n\taddKeysAndValues(ev, keysAndValues...)\n\tev.Msg(msg)\n}\n\n// Warn logs at warn level for the specified sender\nfunc (l *LeveledLogger) Warn(msg string, keysAndValues ...any) {\n\tev := logger.Warn()\n\tev.Timestamp().Str(\"sender\", l.Sender)\n\tif len(l.additionalKeyVals) > 0 {\n\t\taddKeysAndValues(ev, l.additionalKeyVals...)\n\t}\n\taddKeysAndValues(ev, keysAndValues...)\n\tev.Msg(msg)\n}\n\n// Panic logs the panic at error level for the specified sender\nfunc (l *LeveledLogger) Panic(msg string, keysAndValues ...any) {\n\tl.Error(msg, keysAndValues...)\n}\n"
  },
  {
    "path": "internal/logger/mail.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage logger\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/wneessen/go-mail/log\"\n)\n\nconst (\n\tmailLogSender = \"smtpclient\"\n)\n\n// MailAdapter is an adapter for mail.Logger\ntype MailAdapter struct {\n\tConnectionID string\n}\n\n// Errorf emits a log at Error level\nfunc (l *MailAdapter) Errorf(logMsg log.Log) {\n\tformat := l.getFormatString(&logMsg)\n\tErrorToConsole(format, logMsg.Messages...)\n\tLog(LevelError, mailLogSender, l.ConnectionID, format, logMsg.Messages...)\n}\n\n// Warnf emits a log at Warn level\nfunc (l *MailAdapter) Warnf(logMsg log.Log) {\n\tformat := l.getFormatString(&logMsg)\n\tWarnToConsole(format, logMsg.Messages...)\n\tLog(LevelWarn, mailLogSender, l.ConnectionID, format, logMsg.Messages...)\n}\n\n// Infof emits a log at Info level\nfunc (l *MailAdapter) Infof(logMsg log.Log) {\n\tformat := l.getFormatString(&logMsg)\n\tInfoToConsole(format, logMsg.Messages...)\n\tLog(LevelInfo, mailLogSender, l.ConnectionID, format, logMsg.Messages...)\n}\n\n// Debugf emits a log at Debug level\nfunc (l *MailAdapter) Debugf(logMsg log.Log) {\n\tformat := l.getFormatString(&logMsg)\n\tDebugToConsole(format, logMsg.Messages...)\n\tLog(LevelDebug, mailLogSender, l.ConnectionID, format, logMsg.Messages...)\n}\n\nfunc (*MailAdapter) getFormatString(logMsg *log.Log) string {\n\tp := \"C <-- S:\"\n\tif logMsg.Direction == log.DirClientToServer {\n\t\tp = \"C --> S:\"\n\t}\n\treturn fmt.Sprintf(\"%s %s\", p, logMsg.Format)\n}\n"
  },
  {
    "path": "internal/logger/request_logger.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage logger\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/rs/zerolog\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n)\n\n// StructuredLogger defines a simple wrapper around zerolog logger.\n// It implements chi.middleware.LogFormatter interface\ntype StructuredLogger struct {\n\tLogger *zerolog.Logger\n}\n\n// StructuredLoggerEntry defines a log entry.\n// It implements chi.middleware.LogEntry interface\ntype StructuredLoggerEntry struct {\n\t// The zerolog logger\n\tLogger *zerolog.Logger\n\t// fields to write in the log\n\tfields map[string]any\n}\n\n// NewStructuredLogger returns a chi.middleware.RequestLogger using our StructuredLogger.\n// This structured logger is called by the chi.middleware.Logger handler to log each HTTP request\nfunc NewStructuredLogger(logger *zerolog.Logger) func(next http.Handler) http.Handler {\n\treturn middleware.RequestLogger(&StructuredLogger{logger})\n}\n\n// NewLogEntry creates a new log entry for an HTTP request\nfunc (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {\n\tscheme := \"http\"\n\tcipherSuite := \"\"\n\tif r.TLS != nil {\n\t\tscheme = \"https\"\n\t\tcipherSuite = tls.CipherSuiteName(r.TLS.CipherSuite)\n\t}\n\n\tfields := map[string]any{\n\t\t\"local_addr\":   getLocalAddress(r),\n\t\t\"remote_addr\":  r.RemoteAddr,\n\t\t\"proto\":        r.Proto,\n\t\t\"method\":       r.Method,\n\t\t\"user_agent\":   r.UserAgent(),\n\t\t\"uri\":          fmt.Sprintf(\"%s://%s%s\", scheme, r.Host, r.RequestURI),\n\t\t\"cipher_suite\": cipherSuite,\n\t}\n\n\treqID := middleware.GetReqID(r.Context())\n\tif reqID != \"\" {\n\t\tfields[\"request_id\"] = reqID\n\t}\n\n\treturn &StructuredLoggerEntry{Logger: l.Logger, fields: fields}\n}\n\n// Write logs a new entry at the end of the HTTP request\nfunc (l *StructuredLoggerEntry) Write(status, bytes int, _ http.Header, elapsed time.Duration, _ any) {\n\tmetric.HTTPRequestServed(status)\n\tvar ev *zerolog.Event\n\tif status >= http.StatusInternalServerError {\n\t\tev = l.Logger.Error()\n\t} else if status >= http.StatusBadRequest {\n\t\tev = l.Logger.Warn()\n\t} else {\n\t\tev = l.Logger.Debug()\n\t}\n\tev.\n\t\tTimestamp().\n\t\tStr(\"sender\", \"httpd\").\n\t\tFields(l.fields).\n\t\tInt(\"resp_status\", status).\n\t\tInt(\"resp_size\", bytes).\n\t\tInt64(\"elapsed_ms\", elapsed.Nanoseconds()/1000000).\n\t\tSend()\n}\n\n// Panic logs panics\nfunc (l *StructuredLoggerEntry) Panic(v any, stack []byte) {\n\tl.Logger.Error().\n\t\tTimestamp().\n\t\tStr(\"sender\", \"httpd\").\n\t\tFields(l.fields).\n\t\tStr(\"stack\", string(stack)).\n\t\tStr(\"panic\", fmt.Sprintf(\"%+v\", v)).\n\t\tSend()\n}\n\nfunc getLocalAddress(r *http.Request) string {\n\tif r == nil {\n\t\treturn \"\"\n\t}\n\tlocalAddr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr)\n\tif ok {\n\t\treturn localAddr.String()\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/logger/slog.go",
    "content": "// Copyright (C) 2025 Nicola Murino\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, version 3.\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\npackage logger\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"slices\"\n\n\t\"github.com/rs/zerolog\"\n)\n\n// slogAdapter is an adapter for slog.Handler\ntype slogAdapter struct {\n\tsender string\n\tattrs  []slog.Attr\n}\n\n// NewSlogAdapter creates a slog.Handler adapter\nfunc NewSlogAdapter(sender string, attrs []slog.Attr) *slogAdapter {\n\treturn &slogAdapter{\n\t\tsender: sender,\n\t\tattrs:  attrs,\n\t}\n}\n\nfunc (l *slogAdapter) Enabled(ctx context.Context, level slog.Level) bool {\n\t// Log level is handled by our implementation\n\treturn true\n}\n\nfunc (l *slogAdapter) Handle(ctx context.Context, r slog.Record) error {\n\tvar ev *zerolog.Event\n\tswitch r.Level {\n\tcase slog.LevelDebug:\n\t\tev = logger.Debug()\n\tcase slog.LevelInfo:\n\t\tev = logger.Info()\n\tcase slog.LevelWarn:\n\t\tev = logger.Warn()\n\tcase slog.LevelError:\n\t\tev = logger.Error()\n\tdefault:\n\t\tev = logger.Debug()\n\t}\n\n\tev.Timestamp()\n\tif l.sender != \"\" {\n\t\tev.Str(\"sender\", l.sender)\n\t}\n\n\taddSlogAttr := func(a slog.Attr) {\n\t\tif a.Key == \"time\" {\n\t\t\treturn\n\t\t}\n\t\tev.Any(a.Key, a.Value.Any())\n\t}\n\n\tfor _, a := range l.attrs {\n\t\taddSlogAttr(a)\n\t}\n\n\tr.Attrs(func(a slog.Attr) bool {\n\t\taddSlogAttr(a)\n\t\treturn true\n\t})\n\n\tev.Msg(r.Message)\n\n\treturn nil\n}\n\nfunc (l *slogAdapter) WithAttrs(attrs []slog.Attr) slog.Handler {\n\tnewHandler := *l\n\tnewHandler.attrs = slices.Concat(l.attrs, attrs)\n\treturn &newHandler\n}\n\nfunc (l *slogAdapter) WithGroup(name string) slog.Handler {\n\tnewHandler := *l\n\tif name != \"\" {\n\t\tnewHandler.sender = name\n\t}\n\treturn &newHandler\n}\n"
  },
  {
    "path": "internal/logger/sync_wrapper.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage logger\n\nimport (\n\t\"os\"\n\t\"sync\"\n)\n\ntype logSyncWrapper struct {\n\tsync.Mutex\n\toutput *os.File\n}\n\nfunc (l *logSyncWrapper) Write(b []byte) (n int, err error) {\n\tl.Lock()\n\tdefer l.Unlock()\n\treturn l.output.Write(b)\n}\n"
  },
  {
    "path": "internal/metric/metric.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !nometrics\n\n// Package metric provides Prometheus metrics support\npackage metric\n\nimport (\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\tloginMethodPublicKey            = \"publickey\"\n\tloginMethodKeyboardInteractive  = \"keyboard-interactive\"\n\tloginMethodKeyAndPassword       = \"publickey+password\"\n\tloginMethodKeyAndKeyboardInt    = \"publickey+keyboard-interactive\"\n\tloginMethodTLSCertificate       = \"TLSCertificate\"\n\tloginMethodTLSCertificateAndPwd = \"TLSCertificate+password\"\n\tloginMethodIDP                  = \"IDP\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"+metrics\")\n}\n\nvar (\n\t// dataproviderAvailability is the metric that reports the availability for the configured data provider\n\tdataproviderAvailability = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tName: \"sftpgo_dataprovider_availability\",\n\t\tHelp: \"Availability for the configured data provider, 1 means OK, 0 KO\",\n\t})\n\n\t// activeConnections is the metric that reports the total number of active connections\n\tactiveConnections = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tName: \"sftpgo_active_connections\",\n\t\tHelp: \"Total number of logged in users\",\n\t})\n\n\t// totalUploads is the metric that reports the total number of successful uploads\n\ttotalUploads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_uploads_total\",\n\t\tHelp: \"The total number of successful uploads\",\n\t})\n\n\t// totalDownloads is the metric that reports the total number of successful downloads\n\ttotalDownloads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_downloads_total\",\n\t\tHelp: \"The total number of successful downloads\",\n\t})\n\n\t// totalUploadErrors is the metric that reports the total number of upload errors\n\ttotalUploadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_upload_errors_total\",\n\t\tHelp: \"The total number of upload errors\",\n\t})\n\n\t// totalDownloadErrors is the metric that reports the total number of download errors\n\ttotalDownloadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_download_errors_total\",\n\t\tHelp: \"The total number of download errors\",\n\t})\n\n\t// totalUploadSize is the metric that reports the total uploads size as bytes\n\ttotalUploadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_upload_size\",\n\t\tHelp: \"The total upload size as bytes, partial uploads are included\",\n\t})\n\n\t// totalDownloadSize is the metric that reports the total downloads size as bytes\n\ttotalDownloadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_download_size\",\n\t\tHelp: \"The total download size as bytes, partial downloads are included\",\n\t})\n\n\t// totalSSHCommands is the metric that reports the total number of executed SSH commands\n\ttotalSSHCommands = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_ssh_commands_total\",\n\t\tHelp: \"The total number of executed SSH commands\",\n\t})\n\n\t// totalSSHCommandErrors is the metric that reports the total number of SSH command errors\n\ttotalSSHCommandErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_ssh_command_errors_total\",\n\t\tHelp: \"The total number of SSH command errors\",\n\t})\n\n\t// totalLoginAttempts is the metric that reports the total number of login attempts\n\ttotalLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts\",\n\t})\n\n\t// totalNoAuthTried is te metric that reports the total number of clients disconnected\n\t// for inactivity before trying to login\n\ttotalNoAuthTried = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_no_auth_total\",\n\t\tHelp: \"The total number of clients disconnected for inactivity before trying to login\",\n\t})\n\n\t// totalLoginOK is the metric that reports the total number of successful logins\n\ttotalLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_login_ok_total\",\n\t\tHelp: \"The total number of successful logins\",\n\t})\n\n\t// totalLoginFailed is the metric that reports the total number of failed logins\n\ttotalLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_login_ko_total\",\n\t\tHelp: \"The total number of failed logins\",\n\t})\n\n\t// totalPasswordLoginAttempts is the metric that reports the total number of login attempts\n\t// using a password\n\ttotalPasswordLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_password_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts using a password\",\n\t})\n\n\t// totalPasswordLoginOK is the metric that reports the total number of successful logins\n\t// using a password\n\ttotalPasswordLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_password_login_ok_total\",\n\t\tHelp: \"The total number of successful logins using a password\",\n\t})\n\n\t// totalPasswordLoginFailed is the metric that reports the total number of failed logins\n\t// using a password\n\ttotalPasswordLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_password_login_ko_total\",\n\t\tHelp: \"The total number of failed logins using a password\",\n\t})\n\n\t// totalKeyLoginAttempts is the metric that reports the total number of login attempts\n\t// using a public key\n\ttotalKeyLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_public_key_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts using a public key\",\n\t})\n\n\t// totalKeyLoginOK is the metric that reports the total number of successful logins\n\t// using a public key\n\ttotalKeyLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_public_key_login_ok_total\",\n\t\tHelp: \"The total number of successful logins using a public key\",\n\t})\n\n\t// totalKeyLoginFailed is the metric that reports the total number of failed logins\n\t// using a public key\n\ttotalKeyLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_public_key_login_ko_total\",\n\t\tHelp: \"The total number of failed logins using a public key\",\n\t})\n\n\t// totalTLSCertLoginAttempts is the metric that reports the total number of login attempts\n\t// using a TLS certificate\n\ttotalTLSCertLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_tls_cert_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts using a TLS certificate\",\n\t})\n\n\t// totalTLSCertLoginOK is the metric that reports the total number of successful logins\n\t// using a TLS certificate\n\ttotalTLSCertLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_tls_cert_login_ok_total\",\n\t\tHelp: \"The total number of successful logins using a TLS certificate\",\n\t})\n\n\t// totalTLSCertLoginFailed is the metric that reports the total number of failed logins\n\t// using a TLS certificate\n\ttotalTLSCertLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_tls_cert_login_ko_total\",\n\t\tHelp: \"The total number of failed logins using a TLS certificate\",\n\t})\n\n\t// totalTLSCertAndPwdLoginAttempts is the metric that reports the total number of login attempts\n\t// using a TLS certificate+password\n\ttotalTLSCertAndPwdLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_tls_cert_and_pwd_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts using a TLS certificate+password\",\n\t})\n\n\t// totalTLSCertLoginOK is the metric that reports the total number of successful logins\n\t// using a TLS certificate+password\n\ttotalTLSCertAndPwdLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_tls_cert_and_pwd_login_ok_total\",\n\t\tHelp: \"The total number of successful logins using a TLS certificate+password\",\n\t})\n\n\t// totalTLSCertAndPwdLoginFailed is the metric that reports the total number of failed logins\n\t// using a TLS certificate+password\n\ttotalTLSCertAndPwdLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_tls_cert_and_pwd_login_ko_total\",\n\t\tHelp: \"The total number of failed logins using a TLS certificate+password\",\n\t})\n\n\t// totalInteractiveLoginAttempts is the metric that reports the total number of login attempts\n\t// using keyboard interactive authentication\n\ttotalInteractiveLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_keyboard_interactive_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts using keyboard interactive authentication\",\n\t})\n\n\t// totalInteractiveLoginOK is the metric that reports the total number of successful logins\n\t// using keyboard interactive authentication\n\ttotalInteractiveLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_keyboard_interactive_login_ok_total\",\n\t\tHelp: \"The total number of successful logins using keyboard interactive authentication\",\n\t})\n\n\t// totalInteractiveLoginFailed is the metric that reports the total number of failed logins\n\t// using keyboard interactive authentication\n\ttotalInteractiveLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_keyboard_interactive_login_ko_total\",\n\t\tHelp: \"The total number of failed logins using keyboard interactive authentication\",\n\t})\n\n\t// totalKeyAndPasswordLoginAttempts is the metric that reports the total number of\n\t// login attempts using public key + password multi steps auth\n\ttotalKeyAndPasswordLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_key_and_password_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts using public key + password\",\n\t})\n\n\t// totalKeyAndPasswordLoginOK is the metric that reports the total number of\n\t// successful logins using public key + password multi steps auth\n\ttotalKeyAndPasswordLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_key_and_password_login_ok_total\",\n\t\tHelp: \"The total number of successful logins using public key + password\",\n\t})\n\n\t// totalKeyAndPasswordLoginFailed is the metric that reports the total number of\n\t// failed logins using public key + password multi steps auth\n\ttotalKeyAndPasswordLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_key_and_password_login_ko_total\",\n\t\tHelp: \"The total number of failed logins using  public key + password\",\n\t})\n\n\t// totalKeyAndKeyIntLoginAttempts is the metric that reports the total number of\n\t// login attempts using public key + keyboard interactive multi steps auth\n\ttotalKeyAndKeyIntLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_key_and_keyboard_int_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts using public key + keyboard interactive\",\n\t})\n\n\t// totalKeyAndKeyIntLoginOK is the metric that reports the total number of\n\t// successful logins using public key + keyboard interactive multi steps auth\n\ttotalKeyAndKeyIntLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_key_and_keyboard_int_login_ok_total\",\n\t\tHelp: \"The total number of successful logins using public key + keyboard interactive\",\n\t})\n\n\t// totalKeyAndKeyIntLoginFailed is the metric that reports the total number of\n\t// failed logins using public key + keyboard interactive multi steps auth\n\ttotalKeyAndKeyIntLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_key_and_keyboard_int_login_ko_total\",\n\t\tHelp: \"The total number of failed logins using  public key + keyboard interactive\",\n\t})\n\n\t// totalIDPLoginAttempts is the metric that reports the total number of\n\t// login attempts using identity providers\n\ttotalIDPLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_idp_login_attempts_total\",\n\t\tHelp: \"The total number of login attempts using Identity Providers\",\n\t})\n\n\t// totalIDPLoginOK is the metric that reports the total number of\n\t// successful logins using identity providers\n\ttotalIDPLoginOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_idp_login_ok_total\",\n\t\tHelp: \"The total number of successful logins using Identity Providers\",\n\t})\n\n\t// totalIDPLoginFailed is the metric that reports the total number of\n\t// failed logins using identity providers\n\ttotalIDPLoginFailed = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_idp_login_ko_total\",\n\t\tHelp: \"The total number of failed logins using  Identity Providers\",\n\t})\n\n\ttotalHTTPRequests = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_http_req_total\",\n\t\tHelp: \"The total number of HTTP requests served\",\n\t})\n\n\ttotalHTTPOK = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_http_req_ok_total\",\n\t\tHelp: \"The total number of HTTP requests served with 2xx status code\",\n\t})\n\n\ttotalHTTPClientErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_http_client_errors_total\",\n\t\tHelp: \"The total number of HTTP requests served with 4xx status code\",\n\t})\n\n\ttotalHTTPServerErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_http_server_errors_total\",\n\t\tHelp: \"The total number of HTTP requests served with 5xx status code\",\n\t})\n\n\t// totalS3Uploads is the metric that reports the total number of successful S3 uploads\n\ttotalS3Uploads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_uploads_total\",\n\t\tHelp: \"The total number of successful S3 uploads\",\n\t})\n\n\t// totalS3Downloads is the metric that reports the total number of successful S3 downloads\n\ttotalS3Downloads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_downloads_total\",\n\t\tHelp: \"The total number of successful S3 downloads\",\n\t})\n\n\t// totalS3UploadErrors is the metric that reports the total number of S3 upload errors\n\ttotalS3UploadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_upload_errors_total\",\n\t\tHelp: \"The total number of S3 upload errors\",\n\t})\n\n\t// totalS3DownloadErrors is the metric that reports the total number of S3 download errors\n\ttotalS3DownloadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_download_errors_total\",\n\t\tHelp: \"The total number of S3 download errors\",\n\t})\n\n\t// totalS3UploadSize is the metric that reports the total S3 uploads size as bytes\n\ttotalS3UploadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_upload_size\",\n\t\tHelp: \"The total S3 upload size as bytes, partial uploads are included\",\n\t})\n\n\t// totalS3DownloadSize is the metric that reports the total S3 downloads size as bytes\n\ttotalS3DownloadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_download_size\",\n\t\tHelp: \"The total S3 download size as bytes, partial downloads are included\",\n\t})\n\n\t// totalS3ListObjects is the metric that reports the total successful S3 list objects requests\n\ttotalS3ListObjects = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_list_objects\",\n\t\tHelp: \"The total number of successful S3 list objects requests\",\n\t})\n\n\t// totalS3CopyObject is the metric that reports the total successful S3 copy object requests\n\ttotalS3CopyObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_copy_object\",\n\t\tHelp: \"The total number of successful S3 copy object requests\",\n\t})\n\n\t// totalS3DeleteObject is the metric that reports the total successful S3 delete object requests\n\ttotalS3DeleteObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_delete_object\",\n\t\tHelp: \"The total number of successful S3 delete object requests\",\n\t})\n\n\t// totalS3ListObjectsError is the metric that reports the total S3 list objects errors\n\ttotalS3ListObjectsErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_list_objects_errors\",\n\t\tHelp: \"The total number of S3 list objects errors\",\n\t})\n\n\t// totalS3CopyObjectErrors is the metric that reports the total S3 copy object errors\n\ttotalS3CopyObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_copy_object_errors\",\n\t\tHelp: \"The total number of S3 copy object errors\",\n\t})\n\n\t// totalS3DeleteObjectErrors is the metric that reports the total S3 delete object errors\n\ttotalS3DeleteObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_delete_object_errors\",\n\t\tHelp: \"The total number of S3 delete object errors\",\n\t})\n\n\t// totalS3HeadObject is the metric that reports the total successful S3 head object requests\n\ttotalS3HeadObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_head_object\",\n\t\tHelp: \"The total number of successful S3 head object requests\",\n\t})\n\n\t// totalS3HeadObjectErrors is the metric that reports the total S3 head object errors\n\ttotalS3HeadObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_s3_head_object_errors\",\n\t\tHelp: \"The total number of S3 head object errors\",\n\t})\n\n\t// totalGCSUploads is the metric that reports the total number of successful GCS uploads\n\ttotalGCSUploads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_uploads_total\",\n\t\tHelp: \"The total number of successful GCS uploads\",\n\t})\n\n\t// totalGCSDownloads is the metric that reports the total number of successful GCS downloads\n\ttotalGCSDownloads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_downloads_total\",\n\t\tHelp: \"The total number of successful GCS downloads\",\n\t})\n\n\t// totalGCSUploadErrors is the metric that reports the total number of GCS upload errors\n\ttotalGCSUploadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_upload_errors_total\",\n\t\tHelp: \"The total number of GCS upload errors\",\n\t})\n\n\t// totalGCSDownloadErrors is the metric that reports the total number of GCS download errors\n\ttotalGCSDownloadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_download_errors_total\",\n\t\tHelp: \"The total number of GCS download errors\",\n\t})\n\n\t// totalGCSUploadSize is the metric that reports the total GCS uploads size as bytes\n\ttotalGCSUploadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_upload_size\",\n\t\tHelp: \"The total GCS upload size as bytes, partial uploads are included\",\n\t})\n\n\t// totalGCSDownloadSize is the metric that reports the total GCS downloads size as bytes\n\ttotalGCSDownloadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_download_size\",\n\t\tHelp: \"The total GCS download size as bytes, partial downloads are included\",\n\t})\n\n\t// totalGCSListObjects is the metric that reports the total successful GCS list objects requests\n\ttotalGCSListObjects = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_list_objects\",\n\t\tHelp: \"The total number of successful GCS list objects requests\",\n\t})\n\n\t// totalGCSCopyObject is the metric that reports the total successful GCS copy object requests\n\ttotalGCSCopyObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_copy_object\",\n\t\tHelp: \"The total number of successful GCS copy object requests\",\n\t})\n\n\t// totalGCSDeleteObject is the metric that reports the total successful GCS delete object requests\n\ttotalGCSDeleteObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_delete_object\",\n\t\tHelp: \"The total number of successful GCS delete object requests\",\n\t})\n\n\t// totalGCSListObjectsError is the metric that reports the total GCS list objects errors\n\ttotalGCSListObjectsErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_list_objects_errors\",\n\t\tHelp: \"The total number of GCS list objects errors\",\n\t})\n\n\t// totalGCSCopyObjectErrors is the metric that reports the total GCS copy object errors\n\ttotalGCSCopyObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_copy_object_errors\",\n\t\tHelp: \"The total number of GCS copy object errors\",\n\t})\n\n\t// totalGCSDeleteObjectErrors is the metric that reports the total GCS delete object errors\n\ttotalGCSDeleteObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_delete_object_errors\",\n\t\tHelp: \"The total number of GCS delete object errors\",\n\t})\n\n\t// totalGCSHeadObject is the metric that reports the total successful GCS head object requests\n\ttotalGCSHeadObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_head_object\",\n\t\tHelp: \"The total number of successful GCS head object requests\",\n\t})\n\n\t// totalGCSHeadObjectErrors is the metric that reports the total GCS head object errors\n\ttotalGCSHeadObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_gcs_head_object_errors\",\n\t\tHelp: \"The total number of GCS head object errors\",\n\t})\n\n\t// totalAZUploads is the metric that reports the total number of successful Azure uploads\n\ttotalAZUploads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_uploads_total\",\n\t\tHelp: \"The total number of successful Azure uploads\",\n\t})\n\n\t// totalAZDownloads is the metric that reports the total number of successful Azure downloads\n\ttotalAZDownloads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_downloads_total\",\n\t\tHelp: \"The total number of successful Azure downloads\",\n\t})\n\n\t// totalAZUploadErrors is the metric that reports the total number of Azure upload errors\n\ttotalAZUploadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_upload_errors_total\",\n\t\tHelp: \"The total number of Azure upload errors\",\n\t})\n\n\t// totalAZDownloadErrors is the metric that reports the total number of Azure download errors\n\ttotalAZDownloadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_download_errors_total\",\n\t\tHelp: \"The total number of Azure download errors\",\n\t})\n\n\t// totalAZUploadSize is the metric that reports the total Azure uploads size as bytes\n\ttotalAZUploadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_upload_size\",\n\t\tHelp: \"The total Azure upload size as bytes, partial uploads are included\",\n\t})\n\n\t// totalAZDownloadSize is the metric that reports the total Azure downloads size as bytes\n\ttotalAZDownloadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_download_size\",\n\t\tHelp: \"The total Azure download size as bytes, partial downloads are included\",\n\t})\n\n\t// totalAZListObjects is the metric that reports the total successful Azure list objects requests\n\ttotalAZListObjects = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_list_objects\",\n\t\tHelp: \"The total number of successful Azure list objects requests\",\n\t})\n\n\t// totalAZCopyObject is the metric that reports the total successful Azure copy object requests\n\ttotalAZCopyObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_copy_object\",\n\t\tHelp: \"The total number of successful Azure copy object requests\",\n\t})\n\n\t// totalAZDeleteObject is the metric that reports the total successful Azure delete object requests\n\ttotalAZDeleteObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_delete_object\",\n\t\tHelp: \"The total number of successful Azure delete object requests\",\n\t})\n\n\t// totalAZListObjectsError is the metric that reports the total Azure list objects errors\n\ttotalAZListObjectsErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_list_objects_errors\",\n\t\tHelp: \"The total number of Azure list objects errors\",\n\t})\n\n\t// totalAZCopyObjectErrors is the metric that reports the total Azure copy object errors\n\ttotalAZCopyObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_copy_object_errors\",\n\t\tHelp: \"The total number of Azure copy object errors\",\n\t})\n\n\t// totalAZDeleteObjectErrors is the metric that reports the total Azure delete object errors\n\ttotalAZDeleteObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_delete_object_errors\",\n\t\tHelp: \"The total number of Azure delete object errors\",\n\t})\n\n\t// totalAZHeadObject is the metric that reports the total successful Azure head object requests\n\ttotalAZHeadObject = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_head_object\",\n\t\tHelp: \"The total number of successful Azure head object requests\",\n\t})\n\n\t// totalAZHeadObjectErrors is the metric that reports the total Azure head object errors\n\ttotalAZHeadObjectErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_az_head_object_errors\",\n\t\tHelp: \"The total number of Azure head object errors\",\n\t})\n\n\t// totalSFTPFsUploads is the metric that reports the total number of successful SFTPFs uploads\n\ttotalSFTPFsUploads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_sftpfs_uploads_total\",\n\t\tHelp: \"The total number of successful SFTPFs uploads\",\n\t})\n\n\t// totalSFTPFsDownloads is the metric that reports the total number of successful SFTPFs downloads\n\ttotalSFTPFsDownloads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_sftpfs_downloads_total\",\n\t\tHelp: \"The total number of successful SFTPFs downloads\",\n\t})\n\n\t// totalSFTPFsUploadErrors is the metric that reports the total number of SFTPFs upload errors\n\ttotalSFTPFsUploadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_sftpfs_upload_errors_total\",\n\t\tHelp: \"The total number of SFTPFs upload errors\",\n\t})\n\n\t// totalSFTPFsDownloadErrors is the metric that reports the total number of SFTPFs download errors\n\ttotalSFTPFsDownloadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_sftpfs_download_errors_total\",\n\t\tHelp: \"The total number of SFTPFs download errors\",\n\t})\n\n\t// totalSFTPFsUploadSize is the metric that reports the total SFTPFs uploads size as bytes\n\ttotalSFTPFsUploadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_sftpfs_upload_size\",\n\t\tHelp: \"The total SFTPFs upload size as bytes, partial uploads are included\",\n\t})\n\n\t// totalSFTPFsDownloadSize is the metric that reports the total SFTPFs downloads size as bytes\n\ttotalSFTPFsDownloadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_sftpfs_download_size\",\n\t\tHelp: \"The total SFTPFs download size as bytes, partial downloads are included\",\n\t})\n\n\t// totalHTTPFsUploads is the metric that reports the total number of successful HTTPFs uploads\n\ttotalHTTPFsUploads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_httpfs_uploads_total\",\n\t\tHelp: \"The total number of successful HTTPFs uploads\",\n\t})\n\n\t// totalHTTPFsDownloads is the metric that reports the total number of successful HTTPFs downloads\n\ttotalHTTPFsDownloads = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_httpfs_downloads_total\",\n\t\tHelp: \"The total number of successful HTTPFs downloads\",\n\t})\n\n\t// totalHTTPFsUploadErrors is the metric that reports the total number of HTTPFs upload errors\n\ttotalHTTPFsUploadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_httpfs_upload_errors_total\",\n\t\tHelp: \"The total number of HTTPFs upload errors\",\n\t})\n\n\t// totalHTTPFsDownloadErrors is the metric that reports the total number of HTTPFs download errors\n\ttotalHTTPFsDownloadErrors = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_httpfs_download_errors_total\",\n\t\tHelp: \"The total number of HTTPFs download errors\",\n\t})\n\n\t// totalHTTPFsUploadSize is the metric that reports the total HTTPFs uploads size as bytes\n\ttotalHTTPFsUploadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_httpfs_upload_size\",\n\t\tHelp: \"The total HTTPFs upload size as bytes, partial uploads are included\",\n\t})\n\n\t// totalHTTPFsDownloadSize is the metric that reports the total HTTPFs downloads size as bytes\n\ttotalHTTPFsDownloadSize = promauto.NewCounter(prometheus.CounterOpts{\n\t\tName: \"sftpgo_httpfs_download_size\",\n\t\tHelp: \"The total HTTPFs download size as bytes, partial downloads are included\",\n\t})\n)\n\n// AddMetricsEndpoint publishes metrics to the specified endpoint\nfunc AddMetricsEndpoint(metricsPath string, handler chi.Router) {\n\thandler.Handle(metricsPath, promhttp.Handler())\n}\n\n// TransferCompleted updates metrics after an upload or a download\nfunc TransferCompleted(bytesSent, bytesReceived int64, transferKind int, err error, isSFTPFs bool) {\n\tif transferKind == 0 {\n\t\t// upload\n\t\tif err == nil {\n\t\t\ttotalUploads.Inc()\n\t\t} else {\n\t\t\ttotalUploadErrors.Inc()\n\t\t}\n\t} else {\n\t\t// download\n\t\tif err == nil {\n\t\t\ttotalDownloads.Inc()\n\t\t} else {\n\t\t\ttotalDownloadErrors.Inc()\n\t\t}\n\t}\n\tif bytesReceived > 0 {\n\t\ttotalUploadSize.Add(float64(bytesReceived))\n\t}\n\tif bytesSent > 0 {\n\t\ttotalDownloadSize.Add(float64(bytesSent))\n\t}\n\tif isSFTPFs {\n\t\tsftpFsTransferCompleted(bytesSent, bytesReceived, transferKind, err)\n\t}\n}\n\n// S3TransferCompleted updates metrics after an S3 upload or a download\nfunc S3TransferCompleted(bytes int64, transferKind int, err error) {\n\tif transferKind == 0 {\n\t\t// upload\n\t\tif err == nil {\n\t\t\ttotalS3Uploads.Inc()\n\t\t} else {\n\t\t\ttotalS3UploadErrors.Inc()\n\t\t}\n\t\ttotalS3UploadSize.Add(float64(bytes))\n\t} else {\n\t\t// download\n\t\tif err == nil {\n\t\t\ttotalS3Downloads.Inc()\n\t\t} else {\n\t\t\ttotalS3DownloadErrors.Inc()\n\t\t}\n\t\ttotalS3DownloadSize.Add(float64(bytes))\n\t}\n}\n\n// S3ListObjectsCompleted updates metrics after an S3 list objects request terminates\nfunc S3ListObjectsCompleted(err error) {\n\tif err == nil {\n\t\ttotalS3ListObjects.Inc()\n\t} else {\n\t\ttotalS3ListObjectsErrors.Inc()\n\t}\n}\n\n// S3CopyObjectCompleted updates metrics after an S3 copy object request terminates\nfunc S3CopyObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalS3CopyObject.Inc()\n\t} else {\n\t\ttotalS3CopyObjectErrors.Inc()\n\t}\n}\n\n// S3DeleteObjectCompleted updates metrics after an S3 delete object request terminates\nfunc S3DeleteObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalS3DeleteObject.Inc()\n\t} else {\n\t\ttotalS3DeleteObjectErrors.Inc()\n\t}\n}\n\n// S3HeadObjectCompleted updates metrics after a S3 head object request terminates\nfunc S3HeadObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalS3HeadObject.Inc()\n\t} else {\n\t\ttotalS3HeadObjectErrors.Inc()\n\t}\n}\n\n// GCSTransferCompleted updates metrics after a GCS upload or a download\nfunc GCSTransferCompleted(bytes int64, transferKind int, err error) {\n\tif transferKind == 0 {\n\t\t// upload\n\t\tif err == nil {\n\t\t\ttotalGCSUploads.Inc()\n\t\t} else {\n\t\t\ttotalGCSUploadErrors.Inc()\n\t\t}\n\t\ttotalGCSUploadSize.Add(float64(bytes))\n\t} else {\n\t\t// download\n\t\tif err == nil {\n\t\t\ttotalGCSDownloads.Inc()\n\t\t} else {\n\t\t\ttotalGCSDownloadErrors.Inc()\n\t\t}\n\t\ttotalGCSDownloadSize.Add(float64(bytes))\n\t}\n}\n\n// GCSListObjectsCompleted updates metrics after a GCS list objects request terminates\nfunc GCSListObjectsCompleted(err error) {\n\tif err == nil {\n\t\ttotalGCSListObjects.Inc()\n\t} else {\n\t\ttotalGCSListObjectsErrors.Inc()\n\t}\n}\n\n// GCSCopyObjectCompleted updates metrics after a GCS copy object request terminates\nfunc GCSCopyObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalGCSCopyObject.Inc()\n\t} else {\n\t\ttotalGCSCopyObjectErrors.Inc()\n\t}\n}\n\n// GCSDeleteObjectCompleted updates metrics after a GCS delete object request terminates\nfunc GCSDeleteObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalGCSDeleteObject.Inc()\n\t} else {\n\t\ttotalGCSDeleteObjectErrors.Inc()\n\t}\n}\n\n// GCSHeadObjectCompleted updates metrics after a GCS head object request terminates\nfunc GCSHeadObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalGCSHeadObject.Inc()\n\t} else {\n\t\ttotalGCSHeadObjectErrors.Inc()\n\t}\n}\n\n// AZTransferCompleted updates metrics after a Azure upload or a download\nfunc AZTransferCompleted(bytes int64, transferKind int, err error) {\n\tif transferKind == 0 {\n\t\t// upload\n\t\tif err == nil {\n\t\t\ttotalAZUploads.Inc()\n\t\t} else {\n\t\t\ttotalAZUploadErrors.Inc()\n\t\t}\n\t\ttotalAZUploadSize.Add(float64(bytes))\n\t} else {\n\t\t// download\n\t\tif err == nil {\n\t\t\ttotalAZDownloads.Inc()\n\t\t} else {\n\t\t\ttotalAZDownloadErrors.Inc()\n\t\t}\n\t\ttotalAZDownloadSize.Add(float64(bytes))\n\t}\n}\n\n// AZListObjectsCompleted updates metrics after a Azure list objects request terminates\nfunc AZListObjectsCompleted(err error) {\n\tif err == nil {\n\t\ttotalAZListObjects.Inc()\n\t} else {\n\t\ttotalAZListObjectsErrors.Inc()\n\t}\n}\n\n// AZCopyObjectCompleted updates metrics after a Azure copy object request terminates\nfunc AZCopyObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalAZCopyObject.Inc()\n\t} else {\n\t\ttotalAZCopyObjectErrors.Inc()\n\t}\n}\n\n// AZDeleteObjectCompleted updates metrics after a Azure delete object request terminates\nfunc AZDeleteObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalAZDeleteObject.Inc()\n\t} else {\n\t\ttotalAZDeleteObjectErrors.Inc()\n\t}\n}\n\n// AZHeadObjectCompleted updates metrics after a Azure head object request terminates\nfunc AZHeadObjectCompleted(err error) {\n\tif err == nil {\n\t\ttotalAZHeadObject.Inc()\n\t} else {\n\t\ttotalAZHeadObjectErrors.Inc()\n\t}\n}\n\n// sftpFsTransferCompleted updates metrics after an SFTPFs upload or a download\nfunc sftpFsTransferCompleted(bytesSent, bytesReceived int64, transferKind int, err error) {\n\tif transferKind == 0 {\n\t\t// upload\n\t\tif err == nil {\n\t\t\ttotalSFTPFsUploads.Inc()\n\t\t} else {\n\t\t\ttotalSFTPFsUploadErrors.Inc()\n\t\t}\n\t} else {\n\t\t// download\n\t\tif err == nil {\n\t\t\ttotalSFTPFsDownloads.Inc()\n\t\t} else {\n\t\t\ttotalSFTPFsDownloadErrors.Inc()\n\t\t}\n\t}\n\tif bytesReceived > 0 {\n\t\ttotalSFTPFsUploadSize.Add(float64(bytesReceived))\n\t}\n\tif bytesSent > 0 {\n\t\ttotalSFTPFsDownloadSize.Add(float64(bytesSent))\n\t}\n}\n\n// HTTPFsTransferCompleted updates metrics after an HTTPFs upload or a download\nfunc HTTPFsTransferCompleted(bytes int64, transferKind int, err error) {\n\tif transferKind == 0 {\n\t\t// upload\n\t\tif err == nil {\n\t\t\ttotalHTTPFsUploads.Inc()\n\t\t} else {\n\t\t\ttotalHTTPFsUploadErrors.Inc()\n\t\t}\n\t\ttotalHTTPFsUploadSize.Add(float64(bytes))\n\t} else {\n\t\t// download\n\t\tif err == nil {\n\t\t\ttotalHTTPFsDownloads.Inc()\n\t\t} else {\n\t\t\ttotalHTTPFsDownloadErrors.Inc()\n\t\t}\n\t\ttotalHTTPFsDownloadSize.Add(float64(bytes))\n\t}\n}\n\n// SSHCommandCompleted update metrics after an SSH command terminates\nfunc SSHCommandCompleted(err error) {\n\tif err == nil {\n\t\ttotalSSHCommands.Inc()\n\t} else {\n\t\ttotalSSHCommandErrors.Inc()\n\t}\n}\n\n// UpdateDataProviderAvailability updates the metric for the data provider availability\nfunc UpdateDataProviderAvailability(err error) {\n\tif err == nil {\n\t\tdataproviderAvailability.Set(1)\n\t} else {\n\t\tdataproviderAvailability.Set(0)\n\t}\n}\n\n// AddLoginAttempt increments the metrics for login attempts\nfunc AddLoginAttempt(authMethod string) {\n\ttotalLoginAttempts.Inc()\n\tswitch authMethod {\n\tcase loginMethodPublicKey:\n\t\ttotalKeyLoginAttempts.Inc()\n\tcase loginMethodKeyboardInteractive:\n\t\ttotalInteractiveLoginAttempts.Inc()\n\tcase loginMethodKeyAndPassword:\n\t\ttotalKeyAndPasswordLoginAttempts.Inc()\n\tcase loginMethodKeyAndKeyboardInt:\n\t\ttotalKeyAndKeyIntLoginAttempts.Inc()\n\tcase loginMethodTLSCertificate:\n\t\ttotalTLSCertLoginAttempts.Inc()\n\tcase loginMethodTLSCertificateAndPwd:\n\t\ttotalTLSCertAndPwdLoginAttempts.Inc()\n\tcase loginMethodIDP:\n\t\ttotalIDPLoginAttempts.Inc()\n\tdefault:\n\t\ttotalPasswordLoginAttempts.Inc()\n\t}\n}\n\nfunc incLoginOK(authMethod string) {\n\ttotalLoginOK.Inc()\n\tswitch authMethod {\n\tcase loginMethodPublicKey:\n\t\ttotalKeyLoginOK.Inc()\n\tcase loginMethodKeyboardInteractive:\n\t\ttotalInteractiveLoginOK.Inc()\n\tcase loginMethodKeyAndPassword:\n\t\ttotalKeyAndPasswordLoginOK.Inc()\n\tcase loginMethodKeyAndKeyboardInt:\n\t\ttotalKeyAndKeyIntLoginOK.Inc()\n\tcase loginMethodTLSCertificate:\n\t\ttotalTLSCertLoginOK.Inc()\n\tcase loginMethodTLSCertificateAndPwd:\n\t\ttotalTLSCertAndPwdLoginOK.Inc()\n\tcase loginMethodIDP:\n\t\ttotalIDPLoginOK.Inc()\n\tdefault:\n\t\ttotalPasswordLoginOK.Inc()\n\t}\n}\n\nfunc incLoginFailed(authMethod string) {\n\ttotalLoginFailed.Inc()\n\tswitch authMethod {\n\tcase loginMethodPublicKey:\n\t\ttotalKeyLoginFailed.Inc()\n\tcase loginMethodKeyboardInteractive:\n\t\ttotalInteractiveLoginFailed.Inc()\n\tcase loginMethodKeyAndPassword:\n\t\ttotalKeyAndPasswordLoginFailed.Inc()\n\tcase loginMethodKeyAndKeyboardInt:\n\t\ttotalKeyAndKeyIntLoginFailed.Inc()\n\tcase loginMethodTLSCertificate:\n\t\ttotalTLSCertLoginFailed.Inc()\n\tcase loginMethodTLSCertificateAndPwd:\n\t\ttotalTLSCertAndPwdLoginFailed.Inc()\n\tcase loginMethodIDP:\n\t\ttotalIDPLoginFailed.Inc()\n\tdefault:\n\t\ttotalPasswordLoginFailed.Inc()\n\t}\n}\n\n// AddLoginResult increments the metrics for login results\nfunc AddLoginResult(authMethod string, err error) {\n\tif err == nil {\n\t\tincLoginOK(authMethod)\n\t} else {\n\t\tincLoginFailed(authMethod)\n\t}\n}\n\n// AddNoAuthTried increments the metric for clients disconnected\n// for inactivity before trying to login\nfunc AddNoAuthTried() {\n\ttotalNoAuthTried.Inc()\n}\n\n// HTTPRequestServed increments the metrics for HTTP requests\nfunc HTTPRequestServed(status int) {\n\ttotalHTTPRequests.Inc()\n\tif status >= 200 && status < 300 {\n\t\ttotalHTTPOK.Inc()\n\t} else if status >= 400 && status < 500 {\n\t\ttotalHTTPClientErrors.Inc()\n\t} else if status >= 500 {\n\t\ttotalHTTPServerErrors.Inc()\n\t}\n}\n\n// UpdateActiveConnectionsSize sets the metric for active connections\nfunc UpdateActiveConnectionsSize(size int) {\n\tactiveConnections.Set(float64(size))\n}\n"
  },
  {
    "path": "internal/metric/metric_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build nometrics\n\npackage metric\n\nimport (\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-metrics\")\n}\n\n// AddMetricsEndpoint publishes metrics to the specified endpoint\nfunc AddMetricsEndpoint(_ string, _ chi.Router) {}\n\n// TransferCompleted updates metrics after an upload or a download\nfunc TransferCompleted(_, _ int64, _ int, _ error, _ bool) {}\n\n// S3TransferCompleted updates metrics after an S3 upload or a download\nfunc S3TransferCompleted(_ int64, _ int, _ error) {}\n\n// S3ListObjectsCompleted updates metrics after an S3 list objects request terminates\nfunc S3ListObjectsCompleted(_ error) {}\n\n// S3CopyObjectCompleted updates metrics after an S3 copy object request terminates\nfunc S3CopyObjectCompleted(_ error) {}\n\n// S3DeleteObjectCompleted updates metrics after an S3 delete object request terminates\nfunc S3DeleteObjectCompleted(_ error) {}\n\n// S3HeadBucketCompleted updates metrics after an S3 head bucket request terminates\nfunc S3HeadBucketCompleted(_ error) {}\n\n// GCSTransferCompleted updates metrics after a GCS upload or a download\nfunc GCSTransferCompleted(_ int64, _ int, _ error) {}\n\n// GCSListObjectsCompleted updates metrics after a GCS list objects request terminates\nfunc GCSListObjectsCompleted(_ error) {}\n\n// GCSCopyObjectCompleted updates metrics after a GCS copy object request terminates\nfunc GCSCopyObjectCompleted(_ error) {}\n\n// GCSDeleteObjectCompleted updates metrics after a GCS delete object request terminates\nfunc GCSDeleteObjectCompleted(_ error) {}\n\n// GCSHeadBucketCompleted updates metrics after a GCS head bucket request terminates\nfunc GCSHeadBucketCompleted(_ error) {}\n\n// HTTPFsTransferCompleted updates metrics after an HTTPFs upload or a download\nfunc HTTPFsTransferCompleted(_ int64, _ int, _ error) {}\n\n// SSHCommandCompleted update metrics after an SSH command terminates\nfunc SSHCommandCompleted(_ error) {}\n\n// UpdateDataProviderAvailability updates the metric for the data provider availability\nfunc UpdateDataProviderAvailability(_ error) {}\n\n// AddLoginAttempt increments the metrics for login attempts\nfunc AddLoginAttempt(_ string) {}\n\n// AddLoginResult increments the metrics for login results\nfunc AddLoginResult(_ string, _ error) {}\n\n// AddNoAuthTried increments the metric for clients disconnected\n// for inactivity before trying to login\nfunc AddNoAuthTried() {}\n\n// HTTPRequestServed increments the metrics for HTTP requests\nfunc HTTPRequestServed(_ int) {}\n\n// UpdateActiveConnectionsSize sets the metric for active connections\nfunc UpdateActiveConnectionsSize(_ int) {}\n"
  },
  {
    "path": "internal/mfa/mfa.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package mfa provides supports for Multi-Factor authentication modules\npackage mfa\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image/png\"\n\t\"time\"\n\n\t\"github.com/pquerna/otp\"\n)\n\nvar (\n\ttotpConfigs   []*TOTPConfig\n\tserviceStatus ServiceStatus\n)\n\n// ServiceStatus defines the service status\ntype ServiceStatus struct {\n\tIsActive    bool         `json:\"is_active\"`\n\tTOTPConfigs []TOTPConfig `json:\"totp_configs\"`\n}\n\n// GetStatus returns the service status\nfunc GetStatus() ServiceStatus {\n\treturn serviceStatus\n}\n\n// Config defines configuration parameters for Multi-Factor authentication modules\ntype Config struct {\n\t// Time-based one time passwords configurations\n\tTOTP []TOTPConfig `json:\"totp\" mapstructure:\"totp\"`\n}\n\n// Initialize configures the MFA support\nfunc (c *Config) Initialize() error {\n\ttotpConfigs = nil\n\tserviceStatus.IsActive = false\n\tserviceStatus.TOTPConfigs = nil\n\ttotp := make(map[string]bool)\n\tfor _, totpConfig := range c.TOTP {\n\t\ttotpConfig := totpConfig //pin\n\t\tif err := totpConfig.validate(); err != nil {\n\t\t\ttotpConfigs = nil\n\t\t\treturn fmt.Errorf(\"invalid TOTP config %+v: %v\", totpConfig, err)\n\t\t}\n\t\tif _, ok := totp[totpConfig.Name]; ok {\n\t\t\ttotpConfigs = nil\n\t\t\treturn fmt.Errorf(\"totp: duplicate configuration name %q\", totpConfig.Name)\n\t\t}\n\t\ttotp[totpConfig.Name] = true\n\t\ttotpConfigs = append(totpConfigs, &totpConfig)\n\t\tserviceStatus.IsActive = true\n\t\tserviceStatus.TOTPConfigs = append(serviceStatus.TOTPConfigs, totpConfig)\n\t}\n\tstartCleanupTicker(2 * time.Minute)\n\treturn nil\n}\n\n// GetAvailableTOTPConfigs returns the available TOTP configs\nfunc GetAvailableTOTPConfigs() []*TOTPConfig {\n\treturn totpConfigs\n}\n\n// GetAvailableTOTPConfigNames returns the available TOTP config names\nfunc GetAvailableTOTPConfigNames() []string {\n\tvar result []string\n\tfor _, c := range totpConfigs {\n\t\tresult = append(result, c.Name)\n\t}\n\treturn result\n}\n\n// ValidateTOTPPasscode validates a TOTP passcode using the given secret and configName\nfunc ValidateTOTPPasscode(configName, passcode, secret string) (bool, error) {\n\tfor _, config := range totpConfigs {\n\t\tif config.Name == configName {\n\t\t\treturn config.validatePasscode(passcode, secret)\n\t\t}\n\t}\n\n\treturn false, fmt.Errorf(\"totp: no configuration %q\", configName)\n}\n\n// GenerateTOTPSecret generates a new TOTP secret and QR code for the given username\n// using the configuration with configName\nfunc GenerateTOTPSecret(configName, username string) (string, *otp.Key, []byte, error) {\n\tfor _, config := range totpConfigs {\n\t\tif config.Name == configName {\n\t\t\tkey, qrCode, err := config.generate(username, 200, 200)\n\t\t\treturn configName, key, qrCode, err\n\t\t}\n\t}\n\n\treturn \"\", nil, nil, fmt.Errorf(\"totp: no configuration %q\", configName)\n}\n\n// GenerateQRCodeFromURL generates a QR code from a TOTP URL\nfunc GenerateQRCodeFromURL(url string, width, height int) ([]byte, error) {\n\tkey, err := otp.NewKeyFromURL(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar buf bytes.Buffer\n\timg, err := key.Image(width, height)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = png.Encode(&buf, img)\n\treturn buf.Bytes(), err\n}\n\n// the ticker cannot be started/stopped from multiple goroutines\nfunc startCleanupTicker(duration time.Duration) {\n\tstopCleanupTicker()\n\tcleanupTicker = time.NewTicker(duration)\n\tcleanupDone = make(chan bool)\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-cleanupDone:\n\t\t\t\treturn\n\t\t\tcase <-cleanupTicker.C:\n\t\t\t\tcleanupUsedPasscodes()\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc stopCleanupTicker() {\n\tif cleanupTicker != nil {\n\t\tcleanupTicker.Stop()\n\t\tcleanupDone <- true\n\t\tcleanupTicker = nil\n\t}\n}\n"
  },
  {
    "path": "internal/mfa/mfa_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage mfa\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pquerna/otp\"\n\t\"github.com/pquerna/otp/totp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMFAConfig(t *testing.T) {\n\tconfig := Config{\n\t\tTOTP: []TOTPConfig{\n\t\t\t{},\n\t\t},\n\t}\n\tconfigName1 := \"config1\"\n\tconfigName2 := \"config2\"\n\tconfigName3 := \"config3\"\n\terr := config.Initialize()\n\tassert.Error(t, err)\n\tconfig.TOTP[0].Name = configName1\n\terr = config.Initialize()\n\tassert.Error(t, err)\n\tconfig.TOTP[0].Issuer = \"issuer\"\n\terr = config.Initialize()\n\tassert.Error(t, err)\n\tconfig.TOTP[0].Algo = TOTPAlgoSHA1\n\terr = config.Initialize()\n\tassert.NoError(t, err)\n\tconfig.TOTP = append(config.TOTP, TOTPConfig{\n\t\tName:   configName1,\n\t\tIssuer: \"SFTPGo\",\n\t\tAlgo:   TOTPAlgoSHA512,\n\t})\n\terr = config.Initialize()\n\tassert.Error(t, err)\n\tconfig.TOTP[1].Name = configName2\n\terr = config.Initialize()\n\tassert.NoError(t, err)\n\tassert.Len(t, GetAvailableTOTPConfigs(), 2)\n\tassert.Len(t, GetAvailableTOTPConfigNames(), 2)\n\tconfig.TOTP = append(config.TOTP, TOTPConfig{\n\t\tName:   configName3,\n\t\tIssuer: \"SFTPGo\",\n\t\tAlgo:   TOTPAlgoSHA256,\n\t})\n\terr = config.Initialize()\n\tassert.NoError(t, err)\n\tassert.Len(t, GetAvailableTOTPConfigs(), 3)\n\tif assert.Len(t, GetAvailableTOTPConfigNames(), 3) {\n\t\tassert.Contains(t, GetAvailableTOTPConfigNames(), configName1)\n\t\tassert.Contains(t, GetAvailableTOTPConfigNames(), configName2)\n\t\tassert.Contains(t, GetAvailableTOTPConfigNames(), configName3)\n\t}\n\tstatus := GetStatus()\n\tassert.True(t, status.IsActive)\n\tif assert.Len(t, status.TOTPConfigs, 3) {\n\t\tassert.Equal(t, configName1, status.TOTPConfigs[0].Name)\n\t\tassert.Equal(t, configName2, status.TOTPConfigs[1].Name)\n\t\tassert.Equal(t, configName3, status.TOTPConfigs[2].Name)\n\t}\n\t// now generate some secrets and validate some passcodes\n\t_, _, _, err = GenerateTOTPSecret(\"\", \"\") //nolint:dogsled\n\tassert.Error(t, err)\n\tmatch, err := ValidateTOTPPasscode(\"\", \"\", \"\")\n\tassert.Error(t, err)\n\tassert.False(t, match)\n\tcfgName, key, _, err := GenerateTOTPSecret(configName1, \"user1\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, key.Secret())\n\tassert.Equal(t, configName1, cfgName)\n\tpasscode, err := generatePasscode(key.Secret(), otp.AlgorithmSHA1)\n\tassert.NoError(t, err)\n\tmatch, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())\n\tassert.NoError(t, err)\n\tassert.True(t, match)\n\tmatch, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())\n\tassert.ErrorIs(t, err, errPasscodeUsed)\n\tassert.False(t, match)\n\n\tpasscode, err = generatePasscode(key.Secret(), otp.AlgorithmSHA256)\n\tassert.NoError(t, err)\n\t// config1 uses sha1 algo\n\tmatch, err = ValidateTOTPPasscode(configName1, passcode, key.Secret())\n\tassert.NoError(t, err)\n\tassert.False(t, match)\n\t// config3 use the expected algo\n\tmatch, err = ValidateTOTPPasscode(configName3, passcode, key.Secret())\n\tassert.NoError(t, err)\n\tassert.True(t, match)\n\n\tstopCleanupTicker()\n}\n\nfunc TestGenerateQRCodeFromURL(t *testing.T) {\n\t_, err := GenerateQRCodeFromURL(\"http://foo\\x7f.cloud\", 200, 200)\n\tassert.Error(t, err)\n\tconfig := TOTPConfig{\n\t\tName:   \"config name\",\n\t\tIssuer: \"SFTPGo\",\n\t\tAlgo:   TOTPAlgoSHA256,\n\t}\n\tkey, qrCode, err := config.generate(\"a\", 150, 150)\n\trequire.NoError(t, err)\n\n\tqrCode1, err := GenerateQRCodeFromURL(key.URL(), 150, 150)\n\trequire.NoError(t, err)\n\tassert.Equal(t, qrCode, qrCode1)\n\t_, err = GenerateQRCodeFromURL(key.URL(), 10, 10)\n\tassert.Error(t, err)\n}\n\nfunc TestCleanupPasscodes(t *testing.T) {\n\tusedPasscodes.Store(\"key\", time.Now().Add(-24*time.Hour).UTC())\n\tstartCleanupTicker(30 * time.Millisecond)\n\tassert.Eventually(t, func() bool {\n\t\t_, ok := usedPasscodes.Load(\"key\")\n\t\treturn !ok\n\t}, 1000*time.Millisecond, 100*time.Millisecond)\n\tstopCleanupTicker()\n}\n\nfunc TestTOTPGenerateErrors(t *testing.T) {\n\tconfig := TOTPConfig{\n\t\tName:   \"name\",\n\t\tIssuer: \"\",\n\t\talgo:   otp.AlgorithmSHA1,\n\t}\n\t// issuer cannot be empty\n\t_, _, err := config.generate(\"username\", 200, 200) //nolint:dogsled\n\tassert.Error(t, err)\n\tconfig.Issuer = \"issuer\"\n\t// we cannot encode an image smaller than 45x45\n\t_, _, err = config.generate(\"username\", 30, 30) //nolint:dogsled\n\tassert.Error(t, err)\n}\n\nfunc generatePasscode(secret string, algo otp.Algorithm) (string, error) {\n\treturn totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{\n\t\tPeriod:    30,\n\t\tSkew:      1,\n\t\tDigits:    otp.DigitsSix,\n\t\tAlgorithm: algo,\n\t})\n}\n"
  },
  {
    "path": "internal/mfa/totp.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage mfa\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"image/png\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pquerna/otp\"\n\t\"github.com/pquerna/otp/totp\"\n)\n\n// TOTPHMacAlgo is the enumerable for the possible HMAC algorithms for Time-based one time passwords\ntype TOTPHMacAlgo = string\n\n// supported TOTP HMAC algorithms\nconst (\n\tTOTPAlgoSHA1   TOTPHMacAlgo = \"sha1\"\n\tTOTPAlgoSHA256 TOTPHMacAlgo = \"sha256\"\n\tTOTPAlgoSHA512 TOTPHMacAlgo = \"sha512\"\n)\n\nvar (\n\tcleanupTicker   *time.Ticker\n\tcleanupDone     chan bool\n\tusedPasscodes   sync.Map\n\terrPasscodeUsed = errors.New(\"this passcode was already used\")\n)\n\n// TOTPConfig defines the configuration for a Time-based one time password\ntype TOTPConfig struct {\n\tName   string       `json:\"name\" mapstructure:\"name\"`\n\tIssuer string       `json:\"issuer\" mapstructure:\"issuer\"`\n\tAlgo   TOTPHMacAlgo `json:\"algo\" mapstructure:\"algo\"`\n\talgo   otp.Algorithm\n}\n\nfunc (c *TOTPConfig) validate() error {\n\tif c.Name == \"\" {\n\t\treturn errors.New(\"totp: name is mandatory\")\n\t}\n\tif c.Issuer == \"\" {\n\t\treturn errors.New(\"totp: issuer is mandatory\")\n\t}\n\tswitch c.Algo {\n\tcase TOTPAlgoSHA1:\n\t\tc.algo = otp.AlgorithmSHA1\n\tcase TOTPAlgoSHA256:\n\t\tc.algo = otp.AlgorithmSHA256\n\tcase TOTPAlgoSHA512:\n\t\tc.algo = otp.AlgorithmSHA512\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported totp algo %q\", c.Algo)\n\t}\n\treturn nil\n}\n\n// validatePasscode validates a TOTP passcode\nfunc (c *TOTPConfig) validatePasscode(passcode, secret string) (bool, error) {\n\tkey := fmt.Sprintf(\"%v_%v\", secret, passcode)\n\tif _, ok := usedPasscodes.Load(key); ok {\n\t\treturn false, errPasscodeUsed\n\t}\n\tmatch, err := totp.ValidateCustom(passcode, secret, time.Now().UTC(), totp.ValidateOpts{\n\t\tPeriod:    30,\n\t\tSkew:      1,\n\t\tDigits:    otp.DigitsSix,\n\t\tAlgorithm: c.algo,\n\t})\n\tif match && err == nil {\n\t\tusedPasscodes.Store(key, time.Now().Add(1*time.Minute).UTC())\n\t}\n\treturn match, err\n}\n\n// generate generates a new TOTP secret and QR code for the given username\nfunc (c *TOTPConfig) generate(username string, qrCodeWidth, qrCodeHeight int) (*otp.Key, []byte, error) {\n\tkey, err := totp.Generate(totp.GenerateOpts{\n\t\tIssuer:      c.Issuer,\n\t\tAccountName: username,\n\t\tDigits:      otp.DigitsSix,\n\t\tAlgorithm:   c.algo,\n\t})\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tvar buf bytes.Buffer\n\timg, err := key.Image(qrCodeWidth, qrCodeHeight)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\terr = png.Encode(&buf, img)\n\treturn key, buf.Bytes(), err\n}\n\nfunc cleanupUsedPasscodes() {\n\tusedPasscodes.Range(func(key, value any) bool {\n\t\texp, ok := value.(time.Time)\n\t\tif !ok || exp.Before(time.Now().UTC()) {\n\t\t\tusedPasscodes.Delete(key)\n\t\t}\n\t\treturn true\n\t})\n}\n"
  },
  {
    "path": "internal/plugin/auth.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage plugin\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/hashicorp/go-hclog\"\n\t\"github.com/hashicorp/go-plugin\"\n\t\"github.com/sftpgo/sdk/plugin/auth\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// Supported auth scopes\nconst (\n\tAuthScopePassword            = 1\n\tAuthScopePublicKey           = 2\n\tAuthScopeKeyboardInteractive = 4\n\tAuthScopeTLSCertificate      = 8\n)\n\n// KeyboardAuthRequest defines the request for a keyboard interactive authentication step\ntype KeyboardAuthRequest struct {\n\tRequestID string   `json:\"request_id\"`\n\tStep      int      `json:\"step\"`\n\tUsername  string   `json:\"username,omitempty\"`\n\tIP        string   `json:\"ip,omitempty\"`\n\tPassword  string   `json:\"password,omitempty\"`\n\tAnswers   []string `json:\"answers,omitempty\"`\n\tQuestions []string `json:\"questions,omitempty\"`\n}\n\n// KeyboardAuthResponse defines the response for a keyboard interactive authentication step\ntype KeyboardAuthResponse struct {\n\tInstruction string   `json:\"instruction\"`\n\tQuestions   []string `json:\"questions\"`\n\tEchos       []bool   `json:\"echos\"`\n\tAuthResult  int      `json:\"auth_result\"`\n\tCheckPwd    int      `json:\"check_password\"`\n}\n\n// Validate returns an error if the KeyboardAuthResponse is invalid\nfunc (r *KeyboardAuthResponse) Validate() error {\n\tif len(r.Questions) == 0 {\n\t\terr := errors.New(\"interactive auth error: response does not contain questions\")\n\t\treturn err\n\t}\n\tif len(r.Questions) != len(r.Echos) {\n\t\terr := fmt.Errorf(\"interactive auth error: response questions don't match echos: %v %v\",\n\t\t\tlen(r.Questions), len(r.Echos))\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// AuthConfig defines configuration parameters for auth plugins\ntype AuthConfig struct {\n\t// Scope defines the scope for the authentication plugin.\n\t// - 1 means passwords only\n\t// - 2 means public keys only\n\t// - 4 means keyboard interactive only\n\t// - 8 means TLS certificates only\n\t// you can combine the scopes, for example 3 means password and public key, 5 password and keyboard\n\t// interactive and so on\n\tScope int `json:\"scope\" mapstructure:\"scope\"`\n}\n\nfunc (c *AuthConfig) validate() error {\n\tauthScopeMax := AuthScopePassword + AuthScopePublicKey + AuthScopeKeyboardInteractive + AuthScopeTLSCertificate\n\tif c.Scope == 0 || c.Scope > authScopeMax {\n\t\treturn fmt.Errorf(\"invalid auth scope: %v\", c.Scope)\n\t}\n\treturn nil\n}\n\ntype authPlugin struct {\n\tconfig  Config\n\tservice auth.Authenticator\n\tclient  *plugin.Client\n}\n\nfunc newAuthPlugin(config Config) (*authPlugin, error) {\n\tp := &authPlugin{\n\t\tconfig: config,\n\t}\n\tif err := p.initialize(); err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to create auth plugin: %v, config %+v\", err, config)\n\t\treturn nil, err\n\t}\n\treturn p, nil\n}\n\nfunc (p *authPlugin) initialize() error {\n\tkillProcess(p.config.Cmd)\n\tlogger.Debug(logSender, \"\", \"create new auth plugin %q\", p.config.Cmd)\n\tif err := p.config.AuthOptions.validate(); err != nil {\n\t\treturn fmt.Errorf(\"invalid options for auth plugin %q: %v\", p.config.Cmd, err)\n\t}\n\n\tsecureConfig, err := p.config.getSecureConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tclient := plugin.NewClient(&plugin.ClientConfig{\n\t\tHandshakeConfig: auth.Handshake,\n\t\tPlugins:         auth.PluginMap,\n\t\tCmd:             p.config.getCommand(),\n\t\tSkipHostEnv:     true,\n\t\tAllowedProtocols: []plugin.Protocol{\n\t\t\tplugin.ProtocolGRPC,\n\t\t},\n\t\tAutoMTLS:     p.config.AutoMTLS,\n\t\tSecureConfig: secureConfig,\n\t\tManaged:      false,\n\t\tLogger: &logger.HCLogAdapter{\n\t\t\tLogger: hclog.New(&hclog.LoggerOptions{\n\t\t\t\tName:        fmt.Sprintf(\"%v.%v\", logSender, auth.PluginName),\n\t\t\t\tLevel:       pluginsLogLevel,\n\t\t\t\tDisableTime: true,\n\t\t\t}),\n\t\t},\n\t})\n\trpcClient, err := client.Client()\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get rpc client for auth plugin %q: %v\", p.config.Cmd, err)\n\t\treturn err\n\t}\n\traw, err := rpcClient.Dispense(auth.PluginName)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get plugin %v from rpc client for command %q: %v\",\n\t\t\tauth.PluginName, p.config.Cmd, err)\n\t\treturn err\n\t}\n\n\tp.service = raw.(auth.Authenticator)\n\tp.client = client\n\n\treturn nil\n}\n\nfunc (p *authPlugin) exited() bool {\n\treturn p.client.Exited()\n}\n\nfunc (p *authPlugin) cleanup() {\n\tp.client.Kill()\n}\n\nfunc (p *authPlugin) checkUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\treturn p.service.CheckUserAndPass(username, password, ip, protocol, userAsJSON)\n}\n\nfunc (p *authPlugin) checkUserAndTLSCertificate(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\treturn p.service.CheckUserAndTLSCert(username, tlsCert, ip, protocol, userAsJSON)\n}\n\nfunc (p *authPlugin) checkUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\treturn p.service.CheckUserAndPublicKey(username, pubKey, ip, protocol, userAsJSON)\n}\n\nfunc (p *authPlugin) checkUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\treturn p.service.CheckUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)\n}\n\nfunc (p *authPlugin) sendKeyboardIteractiveRequest(req *KeyboardAuthRequest) (*KeyboardAuthResponse, error) {\n\tinstructions, questions, echos, authResult, checkPassword, err := p.service.SendKeyboardAuthRequest(\n\t\treq.RequestID, req.Username, req.Password, req.IP, req.Answers, req.Questions, int32(req.Step))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &KeyboardAuthResponse{\n\t\tInstruction: instructions,\n\t\tQuestions:   questions,\n\t\tEchos:       echos,\n\t\tAuthResult:  authResult,\n\t\tCheckPwd:    checkPassword,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/plugin/ipfilter.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage plugin\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hashicorp/go-hclog\"\n\t\"github.com/hashicorp/go-plugin\"\n\t\"github.com/sftpgo/sdk/plugin/ipfilter\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\ntype ipFilterPlugin struct {\n\tconfig Config\n\tfilter ipfilter.Filter\n\tclient *plugin.Client\n}\n\nfunc newIPFilterPlugin(config Config) (*ipFilterPlugin, error) {\n\tp := &ipFilterPlugin{\n\t\tconfig: config,\n\t}\n\tif err := p.initialize(); err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to create IP filter plugin: %v, config %+v\", err, config)\n\t\treturn nil, err\n\t}\n\treturn p, nil\n}\n\nfunc (p *ipFilterPlugin) exited() bool {\n\treturn p.client.Exited()\n}\n\nfunc (p *ipFilterPlugin) cleanup() {\n\tp.client.Kill()\n}\n\nfunc (p *ipFilterPlugin) initialize() error {\n\tlogger.Debug(logSender, \"\", \"create new IP filter plugin %q\", p.config.Cmd)\n\tkillProcess(p.config.Cmd)\n\tsecureConfig, err := p.config.getSecureConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tclient := plugin.NewClient(&plugin.ClientConfig{\n\t\tHandshakeConfig: ipfilter.Handshake,\n\t\tPlugins:         ipfilter.PluginMap,\n\t\tCmd:             p.config.getCommand(),\n\t\tSkipHostEnv:     true,\n\t\tAllowedProtocols: []plugin.Protocol{\n\t\t\tplugin.ProtocolGRPC,\n\t\t},\n\t\tAutoMTLS:     p.config.AutoMTLS,\n\t\tSecureConfig: secureConfig,\n\t\tManaged:      false,\n\t\tLogger: &logger.HCLogAdapter{\n\t\t\tLogger: hclog.New(&hclog.LoggerOptions{\n\t\t\t\tName:        fmt.Sprintf(\"%v.%v\", logSender, ipfilter.PluginName),\n\t\t\t\tLevel:       pluginsLogLevel,\n\t\t\t\tDisableTime: true,\n\t\t\t}),\n\t\t},\n\t})\n\trpcClient, err := client.Client()\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get rpc client for plugin %q: %v\", p.config.Cmd, err)\n\t\treturn err\n\t}\n\traw, err := rpcClient.Dispense(ipfilter.PluginName)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get plugin %v from rpc client for command %q: %v\",\n\t\t\tipfilter.PluginName, p.config.Cmd, err)\n\t\treturn err\n\t}\n\n\tp.client = client\n\tp.filter = raw.(ipfilter.Filter)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/plugin/kms.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage plugin\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/hashicorp/go-hclog\"\n\t\"github.com/hashicorp/go-plugin\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\tkmsplugin \"github.com/sftpgo/sdk/plugin/kms\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\nvar (\n\tvalidKMSSchemes = []string{sdkkms.SchemeAWS, sdkkms.SchemeGCP, sdkkms.SchemeVaultTransit,\n\t\tsdkkms.SchemeAzureKeyVault, sdkkms.SchemeOracleKeyVault}\n\tvalidKMSEncryptedStatuses = []string{sdkkms.SecretStatusVaultTransit, sdkkms.SecretStatusAWS, sdkkms.SecretStatusGCP,\n\t\tsdkkms.SecretStatusAzureKeyVault, sdkkms.SecretStatusOracleKeyVault}\n)\n\n// KMSConfig defines configuration parameters for kms plugins\ntype KMSConfig struct {\n\tScheme          string `json:\"scheme\" mapstructure:\"scheme\"`\n\tEncryptedStatus string `json:\"encrypted_status\" mapstructure:\"encrypted_status\"`\n}\n\nfunc (c *KMSConfig) validate() error {\n\tif !slices.Contains(validKMSSchemes, c.Scheme) {\n\t\treturn fmt.Errorf(\"invalid kms scheme: %v\", c.Scheme)\n\t}\n\tif !slices.Contains(validKMSEncryptedStatuses, c.EncryptedStatus) {\n\t\treturn fmt.Errorf(\"invalid kms encrypted status: %v\", c.EncryptedStatus)\n\t}\n\treturn nil\n}\n\ntype kmsPlugin struct {\n\tconfig  Config\n\tservice kmsplugin.Service\n\tclient  *plugin.Client\n}\n\nfunc newKMSPlugin(config Config) (*kmsPlugin, error) {\n\tp := &kmsPlugin{\n\t\tconfig: config,\n\t}\n\tif err := p.initialize(); err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to create kms plugin: %v, config %+v\", err, config)\n\t\treturn nil, err\n\t}\n\treturn p, nil\n}\n\nfunc (p *kmsPlugin) initialize() error {\n\tkillProcess(p.config.Cmd)\n\tlogger.Debug(logSender, \"\", \"create new kms plugin %q\", p.config.Cmd)\n\tif err := p.config.KMSOptions.validate(); err != nil {\n\t\treturn fmt.Errorf(\"invalid options for kms plugin %q: %v\", p.config.Cmd, err)\n\t}\n\tsecureConfig, err := p.config.getSecureConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tclient := plugin.NewClient(&plugin.ClientConfig{\n\t\tHandshakeConfig: kmsplugin.Handshake,\n\t\tPlugins:         kmsplugin.PluginMap,\n\t\tCmd:             p.config.getCommand(),\n\t\tSkipHostEnv:     true,\n\t\tAllowedProtocols: []plugin.Protocol{\n\t\t\tplugin.ProtocolGRPC,\n\t\t},\n\t\tAutoMTLS:     p.config.AutoMTLS,\n\t\tSecureConfig: secureConfig,\n\t\tManaged:      false,\n\t\tLogger: &logger.HCLogAdapter{\n\t\t\tLogger: hclog.New(&hclog.LoggerOptions{\n\t\t\t\tName:        fmt.Sprintf(\"%v.%v\", logSender, kmsplugin.PluginName),\n\t\t\t\tLevel:       pluginsLogLevel,\n\t\t\t\tDisableTime: true,\n\t\t\t}),\n\t\t},\n\t})\n\trpcClient, err := client.Client()\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get rpc client for kms plugin %q: %v\", p.config.Cmd, err)\n\t\treturn err\n\t}\n\traw, err := rpcClient.Dispense(kmsplugin.PluginName)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get plugin %v from rpc client for command %q: %v\",\n\t\t\tkmsplugin.PluginName, p.config.Cmd, err)\n\t\treturn err\n\t}\n\n\tp.client = client\n\tp.service = raw.(kmsplugin.Service)\n\n\treturn nil\n}\n\nfunc (p *kmsPlugin) exited() bool {\n\treturn p.client.Exited()\n}\n\nfunc (p *kmsPlugin) cleanup() {\n\tp.client.Kill()\n}\n\nfunc (p *kmsPlugin) Encrypt(secret kms.BaseSecret, url string, masterKey string) (string, string, int32, error) {\n\treturn p.service.Encrypt(secret.Payload, secret.AdditionalData, url, masterKey)\n}\n\nfunc (p *kmsPlugin) Decrypt(secret kms.BaseSecret, url string, masterKey string) (string, error) {\n\treturn p.service.Decrypt(secret.Payload, secret.Key, secret.AdditionalData, secret.Mode, url, masterKey)\n}\n\ntype kmsPluginSecretProvider struct {\n\tkms.BaseSecret\n\tURL       string\n\tMasterKey string\n\tconfig    *Config\n}\n\nfunc (s *kmsPluginSecretProvider) Name() string {\n\treturn fmt.Sprintf(\"KMSPlugin_%v_%v_%v\", filepath.Base(s.config.Cmd), s.config.KMSOptions.Scheme, s.config.kmsID)\n}\n\nfunc (s *kmsPluginSecretProvider) IsEncrypted() bool {\n\treturn s.Status == s.config.KMSOptions.EncryptedStatus\n}\n\nfunc (s *kmsPluginSecretProvider) Encrypt() error {\n\tif s.Status != sdkkms.SecretStatusPlain {\n\t\treturn kms.ErrWrongSecretStatus\n\t}\n\tif s.Payload == \"\" {\n\t\treturn kms.ErrInvalidSecret\n\t}\n\n\tpayload, key, mode, err := Handler.kmsEncrypt(s.BaseSecret, s.URL, s.MasterKey, s.config.kmsID)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.Status = s.config.KMSOptions.EncryptedStatus\n\ts.Payload = payload\n\ts.Key = key\n\ts.Mode = int(mode)\n\n\treturn nil\n}\n\nfunc (s *kmsPluginSecretProvider) Decrypt() error {\n\tif !s.IsEncrypted() {\n\t\treturn kms.ErrWrongSecretStatus\n\t}\n\tpayload, err := Handler.kmsDecrypt(s.BaseSecret, s.URL, s.MasterKey, s.config.kmsID)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.Status = sdkkms.SecretStatusPlain\n\ts.Payload = payload\n\ts.Key = \"\"\n\ts.AdditionalData = \"\"\n\ts.Mode = 0\n\n\treturn nil\n}\n\nfunc (s *kmsPluginSecretProvider) Clone() kms.SecretProvider {\n\tbaseSecret := kms.BaseSecret{\n\t\tStatus:         s.Status,\n\t\tPayload:        s.Payload,\n\t\tKey:            s.Key,\n\t\tAdditionalData: s.AdditionalData,\n\t\tMode:           s.Mode,\n\t}\n\treturn s.config.newKMSPluginSecretProvider(baseSecret, s.URL, s.MasterKey)\n}\n"
  },
  {
    "path": "internal/plugin/notifier.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage plugin\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-hclog\"\n\t\"github.com/hashicorp/go-plugin\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// NotifierConfig defines configuration parameters for notifiers plugins\ntype NotifierConfig struct {\n\tFsEvents          []string `json:\"fs_events\" mapstructure:\"fs_events\"`\n\tProviderEvents    []string `json:\"provider_events\" mapstructure:\"provider_events\"`\n\tProviderObjects   []string `json:\"provider_objects\" mapstructure:\"provider_objects\"`\n\tLogEvents         []int    `json:\"log_events\" mapstructure:\"log_events\"`\n\tRetryMaxTime      int      `json:\"retry_max_time\" mapstructure:\"retry_max_time\"`\n\tRetryQueueMaxSize int      `json:\"retry_queue_max_size\" mapstructure:\"retry_queue_max_size\"`\n}\n\nfunc (c *NotifierConfig) hasActions() bool {\n\tif len(c.FsEvents) > 0 {\n\t\treturn true\n\t}\n\tif len(c.ProviderEvents) > 0 && len(c.ProviderObjects) > 0 {\n\t\treturn true\n\t}\n\tif len(c.LogEvents) > 0 {\n\t\treturn true\n\t}\n\treturn false\n}\n\ntype notifierPlugin struct {\n\tconfig         Config\n\tnotifier       notifier.Notifier\n\tclient         *plugin.Client\n\tmu             sync.RWMutex\n\tfsEvents       []*notifier.FsEvent\n\tproviderEvents []*notifier.ProviderEvent\n\tlogEvents      []*notifier.LogEvent\n}\n\nfunc newNotifierPlugin(config Config) (*notifierPlugin, error) {\n\tp := &notifierPlugin{\n\t\tconfig: config,\n\t}\n\tif err := p.initialize(); err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to create notifier plugin: %v, config %+v\", err, config)\n\t\treturn nil, err\n\t}\n\treturn p, nil\n}\n\nfunc (p *notifierPlugin) exited() bool {\n\treturn p.client.Exited()\n}\n\nfunc (p *notifierPlugin) cleanup() {\n\tp.client.Kill()\n}\n\nfunc (p *notifierPlugin) initialize() error {\n\tkillProcess(p.config.Cmd)\n\tlogger.Debug(logSender, \"\", \"create new notifier plugin %q\", p.config.Cmd)\n\tif !p.config.NotifierOptions.hasActions() {\n\t\treturn fmt.Errorf(\"no actions defined for the notifier plugin %q\", p.config.Cmd)\n\t}\n\tsecureConfig, err := p.config.getSecureConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tclient := plugin.NewClient(&plugin.ClientConfig{\n\t\tHandshakeConfig: notifier.Handshake,\n\t\tPlugins:         notifier.PluginMap,\n\t\tCmd:             p.config.getCommand(),\n\t\tSkipHostEnv:     true,\n\t\tAllowedProtocols: []plugin.Protocol{\n\t\t\tplugin.ProtocolGRPC,\n\t\t},\n\t\tAutoMTLS:     p.config.AutoMTLS,\n\t\tSecureConfig: secureConfig,\n\t\tManaged:      false,\n\t\tLogger: &logger.HCLogAdapter{\n\t\t\tLogger: hclog.New(&hclog.LoggerOptions{\n\t\t\t\tName:        fmt.Sprintf(\"%s.%s\", logSender, notifier.PluginName),\n\t\t\t\tLevel:       pluginsLogLevel,\n\t\t\t\tDisableTime: true,\n\t\t\t}),\n\t\t},\n\t})\n\trpcClient, err := client.Client()\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get rpc client for plugin %q: %v\", p.config.Cmd, err)\n\t\treturn err\n\t}\n\traw, err := rpcClient.Dispense(notifier.PluginName)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get plugin %v from rpc client for command %q: %v\",\n\t\t\tnotifier.PluginName, p.config.Cmd, err)\n\t\treturn err\n\t}\n\n\tp.client = client\n\tp.notifier = raw.(notifier.Notifier)\n\n\treturn nil\n}\n\nfunc (p *notifierPlugin) queueSize() int {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\n\treturn len(p.providerEvents) + len(p.fsEvents) + len(p.logEvents)\n}\n\nfunc (p *notifierPlugin) queueFsEvent(ev *notifier.FsEvent) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tp.fsEvents = append(p.fsEvents, ev)\n}\n\nfunc (p *notifierPlugin) queueProviderEvent(ev *notifier.ProviderEvent) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tp.providerEvents = append(p.providerEvents, ev)\n}\n\nfunc (p *notifierPlugin) queueLogEvent(ev *notifier.LogEvent) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tp.logEvents = append(p.logEvents, ev)\n}\n\nfunc (p *notifierPlugin) canQueueEvent(timestamp int64) bool {\n\tif p.config.NotifierOptions.RetryMaxTime == 0 {\n\t\treturn false\n\t}\n\tif time.Now().After(time.Unix(0, timestamp).Add(time.Duration(p.config.NotifierOptions.RetryMaxTime) * time.Second)) {\n\t\tlogger.Warn(logSender, \"\", \"dropping too late event for plugin %v, event timestamp: %v\",\n\t\t\tp.config.Cmd, time.Unix(0, timestamp))\n\t\treturn false\n\t}\n\tif p.config.NotifierOptions.RetryQueueMaxSize > 0 {\n\t\treturn p.queueSize() < p.config.NotifierOptions.RetryQueueMaxSize\n\t}\n\treturn true\n}\n\nfunc (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {\n\tif !slices.Contains(p.config.NotifierOptions.FsEvents, event.Action) {\n\t\treturn\n\t}\n\tp.sendFsEvent(event)\n}\n\nfunc (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, object Renderer) {\n\tif !slices.Contains(p.config.NotifierOptions.ProviderEvents, event.Action) ||\n\t\t!slices.Contains(p.config.NotifierOptions.ProviderObjects, event.ObjectType) {\n\t\treturn\n\t}\n\tp.sendProviderEvent(event, object)\n}\n\nfunc (p *notifierPlugin) notifyLogEvent(event *notifier.LogEvent) {\n\tp.sendLogEvent(event)\n}\n\nfunc (p *notifierPlugin) sendFsEvent(ev *notifier.FsEvent) {\n\tgo func(event *notifier.FsEvent) {\n\t\tHandler.addTask()\n\t\tdefer Handler.removeTask()\n\n\t\tif err := p.notifier.NotifyFsEvent(event); err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to send fs action notification to plugin %v: %v\", p.config.Cmd, err)\n\t\t\tif p.canQueueEvent(event.Timestamp) {\n\t\t\t\tp.queueFsEvent(event)\n\t\t\t}\n\t\t}\n\t}(ev)\n}\n\nfunc (p *notifierPlugin) sendProviderEvent(ev *notifier.ProviderEvent, object Renderer) {\n\tgo func(event *notifier.ProviderEvent) {\n\t\tHandler.addTask()\n\t\tdefer Handler.removeTask()\n\n\t\tif object != nil {\n\t\t\tobjectAsJSON, err := object.RenderAsJSON(event.Action != \"delete\")\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(logSender, \"\", \"unable to render user as json for action %q: %v\", event.Action, err)\n\t\t\t} else {\n\t\t\t\tevent.ObjectData = objectAsJSON\n\t\t\t}\n\t\t}\n\n\t\tif err := p.notifier.NotifyProviderEvent(event); err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to send user action notification to plugin %v: %v\", p.config.Cmd, err)\n\t\t\tif p.canQueueEvent(event.Timestamp) {\n\t\t\t\tp.queueProviderEvent(event)\n\t\t\t}\n\t\t}\n\t}(ev)\n}\n\nfunc (p *notifierPlugin) sendLogEvent(ev *notifier.LogEvent) {\n\tgo func(event *notifier.LogEvent) {\n\t\tHandler.addTask()\n\t\tdefer Handler.removeTask()\n\n\t\tif err := p.notifier.NotifyLogEvent(event); err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to send log event to plugin %v: %v\", p.config.Cmd, err)\n\t\t\tif p.canQueueEvent(event.Timestamp) {\n\t\t\t\tp.queueLogEvent(event)\n\t\t\t}\n\t\t}\n\t}(ev)\n}\n\nfunc (p *notifierPlugin) sendQueuedEvents() {\n\tqueueSize := p.queueSize()\n\tif queueSize == 0 {\n\t\treturn\n\t}\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tlogger.Debug(logSender, \"\", \"send queued events for notifier %q, events size: %v\", p.config.Cmd, queueSize)\n\n\tfor _, ev := range p.fsEvents {\n\t\tp.sendFsEvent(ev)\n\t}\n\tp.fsEvents = nil\n\n\tfor _, ev := range p.providerEvents {\n\t\tp.sendProviderEvent(ev, nil)\n\t}\n\tp.providerEvents = nil\n\n\tfor _, ev := range p.logEvents {\n\t\tp.sendLogEvent(ev)\n\t}\n\tp.logEvents = nil\n\n\tlogger.Debug(logSender, \"\", \"%d queued events sent for notifier %q,\", queueSize, p.config.Cmd)\n}\n"
  },
  {
    "path": "internal/plugin/plugin.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package plugin provides support for the SFTPGo plugin system\npackage plugin\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-hclog\"\n\t\"github.com/hashicorp/go-plugin\"\n\t\"github.com/sftpgo/sdk/plugin/auth\"\n\t\"github.com/sftpgo/sdk/plugin/eventsearcher\"\n\t\"github.com/sftpgo/sdk/plugin/ipfilter\"\n\tkmsplugin \"github.com/sftpgo/sdk/plugin/kms\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\tlogSender = \"plugins\"\n)\n\nvar (\n\t// Handler defines the plugins manager\n\tHandler         Manager\n\tpluginsLogLevel = hclog.Debug\n\t// ErrNoSearcher defines the error to return for events searches if no plugin is configured\n\tErrNoSearcher = errors.New(\"no events searcher plugin defined\")\n)\n\n// Renderer defines the interface for generic objects rendering\ntype Renderer interface {\n\tRenderAsJSON(reload bool) ([]byte, error)\n}\n\n// Config defines a plugin configuration\ntype Config struct {\n\t// Plugin type\n\tType string `json:\"type\" mapstructure:\"type\"`\n\t// NotifierOptions defines options for notifiers plugins\n\tNotifierOptions NotifierConfig `json:\"notifier_options\" mapstructure:\"notifier_options\"`\n\t// KMSOptions defines options for a KMS plugin\n\tKMSOptions KMSConfig `json:\"kms_options\" mapstructure:\"kms_options\"`\n\t// AuthOptions defines options for authentication plugins\n\tAuthOptions AuthConfig `json:\"auth_options\" mapstructure:\"auth_options\"`\n\t// Path to the plugin executable\n\tCmd string `json:\"cmd\" mapstructure:\"cmd\"`\n\t// Args to pass to the plugin executable\n\tArgs []string `json:\"args\" mapstructure:\"args\"`\n\t// SHA256 checksum for the plugin executable.\n\t// If not empty it will be used to verify the integrity of the executable\n\tSHA256Sum string `json:\"sha256sum\" mapstructure:\"sha256sum\"`\n\t// If enabled the client and the server automatically negotiate mTLS for\n\t// transport authentication. This ensures that only the original client will\n\t// be allowed to connect to the server, and all other connections will be\n\t// rejected. The client will also refuse to connect to any server that isn't\n\t// the original instance started by the client.\n\tAutoMTLS bool `json:\"auto_mtls\" mapstructure:\"auto_mtls\"`\n\t// EnvPrefix defines the prefix for env vars to pass from the SFTPGo process\n\t// environment to the plugin. Set to \"none\" to not pass any environment\n\t// variable, set to \"*\" to pass all environment variables. If empty, the\n\t// prefix is returned as the plugin name in uppercase with \"-\" replaced with\n\t// \"_\" and a trailing \"_\". For example if the plugin name is\n\t// sftpgo-plugin-eventsearch the prefix will be SFTPGO_PLUGIN_EVENTSEARCH_\n\tEnvPrefix string `json:\"env_prefix\" mapstructure:\"env_prefix\"`\n\t// Additional environment variable names to pass from the SFTPGo process\n\t// environment to the plugin.\n\tEnvVars []string `json:\"env_vars\" mapstructure:\"env_vars\"`\n\t// unique identifier for kms plugins\n\tkmsID int\n}\n\nfunc (c *Config) getSecureConfig() (*plugin.SecureConfig, error) {\n\tif c.SHA256Sum != \"\" {\n\t\tchecksum, err := hex.DecodeString(c.SHA256Sum)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid sha256 hash %q: %w\", c.SHA256Sum, err)\n\t\t}\n\t\treturn &plugin.SecureConfig{\n\t\t\tChecksum: checksum,\n\t\t\tHash:     sha256.New(),\n\t\t}, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (c *Config) getEnvVarPrefix() string {\n\tif c.EnvPrefix == \"none\" {\n\t\treturn \"\"\n\t}\n\tif c.EnvPrefix != \"\" {\n\t\treturn c.EnvPrefix\n\t}\n\n\tbaseName := filepath.Base(c.Cmd)\n\tname := strings.TrimSuffix(baseName, filepath.Ext(baseName))\n\tprefix := strings.ToUpper(name) + \"_\"\n\treturn strings.ReplaceAll(prefix, \"-\", \"_\")\n}\n\nfunc (c *Config) getCommand() *exec.Cmd {\n\tcmd := exec.Command(c.Cmd, c.Args...)\n\tcmd.Env = []string{}\n\n\tif envVarPrefix := c.getEnvVarPrefix(); envVarPrefix != \"\" {\n\t\tif envVarPrefix == \"*\" {\n\t\t\tlogger.Debug(logSender, \"\", \"sharing all the environment variables with plugin %q\", c.Cmd)\n\t\t\tcmd.Env = append(cmd.Env, os.Environ()...)\n\t\t\treturn cmd\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"adding env vars with prefix %q for plugin %q\", envVarPrefix, c.Cmd)\n\t\tfor _, val := range os.Environ() {\n\t\t\tif strings.HasPrefix(val, envVarPrefix) {\n\t\t\t\tcmd.Env = append(cmd.Env, val)\n\t\t\t}\n\t\t}\n\t}\n\tlogger.Debug(logSender, \"\", \"additional env vars for plugin %q: %+v\", c.Cmd, c.EnvVars)\n\tfor _, key := range c.EnvVars {\n\t\tcmd.Env = append(cmd.Env, fmt.Sprintf(\"%s=%s\", key, os.Getenv(key)))\n\t}\n\treturn cmd\n}\n\nfunc (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider {\n\treturn &kmsPluginSecretProvider{\n\t\tBaseSecret: base,\n\t\tURL:        url,\n\t\tMasterKey:  masterKey,\n\t\tconfig:     c,\n\t}\n}\n\n// Manager handles enabled plugins\ntype Manager struct {\n\tclosed atomic.Bool\n\tdone   chan bool\n\t// List of configured plugins\n\tConfigs          []Config `json:\"plugins\" mapstructure:\"plugins\"`\n\tnotifLock        sync.RWMutex\n\tnotifiers        []*notifierPlugin\n\tkmsLock          sync.RWMutex\n\tkms              []*kmsPlugin\n\tauthLock         sync.RWMutex\n\tauths            []*authPlugin\n\tsearcherLock     sync.RWMutex\n\tsearcher         *searcherPlugin\n\tipFilterLock     sync.RWMutex\n\tfilter           *ipFilterPlugin\n\tauthScopes       int\n\thasSearcher      bool\n\thasNotifiers     bool\n\thasAuths         bool\n\thasIPFilter      bool\n\tconcurrencyGuard chan struct{}\n}\n\n// Initialize initializes the configured plugins\nfunc Initialize(configs []Config, logLevel string) error {\n\tlogger.Debug(logSender, \"\", \"initialize\")\n\tHandler = Manager{\n\t\tConfigs:          configs,\n\t\tdone:             make(chan bool),\n\t\tauthScopes:       -1,\n\t\tconcurrencyGuard: make(chan struct{}, 250),\n\t}\n\tHandler.closed.Store(false)\n\tsetLogLevel(logLevel)\n\tif len(configs) == 0 {\n\t\treturn nil\n\t}\n\n\tif err := Handler.validateConfigs(); err != nil {\n\t\treturn err\n\t}\n\tif err := initializePlugins(); err != nil {\n\t\treturn err\n\t}\n\n\tstartCheckTicker()\n\treturn nil\n}\n\nfunc initializePlugins() error {\n\tkmsID := 0\n\tfor idx, config := range Handler.Configs {\n\t\tswitch config.Type {\n\t\tcase notifier.PluginName:\n\t\t\tplugin, err := newNotifierPlugin(config)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tHandler.notifiers = append(Handler.notifiers, plugin)\n\t\tcase kmsplugin.PluginName:\n\t\t\tplugin, err := newKMSPlugin(config)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tHandler.kms = append(Handler.kms, plugin)\n\t\t\tHandler.Configs[idx].kmsID = kmsID\n\t\t\tkmsID++\n\t\t\tkms.RegisterSecretProvider(config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus,\n\t\t\t\tHandler.Configs[idx].newKMSPluginSecretProvider)\n\t\t\tlogger.Info(logSender, \"\", \"registered secret provider for scheme %q, encrypted status %q\",\n\t\t\t\tconfig.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus)\n\t\tcase auth.PluginName:\n\t\t\tplugin, err := newAuthPlugin(config)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tHandler.auths = append(Handler.auths, plugin)\n\t\t\tif Handler.authScopes == -1 {\n\t\t\t\tHandler.authScopes = config.AuthOptions.Scope\n\t\t\t} else {\n\t\t\t\tHandler.authScopes |= config.AuthOptions.Scope\n\t\t\t}\n\t\tcase eventsearcher.PluginName:\n\t\t\tplugin, err := newSearcherPlugin(config)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tHandler.searcher = plugin\n\t\tcase ipfilter.PluginName:\n\t\t\tplugin, err := newIPFilterPlugin(config)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tHandler.filter = plugin\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unsupported plugin type: %v\", config.Type)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *Manager) validateConfigs() error {\n\tkmsSchemes := make(map[string]bool)\n\tkmsEncryptions := make(map[string]bool)\n\tm.hasSearcher = false\n\tm.hasNotifiers = false\n\tm.hasAuths = false\n\tm.hasIPFilter = false\n\n\tfor _, config := range m.Configs {\n\t\tswitch config.Type {\n\t\tcase kmsplugin.PluginName:\n\t\t\tif _, ok := kmsSchemes[config.KMSOptions.Scheme]; ok {\n\t\t\t\treturn fmt.Errorf(\"invalid KMS configuration, duplicated scheme %q\", config.KMSOptions.Scheme)\n\t\t\t}\n\t\t\tif _, ok := kmsEncryptions[config.KMSOptions.EncryptedStatus]; ok {\n\t\t\t\treturn fmt.Errorf(\"invalid KMS configuration, duplicated encrypted status %q\", config.KMSOptions.EncryptedStatus)\n\t\t\t}\n\t\t\tkmsSchemes[config.KMSOptions.Scheme] = true\n\t\t\tkmsEncryptions[config.KMSOptions.EncryptedStatus] = true\n\t\tcase eventsearcher.PluginName:\n\t\t\tif m.hasSearcher {\n\t\t\t\treturn errors.New(\"only one eventsearcher plugin can be defined\")\n\t\t\t}\n\t\t\tm.hasSearcher = true\n\t\tcase notifier.PluginName:\n\t\t\tm.hasNotifiers = true\n\t\tcase auth.PluginName:\n\t\t\tm.hasAuths = true\n\t\tcase ipfilter.PluginName:\n\t\t\tm.hasIPFilter = true\n\t\t}\n\t}\n\treturn nil\n}\n\n// HasAuthenticators returns true if there is at least an auth plugin\nfunc (m *Manager) HasAuthenticators() bool {\n\treturn m.hasAuths\n}\n\n// HasNotifiers returns true if there is at least a notifier plugin\nfunc (m *Manager) HasNotifiers() bool {\n\treturn m.hasNotifiers\n}\n\n// NotifyFsEvent sends the fs event notifications using any defined notifier plugins\nfunc (m *Manager) NotifyFsEvent(event *notifier.FsEvent) {\n\tm.notifLock.RLock()\n\tdefer m.notifLock.RUnlock()\n\n\tfor _, n := range m.notifiers {\n\t\tn.notifyFsAction(event)\n\t}\n}\n\n// NotifyProviderEvent sends the provider event notifications using any defined notifier plugins\nfunc (m *Manager) NotifyProviderEvent(event *notifier.ProviderEvent, object Renderer) {\n\tm.notifLock.RLock()\n\tdefer m.notifLock.RUnlock()\n\n\tfor _, n := range m.notifiers {\n\t\tn.notifyProviderAction(event, object)\n\t}\n}\n\n// NotifyLogEvent sends the log event notifications using any defined notifier plugins\nfunc (m *Manager) NotifyLogEvent(event notifier.LogEventType, protocol, username, ip, role string, err error) {\n\tif !m.hasNotifiers {\n\t\treturn\n\t}\n\tm.notifLock.RLock()\n\tdefer m.notifLock.RUnlock()\n\n\tvar e *notifier.LogEvent\n\n\tfor _, n := range m.notifiers {\n\t\tif slices.Contains(n.config.NotifierOptions.LogEvents, int(event)) {\n\t\t\tif e == nil {\n\t\t\t\tmessage := \"\"\n\t\t\t\tif err != nil {\n\t\t\t\t\tmessage = strings.Trim(err.Error(), \"\\x00\")\n\t\t\t\t}\n\n\t\t\t\te = &notifier.LogEvent{\n\t\t\t\t\tTimestamp: time.Now().UnixNano(),\n\t\t\t\t\tEvent:     event,\n\t\t\t\t\tProtocol:  protocol,\n\t\t\t\t\tUsername:  username,\n\t\t\t\t\tIP:        ip,\n\t\t\t\t\tMessage:   message,\n\t\t\t\t\tRole:      role,\n\t\t\t\t}\n\t\t\t}\n\t\t\tn.notifyLogEvent(e)\n\t\t}\n\t}\n}\n\n// HasSearcher returns true if an event searcher plugin is defined\nfunc (m *Manager) HasSearcher() bool {\n\treturn m.hasSearcher\n}\n\n// SearchFsEvents returns the filesystem events matching the specified filters\nfunc (m *Manager) SearchFsEvents(searchFilters *eventsearcher.FsEventSearch) ([]byte, error) {\n\tif !m.hasSearcher {\n\t\treturn nil, ErrNoSearcher\n\t}\n\tm.searcherLock.RLock()\n\tplugin := m.searcher\n\tm.searcherLock.RUnlock()\n\n\treturn plugin.searchear.SearchFsEvents(searchFilters)\n}\n\n// SearchProviderEvents returns the provider events matching the specified filters\nfunc (m *Manager) SearchProviderEvents(searchFilters *eventsearcher.ProviderEventSearch) ([]byte, error) {\n\tif !m.hasSearcher {\n\t\treturn nil, ErrNoSearcher\n\t}\n\tm.searcherLock.RLock()\n\tplugin := m.searcher\n\tm.searcherLock.RUnlock()\n\n\treturn plugin.searchear.SearchProviderEvents(searchFilters)\n}\n\n// SearchLogEvents returns the log events matching the specified filters\nfunc (m *Manager) SearchLogEvents(searchFilters *eventsearcher.LogEventSearch) ([]byte, error) {\n\tif !m.hasSearcher {\n\t\treturn nil, ErrNoSearcher\n\t}\n\tm.searcherLock.RLock()\n\tplugin := m.searcher\n\tm.searcherLock.RUnlock()\n\n\treturn plugin.searchear.SearchLogEvents(searchFilters)\n}\n\n// IsIPBanned returns true if the IP filter plugin does not allow the specified ip.\n// If no IP filter plugin is defined this method returns false\nfunc (m *Manager) IsIPBanned(ip, protocol string) bool {\n\tif !m.hasIPFilter {\n\t\treturn false\n\t}\n\n\tm.ipFilterLock.RLock()\n\tplugin := m.filter\n\tm.ipFilterLock.RUnlock()\n\n\tif plugin.exited() {\n\t\tlogger.Warn(logSender, \"\", \"ip filter plugin is not active, cannot check ip %q\", ip)\n\t\treturn false\n\t}\n\n\treturn plugin.filter.CheckIP(ip, protocol) != nil\n}\n\n// ReloadFilter sends a reload request to the IP filter plugin\nfunc (m *Manager) ReloadFilter() {\n\tif !m.hasIPFilter {\n\t\treturn\n\t}\n\tm.ipFilterLock.RLock()\n\tplugin := m.filter\n\tm.ipFilterLock.RUnlock()\n\n\tif err := plugin.filter.Reload(); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to reload IP filter plugin: %v\", err)\n\t}\n}\n\nfunc (m *Manager) kmsEncrypt(secret kms.BaseSecret, url string, masterKey string, kmsID int) (string, string, int32, error) {\n\tm.kmsLock.RLock()\n\tplugin := m.kms[kmsID]\n\tm.kmsLock.RUnlock()\n\n\treturn plugin.Encrypt(secret, url, masterKey)\n}\n\nfunc (m *Manager) kmsDecrypt(secret kms.BaseSecret, url string, masterKey string, kmsID int) (string, error) {\n\tm.kmsLock.RLock()\n\tplugin := m.kms[kmsID]\n\tm.kmsLock.RUnlock()\n\n\treturn plugin.Decrypt(secret, url, masterKey)\n}\n\n// HasAuthScope returns true if there is an auth plugin that support the specified scope\nfunc (m *Manager) HasAuthScope(scope int) bool {\n\tif m.authScopes == -1 {\n\t\treturn false\n\t}\n\treturn m.authScopes&scope != 0\n}\n\n// Authenticate tries to authenticate the specified user using an external plugin\nfunc (m *Manager) Authenticate(username, password, ip, protocol string, pkey string,\n\ttlsCert *x509.Certificate, authScope int, userAsJSON []byte,\n) ([]byte, error) {\n\tswitch authScope {\n\tcase AuthScopePassword:\n\t\treturn m.checkUserAndPass(username, password, ip, protocol, userAsJSON)\n\tcase AuthScopePublicKey:\n\t\treturn m.checkUserAndPublicKey(username, pkey, ip, protocol, userAsJSON)\n\tcase AuthScopeKeyboardInteractive:\n\t\treturn m.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)\n\tcase AuthScopeTLSCertificate:\n\t\tcert, err := util.EncodeTLSCertToPem(tlsCert)\n\t\tif err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to encode tls certificate to pem: %v\", err)\n\t\t\treturn nil, fmt.Errorf(\"unable to encode tls cert to pem: %w\", err)\n\t\t}\n\t\treturn m.checkUserAndTLSCert(username, cert, ip, protocol, userAsJSON)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported auth scope: %v\", authScope)\n\t}\n}\n\n// ExecuteKeyboardInteractiveStep executes a keyboard interactive step\nfunc (m *Manager) ExecuteKeyboardInteractiveStep(req *KeyboardAuthRequest) (*KeyboardAuthResponse, error) {\n\tvar plugin *authPlugin\n\n\tm.authLock.Lock()\n\tfor _, p := range m.auths {\n\t\tif p.config.AuthOptions.Scope&AuthScopePassword != 0 {\n\t\t\tplugin = p\n\t\t\tbreak\n\t\t}\n\t}\n\tm.authLock.Unlock()\n\n\tif plugin == nil {\n\t\treturn nil, errors.New(\"no auth plugin configured for keyaboard interactive authentication step\")\n\t}\n\n\treturn plugin.sendKeyboardIteractiveRequest(req)\n}\n\nfunc (m *Manager) checkUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\tvar plugin *authPlugin\n\n\tm.authLock.Lock()\n\tfor _, p := range m.auths {\n\t\tif p.config.AuthOptions.Scope&AuthScopePassword != 0 {\n\t\t\tplugin = p\n\t\t\tbreak\n\t\t}\n\t}\n\tm.authLock.Unlock()\n\n\tif plugin == nil {\n\t\treturn nil, errors.New(\"no auth plugin configured for password checking\")\n\t}\n\n\treturn plugin.checkUserAndPass(username, password, ip, protocol, userAsJSON)\n}\n\nfunc (m *Manager) checkUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\tvar plugin *authPlugin\n\n\tm.authLock.Lock()\n\tfor _, p := range m.auths {\n\t\tif p.config.AuthOptions.Scope&AuthScopePublicKey != 0 {\n\t\t\tplugin = p\n\t\t\tbreak\n\t\t}\n\t}\n\tm.authLock.Unlock()\n\n\tif plugin == nil {\n\t\treturn nil, errors.New(\"no auth plugin configured for public key checking\")\n\t}\n\n\treturn plugin.checkUserAndPublicKey(username, pubKey, ip, protocol, userAsJSON)\n}\n\nfunc (m *Manager) checkUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\tvar plugin *authPlugin\n\n\tm.authLock.Lock()\n\tfor _, p := range m.auths {\n\t\tif p.config.AuthOptions.Scope&AuthScopeTLSCertificate != 0 {\n\t\t\tplugin = p\n\t\t\tbreak\n\t\t}\n\t}\n\tm.authLock.Unlock()\n\n\tif plugin == nil {\n\t\treturn nil, errors.New(\"no auth plugin configured for TLS certificate checking\")\n\t}\n\n\treturn plugin.checkUserAndTLSCertificate(username, tlsCert, ip, protocol, userAsJSON)\n}\n\nfunc (m *Manager) checkUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) {\n\tvar plugin *authPlugin\n\n\tm.authLock.Lock()\n\tfor _, p := range m.auths {\n\t\tif p.config.AuthOptions.Scope&AuthScopeKeyboardInteractive != 0 {\n\t\t\tplugin = p\n\t\t\tbreak\n\t\t}\n\t}\n\tm.authLock.Unlock()\n\n\tif plugin == nil {\n\t\treturn nil, errors.New(\"no auth plugin configured for keyboard interactive checking\")\n\t}\n\n\treturn plugin.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON)\n}\n\nfunc (m *Manager) checkCrashedPlugins() {\n\tm.notifLock.RLock()\n\tfor idx, n := range m.notifiers {\n\t\tif n.exited() {\n\t\t\tdefer func(cfg Config, index int) {\n\t\t\t\tHandler.restartNotifierPlugin(cfg, index)\n\t\t\t}(n.config, idx)\n\t\t} else {\n\t\t\tn.sendQueuedEvents()\n\t\t}\n\t}\n\tm.notifLock.RUnlock()\n\n\tm.kmsLock.RLock()\n\tfor idx, k := range m.kms {\n\t\tif k.exited() {\n\t\t\tdefer func(cfg Config, index int) {\n\t\t\t\tHandler.restartKMSPlugin(cfg, index)\n\t\t\t}(k.config, idx)\n\t\t}\n\t}\n\tm.kmsLock.RUnlock()\n\n\tm.authLock.RLock()\n\tfor idx, a := range m.auths {\n\t\tif a.exited() {\n\t\t\tdefer func(cfg Config, index int) {\n\t\t\t\tHandler.restartAuthPlugin(cfg, index)\n\t\t\t}(a.config, idx)\n\t\t}\n\t}\n\tm.authLock.RUnlock()\n\n\tif m.hasSearcher {\n\t\tm.searcherLock.RLock()\n\t\tif m.searcher.exited() {\n\t\t\tdefer func(cfg Config) {\n\t\t\t\tHandler.restartSearcherPlugin(cfg)\n\t\t\t}(m.searcher.config)\n\t\t}\n\t\tm.searcherLock.RUnlock()\n\t}\n\n\tif m.hasIPFilter {\n\t\tm.ipFilterLock.RLock()\n\t\tif m.filter.exited() {\n\t\t\tdefer func(cfg Config) {\n\t\t\t\tHandler.restartIPFilterPlugin(cfg)\n\t\t\t}(m.filter.config)\n\t\t}\n\t\tm.ipFilterLock.RUnlock()\n\t}\n}\n\nfunc (m *Manager) restartNotifierPlugin(config Config, idx int) {\n\tif m.closed.Load() {\n\t\treturn\n\t}\n\tlogger.Info(logSender, \"\", \"try to restart crashed notifier plugin %q, idx: %v\", config.Cmd, idx)\n\tplugin, err := newNotifierPlugin(config)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to restart notifier plugin %q, err: %v\", config.Cmd, err)\n\t\treturn\n\t}\n\n\tm.notifLock.Lock()\n\tplugin.fsEvents = m.notifiers[idx].fsEvents\n\tplugin.providerEvents = m.notifiers[idx].providerEvents\n\tplugin.logEvents = m.notifiers[idx].logEvents\n\tm.notifiers[idx] = plugin\n\tm.notifLock.Unlock()\n\tplugin.sendQueuedEvents()\n}\n\nfunc (m *Manager) restartKMSPlugin(config Config, idx int) {\n\tif m.closed.Load() {\n\t\treturn\n\t}\n\tlogger.Info(logSender, \"\", \"try to restart crashed kms plugin %q, idx: %v\", config.Cmd, idx)\n\tplugin, err := newKMSPlugin(config)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to restart kms plugin %q, err: %v\", config.Cmd, err)\n\t\treturn\n\t}\n\n\tm.kmsLock.Lock()\n\tm.kms[idx] = plugin\n\tm.kmsLock.Unlock()\n}\n\nfunc (m *Manager) restartAuthPlugin(config Config, idx int) {\n\tif m.closed.Load() {\n\t\treturn\n\t}\n\tlogger.Info(logSender, \"\", \"try to restart crashed auth plugin %q, idx: %v\", config.Cmd, idx)\n\tplugin, err := newAuthPlugin(config)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to restart auth plugin %q, err: %v\", config.Cmd, err)\n\t\treturn\n\t}\n\n\tm.authLock.Lock()\n\tm.auths[idx] = plugin\n\tm.authLock.Unlock()\n}\n\nfunc (m *Manager) restartSearcherPlugin(config Config) {\n\tif m.closed.Load() {\n\t\treturn\n\t}\n\tlogger.Info(logSender, \"\", \"try to restart crashed searcher plugin %q\", config.Cmd)\n\tplugin, err := newSearcherPlugin(config)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to restart searcher plugin %q, err: %v\", config.Cmd, err)\n\t\treturn\n\t}\n\n\tm.searcherLock.Lock()\n\tm.searcher = plugin\n\tm.searcherLock.Unlock()\n}\n\nfunc (m *Manager) restartIPFilterPlugin(config Config) {\n\tif m.closed.Load() {\n\t\treturn\n\t}\n\tlogger.Info(logSender, \"\", \"try to restart crashed IP filter plugin %q\", config.Cmd)\n\tplugin, err := newIPFilterPlugin(config)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to restart IP filter plugin %q, err: %v\", config.Cmd, err)\n\t\treturn\n\t}\n\n\tm.ipFilterLock.Lock()\n\tm.filter = plugin\n\tm.ipFilterLock.Unlock()\n}\n\nfunc (m *Manager) addTask() {\n\tm.concurrencyGuard <- struct{}{}\n}\n\nfunc (m *Manager) removeTask() {\n\t<-m.concurrencyGuard\n}\n\n// Cleanup releases all the active plugins\nfunc (m *Manager) Cleanup() {\n\tif m.closed.Swap(true) {\n\t\treturn\n\t}\n\tlogger.Debug(logSender, \"\", \"cleanup\")\n\tclose(m.done)\n\tm.notifLock.Lock()\n\tfor _, n := range m.notifiers {\n\t\tlogger.Debug(logSender, \"\", \"cleanup notifier plugin %v\", n.config.Cmd)\n\t\tn.cleanup()\n\t}\n\tm.notifLock.Unlock()\n\n\tm.kmsLock.Lock()\n\tfor _, k := range m.kms {\n\t\tlogger.Debug(logSender, \"\", \"cleanup kms plugin %v\", k.config.Cmd)\n\t\tk.cleanup()\n\t}\n\tm.kmsLock.Unlock()\n\n\tm.authLock.Lock()\n\tfor _, a := range m.auths {\n\t\tlogger.Debug(logSender, \"\", \"cleanup auth plugin %v\", a.config.Cmd)\n\t\ta.cleanup()\n\t}\n\tm.authLock.Unlock()\n\n\tif m.hasSearcher {\n\t\tm.searcherLock.Lock()\n\t\tlogger.Debug(logSender, \"\", \"cleanup searcher plugin %v\", m.searcher.config.Cmd)\n\t\tm.searcher.cleanup()\n\t\tm.searcherLock.Unlock()\n\t}\n\n\tif m.hasIPFilter {\n\t\tm.ipFilterLock.Lock()\n\t\tlogger.Debug(logSender, \"\", \"cleanup IP filter plugin %v\", m.filter.config.Cmd)\n\t\tm.filter.cleanup()\n\t\tm.ipFilterLock.Unlock()\n\t}\n}\n\nfunc setLogLevel(logLevel string) {\n\tswitch logLevel {\n\tcase \"info\":\n\t\tpluginsLogLevel = hclog.Info\n\tcase \"warn\":\n\t\tpluginsLogLevel = hclog.Warn\n\tcase \"error\":\n\t\tpluginsLogLevel = hclog.Error\n\tdefault:\n\t\tpluginsLogLevel = hclog.Debug\n\t}\n}\n\nfunc startCheckTicker() {\n\tlogger.Debug(logSender, \"\", \"start plugins checker\")\n\n\tgo func() {\n\t\tticker := time.NewTicker(30 * time.Second)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-Handler.done:\n\t\t\t\tlogger.Debug(logSender, \"\", \"handler done, stop plugins checker\")\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tHandler.checkCrashedPlugins()\n\t\t\t}\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "internal/plugin/searcher.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage plugin\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hashicorp/go-hclog\"\n\t\"github.com/hashicorp/go-plugin\"\n\t\"github.com/sftpgo/sdk/plugin/eventsearcher\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\ntype searcherPlugin struct {\n\tconfig    Config\n\tsearchear eventsearcher.Searcher\n\tclient    *plugin.Client\n}\n\nfunc newSearcherPlugin(config Config) (*searcherPlugin, error) {\n\tp := &searcherPlugin{\n\t\tconfig: config,\n\t}\n\tif err := p.initialize(); err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to create events searcher plugin: %v, config %+v\", err, config)\n\t\treturn nil, err\n\t}\n\treturn p, nil\n}\n\nfunc (p *searcherPlugin) exited() bool {\n\treturn p.client.Exited()\n}\n\nfunc (p *searcherPlugin) cleanup() {\n\tp.client.Kill()\n}\n\nfunc (p *searcherPlugin) initialize() error {\n\tkillProcess(p.config.Cmd)\n\tlogger.Debug(logSender, \"\", \"create new searcher plugin %q\", p.config.Cmd)\n\tsecureConfig, err := p.config.getSecureConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tclient := plugin.NewClient(&plugin.ClientConfig{\n\t\tHandshakeConfig: eventsearcher.Handshake,\n\t\tPlugins:         eventsearcher.PluginMap,\n\t\tCmd:             p.config.getCommand(),\n\t\tSkipHostEnv:     true,\n\t\tAllowedProtocols: []plugin.Protocol{\n\t\t\tplugin.ProtocolGRPC,\n\t\t},\n\t\tAutoMTLS:     p.config.AutoMTLS,\n\t\tSecureConfig: secureConfig,\n\t\tManaged:      false,\n\t\tLogger: &logger.HCLogAdapter{\n\t\t\tLogger: hclog.New(&hclog.LoggerOptions{\n\t\t\t\tName:        fmt.Sprintf(\"%v.%v\", logSender, eventsearcher.PluginName),\n\t\t\t\tLevel:       pluginsLogLevel,\n\t\t\t\tDisableTime: true,\n\t\t\t}),\n\t\t},\n\t})\n\trpcClient, err := client.Client()\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get rpc client for plugin %q: %v\", p.config.Cmd, err)\n\t\treturn err\n\t}\n\traw, err := rpcClient.Dispense(eventsearcher.PluginName)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to get plugin %v from rpc client for command %q: %v\",\n\t\t\teventsearcher.PluginName, p.config.Cmd, err)\n\t\treturn err\n\t}\n\n\tp.client = client\n\tp.searchear = raw.(eventsearcher.Searcher)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/plugin/util.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage plugin\n\nimport (\n\t\"github.com/shirou/gopsutil/v3/process\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\nfunc killProcess(processPath string) {\n\tprocs, err := process.Processes()\n\tif err != nil {\n\t\treturn\n\t}\n\tfor _, p := range procs {\n\t\tcmdLine, err := p.Exe()\n\t\tif err == nil {\n\t\t\tif cmdLine == processPath {\n\t\t\t\terr = p.Kill()\n\t\t\t\tlogger.Debug(logSender, \"\", \"killed process %v, pid %v, err %v\", cmdLine, p.Pid, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\tlogger.Debug(logSender, \"\", \"no match for plugin process %v\", processPath)\n}\n"
  },
  {
    "path": "internal/service/service.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package service allows to start and stop the SFTPGo service\npackage service\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/acme\"\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\tlogSender = \"service\"\n)\n\nvar (\n\tgraceTime int\n)\n\n// Service defines the SFTPGo service\ntype Service struct {\n\tConfigDir         string\n\tConfigFile        string\n\tLogFilePath       string\n\tLogMaxSize        int\n\tLogMaxBackups     int\n\tLogMaxAge         int\n\tPortableMode      int\n\tPortableUser      dataprovider.User\n\tLogCompress       bool\n\tLogLevel          string\n\tLogUTCTime        bool\n\tLoadDataClean     bool\n\tLoadDataFrom      string\n\tLoadDataMode      int\n\tLoadDataQuotaScan int\n\tShutdown          chan bool\n\tError             error\n}\n\nfunc (s *Service) initLogger() {\n\tvar logLevel zerolog.Level\n\tswitch s.LogLevel {\n\tcase \"info\":\n\t\tlogLevel = zerolog.InfoLevel\n\tcase \"warn\":\n\t\tlogLevel = zerolog.WarnLevel\n\tcase \"error\":\n\t\tlogLevel = zerolog.ErrorLevel\n\tdefault:\n\t\tlogLevel = zerolog.DebugLevel\n\t}\n\tif !filepath.IsAbs(s.LogFilePath) && util.IsFileInputValid(s.LogFilePath) {\n\t\ts.LogFilePath = filepath.Join(s.ConfigDir, s.LogFilePath)\n\t}\n\tlogger.InitLogger(s.LogFilePath, s.LogMaxSize, s.LogMaxBackups, s.LogMaxAge, s.LogCompress, s.LogUTCTime, logLevel)\n\tif s.PortableMode == 1 {\n\t\tlogger.EnableConsoleLogger(logLevel)\n\t\tif s.LogFilePath == \"\" {\n\t\t\tlogger.DisableLogger()\n\t\t}\n\t}\n}\n\n// Start initializes and starts the service\nfunc (s *Service) Start() error {\n\ts.initLogger()\n\tlogger.Info(logSender, \"\", \"starting SFTPGo %s, config dir: %s, config file: %s, log max size: %d log max backups: %d \"+\n\t\t\"log max age: %d log level: %s, log compress: %t, log utc time: %t, load data from: %q, grace time: %d secs\",\n\t\tversion.GetAsString(), s.ConfigDir, s.ConfigFile, s.LogMaxSize, s.LogMaxBackups, s.LogMaxAge, s.LogLevel,\n\t\ts.LogCompress, s.LogUTCTime, s.LoadDataFrom, graceTime)\n\t// in portable mode we don't read configuration from file\n\tif s.PortableMode != 1 {\n\t\terr := config.LoadConfig(s.ConfigDir, s.ConfigFile)\n\t\tif err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"error loading configuration: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\tif !config.HasServicesToStart() {\n\t\tconst infoString = \"no service configured, nothing to do\"\n\t\tlogger.Info(logSender, \"\", infoString)\n\t\tlogger.InfoToConsole(infoString)\n\t\treturn errors.New(infoString)\n\t}\n\n\tif err := s.initializeServices(); err != nil {\n\t\treturn err\n\t}\n\n\ts.startServices()\n\tgo common.Config.ExecuteStartupHook() //nolint:errcheck\n\n\treturn nil\n}\n\nfunc (s *Service) initializeServices() error {\n\tproviderConf := config.GetProviderConf()\n\tkmsConfig := config.GetKMSConfig()\n\terr := kmsConfig.Initialize()\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to initialize KMS: %v\", err)\n\t\tlogger.ErrorToConsole(\"unable to initialize KMS: %v\", err)\n\t\treturn err\n\t}\n\t// We may have KMS plugins and their schema needs to be registered before\n\t// initializing the data provider which may contain KMS secrets.\n\tif err := plugin.Initialize(config.GetPluginsConfig(), s.LogLevel); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to initialize plugin system: %v\", err)\n\t\tlogger.ErrorToConsole(\"unable to initialize plugin system: %v\", err)\n\t\treturn err\n\t}\n\tmfaConfig := config.GetMFAConfig()\n\terr = mfaConfig.Initialize()\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to initialize MFA: %v\", err)\n\t\tlogger.ErrorToConsole(\"unable to initialize MFA: %v\", err)\n\t\treturn err\n\t}\n\terr = dataprovider.Initialize(providerConf, s.ConfigDir, s.PortableMode == 0)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"error initializing data provider: %v\", err)\n\t\tlogger.ErrorToConsole(\"error initializing data provider: %v\", err)\n\t\treturn err\n\t}\n\tsmtpConfig := config.GetSMTPConfig()\n\terr = smtpConfig.Initialize(s.ConfigDir, s.PortableMode != 1)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to initialize SMTP configuration: %v\", err)\n\t\tlogger.ErrorToConsole(\"unable to initialize SMTP configuration: %v\", err)\n\t\treturn err\n\t}\n\terr = common.Initialize(config.GetCommonConfig(), providerConf.GetShared())\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"%v\", err)\n\t\tlogger.ErrorToConsole(\"%v\", err)\n\t\treturn err\n\t}\n\n\tif s.PortableMode == 1 {\n\t\t// create the user for portable mode\n\t\terr = dataprovider.AddUser(&s.PortableUser, dataprovider.ActionExecutorSystem, \"\", \"\")\n\t\tif err != nil {\n\t\t\tlogger.ErrorToConsole(\"error adding portable user: %v\", err)\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tacmeConfig := config.GetACMEConfig()\n\t\terr = acme.Initialize(acmeConfig, s.ConfigDir, true)\n\t\tif err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"error initializing ACME configuration: %v\", err)\n\t\t\tlogger.ErrorToConsole(\"error initializing ACME configuration: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\thttpConfig := config.GetHTTPConfig()\n\terr = httpConfig.Initialize(s.ConfigDir)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"error initializing http client: %v\", err)\n\t\tlogger.ErrorToConsole(\"error initializing http client: %v\", err)\n\t\treturn err\n\t}\n\tcommandConfig := config.GetCommandConfig()\n\tif err := commandConfig.Initialize(); err != nil {\n\t\tlogger.Error(logSender, \"\", \"error initializing commands configuration: %v\", err)\n\t\tlogger.ErrorToConsole(\"error initializing commands configuration: %v\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *Service) startServices() {\n\terr := s.LoadInitialData()\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to load initial data: %v\", err)\n\t\tlogger.ErrorToConsole(\"unable to load initial data: %v\", err)\n\t}\n\n\tsftpdConf := config.GetSFTPDConfig()\n\tftpdConf := config.GetFTPDConfig()\n\thttpdConf := config.GetHTTPDConfig()\n\twebDavDConf := config.GetWebDAVDConfig()\n\ttelemetryConf := config.GetTelemetryConfig()\n\n\tif sftpdConf.ShouldBind() {\n\t\tgo func() {\n\t\t\tredactedConf := sftpdConf\n\t\t\tredactedConf.KeyboardInteractiveHook = util.GetRedactedURL(sftpdConf.KeyboardInteractiveHook)\n\t\t\tlogger.Info(logSender, \"\", \"initializing SFTP server with config %+v\", redactedConf)\n\t\t\tif err := sftpdConf.Initialize(s.ConfigDir); err != nil {\n\t\t\t\tlogger.Error(logSender, \"\", \"could not start SFTP server: %v\", err)\n\t\t\t\tlogger.ErrorToConsole(\"could not start SFTP server: %v\", err)\n\t\t\t\ts.Error = err\n\t\t\t}\n\t\t\ts.Shutdown <- true\n\t\t}()\n\t} else {\n\t\tlogger.Info(logSender, \"\", \"SFTP server not started, disabled in config file\")\n\t}\n\n\tif httpdConf.ShouldBind() {\n\t\tgo func() {\n\t\t\tproviderConf := config.GetProviderConf()\n\t\t\tif err := httpdConf.Initialize(s.ConfigDir, providerConf.GetShared()); err != nil {\n\t\t\t\tlogger.Error(logSender, \"\", \"could not start HTTP server: %v\", err)\n\t\t\t\tlogger.ErrorToConsole(\"could not start HTTP server: %v\", err)\n\t\t\t\ts.Error = err\n\t\t\t}\n\t\t\ts.Shutdown <- true\n\t\t}()\n\t} else {\n\t\tlogger.Info(logSender, \"\", \"HTTP server not started, disabled in config file\")\n\t\tif s.PortableMode != 1 {\n\t\t\tlogger.InfoToConsole(\"HTTP server not started, disabled in config file\")\n\t\t}\n\t}\n\tif ftpdConf.ShouldBind() {\n\t\tgo func() {\n\t\t\tif err := ftpdConf.Initialize(s.ConfigDir); err != nil {\n\t\t\t\tlogger.Error(logSender, \"\", \"could not start FTP server: %v\", err)\n\t\t\t\tlogger.ErrorToConsole(\"could not start FTP server: %v\", err)\n\t\t\t\ts.Error = err\n\t\t\t}\n\t\t\ts.Shutdown <- true\n\t\t}()\n\t} else {\n\t\tlogger.Info(logSender, \"\", \"FTP server not started, disabled in config file\")\n\t}\n\tif webDavDConf.ShouldBind() {\n\t\tgo func() {\n\t\t\tif err := webDavDConf.Initialize(s.ConfigDir); err != nil {\n\t\t\t\tlogger.Error(logSender, \"\", \"could not start WebDAV server: %v\", err)\n\t\t\t\tlogger.ErrorToConsole(\"could not start WebDAV server: %v\", err)\n\t\t\t\ts.Error = err\n\t\t\t}\n\t\t\ts.Shutdown <- true\n\t\t}()\n\t} else {\n\t\tlogger.Info(logSender, \"\", \"WebDAV server not started, disabled in config file\")\n\t}\n\tif telemetryConf.ShouldBind() {\n\t\tgo func() {\n\t\t\tif err := telemetryConf.Initialize(s.ConfigDir); err != nil {\n\t\t\t\tlogger.Error(logSender, \"\", \"could not start telemetry server: %v\", err)\n\t\t\t\tlogger.ErrorToConsole(\"could not start telemetry server: %v\", err)\n\t\t\t\ts.Error = err\n\t\t\t}\n\t\t\ts.Shutdown <- true\n\t\t}()\n\t} else {\n\t\tlogger.Info(logSender, \"\", \"telemetry server not started, disabled in config file\")\n\t\tif s.PortableMode != 1 {\n\t\t\tlogger.InfoToConsole(\"telemetry server not started, disabled in config file\")\n\t\t}\n\t}\n}\n\n// Wait blocks until the service exits\nfunc (s *Service) Wait() {\n\tif s.PortableMode != 1 {\n\t\tregisterSignals()\n\t}\n\t<-s.Shutdown\n}\n\n// Stop terminates the service unblocking the Wait method\nfunc (s *Service) Stop() {\n\tclose(s.Shutdown)\n\tlogger.Debug(logSender, \"\", \"Service stopped\")\n}\n\n// LoadInitialData if a data file is set\nfunc (s *Service) LoadInitialData() error {\n\tif s.LoadDataFrom == \"\" {\n\t\treturn nil\n\t}\n\tif !filepath.IsAbs(s.LoadDataFrom) {\n\t\treturn fmt.Errorf(\"invalid input_file %q, it must be an absolute path\", s.LoadDataFrom)\n\t}\n\tif s.LoadDataMode < 0 || s.LoadDataMode > 1 {\n\t\treturn fmt.Errorf(\"invalid loaddata-mode %v\", s.LoadDataMode)\n\t}\n\tif s.LoadDataQuotaScan < 0 || s.LoadDataQuotaScan > 2 {\n\t\treturn fmt.Errorf(\"invalid loaddata-scan %v\", s.LoadDataQuotaScan)\n\t}\n\tinfo, err := os.Stat(s.LoadDataFrom)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to stat file %q: %w\", s.LoadDataFrom, err)\n\t}\n\tif info.Size() > httpd.MaxRestoreSize {\n\t\treturn fmt.Errorf(\"unable to restore input file %q size too big: %d/%d bytes\",\n\t\t\ts.LoadDataFrom, info.Size(), httpd.MaxRestoreSize)\n\t}\n\tcontent, err := os.ReadFile(s.LoadDataFrom)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to read input file %q: %w\", s.LoadDataFrom, err)\n\t}\n\tdump, err := dataprovider.ParseDumpData(content)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to parse file to restore %q: %w\", s.LoadDataFrom, err)\n\t}\n\terr = s.restoreDump(&dump)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogger.Info(logSender, \"\", \"data loaded from file %q mode: %v\", s.LoadDataFrom, s.LoadDataMode)\n\tlogger.InfoToConsole(\"data loaded from file %q mode: %v\", s.LoadDataFrom, s.LoadDataMode)\n\tif s.LoadDataClean {\n\t\terr = os.Remove(s.LoadDataFrom)\n\t\tif err == nil {\n\t\t\tlogger.Info(logSender, \"\", \"file %q deleted after successful load\", s.LoadDataFrom)\n\t\t\tlogger.InfoToConsole(\"file %q deleted after successful load\", s.LoadDataFrom)\n\t\t} else {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to delete file %q after successful load: %v\", s.LoadDataFrom, err)\n\t\t\tlogger.WarnToConsole(\"unable to delete file %q after successful load: %v\", s.LoadDataFrom, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Service) restoreDump(dump *dataprovider.BackupData) error {\n\terr := httpd.RestoreConfigs(dump.Configs, s.LoadDataMode, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore configs from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreIPListEntries(dump.IPLists, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore IP list entries from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreRoles(dump.Roles, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore roles from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreFolders(dump.Folders, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore folders from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreGroups(dump.Groups, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore groups from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreUsers(dump.Users, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore users from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore admins from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreAPIKeys(dump.APIKeys, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore API keys from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreShares(dump.Shares, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore API keys from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreEventActions(dump.EventActions, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore event actions from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\terr = httpd.RestoreEventRules(dump.EventRules, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem,\n\t\t\"\", \"\", dump.Version)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to restore event rules from file %q: %v\", s.LoadDataFrom, err)\n\t}\n\treturn nil\n}\n\n// SetGraceTime sets the grace time\nfunc SetGraceTime(val int) {\n\tgraceTime = val\n}\n"
  },
  {
    "path": "internal/service/service_portable.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !noportable\n\npackage service\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/ftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\n// StartPortableMode starts the service in portable mode\nfunc (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort, httpPort int, enabledSSHCommands []string,\n\tftpsCert, ftpsKey, webDavCert, webDavKey, httpsCert, httpsKey string) error {\n\tif s.PortableMode != 1 {\n\t\treturn fmt.Errorf(\"service is not configured for portable mode\")\n\t}\n\terr := config.LoadConfig(s.ConfigDir, s.ConfigFile)\n\tif err != nil {\n\t\tfmt.Printf(\"error loading configuration file: %v using defaults\\n\", err)\n\t}\n\tkmsConfig := config.GetKMSConfig()\n\terr = kmsConfig.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\tprintablePassword := s.configurePortableUser()\n\tdataProviderConf := config.GetProviderConf()\n\tdataProviderConf.Driver = dataprovider.MemoryDataProviderName\n\tdataProviderConf.Name = \"\"\n\tconfig.SetProviderConf(dataProviderConf)\n\thttpdConf := config.GetHTTPDConfig()\n\tfor idx := range httpdConf.Bindings {\n\t\thttpdConf.Bindings[idx].Port = 0\n\t}\n\tconfig.SetHTTPDConfig(httpdConf)\n\ttelemetryConf := config.GetTelemetryConfig()\n\ttelemetryConf.BindPort = 0\n\tconfig.SetTelemetryConfig(telemetryConf)\n\n\tconfigurePortableSFTPService(sftpdPort, enabledSSHCommands)\n\tconfigurePortableFTPService(ftpPort, ftpsCert, ftpsKey)\n\tconfigurePortableWebDAVService(webdavPort, webDavCert, webDavKey)\n\tconfigurePortableHTTPService(httpPort, httpsCert, httpsKey)\n\n\terr = s.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif httpPort >= 0 {\n\t\tadmin := &dataprovider.Admin{\n\t\t\tUsername:    util.GenerateUniqueID(),\n\t\t\tPassword:    util.GenerateUniqueID(),\n\t\t\tStatus:      0,\n\t\t\tPermissions: []string{dataprovider.PermAdminAny},\n\t\t}\n\t\tif err := dataprovider.AddAdmin(admin, dataprovider.ActionExecutorSystem, \"\", \"\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tlogger.InfoToConsole(\"Portable mode ready, user: %q, password: %q, public keys: %v, directory: %q, \"+\n\t\t\"permissions: %+v, file patterns filters: %+v %v\", s.PortableUser.Username,\n\t\tprintablePassword, s.PortableUser.PublicKeys, s.getPortableDirToServe(), s.PortableUser.Permissions,\n\t\ts.PortableUser.Filters.FilePatterns, s.getServiceOptionalInfoString())\n\treturn nil\n}\n\nfunc (s *Service) getServiceOptionalInfoString() string {\n\tvar info strings.Builder\n\tif config.GetSFTPDConfig().Bindings[0].IsValid() {\n\t\tfmt.Fprintf(&info, \"SFTP port: %d \", config.GetSFTPDConfig().Bindings[0].Port)\n\t}\n\tif config.GetFTPDConfig().Bindings[0].IsValid() {\n\t\tfmt.Fprintf(&info, \"FTP port: %d \", config.GetFTPDConfig().Bindings[0].Port)\n\t}\n\tif config.GetWebDAVDConfig().Bindings[0].IsValid() {\n\t\tscheme := \"http\"\n\t\tif config.GetWebDAVDConfig().CertificateFile != \"\" && config.GetWebDAVDConfig().CertificateKeyFile != \"\" {\n\t\t\tscheme = \"https\"\n\t\t}\n\t\tfmt.Fprintf(&info, \"WebDAV URL: %v://<your IP>:%v/ \", scheme, config.GetWebDAVDConfig().Bindings[0].Port)\n\t}\n\tif config.GetHTTPDConfig().Bindings[0].IsValid() {\n\t\tscheme := \"http\"\n\t\tif config.GetHTTPDConfig().CertificateFile != \"\" && config.GetHTTPDConfig().CertificateKeyFile != \"\" {\n\t\t\tscheme = \"https\"\n\t\t}\n\t\tfmt.Fprintf(&info, \"WebClient URL: %s://<your IP>:%d/ \", scheme, config.GetHTTPDConfig().Bindings[0].Port)\n\t}\n\treturn info.String()\n}\n\nfunc (s *Service) getPortableDirToServe() string {\n\tswitch s.PortableUser.FsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\treturn s.PortableUser.FsConfig.S3Config.KeyPrefix\n\tcase sdk.GCSFilesystemProvider:\n\t\treturn s.PortableUser.FsConfig.GCSConfig.KeyPrefix\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\treturn s.PortableUser.FsConfig.AzBlobConfig.KeyPrefix\n\tcase sdk.SFTPFilesystemProvider:\n\t\treturn s.PortableUser.FsConfig.SFTPConfig.Prefix\n\tcase sdk.HTTPFilesystemProvider:\n\t\treturn \"/\"\n\tdefault:\n\t\treturn s.PortableUser.HomeDir\n\t}\n}\n\n// configures the portable user and return the printable password if any\nfunc (s *Service) configurePortableUser() string {\n\tif s.PortableUser.Username == \"\" {\n\t\ts.PortableUser.Username = \"user\"\n\t}\n\tprintablePassword := \"\"\n\tif s.PortableUser.Password != \"\" {\n\t\tprintablePassword = \"[redacted]\"\n\t}\n\tif len(s.PortableUser.PublicKeys) == 0 && s.PortableUser.Password == \"\" {\n\t\ts.PortableUser.Password = util.GenerateUniqueID()\n\t\tprintablePassword = s.PortableUser.Password\n\t}\n\ts.PortableUser.Filters.WebClient = []string{sdk.WebClientSharesDisabled, sdk.WebClientInfoChangeDisabled,\n\t\tsdk.WebClientPubKeyChangeDisabled, sdk.WebClientPasswordChangeDisabled, sdk.WebClientAPIKeyAuthChangeDisabled,\n\t\tsdk.WebClientMFADisabled, sdk.WebClientPasswordResetDisabled, sdk.WebClientTLSCertChangeDisabled,\n\t}\n\tif !s.PortableUser.HasAnyPerm([]string{dataprovider.PermUpload, dataprovider.PermOverwrite}, \"/\") {\n\t\ts.PortableUser.Filters.WebClient = append(s.PortableUser.Filters.WebClient, sdk.WebClientWriteDisabled)\n\t}\n\ts.configurePortableSecrets()\n\treturn printablePassword\n}\n\nfunc (s *Service) configurePortableSecrets() {\n\t// we created the user before to initialize the KMS so we need to create the secret here\n\tswitch s.PortableUser.FsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tpayload := s.PortableUser.FsConfig.S3Config.AccessSecret.GetPayload()\n\t\ts.PortableUser.FsConfig.S3Config.AccessSecret = getSecretFromString(payload)\n\tcase sdk.GCSFilesystemProvider:\n\t\tpayload := s.PortableUser.FsConfig.GCSConfig.Credentials.GetPayload()\n\t\ts.PortableUser.FsConfig.GCSConfig.Credentials = getSecretFromString(payload)\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tpayload := s.PortableUser.FsConfig.AzBlobConfig.AccountKey.GetPayload()\n\t\ts.PortableUser.FsConfig.AzBlobConfig.AccountKey = getSecretFromString(payload)\n\t\tpayload = s.PortableUser.FsConfig.AzBlobConfig.SASURL.GetPayload()\n\t\ts.PortableUser.FsConfig.AzBlobConfig.SASURL = getSecretFromString(payload)\n\tcase sdk.CryptedFilesystemProvider:\n\t\tpayload := s.PortableUser.FsConfig.CryptConfig.Passphrase.GetPayload()\n\t\ts.PortableUser.FsConfig.CryptConfig.Passphrase = getSecretFromString(payload)\n\tcase sdk.SFTPFilesystemProvider:\n\t\tpayload := s.PortableUser.FsConfig.SFTPConfig.Password.GetPayload()\n\t\ts.PortableUser.FsConfig.SFTPConfig.Password = getSecretFromString(payload)\n\t\tpayload = s.PortableUser.FsConfig.SFTPConfig.PrivateKey.GetPayload()\n\t\ts.PortableUser.FsConfig.SFTPConfig.PrivateKey = getSecretFromString(payload)\n\t\tpayload = s.PortableUser.FsConfig.SFTPConfig.KeyPassphrase.GetPayload()\n\t\ts.PortableUser.FsConfig.SFTPConfig.KeyPassphrase = getSecretFromString(payload)\n\tcase sdk.HTTPFilesystemProvider:\n\t\tpayload := s.PortableUser.FsConfig.HTTPConfig.Password.GetPayload()\n\t\ts.PortableUser.FsConfig.HTTPConfig.Password = getSecretFromString(payload)\n\t\tpayload = s.PortableUser.FsConfig.HTTPConfig.APIKey.GetPayload()\n\t\ts.PortableUser.FsConfig.HTTPConfig.APIKey = getSecretFromString(payload)\n\t}\n}\n\nfunc getSecretFromString(payload string) *kms.Secret {\n\tif payload != \"\" {\n\t\treturn kms.NewPlainSecret(payload)\n\t}\n\treturn kms.NewEmptySecret()\n}\n\nfunc configurePortableSFTPService(port int, enabledSSHCommands []string) {\n\tsftpdConf := config.GetSFTPDConfig()\n\tif len(sftpdConf.Bindings) == 0 {\n\t\tsftpdConf.Bindings = append(sftpdConf.Bindings, sftpd.Binding{})\n\t}\n\tif port > 0 {\n\t\tsftpdConf.Bindings[0].Port = port\n\t} else if port == 0 {\n\t\t// dynamic ports starts from 49152\n\t\tsftpdConf.Bindings[0].Port = 49152 + rand.Intn(15000)\n\t} else {\n\t\tsftpdConf.Bindings[0].Port = 0\n\t}\n\tif slices.Contains(enabledSSHCommands, \"*\") {\n\t\tsftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()\n\t} else {\n\t\tsftpdConf.EnabledSSHCommands = enabledSSHCommands\n\t}\n\tconfig.SetSFTPDConfig(sftpdConf)\n}\n\nfunc configurePortableFTPService(port int, cert, key string) {\n\tftpConf := config.GetFTPDConfig()\n\tif len(ftpConf.Bindings) == 0 {\n\t\tftpConf.Bindings = append(ftpConf.Bindings, ftpd.Binding{})\n\t}\n\tif port > 0 {\n\t\tftpConf.Bindings[0].Port = port\n\t} else if port == 0 {\n\t\tftpConf.Bindings[0].Port = 49152 + rand.Intn(15000)\n\t} else {\n\t\tftpConf.Bindings[0].Port = 0\n\t}\n\tftpConf.Bindings[0].CertificateFile = cert\n\tftpConf.Bindings[0].CertificateKeyFile = key\n\tconfig.SetFTPDConfig(ftpConf)\n}\n\nfunc configurePortableWebDAVService(port int, cert, key string) {\n\twebDavConf := config.GetWebDAVDConfig()\n\tif len(webDavConf.Bindings) == 0 {\n\t\twebDavConf.Bindings = append(webDavConf.Bindings, webdavd.Binding{})\n\t}\n\tif port > 0 {\n\t\twebDavConf.Bindings[0].Port = port\n\t} else if port == 0 {\n\t\twebDavConf.Bindings[0].Port = 49152 + rand.Intn(15000)\n\t} else {\n\t\twebDavConf.Bindings[0].Port = 0\n\t}\n\twebDavConf.Bindings[0].CertificateFile = cert\n\twebDavConf.Bindings[0].CertificateKeyFile = key\n\tif cert != \"\" && key != \"\" {\n\t\twebDavConf.Bindings[0].EnableHTTPS = true\n\t}\n\tconfig.SetWebDAVDConfig(webDavConf)\n}\n\nfunc configurePortableHTTPService(port int, cert, key string) {\n\thttpdConf := config.GetHTTPDConfig()\n\tif len(httpdConf.Bindings) == 0 {\n\t\thttpdConf.Bindings = append(httpdConf.Bindings, httpd.Binding{})\n\t}\n\tif port > 0 {\n\t\thttpdConf.Bindings[0].Port = port\n\t} else if port == 0 {\n\t\thttpdConf.Bindings[0].Port = 49152 + rand.Intn(15000)\n\t} else {\n\t\thttpdConf.Bindings[0].Port = 0\n\t}\n\thttpdConf.Bindings[0].CertificateFile = cert\n\thttpdConf.Bindings[0].CertificateKeyFile = key\n\tif cert != \"\" && key != \"\" {\n\t\thttpdConf.Bindings[0].EnableHTTPS = true\n\t}\n\thttpdConf.Bindings[0].EnableWebAdmin = false\n\thttpdConf.Bindings[0].EnableWebClient = true\n\thttpdConf.Bindings[0].EnableRESTAPI = false\n\thttpdConf.Bindings[0].RenderOpenAPI = false\n\tconfig.SetHTTPDConfig(httpdConf)\n}\n"
  },
  {
    "path": "internal/service/service_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage service\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/sys/windows/svc\"\n\t\"golang.org/x/sys/windows/svc/eventlog\"\n\t\"golang.org/x/sys/windows/svc/mgr\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/ftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/telemetry\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\nconst (\n\tserviceName     = \"SFTPGo\"\n\tserviceDesc     = \"Full-featured and highly configurable file transfer server\"\n\trotateLogCmd    = svc.Cmd(128)\n\tacceptRotateLog = svc.Accepted(rotateLogCmd)\n)\n\n// Status defines service status\ntype Status uint8\n\n// Supported values for service status\nconst (\n\tStatusUnknown Status = iota\n\tStatusRunning\n\tStatusStopped\n\tStatusPaused\n\tStatusStartPending\n\tStatusPausePending\n\tStatusContinuePending\n\tStatusStopPending\n)\n\ntype WindowsService struct {\n\tService       Service\n\tisInteractive bool\n}\n\nfunc (s Status) String() string {\n\tswitch s {\n\tcase StatusRunning:\n\t\treturn \"running\"\n\tcase StatusStopped:\n\t\treturn \"stopped\"\n\tcase StatusStartPending:\n\t\treturn \"start pending\"\n\tcase StatusPausePending:\n\t\treturn \"pause pending\"\n\tcase StatusPaused:\n\t\treturn \"paused\"\n\tcase StatusContinuePending:\n\t\treturn \"continue pending\"\n\tcase StatusStopPending:\n\t\treturn \"stop pending\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\nfunc (s *WindowsService) handleExit(wasStopped chan bool) {\n\ts.Service.Wait()\n\n\tselect {\n\tcase <-wasStopped:\n\t\t// the service was stopped nothing to do\n\t\tlogger.Info(logSender, \"\", \"Windows Service was stopped\")\n\t\treturn\n\tdefault:\n\t\t// the server failed while running, we must be sure to exit the process.\n\t\t// The defined recovery action will be executed.\n\t\tlogger.Info(logSender, \"\", \"Service wait ended, error: %v\", s.Service.Error)\n\t\tif s.Service.Error == nil {\n\t\t\tos.Exit(0)\n\t\t} else {\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n}\n\nfunc (s *WindowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {\n\tchanges <- svc.Status{State: svc.StartPending}\n\n\tgo func() {\n\t\tif err := s.Service.Start(); err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"Windows service failed to start, error: %v\", err)\n\t\t\ts.Service.Error = err\n\t\t\ts.Service.Shutdown <- true\n\t\t\treturn\n\t\t}\n\t\tlogger.Info(logSender, \"\", \"Windows service started\")\n\t\tcmdsAccepted := svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange | acceptRotateLog\n\t\tchanges <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}\n\t}()\n\n\twasStopped := make(chan bool, 1)\n\n\tgo s.handleExit(wasStopped)\n\n\tchanges <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}\nloop:\n\tfor {\n\t\tc := <-r\n\t\tswitch c.Cmd {\n\t\tcase svc.Interrogate:\n\t\t\tlogger.Debug(logSender, \"\", \"Received service interrogate request, current status: %v\", c.CurrentStatus)\n\t\t\tchanges <- c.CurrentStatus\n\t\tcase svc.Stop, svc.Shutdown:\n\t\t\tlogger.Debug(logSender, \"\", \"Received service stop request\")\n\t\t\tchanges <- svc.Status{State: svc.StopPending}\n\t\t\twasStopped <- true\n\t\t\ts.Service.Stop()\n\t\t\tplugin.Handler.Cleanup()\n\t\t\tcommon.WaitForTransfers(graceTime)\n\t\t\tbreak loop\n\t\tcase svc.ParamChange:\n\t\t\tlogger.Debug(logSender, \"\", \"Received reload request\")\n\t\t\terr := dataprovider.ReloadConfig()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error reloading dataprovider configuration: %v\", err)\n\t\t\t}\n\t\t\terr = httpd.ReloadCertificateMgr()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error reloading cert manager: %v\", err)\n\t\t\t}\n\t\t\terr = ftpd.ReloadCertificateMgr()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error reloading FTPD cert manager: %v\", err)\n\t\t\t}\n\t\t\terr = webdavd.ReloadCertificateMgr()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error reloading WebDAV cert manager: %v\", err)\n\t\t\t}\n\t\t\terr = telemetry.ReloadCertificateMgr()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error reloading telemetry cert manager: %v\", err)\n\t\t\t}\n\t\t\terr = common.Reload()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error reloading common configs: %v\", err)\n\t\t\t}\n\t\t\terr = sftpd.Reload()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error reloading sftpd revoked certificates: %v\", err)\n\t\t\t}\n\t\tcase rotateLogCmd:\n\t\t\tlogger.Debug(logSender, \"\", \"Received log file rotation request\")\n\t\t\terr := logger.RotateLogFile()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error rotating log file: %v\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\tcontinue loop\n\t\t}\n\t}\n\n\treturn false, 0\n}\n\nfunc (s *WindowsService) RunService() error {\n\texePath, err := s.getExePath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tisService, err := svc.IsWindowsService()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.isInteractive = !isService\n\tdir := filepath.Dir(exePath)\n\tif err = os.Chdir(dir); err != nil {\n\t\treturn err\n\t}\n\tif s.isInteractive {\n\t\treturn s.Start()\n\t}\n\treturn svc.Run(serviceName, s)\n}\n\nfunc (s *WindowsService) Start() error {\n\tm, err := mgr.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer m.Disconnect()\n\tservice, err := m.OpenService(serviceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not access service: %v\", err)\n\t}\n\tdefer service.Close()\n\terr = service.Start()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not start service: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (s *WindowsService) Reload() error {\n\tm, err := mgr.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer m.Disconnect()\n\tservice, err := m.OpenService(serviceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not access service: %v\", err)\n\t}\n\tdefer service.Close()\n\t_, err = service.Control(svc.ParamChange)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not send control=%d: %v\", svc.ParamChange, err)\n\t}\n\treturn nil\n}\n\nfunc (s *WindowsService) RotateLogFile() error {\n\tm, err := mgr.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer m.Disconnect()\n\tservice, err := m.OpenService(serviceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not access service: %v\", err)\n\t}\n\tdefer service.Close()\n\t_, err = service.Control(rotateLogCmd)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not send control=%d: %v\", rotateLogCmd, err)\n\t}\n\treturn nil\n}\n\nfunc (s *WindowsService) Install(args ...string) error {\n\texePath, err := s.getExePath()\n\tif err != nil {\n\t\treturn err\n\t}\n\tm, err := mgr.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer m.Disconnect()\n\tservice, err := m.OpenService(serviceName)\n\tif err == nil {\n\t\tservice.Close()\n\t\treturn fmt.Errorf(\"service %s already exists\", serviceName)\n\t}\n\tconfig := mgr.Config{\n\t\tDisplayName: serviceName,\n\t\tDescription: serviceDesc,\n\t\tStartType:   mgr.StartAutomatic}\n\tservice, err = m.CreateService(serviceName, exePath, config, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer service.Close()\n\terr = eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info)\n\tif err != nil {\n\t\tif !strings.Contains(err.Error(), \"exists\") {\n\t\t\tservice.Delete()\n\t\t\treturn fmt.Errorf(\"SetupEventLogSource() failed: %s\", err)\n\t\t}\n\t}\n\trecoveryActions := []mgr.RecoveryAction{\n\t\t{\n\t\t\tType:  mgr.ServiceRestart,\n\t\t\tDelay: 5 * time.Second,\n\t\t},\n\t\t{\n\t\t\tType:  mgr.ServiceRestart,\n\t\t\tDelay: 60 * time.Second,\n\t\t},\n\t\t{\n\t\t\tType:  mgr.ServiceRestart,\n\t\t\tDelay: 90 * time.Second,\n\t\t},\n\t}\n\terr = service.SetRecoveryActions(recoveryActions, 300)\n\tif err != nil {\n\t\tservice.Delete()\n\t\treturn fmt.Errorf(\"unable to set recovery actions: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (s *WindowsService) Uninstall() error {\n\tm, err := mgr.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer m.Disconnect()\n\tservice, err := m.OpenService(serviceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"service %s is not installed\", serviceName)\n\t}\n\tdefer service.Close()\n\terr = service.Delete()\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = eventlog.Remove(serviceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"RemoveEventLogSource() failed: %s\", err)\n\t}\n\treturn nil\n}\n\nfunc (s *WindowsService) Stop() error {\n\tm, err := mgr.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer m.Disconnect()\n\tservice, err := m.OpenService(serviceName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not access service: %v\", err)\n\t}\n\tdefer service.Close()\n\tstatus, err := service.Control(svc.Stop)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not send control=%d: %v\", svc.Stop, err)\n\t}\n\ttimeout := time.Now().Add(10 * time.Second)\n\tfor status.State != svc.Stopped {\n\t\tif timeout.Before(time.Now()) {\n\t\t\treturn fmt.Errorf(\"timeout waiting for service to go to state=%d\", svc.Stopped)\n\t\t}\n\t\ttime.Sleep(300 * time.Millisecond)\n\t\tstatus, err = service.Query()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not retrieve service status: %v\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *WindowsService) Status() (Status, error) {\n\tm, err := mgr.Connect()\n\tif err != nil {\n\t\treturn StatusUnknown, err\n\t}\n\tdefer m.Disconnect()\n\tservice, err := m.OpenService(serviceName)\n\tif err != nil {\n\t\treturn StatusUnknown, fmt.Errorf(\"could not access service: %v\", err)\n\t}\n\tdefer service.Close()\n\tstatus, err := service.Query()\n\tif err != nil {\n\t\treturn StatusUnknown, fmt.Errorf(\"could not query service status: %v\", err)\n\t}\n\tswitch status.State {\n\tcase svc.StartPending:\n\t\treturn StatusStartPending, nil\n\tcase svc.Running:\n\t\treturn StatusRunning, nil\n\tcase svc.PausePending:\n\t\treturn StatusPausePending, nil\n\tcase svc.Paused:\n\t\treturn StatusPaused, nil\n\tcase svc.ContinuePending:\n\t\treturn StatusContinuePending, nil\n\tcase svc.StopPending:\n\t\treturn StatusStopPending, nil\n\tcase svc.Stopped:\n\t\treturn StatusStopped, nil\n\tdefault:\n\t\treturn StatusUnknown, fmt.Errorf(\"unknown status %v\", status)\n\t}\n}\n\nfunc (s *WindowsService) getExePath() (string, error) {\n\treturn os.Executable()\n}\n"
  },
  {
    "path": "internal/service/signals_unix.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !windows\n\npackage service\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/ftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/telemetry\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\nfunc registerSignals() {\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1)\n\tgo func() {\n\t\tfor sig := range c {\n\t\t\tswitch sig {\n\t\t\tcase syscall.SIGHUP:\n\t\t\t\thandleSIGHUP()\n\t\t\tcase syscall.SIGUSR1:\n\t\t\t\thandleSIGUSR1()\n\t\t\tcase syscall.SIGINT, syscall.SIGTERM:\n\t\t\t\thandleInterrupt()\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc handleSIGHUP() {\n\tlogger.Debug(logSender, \"\", \"Received reload request\")\n\terr := dataprovider.ReloadConfig()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error reloading dataprovider configuration: %v\", err)\n\t}\n\terr = httpd.ReloadCertificateMgr()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error reloading cert manager: %v\", err)\n\t}\n\terr = ftpd.ReloadCertificateMgr()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error reloading FTPD cert manager: %v\", err)\n\t}\n\terr = webdavd.ReloadCertificateMgr()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error reloading WebDAV cert manager: %v\", err)\n\t}\n\terr = telemetry.ReloadCertificateMgr()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error reloading telemetry cert manager: %v\", err)\n\t}\n\terr = common.Reload()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error reloading common configs: %v\", err)\n\t}\n\terr = sftpd.Reload()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error reloading sftpd revoked certificates: %v\", err)\n\t}\n}\n\nfunc handleSIGUSR1() {\n\tlogger.Debug(logSender, \"\", \"Received log file rotation request\")\n\terr := logger.RotateLogFile()\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"error rotating log file: %v\", err)\n\t}\n}\n\nfunc handleInterrupt() {\n\tlogger.Debug(logSender, \"\", \"Received interrupt request\")\n\tplugin.Handler.Cleanup()\n\tcommon.WaitForTransfers(graceTime)\n\tos.Exit(0)\n}\n"
  },
  {
    "path": "internal/service/signals_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage service\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n)\n\nfunc registerSignals() {\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt)\n\tgo func() {\n\t\tfor range c {\n\t\t\tlogger.Debug(logSender, \"\", \"Received interrupt request\")\n\t\t\tplugin.Handler.Cleanup()\n\t\t\tcommon.WaitForTransfers(graceTime)\n\t\t\tos.Exit(0)\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "internal/sftpd/cryptfs_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd_test\n\nimport (\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/minio/sio\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpdtest\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\ttestPassphrase = \"test passphrase\"\n)\n\nfunc TestBasicSFTPCryptoHandling(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUserWithCryptFs(usePubKey)\n\tu.QuotaSize = 6553600\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\tencryptedFileSize, err := getEncryptedFileSize(testFileSize)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize := user.UsedQuotaSize + encryptedFileSize\n\t\texpectedQuotaFiles := user.UsedQuotaFiles + 1\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(\"/missing_dir\", testFileName), testFileSize, client)\n\t\tassert.Error(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tinitialHash, err := computeHashForFile(sha256.New(), testFilePath)\n\t\tassert.NoError(t, err)\n\t\tdownloadedFileHash, err := computeHashForFile(sha256.New(), localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, initialHash, downloadedFileHash)\n\t\tinfo, err := os.Stat(filepath.Join(user.HomeDir, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, encryptedFileSize, info.Size())\n\t\t}\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\tresult, err := client.ReadDir(\".\")\n\t\tassert.NoError(t, err)\n\t\tif assert.Len(t, result, 1) {\n\t\t\tassert.Equal(t, testFileSize, result[0].Size())\n\t\t}\n\t\tinfo, err = client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, info.Size())\n\t\t}\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Lstat(testFileName)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestOpenReadWriteCryptoFs(t *testing.T) {\n\t// read and write is not supported on crypto fs\n\tusePubKey := false\n\tu := getTestUserWithCryptFs(usePubKey)\n\tu.QuotaSize = 6553600\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tsftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestData := []byte(\"sample test data\")\n\t\t\tn, err := sftpFile.Write(testData)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, len(testData), n)\n\t\t\tbuffer := make([]byte, 128)\n\t\t\t_, err = sftpFile.ReadAt(buffer, 1)\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t\t}\n\t\t\terr = sftpFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestEmptyFile(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUserWithCryptFs(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tsftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestData := []byte(\"\")\n\t\t\tn, err := sftpFile.Write(testData)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, len(testData), n)\n\t\t\terr = sftpFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tinfo, err := client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, int64(0), info.Size())\n\t\t}\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, 0, client)\n\t\tassert.NoError(t, err)\n\t\tencryptedFileSize, err := getEncryptedFileSize(0)\n\t\tassert.NoError(t, err)\n\t\tinfo, err = os.Stat(filepath.Join(user.HomeDir, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, encryptedFileSize, info.Size())\n\t\t}\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadResumeCryptFs(t *testing.T) {\n\t// resuming uploads is not supported\n\tusePubKey := true\n\tu := getTestUserWithCryptFs(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\tappendDataSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = appendToTestFile(testFilePath, appendDataSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadResumeFile(testFilePath, testFileName, testFileSize, false, client)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaFileReplaceCryptFs(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUserWithCryptFs(usePubKey)\n\tu.QuotaFiles = 1000\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\ttestFileSize := int64(65535)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\tencryptedFileSize, err := getEncryptedFileSize(testFileSize)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) { //nolint:dupl\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\texpectedQuotaSize := user.UsedQuotaSize + encryptedFileSize\n\t\texpectedQuotaFiles := user.UsedQuotaFiles + 1\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// now replace the same file, the quota must not change\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t// now create a symlink, replace it with a file and check the quota\n\t\t// replacing a symlink is like uploading a new file\n\t\terr = client.Symlink(testFileName, testFileName+\".link\") //nolint:goconst\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\texpectedQuotaFiles = expectedQuotaFiles + 1\n\t\texpectedQuotaSize = expectedQuotaSize + encryptedFileSize\n\t\terr = sftpUploadFile(testFilePath, testFileName+\".link\", testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t}\n\t// now set a quota size restriction and upload the same file, upload should fail for space limit exceeded\n\tuser.QuotaSize = encryptedFileSize*2 - 1\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.Error(t, err, \"quota size exceeded, file upload must fail\")\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaScanCryptFs(t *testing.T) {\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(65535)\n\tencryptedFileSize, err := getEncryptedFileSize(testFileSize)\n\tassert.NoError(t, err)\n\texpectedQuotaSize := user.UsedQuotaSize + encryptedFileSize\n\texpectedQuotaFiles := user.UsedQuotaFiles + 1\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t// create user with the same home dir, so there is at least an untracked file\n\tuser, _, err = httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\tscans, _, err := httpdtest.GetQuotaScans(http.StatusOK)\n\t\tif err == nil {\n\t\t\treturn len(scans) == 0\n\t\t}\n\t\treturn false\n\t}, 1*time.Second, 50*time.Millisecond)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestGetMimeTypeCryptFs(t *testing.T) {\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tsftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestData := []byte(\"some UTF-8 text so we should get a text/plain mime type\")\n\t\t\tn, err := sftpFile.Write(testData)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, len(testData), n)\n\t\t\terr = sftpFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(testPassphrase)\n\tfs, err := user.GetFilesystem(\"connID\")\n\tif assert.NoError(t, err) {\n\t\tassert.True(t, vfs.IsCryptOsFs(fs))\n\t\tmime, err := fs.GetMimeType(filepath.Join(user.GetHomeDir(), testFileName))\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"text/plain; charset=utf-8\", mime)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestTruncate(t *testing.T) {\n\t// truncate is not supported\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tf, err := client.OpenFile(testFileName, os.O_WRONLY|os.O_CREATE)\n\t\tif assert.NoError(t, err) {\n\t\t\terr = f.Truncate(0)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Truncate(1)\n\t\t\tassert.Error(t, err)\n\t\t}\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t\terr = client.Truncate(testFileName, 0)\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPBasicHandlingCryptoFs(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUserWithCryptFs(usePubKey)\n\tu.QuotaSize = 6553600\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(131074)\n\tencryptedFileSize, err := getEncryptedFileSize(testFileSize)\n\tassert.NoError(t, err)\n\texpectedQuotaSize := user.UsedQuotaSize + encryptedFileSize\n\texpectedQuotaFiles := user.UsedQuotaFiles + 1\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\t// test to download a missing file\n\terr = scpDownload(localPath, remoteDownPath, false, false)\n\tassert.Error(t, err, \"downloading a missing file via scp must fail\")\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(localPath, remoteDownPath, false, false)\n\tassert.NoError(t, err)\n\tfi, err := os.Stat(localPath)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, testFileSize, fi.Size())\n\t}\n\tfi, err = os.Stat(filepath.Join(user.GetHomeDir(), testFileName))\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, encryptedFileSize, fi.Size())\n\t}\n\terr = os.Remove(localPath)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t// now overwrite the existing file\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPRecursiveCryptFs(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUserWithCryptFs(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestBaseDirName := \"atestdir\"\n\ttestBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)\n\ttestBaseDirDownName := \"test_dir_down\" //nolint:goconst\n\ttestBaseDirDownPath := filepath.Join(homeBasePath, testBaseDirDownName)\n\ttestFilePath := filepath.Join(homeBasePath, testBaseDirName, testFileName)\n\ttestFilePath1 := filepath.Join(homeBasePath, testBaseDirName, testBaseDirName, testFileName)\n\ttestFileSize := int64(131074)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = createTestFile(testFilePath1, testFileSize)\n\tassert.NoError(t, err)\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testBaseDirName))\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\terr = scpUpload(testBaseDirPath, remoteUpPath, true, false)\n\tassert.NoError(t, err)\n\t// overwrite existing dir\n\terr = scpUpload(testBaseDirPath, remoteUpPath, true, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(testBaseDirDownPath, remoteDownPath, true, true)\n\tassert.NoError(t, err)\n\t// test download without passing -r\n\terr = scpDownload(testBaseDirDownPath, remoteDownPath, true, false)\n\tassert.Error(t, err, \"recursive download without -r must fail\")\n\n\tfi, err := os.Stat(filepath.Join(testBaseDirDownPath, testFileName))\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, testFileSize, fi.Size())\n\t}\n\tfi, err = os.Stat(filepath.Join(testBaseDirDownPath, testBaseDirName, testFileName))\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, testFileSize, fi.Size())\n\t}\n\t// upload to a non existent dir\n\tremoteUpPath = fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/non_existent_dir\")\n\terr = scpUpload(testBaseDirPath, remoteUpPath, true, false)\n\tassert.Error(t, err, \"uploading via scp to a non existent dir must fail\")\n\n\terr = os.RemoveAll(testBaseDirPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(testBaseDirDownPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc getEncryptedFileSize(size int64) (int64, error) {\n\tencSize, err := sio.EncryptedSize(uint64(size))\n\treturn int64(encSize) + 33, err\n}\n\nfunc getTestUserWithCryptFs(usePubKey bool) dataprovider.User {\n\tu := getTestUser(usePubKey)\n\tu.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tu.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(testPassphrase)\n\treturn u\n}\n"
  },
  {
    "path": "internal/sftpd/handler.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/sftp\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\n// Connection details for an authenticated user\ntype Connection struct {\n\t*common.BaseConnection\n\t// client's version string\n\tClientVersion string\n\t// Remote address for this connection\n\tRemoteAddr net.Addr\n\tLocalAddr  net.Addr\n\tchannel    io.ReadWriteCloser\n\tcommand    string\n}\n\n// GetClientVersion returns the connected client's version\nfunc (c *Connection) GetClientVersion() string {\n\treturn c.ClientVersion\n}\n\n// GetLocalAddress returns local connection address\nfunc (c *Connection) GetLocalAddress() string {\n\tif c.LocalAddr == nil {\n\t\treturn \"\"\n\t}\n\treturn c.LocalAddr.String()\n}\n\n// GetRemoteAddress returns the connected client's address\nfunc (c *Connection) GetRemoteAddress() string {\n\tif c.RemoteAddr == nil {\n\t\treturn \"\"\n\t}\n\treturn c.RemoteAddr.String()\n}\n\n// GetCommand returns the SSH command, if any\nfunc (c *Connection) GetCommand() string {\n\treturn c.command\n}\n\n// Fileread creates a reader for a file on the system and returns the reader back.\nfunc (c *Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {\n\tc.UpdateLastActivity()\n\tupdateRequestPaths(request)\n\n\tif !c.User.HasPerm(dataprovider.PermDownload, path.Dir(request.Filepath)) {\n\t\treturn nil, sftp.ErrSSHFxPermissionDenied\n\t}\n\tif err := common.Connections.IsNewTransferAllowed(c.User.Username); err != nil {\n\t\tc.Log(logger.LevelInfo, \"denying file read due to transfer count limits\")\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\ttransferQuota := c.GetTransferQuota()\n\tif !transferQuota.HasDownloadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file read due to quota limits\")\n\t\treturn nil, c.GetReadQuotaExceededError()\n\t}\n\n\tif ok, policy := c.User.IsFileAllowed(request.Filepath); !ok {\n\t\tc.Log(logger.LevelWarn, \"reading file %q is not allowed\", request.Filepath)\n\t\treturn nil, c.GetErrorForDeniedFile(policy)\n\t}\n\n\tfs, p, err := c.GetFsAndResolvedPath(request.Filepath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, request.Filepath, 0, 0); err != nil {\n\t\tc.Log(logger.LevelDebug, \"download for file %q denied by pre action: %v\", request.Filepath, err)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tfile, r, cancelFn, err := fs.Open(p, 0)\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"could not open file %q for reading: %+v\", p, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, p, p, request.Filepath, common.TransferDownload,\n\t\t0, 0, 0, 0, false, fs, transferQuota)\n\tt := newTransfer(baseTransfer, nil, r, nil)\n\n\treturn t, nil\n}\n\n// OpenFile implements OpenFileWriter interface\nfunc (c *Connection) OpenFile(request *sftp.Request) (sftp.WriterAtReaderAt, error) {\n\treturn c.handleFilewrite(request)\n}\n\n// Filewrite handles the write actions for a file on the system.\nfunc (c *Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {\n\treturn c.handleFilewrite(request)\n}\n\nfunc (c *Connection) handleFilewrite(request *sftp.Request) (sftp.WriterAtReaderAt, error) { //nolint:gocyclo\n\tc.UpdateLastActivity()\n\tupdateRequestPaths(request)\n\n\tif err := common.Connections.IsNewTransferAllowed(c.User.Username); err != nil {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to transfer count limits\")\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tif ok, _ := c.User.IsFileAllowed(request.Filepath); !ok {\n\t\tc.Log(logger.LevelWarn, \"writing file %q is not allowed\", request.Filepath)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tfs, p, err := c.GetFsAndResolvedPath(request.Filepath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfilePath := p\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\tfilePath = fs.GetAtomicUploadPath(p)\n\t}\n\n\tvar errForRead error\n\tif !vfs.HasOpenRWSupport(fs) && request.Pflags().Read {\n\t\t// read and write mode is only supported for local filesystem\n\t\terrForRead = sftp.ErrSSHFxOpUnsupported\n\t}\n\tif !c.User.HasPerm(dataprovider.PermDownload, path.Dir(request.Filepath)) {\n\t\t// we can try to read only for local fs here, see above.\n\t\t// os.ErrPermission will become sftp.ErrSSHFxPermissionDenied when sent to\n\t\t// the client\n\t\terrForRead = os.ErrPermission\n\t}\n\n\tstat, statErr := fs.Lstat(p)\n\tif (statErr == nil && stat.Mode()&os.ModeSymlink != 0) || fs.IsNotExist(statErr) {\n\t\tif !c.User.HasPerm(dataprovider.PermUpload, path.Dir(request.Filepath)) {\n\t\t\treturn nil, sftp.ErrSSHFxPermissionDenied\n\t\t}\n\t\treturn c.handleSFTPUploadToNewFile(fs, request.Pflags(), p, filePath, request.Filepath, errForRead)\n\t}\n\n\tif statErr != nil {\n\t\tc.Log(logger.LevelError, \"error performing file stat %q: %+v\", p, statErr)\n\t\treturn nil, c.GetFsError(fs, statErr)\n\t}\n\n\t// This happen if we upload a file that has the same name of an existing directory\n\tif stat.IsDir() {\n\t\tc.Log(logger.LevelError, \"attempted to open a directory for writing to: %q\", p)\n\t\treturn nil, sftp.ErrSSHFxOpUnsupported\n\t}\n\n\tif !c.User.HasPerm(dataprovider.PermOverwrite, path.Dir(request.Filepath)) {\n\t\treturn nil, sftp.ErrSSHFxPermissionDenied\n\t}\n\n\treturn c.handleSFTPUploadToExistingFile(fs, request.Pflags(), p, filePath, stat.Size(), request.Filepath, errForRead)\n}\n\n// Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading\n// or writing to those files.\nfunc (c *Connection) Filecmd(request *sftp.Request) error {\n\tc.UpdateLastActivity()\n\tupdateRequestPaths(request)\n\n\tswitch request.Method {\n\tcase \"Setstat\":\n\t\treturn c.handleSFTPSetstat(request)\n\tcase \"Rename\":\n\t\tif err := c.Rename(request.Filepath, request.Target); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"Rmdir\":\n\t\treturn c.RemoveDir(request.Filepath)\n\tcase \"Mkdir\":\n\t\terr := c.CreateDir(request.Filepath, true)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"Symlink\":\n\t\tif err := c.CreateSymlink(request.Filepath, request.Target); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"Remove\":\n\t\treturn c.handleSFTPRemove(request)\n\tdefault:\n\t\treturn sftp.ErrSSHFxOpUnsupported\n\t}\n\n\treturn sftp.ErrSSHFxOk\n}\n\n// Filelist is the handler for SFTP filesystem list calls. This will handle calls to list the contents of\n// a directory as well as perform file/folder stat calls.\nfunc (c *Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {\n\tc.UpdateLastActivity()\n\tupdateRequestPaths(request)\n\n\tswitch request.Method {\n\tcase \"List\":\n\t\tlister, err := c.ListDir(request.Filepath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmodTime := time.Unix(0, 0)\n\t\tif request.Filepath != \"/\" {\n\t\t\tlister.Prepend(vfs.NewFileInfo(\"..\", true, 0, modTime, false))\n\t\t}\n\t\tlister.Prepend(vfs.NewFileInfo(\".\", true, 0, modTime, false))\n\t\treturn lister, nil\n\tcase \"Stat\":\n\t\tif !c.User.HasPerm(dataprovider.PermListItems, path.Dir(request.Filepath)) {\n\t\t\treturn nil, sftp.ErrSSHFxPermissionDenied\n\t\t}\n\n\t\ts, err := c.DoStat(request.Filepath, 0, true)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn listerAt([]os.FileInfo{s}), nil\n\tdefault:\n\t\treturn nil, sftp.ErrSSHFxOpUnsupported\n\t}\n}\n\n// Readlink implements the ReadlinkFileLister interface\nfunc (c *Connection) Readlink(filePath string) (string, error) {\n\tfilePath = util.CleanPath(filePath)\n\tif err := c.canReadLink(filePath); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfs, p, err := c.GetFsAndResolvedPath(filePath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ts, err := fs.Readlink(p)\n\tif err != nil {\n\t\tc.Log(logger.LevelDebug, \"error running readlink on path %q: %+v\", p, err)\n\t\treturn \"\", c.GetFsError(fs, err)\n\t}\n\n\tif err := c.canReadLink(s); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn s, nil\n}\n\n// Lstat implements LstatFileLister interface\nfunc (c *Connection) Lstat(request *sftp.Request) (sftp.ListerAt, error) {\n\tc.UpdateLastActivity()\n\tupdateRequestPaths(request)\n\n\tif !c.User.HasPerm(dataprovider.PermListItems, path.Dir(request.Filepath)) {\n\t\treturn nil, sftp.ErrSSHFxPermissionDenied\n\t}\n\n\ts, err := c.DoStat(request.Filepath, 1, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn listerAt([]os.FileInfo{s}), nil\n}\n\n// RealPath implements the RealPathFileLister interface\nfunc (c *Connection) RealPath(p string) (string, error) {\n\tif c.User.Filters.StartDirectory == \"\" {\n\t\tp = util.CleanPath(p)\n\t} else {\n\t\tp = util.CleanPathWithBase(c.User.Filters.StartDirectory, p)\n\t}\n\tif !c.User.HasPerm(dataprovider.PermListItems, path.Dir(p)) {\n\t\treturn \"\", sftp.ErrSSHFxPermissionDenied\n\t}\n\tfs, fsPath, err := c.GetFsAndResolvedPath(p)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif realPather, ok := fs.(vfs.FsRealPather); ok {\n\t\trealPath, err := realPather.RealPath(fsPath)\n\t\tif err != nil {\n\t\t\treturn \"\", c.GetFsError(fs, err)\n\t\t}\n\t\treturn realPath, nil\n\t}\n\treturn p, nil\n}\n\n// StatVFS implements StatVFSFileCmder interface\nfunc (c *Connection) StatVFS(r *sftp.Request) (*sftp.StatVFS, error) {\n\tc.UpdateLastActivity()\n\tupdateRequestPaths(r)\n\n\t// we are assuming that r.Filepath is a dir, this could be wrong but should\n\t// not produce any side effect here.\n\t// we don't consider c.User.Filters.MaxUploadFileSize, we return disk stats here\n\t// not the limit for a single file upload\n\tquotaResult, _ := c.HasSpace(true, true, path.Join(r.Filepath, \"fakefile.txt\"))\n\n\tfs, p, err := c.GetFsAndResolvedPath(r.Filepath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !quotaResult.HasSpace {\n\t\treturn c.getStatVFSFromQuotaResult(fs, p, quotaResult)\n\t}\n\n\tif quotaResult.QuotaSize == 0 && quotaResult.QuotaFiles == 0 {\n\t\t// no quota restrictions\n\t\tstatvfs, err := fs.GetAvailableDiskSize(p)\n\t\tif err == vfs.ErrStorageSizeUnavailable {\n\t\t\treturn c.getStatVFSFromQuotaResult(fs, p, quotaResult)\n\t\t}\n\t\treturn statvfs, err\n\t}\n\n\t// there is free space but some limits are configured\n\treturn c.getStatVFSFromQuotaResult(fs, p, quotaResult)\n}\n\nfunc (c *Connection) canReadLink(name string) error {\n\tif !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {\n\t\treturn sftp.ErrSSHFxPermissionDenied\n\t}\n\tok, policy := c.User.IsFileAllowed(name)\n\tif !ok && policy == sdk.DenyPolicyHide {\n\t\treturn sftp.ErrSSHFxNoSuchFile\n\t}\n\treturn nil\n}\n\nfunc (c *Connection) handleSFTPSetstat(request *sftp.Request) error {\n\tattrs := common.StatAttributes{\n\t\tFlags: 0,\n\t}\n\tif request.Attributes() != nil {\n\t\tif request.AttrFlags().Permissions {\n\t\t\tattrs.Flags |= common.StatAttrPerms\n\t\t\tattrs.Mode = request.Attributes().FileMode()\n\t\t}\n\t\tif request.AttrFlags().UidGid {\n\t\t\tattrs.Flags |= common.StatAttrUIDGID\n\t\t\tattrs.UID = int(request.Attributes().UID)\n\t\t\tattrs.GID = int(request.Attributes().GID)\n\t\t}\n\t\tif request.AttrFlags().Acmodtime {\n\t\t\tattrs.Flags |= common.StatAttrTimes\n\t\t\tattrs.Atime = time.Unix(int64(request.Attributes().Atime), 0)\n\t\t\tattrs.Mtime = time.Unix(int64(request.Attributes().Mtime), 0)\n\t\t}\n\t\tif request.AttrFlags().Size {\n\t\t\tattrs.Flags |= common.StatAttrSize\n\t\t\tattrs.Size = int64(request.Attributes().Size)\n\t\t}\n\t}\n\n\treturn c.SetStat(request.Filepath, &attrs)\n}\n\nfunc (c *Connection) handleSFTPRemove(request *sftp.Request) error {\n\tfs, fsPath, err := c.GetFsAndResolvedPath(request.Filepath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar fi os.FileInfo\n\tif fi, err = fs.Lstat(fsPath); err != nil {\n\t\tc.Log(logger.LevelDebug, \"failed to remove file %q: stat error: %+v\", fsPath, err)\n\t\treturn c.GetFsError(fs, err)\n\t}\n\tif fi.IsDir() && fi.Mode()&os.ModeSymlink == 0 {\n\t\tc.Log(logger.LevelDebug, \"cannot remove %q is not a file/symlink\", fsPath)\n\t\treturn sftp.ErrSSHFxFailure\n\t}\n\n\treturn c.RemoveFile(fs, fsPath, request.Filepath, fi)\n}\n\nfunc (c *Connection) handleSFTPUploadToNewFile(fs vfs.Fs, pflags sftp.FileOpenFlags, resolvedPath, filePath, requestPath string, errForRead error) (sftp.WriterAtReaderAt, error) {\n\tdiskQuota, transferQuota := c.HasSpace(true, false, requestPath)\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to quota limits\")\n\t\treturn nil, c.GetQuotaExceededError()\n\t}\n\n\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {\n\t\tc.Log(logger.LevelDebug, \"upload for file %q denied by pre action: %v\", requestPath, err)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tosFlags := getOSOpenFlags(pflags)\n\tfile, w, cancelFn, err := fs.Create(filePath, osFlags, c.GetCreateChecks(requestPath, true, false))\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error creating file %q, os flags %d, pflags %+v: %+v\", resolvedPath, osFlags, pflags, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tvfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())\n\n\t// we can get an error only for resume\n\tmaxWriteSize, _ := c.GetMaxWriteSize(diskQuota, false, 0, fs.IsUploadResumeSupported())\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,\n\t\tcommon.TransferUpload, 0, 0, maxWriteSize, 0, true, fs, transferQuota)\n\tt := newTransfer(baseTransfer, w, nil, errForRead)\n\n\treturn t, nil\n}\n\nfunc (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileOpenFlags, resolvedPath, filePath string,\n\tfileSize int64, requestPath string, errForRead error) (sftp.WriterAtReaderAt, error) {\n\tvar err error\n\tdiskQuota, transferQuota := c.HasSpace(false, false, requestPath)\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to quota limits\")\n\t\treturn nil, c.GetQuotaExceededError()\n\t}\n\n\tosFlags := getOSOpenFlags(pflags)\n\tminWriteOffset := int64(0)\n\tisTruncate := osFlags&os.O_TRUNC != 0\n\t// for upload resumes OpenSSH sets the APPEND flag while WinSCP does not set it,\n\t// so we suppose this is an upload resume if the TRUNCATE flag is not set\n\tisResume := !isTruncate\n\t// if there is a size limit the remaining size cannot be 0 here, since quotaResult.HasSpace\n\t// will return false in this case and we deny the upload before.\n\t// For Cloud FS GetMaxWriteSize will return unsupported operation\n\tmaxWriteSize, err := c.GetMaxWriteSize(diskQuota, isResume, fileSize, vfs.IsUploadResumeSupported(fs, fileSize))\n\tif err != nil {\n\t\tc.Log(logger.LevelDebug, \"unable to get max write size for file %q is resume? %t: %v\",\n\t\t\trequestPath, isResume, err)\n\t\treturn nil, err\n\t}\n\n\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, osFlags); err != nil {\n\t\tc.Log(logger.LevelDebug, \"upload for file %q denied by pre action: %v\", requestPath, err)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\t_, _, err = fs.Rename(resolvedPath, filePath, 0)\n\t\tif err != nil {\n\t\t\tc.Log(logger.LevelError, \"error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v\",\n\t\t\t\tresolvedPath, filePath, err)\n\t\t\treturn nil, c.GetFsError(fs, err)\n\t\t}\n\t}\n\n\tfile, w, cancelFn, err := fs.Create(filePath, osFlags, c.GetCreateChecks(requestPath, false, isResume))\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error opening existing file, os flags %v, pflags: %+v, source: %q, err: %+v\",\n\t\t\tosFlags, pflags, filePath, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tinitialSize := int64(0)\n\ttruncatedSize := int64(0) // bytes truncated and not included in quota\n\tif isResume {\n\t\tc.Log(logger.LevelDebug, \"resuming upload requested, file path %q initial size: %d, has append flag %t\",\n\t\t\tfilePath, fileSize, pflags.Append)\n\t\t// enforce min write offset only if the client passed the APPEND flag or the filesystem\n\t\t// supports emulated resume\n\t\tif pflags.Append || !fs.IsUploadResumeSupported() {\n\t\t\tminWriteOffset = fileSize\n\t\t}\n\t\tinitialSize = fileSize\n\t} else {\n\t\tif isTruncate && vfs.HasTruncateSupport(fs) {\n\t\t\tc.updateQuotaAfterTruncate(requestPath, fileSize)\n\t\t} else {\n\t\t\tinitialSize = fileSize\n\t\t\ttruncatedSize = fileSize\n\t\t}\n\t}\n\n\tvfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,\n\t\tcommon.TransferUpload, minWriteOffset, initialSize, maxWriteSize, truncatedSize, false, fs, transferQuota)\n\tt := newTransfer(baseTransfer, w, nil, errForRead)\n\n\treturn t, nil\n}\n\n// Disconnect disconnects the client by closing the channel\nfunc (c *Connection) Disconnect() error {\n\tif c.channel == nil {\n\t\tc.Log(logger.LevelWarn, \"cannot disconnect a nil channel\")\n\t\treturn nil\n\t}\n\treturn c.channel.Close()\n}\n\nfunc (c *Connection) getStatVFSFromQuotaResult(fs vfs.Fs, name string, quotaResult vfs.QuotaCheckResult) (*sftp.StatVFS, error) {\n\ts, err := fs.GetAvailableDiskSize(name)\n\tif err == nil {\n\t\tif quotaResult.QuotaSize == 0 || quotaResult.QuotaSize > int64(s.TotalSpace()) {\n\t\t\tquotaResult.QuotaSize = int64(s.TotalSpace())\n\t\t}\n\t\tif quotaResult.QuotaFiles == 0 || quotaResult.QuotaFiles > int(s.Files) {\n\t\t\tquotaResult.QuotaFiles = int(s.Files)\n\t\t}\n\t} else if err != vfs.ErrStorageSizeUnavailable {\n\t\treturn nil, err\n\t}\n\t// if we are unable to get quota size or quota files we add some arbitrary values\n\tif quotaResult.QuotaSize == 0 {\n\t\tquotaResult.QuotaSize = quotaResult.UsedSize + 8*1024*1024*1024*1024 // 8TB\n\t}\n\tif quotaResult.QuotaFiles == 0 {\n\t\tquotaResult.QuotaFiles = quotaResult.UsedFiles + 1000000 // 1 million\n\t}\n\n\tbsize := uint64(4096)\n\tfor bsize > uint64(quotaResult.QuotaSize) {\n\t\tbsize /= 4\n\t}\n\tblocks := uint64(quotaResult.QuotaSize) / bsize\n\tbfree := uint64(quotaResult.QuotaSize-quotaResult.UsedSize) / bsize\n\tfiles := uint64(quotaResult.QuotaFiles)\n\tffree := uint64(quotaResult.QuotaFiles - quotaResult.UsedFiles)\n\tif !quotaResult.HasSpace {\n\t\tbfree = 0\n\t\tffree = 0\n\t}\n\n\treturn &sftp.StatVFS{\n\t\tBsize:   bsize,\n\t\tFrsize:  bsize,\n\t\tBlocks:  blocks,\n\t\tBfree:   bfree,\n\t\tBavail:  bfree,\n\t\tFiles:   files,\n\t\tFfree:   ffree,\n\t\tFavail:  ffree,\n\t\tNamemax: 255,\n\t}, nil\n}\n\nfunc (c *Connection) updateQuotaAfterTruncate(requestPath string, fileSize int64) {\n\tvfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))\n\tif err == nil {\n\t\tdataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -fileSize, false)\n\t\treturn\n\t}\n\tdataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck\n}\n\nfunc getOSOpenFlags(requestFlags sftp.FileOpenFlags) (flags int) {\n\tvar osFlags int\n\tif requestFlags.Read && requestFlags.Write {\n\t\tosFlags |= os.O_RDWR\n\t} else if requestFlags.Write {\n\t\tosFlags |= os.O_WRONLY\n\t}\n\t// we ignore Append flag since pkg/sftp use WriteAt that cannot work with os.O_APPEND\n\t/*if requestFlags.Append {\n\t\tosFlags |= os.O_APPEND\n\t}*/\n\tif requestFlags.Creat {\n\t\tosFlags |= os.O_CREATE\n\t}\n\tif requestFlags.Trunc {\n\t\tosFlags |= os.O_TRUNC\n\t}\n\tif requestFlags.Excl {\n\t\tosFlags |= os.O_EXCL\n\t}\n\treturn osFlags\n}\n\nfunc updateRequestPaths(request *sftp.Request) {\n\tif request.Method == \"Symlink\" {\n\t\trequest.Filepath = path.Clean(strings.ReplaceAll(request.Filepath, \"\\\\\", \"/\"))\n\t} else {\n\t\trequest.Filepath = util.CleanPath(request.Filepath)\n\t}\n\n\tif request.Target != \"\" {\n\t\trequest.Target = util.CleanPath(request.Target)\n\t}\n}\n"
  },
  {
    "path": "internal/sftpd/httpfs_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd_test\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpdtest\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\thttpFsPort            = 12345\n\tdefaultHTTPFsUsername = \"httpfs_user\"\n)\n\nvar (\n\thttpFsSocketPath = filepath.Join(os.TempDir(), \"httpfs.sock\")\n)\n\nfunc TestBasicHTTPFsHandling(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUserWithHTTPFs(usePubKey)\n\tu.QuotaSize = 6553600\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\texpectedQuotaSize := user.UsedQuotaSize + testFileSize*2\n\t\texpectedQuotaFiles := user.UsedQuotaFiles + 2\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(\"/missing_dir\", testFileName), testFileSize, client)\n\t\tassert.Error(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, info.Size())\n\t\t}\n\t\tcontents, err := client.ReadDir(\"/\")\n\t\tassert.NoError(t, err)\n\t\tif assert.Len(t, contents, 1) {\n\t\t\tassert.Equal(t, testFileName, contents[0].Name())\n\t\t}\n\t\tdirName := \"test dirname\"\n\t\terr = client.Mkdir(dirName)\n\t\tassert.NoError(t, err)\n\t\tcontents, err = client.ReadDir(\".\")\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, contents, 2)\n\t\tcontents, err = client.ReadDir(dirName)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, contents, 0)\n\t\terr = sftpUploadFile(testFilePath, path.Join(dirName, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tcontents, err = client.ReadDir(dirName)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, contents, 1)\n\t\tdirRenamed := dirName + \"_renamed\"\n\t\terr = client.Rename(dirName, dirRenamed)\n\t\tassert.NoError(t, err)\n\t\tinfo, err = client.Stat(dirRenamed)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.IsDir())\n\t\t}\n\t\t// mode 0666 and 0444 works on Windows too\n\t\tnewPerm := os.FileMode(0444)\n\t\terr = client.Chmod(testFileName, newPerm)\n\t\tassert.NoError(t, err)\n\t\tinfo, err = client.Stat(testFileName)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, newPerm, info.Mode().Perm())\n\t\tnewPerm = os.FileMode(0666)\n\t\terr = client.Chmod(testFileName, newPerm)\n\t\tassert.NoError(t, err)\n\t\tinfo, err = client.Stat(testFileName)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, newPerm, info.Mode().Perm())\n\t\t// chtimes\n\t\tacmodTime := time.Now().Add(-36 * time.Hour)\n\t\terr = client.Chtimes(testFileName, acmodTime, acmodTime)\n\t\tassert.NoError(t, err)\n\t\tinfo, err = client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\tdiff := math.Abs(info.ModTime().Sub(acmodTime).Seconds())\n\t\t\tassert.LessOrEqual(t, diff, float64(1))\n\t\t}\n\t\t_, err = client.StatVFS(\"/\")\n\t\tassert.NoError(t, err)\n\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t// execute a quota scan\n\t\t_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\tscans, _, err := httpdtest.GetQuotaScans(http.StatusOK)\n\t\t\tif err == nil {\n\t\t\t\treturn len(scans) == 0\n\t\t\t}\n\t\t\treturn false\n\t\t}, 1*time.Second, 50*time.Millisecond)\n\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Lstat(testFileName)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)\n\t\t// truncate\n\t\terr = client.Truncate(path.Join(dirRenamed, testFileName), 100)\n\t\tassert.NoError(t, err)\n\t\tinfo, err = client.Stat(path.Join(dirRenamed, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, int64(100), info.Size())\n\t\t}\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(100), user.UsedQuotaSize)\n\t\t// update quota\n\t\t_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\tscans, _, err := httpdtest.GetQuotaScans(http.StatusOK)\n\t\t\tif err == nil {\n\t\t\t\treturn len(scans) == 0\n\t\t\t}\n\t\t\treturn false\n\t\t}, 1*time.Second, 50*time.Millisecond)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(100), user.UsedQuotaSize)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPFsVirtualFolder(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tfolderName := \"httpfsfolder\"\n\tvdirPath := \"/vdir/http fs\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName: folderName,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.HTTPFilesystemProvider,\n\t\t\tHTTPConfig: vfs.HTTPFsConfig{\n\t\t\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\t\t\tEndpoint:          fmt.Sprintf(\"http://127.0.0.1:%d/api/v1\", httpFsPort),\n\t\t\t\t\tUsername:          defaultHTTPFsUsername,\n\t\t\t\t\tEqualityCheckMode: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(path.Join(vdirPath, testFileName))\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPFsWalk(t *testing.T) {\n\tuser := getTestUserWithHTTPFs(false)\n\tuser.FsConfig.HTTPConfig.EqualityCheckMode = 1\n\thttpFs, err := user.GetFilesystem(\"\")\n\trequire.NoError(t, err)\n\tbasePath := filepath.Join(os.TempDir(), \"httpfs\", user.FsConfig.HTTPConfig.Username)\n\terr = os.RemoveAll(basePath)\n\tassert.NoError(t, err)\n\n\tvar walkedPaths []string\n\terr = httpFs.Walk(\"/\", func(walkedPath string, _ fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\twalkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, walkedPaths, 1)\n\trequire.Contains(t, walkedPaths, \"/\")\n\t// now add some files/folders\n\tfor i := 0; i < 10; i++ {\n\t\terr = os.WriteFile(filepath.Join(basePath, fmt.Sprintf(\"file%d\", i)), nil, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.Mkdir(filepath.Join(basePath, fmt.Sprintf(\"dir%d\", i)), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tfor j := 0; j < 5; j++ {\n\t\t\terr = os.WriteFile(filepath.Join(basePath, fmt.Sprintf(\"dir%d\", i), fmt.Sprintf(\"subfile%d\", j)), nil, os.ModePerm)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\twalkedPaths = nil\n\terr = httpFs.Walk(\"/\", func(walkedPath string, _ fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\twalkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, walkedPaths, 71)\n\trequire.Contains(t, walkedPaths, \"/\")\n\tfor i := 0; i < 10; i++ {\n\t\trequire.Contains(t, walkedPaths, path.Join(\"/\", fmt.Sprintf(\"file%d\", i)))\n\t\trequire.Contains(t, walkedPaths, path.Join(\"/\", fmt.Sprintf(\"dir%d\", i)))\n\t\tfor j := 0; j < 5; j++ {\n\t\t\trequire.Contains(t, walkedPaths, path.Join(\"/\", fmt.Sprintf(\"dir%d\", i), fmt.Sprintf(\"subfile%d\", j)))\n\t\t}\n\t}\n\n\terr = os.RemoveAll(basePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestHTTPFsOverUNIXSocket(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"UNIX domain sockets are not supported on Windows\")\n\t}\n\tassert.Eventually(t, func() bool {\n\t\t_, err := os.Stat(httpFsSocketPath)\n\t\treturn err == nil\n\t}, 1*time.Second, 50*time.Millisecond)\n\tusePubKey := true\n\tu := getTestUserWithHTTPFs(usePubKey)\n\tu.FsConfig.HTTPConfig.Endpoint = fmt.Sprintf(\"http://unix?socket_path=%s&api_prefix=%s\",\n\t\turl.QueryEscape(httpFsSocketPath), url.QueryEscape(\"/api/v1\"))\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc getTestUserWithHTTPFs(usePubKey bool) dataprovider.User {\n\tu := getTestUser(usePubKey)\n\tu.FsConfig.Provider = sdk.HTTPFilesystemProvider\n\tu.FsConfig.HTTPConfig = vfs.HTTPFsConfig{\n\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\tEndpoint: fmt.Sprintf(\"http://127.0.0.1:%d/api/v1\", httpFsPort),\n\t\t\tUsername: defaultHTTPFsUsername,\n\t\t},\n\t}\n\treturn u\n}\n\nfunc startHTTPFs() {\n\tif runtime.GOOS != osWindows {\n\t\tgo func() {\n\t\t\tif err := httpdtest.StartTestHTTPFsOverUnixSocket(httpFsSocketPath); err != nil {\n\t\t\t\tlogger.ErrorToConsole(\"could not start HTTPfs test server over UNIX socket: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t}()\n\t}\n\tgo func() {\n\t\tif err := httpdtest.StartTestHTTPFs(httpFsPort, nil); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTPfs test server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\twaitTCPListening(fmt.Sprintf(\":%d\", httpFsPort))\n}\n"
  },
  {
    "path": "internal/sftpd/internal_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/eikenb/pipeat\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tosWindows = \"windows\"\n)\n\nvar (\n\tconfigDir = filepath.Join(\".\", \"..\", \"..\")\n)\n\ntype MockChannel struct {\n\tBuffer        *bytes.Buffer\n\tStdErrBuffer  *bytes.Buffer\n\tReadError     error\n\tWriteError    error\n\tShortWriteErr bool\n}\n\nfunc (c *MockChannel) Read(data []byte) (int, error) {\n\tif c.ReadError != nil {\n\t\treturn 0, c.ReadError\n\t}\n\treturn c.Buffer.Read(data)\n}\n\nfunc (c *MockChannel) Write(data []byte) (int, error) {\n\tif c.WriteError != nil {\n\t\treturn 0, c.WriteError\n\t}\n\tif c.ShortWriteErr {\n\t\treturn 0, nil\n\t}\n\treturn c.Buffer.Write(data)\n}\n\nfunc (c *MockChannel) Close() error {\n\treturn nil\n}\n\nfunc (c *MockChannel) CloseWrite() error {\n\treturn nil\n}\n\nfunc (c *MockChannel) SendRequest(_ string, _ bool, _ []byte) (bool, error) {\n\treturn true, nil\n}\n\nfunc (c *MockChannel) Stderr() io.ReadWriter {\n\treturn c.StdErrBuffer\n}\n\n// MockOsFs mockable OsFs\ntype MockOsFs struct {\n\tvfs.Fs\n\terr                     error\n\tstatErr                 error\n\tisAtomicUploadSupported bool\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs MockOsFs) Name() string {\n\treturn \"mockOsFs\"\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported\nfunc (MockOsFs) IsUploadResumeSupported() bool {\n\treturn false\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (MockOsFs) IsConditionalUploadResumeSupported(_ int64) bool {\n\treturn false\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported\nfunc (fs MockOsFs) IsAtomicUploadSupported() bool {\n\treturn fs.isAtomicUploadSupported\n}\n\n// Stat returns a FileInfo describing the named file\nfunc (fs MockOsFs) Stat(name string) (os.FileInfo, error) {\n\tif fs.statErr != nil {\n\t\treturn nil, fs.statErr\n\t}\n\treturn os.Stat(name)\n}\n\n// Lstat returns a FileInfo describing the named file\nfunc (fs MockOsFs) Lstat(name string) (os.FileInfo, error) {\n\tif fs.statErr != nil {\n\t\treturn nil, fs.statErr\n\t}\n\treturn os.Lstat(name)\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (fs MockOsFs) Remove(name string, _ bool) error {\n\tif fs.err != nil {\n\t\treturn fs.err\n\t}\n\treturn os.Remove(name)\n}\n\n// Rename renames (moves) source to target\nfunc (fs MockOsFs) Rename(source, target string, _ int) (int, int64, error) {\n\tif fs.err != nil {\n\t\treturn -1, -1, fs.err\n\t}\n\terr := os.Rename(source, target)\n\treturn -1, -1, err\n}\n\nfunc newMockOsFs(err, statErr error, atomicUpload bool, connectionID, rootDir string) vfs.Fs {\n\treturn &MockOsFs{\n\t\tFs:                      vfs.NewOsFs(connectionID, rootDir, \"\", nil),\n\t\terr:                     err,\n\t\tstatErr:                 statErr,\n\t\tisAtomicUploadSupported: atomicUpload,\n\t}\n}\n\nfunc TestRemoveNonexistentQuotaScan(t *testing.T) {\n\tassert.False(t, common.QuotaScans.RemoveUserQuotaScan(\"username\"))\n}\n\nfunc TestGetOSOpenFlags(t *testing.T) {\n\tvar flags sftp.FileOpenFlags\n\tflags.Write = true\n\tflags.Excl = true\n\tosFlags := getOSOpenFlags(flags)\n\tassert.NotEqual(t, 0, osFlags&os.O_WRONLY)\n\tassert.NotEqual(t, 0, osFlags&os.O_EXCL)\n\n\tflags.Append = true\n\t// append flag should be ignored to allow resume\n\tassert.NotEqual(t, 0, osFlags&os.O_WRONLY)\n\tassert.NotEqual(t, 0, osFlags&os.O_EXCL)\n}\n\nfunc TestUploadResumeInvalidOffset(t *testing.T) {\n\ttestfile := \"testfile\" //nolint:goconst\n\tfile, err := os.Create(testfile)\n\tassert.NoError(t, err)\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"testuser\",\n\t\t},\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconn := common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", user)\n\tbaseTransfer := common.NewBaseTransfer(file, conn, nil, file.Name(), file.Name(), testfile,\n\t\tcommon.TransferUpload, 10, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\ttransfer := newTransfer(baseTransfer, nil, nil, nil)\n\t_, err = transfer.WriteAt([]byte(\"test\"), 0)\n\tassert.Error(t, err, \"upload with invalid offset must fail\")\n\tif assert.Error(t, transfer.ErrTransfer) {\n\t\tassert.EqualError(t, err, transfer.ErrTransfer.Error())\n\t\tassert.Contains(t, transfer.ErrTransfer.Error(), \"invalid write offset\")\n\t}\n\n\terr = transfer.Close()\n\tif assert.Error(t, err) {\n\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxFailure)\n\t}\n\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n}\n\nfunc TestReadWriteErrors(t *testing.T) {\n\ttestfile := \"testfile\"\n\tfile, err := os.Create(testfile)\n\tassert.NoError(t, err)\n\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"testuser\",\n\t\t},\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconn := common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", user)\n\tbaseTransfer := common.NewBaseTransfer(file, conn, nil, file.Name(), file.Name(), testfile, common.TransferDownload,\n\t\t0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\ttransfer := newTransfer(baseTransfer, nil, nil, nil)\n\terr = file.Close()\n\tassert.NoError(t, err)\n\t_, err = transfer.WriteAt([]byte(\"test\"), 0)\n\tassert.Error(t, err, \"writing to closed file must fail\")\n\tbuf := make([]byte, 32768)\n\t_, err = transfer.ReadAt(buf, 0)\n\tassert.Error(t, err, \"reading from a closed file must fail\")\n\terr = transfer.Close()\n\tassert.Error(t, err)\n\n\tr, _, err := pipeat.Pipe()\n\tassert.NoError(t, err)\n\tbaseTransfer = common.NewBaseTransfer(nil, conn, nil, file.Name(), file.Name(), testfile, common.TransferDownload,\n\t\t0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\ttransfer = newTransfer(baseTransfer, nil, vfs.NewPipeReader(r), nil)\n\terr = transfer.Close()\n\tassert.NoError(t, err)\n\t_, err = transfer.ReadAt(buf, 0)\n\tassert.Error(t, err, \"reading from a closed pipe must fail\")\n\n\tr, w, err := pipeat.Pipe()\n\tassert.NoError(t, err)\n\tpipeWriter := vfs.NewPipeWriter(w)\n\tbaseTransfer = common.NewBaseTransfer(nil, conn, nil, file.Name(), file.Name(), testfile, common.TransferDownload,\n\t\t0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\ttransfer = newTransfer(baseTransfer, pipeWriter, nil, nil)\n\n\terr = r.Close()\n\tassert.NoError(t, err)\n\terrFake := fmt.Errorf(\"fake upload error\")\n\tgo func() {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tpipeWriter.Done(errFake)\n\t}()\n\terr = transfer.closeIO()\n\tassert.EqualError(t, err, errFake.Error())\n\t_, err = transfer.WriteAt([]byte(\"test\"), 0)\n\tassert.Error(t, err, \"writing to closed pipe must fail\")\n\terr = transfer.BaseTransfer.Close()\n\tassert.EqualError(t, err, errFake.Error())\n\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n\tassert.Len(t, conn.GetTransfers(), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestUnsupportedListOP(t *testing.T) {\n\tconn := common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", dataprovider.User{})\n\tsftpConn := Connection{\n\t\tBaseConnection: conn,\n\t}\n\trequest := sftp.NewRequest(\"Unsupported\", \"\")\n\t_, err := sftpConn.Filelist(request)\n\tassert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error())\n}\n\nfunc TestTransferCancelFn(t *testing.T) {\n\ttestfile := \"testfile\"\n\tfile, err := os.Create(testfile)\n\tassert.NoError(t, err)\n\tisCancelled := false\n\tcancelFn := func() {\n\t\tisCancelled = true\n\t}\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"testuser\",\n\t\t},\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconn := common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", user)\n\tbaseTransfer := common.NewBaseTransfer(file, conn, cancelFn, file.Name(), file.Name(), testfile, common.TransferDownload,\n\t\t0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\ttransfer := newTransfer(baseTransfer, nil, nil, nil)\n\n\terrFake := errors.New(\"fake error, this will trigger cancelFn\")\n\ttransfer.TransferError(errFake)\n\terr = transfer.Close()\n\tif assert.Error(t, err) {\n\t\tassert.ErrorIs(t, err, sftp.ErrSSHFxFailure)\n\t}\n\tif assert.Error(t, transfer.ErrTransfer) {\n\t\tassert.EqualError(t, transfer.ErrTransfer, errFake.Error())\n\t}\n\tassert.True(t, isCancelled, \"cancelFn not called!\")\n\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadFiles(t *testing.T) {\n\tcommon.Config.UploadMode = common.UploadModeAtomic\n\tfs := vfs.NewOsFs(\"123\", os.TempDir(), \"\", nil)\n\tu := dataprovider.User{}\n\tc := Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", u),\n\t}\n\tvar flags sftp.FileOpenFlags\n\tflags.Write = true\n\tflags.Trunc = true\n\t_, err := c.handleSFTPUploadToExistingFile(fs, flags, \"missing_path\", \"other_missing_path\", 0, \"/missing_path\", nil)\n\tassert.Error(t, err, \"upload to existing file must fail if one or both paths are invalid\")\n\n\tcommon.Config.UploadMode = common.UploadModeStandard\n\t_, err = c.handleSFTPUploadToExistingFile(fs, flags, \"missing_path\", \"other_missing_path\", 0, \"/missing_path\", nil)\n\tassert.Error(t, err, \"upload to existing file must fail if one or both paths are invalid\")\n\n\tmissingFile := \"missing/relative/file.txt\"\n\tif runtime.GOOS == osWindows {\n\t\tmissingFile = \"missing\\\\relative\\\\file.txt\"\n\t}\n\t_, err = c.handleSFTPUploadToNewFile(fs, flags, \".\", missingFile, \"/missing\", nil)\n\tassert.Error(t, err, \"upload new file in missing path must fail\")\n\n\tfs = newMockOsFs(nil, nil, false, \"123\", os.TempDir())\n\tf, err := os.CreateTemp(\"\", \"temp\")\n\tassert.NoError(t, err)\n\terr = f.Close()\n\tassert.NoError(t, err)\n\n\ttr, err := c.handleSFTPUploadToExistingFile(fs, flags, f.Name(), f.Name(), 123, f.Name(), nil)\n\tif assert.NoError(t, err) {\n\t\ttransfer := tr.(*transfer)\n\t\ttransfers := c.GetTransfers()\n\t\tif assert.Equal(t, 1, len(transfers)) {\n\t\t\tassert.Equal(t, transfers[0].ID, transfer.GetID())\n\t\t\tassert.Equal(t, int64(123), transfer.InitialSize)\n\t\t\terr = transfer.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 0, len(c.GetTransfers()))\n\t\t}\n\t}\n\terr = os.Remove(f.Name())\n\tassert.NoError(t, err)\n\tcommon.Config.UploadMode = common.UploadModeAtomicWithResume\n}\n\nfunc TestWithInvalidHome(t *testing.T) {\n\tu := dataprovider.User{}\n\tu.HomeDir = \"home_rel_path\" //nolint:goconst\n\t_, err := loginUser(&u, dataprovider.LoginMethodPassword, \"\", nil)\n\tassert.Error(t, err, \"login a user with an invalid home_dir must fail\")\n\n\tu.HomeDir = os.TempDir()\n\tfs, err := u.GetFilesystem(\"123\")\n\tassert.NoError(t, err)\n\tc := Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", u),\n\t}\n\tresolved, err := fs.ResolvePath(\"../upper_path\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Join(u.HomeDir, \"upper_path\"), resolved)\n\t_, err = c.StatVFS(&sftp.Request{\n\t\tMethod:   \"StatVFS\",\n\t\tFilepath: \"../unresolvable-path\",\n\t})\n\tassert.Error(t, err)\n}\n\nfunc TestResolveWithRootDir(t *testing.T) {\n\tu := dataprovider.User{}\n\tif runtime.GOOS == osWindows {\n\t\tu.HomeDir = \"C:\\\\\"\n\t} else {\n\t\tu.HomeDir = \"/\"\n\t}\n\tfs, err := u.GetFilesystem(\"\")\n\tassert.NoError(t, err)\n\trel, err := filepath.Rel(u.HomeDir, os.TempDir())\n\tassert.NoError(t, err)\n\tp, err := fs.ResolvePath(rel)\n\tassert.NoError(t, err, \"path %v\", p)\n}\n\nfunc TestSFTPGetUsedQuota(t *testing.T) {\n\tu := dataprovider.User{}\n\tu.HomeDir = \"home_rel_path\"\n\tu.Username = \"test_invalid_user\"\n\tu.QuotaSize = 4096\n\tu.QuotaFiles = 1\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tconnection := Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", u),\n\t}\n\tquotaResult, _ := connection.HasSpace(false, false, \"/\")\n\tassert.False(t, quotaResult.HasSpace)\n}\n\nfunc TestSupportedSSHCommands(t *testing.T) {\n\tcmds := GetSupportedSSHCommands()\n\tassert.Equal(t, len(supportedSSHCommands), len(cmds))\n\n\tfor _, c := range cmds {\n\t\tassert.True(t, slices.Contains(supportedSSHCommands, c))\n\t}\n}\n\nfunc TestSSHCommandPath(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t}\n\tconnection := &Connection{\n\t\tchannel:        &mockSSHChannel,\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSSH, \"\", \"\", dataprovider.User{}),\n\t}\n\tsshCommand := sshCommand{\n\t\tcommand:    \"test\",\n\t\tconnection: connection,\n\t\targs:       []string{},\n\t}\n\tassert.Equal(t, \"\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \"/tmp/../path\"}\n\tassert.Equal(t, \"/path\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \"/tmp/\"}\n\tassert.Equal(t, \"/tmp/\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \"tmp/\"}\n\tassert.Equal(t, \"/tmp/\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \"/tmp/../../../path\"}\n\tassert.Equal(t, \"/path\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \"..\"}\n\tassert.Equal(t, \"/\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \".\"}\n\tassert.Equal(t, \"/\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \"//\"}\n\tassert.Equal(t, \"/\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \"../..\"}\n\tassert.Equal(t, \"/\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-t\", \"/..\"}\n\tassert.Equal(t, \"/\", sshCommand.getDestPath())\n\n\tsshCommand.args = []string{\"-f\", \"/a space.txt\"}\n\tassert.Equal(t, \"/a space.txt\", sshCommand.getDestPath())\n}\n\nfunc TestSSHParseCommandPayload(t *testing.T) {\n\tcmd := \"command -a  -f  /ab\\\\ à/some\\\\ spaces\\\\ \\\\ \\\\(\\\\).txt\"\n\tname, args, _ := parseCommandPayload(cmd)\n\tassert.Equal(t, \"command\", name)\n\tassert.Equal(t, 3, len(args))\n\tassert.Equal(t, \"/ab à/some spaces  ().txt\", args[2])\n\n\t_, _, err := parseCommandPayload(\"\")\n\tassert.Error(t, err, \"parsing invalid command must fail\")\n}\n\nfunc TestSSHCommandErrors(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\treadErr := fmt.Errorf(\"test read error\")\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t}\n\tserver, client := net.Pipe()\n\tdefer func() {\n\t\terr := server.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\tdefer func() {\n\t\terr := client.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\tuser := dataprovider.User{}\n\tuser.Permissions = map[string][]string{\n\t\t\"/\": {dataprovider.PermAny},\n\t}\n\tconnection := Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSSH, \"\", \"\", user),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tcmd := sshCommand{\n\t\tcommand:    \"md5sum\",\n\t\tconnection: &connection,\n\t\targs:       []string{},\n\t}\n\terr := cmd.handle()\n\tassert.Error(t, err, \"ssh command must fail, we are sending a fake error\")\n\n\tcmd = sshCommand{\n\t\tcommand:    \"md5sum\",\n\t\tconnection: &connection,\n\t\targs:       []string{\"/../../test_file_ftp.dat\"},\n\t}\n\terr = cmd.handle()\n\tassert.Error(t, err, \"ssh command must fail, we are requesting an invalid path\")\n\n\tuser = dataprovider.User{}\n\tuser.Permissions = map[string][]string{\n\t\t\"/\": {dataprovider.PermAny},\n\t}\n\tuser.HomeDir = filepath.Clean(os.TempDir())\n\tuser.QuotaFiles = 1\n\tuser.UsedQuotaFiles = 2\n\tcmd.connection.User = user\n\t_, err = cmd.connection.User.GetFilesystem(\"123\")\n\tassert.NoError(t, err)\n\n\tcmd = sshCommand{\n\t\tcommand:    \"sftpgo-remove\",\n\t\tconnection: &connection,\n\t\targs:       []string{\"/../../src\"},\n\t}\n\terr = cmd.handle()\n\tassert.Error(t, err, \"ssh command must fail, we are requesting an invalid path\")\n\n\tcmd = sshCommand{\n\t\tcommand:    \"sftpgo-copy\",\n\t\tconnection: &connection,\n\t\targs:       []string{\"/../../test_src\", \".\"},\n\t}\n\terr = cmd.handle()\n\tassert.Error(t, err, \"ssh command must fail, we are requesting an invalid path\")\n\n\terr = common.Initialize(common.Config, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestCommandsWithExtensionsFilter(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t}\n\tserver, client := net.Pipe()\n\tdefer server.Close()\n\tdefer client.Close()\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"test\",\n\t\t\tHomeDir:  os.TempDir(),\n\t\t\tStatus:   1,\n\t\t},\n\t}\n\tuser.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/subdir\",\n\t\t\tAllowedPatterns: []string{\".jpg\"},\n\t\t\tDeniedPatterns:  []string{},\n\t\t},\n\t}\n\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSSH, \"\", \"\", user),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tcmd := sshCommand{\n\t\tcommand:    \"md5sum\",\n\t\tconnection: connection,\n\t\targs:       []string{\"subdir/test.png\"},\n\t}\n\terr := cmd.handleHashCommands()\n\tassert.EqualError(t, err, common.ErrPermissionDenied.Error())\n}\n\nfunc TestSSHCommandsRemoteFs(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t}\n\tuser := dataprovider.User{}\n\tuser.FsConfig = vfs.Filesystem{\n\t\tProvider: sdk.S3FilesystemProvider,\n\t\tS3Config: vfs.S3FsConfig{\n\t\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\t\tBucket:   \"s3bucket\",\n\t\t\t\tEndpoint: \"endpoint\",\n\t\t\t\tRegion:   \"eu-west-1\",\n\t\t\t},\n\t\t},\n\t}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", user),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tcmd := sshCommand{\n\t\tcommand:    \"md5sum\",\n\t\tconnection: connection,\n\t\targs:       []string{},\n\t}\n\n\terr := cmd.handleSFTPGoCopy()\n\tassert.Error(t, err)\n\tcmd = sshCommand{\n\t\tcommand:    \"sftpgo-remove\",\n\t\tconnection: connection,\n\t\targs:       []string{},\n\t}\n\terr = cmd.handleSFTPGoRemove()\n\tassert.Error(t, err)\n}\n\nfunc TestSSHCmdGetFsErrors(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t}\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: \"relative path\",\n\t\t},\n\t}\n\tuser.Permissions = map[string][]string{}\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", user),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tcmd := sshCommand{\n\t\tcommand:    \"sftpgo-remove\",\n\t\tconnection: connection,\n\t\targs:       []string{\"path\"},\n\t}\n\terr := cmd.handleSFTPGoRemove()\n\tassert.Error(t, err)\n\n\tcmd = sshCommand{\n\t\tcommand:    \"sftpgo-copy\",\n\t\tconnection: connection,\n\t\targs:       []string{\"path1\", \"path2\"},\n\t}\n\terr = cmd.handleSFTPGoCopy()\n\tassert.Error(t, err)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCommandGetFsError(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t},\n\t}\n\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t}\n\tconn := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", user),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: conn,\n\t\t\targs:       []string{\"-t\", \"/tmp\"},\n\t\t},\n\t}\n\n\terr := scpCommand.handleRecursiveUpload()\n\tassert.Error(t, err)\n\terr = scpCommand.handleDownload(\"\")\n\tassert.Error(t, err)\n}\n\nfunc TestSCPFileMode(t *testing.T) {\n\tmode := getFileModeAsString(0, true)\n\tassert.Equal(t, \"0755\", mode)\n\n\tmode = getFileModeAsString(0700, true)\n\tassert.Equal(t, \"0700\", mode)\n\n\tmode = getFileModeAsString(0750, true)\n\tassert.Equal(t, \"0750\", mode)\n\n\tmode = getFileModeAsString(0777, true)\n\tassert.Equal(t, \"0777\", mode)\n\n\tmode = getFileModeAsString(0640, false)\n\tassert.Equal(t, \"0640\", mode)\n\n\tmode = getFileModeAsString(0600, false)\n\tassert.Equal(t, \"0600\", mode)\n\n\tmode = getFileModeAsString(0, false)\n\tassert.Equal(t, \"0644\", mode)\n\n\tfileMode := uint32(0777)\n\tfileMode = fileMode | uint32(os.ModeSetgid)\n\tfileMode = fileMode | uint32(os.ModeSetuid)\n\tfileMode = fileMode | uint32(os.ModeSticky)\n\tmode = getFileModeAsString(os.FileMode(fileMode), false)\n\tassert.Equal(t, \"7777\", mode)\n\n\tfileMode = uint32(0644)\n\tfileMode = fileMode | uint32(os.ModeSetgid)\n\tmode = getFileModeAsString(os.FileMode(fileMode), false)\n\tassert.Equal(t, \"4644\", mode)\n\n\tfileMode = uint32(0600)\n\tfileMode = fileMode | uint32(os.ModeSetuid)\n\tmode = getFileModeAsString(os.FileMode(fileMode), false)\n\tassert.Equal(t, \"2600\", mode)\n\n\tfileMode = uint32(0044)\n\tfileMode = fileMode | uint32(os.ModeSticky)\n\tmode = getFileModeAsString(os.FileMode(fileMode), false)\n\tassert.Equal(t, \"1044\", mode)\n}\n\nfunc TestSCPUploadError(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\twriteErr := fmt.Errorf(\"test write error\")\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   writeErr,\n\t}\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir:     filepath.Join(os.TempDir()),\n\t\t\tPermissions: make(map[string][]string),\n\t\t},\n\t}\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", user),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-t\", \"/\"},\n\t\t},\n\t}\n\terr := scpCommand.handle()\n\tassert.EqualError(t, err, writeErr.Error())\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer([]byte(\"D0755 0 testdir\\n\")),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   writeErr,\n\t}\n\terr = scpCommand.handleRecursiveUpload()\n\tassert.EqualError(t, err, writeErr.Error())\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer([]byte(\"D0755 a testdir\\n\")),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   nil,\n\t}\n\terr = scpCommand.handleRecursiveUpload()\n\tassert.Error(t, err)\n}\n\nfunc TestSCPInvalidEndDir(t *testing.T) {\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer([]byte(\"E\\n\")),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tHomeDir: os.TempDir(),\n\t\t\t},\n\t\t}),\n\t\tchannel: &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-t\", \"/tmp\"},\n\t\t},\n\t}\n\terr := scpCommand.handleRecursiveUpload()\n\tassert.EqualError(t, err, \"unacceptable end dir command\")\n}\n\nfunc TestSCPParseUploadMessage(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tHomeDir: os.TempDir(),\n\t\t\t},\n\t\t}),\n\t\tchannel: &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-t\", \"/tmp\"},\n\t\t},\n\t}\n\t_, _, err := scpCommand.parseUploadMessage(fs, \"invalid\")\n\tassert.Error(t, err, \"parsing invalid upload message must fail\")\n\n\t_, _, err = scpCommand.parseUploadMessage(fs, \"D0755 0\")\n\tassert.Error(t, err, \"parsing incomplete upload message must fail\")\n\n\t_, _, err = scpCommand.parseUploadMessage(fs, \"D0755 invalidsize testdir\")\n\tassert.Error(t, err, \"parsing upload message with invalid size must fail\")\n\n\t_, _, err = scpCommand.parseUploadMessage(fs, \"D0755 0 \")\n\tassert.Error(t, err, \"parsing upload message with invalid name must fail\")\n}\n\nfunc TestSCPProtocolMessages(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\treadErr := fmt.Errorf(\"test read error\")\n\twriteErr := fmt.Errorf(\"test write error\")\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   writeErr,\n\t}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", dataprovider.User{}),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-t\", \"/tmp\"},\n\t\t},\n\t}\n\t_, err := scpCommand.readProtocolMessage()\n\tassert.EqualError(t, err, readErr.Error())\n\n\terr = scpCommand.sendConfirmationMessage()\n\tassert.EqualError(t, err, writeErr.Error())\n\n\terr = scpCommand.sendProtocolMessage(\"E\\n\")\n\tassert.EqualError(t, err, writeErr.Error())\n\n\t_, err = scpCommand.getNextUploadProtocolMessage()\n\tassert.EqualError(t, err, readErr.Error())\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer([]byte(\"T1183832947 0 1183833773 0\\n\")),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   writeErr,\n\t}\n\tscpCommand.connection.channel = &mockSSHChannel\n\t_, err = scpCommand.getNextUploadProtocolMessage()\n\tassert.EqualError(t, err, writeErr.Error())\n\n\trespBuffer := []byte{0x02}\n\tprotocolErrorMsg := \"protocol error msg\"\n\trespBuffer = append(respBuffer, protocolErrorMsg...)\n\trespBuffer = append(respBuffer, 0x0A)\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(respBuffer),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   nil,\n\t}\n\tscpCommand.connection.channel = &mockSSHChannel\n\terr = scpCommand.readConfirmationMessage()\n\tif assert.Error(t, err) {\n\t\tassert.Equal(t, protocolErrorMsg, err.Error())\n\t}\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(respBuffer),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   writeErr,\n\t}\n\tscpCommand.connection.channel = &mockSSHChannel\n\n\terr = scpCommand.downloadDirs(nil, nil)\n\tassert.ErrorIs(t, err, writeErr)\n}\n\nfunc TestSCPTestDownloadProtocolMessages(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\treadErr := fmt.Errorf(\"test read error\")\n\twriteErr := fmt.Errorf(\"test write error\")\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   writeErr,\n\t}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", dataprovider.User{}),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-f\", \"-p\", \"/tmp\"},\n\t\t},\n\t}\n\tpath := \"testDir\"\n\terr := os.Mkdir(path, os.ModePerm)\n\tassert.NoError(t, err)\n\tstat, err := os.Stat(path)\n\tassert.NoError(t, err)\n\terr = scpCommand.sendDownloadProtocolMessages(path, stat)\n\tassert.EqualError(t, err, writeErr.Error())\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   nil,\n\t}\n\n\terr = scpCommand.sendDownloadProtocolMessages(path, stat)\n\tassert.EqualError(t, err, readErr.Error())\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   writeErr,\n\t}\n\tscpCommand.args = []string{\"-f\", \"/tmp\"}\n\tscpCommand.connection.channel = &mockSSHChannel\n\terr = scpCommand.sendDownloadProtocolMessages(path, stat)\n\tassert.EqualError(t, err, writeErr.Error())\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   nil,\n\t}\n\tscpCommand.connection.channel = &mockSSHChannel\n\terr = scpCommand.sendDownloadProtocolMessages(path, stat)\n\tassert.EqualError(t, err, readErr.Error())\n\n\terr = os.Remove(path)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPCommandHandleErrors(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\treadErr := fmt.Errorf(\"test read error\")\n\twriteErr := fmt.Errorf(\"test write error\")\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   writeErr,\n\t}\n\tserver, client := net.Pipe()\n\tdefer func() {\n\t\terr := server.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\tdefer func() {\n\t\terr := client.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", dataprovider.User{}),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-f\", \"/tmp\"},\n\t\t},\n\t}\n\terr := scpCommand.handle()\n\tassert.EqualError(t, err, readErr.Error())\n\tscpCommand.args = []string{\"-i\", \"/tmp\"}\n\terr = scpCommand.handle()\n\tassert.Error(t, err, \"invalid scp command must fail\")\n}\n\nfunc TestSCPErrorsMockFs(t *testing.T) {\n\terrFake := errors.New(\"fake error\")\n\tu := dataprovider.User{}\n\tu.Username = \"test\"\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.HomeDir = os.TempDir()\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t}\n\tserver, client := net.Pipe()\n\tdefer func() {\n\t\terr := server.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\tdefer func() {\n\t\terr := client.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\tconnection := &Connection{\n\t\tchannel:        &mockSSHChannel,\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", u),\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-r\", \"-t\", \"/tmp\"},\n\t\t},\n\t}\n\ttestfile := filepath.Join(u.HomeDir, \"testfile\")\n\terr := os.WriteFile(testfile, []byte(\"test\"), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tfs := newMockOsFs(errFake, nil, true, \"123\", os.TempDir())\n\terr = scpCommand.handleUploadFile(fs, testfile, testfile, 0, false, 4, \"/testfile\")\n\tassert.NoError(t, err)\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPRecursiveDownloadErrors(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\treadErr := fmt.Errorf(\"test read error\")\n\twriteErr := fmt.Errorf(\"test write error\")\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   writeErr,\n\t}\n\tserver, client := net.Pipe()\n\tdefer func() {\n\t\terr := server.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\tdefer func() {\n\t\terr := client.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\tfs := vfs.NewOsFs(\"123\", os.TempDir(), \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tHomeDir: os.TempDir(),\n\t\t\t},\n\t\t}),\n\t\tchannel: &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-r\", \"-f\", \"/tmp\"},\n\t\t},\n\t}\n\tpath := \"testDir\"\n\terr := os.Mkdir(path, os.ModePerm)\n\tassert.NoError(t, err)\n\tstat, err := os.Stat(path)\n\tassert.NoError(t, err)\n\terr = scpCommand.handleRecursiveDownload(fs, \"invalid_dir\", \"invalid_dir\", stat)\n\tassert.EqualError(t, err, writeErr.Error())\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   nil,\n\t}\n\tscpCommand.connection.channel = &mockSSHChannel\n\terr = scpCommand.handleRecursiveDownload(fs, \"invalid_dir\", \"invalid_dir\", stat)\n\tassert.Error(t, err, \"recursive upload download must fail for a non existing dir\")\n\n\terr = os.Remove(path)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPRecursiveUploadErrors(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\treadErr := fmt.Errorf(\"test read error\")\n\twriteErr := fmt.Errorf(\"test write error\")\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   writeErr,\n\t}\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", dataprovider.User{}),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-r\", \"-t\", \"/tmp\"},\n\t\t},\n\t}\n\terr := scpCommand.handleRecursiveUpload()\n\tassert.Error(t, err, \"recursive upload must fail, we send a fake error message\")\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   nil,\n\t}\n\tscpCommand.connection.channel = &mockSSHChannel\n\terr = scpCommand.handleRecursiveUpload()\n\tassert.Error(t, err, \"recursive upload must fail, we send a fake error message\")\n}\n\nfunc TestSCPCreateDirs(t *testing.T) {\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tu := dataprovider.User{}\n\tu.HomeDir = \"home_rel_path\"\n\tu.Username = \"test\"\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   nil,\n\t}\n\tfs, err := u.GetFilesystem(\"123\")\n\tassert.NoError(t, err)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", u),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-r\", \"-t\", \"/tmp\"},\n\t\t},\n\t}\n\terr = scpCommand.handleCreateDir(fs, \"invalid_dir\")\n\tassert.Error(t, err, \"create invalid dir must fail\")\n}\n\nfunc TestSCPDownloadFileData(t *testing.T) {\n\ttestfile := \"testfile\"\n\tbuf := make([]byte, 65535)\n\treadErr := fmt.Errorf(\"test read error\")\n\twriteErr := fmt.Errorf(\"test write error\")\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannelReadErr := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   nil,\n\t}\n\tmockSSHChannelWriteErr := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   writeErr,\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}}),\n\t\tchannel:        &mockSSHChannelReadErr,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-r\", \"-f\", \"/tmp\"},\n\t\t},\n\t}\n\terr := os.WriteFile(testfile, []byte(\"test\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tstat, err := os.Stat(testfile)\n\tassert.NoError(t, err)\n\terr = scpCommand.sendDownloadFileData(fs, testfile, stat, nil)\n\tassert.EqualError(t, err, readErr.Error())\n\n\tscpCommand.connection.channel = &mockSSHChannelWriteErr\n\terr = scpCommand.sendDownloadFileData(fs, testfile, stat, nil)\n\tassert.EqualError(t, err, writeErr.Error())\n\n\tscpCommand.args = []string{\"-r\", \"-p\", \"-f\", \"/tmp\"}\n\terr = scpCommand.sendDownloadFileData(fs, testfile, stat, nil)\n\tassert.EqualError(t, err, writeErr.Error())\n\n\tscpCommand.connection.channel = &mockSSHChannelReadErr\n\terr = scpCommand.sendDownloadFileData(fs, testfile, stat, nil)\n\tassert.EqualError(t, err, readErr.Error())\n\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPUploadFiledata(t *testing.T) {\n\ttestfile := \"testfile\"\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\treadErr := fmt.Errorf(\"test read error\")\n\twriteErr := fmt.Errorf(\"test write error\")\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   writeErr,\n\t}\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"testuser\",\n\t\t},\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", user),\n\t\tchannel:        &mockSSHChannel,\n\t}\n\tscpCommand := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t\targs:       []string{\"-r\", \"-t\", \"/tmp\"},\n\t\t},\n\t}\n\tfile, err := os.Create(testfile)\n\tassert.NoError(t, err)\n\n\tbaseTransfer := common.NewBaseTransfer(file, scpCommand.connection.BaseConnection, nil, file.Name(), file.Name(),\n\t\t\"/\"+testfile, common.TransferDownload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\ttransfer := newTransfer(baseTransfer, nil, nil, nil)\n\n\terr = scpCommand.getUploadFileData(2, transfer)\n\tassert.Error(t, err, \"upload must fail, we send a fake write error message\")\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    readErr,\n\t\tWriteError:   nil,\n\t}\n\tscpCommand.connection.channel = &mockSSHChannel\n\tfile, err = os.Create(testfile)\n\tassert.NoError(t, err)\n\ttransfer.File = file\n\ttransfer.isFinished = false\n\ttransfer.Connection.AddTransfer(transfer)\n\terr = scpCommand.getUploadFileData(2, transfer)\n\tassert.Error(t, err, \"upload must fail, we send a fake read error message\")\n\n\trespBuffer := []byte(\"12\")\n\trespBuffer = append(respBuffer, 0x02)\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(respBuffer),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   nil,\n\t}\n\tscpCommand.connection.channel = &mockSSHChannel\n\tfile, err = os.Create(testfile)\n\tassert.NoError(t, err)\n\tbaseTransfer.File = file\n\ttransfer = newTransfer(baseTransfer, nil, nil, nil)\n\ttransfer.Connection.AddTransfer(transfer)\n\terr = scpCommand.getUploadFileData(2, transfer)\n\tassert.Error(t, err, \"upload must fail, we have not enough data to read\")\n\n\t// the file is already closed so we have an error on trasfer closing\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   nil,\n\t}\n\n\ttransfer.Connection.AddTransfer(transfer)\n\terr = scpCommand.getUploadFileData(0, transfer)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrTransferClosed.Error())\n\t}\n\ttransfer.Connection.RemoveTransfer(transfer)\n\n\tmockSSHChannel = MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t\tReadError:    nil,\n\t\tWriteError:   nil,\n\t}\n\n\ttransfer.Connection.AddTransfer(transfer)\n\terr = scpCommand.getUploadFileData(2, transfer)\n\tassert.ErrorContains(t, err, os.ErrClosed.Error())\n\ttransfer.Connection.RemoveTransfer(transfer)\n\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestUploadError(t *testing.T) {\n\tcommon.Config.UploadMode = common.UploadModeAtomic\n\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"testuser\",\n\t\t},\n\t}\n\tfs := vfs.NewOsFs(\"\", os.TempDir(), \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSCP, \"\", \"\", user),\n\t}\n\n\ttestfile := \"testfile\"\n\tfileTempName := \"temptestfile\"\n\tfile, err := os.Create(fileTempName)\n\tassert.NoError(t, err)\n\tbaseTransfer := common.NewBaseTransfer(file, connection.BaseConnection, nil, testfile, file.Name(),\n\t\ttestfile, common.TransferUpload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})\n\ttransfer := newTransfer(baseTransfer, nil, nil, nil)\n\n\terrFake := errors.New(\"fake error\")\n\ttransfer.TransferError(errFake)\n\terr = transfer.Close()\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\tif assert.Error(t, transfer.ErrTransfer) {\n\t\tassert.EqualError(t, transfer.ErrTransfer, errFake.Error())\n\t}\n\tassert.Equal(t, int64(0), transfer.BytesReceived.Load())\n\n\tassert.NoFileExists(t, testfile)\n\tassert.NoFileExists(t, fileTempName)\n\n\tcommon.Config.UploadMode = common.UploadModeAtomicWithResume\n}\n\nfunc TestTransferFailingReader(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"testuser\",\n\t\t\tHomeDir:  os.TempDir(),\n\t\t},\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(\"crypt secret\"),\n\t\t\t},\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\n\tfs := newMockOsFs(nil, nil, true, \"\", os.TempDir())\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(\"\", common.ProtocolSFTP, \"\", \"\", user),\n\t}\n\n\trequest := sftp.NewRequest(\"Open\", \"afile.txt\")\n\trequest.Flags = 27 // read,write,create,truncate\n\n\ttransfer, err := connection.handleFilewrite(request)\n\trequire.NoError(t, err)\n\tbuf := make([]byte, 32)\n\t_, err = transfer.ReadAt(buf, 0)\n\tassert.ErrorIs(t, err, sftp.ErrSSHFxOpUnsupported)\n\tif c, ok := transfer.(io.Closer); ok {\n\t\terr = c.Close()\n\t\tassert.NoError(t, err)\n\t}\n\n\tfsPath := filepath.Join(os.TempDir(), \"afile.txt\")\n\n\tr, _, err := pipeat.Pipe()\n\tassert.NoError(t, err)\n\tbaseTransfer := common.NewBaseTransfer(nil, connection.BaseConnection, nil, fsPath, fsPath, filepath.Base(fsPath),\n\t\tcommon.TransferUpload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\terrRead := errors.New(\"read is not allowed\")\n\ttr := newTransfer(baseTransfer, nil, vfs.NewPipeReader(r), errRead)\n\t_, err = tr.ReadAt(buf, 0)\n\tassert.ErrorIs(t, err, sftp.ErrSSHFxFailure)\n\n\terr = tr.Close()\n\tassert.NoError(t, err)\n\n\ttr = newTransfer(baseTransfer, nil, nil, errRead)\n\t_, err = tr.ReadAt(buf, 0)\n\tassert.ErrorIs(t, err, sftp.ErrSSHFxFailure)\n\n\terr = tr.Close()\n\tassert.NoError(t, err)\n\n\terr = os.Remove(fsPath)\n\tassert.NoError(t, err)\n\tassert.Len(t, connection.GetTransfers(), 0)\n}\n\nfunc TestConfigsFromProvider(t *testing.T) {\n\terr := dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tc := Configuration{}\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Len(t, c.HostKeyAlgorithms, 0)\n\tassert.Len(t, c.KexAlgorithms, 0)\n\tassert.Len(t, c.Ciphers, 0)\n\tassert.Len(t, c.MACs, 0)\n\tassert.Len(t, c.PublicKeyAlgorithms, 0)\n\tconfigs := dataprovider.Configs{\n\t\tSFTPD: &dataprovider.SFTPDConfigs{\n\t\t\tHostKeyAlgos:   []string{ssh.KeyAlgoRSA},\n\t\t\tKexAlgorithms:  []string{ssh.InsecureKeyExchangeDHGEXSHA1},\n\t\t\tCiphers:        []string{ssh.InsecureCipherAES128CBC},\n\t\t\tMACs:           []string{ssh.HMACSHA512ETM},\n\t\t\tPublicKeyAlgos: []string{ssh.InsecureKeyAlgoDSA}, //nolint:staticcheck\n\t\t},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\texpectedHostKeyAlgos := append(preferredHostKeyAlgos, configs.SFTPD.HostKeyAlgos...)\n\texpectedKEXs := append(preferredKexAlgos, configs.SFTPD.KexAlgorithms...)\n\texpectedCiphers := append(preferredCiphers, configs.SFTPD.Ciphers...)\n\texpectedMACs := append(preferredMACs, configs.SFTPD.MACs...)\n\texpectedPublicKeyAlgos := append(preferredPublicKeyAlgos, configs.SFTPD.PublicKeyAlgos...)\n\tassert.Equal(t, expectedHostKeyAlgos, c.HostKeyAlgorithms)\n\tassert.Equal(t, expectedKEXs, c.KexAlgorithms)\n\tassert.Equal(t, expectedCiphers, c.Ciphers)\n\tassert.Equal(t, expectedMACs, c.MACs)\n\tassert.Equal(t, expectedPublicKeyAlgos, c.PublicKeyAlgorithms)\n\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestSupportedSecurityOptions(t *testing.T) {\n\tc := Configuration{\n\t\tKexAlgorithms: supportedKexAlgos,\n\t\tMACs:          supportedMACs,\n\t\tCiphers:       supportedCiphers,\n\t}\n\tvar defaultKexs []string\n\tfor _, k := range supportedKexAlgos {\n\t\tdefaultKexs = append(defaultKexs, k)\n\t\tif k == ssh.KeyExchangeCurve25519 {\n\t\t\tdefaultKexs = append(defaultKexs, keyExchangeCurve25519SHA256LibSSH)\n\t\t}\n\t}\n\tserverConfig := &ssh.ServerConfig{}\n\terr := c.configureSecurityOptions(serverConfig)\n\tassert.NoError(t, err)\n\tassert.Equal(t, supportedCiphers, serverConfig.Ciphers)\n\tassert.Equal(t, supportedMACs, serverConfig.MACs)\n\tassert.Equal(t, defaultKexs, serverConfig.KeyExchanges)\n\tc.KexAlgorithms = append(c.KexAlgorithms, \"not a kex\")\n\terr = c.configureSecurityOptions(serverConfig)\n\tassert.Error(t, err)\n\tc.KexAlgorithms = append(supportedKexAlgos, \"diffie-hellman-group18-sha512\")\n\tc.MACs = []string{\n\t\t\" hmac-sha2-256-etm@openssh.com \", \" hmac-sha2-512-etm@openssh.com\",\n\t\t\"hmac-sha2-256\", \"hmac-sha2-512 \",\n\t\t\"hmac-sha1 \", \" hmac-sha1-96\",\n\t}\n\terr = c.configureSecurityOptions(serverConfig)\n\tassert.NoError(t, err)\n\tassert.Equal(t, supportedCiphers, serverConfig.Ciphers)\n\tassert.Equal(t, supportedMACs, serverConfig.MACs)\n\tassert.Equal(t, defaultKexs, serverConfig.KeyExchanges)\n}\n\nfunc TestLoadHostKeys(t *testing.T) {\n\tserverConfig := &ssh.ServerConfig{}\n\tc := Configuration{}\n\tc.HostKeys = []string{\".\", \"missing file\"}\n\terr := c.checkAndLoadHostKeys(configDir, serverConfig)\n\tassert.Error(t, err)\n\ttestfile := filepath.Join(os.TempDir(), \"invalidkey\")\n\terr = os.WriteFile(testfile, []byte(\"some bytes\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tc.HostKeys = []string{testfile}\n\terr = c.checkAndLoadHostKeys(configDir, serverConfig)\n\tassert.Error(t, err)\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n\tkeysDir := filepath.Join(os.TempDir(), \"keys\")\n\terr = os.MkdirAll(keysDir, os.ModePerm)\n\tassert.NoError(t, err)\n\trsaKeyName := filepath.Join(keysDir, defaultPrivateRSAKeyName)\n\tecdsaKeyName := filepath.Join(keysDir, defaultPrivateECDSAKeyName)\n\ted25519KeyName := filepath.Join(keysDir, defaultPrivateEd25519KeyName)\n\tnonDefaultKeyName := filepath.Join(keysDir, \"akey\")\n\tc.HostKeys = []string{nonDefaultKeyName, rsaKeyName, ecdsaKeyName, ed25519KeyName}\n\terr = c.checkAndLoadHostKeys(configDir, serverConfig)\n\tassert.Error(t, err)\n\tc.HostKeyAlgorithms = []string{ssh.KeyAlgoRSASHA256}\n\tc.HostKeys = []string{ecdsaKeyName}\n\terr = c.checkAndLoadHostKeys(configDir, serverConfig)\n\tassert.Error(t, err)\n\tc.HostKeyAlgorithms = preferredHostKeyAlgos\n\terr = c.checkAndLoadHostKeys(configDir, serverConfig)\n\tassert.NoError(t, err)\n\tassert.FileExists(t, rsaKeyName)\n\tassert.FileExists(t, ecdsaKeyName)\n\tassert.FileExists(t, ed25519KeyName)\n\tassert.NoFileExists(t, nonDefaultKeyName)\n\terr = os.Remove(rsaKeyName)\n\tassert.NoError(t, err)\n\terr = os.Remove(ecdsaKeyName)\n\tassert.NoError(t, err)\n\terr = os.Remove(ed25519KeyName)\n\tassert.NoError(t, err)\n\tif runtime.GOOS != osWindows {\n\t\terr = os.Chmod(keysDir, 0551)\n\t\tassert.NoError(t, err)\n\t\tc.HostKeys = nil\n\t\terr = c.checkAndLoadHostKeys(keysDir, serverConfig)\n\t\tassert.Error(t, err)\n\t\tc.HostKeys = []string{rsaKeyName, ecdsaKeyName}\n\t\terr = c.checkAndLoadHostKeys(configDir, serverConfig)\n\t\tassert.Error(t, err)\n\t\tc.HostKeys = []string{ecdsaKeyName, rsaKeyName}\n\t\terr = c.checkAndLoadHostKeys(configDir, serverConfig)\n\t\tassert.Error(t, err)\n\t\tc.HostKeys = []string{ed25519KeyName}\n\t\terr = c.checkAndLoadHostKeys(configDir, serverConfig)\n\t\tassert.Error(t, err)\n\t\terr = os.Chmod(keysDir, 0755)\n\t\tassert.NoError(t, err)\n\t}\n\terr = os.RemoveAll(keysDir)\n\tassert.NoError(t, err)\n}\n\nfunc TestCertCheckerInitErrors(t *testing.T) {\n\tc := Configuration{}\n\tc.TrustedUserCAKeys = []string{\".\", \"missing file\"}\n\terr := c.initializeCertChecker(\"\")\n\tassert.Error(t, err)\n\ttestfile := filepath.Join(os.TempDir(), \"invalidkey\")\n\terr = os.WriteFile(testfile, []byte(\"some bytes\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tc.TrustedUserCAKeys = []string{testfile}\n\terr = c.initializeCertChecker(\"\")\n\tassert.Error(t, err)\n\terr = os.Remove(testfile)\n\tassert.NoError(t, err)\n}\n\nfunc TestRecoverer(t *testing.T) {\n\tc := Configuration{}\n\tc.AcceptInboundConnection(nil, nil)\n\tconnID := \"connectionID\"\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolSFTP, \"\", \"\", dataprovider.User{}),\n\t}\n\tc.handleSftpConnection(nil, connection)\n\tsshCmd := sshCommand{\n\t\tcommand:    \"cd\",\n\t\tconnection: connection,\n\t}\n\terr := sshCmd.handle()\n\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\tscpCmd := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: connection,\n\t\t},\n\t}\n\terr = scpCmd.handle()\n\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestListernerAcceptErrors(t *testing.T) {\n\terrFake := errors.New(\"a fake error\")\n\tlistener := newFakeListener(errFake)\n\tc := Configuration{}\n\terr := c.serve(listener, nil)\n\trequire.EqualError(t, err, errFake.Error())\n\terr = listener.Close()\n\trequire.NoError(t, err)\n\n\terrNetFake := &fakeNetError{error: errFake}\n\tlistener = newFakeListener(errNetFake)\n\terr = c.serve(listener, nil)\n\trequire.EqualError(t, err, errFake.Error())\n\terr = listener.Close()\n\trequire.NoError(t, err)\n}\n\ntype fakeNetError struct {\n\terror\n\tcount int\n}\n\nfunc (e *fakeNetError) Timeout() bool {\n\treturn false\n}\n\nfunc (e *fakeNetError) Temporary() bool {\n\te.count++\n\treturn e.count < 10\n}\n\nfunc (e *fakeNetError) Error() string {\n\treturn e.error.Error()\n}\n\ntype fakeListener struct {\n\tserver net.Conn\n\tclient net.Conn\n\terr    error\n}\n\nfunc (l *fakeListener) Accept() (net.Conn, error) {\n\treturn l.client, l.err\n}\n\nfunc (l *fakeListener) Close() error {\n\terrClient := l.client.Close()\n\terrServer := l.server.Close()\n\tif errServer != nil {\n\t\treturn errServer\n\t}\n\treturn errClient\n}\n\nfunc (l *fakeListener) Addr() net.Addr {\n\treturn l.server.LocalAddr()\n}\n\nfunc newFakeListener(err error) net.Listener {\n\tserver, client := net.Pipe()\n\n\treturn &fakeListener{\n\t\tserver: server,\n\t\tclient: client,\n\t\terr:    err,\n\t}\n}\n\nfunc TestLoadRevokedUserCertsFile(t *testing.T) {\n\tr := revokedCertificates{\n\t\tcerts: map[string]bool{},\n\t}\n\terr := r.load()\n\tassert.NoError(t, err)\n\tr.filePath = filepath.Join(os.TempDir(), \"sub\", \"testrevoked\")\n\terr = os.MkdirAll(filepath.Dir(r.filePath), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(r.filePath, []byte(`no json`), 0644)\n\tassert.NoError(t, err)\n\terr = r.load()\n\tassert.Error(t, err)\n\tr.filePath = filepath.Dir(r.filePath)\n\terr = r.load()\n\tassert.Error(t, err)\n\terr = os.RemoveAll(r.filePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestMaxUserSessions(t *testing.T) {\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(xid.New().String(), common.ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername:    \"user_max_sessions\",\n\t\t\t\tHomeDir:     filepath.Clean(os.TempDir()),\n\t\t\t\tMaxSessions: 1,\n\t\t\t},\n\t\t}),\n\t}\n\terr := common.Connections.Add(connection)\n\tassert.NoError(t, err)\n\n\tc := Configuration{}\n\tc.handleSftpConnection(nil, connection)\n\n\tbuf := make([]byte, 65535)\n\tstdErrBuf := make([]byte, 65535)\n\tmockSSHChannel := MockChannel{\n\t\tBuffer:       bytes.NewBuffer(buf),\n\t\tStdErrBuffer: bytes.NewBuffer(stdErrBuf),\n\t}\n\n\tconn := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(xid.New().String(), common.ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername:    \"user_max_sessions\",\n\t\t\t\tHomeDir:     filepath.Clean(os.TempDir()),\n\t\t\t\tMaxSessions: 1,\n\t\t\t},\n\t\t}),\n\t\tchannel: &mockSSHChannel,\n\t}\n\n\tsshCmd := sshCommand{\n\t\tcommand:    \"cd\",\n\t\tconnection: conn,\n\t}\n\terr = sshCmd.handle()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"too many open sessions\")\n\t}\n\tscpCmd := scpCommand{\n\t\tsshCommand: sshCommand{\n\t\t\tcommand:    \"scp\",\n\t\t\tconnection: conn,\n\t\t},\n\t}\n\terr = scpCmd.handle()\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"too many open sessions\")\n\t}\n\n\tcommon.Connections.Remove(connection.GetID())\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestCanReadSymlink(t *testing.T) {\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(xid.New().String(), common.ProtocolSFTP, \"\", \"\", dataprovider.User{\n\t\t\tBaseUser: sdk.BaseUser{\n\t\t\t\tUsername: \"user_can_read_symlink\",\n\t\t\t\tHomeDir:  filepath.Clean(os.TempDir()),\n\t\t\t\tPermissions: map[string][]string{\n\t\t\t\t\t\"/\":    {dataprovider.PermAny},\n\t\t\t\t\t\"/sub\": {dataprovider.PermUpload},\n\t\t\t\t},\n\t\t\t},\n\t\t\tFilters: dataprovider.UserFilters{\n\t\t\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\t\t\tFilePatterns: []sdk.PatternsFilter{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath:           \"/denied\",\n\t\t\t\t\t\t\tDeniedPatterns: []string{\"*.txt\"},\n\t\t\t\t\t\t\tDenyPolicy:     sdk.DenyPolicyHide,\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\terr := connection.canReadLink(\"/sub/link\")\n\tassert.ErrorIs(t, err, sftp.ErrSSHFxPermissionDenied)\n\n\terr = connection.canReadLink(\"/denied/file.txt\")\n\tassert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)\n}\n\nfunc TestAuthenticationErrors(t *testing.T) {\n\tsftpAuthError := newAuthenticationError(nil, \"\", \"\")\n\tloginMethod := dataprovider.SSHLoginMethodPassword\n\tusername := \"test user\"\n\terr := newAuthenticationError(fmt.Errorf(\"cannot validate credentials: %w\", util.NewRecordNotFoundError(\"not found\")),\n\t\tloginMethod, username)\n\tassert.ErrorIs(t, err, sftpAuthError)\n\tassert.ErrorIs(t, err, util.ErrNotFound)\n\tvar sftpAuthErr *authenticationError\n\tif assert.ErrorAs(t, err, &sftpAuthErr) {\n\t\tassert.Equal(t, loginMethod, sftpAuthErr.getLoginMethod())\n\t\tassert.Equal(t, username, sftpAuthErr.getUsername())\n\t}\n\terr = newAuthenticationError(fmt.Errorf(\"cannot validate credentials: %w\", fs.ErrPermission), loginMethod, username)\n\tassert.ErrorIs(t, err, sftpAuthError)\n\tassert.NotErrorIs(t, err, util.ErrNotFound)\n\terr = newAuthenticationError(fmt.Errorf(\"cert has wrong type %d\", ssh.HostCert), loginMethod, username)\n\tassert.ErrorIs(t, err, sftpAuthError)\n\tassert.NotErrorIs(t, err, util.ErrNotFound)\n\terr = newAuthenticationError(errors.New(\"ssh: certificate signed by unrecognized authority\"), loginMethod, username)\n\tassert.ErrorIs(t, err, sftpAuthError)\n\tassert.NotErrorIs(t, err, util.ErrNotFound)\n\terr = newAuthenticationError(nil, loginMethod, username)\n\tassert.ErrorIs(t, err, sftpAuthError)\n\tassert.NotErrorIs(t, err, util.ErrNotFound)\n}\n\ntype mockCommandExecutor struct {\n\tOutput []byte\n\tErr    error\n}\n\nfunc (f mockCommandExecutor) CombinedOutput(ctx context.Context, name string, args ...string) ([]byte, error) {\n\treturn f.Output, f.Err\n}\n\nfunc TestVerifyWithOPKSSH(t *testing.T) {\n\tsshCert := []byte(`ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg4+hKHVPKv183MU/Q7XD/mzDBFSc2YY3eraltxLMGJo0AAAADAQABAAABAQCe6jMoy1xCQgiZkZJ7gi6NLj4uRqz2OaUGK/OJYZTfBqK+SlS9iymAluHu9K+cc4+0qxx0gn7dRTJWINSgzvca6ayYe995EKgD1hE5krh9BH0bRrXB+hGqyslcZOgLNO+v8jYojClQbRtET2tS+xb4k33GCuL5wgla2790ZgOQgs7huQUjG0S8c1W+EYt6fI4cWE/DeEBnv9sqryS8rOb0PbM6WUd7XBadwySFWYQUX0ei56GNt12Z4gADEGlFQV/OnV0PvnTcAMGUl0rfToPgJ4jgogWKoTVWuZ9wyA/x+2LRLRvgm2a969ig937/AH0i0Wq+FzqfK7EXQ99Yf5K/AAAAAAAAAAAAAAACAAAAFGhvc3QuZXhhbXBsZS5jb20ta2V5AAAAFAAAABBob3N0LmV4YW1wbGUuY29tAAAAAGXEzYAAAAAAd8sP4wAAAAAAAAAAAAAAAAAAARcAAAAHc3NoLXJzYQAAAAMBAAEAAAEBAL4PXUPSERufZWCW/hhEnylk3IeMgaa+2HcNY5Cur77a8fYy6OYZAPF+vhJUT0akwGUpTeXAZumAgHECDrJlw1J+jo9ZVT0AKDo0wU77IzNzYxob7+dpB02NJ7DLAXmPauQ07Zc5pWJFVKtmuh7YH9pjYtNXSMOXye7k06PBGzX+ztIt7nPWvD9fR2mZeTSoljeBCGZHwdlnV2ESQlQbBoEI93RPxqxJh/UCDatQPhpDbyverr2ZvB9Y45rqsx6ZVmu5RXl3MfBU1U21W/4ia2di3PybyD4rSmVoam0efcqxo6cBKSHe26OFoTuS9zgdH0iCWL37vqOFmJ7eH91M3nMAAAEUAAAADHJzYS1zaGEyLTI1NgAAAQA/ByIegNZYJRRl413S/8LxGvTZnbxsPwaluoJ/54niGZV9P28THz7d9jXfSHPjalhH93jNPfTYXvI4opnDC37ua1Nu8KKfk40IWXnnDdZLWraUxEidIzhmfVtz8kGdGoFQ8H0EzubL7zKNOTlfSfOoDlmQVOuxT/+eh2mEp4ri0/+8J1mLfLBr8tREX0/iaNjK+RKdcyTMicKursAYMCDdu8vlaphxea+ocyHM9izSX/l33t44V13ueTqIOh2Zbl2UE2k+jk+0dc1CmV0SEoiWiIyt8TRM4yQry1vPlQLsrf28sYM/QMwnhCVhyZO3vs5F25aQWrB9d51VEzBW9/fd host.example.com`)\n\tkey, _, _, _, err := ssh.ParseAuthorizedKey(sshCert) //nolint:dogsled\n\trequire.NoError(t, err)\n\tcert, ok := key.(*ssh.Certificate)\n\trequire.True(t, ok)\n\tc := Configuration{}\n\tc.executor = mockCommandExecutor{\n\t\tErr: errors.New(\"test error\"),\n\t}\n\terr = c.verifyWithOPKSSH(\"user\", cert)\n\tassert.Error(t, err)\n\n\tc.executor = mockCommandExecutor{}\n\terr = c.verifyWithOPKSSH(\"\", cert)\n\tassert.Error(t, err)\n\n\tc.executor = mockCommandExecutor{\n\t\tOutput: ssh.MarshalAuthorizedKey(cert),\n\t}\n\terr = c.verifyWithOPKSSH(\"\", cert)\n\tassert.Error(t, err)\n\n\tc.executor = mockCommandExecutor{\n\t\tOutput: ssh.MarshalAuthorizedKey(cert.SignatureKey),\n\t}\n\terr = c.verifyWithOPKSSH(\"\", cert)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/sftpd/lister.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd\n\nimport (\n\t\"io\"\n\t\"os\"\n)\n\ntype listerAt []os.FileInfo\n\n// ListAt returns the number of entries copied and an io.EOF error if we made it to the end of the file list.\n// Take a look at the pkg/sftp godoc for more information about how this function should work.\nfunc (l listerAt) ListAt(f []os.FileInfo, offset int64) (int, error) {\n\tif offset >= int64(len(l)) {\n\t\treturn 0, io.EOF\n\t}\n\n\tn := copy(f, l[offset:])\n\tif n < len(f) {\n\t\treturn n, io.EOF\n\t}\n\treturn n, nil\n}\n"
  },
  {
    "path": "internal/sftpd/scp.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nvar (\n\tokMsg   = []byte{0x00}\n\twarnMsg = []byte{0x01} // must be followed by an optional message and a newline\n\terrMsg  = []byte{0x02} // must be followed by an optional message and a newline\n\tnewLine = []byte{0x0A}\n)\n\ntype scpCommand struct {\n\tsshCommand\n}\n\nfunc (c *scpCommand) handle() (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlogger.Error(logSender, \"\", \"panic in handle scp command: %q stack trace: %v\", r, string(debug.Stack()))\n\t\t\terr = common.ErrGenericFailure\n\t\t}\n\t}()\n\tif err := common.Connections.Add(c.connection); err != nil {\n\t\tdefer c.connection.CloseFS() //nolint:errcheck\n\t\tlogger.Info(logSender, \"\", \"unable to add SCP connection: %v\", err)\n\t\treturn c.sendErrorResponse(err)\n\t}\n\tdefer common.Connections.Remove(c.connection.GetID())\n\n\tdestPath := c.getDestPath()\n\tc.connection.Log(logger.LevelDebug, \"handle scp command, args: %v user: %s, dest path: %q\",\n\t\tc.args, c.connection.User.Username, destPath)\n\tif c.hasFlag(\"t\") {\n\t\t// -t means \"to\", so upload\n\t\terr = c.sendConfirmationMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = c.handleRecursiveUpload()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if c.hasFlag(\"f\") {\n\t\t// -f means \"from\" so download\n\t\terr = c.readConfirmationMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = c.handleDownload(destPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\terr = fmt.Errorf(\"scp command not supported, args: %v\", c.args)\n\t\tc.connection.Log(logger.LevelDebug, \"unsupported scp command, args: %v\", c.args)\n\t}\n\tc.sendExitStatus(err)\n\treturn err\n}\n\nfunc (c *scpCommand) handleRecursiveUpload() error {\n\tnumDirs := 0\n\tdestPath := c.getDestPath()\n\tfor {\n\t\tfs, err := c.connection.User.GetFilesystemForPath(destPath, c.connection.ID)\n\t\tif err != nil {\n\t\t\tc.connection.Log(logger.LevelError, \"error uploading file %q: %+v\", destPath, err)\n\t\t\tc.sendErrorMessage(nil, fmt.Errorf(\"unable to get fs for path %q\", destPath))\n\t\t\treturn err\n\t\t}\n\t\tcommand, err := c.getNextUploadProtocolMessage()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tc.sendErrorMessage(fs, err)\n\t\t\treturn err\n\t\t}\n\t\tif strings.HasPrefix(command, \"E\") {\n\t\t\tnumDirs--\n\t\t\tc.connection.Log(logger.LevelDebug, \"received end dir command, num dirs: %v\", numDirs)\n\t\t\tif numDirs < 0 {\n\t\t\t\terr = errors.New(\"unacceptable end dir command\")\n\t\t\t\tc.sendErrorMessage(nil, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// the destination dir is now the parent directory\n\t\t\tdestPath = path.Join(destPath, \"..\")\n\t\t} else {\n\t\t\tsizeToRead, name, err := c.parseUploadMessage(fs, command)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif strings.HasPrefix(command, \"D\") {\n\t\t\t\tnumDirs++\n\t\t\t\tdestPath = path.Join(destPath, name)\n\t\t\t\tfs, err = c.connection.User.GetFilesystemForPath(destPath, c.connection.ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tc.connection.Log(logger.LevelError, \"error uploading file %q: %+v\", destPath, err)\n\t\t\t\t\tc.sendErrorMessage(nil, fmt.Errorf(\"unable to get fs for path %q\", destPath))\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\terr = c.handleCreateDir(fs, destPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tc.connection.Log(logger.LevelDebug, \"received start dir command, num dirs: %v destPath: %q\", numDirs, destPath)\n\t\t\t} else if strings.HasPrefix(command, \"C\") {\n\t\t\t\terr = c.handleUpload(c.getFileUploadDestPath(fs, destPath, name), sizeToRead)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\terr = c.sendConfirmationMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (c *scpCommand) handleCreateDir(fs vfs.Fs, dirPath string) error {\n\tc.connection.UpdateLastActivity()\n\n\tp, err := fs.ResolvePath(dirPath)\n\tif err != nil {\n\t\tc.connection.Log(logger.LevelError, \"error creating dir: %q, invalid file path, err: %v\", dirPath, err)\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn err\n\t}\n\tif !c.connection.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(dirPath)) {\n\t\tc.connection.Log(logger.LevelError, \"error creating dir: %q, permission denied\", dirPath)\n\t\tc.sendErrorMessage(fs, common.ErrPermissionDenied)\n\t\treturn common.ErrPermissionDenied\n\t}\n\n\tinfo, err := c.connection.DoStat(dirPath, 1, true)\n\tif err == nil && info.IsDir() {\n\t\treturn nil\n\t}\n\n\terr = c.createDir(fs, p)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.connection.Log(logger.LevelDebug, \"created dir %q\", dirPath)\n\treturn nil\n}\n\n// we need to close the transfer if we have an error\nfunc (c *scpCommand) getUploadFileData(sizeToRead int64, transfer *transfer) error {\n\terr := c.sendConfirmationMessage()\n\tif err != nil {\n\t\ttransfer.TransferError(err)\n\t\ttransfer.Close()\n\t\treturn err\n\t}\n\n\tif sizeToRead > 0 {\n\t\t// we could replace this method with io.CopyN implementing \"Write\" method in transfer struct\n\t\tremaining := sizeToRead\n\t\tbuf := make([]byte, int64(math.Min(32768, float64(sizeToRead))))\n\t\tfor {\n\t\t\tn, err := c.connection.channel.Read(buf)\n\t\t\tif err != nil {\n\t\t\t\ttransfer.TransferError(err)\n\t\t\t\ttransfer.Close()\n\t\t\t\tc.sendErrorMessage(transfer.Fs, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t_, err = transfer.WriteAt(buf[:n], sizeToRead-remaining)\n\t\t\tif err != nil {\n\t\t\t\ttransfer.Close()\n\t\t\t\tc.sendErrorMessage(transfer.Fs, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tremaining -= int64(n)\n\t\t\tif remaining <= 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif remaining < int64(len(buf)) {\n\t\t\t\tbuf = make([]byte, remaining)\n\t\t\t}\n\t\t}\n\t}\n\terr = c.readConfirmationMessage()\n\tif err != nil {\n\t\ttransfer.TransferError(err)\n\t\ttransfer.Close()\n\t\treturn err\n\t}\n\terr = transfer.Close()\n\tif err != nil {\n\t\tc.sendErrorMessage(transfer.Fs, err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string, sizeToRead int64, isNewFile bool, fileSize int64, requestPath string) error {\n\tif err := common.Connections.IsNewTransferAllowed(c.connection.User.Username); err != nil {\n\t\terr := fmt.Errorf(\"denying file write due to transfer count limits\")\n\t\tc.connection.Log(logger.LevelInfo, \"denying file write due to transfer count limits\")\n\t\tc.sendErrorMessage(nil, err)\n\t\treturn err\n\t}\n\tdiskQuota, transferQuota := c.connection.HasSpace(isNewFile, false, requestPath)\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\terr := fmt.Errorf(\"denying file write due to quota limits\")\n\t\tc.connection.Log(logger.LevelError, \"error uploading file: %q, err: %v\", filePath, err)\n\t\tc.sendErrorMessage(nil, err)\n\t\treturn err\n\t}\n\t_, err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath,\n\t\tfileSize, os.O_TRUNC)\n\tif err != nil {\n\t\tc.connection.Log(logger.LevelDebug, \"upload for file %q denied by pre action: %v\", requestPath, err)\n\t\terr = c.connection.GetPermissionDeniedError()\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn err\n\t}\n\n\tmaxWriteSize, _ := c.connection.GetMaxWriteSize(diskQuota, false, fileSize, fs.IsUploadResumeSupported())\n\n\tfile, w, cancelFn, err := fs.Create(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, c.connection.GetCreateChecks(requestPath, isNewFile, false))\n\tif err != nil {\n\t\tc.connection.Log(logger.LevelError, \"error creating file %q: %v\", resolvedPath, err)\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn err\n\t}\n\n\tinitialSize := int64(0)\n\ttruncatedSize := int64(0) // bytes truncated and not included in quota\n\tif !isNewFile {\n\t\tif vfs.HasTruncateSupport(fs) {\n\t\t\tvfolder, err := c.connection.User.GetVirtualFolderForPath(path.Dir(requestPath))\n\t\t\tif err == nil {\n\t\t\t\tdataprovider.UpdateUserFolderQuota(&vfolder, &c.connection.User, 0, -fileSize, false)\n\t\t\t} else {\n\t\t\t\tdataprovider.UpdateUserQuota(&c.connection.User, 0, -fileSize, false) //nolint:errcheck\n\t\t\t}\n\t\t} else {\n\t\t\tinitialSize = fileSize\n\t\t\ttruncatedSize = initialSize\n\t\t}\n\t\tif maxWriteSize > 0 {\n\t\t\tmaxWriteSize += fileSize\n\t\t}\n\t}\n\n\tvfs.SetPathPermissions(fs, filePath, c.connection.User.GetUID(), c.connection.User.GetGID())\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.connection.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,\n\t\tcommon.TransferUpload, 0, initialSize, maxWriteSize, truncatedSize, isNewFile, fs, transferQuota)\n\tt := newTransfer(baseTransfer, w, nil, nil)\n\n\treturn c.getUploadFileData(sizeToRead, t)\n}\n\nfunc (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error {\n\tc.connection.UpdateLastActivity()\n\n\tfs, p, err := c.connection.GetFsAndResolvedPath(uploadFilePath)\n\tif err != nil {\n\t\tc.connection.Log(logger.LevelError, \"error uploading file: %q, err: %v\", uploadFilePath, err)\n\t\tc.sendErrorMessage(nil, err)\n\t\treturn err\n\t}\n\n\tif ok, _ := c.connection.User.IsFileAllowed(uploadFilePath); !ok {\n\t\tc.connection.Log(logger.LevelWarn, \"writing file %q is not allowed\", uploadFilePath)\n\t\tc.sendErrorMessage(fs, c.connection.GetPermissionDeniedError())\n\t\treturn common.ErrPermissionDenied\n\t}\n\n\tfilePath := p\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\tfilePath = fs.GetAtomicUploadPath(p)\n\t}\n\tstat, statErr := fs.Lstat(p)\n\tif (statErr == nil && stat.Mode()&os.ModeSymlink != 0) || fs.IsNotExist(statErr) {\n\t\tif !c.connection.User.HasPerm(dataprovider.PermUpload, path.Dir(uploadFilePath)) {\n\t\t\tc.connection.Log(logger.LevelWarn, \"cannot upload file: %q, permission denied\", uploadFilePath)\n\t\t\tc.sendErrorMessage(fs, common.ErrPermissionDenied)\n\t\t\treturn common.ErrPermissionDenied\n\t\t}\n\t\treturn c.handleUploadFile(fs, p, filePath, sizeToRead, true, 0, uploadFilePath)\n\t}\n\n\tif statErr != nil {\n\t\tc.connection.Log(logger.LevelError, \"error performing file stat %q: %v\", p, statErr)\n\t\tc.sendErrorMessage(fs, statErr)\n\t\treturn statErr\n\t}\n\n\tif stat.IsDir() {\n\t\tc.connection.Log(logger.LevelError, \"attempted to open a directory for writing to: %q\", p)\n\t\terr = fmt.Errorf(\"attempted to open a directory for writing: %q\", p)\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn err\n\t}\n\n\tif !c.connection.User.HasPerm(dataprovider.PermOverwrite, uploadFilePath) {\n\t\tc.connection.Log(logger.LevelWarn, \"cannot overwrite file: %q, permission denied\", uploadFilePath)\n\t\tc.sendErrorMessage(fs, common.ErrPermissionDenied)\n\t\treturn common.ErrPermissionDenied\n\t}\n\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\t_, _, err = fs.Rename(p, filePath, 0)\n\t\tif err != nil {\n\t\t\tc.connection.Log(logger.LevelError, \"error renaming existing file for atomic upload, source: %q, dest: %q, err: %v\",\n\t\t\t\tp, filePath, err)\n\t\t\tc.sendErrorMessage(fs, err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn c.handleUploadFile(fs, p, filePath, sizeToRead, false, stat.Size(), uploadFilePath)\n}\n\nfunc (c *scpCommand) sendDownloadProtocolMessages(virtualDirPath string, stat os.FileInfo) error {\n\tvar err error\n\tif c.sendFileTime() {\n\t\tmodTime := stat.ModTime().UnixNano() / 1000000000\n\t\ttCommand := fmt.Sprintf(\"T%d 0 %d 0\\n\", modTime, modTime)\n\t\terr = c.sendProtocolMessage(tCommand)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = c.readConfirmationMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdirName := path.Base(virtualDirPath)\n\tif dirName == \"/\" || dirName == \".\" {\n\t\tdirName = c.connection.User.Username\n\t}\n\n\tfileMode := fmt.Sprintf(\"D%v 0 %v\\n\", getFileModeAsString(stat.Mode(), stat.IsDir()), dirName)\n\terr = c.sendProtocolMessage(fileMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = c.readConfirmationMessage()\n\treturn err\n}\n\n// We send first all the files in the root directory and then the directories.\n// For each directory we recursively call this method again\nfunc (c *scpCommand) handleRecursiveDownload(fs vfs.Fs, dirPath, virtualPath string, stat os.FileInfo) error {\n\tvar err error\n\tif c.isRecursive() {\n\t\tc.connection.Log(logger.LevelDebug, \"recursive download, dir path %q virtual path %q\", dirPath, virtualPath)\n\t\terr = c.sendDownloadProtocolMessages(virtualPath, stat)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// dirPath is a fs path, not a virtual path\n\t\tlister, err := fs.ReadDir(dirPath)\n\t\tif err != nil {\n\t\t\tc.sendErrorMessage(fs, err)\n\t\t\treturn err\n\t\t}\n\t\tdefer lister.Close()\n\n\t\tvdirs := c.connection.User.GetVirtualFoldersInfo(virtualPath)\n\n\t\tvar dirs []string\n\t\tfor {\n\t\t\tfiles, err := lister.Next(vfs.ListerBatchSize)\n\t\t\tfinished := errors.Is(err, io.EOF)\n\t\t\tif err != nil && !finished {\n\t\t\t\tc.sendErrorMessage(fs, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfiles = c.connection.User.FilterListDir(files, fs.GetRelativePath(dirPath))\n\t\t\tif len(vdirs) > 0 {\n\t\t\t\tfiles = append(files, vdirs...)\n\t\t\t\tvdirs = nil\n\t\t\t}\n\t\t\tfor _, file := range files {\n\t\t\t\tfilePath := fs.GetRelativePath(fs.Join(dirPath, file.Name()))\n\t\t\t\tif file.Mode().IsRegular() || file.Mode()&os.ModeSymlink != 0 {\n\t\t\t\t\terr = c.handleDownload(filePath)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tc.sendErrorMessage(fs, err)\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t} else if file.IsDir() {\n\t\t\t\t\tdirs = append(dirs, filePath)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif finished {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tlister.Close()\n\n\t\treturn c.downloadDirs(fs, dirs)\n\t}\n\terr = errors.New(\"unable to send directory for non recursive copy\")\n\tc.sendErrorMessage(nil, err)\n\treturn err\n}\n\nfunc (c *scpCommand) downloadDirs(fs vfs.Fs, dirs []string) error {\n\tfor _, dir := range dirs {\n\t\tif err := c.handleDownload(dir); err != nil {\n\t\t\tc.sendErrorMessage(fs, err)\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := c.sendProtocolMessage(\"E\\n\"); err != nil {\n\t\treturn err\n\t}\n\treturn c.readConfirmationMessage()\n}\n\nfunc (c *scpCommand) sendDownloadFileData(fs vfs.Fs, filePath string, stat os.FileInfo, transfer *transfer) error {\n\tvar err error\n\tif c.sendFileTime() {\n\t\tmodTime := stat.ModTime().UnixNano() / 1000000000\n\t\ttCommand := fmt.Sprintf(\"T%d 0 %d 0\\n\", modTime, modTime)\n\t\terr = c.sendProtocolMessage(tCommand)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = c.readConfirmationMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif vfs.IsCryptOsFs(fs) {\n\t\tstat = fs.(*vfs.CryptFs).ConvertFileInfo(stat)\n\t}\n\n\tfileSize := stat.Size()\n\treaded := int64(0)\n\tfileMode := fmt.Sprintf(\"C%v %v %v\\n\", getFileModeAsString(stat.Mode(), stat.IsDir()), fileSize, filepath.Base(filePath))\n\terr = c.sendProtocolMessage(fileMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = c.readConfirmationMessage()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// we could replace this method with io.CopyN implementing \"Read\" method in transfer struct\n\tbuf := make([]byte, 32768)\n\tvar n int\n\tfor {\n\t\tn, err = transfer.ReadAt(buf, readed)\n\t\tif err == nil || err == io.EOF {\n\t\t\tif n > 0 {\n\t\t\t\t_, err = c.connection.channel.Write(buf[:n])\n\t\t\t}\n\t\t}\n\t\treaded += int64(n)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\tif err != io.EOF {\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn err\n\t}\n\terr = c.sendConfirmationMessage()\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = c.readConfirmationMessage()\n\treturn err\n}\n\nfunc (c *scpCommand) handleDownload(filePath string) error {\n\tc.connection.UpdateLastActivity()\n\n\tif err := common.Connections.IsNewTransferAllowed(c.connection.User.Username); err != nil {\n\t\terr := fmt.Errorf(\"denying file read due to transfer count limits\")\n\t\tc.connection.Log(logger.LevelInfo, \"denying file read due to transfer count limits\")\n\t\tc.sendErrorMessage(nil, err)\n\t\treturn err\n\t}\n\ttransferQuota := c.connection.GetTransferQuota()\n\tif !transferQuota.HasDownloadSpace() {\n\t\tc.connection.Log(logger.LevelInfo, \"denying file read due to quota limits\")\n\t\tc.sendErrorMessage(nil, c.connection.GetReadQuotaExceededError())\n\t\treturn c.connection.GetReadQuotaExceededError()\n\t}\n\tvar err error\n\n\tfs, p, err := c.connection.GetFsAndResolvedPath(filePath)\n\tif err != nil {\n\t\tc.connection.Log(logger.LevelError, \"error downloading file %q: %+v\", filePath, err)\n\t\tc.sendErrorMessage(nil, fmt.Errorf(\"unable to download file %q: %w\", filePath, err))\n\t\treturn err\n\t}\n\n\tvar stat os.FileInfo\n\tif stat, err = fs.Stat(p); err != nil {\n\t\tc.connection.Log(logger.LevelError, \"error downloading file: %q->%q, err: %v\", filePath, p, err)\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn err\n\t}\n\n\tif stat.IsDir() {\n\t\tif !c.connection.User.HasPerm(dataprovider.PermDownload, filePath) {\n\t\t\tc.connection.Log(logger.LevelWarn, \"error downloading dir: %q, permission denied\", filePath)\n\t\t\tc.sendErrorMessage(fs, common.ErrPermissionDenied)\n\t\t\treturn common.ErrPermissionDenied\n\t\t}\n\t\terr = c.handleRecursiveDownload(fs, p, filePath, stat)\n\t\treturn err\n\t}\n\n\tif !c.connection.User.HasPerm(dataprovider.PermDownload, path.Dir(filePath)) {\n\t\tc.connection.Log(logger.LevelWarn, \"error downloading dir: %q, permission denied\", filePath)\n\t\tc.sendErrorMessage(fs, common.ErrPermissionDenied)\n\t\treturn common.ErrPermissionDenied\n\t}\n\n\tif ok, policy := c.connection.User.IsFileAllowed(filePath); !ok {\n\t\tc.connection.Log(logger.LevelWarn, \"reading file %q is not allowed\", filePath)\n\t\tc.sendErrorMessage(fs, c.connection.GetErrorForDeniedFile(policy))\n\t\treturn common.ErrPermissionDenied\n\t}\n\n\tif _, err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreDownload, p, filePath, 0, 0); err != nil {\n\t\tc.connection.Log(logger.LevelDebug, \"download for file %q denied by pre action: %v\", filePath, err)\n\t\tc.sendErrorMessage(fs, common.ErrPermissionDenied)\n\t\treturn common.ErrPermissionDenied\n\t}\n\n\tfile, r, cancelFn, err := fs.Open(p, 0)\n\tif err != nil {\n\t\tc.connection.Log(logger.LevelError, \"could not open file %q for reading: %v\", p, err)\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn err\n\t}\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.connection.BaseConnection, cancelFn, p, p, filePath,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, transferQuota)\n\tt := newTransfer(baseTransfer, nil, r, nil)\n\n\terr = c.sendDownloadFileData(fs, p, stat, t)\n\t// we need to call Close anyway and return close error if any and\n\t// if we have no previous error\n\tif err == nil {\n\t\terr = t.Close()\n\t} else {\n\t\tt.TransferError(err)\n\t\tt.Close()\n\t}\n\treturn err\n}\n\nfunc (c *scpCommand) sendFileTime() bool {\n\treturn c.hasFlag(\"p\")\n}\n\nfunc (c *scpCommand) isRecursive() bool {\n\treturn c.hasFlag(\"r\")\n}\n\nfunc (c *scpCommand) hasFlag(flag string) bool {\n\tfor idx := 0; idx < len(c.args)-1; idx++ {\n\t\targ := c.args[idx]\n\t\tif !strings.HasPrefix(arg, \"--\") && strings.HasPrefix(arg, \"-\") && strings.Contains(arg, flag) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// read the SCP confirmation message and the optional text message\n// the channel will be closed on errors\nfunc (c *scpCommand) readConfirmationMessage() error {\n\tvar msg strings.Builder\n\tbuf := make([]byte, 1)\n\tn, err := c.connection.channel.Read(buf)\n\tif err != nil {\n\t\tc.connection.channel.Close()\n\t\treturn err\n\t}\n\tif n == 1 && (buf[0] == warnMsg[0] || buf[0] == errMsg[0]) {\n\t\tisError := buf[0] == errMsg[0]\n\t\tfor {\n\t\t\tn, err = c.connection.channel.Read(buf)\n\t\t\treaded := buf[:n]\n\t\t\tif err != nil || (n == 1 && readed[0] == newLine[0]) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif n > 0 {\n\t\t\t\tmsg.Write(readed)\n\t\t\t}\n\t\t}\n\t\tc.connection.Log(logger.LevelInfo, \"scp error message received: %v is error: %v\", msg.String(), isError)\n\t\terr = fmt.Errorf(\"%v\", msg.String())\n\t\tc.connection.channel.Close()\n\t}\n\treturn err\n}\n\n// protool messages are newline terminated\nfunc (c *scpCommand) readProtocolMessage() (string, error) {\n\tvar command strings.Builder\n\tvar err error\n\tbuf := make([]byte, 1)\n\tfor {\n\t\tvar n int\n\t\tn, err = c.connection.channel.Read(buf)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tif n > 0 {\n\t\t\treaded := buf[:n]\n\t\t\tif n == 1 && readed[0] == newLine[0] {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcommand.Write(readed)\n\t\t}\n\t}\n\tif err != nil && !errors.Is(err, io.EOF) {\n\t\tc.connection.channel.Close()\n\t}\n\treturn command.String(), err\n}\n\n// sendErrorMessage sends an error message and close the channel\n// we don't check write errors here, we have to close the channel anyway\n//\n//nolint:errcheck\nfunc (c *scpCommand) sendErrorMessage(fs vfs.Fs, err error) {\n\tc.connection.channel.Write(errMsg)\n\tif fs != nil {\n\t\tc.connection.channel.Write([]byte(c.connection.GetFsError(fs, err).Error()))\n\t} else {\n\t\tc.connection.channel.Write([]byte(err.Error()))\n\t}\n\tc.connection.channel.Write(newLine)\n\tc.connection.channel.Close()\n}\n\n// send scp confirmation message and close the channel if an error happen\nfunc (c *scpCommand) sendConfirmationMessage() error {\n\t_, err := c.connection.channel.Write(okMsg)\n\tif err != nil {\n\t\tc.connection.channel.Close()\n\t}\n\treturn err\n}\n\n// sends a protocol message and close the channel on error\nfunc (c *scpCommand) sendProtocolMessage(message string) error {\n\t_, err := c.connection.channel.Write([]byte(message))\n\tif err != nil {\n\t\tc.connection.Log(logger.LevelError, \"error sending protocol message: %v, err: %v\", message, err)\n\t\tc.connection.channel.Close()\n\t}\n\treturn err\n}\n\n// get the next upload protocol message ignoring T command if any\nfunc (c *scpCommand) getNextUploadProtocolMessage() (string, error) {\n\tvar command string\n\tvar err error\n\tfor {\n\t\tcommand, err = c.readProtocolMessage()\n\t\tif err != nil {\n\t\t\treturn command, err\n\t\t}\n\t\tif strings.HasPrefix(command, \"T\") {\n\t\t\terr = c.sendConfirmationMessage()\n\t\t\tif err != nil {\n\t\t\t\treturn command, err\n\t\t\t}\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn command, err\n}\n\nfunc (c *scpCommand) createDir(fs vfs.Fs, dirPath string) error {\n\terr := fs.Mkdir(dirPath)\n\tif err != nil {\n\t\tc.connection.Log(logger.LevelError, \"error creating dir %q: %v\", dirPath, err)\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn err\n\t}\n\tvfs.SetPathPermissions(fs, dirPath, c.connection.User.GetUID(), c.connection.User.GetGID())\n\treturn err\n}\n\n// parse protocol messages such as:\n// D0755 0 testdir\n// or:\n// C0644 6 testfile\n// and returns file size and file/directory name\nfunc (c *scpCommand) parseUploadMessage(fs vfs.Fs, command string) (int64, string, error) {\n\tvar size int64\n\tvar name string\n\tvar err error\n\tif !strings.HasPrefix(command, \"C\") && !strings.HasPrefix(command, \"D\") {\n\t\terr = fmt.Errorf(\"unknown or invalid upload message: %v args: %v user: %v\",\n\t\t\tcommand, c.args, c.connection.User.Username)\n\t\tc.connection.Log(logger.LevelError, \"error: %v\", err)\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn size, name, err\n\t}\n\tparts := strings.SplitN(command, \" \", 3)\n\tif len(parts) == 3 {\n\t\tsize, err = strconv.ParseInt(parts[1], 10, 64)\n\t\tif err != nil {\n\t\t\tc.connection.Log(logger.LevelError, \"error getting size from upload message: %v\", err)\n\t\t\tc.sendErrorMessage(fs, err)\n\t\t\treturn size, name, err\n\t\t}\n\t\tname = parts[2]\n\t\tif name == \"\" {\n\t\t\terr = fmt.Errorf(\"error getting name from upload message, cannot be empty\")\n\t\t\tc.connection.Log(logger.LevelError, \"error: %v\", err)\n\t\t\tc.sendErrorMessage(fs, err)\n\t\t\treturn size, name, err\n\t\t}\n\t} else {\n\t\terr = fmt.Errorf(\"unable to split upload message: %q\", command)\n\t\tc.connection.Log(logger.LevelError, \"error: %v\", err)\n\t\tc.sendErrorMessage(fs, err)\n\t\treturn size, name, err\n\t}\n\treturn size, name, err\n}\n\nfunc (c *scpCommand) getFileUploadDestPath(fs vfs.Fs, scpDestPath, fileName string) string {\n\tif !c.isRecursive() {\n\t\t// if the upload is not recursive and the destination path does not end with \"/\"\n\t\t// then scpDestPath is the wanted filename, for example:\n\t\t// scp fileName.txt user@127.0.0.1:/newFileName.txt\n\t\t// or\n\t\t// scp fileName.txt user@127.0.0.1:/fileName.txt\n\t\tif !strings.HasSuffix(scpDestPath, \"/\") {\n\t\t\t// but if scpDestPath is an existing directory then we put the uploaded file\n\t\t\t// inside that directory this is as scp command works, for example:\n\t\t\t// scp fileName.txt user@127.0.0.1:/existing_dir\n\t\t\tif p, err := fs.ResolvePath(scpDestPath); err == nil {\n\t\t\t\tif stat, err := fs.Stat(p); err == nil {\n\t\t\t\t\tif stat.IsDir() {\n\t\t\t\t\t\treturn path.Join(scpDestPath, fileName)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn scpDestPath\n\t\t}\n\t}\n\t// if the upload is recursive or scpDestPath has the \"/\" suffix then the destination\n\t// file is relative to scpDestPath\n\treturn path.Join(scpDestPath, fileName)\n}\n\nfunc getFileModeAsString(fileMode os.FileMode, isDir bool) string {\n\tvar defaultMode string\n\tif isDir {\n\t\tdefaultMode = \"0755\"\n\t} else {\n\t\tdefaultMode = \"0644\"\n\t}\n\tif fileMode == 0 {\n\t\treturn defaultMode\n\t}\n\tmodeString := []byte(fileMode.String())\n\tnullPerm := []byte(\"-\")\n\tu := 0\n\tg := 0\n\to := 0\n\ts := 0\n\tlastChar := len(modeString) - 1\n\tif fileMode&os.ModeSticky != 0 {\n\t\ts++\n\t}\n\tif fileMode&os.ModeSetuid != 0 {\n\t\ts += 2\n\t}\n\tif fileMode&os.ModeSetgid != 0 {\n\t\ts += 4\n\t}\n\tif modeString[lastChar-8] != nullPerm[0] {\n\t\tu += 4\n\t}\n\tif modeString[lastChar-7] != nullPerm[0] {\n\t\tu += 2\n\t}\n\tif modeString[lastChar-6] != nullPerm[0] {\n\t\tu++\n\t}\n\tif modeString[lastChar-5] != nullPerm[0] {\n\t\tg += 4\n\t}\n\tif modeString[lastChar-4] != nullPerm[0] {\n\t\tg += 2\n\t}\n\tif modeString[lastChar-3] != nullPerm[0] {\n\t\tg++\n\t}\n\tif modeString[lastChar-2] != nullPerm[0] {\n\t\to += 4\n\t}\n\tif modeString[lastChar-1] != nullPerm[0] {\n\t\to += 2\n\t}\n\tif modeString[lastChar] != nullPerm[0] {\n\t\to++\n\t}\n\treturn fmt.Sprintf(\"%v%v%v%v\", s, u, g, o)\n}\n"
  },
  {
    "path": "internal/sftpd/server.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"maps\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/sftp\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tdefaultPrivateRSAKeyName          = \"id_rsa\"\n\tdefaultPrivateECDSAKeyName        = \"id_ecdsa\"\n\tdefaultPrivateEd25519KeyName      = \"id_ed25519\"\n\tsourceAddressCriticalOption       = \"source-address\"\n\tkeyExchangeCurve25519SHA256LibSSH = \"curve25519-sha256@libssh.org\"\n\textraDataPartialSuccessErrKey     = \"partialSuccessErr\"\n\textraDataUserKey                  = \"user\"\n\textraDataKeyIDKey                 = \"keyID\"\n\textraDataLoginMethodKey           = \"login_method\"\n)\n\nvar (\n\tsupportedAlgos        = ssh.SupportedAlgorithms()\n\tinsecureAlgos         = ssh.InsecureAlgorithms()\n\tsftpExtensions        = []string{\"statvfs@openssh.com\"}\n\tsupportedHostKeyAlgos = append(supportedAlgos.HostKeys, insecureAlgos.HostKeys...)\n\tpreferredHostKeyAlgos = []string{\n\t\tssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512,\n\t\tssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,\n\t\tssh.KeyAlgoED25519,\n\t}\n\tsupportedPublicKeyAlgos = append(supportedAlgos.PublicKeyAuths, insecureAlgos.PublicKeyAuths...)\n\tpreferredPublicKeyAlgos = supportedAlgos.PublicKeyAuths\n\tsupportedKexAlgos       = append(supportedAlgos.KeyExchanges, insecureAlgos.KeyExchanges...)\n\tpreferredKexAlgos       = supportedAlgos.KeyExchanges\n\tsupportedCiphers        = append(supportedAlgos.Ciphers, insecureAlgos.Ciphers...)\n\tpreferredCiphers        = supportedAlgos.Ciphers\n\tsupportedMACs           = append(supportedAlgos.MACs, insecureAlgos.MACs...)\n\tpreferredMACs           = []string{\n\t\tssh.HMACSHA256ETM, ssh.HMACSHA256,\n\t}\n\n\trevokedCertManager = revokedCertificates{\n\t\tcerts: map[string]bool{},\n\t}\n)\n\ntype commandExecutor interface {\n\tCombinedOutput(ctx context.Context, name string, args ...string) ([]byte, error)\n}\n\ntype defaultExecutor struct{}\n\nfunc (d defaultExecutor) CombinedOutput(ctx context.Context, name string, args ...string) ([]byte, error) {\n\tcmd := exec.CommandContext(ctx, name, args...)\n\tcmd.Env = []string{}\n\treturn cmd.CombinedOutput()\n}\n\n// Binding defines the configuration for a network listener\ntype Binding struct {\n\t// The address to listen on. A blank value means listen on all available network interfaces.\n\tAddress string `json:\"address\" mapstructure:\"address\"`\n\t// The port used for serving requests\n\tPort int `json:\"port\" mapstructure:\"port\"`\n\t// Apply the proxy configuration, if any, for this binding\n\tApplyProxyConfig bool `json:\"apply_proxy_config\" mapstructure:\"apply_proxy_config\"`\n}\n\n// GetAddress returns the binding address\nfunc (b *Binding) GetAddress() string {\n\treturn fmt.Sprintf(\"%s:%d\", b.Address, b.Port)\n}\n\n// IsValid returns true if the binding port is > 0\nfunc (b *Binding) IsValid() bool {\n\treturn b.Port > 0\n}\n\n// HasProxy returns true if the proxy protocol is active for this binding\nfunc (b *Binding) HasProxy() bool {\n\treturn b.ApplyProxyConfig && common.Config.ProxyProtocol > 0\n}\n\n// Configuration for the SFTP server\ntype Configuration struct {\n\t// Addresses and ports to bind to\n\tBindings []Binding `json:\"bindings\" mapstructure:\"bindings\"`\n\t// Maximum number of authentication attempts permitted per connection.\n\t// If set to a negative number, the number of attempts is unlimited.\n\t// If set to zero, the number of attempts are limited to 6.\n\tMaxAuthTries int `json:\"max_auth_tries\" mapstructure:\"max_auth_tries\"`\n\t// HostKeys define the daemon's private host keys.\n\t// Each host key can be defined as a path relative to the configuration directory or an absolute one.\n\t// If empty or missing, the daemon will search or try to generate \"id_rsa\" and \"id_ecdsa\" host keys\n\t// inside the configuration directory.\n\tHostKeys []string `json:\"host_keys\" mapstructure:\"host_keys\"`\n\t// HostCertificates defines public host certificates.\n\t// Each certificate can be defined as a path relative to the configuration directory or an absolute one.\n\t// Certificate's public key must match a private host key otherwise it will be silently ignored.\n\tHostCertificates []string `json:\"host_certificates\" mapstructure:\"host_certificates\"`\n\t// HostKeyAlgorithms lists the public key algorithms that the server will accept for host\n\t// key authentication.\n\tHostKeyAlgorithms []string `json:\"host_key_algorithms\" mapstructure:\"host_key_algorithms\"`\n\t// KexAlgorithms specifies the available KEX (Key Exchange) algorithms in\n\t// preference order.\n\tKexAlgorithms []string `json:\"kex_algorithms\" mapstructure:\"kex_algorithms\"`\n\t// Ciphers specifies the ciphers allowed\n\tCiphers []string `json:\"ciphers\" mapstructure:\"ciphers\"`\n\t// MACs Specifies the available MAC (message authentication code) algorithms\n\t// in preference order\n\tMACs []string `json:\"macs\" mapstructure:\"macs\"`\n\t// PublicKeyAlgorithms lists the supported public key algorithms for client authentication.\n\tPublicKeyAlgorithms []string `json:\"public_key_algorithms\" mapstructure:\"public_key_algorithms\"`\n\t// TrustedUserCAKeys specifies a list of public keys paths of certificate authorities\n\t// that are trusted to sign user certificates for authentication.\n\t// The paths can be absolute or relative to the configuration directory\n\tTrustedUserCAKeys []string `json:\"trusted_user_ca_keys\" mapstructure:\"trusted_user_ca_keys\"`\n\t// Path to a file containing the revoked user certificates.\n\t// This file must contain a JSON list with the public key fingerprints of the revoked certificates.\n\t// Example content:\n\t// [\"SHA256:bsBRHC/xgiqBJdSuvSTNpJNLTISP/G356jNMCRYC5Es\",\"SHA256:119+8cL/HH+NLMawRsJx6CzPF1I3xC+jpM60bQHXGE8\"]\n\tRevokedUserCertsFile string `json:\"revoked_user_certs_file\" mapstructure:\"revoked_user_certs_file\"`\n\t// Absolute path to the opkssh binary used for OpenPubkey SSH integration\n\tOPKSSHPath string `json:\"opkssh_path\" mapstructure:\"opkssh_path\"`\n\t// Expected SHA256 checksum of the opkssh binary. It is verified at application startup\n\tOPKSSHChecksum string `json:\"opkssh_checksum\" mapstructure:\"opkssh_checksum\"`\n\t// LoginBannerFile the contents of the specified file, if any, are sent to\n\t// the remote user before authentication is allowed.\n\tLoginBannerFile string `json:\"login_banner_file\" mapstructure:\"login_banner_file\"`\n\t// List of enabled SSH commands.\n\t// We support the following SSH commands:\n\t// - \"scp\". SCP is an experimental feature, we have our own SCP implementation since\n\t//      we can't rely on scp system command to proper handle permissions, quota and\n\t//      user's home dir restrictions.\n\t// \t\tThe SCP protocol is quite simple but there is no official docs about it,\n\t// \t\tso we need more testing and feedbacks before enabling it by default.\n\t// \t\tWe may not handle some borderline cases or have sneaky bugs.\n\t// \t\tPlease do accurate tests yourself before enabling SCP and let us known\n\t// \t\tif something does not work as expected for your use cases.\n\t//      SCP between two remote hosts is supported using the `-3` scp option.\n\t// - \"md5sum\", \"sha1sum\", \"sha256sum\", \"sha384sum\", \"sha512sum\". Useful to check message\n\t//      digests for uploaded files. These commands are implemented inside SFTPGo so they\n\t//      work even if the matching system commands are not available, for example on Windows.\n\t// - \"cd\", \"pwd\". Some mobile SFTP clients does not support the SFTP SSH_FXP_REALPATH and so\n\t//      they use \"cd\" and \"pwd\" SSH commands to get the initial directory.\n\t//      Currently `cd` do nothing and `pwd` always returns the \"/\" path.\n\t//\n\t// The following SSH commands are enabled by default: \"md5sum\", \"sha1sum\", \"cd\", \"pwd\".\n\t// \"*\" enables all supported SSH commands.\n\tEnabledSSHCommands []string `json:\"enabled_ssh_commands\" mapstructure:\"enabled_ssh_commands\"`\n\t// KeyboardInteractiveAuthentication specifies whether keyboard interactive authentication is allowed.\n\t// If no keyboard interactive hook or auth plugin is defined the default is to prompt for the user password and then the\n\t// one time authentication code, if defined.\n\tKeyboardInteractiveAuthentication bool `json:\"keyboard_interactive_authentication\" mapstructure:\"keyboard_interactive_authentication\"`\n\t// Absolute path to an external program or an HTTP URL to invoke for keyboard interactive authentication.\n\t// Leave empty to disable this authentication mode.\n\tKeyboardInteractiveHook string `json:\"keyboard_interactive_auth_hook\" mapstructure:\"keyboard_interactive_auth_hook\"`\n\t// PasswordAuthentication specifies whether password authentication is allowed.\n\tPasswordAuthentication bool `json:\"password_authentication\" mapstructure:\"password_authentication\"`\n\tcertChecker            *ssh.CertChecker\n\tparsedUserCAKeys       []ssh.PublicKey\n\texecutor               commandExecutor\n}\n\ntype authenticationError struct {\n\terr         error\n\tloginMethod string\n\tusername    string\n}\n\nfunc (e *authenticationError) Error() string {\n\treturn fmt.Sprintf(\"Authentication error: %v\", e.err)\n}\n\n// Is reports if target matches\nfunc (e *authenticationError) Is(target error) bool {\n\t_, ok := target.(*authenticationError)\n\treturn ok\n}\n\n// Unwrap returns the wrapped error\nfunc (e *authenticationError) Unwrap() error {\n\treturn e.err\n}\n\nfunc (e *authenticationError) getLoginMethod() string {\n\treturn e.loginMethod\n}\n\nfunc (e *authenticationError) getUsername() string {\n\treturn e.username\n}\n\nfunc newAuthenticationError(err error, loginMethod, username string) *authenticationError {\n\treturn &authenticationError{err: err, loginMethod: loginMethod, username: username}\n}\n\n// ShouldBind returns true if there is at least a valid binding\nfunc (c *Configuration) ShouldBind() bool {\n\tfor _, binding := range c.Bindings {\n\t\tif binding.IsValid() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *Configuration) getServerConfig() *ssh.ServerConfig {\n\tserverConfig := &ssh.ServerConfig{\n\t\tNoClientAuth: false,\n\t\tMaxAuthTries: c.MaxAuthTries,\n\t\tPublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {\n\t\t\tsp, err := c.validatePublicKeyCredentials(conn, pubKey)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, newAuthenticationError(fmt.Errorf(\"could not validate public key credentials: %w\", err),\n\t\t\t\t\tdataprovider.SSHLoginMethodPublicKey, conn.User())\n\t\t\t}\n\n\t\t\treturn sp, nil\n\t\t},\n\t\tVerifiedPublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey, permissions *ssh.Permissions, signatureAlgorithm string) (*ssh.Permissions, error) {\n\t\t\tif partialErr, ok := permissions.ExtraData[extraDataPartialSuccessErrKey]; ok {\n\t\t\t\tlogger.Info(logSender, hex.EncodeToString(conn.SessionID()), \"user %q authenticated with partial success, signature algorithm %q\",\n\t\t\t\t\tconn.User(), signatureAlgorithm)\n\t\t\t\treturn nil, partialErr.(error)\n\t\t\t}\n\t\t\tmethod := dataprovider.SSHLoginMethodPublicKey\n\t\t\tuser := permissions.ExtraData[extraDataUserKey].(dataprovider.User)\n\t\t\tkeyID := permissions.ExtraData[extraDataKeyIDKey].(string)\n\t\t\tsshPerm, err := loginUser(&user, method, fmt.Sprintf(\"%s (%s)\", keyID, signatureAlgorithm), conn)\n\t\t\tif err == nil {\n\t\t\t\t// if we have a SSH user cert we need to merge certificate permissions with our ones\n\t\t\t\t// we only set Extensions, so CriticalOptions are always the ones from the certificate\n\t\t\t\tsshPerm.CriticalOptions = permissions.CriticalOptions\n\t\t\t\tif permissions.Extensions != nil {\n\t\t\t\t\tif sshPerm.Extensions == nil {\n\t\t\t\t\t\tsshPerm.Extensions = make(map[string]string)\n\t\t\t\t\t}\n\t\t\t\t\tmaps.Copy(sshPerm.Extensions, permissions.Extensions)\n\t\t\t\t}\n\t\t\t\tif sshPerm.ExtraData == nil {\n\t\t\t\t\tsshPerm.ExtraData = make(map[any]any)\n\t\t\t\t}\n\t\t\t}\n\t\t\tuser.Username = conn.User()\n\t\t\tipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())\n\t\t\tupdateLoginMetrics(&user, ipAddr, method, err)\n\t\t\treturn sshPerm, err\n\t\t},\n\t\tServerVersion: fmt.Sprintf(\"SSH-2.0-%s\", version.GetServerVersion(\"_\", false)),\n\t}\n\n\tif c.PasswordAuthentication {\n\t\tserverConfig.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {\n\t\t\treturn c.validatePasswordCredentials(conn, password, dataprovider.LoginMethodPassword)\n\t\t}\n\t\tserviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.LoginMethodPassword)\n\t}\n\tserviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey)\n\n\treturn serverConfig\n}\n\nfunc (c *Configuration) updateSupportedAuthentications() {\n\tserviceStatus.Authentications = util.RemoveDuplicates(serviceStatus.Authentications, false)\n\n\tif slices.Contains(serviceStatus.Authentications, dataprovider.LoginMethodPassword) &&\n\t\tslices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {\n\t\tserviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndPassword)\n\t}\n\n\tif slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyboardInteractive) &&\n\t\tslices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {\n\t\tserviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndKeyboardInt)\n\t}\n}\n\nfunc (c *Configuration) loadFromProvider() error {\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to load config from provider: %w\", err)\n\t}\n\tconfigs.SetNilsToEmpty()\n\tif len(configs.SFTPD.HostKeyAlgos) > 0 {\n\t\tif len(c.HostKeyAlgorithms) == 0 {\n\t\t\tc.HostKeyAlgorithms = preferredHostKeyAlgos\n\t\t}\n\t\tc.HostKeyAlgorithms = append(c.HostKeyAlgorithms, configs.SFTPD.HostKeyAlgos...)\n\t}\n\tif len(configs.SFTPD.PublicKeyAlgos) > 0 {\n\t\tif len(c.PublicKeyAlgorithms) == 0 {\n\t\t\tc.PublicKeyAlgorithms = preferredPublicKeyAlgos\n\t\t}\n\t\tc.PublicKeyAlgorithms = append(c.PublicKeyAlgorithms, configs.SFTPD.PublicKeyAlgos...)\n\t}\n\tif len(configs.SFTPD.KexAlgorithms) > 0 {\n\t\tif len(c.KexAlgorithms) == 0 {\n\t\t\tc.KexAlgorithms = preferredKexAlgos\n\t\t}\n\t\tc.KexAlgorithms = append(c.KexAlgorithms, configs.SFTPD.KexAlgorithms...)\n\t}\n\tif len(configs.SFTPD.Ciphers) > 0 {\n\t\tif len(c.Ciphers) == 0 {\n\t\t\tc.Ciphers = preferredCiphers\n\t\t}\n\t\tc.Ciphers = append(c.Ciphers, configs.SFTPD.Ciphers...)\n\t}\n\tif len(configs.SFTPD.MACs) > 0 {\n\t\tif len(c.MACs) == 0 {\n\t\t\tc.MACs = preferredMACs\n\t\t}\n\t\tc.MACs = append(c.MACs, configs.SFTPD.MACs...)\n\t}\n\treturn nil\n}\n\n// Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections.\nfunc (c *Configuration) Initialize(configDir string) error {\n\tc.executor = defaultExecutor{}\n\tif err := c.loadFromProvider(); err != nil {\n\t\treturn fmt.Errorf(\"unable to load configs from provider: %w\", err)\n\t}\n\tserviceStatus = ServiceStatus{}\n\tserverConfig := c.getServerConfig()\n\n\tif !c.ShouldBind() {\n\t\treturn common.ErrNoBinding\n\t}\n\n\tsftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error\n\tsftp.MaxFilelist = 250\n\n\tif err := c.configureSecurityOptions(serverConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {\n\t\tserviceStatus.HostKeys = nil\n\t\treturn err\n\t}\n\tif err := c.initializeCertChecker(configDir); err != nil {\n\t\treturn err\n\t}\n\tif err := c.initializeOPKSSH(); err != nil {\n\t\treturn err\n\t}\n\tc.configureKeyboardInteractiveAuth(serverConfig)\n\tc.configureLoginBanner(serverConfig, configDir)\n\tc.checkSSHCommands()\n\n\texitChannel := make(chan error, 1)\n\tserviceStatus.Bindings = nil\n\n\tfor _, binding := range c.Bindings {\n\t\tif !binding.IsValid() {\n\t\t\tcontinue\n\t\t}\n\t\tserviceStatus.Bindings = append(serviceStatus.Bindings, binding)\n\n\t\tgo func(binding Binding) {\n\t\t\taddr := binding.GetAddress()\n\t\t\tutil.CheckTCP4Port(binding.Port)\n\t\t\tlistener, err := net.Listen(\"tcp\", addr)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error starting listener on address %v: %v\", addr, err)\n\t\t\t\texitChannel <- err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif binding.ApplyProxyConfig && common.Config.ProxyProtocol > 0 {\n\t\t\t\tproxyListener, err := common.Config.GetProxyListener(listener)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogger.Warn(logSender, \"\", \"error enabling proxy listener: %v\", err)\n\t\t\t\t\texitChannel <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlistener = proxyListener\n\t\t\t}\n\n\t\t\texitChannel <- c.serve(listener, serverConfig)\n\t\t}(binding)\n\t}\n\n\tserviceStatus.IsActive = true\n\tserviceStatus.SSHCommands = c.EnabledSSHCommands\n\tc.updateSupportedAuthentications()\n\n\treturn <-exitChannel\n}\n\nfunc (c *Configuration) serve(listener net.Listener, serverConfig *ssh.ServerConfig) error {\n\tlogger.Info(logSender, \"\", \"server listener registered, address: %s\", listener.Addr().String())\n\tvar tempDelay time.Duration // how long to sleep on accept failure\n\n\tfor {\n\t\tconn, err := listener.Accept()\n\t\tif err != nil {\n\t\t\t// see https://github.com/golang/go/blob/4aa1efed4853ea067d665a952eee77c52faac774/src/net/http/server.go#L3046\n\t\t\tif ne, ok := err.(net.Error); ok && ne.Temporary() { //nolint:staticcheck\n\t\t\t\tif tempDelay == 0 {\n\t\t\t\t\ttempDelay = 5 * time.Millisecond\n\t\t\t\t} else {\n\t\t\t\t\ttempDelay *= 2\n\t\t\t\t}\n\t\t\t\tif maxDelay := 1 * time.Second; tempDelay > maxDelay {\n\t\t\t\t\ttempDelay = maxDelay\n\t\t\t\t}\n\t\t\t\tlogger.Warn(logSender, \"\", \"accept error: %v; retrying in %v\", err, tempDelay)\n\t\t\t\ttime.Sleep(tempDelay)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogger.Warn(logSender, \"\", \"unrecoverable accept error: %v\", err)\n\t\t\treturn err\n\t\t}\n\t\ttempDelay = 0\n\n\t\tgo c.AcceptInboundConnection(conn, serverConfig)\n\t}\n}\n\nfunc (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error {\n\tif len(c.HostKeyAlgorithms) == 0 {\n\t\tc.HostKeyAlgorithms = preferredHostKeyAlgos\n\t} else {\n\t\tc.HostKeyAlgorithms = util.RemoveDuplicates(c.HostKeyAlgorithms, true)\n\t}\n\tfor _, hostKeyAlgo := range c.HostKeyAlgorithms {\n\t\tif !slices.Contains(supportedHostKeyAlgos, hostKeyAlgo) {\n\t\t\treturn fmt.Errorf(\"unsupported host key algorithm %q\", hostKeyAlgo)\n\t\t}\n\t}\n\n\tif len(c.PublicKeyAlgorithms) > 0 {\n\t\tc.PublicKeyAlgorithms = util.RemoveDuplicates(c.PublicKeyAlgorithms, true)\n\t\tfor _, algo := range c.PublicKeyAlgorithms {\n\t\t\tif !slices.Contains(supportedPublicKeyAlgos, algo) {\n\t\t\t\treturn fmt.Errorf(\"unsupported public key authentication algorithm %q\", algo)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tc.PublicKeyAlgorithms = preferredPublicKeyAlgos\n\t}\n\tserverConfig.PublicKeyAuthAlgorithms = c.PublicKeyAlgorithms\n\tserviceStatus.PublicKeyAlgorithms = c.PublicKeyAlgorithms\n\n\treturn nil\n}\n\nfunc (c *Configuration) checkKeyExchangeAlgorithms() {\n\tvar kexs []string\n\tfor _, k := range c.KexAlgorithms {\n\t\tif k == \"diffie-hellman-group18-sha512\" {\n\t\t\tlogger.Warn(logSender, \"\", \"KEX %q is not supported and will be ignored\", k)\n\t\t\tcontinue\n\t\t}\n\t\tkexs = append(kexs, k)\n\t\tif strings.TrimSpace(k) == keyExchangeCurve25519SHA256LibSSH {\n\t\t\tkexs = append(kexs, ssh.KeyExchangeCurve25519)\n\t\t}\n\t\tif strings.TrimSpace(k) == ssh.KeyExchangeCurve25519 {\n\t\t\tkexs = append(kexs, keyExchangeCurve25519SHA256LibSSH)\n\t\t}\n\t}\n\tc.KexAlgorithms = util.RemoveDuplicates(kexs, true)\n}\n\nfunc (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig) error {\n\tif err := c.configureKeyAlgos(serverConfig); err != nil {\n\t\treturn err\n\t}\n\n\tif len(c.KexAlgorithms) > 0 {\n\t\tc.checkKeyExchangeAlgorithms()\n\t\tfor _, kex := range c.KexAlgorithms {\n\t\t\tif kex == keyExchangeCurve25519SHA256LibSSH {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !slices.Contains(supportedKexAlgos, kex) {\n\t\t\t\treturn fmt.Errorf(\"unsupported key-exchange algorithm %q\", kex)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tc.KexAlgorithms = preferredKexAlgos\n\t\tc.checkKeyExchangeAlgorithms()\n\t}\n\tserverConfig.KeyExchanges = c.KexAlgorithms\n\tserviceStatus.KexAlgorithms = c.KexAlgorithms\n\n\tif len(c.Ciphers) > 0 {\n\t\tc.Ciphers = util.RemoveDuplicates(c.Ciphers, true)\n\t\tfor _, cipher := range c.Ciphers {\n\t\t\tif slices.Contains([]string{\"aes192-cbc\", \"aes256-cbc\"}, cipher) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !slices.Contains(supportedCiphers, cipher) {\n\t\t\t\treturn fmt.Errorf(\"unsupported cipher %q\", cipher)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tc.Ciphers = preferredCiphers\n\t}\n\tserverConfig.Ciphers = c.Ciphers\n\tserviceStatus.Ciphers = c.Ciphers\n\n\tif len(c.MACs) > 0 {\n\t\tc.MACs = util.RemoveDuplicates(c.MACs, true)\n\t\tfor _, mac := range c.MACs {\n\t\t\tif !slices.Contains(supportedMACs, mac) {\n\t\t\t\treturn fmt.Errorf(\"unsupported MAC algorithm %q\", mac)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tc.MACs = preferredMACs\n\t}\n\tserverConfig.MACs = c.MACs\n\tserviceStatus.MACs = c.MACs\n\n\treturn nil\n}\n\nfunc (c *Configuration) configureLoginBanner(serverConfig *ssh.ServerConfig, configDir string) {\n\tif c.LoginBannerFile != \"\" {\n\t\tbannerFilePath := c.LoginBannerFile\n\t\tif !filepath.IsAbs(bannerFilePath) {\n\t\t\tbannerFilePath = filepath.Join(configDir, bannerFilePath)\n\t\t}\n\t\tbannerContent, err := os.ReadFile(bannerFilePath)\n\t\tif err == nil {\n\t\t\tbanner := util.BytesToString(bannerContent)\n\t\t\tserverConfig.BannerCallback = func(_ ssh.ConnMetadata) string {\n\t\t\t\treturn banner\n\t\t\t}\n\t\t} else {\n\t\t\tlogger.WarnToConsole(\"unable to read SFTPD login banner file: %v\", err)\n\t\t\tlogger.Warn(logSender, \"\", \"unable to read login banner file: %v\", err)\n\t\t}\n\t}\n}\n\nfunc (c *Configuration) configureKeyboardInteractiveAuth(serverConfig *ssh.ServerConfig) {\n\tif !c.KeyboardInteractiveAuthentication {\n\t\treturn\n\t}\n\tif c.KeyboardInteractiveHook != \"\" {\n\t\tif !strings.HasPrefix(c.KeyboardInteractiveHook, \"http\") {\n\t\t\tif !filepath.IsAbs(c.KeyboardInteractiveHook) {\n\t\t\t\tc.KeyboardInteractiveAuthentication = false\n\t\t\t\tlogger.WarnToConsole(\"invalid keyboard interactive authentication program: %q must be an absolute path\",\n\t\t\t\t\tc.KeyboardInteractiveHook)\n\t\t\t\tlogger.Warn(logSender, \"\", \"invalid keyboard interactive authentication program: %q must be an absolute path\",\n\t\t\t\t\tc.KeyboardInteractiveHook)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_, err := os.Stat(c.KeyboardInteractiveHook)\n\t\t\tif err != nil {\n\t\t\t\tc.KeyboardInteractiveAuthentication = false\n\t\t\t\tlogger.WarnToConsole(\"invalid keyboard interactive authentication program:: %v\", err)\n\t\t\t\tlogger.Warn(logSender, \"\", \"invalid keyboard interactive authentication program:: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\tserverConfig.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {\n\t\treturn c.validateKeyboardInteractiveCredentials(conn, client, dataprovider.SSHLoginMethodKeyboardInteractive, false)\n\t}\n\n\tserviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyboardInteractive)\n}\n\n// AcceptInboundConnection handles an inbound connection to the server instance and determines if the request should be served or not.\nfunc (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.ServerConfig) { //nolint:gocyclo\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlogger.Error(logSender, \"\", \"panic in AcceptInboundConnection: %q stack trace: %v\", r, string(debug.Stack()))\n\t\t}\n\t}()\n\n\tipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())\n\tcommon.Connections.AddClientConnection(ipAddr)\n\tdefer common.Connections.RemoveClientConnection(ipAddr)\n\n\tif !canAcceptConnection(ipAddr) {\n\t\tconn.Close()\n\t\treturn\n\t}\n\t// Before beginning a handshake must be performed on the incoming net.Conn\n\t// we'll set a Deadline for handshake to complete, the default is 2 minutes as OpenSSH\n\tconn.SetDeadline(time.Now().Add(handshakeTimeout)) //nolint:errcheck\n\n\tsconn, chans, reqs, err := ssh.NewServerConn(conn, config)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"failed to accept an incoming connection from ip %q: %v\", ipAddr, err)\n\t\tcheckAuthError(ipAddr, err)\n\t\treturn\n\t}\n\t// handshake completed so remove the deadline, we'll use IdleTimeout configuration from now on\n\tconn.SetDeadline(time.Time{}) //nolint:errcheck\n\tgo ssh.DiscardRequests(reqs)\n\n\tdefer sconn.Close()\n\n\tuser := sconn.Permissions.ExtraData[extraDataUserKey].(dataprovider.User)\n\tloginType := sconn.Permissions.ExtraData[extraDataLoginMethodKey].(string)\n\tconnectionID := hex.EncodeToString(sconn.SessionID())\n\n\tdefer user.CloseFs() //nolint:errcheck\n\tif err = user.CheckFsRoot(connectionID); err != nil {\n\t\tlogger.Warn(logSender, connectionID, \"unable to check fs root for user %q: %v\", user.Username, err)\n\t\tgo discardAllChannels(chans, \"invalid root fs\", connectionID)\n\t\treturn\n\t}\n\n\tlogger.LoginLog(user.Username, ipAddr, loginType, common.ProtocolSSH, connectionID,\n\t\tutil.BytesToString(sconn.ClientVersion()), true,\n\t\tfmt.Sprintf(\"negotiated algorithms: %+v\", sconn.Conn.(ssh.AlgorithmsConnMetadata).Algorithms()))\n\n\tdataprovider.UpdateLastLogin(&user)\n\n\tsshConnection := common.NewSSHConnection(connectionID, sconn)\n\tcommon.Connections.AddSSHConnection(sshConnection)\n\n\tdefer common.Connections.RemoveSSHConnection(connectionID)\n\n\tchannelCounter := int64(0)\n\tfor newChannel := range chans {\n\t\t// If its not a session channel we just move on because its not something we\n\t\t// know how to handle at this point.\n\t\tif newChannel.ChannelType() != \"session\" {\n\t\t\tlogger.Log(logger.LevelDebug, common.ProtocolSSH, connectionID, \"received an unknown channel type: %v\",\n\t\t\t\tnewChannel.ChannelType())\n\t\t\tnewChannel.Reject(ssh.UnknownChannelType, \"unknown channel type\") //nolint:errcheck\n\t\t\tcontinue\n\t\t}\n\n\t\tchannel, requests, err := newChannel.Accept()\n\t\tif err != nil {\n\t\t\tlogger.Log(logger.LevelWarn, common.ProtocolSSH, connectionID, \"could not accept a channel: %v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tchannelCounter++\n\t\t// Channels have a type that is dependent on the protocol. For SFTP this is \"subsystem\"\n\t\t// with a payload that (should) be \"sftp\". Discard anything else we receive (\"pty\", \"shell\", etc)\n\t\tgo func(in <-chan *ssh.Request, counter int64) {\n\t\t\tfor req := range in {\n\t\t\t\tok := false\n\t\t\t\tconnID := fmt.Sprintf(\"%s_%d\", connectionID, counter)\n\n\t\t\t\tswitch req.Type {\n\t\t\t\tcase \"subsystem\":\n\t\t\t\t\tif bytes.Equal(req.Payload[4:], []byte(\"sftp\")) {\n\t\t\t\t\t\tok = true\n\t\t\t\t\t\tsshConnection.UpdateLastActivity()\n\t\t\t\t\t\tconnection := &Connection{\n\t\t\t\t\t\t\tBaseConnection: common.NewBaseConnection(connID, common.ProtocolSFTP, conn.LocalAddr().String(),\n\t\t\t\t\t\t\t\tconn.RemoteAddr().String(), user),\n\t\t\t\t\t\t\tClientVersion: util.BytesToString(sconn.ClientVersion()),\n\t\t\t\t\t\t\tRemoteAddr:    conn.RemoteAddr(),\n\t\t\t\t\t\t\tLocalAddr:     conn.LocalAddr(),\n\t\t\t\t\t\t\tchannel:       channel,\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgo c.handleSftpConnection(channel, connection)\n\t\t\t\t\t}\n\t\t\t\tcase \"exec\":\n\t\t\t\t\t// protocol will be set later inside processSSHCommand it could be SSH or SCP\n\t\t\t\t\tconnection := Connection{\n\t\t\t\t\t\tBaseConnection: common.NewBaseConnection(connID, \"sshd_exec\", conn.LocalAddr().String(),\n\t\t\t\t\t\t\tconn.RemoteAddr().String(), user),\n\t\t\t\t\t\tClientVersion: util.BytesToString(sconn.ClientVersion()),\n\t\t\t\t\t\tRemoteAddr:    conn.RemoteAddr(),\n\t\t\t\t\t\tLocalAddr:     conn.LocalAddr(),\n\t\t\t\t\t\tchannel:       channel,\n\t\t\t\t\t}\n\t\t\t\t\tok = processSSHCommand(req.Payload, &connection, c.EnabledSSHCommands)\n\t\t\t\t\tif ok {\n\t\t\t\t\t\tsshConnection.UpdateLastActivity()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif req.WantReply {\n\t\t\t\t\treq.Reply(ok, nil) //nolint:errcheck\n\t\t\t\t}\n\t\t\t}\n\t\t}(requests, channelCounter)\n\t}\n}\n\nfunc (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Connection) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlogger.Error(logSender, \"\", \"panic in handleSftpConnection: %q stack trace: %v\", r, string(debug.Stack()))\n\t\t}\n\t}()\n\tif err := common.Connections.Add(connection); err != nil {\n\t\tdefer connection.CloseFS() //nolint:errcheck\n\t\terrClose := connection.Disconnect()\n\t\tlogger.Info(logSender, \"\", \"unable to add connection: %v, close err: %v\", err, errClose)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\t// Create the server instance for the channel using the handler we created above.\n\tserver := sftp.NewRequestServer(channel, c.createHandlers(connection),\n\t\tsftp.WithStartDirectory(connection.User.Filters.StartDirectory))\n\n\tdefer server.Close()\n\tif err := server.Serve(); errors.Is(err, io.EOF) {\n\t\texitStatus := sshSubsystemExitStatus{Status: uint32(0)}\n\t\t_, err = channel.SendRequest(\"exit-status\", false, ssh.Marshal(&exitStatus))\n\t\tconnection.Log(logger.LevelInfo, \"connection closed, sent exit status %+v error: %v\", exitStatus, err)\n\t} else if err != nil {\n\t\tconnection.Log(logger.LevelError, \"connection closed with error: %v\", err)\n\t}\n}\n\nfunc (c *Configuration) createHandlers(connection *Connection) sftp.Handlers {\n\treturn sftp.Handlers{\n\t\tFileGet:  connection,\n\t\tFilePut:  connection,\n\t\tFileCmd:  connection,\n\t\tFileList: connection,\n\t}\n}\n\nfunc canAcceptConnection(ip string) bool {\n\tif common.IsBanned(ip, common.ProtocolSSH) {\n\t\tlogger.Log(logger.LevelDebug, common.ProtocolSSH, \"\", \"connection refused, ip %q is banned\", ip)\n\t\treturn false\n\t}\n\tif err := common.Connections.IsNewConnectionAllowed(ip, common.ProtocolSSH); err != nil {\n\t\tlogger.Log(logger.LevelDebug, common.ProtocolSSH, \"\", \"connection not allowed from ip %q: %v\", ip, err)\n\t\treturn false\n\t}\n\t_, err := common.LimitRate(common.ProtocolSSH, ip)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif err := common.Config.ExecutePostConnectHook(ip, common.ProtocolSSH); err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc discardAllChannels(in <-chan ssh.NewChannel, message, connectionID string) {\n\tfor req := range in {\n\t\terr := req.Reject(ssh.ConnectionFailed, message)\n\t\tlogger.Debug(logSender, connectionID, \"discarded channel request, message %q err: %v\", message, err)\n\t}\n}\n\nfunc checkAuthError(ip string, err error) {\n\tvar authErrors *ssh.ServerAuthError\n\tif errors.As(err, &authErrors) {\n\t\t// check public key auth errors here\n\t\tfor _, err := range authErrors.Errors {\n\t\t\tvar sftpAuthErr *authenticationError\n\t\t\tif errors.As(err, &sftpAuthErr) {\n\t\t\t\tif sftpAuthErr.getLoginMethod() == dataprovider.SSHLoginMethodPublicKey {\n\t\t\t\t\tevent := common.HostEventLoginFailed\n\t\t\t\t\tlogEv := notifier.LogEventTypeLoginFailed\n\t\t\t\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\t\t\t\tevent = common.HostEventUserNotFound\n\t\t\t\t\t\tlogEv = notifier.LogEventTypeLoginNoUser\n\t\t\t\t\t}\n\t\t\t\t\tcommon.AddDefenderEvent(ip, common.ProtocolSSH, event)\n\t\t\t\t\tplugin.Handler.NotifyLogEvent(logEv, common.ProtocolSSH, sftpAuthErr.getUsername(), ip, \"\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tlogger.ConnectionFailedLog(\"\", ip, dataprovider.LoginMethodNoAuthTried, common.ProtocolSSH, err.Error())\n\t\tmetric.AddNoAuthTried()\n\t\tcommon.AddDefenderEvent(ip, common.ProtocolSSH, common.HostEventNoLoginTried)\n\t\tdataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTried, ip, common.ProtocolSSH, err)\n\t\tlogEv := notifier.LogEventTypeNoLoginTried\n\t\tvar negotiationError *ssh.AlgorithmNegotiationError\n\t\tif errors.As(err, &negotiationError) {\n\t\t\tlogEv = notifier.LogEventTypeNotNegotiated\n\t\t}\n\t\tplugin.Handler.NotifyLogEvent(logEv, common.ProtocolSSH, \"\", ip, \"\", err)\n\t}\n}\n\nfunc loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.ConnMetadata) (*ssh.Permissions, error) {\n\tconnectionID := \"\"\n\tif conn != nil {\n\t\tconnectionID = hex.EncodeToString(conn.SessionID())\n\t}\n\tif !filepath.IsAbs(user.HomeDir) {\n\t\tlogger.Warn(logSender, connectionID, \"user %q has an invalid home dir: %q. Home dir must be an absolute path, login not allowed\",\n\t\t\tuser.Username, user.HomeDir)\n\t\treturn nil, fmt.Errorf(\"cannot login user with invalid home dir: %q\", user.HomeDir)\n\t}\n\tif slices.Contains(user.Filters.DeniedProtocols, common.ProtocolSSH) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, protocol SSH is not allowed\", user.Username)\n\t\treturn nil, fmt.Errorf(\"protocol SSH is not allowed for user %q\", user.Username)\n\t}\n\tif user.MaxSessions > 0 {\n\t\tactiveSessions := common.Connections.GetActiveSessions(user.Username)\n\t\tif activeSessions >= user.MaxSessions {\n\t\t\tlogger.Info(logSender, \"\", \"authentication refused for user: %q, too many open sessions: %v/%v\", user.Username,\n\t\t\t\tactiveSessions, user.MaxSessions)\n\t\t\treturn nil, fmt.Errorf(\"too many open sessions: %v\", activeSessions)\n\t\t}\n\t}\n\tif !user.IsLoginMethodAllowed(loginMethod, common.ProtocolSSH) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, login method %q is not allowed\",\n\t\t\tuser.Username, loginMethod)\n\t\treturn nil, fmt.Errorf(\"login method %q is not allowed for user %q\", loginMethod, user.Username)\n\t}\n\tif user.MustSetSecondFactorForProtocol(common.ProtocolSSH) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, second factor authentication is not set\",\n\t\t\tuser.Username)\n\t\treturn nil, fmt.Errorf(\"second factor authentication is not set for user %q\", user.Username)\n\t}\n\tremoteAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())\n\tif !user.IsLoginFromAddrAllowed(remoteAddr) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, remote address is not allowed: %v\",\n\t\t\tuser.Username, remoteAddr)\n\t\treturn nil, fmt.Errorf(\"login for user %q is not allowed from this address: %v\", user.Username, remoteAddr)\n\t}\n\n\tif publicKey != \"\" {\n\t\tloginMethod = fmt.Sprintf(\"%v: %v\", loginMethod, publicKey)\n\t}\n\tp := &ssh.Permissions{}\n\tp.ExtraData = make(map[any]any)\n\tp.ExtraData[extraDataUserKey] = *user\n\tp.ExtraData[extraDataLoginMethodKey] = loginMethod\n\treturn p, nil\n}\n\nfunc (c *Configuration) checkSSHCommands() {\n\tif slices.Contains(c.EnabledSSHCommands, \"*\") {\n\t\tc.EnabledSSHCommands = GetSupportedSSHCommands()\n\t\treturn\n\t}\n\tsshCommands := []string{}\n\tfor _, command := range c.EnabledSSHCommands {\n\t\tcommand = strings.TrimSpace(command)\n\t\tif slices.Contains(supportedSSHCommands, command) {\n\t\t\tsshCommands = append(sshCommands, command)\n\t\t} else {\n\t\t\tlogger.Warn(logSender, \"\", \"unsupported ssh command: %q ignored\", command)\n\t\t\tlogger.WarnToConsole(\"unsupported ssh command: %q ignored\", command)\n\t\t}\n\t}\n\tc.EnabledSSHCommands = sshCommands\n\tlogger.Debug(logSender, \"\", \"enabled SSH commands %v\", c.EnabledSSHCommands)\n}\n\nfunc (c *Configuration) generateDefaultHostKeys(configDir string) error {\n\tvar err error\n\tdefaultHostKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName, defaultPrivateEd25519KeyName}\n\tfor _, k := range defaultHostKeys {\n\t\tautoFile := filepath.Join(configDir, k)\n\t\tif _, err = os.Stat(autoFile); errors.Is(err, fs.ErrNotExist) {\n\t\t\tlogger.Info(logSender, \"\", \"No host keys configured and %q does not exist; try to create a new host key\", autoFile)\n\t\t\tlogger.InfoToConsole(\"No host keys configured and %q does not exist; try to create a new host key\", autoFile)\n\t\t\tswitch k {\n\t\t\tcase defaultPrivateRSAKeyName:\n\t\t\t\terr = util.GenerateRSAKeys(autoFile)\n\t\t\tcase defaultPrivateECDSAKeyName:\n\t\t\t\terr = util.GenerateECDSAKeys(autoFile)\n\t\t\tdefault:\n\t\t\t\terr = util.GenerateEd25519Keys(autoFile)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"error creating host key %q: %v\", autoFile, err)\n\t\t\t\tlogger.WarnToConsole(\"error creating host key %q: %v\", autoFile, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tc.HostKeys = append(c.HostKeys, k)\n\t}\n\n\treturn err\n}\n\nfunc (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {\n\tfor _, k := range c.HostKeys {\n\t\tk = strings.TrimSpace(k)\n\t\tif filepath.IsAbs(k) {\n\t\t\tif _, err := os.Stat(k); errors.Is(err, fs.ErrNotExist) {\n\t\t\t\tkeyName := filepath.Base(k)\n\t\t\t\tswitch keyName {\n\t\t\t\tcase defaultPrivateRSAKeyName:\n\t\t\t\t\tlogger.Info(logSender, \"\", \"try to create non-existent host key %q\", k)\n\t\t\t\t\tlogger.InfoToConsole(\"try to create non-existent host key %q\", k)\n\t\t\t\t\terr = util.GenerateRSAKeys(k)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Warn(logSender, \"\", \"error creating host key %q: %v\", k, err)\n\t\t\t\t\t\tlogger.WarnToConsole(\"error creating host key %q: %v\", k, err)\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\tcase defaultPrivateECDSAKeyName:\n\t\t\t\t\tlogger.Info(logSender, \"\", \"try to create non-existent host key %q\", k)\n\t\t\t\t\tlogger.InfoToConsole(\"try to create non-existent host key %q\", k)\n\t\t\t\t\terr = util.GenerateECDSAKeys(k)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Warn(logSender, \"\", \"error creating host key %q: %v\", k, err)\n\t\t\t\t\t\tlogger.WarnToConsole(\"error creating host key %q: %v\", k, err)\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\tcase defaultPrivateEd25519KeyName:\n\t\t\t\t\tlogger.Info(logSender, \"\", \"try to create non-existent host key %q\", k)\n\t\t\t\t\tlogger.InfoToConsole(\"try to create non-existent host key %q\", k)\n\t\t\t\t\terr = util.GenerateEd25519Keys(k)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Warn(logSender, \"\", \"error creating host key %q: %v\", k, err)\n\t\t\t\t\t\tlogger.WarnToConsole(\"error creating host key %q: %v\", k, err)\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tlogger.Warn(logSender, \"\", \"non-existent host key %q will not be created\", k)\n\t\t\t\t\tlogger.WarnToConsole(\"non-existent host key %q will not be created\", k)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif len(c.HostKeys) == 0 {\n\t\tif err := c.generateDefaultHostKeys(configDir); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *Configuration) getHostKeyAlgorithms(keyFormat string) []string {\n\tvar algos []string\n\tfor _, algo := range algorithmsForKeyFormat(keyFormat) {\n\t\tif slices.Contains(c.HostKeyAlgorithms, algo) {\n\t\t\talgos = append(algos, algo)\n\t\t}\n\t}\n\treturn algos\n}\n\n// If no host keys are defined we try to use or generate the default ones.\nfunc (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error {\n\tif err := c.checkHostKeyAutoGeneration(configDir); err != nil {\n\t\treturn err\n\t}\n\thostCertificates, err := c.loadHostCertificates(configDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tserviceStatus.HostKeys = nil\n\tfor _, hostKey := range c.HostKeys {\n\t\thostKey = strings.TrimSpace(hostKey)\n\t\tif !util.IsFileInputValid(hostKey) {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to load invalid host key %q\", hostKey)\n\t\t\tlogger.WarnToConsole(\"unable to load invalid host key %q\", hostKey)\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(hostKey) {\n\t\t\thostKey = filepath.Join(configDir, hostKey)\n\t\t}\n\t\tlogger.Info(logSender, \"\", \"Loading private host key %q\", hostKey)\n\n\t\tprivateBytes, err := os.ReadFile(hostKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tprivate, err := ssh.ParsePrivateKey(privateBytes)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tk := HostKey{\n\t\t\tPath:        hostKey,\n\t\t\tFingerprint: ssh.FingerprintSHA256(private.PublicKey()),\n\t\t\tAlgorithms:  c.getHostKeyAlgorithms(private.PublicKey().Type()),\n\t\t}\n\t\tmas, err := ssh.NewSignerWithAlgorithms(private.(ssh.AlgorithmSigner), k.Algorithms)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not create signer for key %q with algorithms %+v: %w\", k.Path, k.Algorithms, err)\n\t\t}\n\t\tserviceStatus.HostKeys = append(serviceStatus.HostKeys, k)\n\t\tlogger.Info(logSender, \"\", \"Host key %q loaded, type %q, fingerprint %q, algorithms %+v\", hostKey,\n\t\t\tprivate.PublicKey().Type(), k.Fingerprint, k.Algorithms)\n\n\t\t// Add private key to the server configuration.\n\t\tserverConfig.AddHostKey(mas)\n\t\tfor _, cert := range hostCertificates {\n\t\t\tsigner, err := ssh.NewCertSigner(cert.Certificate, mas)\n\t\t\tif err == nil {\n\t\t\t\tvar algos []string\n\t\t\t\tfor _, algo := range algorithmsForKeyFormat(signer.PublicKey().Type()) {\n\t\t\t\t\tif underlyingAlgo, ok := certKeyAlgoNames[algo]; ok {\n\t\t\t\t\t\tif slices.Contains(mas.Algorithms(), underlyingAlgo) {\n\t\t\t\t\t\t\talgos = append(algos, algo)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tserviceStatus.HostKeys = append(serviceStatus.HostKeys, HostKey{\n\t\t\t\t\tPath:        cert.Path,\n\t\t\t\t\tFingerprint: ssh.FingerprintSHA256(signer.PublicKey()),\n\t\t\t\t\tAlgorithms:  algos,\n\t\t\t\t})\n\t\t\t\tserverConfig.AddHostKey(signer)\n\t\t\t\tlogger.Info(logSender, \"\", \"Host certificate loaded for host key %q, fingerprint %q, algorithms %+v\",\n\t\t\t\t\thostKey, ssh.FingerprintSHA256(signer.PublicKey()), algos)\n\t\t\t}\n\t\t}\n\t}\n\tvar fp []string\n\tfor idx := range serviceStatus.HostKeys {\n\t\th := &serviceStatus.HostKeys[idx]\n\t\tfp = append(fp, h.Fingerprint)\n\t}\n\tvfs.SetSFTPFingerprints(fp)\n\treturn nil\n}\n\nfunc (c *Configuration) loadHostCertificates(configDir string) ([]hostCertificate, error) {\n\tvar certs []hostCertificate\n\tfor _, certPath := range c.HostCertificates {\n\t\tcertPath = strings.TrimSpace(certPath)\n\t\tif !util.IsFileInputValid(certPath) {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to load invalid host certificate %q\", certPath)\n\t\t\tlogger.WarnToConsole(\"unable to load invalid host certificate %q\", certPath)\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(certPath) {\n\t\t\tcertPath = filepath.Join(configDir, certPath)\n\t\t}\n\t\tcertBytes, err := os.ReadFile(certPath)\n\t\tif err != nil {\n\t\t\treturn certs, fmt.Errorf(\"unable to load host certificate %q: %w\", certPath, err)\n\t\t}\n\t\tparsed, _, _, _, err := ssh.ParseAuthorizedKey(certBytes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse host certificate %q: %w\", certPath, err)\n\t\t}\n\t\tcert, ok := parsed.(*ssh.Certificate)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"the file %q is not an SSH certificate\", certPath)\n\t\t}\n\t\tif cert.CertType != ssh.HostCert {\n\t\t\treturn nil, fmt.Errorf(\"the file %q is not an host certificate\", certPath)\n\t\t}\n\t\tcerts = append(certs, hostCertificate{\n\t\t\tPath:        certPath,\n\t\t\tCertificate: cert,\n\t\t})\n\t}\n\treturn certs, nil\n}\n\nfunc (c *Configuration) initializeOPKSSH() error {\n\tif c.OPKSSHPath != \"\" {\n\t\tif len(c.parsedUserCAKeys) > 0 {\n\t\t\treturn errors.New(\"opkssh and certificate authorities are mutually exclusive\")\n\t\t}\n\t\tif !util.IsFileInputValid(c.OPKSSHPath) || !filepath.IsAbs(c.OPKSSHPath) {\n\t\t\treturn fmt.Errorf(\"opkssh path %q is not valid, it must be an absolute path\", c.OPKSSHPath)\n\t\t}\n\t\tif c.OPKSSHChecksum == \"\" {\n\t\t\tif _, err := os.Stat(c.OPKSSHPath); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error validating opkssh path %q: %w\", c.OPKSSHPath, err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := util.VerifyFileChecksum(c.OPKSSHPath, sha256.New(), c.OPKSSHChecksum, 100*1024*1024); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error validating opkssh checksum: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Configuration) verifyWithOPKSSH(username string, cert *ssh.Certificate) error {\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\targs := []string{\"verify\", username, util.BytesToString(ssh.MarshalAuthorizedKey(cert)), cert.Type()}\n\tout, err := c.executor.CombinedOutput(ctx, c.OPKSSHPath, args...)\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to execute opk verifier: %s\", string(out))\n\t\treturn fmt.Errorf(\"unable to execute opk verifier: %w\", err)\n\t}\n\tpubKey, _, _, _, err := ssh.ParseAuthorizedKey(out) //nolint:dogsled\n\tif err != nil {\n\t\tlogger.Debug(logSender, \"\", \"unable to validate the opk verifier output: %s\", string(out))\n\t\treturn fmt.Errorf(\"unable to validate the opk verifier output: %w\", err)\n\t}\n\tif !bytes.Equal(pubKey.Marshal(), cert.SignatureKey.Marshal()) {\n\t\treturn errors.New(\"unable to validate opk result\")\n\t}\n\treturn nil\n}\n\nfunc (c *Configuration) initializeCertChecker(configDir string) error {\n\tfor _, keyPath := range c.TrustedUserCAKeys {\n\t\tkeyPath = strings.TrimSpace(keyPath)\n\t\tif !util.IsFileInputValid(keyPath) {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to load invalid trusted user CA key %q\", keyPath)\n\t\t\tlogger.WarnToConsole(\"unable to load invalid trusted user CA key %q\", keyPath)\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(keyPath) {\n\t\t\tkeyPath = filepath.Join(configDir, keyPath)\n\t\t}\n\t\tkeyBytes, err := os.ReadFile(keyPath)\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"error loading trusted user CA key %q: %v\", keyPath, err)\n\t\t\tlogger.WarnToConsole(\"error loading trusted user CA key %q: %v\", keyPath, err)\n\t\t\treturn err\n\t\t}\n\t\tparsedKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"error parsing trusted user CA key %q: %v\", keyPath, err)\n\t\t\tlogger.WarnToConsole(\"error parsing trusted user CA key %q: %v\", keyPath, err)\n\t\t\treturn err\n\t\t}\n\t\tc.parsedUserCAKeys = append(c.parsedUserCAKeys, parsedKey)\n\t}\n\tc.certChecker = &ssh.CertChecker{\n\t\tSupportedCriticalOptions: []string{\n\t\t\tsourceAddressCriticalOption,\n\t\t},\n\t\tIsUserAuthority: func(k ssh.PublicKey) bool {\n\t\t\tfor _, key := range c.parsedUserCAKeys {\n\t\t\t\tif bytes.Equal(k.Marshal(), key.Marshal()) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t}\n\tif c.RevokedUserCertsFile != \"\" {\n\t\tif !util.IsFileInputValid(c.RevokedUserCertsFile) {\n\t\t\treturn fmt.Errorf(\"invalid revoked user certificate: %q\", c.RevokedUserCertsFile)\n\t\t}\n\t\tif !filepath.IsAbs(c.RevokedUserCertsFile) {\n\t\t\tc.RevokedUserCertsFile = filepath.Join(configDir, c.RevokedUserCertsFile)\n\t\t}\n\t}\n\trevokedCertManager.filePath = c.RevokedUserCertsFile\n\treturn revokedCertManager.load()\n}\n\nfunc (c *Configuration) getPartialSuccessError(nextAuthMethods []string) error {\n\terr := &ssh.PartialSuccessError{}\n\tif c.PasswordAuthentication && slices.Contains(nextAuthMethods, dataprovider.LoginMethodPassword) {\n\t\terr.Next.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {\n\t\t\treturn c.validatePasswordCredentials(conn, password, dataprovider.SSHLoginMethodKeyAndPassword)\n\t\t}\n\t}\n\tif c.KeyboardInteractiveAuthentication && slices.Contains(nextAuthMethods, dataprovider.SSHLoginMethodKeyboardInteractive) {\n\t\terr.Next.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {\n\t\t\treturn c.validateKeyboardInteractiveCredentials(conn, client, dataprovider.SSHLoginMethodKeyAndKeyboardInt, true)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {\n\tvar user dataprovider.User\n\tvar certPerm *ssh.Permissions\n\n\tmethod := dataprovider.SSHLoginMethodPublicKey\n\tipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())\n\tcert, ok := pubKey.(*ssh.Certificate)\n\tvar certFingerprint string\n\tif ok {\n\t\tcertFingerprint = ssh.FingerprintSHA256(cert.Key)\n\t\tif c.OPKSSHPath != \"\" {\n\t\t\tif err := c.verifyWithOPKSSH(conn.User(), cert); err != nil {\n\t\t\t\terr := fmt.Errorf(\"ssh: verification with OPK failed: %v\", err)\n\t\t\t\tuser.Username = conn.User()\n\t\t\t\tupdateLoginMetrics(&user, ipAddr, method, err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\tif cert.CertType != ssh.UserCert {\n\t\t\t\terr := fmt.Errorf(\"ssh: cert has type %d\", cert.CertType)\n\t\t\t\tuser.Username = conn.User()\n\t\t\t\tupdateLoginMetrics(&user, ipAddr, method, err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif !c.certChecker.IsUserAuthority(cert.SignatureKey) {\n\t\t\t\terr := errors.New(\"ssh: certificate signed by unrecognized authority\")\n\t\t\t\tuser.Username = conn.User()\n\t\t\t\tupdateLoginMetrics(&user, ipAddr, method, err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif len(cert.ValidPrincipals) == 0 {\n\t\t\t\terr := fmt.Errorf(\"ssh: certificate %s has no valid principals, user: \\\"%s\\\"\", certFingerprint, conn.User())\n\t\t\t\tuser.Username = conn.User()\n\t\t\t\tupdateLoginMetrics(&user, ipAddr, method, err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif revokedCertManager.isRevoked(certFingerprint) {\n\t\t\t\terr := fmt.Errorf(\"ssh: certificate %s is revoked\", certFingerprint)\n\t\t\t\tuser.Username = conn.User()\n\t\t\t\tupdateLoginMetrics(&user, ipAddr, method, err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif err := c.certChecker.CheckCert(conn.User(), cert); err != nil {\n\t\t\t\tuser.Username = conn.User()\n\t\t\t\tupdateLoginMetrics(&user, ipAddr, method, err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tcertPerm = &cert.Permissions\n\t}\n\tuser, keyID, err := dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal(), ipAddr, common.ProtocolSSH, ok)\n\tif err != nil {\n\t\tuser.Username = conn.User()\n\t\tupdateLoginMetrics(&user, ipAddr, method, err)\n\t\treturn nil, err\n\t}\n\tif ok {\n\t\tkeyID = fmt.Sprintf(\"%s: ID: %s, serial: %v, CA %s %s\", certFingerprint,\n\t\t\tcert.KeyId, cert.Serial, cert.Type(), ssh.FingerprintSHA256(cert.SignatureKey))\n\t}\n\tif certPerm == nil {\n\t\tcertPerm = &ssh.Permissions{}\n\t}\n\tcertPerm.ExtraData = make(map[any]any)\n\tcertPerm.ExtraData[extraDataKeyIDKey] = keyID\n\tcertPerm.ExtraData[extraDataUserKey] = user\n\tif user.IsPartialAuth() {\n\t\tcertPerm.ExtraData[extraDataPartialSuccessErrKey] = c.getPartialSuccessError(user.GetNextAuthMethods())\n\t}\n\treturn certPerm, nil\n}\n\nfunc (c *Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass []byte, method string) (*ssh.Permissions, error) {\n\tvar err error\n\tvar user dataprovider.User\n\tvar sshPerm *ssh.Permissions\n\n\tipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())\n\tif user, err = dataprovider.CheckUserAndPass(conn.User(), util.BytesToString(pass), ipAddr, common.ProtocolSSH); err == nil {\n\t\tsshPerm, err = loginUser(&user, method, \"\", conn)\n\t}\n\tuser.Username = conn.User()\n\tupdateLoginMetrics(&user, ipAddr, method, err)\n\tif err != nil {\n\t\treturn nil, newAuthenticationError(fmt.Errorf(\"could not validate password credentials: %w\", err), method, conn.User())\n\t}\n\treturn sshPerm, nil\n}\n\nfunc (c *Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge,\n\tmethod string, isPartialAuth bool,\n) (*ssh.Permissions, error) {\n\tvar err error\n\tvar user dataprovider.User\n\tvar sshPerm *ssh.Permissions\n\n\tipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())\n\tif user, err = dataprovider.CheckKeyboardInteractiveAuth(conn.User(), c.KeyboardInteractiveHook, client,\n\t\tipAddr, common.ProtocolSSH, isPartialAuth); err == nil {\n\t\tsshPerm, err = loginUser(&user, method, \"\", conn)\n\t}\n\tuser.Username = conn.User()\n\tupdateLoginMetrics(&user, ipAddr, method, err)\n\tif err != nil {\n\t\treturn nil, newAuthenticationError(fmt.Errorf(\"could not validate keyboard interactive credentials: %w\", err), method, conn.User())\n\t}\n\treturn sshPerm, nil\n}\n\nfunc updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {\n\tmetric.AddLoginAttempt(method)\n\tif err == nil {\n\t\tplugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolSSH, user.Username, ip, \"\", err)\n\t\tcommon.DelayLogin(nil)\n\t} else {\n\t\tlogger.ConnectionFailedLog(user.Username, ip, method, common.ProtocolSSH, err.Error())\n\t\tif method != dataprovider.SSHLoginMethodPublicKey {\n\t\t\t// some clients try all available public keys for a user, we\n\t\t\t// record failed login key auth only once for session if the\n\t\t\t// authentication fails in checkAuthError\n\t\t\tevent := common.HostEventLoginFailed\n\t\t\tlogEv := notifier.LogEventTypeLoginFailed\n\t\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\t\tevent = common.HostEventUserNotFound\n\t\t\t\tlogEv = notifier.LogEventTypeLoginNoUser\n\t\t\t}\n\t\t\tcommon.AddDefenderEvent(ip, common.ProtocolSSH, event)\n\t\t\tplugin.Handler.NotifyLogEvent(logEv, common.ProtocolSSH, user.Username, ip, \"\", err)\n\t\t\tif method != dataprovider.SSHLoginMethodPublicKey {\n\t\t\t\tcommon.DelayLogin(err)\n\t\t\t}\n\t\t}\n\t}\n\tmetric.AddLoginResult(method, err)\n\tdataprovider.ExecutePostLoginHook(user, method, ip, common.ProtocolSSH, err)\n}\n\ntype revokedCertificates struct {\n\tfilePath string\n\tmu       sync.RWMutex\n\tcerts    map[string]bool\n}\n\nfunc (r *revokedCertificates) load() error {\n\tif r.filePath == \"\" {\n\t\treturn nil\n\t}\n\tlogger.Debug(logSender, \"\", \"loading revoked user certificate file %q\", r.filePath)\n\tinfo, err := os.Stat(r.filePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to load revoked user certificate file %q: %w\", r.filePath, err)\n\t}\n\tmaxSize := int64(1048576 * 5) // 5MB\n\tif info.Size() > maxSize {\n\t\treturn fmt.Errorf(\"unable to load revoked user certificate file %q size too big: %v/%v bytes\",\n\t\t\tr.filePath, info.Size(), maxSize)\n\t}\n\tcontent, err := os.ReadFile(r.filePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to read revoked user certificate file %q: %w\", r.filePath, err)\n\t}\n\tvar certs []string\n\terr = json.Unmarshal(content, &certs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to parse revoked user certificate file %q: %w\", r.filePath, err)\n\t}\n\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\tr.certs = map[string]bool{}\n\tfor _, fp := range certs {\n\t\tr.certs[fp] = true\n\t}\n\tlogger.Debug(logSender, \"\", \"revoked user certificate file %q loaded, entries: %v\", r.filePath, len(r.certs))\n\treturn nil\n}\n\nfunc (r *revokedCertificates) isRevoked(fp string) bool {\n\tr.mu.RLock()\n\tdefer r.mu.RUnlock()\n\n\treturn r.certs[fp]\n}\n\n// Reload reloads the list of revoked user certificates\nfunc Reload() error {\n\treturn revokedCertManager.load()\n}\n\nfunc algorithmsForKeyFormat(keyFormat string) []string {\n\tswitch keyFormat {\n\tcase ssh.KeyAlgoRSA:\n\t\treturn []string{ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSA}\n\tcase ssh.CertAlgoRSAv01:\n\t\treturn []string{ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSAv01}\n\tdefault:\n\t\treturn []string{keyFormat}\n\t}\n}\n"
  },
  {
    "path": "internal/sftpd/sftpd.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package sftpd implements the SSH File Transfer Protocol as described in https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02.\n// It uses pkg/sftp library:\n// https://github.com/pkg/sftp\npackage sftpd\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ssh\"\n)\n\nconst (\n\tlogSender        = \"sftpd\"\n\thandshakeTimeout = 2 * time.Minute\n)\n\nvar (\n\tsupportedSSHCommands = []string{\"scp\", \"md5sum\", \"sha1sum\", \"sha256sum\", \"sha384sum\", \"sha512sum\", \"cd\", \"pwd\",\n\t\t\"sftpgo-copy\", \"sftpgo-remove\"}\n\tdefaultSSHCommands = []string{\"md5sum\", \"sha1sum\", \"sha256sum\", \"cd\", \"pwd\", \"scp\"}\n\tsshHashCommands    = []string{\"md5sum\", \"sha1sum\", \"sha256sum\", \"sha384sum\", \"sha512sum\"}\n\tserviceStatus      ServiceStatus\n\tcertKeyAlgoNames   = map[string]string{\n\t\tssh.CertAlgoRSAv01:         ssh.KeyAlgoRSA,\n\t\tssh.CertAlgoRSASHA256v01:   ssh.KeyAlgoRSASHA256,\n\t\tssh.CertAlgoRSASHA512v01:   ssh.KeyAlgoRSASHA512,\n\t\tssh.InsecureCertAlgoDSAv01: ssh.InsecureKeyAlgoDSA, //nolint:staticcheck\n\t\tssh.CertAlgoECDSA256v01:    ssh.KeyAlgoECDSA256,\n\t\tssh.CertAlgoECDSA384v01:    ssh.KeyAlgoECDSA384,\n\t\tssh.CertAlgoECDSA521v01:    ssh.KeyAlgoECDSA521,\n\t\tssh.CertAlgoSKECDSA256v01:  ssh.KeyAlgoSKECDSA256,\n\t\tssh.CertAlgoED25519v01:     ssh.KeyAlgoED25519,\n\t\tssh.CertAlgoSKED25519v01:   ssh.KeyAlgoSKED25519,\n\t}\n)\n\ntype sshSubsystemExitStatus struct {\n\tStatus uint32\n}\n\ntype sshSubsystemExecMsg struct {\n\tCommand string\n}\n\ntype hostCertificate struct {\n\tCertificate *ssh.Certificate\n\tPath        string\n}\n\n// HostKey defines the details for a used host key\ntype HostKey struct {\n\tPath        string   `json:\"path\"`\n\tFingerprint string   `json:\"fingerprint\"`\n\tAlgorithms  []string `json:\"algorithms\"`\n}\n\n// GetAlgosAsString returns the host key algorithms as comma separated string\nfunc (h *HostKey) GetAlgosAsString() string {\n\treturn strings.Join(h.Algorithms, \", \")\n}\n\n// ServiceStatus defines the service status\ntype ServiceStatus struct {\n\tIsActive            bool      `json:\"is_active\"`\n\tBindings            []Binding `json:\"bindings\"`\n\tSSHCommands         []string  `json:\"ssh_commands\"`\n\tHostKeys            []HostKey `json:\"host_keys\"`\n\tAuthentications     []string  `json:\"authentications\"`\n\tMACs                []string  `json:\"macs\"`\n\tKexAlgorithms       []string  `json:\"kex_algorithms\"`\n\tCiphers             []string  `json:\"ciphers\"`\n\tPublicKeyAlgorithms []string  `json:\"public_key_algorithms\"`\n}\n\n// GetSSHCommandsAsString returns enabled SSH commands as comma separated string\nfunc (s *ServiceStatus) GetSSHCommandsAsString() string {\n\treturn strings.Join(s.SSHCommands, \", \")\n}\n\n// GetSupportedAuthsAsString returns the supported authentications as comma separated string\nfunc (s *ServiceStatus) GetSupportedAuthsAsString() string {\n\treturn strings.Join(s.Authentications, \", \")\n}\n\n// GetMACsAsString returns the enabled MAC algorithms as comma separated string\nfunc (s *ServiceStatus) GetMACsAsString() string {\n\treturn strings.Join(s.MACs, \", \")\n}\n\n// GetKEXsAsString returns the enabled KEX algorithms as comma separated string\nfunc (s *ServiceStatus) GetKEXsAsString() string {\n\treturn strings.Join(s.KexAlgorithms, \", \")\n}\n\n// GetCiphersAsString returns the enabled ciphers as comma separated string\nfunc (s *ServiceStatus) GetCiphersAsString() string {\n\treturn strings.Join(s.Ciphers, \", \")\n}\n\n// GetPublicKeysAlgosAsString returns enabled public key authentication\n// algorithms as comma separated string\nfunc (s *ServiceStatus) GetPublicKeysAlgosAsString() string {\n\treturn strings.Join(s.PublicKeyAlgorithms, \", \")\n}\n\n// GetStatus returns the server status\nfunc GetStatus() ServiceStatus {\n\treturn serviceStatus\n}\n\n// GetDefaultSSHCommands returns the SSH commands enabled as default\nfunc GetDefaultSSHCommands() []string {\n\tresult := make([]string, len(defaultSSHCommands))\n\tcopy(result, defaultSSHCommands)\n\treturn result\n}\n\n// GetSupportedSSHCommands returns the supported SSH commands\nfunc GetSupportedSSHCommands() []string {\n\tresult := make([]string, len(supportedSSHCommands))\n\tcopy(result, supportedSSHCommands)\n\treturn result\n}\n"
  },
  {
    "path": "internal/sftpd/sftpd_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd_test\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"io/fs\"\n\t\"math\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t_ \"github.com/mattn/go-sqlite3\"\n\t\"github.com/pquerna/otp\"\n\t\"github.com/pquerna/otp/totp\"\n\n\t\"github.com/pkg/sftp\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/sftpgo/sdk\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpdtest\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/mfa\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tlogSender           = \"sftpdTesting\"\n\tsftpServerAddr      = \"127.0.0.1:2022\"\n\tsftpSrvAddr2222     = \"127.0.0.1:2222\"\n\tdefaultUsername     = \"test_user_sftp\"\n\tdefaultPassword     = \"test_password\"\n\tdefaultSFTPUsername = \"test_sftpfs_user\"\n\ttestPubKey          = \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1\"\n\ttestPubKey1         = \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCd60+/j+y8f0tLftihWV1YN9RSahMI9btQMDIMqts/jeNbD8jgoogM3nhF7KxfcaMKURuD47KC4Ey6iAJUJ0sWkSNNxOcIYuvA+5MlspfZDsa8Ag76Fe1vyz72WeHMHMeh/hwFo2TeIeIXg480T1VI6mzfDrVp2GzUx0SS0dMsQBjftXkuVR8YOiOwMCAH2a//M1OrvV7d/NBk6kBN0WnuIBb2jKm15PAA7+jQQG7tzwk2HedNH3jeL5GH31xkSRwlBczRK0xsCQXehAlx6cT/e/s44iJcJTHfpPKoSk6UAhPJYe7Z1QnuoawY9P9jQaxpyeImBZxxUEowhjpj2avBxKdRGBVK8R7EL8tSOeLbhdyWe5Mwc1+foEbq9Zz5j5Kd+hn3Wm1UnsGCrXUUUoZp1jnlNl0NakCto+5KmqnT9cHxaY+ix2RLUWAZyVFlRq71OYux1UHJnEJPiEI1/tr4jFBSL46qhQZv/TfpkfVW8FLz0lErfqu0gQEZnNHr3Fc= nicola@p1\"\n\ttestPrivateKey      = `-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAYEAtN449A/nY5O6cSH/9Doa8a3ISU0WZJaHydTaCLuO+dkqtNpnV5mq\nzFbKidXAI1eSwVctw9ReVOl1uK6aZF3lbXdOD8W9PXobR9KUUT2qBx5QC4ibfAqDKWymDA\nPG9ylzz64hsYBqJr7VNk9kTFEUsDmWzLabLoH42Elnp8mF/lTkWIcpVp0ly/etS08gttXo\nXenekJ1vRuxOYWDCEzGPU7kGc920TmM14k7IDdPoOh5+3sRUKedKeOUrVDH1f0n7QjHQsZ\ncbshp8tgqzf734zu8cTqNrr+6taptdEOOij1iUL/qYGfzny/hA48tO5+UFUih5W8ftp0+E\nNBIDkkGgk2MJ92I7QAXyMVsIABXco+mJT7pQi9tqlODGIQ3AOj0gcA3X/Ib8QX77Ih3TPi\nXEh77/P1XiYZOgpp2cRmNH8QbqaL9u898hDvJwIPJPuj2lIltTElH7hjBf5LQfCzrLV7BD\n10rM7sl4jr+A2q8jl1Ikp+25kainBBZSbrDummT9AAAFgDU/VLk1P1S5AAAAB3NzaC1yc2\nEAAAGBALTeOPQP52OTunEh//Q6GvGtyElNFmSWh8nU2gi7jvnZKrTaZ1eZqsxWyonVwCNX\nksFXLcPUXlTpdbiummRd5W13Tg/FvT16G0fSlFE9qgceUAuIm3wKgylspgwDxvcpc8+uIb\nGAaia+1TZPZExRFLA5lsy2my6B+NhJZ6fJhf5U5FiHKVadJcv3rUtPILbV6F3p3pCdb0bs\nTmFgwhMxj1O5BnPdtE5jNeJOyA3T6Doeft7EVCnnSnjlK1Qx9X9J+0Ix0LGXG7IafLYKs3\n+9+M7vHE6ja6/urWqbXRDjoo9YlC/6mBn858v4QOPLTuflBVIoeVvH7adPhDQSA5JBoJNj\nCfdiO0AF8jFbCAAV3KPpiU+6UIvbapTgxiENwDo9IHAN1/yG/EF++yId0z4lxIe+/z9V4m\nGToKadnEZjR/EG6mi/bvPfIQ7ycCDyT7o9pSJbUxJR+4YwX+S0Hws6y1ewQ9dKzO7JeI6/\ngNqvI5dSJKftuZGopwQWUm6w7ppk/QAAAAMBAAEAAAGAHKnC+Nq0XtGAkIFE4N18e6SAwy\n0WSWaZqmCzFQM0S2AhJnweOIG/0ZZHjsRzKKauOTmppQk40dgVsejpytIek9R+aH172gxJ\n2n4Cx0UwduRU5x8FFQlNc/kl722B0JWfJuB/snOZXv6LJ4o5aObIkozt2w9tVFeAqjYn2S\n1UsNOfRHBXGsTYwpRDwFWP56nKo2d2wBBTHDhCy6fb2dLW1fvSi/YspueOGIlHpvlYKi2/\nCWqvs9xVrwcScMtiDoQYq0khhO0efLCxvg/o+W9CLMVM2ms4G1zoSUQKN0oYWWQJyW4+VI\nYneWO8UpN0J3ElXKi7bhgAat7dBaM1g9IrAzk153DiEFZNsPxGOgL/+YdQN7zUBx/z7EkI\njyv80RV7fpUXvcq2p+qNl6UVig3VSzRrnsaJkUWu/A0u59ha7ocv6NxDIXjxpIDJme16GF\nquiGVBQNnYJymS/vFEbGf6bgf7iRmMCRUMG4nqLA6fPYP9uAtch+CmDfVLZC/fIdC5AAAA\nwQCDissV4zH6bfqgxJSuYNk8Vbb+19cF3b7gH1rVlB3zxpCAgcRgMHC+dP1z2NRx7UW9MR\nnye6kjpkzZZ0OigLqo7TtEq8uTglD9o6W7mRXqhy5A/ySOmqPL3ernHHQhGuoNODYAHkOU\nu2Rh8HXi+VLwKZcLInPOYJvcuLG4DxN8WfeVvlMHwhAOaTNNOtL4XZDHQeIPc4qHmJymmv\nsV7GuyQ6yW5C10uoGdxRPd90Bh4z4h2bKfZFjvEBbSBVkqrlAAAADBAN/zNtNayd/dX7Cr\nNb4sZuzCh+CW4BH8GOePZWNCATwBbNXBVb5cR+dmuTqYm+Ekz0VxVQRA1TvKncluJOQpoa\nXj8r0xdIgqkehnfDPMKtYVor06B9Fl1jrXtXU0Vrr6QcBWruSVyK1ZxqcmcNK/+KolVepe\nA6vcl/iKaG4U7su166nxLST06M2EgcSVsFJHpKn5+WAXC+X0Gx8kNjWIIb3GpiChdc0xZD\nmq02xZthVJrTCVw/e7gfDoB2QRsNV8HwAAAMEAzsCghZVp+0YsYg9oOrw4tEqcbEXEMhwY\n0jW8JNL8Spr1Ibp5Dw6bRSk5azARjmJtnMJhJ3oeHfF0eoISqcNuQXGndGQbVM9YzzAzc1\nNbbCNsVroqKlChT5wyPNGS+phi2bPARBno7WSDvshTZ7dAVEP2c9MJW0XwoSevwKlhgSdt\nRLFFQ/5nclJSdzPBOmQouC0OBcMFSrYtMeknJ4VvueVvve5HcHFaEsaMc7ABAGaLYaBQOm\niixITGvaNZh/tjAAAACW5pY29sYUBwMQE=\n-----END OPENSSH PRIVATE KEY-----`\n\t// password protected private key\n\ttestPrivateKeyPwd = `-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAvfwQQcs\n+PyMsCLTNFcKiQAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAILqltfCL7IPuIQ2q\n+8w23flfgskjIlKViEwMfjJR4mrbAAAAkHp5xgG8J1XW90M/fT59ZUQht8sZzzP17rEKlX\nwaYKvLzDxkPK6LFIYs55W1EX1eVt/2Maq+zQ7k2SOUmhPNknsUOlPV2gytX3uIYvXF7u2F\nFTBIJuzZ+UQ14wFbraunliE9yye9DajVG1kz2cz2wVgXUbee+gp5NyFVvln+TcTxXwMsWD\nqwlk5iw/jQekxThg==\n-----END OPENSSH PRIVATE KEY-----\n`\n\ttestPubKeyPwd = \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILqltfCL7IPuIQ2q+8w23flfgskjIlKViEwMfjJR4mrb\"\n\tprivateKeyPwd = \"password\"\n\t// test CA user key.\n\t// % ssh-keygen -f ca_user_key\n\ttestCAUserKey = \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF5fcwZHiyixmnE6IlOZJpZhWXoh62gN+yadAA0GJ509SAEaZVLPDP8S5RsE8mUikR3wxynVshxHeqMhrkS+RlNbhSlOXDdNg94yTrq/xF8Z/PgKRInvef74k5i7bAIytza7jERzFJ/ujTEy3537T5k5EYQJ15ZQGuvzynSdv+6o99SjI4jFplyQOZ2QcYbEAmhHm5GgQlIiEFG/RlDtLksOulKZxOY3qPzP0AyQxtZJXn/5vG40aW9LTbwxCJqWlgrkFXMqAAVCbuU5YspwhiXmKt1PsldiXw23oloa4caCKN1jzbFiGuZNXEU2Ebx7JIvjQCPaUYwLjEbkRDxDqN/vmwZqBuKYiuG9Eafx+nFSQkr7QYb5b+mT+/1IFHnmeRGn38731kBqtH7tpzC/t+soRX9p2HtJM+9MYhblO2OqTSPGTlxihWUkyiRBekpAhaiHld16TsG+A3bOJHrojGcX+5g6oGarKGLAMcykL1X+rZqT993Mo6d2Z7q43MOXE= root@p1\"\n\t// this is testPubKey signed using testCAUserKey.\n\t// % ssh-keygen -s ca_user_key -I test_user_sftp -n test_user_sftp -V always:forever -O source-address=127.0.0.1 -z 1 /tmp/test.pub\n\ttestCertValid = \"ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgm2fil1IIoTixrA2QE9tk7Vbspj/JdEY90e3K2htxYv8AAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAQAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAACMAAAAOc291cmNlLWFkZHJlc3MAAAANAAAACTEyNy4wLjAuMQAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAMXl9zBkeLKLGacToiU5kmlmFZeiHraA37Jp0ADQYnnT1IARplUs8M/xLlGwTyZSKRHfDHKdWyHEd6oyGuRL5GU1uFKU5cN02D3jJOur/EXxn8+ApEie95/viTmLtsAjK3NruMRHMUn+6NMTLfnftPmTkRhAnXllAa6/PKdJ2/7qj31KMjiMWmXJA5nZBxhsQCaEebkaBCUiIQUb9GUO0uSw66UpnE5jeo/M/QDJDG1klef/m8bjRpb0tNvDEImpaWCuQVcyoABUJu5TliynCGJeYq3U+yV2JfDbeiWhrhxoIo3WPNsWIa5k1cRTYRvHski+NAI9pRjAuMRuREPEOo3++bBmoG4piK4b0Rp/H6cVJCSvtBhvlv6ZP7/UgUeeZ5EaffzvfWQGq0fu2nML+36yhFf2nYe0kz70xiFuU7Y6pNI8ZOXGKFZSTKJEF6SkCFqIeV3XpOwb4Dds4keuiMZxf7mDqgZqsoYsAxzKQvVf6tmpP33cyjp3Znurjcw5cQAAAZQAAAAMcnNhLXNoYTItNTEyAAABgMNenD7d1J9cF7JWgHA1DYpJ5+5knPtdXbbIgZAznsTxX7qOdptjeeYOuzhQ5Bwklh3fjewiJpGR1rBqbULP+6PAKeYqd7dNLH/upfKBfJweRf5pdXDpoknHaVuIhi4Uu6FeI4NkAzX9nqNKjFAflhJ+7GLGkLNb0UVZxgxr/t0rPmxc5iTg2ZRM+rk1Ij0S5RnGiKVsdAClqNA6h4TDzu5lJVdK5XvuNKBsKVRCvsVBOgJQTtRTLywQaqWR+HBfCiMj8X8EI7atDlJ6XIAlTLOO/f1sM8QPLjT0+tCHZaGFzg/lKPh3/yFQ4MvddZCptMy1Ll1xvj7cz2ynhGR4PiDfikV3YzgJU/KtL5y+ZB4jU08oPRiOP612PjwZZ+MqYOVOFCKUpMpZQs5UJHME+zNKr4LEj8M0x4YFKIciC+RsrCo4ujbJHmz61ionCadU+fmngvl3C3QjmUdgULBevODeUeIpJv4yFahNxrG1SKRTAa8VVDwJ9GdDTtmXM0mrwA== nicola@p1\"\n\t// this is testPubKey signed using a CA user key different from testCAUserKey\n\ttestCertUntrustedCA = \"ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg8oFPWpjYy/DowMmtOjWj7Dq20d2N/4Rxzr/c710tOOUAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAAAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCqgm2gVlptULThfpRR0oCb4SAU3368ULlJaiZOUdq6b94KTfgmu4hTLs7u3a8hyZnVxrKrJ93uAVCwa/HGtgiN96CNC6JUt/QnPqTJ8LQ207RdoE9fbOe6mGwOle5z45+5JFoIi5ZZuD8JsBGodVoa92UepoMyBcNtZyl9q2GP4yT2tIYRon79dtG9AXiDYyhSgePqaObN67dn3ivMc4ZGNukK3cG07cYPic5y0wxX16wSMG3pGQDyUkAu+s4AqpnV9EWHM4PE7SYkCXE99++tUK3QALYqvGZKrLHgzmDKi6n+e14vHYUppAeGDZzwlawiY4oGP9eOW2KUfjZe2ZeL22JTFDYzH2lNV2WtUpeKRGGTSGaUblRVC9hRt6hKCT4c7qpW4kO4kPhE39JpcNPGLql7srNkw+3xXBs8xghMPtH3nOl1Rz2mxnX5tAqmPBb+KiPepnrs+pBRu7i+nCVp8az+iN87STYHy+zPtvTR+QURC8BpNraPOfXwpwM2HaMAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYBnTXCL6tXUO3/Gtsm7lnH9Sulzca8FOoI4Y/4bVYhq4iUNu7Ca452m+Xr9qmCEoIyIJF0LEEcJ8jcS4rfX15e7tNNoknv7JbYXBFAbp1Y/76iqVf89FjfVcbEyH2ToAf7eyQAWzQ3gEKS8mQIkLnAwmCboUXC4GRodSIiOXiTt5Q6T02MVc8TxkhmlTA0uVLd5XgstySgE/oLBnL59lhJcwQmdhHL+m480+PaW55CtMuC36RTwk/tOyuWCDC5qMXnoveNB3yu45o3L/U4hoyJ0/5FyP5C8ahgydY0LoRZQG/mNzuraY4433rK+IfkQvZTyaDtcjhxE6hCD5F40aDDh88i6XaKAPikD6fqra6BN8PoPgLuRHzOJuqsMXBWM99s7qPgSnBbmXlekz/1jvvFiCh3zvAFTxFz2KyE4+SbDcCrhpxkNL7idw6r/ZsHaI/2+zhDcgSs5MgBwYLJEj6zUqVdp5XsF8YfC7yNZV5/qy68qY2+zXrC57SPifU2SCPE= nicola@p1\"\n\t// this is testPubKey signed as host certificate.\n\t// % ssh-keygen -s ca_user_key -I test_user_sftp -h -n test_user_sftp -V always:forever -z 2 /tmp/test.pub\n\ttestHostCert = \"ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg7O2LDpLO1jGTX3SSzEMILoAYJb9DdggyyaUMXUUg3L4AAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAgAAAAIAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAAAAAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAMXl9zBkeLKLGacToiU5kmlmFZeiHraA37Jp0ADQYnnT1IARplUs8M/xLlGwTyZSKRHfDHKdWyHEd6oyGuRL5GU1uFKU5cN02D3jJOur/EXxn8+ApEie95/viTmLtsAjK3NruMRHMUn+6NMTLfnftPmTkRhAnXllAa6/PKdJ2/7qj31KMjiMWmXJA5nZBxhsQCaEebkaBCUiIQUb9GUO0uSw66UpnE5jeo/M/QDJDG1klef/m8bjRpb0tNvDEImpaWCuQVcyoABUJu5TliynCGJeYq3U+yV2JfDbeiWhrhxoIo3WPNsWIa5k1cRTYRvHski+NAI9pRjAuMRuREPEOo3++bBmoG4piK4b0Rp/H6cVJCSvtBhvlv6ZP7/UgUeeZ5EaffzvfWQGq0fu2nML+36yhFf2nYe0kz70xiFuU7Y6pNI8ZOXGKFZSTKJEF6SkCFqIeV3XpOwb4Dds4keuiMZxf7mDqgZqsoYsAxzKQvVf6tmpP33cyjp3Znurjcw5cQAAAZQAAAAMcnNhLXNoYTItNTEyAAABgHlAWMTTzNrE6pxHlkr09ZXsHgJi8U2p7eifs56DOLgklYIXVUJPEEcnzMKGdpPBnqJsvg3+PccqxgOr5L1dFuOmekQ/dGiHd1enrESiGvJOvDfm0WsuBjxEZkSNFWgC9Z2NltToMmRlhVBmb4ZRZtAmi9DAFlJ/BDV4t8ikXZ5oUsigwIeOeLkdPFx3C3x9KZIpuwuAIV4Nfmz75q1NMWY2K1hv682QCKwMYqOWSotz1vWunNmZ0yPRl9UwqAq+nqwO3AApnlrQ3MmHujWQ5tl65PyhfpI8oghhUtB6YrJIAuRXNI/S0+KewCpiYm7nbFBtv9lpecujxAeTibYBrFZ5VODEUm3sdQ/HMdTmkhi6xNgPDQVlvKFqBJAaqoO3tbhKTbEZ865tJMqhyxmZ4XY08wduvSVobrNr7s3rm42/FXLIpung+UOVXonHyeIv9zQ0iJ/bvqKQ1fOsTisZdcD0lz80ZGsjdgJt7yKfUNBnAyVbTXm048E3WsZslJIYCA== nicola@p1\"\n\t// this is testPubKey signed using testCAUserKey but with source address 172.16.34.45.\n\t// % ssh-keygen -s ca_user_key -I test_user_sftp -n test_user_sftp -V always:forever -O source-address=172.16.34.45 -z 3 /tmp/test.pub\n\ttestCertOtherSourceAddress = \"ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgZ4Su0250R4sQRNYJqJH9VTp9OyeYMAvqY5+lJRI4LzMAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAwAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAACYAAAAOc291cmNlLWFkZHJlc3MAAAAQAAAADDE3Mi4xNi4zNC40NQAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAMXl9zBkeLKLGacToiU5kmlmFZeiHraA37Jp0ADQYnnT1IARplUs8M/xLlGwTyZSKRHfDHKdWyHEd6oyGuRL5GU1uFKU5cN02D3jJOur/EXxn8+ApEie95/viTmLtsAjK3NruMRHMUn+6NMTLfnftPmTkRhAnXllAa6/PKdJ2/7qj31KMjiMWmXJA5nZBxhsQCaEebkaBCUiIQUb9GUO0uSw66UpnE5jeo/M/QDJDG1klef/m8bjRpb0tNvDEImpaWCuQVcyoABUJu5TliynCGJeYq3U+yV2JfDbeiWhrhxoIo3WPNsWIa5k1cRTYRvHski+NAI9pRjAuMRuREPEOo3++bBmoG4piK4b0Rp/H6cVJCSvtBhvlv6ZP7/UgUeeZ5EaffzvfWQGq0fu2nML+36yhFf2nYe0kz70xiFuU7Y6pNI8ZOXGKFZSTKJEF6SkCFqIeV3XpOwb4Dds4keuiMZxf7mDqgZqsoYsAxzKQvVf6tmpP33cyjp3Znurjcw5cQAAAZQAAAAMcnNhLXNoYTItNTEyAAABgL34Q3Li8AJIxZLU+fh4i8ehUWpm31vEvlNjXVCeP70xI+5hWuEt6E1TgKw7GCL5GeD4KehX4vVcNs+A2eOdIUZfDBZIFxn88BN8xcMlDpAMJXgvNqGttiOwcspL6X3N288djUgpCI718lLRdz8nvFqcuYBhSpBm5KL4JzH5o1o8yqv75wMJsH8CJYwGhvWi0OgWOqaLRAk3IUxq3Fbgo/nX11NgrkY/dHIZCkGBFaLJ/M5mfmt/K/5hJAVgLcSxMwB/ryyGaziB9Pv7CwZ9uwnMoRcAvyr96lqgdtLt7LNY8ktugAJ7EnBWjQn4+EJAjjRK2sCaiwpdP37ckDZgmk0OWGEL1yVy8VXgl9QBd7Mb1EVl+lhRyw8jlgBXZOGqpdDrmKCdBYGtU7ujyndLXmxZEAlqhef0yCsyZPTkYH3RhjCYs8ATrEqndEpiL59Nej5uUGQURYijJfHep08AMb4rCxvIZATVm1Ocxu48rGCGolv8jZFJzSJq84HCrVRKMw== nicola@p1\"\n\t// this is testPubKey signed using testCAUserKey but expired.\n\t// % ssh-keygen -s ca_user_key -I test_user_sftp -n test_user_sftp -V 20100101123000:20110101123000 -z 4 /tmp/test.pub\n\ttestCertExpired = \"ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgU3TLP5285k20fBSsdZioI78oJUpaRXFlgx5IPg6gWg8AAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAABAAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAEs93LgAAAAATR8QOAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQDF5fcwZHiyixmnE6IlOZJpZhWXoh62gN+yadAA0GJ509SAEaZVLPDP8S5RsE8mUikR3wxynVshxHeqMhrkS+RlNbhSlOXDdNg94yTrq/xF8Z/PgKRInvef74k5i7bAIytza7jERzFJ/ujTEy3537T5k5EYQJ15ZQGuvzynSdv+6o99SjI4jFplyQOZ2QcYbEAmhHm5GgQlIiEFG/RlDtLksOulKZxOY3qPzP0AyQxtZJXn/5vG40aW9LTbwxCJqWlgrkFXMqAAVCbuU5YspwhiXmKt1PsldiXw23oloa4caCKN1jzbFiGuZNXEU2Ebx7JIvjQCPaUYwLjEbkRDxDqN/vmwZqBuKYiuG9Eafx+nFSQkr7QYb5b+mT+/1IFHnmeRGn38731kBqtH7tpzC/t+soRX9p2HtJM+9MYhblO2OqTSPGTlxihWUkyiRBekpAhaiHld16TsG+A3bOJHrojGcX+5g6oGarKGLAMcykL1X+rZqT993Mo6d2Z7q43MOXEAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYAlH3hhj8J6xLyVpeLZjblzwDKrxp/MWiH30hQ965ExPrPRcoAZFEKVqOYdj6bp4Q19Q4Yzqdobg3aN5ym2iH0b2TlOY0mM901CAoHbNJyiLs+0KiFRoJ+30EDj/hcKusg6v8ln2yixPagAyQu3zyiWo4t1ZuO3I86xchGlptStxSdHAHPFCfpbhcnzWFZctiMqUutl82C4ROWyjOZcRzdVdWHeN5h8wnooXuvba2VkT8QPmjYYyRGuQ3Hg+ySdh8Tel4wiix1Dg5MX7Wjh4hKEx80No9UPy+0iyZMNc07lsWAtrY6NRxGM5CzB6mklscB8TzFrVSnIl9u3bquLfaCrFt/Mft5dR7Yy4jmF+zUhjia6h6giCZ91J+FZ4hV+WkBtPCvTfrGWoA1BgEB/iI2xOq/NPqJ7UXRoMXk/l0NPgRPT2JS1adegqnt4ddr6IlmPyZxaSEvXhanjKdfMlEFYO1wz7ouqpYUozQVy4KXBlzFlNwyD1hI+k4+/A6AIYeI= nicola@p1\"\n\t// this is testPubKey signed without a principal\n\t// ssh-keygen -s ca_user_key -I test_user_sftp -V always:forever -O source-address=127.0.0.1 -z 1 /tmp/test.pub\n\ttestCertNoPrincipals = \"ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg2Bx0s8nafJtriqoBuQfbFByhdQMkjDIZhV90JZSGN8AAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAQAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAAAAAAAAAAAAAD//////////wAAACMAAAAOc291cmNlLWFkZHJlc3MAAAANAAAACTEyNy4wLjAuMQAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAMXl9zBkeLKLGacToiU5kmlmFZeiHraA37Jp0ADQYnnT1IARplUs8M/xLlGwTyZSKRHfDHKdWyHEd6oyGuRL5GU1uFKU5cN02D3jJOur/EXxn8+ApEie95/viTmLtsAjK3NruMRHMUn+6NMTLfnftPmTkRhAnXllAa6/PKdJ2/7qj31KMjiMWmXJA5nZBxhsQCaEebkaBCUiIQUb9GUO0uSw66UpnE5jeo/M/QDJDG1klef/m8bjRpb0tNvDEImpaWCuQVcyoABUJu5TliynCGJeYq3U+yV2JfDbeiWhrhxoIo3WPNsWIa5k1cRTYRvHski+NAI9pRjAuMRuREPEOo3++bBmoG4piK4b0Rp/H6cVJCSvtBhvlv6ZP7/UgUeeZ5EaffzvfWQGq0fu2nML+36yhFf2nYe0kz70xiFuU7Y6pNI8ZOXGKFZSTKJEF6SkCFqIeV3XpOwb4Dds4keuiMZxf7mDqgZqsoYsAxzKQvVf6tmpP33cyjp3Znurjcw5cQAAAZQAAAAMcnNhLXNoYTItNTEyAAABgHgax/++NA5YZXDHH180BcQtDBve8Vc+XJzqQUe8xBiqd+KJnas6He7vW62qMaAfu63i0Uycj2Djfjy5dyx1GB9wup8YuP5mXlmJTx+7UPPjwbfrZWtk8iJ7KhFAwjh0KRZD4uIvoeecK8QE9zh64k2LNVqlWbFTdoPulRC29cGcXDpMU2eToFEyWbceHOZyyifXf98ZMZbaQzWzwSZ5rFucJ1b0aeT6aAJWB+Dq7mIQWf/jCWr8kNaeCzMKJsFQkQEfmHls29ChV92sNRhngUDxll0Ir0wpPea1fFEBnUhLRTLC8GhDDbWAzsZtXqx9fjoAkb/gwsU6TGxevuOMxEABjDA9PyJiTXJI9oTUCwDIAUVVFLsCEum3o/BblngXajUGibaif5ZSKBocpP70oTeAngQYB7r1/vquQzGsGFhTN4FUXLSpLu9Zqi1z58/qa7SgKSfNp98X/4zrhltAX73ZEvg0NUMv2HwlwlqHdpF3FYolAxInp7c2jBTncQ2l3w== nicola@p1\"\n\tosWindows            = \"windows\"\n\ttestFileName         = \"test_file_sftp.dat\"\n\ttestDLFileName       = \"test_download_sftp.dat\"\n)\n\nvar (\n\tconfigDir        = filepath.Join(\".\", \"..\", \"..\")\n\tallPerms         = []string{dataprovider.PermAny}\n\thomeBasePath     string\n\tscpPath          string\n\tscpForce         bool\n\tgitPath          string\n\tsshPath          string\n\thookCmdPath      string\n\tpubKeyPath       string\n\tprivateKeyPath   string\n\ttrustedCAUserKey string\n\trevokeUserCerts  string\n\tgitWrapPath      string\n\textAuthPath      string\n\tkeyIntAuthPath   string\n\tpreLoginPath     string\n\tpostConnectPath  string\n\tpreDownloadPath  string\n\tpreUploadPath    string\n\tcheckPwdPath     string\n\tlogFilePath      string\n\thostKeyFPs       []string\n)\n\nfunc TestMain(m *testing.M) {\n\tlogFilePath = filepath.Join(configDir, \"sftpgo_sftpd_test.log\")\n\tloginBannerFileName := \"login_banner\"\n\tloginBannerFile := filepath.Join(configDir, loginBannerFileName)\n\tlogger.InitLogger(logFilePath, 10, 1, 28, false, false, zerolog.DebugLevel)\n\terr := os.WriteFile(loginBannerFile, []byte(\"simple login banner\\n\"), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error creating login banner: %v\", err)\n\t}\n\tos.Setenv(\"SFTPGO_COMMON__UPLOAD_MODE\", \"2\")\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN\", \"1\")\n\tos.Setenv(\"SFTPGO_COMMON__ALLOW_SELF_CONNECTIONS\", \"1\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_USERNAME\", \"admin\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_PASSWORD\", \"password\")\n\terr = config.LoadConfig(configDir, \"\")\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error loading configuration: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tproviderConf := config.GetProviderConf()\n\tlogger.InfoToConsole(\"Starting SFTPD tests, provider: %v\", providerConf.Driver)\n\n\tcommonConf := config.GetCommonConfig()\n\thomeBasePath = os.TempDir()\n\tcheckSystemCommands()\n\tvar scriptArgs string\n\tif runtime.GOOS == osWindows {\n\t\tscriptArgs = \"%*\"\n\t} else {\n\t\tcommonConf.Actions.ExecuteOn = []string{\"download\", \"upload\", \"rename\", \"delete\", \"ssh_cmd\",\n\t\t\t\"pre-download\", \"pre-upload\"}\n\t\tcommonConf.Actions.Hook = hookCmdPath\n\t\tscriptArgs = \"$@\"\n\t}\n\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing data provider: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error resetting configs: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\terr = common.Initialize(commonConf, 0)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"error initializing common: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\thttpConfig := config.GetHTTPConfig()\n\thttpConfig.Initialize(configDir) //nolint:errcheck\n\tkmsConfig := config.GetKMSConfig()\n\terr = kmsConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing kms: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tmfaConfig := config.GetMFAConfig()\n\terr = mfaConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing MFA: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\tsftpdConf := config.GetSFTPDConfig()\n\thttpdConf := config.GetHTTPDConfig()\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort:             2022,\n\t\t\tApplyProxyConfig: true,\n\t\t},\n\t}\n\tsftpdConf.KexAlgorithms = []string{\"curve25519-sha256@libssh.org\", ssh.KeyExchangeECDHP256,\n\t\tssh.KeyExchangeECDHP384}\n\tsftpdConf.Ciphers = []string{ssh.CipherChaCha20Poly1305, ssh.CipherAES128GCM,\n\t\tssh.CipherAES256CTR}\n\tsftpdConf.LoginBannerFile = loginBannerFileName\n\t// we need to test all supported ssh commands\n\tsftpdConf.EnabledSSHCommands = []string{\"*\"}\n\n\tkeyIntAuthPath = filepath.Join(homeBasePath, \"keyintauth.sh\")\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{\"1\", \"2\"}, 0, false, 1), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing keyboard interactive script: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tsftpdConf.KeyboardInteractiveAuthentication = true\n\tsftpdConf.KeyboardInteractiveHook = keyIntAuthPath\n\n\tcreateInitialFiles(scriptArgs)\n\tsftpdConf.TrustedUserCAKeys = append(sftpdConf.TrustedUserCAKeys, trustedCAUserKey)\n\tsftpdConf.RevokedUserCertsFile = revokeUserCerts\n\n\tgo func(cfg sftpd.Configuration) {\n\t\tlogger.Debug(logSender, \"\", \"initializing SFTP server with config %+v\", sftpdConf)\n\t\tif err := cfg.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SFTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}(sftpdConf)\n\n\tgo func() {\n\t\tif err := httpdConf.Initialize(configDir, 0); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twaitTCPListening(sftpdConf.Bindings[0].GetAddress())\n\twaitTCPListening(httpdConf.Bindings[0].GetAddress())\n\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort:             2222,\n\t\t\tApplyProxyConfig: true,\n\t\t},\n\t}\n\tsftpdConf.PasswordAuthentication = false\n\tcommon.Config.ProxyProtocol = 1\n\tgo func(cfg sftpd.Configuration) {\n\t\tlogger.Debug(logSender, \"\", \"initializing SFTP server with config %+v and proxy protocol %v\",\n\t\t\tsftpdConf, common.Config.ProxyProtocol)\n\t\tif err := cfg.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SFTP server with proxy protocol 1: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}(sftpdConf)\n\n\twaitTCPListening(sftpdConf.Bindings[0].GetAddress())\n\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort:             2226,\n\t\t\tApplyProxyConfig: false,\n\t\t},\n\t}\n\tsftpdConf.PasswordAuthentication = true\n\tgo func(cfg sftpd.Configuration) {\n\t\tlogger.Debug(logSender, \"\", \"initializing SFTP server with config %+v and proxy protocol %v\",\n\t\t\tcfg, common.Config.ProxyProtocol)\n\t\tif err := cfg.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SFTP server with proxy protocol 2: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}(sftpdConf)\n\n\twaitTCPListening(sftpdConf.Bindings[0].GetAddress())\n\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort:             2224,\n\t\t\tApplyProxyConfig: true,\n\t\t},\n\t}\n\tsftpdConf.PasswordAuthentication = true\n\tcommon.Config.ProxyProtocol = 2\n\tgo func() {\n\t\tlogger.Debug(logSender, \"\", \"initializing SFTP server with config %+v and proxy protocol %v\",\n\t\t\tsftpdConf, common.Config.ProxyProtocol)\n\t\tif err := sftpdConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SFTP server with proxy protocol 2: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twaitTCPListening(sftpdConf.Bindings[0].GetAddress())\n\tgetHostKeysFingerprints(sftpdConf.HostKeys)\n\tstartHTTPFs()\n\n\texitCode := m.Run()\n\tos.Remove(logFilePath)\n\tos.Remove(loginBannerFile)\n\tos.Remove(pubKeyPath)\n\tos.Remove(privateKeyPath)\n\tos.Remove(trustedCAUserKey)\n\tos.Remove(revokeUserCerts)\n\tos.Remove(gitWrapPath)\n\tos.Remove(extAuthPath)\n\tos.Remove(preLoginPath)\n\tos.Remove(postConnectPath)\n\tos.Remove(preDownloadPath)\n\tos.Remove(preUploadPath)\n\tos.Remove(keyIntAuthPath)\n\tos.Remove(checkPwdPath)\n\tos.Exit(exitCode)\n}\n\nfunc TestInitialization(t *testing.T) {\n\terr := config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tsftpdConf := config.GetSFTPDConfig()\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort:             2022,\n\t\t\tApplyProxyConfig: true,\n\t\t},\n\t\t{\n\t\t\tPort: 0,\n\t\t},\n\t}\n\tsftpdConf.LoginBannerFile = \"invalid_file\"\n\tsftpdConf.EnabledSSHCommands = append(sftpdConf.EnabledSSHCommands, \"ls\")\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.KeyboardInteractiveAuthentication = true\n\tsftpdConf.KeyboardInteractiveHook = \"invalid_file\"\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.KeyboardInteractiveAuthentication = true\n\tsftpdConf.KeyboardInteractiveHook = filepath.Join(homeBasePath, \"invalid_file\")\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.KeyboardInteractiveAuthentication = false\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort:             4444,\n\t\t\tApplyProxyConfig: true,\n\t\t},\n\t}\n\tcommon.Config.ProxyProtocol = 1\n\tassert.True(t, sftpdConf.Bindings[0].HasProxy())\n\tcommon.Config.ProxyProtocol = 0\n\tsftpdConf.HostKeys = []string{\"missing key\"}\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.HostKeys = nil\n\tsftpdConf.TrustedUserCAKeys = []string{\"missing ca key\"}\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.Bindings = nil\n\terr = sftpdConf.Initialize(configDir)\n\tassert.EqualError(t, err, common.ErrNoBinding.Error())\n\tsftpdConf = config.GetSFTPDConfig()\n\tsftpdConf.Ciphers = []string{\"not a cipher\"}\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unsupported cipher\")\n\t}\n\tsftpdConf.Ciphers = nil\n\tsftpdConf.MACs = []string{\"not a MAC\"}\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unsupported MAC algorithm\")\n\t}\n\tsftpdConf.MACs = nil\n\tsftpdConf.KexAlgorithms = []string{\"diffie-hellman-group-exchange-sha1\", \"not a KEX\"}\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unsupported key-exchange algorithm\")\n\t}\n\tsftpdConf.KexAlgorithms = nil\n\tsftpdConf.PublicKeyAlgorithms = []string{\"not a pub key algo\"}\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unsupported public key authentication algorithm\")\n\t}\n\tsftpdConf.PublicKeyAlgorithms = nil\n\tsftpdConf.HostKeyAlgorithms = []string{\"not a host key algo\"}\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unsupported host key algorithm\")\n\t}\n\tsftpdConf.HostKeyAlgorithms = nil\n\tsftpdConf.HostCertificates = []string{\"missing file\"}\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to load host certificate\")\n\t}\n\tsftpdConf.HostCertificates = []string{\".\"}\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\thostCertPath := filepath.Join(os.TempDir(), \"host_cert.pub\")\n\terr = os.WriteFile(hostCertPath, []byte(testCertValid), 0600)\n\tassert.NoError(t, err)\n\tsftpdConf.HostKeys = []string{privateKeyPath}\n\tsftpdConf.HostCertificates = []string{hostCertPath}\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is not an host certificate\")\n\t}\n\terr = os.WriteFile(hostCertPath, []byte(testPubKey), 0600)\n\tassert.NoError(t, err)\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is not an SSH certificate\")\n\t}\n\terr = os.WriteFile(hostCertPath, []byte(\"abc\"), 0600)\n\tassert.NoError(t, err)\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to parse host certificate\")\n\t}\n\terr = os.WriteFile(hostCertPath, []byte(testHostCert), 0600)\n\tassert.NoError(t, err)\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\n\terr = os.Remove(hostCertPath)\n\tassert.NoError(t, err)\n\tsftpdConf.HostKeys = nil\n\tsftpdConf.HostCertificates = nil\n\tsftpdConf.OPKSSHPath = \"relative path\"\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.OPKSSHPath = filepath.Join(os.TempDir(), \"missing path\")\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.OPKSSHChecksum = \"invalid checksum\"\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.OPKSSHPath = \"\"\n\tsftpdConf.OPKSSHChecksum = \"\"\n\tsftpdConf.RevokedUserCertsFile = \".\"\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\tsftpdConf.RevokedUserCertsFile = \"a missing file\"\n\terr = sftpdConf.Initialize(configDir)\n\tassert.ErrorIs(t, err, os.ErrNotExist)\n\n\terr = createTestFile(revokeUserCerts, 10*1024*1024)\n\tassert.NoError(t, err)\n\tsftpdConf.RevokedUserCertsFile = revokeUserCerts\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\n\terr = os.WriteFile(revokeUserCerts, []byte(`[]`), 0644)\n\tassert.NoError(t, err)\n\terr = sftpdConf.Initialize(configDir)\n\tassert.Error(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = sftpdConf.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to load configs from provider\")\n\t}\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicSFTPHandling(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaSize = 6553600\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\texpectedQuotaSize := user.UsedQuotaSize + testFileSize\n\t\texpectedQuotaFiles := user.UsedQuotaFiles + 1\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(\"/missing_dir\", testFileName), testFileSize, client)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), user.FirstUpload)\n\t\tassert.Equal(t, int64(0), user.FirstDownload)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.FirstUpload, int64(0))\n\t\tassert.Equal(t, int64(0), user.FirstDownload)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Lstat(testFileName)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)\n\t\tassert.Greater(t, user.FirstUpload, int64(0))\n\t\tassert.Greater(t, user.FirstDownload, int64(0))\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\tu.Username = \"missing user\"\n\t_, _, err = getSftpClient(u, false)\n\tassert.Error(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tstatus := sftpd.GetStatus()\n\tassert.True(t, status.IsActive)\n\tsshCommands := status.GetSSHCommandsAsString()\n\tassert.NotEmpty(t, sshCommands)\n\tsshAuths := status.GetSupportedAuthsAsString()\n\tassert.NotEmpty(t, sshAuths)\n\tassert.NotEmpty(t, status.HostKeys[0].GetAlgosAsString())\n\tassert.NotEmpty(t, status.GetMACsAsString())\n\tassert.NotEmpty(t, status.GetKEXsAsString())\n\tassert.NotEmpty(t, status.GetCiphersAsString())\n\tassert.NotEmpty(t, status.GetPublicKeysAlgosAsString())\n}\n\nfunc TestBasicSFTPFsHandling(t *testing.T) {\n\tusePubKey := true\n\tbaseUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestSFTPUser(usePubKey)\n\tu.QuotaSize = 6553600\n\tu.FsConfig.SFTPConfig.DisableCouncurrentReads = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\ttestLinkName := testFileName + \".link\"\n\t\ttestLinkToLinkName := testLinkName + \".link\"\n\t\texpectedQuotaSize := testFileSize\n\t\texpectedQuotaFiles := 1\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\terr = client.Symlink(testFileName, testLinkName)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := client.Lstat(testLinkName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.Mode()&os.ModeSymlink != 0)\n\t\t}\n\t\tinfo, err = client.Stat(testLinkName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.Mode()&os.ModeSymlink == 0)\n\t\t}\n\t\tval, err := client.ReadLink(testLinkName)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, path.Join(\"/\", testFileName), val)\n\t\t}\n\t\tlinkDir := \"linkDir\"\n\t\terr = client.Mkdir(linkDir)\n\t\tassert.NoError(t, err)\n\t\tlinkToLinkPath := path.Join(linkDir, testLinkToLinkName)\n\t\terr = client.Symlink(path.Join(\"/\", testLinkName), linkToLinkPath)\n\t\tassert.NoError(t, err)\n\t\tinfo, err = client.Lstat(linkToLinkPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.Mode()&os.ModeSymlink != 0)\n\t\t}\n\t\tinfo, err = client.Stat(linkToLinkPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.Mode()&os.ModeSymlink == 0)\n\t\t}\n\t\tval, err = client.ReadLink(linkToLinkPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, path.Join(\"/\", testLinkName), val)\n\t\t}\n\t\terr = client.Remove(linkToLinkPath)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(linkDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Lstat(testFileName)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t\t// now overwrite the symlink\n\t\terr = sftpUploadFile(testFilePath, testLinkName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tcontents, err := client.ReadDir(\"/\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Len(t, contents, 1)\n\t\t\tassert.Equal(t, testFileSize, contents[0].Size())\n\t\t\tassert.Equal(t, testLinkName, contents[0].Name())\n\t\t\tassert.False(t, contents[0].IsDir())\n\t\t\tassert.True(t, contents[0].Mode().IsRegular())\n\t\t}\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\n\t\tstat, err := client.StatVFS(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, uint64(u.QuotaSize/4096), stat.Blocks)\n\t\tassert.Equal(t, uint64((u.QuotaSize-testFileSize)/4096), stat.Bfree)\n\t\tassert.Equal(t, uint64(1), stat.Files-stat.Ffree)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPFsPasswordProtectedPrivateKey(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(true)\n\tu.PublicKeys = []string{testPubKeyPwd}\n\tbaseUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(testPrivateKeyPwd)\n\tu.FsConfig.SFTPConfig.KeyPassphrase = kms.NewPlainSecret(privateKeyPwd)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\t// update the user, the key must be preserved\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPFsEscapeHomeDir(t *testing.T) {\n\tusePubKey := true\n\tbaseUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestSFTPUser(usePubKey)\n\tsftpPrefix := \"/prefix\"\n\tu.FsConfig.SFTPConfig.Prefix = sftpPrefix\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t\tdirName := \"dir\"\n\t\tlinkName := \"link\"\n\t\terr := client.Mkdir(dirName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Symlink(baseUser.GetHomeDir(), filepath.Join(baseUser.GetHomeDir(), sftpPrefix, dirName, linkName))\n\t\tassert.NoError(t, err)\n\t\terr = os.Symlink(filepath.Join(baseUser.GetHomeDir(), sftpPrefix, dirName, linkName),\n\t\t\tfilepath.Join(baseUser.GetHomeDir(), sftpPrefix, linkName))\n\t\tassert.NoError(t, err)\n\t\t// linkName points to a link inside the home dir and this link points to a dir outside the home dir\n\t\t_, err = client.ReadLink(linkName)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t_, err = client.RealPath(linkName)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t_, err = client.ReadDir(linkName)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t_, err = client.ReadDir(path.Join(dirName, linkName))\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t_, err = client.ReadDir(\"/\")\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestReadDirLongNames(t *testing.T) {\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tnumFiles := 1000\n\t\tfor i := 0; i < 1000; i++ {\n\t\t\tfPath := filepath.Join(user.GetHomeDir(), hex.EncodeToString(util.GenerateRandomBytes(127)))\n\t\t\terr = os.WriteFile(fPath, util.GenerateRandomBytes(30), 0666)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\n\t\tentries, err := client.ReadDir(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, entries, numFiles)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestGroupSettingsOverride(t *testing.T) {\n\tusePubKey := true\n\tg := getTestGroup()\n\tg.UserSettings.Filters.StartDirectory = \"/%username%\"\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser(usePubKey)\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tcurrentDir, err := client.Getwd()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"/\"+user.Username, currentDir)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestStartDirectory(t *testing.T) {\n\tusePubKey := false\n\tstartDir := \"/st@ rt/dir\"\n\tu := getTestUser(usePubKey)\n\tu.Filters.StartDirectory = startDir\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.Filters.StartDirectory = startDir\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\n\t\t\tcurrentDir, err := client.Getwd()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, startDir, currentDir)\n\n\t\t\tentries, err := client.ReadDir(\".\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Len(t, entries, 0)\n\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = client.Stat(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(testFileName, testFileName+\"_rename\")\n\t\t\tassert.NoError(t, err)\n\n\t\t\tentries, err = client.ReadDir(\".\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Len(t, entries, 1)\n\n\t\t\tcurrentDir, err = client.RealPath(\"..\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, path.Dir(startDir), currentDir)\n\n\t\t\tcurrentDir, err = client.RealPath(\"../..\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, \"/\", currentDir)\n\n\t\t\tcurrentDir, err = client.RealPath(\"../../..\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, \"/\", currentDir)\n\n\t\t\terr = client.Remove(testFileName + \"_rename\")\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginNonExistentUser(t *testing.T) {\n\tusePubKey := true\n\tuser := getTestUser(usePubKey)\n\t_, _, err := getSftpClient(user, usePubKey)\n\tassert.Error(t, err)\n}\n\nfunc TestRateLimiter(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.RateLimitersConfig = []common.RateLimiterConfig{\n\t\t{\n\t\t\tAverage:   1,\n\t\t\tPeriod:    1000,\n\t\t\tBurst:     1,\n\t\t\tType:      1,\n\t\t\tProtocols: []string{common.ProtocolSSH},\n\t\t},\n\t}\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\t_, _, err = getSftpClient(user, usePubKey)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestDefender(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.DefenderConfig.Enabled = true\n\tcfg.DefenderConfig.Threshold = 3\n\tcfg.DefenderConfig.ScoreLimitExceeded = 2\n\tcfg.DefenderConfig.ScoreValid = 1\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser.Password = \"wrong_pwd\"\n\t_, _, err = getSftpClient(user, usePubKey)\n\tassert.Error(t, err)\n\thosts, _, err := httpdtest.GetDefenderHosts(http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\thost := hosts[0]\n\t\tassert.Empty(t, host.GetBanTime())\n\t\tassert.Equal(t, 1, host.Score)\n\t}\n\n\tfor i := 0; i < 2; i++ {\n\t\t_, _, err = getSftpClient(user, usePubKey)\n\t\tassert.Error(t, err)\n\t}\n\n\tuser.Password = defaultPassword\n\t_, _, err = getSftpClient(user, usePubKey)\n\tassert.Error(t, err)\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestOpenReadWrite(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaSize = 6553600\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.QuotaSize = 6553600\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\tsftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\ttestData := []byte(\"sample test data\")\n\t\t\t\tn, err := sftpFile.Write(testData)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testData), n)\n\t\t\t\tbuffer := make([]byte, 128)\n\t\t\t\tn, err = sftpFile.ReadAt(buffer, 1)\n\t\t\t\tassert.EqualError(t, err, io.EOF.Error())\n\t\t\t\tassert.Equal(t, len(testData)-1, n)\n\t\t\t\tassert.Equal(t, testData[1:], buffer[:n])\n\t\t\t\terr = sftpFile.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tsftpFile, err = client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\ttestData := []byte(\"new test data\")\n\t\t\t\tn, err := sftpFile.Write(testData)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testData), n)\n\t\t\t\tbuffer := make([]byte, 128)\n\t\t\t\tn, err = sftpFile.ReadAt(buffer, 1)\n\t\t\t\tassert.EqualError(t, err, io.EOF.Error())\n\t\t\t\tassert.Equal(t, len(testData)-1, n)\n\t\t\t\tassert.Equal(t, testData[1:], buffer[:n])\n\t\t\t\terr = sftpFile.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestOpenReadWritePerm(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\t// we cannot read inside \"/sub\", rename is needed otherwise the atomic upload will fail for the sftpfs user\n\tu.Permissions[\"/sub\"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermRename}\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.Permissions[\"/sub\"] = []string{dataprovider.PermUpload, dataprovider.PermListItems}\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = client.Mkdir(\"sub\")\n\t\t\tassert.NoError(t, err)\n\t\t\tsftpFileName := path.Join(\"sub\", \"file.txt\")\n\t\t\tsftpFile, err := client.OpenFile(sftpFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\ttestData := []byte(\"test data\")\n\t\t\t\tn, err := sftpFile.Write(testData)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testData), n)\n\t\t\t\tbuffer := make([]byte, 128)\n\t\t\t\t_, err = sftpFile.ReadAt(buffer, 1)\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.Contains(t, strings.ToLower(err.Error()), \"permission denied\")\n\t\t\t\t}\n\t\t\t\terr = sftpFile.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestConcurrency(t *testing.T) {\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 0\n\n\tusePubKey := true\n\tnumLogins := 50\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = numLogins + 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tvar wg sync.WaitGroup\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(262144)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tvar closedConns atomic.Int32\n\tfor i := 0; i < numLogins; i++ {\n\t\twg.Add(1)\n\t\tgo func(counter int) {\n\t\t\tdefer wg.Done()\n\t\t\tdefer closedConns.Add(1)\n\n\t\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\terr = sftpUploadFile(testFilePath, testFileName+strconv.Itoa(counter), testFileSize, client)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Greater(t, common.Connections.GetActiveSessions(defaultUsername), 0)\n\t\t\t\tclient.Close()\n\t\t\t\tconn.Close()\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tmaxConns := 0\n\t\tmaxSessions := 0\n\t\tfor {\n\t\t\tservedReqs := closedConns.Load()\n\t\t\tif servedReqs > 0 {\n\t\t\t\tstats := common.Connections.GetStats(\"\")\n\t\t\t\tif len(stats) > maxConns {\n\t\t\t\t\tmaxConns = len(stats)\n\t\t\t\t}\n\t\t\t\tactiveSessions := common.Connections.GetActiveSessions(defaultUsername)\n\t\t\t\tif activeSessions > maxSessions {\n\t\t\t\t\tmaxSessions = activeSessions\n\t\t\t\t}\n\t\t\t}\n\t\t\tif servedReqs >= int32(numLogins) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ttime.Sleep(1 * time.Millisecond)\n\t\t}\n\t\tassert.Greater(t, maxConns, 0)\n\t\tassert.Greater(t, maxSessions, 0)\n\t}()\n\n\twg.Wait()\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tfiles, err := client.ReadDir(\".\")\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, files, numLogins)\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetActiveSessions(defaultUsername) == 0\n\t}, 1*time.Second, 50*time.Millisecond)\n\n\tassert.Eventually(t, func() bool {\n\t\treturn len(common.Connections.GetStats(\"\")) == 0\n\t}, 1*time.Second, 50*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\nfunc TestProxyProtocol(t *testing.T) {\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\t// remove the home dir to test auto creation\n\terr = os.RemoveAll(user.HomeDir)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClientWithAddr(user, usePubKey, sftpSrvAddr2222)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t_, _, err = getSftpClientWithAddr(user, usePubKey, \"127.0.0.1:2224\")\n\tassert.Error(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRealPath(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tp, err := client.RealPath(\"../..\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, \"/\", p)\n\t\t\tp, err = client.RealPath(\"../test\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, \"/test\", p)\n\t\t\tsubdir := \"testsubdir\"\n\t\t\terr = client.Mkdir(subdir)\n\t\t\tassert.NoError(t, err)\n\t\t\tlinkName := testFileName + \"_link\"\n\t\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(subdir, linkName))\n\t\t\tassert.NoError(t, err)\n\t\t\tp, err = client.RealPath(path.Join(subdir, linkName))\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, path.Join(\"/\", testFileName), p)\n\t\t\t// an existing path\n\t\t\tsftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\ttestData := []byte(\"hello world\")\n\t\t\t\tn, err := sftpFile.WriteAt(testData, 0)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(testData), n)\n\t\t\t}\n\t\t\tp, err = client.RealPath(path.Join(subdir, linkName))\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, path.Join(\"/\", testFileName), p)\n\t\t\t// now a link outside the home dir\n\t\t\terr = os.Symlink(filepath.Clean(os.TempDir()), filepath.Join(localUser.GetHomeDir(), subdir, \"temp\"))\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = client.RealPath(path.Join(subdir, \"temp\"))\n\t\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\n\t\t\tconn.Close()\n\t\t\tclient.Close()\n\t\t\terr = os.Remove(filepath.Join(localUser.GetHomeDir(), subdir, \"temp\"))\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == localUser.Username {\n\t\t\t\terr = os.RemoveAll(filepath.Join(localUser.GetHomeDir(), subdir))\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestBufferedSFTP(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.FsConfig.SFTPConfig.BufferSize = 2\n\tu.HomeDir = filepath.Join(os.TempDir(), u.Username)\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(sftpUser, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\tappendDataSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tinitialHash, err := computeHashForFile(sha256.New(), testFilePath)\n\t\tassert.NoError(t, err)\n\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = appendToTestFile(testFilePath, appendDataSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadResumeFile(testFilePath, testFileName, testFileSize+appendDataSize, false, client)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t}\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tdownloadedFileHash, err := computeHashForFile(sha256.New(), localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, initialHash, downloadedFileHash)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\n\t\tsftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestData := []byte(\"sample test sftp data\")\n\t\t\tn, err := sftpFile.Write(testData)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, len(testData), n)\n\t\t\terr = sftpFile.Truncate(0)\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t\t}\n\t\t\terr = sftpFile.Truncate(4)\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t\t}\n\t\t\tbuffer := make([]byte, 128)\n\t\t\t_, err = sftpFile.Read(buffer)\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_OP_UNSUPPORTED\")\n\t\t\t}\n\t\t\terr = sftpFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tinfo, err := client.Stat(testFileName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, int64(len(testData)), info.Size())\n\t\t\t}\n\t\t}\n\t\t// test WriteAt\n\t\tsftpFile, err = client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestData := []byte(\"hello world\")\n\t\t\tn, err := sftpFile.WriteAt(testData[:6], 0)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 6, n)\n\t\t\tn, err = sftpFile.WriteAt(testData[6:], 6)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 5, n)\n\t\t\terr = sftpFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tinfo, err := client.Stat(testFileName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, int64(len(testData)), info.Size())\n\t\t\t}\n\t\t}\n\t\t// test ReadAt\n\t\tsftpFile, err = client.OpenFile(testFileName, os.O_RDONLY)\n\t\tif assert.NoError(t, err) {\n\t\t\tbuffer := make([]byte, 128)\n\t\t\tn, err := sftpFile.ReadAt(buffer, 6)\n\t\t\tassert.ErrorIs(t, err, io.EOF)\n\t\t\tassert.Equal(t, 5, n)\n\t\t\tassert.Equal(t, []byte(\"world\"), buffer[:n])\n\t\t\terr = sftpFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(sftpUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadResume(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestUser(usePubKey)\n\tu.FsConfig.OSConfig = sdk.OSFsConfig{\n\t\tWriteBufferSize: 1,\n\t\tReadBufferSize:  1,\n\t}\n\tu.Username += \"_buffered\"\n\tu.HomeDir += \"_with_buf\"\n\tbufferedUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tfor _, user := range []dataprovider.User{localUser, sftpUser, bufferedUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\tappendDataSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = appendToTestFile(testFilePath, appendDataSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadResumeFile(testFilePath, testFileName, testFileSize+appendDataSize, false, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize+appendDataSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tinitialHash, err := computeHashForFile(sha256.New(), testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tdownloadedFileHash, err := computeHashForFile(sha256.New(), localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, initialHash, downloadedFileHash)\n\t\t\terr = sftpUploadResumeFile(testFilePath, testFileName, testFileSize+appendDataSize, true, client)\n\t\t\tassert.Error(t, err, \"resume uploading file with invalid offset must fail\")\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(localDownloadPath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(bufferedUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(bufferedUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDirCommands(t *testing.T) {\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\t// remove the home dir to test auto creation\n\terr = os.RemoveAll(user.HomeDir)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(\"test1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"test1\", \"test\")\n\t\tassert.NoError(t, err)\n\t\t// rename a missing file\n\t\terr = client.Rename(\"test1\", \"test2\")\n\t\tassert.Error(t, err)\n\t\t_, err = client.Lstat(\"/test1\")\n\t\tassert.Error(t, err, \"stat for renamed dir must not succeed\")\n\t\terr = client.PosixRename(\"test\", \"test1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(\"test1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(\"/test/test1\")\n\t\tassert.Error(t, err, \"recursive mkdir must fail\")\n\t\terr = client.Mkdir(\"/test\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(\"/test/test1\")\n\t\tassert.NoError(t, err)\n\t\t_, err = client.ReadDir(\"/this/dir/does/not/exist\")\n\t\tassert.Error(t, err, \"reading a missing dir must fail\")\n\t\terr = client.RemoveDirectory(\"/test/test1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(\"/test\")\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Lstat(\"/test\")\n\t\tassert.Error(t, err, \"stat for deleted dir must not succeed\")\n\t\t_, err = client.Stat(\"/test\")\n\t\tassert.Error(t, err, \"stat for deleted dir must not succeed\")\n\t\terr = client.RemoveDirectory(\"/test\")\n\t\tassert.Error(t, err, \"remove missing path must fail\")\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRemove(t *testing.T) {\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(\"test\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(\"/test/test1\")\n\t\tassert.NoError(t, err)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(\"/test\", testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(\"/test\")\n\t\tassert.Error(t, err, \"remove non empty dir must fail\")\n\t\terr = client.RemoveDirectory(path.Join(\"/test\", testFileName))\n\t\tassert.Error(t, err, \"remove a file with rmdir must fail\")\n\t\terr = client.Remove(path.Join(\"/test\", testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(path.Join(\"/test\", testFileName))\n\t\tassert.Error(t, err, \"remove missing file must fail\")\n\t\terr = client.Remove(\"/test/test1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(\"/test\")\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLink(t *testing.T) {\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\tassert.NoError(t, err)\n\t\tlinkName, err := client.ReadLink(testFileName + \".link\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, path.Join(\"/\", testFileName), linkName)\n\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\tassert.Error(t, err, \"creating a symlink to an existing one must fail\")\n\t\terr = client.Link(testFileName, testFileName+\".hlink\")\n\t\tassert.Error(t, err, \"hard link is not supported and must fail\")\n\t\terr = client.Remove(testFileName + \".link\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestStat(t *testing.T) {\n\tusePubKey := false\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err := client.Lstat(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = client.Stat(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\t// stat a missing path we should get an fs.ErrNotExist error\n\t\t\t_, err = client.Stat(\"missing path\")\n\t\t\tassert.True(t, errors.Is(err, fs.ErrNotExist))\n\t\t\t_, err = client.Lstat(\"missing path\")\n\t\t\tassert.True(t, errors.Is(err, fs.ErrNotExist))\n\t\t\t// mode 0666 and 0444 works on Windows too\n\t\t\tnewPerm := os.FileMode(0666)\n\t\t\terr = client.Chmod(testFileName, newPerm)\n\t\t\tassert.NoError(t, err)\n\t\t\tnewFi, err := client.Lstat(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, newPerm, newFi.Mode().Perm())\n\t\t\tnewPerm = os.FileMode(0444)\n\t\t\terr = client.Chmod(testFileName, newPerm)\n\t\t\tassert.NoError(t, err)\n\t\t\tnewFi, err = client.Lstat(testFileName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, newPerm, newFi.Mode().Perm())\n\t\t\t}\n\t\t\t_, err = client.ReadLink(testFileName)\n\t\t\tassert.Error(t, err, \"readlink on a file must fail\")\n\t\t\tsymlinkName := testFileName + \".sym\"\n\t\t\terr = client.Symlink(testFileName, symlinkName)\n\t\t\tassert.NoError(t, err)\n\t\t\tinfo, err := client.Lstat(symlinkName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.True(t, info.Mode()&os.ModeSymlink != 0)\n\t\t\t}\n\t\t\tinfo, err = client.Stat(symlinkName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.False(t, info.Mode()&os.ModeSymlink != 0)\n\t\t\t}\n\t\t\tlinkName, err := client.ReadLink(symlinkName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, path.Join(\"/\", testFileName), linkName)\n\t\t\tnewPerm = os.FileMode(0666)\n\t\t\terr = client.Chmod(testFileName, newPerm)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Truncate(testFileName, 100)\n\t\t\tassert.NoError(t, err)\n\t\t\tfi, err := client.Stat(testFileName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, int64(100), fi.Size())\n\t\t\t}\n\t\t\tf, err := client.OpenFile(testFileName, os.O_WRONLY)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\terr = f.Truncate(5)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tf, err = client.OpenFile(testFileName, os.O_WRONLY)\n\t\t\tnewPerm = os.FileMode(0444)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\terr = f.Chmod(newPerm)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tnewFi, err = client.Lstat(testFileName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, newPerm, newFi.Mode().Perm())\n\t\t\t}\n\t\t\tnewPerm = os.FileMode(0666)\n\t\t\terr = client.Chmod(testFileName, newPerm)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestStatChownChmod(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"chown is not supported on Windows, chmod is partially supported\")\n\t}\n\tusePubKey := true\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Chown(testFileName, os.Getuid(), os.Getgid())\n\t\t\tassert.NoError(t, err)\n\t\t\tnewPerm := os.FileMode(0600)\n\t\t\terr = client.Chmod(testFileName, newPerm)\n\t\t\tassert.NoError(t, err)\n\t\t\tnewFi, err := client.Lstat(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, newPerm, newFi.Mode().Perm())\n\t\t\terr = client.Remove(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Chmod(testFileName, newPerm)\n\t\t\tassert.EqualError(t, err, os.ErrNotExist.Error())\n\t\t\terr = client.Chown(testFileName, os.Getuid(), os.Getgid())\n\t\t\tassert.EqualError(t, err, os.ErrNotExist.Error())\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPFsLoginWrongFingerprint(t *testing.T) {\n\tusePubKey := true\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(sftpUser, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\tsftpUser.FsConfig.SFTPConfig.Fingerprints = append(sftpUser.FsConfig.SFTPConfig.Fingerprints, \"wrong\")\n\t_, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(sftpUser, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\tout, err := runSSHCommand(\"md5sum\", sftpUser, usePubKey)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(out), \"d41d8cd98f00b204e9800998ecf8427e\")\n\n\tsftpUser.FsConfig.SFTPConfig.Fingerprints = []string{\"wrong\"}\n\t_, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(sftpUser, usePubKey)\n\tif !assert.Error(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestChtimes(t *testing.T) {\n\tusePubKey := false\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\ttestDir := \"test\" //nolint:goconst\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tacmodTime := time.Now()\n\t\t\terr = client.Chtimes(testFileName, acmodTime, acmodTime)\n\t\t\tassert.NoError(t, err)\n\t\t\tnewFi, err := client.Lstat(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t\tdiff := math.Abs(newFi.ModTime().Sub(acmodTime).Seconds())\n\t\t\tassert.LessOrEqual(t, diff, float64(1))\n\t\t\terr = client.Chtimes(\"invalidFile\", acmodTime, acmodTime)\n\t\t\tassert.EqualError(t, err, os.ErrNotExist.Error())\n\t\t\terr = client.Mkdir(testDir)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Chtimes(testDir, acmodTime, acmodTime)\n\t\t\tassert.NoError(t, err)\n\t\t\tnewFi, err = client.Lstat(testDir)\n\t\t\tassert.NoError(t, err)\n\t\t\tdiff = math.Abs(newFi.ModTime().Sub(acmodTime).Seconds())\n\t\t\tassert.LessOrEqual(t, diff, float64(1))\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\n// basic tests to verify virtual chroot, should be improved to cover more cases ...\nfunc TestEscapeHomeDir(t *testing.T) {\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tdirOutsideHome := filepath.Join(homeBasePath, defaultUsername+\"1\", \"dir\")\n\terr = os.MkdirAll(dirOutsideHome, os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\ttestDir := \"testDir\" //nolint:goconst\n\t\tlinkPath := filepath.Join(homeBasePath, defaultUsername, testDir)\n\t\terr = os.Symlink(homeBasePath, linkPath)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.ReadDir(testDir)\n\t\tassert.Error(t, err, \"reading a symbolic link outside home dir should not succeeded\")\n\t\terr = os.Remove(linkPath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Symlink(dirOutsideHome, linkPath)\n\t\tassert.NoError(t, err)\n\t\t_, err := client.ReadDir(testDir)\n\t\tassert.Error(t, err, \"reading a symbolic link outside home dir should not succeeded\")\n\t\terr = client.Chmod(path.Join(testDir, \"sub\", \"dir\"), os.ModePerm)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\tassert.Error(t, err, \"setstat on a file outside home dir must fail\")\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tremoteDestPath := path.Join(\"..\", \"..\", testFileName)\n\t\terr = sftpUploadFile(testFilePath, remoteDestPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Lstat(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\tlinkPath = filepath.Join(homeBasePath, defaultUsername, testFileName)\n\t\terr = os.Symlink(homeBasePath, linkPath)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, testFilePath, 0, client)\n\t\tassert.Error(t, err, \"download file outside home dir must fail\")\n\t\terr = sftpUploadFile(testFilePath, remoteDestPath, testFileSize, client)\n\t\tassert.Error(t, err, \"overwrite a file outside home dir must fail\")\n\t\terr = client.Chmod(remoteDestPath, 0644)\n\t\tassert.Error(t, err, \"setstat on a file outside home dir must fail\")\n\t\terr = os.Remove(linkPath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(filepath.Join(homeBasePath, defaultUsername+\"1\"))\n\tassert.NoError(t, err)\n}\n\nfunc TestEscapeSFTPFsPrefix(t *testing.T) {\n\tusePubKey := false\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestSFTPUser(usePubKey)\n\tsftpPrefix := \"/prefix\"\n\toutPrefix1 := \"/pre\"\n\toutPrefix2 := sftpPrefix + \"1\"\n\tout1 := \"out1\"\n\tout2 := \"out2\"\n\tu.FsConfig.SFTPConfig.Prefix = sftpPrefix\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(localUser, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(sftpPrefix)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(outPrefix1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(outPrefix2)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(outPrefix1, path.Join(sftpPrefix, out1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(outPrefix2, path.Join(sftpPrefix, out2))\n\t\tassert.NoError(t, err)\n\t}\n\n\tconn, client, err = getSftpClient(sftpUser, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tcontents, err := client.ReadDir(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, contents, 2)\n\t\t_, err = client.ReadDir(out1)\n\t\tassert.Error(t, err)\n\t\t_, err = client.ReadDir(out2)\n\t\tassert.Error(t, err)\n\t\terr = client.Mkdir(path.Join(out1, \"subout1\"))\n\t\tassert.Error(t, err)\n\t\terr = client.Mkdir(path.Join(out2, \"subout2\"))\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestGetMimeTypeSFTPFs(t *testing.T) {\n\tusePubKey := false\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(localUser, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tsftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)\n\t\tif assert.NoError(t, err) {\n\t\t\ttestData := []byte(\"some UTF-8 text so we should get a text/plain mime type\")\n\t\t\tn, err := sftpFile.Write(testData)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, len(testData), n)\n\t\t\terr = sftpFile.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\tsftpUser.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\tsftpUser.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret()\n\tfs, err := sftpUser.GetFilesystem(\"connID\")\n\tif assert.NoError(t, err) {\n\t\tassert.True(t, vfs.IsSFTPFs(fs))\n\t\tmime, err := fs.GetMimeType(testFileName)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"text/plain; charset=utf-8\", mime)\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHomeSpecialChars(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.HomeDir = filepath.Join(homeBasePath, \"abc açà#&%lk\")\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tfiles, err := client.ReadDir(\".\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, len(files))\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLogin(t *testing.T) {\n\tu := getTestUser(false)\n\tu.PublicKeys = []string{testPubKey}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, false)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.LastLogin, int64(0), \"last login must be updated after a successful login: %v\", user.LastLogin)\n\t}\n\tconn, client, err = getSftpClient(user, true)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser.Password = \"invalid password\"\n\tconn, client, err = getSftpClient(user, false)\n\tif !assert.Error(t, err, \"login with invalid password must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t// testPubKey1 is not authorized\n\tuser.PublicKeys = []string{testPubKey1}\n\tuser.Password = \"\"\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, true)\n\tif !assert.Error(t, err, \"login with invalid public key must fail\") {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t}\n\t// login a user with multiple public keys, only the second one is valid\n\tuser.PublicKeys = []string{testPubKey1, testPubKey}\n\tuser.Password = \"\"\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, true)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginUserCert(t *testing.T) {\n\tu := getTestUser(true)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// try login using a cert signed from a trusted CA\n\tsigner, err := getSignerForUserCert([]byte(testCertValid))\n\tassert.NoError(t, err)\n\tconn, client, err := getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t// revoke the certificate\n\tcerts := []string{\"SHA256:OkxVB1ImSJ2XeI8nA2Wg+6zJVlxdevD1FYBSEJjFEN4\"}\n\tdata, err := json.Marshal(certs)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(revokeUserCerts, data, 0644)\n\tassert.NoError(t, err)\n\terr = sftpd.Reload()\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t// if we remove the revoked certificate login should work again\n\tcerts = []string{\"SHA256:bsBRHC/xgiqBJdSuvSTNpJNLTISP/G356jNMCRYC5Es, SHA256:1kxVB1ImSJ2XeI8nA2Wg+6zJVlxdevD1FYBSEJjFEN4\"}\n\tdata, err = json.Marshal(certs)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(revokeUserCerts, data, 0644)\n\tassert.NoError(t, err)\n\terr = sftpd.Reload()\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\t// try login using a cert signed from an untrusted CA\n\tsigner, err = getSignerForUserCert([]byte(testCertUntrustedCA))\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t// try login using an host certificate instead of an user certificate\n\tsigner, err = getSignerForUserCert([]byte(testHostCert))\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t// try login using a user certificate with an authorized source address different from localhost\n\tsigner, err = getSignerForUserCert([]byte(testCertOtherSourceAddress))\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t// try login using an expired certificate\n\tsigner, err = getSignerForUserCert([]byte(testCertExpired))\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t// try login using a certificate with no principals\n\tsigner, err = getSignerForUserCert([]byte(testCertNoPrincipals))\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\t// the user does not exist\n\tsigner, err = getSignerForUserCert([]byte(testCertValid))\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t// now login with a username not in the set of valid principals for the given certificate\n\tu.Username += \"1\"\n\tuser, _, err = httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tsigner, err = getSignerForUserCert([]byte(testCertValid))\n\tassert.NoError(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\terr = os.WriteFile(revokeUserCerts, []byte(`[]`), 0644)\n\tassert.NoError(t, err)\n\terr = sftpd.Reload()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMultiStepLoginKeyAndPwd(t *testing.T) {\n\tu := getTestUser(true)\n\tu.Password = defaultPassword\n\tu.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}...)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, true)\n\tif !assert.Error(t, err, \"login with public key is disallowed and must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tconn, client, err = getSftpClient(user, true)\n\tif !assert.Error(t, err, \"login with password is disallowed and must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tsigner, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))\n\tauthMethods := []ssh.AuthMethod{\n\t\tssh.PublicKeys(signer),\n\t\tssh.Password(defaultPassword),\n\t}\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods, sftpSrvAddr2222)\n\tif !assert.Error(t, err, \"password auth is disabled on port 2222, multi-step auth must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tauthMethods = []ssh.AuthMethod{\n\t\tssh.Password(defaultPassword),\n\t\tssh.PublicKeys(signer),\n\t}\n\t_, _, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tassert.Error(t, err, \"multi step auth login with wrong order must fail\")\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMultiStepLoginKeyAndKeyInt(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser(true)\n\tu.Password = defaultPassword\n\tu.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{\n\t\tdataprovider.SSHLoginMethodKeyAndPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}...)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{\"1\", \"2\"}, 0, false, 1), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, true)\n\tif !assert.Error(t, err, \"login with public key is disallowed and must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\tsigner, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))\n\tauthMethods := []ssh.AuthMethod{\n\t\tssh.PublicKeys(signer),\n\t\tssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) {\n\t\t\treturn []string{\"1\", \"2\"}, nil\n\t\t}),\n\t}\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods, sftpSrvAddr2222)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tauthMethods = []ssh.AuthMethod{\n\t\tssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) {\n\t\t\treturn []string{\"1\", \"2\"}, nil\n\t\t}),\n\t\tssh.PublicKeys(signer),\n\t}\n\t_, _, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tassert.Error(t, err, \"multi step auth login with wrong order must fail\")\n\n\tauthMethods = []ssh.AuthMethod{\n\t\tssh.PublicKeys(signer),\n\t\tssh.Password(defaultPassword),\n\t}\n\t_, _, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tassert.Error(t, err, \"multi step auth login with wrong method must fail\")\n\n\tuser.Filters.DeniedLoginMethods = nil\n\tuser.Filters.DeniedLoginMethods = append(user.Filters.DeniedLoginMethods, []string{\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}...)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, _, err = getCustomAuthSftpClient(user, authMethods, sftpSrvAddr2222)\n\tassert.Error(t, err)\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tif assert.NoError(t, err) {\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMultiStepLoginCertAndPwd(t *testing.T) {\n\tu := getTestUser(true)\n\tu.Password = defaultPassword\n\tu.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}...)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsigner, err := getSignerForUserCert([]byte(testCertValid))\n\tassert.NoError(t, err)\n\tauthMethods := []ssh.AuthMethod{\n\t\tssh.PublicKeys(signer),\n\t\tssh.Password(defaultPassword),\n\t}\n\tconn, client, err := getCustomAuthSftpClient(user, authMethods, \"\")\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\tsigner, err = getSignerForUserCert([]byte(testCertOtherSourceAddress))\n\tassert.NoError(t, err)\n\tauthMethods = []ssh.AuthMethod{\n\t\tssh.PublicKeys(signer),\n\t\tssh.Password(defaultPassword),\n\t}\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginUserStatus(t *testing.T) {\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.LastLogin, int64(0), \"last login must be updated after a successful login: %v\", user.LastLogin)\n\t}\n\tuser.Status = 0\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif !assert.Error(t, err, \"login for a disabled user must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginUserExpiration(t *testing.T) {\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.LastLogin, int64(0), \"last login must be updated after a successful login: %v\", user.LastLogin)\n\t}\n\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now()) - 120000\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif !assert.Error(t, err, \"login for an expired user must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tuser.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now()) + 120000\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginWithDatabaseCredentials(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"testbucket\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ \"type\": \"service_account\", \"private_key\": \" \", \"client_email\": \"example@iam.gserviceaccount.com\" }`)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())\n\tassert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())\n\tassert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey())\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginInvalidFs(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"test\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(\"invalid JSON for credentials\")\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif !assert.Error(t, err, \"login must fail, the user has an invalid filesystem config\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDeniedProtocols(t *testing.T) {\n\tu := getTestUser(true)\n\tu.Filters.DeniedProtocols = []string{common.ProtocolSSH}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, true)\n\tif !assert.Error(t, err, \"SSH protocol is disabled, authentication must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolFTP, common.ProtocolWebDAV}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, true)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDeniedLoginMethods(t *testing.T) {\n\tu := getTestUser(true)\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.LoginMethodPassword}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, true)\n\tif !assert.Error(t, err, \"public key login is disabled, authentication must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.LoginMethodPassword}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, true)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser.Password = defaultPassword\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(user, false)\n\tif !assert.Error(t, err, \"password login is disabled, authentication must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodPublicKey}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, false)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginWithIPFilters(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Filters.DeniedIP = []string{\"192.167.0.0/24\", \"172.18.0.0/16\"}\n\tu.Filters.AllowedIP = []string{}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.LastLogin, int64(0), \"last login must be updated after a successful login: %v\", user.LastLogin)\n\t}\n\tuser.Filters.AllowedIP = []string{\"127.0.0.0/8\"}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser.Filters.AllowedIP = []string{\"172.19.0.0/16\"}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif !assert.Error(t, err, \"login from an not allowed IP must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginEmptyPassword(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Password = \"\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Password = \"empty\"\n\t_, _, err = getSftpClient(user, usePubKey)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginAnonymousUser(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Password = \"\"\n\tu.Filters.IsAnonymous = true\n\t_, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.Error(t, err)\n\tuser, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.IsAnonymous)\n\tassert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions[\"/\"])\n\tassert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)\n\tassert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,\n\t\tdataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)\n\t_, _, err = getSftpClient(user, usePubKey)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestAnonymousGroupInheritance(t *testing.T) {\n\tg := getTestGroup()\n\tg.UserSettings.Filters.IsAnonymous = true\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: group.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\t_, _, err = getSftpClient(user, usePubKey)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginAfterUserUpdateEmptyPwd(t *testing.T) {\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Password = \"\"\n\t// password should remain unchanged\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginKeyboardInteractiveAuth(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tuser, _, err := httpdtest.AddUser(getTestUser(false), http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{\"1\", \"2\"}, 0, false, 1), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err := getKeyboardInteractiveSftpClient(user, []string{\"1\", \"2\"})\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser.Status = 0\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getKeyboardInteractiveSftpClient(user, []string{\"1\", \"2\"})\n\tif !assert.Error(t, err, \"keyboard interactive auth must fail the user is disabled\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tuser.Status = 1\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{\"1\", \"2\"}, 0, false, -1), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getKeyboardInteractiveSftpClient(user, []string{\"1\", \"2\"})\n\tif !assert.Error(t, err, \"keyboard interactive auth must fail the script returned -1\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{\"1\", \"2\"}, 0, true, 1), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getKeyboardInteractiveSftpClient(user, []string{\"1\", \"2\"})\n\tif !assert.Error(t, err, \"keyboard interactive auth must fail the script returned bad json\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{\"1\", \"2\"}, 5, true, 1), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getKeyboardInteractiveSftpClient(user, []string{\"1\", \"2\"})\n\tif !assert.Error(t, err, \"keyboard interactive auth must fail the script returned bad json\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestInteractiveLoginWithPasscode(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tuser, _, err := httpdtest.AddUser(getTestUser(false), http.StatusCreated)\n\tassert.NoError(t, err)\n\t// test password check\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptForBuiltinChecks(false, 1), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err := getKeyboardInteractiveSftpClient(user, []string{defaultPassword})\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t// wrong password\n\t_, _, err = getKeyboardInteractiveSftpClient(user, []string{\"wrong_password\"})\n\tassert.Error(t, err)\n\t// correct password but the script returns an error\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptForBuiltinChecks(false, 0), os.ModePerm)\n\tassert.NoError(t, err)\n\t_, _, err = getKeyboardInteractiveSftpClient(user, []string{\"wrong_password\"})\n\tassert.Error(t, err)\n\t// add multi-factor authentication\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tpasscode, err := totp.GenerateCodeCustom(key.Secret(), time.Now(), totp.ValidateOpts{\n\t\tPeriod:    30,\n\t\tSkew:      1,\n\t\tDigits:    otp.DigitsSix,\n\t\tAlgorithm: otp.AlgorithmSHA1,\n\t})\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptForBuiltinChecks(true, 1), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tpasswordAsked := false\n\tpasscodeAsked := false\n\tauthMethods := []ssh.AuthMethod{\n\t\tssh.KeyboardInteractive(func(_, _ string, questions []string, _ []bool) ([]string, error) {\n\t\t\tvar answers []string\n\t\t\tif strings.HasPrefix(questions[0], \"Password\") {\n\t\t\t\tanswers = append(answers, defaultPassword)\n\t\t\t\tpasswordAsked = true\n\t\t\t} else {\n\t\t\t\tanswers = append(answers, passcode)\n\t\t\t\tpasscodeAsked = true\n\t\t\t}\n\t\t\treturn answers, nil\n\t\t}),\n\t}\n\tconn, client, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tassert.True(t, passwordAsked)\n\tassert.True(t, passcodeAsked)\n\t// the same passcode cannot be reused\n\t_, _, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tassert.Error(t, err)\n\t// correct passcode but the script returns an error\n\tconfigName, key, _, err = mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tpasscode, err = totp.GenerateCodeCustom(key.Secret(), time.Now(), totp.ValidateOpts{\n\t\tPeriod:    30,\n\t\tSkew:      1,\n\t\tDigits:    otp.DigitsSix,\n\t\tAlgorithm: otp.AlgorithmSHA1,\n\t})\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptForBuiltinChecks(true, 0), os.ModePerm)\n\tassert.NoError(t, err)\n\tpasswordAsked = false\n\tpasscodeAsked = false\n\t_, _, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tassert.Error(t, err)\n\tauthMethods = []ssh.AuthMethod{\n\t\tssh.KeyboardInteractive(func(_, _ string, questions []string, _ []bool) ([]string, error) {\n\t\t\tvar answers []string\n\t\t\tif strings.HasPrefix(questions[0], \"Password\") {\n\t\t\t\tanswers = append(answers, defaultPassword)\n\t\t\t\tpasswordAsked = true\n\t\t\t} else {\n\t\t\t\tanswers = append(answers, passcode)\n\t\t\t\tpasscodeAsked = true\n\t\t\t}\n\t\t\treturn answers, nil\n\t\t}),\n\t}\n\t_, _, err = getCustomAuthSftpClient(user, authMethods, \"\")\n\tassert.Error(t, err)\n\tassert.True(t, passwordAsked)\n\tassert.True(t, passcodeAsked)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMustChangePasswordRequirement(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Filters.RequirePasswordChange = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\t// public key auth works even if the user must change password\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t// password auth does not work\n\t_, _, err = getSftpClient(user, false)\n\tassert.Error(t, err)\n\t// change password\n\terr = dataprovider.UpdateUserPassword(user.Username, defaultPassword, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, false)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSecondFactorRequirement(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\t_, _, err = getSftpClient(user, usePubKey)\n\tassert.Error(t, err)\n\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestNamingRules(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.NamingRules = 7\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Username = \"useR@user.com \"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"user@user.com\", user.Username)\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tu.Password = defaultPassword\n\t_, _, err = httpdtest.UpdateUser(u, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, false)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t_, err = httpdtest.RemoveUser(u, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(u.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreLoginScript(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tfolderName := filepath.Base(mappedPath)\n\tfolderMountPath := \"/vpath\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: folderMountPath,\n\t})\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestData := []byte(\"test data\")\n\t\terr = os.WriteFile(testFilePath, testData, 0666)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(folderMountPath, testFileName), int64(len(testData)), client)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := os.Stat(filepath.Join(mappedPath, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Greater(t, info.Size(), int64(len(testData)))\n\t\t}\n\t}\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err, \"pre-login script returned a non json response, login must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t// now disable the the hook\n\tuser.Filters.Hooks.PreLoginDisabled = true\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\tuser.Filters.Hooks.PreLoginDisabled = false\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tuser.Status = 0\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err, \"pre-login script returned a disabled user, login must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreLoginUserCreation(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/list\"] = []string{\"list\", \"download\"}\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\t_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Permissions, 2)\n\tassert.Empty(t, user.Description)\n\tu.Description = \"some desc\"\n\tdelete(u.Permissions, \"/list\")\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\t// The user should be updated and list permission removed\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Permissions, 1)\n\tassert.NotEmpty(t, user.Description)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreLoginHookPreserveMFAConfig(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t// add multi-factor authentication\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.RecoveryCodes, 0)\n\tassert.False(t, user.Filters.TOTPConfig.Enabled)\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tfor i := 0; i < 12; i++ {\n\t\tuser.Filters.RecoveryCodes = append(user.Filters.RecoveryCodes, dataprovider.RecoveryCode{\n\t\t\tSecret: kms.NewPlainSecret(fmt.Sprintf(\"RC-%v\", strings.ToUpper(util.GenerateUniqueID()))),\n\t\t})\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.RecoveryCodes, 12)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, configName, user.Filters.TOTPConfig.ConfigName)\n\tassert.Equal(t, []string{common.ProtocolSSH}, user.Filters.TOTPConfig.Protocols)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())\n\n\terr = os.WriteFile(extAuthPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.RecoveryCodes, 12)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, configName, user.Filters.TOTPConfig.ConfigName)\n\tassert.Equal(t, []string{common.ProtocolSSH}, user.Filters.TOTPConfig.Protocols)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreDownloadHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldExecuteOn := common.Config.Actions.ExecuteOn\n\toldHook := common.Config.Actions.Hook\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}\n\tcommon.Config.Actions.Hook = preDownloadPath\n\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preDownloadPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(131072)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t}\n\n\tremoteSCPDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\terr = scpDownload(localDownloadPath, remoteSCPDownPath, false, false)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(preDownloadPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t}\n\n\terr = scpDownload(localDownloadPath, remoteSCPDownPath, false, false)\n\tassert.Error(t, err)\n\n\tcommon.Config.Actions.Hook = \"http://127.0.0.1:8080/web/admin/login\"\n\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t}\n\terr = scpDownload(localDownloadPath, remoteSCPDownPath, false, false)\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.Hook = \"http://127.0.0.1:8080/\"\n\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t}\n\terr = scpDownload(localDownloadPath, remoteSCPDownPath, false, false)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.ExecuteOn = oldExecuteOn\n\tcommon.Config.Actions.Hook = oldHook\n}\n\nfunc TestPreUploadHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldExecuteOn := common.Config.Actions.ExecuteOn\n\toldHook := common.Config.Actions.Hook\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreUpload}\n\tcommon.Config.Actions.Hook = preUploadPath\n\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preUploadPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(131072)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t}\n\n\tremoteSCPUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\terr = scpUpload(testFilePath, remoteSCPUpPath, true, false)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(preUploadPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = sftpUploadFile(testFilePath, testFileName+\"1\", testFileSize, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t}\n\terr = scpUpload(testFilePath, remoteSCPUpPath, true, false)\n\tassert.Error(t, err)\n\n\tcommon.Config.Actions.Hook = \"http://127.0.0.1:8080/web/client/login\"\n\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t}\n\terr = scpUpload(testFilePath, remoteSCPUpPath, true, false)\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.Hook = \"http://127.0.0.1:8080/web\"\n\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = sftpUploadFile(testFilePath, testFileName+\"1\", testFileSize, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t}\n\n\terr = scpUpload(testFilePath, remoteSCPUpPath, true, false)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.Actions.ExecuteOn = oldExecuteOn\n\tcommon.Config.Actions.Hook = oldHook\n}\n\nfunc TestPostConnectHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tcommon.Config.PostConnectHook = postConnectPath\n\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\terr = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\tcommon.Config.PostConnectHook = \"http://127.0.0.1:8080/healthz\"\n\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\tcommon.Config.PostConnectHook = \"http://127.0.0.1:8080/notfound\"\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.PostConnectHook = \"\"\n}\n\nfunc TestCheckPwdHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1000\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(checkPwdPath, getCheckPwdScriptsContents(2, defaultPassword), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.CheckPasswordHook = checkPwdPath\n\tproviderConf.CheckPasswordScope = 1\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\terr = os.WriteFile(checkPwdPath, getCheckPwdScriptsContents(0, defaultPassword), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t// now disable the the hook\n\tuser.Filters.Hooks.CheckPasswordDisabled = true\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t// enable the hook again\n\tuser.Filters.Hooks.CheckPasswordDisabled = false\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(checkPwdPath, getCheckPwdScriptsContents(1, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword + \"1\"\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\tproviderConf.CheckPasswordScope = 6\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword + \"1\"\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(checkPwdPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginExternalAuthPwdAndPubKey(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1000\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\ttestFileSize := int64(65535)\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\tu.Username = defaultUsername + \"1\"\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err, \"external auth login with invalid user must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tusePubKey = false\n\tu = getTestUser(usePubKey)\n\tu.PublicKeys = []string{}\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, len(user.PublicKeys))\n\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\n\tu.Status = 0\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err) {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t// now disable the the hook\n\tuser.Filters.Hooks.ExternalAuthDisabled = true\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthMultiStepLoginKeyAndPwd(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser(true)\n\tu.Password = defaultPassword\n\tu.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}...)\n\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tsigner, err := ssh.ParsePrivateKey([]byte(testPrivateKey))\n\tassert.NoError(t, err)\n\tauthMethods := []ssh.AuthMethod{\n\t\tssh.PublicKeys(signer),\n\t\tssh.Password(defaultPassword),\n\t}\n\tconn, client, err := getCustomAuthSftpClient(u, authMethods, \"\")\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t// wrong sequence should fail\n\tauthMethods = []ssh.AuthMethod{\n\t\tssh.Password(defaultPassword),\n\t\tssh.PublicKeys(signer),\n\t}\n\t_, _, err = getCustomAuthSftpClient(u, authMethods, \"\")\n\tassert.Error(t, err)\n\n\t// public key only auth must fail\n\t_, _, err = getSftpClient(u, true)\n\tassert.Error(t, err)\n\t// password only auth must fail\n\t_, _, err = getSftpClient(u, false)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(u, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(u.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthEmptyResponse(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1000\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\ttestFileSize := int64(65535)\n\t// the user will be created\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, len(user.PublicKeys))\n\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t// now modify the user\n\tuser.MaxSessions = 10\n\tuser.QuotaFiles = 100\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, true, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 10, user.MaxSessions)\n\tassert.Equal(t, 100, user.QuotaFiles)\n\n\t// the auth script accepts any password and returns an empty response, the\n\t// user password must be updated\n\tu.Password = defaultUsername\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthDifferentUsername(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\textAuthUsername := \"common_user\"\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1000\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, extAuthUsername), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\t// the user logins using \"defaultUsername\" and the external auth returns \"extAuthUsername\"\n\ttestFileSize := int64(65535)\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t// logins again to test that used quota is preserved\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = checkBasicSFTP(client)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.GetUserByUsername(extAuthUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, len(user.PublicKeys))\n\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginExternalAuth(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName := filepath.Base(mappedPath)\n\textAuthScopes := []int{1, 2}\n\tfor _, authScope := range extAuthScopes {\n\t\tvar usePubKey bool\n\t\tif authScope == 1 {\n\t\t\tusePubKey = false\n\t\t} else {\n\t\t\tusePubKey = true\n\t\t}\n\t\tu := getTestUser(usePubKey)\n\t\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\t\tName: folderName,\n\t\t\t},\n\t\t\tVirtualPath: \"/vpath\",\n\t\t\tQuotaFiles:  1 + authScope,\n\t\t\tQuotaSize:   10 + int64(authScope),\n\t\t})\n\n\t\terr := dataprovider.Close()\n\t\tassert.NoError(t, err)\n\t\terr = config.LoadConfig(configDir, \"\")\n\t\tassert.NoError(t, err)\n\t\tproviderConf := config.GetProviderConf()\n\t\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tproviderConf.ExternalAuthHook = extAuthPath\n\t\tproviderConf.ExternalAuthScope = authScope\n\t\terr = dataprovider.Initialize(providerConf, configDir, true)\n\t\tassert.NoError(t, err)\n\n\t\tf := vfs.BaseVirtualFolder{\n\t\t\tName:       folderName,\n\t\t\tMappedPath: mappedPath,\n\t\t}\n\t\t_, _, err = httpdtest.AddFolder(f, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\n\t\tconn, client, err := getSftpClient(u, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\t}\n\t\tif !usePubKey {\n\t\t\tdbUser, err := dataprovider.UserExists(defaultUsername, \"\")\n\t\t\tassert.NoError(t, err)\n\t\t\tfound, match := dataprovider.CheckCachedUserPassword(defaultUsername, defaultPassword, dbUser.Password)\n\t\t\tassert.True(t, found)\n\t\t\tassert.True(t, match)\n\t\t}\n\t\tu.Username = defaultUsername + \"1\"\n\t\tconn, client, err = getSftpClient(u, usePubKey)\n\t\tif !assert.Error(t, err, \"external auth login with invalid user must fail\") {\n\t\t\tclient.Close()\n\t\t\tconn.Close()\n\t\t}\n\t\tusePubKey = !usePubKey\n\t\tu = getTestUser(usePubKey)\n\t\tconn, client, err = getSftpClient(u, usePubKey)\n\t\tif !assert.Error(t, err, \"external auth login with valid user but invalid auth scope must fail\") {\n\t\t\tclient.Close()\n\t\t\tconn.Close()\n\t\t}\n\t\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tif assert.Len(t, user.VirtualFolders, 1) {\n\t\t\tfolder := user.VirtualFolders[0]\n\t\t\tassert.Equal(t, folderName, folder.Name)\n\t\t\tassert.Equal(t, mappedPath, folder.MappedPath)\n\t\t\tassert.Equal(t, 1+authScope, folder.QuotaFiles)\n\t\t\tassert.Equal(t, 10+int64(authScope), folder.QuotaSize)\n\t\t}\n\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\n\t\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = dataprovider.Close()\n\t\tassert.NoError(t, err)\n\t\terr = config.LoadConfig(configDir, \"\")\n\t\tassert.NoError(t, err)\n\t\tproviderConf = config.GetProviderConf()\n\t\terr = dataprovider.Initialize(providerConf, configDir, true)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(extAuthPath)\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestLoginExternalAuthCache(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser(false)\n\tu.Filters.ExternalAuthCacheTime = 120\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 1\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(u, false)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tlastLogin := user.LastLogin\n\tassert.Greater(t, lastLogin, int64(0))\n\tassert.Equal(t, u.Filters.ExternalAuthCacheTime, user.Filters.ExternalAuthCacheTime)\n\t// the auth should be now cached so update the hook to return an error\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, true, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(u, false)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, lastLogin, user.LastLogin)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginExternalAuthInteractive(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 4\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{\"1\", \"2\"}, 0, false, 1), os.ModePerm)\n\tassert.NoError(t, err)\n\tconn, client, err := getKeyboardInteractiveSftpClient(u, []string{\"1\", \"2\"})\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tu.Username = defaultUsername + \"1\"\n\tconn, client, err = getKeyboardInteractiveSftpClient(u, []string{\"1\", \"2\"})\n\tif !assert.Error(t, err, \"external auth login with invalid user must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tusePubKey = true\n\tu = getTestUser(usePubKey)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err, \"external auth login with valid user but invalid auth scope must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginExternalAuthErrors(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, true, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err, \"login must fail, external auth returns a non json response\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\n\tusePubKey = false\n\tu = getTestUser(usePubKey)\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif !assert.Error(t, err, \"login must fail, external auth returns a non json response\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthReturningAnonymousUser(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Filters.IsAnonymous = true\n\tu.Password = \"\"\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t_, _, err = getSftpClient(u, usePubKey)\n\tassert.Error(t, err)\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.IsAnonymous)\n\tassert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions[\"/\"])\n\tassert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)\n\tassert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,\n\t\tdataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)\n\n\t// test again, the user now exists\n\t_, _, err = getSftpClient(u, usePubKey)\n\tassert.Error(t, err)\n\tupdatedUser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tuser.UpdatedAt = updatedUser.UpdatedAt\n\tuser.LastPasswordChange = updatedUser.LastPasswordChange\n\tassert.Equal(t, user, updatedUser)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthPreserveMFAConfig(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\t// add multi-factor authentication\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.RecoveryCodes, 0)\n\tassert.False(t, user.Filters.TOTPConfig.Enabled)\n\tconfigName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)\n\tassert.NoError(t, err)\n\tuser.Password = defaultPassword\n\tuser.Filters.TOTPConfig = dataprovider.UserTOTPConfig{\n\t\tEnabled:    true,\n\t\tConfigName: configName,\n\t\tSecret:     kms.NewPlainSecret(key.Secret()),\n\t\tProtocols:  []string{common.ProtocolSSH},\n\t}\n\tfor i := 0; i < 12; i++ {\n\t\tuser.Filters.RecoveryCodes = append(user.Filters.RecoveryCodes, dataprovider.RecoveryCode{\n\t\t\tSecret: kms.NewPlainSecret(fmt.Sprintf(\"RC-%v\", strings.ToUpper(util.GenerateUniqueID()))),\n\t\t})\n\t}\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// login again and check that the MFA configs are preserved\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.RecoveryCodes, 12)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, configName, user.Filters.TOTPConfig.ConfigName)\n\tassert.Equal(t, []string{common.ProtocolSSH}, user.Filters.TOTPConfig.Protocols)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())\n\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, true, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tconn, client, err = getSftpClient(u, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Len(t, user.Filters.RecoveryCodes, 12)\n\tassert.True(t, user.Filters.TOTPConfig.Enabled)\n\tassert.Equal(t, configName, user.Filters.TOTPConfig.ConfigName)\n\tassert.Equal(t, []string{common.ProtocolSSH}, user.Filters.TOTPConfig.Protocols)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.Filters.TOTPConfig.Secret.GetStatus())\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaDisabledError(t *testing.T) {\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\tproviderConf.TrackQuota = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName+\"1\", testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName+\"1\", testFileName+\".rename\") //nolint:goconst\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\n//nolint:dupl\nfunc TestMaxConnections(t *testing.T) {\n\toldValue := common.Config.MaxTotalConnections\n\tcommon.Config.MaxTotalConnections = 1\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tusePubKey := true\n\tuser := getTestUser(usePubKey)\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser.Password = \"\"\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\ts, c, err := getSftpClient(user, usePubKey)\n\t\tif !assert.Error(t, err, \"max total connections exceeded, new login should not succeed\") {\n\t\t\tc.Close()\n\t\t\ts.Close()\n\t\t}\n\t\terr = client.Close()\n\t\tassert.NoError(t, err)\n\t\terr = conn.Close()\n\t\tassert.NoError(t, err)\n\t}\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.MaxTotalConnections = oldValue\n}\n\n//nolint:dupl\nfunc TestMaxPerHostConnections(t *testing.T) {\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 1\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tusePubKey := true\n\tuser := getTestUser(usePubKey)\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser.Password = \"\"\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\ts, c, err := getSftpClient(user, usePubKey)\n\t\tif !assert.Error(t, err, \"max per host connections exceeded, new login should not succeed\") {\n\t\t\tc.Close()\n\t\t\ts.Close()\n\t\t}\n\t\terr = client.Close()\n\t\tassert.NoError(t, err)\n\t\terr = conn.Close()\n\t\tassert.NoError(t, err)\n\t}\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\nfunc TestMaxTransfers(t *testing.T) {\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 2\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tusePubKey := true\n\tuser := getTestUser(usePubKey)\n\terr := dataprovider.AddUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser.Password = \"\"\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\n\t\tf1, err := client.Create(\"file1\")\n\t\tassert.NoError(t, err)\n\t\tf2, err := client.Create(\"file2\")\n\t\tassert.NoError(t, err)\n\t\t_, err = f1.Write([]byte(\" \"))\n\t\tassert.NoError(t, err)\n\t\t_, err = f2.Write([]byte(\" \"))\n\t\tassert.NoError(t, err)\n\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.ErrorContains(t, err, sftp.ErrSSHFxPermissionDenied.Error())\n\n\t\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\t\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\t\tassert.Error(t, err)\n\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.ErrorContains(t, err, sftp.ErrSSHFxPermissionDenied.Error())\n\n\t\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\t\terr = scpDownload(localDownloadPath, remoteDownPath, false, false)\n\t\tassert.Error(t, err)\n\n\t\terr = f1.Close()\n\t\tassert.NoError(t, err)\n\t\terr = f2.Close()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\terr = client.Close()\n\t\tassert.NoError(t, err)\n\t\terr = conn.Close()\n\t\tassert.NoError(t, err)\n\t}\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetTotalTransfers() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\nfunc TestMaxSessions(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Username += \"1\"\n\tu.MaxSessions = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\ts, c, err := getSftpClient(user, usePubKey)\n\t\tif !assert.Error(t, err, \"max sessions exceeded, new login should not succeed\") {\n\t\t\tc.Close()\n\t\t\ts.Close()\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSupportedExtensions(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\tv, ok := client.HasExtension(\"statvfs@openssh.com\")\n\t\tassert.Equal(t, \"2\", v)\n\t\tassert.True(t, ok)\n\t\t_, ok = client.HasExtension(\"hardlink@openssh.com\")\n\t\tassert.False(t, ok)\n\t\t_, ok = client.HasExtension(\"posix-rename@openssh.com\")\n\t\tassert.False(t, ok)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaFileReplace(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1000\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.QuotaFiles = 1000\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(65535)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) { //nolint:dupl\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\texpectedQuotaSize := testFileSize\n\t\t\texpectedQuotaFiles := 1\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\t// now replace the same file, the quota must not change\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t// now create a symlink, replace it with a file and check the quota\n\t\t\t// replacing a symlink is like uploading a new file\n\t\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\texpectedQuotaFiles++\n\t\t\texpectedQuotaSize += testFileSize\n\t\t\terr = sftpUploadFile(testFilePath, testFileName+\".link\", testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t}\n\t\t// now set a quota size restriction and upload the same file, upload should fail for space limit exceeded\n\t\tuser.QuotaSize = testFileSize*2 - 1\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tconn, client, err = getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.Error(t, err, \"quota size exceeded, file upload must fail\")\n\t\t\terr = client.Remove(testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\tuser.QuotaSize = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRename(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1000\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.QuotaFiles = 1000\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(65535)\n\ttestFileSize1 := int64(65537)\n\ttestFileName1 := \"test_file1.dat\" //nolint:goconst\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\t\terr = client.Rename(testFileName1, testFileName+\".rename\")\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\t\terr = client.Symlink(testFileName+\".rename\", testFileName+\".symlink\") //nolint:goconst\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\t// overwrite a symlink\n\t\t\terr = client.Rename(testFileName, testFileName+\".symlink\")\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Mkdir(\"testdir\")\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(\"testdir\", \"testdir1\")\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Mkdir(\"testdir\")\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(\"testdir\", \"testdir1\")\n\t\t\tassert.Error(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\t\ttestDir := \"tdir\"\n\t\t\terr = client.Mkdir(testDir)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, path.Join(testDir, testFileName), testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath1, path.Join(testDir, testFileName1), testFileSize1, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 4, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, testFileSize*2+testFileSize1*2, user.UsedQuotaSize)\n\t\t\terr = client.Rename(testDir, testDir+\"1\")\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 4, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, testFileSize*2+testFileSize1*2, user.UsedQuotaSize)\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaScan(t *testing.T) {\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(65535)\n\texpectedQuotaSize := user.UsedQuotaSize + testFileSize\n\texpectedQuotaFiles := user.UsedQuotaFiles + 1\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t// create user with the same home dir, so there is at least an untracked file\n\tuser, _, err = httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\tscans, _, err := httpdtest.GetQuotaScans(http.StatusOK)\n\t\tif err == nil {\n\t\t\treturn len(scans) == 0\n\t\t}\n\t\treturn false\n\t}, 1*time.Second, 50*time.Millisecond)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMultipleQuotaScans(t *testing.T) {\n\tres := common.QuotaScans.AddUserQuotaScan(defaultUsername, \"\")\n\tassert.True(t, res)\n\tres = common.QuotaScans.AddUserQuotaScan(defaultUsername, \"\")\n\tassert.False(t, res, \"add quota must fail if another scan is already active\")\n\tassert.True(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername))\n\tactiveScans := common.QuotaScans.GetUsersQuotaScans(\"\")\n\tassert.Equal(t, 0, len(activeScans))\n\tassert.False(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername))\n}\n\nfunc TestQuotaLimits(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.QuotaFiles = 1\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(65535)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\ttestFileSize1 := int64(131072)\n\ttestFileName1 := \"test_file1.dat\"\n\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\terr = createTestFile(testFilePath1, testFileSize1)\n\tassert.NoError(t, err)\n\ttestFileSize2 := int64(32768)\n\ttestFileName2 := \"test_file2.dat\" //nolint:goconst\n\ttestFilePath2 := filepath.Join(homeBasePath, testFileName2)\n\terr = createTestFile(testFilePath2, testFileSize2)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\t// test quota files\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = sftpUploadFile(testFilePath, testFileName+\".quota\", testFileSize, client) //nolint:goconst\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName+\".quota.1\", testFileSize, client)\n\t\t\tif assert.Error(t, err, \"user is over quota files, upload must fail\") {\n\t\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_FAILURE\")\n\t\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t\t}\n\t\t\t// rename should work\n\t\t\terr = client.Rename(testFileName+\".quota\", testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\t// test quota size\n\t\tuser.QuotaSize = testFileSize - 1\n\t\tuser.QuotaFiles = 0\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tconn, client, err = getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = sftpUploadFile(testFilePath, testFileName+\".quota.1\", testFileSize, client)\n\t\t\tif assert.Error(t, err, \"user is over quota size, upload must fail\") {\n\t\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_FAILURE\")\n\t\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t\t}\n\t\t\terr = client.Rename(testFileName, testFileName+\".quota\")\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(testFileName+\".quota\", testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\t// now test quota limits while uploading the current file, we have 1 bytes remaining\n\t\tuser.QuotaSize = testFileSize + 1\n\t\tuser.QuotaFiles = 0\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tconn, client, err = getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Contains(t, err.Error(), \"SSH_FX_FAILURE\")\n\t\t\t\tif user.Username == localUser.Username {\n\t\t\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t\t_, err = client.Stat(testFileName1)\n\t\t\tassert.Error(t, err)\n\t\t\t_, err = client.Lstat(testFileName1)\n\t\t\tassert.Error(t, err)\n\t\t\t// overwriting an existing file will work if the resulting size is lesser or equal than the current one\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath2, testFileName, testFileSize2, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath1, testFileName, testFileSize1, client)\n\t\t\tassert.Error(t, err)\n\t\t\t_, err := client.Stat(testFileName)\n\t\t\tassert.Error(t, err)\n\t\t}\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath1)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath2)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestTransferQuotaLimits(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.DownloadDataTransfer = 1\n\tu.UploadDataTransfer = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(550000)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// error while download is active\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error())\n\t\t}\n\t\t// error before starting the download\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error())\n\t\t}\n\t\t// error while upload is active\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t}\n\t\t// error before starting the upload\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t}\n\t}\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, user.UsedDownloadDataTransfer, int64(1024*1024))\n\tassert.Greater(t, user.UsedUploadDataTransfer, int64(1024*1024))\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadMaxSize(t *testing.T) {\n\ttestFileSize := int64(65535)\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Filters.MaxUploadFileSize = testFileSize + 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\ttestFileSize1 := int64(131072)\n\ttestFileName1 := \"test_file1.dat\"\n\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\terr = createTestFile(testFilePath1, testFileSize1)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)\n\t\tassert.Error(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// now test overwrite an existing file with a size bigger than the allowed one\n\t\terr = createTestFile(filepath.Join(user.GetHomeDir(), testFileName1), testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)\n\t\tassert.Error(t, err)\n\t}\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath1)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestBandwidthAndConnections(t *testing.T) {\n\tusePubKey := false\n\ttestFileSize := int64(524288)\n\tu := getTestUser(usePubKey)\n\tu.UploadBandwidth = 120\n\tu.DownloadBandwidth = 100\n\twantedUploadElapsed := 1000 * (testFileSize / 1024) / u.UploadBandwidth\n\twantedDownloadElapsed := 1000 * (testFileSize / 1024) / u.DownloadBandwidth\n\t// 100 ms tolerance\n\twantedUploadElapsed -= 100\n\twantedDownloadElapsed -= 100\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tstartTime := time.Now()\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\telapsed := time.Since(startTime).Nanoseconds() / 1000000\n\t\tassert.GreaterOrEqual(t, elapsed, wantedUploadElapsed, \"upload bandwidth throttling not respected\")\n\t\tstartTime = time.Now()\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\tc := sftpDownloadNonBlocking(testFileName, localDownloadPath, testFileSize, client)\n\t\twaitForActiveTransfers(t)\n\t\t// wait some additional arbitrary time to wait for transfer activity to happen\n\t\t// it is need to reach all the code in CheckIdleConnections\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\terr = <-c\n\t\tassert.NoError(t, err)\n\t\telapsed = time.Since(startTime).Nanoseconds() / 1000000\n\t\tassert.GreaterOrEqual(t, elapsed, wantedDownloadElapsed, \"download bandwidth throttling not respected\")\n\t\t// test disconnection\n\t\tc = sftpUploadNonBlocking(testFilePath, testFileName+\"_partial\", testFileSize, client)\n\t\twaitForActiveTransfers(t)\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\t\tcommon.Connections.Close(stat.ConnectionID, \"\")\n\t\t}\n\t\terr = <-c\n\t\tassert.Error(t, err, \"connection closed while uploading: the upload must fail\")\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn len(common.Connections.GetStats(\"\")) == 0\n\t\t}, 10*time.Second, 200*time.Millisecond)\n\t\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPatternsFilters(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(131072)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName+\".zip\", testFileSize, client)\n\t\tassert.NoError(t, err)\n\t}\n\tuser.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/\",\n\t\t\tAllowedPatterns: []string{\"*.zIp\"},\n\t\t\tDeniedPatterns:  []string{},\n\t\t},\n\t}\n\tuser.Filters.DisableFsChecks = true\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.Error(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.Error(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\"1\")\n\t\tassert.Error(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.Error(t, err)\n\t\terr = sftpDownloadFile(testFileName+\".zip\", localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(\"dir.zip\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"dir.zip\", \"dir1.zip\")\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFolders(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tfolderName := filepath.Base(mappedPath)\n\tvdirPath := \"/vdir/subdir\"\n\ttestDir := \"/userDir\"\n\ttestDir1 := \"/userDir1\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t})\n\tu.Permissions[testDir] = []string{dataprovider.PermCreateDirs}\n\tu.Permissions[testDir1] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermRename}\n\tu.Permissions[path.Join(testDir1, \"subdir\")] = []string{dataprovider.PermRename}\n\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\t// check virtual folder auto creation\n\t\t_, err = os.Stat(mappedPath)\n\t\tassert.NoError(t, err)\n\t\ttestFileSize := int64(131072)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(vdirPath, \"new_name\")\n\t\tassert.Error(t, err, \"renaming a virtual folder must fail\")\n\t\terr = client.RemoveDirectory(vdirPath)\n\t\tassert.Error(t, err, \"removing a virtual folder must fail\")\n\t\terr = client.Mkdir(vdirPath)\n\t\tassert.Error(t, err, \"creating a virtual folder must fail\")\n\t\terr = client.Symlink(path.Join(vdirPath, testFileName), vdirPath)\n\t\tassert.Error(t, err, \"symlink to a virtual folder must fail\")\n\t\terr = client.Rename(\"/vdir\", \"/vdir1\")\n\t\tassert.Error(t, err, \"renaming a directory with a virtual folder inside must fail\")\n\t\terr = client.RemoveDirectory(\"/vdir\")\n\t\tassert.Error(t, err, \"removing a directory with a virtual folder inside must fail\")\n\t\terr = client.Mkdir(\"vdir1\")\n\t\tassert.NoError(t, err)\n\t\t// rename empty dir /vdir1, we have permission on /\n\t\terr = client.Rename(\"vdir1\", \"vdir2\")\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(\"vdir2\", testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// we don't have rename permission in testDir and vdir2 contains a file\n\t\terr = client.Rename(\"vdir2\", testDir)\n\t\tassert.Error(t, err)\n\t\terr = client.Rename(\"vdir2\", testDir1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testDir1, \"vdir2\")\n\t\tassert.NoError(t, err)\n\t\terr = client.MkdirAll(path.Join(\"vdir2\", \"subdir\"))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(\"vdir2\", \"subdir\", testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"vdir2\", testDir1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testDir1, \"vdir2\")\n\t\tassert.NoError(t, err)\n\t\terr = client.MkdirAll(path.Join(\"vdir2\", \"subdir\", \"subdir\"))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(\"vdir2\", \"subdir\", \"subdir\", testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"vdir2\", testDir1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testDir1, \"vdir3\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(path.Join(\"vdir3\", \"subdir\", \"subdir\", testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(path.Join(\"vdir3\", \"subdir\", \"subdir\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"vdir3\", testDir1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testDir1, \"vdir2\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(\"vdir2\", \"subdir\", testFileName), path.Join(\"vdir2\", \"subdir\", \"alink\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"vdir2\", testDir1)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFoldersQuotaLimit(t *testing.T) {\n\tusePubKey := false\n\tu1 := getTestUser(usePubKey)\n\tu1.QuotaFiles = 1\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\" //nolint:goconst\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\" //nolint:goconst\n\tu1.VirtualFolders = append(u1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tu1.VirtualFolders = append(u1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  1,\n\t\tQuotaSize:   0,\n\t})\n\ttestFileSize := int64(131072)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\terr := createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tu2 := getTestUser(usePubKey)\n\tu2.QuotaSize = testFileSize + 1\n\tu2.VirtualFolders = append(u2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tu2.VirtualFolders = append(u2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  0,\n\t\tQuotaSize:   testFileSize + 1,\n\t})\n\tusers := []dataprovider.User{u1, u2}\n\tfor _, u := range users {\n\t\terr = os.MkdirAll(mappedPath1, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tf1 := vfs.BaseVirtualFolder{\n\t\t\tName:       folderName1,\n\t\t\tMappedPath: mappedPath1,\n\t\t}\n\t\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\t\tf2 := vfs.BaseVirtualFolder{\n\t\t\tName:       folderName2,\n\t\t\tMappedPath: mappedPath2,\n\t\t}\n\t\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.Error(t, err)\n\t\t\t_, err = client.Stat(testFileName)\n\t\t\tassert.Error(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName+\"1\"), testFileSize, client)\n\t\t\tassert.Error(t, err)\n\t\t\t_, err = client.Stat(path.Join(vdirPath1, testFileName+\"1\"))\n\t\t\tassert.Error(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName+\"1\"), testFileSize, client)\n\t\t\tassert.Error(t, err)\n\t\t\t_, err = client.Stat(path.Join(vdirPath2, testFileName+\"1\"))\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Remove(path.Join(vdirPath1, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\t\tassert.Error(t, err)\n\t\t\t// now test renames\n\t\t\terr = client.Rename(testFileName, path.Join(vdirPath1, testFileName))\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(path.Join(vdirPath1, testFileName), path.Join(vdirPath1, testFileName+\".rename\"))\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(path.Join(vdirPath2, testFileName), path.Join(vdirPath2, testFileName+\".rename\"))\n\t\t\tassert.NoError(t, err)\n\t\t\terr = client.Rename(path.Join(vdirPath2, testFileName+\".rename\"), testFileName+\".rename\")\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Rename(path.Join(vdirPath2, testFileName+\".rename\"), path.Join(vdirPath1, testFileName))\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Rename(path.Join(vdirPath1, testFileName+\".rename\"), path.Join(vdirPath2, testFileName))\n\t\t\tassert.Error(t, err)\n\t\t\terr = client.Rename(path.Join(vdirPath1, testFileName+\".rename\"), testFileName)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(mappedPath1)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(mappedPath2)\n\t\tassert.NoError(t, err)\n\t}\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPLoopSimple(t *testing.T) {\n\tusePubKey := false\n\tuser1 := getTestSFTPUser(usePubKey)\n\tuser2 := getTestSFTPUser(usePubKey)\n\tuser1.Username += \"1\"\n\tuser2.Username += \"2\"\n\tuser1.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser2.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser1.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: sftpServerAddr,\n\t\t\tUsername: user2.Username,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t}\n\tuser2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: sftpServerAddr,\n\t\t\tUsername: user1.Username,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t}\n\tuser1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tuser2, resp, err = httpdtest.AddUser(user2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\t_, _, err = getSftpClient(user1, usePubKey)\n\tassert.Error(t, err)\n\t_, _, err = getSftpClient(user2, usePubKey)\n\tassert.Error(t, err)\n\n\tuser1.FsConfig.SFTPConfig.Username = user1.Username\n\tuser1.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\n\t_, _, err = httpdtest.UpdateUser(user1, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, _, err = getSftpClient(user1, usePubKey)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user2.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPLoopVirtualFolders(t *testing.T) {\n\tusePubKey := false\n\tsftpFloderName := \"sftp\"\n\tuser1 := getTestUser(usePubKey)\n\tuser2 := getTestSFTPUser(usePubKey)\n\tuser3 := getTestSFTPUser(usePubKey)\n\tuser1.Username += \"1\"\n\tuser2.Username += \"2\"\n\tuser3.Username += \"3\"\n\n\t// user1 is a local account with a virtual SFTP folder to user2\n\t// user2 has user1 as SFTP fs\n\tuser1.VirtualFolders = append(user1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: sftpFloderName,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t})\n\n\tuser2.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: sftpServerAddr,\n\t\t\tUsername: user1.Username,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t}\n\tuser3.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser3.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: sftpServerAddr,\n\t\t\tUsername: user1.Username,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t}\n\tf := vfs.BaseVirtualFolder{\n\t\tName: sftpFloderName,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint:          sftpServerAddr,\n\t\t\t\t\tUsername:          user2.Username,\n\t\t\t\t\tEqualityCheckMode: 1,\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tuser2, resp, err = httpdtest.AddUser(user2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tuser3, resp, err = httpdtest.AddUser(user3, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\t// login will work but /vdir will not be accessible\n\tconn, client, err := getSftpClient(user1, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\t_, err = client.ReadDir(\"/vdir\")\n\t\tassert.Error(t, err)\n\t}\n\t// now make user2 a local account with an SFTP virtual folder to user1.\n\t// So we have:\n\t// user1 -> local account with the SFTP virtual folder /vdir to user2\n\t// user2 -> local account with the SFTP virtual folder /vdir2 to user3\n\t// user3 -> sftp user with user1 as fs\n\tuser2.FsConfig.Provider = sdk.LocalFilesystemProvider\n\tuser2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}\n\tuser2.VirtualFolders = append(user2.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: sftpFloderName,\n\t\t\tFsConfig: vfs.Filesystem{\n\t\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\t\tUsername: user3.Username,\n\t\t\t\t\t},\n\t\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tVirtualPath: \"/vdir2\",\n\t})\n\tuser2, _, err = httpdtest.UpdateUser(user2, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\t// login will work but /vdir will not be accessible\n\tconn, client, err = getSftpClient(user1, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\t_, err = client.ReadDir(\"/vdir\")\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user2.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user3, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user3.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: sftpFloderName}, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestNestedVirtualFolders(t *testing.T) {\n\tusePubKey := true\n\tbaseUser, resp, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tu := getTestSFTPUser(usePubKey)\n\tu.QuotaFiles = 1000\n\tmappedPathCrypt := filepath.Join(os.TempDir(), \"crypt\")\n\tfolderNameCrypt := filepath.Base(mappedPathCrypt)\n\tvdirCryptPath := \"/vdir/crypt\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameCrypt,\n\t\t},\n\t\tVirtualPath: vdirCryptPath,\n\t\tQuotaFiles:  100,\n\t})\n\tmappedPath := filepath.Join(os.TempDir(), \"local\")\n\tfolderName := filepath.Base(mappedPath)\n\tvdirPath := \"/vdir/local\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tmappedPathNested := filepath.Join(os.TempDir(), \"nested\")\n\tfolderNameNested := filepath.Base(mappedPathNested)\n\tvdirNestedPath := \"/vdir/crypt/nested\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameNested,\n\t\t},\n\t\tVirtualPath: vdirNestedPath,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName: folderNameCrypt,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t\tMappedPath: mappedPathCrypt,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folderNameNested,\n\t\tMappedPath: mappedPathNested,\n\t}\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\texpectedQuotaSize := int64(0)\n\t\texpectedQuotaFiles := 0\n\t\tfileSize := int64(32765)\n\t\terr = writeSFTPFile(testFileName, fileSize, client)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\tfileSize = 38764\n\t\terr = writeSFTPFile(path.Join(\"/vdir\", testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\tfileSize = 18769\n\t\terr = writeSFTPFile(path.Join(vdirPath, testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\tfileSize = 27658\n\t\terr = writeSFTPFile(path.Join(vdirNestedPath, testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\tfileSize = 39765\n\t\terr = writeSFTPFile(path.Join(vdirCryptPath, testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\n\t\tuserGet, _, err := httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, userGet.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, userGet.UsedQuotaSize)\n\n\t\tfolderGet, _, err := httpdtest.GetFolderByName(folderNameCrypt, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, folderGet.UsedQuotaSize, fileSize)\n\t\tassert.Equal(t, 1, folderGet.UsedQuotaFiles)\n\n\t\tfolderGet, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), folderGet.UsedQuotaSize)\n\t\tassert.Equal(t, 0, folderGet.UsedQuotaFiles)\n\n\t\tfolderGet, _, err = httpdtest.GetFolderByName(folderNameNested, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), folderGet.UsedQuotaSize)\n\t\tassert.Equal(t, 0, folderGet.UsedQuotaFiles)\n\n\t\tfiles, err := client.ReadDir(\"/\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Len(t, files, 2)\n\t\t}\n\t\tinfo, err := client.Stat(\"vdir\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.IsDir())\n\t\t}\n\t\tfiles, err = client.ReadDir(\"/vdir\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Len(t, files, 3)\n\t\t}\n\t\tfiles, err = client.ReadDir(vdirCryptPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Len(t, files, 2)\n\t\t}\n\t\tinfo, err = client.Stat(vdirNestedPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.IsDir())\n\t\t}\n\t\t// finally add some files directly using os method and then check quota\n\t\tfName := \"testfile\"\n\t\tfileSize = 123456\n\t\terr = createTestFile(filepath.Join(baseUser.HomeDir, fName), fileSize)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\tfileSize = 8765\n\t\terr = createTestFile(filepath.Join(mappedPath, fName), fileSize)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\tfileSize = 98751\n\t\terr = createTestFile(filepath.Join(mappedPathNested, fName), fileSize)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\terr = createTestFile(filepath.Join(mappedPathCrypt, fName), fileSize)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\tscans, _, err := httpdtest.GetQuotaScans(http.StatusOK)\n\t\t\tif err == nil {\n\t\t\t\treturn len(scans) == 0\n\t\t\t}\n\t\t\treturn false\n\t\t}, 1*time.Second, 50*time.Millisecond)\n\n\t\tuserGet, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, userGet.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, userGet.UsedQuotaSize)\n\n\t\t// the crypt folder is not included within user quota so we need to do a separate scan\n\t\t_, err = httpdtest.StartFolderQuotaScan(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusAccepted)\n\t\tassert.NoError(t, err)\n\t\tassert.Eventually(t, func() bool {\n\t\t\tscans, _, err := httpdtest.GetFoldersQuotaScans(http.StatusOK)\n\t\t\tif err == nil {\n\t\t\t\treturn len(scans) == 0\n\t\t\t}\n\t\t\treturn false\n\t\t}, 1*time.Second, 50*time.Millisecond)\n\t\tfolderGet, _, err = httpdtest.GetFolderByName(folderNameCrypt, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, folderGet.UsedQuotaSize, int64(39765+98751))\n\t\tassert.Equal(t, 2, folderGet.UsedQuotaFiles)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameNested}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathCrypt)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathNested)\n\tassert.NoError(t, err)\n}\n\nfunc TestBufferedUser(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1000\n\tu.FsConfig.OSConfig = sdk.OSFsConfig{\n\t\tWriteBufferSize: 2,\n\t\tReadBufferSize:  1,\n\t}\n\tvdirPath := \"/crypted\"\n\tmappedPath := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tfolderName := filepath.Base(mappedPath)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tOSFsConfig: sdk.OSFsConfig{\n\t\t\t\t\tWriteBufferSize: 3,\n\t\t\t\t\tReadBufferSize:  2,\n\t\t\t\t},\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\texpectedQuotaSize := int64(0)\n\t\texpectedQuotaFiles := 0\n\t\tfileSize := int64(32768)\n\t\terr = writeSFTPFile(testFileName, fileSize, client)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\terr = writeSFTPFile(path.Join(vdirPath, testFileName), fileSize, client)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaSize += fileSize\n\t\texpectedQuotaFiles++\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Greater(t, user.UsedQuotaSize, expectedQuotaSize)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(path.Join(vdirPath, testFileName), localDownloadPath, fileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(path.Join(vdirPath, testFileName))\n\t\tassert.NoError(t, err)\n\n\t\tdata := []byte(\"test data\")\n\t\tf, err := client.OpenFile(testFileName, os.O_WRONLY|os.O_CREATE)\n\t\tif assert.NoError(t, err) {\n\t\t\tn, err := f.Write(data)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, len(data), n)\n\t\t\terr = f.Truncate(2)\n\t\t\tassert.NoError(t, err)\n\t\t\texpectedQuotaSize := int64(2)\n\t\t\texpectedQuotaFiles := 0\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t_, err = f.Seek(expectedQuotaSize, io.SeekStart)\n\t\t\tassert.NoError(t, err)\n\t\t\tn, err = f.Write(data)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, len(data), n)\n\t\t\terr = f.Truncate(5)\n\t\t\tassert.NoError(t, err)\n\t\t\texpectedQuotaSize = int64(5)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t_, err = f.Seek(expectedQuotaSize, io.SeekStart)\n\t\t\tassert.NoError(t, err)\n\t\t\tn, err = f.Write(data)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, len(data), n)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\texpectedQuotaSize = int64(5) + int64(len(data))\n\t\t\texpectedQuotaFiles = 1\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t}\n\t\t// now truncate by path\n\t\terr = client.Truncate(testFileName, 5)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(5), user.UsedQuotaSize)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestTruncateQuotaLimits(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaSize = 20\n\tmappedPath := filepath.Join(os.TempDir(), \"mapped\")\n\tfolderName := filepath.Base(mappedPath)\n\terr := os.MkdirAll(mappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tvdirPath := \"/vmapped\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t\tQuotaFiles:  10,\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err = httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.QuotaSize = 20\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\tdata := []byte(\"test data\")\n\t\t\tf, err := client.OpenFile(testFileName, os.O_WRONLY|os.O_CREATE)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tn, err := f.Write(data)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(data), n)\n\t\t\t\terr = f.Truncate(2)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\texpectedQuotaFiles := 0\n\t\t\t\texpectedQuotaSize := int64(2)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(expectedQuotaSize, io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tn, err = f.Write(data)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(data), n)\n\t\t\t\terr = f.Truncate(5)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\texpectedQuotaSize = int64(5)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(expectedQuotaSize, io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tn, err = f.Write(data)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(data), n)\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\texpectedQuotaFiles = 1\n\t\t\t\texpectedQuotaSize = int64(5) + int64(len(data))\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\t\t}\n\t\t\t// now truncate by path\n\t\t\terr = client.Truncate(testFileName, 5)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, int64(5), user.UsedQuotaSize)\n\t\t\t// now open an existing file without truncate it, quota should not change\n\t\t\tf, err = client.OpenFile(testFileName, os.O_WRONLY)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(5), user.UsedQuotaSize)\n\t\t\t}\n\t\t\t// open the file truncating it\n\t\t\tf, err = client.OpenFile(testFileName, os.O_WRONLY|os.O_TRUNC)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t\t\t}\n\t\t\t// now test max write size\n\t\t\tf, err = client.OpenFile(testFileName, os.O_WRONLY)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tn, err := f.Write(data)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(data), n)\n\t\t\t\terr = f.Truncate(11)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(11), user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(int64(11), io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tn, err = f.Write(data)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(data), n)\n\t\t\t\terr = f.Truncate(5)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(5), user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(int64(5), io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tn, err = f.Write(data)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, len(data), n)\n\t\t\t\terr = f.Truncate(12)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(12), user.UsedQuotaSize)\n\t\t\t\t_, err = f.Seek(int64(12), io.SeekStart)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = f.Write(data)\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.Contains(t, err.Error(), common.ErrQuotaExceeded.Error())\n\t\t\t\t}\n\t\t\t\terr = f.Close()\n\t\t\t\tassert.Error(t, err)\n\t\t\t\t// the file is deleted\n\t\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\t\t\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t\t\t}\n\n\t\t\tif user.Username == defaultUsername {\n\t\t\t\t// basic test inside a virtual folder\n\t\t\t\tvfileName := path.Join(vdirPath, testFileName)\n\t\t\t\tf, err = client.OpenFile(vfileName, os.O_WRONLY|os.O_CREATE)\n\t\t\t\tif assert.NoError(t, err) {\n\t\t\t\t\tn, err := f.Write(data)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, len(data), n)\n\t\t\t\t\terr = f.Truncate(2)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\texpectedQuotaFiles := 0\n\t\t\t\t\texpectedQuotaSize := int64(2)\n\t\t\t\t\tfold, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)\n\t\t\t\t\tassert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)\n\t\t\t\t\terr = f.Close()\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\texpectedQuotaFiles = 1\n\t\t\t\t\tfold, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)\n\t\t\t\t\tassert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)\n\t\t\t\t}\n\t\t\t\terr = client.Truncate(vfileName, 1)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tfold, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, int64(1), fold.UsedQuotaSize)\n\t\t\t\tassert.Equal(t, 1, fold.UsedQuotaFiles)\n\t\t\t\t// cleanup\n\t\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tuser.Password = defaultPassword\n\t\t\t\tuser.ID = 0\n\t\t\t\tuser.CreatedAt = 0\n\t\t\t\tuser.QuotaSize = 0\n\t\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\t\tassert.NoError(t, err, string(resp))\n\t\t\t}\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {\n\tusePubKey := true\n\ttestFileSize := int64(131072)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize1 := int64(65537)\n\ttestFileName1 := \"test_file1.dat\"\n\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\terr := createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = createTestFile(testFilePath1, testFileSize1)\n\tassert.NoError(t, err)\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 0\n\tu.QuotaSize = 0\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tmappedPath3 := filepath.Join(os.TempDir(), \"vdir3\")\n\tfolderName3 := filepath.Base(mappedPath3)\n\tvdirPath3 := \"/vdir3\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  2,\n\t\tQuotaSize:   0,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  0,\n\t\tQuotaSize:   testFileSize + testFileSize1 + 1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName3,\n\t\t},\n\t\tVirtualPath: vdirPath3,\n\t\tQuotaFiles:  2,\n\t\tQuotaSize:   testFileSize * 2,\n\t})\n\terr = os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath3, os.ModePerm)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folderName3,\n\t\tMappedPath: mappedPath3,\n\t}\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath3, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath3, testFileName+\"1\"), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, path.Join(vdirPath1, testFileName+\".rename\"))\n\t\tassert.Error(t, err)\n\t\t// we overwrite an existing file and we have unlimited size\n\t\terr = client.Rename(testFileName, path.Join(vdirPath1, testFileName))\n\t\tassert.NoError(t, err)\n\t\t// we have no space and we try to overwrite a bigger file with a smaller one, this should succeed\n\t\terr = client.Rename(testFileName1, path.Join(vdirPath2, testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// we have no space and we try to overwrite a smaller file with a bigger one, this should fail\n\t\terr = client.Rename(testFileName, path.Join(vdirPath2, testFileName1))\n\t\tassert.Error(t, err)\n\t\tfi, err := client.Stat(path.Join(vdirPath1, testFileName1))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize1, fi.Size())\n\t\t}\n\t\t// we are overquota inside vdir3 size 2/2 and size 262144/262144\n\t\terr = client.Rename(path.Join(vdirPath1, testFileName1), path.Join(vdirPath3, testFileName1+\".rename\"))\n\t\tassert.Error(t, err)\n\t\t// we overwrite an existing file and we have enough size\n\t\terr = client.Rename(path.Join(vdirPath1, testFileName1), path.Join(vdirPath3, testFileName))\n\t\tassert.NoError(t, err)\n\t\ttestFileName2 := \"test_file2.dat\"\n\t\ttestFilePath2 := filepath.Join(homeBasePath, testFileName2)\n\t\terr = createTestFile(testFilePath2, testFileSize+testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath2, testFileName2, testFileSize+testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// we overwrite an existing file and we haven't enough size\n\t\terr = client.Rename(testFileName2, path.Join(vdirPath3, testFileName))\n\t\tassert.Error(t, err)\n\t\terr = os.Remove(testFilePath2)\n\t\tassert.NoError(t, err)\n\t\t// now remove a file from vdir3, create a dir with 2 files and try to rename it in vdir3\n\t\t// this will fail since the rename will result in 3 files inside vdir3 and quota limits only\n\t\t// allow 2 total files there\n\t\terr = client.Remove(path.Join(vdirPath3, testFileName+\"1\"))\n\t\tassert.NoError(t, err)\n\t\taDir := \"a dir\"\n\t\terr = client.Mkdir(aDir)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(aDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(aDir, testFileName1+\"1\"), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(aDir, path.Join(vdirPath3, aDir))\n\t\tassert.Error(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName3}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath3)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath1)\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFoldersQuotaValues(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tvdirPath1 := \"/vdir1\" //nolint:goconst\n\tfolderName1 := filepath.Base(mappedPath1)\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tvdirPath2 := \"/vdir2\" //nolint:goconst\n\tfolderName2 := filepath.Base(mappedPath2)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\terr := os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(131072)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// we copy the same file two times to test quota update on file overwrite\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\texpectedQuotaFiles := 2\n\t\texpectedQuotaSize := testFileSize * 2\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\n\t\terr = client.Remove(path.Join(vdirPath1, testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(path.Join(vdirPath2, testFileName))\n\t\tassert.NoError(t, err)\n\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tvdirPath1 := \"/vdir1\"\n\tfolderName1 := filepath.Base(mappedPath1)\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tvdirPath2 := \"/vdir2\"\n\tfolderName2 := filepath.Base(mappedPath2)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65535)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\tdir1 := \"dir1\" //nolint:goconst\n\t\tdir2 := \"dir2\" //nolint:goconst\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath1, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\t// initial files:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\t//\n\t\t// rename a file inside vdir1 it is included inside user quota, so we have:\n\t\t// - vdir1/dir1/testFileName.rename\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(vdirPath1, dir1, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// rename a file inside vdir2, it isn't included inside user quota, so we have:\n\t\t// - vdir1/dir1/testFileName.rename\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName.rename\n\t\t// - vdir2/dir2/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(vdirPath2, dir1, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// rename a file inside vdir2 overwriting an existing, we now have:\n\t\t// - vdir1/dir1/testFileName.rename\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName.rename (initial testFileName1)\n\t\terr = client.Rename(path.Join(vdirPath2, dir2, testFileName1), path.Join(vdirPath2, dir1, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// rename a file inside vdir1 overwriting an existing, we now have:\n\t\t// - vdir1/dir1/testFileName.rename (initial testFileName1)\n\t\t// - vdir2/dir1/testFileName.rename (initial testFileName1)\n\t\terr = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(vdirPath1, dir1, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// rename a directory inside the same virtual folder, quota should not change\n\t\terr = client.RemoveDirectory(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirPath1, dir1), path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(path.Join(vdirPath2, dir1), path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameBetweenVirtualFolder(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\terr := os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65535)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\tdir1 := \"dir1\"\n\t\tdir2 := \"dir2\"\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath1, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// initial files:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\t//\n\t\t// rename a file from vdir1 to vdir2, vdir1 is included inside user quota, so we have:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName1.rename\n\t\terr = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(vdirPath2, dir1, testFileName1+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 3, f.UsedQuotaFiles)\n\t\t// rename a file from vdir2 to vdir1, vdir2 is not included inside user quota, so we have:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir2/testFileName.rename\n\t\t// - vdir2/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName1.rename\n\t\terr = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(vdirPath1, dir2, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*2, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1*2, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\t// rename a file from vdir1 to vdir2 overwriting an existing file, vdir1 is included inside user quota, so we have:\n\t\t// - vdir1/dir2/testFileName.rename\n\t\t// - vdir2/dir2/testFileName1 (is the initial testFileName)\n\t\t// - vdir2/dir1/testFileName1.rename\n\t\terr = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(vdirPath2, dir2, testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1+testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\t// rename a file from vdir2 to vdir1 overwriting an existing file, vdir2 is not included inside user quota, so we have:\n\t\t// - vdir1/dir2/testFileName.rename (is the initial testFileName1)\n\t\t// - vdir2/dir2/testFileName1 (is the initial testFileName)\n\t\terr = client.Rename(path.Join(vdirPath2, dir1, testFileName1+\".rename\"), path.Join(vdirPath1, dir2, testFileName+\".rename\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, dir2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, dir2, testFileName), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, dir2, testFileName+\"1.dupl\"), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\t// - vdir1/dir2/testFileName.rename (initial testFileName1)\n\t\t// - vdir1/dir2/testFileName\n\t\t// - vdir2/dir2/testFileName1 (initial testFileName)\n\t\t// - vdir2/dir2/testFileName (initial testFileName1)\n\t\t// - vdir2/dir2/testFileName1.dupl\n\t\t// rename directories between the two virtual folders\n\t\terr = client.Rename(path.Join(vdirPath2, dir2), path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 5, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1*3+testFileSize*2, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// now move on vpath2\n\t\terr = client.Rename(path.Join(vdirPath1, dir2), path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1*2+testFileSize, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameFromVirtualFolder(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\terr := os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65535)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\tdir1 := \"dir1\"\n\t\tdir2 := \"dir2\"\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath1, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, dir2, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// initial files:\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\t//\n\t\t// rename a file from vdir1 to the user home dir, vdir1 is included in user quota so we have:\n\t\t// - testFileName\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\t// - vdir2/dir2/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath1, dir1, testFileName), path.Join(testFileName))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\t\t// rename a file from vdir2 to the user home dir, vdir2 is not included in user quota so we have:\n\t\t// - testFileName\n\t\t// - testFileName1\n\t\t// - vdir1/dir2/testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\terr = client.Rename(path.Join(vdirPath2, dir2, testFileName1), path.Join(testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// rename a file from vdir1 to the user home dir overwriting an existing file, vdir1 is included in user quota so we have:\n\t\t// - testFileName (initial testFileName1)\n\t\t// - testFileName1\n\t\t// - vdir2/dir1/testFileName\n\t\terr = client.Rename(path.Join(vdirPath1, dir2, testFileName1), path.Join(testFileName))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// rename a file from vdir2 to the user home dir overwriting an existing file, vdir2 is not included in user quota so we have:\n\t\t// - testFileName (initial testFileName1)\n\t\t// - testFileName1 (initial testFileName)\n\t\terr = client.Rename(path.Join(vdirPath2, dir1, testFileName), path.Join(testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// dir rename\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath1, dir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, dir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// - testFileName (initial testFileName1)\n\t\t// - testFileName1 (initial testFileName)\n\t\t// - vdir1/dir1/testFileName\n\t\t// - vdir1/dir1/testFileName1\n\t\t// - dir1/testFileName\n\t\t// - dir1/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath2, dir1), dir1)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 6, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*3+testFileSize1*3, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// - testFileName (initial testFileName1)\n\t\t// - testFileName1 (initial testFileName)\n\t\t// - dir2/testFileName\n\t\t// - dir2/testFileName1\n\t\t// - dir1/testFileName\n\t\t// - dir1/testFileName1\n\t\terr = client.Rename(path.Join(vdirPath1, dir1), dir2)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 6, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*3+testFileSize1*3, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaRenameToVirtualFolder(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\terr := os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFileSize1 := int64(65535)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\tdir1 := \"dir1\"\n\t\tdir2 := \"dir2\"\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, dir2))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// initial files:\n\t\t// - testFileName\n\t\t// - testFileName1\n\t\t//\n\t\t// rename a file from user home dir to vdir1, vdir1 is included in user quota so we have:\n\t\t// - testFileName\n\t\t// - /vdir1/dir1/testFileName1\n\t\terr = client.Rename(testFileName1, path.Join(vdirPath1, dir1, testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t// rename a file from user home dir to vdir2, vdir2 is not included in user quota so we have:\n\t\t// - /vdir2/dir1/testFileName\n\t\t// - /vdir1/dir1/testFileName1\n\t\terr = client.Rename(testFileName, path.Join(vdirPath2, dir1, testFileName))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// upload two new files to the user home dir so we have:\n\t\t// - testFileName\n\t\t// - testFileName1\n\t\t// - /vdir1/dir1/testFileName1\n\t\t// - /vdir2/dir1/testFileName\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1+testFileSize1, user.UsedQuotaSize)\n\t\t// rename a file from user home dir to vdir1 overwriting an existing file, vdir1 is included in user quota so we have:\n\t\t// - testFileName1\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName\n\t\terr = client.Rename(testFileName, path.Join(vdirPath1, dir1, testFileName1))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// rename a file from user home dir to vdir2 overwriting an existing file, vdir2 is not included in user quota so we have:\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName (initial testFileName1)\n\t\terr = client.Rename(testFileName1, path.Join(vdirPath2, dir1, testFileName))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\n\t\terr = client.Mkdir(dir1)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(dir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// - /dir1/testFileName\n\t\t// - /dir1/testFileName1\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName (initial testFileName1)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\t// - /vdir1/adir/testFileName\n\t\t// - /vdir1/adir/testFileName1\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName (initial testFileName1)\n\t\terr = client.Rename(dir1, path.Join(vdirPath1, \"adir\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t\terr = client.Mkdir(dir1)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(dir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(dir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t// - /vdir1/adir/testFileName\n\t\t// - /vdir1/adir/testFileName1\n\t\t// - /vdir1/dir1/testFileName1 (initial testFileName)\n\t\t// - /vdir2/dir1/testFileName (initial testFileName1)\n\t\t// - /vdir2/adir/testFileName\n\t\t// - /vdir2/adir/testFileName1\n\t\terr = client.Rename(dir1, path.Join(vdirPath2, \"adir\"))\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize*2+testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize1*2+testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 3, f.UsedQuotaFiles)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFoldersLink(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\t// quota is unlimited and excluded from user's one\n\t\tQuotaFiles: 0,\n\t\tQuotaSize:  0,\n\t})\n\terr := os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(131072)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestDir := \"adir\"\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath1, testFileName), path.Join(vdirPath1, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath1, testFileName), path.Join(vdirPath1, testDir, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath2, testFileName), path.Join(vdirPath2, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath2, testFileName), path.Join(vdirPath2, testDir, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(vdirPath1, testFileName+\".link1\")) //nolint:goconst\n\t\tassert.Error(t, err)\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(vdirPath1, testDir, testFileName+\".link1\"))\n\t\tassert.Error(t, err)\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(vdirPath2, testFileName+\".link1\"))\n\t\tassert.Error(t, err)\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(vdirPath2, testDir, testFileName+\".link1\"))\n\t\tassert.Error(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath1, testFileName), testFileName+\".link1\")\n\t\tassert.Error(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath2, testFileName), testFileName+\".link1\")\n\t\tassert.Error(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath1, testFileName), path.Join(vdirPath2, testDir, testFileName+\".link1\"))\n\t\tassert.Error(t, err)\n\t\terr = client.Symlink(path.Join(vdirPath2, testFileName), path.Join(vdirPath1, testFileName+\".link1\"))\n\t\tassert.Error(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualFolderQuotaScan(t *testing.T) {\n\tmappedPath := filepath.Join(os.TempDir(), \"mapped_dir\")\n\tfolderName := filepath.Base(mappedPath)\n\terr := os.MkdirAll(mappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(65535)\n\ttestFilePath := filepath.Join(mappedPath, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\texpectedQuotaSize := testFileSize\n\texpectedQuotaFiles := 1\n\tfolder, _, err := httpdtest.AddFolder(vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}, http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.StartFolderQuotaScan(folder, http.StatusAccepted)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool {\n\t\tscans, _, err := httpdtest.GetFoldersQuotaScans(http.StatusOK)\n\t\tif err == nil {\n\t\t\treturn len(scans) == 0\n\t\t}\n\t\treturn false\n\t}, 1*time.Second, 50*time.Millisecond)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, folder.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, folder.UsedQuotaSize)\n\t_, err = httpdtest.RemoveFolder(folder, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestVFolderMultipleQuotaScan(t *testing.T) {\n\tfolderName := \"folder_name\"\n\tres := common.QuotaScans.AddVFolderQuotaScan(folderName)\n\tassert.True(t, res)\n\tres = common.QuotaScans.AddVFolderQuotaScan(folderName)\n\tassert.False(t, res)\n\tres = common.QuotaScans.RemoveVFolderQuotaScan(folderName)\n\tassert.True(t, res)\n\tactiveScans := common.QuotaScans.GetVFoldersQuotaScans()\n\tassert.Len(t, activeScans, 0)\n\tres = common.QuotaScans.RemoveVFolderQuotaScan(folderName)\n\tassert.False(t, res)\n}\n\nfunc TestVFolderQuotaSize(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\ttestFileSize := int64(131072)\n\tu.QuotaFiles = 1\n\tu.QuotaSize = testFileSize + 1\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vpath1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vpath2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\t// quota is included in the user's one\n\t\tQuotaFiles: -1,\n\t\tQuotaSize:  -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  1,\n\t\tQuotaSize:   testFileSize * 2,\n\t})\n\terr := os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// vdir1 is included in the user quota so upload must fail\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.Error(t, err)\n\t\t// upload to vdir2 must work, it has its own quota\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// now vdir2 is over quota\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName+\".quota\"), testFileSize, client)\n\t\tassert.Error(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\t// remove a file\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t\t// upload to vdir1 must work now\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, f.UsedQuotaSize)\n\t\tassert.Equal(t, 1, f.UsedQuotaFiles)\n\t}\n\t// now create another user with the same shared folder but a different quota limit\n\tu.Username = defaultUsername + \"1\"\n\tu.VirtualFolders = nil\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName:       folderName2,\n\t\t\tMappedPath: mappedPath2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  10,\n\t\tQuotaSize:   testFileSize*2 + 1,\n\t})\n\tuser1, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user1, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName+\".quota\"), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// the folder is now over quota for size but not for files\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName+\".quota1\"), testFileSize, client)\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestMissingFile(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(\"missing_file\", localDownloadPath, 0, client)\n\t\tassert.Error(t, err, \"download missing file must fail\")\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestOpenError(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = os.Chmod(user.GetHomeDir(), 0001)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.ReadDir(\".\")\n\t\tassert.Error(t, err, \"read dir must fail if we have no filesystem read permissions\")\n\t\terr = os.Chmod(user.GetHomeDir(), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\ttestFileSize := int64(65535)\n\t\ttestFilePath := filepath.Join(user.GetHomeDir(), testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(testFileName)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(testFilePath, 0001)\n\t\tassert.NoError(t, err)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.Error(t, err, \"file download must fail if we have no filesystem read permissions\")\n\t\terr = sftpUploadFile(localDownloadPath, testFileName, testFileSize, client)\n\t\tassert.Error(t, err, \"upload must fail if we have no filesystem write permissions\")\n\t\ttestDir := \"test\"\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(filepath.Join(user.GetHomeDir(), testDir, testFileName), testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(user.GetHomeDir(), 0000)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Lstat(testFileName)\n\t\tassert.Error(t, err, \"file stat must fail if we have no filesystem read permissions\")\n\t\terr = sftpUploadFile(localDownloadPath, path.Join(testDir, testFileName), testFileSize, client)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\t_, err = client.ReadLink(testFileName)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = client.Remove(testFileName)\n\t\tassert.ErrorIs(t, err, os.ErrPermission)\n\t\terr = os.Chmod(user.GetHomeDir(), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(filepath.Join(user.GetHomeDir(), testDir), 0000)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, path.Join(testDir, testFileName))\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = os.Chmod(filepath.Join(user.GetHomeDir(), testDir), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestOverwriteDirWithFile(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(65535)\n\t\ttestDirName := \"test_dir\" //nolint:goconst\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testDirName)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testDirName, testFileSize, client)\n\t\tassert.Error(t, err, \"copying a file over an existing dir must fail\")\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testDirName)\n\t\tassert.Error(t, err, \"rename a file over an existing dir must fail\")\n\t\terr = client.RemoveDirectory(testDirName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHashedPasswords(t *testing.T) {\n\tusePubKey := false\n\tplainPwd := \"password\"\n\tpwdMapping := make(map[string]string)\n\tpwdMapping[\"$argon2id$v=19$m=65536,t=3,p=2$xtcO/oRkC8O2Tn+mryl2mw$O7bn24f2kuSGRMi9s5Cm61Wqd810px1jDsAasrGWkzQ\"] = plainPwd\n\tpwdMapping[\"$pbkdf2-sha1$150000$DveVjgYUD05R$X6ydQZdyMeOvpgND2nqGR/0GGic=\"] = plainPwd\n\tpwdMapping[\"$pbkdf2-sha256$150000$E86a9YMX3zC7$R5J62hsSq+pYw00hLLPKBbcGXmq7fj5+/M0IFoYtZbo=\"] = plainPwd\n\tpwdMapping[\"$pbkdf2-sha512$150000$dsu7T5R3IaVQ$1hFXPO1ntRBcoWkSLKw+s4sAP09Xtu4Ya7CyxFq64jM9zdUg8eRJVr3NcR2vQgb0W9HHvZaILHsL4Q/Vr6arCg==\"] = plainPwd\n\tpwdMapping[\"$1$b5caebda$VODr/nyhGWgZaY8sJ4x05.\"] = plainPwd\n\tpwdMapping[\"$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK\"] = \"secret\"\n\tpwdMapping[\"$6$459ead56b72e44bc$uog86fUxscjt28BZxqFBE2pp2QD8P/1e98MNF75Z9xJfQvOckZnQ/1YJqiq1XeytPuDieHZvDAMoP7352ELkO1\"] = \"secret\"\n\tpwdMapping[\"$5$h4Aalt0fJdGX8sgv$Rd2ew0fvgzUN.DzAVlKa9QL4q/DZWo4SsKpB9.3AyZ/\"] = plainPwd\n\tpwdMapping[\"$apr1$OBWLeSme$WoJbB736e7kKxMBIAqilb1\"] = plainPwd\n\tpwdMapping[\"{MD5}5f4dcc3b5aa765d61d8327deb882cf99\"] = plainPwd\n\tpwdMapping[\"{SHA256}5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8\"] = plainPwd\n\tpwdMapping[\"{SHA512}b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86\"] = plainPwd\n\tfor pwd, clearPwd := range pwdMapping {\n\t\tu := getTestUser(usePubKey)\n\t\tu.Password = pwd\n\t\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\t\tuser.Password = \"\"\n\t\tuserGetInitial, _, err := httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tuser, err = dataprovider.UserExists(user.Username, \"\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, pwd, user.Password)\n\t\tuser.Password = clearPwd\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err, \"unable to login with password %q\", pwd) {\n\t\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\t\tconn.Close()\n\t\t\tclient.Close()\n\t\t}\n\t\tuser.Password = pwd\n\t\tconn, client, err = getSftpClient(user, usePubKey)\n\t\tif !assert.Error(t, err, \"login with wrong password must fail\") {\n\t\t\tclient.Close()\n\t\t\tconn.Close()\n\t\t}\n\t\t// the password must converted to bcrypt and we should still be able to login\n\t\tuser, err = dataprovider.UserExists(user.Username, \"\")\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, strings.HasPrefix(user.Password, \"$2a$\"))\n\t\t// update the user to invalidate the cached password and force a new check\n\t\tuser.Password = \"\"\n\t\tuserGet, _, err := httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tuserGetInitial.LastLogin = userGet.LastLogin\n\t\tuserGetInitial.UpdatedAt = userGet.UpdatedAt\n\t\tassert.Equal(t, userGetInitial, userGet)\n\t\t// login should still work\n\t\tuser.Password = clearPwd\n\t\tconn, client, err = getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err, \"unable to login with password %q\", pwd) {\n\t\t\tassert.NoError(t, checkBasicSFTP(client))\n\t\t\tconn.Close()\n\t\t\tclient.Close()\n\t\t}\n\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestPasswordsHashPbkdf2Sha256_389DS(t *testing.T) {\n\tpbkdf389dsPwd := \"{PBKDF2_SHA256}AAAIAMZIKG4ie44zJY4HOXI+upFR74PzWLUQV63jg+zzkbEjCK3N4qW583WF7EdcpeoOMQ4HY3aWEXB6lnXhXJixbJkU4vVSJkL6YCbU3TrD0qn1uUUVSkaIgAOtmZENitwbhYhiWfEzGyAtFqkFd75P5xhWJEog9XhQKYrR0f7S3WGGZq03JRcLJ460xpU97bE/sWRn7sshgkWzLuyrs0I+XRKmK7FJeaA9zd+1m44Y3IVmZ2YLdKATzjRHAIgpBC6i1TWOcpKJT1+feP1C9hrxH8vU9baw9thNiO8jSHaZlwb//KpJFe0ahVnG/1ubiG8cO0+CCqDqXVJR6Vr4QZxHP+4pwooW+4TP/L+HFdyA1y6z4gKfqYnBsmb3sD1R1TbxfH4btTdvgZAnBk9CmR3QASkFXxeTYsrmNd5+9IAHc6dm\"\n\tpbkdf389dsPwd = pbkdf389dsPwd[15:]\n\thashBytes, err := base64.StdEncoding.DecodeString(pbkdf389dsPwd)\n\tassert.NoError(t, err)\n\titerBytes := hashBytes[0:4]\n\tvar iterations int32\n\terr = binary.Read(bytes.NewBuffer(iterBytes), binary.BigEndian, &iterations)\n\tassert.NoError(t, err)\n\tsalt := hashBytes[4:68]\n\ttargetKey := hashBytes[68:]\n\tkey := base64.StdEncoding.EncodeToString(targetKey)\n\tpbkdf2Pwd := fmt.Sprintf(\"$pbkdf2-b64salt-sha256$%v$%v$%v\", iterations, base64.StdEncoding.EncodeToString(salt), key)\n\tpbkdf2ClearPwd := \"password\"\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Password = pbkdf2Pwd\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser.Password = pbkdf2ClearPwd\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tassert.NoError(t, checkBasicSFTP(client))\n\t}\n\tuser.Password = pbkdf2Pwd\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif !assert.Error(t, err, \"login with wrong password must fail\") {\n\t\tclient.Close()\n\t\tconn.Close()\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermList(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tu.Permissions[\"/sub\"] = []string{dataprovider.PermCreateSymlinks, dataprovider.PermListItems}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\t_, err = client.ReadDir(\".\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission, \"read remote dir without permission should not succeed\")\n\t\t_, err = client.Stat(\"test_file\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission, \"stat remote file without permission should not succeed\")\n\t\t_, err = client.Lstat(\"test_file\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission, \"lstat remote file without permission should not succeed\")\n\t\t_, err = client.ReadLink(\"test_link\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission, \"read remote link without permission on source dir should not succeed\")\n\t\t_, err = client.RealPath(\".\")\n\t\tassert.ErrorIs(t, err, os.ErrPermission, \"real path without permission should not succeed\")\n\t\tf, err := client.Create(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\t_, err = f.Write([]byte(\"content\"))\n\t\t\tassert.NoError(t, err)\n\t\t\terr = f.Close()\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\terr = client.Mkdir(\"sub\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(\"/\", testFileName), path.Join(\"/sub\", testFileName))\n\t\tassert.NoError(t, err)\n\t\t_, err = client.ReadLink(path.Join(\"/sub\", testFileName))\n\t\tassert.Error(t, err, \"read remote link without permission on targe dir should not succeed\")\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermDownload(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.Error(t, err, \"file download without permission should not succeed\")\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermUpload(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.Error(t, err, \"file upload without permission should not succeed\")\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermOverwrite(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,\n\t\tdataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.Error(t, err, \"file overwrite without permission should not succeed\")\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermDelete(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.Error(t, err, \"delete without permission should not succeed\")\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\n//nolint:dupl\nfunc TestPermRename(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\t_, err = client.Stat(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\n//nolint:dupl\nfunc TestPermRenameOverwrite(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,\n\t\tdataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod, dataprovider.PermRename,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermCreateDirs(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,\n\t\tdataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(\"testdir\")\n\t\tassert.Error(t, err, \"mkdir without permission should not succeed\")\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\n//nolint:dupl\nfunc TestPermSymlink(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,\n\t\tdataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown,\n\t\tdataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(testFilePath, testFilePath+\".symlink\")\n\t\tassert.Error(t, err, \"symlink without permission should not succeed\")\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermChmod(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,\n\t\tdataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite,\n\t\tdataprovider.PermChown, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Chmod(testFileName, os.ModePerm)\n\t\tassert.Error(t, err, \"chmod without permission should not succeed\")\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\n//nolint:dupl\nfunc TestPermChown(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,\n\t\tdataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite,\n\t\tdataprovider.PermChmod, dataprovider.PermChtimes}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Chown(testFileName, os.Getuid(), os.Getgid())\n\t\tassert.Error(t, err, \"chown without permission should not succeed\")\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\n//nolint:dupl\nfunc TestPermChtimes(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,\n\t\tdataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite,\n\t\tdataprovider.PermChmod, dataprovider.PermChown}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Chtimes(testFileName, time.Now(), time.Now())\n\t\tassert.Error(t, err, \"chtimes without permission should not succeed\")\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSubDirsUploads(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermChtimes, dataprovider.PermDownload, dataprovider.PermOverwrite}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(\"subdir\")\n\t\tassert.NoError(t, err)\n\t\ttestFileNameSub := \"/subdir/test_file_dat\"\n\t\ttestSubFile := filepath.Join(user.GetHomeDir(), \"subdir\", \"file.dat\")\n\t\ttestDir := \"testdir\"\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testSubFile, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileNameSub, testFileSize, client)\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Symlink(testFileName, testFileNameSub+\".link\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testFileNameSub+\".rename\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// rename overwriting an existing file\n\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\tassert.NoError(t, err)\n\t\t// now try to overwrite a directory\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(testFileName, testDir)\n\t\tassert.Error(t, err)\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(path.Join(\"/subdir\", \"file.dat\"))\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Remove(testFileName + \".rename\")\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSubDirsOverwrite(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermOverwrite, dataprovider.PermListItems}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileName := \"/subdir/test_file.dat\" //nolint:goconst\n\t\ttestFilePath := filepath.Join(homeBasePath, \"test_file.dat\")\n\t\ttestFileSFTPPath := filepath.Join(u.GetHomeDir(), \"subdir\", \"test_file.dat\")\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFileSFTPPath, 16384)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName+\".new\", testFileSize, client)\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSubDirsDownloads(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermChmod, dataprovider.PermUpload, dataprovider.PermListItems}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(\"subdir\")\n\t\tassert.NoError(t, err)\n\t\ttestFileName := \"/subdir/test_file.dat\"\n\t\ttestFilePath := filepath.Join(homeBasePath, \"test_file.dat\")\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Chtimes(testFileName, time.Now(), time.Now())\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Rename(testFileName, testFileName+\".rename\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Remove(testFileName)\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermsSubDirsSetstat(t *testing.T) {\n\t// for setstat we check the parent dir permission if the requested path is a dir\n\t// otherwise the path permission\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermListItems, dataprovider.PermCreateDirs}\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermAny}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(\"subdir\")\n\t\tassert.NoError(t, err)\n\t\ttestFileName := \"/subdir/test_file.dat\"\n\t\ttestFilePath := filepath.Join(homeBasePath, \"test_file.dat\")\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Chtimes(\"/subdir/\", time.Now(), time.Now())\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Chtimes(\"subdir/\", time.Now(), time.Now())\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Chtimes(testFileName, time.Now(), time.Now())\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestOpenUnhandledChannel(t *testing.T) {\n\tu := getTestUser(false)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tAuth:            []ssh.AuthMethod{ssh.Password(defaultPassword)},\n\t\tTimeout:         5 * time.Second,\n\t}\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif assert.NoError(t, err) {\n\t\t_, _, err = conn.OpenChannel(\"unhandled\", nil)\n\t\tif assert.Error(t, err) {\n\t\t\tassert.Contains(t, err.Error(), \"unknown channel type\")\n\t\t}\n\t\terr = conn.Close()\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestAlgorithmNotNegotiated(t *testing.T) {\n\tu := getTestUser(false)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tconfig := &ssh.ClientConfig{\n\t\tConfig: ssh.Config{\n\t\t\tCiphers: []string{ssh.InsecureCipherRC4},\n\t\t},\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tAuth:            []ssh.AuthMethod{ssh.Password(defaultPassword)},\n\t\tTimeout:         5 * time.Second,\n\t}\n\t_, err = ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif assert.Error(t, err) {\n\t\tnegotiationErr := &ssh.AlgorithmNegotiationError{}\n\t\tassert.ErrorAs(t, err, &negotiationErr)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPermsSubDirsCommands(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs}\n\tu.Permissions[\"/subdir/otherdir\"] = []string{dataprovider.PermListItems, dataprovider.PermDownload}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\terr = client.Mkdir(\"subdir\")\n\t\tassert.NoError(t, err)\n\t\tacmodTime := time.Now()\n\t\terr = client.Chtimes(\"/subdir\", acmodTime, acmodTime)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(\"/subdir\")\n\t\tassert.NoError(t, err)\n\t\t_, err = client.ReadDir(\"/\")\n\t\tassert.NoError(t, err)\n\t\t_, err = client.ReadDir(\"/subdir\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.RemoveDirectory(\"/subdir/dir\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Mkdir(\"/subdir/otherdir/dir\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Mkdir(\"/otherdir\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(\"/subdir/otherdir\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"/otherdir\", \"/subdir/otherdir/adir\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Symlink(\"/otherdir\", \"/subdir/otherdir\")\n\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\terr = client.Symlink(\"/otherdir\", \"/otherdir_link\")\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(\"/otherdir\", \"/otherdir1\")\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveDirectory(\"/otherdir1\")\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRootDirCommands(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = client.Rename(\"/\", \"rootdir\")\n\t\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\t\terr = client.Symlink(\"/\", \"rootdir\")\n\t\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\t\terr = client.RemoveDirectory(\"/\")\n\t\t\tassert.True(t, errors.Is(err, fs.ErrPermission))\n\t\t}\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\tuser.Permissions = make(map[string][]string)\n\t\t\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRelativePaths(t *testing.T) {\n\tuser := getTestUser(true)\n\tvar path, rel string\n\tfilesystems := []vfs.Fs{vfs.NewOsFs(\"\", user.GetHomeDir(), \"\", nil)}\n\tkeyPrefix := strings.TrimPrefix(user.GetHomeDir(), \"/\") + \"/\"\n\ts3config := vfs.S3FsConfig{\n\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\tKeyPrefix: keyPrefix,\n\t\t},\n\t}\n\ts3fs, _ := vfs.NewS3Fs(\"\", user.GetHomeDir(), \"\", s3config)\n\tgcsConfig := vfs.GCSFsConfig{\n\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\tKeyPrefix: keyPrefix,\n\t\t},\n\t}\n\tgcsfs, _ := vfs.NewGCSFs(\"\", user.GetHomeDir(), \"\", gcsConfig)\n\tsftpconfig := vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: sftpServerAddr,\n\t\t\tUsername: defaultUsername,\n\t\t\tPrefix:   keyPrefix,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t}\n\tsftpfs, _ := vfs.NewSFTPFs(\"\", \"\", os.TempDir(), []string{user.Username}, sftpconfig)\n\tif runtime.GOOS != osWindows {\n\t\tfilesystems = append(filesystems, s3fs, gcsfs, sftpfs)\n\t}\n\trootPath := \"/\"\n\tfor _, fs := range filesystems {\n\t\tpath = filepath.Join(user.HomeDir, \"/\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, rootPath, rel)\n\t\tpath = filepath.Join(user.HomeDir, \"//\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, rootPath, rel)\n\t\tpath = filepath.Join(user.HomeDir, \"../..\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, rootPath, rel)\n\t\tpath = filepath.Join(user.HomeDir, \"../../../../../\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, rootPath, rel)\n\t\tpath = filepath.Join(user.HomeDir, \"/..\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, rootPath, rel)\n\t\tpath = filepath.Join(user.HomeDir, \"/../../../..\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, rootPath, rel)\n\t\tpath = filepath.Join(user.HomeDir, \"\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, rootPath, rel)\n\t\tpath = filepath.Join(user.HomeDir, \".\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, rootPath, rel)\n\t\tpath = filepath.Join(user.HomeDir, \"somedir\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, \"/somedir\", rel)\n\t\tpath = filepath.Join(user.HomeDir, \"/somedir/subdir\")\n\t\trel = fs.GetRelativePath(path)\n\t\tassert.Equal(t, \"/somedir/subdir\", rel)\n\t}\n}\n\nfunc TestResolvePaths(t *testing.T) {\n\tuser := getTestUser(true)\n\tvar path, resolved string\n\tvar err error\n\tfilesystems := []vfs.Fs{vfs.NewOsFs(\"\", user.GetHomeDir(), \"\", nil)}\n\tkeyPrefix := strings.TrimPrefix(user.GetHomeDir(), \"/\") + \"/\"\n\ts3config := vfs.S3FsConfig{\n\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\tKeyPrefix: keyPrefix,\n\t\t\tBucket:    \"bucket\",\n\t\t\tRegion:    \"us-east-1\",\n\t\t},\n\t}\n\terr = os.MkdirAll(user.GetHomeDir(), os.ModePerm)\n\tassert.NoError(t, err)\n\ts3fs, err := vfs.NewS3Fs(\"\", user.GetHomeDir(), \"\", s3config)\n\tassert.NoError(t, err)\n\tgcsConfig := vfs.GCSFsConfig{\n\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\tKeyPrefix: keyPrefix,\n\t\t},\n\t}\n\tgcsfs, _ := vfs.NewGCSFs(\"\", user.GetHomeDir(), \"\", gcsConfig)\n\tif runtime.GOOS != osWindows {\n\t\tfilesystems = append(filesystems, s3fs, gcsfs)\n\t}\n\tfor _, fs := range filesystems {\n\t\tpath = \"/\"\n\t\tresolved, _ = fs.ResolvePath(filepath.ToSlash(path))\n\t\tassert.Equal(t, fs.Join(user.GetHomeDir(), \"/\"), resolved)\n\t\tpath = \".\"\n\t\tresolved, _ = fs.ResolvePath(filepath.ToSlash(path))\n\t\tassert.Equal(t, fs.Join(user.GetHomeDir(), \"/\"), resolved)\n\t\tpath = \"test/sub\"\n\t\tresolved, _ = fs.ResolvePath(filepath.ToSlash(path))\n\t\tassert.Equal(t, fs.Join(user.GetHomeDir(), \"/test/sub\"), resolved)\n\t\tpath = \"../test/sub\"\n\t\tresolved, err = fs.ResolvePath(filepath.ToSlash(path))\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fs.Join(user.GetHomeDir(), \"/test/sub\"), resolved)\n\t\tpath = \"../../../test/../sub\"\n\t\tresolved, err = fs.ResolvePath(filepath.ToSlash(path))\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fs.Join(user.GetHomeDir(), \"/sub\"), resolved)\n\t}\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestVirtualRelativePaths(t *testing.T) {\n\tuser := getTestUser(true)\n\tmappedPath := filepath.Join(os.TempDir(), \"mdir\")\n\tvdirPath := \"/vdir\" //nolint:goconst\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t})\n\terr := os.MkdirAll(mappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tfsRoot := vfs.NewOsFs(\"\", user.GetHomeDir(), \"\", nil)\n\tfsVdir := vfs.NewOsFs(\"\", mappedPath, vdirPath, nil)\n\trel := fsVdir.GetRelativePath(mappedPath)\n\tassert.Equal(t, vdirPath, rel)\n\trel = fsRoot.GetRelativePath(filepath.Join(mappedPath, \"..\"))\n\tassert.Equal(t, \"/\", rel)\n\t// path outside home and virtual dir\n\trel = fsRoot.GetRelativePath(filepath.Join(mappedPath, \"../vdir1\"))\n\tassert.Equal(t, \"/\", rel)\n\trel = fsVdir.GetRelativePath(filepath.Join(mappedPath, \"../vdir1\"))\n\tassert.Equal(t, \"/vdir\", rel)\n\trel = fsVdir.GetRelativePath(filepath.Join(mappedPath, \"file.txt\"))\n\tassert.Equal(t, \"/vdir/file.txt\", rel)\n\trel = fsRoot.GetRelativePath(filepath.Join(user.HomeDir, \"vdir1/file.txt\"))\n\tassert.Equal(t, \"/vdir1/file.txt\", rel)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestUserPerms(t *testing.T) {\n\tuser := getTestUser(true)\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermListItems}\n\tuser.Permissions[\"/p\"] = []string{dataprovider.PermDelete}\n\tuser.Permissions[\"/p/1\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}\n\tuser.Permissions[\"/p/2\"] = []string{dataprovider.PermCreateDirs}\n\tuser.Permissions[\"/p/3\"] = []string{dataprovider.PermChmod}\n\tuser.Permissions[\"/p/3/4\"] = []string{dataprovider.PermChtimes}\n\tuser.Permissions[\"/tmp\"] = []string{dataprovider.PermRename}\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \"/\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \".\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \"\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \"../\"))\n\t// path p and /p are the same\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/p\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDownload, \"/p/1\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermCreateDirs, \"p/2\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermChmod, \"/p/3\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermChtimes, \"p/3/4/\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermChtimes, \"p/3/4/../4\"))\n\t// undefined paths have permissions of the nearest path\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \"/p34\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \"/p34/p1/file.dat\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermChtimes, \"/p/3/4/5/6\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDownload, \"/p/1/test/file.dat\"))\n}\n\nfunc TestWildcardPermissions(t *testing.T) {\n\tuser := getTestUser(true)\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermListItems}\n\tuser.Permissions[\"/p*\"] = []string{dataprovider.PermDelete}\n\tuser.Permissions[\"/p/*\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}\n\tuser.Permissions[\"/p/2\"] = []string{dataprovider.PermCreateDirs}\n\tuser.Permissions[\"/pa\"] = []string{dataprovider.PermChmod}\n\tuser.Permissions[\"/p/3/4\"] = []string{dataprovider.PermChtimes}\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \"/\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/p1\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/ppppp\"))\n\tassert.False(t, user.HasPerm(dataprovider.PermDelete, \"/pa\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermChmod, \"/pa\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermUpload, \"/p/1\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermUpload, \"/p/p\"))\n\tassert.False(t, user.HasPerm(dataprovider.PermUpload, \"/p/2\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDownload, \"/p/3\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDownload, \"/p/a/a/a\"))\n\tassert.False(t, user.HasPerm(dataprovider.PermDownload, \"/p/3/4\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermChtimes, \"/p/3/4\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/pb/a/a/a\"))\n\tassert.False(t, user.HasPerm(dataprovider.PermDelete, \"/abc/a/a/a\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \"/abc/a/a/a/b\"))\n}\n\nfunc TestRootWildcardPerms(t *testing.T) {\n\tuser := getTestUser(true)\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermListItems}\n\tuser.Permissions[\"/*\"] = []string{dataprovider.PermDelete}\n\tuser.Permissions[\"/p/*\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}\n\tuser.Permissions[\"/p/2\"] = []string{dataprovider.PermCreateDirs}\n\tuser.Permissions[\"/pa\"] = []string{dataprovider.PermChmod}\n\tuser.Permissions[\"/p/3/4\"] = []string{dataprovider.PermChtimes}\n\tassert.True(t, user.HasPerm(dataprovider.PermListItems, \"/\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/p1\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/ppppp\"))\n\tassert.False(t, user.HasPerm(dataprovider.PermDelete, \"/pa\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermChmod, \"/pa\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermUpload, \"/p/1\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermUpload, \"/p/p\"))\n\tassert.False(t, user.HasPerm(dataprovider.PermUpload, \"/p/2\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermCreateDirs, \"/p/2\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermCreateDirs, \"/p/2/a\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDownload, \"/p/3\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDownload, \"/p/a/a/a\"))\n\tassert.False(t, user.HasPerm(dataprovider.PermDownload, \"/p/3/4\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermChtimes, \"/p/3/4\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/pb/a/a/a\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/abc/a/a/a\"))\n\tassert.False(t, user.HasPerm(dataprovider.PermListItems, \"/abc/a/a/a/b\"))\n\tassert.True(t, user.HasPerm(dataprovider.PermDelete, \"/abc/a/a/a/b\"))\n}\n\nfunc TestFilterFilePatterns(t *testing.T) {\n\tuser := getTestUser(true)\n\tpattern := sdk.PatternsFilter{\n\t\tPath:            \"/test\",\n\t\tAllowedPatterns: []string{\"*.jpg\", \"*.png\"},\n\t\tDeniedPatterns:  []string{\"*.pdf\"},\n\t}\n\tfilters := dataprovider.UserFilters{\n\t\tBaseUserFilters: sdk.BaseUserFilters{\n\t\t\tFilePatterns: []sdk.PatternsFilter{pattern},\n\t\t},\n\t}\n\tuser.Filters = filters\n\tok, _ := user.IsFileAllowed(\"/test/test.jPg\")\n\tassert.True(t, ok)\n\tok, _ = user.IsFileAllowed(\"/test/test.pdf\")\n\tassert.False(t, ok)\n\tok, _ = user.IsFileAllowed(\"/test.pDf\")\n\tassert.True(t, ok)\n\n\tfilters.FilePatterns = append(filters.FilePatterns, sdk.PatternsFilter{\n\t\tPath:            \"/\",\n\t\tAllowedPatterns: []string{\"*.zip\", \"*.rar\", \"*.pdf\"},\n\t\tDeniedPatterns:  []string{\"*.gz\"},\n\t})\n\tuser.Filters = filters\n\tok, _ = user.IsFileAllowed(\"/test1/test.gz\")\n\tassert.False(t, ok)\n\tok, _ = user.IsFileAllowed(\"/test1/test.zip\")\n\tassert.True(t, ok)\n\tok, _ = user.IsFileAllowed(\"/test/sub/test.pdf\")\n\tassert.False(t, ok)\n\tok, _ = user.IsFileAllowed(\"/test1/test.png\")\n\tassert.False(t, ok)\n\n\tfilters.FilePatterns = append(filters.FilePatterns, sdk.PatternsFilter{\n\t\tPath:           \"/test/sub\",\n\t\tDeniedPatterns: []string{\"*.tar\"},\n\t})\n\tuser.Filters = filters\n\tok, _ = user.IsFileAllowed(\"/test/sub/sub/test.tar\")\n\tassert.False(t, ok)\n\tok, _ = user.IsFileAllowed(\"/test/sub/test.gz\")\n\tassert.True(t, ok)\n\tok, _ = user.IsFileAllowed(\"/test/test.zip\")\n\tassert.False(t, ok)\n}\n\nfunc TestUserAllowedLoginMethods(t *testing.T) {\n\tuser := getTestUser(true)\n\tuser.Filters.DeniedLoginMethods = dataprovider.ValidLoginMethods\n\tallowedMethods := user.GetAllowedLoginMethods()\n\tassert.Equal(t, 0, len(allowedMethods))\n\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}\n\tallowedMethods = user.GetAllowedLoginMethods()\n\tassert.Equal(t, 4, len(allowedMethods))\n\n\tassert.True(t, slices.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndKeyboardInt))\n\tassert.True(t, slices.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndPassword))\n}\n\nfunc TestUserPartialAuth(t *testing.T) {\n\tuser := getTestUser(true)\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}\n\tassert.True(t, user.IsPartialAuth())\n\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}\n\tassert.False(t, user.IsPartialAuth())\n\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t}\n\tassert.False(t, user.IsPartialAuth())\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.SSHLoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}\n\tassert.True(t, user.IsPartialAuth())\n}\n\nfunc TestUserGetNextAuthMethods(t *testing.T) {\n\tuser := getTestUser(true)\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}\n\tmethods := user.GetNextAuthMethods()\n\trequire.Len(t, methods, 2)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, methods[0])\n\tassert.Equal(t, dataprovider.SSHLoginMethodKeyboardInteractive, methods[1])\n\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt,\n\t}\n\tmethods = user.GetNextAuthMethods()\n\trequire.Len(t, methods, 1)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, methods[0])\n\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t\tdataprovider.SSHLoginMethodKeyAndPassword,\n\t}\n\tmethods = user.GetNextAuthMethods()\n\trequire.Len(t, methods, 1)\n\tassert.Equal(t, dataprovider.SSHLoginMethodKeyboardInteractive, methods[0])\n\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyAndPassword,\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt,\n\t}\n\tmethods = user.GetNextAuthMethods()\n\trequire.Len(t, methods, 0)\n}\n\nfunc TestUserIsLoginMethodAllowed(t *testing.T) {\n\tuser := getTestUser(true)\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.LoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}\n\tassert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolSSH))\n\tassert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolFTP))\n\tassert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolWebDAV))\n\tassert.False(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodPublicKey, common.ProtocolSSH))\n\tassert.False(t, user.IsLoginMethodAllowed(dataprovider.SSHLoginMethodKeyboardInteractive, common.ProtocolSSH))\n\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.SSHLoginMethodPublicKey,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive,\n\t}\n\tassert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolSSH))\n\n\tuser.Filters.DeniedLoginMethods = []string{\n\t\tdataprovider.SSHLoginMethodPassword,\n\t}\n\tassert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP))\n\tassert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolFTP))\n\tassert.True(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolWebDAV))\n\tassert.False(t, user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolSSH))\n}\n\nfunc TestUserEmptySubDirPerms(t *testing.T) {\n\tuser := getTestUser(true)\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/emptyperms\"] = []string{}\n\tfor _, p := range dataprovider.ValidPerms {\n\t\tassert.False(t, user.HasPerm(p, \"/emptyperms\"))\n\t}\n}\n\nfunc TestUserFiltersIPMaskConditions(t *testing.T) {\n\tuser := getTestUser(true)\n\t// with no filter login must be allowed even if the remoteIP is invalid\n\tassert.True(t, user.IsLoginFromAddrAllowed(\"192.168.1.5\"))\n\tassert.True(t, user.IsLoginFromAddrAllowed(\"invalid\"))\n\n\tuser.Filters.DeniedIP = append(user.Filters.DeniedIP, \"192.168.1.0/24\")\n\tassert.False(t, user.IsLoginFromAddrAllowed(\"192.168.1.5\"))\n\tassert.True(t, user.IsLoginFromAddrAllowed(\"192.168.2.6\"))\n\n\tuser.Filters.AllowedIP = append(user.Filters.AllowedIP, \"192.168.1.5/32\")\n\t// if the same ip/mask is both denied and allowed then login must be allowed\n\tassert.True(t, user.IsLoginFromAddrAllowed(\"192.168.1.5\"))\n\tassert.False(t, user.IsLoginFromAddrAllowed(\"192.168.1.3\"))\n\tassert.False(t, user.IsLoginFromAddrAllowed(\"192.168.3.6\"))\n\n\tuser.Filters.DeniedIP = []string{}\n\tassert.True(t, user.IsLoginFromAddrAllowed(\"192.168.1.5\"))\n\tassert.False(t, user.IsLoginFromAddrAllowed(\"192.168.1.6\"))\n\n\tuser.Filters.DeniedIP = []string{\"192.168.0.0/16\", \"172.16.0.0/16\"}\n\tuser.Filters.AllowedIP = []string{}\n\tassert.False(t, user.IsLoginFromAddrAllowed(\"192.168.5.255\"))\n\tassert.False(t, user.IsLoginFromAddrAllowed(\"172.16.1.2\"))\n\tassert.True(t, user.IsLoginFromAddrAllowed(\"172.18.2.1\"))\n\n\tuser.Filters.AllowedIP = []string{\"10.4.4.0/24\"}\n\tassert.False(t, user.IsLoginFromAddrAllowed(\"10.5.4.2\"))\n\tassert.True(t, user.IsLoginFromAddrAllowed(\"10.4.4.2\"))\n\tassert.True(t, user.IsLoginFromAddrAllowed(\"invalid\"))\n}\n\nfunc TestGetVirtualFolderForPath(t *testing.T) {\n\tuser := getTestUser(true)\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vpath1\")\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vpath1\")\n\tmappedPath3 := filepath.Join(os.TempDir(), \"vpath3\")\n\tvdirPath := \"/vdir/sub\"\n\tvSubDirPath := path.Join(vdirPath, \"subdir\", \"subdir\")\n\tvSubDir1Path := path.Join(vSubDirPath, \"subdir\", \"subdir\")\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath1,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t})\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath2,\n\t\t},\n\t\tVirtualPath: vSubDir1Path,\n\t})\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tMappedPath: mappedPath3,\n\t\t},\n\t\tVirtualPath: vSubDirPath,\n\t})\n\tfolder, err := user.GetVirtualFolderForPath(path.Join(vSubDirPath, \"file\"))\n\tassert.NoError(t, err)\n\tassert.Equal(t, folder.MappedPath, mappedPath3)\n\t_, err = user.GetVirtualFolderForPath(\"/file\")\n\tassert.Error(t, err)\n\tfolder, err = user.GetVirtualFolderForPath(path.Join(vdirPath, \"/file\"))\n\tassert.NoError(t, err)\n\tassert.Equal(t, folder.MappedPath, mappedPath1)\n\tfolder, err = user.GetVirtualFolderForPath(path.Join(vSubDirPath+\"1\", \"file\"))\n\tassert.NoError(t, err)\n\tassert.Equal(t, folder.MappedPath, mappedPath1)\n\t_, err = user.GetVirtualFolderForPath(\"/vdir/sub1/file\")\n\tassert.Error(t, err)\n\tfolder, err = user.GetVirtualFolderForPath(vdirPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestStatVFS(t *testing.T) {\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(65535)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tstat, err := client.StatVFS(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, stat.ID, uint32(0))\n\t\tassert.Greater(t, stat.Blocks, uint64(0))\n\t\tassert.Greater(t, stat.Bsize, uint64(0))\n\n\t\t_, err = client.StatVFS(\"missing-path\")\n\t\tassert.Error(t, err)\n\t\tassert.True(t, errors.Is(err, fs.ErrNotExist))\n\t}\n\tuser.QuotaFiles = 100\n\tuser.Filters.DisableFsChecks = true\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\n\t\t_, err = client.StatVFS(\"missing-path\")\n\t\tassert.Error(t, err)\n\t\tassert.ErrorIs(t, err, fs.ErrNotExist)\n\n\t\tstat, err := client.StatVFS(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, stat.ID, uint32(0))\n\t\tassert.Greater(t, stat.Blocks, uint64(0))\n\t\tassert.Greater(t, stat.Bsize, uint64(0))\n\t\tassert.Equal(t, uint64(100), stat.Files)\n\t\tassert.Equal(t, uint64(99), stat.Ffree)\n\t}\n\n\tuser.QuotaSize = 8192\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tstat, err := client.StatVFS(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, stat.ID, uint32(0))\n\t\tassert.Greater(t, stat.Blocks, uint64(0))\n\t\tassert.Greater(t, stat.Bsize, uint64(0))\n\t\tassert.Equal(t, uint64(100), stat.Files)\n\t\tassert.Equal(t, uint64(0), stat.Ffree)\n\t\tassert.Equal(t, uint64(2), stat.Blocks)\n\t\tassert.Equal(t, uint64(0), stat.Bfree)\n\t}\n\tuser.QuotaFiles = 0\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tstat, err := client.StatVFS(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, stat.ID, uint32(0))\n\t\tassert.Greater(t, stat.Blocks, uint64(0))\n\t\tassert.Greater(t, stat.Bsize, uint64(0))\n\t\tassert.Greater(t, stat.Files, uint64(0))\n\t\tassert.Equal(t, uint64(0), stat.Ffree)\n\t\tassert.Equal(t, uint64(2), stat.Blocks)\n\t\tassert.Equal(t, uint64(0), stat.Bfree)\n\t}\n\n\tuser.QuotaSize = 1\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\tstat, err := client.StatVFS(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, stat.ID, uint32(0))\n\t\tassert.Equal(t, uint64(1), stat.Blocks)\n\t\tassert.Equal(t, uint64(1), stat.Bsize)\n\t\tassert.Greater(t, stat.Files, uint64(0))\n\t\tassert.Equal(t, uint64(0), stat.Ffree)\n\t\tassert.Equal(t, uint64(1), stat.Blocks)\n\t\tassert.Equal(t, uint64(0), stat.Bfree)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestStatVFSCloudBackend(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.FsConfig.Provider = sdk.AzureBlobFilesystemProvider\n\tu.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret(\"https://myaccount.blob.core.windows.net/sasurl\")\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\n\t\terr = dataprovider.UpdateUserQuota(&user, 100, 8192, true)\n\t\tassert.NoError(t, err)\n\t\tstat, err := client.StatVFS(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, stat.ID, uint32(0))\n\t\tassert.Greater(t, stat.Blocks, uint64(0))\n\t\tassert.Greater(t, stat.Bsize, uint64(0))\n\t\tassert.Equal(t, uint64(1000000+100), stat.Files)\n\t\tassert.Equal(t, uint64(2147483648+2), stat.Blocks)\n\t\tassert.Equal(t, uint64(1000000), stat.Ffree)\n\t\tassert.Equal(t, uint64(2147483648), stat.Bfree)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSSHCommands(t *testing.T) {\n\tusePubKey := false\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\t_, err = runSSHCommand(\"ls\", user, usePubKey)\n\tassert.Error(t, err, \"unsupported ssh command must fail\")\n\n\t_, err = runSSHCommand(\"cd\", user, usePubKey)\n\tassert.NoError(t, err)\n\tout, err := runSSHCommand(\"pwd\", user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, \"/\\n\", string(out))\n\t}\n\tout, err = runSSHCommand(\"md5sum\", user, usePubKey)\n\tassert.NoError(t, err)\n\t// echo -n '' | md5sum\n\tassert.Contains(t, string(out), \"d41d8cd98f00b204e9800998ecf8427e\")\n\n\tout, err = runSSHCommand(\"sha1sum\", user, usePubKey)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(out), \"da39a3ee5e6b4b0d3255bfef95601890afd80709\")\n\n\tout, err = runSSHCommand(\"sha256sum\", user, usePubKey)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(out), \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\")\n\n\tout, err = runSSHCommand(\"sha384sum\", user, usePubKey)\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(out), \"38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSSHFileHash(t *testing.T) {\n\tusePubKey := true\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUserWithCryptFs(usePubKey)\n\tu.Username = u.Username + \"_crypt\"\n\tcryptUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser, cryptUser} {\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\t\ttestFileSize := int64(65535)\n\t\t\terr = createTestFile(testFilePath, testFileSize)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Permissions = make(map[string][]string)\n\t\t\tuser.Permissions[\"/\"] = []string{dataprovider.PermUpload}\n\t\t\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = runSSHCommand(\"sha512sum \"+testFileName, user, usePubKey)\n\t\t\tassert.Error(t, err, \"hash command with no list permission must fail\")\n\n\t\t\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\t\t\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\t\tassert.NoError(t, err)\n\n\t\t\tinitialHash, err := computeHashForFile(sha512.New(), testFilePath)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tout, err := runSSHCommand(\"sha512sum \"+testFileName, user, usePubKey)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Contains(t, string(out), initialHash)\n\t\t\t}\n\t\t\t_, err = runSSHCommand(\"sha512sum invalid_path\", user, usePubKey)\n\t\t\tassert.Error(t, err, \"hash for an invalid path must fail\")\n\n\t\t\terr = os.Remove(testFilePath)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(cryptUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSSHCopy(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1/subdir\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2/subdir\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  100,\n\t\tQuotaSize:   0,\n\t})\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           \"/\",\n\t\t\tDeniedPatterns: []string{\"*.denied\"},\n\t\t},\n\t}\n\terr := os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestDir := \"adir\"\n\ttestDir1 := \"adir1\"\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(131072)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize1 := int64(65537)\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, testDir1))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, testDir1))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(testDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testDir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath1, testDir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testDir1, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, testDir1, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(path.Join(testDir, testFileName), testFileName)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 4, user.UsedQuotaFiles)\n\t\tassert.Equal(t, 2*testFileSize+2*testFileSize1, user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize+testFileSize1, f.UsedQuotaSize)\n\t\tassert.Equal(t, 2, f.UsedQuotaFiles)\n\n\t\t_, err = client.Stat(testDir1)\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s\", path.Join(vdirPath1, testDir1)), user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(\"sftpgo-copy\", user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", testFileName, testFileName+\".linkcopy\"), user, usePubKey)\n\t\tassert.Error(t, err)\n\t\tout, err := runSSHCommand(fmt.Sprintf(\"sftpgo-copy %s %s\", path.Join(vdirPath1, testDir1), \".\"), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t\tfi, err := client.Stat(testDir1)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.True(t, fi.IsDir())\n\t\t\t}\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 6, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, 3*testFileSize+3*testFileSize1, user.UsedQuotaSize)\n\t\t}\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", \"missing\\\\ dir\", \".\"), user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath1, testDir1), \".\"), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\t// all files are overwritten, quota must remain unchanged\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 6, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, 3*testFileSize+3*testFileSize1, user.UsedQuotaSize)\n\t\t}\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath2, testDir1, testFileName), testFileName+\".copy\"), //nolint:goconst\n\t\t\tuser, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t\tfi, err := client.Stat(testFileName + \".copy\") //nolint:goconst\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.True(t, fi.Mode().IsRegular())\n\t\t\t}\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 7, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, 4*testFileSize+3*testFileSize1, user.UsedQuotaSize)\n\t\t}\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath1, testDir1), path.Join(vdirPath2, testDir1+\"copy\")), //nolint:goconst\n\t\t\tuser, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t\tfi, err := client.Stat(path.Join(vdirPath2, testDir1+\"copy\"))\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.True(t, fi.IsDir())\n\t\t\t}\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 7, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, 4*testFileSize+3*testFileSize1, user.UsedQuotaSize)\n\t\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testFileSize*2+testFileSize1*2, f.UsedQuotaSize)\n\t\t\tassert.Equal(t, 4, f.UsedQuotaFiles)\n\t\t}\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath1, testDir1), path.Join(vdirPath1, testDir1+\"copy\")),\n\t\t\tuser, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t\t_, err := client.Stat(path.Join(vdirPath2, testDir1+\"copy\"))\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 9, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, 5*testFileSize+4*testFileSize1, user.UsedQuotaSize)\n\t\t\tf, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\t}\n\t\t// cross folder copy\n\t\tnewDir := \"newdir\"\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath2, \"..\"), newDir), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(newDir)\n\t\tassert.NoError(t, err)\n\t\t// denied pattern\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(testDir, testFileName), testFileName+\".denied\"), user, usePubKey)\n\t\tassert.Error(t, err)\n\t\tif runtime.GOOS != osWindows {\n\t\t\tsubPath := filepath.Join(mappedPath1, testDir1, \"asubdir\", \"anothersub\", \"another\")\n\t\t\terr = os.MkdirAll(subPath, os.ModePerm)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Chmod(subPath, 0001)\n\t\t\tassert.NoError(t, err)\n\t\t\t// listing contents for subdirs with no permissions will fail\n\t\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", vdirPath1, \"newdir1\"), user, usePubKey)\n\t\t\tassert.Error(t, err)\n\t\t\terr = os.Chmod(subPath, os.ModePerm)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = os.Chmod(filepath.Join(user.GetHomeDir(), testDir1), 0555)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath1, testDir1, testFileName),\n\t\t\t\tpath.Join(testDir1, \"anewdir\")), user, usePubKey)\n\t\t\tassert.Error(t, err)\n\t\t\terr = os.Chmod(filepath.Join(user.GetHomeDir(), testDir1), os.ModePerm)\n\t\t\tassert.NoError(t, err)\n\n\t\t\terr = os.Chmod(user.GetHomeDir(), os.ModePerm)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestSSHCopyPermissions(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/dir1\"] = []string{dataprovider.PermUpload, dataprovider.PermDownload, dataprovider.PermListItems}\n\tu.Permissions[\"/dir2\"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDownload,\n\t\tdataprovider.PermListItems, dataprovider.PermCopy}\n\tu.Permissions[\"/dir3\"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermDownload,\n\t\tdataprovider.PermListItems}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestDir := \"tDir\"\n\t\ttestFileSize := int64(131072)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(\"/\", testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// test copy file with no permission\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(\"/\", testDir, testFileName), path.Join(\"/dir3\", testFileName)),\n\t\t\tuser, usePubKey)\n\t\tassert.Error(t, err)\n\t\t// test copy dir with no create dirs perm\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(\"/\", testDir), \"/dir1/\"), user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t// dir2 has the needed permissions\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(\"/\", testDir), \"/dir2/\"), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\tinfo, err := client.Stat(path.Join(\"/dir2\", testDir))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.IsDir())\n\t\t}\n\t\tinfo, err = client.Stat(path.Join(\"/dir2\", testDir, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.True(t, info.Mode().IsRegular())\n\t\t}\n\t\t// now create a symlink, dir2 has no create symlink permission, but symlinks will be ignored\n\t\terr = client.Symlink(path.Join(\"/\", testDir, testFileName), path.Join(\"/\", testDir, testFileName+\".link\"))\n\t\tassert.NoError(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(\"/\", testDir), \"/dir2/sub\"), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(\"/\", testDir), \"/newdir\"), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\t// now delete the file and copy inside /dir3\n\t\terr = client.Remove(path.Join(\"/\", testDir, testFileName))\n\t\tassert.NoError(t, err)\n\t\t// the symlink will be skipped, so no errors\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(\"/\", testDir), \"/dir3\"), user, usePubKey)\n\t\tassert.NoError(t, err)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSSHCopyQuotaLimits(t *testing.T) {\n\tusePubKey := true\n\ttestFileSize := int64(131072)\n\ttestFileSize1 := int64(65536)\n\ttestFileSize2 := int64(32768)\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 3\n\tu.QuotaSize = testFileSize + testFileSize1 + 1\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  3,\n\t\tQuotaSize:   testFileSize + testFileSize1 + 1,\n\t})\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:           \"/\",\n\t\t\tDeniedPatterns: []string{\"*.denied\"},\n\t\t},\n\t}\n\terr := os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestDir := \"testDir\"\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\ttestFileName2 := \"test_file2.dat\"\n\t\ttestFilePath2 := filepath.Join(homeBasePath, testFileName2)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFilePath2, testFileSize2)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath2, path.Join(testDir, testFileName2), testFileSize2, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath2, path.Join(testDir, testFileName2+\".dupl\"), testFileSize2, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath2, path.Join(vdirPath2, testDir, testFileName2), testFileSize2, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath2, path.Join(vdirPath2, testDir, testFileName2+\".dupl\"), testFileSize2, client)\n\t\tassert.NoError(t, err)\n\t\t// user quota: 2 files, size: 32768*2, folder2 quota: 2 files, size: 32768*2\n\t\t// try to duplicate testDir, this will result in 4 file (over quota) and 32768*4 bytes (not over quota)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", testDir, testDir+\"_copy\"), user, usePubKey) //nolint:goconst\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath2, testDir),\n\t\t\tpath.Join(vdirPath2, testDir+\"_copy\")), user, usePubKey)\n\t\tassert.Error(t, err)\n\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", testDir), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", path.Join(vdirPath2, testDir)), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\t// remove partially copied dirs\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", testDir+\"_copy\"), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", path.Join(vdirPath2, testDir+\"_copy\")), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, user.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(0), user.UsedQuotaSize)\n\t\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\t\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\t\terr = client.Mkdir(path.Join(vdirPath1, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath1, testDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, testDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\n\t\t// vdir1 is included in user quota, file limit will be exceeded\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath1, testDir), \"/\"), user, usePubKey)\n\t\tassert.Error(t, err)\n\n\t\t// vdir2 size limit will be exceeded\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath1, testDir, testFileName),\n\t\t\tvdirPath2+\"/\"), user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t// now decrease the limits\n\t\tuser.QuotaFiles = 1\n\t\tuser.QuotaSize = testFileSize * 10\n\t\tfor idx, f := range user.VirtualFolders {\n\t\t\tif f.Name == folderName2 {\n\t\t\t\tuser.VirtualFolders[idx].QuotaSize = testFileSize\n\t\t\t\tuser.VirtualFolders[idx].QuotaFiles = 10\n\t\t\t}\n\t\t}\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.QuotaFiles)\n\t\tassert.Equal(t, testFileSize*10, user.QuotaSize)\n\t\tif assert.Len(t, user.VirtualFolders, 2) {\n\t\t\tfor _, f := range user.VirtualFolders {\n\t\t\t\tif f.Name == folderName2 {\n\t\t\t\t\tassert.Equal(t, testFileSize, f.QuotaSize)\n\t\t\t\t\tassert.Equal(t, 10, f.QuotaFiles)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath1, testDir),\n\t\t\tpath.Join(vdirPath2, testDir+\".copy\")), user, usePubKey)\n\t\tassert.Error(t, err)\n\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-copy %v %v\", path.Join(vdirPath2, testDir),\n\t\t\ttestDir+\".copy\"), user, usePubKey)\n\t\tassert.Error(t, err)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath2)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestSSHRemove(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1/sub\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2/sub\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  100,\n\t\tQuotaSize:   0,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestFileSize := int64(131072)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFileSize1 := int64(65537)\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\ttestDir := \"testdir\"\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath1, testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath1, testDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath, path.Join(vdirPath2, testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = sftpUploadFile(testFilePath1, path.Join(vdirPath2, testDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\tassert.NoError(t, err)\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", testFileName+\".link\"), user, usePubKey)\n\t\tassert.NoError(t, err)\n\t\t_, err = runSSHCommand(\"sftpgo-remove /vdir1\", user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(\"sftpgo-remove /\", user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(\"sftpgo-remove\", user, usePubKey)\n\t\tassert.Error(t, err)\n\t\tout, err := runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", testFileName), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t\t_, err := client.Stat(testFileName)\n\t\t\tassert.Error(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 3, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, testFileSize+2*testFileSize1, user.UsedQuotaSize)\n\t\t}\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", path.Join(vdirPath1, testDir)), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t\t_, err := client.Stat(path.Join(vdirPath1, testFileName))\n\t\t\tassert.Error(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t\tassert.Equal(t, testFileSize1, user.UsedQuotaSize)\n\t\t}\n\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", vdirPath1), user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(\"sftpgo-remove /\", user, usePubKey)\n\t\tassert.Error(t, err)\n\t\t_, err = runSSHCommand(\"sftpgo-remove missing_file\", user, usePubKey)\n\t\tassert.Error(t, err)\n\t\tif runtime.GOOS != osWindows {\n\t\t\terr = os.Chmod(filepath.Join(mappedPath2, testDir), 0555)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", path.Join(vdirPath2, testDir)), user, usePubKey)\n\t\t\tassert.Error(t, err)\n\t\t\terr = os.Chmod(filepath.Join(mappedPath2, testDir), 0001)\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", path.Join(vdirPath2, testDir)), user, usePubKey)\n\t\t\tassert.Error(t, err)\n\t\t\terr = os.Chmod(filepath.Join(mappedPath2, testDir), os.ModePerm)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\t// test remove dir with no delete perm\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermUpload, dataprovider.PermDownload, dataprovider.PermListItems}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tconn, client, err = getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\t_, err = runSSHCommand(\"sftpgo-remove adir\", user, usePubKey)\n\t\tassert.Error(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestSSHRemoveCryptFs(t *testing.T) {\n\tusePubKey := false\n\tu := getTestUserWithCryptFs(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1/sub\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2/sub\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  100,\n\t\tQuotaSize:   0,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\ttestDir := \"tdir\"\n\t\terr = client.Mkdir(testDir)\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath1, testDir))\n\t\tassert.NoError(t, err)\n\t\terr = client.Mkdir(path.Join(vdirPath2, testDir))\n\t\tassert.NoError(t, err)\n\t\ttestFileSize := int64(32768)\n\t\ttestFileSize1 := int64(65536)\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\terr = writeSFTPFile(testFileName, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(testFileName1, testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(testDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath1, testDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testDir, testFileName1), testFileSize1, client)\n\t\tassert.NoError(t, err)\n\t\t_, err = runSSHCommand(\"sftpgo-remove /vdir2\", user, usePubKey)\n\t\tassert.Error(t, err)\n\t\tout, err := runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", testFileName), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t\t_, err := client.Stat(testFileName)\n\t\t\tassert.Error(t, err)\n\t\t}\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", testDir), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t}\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", path.Join(vdirPath1, testDir)), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t}\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", path.Join(vdirPath2, testDir, testFileName)), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t}\n\t\terr = writeSFTPFile(path.Join(vdirPath2, testDir, testFileName), testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tout, err = runSSHCommand(fmt.Sprintf(\"sftpgo-remove %v\", path.Join(vdirPath2, testDir)), user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, \"OK\\n\", string(out))\n\t\t}\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\tassert.Greater(t, user.UsedQuotaSize, testFileSize1)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestSSHCommandMaxTransfers(t *testing.T) {\n\tif len(gitPath) == 0 || len(sshPath) == 0 || runtime.GOOS == osWindows {\n\t\tt.Skip(\"git and/or ssh command not found or OS is windows, unable to execute this test\")\n\t}\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 2\n\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\trepoName := \"testrepo\" //nolint:goconst\n\tclonePath := filepath.Join(homeBasePath, repoName)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(filepath.Join(homeBasePath, repoName))\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tf1, err := client.Create(\"file1\")\n\t\tassert.NoError(t, err)\n\t\tf2, err := client.Create(\"file2\")\n\t\tassert.NoError(t, err)\n\t\t_, err = f1.Write([]byte(\" \"))\n\t\tassert.NoError(t, err)\n\t\t_, err = f2.Write([]byte(\" \"))\n\t\tassert.NoError(t, err)\n\n\t\t_, err = client.Create(\"file3\")\n\t\tassert.Error(t, err)\n\n\t\terr = f1.Close()\n\t\tassert.NoError(t, err)\n\t\terr = f2.Close()\n\t\tassert.NoError(t, err)\n\t\terr = client.Close()\n\t\tassert.NoError(t, err)\n\t\terr = conn.Close()\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(clonePath)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\n// Start SCP tests\nfunc TestSCPBasicHandling(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaSize = 6553600\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.QuotaSize = 6553600\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(131074)\n\texpectedQuotaSize := testFileSize\n\texpectedQuotaFiles := 1\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\t\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\t\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\t\t// test to download a missing file\n\t\terr = scpDownload(localPath, remoteDownPath, false, false)\n\t\tassert.Error(t, err, \"downloading a missing file via scp must fail\")\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), user.FirstUpload)\n\t\tassert.Equal(t, int64(0), user.FirstDownload)\n\t\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.FirstUpload, int64(0))\n\t\tassert.Equal(t, int64(0), user.FirstDownload)\n\t\terr = scpDownload(localPath, remoteDownPath, false, false)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.FirstUpload, int64(0))\n\t\tassert.Greater(t, user.FirstDownload, int64(0))\n\t\tfi, err := os.Stat(localPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, fi.Size())\n\t\t}\n\t\terr = os.Remove(localPath)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPUploadFileOverwrite(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1000\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser(usePubKey)\n\tu.QuotaFiles = 1000\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(32760)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\t\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\t\tassert.NoError(t, err)\n\t\t// test a new upload that must overwrite the existing file\n\t\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\n\t\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\t\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\t\terr = scpDownload(localPath, remoteDownPath, false, false)\n\t\tassert.NoError(t, err)\n\n\t\tfi, err := os.Stat(localPath)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, fi.Size())\n\t\t}\n\t\t// now create a simlink via SFTP, replace the symlink with a file via SCP and check quota usage\n\t\tconn, client, err := getSftpClient(user, usePubKey)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer conn.Close()\n\t\t\tdefer client.Close()\n\t\t\terr = client.Symlink(testFileName, testFileName+\".link\")\n\t\t\tassert.NoError(t, err)\n\t\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\t\t\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\t\t}\n\t\terr = scpUpload(testFilePath, remoteUpPath+\".link\", true, false)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, testFileSize*2, user.UsedQuotaSize)\n\t\tassert.Equal(t, 2, user.UsedQuotaFiles)\n\n\t\terr = os.Remove(localPath)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPRecursive(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tlocalUser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(getTestSFTPUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestBaseDirName := \"test_dir\"\n\ttestBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)\n\ttestBaseDirDownName := \"test_dir_down\" //nolint:goconst\n\ttestBaseDirDownPath := filepath.Join(homeBasePath, testBaseDirDownName)\n\ttestFilePath := filepath.Join(homeBasePath, testBaseDirName, testFileName)\n\ttestFilePath1 := filepath.Join(homeBasePath, testBaseDirName, testBaseDirName, testFileName)\n\ttestFileSize := int64(131074)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = createTestFile(testFilePath1, testFileSize)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testBaseDirName))\n\t\t// test to download a missing dir\n\t\terr = scpDownload(testBaseDirDownPath, remoteDownPath, true, true)\n\t\tassert.Error(t, err, \"downloading a missing dir via scp must fail\")\n\n\t\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\t\terr = scpUpload(testBaseDirPath, remoteUpPath, true, false)\n\t\tassert.NoError(t, err)\n\t\t// overwrite existing dir\n\t\terr = scpUpload(testBaseDirPath, remoteUpPath, true, false)\n\t\tassert.NoError(t, err)\n\t\terr = scpDownload(testBaseDirDownPath, remoteDownPath, true, true)\n\t\tassert.NoError(t, err)\n\t\t// test download without passing -r\n\t\terr = scpDownload(testBaseDirDownPath, remoteDownPath, true, false)\n\t\tassert.Error(t, err, \"recursive download without -r must fail\")\n\n\t\tfi, err := os.Stat(filepath.Join(testBaseDirDownPath, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, fi.Size())\n\t\t}\n\t\tfi, err = os.Stat(filepath.Join(testBaseDirDownPath, testBaseDirName, testFileName))\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, testFileSize, fi.Size())\n\t\t}\n\t\t// upload to a non existent dir\n\t\tremoteUpPath = fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/non_existent_dir\")\n\t\terr = scpUpload(testBaseDirPath, remoteUpPath, true, false)\n\t\tassert.Error(t, err, \"uploading via scp to a non existent dir must fail\")\n\n\t\terr = os.RemoveAll(testBaseDirDownPath)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\n\terr = os.RemoveAll(testBaseDirPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPStartDirectory(t *testing.T) {\n\tusePubKey := true\n\tstartDir := \"/sta rt/dir\"\n\tu := getTestUser(usePubKey)\n\tu.Filters.StartDirectory = startDir\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFileSize := int64(131072)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:\", user.Username)\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(localPath, remoteDownPath, false, false)\n\tassert.NoError(t, err)\n\t// check that the file is in the start directory\n\t_, err = os.Stat(filepath.Join(user.HomeDir, startDir, testFileName))\n\tassert.NoError(t, err)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPPatternsFilter(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(131072)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\tuser.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/\",\n\t\t\tAllowedPatterns: []string{\"*.zip\"},\n\t\t\tDeniedPatterns:  []string{},\n\t\t},\n\t}\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\terr = scpDownload(localPath, remoteDownPath, false, false)\n\tassert.Error(t, err, \"scp download must fail\")\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.Error(t, err, \"scp upload must fail\")\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = os.Stat(localPath)\n\tif err == nil {\n\t\terr = os.Remove(localPath)\n\t\tassert.NoError(t, err)\n\t}\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPTransferQuotaLimits(t *testing.T) {\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.DownloadDataTransfer = 1\n\tu.UploadDataTransfer = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(550000)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(localDownloadPath, remoteDownPath, false, false)\n\tassert.NoError(t, err)\n\t// error while download is active\n\terr = scpDownload(localDownloadPath, remoteDownPath, false, false)\n\tassert.Error(t, err)\n\t// error before starting the download\n\terr = scpDownload(localDownloadPath, remoteDownPath, false, false)\n\tassert.Error(t, err)\n\t// error while upload is active\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.Error(t, err)\n\t// error before starting the upload\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.Error(t, err)\n\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Greater(t, user.UsedDownloadDataTransfer, int64(1024*1024))\n\tif !assert.Greater(t, user.UsedUploadDataTransfer, int64(1024*1024), user.UsedDownloadDataTransfer) {\n\t\tprintLatestLogs(30)\n\t}\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPUploadMaxSize(t *testing.T) {\n\ttestFileSize := int64(65535)\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Filters.MaxUploadFileSize = testFileSize + 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\ttestFileSize1 := int64(131072)\n\ttestFileName1 := \"test_file1.dat\"\n\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\terr = createTestFile(testFilePath1, testFileSize1)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\terr = scpUpload(testFilePath1, remoteUpPath, false, false)\n\tassert.Error(t, err)\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath1)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPVirtualFolders(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tmappedPath := filepath.Join(os.TempDir(), \"vdir\")\n\tfolderName := filepath.Base(mappedPath)\n\tvdirPath := \"/vdir\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestBaseDirName := \"test_dir\"\n\ttestBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)\n\ttestBaseDirDownName := \"test_dir_down\"\n\ttestBaseDirDownPath := filepath.Join(homeBasePath, testBaseDirDownName)\n\ttestFilePath := filepath.Join(homeBasePath, testBaseDirName, testFileName)\n\ttestFilePath1 := filepath.Join(homeBasePath, testBaseDirName, testBaseDirName, testFileName)\n\ttestFileSize := int64(131074)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = createTestFile(testFilePath1, testFileSize)\n\tassert.NoError(t, err)\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, vdirPath)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, vdirPath)\n\terr = scpUpload(testBaseDirPath, remoteUpPath, true, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(testBaseDirDownPath, remoteDownPath, true, true)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(testBaseDirPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(testBaseDirDownPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPNestedFolders(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tbaseUser, resp, err := httpdtest.AddUser(getTestUser(false), http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.HomeDir += \"_folders\"\n\tu.Username += \"_folders\"\n\tmappedPathSFTP := filepath.Join(os.TempDir(), \"sftp\")\n\tfolderNameSFTP := filepath.Base(mappedPathSFTP)\n\tvdirSFTPPath := \"/vdir/sftp\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameSFTP,\n\t\t},\n\t\tVirtualPath: vdirSFTPPath,\n\t})\n\tmappedPathCrypt := filepath.Join(os.TempDir(), \"crypt\")\n\tfolderNameCrypt := filepath.Base(mappedPathCrypt)\n\tvdirCryptPath := \"/vdir/crypt\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameCrypt,\n\t\t},\n\t\tVirtualPath: vdirCryptPath,\n\t})\n\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName: folderNameSFTP,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\tUsername: baseUser.Username,\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName: folderNameCrypt,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t\tMappedPath: mappedPathCrypt,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser, resp, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tbaseDirDownPath := filepath.Join(os.TempDir(), \"basedir-down\")\n\terr = os.Mkdir(baseDirDownPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tbaseDir := filepath.Join(os.TempDir(), \"basedir\")\n\terr = os.Mkdir(baseDir, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(filepath.Join(baseDir, vdirSFTPPath), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(filepath.Join(baseDir, vdirCryptPath), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = createTestFile(filepath.Join(baseDir, vdirSFTPPath, testFileName), 32768)\n\tassert.NoError(t, err)\n\terr = createTestFile(filepath.Join(baseDir, vdirCryptPath, testFileName), 65535)\n\tassert.NoError(t, err)\n\terr = createTestFile(filepath.Join(baseDir, \"vdir\", testFileName), 65536)\n\tassert.NoError(t, err)\n\n\tremoteRootPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\terr = scpUpload(filepath.Join(baseDir, \"vdir\"), remoteRootPath, true, false)\n\tassert.NoError(t, err)\n\n\tconn, client, err := getSftpClient(user, usePubKey)\n\tif assert.NoError(t, err) {\n\t\tdefer conn.Close()\n\t\tdefer client.Close()\n\t\tinfo, err := client.Stat(path.Join(vdirCryptPath, testFileName))\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(65535), info.Size())\n\t\tinfo, err = client.Stat(path.Join(vdirSFTPPath, testFileName))\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(32768), info.Size())\n\t\tinfo, err = client.Stat(path.Join(\"/vdir\", testFileName))\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(65536), info.Size())\n\t}\n\n\terr = scpDownload(baseDirDownPath, remoteRootPath, true, true)\n\tassert.NoError(t, err)\n\n\tassert.FileExists(t, filepath.Join(baseDirDownPath, user.Username, \"vdir\", testFileName))\n\tassert.FileExists(t, filepath.Join(baseDirDownPath, user.Username, vdirCryptPath, testFileName))\n\tassert.FileExists(t, filepath.Join(baseDirDownPath, user.Username, vdirSFTPPath, testFileName))\n\n\tif runtime.GOOS != osWindows {\n\t\terr = os.Chmod(filepath.Join(baseUser.GetHomeDir(), testFileName), 0001)\n\t\tassert.NoError(t, err)\n\t\terr = scpDownload(baseDirDownPath, remoteRootPath, true, true)\n\t\tassert.Error(t, err)\n\t\terr = os.Chmod(filepath.Join(baseUser.GetHomeDir(), testFileName), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\n\t// now change the password for the base user, so SFTP folder will not work\n\tbaseUser.Password = defaultPassword + \"_mod\"\n\t_, _, err = httpdtest.UpdateUser(baseUser, http.StatusOK, \"1\")\n\tassert.NoError(t, err)\n\n\terr = scpUpload(filepath.Join(baseDir, \"vdir\"), remoteRootPath, true, false)\n\tassert.Error(t, err)\n\n\terr = scpDownload(baseDirDownPath, remoteRootPath, true, true)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameSFTP}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(baseUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathCrypt)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathSFTP)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseDir)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(baseDirDownPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPVirtualFoldersQuota(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 100\n\tmappedPath1 := filepath.Join(os.TempDir(), \"vdir1\")\n\tfolderName1 := filepath.Base(mappedPath1)\n\tvdirPath1 := \"/vdir1\"\n\tmappedPath2 := filepath.Join(os.TempDir(), \"vdir2\")\n\tfolderName2 := filepath.Base(mappedPath2)\n\tvdirPath2 := \"/vdir2\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName1,\n\t\t},\n\t\tVirtualPath: vdirPath1,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName2,\n\t\t},\n\t\tVirtualPath: vdirPath2,\n\t\tQuotaFiles:  0,\n\t\tQuotaSize:   0,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName:       folderName1,\n\t\tMappedPath: mappedPath1,\n\t}\n\t_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName2,\n\t\tMappedPath: mappedPath2,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(mappedPath2, os.ModePerm)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestBaseDirName := \"test_dir\"\n\ttestBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)\n\ttestBaseDirDownName := \"test_dir_down\"\n\ttestBaseDirDownPath := filepath.Join(homeBasePath, testBaseDirDownName)\n\ttestFilePath := filepath.Join(homeBasePath, testBaseDirName, testFileName)\n\ttestFilePath1 := filepath.Join(homeBasePath, testBaseDirName, testBaseDirName, testFileName)\n\ttestFileSize := int64(131074)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = createTestFile(testFilePath1, testFileSize)\n\tassert.NoError(t, err)\n\tremoteDownPath1 := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", vdirPath1))\n\tremoteUpPath1 := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, vdirPath1)\n\tremoteDownPath2 := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", vdirPath2))\n\tremoteUpPath2 := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, vdirPath2)\n\t// we upload two times to test overwrite\n\terr = scpUpload(testBaseDirPath, remoteUpPath1, true, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(testBaseDirDownPath, remoteDownPath1, true, true)\n\tassert.NoError(t, err)\n\terr = scpUpload(testBaseDirPath, remoteUpPath1, true, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(testBaseDirDownPath, remoteDownPath1, true, true)\n\tassert.NoError(t, err)\n\terr = scpUpload(testBaseDirPath, remoteUpPath2, true, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(testBaseDirDownPath, remoteDownPath2, true, true)\n\tassert.NoError(t, err)\n\texpectedQuotaFiles := 2\n\texpectedQuotaSize := testFileSize * 2\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\tf, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), f.UsedQuotaSize)\n\tassert.Equal(t, 0, f.UsedQuotaFiles)\n\tf, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaSize, f.UsedQuotaSize)\n\tassert.Equal(t, expectedQuotaFiles, f.UsedQuotaFiles)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(testBaseDirPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(testBaseDirDownPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath1)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath2)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPPermsSubDirs(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.Permissions[\"/somedir\"] = []string{dataprovider.PermListItems, dataprovider.PermUpload}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\tsubPath := filepath.Join(user.GetHomeDir(), \"somedir\")\n\ttestFileSize := int64(65535)\n\terr = os.MkdirAll(subPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/somedir\")\n\terr = scpDownload(localPath, remoteDownPath, false, true)\n\tassert.Error(t, err, \"download a dir with no permissions must fail\")\n\terr = os.Remove(subPath)\n\tassert.NoError(t, err)\n\terr = createTestFile(subPath, testFileSize)\n\tassert.NoError(t, err)\n\terr = scpDownload(localPath, remoteDownPath, false, false)\n\tassert.NoError(t, err)\n\tif runtime.GOOS != osWindows {\n\t\terr = os.Chmod(subPath, 0001)\n\t\tassert.NoError(t, err)\n\t\terr = scpDownload(localPath, remoteDownPath, false, false)\n\t\tassert.Error(t, err, \"download a file with no system permissions must fail\")\n\t\terr = os.Chmod(subPath, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t}\n\terr = os.Remove(localPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPPermCreateDirs(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(32760)\n\ttestBaseDirName := \"test_dir\"\n\ttestBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)\n\ttestFilePath1 := filepath.Join(homeBasePath, testBaseDirName, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = createTestFile(testFilePath1, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/tmp/\")\n\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\tassert.Error(t, err, \"scp upload must fail, the user cannot create files in a missing dir\")\n\terr = scpUpload(testBaseDirPath, remoteUpPath, true, false)\n\tassert.Error(t, err, \"scp upload must fail, the user cannot create new dirs\")\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(testBaseDirPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPPermUpload(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65536)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/tmp\")\n\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\tassert.Error(t, err, \"scp upload must fail, the user cannot upload\")\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPPermOverwrite(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65536)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/tmp\")\n\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\tassert.NoError(t, err)\n\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\tassert.Error(t, err, \"scp upload must fail, the user cannot ovewrite existing files\")\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPPermDownload(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tu := getTestUser(usePubKey)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65537)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\tassert.NoError(t, err)\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\terr = scpDownload(localPath, remoteDownPath, false, false)\n\tassert.Error(t, err, \"scp download must fail, the user cannot download\")\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPQuotaSize(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\ttestFileSize := int64(65535)\n\tu := getTestUser(usePubKey)\n\tu.QuotaFiles = 1\n\tu.QuotaSize = testFileSize + 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\ttestFileSize1 := int64(131072)\n\ttestFileName1 := \"test_file1.dat\"\n\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\terr = createTestFile(testFilePath1, testFileSize1)\n\tassert.NoError(t, err)\n\ttestFileSize2 := int64(32768)\n\ttestFileName2 := \"test_file2.dat\"\n\ttestFilePath2 := filepath.Join(homeBasePath, testFileName2)\n\terr = createTestFile(testFilePath2, testFileSize2)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\tassert.NoError(t, err)\n\terr = scpUpload(testFilePath, remoteUpPath+\".quota\", true, false)\n\tassert.Error(t, err, \"user is over quota scp upload must fail\")\n\n\t// now test quota limits while uploading the current file, we have 1 bytes remaining\n\tuser.QuotaSize = testFileSize + 1\n\tuser.QuotaFiles = 0\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\terr = scpUpload(testFilePath2, remoteUpPath+\".quota\", true, false)\n\tassert.Error(t, err, \"user is over quota scp upload must fail\")\n\t// overwriting an existing file will work if the resulting size is lesser or equal than the current one\n\terr = scpUpload(testFilePath1, remoteUpPath, true, false)\n\tassert.Error(t, err)\n\terr = scpUpload(testFilePath2, remoteUpPath, true, false)\n\tassert.NoError(t, err)\n\terr = scpUpload(testFilePath, remoteUpPath, true, false)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath1)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath2)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPEscapeHomeDir(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(user.GetHomeDir(), os.ModePerm)\n\tassert.NoError(t, err)\n\ttestDir := \"testDir\"\n\tlinkPath := filepath.Join(homeBasePath, defaultUsername, testDir)\n\terr = os.Symlink(homeBasePath, linkPath)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(testDir, testDir))\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.Error(t, err, \"uploading to a dir with a symlink outside home dir must fail\")\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testDir, testFileName))\n\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\terr = scpDownload(localPath, remoteDownPath, false, false)\n\tassert.Error(t, err, \"scp download must fail, the requested file has a symlink outside user home\")\n\tremoteDownPath = fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testDir))\n\terr = scpDownload(homeBasePath, remoteDownPath, false, true)\n\tassert.Error(t, err, \"scp download must fail, the requested dir is a symlink outside user home\")\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPUploadPaths(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\ttestDirName := \"testDir\"\n\ttestDirPath := filepath.Join(user.GetHomeDir(), testDirName)\n\terr = os.MkdirAll(testDirPath, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, testDirName)\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(testDirName, testFileName))\n\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\terr = scpDownload(localPath, remoteDownPath, false, false)\n\tassert.NoError(t, err)\n\t// upload a file to a missing dir\n\tremoteUpPath = fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(testDirName, testDirName, testFileName))\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.Error(t, err, \"scp upload to a missing dir must fail\")\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.Remove(localPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPOverwriteDirWithFile(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\ttestDirPath := filepath.Join(user.GetHomeDir(), testFileName)\n\terr = os.MkdirAll(testDirPath, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.Error(t, err, \"copying a file over an existing dir must fail\")\n\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPRemoteToRemote(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"scp between remote hosts is not supported on Windows\")\n\t}\n\tusePubKey := true\n\tuser, _, err := httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)\n\tassert.NoError(t, err)\n\tu := getTestUser(usePubKey)\n\tu.Username += \"1\"\n\tu.HomeDir += \"1\"\n\tuser1, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\tremote1UpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user1.Username, path.Join(\"/\", testFileName))\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\terr = scpUpload(remoteUpPath, remote1UpPath, false, true)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPErrors(t *testing.T) {\n\tif scpPath == \"\" {\n\t\tt.Skip(\"scp command not found, unable to execute this test\")\n\t}\n\tu := getTestUser(true)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttestFileSize := int64(524288)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tremoteUpPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, \"/\")\n\tremoteDownPath := fmt.Sprintf(\"%v@127.0.0.1:%v\", user.Username, path.Join(\"/\", testFileName))\n\tlocalPath := filepath.Join(homeBasePath, \"scp_download.dat\")\n\terr = scpUpload(testFilePath, remoteUpPath, false, false)\n\tassert.NoError(t, err)\n\tuser.UploadBandwidth = 512\n\tuser.DownloadBandwidth = 512\n\t_, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tcmd := getScpDownloadCommand(localPath, remoteDownPath, false, false)\n\tgo func() {\n\t\terr := cmd.Run()\n\t\tassert.Error(t, err, \"SCP download must fail\")\n\t}()\n\twaitForActiveTransfers(t)\n\t// wait some additional arbitrary time to wait for transfer activity to happen\n\t// it is need to reach all the code in CheckIdleConnections\n\ttime.Sleep(100 * time.Millisecond)\n\terr = cmd.Process.Kill()\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 }, 2*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\tcmd = getScpUploadCommand(testFilePath, remoteUpPath, false, false)\n\tgo func() {\n\t\terr := cmd.Run()\n\t\tassert.Error(t, err, \"SCP upload must fail\")\n\t}()\n\twaitForActiveTransfers(t)\n\t// wait some additional arbitrary time to wait for transfer activity to happen\n\t// it is need to reach all the code in CheckIdleConnections\n\ttime.Sleep(100 * time.Millisecond)\n\terr = cmd.Process.Kill()\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 }, 2*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\tos.Remove(localPath)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\n// End SCP tests\n\nfunc waitTCPListening(address string) {\n\tfor {\n\t\tconn, err := net.Dial(\"tcp\", address)\n\t\tif err != nil {\n\t\t\tlogger.WarnToConsole(\"tcp server %v not listening: %v\", address, err)\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\t\tlogger.InfoToConsole(\"tcp server %v now listening\", address)\n\t\tconn.Close()\n\t\tbreak\n\t}\n}\n\nfunc getTestGroup() dataprovider.Group {\n\treturn dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName:        \"test_group\",\n\t\t\tDescription: \"test group description\",\n\t\t},\n\t}\n}\n\nfunc getTestUser(usePubKey bool) dataprovider.User {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:       defaultUsername,\n\t\t\tPassword:       defaultPassword,\n\t\t\tHomeDir:        filepath.Join(homeBasePath, defaultUsername),\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: 0,\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = allPerms\n\tif usePubKey {\n\t\tuser.PublicKeys = []string{testPubKey}\n\t\tuser.Password = \"\"\n\t}\n\treturn user\n}\n\nfunc getTestSFTPUser(usePubKey bool) dataprovider.User {\n\tu := getTestUser(usePubKey)\n\tu.Username = defaultSFTPUsername\n\tu.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tu.FsConfig.SFTPConfig.Endpoint = sftpServerAddr\n\tu.FsConfig.SFTPConfig.Username = defaultUsername\n\tu.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\tif usePubKey {\n\t\tu.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(testPrivateKey)\n\t\tu.FsConfig.SFTPConfig.Fingerprints = hostKeyFPs\n\t}\n\treturn u\n}\n\nfunc runSSHCommand(command string, user dataprovider.User, usePubKey bool) ([]byte, error) {\n\tvar sshSession *ssh.Session\n\tvar output []byte\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         5 * time.Second,\n\t}\n\tif usePubKey {\n\t\tkey, err := ssh.ParsePrivateKey([]byte(testPrivateKey))\n\t\tif err != nil {\n\t\t\treturn output, err\n\t\t}\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(key)}\n\t} else {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}\n\t}\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif err != nil {\n\t\treturn output, err\n\t}\n\tdefer conn.Close()\n\tsshSession, err = conn.NewSession()\n\tif err != nil {\n\t\treturn output, err\n\t}\n\tvar stdout, stderr bytes.Buffer\n\tsshSession.Stdout = &stdout\n\tsshSession.Stderr = &stderr\n\terr = sshSession.Run(command)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run command %v: %v\", command, stderr.Bytes())\n\t}\n\treturn stdout.Bytes(), err\n}\n\nfunc getSignerForUserCert(certBytes []byte) (ssh.Signer, error) {\n\tsigner, err := ssh.ParsePrivateKey([]byte(testPrivateKey))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcert, _, _, _, err := ssh.ParseAuthorizedKey(certBytes) //nolint:dogsled\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ssh.NewCertSigner(cert.(*ssh.Certificate), signer)\n}\n\nfunc getSftpClientWithAddr(user dataprovider.User, usePubKey bool, addr string) (*ssh.Client, *sftp.Client, error) {\n\tvar sftpClient *sftp.Client\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         5 * time.Second,\n\t}\n\tif usePubKey {\n\t\tsigner, err := ssh.ParsePrivateKey([]byte(testPrivateKey))\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}\n\t} else {\n\t\tif user.Password != \"\" {\n\t\t\tif user.Password == \"empty\" {\n\t\t\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(\"\")}\n\t\t\t} else {\n\t\t\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}\n\t\t\t}\n\t\t} else {\n\t\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}\n\t\t}\n\t}\n\tconn, err := ssh.Dial(\"tcp\", addr, config)\n\tif err != nil {\n\t\treturn conn, sftpClient, err\n\t}\n\tsftpClient, err = sftp.NewClient(conn)\n\tif err != nil {\n\t\tconn.Close()\n\t}\n\treturn conn, sftpClient, err\n}\n\nfunc getSftpClient(user dataprovider.User, usePubKey bool) (*ssh.Client, *sftp.Client, error) {\n\treturn getSftpClientWithAddr(user, usePubKey, sftpServerAddr)\n}\n\nfunc getKeyboardInteractiveSftpClient(user dataprovider.User, answers []string) (*ssh.Client, *sftp.Client, error) {\n\tvar sftpClient *sftp.Client\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tAuth: []ssh.AuthMethod{\n\t\t\tssh.KeyboardInteractive(func(_, _ string, _ []string, _ []bool) ([]string, error) {\n\t\t\t\treturn answers, nil\n\t\t\t}),\n\t\t},\n\t\tTimeout: 5 * time.Second,\n\t}\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif err != nil {\n\t\treturn nil, sftpClient, err\n\t}\n\tsftpClient, err = sftp.NewClient(conn)\n\tif err != nil {\n\t\tconn.Close()\n\t}\n\treturn conn, sftpClient, err\n}\n\nfunc getCustomAuthSftpClient(user dataprovider.User, authMethods []ssh.AuthMethod, addr string) (*ssh.Client, *sftp.Client, error) {\n\tvar sftpClient *sftp.Client\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tAuth:            authMethods,\n\t\tTimeout:         5 * time.Second,\n\t}\n\tvar err error\n\tvar conn *ssh.Client\n\tif addr != \"\" {\n\t\tconn, err = ssh.Dial(\"tcp\", addr, config)\n\t} else {\n\t\tconn, err = ssh.Dial(\"tcp\", sftpServerAddr, config)\n\t}\n\tif err != nil {\n\t\treturn conn, sftpClient, err\n\t}\n\tsftpClient, err = sftp.NewClient(conn)\n\tif err != nil {\n\t\tconn.Close()\n\t}\n\treturn conn, sftpClient, err\n}\n\nfunc createTestFile(path string, size int64) error {\n\tbaseDir := filepath.Dir(path)\n\tif _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) {\n\t\terr = os.MkdirAll(baseDir, os.ModePerm)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tcontent := make([]byte, size)\n\t_, err := rand.Read(content)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, content, os.ModePerm)\n}\n\nfunc appendToTestFile(path string, size int64) error {\n\tcontent := make([]byte, size)\n\t_, err := rand.Read(content)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, os.ModePerm)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\twritten, err := io.Copy(f, bytes.NewReader(content))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif written != size {\n\t\treturn fmt.Errorf(\"write error, written: %v/%v\", written, size)\n\t}\n\treturn nil\n}\n\nfunc checkBasicSFTP(client *sftp.Client) error {\n\t_, err := client.Getwd()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = client.ReadDir(\".\")\n\treturn err\n}\n\nfunc writeSFTPFile(name string, size int64, client *sftp.Client) error {\n\tcontent := make([]byte, size)\n\t_, err := rand.Read(content)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf, err := client.Create(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = io.Copy(f, bytes.NewBuffer(content))\n\tif err != nil {\n\t\tf.Close()\n\t\treturn err\n\t}\n\terr = f.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\tinfo, err := client.Stat(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif info.Size() != size {\n\t\treturn fmt.Errorf(\"file size mismatch, wanted %v, actual %v\", size, info.Size())\n\t}\n\treturn nil\n}\n\nfunc sftpUploadFile(localSourcePath string, remoteDestPath string, expectedSize int64, client *sftp.Client) error {\n\tsrcFile, err := os.Open(localSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer srcFile.Close()\n\tdestFile, err := client.Create(remoteDestPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = io.Copy(destFile, srcFile)\n\tif err != nil {\n\t\tdestFile.Close()\n\t\treturn err\n\t}\n\t// we need to close the file to trigger the server side close method\n\t// we cannot defer closing otherwise Stat will fail for upload atomic mode\n\tdestFile.Close()\n\tif expectedSize > 0 {\n\t\tfi, err := client.Stat(remoteDestPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fi.Size() != expectedSize {\n\t\t\treturn fmt.Errorf(\"uploaded file size does not match, actual: %v, expected: %v\", fi.Size(), expectedSize)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc sftpUploadResumeFile(localSourcePath string, remoteDestPath string, expectedSize int64, invalidOffset bool, //nolint:unparam\n\tclient *sftp.Client) error {\n\tsrcFile, err := os.Open(localSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer srcFile.Close()\n\tfi, err := client.Lstat(remoteDestPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !invalidOffset {\n\t\t_, err = srcFile.Seek(fi.Size(), 0)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tdestFile, err := client.OpenFile(remoteDestPath, os.O_WRONLY|os.O_APPEND)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !invalidOffset {\n\t\t_, err = destFile.Seek(fi.Size(), 0)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_, err = io.Copy(destFile, srcFile)\n\tif err != nil {\n\t\tdestFile.Close()\n\t\treturn err\n\t}\n\t// we need to close the file to trigger the server side close method\n\t// we cannot defer closing otherwise Stat will fail for upload atomic mode\n\tdestFile.Close()\n\tif expectedSize > 0 {\n\t\tfi, err := client.Lstat(remoteDestPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fi.Size() != expectedSize {\n\t\t\treturn fmt.Errorf(\"uploaded file size does not match, actual: %v, expected: %v\", fi.Size(), expectedSize)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc sftpDownloadFile(remoteSourcePath string, localDestPath string, expectedSize int64, client *sftp.Client) error {\n\tdownloadDest, err := os.Create(localDestPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer downloadDest.Close()\n\tsftpSrcFile, err := client.Open(remoteSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sftpSrcFile.Close()\n\t_, err = io.Copy(downloadDest, sftpSrcFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = downloadDest.Sync()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif expectedSize > 0 {\n\t\tfi, err := downloadDest.Stat()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fi.Size() != expectedSize {\n\t\t\treturn fmt.Errorf(\"downloaded file size does not match, actual: %v, expected: %v\", fi.Size(), expectedSize)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc sftpUploadNonBlocking(localSourcePath string, remoteDestPath string, expectedSize int64, client *sftp.Client) <-chan error {\n\tc := make(chan error, 1)\n\tgo func() {\n\t\tc <- sftpUploadFile(localSourcePath, remoteDestPath, expectedSize, client)\n\t}()\n\treturn c\n}\n\nfunc sftpDownloadNonBlocking(remoteSourcePath string, localDestPath string, expectedSize int64, client *sftp.Client) <-chan error {\n\tc := make(chan error, 1)\n\tgo func() {\n\t\tc <- sftpDownloadFile(remoteSourcePath, localDestPath, expectedSize, client)\n\t}()\n\treturn c\n}\n\nfunc scpUpload(localPath, remotePath string, preserveTime, remoteToRemote bool) error {\n\tcmd := getScpUploadCommand(localPath, remotePath, preserveTime, remoteToRemote)\n\treturn cmd.Run()\n}\n\nfunc scpDownload(localPath, remotePath string, preserveTime, recursive bool) error {\n\tcmd := getScpDownloadCommand(localPath, remotePath, preserveTime, recursive)\n\treturn cmd.Run()\n}\n\nfunc getScpDownloadCommand(localPath, remotePath string, preserveTime, recursive bool) *exec.Cmd {\n\tvar args []string\n\tif preserveTime {\n\t\targs = append(args, \"-p\")\n\t}\n\tif recursive {\n\t\targs = append(args, \"-r\")\n\t}\n\tif scpForce {\n\t\targs = append(args, \"-O\")\n\t}\n\targs = append(args, \"-P\")\n\targs = append(args, \"2022\")\n\targs = append(args, \"-o\")\n\targs = append(args, \"StrictHostKeyChecking=no\")\n\targs = append(args, \"-i\")\n\targs = append(args, privateKeyPath)\n\targs = append(args, remotePath)\n\targs = append(args, localPath)\n\treturn exec.Command(scpPath, args...)\n}\n\nfunc getScpUploadCommand(localPath, remotePath string, preserveTime, remoteToRemote bool) *exec.Cmd {\n\tvar args []string\n\tif remoteToRemote {\n\t\targs = append(args, \"-3\")\n\t}\n\tif preserveTime {\n\t\targs = append(args, \"-p\")\n\t}\n\tfi, err := os.Stat(localPath)\n\tif err == nil {\n\t\tif fi.IsDir() {\n\t\t\targs = append(args, \"-r\")\n\t\t}\n\t}\n\tif scpForce {\n\t\targs = append(args, \"-O\")\n\t}\n\targs = append(args, \"-P\")\n\targs = append(args, \"2022\")\n\targs = append(args, \"-o\")\n\targs = append(args, \"StrictHostKeyChecking=no\")\n\targs = append(args, \"-o\")\n\targs = append(args, \"HostKeyAlgorithms=+ssh-rsa\")\n\targs = append(args, \"-i\")\n\targs = append(args, privateKeyPath)\n\targs = append(args, localPath)\n\targs = append(args, remotePath)\n\treturn exec.Command(scpPath, args...)\n}\n\nfunc computeHashForFile(hasher hash.Hash, path string) (string, error) {\n\thash := \"\"\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn hash, err\n\t}\n\tdefer f.Close()\n\t_, err = io.Copy(hasher, f)\n\tif err == nil {\n\t\thash = fmt.Sprintf(\"%x\", hasher.Sum(nil))\n\t}\n\treturn hash, err\n}\n\nfunc waitForActiveTransfers(t *testing.T) {\n\tassert.Eventually(t, func() bool {\n\t\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\t\tif len(stat.Transfers) > 0 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}, 1*time.Second, 50*time.Millisecond)\n}\n\nfunc checkSystemCommands() {\n\tvar err error\n\tgitPath, err = exec.LookPath(\"git\")\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to get git command. GIT tests will be skipped, err: %v\", err)\n\t\tlogger.WarnToConsole(\"unable to get git command. GIT tests will be skipped, err: %v\", err)\n\t\tgitPath = \"\"\n\t}\n\n\tsshPath, err = exec.LookPath(\"ssh\")\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to get ssh command. GIT tests will be skipped, err: %v\", err)\n\t\tlogger.WarnToConsole(\"unable to get ssh command. GIT tests will be skipped, err: %v\", err)\n\t\tgitPath = \"\"\n\t}\n\thookCmdPath, err = exec.LookPath(\"true\")\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to get hook command: %v\", err)\n\t\tlogger.WarnToConsole(\"unable to get hook command: %v\", err)\n\t}\n\tscpPath, err = exec.LookPath(\"scp\")\n\tif err != nil {\n\t\tlogger.Warn(logSender, \"\", \"unable to get scp command. SCP tests will be skipped, err: %v\", err)\n\t\tlogger.WarnToConsole(\"unable to get scp command. SCP tests will be skipped, err: %v\", err)\n\t\tscpPath = \"\"\n\t} else {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\t\tcmd := exec.CommandContext(ctx, scpPath, \"-O\")\n\t\tout, _ := cmd.CombinedOutput()\n\t\tscpForce = !strings.Contains(string(out), \"option -- O\")\n\t}\n}\n\nfunc getKeyboardInteractiveScriptForBuiltinChecks(addPasscode bool, result int) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\techos := []bool{false}\n\tq, _ := json.Marshal([]string{\"Password: \"})\n\te, _ := json.Marshal(echos)\n\tcontent = append(content, []byte(fmt.Sprintf(\"echo '{\\\"questions\\\":%v,\\\"echos\\\":%v,\\\"check_password\\\":1}'\\n\", string(q), string(e)))...)\n\tcontent = append(content, []byte(\"read ANSWER\\n\\n\")...)\n\tcontent = append(content, []byte(\"if test \\\"$ANSWER\\\" != \\\"OK\\\"; then\\n\")...)\n\tcontent = append(content, []byte(\"exit 1\\n\")...)\n\tcontent = append(content, []byte(\"fi\\n\\n\")...)\n\tif addPasscode {\n\t\tq, _ := json.Marshal([]string{\"Passcode: \"})\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"echo '{\\\"questions\\\":%v,\\\"echos\\\":%v,\\\"check_password\\\":2}'\\n\", string(q), string(e)))...)\n\t\tcontent = append(content, []byte(\"read ANSWER\\n\\n\")...)\n\t\tcontent = append(content, []byte(\"if test \\\"$ANSWER\\\" != \\\"OK\\\"; then\\n\")...)\n\t\tcontent = append(content, []byte(\"exit 1\\n\")...)\n\t\tcontent = append(content, []byte(\"fi\\n\\n\")...)\n\t}\n\tcontent = append(content, []byte(fmt.Sprintf(\"echo '{\\\"auth_result\\\":%v}'\\n\", result))...)\n\treturn content\n}\n\nfunc getKeyboardInteractiveScriptContent(questions []string, sleepTime int, nonJSONResponse bool, result int) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tq, _ := json.Marshal(questions)\n\techos := []bool{}\n\tfor index := range questions {\n\t\techos = append(echos, index%2 == 0)\n\t}\n\te, _ := json.Marshal(echos)\n\tif nonJSONResponse {\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"echo 'questions: %v echos: %v\\n\", string(q), string(e)))...)\n\t} else {\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"echo '{\\\"questions\\\":%v,\\\"echos\\\":%v}'\\n\", string(q), string(e)))...)\n\t}\n\tfor index := range questions {\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"read ANSWER%v\\n\", index))...)\n\t}\n\tif sleepTime > 0 {\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"sleep %v\\n\", sleepTime))...)\n\t}\n\tcontent = append(content, []byte(fmt.Sprintf(\"echo '{\\\"auth_result\\\":%v}'\\n\", result))...)\n\treturn content\n}\n\nfunc getExtAuthScriptContent(user dataprovider.User, nonJSONResponse, emptyResponse bool, username string) []byte {\n\textAuthContent := []byte(\"#!/bin/sh\\n\\n\")\n\tif emptyResponse {\n\t\treturn extAuthContent\n\t}\n\textAuthContent = append(extAuthContent, []byte(fmt.Sprintf(\"if test \\\"$SFTPGO_AUTHD_USERNAME\\\" = \\\"%v\\\"; then\\n\", user.Username))...)\n\tif username != \"\" {\n\t\tuser.Username = username\n\t}\n\tu, _ := json.Marshal(user)\n\tif nonJSONResponse {\n\t\textAuthContent = append(extAuthContent, []byte(\"echo 'text response'\\n\")...)\n\t} else {\n\t\textAuthContent = append(extAuthContent, []byte(fmt.Sprintf(\"echo '%v'\\n\", string(u)))...)\n\t}\n\textAuthContent = append(extAuthContent, []byte(\"else\\n\")...)\n\tif nonJSONResponse {\n\t\textAuthContent = append(extAuthContent, []byte(\"echo 'text response'\\n\")...)\n\t} else {\n\t\textAuthContent = append(extAuthContent, []byte(\"echo '{\\\"username\\\":\\\"\\\"}'\\n\")...)\n\t}\n\textAuthContent = append(extAuthContent, []byte(\"fi\\n\")...)\n\treturn extAuthContent\n}\n\nfunc getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tif nonJSONResponse {\n\t\tcontent = append(content, []byte(\"echo 'text response'\\n\")...)\n\t\treturn content\n\t}\n\tif len(user.Username) > 0 {\n\t\tu, _ := json.Marshal(user)\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"echo '%v'\\n\", string(u)))...)\n\t}\n\treturn content\n}\n\nfunc getExitCodeScriptContent(exitCode int) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tcontent = append(content, []byte(fmt.Sprintf(\"exit %v\", exitCode))...)\n\treturn content\n}\n\nfunc getCheckPwdScriptsContents(status int, toVerify string) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tcontent = append(content, []byte(fmt.Sprintf(\"echo '{\\\"status\\\":%v,\\\"to_verify\\\":\\\"%v\\\"}'\\n\", status, toVerify))...)\n\tif status > 0 {\n\t\tcontent = append(content, []byte(\"exit 0\")...)\n\t} else {\n\t\tcontent = append(content, []byte(\"exit 1\")...)\n\t}\n\treturn content\n}\n\nfunc printLatestLogs(maxNumberOfLines int) {\n\tvar lines []string\n\tf, err := os.Open(logFilePath)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer f.Close()\n\tscanner := bufio.NewScanner(f)\n\tfor scanner.Scan() {\n\t\tlines = append(lines, scanner.Text()+\"\\r\\n\")\n\t\tfor len(lines) > maxNumberOfLines {\n\t\t\tlines = lines[1:]\n\t\t}\n\t}\n\tif scanner.Err() != nil {\n\t\tlogger.WarnToConsole(\"Unable to print latest logs: %v\", scanner.Err())\n\t\treturn\n\t}\n\tfor _, line := range lines {\n\t\tlogger.DebugToConsole(\"%s\", line)\n\t}\n}\n\nfunc getHostKeyFingerprint(name string) (string, error) {\n\tprivateBytes, err := os.ReadFile(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tprivate, err := ssh.ParsePrivateKey(privateBytes)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn ssh.FingerprintSHA256(private.PublicKey()), nil\n}\n\nfunc getHostKeysFingerprints(hostKeys []string) {\n\tfor _, k := range hostKeys {\n\t\tfp, err := getHostKeyFingerprint(filepath.Join(configDir, k))\n\t\tif err != nil {\n\t\t\tlogger.ErrorToConsole(\"unable to get fingerprint for host key %q: %v\", k, err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\thostKeyFPs = append(hostKeyFPs, fp)\n\t}\n}\n\nfunc createInitialFiles(scriptArgs string) {\n\tpubKeyPath = filepath.Join(homeBasePath, \"ssh_key.pub\")\n\tprivateKeyPath = filepath.Join(homeBasePath, \"ssh_key\")\n\ttrustedCAUserKey = filepath.Join(homeBasePath, \"ca_user_key\")\n\tgitWrapPath = filepath.Join(homeBasePath, \"gitwrap.sh\")\n\textAuthPath = filepath.Join(homeBasePath, \"extauth.sh\")\n\tpreLoginPath = filepath.Join(homeBasePath, \"prelogin.sh\")\n\tpostConnectPath = filepath.Join(homeBasePath, \"postconnect.sh\")\n\tcheckPwdPath = filepath.Join(homeBasePath, \"checkpwd.sh\")\n\tpreDownloadPath = filepath.Join(homeBasePath, \"predownload.sh\")\n\tpreUploadPath = filepath.Join(homeBasePath, \"preupload.sh\")\n\trevokeUserCerts = filepath.Join(homeBasePath, \"revoked_certs.json\")\n\terr := os.WriteFile(pubKeyPath, []byte(testPubKey+\"\\n\"), 0600)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"unable to save public key to file: %v\", err)\n\t}\n\terr = os.WriteFile(privateKeyPath, []byte(testPrivateKey+\"\\n\"), 0600)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"unable to save private key to file: %v\", err)\n\t}\n\terr = os.WriteFile(gitWrapPath, []byte(fmt.Sprintf(\"%v -i %v -oStrictHostKeyChecking=no %v\\n\",\n\t\tsshPath, privateKeyPath, scriptArgs)), os.ModePerm)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"unable to save gitwrap shell script: %v\", err)\n\t}\n\terr = os.WriteFile(trustedCAUserKey, []byte(testCAUserKey), 0600)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"unable to save trusted CA user key: %v\", err)\n\t}\n\terr = os.WriteFile(revokeUserCerts, []byte(`[]`), 0644)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"unable to save revoked user certs: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/sftpd/ssh_cmd.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/shlex\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\tscpCmdName          = \"scp\"\n\tsshCommandLogSender = \"SSHCommand\"\n)\n\ntype sshCommand struct {\n\tcommand    string\n\targs       []string\n\tconnection *Connection\n\tstartTime  time.Time\n}\n\nfunc processSSHCommand(payload []byte, connection *Connection, enabledSSHCommands []string) bool {\n\tvar msg sshSubsystemExecMsg\n\tif err := ssh.Unmarshal(payload, &msg); err == nil {\n\t\tname, args, err := parseCommandPayload(msg.Command)\n\t\tconnection.Log(logger.LevelDebug, \"new ssh command: %q args: %v num args: %d user: %s, error: %v\",\n\t\t\tname, args, len(args), connection.User.Username, err)\n\t\tif err == nil && slices.Contains(enabledSSHCommands, name) {\n\t\t\tconnection.command = msg.Command\n\t\t\tif name == scpCmdName && len(args) >= 2 {\n\t\t\t\tconnection.SetProtocol(common.ProtocolSCP)\n\t\t\t\tscpCommand := scpCommand{\n\t\t\t\t\tsshCommand: sshCommand{\n\t\t\t\t\t\tcommand:    name,\n\t\t\t\t\t\tconnection: connection,\n\t\t\t\t\t\tstartTime:  time.Now(),\n\t\t\t\t\t\targs:       args},\n\t\t\t\t}\n\t\t\t\tgo scpCommand.handle() //nolint:errcheck\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif name != scpCmdName {\n\t\t\t\tconnection.SetProtocol(common.ProtocolSSH)\n\t\t\t\tsshCommand := sshCommand{\n\t\t\t\t\tcommand:    name,\n\t\t\t\t\tconnection: connection,\n\t\t\t\t\tstartTime:  time.Now(),\n\t\t\t\t\targs:       args,\n\t\t\t\t}\n\t\t\t\tgo sshCommand.handle() //nolint:errcheck\n\t\t\t\treturn true\n\t\t\t}\n\t\t} else {\n\t\t\tconnection.Log(logger.LevelInfo, \"ssh command not enabled/supported: %q\", name)\n\t\t}\n\t}\n\terr := connection.CloseFS()\n\tconnection.Log(logger.LevelError, \"unable to unmarshal ssh command, close fs, err: %v\", err)\n\treturn false\n}\n\nfunc (c *sshCommand) handle() (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlogger.Error(logSender, \"\", \"panic in handle ssh command: %q stack trace: %v\", r, string(debug.Stack()))\n\t\t\terr = common.ErrGenericFailure\n\t\t}\n\t}()\n\tif err := common.Connections.Add(c.connection); err != nil {\n\t\tdefer c.connection.CloseFS() //nolint:errcheck\n\t\tlogger.Info(logSender, \"\", \"unable to add SSH command connection: %v\", err)\n\t\treturn c.sendErrorResponse(err)\n\t}\n\tdefer common.Connections.Remove(c.connection.GetID())\n\n\tc.connection.UpdateLastActivity()\n\tif slices.Contains(sshHashCommands, c.command) {\n\t\treturn c.handleHashCommands()\n\t} else if c.command == \"cd\" {\n\t\tc.sendExitStatus(nil)\n\t} else if c.command == \"pwd\" {\n\t\t// hard coded response to the start directory\n\t\tc.connection.channel.Write([]byte(util.CleanPath(c.connection.User.Filters.StartDirectory) + \"\\n\")) //nolint:errcheck\n\t\tc.sendExitStatus(nil)\n\t} else if c.command == \"sftpgo-copy\" {\n\t\treturn c.handleSFTPGoCopy()\n\t} else if c.command == \"sftpgo-remove\" {\n\t\treturn c.handleSFTPGoRemove()\n\t}\n\treturn\n}\n\nfunc (c *sshCommand) handleSFTPGoCopy() error {\n\tsshSourcePath := c.getSourcePath()\n\tsshDestPath := c.getDestPath()\n\tif sshSourcePath == \"\" || sshDestPath == \"\" || len(c.args) != 2 {\n\t\treturn c.sendErrorResponse(errors.New(\"usage sftpgo-copy <source dir path> <destination dir path>\"))\n\t}\n\tc.connection.Log(logger.LevelDebug, \"requested copy %q -> %q\", sshSourcePath, sshDestPath)\n\tif err := c.connection.Copy(sshSourcePath, sshDestPath); err != nil {\n\t\treturn c.sendErrorResponse(err)\n\t}\n\tc.connection.channel.Write([]byte(\"OK\\n\")) //nolint:errcheck\n\tc.sendExitStatus(nil)\n\treturn nil\n}\n\nfunc (c *sshCommand) handleSFTPGoRemove() error {\n\tsshDestPath, err := c.getRemovePath()\n\tif err != nil {\n\t\treturn c.sendErrorResponse(err)\n\t}\n\tif err := c.connection.RemoveAll(sshDestPath); err != nil {\n\t\treturn c.sendErrorResponse(err)\n\t}\n\tc.connection.channel.Write([]byte(\"OK\\n\")) //nolint:errcheck\n\tc.sendExitStatus(nil)\n\treturn nil\n}\n\nfunc (c *sshCommand) handleHashCommands() error {\n\tvar h hash.Hash\n\tswitch c.command {\n\tcase \"md5sum\":\n\t\th = md5.New()\n\tcase \"sha1sum\":\n\t\th = sha1.New()\n\tcase \"sha256sum\":\n\t\th = sha256.New()\n\tcase \"sha384sum\":\n\t\th = sha512.New384()\n\tdefault:\n\t\th = sha512.New()\n\t}\n\tvar response string\n\tif len(c.args) == 0 {\n\t\t// without args we need to read the string to hash from stdin\n\t\tbuf := make([]byte, 4096)\n\t\tn, err := c.connection.channel.Read(buf)\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn c.sendErrorResponse(err)\n\t\t}\n\t\th.Write(buf[:n]) //nolint:errcheck\n\t\tresponse = fmt.Sprintf(\"%x  -\\n\", h.Sum(nil))\n\t} else {\n\t\tsshPath := c.getDestPath()\n\t\tif ok, policy := c.connection.User.IsFileAllowed(sshPath); !ok {\n\t\t\tc.connection.Log(logger.LevelInfo, \"hash not allowed for file %q\", sshPath)\n\t\t\treturn c.sendErrorResponse(c.connection.GetErrorForDeniedFile(policy))\n\t\t}\n\t\tfs, fsPath, err := c.connection.GetFsAndResolvedPath(sshPath)\n\t\tif err != nil {\n\t\t\treturn c.sendErrorResponse(err)\n\t\t}\n\t\tif !c.connection.User.HasPerm(dataprovider.PermListItems, sshPath) {\n\t\t\treturn c.sendErrorResponse(c.connection.GetPermissionDeniedError())\n\t\t}\n\t\thash, err := c.computeHashForFile(fs, h, fsPath)\n\t\tif err != nil {\n\t\t\treturn c.sendErrorResponse(c.connection.GetFsError(fs, err))\n\t\t}\n\t\tresponse = fmt.Sprintf(\"%v  %v\\n\", hash, sshPath)\n\t}\n\tc.connection.channel.Write([]byte(response)) //nolint:errcheck\n\tc.sendExitStatus(nil)\n\treturn nil\n}\n\n// for the supported commands, the destination path, if any, is the last argument\nfunc (c *sshCommand) getDestPath() string {\n\tif len(c.args) == 0 {\n\t\treturn \"\"\n\t}\n\treturn c.cleanCommandPath(c.args[len(c.args)-1])\n}\n\n// for the supported commands, the destination path, if any, is the second-last argument\nfunc (c *sshCommand) getSourcePath() string {\n\tif len(c.args) < 2 {\n\t\treturn \"\"\n\t}\n\treturn c.cleanCommandPath(c.args[len(c.args)-2])\n}\n\nfunc (c *sshCommand) cleanCommandPath(name string) string {\n\tname = strings.Trim(name, \"'\")\n\tname = strings.Trim(name, \"\\\"\")\n\tresult := c.connection.User.GetCleanedPath(name)\n\tif strings.HasSuffix(name, \"/\") && !strings.HasSuffix(result, \"/\") {\n\t\tresult += \"/\"\n\t}\n\treturn result\n}\n\nfunc (c *sshCommand) getRemovePath() (string, error) {\n\tsshDestPath := c.getDestPath()\n\tif sshDestPath == \"\" || len(c.args) != 1 {\n\t\terr := errors.New(\"usage sftpgo-remove <destination path>\")\n\t\treturn \"\", err\n\t}\n\tif len(sshDestPath) > 1 {\n\t\tsshDestPath = strings.TrimSuffix(sshDestPath, \"/\")\n\t}\n\treturn sshDestPath, nil\n}\n\nfunc (c *sshCommand) sendErrorResponse(err error) error {\n\terrorString := fmt.Sprintf(\"%v: %v %v\\n\", c.command, c.getDestPath(), err)\n\tc.connection.channel.Write([]byte(errorString)) //nolint:errcheck\n\tc.sendExitStatus(err)\n\treturn err\n}\n\nfunc (c *sshCommand) sendExitStatus(err error) {\n\tstatus := uint32(0)\n\tvCmdPath := c.getDestPath()\n\tcmdPath := \"\"\n\ttargetPath := \"\"\n\tvTargetPath := \"\"\n\tif c.command == \"sftpgo-copy\" {\n\t\tvTargetPath = vCmdPath\n\t\tvCmdPath = c.getSourcePath()\n\t}\n\tif err != nil {\n\t\tstatus = uint32(1)\n\t\tc.connection.Log(logger.LevelError, \"command failed: %q args: %v user: %s err: %v\",\n\t\t\tc.command, c.args, c.connection.User.Username, err)\n\t}\n\texitStatus := sshSubsystemExitStatus{\n\t\tStatus: status,\n\t}\n\t_, errClose := c.connection.channel.(ssh.Channel).SendRequest(\"exit-status\", false, ssh.Marshal(&exitStatus))\n\tc.connection.Log(logger.LevelDebug, \"exit status sent, error: %v\", errClose)\n\tc.connection.channel.Close()\n\t// for scp we notify single uploads/downloads\n\tif c.command != scpCmdName {\n\t\telapsed := time.Since(c.startTime).Nanoseconds() / 1000000\n\t\tmetric.SSHCommandCompleted(err)\n\t\tif vCmdPath != \"\" {\n\t\t\t_, p, errFs := c.connection.GetFsAndResolvedPath(vCmdPath)\n\t\t\tif errFs == nil {\n\t\t\t\tcmdPath = p\n\t\t\t}\n\t\t}\n\t\tif vTargetPath != \"\" {\n\t\t\t_, p, errFs := c.connection.GetFsAndResolvedPath(vTargetPath)\n\t\t\tif errFs == nil {\n\t\t\t\ttargetPath = p\n\t\t\t}\n\t\t}\n\t\tcommon.ExecuteActionNotification(c.connection.BaseConnection, common.OperationSSHCmd, cmdPath, vCmdPath, //nolint:errcheck\n\t\t\ttargetPath, vTargetPath, c.command, 0, err, elapsed, nil)\n\t\tif err == nil {\n\t\t\tlogger.CommandLog(sshCommandLogSender, cmdPath, targetPath, c.connection.User.Username, \"\", c.connection.ID,\n\t\t\t\tcommon.ProtocolSSH, -1, -1, \"\", \"\", c.connection.command, -1, c.connection.GetLocalAddress(),\n\t\t\t\tc.connection.GetRemoteAddress(), elapsed)\n\t\t}\n\t}\n}\n\nfunc (c *sshCommand) computeHashForFile(fs vfs.Fs, hasher hash.Hash, path string) (string, error) {\n\thash := \"\"\n\tf, r, _, err := fs.Open(path, 0)\n\tif err != nil {\n\t\treturn hash, err\n\t}\n\tvar reader io.ReadCloser\n\tif f != nil {\n\t\treader = f\n\t} else {\n\t\treader = r\n\t}\n\tdefer reader.Close()\n\t_, err = io.Copy(hasher, reader)\n\tif err == nil {\n\t\thash = fmt.Sprintf(\"%x\", hasher.Sum(nil))\n\t}\n\treturn hash, err\n}\n\nfunc parseCommandPayload(command string) (string, []string, error) {\n\tparts, err := shlex.Split(command)\n\tif err == nil && len(parts) == 0 {\n\t\terr = fmt.Errorf(\"invalid command: %q\", command)\n\t}\n\tif err != nil {\n\t\treturn \"\", []string{}, err\n\t}\n\tif len(parts) < 2 {\n\t\treturn parts[0], []string{}, nil\n\t}\n\treturn parts[0], parts[1:], nil\n}\n"
  },
  {
    "path": "internal/sftpd/transfer.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage sftpd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\ntype writerAtCloser interface {\n\tio.WriterAt\n\tio.Closer\n}\n\ntype readerAtCloser interface {\n\tio.ReaderAt\n\tio.Closer\n}\n\ntype failingReader struct {\n\tinnerReader readerAtCloser\n\terrRead     error\n}\n\nfunc (r *failingReader) ReadAt(_ []byte, _ int64) (n int, err error) {\n\treturn 0, r.errRead\n}\n\nfunc (r *failingReader) Close() error {\n\tif r.innerReader == nil {\n\t\treturn nil\n\t}\n\treturn r.innerReader.Close()\n}\n\n// transfer defines the transfer details.\n// It implements the io.ReaderAt and io.WriterAt interfaces to handle SFTP downloads and uploads\ntype transfer struct {\n\t*common.BaseTransfer\n\twriterAt   writerAtCloser\n\treaderAt   readerAtCloser\n\tisFinished bool\n}\n\nfunc newTransfer(baseTransfer *common.BaseTransfer, pipeWriter vfs.PipeWriter, pipeReader vfs.PipeReader,\n\terrForRead error) *transfer {\n\tvar writer writerAtCloser\n\tvar reader readerAtCloser\n\tif baseTransfer.File != nil {\n\t\twriter = baseTransfer.File\n\t\tif errForRead == nil {\n\t\t\treader = baseTransfer.File\n\t\t} else {\n\t\t\treader = &failingReader{\n\t\t\t\tinnerReader: baseTransfer.File,\n\t\t\t\terrRead:     errForRead,\n\t\t\t}\n\t\t}\n\t} else if pipeWriter != nil {\n\t\twriter = pipeWriter\n\t} else if pipeReader != nil {\n\t\tif errForRead == nil {\n\t\t\treader = pipeReader\n\t\t} else {\n\t\t\treader = &failingReader{\n\t\t\t\tinnerReader: pipeReader,\n\t\t\t\terrRead:     errForRead,\n\t\t\t}\n\t\t}\n\t}\n\tif baseTransfer.File == nil && errForRead != nil && pipeReader == nil {\n\t\treader = &failingReader{\n\t\t\tinnerReader: nil,\n\t\t\terrRead:     errForRead,\n\t\t}\n\t}\n\treturn &transfer{\n\t\tBaseTransfer: baseTransfer,\n\t\twriterAt:     writer,\n\t\treaderAt:     reader,\n\t\tisFinished:   false,\n\t}\n}\n\n// ReadAt reads len(p) bytes from the File to download starting at byte offset off and updates the bytes sent.\n// It handles download bandwidth throttling too\nfunc (t *transfer) ReadAt(p []byte, off int64) (n int, err error) {\n\tt.Connection.UpdateLastActivity()\n\n\tn, err = t.readerAt.ReadAt(p, off)\n\tt.BytesSent.Add(int64(n))\n\n\tif err == nil {\n\t\terr = t.CheckRead()\n\t}\n\tif err != nil && err != io.EOF {\n\t\tif t.GetType() == common.TransferDownload {\n\t\t\tt.TransferError(err)\n\t\t}\n\t\terr = t.ConvertError(err)\n\t\treturn\n\t}\n\tt.HandleThrottle()\n\treturn\n}\n\n// WriteAt writes len(p) bytes to the uploaded file starting at byte offset off and updates the bytes received.\n// It handles upload bandwidth throttling too\nfunc (t *transfer) WriteAt(p []byte, off int64) (n int, err error) {\n\tt.Connection.UpdateLastActivity()\n\tif off < t.MinWriteOffset {\n\t\terr := fmt.Errorf(\"invalid write offset: %v minimum valid value: %v\", off, t.MinWriteOffset)\n\t\tt.TransferError(err)\n\t\treturn 0, err\n\t}\n\n\tn, err = t.writerAt.WriteAt(p, off)\n\tt.BytesReceived.Add(int64(n))\n\n\tif err == nil {\n\t\terr = t.CheckWrite()\n\t}\n\tif err != nil {\n\t\tt.TransferError(err)\n\t\terr = t.ConvertError(err)\n\t\treturn\n\t}\n\tt.HandleThrottle()\n\treturn\n}\n\n// Close it is called when the transfer is completed.\n// It closes the underlying file, logs the transfer info, updates the user quota (for uploads)\n// and executes any defined action.\n// If there is an error no action will be executed and, in atomic mode, we try to delete\n// the temporary file\nfunc (t *transfer) Close() error {\n\tif err := t.setFinished(); err != nil {\n\t\treturn err\n\t}\n\terr := t.closeIO()\n\terrBaseClose := t.BaseTransfer.Close()\n\tif errBaseClose != nil {\n\t\terr = errBaseClose\n\t}\n\treturn t.Connection.GetFsError(t.Fs, err)\n}\n\nfunc (t *transfer) closeIO() error {\n\tvar err error\n\tif t.File != nil {\n\t\terr = t.File.Close()\n\t} else if t.writerAt != nil {\n\t\terr = t.writerAt.Close()\n\t\tt.Lock()\n\t\t// we set ErrTransfer here so quota is not updated, in this case the uploads are atomic\n\t\tif err != nil && t.ErrTransfer == nil {\n\t\t\tt.ErrTransfer = err\n\t\t}\n\t\tt.Unlock()\n\t} else if t.readerAt != nil {\n\t\terr = t.readerAt.Close()\n\t\tif metadater, ok := t.readerAt.(vfs.Metadater); ok {\n\t\t\tt.SetMetadata(metadater.Metadata())\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (t *transfer) setFinished() error {\n\tt.Lock()\n\tdefer t.Unlock()\n\tif t.isFinished {\n\t\treturn common.ErrTransferClosed\n\t}\n\tt.isFinished = true\n\treturn nil\n}\n"
  },
  {
    "path": "internal/smtp/oauth2.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package smtp provides supports for sending emails\npackage smtp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\t\"golang.org/x/oauth2/microsoft\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// Supported OAuth2 providers\nconst (\n\tOAuth2ProviderGoogle = iota\n\tOAuth2ProviderMicrosoft\n)\n\nvar supportedOAuth2Providers = []int{OAuth2ProviderGoogle, OAuth2ProviderMicrosoft}\n\n// OAuth2Config defines OAuth2 settings\ntype OAuth2Config struct {\n\tProvider int `json:\"provider\" mapstructure:\"provider\"`\n\t// Tenant for Microsoft provider, if empty \"common\" is used\n\tTenant string `json:\"tenant\" mapstructure:\"tenant\"`\n\t// ClientID is the application's ID\n\tClientID string `json:\"client_id\" mapstructure:\"client_id\"`\n\t// ClientSecret is the application's secret\n\tClientSecret string `json:\"client_secret\" mapstructure:\"client_secret\"`\n\t// Token to use to get/renew access tokens\n\tRefreshToken string `json:\"refresh_token\" mapstructure:\"refresh_token\"`\n\tmu           *sync.RWMutex\n\tconfig       *oauth2.Config\n\taccessToken  *oauth2.Token\n}\n\n// Validate validates and initializes the configuration\nfunc (c *OAuth2Config) Validate() error {\n\tif !slices.Contains(supportedOAuth2Providers, c.Provider) {\n\t\treturn fmt.Errorf(\"smtp oauth2: unsupported provider %d\", c.Provider)\n\t}\n\tif c.ClientID == \"\" {\n\t\treturn errors.New(\"smtp oauth2: client id is required\")\n\t}\n\tif c.ClientSecret == \"\" {\n\t\treturn errors.New(\"smtp oauth2: client secret is required\")\n\t}\n\tif c.RefreshToken == \"\" {\n\t\treturn errors.New(\"smtp oauth2: refresh token is required\")\n\t}\n\tc.initialize()\n\treturn nil\n}\n\nfunc (c *OAuth2Config) isEqual(other *OAuth2Config) bool {\n\tif c.Provider != other.Provider {\n\t\treturn false\n\t}\n\tif c.Tenant != other.Tenant {\n\t\treturn false\n\t}\n\tif c.ClientID != other.ClientID {\n\t\treturn false\n\t}\n\tif c.ClientSecret != other.ClientSecret {\n\t\treturn false\n\t}\n\tif c.RefreshToken != other.RefreshToken {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (c *OAuth2Config) getAccessToken() (string, error) {\n\tc.mu.RLock()\n\tif c.accessToken.Expiry.After(time.Now().Add(30 * time.Second)) {\n\t\taccessToken := c.accessToken.AccessToken\n\t\tc.mu.RUnlock()\n\n\t\treturn accessToken, nil\n\t}\n\tlogger.Debug(logSender, \"\", \"renew oauth2 token required, current token expires at %s\", c.accessToken.Expiry)\n\ttoken := new(oauth2.Token)\n\t*token = *c.accessToken\n\tc.mu.RUnlock()\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tnewToken, err := c.config.TokenSource(ctx, token).Token()\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to get new token: %v\", err)\n\t\treturn \"\", err\n\t}\n\taccessToken := newToken.AccessToken\n\trefreshToken := newToken.RefreshToken\n\tif refreshToken != \"\" && refreshToken != token.RefreshToken {\n\t\tc.mu.Lock()\n\t\tc.RefreshToken = refreshToken\n\t\tc.accessToken = newToken\n\t\tc.mu.Unlock()\n\n\t\tlogger.Debug(logSender, \"\", \"oauth2 refresh token changed\")\n\t\tgo updateRefreshToken(refreshToken)\n\t}\n\tif accessToken != token.AccessToken {\n\t\tc.mu.Lock()\n\t\tc.accessToken = newToken\n\t\tc.mu.Unlock()\n\n\t\tlogger.Debug(logSender, \"\", \"new oauth2 token saved, expires at %s\", c.accessToken.Expiry)\n\t}\n\treturn accessToken, nil\n}\n\nfunc (c *OAuth2Config) initialize() {\n\tc.mu = new(sync.RWMutex)\n\tc.config = c.GetOAuth2()\n\tc.accessToken = &oauth2.Token{\n\t\tTokenType:    \"Bearer\",\n\t\tRefreshToken: c.RefreshToken,\n\t}\n}\n\n// GetOAuth2 returns the oauth2 configuration for the provided parameters.\nfunc (c *OAuth2Config) GetOAuth2() *oauth2.Config {\n\tvar endpoint oauth2.Endpoint\n\tvar scopes []string\n\n\tswitch c.Provider {\n\tcase OAuth2ProviderMicrosoft:\n\t\tendpoint = microsoft.AzureADEndpoint(c.Tenant)\n\t\tscopes = []string{\"offline_access\", \"https://outlook.office.com/SMTP.Send\"}\n\tdefault:\n\t\tendpoint = google.Endpoint\n\t\tscopes = []string{\"https://mail.google.com/\"}\n\t}\n\n\treturn &oauth2.Config{\n\t\tClientID:     c.ClientID,\n\t\tClientSecret: c.ClientSecret,\n\t\tScopes:       scopes,\n\t\tEndpoint:     endpoint,\n\t}\n}\n"
  },
  {
    "path": "internal/smtp/smtp.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package smtp provides supports for sending emails\npackage smtp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/rs/xid\"\n\t\"github.com/wneessen/go-mail\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\tlogSender = \"smtp\"\n)\n\n// EmailContentType defines the support content types for email body\ntype EmailContentType int\n\n// Supported email body content type\nconst (\n\tEmailContentTypeTextPlain EmailContentType = iota\n\tEmailContentTypeTextHTML\n)\n\nconst (\n\ttemplateEmailDir           = \"email\"\n\ttemplatePasswordReset      = \"reset-password.html\"\n\ttemplatePasswordExpiration = \"password-expiration.html\"\n\tdialTimeout                = 10 * time.Second\n)\n\nvar (\n\tconfig         = &activeConfig{}\n\tinitialConfig  *Config\n\temailTemplates = make(map[string]*template.Template)\n)\n\ntype activeConfig struct {\n\tsync.RWMutex\n\tconfig *Config\n}\n\nfunc (c *activeConfig) isEnabled() bool {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\treturn c.config != nil && c.config.Host != \"\"\n}\n\nfunc (c *activeConfig) Set(cfg *dataprovider.SMTPConfigs) {\n\tvar config *Config\n\tif cfg != nil {\n\t\tconfig = &Config{\n\t\t\tHost:       cfg.Host,\n\t\t\tPort:       cfg.Port,\n\t\t\tFrom:       cfg.From,\n\t\t\tUser:       cfg.User,\n\t\t\tPassword:   cfg.Password.GetPayload(),\n\t\t\tAuthType:   cfg.AuthType,\n\t\t\tEncryption: cfg.Encryption,\n\t\t\tDomain:     cfg.Domain,\n\t\t\tDebug:      cfg.Debug,\n\t\t\tOAuth2: OAuth2Config{\n\t\t\t\tProvider:     cfg.OAuth2.Provider,\n\t\t\t\tTenant:       cfg.OAuth2.Tenant,\n\t\t\t\tClientID:     cfg.OAuth2.ClientID,\n\t\t\t\tClientSecret: cfg.OAuth2.ClientSecret.GetPayload(),\n\t\t\t\tRefreshToken: cfg.OAuth2.RefreshToken.GetPayload(),\n\t\t\t},\n\t\t}\n\t\tconfig.OAuth2.initialize()\n\t}\n\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tif config != nil && config.Host != \"\" {\n\t\tif c.config != nil && c.config.isEqual(config) {\n\t\t\treturn\n\t\t}\n\t\tc.config = config\n\t\tlogger.Info(logSender, \"\", \"activated new config, server %s:%d\", c.config.Host, c.config.Port)\n\t} else {\n\t\tlogger.Debug(logSender, \"\", \"activating initial config\")\n\t\tc.config = initialConfig\n\t\tif c.config == nil || c.config.Host == \"\" {\n\t\t\tlogger.Debug(logSender, \"\", \"configuration disabled, email capabilities will not be available\")\n\t\t}\n\t}\n}\n\nfunc (c *activeConfig) getSMTPClientAndMsg(to, bcc []string, subject, body string, contentType EmailContentType,\n\tattachments ...*mail.File,\n) (*mail.Client, *mail.Msg, error) {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tif c.config == nil || c.config.Host == \"\" {\n\t\treturn nil, nil, errors.New(\"smtp: not configured\")\n\t}\n\n\treturn c.config.getSMTPClientAndMsg(to, bcc, subject, body, contentType, attachments...)\n}\n\nfunc (c *activeConfig) sendEmail(to, bcc []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error {\n\tclient, msg, err := c.getSMTPClientAndMsg(to, bcc, subject, body, contentType, attachments...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancelFn := context.WithTimeout(context.Background(), dialTimeout)\n\tdefer cancelFn()\n\n\treturn client.DialAndSendWithContext(ctx, msg)\n}\n\n// IsEnabled returns true if an SMTP server is configured\nfunc IsEnabled() bool {\n\treturn config.isEnabled()\n}\n\n// Activate sets the specified config as active\nfunc Activate(c *dataprovider.SMTPConfigs) {\n\tconfig.Set(c)\n}\n\n// Config defines the SMTP configuration to use to send emails\ntype Config struct {\n\t// Location of SMTP email server. Leavy empty to disable email sending capabilities\n\tHost string `json:\"host\" mapstructure:\"host\"`\n\t// Port of SMTP email server\n\tPort int `json:\"port\" mapstructure:\"port\"`\n\t// From address, for example \"SFTPGo <sftpgo@example.com>\".\n\t// Many SMTP servers reject emails without a `From` header so, if not set,\n\t// SFTPGo will try to use the username as fallback, this may or may not be appropriate\n\tFrom string `json:\"from\" mapstructure:\"from\"`\n\t// SMTP username\n\tUser string `json:\"user\" mapstructure:\"user\"`\n\t// SMTP password. Leaving both username and password empty the SMTP authentication\n\t// will be disabled\n\tPassword string `json:\"password\" mapstructure:\"password\"`\n\t// 0 Plain\n\t// 1 Login\n\t// 2 CRAM-MD5\n\t// 3 OAuth2\n\tAuthType int `json:\"auth_type\" mapstructure:\"auth_type\"`\n\t// 0 no encryption\n\t// 1 TLS\n\t// 2 start TLS\n\tEncryption int `json:\"encryption\" mapstructure:\"encryption\"`\n\t// Domain to use for HELO command, if empty localhost will be used\n\tDomain string `json:\"domain\" mapstructure:\"domain\"`\n\t// Path to the email templates. This can be an absolute path or a path relative to the config dir.\n\t// Templates are searched within a subdirectory named \"email\" in the specified path\n\tTemplatesPath string `json:\"templates_path\" mapstructure:\"templates_path\"`\n\t// Set to 1 to enable debug logs\n\tDebug int `json:\"debug\" mapstructure:\"debug\"`\n\t// OAuth2 related settings\n\tOAuth2 OAuth2Config `json:\"oauth2\" mapstructure:\"oauth2\"`\n}\n\nfunc (c *Config) isEqual(other *Config) bool {\n\tif c.Host != other.Host {\n\t\treturn false\n\t}\n\tif c.Port != other.Port {\n\t\treturn false\n\t}\n\tif c.From != other.From {\n\t\treturn false\n\t}\n\tif c.User != other.User {\n\t\treturn false\n\t}\n\tif c.Password != other.Password {\n\t\treturn false\n\t}\n\tif c.AuthType != other.AuthType {\n\t\treturn false\n\t}\n\tif c.Encryption != other.Encryption {\n\t\treturn false\n\t}\n\tif c.Domain != other.Domain {\n\t\treturn false\n\t}\n\tif c.Debug != other.Debug {\n\t\treturn false\n\t}\n\treturn c.OAuth2.isEqual(&other.OAuth2)\n}\n\nfunc (c *Config) validate() error {\n\tif c.Port <= 0 || c.Port > 65535 {\n\t\treturn fmt.Errorf(\"smtp: invalid port %d\", c.Port)\n\t}\n\tif c.AuthType < 0 || c.AuthType > 3 {\n\t\treturn fmt.Errorf(\"smtp: invalid auth type %d\", c.AuthType)\n\t}\n\tif c.Encryption < 0 || c.Encryption > 2 {\n\t\treturn fmt.Errorf(\"smtp: invalid encryption %d\", c.Encryption)\n\t}\n\tif c.From == \"\" && c.User == \"\" {\n\t\treturn errors.New(`smtp: from address and user cannot both be empty`)\n\t}\n\tif c.AuthType == 3 {\n\t\treturn c.OAuth2.Validate()\n\t}\n\treturn nil\n}\n\nfunc (c *Config) loadTemplates(configDir string) error {\n\tif c.TemplatesPath == \"\" {\n\t\tlogger.Debug(logSender, \"\", \"templates path empty, using default\")\n\t\tc.TemplatesPath = \"templates\"\n\t}\n\ttemplatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir)\n\tif templatesPath == \"\" {\n\t\treturn fmt.Errorf(\"smtp: invalid templates path %q\", templatesPath)\n\t}\n\tloadTemplates(filepath.Join(templatesPath, templateEmailDir))\n\treturn nil\n}\n\n// Initialize initializes and validates the SMTP configuration\nfunc (c *Config) Initialize(configDir string, isService bool) error {\n\tif !isService && c.Host == \"\" {\n\t\tif err := loadConfigFromProvider(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !config.isEnabled() {\n\t\t\treturn nil\n\t\t}\n\t\t// If not running as a service, templates will only be loaded if required.\n\t\treturn c.loadTemplates(configDir)\n\t}\n\t// In service mode SMTP can be enabled from the WebAdmin at runtime so we\n\t// always load templates.\n\tif err := c.loadTemplates(configDir); err != nil {\n\t\treturn err\n\t}\n\tif c.Host == \"\" {\n\t\treturn loadConfigFromProvider()\n\t}\n\tif err := c.validate(); err != nil {\n\t\treturn err\n\t}\n\tinitialConfig = c\n\tconfig.Set(nil)\n\tlogger.Debug(logSender, \"\", \"configuration successfully initialized, host: %q, port: %d, username: %q, auth: %d, encryption: %d, helo: %q\",\n\t\tc.Host, c.Port, c.User, c.AuthType, c.Encryption, c.Domain)\n\treturn loadConfigFromProvider()\n}\n\nfunc (c *Config) getMailClientOptions() []mail.Option {\n\toptions := []mail.Option{mail.WithPort(c.Port), mail.WithoutNoop()}\n\n\tswitch c.Encryption {\n\tcase 1:\n\t\toptions = append(options, mail.WithSSL())\n\tcase 2:\n\t\toptions = append(options, mail.WithTLSPolicy(mail.TLSMandatory))\n\tdefault:\n\t\toptions = append(options, mail.WithTLSPolicy(mail.NoTLS))\n\t}\n\tif c.User != \"\" {\n\t\toptions = append(options, mail.WithUsername(c.User))\n\t}\n\tif c.Password != \"\" {\n\t\toptions = append(options, mail.WithPassword(c.Password))\n\t}\n\tif c.User != \"\" || c.Password != \"\" {\n\t\tswitch c.AuthType {\n\t\tcase 1:\n\t\t\toptions = append(options, mail.WithSMTPAuth(mail.SMTPAuthLogin))\n\t\tcase 2:\n\t\t\toptions = append(options, mail.WithSMTPAuth(mail.SMTPAuthCramMD5))\n\t\tcase 3:\n\t\t\toptions = append(options, mail.WithSMTPAuth(mail.SMTPAuthXOAUTH2))\n\t\tdefault:\n\t\t\toptions = append(options, mail.WithSMTPAuth(mail.SMTPAuthPlain))\n\t\t}\n\t}\n\tif c.Domain != \"\" {\n\t\toptions = append(options, mail.WithHELO(c.Domain))\n\t}\n\tif c.Debug > 0 {\n\t\toptions = append(options,\n\t\t\tmail.WithLogger(&logger.MailAdapter{\n\t\t\t\tConnectionID: xid.New().String(),\n\t\t\t}),\n\t\t\tmail.WithDebugLog())\n\t}\n\treturn options\n}\n\nfunc (c *Config) getSMTPClientAndMsg(to, bcc []string, subject, body string, contentType EmailContentType,\n\tattachments ...*mail.File) (*mail.Client, *mail.Msg, error) {\n\tmsg := mail.NewMsg()\n\tmsg.SetUserAgent(version.GetServerVersion(\" \", false))\n\n\tvar from string\n\tif c.From != \"\" {\n\t\tfrom = c.From\n\t} else {\n\t\tfrom = c.User\n\t}\n\tif err := msg.From(from); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid from address: %w\", err)\n\t}\n\tif err := msg.To(to...); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif len(bcc) > 0 {\n\t\tif err := msg.Bcc(bcc...); err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\tmsg.Subject(subject)\n\tmsg.SetDate()\n\tmsg.SetMessageID()\n\tmsg.SetAttachments(attachments)\n\n\tswitch contentType {\n\tcase EmailContentTypeTextPlain:\n\t\tmsg.SetBodyString(mail.TypeTextPlain, body)\n\tcase EmailContentTypeTextHTML:\n\t\tmsg.SetBodyString(mail.TypeTextHTML, body)\n\tdefault:\n\t\treturn nil, nil, fmt.Errorf(\"smtp: unsupported body content type %v\", contentType)\n\t}\n\n\tclient, err := mail.NewClient(c.Host, c.getMailClientOptions()...)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"unable to create mail client: %w\", err)\n\t}\n\tif c.AuthType == 3 {\n\t\ttoken, err := c.OAuth2.getAccessToken()\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"unable to get oauth2 access token: %w\", err)\n\t\t}\n\t\tclient.SetPassword(token)\n\t}\n\treturn client, msg, nil\n}\n\n// SendEmail tries to send an email using the specified parameters\nfunc (c *Config) SendEmail(to, bcc []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error {\n\tclient, msg, err := c.getSMTPClientAndMsg(to, bcc, subject, body, contentType, attachments...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancelFn := context.WithTimeout(context.Background(), dialTimeout)\n\tdefer cancelFn()\n\n\treturn client.DialAndSendWithContext(ctx, msg)\n}\n\nfunc loadTemplates(templatesPath string) {\n\tlogger.Debug(logSender, \"\", \"loading templates from %q\", templatesPath)\n\n\tpasswordResetPath := filepath.Join(templatesPath, templatePasswordReset)\n\tpwdResetTmpl := util.LoadTemplate(nil, passwordResetPath)\n\tpasswordExpirationPath := filepath.Join(templatesPath, templatePasswordExpiration)\n\tpwdExpirationTmpl := util.LoadTemplate(nil, passwordExpirationPath)\n\n\temailTemplates[templatePasswordReset] = pwdResetTmpl\n\temailTemplates[templatePasswordExpiration] = pwdExpirationTmpl\n}\n\n// RenderPasswordResetTemplate executes the password reset template\nfunc RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {\n\tif !IsEnabled() {\n\t\treturn errors.New(\"smtp: not configured\")\n\t}\n\treturn emailTemplates[templatePasswordReset].Execute(buf, data)\n}\n\n// RenderPasswordExpirationTemplate executes the password expiration template\nfunc RenderPasswordExpirationTemplate(buf *bytes.Buffer, data any) error {\n\tif !IsEnabled() {\n\t\treturn errors.New(\"smtp: not configured\")\n\t}\n\treturn emailTemplates[templatePasswordExpiration].Execute(buf, data)\n}\n\n// SendEmail tries to send an email using the specified parameters.\nfunc SendEmail(to, bcc []string, subject, body string, contentType EmailContentType, attachments ...*mail.File) error {\n\treturn config.sendEmail(to, bcc, subject, body, contentType, attachments...)\n}\n\nfunc loadConfigFromProvider() error {\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to load config from provider: %v\", err)\n\t\treturn fmt.Errorf(\"smtp: unable to load config from provider: %w\", err)\n\t}\n\tconfigs.SetNilsToEmpty()\n\tif err := configs.SMTP.TryDecrypt(); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to decrypt smtp config: %v\", err)\n\t\treturn fmt.Errorf(\"smtp: unable to decrypt smtp config: %w\", err)\n\t}\n\tconfig.Set(configs.SMTP)\n\treturn nil\n}\n\nfunc updateRefreshToken(token string) {\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to load config from provider, updating refresh token not possible: %v\", err)\n\t\treturn\n\t}\n\tconfigs.SetNilsToEmpty()\n\tif configs.SMTP.IsEmpty() {\n\t\tlogger.Warn(logSender, \"\", \"unable to update refresh token, smtp not configured in the data provider\")\n\t\treturn\n\t}\n\tconfigs.SMTP.OAuth2.RefreshToken = kms.NewPlainSecret(token)\n\tif err := dataprovider.UpdateConfigs(&configs, dataprovider.ActionExecutorSystem, \"\", \"\"); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to save new refresh token: %v\", err)\n\t\treturn\n\t}\n\tlogger.Info(logSender, \"\", \"refresh token updated\")\n}\n"
  },
  {
    "path": "internal/telemetry/router.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage telemetry\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/go-chi/render\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n)\n\nfunc initializeRouter(enableProfiler bool) {\n\trouter = chi.NewRouter()\n\n\trouter.Use(middleware.GetHead)\n\trouter.Use(logger.NewStructuredLogger(logger.GetLogger()))\n\trouter.Use(middleware.Recoverer)\n\n\trouter.Group(func(r chi.Router) {\n\t\tr.Get(\"/healthz\", func(w http.ResponseWriter, r *http.Request) {\n\t\t\trender.PlainText(w, r, \"ok\")\n\t\t})\n\t})\n\n\trouter.Group(func(router chi.Router) {\n\t\trouter.Use(checkAuth)\n\t\tmetric.AddMetricsEndpoint(metricsPath, router)\n\n\t\tif enableProfiler {\n\t\t\tlogger.InfoToConsole(\"enabling the built-in profiler\")\n\t\t\tlogger.Info(logSender, \"\", \"enabling the built-in profiler\")\n\t\t\trouter.Mount(pprofBasePath, middleware.Profiler())\n\t\t}\n\t})\n}\n\nfunc checkAuth(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif !validateCredentials(r) {\n\t\t\tw.Header().Set(common.HTTPAuthenticationHeader, \"Basic realm=\\\"SFTPGo telemetry\\\"\")\n\t\t\thttp.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc validateCredentials(r *http.Request) bool {\n\tif !httpAuth.IsEnabled() {\n\t\treturn true\n\t}\n\tusername, password, ok := r.BasicAuth()\n\tif !ok {\n\t\treturn false\n\t}\n\treturn httpAuth.ValidateCredentials(username, password)\n}\n"
  },
  {
    "path": "internal/telemetry/telemetry.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package telemetry provides telemetry information for SFTPGo, such as:\n//   - health information (for health checks)\n//   - metrics\n//   - profiling information\npackage telemetry\n\nimport (\n\t\"crypto/tls\"\n\t\"log\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\tlogSender     = \"telemetry\"\n\tmetricsPath   = \"/metrics\"\n\tpprofBasePath = \"/debug\"\n)\n\nvar (\n\trouter   *chi.Mux\n\thttpAuth common.HTTPAuthProvider\n\tcertMgr  *common.CertManager\n)\n\n// Conf telemetry server configuration.\ntype Conf struct {\n\t// The port used for serving HTTP requests. 0 disable the HTTP server. Default: 0\n\tBindPort int `json:\"bind_port\" mapstructure:\"bind_port\"`\n\t// The address to listen on. A blank value means listen on all available network interfaces. Default: \"127.0.0.1\"\n\tBindAddress string `json:\"bind_address\" mapstructure:\"bind_address\"`\n\t// Enable the built-in profiler.\n\t// The profiler will be accessible via HTTP/HTTPS using the base URL \"/debug/pprof/\"\n\tEnableProfiler bool `json:\"enable_profiler\" mapstructure:\"enable_profiler\"`\n\t// Path to a file used to store usernames and password for basic authentication.\n\t// This can be an absolute path or a path relative to the config dir.\n\t// We support HTTP basic authentication and the file format must conform to the one generated using the Apache\n\t// htpasswd tool. The supported password formats are bcrypt ($2y$ prefix) and md5 crypt ($apr1$ prefix).\n\t// If empty HTTP authentication is disabled\n\tAuthUserFile string `json:\"auth_user_file\" mapstructure:\"auth_user_file\"`\n\t// If files containing a certificate and matching private key for the server are provided the server will expect\n\t// HTTPS connections.\n\t// Certificate and key files can be reloaded on demand sending a \"SIGHUP\" signal on Unix based systems and a\n\t// \"paramchange\" request to the running service on Windows.\n\tCertificateFile    string `json:\"certificate_file\" mapstructure:\"certificate_file\"`\n\tCertificateKeyFile string `json:\"certificate_key_file\" mapstructure:\"certificate_key_file\"`\n\t// TLSCipherSuites is a list of supported cipher suites for TLS version 1.2.\n\t// If CipherSuites is nil/empty, a default list of secure cipher suites\n\t// is used, with a preference order based on hardware performance.\n\t// Note that TLS 1.3 ciphersuites are not configurable.\n\t// The supported ciphersuites names are defined here:\n\t//\n\t// https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53\n\t//\n\t// any invalid name will be silently ignored.\n\t// The order matters, the ciphers listed first will be the preferred ones.\n\tTLSCipherSuites []string `json:\"tls_cipher_suites\" mapstructure:\"tls_cipher_suites\"`\n\t// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2\n\tMinTLSVersion int `json:\"min_tls_version\" mapstructure:\"min_tls_version\"`\n\t// HTTP protocols to enable in preference order. Supported values: http/1.1, h2\n\tProtocols []string `json:\"tls_protocols\" mapstructure:\"tls_protocols\"`\n}\n\n// ShouldBind returns true if there service must be started\nfunc (c Conf) ShouldBind() bool {\n\tif c.BindPort > 0 {\n\t\treturn true\n\t}\n\tif filepath.IsAbs(c.BindAddress) && runtime.GOOS != \"windows\" {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Initialize configures and starts the telemetry server.\nfunc (c Conf) Initialize(configDir string) error {\n\tvar err error\n\tlogger.Info(logSender, \"\", \"initializing telemetry server with config %+v\", c)\n\tauthUserFile := getConfigPath(c.AuthUserFile, configDir)\n\thttpAuth, err = common.NewBasicAuthProvider(authUserFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcertificateFile := getConfigPath(c.CertificateFile, configDir)\n\tcertificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)\n\tinitializeRouter(c.EnableProfiler)\n\thttpServer := &http.Server{\n\t\tHandler:           router,\n\t\tReadHeaderTimeout: 30 * time.Second,\n\t\tReadTimeout:       60 * time.Second,\n\t\tWriteTimeout:      60 * time.Second,\n\t\tIdleTimeout:       60 * time.Second,\n\t\tMaxHeaderBytes:    1 << 14, // 16KB\n\t\tErrorLog:          log.New(&logger.StdLoggerWrapper{Sender: logSender}, \"\", 0),\n\t}\n\tif certificateFile != \"\" && certificateKeyFile != \"\" {\n\t\tkeyPairs := []common.TLSKeyPair{\n\t\t\t{\n\t\t\t\tCert: certificateFile,\n\t\t\t\tKey:  certificateKeyFile,\n\t\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t\t},\n\t\t}\n\t\tcertMgr, err = common.NewCertManager(keyPairs, configDir, logSender)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfig := &tls.Config{\n\t\t\tGetCertificate: certMgr.GetCertificateFunc(common.DefaultTLSKeyPaidID),\n\t\t\tMinVersion:     util.GetTLSVersion(c.MinTLSVersion),\n\t\t\tNextProtos:     util.GetALPNProtocols(c.Protocols),\n\t\t\tCipherSuites:   util.GetTLSCiphersFromNames(c.TLSCipherSuites),\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"configured TLS cipher suites: %v\", config.CipherSuites)\n\t\thttpServer.TLSConfig = config\n\t\treturn util.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true, nil, logSender)\n\t}\n\treturn util.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false, nil, logSender)\n}\n\n// ReloadCertificateMgr reloads the certificate manager\nfunc ReloadCertificateMgr() error {\n\tif certMgr != nil {\n\t\treturn certMgr.Reload()\n\t}\n\treturn nil\n}\n\nfunc getConfigPath(name, configDir string) string {\n\tif !util.IsFileInputValid(name) {\n\t\treturn \"\"\n\t}\n\tif name != \"\" && !filepath.IsAbs(name) {\n\t\treturn filepath.Join(configDir, name)\n\t}\n\treturn name\n}\n"
  },
  {
    "path": "internal/telemetry/telemetry_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage telemetry\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n)\n\nconst (\n\thttpsCert = `-----BEGIN CERTIFICATE-----\nMIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw\nRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw\nOTUzMDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD\nVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVqWvrJ51t5OxV0v25NsOgR82CA\nNXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIVCzgWkxiz7XE4lgUwX44FCXZM\n3+JeUbKjUzBRMB0GA1UdDgQWBBRhLw+/o3+Z02MI/d4tmaMui9W16jAfBgNVHSME\nGDAWgBRhLw+/o3+Z02MI/d4tmaMui9W16jAPBgNVHRMBAf8EBTADAQH/MAoGCCqG\nSM49BAMCA2kAMGYCMQDqLt2lm8mE+tGgtjDmtFgdOcI72HSbRQ74D5rYTzgST1rY\n/8wTi5xl8TiFUyLMUsICMQC5ViVxdXbhuG7gX6yEqSkMKZICHpO8hqFwOD/uaFVI\ndV4vKmHUzwK/eIx+8Ay3neE=\n-----END CERTIFICATE-----`\n\thttpsKey = `-----BEGIN EC PARAMETERS-----\nBgUrgQQAIg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDCfMNsN6miEE3rVyUPwElfiJSWaR5huPCzUenZOfJT04GAcQdWvEju3\nUM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq\nWvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV\nCzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=\n-----END EC PRIVATE KEY-----`\n)\n\nfunc TestInitialization(t *testing.T) {\n\tconfigDir := filepath.Join(\".\", \"..\", \"..\")\n\tproviderConf := dataprovider.Config{\n\t\tDriver:      dataprovider.MemoryDataProviderName,\n\t\tBackupsPath: \"backups\",\n\t}\n\terr := dataprovider.Initialize(providerConf, configDir, false)\n\trequire.NoError(t, err)\n\tcommonConfig := common.Configuration{}\n\terr = common.Initialize(commonConfig, 0)\n\trequire.NoError(t, err)\n\tc := Conf{\n\t\tBindPort:       10000,\n\t\tBindAddress:    \"invalid address\",\n\t\tEnableProfiler: false,\n\t}\n\terr = c.Initialize(configDir)\n\trequire.Error(t, err)\n\n\tc.AuthUserFile = \"missing\"\n\terr = c.Initialize(\".\")\n\trequire.Error(t, err)\n\n\terr = ReloadCertificateMgr()\n\trequire.NoError(t, err)\n\n\tc.AuthUserFile = \"\"\n\tc.CertificateFile = \"crt\"\n\tc.CertificateKeyFile = \"key\"\n\n\terr = c.Initialize(\".\")\n\trequire.Error(t, err)\n\n\tcertPath := filepath.Join(os.TempDir(), \"test.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test.key\")\n\terr = os.WriteFile(certPath, []byte(httpsCert), os.ModePerm)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(keyPath, []byte(httpsKey), os.ModePerm)\n\trequire.NoError(t, err)\n\n\tc.CertificateFile = certPath\n\tc.CertificateKeyFile = keyPath\n\n\terr = c.Initialize(\".\")\n\trequire.Error(t, err)\n\n\terr = ReloadCertificateMgr()\n\trequire.NoError(t, err)\n\n\terr = os.Remove(certPath)\n\trequire.NoError(t, err)\n\terr = os.Remove(keyPath)\n\trequire.NoError(t, err)\n}\n\nfunc TestShouldBind(t *testing.T) {\n\tc := Conf{\n\t\tBindPort:       10000,\n\t\tEnableProfiler: false,\n\t}\n\trequire.True(t, c.ShouldBind())\n\n\tc.BindPort = 0\n\trequire.False(t, c.ShouldBind())\n\n\tif runtime.GOOS != \"windows\" {\n\t\tc.BindAddress = \"/absolute/path\"\n\t\trequire.True(t, c.ShouldBind())\n\t}\n}\n\nfunc TestRouter(t *testing.T) {\n\tauthUserFile := filepath.Join(os.TempDir(), \"http_users.txt\")\n\tauthUserData := []byte(\"test1:$2y$05$bcHSED7aO1cfLto6ZdDBOOKzlwftslVhtpIkRhAtSa4GuLmk5mola\\n\")\n\terr := os.WriteFile(authUserFile, authUserData, os.ModePerm)\n\trequire.NoError(t, err)\n\n\thttpAuth, err = common.NewBasicAuthProvider(authUserFile)\n\trequire.NoError(t, err)\n\n\tinitializeRouter(true)\n\ttestServer := httptest.NewServer(router)\n\tdefer testServer.Close()\n\n\treq, err := http.NewRequest(http.MethodGet, \"/healthz\", nil)\n\trequire.NoError(t, err)\n\trr := httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\trequire.Equal(t, http.StatusOK, rr.Code)\n\trequire.Equal(t, \"ok\", rr.Body.String())\n\n\treq, err = http.NewRequest(http.MethodGet, \"/metrics\", nil)\n\trequire.NoError(t, err)\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\trequire.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\treq.SetBasicAuth(\"test1\", \"password1\")\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\trequire.Equal(t, http.StatusOK, rr.Code)\n\n\treq, err = http.NewRequest(http.MethodGet, pprofBasePath+\"/pprof/\", nil)\n\trequire.NoError(t, err)\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\trequire.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\treq.SetBasicAuth(\"test1\", \"password1\")\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\trequire.Equal(t, http.StatusOK, rr.Code)\n\n\thttpAuth, err = common.NewBasicAuthProvider(\"\")\n\trequire.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodGet, \"/metrics\", nil)\n\trequire.NoError(t, err)\n\trr = httptest.NewRecorder()\n\ttestServer.Config.Handler.ServeHTTP(rr, req)\n\trequire.Equal(t, http.StatusOK, rr.Code)\n\n\terr = os.Remove(authUserFile)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/util/errors.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage util\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\nconst (\n\ttemplateLoadErrorHints = \"Try setting the absolute templates path in your configuration file \" +\n\t\t\"or specifying the config directory adding the `-c` flag to the serve options. For example: \" +\n\t\t\"sftpgo serve -c \\\"<path to dir containing the default config file and templates directory>\\\"\"\n)\n\n// MaxRecursion defines the maximum number of allowed recursions\nconst MaxRecursion = 1000\n\n// errors definitions\nvar (\n\tErrValidation       = NewValidationError(\"\")\n\tErrNotFound         = NewRecordNotFoundError(\"\")\n\tErrMethodDisabled   = NewMethodDisabledError(\"\")\n\tErrGeneric          = NewGenericError(\"\")\n\tErrRecursionTooDeep = errors.New(\"recursion too deep\")\n)\n\n// ValidationError raised if input data is not valid\ntype ValidationError struct {\n\terr string\n}\n\n// Validation error details\nfunc (e *ValidationError) Error() string {\n\treturn fmt.Sprintf(\"Validation error: %s\", e.err)\n}\n\n// GetErrorString returns the unmodified error string\nfunc (e *ValidationError) GetErrorString() string {\n\treturn e.err\n}\n\n// Is reports if target matches\nfunc (e *ValidationError) Is(target error) bool {\n\t_, ok := target.(*ValidationError)\n\treturn ok\n}\n\n// NewValidationError returns a validation errors\nfunc NewValidationError(errorString string) *ValidationError {\n\treturn &ValidationError{\n\t\terr: errorString,\n\t}\n}\n\n// RecordNotFoundError raised if a requested object is not found\ntype RecordNotFoundError struct {\n\terr string\n}\n\nfunc (e *RecordNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"not found: %s\", e.err)\n}\n\n// Is reports if target matches\nfunc (e *RecordNotFoundError) Is(target error) bool {\n\t_, ok := target.(*RecordNotFoundError)\n\treturn ok\n}\n\n// NewRecordNotFoundError returns a not found error\nfunc NewRecordNotFoundError(errorString string) *RecordNotFoundError {\n\treturn &RecordNotFoundError{\n\t\terr: errorString,\n\t}\n}\n\n// MethodDisabledError raised if a method is disabled in config file.\n// For example, if user management is disabled, this error is raised\n// every time a user operation is done using the REST API\ntype MethodDisabledError struct {\n\terr string\n}\n\n// Method disabled error details\nfunc (e *MethodDisabledError) Error() string {\n\treturn fmt.Sprintf(\"Method disabled error: %s\", e.err)\n}\n\n// Is reports if target matches\nfunc (e *MethodDisabledError) Is(target error) bool {\n\t_, ok := target.(*MethodDisabledError)\n\treturn ok\n}\n\n// NewMethodDisabledError returns a method disabled error\nfunc NewMethodDisabledError(errorString string) *MethodDisabledError {\n\treturn &MethodDisabledError{\n\t\terr: errorString,\n\t}\n}\n\n// GenericError raised for not well categorized error\ntype GenericError struct {\n\terr string\n}\n\nfunc (e *GenericError) Error() string {\n\treturn e.err\n}\n\n// Is reports if target matches\nfunc (e *GenericError) Is(target error) bool {\n\t_, ok := target.(*GenericError)\n\treturn ok\n}\n\n// NewGenericError returns a generic error\nfunc NewGenericError(errorString string) *GenericError {\n\treturn &GenericError{\n\t\terr: errorString,\n\t}\n}\n"
  },
  {
    "path": "internal/util/i18n.go",
    "content": "// Copyright (C) 2023 Nicola Murino\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, version 3.\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\npackage util\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n)\n\n// localization id for the Web frontend\nconst (\n\tI18nSetupTitle                     = \"title.setup\"\n\tI18nLoginTitle                     = \"title.login\"\n\tI18nShareLoginTitle                = \"title.share_login\"\n\tI18nFilesTitle                     = \"title.files\"\n\tI18nSharesTitle                    = \"title.shares\"\n\tI18nShareAddTitle                  = \"title.add_share\"\n\tI18nShareUpdateTitle               = \"title.update_share\"\n\tI18nProfileTitle                   = \"title.profile\"\n\tI18nUsersTitle                     = \"title.users\"\n\tI18nGroupsTitle                    = \"title.groups\"\n\tI18nFoldersTitle                   = \"title.folders\"\n\tI18nChangePwdTitle                 = \"title.change_password\"\n\tI18n2FATitle                       = \"title.two_factor_auth\"\n\tI18nEditFileTitle                  = \"title.edit_file\"\n\tI18nViewFileTitle                  = \"title.view_file\"\n\tI18nForgotPwdTitle                 = \"title.recovery_password\"\n\tI18nResetPwdTitle                  = \"title.reset_password\"\n\tI18nSharedFilesTitle               = \"title.shared_files\"\n\tI18nShareUploadTitle               = \"title.upload_to_share\"\n\tI18nShareDownloadTitle             = \"title.download_shared_file\"\n\tI18nShareAccessErrorTitle          = \"title.share_access_error\"\n\tI18nInvalidAuthReqTitle            = \"title.invalid_auth_request\"\n\tI18nError403Title                  = \"title.error403\"\n\tI18nError400Title                  = \"title.error400\"\n\tI18nError404Title                  = \"title.error404\"\n\tI18nError416Title                  = \"title.error416\"\n\tI18nError429Title                  = \"title.error429\"\n\tI18nError500Title                  = \"title.error500\"\n\tI18nErrorPDFTitle                  = \"title.errorPDF\"\n\tI18nErrorEditorTitle               = \"title.error_editor\"\n\tI18nAddUserTitle                   = \"title.add_user\"\n\tI18nUpdateUserTitle                = \"title.update_user\"\n\tI18nAddAdminTitle                  = \"title.add_admin\"\n\tI18nUpdateAdminTitle               = \"title.update_admin\"\n\tI18nTemplateUserTitle              = \"title.template_user\"\n\tI18nMaintenanceTitle               = \"title.maintenance\"\n\tI18nConfigsTitle                   = \"title.configs\"\n\tI18nOAuth2Title                    = \"title.oauth2_success\"\n\tI18nOAuth2ErrorTitle               = \"title.oauth2_error\"\n\tI18nSessionsTitle                  = \"title.connections\"\n\tI18nRolesTitle                     = \"title.roles\"\n\tI18nAdminsTitle                    = \"title.admins\"\n\tI18nIPListsTitle                   = \"title.ip_lists\"\n\tI18nAddIPListTitle                 = \"title.add_ip_list\"\n\tI18nUpdateIPListTitle              = \"title.update_ip_list\"\n\tI18nDefenderTitle                  = \"title.defender\"\n\tI18nEventsTitle                    = \"title.logs\"\n\tI18nActionsTitle                   = \"title.event_actions\"\n\tI18nRulesTitle                     = \"title.event_rules\"\n\tI18nAddActionTitle                 = \"title.add_action\"\n\tI18nUpdateActionTitle              = \"title.update_action\"\n\tI18nAddRuleTitle                   = \"title.add_rule\"\n\tI18nUpdateRuleTitle                = \"title.update_rule\"\n\tI18nStatusTitle                    = \"status.desc\"\n\tI18nErrorSetupInstallCode          = \"setup.install_code_mismatch\"\n\tI18nInvalidAuth                    = \"general.invalid_auth_request\"\n\tI18nError429Message                = \"general.error429\"\n\tI18nError400Message                = \"general.error400\"\n\tI18nError403Message                = \"general.error403\"\n\tI18nError404Message                = \"general.error404\"\n\tI18nError416Message                = \"general.error416\"\n\tI18nError500Message                = \"general.error500\"\n\tI18nErrorPDFMessage                = \"general.errorPDF\"\n\tI18nErrorInvalidToken              = \"general.invalid_token\"\n\tI18nErrorInvalidForm               = \"general.invalid_form\"\n\tI18nErrorInvalidCredentials        = \"general.invalid_credentials\"\n\tI18nErrorInvalidCSRF               = \"general.invalid_csrf\"\n\tI18nErrorFsGeneric                 = \"fs.err_generic\"\n\tI18nErrorDirListGeneric            = \"fs.dir_list.err_generic\"\n\tI18nErrorDirList403                = \"fs.dir_list.err_403\"\n\tI18nErrorDirList429                = \"fs.dir_list.err_429\"\n\tI18nErrorDirListUser               = \"fs.dir_list.err_user\"\n\tI18nErrorFsValidation              = \"fs.err_validation\"\n\tI18nErrorChangePwdRequiredFields   = \"change_pwd.required_fields\"\n\tI18nErrorChangePwdNoMatch          = \"change_pwd.no_match\"\n\tI18nErrorChangePwdGeneric          = \"change_pwd.generic\"\n\tI18nErrorChangePwdNoDifferent      = \"change_pwd.no_different\"\n\tI18nErrorChangePwdCurrentNoMatch   = \"change_pwd.current_no_match\"\n\tI18nErrorChangePwdRequired         = \"change_pwd.required\"\n\tI18nErrorUsernameRequired          = \"general.username_required\"\n\tI18nErrorPasswordRequired          = \"general.password_required\"\n\tI18nErrorPermissionsRequired       = \"general.permissions_required\"\n\tI18nErrorGetUser                   = \"general.err_user\"\n\tI18nErrorPwdResetForbidded         = \"login.reset_pwd_forbidden\"\n\tI18nErrorPwdResetNoEmail           = \"login.reset_pwd_no_email\"\n\tI18nErrorPwdResetSendEmail         = \"login.reset_pwd_send_email_err\"\n\tI18nErrorPwdResetGeneric           = \"login.reset_pwd_err_generic\"\n\tI18nErrorProtocolForbidden         = \"general.err_protocol_forbidden\"\n\tI18nErrorPwdLoginForbidden         = \"general.pwd_login_forbidden\"\n\tI18nErrorIPForbidden               = \"general.ip_forbidden\"\n\tI18nErrorConnectionForbidden       = \"general.connection_forbidden\"\n\tI18nErrorReservedUsername          = \"user.username_reserved\"\n\tI18nErrorInvalidEmail              = \"general.email_invalid\"\n\tI18nErrorInvalidInput              = \"general.invalid_input\"\n\tI18nErrorInvalidUser               = \"user.username_invalid\"\n\tI18nErrorInvalidName               = \"general.name_invalid\"\n\tI18nErrorHomeRequired              = \"user.home_required\"\n\tI18nErrorHomeInvalid               = \"user.home_invalid\"\n\tI18nErrorPubKeyInvalid             = \"user.pub_key_invalid\"\n\tI18nErrorPrivKeyInvalid            = \"user.priv_key_invalid\"\n\tI18nErrorKeySizeInvalid            = \"user.key_invalid_size\"\n\tI18nErrorKeyInsecure               = \"user.key_insecure\"\n\tI18nErrorPrimaryGroup              = \"user.err_primary_group\"\n\tI18nErrorDuplicateGroup            = \"user.err_duplicate_group\"\n\tI18nErrorNoPermission              = \"user.no_permissions\"\n\tI18nErrorNoRootPermission          = \"user.no_root_permissions\"\n\tI18nErrorGenericPermission         = \"user.err_permissions_generic\"\n\tI18nError2FAInvalid                = \"user.2fa_invalid\"\n\tI18nErrorRecoveryCodesInvalid      = \"user.recovery_codes_invalid\"\n\tI18nErrorFolderNameRequired        = \"general.foldername_required\"\n\tI18nErrorFolderMountPathRequired   = \"user.folder_path_required\"\n\tI18nErrorDuplicatedFolders         = \"user.folder_duplicated\"\n\tI18nErrorOverlappedFolders         = \"user.folder_overlapped\"\n\tI18nErrorFolderQuotaSizeInvalid    = \"user.folder_quota_size_invalid\"\n\tI18nErrorFolderQuotaFileInvalid    = \"user.folder_quota_file_invalid\"\n\tI18nErrorFolderQuotaInvalid        = \"user.folder_quota_invalid\"\n\tI18nErrorPasswordComplexity        = \"general.err_password_complexity\"\n\tI18nErrorIPFiltersInvalid          = \"user.ip_filters_invalid\"\n\tI18nErrorSourceBWLimitInvalid      = \"user.src_bw_limits_invalid\"\n\tI18nErrorShareExpirationInvalid    = \"user.share_expiration_invalid\"\n\tI18nErrorFilePatternPathInvalid    = \"user.file_pattern_path_invalid\"\n\tI18nErrorFilePatternDuplicated     = \"user.file_pattern_duplicated\"\n\tI18nErrorFilePatternInvalid        = \"user.file_pattern_invalid\"\n\tI18nErrorDisableActive2FA          = \"user.disable_active_2fa\"\n\tI18nErrorPwdChangeConflict         = \"user.pwd_change_conflict\"\n\tI18nError2FAConflict               = \"user.two_factor_conflict\"\n\tI18nErrorLoginAfterReset           = \"login.reset_ok_login_error\"\n\tI18nErrorShareScope                = \"share.scope_invalid\"\n\tI18nErrorShareMaxTokens            = \"share.max_tokens_invalid\"\n\tI18nErrorShareExpiration           = \"share.expiration_invalid\"\n\tI18nErrorShareNoPwd                = \"share.err_no_password\"\n\tI18nErrorShareExpirationOutOfRange = \"share.expiration_out_of_range\"\n\tI18nErrorShareGeneric              = \"share.generic\"\n\tI18nErrorNameRequired              = \"general.name_required\"\n\tI18nErrorSharePathRequired         = \"share.path_required\"\n\tI18nErrorShareWriteScope           = \"share.path_write_scope\"\n\tI18nErrorShareNestedPaths          = \"share.nested_paths\"\n\tI18nErrorShareExpirationPast       = \"share.expiration_past\"\n\tI18nErrorInvalidIPMask             = \"general.allowed_ip_mask_invalid\"\n\tI18nErrorShareUsage                = \"share.usage_exceed\"\n\tI18nErrorShareExpired              = \"share.expired\"\n\tI18nErrorLoginFromIPDenied         = \"login.ip_not_allowed\"\n\tI18nError2FARequired               = \"login.two_factor_required\"\n\tI18nError2FARequiredGeneric        = \"login.two_factor_required_generic\"\n\tI18nErrorNoOIDCFeature             = \"general.no_oidc_feature\"\n\tI18nErrorNoPermissions             = \"general.no_permissions\"\n\tI18nErrorShareBrowsePaths          = \"share.browsable_multiple_paths\"\n\tI18nErrorShareBrowseNoDir          = \"share.browsable_non_dir\"\n\tI18nErrorShareInvalidPath          = \"share.invalid_path\"\n\tI18nErrorPathInvalid               = \"general.path_invalid\"\n\tI18nErrorQuotaRead                 = \"general.err_quota_read\"\n\tI18nErrorEditDir                   = \"general.error_edit_dir\"\n\tI18nErrorEditSize                  = \"general.error_edit_size\"\n\tI18nProfileUpdated                 = \"general.profile_updated\"\n\tI18nShareLoginOK                   = \"general.share_ok\"\n\tI18n2FADisabled                    = \"2fa.disabled\"\n\tI18nOIDCTokenExpired               = \"oidc.token_expired\"\n\tI18nOIDCTokenInvalidAdmin          = \"oidc.token_invalid_webadmin\"\n\tI18nOIDCTokenInvalidUser           = \"oidc.token_invalid_webclient\"\n\tI18nOIDCErrTokenExchange           = \"oidc.token_exchange_err\"\n\tI18nOIDCTokenInvalid               = \"oidc.token_invalid\"\n\tI18nOIDCTokenInvalidRoleAdmin      = \"oidc.role_admin_err\"\n\tI18nOIDCTokenInvalidRoleUser       = \"oidc.role_user_err\"\n\tI18nOIDCErrGetUser                 = \"oidc.get_user_err\"\n\tI18nErrorInvalidQuotaSize          = \"user.invalid_quota_size\"\n\tI18nErrorTimeOfDayInvalid          = \"user.time_of_day_invalid\"\n\tI18nErrorTimeOfDayConflict         = \"user.time_of_day_conflict\"\n\tI18nErrorInvalidMaxFilesize        = \"filters.max_upload_size_invalid\"\n\tI18nErrorInvalidHomeDir            = \"storage.home_dir_invalid\"\n\tI18nErrorBucketRequired            = \"storage.bucket_required\"\n\tI18nErrorRegionRequired            = \"storage.region_required\"\n\tI18nErrorKeyPrefixInvalid          = \"storage.key_prefix_invalid\"\n\tI18nErrorULPartSizeInvalid         = \"storage.ul_part_size_invalid\"\n\tI18nErrorDLPartSizeInvalid         = \"storage.dl_part_size_invalid\"\n\tI18nErrorULConcurrencyInvalid      = \"storage.ul_concurrency_invalid\"\n\tI18nErrorDLConcurrencyInvalid      = \"storage.dl_concurrency_invalid\"\n\tI18nErrorAccessKeyRequired         = \"storage.access_key_required\"\n\tI18nErrorAccessSecretRequired      = \"storage.access_secret_required\"\n\tI18nErrorFsCredentialsRequired     = \"storage.credentials_required\"\n\tI18nErrorContainerRequired         = \"storage.container_required\"\n\tI18nErrorAccountNameRequired       = \"storage.account_name_required\"\n\tI18nErrorSASURLInvalid             = \"storage.sas_url_invalid\"\n\tI18nErrorPassphraseRequired        = \"storage.passphrase_required\"\n\tI18nErrorEndpointInvalid           = \"storage.endpoint_invalid\"\n\tI18nErrorEndpointRequired          = \"storage.endpoint_required\"\n\tI18nErrorFsUsernameRequired        = \"storage.username_required\"\n\tI18nAddGroupTitle                  = \"title.add_group\"\n\tI18nUpdateGroupTitle               = \"title.update_group\"\n\tI18nRoleAddTitle                   = \"title.add_role\"\n\tI18nRoleUpdateTitle                = \"title.update_role\"\n\tI18nErrorInvalidTLSCert            = \"user.tls_cert_invalid\"\n\tI18nAddFolderTitle                 = \"title.add_folder\"\n\tI18nUpdateFolderTitle              = \"title.update_folder\"\n\tI18nTemplateFolderTitle            = \"title.template_folder\"\n\tI18nErrorDuplicatedUsername        = \"general.duplicated_username\"\n\tI18nErrorDuplicatedName            = \"general.duplicated_name\"\n\tI18nErrorDuplicatedIPNet           = \"ip_list.duplicated\"\n\tI18nErrorRoleAdminPerms            = \"admin.role_permissions\"\n\tI18nBackupOK                       = \"maintenance.backup_ok\"\n\tI18nErrorFolderTemplate            = \"virtual_folders.template_no_folder\"\n\tI18nErrorUserTemplate              = \"user.template_no_user\"\n\tI18nConfigsOK                      = \"general.configs_saved\"\n\tI18nOAuth2ErrorVerifyState         = \"oauth2.auth_verify_error\"\n\tI18nOAuth2ErrorValidateState       = \"oauth2.auth_validation_error\"\n\tI18nOAuth2InvalidState             = \"oauth2.auth_invalid\"\n\tI18nOAuth2ErrTokenExchange         = \"oauth2.token_exchange_err\"\n\tI18nOAuth2ErrNoRefreshToken        = \"oauth2.no_refresh_token\"\n\tI18nOAuth2OK                       = \"oauth2.success\"\n\tI18nErrorAdminSelfPerms            = \"admin.self_permissions\"\n\tI18nErrorAdminSelfDisable          = \"admin.self_disable\"\n\tI18nErrorAdminSelfRole             = \"admin.self_role\"\n\tI18nErrorIPInvalid                 = \"ip_list.ip_invalid\"\n\tI18nErrorNetInvalid                = \"ip_list.net_invalid\"\n\tI18nFTPTLSDisabled                 = \"status.tls_disabled\"\n\tI18nFTPTLSExplicit                 = \"status.tls_explicit\"\n\tI18nFTPTLSImplicit                 = \"status.tls_implicit\"\n\tI18nFTPTLSMixed                    = \"status.tls_mixed\"\n\tI18nErrorBackupFile                = \"maintenance.backup_invalid_file\"\n\tI18nErrorRestore                   = \"maintenance.restore_error\"\n\tI18nErrorACMEGeneric               = \"acme.generic_error\"\n\tI18nErrorSMTPRequiredFields        = \"smtp.err_required_fields\"\n\tI18nErrorClientIDRequired          = \"oauth2.client_id_required\"\n\tI18nErrorClientSecretRequired      = \"oauth2.client_secret_required\"\n\tI18nErrorRefreshTokenRequired      = \"oauth2.refresh_token_required\"\n\tI18nErrorURLRequired               = \"actions.http_url_required\"\n\tI18nErrorURLInvalid                = \"actions.http_url_invalid\"\n\tI18nErrorHTTPPartNameRequired      = \"actions.http_part_name_required\"\n\tI18nErrorHTTPPartBodyRequired      = \"actions.http_part_body_required\"\n\tI18nErrorMultipartBody             = \"actions.http_multipart_body_error\"\n\tI18nErrorMultipartCType            = \"actions.http_multipart_ctype_error\"\n\tI18nErrorPathDuplicated            = \"actions.path_duplicated\"\n\tI18nErrorCommandRequired           = \"actions.command_required\"\n\tI18nErrorCommandInvalid            = \"actions.command_invalid\"\n\tI18nErrorEmailRecipientRequired    = \"actions.email_recipient_required\"\n\tI18nErrorEmailSubjectRequired      = \"actions.email_subject_required\"\n\tI18nErrorEmailBodyRequired         = \"actions.email_body_required\"\n\tI18nErrorRetentionDirRequired      = \"actions.retention_directory_required\"\n\tI18nErrorPathRequired              = \"actions.path_required\"\n\tI18nErrorSourceDestMatch           = \"actions.source_dest_different\"\n\tI18nErrorRootNotAllowed            = \"actions.root_not_allowed\"\n\tI18nErrorArchiveNameRequired       = \"actions.archive_name_required\"\n\tI18nErrorIDPTemplateRequired       = \"actions.idp_template_required\"\n\tI18nActionTypeHTTP                 = \"actions.types.http\"\n\tI18nActionTypeEmail                = \"actions.types.email\"\n\tI18nActionTypeBackup               = \"actions.types.backup\"\n\tI18nActionTypeUserQuotaReset       = \"actions.types.user_quota_reset\"\n\tI18nActionTypeFolderQuotaReset     = \"actions.types.folder_quota_reset\"\n\tI18nActionTypeTransferQuotaReset   = \"actions.types.transfer_quota_reset\"\n\tI18nActionTypeDataRetentionCheck   = \"actions.types.data_retention_check\"\n\tI18nActionTypeFilesystem           = \"actions.types.filesystem\"\n\tI18nActionTypePwdExpirationCheck   = \"actions.types.password_expiration_check\"\n\tI18nActionTypeUserExpirationCheck  = \"actions.types.user_expiration_check\"\n\tI18nActionTypeUserInactivityCheck  = \"actions.types.user_inactivity_check\"\n\tI18nActionTypeIDPCheck             = \"actions.types.idp_check\"\n\tI18nActionTypeCommand              = \"actions.types.command\"\n\tI18nActionTypeRotateLogs           = \"actions.types.rotate_logs\"\n\tI18nActionFsTypeRename             = \"actions.fs_types.rename\"\n\tI18nActionFsTypeDelete             = \"actions.fs_types.delete\"\n\tI18nActionFsTypePathExists         = \"actions.fs_types.path_exists\"\n\tI18nActionFsTypeCompress           = \"actions.fs_types.compress\"\n\tI18nActionFsTypeCopy               = \"actions.fs_types.copy\"\n\tI18nActionFsTypeCreateDirs         = \"actions.fs_types.create_dirs\"\n\tI18nActionThresholdRequired        = \"actions.inactivity_threshold_required\"\n\tI18nActionThresholdsInvalid        = \"actions.inactivity_thresholds_invalid\"\n\tI18nTriggerFsEvent                 = \"rules.triggers.fs_event\"\n\tI18nTriggerProviderEvent           = \"rules.triggers.provider_event\"\n\tI18nTriggerIPBlockedEvent          = \"rules.triggers.ip_blocked\"\n\tI18nTriggerCertificateRenewEvent   = \"rules.triggers.certificate_renewal\"\n\tI18nTriggerOnDemandEvent           = \"rules.triggers.on_demand\"\n\tI18nTriggerIDPLoginEvent           = \"rules.triggers.idp_login\"\n\tI18nTriggerScheduleEvent           = \"rules.triggers.schedule\"\n\tI18nErrorInvalidMinSize            = \"rules.invalid_fs_min_size\"\n\tI18nErrorInvalidMaxSize            = \"rules.invalid_fs_max_size\"\n\tI18nErrorRuleActionRequired        = \"rules.action_required\"\n\tI18nErrorRuleFsEventRequired       = \"rules.fs_event_required\"\n\tI18nErrorRuleProviderEventRequired = \"rules.provider_event_required\"\n\tI18nErrorRuleScheduleRequired      = \"rules.schedule_required\"\n\tI18nErrorRuleScheduleInvalid       = \"rules.schedule_invalid\"\n\tI18nErrorRuleDuplicateActions      = \"rules.duplicate_actions\"\n\tI18nErrorEvSyncFailureActions      = \"rules.sync_failure_actions\"\n\tI18nErrorEvSyncUnsupported         = \"rules.sync_unsupported\"\n\tI18nErrorEvSyncUnsupportedFs       = \"rules.sync_unsupported_fs_event\"\n\tI18nErrorRuleFailureActionsOnly    = \"rules.only_failure_actions\"\n\tI18nErrorRuleSyncActionRequired    = \"rules.sync_action_required\"\n\tI18nErrorInvalidPNG                = \"branding.invalid_png\"\n\tI18nErrorInvalidPNGSize            = \"branding.invalid_png_size\"\n\tI18nErrorInvalidDisclaimerURL      = \"branding.invalid_disclaimer_url\"\n)\n\n// NewI18nError returns a I18nError wrappring the provided error\nfunc NewI18nError(err error, message string, options ...I18nErrorOption) *I18nError {\n\tvar errI18n *I18nError\n\tif errors.As(err, &errI18n) {\n\t\treturn errI18n\n\t}\n\terrI18n = &I18nError{\n\t\terr:     err,\n\t\tMessage: message,\n\t\targs:    nil,\n\t}\n\tfor _, opt := range options {\n\t\topt(errI18n)\n\t}\n\treturn errI18n\n}\n\n// I18nErrorOption defines a functional option type that allows to configure the I18nError.\ntype I18nErrorOption func(*I18nError)\n\n// I18nErrorArgs is a functional option to set I18nError arguments.\nfunc I18nErrorArgs(args map[string]any) I18nErrorOption {\n\treturn func(e *I18nError) {\n\t\te.args = args\n\t}\n}\n\n// I18nError is an error wrapper that add a message to use for localization.\ntype I18nError struct {\n\terr     error\n\tMessage string\n\targs    map[string]any\n}\n\n// Error returns the wrapped error string.\nfunc (e *I18nError) Error() string {\n\treturn e.err.Error()\n}\n\n// Unwrap returns the underlying error\nfunc (e *I18nError) Unwrap() error {\n\treturn e.err\n}\n\n// Is reports if target matches\nfunc (e *I18nError) Is(target error) bool {\n\tif errors.Is(e.err, target) {\n\t\treturn true\n\t}\n\t_, ok := target.(*I18nError)\n\treturn ok\n}\n\n// HasArgs returns true if the error has i18n args.\nfunc (e *I18nError) HasArgs() bool {\n\treturn len(e.args) > 0\n}\n\n// Args returns the provided args in JSON format\nfunc (e *I18nError) Args() string {\n\tif len(e.args) > 0 {\n\t\tdata, err := json.Marshal(e.args)\n\t\tif err == nil {\n\t\t\treturn BytesToString(data)\n\t\t}\n\t}\n\treturn \"{}\"\n}\n"
  },
  {
    "path": "internal/util/resources.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !bundle\n\npackage util\n\nimport (\n\t\"html/template\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// FindSharedDataPath searches for the specified directory name in searchDir\n// and in system-wide shared data directories.\n// If name is an absolute path it is returned unmodified.\nfunc FindSharedDataPath(name, searchDir string) string {\n\tif !IsFileInputValid(name) {\n\t\treturn \"\"\n\t}\n\tif name != \"\" && !filepath.IsAbs(name) {\n\t\tsearchList := []string{searchDir}\n\t\tif additionalSharedDataSearchPath != \"\" {\n\t\t\tsearchList = append(searchList, additionalSharedDataSearchPath)\n\t\t}\n\t\tif runtime.GOOS != osWindows {\n\t\t\tsearchList = append(searchList, \"/usr/share/sftpgo\")\n\t\t\tsearchList = append(searchList, \"/usr/local/share/sftpgo\")\n\t\t}\n\t\tsearchList = RemoveDuplicates(searchList, false)\n\t\tfor _, basePath := range searchList {\n\t\t\tres := filepath.Join(basePath, name)\n\t\t\t_, err := os.Stat(res)\n\t\t\tif err == nil {\n\t\t\t\tlogger.Debug(logSender, \"\", \"found share data path for name %q: %q\", name, res)\n\t\t\t\treturn res\n\t\t\t}\n\t\t}\n\t\treturn filepath.Join(searchDir, name)\n\t}\n\treturn name\n}\n\n// LoadTemplate parses the given template paths.\n// It behaves like template.Must but it writes a log before exiting.\nfunc LoadTemplate(base *template.Template, paths ...string) *template.Template {\n\tif base != nil {\n\t\tbaseTmpl, err := base.Clone()\n\t\tif err != nil {\n\t\t\tshowTemplateLoadingError(err)\n\t\t}\n\t\tt, err := baseTmpl.ParseFiles(paths...)\n\t\tif err != nil {\n\t\t\tshowTemplateLoadingError(err)\n\t\t}\n\t\treturn t\n\t}\n\n\tt, err := template.ParseFiles(paths...)\n\tif err != nil {\n\t\tshowTemplateLoadingError(err)\n\t}\n\treturn t\n}\n\nfunc showTemplateLoadingError(err error) {\n\tlogger.ErrorToConsole(\"error loading required template: %v\", err)\n\tlogger.ErrorToConsole(templateLoadErrorHints)\n\tlogger.Error(logSender, \"\", \"error loading required template: %v\", err)\n\tos.Exit(1)\n}\n"
  },
  {
    "path": "internal/util/resources_embedded.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build bundle\n\npackage util\n\nimport (\n\t\"html/template\"\n\t\"os\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/bundle\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// FindSharedDataPath searches for the specified directory name in searchDir\n// and in system-wide shared data directories.\n// If name is an absolute path it is returned unmodified.\nfunc FindSharedDataPath(name, _ string) string {\n\treturn name\n}\n\n// LoadTemplate parses the given template paths.\n// It behaves like template.Must but it writes a log before exiting.\n// You can optionally provide a base template (e.g. to define some custom functions)\nfunc LoadTemplate(base *template.Template, paths ...string) *template.Template {\n\tvar t *template.Template\n\tvar err error\n\n\ttemplateFs := bundle.GetTemplatesFs()\n\tif base != nil {\n\t\tbase, err = base.Clone()\n\t\tif err == nil {\n\t\t\tt, err = base.ParseFS(templateFs, paths...)\n\t\t}\n\t} else {\n\t\tt, err = template.ParseFS(templateFs, paths...)\n\t}\n\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error loading required template: %v\", err)\n\t\tlogger.ErrorToConsole(templateLoadErrorHints)\n\t\tlogger.Error(logSender, \"\", \"error loading required template: %v\", err)\n\t\tos.Exit(1)\n\t}\n\treturn t\n}\n"
  },
  {
    "path": "internal/util/util.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package util provides some common utility methods\npackage util\n\nimport (\n\t\"bytes\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/subtle\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"io/fs\"\n\t\"math\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\t\"unsafe\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/lithammer/shortuuid/v4\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\nconst (\n\tlogSender    = \"util\"\n\tosWindows    = \"windows\"\n\tpubKeySuffix = \".pub\"\n)\n\nvar (\n\temailRegex = regexp.MustCompile(\"^(?:(?:(?:(?:[a-zA-Z]|\\\\d|[!#\\\\$%&'\\\\*\\\\+\\\\-\\\\/=\\\\?\\\\^_`{\\\\|}~]|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])+(?:\\\\.([a-zA-Z]|\\\\d|[!#\\\\$%&'\\\\*\\\\+\\\\-\\\\/=\\\\?\\\\^_`{\\\\|}~]|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])+)*)|(?:(?:\\\\x22)(?:(?:(?:(?:\\\\x20|\\\\x09)*(?:\\\\x0d\\\\x0a))?(?:\\\\x20|\\\\x09)+)?(?:(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x7f]|\\\\x21|[\\\\x23-\\\\x5b]|[\\\\x5d-\\\\x7e]|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])|(?:(?:[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0d-\\\\x7f]|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}]))))*(?:(?:(?:\\\\x20|\\\\x09)*(?:\\\\x0d\\\\x0a))?(\\\\x20|\\\\x09)+)?(?:\\\\x22))))@(?:(?:(?:[a-zA-Z]|\\\\d|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])|(?:(?:[a-zA-Z]|\\\\d|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])(?:[a-zA-Z]|\\\\d|-|\\\\.|~|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])*(?:[a-zA-Z]|\\\\d|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])))\\\\.)+(?:(?:[a-zA-Z]|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])(?:[a-zA-Z]|\\\\d|-|\\\\.|~|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])*(?:[a-zA-Z]|[\\\\x{00A0}-\\\\x{D7FF}\\\\x{F900}-\\\\x{FDCF}\\\\x{FDF0}-\\\\x{FFEF}])))\\\\.?$\")\n\t// this can be set at build time\n\tadditionalSharedDataSearchPath = \"\"\n\t// CertsBasePath defines base path for certificates obtained using the built-in ACME protocol.\n\t// It is empty is ACME support is disabled\n\tCertsBasePath string\n\t// Defines the TLS ciphers used by default for TLS 1.0-1.2 if no preference is specified.\n\tdefaultTLSCiphers = []uint16{\n\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,\n\t}\n)\n\n// IEC Sizes.\n// kibis of bits\nconst (\n\toneByte = 1 << (iota * 10)\n\tkiByte\n\tmiByte\n\tgiByte\n\ttiByte\n\tpiByte\n\teiByte\n)\n\n// SI Sizes.\nconst (\n\tiByte  = 1\n\tkbByte = iByte * 1000\n\tmByte  = kbByte * 1000\n\tgByte  = mByte * 1000\n\ttByte  = gByte * 1000\n\tpByte  = tByte * 1000\n\teByte  = pByte * 1000\n)\n\nvar bytesSizeTable = map[string]uint64{\n\t\"b\":   oneByte,\n\t\"kib\": kiByte,\n\t\"kb\":  kbByte,\n\t\"mib\": miByte,\n\t\"mb\":  mByte,\n\t\"gib\": giByte,\n\t\"gb\":  gByte,\n\t\"tib\": tiByte,\n\t\"tb\":  tByte,\n\t\"pib\": piByte,\n\t\"pb\":  pByte,\n\t\"eib\": eiByte,\n\t\"eb\":  eByte,\n\t// Without suffix\n\t\"\":   oneByte,\n\t\"ki\": kiByte,\n\t\"k\":  kbByte,\n\t\"mi\": miByte,\n\t\"m\":  mByte,\n\t\"gi\": giByte,\n\t\"g\":  gByte,\n\t\"ti\": tiByte,\n\t\"t\":  tByte,\n\t\"pi\": piByte,\n\t\"p\":  pByte,\n\t\"ei\": eiByte,\n\t\"e\":  eByte,\n}\n\n// IsStringPrefixInSlice searches a string prefix in a slice and returns true\n// if a matching prefix is found\nfunc IsStringPrefixInSlice(obj string, list []string) bool {\n\tfor i := 0; i < len(list); i++ {\n\t\tif strings.HasPrefix(obj, list[i]) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// RemoveDuplicates returns a new slice removing any duplicate element from the initial one\nfunc RemoveDuplicates(obj []string, trim bool) []string {\n\tif len(obj) == 0 {\n\t\treturn obj\n\t}\n\tseen := make(map[string]bool)\n\tvalidIdx := 0\n\tfor _, item := range obj {\n\t\tif trim {\n\t\t\titem = strings.TrimSpace(item)\n\t\t}\n\t\tif !seen[item] {\n\t\t\tseen[item] = true\n\t\t\tobj[validIdx] = item\n\t\t\tvalidIdx++\n\t\t}\n\t}\n\treturn obj[:validIdx]\n}\n\n// IsNameValid validates that a name/username contains only safe characters.\nfunc IsNameValid(name string) bool {\n\tif name == \"\" {\n\t\treturn false\n\t}\n\tif len(name) > 255 {\n\t\treturn false\n\t}\n\tfor _, r := range name {\n\t\tif unicode.IsControl(r) {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch r {\n\t\tcase '/', '\\\\':\n\t\t\treturn false\n\t\tcase ':', '*', '?', '\"', '<', '>', '|':\n\t\t\treturn false\n\t\t}\n\t}\n\n\tif name == \".\" || name == \"..\" {\n\t\treturn false\n\t}\n\n\tupperName := strings.ToUpper(name)\n\tbaseName := strings.Split(upperName, \".\")[0]\n\n\tswitch baseName {\n\tcase \"CON\", \"PRN\", \"AUX\", \"NUL\",\n\t\t\"COM1\", \"COM2\", \"COM3\", \"COM4\", \"COM5\", \"COM6\", \"COM7\", \"COM8\", \"COM9\",\n\t\t\"LPT1\", \"LPT2\", \"LPT3\", \"LPT4\", \"LPT5\", \"LPT6\", \"LPT7\", \"LPT8\", \"LPT9\":\n\t\treturn false\n\t}\n\n\tif strings.HasSuffix(name, \" \") || strings.HasSuffix(name, \".\") {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// GetTimeAsMsSinceEpoch returns unix timestamp as milliseconds from a time struct\nfunc GetTimeAsMsSinceEpoch(t time.Time) int64 {\n\treturn t.UnixMilli()\n}\n\n// GetTimeFromMsecSinceEpoch return a time struct from a unix timestamp with millisecond precision\nfunc GetTimeFromMsecSinceEpoch(msec int64) time.Time {\n\treturn time.Unix(0, msec*1000000)\n}\n\n// GetDurationAsString returns a string representation for a time.Duration\nfunc GetDurationAsString(d time.Duration) string {\n\td = d.Round(time.Second)\n\th := d / time.Hour\n\td -= h * time.Hour\n\tm := d / time.Minute\n\td -= m * time.Minute\n\ts := d / time.Second\n\tif h > 0 {\n\t\treturn fmt.Sprintf(\"%02d:%02d:%02d\", h, m, s)\n\t}\n\treturn fmt.Sprintf(\"%02d:%02d\", m, s)\n}\n\n// ByteCountSI returns humanized size in SI (decimal) format\nfunc ByteCountSI(b int64) string {\n\treturn byteCount(b, 1000, true)\n}\n\n// ByteCountIEC returns humanized size in IEC (binary) format\nfunc ByteCountIEC(b int64) string {\n\treturn byteCount(b, 1024, false)\n}\n\nfunc byteCount(b int64, unit int64, maxPrecision bool) string {\n\tif b <= 0 && maxPrecision {\n\t\treturn strconv.FormatInt(b, 10)\n\t}\n\tif b < unit {\n\t\treturn fmt.Sprintf(\"%d B\", b)\n\t}\n\tdiv, exp := unit, 0\n\tfor n := b / unit; n >= unit; n /= unit {\n\t\tdiv *= unit\n\t\texp++\n\t}\n\tvar val string\n\tif maxPrecision {\n\t\tval = strconv.FormatFloat(float64(b)/float64(div), 'f', -1, 64)\n\t} else {\n\t\tval = fmt.Sprintf(\"%.1f\", float64(b)/float64(div))\n\t}\n\tif unit == 1000 {\n\t\treturn fmt.Sprintf(\"%s %cB\", val, \"KMGTPE\"[exp])\n\t}\n\treturn fmt.Sprintf(\"%s %ciB\", val, \"KMGTPE\"[exp])\n}\n\n// ParseBytes parses a string representation of bytes into the number\n// of bytes it represents.\n//\n// ParseBytes(\"42 MB\") -> 42000000, nil\n// ParseBytes(\"42 mib\") -> 44040192, nil\n//\n// copied from here:\n//\n// https://github.com/dustin/go-humanize/blob/master/bytes.go\n//\n// with minor modifications\nfunc ParseBytes(s string) (int64, error) {\n\ts = strings.TrimSpace(s)\n\tlastDigit := 0\n\thasComma := false\n\tfor _, r := range s {\n\t\tif !unicode.IsDigit(r) && r != '.' && r != ',' {\n\t\t\tbreak\n\t\t}\n\t\tif r == ',' {\n\t\t\thasComma = true\n\t\t}\n\t\tlastDigit++\n\t}\n\n\tnum := s[:lastDigit]\n\tif hasComma {\n\t\tnum = strings.ReplaceAll(num, \",\", \"\")\n\t}\n\n\tf, err := strconv.ParseFloat(num, 64)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\textra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))\n\tif m, ok := bytesSizeTable[extra]; ok {\n\t\tf *= float64(m)\n\t\tif f >= math.MaxInt64 {\n\t\t\treturn 0, fmt.Errorf(\"value too large: %v\", s)\n\t\t}\n\t\tif f < 0 {\n\t\t\treturn 0, fmt.Errorf(\"negative value not allowed: %v\", s)\n\t\t}\n\t\treturn int64(f), nil\n\t}\n\n\treturn 0, fmt.Errorf(\"unhandled size name: %v\", extra)\n}\n\n// BytesToString converts []byte to string without allocations.\n// https://github.com/kubernetes/kubernetes/blob/e4b74dd12fa8cb63c174091d5536a10b8ec19d34/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cached_token_authenticator.go#L278\n// Use only if strictly required, this method uses unsafe.\nfunc BytesToString(b []byte) string {\n\t// unsafe.SliceData relies on cap whereas we want to rely on len\n\tif len(b) == 0 {\n\t\treturn \"\"\n\t}\n\t// https://github.com/golang/go/blob/4ed358b57efdad9ed710be7f4fc51495a7620ce2/src/strings/builder.go#L41\n\treturn unsafe.String(unsafe.SliceData(b), len(b))\n}\n\n// StringToBytes convert string to []byte without allocations.\n// https://github.com/kubernetes/kubernetes/blob/e4b74dd12fa8cb63c174091d5536a10b8ec19d34/staging/src/k8s.io/apiserver/pkg/authentication/token/cache/cached_token_authenticator.go#L289\n// Use only if strictly required, this method uses unsafe.\nfunc StringToBytes(s string) []byte {\n\t// unsafe.StringData is unspecified for the empty string, so we provide a strict interpretation\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\t// https://github.com/golang/go/blob/4ed358b57efdad9ed710be7f4fc51495a7620ce2/src/os/file.go#L300\n\treturn unsafe.Slice(unsafe.StringData(s), len(s))\n}\n\n// GetIPFromRemoteAddress returns the IP from the remote address.\n// If the given remote address cannot be parsed it will be returned unchanged\nfunc GetIPFromRemoteAddress(remoteAddress string) string {\n\tip, _, err := net.SplitHostPort(remoteAddress)\n\tif err == nil {\n\t\treturn ip\n\t}\n\treturn remoteAddress\n}\n\n// GetIPFromNetAddr returns the IP from the network address\nfunc GetIPFromNetAddr(upstream net.Addr) (net.IP, error) {\n\tif upstream == nil {\n\t\treturn nil, errors.New(\"invalid address\")\n\t}\n\tupstreamString, _, err := net.SplitHostPort(upstream.String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tupstreamIP := net.ParseIP(upstreamString)\n\tif upstreamIP == nil {\n\t\treturn nil, fmt.Errorf(\"invalid IP address: %q\", upstreamString)\n\t}\n\n\treturn upstreamIP, nil\n}\n\n// NilIfEmpty returns nil if the input string is empty\nfunc NilIfEmpty(s string) *string {\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\treturn &s\n}\n\n// GetStringFromPointer returns the string value or empty if nil\nfunc GetStringFromPointer(val *string) string {\n\tif val == nil {\n\t\treturn \"\"\n\t}\n\treturn *val\n}\n\n// GetIntFromPointer returns the int value or zero\nfunc GetIntFromPointer(val *int64) int64 {\n\tif val == nil {\n\t\treturn 0\n\t}\n\treturn *val\n}\n\n// GetTimeFromPointer returns the time value or now\nfunc GetTimeFromPointer(val *time.Time) time.Time {\n\tif val == nil {\n\t\treturn time.Unix(0, 0)\n\t}\n\treturn *val\n}\n\n// GenerateRSAKeys generate rsa private and public keys and write the\n// private key to specified file and the public key to the specified\n// file adding the .pub suffix\nfunc GenerateRSAKeys(file string) error {\n\tif err := createDirPathIfMissing(file, 0700); err != nil {\n\t\treturn err\n\t}\n\tkey, err := rsa.GenerateKey(rand.Reader, 3072)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\to, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer o.Close()\n\n\tpriv := &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(key),\n\t}\n\n\tif err := pem.Encode(o, priv); err != nil {\n\t\treturn err\n\t}\n\n\tpub, err := ssh.NewPublicKey(&key.PublicKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(file+pubKeySuffix, ssh.MarshalAuthorizedKey(pub), 0600)\n}\n\n// GenerateECDSAKeys generate ecdsa private and public keys and write the\n// private key to specified file and the public key to the specified\n// file adding the .pub suffix\nfunc GenerateECDSAKeys(file string) error {\n\tif err := createDirPathIfMissing(file, 0700); err != nil {\n\t\treturn err\n\t}\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tkeyBytes, err := x509.MarshalECPrivateKey(key)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpriv := &pem.Block{\n\t\tType:  \"EC PRIVATE KEY\",\n\t\tBytes: keyBytes,\n\t}\n\n\to, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer o.Close()\n\n\tif err := pem.Encode(o, priv); err != nil {\n\t\treturn err\n\t}\n\n\tpub, err := ssh.NewPublicKey(&key.PublicKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(file+pubKeySuffix, ssh.MarshalAuthorizedKey(pub), 0600)\n}\n\n// GenerateEd25519Keys generate ed25519 private and public keys and write the\n// private key to specified file and the public key to the specified\n// file adding the .pub suffix\nfunc GenerateEd25519Keys(file string) error {\n\tpubKey, privKey, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\tkeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpriv := &pem.Block{\n\t\tType:  \"PRIVATE KEY\",\n\t\tBytes: keyBytes,\n\t}\n\to, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer o.Close()\n\n\tif err := pem.Encode(o, priv); err != nil {\n\t\treturn err\n\t}\n\tpub, err := ssh.NewPublicKey(pubKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(file+pubKeySuffix, ssh.MarshalAuthorizedKey(pub), 0600)\n}\n\n// IsDirOverlapped returns true if dir1 and dir2 overlap\nfunc IsDirOverlapped(dir1, dir2 string, fullCheck bool, separator string) bool {\n\tif dir1 == dir2 {\n\t\treturn true\n\t}\n\tif fullCheck {\n\t\tif len(dir1) > len(dir2) {\n\t\t\tif strings.HasPrefix(dir1, dir2+separator) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\tif len(dir2) > len(dir1) {\n\t\t\tif strings.HasPrefix(dir2, dir1+separator) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// GetDirsForVirtualPath returns all the directory for the given path in reverse order\n// for example if the path is: /1/2/3/4 it returns:\n// [ \"/1/2/3/4\", \"/1/2/3\", \"/1/2\", \"/1\", \"/\" ]\nfunc GetDirsForVirtualPath(virtualPath string) []string {\n\tif virtualPath == \"\" || virtualPath == \".\" {\n\t\tvirtualPath = \"/\"\n\t} else {\n\t\tif !path.IsAbs(virtualPath) {\n\t\t\tvirtualPath = CleanPath(virtualPath)\n\t\t}\n\t}\n\tdirsForPath := []string{virtualPath}\n\tfor virtualPath != \"/\" {\n\t\tvirtualPath = path.Dir(virtualPath)\n\t\tdirsForPath = append(dirsForPath, virtualPath)\n\t}\n\treturn dirsForPath\n}\n\n// CleanPath returns a clean POSIX (/) absolute path to work with\nfunc CleanPath(p string) string {\n\treturn CleanPathWithBase(\"/\", p)\n}\n\n// CleanPathWithBase returns a clean POSIX (/) absolute path to work with.\n// The specified base will be used if the provided path is not absolute\nfunc CleanPathWithBase(base, p string) string {\n\tp = strings.ReplaceAll(p, \"\\\\\", \"/\")\n\tif !path.IsAbs(p) {\n\t\tp = path.Join(base, p)\n\t}\n\treturn path.Clean(p)\n}\n\n// IsFileInputValid returns true this is a valid file name.\n// This method must be used before joining a file name, generally provided as\n// user input, with a directory\nfunc IsFileInputValid(fileInput string) bool {\n\tcleanInput := filepath.Clean(fileInput)\n\tif cleanInput == \".\" || cleanInput == \"..\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// CleanDirInput sanitizes user input for directories.\n// On Windows it removes any trailing `\"`.\n// We try to help windows users that set an invalid path such as \"C:\\ProgramData\\SFTPGO\\\".\n// This will only help if the invalid path is the last argument, for example in this command:\n// sftpgo.exe serve -c \"C:\\ProgramData\\SFTPGO\\\" -l \"sftpgo.log\"\n// the -l flag will be ignored and the -c flag will get the value `C:\\ProgramData\\SFTPGO\" -l sftpgo.log`\n// since the backslash after SFTPGO escape the double quote. This is definitely a bad user input\nfunc CleanDirInput(dirInput string) string {\n\tif runtime.GOOS == osWindows {\n\t\tfor strings.HasSuffix(dirInput, \"\\\"\") {\n\t\t\tdirInput = strings.TrimSuffix(dirInput, \"\\\"\")\n\t\t}\n\t}\n\treturn filepath.Clean(dirInput)\n}\n\nfunc createDirPathIfMissing(file string, perm os.FileMode) error {\n\tdirPath := filepath.Dir(file)\n\tif _, err := os.Stat(dirPath); errors.Is(err, fs.ErrNotExist) {\n\t\terr = os.MkdirAll(dirPath, perm)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// GenerateRandomBytes generates random bytes with the specified length\nfunc GenerateRandomBytes(length int) []byte {\n\tb := make([]byte, length)\n\t_, err := io.ReadFull(rand.Reader, b)\n\tif err != nil {\n\t\tPanicOnError(fmt.Errorf(\"failed to read random data (see https://go.dev/issue/66821): %w\", err))\n\t}\n\treturn b\n}\n\n// GenerateOpaqueString generates a cryptographically secure opaque string\nfunc GenerateOpaqueString() string {\n\trandomBytes := sha256.Sum256(GenerateRandomBytes(32))\n\treturn hex.EncodeToString(randomBytes[:])\n}\n\n// GenerateUniqueID returns an unique ID\nfunc GenerateUniqueID() string {\n\tu, err := uuid.NewRandom()\n\tif err != nil {\n\t\tPanicOnError(fmt.Errorf(\"failed to read random data (see https://go.dev/issue/66821): %w\", err))\n\t}\n\treturn shortuuid.DefaultEncoder.Encode(u)\n}\n\n// HTTPListenAndServe is a wrapper for ListenAndServe that support both tcp\n// and Unix-domain sockets\nfunc HTTPListenAndServe(srv *http.Server, address string, port int, isTLS bool,\n\tlistenerWrapper func(net.Listener) (net.Listener, error),\n\tlogSender string,\n) error {\n\tvar listener net.Listener\n\tvar err error\n\n\tif filepath.IsAbs(address) && runtime.GOOS != osWindows {\n\t\tif !IsFileInputValid(address) {\n\t\t\treturn fmt.Errorf(\"invalid socket address %q\", address)\n\t\t}\n\t\terr = createDirPathIfMissing(address, 0770)\n\t\tif err != nil {\n\t\t\tlogger.ErrorToConsole(\"error creating Unix-domain socket parent dir: %v\", err)\n\t\t\tlogger.Error(logSender, \"\", \"error creating Unix-domain socket parent dir: %v\", err)\n\t\t}\n\t\tos.Remove(address)\n\t\tlistener, err = net.Listen(\"unix\", address)\n\t\tif err == nil {\n\t\t\t// should a chmod err be fatal?\n\t\t\tif errChmod := os.Chmod(address, 0770); errChmod != nil {\n\t\t\t\tlogger.Warn(logSender, \"\", \"unable to set the Unix-domain socket group writable: %v\", errChmod)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tCheckTCP4Port(port)\n\t\tlistener, err = net.Listen(\"tcp\", fmt.Sprintf(\"%s:%d\", address, port))\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tif listenerWrapper != nil {\n\t\tlistener, err = listenerWrapper(listener)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tlogger.Info(logSender, \"\", \"server listener registered, address: %s TLS enabled: %t\", listener.Addr().String(), isTLS)\n\n\tdefer listener.Close()\n\n\tif isTLS {\n\t\treturn srv.ServeTLS(listener, \"\", \"\")\n\t}\n\treturn srv.Serve(listener)\n}\n\n// GetTLSCiphersFromNames returns the TLS ciphers from the specified names\nfunc GetTLSCiphersFromNames(cipherNames []string) []uint16 {\n\tvar ciphers []uint16\n\n\tfor _, name := range RemoveDuplicates(cipherNames, false) {\n\t\tfor _, c := range tls.CipherSuites() {\n\t\t\tif c.Name == strings.TrimSpace(name) {\n\t\t\t\tciphers = append(ciphers, c.ID)\n\t\t\t}\n\t\t}\n\t\tfor _, c := range tls.InsecureCipherSuites() {\n\t\t\tif c.Name == strings.TrimSpace(name) {\n\t\t\t\tciphers = append(ciphers, c.ID)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(ciphers) == 0 {\n\t\t// return a secure default\n\t\treturn defaultTLSCiphers\n\t}\n\n\treturn ciphers\n}\n\n// GetALPNProtocols returns the ALPN protocols, any invalid protocol will be\n// silently ignored. If no protocol or no valid protocol is provided the default\n// is http/1.1, h2\nfunc GetALPNProtocols(protocols []string) []string {\n\tvar result []string\n\tfor _, p := range protocols {\n\t\tswitch p {\n\t\tcase \"http/1.1\", \"h2\":\n\t\t\tresult = append(result, p)\n\t\t}\n\t}\n\tif len(result) == 0 {\n\t\treturn []string{\"http/1.1\", \"h2\"}\n\t}\n\treturn result\n}\n\n// EncodeTLSCertToPem returns the specified certificate PEM encoded.\n// This can be verified using openssl x509 -in cert.crt  -text -noout\nfunc EncodeTLSCertToPem(tlsCert *x509.Certificate) (string, error) {\n\tif len(tlsCert.Raw) == 0 {\n\t\treturn \"\", errors.New(\"invalid x509 certificate, no der contents\")\n\t}\n\tpublicKeyBlock := pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: tlsCert.Raw,\n\t}\n\treturn BytesToString(pem.EncodeToMemory(&publicKeyBlock)), nil\n}\n\n// CheckTCP4Port quits the app if bind on the given IPv4 port fails.\n// This is a ugly hack to avoid to bind on an already used port.\n// It is required on Windows only. Upstream does not consider this\n// behaviour a bug:\n// https://github.com/golang/go/issues/45150\nfunc CheckTCP4Port(port int) {\n\tif runtime.GOOS != osWindows {\n\t\treturn\n\t}\n\tlistener, err := net.Listen(\"tcp4\", fmt.Sprintf(\":%d\", port))\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"unable to bind on tcp4 address: %v\", err)\n\t\tlogger.Error(logSender, \"\", \"unable to bind on tcp4 address: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tlistener.Close()\n}\n\n// IsByteArrayEmpty return true if the byte array is empty or a new line\nfunc IsByteArrayEmpty(b []byte) bool {\n\tif len(b) == 0 {\n\t\treturn true\n\t}\n\tif bytes.Equal(b, []byte(\"\\n\")) {\n\t\treturn true\n\t}\n\tif bytes.Equal(b, []byte(\"\\r\\n\")) {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// GetSSHPublicKeyAsString returns an SSH public key serialized as string\nfunc GetSSHPublicKeyAsString(pubKey []byte) (string, error) {\n\tif len(pubKey) == 0 {\n\t\treturn \"\", nil\n\t}\n\tk, err := ssh.ParsePublicKey(pubKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn BytesToString(ssh.MarshalAuthorizedKey(k)), nil\n}\n\n// GetRealIP returns the ip address as result of parsing the specified\n// header and using the specified depth\nfunc GetRealIP(r *http.Request, header string, depth int) string {\n\tif header == \"\" {\n\t\treturn \"\"\n\t}\n\tvar ipAddresses []string\n\n\tfor _, h := range r.Header.Values(header) {\n\t\tfor ipStr := range strings.SplitSeq(h, \",\") {\n\t\t\tipStr = strings.TrimSpace(ipStr)\n\t\t\tipAddresses = append(ipAddresses, ipStr)\n\t\t}\n\t}\n\n\tidx := len(ipAddresses) - 1 - depth\n\tif idx >= 0 {\n\t\tip := strings.TrimSpace(ipAddresses[idx])\n\t\tif ip == \"\" || net.ParseIP(ip) == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn ip\n\t}\n\n\treturn \"\"\n}\n\n// GetHTTPLocalAddress returns the local address for an http.Request\n// or empty if it cannot be determined\nfunc GetHTTPLocalAddress(r *http.Request) string {\n\tif r == nil {\n\t\treturn \"\"\n\t}\n\tlocalAddr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr)\n\tif ok {\n\t\treturn localAddr.String()\n\t}\n\treturn \"\"\n}\n\n// ParseAllowedIPAndRanges returns a list of functions that allow to find if an\n// IP is equal or is contained within the allowed list\nfunc ParseAllowedIPAndRanges(allowed []string) ([]func(net.IP) bool, error) {\n\tres := make([]func(net.IP) bool, len(allowed))\n\tfor i, allowFrom := range allowed {\n\t\tif strings.LastIndex(allowFrom, \"/\") > 0 {\n\t\t\t_, ipRange, err := net.ParseCIDR(allowFrom)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"given string %q is not a valid IP range: %v\", allowFrom, err)\n\t\t\t}\n\n\t\t\tres[i] = ipRange.Contains\n\t\t} else {\n\t\t\tallowed := net.ParseIP(allowFrom)\n\t\t\tif allowed == nil {\n\t\t\t\treturn nil, fmt.Errorf(\"given string %q is not a valid IP address\", allowFrom)\n\t\t\t}\n\n\t\t\tres[i] = allowed.Equal\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\n// GetRedactedURL returns the url redacting the password if any\nfunc GetRedactedURL(rawurl string) string {\n\tif !strings.HasPrefix(rawurl, \"http\") {\n\t\treturn rawurl\n\t}\n\tu, err := url.Parse(rawurl)\n\tif err != nil {\n\t\treturn rawurl\n\t}\n\treturn u.Redacted()\n}\n\n// GetTLSVersion returns the TLS version from an integer value:\n// - 10 means TLS 1.0\n// - 11 means TLS 1.1\n// - 12 means TLS 1.2\n// - 13 means TLS 1.3\n// default is TLS 1.2\nfunc GetTLSVersion(val int) uint16 {\n\tswitch val {\n\tcase 13:\n\t\treturn tls.VersionTLS13\n\tcase 11:\n\t\treturn tls.VersionTLS11\n\tcase 10:\n\t\treturn tls.VersionTLS10\n\tdefault:\n\t\treturn tls.VersionTLS12\n\t}\n}\n\n// IsEmailValid returns true if the specified email address is valid\nfunc IsEmailValid(email string) bool {\n\treturn emailRegex.MatchString(email)\n}\n\n// SanitizeDomain return the specified domain name in a form suitable to save as file\nfunc SanitizeDomain(domain string) string {\n\treturn strings.NewReplacer(\":\", \"_\", \"*\", \"_\", \",\", \"_\", \" \", \"_\").Replace(domain)\n}\n\n// PanicOnError calls panic if err is not nil\nfunc PanicOnError(err error) {\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unexpected error: %w\", err))\n\t}\n}\n\n// GetAbsolutePath returns an absolute path using the current dir as base\n// if name defines a relative path\nfunc GetAbsolutePath(name string) (string, error) {\n\tif name == \"\" {\n\t\treturn name, errors.New(\"input path cannot be empty\")\n\t}\n\tif filepath.IsAbs(name) {\n\t\treturn name, nil\n\t}\n\tcurDir, err := os.Getwd()\n\tif err != nil {\n\t\treturn name, err\n\t}\n\treturn filepath.Join(curDir, name), nil\n}\n\n// GetACMECertificateKeyPair returns the path to the ACME TLS crt and key for the specified domain\nfunc GetACMECertificateKeyPair(domain string) (string, string) {\n\tif CertsBasePath == \"\" {\n\t\treturn \"\", \"\"\n\t}\n\tdomain = SanitizeDomain(domain)\n\treturn filepath.Join(CertsBasePath, domain+\".crt\"), filepath.Join(CertsBasePath, domain+\".key\")\n}\n\n// GetLastIPForPrefix returns the last IP for the given prefix\n// https://github.com/go4org/netipx/blob/8449b0a6169f5140fb0340cb4fc0de4c9b281ef6/netipx.go#L173\nfunc GetLastIPForPrefix(p netip.Prefix) netip.Addr {\n\tif !p.IsValid() {\n\t\treturn netip.Addr{}\n\t}\n\ta16 := p.Addr().As16()\n\tvar off uint8\n\tvar bits uint8 = 128\n\tif p.Addr().Is4() {\n\t\toff = 12\n\t\tbits = 32\n\t}\n\tfor b := uint8(p.Bits()); b < bits; b++ {\n\t\tbyteNum, bitInByte := b/8, 7-(b%8)\n\t\ta16[off+byteNum] |= 1 << uint(bitInByte)\n\t}\n\tif p.Addr().Is4() {\n\t\treturn netip.AddrFrom16(a16).Unmap()\n\t}\n\treturn netip.AddrFrom16(a16) // doesn't unmap\n}\n\n// JSONEscape returns the JSON escaped format for the input string\nfunc JSONEscape(val string) string {\n\tif val == \"\" {\n\t\treturn val\n\t}\n\tb, err := json.Marshal(val)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn BytesToString(b[1 : len(b)-1])\n}\n\n// ReadConfigFromFile reads a configuration parameter from the specified file\nfunc ReadConfigFromFile(name, configDir string) (string, error) {\n\tif !IsFileInputValid(name) {\n\t\treturn \"\", fmt.Errorf(\"invalid file input: %q\", name)\n\t}\n\tif configDir == \"\" {\n\t\tif !filepath.IsAbs(name) {\n\t\t\treturn \"\", fmt.Errorf(\"%q must be an absolute file path\", name)\n\t\t}\n\t} else {\n\t\tif name != \"\" && !filepath.IsAbs(name) {\n\t\t\tname = filepath.Join(configDir, name)\n\t\t}\n\t}\n\tval, err := os.ReadFile(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(BytesToString(val)), nil\n}\n\n// SlicesEqual checks if the provided slices contain the same elements,\n// also in different order.\nfunc SlicesEqual(s1, s2 []string) bool {\n\tif len(s1) != len(s2) {\n\t\treturn false\n\t}\n\tfor _, v := range s1 {\n\t\tif !slices.Contains(s2, v) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// VerifyFileChecksum computes the hash of the given file using the provided\n// hash algorithm and compares it against the expected checksum (in hex format).\n// It returns an error if the checksum does not match or if the operation fails.\nfunc VerifyFileChecksum(filePath string, h hash.Hash, expectedHex string, maxSize int64) error {\n\texpected, err := hex.DecodeString(expectedHex)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid checksum %q: %w\", expectedHex, err)\n\t}\n\n\tf, err := os.Open(filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\tif maxSize > 0 {\n\t\tfi, err := f.Stat()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fi.Size() > maxSize {\n\t\t\treturn fmt.Errorf(\"file too large: %s\", ByteCountIEC(fi.Size()))\n\t\t}\n\t}\n\n\tif _, err := io.Copy(h, f); err != nil {\n\t\treturn err\n\t}\n\n\tactual := h.Sum(nil)\n\tif subtle.ConstantTimeCompare(actual, expected) != 1 {\n\t\treturn errors.New(\"checksum mismatch\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/util/util_fallback.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !unix\n\npackage util\n\nimport (\n\t\"runtime\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// SetUmask sets the specified umask\nfunc SetUmask(val string) {\n\tif val == \"\" {\n\t\treturn\n\t}\n\tlogger.Debug(logSender, \"\", \"umask not supported on OS %q\", runtime.GOOS)\n}\n"
  },
  {
    "path": "internal/util/util_unix.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build unix\n\npackage util\n\nimport (\n\t\"strconv\"\n\t\"syscall\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\n// SetUmask sets the specified umask\nfunc SetUmask(val string) {\n\tif val == \"\" {\n\t\treturn\n\t}\n\tumask, err := strconv.ParseUint(val, 8, 31)\n\tif err != nil {\n\t\tlogger.Error(logSender, \"\", \"invalid umask %q: %v\", val, err)\n\t\treturn\n\t}\n\tlogger.Debug(logSender, \"\", \"set umask to: %d, configured value: %q\", umask, val)\n\tsyscall.Umask(int(umask))\n}\n"
  },
  {
    "path": "internal/version/version.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package version defines SFTPGo version details\npackage version\n\nimport \"strings\"\n\nconst (\n\tversion = \"2.7.99-dev\"\n\tappName = \"SFTPGo\"\n)\n\nvar (\n\tcommit = \"\"\n\tdate   = \"\"\n\tinfo   Info\n)\n\nvar (\n\tconfig string\n)\n\n// Info defines version details\ntype Info struct {\n\tVersion    string   `json:\"version\"`\n\tBuildDate  string   `json:\"build_date\"`\n\tCommitHash string   `json:\"commit_hash\"`\n\tFeatures   []string `json:\"features\"`\n}\n\n// GetAsString returns the string representation of the version\nfunc GetAsString() string {\n\tvar sb strings.Builder\n\tsb.WriteString(info.Version)\n\tif info.CommitHash != \"\" {\n\t\tsb.WriteString(\"-\")\n\t\tsb.WriteString(info.CommitHash)\n\t}\n\tif info.BuildDate != \"\" {\n\t\tsb.WriteString(\"-\")\n\t\tsb.WriteString(info.BuildDate)\n\t}\n\tif len(info.Features) > 0 {\n\t\tsb.WriteString(\" \")\n\t\tsb.WriteString(strings.Join(info.Features, \" \"))\n\t}\n\treturn sb.String()\n}\n\nfunc init() {\n\tinfo = Info{\n\t\tVersion:    version,\n\t\tCommitHash: commit,\n\t\tBuildDate:  date,\n\t}\n}\n\n// AddFeature adds a feature description\nfunc AddFeature(feature string) {\n\tinfo.Features = append(info.Features, feature)\n}\n\n// Get returns the Info struct\nfunc Get() Info {\n\treturn info\n}\n\n// SetConfig sets the version configuration\nfunc SetConfig(val string) {\n\tconfig = val\n}\n\n// GetServerVersion returns the server version according to the configuration\n// and the provided parameters.\nfunc GetServerVersion(separator string, addHash bool) string {\n\tvar sb strings.Builder\n\tsb.WriteString(appName)\n\tif config != \"short\" {\n\t\tsb.WriteString(separator)\n\t\tsb.WriteString(info.Version)\n\t}\n\tif addHash {\n\t\tsb.WriteString(separator)\n\t\tsb.WriteString(info.CommitHash)\n\t}\n\treturn sb.String()\n}\n\n// GetVersionHash returns the server identification string with the commit hash.\nfunc GetVersionHash() string {\n\tvar sb strings.Builder\n\tsb.WriteString(appName)\n\tsb.WriteString(\"-\")\n\tsb.WriteString(info.CommitHash)\n\treturn sb.String()\n}\n"
  },
  {
    "path": "internal/vfs/azblobfs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !noazblob\n\npackage vfs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azidentity\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/sftp\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\tazureDefaultEndpoint = \"blob.core.windows.net\"\n\tazFolderKey          = \"hdi_isfolder\"\n)\n\nvar (\n\tazureBlobDefaultPageSize = int32(5000)\n)\n\n// AzureBlobFs is a Fs implementation for Azure Blob storage.\ntype AzureBlobFs struct {\n\tconnectionID string\n\tlocalTempDir string\n\t// if not empty this fs is mouted as virtual folder in the specified path\n\tmountPath       string\n\tconfig          *AzBlobFsConfig\n\tcontainerClient *container.Client\n\tctxTimeout      time.Duration\n\tctxLongTimeout  time.Duration\n}\n\nfunc init() {\n\tversion.AddFeature(\"+azblob\")\n}\n\n// NewAzBlobFs returns an AzBlobFs object that allows to interact with Azure Blob storage\nfunc NewAzBlobFs(connectionID, localTempDir, mountPath string, config AzBlobFsConfig) (Fs, error) {\n\tif localTempDir == \"\" {\n\t\tlocalTempDir = getLocalTempDir()\n\t}\n\tfs := &AzureBlobFs{\n\t\tconnectionID:   connectionID,\n\t\tlocalTempDir:   localTempDir,\n\t\tmountPath:      getMountPath(mountPath),\n\t\tconfig:         &config,\n\t\tctxTimeout:     30 * time.Second,\n\t\tctxLongTimeout: 90 * time.Second,\n\t}\n\tif err := fs.config.validate(); err != nil {\n\t\treturn fs, err\n\t}\n\n\tif err := fs.config.tryDecrypt(); err != nil {\n\t\treturn fs, err\n\t}\n\n\tfs.setConfigDefaults()\n\n\tif fs.config.SASURL.GetPayload() != \"\" {\n\t\treturn fs.initFromSASURL()\n\t}\n\n\tvar endpoint string\n\tif fs.config.UseEmulator {\n\t\tendpoint = fmt.Sprintf(\"%s/%s\", fs.config.Endpoint, fs.config.AccountName)\n\t} else {\n\t\tendpoint = fmt.Sprintf(\"https://%s.%s/\", fs.config.AccountName, fs.config.Endpoint)\n\t}\n\tcontainerURL := runtime.JoinPaths(endpoint, fs.config.Container)\n\tif fs.config.AccountKey.GetPayload() != \"\" {\n\t\tcredential, err := blob.NewSharedKeyCredential(fs.config.AccountName, fs.config.AccountKey.GetPayload())\n\t\tif err != nil {\n\t\t\treturn fs, fmt.Errorf(\"invalid credentials: %v\", err)\n\t\t}\n\t\tsvc, err := container.NewClientWithSharedKeyCredential(containerURL, credential, getAzContainerClientOptions())\n\t\tif err != nil {\n\t\t\treturn fs, fmt.Errorf(\"unable to create the storage client using shared key credentials: %v\", err)\n\t\t}\n\t\tfs.containerClient = svc\n\t\treturn fs, err\n\t}\n\tcredential, err := azidentity.NewDefaultAzureCredential(nil)\n\tif err != nil {\n\t\treturn fs, fmt.Errorf(\"invalid default azure credentials: %v\", err)\n\t}\n\tsvc, err := container.NewClient(containerURL, credential, getAzContainerClientOptions())\n\tif err != nil {\n\t\treturn fs, fmt.Errorf(\"unable to create the storage client using azure credentials: %v\", err)\n\t}\n\tfs.containerClient = svc\n\treturn fs, err\n}\n\nfunc (fs *AzureBlobFs) initFromSASURL() (Fs, error) {\n\tparts, err := blob.ParseURL(fs.config.SASURL.GetPayload())\n\tif err != nil {\n\t\treturn fs, fmt.Errorf(\"invalid SAS URL: %w\", err)\n\t}\n\tif parts.BlobName != \"\" {\n\t\treturn fs, fmt.Errorf(\"SAS URL with blob name not supported\")\n\t}\n\tif parts.ContainerName != \"\" {\n\t\tif fs.config.Container != \"\" && fs.config.Container != parts.ContainerName {\n\t\t\treturn fs, fmt.Errorf(\"container name in SAS URL %q and container provided %q do not match\",\n\t\t\t\tparts.ContainerName, fs.config.Container)\n\t\t}\n\t\tsvc, err := container.NewClientWithNoCredential(fs.config.SASURL.GetPayload(), getAzContainerClientOptions())\n\t\tif err != nil {\n\t\t\treturn fs, fmt.Errorf(\"invalid credentials: %v\", err)\n\t\t}\n\t\tfs.config.Container = parts.ContainerName\n\t\tfs.containerClient = svc\n\t\treturn fs, nil\n\t}\n\tif fs.config.Container == \"\" {\n\t\treturn fs, errors.New(\"container is required with this SAS URL\")\n\t}\n\tsasURL := runtime.JoinPaths(fs.config.SASURL.GetPayload(), fs.config.Container)\n\tsvc, err := container.NewClientWithNoCredential(sasURL, getAzContainerClientOptions())\n\tif err != nil {\n\t\treturn fs, fmt.Errorf(\"invalid credentials: %v\", err)\n\t}\n\tfs.containerClient = svc\n\treturn fs, nil\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *AzureBlobFs) Name() string {\n\tif !fs.config.SASURL.IsEmpty() {\n\t\treturn fmt.Sprintf(\"%s with SAS URL, container %q\", azBlobFsName, fs.config.Container)\n\t}\n\treturn fmt.Sprintf(\"%s container %q\", azBlobFsName, fs.config.Container)\n}\n\n// ConnectionID returns the connection ID associated to this Fs implementation\nfunc (fs *AzureBlobFs) ConnectionID() string {\n\treturn fs.connectionID\n}\n\n// Stat returns a FileInfo describing the named file\nfunc (fs *AzureBlobFs) Stat(name string) (os.FileInfo, error) {\n\tif name == \"\" || name == \"/\" || name == \".\" {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t}\n\tif fs.config.KeyPrefix == name+\"/\" {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t}\n\n\tattrs, err := fs.headObject(name)\n\tif err == nil {\n\t\tcontentType := util.GetStringFromPointer(attrs.ContentType)\n\t\tisDir := checkDirectoryMarkers(contentType, attrs.Metadata)\n\t\tlastModified := util.GetTimeFromPointer(attrs.LastModified)\n\t\tif val := getAzureLastModified(attrs.Metadata); val > 0 {\n\t\t\tlastModified = util.GetTimeFromMsecSinceEpoch(val)\n\t\t}\n\t\tinfo := NewFileInfo(name, isDir, util.GetIntFromPointer(attrs.ContentLength), lastModified, false)\n\t\tif !isDir {\n\t\t\tinfo.setMetadataFromPointerVal(attrs.Metadata)\n\t\t}\n\t\treturn info, nil\n\t}\n\tif !fs.IsNotExist(err) {\n\t\treturn nil, err\n\t}\n\t// now check if this is a prefix (virtual directory)\n\thasContents, err := fs.hasContents(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif hasContents {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t}\n\treturn nil, os.ErrNotExist\n}\n\n// Lstat returns a FileInfo describing the named file\nfunc (fs *AzureBlobFs) Lstat(name string) (os.FileInfo, error) {\n\treturn fs.Stat(name)\n}\n\n// Open opens the named file for reading\nfunc (fs *AzureBlobFs) Open(name string, offset int64) (File, PipeReader, func(), error) {\n\tr, w, err := createPipeFn(fs.localTempDir, fs.config.DownloadPartSize*int64(fs.config.DownloadConcurrency)+1)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeReader(r)\n\tctx, cancelFn := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\tdefer cancelFn()\n\n\t\tblockBlob := fs.containerClient.NewBlockBlobClient(name)\n\t\terr := fs.handleMultipartDownload(ctx, blockBlob, offset, w, p)\n\t\tw.CloseWithError(err) //nolint:errcheck\n\t\tfsLog(fs, logger.LevelDebug, \"download completed, path: %q size: %v, err: %+v\", name, w.GetWrittenBytes(), err)\n\t\tmetric.AZTransferCompleted(w.GetWrittenBytes(), 1, err)\n\t}()\n\n\treturn nil, p, cancelFn, nil\n}\n\n// Create creates or opens the named file for writing\nfunc (fs *AzureBlobFs) Create(name string, flag, checks int) (File, PipeWriter, func(), error) {\n\tif checks&CheckParentDir != 0 {\n\t\t_, err := fs.Stat(path.Dir(name))\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, err\n\t\t}\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, fs.config.UploadPartSize+1024*1024)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tctx, cancelFn := context.WithCancel(context.Background())\n\n\tvar p PipeWriter\n\tif checks&CheckResume != 0 {\n\t\tp = newPipeWriterAtOffset(w, 0)\n\t} else {\n\t\tp = NewPipeWriter(w)\n\t}\n\theaders := blob.HTTPHeaders{}\n\tvar contentType string\n\tvar metadata map[string]*string\n\tif flag == -1 {\n\t\tcontentType = dirMimeType\n\t\tmetadata = map[string]*string{\n\t\t\tazFolderKey: util.NilIfEmpty(\"true\"),\n\t\t}\n\t} else {\n\t\tcontentType = mime.TypeByExtension(path.Ext(name))\n\t}\n\tif contentType != \"\" {\n\t\theaders.BlobContentType = &contentType\n\t}\n\n\tgo func() {\n\t\tdefer cancelFn()\n\n\t\tblockBlob := fs.containerClient.NewBlockBlobClient(name)\n\t\terr := fs.handleMultipartUpload(ctx, r, blockBlob, &headers, metadata)\n\t\tr.CloseWithError(err) //nolint:errcheck\n\t\tp.Done(err)\n\t\tfsLog(fs, logger.LevelDebug, \"upload completed, path: %q, readed bytes: %v, err: %+v\", name, r.GetReadedBytes(), err)\n\t\tmetric.AZTransferCompleted(r.GetReadedBytes(), 0, err)\n\t}()\n\n\tif checks&CheckResume != 0 {\n\t\treadCh := make(chan error, 1)\n\n\t\tgo func() {\n\t\t\tn, err := fs.downloadToWriter(name, p)\n\t\t\tpw := p.(*pipeWriterAtOffset)\n\t\t\tpw.offset = 0\n\t\t\tpw.writeOffset = n\n\t\t\treadCh <- err\n\t\t}()\n\n\t\terr = <-readCh\n\t\tif err != nil {\n\t\t\tcancelFn()\n\t\t\tp.Close()\n\t\t\tfsLog(fs, logger.LevelDebug, \"download before resume failed, writer closed and read cancelled\")\n\t\t\treturn nil, nil, nil, err\n\t\t}\n\t}\n\n\tif uploadMode&16 != 0 {\n\t\treturn nil, p, nil, nil\n\t}\n\treturn nil, p, cancelFn, nil\n}\n\n// Rename renames (moves) source to target.\nfunc (fs *AzureBlobFs) Rename(source, target string, checks int) (int, int64, error) {\n\tif source == target {\n\t\treturn -1, -1, nil\n\t}\n\tif checks&CheckParentDir != 0 {\n\t\t_, err := fs.Stat(path.Dir(target))\n\t\tif err != nil {\n\t\t\treturn -1, -1, err\n\t\t}\n\t}\n\tfi, err := fs.Stat(source)\n\tif err != nil {\n\t\treturn -1, -1, err\n\t}\n\treturn fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (fs *AzureBlobFs) Remove(name string, isDir bool) error {\n\tif isDir {\n\t\thasContents, err := fs.hasContents(name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif hasContents {\n\t\t\treturn fmt.Errorf(\"cannot remove non empty directory: %q\", name)\n\t\t}\n\t}\n\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tblobBlock := fs.containerClient.NewBlockBlobClient(name)\n\tvar deletSnapshots blob.DeleteSnapshotsOptionType\n\tif !isDir {\n\t\tdeletSnapshots = blob.DeleteSnapshotsOptionTypeInclude\n\t}\n\t_, err := blobBlock.Delete(ctx, &blob.DeleteOptions{\n\t\tDeleteSnapshots: &deletSnapshots,\n\t})\n\tif err != nil && isDir {\n\t\tif fs.isBadRequestError(err) {\n\t\t\tdeletSnapshots = blob.DeleteSnapshotsOptionTypeInclude\n\t\t\t_, err = blobBlock.Delete(ctx, &blob.DeleteOptions{\n\t\t\t\tDeleteSnapshots: &deletSnapshots,\n\t\t\t})\n\t\t}\n\t}\n\tmetric.AZDeleteObjectCompleted(err)\n\treturn err\n}\n\n// Mkdir creates a new directory with the specified name and default permissions\nfunc (fs *AzureBlobFs) Mkdir(name string) error {\n\t_, err := fs.Stat(name)\n\tif !fs.IsNotExist(err) {\n\t\treturn err\n\t}\n\treturn fs.mkdirInternal(name)\n}\n\n// Symlink creates source as a symbolic link to target.\nfunc (*AzureBlobFs) Symlink(_, _ string) error {\n\treturn ErrVfsUnsupported\n}\n\n// Readlink returns the destination of the named symbolic link\nfunc (*AzureBlobFs) Readlink(_ string) (string, error) {\n\treturn \"\", ErrVfsUnsupported\n}\n\n// Chown changes the numeric uid and gid of the named file.\nfunc (*AzureBlobFs) Chown(_ string, _ int, _ int) error {\n\treturn ErrVfsUnsupported\n}\n\n// Chmod changes the mode of the named file to mode.\nfunc (*AzureBlobFs) Chmod(_ string, _ os.FileMode) error {\n\treturn ErrVfsUnsupported\n}\n\n// Chtimes changes the access and modification times of the named file.\nfunc (fs *AzureBlobFs) Chtimes(name string, _, mtime time.Time, isUploading bool) error {\n\tif isUploading {\n\t\treturn nil\n\t}\n\tprops, err := fs.headObject(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmetadata := props.Metadata\n\tif metadata == nil {\n\t\tmetadata = make(map[string]*string)\n\t}\n\tfound := false\n\tfor k := range metadata {\n\t\tif strings.EqualFold(k, lastModifiedField) {\n\t\t\tmetadata[k] = to.Ptr(strconv.FormatInt(mtime.UnixMilli(), 10))\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tmetadata[lastModifiedField] = to.Ptr(strconv.FormatInt(mtime.UnixMilli(), 10))\n\t}\n\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\t_, err = fs.containerClient.NewBlockBlobClient(name).SetMetadata(ctx, metadata, &blob.SetMetadataOptions{})\n\treturn err\n}\n\n// Truncate changes the size of the named file.\n// Truncate by path is not supported, while truncating an opened\n// file is handled inside base transfer\nfunc (*AzureBlobFs) Truncate(_ string, _ int64) error {\n\treturn ErrVfsUnsupported\n}\n\n// ReadDir reads the directory named by dirname and returns\n// a list of directory entries.\nfunc (fs *AzureBlobFs) ReadDir(dirname string) (DirLister, error) {\n\t// dirname must be already cleaned\n\tprefix := fs.getPrefix(dirname)\n\tpager := fs.containerClient.NewListBlobsHierarchyPager(\"/\", &container.ListBlobsHierarchyOptions{\n\t\tInclude: container.ListBlobsInclude{\n\t\t\tMetadata: true,\n\t\t},\n\t\tPrefix:     &prefix,\n\t\tMaxResults: &azureBlobDefaultPageSize,\n\t})\n\n\treturn &azureBlobDirLister{\n\t\tpaginator: pager,\n\t\ttimeout:   fs.ctxTimeout,\n\t\tprefix:    prefix,\n\t\tprefixes:  make(map[string]bool),\n\t}, nil\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported.\n// Resuming uploads is not supported on Azure Blob\nfunc (*AzureBlobFs) IsUploadResumeSupported() bool {\n\treturn false\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (*AzureBlobFs) IsConditionalUploadResumeSupported(size int64) bool {\n\treturn size <= resumeMaxSize\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported.\n// Azure Blob uploads are already atomic, we don't need to upload to a temporary\n// file\nfunc (*AzureBlobFs) IsAtomicUploadSupported() bool {\n\treturn false\n}\n\n// IsNotExist returns a boolean indicating whether the error is known to\n// report that a file or directory does not exist\nfunc (*AzureBlobFs) IsNotExist(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar respErr *azcore.ResponseError\n\tif errors.As(err, &respErr) {\n\t\treturn respErr.StatusCode == http.StatusNotFound\n\t}\n\t// os.ErrNotExist can be returned internally by fs.Stat\n\treturn errors.Is(err, os.ErrNotExist)\n}\n\n// IsPermission returns a boolean indicating whether the error is known to\n// report that permission is denied.\nfunc (*AzureBlobFs) IsPermission(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar respErr *azcore.ResponseError\n\tif errors.As(err, &respErr) {\n\t\treturn respErr.StatusCode == http.StatusForbidden || respErr.StatusCode == http.StatusUnauthorized\n\t}\n\treturn false\n}\n\n// IsNotSupported returns true if the error indicate an unsupported operation\nfunc (*AzureBlobFs) IsNotSupported(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn errors.Is(err, ErrVfsUnsupported)\n}\n\nfunc (*AzureBlobFs) isBadRequestError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar respErr *azcore.ResponseError\n\tif errors.As(err, &respErr) {\n\t\treturn respErr.StatusCode == http.StatusBadRequest\n\t}\n\treturn false\n}\n\n// CheckRootPath creates the specified local root directory if it does not exists\nfunc (fs *AzureBlobFs) CheckRootPath(username string, uid int, gid int) bool {\n\t// we need a local directory for temporary files\n\tosFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, \"\", nil)\n\treturn osFs.CheckRootPath(username, uid, gid)\n}\n\n// ScanRootDirContents returns the number of files contained in the bucket,\n// and their size\nfunc (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {\n\treturn fs.GetDirSize(fs.config.KeyPrefix)\n}\n\n// GetDirSize returns the number of files and the size for a folder\n// including any subfolders\nfunc (fs *AzureBlobFs) GetDirSize(dirname string) (int, int64, error) {\n\tnumFiles := 0\n\tsize := int64(0)\n\tprefix := fs.getPrefix(dirname)\n\n\tpager := fs.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{\n\t\tInclude: container.ListBlobsInclude{\n\t\t\tMetadata: true,\n\t\t},\n\t\tPrefix:     &prefix,\n\t\tMaxResults: &azureBlobDefaultPageSize,\n\t})\n\n\tfor pager.More() {\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\tresp, err := pager.NextPage(ctx)\n\t\tif err != nil {\n\t\t\tmetric.AZListObjectsCompleted(err)\n\t\t\treturn numFiles, size, err\n\t\t}\n\t\tfor _, blobItem := range resp.Segment.BlobItems {\n\t\t\tif blobItem.Properties != nil {\n\t\t\t\tcontentType := util.GetStringFromPointer(blobItem.Properties.ContentType)\n\t\t\t\tisDir := checkDirectoryMarkers(contentType, blobItem.Metadata)\n\t\t\t\tblobSize := util.GetIntFromPointer(blobItem.Properties.ContentLength)\n\t\t\t\tif isDir && blobSize == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tnumFiles++\n\t\t\t\tsize += blobSize\n\t\t\t}\n\t\t}\n\t\tfsLog(fs, logger.LevelDebug, \"scan in progress for %q, files: %d, size: %d\", dirname, numFiles, size)\n\t}\n\tmetric.AZListObjectsCompleted(nil)\n\n\treturn numFiles, size, nil\n}\n\n// GetAtomicUploadPath returns the path to use for an atomic upload.\n// Azure Blob Storage uploads are already atomic, we never call this method\nfunc (*AzureBlobFs) GetAtomicUploadPath(_ string) string {\n\treturn \"\"\n}\n\n// GetRelativePath returns the path for a file relative to the user's home dir.\n// This is the path as seen by SFTPGo users\nfunc (fs *AzureBlobFs) GetRelativePath(name string) string {\n\trel := path.Clean(name)\n\tif rel == \".\" {\n\t\trel = \"\"\n\t}\n\tif !path.IsAbs(rel) {\n\t\trel = \"/\" + rel\n\t}\n\tif fs.config.KeyPrefix != \"\" {\n\t\tif !strings.HasPrefix(rel, \"/\"+fs.config.KeyPrefix) {\n\t\t\trel = \"/\"\n\t\t}\n\t\trel = path.Clean(\"/\" + strings.TrimPrefix(rel, \"/\"+fs.config.KeyPrefix))\n\t}\n\tif fs.mountPath != \"\" {\n\t\trel = path.Join(fs.mountPath, rel)\n\t}\n\treturn rel\n}\n\n// Walk walks the file tree rooted at root, calling walkFn for each file or\n// directory in the tree, including root\nfunc (fs *AzureBlobFs) Walk(root string, walkFn filepath.WalkFunc) error {\n\tprefix := fs.getPrefix(root)\n\tpager := fs.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{\n\t\tInclude: container.ListBlobsInclude{\n\t\t\tMetadata: true,\n\t\t},\n\t\tPrefix:     &prefix,\n\t\tMaxResults: &azureBlobDefaultPageSize,\n\t})\n\n\tfor pager.More() {\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\tresp, err := pager.NextPage(ctx)\n\t\tif err != nil {\n\t\t\tmetric.AZListObjectsCompleted(err)\n\t\t\treturn err\n\t\t}\n\t\tfor _, blobItem := range resp.Segment.BlobItems {\n\t\t\tname := util.GetStringFromPointer(blobItem.Name)\n\t\t\tif fs.isEqual(name, prefix) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tblobSize := int64(0)\n\t\t\tlastModified := time.Unix(0, 0)\n\t\t\tisDir := false\n\t\t\tif blobItem.Properties != nil {\n\t\t\t\tcontentType := util.GetStringFromPointer(blobItem.Properties.ContentType)\n\t\t\t\tisDir = checkDirectoryMarkers(contentType, blobItem.Metadata)\n\t\t\t\tblobSize = util.GetIntFromPointer(blobItem.Properties.ContentLength)\n\t\t\t\tlastModified = util.GetTimeFromPointer(blobItem.Properties.LastModified)\n\t\t\t\tif val := getAzureLastModified(blobItem.Metadata); val > 0 {\n\t\t\t\t\tlastModified = util.GetTimeFromMsecSinceEpoch(val)\n\t\t\t\t}\n\t\t\t}\n\t\t\terr := walkFn(name, NewFileInfo(name, isDir, blobSize, lastModified, false), nil)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tmetric.AZListObjectsCompleted(nil)\n\treturn walkFn(root, NewFileInfo(root, true, 0, time.Unix(0, 0), false), nil)\n}\n\n// Join joins any number of path elements into a single path\nfunc (*AzureBlobFs) Join(elem ...string) string {\n\treturn strings.TrimPrefix(path.Join(elem...), \"/\")\n}\n\n// HasVirtualFolders returns true if folders are emulated\nfunc (*AzureBlobFs) HasVirtualFolders() bool {\n\treturn true\n}\n\n// ResolvePath returns the matching filesystem path for the specified sftp path\nfunc (fs *AzureBlobFs) ResolvePath(virtualPath string) (string, error) {\n\tif fs.mountPath != \"\" {\n\t\tif after, found := strings.CutPrefix(virtualPath, fs.mountPath); found {\n\t\t\tvirtualPath = after\n\t\t}\n\t}\n\tvirtualPath = path.Clean(\"/\" + virtualPath)\n\treturn fs.Join(fs.config.KeyPrefix, strings.TrimPrefix(virtualPath, \"/\")), nil\n}\n\n// CopyFile implements the FsFileCopier interface\nfunc (fs *AzureBlobFs) CopyFile(source, target string, srcInfo os.FileInfo) (int, int64, error) {\n\tnumFiles := 1\n\tsizeDiff := srcInfo.Size()\n\tattrs, err := fs.headObject(target)\n\tif err == nil {\n\t\tsizeDiff -= util.GetIntFromPointer(attrs.ContentLength)\n\t\tnumFiles = 0\n\t} else {\n\t\tif !fs.IsNotExist(err) {\n\t\t\treturn 0, 0, err\n\t\t}\n\t}\n\tif err := fs.copyFileInternal(source, target, srcInfo, true); err != nil {\n\t\treturn 0, 0, err\n\t}\n\treturn numFiles, sizeDiff, nil\n}\n\nfunc (fs *AzureBlobFs) headObject(name string) (blob.GetPropertiesResponse, error) {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.containerClient.NewBlockBlobClient(name).GetProperties(ctx, &blob.GetPropertiesOptions{})\n\n\tmetric.AZHeadObjectCompleted(err)\n\treturn resp, err\n}\n\n// GetMimeType returns the content type\nfunc (fs *AzureBlobFs) GetMimeType(name string) (string, error) {\n\tresponse, err := fs.headObject(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn util.GetStringFromPointer(response.ContentType), nil\n}\n\n// Close closes the fs\nfunc (*AzureBlobFs) Close() error {\n\treturn nil\n}\n\n// GetAvailableDiskSize returns the available size for the specified path\nfunc (*AzureBlobFs) GetAvailableDiskSize(_ string) (*sftp.StatVFS, error) {\n\treturn nil, ErrStorageSizeUnavailable\n}\n\nfunc (*AzureBlobFs) getPrefix(name string) string {\n\tprefix := \"\"\n\tif name != \"\" && name != \".\" {\n\t\tprefix = strings.TrimPrefix(name, \"/\")\n\t\tif !strings.HasSuffix(prefix, \"/\") {\n\t\t\tprefix += \"/\"\n\t\t}\n\t}\n\treturn prefix\n}\n\nfunc (fs *AzureBlobFs) isEqual(key string, virtualName string) bool {\n\tif key == virtualName {\n\t\treturn true\n\t}\n\tif key == virtualName+\"/\" {\n\t\treturn true\n\t}\n\tif key+\"/\" == virtualName {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (fs *AzureBlobFs) setConfigDefaults() {\n\tif fs.config.Endpoint == \"\" {\n\t\tfs.config.Endpoint = azureDefaultEndpoint\n\t}\n\tif fs.config.UploadPartSize == 0 {\n\t\tfs.config.UploadPartSize = 5\n\t}\n\tif fs.config.UploadPartSize < 1024*1024 {\n\t\tfs.config.UploadPartSize *= 1024 * 1024\n\t}\n\tif fs.config.UploadConcurrency == 0 {\n\t\tfs.config.UploadConcurrency = 5\n\t}\n\tif fs.config.DownloadPartSize == 0 {\n\t\tfs.config.DownloadPartSize = 5\n\t}\n\tif fs.config.DownloadPartSize < 1024*1024 {\n\t\tfs.config.DownloadPartSize *= 1024 * 1024\n\t}\n\tif fs.config.DownloadConcurrency == 0 {\n\t\tfs.config.DownloadConcurrency = 5\n\t}\n}\n\nfunc (fs *AzureBlobFs) copyFileInternal(source, target string, srcInfo os.FileInfo, updateModTime bool) error {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))\n\tdefer cancelFn()\n\n\tsrcBlob := fs.containerClient.NewBlockBlobClient(source)\n\tdstBlob := fs.containerClient.NewBlockBlobClient(target)\n\tresp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions(srcInfo, updateModTime))\n\tif err != nil {\n\t\tmetric.AZCopyObjectCompleted(err)\n\t\treturn err\n\t}\n\tcopyStatus := blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))\n\tnErrors := 0\n\tfor copyStatus == blob.CopyStatusTypePending {\n\t\t// Poll until the copy is complete.\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tresp, err := dstBlob.GetProperties(ctx, &blob.GetPropertiesOptions{})\n\t\tif err != nil {\n\t\t\t// A GetProperties failure may be transient, so allow a couple\n\t\t\t// of them before giving up.\n\t\t\tnErrors++\n\t\t\tif ctx.Err() != nil || nErrors == 3 {\n\t\t\t\tmetric.AZCopyObjectCompleted(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tcopyStatus = blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))\n\t\t}\n\t}\n\tif copyStatus != blob.CopyStatusTypeSuccess {\n\t\terr := fmt.Errorf(\"copy failed with status: %s\", copyStatus)\n\t\tmetric.AZCopyObjectCompleted(err)\n\t\treturn err\n\t}\n\n\tmetric.AZCopyObjectCompleted(nil)\n\treturn nil\n}\n\nfunc (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,\n\tupdateModTime bool,\n) (int, int64, error) {\n\tvar numFiles int\n\tvar filesSize int64\n\n\tif srcInfo.IsDir() {\n\t\tif renameMode == 0 {\n\t\t\thasContents, err := fs.hasContents(source)\n\t\t\tif err != nil {\n\t\t\t\treturn numFiles, filesSize, err\n\t\t\t}\n\t\t\tif hasContents {\n\t\t\t\treturn numFiles, filesSize, fmt.Errorf(\"%w: cannot rename non empty directory: %q\", ErrVfsUnsupported, source)\n\t\t\t}\n\t\t}\n\t\tif err := fs.mkdirInternal(target); err != nil {\n\t\t\treturn numFiles, filesSize, err\n\t\t}\n\t\tif renameMode == 1 {\n\t\t\tfiles, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)\n\t\t\tnumFiles += files\n\t\t\tfilesSize += size\n\t\t\tif err != nil {\n\t\t\t\treturn numFiles, filesSize, err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif err := fs.copyFileInternal(source, target, srcInfo, updateModTime); err != nil {\n\t\t\treturn numFiles, filesSize, err\n\t\t}\n\t\tnumFiles++\n\t\tfilesSize += srcInfo.Size()\n\t}\n\terr := fs.skipNotExistErr(fs.Remove(source, srcInfo.IsDir()))\n\treturn numFiles, filesSize, err\n}\n\nfunc (fs *AzureBlobFs) skipNotExistErr(err error) error {\n\tif fs.IsNotExist(err) {\n\t\treturn nil\n\t}\n\treturn err\n}\n\nfunc (fs *AzureBlobFs) mkdirInternal(name string) error {\n\t_, w, _, err := fs.Create(name, -1, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn w.Close()\n}\n\nfunc (fs *AzureBlobFs) hasContents(name string) (bool, error) {\n\tresult := false\n\tprefix := fs.getPrefix(name)\n\n\tmaxResults := int32(1)\n\tpager := fs.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{\n\t\tMaxResults: &maxResults,\n\t\tPrefix:     &prefix,\n\t})\n\n\tif pager.More() {\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\tresp, err := pager.NextPage(ctx)\n\t\tif err != nil {\n\t\t\tmetric.AZListObjectsCompleted(err)\n\t\t\treturn result, err\n\t\t}\n\n\t\tresult = len(resp.Segment.BlobItems) > 0\n\t}\n\n\tmetric.AZListObjectsCompleted(nil)\n\treturn result, nil\n}\n\nfunc (fs *AzureBlobFs) downloadPart(ctx context.Context, blockBlob *blockblob.Client, buf []byte,\n\tw io.WriterAt, offset, count, writeOffset int64,\n) error {\n\tif count == 0 {\n\t\treturn nil\n\t}\n\n\tresp, err := blockBlob.DownloadStream(ctx, &blob.DownloadStreamOptions{\n\t\tRange: blob.HTTPRange{\n\t\t\tOffset: offset,\n\t\t\tCount:  count,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\t_, err = io.ReadAtLeast(resp.Body, buf, int(count))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writeAtFull(w, buf, writeOffset, int(count))\n}\n\nfunc (fs *AzureBlobFs) handleMultipartDownload(ctx context.Context, blockBlob *blockblob.Client,\n\toffset int64, writer io.WriterAt, pipeReader PipeReader,\n) error {\n\tprops, err := blockBlob.GetProperties(ctx, &blob.GetPropertiesOptions{})\n\tmetric.AZHeadObjectCompleted(err)\n\tif err != nil {\n\t\tfsLog(fs, logger.LevelError, \"unable to get blob properties, download aborted: %+v\", err)\n\t\treturn err\n\t}\n\tif readMetadata > 0 && pipeReader != nil {\n\t\tpipeReader.setMetadataFromPointerVal(props.Metadata)\n\t}\n\tcontentLength := util.GetIntFromPointer(props.ContentLength)\n\tsizeToDownload := contentLength - offset\n\tif sizeToDownload < 0 {\n\t\tfsLog(fs, logger.LevelError, \"invalid multipart download size or offset, size: %v, offset: %v, size to download: %v\",\n\t\t\tcontentLength, offset, sizeToDownload)\n\t\treturn errors.New(\"the requested offset exceeds the file size\")\n\t}\n\tif sizeToDownload == 0 {\n\t\tfsLog(fs, logger.LevelDebug, \"nothing to download, offset %v, content length %v\", offset, contentLength)\n\t\treturn nil\n\t}\n\tpartSize := fs.config.DownloadPartSize\n\tguard := make(chan struct{}, fs.config.DownloadConcurrency)\n\tblockCtxTimeout := time.Duration(fs.config.DownloadPartSize/(1024*1024)) * time.Minute\n\tpool := newBufferAllocator(int(partSize))\n\tdefer pool.free()\n\n\tfinished := false\n\tvar wg sync.WaitGroup\n\tvar errOnce sync.Once\n\tvar hasError atomic.Bool\n\tvar poolError error\n\n\tpoolCtx, poolCancel := context.WithCancel(ctx)\n\tdefer poolCancel()\n\n\tfor part := 0; !finished; part++ {\n\t\tstart := offset\n\t\tend := offset + partSize\n\t\tif end >= contentLength {\n\t\t\tend = contentLength\n\t\t\tfinished = true\n\t\t}\n\t\twriteOffset := int64(part) * partSize\n\t\toffset = end\n\n\t\tguard <- struct{}{}\n\t\tif hasError.Load() {\n\t\t\tfsLog(fs, logger.LevelDebug, \"pool error, download for part %v not started\", part)\n\t\t\tbreak\n\t\t}\n\n\t\tbuf := pool.getBuffer()\n\t\twg.Add(1)\n\t\tgo func(start, end, writeOffset int64, buf []byte) {\n\t\t\tdefer func() {\n\t\t\t\tpool.releaseBuffer(buf)\n\t\t\t\t<-guard\n\t\t\t\twg.Done()\n\t\t\t}()\n\n\t\t\tinnerCtx, cancelFn := context.WithDeadline(poolCtx, time.Now().Add(blockCtxTimeout))\n\t\t\tdefer cancelFn()\n\n\t\t\tcount := end - start\n\n\t\t\terr := fs.downloadPart(innerCtx, blockBlob, buf, writer, start, count, writeOffset)\n\t\t\tif err != nil {\n\t\t\t\terrOnce.Do(func() {\n\t\t\t\t\tfsLog(fs, logger.LevelError, \"multipart download error: %+v\", err)\n\t\t\t\t\thasError.Store(true)\n\t\t\t\t\tpoolError = fmt.Errorf(\"multipart download error: %w\", err)\n\t\t\t\t\tpoolCancel()\n\t\t\t\t})\n\t\t\t}\n\t\t}(start, end, writeOffset, buf)\n\t}\n\n\twg.Wait()\n\tclose(guard)\n\n\treturn poolError\n}\n\nfunc (fs *AzureBlobFs) handleMultipartUpload(ctx context.Context, reader io.Reader,\n\tblockBlob *blockblob.Client, httpHeaders *blob.HTTPHeaders, metadata map[string]*string,\n) error {\n\tpartSize := fs.config.UploadPartSize\n\tguard := make(chan struct{}, fs.config.UploadConcurrency)\n\tblockCtxTimeout := time.Duration(fs.config.UploadPartSize/(1024*1024)) * time.Minute\n\n\t// sync.Pool seems to use a lot of memory so prefer our own, very simple, allocator\n\t// we only need to recycle few byte slices\n\tpool := newBufferAllocator(int(partSize))\n\tdefer pool.free()\n\n\tfinished := false\n\tvar blocks []string\n\tvar wg sync.WaitGroup\n\tvar errOnce sync.Once\n\tvar hasError atomic.Bool\n\tvar poolError error\n\n\tpoolCtx, poolCancel := context.WithCancel(ctx)\n\tdefer poolCancel()\n\n\tfinalizeFailedUpload := func(err error) {\n\t\tfsLog(fs, logger.LevelDebug, \"multipart upload error: %+v\", err)\n\t\thasError.Store(true)\n\t\tpoolError = fmt.Errorf(\"multipart upload error: %w\", err)\n\t\tpoolCancel()\n\t}\n\n\tfor part := 0; !finished; part++ {\n\t\tbuf := pool.getBuffer()\n\n\t\tn, err := readFill(reader, buf)\n\t\tif err == io.EOF {\n\t\t\t// read finished, if n > 0 we need to process the last data chunck\n\t\t\tif n == 0 {\n\t\t\t\tpool.releaseBuffer(buf)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfinished = true\n\t\t} else if err != nil {\n\t\t\tpool.releaseBuffer(buf)\n\t\t\terrOnce.Do(func() {\n\t\t\t\tfinalizeFailedUpload(err)\n\t\t\t})\n\t\t\tbreak\n\t\t}\n\n\t\t// Block IDs are unique values to avoid issue if 2+ clients are uploading blocks\n\t\t// at the same time causing CommitBlockList to get a mix of blocks from all the clients.\n\t\tgeneratedUUID, err := uuid.NewRandom()\n\t\tif err != nil {\n\t\t\tpool.releaseBuffer(buf)\n\t\t\terrOnce.Do(func() {\n\t\t\t\tfinalizeFailedUpload(err)\n\t\t\t})\n\t\t\tbreak\n\t\t}\n\t\tblockID := base64.StdEncoding.EncodeToString([]byte(generatedUUID.String()))\n\t\tblocks = append(blocks, blockID)\n\n\t\tguard <- struct{}{}\n\t\tif hasError.Load() {\n\t\t\tfsLog(fs, logger.LevelError, \"pool error, upload for part %d not started\", part)\n\t\t\tpool.releaseBuffer(buf)\n\t\t\tbreak\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo func(blockID string, buf []byte, bufSize int) {\n\t\t\tdefer func() {\n\t\t\t\tpool.releaseBuffer(buf)\n\t\t\t\t<-guard\n\t\t\t\twg.Done()\n\t\t\t}()\n\n\t\t\tbufferReader := &bytesReaderWrapper{\n\t\t\t\tReader: bytes.NewReader(buf[:bufSize]),\n\t\t\t}\n\t\t\tinnerCtx, cancelFn := context.WithDeadline(poolCtx, time.Now().Add(blockCtxTimeout))\n\t\t\tdefer cancelFn()\n\n\t\t\t_, err := blockBlob.StageBlock(innerCtx, blockID, bufferReader, &blockblob.StageBlockOptions{})\n\t\t\tif err != nil {\n\t\t\t\terrOnce.Do(func() {\n\t\t\t\t\tfsLog(fs, logger.LevelDebug, \"multipart upload error: %+v\", err)\n\t\t\t\t\tfinalizeFailedUpload(err)\n\t\t\t\t})\n\t\t\t}\n\t\t}(blockID, buf, n)\n\t}\n\n\twg.Wait()\n\tclose(guard)\n\n\tif poolError != nil {\n\t\treturn poolError\n\t}\n\n\tcommitOptions := blockblob.CommitBlockListOptions{\n\t\tHTTPHeaders: httpHeaders,\n\t\tMetadata:    metadata,\n\t}\n\tif fs.config.AccessTier != \"\" {\n\t\tcommitOptions.Tier = (*blob.AccessTier)(&fs.config.AccessTier)\n\t}\n\n\t_, err := blockBlob.CommitBlockList(ctx, blocks, &commitOptions)\n\treturn err\n}\n\nfunc (fs *AzureBlobFs) getCopyOptions(srcInfo os.FileInfo, updateModTime bool) *blob.StartCopyFromURLOptions {\n\tcopyOptions := &blob.StartCopyFromURLOptions{}\n\tif fs.config.AccessTier != \"\" {\n\t\tcopyOptions.Tier = (*blob.AccessTier)(&fs.config.AccessTier)\n\t}\n\tif updateModTime {\n\t\tmetadata := make(map[string]*string)\n\t\tfor k, v := range getMetadata(srcInfo) {\n\t\t\tif v != \"\" {\n\t\t\t\tif strings.EqualFold(k, lastModifiedField) {\n\t\t\t\t\tmetadata[k] = to.Ptr(\"0\")\n\t\t\t\t} else {\n\t\t\t\t\tmetadata[k] = to.Ptr(v)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(metadata) > 0 {\n\t\t\tcopyOptions.Metadata = metadata\n\t\t}\n\t}\n\n\treturn copyOptions\n}\n\nfunc (fs *AzureBlobFs) downloadToWriter(name string, w PipeWriter) (int64, error) {\n\tfsLog(fs, logger.LevelDebug, \"starting download before resuming upload, path %q\", name)\n\tctx, cancelFn := context.WithTimeout(context.Background(), preResumeTimeout)\n\tdefer cancelFn()\n\n\tblockBlob := fs.containerClient.NewBlockBlobClient(name)\n\terr := fs.handleMultipartDownload(ctx, blockBlob, 0, w, nil)\n\tn := w.GetWrittenBytes()\n\tfsLog(fs, logger.LevelDebug, \"download before resuming upload completed, path %q size: %d, err: %+v\",\n\t\tname, n, err)\n\tmetric.AZTransferCompleted(n, 1, err)\n\treturn n, err\n}\n\nfunc checkDirectoryMarkers(contentType string, metadata map[string]*string) bool {\n\tif contentType == dirMimeType {\n\t\treturn true\n\t}\n\tfor k, v := range metadata {\n\t\tif strings.EqualFold(k, azFolderKey) {\n\t\t\treturn strings.EqualFold(util.GetStringFromPointer(v), \"true\")\n\t\t}\n\t}\n\treturn false\n}\n\nfunc getAzContainerClientOptions() *container.ClientOptions {\n\treturn &container.ClientOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tTelemetry: policy.TelemetryOptions{\n\t\t\t\tApplicationID: version.GetVersionHash(),\n\t\t\t},\n\t\t},\n\t}\n}\n\ntype azureBlobDirLister struct {\n\tbaseDirLister\n\tpaginator     *runtime.Pager[container.ListBlobsHierarchyResponse]\n\ttimeout       time.Duration\n\tprefix        string\n\tprefixes      map[string]bool\n\tmetricUpdated bool\n}\n\nfunc (l *azureBlobDirLister) Next(limit int) ([]os.FileInfo, error) {\n\tif limit <= 0 {\n\t\treturn nil, errInvalidDirListerLimit\n\t}\n\tif len(l.cache) >= limit {\n\t\treturn l.returnFromCache(limit), nil\n\t}\n\tif !l.paginator.More() {\n\t\tif !l.metricUpdated {\n\t\t\tl.metricUpdated = true\n\t\t\tmetric.AZListObjectsCompleted(nil)\n\t\t}\n\t\treturn l.returnFromCache(limit), io.EOF\n\t}\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(l.timeout))\n\tdefer cancelFn()\n\n\tpage, err := l.paginator.NextPage(ctx)\n\tif err != nil {\n\t\tmetric.AZListObjectsCompleted(err)\n\t\treturn l.cache, err\n\t}\n\n\tfor _, blobPrefix := range page.Segment.BlobPrefixes {\n\t\tname := util.GetStringFromPointer(blobPrefix.Name)\n\t\t// we don't support prefixes == \"/\" this will be sent if a key starts with \"/\"\n\t\tif name == \"\" || name == \"/\" {\n\t\t\tcontinue\n\t\t}\n\t\t// sometime we have duplicate prefixes, maybe an Azurite bug\n\t\tname = strings.TrimPrefix(name, l.prefix)\n\t\tif _, ok := l.prefixes[strings.TrimSuffix(name, \"/\")]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tl.cache = append(l.cache, NewFileInfo(name, true, 0, time.Unix(0, 0), false))\n\t\tl.prefixes[strings.TrimSuffix(name, \"/\")] = true\n\t}\n\n\tfor _, blobItem := range page.Segment.BlobItems {\n\t\tname := util.GetStringFromPointer(blobItem.Name)\n\t\tname = strings.TrimPrefix(name, l.prefix)\n\t\tsize := int64(0)\n\t\tisDir := false\n\t\tvar metadata map[string]*string\n\t\tmodTime := time.Unix(0, 0)\n\t\tif blobItem.Properties != nil {\n\t\t\tsize = util.GetIntFromPointer(blobItem.Properties.ContentLength)\n\t\t\tmodTime = util.GetTimeFromPointer(blobItem.Properties.LastModified)\n\t\t\tcontentType := util.GetStringFromPointer(blobItem.Properties.ContentType)\n\t\t\tisDir = checkDirectoryMarkers(contentType, blobItem.Metadata)\n\t\t\tif isDir {\n\t\t\t\t// check if the dir is already included, it will be sent as blob prefix if it contains at least one item\n\t\t\t\tif _, ok := l.prefixes[name]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tl.prefixes[name] = true\n\t\t\t} else {\n\t\t\t\tmetadata = blobItem.Metadata\n\t\t\t}\n\t\t\tif val := getAzureLastModified(blobItem.Metadata); val > 0 {\n\t\t\t\tmodTime = util.GetTimeFromMsecSinceEpoch(val)\n\t\t\t}\n\t\t}\n\t\tinfo := NewFileInfo(name, isDir, size, modTime, false)\n\t\tinfo.setMetadataFromPointerVal(metadata)\n\t\tl.cache = append(l.cache, info)\n\t}\n\n\treturn l.returnFromCache(limit), nil\n}\n\nfunc (l *azureBlobDirLister) Close() error {\n\tclear(l.prefixes)\n\treturn l.baseDirLister.Close()\n}\n"
  },
  {
    "path": "internal/vfs/azblobfs_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build noazblob\n\npackage vfs\n\nimport (\n\t\"errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-azblob\")\n}\n\n// NewAzBlobFs returns an error, Azure Blob storage is disabled\nfunc NewAzBlobFs(_, _, _ string, _ AzBlobFsConfig) (Fs, error) {\n\treturn nil, errors.New(\"Azure Blob Storage disabled at build time\")\n}\n"
  },
  {
    "path": "internal/vfs/cryptfs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage vfs\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/minio/sio\"\n\t\"golang.org/x/crypto/hkdf\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\nconst (\n\t// cryptFsName is the name for the local Fs implementation with encryption support\n\tcryptFsName         = \"cryptfs\"\n\tversion10     byte  = 0x10\n\tnonceV10Size  int   = 32\n\theaderV10Size int64 = 33 // 1 (version byte) + 32 (nonce size)\n)\n\n// CryptFs is a Fs implementation that allows to encrypts/decrypts local files\ntype CryptFs struct {\n\t*OsFs\n\tlocalTempDir string\n\tmasterKey    []byte\n}\n\n// NewCryptFs returns a CryptFs object\nfunc NewCryptFs(connectionID, rootDir, mountPath string, config CryptFsConfig) (Fs, error) {\n\tif err := config.validate(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := config.Passphrase.TryDecrypt(); err != nil {\n\t\treturn nil, err\n\t}\n\tfs := &CryptFs{\n\t\tOsFs: &OsFs{\n\t\t\tname:            cryptFsName,\n\t\t\tconnectionID:    connectionID,\n\t\t\trootDir:         rootDir,\n\t\t\tmountPath:       getMountPath(mountPath),\n\t\t\treadBufferSize:  config.ReadBufferSize * 1024 * 1024,\n\t\t\twriteBufferSize: config.WriteBufferSize * 1024 * 1024,\n\t\t},\n\t\tmasterKey: []byte(config.Passphrase.GetPayload()),\n\t}\n\tif tempPath == \"\" {\n\t\tfs.localTempDir = rootDir\n\t} else {\n\t\tfs.localTempDir = tempPath\n\t}\n\treturn fs, nil\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *CryptFs) Name() string {\n\treturn fs.name\n}\n\n// Open opens the named file for reading\nfunc (fs *CryptFs) Open(name string, offset int64) (File, PipeReader, func(), error) {\n\tf, key, err := fs.getFileAndEncryptionKey(name)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tisZeroDownload, err := isZeroBytesDownload(f, offset)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeReader(r)\n\n\tgo func() {\n\t\tif isZeroDownload {\n\t\t\tw.CloseWithError(err) //nolint:errcheck\n\t\t\tf.Close()\n\t\t\tfsLog(fs, logger.LevelDebug, \"zero bytes download completed, path: %q\", name)\n\t\t\treturn\n\t\t}\n\t\tvar n int64\n\t\tvar err error\n\n\t\tif offset == 0 {\n\t\t\tn, err = fs.decryptWrapper(w, f, fs.getSIOConfig(key))\n\t\t} else {\n\t\t\tvar readerAt io.ReaderAt\n\t\t\tvar readed, written int\n\t\t\tbuf := make([]byte, 65568)\n\t\t\twrapper := &cryptedFileWrapper{\n\t\t\t\tFile: f,\n\t\t\t}\n\t\t\treaderAt, err = sio.DecryptReaderAt(wrapper, fs.getSIOConfig(key))\n\t\t\tif err == nil {\n\t\t\t\tfinished := false\n\t\t\t\tfor !finished {\n\t\t\t\t\treaded, err = readerAt.ReadAt(buf, offset)\n\t\t\t\t\toffset += int64(readed)\n\t\t\t\t\tif err != nil && err != io.EOF {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tif err == io.EOF {\n\t\t\t\t\t\tfinished = true\n\t\t\t\t\t\terr = nil\n\t\t\t\t\t}\n\t\t\t\t\tif readed > 0 {\n\t\t\t\t\t\twritten, err = w.Write(buf[:readed])\n\t\t\t\t\t\tn += int64(written)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tif err == io.EOF {\n\t\t\t\t\t\t\t\terr = io.ErrUnexpectedEOF\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif readed != written {\n\t\t\t\t\t\t\terr = io.ErrShortWrite\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}\n\t\t\t}\n\t\t}\n\t\tw.CloseWithError(err) //nolint:errcheck\n\t\tf.Close()\n\t\tfsLog(fs, logger.LevelDebug, \"download completed, path: %q size: %v, err: %v\", name, n, err)\n\t}()\n\n\treturn nil, p, nil, nil\n}\n\n// Create creates or opens the named file for writing\nfunc (fs *CryptFs) Create(name string, _, _ int) (File, PipeWriter, func(), error) {\n\tf, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\theader := encryptedFileHeader{\n\t\tversion: version10,\n\t\tnonce:   make([]byte, 32),\n\t}\n\t_, err = io.ReadFull(rand.Reader, header.nonce)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tvar key [32]byte\n\tkdf := hkdf.New(sha256.New, fs.masterKey, header.nonce, nil)\n\t_, err = io.ReadFull(kdf, key[:])\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\terr = header.Store(f)\n\tif err != nil {\n\t\tr.Close()\n\t\tw.Close()\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeWriter(w)\n\n\tgo func() {\n\t\tvar n int64\n\t\tvar err error\n\t\tif fs.writeBufferSize <= 0 {\n\t\t\tn, err = sio.Encrypt(f, r, fs.getSIOConfig(key))\n\t\t} else {\n\t\t\tbw := bufio.NewWriterSize(f, fs.writeBufferSize)\n\t\t\tn, err = fs.encryptWrapper(bw, r, fs.getSIOConfig(key))\n\t\t\terrFlush := bw.Flush()\n\t\t\tif err == nil && errFlush != nil {\n\t\t\t\terr = errFlush\n\t\t\t}\n\t\t}\n\t\terrClose := f.Close()\n\t\tif err == nil && errClose != nil {\n\t\t\terr = errClose\n\t\t}\n\t\tr.CloseWithError(err) //nolint:errcheck\n\t\tp.Done(err)\n\t\tfsLog(fs, logger.LevelDebug, \"upload completed, path: %q, readed bytes: %v, err: %v\", name, n, err)\n\t}()\n\n\treturn nil, p, nil, nil\n}\n\n// Truncate changes the size of the named file\nfunc (*CryptFs) Truncate(_ string, _ int64) error {\n\treturn ErrVfsUnsupported\n}\n\n// ReadDir reads the directory named by dirname and returns\n// a list of directory entries.\nfunc (fs *CryptFs) ReadDir(dirname string) (DirLister, error) {\n\tf, err := os.Open(dirname)\n\tif err != nil {\n\t\tif isInvalidNameError(err) {\n\t\t\terr = os.ErrNotExist\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn &cryptFsDirLister{f}, nil\n}\n\n// IsUploadResumeSupported returns false sio does not support random access writes\nfunc (*CryptFs) IsUploadResumeSupported() bool {\n\treturn false\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (*CryptFs) IsConditionalUploadResumeSupported(_ int64) bool {\n\treturn false\n}\n\n// GetMimeType returns the content type\nfunc (fs *CryptFs) GetMimeType(name string) (string, error) {\n\tf, key, err := fs.getFileAndEncryptionKey(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer f.Close()\n\n\treadSize, err := sio.DecryptedSize(512)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbuf := make([]byte, readSize)\n\tn, err := io.ReadFull(f, buf)\n\tif err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t\treturn \"\", err\n\t}\n\n\tdecrypted := bytes.NewBuffer(nil)\n\t_, err = sio.Decrypt(decrypted, bytes.NewBuffer(buf[:n]), fs.getSIOConfig(key))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tctype := http.DetectContentType(decrypted.Bytes())\n\t// Rewind file.\n\t_, err = f.Seek(0, io.SeekStart)\n\treturn ctype, err\n}\n\nfunc (fs *CryptFs) getSIOConfig(key [32]byte) sio.Config {\n\treturn sio.Config{\n\t\tMinVersion: sio.Version20,\n\t\tMaxVersion: sio.Version20,\n\t\tKey:        key[:],\n\t}\n}\n\n// ConvertFileInfo returns a FileInfo with the decrypted size\nfunc (fs *CryptFs) ConvertFileInfo(info os.FileInfo) os.FileInfo {\n\treturn convertCryptFsInfo(info)\n}\n\nfunc (fs *CryptFs) getFileAndEncryptionKey(name string) (*os.File, [32]byte, error) {\n\tvar key [32]byte\n\tf, err := os.Open(name)\n\tif err != nil {\n\t\treturn nil, key, err\n\t}\n\theader := encryptedFileHeader{}\n\terr = header.Load(f)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, key, err\n\t}\n\tkdf := hkdf.New(sha256.New, fs.masterKey, header.nonce, nil)\n\t_, err = io.ReadFull(kdf, key[:])\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, key, err\n\t}\n\treturn f, key, err\n}\n\nfunc (*CryptFs) encryptWrapper(dst io.Writer, src io.Reader, config sio.Config) (int64, error) {\n\tencReader, err := sio.EncryptReader(src, config)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn doCopy(dst, encReader, make([]byte, 65568))\n}\n\nfunc (fs *CryptFs) decryptWrapper(dst io.Writer, src io.Reader, config sio.Config) (int64, error) {\n\tif fs.readBufferSize <= 0 {\n\t\treturn sio.Decrypt(dst, src, config)\n\t}\n\tbr := bufio.NewReaderSize(src, fs.readBufferSize)\n\tdecReader, err := sio.DecryptReader(br, config)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn doCopy(dst, decReader, make([]byte, 65568))\n}\n\nfunc isZeroBytesDownload(f *os.File, offset int64) (bool, error) {\n\tinfo, err := f.Stat()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif info.Size() == headerV10Size {\n\t\treturn true, nil\n\t}\n\tif info.Size() > headerV10Size {\n\t\tdecSize, err := sio.DecryptedSize(uint64(info.Size() - headerV10Size))\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif int64(decSize) == offset {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\nfunc convertCryptFsInfo(info os.FileInfo) os.FileInfo {\n\tif !info.Mode().IsRegular() {\n\t\treturn info\n\t}\n\tsize := info.Size()\n\tif size >= headerV10Size {\n\t\tsize -= headerV10Size\n\t\tdecryptedSize, err := sio.DecryptedSize(uint64(size))\n\t\tif err == nil {\n\t\t\tsize = int64(decryptedSize)\n\t\t}\n\t} else {\n\t\tsize = 0\n\t}\n\treturn NewFileInfo(info.Name(), info.IsDir(), size, info.ModTime(), false)\n}\n\ntype encryptedFileHeader struct {\n\tversion byte\n\tnonce   []byte\n}\n\nfunc (h *encryptedFileHeader) Store(f *os.File) error {\n\tbuf := make([]byte, 0, headerV10Size)\n\tbuf = append(buf, version10)\n\tbuf = append(buf, h.nonce...)\n\t_, err := f.Write(buf)\n\treturn err\n}\n\nfunc (h *encryptedFileHeader) Load(f *os.File) error {\n\theader := make([]byte, 1+nonceV10Size)\n\t_, err := io.ReadFull(f, header)\n\tif err != nil {\n\t\treturn err\n\t}\n\th.version = header[0]\n\tif h.version == version10 {\n\t\th.nonce = header[1:]\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"unsupported encryption version: %v\", h.version)\n}\n\ntype cryptedFileWrapper struct {\n\t*os.File\n}\n\nfunc (w *cryptedFileWrapper) ReadAt(p []byte, offset int64) (n int, err error) {\n\treturn w.File.ReadAt(p, offset+headerV10Size)\n}\n\ntype cryptFsDirLister struct {\n\tf *os.File\n}\n\nfunc (l *cryptFsDirLister) Next(limit int) ([]os.FileInfo, error) {\n\tif limit <= 0 {\n\t\treturn nil, errInvalidDirListerLimit\n\t}\n\tfiles, err := l.f.Readdir(limit)\n\tfor idx := range files {\n\t\tfiles[idx] = convertCryptFsInfo(files[idx])\n\t}\n\treturn files, err\n}\n\nfunc (l *cryptFsDirLister) Close() error {\n\treturn l.f.Close()\n}\n"
  },
  {
    "path": "internal/vfs/fileinfo.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage vfs\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// FileInfo implements os.FileInfo for a Cloud Storage file.\ntype FileInfo struct {\n\tname        string\n\tsizeInBytes int64\n\tmodTime     time.Time\n\tmode        os.FileMode\n\tmetadata    map[string]string\n}\n\n// NewFileInfo creates file info.\nfunc NewFileInfo(name string, isDirectory bool, sizeInBytes int64, modTime time.Time, fullName bool) *FileInfo {\n\tmode := os.FileMode(0644)\n\tif isDirectory {\n\t\tmode = os.FileMode(0755) | os.ModeDir\n\t}\n\tif !fullName {\n\t\t// we have always Unix style paths here\n\t\tname = path.Base(name)\n\t}\n\n\treturn &FileInfo{\n\t\tname:        name,\n\t\tsizeInBytes: sizeInBytes,\n\t\tmodTime:     modTime,\n\t\tmode:        mode,\n\t}\n}\n\n// Name provides the base name of the file.\nfunc (fi *FileInfo) Name() string {\n\treturn fi.name\n}\n\n// Size provides the length in bytes for a file.\nfunc (fi *FileInfo) Size() int64 {\n\treturn fi.sizeInBytes\n}\n\n// Mode provides the file mode bits\nfunc (fi *FileInfo) Mode() os.FileMode {\n\treturn fi.mode\n}\n\n// ModTime provides the last modification time.\nfunc (fi *FileInfo) ModTime() time.Time {\n\treturn fi.modTime\n}\n\n// IsDir provides the abbreviation for Mode().IsDir()\nfunc (fi *FileInfo) IsDir() bool {\n\treturn fi.mode&os.ModeDir != 0\n}\n\n// SetMode sets the file mode\nfunc (fi *FileInfo) SetMode(mode os.FileMode) {\n\tfi.mode = mode\n}\n\n// Sys provides the underlying data source (can return nil)\nfunc (fi *FileInfo) Sys() any {\n\treturn fi.metadata\n}\n\nfunc (fi *FileInfo) setMetadata(value map[string]string) {\n\tfi.metadata = value\n}\n\nfunc (fi *FileInfo) setMetadataFromPointerVal(value map[string]*string) {\n\tif len(value) == 0 {\n\t\tfi.metadata = nil\n\t\treturn\n\t}\n\n\tfi.metadata = map[string]string{}\n\tfor k, v := range value {\n\t\tval := util.GetStringFromPointer(v)\n\t\tif val != \"\" {\n\t\t\tfi.metadata[k] = val\n\t\t}\n\t}\n}\n\nfunc getMetadata(fi os.FileInfo) map[string]string {\n\tif fi.Sys() == nil {\n\t\treturn nil\n\t}\n\tif val, ok := fi.Sys().(map[string]string); ok {\n\t\tif len(val) > 0 {\n\t\t\treturn val\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/vfs/filesystem.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage vfs\n\nimport (\n\t\"os\"\n\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\n// Filesystem defines filesystem details\ntype Filesystem struct {\n\tRedactedSecret string                 `json:\"-\"`\n\tProvider       sdk.FilesystemProvider `json:\"provider\"`\n\tOSConfig       sdk.OSFsConfig         `json:\"osconfig,omitempty\"`\n\tS3Config       S3FsConfig             `json:\"s3config,omitempty\"`\n\tGCSConfig      GCSFsConfig            `json:\"gcsconfig,omitempty\"`\n\tAzBlobConfig   AzBlobFsConfig         `json:\"azblobconfig,omitempty\"`\n\tCryptConfig    CryptFsConfig          `json:\"cryptconfig,omitempty\"`\n\tSFTPConfig     SFTPFsConfig           `json:\"sftpconfig,omitempty\"`\n\tHTTPConfig     HTTPFsConfig           `json:\"httpconfig,omitempty\"`\n}\n\n// SetEmptySecrets sets the secrets to empty\nfunc (f *Filesystem) SetEmptySecrets() {\n\tf.S3Config.AccessSecret = kms.NewEmptySecret()\n\tf.S3Config.SSECustomerKey = kms.NewEmptySecret()\n\tf.GCSConfig.Credentials = kms.NewEmptySecret()\n\tf.AzBlobConfig.AccountKey = kms.NewEmptySecret()\n\tf.AzBlobConfig.SASURL = kms.NewEmptySecret()\n\tf.CryptConfig.Passphrase = kms.NewEmptySecret()\n\tf.SFTPConfig.Password = kms.NewEmptySecret()\n\tf.SFTPConfig.PrivateKey = kms.NewEmptySecret()\n\tf.SFTPConfig.KeyPassphrase = kms.NewEmptySecret()\n\tf.HTTPConfig.Password = kms.NewEmptySecret()\n\tf.HTTPConfig.APIKey = kms.NewEmptySecret()\n}\n\n// SetEmptySecretsIfNil sets the secrets to empty if nil\nfunc (f *Filesystem) SetEmptySecretsIfNil() {\n\tif f.S3Config.AccessSecret == nil {\n\t\tf.S3Config.AccessSecret = kms.NewEmptySecret()\n\t}\n\tif f.S3Config.SSECustomerKey == nil {\n\t\tf.S3Config.SSECustomerKey = kms.NewEmptySecret()\n\t}\n\tif f.GCSConfig.Credentials == nil {\n\t\tf.GCSConfig.Credentials = kms.NewEmptySecret()\n\t}\n\tif f.AzBlobConfig.AccountKey == nil {\n\t\tf.AzBlobConfig.AccountKey = kms.NewEmptySecret()\n\t}\n\tif f.AzBlobConfig.SASURL == nil {\n\t\tf.AzBlobConfig.SASURL = kms.NewEmptySecret()\n\t}\n\tif f.CryptConfig.Passphrase == nil {\n\t\tf.CryptConfig.Passphrase = kms.NewEmptySecret()\n\t}\n\tif f.SFTPConfig.Password == nil {\n\t\tf.SFTPConfig.Password = kms.NewEmptySecret()\n\t}\n\tif f.SFTPConfig.PrivateKey == nil {\n\t\tf.SFTPConfig.PrivateKey = kms.NewEmptySecret()\n\t}\n\tif f.SFTPConfig.KeyPassphrase == nil {\n\t\tf.SFTPConfig.KeyPassphrase = kms.NewEmptySecret()\n\t}\n\tif f.HTTPConfig.Password == nil {\n\t\tf.HTTPConfig.Password = kms.NewEmptySecret()\n\t}\n\tif f.HTTPConfig.APIKey == nil {\n\t\tf.HTTPConfig.APIKey = kms.NewEmptySecret()\n\t}\n}\n\n// SetNilSecretsIfEmpty set the secrets to nil if empty.\n// This is useful before rendering as JSON so the empty fields\n// will not be serialized.\nfunc (f *Filesystem) SetNilSecretsIfEmpty() {\n\tif f.S3Config.AccessSecret != nil && f.S3Config.AccessSecret.IsEmpty() {\n\t\tf.S3Config.AccessSecret = nil\n\t}\n\tif f.S3Config.SSECustomerKey != nil && f.S3Config.SSECustomerKey.IsEmpty() {\n\t\tf.S3Config.SSECustomerKey = nil\n\t}\n\tif f.GCSConfig.Credentials != nil && f.GCSConfig.Credentials.IsEmpty() {\n\t\tf.GCSConfig.Credentials = nil\n\t}\n\tif f.AzBlobConfig.AccountKey != nil && f.AzBlobConfig.AccountKey.IsEmpty() {\n\t\tf.AzBlobConfig.AccountKey = nil\n\t}\n\tif f.AzBlobConfig.SASURL != nil && f.AzBlobConfig.SASURL.IsEmpty() {\n\t\tf.AzBlobConfig.SASURL = nil\n\t}\n\tif f.CryptConfig.Passphrase != nil && f.CryptConfig.Passphrase.IsEmpty() {\n\t\tf.CryptConfig.Passphrase = nil\n\t}\n\tf.SFTPConfig.setNilSecretsIfEmpty()\n\tf.HTTPConfig.setNilSecretsIfEmpty()\n}\n\n// IsEqual returns true if the fs is equal to other\nfunc (f *Filesystem) IsEqual(other Filesystem) bool {\n\tif f.Provider != other.Provider {\n\t\treturn false\n\t}\n\tswitch f.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\treturn f.S3Config.isEqual(other.S3Config)\n\tcase sdk.GCSFilesystemProvider:\n\t\treturn f.GCSConfig.isEqual(other.GCSConfig)\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\treturn f.AzBlobConfig.isEqual(other.AzBlobConfig)\n\tcase sdk.CryptedFilesystemProvider:\n\t\treturn f.CryptConfig.isEqual(other.CryptConfig)\n\tcase sdk.SFTPFilesystemProvider:\n\t\treturn f.SFTPConfig.isEqual(other.SFTPConfig)\n\tcase sdk.HTTPFilesystemProvider:\n\t\treturn f.HTTPConfig.isEqual(other.HTTPConfig)\n\tdefault:\n\t\treturn true\n\t}\n}\n\n// IsSameResource returns true if fs point to the same resource as other\nfunc (f *Filesystem) IsSameResource(other Filesystem) bool {\n\tif f.Provider != other.Provider {\n\t\treturn false\n\t}\n\tswitch f.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\treturn f.S3Config.isSameResource(other.S3Config)\n\tcase sdk.GCSFilesystemProvider:\n\t\treturn f.GCSConfig.isSameResource(other.GCSConfig)\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\treturn f.AzBlobConfig.isSameResource(other.AzBlobConfig)\n\tcase sdk.CryptedFilesystemProvider:\n\t\treturn f.CryptConfig.isSameResource(other.CryptConfig)\n\tcase sdk.SFTPFilesystemProvider:\n\t\treturn f.SFTPConfig.isSameResource(other.SFTPConfig)\n\tcase sdk.HTTPFilesystemProvider:\n\t\treturn f.HTTPConfig.isSameResource(other.HTTPConfig)\n\tdefault:\n\t\treturn true\n\t}\n}\n\n// GetPathSeparator returns the path separator\nfunc (f *Filesystem) GetPathSeparator() string {\n\tswitch f.Provider {\n\tcase sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider:\n\t\treturn string(os.PathSeparator)\n\tdefault:\n\t\treturn \"/\"\n\t}\n}\n\n// Validate verifies the FsConfig matching the configured provider and sets all other\n// Filesystem.*Config to their zero value if successful\nfunc (f *Filesystem) Validate(additionalData string) error {\n\tswitch f.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tif err := f.S3Config.ValidateAndEncryptCredentials(additionalData); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf.OSConfig = sdk.OSFsConfig{}\n\t\tf.GCSConfig = GCSFsConfig{}\n\t\tf.AzBlobConfig = AzBlobFsConfig{}\n\t\tf.CryptConfig = CryptFsConfig{}\n\t\tf.SFTPConfig = SFTPFsConfig{}\n\t\tf.HTTPConfig = HTTPFsConfig{}\n\t\treturn nil\n\tcase sdk.GCSFilesystemProvider:\n\t\tif err := f.GCSConfig.ValidateAndEncryptCredentials(additionalData); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf.OSConfig = sdk.OSFsConfig{}\n\t\tf.S3Config = S3FsConfig{}\n\t\tf.AzBlobConfig = AzBlobFsConfig{}\n\t\tf.CryptConfig = CryptFsConfig{}\n\t\tf.SFTPConfig = SFTPFsConfig{}\n\t\tf.HTTPConfig = HTTPFsConfig{}\n\t\treturn nil\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tif err := f.AzBlobConfig.ValidateAndEncryptCredentials(additionalData); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf.OSConfig = sdk.OSFsConfig{}\n\t\tf.S3Config = S3FsConfig{}\n\t\tf.GCSConfig = GCSFsConfig{}\n\t\tf.CryptConfig = CryptFsConfig{}\n\t\tf.SFTPConfig = SFTPFsConfig{}\n\t\tf.HTTPConfig = HTTPFsConfig{}\n\t\treturn nil\n\tcase sdk.CryptedFilesystemProvider:\n\t\tif err := f.CryptConfig.ValidateAndEncryptCredentials(additionalData); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf.OSConfig = sdk.OSFsConfig{}\n\t\tf.S3Config = S3FsConfig{}\n\t\tf.GCSConfig = GCSFsConfig{}\n\t\tf.AzBlobConfig = AzBlobFsConfig{}\n\t\tf.SFTPConfig = SFTPFsConfig{}\n\t\tf.HTTPConfig = HTTPFsConfig{}\n\t\treturn validateOSFsConfig(&f.CryptConfig.OSFsConfig)\n\tcase sdk.SFTPFilesystemProvider:\n\t\tif err := f.SFTPConfig.ValidateAndEncryptCredentials(additionalData); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf.OSConfig = sdk.OSFsConfig{}\n\t\tf.S3Config = S3FsConfig{}\n\t\tf.GCSConfig = GCSFsConfig{}\n\t\tf.AzBlobConfig = AzBlobFsConfig{}\n\t\tf.CryptConfig = CryptFsConfig{}\n\t\tf.HTTPConfig = HTTPFsConfig{}\n\t\treturn nil\n\tcase sdk.HTTPFilesystemProvider:\n\t\tif err := f.HTTPConfig.ValidateAndEncryptCredentials(additionalData); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf.OSConfig = sdk.OSFsConfig{}\n\t\tf.S3Config = S3FsConfig{}\n\t\tf.GCSConfig = GCSFsConfig{}\n\t\tf.AzBlobConfig = AzBlobFsConfig{}\n\t\tf.CryptConfig = CryptFsConfig{}\n\t\tf.SFTPConfig = SFTPFsConfig{}\n\t\treturn nil\n\tcase sdk.LocalFilesystemProvider:\n\t\tf.S3Config = S3FsConfig{}\n\t\tf.GCSConfig = GCSFsConfig{}\n\t\tf.AzBlobConfig = AzBlobFsConfig{}\n\t\tf.CryptConfig = CryptFsConfig{}\n\t\tf.SFTPConfig = SFTPFsConfig{}\n\t\tf.HTTPConfig = HTTPFsConfig{}\n\t\treturn validateOSFsConfig(&f.OSConfig)\n\tdefault:\n\t\treturn util.NewI18nError(\n\t\t\tutil.NewValidationError(\"invalid filesystem provider\"),\n\t\t\tutil.I18nErrorFsValidation,\n\t\t)\n\t}\n}\n\n// HasRedactedSecret returns true if configured the filesystem configuration has a redacted secret\nfunc (f *Filesystem) HasRedactedSecret() bool {\n\t// TODO move vfs specific code into each *FsConfig struct\n\tswitch f.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tif f.S3Config.SSECustomerKey.IsRedacted() {\n\t\t\treturn true\n\t\t}\n\t\treturn f.S3Config.AccessSecret.IsRedacted()\n\tcase sdk.GCSFilesystemProvider:\n\t\treturn f.GCSConfig.Credentials.IsRedacted()\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tif f.AzBlobConfig.AccountKey.IsRedacted() {\n\t\t\treturn true\n\t\t}\n\t\treturn f.AzBlobConfig.SASURL.IsRedacted()\n\tcase sdk.CryptedFilesystemProvider:\n\t\treturn f.CryptConfig.Passphrase.IsRedacted()\n\tcase sdk.SFTPFilesystemProvider:\n\t\tif f.SFTPConfig.Password.IsRedacted() {\n\t\t\treturn true\n\t\t}\n\t\tif f.SFTPConfig.PrivateKey.IsRedacted() {\n\t\t\treturn true\n\t\t}\n\t\treturn f.SFTPConfig.KeyPassphrase.IsRedacted()\n\tcase sdk.HTTPFilesystemProvider:\n\t\tif f.HTTPConfig.Password.IsRedacted() {\n\t\t\treturn true\n\t\t}\n\t\treturn f.HTTPConfig.APIKey.IsRedacted()\n\t}\n\n\treturn false\n}\n\n// HideConfidentialData hides filesystem confidential data\nfunc (f *Filesystem) HideConfidentialData() {\n\tswitch f.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tf.S3Config.HideConfidentialData()\n\tcase sdk.GCSFilesystemProvider:\n\t\tf.GCSConfig.HideConfidentialData()\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tf.AzBlobConfig.HideConfidentialData()\n\tcase sdk.CryptedFilesystemProvider:\n\t\tf.CryptConfig.HideConfidentialData()\n\tcase sdk.SFTPFilesystemProvider:\n\t\tf.SFTPConfig.HideConfidentialData()\n\tcase sdk.HTTPFilesystemProvider:\n\t\tf.HTTPConfig.HideConfidentialData()\n\t}\n}\n\n// GetACopy returns a filesystem copy\nfunc (f *Filesystem) GetACopy() Filesystem {\n\tf.SetEmptySecretsIfNil()\n\tfs := Filesystem{\n\t\tProvider: f.Provider,\n\t\tOSConfig: sdk.OSFsConfig{\n\t\t\tReadBufferSize:  f.OSConfig.ReadBufferSize,\n\t\t\tWriteBufferSize: f.OSConfig.WriteBufferSize,\n\t\t},\n\t\tS3Config: S3FsConfig{\n\t\t\tBaseS3FsConfig: sdk.BaseS3FsConfig{\n\t\t\t\tBucket:              f.S3Config.Bucket,\n\t\t\t\tRegion:              f.S3Config.Region,\n\t\t\t\tAccessKey:           f.S3Config.AccessKey,\n\t\t\t\tRoleARN:             f.S3Config.RoleARN,\n\t\t\t\tEndpoint:            f.S3Config.Endpoint,\n\t\t\t\tStorageClass:        f.S3Config.StorageClass,\n\t\t\t\tACL:                 f.S3Config.ACL,\n\t\t\t\tKeyPrefix:           f.S3Config.KeyPrefix,\n\t\t\t\tUploadPartSize:      f.S3Config.UploadPartSize,\n\t\t\t\tUploadConcurrency:   f.S3Config.UploadConcurrency,\n\t\t\t\tDownloadPartSize:    f.S3Config.DownloadPartSize,\n\t\t\t\tDownloadConcurrency: f.S3Config.DownloadConcurrency,\n\t\t\t\tDownloadPartMaxTime: f.S3Config.DownloadPartMaxTime,\n\t\t\t\tUploadPartMaxTime:   f.S3Config.UploadPartMaxTime,\n\t\t\t\tForcePathStyle:      f.S3Config.ForcePathStyle,\n\t\t\t\tSkipTLSVerify:       f.S3Config.SkipTLSVerify,\n\t\t\t},\n\t\t\tAccessSecret:   f.S3Config.AccessSecret.Clone(),\n\t\t\tSSECustomerKey: f.S3Config.SSECustomerKey.Clone(),\n\t\t},\n\t\tGCSConfig: GCSFsConfig{\n\t\t\tBaseGCSFsConfig: sdk.BaseGCSFsConfig{\n\t\t\t\tBucket:               f.GCSConfig.Bucket,\n\t\t\t\tAutomaticCredentials: f.GCSConfig.AutomaticCredentials,\n\t\t\t\tStorageClass:         f.GCSConfig.StorageClass,\n\t\t\t\tACL:                  f.GCSConfig.ACL,\n\t\t\t\tKeyPrefix:            f.GCSConfig.KeyPrefix,\n\t\t\t\tUploadPartSize:       f.GCSConfig.UploadPartSize,\n\t\t\t\tUploadPartMaxTime:    f.GCSConfig.UploadPartMaxTime,\n\t\t\t},\n\t\t\tCredentials: f.GCSConfig.Credentials.Clone(),\n\t\t},\n\t\tAzBlobConfig: AzBlobFsConfig{\n\t\t\tBaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{\n\t\t\t\tContainer:           f.AzBlobConfig.Container,\n\t\t\t\tAccountName:         f.AzBlobConfig.AccountName,\n\t\t\t\tEndpoint:            f.AzBlobConfig.Endpoint,\n\t\t\t\tKeyPrefix:           f.AzBlobConfig.KeyPrefix,\n\t\t\t\tUploadPartSize:      f.AzBlobConfig.UploadPartSize,\n\t\t\t\tUploadConcurrency:   f.AzBlobConfig.UploadConcurrency,\n\t\t\t\tDownloadPartSize:    f.AzBlobConfig.DownloadPartSize,\n\t\t\t\tDownloadConcurrency: f.AzBlobConfig.DownloadConcurrency,\n\t\t\t\tUseEmulator:         f.AzBlobConfig.UseEmulator,\n\t\t\t\tAccessTier:          f.AzBlobConfig.AccessTier,\n\t\t\t},\n\t\t\tAccountKey: f.AzBlobConfig.AccountKey.Clone(),\n\t\t\tSASURL:     f.AzBlobConfig.SASURL.Clone(),\n\t\t},\n\t\tCryptConfig: CryptFsConfig{\n\t\t\tOSFsConfig: sdk.OSFsConfig{\n\t\t\t\tReadBufferSize:  f.CryptConfig.ReadBufferSize,\n\t\t\t\tWriteBufferSize: f.CryptConfig.WriteBufferSize,\n\t\t\t},\n\t\t\tPassphrase: f.CryptConfig.Passphrase.Clone(),\n\t\t},\n\t\tSFTPConfig: SFTPFsConfig{\n\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\tEndpoint:                f.SFTPConfig.Endpoint,\n\t\t\t\tUsername:                f.SFTPConfig.Username,\n\t\t\t\tPrefix:                  f.SFTPConfig.Prefix,\n\t\t\t\tDisableCouncurrentReads: f.SFTPConfig.DisableCouncurrentReads,\n\t\t\t\tBufferSize:              f.SFTPConfig.BufferSize,\n\t\t\t\tEqualityCheckMode:       f.SFTPConfig.EqualityCheckMode,\n\t\t\t},\n\t\t\tPassword:      f.SFTPConfig.Password.Clone(),\n\t\t\tPrivateKey:    f.SFTPConfig.PrivateKey.Clone(),\n\t\t\tKeyPassphrase: f.SFTPConfig.KeyPassphrase.Clone(),\n\t\t},\n\t\tHTTPConfig: HTTPFsConfig{\n\t\t\tBaseHTTPFsConfig: sdk.BaseHTTPFsConfig{\n\t\t\t\tEndpoint:          f.HTTPConfig.Endpoint,\n\t\t\t\tUsername:          f.HTTPConfig.Username,\n\t\t\t\tSkipTLSVerify:     f.HTTPConfig.SkipTLSVerify,\n\t\t\t\tEqualityCheckMode: f.HTTPConfig.EqualityCheckMode,\n\t\t\t},\n\t\t\tPassword: f.HTTPConfig.Password.Clone(),\n\t\t\tAPIKey:   f.HTTPConfig.APIKey.Clone(),\n\t\t},\n\t}\n\tif len(f.SFTPConfig.Fingerprints) > 0 {\n\t\tfs.SFTPConfig.Fingerprints = make([]string, len(f.SFTPConfig.Fingerprints))\n\t\tcopy(fs.SFTPConfig.Fingerprints, f.SFTPConfig.Fingerprints)\n\t}\n\treturn fs\n}\n"
  },
  {
    "path": "internal/vfs/folder.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage vfs\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n)\n\n// BaseVirtualFolder defines the path for the virtual folder and the used quota limits.\n// The same folder can be shared among multiple users and each user can have different\n// quota limits or a different virtual path.\ntype BaseVirtualFolder struct {\n\tID            int64  `json:\"id\"`\n\tName          string `json:\"name\"`\n\tMappedPath    string `json:\"mapped_path,omitempty\"`\n\tDescription   string `json:\"description,omitempty\"`\n\tUsedQuotaSize int64  `json:\"used_quota_size\"`\n\t// Used quota as number of files\n\tUsedQuotaFiles int `json:\"used_quota_files\"`\n\t// Last quota update as unix timestamp in milliseconds\n\tLastQuotaUpdate int64 `json:\"last_quota_update\"`\n\t// list of usernames associated with this virtual folder\n\tUsers []string `json:\"users,omitempty\"`\n\t// list of group names associated with this virtual folder\n\tGroups []string `json:\"groups,omitempty\"`\n\t// Filesystem configuration details\n\tFsConfig Filesystem `json:\"filesystem\"`\n}\n\n// GetEncryptionAdditionalData returns the additional data to use for AEAD\nfunc (v *BaseVirtualFolder) GetEncryptionAdditionalData() string {\n\treturn fmt.Sprintf(\"folder_%v\", v.Name)\n}\n\n// GetACopy returns a copy\nfunc (v *BaseVirtualFolder) GetACopy() BaseVirtualFolder {\n\tusers := make([]string, len(v.Users))\n\tcopy(users, v.Users)\n\tgroups := make([]string, len(v.Groups))\n\tcopy(groups, v.Groups)\n\treturn BaseVirtualFolder{\n\t\tID:              v.ID,\n\t\tName:            v.Name,\n\t\tDescription:     v.Description,\n\t\tMappedPath:      v.MappedPath,\n\t\tUsedQuotaSize:   v.UsedQuotaSize,\n\t\tUsedQuotaFiles:  v.UsedQuotaFiles,\n\t\tLastQuotaUpdate: v.LastQuotaUpdate,\n\t\tUsers:           users,\n\t\tGroups:          v.Groups,\n\t\tFsConfig:        v.FsConfig.GetACopy(),\n\t}\n}\n\n// IsLocalOrLocalCrypted returns true if the folder provider is local or local encrypted\nfunc (v *BaseVirtualFolder) IsLocalOrLocalCrypted() bool {\n\treturn v.FsConfig.Provider == sdk.LocalFilesystemProvider || v.FsConfig.Provider == sdk.CryptedFilesystemProvider\n}\n\n// hideConfidentialData hides folder confidential data\nfunc (v *BaseVirtualFolder) hideConfidentialData() {\n\tswitch v.FsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tv.FsConfig.S3Config.HideConfidentialData()\n\tcase sdk.GCSFilesystemProvider:\n\t\tv.FsConfig.GCSConfig.HideConfidentialData()\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tv.FsConfig.AzBlobConfig.HideConfidentialData()\n\tcase sdk.CryptedFilesystemProvider:\n\t\tv.FsConfig.CryptConfig.HideConfidentialData()\n\tcase sdk.SFTPFilesystemProvider:\n\t\tv.FsConfig.SFTPConfig.HideConfidentialData()\n\tcase sdk.HTTPFilesystemProvider:\n\t\tv.FsConfig.HTTPConfig.HideConfidentialData()\n\t}\n}\n\n// PrepareForRendering prepares a folder for rendering.\n// It hides confidential data and set to nil the empty secrets\n// so they are not serialized\nfunc (v *BaseVirtualFolder) PrepareForRendering() {\n\tv.hideConfidentialData()\n\tv.FsConfig.SetEmptySecretsIfNil()\n}\n\n// HasRedactedSecret returns true if the folder has a redacted secret\nfunc (v *BaseVirtualFolder) HasRedactedSecret() bool {\n\treturn v.FsConfig.HasRedactedSecret()\n}\n\n// hasPathPlaceholder returns true if the folder has a path placeholder\nfunc (v *BaseVirtualFolder) hasPathPlaceholder() bool {\n\tplaceholders := []string{\"%username%\", \"%role%\"}\n\tvar config string\n\tswitch v.FsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\tconfig = v.FsConfig.S3Config.KeyPrefix\n\tcase sdk.GCSFilesystemProvider:\n\t\tconfig = v.FsConfig.GCSConfig.KeyPrefix\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\tconfig = v.FsConfig.AzBlobConfig.KeyPrefix\n\tcase sdk.SFTPFilesystemProvider:\n\t\tconfig = v.FsConfig.SFTPConfig.Prefix\n\tcase sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider:\n\t\tconfig = v.MappedPath\n\t}\n\tfor _, placeholder := range placeholders {\n\t\tif strings.Contains(config, placeholder) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// VirtualFolder defines a mapping between an SFTPGo virtual path and a\n// filesystem path outside the user home directory.\n// The specified paths must be absolute and the virtual path cannot be \"/\",\n// it must be a sub directory. The parent directory for the specified virtual\n// path must exist. SFTPGo will try to automatically create any missing\n// parent directory for the configured virtual folders at user login.\ntype VirtualFolder struct {\n\tBaseVirtualFolder\n\tVirtualPath string `json:\"virtual_path\"`\n\t// Maximum size allowed as bytes. 0 means unlimited, -1 included in user quota\n\tQuotaSize int64 `json:\"quota_size\"`\n\t// Maximum number of files allowed. 0 means unlimited, -1 included in user quota\n\tQuotaFiles int `json:\"quota_files\"`\n}\n\n// GetFilesystem returns the filesystem for this folder\nfunc (v *VirtualFolder) GetFilesystem(connectionID string, forbiddenSelfUsers []string) (Fs, error) {\n\tswitch v.FsConfig.Provider {\n\tcase sdk.S3FilesystemProvider:\n\t\treturn NewS3Fs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.S3Config)\n\tcase sdk.GCSFilesystemProvider:\n\t\treturn NewGCSFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.GCSConfig)\n\tcase sdk.AzureBlobFilesystemProvider:\n\t\treturn NewAzBlobFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.AzBlobConfig)\n\tcase sdk.CryptedFilesystemProvider:\n\t\treturn NewCryptFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.CryptConfig)\n\tcase sdk.SFTPFilesystemProvider:\n\t\treturn NewSFTPFs(connectionID, v.VirtualPath, v.MappedPath, forbiddenSelfUsers, v.FsConfig.SFTPConfig)\n\tcase sdk.HTTPFilesystemProvider:\n\t\treturn NewHTTPFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.HTTPConfig)\n\tdefault:\n\t\treturn NewOsFs(connectionID, v.MappedPath, v.VirtualPath, &v.FsConfig.OSConfig), nil\n\t}\n}\n\n// ScanQuota scans the folder and returns the number of files and their size\nfunc (v *VirtualFolder) ScanQuota() (int, int64, error) {\n\tif v.hasPathPlaceholder() {\n\t\treturn 0, 0, errors.New(\"cannot scan quota: this folder has a path placeholder\")\n\t}\n\tfs, err := v.GetFilesystem(xid.New().String(), nil)\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\tdefer fs.Close()\n\n\treturn fs.ScanRootDirContents()\n}\n\n// IsIncludedInUserQuota returns true if the virtual folder is included in user quota\nfunc (v *VirtualFolder) IsIncludedInUserQuota() bool {\n\treturn v.QuotaFiles == -1 && v.QuotaSize == -1\n}\n\n// HasNoQuotaRestrictions returns true if no quota restrictions need to be applyed\nfunc (v *VirtualFolder) HasNoQuotaRestrictions(checkFiles bool) bool {\n\tif v.QuotaSize == 0 && (!checkFiles || v.QuotaFiles == 0) {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// GetACopy returns a copy\nfunc (v *VirtualFolder) GetACopy() VirtualFolder {\n\treturn VirtualFolder{\n\t\tBaseVirtualFolder: v.BaseVirtualFolder.GetACopy(),\n\t\tVirtualPath:       v.VirtualPath,\n\t\tQuotaSize:         v.QuotaSize,\n\t\tQuotaFiles:        v.QuotaFiles,\n\t}\n}\n"
  },
  {
    "path": "internal/vfs/gcsfs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !nogcs\n\npackage vfs\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/rs/xid\"\n\t\"google.golang.org/api/googleapi\"\n\t\"google.golang.org/api/iterator\"\n\t\"google.golang.org/api/option\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\tdefaultGCSPageSize = 5000\n)\n\nvar (\n\tgcsDefaultFieldsSelection = []string{\"Name\", \"Size\", \"Deleted\", \"Updated\", \"ContentType\", \"Metadata\"}\n)\n\n// GCSFs is a Fs implementation for Google Cloud Storage.\ntype GCSFs struct {\n\tconnectionID string\n\tlocalTempDir string\n\t// if not empty this fs is mouted as virtual folder in the specified path\n\tmountPath      string\n\tconfig         *GCSFsConfig\n\tsvc            *storage.Client\n\tctxTimeout     time.Duration\n\tctxLongTimeout time.Duration\n}\n\nfunc init() {\n\tversion.AddFeature(\"+gcs\")\n}\n\n// NewGCSFs returns an GCSFs object that allows to interact with Google Cloud Storage\nfunc NewGCSFs(connectionID, localTempDir, mountPath string, config GCSFsConfig) (Fs, error) {\n\tif localTempDir == \"\" {\n\t\tlocalTempDir = getLocalTempDir()\n\t}\n\n\tvar err error\n\tfs := &GCSFs{\n\t\tconnectionID:   connectionID,\n\t\tlocalTempDir:   localTempDir,\n\t\tmountPath:      getMountPath(mountPath),\n\t\tconfig:         &config,\n\t\tctxTimeout:     30 * time.Second,\n\t\tctxLongTimeout: 300 * time.Second,\n\t}\n\tif err = fs.config.validate(); err != nil {\n\t\treturn fs, err\n\t}\n\tctx := context.Background()\n\tif fs.config.AutomaticCredentials > 0 {\n\t\tfs.svc, err = storage.NewClient(ctx,\n\t\t\tstorage.WithJSONReads(),\n\t\t\toption.WithUserAgent(version.GetVersionHash()),\n\t\t)\n\t} else {\n\t\terr = fs.config.Credentials.TryDecrypt()\n\t\tif err != nil {\n\t\t\treturn fs, err\n\t\t}\n\t\tfs.svc, err = storage.NewClient(ctx,\n\t\t\tstorage.WithJSONReads(),\n\t\t\toption.WithUserAgent(version.GetVersionHash()),\n\t\t\toption.WithAuthCredentialsJSON(option.ServiceAccount, []byte(fs.config.Credentials.GetPayload())),\n\t\t)\n\t}\n\treturn fs, err\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *GCSFs) Name() string {\n\treturn fmt.Sprintf(\"%s bucket %q\", gcsfsName, fs.config.Bucket)\n}\n\n// ConnectionID returns the connection ID associated to this Fs implementation\nfunc (fs *GCSFs) ConnectionID() string {\n\treturn fs.connectionID\n}\n\n// Stat returns a FileInfo describing the named file\nfunc (fs *GCSFs) Stat(name string) (os.FileInfo, error) {\n\tif name == \"\" || name == \"/\" || name == \".\" {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t}\n\tif fs.config.KeyPrefix == name+\"/\" {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t}\n\treturn fs.getObjectStat(name)\n}\n\n// Lstat returns a FileInfo describing the named file\nfunc (fs *GCSFs) Lstat(name string) (os.FileInfo, error) {\n\treturn fs.Stat(name)\n}\n\n// Open opens the named file for reading\nfunc (fs *GCSFs) Open(name string, offset int64) (File, PipeReader, func(), error) {\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeReader(r)\n\tif readMetadata > 0 {\n\t\tattrs, err := fs.headObject(name)\n\t\tif err != nil {\n\t\t\tr.Close()\n\t\t\tw.Close()\n\t\t\treturn nil, nil, nil, err\n\t\t}\n\t\tp.setMetadata(attrs.Metadata)\n\t}\n\tbkt := fs.svc.Bucket(fs.config.Bucket)\n\tobj := bkt.Object(name)\n\tctx, cancelFn := context.WithCancel(context.Background())\n\tobjectReader, err := obj.NewRangeReader(ctx, offset, -1)\n\tif err == nil && offset > 0 && objectReader.Attrs.ContentEncoding == \"gzip\" {\n\t\terr = fmt.Errorf(\"range request is not possible for gzip content encoding, requested offset %d\", offset)\n\t\tobjectReader.Close()\n\t}\n\tif err != nil {\n\t\tr.Close()\n\t\tw.Close()\n\t\tcancelFn()\n\t\treturn nil, nil, nil, err\n\t}\n\tgo func() {\n\t\tdefer cancelFn()\n\t\tdefer objectReader.Close()\n\n\t\tn, err := io.Copy(w, objectReader)\n\t\tw.CloseWithError(err) //nolint:errcheck\n\t\tfsLog(fs, logger.LevelDebug, \"download completed, path: %q size: %v, err: %+v\", name, n, err)\n\t\tmetric.GCSTransferCompleted(n, 1, err)\n\t}()\n\treturn nil, p, cancelFn, nil\n}\n\n// Create creates or opens the named file for writing\nfunc (fs *GCSFs) Create(name string, flag, checks int) (File, PipeWriter, func(), error) {\n\tif checks&CheckParentDir != 0 {\n\t\t_, err := fs.Stat(path.Dir(name))\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, err\n\t\t}\n\t}\n\tchunkSize := googleapi.DefaultUploadChunkSize\n\tif fs.config.UploadPartSize > 0 {\n\t\tchunkSize = int(fs.config.UploadPartSize) * 1024 * 1024\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, int64(chunkSize+1024*1024))\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tvar partialFileName string\n\tvar attrs *storage.ObjectAttrs\n\tvar statErr error\n\n\tbkt := fs.svc.Bucket(fs.config.Bucket)\n\tobj := bkt.Object(name)\n\n\tif flag == -1 {\n\t\tobj = obj.If(storage.Conditions{DoesNotExist: true})\n\t} else {\n\t\tattrs, statErr = fs.headObject(name)\n\t\tif statErr == nil {\n\t\t\tobj = obj.If(storage.Conditions{GenerationMatch: attrs.Generation})\n\t\t} else if fs.IsNotExist(statErr) {\n\t\t\tobj = obj.If(storage.Conditions{DoesNotExist: true})\n\t\t} else {\n\t\t\tfsLog(fs, logger.LevelWarn, \"unable to set precondition for %q, stat err: %v\", name, statErr)\n\t\t}\n\t}\n\tctx, cancelFn := context.WithCancel(context.Background())\n\n\tvar p PipeWriter\n\tvar objectWriter *storage.Writer\n\tif checks&CheckResume != 0 {\n\t\tif statErr != nil {\n\t\t\tcancelFn()\n\t\t\tr.Close()\n\t\t\tw.Close()\n\t\t\treturn nil, nil, nil, fmt.Errorf(\"unable to resume %q stat error: %w\", name, statErr)\n\t\t}\n\t\tp = newPipeWriterAtOffset(w, attrs.Size)\n\t\tpartialFileName = fs.getTempObject(name)\n\t\tpartialObj := bkt.Object(partialFileName)\n\t\tpartialObj = partialObj.If(storage.Conditions{DoesNotExist: true})\n\t\tobjectWriter = partialObj.NewWriter(ctx)\n\t} else {\n\t\tp = NewPipeWriter(w)\n\t\tobjectWriter = obj.NewWriter(ctx)\n\t}\n\n\tobjectWriter.ChunkSize = chunkSize\n\tif fs.config.UploadPartMaxTime > 0 {\n\t\tobjectWriter.ChunkRetryDeadline = time.Duration(fs.config.UploadPartMaxTime) * time.Second\n\t}\n\tfs.setWriterAttrs(objectWriter, flag, name)\n\n\tgo func() {\n\t\tdefer cancelFn()\n\n\t\tn, err := io.Copy(objectWriter, r)\n\t\tcloseErr := objectWriter.Close()\n\t\tif err == nil {\n\t\t\terr = closeErr\n\t\t}\n\t\tif err == nil && partialFileName != \"\" {\n\t\t\tpartialObject := bkt.Object(partialFileName)\n\t\t\tpartialObject = partialObject.If(storage.Conditions{GenerationMatch: objectWriter.Attrs().Generation})\n\t\t\terr = fs.composeObjects(ctx, obj, partialObject)\n\t\t}\n\t\tr.CloseWithError(err) //nolint:errcheck\n\t\tp.Done(err)\n\t\tfsLog(fs, logger.LevelDebug, \"upload completed, path: %q, acl: %q, readed bytes: %v, err: %+v\",\n\t\t\tname, fs.config.ACL, n, err)\n\t\tmetric.GCSTransferCompleted(n, 0, err)\n\t}()\n\n\tif uploadMode&8 != 0 {\n\t\treturn nil, p, nil, nil\n\t}\n\treturn nil, p, cancelFn, nil\n}\n\n// Rename renames (moves) source to target.\nfunc (fs *GCSFs) Rename(source, target string, checks int) (int, int64, error) {\n\tif source == target {\n\t\treturn -1, -1, nil\n\t}\n\tif checks&CheckParentDir != 0 {\n\t\t_, err := fs.Stat(path.Dir(target))\n\t\tif err != nil {\n\t\t\treturn -1, -1, err\n\t\t}\n\t}\n\tfi, err := fs.getObjectStat(source)\n\tif err != nil {\n\t\treturn -1, -1, err\n\t}\n\treturn fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (fs *GCSFs) Remove(name string, isDir bool) error {\n\tif isDir {\n\t\thasContents, err := fs.hasContents(name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif hasContents {\n\t\t\treturn fmt.Errorf(\"cannot remove non empty directory: %q\", name)\n\t\t}\n\t\tif !strings.HasSuffix(name, \"/\") {\n\t\t\tname += \"/\"\n\t\t}\n\t}\n\tobj := fs.svc.Bucket(fs.config.Bucket).Object(name)\n\tattrs, statErr := fs.headObject(name)\n\tif statErr == nil {\n\t\tobj = obj.If(storage.Conditions{GenerationMatch: attrs.Generation})\n\t} else {\n\t\tfsLog(fs, logger.LevelWarn, \"unable to set precondition for deleting %q, stat err: %v\",\n\t\t\tname, statErr)\n\t}\n\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\terr := obj.Delete(ctx)\n\tif isDir && fs.IsNotExist(err) {\n\t\t// we can have directories without a trailing \"/\" (created using v2.1.0 and before)\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\terr = fs.svc.Bucket(fs.config.Bucket).Object(strings.TrimSuffix(name, \"/\")).Delete(ctx)\n\t}\n\tmetric.GCSDeleteObjectCompleted(err)\n\treturn err\n}\n\n// Mkdir creates a new directory with the specified name and default permissions\nfunc (fs *GCSFs) Mkdir(name string) error {\n\t_, err := fs.Stat(name)\n\tif !fs.IsNotExist(err) {\n\t\treturn err\n\t}\n\treturn fs.mkdirInternal(name)\n}\n\n// Symlink creates source as a symbolic link to target.\nfunc (*GCSFs) Symlink(_, _ string) error {\n\treturn ErrVfsUnsupported\n}\n\n// Readlink returns the destination of the named symbolic link\nfunc (*GCSFs) Readlink(_ string) (string, error) {\n\treturn \"\", ErrVfsUnsupported\n}\n\n// Chown changes the numeric uid and gid of the named file.\nfunc (*GCSFs) Chown(_ string, _ int, _ int) error {\n\treturn ErrVfsUnsupported\n}\n\n// Chmod changes the mode of the named file to mode.\nfunc (*GCSFs) Chmod(_ string, _ os.FileMode) error {\n\treturn ErrVfsUnsupported\n}\n\n// Chtimes changes the access and modification times of the named file.\nfunc (fs *GCSFs) Chtimes(name string, _, mtime time.Time, isUploading bool) error {\n\tif isUploading {\n\t\treturn nil\n\t}\n\tobj := fs.svc.Bucket(fs.config.Bucket).Object(name)\n\tattrs, err := fs.headObject(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tobj = obj.If(storage.Conditions{MetagenerationMatch: attrs.Metageneration})\n\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tmetadata := attrs.Metadata\n\tif metadata == nil {\n\t\tmetadata = make(map[string]string)\n\t}\n\tmetadata[lastModifiedField] = strconv.FormatInt(mtime.UnixMilli(), 10)\n\n\tobjectAttrsToUpdate := storage.ObjectAttrsToUpdate{\n\t\tMetadata: metadata,\n\t}\n\t_, err = obj.Update(ctx, objectAttrsToUpdate)\n\n\treturn err\n}\n\n// Truncate changes the size of the named file.\n// Truncate by path is not supported, while truncating an opened\n// file is handled inside base transfer\nfunc (*GCSFs) Truncate(_ string, _ int64) error {\n\treturn ErrVfsUnsupported\n}\n\n// ReadDir reads the directory named by dirname and returns\n// a list of directory entries.\nfunc (fs *GCSFs) ReadDir(dirname string) (DirLister, error) {\n\t// dirname must be already cleaned\n\tprefix := fs.getPrefix(dirname)\n\tquery := &storage.Query{Prefix: prefix, Delimiter: \"/\"}\n\terr := query.SetAttrSelection(gcsDefaultFieldsSelection)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbkt := fs.svc.Bucket(fs.config.Bucket)\n\n\treturn &gcsDirLister{\n\t\tbucket:   bkt,\n\t\tquery:    query,\n\t\ttimeout:  fs.ctxTimeout,\n\t\tprefix:   prefix,\n\t\tprefixes: make(map[string]bool),\n\t}, nil\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported.\n// Resuming uploads is not supported on GCS\nfunc (*GCSFs) IsUploadResumeSupported() bool {\n\treturn false\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (*GCSFs) IsConditionalUploadResumeSupported(_ int64) bool {\n\treturn true\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported.\n// S3 uploads are already atomic, we don't need to upload to a temporary\n// file\nfunc (*GCSFs) IsAtomicUploadSupported() bool {\n\treturn false\n}\n\n// IsNotExist returns a boolean indicating whether the error is known to\n// report that a file or directory does not exist\nfunc (*GCSFs) IsNotExist(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tif errors.Is(err, storage.ErrObjectNotExist) {\n\t\treturn true\n\t}\n\tvar apiErr *googleapi.Error\n\tif errors.As(err, &apiErr) {\n\t\tif apiErr.Code == http.StatusNotFound {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// IsPermission returns a boolean indicating whether the error is known to\n// report that permission is denied.\nfunc (*GCSFs) IsPermission(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tvar apiErr *googleapi.Error\n\tif errors.As(err, &apiErr) {\n\t\tif apiErr.Code == http.StatusForbidden || apiErr.Code == http.StatusUnauthorized {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// IsNotSupported returns true if the error indicate an unsupported operation\nfunc (*GCSFs) IsNotSupported(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn errors.Is(err, ErrVfsUnsupported)\n}\n\n// CheckRootPath creates the specified local root directory if it does not exists\nfunc (fs *GCSFs) CheckRootPath(username string, uid int, gid int) bool {\n\t// we need a local directory for temporary files\n\tosFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, \"\", nil)\n\treturn osFs.CheckRootPath(username, uid, gid)\n}\n\n// ScanRootDirContents returns the number of files contained in the bucket,\n// and their size\nfunc (fs *GCSFs) ScanRootDirContents() (int, int64, error) {\n\treturn fs.GetDirSize(fs.config.KeyPrefix)\n}\n\n// GetDirSize returns the number of files and the size for a folder\n// including any subfolders\nfunc (fs *GCSFs) GetDirSize(dirname string) (int, int64, error) {\n\tprefix := fs.getPrefix(dirname)\n\tnumFiles := 0\n\tsize := int64(0)\n\n\tquery := &storage.Query{Prefix: prefix}\n\terr := query.SetAttrSelection(gcsDefaultFieldsSelection)\n\tif err != nil {\n\t\treturn numFiles, size, err\n\t}\n\n\titeratePage := func(nextPageToken string) (string, error) {\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\tbkt := fs.svc.Bucket(fs.config.Bucket)\n\t\tit := bkt.Objects(ctx, query)\n\t\tpager := iterator.NewPager(it, defaultGCSPageSize, nextPageToken)\n\n\t\tvar objects []*storage.ObjectAttrs\n\t\tpageToken, err := pager.NextPage(&objects)\n\t\tif err != nil {\n\t\t\treturn pageToken, err\n\t\t}\n\t\tfor _, attrs := range objects {\n\t\t\tif !attrs.Deleted.IsZero() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tisDir := strings.HasSuffix(attrs.Name, \"/\") || attrs.ContentType == dirMimeType\n\t\t\tif isDir && attrs.Size == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnumFiles++\n\t\t\tsize += attrs.Size\n\t\t}\n\t\treturn pageToken, nil\n\t}\n\n\tpageToken := \"\"\n\tfor {\n\t\tpageToken, err = iteratePage(pageToken)\n\t\tif err != nil {\n\t\t\tmetric.GCSListObjectsCompleted(err)\n\t\t\treturn numFiles, size, err\n\t\t}\n\t\tfsLog(fs, logger.LevelDebug, \"scan in progress for %q, files: %d, size: %d\", dirname, numFiles, size)\n\t\tif pageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tmetric.GCSListObjectsCompleted(nil)\n\treturn numFiles, size, err\n}\n\n// GetAtomicUploadPath returns the path to use for an atomic upload.\n// GCS uploads are already atomic, we never call this method for GCS\nfunc (*GCSFs) GetAtomicUploadPath(_ string) string {\n\treturn \"\"\n}\n\n// GetRelativePath returns the path for a file relative to the user's home dir.\n// This is the path as seen by SFTPGo users\nfunc (fs *GCSFs) GetRelativePath(name string) string {\n\trel := path.Clean(name)\n\tif rel == \".\" {\n\t\trel = \"\"\n\t}\n\tif !path.IsAbs(rel) {\n\t\trel = \"/\" + rel\n\t}\n\tif fs.config.KeyPrefix != \"\" {\n\t\tif !strings.HasPrefix(rel, \"/\"+fs.config.KeyPrefix) {\n\t\t\trel = \"/\"\n\t\t}\n\t\trel = path.Clean(\"/\" + strings.TrimPrefix(rel, \"/\"+fs.config.KeyPrefix))\n\t}\n\tif fs.mountPath != \"\" {\n\t\trel = path.Join(fs.mountPath, rel)\n\t}\n\treturn rel\n}\n\n// Walk walks the file tree rooted at root, calling walkFn for each file or\n// directory in the tree, including root\nfunc (fs *GCSFs) Walk(root string, walkFn filepath.WalkFunc) error {\n\tprefix := fs.getPrefix(root)\n\n\tquery := &storage.Query{Prefix: prefix}\n\terr := query.SetAttrSelection(gcsDefaultFieldsSelection)\n\tif err != nil {\n\t\twalkFn(root, nil, err) //nolint:errcheck\n\t\treturn err\n\t}\n\n\titeratePage := func(nextPageToken string) (string, error) {\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\tbkt := fs.svc.Bucket(fs.config.Bucket)\n\t\tit := bkt.Objects(ctx, query)\n\t\tpager := iterator.NewPager(it, defaultGCSPageSize, nextPageToken)\n\n\t\tvar objects []*storage.ObjectAttrs\n\t\tpageToken, err := pager.NextPage(&objects)\n\t\tif err != nil {\n\t\t\twalkFn(root, nil, err) //nolint:errcheck\n\t\t\treturn pageToken, err\n\t\t}\n\t\tfor _, attrs := range objects {\n\t\t\tif !attrs.Deleted.IsZero() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname, isDir := fs.resolve(attrs.Name, prefix, attrs.ContentType)\n\t\t\tif name == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tobjectModTime := attrs.Updated\n\t\t\tif val := getLastModified(attrs.Metadata); val > 0 {\n\t\t\t\tobjectModTime = util.GetTimeFromMsecSinceEpoch(val)\n\t\t\t}\n\t\t\terr = walkFn(attrs.Name, NewFileInfo(name, isDir, attrs.Size, objectModTime, false), nil)\n\t\t\tif err != nil {\n\t\t\t\treturn pageToken, err\n\t\t\t}\n\t\t}\n\n\t\treturn pageToken, nil\n\t}\n\n\tpageToken := \"\"\n\tfor {\n\t\tpageToken, err = iteratePage(pageToken)\n\t\tif err != nil {\n\t\t\tmetric.GCSListObjectsCompleted(err)\n\t\t\treturn err\n\t\t}\n\t\tif pageToken == \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\n\twalkFn(root, NewFileInfo(root, true, 0, time.Unix(0, 0), false), err) //nolint:errcheck\n\tmetric.GCSListObjectsCompleted(err)\n\treturn err\n}\n\n// Join joins any number of path elements into a single path\nfunc (*GCSFs) Join(elem ...string) string {\n\treturn strings.TrimPrefix(path.Join(elem...), \"/\")\n}\n\n// HasVirtualFolders returns true if folders are emulated\nfunc (*GCSFs) HasVirtualFolders() bool {\n\treturn true\n}\n\n// ResolvePath returns the matching filesystem path for the specified virtual path\nfunc (fs *GCSFs) ResolvePath(virtualPath string) (string, error) {\n\tif fs.mountPath != \"\" {\n\t\tif after, found := strings.CutPrefix(virtualPath, fs.mountPath); found {\n\t\t\tvirtualPath = after\n\t\t}\n\t}\n\tvirtualPath = path.Clean(\"/\" + virtualPath)\n\treturn fs.Join(fs.config.KeyPrefix, strings.TrimPrefix(virtualPath, \"/\")), nil\n}\n\n// CopyFile implements the FsFileCopier interface\nfunc (fs *GCSFs) CopyFile(source, target string, srcInfo os.FileInfo) (int, int64, error) {\n\tnumFiles := 1\n\tsizeDiff := srcInfo.Size()\n\tvar conditions *storage.Conditions\n\tattrs, err := fs.headObject(target)\n\tif err == nil {\n\t\tsizeDiff -= attrs.Size\n\t\tnumFiles = 0\n\t\tconditions = &storage.Conditions{GenerationMatch: attrs.Generation}\n\t} else {\n\t\tif !fs.IsNotExist(err) {\n\t\t\treturn 0, 0, err\n\t\t}\n\t\tconditions = &storage.Conditions{DoesNotExist: true}\n\t}\n\tif err := fs.copyFileInternal(source, target, conditions, srcInfo, true); err != nil {\n\t\treturn 0, 0, err\n\t}\n\treturn numFiles, sizeDiff, nil\n}\n\nfunc (fs *GCSFs) resolve(name, prefix, contentType string) (string, bool) {\n\tresult := strings.TrimPrefix(name, prefix)\n\tisDir := strings.HasSuffix(result, \"/\")\n\tif isDir {\n\t\tresult = strings.TrimSuffix(result, \"/\")\n\t}\n\tif contentType == dirMimeType {\n\t\tisDir = true\n\t}\n\treturn result, isDir\n}\n\n// getObjectStat returns the stat result\nfunc (fs *GCSFs) getObjectStat(name string) (os.FileInfo, error) {\n\tattrs, err := fs.headObject(name)\n\tif err == nil {\n\t\tobjSize := attrs.Size\n\t\tobjectModTime := attrs.Updated\n\t\tif val := getLastModified(attrs.Metadata); val > 0 {\n\t\t\tobjectModTime = util.GetTimeFromMsecSinceEpoch(val)\n\t\t}\n\t\tisDir := attrs.ContentType == dirMimeType || strings.HasSuffix(attrs.Name, \"/\")\n\t\tinfo := NewFileInfo(name, isDir, objSize, objectModTime, false)\n\t\tif !isDir {\n\t\t\tinfo.setMetadata(attrs.Metadata)\n\t\t}\n\t\treturn info, nil\n\t}\n\tif !fs.IsNotExist(err) {\n\t\treturn nil, err\n\t}\n\t// now check if this is a prefix (virtual directory)\n\thasContents, err := fs.hasContents(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif hasContents {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t}\n\t// finally check if this is an object with a trailing /\n\tattrs, err = fs.headObject(name + \"/\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tobjectModTime := attrs.Updated\n\tif val := getLastModified(attrs.Metadata); val > 0 {\n\t\tobjectModTime = util.GetTimeFromMsecSinceEpoch(val)\n\t}\n\treturn NewFileInfo(name, true, attrs.Size, objectModTime, false), nil\n}\n\nfunc (fs *GCSFs) setWriterAttrs(objectWriter *storage.Writer, flag int, name string) {\n\tvar contentType string\n\tif flag == -1 {\n\t\tcontentType = dirMimeType\n\t} else {\n\t\tcontentType = mime.TypeByExtension(path.Ext(name))\n\t}\n\tif contentType != \"\" {\n\t\tobjectWriter.ContentType = contentType\n\t}\n\tif fs.config.StorageClass != \"\" {\n\t\tobjectWriter.StorageClass = fs.config.StorageClass\n\t}\n\tif fs.config.ACL != \"\" {\n\t\tobjectWriter.PredefinedACL = fs.config.ACL\n\t}\n}\n\nfunc (fs *GCSFs) composeObjects(ctx context.Context, dst, partialObject *storage.ObjectHandle) error {\n\tfsLog(fs, logger.LevelDebug, \"start object compose for partial file %q, destination %q\",\n\t\tpartialObject.ObjectName(), dst.ObjectName())\n\tcomposer := dst.ComposerFrom(dst, partialObject)\n\tif fs.config.StorageClass != \"\" {\n\t\tcomposer.StorageClass = fs.config.StorageClass\n\t}\n\tif fs.config.ACL != \"\" {\n\t\tcomposer.PredefinedACL = fs.config.ACL\n\t}\n\tcontentType := mime.TypeByExtension(path.Ext(dst.ObjectName()))\n\tif contentType != \"\" {\n\t\tcomposer.ContentType = contentType\n\t}\n\t_, err := composer.Run(ctx)\n\tfsLog(fs, logger.LevelDebug, \"object compose for %q finished, err: %v\", dst.ObjectName(), err)\n\n\tdelCtx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\terrDelete := partialObject.Delete(delCtx)\n\tmetric.GCSDeleteObjectCompleted(errDelete)\n\tfsLog(fs, logger.LevelDebug, \"deleted partial file %q after composing with %q, err: %v\",\n\t\tpartialObject.ObjectName(), dst.ObjectName(), errDelete)\n\treturn err\n}\n\nfunc (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Conditions,\n\tsrcInfo os.FileInfo, updateModTime bool,\n) error {\n\tsrc := fs.svc.Bucket(fs.config.Bucket).Object(source)\n\tdst := fs.svc.Bucket(fs.config.Bucket).Object(target)\n\tif conditions != nil {\n\t\tdst = dst.If(*conditions)\n\t} else {\n\t\tattrs, err := fs.headObject(target)\n\t\tif err == nil {\n\t\t\tdst = dst.If(storage.Conditions{GenerationMatch: attrs.Generation})\n\t\t} else if fs.IsNotExist(err) {\n\t\t\tdst = dst.If(storage.Conditions{DoesNotExist: true})\n\t\t} else {\n\t\t\tfsLog(fs, logger.LevelWarn, \"unable to set precondition for copy, target %q, stat err: %v\",\n\t\t\t\ttarget, err)\n\t\t}\n\t}\n\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))\n\tdefer cancelFn()\n\n\tcopier := dst.CopierFrom(src)\n\tif fs.config.StorageClass != \"\" {\n\t\tcopier.StorageClass = fs.config.StorageClass\n\t}\n\tif fs.config.ACL != \"\" {\n\t\tcopier.PredefinedACL = fs.config.ACL\n\t}\n\tcontentType := mime.TypeByExtension(path.Ext(source))\n\tif contentType != \"\" {\n\t\tcopier.ContentType = contentType\n\t}\n\tmetadata := getMetadata(srcInfo)\n\tif updateModTime && len(metadata) > 0 {\n\t\tdelete(metadata, lastModifiedField)\n\t}\n\tif len(metadata) > 0 {\n\t\tcopier.Metadata = metadata\n\t}\n\t_, err := copier.Run(ctx)\n\tmetric.GCSCopyObjectCompleted(err)\n\treturn err\n}\n\nfunc (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,\n\tupdateModTime bool,\n) (int, int64, error) {\n\tvar numFiles int\n\tvar filesSize int64\n\n\tif srcInfo.IsDir() {\n\t\tif renameMode == 0 {\n\t\t\thasContents, err := fs.hasContents(source)\n\t\t\tif err != nil {\n\t\t\t\treturn numFiles, filesSize, err\n\t\t\t}\n\t\t\tif hasContents {\n\t\t\t\treturn numFiles, filesSize, fmt.Errorf(\"%w: cannot rename non empty directory: %q\", ErrVfsUnsupported, source)\n\t\t\t}\n\t\t}\n\t\tif err := fs.mkdirInternal(target); err != nil {\n\t\t\treturn numFiles, filesSize, err\n\t\t}\n\t\tif renameMode == 1 {\n\t\t\tfiles, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)\n\t\t\tnumFiles += files\n\t\t\tfilesSize += size\n\t\t\tif err != nil {\n\t\t\t\treturn numFiles, filesSize, err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif err := fs.copyFileInternal(source, target, nil, srcInfo, updateModTime); err != nil {\n\t\t\treturn numFiles, filesSize, err\n\t\t}\n\t\tnumFiles++\n\t\tfilesSize += srcInfo.Size()\n\t}\n\terr := fs.Remove(source, srcInfo.IsDir())\n\tif fs.IsNotExist(err) {\n\t\terr = nil\n\t}\n\treturn numFiles, filesSize, err\n}\n\nfunc (fs *GCSFs) mkdirInternal(name string) error {\n\tif !strings.HasSuffix(name, \"/\") {\n\t\tname += \"/\"\n\t}\n\t_, w, _, err := fs.Create(name, -1, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn w.Close()\n}\n\nfunc (fs *GCSFs) hasContents(name string) (bool, error) {\n\tresult := false\n\tprefix := fs.getPrefix(name)\n\tquery := &storage.Query{Prefix: prefix}\n\terr := query.SetAttrSelection(gcsDefaultFieldsSelection)\n\tif err != nil {\n\t\treturn result, err\n\t}\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tbkt := fs.svc.Bucket(fs.config.Bucket)\n\tit := bkt.Objects(ctx, query)\n\t// if we have a dir object with a trailing slash it will be returned so we set the size to 2\n\tpager := iterator.NewPager(it, 2, \"\")\n\n\tvar objects []*storage.ObjectAttrs\n\t_, err = pager.NextPage(&objects)\n\tif err != nil {\n\t\tmetric.GCSListObjectsCompleted(err)\n\t\treturn result, err\n\t}\n\n\tfor _, attrs := range objects {\n\t\tname, _ := fs.resolve(attrs.Name, prefix, attrs.ContentType)\n\t\t// a dir object with a trailing slash will result in an empty name\n\t\tif name == \"/\" || name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = true\n\t\tbreak\n\t}\n\n\tmetric.GCSListObjectsCompleted(nil)\n\treturn result, nil\n}\n\nfunc (fs *GCSFs) getPrefix(name string) string {\n\tprefix := \"\"\n\tif name != \"\" && name != \".\" && name != \"/\" {\n\t\tprefix = strings.TrimPrefix(name, \"/\")\n\t\tif !strings.HasSuffix(prefix, \"/\") {\n\t\t\tprefix += \"/\"\n\t\t}\n\t}\n\treturn prefix\n}\n\nfunc (fs *GCSFs) headObject(name string) (*storage.ObjectAttrs, error) {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tbkt := fs.svc.Bucket(fs.config.Bucket)\n\tobj := bkt.Object(name)\n\tattrs, err := obj.Attrs(ctx)\n\tmetric.GCSHeadObjectCompleted(err)\n\treturn attrs, err\n}\n\n// GetMimeType returns the content type\nfunc (fs *GCSFs) GetMimeType(name string) (string, error) {\n\tattrs, err := fs.headObject(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn attrs.ContentType, nil\n}\n\n// Close closes the fs\nfunc (fs *GCSFs) Close() error {\n\treturn nil\n}\n\n// GetAvailableDiskSize returns the available size for the specified path\nfunc (*GCSFs) GetAvailableDiskSize(_ string) (*sftp.StatVFS, error) {\n\treturn nil, ErrStorageSizeUnavailable\n}\n\nfunc (*GCSFs) getTempObject(name string) string {\n\tdir := filepath.Dir(name)\n\tguid := xid.New().String()\n\treturn filepath.Join(dir, \".sftpgo-partial.\"+guid+\".\"+filepath.Base(name))\n}\n\ntype gcsDirLister struct {\n\tbaseDirLister\n\tbucket        *storage.BucketHandle\n\tquery         *storage.Query\n\ttimeout       time.Duration\n\tnextPageToken string\n\tnoMorePages   bool\n\tprefix        string\n\tprefixes      map[string]bool\n\tmetricUpdated bool\n}\n\nfunc (l *gcsDirLister) resolve(name, contentType string) (string, bool) {\n\tresult := strings.TrimPrefix(name, l.prefix)\n\tisDir := strings.HasSuffix(result, \"/\")\n\tif isDir {\n\t\tresult = strings.TrimSuffix(result, \"/\")\n\t}\n\tif contentType == dirMimeType {\n\t\tisDir = true\n\t}\n\treturn result, isDir\n}\n\nfunc (l *gcsDirLister) Next(limit int) ([]os.FileInfo, error) {\n\tif limit <= 0 {\n\t\treturn nil, errInvalidDirListerLimit\n\t}\n\tif len(l.cache) >= limit {\n\t\treturn l.returnFromCache(limit), nil\n\t}\n\n\tif l.noMorePages {\n\t\tif !l.metricUpdated {\n\t\t\tl.metricUpdated = true\n\t\t\tmetric.GCSListObjectsCompleted(nil)\n\t\t}\n\t\treturn l.returnFromCache(limit), io.EOF\n\t}\n\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(l.timeout))\n\tdefer cancelFn()\n\n\tit := l.bucket.Objects(ctx, l.query)\n\tpaginator := iterator.NewPager(it, defaultGCSPageSize, l.nextPageToken)\n\tvar objects []*storage.ObjectAttrs\n\n\tpageToken, err := paginator.NextPage(&objects)\n\tif err != nil {\n\t\tmetric.GCSListObjectsCompleted(err)\n\t\treturn l.cache, err\n\t}\n\n\tfor _, attrs := range objects {\n\t\tif attrs.Prefix != \"\" {\n\t\t\tname, _ := l.resolve(attrs.Prefix, attrs.ContentType)\n\t\t\tif name == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := l.prefixes[name]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tl.cache = append(l.cache, NewFileInfo(name, true, 0, time.Unix(0, 0), false))\n\t\t\tl.prefixes[name] = true\n\t\t} else {\n\t\t\tname, isDir := l.resolve(attrs.Name, attrs.ContentType)\n\t\t\tif name == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !attrs.Deleted.IsZero() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif isDir {\n\t\t\t\t// check if the dir is already included, it will be sent as blob prefix if it contains at least one item\n\t\t\t\tif _, ok := l.prefixes[name]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tl.prefixes[name] = true\n\t\t\t}\n\t\t\tmodTime := attrs.Updated\n\t\t\tif val := getLastModified(attrs.Metadata); val > 0 {\n\t\t\t\tmodTime = util.GetTimeFromMsecSinceEpoch(val)\n\t\t\t}\n\t\t\tinfo := NewFileInfo(name, isDir, attrs.Size, modTime, false)\n\t\t\tinfo.setMetadata(attrs.Metadata)\n\t\t\tl.cache = append(l.cache, info)\n\t\t}\n\t}\n\n\tl.nextPageToken = pageToken\n\tl.noMorePages = (l.nextPageToken == \"\")\n\n\treturn l.returnFromCache(limit), nil\n}\n\nfunc (l *gcsDirLister) Close() error {\n\tclear(l.prefixes)\n\treturn l.baseDirLister.Close()\n}\n"
  },
  {
    "path": "internal/vfs/gcsfs_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build nogcs\n\npackage vfs\n\nimport (\n\t\"errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-gcs\")\n}\n\n// NewGCSFs returns an error, GCS is disabled\nfunc NewGCSFs(_, _, _ string, _ GCSFsConfig) (Fs, error) {\n\treturn nil, errors.New(\"Google Cloud Storage disabled at build time\")\n}\n"
  },
  {
    "path": "internal/vfs/httpfs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage vfs\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/sftp\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\t// httpFsName is the name for the HTTP Fs implementation\n\thttpFsName            = \"httpfs\"\n\tmaxHTTPFsResponseSize = 1048576\n)\n\nvar (\n\tsupportedEndpointSchema = []string{\"http://\", \"https://\"}\n)\n\n// HTTPFsConfig defines the configuration for HTTP based filesystem\ntype HTTPFsConfig struct {\n\tsdk.BaseHTTPFsConfig\n\tPassword *kms.Secret `json:\"password,omitempty\"`\n\tAPIKey   *kms.Secret `json:\"api_key,omitempty\"`\n}\n\nfunc (c *HTTPFsConfig) isUnixDomainSocket() bool {\n\treturn strings.HasPrefix(c.Endpoint, \"http://unix\") || strings.HasPrefix(c.Endpoint, \"https://unix\")\n}\n\n// HideConfidentialData hides confidential data\nfunc (c *HTTPFsConfig) HideConfidentialData() {\n\tif c.Password != nil {\n\t\tc.Password.Hide()\n\t}\n\tif c.APIKey != nil {\n\t\tc.APIKey.Hide()\n\t}\n}\n\nfunc (c *HTTPFsConfig) setNilSecretsIfEmpty() {\n\tif c.Password != nil && c.Password.IsEmpty() {\n\t\tc.Password = nil\n\t}\n\tif c.APIKey != nil && c.APIKey.IsEmpty() {\n\t\tc.APIKey = nil\n\t}\n}\n\nfunc (c *HTTPFsConfig) setEmptyCredentialsIfNil() {\n\tif c.Password == nil {\n\t\tc.Password = kms.NewEmptySecret()\n\t}\n\tif c.APIKey == nil {\n\t\tc.APIKey = kms.NewEmptySecret()\n\t}\n}\n\nfunc (c *HTTPFsConfig) isEqual(other HTTPFsConfig) bool {\n\tif c.Endpoint != other.Endpoint {\n\t\treturn false\n\t}\n\tif c.Username != other.Username {\n\t\treturn false\n\t}\n\tif c.SkipTLSVerify != other.SkipTLSVerify {\n\t\treturn false\n\t}\n\tc.setEmptyCredentialsIfNil()\n\tother.setEmptyCredentialsIfNil()\n\tif !c.Password.IsEqual(other.Password) {\n\t\treturn false\n\t}\n\treturn c.APIKey.IsEqual(other.APIKey)\n}\n\nfunc (c *HTTPFsConfig) isSameResource(other HTTPFsConfig) bool {\n\tif c.EqualityCheckMode > 0 || other.EqualityCheckMode > 0 {\n\t\tif c.Username != other.Username {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn c.Endpoint == other.Endpoint\n}\n\n// validate returns an error if the configuration is not valid\nfunc (c *HTTPFsConfig) validate() error {\n\tc.setEmptyCredentialsIfNil()\n\tif c.Endpoint == \"\" {\n\t\treturn util.NewI18nError(errors.New(\"httpfs: endpoint cannot be empty\"), util.I18nErrorEndpointRequired)\n\t}\n\tc.Endpoint = strings.TrimRight(c.Endpoint, \"/\")\n\tendpointURL, err := url.Parse(c.Endpoint)\n\tif err != nil {\n\t\treturn util.NewI18nError(fmt.Errorf(\"httpfs: invalid endpoint: %w\", err), util.I18nErrorEndpointInvalid)\n\t}\n\tif !util.IsStringPrefixInSlice(c.Endpoint, supportedEndpointSchema) {\n\t\treturn util.NewI18nError(\n\t\t\terrors.New(\"httpfs: invalid endpoint schema: http and https are supported\"),\n\t\t\tutil.I18nErrorEndpointInvalid,\n\t\t)\n\t}\n\tif endpointURL.Host == \"unix\" {\n\t\tsocketPath := endpointURL.Query().Get(\"socket_path\")\n\t\tif !filepath.IsAbs(socketPath) {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tfmt.Errorf(\"httpfs: invalid unix domain socket path: %q\", socketPath),\n\t\t\t\tutil.I18nErrorEndpointInvalid,\n\t\t\t)\n\t\t}\n\t}\n\tif !isEqualityCheckModeValid(c.EqualityCheckMode) {\n\t\treturn errors.New(\"invalid equality_check_mode\")\n\t}\n\tif c.Password.IsEncrypted() && !c.Password.IsValid() {\n\t\treturn errors.New(\"httpfs: invalid encrypted password\")\n\t}\n\tif !c.Password.IsEmpty() && !c.Password.IsValidInput() {\n\t\treturn errors.New(\"httpfs: invalid password\")\n\t}\n\tif c.APIKey.IsEncrypted() && !c.APIKey.IsValid() {\n\t\treturn errors.New(\"httpfs: invalid encrypted API key\")\n\t}\n\tif !c.APIKey.IsEmpty() && !c.APIKey.IsValidInput() {\n\t\treturn errors.New(\"httpfs: invalid API key\")\n\t}\n\treturn nil\n}\n\n// ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text\nfunc (c *HTTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {\n\terr := c.validate()\n\tif err != nil {\n\t\tvar errI18n *util.I18nError\n\t\terrValidation := util.NewValidationError(fmt.Sprintf(\"could not validate HTTP fs config: %v\", err))\n\t\tif errors.As(err, &errI18n) {\n\t\t\treturn util.NewI18nError(errValidation, errI18n.Message)\n\t\t}\n\t\treturn util.NewI18nError(errValidation, util.I18nErrorFsValidation)\n\t}\n\tif c.Password.IsPlain() {\n\t\tc.Password.SetAdditionalData(additionalData)\n\t\tif err := c.Password.Encrypt(); err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt HTTP fs password: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\tif c.APIKey.IsPlain() {\n\t\tc.APIKey.SetAdditionalData(additionalData)\n\t\tif err := c.APIKey.Encrypt(); err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt HTTP fs API key: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\n// HTTPFs is a Fs implementation for the SFTPGo HTTP filesystem backend\ntype HTTPFs struct {\n\tconnectionID string\n\tlocalTempDir string\n\t// if not empty this fs is mouted as virtual folder in the specified path\n\tmountPath  string\n\tconfig     *HTTPFsConfig\n\tclient     *http.Client\n\tctxTimeout time.Duration\n}\n\n// NewHTTPFs returns an HTTPFs object that allows to interact with SFTPGo HTTP filesystem backends\nfunc NewHTTPFs(connectionID, localTempDir, mountPath string, config HTTPFsConfig) (Fs, error) {\n\tif localTempDir == \"\" {\n\t\tlocalTempDir = getLocalTempDir()\n\t}\n\tconfig.setEmptyCredentialsIfNil()\n\tif !config.Password.IsEmpty() {\n\t\tif err := config.Password.TryDecrypt(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif !config.APIKey.IsEmpty() {\n\t\tif err := config.APIKey.TryDecrypt(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tfs := &HTTPFs{\n\t\tconnectionID: connectionID,\n\t\tlocalTempDir: localTempDir,\n\t\tmountPath:    mountPath,\n\t\tconfig:       &config,\n\t\tctxTimeout:   30 * time.Second,\n\t}\n\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\ttransport.MaxResponseHeaderBytes = 1 << 16\n\ttransport.WriteBufferSize = 1 << 16\n\ttransport.ReadBufferSize = 1 << 16\n\tif fs.config.isUnixDomainSocket() {\n\t\tendpointURL, err := url.Parse(fs.config.Endpoint)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif endpointURL.Host == \"unix\" {\n\t\t\tsocketPath := endpointURL.Query().Get(\"socket_path\")\n\t\t\tif !filepath.IsAbs(socketPath) {\n\t\t\t\treturn nil, fmt.Errorf(\"httpfs: invalid unix domain socket path: %q\", socketPath)\n\t\t\t}\n\t\t\tif endpointURL.Scheme == \"https\" {\n\t\t\t\ttransport.DialTLSContext = func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\t\tvar tlsConfig *tls.Config\n\t\t\t\t\tvar d tls.Dialer\n\t\t\t\t\tif config.SkipTLSVerify {\n\t\t\t\t\t\ttlsConfig = getInsecureTLSConfig()\n\t\t\t\t\t}\n\t\t\t\t\td.Config = tlsConfig\n\t\t\t\t\treturn d.DialContext(ctx, \"unix\", socketPath)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\t\tvar d net.Dialer\n\t\t\t\t\treturn d.DialContext(ctx, \"unix\", socketPath)\n\t\t\t\t}\n\t\t\t}\n\t\t\tendpointURL.Path = path.Join(endpointURL.Path, endpointURL.Query().Get(\"api_prefix\"))\n\t\t\tendpointURL.RawQuery = \"\"\n\t\t\tendpointURL.RawFragment = \"\"\n\t\t\tfs.config.Endpoint = endpointURL.String()\n\t\t}\n\t}\n\tif config.SkipTLSVerify {\n\t\tif transport.TLSClientConfig != nil {\n\t\t\ttransport.TLSClientConfig.InsecureSkipVerify = true\n\t\t} else {\n\t\t\ttransport.TLSClientConfig = getInsecureTLSConfig()\n\t\t}\n\t}\n\tfs.client = &http.Client{\n\t\tTransport: transport,\n\t}\n\treturn fs, nil\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *HTTPFs) Name() string {\n\treturn fmt.Sprintf(\"%v %q\", httpFsName, fs.config.Endpoint)\n}\n\n// ConnectionID returns the connection ID associated to this Fs implementation\nfunc (fs *HTTPFs) ConnectionID() string {\n\treturn fs.connectionID\n}\n\n// Stat returns a FileInfo describing the named file\nfunc (fs *HTTPFs) Stat(name string) (os.FileInfo, error) {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodGet, \"stat\", name, \"\", \"\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(io.LimitReader(resp.Body, maxHTTPFsResponseSize))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar response statResponse\n\terr = json.Unmarshal(respBody, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn response.getFileInfo(), nil\n}\n\n// Lstat returns a FileInfo describing the named file\nfunc (fs *HTTPFs) Lstat(name string) (os.FileInfo, error) {\n\treturn fs.Stat(name)\n}\n\n// Open opens the named file for reading\nfunc (fs *HTTPFs) Open(name string, offset int64) (File, PipeReader, func(), error) {\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeReader(r)\n\tctx, cancelFn := context.WithCancel(context.Background())\n\n\tvar queryString string\n\tif offset > 0 {\n\t\tqueryString = fmt.Sprintf(\"?offset=%d\", offset)\n\t}\n\n\tgo func() {\n\t\tdefer cancelFn()\n\n\t\tresp, err := fs.sendHTTPRequest(ctx, http.MethodGet, \"open\", name, queryString, \"\", nil)\n\t\tif err != nil {\n\t\t\tfsLog(fs, logger.LevelError, \"download error, path %q, err: %v\", name, err)\n\t\t\tw.CloseWithError(err) //nolint:errcheck\n\t\t\tmetric.HTTPFsTransferCompleted(0, 1, err)\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tn, err := io.Copy(w, resp.Body)\n\t\tw.CloseWithError(err) //nolint:errcheck\n\t\tfsLog(fs, logger.LevelDebug, \"download completed, path %q size: %v, err: %+v\", name, n, err)\n\t\tmetric.HTTPFsTransferCompleted(n, 1, err)\n\t}()\n\n\treturn nil, p, cancelFn, nil\n}\n\n// Create creates or opens the named file for writing\nfunc (fs *HTTPFs) Create(name string, flag, checks int) (File, PipeWriter, func(), error) {\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeWriter(w)\n\tctx, cancelFn := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\tdefer cancelFn()\n\n\t\tcontentType := mime.TypeByExtension(path.Ext(name))\n\t\tqueryString := fmt.Sprintf(\"?flags=%d&checks=%d\", flag, checks)\n\t\tresp, err := fs.sendHTTPRequest(ctx, http.MethodPost, \"create\", name, queryString, contentType,\n\t\t\t&wrapReader{reader: r})\n\t\tif err != nil {\n\t\t\tfsLog(fs, logger.LevelError, \"upload error, path %q, err: %v\", name, err)\n\t\t\tr.CloseWithError(err) //nolint:errcheck\n\t\t\tp.Done(err)\n\t\t\tmetric.HTTPFsTransferCompleted(0, 0, err)\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tr.CloseWithError(err) //nolint:errcheck\n\t\tp.Done(err)\n\t\tfsLog(fs, logger.LevelDebug, \"upload completed, path: %q, readed bytes: %d\", name, r.GetReadedBytes())\n\t\tmetric.HTTPFsTransferCompleted(r.GetReadedBytes(), 0, err)\n\t}()\n\n\treturn nil, p, cancelFn, nil\n}\n\n// Rename renames (moves) source to target.\nfunc (fs *HTTPFs) Rename(source, target string, checks int) (int, int64, error) {\n\tif source == target {\n\t\treturn -1, -1, nil\n\t}\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tqueryString := fmt.Sprintf(\"?target=%s\", url.QueryEscape(target))\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, \"rename\", source, queryString, \"\", nil)\n\tif err != nil {\n\t\treturn -1, -1, err\n\t}\n\tdefer resp.Body.Close()\n\tif checks&CheckUpdateModTime != 0 {\n\t\tfs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck\n\t}\n\treturn -1, -1, nil\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (fs *HTTPFs) Remove(name string, _ bool) error {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodDelete, \"remove\", name, \"\", \"\", nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\treturn nil\n}\n\n// Mkdir creates a new directory with the specified name and default permissions\nfunc (fs *HTTPFs) Mkdir(name string) error {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodPost, \"mkdir\", name, \"\", \"\", nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\treturn nil\n}\n\n// Symlink creates source as a symbolic link to target.\nfunc (*HTTPFs) Symlink(_, _ string) error {\n\treturn ErrVfsUnsupported\n}\n\n// Readlink returns the destination of the named symbolic link\nfunc (*HTTPFs) Readlink(_ string) (string, error) {\n\treturn \"\", ErrVfsUnsupported\n}\n\n// Chown changes the numeric uid and gid of the named file.\nfunc (fs *HTTPFs) Chown(_ string, _ int, _ int) error {\n\treturn ErrVfsUnsupported\n}\n\n// Chmod changes the mode of the named file to mode.\nfunc (fs *HTTPFs) Chmod(name string, mode os.FileMode) error {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tqueryString := fmt.Sprintf(\"?mode=%d\", mode)\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, \"chmod\", name, queryString, \"\", nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\treturn nil\n}\n\n// Chtimes changes the access and modification times of the named file.\nfunc (fs *HTTPFs) Chtimes(name string, atime, mtime time.Time, _ bool) error {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tqueryString := fmt.Sprintf(\"?access_time=%s&modification_time=%s\", atime.UTC().Format(time.RFC3339),\n\t\tmtime.UTC().Format(time.RFC3339))\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, \"chtimes\", name, queryString, \"\", nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\treturn nil\n}\n\n// Truncate changes the size of the named file.\n// Truncate by path is not supported, while truncating an opened\n// file is handled inside base transfer\nfunc (fs *HTTPFs) Truncate(name string, size int64) error {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tqueryString := fmt.Sprintf(\"?size=%d\", size)\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, \"truncate\", name, queryString, \"\", nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\treturn nil\n}\n\n// ReadDir reads the directory named by dirname and returns\n// a list of directory entries.\nfunc (fs *HTTPFs) ReadDir(dirname string) (DirLister, error) {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodGet, \"readdir\", dirname, \"\", \"\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(io.LimitReader(resp.Body, maxHTTPFsResponseSize*10))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar response []statResponse\n\terr = json.Unmarshal(respBody, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]os.FileInfo, 0, len(response))\n\tfor _, stat := range response {\n\t\tresult = append(result, stat.getFileInfo())\n\t}\n\treturn &baseDirLister{result}, nil\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported.\nfunc (*HTTPFs) IsUploadResumeSupported() bool {\n\treturn false\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (*HTTPFs) IsConditionalUploadResumeSupported(_ int64) bool {\n\treturn false\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported.\nfunc (*HTTPFs) IsAtomicUploadSupported() bool {\n\treturn false\n}\n\n// IsNotExist returns a boolean indicating whether the error is known to\n// report that a file or directory does not exist\nfunc (*HTTPFs) IsNotExist(err error) bool {\n\treturn errors.Is(err, fs.ErrNotExist)\n}\n\n// IsPermission returns a boolean indicating whether the error is known to\n// report that permission is denied.\nfunc (*HTTPFs) IsPermission(err error) bool {\n\treturn errors.Is(err, fs.ErrPermission)\n}\n\n// IsNotSupported returns true if the error indicate an unsupported operation\nfunc (*HTTPFs) IsNotSupported(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn err == ErrVfsUnsupported\n}\n\n// CheckRootPath creates the specified local root directory if it does not exists\nfunc (fs *HTTPFs) CheckRootPath(username string, uid int, gid int) bool {\n\t// we need a local directory for temporary files\n\tosFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, \"\", nil)\n\treturn osFs.CheckRootPath(username, uid, gid)\n}\n\n// ScanRootDirContents returns the number of files and their size\nfunc (fs *HTTPFs) ScanRootDirContents() (int, int64, error) {\n\treturn fs.GetDirSize(\"/\")\n}\n\n// CheckMetadata checks the metadata consistency\nfunc (*HTTPFs) CheckMetadata() error {\n\treturn nil\n}\n\n// GetDirSize returns the number of files and the size for a folder\n// including any subfolders\nfunc (fs *HTTPFs) GetDirSize(dirname string) (int, int64, error) {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodGet, \"dirsize\", dirname, \"\", \"\", nil)\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(io.LimitReader(resp.Body, maxHTTPFsResponseSize))\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\n\tvar response dirSizeResponse\n\terr = json.Unmarshal(respBody, &response)\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\treturn response.Files, response.Size, nil\n}\n\n// GetAtomicUploadPath returns the path to use for an atomic upload.\nfunc (*HTTPFs) GetAtomicUploadPath(_ string) string {\n\treturn \"\"\n}\n\n// GetRelativePath returns the path for a file relative to the user's home dir.\n// This is the path as seen by SFTPGo users\nfunc (fs *HTTPFs) GetRelativePath(name string) string {\n\trel := path.Clean(name)\n\tif rel == \".\" {\n\t\trel = \"\"\n\t}\n\tif !path.IsAbs(rel) {\n\t\trel = \"/\" + rel\n\t}\n\tif fs.mountPath != \"\" {\n\t\trel = path.Join(fs.mountPath, rel)\n\t}\n\treturn rel\n}\n\n// Walk walks the file tree rooted at root, calling walkFn for each file or\n// directory in the tree, including root. The result are unordered\nfunc (fs *HTTPFs) Walk(root string, walkFn filepath.WalkFunc) error {\n\tinfo, err := fs.Lstat(root)\n\tif err != nil {\n\t\treturn walkFn(root, nil, err)\n\t}\n\treturn fs.walk(root, info, walkFn)\n}\n\n// Join joins any number of path elements into a single path\nfunc (*HTTPFs) Join(elem ...string) string {\n\treturn strings.TrimPrefix(path.Join(elem...), \"/\")\n}\n\n// HasVirtualFolders returns true if folders are emulated\nfunc (*HTTPFs) HasVirtualFolders() bool {\n\treturn false\n}\n\n// ResolvePath returns the matching filesystem path for the specified virtual path\nfunc (fs *HTTPFs) ResolvePath(virtualPath string) (string, error) {\n\tif fs.mountPath != \"\" {\n\t\tif after, found := strings.CutPrefix(virtualPath, fs.mountPath); found {\n\t\t\tvirtualPath = after\n\t\t}\n\t}\n\treturn path.Clean(\"/\" + virtualPath), nil\n}\n\n// GetMimeType returns the content type\nfunc (fs *HTTPFs) GetMimeType(name string) (string, error) {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodGet, \"stat\", name, \"\", \"\", nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(io.LimitReader(resp.Body, maxHTTPFsResponseSize))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar response mimeTypeResponse\n\terr = json.Unmarshal(respBody, &response)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn response.Mime, nil\n}\n\n// Close closes the fs\nfunc (fs *HTTPFs) Close() error {\n\tfs.client.CloseIdleConnections()\n\treturn nil\n}\n\n// GetAvailableDiskSize returns the available size for the specified path\nfunc (fs *HTTPFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.sendHTTPRequest(ctx, http.MethodGet, \"statvfs\", dirName, \"\", \"\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(io.LimitReader(resp.Body, maxHTTPFsResponseSize))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar response statVFSResponse\n\terr = json.Unmarshal(respBody, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn response.toSFTPStatVFS(), nil\n}\n\nfunc (fs *HTTPFs) sendHTTPRequest(ctx context.Context, method, base, name, queryString, contentType string,\n\tbody io.Reader,\n) (*http.Response, error) {\n\turl := fmt.Sprintf(\"%s/%s/%s%s\", fs.config.Endpoint, base, url.PathEscape(name), queryString)\n\treq, err := http.NewRequest(method, url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif contentType != \"\" {\n\t\treq.Header.Set(\"Content-Type\", contentType)\n\t}\n\tif fs.config.APIKey.GetPayload() != \"\" {\n\t\treq.Header.Set(\"X-API-KEY\", fs.config.APIKey.GetPayload())\n\t}\n\tif fs.config.Username != \"\" || fs.config.Password.GetPayload() != \"\" {\n\t\treq.SetBasicAuth(fs.config.Username, fs.config.Password.GetPayload())\n\t}\n\tresp, err := fs.client.Do(req.WithContext(ctx))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to send HTTP request to URL %v: %w\", url, err)\n\t}\n\tif err = getErrorFromResponseCode(resp.StatusCode); err != nil {\n\t\tresp.Body.Close()\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// walk recursively descends path, calling walkFn.\nfunc (fs *HTTPFs) walk(filePath string, info fs.FileInfo, walkFn filepath.WalkFunc) error {\n\tif !info.IsDir() {\n\t\treturn walkFn(filePath, info, nil)\n\t}\n\tlister, err := fs.ReadDir(filePath)\n\terr1 := walkFn(filePath, info, err)\n\tif err != nil || err1 != nil {\n\t\tif err == nil {\n\t\t\tlister.Close()\n\t\t}\n\t\treturn err1\n\t}\n\tdefer lister.Close()\n\n\tfor {\n\t\tfiles, err := lister.Next(ListerBatchSize)\n\t\tfinished := errors.Is(err, io.EOF)\n\t\tif err != nil && !finished {\n\t\t\treturn err\n\t\t}\n\t\tfor _, fi := range files {\n\t\t\tobjName := path.Join(filePath, fi.Name())\n\t\t\terr = fs.walk(objName, fi, walkFn)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif finished {\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc getErrorFromResponseCode(code int) error {\n\tswitch code {\n\tcase 401, 403:\n\t\treturn os.ErrPermission\n\tcase 404:\n\t\treturn os.ErrNotExist\n\tcase 501:\n\t\treturn ErrVfsUnsupported\n\tcase 200, 201:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected response code: %v\", code)\n\t}\n}\n\nfunc getInsecureTLSConfig() *tls.Config {\n\treturn &tls.Config{\n\t\tInsecureSkipVerify: true,\n\t}\n}\n\ntype wrapReader struct {\n\treader io.Reader\n}\n\nfunc (r *wrapReader) Read(p []byte) (n int, err error) {\n\treturn r.reader.Read(p)\n}\n\ntype statResponse struct {\n\tName         string    `json:\"name\"`\n\tSize         int64     `json:\"size\"`\n\tMode         uint32    `json:\"mode\"`\n\tLastModified time.Time `json:\"last_modified\"`\n}\n\nfunc (s *statResponse) getFileInfo() os.FileInfo {\n\tinfo := NewFileInfo(s.Name, false, s.Size, s.LastModified, false)\n\tinfo.SetMode(fs.FileMode(s.Mode))\n\treturn info\n}\n\ntype dirSizeResponse struct {\n\tFiles int   `json:\"files\"`\n\tSize  int64 `json:\"size\"`\n}\n\ntype mimeTypeResponse struct {\n\tMime string `json:\"mime\"`\n}\n\ntype statVFSResponse struct {\n\tID      uint32 `json:\"-\"`\n\tBsize   uint64 `json:\"bsize\"`\n\tFrsize  uint64 `json:\"frsize\"`\n\tBlocks  uint64 `json:\"blocks\"`\n\tBfree   uint64 `json:\"bfree\"`\n\tBavail  uint64 `json:\"bavail\"`\n\tFiles   uint64 `json:\"files\"`\n\tFfree   uint64 `json:\"ffree\"`\n\tFavail  uint64 `json:\"favail\"`\n\tFsid    uint64 `json:\"fsid\"`\n\tFlag    uint64 `json:\"flag\"`\n\tNamemax uint64 `json:\"namemax\"`\n}\n\nfunc (s *statVFSResponse) toSFTPStatVFS() *sftp.StatVFS {\n\treturn &sftp.StatVFS{\n\t\tBsize:   s.Bsize,\n\t\tFrsize:  s.Frsize,\n\t\tBlocks:  s.Blocks,\n\t\tBfree:   s.Bfree,\n\t\tBavail:  s.Bavail,\n\t\tFiles:   s.Files,\n\t\tFfree:   s.Ffree,\n\t\tFavail:  s.Ffree,\n\t\tFlag:    s.Flag,\n\t\tNamemax: s.Namemax,\n\t}\n}\n"
  },
  {
    "path": "internal/vfs/osfs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage vfs\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\tfscopy \"github.com/otiai10/copy\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n)\n\nconst (\n\t// osFsName is the name for the local Fs implementation\n\tosFsName = \"osfs\"\n)\n\ntype pathResolutionError struct {\n\terr string\n}\n\nfunc (e *pathResolutionError) Error() string {\n\treturn fmt.Sprintf(\"Path resolution error: %s\", e.err)\n}\n\n// OsFs is a Fs implementation that uses functions provided by the os package.\ntype OsFs struct {\n\tname         string\n\tconnectionID string\n\trootDir      string\n\t// if not empty this fs is mouted as virtual folder in the specified path\n\tmountPath       string\n\tlocalTempDir    string\n\treadBufferSize  int\n\twriteBufferSize int\n}\n\n// NewOsFs returns an OsFs object that allows to interact with local Os filesystem\nfunc NewOsFs(connectionID, rootDir, mountPath string, config *sdk.OSFsConfig) Fs {\n\tvar readBufferSize, writeBufferSize int\n\tif config != nil {\n\t\treadBufferSize = config.ReadBufferSize * 1024 * 1024\n\t\twriteBufferSize = config.WriteBufferSize * 1024 * 1024\n\t}\n\treturn &OsFs{\n\t\tname:            osFsName,\n\t\tconnectionID:    connectionID,\n\t\trootDir:         rootDir,\n\t\tmountPath:       getMountPath(mountPath),\n\t\tlocalTempDir:    getLocalTempDir(),\n\t\treadBufferSize:  readBufferSize,\n\t\twriteBufferSize: writeBufferSize,\n\t}\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *OsFs) Name() string {\n\treturn fs.name\n}\n\n// ConnectionID returns the SSH connection ID associated to this Fs implementation\nfunc (fs *OsFs) ConnectionID() string {\n\treturn fs.connectionID\n}\n\n// Stat returns a FileInfo describing the named file\nfunc (fs *OsFs) Stat(name string) (os.FileInfo, error) {\n\treturn os.Stat(name)\n}\n\n// Lstat returns a FileInfo describing the named file\nfunc (fs *OsFs) Lstat(name string) (os.FileInfo, error) {\n\treturn os.Lstat(name)\n}\n\n// Open opens the named file for reading\nfunc (fs *OsFs) Open(name string, offset int64) (File, PipeReader, func(), error) {\n\tf, err := os.Open(name)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tif offset > 0 {\n\t\t_, err = f.Seek(offset, io.SeekStart)\n\t\tif err != nil {\n\t\t\tf.Close()\n\t\t\treturn nil, nil, nil, err\n\t\t}\n\t}\n\tif fs.readBufferSize <= 0 {\n\t\treturn f, nil, nil, err\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeReader(r)\n\tgo func() {\n\t\tbr := bufio.NewReaderSize(f, fs.readBufferSize)\n\t\tn, err := doCopy(w, br, nil)\n\t\tw.CloseWithError(err) //nolint:errcheck\n\t\tf.Close()\n\t\tfsLog(fs, logger.LevelDebug, \"download completed, path: %q size: %v, err: %v\", name, n, err)\n\t}()\n\n\treturn nil, p, nil, nil\n}\n\n// Create creates or opens the named file for writing\nfunc (fs *OsFs) Create(name string, flag, _ int) (File, PipeWriter, func(), error) {\n\tif !fs.useWriteBuffering(flag) {\n\t\tvar err error\n\t\tvar f *os.File\n\t\tif flag == 0 {\n\t\t\tf, err = os.Create(name)\n\t\t} else {\n\t\t\tf, err = os.OpenFile(name, flag, 0666)\n\t\t}\n\t\treturn f, nil, nil, err\n\t}\n\tf, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeWriter(w)\n\n\tgo func() {\n\t\tbw := bufio.NewWriterSize(f, fs.writeBufferSize)\n\t\tn, err := doCopy(bw, r, nil)\n\t\terrFlush := bw.Flush()\n\t\tif err == nil && errFlush != nil {\n\t\t\terr = errFlush\n\t\t}\n\t\terrClose := f.Close()\n\t\tif err == nil && errClose != nil {\n\t\t\terr = errClose\n\t\t}\n\t\tr.CloseWithError(err) //nolint:errcheck\n\t\tp.Done(err)\n\t\tfsLog(fs, logger.LevelDebug, \"upload completed, path: %q, readed bytes: %v, err: %v\", name, n, err)\n\t}()\n\n\treturn nil, p, nil, nil\n}\n\n// Rename renames (moves) source to target\nfunc (fs *OsFs) Rename(source, target string, checks int) (int, int64, error) {\n\tif source == target {\n\t\treturn -1, -1, nil\n\t}\n\terr := os.Rename(source, target)\n\tif err != nil && isCrossDeviceError(err) {\n\t\tfsLog(fs, logger.LevelError, \"cross device error detected while renaming %q -> %q. Trying a copy and remove, this could take a long time\",\n\t\t\tsource, target)\n\t\tvar readBufferSize uint\n\t\tif fs.readBufferSize > 0 {\n\t\t\treadBufferSize = uint(fs.readBufferSize)\n\t\t}\n\n\t\terr = fscopy.Copy(source, target, fscopy.Options{\n\t\t\tOnSymlink: func(_ string) fscopy.SymlinkAction {\n\t\t\t\treturn fscopy.Skip\n\t\t\t},\n\t\t\tCopyBufferSize: readBufferSize,\n\t\t})\n\t\tif err != nil {\n\t\t\tfsLog(fs, logger.LevelError, \"cross device copy error: %v\", err)\n\t\t\treturn -1, -1, err\n\t\t}\n\t\tif checks&CheckUpdateModTime != 0 {\n\t\t\tfs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck\n\t\t}\n\t\terr = os.RemoveAll(source)\n\t\treturn -1, -1, err\n\t}\n\tif checks&CheckUpdateModTime != 0 && err == nil {\n\t\tfs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck\n\t}\n\treturn -1, -1, err\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (*OsFs) Remove(name string, _ bool) error {\n\treturn os.Remove(name)\n}\n\n// Mkdir creates a new directory with the specified name and default permissions\nfunc (*OsFs) Mkdir(name string) error {\n\treturn os.Mkdir(name, os.ModePerm)\n}\n\n// Symlink creates source as a symbolic link to target.\nfunc (*OsFs) Symlink(source, target string) error {\n\treturn os.Symlink(source, target)\n}\n\n// Readlink returns the destination of the named symbolic link\n// as absolute virtual path\nfunc (fs *OsFs) Readlink(name string) (string, error) {\n\t// we don't have to follow multiple links:\n\t// https://github.com/openssh/openssh-portable/blob/7bf2eb958fbb551e7d61e75c176bb3200383285d/sftp-server.c#L1329\n\tresolved, err := os.Readlink(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresolved = filepath.Clean(resolved)\n\tif !filepath.IsAbs(resolved) {\n\t\tresolved = filepath.Join(filepath.Dir(name), resolved)\n\t}\n\treturn fs.GetRelativePath(resolved), nil\n}\n\n// Chown changes the numeric uid and gid of the named file.\nfunc (*OsFs) Chown(name string, uid int, gid int) error {\n\treturn os.Chown(name, uid, gid)\n}\n\n// Chmod changes the mode of the named file to mode\nfunc (*OsFs) Chmod(name string, mode os.FileMode) error {\n\treturn os.Chmod(name, mode)\n}\n\n// Chtimes changes the access and modification times of the named file\nfunc (*OsFs) Chtimes(name string, atime, mtime time.Time, _ bool) error {\n\treturn os.Chtimes(name, atime, mtime)\n}\n\n// Truncate changes the size of the named file\nfunc (*OsFs) Truncate(name string, size int64) error {\n\treturn os.Truncate(name, size)\n}\n\n// ReadDir reads the directory named by dirname and returns\n// a list of directory entries.\nfunc (*OsFs) ReadDir(dirname string) (DirLister, error) {\n\tf, err := os.Open(dirname)\n\tif err != nil {\n\t\tif isInvalidNameError(err) {\n\t\t\terr = os.ErrNotExist\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &osFsDirLister{f}, nil\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported\nfunc (*OsFs) IsUploadResumeSupported() bool {\n\treturn true\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (*OsFs) IsConditionalUploadResumeSupported(_ int64) bool {\n\treturn true\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported\nfunc (*OsFs) IsAtomicUploadSupported() bool {\n\treturn true\n}\n\n// IsNotExist returns a boolean indicating whether the error is known to\n// report that a file or directory does not exist\nfunc (*OsFs) IsNotExist(err error) bool {\n\treturn errors.Is(err, fs.ErrNotExist)\n}\n\n// IsPermission returns a boolean indicating whether the error is known to\n// report that permission is denied.\nfunc (*OsFs) IsPermission(err error) bool {\n\tif _, ok := err.(*pathResolutionError); ok {\n\t\treturn true\n\t}\n\treturn errors.Is(err, fs.ErrPermission)\n}\n\n// IsNotSupported returns true if the error indicate an unsupported operation\nfunc (*OsFs) IsNotSupported(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn err == ErrVfsUnsupported\n}\n\n// CheckRootPath creates the root directory if it does not exists\nfunc (fs *OsFs) CheckRootPath(username string, uid int, gid int) bool {\n\tvar err error\n\tif _, err = fs.Stat(fs.rootDir); fs.IsNotExist(err) {\n\t\terr = os.MkdirAll(fs.rootDir, os.ModePerm)\n\t\tif err == nil {\n\t\t\tSetPathPermissions(fs, fs.rootDir, uid, gid)\n\t\t} else {\n\t\t\tfsLog(fs, logger.LevelError, \"error creating root directory %q for user %q: %v\", fs.rootDir, username, err)\n\t\t}\n\t}\n\treturn err == nil\n}\n\n// ScanRootDirContents returns the number of files contained in the root\n// directory and their size\nfunc (fs *OsFs) ScanRootDirContents() (int, int64, error) {\n\treturn fs.GetDirSize(fs.rootDir)\n}\n\n// CheckMetadata checks the metadata consistency\nfunc (*OsFs) CheckMetadata() error {\n\treturn nil\n}\n\n// GetAtomicUploadPath returns the path to use for an atomic upload\nfunc (*OsFs) GetAtomicUploadPath(name string) string {\n\tdir := filepath.Dir(name)\n\tif tempPath != \"\" {\n\t\tdir = tempPath\n\t}\n\tguid := xid.New().String()\n\treturn filepath.Join(dir, \".sftpgo-upload.\"+guid+\".\"+filepath.Base(name))\n}\n\n// GetRelativePath returns the path for a file relative to the user's home dir.\n// This is the path as seen by SFTPGo users\nfunc (fs *OsFs) GetRelativePath(name string) string {\n\tvirtualPath := \"/\"\n\tif fs.mountPath != \"\" {\n\t\tvirtualPath = fs.mountPath\n\t}\n\trel, err := filepath.Rel(fs.rootDir, filepath.Clean(name))\n\tif err != nil {\n\t\treturn virtualPath\n\t}\n\trel = filepath.ToSlash(rel)\n\tif rel == \"..\" || strings.HasPrefix(rel, \"../\") {\n\t\treturn virtualPath\n\t}\n\tif rel == \".\" {\n\t\trel = \"\"\n\t}\n\treturn path.Join(virtualPath, rel)\n}\n\n// Walk walks the file tree rooted at root, calling walkFn for each file or\n// directory in the tree, including root\nfunc (*OsFs) Walk(root string, walkFn filepath.WalkFunc) error {\n\treturn filepath.Walk(root, walkFn)\n}\n\n// Join joins any number of path elements into a single path\nfunc (*OsFs) Join(elem ...string) string {\n\treturn filepath.Join(elem...)\n}\n\n// ResolvePath returns the matching filesystem path for the specified sftp path\nfunc (fs *OsFs) ResolvePath(virtualPath string) (string, error) {\n\tif !filepath.IsAbs(fs.rootDir) {\n\t\treturn \"\", fmt.Errorf(\"invalid root path %q\", fs.rootDir)\n\t}\n\tif fs.mountPath != \"\" {\n\t\tif after, found := strings.CutPrefix(virtualPath, fs.mountPath); found {\n\t\t\tvirtualPath = after\n\t\t}\n\t}\n\tvirtualPath = path.Clean(\"/\" + virtualPath)\n\tr := filepath.Clean(filepath.Join(fs.rootDir, virtualPath))\n\tp, err := filepath.EvalSymlinks(r)\n\tif isInvalidNameError(err) {\n\t\terr = os.ErrNotExist\n\t}\n\tisNotExist := fs.IsNotExist(err)\n\tif err != nil && !isNotExist {\n\t\treturn \"\", err\n\t} else if isNotExist {\n\t\t// The requested path doesn't exist, so at this point we need to iterate up the\n\t\t// path chain until we hit a directory that _does_ exist and can be validated.\n\t\t_, err = fs.findFirstExistingDir(r)\n\t\tif err != nil {\n\t\t\tfsLog(fs, logger.LevelError, \"error resolving non-existent path %q\", err)\n\t\t}\n\t\treturn r, err\n\t}\n\n\terr = fs.isSubDir(p)\n\tif err != nil {\n\t\tfsLog(fs, logger.LevelError, \"Invalid path resolution, path %q original path %q resolved %q err: %v\",\n\t\t\tp, virtualPath, r, err)\n\t}\n\treturn r, err\n}\n\n// RealPath implements the FsRealPather interface\nfunc (fs *OsFs) RealPath(p string) (string, error) {\n\tlinksWalked := 0\n\tfor {\n\t\tinfo, err := os.Lstat(p)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\treturn fs.GetRelativePath(p), nil\n\t\t\t}\n\t\t\treturn \"\", err\n\t\t}\n\t\tif info.Mode()&os.ModeSymlink == 0 {\n\t\t\treturn fs.GetRelativePath(p), nil\n\t\t}\n\t\tresolvedLink, err := os.Readlink(p)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tresolvedLink = filepath.Clean(resolvedLink)\n\t\tif filepath.IsAbs(resolvedLink) {\n\t\t\tp = resolvedLink\n\t\t} else {\n\t\t\tp = filepath.Join(filepath.Dir(p), resolvedLink)\n\t\t}\n\t\tlinksWalked++\n\t\tif linksWalked > 10 {\n\t\t\tfsLog(fs, logger.LevelError, \"unable to get real path, too many links: %d\", linksWalked)\n\t\t\treturn \"\", &pathResolutionError{err: \"too many links\"}\n\t\t}\n\t}\n}\n\n// GetDirSize returns the number of files and the size for a folder\n// including any subfolders\nfunc (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {\n\tnumFiles := 0\n\tsize := int64(0)\n\tisDir, err := isDirectory(fs, dirname)\n\tif err == nil && isDir {\n\t\terr = filepath.Walk(dirname, func(_ string, info os.FileInfo, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif info != nil && info.Mode().IsRegular() {\n\t\t\t\tsize += info.Size()\n\t\t\t\tnumFiles++\n\t\t\t\tif numFiles%1000 == 0 {\n\t\t\t\t\tfsLog(fs, logger.LevelDebug, \"dirname %q scan in progress, files: %d, size: %d\", dirname, numFiles, size)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t}\n\treturn numFiles, size, err\n}\n\n// HasVirtualFolders returns true if folders are emulated\nfunc (*OsFs) HasVirtualFolders() bool {\n\treturn false\n}\n\nfunc (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) {\n\tresults := []string{}\n\tcleanPath := filepath.Clean(filePath)\n\tparent := filepath.Dir(cleanPath)\n\t_, err := os.Stat(parent)\n\n\tfor fs.IsNotExist(err) {\n\t\tresults = append(results, parent)\n\t\tparent = filepath.Dir(parent)\n\t\tif slices.Contains(results, parent) {\n\t\t\tbreak\n\t\t}\n\t\t_, err = os.Stat(parent)\n\t}\n\tif err != nil {\n\t\treturn results, err\n\t}\n\tp, err := filepath.EvalSymlinks(parent)\n\tif err != nil {\n\t\treturn results, err\n\t}\n\terr = fs.isSubDir(p)\n\tif err != nil {\n\t\tfsLog(fs, logger.LevelError, \"error finding non existing dir: %v\", err)\n\t}\n\treturn results, err\n}\n\nfunc (fs *OsFs) findFirstExistingDir(path string) (string, error) {\n\tresults, err := fs.findNonexistentDirs(path)\n\tif err != nil {\n\t\tfsLog(fs, logger.LevelError, \"unable to find non existent dirs: %v\", err)\n\t\treturn \"\", err\n\t}\n\tvar parent string\n\tif len(results) > 0 {\n\t\tlastMissingDir := results[len(results)-1]\n\t\tparent = filepath.Dir(lastMissingDir)\n\t} else {\n\t\tparent = fs.rootDir\n\t}\n\tp, err := filepath.EvalSymlinks(parent)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfileInfo, err := os.Stat(p)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !fileInfo.IsDir() {\n\t\treturn \"\", fmt.Errorf(\"resolved path is not a dir: %q\", p)\n\t}\n\terr = fs.isSubDir(p)\n\treturn p, err\n}\n\nfunc (fs *OsFs) isSubDir(sub string) error {\n\t// fs.rootDir must exist and it is already a validated absolute path\n\tparent, err := filepath.EvalSymlinks(fs.rootDir)\n\tif err != nil {\n\t\tfsLog(fs, logger.LevelError, \"invalid root path %q: %v\", fs.rootDir, err)\n\t\treturn err\n\t}\n\tif parent == sub {\n\t\treturn nil\n\t}\n\tif len(sub) < len(parent) {\n\t\terr = fmt.Errorf(\"path %q is not inside %q\", sub, parent)\n\t\treturn &pathResolutionError{err: err.Error()}\n\t}\n\tseparator := string(os.PathSeparator)\n\tif parent == filepath.Dir(parent) {\n\t\t// parent is the root dir, on Windows we can have C:\\, D:\\ and so on here\n\t\t// so we still need the prefix check\n\t\tseparator = \"\"\n\t}\n\tif !strings.HasPrefix(sub, parent+separator) {\n\t\terr = fmt.Errorf(\"path %q is not inside %q\", sub, parent)\n\t\treturn &pathResolutionError{err: err.Error()}\n\t}\n\treturn nil\n}\n\n// GetMimeType returns the content type\nfunc (fs *OsFs) GetMimeType(name string) (string, error) {\n\tf, err := os.OpenFile(name, os.O_RDONLY, 0)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer f.Close()\n\tvar buf [512]byte\n\tn, err := io.ReadFull(f, buf[:])\n\tif err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t\treturn \"\", err\n\t}\n\tctype := http.DetectContentType(buf[:n])\n\t// Rewind file.\n\t_, err = f.Seek(0, io.SeekStart)\n\treturn ctype, err\n}\n\n// Close closes the fs\nfunc (*OsFs) Close() error {\n\treturn nil\n}\n\n// GetAvailableDiskSize returns the available size for the specified path\nfunc (*OsFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {\n\treturn getStatFS(dirName)\n}\n\nfunc (fs *OsFs) useWriteBuffering(flag int) bool {\n\tif fs.writeBufferSize <= 0 {\n\t\treturn false\n\t}\n\tif flag == 0 {\n\t\treturn true\n\t}\n\tif flag&os.O_TRUNC == 0 {\n\t\tfsLog(fs, logger.LevelDebug, \"truncate flag missing, buffering write not possible\")\n\t\treturn false\n\t}\n\tif flag&os.O_RDWR != 0 {\n\t\tfsLog(fs, logger.LevelDebug, \"read and write flag found, buffering write not possible\")\n\t\treturn false\n\t}\n\treturn true\n}\n\ntype osFsDirLister struct {\n\tf *os.File\n}\n\nfunc (l *osFsDirLister) Next(limit int) ([]os.FileInfo, error) {\n\tif limit <= 0 {\n\t\treturn nil, errInvalidDirListerLimit\n\t}\n\treturn l.f.Readdir(limit)\n}\n\nfunc (l *osFsDirLister) Close() error {\n\treturn l.f.Close()\n}\n"
  },
  {
    "path": "internal/vfs/s3fs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !nos3\n\npackage vfs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawshttp \"github.com/aws/aws-sdk-go-v2/aws/transport/http\"\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials/stscreds\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\t\"github.com/pkg/sftp\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\t// using this mime type for directories improves compatibility with s3fs-fuse\n\ts3DirMimeType         = \"application/x-directory\"\n\ts3TransferBufferSize  = 256 * 1024\n\ts3CopyObjectThreshold = 500 * 1024 * 1024\n)\n\nvar (\n\ts3DirMimeTypes    = []string{s3DirMimeType, \"httpd/unix-directory\"}\n\ts3DefaultPageSize = int32(1000)\n)\n\n// S3Fs is a Fs implementation for AWS S3 compatible object storages\ntype S3Fs struct {\n\tconnectionID string\n\tlocalTempDir string\n\t// if not empty this fs is mouted as virtual folder in the specified path\n\tmountPath         string\n\tconfig            *S3FsConfig\n\tsvc               *s3.Client\n\tctxTimeout        time.Duration\n\tsseCustomerKey    string\n\tsseCustomerKeyMD5 string\n\tsseCustomerAlgo   string\n}\n\nfunc init() {\n\tversion.AddFeature(\"+s3\")\n}\n\n// NewS3Fs returns an S3Fs object that allows to interact with an s3 compatible\n// object storage\nfunc NewS3Fs(connectionID, localTempDir, mountPath string, s3Config S3FsConfig) (Fs, error) {\n\tif localTempDir == \"\" {\n\t\tlocalTempDir = getLocalTempDir()\n\t}\n\tfs := &S3Fs{\n\t\tconnectionID: connectionID,\n\t\tlocalTempDir: localTempDir,\n\t\tmountPath:    getMountPath(mountPath),\n\t\tconfig:       &s3Config,\n\t\tctxTimeout:   30 * time.Second,\n\t}\n\tif err := fs.config.validate(); err != nil {\n\t\treturn fs, err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tawsConfig, err := config.LoadDefaultConfig(ctx, config.WithHTTPClient(\n\t\tgetAWSHTTPClient(0, 30*time.Second, fs.config.SkipTLSVerify)),\n\t)\n\tif err != nil {\n\t\treturn fs, fmt.Errorf(\"unable to get AWS config: %w\", err)\n\t}\n\tif fs.config.Region != \"\" {\n\t\tawsConfig.Region = fs.config.Region\n\t}\n\tif !fs.config.AccessSecret.IsEmpty() {\n\t\tif err := fs.config.AccessSecret.TryDecrypt(); err != nil {\n\t\t\treturn fs, err\n\t\t}\n\t\tawsConfig.Credentials = aws.NewCredentialsCache(\n\t\t\tcredentials.NewStaticCredentialsProvider(\n\t\t\t\tfs.config.AccessKey,\n\t\t\t\tfs.config.AccessSecret.GetPayload(),\n\t\t\t\tfs.config.SessionToken),\n\t\t)\n\t}\n\tif !fs.config.SSECustomerKey.IsEmpty() {\n\t\tif err := fs.config.SSECustomerKey.TryDecrypt(); err != nil {\n\t\t\treturn fs, err\n\t\t}\n\t\tkey := fs.config.SSECustomerKey.GetPayload()\n\t\tif len(key) == 32 {\n\t\t\tmd5sumBinary := md5.Sum([]byte(key))\n\t\t\tfs.sseCustomerKey = base64.StdEncoding.EncodeToString([]byte(key))\n\t\t\tfs.sseCustomerKeyMD5 = base64.StdEncoding.EncodeToString(md5sumBinary[:])\n\t\t} else {\n\t\t\tkeyHash := sha256.Sum256([]byte(key))\n\t\t\tmd5sumBinary := md5.Sum(keyHash[:])\n\t\t\tfs.sseCustomerKey = base64.StdEncoding.EncodeToString(keyHash[:])\n\t\t\tfs.sseCustomerKeyMD5 = base64.StdEncoding.EncodeToString(md5sumBinary[:])\n\t\t}\n\t\tfs.sseCustomerAlgo = \"AES256\"\n\t}\n\n\tfs.setConfigDefaults()\n\n\tif fs.config.RoleARN != \"\" {\n\t\tclient := sts.NewFromConfig(awsConfig)\n\t\tcreds := stscreds.NewAssumeRoleProvider(client, fs.config.RoleARN)\n\t\tawsConfig.Credentials = creds\n\t}\n\tfs.svc = s3.NewFromConfig(awsConfig, func(o *s3.Options) {\n\t\to.AppID = version.GetVersionHash()\n\t\to.UsePathStyle = fs.config.ForcePathStyle\n\t\to.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired\n\t\to.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequired\n\t\tif fs.config.Endpoint != \"\" {\n\t\t\to.BaseEndpoint = aws.String(fs.config.Endpoint)\n\t\t}\n\t})\n\treturn fs, nil\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *S3Fs) Name() string {\n\treturn fmt.Sprintf(\"%s bucket %q\", s3fsName, fs.config.Bucket)\n}\n\n// ConnectionID returns the connection ID associated to this Fs implementation\nfunc (fs *S3Fs) ConnectionID() string {\n\treturn fs.connectionID\n}\n\n// Stat returns a FileInfo describing the named file\nfunc (fs *S3Fs) Stat(name string) (os.FileInfo, error) {\n\tvar result *FileInfo\n\tif name == \"\" || name == \"/\" || name == \".\" {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t}\n\tif fs.config.KeyPrefix == name+\"/\" {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t}\n\tobj, err := fs.headObject(name)\n\tif err == nil {\n\t\t// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.\n\t\t// So we check some common content types to detect if this is a \"directory\".\n\t\tisDir := slices.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))\n\t\tif util.GetIntFromPointer(obj.ContentLength) == 0 && !isDir {\n\t\t\t_, err = fs.headObject(name + \"/\")\n\t\t\tisDir = err == nil\n\t\t}\n\t\tinfo := NewFileInfo(name, isDir, util.GetIntFromPointer(obj.ContentLength), util.GetTimeFromPointer(obj.LastModified), false)\n\t\treturn info, nil\n\t}\n\tif !fs.IsNotExist(err) {\n\t\treturn result, err\n\t}\n\t// now check if this is a prefix (virtual directory)\n\thasContents, err := fs.hasContents(name)\n\tif err == nil && hasContents {\n\t\treturn NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\t// the requested file may still be a directory as a zero bytes key\n\t// with a trailing forward slash (created using mkdir).\n\t// S3 doesn't return content type when listing objects, so we have\n\t// create \"dirs\" adding a trailing \"/\" to the key\n\treturn fs.getStatForDir(name)\n}\n\nfunc (fs *S3Fs) getStatForDir(name string) (os.FileInfo, error) {\n\tvar result *FileInfo\n\tobj, err := fs.headObject(name + \"/\")\n\tif err != nil {\n\t\treturn result, err\n\t}\n\treturn NewFileInfo(name, true, util.GetIntFromPointer(obj.ContentLength), util.GetTimeFromPointer(obj.LastModified), false), nil\n}\n\n// Lstat returns a FileInfo describing the named file\nfunc (fs *S3Fs) Lstat(name string) (os.FileInfo, error) {\n\treturn fs.Stat(name)\n}\n\n// Open opens the named file for reading\nfunc (fs *S3Fs) Open(name string, offset int64) (File, PipeReader, func(), error) {\n\tattrs, err := fs.headObject(name)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, fs.config.DownloadPartSize*int64(fs.config.DownloadConcurrency)+1)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeReader(r)\n\tif readMetadata > 0 {\n\t\tp.setMetadata(attrs.Metadata)\n\t}\n\tctx, cancelFn := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\tdefer cancelFn()\n\n\t\terr := fs.handleDownload(ctx, name, offset, w, attrs)\n\t\tw.CloseWithError(err) //nolint:errcheck\n\t\tfsLog(fs, logger.LevelDebug, \"download completed, path: %q size: %d, err: %+v\", name, w.GetWrittenBytes(), err)\n\t\tmetric.S3TransferCompleted(w.GetWrittenBytes(), 1, err)\n\t}()\n\n\treturn nil, p, cancelFn, nil\n}\n\n// Create creates or opens the named file for writing\nfunc (fs *S3Fs) Create(name string, flag, checks int) (File, PipeWriter, func(), error) {\n\tif checks&CheckParentDir != 0 {\n\t\t_, err := fs.Stat(path.Dir(name))\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, err\n\t\t}\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, fs.config.UploadPartSize+1024*1024)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tvar p PipeWriter\n\tif checks&CheckResume != 0 {\n\t\tp = newPipeWriterAtOffset(w, 0)\n\t} else {\n\t\tp = NewPipeWriter(w)\n\t}\n\tctx, cancelFn := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\tdefer cancelFn()\n\n\t\tvar contentType string\n\t\tif flag == -1 {\n\t\t\tcontentType = s3DirMimeType\n\t\t} else {\n\t\t\tcontentType = mime.TypeByExtension(path.Ext(name))\n\t\t}\n\t\terr := fs.handleUpload(ctx, r, name, contentType)\n\t\tr.CloseWithError(err) //nolint:errcheck\n\t\tp.Done(err)\n\t\tfsLog(fs, logger.LevelDebug, \"upload completed, path: %q, acl: %q, readed bytes: %d, err: %+v\",\n\t\t\tname, fs.config.ACL, r.GetReadedBytes(), err)\n\t\tmetric.S3TransferCompleted(r.GetReadedBytes(), 0, err)\n\t}()\n\n\tif checks&CheckResume != 0 {\n\t\treadCh := make(chan error, 1)\n\n\t\tgo func() {\n\t\t\tn, err := fs.downloadToWriter(name, p)\n\t\t\tpw := p.(*pipeWriterAtOffset)\n\t\t\tpw.offset = 0\n\t\t\tpw.writeOffset = n\n\t\t\treadCh <- err\n\t\t}()\n\n\t\terr = <-readCh\n\t\tif err != nil {\n\t\t\tcancelFn()\n\t\t\tp.Close()\n\t\t\tfsLog(fs, logger.LevelDebug, \"download before resume failed, writer closed and read cancelled\")\n\t\t\treturn nil, nil, nil, err\n\t\t}\n\t}\n\n\tif uploadMode&4 != 0 {\n\t\treturn nil, p, nil, nil\n\t}\n\treturn nil, p, cancelFn, nil\n}\n\n// Rename renames (moves) source to target.\nfunc (fs *S3Fs) Rename(source, target string, checks int) (int, int64, error) {\n\tif source == target {\n\t\treturn -1, -1, nil\n\t}\n\tif checks&CheckParentDir != 0 {\n\t\t_, err := fs.Stat(path.Dir(target))\n\t\tif err != nil {\n\t\t\treturn -1, -1, err\n\t\t}\n\t}\n\tfi, err := fs.Stat(source)\n\tif err != nil {\n\t\treturn -1, -1, err\n\t}\n\treturn fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (fs *S3Fs) Remove(name string, isDir bool) error {\n\tif isDir {\n\t\thasContents, err := fs.hasContents(name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif hasContents {\n\t\t\treturn fmt.Errorf(\"cannot remove non empty directory: %q\", name)\n\t\t}\n\t\tif !strings.HasSuffix(name, \"/\") {\n\t\t\tname += \"/\"\n\t\t}\n\t}\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\t_, err := fs.svc.DeleteObject(ctx, &s3.DeleteObjectInput{\n\t\tBucket: aws.String(fs.config.Bucket),\n\t\tKey:    aws.String(name),\n\t})\n\tmetric.S3DeleteObjectCompleted(err)\n\treturn err\n}\n\n// Mkdir creates a new directory with the specified name and default permissions\nfunc (fs *S3Fs) Mkdir(name string) error {\n\t_, err := fs.Stat(name)\n\tif !fs.IsNotExist(err) {\n\t\treturn err\n\t}\n\treturn fs.mkdirInternal(name)\n}\n\n// Symlink creates source as a symbolic link to target.\nfunc (*S3Fs) Symlink(_, _ string) error {\n\treturn ErrVfsUnsupported\n}\n\n// Readlink returns the destination of the named symbolic link\nfunc (*S3Fs) Readlink(_ string) (string, error) {\n\treturn \"\", ErrVfsUnsupported\n}\n\n// Chown changes the numeric uid and gid of the named file.\nfunc (*S3Fs) Chown(_ string, _ int, _ int) error {\n\treturn ErrVfsUnsupported\n}\n\n// Chmod changes the mode of the named file to mode.\nfunc (*S3Fs) Chmod(_ string, _ os.FileMode) error {\n\treturn ErrVfsUnsupported\n}\n\n// Chtimes changes the access and modification times of the named file.\nfunc (fs *S3Fs) Chtimes(_ string, _, _ time.Time, _ bool) error {\n\treturn ErrVfsUnsupported\n}\n\n// Truncate changes the size of the named file.\n// Truncate by path is not supported, while truncating an opened\n// file is handled inside base transfer\nfunc (*S3Fs) Truncate(_ string, _ int64) error {\n\treturn ErrVfsUnsupported\n}\n\n// ReadDir reads the directory named by dirname and returns\n// a list of directory entries.\nfunc (fs *S3Fs) ReadDir(dirname string) (DirLister, error) {\n\t// dirname must be already cleaned\n\tprefix := fs.getPrefix(dirname)\n\tpaginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{\n\t\tBucket:    aws.String(fs.config.Bucket),\n\t\tPrefix:    aws.String(prefix),\n\t\tDelimiter: aws.String(\"/\"),\n\t\tMaxKeys:   &s3DefaultPageSize,\n\t})\n\n\treturn &s3DirLister{\n\t\tpaginator: paginator,\n\t\ttimeout:   fs.ctxTimeout,\n\t\tprefix:    prefix,\n\t\tprefixes:  make(map[string]bool),\n\t}, nil\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported.\n// Resuming uploads is not supported on S3\nfunc (*S3Fs) IsUploadResumeSupported() bool {\n\treturn false\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (*S3Fs) IsConditionalUploadResumeSupported(size int64) bool {\n\treturn size <= resumeMaxSize\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported.\n// S3 uploads are already atomic, we don't need to upload to a temporary\n// file\nfunc (*S3Fs) IsAtomicUploadSupported() bool {\n\treturn false\n}\n\n// IsNotExist returns a boolean indicating whether the error is known to\n// report that a file or directory does not exist\nfunc (*S3Fs) IsNotExist(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\tvar re *awshttp.ResponseError\n\tif errors.As(err, &re) {\n\t\tif re.Response != nil {\n\t\t\treturn re.Response.StatusCode == http.StatusNotFound\n\t\t}\n\t}\n\treturn false\n}\n\n// IsPermission returns a boolean indicating whether the error is known to\n// report that permission is denied.\nfunc (*S3Fs) IsPermission(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\tvar re *awshttp.ResponseError\n\tif errors.As(err, &re) {\n\t\tif re.Response != nil {\n\t\t\treturn re.Response.StatusCode == http.StatusForbidden ||\n\t\t\t\tre.Response.StatusCode == http.StatusUnauthorized\n\t\t}\n\t}\n\treturn false\n}\n\n// IsNotSupported returns true if the error indicate an unsupported operation\nfunc (*S3Fs) IsNotSupported(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn errors.Is(err, ErrVfsUnsupported)\n}\n\n// CheckRootPath creates the specified local root directory if it does not exists\nfunc (fs *S3Fs) CheckRootPath(username string, uid int, gid int) bool {\n\t// we need a local directory for temporary files\n\tosFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, \"\", nil)\n\treturn osFs.CheckRootPath(username, uid, gid)\n}\n\n// ScanRootDirContents returns the number of files contained in the bucket,\n// and their size\nfunc (fs *S3Fs) ScanRootDirContents() (int, int64, error) {\n\treturn fs.GetDirSize(fs.config.KeyPrefix)\n}\n\n// GetDirSize returns the number of files and the size for a folder\n// including any subfolders\nfunc (fs *S3Fs) GetDirSize(dirname string) (int, int64, error) {\n\tprefix := fs.getPrefix(dirname)\n\tnumFiles := 0\n\tsize := int64(0)\n\n\tpaginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{\n\t\tBucket:  aws.String(fs.config.Bucket),\n\t\tPrefix:  aws.String(prefix),\n\t\tMaxKeys: &s3DefaultPageSize,\n\t})\n\n\tfor paginator.HasMorePages() {\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\tpage, err := paginator.NextPage(ctx)\n\t\tif err != nil {\n\t\t\tmetric.S3ListObjectsCompleted(err)\n\t\t\treturn numFiles, size, err\n\t\t}\n\t\tfor _, fileObject := range page.Contents {\n\t\t\tisDir := strings.HasSuffix(util.GetStringFromPointer(fileObject.Key), \"/\")\n\t\t\tobjectSize := util.GetIntFromPointer(fileObject.Size)\n\t\t\tif isDir && objectSize == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnumFiles++\n\t\t\tsize += objectSize\n\t\t}\n\t\tfsLog(fs, logger.LevelDebug, \"scan in progress for %q, files: %d, size: %d\", dirname, numFiles, size)\n\t}\n\n\tmetric.S3ListObjectsCompleted(nil)\n\treturn numFiles, size, nil\n}\n\n// GetAtomicUploadPath returns the path to use for an atomic upload.\n// S3 uploads are already atomic, we never call this method for S3\nfunc (*S3Fs) GetAtomicUploadPath(_ string) string {\n\treturn \"\"\n}\n\n// GetRelativePath returns the path for a file relative to the user's home dir.\n// This is the path as seen by SFTPGo users\nfunc (fs *S3Fs) GetRelativePath(name string) string {\n\trel := path.Clean(name)\n\tif rel == \".\" {\n\t\trel = \"\"\n\t}\n\tif !path.IsAbs(rel) {\n\t\trel = \"/\" + rel\n\t}\n\tif fs.config.KeyPrefix != \"\" {\n\t\tif !strings.HasPrefix(rel, \"/\"+fs.config.KeyPrefix) {\n\t\t\trel = \"/\"\n\t\t}\n\t\trel = path.Clean(\"/\" + strings.TrimPrefix(rel, \"/\"+fs.config.KeyPrefix))\n\t}\n\tif fs.mountPath != \"\" {\n\t\trel = path.Join(fs.mountPath, rel)\n\t}\n\treturn rel\n}\n\n// Walk walks the file tree rooted at root, calling walkFn for each file or\n// directory in the tree, including root. The result are unordered\nfunc (fs *S3Fs) Walk(root string, walkFn filepath.WalkFunc) error {\n\tprefix := fs.getPrefix(root)\n\n\tpaginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{\n\t\tBucket:  aws.String(fs.config.Bucket),\n\t\tPrefix:  aws.String(prefix),\n\t\tMaxKeys: &s3DefaultPageSize,\n\t})\n\n\tfor paginator.HasMorePages() {\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\tpage, err := paginator.NextPage(ctx)\n\t\tif err != nil {\n\t\t\tmetric.S3ListObjectsCompleted(err)\n\t\t\twalkFn(root, NewFileInfo(root, true, 0, time.Unix(0, 0), false), err) //nolint:errcheck\n\t\t\treturn err\n\t\t}\n\t\tfor _, fileObject := range page.Contents {\n\t\t\tname, isDir := fs.resolve(fileObject.Key, prefix)\n\t\t\tif name == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr := walkFn(util.GetStringFromPointer(fileObject.Key),\n\t\t\t\tNewFileInfo(name, isDir, util.GetIntFromPointer(fileObject.Size),\n\t\t\t\t\tutil.GetTimeFromPointer(fileObject.LastModified), false), nil)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tmetric.S3ListObjectsCompleted(nil)\n\twalkFn(root, NewFileInfo(root, true, 0, time.Unix(0, 0), false), nil) //nolint:errcheck\n\treturn nil\n}\n\n// Join joins any number of path elements into a single path\nfunc (*S3Fs) Join(elem ...string) string {\n\treturn strings.TrimPrefix(path.Join(elem...), \"/\")\n}\n\n// HasVirtualFolders returns true if folders are emulated\nfunc (*S3Fs) HasVirtualFolders() bool {\n\treturn true\n}\n\n// ResolvePath returns the matching filesystem path for the specified virtual path\nfunc (fs *S3Fs) ResolvePath(virtualPath string) (string, error) {\n\tif fs.mountPath != \"\" {\n\t\tif after, found := strings.CutPrefix(virtualPath, fs.mountPath); found {\n\t\t\tvirtualPath = after\n\t\t}\n\t}\n\tvirtualPath = path.Clean(\"/\" + virtualPath)\n\treturn fs.Join(fs.config.KeyPrefix, strings.TrimPrefix(virtualPath, \"/\")), nil\n}\n\n// CopyFile implements the FsFileCopier interface\nfunc (fs *S3Fs) CopyFile(source, target string, srcInfo os.FileInfo) (int, int64, error) {\n\tnumFiles := 1\n\tsizeDiff := srcInfo.Size()\n\tattrs, err := fs.headObject(target)\n\tif err == nil {\n\t\tsizeDiff -= util.GetIntFromPointer(attrs.ContentLength)\n\t\tnumFiles = 0\n\t} else {\n\t\tif !fs.IsNotExist(err) {\n\t\t\treturn 0, 0, err\n\t\t}\n\t}\n\tif err := fs.copyFileInternal(source, target, srcInfo); err != nil {\n\t\treturn 0, 0, err\n\t}\n\treturn numFiles, sizeDiff, nil\n}\n\nfunc (fs *S3Fs) resolve(name *string, prefix string) (string, bool) {\n\tresult := strings.TrimPrefix(util.GetStringFromPointer(name), prefix)\n\tisDir := strings.HasSuffix(result, \"/\")\n\tif isDir {\n\t\tresult = strings.TrimSuffix(result, \"/\")\n\t}\n\treturn result, isDir\n}\n\nfunc (fs *S3Fs) setConfigDefaults() {\n\tconst defaultPartSize = 1024 * 1024 * 5\n\tconst defaultConcurrency = 5\n\n\tif fs.config.UploadPartSize == 0 {\n\t\tfs.config.UploadPartSize = defaultPartSize\n\t} else {\n\t\tif fs.config.UploadPartSize < 1024*1024 {\n\t\t\tfs.config.UploadPartSize *= 1024 * 1024\n\t\t}\n\t}\n\tif fs.config.UploadConcurrency == 0 {\n\t\tfs.config.UploadConcurrency = defaultConcurrency\n\t}\n\tif fs.config.DownloadPartSize == 0 {\n\t\tfs.config.DownloadPartSize = defaultPartSize\n\t} else {\n\t\tif fs.config.DownloadPartSize < 1024*1024 {\n\t\t\tfs.config.DownloadPartSize *= 1024 * 1024\n\t\t}\n\t}\n\tif fs.config.DownloadConcurrency == 0 {\n\t\tfs.config.DownloadConcurrency = defaultConcurrency\n\t}\n}\n\nfunc (fs *S3Fs) copyFileInternal(source, target string, srcInfo os.FileInfo) error {\n\tcontentType := mime.TypeByExtension(path.Ext(source))\n\tcopySource := pathEscape(fs.Join(fs.config.Bucket, source))\n\n\tif srcInfo.Size() > s3CopyObjectThreshold {\n\t\tfsLog(fs, logger.LevelDebug, \"renaming file %q with size %d using multipart copy\",\n\t\t\tsource, srcInfo.Size())\n\t\terr := fs.doMultipartCopy(copySource, target, contentType, srcInfo.Size())\n\t\tmetric.S3CopyObjectCompleted(err)\n\t\treturn err\n\t}\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tcopyObject := &s3.CopyObjectInput{\n\t\tBucket:                         aws.String(fs.config.Bucket),\n\t\tCopySource:                     aws.String(copySource),\n\t\tKey:                            aws.String(target),\n\t\tStorageClass:                   types.StorageClass(fs.config.StorageClass),\n\t\tACL:                            types.ObjectCannedACL(fs.config.ACL),\n\t\tContentType:                    util.NilIfEmpty(contentType),\n\t\tCopySourceSSECustomerKey:       util.NilIfEmpty(fs.sseCustomerKey),\n\t\tCopySourceSSECustomerAlgorithm: util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\tCopySourceSSECustomerKeyMD5:    util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t\tSSECustomerKey:                 util.NilIfEmpty(fs.sseCustomerKey),\n\t\tSSECustomerAlgorithm:           util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\tSSECustomerKeyMD5:              util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t}\n\n\t_, err := fs.svc.CopyObject(ctx, copyObject)\n\n\tmetric.S3CopyObjectCompleted(err)\n\treturn err\n}\n\nfunc (fs *S3Fs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,\n\tupdateModTime bool,\n) (int, int64, error) {\n\tvar numFiles int\n\tvar filesSize int64\n\n\tif srcInfo.IsDir() {\n\t\tif renameMode == 0 {\n\t\t\thasContents, err := fs.hasContents(source)\n\t\t\tif err != nil {\n\t\t\t\treturn numFiles, filesSize, err\n\t\t\t}\n\t\t\tif hasContents {\n\t\t\t\treturn numFiles, filesSize, fmt.Errorf(\"%w: cannot rename non empty directory: %q\", ErrVfsUnsupported, source)\n\t\t\t}\n\t\t}\n\t\tif err := fs.mkdirInternal(target); err != nil {\n\t\t\treturn numFiles, filesSize, err\n\t\t}\n\t\tif renameMode == 1 {\n\t\t\tfiles, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)\n\t\t\tnumFiles += files\n\t\t\tfilesSize += size\n\t\t\tif err != nil {\n\t\t\t\treturn numFiles, filesSize, err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif err := fs.copyFileInternal(source, target, srcInfo); err != nil {\n\t\t\treturn numFiles, filesSize, err\n\t\t}\n\t\tnumFiles++\n\t\tfilesSize += srcInfo.Size()\n\t}\n\terr := fs.Remove(source, srcInfo.IsDir())\n\tif fs.IsNotExist(err) {\n\t\terr = nil\n\t}\n\treturn numFiles, filesSize, err\n}\n\nfunc (fs *S3Fs) mkdirInternal(name string) error {\n\tif !strings.HasSuffix(name, \"/\") {\n\t\tname += \"/\"\n\t}\n\t_, w, _, err := fs.Create(name, -1, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn w.Close()\n}\n\nfunc (fs *S3Fs) hasContents(name string) (bool, error) {\n\tprefix := fs.getPrefix(name)\n\tmaxKeys := int32(2)\n\tpaginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{\n\t\tBucket:  aws.String(fs.config.Bucket),\n\t\tPrefix:  aws.String(prefix),\n\t\tMaxKeys: &maxKeys,\n\t})\n\n\tif paginator.HasMorePages() {\n\t\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\t\tdefer cancelFn()\n\n\t\tpage, err := paginator.NextPage(ctx)\n\t\tmetric.S3ListObjectsCompleted(err)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tfor _, obj := range page.Contents {\n\t\t\tname, _ := fs.resolve(obj.Key, prefix)\n\t\t\tif name == \"\" || name == \"/\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn true, nil\n\t\t}\n\t\treturn false, nil\n\t}\n\n\tmetric.S3ListObjectsCompleted(nil)\n\treturn false, nil\n}\n\nfunc (fs *S3Fs) downloadPart(ctx context.Context, name string, buf []byte, w io.WriterAt, start, count, writeOffset int64) error {\n\tif count == 0 {\n\t\treturn nil\n\t}\n\trangeHeader := fmt.Sprintf(\"bytes=%d-%d\", start, start+count-1)\n\n\tresp, err := fs.svc.GetObject(ctx, &s3.GetObjectInput{\n\t\tBucket:               aws.String(fs.config.Bucket),\n\t\tKey:                  aws.String(name),\n\t\tRange:                &rangeHeader,\n\t\tSSECustomerKey:       util.NilIfEmpty(fs.sseCustomerKey),\n\t\tSSECustomerAlgorithm: util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\tSSECustomerKeyMD5:    util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\t_, err = io.ReadAtLeast(resp.Body, buf, int(count))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writeAtFull(w, buf, writeOffset, int(count))\n}\n\nfunc (fs *S3Fs) handleDownload(ctx context.Context, name string, offset int64, writer io.WriterAt, attrs *s3.HeadObjectOutput) error {\n\tcontentLength := util.GetIntFromPointer(attrs.ContentLength)\n\tsizeToDownload := contentLength - offset\n\tif sizeToDownload < 0 {\n\t\tfsLog(fs, logger.LevelError, \"invalid multipart download size or offset, size: %d, offset: %d, size to download: %d\",\n\t\t\tcontentLength, offset, sizeToDownload)\n\t\treturn errors.New(\"the requested offset exceeds the file size\")\n\t}\n\tif sizeToDownload == 0 {\n\t\tfsLog(fs, logger.LevelDebug, \"nothing to download, offset %d, content length %d\", offset, contentLength)\n\t\treturn nil\n\t}\n\tpartSize := fs.config.DownloadPartSize\n\tguard := make(chan struct{}, fs.config.DownloadConcurrency)\n\tvar blockCtxTimeout time.Duration\n\tif fs.config.DownloadPartMaxTime > 0 {\n\t\tblockCtxTimeout = time.Duration(fs.config.DownloadPartMaxTime) * time.Second\n\t} else {\n\t\tblockCtxTimeout = time.Duration(fs.config.DownloadPartSize/(1024*1024)) * time.Minute\n\t}\n\tpool := newBufferAllocator(int(partSize))\n\tdefer pool.free()\n\n\tfinished := false\n\tvar wg sync.WaitGroup\n\tvar errOnce sync.Once\n\tvar hasError atomic.Bool\n\tvar poolError error\n\n\tpoolCtx, poolCancel := context.WithCancel(ctx)\n\tdefer poolCancel()\n\n\tfor part := 0; !finished; part++ {\n\t\tstart := offset\n\t\tend := offset + partSize\n\t\tif end >= contentLength {\n\t\t\tend = contentLength\n\t\t\tfinished = true\n\t\t}\n\t\twriteOffset := int64(part) * partSize\n\t\toffset = end\n\n\t\tguard <- struct{}{}\n\t\tif hasError.Load() {\n\t\t\tfsLog(fs, logger.LevelDebug, \"pool error, download for part %d not started\", part)\n\t\t\tbreak\n\t\t}\n\n\t\tbuf := pool.getBuffer()\n\t\twg.Add(1)\n\t\tgo func(start, end, writeOffset int64, buf []byte) {\n\t\t\tdefer func() {\n\t\t\t\tpool.releaseBuffer(buf)\n\t\t\t\t<-guard\n\t\t\t\twg.Done()\n\t\t\t}()\n\n\t\t\tinnerCtx, cancelFn := context.WithDeadline(poolCtx, time.Now().Add(blockCtxTimeout))\n\t\t\tdefer cancelFn()\n\n\t\t\terr := fs.downloadPart(innerCtx, name, buf, writer, start, end-start, writeOffset)\n\t\t\tif err != nil {\n\t\t\t\terrOnce.Do(func() {\n\t\t\t\t\tfsLog(fs, logger.LevelError, \"multipart download error: %+v\", err)\n\t\t\t\t\thasError.Store(true)\n\t\t\t\t\tpoolError = fmt.Errorf(\"multipart download error: %w\", err)\n\t\t\t\t\tpoolCancel()\n\t\t\t\t})\n\t\t\t}\n\t\t}(start, end, writeOffset, buf)\n\t}\n\n\twg.Wait()\n\tclose(guard)\n\n\treturn poolError\n}\n\nfunc (fs *S3Fs) initiateMultipartUpload(ctx context.Context, name, contentType string) (string, error) {\n\tctx, cancelFn := context.WithDeadline(ctx, time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tres, err := fs.svc.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{\n\t\tBucket:               aws.String(fs.config.Bucket),\n\t\tKey:                  aws.String(name),\n\t\tStorageClass:         types.StorageClass(fs.config.StorageClass),\n\t\tACL:                  types.ObjectCannedACL(fs.config.ACL),\n\t\tContentType:          util.NilIfEmpty(contentType),\n\t\tSSECustomerKey:       util.NilIfEmpty(fs.sseCustomerKey),\n\t\tSSECustomerAlgorithm: util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\tSSECustomerKeyMD5:    util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create multipart upload request: %w\", err)\n\t}\n\tuploadID := util.GetStringFromPointer(res.UploadId)\n\tif uploadID == \"\" {\n\t\treturn \"\", errors.New(\"unable to get multipart upload ID\")\n\t}\n\treturn uploadID, nil\n}\n\nfunc (fs *S3Fs) uploadPart(ctx context.Context, name, uploadID string, partNumber int32, data []byte) (*string, error) {\n\ttimeout := time.Duration(fs.config.UploadPartSize/(1024*1024)) * time.Minute\n\tif fs.config.UploadPartMaxTime > 0 {\n\t\ttimeout = time.Duration(fs.config.UploadPartMaxTime) * time.Second\n\t}\n\tctx, cancelFn := context.WithDeadline(ctx, time.Now().Add(timeout))\n\tdefer cancelFn()\n\n\tresp, err := fs.svc.UploadPart(ctx, &s3.UploadPartInput{\n\t\tBucket:               aws.String(fs.config.Bucket),\n\t\tKey:                  aws.String(name),\n\t\tPartNumber:           &partNumber,\n\t\tUploadId:             aws.String(uploadID),\n\t\tBody:                 bytes.NewReader(data),\n\t\tSSECustomerKey:       util.NilIfEmpty(fs.sseCustomerKey),\n\t\tSSECustomerAlgorithm: util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\tSSECustomerKeyMD5:    util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to upload part number %d: %w\", partNumber, err)\n\t}\n\treturn resp.ETag, nil\n}\n\nfunc (fs *S3Fs) completeMultipartUpload(ctx context.Context, name, uploadID string, completedParts []types.CompletedPart) error {\n\tctx, cancelFn := context.WithDeadline(ctx, time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\t_, err := fs.svc.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{\n\t\tBucket:   aws.String(fs.config.Bucket),\n\t\tKey:      aws.String(name),\n\t\tUploadId: aws.String(uploadID),\n\t\tMultipartUpload: &types.CompletedMultipartUpload{\n\t\t\tParts: completedParts,\n\t\t},\n\t})\n\treturn err\n}\n\nfunc (fs *S3Fs) abortMultipartUpload(name, uploadID string) error {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\t_, err := fs.svc.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{\n\t\tBucket:   aws.String(fs.config.Bucket),\n\t\tKey:      aws.String(name),\n\t\tUploadId: aws.String(uploadID),\n\t})\n\treturn err\n}\n\nfunc (fs *S3Fs) singlePartUpload(ctx context.Context, name, contentType string, data []byte) error {\n\ttimeout := time.Duration(fs.config.UploadPartSize/(1024*1024)) * time.Minute\n\tif fs.config.UploadPartMaxTime > 0 {\n\t\ttimeout = time.Duration(fs.config.UploadPartMaxTime) * time.Second\n\t}\n\tctx, cancelFn := context.WithDeadline(ctx, time.Now().Add(timeout))\n\tdefer cancelFn()\n\n\tcontentLength := int64(len(data))\n\t_, err := fs.svc.PutObject(ctx, &s3.PutObjectInput{\n\t\tBucket:               aws.String(fs.config.Bucket),\n\t\tKey:                  aws.String(name),\n\t\tACL:                  types.ObjectCannedACL(fs.config.ACL),\n\t\tBody:                 bytes.NewReader(data),\n\t\tContentType:          util.NilIfEmpty(contentType),\n\t\tContentLength:        &contentLength,\n\t\tSSECustomerKey:       util.NilIfEmpty(fs.sseCustomerKey),\n\t\tSSECustomerAlgorithm: util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\tSSECustomerKeyMD5:    util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t\tStorageClass:         types.StorageClass(fs.config.StorageClass),\n\t})\n\treturn err\n}\n\nfunc (fs *S3Fs) handleUpload(ctx context.Context, reader io.Reader, name, contentType string) error {\n\tpool := newBufferAllocator(int(fs.config.UploadPartSize))\n\tdefer pool.free()\n\n\tfirstBuf := pool.getBuffer()\n\tfirstReadSize, err := readFill(reader, firstBuf)\n\tif err == io.EOF {\n\t\treturn fs.singlePartUpload(ctx, name, contentType, firstBuf[:firstReadSize])\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadID, err := fs.initiateMultipartUpload(ctx, name, contentType)\n\tif err != nil {\n\t\treturn err\n\t}\n\tguard := make(chan struct{}, fs.config.UploadConcurrency)\n\tfinished := false\n\tvar partMutex sync.Mutex\n\tvar completedParts []types.CompletedPart\n\tvar wg sync.WaitGroup\n\tvar hasError atomic.Bool\n\tvar poolErr error\n\tvar errOnce sync.Once\n\tvar partNumber int32\n\n\tpoolCtx, poolCancel := context.WithCancel(ctx)\n\tdefer poolCancel()\n\n\tfinalizeFailedUpload := func(err error) {\n\t\tfsLog(fs, logger.LevelError, \"finalize failed multipart upload after error: %v\", err)\n\t\thasError.Store(true)\n\t\tpoolErr = err\n\t\tpoolCancel()\n\t\tif abortErr := fs.abortMultipartUpload(name, uploadID); abortErr != nil {\n\t\t\tfsLog(fs, logger.LevelError, \"unable to abort multipart upload: %+v\", abortErr)\n\t\t}\n\t}\n\n\tuploadPart := func(partNum int32, buf []byte, bytesRead int) {\n\t\tdefer func() {\n\t\t\tpool.releaseBuffer(buf)\n\t\t\t<-guard\n\t\t\twg.Done()\n\t\t}()\n\n\t\tetag, err := fs.uploadPart(poolCtx, name, uploadID, partNum, buf[:bytesRead])\n\t\tif err != nil {\n\t\t\terrOnce.Do(func() {\n\t\t\t\tfinalizeFailedUpload(err)\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t\tpartMutex.Lock()\n\t\tcompletedParts = append(completedParts, types.CompletedPart{\n\t\t\tPartNumber: &partNum,\n\t\t\tETag:       etag,\n\t\t})\n\t\tpartMutex.Unlock()\n\t}\n\n\tpartNumber = 1\n\tguard <- struct{}{}\n\n\twg.Add(1)\n\tgo uploadPart(partNumber, firstBuf, firstReadSize)\n\n\tfor partNumber = 2; !finished; partNumber++ {\n\t\tbuf := pool.getBuffer()\n\n\t\tn, err := readFill(reader, buf)\n\t\tif err == io.EOF {\n\t\t\tif n == 0 {\n\t\t\t\tpool.releaseBuffer(buf)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfinished = true\n\t\t} else if err != nil {\n\t\t\tpool.releaseBuffer(buf)\n\t\t\terrOnce.Do(func() {\n\t\t\t\tfinalizeFailedUpload(err)\n\t\t\t})\n\t\t\tbreak\n\t\t}\n\t\tguard <- struct{}{}\n\t\tif hasError.Load() {\n\t\t\tfsLog(fs, logger.LevelError, \"pool error, upload for part %d not started\", partNumber)\n\t\t\tpool.releaseBuffer(buf)\n\t\t\tbreak\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo uploadPart(partNumber, buf, n)\n\t}\n\n\twg.Wait()\n\tclose(guard)\n\n\tif poolErr != nil {\n\t\treturn poolErr\n\t}\n\n\tsort.Slice(completedParts, func(i, j int) bool {\n\t\tgetPartNumber := func(number *int32) int32 {\n\t\t\tif number == nil {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\treturn *number\n\t\t}\n\n\t\treturn getPartNumber(completedParts[i].PartNumber) < getPartNumber(completedParts[j].PartNumber)\n\t})\n\n\treturn fs.completeMultipartUpload(ctx, name, uploadID, completedParts)\n}\n\nfunc (fs *S3Fs) doMultipartCopy(source, target, contentType string, fileSize int64) error {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tres, err := fs.svc.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{\n\t\tBucket:               aws.String(fs.config.Bucket),\n\t\tKey:                  aws.String(target),\n\t\tStorageClass:         types.StorageClass(fs.config.StorageClass),\n\t\tACL:                  types.ObjectCannedACL(fs.config.ACL),\n\t\tContentType:          util.NilIfEmpty(contentType),\n\t\tSSECustomerKey:       util.NilIfEmpty(fs.sseCustomerKey),\n\t\tSSECustomerAlgorithm: util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\tSSECustomerKeyMD5:    util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create multipart copy request: %w\", err)\n\t}\n\tuploadID := util.GetStringFromPointer(res.UploadId)\n\tif uploadID == \"\" {\n\t\treturn errors.New(\"unable to get multipart copy upload ID\")\n\t}\n\t// We use 32 MB part size and copy 10 parts in parallel.\n\t// These values are arbitrary. We don't want to start too many goroutines\n\tmaxPartSize := int64(32 * 1024 * 1024)\n\tif fileSize > int64(100*1024*1024*1024) {\n\t\tmaxPartSize = int64(500 * 1024 * 1024)\n\t}\n\tguard := make(chan struct{}, 10)\n\tfinished := false\n\tvar completedParts []types.CompletedPart\n\tvar partMutex sync.Mutex\n\tvar wg sync.WaitGroup\n\tvar hasError atomic.Bool\n\tvar errOnce sync.Once\n\tvar copyError error\n\tvar partNumber int32\n\tvar offset int64\n\n\topCtx, opCancel := context.WithCancel(context.Background())\n\tdefer opCancel()\n\n\tfor partNumber = 1; !finished; partNumber++ {\n\t\tstart := offset\n\t\tend := offset + maxPartSize\n\t\tif end >= fileSize {\n\t\t\tend = fileSize\n\t\t\tfinished = true\n\t\t}\n\t\toffset = end\n\n\t\tguard <- struct{}{}\n\t\tif hasError.Load() {\n\t\t\tfsLog(fs, logger.LevelDebug, \"previous multipart copy error, copy for part %d not started\", partNumber)\n\t\t\tbreak\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo func(partNum int32, partStart, partEnd int64) {\n\t\t\tdefer func() {\n\t\t\t\t<-guard\n\t\t\t\twg.Done()\n\t\t\t}()\n\n\t\t\tinnerCtx, innerCancelFn := context.WithDeadline(opCtx, time.Now().Add(fs.ctxTimeout))\n\t\t\tdefer innerCancelFn()\n\n\t\t\tpartResp, err := fs.svc.UploadPartCopy(innerCtx, &s3.UploadPartCopyInput{\n\t\t\t\tBucket:                         aws.String(fs.config.Bucket),\n\t\t\t\tCopySource:                     aws.String(source),\n\t\t\t\tKey:                            aws.String(target),\n\t\t\t\tPartNumber:                     &partNum,\n\t\t\t\tUploadId:                       aws.String(uploadID),\n\t\t\t\tCopySourceRange:                aws.String(fmt.Sprintf(\"bytes=%d-%d\", partStart, partEnd-1)),\n\t\t\t\tCopySourceSSECustomerKey:       util.NilIfEmpty(fs.sseCustomerKey),\n\t\t\t\tCopySourceSSECustomerAlgorithm: util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\t\t\tCopySourceSSECustomerKeyMD5:    util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t\t\t\tSSECustomerKey:                 util.NilIfEmpty(fs.sseCustomerKey),\n\t\t\t\tSSECustomerAlgorithm:           util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\t\t\tSSECustomerKeyMD5:              util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\terrOnce.Do(func() {\n\t\t\t\t\tfsLog(fs, logger.LevelError, \"unable to copy part number %d: %+v\", partNum, err)\n\t\t\t\t\thasError.Store(true)\n\t\t\t\t\tcopyError = fmt.Errorf(\"error copying part number %d: %w\", partNum, err)\n\t\t\t\t\topCancel()\n\n\t\t\t\t\tif errAbort := fs.abortMultipartUpload(target, uploadID); errAbort != nil {\n\t\t\t\t\t\tfsLog(fs, logger.LevelError, \"unable to abort multipart copy: %+v\", errAbort)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpartMutex.Lock()\n\t\t\tcompletedParts = append(completedParts, types.CompletedPart{\n\t\t\t\tETag:       partResp.CopyPartResult.ETag,\n\t\t\t\tPartNumber: &partNum,\n\t\t\t})\n\t\t\tpartMutex.Unlock()\n\t\t}(partNumber, start, end)\n\t}\n\n\twg.Wait()\n\tclose(guard)\n\n\tif copyError != nil {\n\t\treturn copyError\n\t}\n\tsort.Slice(completedParts, func(i, j int) bool {\n\t\tgetPartNumber := func(number *int32) int32 {\n\t\t\tif number == nil {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\treturn *number\n\t\t}\n\n\t\treturn getPartNumber(completedParts[i].PartNumber) < getPartNumber(completedParts[j].PartNumber)\n\t})\n\n\tcompleteCtx, completeCancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer completeCancelFn()\n\n\t_, err = fs.svc.CompleteMultipartUpload(completeCtx, &s3.CompleteMultipartUploadInput{\n\t\tBucket:   aws.String(fs.config.Bucket),\n\t\tKey:      aws.String(target),\n\t\tUploadId: aws.String(uploadID),\n\t\tMultipartUpload: &types.CompletedMultipartUpload{\n\t\t\tParts: completedParts,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to complete multipart upload: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (fs *S3Fs) getPrefix(name string) string {\n\tprefix := \"\"\n\tif name != \"\" && name != \".\" && name != \"/\" {\n\t\tprefix = strings.TrimPrefix(name, \"/\")\n\t\tif !strings.HasSuffix(prefix, \"/\") {\n\t\t\tprefix += \"/\"\n\t\t}\n\t}\n\treturn prefix\n}\n\nfunc (fs *S3Fs) headObject(name string) (*s3.HeadObjectOutput, error) {\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))\n\tdefer cancelFn()\n\n\tobj, err := fs.svc.HeadObject(ctx, &s3.HeadObjectInput{\n\t\tBucket:               aws.String(fs.config.Bucket),\n\t\tKey:                  aws.String(name),\n\t\tSSECustomerKey:       util.NilIfEmpty(fs.sseCustomerKey),\n\t\tSSECustomerAlgorithm: util.NilIfEmpty(fs.sseCustomerAlgo),\n\t\tSSECustomerKeyMD5:    util.NilIfEmpty(fs.sseCustomerKeyMD5),\n\t})\n\tmetric.S3HeadObjectCompleted(err)\n\treturn obj, err\n}\n\n// GetMimeType returns the content type\nfunc (fs *S3Fs) GetMimeType(name string) (string, error) {\n\tobj, err := fs.headObject(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn util.GetStringFromPointer(obj.ContentType), nil\n}\n\n// Close closes the fs\nfunc (*S3Fs) Close() error {\n\treturn nil\n}\n\n// GetAvailableDiskSize returns the available size for the specified path\nfunc (*S3Fs) GetAvailableDiskSize(_ string) (*sftp.StatVFS, error) {\n\treturn nil, ErrStorageSizeUnavailable\n}\n\nfunc (fs *S3Fs) downloadToWriter(name string, w PipeWriter) (int64, error) {\n\tfsLog(fs, logger.LevelDebug, \"starting download before resuming upload, path %q\", name)\n\tattrs, err := fs.headObject(name)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tctx, cancelFn := context.WithTimeout(context.Background(), preResumeTimeout)\n\tdefer cancelFn()\n\n\terr = fs.handleDownload(ctx, name, 0, w, attrs)\n\tfsLog(fs, logger.LevelDebug, \"download before resuming upload completed, path %q size: %d, err: %+v\",\n\t\tname, w.GetWrittenBytes(), err)\n\tmetric.S3TransferCompleted(w.GetWrittenBytes(), 1, err)\n\treturn w.GetWrittenBytes(), err\n}\n\ntype s3DirLister struct {\n\tbaseDirLister\n\tpaginator     *s3.ListObjectsV2Paginator\n\ttimeout       time.Duration\n\tprefix        string\n\tprefixes      map[string]bool\n\tmetricUpdated bool\n}\n\nfunc (l *s3DirLister) resolve(name *string) (string, bool) {\n\tresult := strings.TrimPrefix(util.GetStringFromPointer(name), l.prefix)\n\tisDir := strings.HasSuffix(result, \"/\")\n\tif isDir {\n\t\tresult = strings.TrimSuffix(result, \"/\")\n\t}\n\treturn result, isDir\n}\n\nfunc (l *s3DirLister) Next(limit int) ([]os.FileInfo, error) {\n\tif limit <= 0 {\n\t\treturn nil, errInvalidDirListerLimit\n\t}\n\tif len(l.cache) >= limit {\n\t\treturn l.returnFromCache(limit), nil\n\t}\n\tif !l.paginator.HasMorePages() {\n\t\tif !l.metricUpdated {\n\t\t\tl.metricUpdated = true\n\t\t\tmetric.S3ListObjectsCompleted(nil)\n\t\t}\n\t\treturn l.returnFromCache(limit), io.EOF\n\t}\n\tctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(l.timeout))\n\tdefer cancelFn()\n\n\tpage, err := l.paginator.NextPage(ctx)\n\tif err != nil {\n\t\tmetric.S3ListObjectsCompleted(err)\n\t\treturn l.cache, err\n\t}\n\tfor _, p := range page.CommonPrefixes {\n\t\t// prefixes have a trailing slash\n\t\tname, _ := l.resolve(p.Prefix)\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := l.prefixes[name]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tl.cache = append(l.cache, NewFileInfo(name, true, 0, time.Unix(0, 0), false))\n\t\tl.prefixes[name] = true\n\t}\n\tfor _, fileObject := range page.Contents {\n\t\tobjectModTime := util.GetTimeFromPointer(fileObject.LastModified)\n\t\tobjectSize := util.GetIntFromPointer(fileObject.Size)\n\t\tname, isDir := l.resolve(fileObject.Key)\n\t\tif name == \"\" || name == \"/\" {\n\t\t\tcontinue\n\t\t}\n\t\tif isDir {\n\t\t\tif _, ok := l.prefixes[name]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tl.prefixes[name] = true\n\t\t}\n\n\t\tl.cache = append(l.cache, NewFileInfo(name, (isDir && objectSize == 0), objectSize, objectModTime, false))\n\t}\n\treturn l.returnFromCache(limit), nil\n}\n\nfunc (l *s3DirLister) Close() error {\n\treturn l.baseDirLister.Close()\n}\n\nfunc getAWSHTTPClient(timeout int, idleConnectionTimeout time.Duration, skipTLSVerify bool) *awshttp.BuildableClient {\n\tc := awshttp.NewBuildableClient().\n\t\tWithDialerOptions(func(d *net.Dialer) {\n\t\t\td.Timeout = 8 * time.Second\n\t\t}).\n\t\tWithTransportOptions(func(tr *http.Transport) {\n\t\t\ttr.IdleConnTimeout = idleConnectionTimeout\n\t\t\ttr.WriteBufferSize = s3TransferBufferSize\n\t\t\ttr.ReadBufferSize = s3TransferBufferSize\n\t\t\tif skipTLSVerify {\n\t\t\t\tif tr.TLSClientConfig != nil {\n\t\t\t\t\ttr.TLSClientConfig.InsecureSkipVerify = skipTLSVerify\n\t\t\t\t} else {\n\t\t\t\t\ttr.TLSClientConfig = &tls.Config{\n\t\t\t\t\t\tMinVersion:         awshttp.DefaultHTTPTransportTLSMinVersion,\n\t\t\t\t\t\tInsecureSkipVerify: skipTLSVerify,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\tif timeout > 0 {\n\t\tc = c.WithTimeout(time.Duration(timeout) * time.Second)\n\t}\n\treturn c\n}\n\n// ideally we should simply use url.PathEscape:\n//\n// https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/go/example_code/s3/s3_copy_object.go#L65\n//\n// but this cause issue with some vendors, see #483, the code below is copied from rclone\nfunc pathEscape(in string) string {\n\tvar u url.URL\n\tu.Path = in\n\treturn strings.ReplaceAll(u.String(), \"+\", \"%2B\")\n}\n"
  },
  {
    "path": "internal/vfs/s3fs_disabled.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build nos3\n\npackage vfs\n\nimport (\n\t\"errors\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nfunc init() {\n\tversion.AddFeature(\"-s3\")\n}\n\n// NewS3Fs returns an error, S3 is disabled\nfunc NewS3Fs(_, _, _ string, _ S3FsConfig) (Fs, error) {\n\treturn nil, errors.New(\"S3 disabled at build time\")\n}\n"
  },
  {
    "path": "internal/vfs/sftpfs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage vfs\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/pkg/sftp\"\n\t\"github.com/robfig/cron/v3\"\n\t\"github.com/rs/xid\"\n\t\"github.com/sftpgo/sdk\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\nconst (\n\t// sftpFsName is the name for the SFTP Fs implementation\n\tsftpFsName               = \"sftpfs\"\n\tlogSenderSFTPCache       = \"sftpCache\"\n\tmaxSessionsPerConnection = 5\n)\n\nvar (\n\t// ErrSFTPLoop defines the error to return if an SFTP loop is detected\n\tErrSFTPLoop    = errors.New(\"SFTP loop or nested local SFTP folders detected\")\n\tsftpConnsCache = newSFTPConnectionCache()\n)\n\n// SFTPFsConfig defines the configuration for SFTP based filesystem\ntype SFTPFsConfig struct {\n\tsdk.BaseSFTPFsConfig\n\tPassword               *kms.Secret `json:\"password,omitempty\"`\n\tPrivateKey             *kms.Secret `json:\"private_key,omitempty\"`\n\tKeyPassphrase          *kms.Secret `json:\"key_passphrase,omitempty\"`\n\tforbiddenSelfUsernames []string    `json:\"-\"`\n}\n\nfunc (c *SFTPFsConfig) getKeySigner() (ssh.Signer, error) {\n\tprivPayload := c.PrivateKey.GetPayload()\n\tif privPayload == \"\" {\n\t\treturn nil, nil\n\t}\n\tif key := c.KeyPassphrase.GetPayload(); key != \"\" {\n\t\treturn ssh.ParsePrivateKeyWithPassphrase([]byte(privPayload), []byte(key))\n\t}\n\treturn ssh.ParsePrivateKey([]byte(privPayload))\n}\n\n// HideConfidentialData hides confidential data\nfunc (c *SFTPFsConfig) HideConfidentialData() {\n\tif c.Password != nil {\n\t\tc.Password.Hide()\n\t}\n\tif c.PrivateKey != nil {\n\t\tc.PrivateKey.Hide()\n\t}\n\tif c.KeyPassphrase != nil {\n\t\tc.KeyPassphrase.Hide()\n\t}\n}\n\nfunc (c *SFTPFsConfig) setNilSecretsIfEmpty() {\n\tif c.Password != nil && c.Password.IsEmpty() {\n\t\tc.Password = nil\n\t}\n\tif c.PrivateKey != nil && c.PrivateKey.IsEmpty() {\n\t\tc.PrivateKey = nil\n\t}\n\tif c.KeyPassphrase != nil && c.KeyPassphrase.IsEmpty() {\n\t\tc.KeyPassphrase = nil\n\t}\n}\n\nfunc (c *SFTPFsConfig) isEqual(other SFTPFsConfig) bool {\n\tif c.Endpoint != other.Endpoint {\n\t\treturn false\n\t}\n\tif c.Username != other.Username {\n\t\treturn false\n\t}\n\tif c.Prefix != other.Prefix {\n\t\treturn false\n\t}\n\tif c.DisableCouncurrentReads != other.DisableCouncurrentReads {\n\t\treturn false\n\t}\n\tif c.BufferSize != other.BufferSize {\n\t\treturn false\n\t}\n\tif len(c.Fingerprints) != len(other.Fingerprints) {\n\t\treturn false\n\t}\n\tfor _, fp := range c.Fingerprints {\n\t\tif !slices.Contains(other.Fingerprints, fp) {\n\t\t\treturn false\n\t\t}\n\t}\n\tc.setEmptyCredentialsIfNil()\n\tother.setEmptyCredentialsIfNil()\n\tif !c.Password.IsEqual(other.Password) {\n\t\treturn false\n\t}\n\tif !c.KeyPassphrase.IsEqual(other.KeyPassphrase) {\n\t\treturn false\n\t}\n\treturn c.PrivateKey.IsEqual(other.PrivateKey)\n}\n\nfunc (c *SFTPFsConfig) setEmptyCredentialsIfNil() {\n\tif c.Password == nil {\n\t\tc.Password = kms.NewEmptySecret()\n\t}\n\tif c.PrivateKey == nil {\n\t\tc.PrivateKey = kms.NewEmptySecret()\n\t}\n\tif c.KeyPassphrase == nil {\n\t\tc.KeyPassphrase = kms.NewEmptySecret()\n\t}\n}\n\nfunc (c *SFTPFsConfig) isSameResource(other SFTPFsConfig) bool {\n\tif c.EqualityCheckMode > 0 || other.EqualityCheckMode > 0 {\n\t\tif c.Username != other.Username {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn c.Endpoint == other.Endpoint\n}\n\n// validate returns an error if the configuration is not valid\nfunc (c *SFTPFsConfig) validate() error {\n\tc.setEmptyCredentialsIfNil()\n\tif c.Endpoint == \"\" {\n\t\treturn util.NewI18nError(errors.New(\"endpoint cannot be empty\"), util.I18nErrorEndpointRequired)\n\t}\n\tif !strings.Contains(c.Endpoint, \":\") {\n\t\tc.Endpoint += \":22\"\n\t}\n\t_, _, err := net.SplitHostPort(c.Endpoint)\n\tif err != nil {\n\t\treturn util.NewI18nError(fmt.Errorf(\"invalid endpoint: %v\", err), util.I18nErrorEndpointInvalid)\n\t}\n\tif c.Username == \"\" {\n\t\treturn util.NewI18nError(errors.New(\"username cannot be empty\"), util.I18nErrorFsUsernameRequired)\n\t}\n\tif c.BufferSize < 0 || c.BufferSize > 16 {\n\t\treturn errors.New(\"invalid buffer_size, valid range is 0-16\")\n\t}\n\tif !isEqualityCheckModeValid(c.EqualityCheckMode) {\n\t\treturn errors.New(\"invalid equality_check_mode\")\n\t}\n\tif err := c.validateCredentials(); err != nil {\n\t\treturn err\n\t}\n\tif c.Prefix != \"\" {\n\t\tc.Prefix = util.CleanPath(c.Prefix)\n\t} else {\n\t\tc.Prefix = \"/\"\n\t}\n\treturn c.validatePrivateKey()\n}\n\nfunc (c *SFTPFsConfig) validatePrivateKey() error {\n\tif c.PrivateKey.IsPlain() {\n\t\tsigner, err := c.getKeySigner()\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(fmt.Errorf(\"invalid private key: %w\", err), util.I18nErrorPrivKeyInvalid)\n\t\t}\n\t\tif signer != nil {\n\t\t\tif key, ok := signer.PublicKey().(ssh.CryptoPublicKey); ok {\n\t\t\t\tcryptoKey := key.CryptoPublicKey()\n\t\t\t\tif rsaKey, ok := cryptoKey.(*rsa.PublicKey); ok {\n\t\t\t\t\tif size := rsaKey.N.BitLen(); size < 2048 {\n\t\t\t\t\t\treturn util.NewI18nError(\n\t\t\t\t\t\t\tfmt.Errorf(\"rsa key with size %d not accepted, minimum 2048\", size),\n\t\t\t\t\t\t\tutil.I18nErrorKeySizeInvalid,\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\treturn nil\n}\n\nfunc (c *SFTPFsConfig) validateCredentials() error {\n\tif c.Password.IsEmpty() && c.PrivateKey.IsEmpty() {\n\t\treturn util.NewI18nError(errors.New(\"credentials cannot be empty\"), util.I18nErrorFsCredentialsRequired)\n\t}\n\tif c.Password.IsEncrypted() && !c.Password.IsValid() {\n\t\treturn errors.New(\"invalid encrypted password\")\n\t}\n\tif !c.Password.IsEmpty() && !c.Password.IsValidInput() {\n\t\treturn errors.New(\"invalid password\")\n\t}\n\tif c.PrivateKey.IsEncrypted() && !c.PrivateKey.IsValid() {\n\t\treturn errors.New(\"invalid encrypted private key\")\n\t}\n\tif !c.PrivateKey.IsEmpty() && !c.PrivateKey.IsValidInput() {\n\t\treturn errors.New(\"invalid private key\")\n\t}\n\tif c.KeyPassphrase.IsEncrypted() && !c.KeyPassphrase.IsValid() {\n\t\treturn errors.New(\"invalid encrypted private key passphrase\")\n\t}\n\tif !c.KeyPassphrase.IsEmpty() && !c.KeyPassphrase.IsValidInput() {\n\t\treturn errors.New(\"invalid private key passphrase\")\n\t}\n\treturn nil\n}\n\n// ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text\nfunc (c *SFTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {\n\tif err := c.validate(); err != nil {\n\t\tvar errI18n *util.I18nError\n\t\terrValidation := util.NewValidationError(fmt.Sprintf(\"could not validate SFTP fs config: %v\", err))\n\t\tif errors.As(err, &errI18n) {\n\t\t\treturn util.NewI18nError(errValidation, errI18n.Message)\n\t\t}\n\t\treturn util.NewI18nError(errValidation, util.I18nErrorFsValidation)\n\t}\n\tif c.Password.IsPlain() {\n\t\tc.Password.SetAdditionalData(additionalData)\n\t\tif err := c.Password.Encrypt(); err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt SFTP fs password: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\tif c.PrivateKey.IsPlain() {\n\t\tc.PrivateKey.SetAdditionalData(additionalData)\n\t\tif err := c.PrivateKey.Encrypt(); err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt SFTP fs private key: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\tif c.KeyPassphrase.IsPlain() {\n\t\tc.KeyPassphrase.SetAdditionalData(additionalData)\n\t\tif err := c.KeyPassphrase.Encrypt(); err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt SFTP fs private key passphrase: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\n// getUniqueID returns an hash of the settings used to connect to the SFTP server\nfunc (c *SFTPFsConfig) getUniqueID(partition int) string {\n\th := sha256.New()\n\tvar b bytes.Buffer\n\n\tb.WriteString(c.Endpoint)\n\tb.WriteString(c.Username)\n\tb.WriteString(strings.Join(c.Fingerprints, \"\"))\n\tb.WriteString(strconv.FormatBool(c.DisableCouncurrentReads))\n\tb.WriteString(strconv.FormatInt(c.BufferSize, 10))\n\tb.WriteString(c.Password.GetPayload())\n\tb.WriteString(c.PrivateKey.GetPayload())\n\tb.WriteString(c.KeyPassphrase.GetPayload())\n\tif allowSelfConnections != 0 {\n\t\tb.WriteString(strings.Join(c.forbiddenSelfUsernames, \"\"))\n\t}\n\tb.WriteString(strconv.Itoa(partition))\n\n\th.Write(b.Bytes())\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\n// SFTPFs is a Fs implementation for SFTP backends\ntype SFTPFs struct {\n\tconnectionID string\n\t// if not empty this fs is mouted as virtual folder in the specified path\n\tmountPath    string\n\tlocalTempDir string\n\tconfig       *SFTPFsConfig\n\tconn         *sftpConnection\n}\n\n// NewSFTPFs returns an SFTPFs object that allows to interact with an SFTP server\nfunc NewSFTPFs(connectionID, mountPath, localTempDir string, forbiddenSelfUsernames []string, config SFTPFsConfig) (Fs, error) {\n\tif localTempDir == \"\" {\n\t\tlocalTempDir = getLocalTempDir()\n\t}\n\tif err := config.validate(); err != nil {\n\t\treturn nil, err\n\t}\n\tif !config.Password.IsEmpty() {\n\t\tif err := config.Password.TryDecrypt(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif !config.PrivateKey.IsEmpty() {\n\t\tif err := config.PrivateKey.TryDecrypt(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif !config.KeyPassphrase.IsEmpty() {\n\t\tif err := config.KeyPassphrase.TryDecrypt(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tconn, err := sftpConnsCache.Get(&config, connectionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconfig.forbiddenSelfUsernames = forbiddenSelfUsernames\n\tsftpFs := &SFTPFs{\n\t\tconnectionID: connectionID,\n\t\tmountPath:    getMountPath(mountPath),\n\t\tlocalTempDir: localTempDir,\n\t\tconfig:       &config,\n\t\tconn:         conn,\n\t}\n\terr = sftpFs.createConnection()\n\tif err != nil {\n\t\tsftpFs.Close() //nolint:errcheck\n\t}\n\treturn sftpFs, err\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *SFTPFs) Name() string {\n\treturn fmt.Sprintf(`%s %q@%q`, sftpFsName, fs.config.Username, fs.config.Endpoint)\n}\n\n// ConnectionID returns the connection ID associated to this Fs implementation\nfunc (fs *SFTPFs) ConnectionID() string {\n\treturn fs.connectionID\n}\n\n// Stat returns a FileInfo describing the named file\nfunc (fs *SFTPFs) Stat(name string) (os.FileInfo, error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn client.Stat(name)\n}\n\n// Lstat returns a FileInfo describing the named file\nfunc (fs *SFTPFs) Lstat(name string) (os.FileInfo, error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn client.Lstat(name)\n}\n\n// Open opens the named file for reading\nfunc (fs *SFTPFs) Open(name string, offset int64) (File, PipeReader, func(), error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tf, err := client.Open(name)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tif offset > 0 {\n\t\t_, err = f.Seek(offset, io.SeekStart)\n\t\tif err != nil {\n\t\t\tf.Close()\n\t\t\treturn nil, nil, nil, err\n\t\t}\n\t}\n\tif fs.config.BufferSize == 0 {\n\t\treturn f, nil, nil, nil\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeReader(r)\n\n\tgo func() {\n\t\t// if we enable buffering the client stalls\n\t\t//br := bufio.NewReaderSize(f, int(fs.config.BufferSize)*1024*1024)\n\t\t//n, err := fs.copy(w, br)\n\t\tn, err := io.Copy(w, f)\n\t\tw.CloseWithError(err) //nolint:errcheck\n\t\tf.Close()\n\t\tfsLog(fs, logger.LevelDebug, \"download completed, path: %q size: %v, err: %v\", name, n, err)\n\t}()\n\n\treturn nil, p, nil, nil\n}\n\n// Create creates or opens the named file for writing\nfunc (fs *SFTPFs) Create(name string, flag, _ int) (File, PipeWriter, func(), error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tif fs.config.BufferSize == 0 {\n\t\tvar f File\n\t\tif flag == 0 {\n\t\t\tf, err = client.Create(name)\n\t\t} else {\n\t\t\tf, err = client.OpenFile(name, flag)\n\t\t}\n\t\treturn f, nil, nil, err\n\t}\n\t// buffering is enabled\n\tf, err := client.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tr, w, err := createPipeFn(fs.localTempDir, 0)\n\tif err != nil {\n\t\tf.Close()\n\t\treturn nil, nil, nil, err\n\t}\n\tp := NewPipeWriter(w)\n\n\tgo func() {\n\t\tbw := bufio.NewWriterSize(f, int(fs.config.BufferSize)*1024*1024)\n\t\t// we don't use io.Copy since bufio.Writer implements io.WriterTo and\n\t\t// so it calls the sftp.File WriteTo method without buffering\n\t\tn, err := doCopy(bw, r, nil)\n\t\terrFlush := bw.Flush()\n\t\tif err == nil && errFlush != nil {\n\t\t\terr = errFlush\n\t\t}\n\t\tvar errTruncate error\n\t\tif err != nil {\n\t\t\terrTruncate = f.Truncate(n)\n\t\t}\n\t\terrClose := f.Close()\n\t\tif err == nil && errClose != nil {\n\t\t\terr = errClose\n\t\t}\n\t\tr.CloseWithError(err) //nolint:errcheck\n\t\tp.Done(err)\n\t\tfsLog(fs, logger.LevelDebug, \"upload completed, path: %q, readed bytes: %v, err: %v err truncate: %v\",\n\t\t\tname, n, err, errTruncate)\n\t}()\n\n\treturn nil, p, nil, nil\n}\n\n// Rename renames (moves) source to target.\nfunc (fs *SFTPFs) Rename(source, target string, checks int) (int, int64, error) {\n\tif source == target {\n\t\treturn -1, -1, nil\n\t}\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn -1, -1, err\n\t}\n\tif _, ok := client.HasExtension(\"posix-rename@openssh.com\"); ok {\n\t\terr := client.PosixRename(source, target)\n\t\tif checks&CheckUpdateModTime != 0 && err == nil {\n\t\t\tfs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck\n\t\t}\n\t\treturn -1, -1, err\n\t}\n\terr = client.Rename(source, target)\n\tif checks&CheckUpdateModTime != 0 && err == nil {\n\t\tfs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck\n\t}\n\treturn -1, -1, err\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (fs *SFTPFs) Remove(name string, isDir bool) error {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif isDir {\n\t\treturn client.RemoveDirectory(name)\n\t}\n\treturn client.Remove(name)\n}\n\n// Mkdir creates a new directory with the specified name and default permissions\nfunc (fs *SFTPFs) Mkdir(name string) error {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn client.Mkdir(name)\n}\n\n// Symlink creates source as a symbolic link to target.\nfunc (fs *SFTPFs) Symlink(source, target string) error {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn client.Symlink(source, target)\n}\n\n// Readlink returns the destination of the named symbolic link\nfunc (fs *SFTPFs) Readlink(name string) (string, error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresolved, err := client.ReadLink(name)\n\tif err != nil {\n\t\treturn resolved, err\n\t}\n\tresolved = path.Clean(strings.ReplaceAll(resolved, \"\\\\\", \"/\"))\n\tif !path.IsAbs(resolved) {\n\t\t// we assume that multiple links are not followed\n\t\tresolved = path.Join(path.Dir(name), resolved)\n\t}\n\treturn fs.GetRelativePath(resolved), nil\n}\n\n// Chown changes the numeric uid and gid of the named file.\nfunc (fs *SFTPFs) Chown(name string, uid int, gid int) error {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn client.Chown(name, uid, gid)\n}\n\n// Chmod changes the mode of the named file to mode.\nfunc (fs *SFTPFs) Chmod(name string, mode os.FileMode) error {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn client.Chmod(name, mode)\n}\n\n// Chtimes changes the access and modification times of the named file.\nfunc (fs *SFTPFs) Chtimes(name string, atime, mtime time.Time, _ bool) error {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn client.Chtimes(name, atime, mtime)\n}\n\n// Truncate changes the size of the named file.\nfunc (fs *SFTPFs) Truncate(name string, size int64) error {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn client.Truncate(name, size)\n}\n\n// ReadDir reads the directory named by dirname and returns\n// a list of directory entries.\nfunc (fs *SFTPFs) ReadDir(dirname string) (DirLister, error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfiles, err := client.ReadDir(dirname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &baseDirLister{files}, nil\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported.\nfunc (fs *SFTPFs) IsUploadResumeSupported() bool {\n\treturn fs.config.BufferSize == 0\n}\n\n// IsConditionalUploadResumeSupported returns if resuming uploads is supported\n// for the specified size\nfunc (fs *SFTPFs) IsConditionalUploadResumeSupported(_ int64) bool {\n\treturn fs.IsUploadResumeSupported()\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported.\nfunc (fs *SFTPFs) IsAtomicUploadSupported() bool {\n\treturn fs.config.BufferSize == 0\n}\n\n// IsNotExist returns a boolean indicating whether the error is known to\n// report that a file or directory does not exist\nfunc (*SFTPFs) IsNotExist(err error) bool {\n\treturn errors.Is(err, fs.ErrNotExist)\n}\n\n// IsPermission returns a boolean indicating whether the error is known to\n// report that permission is denied.\nfunc (*SFTPFs) IsPermission(err error) bool {\n\tif _, ok := err.(*pathResolutionError); ok {\n\t\treturn true\n\t}\n\treturn errors.Is(err, fs.ErrPermission)\n}\n\n// IsNotSupported returns true if the error indicate an unsupported operation\nfunc (*SFTPFs) IsNotSupported(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn err == ErrVfsUnsupported\n}\n\n// CheckRootPath creates the specified local root directory if it does not exists\nfunc (fs *SFTPFs) CheckRootPath(username string, uid int, gid int) bool {\n\t// local directory for temporary files in buffer mode\n\tosFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, \"\", nil)\n\tosFs.CheckRootPath(username, uid, gid)\n\tif fs.config.Prefix == \"/\" {\n\t\treturn true\n\t}\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn false\n\t}\n\tif err := client.MkdirAll(fs.config.Prefix); err != nil {\n\t\tfsLog(fs, logger.LevelDebug, \"error creating root directory %q for user %q: %v\", fs.config.Prefix, username, err)\n\t\treturn false\n\t}\n\treturn true\n}\n\n// ScanRootDirContents returns the number of files contained in a directory and\n// their size\nfunc (fs *SFTPFs) ScanRootDirContents() (int, int64, error) {\n\treturn fs.GetDirSize(fs.config.Prefix)\n}\n\n// CheckMetadata checks the metadata consistency\nfunc (*SFTPFs) CheckMetadata() error {\n\treturn nil\n}\n\n// GetAtomicUploadPath returns the path to use for an atomic upload\nfunc (*SFTPFs) GetAtomicUploadPath(name string) string {\n\tdir := path.Dir(name)\n\tguid := xid.New().String()\n\treturn path.Join(dir, \".sftpgo-upload.\"+guid+\".\"+path.Base(name))\n}\n\n// GetRelativePath returns the path for a file relative to the sftp prefix if any.\n// This is the path as seen by SFTPGo users\nfunc (fs *SFTPFs) GetRelativePath(name string) string {\n\trel := path.Clean(name)\n\tif rel == \".\" {\n\t\trel = \"\"\n\t}\n\tif !path.IsAbs(rel) {\n\t\t// If we have a relative path we assume it is already relative to the virtual root\n\t\trel = \"/\" + rel\n\t} else if fs.config.Prefix != \"/\" {\n\t\tprefixDir := fs.config.Prefix\n\t\tif !strings.HasSuffix(prefixDir, \"/\") {\n\t\t\tprefixDir += \"/\"\n\t\t}\n\n\t\tif rel == fs.config.Prefix {\n\t\t\trel = \"/\"\n\t\t} else if after, found := strings.CutPrefix(rel, prefixDir); found {\n\t\t\trel = path.Clean(\"/\" + after)\n\t\t} else {\n\t\t\t// Absolute path outside of the configured prefix\n\t\t\tfsLog(fs, logger.LevelWarn, \"path %q is an absolute path outside %q\", name, fs.config.Prefix)\n\t\t\trel = \"/\"\n\t\t}\n\t}\n\tif fs.mountPath != \"\" {\n\t\trel = path.Join(fs.mountPath, rel)\n\t}\n\treturn rel\n}\n\n// Walk walks the file tree rooted at root, calling walkFn for each file or\n// directory in the tree, including root\nfunc (fs *SFTPFs) Walk(root string, walkFn filepath.WalkFunc) error {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\twalker := client.Walk(root)\n\tfor walker.Step() {\n\t\terr := walker.Err()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = walkFn(walker.Path(), walker.Stat(), err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Join joins any number of path elements into a single path\nfunc (*SFTPFs) Join(elem ...string) string {\n\treturn path.Join(elem...)\n}\n\n// HasVirtualFolders returns true if folders are emulated\nfunc (*SFTPFs) HasVirtualFolders() bool {\n\treturn false\n}\n\n// ResolvePath returns the matching filesystem path for the specified virtual path\nfunc (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {\n\tif fs.mountPath != \"\" {\n\t\tif after, found := strings.CutPrefix(virtualPath, fs.mountPath); found {\n\t\t\tvirtualPath = after\n\t\t}\n\t}\n\tvirtualPath = path.Clean(\"/\" + virtualPath)\n\tfsPath := fs.Join(fs.config.Prefix, virtualPath)\n\tif fs.config.Prefix != \"/\" && fsPath != \"/\" {\n\t\t// we need to check if this path is a symlink outside the given prefix\n\t\t// or a file/dir inside a dir symlinked outside the prefix\n\t\tvar validatedPath string\n\t\tvar err error\n\t\tvalidatedPath, err = fs.getRealPath(fsPath)\n\t\tisNotExist := fs.IsNotExist(err)\n\t\tif err != nil && !isNotExist {\n\t\t\tfsLog(fs, logger.LevelError, \"Invalid path resolution, original path %v resolved %q err: %v\",\n\t\t\t\tvirtualPath, fsPath, err)\n\t\t\treturn \"\", err\n\t\t} else if isNotExist {\n\t\t\tfor fs.IsNotExist(err) {\n\t\t\t\tvalidatedPath = path.Dir(validatedPath)\n\t\t\t\tif validatedPath == \"/\" {\n\t\t\t\t\terr = nil\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tvalidatedPath, err = fs.getRealPath(validatedPath)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tfsLog(fs, logger.LevelError, \"Invalid path resolution, dir %q original path %q resolved %q err: %v\",\n\t\t\t\t\tvalidatedPath, virtualPath, fsPath, err)\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\t\tif err := fs.isSubDir(validatedPath); err != nil {\n\t\t\tfsLog(fs, logger.LevelError, \"Invalid path resolution, dir %q original path %q resolved %q err: %v\",\n\t\t\t\tvalidatedPath, virtualPath, fsPath, err)\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn fsPath, nil\n}\n\n// RealPath implements the FsRealPather interface\nfunc (fs *SFTPFs) RealPath(p string) (string, error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresolved, err := client.RealPath(p)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresolved = path.Clean(strings.ReplaceAll(resolved, \"\\\\\", \"/\"))\n\tif fs.config.Prefix != \"/\" {\n\t\tif err := fs.isSubDir(resolved); err != nil {\n\t\t\tfsLog(fs, logger.LevelError, \"Invalid real path resolution, original path %q resolved %q err: %v\",\n\t\t\t\tp, resolved, err)\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn fs.GetRelativePath(resolved), nil\n}\n\n// getRealPath returns the real remote path trying to resolve symbolic links if any\nfunc (fs *SFTPFs) getRealPath(name string) (string, error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tlinksWalked := 0\n\tfor {\n\t\tinfo, err := client.Lstat(name)\n\t\tif err != nil {\n\t\t\treturn name, err\n\t\t}\n\t\tif info.Mode()&os.ModeSymlink == 0 {\n\t\t\treturn name, nil\n\t\t}\n\t\tresolvedLink, err := client.ReadLink(name)\n\t\tif err != nil {\n\t\t\treturn name, fmt.Errorf(\"unable to resolve link to %q: %w\", name, err)\n\t\t}\n\t\tresolvedLink = strings.ReplaceAll(resolvedLink, \"\\\\\", \"/\")\n\t\tresolvedLink = path.Clean(resolvedLink)\n\t\tif path.IsAbs(resolvedLink) {\n\t\t\tname = resolvedLink\n\t\t} else {\n\t\t\tname = path.Join(path.Dir(name), resolvedLink)\n\t\t}\n\t\tlinksWalked++\n\t\tif linksWalked > 10 {\n\t\t\tfsLog(fs, logger.LevelError, \"unable to get real path, too many links: %d\", linksWalked)\n\t\t\treturn \"\", &pathResolutionError{err: \"too many links\"}\n\t\t}\n\t}\n}\n\nfunc (fs *SFTPFs) isSubDir(name string) error {\n\tif name == fs.config.Prefix {\n\t\treturn nil\n\t}\n\tif len(name) < len(fs.config.Prefix) {\n\t\terr := fmt.Errorf(\"path %q is not inside: %q\", name, fs.config.Prefix)\n\t\treturn &pathResolutionError{err: err.Error()}\n\t}\n\tif !strings.HasPrefix(name, fs.config.Prefix+\"/\") {\n\t\terr := fmt.Errorf(\"path %q is not inside: %q\", name, fs.config.Prefix)\n\t\treturn &pathResolutionError{err: err.Error()}\n\t}\n\treturn nil\n}\n\n// GetDirSize returns the number of files and the size for a folder\n// including any subfolders\nfunc (fs *SFTPFs) GetDirSize(dirname string) (int, int64, error) {\n\tnumFiles := 0\n\tsize := int64(0)\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn numFiles, size, err\n\t}\n\tisDir, err := isDirectory(fs, dirname)\n\tif err == nil && isDir {\n\t\twalker := client.Walk(dirname)\n\t\tfor walker.Step() {\n\t\t\terr := walker.Err()\n\t\t\tif err != nil {\n\t\t\t\treturn numFiles, size, err\n\t\t\t}\n\t\t\tif walker.Stat().Mode().IsRegular() {\n\t\t\t\tsize += walker.Stat().Size()\n\t\t\t\tnumFiles++\n\t\t\t\tif numFiles%1000 == 0 {\n\t\t\t\t\tfsLog(fs, logger.LevelDebug, \"dirname %q scan in progress, files: %d, size: %d\", dirname, numFiles, size)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn numFiles, size, err\n}\n\n// GetMimeType returns the content type\nfunc (fs *SFTPFs) GetMimeType(name string) (string, error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tf, err := client.OpenFile(name, os.O_RDONLY)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer f.Close()\n\tvar buf [512]byte\n\tn, err := io.ReadFull(f, buf[:])\n\tif err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t\treturn \"\", err\n\t}\n\tctype := http.DetectContentType(buf[:n])\n\t// Rewind file.\n\t_, err = f.Seek(0, io.SeekStart)\n\treturn ctype, err\n}\n\n// GetAvailableDiskSize returns the available size for the specified path\nfunc (fs *SFTPFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {\n\tclient, err := fs.conn.getClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif _, ok := client.HasExtension(\"statvfs@openssh.com\"); !ok {\n\t\treturn nil, ErrStorageSizeUnavailable\n\t}\n\treturn client.StatVFS(dirName)\n}\n\n// Close the connection\nfunc (fs *SFTPFs) Close() error {\n\tfs.conn.RemoveSession(fs.connectionID)\n\treturn nil\n}\n\nfunc (fs *SFTPFs) createConnection() error {\n\terr := fs.conn.OpenConnection()\n\tif err != nil {\n\t\tfsLog(fs, logger.LevelError, \"error opening connection: %v\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\ntype sftpConnection struct {\n\tconfig       *SFTPFsConfig\n\tlogSender    string\n\tsshClient    *ssh.Client\n\tsftpClient   *sftp.Client\n\tmu           sync.RWMutex\n\tisConnected  bool\n\tsessions     map[string]bool\n\tlastActivity time.Time\n\tsigner       ssh.Signer\n}\n\nfunc newSFTPConnection(config *SFTPFsConfig, sessionID string) *sftpConnection {\n\tc := &sftpConnection{\n\t\tconfig:       config,\n\t\tlogSender:    fmt.Sprintf(`%s \"%s@%s\"`, sftpFsName, config.Username, config.Endpoint),\n\t\tisConnected:  false,\n\t\tsessions:     map[string]bool{},\n\t\tlastActivity: time.Now().UTC(),\n\t\tsigner:       nil,\n\t}\n\tc.sessions[sessionID] = true\n\treturn c\n}\n\nfunc (c *sftpConnection) OpenConnection() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\treturn c.openConnNoLock()\n}\n\nfunc (c *sftpConnection) openConnNoLock() error {\n\tif c.isConnected {\n\t\tlogger.Debug(c.logSender, \"\", \"reusing connection\")\n\t\treturn nil\n\t}\n\n\tlogger.Debug(c.logSender, \"\", \"try to open a new connection\")\n\tclientConfig := &ssh.ClientConfig{\n\t\tUser: c.config.Username,\n\t\tHostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {\n\t\t\tfp := ssh.FingerprintSHA256(key)\n\t\t\tif slices.Contains(sftpFingerprints, fp) {\n\t\t\t\tif allowSelfConnections == 0 {\n\t\t\t\t\tlogger.Log(logger.LevelError, c.logSender, \"\", \"SFTP self connections not allowed\")\n\t\t\t\t\treturn ErrSFTPLoop\n\t\t\t\t}\n\t\t\t\tif slices.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {\n\t\t\t\t\tlogger.Log(logger.LevelError, c.logSender, \"\",\n\t\t\t\t\t\t\"SFTP loop or nested local SFTP folders detected, username %q, forbidden usernames: %+v\",\n\t\t\t\t\t\tc.config.Username, c.config.forbiddenSelfUsernames)\n\t\t\t\t\treturn ErrSFTPLoop\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(c.config.Fingerprints) > 0 {\n\t\t\t\tfor _, provided := range c.config.Fingerprints {\n\t\t\t\t\tif provided == fp {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"invalid fingerprint %q\", fp)\n\t\t\t}\n\t\t\tlogger.Log(logger.LevelWarn, c.logSender, \"\", \"login without host key validation, please provide at least a fingerprint!\")\n\t\t\treturn nil\n\t\t},\n\t\tTimeout:       15 * time.Second,\n\t\tClientVersion: fmt.Sprintf(\"SSH-2.0-%s\", version.GetServerVersion(\"_\", false)),\n\t}\n\tif c.signer != nil {\n\t\tclientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(c.signer))\n\t}\n\tif pwd := c.config.Password.GetPayload(); pwd != \"\" {\n\t\tclientConfig.Auth = append(clientConfig.Auth, ssh.Password(pwd))\n\t}\n\tsupportedAlgos := ssh.SupportedAlgorithms()\n\tinsecureAlgos := ssh.InsecureAlgorithms()\n\t// add all available ciphers, KEXs and MACs, they are negotiated according to the order\n\tclientConfig.Ciphers = append(supportedAlgos.Ciphers, ssh.InsecureCipherAES128CBC)\n\tclientConfig.KeyExchanges = append(supportedAlgos.KeyExchanges, insecureAlgos.KeyExchanges...)\n\tclientConfig.MACs = append(supportedAlgos.MACs, insecureAlgos.MACs...)\n\tsshClient, err := ssh.Dial(\"tcp\", c.config.Endpoint, clientConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sftpfs: unable to connect: %w\", err)\n\t}\n\tsftpClient, err := sftp.NewClient(sshClient, c.getClientOptions()...)\n\tif err != nil {\n\t\tsshClient.Close()\n\t\treturn fmt.Errorf(\"sftpfs: unable to create SFTP client: %w\", err)\n\t}\n\tc.sshClient = sshClient\n\tc.sftpClient = sftpClient\n\tc.isConnected = true\n\tgo c.Wait()\n\treturn nil\n}\n\nfunc (c *sftpConnection) getClientOptions() []sftp.ClientOption {\n\tvar options []sftp.ClientOption\n\tif c.config.DisableCouncurrentReads {\n\t\toptions = append(options, sftp.UseConcurrentReads(false))\n\t\tlogger.Debug(c.logSender, \"\", \"disabling concurrent reads\")\n\t}\n\tif c.config.BufferSize > 0 {\n\t\toptions = append(options, sftp.UseConcurrentWrites(true))\n\t\tlogger.Debug(c.logSender, \"\", \"enabling concurrent writes\")\n\t}\n\treturn options\n}\n\nfunc (c *sftpConnection) getClient() (*sftp.Client, error) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif c.isConnected {\n\t\treturn c.sftpClient, nil\n\t}\n\terr := c.openConnNoLock()\n\treturn c.sftpClient, err\n}\n\nfunc (c *sftpConnection) Wait() {\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tvar watchdogInProgress atomic.Bool\n\t\tticker := time.NewTicker(30 * time.Second)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\tif watchdogInProgress.Load() {\n\t\t\t\t\tlogger.Error(c.logSender, \"\", \"watchdog still in progress, closing hanging connection\")\n\t\t\t\t\tc.sshClient.Close()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tgo func() {\n\t\t\t\t\twatchdogInProgress.Store(true)\n\t\t\t\t\tdefer watchdogInProgress.Store(false)\n\n\t\t\t\t\t_, err := c.sftpClient.Getwd()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Error(c.logSender, \"\", \"watchdog error: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\tcase <-done:\n\t\t\t\tlogger.Debug(c.logSender, \"\", \"quitting watchdog\")\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\t// we wait on the sftp client otherwise if the channel is closed but not the connection\n\t// we don't detect the event.\n\terr := c.sftpClient.Wait()\n\tlogger.Log(logger.LevelDebug, c.logSender, \"\", \"sftp channel closed: %v\", err)\n\tclose(done)\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.isConnected = false\n\tif c.sshClient != nil {\n\t\tc.sshClient.Close()\n\t}\n}\n\nfunc (c *sftpConnection) Close() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tlogger.Debug(c.logSender, \"\", \"closing connection\")\n\tvar sftpErr, sshErr error\n\tif c.sftpClient != nil {\n\t\tsftpErr = c.sftpClient.Close()\n\t}\n\tif c.sshClient != nil {\n\t\tsshErr = c.sshClient.Close()\n\t}\n\tif sftpErr != nil {\n\t\treturn sftpErr\n\t}\n\tc.isConnected = false\n\treturn sshErr\n}\n\nfunc (c *sftpConnection) AddSession(sessionID string) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.sessions[sessionID] = true\n\tlogger.Debug(c.logSender, \"\", \"added session %s, active sessions: %d\", sessionID, len(c.sessions))\n}\n\nfunc (c *sftpConnection) RemoveSession(sessionID string) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tdelete(c.sessions, sessionID)\n\tlogger.Debug(c.logSender, \"\", \"removed session %s, active sessions: %d\", sessionID, len(c.sessions))\n\tif len(c.sessions) == 0 {\n\t\tc.lastActivity = time.Now().UTC()\n\t}\n}\n\nfunc (c *sftpConnection) ActiveSessions() int {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\treturn len(c.sessions)\n}\n\nfunc (c *sftpConnection) GetLastActivity() time.Time {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\n\tif len(c.sessions) > 0 {\n\t\treturn time.Now().UTC()\n\t}\n\tlogger.Debug(c.logSender, \"\", \"last activity %s\", c.lastActivity)\n\treturn c.lastActivity\n}\n\ntype sftpConnectionsCache struct {\n\tscheduler *cron.Cron\n\tsync.Mutex\n\titems map[string]*sftpConnection\n}\n\nfunc newSFTPConnectionCache() *sftpConnectionsCache {\n\tc := &sftpConnectionsCache{\n\t\tscheduler: cron.New(cron.WithLocation(time.UTC), cron.WithLogger(cron.DiscardLogger)),\n\t\titems:     make(map[string]*sftpConnection),\n\t}\n\t_, err := c.scheduler.AddFunc(\"@every 1m\", c.Cleanup)\n\tutil.PanicOnError(err)\n\tc.scheduler.Start()\n\treturn c\n}\n\nfunc (c *sftpConnectionsCache) Get(config *SFTPFsConfig, sessionID string) (*sftpConnection, error) {\n\tpartition := 0\n\tkey := config.getUniqueID(partition)\n\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tfor {\n\t\tif val, ok := c.items[key]; ok {\n\t\t\tactiveSessions := val.ActiveSessions()\n\t\t\tif activeSessions < maxSessionsPerConnection {\n\t\t\t\tlogger.Debug(logSenderSFTPCache, \"\",\n\t\t\t\t\t\"reusing connection for session ID %q, key %s, active sessions %d, active connections: %d\",\n\t\t\t\t\tsessionID, key, activeSessions+1, len(c.items))\n\t\t\t\tval.AddSession(sessionID)\n\t\t\t\treturn val, nil\n\t\t\t}\n\t\t\tpartition++\n\t\t\tkey = config.getUniqueID(partition)\n\t\t\tlogger.Debug(logSenderSFTPCache, \"\",\n\t\t\t\t\"connection full, generated new key for partition: %d, active sessions: %d, key: %s\",\n\t\t\t\tpartition, activeSessions, key)\n\t\t} else {\n\t\t\tconn := newSFTPConnection(config, sessionID)\n\t\t\tsigner, err := config.getKeySigner()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"sftpfs: unable to parse the private key: %w\", err)\n\t\t\t}\n\t\t\tconn.signer = signer\n\t\t\tc.items[key] = conn\n\t\t\tlogger.Debug(logSenderSFTPCache, \"\",\n\t\t\t\t\"adding new connection for session ID %q, partition: %d, key: %s, active connections: %d\",\n\t\t\t\tsessionID, partition, key, len(c.items))\n\t\t\treturn conn, nil\n\t\t}\n\t}\n}\n\nfunc (c *sftpConnectionsCache) Cleanup() {\n\tc.Lock()\n\n\tvar connectionsToClose []*sftpConnection\n\n\tfor k, conn := range c.items {\n\t\tif val := conn.GetLastActivity(); val.Before(time.Now().Add(-30 * time.Second)) {\n\t\t\tdelete(c.items, k)\n\t\t\tlogger.Debug(logSenderSFTPCache, \"\", \"removed connection with key %s, last activity %s, active connections: %d\",\n\t\t\t\tk, val, len(c.items))\n\t\t\tconnectionsToClose = append(connectionsToClose, conn)\n\t\t}\n\t}\n\n\tc.Unlock()\n\n\tfor _, conn := range connectionsToClose {\n\t\terr := conn.Close()\n\t\tlogger.Debug(logSenderSFTPCache, \"\", \"connection closed, err: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/vfs/statvfs_fallback.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !darwin && !linux && !freebsd\n\npackage vfs\n\nimport (\n\t\"github.com/pkg/sftp\"\n\t\"github.com/shirou/gopsutil/v3/disk\"\n)\n\nconst bsize = uint64(4096)\n\nfunc getStatFS(path string) (*sftp.StatVFS, error) {\n\tusage, err := disk.Usage(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// we assume block size = 4096\n\tblocks := usage.Total / bsize\n\tbfree := usage.Free / bsize\n\tfiles := usage.InodesTotal\n\tffree := usage.InodesFree\n\tif files == 0 {\n\t\t// these assumptions are wrong but still better than returning 0\n\t\tfiles = blocks / 4\n\t\tffree = bfree / 4\n\t}\n\treturn &sftp.StatVFS{\n\t\tBsize:   bsize,\n\t\tFrsize:  bsize,\n\t\tBlocks:  blocks,\n\t\tBfree:   bfree,\n\t\tBavail:  bfree,\n\t\tFiles:   files,\n\t\tFfree:   ffree,\n\t\tFavail:  ffree,\n\t\tNamemax: 255,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/vfs/statvfs_linux.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build linux\n\npackage vfs\n\nimport (\n\t\"github.com/pkg/sftp\"\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc getStatFS(path string) (*sftp.StatVFS, error) {\n\tstat := unix.Statfs_t{}\n\terr := unix.Statfs(path, &stat)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &sftp.StatVFS{\n\t\tBsize:   uint64(stat.Bsize),\n\t\tFrsize:  uint64(stat.Frsize),\n\t\tBlocks:  stat.Blocks,\n\t\tBfree:   stat.Bfree,\n\t\tBavail:  stat.Bavail,\n\t\tFiles:   stat.Files,\n\t\tFfree:   stat.Ffree,\n\t\tFavail:  stat.Ffree, // not sure how to calculate Favail\n\t\tFlag:    uint64(stat.Flags),\n\t\tNamemax: uint64(stat.Namelen),\n\t}, nil\n}\n"
  },
  {
    "path": "internal/vfs/statvfs_unix.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build freebsd || darwin\n\npackage vfs\n\nimport (\n\t\"github.com/pkg/sftp\"\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc getStatFS(path string) (*sftp.StatVFS, error) {\n\tstat := unix.Statfs_t{}\n\terr := unix.Statfs(path, &stat)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &sftp.StatVFS{\n\t\tBsize:   uint64(stat.Bsize),\n\t\tFrsize:  uint64(stat.Bsize),\n\t\tBlocks:  stat.Blocks,\n\t\tBfree:   stat.Bfree,\n\t\tBavail:  uint64(stat.Bavail),\n\t\tFiles:   stat.Files,\n\t\tFfree:   uint64(stat.Ffree),\n\t\tFavail:  uint64(stat.Ffree), // not sure how to calculate Favail\n\t\tFlag:    uint64(stat.Flags),\n\t\tNamemax: 255, // we use a conservative value here\n\t}, nil\n}\n"
  },
  {
    "path": "internal/vfs/sys_unix.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n//go:build !windows\n\npackage vfs\n\nimport (\n\t\"errors\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc isCrossDeviceError(err error) bool {\n\treturn errors.Is(err, unix.EXDEV)\n}\n\nfunc isInvalidNameError(_ error) bool {\n\treturn false\n}\n"
  },
  {
    "path": "internal/vfs/sys_windows.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage vfs\n\nimport (\n\t\"errors\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc isCrossDeviceError(err error) bool {\n\treturn errors.Is(err, windows.ERROR_NOT_SAME_DEVICE)\n}\n\nfunc isInvalidNameError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn errors.Is(err, windows.ERROR_INVALID_NAME)\n}\n"
  },
  {
    "path": "internal/vfs/vfs.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package vfs provides local and remote filesystems support\npackage vfs\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/eikenb/pipeat\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/sftpgo/sdk\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\nconst (\n\tdirMimeType       = \"inode/directory\"\n\ts3fsName          = \"S3Fs\"\n\tgcsfsName         = \"GCSFs\"\n\tazBlobFsName      = \"AzureBlobFs\"\n\tlastModifiedField = \"sftpgo_last_modified\"\n\tpreResumeTimeout  = 90 * time.Second\n\t// ListerBatchSize defines the default limit for DirLister implementations\n\tListerBatchSize = 1000\n)\n\n// Additional checks for files\nconst (\n\tCheckParentDir     = 1\n\tCheckResume        = 2\n\tCheckUpdateModTime = 4\n)\n\nvar (\n\tvalidAzAccessTier = []string{\"\", \"Archive\", \"Hot\", \"Cool\"}\n\t// ErrStorageSizeUnavailable is returned if the storage backend does not support getting the size\n\tErrStorageSizeUnavailable = errors.New(\"unable to get available size for this storage backend\")\n\t// ErrVfsUnsupported defines the error for an unsupported VFS operation\n\tErrVfsUnsupported        = errors.New(\"not supported\")\n\terrInvalidDirListerLimit = errors.New(\"dir lister: invalid limit, must be > 0\")\n\ttempPath                 string\n\tsftpFingerprints         []string\n\tallowSelfConnections     int\n\trenameMode               int\n\treadMetadata             int\n\tresumeMaxSize            int64\n\tuploadMode               int\n)\n\nvar (\n\tcreatePipeFn = func(dirPath string, _ int64) (pipeReaderAt, pipeWriterAt, error) {\n\t\treturn pipeat.PipeInDir(dirPath)\n\t}\n)\n\n// SetAllowSelfConnections sets the desired behaviour for self connections\nfunc SetAllowSelfConnections(value int) {\n\tallowSelfConnections = value\n}\n\n// SetTempPath sets the path for temporary files\nfunc SetTempPath(fsPath string) {\n\ttempPath = fsPath\n}\n\n// GetTempPath returns the path for temporary files\nfunc GetTempPath() string {\n\treturn tempPath\n}\n\n// SetSFTPFingerprints sets the SFTP host key fingerprints\nfunc SetSFTPFingerprints(fp []string) {\n\tsftpFingerprints = fp\n}\n\n// SetRenameMode sets the rename mode\nfunc SetRenameMode(val int) {\n\trenameMode = val\n}\n\n// SetReadMetadataMode sets the read metadata mode\nfunc SetReadMetadataMode(val int) {\n\treadMetadata = val\n}\n\n// SetResumeMaxSize sets the max size allowed for resuming uploads for backends\n// with immutable objects\nfunc SetResumeMaxSize(val int64) {\n\tresumeMaxSize = val\n}\n\n// SetUploadMode sets the upload mode\nfunc SetUploadMode(val int) {\n\tuploadMode = val\n}\n\n// Fs defines the interface for filesystem backends\ntype Fs interface {\n\tName() string\n\tConnectionID() string\n\tStat(name string) (os.FileInfo, error)\n\tLstat(name string) (os.FileInfo, error)\n\tOpen(name string, offset int64) (File, PipeReader, func(), error)\n\tCreate(name string, flag, checks int) (File, PipeWriter, func(), error)\n\tRename(source, target string, checks int) (int, int64, error)\n\tRemove(name string, isDir bool) error\n\tMkdir(name string) error\n\tSymlink(source, target string) error\n\tChown(name string, uid int, gid int) error\n\tChmod(name string, mode os.FileMode) error\n\tChtimes(name string, atime, mtime time.Time, isUploading bool) error\n\tTruncate(name string, size int64) error\n\tReadDir(dirname string) (DirLister, error)\n\tReadlink(name string) (string, error)\n\tIsUploadResumeSupported() bool\n\tIsConditionalUploadResumeSupported(size int64) bool\n\tIsAtomicUploadSupported() bool\n\tCheckRootPath(username string, uid int, gid int) bool\n\tResolvePath(virtualPath string) (string, error)\n\tIsNotExist(err error) bool\n\tIsPermission(err error) bool\n\tIsNotSupported(err error) bool\n\tScanRootDirContents() (int, int64, error)\n\tGetDirSize(dirname string) (int, int64, error)\n\tGetAtomicUploadPath(name string) string\n\tGetRelativePath(name string) string\n\tWalk(root string, walkFn filepath.WalkFunc) error\n\tJoin(elem ...string) string\n\tHasVirtualFolders() bool\n\tGetMimeType(name string) (string, error)\n\tGetAvailableDiskSize(dirName string) (*sftp.StatVFS, error)\n\tClose() error\n}\n\n// FsRealPather is a Fs that implements the RealPath method.\ntype FsRealPather interface {\n\tFs\n\tRealPath(p string) (string, error)\n}\n\n// FsFileCopier is a Fs that implements the CopyFile method.\ntype FsFileCopier interface {\n\tFs\n\tCopyFile(source, target string, srcInfo os.FileInfo) (int, int64, error)\n}\n\n// File defines an interface representing a SFTPGo file\ntype File interface {\n\tio.Reader\n\tio.Writer\n\tio.Closer\n\tio.ReaderAt\n\tio.WriterAt\n\tio.Seeker\n\tStat() (os.FileInfo, error)\n\tName() string\n\tTruncate(size int64) error\n}\n\n// PipeWriter defines an interface representing a SFTPGo pipe writer\ntype PipeWriter interface {\n\tio.Writer\n\tio.WriterAt\n\tio.Closer\n\tDone(err error)\n\tGetWrittenBytes() int64\n}\n\n// PipeReader defines an interface representing a SFTPGo pipe reader\ntype PipeReader interface {\n\tio.Reader\n\tio.ReaderAt\n\tio.Closer\n\tsetMetadata(value map[string]string)\n\tsetMetadataFromPointerVal(value map[string]*string)\n\tMetadata() map[string]string\n}\n\ntype pipeReaderAt interface {\n\tRead(p []byte) (int, error)\n\tReadAt(p []byte, offset int64) (int, error)\n\tGetReadedBytes() int64\n\tClose() error\n\tCloseWithError(err error) error\n}\n\ntype pipeWriterAt interface {\n\tWrite(p []byte) (int, error)\n\tWriteAt(p []byte, offset int64) (int, error)\n\tGetWrittenBytes() int64\n\tClose() error\n\tCloseWithError(err error) error\n}\n\n// DirLister defines an interface for a directory lister\ntype DirLister interface {\n\tNext(limit int) ([]os.FileInfo, error)\n\tClose() error\n}\n\n// Metadater defines an interface to implement to return metadata for a file\ntype Metadater interface {\n\tMetadata() map[string]string\n}\n\ntype baseDirLister struct {\n\tcache []os.FileInfo\n}\n\nfunc (l *baseDirLister) Next(limit int) ([]os.FileInfo, error) {\n\tif limit <= 0 {\n\t\treturn nil, errInvalidDirListerLimit\n\t}\n\tif len(l.cache) >= limit {\n\t\treturn l.returnFromCache(limit), nil\n\t}\n\treturn l.returnFromCache(limit), io.EOF\n}\n\nfunc (l *baseDirLister) returnFromCache(limit int) []os.FileInfo {\n\tif len(l.cache) >= limit {\n\t\tresult := l.cache[:limit]\n\t\tl.cache = l.cache[limit:]\n\t\treturn result\n\t}\n\tresult := l.cache\n\tl.cache = nil\n\treturn result\n}\n\nfunc (l *baseDirLister) Close() error {\n\tl.cache = nil\n\treturn nil\n}\n\n// QuotaCheckResult defines the result for a quota check\ntype QuotaCheckResult struct {\n\tHasSpace     bool\n\tAllowedSize  int64\n\tAllowedFiles int\n\tUsedSize     int64\n\tUsedFiles    int\n\tQuotaSize    int64\n\tQuotaFiles   int\n}\n\n// GetRemainingSize returns the remaining allowed size\nfunc (q *QuotaCheckResult) GetRemainingSize() int64 {\n\tif q.QuotaSize > 0 {\n\t\treturn q.QuotaSize - q.UsedSize\n\t}\n\treturn 0\n}\n\n// GetRemainingFiles returns the remaining allowed files\nfunc (q *QuotaCheckResult) GetRemainingFiles() int {\n\tif q.QuotaFiles > 0 {\n\t\treturn q.QuotaFiles - q.UsedFiles\n\t}\n\treturn 0\n}\n\n// S3FsConfig defines the configuration for S3 based filesystem\ntype S3FsConfig struct {\n\tsdk.BaseS3FsConfig\n\tAccessSecret   *kms.Secret `json:\"access_secret,omitempty\"`\n\tSSECustomerKey *kms.Secret `json:\"sse_customer_key,omitempty\"`\n}\n\n// HideConfidentialData hides confidential data\nfunc (c *S3FsConfig) HideConfidentialData() {\n\tif c.AccessSecret != nil {\n\t\tc.AccessSecret.Hide()\n\t}\n\tif c.SSECustomerKey != nil {\n\t\tc.SSECustomerKey.Hide()\n\t}\n}\n\nfunc (c *S3FsConfig) isEqual(other S3FsConfig) bool {\n\tif c.Bucket != other.Bucket {\n\t\treturn false\n\t}\n\tif c.KeyPrefix != other.KeyPrefix {\n\t\treturn false\n\t}\n\tif c.Region != other.Region {\n\t\treturn false\n\t}\n\tif c.AccessKey != other.AccessKey {\n\t\treturn false\n\t}\n\tif c.RoleARN != other.RoleARN {\n\t\treturn false\n\t}\n\tif c.Endpoint != other.Endpoint {\n\t\treturn false\n\t}\n\tif c.StorageClass != other.StorageClass {\n\t\treturn false\n\t}\n\tif c.ACL != other.ACL {\n\t\treturn false\n\t}\n\tif !c.areMultipartFieldsEqual(other) {\n\t\treturn false\n\t}\n\tif c.ForcePathStyle != other.ForcePathStyle {\n\t\treturn false\n\t}\n\tif c.SkipTLSVerify != other.SkipTLSVerify {\n\t\treturn false\n\t}\n\treturn c.isSecretEqual(other)\n}\n\nfunc (c *S3FsConfig) areMultipartFieldsEqual(other S3FsConfig) bool {\n\tif c.UploadPartSize != other.UploadPartSize {\n\t\treturn false\n\t}\n\tif c.UploadConcurrency != other.UploadConcurrency {\n\t\treturn false\n\t}\n\tif c.DownloadConcurrency != other.DownloadConcurrency {\n\t\treturn false\n\t}\n\tif c.DownloadPartSize != other.DownloadPartSize {\n\t\treturn false\n\t}\n\tif c.DownloadPartMaxTime != other.DownloadPartMaxTime {\n\t\treturn false\n\t}\n\tif c.UploadPartMaxTime != other.UploadPartMaxTime {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (c *S3FsConfig) isSecretEqual(other S3FsConfig) bool {\n\tif c.SSECustomerKey == nil {\n\t\tc.SSECustomerKey = kms.NewEmptySecret()\n\t}\n\tif other.SSECustomerKey == nil {\n\t\tother.SSECustomerKey = kms.NewEmptySecret()\n\t}\n\tif !c.SSECustomerKey.IsEqual(other.SSECustomerKey) {\n\t\treturn false\n\t}\n\tif c.AccessSecret == nil {\n\t\tc.AccessSecret = kms.NewEmptySecret()\n\t}\n\tif other.AccessSecret == nil {\n\t\tother.AccessSecret = kms.NewEmptySecret()\n\t}\n\treturn c.AccessSecret.IsEqual(other.AccessSecret)\n}\n\nfunc (c *S3FsConfig) checkCredentials() error {\n\tif c.AccessKey == \"\" && !c.AccessSecret.IsEmpty() {\n\t\treturn util.NewI18nError(\n\t\t\terrors.New(\"access_key cannot be empty with access_secret not empty\"),\n\t\t\tutil.I18nErrorAccessKeyRequired,\n\t\t)\n\t}\n\tif c.AccessSecret.IsEmpty() && c.AccessKey != \"\" {\n\t\treturn util.NewI18nError(\n\t\t\terrors.New(\"access_secret cannot be empty with access_key not empty\"),\n\t\t\tutil.I18nErrorAccessSecretRequired,\n\t\t)\n\t}\n\tif c.AccessSecret.IsEncrypted() && !c.AccessSecret.IsValid() {\n\t\treturn errors.New(\"invalid encrypted access_secret\")\n\t}\n\tif !c.AccessSecret.IsEmpty() && !c.AccessSecret.IsValidInput() {\n\t\treturn errors.New(\"invalid access_secret\")\n\t}\n\tif c.SSECustomerKey.IsEncrypted() && !c.SSECustomerKey.IsValid() {\n\t\treturn errors.New(\"invalid encrypted sse_customer_key\")\n\t}\n\tif !c.SSECustomerKey.IsEmpty() && !c.SSECustomerKey.IsValidInput() {\n\t\treturn errors.New(\"invalid sse_customer_key\")\n\t}\n\treturn nil\n}\n\n// ValidateAndEncryptCredentials validates the configuration and encrypts access secret if it is in plain text\nfunc (c *S3FsConfig) ValidateAndEncryptCredentials(additionalData string) error {\n\tif err := c.validate(); err != nil {\n\t\tvar errI18n *util.I18nError\n\t\terrValidation := util.NewValidationError(fmt.Sprintf(\"could not validate s3config: %v\", err))\n\t\tif errors.As(err, &errI18n) {\n\t\t\treturn util.NewI18nError(errValidation, errI18n.Message)\n\t\t}\n\t\treturn util.NewI18nError(errValidation, util.I18nErrorFsValidation)\n\t}\n\tif c.AccessSecret.IsPlain() {\n\t\tc.AccessSecret.SetAdditionalData(additionalData)\n\t\terr := c.AccessSecret.Encrypt()\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt s3 access secret: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\tif c.SSECustomerKey.IsPlain() {\n\t\tc.SSECustomerKey.SetAdditionalData(additionalData)\n\t\terr := c.SSECustomerKey.Encrypt()\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt s3 SSE customer key: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *S3FsConfig) checkPartSizeAndConcurrency() error {\n\tif c.UploadPartSize != 0 && (c.UploadPartSize < 5 || c.UploadPartSize > 2000) {\n\t\treturn util.NewI18nError(\n\t\t\terrors.New(\"upload_part_size cannot be != 0, lower than 5 (MB) or greater than 2000 (MB)\"),\n\t\t\tutil.I18nErrorULPartSizeInvalid,\n\t\t)\n\t}\n\tif c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"invalid upload concurrency: %v\", c.UploadConcurrency),\n\t\t\tutil.I18nErrorULConcurrencyInvalid,\n\t\t)\n\t}\n\tif c.DownloadPartSize != 0 && (c.DownloadPartSize < 5 || c.DownloadPartSize > 2000) {\n\t\treturn util.NewI18nError(\n\t\t\terrors.New(\"download_part_size cannot be != 0, lower than 5 (MB) or greater than 2000 (MB)\"),\n\t\t\tutil.I18nErrorDLPartSizeInvalid,\n\t\t)\n\t}\n\tif c.DownloadConcurrency < 0 || c.DownloadConcurrency > 64 {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"invalid download concurrency: %v\", c.DownloadConcurrency),\n\t\t\tutil.I18nErrorDLConcurrencyInvalid,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc (c *S3FsConfig) isSameResource(other S3FsConfig) bool {\n\tif c.Bucket != other.Bucket {\n\t\treturn false\n\t}\n\tif c.Endpoint != other.Endpoint {\n\t\treturn false\n\t}\n\treturn c.Region == other.Region\n}\n\n// validate returns an error if the configuration is not valid\nfunc (c *S3FsConfig) validate() error {\n\tif c.AccessSecret == nil {\n\t\tc.AccessSecret = kms.NewEmptySecret()\n\t}\n\tif c.SSECustomerKey == nil {\n\t\tc.SSECustomerKey = kms.NewEmptySecret()\n\t}\n\tif c.Bucket == \"\" {\n\t\treturn util.NewI18nError(errors.New(\"bucket cannot be empty\"), util.I18nErrorBucketRequired)\n\t}\n\t// the region may be embedded within the endpoint for some S3 compatible\n\t// object storage, for example B2\n\tif c.Endpoint == \"\" && c.Region == \"\" {\n\t\treturn util.NewI18nError(errors.New(\"region cannot be empty\"), util.I18nErrorRegionRequired)\n\t}\n\tif err := c.checkCredentials(); err != nil {\n\t\treturn err\n\t}\n\tif c.KeyPrefix != \"\" {\n\t\tif strings.HasPrefix(c.KeyPrefix, \"/\") {\n\t\t\treturn util.NewI18nError(errors.New(\"key_prefix cannot start with /\"), util.I18nErrorKeyPrefixInvalid)\n\t\t}\n\t\tc.KeyPrefix = path.Clean(c.KeyPrefix)\n\t\tif !strings.HasSuffix(c.KeyPrefix, \"/\") {\n\t\t\tc.KeyPrefix += \"/\"\n\t\t}\n\t}\n\tc.StorageClass = strings.TrimSpace(c.StorageClass)\n\tc.ACL = strings.TrimSpace(c.ACL)\n\treturn c.checkPartSizeAndConcurrency()\n}\n\n// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem\ntype GCSFsConfig struct {\n\tsdk.BaseGCSFsConfig\n\tCredentials *kms.Secret `json:\"credentials,omitempty\"`\n}\n\n// HideConfidentialData hides confidential data\nfunc (c *GCSFsConfig) HideConfidentialData() {\n\tif c.Credentials != nil {\n\t\tc.Credentials.Hide()\n\t}\n}\n\n// ValidateAndEncryptCredentials validates the configuration and encrypts credentials if they are in plain text\nfunc (c *GCSFsConfig) ValidateAndEncryptCredentials(additionalData string) error {\n\tif err := c.validate(); err != nil {\n\t\tvar errI18n *util.I18nError\n\t\terrValidation := util.NewValidationError(fmt.Sprintf(\"could not validate GCS config: %v\", err))\n\t\tif errors.As(err, &errI18n) {\n\t\t\treturn util.NewI18nError(errValidation, errI18n.Message)\n\t\t}\n\t\treturn util.NewI18nError(errValidation, util.I18nErrorFsValidation)\n\t}\n\tif c.Credentials.IsPlain() {\n\t\tc.Credentials.SetAdditionalData(additionalData)\n\t\terr := c.Credentials.Encrypt()\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt GCS credentials: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *GCSFsConfig) isEqual(other GCSFsConfig) bool {\n\tif c.Bucket != other.Bucket {\n\t\treturn false\n\t}\n\tif c.KeyPrefix != other.KeyPrefix {\n\t\treturn false\n\t}\n\tif c.AutomaticCredentials != other.AutomaticCredentials {\n\t\treturn false\n\t}\n\tif c.StorageClass != other.StorageClass {\n\t\treturn false\n\t}\n\tif c.ACL != other.ACL {\n\t\treturn false\n\t}\n\tif c.UploadPartSize != other.UploadPartSize {\n\t\treturn false\n\t}\n\tif c.UploadPartMaxTime != other.UploadPartMaxTime {\n\t\treturn false\n\t}\n\tif c.Credentials == nil {\n\t\tc.Credentials = kms.NewEmptySecret()\n\t}\n\tif other.Credentials == nil {\n\t\tother.Credentials = kms.NewEmptySecret()\n\t}\n\treturn c.Credentials.IsEqual(other.Credentials)\n}\n\nfunc (c *GCSFsConfig) isSameResource(other GCSFsConfig) bool {\n\treturn c.Bucket == other.Bucket\n}\n\n// validate returns an error if the configuration is not valid\nfunc (c *GCSFsConfig) validate() error { //nolint:gocyclo\n\tif c.Credentials == nil || c.AutomaticCredentials == 1 {\n\t\tc.Credentials = kms.NewEmptySecret()\n\t}\n\tif c.Bucket == \"\" {\n\t\treturn util.NewI18nError(errors.New(\"bucket cannot be empty\"), util.I18nErrorBucketRequired)\n\t}\n\tif c.KeyPrefix != \"\" {\n\t\tif strings.HasPrefix(c.KeyPrefix, \"/\") {\n\t\t\treturn util.NewI18nError(errors.New(\"key_prefix cannot start with /\"), util.I18nErrorKeyPrefixInvalid)\n\t\t}\n\t\tc.KeyPrefix = path.Clean(c.KeyPrefix)\n\t\tif !strings.HasSuffix(c.KeyPrefix, \"/\") {\n\t\t\tc.KeyPrefix += \"/\"\n\t\t}\n\t}\n\tif c.Credentials.IsEncrypted() && !c.Credentials.IsValid() {\n\t\treturn errors.New(\"invalid encrypted credentials\")\n\t}\n\tif c.AutomaticCredentials == 0 && !c.Credentials.IsValidInput() {\n\t\treturn util.NewI18nError(errors.New(\"invalid credentials\"), util.I18nErrorFsCredentialsRequired)\n\t}\n\tc.StorageClass = strings.TrimSpace(c.StorageClass)\n\tc.ACL = strings.TrimSpace(c.ACL)\n\tif c.UploadPartSize < 0 || c.UploadPartSize > 2000 {\n\t\tc.UploadPartSize = 0\n\t}\n\tif c.UploadPartMaxTime < 0 {\n\t\tc.UploadPartMaxTime = 0\n\t}\n\treturn nil\n}\n\n// AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem\ntype AzBlobFsConfig struct {\n\tsdk.BaseAzBlobFsConfig\n\t// Storage Account Key leave blank to use SAS URL.\n\t// The access key is stored encrypted based on the kms configuration\n\tAccountKey *kms.Secret `json:\"account_key,omitempty\"`\n\t// Shared access signature URL, leave blank if using account/key\n\tSASURL *kms.Secret `json:\"sas_url,omitempty\"`\n}\n\n// HideConfidentialData hides confidential data\nfunc (c *AzBlobFsConfig) HideConfidentialData() {\n\tif c.AccountKey != nil {\n\t\tc.AccountKey.Hide()\n\t}\n\tif c.SASURL != nil {\n\t\tc.SASURL.Hide()\n\t}\n}\n\nfunc (c *AzBlobFsConfig) isEqual(other AzBlobFsConfig) bool {\n\tif c.Container != other.Container {\n\t\treturn false\n\t}\n\tif c.AccountName != other.AccountName {\n\t\treturn false\n\t}\n\tif c.Endpoint != other.Endpoint {\n\t\treturn false\n\t}\n\tif c.SASURL.IsEmpty() {\n\t\tc.SASURL = kms.NewEmptySecret()\n\t}\n\tif other.SASURL.IsEmpty() {\n\t\tother.SASURL = kms.NewEmptySecret()\n\t}\n\tif !c.SASURL.IsEqual(other.SASURL) {\n\t\treturn false\n\t}\n\tif c.KeyPrefix != other.KeyPrefix {\n\t\treturn false\n\t}\n\tif c.UploadPartSize != other.UploadPartSize {\n\t\treturn false\n\t}\n\tif c.UploadConcurrency != other.UploadConcurrency {\n\t\treturn false\n\t}\n\tif c.DownloadPartSize != other.DownloadPartSize {\n\t\treturn false\n\t}\n\tif c.DownloadConcurrency != other.DownloadConcurrency {\n\t\treturn false\n\t}\n\tif c.UseEmulator != other.UseEmulator {\n\t\treturn false\n\t}\n\tif c.AccessTier != other.AccessTier {\n\t\treturn false\n\t}\n\treturn c.isSecretEqual(other)\n}\n\nfunc (c *AzBlobFsConfig) isSecretEqual(other AzBlobFsConfig) bool {\n\tif c.AccountKey == nil {\n\t\tc.AccountKey = kms.NewEmptySecret()\n\t}\n\tif other.AccountKey == nil {\n\t\tother.AccountKey = kms.NewEmptySecret()\n\t}\n\treturn c.AccountKey.IsEqual(other.AccountKey)\n}\n\n// ValidateAndEncryptCredentials validates the configuration and  encrypts access secret if it is in plain text\nfunc (c *AzBlobFsConfig) ValidateAndEncryptCredentials(additionalData string) error {\n\tif err := c.validate(); err != nil {\n\t\tvar errI18n *util.I18nError\n\t\terrValidation := util.NewValidationError(fmt.Sprintf(\"could not validate Azure Blob config: %v\", err))\n\t\tif errors.As(err, &errI18n) {\n\t\t\treturn util.NewI18nError(errValidation, errI18n.Message)\n\t\t}\n\t\treturn util.NewI18nError(errValidation, util.I18nErrorFsValidation)\n\t}\n\tif c.AccountKey.IsPlain() {\n\t\tc.AccountKey.SetAdditionalData(additionalData)\n\t\tif err := c.AccountKey.Encrypt(); err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt Azure blob account key: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\tif c.SASURL.IsPlain() {\n\t\tc.SASURL.SetAdditionalData(additionalData)\n\t\tif err := c.SASURL.Encrypt(); err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt Azure blob SAS URL: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *AzBlobFsConfig) checkCredentials() error {\n\tif c.SASURL.IsPlain() {\n\t\t_, err := url.Parse(c.SASURL.GetPayload())\n\t\tif err != nil {\n\t\t\treturn util.NewI18nError(err, util.I18nErrorSASURLInvalid)\n\t\t}\n\t\treturn nil\n\t}\n\tif c.SASURL.IsEncrypted() && !c.SASURL.IsValid() {\n\t\treturn errors.New(\"invalid encrypted sas_url\")\n\t}\n\tif !c.SASURL.IsEmpty() {\n\t\treturn nil\n\t}\n\tif c.AccountName == \"\" {\n\t\treturn util.NewI18nError(errors.New(\"account name is required\"), util.I18nErrorAccountNameRequired)\n\t}\n\tif c.AccountKey.IsEncrypted() && !c.AccountKey.IsValid() {\n\t\treturn errors.New(\"invalid encrypted account_key\")\n\t}\n\treturn nil\n}\n\nfunc (c *AzBlobFsConfig) checkPartSizeAndConcurrency() error {\n\tif c.UploadPartSize < 0 || c.UploadPartSize > 2000 {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"invalid upload part size: %v\", c.UploadPartSize),\n\t\t\tutil.I18nErrorULPartSizeInvalid,\n\t\t)\n\t}\n\tif c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"invalid upload concurrency: %v\", c.UploadConcurrency),\n\t\t\tutil.I18nErrorULConcurrencyInvalid,\n\t\t)\n\t}\n\tif c.DownloadPartSize < 0 || c.DownloadPartSize > 2000 {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"invalid download part size: %v\", c.DownloadPartSize),\n\t\t\tutil.I18nErrorDLPartSizeInvalid,\n\t\t)\n\t}\n\tif c.DownloadConcurrency < 0 || c.DownloadConcurrency > 64 {\n\t\treturn util.NewI18nError(\n\t\t\tfmt.Errorf(\"invalid upload concurrency: %v\", c.DownloadConcurrency),\n\t\t\tutil.I18nErrorDLConcurrencyInvalid,\n\t\t)\n\t}\n\treturn nil\n}\n\nfunc (c *AzBlobFsConfig) tryDecrypt() error {\n\tif err := c.AccountKey.TryDecrypt(); err != nil {\n\t\treturn fmt.Errorf(\"unable to decrypt account key: %w\", err)\n\t}\n\tif err := c.SASURL.TryDecrypt(); err != nil {\n\t\treturn fmt.Errorf(\"unable to decrypt SAS URL: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (c *AzBlobFsConfig) isSameResource(other AzBlobFsConfig) bool {\n\tif c.AccountName != other.AccountName {\n\t\treturn false\n\t}\n\tif c.Endpoint != other.Endpoint {\n\t\treturn false\n\t}\n\tif c.SASURL == nil {\n\t\tc.SASURL = kms.NewEmptySecret()\n\t}\n\tif other.SASURL == nil {\n\t\tother.SASURL = kms.NewEmptySecret()\n\t}\n\treturn c.SASURL.GetPayload() == other.SASURL.GetPayload()\n}\n\n// validate returns an error if the configuration is not valid\nfunc (c *AzBlobFsConfig) validate() error {\n\tif c.AccountKey == nil {\n\t\tc.AccountKey = kms.NewEmptySecret()\n\t}\n\tif c.SASURL == nil {\n\t\tc.SASURL = kms.NewEmptySecret()\n\t}\n\t// container could be embedded within SAS URL we check this at runtime\n\tif c.SASURL.IsEmpty() && c.Container == \"\" {\n\t\treturn util.NewI18nError(errors.New(\"container cannot be empty\"), util.I18nErrorContainerRequired)\n\t}\n\tif err := c.checkCredentials(); err != nil {\n\t\treturn err\n\t}\n\tif c.KeyPrefix != \"\" {\n\t\tif strings.HasPrefix(c.KeyPrefix, \"/\") {\n\t\t\treturn util.NewI18nError(errors.New(\"key_prefix cannot start with /\"), util.I18nErrorKeyPrefixInvalid)\n\t\t}\n\t\tc.KeyPrefix = path.Clean(c.KeyPrefix)\n\t\tif !strings.HasSuffix(c.KeyPrefix, \"/\") {\n\t\t\tc.KeyPrefix += \"/\"\n\t\t}\n\t}\n\tif err := c.checkPartSizeAndConcurrency(); err != nil {\n\t\treturn err\n\t}\n\tif !slices.Contains(validAzAccessTier, c.AccessTier) {\n\t\treturn fmt.Errorf(\"invalid access tier %q, valid values: \\\"''%v\\\"\", c.AccessTier, strings.Join(validAzAccessTier, \", \"))\n\t}\n\treturn nil\n}\n\n// CryptFsConfig defines the configuration to store local files as encrypted\ntype CryptFsConfig struct {\n\tsdk.OSFsConfig\n\tPassphrase *kms.Secret `json:\"passphrase,omitempty\"`\n}\n\n// HideConfidentialData hides confidential data\nfunc (c *CryptFsConfig) HideConfidentialData() {\n\tif c.Passphrase != nil {\n\t\tc.Passphrase.Hide()\n\t}\n}\n\nfunc (c *CryptFsConfig) isEqual(other CryptFsConfig) bool {\n\tif c.Passphrase == nil {\n\t\tc.Passphrase = kms.NewEmptySecret()\n\t}\n\tif other.Passphrase == nil {\n\t\tother.Passphrase = kms.NewEmptySecret()\n\t}\n\treturn c.Passphrase.IsEqual(other.Passphrase)\n}\n\n// ValidateAndEncryptCredentials validates the configuration and encrypts the passphrase if it is in plain text\nfunc (c *CryptFsConfig) ValidateAndEncryptCredentials(additionalData string) error {\n\tif err := c.validate(); err != nil {\n\t\tvar errI18n *util.I18nError\n\t\terrValidation := util.NewValidationError(fmt.Sprintf(\"could not validate crypt fs config: %v\", err))\n\t\tif errors.As(err, &errI18n) {\n\t\t\treturn util.NewI18nError(errValidation, errI18n.Message)\n\t\t}\n\t\treturn util.NewI18nError(errValidation, util.I18nErrorFsValidation)\n\t}\n\tif c.Passphrase.IsPlain() {\n\t\tc.Passphrase.SetAdditionalData(additionalData)\n\t\tif err := c.Passphrase.Encrypt(); err != nil {\n\t\t\treturn util.NewI18nError(\n\t\t\t\tutil.NewValidationError(fmt.Sprintf(\"could not encrypt Crypt fs passphrase: %v\", err)),\n\t\t\t\tutil.I18nErrorFsValidation,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *CryptFsConfig) isSameResource(other CryptFsConfig) bool {\n\treturn c.Passphrase.GetPayload() == other.Passphrase.GetPayload()\n}\n\n// validate returns an error if the configuration is not valid\nfunc (c *CryptFsConfig) validate() error {\n\tif c.Passphrase == nil || c.Passphrase.IsEmpty() {\n\t\treturn util.NewI18nError(errors.New(\"invalid passphrase\"), util.I18nErrorPassphraseRequired)\n\t}\n\tif !c.Passphrase.IsValidInput() {\n\t\treturn util.NewI18nError(errors.New(\"passphrase cannot be empty or invalid\"), util.I18nErrorPassphraseRequired)\n\t}\n\tif c.Passphrase.IsEncrypted() && !c.Passphrase.IsValid() {\n\t\treturn errors.New(\"invalid encrypted passphrase\")\n\t}\n\treturn nil\n}\n\n// pipeWriter defines a wrapper for a pipeWriterAt.\ntype pipeWriter struct {\n\tpipeWriterAt\n\terr  error\n\tdone chan bool\n}\n\n// NewPipeWriter initializes a new PipeWriter\nfunc NewPipeWriter(w pipeWriterAt) PipeWriter {\n\treturn &pipeWriter{\n\t\tpipeWriterAt: w,\n\t\terr:          nil,\n\t\tdone:         make(chan bool),\n\t}\n}\n\n// Close waits for the upload to end, closes the pipeWriterAt and returns an error if any.\nfunc (p *pipeWriter) Close() error {\n\tp.pipeWriterAt.Close() //nolint:errcheck // the returned error is always null\n\t<-p.done\n\treturn p.err\n}\n\n// Done unlocks other goroutines waiting on Close().\n// It must be called when the upload ends\nfunc (p *pipeWriter) Done(err error) {\n\tp.err = err\n\tp.done <- true\n}\n\nfunc newPipeWriterAtOffset(w pipeWriterAt, offset int64) PipeWriter {\n\treturn &pipeWriterAtOffset{\n\t\tpipeWriter: &pipeWriter{\n\t\t\tpipeWriterAt: w,\n\t\t\terr:          nil,\n\t\t\tdone:         make(chan bool),\n\t\t},\n\t\toffset:      offset,\n\t\twriteOffset: offset,\n\t}\n}\n\ntype pipeWriterAtOffset struct {\n\t*pipeWriter\n\toffset      int64\n\twriteOffset int64\n}\n\nfunc (p *pipeWriterAtOffset) WriteAt(buf []byte, off int64) (int, error) {\n\tif off < p.offset {\n\t\treturn 0, fmt.Errorf(\"invalid offset %d, minimum accepted %d\", off, p.offset)\n\t}\n\treturn p.pipeWriter.WriteAt(buf, off-p.offset)\n}\n\nfunc (p *pipeWriterAtOffset) Write(buf []byte) (int, error) {\n\tn, err := p.WriteAt(buf, p.writeOffset)\n\tp.writeOffset += int64(n)\n\treturn n, err\n}\n\n// NewPipeReader initializes a new PipeReader\nfunc NewPipeReader(r pipeReaderAt) PipeReader {\n\treturn &pipeReader{\n\t\tpipeReaderAt: r,\n\t}\n}\n\n// pipeReader defines a wrapper for pipeat.PipeReaderAt.\ntype pipeReader struct {\n\tpipeReaderAt\n\tmu       sync.RWMutex\n\tmetadata map[string]string\n}\n\nfunc (p *pipeReader) setMetadata(value map[string]string) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tp.metadata = value\n}\n\nfunc (p *pipeReader) setMetadataFromPointerVal(value map[string]*string) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tif len(value) == 0 {\n\t\tp.metadata = nil\n\t\treturn\n\t}\n\n\tp.metadata = map[string]string{}\n\tfor k, v := range value {\n\t\tval := util.GetStringFromPointer(v)\n\t\tif val != \"\" {\n\t\t\tp.metadata[k] = val\n\t\t}\n\t}\n}\n\n// Metadata implements the Metadater interface\nfunc (p *pipeReader) Metadata() map[string]string {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\n\tif len(p.metadata) == 0 {\n\t\treturn nil\n\t}\n\tresult := make(map[string]string)\n\tfor k, v := range p.metadata {\n\t\tresult[k] = v\n\t}\n\treturn result\n}\n\nfunc isEqualityCheckModeValid(mode int) bool {\n\treturn mode >= 0 || mode <= 1\n}\n\n// isDirectory checks if a path exists and is a directory\nfunc isDirectory(fs Fs, path string) (bool, error) {\n\tfileInfo, err := fs.Stat(path)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn fileInfo.IsDir(), err\n}\n\n// IsLocalOsFs returns true if fs is a local filesystem implementation\nfunc IsLocalOsFs(fs Fs) bool {\n\treturn fs.Name() == osFsName\n}\n\n// IsCryptOsFs returns true if fs is an encrypted local filesystem implementation\nfunc IsCryptOsFs(fs Fs) bool {\n\treturn fs.Name() == cryptFsName\n}\n\n// IsSFTPFs returns true if fs is an SFTP filesystem\nfunc IsSFTPFs(fs Fs) bool {\n\treturn strings.HasPrefix(fs.Name(), sftpFsName)\n}\n\n// IsHTTPFs returns true if fs is an HTTP filesystem\nfunc IsHTTPFs(fs Fs) bool {\n\treturn strings.HasPrefix(fs.Name(), httpFsName)\n}\n\n// IsBufferedLocalOrSFTPFs returns true if this is a buffered SFTP or local filesystem\nfunc IsBufferedLocalOrSFTPFs(fs Fs) bool {\n\tif osFs, ok := fs.(*OsFs); ok {\n\t\treturn osFs.writeBufferSize > 0\n\t}\n\tif !IsSFTPFs(fs) {\n\t\treturn false\n\t}\n\treturn !fs.IsUploadResumeSupported()\n}\n\n// FsOpenReturnsFile returns true if fs.Open returns a *os.File handle\nfunc FsOpenReturnsFile(fs Fs) bool {\n\tif osFs, ok := fs.(*OsFs); ok {\n\t\treturn osFs.readBufferSize == 0\n\t}\n\tif sftpFs, ok := fs.(*SFTPFs); ok {\n\t\treturn sftpFs.config.BufferSize == 0\n\t}\n\treturn false\n}\n\n// IsLocalOrSFTPFs returns true if fs is local or SFTP\nfunc IsLocalOrSFTPFs(fs Fs) bool {\n\treturn IsLocalOsFs(fs) || IsSFTPFs(fs)\n}\n\n// HasTruncateSupport returns true if the fs supports truncate files\nfunc HasTruncateSupport(fs Fs) bool {\n\treturn IsLocalOsFs(fs) || IsSFTPFs(fs) || IsHTTPFs(fs)\n}\n\n// IsRenameAtomic returns true if renaming a directory is supposed to be atomic\nfunc IsRenameAtomic(fs Fs) bool {\n\tif strings.HasPrefix(fs.Name(), s3fsName) {\n\t\treturn false\n\t}\n\tif strings.HasPrefix(fs.Name(), gcsfsName) {\n\t\treturn false\n\t}\n\tif strings.HasPrefix(fs.Name(), azBlobFsName) {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// HasImplicitAtomicUploads returns true if the fs don't persists partial files on error\nfunc HasImplicitAtomicUploads(fs Fs) bool {\n\tif strings.HasPrefix(fs.Name(), s3fsName) {\n\t\treturn uploadMode&4 == 0\n\t}\n\tif strings.HasPrefix(fs.Name(), gcsfsName) {\n\t\treturn uploadMode&8 == 0\n\t}\n\tif strings.HasPrefix(fs.Name(), azBlobFsName) {\n\t\treturn uploadMode&16 == 0\n\t}\n\treturn false\n}\n\n// HasOpenRWSupport returns true if the fs can open a file\n// for reading and writing at the same time\nfunc HasOpenRWSupport(fs Fs) bool {\n\tif IsLocalOsFs(fs) {\n\t\treturn true\n\t}\n\tif IsSFTPFs(fs) && fs.IsUploadResumeSupported() {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// IsLocalOrCryptoFs returns true if fs is local or local encrypted\nfunc IsLocalOrCryptoFs(fs Fs) bool {\n\treturn IsLocalOsFs(fs) || IsCryptOsFs(fs)\n}\n\n// SetPathPermissions calls fs.Chown.\n// It does nothing for local filesystem on windows\nfunc SetPathPermissions(fs Fs, path string, uid int, gid int) {\n\tif uid == -1 && gid == -1 {\n\t\treturn\n\t}\n\tif IsLocalOsFs(fs) {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\treturn\n\t\t}\n\t}\n\tif err := fs.Chown(path, uid, gid); err != nil {\n\t\tfsLog(fs, logger.LevelWarn, \"error chowning path %v: %v\", path, err)\n\t}\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported\nfunc IsUploadResumeSupported(fs Fs, size int64) bool {\n\tif fs.IsUploadResumeSupported() {\n\t\treturn true\n\t}\n\treturn fs.IsConditionalUploadResumeSupported(size)\n}\n\nfunc getLastModified(metadata map[string]string) int64 {\n\tif val, ok := metadata[lastModifiedField]; ok && val != \"\" {\n\t\tlastModified, err := strconv.ParseInt(val, 10, 64)\n\t\tif err == nil {\n\t\t\treturn lastModified\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc getAzureLastModified(metadata map[string]*string) int64 {\n\tfor k, v := range metadata {\n\t\tif strings.EqualFold(k, lastModifiedField) {\n\t\t\tif val := util.GetStringFromPointer(v); val != \"\" {\n\t\t\t\tlastModified, err := strconv.ParseInt(val, 10, 64)\n\t\t\t\tif err == nil {\n\t\t\t\t\treturn lastModified\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 0\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc validateOSFsConfig(config *sdk.OSFsConfig) error {\n\tif config.ReadBufferSize < 0 || config.ReadBufferSize > 10 {\n\t\treturn fmt.Errorf(\"invalid read buffer size must be between 0 and 10 MB\")\n\t}\n\tif config.WriteBufferSize < 0 || config.WriteBufferSize > 10 {\n\t\treturn fmt.Errorf(\"invalid write buffer size must be between 0 and 10 MB\")\n\t}\n\treturn nil\n}\n\nfunc doCopy(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) {\n\tif buf == nil {\n\t\tbuf = make([]byte, 32768)\n\t}\n\tfor {\n\t\tnr, er := src.Read(buf)\n\t\tif nr > 0 {\n\t\t\tnw, ew := dst.Write(buf[0:nr])\n\t\t\tif nw < 0 || nr < nw {\n\t\t\t\tnw = 0\n\t\t\t\tif ew == nil {\n\t\t\t\t\tew = errors.New(\"invalid write\")\n\t\t\t\t}\n\t\t\t}\n\t\t\twritten += int64(nw)\n\t\t\tif ew != nil {\n\t\t\t\terr = ew\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif nr != nw {\n\t\t\t\terr = io.ErrShortWrite\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif er != nil {\n\t\t\tif er != io.EOF {\n\t\t\t\terr = er\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\treturn written, err\n}\n\nfunc getMountPath(mountPath string) string {\n\tif mountPath == \"/\" {\n\t\treturn \"\"\n\t}\n\treturn mountPath\n}\n\nfunc getLocalTempDir() string {\n\tif tempPath != \"\" {\n\t\treturn tempPath\n\t}\n\treturn filepath.Clean(os.TempDir())\n}\n\nfunc doRecursiveRename(fs Fs, source, target string,\n\trenameFn func(string, string, os.FileInfo, int, bool) (int, int64, error),\n\trecursion int, updateModTime bool,\n) (int, int64, error) {\n\tvar numFiles int\n\tvar filesSize int64\n\n\tif recursion > util.MaxRecursion {\n\t\treturn numFiles, filesSize, util.ErrRecursionTooDeep\n\t}\n\trecursion++\n\n\tlister, err := fs.ReadDir(source)\n\tif err != nil {\n\t\treturn numFiles, filesSize, err\n\t}\n\tdefer lister.Close()\n\n\tfor {\n\t\tentries, err := lister.Next(ListerBatchSize)\n\t\tfinished := errors.Is(err, io.EOF)\n\t\tif err != nil && !finished {\n\t\t\treturn numFiles, filesSize, err\n\t\t}\n\t\tfor _, info := range entries {\n\t\t\tsourceEntry := fs.Join(source, info.Name())\n\t\t\ttargetEntry := fs.Join(target, info.Name())\n\t\t\tfiles, size, err := renameFn(sourceEntry, targetEntry, info, recursion, updateModTime)\n\t\t\tif err != nil {\n\t\t\t\tif fs.IsNotExist(err) {\n\t\t\t\t\tfsLog(fs, logger.LevelInfo, \"skipping rename for %q: %v\", sourceEntry, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn numFiles, filesSize, err\n\t\t\t}\n\t\t\tnumFiles += files\n\t\t\tfilesSize += size\n\t\t}\n\t\tif finished {\n\t\t\treturn numFiles, filesSize, nil\n\t\t}\n\t}\n}\n\n// copied from rclone\nfunc readFill(r io.Reader, buf []byte) (n int, err error) {\n\tvar nn int\n\tfor n < len(buf) && err == nil {\n\t\tnn, err = r.Read(buf[n:])\n\t\tn += nn\n\t}\n\treturn n, err\n}\n\nfunc writeAtFull(w io.WriterAt, buf []byte, offset int64, count int) error {\n\twritten := 0\n\tfor written < count {\n\t\tn, err := w.WriteAt(buf[written:count], offset+int64(written))\n\t\twritten += n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\ntype bytesReaderWrapper struct {\n\t*bytes.Reader\n}\n\nfunc (b *bytesReaderWrapper) Close() error {\n\treturn nil\n}\n\ntype bufferAllocator struct {\n\tsync.Mutex\n\tavailable  [][]byte\n\tbufferSize int\n\tfinalized  bool\n}\n\nfunc newBufferAllocator(size int) *bufferAllocator {\n\treturn &bufferAllocator{\n\t\tbufferSize: size,\n\t\tfinalized:  false,\n\t}\n}\n\nfunc (b *bufferAllocator) getBuffer() []byte {\n\tb.Lock()\n\tdefer b.Unlock()\n\n\tif len(b.available) > 0 {\n\t\tvar result []byte\n\n\t\ttruncLength := len(b.available) - 1\n\t\tresult = b.available[truncLength]\n\n\t\tb.available[truncLength] = nil\n\t\tb.available = b.available[:truncLength]\n\n\t\treturn result\n\t}\n\n\treturn make([]byte, b.bufferSize)\n}\n\nfunc (b *bufferAllocator) releaseBuffer(buf []byte) {\n\tb.Lock()\n\tdefer b.Unlock()\n\n\tif b.finalized || len(buf) != b.bufferSize {\n\t\treturn\n\t}\n\n\tb.available = append(b.available, buf)\n}\n\nfunc (b *bufferAllocator) free() {\n\tb.Lock()\n\tdefer b.Unlock()\n\n\tb.available = nil\n\tb.finalized = true\n}\n\nfunc fsLog(fs Fs, level logger.LogLevel, format string, v ...any) {\n\tlogger.Log(level, fs.Name(), fs.ConnectionID(), format, v...)\n}\n"
  },
  {
    "path": "internal/webdavd/file.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage webdavd\n\nimport (\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"io\"\n\t\"mime\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/drakkan/webdav\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nvar (\n\terrTransferAborted = errors.New(\"transfer aborted\")\n\tlastModifiedProps  = []string{\"Win32LastModifiedTime\", \"getlastmodified\"}\n)\n\ntype webDavFile struct {\n\t*common.BaseTransfer\n\twriter      io.WriteCloser\n\treader      io.ReadCloser\n\tinfo        os.FileInfo\n\tstartOffset int64\n\tisFinished  bool\n\treadTried   atomic.Bool\n}\n\nfunc newWebDavFile(baseTransfer *common.BaseTransfer, pipeWriter vfs.PipeWriter, pipeReader vfs.PipeReader) *webDavFile {\n\tvar writer io.WriteCloser\n\tvar reader io.ReadCloser\n\tif baseTransfer.File != nil {\n\t\twriter = baseTransfer.File\n\t\treader = baseTransfer.File\n\t} else if pipeWriter != nil {\n\t\twriter = pipeWriter\n\t} else if pipeReader != nil {\n\t\treader = pipeReader\n\t}\n\tf := &webDavFile{\n\t\tBaseTransfer: baseTransfer,\n\t\twriter:       writer,\n\t\treader:       reader,\n\t\tisFinished:   false,\n\t\tstartOffset:  0,\n\t\tinfo:         nil,\n\t}\n\tf.readTried.Store(false)\n\treturn f\n}\n\ntype webDavFileInfo struct {\n\tos.FileInfo\n\tFs          vfs.Fs\n\tvirtualPath string\n\tfsPath      string\n}\n\n// ContentType implements webdav.ContentTyper interface\nfunc (fi *webDavFileInfo) ContentType(_ context.Context) (string, error) {\n\textension := path.Ext(fi.virtualPath)\n\tif ctype, ok := customMimeTypeMapping[extension]; ok {\n\t\treturn ctype, nil\n\t}\n\tif extension == \"\" || extension == \".dat\" {\n\t\treturn \"application/octet-stream\", nil\n\t}\n\tcontentType := mime.TypeByExtension(extension)\n\tif contentType != \"\" {\n\t\treturn contentType, nil\n\t}\n\tcontentType = mimeTypeCache.getMimeFromCache(extension)\n\tif contentType != \"\" {\n\t\treturn contentType, nil\n\t}\n\tcontentType, err := fi.Fs.GetMimeType(fi.fsPath)\n\tif contentType != \"\" {\n\t\tmimeTypeCache.addMimeToCache(extension, contentType)\n\t\treturn contentType, err\n\t}\n\treturn \"\", webdav.ErrNotImplemented\n}\n\n// Readdir reads directory entries from the handle\nfunc (f *webDavFile) Readdir(_ int) ([]os.FileInfo, error) {\n\treturn nil, webdav.ErrNotImplemented\n}\n\n// ReadDir implements the FileDirLister interface\nfunc (f *webDavFile) ReadDir() (webdav.DirLister, error) {\n\tif !f.Connection.User.HasPerm(dataprovider.PermListItems, f.GetVirtualPath()) {\n\t\treturn nil, f.Connection.GetPermissionDeniedError()\n\t}\n\tlister, err := f.Connection.ListDir(f.GetVirtualPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &webDavDirLister{\n\t\tDirLister:      lister,\n\t\tfs:             f.Fs,\n\t\tvirtualDirPath: f.GetVirtualPath(),\n\t\tfsDirPath:      f.GetFsPath(),\n\t}, nil\n}\n\n// Stat the handle\nfunc (f *webDavFile) Stat() (os.FileInfo, error) {\n\tif f.GetType() == common.TransferDownload && !f.Connection.User.HasPerm(dataprovider.PermListItems, path.Dir(f.GetVirtualPath())) {\n\t\treturn nil, f.Connection.GetPermissionDeniedError()\n\t}\n\tf.Lock()\n\terrUpload := f.ErrTransfer\n\tf.Unlock()\n\tif f.GetType() == common.TransferUpload && errUpload == nil {\n\t\tinfo := &webDavFileInfo{\n\t\t\tFileInfo:    vfs.NewFileInfo(f.GetFsPath(), false, f.BytesReceived.Load(), time.Now(), false),\n\t\t\tFs:          f.Fs,\n\t\t\tvirtualPath: f.GetVirtualPath(),\n\t\t\tfsPath:      f.GetFsPath(),\n\t\t}\n\t\treturn info, nil\n\t}\n\tinfo, err := f.Fs.Stat(f.GetFsPath())\n\tif err != nil {\n\t\treturn nil, f.Connection.GetFsError(f.Fs, err)\n\t}\n\tif vfs.IsCryptOsFs(f.Fs) {\n\t\tinfo = f.Fs.(*vfs.CryptFs).ConvertFileInfo(info)\n\t}\n\tfi := &webDavFileInfo{\n\t\tFileInfo:    info,\n\t\tFs:          f.Fs,\n\t\tvirtualPath: f.GetVirtualPath(),\n\t\tfsPath:      f.GetFsPath(),\n\t}\n\treturn fi, nil\n}\n\nfunc (f *webDavFile) checkFirstRead() error {\n\tif !f.Connection.User.HasPerm(dataprovider.PermDownload, path.Dir(f.GetVirtualPath())) {\n\t\treturn f.Connection.GetPermissionDeniedError()\n\t}\n\ttransferQuota := f.GetTransferQuota()\n\tif !transferQuota.HasDownloadSpace() {\n\t\tf.Connection.Log(logger.LevelInfo, \"denying file read due to quota limits\")\n\t\treturn f.Connection.GetReadQuotaExceededError()\n\t}\n\tif ok, policy := f.Connection.User.IsFileAllowed(f.GetVirtualPath()); !ok {\n\t\tf.Connection.Log(logger.LevelWarn, \"reading file %q is not allowed\", f.GetVirtualPath())\n\t\treturn f.Connection.GetErrorForDeniedFile(policy)\n\t}\n\t_, err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)\n\tif err != nil {\n\t\tf.Connection.Log(logger.LevelDebug, \"download for file %q denied by pre action: %v\", f.GetVirtualPath(), err)\n\t\treturn f.Connection.GetPermissionDeniedError()\n\t}\n\tf.readTried.Store(true)\n\treturn nil\n}\n\n// Read reads the contents to downloads.\nfunc (f *webDavFile) Read(p []byte) (n int, err error) {\n\tif f.AbortTransfer.Load() {\n\t\treturn 0, errTransferAborted\n\t}\n\tif !f.readTried.Load() {\n\t\tif err := f.checkFirstRead(); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\tf.Connection.UpdateLastActivity()\n\n\t// the file is read sequentially we don't need to check for concurrent reads and so\n\t// lock the transfer while opening the remote file\n\tif f.reader == nil {\n\t\tif f.GetType() != common.TransferDownload {\n\t\t\tf.TransferError(common.ErrOpUnsupported)\n\t\t\treturn 0, common.ErrOpUnsupported\n\t\t}\n\t\tfile, r, cancelFn, e := f.Fs.Open(f.GetFsPath(), 0)\n\t\tf.Lock()\n\t\tif e == nil {\n\t\t\tif file != nil {\n\t\t\t\tf.File = file\n\t\t\t\tf.writer = f.File\n\t\t\t\tf.reader = f.File\n\t\t\t} else if r != nil {\n\t\t\t\tf.reader = r\n\t\t\t}\n\t\t\tf.SetCancelFn(cancelFn)\n\t\t}\n\t\tf.ErrTransfer = e\n\t\tf.startOffset = 0\n\t\tf.Unlock()\n\t\tif e != nil {\n\t\t\treturn 0, f.Connection.GetFsError(f.Fs, e)\n\t\t}\n\t}\n\n\tn, err = f.reader.Read(p)\n\tf.BytesSent.Add(int64(n))\n\tif err == nil {\n\t\terr = f.CheckRead()\n\t}\n\tif err != nil && err != io.EOF {\n\t\tf.TransferError(err)\n\t\terr = f.ConvertError(err)\n\t\treturn\n\t}\n\tf.HandleThrottle()\n\treturn\n}\n\n// Write writes the uploaded contents.\nfunc (f *webDavFile) Write(p []byte) (n int, err error) {\n\tif f.AbortTransfer.Load() {\n\t\treturn 0, errTransferAborted\n\t}\n\n\tf.Connection.UpdateLastActivity()\n\n\tn, err = f.writer.Write(p)\n\tf.BytesReceived.Add(int64(n))\n\n\tif err == nil {\n\t\terr = f.CheckWrite()\n\t}\n\tif err != nil {\n\t\tf.TransferError(err)\n\t\terr = f.ConvertError(err)\n\t\treturn\n\t}\n\tf.HandleThrottle()\n\treturn\n}\n\nfunc (f *webDavFile) updateStatInfo() error {\n\tif f.info != nil {\n\t\treturn nil\n\t}\n\tinfo, err := f.Fs.Stat(f.GetFsPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif vfs.IsCryptOsFs(f.Fs) {\n\t\tinfo = f.Fs.(*vfs.CryptFs).ConvertFileInfo(info)\n\t}\n\tf.info = info\n\treturn nil\n}\n\nfunc (f *webDavFile) updateTransferQuotaOnSeek() {\n\ttransferQuota := f.GetTransferQuota()\n\tif transferQuota.HasSizeLimits() {\n\t\tgo func(ulSize, dlSize int64, user dataprovider.User) {\n\t\t\tdataprovider.UpdateUserTransferQuota(&user, ulSize, dlSize, false) //nolint:errcheck\n\t\t}(f.BytesReceived.Load(), f.BytesSent.Load(), f.Connection.User)\n\t}\n}\n\nfunc (f *webDavFile) checkFile() error {\n\tif f.File == nil && vfs.FsOpenReturnsFile(f.Fs) {\n\t\tfile, _, _, err := f.Fs.Open(f.GetFsPath(), 0)\n\t\tif err != nil {\n\t\t\tf.Connection.Log(logger.LevelWarn, \"could not open file %q for seeking: %v\",\n\t\t\t\tf.GetFsPath(), err)\n\t\t\tf.TransferError(err)\n\t\t\treturn err\n\t\t}\n\t\tf.File = file\n\t\tf.reader = file\n\t\tf.writer = file\n\t}\n\treturn nil\n}\n\nfunc (f *webDavFile) seekFile(offset int64, whence int) (int64, error) {\n\tret, err := f.File.Seek(offset, whence)\n\tif err != nil {\n\t\tf.TransferError(err)\n\t}\n\treturn ret, err\n}\n\n// Seek sets the offset for the next Read or Write on the writer to offset,\n// interpreted according to whence: 0 means relative to the origin of the file,\n// 1 means relative to the current offset, and 2 means relative to the end.\n// It returns the new offset and an error, if any.\nfunc (f *webDavFile) Seek(offset int64, whence int) (int64, error) {\n\tf.Connection.UpdateLastActivity()\n\tif err := f.checkFile(); err != nil {\n\t\treturn 0, err\n\t}\n\tif f.File != nil {\n\t\treturn f.seekFile(offset, whence)\n\t}\n\tif f.GetType() == common.TransferDownload {\n\t\treadOffset := f.startOffset + f.BytesSent.Load()\n\t\tif offset == 0 && readOffset == 0 {\n\t\t\tswitch whence {\n\t\t\tcase io.SeekStart:\n\t\t\t\treturn 0, nil\n\t\t\tcase io.SeekEnd:\n\t\t\t\tif err := f.updateStatInfo(); err != nil {\n\t\t\t\t\treturn 0, err\n\t\t\t\t}\n\t\t\t\treturn f.info.Size(), nil\n\t\t\t}\n\t\t}\n\n\t\t// close the reader and create a new one at startByte\n\t\tif f.reader != nil {\n\t\t\tf.reader.Close() //nolint:errcheck\n\t\t\tf.reader = nil\n\t\t}\n\t\tstartByte := int64(0)\n\t\tf.BytesReceived.Store(0)\n\t\tf.BytesSent.Store(0)\n\t\tf.updateTransferQuotaOnSeek()\n\n\t\tswitch whence {\n\t\tcase io.SeekStart:\n\t\t\tstartByte = offset\n\t\tcase io.SeekCurrent:\n\t\t\tstartByte = readOffset + offset\n\t\tcase io.SeekEnd:\n\t\t\tif err := f.updateStatInfo(); err != nil {\n\t\t\t\tf.TransferError(err)\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tstartByte = f.info.Size() - offset\n\t\t}\n\n\t\t_, r, cancelFn, err := f.Fs.Open(f.GetFsPath(), startByte)\n\n\t\tf.Lock()\n\t\tif err == nil {\n\t\t\tf.startOffset = startByte\n\t\t\tf.reader = r\n\t\t}\n\t\tf.ErrTransfer = err\n\t\tf.SetCancelFn(cancelFn)\n\t\tf.Unlock()\n\n\t\treturn startByte, err\n\t}\n\treturn 0, common.ErrOpUnsupported\n}\n\n// Close closes the open directory or the current transfer\nfunc (f *webDavFile) Close() error {\n\tif err := f.setFinished(); err != nil {\n\t\treturn err\n\t}\n\terr := f.closeIO()\n\tif f.isTransfer() {\n\t\terrBaseClose := f.BaseTransfer.Close()\n\t\tif errBaseClose != nil {\n\t\t\terr = errBaseClose\n\t\t}\n\t} else {\n\t\tf.Connection.RemoveTransfer(f.BaseTransfer)\n\t}\n\treturn f.Connection.GetFsError(f.Fs, err)\n}\n\nfunc (f *webDavFile) closeIO() error {\n\tvar err error\n\tif f.File != nil {\n\t\terr = f.File.Close()\n\t} else if f.writer != nil {\n\t\terr = f.writer.Close()\n\t\tf.Lock()\n\t\t// we set ErrTransfer here so quota is not updated, in this case the uploads are atomic\n\t\tif err != nil && f.ErrTransfer == nil {\n\t\t\tf.ErrTransfer = err\n\t\t}\n\t\tf.Unlock()\n\t} else if f.reader != nil {\n\t\terr = f.reader.Close()\n\t\tif metadater, ok := f.reader.(vfs.Metadater); ok {\n\t\t\tf.SetMetadata(metadater.Metadata())\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (f *webDavFile) setFinished() error {\n\tf.Lock()\n\tdefer f.Unlock()\n\n\tif f.isFinished {\n\t\treturn common.ErrTransferClosed\n\t}\n\tf.isFinished = true\n\treturn nil\n}\n\nfunc (f *webDavFile) isTransfer() bool {\n\tif f.GetType() == common.TransferDownload {\n\t\treturn f.readTried.Load()\n\t}\n\treturn true\n}\n\n// DeadProps returns a copy of the dead properties held.\n// We always return nil for now, we only support the last modification time\n// and it is already included in \"live\" properties\nfunc (f *webDavFile) DeadProps() (map[xml.Name]webdav.Property, error) {\n\treturn nil, nil\n}\n\n// Patch patches the dead properties held.\n// In our minimal implementation we just support Win32LastModifiedTime and\n// getlastmodified to set the the modification time.\n// We ignore any other property and just return an OK response if the patch sets\n// the modification time, otherwise a Forbidden response\nfunc (f *webDavFile) Patch(patches []webdav.Proppatch) ([]webdav.Propstat, error) {\n\tresp := make([]webdav.Propstat, 0, len(patches))\n\thasError := false\n\tfor _, patch := range patches {\n\t\tstatus := http.StatusForbidden\n\t\tpstat := webdav.Propstat{}\n\t\tfor _, p := range patch.Props {\n\t\t\tif status == http.StatusForbidden && !hasError {\n\t\t\t\tif !patch.Remove && slices.Contains(lastModifiedProps, p.XMLName.Local) {\n\t\t\t\t\tparsed, err := parseTime(util.BytesToString(p.InnerXML))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tf.Connection.Log(logger.LevelWarn, \"unsupported last modification time: %q, err: %v\",\n\t\t\t\t\t\t\tp.InnerXML, err)\n\t\t\t\t\t\thasError = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tattrs := &common.StatAttributes{\n\t\t\t\t\t\tFlags: common.StatAttrTimes,\n\t\t\t\t\t\tAtime: parsed,\n\t\t\t\t\t\tMtime: parsed,\n\t\t\t\t\t}\n\t\t\t\t\tif err := f.Connection.SetStat(f.GetVirtualPath(), attrs); err != nil {\n\t\t\t\t\t\tf.Connection.Log(logger.LevelWarn, \"unable to set modification time for %q, err :%v\",\n\t\t\t\t\t\t\tf.GetVirtualPath(), err)\n\t\t\t\t\t\thasError = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tstatus = http.StatusOK\n\t\t\t\t}\n\t\t\t}\n\t\t\tpstat.Props = append(pstat.Props, webdav.Property{XMLName: p.XMLName})\n\t\t}\n\t\tpstat.Status = status\n\t\tresp = append(resp, pstat)\n\t}\n\treturn resp, nil\n}\n\ntype webDavDirLister struct {\n\tvfs.DirLister\n\tfs             vfs.Fs\n\tvirtualDirPath string\n\tfsDirPath      string\n}\n\nfunc (l *webDavDirLister) Next(limit int) ([]os.FileInfo, error) {\n\tfiles, err := l.DirLister.Next(limit)\n\tfor idx := range files {\n\t\tinfo := files[idx]\n\t\tfiles[idx] = &webDavFileInfo{\n\t\t\tFileInfo:    info,\n\t\t\tFs:          l.fs,\n\t\t\tvirtualPath: path.Join(l.virtualDirPath, info.Name()),\n\t\t\tfsPath:      l.fs.Join(l.fsDirPath, info.Name()),\n\t\t}\n\t}\n\treturn files, err\n}\n"
  },
  {
    "path": "internal/webdavd/handler.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage webdavd\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/drakkan/webdav\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\n// Connection details for a WebDav connection.\ntype Connection struct {\n\t*common.BaseConnection\n\trequest *http.Request\n\trc      *http.ResponseController\n}\n\nfunc newConnection(conn *common.BaseConnection, w http.ResponseWriter, r *http.Request) *Connection {\n\trc := http.NewResponseController(w)\n\tresponseControllerDeadlines(rc, time.Time{}, time.Time{})\n\treturn &Connection{\n\t\tBaseConnection: conn,\n\t\trequest:        r,\n\t\trc:             rc,\n\t}\n}\n\nfunc (c *Connection) getModificationTime() time.Time {\n\tif c.request == nil {\n\t\treturn time.Time{}\n\t}\n\tif val := c.request.Header.Get(\"X-OC-Mtime\"); val != \"\" {\n\t\tif unixTime, err := strconv.ParseInt(val, 10, 64); err == nil {\n\t\t\treturn time.Unix(unixTime, 0)\n\t\t}\n\t}\n\treturn time.Time{}\n}\n\n// GetClientVersion returns the connected client's version.\nfunc (c *Connection) GetClientVersion() string {\n\tif c.request != nil {\n\t\treturn c.request.UserAgent()\n\t}\n\treturn \"\"\n}\n\n// GetLocalAddress returns local connection address\nfunc (c *Connection) GetLocalAddress() string {\n\treturn util.GetHTTPLocalAddress(c.request)\n}\n\n// GetRemoteAddress returns the connected client's address\nfunc (c *Connection) GetRemoteAddress() string {\n\tif c.request != nil {\n\t\treturn c.request.RemoteAddr\n\t}\n\treturn \"\"\n}\n\n// Disconnect closes the active transfer\nfunc (c *Connection) Disconnect() error {\n\tif c.rc != nil {\n\t\tresponseControllerDeadlines(c.rc, time.Now().Add(5*time.Second), time.Now().Add(5*time.Second))\n\t}\n\treturn c.SignalTransfersAbort()\n}\n\n// GetCommand returns the request method\nfunc (c *Connection) GetCommand() string {\n\tif c.request != nil {\n\t\treturn strings.ToUpper(c.request.Method)\n\t}\n\treturn \"\"\n}\n\n// Mkdir creates a directory using the connection filesystem\nfunc (c *Connection) Mkdir(_ context.Context, name string, _ os.FileMode) error {\n\tc.UpdateLastActivity()\n\n\tname = util.CleanPath(name)\n\treturn c.CreateDir(name, true)\n}\n\n// Rename renames a file or a directory\nfunc (c *Connection) Rename(_ context.Context, oldName, newName string) error {\n\tc.UpdateLastActivity()\n\n\toldName = util.CleanPath(oldName)\n\tnewName = util.CleanPath(newName)\n\n\terr := c.BaseConnection.Rename(oldName, newName)\n\tif err == nil {\n\t\tif mtime := c.getModificationTime(); !mtime.IsZero() {\n\t\t\tattrs := &common.StatAttributes{\n\t\t\t\tFlags: common.StatAttrTimes,\n\t\t\t\tAtime: mtime,\n\t\t\t\tMtime: mtime,\n\t\t\t}\n\t\t\tsetStatErr := c.SetStat(newName, attrs)\n\t\t\tc.Log(logger.LevelDebug, \"mtime header found for %q, value: %s, err: %v\", newName, mtime, setStatErr)\n\t\t}\n\t}\n\treturn err\n}\n\n// Stat returns a FileInfo describing the named file/directory, or an error,\n// if any happens\nfunc (c *Connection) Stat(_ context.Context, name string) (os.FileInfo, error) {\n\tc.UpdateLastActivity()\n\n\tname = util.CleanPath(name)\n\tif !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tfi, err := c.DoStat(name, 0, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn fi, err\n}\n\n// RemoveAll removes path and any children it contains.\n// If the path does not exist, RemoveAll returns nil (no error).\nfunc (c *Connection) RemoveAll(_ context.Context, name string) error {\n\tc.UpdateLastActivity()\n\n\tname = util.CleanPath(name)\n\treturn c.BaseConnection.RemoveAll(name)\n}\n\n// OpenFile opens the named file with specified flag.\n// This method is used for uploads and downloads but also for Stat and Readdir\nfunc (c *Connection) OpenFile(_ context.Context, name string, flag int, _ os.FileMode) (webdav.File, error) {\n\tc.UpdateLastActivity()\n\n\tif err := common.Connections.IsNewTransferAllowed(c.User.Username); err != nil {\n\t\tc.Log(logger.LevelInfo, \"denying transfer due to count limits\")\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tname = util.CleanPath(name)\n\tfs, p, err := c.GetFsAndResolvedPath(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif flag == os.O_RDONLY || c.request.Method == \"PROPPATCH\" {\n\t\t// Download, Stat, Readdir or simply open/close\n\t\treturn c.getFile(fs, p, name)\n\t}\n\treturn c.putFile(fs, p, name)\n}\n\nfunc (c *Connection) getFile(fs vfs.Fs, fsPath, virtualPath string) (webdav.File, error) {\n\tvar cancelFn func()\n\n\t// we open the file when we receive the first read so we only open the file if necessary\n\tbaseTransfer := common.NewBaseTransfer(nil, c.BaseConnection, cancelFn, fsPath, fsPath, virtualPath,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, c.GetTransferQuota())\n\n\treturn newWebDavFile(baseTransfer, nil, nil), nil\n}\n\nfunc (c *Connection) putFile(fs vfs.Fs, fsPath, virtualPath string) (webdav.File, error) {\n\tif ok, _ := c.User.IsFileAllowed(virtualPath); !ok {\n\t\tc.Log(logger.LevelWarn, \"writing file %q is not allowed\", virtualPath)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\tfilePath := fsPath\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\tfilePath = fs.GetAtomicUploadPath(fsPath)\n\t}\n\n\tstat, statErr := fs.Lstat(fsPath)\n\tif (statErr == nil && stat.Mode()&os.ModeSymlink != 0) || fs.IsNotExist(statErr) {\n\t\tif !c.User.HasPerm(dataprovider.PermUpload, path.Dir(virtualPath)) {\n\t\t\treturn nil, c.GetPermissionDeniedError()\n\t\t}\n\t\treturn c.handleUploadToNewFile(fs, fsPath, filePath, virtualPath)\n\t}\n\n\tif statErr != nil {\n\t\tc.Log(logger.LevelError, \"error performing file stat %q: %+v\", fsPath, statErr)\n\t\treturn nil, c.GetFsError(fs, statErr)\n\t}\n\n\t// This happen if we upload a file that has the same name of an existing directory\n\tif stat.IsDir() {\n\t\tc.Log(logger.LevelError, \"attempted to open a directory for writing to: %q\", fsPath)\n\t\treturn nil, c.GetOpUnsupportedError()\n\t}\n\n\tif !c.User.HasPerm(dataprovider.PermOverwrite, path.Dir(virtualPath)) {\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\treturn c.handleUploadToExistingFile(fs, fsPath, filePath, stat.Size(), virtualPath)\n}\n\nfunc (c *Connection) handleUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, requestPath string) (webdav.File, error) {\n\tdiskQuota, transferQuota := c.HasSpace(true, false, requestPath)\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to quota limits\")\n\t\treturn nil, common.ErrQuotaExceeded\n\t}\n\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {\n\t\tc.Log(logger.LevelDebug, \"upload for file %q denied by pre action: %v\", requestPath, err)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\tfile, w, cancelFn, err := fs.Create(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, c.GetCreateChecks(requestPath, true, false))\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error creating file %q: %+v\", resolvedPath, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\n\tvfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())\n\n\t// we can get an error only for resume\n\tmaxWriteSize, _ := c.GetMaxWriteSize(diskQuota, false, 0, fs.IsUploadResumeSupported())\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,\n\t\tcommon.TransferUpload, 0, 0, maxWriteSize, 0, true, fs, transferQuota)\n\tmtime := c.getModificationTime()\n\tbaseTransfer.SetTimes(resolvedPath, mtime, mtime)\n\n\treturn newWebDavFile(baseTransfer, w, nil), nil\n}\n\nfunc (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePath string, fileSize int64,\n\trequestPath string,\n) (webdav.File, error) {\n\tvar err error\n\tdiskQuota, transferQuota := c.HasSpace(false, false, requestPath)\n\tif !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {\n\t\tc.Log(logger.LevelInfo, \"denying file write due to quota limits\")\n\t\treturn nil, common.ErrQuotaExceeded\n\t}\n\tif _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath,\n\t\tfileSize, os.O_TRUNC); err != nil {\n\t\tc.Log(logger.LevelDebug, \"upload for file %q denied by pre action: %v\", requestPath, err)\n\t\treturn nil, c.GetPermissionDeniedError()\n\t}\n\n\t// if there is a size limit remaining size cannot be 0 here, since quotaResult.HasSpace\n\t// will return false in this case and we deny the upload before\n\tmaxWriteSize, _ := c.GetMaxWriteSize(diskQuota, false, fileSize, fs.IsUploadResumeSupported())\n\n\tif common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {\n\t\t_, _, err = fs.Rename(resolvedPath, filePath, 0)\n\t\tif err != nil {\n\t\t\tc.Log(logger.LevelError, \"error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v\",\n\t\t\t\tresolvedPath, filePath, err)\n\t\t\treturn nil, c.GetFsError(fs, err)\n\t\t}\n\t}\n\n\tfile, w, cancelFn, err := fs.Create(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, c.GetCreateChecks(requestPath, false, false))\n\tif err != nil {\n\t\tc.Log(logger.LevelError, \"error creating file %q: %+v\", resolvedPath, err)\n\t\treturn nil, c.GetFsError(fs, err)\n\t}\n\tinitialSize := int64(0)\n\ttruncatedSize := int64(0) // bytes truncated and not included in quota\n\tif vfs.HasTruncateSupport(fs) {\n\t\tvfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))\n\t\tif err == nil {\n\t\t\tdataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -fileSize, false)\n\t\t} else {\n\t\t\tdataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck\n\t\t}\n\t} else {\n\t\tinitialSize = fileSize\n\t\ttruncatedSize = fileSize\n\t}\n\n\tvfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())\n\n\tbaseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,\n\t\tcommon.TransferUpload, 0, initialSize, maxWriteSize, truncatedSize, false, fs, transferQuota)\n\tmtime := c.getModificationTime()\n\tbaseTransfer.SetTimes(resolvedPath, mtime, mtime)\n\n\treturn newWebDavFile(baseTransfer, w, nil), nil\n}\n"
  },
  {
    "path": "internal/webdavd/internal_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage webdavd\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/drakkan/webdav\"\n\t\"github.com/eikenb/pipeat\"\n\t\"github.com/sftpgo/sdk\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n)\n\nconst (\n\ttestFile   = \"test_dav_file\"\n\twebDavCert = `-----BEGIN CERTIFICATE-----\nMIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw\nRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw\nOTUzMDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD\nVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVqWvrJ51t5OxV0v25NsOgR82CA\nNXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIVCzgWkxiz7XE4lgUwX44FCXZM\n3+JeUbKjUzBRMB0GA1UdDgQWBBRhLw+/o3+Z02MI/d4tmaMui9W16jAfBgNVHSME\nGDAWgBRhLw+/o3+Z02MI/d4tmaMui9W16jAPBgNVHRMBAf8EBTADAQH/MAoGCCqG\nSM49BAMCA2kAMGYCMQDqLt2lm8mE+tGgtjDmtFgdOcI72HSbRQ74D5rYTzgST1rY\n/8wTi5xl8TiFUyLMUsICMQC5ViVxdXbhuG7gX6yEqSkMKZICHpO8hqFwOD/uaFVI\ndV4vKmHUzwK/eIx+8Ay3neE=\n-----END CERTIFICATE-----`\n\twebDavKey = `-----BEGIN EC PARAMETERS-----\nBgUrgQQAIg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDCfMNsN6miEE3rVyUPwElfiJSWaR5huPCzUenZOfJT04GAcQdWvEju3\nUM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq\nWvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV\nCzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=\n-----END EC PRIVATE KEY-----`\n\tcaCRT = `-----BEGIN CERTIFICATE-----\nMIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0\nQXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT\nCENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m\nfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1\nv03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh\nyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL\nCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U\n4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb\nKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo\nNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLqR/BJTbbyXUB0imne1u00fuzb\nS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb78x7ivdyXSF5LVQJ1JvhhWu6i\nM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpBP8d2jcRZVUVrSXGc2mAGuGOY\n/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2ZxugCXULtRWJ9p4C9zUl40HEy\nOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD\nAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNoJhIvDZQrEf/VQbWuu\nXgNnt2m5MA0GCSqGSIb3DQEBCwUAA4ICAQCYhT5SRqk19hGrQ09hVSZOzynXAa5F\nsYkEWJzFyLg9azhnTPE1bFM18FScnkd+dal6mt+bQiJvdh24NaVkDghVB7GkmXki\npAiZwEDHMqtbhiPxY8LtSeCBAz5JqXVU2Q0TpAgNSH4W7FbGWNThhxcJVOoIrXKE\njbzhwl1Etcaf0DBKWliUbdlxQQs65DLy+rNBYtOeK0pzhzn1vpehUlJ4eTFzP9KX\ny2Mksuq9AspPbqnqpWW645MdTxMb5T57MCrY3GDKw63z5z3kz88LWJF3nOxZmgQy\nWFUhbLmZm7x6N5eiu6Wk8/B4yJ/n5UArD4cEP1i7nqu+mbbM/SZlq1wnGpg/sbRV\noUF+a7pRcSbfxEttle4pLFhS+ErKatjGcNEab2OlU3bX5UoBs+TYodnCWGKOuBKV\nL/CYc65QyeYZ+JiwYn9wC8YkzOnnVIQjiCEkLgSL30h9dxpnTZDLrdAA8ItelDn5\nDvjuQq58CGDsaVqpSobiSC1DMXYWot4Ets1wwovUNEq1l0MERB+2olE+JU/8E23E\neL1/aA7Kw/JibkWz1IyzClpFDKXf6kR2onJyxerdwUL+is7tqYFLysiHxZDL1bli\nSXbW8hMa5gvo0IilFP9Rznn8PplIfCsvBDVv6xsRr5nTAFtwKaMBVgznE2ghs69w\nkK8u1YiiVenmoQ==\n-----END CERTIFICATE-----`\n\tcaKey = `-----BEGIN RSA PRIVATE KEY-----\nMIIJKgIBAAKCAgEA7WHW216mfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7x\nb64rkpdzx1aWetSiCrEyc3D1v03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvD\nOBUYZgtMqHZzpE6xRrqQ84zhyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKz\nn/2uEVt33qmO85WtN3RzbSqLCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj\n7B5P5MeamkkogwbExUjdHp3U4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZ\nDe67V/Q8iB2May1k7zBz1ZtbKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmOb\ncn8AIfH6smLQrn0C3cs7CYfoNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLq\nR/BJTbbyXUB0imne1u00fuzbS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb7\n8x7ivdyXSF5LVQJ1JvhhWu6iM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpB\nP8d2jcRZVUVrSXGc2mAGuGOY/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2Z\nxugCXULtRWJ9p4C9zUl40HEyOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEA\nAQKCAgEA4x0OoceG54ZrVxifqVaQd8qw3uRmUKUMIMdfuMlsdideeLO97ynmSlRY\n00kGo/I4Lp6mNEjI9gUie9+uBrcUhri4YLcujHCH+YlNnCBDbGjwbe0ds9SLCWaa\nKztZHMSlW5Q4Bqytgu+MpOnxSgqjlOk+vz9TcGFKVnUkHIkAcqKFJX8gOFxPZA/t\nOb1kJaz4kuv5W2Kur/ISKvQtvFvOtQeV0aJyZm8LqXnvS4cPI7yN4329NDU0HyDR\ny/deqS2aqV4zII3FFqbz8zix/m1xtVQzWCugZGMKrz0iuJMfNeCABb8rRGc6GsZz\n+465v/kobqgeyyneJ1s5rMFrLp2o+dwmnIVMNsFDUiN1lIZDHLvlgonaUO3IdTZc\n9asamFWKFKUMgWqM4zB1vmUO12CKowLNIIKb0L+kf1ixaLLDRGf/f9vLtSHE+oyx\nlATiS18VNA8+CGsHF6uXMRwf2auZdRI9+s6AAeyRISSbO1khyWKHo+bpOvmPAkDR\nnknTjbYgkoZOV+mrsU5oxV8s6vMkuvA3rwFhT2gie8pokuACFcCRrZi9MVs4LmUQ\nu0GYTHvp2WJUjMWBm6XX7Hk3g2HV842qpk/mdtTjNsXws81djtJPn4I/soIXSgXz\npY3SvKTuOckP9OZVF0yqKGeZXKpD288PKpC+MAg3GvEJaednagECggEBAPsfLwuP\nL1kiDjXyMcRoKlrQ6Q/zBGyBmJbZ5uVGa02+XtYtDAzLoVupPESXL0E7+r8ZpZ39\n0dV4CEJKpbVS/BBtTEkPpTK5kz778Ib04TAyj+YLhsZjsnuja3T5bIBZXFDeDVDM\n0ZaoFoKpIjTu2aO6pzngsgXs6EYbo2MTuJD3h0nkGZsICL7xvT9Mw0P1p2Ftt/hN\n+jKk3vN220wTWUsq43AePi45VwK+PNP12ZXv9HpWDxlPo3j0nXtgYXittYNAT92u\nBZbFAzldEIX9WKKZgsWtIzLaASjVRntpxDCTby/nlzQ5dw3DHU1DV3PIqxZS2+Oe\nKV+7XFWgZ44YjYECggEBAPH+VDu3QSrqSahkZLkgBtGRkiZPkZFXYvU6kL8qf5wO\nZ/uXMeqHtznAupLea8I4YZLfQim/NfC0v1cAcFa9Ckt9g3GwTSirVcN0AC1iOyv3\n/hMZCA1zIyIcuUplNr8qewoX71uPOvCNH0dix77423mKFkJmNwzy4Q+rV+qkRdLn\nv+AAgh7g5N91pxNd6LQJjoyfi1Ka6rRP2yGXM5v7QOwD16eN4JmExUxX1YQ7uNuX\npVS+HRxnBquA+3/DB1LtBX6pa2cUa+LRUmE/NCPHMvJcyuNkYpJKlNTd9vnbfo0H\nRNSJSWm+aGxDFMjuPjV3JLj2OdKMPwpnXdh2vBZCPpMCggEAM+yTvrEhmi2HgLIO\nhkz/jP2rYyfdn04ArhhqPLgd0dpuI5z24+Jq/9fzZT9ZfwSW6VK1QwDLlXcXRhXH\nQ8Hf6smev3CjuORURO61IkKaGWwrAucZPAY7ToNQ4cP9ImDXzMTNPgrLv3oMBYJR\nV16X09nxX+9NABqnQG/QjdjzDc6Qw7+NZ9f2bvzvI5qMuY2eyW91XbtJ45ThoLfP\nymAp03gPxQwL0WT7z85kJ3OrROxzwaPvxU0JQSZbNbqNDPXmFTiECxNDhpRAAWlz\n1DC5Vg2l05fkMkyPdtD6nOQWs/CYSfB5/EtxiX/xnBszhvZUIe6KFvuKFIhaJD5h\niykagQKCAQEAoBRm8k3KbTIo4ZzvyEq4V/+dF3zBRczx6FkCkYLygXBCNvsQiR2Y\nBjtI8Ijz7bnQShEoOmeDriRTAqGGrspEuiVgQ1+l2wZkKHRe/aaij/Zv+4AuhH8q\nuZEYvW7w5Uqbs9SbgQzhp2kjTNy6V8lVnjPLf8cQGZ+9Y9krwktC6T5m/i435WdN\n38h7amNP4XEE/F86Eb3rDrZYtgLIoCF4E+iCyxMehU+AGH1uABhls9XAB6vvo+8/\nSUp8lEqWWLP0U5KNOtYWfCeOAEiIHDbUq+DYUc4BKtbtV1cx3pzlPTOWw6XBi5Lq\njttdL4HyYvnasAQpwe8GcMJqIRyCVZMiwwKCAQEAhQTTS3CC8PwcoYrpBdTjW1ck\nvVFeF1YbfqPZfYxASCOtdx6wRnnEJ+bjqntagns9e88muxj9UhxSL6q9XaXQBD8+\n2AmKUxphCZQiYFZcTucjQEQEI2nN+nAKgRrUSMMGiR8Ekc2iFrcxBU0dnSohw+aB\nPbMKVypQCREu9PcDFIp9rXQTeElbaNsIg1C1w/SQjODbmN/QFHTVbRODYqLeX1J/\nVcGsykSIq7hv6bjn7JGkr2JTdANbjk9LnMjMdJFsKRYxPKkOQfYred6Hiojp5Sor\nPW5am8ejnNSPhIfqQp3uV3KhwPDKIeIpzvrB4uPfTjQWhekHCb8cKSWux3flqw==\n-----END RSA PRIVATE KEY-----`\n\tcaCRL = `-----BEGIN X509 CRL-----\nMIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN\nMjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg\nZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r\nrl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV\n+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199\nXSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE\njF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF\nDouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD\nMSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4\niYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE\nypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4\nhVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94\ncp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67\nsn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn\nw7bXJGfJcIMrrKs=\n-----END X509 CRL-----`\n\tclient1Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAJr32nHRlhyPiS7IfZ/ZWYowDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjM3WhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+kiJ3X6\nHUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi85xE\nOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLWeYl7\nQie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4ZdRf\nXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dybbhO\nc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRUh5Xo\nGzjh6iReaPSOgGatqOw9bDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEAyAK7cOTWqjyLgFM0kyyx1fNPvm2GwKep3MuU\nOrSnLuWjoxzb7WcbKNVMlnvnmSUAWuErxsY0PUJNfcuqWiGmEp4d/SWfWPigG6DC\nsDej35BlSfX8FCufYrfC74VNk4yBS2LVYmIqcpqUrfay0I2oZA8+ToLEpdUvEv2I\nl59eOhJO2jsC3JbOyZZmK2Kv7d94fR+1tg2Rq1Wbnmc9AZKq7KDReAlIJh4u2KHb\nBbtF79idusMwZyP777tqSQ4THBMa+VAEc2UrzdZqTIAwqlKQOvO2fRz2P+ARR+Tz\nMYJMdCdmPZ9qAc8U1OcFBG6qDDltO8wf/Nu/PsSI5LGCIhIuPPIuKfm0rRfTqCG7\nQPQPWjRoXtGGhwjdIuWbX9fIB+c+NpAEKHgLtV+Rxj8s5IVxqG9a5TtU9VkfVXJz\nJ20naoz/G+vDsVINpd3kH0ziNvdrKfGRM5UgtnUOPCXB22fVmkIsMH2knI10CKK+\noffI56NTkLRu00xvg98/wdukhkwIAxg6PQI/BHY5mdvoacEHHHdOhMq+GSAh7DDX\nG8+HdbABM1ExkPnZLat15q706ztiuUpQv1C2DI8YviUVkMqCslj4cD4F8EFPo4kr\nkvme0Cuc9Qlf7N5rjdV3cjwavhFx44dyXj9aesft2Q1okPiIqbGNpcjHcIRlj4Au\nMU3Bo0A=\n-----END CERTIFICATE-----`\n\tclient1Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+ki\nJ3X6HUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi\n85xEOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLW\neYl7Qie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4\nZdRfXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dy\nbbhOc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABAoIBAFjSHK7gENVZxphO\nhHg8k9ShnDo8eyDvK8l9Op3U3/yOsXKxolivvyx//7UFmz3vXDahjNHe7YScAXdw\neezbqBXa7xrvghqZzp2HhFYwMJ0210mcdncBKVFzK4ztZHxgQ0PFTqet0R19jZjl\nX3A325/eNZeuBeOied4qb/24AD6JGc6A0J55f5/QUQtdwYwrL15iC/KZXDL90PPJ\nCFJyrSzcXvOMEvOfXIFxhDVKRCppyIYXG7c80gtNC37I6rxxMNQ4mxjwUI2IVhxL\nj+nZDu0JgRZ4NaGjOq2e79QxUVm/GG3z25XgmBFBrXkEVV+sCZE1VDyj6kQfv9FU\nNhOrwGECgYEAzq47r/HwXifuGYBV/mvInFw3BNLrKry+iUZrJ4ms4g+LfOi0BAgf\nsXsWXulpBo2YgYjFdO8G66f69GlB4B7iLscpABXbRtpDZEnchQpaF36/+4g3i8gB\nZ29XHNDB8+7t4wbXvlSnLv1tZWey2fS4hPosc2YlvS87DMmnJMJqhs8CgYEA4oiB\nLGQP6VNdX0Uigmh5fL1g1k95eC8GP1ylczCcIwsb2OkAq0MT7SHRXOlg3leEq4+g\nmCHk1NdjkSYxDL2ZeTKTS/gy4p1jlcDa6Ilwi4pVvatNvu4o80EYWxRNNb1mAn67\nT8TN9lzc6mEi+LepQM3nYJ3F+ZWTKgxH8uoJwMUCgYEArpumE1vbjUBAuEyi2eGn\nRunlFW83fBCfDAxw5KM8anNlja5uvuU6GU/6s06QCxg+2lh5MPPrLdXpfukZ3UVa\nItjg+5B7gx1MSALaiY8YU7cibFdFThM3lHIM72wyH2ogkWcrh0GvSFSUQlJcWCSW\nasmMGiYXBgBL697FFZomMyMCgYEAkAnp0JcDQwHd4gDsk2zoqnckBsDb5J5J46n+\nDYNAFEww9bgZ08u/9MzG+cPu8xFE621U2MbcYLVfuuBE2ewIlPaij/COMmeO9Z59\n0tPpOuDH6eTtd1SptxqR6P+8pEn8feOlKHBj4Z1kXqdK/EiTlwAVeep4Al2oCFls\nujkz4F0CgYAe8vHnVFHlWi16zAqZx4ZZZhNuqPtgFkvPg9LfyNTA4dz7F9xgtUaY\nnXBPyCe/8NtgBfT79HkPiG3TM0xRZY9UZgsJKFtqAu5u4ManuWDnsZI9RK2QTLHe\nyEbH5r3Dg3n9k/3GbjXFIWdU9UaYsdnSKHHtMw9ZODc14LaAogEQug==\n-----END RSA PRIVATE KEY-----`\n\t// client 2 crt is revoked\n\tclient2Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAM5p4duNjikSPxYGaqBm5FEwDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjUyWhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImVjm/b\nQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TPkZua\neq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEKh4LQ\ncr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81PQAmT\nA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu4Ic0\n6tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBR5mf0f\nZjf8ZCGXqU2+45th7VkkLDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEARhFxNAouwbpEfN1M90+ao5rwyxEewerSoCCz\nPQzeUZ66MA/FkS/tFUGgGGG+wERN+WLbe1cN6q/XFr0FSMLuUxLXDNV02oUL/FnY\nxcyNLaZUZ0pP7sA+Hmx2AdTA6baIwQbyIY9RLAaz6hzo1YbI8yeis645F1bxgL2D\nEP5kXa3Obv0tqWByMZtrmJPv3p0W5GJKXVDn51GR/E5KI7pliZX2e0LmMX9mxfPB\n4sXFUggMHXxWMMSAmXPVsxC2KX6gMnajO7JUraTwuGm+6V371FzEX+UKXHI+xSvO\n78TseTIYsBGLjeiA8UjkKlD3T9qsQm2mb2PlKyqjvIm4i2ilM0E2w4JZmd45b925\n7q/QLV3NZ/zZMi6AMyULu28DWKfAx3RLKwnHWSFcR4lVkxQrbDhEUMhAhLAX+2+e\nqc7qZm3dTabi7ZJiiOvYK/yNgFHa/XtZp5uKPB5tigPIa+34hbZF7s2/ty5X3O1N\nf5Ardz7KNsxJjZIt6HvB28E/PPOvBqCKJc1Y08J9JbZi8p6QS1uarGoR7l7rT1Hv\n/ZXkNTw2bw1VpcWdzDBLLVHYNnJmS14189LVk11PcJJpSmubwCqg+ZZULdgtVr3S\nANas2dgMPVwXhnAalgkcc+lb2QqaEz06axfbRGBsgnyqR5/koKCg1Hr0+vThHSsR\nE0+r2+4=\n-----END CERTIFICATE-----`\n\tclient2Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImV\njm/bQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TP\nkZuaeq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEK\nh4LQcr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81P\nQAmTA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu\n4Ic06tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABAoIBAQCMnEeg9uXQmdvq\nop4qi6bV+ZcDWvvkLwvHikFMnYpIaheYBpF2ZMKzdmO4xgCSWeFCQ4Hah8KxfHCM\nqLuWvw2bBBE5J8yQ/JaPyeLbec7RX41GQ2YhPoxDdP0PdErREdpWo4imiFhH/Ewt\nRvq7ufRdpdLoS8dzzwnvX3r+H2MkHoC/QANW2AOuVoZK5qyCH5N8yEAAbWKaQaeL\nVBhAYEVKbAkWEtXw7bYXzxRR7WIM3f45v3ncRusDIG+Hf75ZjatoH0lF1gHQNofO\nqkCVZVzjkLFuzDic2KZqsNORglNs4J6t5Dahb9v3hnoK963YMnVSUjFvqQ+/RZZy\nVILFShilAoGBANucwZU61eJ0tLKBYEwmRY/K7Gu1MvvcYJIOoX8/BL3zNmNO0CLl\nNiABtNt9WOVwZxDsxJXdo1zvMtAegNqS6W11R1VAZbL6mQ/krScbLDE6JKA5DmA7\n4nNi1gJOW1ziAfdBAfhe4cLbQOb94xkOK5xM1YpO0xgDJLwrZbehDMmPAoGBAMAl\n/owPDAvcXz7JFynT0ieYVc64MSFiwGYJcsmxSAnbEgQ+TR5FtkHYe91OSqauZcCd\naoKXQNyrYKIhyounRPFTdYQrlx6KtEs7LU9wOxuphhpJtGjRnhmA7IqvX703wNvu\nkhrEavn86G5boH8R80371SrN0Rh9UeAlQGuNBdvfAoGAEAmokW9Ug08miwqrr6Pz\n3IZjMZJwALidTM1IufQuMnj6ddIhnQrEIx48yPKkdUz6GeBQkuk2rujA+zXfDxc/\neMDhzrX/N0zZtLFse7ieR5IJbrH7/MciyG5lVpHGVkgjAJ18uVikgAhm+vd7iC7i\nvG1YAtuyysQgAKXircBTIL0CgYAHeTLWVbt9NpwJwB6DhPaWjalAug9HIiUjktiB\nGcEYiQnBWn77X3DATOA8clAa/Yt9m2HKJIHkU1IV3ESZe+8Fh955PozJJlHu3yVb\nAp157PUHTriSnxyMF2Sb3EhX/rQkmbnbCqqygHC14iBy8MrKzLG00X6BelZV5n0D\n8d85dwKBgGWY2nsaemPH/TiTVF6kW1IKSQoIyJChkngc+Xj/2aCCkkmAEn8eqncl\nRKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA\nubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl\n-----END RSA PRIVATE KEY-----`\n\tosWindows = \"windows\"\n)\n\n// MockOsFs mockable OsFs\ntype MockOsFs struct {\n\tvfs.Fs\n\terr                     error\n\tisAtomicUploadSupported bool\n\treader                  *pipeat.PipeReaderAt\n}\n\n// Name returns the name for the Fs implementation\nfunc (fs *MockOsFs) Name() string {\n\treturn \"mockOsFs\"\n}\n\n// Open returns nil\nfunc (fs *MockOsFs) Open(name string, offset int64) (vfs.File, vfs.PipeReader, func(), error) {\n\tif fs.reader != nil {\n\t\treturn nil, vfs.NewPipeReader(fs.reader), nil, nil\n\t}\n\treturn fs.Fs.Open(name, offset)\n}\n\n// IsUploadResumeSupported returns true if resuming uploads is supported\nfunc (*MockOsFs) IsUploadResumeSupported() bool {\n\treturn false\n}\n\n// IsAtomicUploadSupported returns true if atomic upload is supported\nfunc (fs *MockOsFs) IsAtomicUploadSupported() bool {\n\treturn fs.isAtomicUploadSupported\n}\n\n// Remove removes the named file or (empty) directory.\nfunc (fs *MockOsFs) Remove(name string, _ bool) error {\n\tif fs.err != nil {\n\t\treturn fs.err\n\t}\n\treturn os.Remove(name)\n}\n\n// Rename renames (moves) source to target\nfunc (fs *MockOsFs) Rename(source, target string, _ int) (int, int64, error) {\n\terr := os.Rename(source, target)\n\treturn -1, -1, err\n}\n\n// GetMimeType returns the content type\nfunc (fs *MockOsFs) GetMimeType(_ string) (string, error) {\n\tif fs.err != nil {\n\t\treturn \"\", fs.err\n\t}\n\treturn \"application/custom-mime\", nil\n}\n\nfunc newMockOsFs(atomicUpload bool, connectionID, rootDir string, reader *pipeat.PipeReaderAt, err error) vfs.Fs {\n\treturn &MockOsFs{\n\t\tFs:                      vfs.NewOsFs(connectionID, rootDir, \"\", nil),\n\t\tisAtomicUploadSupported: atomicUpload,\n\t\treader:                  reader,\n\t\terr:                     err,\n\t}\n}\n\nfunc TestUserInvalidParams(t *testing.T) {\n\tu := &dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername: \"username\",\n\t\t\tHomeDir:  \"invalid\",\n\t\t},\n\t}\n\tc := &Configuration{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort: 9000,\n\t\t\t},\n\t\t},\n\t}\n\n\tserver := webDavServer{\n\t\tconfig:  c,\n\t\tbinding: c.Bindings[0],\n\t}\n\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", u.Username), nil)\n\tassert.NoError(t, err)\n\n\t_, err = server.validateUser(u, req, dataprovider.LoginMethodPassword)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, fmt.Sprintf(\"cannot login user with invalid home dir: %q\", u.HomeDir))\n\t}\n\n\treq.TLS = &tls.ConnectionState{}\n\twriteLog(req, http.StatusOK, nil)\n}\n\nfunc TestAllowedProxyUnixDomainSocket(t *testing.T) {\n\tb := Binding{\n\t\tAddress:      filepath.Join(os.TempDir(), \"sock\"),\n\t\tProxyAllowed: []string{\"127.0.0.1\", \"127.0.1.1\"},\n\t}\n\terr := b.parseAllowedProxy()\n\tassert.NoError(t, err)\n\tif assert.Len(t, b.allowHeadersFrom, 1) {\n\t\tassert.True(t, b.allowHeadersFrom[0](nil))\n\t}\n}\n\nfunc TestProxyListenerWrapper(t *testing.T) {\n\tb := Binding{\n\t\tProxyMode: 0,\n\t}\n\trequire.Nil(t, b.listenerWrapper())\n\tb.ProxyMode = 1\n\trequire.NotNil(t, b.listenerWrapper())\n}\n\nfunc TestRemoteAddress(t *testing.T) {\n\tremoteAddr1 := \"100.100.100.100\"\n\tremoteAddr2 := \"172.172.172.172\"\n\n\tc := &Configuration{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort:         9000,\n\t\t\t\tProxyAllowed: []string{remoteAddr2, \"10.8.0.0/30\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tserver := webDavServer{\n\t\tconfig:  c,\n\t\tbinding: c.Bindings[0],\n\t}\n\terr := server.binding.parseAllowedProxy()\n\tassert.NoError(t, err)\n\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(t, err)\n\tassert.Empty(t, req.RemoteAddr)\n\n\ttrueClientIP := \"True-Client-IP\"\n\tcfConnectingIP := \"CF-Connecting-IP\"\n\txff := \"X-Forwarded-For\"\n\txRealIP := \"X-Real-IP\"\n\n\treq.Header.Set(trueClientIP, remoteAddr1)\n\tip := util.GetRealIP(req, trueClientIP, 0)\n\tassert.Equal(t, remoteAddr1, ip)\n\tip = util.GetRealIP(req, trueClientIP, 2)\n\tassert.Empty(t, ip)\n\treq.Header.Del(trueClientIP)\n\treq.Header.Set(cfConnectingIP, remoteAddr1)\n\tip = util.GetRealIP(req, cfConnectingIP, 0)\n\tassert.Equal(t, remoteAddr1, ip)\n\treq.Header.Del(cfConnectingIP)\n\treq.Header.Set(xff, remoteAddr1)\n\tip = util.GetRealIP(req, xff, 0)\n\tassert.Equal(t, remoteAddr1, ip)\n\t// this will be ignored, remoteAddr1 is not allowed to se this header\n\treq.Header.Set(xff, remoteAddr2)\n\treq.RemoteAddr = remoteAddr1\n\tip = server.checkRemoteAddress(req)\n\tassert.Equal(t, remoteAddr1, ip)\n\treq.RemoteAddr = \"\"\n\tip = server.checkRemoteAddress(req)\n\tassert.Empty(t, ip)\n\n\treq.Header.Set(xff, fmt.Sprintf(\"%v , %v\", remoteAddr2, remoteAddr1))\n\tip = util.GetRealIP(req, xff, 1)\n\tassert.Equal(t, remoteAddr2, ip)\n\n\treq.RemoteAddr = remoteAddr2\n\treq.Header.Set(xff, fmt.Sprintf(\"%v,%v\", \"12.34.56.78\", \"172.16.2.4\"))\n\tserver.binding.ClientIPHeaderDepth = 1\n\tserver.binding.ClientIPProxyHeader = xff\n\tip = server.checkRemoteAddress(req)\n\tassert.Equal(t, \"12.34.56.78\", ip)\n\tassert.Equal(t, ip, req.RemoteAddr)\n\n\treq.RemoteAddr = remoteAddr2\n\treq.Header.Set(xff, fmt.Sprintf(\"%v,%v\", \"12.34.56.79\", \"172.16.2.5\"))\n\tserver.binding.ClientIPHeaderDepth = 0\n\tip = server.checkRemoteAddress(req)\n\tassert.Equal(t, \"172.16.2.5\", ip)\n\tassert.Equal(t, ip, req.RemoteAddr)\n\n\treq.RemoteAddr = \"10.8.0.2\"\n\treq.Header.Set(xff, remoteAddr1)\n\tip = server.checkRemoteAddress(req)\n\tassert.Equal(t, remoteAddr1, ip)\n\tassert.Equal(t, ip, req.RemoteAddr)\n\n\treq.RemoteAddr = \"10.8.0.3\"\n\treq.Header.Set(xff, \"not an ip\")\n\tip = server.checkRemoteAddress(req)\n\tassert.Equal(t, \"10.8.0.3\", ip)\n\tassert.Equal(t, ip, req.RemoteAddr)\n\n\treq.Header.Del(xff)\n\treq.RemoteAddr = \"\"\n\treq.Header.Set(xRealIP, remoteAddr1)\n\tip = util.GetRealIP(req, \"x-real-ip\", 0)\n\tassert.Equal(t, remoteAddr1, ip)\n\treq.RemoteAddr = \"\"\n}\n\nfunc TestConnWithNilRequest(t *testing.T) {\n\tc := &Connection{}\n\tassert.Empty(t, c.GetClientVersion())\n\tassert.Empty(t, c.GetCommand())\n\tassert.Empty(t, c.GetRemoteAddress())\n\tassert.True(t, c.getModificationTime().IsZero())\n}\n\nfunc TestResolvePathErrors(t *testing.T) {\n\tctx := context.Background()\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: \"invalid\",\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfs := vfs.NewOsFs(\"connID\", user.HomeDir, \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, \"\", \"\", user),\n\t}\n\n\terr := connection.Mkdir(ctx, \"\", os.ModePerm)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\n\terr = connection.Rename(ctx, \"oldName\", \"newName\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\n\t_, err = connection.Stat(ctx, \"name\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\n\terr = connection.RemoveAll(ctx, \"\")\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\n\t_, err = connection.OpenFile(ctx, \"\", 0, os.ModePerm)\n\tif assert.Error(t, err) {\n\t\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\t}\n\n\tif runtime.GOOS != osWindows {\n\t\tuser.HomeDir = filepath.Clean(os.TempDir())\n\t\tconnection.User = user\n\t\tfs := vfs.NewOsFs(\"connID\", connection.User.HomeDir, \"\", nil)\n\t\tsubDir := \"sub\"\n\t\ttestTxtFile := \"file.txt\"\n\t\terr = os.MkdirAll(filepath.Join(os.TempDir(), subDir, subDir), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(filepath.Join(os.TempDir(), subDir, subDir, testTxtFile), []byte(\"content\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.Chmod(filepath.Join(os.TempDir(), subDir, subDir), 0001)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(filepath.Join(os.TempDir(), testTxtFile), []byte(\"test content\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = connection.Rename(ctx, testTxtFile, path.Join(subDir, subDir, testTxtFile))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.EqualError(t, err, common.ErrPermissionDenied.Error())\n\t\t}\n\t\t_, err = connection.putFile(fs, filepath.Join(connection.User.HomeDir, subDir, subDir, testTxtFile),\n\t\t\tpath.Join(subDir, subDir, testTxtFile))\n\t\tif assert.Error(t, err) {\n\t\t\tassert.EqualError(t, err, common.ErrPermissionDenied.Error())\n\t\t}\n\t\terr = os.Chmod(filepath.Join(os.TempDir(), subDir, subDir), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(filepath.Join(os.TempDir(), subDir))\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(filepath.Join(os.TempDir(), testTxtFile))\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestFileAccessErrors(t *testing.T) {\n\tctx := context.Background()\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfs := vfs.NewOsFs(\"connID\", user.HomeDir, \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, \"\", \"\", user),\n\t}\n\tmissingPath := \"missing path\"\n\tfsMissingPath := filepath.Join(user.HomeDir, missingPath)\n\terr := connection.RemoveAll(ctx, missingPath)\n\tassert.ErrorIs(t, err, os.ErrNotExist)\n\tdavFile, err := connection.getFile(fs, fsMissingPath, missingPath)\n\tassert.NoError(t, err)\n\tbuf := make([]byte, 64)\n\t_, err = davFile.Read(buf)\n\tassert.ErrorIs(t, err, os.ErrNotExist)\n\terr = davFile.Close()\n\tassert.ErrorIs(t, err, os.ErrNotExist)\n\tp := filepath.Join(user.HomeDir, \"adir\", missingPath)\n\t_, err = connection.handleUploadToNewFile(fs, p, p, path.Join(\"adir\", missingPath))\n\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t_, err = connection.handleUploadToExistingFile(fs, p, \"_\"+p, 0, path.Join(\"adir\", missingPath))\n\tif assert.Error(t, err) {\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t}\n\n\tfs = newMockOsFs(false, fs.ConnectionID(), user.HomeDir, nil, nil)\n\t_, err = connection.handleUploadToExistingFile(fs, p, p, 0, path.Join(\"adir\", missingPath))\n\tassert.ErrorIs(t, err, os.ErrNotExist)\n\n\tf, err := os.CreateTemp(\"\", \"temp\")\n\tassert.NoError(t, err)\n\terr = f.Close()\n\tassert.NoError(t, err)\n\tdavFile, err = connection.handleUploadToExistingFile(fs, f.Name(), f.Name(), 123, f.Name())\n\tif assert.NoError(t, err) {\n\t\ttransfer := davFile.(*webDavFile)\n\t\ttransfers := connection.GetTransfers()\n\t\tif assert.Equal(t, 1, len(transfers)) {\n\t\t\tassert.Equal(t, transfers[0].ID, transfer.GetID())\n\t\t\tassert.Equal(t, int64(123), transfer.InitialSize)\n\t\t\terr = transfer.Close()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, 0, len(connection.GetTransfers()))\n\t\t}\n\t\t// test PROPPATCH date parsing error\n\t\tpstats, err := transfer.Patch([]webdav.Proppatch{\n\t\t\t{\n\t\t\t\tProps: []webdav.Property{\n\t\t\t\t\t{\n\t\t\t\t\t\tXMLName: xml.Name{\n\t\t\t\t\t\t\tSpace: \"DAV\",\n\t\t\t\t\t\t\tLocal: \"getlastmodified\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tInnerXML: []byte(`Wid, 04 Nov 2020 13:25:51 GMT`),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tfor _, pstat := range pstats {\n\t\t\tassert.Equal(t, http.StatusForbidden, pstat.Status)\n\t\t}\n\n\t\terr = os.Remove(f.Name())\n\t\tassert.NoError(t, err)\n\t\t// the file is deleted PROPPATCH should fail\n\t\tpstats, err = transfer.Patch([]webdav.Proppatch{\n\t\t\t{\n\t\t\t\tProps: []webdav.Property{\n\t\t\t\t\t{\n\t\t\t\t\t\tXMLName: xml.Name{\n\t\t\t\t\t\t\tSpace: \"DAV\",\n\t\t\t\t\t\t\tLocal: \"getlastmodified\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tInnerXML: []byte(`Wed, 04 Nov 2020 13:25:51 GMT`),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tfor _, pstat := range pstats {\n\t\t\tassert.Equal(t, http.StatusForbidden, pstat.Status)\n\t\t}\n\t}\n}\n\nfunc TestCheckRequestMethodWithPrefix(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: filepath.Clean(os.TempDir()),\n\t\t\tPermissions: map[string][]string{\n\t\t\t\t\"/\": {dataprovider.PermAny},\n\t\t\t},\n\t\t},\n\t}\n\tfs := vfs.NewOsFs(\"connID\", user.HomeDir, \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, \"\", \"\", user),\n\t}\n\tserver := webDavServer{\n\t\tbinding: Binding{\n\t\t\tPrefix: \"/dav\",\n\t\t},\n\t}\n\treq, err := http.NewRequest(http.MethodGet, \"/../dav\", nil)\n\trequire.NoError(t, err)\n\tserver.checkRequestMethod(context.Background(), req, connection)\n\trequire.Equal(t, \"PROPFIND\", req.Method)\n\trequire.Equal(t, \"1\", req.Header.Get(\"Depth\"))\n}\n\nfunc TestContentType(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfs := vfs.NewOsFs(\"connID\", user.HomeDir, \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, \"\", \"\", user),\n\t}\n\ttestFilePath := filepath.Join(user.HomeDir, testFile)\n\tctx := context.Background()\n\tbaseTransfer := common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+\".unknown\",\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\tfs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil, nil)\n\terr := os.WriteFile(testFilePath, []byte(\"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tdavFile := newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.Fs = fs\n\tfi, err := davFile.Stat()\n\tif assert.NoError(t, err) {\n\t\tctype, err := fi.(*webDavFileInfo).ContentType(ctx)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"application/custom-mime\", ctype)\n\t}\n\t_, err = davFile.Readdir(-1)\n\tassert.ErrorIs(t, err, webdav.ErrNotImplemented)\n\t_, err = davFile.ReadDir()\n\tassert.Error(t, err)\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+\".unknown1\",\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.Fs = vfs.NewOsFs(\"id\", user.HomeDir, \"\", nil)\n\tfi, err = davFile.Stat()\n\tif assert.NoError(t, err) {\n\t\tctype, err := fi.(*webDavFileInfo).ContentType(ctx)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"text/plain; charset=utf-8\", ctype)\n\t}\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.Fs = vfs.NewOsFs(\"id\", user.HomeDir, \"\", nil)\n\tfi, err = davFile.Stat()\n\tif assert.NoError(t, err) {\n\t\tctype, err := fi.(*webDavFileInfo).ContentType(ctx)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"application/octet-stream\", ctype)\n\t}\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tfor i := 0; i < 2; i++ {\n\t\t// the second time the cache will be used\n\t\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+\".custom\",\n\t\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\t\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\t\tdavFile.Fs = vfs.NewOsFs(\"id\", user.HomeDir, \"\", nil)\n\t\tfi, err = davFile.Stat()\n\t\tif assert.NoError(t, err) {\n\t\t\tctype, err := fi.(*webDavFileInfo).ContentType(ctx)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, \"text/plain; charset=utf-8\", ctype)\n\t\t}\n\t\terr = davFile.Close()\n\t\tassert.NoError(t, err)\n\t}\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+\".sftpgo\",\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\tfs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil, os.ErrInvalid)\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.Fs = fs\n\tfi, err = davFile.Stat()\n\tif assert.NoError(t, err) {\n\t\tctype, err := fi.(*webDavFileInfo).ContentType(ctx)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"application/sftpgo\", ctype)\n\t}\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+\".unknown2\",\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\tfs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil, os.ErrInvalid)\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.Fs = fs\n\tfi, err = davFile.Stat()\n\tif assert.NoError(t, err) {\n\t\tctype, err := fi.(*webDavFileInfo).ContentType(ctx)\n\t\tassert.EqualError(t, err, webdav.ErrNotImplemented.Error(), \"unexpected content type %q\", ctype)\n\t}\n\tcache := mimeCache{\n\t\tmaxSize:   10,\n\t\tmimeTypes: map[string]string{},\n\t}\n\tcache.addMimeToCache(\"\", \"\")\n\tcache.RLock()\n\tassert.Len(t, cache.mimeTypes, 0)\n\tcache.RUnlock()\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestTransferReadWriteErrors(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfs := vfs.NewOsFs(\"connID\", user.HomeDir, \"\", nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, \"\", \"\", user),\n\t}\n\ttestFilePath := filepath.Join(user.HomeDir, testFile)\n\tbaseTransfer := common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferUpload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\tdavFile := newWebDavFile(baseTransfer, nil, nil)\n\tp := make([]byte, 1)\n\t_, err := davFile.Read(p)\n\tassert.EqualError(t, err, common.ErrOpUnsupported.Error())\n\n\tr, w, err := pipeat.Pipe()\n\tassert.NoError(t, err)\n\tdavFile = newWebDavFile(baseTransfer, nil, vfs.NewPipeReader(r))\n\tdavFile.Connection.RemoveTransfer(davFile.BaseTransfer)\n\tdavFile = newWebDavFile(baseTransfer, vfs.NewPipeWriter(w), nil)\n\tdavFile.Connection.RemoveTransfer(davFile.BaseTransfer)\n\terr = r.Close()\n\tassert.NoError(t, err)\n\terr = w.Close()\n\tassert.NoError(t, err)\n\terr = davFile.BaseTransfer.Close()\n\tassert.Error(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\t_, err = davFile.Read(p)\n\tassert.True(t, fs.IsNotExist(err))\n\t_, err = davFile.Stat()\n\tassert.True(t, fs.IsNotExist(err))\n\terr = davFile.Close()\n\tassert.Error(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\terr = os.WriteFile(testFilePath, []byte(\"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tf, err := os.Open(testFilePath)\n\tif assert.NoError(t, err) {\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t}\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.reader = f\n\terr = davFile.Close()\n\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\terr = davFile.Close()\n\tassert.EqualError(t, err, common.ErrTransferClosed.Error())\n\t_, err = davFile.Read(p)\n\tassert.Error(t, err)\n\tinfo, err := davFile.Stat()\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, int64(0), info.Size())\n\t}\n\terr = davFile.Close()\n\tassert.Error(t, err)\n\n\tr, w, err = pipeat.Pipe()\n\tassert.NoError(t, err)\n\tmockFs := newMockOsFs(false, fs.ConnectionID(), user.HomeDir, r, nil)\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, mockFs, dataprovider.TransferQuota{})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\n\twriteContent := []byte(\"content\\r\\n\")\n\tgo func() {\n\t\tn, err := w.Write(writeContent)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, len(writeContent), n)\n\t\terr = w.Close()\n\t\tassert.NoError(t, err)\n\t}()\n\n\tp = make([]byte, 64)\n\tn, err := davFile.Read(p)\n\tassert.EqualError(t, err, io.EOF.Error())\n\tassert.Equal(t, len(writeContent), n)\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.writer = f\n\terr = davFile.Close()\n\tassert.EqualError(t, err, common.ErrGenericFailure.Error())\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestTransferSeek(t *testing.T) {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir: filepath.Clean(os.TempDir()),\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tfs := newMockOsFs(true, \"connID\", user.HomeDir, nil, nil)\n\tconnection := &Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, \"\", \"\", user),\n\t}\n\ttestFilePath := filepath.Join(user.HomeDir, testFile)\n\ttestFileContents := []byte(\"content\")\n\tbaseTransfer := common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferUpload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\tdavFile := newWebDavFile(baseTransfer, nil, nil)\n\t_, err := davFile.Seek(0, io.SeekStart)\n\tassert.EqualError(t, err, common.ErrOpUnsupported.Error())\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\t_, err = davFile.Seek(0, io.SeekCurrent)\n\tassert.True(t, fs.IsNotExist(err))\n\tdavFile.Connection.RemoveTransfer(davFile.BaseTransfer)\n\n\terr = os.WriteFile(testFilePath, testFileContents, os.ModePerm)\n\tassert.NoError(t, err)\n\tf, err := os.Open(testFilePath)\n\tif assert.NoError(t, err) {\n\t\terr = f.Close()\n\t\tassert.NoError(t, err)\n\t}\n\tbaseTransfer = common.NewBaseTransfer(f, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\t_, err = davFile.Seek(0, io.SeekStart)\n\tassert.Error(t, err)\n\tdavFile.Connection.RemoveTransfer(davFile.BaseTransfer)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tres, err := davFile.Seek(0, io.SeekStart)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), res)\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\tdavFile.Connection.RemoveTransfer(davFile.BaseTransfer)\n\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tres, err = davFile.Seek(0, io.SeekEnd)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(len(testFileContents)), res)\n\terr = davFile.updateStatInfo()\n\tassert.NoError(t, err)\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+\"1\", testFilePath+\"1\", testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\t_, err = davFile.Seek(0, io.SeekEnd)\n\tassert.True(t, fs.IsNotExist(err))\n\tdavFile.Connection.RemoveTransfer(davFile.BaseTransfer)\n\n\tfs = vfs.NewOsFs(fs.ConnectionID(), user.GetHomeDir(), \"\", nil)\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+\"1\", testFilePath+\"1\", testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\t_, err = davFile.Seek(0, io.SeekEnd)\n\tassert.True(t, fs.IsNotExist(err))\n\tdavFile.Connection.RemoveTransfer(davFile.BaseTransfer)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.reader = f\n\tr, _, err := pipeat.Pipe()\n\tassert.NoError(t, err)\n\tdavFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), r, nil)\n\tres, err = davFile.Seek(2, io.SeekStart)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(2), res)\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tr, _, err = pipeat.Pipe()\n\tassert.NoError(t, err)\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), r, nil)\n\tres, err = davFile.Seek(2, io.SeekEnd)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(5), res)\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tbaseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+\"1\", testFilePath+\"1\", testFile,\n\t\tcommon.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})\n\n\tdavFile = newWebDavFile(baseTransfer, nil, nil)\n\tdavFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil, nil)\n\tres, err = davFile.Seek(2, io.SeekEnd)\n\tassert.True(t, fs.IsNotExist(err))\n\tassert.Equal(t, int64(0), res)\n\terr = davFile.Close()\n\tassert.NoError(t, err)\n\n\tassert.Len(t, common.Connections.GetStats(\"\"), 0)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicUsersCache(t *testing.T) {\n\tusername := \"webdav_internal_test\"\n\tpassword := \"pwd\"\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:       username,\n\t\t\tPassword:       password,\n\t\t\tHomeDir:        filepath.Join(os.TempDir(), username),\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: 0,\n\t\t},\n\t}\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\terr := dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\n\tc := &Configuration{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort: 9000,\n\t\t\t},\n\t\t},\n\t\tCache: Cache{\n\t\t\tUsers: UsersCacheConfig{\n\t\t\t\tMaxSize:        50,\n\t\t\t\tExpirationTime: 1,\n\t\t\t},\n\t\t},\n\t}\n\tdataprovider.InitializeWebDAVUserCache(c.Cache.Users.MaxSize)\n\tserver := webDavServer{\n\t\tconfig:  c,\n\t\tbinding: c.Bindings[0],\n\t}\n\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user.Username), nil)\n\tassert.NoError(t, err)\n\n\tipAddr := \"127.0.0.1\"\n\n\t_, _, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled\n\tassert.Error(t, err)\n\n\tnow := time.Now()\n\treq.SetBasicAuth(username, password)\n\t_, isCached, _, loginMethod, err := server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)\n\t// now the user should be cached\n\tcachedUser, ok := dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.False(t, cachedUser.IsExpired())\n\t\tassert.True(t, cachedUser.Expiration.After(now.Add(time.Duration(c.Cache.Users.ExpirationTime)*time.Minute)))\n\t\t// authenticate must return the cached user now\n\t\tauthUser, isCached, _, _, err := server.authenticate(req, ipAddr)\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, isCached)\n\t\tassert.Equal(t, cachedUser.User, authUser)\n\t}\n\t// a wrong password must fail\n\treq.SetBasicAuth(username, \"wrong\")\n\t_, _, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled\n\tassert.EqualError(t, err, dataprovider.ErrInvalidCredentials.Error())\n\treq.SetBasicAuth(username, password)\n\n\t// force cached user expiration\n\tcachedUser.Expiration = now\n\tdataprovider.CacheWebDAVUser(cachedUser)\n\tcachedUser, ok = dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.True(t, cachedUser.IsExpired())\n\t}\n\t// now authenticate should get the user from the data provider and update the cache\n\t_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)\n\tcachedUser, ok = dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.False(t, cachedUser.IsExpired())\n\t}\n\t// cache is not invalidated after a user modification if the fs does not change\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t_, ok = dataprovider.GetCachedWebDAVUser(username)\n\tassert.True(t, ok)\n\tfolderName := \"testFolder\"\n\tf := &vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: filepath.Join(os.TempDir(), \"mapped\"),\n\t}\n\terr = dataprovider.AddFolder(f, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t})\n\n\terr = dataprovider.UpdateUser(&user, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t_, ok = dataprovider.GetCachedWebDAVUser(username)\n\tassert.False(t, ok)\n\n\t_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)\n\t_, ok = dataprovider.GetCachedWebDAVUser(username)\n\tassert.True(t, ok)\n\t// cache is invalidated after user deletion\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t_, ok = dataprovider.GetCachedWebDAVUser(username)\n\tassert.False(t, ok)\n\n\terr = dataprovider.DeleteFolder(folderName, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = os.RemoveAll(u.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestCachedUserWithFolders(t *testing.T) {\n\tusername := \"webdav_internal_folder_test\"\n\tpassword := \"dav_pwd\"\n\tfolderName := \"test_folder\"\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:       username,\n\t\t\tPassword:       password,\n\t\t\tHomeDir:        filepath.Join(os.TempDir(), username),\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: 0,\n\t\t},\n\t}\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: \"/vpath\",\n\t})\n\tf := &vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: filepath.Join(os.TempDir(), folderName),\n\t}\n\terr := dataprovider.AddFolder(f, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\n\tc := &Configuration{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort: 9000,\n\t\t\t},\n\t\t},\n\t\tCache: Cache{\n\t\t\tUsers: UsersCacheConfig{\n\t\t\t\tMaxSize:        50,\n\t\t\t\tExpirationTime: 1,\n\t\t\t},\n\t\t},\n\t}\n\tdataprovider.InitializeWebDAVUserCache(c.Cache.Users.MaxSize)\n\tserver := webDavServer{\n\t\tconfig:  c,\n\t\tbinding: c.Bindings[0],\n\t}\n\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user.Username), nil)\n\tassert.NoError(t, err)\n\n\tipAddr := \"127.0.0.1\"\n\n\t_, _, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled\n\tassert.Error(t, err)\n\n\tnow := time.Now()\n\treq.SetBasicAuth(username, password)\n\t_, isCached, _, loginMethod, err := server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)\n\t// now the user should be cached\n\tcachedUser, ok := dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.False(t, cachedUser.IsExpired())\n\t\tassert.True(t, cachedUser.Expiration.After(now.Add(time.Duration(c.Cache.Users.ExpirationTime)*time.Minute)))\n\t\t// authenticate must return the cached user now\n\t\tauthUser, isCached, _, _, err := server.authenticate(req, ipAddr)\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, isCached)\n\t\tassert.Equal(t, cachedUser.User, authUser)\n\t}\n\n\tfolder, err := dataprovider.GetFolderByName(folderName)\n\tassert.NoError(t, err)\n\t// updating a used folder should invalidate the cache only if the fs changed\n\terr = dataprovider.UpdateFolder(&folder, folder.Users, folder.Groups, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\t_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.True(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)\n\tcachedUser, ok = dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.False(t, cachedUser.IsExpired())\n\t}\n\t// changing the folder path should invalidate the cache\n\tfolder.MappedPath = filepath.Join(os.TempDir(), \"anotherpath\")\n\terr = dataprovider.UpdateFolder(&folder, folder.Users, folder.Groups, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)\n\tcachedUser, ok = dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.False(t, cachedUser.IsExpired())\n\t}\n\n\terr = dataprovider.DeleteFolder(folderName, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t// removing a used folder should invalidate the cache\n\t_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)\n\tcachedUser, ok = dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.False(t, cachedUser.IsExpired())\n\t}\n\n\terr = dataprovider.DeleteUser(user.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t_, ok = dataprovider.GetCachedWebDAVUser(username)\n\tassert.False(t, ok)\n\n\terr = os.RemoveAll(u.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = os.RemoveAll(folder.MappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestUsersCacheSizeAndExpiration(t *testing.T) {\n\tusername := \"webdav_internal_test\"\n\tpassword := \"pwd\"\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tHomeDir:        filepath.Join(os.TempDir(), username),\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: 0,\n\t\t},\n\t}\n\tu.Username = username + \"1\"\n\tu.Password = password + \"1\"\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\terr := dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser1, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tu.Username = username + \"2\"\n\tu.Password = password + \"2\"\n\terr = dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser2, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tu.Username = username + \"3\"\n\tu.Password = password + \"3\"\n\terr = dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser3, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tu.Username = username + \"4\"\n\tu.Password = password + \"4\"\n\terr = dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser4, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\n\tc := &Configuration{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort: 9000,\n\t\t\t},\n\t\t},\n\t\tCache: Cache{\n\t\t\tUsers: UsersCacheConfig{\n\t\t\t\tMaxSize:        3,\n\t\t\t\tExpirationTime: 1,\n\t\t\t},\n\t\t},\n\t}\n\tdataprovider.InitializeWebDAVUserCache(c.Cache.Users.MaxSize)\n\tserver := webDavServer{\n\t\tconfig:  c,\n\t\tbinding: c.Bindings[0],\n\t}\n\n\tipAddr := \"127.0.1.1\"\n\treq, err := http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user1.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user1.Username, password+\"1\")\n\t_, isCached, _, loginMehod, err := server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user2.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user2.Username, password+\"2\")\n\t_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user3.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user3.Username, password+\"3\")\n\t_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\n\t// the first 3 users are now cached\n\t_, ok := dataprovider.GetCachedWebDAVUser(user1.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user3.Username)\n\tassert.True(t, ok)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user4.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user4.Username, password+\"4\")\n\t_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\t// user1, the first cached, should be removed now\n\t_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)\n\tassert.False(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user3.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)\n\tassert.True(t, ok)\n\n\t// a sleep ensures that expiration times are different\n\ttime.Sleep(20 * time.Millisecond)\n\t// user1 logins, user2 should be removed\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user1.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user1.Username, password+\"1\")\n\t_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)\n\tassert.False(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user3.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)\n\tassert.True(t, ok)\n\n\t// a sleep ensures that expiration times are different\n\ttime.Sleep(20 * time.Millisecond)\n\t// user2 logins, user3 should be removed\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user2.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user2.Username, password+\"2\")\n\t_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user3.Username)\n\tassert.False(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)\n\tassert.True(t, ok)\n\n\t// a sleep ensures that expiration times are different\n\ttime.Sleep(20 * time.Millisecond)\n\t// user3 logins, user4 should be removed\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user3.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user3.Username, password+\"3\")\n\t_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)\n\tassert.False(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user3.Username)\n\tassert.True(t, ok)\n\n\t// now remove user1 after an update\n\tuser1.HomeDir += \"_mod\"\n\terr = dataprovider.UpdateUser(&user1, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)\n\tassert.False(t, ok)\n\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user4.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user4.Username, password+\"4\")\n\t_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\n\t// a sleep ensures that expiration times are different\n\ttime.Sleep(20 * time.Millisecond)\n\treq, err = http.NewRequest(http.MethodGet, fmt.Sprintf(\"/%v\", user1.Username), nil)\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(user1.Username, password+\"1\")\n\t_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)\n\tassert.NoError(t, err)\n\tassert.False(t, isCached)\n\tassert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)\n\tassert.False(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user3.Username)\n\tassert.True(t, ok)\n\t_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)\n\tassert.True(t, ok)\n\n\terr = dataprovider.DeleteUser(user1.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(user2.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(user3.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\terr = dataprovider.DeleteUser(user4.Username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\terr = os.RemoveAll(u.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUserCacheIsolation(t *testing.T) {\n\tdataprovider.InitializeWebDAVUserCache(10)\n\tusername := \"webdav_internal_cache_test\"\n\tpassword := \"dav_pwd\"\n\tu := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:       username,\n\t\t\tPassword:       password,\n\t\t\tHomeDir:        filepath.Join(os.TempDir(), username),\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: 0,\n\t\t},\n\t}\n\tu.Permissions = make(map[string][]string)\n\tu.Permissions[\"/\"] = []string{dataprovider.PermAny}\n\terr := dataprovider.AddUser(&u, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tuser, err := dataprovider.UserExists(u.Username, \"\")\n\tassert.NoError(t, err)\n\tcachedUser := &dataprovider.CachedUser{\n\t\tUser:       user,\n\t\tExpiration: time.Now().Add(24 * time.Hour),\n\t\tPassword:   password,\n\t\tLockSystem: webdav.NewMemLS(),\n\t}\n\tcachedUser.User.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(\"test secret\")\n\tcachedUser.User.FsConfig.S3Config.SSECustomerKey = kms.NewPlainSecret(\"test key\")\n\terr = cachedUser.User.FsConfig.S3Config.AccessSecret.Encrypt()\n\tassert.NoError(t, err)\n\terr = cachedUser.User.FsConfig.S3Config.SSECustomerKey.Encrypt()\n\tassert.NoError(t, err)\n\tdataprovider.CacheWebDAVUser(cachedUser)\n\tcachedUser, ok := dataprovider.GetCachedWebDAVUser(username)\n\n\tif assert.True(t, ok) {\n\t\t_, err = cachedUser.User.GetFilesystem(\"\")\n\t\tassert.NoError(t, err)\n\t\t// the filesystem is now cached\n\t}\n\tcachedUser, ok = dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.True(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted())\n\t\terr = cachedUser.User.FsConfig.S3Config.AccessSecret.Decrypt()\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, cachedUser.User.FsConfig.S3Config.SSECustomerKey.IsEncrypted())\n\t\terr = cachedUser.User.FsConfig.S3Config.SSECustomerKey.Decrypt()\n\t\tassert.NoError(t, err)\n\t\tcachedUser.User.FsConfig.Provider = sdk.S3FilesystemProvider\n\t\t_, err = cachedUser.User.GetFilesystem(\"\")\n\t\tassert.Error(t, err, \"we don't have to get the previously cached filesystem!\")\n\t}\n\tcachedUser, ok = dataprovider.GetCachedWebDAVUser(username)\n\tif assert.True(t, ok) {\n\t\tassert.Equal(t, sdk.LocalFilesystemProvider, cachedUser.User.FsConfig.Provider)\n\t\tassert.False(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted())\n\t\tassert.False(t, cachedUser.User.FsConfig.S3Config.SSECustomerKey.IsEncrypted())\n\t}\n\n\terr = dataprovider.DeleteUser(username, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\t_, ok = dataprovider.GetCachedWebDAVUser(username)\n\tassert.False(t, ok)\n}\n\nfunc TestRecoverer(t *testing.T) {\n\tc := &Configuration{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort: 9000,\n\t\t\t},\n\t\t},\n\t}\n\tserver := webDavServer{\n\t\tconfig:  c,\n\t\tbinding: c.Bindings[0],\n\t}\n\trr := httptest.NewRecorder()\n\tserver.ServeHTTP(rr, nil)\n\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n}\n\nfunc TestMimeCache(t *testing.T) {\n\tcache := mimeCache{\n\t\tmaxSize:   0,\n\t\tmimeTypes: make(map[string]string),\n\t}\n\tcache.addMimeToCache(\".zip\", \"application/zip\")\n\tmtype := cache.getMimeFromCache(\".zip\")\n\tassert.Equal(t, \"\", mtype)\n\tcache.maxSize = 1\n\tcache.addMimeToCache(\".zip\", \"application/zip\")\n\tmtype = cache.getMimeFromCache(\".zip\")\n\tassert.Equal(t, \"application/zip\", mtype)\n\tcache.addMimeToCache(\".jpg\", \"image/jpeg\")\n\tmtype = cache.getMimeFromCache(\".jpg\")\n\tassert.Equal(t, \"\", mtype)\n}\n\nfunc TestVerifyTLSConnection(t *testing.T) {\n\toldCertMgr := certMgr\n\n\tcaCrlPath := filepath.Join(os.TempDir(), \"testcrl.crt\")\n\tcertPath := filepath.Join(os.TempDir(), \"test.crt\")\n\tkeyPath := filepath.Join(os.TempDir(), \"test.key\")\n\terr := os.WriteFile(caCrlPath, []byte(caCRL), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(certPath, []byte(webDavCert), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(keyPath, []byte(webDavKey), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tkeyPairs := []common.TLSKeyPair{\n\t\t{\n\t\t\tCert: certPath,\n\t\t\tKey:  keyPath,\n\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t},\n\t}\n\tcertMgr, err = common.NewCertManager(keyPairs, \"\", \"webdav_test\")\n\tassert.NoError(t, err)\n\n\tcertMgr.SetCARevocationLists([]string{caCrlPath})\n\terr = certMgr.LoadCRLs()\n\tassert.NoError(t, err)\n\n\tcrt, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\tx509crt, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tserver := webDavServer{}\n\tstate := tls.ConnectionState{\n\t\tPeerCertificates: []*x509.Certificate{x509crt},\n\t}\n\n\terr = server.verifyTLSConnection(state)\n\tassert.Error(t, err) // no verified certification chain\n\n\tcrt, err = tls.X509KeyPair([]byte(caCRT), []byte(caKey))\n\tassert.NoError(t, err)\n\n\tx509CAcrt, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tstate.VerifiedChains = append(state.VerifiedChains, []*x509.Certificate{x509crt, x509CAcrt})\n\terr = server.verifyTLSConnection(state)\n\tassert.NoError(t, err)\n\n\tcrt, err = tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))\n\tassert.NoError(t, err)\n\tx509crtRevoked, err := x509.ParseCertificate(crt.Certificate[0])\n\tassert.NoError(t, err)\n\n\tstate.VerifiedChains = append(state.VerifiedChains, []*x509.Certificate{x509crtRevoked, x509CAcrt})\n\tstate.PeerCertificates = []*x509.Certificate{x509crtRevoked}\n\terr = server.verifyTLSConnection(state)\n\tassert.EqualError(t, err, common.ErrCrtRevoked.Error())\n\n\terr = os.Remove(caCrlPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(certPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n\n\tcertMgr = oldCertMgr\n}\n\nfunc TestMisc(t *testing.T) {\n\toldCertMgr := certMgr\n\n\tcertMgr = nil\n\terr := ReloadCertificateMgr()\n\tassert.Nil(t, err)\n\tval := getConfigPath(\"\", \".\")\n\tassert.Empty(t, val)\n\n\tcertMgr = oldCertMgr\n}\n\nfunc TestParseTime(t *testing.T) {\n\tres, err := parseTime(\"Sat, 4 Feb 2023 17:00:50 GMT\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, int64(1675530050), res.Unix())\n\tres, err = parseTime(\"Wed, 04 Nov 2020 13:25:51 GMT\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, int64(1604496351), res.Unix())\n}\n\nfunc TestConfigsFromProvider(t *testing.T) {\n\tconfigDir := \".\"\n\terr := dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tc := Configuration{\n\t\tBindings: []Binding{\n\t\t\t{\n\t\t\t\tPort: 1234,\n\t\t\t},\n\t\t},\n\t}\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tconfigs := dataprovider.Configs{\n\t\tACME: &dataprovider.ACMEConfigs{\n\t\t\tDomain:          \"domain.com\",\n\t\t\tEmail:           \"info@domain.com\",\n\t\t\tHTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: 80},\n\t\t\tProtocols:       7,\n\t\t},\n\t}\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tutil.CertsBasePath = \"\"\n\t// crt and key empty\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tutil.CertsBasePath = filepath.Clean(os.TempDir())\n\t// crt not found\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs := c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\tcrtPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+\".crt\")\n\terr = os.WriteFile(crtPath, nil, 0666)\n\tassert.NoError(t, err)\n\t// key not found\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\tkeyPath := filepath.Join(util.CertsBasePath, util.SanitizeDomain(configs.ACME.Domain)+\".key\")\n\terr = os.WriteFile(keyPath, nil, 0666)\n\tassert.NoError(t, err)\n\t// acme cert used\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Equal(t, configs.ACME.Domain, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 1)\n\tassert.True(t, c.Bindings[0].EnableHTTPS)\n\t// protocols does not match\n\tconfigs.ACME.Protocols = 3\n\terr = dataprovider.UpdateConfigs(&configs, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\tc.acmeDomain = \"\"\n\terr = c.loadFromProvider()\n\tassert.NoError(t, err)\n\tassert.Empty(t, c.acmeDomain)\n\tkeyPairs = c.getKeyPairs(configDir)\n\tassert.Len(t, keyPairs, 0)\n\n\terr = os.Remove(crtPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(keyPath)\n\tassert.NoError(t, err)\n\tutil.CertsBasePath = \"\"\n\terr = dataprovider.UpdateConfigs(nil, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n}\n\nfunc TestGetCacheExpirationTime(t *testing.T) {\n\tc := UsersCacheConfig{}\n\tassert.True(t, c.getExpirationTime().IsZero())\n\tc.ExpirationTime = 1\n\tassert.False(t, c.getExpirationTime().IsZero())\n}\n\nfunc TestBindingGetAddress(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tbinding Binding\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"IP address with port\",\n\t\t\tbinding: Binding{Address: \"127.0.0.1\", Port: 8080},\n\t\t\twant:    \"127.0.0.1:8080\",\n\t\t},\n\t\t{\n\t\t\tname:    \"Unix socket path (no port)\",\n\t\t\tbinding: Binding{Address: \"/tmp/app.sock\", Port: 0},\n\t\t\twant:    \"/tmp/app.sock\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.binding.GetAddress(); got != tt.want {\n\t\t\t\tt.Errorf(\"GetAddress() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBindingIsValid(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tbinding Binding\n\t\twant    bool\n\t}{\n\t\t{\n\t\t\tname:    \"Valid: Positive port\",\n\t\t\tbinding: Binding{Address: \"127.0.0.1\", Port: 10080},\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Valid: Absolute path on Unix (non-Windows)\",\n\t\t\tbinding: Binding{Address: \"/var/run/app.sock\", Port: 0},\n\t\t\t// This test outcome is dynamic based on the OS\n\t\t\twant: runtime.GOOS != osWindows,\n\t\t},\n\t\t{\n\t\t\tname:    \"Invalid: Port 0 and relative path\",\n\t\t\tbinding: Binding{Address: \"relative/path\", Port: 0},\n\t\t\twant:    false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Invalid: Empty address and port 0\",\n\t\t\tbinding: Binding{Address: \"\", Port: 0},\n\t\t\twant:    false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.binding.IsValid(); got != tt.want {\n\t\t\t\tt.Errorf(\"IsValid() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/webdavd/mimecache.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage webdavd\n\nimport \"sync\"\n\ntype mimeCache struct {\n\tmaxSize int\n\tsync.RWMutex\n\tmimeTypes map[string]string\n}\n\nvar (\n\tmimeTypeCache         mimeCache\n\tcustomMimeTypeMapping map[string]string\n)\n\nfunc (c *mimeCache) addMimeToCache(key, value string) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tif key == \"\" || value == \"\" {\n\t\treturn\n\t}\n\n\tif len(c.mimeTypes) >= c.maxSize {\n\t\treturn\n\t}\n\tc.mimeTypes[key] = value\n}\n\nfunc (c *mimeCache) getMimeFromCache(key string) string {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tif val, ok := c.mimeTypes[key]; ok {\n\t\treturn val\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/webdavd/server.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage webdavd\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/drakkan/webdav\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"github.com/rs/cors\"\n\t\"github.com/rs/xid\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/sftpgo/sdk/plugin/notifier\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/metric\"\n\t\"github.com/drakkan/sftpgo/v2/internal/plugin\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/version\"\n)\n\ntype webDavServer struct {\n\tconfig  *Configuration\n\tbinding Binding\n}\n\nfunc (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {\n\thandler := compressor.Handler(s)\n\thttpServer := &http.Server{\n\t\tReadHeaderTimeout: 30 * time.Second,\n\t\tIdleTimeout:       60 * time.Second,\n\t\tMaxHeaderBytes:    1 << 16, // 64KB\n\t\tErrorLog:          log.New(&logger.StdLoggerWrapper{Sender: logSender}, \"\", 0),\n\t}\n\tif s.config.Cors.Enabled {\n\t\tc := cors.New(cors.Options{\n\t\t\tAllowedOrigins:       util.RemoveDuplicates(s.config.Cors.AllowedOrigins, true),\n\t\t\tAllowedMethods:       util.RemoveDuplicates(s.config.Cors.AllowedMethods, true),\n\t\t\tAllowedHeaders:       util.RemoveDuplicates(s.config.Cors.AllowedHeaders, true),\n\t\t\tExposedHeaders:       util.RemoveDuplicates(s.config.Cors.ExposedHeaders, true),\n\t\t\tMaxAge:               s.config.Cors.MaxAge,\n\t\t\tAllowCredentials:     s.config.Cors.AllowCredentials,\n\t\t\tOptionsPassthrough:   s.config.Cors.OptionsPassthrough,\n\t\t\tOptionsSuccessStatus: s.config.Cors.OptionsSuccessStatus,\n\t\t\tAllowPrivateNetwork:  s.config.Cors.AllowPrivateNetwork,\n\t\t})\n\t\thandler = c.Handler(handler)\n\t}\n\thttpServer.Handler = handler\n\tif certMgr != nil && s.binding.EnableHTTPS {\n\t\tserviceStatus.Bindings = append(serviceStatus.Bindings, s.binding)\n\t\tcertID := common.DefaultTLSKeyPaidID\n\t\tif getConfigPath(s.binding.CertificateFile, \"\") != \"\" && getConfigPath(s.binding.CertificateKeyFile, \"\") != \"\" {\n\t\t\tcertID = s.binding.GetAddress()\n\t\t}\n\t\thttpServer.TLSConfig = &tls.Config{\n\t\t\tGetCertificate: certMgr.GetCertificateFunc(certID),\n\t\t\tMinVersion:     util.GetTLSVersion(s.binding.MinTLSVersion),\n\t\t\tNextProtos:     util.GetALPNProtocols(s.binding.Protocols),\n\t\t\tCipherSuites:   util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),\n\t\t}\n\t\tlogger.Debug(logSender, \"\", \"configured TLS cipher suites for binding %q: %v, certID: %v\",\n\t\t\ts.binding.GetAddress(), httpServer.TLSConfig.CipherSuites, certID)\n\t\tif s.binding.isMutualTLSEnabled() {\n\t\t\thttpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()\n\t\t\thttpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection\n\t\t\tswitch s.binding.ClientAuthType {\n\t\t\tcase 1:\n\t\t\t\thttpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert\n\t\t\tcase 2:\n\t\t\t\thttpServer.TLSConfig.ClientAuth = tls.VerifyClientCertIfGiven\n\t\t\t}\n\t\t}\n\t\treturn util.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, true,\n\t\t\ts.binding.listenerWrapper(), logSender)\n\t}\n\ts.binding.EnableHTTPS = false\n\tserviceStatus.Bindings = append(serviceStatus.Bindings, s.binding)\n\treturn util.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, false,\n\t\ts.binding.listenerWrapper(), logSender)\n}\n\nfunc (s *webDavServer) verifyTLSConnection(state tls.ConnectionState) error {\n\tif certMgr != nil {\n\t\tvar clientCrt *x509.Certificate\n\t\tvar clientCrtName string\n\t\tif len(state.PeerCertificates) > 0 {\n\t\t\tclientCrt = state.PeerCertificates[0]\n\t\t\tclientCrtName = clientCrt.Subject.String()\n\t\t}\n\t\tif len(state.VerifiedChains) == 0 {\n\t\t\tif s.binding.ClientAuthType == 2 {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tlogger.Warn(logSender, \"\", \"TLS connection cannot be verified: unable to get verification chain\")\n\t\t\treturn errors.New(\"TLS connection cannot be verified: unable to get verification chain\")\n\t\t}\n\t\tfor _, verifiedChain := range state.VerifiedChains {\n\t\t\tvar caCrt *x509.Certificate\n\t\t\tif len(verifiedChain) > 0 {\n\t\t\t\tcaCrt = verifiedChain[len(verifiedChain)-1]\n\t\t\t}\n\t\t\tif certMgr.IsRevoked(clientCrt, caCrt) {\n\t\t\t\tlogger.Debug(logSender, \"\", \"tls handshake error, client certificate %q has been revoked\", clientCrtName)\n\t\t\t\treturn common.ErrCrtRevoked\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// returns true if we have to handle a HEAD response, for a directory, ourself\nfunc (s *webDavServer) checkRequestMethod(ctx context.Context, r *http.Request, connection *Connection) bool {\n\t// see RFC4918, section 9.4\n\tif r.Method == http.MethodGet || r.Method == http.MethodHead {\n\t\tp := path.Clean(r.URL.Path)\n\t\tif s.binding.Prefix != \"\" {\n\t\t\tp = strings.TrimPrefix(p, s.binding.Prefix)\n\t\t}\n\t\tinfo, err := connection.Stat(ctx, p)\n\t\tif err == nil && info.IsDir() {\n\t\t\tif r.Method == http.MethodHead {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tr.Method = \"PROPFIND\"\n\t\t\tif r.Header.Get(\"Depth\") == \"\" {\n\t\t\t\tr.Header.Add(\"Depth\", \"1\")\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// ServeHTTP implements the http.Handler interface\nfunc (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlogger.Error(logSender, \"\", \"panic in ServeHTTP: %q stack trace: %v\", r, string(debug.Stack()))\n\t\t\thttp.Error(w, common.ErrGenericFailure.Error(), http.StatusInternalServerError)\n\t\t}\n\t}()\n\n\tresponseControllerDeadlines(\n\t\thttp.NewResponseController(w),\n\t\ttime.Now().Add(60*time.Second),\n\t\ttime.Now().Add(60*time.Second),\n\t)\n\tw.Header().Set(\"Server\", version.GetServerVersion(\"/\", false))\n\tipAddr := s.checkRemoteAddress(r)\n\n\tcommon.Connections.AddClientConnection(ipAddr)\n\tdefer common.Connections.RemoveClientConnection(ipAddr)\n\n\tif err := common.Connections.IsNewConnectionAllowed(ipAddr, common.ProtocolWebDAV); err != nil {\n\t\tlogger.Log(logger.LevelDebug, common.ProtocolWebDAV, \"\", \"connection not allowed from ip %q: %v\", ipAddr, err)\n\t\thttp.Error(w, err.Error(), http.StatusServiceUnavailable)\n\t\treturn\n\t}\n\tif common.IsBanned(ipAddr, common.ProtocolWebDAV) {\n\t\thttp.Error(w, common.ErrConnectionDenied.Error(), http.StatusForbidden)\n\t\treturn\n\t}\n\tdelay, err := common.LimitRate(common.ProtocolWebDAV, ipAddr)\n\tif err != nil {\n\t\tdelay += 499999999 * time.Nanosecond\n\t\tw.Header().Set(\"Retry-After\", fmt.Sprintf(\"%.0f\", delay.Seconds()))\n\t\tw.Header().Set(\"X-Retry-In\", delay.String())\n\t\thttp.Error(w, err.Error(), http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tif err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolWebDAV); err != nil {\n\t\thttp.Error(w, common.ErrConnectionDenied.Error(), http.StatusForbidden)\n\t\treturn\n\t}\n\tuser, isCached, lockSystem, loginMethod, err := s.authenticate(r, ipAddr)\n\tif err != nil {\n\t\tif !s.binding.DisableWWWAuthHeader {\n\t\t\tw.Header().Set(\"WWW-Authenticate\", fmt.Sprintf(\"Basic realm=\\\"%s WebDAV\\\"\", version.GetServerVersion(\"_\", false)))\n\t\t}\n\t\thttp.Error(w, fmt.Sprintf(\"Authentication error: %v\", err), http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\tconnectionID, err := s.validateUser(&user, r, loginMethod)\n\tif err != nil {\n\t\t// remove the cached user, we have not yet validated its filesystem\n\t\tdataprovider.RemoveCachedWebDAVUser(user.Username)\n\t\tupdateLoginMetrics(&user, ipAddr, loginMethod, err, r)\n\t\thttp.Error(w, err.Error(), http.StatusForbidden)\n\t\treturn\n\t}\n\n\tif !isCached {\n\t\terr = user.CheckFsRoot(connectionID)\n\t} else {\n\t\t_, err = user.GetFilesystemForPath(\"/\", connectionID)\n\t}\n\tif err != nil {\n\t\terrClose := user.CloseFs()\n\t\tlogger.Warn(logSender, connectionID, \"unable to check fs root: %v close fs error: %v\", err, errClose)\n\t\tupdateLoginMetrics(&user, ipAddr, loginMethod, common.ErrInternalFailure, r)\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbaseConn := common.NewBaseConnection(connectionID, common.ProtocolWebDAV, util.GetHTTPLocalAddress(r),\n\t\tr.RemoteAddr, user)\n\tconnection := newConnection(baseConn, w, r)\n\tif err = common.Connections.Add(connection); err != nil {\n\t\terrClose := user.CloseFs()\n\t\tlogger.Warn(logSender, connectionID, \"unable add connection: %v close fs error: %v\", err, errClose)\n\t\tupdateLoginMetrics(&user, ipAddr, loginMethod, err, r)\n\t\thttp.Error(w, err.Error(), http.StatusTooManyRequests)\n\t\treturn\n\t}\n\tdefer common.Connections.Remove(connection.GetID())\n\n\tupdateLoginMetrics(&user, ipAddr, loginMethod, err, r)\n\n\tctx := context.WithValue(r.Context(), requestIDKey, connectionID)\n\tctx = context.WithValue(ctx, requestStartKey, time.Now())\n\n\tdataprovider.UpdateLastLogin(&user)\n\n\tif s.checkRequestMethod(ctx, r, connection) {\n\t\tw.Header().Set(\"Content-Type\", \"text/xml; charset=utf-8\")\n\t\tw.WriteHeader(http.StatusMultiStatus)\n\t\tw.Write([]byte(\"\")) //nolint:errcheck\n\t\twriteLog(r, http.StatusMultiStatus, nil)\n\t\treturn\n\t}\n\n\thandler := webdav.Handler{\n\t\tPrefix:     s.binding.Prefix,\n\t\tFileSystem: connection,\n\t\tLockSystem: lockSystem,\n\t\tLogger:     writeLog,\n\t}\n\thandler.ServeHTTP(w, r.WithContext(ctx))\n}\n\nfunc (s *webDavServer) getCredentialsAndLoginMethod(r *http.Request) (string, string, string, *x509.Certificate, bool) {\n\tvar tlsCert *x509.Certificate\n\tloginMethod := dataprovider.LoginMethodPassword\n\tusername, password, ok := r.BasicAuth()\n\tif s.binding.isMutualTLSEnabled() && r.TLS != nil {\n\t\tif len(r.TLS.PeerCertificates) > 0 {\n\t\t\ttlsCert = r.TLS.PeerCertificates[0]\n\t\t\tif ok {\n\t\t\t\tloginMethod = dataprovider.LoginMethodTLSCertificateAndPwd\n\t\t\t} else {\n\t\t\t\tloginMethod = dataprovider.LoginMethodTLSCertificate\n\t\t\t\tusername = tlsCert.Subject.CommonName\n\t\t\t\tpassword = \"\"\n\t\t\t}\n\t\t\tok = true\n\t\t}\n\t}\n\treturn username, password, loginMethod, tlsCert, ok\n}\n\nfunc (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.User, bool, webdav.LockSystem, string, error) {\n\tvar user dataprovider.User\n\tvar err error\n\tusername, password, loginMethod, tlsCert, ok := s.getCredentialsAndLoginMethod(r)\n\tif !ok {\n\t\tuser.Username = username\n\t\treturn user, false, nil, loginMethod, common.ErrNoCredentials\n\t}\n\tcachedUser, ok := dataprovider.GetCachedWebDAVUser(username)\n\tif ok {\n\t\tif cachedUser.IsExpired() {\n\t\t\tdataprovider.RemoveCachedWebDAVUser(username)\n\t\t} else {\n\t\t\tif !cachedUser.User.IsTLSVerificationEnabled() {\n\t\t\t\t// for backward compatibility with 2.0.x we only check the password\n\t\t\t\ttlsCert = nil\n\t\t\t\tloginMethod = dataprovider.LoginMethodPassword\n\t\t\t}\n\t\t\tcu, u, err := dataprovider.CheckCachedUserCredentials(cachedUser, password, ip, loginMethod, common.ProtocolWebDAV, tlsCert)\n\t\t\tif err == nil {\n\t\t\t\tif cu != nil {\n\t\t\t\t\treturn cu.User, true, cu.LockSystem, loginMethod, nil\n\t\t\t\t}\n\t\t\t\tlockSystem := webdav.NewMemLS()\n\t\t\t\tcachedUser = &dataprovider.CachedUser{\n\t\t\t\t\tUser:       *u,\n\t\t\t\t\tPassword:   password,\n\t\t\t\t\tLockSystem: lockSystem,\n\t\t\t\t\tExpiration: s.config.Cache.Users.getExpirationTime(),\n\t\t\t\t}\n\t\t\t\tdataprovider.CacheWebDAVUser(cachedUser)\n\t\t\t\treturn cachedUser.User, false, cachedUser.LockSystem, loginMethod, nil\n\t\t\t}\n\t\t\tupdateLoginMetrics(&cachedUser.User, ip, loginMethod, dataprovider.ErrInvalidCredentials, r)\n\t\t\treturn user, false, nil, loginMethod, dataprovider.ErrInvalidCredentials\n\t\t}\n\t}\n\tuser, loginMethod, err = dataprovider.CheckCompositeCredentials(username, password, ip, loginMethod,\n\t\tcommon.ProtocolWebDAV, tlsCert)\n\tif err != nil {\n\t\tuser.Username = username\n\t\tupdateLoginMetrics(&user, ip, loginMethod, err, r)\n\t\treturn user, false, nil, loginMethod, dataprovider.ErrInvalidCredentials\n\t}\n\tlockSystem := webdav.NewMemLS()\n\tcachedUser = &dataprovider.CachedUser{\n\t\tUser:       user,\n\t\tPassword:   password,\n\t\tLockSystem: lockSystem,\n\t\tExpiration: s.config.Cache.Users.getExpirationTime(),\n\t}\n\tdataprovider.CacheWebDAVUser(cachedUser)\n\treturn user, false, lockSystem, loginMethod, nil\n}\n\nfunc (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, loginMethod string) (string, error) {\n\tconnID := xid.New().String()\n\tconnectionID := fmt.Sprintf(\"%v_%v\", common.ProtocolWebDAV, connID)\n\n\tif !filepath.IsAbs(user.HomeDir) {\n\t\tlogger.Warn(logSender, connectionID, \"user %q has an invalid home dir: %q. Home dir must be an absolute path, login not allowed\",\n\t\t\tuser.Username, user.HomeDir)\n\t\treturn connID, fmt.Errorf(\"cannot login user with invalid home dir: %q\", user.HomeDir)\n\t}\n\tif slices.Contains(user.Filters.DeniedProtocols, common.ProtocolWebDAV) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, protocol DAV is not allowed\", user.Username)\n\t\treturn connID, fmt.Errorf(\"protocol DAV is not allowed for user %q\", user.Username)\n\t}\n\tif !user.IsLoginMethodAllowed(loginMethod, common.ProtocolWebDAV) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, %v login method is not allowed\",\n\t\t\tuser.Username, loginMethod)\n\t\treturn connID, fmt.Errorf(\"login method %v is not allowed for user %q\", loginMethod, user.Username)\n\t}\n\tif !user.IsLoginFromAddrAllowed(r.RemoteAddr) {\n\t\tlogger.Info(logSender, connectionID, \"cannot login user %q, remote address is not allowed: %v\",\n\t\t\tuser.Username, r.RemoteAddr)\n\t\treturn connID, fmt.Errorf(\"login for user %q is not allowed from this address: %v\", user.Username, r.RemoteAddr)\n\t}\n\treturn connID, nil\n}\n\nfunc (s *webDavServer) checkRemoteAddress(r *http.Request) string {\n\tipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)\n\tvar ip net.IP\n\tisUnixSocket := filepath.IsAbs(s.binding.Address)\n\tif !isUnixSocket {\n\t\tip = net.ParseIP(ipAddr)\n\t}\n\tif isUnixSocket || ip != nil {\n\t\tfor _, allow := range s.binding.allowHeadersFrom {\n\t\t\tif allow(ip) {\n\t\t\t\tparsedIP := util.GetRealIP(r, s.binding.ClientIPProxyHeader, s.binding.ClientIPHeaderDepth)\n\t\t\t\tif parsedIP != \"\" {\n\t\t\t\t\tipAddr = parsedIP\n\t\t\t\t\tr.RemoteAddr = ipAddr\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn ipAddr\n}\n\nfunc responseControllerDeadlines(rc *http.ResponseController, read, write time.Time) {\n\tif err := rc.SetReadDeadline(read); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to set read timeout to %s: %v\", read, err)\n\t}\n\tif err := rc.SetWriteDeadline(write); err != nil {\n\t\tlogger.Error(logSender, \"\", \"unable to set write timeout to %s: %v\", write, err)\n\t}\n}\n\nfunc writeLog(r *http.Request, status int, err error) {\n\tscheme := \"http\"\n\tcipherSuite := \"\"\n\tif r.TLS != nil {\n\t\tscheme = \"https\"\n\t\tcipherSuite = tls.CipherSuiteName(r.TLS.CipherSuite)\n\t}\n\tfields := map[string]any{\n\t\t\"remote_addr\":  r.RemoteAddr,\n\t\t\"proto\":        r.Proto,\n\t\t\"method\":       r.Method,\n\t\t\"user_agent\":   r.UserAgent(),\n\t\t\"uri\":          fmt.Sprintf(\"%s://%s%s\", scheme, r.Host, r.RequestURI),\n\t\t\"cipher_suite\": cipherSuite,\n\t}\n\tif reqID, ok := r.Context().Value(requestIDKey).(string); ok {\n\t\tfields[\"request_id\"] = reqID\n\t}\n\tif reqStart, ok := r.Context().Value(requestStartKey).(time.Time); ok {\n\t\tfields[\"elapsed_ms\"] = time.Since(reqStart).Nanoseconds() / 1000000\n\t}\n\tif depth := r.Header.Get(\"Depth\"); depth != \"\" {\n\t\tfields[\"depth\"] = depth\n\t}\n\tif contentLength := r.Header.Get(\"Content-Length\"); contentLength != \"\" {\n\t\tfields[\"content_length\"] = contentLength\n\t}\n\tif timeout := r.Header.Get(\"Timeout\"); timeout != \"\" {\n\t\tfields[\"timeout\"] = timeout\n\t}\n\tif status != 0 {\n\t\tfields[\"resp_status\"] = status\n\t}\n\tvar ev *zerolog.Event\n\tif status >= http.StatusInternalServerError {\n\t\tev = logger.GetLogger().Error()\n\t} else if status >= http.StatusBadRequest {\n\t\tev = logger.GetLogger().Warn()\n\t} else {\n\t\tev = logger.GetLogger().Debug()\n\t}\n\tev.\n\t\tTimestamp().\n\t\tStr(\"sender\", logSender).\n\t\tFields(fields).\n\t\tErr(err).\n\t\tSend()\n}\n\nfunc updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error, r *http.Request) {\n\tmetric.AddLoginAttempt(loginMethod)\n\tif err == nil {\n\t\tlogger.LoginLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, \"\", r.UserAgent(), r.TLS != nil, \"\")\n\t\tplugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolWebDAV, user.Username, ip, \"\", nil)\n\t\tcommon.DelayLogin(nil)\n\t} else if err != common.ErrInternalFailure && err != common.ErrNoCredentials {\n\t\tlogger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())\n\t\tevent := common.HostEventLoginFailed\n\t\tlogEv := notifier.LogEventTypeLoginFailed\n\t\tif errors.Is(err, util.ErrNotFound) {\n\t\t\tevent = common.HostEventUserNotFound\n\t\t\tlogEv = notifier.LogEventTypeLoginNoUser\n\t\t}\n\t\tcommon.AddDefenderEvent(ip, common.ProtocolWebDAV, event)\n\t\tplugin.Handler.NotifyLogEvent(logEv, common.ProtocolWebDAV, user.Username, ip, \"\", err)\n\t\tif loginMethod != dataprovider.LoginMethodTLSCertificate {\n\t\t\tcommon.DelayLogin(err)\n\t\t}\n\t}\n\tmetric.AddLoginResult(loginMethod, err)\n\tdataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolWebDAV, err)\n}\n"
  },
  {
    "path": "internal/webdavd/webdavd.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Package webdavd implements the WebDAV protocol\npackage webdavd\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5/middleware\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n)\n\ntype ctxReqParams int\n\nconst (\n\trequestIDKey ctxReqParams = iota\n\trequestStartKey\n)\n\nconst (\n\tlogSender = \"webdavd\"\n)\n\nvar (\n\tcertMgr       *common.CertManager\n\tserviceStatus ServiceStatus\n\ttimeFormats   = []string{\n\t\thttp.TimeFormat,\n\t\t\"Mon, _2 Jan 2006 15:04:05 GMT\",\n\t\ttime.RFC850,\n\t\ttime.ANSIC,\n\t}\n)\n\n// ServiceStatus defines the service status\ntype ServiceStatus struct {\n\tIsActive bool      `json:\"is_active\"`\n\tBindings []Binding `json:\"bindings\"`\n}\n\n// CorsConfig defines the CORS configuration\ntype CorsConfig struct {\n\tAllowedOrigins       []string `json:\"allowed_origins\" mapstructure:\"allowed_origins\"`\n\tAllowedMethods       []string `json:\"allowed_methods\" mapstructure:\"allowed_methods\"`\n\tAllowedHeaders       []string `json:\"allowed_headers\" mapstructure:\"allowed_headers\"`\n\tExposedHeaders       []string `json:\"exposed_headers\" mapstructure:\"exposed_headers\"`\n\tAllowCredentials     bool     `json:\"allow_credentials\" mapstructure:\"allow_credentials\"`\n\tEnabled              bool     `json:\"enabled\" mapstructure:\"enabled\"`\n\tMaxAge               int      `json:\"max_age\" mapstructure:\"max_age\"`\n\tOptionsPassthrough   bool     `json:\"options_passthrough\" mapstructure:\"options_passthrough\"`\n\tOptionsSuccessStatus int      `json:\"options_success_status\" mapstructure:\"options_success_status\"`\n\tAllowPrivateNetwork  bool     `json:\"allow_private_network\" mapstructure:\"allow_private_network\"`\n}\n\n// CustomMimeMapping defines additional, user defined mime mappings\ntype CustomMimeMapping struct {\n\tExt  string `json:\"ext\" mapstructure:\"ext\"`\n\tMime string `json:\"mime\" mapstructure:\"mime\"`\n}\n\n// UsersCacheConfig defines the cache configuration for users\ntype UsersCacheConfig struct {\n\tExpirationTime int `json:\"expiration_time\" mapstructure:\"expiration_time\"`\n\tMaxSize        int `json:\"max_size\" mapstructure:\"max_size\"`\n}\n\nfunc (c *UsersCacheConfig) getExpirationTime() time.Time {\n\tif c.ExpirationTime > 0 {\n\t\treturn time.Now().Add(time.Duration(c.ExpirationTime) * time.Minute)\n\t}\n\treturn time.Time{}\n}\n\n// MimeCacheConfig defines the cache configuration for mime types\ntype MimeCacheConfig struct {\n\tEnabled        bool                `json:\"enabled\" mapstructure:\"enabled\"`\n\tMaxSize        int                 `json:\"max_size\" mapstructure:\"max_size\"`\n\tCustomMappings []CustomMimeMapping `json:\"custom_mappings\" mapstructure:\"custom_mappings\"`\n}\n\n// Cache configuration\ntype Cache struct {\n\tUsers     UsersCacheConfig `json:\"users\" mapstructure:\"users\"`\n\tMimeTypes MimeCacheConfig  `json:\"mime_types\" mapstructure:\"mime_types\"`\n}\n\n// Binding defines the configuration for a network listener\ntype Binding struct {\n\t// The address to listen on. A blank value means listen on all available network interfaces.\n\tAddress string `json:\"address\" mapstructure:\"address\"`\n\t// The port used for serving requests\n\tPort int `json:\"port\" mapstructure:\"port\"`\n\t// you also need to provide a certificate for enabling HTTPS\n\tEnableHTTPS bool `json:\"enable_https\" mapstructure:\"enable_https\"`\n\t// Certificate and matching private key for this specific binding, if empty the global\n\t// ones will be used, if any\n\tCertificateFile    string `json:\"certificate_file\" mapstructure:\"certificate_file\"`\n\tCertificateKeyFile string `json:\"certificate_key_file\" mapstructure:\"certificate_key_file\"`\n\t// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2\n\tMinTLSVersion int `json:\"min_tls_version\" mapstructure:\"min_tls_version\"`\n\t// set to 1 to require client certificate authentication in addition to basic auth.\n\t// You need to define at least a certificate authority for this to work\n\tClientAuthType int `json:\"client_auth_type\" mapstructure:\"client_auth_type\"`\n\t// TLSCipherSuites is a list of supported cipher suites for TLS version 1.2.\n\t// If CipherSuites is nil/empty, a default list of secure cipher suites\n\t// is used, with a preference order based on hardware performance.\n\t// Note that TLS 1.3 ciphersuites are not configurable.\n\t// The supported ciphersuites names are defined here:\n\t//\n\t// https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53\n\t//\n\t// any invalid name will be silently ignored.\n\t// The order matters, the ciphers listed first will be the preferred ones.\n\tTLSCipherSuites []string `json:\"tls_cipher_suites\" mapstructure:\"tls_cipher_suites\"`\n\t// HTTP protocols to enable in preference order. Supported values: http/1.1, h2\n\tProtocols []string `json:\"tls_protocols\" mapstructure:\"tls_protocols\"`\n\t// Prefix for WebDAV resources, if empty WebDAV resources will be available at the\n\t// root (\"/\") URI. If defined it must be an absolute URI.\n\tPrefix string `json:\"prefix\" mapstructure:\"prefix\"`\n\t// Defines whether to use the common proxy protocol configuration or the\n\t// binding-specific proxy header configuration.\n\tProxyMode int `json:\"proxy_mode\" mapstructure:\"proxy_mode\"`\n\t// List of IP addresses and IP ranges allowed to set client IP proxy headers\n\tProxyAllowed []string `json:\"proxy_allowed\" mapstructure:\"proxy_allowed\"`\n\t// Allowed client IP proxy header such as \"X-Forwarded-For\", \"X-Real-IP\"\n\tClientIPProxyHeader string `json:\"client_ip_proxy_header\" mapstructure:\"client_ip_proxy_header\"`\n\t// Some client IP headers such as \"X-Forwarded-For\" can contain multiple IP address, this setting\n\t// define the position to trust starting from the right. For example if we have:\n\t// \"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1\" and the depth is 0, SFTPGo will use \"13.0.0.1\"\n\t// as client IP, if depth is 1, \"12.0.0.1\" will be used and so on\n\tClientIPHeaderDepth int `json:\"client_ip_header_depth\" mapstructure:\"client_ip_header_depth\"`\n\t// Do not add the WWW-Authenticate header after an authentication error,\n\t// only the 401 status code will be sent\n\tDisableWWWAuthHeader bool `json:\"disable_www_auth_header\" mapstructure:\"disable_www_auth_header\"`\n\tallowHeadersFrom     []func(net.IP) bool\n}\n\nfunc (b *Binding) parseAllowedProxy() error {\n\tif filepath.IsAbs(b.Address) && len(b.ProxyAllowed) > 0 {\n\t\t// unix domain socket\n\t\tb.allowHeadersFrom = []func(net.IP) bool{func(_ net.IP) bool { return true }}\n\t\treturn nil\n\t}\n\tallowedFuncs, err := util.ParseAllowedIPAndRanges(b.ProxyAllowed)\n\tif err != nil {\n\t\treturn err\n\t}\n\tb.allowHeadersFrom = allowedFuncs\n\treturn nil\n}\n\nfunc (b *Binding) isMutualTLSEnabled() bool {\n\treturn b.ClientAuthType == 1 || b.ClientAuthType == 2\n}\n\n// GetAddress returns the binding address\nfunc (b *Binding) GetAddress() string {\n\tif b.Port > 0 {\n\t\treturn fmt.Sprintf(\"%s:%d\", b.Address, b.Port)\n\t}\n\treturn b.Address\n}\n\n// IsValid returns true if the binding is valid\nfunc (b *Binding) IsValid() bool {\n\tif b.Port > 0 {\n\t\treturn true\n\t}\n\tif filepath.IsAbs(b.Address) && runtime.GOOS != \"windows\" {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (b *Binding) listenerWrapper() func(net.Listener) (net.Listener, error) {\n\tif b.ProxyMode == 1 {\n\t\treturn common.Config.GetProxyListener\n\t}\n\treturn nil\n}\n\n// Configuration defines the configuration for the WevDAV server\ntype Configuration struct {\n\t// Addresses and ports to bind to\n\tBindings []Binding `json:\"bindings\" mapstructure:\"bindings\"`\n\t// If files containing a certificate and matching private key for the server are provided you\n\t// can enable HTTPS connections for the configured bindings\n\t// Certificate and key files can be reloaded on demand sending a \"SIGHUP\" signal on Unix based systems and a\n\t// \"paramchange\" request to the running service on Windows.\n\tCertificateFile    string `json:\"certificate_file\" mapstructure:\"certificate_file\"`\n\tCertificateKeyFile string `json:\"certificate_key_file\" mapstructure:\"certificate_key_file\"`\n\t// CACertificates defines the set of root certificate authorities to be used to verify client certificates.\n\tCACertificates []string `json:\"ca_certificates\" mapstructure:\"ca_certificates\"`\n\t// CARevocationLists defines a set a revocation lists, one for each root CA, to be used to check\n\t// if a client certificate has been revoked\n\tCARevocationLists []string `json:\"ca_revocation_lists\" mapstructure:\"ca_revocation_lists\"`\n\t// CORS configuration\n\tCors CorsConfig `json:\"cors\" mapstructure:\"cors\"`\n\t// Cache configuration\n\tCache      Cache `json:\"cache\" mapstructure:\"cache\"`\n\tacmeDomain string\n}\n\n// GetStatus returns the server status\nfunc GetStatus() ServiceStatus {\n\treturn serviceStatus\n}\n\n// ShouldBind returns true if there is at least a valid binding\nfunc (c *Configuration) ShouldBind() bool {\n\tfor _, binding := range c.Bindings {\n\t\tif binding.IsValid() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *Configuration) getKeyPairs(configDir string) []common.TLSKeyPair {\n\tvar keyPairs []common.TLSKeyPair\n\n\tfor _, binding := range c.Bindings {\n\t\tcertificateFile := getConfigPath(binding.CertificateFile, configDir)\n\t\tcertificateKeyFile := getConfigPath(binding.CertificateKeyFile, configDir)\n\t\tif certificateFile != \"\" && certificateKeyFile != \"\" {\n\t\t\tkeyPairs = append(keyPairs, common.TLSKeyPair{\n\t\t\t\tCert: certificateFile,\n\t\t\t\tKey:  certificateKeyFile,\n\t\t\t\tID:   binding.GetAddress(),\n\t\t\t})\n\t\t}\n\t}\n\tvar certificateFile, certificateKeyFile string\n\tif c.acmeDomain != \"\" {\n\t\tcertificateFile, certificateKeyFile = util.GetACMECertificateKeyPair(c.acmeDomain)\n\t} else {\n\t\tcertificateFile = getConfigPath(c.CertificateFile, configDir)\n\t\tcertificateKeyFile = getConfigPath(c.CertificateKeyFile, configDir)\n\t}\n\tif certificateFile != \"\" && certificateKeyFile != \"\" {\n\t\tkeyPairs = append(keyPairs, common.TLSKeyPair{\n\t\t\tCert: certificateFile,\n\t\t\tKey:  certificateKeyFile,\n\t\t\tID:   common.DefaultTLSKeyPaidID,\n\t\t})\n\t}\n\treturn keyPairs\n}\n\nfunc (c *Configuration) loadFromProvider() error {\n\tconfigs, err := dataprovider.GetConfigs()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to load config from provider: %w\", err)\n\t}\n\tconfigs.SetNilsToEmpty()\n\tif configs.ACME.Domain == \"\" || !configs.ACME.HasProtocol(common.ProtocolWebDAV) {\n\t\treturn nil\n\t}\n\tcrt, key := util.GetACMECertificateKeyPair(configs.ACME.Domain)\n\tif crt != \"\" && key != \"\" {\n\t\tif _, err := os.Stat(crt); err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to load acme cert file %q: %v\", crt, err)\n\t\t\treturn nil\n\t\t}\n\t\tif _, err := os.Stat(key); err != nil {\n\t\t\tlogger.Error(logSender, \"\", \"unable to load acme key file %q: %v\", key, err)\n\t\t\treturn nil\n\t\t}\n\t\tfor idx := range c.Bindings {\n\t\t\tc.Bindings[idx].EnableHTTPS = true\n\t\t}\n\t\tc.acmeDomain = configs.ACME.Domain\n\t\tlogger.Info(logSender, \"\", \"acme domain set to %q\", c.acmeDomain)\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// Initialize configures and starts the WebDAV server\nfunc (c *Configuration) Initialize(configDir string) error {\n\tif err := c.loadFromProvider(); err != nil {\n\t\treturn err\n\t}\n\tlogger.Info(logSender, \"\", \"initializing WebDAV server with config %+v\", *c)\n\tmimeTypeCache = mimeCache{\n\t\tmaxSize:   c.Cache.MimeTypes.MaxSize,\n\t\tmimeTypes: make(map[string]string),\n\t}\n\tif !c.Cache.MimeTypes.Enabled {\n\t\tmimeTypeCache.maxSize = 0\n\t} else {\n\t\tcustomMimeTypeMapping = make(map[string]string)\n\t\tfor _, m := range c.Cache.MimeTypes.CustomMappings {\n\t\t\tif m.Mime != \"\" {\n\t\t\t\tlogger.Debug(logSender, \"\", \"adding custom mime mapping for extension %q, mime type %q\", m.Ext, m.Mime)\n\t\t\t\tcustomMimeTypeMapping[m.Ext] = m.Mime\n\t\t\t}\n\t\t}\n\t}\n\n\tif !c.ShouldBind() {\n\t\treturn common.ErrNoBinding\n\t}\n\n\tkeyPairs := c.getKeyPairs(configDir)\n\tif len(keyPairs) > 0 {\n\t\tmgr, err := common.NewCertManager(keyPairs, configDir, logSender)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmgr.SetCACertificates(c.CACertificates)\n\t\tif err := mgr.LoadRootCAs(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmgr.SetCARevocationLists(c.CARevocationLists)\n\t\tif err := mgr.LoadCRLs(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcertMgr = mgr\n\t}\n\tcompressor := middleware.NewCompressor(5, \"text/*\")\n\tdataprovider.InitializeWebDAVUserCache(c.Cache.Users.MaxSize)\n\n\tserviceStatus = ServiceStatus{\n\t\tBindings: nil,\n\t}\n\n\texitChannel := make(chan error, 1)\n\n\tfor _, binding := range c.Bindings {\n\t\tif !binding.IsValid() {\n\t\t\tcontinue\n\t\t}\n\t\tif err := binding.parseAllowedProxy(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tgo func(binding Binding) {\n\t\t\tserver := webDavServer{\n\t\t\t\tconfig:  c,\n\t\t\t\tbinding: binding,\n\t\t\t}\n\t\t\texitChannel <- server.listenAndServe(compressor)\n\t\t}(binding)\n\t}\n\n\tserviceStatus.IsActive = true\n\n\treturn <-exitChannel\n}\n\n// ReloadCertificateMgr reloads the certificate manager\nfunc ReloadCertificateMgr() error {\n\tif certMgr != nil {\n\t\treturn certMgr.Reload()\n\t}\n\treturn nil\n}\n\nfunc getConfigPath(name, configDir string) string {\n\tif !util.IsFileInputValid(name) {\n\t\treturn \"\"\n\t}\n\tif name != \"\" && !filepath.IsAbs(name) {\n\t\treturn filepath.Join(configDir, name)\n\t}\n\treturn name\n}\n\nfunc parseTime(text string) (t time.Time, err error) {\n\tfor _, layout := range timeFormats {\n\t\tt, err = time.Parse(layout, text)\n\t\tif err == nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/webdavd/webdavd_test.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\npackage webdavd_test\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/minio/sio\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/sftpgo/sdk\"\n\tsdkkms \"github.com/sftpgo/sdk/kms\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/studio-b12/gowebdav\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/drakkan/sftpgo/v2/internal/common\"\n\t\"github.com/drakkan/sftpgo/v2/internal/config\"\n\t\"github.com/drakkan/sftpgo/v2/internal/dataprovider\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpclient\"\n\t\"github.com/drakkan/sftpgo/v2/internal/httpdtest\"\n\t\"github.com/drakkan/sftpgo/v2/internal/kms\"\n\t\"github.com/drakkan/sftpgo/v2/internal/logger\"\n\t\"github.com/drakkan/sftpgo/v2/internal/sftpd\"\n\t\"github.com/drakkan/sftpgo/v2/internal/util\"\n\t\"github.com/drakkan/sftpgo/v2/internal/vfs\"\n\t\"github.com/drakkan/sftpgo/v2/internal/webdavd\"\n)\n\nconst (\n\tlogSender           = \"webavdTesting\"\n\twebDavServerAddr    = \"localhost:9090\"\n\twebDavTLSServerAddr = \"localhost:9443\"\n\twebDavServerPort    = 9090\n\twebDavTLSServerPort = 9443\n\tsftpServerAddr      = \"127.0.0.1:9022\"\n\tdefaultUsername     = \"test_user_dav\"\n\tdefaultPassword     = \"test_password\"\n\tosWindows           = \"windows\"\n\twebDavCert          = `-----BEGIN CERTIFICATE-----\nMIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw\nRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw\nOTUzMDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD\nVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVqWvrJ51t5OxV0v25NsOgR82CA\nNXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIVCzgWkxiz7XE4lgUwX44FCXZM\n3+JeUbKjUzBRMB0GA1UdDgQWBBRhLw+/o3+Z02MI/d4tmaMui9W16jAfBgNVHSME\nGDAWgBRhLw+/o3+Z02MI/d4tmaMui9W16jAPBgNVHRMBAf8EBTADAQH/MAoGCCqG\nSM49BAMCA2kAMGYCMQDqLt2lm8mE+tGgtjDmtFgdOcI72HSbRQ74D5rYTzgST1rY\n/8wTi5xl8TiFUyLMUsICMQC5ViVxdXbhuG7gX6yEqSkMKZICHpO8hqFwOD/uaFVI\ndV4vKmHUzwK/eIx+8Ay3neE=\n-----END CERTIFICATE-----`\n\twebDavKey = `-----BEGIN EC PARAMETERS-----\nBgUrgQQAIg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDCfMNsN6miEE3rVyUPwElfiJSWaR5huPCzUenZOfJT04GAcQdWvEju3\nUM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq\nWvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV\nCzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=\n-----END EC PRIVATE KEY-----`\n\tcaCRT = `-----BEGIN CERTIFICATE-----\nMIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0\nQXV0aDAeFw0yNDAxMTAxODEyMDRaFw0zNDAxMTAxODIxNTRaMBMxETAPBgNVBAMT\nCENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7WHW216m\nfi4uF8cx6HWf8wvAxaEWgCHTOi2MwFIzOrOtuT7xb64rkpdzx1aWetSiCrEyc3D1\nv03k0Akvlz1gtnDtO64+MA8bqlTnCydZJY4cCTvDOBUYZgtMqHZzpE6xRrqQ84zh\nyzjKQ5bR0st+XGfIkuhjSuf2n/ZPS37fge9j6AKzn/2uEVt33qmO85WtN3RzbSqL\nCdOJ6cQ216j3la1C5+NWvzIKC7t6NE1bBGI4+tRj7B5P5MeamkkogwbExUjdHp3U\n4yasvoGcCHUQDoa4Dej1faywz6JlwB6rTV4ys4aZDe67V/Q8iB2May1k7zBz1Ztb\nKF5Em3xewP1LqPEowF1uc4KtPGcP4bxdaIpSpmObcn8AIfH6smLQrn0C3cs7CYfo\nNlFuTbwzENUhjz0X6EsoM4w4c87lO+dRNR7YpHLqR/BJTbbyXUB0imne1u00fuzb\nS7OtweiA9w7DRCkr2gU4lmHe7l0T+SA9pxIeVLb78x7ivdyXSF5LVQJ1JvhhWu6i\nM6GQdLHat/0fpRFUbEe34RQSDJ2eOBifMJqvsvpBP8d2jcRZVUVrSXGc2mAGuGOY\n/tmnCJGW8Fd+sgpCVAqM0pxCM+apqrvJYUqqQZ2ZxugCXULtRWJ9p4C9zUl40HEy\nOQ+AaiiwFll/doXELglcJdNg8AZPGhugfxMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD\nAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNoJhIvDZQrEf/VQbWuu\nXgNnt2m5MA0GCSqGSIb3DQEBCwUAA4ICAQCYhT5SRqk19hGrQ09hVSZOzynXAa5F\nsYkEWJzFyLg9azhnTPE1bFM18FScnkd+dal6mt+bQiJvdh24NaVkDghVB7GkmXki\npAiZwEDHMqtbhiPxY8LtSeCBAz5JqXVU2Q0TpAgNSH4W7FbGWNThhxcJVOoIrXKE\njbzhwl1Etcaf0DBKWliUbdlxQQs65DLy+rNBYtOeK0pzhzn1vpehUlJ4eTFzP9KX\ny2Mksuq9AspPbqnqpWW645MdTxMb5T57MCrY3GDKw63z5z3kz88LWJF3nOxZmgQy\nWFUhbLmZm7x6N5eiu6Wk8/B4yJ/n5UArD4cEP1i7nqu+mbbM/SZlq1wnGpg/sbRV\noUF+a7pRcSbfxEttle4pLFhS+ErKatjGcNEab2OlU3bX5UoBs+TYodnCWGKOuBKV\nL/CYc65QyeYZ+JiwYn9wC8YkzOnnVIQjiCEkLgSL30h9dxpnTZDLrdAA8ItelDn5\nDvjuQq58CGDsaVqpSobiSC1DMXYWot4Ets1wwovUNEq1l0MERB+2olE+JU/8E23E\neL1/aA7Kw/JibkWz1IyzClpFDKXf6kR2onJyxerdwUL+is7tqYFLysiHxZDL1bli\nSXbW8hMa5gvo0IilFP9Rznn8PplIfCsvBDVv6xsRr5nTAFtwKaMBVgznE2ghs69w\nkK8u1YiiVenmoQ==\n-----END CERTIFICATE-----`\n\tcaCRL = `-----BEGIN X509 CRL-----\nMIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN\nMjQwMTEwMTgyMjU4WhcNMjYwMTA5MTgyMjU4WjAkMCICEQDOaeHbjY4pEj8WBmqg\nZuRRFw0yNDAxMTAxODIyNThaoCMwITAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1r\nrl4DZ7dpuTANBgkqhkiG9w0BAQsFAAOCAgEAZzZ4aBqCcAJigR9e/mqKpJa4B6FV\n+jZmnWXolGeUuVkjdiG9w614x7mB2S768iioJyALejjCZjqsp6ydxtn0epQw4199\nXSfPIxA9lxc7w79GLe0v3ztojvxDPh5V1+lwPzGf9i8AsGqb2BrcBqgxDeatndnE\njF+18bY1saXOBpukNLjtRScUXzy5YcSuO6mwz4548v+1ebpF7W4Yh+yh0zldJKcF\nDouuirZWujJwTwxxfJ+2+yP7GAuefXUOhYs/1y9ylvUgvKFqSyokv6OaVgTooKYD\nMSADzmNcbRvwyAC5oL2yJTVVoTFeP6fXl/BdFH3sO/hlKXGy4Wh1AjcVE6T0CSJ4\niYFX3gLFh6dbP9IQWMlIM5DKtAKSjmgOywEaWii3e4M0NFSf/Cy17p2E5/jXSLlE\nypDileK0aALkx2twGWwogh6sY1dQ6R3GpKSRPD2muQxVOG6wXvuJce0E9WLx1Ud4\nhVUdUEMlKUvm77/15U5awarH2cCJQxzS/GMeIintQiG7hUlgRzRdmWVe3vOOvt94\ncp8+ZUH/QSDOo41ATTHpFeC/XqF5E2G/ahXqra+O5my52V/FP0bSJnkorJ8apy67\nsn6DFbkqX9khTXGtacczh2PcqVjcQjBniYl2sPO3qIrrrY3tic96tMnM/u3JRdcn\nw7bXJGfJcIMrrKs=\n-----END X509 CRL-----`\n\tclient1Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAJr32nHRlhyPiS7IfZ/ZWYowDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjM3WhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+kiJ3X6\nHUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi85xE\nOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLWeYl7\nQie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4ZdRf\nXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dybbhO\nc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRUh5Xo\nGzjh6iReaPSOgGatqOw9bDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEAyAK7cOTWqjyLgFM0kyyx1fNPvm2GwKep3MuU\nOrSnLuWjoxzb7WcbKNVMlnvnmSUAWuErxsY0PUJNfcuqWiGmEp4d/SWfWPigG6DC\nsDej35BlSfX8FCufYrfC74VNk4yBS2LVYmIqcpqUrfay0I2oZA8+ToLEpdUvEv2I\nl59eOhJO2jsC3JbOyZZmK2Kv7d94fR+1tg2Rq1Wbnmc9AZKq7KDReAlIJh4u2KHb\nBbtF79idusMwZyP777tqSQ4THBMa+VAEc2UrzdZqTIAwqlKQOvO2fRz2P+ARR+Tz\nMYJMdCdmPZ9qAc8U1OcFBG6qDDltO8wf/Nu/PsSI5LGCIhIuPPIuKfm0rRfTqCG7\nQPQPWjRoXtGGhwjdIuWbX9fIB+c+NpAEKHgLtV+Rxj8s5IVxqG9a5TtU9VkfVXJz\nJ20naoz/G+vDsVINpd3kH0ziNvdrKfGRM5UgtnUOPCXB22fVmkIsMH2knI10CKK+\noffI56NTkLRu00xvg98/wdukhkwIAxg6PQI/BHY5mdvoacEHHHdOhMq+GSAh7DDX\nG8+HdbABM1ExkPnZLat15q706ztiuUpQv1C2DI8YviUVkMqCslj4cD4F8EFPo4kr\nkvme0Cuc9Qlf7N5rjdV3cjwavhFx44dyXj9aesft2Q1okPiIqbGNpcjHcIRlj4Au\nMU3Bo0A=\n-----END CERTIFICATE-----`\n\tclient1Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtuQFiqvdjd8WLxP0FgPDyDEJ1/uJ+Aoj6QllNV7svWxwW+ki\nJ3X6HUVNWhhCsNfly4pGW4erF4fZzmesElGx1PoWgQCWZKsa/N08bznelWgdmkyi\n85xEOkTj6e/cTWHFSOBURNJaXkGHZ0ROSh7qu0Ld+eqNo3k9W+NqZaqYvs2K7MLW\neYl7Qie8Ctuq5Qaz/jm0XwR2PFBROVQSaCPCukancPQ21ftqHPhAbjxoxvvN5QP4\nZdRfXlH/LDLhlFnJzPZdHnVy9xisSPPRfFApJiwyfjRYdtslpJOcNgP6oPlpX/dy\nbbhOc9FEUgj/Q90Je8EfioBYFYsqVD6/dFv9SwIDAQABAoIBAFjSHK7gENVZxphO\nhHg8k9ShnDo8eyDvK8l9Op3U3/yOsXKxolivvyx//7UFmz3vXDahjNHe7YScAXdw\neezbqBXa7xrvghqZzp2HhFYwMJ0210mcdncBKVFzK4ztZHxgQ0PFTqet0R19jZjl\nX3A325/eNZeuBeOied4qb/24AD6JGc6A0J55f5/QUQtdwYwrL15iC/KZXDL90PPJ\nCFJyrSzcXvOMEvOfXIFxhDVKRCppyIYXG7c80gtNC37I6rxxMNQ4mxjwUI2IVhxL\nj+nZDu0JgRZ4NaGjOq2e79QxUVm/GG3z25XgmBFBrXkEVV+sCZE1VDyj6kQfv9FU\nNhOrwGECgYEAzq47r/HwXifuGYBV/mvInFw3BNLrKry+iUZrJ4ms4g+LfOi0BAgf\nsXsWXulpBo2YgYjFdO8G66f69GlB4B7iLscpABXbRtpDZEnchQpaF36/+4g3i8gB\nZ29XHNDB8+7t4wbXvlSnLv1tZWey2fS4hPosc2YlvS87DMmnJMJqhs8CgYEA4oiB\nLGQP6VNdX0Uigmh5fL1g1k95eC8GP1ylczCcIwsb2OkAq0MT7SHRXOlg3leEq4+g\nmCHk1NdjkSYxDL2ZeTKTS/gy4p1jlcDa6Ilwi4pVvatNvu4o80EYWxRNNb1mAn67\nT8TN9lzc6mEi+LepQM3nYJ3F+ZWTKgxH8uoJwMUCgYEArpumE1vbjUBAuEyi2eGn\nRunlFW83fBCfDAxw5KM8anNlja5uvuU6GU/6s06QCxg+2lh5MPPrLdXpfukZ3UVa\nItjg+5B7gx1MSALaiY8YU7cibFdFThM3lHIM72wyH2ogkWcrh0GvSFSUQlJcWCSW\nasmMGiYXBgBL697FFZomMyMCgYEAkAnp0JcDQwHd4gDsk2zoqnckBsDb5J5J46n+\nDYNAFEww9bgZ08u/9MzG+cPu8xFE621U2MbcYLVfuuBE2ewIlPaij/COMmeO9Z59\n0tPpOuDH6eTtd1SptxqR6P+8pEn8feOlKHBj4Z1kXqdK/EiTlwAVeep4Al2oCFls\nujkz4F0CgYAe8vHnVFHlWi16zAqZx4ZZZhNuqPtgFkvPg9LfyNTA4dz7F9xgtUaY\nnXBPyCe/8NtgBfT79HkPiG3TM0xRZY9UZgsJKFtqAu5u4ManuWDnsZI9RK2QTLHe\nyEbH5r3Dg3n9k/3GbjXFIWdU9UaYsdnSKHHtMw9ZODc14LaAogEQug==\n-----END RSA PRIVATE KEY-----`\n\t// client 2 crt is revoked\n\tclient2Crt = `-----BEGIN CERTIFICATE-----\nMIIEITCCAgmgAwIBAgIRAM5p4duNjikSPxYGaqBm5FEwDQYJKoZIhvcNAQELBQAw\nEzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjQwMTEwMTgxMjUyWhcNMzQwMTEwMTgy\nMTUzWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImVjm/b\nQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TPkZua\neq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEKh4LQ\ncr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81PQAmT\nA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu4Ic0\n6tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC\nA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBR5mf0f\nZjf8ZCGXqU2+45th7VkkLDAfBgNVHSMEGDAWgBTaCYSLw2UKxH/1UG1rrl4DZ7dp\nuTANBgkqhkiG9w0BAQsFAAOCAgEARhFxNAouwbpEfN1M90+ao5rwyxEewerSoCCz\nPQzeUZ66MA/FkS/tFUGgGGG+wERN+WLbe1cN6q/XFr0FSMLuUxLXDNV02oUL/FnY\nxcyNLaZUZ0pP7sA+Hmx2AdTA6baIwQbyIY9RLAaz6hzo1YbI8yeis645F1bxgL2D\nEP5kXa3Obv0tqWByMZtrmJPv3p0W5GJKXVDn51GR/E5KI7pliZX2e0LmMX9mxfPB\n4sXFUggMHXxWMMSAmXPVsxC2KX6gMnajO7JUraTwuGm+6V371FzEX+UKXHI+xSvO\n78TseTIYsBGLjeiA8UjkKlD3T9qsQm2mb2PlKyqjvIm4i2ilM0E2w4JZmd45b925\n7q/QLV3NZ/zZMi6AMyULu28DWKfAx3RLKwnHWSFcR4lVkxQrbDhEUMhAhLAX+2+e\nqc7qZm3dTabi7ZJiiOvYK/yNgFHa/XtZp5uKPB5tigPIa+34hbZF7s2/ty5X3O1N\nf5Ardz7KNsxJjZIt6HvB28E/PPOvBqCKJc1Y08J9JbZi8p6QS1uarGoR7l7rT1Hv\n/ZXkNTw2bw1VpcWdzDBLLVHYNnJmS14189LVk11PcJJpSmubwCqg+ZZULdgtVr3S\nANas2dgMPVwXhnAalgkcc+lb2QqaEz06axfbRGBsgnyqR5/koKCg1Hr0+vThHSsR\nE0+r2+4=\n-----END CERTIFICATE-----`\n\tclient2Key = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApNYpNZVmXZtAObpRRIuP2o/7z04H2E161vKZvJ3LSLlUTImV\njm/bQe6DTNCUVLnzQuanmUlu2rUnN3lDSfYoBcJWbvC3y1OCPRkCjDV6KiYMA9TP\nkZuaeq6y3+bFFfEmyumsVEe0bSuzNHXCOIBT7PqYMdovECcwBh/RZCA5mqO5omEK\nh4LQcr6+sVVkvD3nsyx0Alz/kTLFqc0mVflmpJq+0BpdetHRg4n5vy/I/08jZ81P\nQAmTA0kyl0Jh132JBGFdA8eyugPPP8n5edU4f3HXV/nR7XLwBrpSt8KgEg8cwfAu\n4Ic06tGzB0CH8lSGtU0tH2/cOlDuguDD7VvokQIDAQABAoIBAQCMnEeg9uXQmdvq\nop4qi6bV+ZcDWvvkLwvHikFMnYpIaheYBpF2ZMKzdmO4xgCSWeFCQ4Hah8KxfHCM\nqLuWvw2bBBE5J8yQ/JaPyeLbec7RX41GQ2YhPoxDdP0PdErREdpWo4imiFhH/Ewt\nRvq7ufRdpdLoS8dzzwnvX3r+H2MkHoC/QANW2AOuVoZK5qyCH5N8yEAAbWKaQaeL\nVBhAYEVKbAkWEtXw7bYXzxRR7WIM3f45v3ncRusDIG+Hf75ZjatoH0lF1gHQNofO\nqkCVZVzjkLFuzDic2KZqsNORglNs4J6t5Dahb9v3hnoK963YMnVSUjFvqQ+/RZZy\nVILFShilAoGBANucwZU61eJ0tLKBYEwmRY/K7Gu1MvvcYJIOoX8/BL3zNmNO0CLl\nNiABtNt9WOVwZxDsxJXdo1zvMtAegNqS6W11R1VAZbL6mQ/krScbLDE6JKA5DmA7\n4nNi1gJOW1ziAfdBAfhe4cLbQOb94xkOK5xM1YpO0xgDJLwrZbehDMmPAoGBAMAl\n/owPDAvcXz7JFynT0ieYVc64MSFiwGYJcsmxSAnbEgQ+TR5FtkHYe91OSqauZcCd\naoKXQNyrYKIhyounRPFTdYQrlx6KtEs7LU9wOxuphhpJtGjRnhmA7IqvX703wNvu\nkhrEavn86G5boH8R80371SrN0Rh9UeAlQGuNBdvfAoGAEAmokW9Ug08miwqrr6Pz\n3IZjMZJwALidTM1IufQuMnj6ddIhnQrEIx48yPKkdUz6GeBQkuk2rujA+zXfDxc/\neMDhzrX/N0zZtLFse7ieR5IJbrH7/MciyG5lVpHGVkgjAJ18uVikgAhm+vd7iC7i\nvG1YAtuyysQgAKXircBTIL0CgYAHeTLWVbt9NpwJwB6DhPaWjalAug9HIiUjktiB\nGcEYiQnBWn77X3DATOA8clAa/Yt9m2HKJIHkU1IV3ESZe+8Fh955PozJJlHu3yVb\nAp157PUHTriSnxyMF2Sb3EhX/rQkmbnbCqqygHC14iBy8MrKzLG00X6BelZV5n0D\n8d85dwKBgGWY2nsaemPH/TiTVF6kW1IKSQoIyJChkngc+Xj/2aCCkkmAEn8eqncl\nRKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA\nubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl\n-----END RSA PRIVATE KEY-----`\n\ttestFileName        = \"test_file_dav.dat\"\n\ttestDLFileName      = \"test_download_dav.dat\"\n\ttlsClient1Username  = \"client1\"\n\ttlsClient2Username  = \"client2\"\n\temptyPwdPlaceholder = \"empty\"\n\tocMtimeHeader       = \"X-OC-Mtime\"\n)\n\nvar (\n\tconfigDir       = filepath.Join(\".\", \"..\", \"..\")\n\tallPerms        = []string{dataprovider.PermAny}\n\thomeBasePath    string\n\thookCmdPath     string\n\textAuthPath     string\n\tpreLoginPath    string\n\tpostConnectPath string\n\tpreDownloadPath string\n\tpreUploadPath   string\n\tlogFilePath     string\n\tcertPath        string\n\tkeyPath         string\n\tcaCrtPath       string\n\tcaCRLPath       string\n)\n\nfunc TestMain(m *testing.M) {\n\tlogFilePath = filepath.Join(configDir, \"sftpgo_webdavd_test.log\")\n\tlogger.InitLogger(logFilePath, 5, 1, 28, false, false, zerolog.DebugLevel)\n\tos.Setenv(\"SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN\", \"1\")\n\tos.Setenv(\"SFTPGO_COMMON__ALLOW_SELF_CONNECTIONS\", \"1\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_USERNAME\", \"admin\")\n\tos.Setenv(\"SFTPGO_DEFAULT_ADMIN_PASSWORD\", \"password\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__0__EXT\", \".sftpgo\")\n\tos.Setenv(\"SFTPGO_WEBDAVD__CACHE__MIME_TYPES__CUSTOM_MAPPINGS__0__MIME\", \"application/sftpgo\")\n\terr := config.LoadConfig(configDir, \"\")\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error loading configuration: %v\", err)\n\t\tos.Exit(1)\n\t}\n\tproviderConf := config.GetProviderConf()\n\tlogger.InfoToConsole(\"Starting WebDAVD tests, provider: %v\", providerConf.Driver)\n\tcommonConf := config.GetCommonConfig()\n\tcommonConf.UploadMode = 2\n\thomeBasePath = os.TempDir()\n\tif runtime.GOOS != osWindows {\n\t\tcommonConf.Actions.ExecuteOn = []string{\"download\", \"upload\", \"rename\", \"delete\"}\n\t\tcommonConf.Actions.Hook = hookCmdPath\n\t\thookCmdPath, err = exec.LookPath(\"true\")\n\t\tif err != nil {\n\t\t\tlogger.Warn(logSender, \"\", \"unable to get hook command: %v\", err)\n\t\t\tlogger.WarnToConsole(\"unable to get hook command: %v\", err)\n\t\t}\n\t}\n\n\tcertPath = filepath.Join(os.TempDir(), \"test_dav.crt\")\n\tkeyPath = filepath.Join(os.TempDir(), \"test_dav.key\")\n\tcaCrtPath = filepath.Join(os.TempDir(), \"test_dav_ca.crt\")\n\tcaCRLPath = filepath.Join(os.TempDir(), \"test_dav_crl.crt\")\n\terr = os.WriteFile(certPath, []byte(webDavCert), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing WebDAV certificate: %v\", err)\n\t\tos.Exit(1)\n\t}\n\terr = os.WriteFile(keyPath, []byte(webDavKey), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing WebDAV private key: %v\", err)\n\t\tos.Exit(1)\n\t}\n\terr = os.WriteFile(caCrtPath, []byte(caCRT), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing WebDAV CA crt: %v\", err)\n\t\tos.Exit(1)\n\t}\n\terr = os.WriteFile(caCRLPath, []byte(caCRL), os.ModePerm)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error writing WebDAV CRL: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing data provider: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\terr = common.Initialize(commonConf, 0)\n\tif err != nil {\n\t\tlogger.WarnToConsole(\"error initializing common: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\thttpConfig := config.GetHTTPConfig()\n\thttpConfig.Initialize(configDir) //nolint:errcheck\n\tkmsConfig := config.GetKMSConfig()\n\terr = kmsConfig.Initialize()\n\tif err != nil {\n\t\tlogger.ErrorToConsole(\"error initializing kms: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\thttpdConf := config.GetHTTPDConfig()\n\thttpdConf.Bindings[0].Port = 8078\n\thttpdtest.SetBaseURL(\"http://127.0.0.1:8078\")\n\n\t// required to test sftpfs\n\tsftpdConf := config.GetSFTPDConfig()\n\tsftpdConf.Bindings = []sftpd.Binding{\n\t\t{\n\t\t\tPort: 9022,\n\t\t},\n\t}\n\thostKeyPath := filepath.Join(os.TempDir(), \"id_ecdsa\")\n\tsftpdConf.HostKeys = []string{hostKeyPath}\n\n\twebDavConf := config.GetWebDAVDConfig()\n\twebDavConf.CACertificates = []string{caCrtPath}\n\twebDavConf.CARevocationLists = []string{caCRLPath}\n\twebDavConf.Bindings = []webdavd.Binding{\n\t\t{\n\t\t\tPort: webDavServerPort,\n\t\t},\n\t\t{\n\t\t\tPort:               webDavTLSServerPort,\n\t\t\tEnableHTTPS:        true,\n\t\t\tCertificateFile:    certPath,\n\t\t\tCertificateKeyFile: keyPath,\n\t\t\tClientAuthType:     2,\n\t\t},\n\t}\n\twebDavConf.Cors = webdavd.CorsConfig{\n\t\tEnabled:        true,\n\t\tAllowedOrigins: []string{\"*\"},\n\t\tAllowedMethods: []string{\n\t\t\thttp.MethodHead,\n\t\t\thttp.MethodGet,\n\t\t\thttp.MethodPost,\n\t\t\thttp.MethodPut,\n\t\t\thttp.MethodPatch,\n\t\t\thttp.MethodDelete,\n\t\t},\n\t\tAllowedHeaders:   []string{\"*\"},\n\t\tAllowCredentials: true,\n\t}\n\n\tstatus := webdavd.GetStatus()\n\tif status.IsActive {\n\t\tlogger.ErrorToConsole(\"webdav server is already active\")\n\t\tos.Exit(1)\n\t}\n\n\textAuthPath = filepath.Join(homeBasePath, \"extauth.sh\")\n\tpreLoginPath = filepath.Join(homeBasePath, \"prelogin.sh\")\n\tpostConnectPath = filepath.Join(homeBasePath, \"postconnect.sh\")\n\tpreDownloadPath = filepath.Join(homeBasePath, \"predownload.sh\")\n\tpreUploadPath = filepath.Join(homeBasePath, \"preupload.sh\")\n\n\tgo func() {\n\t\tlogger.Debug(logSender, \"\", \"initializing WebDAV server with config %+v\", webDavConf)\n\t\tif err := webDavConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start WebDAV server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tif err := httpdConf.Initialize(configDir, 0); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start HTTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tlogger.Debug(logSender, \"\", \"initializing SFTP server with config %+v\", sftpdConf)\n\t\tif err := sftpdConf.Initialize(configDir); err != nil {\n\t\t\tlogger.ErrorToConsole(\"could not start SFTP server: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\n\twaitTCPListening(webDavConf.Bindings[0].GetAddress())\n\twaitTCPListening(webDavConf.Bindings[1].GetAddress())\n\twaitTCPListening(httpdConf.Bindings[0].GetAddress())\n\twaitTCPListening(sftpdConf.Bindings[0].GetAddress())\n\twebdavd.ReloadCertificateMgr() //nolint:errcheck\n\n\texitCode := m.Run()\n\tos.Remove(logFilePath)\n\tos.Remove(extAuthPath)\n\tos.Remove(preLoginPath)\n\tos.Remove(postConnectPath)\n\tos.Remove(preDownloadPath)\n\tos.Remove(preUploadPath)\n\tos.Remove(certPath)\n\tos.Remove(keyPath)\n\tos.Remove(caCrtPath)\n\tos.Remove(caCRLPath)\n\tos.Remove(hostKeyPath)\n\tos.Remove(hostKeyPath + \".pub\")\n\tos.Exit(exitCode)\n}\n\nfunc TestInitialization(t *testing.T) {\n\tcfg := webdavd.Configuration{\n\t\tBindings: []webdavd.Binding{\n\t\t\t{\n\t\t\t\tPort:        1234,\n\t\t\t\tEnableHTTPS: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tPort: 0,\n\t\t\t},\n\t\t},\n\t\tCertificateFile:    \"missing path\",\n\t\tCertificateKeyFile: \"bad path\",\n\t}\n\terr := cfg.Initialize(configDir)\n\tassert.Error(t, err)\n\n\tcfg.Cache = config.GetWebDAVDConfig().Cache\n\tcfg.Bindings[0].Port = webDavServerPort\n\tcfg.CertificateFile = certPath\n\tcfg.CertificateKeyFile = keyPath\n\terr = cfg.Initialize(configDir)\n\tassert.Error(t, err)\n\terr = webdavd.ReloadCertificateMgr()\n\tassert.NoError(t, err)\n\n\tcfg.Bindings = []webdavd.Binding{\n\t\t{\n\t\t\tPort: 0,\n\t\t},\n\t}\n\terr = cfg.Initialize(configDir)\n\tassert.EqualError(t, err, common.ErrNoBinding.Error())\n\n\tcfg.CertificateFile = certPath\n\tcfg.CertificateKeyFile = keyPath\n\tcfg.CACertificates = []string{\"\"}\n\n\tcfg.Bindings = []webdavd.Binding{\n\t\t{\n\t\t\tPort:           9022,\n\t\t\tClientAuthType: 1,\n\t\t\tEnableHTTPS:    true,\n\t\t},\n\t}\n\terr = cfg.Initialize(configDir)\n\tassert.Error(t, err)\n\n\tcfg.CACertificates = nil\n\tcfg.CARevocationLists = []string{\"\"}\n\terr = cfg.Initialize(configDir)\n\tassert.Error(t, err)\n\n\tcfg.CARevocationLists = nil\n\terr = cfg.Initialize(configDir)\n\tassert.Error(t, err)\n\n\tcfg.CertificateFile = certPath\n\tcfg.CertificateKeyFile = keyPath\n\tcfg.CACertificates = []string{caCrtPath}\n\tcfg.CARevocationLists = []string{caCRLPath}\n\tcfg.Bindings[0].ProxyAllowed = []string{\"not valid\"}\n\terr = cfg.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"is not a valid IP address\")\n\t}\n\tcfg.Bindings[0].ProxyAllowed = nil\n\terr = cfg.Initialize(configDir)\n\tassert.Error(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = cfg.Initialize(configDir)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"unable to load config from provider\")\n\t}\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n}\n\nfunc TestBasicHandling(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaSize = 6553600\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaSize = 6553600\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tclient := getWebDavClient(user, true, nil)\n\t\tassert.NoError(t, checkBasicFunc(client))\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\texpectedQuotaSize := testFileSize\n\t\texpectedQuotaFiles := 1\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, int64(0), user.FirstUpload)\n\t\tassert.Equal(t, int64(0), user.FirstDownload)\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\t\ttrue, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, user.FirstUpload, int64(0))\n\t\tassert.Greater(t, user.FirstDownload, int64(0)) // webdav read the mime type\n\t\t// overwrite an existing file\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\t\ttrue, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// wrong password\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword+\"1\",\n\t\t\ttrue, testFileSize, client)\n\t\tassert.Error(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\t\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\tassert.Greater(t, user.FirstUpload, int64(0))\n\t\tassert.Greater(t, user.FirstDownload, int64(0))\n\t\terr = client.Rename(testFileName, testFileName+\"1\", false)\n\t\tassert.NoError(t, err)\n\t\t_, err = client.Stat(testFileName)\n\t\tassert.Error(t, err)\n\t\t// the webdav client hide the error we check the quota\n\t\terr = client.Remove(testFileName)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\t\terr = client.Remove(testFileName + \"1\")\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\t\tassert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)\n\t\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\tassert.Error(t, err)\n\n\t\ttestDir := \"testdir\"\n\t\terr = client.Mkdir(testDir, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = client.MkdirAll(path.Join(testDir, \"sub\", \"sub\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = client.MkdirAll(path.Join(testDir, \"sub1\", \"sub1\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = client.MkdirAll(path.Join(testDir, \"sub2\", \"sub2\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath, path.Join(testDir, testFileName+\".txt\"),\n\t\t\tuser.Username, defaultPassword, true, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath, path.Join(testDir, testFileName),\n\t\t\tuser.Username, defaultPassword, true, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\tfiles, err := client.ReadDir(testDir)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, files, 5)\n\t\terr = client.Copy(testDir, testDir+\"_copy\", false) //nolint:goconst\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveAll(testDir)\n\t\tassert.NoError(t, err)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\tstatus := webdavd.GetStatus()\n\tassert.True(t, status.IsActive)\n}\n\nfunc TestBasicHandlingCryptFs(t *testing.T) {\n\tu := getTestUserWithCryptFs()\n\tu.QuotaSize = 6553600\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\tencryptedFileSize, err := getEncryptedFileSize(testFileSize)\n\tassert.NoError(t, err)\n\texpectedQuotaSize := user.UsedQuotaSize + encryptedFileSize\n\texpectedQuotaFiles := user.UsedQuotaFiles + 1\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName,\n\t\tuser.Username, defaultPassword, false, testFileSize, client)\n\tassert.NoError(t, err)\n\t// overwrite an existing file\n\terr = uploadFileWithRawClient(testFilePath, testFileName,\n\t\tuser.Username, defaultPassword, false, testFileSize, client)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\tfiles, err := client.ReadDir(\"/\")\n\tassert.NoError(t, err)\n\tif assert.Len(t, files, 1) {\n\t\tassert.Equal(t, testFileSize, files[0].Size())\n\t}\n\terr = client.Remove(testFileName)\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.Error(t, err)\n\ttestDir := \"testdir\"\n\terr = client.Mkdir(testDir, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = client.MkdirAll(path.Join(testDir, \"sub\", \"sub\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = client.MkdirAll(path.Join(testDir, \"sub1\", \"sub1\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = client.MkdirAll(path.Join(testDir, \"sub2\", \"sub2\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(testDir, testFileName+\".txt\"),\n\t\tuser.Username, defaultPassword, false, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(testDir, testFileName),\n\t\tuser.Username, defaultPassword, false, testFileSize, client)\n\tassert.NoError(t, err)\n\tfiles, err = client.ReadDir(testDir)\n\tassert.NoError(t, err)\n\tassert.Len(t, files, 5)\n\tfor _, f := range files {\n\t\tif strings.HasPrefix(f.Name(), testFileName) {\n\t\t\tassert.Equal(t, testFileSize, f.Size())\n\t\t} else {\n\t\t\tassert.True(t, f.IsDir())\n\t\t}\n\t}\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestBufferedUser(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.OSConfig = sdk.OSFsConfig{\n\t\tWriteBufferSize: 2,\n\t\tReadBufferSize:  1,\n\t}\n\tvdirPath := \"/crypted\"\n\tmappedPath := filepath.Join(os.TempDir(), util.GenerateUniqueID())\n\tfolderName := filepath.Base(mappedPath)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tOSFsConfig: sdk.OSFsConfig{\n\t\t\t\t\tWriteBufferSize: 3,\n\t\t\t\t\tReadBufferSize:  2,\n\t\t\t\t},\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName,\n\t\tuser.Username, defaultPassword, false, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(vdirPath, testFileName),\n\t\tuser.Username, defaultPassword, false, testFileSize, client)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = downloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginEmptyPassword(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser.Password = emptyPwdPlaceholder\n\tclient := getWebDavClient(user, false, nil)\n\terr = checkBasicFunc(client)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"401\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestAnonymousUser(t *testing.T) {\n\tu := getTestUser()\n\tu.Password = \"\"\n\tu.Filters.IsAnonymous = true\n\t_, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.Error(t, err)\n\tuser, _, err := httpdtest.GetUserByUsername(u.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\tuser.Password = emptyPwdPlaceholder\n\tclient = getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"403\")\n\t}\n\terr = client.Mkdir(\"testdir\", os.ModePerm)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"403\")\n\t}\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLockAfterDelete(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tassert.NoError(t, err)\n\tlockBody := `<?xml version=\"1.0\" encoding=\"utf-8\" ?><d:lockinfo xmlns:d=\"DAV:\"><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype></d:lockinfo>`\n\treq, err := http.NewRequest(\"LOCK\", fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName), bytes.NewReader([]byte(lockBody)))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(u.Username, u.Password)\n\treq.Header.Set(\"Timeout\", \"Second-3600\")\n\thttpClient := httpclient.GetHTTPClient()\n\tresp, err := httpClient.Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponse, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tre := regexp.MustCompile(`\\<D:locktoken><D:href>.*</D:href>`)\n\tlockToken := string(re.Find(response))\n\tlockToken = strings.Replace(lockToken, \"<D:locktoken><D:href>\", \"\", 1)\n\tlockToken = strings.Replace(lockToken, \"</D:href>\", \"\", 1)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\treq, err = http.NewRequest(http.MethodDelete, fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"If\", fmt.Sprintf(\"(%v)\", lockToken))\n\treq.SetBasicAuth(u.Username, u.Password)\n\tresp, err = httpClient.Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusNoContent, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// if we try to lock again it must succeed, the lock must be deleted with the object\n\treq, err = http.NewRequest(\"LOCK\", fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName), bytes.NewReader([]byte(lockBody)))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(u.Username, u.Password)\n\tresp, err = httpClient.Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusCreated, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMtimeHeader(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client, dataprovider.KeyValue{Key: ocMtimeHeader, Value: \"1668879480\"})\n\tassert.NoError(t, err)\n\t// check the modification time\n\tinfo, err := client.Stat(testFileName)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, time.Unix(1668879480, 0).UTC(), info.ModTime().UTC())\n\t}\n\t// test on overwrite\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client, dataprovider.KeyValue{Key: ocMtimeHeader, Value: \"1667879480\"})\n\tassert.NoError(t, err)\n\tinfo, err = client.Stat(testFileName)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, time.Unix(1667879480, 0).UTC(), info.ModTime().UTC())\n\t}\n\t// invalid time will be silently ignored and the time set to now\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client, dataprovider.KeyValue{Key: ocMtimeHeader, Value: \"not unix time\"})\n\tassert.NoError(t, err)\n\tinfo, err = client.Stat(testFileName)\n\tif assert.NoError(t, err) {\n\t\tassert.NotEqual(t, time.Unix(1667879480, 0).UTC(), info.ModTime().UTC())\n\t}\n\n\treq, err := http.NewRequest(\"MOVE\", fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Overwrite\", \"T\")\n\treq.Header.Set(\"Destination\", path.Join(\"/\", testFileName+\"rename\"))\n\treq.Header.Set(ocMtimeHeader, \"1666779480\")\n\treq.SetBasicAuth(u.Username, u.Password)\n\thttpClient := httpclient.GetHTTPClient()\n\tresp, err := httpClient.Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusCreated, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// check the modification time\n\tinfo, err = client.Stat(testFileName + \"rename\")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, time.Unix(1666779480, 0).UTC(), info.ModTime().UTC())\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestRenameWithLock(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tassert.NoError(t, err)\n\n\tlockBody := `<?xml version=\"1.0\" encoding=\"utf-8\" ?><d:lockinfo xmlns:d=\"DAV:\"><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype></d:lockinfo>`\n\treq, err := http.NewRequest(\"LOCK\", fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName), bytes.NewReader([]byte(lockBody)))\n\tassert.NoError(t, err)\n\treq.SetBasicAuth(u.Username, u.Password)\n\thttpClient := httpclient.GetHTTPClient()\n\tresp, err := httpClient.Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\tresponse, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tre := regexp.MustCompile(`\\<D:locktoken><D:href>.*</D:href>`)\n\tlockToken := string(re.Find(response))\n\tlockToken = strings.Replace(lockToken, \"<D:locktoken><D:href>\", \"\", 1)\n\tlockToken = strings.Replace(lockToken, \"</D:href>\", \"\", 1)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\t// MOVE with a lock should succeeded\n\treq, err = http.NewRequest(\"MOVE\", fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName), nil)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"If\", fmt.Sprintf(\"(%v)\", lockToken))\n\treq.Header.Set(\"Overwrite\", \"T\")\n\treq.Header.Set(\"Destination\", path.Join(\"/\", testFileName+\"1\"))\n\treq.SetBasicAuth(u.Username, u.Password)\n\tresp, err = httpClient.Do(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusCreated, resp.StatusCode)\n\terr = resp.Body.Close()\n\tassert.NoError(t, err)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestPropPatch(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = u.Username + \"1\"\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser := getTestSFTPUser()\n\tsftpUser.FsConfig.SFTPConfig.Username = localUser.Username\n\n\tfor _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} {\n\t\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\t\tclient := getWebDavClient(user, true, nil)\n\t\tassert.NoError(t, checkBasicFunc(client), sftpUser.Username)\n\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\thttpClient := httpclient.GetHTTPClient()\n\t\tpropatchBody := `<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:propertyupdate xmlns:D=\"DAV:\" xmlns:Z=\"urn:schemas-microsoft-com:\"><D:set><D:prop><Z:Win32CreationTime>Wed, 04 Nov 2020 13:25:51 GMT</Z:Win32CreationTime><Z:Win32LastAccessTime>Sat, 05 Dec 2020 21:16:12 GMT</Z:Win32LastAccessTime><Z:Win32LastModifiedTime>Wed, 04 Nov 2020 13:25:51 GMT</Z:Win32LastModifiedTime><Z:Win32FileAttributes>00000000</Z:Win32FileAttributes></D:prop></D:set></D:propertyupdate>`\n\t\treq, err := http.NewRequest(\"PROPPATCH\", fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName), bytes.NewReader([]byte(propatchBody)))\n\t\tassert.NoError(t, err)\n\t\treq.SetBasicAuth(u.Username, u.Password)\n\t\tresp, err := httpClient.Do(req)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusMultiStatus, resp.StatusCode)\n\t\terr = resp.Body.Close()\n\t\tassert.NoError(t, err)\n\t\tinfo, err := client.Stat(testFileName)\n\t\tif assert.NoError(t, err) {\n\t\t\texpected, err := http.ParseTime(\"Wed, 04 Nov 2020 13:25:51 GMT\")\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testFileSize, info.Size())\n\t\t\tassert.Equal(t, expected.Format(http.TimeFormat), info.ModTime().Format(http.TimeFormat))\n\t\t}\n\t\t// wrong date\n\t\tpropatchBody = `<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:propertyupdate xmlns:D=\"DAV:\" xmlns:Z=\"urn:schemas-microsoft-com:\"><D:set><D:prop><Z:Win32CreationTime>Wed, 04 Nov 2020 13:25:51 GMT</Z:Win32CreationTime><Z:Win32LastAccessTime>Sat, 05 Dec 2020 21:16:12 GMT</Z:Win32LastAccessTime><Z:Win32LastModifiedTime>Wid, 04 Nov 2020 13:25:51 GMT</Z:Win32LastModifiedTime><Z:Win32FileAttributes>00000000</Z:Win32FileAttributes></D:prop></D:set></D:propertyupdate>`\n\t\treq, err = http.NewRequest(\"PROPPATCH\", fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName), bytes.NewReader([]byte(propatchBody)))\n\t\tassert.NoError(t, err)\n\t\treq.SetBasicAuth(u.Username, u.Password)\n\t\tresp, err = httpClient.Do(req)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusMultiStatus, resp.StatusCode)\n\t\terr = resp.Body.Close()\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestLoginInvalidPwd(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\tuser.Password = \"wrong\"\n\tclient = getWebDavClient(user, false, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginNonExistentUser(t *testing.T) {\n\tuser := getTestUser()\n\tclient := getWebDavClient(user, true, nil)\n\tassert.Error(t, checkBasicFunc(client))\n}\n\nfunc TestRateLimiter(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.RateLimitersConfig = []common.RateLimiterConfig{\n\t\t{\n\t\t\tAverage:   1,\n\t\t\tPeriod:    1000,\n\t\t\tBurst:     3,\n\t\t\tType:      1,\n\t\t\tProtocols: []string{common.ProtocolWebDAV},\n\t\t},\n\t}\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\t_, err = client.ReadDir(\".\")\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"429\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestDefender(t *testing.T) {\n\toldConfig := config.GetCommonConfig()\n\n\tcfg := config.GetCommonConfig()\n\tcfg.DefenderConfig.Enabled = true\n\tcfg.DefenderConfig.Threshold = 3\n\tcfg.DefenderConfig.ScoreLimitExceeded = 2\n\tcfg.DefenderConfig.ScoreValid = 1\n\n\terr := common.Initialize(cfg, 0)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\tuser.Password = \"wrong_pwd\"\n\tclient = getWebDavClient(user, false, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\thosts, _, err := httpdtest.GetDefenderHosts(http.StatusOK)\n\tassert.NoError(t, err)\n\tif assert.Len(t, hosts, 1) {\n\t\thost := hosts[0]\n\t\tassert.Empty(t, host.GetBanTime())\n\t\tassert.Equal(t, 1, host.Score)\n\t}\n\n\tfor i := 0; i < 2; i++ {\n\t\tclient = getWebDavClient(user, false, nil)\n\t\tassert.Error(t, checkBasicFunc(client))\n\t}\n\n\tuser.Password = defaultPassword\n\tclient = getWebDavClient(user, true, nil)\n\terr = checkBasicFunc(client)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"403\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\terr = common.Initialize(oldConfig, 0)\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginExternalAuth(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(u, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\tu.Username = defaultUsername + \"1\"\n\tclient = getWebDavClient(u, false, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, defaultUsername, user.Username)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthPasswordChange(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, defaultPassword), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(u, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\tu.Username = defaultUsername + \"1\"\n\tclient = getWebDavClient(u, false, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, defaultPassword+\"1\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(u, false, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\tu.Password = defaultPassword + \"1\"\n\tclient = getWebDavClient(u, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, defaultUsername, user.Username)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tuser, _, err = httpdtest.GetUserByUsername(defaultUsername+\"1\", http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthReturningAnonymousUser(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\tu.Filters.IsAnonymous = true\n\tu.Filters.DeniedProtocols = []string{common.ProtocolSSH}\n\tu.Password = \"\"\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(u, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, u.Username, emptyPwdPlaceholder,\n\t\tfalse, testFileSize, client)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"403\")\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.True(t, user.Filters.IsAnonymous)\n\tassert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDownload}, user.Permissions[\"/\"])\n\tassert.Equal(t, []string{common.ProtocolSSH, common.ProtocolHTTP}, user.Filters.DeniedProtocols)\n\tassert.Equal(t, []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodPassword,\n\t\tdataprovider.SSHLoginMethodKeyboardInteractive, dataprovider.SSHLoginMethodKeyAndPassword,\n\t\tdataprovider.SSHLoginMethodKeyAndKeyboardInt, dataprovider.LoginMethodTLSCertificate,\n\t\tdataprovider.LoginMethodTLSCertificateAndPwd}, user.Filters.DeniedLoginMethods)\n\n\tu.Password = emptyPwdPlaceholder\n\tclient = getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"403\")\n\t}\n\terr = client.Mkdir(\"testdir\", os.ModePerm)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"403\")\n\t}\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestExternalAuthAnonymousGroupInheritance(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tg := dataprovider.Group{\n\t\tBaseGroup: sdk.BaseGroup{\n\t\t\tName: \"test_group\",\n\t\t},\n\t\tUserSettings: dataprovider.GroupUserSettings{\n\t\t\tBaseGroupUserSettings: sdk.BaseGroupUserSettings{\n\t\t\t\tPermissions: map[string][]string{\n\t\t\t\t\t\"/\": allPerms,\n\t\t\t\t},\n\t\t\t\tFilters: sdk.BaseUserFilters{\n\t\t\t\t\tIsAnonymous: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tu := getTestUser()\n\tu.Groups = []sdk.GroupMapping{\n\t\t{\n\t\t\tName: g.Name,\n\t\t\tType: sdk.GroupTypePrimary,\n\t\t},\n\t}\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\tgroup, _, err := httpdtest.AddGroup(g, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tu.Password = emptyPwdPlaceholder\n\tclient := getWebDavClient(u, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\terr = client.Mkdir(\"tdir\", os.ModePerm)\n\tif assert.Error(t, err) {\n\t\tassert.Contains(t, err.Error(), \"403\")\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.False(t, user.Filters.IsAnonymous)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveGroup(group, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreLoginHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(u, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\tuser, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)\n\tassert.NoError(t, err)\n\t// test login with an existing user\n\tclient = getWebDavClient(user, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)\n\tassert.NoError(t, err)\n\t// update the user to remove it from the cache\n\tuser.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(defaultPassword)\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(user, true, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\t// update the user to remove it from the cache\n\tuser.FsConfig.Provider = sdk.LocalFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase = nil\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tuser.Status = 0\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(user, true, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreDownloadHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldExecuteOn := common.Config.Actions.ExecuteOn\n\toldHook := common.Config.Actions.Hook\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}\n\tcommon.Config.Actions.Hook = preDownloadPath\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preDownloadPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(preDownloadPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.Error(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}\n\tcommon.Config.Actions.Hook = preDownloadPath\n\n\tcommon.Config.Actions.ExecuteOn = oldExecuteOn\n\tcommon.Config.Actions.Hook = oldHook\n}\n\nfunc TestPreUploadHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\toldExecuteOn := common.Config.Actions.ExecuteOn\n\toldHook := common.Config.Actions.Hook\n\n\tcommon.Config.Actions.ExecuteOn = []string{common.OperationPreUpload}\n\tcommon.Config.Actions.Hook = preUploadPath\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(preUploadPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(preUploadPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.Error(t, err)\n\n\terr = uploadFileWithRawClient(testFilePath, testFileName+\"1\", user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tassert.Error(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\tcommon.Config.Actions.ExecuteOn = oldExecuteOn\n\tcommon.Config.Actions.Hook = oldHook\n}\n\nfunc TestPostConnectHook(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tcommon.Config.PostConnectHook = postConnectPath\n\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\terr = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)\n\tassert.NoError(t, err)\n\tassert.Error(t, checkBasicFunc(client))\n\n\tcommon.Config.PostConnectHook = \"http://127.0.0.1:8078/healthz\"\n\tassert.NoError(t, checkBasicFunc(client))\n\n\tcommon.Config.PostConnectHook = \"http://127.0.0.1:8078/notfound\"\n\tassert.Error(t, checkBasicFunc(client))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\n\tcommon.Config.PostConnectHook = \"\"\n}\n\nfunc TestMaxConnections(t *testing.T) {\n\toldValue := common.Config.MaxTotalConnections\n\tcommon.Config.MaxTotalConnections = 1\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\t// now add a fake connection\n\tfs := vfs.NewOsFs(\"id\", os.TempDir(), \"\", nil)\n\tconnection := &webdavd.Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, \"\", \"\", user),\n\t}\n\terr = common.Connections.Add(connection)\n\tassert.NoError(t, err)\n\tassert.Error(t, checkBasicFunc(client))\n\tcommon.Connections.Remove(connection.GetID())\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\tcommon.Config.MaxTotalConnections = oldValue\n}\n\nfunc TestMaxPerHostConnections(t *testing.T) {\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 1\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\t// now add a fake connection\n\taddrs, err := net.LookupHost(\"localhost\")\n\tassert.NoError(t, err)\n\tfor _, addr := range addrs {\n\t\tcommon.Connections.AddClientConnection(addr)\n\t}\n\tassert.Error(t, checkBasicFunc(client))\n\tfor _, addr := range addrs {\n\t\tcommon.Connections.RemoveClientConnection(addr)\n\t}\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\nfunc TestMaxTransfers(t *testing.T) {\n\toldValue := common.Config.MaxPerHostConnections\n\tcommon.Config.MaxPerHostConnections = 2\n\n\tassert.Eventually(t, func() bool {\n\t\treturn common.Connections.GetClientConnections() == 0\n\t}, 1000*time.Millisecond, 50*time.Millisecond)\n\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\tconn, sftpClient, err := getSftpClient(user)\n\tassert.NoError(t, err)\n\tdefer conn.Close()\n\tdefer sftpClient.Close()\n\n\tf1, err := sftpClient.Create(\"file1\")\n\tassert.NoError(t, err)\n\tf2, err := sftpClient.Create(\"file2\")\n\tassert.NoError(t, err)\n\t_, err = f1.Write([]byte(\" \"))\n\tassert.NoError(t, err)\n\t_, err = f2.Write([]byte(\" \"))\n\tassert.NoError(t, err)\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tassert.Error(t, err)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\n\terr = f1.Close()\n\tassert.NoError(t, err)\n\terr = f2.Close()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\tcommon.Config.MaxPerHostConnections = oldValue\n}\n\nfunc TestMustChangePasswordRequirement(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.RequirePasswordChange = true\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, false, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\n\terr = dataprovider.UpdateUserPassword(user.Username, defaultPassword, \"\", \"\", \"\")\n\tassert.NoError(t, err)\n\n\tclient = getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestMaxSessions(t *testing.T) {\n\tu := getTestUser()\n\tu.MaxSessions = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\t// now add a fake connection\n\tfs := vfs.NewOsFs(\"id\", os.TempDir(), \"\", nil)\n\tconnection := &webdavd.Connection{\n\t\tBaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, \"\", \"\", user),\n\t}\n\terr = common.Connections.Add(connection)\n\tassert.NoError(t, err)\n\tassert.Error(t, checkBasicFunc(client))\n\tcommon.Connections.Remove(connection.GetID())\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc TestLoginWithIPilters(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.DeniedIP = []string{\"192.167.0.0/24\", \"172.18.0.0/16\"}\n\tu.Filters.AllowedIP = []string{\"172.19.0.0/16\"}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDownloadErrors(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 1\n\tsubDir1 := \"sub1\"\n\tsubDir2 := \"sub2\"\n\tu.Permissions[path.Join(\"/\", subDir1)] = []string{dataprovider.PermListItems}\n\tu.Permissions[path.Join(\"/\", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload,\n\t\tdataprovider.PermDelete, dataprovider.PermDownload}\n\t// use an unknown mime to trigger content type detection\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/sub2\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{\"*.jpg\", \"*.zipp\"},\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\ttestFilePath1 := filepath.Join(user.HomeDir, subDir1, \"file.zipp\")\n\ttestFilePath2 := filepath.Join(user.HomeDir, subDir2, \"file.zipp\")\n\ttestFilePath3 := filepath.Join(user.HomeDir, subDir2, \"file.jpg\")\n\terr = os.MkdirAll(filepath.Dir(testFilePath1), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.MkdirAll(filepath.Dir(testFilePath2), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(testFilePath1, []byte(\"file1\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(testFilePath2, []byte(\"file2\"), os.ModePerm)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(testFilePath3, []byte(\"file3\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\terr = downloadFile(path.Join(\"/\", subDir1, \"file.zipp\"), localDownloadPath, 5, client)\n\tassert.Error(t, err)\n\terr = downloadFile(path.Join(\"/\", subDir2, \"file.zipp\"), localDownloadPath, 5, client)\n\tassert.Error(t, err)\n\terr = downloadFile(path.Join(\"/\", subDir2, \"file.jpg\"), localDownloadPath, 5, client)\n\tassert.Error(t, err)\n\terr = downloadFile(path.Join(\"missing.zip\"), localDownloadPath, 5, client)\n\tassert.Error(t, err)\n\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadErrors(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaSize = 65535\n\tsubDir1 := \"sub1\"\n\tsubDir2 := \"sub2\"\n\t// we need download permission to get size since PROPFIND will open the file\n\tu.Permissions[path.Join(\"/\", subDir1)] = []string{dataprovider.PermListItems, dataprovider.PermDownload}\n\tu.Permissions[path.Join(\"/\", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload,\n\t\tdataprovider.PermDelete, dataprovider.PermDownload}\n\tu.Filters.FilePatterns = []sdk.PatternsFilter{\n\t\t{\n\t\t\tPath:            \"/sub2\",\n\t\t\tAllowedPatterns: []string{},\n\t\t\tDeniedPatterns:  []string{\"*.zip\"},\n\t\t},\n\t}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := user.QuotaSize\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = client.Mkdir(subDir1, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = client.Mkdir(subDir2, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(subDir1, testFileName), user.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.Error(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(subDir2, testFileName+\".zip\"), user.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\n\tassert.Error(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(subDir2, testFileName), user.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = client.Rename(path.Join(subDir2, testFileName), path.Join(subDir1, testFileName), false)\n\tassert.Error(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(subDir2, testFileName), user.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.Error(t, err)\n\terr = uploadFileWithRawClient(testFilePath, subDir1, user.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.Error(t, err)\n\t// overquota\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.Error(t, err)\n\terr = client.Remove(path.Join(subDir2, testFileName))\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.Error(t, err)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDeniedLoginMethod(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndKeyboardInt}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(user, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestDeniedProtocols(t *testing.T) {\n\tu := getTestUser()\n\tu.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\n\tuser.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolFTP}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(user, false, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestQuotaLimits(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 1\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaFiles = 1\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\ttestFileSize := int64(65536)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\ttestFileSize1 := int64(131072)\n\t\ttestFileName1 := \"test_file1.dat\"\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\ttestFileSize2 := int64(32768)\n\t\ttestFileName2 := \"test_file2.dat\"\n\t\ttestFilePath2 := filepath.Join(homeBasePath, testFileName2)\n\t\terr = createTestFile(testFilePath2, testFileSize2)\n\t\tassert.NoError(t, err)\n\t\tclient := getWebDavClient(user, false, nil)\n\t\t// test quota files\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName+\".quota\", user.Username, defaultPassword, false, //nolint:goconst\n\t\t\ttestFileSize, client)\n\t\tif !assert.NoError(t, err, \"username: %v\", user.Username) {\n\t\t\tinfo, err := os.Stat(testFilePath)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tfmt.Printf(\"local file size: %v\\n\", info.Size())\n\t\t\t}\n\t\t\tprintLatestLogs(20)\n\t\t}\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName+\".quota1\", user.Username, defaultPassword,\n\t\t\tfalse, testFileSize, client)\n\t\tassert.Error(t, err, \"username: %v\", user.Username)\n\t\terr = client.Rename(testFileName+\".quota\", testFileName, false)\n\t\tassert.NoError(t, err)\n\t\tfiles, err := client.ReadDir(\"/\")\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, files, 1)\n\t\t// test quota size\n\t\tuser.QuotaSize = testFileSize - 1\n\t\tuser.QuotaFiles = 0\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName+\".quota\", user.Username, defaultPassword,\n\t\t\tfalse, testFileSize, client)\n\t\tassert.Error(t, err)\n\t\terr = client.Rename(testFileName, testFileName+\".quota\", false)\n\t\tassert.NoError(t, err)\n\t\t// now test quota limits while uploading the current file, we have 1 bytes remaining\n\t\tuser.QuotaSize = testFileSize + 1\n\t\tuser.QuotaFiles = 0\n\t\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath1, testFileName1, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize1, client)\n\t\tassert.Error(t, err)\n\t\t_, err = client.Stat(testFileName1)\n\t\tassert.Error(t, err)\n\t\terr = client.Rename(testFileName+\".quota\", testFileName, false)\n\t\tassert.NoError(t, err)\n\t\t// overwriting an existing file will work if the resulting size is lesser or equal than the current one\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath2, testFileName, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize2, client)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath1, testFileName, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize1, client)\n\t\tassert.Error(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath2, testFileName, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize2, client)\n\t\tassert.NoError(t, err)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath2)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\tuser.QuotaFiles = 0\n\t\t\tuser.QuotaSize = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestTransferQuotaLimits(t *testing.T) {\n\tu := getTestUser()\n\tu.DownloadDataTransfer = 1\n\tu.UploadDataTransfer = 1\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(550000)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, false, nil)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\t// error while download is active\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.Error(t, err)\n\t// error before starting the download\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.Error(t, err)\n\t// error while upload is active\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tassert.Error(t, err)\n\t// error before starting the upload\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\tfalse, testFileSize, client)\n\tassert.Error(t, err)\n\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadMaxSize(t *testing.T) {\n\ttestFileSize := int64(65535)\n\tu := getTestUser()\n\tu.Filters.MaxUploadFileSize = testFileSize + 1\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.Filters.MaxUploadFileSize = testFileSize + 1\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\ttestFileSize1 := int64(131072)\n\t\ttestFileName1 := \"test_file_dav1.dat\"\n\t\ttestFilePath1 := filepath.Join(homeBasePath, testFileName1)\n\t\terr = createTestFile(testFilePath1, testFileSize1)\n\t\tassert.NoError(t, err)\n\t\tclient := getWebDavClient(user, false, nil)\n\t\terr = uploadFileWithRawClient(testFilePath1, testFileName1, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize1, client)\n\t\tassert.Error(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\t// now test overwrite an existing file with a size bigger than the allowed one\n\t\terr = createTestFile(filepath.Join(user.GetHomeDir(), testFileName1), testFileSize1)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath1, testFileName1, user.Username, defaultPassword,\n\t\t\tfalse, testFileSize1, client)\n\t\tassert.Error(t, err)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\terr = os.Remove(testFilePath1)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Filters.MaxUploadFileSize = 65536000\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestClientClose(t *testing.T) {\n\tu := getTestUser()\n\tu.UploadBandwidth = 64\n\tu.DownloadBandwidth = 64\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.UploadBandwidth = 64\n\tu.DownloadBandwidth = 64\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\ttestFileSize := int64(1048576)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tclient := getWebDavClient(user, true, nil)\n\t\tassert.NoError(t, checkBasicFunc(client))\n\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\t\t\ttrue, testFileSize, client)\n\t\t\tassert.Error(t, err)\n\t\t\twg.Done()\n\t\t}()\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\t\t\tif len(stat.Transfers) > 0 {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}, 1*time.Second, 50*time.Millisecond)\n\n\t\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\t\tcommon.Connections.Close(stat.ConnectionID, \"\")\n\t\t}\n\t\twg.Wait()\n\t\t// for the sftp user a stat is done after the failed upload and\n\t\t// this triggers a new connection\n\t\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\t\tcommon.Connections.Close(stat.ConnectionID, \"\")\n\t\t}\n\t\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t\t1*time.Second, 100*time.Millisecond)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\ttestFilePath = filepath.Join(user.HomeDir, testFileName)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\t\t\tassert.Error(t, err)\n\t\t\twg.Done()\n\t\t}()\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\t\t\tif len(stat.Transfers) > 0 {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}, 1*time.Second, 50*time.Millisecond)\n\n\t\tfor _, stat := range common.Connections.GetStats(\"\") {\n\t\t\tcommon.Connections.Close(stat.ConnectionID, \"\")\n\t\t}\n\t\twg.Wait()\n\t\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t\t1*time.Second, 100*time.Millisecond)\n\t\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n\n\t\terr = os.Remove(localDownloadPath)\n\t\tassert.NoError(t, err)\n\t}\n\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginWithDatabaseCredentials(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"test\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ \"type\": \"service_account\", \"private_key\": \" \", \"client_email\": \"example@iam.gserviceaccount.com\" }`)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())\n\tassert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())\n\tassert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData())\n\tassert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey())\n\n\tclient := getWebDavClient(user, false, nil)\n\n\terr = client.Connect()\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestLoginInvalidFs(t *testing.T) {\n\tu := getTestUser()\n\tu.FsConfig.Provider = sdk.GCSFilesystemProvider\n\tu.FsConfig.GCSConfig.Bucket = \"test\"\n\tu.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(\"invalid JSON for credentials\")\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(user, true, nil)\n\tassert.Error(t, checkBasicFunc(client))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPBuffered(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaFiles = 1000\n\tu.HomeDir = filepath.Join(os.TempDir(), u.Username)\n\tu.FsConfig.SFTPConfig.BufferSize = 2\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(sftpUser, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\texpectedQuotaSize := testFileSize\n\texpectedQuotaFiles := 1\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, sftpUser.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.NoError(t, err)\n\t// overwrite an existing file\n\terr = uploadFileWithRawClient(testFilePath, testFileName, sftpUser.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.NoError(t, err)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\n\tuser, _, err := httpdtest.GetUserByUsername(sftpUser.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)\n\tassert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)\n\n\tfileContent := []byte(\"test file contents\")\n\terr = os.WriteFile(testFilePath, fileContent, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, sftpUser.Username, defaultPassword,\n\t\ttrue, int64(len(fileContent)), client)\n\tassert.NoError(t, err)\n\tremotePath := fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName)\n\treq, err := http.NewRequest(http.MethodGet, remotePath, nil)\n\tassert.NoError(t, err)\n\thttpClient := httpclient.GetHTTPClient()\n\treq.SetBasicAuth(user.Username, defaultPassword)\n\treq.Header.Set(\"Range\", \"bytes=5-\")\n\tresp, err := httpClient.Do(req)\n\tif assert.NoError(t, err) {\n\t\tdefer resp.Body.Close()\n\t\tassert.Equal(t, http.StatusPartialContent, resp.StatusCode)\n\t\tbodyBytes, err := io.ReadAll(resp.Body)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"file contents\", string(bodyBytes))\n\t}\n\treq.Header.Set(\"Range\", \"bytes=5-8\")\n\tresp, err = httpClient.Do(req)\n\tif assert.NoError(t, err) {\n\t\tdefer resp.Body.Close()\n\t\tassert.Equal(t, http.StatusPartialContent, resp.StatusCode)\n\t\tbodyBytes, err := io.ReadAll(resp.Body)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"file\", string(bodyBytes))\n\t}\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(sftpUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestBytesRangeRequests(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = u.Username + \"1\"\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser := getTestSFTPUser()\n\tsftpUser.FsConfig.SFTPConfig.Username = localUser.Username\n\n\tfor _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} {\n\t\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\t\tassert.NoError(t, err)\n\t\ttestFileName := \"test_file.txt\"\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\tfileContent := []byte(\"test file contents\")\n\t\terr = os.WriteFile(testFilePath, fileContent, os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\tclient := getWebDavClient(user, true, nil)\n\t\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\t\ttrue, int64(len(fileContent)), client)\n\t\tassert.NoError(t, err)\n\t\tremotePath := fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName)\n\t\treq, err := http.NewRequest(http.MethodGet, remotePath, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\thttpClient := httpclient.GetHTTPClient()\n\t\t\treq.SetBasicAuth(user.Username, defaultPassword)\n\t\t\treq.Header.Set(\"Range\", \"bytes=5-\")\n\t\t\tresp, err := httpClient.Do(req)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tdefer resp.Body.Close()\n\t\t\t\tassert.Equal(t, http.StatusPartialContent, resp.StatusCode)\n\t\t\t\tbodyBytes, err := io.ReadAll(resp.Body)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, \"file contents\", string(bodyBytes))\n\t\t\t}\n\t\t\treq.Header.Set(\"Range\", \"bytes=5-8\")\n\t\t\tresp, err = httpClient.Do(req)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tdefer resp.Body.Close()\n\t\t\t\tassert.Equal(t, http.StatusPartialContent, resp.StatusCode)\n\t\t\t\tbodyBytes, err := io.ReadAll(resp.Body)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, \"file\", string(bodyBytes))\n\t\t\t}\n\t\t}\n\t\t// seek on a missing file\n\t\tremotePath = fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName+\"_missing\")\n\t\treq, err = http.NewRequest(http.MethodGet, remotePath, nil)\n\t\tif assert.NoError(t, err) {\n\t\t\thttpClient := httpclient.GetHTTPClient()\n\t\t\treq.SetBasicAuth(user.Username, defaultPassword)\n\t\t\treq.Header.Set(\"Range\", \"bytes=5-\")\n\t\t\tresp, err := httpClient.Do(req)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tdefer resp.Body.Close()\n\t\t\t\tassert.Equal(t, http.StatusNotFound, resp.StatusCode)\n\t\t\t}\n\t\t}\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\tassert.NoError(t, err)\n\t}\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestContentTypeGET(t *testing.T) {\n\tuser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)\n\tassert.NoError(t, err)\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(64)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\terr = uploadFileWithRawClient(testFilePath, testFileName+\".sftpgo\", user.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.NoError(t, err)\n\tremotePath := fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, testFileName+\".sftpgo\")\n\treq, err := http.NewRequest(http.MethodGet, remotePath, nil)\n\tif assert.NoError(t, err) {\n\t\thttpClient := httpclient.GetHTTPClient()\n\t\treq.SetBasicAuth(user.Username, defaultPassword)\n\t\tresp, err := httpClient.Do(req)\n\t\tif assert.NoError(t, err) {\n\t\t\tdefer resp.Body.Close()\n\t\t\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\t\t\tassert.Equal(t, \"application/sftpgo\", resp.Header.Get(\"Content-Type\"))\n\t\t}\n\t}\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestHEAD(t *testing.T) {\n\tu := getTestUser()\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\trootPath := fmt.Sprintf(\"http://%v\", webDavServerAddr)\n\thttpClient := httpclient.GetHTTPClient()\n\treq, err := http.NewRequest(http.MethodHead, rootPath, nil)\n\tif assert.NoError(t, err) {\n\t\treq.SetBasicAuth(u.Username, u.Password)\n\t\tresp, err := httpClient.Do(req)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, http.StatusMultiStatus, resp.StatusCode)\n\t\t\tassert.Equal(t, \"text/xml; charset=utf-8\", resp.Header.Get(\"Content-Type\"))\n\t\t\tresp.Body.Close()\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestGETAsPROPFIND(t *testing.T) {\n\tu := getTestUser()\n\tsubDir1 := \"/sub1\"\n\tu.Permissions[subDir1] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\trootPath := fmt.Sprintf(\"http://%v/\", webDavServerAddr)\n\thttpClient := httpclient.GetHTTPClient()\n\treq, err := http.NewRequest(http.MethodGet, rootPath, nil)\n\tif assert.NoError(t, err) {\n\t\treq.SetBasicAuth(u.Username, u.Password)\n\t\tresp, err := httpClient.Do(req)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, http.StatusMultiStatus, resp.StatusCode)\n\t\t\tresp.Body.Close()\n\t\t}\n\t}\n\tclient := getWebDavClient(user, false, nil)\n\terr = client.MkdirAll(path.Join(subDir1, \"sub\", \"sub1\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tsubPath := fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, subDir1)\n\treq, err = http.NewRequest(http.MethodGet, subPath, nil)\n\tif assert.NoError(t, err) {\n\t\treq.SetBasicAuth(u.Username, u.Password)\n\t\tresp, err := httpClient.Do(req)\n\t\tif assert.NoError(t, err) {\n\t\t\t// before the performance patch we have a 500 here, now we have 207 but an empty list\n\t\t\t//assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)\n\t\t\tassert.Equal(t, http.StatusMultiStatus, resp.StatusCode)\n\t\t\tresp.Body.Close()\n\t\t}\n\t}\n\t// we cannot stat the sub at all\n\tsubPath1 := fmt.Sprintf(\"http://%v/%v\", webDavServerAddr, path.Join(subDir1, \"sub\"))\n\treq, err = http.NewRequest(http.MethodGet, subPath1, nil)\n\tif assert.NoError(t, err) {\n\t\treq.SetBasicAuth(u.Username, u.Password)\n\t\tresp, err := httpClient.Do(req)\n\t\tif assert.NoError(t, err) {\n\t\t\t// here the stat will fail, so the request will not be changed in propfind\n\t\t\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\t\t\tresp.Body.Close()\n\t\t}\n\t}\n\n\t// we have no permission, we get an empty list\n\tfiles, err := client.ReadDir(subDir1)\n\tassert.NoError(t, err)\n\tassert.Len(t, files, 0)\n\t// if we grant the permissions the files are listed\n\tuser.Permissions[subDir1] = []string{dataprovider.PermDownload, dataprovider.PermListItems}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tfiles, err = client.ReadDir(subDir1)\n\tassert.NoError(t, err)\n\tassert.Len(t, files, 1)\n\t// PROPFIND with infinity depth is forbidden\n\treq, err = http.NewRequest(http.MethodGet, rootPath, nil)\n\tif assert.NoError(t, err) {\n\t\treq.SetBasicAuth(u.Username, u.Password)\n\t\treq.Header.Set(\"Depth\", \"infinity\")\n\t\tresp, err := httpClient.Do(req)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, http.StatusForbidden, resp.StatusCode)\n\t\t\tresp.Body.Close()\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestStat(t *testing.T) {\n\tu := getTestUser()\n\tu.Permissions[\"/subdir\"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermDownload}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, nil)\n\tsubDir := \"subdir\"\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = client.Mkdir(subDir, os.ModePerm)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,\n\t\ttrue, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(\"/\", subDir, testFileName), user.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\tuser.Permissions[\"/subdir\"] = []string{dataprovider.PermUpload, dataprovider.PermDownload}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\t_, err = client.Stat(testFileName)\n\tassert.NoError(t, err)\n\t_, err = client.Stat(path.Join(\"/\", subDir, testFileName))\n\tassert.Error(t, err)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestUploadOverwriteVfolder(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 1000\n\tvdir := \"/vdir\"\n\tmappedPath := filepath.Join(os.TempDir(), \"mappedDir\")\n\tfolderName := filepath.Base(mappedPath)\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdir,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\terr = os.MkdirAll(mappedPath, os.ModePerm)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\tfiles, err := client.ReadDir(\".\")\n\tassert.NoError(t, err)\n\tvdirFound := false\n\tfor _, info := range files {\n\t\tif info.Name() == path.Base(vdir) {\n\t\t\tvdirFound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tassert.True(t, vdirFound)\n\tinfo, err := client.Stat(vdir)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, path.Base(vdir), info.Name())\n\t}\n\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(vdir, testFileName), user.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\tfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), folder.UsedQuotaSize)\n\tassert.Equal(t, 0, folder.UsedQuotaFiles)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\n\terr = uploadFileWithRawClient(testFilePath, path.Join(vdir, testFileName), user.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\tfolder, _, err = httpdtest.GetFolderByName(folderName, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(0), folder.UsedQuotaSize)\n\tassert.Equal(t, 0, folder.UsedQuotaFiles)\n\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, testFileSize, user.UsedQuotaSize)\n\tassert.Equal(t, 1, user.UsedQuotaFiles)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestOsErrors(t *testing.T) {\n\tu := getTestUser()\n\tvdir := \"/vdir\"\n\tmappedPath := filepath.Join(os.TempDir(), \"mappedDir\")\n\tfolderName := filepath.Base(mappedPath)\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdir,\n\t\tQuotaSize:   -1,\n\t\tQuotaFiles:  -1,\n\t})\n\tf := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, false, nil)\n\tfiles, err := client.ReadDir(\".\")\n\tassert.NoError(t, err)\n\tassert.Len(t, files, 1)\n\tinfo, err := client.Stat(vdir)\n\tassert.NoError(t, err)\n\tassert.True(t, info.IsDir())\n\t// now remove the folder mapped to vdir. It still appear in directory listing\n\t// virtual folders are automatically added\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n\tfiles, err = client.ReadDir(\".\")\n\tassert.NoError(t, err)\n\tassert.Len(t, files, 1)\n\terr = createTestFile(filepath.Join(user.GetHomeDir(), testFileName), 32768)\n\tassert.NoError(t, err)\n\tfiles, err = client.ReadDir(\".\")\n\tassert.NoError(t, err)\n\tif assert.Len(t, files, 2) {\n\t\tvar names []string\n\t\tfor _, info := range files {\n\t\t\tnames = append(names, info.Name())\n\t\t}\n\t\tassert.Contains(t, names, testFileName)\n\t\tassert.Contains(t, names, \"vdir\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestMiscCommands(t *testing.T) {\n\tu := getTestUser()\n\tu.QuotaFiles = 100\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tu.QuotaFiles = 100\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tfor _, user := range []dataprovider.User{localUser, sftpUser} {\n\t\tdir := \"testDir\"\n\t\tclient := getWebDavClient(user, true, nil)\n\t\terr = client.MkdirAll(path.Join(dir, \"sub1\", \"sub2\"), os.ModePerm)\n\t\tassert.NoError(t, err)\n\t\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\t\ttestFileSize := int64(65535)\n\t\terr = createTestFile(testFilePath, testFileSize)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath, path.Join(dir, testFileName), user.Username,\n\t\t\tdefaultPassword, true, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath, path.Join(dir, \"sub1\", testFileName), user.Username,\n\t\t\tdefaultPassword, true, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = uploadFileWithRawClient(testFilePath, path.Join(dir, \"sub1\", \"sub2\", testFileName), user.Username,\n\t\t\tdefaultPassword, true, testFileSize, client)\n\t\tassert.NoError(t, err)\n\t\terr = client.Copy(dir, dir+\"_copy\", false)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 6, user.UsedQuotaFiles)\n\t\tassert.Equal(t, 6*testFileSize, user.UsedQuotaSize)\n\t\terr = client.Copy(dir, dir+\"_copy1\", false) //nolint:goconst\n\t\tassert.NoError(t, err)\n\t\terr = client.Copy(dir+\"_copy\", dir+\"_copy1\", false)\n\t\tassert.Error(t, err)\n\t\terr = client.Copy(dir+\"_copy\", dir+\"_copy1\", true)\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 9, user.UsedQuotaFiles)\n\t\tassert.Equal(t, 9*testFileSize, user.UsedQuotaSize)\n\t\terr = client.Rename(dir+\"_copy1\", dir+\"_copy2\", false)\n\t\tassert.NoError(t, err)\n\t\terr = client.Remove(path.Join(dir+\"_copy\", testFileName))\n\t\tassert.NoError(t, err)\n\t\terr = client.Rename(dir+\"_copy2\", dir+\"_copy\", true)\n\t\tassert.NoError(t, err)\n\t\terr = client.Copy(dir+\"_copy\", dir+\"_copy1\", false)\n\t\tassert.NoError(t, err)\n\t\terr = client.RemoveAll(dir + \"_copy1\")\n\t\tassert.NoError(t, err)\n\t\tuser, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 6, user.UsedQuotaFiles)\n\t\tassert.Equal(t, 6*testFileSize, user.UsedQuotaSize)\n\n\t\terr = os.Remove(testFilePath)\n\t\tassert.NoError(t, err)\n\t\tif user.Username == defaultUsername {\n\t\t\terr = os.RemoveAll(user.GetHomeDir())\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\t\t\tassert.NoError(t, err)\n\t\t\tuser.Password = defaultPassword\n\t\t\tuser.ID = 0\n\t\t\tuser.CreatedAt = 0\n\t\t\tuser.QuotaFiles = 0\n\t\t\t_, resp, err := httpdtest.AddUser(user, http.StatusCreated)\n\t\t\tassert.NoError(t, err, string(resp))\n\t\t}\n\t}\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestClientCertificateAuthRevokedCert(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = tlsClient2Username\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\tclient := getWebDavClient(user, true, tlsConfig)\n\terr = checkBasicFunc(client)\n\tif assert.Error(t, err) {\n\t\tif !strings.Contains(err.Error(), \"bad certificate\") && !strings.Contains(err.Error(), \"broken pipe\") {\n\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t}\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestClientCertificateAuth(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificateAndPwd}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\t// TLS username is not enabled, mutual TLS should fail\n\tresp, err := getTLSHTTPClient(tlsConfig).Get(fmt.Sprintf(\"https://%v/\", webDavTLSServerAddr))\n\tif assert.NoError(t, err) {\n\t\tdefer resp.Body.Close()\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode, string(body))\n\t}\n\n\tuser.Filters.TLSUsername = sdk.TLSUsernameCN\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient := getWebDavClient(user, true, tlsConfig)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\tuser.Filters.TLSUsername = sdk.TLSUsernameNone\n\tuser.Filters.TLSCerts = []string{client1Crt}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(user, true, tlsConfig)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(user, true, tlsConfig)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestWrongClientCertificate(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = tlsClient2Username\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificateAndPwd}\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\n\t// the certificate common name is client1 and it does not exists\n\tresp, err := getTLSHTTPClient(tlsConfig).Get(fmt.Sprintf(\"https://%v/\", webDavTLSServerAddr))\n\tif assert.NoError(t, err) {\n\t\tdefer resp.Body.Close()\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode, string(body))\n\t}\n\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\t// now create client1\n\tu = getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate}\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tuser1, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tresp, err = getTLSHTTPClient(tlsConfig).Get(fmt.Sprintf(\"https://%v:%v@%v/\", tlsClient2Username, defaultPassword,\n\t\twebDavTLSServerAddr))\n\tif assert.NoError(t, err) {\n\t\tdefer resp.Body.Close()\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode, string(body))\n\t\tassert.Contains(t, string(body), \"invalid credentials\")\n\t}\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestClientCertificateAuthCachedUser(t *testing.T) {\n\tu := getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificateAndPwd}\n\tuser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\tclient := getWebDavClient(user, true, tlsConfig)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\t// the user is now cached without a password, try a simple password login with and without TLS\n\tclient = getWebDavClient(user, true, nil)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\n\tclient = getWebDavClient(user, false, nil)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\n\t// and now with a wrong password\n\tuser.Password = \"wrong\"\n\tclient = getWebDavClient(user, false, nil)\n\terr = checkBasicFunc(client)\n\tassert.Error(t, err)\n\n\t// allow cert+password only\n\tuser.Password = \"\"\n\tuser.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificate}\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\n\tclient = getWebDavClient(user, true, tlsConfig)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\t// the user is now cached\n\tclient = getWebDavClient(user, true, tlsConfig)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\t// password auth should work too\n\tclient = getWebDavClient(user, false, nil)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\n\tclient = getWebDavClient(user, true, nil)\n\terr = checkBasicFunc(client)\n\tassert.NoError(t, err)\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n}\n\nfunc TestExternatAuthWithClientCert(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificate, dataprovider.LoginMethodPassword}\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, \"\"), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.ExternalAuthHook = extAuthPath\n\tproviderConf.ExternalAuthScope = 0\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\tclient := getWebDavClient(u, true, tlsConfig)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\tresp, err := getTLSHTTPClient(tlsConfig).Get(fmt.Sprintf(\"https://%v:%v@%v/\", tlsClient2Username, defaultPassword,\n\t\twebDavTLSServerAddr))\n\tif assert.NoError(t, err) {\n\t\tdefer resp.Body.Close()\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusUnauthorized, resp.StatusCode, string(body))\n\t\tassert.Contains(t, string(body), \"invalid credentials\")\n\t}\n\n\tuser, _, err := httpdtest.GetUserByUsername(tlsClient1Username, http.StatusOK)\n\tassert.NoError(t, err)\n\tassert.Equal(t, tlsClient1Username, user.Username)\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(extAuthPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestPreLoginHookWithClientCert(t *testing.T) {\n\tif runtime.GOOS == osWindows {\n\t\tt.Skip(\"this test is not available on Windows\")\n\t}\n\tu := getTestUser()\n\tu.Username = tlsClient1Username\n\tu.Filters.TLSUsername = sdk.TLSUsernameCN\n\tu.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificate, dataprovider.LoginMethodPassword}\n\terr := dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf := config.GetProviderConf()\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tproviderConf.PreLoginHook = preLoginPath\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\t_, _, err = httpdtest.GetUserByUsername(tlsClient1Username, http.StatusNotFound)\n\tassert.NoError(t, err)\n\ttlsConfig := &tls.Config{\n\t\tServerName:         \"localhost\",\n\t\tInsecureSkipVerify: true, // use this for tests only\n\t\tMinVersion:         tls.VersionTLS12,\n\t}\n\ttlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))\n\tassert.NoError(t, err)\n\ttlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)\n\tclient := getWebDavClient(u, true, tlsConfig)\n\tassert.NoError(t, checkBasicFunc(client))\n\n\tuser, _, err := httpdtest.GetUserByUsername(tlsClient1Username, http.StatusOK)\n\tassert.NoError(t, err)\n\t// test login with an existing user\n\tclient = getWebDavClient(user, true, tlsConfig)\n\tassert.NoError(t, checkBasicFunc(client))\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)\n\tassert.NoError(t, err)\n\t// update the user to remove it from the cache\n\tuser.Password = defaultPassword\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(user, true, tlsConfig)\n\tassert.Error(t, checkBasicFunc(client))\n\t// update the user to remove it from the cache\n\tuser.Password = defaultPassword\n\tuser, _, err = httpdtest.UpdateUser(user, http.StatusOK, \"\")\n\tassert.NoError(t, err)\n\tuser.Status = 0\n\terr = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)\n\tassert.NoError(t, err)\n\tclient = getWebDavClient(user, true, tlsConfig)\n\tassert.Error(t, checkBasicFunc(client))\n\n\t_, err = httpdtest.RemoveUser(user, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user.GetHomeDir())\n\tassert.NoError(t, err)\n\terr = dataprovider.Close()\n\tassert.NoError(t, err)\n\terr = config.LoadConfig(configDir, \"\")\n\tassert.NoError(t, err)\n\tproviderConf = config.GetProviderConf()\n\terr = dataprovider.Initialize(providerConf, configDir, true)\n\tassert.NoError(t, err)\n\terr = os.Remove(preLoginPath)\n\tassert.NoError(t, err)\n}\n\nfunc TestSFTPLoopVirtualFolders(t *testing.T) {\n\tuser1 := getTestUser()\n\tuser2 := getTestUser()\n\tuser1.Username += \"1\"\n\tuser2.Username += \"2\"\n\t// user1 is a local account with a virtual SFTP folder to user2\n\t// user2 has user1 as SFTP fs\n\tfolderName := \"sftp\"\n\tuser1.VirtualFolders = append(user1.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: \"/vdir\",\n\t})\n\tuser2.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tuser2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{\n\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\tEndpoint: sftpServerAddr,\n\t\t\tUsername: user1.Username,\n\t\t},\n\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t}\n\tf := vfs.BaseVirtualFolder{\n\t\tName: folderName,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.SFTPFilesystemProvider,\n\t\t\tSFTPConfig: vfs.SFTPFsConfig{\n\t\t\t\tBaseSFTPFsConfig: sdk.BaseSFTPFsConfig{\n\t\t\t\t\tEndpoint: sftpServerAddr,\n\t\t\t\t\tUsername: user2.Username,\n\t\t\t\t},\n\t\t\t\tPassword: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t}\n\t_, _, err := httpdtest.AddFolder(f, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tuser1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\tuser2, resp, err = httpdtest.AddUser(user2, http.StatusCreated)\n\tassert.NoError(t, err, string(resp))\n\n\tclient := getWebDavClient(user1, true, nil)\n\n\ttestDir := \"tdir\"\n\terr = client.Mkdir(testDir, os.ModePerm)\n\tassert.NoError(t, err)\n\n\tcontents, err := client.ReadDir(\"/\")\n\tassert.NoError(t, err)\n\tif assert.Len(t, contents, 2) {\n\t\texpected := 0\n\t\tfor _, info := range contents {\n\t\t\tswitch info.Name() {\n\t\t\tcase testDir, \"vdir\":\n\t\t\t\tassert.True(t, info.IsDir())\n\t\t\t\texpected++\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"unexpected file/dir %q\", info.Name())\n\t\t\t}\n\t\t}\n\t\tassert.Equal(t, expected, 2)\n\t}\n\n\t_, err = httpdtest.RemoveUser(user1, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user1.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(user2, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(user2.GetHomeDir())\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n}\n\nfunc TestNestedVirtualFolders(t *testing.T) {\n\tu := getTestUser()\n\tlocalUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\tu = getTestSFTPUser()\n\tmappedPathCrypt := filepath.Join(os.TempDir(), \"crypt\")\n\tfolderNameCrypt := filepath.Base(mappedPathCrypt)\n\tvdirCryptPath := \"/vdir/crypt\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameCrypt,\n\t\t},\n\t\tVirtualPath: vdirCryptPath,\n\t})\n\tmappedPath := filepath.Join(os.TempDir(), \"local\")\n\tfolderName := filepath.Base(mappedPath)\n\tvdirPath := \"/vdir/local\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderName,\n\t\t},\n\t\tVirtualPath: vdirPath,\n\t})\n\tmappedPathNested := filepath.Join(os.TempDir(), \"nested\")\n\tfolderNameNested := filepath.Base(mappedPathNested)\n\tvdirNestedPath := \"/vdir/crypt/nested\"\n\tu.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{\n\t\tBaseVirtualFolder: vfs.BaseVirtualFolder{\n\t\t\tName: folderNameNested,\n\t\t},\n\t\tVirtualPath: vdirNestedPath,\n\t\tQuotaFiles:  -1,\n\t\tQuotaSize:   -1,\n\t})\n\tf1 := vfs.BaseVirtualFolder{\n\t\tName: folderNameCrypt,\n\t\tFsConfig: vfs.Filesystem{\n\t\t\tProvider: sdk.CryptedFilesystemProvider,\n\t\t\tCryptConfig: vfs.CryptFsConfig{\n\t\t\t\tPassphrase: kms.NewPlainSecret(defaultPassword),\n\t\t\t},\n\t\t},\n\t\tMappedPath: mappedPathCrypt,\n\t}\n\t_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf2 := vfs.BaseVirtualFolder{\n\t\tName:       folderName,\n\t\tMappedPath: mappedPath,\n\t}\n\t_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)\n\tassert.NoError(t, err)\n\tf3 := vfs.BaseVirtualFolder{\n\t\tName:       folderNameNested,\n\t\tMappedPath: mappedPathNested,\n\t}\n\t_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)\n\tassert.NoError(t, err)\n\tsftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)\n\tassert.NoError(t, err)\n\n\tclient := getWebDavClient(sftpUser, true, nil)\n\tassert.NoError(t, checkBasicFunc(client))\n\ttestFilePath := filepath.Join(homeBasePath, testFileName)\n\ttestFileSize := int64(65535)\n\tlocalDownloadPath := filepath.Join(homeBasePath, testDLFileName)\n\terr = createTestFile(testFilePath, testFileSize)\n\tassert.NoError(t, err)\n\n\terr = uploadFileWithRawClient(testFilePath, testFileName, sftpUser.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = downloadFile(testFileName, localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(\"/vdir\", testFileName), sftpUser.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = downloadFile(path.Join(\"/vdir\", testFileName), localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(vdirPath, testFileName), sftpUser.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = downloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(vdirCryptPath, testFileName), sftpUser.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = downloadFile(path.Join(vdirCryptPath, testFileName), localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = uploadFileWithRawClient(testFilePath, path.Join(vdirNestedPath, testFileName), sftpUser.Username,\n\t\tdefaultPassword, true, testFileSize, client)\n\tassert.NoError(t, err)\n\terr = downloadFile(path.Join(vdirNestedPath, testFileName), localDownloadPath, testFileSize, client)\n\tassert.NoError(t, err)\n\n\terr = os.Remove(testFilePath)\n\tassert.NoError(t, err)\n\terr = os.Remove(localDownloadPath)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveUser(localUser, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameCrypt}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)\n\tassert.NoError(t, err)\n\t_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderNameNested}, http.StatusOK)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathCrypt)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPath)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(mappedPathNested)\n\tassert.NoError(t, err)\n\terr = os.RemoveAll(localUser.GetHomeDir())\n\tassert.NoError(t, err)\n\tassert.Eventually(t, func() bool { return len(common.Connections.GetStats(\"\")) == 0 },\n\t\t1*time.Second, 100*time.Millisecond)\n\tassert.Equal(t, int32(0), common.Connections.GetTotalTransfers())\n}\n\nfunc checkBasicFunc(client *gowebdav.Client) error {\n\terr := client.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = client.ReadDir(\"/\")\n\treturn err\n}\n\nfunc checkFileSize(remoteDestPath string, expectedSize int64, client *gowebdav.Client) error {\n\tinfo, err := client.Stat(remoteDestPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif info.Size() != expectedSize {\n\t\treturn fmt.Errorf(\"uploaded file size does not match, actual: %v, expected: %v\", info.Size(), expectedSize)\n\t}\n\treturn nil\n}\n\nfunc uploadFileWithRawClient(localSourcePath string, remoteDestPath string, username, password string,\n\tuseTLS bool, expectedSize int64, client *gowebdav.Client, headers ...dataprovider.KeyValue,\n) error {\n\tsrcFile, err := os.Open(localSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer srcFile.Close()\n\n\tvar tlsConfig *tls.Config\n\trootPath := fmt.Sprintf(\"http://%v/\", webDavServerAddr)\n\tif useTLS {\n\t\trootPath = fmt.Sprintf(\"https://%v/\", webDavTLSServerAddr)\n\t\ttlsConfig = &tls.Config{\n\t\t\tServerName:         \"localhost\",\n\t\t\tInsecureSkipVerify: true, // use this for tests only\n\t\t\tMinVersion:         tls.VersionTLS12,\n\t\t}\n\t}\n\treq, err := http.NewRequest(http.MethodPut, fmt.Sprintf(\"%v%v\", rootPath, remoteDestPath), srcFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.SetBasicAuth(username, password)\n\tfor _, kv := range headers {\n\t\treq.Header.Set(kv.Key, kv.Value)\n\t}\n\thttpClient := &http.Client{Timeout: 10 * time.Second}\n\tif tlsConfig != nil {\n\t\tcustomTransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\tcustomTransport.TLSClientConfig = tlsConfig\n\t\thttpClient.Transport = customTransport\n\t}\n\tdefer httpClient.CloseIdleConnections()\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusCreated {\n\t\treturn fmt.Errorf(\"unexpected status code: %v\", resp.StatusCode)\n\t}\n\tif expectedSize > 0 {\n\t\treturn checkFileSize(remoteDestPath, expectedSize, client)\n\t}\n\treturn nil\n}\n\n// This method is buggy. I have to find time to better investigate and eventually report the issue upstream.\n// For now we upload using the uploadFileWithRawClient method\n/*func uploadFile(localSourcePath string, remoteDestPath string, expectedSize int64, client *gowebdav.Client) error {\n\tsrcFile, err := os.Open(localSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer srcFile.Close()\n\terr = client.WriteStream(remoteDestPath, srcFile, os.ModePerm)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif expectedSize > 0 {\n\t\treturn checkFileSize(remoteDestPath, expectedSize, client)\n\t}\n\treturn nil\n}*/\n\nfunc downloadFile(remoteSourcePath string, localDestPath string, expectedSize int64, client *gowebdav.Client) error {\n\tdownloadDest, err := os.Create(localDestPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer downloadDest.Close()\n\n\treader, err := client.ReadStream(remoteSourcePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer reader.Close()\n\twritten, err := io.Copy(downloadDest, reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif written != expectedSize {\n\t\treturn fmt.Errorf(\"downloaded file size does not match, actual: %v, expected: %v\", written, expectedSize)\n\t}\n\treturn nil\n}\n\nfunc getTLSHTTPClient(tlsConfig *tls.Config) *http.Client {\n\tcustomTransport := http.DefaultTransport.(*http.Transport).Clone()\n\tcustomTransport.TLSClientConfig = tlsConfig\n\n\treturn &http.Client{\n\t\tTimeout:   5 * time.Second,\n\t\tTransport: customTransport,\n\t}\n}\n\nfunc getWebDavClient(user dataprovider.User, useTLS bool, tlsConfig *tls.Config) *gowebdav.Client {\n\trootPath := fmt.Sprintf(\"http://%v/\", webDavServerAddr)\n\tif useTLS {\n\t\trootPath = fmt.Sprintf(\"https://%v/\", webDavTLSServerAddr)\n\t\tif tlsConfig == nil {\n\t\t\ttlsConfig = &tls.Config{\n\t\t\t\tServerName:         \"localhost\",\n\t\t\t\tInsecureSkipVerify: true, // use this for tests only\n\t\t\t\tMinVersion:         tls.VersionTLS12,\n\t\t\t}\n\t\t}\n\t}\n\tpwd := defaultPassword\n\tif user.Password != \"\" {\n\t\tif user.Password == emptyPwdPlaceholder {\n\t\t\tpwd = \"\"\n\t\t} else {\n\t\t\tpwd = user.Password\n\t\t}\n\t}\n\tclient := gowebdav.NewClient(rootPath, user.Username, pwd)\n\tclient.SetTimeout(10 * time.Second)\n\tif tlsConfig != nil {\n\t\tcustomTransport := http.DefaultTransport.(*http.Transport).Clone()\n\t\tcustomTransport.TLSClientConfig = tlsConfig\n\t\tclient.SetTransport(customTransport)\n\t}\n\treturn client\n}\n\nfunc waitTCPListening(address string) {\n\tfor {\n\t\tconn, err := net.Dial(\"tcp\", address)\n\t\tif err != nil {\n\t\t\tlogger.WarnToConsole(\"tcp server %v not listening: %v\", address, err)\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\t\tlogger.InfoToConsole(\"tcp server %v now listening\", address)\n\t\tconn.Close()\n\t\tbreak\n\t}\n}\n\nfunc getTestUser() dataprovider.User {\n\tuser := dataprovider.User{\n\t\tBaseUser: sdk.BaseUser{\n\t\t\tUsername:       defaultUsername,\n\t\t\tPassword:       defaultPassword,\n\t\t\tHomeDir:        filepath.Join(homeBasePath, defaultUsername),\n\t\t\tStatus:         1,\n\t\t\tExpirationDate: 0,\n\t\t},\n\t}\n\tuser.Permissions = make(map[string][]string)\n\tuser.Permissions[\"/\"] = allPerms\n\treturn user\n}\n\nfunc getTestSFTPUser() dataprovider.User {\n\tu := getTestUser()\n\tu.Username = u.Username + \"_sftp\"\n\tu.FsConfig.Provider = sdk.SFTPFilesystemProvider\n\tu.FsConfig.SFTPConfig.Endpoint = sftpServerAddr\n\tu.FsConfig.SFTPConfig.Username = defaultUsername\n\tu.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)\n\treturn u\n}\n\nfunc getTestUserWithCryptFs() dataprovider.User {\n\tuser := getTestUser()\n\tuser.FsConfig.Provider = sdk.CryptedFilesystemProvider\n\tuser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(\"testPassphrase\")\n\treturn user\n}\n\nfunc getSftpClient(user dataprovider.User) (*ssh.Client, *sftp.Client, error) {\n\tvar sftpClient *sftp.Client\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            user.Username,\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         5 * time.Second,\n\t}\n\tif user.Password != \"\" {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}\n\t} else {\n\t\tconfig.Auth = []ssh.AuthMethod{ssh.Password(defaultPassword)}\n\t}\n\n\tconn, err := ssh.Dial(\"tcp\", sftpServerAddr, config)\n\tif err != nil {\n\t\treturn conn, sftpClient, err\n\t}\n\tsftpClient, err = sftp.NewClient(conn)\n\tif err != nil {\n\t\tconn.Close()\n\t}\n\treturn conn, sftpClient, err\n}\n\nfunc getEncryptedFileSize(size int64) (int64, error) {\n\tencSize, err := sio.EncryptedSize(uint64(size))\n\treturn int64(encSize) + 33, err\n}\n\nfunc getExtAuthScriptContent(user dataprovider.User, password string) []byte {\n\textAuthContent := []byte(\"#!/bin/sh\\n\\n\")\n\tif password != \"\" {\n\t\textAuthContent = append(extAuthContent, []byte(fmt.Sprintf(\"if test \\\"$SFTPGO_AUTHD_USERNAME\\\" = \\\"%s\\\" -a \\\"$SFTPGO_AUTHD_PASSWORD\\\" = \\\"%s\\\"; then\\n\", user.Username, password))...)\n\t} else {\n\t\textAuthContent = append(extAuthContent, []byte(fmt.Sprintf(\"if test \\\"$SFTPGO_AUTHD_USERNAME\\\" = \\\"%s\\\"; then\\n\", user.Username))...)\n\t}\n\tu, _ := json.Marshal(user)\n\textAuthContent = append(extAuthContent, []byte(fmt.Sprintf(\"echo '%s'\\n\", string(u)))...)\n\textAuthContent = append(extAuthContent, []byte(\"else\\n\")...)\n\textAuthContent = append(extAuthContent, []byte(\"echo '{\\\"username\\\":\\\"\\\"}'\\n\")...)\n\textAuthContent = append(extAuthContent, []byte(\"fi\\n\")...)\n\treturn extAuthContent\n}\n\nfunc getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tif nonJSONResponse {\n\t\tcontent = append(content, []byte(\"echo 'text response'\\n\")...)\n\t\treturn content\n\t}\n\tif len(user.Username) > 0 {\n\t\tu, _ := json.Marshal(user)\n\t\tcontent = append(content, []byte(fmt.Sprintf(\"echo '%v'\\n\", string(u)))...)\n\t}\n\treturn content\n}\n\nfunc getExitCodeScriptContent(exitCode int) []byte {\n\tcontent := []byte(\"#!/bin/sh\\n\\n\")\n\tcontent = append(content, []byte(fmt.Sprintf(\"exit %v\", exitCode))...)\n\treturn content\n}\n\nfunc createTestFile(path string, size int64) error {\n\tbaseDir := filepath.Dir(path)\n\tif _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) {\n\t\terr = os.MkdirAll(baseDir, os.ModePerm)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tcontent := make([]byte, size)\n\t_, err := rand.Read(content)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = os.WriteFile(path, content, os.ModePerm)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfi, err := os.Stat(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif fi.Size() != size {\n\t\treturn fmt.Errorf(\"unexpected size %v, expected %v\", fi.Size(), size)\n\t}\n\treturn nil\n}\n\nfunc printLatestLogs(maxNumberOfLines int) {\n\tvar lines []string\n\tf, err := os.Open(logFilePath)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer f.Close()\n\tscanner := bufio.NewScanner(f)\n\tfor scanner.Scan() {\n\t\tlines = append(lines, scanner.Text()+\"\\r\\n\")\n\t\tfor len(lines) > maxNumberOfLines {\n\t\t\tlines = lines[1:]\n\t\t}\n\t}\n\tif scanner.Err() != nil {\n\t\tlogger.WarnToConsole(\"Unable to print latest logs: %v\", scanner.Err())\n\t\treturn\n\t}\n\tfor _, line := range lines {\n\t\tlogger.DebugToConsole(\"%s\", line)\n\t}\n}\n"
  },
  {
    "path": "main.go",
    "content": "// Copyright (C) 2019 Nicola Murino\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, version 3.\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\n// Fully featured and highly configurable SFTP server with optional\n// FTP/S and WebDAV support.\n// For more details about features, installation, configuration and usage\n// please refer to the README inside the source tree:\n// https://github.com/drakkan/sftpgo/blob/main/README.md\npackage main // import \"github.com/drakkan/sftpgo\"\n\nimport (\n\t\"github.com/drakkan/sftpgo/v2/internal/cmd\"\n)\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "openapi/httpfs.yaml",
    "content": "openapi: 3.0.3\ntags:\n  - name: fs\ninfo:\n  title: SFTPGo HTTPFs\n  description: |\n    SFTPGo can use custom storage backend implementations compliant with the API defined here.\n    HTTPFs is a work in progress and makes no API stability promises.\n  version: 0.1.0\n  license:\n    name: AGPL-3.0-only\n    url: 'https://www.gnu.org/licenses/agpl-3.0.en.html'\nservers:\n- url: /v1\nsecurity:\n- ApiKeyAuth: []\n- BasicAuth: []\npaths:\n  /stat/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - fs\n      summary: Describes the named object\n      operationId: stat\n      responses:\n        200:\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/FileInfo'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /open/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n      - name: offset\n        in: query\n        description: 'offset, in bytes, from the start. If not specified 0 must be assumed'\n        required: false\n        schema:\n          type: integer\n          format: int64\n    get:\n      tags:\n        - fs\n      summary: Opens the named file for reading\n      operationId: open\n      responses:\n        '200':\n          description: successful operation\n          content:\n            '*/*':\n              schema:\n                type: string\n                format: binary\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /create/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n      - name: flags\n        in: query\n        description: 'flags to use for opening the file, if omitted O_RDWR|O_CREATE|O_TRUNC must be assumed. Supported flags: https://pkg.go.dev/os#pkg-constants'\n        required: false\n        schema:\n          type: integer\n          format: int32\n      - name: checks\n        in: query\n        description: 'If set to `1`, the parent directory must exist before creating the file'\n        required: false\n        schema:\n          type: integer\n          format: int32\n    post:\n      tags:\n        - fs\n      summary: Creates or opens the named file for writing\n      operationId: create\n      requestBody:\n        content:\n          '*/*':\n            schema:\n              type: string\n              format: binary\n        required: true\n      responses:\n        201:\n          $ref: '#/components/responses/OKResponse'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /rename/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n      - name: target\n        in: query\n        description: target name\n        required: true\n        schema:\n          type: string\n    patch:\n      tags:\n        - fs\n      summary: Renames (moves) source to target\n      operationId: rename\n      responses:\n        200:\n          $ref: '#/components/responses/OKResponse'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /remove/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n    delete:\n      tags:\n        - fs\n      summary: Removes the named file or (empty) directory.\n      operationId: delete\n      responses:\n        200:\n          $ref: '#/components/responses/OKResponse'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /mkdir/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n    post:\n      tags:\n        - fs\n      summary: Creates a new directory with the specified name\n      operationId: mkdir\n      responses:\n        200:\n          $ref: '#/components/responses/OKResponse'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /chmod/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n      - name: mode\n        in: query\n        required: true\n        schema:\n          type: integer\n    patch:\n      tags:\n        - fs\n      summary: Changes the mode of the named file\n      operationId: chmod\n      responses:\n        200:\n          $ref: '#/components/responses/OKResponse'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /chtimes/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n      - name: access_time\n        in: query\n        required: true\n        schema:\n          type: string\n          format: date-time\n      - name: modification_time\n        in: query\n        required: true\n        schema:\n          type: string\n          format: date-time\n    patch:\n      tags:\n        - fs\n      summary: Changes the access and modification time of the named file\n      operationId: chtimes\n      responses:\n        200:\n          $ref: '#/components/responses/OKResponse'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /truncate/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n      - name: size\n        in: query\n        required: true\n        description: 'new file size in bytes'\n        schema:\n          type: integer\n          format: int64\n    patch:\n      tags:\n        - fs\n      summary: Changes the size of the named file\n      operationId: truncate\n      responses:\n        200:\n          $ref: '#/components/responses/OKResponse'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /readdir/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - fs\n      summary: Reads the named directory and returns the contents\n      operationId: readdir\n      responses:\n        200:\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/FileInfo'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /dirsize/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - fs\n      summary: Returns the number of files and the size for the named directory including any sub-directory\n      operationId: dirsize\n      responses:\n        200:\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  files:\n                    type: integer\n                    description: 'Total number of files'\n                  size:\n                    type: integer\n                    format: int64\n                    description: 'Total size of files'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /mimetype/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - fs\n      summary: Returns the mime type for the named file\n      operationId: mimetype\n      responses:\n        200:\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  mime:\n                    type: string\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /statvfs/{name}:\n    parameters:\n      - name: name\n        in: path\n        description: object name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - fs\n      summary: Returns the VFS stats for the specified path\n      operationId: statvfs\n      responses:\n        200:\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/StatVFS'\n        401:\n          $ref: '#/components/responses/Unauthorized'\n        403:\n          $ref: '#/components/responses/Forbidden'\n        404:\n          $ref: '#/components/responses/NotFound'\n        500:\n          $ref: '#/components/responses/InternalServerError'\n        501:\n          $ref: '#/components/responses/NotImplemented'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\ncomponents:\n  responses:\n    OKResponse:\n      description: successful operation\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    BadRequest:\n      description: Bad Request\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    Unauthorized:\n      description: Unauthorized\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    Forbidden:\n      description: Forbidden\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    NotFound:\n      description: Not Found\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    NotImplemented:\n      description: Not Implemented\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    Conflict:\n      description: Conflict\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    RequestEntityTooLarge:\n      description: Request Entity Too Large, max allowed size exceeded\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    InternalServerError:\n      description: Internal Server Error\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    DefaultResponse:\n      description: Unexpected Error\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n  schemas:\n    ApiResponse:\n      type: object\n      properties:\n        message:\n          type: string\n          description: 'message, can be empty'\n        error:\n          type: string\n          description: error description if any\n    FileInfo:\n      type: object\n      properties:\n        name:\n          type: string\n          description: base name of the file\n        size:\n          type: integer\n          format: int64\n          description: length in bytes for regular files; system-dependent for others\n        mode:\n          type: integer\n          description: |\n            File mode and permission bits. More details here: https://golang.org/pkg/io/fs/#FileMode.\n            Let's see some examples:\n            - for a directory mode&2147483648 != 0\n            - for a symlink mode&134217728 != 0\n            - for a regular file mode&2401763328 == 0\n        last_modified:\n          type: string\n          format: date-time\n    StatVFS:\n      type: object\n      properties:\n        bsize:\n          type: integer\n          description: file system block size\n        frsize:\n          type: integer\n          description: fundamental fs block size\n        blocks:\n          type: integer\n          description: number of blocks\n        bfree:\n          type: integer\n          description: free blocks in file system\n        bavail:\n          type: integer\n          description: free blocks for non-root\n        files:\n          type: integer\n          description: total file inodes\n        ffree:\n          type: integer\n          description: free file inodes\n        favail:\n          type: integer\n          description: free file inodes for non-root\n        fsid:\n          type: integer\n          description: file system id\n        flag:\n          type: integer\n          description: bit mask of f_flag values\n        namemax:\n          type: integer\n          description: maximum filename length\n  securitySchemes:\n    BasicAuth:\n      type: http\n      scheme: basic\n    ApiKeyAuth:\n      type: apiKey\n      in: header\n      name: X-API-KEY"
  },
  {
    "path": "openapi/openapi.yaml",
    "content": "openapi: 3.0.3\ntags:\n  - name: healthcheck\n  - name: token\n  - name: maintenance\n  - name: admins\n  - name: API keys\n  - name: connections\n  - name: IP Lists\n  - name: defender\n  - name: quota\n  - name: folders\n  - name: groups\n  - name: roles\n  - name: users\n  - name: data retention\n  - name: events\n  - name: metadata\n  - name: user APIs\n  - name: public shares\n  - name: event manager\ninfo:\n  title: SFTPGo\n  description: |\n    SFTPGo allows you to securely share your files over SFTP and optionally over HTTP/S, FTP/S and WebDAV as well.\n    Several storage backends are supported and they are configurable per-user, so you can serve a local directory for a user and an S3 bucket (or part of it) for another one.\n    SFTPGo also supports virtual folders, a virtual folder can use any of the supported storage backends. So you can have, for example, a user with the S3 backend mapping a Google Cloud Storage bucket (or part of it) on a specified path and an encrypted local filesystem on another one.\n    Virtual folders can be private or shared among multiple users, for shared virtual folders you can define different quota limits for each user.\n    SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.\n    The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.\n    From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.\n  version: v2.7.0\n  contact:\n    name: API support\n    url: 'https://github.com/drakkan/sftpgo'\n  license:\n    name: AGPL-3.0-only\n    url: 'https://www.gnu.org/licenses/agpl-3.0.en.html'\nservers:\n  - url: /api/v2\nsecurity:\n  - BearerAuth: []\n  - APIKeyAuth: []\npaths:\n  /healthz:\n    get:\n      security: []\n      servers:\n        - url: /\n      tags:\n        - healthcheck\n      summary: health check\n      description: This endpoint can be used to check if the application is running and responding to requests\n      operationId: healthz\n      responses:\n        '200':\n          description: successful operation\n          content:\n            text/plain; charset=utf-8:\n              schema:\n                type: string\n                example: ok\n  /shares/{id}:\n    parameters:\n      - name: id\n        in: path\n        description: the share id\n        required: true\n        schema:\n          type: string\n    get:\n      security:\n        - BasicAuth: []\n      tags:\n        - public shares\n      summary: Download shared files and folders as a single zip file\n      description: A zip file, containing the shared files and folders, will be generated on the fly and returned as response body. Only folders and regular files will be included in the zip. The share must be defined with the read scope and the associated user must have list and download permissions\n      operationId: get_share\n      parameters:\n        - in: query\n          name: compress\n          schema:\n            type: boolean\n            default: true\n          required: false\n      responses:\n        '200':\n          description: successful operation\n          content:\n            '*/*':\n              schema:\n                type: string\n                format: binary\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      security:\n        - BasicAuth: []\n      tags:\n        - public shares\n      summary: Upload one or more files to the shared path\n      description: The share must be defined with the write scope and the associated user must have the upload permission\n      operationId: upload_to_share\n      requestBody:\n        content:\n          multipart/form-data:\n            schema:\n              type: object\n              properties:\n                filenames:\n                  type: array\n                  items:\n                    type: string\n                    format: binary\n                  minItems: 1\n                  uniqueItems: true\n        required: true\n      responses:\n        '201':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '413':\n          $ref: '#/components/responses/RequestEntityTooLarge'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /shares/{id}/files:\n    parameters:\n      - name: id\n        in: path\n        description: the share id\n        required: true\n        schema:\n          type: string\n    get:\n      security:\n        - BasicAuth: []\n      tags:\n        - public shares\n      summary: Download a single file\n      description: Returns the file contents as response body. The share must have exactly one path defined and it must be a directory for this to work\n      operationId: download_share_file\n      parameters:\n        - in: query\n          name: path\n          required: true\n          description: Path to the file to download. It must be URL encoded, for example the path \"my dir/àdir/file.txt\" must be sent as \"my%20dir%2F%C3%A0dir%2Ffile.txt\"\n          schema:\n            type: string\n        - in: query\n          name: inline\n          required: false\n          description: 'If set, the response will not have the Content-Disposition header set to `attachment`'\n          schema:\n            type: string\n      responses:\n        '200':\n          description: successful operation\n          content:\n            '*/*':\n              schema:\n                type: string\n                format: binary\n        '206':\n          description: successful operation\n          content:\n            '*/*':\n              schema:\n                type: string\n                format: binary\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /shares/{id}/dirs:\n    parameters:\n      - name: id\n        in: path\n        description: the share id\n        required: true\n        schema:\n          type: string\n    get:\n      security:\n        - BasicAuth: []\n      tags:\n        - public shares\n      summary: Read directory contents\n      description: Returns the contents of the specified directory for the specified share. The share must have exactly one path defined and it must be a directory for this to work\n      operationId: get_share_dir_contents\n      parameters:\n        - in: query\n          name: path\n          description: Path to the folder to read. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\". If empty or missing the user's start directory is assumed. If relative, the user's start directory is used as the base\n          schema:\n            type: string\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/DirEntry'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /shares/{id}/{fileName}:\n    parameters:\n      - name: id\n        in: path\n        description: the share id\n        required: true\n        schema:\n          type: string\n      - name: fileName\n        in: path\n        description: the name of the new file. It must be path encoded. Sub directories are not accepted\n        required: true\n        schema:\n          type: string\n      - name: X-SFTPGO-MTIME\n        in: header\n        schema:\n          type: integer\n        description: File modification time as unix timestamp in milliseconds\n    post:\n      security:\n        - BasicAuth: []\n      tags:\n        - public shares\n      summary: Upload a single file to the shared path\n      description: The share must be defined with the write scope and the associated user must have the upload/overwrite permissions\n      operationId: upload_single_to_share\n      requestBody:\n        content:\n          application/*:\n            schema:\n              type: string\n              format: binary\n          text/*:\n            schema:\n              type: string\n              format: binary\n          image/*:\n            schema:\n              type: string\n              format: binary\n          audio/*:\n            schema:\n              type: string\n              format: binary\n          video/*:\n            schema:\n              type: string\n              format: binary\n        required: true\n      responses:\n        '201':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '413':\n          $ref: '#/components/responses/RequestEntityTooLarge'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /token:\n    get:\n      security:\n        - BasicAuth: []\n      tags:\n        - token\n      summary: Get a new admin access token\n      description: Returns an access token and its expiration\n      operationId: get_token\n      parameters:\n        - in: header\n          name: X-SFTPGO-OTP\n          schema:\n            type: string\n          required: false\n          description: 'If you have 2FA configured for the admin attempting to log in you need to set the authentication code using this header parameter'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Token'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /logout:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - token\n      summary: Invalidate an admin access token\n      description: Allows to invalidate an admin token before its expiration\n      operationId: logout\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/token:\n    get:\n      security:\n        - BasicAuth: []\n      tags:\n        - token\n      summary: Get a new user access token\n      description: Returns an access token and its expiration\n      operationId: get_user_token\n      parameters:\n        - in: header\n          name: X-SFTPGO-OTP\n          schema:\n            type: string\n          required: false\n          description: 'If you have 2FA configured, for the HTTP protocol, for the user attempting to log in you need to set the authentication code using this header parameter'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Token'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/logout:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - token\n      summary: Invalidate a user access token\n      description: Allows to invalidate a client token before its expiration\n      operationId: client_logout\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /version:\n    get:\n      tags:\n        - maintenance\n      summary: Get version details\n      description: 'Returns version details such as the version number, build date, commit hash and enabled features'\n      operationId: get_version\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/VersionInfo'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /admin/changepwd:\n    put:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Change admin password\n      description: Changes the password for the logged in admin\n      operationId: change_admin_password\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PwdChange'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /admin/profile:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Get admin profile\n      description: 'Returns the profile for the logged in admin'\n      operationId: get_admin_profile\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/AdminProfile'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Update admin profile\n      description: 'Allows to update the profile for the logged in admin'\n      operationId: update_admin_profile\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/AdminProfile'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /admin/2fa/recoverycodes:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Get recovery codes\n      description: 'Returns the recovery codes for the logged in admin. Recovery codes can be used if the admin loses access to their second factor auth device. Recovery codes are returned unencrypted'\n      operationId: get_admin_recovery_codes\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/RecoveryCode'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Generate recovery codes\n      description: 'Generates new recovery codes for the logged in admin. Generating new recovery codes you automatically invalidate old ones'\n      operationId: generate_admin_recovery_codes\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: string\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /admin/totp/configs:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Get available TOTP configuration\n      description: Returns the available TOTP configurations for the logged in admin\n      operationId: get_admin_totp_configs\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/TOTPConfig'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /admin/totp/generate:\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Generate a new TOTP secret\n      description: 'Generates a new TOTP secret, including the QR code as png, using the specified configuration for the logged in admin'\n      operationId: generate_admin_totp_secret\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                config_name:\n                  type: string\n                  description: 'name of the configuration to use to generate the secret'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  config_name:\n                    type: string\n                  issuer:\n                    type: string\n                  secret:\n                    type: string\n                  url:\n                    type: string\n                  qr_code:\n                    type: string\n                    format: byte\n                    description: 'QR code png encoded as BASE64'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /admin/totp/validate:\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Validate a one time authentication code\n      description: 'Checks if the given authentication code can be validated using the specified secret and config name'\n      operationId: validate_admin_totp_secret\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                config_name:\n                  type: string\n                  description: 'name of the configuration to use to validate the passcode'\n                passcode:\n                  type: string\n                  description: 'passcode to validate'\n                secret:\n                  type: string\n                  description: 'secret to use to validate the passcode'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Passcode successfully validated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /admin/totp/save:\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - admins\n      summary: Save a TOTP config\n      description: 'Saves the specified TOTP config for the logged in admin'\n      operationId: save_admin_totp_config\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/AdminTOTPConfig'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: TOTP configuration saved\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /connections:\n    get:\n      tags:\n        - connections\n      summary: Get connections details\n      description: Returns the active users and info about their current uploads/downloads\n      operationId: get_connections\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/ConnectionStatus'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/connections/{connectionID}':\n    delete:\n      tags:\n        - connections\n      summary: Close connection\n      description: Terminates an active connection\n      operationId: close_connection\n      parameters:\n        - name: connectionID\n          in: path\n          description: ID of the connection to close\n          required: true\n          schema:\n            type: string\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Connection closed\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /iplists/{type}:\n    parameters:\n      - name: type\n        in: path\n        description: IP list type\n        required: true\n        schema:\n          $ref: '#/components/schemas/IPListType'\n    get:\n      tags:\n        - IP Lists\n      summary: Get IP list entries\n      description: Returns an array with one or more IP list entry\n      operationId: get_ip_list_entries\n      parameters:\n        - in: query\n          name: filter\n          schema:\n            type: string\n          description: restrict results to ipornet matching or starting with this filter\n        - in: query\n          name: from\n          schema:\n            type: string\n          description: ipornet to start from\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering entries by ipornet field. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/IPListEntry'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - IP Lists\n      summary: Add a new IP list entry\n      description: Add an IP address or a CIDR network to a supported list\n      operationId: add_ip_list_entry\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/IPListEntry'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            Location:\n              schema:\n                type: string\n              description: 'URI of the newly created object'\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Entry added\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /iplists/{type}/{ipornet}:\n    parameters:\n      - name: type\n        in: path\n        description: IP list type\n        required: true\n        schema:\n          $ref: '#/components/schemas/IPListType'\n      - name: ipornet\n        in: path\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - IP Lists\n      summary: Find entry by ipornet\n      description: Returns the entry with the given ipornet if it exists.\n      operationId: get_ip_list_by_ipornet\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/IPListEntry'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - IP Lists\n      summary: Update IP list entry\n      description: Updates an existing IP list entry\n      operationId: update_ip_list_entry\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/IPListEntry'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Entry updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - IP Lists\n      summary: Delete IP list entry\n      description: Deletes an existing IP list entry\n      operationId: delete_ip_list_entry\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Entry deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /defender/hosts:\n    get:\n      tags:\n        - defender\n      summary: Get hosts\n      description: Returns hosts that are banned or for which some violations have been detected\n      operationId: get_defender_hosts\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/DefenderEntry'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /defender/hosts/{id}:\n    parameters:\n      - name: id\n        in: path\n        description: host id\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - defender\n      summary: Get host by id\n      description: Returns the host with the given id, if it exists\n      operationId: get_defender_host_by_id\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/DefenderEntry'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - defender\n      summary: Removes a host from the defender lists\n      description: Unbans the specified host or clears its violations\n      operationId: delete_defender_host_by_id\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /retention/users/checks:\n    get:\n      tags:\n        - data retention\n      summary: Get retention checks\n      description: Returns the active retention checks\n      operationId: get_users_retention_checks\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/RetentionCheck'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /quotas/users/scans:\n    get:\n      tags:\n        - quota\n      summary: Get active user quota scans\n      description: Returns the active user quota scans\n      operationId: get_users_quota_scans\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/QuotaScan'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /quotas/users/{username}/scan:\n    parameters:\n      - name: username\n        in: path\n        description: the username\n        required: true\n        schema:\n          type: string\n    post:\n      tags:\n        - quota\n      summary: Start a user quota scan\n      description: Starts a new quota scan for the given user. A quota scan updates the number of files and their total size for the specified user and the virtual folders, if any, included in his quota\n      operationId: start_user_quota_scan\n      responses:\n        '202':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Scan started\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          $ref: '#/components/responses/Conflict'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /quotas/users/{username}/usage:\n    parameters:\n      - name: username\n        in: path\n        description: the username\n        required: true\n        schema:\n          type: string\n      - in: query\n        name: mode\n        required: false\n        description: the update mode specifies if the given quota usage values should be added or replace the current ones\n        schema:\n          type: string\n          enum:\n            - add\n            - reset\n          description: |\n            Update type:\n                * `add` - add the specified quota limits to the current used ones\n                * `reset` - reset the values to the specified ones. This is the default\n          example: reset\n    put:\n      tags:\n        - quota\n      summary: Update disk quota usage limits\n      description: Sets the current used quota limits for the given user\n      operationId: user_quota_update_usage\n      requestBody:\n        required: true\n        description: 'If used_quota_size and used_quota_files are missing they will default to 0, this means that if mode is \"add\" the current value, for the missing field, will remain unchanged, if mode is \"reset\" the missing field is set to 0'\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/QuotaUsage'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Quota updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          $ref: '#/components/responses/Conflict'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /quotas/users/{username}/transfer-usage:\n    parameters:\n      - name: username\n        in: path\n        description: the username\n        required: true\n        schema:\n          type: string\n      - in: query\n        name: mode\n        required: false\n        description: the update mode specifies if the given quota usage values should be added or replace the current ones\n        schema:\n          type: string\n          enum:\n            - add\n            - reset\n          description: |\n            Update type:\n                * `add` - add the specified quota limits to the current used ones\n                * `reset` - reset the values to the specified ones. This is the default\n          example: reset\n    put:\n      tags:\n        - quota\n      summary: Update transfer quota usage limits\n      description: Sets the current used transfer quota limits for the given user\n      operationId: user_transfer_quota_update_usage\n      requestBody:\n        required: true\n        description: 'If used_upload_data_transfer and used_download_data_transfer are missing they will default to 0, this means that if mode is \"add\" the current value, for the missing field, will remain unchanged, if mode is \"reset\" the missing field is set to 0'\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/TransferQuotaUsage'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Quota updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          $ref: '#/components/responses/Conflict'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /quotas/folders/scans:\n    get:\n      tags:\n        - quota\n      summary: Get active folder quota scans\n      description: Returns the active folder quota scans\n      operationId: get_folders_quota_scans\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/FolderQuotaScan'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /quotas/folders/{name}/scan:\n    parameters:\n      - name: name\n        in: path\n        description: folder name\n        required: true\n        schema:\n          type: string\n    post:\n      tags:\n        - quota\n      summary: Start a folder quota scan\n      description: Starts a new quota scan for the given folder. A quota scan update the number of files and their total size for the specified folder\n      operationId: start_folder_quota_scan\n      responses:\n        '202':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Scan started\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          $ref: '#/components/responses/Conflict'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /quotas/folders/{name}/usage:\n    parameters:\n      - name: name\n        in: path\n        description: folder name\n        required: true\n        schema:\n          type: string\n      - in: query\n        name: mode\n        required: false\n        description: the update mode specifies if the given quota usage values should be added or replace the current ones\n        schema:\n          type: string\n          enum:\n            - add\n            - reset\n          description: |\n            Update type:\n                * `add` - add the specified quota limits to the current used ones\n                * `reset` - reset the values to the specified ones. This is the default\n          example: reset\n    put:\n      tags:\n        - quota\n      summary: Update folder quota usage limits\n      description: Sets the current used quota limits for the given folder\n      operationId: folder_quota_update_usage\n      parameters:\n        - in: query\n          name: mode\n          required: false\n          description: the update mode specifies if the given quota usage values should be added or replace the current ones\n          schema:\n            type: string\n            enum:\n              - add\n              - reset\n            description: |\n              Update type:\n                * `add` - add the specified quota limits to the current used ones\n                * `reset` - reset the values to the specified ones. This is the default\n            example: reset\n      requestBody:\n        required: true\n        description: 'If used_quota_size and used_quota_files are missing they will default to 0, this means that if mode is \"add\" the current value, for the missing field, will remain unchanged, if mode is \"reset\" the missing field is set to 0'\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/QuotaUsage'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Quota updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '409':\n          $ref: '#/components/responses/Conflict'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /folders:\n    get:\n      tags:\n        - folders\n      summary: Get folders\n      description: Returns an array with one or more folders\n      operationId: get_folders\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering folders by name. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/BaseVirtualFolder'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - folders\n      summary: Add folder\n      operationId: add_folder\n      description: Adds a new folder. A quota scan is required to update the used files/size\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BaseVirtualFolder'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            Location:\n              schema:\n                type: string\n              description: 'URI of the newly created object'\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/BaseVirtualFolder'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/folders/{name}':\n    parameters:\n      - name: name\n        in: path\n        description: folder name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - folders\n      summary: Find folders by name\n      description: Returns the folder with the given name if it exists.\n      operationId: get_folder_by_name\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/BaseVirtualFolder'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - folders\n      summary: Update folder\n      description: Updates an existing folder\n      operationId: update_folder\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BaseVirtualFolder'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Folder updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - folders\n      summary: Delete folder\n      description: Deletes an existing folder\n      operationId: delete_folder\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: User deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /groups:\n    get:\n      tags:\n        - groups\n      summary: Get groups\n      description: Returns an array with one or more groups\n      operationId: get_groups\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering groups by name. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Group'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - groups\n      summary: Add group\n      operationId: add_group\n      description: Adds a new group\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/Group'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            Location:\n              schema:\n                type: string\n              description: 'URI of the newly created object'\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Group'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/groups/{name}':\n    parameters:\n      - name: name\n        in: path\n        description: group name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - groups\n      summary: Find groups by name\n      description: Returns the group with the given name if it exists.\n      operationId: get_group_by_name\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Group'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - groups\n      summary: Update group\n      description: Updates an existing group\n      operationId: update_group\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/Group'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Group updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - groups\n      summary: Delete group\n      description: Deletes an existing group\n      operationId: delete_group\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Group deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /roles:\n    get:\n      tags:\n        - roles\n      summary: Get roles\n      description: Returns an array with one or more roles\n      operationId: get_roles\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering groups by name. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Role'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - roles\n      summary: Add role\n      operationId: add_role\n      description: Adds a new role\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/Role'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            Location:\n              schema:\n                type: string\n              description: 'URI of the newly created object'\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Role'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/roles/{name}':\n    parameters:\n      - name: name\n        in: path\n        description: role name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - roles\n      summary: Find roles by name\n      description: Returns the role with the given name if it exists.\n      operationId: get_role_by_name\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Role'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - roles\n      summary: Update role\n      description: Updates an existing role\n      operationId: update_role\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/Role'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Group updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - roles\n      summary: Delete role\n      description: Deletes an existing role\n      operationId: delete_role\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Group deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /eventactions:\n    get:\n      tags:\n        - event manager\n      summary: Get event actions\n      description: Returns an array with one or more event actions\n      operationId: get_event_actons\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering actions by name. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/BaseEventAction'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - event manager\n      summary: Add event action\n      operationId: add_event_action\n      description: Adds a new event actions\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BaseEventAction'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            Location:\n              schema:\n                type: string\n              description: 'URI of the newly created object'\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/BaseEventAction'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/eventactions/{name}':\n    parameters:\n      - name: name\n        in: path\n        description: action name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - event manager\n      summary: Find event actions by name\n      description: Returns the event action with the given name if it exists.\n      operationId: get_event_action_by_name\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/BaseEventAction'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - event manager\n      summary: Update event action\n      description: Updates an existing event action\n      operationId: update_event_action\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BaseEventAction'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Event action updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - event manager\n      summary: Delete event action\n      description: Deletes an existing event action\n      operationId: delete_event_action\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Event action deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /eventrules:\n    get:\n      tags:\n        - event manager\n      summary: Get event rules\n      description: Returns an array with one or more event rules\n      operationId: get_event_rules\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering rules by name. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/EventRule'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - event manager\n      summary: Add event rule\n      operationId: add_event_rule\n      description: Adds a new event rule\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/EventRuleMinimal'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            Location:\n              schema:\n                type: string\n              description: 'URI of the newly created object'\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/EventRule'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/eventrules/{name}':\n    parameters:\n      - name: name\n        in: path\n        description: rule name\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - event manager\n      summary: Find event rules by name\n      description: Returns the event rule with the given name if it exists.\n      operationId: get_event_rile_by_name\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/EventRule'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - event manager\n      summary: Update event rule\n      description: Updates an existing event rule\n      operationId: update_event_rule\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/EventRuleMinimal'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Event rules updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - event manager\n      summary: Delete event rule\n      description: Deletes an existing event rule\n      operationId: delete_event_rule\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Event rules deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/eventrules/run/{name}':\n    parameters:\n      - name: name\n        in: path\n        description: on-demand rule name\n        required: true\n        schema:\n          type: string\n    post:\n      tags:\n        - event manager\n      summary: Run an on-demand event rule\n      description: The rule's actions will run in background. SFTPGo will not monitor any concurrency and such. If you want to be notified at the end of the execution please add an appropriate action\n      operationId: run_event_rule\n      responses:\n        '202':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Event rule started\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /events/fs:\n    get:\n      tags:\n        - events\n      summary: Get filesystem events\n      description: 'Returns an array with one or more filesystem events applying the specified filters. This API is only available if you configure an \"eventsearcher\" plugin'\n      operationId: get_fs_events\n      parameters:\n        - in: query\n          name: start_timestamp\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n            default: 0\n          required: false\n          description: 'the event timestamp, unix timestamp in nanoseconds, must be greater than or equal to the specified one. 0 or missing means omit this filter'\n        - in: query\n          name: end_timestamp\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n            default: 0\n          required: false\n          description: 'the event timestamp, unix timestamp in nanoseconds, must be less than or equal to the specified one. 0 or missing means omit this filter'\n        - in: query\n          name: actions\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/FsEventAction'\n          description: 'the event action must be included among those specified. Empty or missing means omit this filter. Actions must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: username\n          schema:\n            type: string\n          description: 'the event username must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: ip\n          schema:\n            type: string\n          description: 'the event IP must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: ssh_cmd\n          schema:\n            type: string\n          description: 'the event SSH command must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: fs_provider\n          schema:\n            $ref: '#/components/schemas/FsProviders'\n          description: 'the event filesystem provider must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: bucket\n          schema:\n            type: string\n          description: 'the bucket must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: endpoint\n          schema:\n            type: string\n          description: 'the endpoint must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: protocols\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/EventProtocols'\n          description: 'the event protocol must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: statuses\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/FsEventStatus'\n          description: 'the event status must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: instance_ids\n          schema:\n            type: array\n            items:\n              type: string\n          description: 'the event instance id must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: from_id\n          schema:\n            type: string\n          description: 'the event id to start from. This is useful for cursor based pagination. Empty or missing means omit this filter.'\n          required: false\n        - in: query\n          name: role\n          schema:\n            type: string\n          description: 'User role. Empty or missing means omit this filter. Ignored if the admin has a role'\n          required: false\n        - in: query\n          name: csv_export\n          schema:\n            type: boolean\n            default: false\n          required: false\n          description: 'If enabled, events are exported as a CSV file'\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 1000\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 1000, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering events by timestamp. Default DESC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: DESC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/FsEvent'\n            text/csv:\n              schema:\n                type: string\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /events/provider:\n    get:\n      tags:\n        - events\n      summary: Get provider events\n      description: 'Returns an array with one or more provider events applying the specified filters. This API is only available if you configure an \"eventsearcher\" plugin'\n      operationId: get_provider_events\n      parameters:\n        - in: query\n          name: start_timestamp\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n            default: 0\n          required: false\n          description: 'the event timestamp, unix timestamp in nanoseconds, must be greater than or equal to the specified one. 0 or missing means omit this filter'\n        - in: query\n          name: end_timestamp\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n            default: 0\n          required: false\n          description: 'the event timestamp, unix timestamp in nanoseconds, must be less than or equal to the specified one. 0 or missing means omit this filter'\n        - in: query\n          name: actions\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/ProviderEventAction'\n          description: 'the event action must be included among those specified. Empty or missing means omit this filter. Actions must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: username\n          schema:\n            type: string\n          description: 'the event username must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: ip\n          schema:\n            type: string\n          description: 'the event IP must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: object_name\n          schema:\n            type: string\n          description: 'the event object name must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: object_types\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/ProviderEventObjectType'\n          description: 'the event object type must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: instance_ids\n          schema:\n            type: array\n            items:\n              type: string\n          description: 'the event instance id must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: from_id\n          schema:\n            type: string\n          description: 'the event id to start from. This is useful for cursor based pagination. Empty or missing means omit this filter.'\n          required: false\n        - in: query\n          name: role\n          schema:\n            type: string\n          description: 'Admin role. Empty or missing means omit this filter. Ignored if the admin has a role'\n          required: false\n        - in: query\n          name: csv_export\n          schema:\n            type: boolean\n            default: false\n          required: false\n          description: 'If enabled, events are exported as a CSV file'\n        - in: query\n          name: omit_object_data\n          schema:\n            type: boolean\n            default: false\n          required: false\n          description: 'If enabled, returned events will not contain the `object_data` field'\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 1000\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 1000, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering events by timestamp. Default DESC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: DESC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/ProviderEvent'\n            text/csv:\n              schema:\n                type: string\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /events/logs:\n    get:\n      tags:\n        - events\n      summary: Get log events\n      description: 'Returns an array with one or more log events applying the specified filters. This API is only available if you configure an \"eventsearcher\" plugin'\n      operationId: get_log_events\n      parameters:\n        - in: query\n          name: start_timestamp\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n            default: 0\n          required: false\n          description: 'the event timestamp, unix timestamp in nanoseconds, must be greater than or equal to the specified one. 0 or missing means omit this filter'\n        - in: query\n          name: end_timestamp\n          schema:\n            type: integer\n            format: int64\n            minimum: 0\n            default: 0\n          required: false\n          description: 'the event timestamp, unix timestamp in nanoseconds, must be less than or equal to the specified one. 0 or missing means omit this filter'\n        - in: query\n          name: events\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/LogEventType'\n          description: 'the log events must be included among those specified. Empty or missing means omit this filter. Events must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: username\n          schema:\n            type: string\n          description: 'the event username must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: ip\n          schema:\n            type: string\n          description: 'the event IP must be the same as the one specified. Empty or missing means omit this filter'\n          required: false\n        - in: query\n          name: protocols\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/EventProtocols'\n          description: 'the event protocol must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: instance_ids\n          schema:\n            type: array\n            items:\n              type: string\n          description: 'the event instance id must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'\n          explode: false\n          required: false\n        - in: query\n          name: from_id\n          schema:\n            type: string\n          description: 'the event id to start from. This is useful for cursor based pagination. Empty or missing means omit this filter.'\n          required: false\n        - in: query\n          name: role\n          schema:\n            type: string\n          description: 'User role. Empty or missing means omit this filter. Ignored if the admin has a role'\n          required: false\n        - in: query\n          name: csv_export\n          schema:\n            type: boolean\n            default: false\n          required: false\n          description: 'If enabled, events are exported as a CSV file'\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 1000\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 1000, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering events by timestamp. Default DESC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: DESC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/LogEvent'\n            text/csv:\n              schema:\n                type: string\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /apikeys:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - API keys\n      summary: Get API keys\n      description: Returns an array with one or more API keys. For security reasons hashed keys are omitted in the response\n      operationId: get_api_keys\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering API keys by id. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/APIKey'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - API keys\n      summary: Add API key\n      description: Adds a new API key\n      operationId: add_api_key\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/APIKey'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            X-Object-ID:\n              schema:\n                type: string\n              description: ID for the new created API key\n            Location:\n              schema:\n                type: string\n              description: URI to retrieve the details for the new created API key\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  mesage:\n                    type: string\n                    example: 'API key created. This is the only time the API key is visible, please save it.'\n                  key:\n                    type: string\n                    description: 'generated API key'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/apikeys/{id}':\n    parameters:\n      - name: id\n        in: path\n        description: the key id\n        required: true\n        schema:\n          type: string\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - API keys\n      summary: Find API key by id\n      description: Returns the API key with the given id, if it exists. For security reasons the hashed key is omitted in the response\n      operationId: get_api_key_by_id\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/APIKey'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      security:\n        - BearerAuth: []\n      tags:\n        - API keys\n      summary: Update API key\n      description: Updates an existing API key. You cannot update the key itself, the creation date and the last use\n      operationId: update_api_key\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/APIKey'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: API key updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      security:\n        - BearerAuth: []\n      tags:\n        - API keys\n      summary: Delete API key\n      description: Deletes an existing API key\n      operationId: delete_api_key\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Admin deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /admins:\n    get:\n      tags:\n        - admins\n      summary: Get admins\n      description: Returns an array with one or more admins. For security reasons hashed passwords are omitted in the response\n      operationId: get_admins\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering admins by username. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Admin'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - admins\n      summary: Add admin\n      description: 'Adds a new admin. Recovery codes and TOTP configuration cannot be set using this API: each admin must use the specific APIs'\n      operationId: add_admin\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/Admin'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            Location:\n              schema:\n                type: string\n              description: 'URI of the newly created object'\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Admin'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/admins/{username}':\n    parameters:\n      - name: username\n        in: path\n        description: the admin username\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - admins\n      summary: Find admins by username\n      description: 'Returns the admin with the given username, if it exists. For security reasons the hashed password is omitted in the response'\n      operationId: get_admin_by_username\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Admin'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - admins\n      summary: Update admin\n      description: 'Updates an existing admin. Recovery codes and TOTP configuration cannot be set/updated using this API: each admin must use the specific APIs. You are not allowed to update the admin impersonated using an API key'\n      operationId: update_admin\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/Admin'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Admin updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - admins\n      summary: Delete admin\n      description: Deletes an existing admin\n      operationId: delete_admin\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Admin deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/admins/{username}/2fa/disable':\n    parameters:\n      - name: username\n        in: path\n        description: the admin username\n        required: true\n        schema:\n          type: string\n    put:\n      tags:\n        - admins\n      summary: Disable second factor authentication\n      description: 'Disables second factor authentication for the given admin. This API must be used if the admin loses access to their second factor auth device and has no recovery codes'\n      operationId: disable_admin_2fa\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: 2FA disabled\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/admins/{username}/forgot-password':\n    parameters:\n      - name: username\n        in: path\n        description: the admin username\n        required: true\n        schema:\n          type: string\n    post:\n      security: []\n      tags:\n        - admins\n      summary: Send a password reset code by email\n      description: 'You must set up an SMTP server and the account must have a valid email address, in which case SFTPGo will send a code via email to reset the password. If the specified admin does not exist, the request will be silently ignored (a success response will be returned) to avoid disclosing existing admins'\n      operationId: admin_forgot_password\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/admins/{username}/reset-password':\n    parameters:\n      - name: username\n        in: path\n        description: the admin username\n        required: true\n        schema:\n          type: string\n    post:\n      security: []\n      tags:\n        - admins\n      summary: Reset the password\n      description: 'Set a new password using the code received via email'\n      operationId: admin_reset_password\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                code:\n                  type: string\n                password:\n                  type: string\n        required: true\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /users:\n    get:\n      tags:\n        - users\n      summary: Get users\n      description: Returns an array with one or more users. For security reasons hashed passwords are omitted in the response\n      operationId: get_users\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering users by username. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/User'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - users\n      summary: Add user\n      description: 'Adds a new user.Recovery codes and TOTP configuration cannot be set using this API: each user must use the specific APIs'\n      operationId: add_user\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the hash of the password and the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/User'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            Location:\n              schema:\n                type: string\n              description: 'URI of the newly created object'\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/User'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/users/{username}':\n    parameters:\n      - name: username\n        in: path\n        description: the username\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - users\n      summary: Find users by username\n      description: Returns the user with the given username if it exists. For security reasons the hashed password is omitted in the response\n      operationId: get_user_by_username\n      parameters:\n        - in: query\n          name: confidential_data\n          schema:\n            type: integer\n          description: 'If set to 1 confidential data will not be hidden. This means that the response will contain the hash of the password and the key and additional data for secrets. If a master key is not set or an external KMS is used, the data returned are enough to get the secrets in cleartext. Ignored if the * permission is not granted.'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/User'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - users\n      summary: Update user\n      description: 'Updates an existing user and optionally disconnects it, if connected, to apply the new settings. The current password will be preserved if the password field is omitted in the request body. Recovery codes and TOTP configuration cannot be set/updated using this API: each user must use the specific APIs'\n      operationId: update_user\n      parameters:\n        - in: query\n          name: disconnect\n          schema:\n            type: integer\n            enum:\n              - 0\n              - 1\n          description: |\n            Disconnect:\n              * `0` The user will not be disconnected and it will continue to use the old configuration until connected. This is the default\n              * `1` The user will be disconnected after a successful update. It must login again and so it will be forced to use the new configuration\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/User'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: User updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - users\n      summary: Delete user\n      description: Deletes an existing user\n      operationId: delete_user\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: User deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/users/{username}/2fa/disable':\n    parameters:\n      - name: username\n        in: path\n        description: the username\n        required: true\n        schema:\n          type: string\n    put:\n      tags:\n        - users\n      summary: Disable second factor authentication\n      description: 'Disables second factor authentication for the given user. This API must be used if the user loses access to their second factor auth device and has no recovery codes'\n      operationId: disable_user_2fa\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: 2FA disabled\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/users/{username}/forgot-password':\n    parameters:\n      - name: username\n        in: path\n        description: the username\n        required: true\n        schema:\n          type: string\n    post:\n      security: []\n      tags:\n        - users\n      summary: Send a password reset code by email\n      description: 'You must configure an SMTP server, the account must have a valid email address and must not have the \"reset-password-disabled\" restriction, in which case SFTPGo will send a code via email to reset the password. If the specified user does not exist, the request will be silently ignored (a success response will be returned) to avoid disclosing existing users'\n      operationId: user_forgot_password\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/users/{username}/reset-password':\n    parameters:\n      - name: username\n        in: path\n        description: the username\n        required: true\n        schema:\n          type: string\n    post:\n      security: []\n      tags:\n        - users\n      summary: Reset the password\n      description: 'Set a new password using the code received via email'\n      operationId: user_reset_password\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                code:\n                  type: string\n                password:\n                  type: string\n        required: true\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /status:\n    get:\n      tags:\n        - maintenance\n      summary: Get status\n      description: Retrieves the status of the active services\n      operationId: get_status\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ServicesStatus'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /dumpdata:\n    get:\n      tags:\n        - maintenance\n      summary: Dump data\n      description: 'Backups data as data provider independent JSON. The backup can be saved in a local file on the server, to avoid exposing sensitive data over the network, or returned as response body. The output of dumpdata can be used as input for loaddata'\n      operationId: dumpdata\n      parameters:\n        - in: query\n          name: output-file\n          schema:\n            type: string\n          description: Path for the file to write the JSON serialized data to. This path is relative to the configured \"backups_path\". If this file already exists it will be overwritten. To return the backup as response body set `output_data` to true instead.\n        - in: query\n          name: output-data\n          schema:\n            type: integer\n            enum:\n              - 0\n              - 1\n          description: |\n            output data:\n              * `0` or any other value != 1, the backup will be saved to a file on the server, `output_file` is required\n              * `1` the backup will be returned as response body\n        - in: query\n          name: indent\n          schema:\n            type: integer\n            enum:\n              - 0\n              - 1\n          description: |\n            indent:\n              * `0` no indentation. This is the default\n              * `1` format the output JSON\n        - in: query\n          name: scopes\n          schema:\n            type: array\n            items:\n              $ref: '#/components/schemas/DumpDataScopes'\n          description: 'You can limit the dump contents to the specified scopes. Empty or missing means any supported scope. Scopes must be specified comma separated'\n          explode: false\n          required: false\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                oneOf:\n                  - $ref: '#/components/schemas/ApiResponse'\n                  - $ref: '#/components/schemas/BackupData'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /loaddata:\n    parameters:\n      - in: query\n        name: scan-quota\n        schema:\n          type: integer\n          enum:\n            - 0\n            - 1\n            - 2\n        description: |\n          Quota scan:\n            * `0` no quota scan is done, the imported users/folders will have used_quota_size and used_quota_files = 0 or the existing values if they already exists. This is the default\n            * `1` scan quota\n            * `2` scan quota if the user has quota restrictions\n          required: false\n      - in: query\n        name: mode\n        schema:\n          type: integer\n          enum:\n            - 0\n            - 1\n            - 2\n        description: |\n          Mode:\n            * `0` New objects are added, existing ones are updated. This is the default\n            * `1` New objects are added, existing ones are not modified\n            * `2` New objects are added, existing ones are updated and connected users are disconnected and so forced to use the new configuration\n    get:\n      tags:\n        - maintenance\n      summary: Load data from path\n      description: 'Restores SFTPGo data from a JSON backup file on the server. Objects will be restored one by one and the restore is stopped if a object cannot be added or updated, so it could happen a partial restore'\n      operationId: loaddata_from_file\n      parameters:\n        - in: query\n          name: input-file\n          schema:\n            type: string\n          required: true\n          description: Path for the file to read the JSON serialized data from. This can be an absolute path or a path relative to the configured \"backups_path\". The max allowed file size is 10MB\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Data restored\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - maintenance\n      summary: Load data\n      description: 'Restores SFTPGo data from a JSON backup. Objects will be restored one by one and the restore is stopped if a object cannot be added or updated, so it could happen a partial restore'\n      operationId: loaddata_from_request_body\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/BackupData'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Data restored\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/changepwd:\n    put:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Change user password\n      description: Changes the password for the logged in user\n      operationId: change_user_password\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PwdChange'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/profile:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Get user profile\n      description: 'Returns the profile for the logged in user'\n      operationId: get_user_profile\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/UserProfile'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Update user profile\n      description: 'Allows to update the profile for the logged in user'\n      operationId: update_user_profile\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/UserProfile'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/2fa/recoverycodes:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Get recovery codes\n      description: 'Returns the recovery codes for the logged in user. Recovery codes can be used if the user loses access to their second factor auth device. Recovery codes are returned unencrypted'\n      operationId: get_user_recovery_codes\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/RecoveryCode'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Generate recovery codes\n      description: 'Generates new recovery codes for the logged in user. Generating new recovery codes you automatically invalidate old ones'\n      operationId: generate_user_recovery_codes\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: string\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/totp/configs:\n    get:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Get available TOTP configuration\n      description: Returns the available TOTP configurations for the logged in user\n      operationId: get_user_totp_configs\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/TOTPConfig'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/totp/generate:\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Generate a new TOTP secret\n      description: 'Generates a new TOTP secret, including the QR code as png, using the specified configuration for the logged in user'\n      operationId: generate_user_totp_secret\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                config_name:\n                  type: string\n                  description: 'name of the configuration to use to generate the secret'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  config_name:\n                    type: string\n                  issuer:\n                    type: string\n                  secret:\n                    type: string\n                  qr_code:\n                    type: string\n                    format: byte\n                    description: 'QR code png encoded as BASE64'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/totp/validate:\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Validate a one time authentication code\n      description: 'Checks if the given authentication code can be validated using the specified secret and config name'\n      operationId: validate_user_totp_secret\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                config_name:\n                  type: string\n                  description: 'name of the configuration to use to validate the passcode'\n                passcode:\n                  type: string\n                  description: 'passcode to validate'\n                secret:\n                  type: string\n                  description: 'secret to use to validate the passcode'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Passcode successfully validated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/totp/save:\n    post:\n      security:\n        - BearerAuth: []\n      tags:\n        - user APIs\n      summary: Save a TOTP config\n      description: 'Saves the specified TOTP config for the logged in user'\n      operationId: save_user_totp_config\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/UserTOTPConfig'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: TOTP configuration saved\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/shares:\n    get:\n      tags:\n        - user APIs\n      summary: List user shares\n      description: Returns the share for the logged in user\n      operationId: get_user_shares\n      parameters:\n        - in: query\n          name: offset\n          schema:\n            type: integer\n            minimum: 0\n            default: 0\n          required: false\n        - in: query\n          name: limit\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 500\n            default: 100\n          required: false\n          description: 'The maximum number of items to return. Max value is 500, default is 100'\n        - in: query\n          name: order\n          required: false\n          description: Ordering shares by ID. Default ASC\n          schema:\n            type: string\n            enum:\n              - ASC\n              - DESC\n            example: ASC\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Share'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - user APIs\n      summary: Add a share\n      operationId: add_share\n      description: 'Adds a new share. The share id will be auto-generated'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/Share'\n      responses:\n        '201':\n          description: successful operation\n          headers:\n            X-Object-ID:\n              schema:\n                type: string\n              description: ID for the new created share\n            Location:\n              schema:\n                type: string\n              description: URI to retrieve the details for the new created share\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  '/user/shares/{id}':\n    parameters:\n      - name: id\n        in: path\n        description: the share id\n        required: true\n        schema:\n          type: string\n    get:\n      tags:\n        - user APIs\n      summary: Get share by id\n      description: Returns a share by id for the logged in user\n      operationId: get_user_share_by_id\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Share'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    put:\n      tags:\n        - user APIs\n      summary: Update share\n      description: 'Updates an existing share belonging to the logged in user'\n      operationId: update_user_share\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/Share'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Share updated\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - user APIs\n      summary: Delete share\n      description: 'Deletes an existing share belonging to the logged in user'\n      operationId: delete_user_share\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n              example:\n                message: Share deleted\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '404':\n          $ref: '#/components/responses/NotFound'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/file-actions/copy:\n    parameters:\n      - in: query\n        name: path\n        description: Path to the file/folder to copy. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\"\n        schema:\n          type: string\n        required: true\n      - in: query\n        name: target\n        description: New name. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\"\n        schema:\n          type: string\n        required: true\n    post:\n      tags:\n        - user APIs\n      summary: 'Copy a file or a directory'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/file-actions/move:\n    parameters:\n      - in: query\n        name: path\n        description: Path to the file/folder to rename. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\"\n        schema:\n          type: string\n        required: true\n      - in: query\n        name: target\n        description: New name. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\"\n        schema:\n          type: string\n        required: true\n    post:\n      tags:\n        - user APIs\n      summary: 'Move (rename) a file or a directory'\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/dirs:\n    get:\n      tags:\n        - user APIs\n      summary: Read directory contents\n      description: Returns the contents of the specified directory for the logged in user\n      operationId: get_user_dir_contents\n      parameters:\n        - in: query\n          name: path\n          description: Path to the folder to read. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\". If empty or missing the user's start directory is assumed. If relative, the user's start directory is used as the base\n          schema:\n            type: string\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/DirEntry'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - user APIs\n      summary: Create a directory\n      description: Create a directory for the logged in user\n      operationId: create_user_dir\n      parameters:\n        - in: query\n          name: path\n          description: Path to the folder to create. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\"\n          schema:\n            type: string\n          required: true\n        - in: query\n          name: mkdir_parents\n          description: Create parent directories if they do not exist?\n          schema:\n            type: boolean\n          required: false\n      responses:\n        '201':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    patch:\n      tags:\n        - user APIs\n      deprecated: true\n      summary: 'Rename a directory. Deprecated, use \"file-actions/move\"'\n      description: Rename a directory for the logged in user. The rename is allowed for empty directory or for non empty local directories, with no virtual folders inside\n      operationId: rename_user_dir\n      parameters:\n        - in: query\n          name: path\n          description: Path to the folder to rename. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\"\n          schema:\n            type: string\n          required: true\n        - in: query\n          name: target\n          description: New name. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\"\n          schema:\n            type: string\n          required: true\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - user APIs\n      summary: Delete a directory\n      description: Delete a directory and any children it contains for the logged in user\n      operationId: delete_user_dir\n      parameters:\n        - in: query\n          name: path\n          description: Path to the folder to delete. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\"\n          schema:\n            type: string\n          required: true\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/files:\n    get:\n      tags:\n        - user APIs\n      summary: Download a single file\n      description: Returns the file contents as response body\n      operationId: download_user_file\n      parameters:\n        - in: query\n          name: path\n          required: true\n          description: Path to the file to download. It must be URL encoded, for example the path \"my dir/àdir/file.txt\" must be sent as \"my%20dir%2F%C3%A0dir%2Ffile.txt\"\n          schema:\n            type: string\n        - in: query\n          name: inline\n          required: false\n          description: 'If set, the response will not have the Content-Disposition header set to `attachment`'\n          schema:\n            type: string\n      responses:\n        '200':\n          description: successful operation\n          content:\n            '*/*':\n              schema:\n                type: string\n                format: binary\n        '206':\n          description: successful operation\n          content:\n            '*/*':\n              schema:\n                type: string\n                format: binary\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    post:\n      tags:\n        - user APIs\n      summary: Upload files\n      description: Upload one or more files for the logged in user\n      operationId: create_user_files\n      parameters:\n        - in: query\n          name: path\n          description: Parent directory for the uploaded files. It must be URL encoded, for example the path \"my dir/àdir\" must be sent as \"my%20dir%2F%C3%A0dir\". If empty or missing the root path is assumed. If a file with the same name already exists, it will be overwritten\n          schema:\n            type: string\n        - in: query\n          name: mkdir_parents\n          description: Create parent directories if they do not exist?\n          schema:\n            type: boolean\n          required: false\n      requestBody:\n        content:\n          multipart/form-data:\n            schema:\n              type: object\n              properties:\n                filenames:\n                  type: array\n                  items:\n                    type: string\n                    format: binary\n                  minItems: 1\n                  uniqueItems: true\n        required: true\n      responses:\n        '201':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '413':\n          $ref: '#/components/responses/RequestEntityTooLarge'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    patch:\n      tags:\n        - user APIs\n      deprecated: true\n      summary: Rename a file\n      description: 'Rename a file for the logged in user. Deprecated, use \"file-actions/move\"'\n      operationId: rename_user_file\n      parameters:\n        - in: query\n          name: path\n          description: Path to the file to rename. It must be URL encoded\n          schema:\n            type: string\n          required: true\n        - in: query\n          name: target\n          description: New name. It must be URL encoded\n          schema:\n            type: string\n          required: true\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n    delete:\n      tags:\n        - user APIs\n      summary: Delete a file\n      description: Delete a file for the logged in user.\n      operationId: delete_user_file\n      parameters:\n        - in: query\n          name: path\n          description: Path to the file to delete. It must be URL encoded\n          schema:\n            type: string\n          required: true\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/files/upload:\n    post:\n      tags:\n        - user APIs\n      summary: Upload a single file\n      description: 'Upload a single file for the logged in user to an existing directory. This API does not use multipart/form-data and so no temporary files are created server side but only a single file can be uploaded as POST body'\n      operationId: create_user_file\n      parameters:\n        - in: query\n          name: path\n          description: Full file path. It must be path encoded, for example the path \"my dir/àdir/file.txt\" must be sent as \"my%20dir%2F%C3%A0dir%2Ffile.txt\". The parent directory must exist. If a file with the same name already exists, it will be overwritten\n          schema:\n            type: string\n          required: true\n        - in: query\n          name: mkdir_parents\n          description: Create parent directories if they do not exist?\n          schema:\n            type: boolean\n          required: false\n        - in: header\n          name: X-SFTPGO-MTIME\n          schema:\n            type: integer\n          description: File modification time as unix timestamp in milliseconds\n      requestBody:\n        content:\n          application/*:\n            schema:\n              type: string\n              format: binary\n          text/*:\n            schema:\n              type: string\n              format: binary\n          image/*:\n            schema:\n              type: string\n              format: binary\n          audio/*:\n            schema:\n              type: string\n              format: binary\n          video/*:\n            schema:\n              type: string\n              format: binary\n        required: true\n      responses:\n        '201':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '413':\n          $ref: '#/components/responses/RequestEntityTooLarge'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/files/metadata:\n    patch:\n      tags:\n        - user APIs\n      summary: Set metadata for a file/directory\n      description: 'Set supported metadata attributes for the specified file or directory'\n      operationId: setprops_user_file\n      parameters:\n        - in: query\n          name: path\n          description: Full file/directory path. It must be URL encoded, for example the path \"my dir/àdir/file.txt\" must be sent as \"my%20dir%2F%C3%A0dir%2Ffile.txt\"\n          schema:\n            type: string\n          required: true\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                modification_time:\n                  type: integer\n                  description: File modification time as unix timestamp in milliseconds\n        required: true\n      responses:\n        '200':\n          description: successful operation\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/ApiResponse'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '413':\n          $ref: '#/components/responses/RequestEntityTooLarge'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\n  /user/streamzip:\n    post:\n      tags:\n        - user APIs\n      summary: Download multiple files and folders as a single zip file\n      description: A zip file, containing the specified files and folders, will be generated on the fly and returned as response body. Only folders and regular files will be included in the zip\n      operationId: streamzip\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: array\n              items:\n                type: string\n                description: Absolute file or folder path\n      responses:\n        '200':\n          description: successful operation\n          content:\n            'application/zip':\n              schema:\n                type: string\n                format: binary\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n        '403':\n          $ref: '#/components/responses/Forbidden'\n        '500':\n          $ref: '#/components/responses/InternalServerError'\n        default:\n          $ref: '#/components/responses/DefaultResponse'\ncomponents:\n  responses:\n    BadRequest:\n      description: Bad Request\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    Unauthorized:\n      description: Unauthorized\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    Forbidden:\n      description: Forbidden\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    NotFound:\n      description: Not Found\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    Conflict:\n      description: Conflict\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    RequestEntityTooLarge:\n      description: Request Entity Too Large, max allowed size exceeded\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    InternalServerError:\n      description: Internal Server Error\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n    DefaultResponse:\n      description: Unexpected Error\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ApiResponse'\n  schemas:\n    Permission:\n      type: string\n      enum:\n        - '*'\n        - list\n        - download\n        - upload\n        - overwrite\n        - delete\n        - delete_files\n        - delete_dirs\n        - rename\n        - rename_files\n        - rename_dirs\n        - create_dirs\n        - create_symlinks\n        - chmod\n        - chown\n        - chtimes\n        - copy\n      description: |\n        Permissions:\n          * `*` - all permissions are granted\n          * `list` - list items is allowed\n          * `download` - download files is allowed\n          * `upload` - upload files is allowed\n          * `overwrite` - overwrite an existing file, while uploading, is allowed. upload permission is required to allow file overwrite\n          * `delete` - delete files or directories is allowed\n          * `delete_files` - delete files is allowed\n          * `delete_dirs` - delete directories is allowed\n          * `rename` - rename files or directories is allowed\n          * `rename_files` - rename files is allowed\n          * `rename_dirs` - rename directories is allowed\n          * `create_dirs` - create directories is allowed\n          * `create_symlinks` - create links is allowed\n          * `chmod` changing file or directory permissions is allowed\n          * `chown` changing file or directory owner and group is allowed\n          * `chtimes` changing file or directory access and modification time is allowed\n          * `copy`, copying files or directories is allowed\n    AdminPermissions:\n      type: string\n      enum:\n        - '*'\n        - add_users\n        - edit_users\n        - del_users\n        - view_users\n        - view_conns\n        - close_conns\n        - view_status\n        - manage_folders\n        - manage_groups\n        - quota_scans\n        - manage_defender\n        - view_defender\n        - view_events\n        - disable_mfa\n      description: |\n        Admin permissions:\n          * `*` - super admin permissions are granted\n          * `add_users` - add new users is allowed\n          * `edit_users` - change existing users is allowed\n          * `del_users` - remove users is allowed\n          * `view_users` - list users is allowed\n          * `view_conns` - list active connections is allowed\n          * `close_conns` - close active connections is allowed\n          * `view_status` - view the server status is allowed\n          * `manage_folders` - manage folders is allowed\n          * `manage_groups` - manage groups is allowed\n          * `quota_scans` - view and start quota scans is allowed\n          * `manage_defender` - remove ip from the dynamic blocklist is allowed\n          * `view_defender` - list the dynamic blocklist is allowed\n          * `view_events` - view and search filesystem and provider events is allowed\n          * `disable_mfa` - allow to disable two-factor authentication for users and admins\n    FsProviders:\n      type: integer\n      enum:\n        - 0\n        - 1\n        - 2\n        - 3\n        - 4\n        - 5\n        - 6\n      description: |\n        Filesystem providers:\n          * `0` - Local filesystem\n          * `1` - S3 Compatible Object Storage\n          * `2` - Google Cloud Storage\n          * `3` - Azure Blob Storage\n          * `4` - Local filesystem encrypted\n          * `5` - SFTP\n          * `6` - HTTP filesystem\n    EventActionTypes:\n      type: integer\n      enum:\n        - 1\n        - 2\n        - 3\n        - 4\n        - 5\n        - 6\n        - 7\n        - 8\n        - 9\n        - 11\n        - 12\n        - 13\n        - 14\n        - 15\n      description: |\n        Supported event action types:\n          * `1` - HTTP\n          * `2` - Command\n          * `3` - Email\n          * `4` - Backup\n          * `5` - User quota reset\n          * `6` - Folder quota reset\n          * `7` - Transfer quota reset\n          * `8` - Data retention check\n          * `9` - Filesystem\n          * `11` - Password expiration check\n          * `12` - User expiration check\n          * `13` - Identity Provider account check\n          * `14` - User inactivity check\n          * `15` - Rotate log file\n    FilesystemActionTypes:\n      type: integer\n      enum:\n        - 1\n        - 2\n        - 3\n        - 4\n        - 5\n        - 6\n      description: |\n        Supported filesystem action types:\n          * `1` - Rename\n          * `2` - Delete\n          * `3` - Mkdis\n          * `4` - Exist\n          * `5` - Compress\n          * `6` - Copy\n    EventTriggerTypes:\n      type: integer\n      enum:\n        - 1\n        - 2\n        - 3\n        - 4\n        - 5\n        - 6\n        - 7\n      description: |\n        Supported event trigger types:\n          * `1` - Filesystem event\n          * `2` - Provider event\n          * `3` - Schedule\n          * `4` - IP blocked\n          * `5` - Certificate renewal\n          * `6` - On demand, like schedule but executed on demand\n          * `7` - Identity provider login\n    LoginMethods:\n      type: string\n      enum:\n        - publickey\n        - password\n        - password-over-SSH\n        - keyboard-interactive\n        - publickey+password\n        - publickey+keyboard-interactive\n        - TLSCertificate\n        - TLSCertificate+password\n      description: |\n        Available login methods. To enable multi-step authentication you have to allow only multi-step login methods\n          * `publickey`\n          * `password`, password for all the supported protocols\n          * `password-over-SSH`, password over SSH protocol (SSH/SFTP/SCP)\n          * `keyboard-interactive`\n          * `publickey+password` - multi-step auth: public key and password\n          * `publickey+keyboard-interactive` - multi-step auth: public key and keyboard interactive\n          * `TLSCertificate`\n          * `TLSCertificate+password` - multi-step auth: TLS client certificate and password\n    SupportedProtocols:\n      type: string\n      enum:\n        - SSH\n        - FTP\n        - DAV\n        - HTTP\n      description: |\n        Protocols:\n          * `SSH` - includes both SFTP and SSH commands\n          * `FTP` - plain FTP and FTPES/FTPS\n          * `DAV` - WebDAV over HTTP/HTTPS\n          * `HTTP` - WebClient/REST API\n    MFAProtocols:\n      type: string\n      enum:\n        - SSH\n        - FTP\n        - HTTP\n      description: |\n        Protocols:\n          * `SSH` - includes both SFTP and SSH commands\n          * `FTP` - plain FTP and FTPES/FTPS\n          * `HTTP` - WebClient/REST API\n    EventProtocols:\n      type: string\n      enum:\n        - SSH\n        - SFTP\n        - SCP\n        - FTP\n        - DAV\n        - HTTP\n        - HTTPShare\n        - DataRetention\n        - EventAction\n        - OIDC\n      description: |\n        Protocols:\n          * `SSH` - SSH commands\n          * `SFTP` - SFTP protocol\n          * `SCP` - SCP protocol\n          * `FTP` - plain FTP and FTPES/FTPS\n          * `DAV` - WebDAV\n          * `HTTP` - WebClient/REST API\n          * `HTTPShare` - the event is generated in a public share\n          * `DataRetention` - the event is generated by a data retention check\n          * `EventAction` - the event is generated by an EventManager action\n          * `OIDC` - OpenID Connect\n    WebClientOptions:\n      type: string\n      enum:\n        - publickey-change-disabled\n        - tls-cert-change-disabled\n        - write-disabled\n        - mfa-disabled\n        - password-change-disabled\n        - api-key-auth-change-disabled\n        - info-change-disabled\n        - shares-disabled\n        - password-reset-disabled\n        - shares-without-password-disabled\n      description: |\n        Options:\n          * `publickey-change-disabled` - changing SSH public keys is not allowed\n          * `tls-cert-change-disabled` - changing TLS certificates is not allowed\n          * `write-disabled` - upload, rename, delete are not allowed even if the user has permissions for these actions\n          * `mfa-disabled` - enabling multi-factor authentication is not allowed. This option cannot be set if the user has MFA already enabled\n          * `password-change-disabled` - changing password is not allowed\n          * `api-key-auth-change-disabled` - enabling/disabling API key authentication is not allowed\n          * `info-change-disabled` - changing info such as email and description is not allowed\n          * `shares-disabled` - sharing files and directories with external users is not allowed\n          * `password-reset-disabled` - resetting the password is not allowed\n          * `shares-without-password-disabled` - creating shares without password protection is not allowed\n    APIKeyScope:\n      type: integer\n      enum:\n        - 1\n        - 2\n      description: |\n        Options:\n          * `1` - admin scope. The API key will be used to impersonate an SFTPGo admin\n          * `2` - user scope. The API key will be used to impersonate an SFTPGo user\n    ShareScope:\n      type: integer\n      enum:\n        - 1\n        - 2\n      description: |\n        Options:\n          * `1` - read scope\n          * `2` - write scope\n    TOTPHMacAlgo:\n      type: string\n      enum:\n        - sha1\n        - sha256\n        - sha512\n      description: 'Supported HMAC algorithms for Time-based one time passwords'\n    UserType:\n      type: string\n      enum:\n        - ''\n        - LDAPUser\n        - OSUser\n      description: This is an hint for authentication plugins. It is ignored when using SFTPGo internal authentication\n    DumpDataScopes:\n      type: string\n      enum:\n        - users\n        - folders\n        - groups\n        - admins\n        - api_keys\n        - shares\n        - actions\n        - rules\n        - roles\n        - ip_lists\n        - configs\n    LogEventType:\n      type: integer\n      enum:\n        - 1\n        - 2\n        - 3\n        - 4\n        - 5\n      description: >\n        Event status:\n          * `1` - Login failed\n          * `2` - Login failed non-existent user\n          * `3` - No login tried\n          * `4` - Algorithm negotiation failed\n          * `5` - Login succeeded\n    FsEventStatus:\n      type: integer\n      enum:\n        - 1\n        - 2\n        - 3\n      description: >\n        Event status:\n          * `1` - no error\n          * `2` - generic error\n          * `3` - quota exceeded error\n    FsEventAction:\n      type: string\n      enum:\n        - download\n        - upload\n        - first-upload\n        - first-download\n        - delete\n        - rename\n        - mkdir\n        - rmdir\n        - ssh_cmd\n    ProviderEventAction:\n      type: string\n      enum:\n        - add\n        - update\n        - delete\n    ProviderEventObjectType:\n      type: string\n      enum:\n        - user\n        - folder\n        - group\n        - admin\n        - api_key\n        - share\n        - event_action\n        - event_rule\n        - role\n    SSHAuthentications:\n      type: string\n      enum:\n        - publickey\n        - password\n        - keyboard-interactive\n        - publickey+password\n        - publickey+keyboard-interactive\n    TLSVersions:\n      type: integer\n      enum:\n        - 12\n        - 13\n      description: >\n        TLS version:\n          * `12` - TLS 1.2\n          * `13` - TLS 1.3\n    IPListType:\n      type: integer\n      enum:\n        - 1\n        - 2\n        - 3\n      description: >\n        IP List types:\n          * `1` - allow list\n          * `2` - defender\n          * `3` - rate limiter safe list\n    IPListMode:\n      type: integer\n      enum:\n        - 1\n        - 2\n      description: >\n        IP list modes\n          * `1` - allow\n          * `2` - deny, supported for defender list type only\n    TOTPConfig:\n      type: object\n      properties:\n        name:\n          type: string\n        issuer:\n          type: string\n        algo:\n          $ref: '#/components/schemas/TOTPHMacAlgo'\n    RecoveryCode:\n      type: object\n      properties:\n        secret:\n          $ref: '#/components/schemas/Secret'\n        used:\n          type: boolean\n      description: 'Recovery codes to use if the user loses access to their second factor auth device. Each code can only be used once, you should use these codes to login and disable or reset 2FA for your account'\n    BaseTOTPConfig:\n      type: object\n      properties:\n        enabled:\n          type: boolean\n        config_name:\n          type: string\n          description: 'This name must be defined within the \"totp\" section of the SFTPGo configuration file. You will be unable to save a user/admin referencing a missing config_name'\n        secret:\n          $ref: '#/components/schemas/Secret'\n    AdminTOTPConfig:\n      allOf:\n        - $ref: '#/components/schemas/BaseTOTPConfig'\n    UserTOTPConfig:\n      allOf:\n        - $ref: '#/components/schemas/BaseTOTPConfig'\n        - type: object\n          properties:\n            protocols:\n              type: array\n              items:\n                $ref: '#/components/schemas/MFAProtocols'\n              description: 'TOTP will be required for the specified protocols. SSH protocol (SFTP/SCP/SSH commands) will ask for the TOTP passcode if the client uses keyboard interactive authentication. FTP has no standard way to support two factor authentication, if you enable the FTP support, you have to add the TOTP passcode after the password. For example if your password is \"password\" and your one time passcode is \"123456\" you have to use \"password123456\" as password. WebDAV is not supported since each single request must be authenticated and a passcode cannot be reused.'\n    PatternsFilter:\n      type: object\n      properties:\n        path:\n          type: string\n          description: 'virtual path as seen by users, if no other specific filter is defined, the filter applies for sub directories too. For example if filters are defined for the paths \"/\" and \"/sub\" then the filters for \"/\" are applied for any file outside the \"/sub\" directory'\n        allowed_patterns:\n          type: array\n          items:\n            type: string\n          description: 'list of, case insensitive, allowed shell like patterns. Allowed patterns are evaluated before the denied ones'\n          example:\n            - '*.jpg'\n            - a*b?.png\n        denied_patterns:\n          type: array\n          items:\n            type: string\n          description: 'list of, case insensitive, denied shell like patterns'\n          example:\n            - '*.zip'\n        deny_policy:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            Policies for denied patterns\n              * `0` - default policy. Denied files/directories matching the filters are visible in directory listing but cannot be uploaded/downloaded/overwritten/renamed\n              * `1` - deny policy hide. This policy applies the same restrictions as the default one and denied files/directories matching the filters will also be hidden in directory listing. This mode may cause performance issues for large directories\n    HooksFilter:\n      type: object\n      properties:\n        external_auth_disabled:\n          type: boolean\n          example: false\n          description: If true, the external auth hook, if defined, will not be executed\n        pre_login_disabled:\n          type: boolean\n          example: false\n          description: If true, the pre-login hook, if defined, will not be executed\n        check_password_disabled:\n          type: boolean\n          example: false\n          description: If true, the check password hook, if defined, will not be executed\n      description: User specific hook overrides\n    BandwidthLimit:\n      type: object\n      properties:\n        sources:\n          type: array\n          items:\n            type: string\n          description: 'Source networks in CIDR notation as defined in RFC 4632 and RFC 4291 for example `192.0.2.0/24` or `2001:db8::/32`. The limit applies if the defined networks contain the client IP'\n        upload_bandwidth:\n          type: integer\n          format: int32\n          description: 'Maximum upload bandwidth as KB/s, 0 means unlimited'\n        download_bandwidth:\n          type: integer\n          format: int32\n          description: 'Maximum download bandwidth as KB/s, 0 means unlimited'\n    TimePeriod:\n      type: object\n      properties:\n        day_of_week:\n          type: integer\n          enum:\n            - 0\n            - 1\n            - 2\n            - 3\n            - 4\n            - 5\n            - 6\n          description: Day of week, 0 Sunday, 6 Saturday\n        from:\n          type: string\n          description: Start time in HH:MM format\n        to:\n          type: string\n          description: End time in HH:MM format\n    BaseUserFilters:\n      type: object\n      properties:\n        allowed_ip:\n          type: array\n          items:\n            type: string\n          description: 'only clients connecting from these IP/Mask are allowed. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example \"192.0.2.0/24\" or \"2001:db8::/32\"'\n          example:\n            - 192.0.2.0/24\n            - '2001:db8::/32'\n        denied_ip:\n          type: array\n          items:\n            type: string\n          description: clients connecting from these IP/Mask are not allowed. Denied rules are evaluated before allowed ones\n          example:\n            - 172.16.0.0/16\n        denied_login_methods:\n          type: array\n          items:\n            $ref: '#/components/schemas/LoginMethods'\n          description: if null or empty any available login method is allowed\n        denied_protocols:\n          type: array\n          items:\n            $ref: '#/components/schemas/SupportedProtocols'\n          description: if null or empty any available protocol is allowed\n        file_patterns:\n          type: array\n          items:\n            $ref: '#/components/schemas/PatternsFilter'\n          description: 'filters based on shell like file patterns. These restrictions do not apply to files listing for performance reasons, so a denied file cannot be downloaded/overwritten/renamed but it will still be in the list of files. Please note that these restrictions can be easily bypassed'\n        max_upload_file_size:\n          type: integer\n          format: int64\n          description: 'maximum allowed size, as bytes, for a single file upload. The upload will be aborted if/when the size of the file being sent exceeds this limit. 0 means unlimited'\n        tls_username:\n          type: string\n          description: 'defines the TLS certificate field to use as username. For FTP clients it must match the name provided using the \"USER\" command. For WebDAV, if no username is provided, the CN will be used as username. For WebDAV clients it must match the implicit or provided username. Ignored if mutual TLS is disabled. Currently the only supported value is `CommonName`'\n        hooks:\n          $ref: '#/components/schemas/HooksFilter'\n        disable_fs_checks:\n          type: boolean\n          example: false\n          description: Disable checks for existence and automatic creation of home directory and virtual folders. SFTPGo requires that the user's home directory, virtual folder root, and intermediate paths to virtual folders exist to work properly. If you already know that the required directories exist, disabling these checks will speed up login. You could, for example, disable these checks after the first login\n        web_client:\n          type: array\n          items:\n            $ref: '#/components/schemas/WebClientOptions'\n          description: WebClient/user REST API related configuration options\n        allow_api_key_auth:\n          type: boolean\n          description: 'API key authentication allows to impersonate this user with an API key'\n        user_type:\n          $ref: '#/components/schemas/UserType'\n        bandwidth_limits:\n          type: array\n          items:\n            $ref: '#/components/schemas/BandwidthLimit'\n        external_auth_cache_time:\n          type: integer\n          description: 'Defines the cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache'\n        start_directory:\n          type: string\n          description: 'Specifies an alternate starting directory. If not set, the default is \"/\". This option is supported for SFTP/SCP, FTP and HTTP (WebClient/REST API) protocols. Relative paths will use this directory as base.'\n        two_factor_protocols:\n          type: array\n          items:\n            $ref: '#/components/schemas/MFAProtocols'\n          description: 'Defines protocols that require two factor authentication'\n        ftp_security:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: 'Set to `1` to require TLS for both data and control connection. his setting is useful if you want to allow both encrypted and plain text FTP sessions globally and then you want to require encrypted sessions on a per-user basis. It has no effect if TLS is already required for all users in the configuration file.'\n        is_anonymous:\n          type: boolean\n          description: 'If enabled the user can login with any password or no password at all. Anonymous users are supported for FTP and WebDAV protocols and permissions will be automatically set to \"list\" and \"download\" (read only)'\n        default_shares_expiration:\n          type: integer\n          description: 'Defines the default expiration for newly created shares as number of days. 0 means no expiration'\n        max_shares_expiration:\n          type: integer\n          description: 'Defines the maximum allowed expiration, as a number of days, when a user creates or updates a share. 0 means no expiration'\n        password_expiration:\n          type: integer\n          description: 'The password expires after the defined number of days. 0 means no expiration'\n        password_strength:\n          type: integer\n          description: 'Defines the minimum password strength. 0 means disabled, any password will be accepted. Values in the 50-70 range are suggested for common use cases'\n        access_time:\n          type: array\n          items:\n            $ref: '#/components/schemas/TimePeriod'\n      description: Additional user options\n    UserFilters:\n      allOf:\n        - $ref: '#/components/schemas/BaseUserFilters'\n        - type: object\n          properties:\n            require_password_change:\n              type: boolean\n              description: 'User must change password from WebClient/REST API at next login'\n            totp_config:\n              $ref: '#/components/schemas/UserTOTPConfig'\n            recovery_codes:\n              type: array\n              items:\n                $ref: '#/components/schemas/RecoveryCode'\n            tls_certs:\n              type: array\n              items:\n                type: string\n            additional_emails:\n              type: array\n              items:\n                type: string\n                format: email\n    Secret:\n      type: object\n      properties:\n        status:\n          type: string\n          enum:\n            - Plain\n            - AES-256-GCM\n            - Secretbox\n            - GCP\n            - AWS\n            - VaultTransit\n            - AzureKeyVault\n            - Redacted\n          description: 'Set to \"Plain\" to add or update an existing secret, set to \"Redacted\" to preserve the existing value'\n        payload:\n          type: string\n        key:\n          type: string\n        additional_data:\n          type: string\n        mode:\n          type: integer\n          description: 1 means encrypted using a master key\n      description: The secret is encrypted before saving, so to set a new secret you must provide a payload and set the status to \"Plain\". The encryption key and additional data will be generated automatically. If you set the status to \"Redacted\" the existing secret will be preserved\n    S3Config:\n      type: object\n      properties:\n        bucket:\n          type: string\n          minLength: 1\n        region:\n          type: string\n          minLength: 1\n        access_key:\n          type: string\n        access_secret:\n          $ref: '#/components/schemas/Secret'\n        sse_customer_key:\n          $ref: '#/components/schemas/Secret'\n        role_arn:\n          type: string\n          description: 'Optional IAM Role ARN to assume'\n        session_token:\n          type: string\n          description: 'Optional Session token that is a part of temporary security credentials provisioned by AWS STS'\n        endpoint:\n          type: string\n          description: optional endpoint\n        storage_class:\n          type: string\n        acl:\n          type: string\n          description: 'The canned ACL to apply to uploaded objects. Leave empty to use the default ACL. For more information and available ACLs, see here: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl'\n        upload_part_size:\n          type: integer\n          description: 'the buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB, and if this value is set to zero, the default value (5MB) for the AWS SDK will be used. The minimum allowed value is 5.'\n        upload_concurrency:\n          type: integer\n          description: 'the number of parts to upload in parallel. If this value is set to zero, the default value (5) will be used'\n        upload_part_max_time:\n          type: integer\n          description: 'the maximum time allowed, in seconds, to upload a single chunk (the chunk size is defined via \"upload_part_size\"). 0 means no timeout'\n        download_part_size:\n          type: integer\n          description: 'the buffer size (in MB) to use for multipart downloads. The minimum allowed part size is 5MB, and if this value is set to zero, the default value (5MB) for the AWS SDK will be used. The minimum allowed value is 5. Ignored for partial downloads'\n        download_concurrency:\n          type: integer\n          description: 'the number of parts to download in parallel. If this value is set to zero, the default value (5) will be used. Ignored for partial downloads'\n        download_part_max_time:\n          type: integer\n          description: 'the maximum time allowed, in seconds, to download a single chunk (the chunk size is defined via \"download_part_size\"). 0 means no timeout. Ignored for partial downloads.'\n        force_path_style:\n          type: boolean\n          description: 'Set this to \"true\" to force the request to use path-style addressing, i.e., \"http://s3.amazonaws.com/BUCKET/KEY\". By default, the S3 client will use virtual hosted bucket addressing when possible (\"http://BUCKET.s3.amazonaws.com/KEY\")'\n        key_prefix:\n          type: string\n          description: 'key_prefix is similar to a chroot directory for a local filesystem. If specified the user will only see contents that starts with this prefix and so you can restrict access to a specific virtual folder. The prefix, if not empty, must not start with \"/\" and must end with \"/\". If empty the whole bucket contents will be available'\n          example: folder/subfolder/\n      description: S3 Compatible Object Storage configuration details\n    GCSConfig:\n      type: object\n      properties:\n        bucket:\n          type: string\n          minLength: 1\n        credentials:\n          $ref: '#/components/schemas/Secret'\n        automatic_credentials:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            Automatic credentials:\n              * `0` - disabled, explicit credentials, using a JSON credentials file, must be provided. This is the default value if the field is null\n              * `1` - enabled, we try to use the Application Default Credentials (ADC) strategy to find your application's credentials\n        storage_class:\n          type: string\n        acl:\n          type: string\n          description: 'The ACL to apply to uploaded objects. Leave empty to use the default ACL. For more information and available ACLs, refer to the JSON API here: https://cloud.google.com/storage/docs/access-control/lists#predefined-acl'\n        key_prefix:\n          type: string\n          description: 'key_prefix is similar to a chroot directory for a local filesystem. If specified the user will only see contents that starts with this prefix and so you can restrict access to a specific virtual folder. The prefix, if not empty, must not start with \"/\" and must end with \"/\". If empty the whole bucket contents will be available'\n          example: folder/subfolder/\n        upload_part_size:\n          type: integer\n          description: 'The buffer size (in MB) to use for multipart uploads. The default value is 16MB. 0 means use the default'\n        upload_part_max_time:\n          type: integer\n          description: 'The maximum time allowed, in seconds, to upload a single chunk. The default value is 32. 0 means use the default'\n      description: 'Google Cloud Storage configuration details. The \"credentials\" field must be populated only when adding/updating a user. It will be always omitted, since there are sensitive data, when you search/get users'\n    AzureBlobFsConfig:\n      type: object\n      properties:\n        container:\n          type: string\n        account_name:\n          type: string\n          description: 'Storage Account Name, leave blank to use SAS URL'\n        account_key:\n          $ref: '#/components/schemas/Secret'\n        sas_url:\n          $ref: '#/components/schemas/Secret'\n        endpoint:\n          type: string\n          description: 'optional endpoint. Default is \"blob.core.windows.net\". If you use the emulator the endpoint must include the protocol, for example \"http://127.0.0.1:10000\"'\n        upload_part_size:\n          type: integer\n          description: 'the buffer size (in MB) to use for multipart uploads. If this value is set to zero, the default value (5MB) will be used.'\n        upload_concurrency:\n          type: integer\n          description: 'the number of parts to upload in parallel. If this value is set to zero, the default value (5) will be used'\n        download_part_size:\n          type: integer\n          description: 'the buffer size (in MB) to use for multipart downloads. If this value is set to zero, the default value (5MB) will be used.'\n        download_concurrency:\n          type: integer\n          description: 'the number of parts to download in parallel. If this value is set to zero, the default value (5) will be used'\n        access_tier:\n          type: string\n          enum:\n            - ''\n            - Archive\n            - Hot\n            - Cool\n        key_prefix:\n          type: string\n          description: 'key_prefix is similar to a chroot directory for a local filesystem. If specified the user will only see contents that starts with this prefix and so you can restrict access to a specific virtual folder. The prefix, if not empty, must not start with \"/\" and must end with \"/\". If empty the whole container contents will be available'\n          example: folder/subfolder/\n        use_emulator:\n          type: boolean\n      description: Azure Blob Storage configuration details\n    OSFsConfig:\n      type: object\n      properties:\n        read_buffer_size:\n          type: integer\n          minimum: 0\n          maximum: 10\n          description: \"The read buffer size, as MB, to use for downloads. 0 means no buffering, that's fine in most use cases.\"\n        write_buffer_size:\n          type: integer\n          minimum: 0\n          maximum: 10\n          description: \"The write buffer size, as MB, to use for uploads. 0 means no buffering, that's fine in most use cases.\"\n    CryptFsConfig:\n      type: object\n      properties:\n        passphrase:\n          $ref: '#/components/schemas/Secret'\n        read_buffer_size:\n          type: integer\n          minimum: 0\n          maximum: 10\n          description: \"The read buffer size, as MB, to use for downloads. 0 means no buffering, that's fine in most use cases.\"\n        write_buffer_size:\n          type: integer\n          minimum: 0\n          maximum: 10\n          description: \"The write buffer size, as MB, to use for uploads. 0 means no buffering, that's fine in most use cases.\"\n      description: Crypt filesystem configuration details\n    SFTPFsConfig:\n      type: object\n      properties:\n        endpoint:\n          type: string\n          description: 'remote SFTP endpoint as host:port'\n        username:\n          type: string\n          description: you can specify a password or private key or both. In the latter case the private key will be tried first.\n        password:\n          $ref: '#/components/schemas/Secret'\n        private_key:\n          $ref: '#/components/schemas/Secret'\n        key_passphrase:\n          $ref: '#/components/schemas/Secret'\n        fingerprints:\n          type: array\n          items:\n            type: string\n          description: 'SHA256 fingerprints to use for host key verification. If you don''t provide any fingerprint the remote host key will not be verified, this is a security risk'\n        prefix:\n          type: string\n          description: Specifying a prefix you can restrict all operations to a given path within the remote SFTP server.\n        disable_concurrent_reads:\n          type: boolean\n          description: Concurrent reads are safe to use and disabling them will degrade performance. Some servers automatically delete files once they are downloaded. Using concurrent reads is problematic with such servers.\n        buffer_size:\n          type: integer\n          minimum: 0\n          maximum: 16\n          example: 2\n          description: The size of the buffer (in MB) to use for transfers. By enabling buffering, the reads and writes, from/to the remote SFTP server, are split in multiple concurrent requests and this allows data to be transferred at a faster rate, over high latency networks, by overlapping round-trip times. With buffering enabled, resuming uploads is not supported and a file cannot be opened for both reading and writing at the same time. 0 means disabled.\n        equality_check_mode:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n             Defines how to check if this config points to the same server as another config. If different configs point to the same server the renaming between the fs configs is allowed:\n              * `0` username and endpoint must match. This is the default\n              * `1` only the endpoint must match\n    HTTPFsConfig:\n      type: object\n      properties:\n        endpoint:\n          type: string\n          description: 'HTTP/S endpoint URL. SFTPGo will use this URL as base, for example for the `stat` API, SFTPGo will add `/stat/{name}`'\n        username:\n          type: string\n        password:\n          $ref: '#/components/schemas/Secret'\n        api_key:\n          $ref: '#/components/schemas/Secret'\n        skip_tls_verify:\n          type: boolean\n        equality_check_mode:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n             Defines how to check if this config points to the same server as another config. If different configs point to the same server the renaming between the fs configs is allowed:\n              * `0` username and endpoint must match. This is the default\n              * `1` only the endpoint must match\n    FilesystemConfig:\n      type: object\n      properties:\n        provider:\n          $ref: '#/components/schemas/FsProviders'\n        osconfig:\n          $ref: '#/components/schemas/OSFsConfig'\n        s3config:\n          $ref: '#/components/schemas/S3Config'\n        gcsconfig:\n          $ref: '#/components/schemas/GCSConfig'\n        azblobconfig:\n          $ref: '#/components/schemas/AzureBlobFsConfig'\n        cryptconfig:\n          $ref: '#/components/schemas/CryptFsConfig'\n        sftpconfig:\n          $ref: '#/components/schemas/SFTPFsConfig'\n        httpconfig:\n          $ref: '#/components/schemas/HTTPFsConfig'\n      description: Storage filesystem details\n    BaseVirtualFolder:\n      type: object\n      properties:\n        id:\n          type: integer\n          format: int32\n          minimum: 1\n        name:\n          type: string\n          description: unique name for this virtual folder\n        mapped_path:\n          type: string\n          description: absolute filesystem path to use as virtual folder\n        description:\n          type: string\n          description: optional description\n        used_quota_size:\n          type: integer\n          format: int64\n        used_quota_files:\n          type: integer\n          format: int32\n        last_quota_update:\n          type: integer\n          format: int64\n          description: Last quota update as unix timestamp in milliseconds\n        users:\n          type: array\n          items:\n            type: string\n          description: list of usernames associated with this virtual folder\n        filesystem:\n          $ref: '#/components/schemas/FilesystemConfig'\n      description: 'Defines the filesystem for the virtual folder and the used quota limits. The same folder can be shared among multiple users and each user can have different quota limits or a different virtual path.'\n    VirtualFolder:\n      allOf:\n        - $ref: '#/components/schemas/BaseVirtualFolder'\n        - type: object\n          properties:\n            virtual_path:\n              type: string\n            quota_size:\n              type: integer\n              format: int64\n              description: 'Quota as size in bytes. 0 means unlimited, -1 means included in user quota. Please note that quota is updated if files are added/removed via SFTPGo otherwise a quota scan or a manual quota update is needed'\n            quota_files:\n              type: integer\n              format: int32\n              description: 'Quota as number of files. 0 means unlimited, , -1 means included in user quota. Please note that quota is updated if files are added/removed via SFTPGo otherwise a quota scan or a manual quota update is needed'\n          required:\n            - virtual_path\n      description: 'A virtual folder is a mapping between a SFTPGo virtual path and a filesystem path outside the user home directory. The specified paths must be absolute and the virtual path cannot be \"/\", it must be a sub directory. The parent directory for the specified virtual path must exist. SFTPGo will try to automatically create any missing parent directory for the configured virtual folders at user login.'\n    User:\n      type: object\n      properties:\n        id:\n          type: integer\n          format: int32\n          minimum: 1\n        status:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            status:\n              * `0` user is disabled, login is not allowed\n              * `1` user is enabled\n        username:\n          type: string\n          description: username is unique\n        email:\n          type: string\n          format: email\n        description:\n          type: string\n          description: 'optional description, for example the user full name'\n        expiration_date:\n          type: integer\n          format: int64\n          description: expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration\n        password:\n          type: string\n          format: password\n          description: If the password has no known hashing algo prefix it will be stored, by default, using bcrypt, argon2id is supported too. You can send a password hashed as bcrypt ($2a$ prefix), argon2id, pbkdf2 or unix crypt and it will be stored as is. For security reasons this field is omitted when you search/get users\n        public_keys:\n          type: array\n          items:\n            type: string\n            example: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEUWwDwEWhTbF0MqAsp/oXK1HR2cElhM8oo1uVmL3ZeDKDiTm4ljMr92wfTgIGDqIoxmVqgYIkAOAhuykAVWBzc= user@host\n          description: Public keys in OpenSSH format.\n        has_password:\n          type: boolean\n          description: Indicates whether the password is set\n        home_dir:\n          type: string\n          description: path to the user home directory. The user cannot upload or download files outside this directory. SFTPGo tries to automatically create this folder if missing. Must be an absolute path\n        virtual_folders:\n          type: array\n          items:\n            $ref: '#/components/schemas/VirtualFolder'\n          description: mapping between virtual SFTPGo paths and virtual folders\n        uid:\n          type: integer\n          format: int32\n          minimum: 0\n          maximum: 2147483647\n          description: 'if you run SFTPGo as root user, the created files and directories will be assigned to this uid. 0 means no change, the owner will be the user that runs SFTPGo. Ignored on windows'\n        gid:\n          type: integer\n          format: int32\n          minimum: 0\n          maximum: 2147483647\n          description: 'if you run SFTPGo as root user, the created files and directories will be assigned to this gid. 0 means no change, the group will be the one of the user that runs SFTPGo. Ignored on windows'\n        max_sessions:\n          type: integer\n          format: int32\n          description: Limit the sessions that a user can open. 0 means unlimited\n        quota_size:\n          type: integer\n          format: int64\n          description: Quota as size in bytes. 0 means unlimited. Please note that quota is updated if files are added/removed via SFTPGo otherwise a quota scan or a manual quota update is needed\n        quota_files:\n          type: integer\n          format: int32\n          description: Quota as number of files. 0 means unlimited. Please note that quota is updated if files are added/removed via SFTPGo otherwise a quota scan or a manual quota update is needed\n        permissions:\n          type: object\n          additionalProperties:\n            type: array\n            items:\n              $ref: '#/components/schemas/Permission'\n            minItems: 1\n          minProperties: 1\n          description: 'hash map with directory as key and an array of permissions as value. Directories must be absolute paths, permissions for root directory (\"/\") are required'\n          example:\n            /:\n              - '*'\n            /somedir:\n              - list\n              - download\n        used_quota_size:\n          type: integer\n          format: int64\n        used_quota_files:\n          type: integer\n          format: int32\n        last_quota_update:\n          type: integer\n          format: int64\n          description: Last quota update as unix timestamp in milliseconds\n        upload_bandwidth:\n          type: integer\n          description: 'Maximum upload bandwidth as KB/s, 0 means unlimited'\n        download_bandwidth:\n          type: integer\n          description: 'Maximum download bandwidth as KB/s, 0 means unlimited'\n        upload_data_transfer:\n          type: integer\n          description: 'Maximum data transfer allowed for uploads as MB. 0 means no limit'\n        download_data_transfer:\n          type: integer\n          description: 'Maximum data transfer allowed for downloads as MB. 0 means no limit'\n        total_data_transfer:\n          type: integer\n          description: 'Maximum total data transfer as MB. 0 means unlimited. You can set a total data transfer instead of the individual values for uploads and downloads'\n        used_upload_data_transfer:\n          type: integer\n          description: 'Uploaded size, as bytes, since the last reset'\n        used_download_data_transfer:\n          type: integer\n          description: 'Downloaded size, as bytes, since the last reset'\n        created_at:\n          type: integer\n          format: int64\n          description: 'creation time as unix timestamp in milliseconds. It will be 0 for users created before v2.2.0'\n        updated_at:\n          type: integer\n          format: int64\n          description: last update time as unix timestamp in milliseconds\n        last_login:\n          type: integer\n          format: int64\n          description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes\n        first_download:\n          type: integer\n          format: int64\n          description: first download time as unix timestamp in milliseconds\n        first_upload:\n          type: integer\n          format: int64\n          description: first upload time as unix timestamp in milliseconds\n        last_password_change:\n          type: integer\n          format: int64\n          description: last password change time as unix timestamp in milliseconds\n        filters:\n          $ref: '#/components/schemas/UserFilters'\n        filesystem:\n          $ref: '#/components/schemas/FilesystemConfig'\n        additional_info:\n          type: string\n          description: Free form text field for external systems\n        groups:\n          type: array\n          items:\n            $ref: '#/components/schemas/GroupMapping'\n        oidc_custom_fields:\n          type: object\n          additionalProperties: true\n          description: 'This field is passed to the pre-login hook if custom OIDC token fields have been configured. Field values can be of any type (this is a free form object) and depend on the type of the configured OIDC token fields'\n        role:\n          type: string\n    AdminPreferences:\n      type: object\n      properties:\n        hide_user_page_sections:\n          type: integer\n          description: 'Allow to hide some sections from the user page. These are not security settings and are not enforced server side in any way. They are only intended to simplify the user page in the WebAdmin UI. 1 means hide groups section, 2 means hide filesystem section, \"users_base_dir\" must be set in the config file otherwise this setting is ignored, 4 means hide virtual folders section, 8 means hide profile section, 16 means hide ACLs section, 32 means hide disk and bandwidth quota limits section, 64 means hide advanced settings section. The settings can be combined'\n        default_users_expiration:\n          type: integer\n          description: 'Defines the default expiration for newly created users as number of days. 0 means no expiration'\n    AdminFilters:\n      type: object\n      properties:\n        allow_list:\n          type: array\n          items:\n            type: string\n          description: 'only clients connecting from these IP/Mask are allowed. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example \"192.0.2.0/24\" or \"2001:db8::/32\"'\n          example:\n            - 192.0.2.0/24\n            - '2001:db8::/32'\n        allow_api_key_auth:\n          type: boolean\n          description: 'API key auth allows to impersonate this administrator with an API key'\n        require_two_factor:\n          type: boolean\n        require_password_change:\n          type: boolean\n        totp_config:\n          $ref: '#/components/schemas/AdminTOTPConfig'\n        recovery_codes:\n          type: array\n          items:\n            $ref: '#/components/schemas/RecoveryCode'\n        preferences:\n          $ref: '#/components/schemas/AdminPreferences'\n    Admin:\n      type: object\n      properties:\n        id:\n          type: integer\n          format: int32\n          minimum: 1\n        status:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            status:\n              * `0` user is disabled, login is not allowed\n              * `1` user is enabled\n        username:\n          type: string\n          description: username is unique\n        description:\n          type: string\n          description: 'optional description, for example the admin full name'\n        password:\n          type: string\n          format: password\n          description: Admin password. For security reasons this field is omitted when you search/get admins\n        email:\n          type: string\n          format: email\n        permissions:\n          type: array\n          items:\n            $ref: '#/components/schemas/AdminPermissions'\n        filters:\n          $ref: '#/components/schemas/AdminFilters'\n        additional_info:\n          type: string\n          description: Free form text field\n        groups:\n          type: array\n          items:\n            $ref: '#/components/schemas/AdminGroupMapping'\n          description: 'Groups automatically selected for new users created by this admin. The admin will still be able to choose different groups. These settings are only used for this admin UI and they will be ignored in REST API/hooks.'\n        created_at:\n          type: integer\n          format: int64\n          description: 'creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0'\n        updated_at:\n          type: integer\n          format: int64\n          description: last update time as unix timestamp in milliseconds\n        last_login:\n          type: integer\n          format: int64\n          description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes\n        role:\n          type: string\n          description: 'If set the admin can only administer users with the same role. Role admins cannot have the \"*\" permission'\n    AdminProfile:\n      type: object\n      properties:\n        email:\n          type: string\n          format: email\n        description:\n          type: string\n        allow_api_key_auth:\n          type: boolean\n          description: 'If enabled, you can impersonate this admin, in REST API, using an API key. If disabled admin credentials are required for impersonation'\n    UserProfile:\n      type: object\n      properties:\n        email:\n          type: string\n          format: email\n        description:\n          type: string\n        allow_api_key_auth:\n          type: boolean\n          description: 'If enabled, you can impersonate this user, in REST API, using an API key. If disabled user credentials are required for impersonation'\n        public_keys:\n          type: array\n          items:\n            type: string\n            example: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEUWwDwEWhTbF0MqAsp/oXK1HR2cElhM8oo1uVmL3ZeDKDiTm4ljMr92wfTgIGDqIoxmVqgYIkAOAhuykAVWBzc= user@host\n            description: Public keys in OpenSSH format\n    APIKey:\n      type: object\n      properties:\n        id:\n          type: string\n          description: unique key identifier\n        name:\n          type: string\n          description: User friendly key name\n        key:\n          type: string\n          format: password\n          description: We store the hash of the key. This is just like a password. For security reasons this field is omitted when you search/get API keys\n        scope:\n          $ref: '#/components/schemas/APIKeyScope'\n        created_at:\n          type: integer\n          format: int64\n          description: creation time as unix timestamp in milliseconds\n        updated_at:\n          type: integer\n          format: int64\n          description: last update time as unix timestamp in milliseconds\n        last_use_at:\n          type: integer\n          format: int64\n          description: last use time as unix timestamp in milliseconds. It is saved at most once every 10 minutes\n        expires_at:\n          type: integer\n          format: int64\n          description: expiration time as unix timestamp in milliseconds\n        description:\n          type: string\n          description: optional description\n        user:\n          type: string\n          description: username associated with this API key. If empty and the scope is \"user scope\" the key can impersonate any user\n        admin:\n          type: string\n          description: admin associated with this API key. If empty and the scope is \"admin scope\" the key can impersonate any admin\n    QuotaUsage:\n      type: object\n      properties:\n        used_quota_size:\n          type: integer\n          format: int64\n        used_quota_files:\n          type: integer\n          format: int32\n    TransferQuotaUsage:\n      type: object\n      properties:\n        used_upload_data_transfer:\n          type: integer\n          format: int64\n          description: 'The value must be specified as bytes'\n        used_download_data_transfer:\n          type: integer\n          format: int64\n          description: 'The value must be specified as bytes'\n    Transfer:\n      type: object\n      properties:\n        operation_type:\n          type: string\n          enum:\n            - upload\n            - download\n          description: |\n            Operations:\n              * `upload`\n              * `download`\n        path:\n          type: string\n          description: file path for the upload/download\n        start_time:\n          type: integer\n          format: int64\n          description: start time as unix timestamp in milliseconds\n        size:\n          type: integer\n          format: int64\n          description: bytes transferred\n    ConnectionStatus:\n      type: object\n      properties:\n        username:\n          type: string\n          description: connected username\n        connection_id:\n          type: string\n          description: unique connection identifier\n        client_version:\n          type: string\n          description: client version\n        remote_address:\n          type: string\n          description: Remote address for the connected client\n        connection_time:\n          type: integer\n          format: int64\n          description: connection time as unix timestamp in milliseconds\n        command:\n          type: string\n          description: Last SSH/FTP command or WebDAV method\n        last_activity:\n          type: integer\n          format: int64\n          description: last client activity as unix timestamp in milliseconds\n        protocol:\n          type: string\n          enum:\n            - SFTP\n            - SCP\n            - SSH\n            - FTP\n            - DAV\n        active_transfers:\n          type: array\n          items:\n            $ref: '#/components/schemas/Transfer'\n        node:\n          type: string\n          description: 'Node identifier, omitted for single node installations'\n    FolderRetention:\n      type: object\n      properties:\n        path:\n          type: string\n          description: 'virtual directory path as seen by users, if no other specific retention is defined, the retention applies for sub directories too. For example if retention is defined for the paths \"/\" and \"/sub\" then the retention for \"/\" is applied for any file outside the \"/sub\" directory'\n          example: '/'\n        retention:\n          type: integer\n          description: retention time in hours. All the files with a modification time older than the defined value will be deleted. 0 means exclude this path\n          example: 24\n        delete_empty_dirs:\n          type: boolean\n          description: if enabled, empty directories will be deleted\n    RetentionCheck:\n      type: object\n      properties:\n        username:\n          type: string\n          description: username to which the retention check refers\n        folders:\n          type: array\n          items:\n            $ref: '#/components/schemas/FolderRetention'\n        start_time:\n          type: integer\n          format: int64\n          description: check start time as unix timestamp in milliseconds\n    QuotaScan:\n      type: object\n      properties:\n        username:\n          type: string\n          description: username to which the quota scan refers\n        start_time:\n          type: integer\n          format: int64\n          description: scan start time as unix timestamp in milliseconds\n    FolderQuotaScan:\n      type: object\n      properties:\n        name:\n          type: string\n          description: folder name to which the quota scan refers\n        start_time:\n          type: integer\n          format: int64\n          description: scan start time as unix timestamp in milliseconds\n    DefenderEntry:\n      type: object\n      properties:\n        id:\n          type: string\n        ip:\n          type: string\n        score:\n          type: integer\n          description: the score increases whenever a violation is detected, such as an attempt to log in using an incorrect password or invalid username. If the score exceeds the configured threshold, the IP is banned. Omitted for banned IPs\n        ban_time:\n          type: string\n          format: date-time\n          description: date time until the IP is banned. For already banned hosts, the ban time is increased each time a new violation is detected. Omitted if the IP is not banned\n    SSHHostKey:\n      type: object\n      properties:\n        path:\n          type: string\n        fingerprint:\n          type: string\n        algorithms:\n          type: array\n          items:\n            type: string\n    SSHBinding:\n      type: object\n      properties:\n        address:\n          type: string\n          description: TCP address the server listen on\n        port:\n          type: integer\n          description: the port used for serving requests\n        apply_proxy_config:\n          type: boolean\n          description: 'apply the proxy configuration, if any'\n    WebDAVBinding:\n      type: object\n      properties:\n        address:\n          type: string\n          description: TCP address the server listen on\n        port:\n          type: integer\n          description: the port used for serving requests\n        enable_https:\n          type: boolean\n        min_tls_version:\n          $ref: '#/components/schemas/TLSVersions'\n        client_auth_type:\n          type: integer\n          description: 1 means that client certificate authentication is required in addition to HTTP basic authentication\n        tls_cipher_suites:\n          type: array\n          items:\n            type: string\n          description: 'List of supported cipher suites for TLS version 1.2. If empty  a default list of secure cipher suites is used, with a preference order based on hardware performance'\n        prefix:\n          type: string\n          description: 'Prefix for WebDAV resources, if empty WebDAV resources will be available at the `/` URI'\n        proxy_allowed:\n          type: array\n          items:\n            type: string\n          description: 'List of IP addresses and IP ranges allowed to set proxy headers'\n    PassiveIPOverride:\n      type: object\n      properties:\n        networks:\n          type: array\n          items:\n            type: string\n        ip:\n          type: string\n    FTPDBinding:\n      type: object\n      properties:\n        address:\n          type: string\n          description: TCP address the server listen on\n        port:\n          type: integer\n          description: the port used for serving requests\n        apply_proxy_config:\n          type: boolean\n          description: 'apply the proxy configuration, if any'\n        tls_mode:\n          type: integer\n          enum:\n            - 0\n            - 1\n            - 2\n          description: |\n            TLS mode:\n              * `0` - clear or explicit TLS\n              * `1` - explicit TLS required\n              * `2` - implicit TLS\n        min_tls_version:\n          $ref: '#/components/schemas/TLSVersions'\n        force_passive_ip:\n          type: string\n          description: External IP address for passive connections\n        passive_ip_overrides:\n          type: array\n          items:\n            $ref: '#/components/schemas/PassiveIPOverride'\n        client_auth_type:\n          type: integer\n          description: 1 means that client certificate authentication is required in addition to FTP authentication\n        tls_cipher_suites:\n          type: array\n          items:\n            type: string\n          description: 'List of supported cipher suites for TLS version 1.2. If empty  a default list of secure cipher suites is used, with a preference order based on hardware performance'\n        passive_connections_security:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            Active connections security:\n              * `0` - require matching peer IP addresses of control and data connection\n              * `1` - disable any checks\n        active_connections_security:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            Active connections security:\n              * `0` - require matching peer IP addresses of control and data connection\n              * `1` - disable any checks\n        ignore_ascii_transfer_type:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            Ignore client requests to perform ASCII translations:\n              * `0` - ASCII translations are enabled\n              * `1` - ASCII translations are silently ignored\n        debug:\n          type: boolean\n          description: 'If enabled any FTP command will be logged'\n    SSHServiceStatus:\n      type: object\n      properties:\n        is_active:\n          type: boolean\n        bindings:\n          type: array\n          items:\n            $ref: '#/components/schemas/SSHBinding'\n          nullable: true\n        host_keys:\n          type: array\n          items:\n            $ref: '#/components/schemas/SSHHostKey'\n          nullable: true\n        ssh_commands:\n          type: array\n          items:\n            type: string\n        authentications:\n          type: array\n          items:\n            $ref: '#/components/schemas/SSHAuthentications'\n        public_key_algorithms:\n          type: array\n          items:\n            type: string\n        macs:\n          type: array\n          items:\n            type: string\n        kex_algorithms:\n          type: array\n          items:\n            type: string\n        ciphers:\n          type: array\n          items:\n            type: string\n    FTPPassivePortRange:\n      type: object\n      properties:\n        start:\n          type: integer\n        end:\n          type: integer\n    FTPServiceStatus:\n      type: object\n      properties:\n        is_active:\n          type: boolean\n        bindings:\n          type: array\n          items:\n            $ref: '#/components/schemas/FTPDBinding'\n          nullable: true\n        passive_port_range:\n          $ref: '#/components/schemas/FTPPassivePortRange'\n    WebDAVServiceStatus:\n      type: object\n      properties:\n        is_active:\n          type: boolean\n        bindings:\n          type: array\n          items:\n            $ref: '#/components/schemas/WebDAVBinding'\n          nullable: true\n    DataProviderStatus:\n      type: object\n      properties:\n        is_active:\n          type: boolean\n        driver:\n          type: string\n        error:\n          type: string\n    MFAStatus:\n      type: object\n      properties:\n        is_active:\n          type: boolean\n        totp_configs:\n          type: array\n          items:\n            $ref: '#/components/schemas/TOTPConfig'\n    ServicesStatus:\n      type: object\n      properties:\n        ssh:\n          $ref: '#/components/schemas/SSHServiceStatus'\n        ftp:\n          $ref: '#/components/schemas/FTPServiceStatus'\n        webdav:\n          $ref: '#/components/schemas/WebDAVServiceStatus'\n        data_provider:\n          $ref: '#/components/schemas/DataProviderStatus'\n        defender:\n          type: object\n          properties:\n            is_active:\n              type: boolean\n        mfa:\n          $ref: '#/components/schemas/MFAStatus'\n        allow_list:\n          type: object\n          properties:\n            is_active:\n              type: boolean\n        rate_limiters:\n          type: object\n          properties:\n            is_active:\n              type: boolean\n            protocols:\n              type: array\n              items:\n                type: string\n                example: SSH\n    Share:\n      type: object\n      properties:\n        id:\n          type: string\n          description: auto-generated unique share identifier\n        name:\n          type: string\n        description:\n          type: string\n          description: optional description\n        scope:\n          $ref: '#/components/schemas/ShareScope'\n        paths:\n          type: array\n          items:\n            type: string\n          description: 'paths to files or directories, for share scope write this array must contain exactly one directory. Paths will not be validated on save so you can also create them after creating the share'\n          example:\n            - '/dir1'\n            - '/dir2/file.txt'\n            - '/dir3/subdir'\n        username:\n          type: string\n        created_at:\n          type: integer\n          format: int64\n          description: 'creation time as unix timestamp in milliseconds'\n        updated_at:\n          type: integer\n          format: int64\n          description: 'last update time as unix timestamp in milliseconds'\n        last_use_at:\n          type: integer\n          format: int64\n          description: last use time as unix timestamp in milliseconds\n        expires_at:\n          type: integer\n          format: int64\n          description: 'optional share expiration, as unix timestamp in milliseconds. 0 means no expiration'\n        password:\n          type: string\n          description: 'optional password to protect the share. The special value \"[**redacted**]\" means that a password has been set, you can use this value if you want to preserve the current password when you update a share'\n        max_tokens:\n          type: integer\n          description: 'maximum allowed access tokens. 0 means no limit'\n        used_tokens:\n          type: integer\n        allow_from:\n          type: array\n          items:\n            type: string\n          description: 'Limit the share availability to these IP/Mask. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example \"192.0.2.0/24\" or \"2001:db8::/32\". An empty list means no restrictions'\n          example:\n            - 192.0.2.0/24\n            - '2001:db8::/32'\n    GroupUserSettings:\n      type: object\n      properties:\n        home_dir:\n          type: string\n        max_sessions:\n          type: integer\n          format: int32\n        quota_size:\n          type: integer\n          format: int64\n        quota_files:\n          type: integer\n          format: int32\n        permissions:\n          type: object\n          additionalProperties:\n            type: array\n            items:\n              $ref: '#/components/schemas/Permission'\n            minItems: 1\n          minProperties: 1\n          description: 'hash map with directory as key and an array of permissions as value. Directories must be absolute paths, permissions for root directory (\"/\") are required'\n          example:\n            /:\n              - '*'\n            /somedir:\n              - list\n              - download\n        upload_bandwidth:\n          type: integer\n          description: 'Maximum upload bandwidth as KB/s'\n        download_bandwidth:\n          type: integer\n          description: 'Maximum download bandwidth as KB/s'\n        upload_data_transfer:\n          type: integer\n          description: 'Maximum data transfer allowed for uploads as MB'\n        download_data_transfer:\n          type: integer\n          description: 'Maximum data transfer allowed for downloads as MB'\n        total_data_transfer:\n          type: integer\n          description: 'Maximum total data transfer as MB'\n        expires_in:\n          type: integer\n          description: 'Account expiration in number of days from creation. 0 means no expiration'\n        filters:\n          $ref: '#/components/schemas/BaseUserFilters'\n        filesystem:\n          $ref: '#/components/schemas/FilesystemConfig'\n    Role:\n      type: object\n      properties:\n        id:\n          type: integer\n          format: int32\n          minimum: 1\n        name:\n          type: string\n          description: name is unique\n        description:\n          type: string\n          description: 'optional description'\n        created_at:\n          type: integer\n          format: int64\n          description: creation time as unix timestamp in milliseconds\n        updated_at:\n          type: integer\n          format: int64\n          description: last update time as unix timestamp in milliseconds\n        users:\n          type: array\n          items:\n            type: string\n          description: list of usernames associated with this group\n        admins:\n          type: array\n          items:\n            type: string\n          description: list of admins usernames associated with this group\n    Group:\n      type: object\n      properties:\n        id:\n          type: integer\n          format: int32\n          minimum: 1\n        name:\n          type: string\n          description: name is unique\n        description:\n          type: string\n          description: 'optional description'\n        created_at:\n          type: integer\n          format: int64\n          description: creation time as unix timestamp in milliseconds\n        updated_at:\n          type: integer\n          format: int64\n          description: last update time as unix timestamp in milliseconds\n        user_settings:\n          $ref: '#/components/schemas/GroupUserSettings'\n        virtual_folders:\n          type: array\n          items:\n            $ref: '#/components/schemas/VirtualFolder'\n          description: mapping between virtual SFTPGo paths and folders\n        users:\n          type: array\n          items:\n            type: string\n          description: list of usernames associated with this group\n        admins:\n          type: array\n          items:\n            type: string\n          description: list of admins usernames associated with this group\n    GroupMapping:\n      type: object\n      properties:\n        name:\n          type: string\n          description: group name\n        type:\n          enum:\n            - 1\n            - 2\n            - 3\n          description: |\n            Group type:\n              * `1` - Primary group\n              * `2` - Secondary group\n              * `3` - Membership only, no settings are inherited from this group type\n    AdminGroupMappingOptions:\n      type: object\n      properties:\n        add_to_users_as:\n          enum:\n            - 0\n            - 1\n            - 2\n          description: |\n            Add to new users as:\n              * `0` - the admin's group will be added as membership group for new users\n              * `1` - the admin's group will be added as primary group for new users\n              * `2` - the admin's group will be added as secondary group for new users\n    AdminGroupMapping:\n      type: object\n      properties:\n        name:\n          type: string\n          description: group name\n        options:\n          $ref: '#/components/schemas/AdminGroupMappingOptions'\n    BackupData:\n      type: object\n      properties:\n        users:\n          type: array\n          items:\n            $ref: '#/components/schemas/User'\n        folders:\n          type: array\n          items:\n            $ref: '#/components/schemas/BaseVirtualFolder'\n        groups:\n          type: array\n          items:\n            $ref: '#/components/schemas/Group'\n        admins:\n          type: array\n          items:\n            $ref: '#/components/schemas/Admin'\n        api_keys:\n          type: array\n          items:\n            $ref: '#/components/schemas/APIKey'\n        shares:\n          type: array\n          items:\n            $ref: '#/components/schemas/Share'\n        event_actions:\n          type: array\n          items:\n            $ref: '#/components/schemas/EventAction'\n        event_rules:\n          type: array\n          items:\n            $ref: '#/components/schemas/EventRule'\n        roles:\n          type: array\n          items:\n            $ref: '#/components/schemas/Role'\n        version:\n          type: integer\n    PwdChange:\n      type: object\n      properties:\n        current_password:\n          type: string\n        new_password:\n          type: string\n    DirEntry:\n      type: object\n      properties:\n        name:\n          type: string\n          description: name of the file (or subdirectory) described by the entry. This name is the final element of the path (the base name), not the entire path\n        size:\n          type: integer\n          format: int64\n          description: file size, omitted for folders and non regular files\n        mode:\n          type: integer\n          description: |\n            File mode and permission bits. More details here: https://golang.org/pkg/io/fs/#FileMode.\n            Let's see some examples:\n            - for a directory mode&2147483648 != 0\n            - for a symlink mode&134217728 != 0\n            - for a regular file mode&2401763328 == 0\n        last_modified:\n          type: string\n          format: date-time\n    FsEvent:\n      type: object\n      properties:\n        id:\n          type: string\n        timestamp:\n          type: integer\n          format: int64\n          description: 'unix timestamp in nanoseconds'\n        action:\n          $ref: '#/components/schemas/FsEventAction'\n        username:\n          type: string\n        fs_path:\n          type: string\n        fs_target_path:\n          type: string\n        virtual_path:\n          type: string\n        virtual_target_path:\n          type: string\n        ssh_cmd:\n          type: string\n        file_size:\n          type: integer\n          format: int64\n        elapsed:\n          type: integer\n          format: int64\n          description: elapsed time as milliseconds\n        status:\n          $ref: '#/components/schemas/FsEventStatus'\n        protocol:\n          $ref: '#/components/schemas/EventProtocols'\n        ip:\n          type: string\n        session_id:\n          type: string\n        fs_provider:\n          $ref: '#/components/schemas/FsProviders'\n        bucket:\n          type: string\n        endpoint:\n          type: string\n        open_flags:\n          type: string\n        role:\n          type: string\n        instance_id:\n          type: string\n    ProviderEvent:\n      type: object\n      properties:\n        id:\n          type: string\n        timestamp:\n          type: integer\n          format: int64\n          description: 'unix timestamp in nanoseconds'\n        action:\n          $ref: '#/components/schemas/ProviderEventAction'\n        username:\n          type: string\n        ip:\n          type: string\n        object_type:\n          $ref: '#/components/schemas/ProviderEventObjectType'\n        object_name:\n          type: string\n        object_data:\n          type: string\n          format: byte\n          description: 'base64 of the JSON serialized object with sensitive fields removed'\n        role:\n          type: string\n        instance_id:\n          type: string\n    LogEvent:\n      type: object\n      properties:\n        id:\n          type: string\n        timestamp:\n          type: integer\n          format: int64\n          description: 'unix timestamp in nanoseconds'\n        event:\n          $ref: '#/components/schemas/LogEventType'\n        protocol:\n          $ref: '#/components/schemas/EventProtocols'\n        username:\n          type: string\n        ip:\n          type: string\n        message:\n          type: string\n        role:\n          type: string\n        instance_id:\n          type: string\n    KeyValue:\n      type: object\n      properties:\n        key:\n          type: string\n        value:\n          type: string\n    RenameConfig:\n      allOf:\n        - $ref: '#/components/schemas/KeyValue'\n        - type: object\n          properties:\n            update_modtime:\n              type: boolean\n              description: 'Update modification time. This setting is not recursive and only applies to storage providers that support changing modification times'\n    HTTPPart:\n      type: object\n      properties:\n        name:\n          type: string\n        headers:\n          type: array\n          items:\n            $ref: '#/components/schemas/KeyValue'\n          description: 'Additional headers. Content-Disposition header is automatically set. Content-Type header is automatically detect for files to attach'\n        filepath:\n          type: string\n          description: 'path to the file to be sent as an attachment'\n        body:\n          type: string\n    EventActionHTTPConfig:\n      type: object\n      properties:\n        endpoint:\n          type: string\n          description: HTTP endpoint\n          example: https://example.com\n        username:\n          type: string\n        password:\n          $ref: '#/components/schemas/Secret'\n        headers:\n          type: array\n          items:\n            $ref: '#/components/schemas/KeyValue'\n          description: headers to add\n        timeout:\n          type: integer\n          minimum: 1\n          maximum: 180\n          description: 'Ignored for multipart requests with files as attachments'\n        skip_tls_verify:\n          type: boolean\n          description: 'if enabled the HTTP client accepts any TLS certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing.'\n        method:\n          type: string\n          enum:\n            - GET\n            - POST\n            - PUT\n            - DELETE\n        query_parameters:\n          type: array\n          items:\n            $ref: '#/components/schemas/KeyValue'\n        body:\n          type: string\n          description: HTTP POST/PUT body\n        parts:\n          type: array\n          items:\n            $ref: '#/components/schemas/HTTPPart'\n          description: 'Multipart requests allow to combine one or more sets of data into a single body. For each part, you can set a file path or a body as text. Placeholders are supported in file path, body, header values.'\n    EventActionCommandConfig:\n      type: object\n      properties:\n        cmd:\n          type: string\n          description: absolute path to the command to execute\n        args:\n          type: array\n          items:\n            type: string\n          description: 'command line arguments'\n        timeout:\n          type: integer\n          minimum: 1\n          maximum: 120\n        env_vars:\n          type: array\n          items:\n            $ref: '#/components/schemas/KeyValue'\n    EventActionEmailConfig:\n      type: object\n      properties:\n        recipients:\n          type: array\n          items:\n            type: string\n        bcc:\n          type: array\n          items:\n            type: string\n        subject:\n          type: string\n        body:\n          type: string\n        content_type:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            Content type:\n              * `0` text/plain\n              * `1` text/html\n        attachments:\n          type: array\n          items:\n            type: string\n          description: 'list of file paths to attach. The total size is limited to 10 MB'\n    EventActionDataRetentionConfig:\n      type: object\n      properties:\n        folders:\n          type: array\n          items:\n            $ref: '#/components/schemas/FolderRetention'\n    EventActionFsCompress:\n      type: object\n      properties:\n        name:\n          type: string\n          description: 'Full path to the (zip) archive to create. The parent dir must exist'\n        paths:\n          type: array\n          items:\n            type: string\n          description: 'paths to add the archive'\n    EventActionFilesystemConfig:\n      type: object\n      properties:\n        type:\n          $ref: '#/components/schemas/FilesystemActionTypes'\n        renames:\n          type: array\n          items:\n            $ref: '#/components/schemas/RenameConfig'\n        mkdirs:\n          type: array\n          items:\n            type: string\n        deletes:\n          type: array\n          items:\n            type: string\n        exist:\n          type: array\n          items:\n            type: string\n        copy:\n          type: array\n          items:\n            $ref: '#/components/schemas/KeyValue'\n        compress:\n          $ref: '#/components/schemas/EventActionFsCompress'\n    EventActionPasswordExpiration:\n      type: object\n      properties:\n        threshold:\n          type: integer\n          description: 'An email notification will be generated for users whose password expires in a number of days less than or equal to this threshold'\n    EventActionUserInactivity:\n      type: object\n      properties:\n        disable_threshold:\n          type: integer\n          description: 'Inactivity threshold, in days, before disabling the account'\n        delete_threshold:\n          type: integer\n          description: 'Inactivity threshold, in days, before deleting the account'\n    EventActionIDPAccountCheck:\n      type: object\n      properties:\n        mode:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            Account check mode:\n              * `0` Create or update the account\n              * `1` Create the account if it doesn't exist\n        template_user:\n          type: string\n          description: 'SFTPGo user template in JSON format'\n        template_admin:\n          type: string\n          description: 'SFTPGo admin template in JSON format'\n    BaseEventActionOptions:\n      type: object\n      properties:\n        http_config:\n          $ref: '#/components/schemas/EventActionHTTPConfig'\n        cmd_config:\n          $ref: '#/components/schemas/EventActionCommandConfig'\n        email_config:\n          $ref: '#/components/schemas/EventActionEmailConfig'\n        retention_config:\n          $ref: '#/components/schemas/EventActionDataRetentionConfig'\n        fs_config:\n          $ref: '#/components/schemas/EventActionFilesystemConfig'\n        pwd_expiration_config:\n          $ref: '#/components/schemas/EventActionPasswordExpiration'\n        user_inactivity_config:\n          $ref: '#/components/schemas/EventActionUserInactivity'\n        idp_config:\n          $ref: '#/components/schemas/EventActionIDPAccountCheck'\n    BaseEventAction:\n      type: object\n      properties:\n        id:\n          type: integer\n          format: int32\n          minimum: 1\n        name:\n          type: string\n          description: unique name\n        description:\n          type: string\n          description: optional description\n        type:\n          $ref: '#/components/schemas/EventActionTypes'\n        options:\n          $ref: '#/components/schemas/BaseEventActionOptions'\n        rules:\n          type: array\n          items:\n            type: string\n          description: list of event rules names associated with this action\n    EventActionOptions:\n      type: object\n      properties:\n        is_failure_action:\n          type: boolean\n        stop_on_failure:\n          type: boolean\n        execute_sync:\n          type: boolean\n    EventAction:\n      allOf:\n        - $ref: '#/components/schemas/BaseEventAction'\n        - type: object\n          properties:\n            order:\n              type: integer\n              description: execution order\n            relation_options:\n              $ref: '#/components/schemas/EventActionOptions'\n    EventActionMinimal:\n      type: object\n      properties:\n        name:\n          type: string\n        order:\n          type: integer\n          description: execution order\n        relation_options:\n          $ref: '#/components/schemas/EventActionOptions'\n    ConditionPattern:\n      type: object\n      properties:\n        pattern:\n          type: string\n        inverse_match:\n          type: boolean\n    ConditionOptions:\n      type: object\n      properties:\n        names:\n          type: array\n          items:\n            $ref: '#/components/schemas/ConditionPattern'\n        group_names:\n          type: array\n          items:\n            $ref: '#/components/schemas/ConditionPattern'\n        role_names:\n          type: array\n          items:\n            $ref: '#/components/schemas/ConditionPattern'\n        fs_paths:\n          type: array\n          items:\n            $ref: '#/components/schemas/ConditionPattern'\n        protocols:\n          type: array\n          items:\n            type: string\n            enum:\n              - SFTP\n              - SCP\n              - SSH\n              - FTP\n              - DAV\n              - HTTP\n              - HTTPShare\n              - OIDC\n        provider_objects:\n          type: array\n          items:\n            type: string\n            enum:\n              - user\n              - group\n              - admin\n              - api_key\n              - share\n              - event_action\n              - event_rule\n        min_size:\n          type: integer\n          format: int64\n        max_size:\n          type: integer\n          format: int64\n        event_statuses:\n          type: array\n          items:\n            type: integer\n            enum:\n              - 1\n              - 2\n              - 3\n            description: |\n              Event status:\n                - `1` OK\n                - `2` Failed\n                - `3` Quota exceeded\n        concurrent_execution:\n          type: boolean\n          description: allow concurrent execution from multiple nodes\n    Schedule:\n      type: object\n      properties:\n        hour:\n          type: string\n        day_of_week:\n          type: string\n        day_of_month:\n          type: string\n        month:\n          type: string\n    EventConditions:\n      type: object\n      properties:\n        fs_events:\n          type: array\n          items:\n            type: string\n            enum:\n              - upload\n              - download\n              - delete\n              - rename\n              - mkdir\n              - rmdir\n              - copy\n              - ssh_cmd\n              - pre-upload\n              - pre-download\n              - pre-delete\n              - first-upload\n              - first-download\n        provider_events:\n          type: array\n          items:\n            type: string\n            enum:\n              - add\n              - update\n              - delete\n        schedules:\n          type: array\n          items:\n            $ref: '#/components/schemas/Schedule'\n        idp_login_event:\n          type: integer\n          enum:\n            - 0\n            - 1\n            - 2\n          description: |\n            IDP login events:\n              - `0` any login event\n              - `1` user login event\n              - `2` admin login event\n        options:\n          $ref: '#/components/schemas/ConditionOptions'\n    BaseEventRule:\n      type: object\n      properties:\n        id:\n          type: integer\n          format: int32\n          minimum: 1\n        name:\n          type: string\n          description: unique name\n        status:\n          type: integer\n          enum:\n            - 0\n            - 1\n          description: |\n            status:\n              * `0` disabled\n              * `1` enabled\n        description:\n          type: string\n          description: optional description\n        created_at:\n          type: integer\n          format: int64\n          description: creation time as unix timestamp in milliseconds\n        updated_at:\n          type: integer\n          format: int64\n          description: last update time as unix timestamp in millisecond\n        trigger:\n          $ref: '#/components/schemas/EventTriggerTypes'\n        conditions:\n          $ref: '#/components/schemas/EventConditions'\n    EventRule:\n      allOf:\n        - $ref: '#/components/schemas/BaseEventRule'\n        - type: object\n          properties:\n            actions:\n              type: array\n              items:\n                $ref: '#/components/schemas/EventAction'\n    EventRuleMinimal:\n      allOf:\n        - $ref: '#/components/schemas/BaseEventRule'\n        - type: object\n          properties:\n            actions:\n              type: array\n              items:\n                $ref: '#/components/schemas/EventActionMinimal'\n    IPListEntry:\n      type: object\n      properties:\n        ipornet:\n          type: string\n          description: IP address or network in CIDR format, for example `192.168.1.2/32`, `192.168.0.0/24`, `2001:db8::/32`\n        description:\n          type: string\n          description: optional description\n        type:\n          $ref: '#/components/schemas/IPListType'\n        mode:\n          $ref: '#/components/schemas/IPListMode'\n        protocols:\n          type: integer\n          description: Defines the protocol the entry applies to. `0` means all the supported protocols, 1 SSH, 2 FTP, 4 WebDAV, 8 HTTP. Protocols can be combined, for example 3 means SSH and FTP\n        created_at:\n          type: integer\n          format: int64\n          description: creation time as unix timestamp in milliseconds\n        updated_at:\n          type: integer\n          format: int64\n          description: last update time as unix timestamp in millisecond\n    ApiResponse:\n      type: object\n      properties:\n        message:\n          type: string\n          description: 'message, can be empty'\n        error:\n          type: string\n          description: error description if any\n    VersionInfo:\n      type: object\n      properties:\n        version:\n          type: string\n        build_date:\n          type: string\n        commit_hash:\n          type: string\n        features:\n          type: array\n          items:\n            type: string\n          description: 'Features for the current build. Available features are `portable`, `bolt`, `mysql`, `sqlite`, `pgsql`, `s3`, `gcs`, `azblob`, `metrics`, `unixcrypt`. If a feature is available it has a `+` prefix, otherwise a `-` prefix'\n    Token:\n      type: object\n      properties:\n        access_token:\n          type: string\n        expires_at:\n          type: string\n          format: date-time\n  securitySchemes:\n    BasicAuth:\n      type: http\n      scheme: basic\n    BearerAuth:\n      type: http\n      scheme: bearer\n      bearerFormat: JWT\n    APIKeyAuth:\n      type: apiKey\n      in: header\n      name: X-SFTPGO-API-KEY\n      description: 'API key to use for authentication. API key authentication is intrinsically less secure than using a short lived JWT token. You should prefer API key authentication only for machine-to-machine communications in trusted environments. If no admin/user is associated to the provided key you need to add \".username\" at the end of the key. For example if your API key is \"6ajKLwswLccVBGpZGv596G.ySAXc8vtp9hMiwAuaLtzof\" and you want to impersonate the admin with username \"myadmin\" you have to use \"6ajKLwswLccVBGpZGv596G.ySAXc8vtp9hMiwAuaLtzof.myadmin\" as API key. When using API key authentication you cannot manage API keys, update the impersonated admin, change password or public keys for the impersonated user.'\n"
  },
  {
    "path": "openapi/swagger-ui/index.css",
    "content": "html {\n    box-sizing: border-box;\n    overflow: -moz-scrollbars-vertical;\n    overflow-y: scroll;\n}\n\n*,\n*:before,\n*:after {\n    box-sizing: inherit;\n}\n\nbody {\n    margin: 0;\n    background: #fafafa;\n}\n"
  },
  {
    "path": "openapi/swagger-ui/index.html",
    "content": "<!-- HTML for static distribution bundle build -->\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>Swagger UI</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"./swagger-ui.css\" />\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"index.css\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"./favicon-32x32.png\" sizes=\"32x32\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"./favicon-16x16.png\" sizes=\"16x16\" />\n  </head>\n\n  <body>\n    <div id=\"swagger-ui\"></div>\n    <script src=\"./swagger-ui-bundle.js\" charset=\"UTF-8\"> </script>\n    <script src=\"./swagger-ui-standalone-preset.js\" charset=\"UTF-8\"> </script>\n    <script src=\"./swagger-initializer.js\" charset=\"UTF-8\"> </script>\n  </body>\n</html>\n"
  },
  {
    "path": "openapi/swagger-ui/swagger-initializer.js",
    "content": "window.onload = function() {\n  //<editor-fold desc=\"Changeable Configuration Block\">\n\n  // the following lines will be replaced by docker/configurator, when it runs in a docker-container\n  window.ui = SwaggerUIBundle({\n    url: \"../openapi.yaml\",\n    dom_id: '#swagger-ui',\n    deepLinking: true,\n    presets: [\n      SwaggerUIBundle.presets.apis,\n      SwaggerUIStandalonePreset\n    ],\n    plugins: [\n      SwaggerUIBundle.plugins.DownloadUrl\n    ],\n    layout: \"StandaloneLayout\"\n  });\n\n  //</editor-fold>\n};\n"
  },
  {
    "path": "openapi/swagger-ui/swagger-ui-bundle.js",
    "content": "/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */\n!function webpackUniversalModuleDefinition(s,o){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=o():\"function\"==typeof define&&define.amd?define([],o):\"object\"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(()=>(()=>{var s={251:(s,o)=>{o.read=function(s,o,i,a,u){var _,w,x=8*u-a-1,C=(1<<x)-1,j=C>>1,L=-7,B=i?u-1:0,$=i?-1:1,U=s[o+B];for(B+=$,_=U&(1<<-L)-1,U>>=-L,L+=x;L>0;_=256*_+s[o+B],B+=$,L-=8);for(w=_&(1<<-L)-1,_>>=-L,L+=a;L>0;w=256*w+s[o+B],B+=$,L-=8);if(0===_)_=1-j;else{if(_===C)return w?NaN:1/0*(U?-1:1);w+=Math.pow(2,a),_-=j}return(U?-1:1)*w*Math.pow(2,_-a)},o.write=function(s,o,i,a,u,_){var w,x,C,j=8*_-u-1,L=(1<<j)-1,B=L>>1,$=23===u?Math.pow(2,-24)-Math.pow(2,-77):0,U=a?0:_-1,V=a?1:-1,z=o<0||0===o&&1/o<0?1:0;for(o=Math.abs(o),isNaN(o)||o===1/0?(x=isNaN(o)?1:0,w=L):(w=Math.floor(Math.log(o)/Math.LN2),o*(C=Math.pow(2,-w))<1&&(w--,C*=2),(o+=w+B>=1?$/C:$*Math.pow(2,1-B))*C>=2&&(w++,C/=2),w+B>=L?(x=0,w=L):w+B>=1?(x=(o*C-1)*Math.pow(2,u),w+=B):(x=o*Math.pow(2,B-1)*Math.pow(2,u),w=0));u>=8;s[i+U]=255&x,U+=V,x/=256,u-=8);for(w=w<<u|x,j+=u;j>0;s[i+U]=255&w,U+=V,w/=256,j-=8);s[i+U-V]|=128*z}},462:(s,o,i)=>{\"use strict\";var a=i(40975);s.exports=a},659:(s,o,i)=>{var a=i(51873),u=Object.prototype,_=u.hasOwnProperty,w=u.toString,x=a?a.toStringTag:void 0;s.exports=function getRawTag(s){var o=_.call(s,x),i=s[x];try{s[x]=void 0;var a=!0}catch(s){}var u=w.call(s);return a&&(o?s[x]=i:delete s[x]),u}},694:(s,o,i)=>{\"use strict\";i(91599);var a=i(37257);i(12560),s.exports=a},953:(s,o,i)=>{\"use strict\";s.exports=i(53375)},1733:s=>{var o=/[^\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7f]+/g;s.exports=function asciiWords(s){return s.match(o)||[]}},1882:(s,o,i)=>{var a=i(72552),u=i(23805);s.exports=function isFunction(s){if(!u(s))return!1;var o=a(s);return\"[object Function]\"==o||\"[object GeneratorFunction]\"==o||\"[object AsyncFunction]\"==o||\"[object Proxy]\"==o}},1907:(s,o,i)=>{\"use strict\";var a=i(41505),u=Function.prototype,_=u.call,w=a&&u.bind.bind(_,_);s.exports=a?w:function(s){return function(){return _.apply(s,arguments)}}},2205:function(s,o,i){var a;a=void 0!==i.g?i.g:this,s.exports=function(s){if(s.CSS&&s.CSS.escape)return s.CSS.escape;var cssEscape=function(s){if(0==arguments.length)throw new TypeError(\"`CSS.escape` requires an argument.\");for(var o,i=String(s),a=i.length,u=-1,_=\"\",w=i.charCodeAt(0);++u<a;)0!=(o=i.charCodeAt(u))?_+=o>=1&&o<=31||127==o||0==u&&o>=48&&o<=57||1==u&&o>=48&&o<=57&&45==w?\"\\\\\"+o.toString(16)+\" \":0==u&&1==a&&45==o||!(o>=128||45==o||95==o||o>=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122)?\"\\\\\"+i.charAt(u):i.charAt(u):_+=\"�\";return _};return s.CSS||(s.CSS={}),s.CSS.escape=cssEscape,cssEscape}(a)},2209:(s,o,i)=>{\"use strict\";var a,u=i(9404),_=function productionTypeChecker(){invariant(!1,\"ImmutablePropTypes type checking code is stripped in production.\")};_.isRequired=_;var w=function getProductionTypeChecker(){return _};function getPropType(s){var o=typeof s;return Array.isArray(s)?\"array\":s instanceof RegExp?\"object\":s instanceof u.Iterable?\"Immutable.\"+s.toSource().split(\" \")[0]:o}function createChainableTypeChecker(s){function checkType(o,i,a,u,_,w){for(var x=arguments.length,C=Array(x>6?x-6:0),j=6;j<x;j++)C[j-6]=arguments[j];return w=w||a,u=u||\"<<anonymous>>\",null!=i[a]?s.apply(void 0,[i,a,u,_,w].concat(C)):o?new Error(\"Required \"+_+\" `\"+w+\"` was not specified in `\"+u+\"`.\"):void 0}var o=checkType.bind(null,!1);return o.isRequired=checkType.bind(null,!0),o}function createIterableSubclassTypeChecker(s,o){return function createImmutableTypeChecker(s,o){return createChainableTypeChecker((function validate(i,a,u,_,w){var x=i[a];if(!o(x)){var C=getPropType(x);return new Error(\"Invalid \"+_+\" `\"+w+\"` of type `\"+C+\"` supplied to `\"+u+\"`, expected `\"+s+\"`.\")}return null}))}(\"Iterable.\"+s,(function(s){return u.Iterable.isIterable(s)&&o(s)}))}(a={listOf:w,mapOf:w,orderedMapOf:w,setOf:w,orderedSetOf:w,stackOf:w,iterableOf:w,recordOf:w,shape:w,contains:w,mapContains:w,orderedMapContains:w,list:_,map:_,orderedMap:_,set:_,orderedSet:_,stack:_,seq:_,record:_,iterable:_}).iterable.indexed=createIterableSubclassTypeChecker(\"Indexed\",u.Iterable.isIndexed),a.iterable.keyed=createIterableSubclassTypeChecker(\"Keyed\",u.Iterable.isKeyed),s.exports=a},2404:(s,o,i)=>{var a=i(60270);s.exports=function isEqual(s,o){return a(s,o)}},2523:s=>{s.exports=function baseFindIndex(s,o,i,a){for(var u=s.length,_=i+(a?1:-1);a?_--:++_<u;)if(o(s[_],_,s))return _;return-1}},2532:(s,o,i)=>{\"use strict\";var a=i(45951),u=Object.defineProperty;s.exports=function(s,o){try{u(a,s,{value:o,configurable:!0,writable:!0})}catch(i){a[s]=o}return o}},2694:(s,o,i)=>{\"use strict\";var a=i(6925);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,s.exports=function(){function shim(s,o,i,u,_,w){if(w!==a){var x=new Error(\"Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types\");throw x.name=\"Invariant Violation\",x}}function getShim(){return shim}shim.isRequired=shim;var s={array:shim,bigint:shim,bool:shim,func:shim,number:shim,object:shim,string:shim,symbol:shim,any:shim,arrayOf:getShim,element:shim,elementType:shim,instanceOf:getShim,node:shim,objectOf:getShim,oneOf:getShim,oneOfType:getShim,shape:getShim,exact:getShim,checkPropTypes:emptyFunctionWithReset,resetWarningCache:emptyFunction};return s.PropTypes=s,s}},2874:s=>{s.exports={}},2875:(s,o,i)=>{\"use strict\";var a=i(23045),u=i(80376);s.exports=Object.keys||function keys(s){return a(s,u)}},2955:(s,o,i)=>{\"use strict\";var a,u=i(65606);function _defineProperty(s,o,i){return(o=function _toPropertyKey(s){var o=function _toPrimitive(s,o){if(\"object\"!=typeof s||null===s)return s;var i=s[Symbol.toPrimitive];if(void 0!==i){var a=i.call(s,o||\"default\");if(\"object\"!=typeof a)return a;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===o?String:Number)(s)}(s,\"string\");return\"symbol\"==typeof o?o:String(o)}(o))in s?Object.defineProperty(s,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):s[o]=i,s}var _=i(86238),w=Symbol(\"lastResolve\"),x=Symbol(\"lastReject\"),C=Symbol(\"error\"),j=Symbol(\"ended\"),L=Symbol(\"lastPromise\"),B=Symbol(\"handlePromise\"),$=Symbol(\"stream\");function createIterResult(s,o){return{value:s,done:o}}function readAndResolve(s){var o=s[w];if(null!==o){var i=s[$].read();null!==i&&(s[L]=null,s[w]=null,s[x]=null,o(createIterResult(i,!1)))}}function onReadable(s){u.nextTick(readAndResolve,s)}var U=Object.getPrototypeOf((function(){})),V=Object.setPrototypeOf((_defineProperty(a={get stream(){return this[$]},next:function next(){var s=this,o=this[C];if(null!==o)return Promise.reject(o);if(this[j])return Promise.resolve(createIterResult(void 0,!0));if(this[$].destroyed)return new Promise((function(o,i){u.nextTick((function(){s[C]?i(s[C]):o(createIterResult(void 0,!0))}))}));var i,a=this[L];if(a)i=new Promise(function wrapForNext(s,o){return function(i,a){s.then((function(){o[j]?i(createIterResult(void 0,!0)):o[B](i,a)}),a)}}(a,this));else{var _=this[$].read();if(null!==_)return Promise.resolve(createIterResult(_,!1));i=new Promise(this[B])}return this[L]=i,i}},Symbol.asyncIterator,(function(){return this})),_defineProperty(a,\"return\",(function _return(){var s=this;return new Promise((function(o,i){s[$].destroy(null,(function(s){s?i(s):o(createIterResult(void 0,!0))}))}))})),a),U);s.exports=function createReadableStreamAsyncIterator(s){var o,i=Object.create(V,(_defineProperty(o={},$,{value:s,writable:!0}),_defineProperty(o,w,{value:null,writable:!0}),_defineProperty(o,x,{value:null,writable:!0}),_defineProperty(o,C,{value:null,writable:!0}),_defineProperty(o,j,{value:s._readableState.endEmitted,writable:!0}),_defineProperty(o,B,{value:function value(s,o){var a=i[$].read();a?(i[L]=null,i[w]=null,i[x]=null,s(createIterResult(a,!1))):(i[w]=s,i[x]=o)},writable:!0}),o));return i[L]=null,_(s,(function(s){if(s&&\"ERR_STREAM_PREMATURE_CLOSE\"!==s.code){var o=i[x];return null!==o&&(i[L]=null,i[w]=null,i[x]=null,o(s)),void(i[C]=s)}var a=i[w];null!==a&&(i[L]=null,i[w]=null,i[x]=null,a(createIterResult(void 0,!0))),i[j]=!0})),s.on(\"readable\",onReadable.bind(null,i)),i}},3110:(s,o,i)=>{const a=i(5187),u=i(85015),_=i(98023),w=i(53812),x=i(23805),C=i(85105),j=i(86804);class Namespace{constructor(s){this.elementMap={},this.elementDetection=[],this.Element=j.Element,this.KeyValuePair=j.KeyValuePair,s&&s.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(s){return s.namespace&&s.namespace({base:this}),s.load&&s.load({base:this}),this}useDefault(){return this.register(\"null\",j.NullElement).register(\"string\",j.StringElement).register(\"number\",j.NumberElement).register(\"boolean\",j.BooleanElement).register(\"array\",j.ArrayElement).register(\"object\",j.ObjectElement).register(\"member\",j.MemberElement).register(\"ref\",j.RefElement).register(\"link\",j.LinkElement),this.detect(a,j.NullElement,!1).detect(u,j.StringElement,!1).detect(_,j.NumberElement,!1).detect(w,j.BooleanElement,!1).detect(Array.isArray,j.ArrayElement,!1).detect(x,j.ObjectElement,!1),this}register(s,o){return this._elements=void 0,this.elementMap[s]=o,this}unregister(s){return this._elements=void 0,delete this.elementMap[s],this}detect(s,o,i){return void 0===i||i?this.elementDetection.unshift([s,o]):this.elementDetection.push([s,o]),this}toElement(s){if(s instanceof this.Element)return s;let o;for(let i=0;i<this.elementDetection.length;i+=1){const a=this.elementDetection[i][0],u=this.elementDetection[i][1];if(a(s)){o=new u(s);break}}return o}getElementClass(s){const o=this.elementMap[s];return void 0===o?this.Element:o}fromRefract(s){return this.serialiser.deserialise(s)}toRefract(s){return this.serialiser.serialise(s)}get elements(){return void 0===this._elements&&(this._elements={Element:this.Element},Object.keys(this.elementMap).forEach((s=>{const o=s[0].toUpperCase()+s.substr(1);this._elements[o]=this.elementMap[s]}))),this._elements}get serialiser(){return new C(this)}}C.prototype.Namespace=Namespace,s.exports=Namespace},3121:(s,o,i)=>{\"use strict\";var a=i(65482),u=Math.min;s.exports=function(s){var o=a(s);return o>0?u(o,9007199254740991):0}},3209:(s,o,i)=>{var a=i(91596),u=i(53320),_=i(36306),w=\"__lodash_placeholder__\",x=128,C=Math.min;s.exports=function mergeData(s,o){var i=s[1],j=o[1],L=i|j,B=L<131,$=j==x&&8==i||j==x&&256==i&&s[7].length<=o[8]||384==j&&o[7].length<=o[8]&&8==i;if(!B&&!$)return s;1&j&&(s[2]=o[2],L|=1&i?0:4);var U=o[3];if(U){var V=s[3];s[3]=V?a(V,U,o[4]):U,s[4]=V?_(s[3],w):o[4]}return(U=o[5])&&(V=s[5],s[5]=V?u(V,U,o[6]):U,s[6]=V?_(s[5],w):o[6]),(U=o[7])&&(s[7]=U),j&x&&(s[8]=null==s[8]?o[8]:C(s[8],o[8])),null==s[9]&&(s[9]=o[9]),s[0]=o[0],s[1]=L,s}},3650:(s,o,i)=>{var a=i(74335)(Object.keys,Object);s.exports=a},3656:(s,o,i)=>{s=i.nmd(s);var a=i(9325),u=i(89935),_=o&&!o.nodeType&&o,w=_&&s&&!s.nodeType&&s,x=w&&w.exports===_?a.Buffer:void 0,C=(x?x.isBuffer:void 0)||u;s.exports=C},4509:(s,o,i)=>{var a=i(12651);s.exports=function mapCacheHas(s){return a(this,s).has(s)}},4640:s=>{\"use strict\";var o=String;s.exports=function(s){try{return o(s)}catch(s){return\"Object\"}}},4664:(s,o,i)=>{var a=i(79770),u=i(63345),_=Object.prototype.propertyIsEnumerable,w=Object.getOwnPropertySymbols,x=w?function(s){return null==s?[]:(s=Object(s),a(w(s),(function(o){return _.call(s,o)})))}:u;s.exports=x},4901:(s,o,i)=>{var a=i(72552),u=i(30294),_=i(40346),w={};w[\"[object Float32Array]\"]=w[\"[object Float64Array]\"]=w[\"[object Int8Array]\"]=w[\"[object Int16Array]\"]=w[\"[object Int32Array]\"]=w[\"[object Uint8Array]\"]=w[\"[object Uint8ClampedArray]\"]=w[\"[object Uint16Array]\"]=w[\"[object Uint32Array]\"]=!0,w[\"[object Arguments]\"]=w[\"[object Array]\"]=w[\"[object ArrayBuffer]\"]=w[\"[object Boolean]\"]=w[\"[object DataView]\"]=w[\"[object Date]\"]=w[\"[object Error]\"]=w[\"[object Function]\"]=w[\"[object Map]\"]=w[\"[object Number]\"]=w[\"[object Object]\"]=w[\"[object RegExp]\"]=w[\"[object Set]\"]=w[\"[object String]\"]=w[\"[object WeakMap]\"]=!1,s.exports=function baseIsTypedArray(s){return _(s)&&u(s.length)&&!!w[a(s)]}},4993:(s,o,i)=>{\"use strict\";var a=i(16946),u=i(74239);s.exports=function(s){return a(u(s))}},5187:s=>{s.exports=function isNull(s){return null===s}},5419:s=>{s.exports=function(s,o,i,a){var u=new Blob(void 0!==a?[a,s]:[s],{type:i||\"application/octet-stream\"});if(void 0!==window.navigator.msSaveBlob)window.navigator.msSaveBlob(u,o);else{var _=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(u):window.webkitURL.createObjectURL(u),w=document.createElement(\"a\");w.style.display=\"none\",w.href=_,w.setAttribute(\"download\",o),void 0===w.download&&w.setAttribute(\"target\",\"_blank\"),document.body.appendChild(w),w.click(),setTimeout((function(){document.body.removeChild(w),window.URL.revokeObjectURL(_)}),200)}}},5556:(s,o,i)=>{s.exports=i(2694)()},5861:(s,o,i)=>{var a=i(55580),u=i(68223),_=i(32804),w=i(76545),x=i(28303),C=i(72552),j=i(47473),L=\"[object Map]\",B=\"[object Promise]\",$=\"[object Set]\",U=\"[object WeakMap]\",V=\"[object DataView]\",z=j(a),Y=j(u),Z=j(_),ee=j(w),ie=j(x),ae=C;(a&&ae(new a(new ArrayBuffer(1)))!=V||u&&ae(new u)!=L||_&&ae(_.resolve())!=B||w&&ae(new w)!=$||x&&ae(new x)!=U)&&(ae=function(s){var o=C(s),i=\"[object Object]\"==o?s.constructor:void 0,a=i?j(i):\"\";if(a)switch(a){case z:return V;case Y:return L;case Z:return B;case ee:return $;case ie:return U}return o}),s.exports=ae},6048:s=>{s.exports=function negate(s){if(\"function\"!=typeof s)throw new TypeError(\"Expected a function\");return function(){var o=arguments;switch(o.length){case 0:return!s.call(this);case 1:return!s.call(this,o[0]);case 2:return!s.call(this,o[0],o[1]);case 3:return!s.call(this,o[0],o[1],o[2])}return!s.apply(this,o)}}},6188:s=>{\"use strict\";s.exports=Math.max},6205:s=>{s.exports={ROOT:0,GROUP:1,POSITION:2,SET:3,RANGE:4,REPETITION:5,REFERENCE:6,CHAR:7}},6233:(s,o,i)=>{const a=i(6048),u=i(10316),_=i(92340);class ArrayElement extends u{constructor(s,o,i){super(s||[],o,i),this.element=\"array\"}primitive(){return\"array\"}get(s){return this.content[s]}getValue(s){const o=this.get(s);if(o)return o.toValue()}getIndex(s){return this.content[s]}set(s,o){return this.content[s]=this.refract(o),this}remove(s){const o=this.content.splice(s,1);return o.length?o[0]:null}map(s,o){return this.content.map(s,o)}flatMap(s,o){return this.map(s,o).reduce(((s,o)=>s.concat(o)),[])}compactMap(s,o){const i=[];return this.forEach((a=>{const u=s.bind(o)(a);u&&i.push(u)})),i}filter(s,o){return new _(this.content.filter(s,o))}reject(s,o){return this.filter(a(s),o)}reduce(s,o){let i,a;void 0!==o?(i=0,a=this.refract(o)):(i=1,a=\"object\"===this.primitive()?this.first.value:this.first);for(let o=i;o<this.length;o+=1){const i=this.content[o];a=\"object\"===this.primitive()?this.refract(s(a,i.value,i.key,i,this)):this.refract(s(a,i,o,this))}return a}forEach(s,o){this.content.forEach(((i,a)=>{s.bind(o)(i,this.refract(a))}))}shift(){return this.content.shift()}unshift(s){this.content.unshift(this.refract(s))}push(s){return this.content.push(this.refract(s)),this}add(s){this.push(s)}findElements(s,o){const i=o||{},a=!!i.recursive,u=void 0===i.results?[]:i.results;return this.forEach(((o,i,_)=>{a&&void 0!==o.findElements&&o.findElements(s,{results:u,recursive:a}),s(o,i,_)&&u.push(o)})),u}find(s){return new _(this.findElements(s,{recursive:!0}))}findByElement(s){return this.find((o=>o.element===s))}findByClass(s){return this.find((o=>o.classes.includes(s)))}getById(s){return this.find((o=>o.id.toValue()===s)).first}includes(s){return this.content.some((o=>o.equals(s)))}contains(s){return this.includes(s)}empty(){return new this.constructor([])}\"fantasy-land/empty\"(){return this.empty()}concat(s){return new this.constructor(this.content.concat(s.content))}\"fantasy-land/concat\"(s){return this.concat(s)}\"fantasy-land/map\"(s){return new this.constructor(this.map(s))}\"fantasy-land/chain\"(s){return this.map((o=>s(o)),this).reduce(((s,o)=>s.concat(o)),this.empty())}\"fantasy-land/filter\"(s){return new this.constructor(this.content.filter(s))}\"fantasy-land/reduce\"(s,o){return this.content.reduce(s,o)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}ArrayElement.empty=function empty(){return new this},ArrayElement[\"fantasy-land/empty\"]=ArrayElement.empty,\"undefined\"!=typeof Symbol&&(ArrayElement.prototype[Symbol.iterator]=function symbol(){return this.content[Symbol.iterator]()}),s.exports=ArrayElement},6499:(s,o,i)=>{\"use strict\";var a=i(1907),u=0,_=Math.random(),w=a(1..toString);s.exports=function(s){return\"Symbol(\"+(void 0===s?\"\":s)+\")_\"+w(++u+_,36)}},6549:s=>{\"use strict\";s.exports=Object.getOwnPropertyDescriptor},6925:s=>{\"use strict\";s.exports=\"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED\"},7057:(s,o,i)=>{\"use strict\";var a=i(11470).charAt,u=i(90160),_=i(64932),w=i(60183),x=i(59550),C=\"String Iterator\",j=_.set,L=_.getterFor(C);w(String,\"String\",(function(s){j(this,{type:C,string:u(s),index:0})}),(function next(){var s,o=L(this),i=o.string,u=o.index;return u>=i.length?x(void 0,!0):(s=a(i,u),o.index+=s.length,x(s,!1))}))},7176:(s,o,i)=>{\"use strict\";var a,u=i(73126),_=i(75795);try{a=[].__proto__===Array.prototype}catch(s){if(!s||\"object\"!=typeof s||!(\"code\"in s)||\"ERR_PROTO_ACCESS\"!==s.code)throw s}var w=!!a&&_&&_(Object.prototype,\"__proto__\"),x=Object,C=x.getPrototypeOf;s.exports=w&&\"function\"==typeof w.get?u([w.get]):\"function\"==typeof C&&function getDunder(s){return C(null==s?s:x(s))}},7309:(s,o,i)=>{var a=i(62006)(i(24713));s.exports=a},7376:s=>{\"use strict\";s.exports=!0},7463:(s,o,i)=>{\"use strict\";var a=i(98828),u=i(62250),_=/#|\\.prototype\\./,isForced=function(s,o){var i=x[w(s)];return i===j||i!==C&&(u(o)?a(o):!!o)},w=isForced.normalize=function(s){return String(s).replace(_,\".\").toLowerCase()},x=isForced.data={},C=isForced.NATIVE=\"N\",j=isForced.POLYFILL=\"P\";s.exports=isForced},7666:(s,o,i)=>{var a=i(84851),u=i(953);function _extends(){var o;return s.exports=_extends=a?u(o=a).call(o):function(s){for(var o=1;o<arguments.length;o++){var i=arguments[o];for(var a in i)({}).hasOwnProperty.call(i,a)&&(s[a]=i[a])}return s},s.exports.__esModule=!0,s.exports.default=s.exports,_extends.apply(null,arguments)}s.exports=_extends,s.exports.__esModule=!0,s.exports.default=s.exports},8048:(s,o,i)=>{const a=i(6205);o.wordBoundary=()=>({type:a.POSITION,value:\"b\"}),o.nonWordBoundary=()=>({type:a.POSITION,value:\"B\"}),o.begin=()=>({type:a.POSITION,value:\"^\"}),o.end=()=>({type:a.POSITION,value:\"$\"})},8068:s=>{\"use strict\";var o=(()=>{var s=Object.defineProperty,o=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getOwnPropertySymbols,u=Object.prototype.hasOwnProperty,_=Object.prototype.propertyIsEnumerable,__defNormalProp=(o,i,a)=>i in o?s(o,i,{enumerable:!0,configurable:!0,writable:!0,value:a}):o[i]=a,__spreadValues=(s,o)=>{for(var i in o||(o={}))u.call(o,i)&&__defNormalProp(s,i,o[i]);if(a)for(var i of a(o))_.call(o,i)&&__defNormalProp(s,i,o[i]);return s},__publicField=(s,o,i)=>__defNormalProp(s,\"symbol\"!=typeof o?o+\"\":o,i),w={};((o,i)=>{for(var a in i)s(o,a,{get:i[a],enumerable:!0})})(w,{DEFAULT_OPTIONS:()=>C,DEFAULT_UUID_LENGTH:()=>x,default:()=>B});var x=6,C={dictionary:\"alphanum\",shuffle:!0,debug:!1,length:x,counter:0},j=class _ShortUniqueId{constructor(s={}){__publicField(this,\"counter\"),__publicField(this,\"debug\"),__publicField(this,\"dict\"),__publicField(this,\"version\"),__publicField(this,\"dictIndex\",0),__publicField(this,\"dictRange\",[]),__publicField(this,\"lowerBound\",0),__publicField(this,\"upperBound\",0),__publicField(this,\"dictLength\",0),__publicField(this,\"uuidLength\"),__publicField(this,\"_digit_first_ascii\",48),__publicField(this,\"_digit_last_ascii\",58),__publicField(this,\"_alpha_lower_first_ascii\",97),__publicField(this,\"_alpha_lower_last_ascii\",123),__publicField(this,\"_hex_last_ascii\",103),__publicField(this,\"_alpha_upper_first_ascii\",65),__publicField(this,\"_alpha_upper_last_ascii\",91),__publicField(this,\"_number_dict_ranges\",{digits:[this._digit_first_ascii,this._digit_last_ascii]}),__publicField(this,\"_alpha_dict_ranges\",{lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,\"_alpha_lower_dict_ranges\",{lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii]}),__publicField(this,\"_alpha_upper_dict_ranges\",{upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,\"_alphanum_dict_ranges\",{digits:[this._digit_first_ascii,this._digit_last_ascii],lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,\"_alphanum_lower_dict_ranges\",{digits:[this._digit_first_ascii,this._digit_last_ascii],lowerCase:[this._alpha_lower_first_ascii,this._alpha_lower_last_ascii]}),__publicField(this,\"_alphanum_upper_dict_ranges\",{digits:[this._digit_first_ascii,this._digit_last_ascii],upperCase:[this._alpha_upper_first_ascii,this._alpha_upper_last_ascii]}),__publicField(this,\"_hex_dict_ranges\",{decDigits:[this._digit_first_ascii,this._digit_last_ascii],alphaDigits:[this._alpha_lower_first_ascii,this._hex_last_ascii]}),__publicField(this,\"_dict_ranges\",{_number_dict_ranges:this._number_dict_ranges,_alpha_dict_ranges:this._alpha_dict_ranges,_alpha_lower_dict_ranges:this._alpha_lower_dict_ranges,_alpha_upper_dict_ranges:this._alpha_upper_dict_ranges,_alphanum_dict_ranges:this._alphanum_dict_ranges,_alphanum_lower_dict_ranges:this._alphanum_lower_dict_ranges,_alphanum_upper_dict_ranges:this._alphanum_upper_dict_ranges,_hex_dict_ranges:this._hex_dict_ranges}),__publicField(this,\"log\",((...s)=>{const o=[...s];o[0]=\"[short-unique-id] \".concat(s[0]),!0!==this.debug||\"undefined\"==typeof console||null===console||console.log(...o)})),__publicField(this,\"_normalizeDictionary\",((s,o)=>{let i;if(s&&Array.isArray(s)&&s.length>1)i=s;else{i=[],this.dictIndex=0;const o=\"_\".concat(s,\"_dict_ranges\"),a=this._dict_ranges[o];let u=0;for(const[,s]of Object.entries(a)){const[o,i]=s;u+=Math.abs(i-o)}i=new Array(u);let _=0;for(const[,s]of Object.entries(a)){this.dictRange=s,this.lowerBound=this.dictRange[0],this.upperBound=this.dictRange[1];const o=this.lowerBound<=this.upperBound,a=this.lowerBound,u=this.upperBound;if(o)for(let s=a;s<u;s++)i[_++]=String.fromCharCode(s),this.dictIndex=s;else for(let s=a;s>u;s--)i[_++]=String.fromCharCode(s),this.dictIndex=s}i.length=_}if(o){for(let s=i.length-1;s>0;s--){const o=Math.floor(Math.random()*(s+1));[i[s],i[o]]=[i[o],i[s]]}}return i})),__publicField(this,\"setDictionary\",((s,o)=>{this.dict=this._normalizeDictionary(s,o),this.dictLength=this.dict.length,this.setCounter(0)})),__publicField(this,\"seq\",(()=>this.sequentialUUID())),__publicField(this,\"sequentialUUID\",(()=>{const s=this.dictLength,o=this.dict;let i=this.counter;const a=[];do{const u=i%s;i=Math.trunc(i/s),a.push(o[u])}while(0!==i);const u=a.join(\"\");return this.counter+=1,u})),__publicField(this,\"rnd\",((s=this.uuidLength||x)=>this.randomUUID(s))),__publicField(this,\"randomUUID\",((s=this.uuidLength||x)=>{if(null==s||s<1)throw new Error(\"Invalid UUID Length Provided\");const o=new Array(s),i=this.dictLength,a=this.dict;for(let u=0;u<s;u++){const s=Math.floor(Math.random()*i);o[u]=a[s]}return o.join(\"\")})),__publicField(this,\"fmt\",((s,o)=>this.formattedUUID(s,o))),__publicField(this,\"formattedUUID\",((s,o)=>{const i={$r:this.randomUUID,$s:this.sequentialUUID,$t:this.stamp};return s.replace(/\\$[rs]\\d{0,}|\\$t0|\\$t[1-9]\\d{1,}/g,(s=>{const a=s.slice(0,2),u=Number.parseInt(s.slice(2),10);return\"$s\"===a?i[a]().padStart(u,\"0\"):\"$t\"===a&&o?i[a](u,o):i[a](u)}))})),__publicField(this,\"availableUUIDs\",((s=this.uuidLength)=>Number.parseFloat(([...new Set(this.dict)].length**s).toFixed(0)))),__publicField(this,\"_collisionCache\",new Map),__publicField(this,\"approxMaxBeforeCollision\",((s=this.availableUUIDs(this.uuidLength))=>{const o=s,i=this._collisionCache.get(o);if(void 0!==i)return i;const a=Number.parseFloat(Math.sqrt(Math.PI/2*s).toFixed(20));return this._collisionCache.set(o,a),a})),__publicField(this,\"collisionProbability\",((s=this.availableUUIDs(this.uuidLength),o=this.uuidLength)=>Number.parseFloat((this.approxMaxBeforeCollision(s)/this.availableUUIDs(o)).toFixed(20)))),__publicField(this,\"uniqueness\",((s=this.availableUUIDs(this.uuidLength))=>{const o=Number.parseFloat((1-this.approxMaxBeforeCollision(s)/s).toFixed(20));return o>1?1:o<0?0:o})),__publicField(this,\"getVersion\",(()=>this.version)),__publicField(this,\"stamp\",((s,o)=>{const i=Math.floor(+(o||new Date)/1e3).toString(16);if(\"number\"==typeof s&&0===s)return i;if(\"number\"!=typeof s||s<10)throw new Error([\"Param finalLength must be a number greater than or equal to 10,\",\"or 0 if you want the raw hexadecimal timestamp\"].join(\"\\n\"));const a=s-9,u=Math.round(Math.random()*(a>15?15:a)),_=this.randomUUID(a);return\"\".concat(_.substring(0,u)).concat(i).concat(_.substring(u)).concat(u.toString(16))})),__publicField(this,\"parseStamp\",((s,o)=>{if(o&&!/t0|t[1-9]\\d{1,}/.test(o))throw new Error(\"Cannot extract date from a formated UUID with no timestamp in the format\");const i=o?o.replace(/\\$[rs]\\d{0,}|\\$t0|\\$t[1-9]\\d{1,}/g,(s=>{const o={$r:s=>[...Array(s)].map((()=>\"r\")).join(\"\"),$s:s=>[...Array(s)].map((()=>\"s\")).join(\"\"),$t:s=>[...Array(s)].map((()=>\"t\")).join(\"\")},i=s.slice(0,2),a=Number.parseInt(s.slice(2),10);return o[i](a)})).replace(/^(.*?)(t{8,})(.*)$/g,((o,i,a)=>s.substring(i.length,i.length+a.length))):s;if(8===i.length)return new Date(1e3*Number.parseInt(i,16));if(i.length<10)throw new Error(\"Stamp length invalid\");const a=Number.parseInt(i.substring(i.length-1),16);return new Date(1e3*Number.parseInt(i.substring(a,a+8),16))})),__publicField(this,\"setCounter\",(s=>{this.counter=s})),__publicField(this,\"validate\",((s,o)=>{const i=o?this._normalizeDictionary(o):this.dict;return s.split(\"\").every((s=>i.includes(s)))}));const o=__spreadValues(__spreadValues({},C),s);this.counter=0,this.debug=!1,this.dict=[],this.version=\"5.3.2\";const{dictionary:i,shuffle:a,length:u,counter:_}=o;this.uuidLength=u,this.setDictionary(i,a),this.setCounter(_),this.debug=o.debug,this.log(this.dict),this.log(\"Generator instantiated with Dictionary Size \".concat(this.dictLength,\" and counter set to \").concat(this.counter)),this.log=this.log.bind(this),this.setDictionary=this.setDictionary.bind(this),this.setCounter=this.setCounter.bind(this),this.seq=this.seq.bind(this),this.sequentialUUID=this.sequentialUUID.bind(this),this.rnd=this.rnd.bind(this),this.randomUUID=this.randomUUID.bind(this),this.fmt=this.fmt.bind(this),this.formattedUUID=this.formattedUUID.bind(this),this.availableUUIDs=this.availableUUIDs.bind(this),this.approxMaxBeforeCollision=this.approxMaxBeforeCollision.bind(this),this.collisionProbability=this.collisionProbability.bind(this),this.uniqueness=this.uniqueness.bind(this),this.getVersion=this.getVersion.bind(this),this.stamp=this.stamp.bind(this),this.parseStamp=this.parseStamp.bind(this)}};__publicField(j,\"default\",j);var L,B=j;return L=w,((a,_,w,x)=>{if(_&&\"object\"==typeof _||\"function\"==typeof _)for(let C of i(_))u.call(a,C)||C===w||s(a,C,{get:()=>_[C],enumerable:!(x=o(_,C))||x.enumerable});return a})(s({},\"__esModule\",{value:!0}),L)})();s.exports=o.default,\"undefined\"!=typeof window&&(o=o.default)},9325:(s,o,i)=>{var a=i(34840),u=\"object\"==typeof self&&self&&self.Object===Object&&self,_=a||u||Function(\"return this\")();s.exports=_},9404:function(s){s.exports=function(){\"use strict\";var s=Array.prototype.slice;function createClass(s,o){o&&(s.prototype=Object.create(o.prototype)),s.prototype.constructor=s}function Iterable(s){return isIterable(s)?s:Seq(s)}function KeyedIterable(s){return isKeyed(s)?s:KeyedSeq(s)}function IndexedIterable(s){return isIndexed(s)?s:IndexedSeq(s)}function SetIterable(s){return isIterable(s)&&!isAssociative(s)?s:SetSeq(s)}function isIterable(s){return!(!s||!s[o])}function isKeyed(s){return!(!s||!s[i])}function isIndexed(s){return!(!s||!s[a])}function isAssociative(s){return isKeyed(s)||isIndexed(s)}function isOrdered(s){return!(!s||!s[u])}createClass(KeyedIterable,Iterable),createClass(IndexedIterable,Iterable),createClass(SetIterable,Iterable),Iterable.isIterable=isIterable,Iterable.isKeyed=isKeyed,Iterable.isIndexed=isIndexed,Iterable.isAssociative=isAssociative,Iterable.isOrdered=isOrdered,Iterable.Keyed=KeyedIterable,Iterable.Indexed=IndexedIterable,Iterable.Set=SetIterable;var o=\"@@__IMMUTABLE_ITERABLE__@@\",i=\"@@__IMMUTABLE_KEYED__@@\",a=\"@@__IMMUTABLE_INDEXED__@@\",u=\"@@__IMMUTABLE_ORDERED__@@\",_=\"delete\",w=5,x=1<<w,C=x-1,j={},L={value:!1},B={value:!1};function MakeRef(s){return s.value=!1,s}function SetRef(s){s&&(s.value=!0)}function OwnerID(){}function arrCopy(s,o){o=o||0;for(var i=Math.max(0,s.length-o),a=new Array(i),u=0;u<i;u++)a[u]=s[u+o];return a}function ensureSize(s){return void 0===s.size&&(s.size=s.__iterate(returnTrue)),s.size}function wrapIndex(s,o){if(\"number\"!=typeof o){var i=o>>>0;if(\"\"+i!==o||4294967295===i)return NaN;o=i}return o<0?ensureSize(s)+o:o}function returnTrue(){return!0}function wholeSlice(s,o,i){return(0===s||void 0!==i&&s<=-i)&&(void 0===o||void 0!==i&&o>=i)}function resolveBegin(s,o){return resolveIndex(s,o,0)}function resolveEnd(s,o){return resolveIndex(s,o,o)}function resolveIndex(s,o,i){return void 0===s?i:s<0?Math.max(0,o+s):void 0===o?s:Math.min(o,s)}var $=0,U=1,V=2,z=\"function\"==typeof Symbol&&Symbol.iterator,Y=\"@@iterator\",Z=z||Y;function Iterator(s){this.next=s}function iteratorValue(s,o,i,a){var u=0===s?o:1===s?i:[o,i];return a?a.value=u:a={value:u,done:!1},a}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(s){return!!getIteratorFn(s)}function isIterator(s){return s&&\"function\"==typeof s.next}function getIterator(s){var o=getIteratorFn(s);return o&&o.call(s)}function getIteratorFn(s){var o=s&&(z&&s[z]||s[Y]);if(\"function\"==typeof o)return o}function isArrayLike(s){return s&&\"number\"==typeof s.length}function Seq(s){return null==s?emptySequence():isIterable(s)?s.toSeq():seqFromValue(s)}function KeyedSeq(s){return null==s?emptySequence().toKeyedSeq():isIterable(s)?isKeyed(s)?s.toSeq():s.fromEntrySeq():keyedSeqFromValue(s)}function IndexedSeq(s){return null==s?emptySequence():isIterable(s)?isKeyed(s)?s.entrySeq():s.toIndexedSeq():indexedSeqFromValue(s)}function SetSeq(s){return(null==s?emptySequence():isIterable(s)?isKeyed(s)?s.entrySeq():s:indexedSeqFromValue(s)).toSetSeq()}Iterator.prototype.toString=function(){return\"[Iterator]\"},Iterator.KEYS=$,Iterator.VALUES=U,Iterator.ENTRIES=V,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[Z]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString(\"Seq {\",\"}\")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(s,o){return seqIterate(this,s,o,!0)},Seq.prototype.__iterator=function(s,o){return seqIterator(this,s,o,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString(\"Seq [\",\"]\")},IndexedSeq.prototype.__iterate=function(s,o){return seqIterate(this,s,o,!1)},IndexedSeq.prototype.__iterator=function(s,o){return seqIterator(this,s,o,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var ee,ie,ae,ce=\"@@__IMMUTABLE_SEQ__@@\";function ArraySeq(s){this._array=s,this.size=s.length}function ObjectSeq(s){var o=Object.keys(s);this._object=s,this._keys=o,this.size=o.length}function IterableSeq(s){this._iterable=s,this.size=s.length||s.size}function IteratorSeq(s){this._iterator=s,this._iteratorCache=[]}function isSeq(s){return!(!s||!s[ce])}function emptySequence(){return ee||(ee=new ArraySeq([]))}function keyedSeqFromValue(s){var o=Array.isArray(s)?new ArraySeq(s).fromEntrySeq():isIterator(s)?new IteratorSeq(s).fromEntrySeq():hasIterator(s)?new IterableSeq(s).fromEntrySeq():\"object\"==typeof s?new ObjectSeq(s):void 0;if(!o)throw new TypeError(\"Expected Array or iterable object of [k, v] entries, or keyed object: \"+s);return o}function indexedSeqFromValue(s){var o=maybeIndexedSeqFromValue(s);if(!o)throw new TypeError(\"Expected Array or iterable object of values: \"+s);return o}function seqFromValue(s){var o=maybeIndexedSeqFromValue(s)||\"object\"==typeof s&&new ObjectSeq(s);if(!o)throw new TypeError(\"Expected Array or iterable object of values, or keyed object: \"+s);return o}function maybeIndexedSeqFromValue(s){return isArrayLike(s)?new ArraySeq(s):isIterator(s)?new IteratorSeq(s):hasIterator(s)?new IterableSeq(s):void 0}function seqIterate(s,o,i,a){var u=s._cache;if(u){for(var _=u.length-1,w=0;w<=_;w++){var x=u[i?_-w:w];if(!1===o(x[1],a?x[0]:w,s))return w+1}return w}return s.__iterateUncached(o,i)}function seqIterator(s,o,i,a){var u=s._cache;if(u){var _=u.length-1,w=0;return new Iterator((function(){var s=u[i?_-w:w];return w++>_?iteratorDone():iteratorValue(o,a?s[0]:w-1,s[1])}))}return s.__iteratorUncached(o,i)}function fromJS(s,o){return o?fromJSWith(o,s,\"\",{\"\":s}):fromJSDefault(s)}function fromJSWith(s,o,i,a){return Array.isArray(o)?s.call(a,i,IndexedSeq(o).map((function(i,a){return fromJSWith(s,i,a,o)}))):isPlainObj(o)?s.call(a,i,KeyedSeq(o).map((function(i,a){return fromJSWith(s,i,a,o)}))):o}function fromJSDefault(s){return Array.isArray(s)?IndexedSeq(s).map(fromJSDefault).toList():isPlainObj(s)?KeyedSeq(s).map(fromJSDefault).toMap():s}function isPlainObj(s){return s&&(s.constructor===Object||void 0===s.constructor)}function is(s,o){if(s===o||s!=s&&o!=o)return!0;if(!s||!o)return!1;if(\"function\"==typeof s.valueOf&&\"function\"==typeof o.valueOf){if((s=s.valueOf())===(o=o.valueOf())||s!=s&&o!=o)return!0;if(!s||!o)return!1}return!(\"function\"!=typeof s.equals||\"function\"!=typeof o.equals||!s.equals(o))}function deepEqual(s,o){if(s===o)return!0;if(!isIterable(o)||void 0!==s.size&&void 0!==o.size&&s.size!==o.size||void 0!==s.__hash&&void 0!==o.__hash&&s.__hash!==o.__hash||isKeyed(s)!==isKeyed(o)||isIndexed(s)!==isIndexed(o)||isOrdered(s)!==isOrdered(o))return!1;if(0===s.size&&0===o.size)return!0;var i=!isAssociative(s);if(isOrdered(s)){var a=s.entries();return o.every((function(s,o){var u=a.next().value;return u&&is(u[1],s)&&(i||is(u[0],o))}))&&a.next().done}var u=!1;if(void 0===s.size)if(void 0===o.size)\"function\"==typeof s.cacheResult&&s.cacheResult();else{u=!0;var _=s;s=o,o=_}var w=!0,x=o.__iterate((function(o,a){if(i?!s.has(o):u?!is(o,s.get(a,j)):!is(s.get(a,j),o))return w=!1,!1}));return w&&s.size===x}function Repeat(s,o){if(!(this instanceof Repeat))return new Repeat(s,o);if(this._value=s,this.size=void 0===o?1/0:Math.max(0,o),0===this.size){if(ie)return ie;ie=this}}function invariant(s,o){if(!s)throw new Error(o)}function Range(s,o,i){if(!(this instanceof Range))return new Range(s,o,i);if(invariant(0!==i,\"Cannot step a Range by 0\"),s=s||0,void 0===o&&(o=1/0),i=void 0===i?1:Math.abs(i),o<s&&(i=-i),this._start=s,this._end=o,this._step=i,this.size=Math.max(0,Math.ceil((o-s)/i-1)+1),0===this.size){if(ae)return ae;ae=this}}function Collection(){throw TypeError(\"Abstract\")}function KeyedCollection(){}function IndexedCollection(){}function SetCollection(){}Seq.prototype[ce]=!0,createClass(ArraySeq,IndexedSeq),ArraySeq.prototype.get=function(s,o){return this.has(s)?this._array[wrapIndex(this,s)]:o},ArraySeq.prototype.__iterate=function(s,o){for(var i=this._array,a=i.length-1,u=0;u<=a;u++)if(!1===s(i[o?a-u:u],u,this))return u+1;return u},ArraySeq.prototype.__iterator=function(s,o){var i=this._array,a=i.length-1,u=0;return new Iterator((function(){return u>a?iteratorDone():iteratorValue(s,u,i[o?a-u++:u++])}))},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(s,o){return void 0===o||this.has(s)?this._object[s]:o},ObjectSeq.prototype.has=function(s){return this._object.hasOwnProperty(s)},ObjectSeq.prototype.__iterate=function(s,o){for(var i=this._object,a=this._keys,u=a.length-1,_=0;_<=u;_++){var w=a[o?u-_:_];if(!1===s(i[w],w,this))return _+1}return _},ObjectSeq.prototype.__iterator=function(s,o){var i=this._object,a=this._keys,u=a.length-1,_=0;return new Iterator((function(){var w=a[o?u-_:_];return _++>u?iteratorDone():iteratorValue(s,w,i[w])}))},ObjectSeq.prototype[u]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(s,o){if(o)return this.cacheResult().__iterate(s,o);var i=getIterator(this._iterable),a=0;if(isIterator(i))for(var u;!(u=i.next()).done&&!1!==s(u.value,a++,this););return a},IterableSeq.prototype.__iteratorUncached=function(s,o){if(o)return this.cacheResult().__iterator(s,o);var i=getIterator(this._iterable);if(!isIterator(i))return new Iterator(iteratorDone);var a=0;return new Iterator((function(){var o=i.next();return o.done?o:iteratorValue(s,a++,o.value)}))},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(s,o){if(o)return this.cacheResult().__iterate(s,o);for(var i,a=this._iterator,u=this._iteratorCache,_=0;_<u.length;)if(!1===s(u[_],_++,this))return _;for(;!(i=a.next()).done;){var w=i.value;if(u[_]=w,!1===s(w,_++,this))break}return _},IteratorSeq.prototype.__iteratorUncached=function(s,o){if(o)return this.cacheResult().__iterator(s,o);var i=this._iterator,a=this._iteratorCache,u=0;return new Iterator((function(){if(u>=a.length){var o=i.next();if(o.done)return o;a[u]=o.value}return iteratorValue(s,u,a[u++])}))},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?\"Repeat []\":\"Repeat [ \"+this._value+\" \"+this.size+\" times ]\"},Repeat.prototype.get=function(s,o){return this.has(s)?this._value:o},Repeat.prototype.includes=function(s){return is(this._value,s)},Repeat.prototype.slice=function(s,o){var i=this.size;return wholeSlice(s,o,i)?this:new Repeat(this._value,resolveEnd(o,i)-resolveBegin(s,i))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(s){return is(this._value,s)?0:-1},Repeat.prototype.lastIndexOf=function(s){return is(this._value,s)?this.size:-1},Repeat.prototype.__iterate=function(s,o){for(var i=0;i<this.size;i++)if(!1===s(this._value,i,this))return i+1;return i},Repeat.prototype.__iterator=function(s,o){var i=this,a=0;return new Iterator((function(){return a<i.size?iteratorValue(s,a++,i._value):iteratorDone()}))},Repeat.prototype.equals=function(s){return s instanceof Repeat?is(this._value,s._value):deepEqual(s)},createClass(Range,IndexedSeq),Range.prototype.toString=function(){return 0===this.size?\"Range []\":\"Range [ \"+this._start+\"...\"+this._end+(1!==this._step?\" by \"+this._step:\"\")+\" ]\"},Range.prototype.get=function(s,o){return this.has(s)?this._start+wrapIndex(this,s)*this._step:o},Range.prototype.includes=function(s){var o=(s-this._start)/this._step;return o>=0&&o<this.size&&o===Math.floor(o)},Range.prototype.slice=function(s,o){return wholeSlice(s,o,this.size)?this:(s=resolveBegin(s,this.size),(o=resolveEnd(o,this.size))<=s?new Range(0,0):new Range(this.get(s,this._end),this.get(o,this._end),this._step))},Range.prototype.indexOf=function(s){var o=s-this._start;if(o%this._step==0){var i=o/this._step;if(i>=0&&i<this.size)return i}return-1},Range.prototype.lastIndexOf=function(s){return this.indexOf(s)},Range.prototype.__iterate=function(s,o){for(var i=this.size-1,a=this._step,u=o?this._start+i*a:this._start,_=0;_<=i;_++){if(!1===s(u,_,this))return _+1;u+=o?-a:a}return _},Range.prototype.__iterator=function(s,o){var i=this.size-1,a=this._step,u=o?this._start+i*a:this._start,_=0;return new Iterator((function(){var w=u;return u+=o?-a:a,_>i?iteratorDone():iteratorValue(s,_++,w)}))},Range.prototype.equals=function(s){return s instanceof Range?this._start===s._start&&this._end===s._end&&this._step===s._step:deepEqual(this,s)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var le=\"function\"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(s,o){var i=65535&(s|=0),a=65535&(o|=0);return i*a+((s>>>16)*a+i*(o>>>16)<<16>>>0)|0};function smi(s){return s>>>1&1073741824|3221225471&s}function hash(s){if(!1===s||null==s)return 0;if(\"function\"==typeof s.valueOf&&(!1===(s=s.valueOf())||null==s))return 0;if(!0===s)return 1;var o=typeof s;if(\"number\"===o){if(s!=s||s===1/0)return 0;var i=0|s;for(i!==s&&(i^=4294967295*s);s>4294967295;)i^=s/=4294967295;return smi(i)}if(\"string\"===o)return s.length>Se?cachedHashString(s):hashString(s);if(\"function\"==typeof s.hashCode)return s.hashCode();if(\"object\"===o)return hashJSObj(s);if(\"function\"==typeof s.toString)return hashString(s.toString());throw new Error(\"Value type \"+o+\" cannot be hashed.\")}function cachedHashString(s){var o=Pe[s];return void 0===o&&(o=hashString(s),xe===we&&(xe=0,Pe={}),xe++,Pe[s]=o),o}function hashString(s){for(var o=0,i=0;i<s.length;i++)o=31*o+s.charCodeAt(i)|0;return smi(o)}function hashJSObj(s){var o;if(ye&&void 0!==(o=fe.get(s)))return o;if(void 0!==(o=s[_e]))return o;if(!de){if(void 0!==(o=s.propertyIsEnumerable&&s.propertyIsEnumerable[_e]))return o;if(void 0!==(o=getIENodeHash(s)))return o}if(o=++be,1073741824&be&&(be=0),ye)fe.set(s,o);else{if(void 0!==pe&&!1===pe(s))throw new Error(\"Non-extensible objects are not allowed as keys.\");if(de)Object.defineProperty(s,_e,{enumerable:!1,configurable:!1,writable:!1,value:o});else if(void 0!==s.propertyIsEnumerable&&s.propertyIsEnumerable===s.constructor.prototype.propertyIsEnumerable)s.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},s.propertyIsEnumerable[_e]=o;else{if(void 0===s.nodeType)throw new Error(\"Unable to set a non-enumerable property on object.\");s[_e]=o}}return o}var pe=Object.isExtensible,de=function(){try{return Object.defineProperty({},\"@\",{}),!0}catch(s){return!1}}();function getIENodeHash(s){if(s&&s.nodeType>0)switch(s.nodeType){case 1:return s.uniqueID;case 9:return s.documentElement&&s.documentElement.uniqueID}}var fe,ye=\"function\"==typeof WeakMap;ye&&(fe=new WeakMap);var be=0,_e=\"__immutablehash__\";\"function\"==typeof Symbol&&(_e=Symbol(_e));var Se=16,we=255,xe=0,Pe={};function assertNotInfinite(s){invariant(s!==1/0,\"Cannot perform this action with an infinite size.\")}function Map(s){return null==s?emptyMap():isMap(s)&&!isOrdered(s)?s:emptyMap().withMutations((function(o){var i=KeyedIterable(s);assertNotInfinite(i.size),i.forEach((function(s,i){return o.set(i,s)}))}))}function isMap(s){return!(!s||!s[Re])}createClass(Map,KeyedCollection),Map.of=function(){var o=s.call(arguments,0);return emptyMap().withMutations((function(s){for(var i=0;i<o.length;i+=2){if(i+1>=o.length)throw new Error(\"Missing value for key: \"+o[i]);s.set(o[i],o[i+1])}}))},Map.prototype.toString=function(){return this.__toString(\"Map {\",\"}\")},Map.prototype.get=function(s,o){return this._root?this._root.get(0,void 0,s,o):o},Map.prototype.set=function(s,o){return updateMap(this,s,o)},Map.prototype.setIn=function(s,o){return this.updateIn(s,j,(function(){return o}))},Map.prototype.remove=function(s){return updateMap(this,s,j)},Map.prototype.deleteIn=function(s){return this.updateIn(s,(function(){return j}))},Map.prototype.update=function(s,o,i){return 1===arguments.length?s(this):this.updateIn([s],o,i)},Map.prototype.updateIn=function(s,o,i){i||(i=o,o=void 0);var a=updateInDeepMap(this,forceIterator(s),o,i);return a===j?void 0:a},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(o){return mergeIntoMapWith(this,o,s.call(arguments,1))},Map.prototype.mergeIn=function(o){var i=s.call(arguments,1);return this.updateIn(o,emptyMap(),(function(s){return\"function\"==typeof s.merge?s.merge.apply(s,i):i[i.length-1]}))},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(o){var i=s.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(o),i)},Map.prototype.mergeDeepIn=function(o){var i=s.call(arguments,1);return this.updateIn(o,emptyMap(),(function(s){return\"function\"==typeof s.mergeDeep?s.mergeDeep.apply(s,i):i[i.length-1]}))},Map.prototype.sort=function(s){return OrderedMap(sortFactory(this,s))},Map.prototype.sortBy=function(s,o){return OrderedMap(sortFactory(this,o,s))},Map.prototype.withMutations=function(s){var o=this.asMutable();return s(o),o.wasAltered()?o.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(s,o){return new MapIterator(this,s,o)},Map.prototype.__iterate=function(s,o){var i=this,a=0;return this._root&&this._root.iterate((function(o){return a++,s(o[1],o[0],i)}),o),a},Map.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeMap(this.size,this._root,s,this.__hash):(this.__ownerID=s,this.__altered=!1,this)},Map.isMap=isMap;var Te,Re=\"@@__IMMUTABLE_MAP__@@\",$e=Map.prototype;function ArrayMapNode(s,o){this.ownerID=s,this.entries=o}function BitmapIndexedNode(s,o,i){this.ownerID=s,this.bitmap=o,this.nodes=i}function HashArrayMapNode(s,o,i){this.ownerID=s,this.count=o,this.nodes=i}function HashCollisionNode(s,o,i){this.ownerID=s,this.keyHash=o,this.entries=i}function ValueNode(s,o,i){this.ownerID=s,this.keyHash=o,this.entry=i}function MapIterator(s,o,i){this._type=o,this._reverse=i,this._stack=s._root&&mapIteratorFrame(s._root)}function mapIteratorValue(s,o){return iteratorValue(s,o[0],o[1])}function mapIteratorFrame(s,o){return{node:s,index:0,__prev:o}}function makeMap(s,o,i,a){var u=Object.create($e);return u.size=s,u._root=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyMap(){return Te||(Te=makeMap(0))}function updateMap(s,o,i){var a,u;if(s._root){var _=MakeRef(L),w=MakeRef(B);if(a=updateNode(s._root,s.__ownerID,0,void 0,o,i,_,w),!w.value)return s;u=s.size+(_.value?i===j?-1:1:0)}else{if(i===j)return s;u=1,a=new ArrayMapNode(s.__ownerID,[[o,i]])}return s.__ownerID?(s.size=u,s._root=a,s.__hash=void 0,s.__altered=!0,s):a?makeMap(u,a):emptyMap()}function updateNode(s,o,i,a,u,_,w,x){return s?s.update(o,i,a,u,_,w,x):_===j?s:(SetRef(x),SetRef(w),new ValueNode(o,a,[u,_]))}function isLeafNode(s){return s.constructor===ValueNode||s.constructor===HashCollisionNode}function mergeIntoNode(s,o,i,a,u){if(s.keyHash===a)return new HashCollisionNode(o,a,[s.entry,u]);var _,x=(0===i?s.keyHash:s.keyHash>>>i)&C,j=(0===i?a:a>>>i)&C;return new BitmapIndexedNode(o,1<<x|1<<j,x===j?[mergeIntoNode(s,o,i+w,a,u)]:(_=new ValueNode(o,a,u),x<j?[s,_]:[_,s]))}function createNodes(s,o,i,a){s||(s=new OwnerID);for(var u=new ValueNode(s,hash(i),[i,a]),_=0;_<o.length;_++){var w=o[_];u=u.update(s,0,void 0,w[0],w[1])}return u}function packNodes(s,o,i,a){for(var u=0,_=0,w=new Array(i),x=0,C=1,j=o.length;x<j;x++,C<<=1){var L=o[x];void 0!==L&&x!==a&&(u|=C,w[_++]=L)}return new BitmapIndexedNode(s,u,w)}function expandNodes(s,o,i,a,u){for(var _=0,w=new Array(x),C=0;0!==i;C++,i>>>=1)w[C]=1&i?o[_++]:void 0;return w[a]=u,new HashArrayMapNode(s,_+1,w)}function mergeIntoMapWith(s,o,i){for(var a=[],u=0;u<i.length;u++){var _=i[u],w=KeyedIterable(_);isIterable(_)||(w=w.map((function(s){return fromJS(s)}))),a.push(w)}return mergeIntoCollectionWith(s,o,a)}function deepMerger(s,o,i){return s&&s.mergeDeep&&isIterable(o)?s.mergeDeep(o):is(s,o)?s:o}function deepMergerWith(s){return function(o,i,a){if(o&&o.mergeDeepWith&&isIterable(i))return o.mergeDeepWith(s,i);var u=s(o,i,a);return is(o,u)?o:u}}function mergeIntoCollectionWith(s,o,i){return 0===(i=i.filter((function(s){return 0!==s.size}))).length?s:0!==s.size||s.__ownerID||1!==i.length?s.withMutations((function(s){for(var a=o?function(i,a){s.update(a,j,(function(s){return s===j?i:o(s,i,a)}))}:function(o,i){s.set(i,o)},u=0;u<i.length;u++)i[u].forEach(a)})):s.constructor(i[0])}function updateInDeepMap(s,o,i,a){var u=s===j,_=o.next();if(_.done){var w=u?i:s,x=a(w);return x===w?s:x}invariant(u||s&&s.set,\"invalid keyPath\");var C=_.value,L=u?j:s.get(C,j),B=updateInDeepMap(L,o,i,a);return B===L?s:B===j?s.remove(C):(u?emptyMap():s).set(C,B)}function popCount(s){return s=(s=(858993459&(s-=s>>1&1431655765))+(s>>2&858993459))+(s>>4)&252645135,s+=s>>8,127&(s+=s>>16)}function setIn(s,o,i,a){var u=a?s:arrCopy(s);return u[o]=i,u}function spliceIn(s,o,i,a){var u=s.length+1;if(a&&o+1===u)return s[o]=i,s;for(var _=new Array(u),w=0,x=0;x<u;x++)x===o?(_[x]=i,w=-1):_[x]=s[x+w];return _}function spliceOut(s,o,i){var a=s.length-1;if(i&&o===a)return s.pop(),s;for(var u=new Array(a),_=0,w=0;w<a;w++)w===o&&(_=1),u[w]=s[w+_];return u}$e[Re]=!0,$e[_]=$e.remove,$e.removeIn=$e.deleteIn,ArrayMapNode.prototype.get=function(s,o,i,a){for(var u=this.entries,_=0,w=u.length;_<w;_++)if(is(i,u[_][0]))return u[_][1];return a},ArrayMapNode.prototype.update=function(s,o,i,a,u,_,w){for(var x=u===j,C=this.entries,L=0,B=C.length;L<B&&!is(a,C[L][0]);L++);var $=L<B;if($?C[L][1]===u:x)return this;if(SetRef(w),(x||!$)&&SetRef(_),!x||1!==C.length){if(!$&&!x&&C.length>=qe)return createNodes(s,C,a,u);var U=s&&s===this.ownerID,V=U?C:arrCopy(C);return $?x?L===B-1?V.pop():V[L]=V.pop():V[L]=[a,u]:V.push([a,u]),U?(this.entries=V,this):new ArrayMapNode(s,V)}},BitmapIndexedNode.prototype.get=function(s,o,i,a){void 0===o&&(o=hash(i));var u=1<<((0===s?o:o>>>s)&C),_=this.bitmap;return _&u?this.nodes[popCount(_&u-1)].get(s+w,o,i,a):a},BitmapIndexedNode.prototype.update=function(s,o,i,a,u,_,x){void 0===i&&(i=hash(a));var L=(0===o?i:i>>>o)&C,B=1<<L,$=this.bitmap,U=!!($&B);if(!U&&u===j)return this;var V=popCount($&B-1),z=this.nodes,Y=U?z[V]:void 0,Z=updateNode(Y,s,o+w,i,a,u,_,x);if(Z===Y)return this;if(!U&&Z&&z.length>=ze)return expandNodes(s,z,$,L,Z);if(U&&!Z&&2===z.length&&isLeafNode(z[1^V]))return z[1^V];if(U&&Z&&1===z.length&&isLeafNode(Z))return Z;var ee=s&&s===this.ownerID,ie=U?Z?$:$^B:$|B,ae=U?Z?setIn(z,V,Z,ee):spliceOut(z,V,ee):spliceIn(z,V,Z,ee);return ee?(this.bitmap=ie,this.nodes=ae,this):new BitmapIndexedNode(s,ie,ae)},HashArrayMapNode.prototype.get=function(s,o,i,a){void 0===o&&(o=hash(i));var u=(0===s?o:o>>>s)&C,_=this.nodes[u];return _?_.get(s+w,o,i,a):a},HashArrayMapNode.prototype.update=function(s,o,i,a,u,_,x){void 0===i&&(i=hash(a));var L=(0===o?i:i>>>o)&C,B=u===j,$=this.nodes,U=$[L];if(B&&!U)return this;var V=updateNode(U,s,o+w,i,a,u,_,x);if(V===U)return this;var z=this.count;if(U){if(!V&&--z<We)return packNodes(s,$,z,L)}else z++;var Y=s&&s===this.ownerID,Z=setIn($,L,V,Y);return Y?(this.count=z,this.nodes=Z,this):new HashArrayMapNode(s,z,Z)},HashCollisionNode.prototype.get=function(s,o,i,a){for(var u=this.entries,_=0,w=u.length;_<w;_++)if(is(i,u[_][0]))return u[_][1];return a},HashCollisionNode.prototype.update=function(s,o,i,a,u,_,w){void 0===i&&(i=hash(a));var x=u===j;if(i!==this.keyHash)return x?this:(SetRef(w),SetRef(_),mergeIntoNode(this,s,o,i,[a,u]));for(var C=this.entries,L=0,B=C.length;L<B&&!is(a,C[L][0]);L++);var $=L<B;if($?C[L][1]===u:x)return this;if(SetRef(w),(x||!$)&&SetRef(_),x&&2===B)return new ValueNode(s,this.keyHash,C[1^L]);var U=s&&s===this.ownerID,V=U?C:arrCopy(C);return $?x?L===B-1?V.pop():V[L]=V.pop():V[L]=[a,u]:V.push([a,u]),U?(this.entries=V,this):new HashCollisionNode(s,this.keyHash,V)},ValueNode.prototype.get=function(s,o,i,a){return is(i,this.entry[0])?this.entry[1]:a},ValueNode.prototype.update=function(s,o,i,a,u,_,w){var x=u===j,C=is(a,this.entry[0]);return(C?u===this.entry[1]:x)?this:(SetRef(w),x?void SetRef(_):C?s&&s===this.ownerID?(this.entry[1]=u,this):new ValueNode(s,this.keyHash,[a,u]):(SetRef(_),mergeIntoNode(this,s,o,hash(a),[a,u])))},ArrayMapNode.prototype.iterate=HashCollisionNode.prototype.iterate=function(s,o){for(var i=this.entries,a=0,u=i.length-1;a<=u;a++)if(!1===s(i[o?u-a:a]))return!1},BitmapIndexedNode.prototype.iterate=HashArrayMapNode.prototype.iterate=function(s,o){for(var i=this.nodes,a=0,u=i.length-1;a<=u;a++){var _=i[o?u-a:a];if(_&&!1===_.iterate(s,o))return!1}},ValueNode.prototype.iterate=function(s,o){return s(this.entry)},createClass(MapIterator,Iterator),MapIterator.prototype.next=function(){for(var s=this._type,o=this._stack;o;){var i,a=o.node,u=o.index++;if(a.entry){if(0===u)return mapIteratorValue(s,a.entry)}else if(a.entries){if(u<=(i=a.entries.length-1))return mapIteratorValue(s,a.entries[this._reverse?i-u:u])}else if(u<=(i=a.nodes.length-1)){var _=a.nodes[this._reverse?i-u:u];if(_){if(_.entry)return mapIteratorValue(s,_.entry);o=this._stack=mapIteratorFrame(_,o)}continue}o=this._stack=this._stack.__prev}return iteratorDone()};var qe=x/4,ze=x/2,We=x/4;function List(s){var o=emptyList();if(null==s)return o;if(isList(s))return s;var i=IndexedIterable(s),a=i.size;return 0===a?o:(assertNotInfinite(a),a>0&&a<x?makeList(0,a,w,null,new VNode(i.toArray())):o.withMutations((function(s){s.setSize(a),i.forEach((function(o,i){return s.set(i,o)}))})))}function isList(s){return!(!s||!s[He])}createClass(List,IndexedCollection),List.of=function(){return this(arguments)},List.prototype.toString=function(){return this.__toString(\"List [\",\"]\")},List.prototype.get=function(s,o){if((s=wrapIndex(this,s))>=0&&s<this.size){var i=listNodeFor(this,s+=this._origin);return i&&i.array[s&C]}return o},List.prototype.set=function(s,o){return updateList(this,s,o)},List.prototype.remove=function(s){return this.has(s)?0===s?this.shift():s===this.size-1?this.pop():this.splice(s,1):this},List.prototype.insert=function(s,o){return this.splice(s,0,o)},List.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=w,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):emptyList()},List.prototype.push=function(){var s=arguments,o=this.size;return this.withMutations((function(i){setListBounds(i,0,o+s.length);for(var a=0;a<s.length;a++)i.set(o+a,s[a])}))},List.prototype.pop=function(){return setListBounds(this,0,-1)},List.prototype.unshift=function(){var s=arguments;return this.withMutations((function(o){setListBounds(o,-s.length);for(var i=0;i<s.length;i++)o.set(i,s[i])}))},List.prototype.shift=function(){return setListBounds(this,1)},List.prototype.merge=function(){return mergeIntoListWith(this,void 0,arguments)},List.prototype.mergeWith=function(o){return mergeIntoListWith(this,o,s.call(arguments,1))},List.prototype.mergeDeep=function(){return mergeIntoListWith(this,deepMerger,arguments)},List.prototype.mergeDeepWith=function(o){var i=s.call(arguments,1);return mergeIntoListWith(this,deepMergerWith(o),i)},List.prototype.setSize=function(s){return setListBounds(this,0,s)},List.prototype.slice=function(s,o){var i=this.size;return wholeSlice(s,o,i)?this:setListBounds(this,resolveBegin(s,i),resolveEnd(o,i))},List.prototype.__iterator=function(s,o){var i=0,a=iterateList(this,o);return new Iterator((function(){var o=a();return o===et?iteratorDone():iteratorValue(s,i++,o)}))},List.prototype.__iterate=function(s,o){for(var i,a=0,u=iterateList(this,o);(i=u())!==et&&!1!==s(i,a++,this););return a},List.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeList(this._origin,this._capacity,this._level,this._root,this._tail,s,this.__hash):(this.__ownerID=s,this)},List.isList=isList;var He=\"@@__IMMUTABLE_LIST__@@\",Ye=List.prototype;function VNode(s,o){this.array=s,this.ownerID=o}Ye[He]=!0,Ye[_]=Ye.remove,Ye.setIn=$e.setIn,Ye.deleteIn=Ye.removeIn=$e.removeIn,Ye.update=$e.update,Ye.updateIn=$e.updateIn,Ye.mergeIn=$e.mergeIn,Ye.mergeDeepIn=$e.mergeDeepIn,Ye.withMutations=$e.withMutations,Ye.asMutable=$e.asMutable,Ye.asImmutable=$e.asImmutable,Ye.wasAltered=$e.wasAltered,VNode.prototype.removeBefore=function(s,o,i){if(i===o?1<<o:0===this.array.length)return this;var a=i>>>o&C;if(a>=this.array.length)return new VNode([],s);var u,_=0===a;if(o>0){var x=this.array[a];if((u=x&&x.removeBefore(s,o-w,i))===x&&_)return this}if(_&&!u)return this;var j=editableVNode(this,s);if(!_)for(var L=0;L<a;L++)j.array[L]=void 0;return u&&(j.array[a]=u),j},VNode.prototype.removeAfter=function(s,o,i){if(i===(o?1<<o:0)||0===this.array.length)return this;var a,u=i-1>>>o&C;if(u>=this.array.length)return this;if(o>0){var _=this.array[u];if((a=_&&_.removeAfter(s,o-w,i))===_&&u===this.array.length-1)return this}var x=editableVNode(this,s);return x.array.splice(u+1),a&&(x.array[u]=a),x};var Xe,Qe,et={};function iterateList(s,o){var i=s._origin,a=s._capacity,u=getTailOffset(a),_=s._tail;return iterateNodeOrLeaf(s._root,s._level,0);function iterateNodeOrLeaf(s,o,i){return 0===o?iterateLeaf(s,i):iterateNode(s,o,i)}function iterateLeaf(s,w){var C=w===u?_&&_.array:s&&s.array,j=w>i?0:i-w,L=a-w;return L>x&&(L=x),function(){if(j===L)return et;var s=o?--L:j++;return C&&C[s]}}function iterateNode(s,u,_){var C,j=s&&s.array,L=_>i?0:i-_>>u,B=1+(a-_>>u);return B>x&&(B=x),function(){for(;;){if(C){var s=C();if(s!==et)return s;C=null}if(L===B)return et;var i=o?--B:L++;C=iterateNodeOrLeaf(j&&j[i],u-w,_+(i<<u))}}}}function makeList(s,o,i,a,u,_,w){var x=Object.create(Ye);return x.size=o-s,x._origin=s,x._capacity=o,x._level=i,x._root=a,x._tail=u,x.__ownerID=_,x.__hash=w,x.__altered=!1,x}function emptyList(){return Xe||(Xe=makeList(0,0,w))}function updateList(s,o,i){if((o=wrapIndex(s,o))!=o)return s;if(o>=s.size||o<0)return s.withMutations((function(s){o<0?setListBounds(s,o).set(0,i):setListBounds(s,0,o+1).set(o,i)}));o+=s._origin;var a=s._tail,u=s._root,_=MakeRef(B);return o>=getTailOffset(s._capacity)?a=updateVNode(a,s.__ownerID,0,o,i,_):u=updateVNode(u,s.__ownerID,s._level,o,i,_),_.value?s.__ownerID?(s._root=u,s._tail=a,s.__hash=void 0,s.__altered=!0,s):makeList(s._origin,s._capacity,s._level,u,a):s}function updateVNode(s,o,i,a,u,_){var x,j=a>>>i&C,L=s&&j<s.array.length;if(!L&&void 0===u)return s;if(i>0){var B=s&&s.array[j],$=updateVNode(B,o,i-w,a,u,_);return $===B?s:((x=editableVNode(s,o)).array[j]=$,x)}return L&&s.array[j]===u?s:(SetRef(_),x=editableVNode(s,o),void 0===u&&j===x.array.length-1?x.array.pop():x.array[j]=u,x)}function editableVNode(s,o){return o&&s&&o===s.ownerID?s:new VNode(s?s.array.slice():[],o)}function listNodeFor(s,o){if(o>=getTailOffset(s._capacity))return s._tail;if(o<1<<s._level+w){for(var i=s._root,a=s._level;i&&a>0;)i=i.array[o>>>a&C],a-=w;return i}}function setListBounds(s,o,i){void 0!==o&&(o|=0),void 0!==i&&(i|=0);var a=s.__ownerID||new OwnerID,u=s._origin,_=s._capacity,x=u+o,j=void 0===i?_:i<0?_+i:u+i;if(x===u&&j===_)return s;if(x>=j)return s.clear();for(var L=s._level,B=s._root,$=0;x+$<0;)B=new VNode(B&&B.array.length?[void 0,B]:[],a),$+=1<<(L+=w);$&&(x+=$,u+=$,j+=$,_+=$);for(var U=getTailOffset(_),V=getTailOffset(j);V>=1<<L+w;)B=new VNode(B&&B.array.length?[B]:[],a),L+=w;var z=s._tail,Y=V<U?listNodeFor(s,j-1):V>U?new VNode([],a):z;if(z&&V>U&&x<_&&z.array.length){for(var Z=B=editableVNode(B,a),ee=L;ee>w;ee-=w){var ie=U>>>ee&C;Z=Z.array[ie]=editableVNode(Z.array[ie],a)}Z.array[U>>>w&C]=z}if(j<_&&(Y=Y&&Y.removeAfter(a,0,j)),x>=V)x-=V,j-=V,L=w,B=null,Y=Y&&Y.removeBefore(a,0,x);else if(x>u||V<U){for($=0;B;){var ae=x>>>L&C;if(ae!==V>>>L&C)break;ae&&($+=(1<<L)*ae),L-=w,B=B.array[ae]}B&&x>u&&(B=B.removeBefore(a,L,x-$)),B&&V<U&&(B=B.removeAfter(a,L,V-$)),$&&(x-=$,j-=$)}return s.__ownerID?(s.size=j-x,s._origin=x,s._capacity=j,s._level=L,s._root=B,s._tail=Y,s.__hash=void 0,s.__altered=!0,s):makeList(x,j,L,B,Y)}function mergeIntoListWith(s,o,i){for(var a=[],u=0,_=0;_<i.length;_++){var w=i[_],x=IndexedIterable(w);x.size>u&&(u=x.size),isIterable(w)||(x=x.map((function(s){return fromJS(s)}))),a.push(x)}return u>s.size&&(s=s.setSize(u)),mergeIntoCollectionWith(s,o,a)}function getTailOffset(s){return s<x?0:s-1>>>w<<w}function OrderedMap(s){return null==s?emptyOrderedMap():isOrderedMap(s)?s:emptyOrderedMap().withMutations((function(o){var i=KeyedIterable(s);assertNotInfinite(i.size),i.forEach((function(s,i){return o.set(i,s)}))}))}function isOrderedMap(s){return isMap(s)&&isOrdered(s)}function makeOrderedMap(s,o,i,a){var u=Object.create(OrderedMap.prototype);return u.size=s?s.size:0,u._map=s,u._list=o,u.__ownerID=i,u.__hash=a,u}function emptyOrderedMap(){return Qe||(Qe=makeOrderedMap(emptyMap(),emptyList()))}function updateOrderedMap(s,o,i){var a,u,_=s._map,w=s._list,C=_.get(o),L=void 0!==C;if(i===j){if(!L)return s;w.size>=x&&w.size>=2*_.size?(a=(u=w.filter((function(s,o){return void 0!==s&&C!==o}))).toKeyedSeq().map((function(s){return s[0]})).flip().toMap(),s.__ownerID&&(a.__ownerID=u.__ownerID=s.__ownerID)):(a=_.remove(o),u=C===w.size-1?w.pop():w.set(C,void 0))}else if(L){if(i===w.get(C)[1])return s;a=_,u=w.set(C,[o,i])}else a=_.set(o,w.size),u=w.set(w.size,[o,i]);return s.__ownerID?(s.size=a.size,s._map=a,s._list=u,s.__hash=void 0,s):makeOrderedMap(a,u)}function ToKeyedSequence(s,o){this._iter=s,this._useKeys=o,this.size=s.size}function ToIndexedSequence(s){this._iter=s,this.size=s.size}function ToSetSequence(s){this._iter=s,this.size=s.size}function FromEntriesSequence(s){this._iter=s,this.size=s.size}function flipFactory(s){var o=makeSequence(s);return o._iter=s,o.size=s.size,o.flip=function(){return s},o.reverse=function(){var o=s.reverse.apply(this);return o.flip=function(){return s.reverse()},o},o.has=function(o){return s.includes(o)},o.includes=function(o){return s.has(o)},o.cacheResult=cacheResultThrough,o.__iterateUncached=function(o,i){var a=this;return s.__iterate((function(s,i){return!1!==o(i,s,a)}),i)},o.__iteratorUncached=function(o,i){if(o===V){var a=s.__iterator(o,i);return new Iterator((function(){var s=a.next();if(!s.done){var o=s.value[0];s.value[0]=s.value[1],s.value[1]=o}return s}))}return s.__iterator(o===U?$:U,i)},o}function mapFactory(s,o,i){var a=makeSequence(s);return a.size=s.size,a.has=function(o){return s.has(o)},a.get=function(a,u){var _=s.get(a,j);return _===j?u:o.call(i,_,a,s)},a.__iterateUncached=function(a,u){var _=this;return s.__iterate((function(s,u,w){return!1!==a(o.call(i,s,u,w),u,_)}),u)},a.__iteratorUncached=function(a,u){var _=s.__iterator(V,u);return new Iterator((function(){var u=_.next();if(u.done)return u;var w=u.value,x=w[0];return iteratorValue(a,x,o.call(i,w[1],x,s),u)}))},a}function reverseFactory(s,o){var i=makeSequence(s);return i._iter=s,i.size=s.size,i.reverse=function(){return s},s.flip&&(i.flip=function(){var o=flipFactory(s);return o.reverse=function(){return s.flip()},o}),i.get=function(i,a){return s.get(o?i:-1-i,a)},i.has=function(i){return s.has(o?i:-1-i)},i.includes=function(o){return s.includes(o)},i.cacheResult=cacheResultThrough,i.__iterate=function(o,i){var a=this;return s.__iterate((function(s,i){return o(s,i,a)}),!i)},i.__iterator=function(o,i){return s.__iterator(o,!i)},i}function filterFactory(s,o,i,a){var u=makeSequence(s);return a&&(u.has=function(a){var u=s.get(a,j);return u!==j&&!!o.call(i,u,a,s)},u.get=function(a,u){var _=s.get(a,j);return _!==j&&o.call(i,_,a,s)?_:u}),u.__iterateUncached=function(u,_){var w=this,x=0;return s.__iterate((function(s,_,C){if(o.call(i,s,_,C))return x++,u(s,a?_:x-1,w)}),_),x},u.__iteratorUncached=function(u,_){var w=s.__iterator(V,_),x=0;return new Iterator((function(){for(;;){var _=w.next();if(_.done)return _;var C=_.value,j=C[0],L=C[1];if(o.call(i,L,j,s))return iteratorValue(u,a?j:x++,L,_)}}))},u}function countByFactory(s,o,i){var a=Map().asMutable();return s.__iterate((function(u,_){a.update(o.call(i,u,_,s),0,(function(s){return s+1}))})),a.asImmutable()}function groupByFactory(s,o,i){var a=isKeyed(s),u=(isOrdered(s)?OrderedMap():Map()).asMutable();s.__iterate((function(_,w){u.update(o.call(i,_,w,s),(function(s){return(s=s||[]).push(a?[w,_]:_),s}))}));var _=iterableClass(s);return u.map((function(o){return reify(s,_(o))}))}function sliceFactory(s,o,i,a){var u=s.size;if(void 0!==o&&(o|=0),void 0!==i&&(i===1/0?i=u:i|=0),wholeSlice(o,i,u))return s;var _=resolveBegin(o,u),w=resolveEnd(i,u);if(_!=_||w!=w)return sliceFactory(s.toSeq().cacheResult(),o,i,a);var x,C=w-_;C==C&&(x=C<0?0:C);var j=makeSequence(s);return j.size=0===x?x:s.size&&x||void 0,!a&&isSeq(s)&&x>=0&&(j.get=function(o,i){return(o=wrapIndex(this,o))>=0&&o<x?s.get(o+_,i):i}),j.__iterateUncached=function(o,i){var u=this;if(0===x)return 0;if(i)return this.cacheResult().__iterate(o,i);var w=0,C=!0,j=0;return s.__iterate((function(s,i){if(!C||!(C=w++<_))return j++,!1!==o(s,a?i:j-1,u)&&j!==x})),j},j.__iteratorUncached=function(o,i){if(0!==x&&i)return this.cacheResult().__iterator(o,i);var u=0!==x&&s.__iterator(o,i),w=0,C=0;return new Iterator((function(){for(;w++<_;)u.next();if(++C>x)return iteratorDone();var s=u.next();return a||o===U?s:iteratorValue(o,C-1,o===$?void 0:s.value[1],s)}))},j}function takeWhileFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterate(a,u);var w=0;return s.__iterate((function(s,u,x){return o.call(i,s,u,x)&&++w&&a(s,u,_)})),w},a.__iteratorUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterator(a,u);var w=s.__iterator(V,u),x=!0;return new Iterator((function(){if(!x)return iteratorDone();var s=w.next();if(s.done)return s;var u=s.value,C=u[0],j=u[1];return o.call(i,j,C,_)?a===V?s:iteratorValue(a,C,j,s):(x=!1,iteratorDone())}))},a}function skipWhileFactory(s,o,i,a){var u=makeSequence(s);return u.__iterateUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterate(u,_);var x=!0,C=0;return s.__iterate((function(s,_,j){if(!x||!(x=o.call(i,s,_,j)))return C++,u(s,a?_:C-1,w)})),C},u.__iteratorUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterator(u,_);var x=s.__iterator(V,_),C=!0,j=0;return new Iterator((function(){var s,_,L;do{if((s=x.next()).done)return a||u===U?s:iteratorValue(u,j++,u===$?void 0:s.value[1],s);var B=s.value;_=B[0],L=B[1],C&&(C=o.call(i,L,_,w))}while(C);return u===V?s:iteratorValue(u,_,L,s)}))},u}function concatFactory(s,o){var i=isKeyed(s),a=[s].concat(o).map((function(s){return isIterable(s)?i&&(s=KeyedIterable(s)):s=i?keyedSeqFromValue(s):indexedSeqFromValue(Array.isArray(s)?s:[s]),s})).filter((function(s){return 0!==s.size}));if(0===a.length)return s;if(1===a.length){var u=a[0];if(u===s||i&&isKeyed(u)||isIndexed(s)&&isIndexed(u))return u}var _=new ArraySeq(a);return i?_=_.toKeyedSeq():isIndexed(s)||(_=_.toSetSeq()),(_=_.flatten(!0)).size=a.reduce((function(s,o){if(void 0!==s){var i=o.size;if(void 0!==i)return s+i}}),0),_}function flattenFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=0,w=!1;function flatDeep(s,x){var C=this;s.__iterate((function(s,u){return(!o||x<o)&&isIterable(s)?flatDeep(s,x+1):!1===a(s,i?u:_++,C)&&(w=!0),!w}),u)}return flatDeep(s,0),_},a.__iteratorUncached=function(a,u){var _=s.__iterator(a,u),w=[],x=0;return new Iterator((function(){for(;_;){var s=_.next();if(!1===s.done){var C=s.value;if(a===V&&(C=C[1]),o&&!(w.length<o)||!isIterable(C))return i?s:iteratorValue(a,x++,C,s);w.push(_),_=C.__iterator(a,u)}else _=w.pop()}return iteratorDone()}))},a}function flatMapFactory(s,o,i){var a=iterableClass(s);return s.toSeq().map((function(u,_){return a(o.call(i,u,_,s))})).flatten(!0)}function interposeFactory(s,o){var i=makeSequence(s);return i.size=s.size&&2*s.size-1,i.__iterateUncached=function(i,a){var u=this,_=0;return s.__iterate((function(s,a){return(!_||!1!==i(o,_++,u))&&!1!==i(s,_++,u)}),a),_},i.__iteratorUncached=function(i,a){var u,_=s.__iterator(U,a),w=0;return new Iterator((function(){return(!u||w%2)&&(u=_.next()).done?u:w%2?iteratorValue(i,w++,o):iteratorValue(i,w++,u.value,u)}))},i}function sortFactory(s,o,i){o||(o=defaultComparator);var a=isKeyed(s),u=0,_=s.toSeq().map((function(o,a){return[a,o,u++,i?i(o,a,s):o]})).toArray();return _.sort((function(s,i){return o(s[3],i[3])||s[2]-i[2]})).forEach(a?function(s,o){_[o].length=2}:function(s,o){_[o]=s[1]}),a?KeyedSeq(_):isIndexed(s)?IndexedSeq(_):SetSeq(_)}function maxFactory(s,o,i){if(o||(o=defaultComparator),i){var a=s.toSeq().map((function(o,a){return[o,i(o,a,s)]})).reduce((function(s,i){return maxCompare(o,s[1],i[1])?i:s}));return a&&a[0]}return s.reduce((function(s,i){return maxCompare(o,s,i)?i:s}))}function maxCompare(s,o,i){var a=s(i,o);return 0===a&&i!==o&&(null==i||i!=i)||a>0}function zipWithFactory(s,o,i){var a=makeSequence(s);return a.size=new ArraySeq(i).map((function(s){return s.size})).min(),a.__iterate=function(s,o){for(var i,a=this.__iterator(U,o),u=0;!(i=a.next()).done&&!1!==s(i.value,u++,this););return u},a.__iteratorUncached=function(s,a){var u=i.map((function(s){return s=Iterable(s),getIterator(a?s.reverse():s)})),_=0,w=!1;return new Iterator((function(){var i;return w||(i=u.map((function(s){return s.next()})),w=i.some((function(s){return s.done}))),w?iteratorDone():iteratorValue(s,_++,o.apply(null,i.map((function(s){return s.value}))))}))},a}function reify(s,o){return isSeq(s)?o:s.constructor(o)}function validateEntry(s){if(s!==Object(s))throw new TypeError(\"Expected [K, V] tuple: \"+s)}function resolveSize(s){return assertNotInfinite(s.size),ensureSize(s)}function iterableClass(s){return isKeyed(s)?KeyedIterable:isIndexed(s)?IndexedIterable:SetIterable}function makeSequence(s){return Object.create((isKeyed(s)?KeyedSeq:isIndexed(s)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(s,o){return s>o?1:s<o?-1:0}function forceIterator(s){var o=getIterator(s);if(!o){if(!isArrayLike(s))throw new TypeError(\"Expected iterable or array-like: \"+s);o=getIterator(Iterable(s))}return o}function Record(s,o){var i,a=function Record(_){if(_ instanceof a)return _;if(!(this instanceof a))return new a(_);if(!i){i=!0;var w=Object.keys(s);setProps(u,w),u.size=w.length,u._name=o,u._keys=w,u._defaultValues=s}this._map=Map(_)},u=a.prototype=Object.create(tt);return u.constructor=a,a}createClass(OrderedMap,Map),OrderedMap.of=function(){return this(arguments)},OrderedMap.prototype.toString=function(){return this.__toString(\"OrderedMap {\",\"}\")},OrderedMap.prototype.get=function(s,o){var i=this._map.get(s);return void 0!==i?this._list.get(i)[1]:o},OrderedMap.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):emptyOrderedMap()},OrderedMap.prototype.set=function(s,o){return updateOrderedMap(this,s,o)},OrderedMap.prototype.remove=function(s){return updateOrderedMap(this,s,j)},OrderedMap.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},OrderedMap.prototype.__iterate=function(s,o){var i=this;return this._list.__iterate((function(o){return o&&s(o[1],o[0],i)}),o)},OrderedMap.prototype.__iterator=function(s,o){return this._list.fromEntrySeq().__iterator(s,o)},OrderedMap.prototype.__ensureOwner=function(s){if(s===this.__ownerID)return this;var o=this._map.__ensureOwner(s),i=this._list.__ensureOwner(s);return s?makeOrderedMap(o,i,s,this.__hash):(this.__ownerID=s,this._map=o,this._list=i,this)},OrderedMap.isOrderedMap=isOrderedMap,OrderedMap.prototype[u]=!0,OrderedMap.prototype[_]=OrderedMap.prototype.remove,createClass(ToKeyedSequence,KeyedSeq),ToKeyedSequence.prototype.get=function(s,o){return this._iter.get(s,o)},ToKeyedSequence.prototype.has=function(s){return this._iter.has(s)},ToKeyedSequence.prototype.valueSeq=function(){return this._iter.valueSeq()},ToKeyedSequence.prototype.reverse=function(){var s=this,o=reverseFactory(this,!0);return this._useKeys||(o.valueSeq=function(){return s._iter.toSeq().reverse()}),o},ToKeyedSequence.prototype.map=function(s,o){var i=this,a=mapFactory(this,s,o);return this._useKeys||(a.valueSeq=function(){return i._iter.toSeq().map(s,o)}),a},ToKeyedSequence.prototype.__iterate=function(s,o){var i,a=this;return this._iter.__iterate(this._useKeys?function(o,i){return s(o,i,a)}:(i=o?resolveSize(this):0,function(u){return s(u,o?--i:i++,a)}),o)},ToKeyedSequence.prototype.__iterator=function(s,o){if(this._useKeys)return this._iter.__iterator(s,o);var i=this._iter.__iterator(U,o),a=o?resolveSize(this):0;return new Iterator((function(){var u=i.next();return u.done?u:iteratorValue(s,o?--a:a++,u.value,u)}))},ToKeyedSequence.prototype[u]=!0,createClass(ToIndexedSequence,IndexedSeq),ToIndexedSequence.prototype.includes=function(s){return this._iter.includes(s)},ToIndexedSequence.prototype.__iterate=function(s,o){var i=this,a=0;return this._iter.__iterate((function(o){return s(o,a++,i)}),o)},ToIndexedSequence.prototype.__iterator=function(s,o){var i=this._iter.__iterator(U,o),a=0;return new Iterator((function(){var o=i.next();return o.done?o:iteratorValue(s,a++,o.value,o)}))},createClass(ToSetSequence,SetSeq),ToSetSequence.prototype.has=function(s){return this._iter.includes(s)},ToSetSequence.prototype.__iterate=function(s,o){var i=this;return this._iter.__iterate((function(o){return s(o,o,i)}),o)},ToSetSequence.prototype.__iterator=function(s,o){var i=this._iter.__iterator(U,o);return new Iterator((function(){var o=i.next();return o.done?o:iteratorValue(s,o.value,o.value,o)}))},createClass(FromEntriesSequence,KeyedSeq),FromEntriesSequence.prototype.entrySeq=function(){return this._iter.toSeq()},FromEntriesSequence.prototype.__iterate=function(s,o){var i=this;return this._iter.__iterate((function(o){if(o){validateEntry(o);var a=isIterable(o);return s(a?o.get(1):o[1],a?o.get(0):o[0],i)}}),o)},FromEntriesSequence.prototype.__iterator=function(s,o){var i=this._iter.__iterator(U,o);return new Iterator((function(){for(;;){var o=i.next();if(o.done)return o;var a=o.value;if(a){validateEntry(a);var u=isIterable(a);return iteratorValue(s,u?a.get(0):a[0],u?a.get(1):a[1],o)}}}))},ToIndexedSequence.prototype.cacheResult=ToKeyedSequence.prototype.cacheResult=ToSetSequence.prototype.cacheResult=FromEntriesSequence.prototype.cacheResult=cacheResultThrough,createClass(Record,KeyedCollection),Record.prototype.toString=function(){return this.__toString(recordName(this)+\" {\",\"}\")},Record.prototype.has=function(s){return this._defaultValues.hasOwnProperty(s)},Record.prototype.get=function(s,o){if(!this.has(s))return o;var i=this._defaultValues[s];return this._map?this._map.get(s,i):i},Record.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var s=this.constructor;return s._empty||(s._empty=makeRecord(this,emptyMap()))},Record.prototype.set=function(s,o){if(!this.has(s))throw new Error('Cannot set unknown key \"'+s+'\" on '+recordName(this));if(this._map&&!this._map.has(s)&&o===this._defaultValues[s])return this;var i=this._map&&this._map.set(s,o);return this.__ownerID||i===this._map?this:makeRecord(this,i)},Record.prototype.remove=function(s){if(!this.has(s))return this;var o=this._map&&this._map.remove(s);return this.__ownerID||o===this._map?this:makeRecord(this,o)},Record.prototype.wasAltered=function(){return this._map.wasAltered()},Record.prototype.__iterator=function(s,o){var i=this;return KeyedIterable(this._defaultValues).map((function(s,o){return i.get(o)})).__iterator(s,o)},Record.prototype.__iterate=function(s,o){var i=this;return KeyedIterable(this._defaultValues).map((function(s,o){return i.get(o)})).__iterate(s,o)},Record.prototype.__ensureOwner=function(s){if(s===this.__ownerID)return this;var o=this._map&&this._map.__ensureOwner(s);return s?makeRecord(this,o,s):(this.__ownerID=s,this._map=o,this)};var tt=Record.prototype;function makeRecord(s,o,i){var a=Object.create(Object.getPrototypeOf(s));return a._map=o,a.__ownerID=i,a}function recordName(s){return s._name||s.constructor.name||\"Record\"}function setProps(s,o){try{o.forEach(setProp.bind(void 0,s))}catch(s){}}function setProp(s,o){Object.defineProperty(s,o,{get:function(){return this.get(o)},set:function(s){invariant(this.__ownerID,\"Cannot set on an immutable record.\"),this.set(o,s)}})}function Set(s){return null==s?emptySet():isSet(s)&&!isOrdered(s)?s:emptySet().withMutations((function(o){var i=SetIterable(s);assertNotInfinite(i.size),i.forEach((function(s){return o.add(s)}))}))}function isSet(s){return!(!s||!s[nt])}tt[_]=tt.remove,tt.deleteIn=tt.removeIn=$e.removeIn,tt.merge=$e.merge,tt.mergeWith=$e.mergeWith,tt.mergeIn=$e.mergeIn,tt.mergeDeep=$e.mergeDeep,tt.mergeDeepWith=$e.mergeDeepWith,tt.mergeDeepIn=$e.mergeDeepIn,tt.setIn=$e.setIn,tt.update=$e.update,tt.updateIn=$e.updateIn,tt.withMutations=$e.withMutations,tt.asMutable=$e.asMutable,tt.asImmutable=$e.asImmutable,createClass(Set,SetCollection),Set.of=function(){return this(arguments)},Set.fromKeys=function(s){return this(KeyedIterable(s).keySeq())},Set.prototype.toString=function(){return this.__toString(\"Set {\",\"}\")},Set.prototype.has=function(s){return this._map.has(s)},Set.prototype.add=function(s){return updateSet(this,this._map.set(s,!0))},Set.prototype.remove=function(s){return updateSet(this,this._map.remove(s))},Set.prototype.clear=function(){return updateSet(this,this._map.clear())},Set.prototype.union=function(){var o=s.call(arguments,0);return 0===(o=o.filter((function(s){return 0!==s.size}))).length?this:0!==this.size||this.__ownerID||1!==o.length?this.withMutations((function(s){for(var i=0;i<o.length;i++)SetIterable(o[i]).forEach((function(o){return s.add(o)}))})):this.constructor(o[0])},Set.prototype.intersect=function(){var o=s.call(arguments,0);if(0===o.length)return this;o=o.map((function(s){return SetIterable(s)}));var i=this;return this.withMutations((function(s){i.forEach((function(i){o.every((function(s){return s.includes(i)}))||s.remove(i)}))}))},Set.prototype.subtract=function(){var o=s.call(arguments,0);if(0===o.length)return this;o=o.map((function(s){return SetIterable(s)}));var i=this;return this.withMutations((function(s){i.forEach((function(i){o.some((function(s){return s.includes(i)}))&&s.remove(i)}))}))},Set.prototype.merge=function(){return this.union.apply(this,arguments)},Set.prototype.mergeWith=function(o){var i=s.call(arguments,1);return this.union.apply(this,i)},Set.prototype.sort=function(s){return OrderedSet(sortFactory(this,s))},Set.prototype.sortBy=function(s,o){return OrderedSet(sortFactory(this,o,s))},Set.prototype.wasAltered=function(){return this._map.wasAltered()},Set.prototype.__iterate=function(s,o){var i=this;return this._map.__iterate((function(o,a){return s(a,a,i)}),o)},Set.prototype.__iterator=function(s,o){return this._map.map((function(s,o){return o})).__iterator(s,o)},Set.prototype.__ensureOwner=function(s){if(s===this.__ownerID)return this;var o=this._map.__ensureOwner(s);return s?this.__make(o,s):(this.__ownerID=s,this._map=o,this)},Set.isSet=isSet;var rt,nt=\"@@__IMMUTABLE_SET__@@\",st=Set.prototype;function updateSet(s,o){return s.__ownerID?(s.size=o.size,s._map=o,s):o===s._map?s:0===o.size?s.__empty():s.__make(o)}function makeSet(s,o){var i=Object.create(st);return i.size=s?s.size:0,i._map=s,i.__ownerID=o,i}function emptySet(){return rt||(rt=makeSet(emptyMap()))}function OrderedSet(s){return null==s?emptyOrderedSet():isOrderedSet(s)?s:emptyOrderedSet().withMutations((function(o){var i=SetIterable(s);assertNotInfinite(i.size),i.forEach((function(s){return o.add(s)}))}))}function isOrderedSet(s){return isSet(s)&&isOrdered(s)}st[nt]=!0,st[_]=st.remove,st.mergeDeep=st.merge,st.mergeDeepWith=st.mergeWith,st.withMutations=$e.withMutations,st.asMutable=$e.asMutable,st.asImmutable=$e.asImmutable,st.__empty=emptySet,st.__make=makeSet,createClass(OrderedSet,Set),OrderedSet.of=function(){return this(arguments)},OrderedSet.fromKeys=function(s){return this(KeyedIterable(s).keySeq())},OrderedSet.prototype.toString=function(){return this.__toString(\"OrderedSet {\",\"}\")},OrderedSet.isOrderedSet=isOrderedSet;var ot,it=OrderedSet.prototype;function makeOrderedSet(s,o){var i=Object.create(it);return i.size=s?s.size:0,i._map=s,i.__ownerID=o,i}function emptyOrderedSet(){return ot||(ot=makeOrderedSet(emptyOrderedMap()))}function Stack(s){return null==s?emptyStack():isStack(s)?s:emptyStack().unshiftAll(s)}function isStack(s){return!(!s||!s[ct])}it[u]=!0,it.__empty=emptyOrderedSet,it.__make=makeOrderedSet,createClass(Stack,IndexedCollection),Stack.of=function(){return this(arguments)},Stack.prototype.toString=function(){return this.__toString(\"Stack [\",\"]\")},Stack.prototype.get=function(s,o){var i=this._head;for(s=wrapIndex(this,s);i&&s--;)i=i.next;return i?i.value:o},Stack.prototype.peek=function(){return this._head&&this._head.value},Stack.prototype.push=function(){if(0===arguments.length)return this;for(var s=this.size+arguments.length,o=this._head,i=arguments.length-1;i>=0;i--)o={value:arguments[i],next:o};return this.__ownerID?(this.size=s,this._head=o,this.__hash=void 0,this.__altered=!0,this):makeStack(s,o)},Stack.prototype.pushAll=function(s){if(0===(s=IndexedIterable(s)).size)return this;assertNotInfinite(s.size);var o=this.size,i=this._head;return s.reverse().forEach((function(s){o++,i={value:s,next:i}})),this.__ownerID?(this.size=o,this._head=i,this.__hash=void 0,this.__altered=!0,this):makeStack(o,i)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(s){return this.pushAll(s)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(s,o){if(wholeSlice(s,o,this.size))return this;var i=resolveBegin(s,this.size);if(resolveEnd(o,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,s,o);for(var a=this.size-i,u=this._head;i--;)u=u.next;return this.__ownerID?(this.size=a,this._head=u,this.__hash=void 0,this.__altered=!0,this):makeStack(a,u)},Stack.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeStack(this.size,this._head,s,this.__hash):(this.__ownerID=s,this.__altered=!1,this)},Stack.prototype.__iterate=function(s,o){if(o)return this.reverse().__iterate(s);for(var i=0,a=this._head;a&&!1!==s(a.value,i++,this);)a=a.next;return i},Stack.prototype.__iterator=function(s,o){if(o)return this.reverse().__iterator(s);var i=0,a=this._head;return new Iterator((function(){if(a){var o=a.value;return a=a.next,iteratorValue(s,i++,o)}return iteratorDone()}))},Stack.isStack=isStack;var at,ct=\"@@__IMMUTABLE_STACK__@@\",lt=Stack.prototype;function makeStack(s,o,i,a){var u=Object.create(lt);return u.size=s,u._head=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyStack(){return at||(at=makeStack(0))}function mixin(s,o){var keyCopier=function(i){s.prototype[i]=o[i]};return Object.keys(o).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(o).forEach(keyCopier),s}lt[ct]=!0,lt.withMutations=$e.withMutations,lt.asMutable=$e.asMutable,lt.asImmutable=$e.asImmutable,lt.wasAltered=$e.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var s=new Array(this.size||0);return this.valueSeq().__iterate((function(o,i){s[i]=o})),s},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map((function(s){return s&&\"function\"==typeof s.toJS?s.toJS():s})).__toJS()},toJSON:function(){return this.toSeq().map((function(s){return s&&\"function\"==typeof s.toJSON?s.toJSON():s})).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var s={};return this.__iterate((function(o,i){s[i]=o})),s},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return\"[Iterable]\"},__toString:function(s,o){return 0===this.size?s+o:s+\" \"+this.toSeq().map(this.__toStringMapper).join(\", \")+\" \"+o},concat:function(){return reify(this,concatFactory(this,s.call(arguments,0)))},includes:function(s){return this.some((function(o){return is(o,s)}))},entries:function(){return this.__iterator(V)},every:function(s,o){assertNotInfinite(this.size);var i=!0;return this.__iterate((function(a,u,_){if(!s.call(o,a,u,_))return i=!1,!1})),i},filter:function(s,o){return reify(this,filterFactory(this,s,o,!0))},find:function(s,o,i){var a=this.findEntry(s,o);return a?a[1]:i},forEach:function(s,o){return assertNotInfinite(this.size),this.__iterate(o?s.bind(o):s)},join:function(s){assertNotInfinite(this.size),s=void 0!==s?\"\"+s:\",\";var o=\"\",i=!0;return this.__iterate((function(a){i?i=!1:o+=s,o+=null!=a?a.toString():\"\"})),o},keys:function(){return this.__iterator($)},map:function(s,o){return reify(this,mapFactory(this,s,o))},reduce:function(s,o,i){var a,u;return assertNotInfinite(this.size),arguments.length<2?u=!0:a=o,this.__iterate((function(o,_,w){u?(u=!1,a=o):a=s.call(i,a,o,_,w)})),a},reduceRight:function(s,o,i){var a=this.toKeyedSeq().reverse();return a.reduce.apply(a,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(s,o){return reify(this,sliceFactory(this,s,o,!0))},some:function(s,o){return!this.every(not(s),o)},sort:function(s){return reify(this,sortFactory(this,s))},values:function(){return this.__iterator(U)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(s,o){return ensureSize(s?this.toSeq().filter(s,o):this)},countBy:function(s,o){return countByFactory(this,s,o)},equals:function(s){return deepEqual(this,s)},entrySeq:function(){var s=this;if(s._cache)return new ArraySeq(s._cache);var o=s.toSeq().map(entryMapper).toIndexedSeq();return o.fromEntrySeq=function(){return s.toSeq()},o},filterNot:function(s,o){return this.filter(not(s),o)},findEntry:function(s,o,i){var a=i;return this.__iterate((function(i,u,_){if(s.call(o,i,u,_))return a=[u,i],!1})),a},findKey:function(s,o){var i=this.findEntry(s,o);return i&&i[0]},findLast:function(s,o,i){return this.toKeyedSeq().reverse().find(s,o,i)},findLastEntry:function(s,o,i){return this.toKeyedSeq().reverse().findEntry(s,o,i)},findLastKey:function(s,o){return this.toKeyedSeq().reverse().findKey(s,o)},first:function(){return this.find(returnTrue)},flatMap:function(s,o){return reify(this,flatMapFactory(this,s,o))},flatten:function(s){return reify(this,flattenFactory(this,s,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(s,o){return this.find((function(o,i){return is(i,s)}),void 0,o)},getIn:function(s,o){for(var i,a=this,u=forceIterator(s);!(i=u.next()).done;){var _=i.value;if((a=a&&a.get?a.get(_,j):j)===j)return o}return a},groupBy:function(s,o){return groupByFactory(this,s,o)},has:function(s){return this.get(s,j)!==j},hasIn:function(s){return this.getIn(s,j)!==j},isSubset:function(s){return s=\"function\"==typeof s.includes?s:Iterable(s),this.every((function(o){return s.includes(o)}))},isSuperset:function(s){return(s=\"function\"==typeof s.isSubset?s:Iterable(s)).isSubset(this)},keyOf:function(s){return this.findKey((function(o){return is(o,s)}))},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(s){return this.toKeyedSeq().reverse().keyOf(s)},max:function(s){return maxFactory(this,s)},maxBy:function(s,o){return maxFactory(this,o,s)},min:function(s){return maxFactory(this,s?neg(s):defaultNegComparator)},minBy:function(s,o){return maxFactory(this,o?neg(o):defaultNegComparator,s)},rest:function(){return this.slice(1)},skip:function(s){return this.slice(Math.max(0,s))},skipLast:function(s){return reify(this,this.toSeq().reverse().skip(s).reverse())},skipWhile:function(s,o){return reify(this,skipWhileFactory(this,s,o,!0))},skipUntil:function(s,o){return this.skipWhile(not(s),o)},sortBy:function(s,o){return reify(this,sortFactory(this,o,s))},take:function(s){return this.slice(0,Math.max(0,s))},takeLast:function(s){return reify(this,this.toSeq().reverse().take(s).reverse())},takeWhile:function(s,o){return reify(this,takeWhileFactory(this,s,o))},takeUntil:function(s,o){return this.takeWhile(not(s),o)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=hashIterable(this))}});var ut=Iterable.prototype;ut[o]=!0,ut[Z]=ut.values,ut.__toJS=ut.toArray,ut.__toStringMapper=quoteString,ut.inspect=ut.toSource=function(){return this.toString()},ut.chain=ut.flatMap,ut.contains=ut.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(s,o){var i=this,a=0;return reify(this,this.toSeq().map((function(u,_){return s.call(o,[_,u],a++,i)})).fromEntrySeq())},mapKeys:function(s,o){var i=this;return reify(this,this.toSeq().flip().map((function(a,u){return s.call(o,a,u,i)})).flip())}});var pt=KeyedIterable.prototype;function keyMapper(s,o){return o}function entryMapper(s,o){return[o,s]}function not(s){return function(){return!s.apply(this,arguments)}}function neg(s){return function(){return-s.apply(this,arguments)}}function quoteString(s){return\"string\"==typeof s?JSON.stringify(s):String(s)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(s,o){return s<o?1:s>o?-1:0}function hashIterable(s){if(s.size===1/0)return 0;var o=isOrdered(s),i=isKeyed(s),a=o?1:0;return murmurHashOfSize(s.__iterate(i?o?function(s,o){a=31*a+hashMerge(hash(s),hash(o))|0}:function(s,o){a=a+hashMerge(hash(s),hash(o))|0}:o?function(s){a=31*a+hash(s)|0}:function(s){a=a+hash(s)|0}),a)}function murmurHashOfSize(s,o){return o=le(o,3432918353),o=le(o<<15|o>>>-15,461845907),o=le(o<<13|o>>>-13,5),o=le((o=o+3864292196^s)^o>>>16,2246822507),o=smi((o=le(o^o>>>13,3266489909))^o>>>16)}function hashMerge(s,o){return s^o+2654435769+(s<<6)+(s>>2)}return pt[i]=!0,pt[Z]=ut.entries,pt.__toJS=ut.toObject,pt.__toStringMapper=function(s,o){return JSON.stringify(o)+\": \"+quoteString(s)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(s,o){return reify(this,filterFactory(this,s,o,!1))},findIndex:function(s,o){var i=this.findEntry(s,o);return i?i[0]:-1},indexOf:function(s){var o=this.keyOf(s);return void 0===o?-1:o},lastIndexOf:function(s){var o=this.lastKeyOf(s);return void 0===o?-1:o},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(s,o){return reify(this,sliceFactory(this,s,o,!1))},splice:function(s,o){var i=arguments.length;if(o=Math.max(0|o,0),0===i||2===i&&!o)return this;s=resolveBegin(s,s<0?this.count():this.size);var a=this.slice(0,s);return reify(this,1===i?a:a.concat(arrCopy(arguments,2),this.slice(s+o)))},findLastIndex:function(s,o){var i=this.findLastEntry(s,o);return i?i[0]:-1},first:function(){return this.get(0)},flatten:function(s){return reify(this,flattenFactory(this,s,!1))},get:function(s,o){return(s=wrapIndex(this,s))<0||this.size===1/0||void 0!==this.size&&s>this.size?o:this.find((function(o,i){return i===s}),void 0,o)},has:function(s){return(s=wrapIndex(this,s))>=0&&(void 0!==this.size?this.size===1/0||s<this.size:-1!==this.indexOf(s))},interpose:function(s){return reify(this,interposeFactory(this,s))},interleave:function(){var s=[this].concat(arrCopy(arguments)),o=zipWithFactory(this.toSeq(),IndexedSeq.of,s),i=o.flatten(!0);return o.size&&(i.size=o.size*s.length),reify(this,i)},keySeq:function(){return Range(0,this.size)},last:function(){return this.get(-1)},skipWhile:function(s,o){return reify(this,skipWhileFactory(this,s,o,!1))},zip:function(){return reify(this,zipWithFactory(this,defaultZipper,[this].concat(arrCopy(arguments))))},zipWith:function(s){var o=arrCopy(arguments);return o[0]=this,reify(this,zipWithFactory(this,s,o))}}),IndexedIterable.prototype[a]=!0,IndexedIterable.prototype[u]=!0,mixin(SetIterable,{get:function(s,o){return this.has(s)?s:o},includes:function(s){return this.has(s)},keySeq:function(){return this.valueSeq()}}),SetIterable.prototype.has=ut.includes,SetIterable.prototype.contains=SetIterable.prototype.includes,mixin(KeyedSeq,KeyedIterable.prototype),mixin(IndexedSeq,IndexedIterable.prototype),mixin(SetSeq,SetIterable.prototype),mixin(KeyedCollection,KeyedIterable.prototype),mixin(IndexedCollection,IndexedIterable.prototype),mixin(SetCollection,SetIterable.prototype),{Iterable,Seq,Collection,Map,OrderedMap,List,Stack,Set,OrderedSet,Record,Range,Repeat,is,fromJS}}()},9748:(s,o,i)=>{\"use strict\";i(71340);var a=i(92046);s.exports=a.Object.assign},9957:(s,o,i)=>{\"use strict\";var a=Function.prototype.call,u=Object.prototype.hasOwnProperty,_=i(66743);s.exports=_.call(a,u)},9999:(s,o,i)=>{var a=i(37217),u=i(83729),_=i(16547),w=i(74733),x=i(43838),C=i(93290),j=i(23007),L=i(92271),B=i(48948),$=i(50002),U=i(83349),V=i(5861),z=i(76189),Y=i(77199),Z=i(35529),ee=i(56449),ie=i(3656),ae=i(87730),ce=i(23805),le=i(38440),pe=i(95950),de=i(37241),fe=\"[object Arguments]\",ye=\"[object Function]\",be=\"[object Object]\",_e={};_e[fe]=_e[\"[object Array]\"]=_e[\"[object ArrayBuffer]\"]=_e[\"[object DataView]\"]=_e[\"[object Boolean]\"]=_e[\"[object Date]\"]=_e[\"[object Float32Array]\"]=_e[\"[object Float64Array]\"]=_e[\"[object Int8Array]\"]=_e[\"[object Int16Array]\"]=_e[\"[object Int32Array]\"]=_e[\"[object Map]\"]=_e[\"[object Number]\"]=_e[be]=_e[\"[object RegExp]\"]=_e[\"[object Set]\"]=_e[\"[object String]\"]=_e[\"[object Symbol]\"]=_e[\"[object Uint8Array]\"]=_e[\"[object Uint8ClampedArray]\"]=_e[\"[object Uint16Array]\"]=_e[\"[object Uint32Array]\"]=!0,_e[\"[object Error]\"]=_e[ye]=_e[\"[object WeakMap]\"]=!1,s.exports=function baseClone(s,o,i,Se,we,xe){var Pe,Te=1&o,Re=2&o,$e=4&o;if(i&&(Pe=we?i(s,Se,we,xe):i(s)),void 0!==Pe)return Pe;if(!ce(s))return s;var qe=ee(s);if(qe){if(Pe=z(s),!Te)return j(s,Pe)}else{var ze=V(s),We=ze==ye||\"[object GeneratorFunction]\"==ze;if(ie(s))return C(s,Te);if(ze==be||ze==fe||We&&!we){if(Pe=Re||We?{}:Z(s),!Te)return Re?B(s,x(Pe,s)):L(s,w(Pe,s))}else{if(!_e[ze])return we?s:{};Pe=Y(s,ze,Te)}}xe||(xe=new a);var He=xe.get(s);if(He)return He;xe.set(s,Pe),le(s)?s.forEach((function(a){Pe.add(baseClone(a,o,i,a,s,xe))})):ae(s)&&s.forEach((function(a,u){Pe.set(u,baseClone(a,o,i,u,s,xe))}));var Ye=qe?void 0:($e?Re?U:$:Re?de:pe)(s);return u(Ye||s,(function(a,u){Ye&&(a=s[u=a]),_(Pe,u,baseClone(a,o,i,u,s,xe))})),Pe}},10023:(s,o,i)=>{const a=i(6205),INTS=()=>[{type:a.RANGE,from:48,to:57}],WORDS=()=>[{type:a.CHAR,value:95},{type:a.RANGE,from:97,to:122},{type:a.RANGE,from:65,to:90}].concat(INTS()),WHITESPACE=()=>[{type:a.CHAR,value:9},{type:a.CHAR,value:10},{type:a.CHAR,value:11},{type:a.CHAR,value:12},{type:a.CHAR,value:13},{type:a.CHAR,value:32},{type:a.CHAR,value:160},{type:a.CHAR,value:5760},{type:a.RANGE,from:8192,to:8202},{type:a.CHAR,value:8232},{type:a.CHAR,value:8233},{type:a.CHAR,value:8239},{type:a.CHAR,value:8287},{type:a.CHAR,value:12288},{type:a.CHAR,value:65279}];o.words=()=>({type:a.SET,set:WORDS(),not:!1}),o.notWords=()=>({type:a.SET,set:WORDS(),not:!0}),o.ints=()=>({type:a.SET,set:INTS(),not:!1}),o.notInts=()=>({type:a.SET,set:INTS(),not:!0}),o.whitespace=()=>({type:a.SET,set:WHITESPACE(),not:!1}),o.notWhitespace=()=>({type:a.SET,set:WHITESPACE(),not:!0}),o.anyChar=()=>({type:a.SET,set:[{type:a.CHAR,value:10},{type:a.CHAR,value:13},{type:a.CHAR,value:8232},{type:a.CHAR,value:8233}],not:!0})},10043:(s,o,i)=>{\"use strict\";var a=i(54018),u=String,_=TypeError;s.exports=function(s){if(a(s))return s;throw new _(\"Can't set \"+u(s)+\" as a prototype\")}},10076:s=>{\"use strict\";s.exports=Function.prototype.call},10124:(s,o,i)=>{var a=i(9325);s.exports=function(){return a.Date.now()}},10300:(s,o,i)=>{\"use strict\";var a=i(13930),u=i(82159),_=i(36624),w=i(4640),x=i(73448),C=TypeError;s.exports=function(s,o){var i=arguments.length<2?x(s):o;if(u(i))return _(a(i,s));throw new C(w(s)+\" is not iterable\")}},10316:(s,o,i)=>{const a=i(2404),u=i(55973),_=i(92340);class Element{constructor(s,o,i){o&&(this.meta=o),i&&(this.attributes=i),this.content=s}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((s=>{s.parent=this,s.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const s=new this.constructor;return s.element=this.element,this.meta.length&&(s._meta=this.meta.clone()),this.attributes.length&&(s._attributes=this.attributes.clone()),this.content?this.content.clone?s.content=this.content.clone():Array.isArray(this.content)?s.content=this.content.map((s=>s.clone())):s.content=this.content:s.content=this.content,s}toValue(){return this.content instanceof Element?this.content.toValue():this.content instanceof u?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((s=>s.toValue()),this):this.content}toRef(s){if(\"\"===this.id.toValue())throw Error(\"Cannot create reference to an element that does not contain an ID\");const o=new this.RefElement(this.id.toValue());return s&&(o.path=s),o}findRecursive(...s){if(arguments.length>1&&!this.isFrozen)throw new Error(\"Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`\");const o=s.pop();let i=new _;const append=(s,o)=>(s.push(o),s),checkElement=(s,i)=>{i.element===o&&s.push(i);const a=i.findRecursive(o);return a&&a.reduce(append,s),i.content instanceof u&&(i.content.key&&checkElement(s,i.content.key),i.content.value&&checkElement(s,i.content.value)),s};return this.content&&(this.content.element&&checkElement(i,this.content),Array.isArray(this.content)&&this.content.reduce(checkElement,i)),s.isEmpty||(i=i.filter((o=>{let i=o.parents.map((s=>s.element));for(const o in s){const a=s[o],u=i.indexOf(a);if(-1===u)return!1;i=i.splice(0,u)}return!0}))),i}set(s){return this.content=s,this}equals(s){return a(this.toValue(),s)}getMetaProperty(s,o){if(!this.meta.hasKey(s)){if(this.isFrozen){const s=this.refract(o);return s.freeze(),s}this.meta.set(s,o)}return this.meta.get(s)}setMetaProperty(s,o){this.meta.set(s,o)}get element(){return this._storedElement||\"element\"}set element(s){this._storedElement=s}get content(){return this._content}set content(s){if(s instanceof Element)this._content=s;else if(s instanceof _)this.content=s.elements;else if(\"string\"==typeof s||\"number\"==typeof s||\"boolean\"==typeof s||\"null\"===s||null==s)this._content=s;else if(s instanceof u)this._content=s;else if(Array.isArray(s))this._content=s.map(this.refract);else{if(\"object\"!=typeof s)throw new Error(\"Cannot set content to given value\");this._content=Object.keys(s).map((o=>new this.MemberElement(o,s[o])))}}get meta(){if(!this._meta){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._meta=new this.ObjectElement}return this._meta}set meta(s){s instanceof this.ObjectElement?this._meta=s:this.meta.set(s||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._attributes=new this.ObjectElement}return this._attributes}set attributes(s){s instanceof this.ObjectElement?this._attributes=s:this.attributes.set(s||{})}get id(){return this.getMetaProperty(\"id\",\"\")}set id(s){this.setMetaProperty(\"id\",s)}get classes(){return this.getMetaProperty(\"classes\",[])}set classes(s){this.setMetaProperty(\"classes\",s)}get title(){return this.getMetaProperty(\"title\",\"\")}set title(s){this.setMetaProperty(\"title\",s)}get description(){return this.getMetaProperty(\"description\",\"\")}set description(s){this.setMetaProperty(\"description\",s)}get links(){return this.getMetaProperty(\"links\",[])}set links(s){this.setMetaProperty(\"links\",s)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:s}=this;const o=new _;for(;s;)o.push(s),s=s.parent;return o}get children(){if(Array.isArray(this.content))return new _(this.content);if(this.content instanceof u){const s=new _([this.content.key]);return this.content.value&&s.push(this.content.value),s}return this.content instanceof Element?new _([this.content]):new _}get recursiveChildren(){const s=new _;return this.children.forEach((o=>{s.push(o),o.recursiveChildren.forEach((o=>{s.push(o)}))})),s}}s.exports=Element},10392:s=>{s.exports=function getValue(s,o){return null==s?void 0:s[o]}},10487:(s,o,i)=>{\"use strict\";var a=i(96897),u=i(30655),_=i(73126),w=i(12205);s.exports=function callBind(s){var o=_(arguments),i=s.length-(arguments.length-1);return a(o,1+(i>0?i:0),!0)},u?u(s.exports,\"apply\",{value:w}):s.exports.apply=w},10776:(s,o,i)=>{var a=i(30756),u=i(95950);s.exports=function getMatchData(s){for(var o=u(s),i=o.length;i--;){var _=o[i],w=s[_];o[i]=[_,w,a(w)]}return o}},10866:(s,o,i)=>{const a=i(6048),u=i(92340);class ObjectSlice extends u{map(s,o){return this.elements.map((i=>s.bind(o)(i.value,i.key,i)))}filter(s,o){return new ObjectSlice(this.elements.filter((i=>s.bind(o)(i.value,i.key,i))))}reject(s,o){return this.filter(a(s.bind(o)))}forEach(s,o){return this.elements.forEach(((i,a)=>{s.bind(o)(i.value,i.key,i,a)}))}keys(){return this.map(((s,o)=>o.toValue()))}values(){return this.map((s=>s.toValue()))}}s.exports=ObjectSlice},11002:s=>{\"use strict\";s.exports=Function.prototype.apply},11042:(s,o,i)=>{\"use strict\";var a=i(85582),u=i(1907),_=i(24443),w=i(87170),x=i(36624),C=u([].concat);s.exports=a(\"Reflect\",\"ownKeys\")||function ownKeys(s){var o=_.f(x(s)),i=w.f;return i?C(o,i(s)):o}},11091:(s,o,i)=>{\"use strict\";var a=i(45951),u=i(76024),_=i(92361),w=i(62250),x=i(13846).f,C=i(7463),j=i(92046),L=i(28311),B=i(61626),$=i(49724);i(36128);var wrapConstructor=function(s){var Wrapper=function(o,i,a){if(this instanceof Wrapper){switch(arguments.length){case 0:return new s;case 1:return new s(o);case 2:return new s(o,i)}return new s(o,i,a)}return u(s,this,arguments)};return Wrapper.prototype=s.prototype,Wrapper};s.exports=function(s,o){var i,u,U,V,z,Y,Z,ee,ie,ae=s.target,ce=s.global,le=s.stat,pe=s.proto,de=ce?a:le?a[ae]:a[ae]&&a[ae].prototype,fe=ce?j:j[ae]||B(j,ae,{})[ae],ye=fe.prototype;for(V in o)u=!(i=C(ce?V:ae+(le?\".\":\"#\")+V,s.forced))&&de&&$(de,V),Y=fe[V],u&&(Z=s.dontCallGetSet?(ie=x(de,V))&&ie.value:de[V]),z=u&&Z?Z:o[V],(i||pe||typeof Y!=typeof z)&&(ee=s.bind&&u?L(z,a):s.wrap&&u?wrapConstructor(z):pe&&w(z)?_(z):z,(s.sham||z&&z.sham||Y&&Y.sham)&&B(ee,\"sham\",!0),B(fe,V,ee),pe&&($(j,U=ae+\"Prototype\")||B(j,U,{}),B(j[U],V,z),s.real&&ye&&(i||!ye[V])&&B(ye,V,z)))}},11287:s=>{s.exports=function getHolder(s){return s.placeholder}},11331:(s,o,i)=>{var a=i(72552),u=i(28879),_=i(40346),w=Function.prototype,x=Object.prototype,C=w.toString,j=x.hasOwnProperty,L=C.call(Object);s.exports=function isPlainObject(s){if(!_(s)||\"[object Object]\"!=a(s))return!1;var o=u(s);if(null===o)return!0;var i=j.call(o,\"constructor\")&&o.constructor;return\"function\"==typeof i&&i instanceof i&&C.call(i)==L}},11470:(s,o,i)=>{\"use strict\";var a=i(1907),u=i(65482),_=i(90160),w=i(74239),x=a(\"\".charAt),C=a(\"\".charCodeAt),j=a(\"\".slice),createMethod=function(s){return function(o,i){var a,L,B=_(w(o)),$=u(i),U=B.length;return $<0||$>=U?s?\"\":void 0:(a=C(B,$))<55296||a>56319||$+1===U||(L=C(B,$+1))<56320||L>57343?s?x(B,$):a:s?j(B,$,$+2):L-56320+(a-55296<<10)+65536}};s.exports={codeAt:createMethod(!1),charAt:createMethod(!0)}},11842:(s,o,i)=>{var a=i(82819),u=i(9325);s.exports=function createBind(s,o,i){var _=1&o,w=a(s);return function wrapper(){return(this&&this!==u&&this instanceof wrapper?w:s).apply(_?i:this,arguments)}}},12205:(s,o,i)=>{\"use strict\";var a=i(66743),u=i(11002),_=i(13144);s.exports=function applyBind(){return _(a,u,arguments)}},12242:(s,o,i)=>{const a=i(10316);s.exports=class BooleanElement extends a{constructor(s,o,i){super(s,o,i),this.element=\"boolean\"}primitive(){return\"boolean\"}}},12507:(s,o,i)=>{var a=i(28754),u=i(49698),_=i(63912),w=i(13222);s.exports=function createCaseFirst(s){return function(o){o=w(o);var i=u(o)?_(o):void 0,x=i?i[0]:o.charAt(0),C=i?a(i,1).join(\"\"):o.slice(1);return x[s]()+C}}},12560:(s,o,i)=>{\"use strict\";i(99363);var a=i(19287),u=i(45951),_=i(14840),w=i(93742);for(var x in a)_(u[x],x),w[x]=w.Array},12651:(s,o,i)=>{var a=i(74218);s.exports=function getMapData(s,o){var i=s.__data__;return a(o)?i[\"string\"==typeof o?\"string\":\"hash\"]:i.map}},12749:(s,o,i)=>{var a=i(81042),u=Object.prototype.hasOwnProperty;s.exports=function hashHas(s){var o=this.__data__;return a?void 0!==o[s]:u.call(o,s)}},13144:(s,o,i)=>{\"use strict\";var a=i(66743),u=i(11002),_=i(10076),w=i(47119);s.exports=w||a.call(_,u)},13222:(s,o,i)=>{var a=i(77556);s.exports=function toString(s){return null==s?\"\":a(s)}},13846:(s,o,i)=>{\"use strict\";var a=i(39447),u=i(13930),_=i(22574),w=i(75817),x=i(4993),C=i(70470),j=i(49724),L=i(73648),B=Object.getOwnPropertyDescriptor;o.f=a?B:function getOwnPropertyDescriptor(s,o){if(s=x(s),o=C(o),L)try{return B(s,o)}catch(s){}if(j(s,o))return w(!u(_.f,s,o),s[o])}},13930:(s,o,i)=>{\"use strict\";var a=i(41505),u=Function.prototype.call;s.exports=a?u.bind(u):function(){return u.apply(u,arguments)}},14248:s=>{s.exports=function arraySome(s,o){for(var i=-1,a=null==s?0:s.length;++i<a;)if(o(s[i],i,s))return!0;return!1}},14528:s=>{s.exports=function arrayPush(s,o){for(var i=-1,a=o.length,u=s.length;++i<a;)s[u+i]=o[i];return s}},14540:(s,o,i)=>{const a=i(10316);s.exports=class RefElement extends a{constructor(s,o,i){super(s||[],o,i),this.element=\"ref\",this.path||(this.path=\"element\")}get path(){return this.attributes.get(\"path\")}set path(s){this.attributes.set(\"path\",s)}}},14744:s=>{\"use strict\";var o=function isMergeableObject(s){return function isNonNullObject(s){return!!s&&\"object\"==typeof s}(s)&&!function isSpecial(s){var o=Object.prototype.toString.call(s);return\"[object RegExp]\"===o||\"[object Date]\"===o||function isReactElement(s){return s.$$typeof===i}(s)}(s)};var i=\"function\"==typeof Symbol&&Symbol.for?Symbol.for(\"react.element\"):60103;function cloneUnlessOtherwiseSpecified(s,o){return!1!==o.clone&&o.isMergeableObject(s)?deepmerge(function emptyTarget(s){return Array.isArray(s)?[]:{}}(s),s,o):s}function defaultArrayMerge(s,o,i){return s.concat(o).map((function(s){return cloneUnlessOtherwiseSpecified(s,i)}))}function getKeys(s){return Object.keys(s).concat(function getEnumerableOwnPropertySymbols(s){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(s).filter((function(o){return Object.propertyIsEnumerable.call(s,o)})):[]}(s))}function propertyIsOnObject(s,o){try{return o in s}catch(s){return!1}}function mergeObject(s,o,i){var a={};return i.isMergeableObject(s)&&getKeys(s).forEach((function(o){a[o]=cloneUnlessOtherwiseSpecified(s[o],i)})),getKeys(o).forEach((function(u){(function propertyIsUnsafe(s,o){return propertyIsOnObject(s,o)&&!(Object.hasOwnProperty.call(s,o)&&Object.propertyIsEnumerable.call(s,o))})(s,u)||(propertyIsOnObject(s,u)&&i.isMergeableObject(o[u])?a[u]=function getMergeFunction(s,o){if(!o.customMerge)return deepmerge;var i=o.customMerge(s);return\"function\"==typeof i?i:deepmerge}(u,i)(s[u],o[u],i):a[u]=cloneUnlessOtherwiseSpecified(o[u],i))})),a}function deepmerge(s,i,a){(a=a||{}).arrayMerge=a.arrayMerge||defaultArrayMerge,a.isMergeableObject=a.isMergeableObject||o,a.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var u=Array.isArray(i);return u===Array.isArray(s)?u?a.arrayMerge(s,i,a):mergeObject(s,i,a):cloneUnlessOtherwiseSpecified(i,a)}deepmerge.all=function deepmergeAll(s,o){if(!Array.isArray(s))throw new Error(\"first argument should be an array\");return s.reduce((function(s,i){return deepmerge(s,i,o)}),{})};var a=deepmerge;s.exports=a},14792:(s,o,i)=>{var a=i(13222),u=i(55808);s.exports=function capitalize(s){return u(a(s).toLowerCase())}},14840:(s,o,i)=>{\"use strict\";var a=i(52623),u=i(74284).f,_=i(61626),w=i(49724),x=i(54878),C=i(76264)(\"toStringTag\");s.exports=function(s,o,i,j){var L=i?s:s&&s.prototype;L&&(w(L,C)||u(L,C,{configurable:!0,value:o}),j&&!a&&_(L,\"toString\",x))}},14974:s=>{s.exports=function safeGet(s,o){if((\"constructor\"!==o||\"function\"!=typeof s[o])&&\"__proto__\"!=o)return s[o]}},15287:(s,o)=>{\"use strict\";var i=Symbol.for(\"react.element\"),a=Symbol.for(\"react.portal\"),u=Symbol.for(\"react.fragment\"),_=Symbol.for(\"react.strict_mode\"),w=Symbol.for(\"react.profiler\"),x=Symbol.for(\"react.provider\"),C=Symbol.for(\"react.context\"),j=Symbol.for(\"react.forward_ref\"),L=Symbol.for(\"react.suspense\"),B=Symbol.for(\"react.memo\"),$=Symbol.for(\"react.lazy\"),U=Symbol.iterator;var V={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},z=Object.assign,Y={};function E(s,o,i){this.props=s,this.context=o,this.refs=Y,this.updater=i||V}function F(){}function G(s,o,i){this.props=s,this.context=o,this.refs=Y,this.updater=i||V}E.prototype.isReactComponent={},E.prototype.setState=function(s,o){if(\"object\"!=typeof s&&\"function\"!=typeof s&&null!=s)throw Error(\"setState(...): takes an object of state variables to update or a function which returns an object of state variables.\");this.updater.enqueueSetState(this,s,o,\"setState\")},E.prototype.forceUpdate=function(s){this.updater.enqueueForceUpdate(this,s,\"forceUpdate\")},F.prototype=E.prototype;var Z=G.prototype=new F;Z.constructor=G,z(Z,E.prototype),Z.isPureReactComponent=!0;var ee=Array.isArray,ie=Object.prototype.hasOwnProperty,ae={current:null},ce={key:!0,ref:!0,__self:!0,__source:!0};function M(s,o,a){var u,_={},w=null,x=null;if(null!=o)for(u in void 0!==o.ref&&(x=o.ref),void 0!==o.key&&(w=\"\"+o.key),o)ie.call(o,u)&&!ce.hasOwnProperty(u)&&(_[u]=o[u]);var C=arguments.length-2;if(1===C)_.children=a;else if(1<C){for(var j=Array(C),L=0;L<C;L++)j[L]=arguments[L+2];_.children=j}if(s&&s.defaultProps)for(u in C=s.defaultProps)void 0===_[u]&&(_[u]=C[u]);return{$$typeof:i,type:s,key:w,ref:x,props:_,_owner:ae.current}}function O(s){return\"object\"==typeof s&&null!==s&&s.$$typeof===i}var le=/\\/+/g;function Q(s,o){return\"object\"==typeof s&&null!==s&&null!=s.key?function escape(s){var o={\"=\":\"=0\",\":\":\"=2\"};return\"$\"+s.replace(/[=:]/g,(function(s){return o[s]}))}(\"\"+s.key):o.toString(36)}function R(s,o,u,_,w){var x=typeof s;\"undefined\"!==x&&\"boolean\"!==x||(s=null);var C=!1;if(null===s)C=!0;else switch(x){case\"string\":case\"number\":C=!0;break;case\"object\":switch(s.$$typeof){case i:case a:C=!0}}if(C)return w=w(C=s),s=\"\"===_?\".\"+Q(C,0):_,ee(w)?(u=\"\",null!=s&&(u=s.replace(le,\"$&/\")+\"/\"),R(w,o,u,\"\",(function(s){return s}))):null!=w&&(O(w)&&(w=function N(s,o){return{$$typeof:i,type:s.type,key:o,ref:s.ref,props:s.props,_owner:s._owner}}(w,u+(!w.key||C&&C.key===w.key?\"\":(\"\"+w.key).replace(le,\"$&/\")+\"/\")+s)),o.push(w)),1;if(C=0,_=\"\"===_?\".\":_+\":\",ee(s))for(var j=0;j<s.length;j++){var L=_+Q(x=s[j],j);C+=R(x,o,u,L,w)}else if(L=function A(s){return null===s||\"object\"!=typeof s?null:\"function\"==typeof(s=U&&s[U]||s[\"@@iterator\"])?s:null}(s),\"function\"==typeof L)for(s=L.call(s),j=0;!(x=s.next()).done;)C+=R(x=x.value,o,u,L=_+Q(x,j++),w);else if(\"object\"===x)throw o=String(s),Error(\"Objects are not valid as a React child (found: \"+(\"[object Object]\"===o?\"object with keys {\"+Object.keys(s).join(\", \")+\"}\":o)+\"). If you meant to render a collection of children, use an array instead.\");return C}function S(s,o,i){if(null==s)return s;var a=[],u=0;return R(s,a,\"\",\"\",(function(s){return o.call(i,s,u++)})),a}function T(s){if(-1===s._status){var o=s._result;(o=o()).then((function(o){0!==s._status&&-1!==s._status||(s._status=1,s._result=o)}),(function(o){0!==s._status&&-1!==s._status||(s._status=2,s._result=o)})),-1===s._status&&(s._status=0,s._result=o)}if(1===s._status)return s._result.default;throw s._result}var pe={current:null},de={transition:null},fe={ReactCurrentDispatcher:pe,ReactCurrentBatchConfig:de,ReactCurrentOwner:ae};function X(){throw Error(\"act(...) is not supported in production builds of React.\")}o.Children={map:S,forEach:function(s,o,i){S(s,(function(){o.apply(this,arguments)}),i)},count:function(s){var o=0;return S(s,(function(){o++})),o},toArray:function(s){return S(s,(function(s){return s}))||[]},only:function(s){if(!O(s))throw Error(\"React.Children.only expected to receive a single React element child.\");return s}},o.Component=E,o.Fragment=u,o.Profiler=w,o.PureComponent=G,o.StrictMode=_,o.Suspense=L,o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=fe,o.act=X,o.cloneElement=function(s,o,a){if(null==s)throw Error(\"React.cloneElement(...): The argument must be a React element, but you passed \"+s+\".\");var u=z({},s.props),_=s.key,w=s.ref,x=s._owner;if(null!=o){if(void 0!==o.ref&&(w=o.ref,x=ae.current),void 0!==o.key&&(_=\"\"+o.key),s.type&&s.type.defaultProps)var C=s.type.defaultProps;for(j in o)ie.call(o,j)&&!ce.hasOwnProperty(j)&&(u[j]=void 0===o[j]&&void 0!==C?C[j]:o[j])}var j=arguments.length-2;if(1===j)u.children=a;else if(1<j){C=Array(j);for(var L=0;L<j;L++)C[L]=arguments[L+2];u.children=C}return{$$typeof:i,type:s.type,key:_,ref:w,props:u,_owner:x}},o.createContext=function(s){return(s={$$typeof:C,_currentValue:s,_currentValue2:s,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null}).Provider={$$typeof:x,_context:s},s.Consumer=s},o.createElement=M,o.createFactory=function(s){var o=M.bind(null,s);return o.type=s,o},o.createRef=function(){return{current:null}},o.forwardRef=function(s){return{$$typeof:j,render:s}},o.isValidElement=O,o.lazy=function(s){return{$$typeof:$,_payload:{_status:-1,_result:s},_init:T}},o.memo=function(s,o){return{$$typeof:B,type:s,compare:void 0===o?null:o}},o.startTransition=function(s){var o=de.transition;de.transition={};try{s()}finally{de.transition=o}},o.unstable_act=X,o.useCallback=function(s,o){return pe.current.useCallback(s,o)},o.useContext=function(s){return pe.current.useContext(s)},o.useDebugValue=function(){},o.useDeferredValue=function(s){return pe.current.useDeferredValue(s)},o.useEffect=function(s,o){return pe.current.useEffect(s,o)},o.useId=function(){return pe.current.useId()},o.useImperativeHandle=function(s,o,i){return pe.current.useImperativeHandle(s,o,i)},o.useInsertionEffect=function(s,o){return pe.current.useInsertionEffect(s,o)},o.useLayoutEffect=function(s,o){return pe.current.useLayoutEffect(s,o)},o.useMemo=function(s,o){return pe.current.useMemo(s,o)},o.useReducer=function(s,o,i){return pe.current.useReducer(s,o,i)},o.useRef=function(s){return pe.current.useRef(s)},o.useState=function(s){return pe.current.useState(s)},o.useSyncExternalStore=function(s,o,i){return pe.current.useSyncExternalStore(s,o,i)},o.useTransition=function(){return pe.current.useTransition()},o.version=\"18.3.1\"},15325:(s,o,i)=>{var a=i(96131);s.exports=function arrayIncludes(s,o){return!!(null==s?0:s.length)&&a(s,o,0)>-1}},15340:()=>{},15377:(s,o,i)=>{\"use strict\";var a=i(92861).Buffer,u=i(64634),_=i(74372),w=ArrayBuffer.isView||function isView(s){try{return _(s),!0}catch(s){return!1}},x=\"undefined\"!=typeof Uint8Array,C=\"undefined\"!=typeof ArrayBuffer&&\"undefined\"!=typeof Uint8Array,j=C&&(a.prototype instanceof Uint8Array||a.TYPED_ARRAY_SUPPORT);s.exports=function toBuffer(s,o){if(s instanceof a)return s;if(\"string\"==typeof s)return a.from(s,o);if(C&&w(s)){if(0===s.byteLength)return a.alloc(0);if(j){var i=a.from(s.buffer,s.byteOffset,s.byteLength);if(i.byteLength===s.byteLength)return i}var _=s instanceof Uint8Array?s:new Uint8Array(s.buffer,s.byteOffset,s.byteLength),L=a.from(_);if(L.length===s.byteLength)return L}if(x&&s instanceof Uint8Array)return a.from(s);var B=u(s);if(B)for(var $=0;$<s.length;$+=1){var U=s[$];if(\"number\"!=typeof U||U<0||U>255||~~U!==U)throw new RangeError(\"Array items must be numbers in the range 0-255.\")}if(B||a.isBuffer(s)&&s.constructor&&\"function\"==typeof s.constructor.isBuffer&&s.constructor.isBuffer(s))return a.from(s);throw new TypeError('The \"data\" argument must be a string, an Array, a Buffer, a Uint8Array, or a DataView.')}},15389:(s,o,i)=>{var a=i(93663),u=i(87978),_=i(83488),w=i(56449),x=i(50583);s.exports=function baseIteratee(s){return\"function\"==typeof s?s:null==s?_:\"object\"==typeof s?w(s)?u(s[0],s[1]):a(s):x(s)}},15972:(s,o,i)=>{\"use strict\";var a=i(49724),u=i(62250),_=i(39298),w=i(92522),x=i(57382),C=w(\"IE_PROTO\"),j=Object,L=j.prototype;s.exports=x?j.getPrototypeOf:function(s){var o=_(s);if(a(o,C))return o[C];var i=o.constructor;return u(i)&&o instanceof i?i.prototype:o instanceof j?L:null}},16038:(s,o,i)=>{var a=i(5861),u=i(40346);s.exports=function baseIsSet(s){return u(s)&&\"[object Set]\"==a(s)}},16426:s=>{s.exports=function(){var s=document.getSelection();if(!s.rangeCount)return function(){};for(var o=document.activeElement,i=[],a=0;a<s.rangeCount;a++)i.push(s.getRangeAt(a));switch(o.tagName.toUpperCase()){case\"INPUT\":case\"TEXTAREA\":o.blur();break;default:o=null}return s.removeAllRanges(),function(){\"Caret\"===s.type&&s.removeAllRanges(),s.rangeCount||i.forEach((function(o){s.addRange(o)})),o&&o.focus()}}},16547:(s,o,i)=>{var a=i(43360),u=i(75288),_=Object.prototype.hasOwnProperty;s.exports=function assignValue(s,o,i){var w=s[o];_.call(s,o)&&u(w,i)&&(void 0!==i||o in s)||a(s,o,i)}},16708:(s,o,i)=>{\"use strict\";var a,u=i(65606);function CorkedRequest(s){var o=this;this.next=null,this.entry=null,this.finish=function(){!function onCorkedFinish(s,o,i){var a=s.entry;s.entry=null;for(;a;){var u=a.callback;o.pendingcb--,u(i),a=a.next}o.corkedRequestsFree.next=s}(o,s)}}s.exports=Writable,Writable.WritableState=WritableState;var _={deprecate:i(94643)},w=i(40345),x=i(48287).Buffer,C=(void 0!==i.g?i.g:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:{}).Uint8Array||function(){};var j,L=i(75896),B=i(65291).getHighWaterMark,$=i(86048).F,U=$.ERR_INVALID_ARG_TYPE,V=$.ERR_METHOD_NOT_IMPLEMENTED,z=$.ERR_MULTIPLE_CALLBACK,Y=$.ERR_STREAM_CANNOT_PIPE,Z=$.ERR_STREAM_DESTROYED,ee=$.ERR_STREAM_NULL_VALUES,ie=$.ERR_STREAM_WRITE_AFTER_END,ae=$.ERR_UNKNOWN_ENCODING,ce=L.errorOrDestroy;function nop(){}function WritableState(s,o,_){a=a||i(25382),s=s||{},\"boolean\"!=typeof _&&(_=o instanceof a),this.objectMode=!!s.objectMode,_&&(this.objectMode=this.objectMode||!!s.writableObjectMode),this.highWaterMark=B(this,s,\"writableHighWaterMark\",_),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var w=!1===s.decodeStrings;this.decodeStrings=!w,this.defaultEncoding=s.defaultEncoding||\"utf8\",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(s){!function onwrite(s,o){var i=s._writableState,a=i.sync,_=i.writecb;if(\"function\"!=typeof _)throw new z;if(function onwriteStateUpdate(s){s.writing=!1,s.writecb=null,s.length-=s.writelen,s.writelen=0}(i),o)!function onwriteError(s,o,i,a,_){--o.pendingcb,i?(u.nextTick(_,a),u.nextTick(finishMaybe,s,o),s._writableState.errorEmitted=!0,ce(s,a)):(_(a),s._writableState.errorEmitted=!0,ce(s,a),finishMaybe(s,o))}(s,i,a,o,_);else{var w=needFinish(i)||s.destroyed;w||i.corked||i.bufferProcessing||!i.bufferedRequest||clearBuffer(s,i),a?u.nextTick(afterWrite,s,i,w,_):afterWrite(s,i,w,_)}}(o,s)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.emitClose=!1!==s.emitClose,this.autoDestroy=!!s.autoDestroy,this.bufferedRequestCount=0,this.corkedRequestsFree=new CorkedRequest(this)}function Writable(s){var o=this instanceof(a=a||i(25382));if(!o&&!j.call(Writable,this))return new Writable(s);this._writableState=new WritableState(s,this,o),this.writable=!0,s&&(\"function\"==typeof s.write&&(this._write=s.write),\"function\"==typeof s.writev&&(this._writev=s.writev),\"function\"==typeof s.destroy&&(this._destroy=s.destroy),\"function\"==typeof s.final&&(this._final=s.final)),w.call(this)}function doWrite(s,o,i,a,u,_,w){o.writelen=a,o.writecb=w,o.writing=!0,o.sync=!0,o.destroyed?o.onwrite(new Z(\"write\")):i?s._writev(u,o.onwrite):s._write(u,_,o.onwrite),o.sync=!1}function afterWrite(s,o,i,a){i||function onwriteDrain(s,o){0===o.length&&o.needDrain&&(o.needDrain=!1,s.emit(\"drain\"))}(s,o),o.pendingcb--,a(),finishMaybe(s,o)}function clearBuffer(s,o){o.bufferProcessing=!0;var i=o.bufferedRequest;if(s._writev&&i&&i.next){var a=o.bufferedRequestCount,u=new Array(a),_=o.corkedRequestsFree;_.entry=i;for(var w=0,x=!0;i;)u[w]=i,i.isBuf||(x=!1),i=i.next,w+=1;u.allBuffers=x,doWrite(s,o,!0,o.length,u,\"\",_.finish),o.pendingcb++,o.lastBufferedRequest=null,_.next?(o.corkedRequestsFree=_.next,_.next=null):o.corkedRequestsFree=new CorkedRequest(o),o.bufferedRequestCount=0}else{for(;i;){var C=i.chunk,j=i.encoding,L=i.callback;if(doWrite(s,o,!1,o.objectMode?1:C.length,C,j,L),i=i.next,o.bufferedRequestCount--,o.writing)break}null===i&&(o.lastBufferedRequest=null)}o.bufferedRequest=i,o.bufferProcessing=!1}function needFinish(s){return s.ending&&0===s.length&&null===s.bufferedRequest&&!s.finished&&!s.writing}function callFinal(s,o){s._final((function(i){o.pendingcb--,i&&ce(s,i),o.prefinished=!0,s.emit(\"prefinish\"),finishMaybe(s,o)}))}function finishMaybe(s,o){var i=needFinish(o);if(i&&(function prefinish(s,o){o.prefinished||o.finalCalled||(\"function\"!=typeof s._final||o.destroyed?(o.prefinished=!0,s.emit(\"prefinish\")):(o.pendingcb++,o.finalCalled=!0,u.nextTick(callFinal,s,o)))}(s,o),0===o.pendingcb&&(o.finished=!0,s.emit(\"finish\"),o.autoDestroy))){var a=s._readableState;(!a||a.autoDestroy&&a.endEmitted)&&s.destroy()}return i}i(56698)(Writable,w),WritableState.prototype.getBuffer=function getBuffer(){for(var s=this.bufferedRequest,o=[];s;)o.push(s),s=s.next;return o},function(){try{Object.defineProperty(WritableState.prototype,\"buffer\",{get:_.deprecate((function writableStateBufferGetter(){return this.getBuffer()}),\"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.\",\"DEP0003\")})}catch(s){}}(),\"function\"==typeof Symbol&&Symbol.hasInstance&&\"function\"==typeof Function.prototype[Symbol.hasInstance]?(j=Function.prototype[Symbol.hasInstance],Object.defineProperty(Writable,Symbol.hasInstance,{value:function value(s){return!!j.call(this,s)||this===Writable&&(s&&s._writableState instanceof WritableState)}})):j=function realHasInstance(s){return s instanceof this},Writable.prototype.pipe=function(){ce(this,new Y)},Writable.prototype.write=function(s,o,i){var a=this._writableState,_=!1,w=!a.objectMode&&function _isUint8Array(s){return x.isBuffer(s)||s instanceof C}(s);return w&&!x.isBuffer(s)&&(s=function _uint8ArrayToBuffer(s){return x.from(s)}(s)),\"function\"==typeof o&&(i=o,o=null),w?o=\"buffer\":o||(o=a.defaultEncoding),\"function\"!=typeof i&&(i=nop),a.ending?function writeAfterEnd(s,o){var i=new ie;ce(s,i),u.nextTick(o,i)}(this,i):(w||function validChunk(s,o,i,a){var _;return null===i?_=new ee:\"string\"==typeof i||o.objectMode||(_=new U(\"chunk\",[\"string\",\"Buffer\"],i)),!_||(ce(s,_),u.nextTick(a,_),!1)}(this,a,s,i))&&(a.pendingcb++,_=function writeOrBuffer(s,o,i,a,u,_){if(!i){var w=function decodeChunk(s,o,i){s.objectMode||!1===s.decodeStrings||\"string\"!=typeof o||(o=x.from(o,i));return o}(o,a,u);a!==w&&(i=!0,u=\"buffer\",a=w)}var C=o.objectMode?1:a.length;o.length+=C;var j=o.length<o.highWaterMark;j||(o.needDrain=!0);if(o.writing||o.corked){var L=o.lastBufferedRequest;o.lastBufferedRequest={chunk:a,encoding:u,isBuf:i,callback:_,next:null},L?L.next=o.lastBufferedRequest:o.bufferedRequest=o.lastBufferedRequest,o.bufferedRequestCount+=1}else doWrite(s,o,!1,C,a,u,_);return j}(this,a,w,s,o,i)),_},Writable.prototype.cork=function(){this._writableState.corked++},Writable.prototype.uncork=function(){var s=this._writableState;s.corked&&(s.corked--,s.writing||s.corked||s.bufferProcessing||!s.bufferedRequest||clearBuffer(this,s))},Writable.prototype.setDefaultEncoding=function setDefaultEncoding(s){if(\"string\"==typeof s&&(s=s.toLowerCase()),!([\"hex\",\"utf8\",\"utf-8\",\"ascii\",\"binary\",\"base64\",\"ucs2\",\"ucs-2\",\"utf16le\",\"utf-16le\",\"raw\"].indexOf((s+\"\").toLowerCase())>-1))throw new ae(s);return this._writableState.defaultEncoding=s,this},Object.defineProperty(Writable.prototype,\"writableBuffer\",{enumerable:!1,get:function get(){return this._writableState&&this._writableState.getBuffer()}}),Object.defineProperty(Writable.prototype,\"writableHighWaterMark\",{enumerable:!1,get:function get(){return this._writableState.highWaterMark}}),Writable.prototype._write=function(s,o,i){i(new V(\"_write()\"))},Writable.prototype._writev=null,Writable.prototype.end=function(s,o,i){var a=this._writableState;return\"function\"==typeof s?(i=s,s=null,o=null):\"function\"==typeof o&&(i=o,o=null),null!=s&&this.write(s,o),a.corked&&(a.corked=1,this.uncork()),a.ending||function endWritable(s,o,i){o.ending=!0,finishMaybe(s,o),i&&(o.finished?u.nextTick(i):s.once(\"finish\",i));o.ended=!0,s.writable=!1}(this,a,i),this},Object.defineProperty(Writable.prototype,\"writableLength\",{enumerable:!1,get:function get(){return this._writableState.length}}),Object.defineProperty(Writable.prototype,\"destroyed\",{enumerable:!1,get:function get(){return void 0!==this._writableState&&this._writableState.destroyed},set:function set(s){this._writableState&&(this._writableState.destroyed=s)}}),Writable.prototype.destroy=L.destroy,Writable.prototype._undestroy=L.undestroy,Writable.prototype._destroy=function(s,o){o(s)}},16946:(s,o,i)=>{\"use strict\";var a=i(1907),u=i(98828),_=i(45807),w=Object,x=a(\"\".split);s.exports=u((function(){return!w(\"z\").propertyIsEnumerable(0)}))?function(s){return\"String\"===_(s)?x(s,\"\"):w(s)}:w},16962:(s,o)=>{o.aliasToReal={each:\"forEach\",eachRight:\"forEachRight\",entries:\"toPairs\",entriesIn:\"toPairsIn\",extend:\"assignIn\",extendAll:\"assignInAll\",extendAllWith:\"assignInAllWith\",extendWith:\"assignInWith\",first:\"head\",conforms:\"conformsTo\",matches:\"isMatch\",property:\"get\",__:\"placeholder\",F:\"stubFalse\",T:\"stubTrue\",all:\"every\",allPass:\"overEvery\",always:\"constant\",any:\"some\",anyPass:\"overSome\",apply:\"spread\",assoc:\"set\",assocPath:\"set\",complement:\"negate\",compose:\"flowRight\",contains:\"includes\",dissoc:\"unset\",dissocPath:\"unset\",dropLast:\"dropRight\",dropLastWhile:\"dropRightWhile\",equals:\"isEqual\",identical:\"eq\",indexBy:\"keyBy\",init:\"initial\",invertObj:\"invert\",juxt:\"over\",omitAll:\"omit\",nAry:\"ary\",path:\"get\",pathEq:\"matchesProperty\",pathOr:\"getOr\",paths:\"at\",pickAll:\"pick\",pipe:\"flow\",pluck:\"map\",prop:\"get\",propEq:\"matchesProperty\",propOr:\"getOr\",props:\"at\",symmetricDifference:\"xor\",symmetricDifferenceBy:\"xorBy\",symmetricDifferenceWith:\"xorWith\",takeLast:\"takeRight\",takeLastWhile:\"takeRightWhile\",unapply:\"rest\",unnest:\"flatten\",useWith:\"overArgs\",where:\"conformsTo\",whereEq:\"isMatch\",zipObj:\"zipObject\"},o.aryMethod={1:[\"assignAll\",\"assignInAll\",\"attempt\",\"castArray\",\"ceil\",\"create\",\"curry\",\"curryRight\",\"defaultsAll\",\"defaultsDeepAll\",\"floor\",\"flow\",\"flowRight\",\"fromPairs\",\"invert\",\"iteratee\",\"memoize\",\"method\",\"mergeAll\",\"methodOf\",\"mixin\",\"nthArg\",\"over\",\"overEvery\",\"overSome\",\"rest\",\"reverse\",\"round\",\"runInContext\",\"spread\",\"template\",\"trim\",\"trimEnd\",\"trimStart\",\"uniqueId\",\"words\",\"zipAll\"],2:[\"add\",\"after\",\"ary\",\"assign\",\"assignAllWith\",\"assignIn\",\"assignInAllWith\",\"at\",\"before\",\"bind\",\"bindAll\",\"bindKey\",\"chunk\",\"cloneDeepWith\",\"cloneWith\",\"concat\",\"conformsTo\",\"countBy\",\"curryN\",\"curryRightN\",\"debounce\",\"defaults\",\"defaultsDeep\",\"defaultTo\",\"delay\",\"difference\",\"divide\",\"drop\",\"dropRight\",\"dropRightWhile\",\"dropWhile\",\"endsWith\",\"eq\",\"every\",\"filter\",\"find\",\"findIndex\",\"findKey\",\"findLast\",\"findLastIndex\",\"findLastKey\",\"flatMap\",\"flatMapDeep\",\"flattenDepth\",\"forEach\",\"forEachRight\",\"forIn\",\"forInRight\",\"forOwn\",\"forOwnRight\",\"get\",\"groupBy\",\"gt\",\"gte\",\"has\",\"hasIn\",\"includes\",\"indexOf\",\"intersection\",\"invertBy\",\"invoke\",\"invokeMap\",\"isEqual\",\"isMatch\",\"join\",\"keyBy\",\"lastIndexOf\",\"lt\",\"lte\",\"map\",\"mapKeys\",\"mapValues\",\"matchesProperty\",\"maxBy\",\"meanBy\",\"merge\",\"mergeAllWith\",\"minBy\",\"multiply\",\"nth\",\"omit\",\"omitBy\",\"overArgs\",\"pad\",\"padEnd\",\"padStart\",\"parseInt\",\"partial\",\"partialRight\",\"partition\",\"pick\",\"pickBy\",\"propertyOf\",\"pull\",\"pullAll\",\"pullAt\",\"random\",\"range\",\"rangeRight\",\"rearg\",\"reject\",\"remove\",\"repeat\",\"restFrom\",\"result\",\"sampleSize\",\"some\",\"sortBy\",\"sortedIndex\",\"sortedIndexOf\",\"sortedLastIndex\",\"sortedLastIndexOf\",\"sortedUniqBy\",\"split\",\"spreadFrom\",\"startsWith\",\"subtract\",\"sumBy\",\"take\",\"takeRight\",\"takeRightWhile\",\"takeWhile\",\"tap\",\"throttle\",\"thru\",\"times\",\"trimChars\",\"trimCharsEnd\",\"trimCharsStart\",\"truncate\",\"union\",\"uniqBy\",\"uniqWith\",\"unset\",\"unzipWith\",\"without\",\"wrap\",\"xor\",\"zip\",\"zipObject\",\"zipObjectDeep\"],3:[\"assignInWith\",\"assignWith\",\"clamp\",\"differenceBy\",\"differenceWith\",\"findFrom\",\"findIndexFrom\",\"findLastFrom\",\"findLastIndexFrom\",\"getOr\",\"includesFrom\",\"indexOfFrom\",\"inRange\",\"intersectionBy\",\"intersectionWith\",\"invokeArgs\",\"invokeArgsMap\",\"isEqualWith\",\"isMatchWith\",\"flatMapDepth\",\"lastIndexOfFrom\",\"mergeWith\",\"orderBy\",\"padChars\",\"padCharsEnd\",\"padCharsStart\",\"pullAllBy\",\"pullAllWith\",\"rangeStep\",\"rangeStepRight\",\"reduce\",\"reduceRight\",\"replace\",\"set\",\"slice\",\"sortedIndexBy\",\"sortedLastIndexBy\",\"transform\",\"unionBy\",\"unionWith\",\"update\",\"xorBy\",\"xorWith\",\"zipWith\"],4:[\"fill\",\"setWith\",\"updateWith\"]},o.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},o.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},o.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},o.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},o.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},o.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},o.realToAlias=function(){var s=Object.prototype.hasOwnProperty,i=o.aliasToReal,a={};for(var u in i){var _=i[u];s.call(a,_)?a[_].push(u):a[_]=[u]}return a}(),o.remap={assignAll:\"assign\",assignAllWith:\"assignWith\",assignInAll:\"assignIn\",assignInAllWith:\"assignInWith\",curryN:\"curry\",curryRightN:\"curryRight\",defaultsAll:\"defaults\",defaultsDeepAll:\"defaultsDeep\",findFrom:\"find\",findIndexFrom:\"findIndex\",findLastFrom:\"findLast\",findLastIndexFrom:\"findLastIndex\",getOr:\"get\",includesFrom:\"includes\",indexOfFrom:\"indexOf\",invokeArgs:\"invoke\",invokeArgsMap:\"invokeMap\",lastIndexOfFrom:\"lastIndexOf\",mergeAll:\"merge\",mergeAllWith:\"mergeWith\",padChars:\"pad\",padCharsEnd:\"padEnd\",padCharsStart:\"padStart\",propertyOf:\"get\",rangeStep:\"range\",rangeStepRight:\"rangeRight\",restFrom:\"rest\",spreadFrom:\"spread\",trimChars:\"trim\",trimCharsEnd:\"trimEnd\",trimCharsStart:\"trimStart\",zipAll:\"zip\"},o.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},o.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},17255:(s,o,i)=>{var a=i(47422);s.exports=function basePropertyDeep(s){return function(o){return a(o,s)}}},17285:s=>{function source(s){return s?\"string\"==typeof s?s:s.source:null}function lookahead(s){return concat(\"(?=\",s,\")\")}function concat(...s){return s.map((s=>source(s))).join(\"\")}function either(...s){return\"(\"+s.map((s=>source(s))).join(\"|\")+\")\"}s.exports=function xml(s){const o=concat(/[A-Z_]/,function optional(s){return concat(\"(\",s,\")?\")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),i={className:\"symbol\",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},a={begin:/\\s/,contains:[{className:\"meta-keyword\",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\\n/}]},u=s.inherit(a,{begin:/\\(/,end:/\\)/}),_=s.inherit(s.APOS_STRING_MODE,{className:\"meta-string\"}),w=s.inherit(s.QUOTE_STRING_MODE,{className:\"meta-string\"}),x={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:\"attr\",begin:/[A-Za-z0-9._:-]+/,relevance:0},{begin:/=\\s*/,relevance:0,contains:[{className:\"string\",endsParent:!0,variants:[{begin:/\"/,end:/\"/,contains:[i]},{begin:/'/,end:/'/,contains:[i]},{begin:/[^\\s\"'=<>`]+/}]}]}]};return{name:\"HTML, XML\",aliases:[\"html\",\"xhtml\",\"rss\",\"atom\",\"xjb\",\"xsd\",\"xsl\",\"plist\",\"wsf\",\"svg\"],case_insensitive:!0,contains:[{className:\"meta\",begin:/<![a-z]/,end:/>/,relevance:10,contains:[a,w,_,u,{begin:/\\[/,end:/\\]/,contains:[{className:\"meta\",begin:/<![a-z]/,end:/>/,contains:[a,u,w,_]}]}]},s.COMMENT(/<!--/,/-->/,{relevance:10}),{begin:/<!\\[CDATA\\[/,end:/\\]\\]>/,relevance:10},i,{className:\"meta\",begin:/<\\?xml/,end:/\\?>/,relevance:10},{className:\"tag\",begin:/<style(?=\\s|>)/,end:/>/,keywords:{name:\"style\"},contains:[x],starts:{end:/<\\/style>/,returnEnd:!0,subLanguage:[\"css\",\"xml\"]}},{className:\"tag\",begin:/<script(?=\\s|>)/,end:/>/,keywords:{name:\"script\"},contains:[x],starts:{end:/<\\/script>/,returnEnd:!0,subLanguage:[\"javascript\",\"handlebars\",\"xml\"]}},{className:\"tag\",begin:/<>|<\\/>/},{className:\"tag\",begin:concat(/</,lookahead(concat(o,either(/\\/>/,/>/,/\\s/)))),end:/\\/?>/,contains:[{className:\"name\",begin:o,relevance:0,starts:x}]},{className:\"tag\",begin:concat(/<\\//,lookahead(concat(o,/>/))),contains:[{className:\"name\",begin:o,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},17400:(s,o,i)=>{var a=i(99374),u=1/0;s.exports=function toFinite(s){return s?(s=a(s))===u||s===-1/0?17976931348623157e292*(s<0?-1:1):s==s?s:0:0===s?s:0}},17533:s=>{s.exports=function yaml(s){var o=\"true false yes no null\",i=\"[\\\\w#;/?:@&=+$,.~*'()[\\\\]]+\",a={className:\"string\",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/\"/,end:/\"/},{begin:/\\S+/}],contains:[s.BACKSLASH_ESCAPE,{className:\"template-variable\",variants:[{begin:/\\{\\{/,end:/\\}\\}/},{begin:/%\\{/,end:/\\}/}]}]},u=s.inherit(a,{variants:[{begin:/'/,end:/'/},{begin:/\"/,end:/\"/},{begin:/[^\\s,{}[\\]]+/}]}),_={className:\"number\",begin:\"\\\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\\\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\\\.[0-9]*)?([ \\\\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\\\b\"},w={end:\",\",endsWithParent:!0,excludeEnd:!0,keywords:o,relevance:0},x={begin:/\\{/,end:/\\}/,contains:[w],illegal:\"\\\\n\",relevance:0},C={begin:\"\\\\[\",end:\"\\\\]\",contains:[w],illegal:\"\\\\n\",relevance:0},j=[{className:\"attr\",variants:[{begin:\"\\\\w[\\\\w :\\\\/.-]*:(?=[ \\t]|$)\"},{begin:'\"\\\\w[\\\\w :\\\\/.-]*\":(?=[ \\t]|$)'},{begin:\"'\\\\w[\\\\w :\\\\/.-]*':(?=[ \\t]|$)\"}]},{className:\"meta\",begin:\"^---\\\\s*$\",relevance:10},{className:\"string\",begin:\"[\\\\|>]([1-9]?[+-])?[ ]*\\\\n( +)[^ ][^\\\\n]*\\\\n(\\\\2[^\\\\n]+\\\\n?)*\"},{begin:\"<%[%=-]?\",end:\"[%-]?%>\",subLanguage:\"ruby\",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:\"type\",begin:\"!\\\\w+!\"+i},{className:\"type\",begin:\"!<\"+i+\">\"},{className:\"type\",begin:\"!\"+i},{className:\"type\",begin:\"!!\"+i},{className:\"meta\",begin:\"&\"+s.UNDERSCORE_IDENT_RE+\"$\"},{className:\"meta\",begin:\"\\\\*\"+s.UNDERSCORE_IDENT_RE+\"$\"},{className:\"bullet\",begin:\"-(?=[ ]|$)\",relevance:0},s.HASH_COMMENT_MODE,{beginKeywords:o,keywords:{literal:o}},_,{className:\"number\",begin:s.C_NUMBER_RE+\"\\\\b\",relevance:0},x,C,a],L=[...j];return L.pop(),L.push(u),w.contains=L,{name:\"YAML\",case_insensitive:!0,aliases:[\"yml\"],contains:j}}},17670:(s,o,i)=>{var a=i(12651);s.exports=function mapCacheDelete(s){var o=a(this,s).delete(s);return this.size-=o?1:0,o}},17965:(s,o,i)=>{\"use strict\";var a=i(16426),u={\"text/plain\":\"Text\",\"text/html\":\"Url\",default:\"Text\"};s.exports=function copy(s,o){var i,_,w,x,C,j,L=!1;o||(o={}),i=o.debug||!1;try{if(w=a(),x=document.createRange(),C=document.getSelection(),(j=document.createElement(\"span\")).textContent=s,j.ariaHidden=\"true\",j.style.all=\"unset\",j.style.position=\"fixed\",j.style.top=0,j.style.clip=\"rect(0, 0, 0, 0)\",j.style.whiteSpace=\"pre\",j.style.webkitUserSelect=\"text\",j.style.MozUserSelect=\"text\",j.style.msUserSelect=\"text\",j.style.userSelect=\"text\",j.addEventListener(\"copy\",(function(a){if(a.stopPropagation(),o.format)if(a.preventDefault(),void 0===a.clipboardData){i&&console.warn(\"unable to use e.clipboardData\"),i&&console.warn(\"trying IE specific stuff\"),window.clipboardData.clearData();var _=u[o.format]||u.default;window.clipboardData.setData(_,s)}else a.clipboardData.clearData(),a.clipboardData.setData(o.format,s);o.onCopy&&(a.preventDefault(),o.onCopy(a.clipboardData))})),document.body.appendChild(j),x.selectNodeContents(j),C.addRange(x),!document.execCommand(\"copy\"))throw new Error(\"copy command was unsuccessful\");L=!0}catch(a){i&&console.error(\"unable to copy using execCommand: \",a),i&&console.warn(\"trying IE specific stuff\");try{window.clipboardData.setData(o.format||\"text\",s),o.onCopy&&o.onCopy(window.clipboardData),L=!0}catch(a){i&&console.error(\"unable to copy using clipboardData: \",a),i&&console.error(\"falling back to prompt\"),_=function format(s){var o=(/mac os x/i.test(navigator.userAgent)?\"⌘\":\"Ctrl\")+\"+C\";return s.replace(/#{\\s*key\\s*}/g,o)}(\"message\"in o?o.message:\"Copy to clipboard: #{key}, Enter\"),window.prompt(_,s)}}finally{C&&(\"function\"==typeof C.removeRange?C.removeRange(x):C.removeAllRanges()),j&&document.body.removeChild(j),w()}return L}},18073:(s,o,i)=>{var a=i(85087),u=i(54641),_=i(70981);s.exports=function createRecurry(s,o,i,w,x,C,j,L,B,$){var U=8&o;o|=U?32:64,4&(o&=~(U?64:32))||(o&=-4);var V=[s,o,x,U?C:void 0,U?j:void 0,U?void 0:C,U?void 0:j,L,B,$],z=i.apply(void 0,V);return a(s)&&u(z,V),z.placeholder=w,_(z,s,o)}},19123:(s,o,i)=>{var a=i(65606),u=i(31499),_=i(88310).Stream;function resolve(s,o,i){var a,_=function create_indent(s,o){return new Array(o||0).join(s||\"\")}(o,i=i||0),w=s;if(\"object\"==typeof s&&((w=s[a=Object.keys(s)[0]])&&w._elem))return w._elem.name=a,w._elem.icount=i,w._elem.indent=o,w._elem.indents=_,w._elem.interrupt=w,w._elem;var x,C=[],j=[];function get_attributes(s){Object.keys(s).forEach((function(o){C.push(function attribute(s,o){return s+'=\"'+u(o)+'\"'}(o,s[o]))}))}switch(typeof w){case\"object\":if(null===w)break;w._attr&&get_attributes(w._attr),w._cdata&&j.push((\"<![CDATA[\"+w._cdata).replace(/\\]\\]>/g,\"]]]]><![CDATA[>\")+\"]]>\"),w.forEach&&(x=!1,j.push(\"\"),w.forEach((function(s){\"object\"==typeof s?\"_attr\"==Object.keys(s)[0]?get_attributes(s._attr):j.push(resolve(s,o,i+1)):(j.pop(),x=!0,j.push(u(s)))})),x||j.push(\"\"));break;default:j.push(u(w))}return{name:a,interrupt:!1,attributes:C,content:j,icount:i,indents:_,indent:o}}function format(s,o,i){if(\"object\"!=typeof o)return s(!1,o);var a=o.interrupt?1:o.content.length;function proceed(){for(;o.content.length;){var u=o.content.shift();if(void 0!==u){if(interrupt(u))return;format(s,u)}}s(!1,(a>1?o.indents:\"\")+(o.name?\"</\"+o.name+\">\":\"\")+(o.indent&&!i?\"\\n\":\"\")),i&&i()}function interrupt(o){return!!o.interrupt&&(o.interrupt.append=s,o.interrupt.end=proceed,o.interrupt=!1,s(!0),!0)}if(s(!1,o.indents+(o.name?\"<\"+o.name:\"\")+(o.attributes.length?\" \"+o.attributes.join(\" \"):\"\")+(a?o.name?\">\":\"\":o.name?\"/>\":\"\")+(o.indent&&a>1?\"\\n\":\"\")),!a)return s(!1,o.indent?\"\\n\":\"\");interrupt(o)||proceed()}s.exports=function xml(s,o){\"object\"!=typeof o&&(o={indent:o});var i=o.stream?new _:null,u=\"\",w=!1,x=o.indent?!0===o.indent?\"    \":o.indent:\"\",C=!0;function delay(s){C?a.nextTick(s):s()}function append(s,o){if(void 0!==o&&(u+=o),s&&!w&&(i=i||new _,w=!0),s&&w){var a=u;delay((function(){i.emit(\"data\",a)})),u=\"\"}}function add(s,o){format(append,resolve(s,x,x?1:0),o)}function end(){if(i){var s=u;delay((function(){i.emit(\"data\",s),i.emit(\"end\"),i.readable=!1,i.emit(\"close\")}))}}return delay((function(){C=!1})),o.declaration&&function addXmlDeclaration(s){var o={version:\"1.0\",encoding:s.encoding||\"UTF-8\"};s.standalone&&(o.standalone=s.standalone),add({\"?xml\":{_attr:o}}),u=u.replace(\"/>\",\"?>\")}(o.declaration),s&&s.forEach?s.forEach((function(o,i){var a;i+1===s.length&&(a=end),add(o,a)})):add(s,end),i?(i.readable=!0,i):u},s.exports.element=s.exports.Element=function element(){var s={_elem:resolve(Array.prototype.slice.call(arguments)),push:function(s){if(!this.append)throw new Error(\"not assigned to a parent!\");var o=this,i=this._elem.indent;format(this.append,resolve(s,i,this._elem.icount+(i?1:0)),(function(){o.append(!0)}))},close:function(s){void 0!==s&&this.push(s),this.end&&this.end()}};return s}},19219:s=>{s.exports=function cacheHas(s,o){return s.has(o)}},19287:s=>{\"use strict\";s.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},19358:(s,o,i)=>{\"use strict\";var a=i(85582),u=i(49724),_=i(61626),w=i(88280),x=i(79192),C=i(19595),j=i(54829),L=i(34084),B=i(32096),$=i(39259),U=i(85884),V=i(39447),z=i(7376);s.exports=function(s,o,i,Y){var Z=\"stackTraceLimit\",ee=Y?2:1,ie=s.split(\".\"),ae=ie[ie.length-1],ce=a.apply(null,ie);if(ce){var le=ce.prototype;if(!z&&u(le,\"cause\")&&delete le.cause,!i)return ce;var pe=a(\"Error\"),de=o((function(s,o){var i=B(Y?o:s,void 0),a=Y?new ce(s):new ce;return void 0!==i&&_(a,\"message\",i),U(a,de,a.stack,2),this&&w(le,this)&&L(a,this,de),arguments.length>ee&&$(a,arguments[ee]),a}));if(de.prototype=le,\"Error\"!==ae?x?x(de,pe):C(de,pe,{name:!0}):V&&Z in ce&&(j(de,ce,Z),j(de,ce,\"prepareStackTrace\")),C(de,ce),!z)try{le.name!==ae&&_(le,\"name\",ae),le.constructor=de}catch(s){}return de}}},19570:(s,o,i)=>{var a=i(37334),u=i(93243),_=i(83488),w=u?function(s,o){return u(s,\"toString\",{configurable:!0,enumerable:!1,value:a(o),writable:!0})}:_;s.exports=w},19595:(s,o,i)=>{\"use strict\";var a=i(49724),u=i(11042),_=i(13846),w=i(74284);s.exports=function(s,o,i){for(var x=u(o),C=w.f,j=_.f,L=0;L<x.length;L++){var B=x[L];a(s,B)||i&&a(i,B)||C(s,B,j(o,B))}}},19709:(s,o,i)=>{\"use strict\";var a=i(23034);s.exports=a},19846:(s,o,i)=>{\"use strict\";var a=i(20798),u=i(98828),_=i(45951).String;s.exports=!!Object.getOwnPropertySymbols&&!u((function(){var s=Symbol(\"symbol detection\");return!_(s)||!(Object(s)instanceof Symbol)||!Symbol.sham&&a&&a<41}))},19931:(s,o,i)=>{var a=i(31769),u=i(68090),_=i(68969),w=i(77797);s.exports=function baseUnset(s,o){return o=a(o,s),null==(s=_(s,o))||delete s[w(u(o))]}},20181:(s,o,i)=>{var a=/^\\s+|\\s+$/g,u=/^[-+]0x[0-9a-f]+$/i,_=/^0b[01]+$/i,w=/^0o[0-7]+$/i,x=parseInt,C=\"object\"==typeof i.g&&i.g&&i.g.Object===Object&&i.g,j=\"object\"==typeof self&&self&&self.Object===Object&&self,L=C||j||Function(\"return this\")(),B=Object.prototype.toString,$=Math.max,U=Math.min,now=function(){return L.Date.now()};function isObject(s){var o=typeof s;return!!s&&(\"object\"==o||\"function\"==o)}function toNumber(s){if(\"number\"==typeof s)return s;if(function isSymbol(s){return\"symbol\"==typeof s||function isObjectLike(s){return!!s&&\"object\"==typeof s}(s)&&\"[object Symbol]\"==B.call(s)}(s))return NaN;if(isObject(s)){var o=\"function\"==typeof s.valueOf?s.valueOf():s;s=isObject(o)?o+\"\":o}if(\"string\"!=typeof s)return 0===s?s:+s;s=s.replace(a,\"\");var i=_.test(s);return i||w.test(s)?x(s.slice(2),i?2:8):u.test(s)?NaN:+s}s.exports=function debounce(s,o,i){var a,u,_,w,x,C,j=0,L=!1,B=!1,V=!0;if(\"function\"!=typeof s)throw new TypeError(\"Expected a function\");function invokeFunc(o){var i=a,_=u;return a=u=void 0,j=o,w=s.apply(_,i)}function shouldInvoke(s){var i=s-C;return void 0===C||i>=o||i<0||B&&s-j>=_}function timerExpired(){var s=now();if(shouldInvoke(s))return trailingEdge(s);x=setTimeout(timerExpired,function remainingWait(s){var i=o-(s-C);return B?U(i,_-(s-j)):i}(s))}function trailingEdge(s){return x=void 0,V&&a?invokeFunc(s):(a=u=void 0,w)}function debounced(){var s=now(),i=shouldInvoke(s);if(a=arguments,u=this,C=s,i){if(void 0===x)return function leadingEdge(s){return j=s,x=setTimeout(timerExpired,o),L?invokeFunc(s):w}(C);if(B)return x=setTimeout(timerExpired,o),invokeFunc(C)}return void 0===x&&(x=setTimeout(timerExpired,o)),w}return o=toNumber(o)||0,isObject(i)&&(L=!!i.leading,_=(B=\"maxWait\"in i)?$(toNumber(i.maxWait)||0,o):_,V=\"trailing\"in i?!!i.trailing:V),debounced.cancel=function cancel(){void 0!==x&&clearTimeout(x),j=0,a=C=u=x=void 0},debounced.flush=function flush(){return void 0===x?w:trailingEdge(now())},debounced}},20317:s=>{s.exports=function mapToArray(s){var o=-1,i=Array(s.size);return s.forEach((function(s,a){i[++o]=[a,s]})),i}},20334:(s,o,i)=>{\"use strict\";var a=i(48287).Buffer;class NonError extends Error{constructor(s){super(NonError._prepareSuperMessage(s)),Object.defineProperty(this,\"name\",{value:\"NonError\",configurable:!0,writable:!0}),Error.captureStackTrace&&Error.captureStackTrace(this,NonError)}static _prepareSuperMessage(s){try{return JSON.stringify(s)}catch{return String(s)}}}const u=[{property:\"name\",enumerable:!1},{property:\"message\",enumerable:!1},{property:\"stack\",enumerable:!1},{property:\"code\",enumerable:!0}],_=Symbol(\".toJSON called\"),destroyCircular=({from:s,seen:o,to_:i,forceEnumerable:w,maxDepth:x,depth:C})=>{const j=i||(Array.isArray(s)?[]:{});if(o.push(s),C>=x)return j;if(\"function\"==typeof s.toJSON&&!0!==s[_])return(s=>{s[_]=!0;const o=s.toJSON();return delete s[_],o})(s);for(const[i,u]of Object.entries(s))\"function\"==typeof a&&a.isBuffer(u)?j[i]=\"[object Buffer]\":\"function\"!=typeof u&&(u&&\"object\"==typeof u?o.includes(s[i])?j[i]=\"[Circular]\":(C++,j[i]=destroyCircular({from:s[i],seen:o.slice(),forceEnumerable:w,maxDepth:x,depth:C})):j[i]=u);for(const{property:o,enumerable:i}of u)\"string\"==typeof s[o]&&Object.defineProperty(j,o,{value:s[o],enumerable:!!w||i,configurable:!0,writable:!0});return j};s.exports={serializeError:(s,o={})=>{const{maxDepth:i=Number.POSITIVE_INFINITY}=o;return\"object\"==typeof s&&null!==s?destroyCircular({from:s,seen:[],forceEnumerable:!0,maxDepth:i,depth:0}):\"function\"==typeof s?`[Function: ${s.name||\"anonymous\"}]`:s},deserializeError:(s,o={})=>{const{maxDepth:i=Number.POSITIVE_INFINITY}=o;if(s instanceof Error)return s;if(\"object\"==typeof s&&null!==s&&!Array.isArray(s)){const o=new Error;return destroyCircular({from:s,seen:[],to_:o,maxDepth:i,depth:0}),o}return new NonError(s)}}},20426:s=>{var o=Object.prototype.hasOwnProperty;s.exports=function baseHas(s,i){return null!=s&&o.call(s,i)}},20575:(s,o,i)=>{\"use strict\";var a=i(3121);s.exports=function(s){return a(s.length)}},20798:(s,o,i)=>{\"use strict\";var a,u,_=i(45951),w=i(96794),x=_.process,C=_.Deno,j=x&&x.versions||C&&C.version,L=j&&j.v8;L&&(u=(a=L.split(\".\"))[0]>0&&a[0]<4?1:+(a[0]+a[1])),!u&&w&&(!(a=w.match(/Edge\\/(\\d+)/))||a[1]>=74)&&(a=w.match(/Chrome\\/(\\d+)/))&&(u=+a[1]),s.exports=u},20850:(s,o,i)=>{\"use strict\";s.exports=i(46076)},20999:(s,o,i)=>{var a=i(69302),u=i(36800);s.exports=function createAssigner(s){return a((function(o,i){var a=-1,_=i.length,w=_>1?i[_-1]:void 0,x=_>2?i[2]:void 0;for(w=s.length>3&&\"function\"==typeof w?(_--,w):void 0,x&&u(i[0],i[1],x)&&(w=_<3?void 0:w,_=1),o=Object(o);++a<_;){var C=i[a];C&&s(o,C,a,w)}return o}))}},21549:(s,o,i)=>{var a=i(22032),u=i(63862),_=i(66721),w=i(12749),x=i(35749);function Hash(s){var o=-1,i=null==s?0:s.length;for(this.clear();++o<i;){var a=s[o];this.set(a[0],a[1])}}Hash.prototype.clear=a,Hash.prototype.delete=u,Hash.prototype.get=_,Hash.prototype.has=w,Hash.prototype.set=x,s.exports=Hash},21791:(s,o,i)=>{var a=i(16547),u=i(43360);s.exports=function copyObject(s,o,i,_){var w=!i;i||(i={});for(var x=-1,C=o.length;++x<C;){var j=o[x],L=_?_(i[j],s[j],j,i,s):void 0;void 0===L&&(L=s[j]),w?u(i,j,L):a(i,j,L)}return i}},21986:(s,o,i)=>{var a=i(51873),u=i(37828),_=i(75288),w=i(25911),x=i(20317),C=i(84247),j=a?a.prototype:void 0,L=j?j.valueOf:void 0;s.exports=function equalByTag(s,o,i,a,j,B,$){switch(i){case\"[object DataView]\":if(s.byteLength!=o.byteLength||s.byteOffset!=o.byteOffset)return!1;s=s.buffer,o=o.buffer;case\"[object ArrayBuffer]\":return!(s.byteLength!=o.byteLength||!B(new u(s),new u(o)));case\"[object Boolean]\":case\"[object Date]\":case\"[object Number]\":return _(+s,+o);case\"[object Error]\":return s.name==o.name&&s.message==o.message;case\"[object RegExp]\":case\"[object String]\":return s==o+\"\";case\"[object Map]\":var U=x;case\"[object Set]\":var V=1&a;if(U||(U=C),s.size!=o.size&&!V)return!1;var z=$.get(s);if(z)return z==o;a|=2,$.set(s,o);var Y=w(U(s),U(o),a,j,B,$);return $.delete(s),Y;case\"[object Symbol]\":if(L)return L.call(s)==L.call(o)}return!1}},22032:(s,o,i)=>{var a=i(81042);s.exports=function hashClear(){this.__data__=a?a(null):{},this.size=0}},22225:s=>{var o=\"\\\\ud800-\\\\udfff\",i=\"\\\\u2700-\\\\u27bf\",a=\"a-z\\\\xdf-\\\\xf6\\\\xf8-\\\\xff\",u=\"A-Z\\\\xc0-\\\\xd6\\\\xd8-\\\\xde\",_=\"\\\\xac\\\\xb1\\\\xd7\\\\xf7\\\\x00-\\\\x2f\\\\x3a-\\\\x40\\\\x5b-\\\\x60\\\\x7b-\\\\xbf\\\\u2000-\\\\u206f \\\\t\\\\x0b\\\\f\\\\xa0\\\\ufeff\\\\n\\\\r\\\\u2028\\\\u2029\\\\u1680\\\\u180e\\\\u2000\\\\u2001\\\\u2002\\\\u2003\\\\u2004\\\\u2005\\\\u2006\\\\u2007\\\\u2008\\\\u2009\\\\u200a\\\\u202f\\\\u205f\\\\u3000\",w=\"[\"+_+\"]\",x=\"\\\\d+\",C=\"[\"+i+\"]\",j=\"[\"+a+\"]\",L=\"[^\"+o+_+x+i+a+u+\"]\",B=\"(?:\\\\ud83c[\\\\udde6-\\\\uddff]){2}\",$=\"[\\\\ud800-\\\\udbff][\\\\udc00-\\\\udfff]\",U=\"[\"+u+\"]\",V=\"(?:\"+j+\"|\"+L+\")\",z=\"(?:\"+U+\"|\"+L+\")\",Y=\"(?:['’](?:d|ll|m|re|s|t|ve))?\",Z=\"(?:['’](?:D|LL|M|RE|S|T|VE))?\",ee=\"(?:[\\\\u0300-\\\\u036f\\\\ufe20-\\\\ufe2f\\\\u20d0-\\\\u20ff]|\\\\ud83c[\\\\udffb-\\\\udfff])?\",ie=\"[\\\\ufe0e\\\\ufe0f]?\",ae=ie+ee+(\"(?:\\\\u200d(?:\"+[\"[^\"+o+\"]\",B,$].join(\"|\")+\")\"+ie+ee+\")*\"),ce=\"(?:\"+[C,B,$].join(\"|\")+\")\"+ae,le=RegExp([U+\"?\"+j+\"+\"+Y+\"(?=\"+[w,U,\"$\"].join(\"|\")+\")\",z+\"+\"+Z+\"(?=\"+[w,U+V,\"$\"].join(\"|\")+\")\",U+\"?\"+V+\"+\"+Y,U+\"+\"+Z,\"\\\\d*(?:1ST|2ND|3RD|(?![123])\\\\dTH)(?=\\\\b|[a-z_])\",\"\\\\d*(?:1st|2nd|3rd|(?![123])\\\\dth)(?=\\\\b|[A-Z_])\",x,ce].join(\"|\"),\"g\");s.exports=function unicodeWords(s){return s.match(le)||[]}},22551:(s,o,i)=>{\"use strict\";var a=i(96540),u=i(69982);function p(s){for(var o=\"https://reactjs.org/docs/error-decoder.html?invariant=\"+s,i=1;i<arguments.length;i++)o+=\"&args[]=\"+encodeURIComponent(arguments[i]);return\"Minified React error #\"+s+\"; visit \"+o+\" for the full message or use the non-minified dev environment for full errors and additional helpful warnings.\"}var _=new Set,w={};function fa(s,o){ha(s,o),ha(s+\"Capture\",o)}function ha(s,o){for(w[s]=o,s=0;s<o.length;s++)_.add(o[s])}var x=!(\"undefined\"==typeof window||void 0===window.document||void 0===window.document.createElement),C=Object.prototype.hasOwnProperty,j=/^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$/,L={},B={};function v(s,o,i,a,u,_,w){this.acceptsBooleans=2===o||3===o||4===o,this.attributeName=a,this.attributeNamespace=u,this.mustUseProperty=i,this.propertyName=s,this.type=o,this.sanitizeURL=_,this.removeEmptyString=w}var $={};\"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style\".split(\" \").forEach((function(s){$[s]=new v(s,0,!1,s,null,!1,!1)})),[[\"acceptCharset\",\"accept-charset\"],[\"className\",\"class\"],[\"htmlFor\",\"for\"],[\"httpEquiv\",\"http-equiv\"]].forEach((function(s){var o=s[0];$[o]=new v(o,1,!1,s[1],null,!1,!1)})),[\"contentEditable\",\"draggable\",\"spellCheck\",\"value\"].forEach((function(s){$[s]=new v(s,2,!1,s.toLowerCase(),null,!1,!1)})),[\"autoReverse\",\"externalResourcesRequired\",\"focusable\",\"preserveAlpha\"].forEach((function(s){$[s]=new v(s,2,!1,s,null,!1,!1)})),\"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope\".split(\" \").forEach((function(s){$[s]=new v(s,3,!1,s.toLowerCase(),null,!1,!1)})),[\"checked\",\"multiple\",\"muted\",\"selected\"].forEach((function(s){$[s]=new v(s,3,!0,s,null,!1,!1)})),[\"capture\",\"download\"].forEach((function(s){$[s]=new v(s,4,!1,s,null,!1,!1)})),[\"cols\",\"rows\",\"size\",\"span\"].forEach((function(s){$[s]=new v(s,6,!1,s,null,!1,!1)})),[\"rowSpan\",\"start\"].forEach((function(s){$[s]=new v(s,5,!1,s.toLowerCase(),null,!1,!1)}));var U=/[\\-:]([a-z])/g;function sa(s){return s[1].toUpperCase()}function ta(s,o,i,a){var u=$.hasOwnProperty(o)?$[o]:null;(null!==u?0!==u.type:a||!(2<o.length)||\"o\"!==o[0]&&\"O\"!==o[0]||\"n\"!==o[1]&&\"N\"!==o[1])&&(function qa(s,o,i,a){if(null==o||function pa(s,o,i,a){if(null!==i&&0===i.type)return!1;switch(typeof o){case\"function\":case\"symbol\":return!0;case\"boolean\":return!a&&(null!==i?!i.acceptsBooleans:\"data-\"!==(s=s.toLowerCase().slice(0,5))&&\"aria-\"!==s);default:return!1}}(s,o,i,a))return!0;if(a)return!1;if(null!==i)switch(i.type){case 3:return!o;case 4:return!1===o;case 5:return isNaN(o);case 6:return isNaN(o)||1>o}return!1}(o,i,u,a)&&(i=null),a||null===u?function oa(s){return!!C.call(B,s)||!C.call(L,s)&&(j.test(s)?B[s]=!0:(L[s]=!0,!1))}(o)&&(null===i?s.removeAttribute(o):s.setAttribute(o,\"\"+i)):u.mustUseProperty?s[u.propertyName]=null===i?3!==u.type&&\"\":i:(o=u.attributeName,a=u.attributeNamespace,null===i?s.removeAttribute(o):(i=3===(u=u.type)||4===u&&!0===i?\"\":\"\"+i,a?s.setAttributeNS(a,o,i):s.setAttribute(o,i))))}\"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height\".split(\" \").forEach((function(s){var o=s.replace(U,sa);$[o]=new v(o,1,!1,s,null,!1,!1)})),\"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type\".split(\" \").forEach((function(s){var o=s.replace(U,sa);$[o]=new v(o,1,!1,s,\"http://www.w3.org/1999/xlink\",!1,!1)})),[\"xml:base\",\"xml:lang\",\"xml:space\"].forEach((function(s){var o=s.replace(U,sa);$[o]=new v(o,1,!1,s,\"http://www.w3.org/XML/1998/namespace\",!1,!1)})),[\"tabIndex\",\"crossOrigin\"].forEach((function(s){$[s]=new v(s,1,!1,s.toLowerCase(),null,!1,!1)})),$.xlinkHref=new v(\"xlinkHref\",1,!1,\"xlink:href\",\"http://www.w3.org/1999/xlink\",!0,!1),[\"src\",\"href\",\"action\",\"formAction\"].forEach((function(s){$[s]=new v(s,1,!1,s.toLowerCase(),null,!0,!0)}));var V=a.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,z=Symbol.for(\"react.element\"),Y=Symbol.for(\"react.portal\"),Z=Symbol.for(\"react.fragment\"),ee=Symbol.for(\"react.strict_mode\"),ie=Symbol.for(\"react.profiler\"),ae=Symbol.for(\"react.provider\"),ce=Symbol.for(\"react.context\"),le=Symbol.for(\"react.forward_ref\"),pe=Symbol.for(\"react.suspense\"),de=Symbol.for(\"react.suspense_list\"),fe=Symbol.for(\"react.memo\"),ye=Symbol.for(\"react.lazy\");Symbol.for(\"react.scope\"),Symbol.for(\"react.debug_trace_mode\");var be=Symbol.for(\"react.offscreen\");Symbol.for(\"react.legacy_hidden\"),Symbol.for(\"react.cache\"),Symbol.for(\"react.tracing_marker\");var _e=Symbol.iterator;function Ka(s){return null===s||\"object\"!=typeof s?null:\"function\"==typeof(s=_e&&s[_e]||s[\"@@iterator\"])?s:null}var Se,we=Object.assign;function Ma(s){if(void 0===Se)try{throw Error()}catch(s){var o=s.stack.trim().match(/\\n( *(at )?)/);Se=o&&o[1]||\"\"}return\"\\n\"+Se+s}var xe=!1;function Oa(s,o){if(!s||xe)return\"\";xe=!0;var i=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(o)if(o=function(){throw Error()},Object.defineProperty(o.prototype,\"props\",{set:function(){throw Error()}}),\"object\"==typeof Reflect&&Reflect.construct){try{Reflect.construct(o,[])}catch(s){var a=s}Reflect.construct(s,[],o)}else{try{o.call()}catch(s){a=s}s.call(o.prototype)}else{try{throw Error()}catch(s){a=s}s()}}catch(o){if(o&&a&&\"string\"==typeof o.stack){for(var u=o.stack.split(\"\\n\"),_=a.stack.split(\"\\n\"),w=u.length-1,x=_.length-1;1<=w&&0<=x&&u[w]!==_[x];)x--;for(;1<=w&&0<=x;w--,x--)if(u[w]!==_[x]){if(1!==w||1!==x)do{if(w--,0>--x||u[w]!==_[x]){var C=\"\\n\"+u[w].replace(\" at new \",\" at \");return s.displayName&&C.includes(\"<anonymous>\")&&(C=C.replace(\"<anonymous>\",s.displayName)),C}}while(1<=w&&0<=x);break}}}finally{xe=!1,Error.prepareStackTrace=i}return(s=s?s.displayName||s.name:\"\")?Ma(s):\"\"}function Pa(s){switch(s.tag){case 5:return Ma(s.type);case 16:return Ma(\"Lazy\");case 13:return Ma(\"Suspense\");case 19:return Ma(\"SuspenseList\");case 0:case 2:case 15:return s=Oa(s.type,!1);case 11:return s=Oa(s.type.render,!1);case 1:return s=Oa(s.type,!0);default:return\"\"}}function Qa(s){if(null==s)return null;if(\"function\"==typeof s)return s.displayName||s.name||null;if(\"string\"==typeof s)return s;switch(s){case Z:return\"Fragment\";case Y:return\"Portal\";case ie:return\"Profiler\";case ee:return\"StrictMode\";case pe:return\"Suspense\";case de:return\"SuspenseList\"}if(\"object\"==typeof s)switch(s.$$typeof){case ce:return(s.displayName||\"Context\")+\".Consumer\";case ae:return(s._context.displayName||\"Context\")+\".Provider\";case le:var o=s.render;return(s=s.displayName)||(s=\"\"!==(s=o.displayName||o.name||\"\")?\"ForwardRef(\"+s+\")\":\"ForwardRef\"),s;case fe:return null!==(o=s.displayName||null)?o:Qa(s.type)||\"Memo\";case ye:o=s._payload,s=s._init;try{return Qa(s(o))}catch(s){}}return null}function Ra(s){var o=s.type;switch(s.tag){case 24:return\"Cache\";case 9:return(o.displayName||\"Context\")+\".Consumer\";case 10:return(o._context.displayName||\"Context\")+\".Provider\";case 18:return\"DehydratedFragment\";case 11:return s=(s=o.render).displayName||s.name||\"\",o.displayName||(\"\"!==s?\"ForwardRef(\"+s+\")\":\"ForwardRef\");case 7:return\"Fragment\";case 5:return o;case 4:return\"Portal\";case 3:return\"Root\";case 6:return\"Text\";case 16:return Qa(o);case 8:return o===ee?\"StrictMode\":\"Mode\";case 22:return\"Offscreen\";case 12:return\"Profiler\";case 21:return\"Scope\";case 13:return\"Suspense\";case 19:return\"SuspenseList\";case 25:return\"TracingMarker\";case 1:case 0:case 17:case 2:case 14:case 15:if(\"function\"==typeof o)return o.displayName||o.name||null;if(\"string\"==typeof o)return o}return null}function Sa(s){switch(typeof s){case\"boolean\":case\"number\":case\"string\":case\"undefined\":case\"object\":return s;default:return\"\"}}function Ta(s){var o=s.type;return(s=s.nodeName)&&\"input\"===s.toLowerCase()&&(\"checkbox\"===o||\"radio\"===o)}function Va(s){s._valueTracker||(s._valueTracker=function Ua(s){var o=Ta(s)?\"checked\":\"value\",i=Object.getOwnPropertyDescriptor(s.constructor.prototype,o),a=\"\"+s[o];if(!s.hasOwnProperty(o)&&void 0!==i&&\"function\"==typeof i.get&&\"function\"==typeof i.set){var u=i.get,_=i.set;return Object.defineProperty(s,o,{configurable:!0,get:function(){return u.call(this)},set:function(s){a=\"\"+s,_.call(this,s)}}),Object.defineProperty(s,o,{enumerable:i.enumerable}),{getValue:function(){return a},setValue:function(s){a=\"\"+s},stopTracking:function(){s._valueTracker=null,delete s[o]}}}}(s))}function Wa(s){if(!s)return!1;var o=s._valueTracker;if(!o)return!0;var i=o.getValue(),a=\"\";return s&&(a=Ta(s)?s.checked?\"true\":\"false\":s.value),(s=a)!==i&&(o.setValue(s),!0)}function Xa(s){if(void 0===(s=s||(\"undefined\"!=typeof document?document:void 0)))return null;try{return s.activeElement||s.body}catch(o){return s.body}}function Ya(s,o){var i=o.checked;return we({},o,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=i?i:s._wrapperState.initialChecked})}function Za(s,o){var i=null==o.defaultValue?\"\":o.defaultValue,a=null!=o.checked?o.checked:o.defaultChecked;i=Sa(null!=o.value?o.value:i),s._wrapperState={initialChecked:a,initialValue:i,controlled:\"checkbox\"===o.type||\"radio\"===o.type?null!=o.checked:null!=o.value}}function ab(s,o){null!=(o=o.checked)&&ta(s,\"checked\",o,!1)}function bb(s,o){ab(s,o);var i=Sa(o.value),a=o.type;if(null!=i)\"number\"===a?(0===i&&\"\"===s.value||s.value!=i)&&(s.value=\"\"+i):s.value!==\"\"+i&&(s.value=\"\"+i);else if(\"submit\"===a||\"reset\"===a)return void s.removeAttribute(\"value\");o.hasOwnProperty(\"value\")?cb(s,o.type,i):o.hasOwnProperty(\"defaultValue\")&&cb(s,o.type,Sa(o.defaultValue)),null==o.checked&&null!=o.defaultChecked&&(s.defaultChecked=!!o.defaultChecked)}function db(s,o,i){if(o.hasOwnProperty(\"value\")||o.hasOwnProperty(\"defaultValue\")){var a=o.type;if(!(\"submit\"!==a&&\"reset\"!==a||void 0!==o.value&&null!==o.value))return;o=\"\"+s._wrapperState.initialValue,i||o===s.value||(s.value=o),s.defaultValue=o}\"\"!==(i=s.name)&&(s.name=\"\"),s.defaultChecked=!!s._wrapperState.initialChecked,\"\"!==i&&(s.name=i)}function cb(s,o,i){\"number\"===o&&Xa(s.ownerDocument)===s||(null==i?s.defaultValue=\"\"+s._wrapperState.initialValue:s.defaultValue!==\"\"+i&&(s.defaultValue=\"\"+i))}var Pe=Array.isArray;function fb(s,o,i,a){if(s=s.options,o){o={};for(var u=0;u<i.length;u++)o[\"$\"+i[u]]=!0;for(i=0;i<s.length;i++)u=o.hasOwnProperty(\"$\"+s[i].value),s[i].selected!==u&&(s[i].selected=u),u&&a&&(s[i].defaultSelected=!0)}else{for(i=\"\"+Sa(i),o=null,u=0;u<s.length;u++){if(s[u].value===i)return s[u].selected=!0,void(a&&(s[u].defaultSelected=!0));null!==o||s[u].disabled||(o=s[u])}null!==o&&(o.selected=!0)}}function gb(s,o){if(null!=o.dangerouslySetInnerHTML)throw Error(p(91));return we({},o,{value:void 0,defaultValue:void 0,children:\"\"+s._wrapperState.initialValue})}function hb(s,o){var i=o.value;if(null==i){if(i=o.children,o=o.defaultValue,null!=i){if(null!=o)throw Error(p(92));if(Pe(i)){if(1<i.length)throw Error(p(93));i=i[0]}o=i}null==o&&(o=\"\"),i=o}s._wrapperState={initialValue:Sa(i)}}function ib(s,o){var i=Sa(o.value),a=Sa(o.defaultValue);null!=i&&((i=\"\"+i)!==s.value&&(s.value=i),null==o.defaultValue&&s.defaultValue!==i&&(s.defaultValue=i)),null!=a&&(s.defaultValue=\"\"+a)}function jb(s){var o=s.textContent;o===s._wrapperState.initialValue&&\"\"!==o&&null!==o&&(s.value=o)}function kb(s){switch(s){case\"svg\":return\"http://www.w3.org/2000/svg\";case\"math\":return\"http://www.w3.org/1998/Math/MathML\";default:return\"http://www.w3.org/1999/xhtml\"}}function lb(s,o){return null==s||\"http://www.w3.org/1999/xhtml\"===s?kb(o):\"http://www.w3.org/2000/svg\"===s&&\"foreignObject\"===o?\"http://www.w3.org/1999/xhtml\":s}var Te,Re,$e=(Re=function(s,o){if(\"http://www.w3.org/2000/svg\"!==s.namespaceURI||\"innerHTML\"in s)s.innerHTML=o;else{for((Te=Te||document.createElement(\"div\")).innerHTML=\"<svg>\"+o.valueOf().toString()+\"</svg>\",o=Te.firstChild;s.firstChild;)s.removeChild(s.firstChild);for(;o.firstChild;)s.appendChild(o.firstChild)}},\"undefined\"!=typeof MSApp&&MSApp.execUnsafeLocalFunction?function(s,o,i,a){MSApp.execUnsafeLocalFunction((function(){return Re(s,o)}))}:Re);function ob(s,o){if(o){var i=s.firstChild;if(i&&i===s.lastChild&&3===i.nodeType)return void(i.nodeValue=o)}s.textContent=o}var qe={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},ze=[\"Webkit\",\"ms\",\"Moz\",\"O\"];function rb(s,o,i){return null==o||\"boolean\"==typeof o||\"\"===o?\"\":i||\"number\"!=typeof o||0===o||qe.hasOwnProperty(s)&&qe[s]?(\"\"+o).trim():o+\"px\"}function sb(s,o){for(var i in s=s.style,o)if(o.hasOwnProperty(i)){var a=0===i.indexOf(\"--\"),u=rb(i,o[i],a);\"float\"===i&&(i=\"cssFloat\"),a?s.setProperty(i,u):s[i]=u}}Object.keys(qe).forEach((function(s){ze.forEach((function(o){o=o+s.charAt(0).toUpperCase()+s.substring(1),qe[o]=qe[s]}))}));var We=we({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ub(s,o){if(o){if(We[s]&&(null!=o.children||null!=o.dangerouslySetInnerHTML))throw Error(p(137,s));if(null!=o.dangerouslySetInnerHTML){if(null!=o.children)throw Error(p(60));if(\"object\"!=typeof o.dangerouslySetInnerHTML||!(\"__html\"in o.dangerouslySetInnerHTML))throw Error(p(61))}if(null!=o.style&&\"object\"!=typeof o.style)throw Error(p(62))}}function vb(s,o){if(-1===s.indexOf(\"-\"))return\"string\"==typeof o.is;switch(s){case\"annotation-xml\":case\"color-profile\":case\"font-face\":case\"font-face-src\":case\"font-face-uri\":case\"font-face-format\":case\"font-face-name\":case\"missing-glyph\":return!1;default:return!0}}var He=null;function xb(s){return(s=s.target||s.srcElement||window).correspondingUseElement&&(s=s.correspondingUseElement),3===s.nodeType?s.parentNode:s}var Ye=null,Xe=null,Qe=null;function Bb(s){if(s=Cb(s)){if(\"function\"!=typeof Ye)throw Error(p(280));var o=s.stateNode;o&&(o=Db(o),Ye(s.stateNode,s.type,o))}}function Eb(s){Xe?Qe?Qe.push(s):Qe=[s]:Xe=s}function Fb(){if(Xe){var s=Xe,o=Qe;if(Qe=Xe=null,Bb(s),o)for(s=0;s<o.length;s++)Bb(o[s])}}function Gb(s,o){return s(o)}function Hb(){}var et=!1;function Jb(s,o,i){if(et)return s(o,i);et=!0;try{return Gb(s,o,i)}finally{et=!1,(null!==Xe||null!==Qe)&&(Hb(),Fb())}}function Kb(s,o){var i=s.stateNode;if(null===i)return null;var a=Db(i);if(null===a)return null;i=a[o];e:switch(o){case\"onClick\":case\"onClickCapture\":case\"onDoubleClick\":case\"onDoubleClickCapture\":case\"onMouseDown\":case\"onMouseDownCapture\":case\"onMouseMove\":case\"onMouseMoveCapture\":case\"onMouseUp\":case\"onMouseUpCapture\":case\"onMouseEnter\":(a=!a.disabled)||(a=!(\"button\"===(s=s.type)||\"input\"===s||\"select\"===s||\"textarea\"===s)),s=!a;break e;default:s=!1}if(s)return null;if(i&&\"function\"!=typeof i)throw Error(p(231,o,typeof i));return i}var tt=!1;if(x)try{var rt={};Object.defineProperty(rt,\"passive\",{get:function(){tt=!0}}),window.addEventListener(\"test\",rt,rt),window.removeEventListener(\"test\",rt,rt)}catch(Re){tt=!1}function Nb(s,o,i,a,u,_,w,x,C){var j=Array.prototype.slice.call(arguments,3);try{o.apply(i,j)}catch(s){this.onError(s)}}var nt=!1,st=null,ot=!1,it=null,at={onError:function(s){nt=!0,st=s}};function Tb(s,o,i,a,u,_,w,x,C){nt=!1,st=null,Nb.apply(at,arguments)}function Vb(s){var o=s,i=s;if(s.alternate)for(;o.return;)o=o.return;else{s=o;do{!!(4098&(o=s).flags)&&(i=o.return),s=o.return}while(s)}return 3===o.tag?i:null}function Wb(s){if(13===s.tag){var o=s.memoizedState;if(null===o&&(null!==(s=s.alternate)&&(o=s.memoizedState)),null!==o)return o.dehydrated}return null}function Xb(s){if(Vb(s)!==s)throw Error(p(188))}function Zb(s){return null!==(s=function Yb(s){var o=s.alternate;if(!o){if(null===(o=Vb(s)))throw Error(p(188));return o!==s?null:s}for(var i=s,a=o;;){var u=i.return;if(null===u)break;var _=u.alternate;if(null===_){if(null!==(a=u.return)){i=a;continue}break}if(u.child===_.child){for(_=u.child;_;){if(_===i)return Xb(u),s;if(_===a)return Xb(u),o;_=_.sibling}throw Error(p(188))}if(i.return!==a.return)i=u,a=_;else{for(var w=!1,x=u.child;x;){if(x===i){w=!0,i=u,a=_;break}if(x===a){w=!0,a=u,i=_;break}x=x.sibling}if(!w){for(x=_.child;x;){if(x===i){w=!0,i=_,a=u;break}if(x===a){w=!0,a=_,i=u;break}x=x.sibling}if(!w)throw Error(p(189))}}if(i.alternate!==a)throw Error(p(190))}if(3!==i.tag)throw Error(p(188));return i.stateNode.current===i?s:o}(s))?$b(s):null}function $b(s){if(5===s.tag||6===s.tag)return s;for(s=s.child;null!==s;){var o=$b(s);if(null!==o)return o;s=s.sibling}return null}var ct=u.unstable_scheduleCallback,lt=u.unstable_cancelCallback,ut=u.unstable_shouldYield,pt=u.unstable_requestPaint,ht=u.unstable_now,dt=u.unstable_getCurrentPriorityLevel,mt=u.unstable_ImmediatePriority,gt=u.unstable_UserBlockingPriority,yt=u.unstable_NormalPriority,vt=u.unstable_LowPriority,bt=u.unstable_IdlePriority,_t=null,St=null;var Et=Math.clz32?Math.clz32:function nc(s){return s>>>=0,0===s?32:31-(wt(s)/xt|0)|0},wt=Math.log,xt=Math.LN2;var kt=64,Ot=4194304;function tc(s){switch(s&-s){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return 4194240&s;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return 130023424&s;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return s}}function uc(s,o){var i=s.pendingLanes;if(0===i)return 0;var a=0,u=s.suspendedLanes,_=s.pingedLanes,w=268435455&i;if(0!==w){var x=w&~u;0!==x?a=tc(x):0!==(_&=w)&&(a=tc(_))}else 0!==(w=i&~u)?a=tc(w):0!==_&&(a=tc(_));if(0===a)return 0;if(0!==o&&o!==a&&!(o&u)&&((u=a&-a)>=(_=o&-o)||16===u&&4194240&_))return o;if(4&a&&(a|=16&i),0!==(o=s.entangledLanes))for(s=s.entanglements,o&=a;0<o;)u=1<<(i=31-Et(o)),a|=s[i],o&=~u;return a}function vc(s,o){switch(s){case 1:case 2:case 4:return o+250;case 8:case 16:case 32:case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return o+5e3;default:return-1}}function xc(s){return 0!==(s=-1073741825&s.pendingLanes)?s:1073741824&s?1073741824:0}function yc(){var s=kt;return!(4194240&(kt<<=1))&&(kt=64),s}function zc(s){for(var o=[],i=0;31>i;i++)o.push(s);return o}function Ac(s,o,i){s.pendingLanes|=o,536870912!==o&&(s.suspendedLanes=0,s.pingedLanes=0),(s=s.eventTimes)[o=31-Et(o)]=i}function Cc(s,o){var i=s.entangledLanes|=o;for(s=s.entanglements;i;){var a=31-Et(i),u=1<<a;u&o|s[a]&o&&(s[a]|=o),i&=~u}}var At=0;function Dc(s){return 1<(s&=-s)?4<s?268435455&s?16:536870912:4:1}var Ct,jt,Pt,It,Tt,Nt=!1,Mt=[],Rt=null,Dt=null,Lt=null,Ft=new Map,Bt=new Map,$t=[],qt=\"mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset submit\".split(\" \");function Sc(s,o){switch(s){case\"focusin\":case\"focusout\":Rt=null;break;case\"dragenter\":case\"dragleave\":Dt=null;break;case\"mouseover\":case\"mouseout\":Lt=null;break;case\"pointerover\":case\"pointerout\":Ft.delete(o.pointerId);break;case\"gotpointercapture\":case\"lostpointercapture\":Bt.delete(o.pointerId)}}function Tc(s,o,i,a,u,_){return null===s||s.nativeEvent!==_?(s={blockedOn:o,domEventName:i,eventSystemFlags:a,nativeEvent:_,targetContainers:[u]},null!==o&&(null!==(o=Cb(o))&&jt(o)),s):(s.eventSystemFlags|=a,o=s.targetContainers,null!==u&&-1===o.indexOf(u)&&o.push(u),s)}function Vc(s){var o=Wc(s.target);if(null!==o){var i=Vb(o);if(null!==i)if(13===(o=i.tag)){if(null!==(o=Wb(i)))return s.blockedOn=o,void Tt(s.priority,(function(){Pt(i)}))}else if(3===o&&i.stateNode.current.memoizedState.isDehydrated)return void(s.blockedOn=3===i.tag?i.stateNode.containerInfo:null)}s.blockedOn=null}function Xc(s){if(null!==s.blockedOn)return!1;for(var o=s.targetContainers;0<o.length;){var i=Yc(s.domEventName,s.eventSystemFlags,o[0],s.nativeEvent);if(null!==i)return null!==(o=Cb(i))&&jt(o),s.blockedOn=i,!1;var a=new(i=s.nativeEvent).constructor(i.type,i);He=a,i.target.dispatchEvent(a),He=null,o.shift()}return!0}function Zc(s,o,i){Xc(s)&&i.delete(o)}function $c(){Nt=!1,null!==Rt&&Xc(Rt)&&(Rt=null),null!==Dt&&Xc(Dt)&&(Dt=null),null!==Lt&&Xc(Lt)&&(Lt=null),Ft.forEach(Zc),Bt.forEach(Zc)}function ad(s,o){s.blockedOn===o&&(s.blockedOn=null,Nt||(Nt=!0,u.unstable_scheduleCallback(u.unstable_NormalPriority,$c)))}function bd(s){function b(o){return ad(o,s)}if(0<Mt.length){ad(Mt[0],s);for(var o=1;o<Mt.length;o++){var i=Mt[o];i.blockedOn===s&&(i.blockedOn=null)}}for(null!==Rt&&ad(Rt,s),null!==Dt&&ad(Dt,s),null!==Lt&&ad(Lt,s),Ft.forEach(b),Bt.forEach(b),o=0;o<$t.length;o++)(i=$t[o]).blockedOn===s&&(i.blockedOn=null);for(;0<$t.length&&null===(o=$t[0]).blockedOn;)Vc(o),null===o.blockedOn&&$t.shift()}var Ut=V.ReactCurrentBatchConfig,Vt=!0;function ed(s,o,i,a){var u=At,_=Ut.transition;Ut.transition=null;try{At=1,fd(s,o,i,a)}finally{At=u,Ut.transition=_}}function gd(s,o,i,a){var u=At,_=Ut.transition;Ut.transition=null;try{At=4,fd(s,o,i,a)}finally{At=u,Ut.transition=_}}function fd(s,o,i,a){if(Vt){var u=Yc(s,o,i,a);if(null===u)hd(s,o,a,zt,i),Sc(s,a);else if(function Uc(s,o,i,a,u){switch(o){case\"focusin\":return Rt=Tc(Rt,s,o,i,a,u),!0;case\"dragenter\":return Dt=Tc(Dt,s,o,i,a,u),!0;case\"mouseover\":return Lt=Tc(Lt,s,o,i,a,u),!0;case\"pointerover\":var _=u.pointerId;return Ft.set(_,Tc(Ft.get(_)||null,s,o,i,a,u)),!0;case\"gotpointercapture\":return _=u.pointerId,Bt.set(_,Tc(Bt.get(_)||null,s,o,i,a,u)),!0}return!1}(u,s,o,i,a))a.stopPropagation();else if(Sc(s,a),4&o&&-1<qt.indexOf(s)){for(;null!==u;){var _=Cb(u);if(null!==_&&Ct(_),null===(_=Yc(s,o,i,a))&&hd(s,o,a,zt,i),_===u)break;u=_}null!==u&&a.stopPropagation()}else hd(s,o,a,null,i)}}var zt=null;function Yc(s,o,i,a){if(zt=null,null!==(s=Wc(s=xb(a))))if(null===(o=Vb(s)))s=null;else if(13===(i=o.tag)){if(null!==(s=Wb(o)))return s;s=null}else if(3===i){if(o.stateNode.current.memoizedState.isDehydrated)return 3===o.tag?o.stateNode.containerInfo:null;s=null}else o!==s&&(s=null);return zt=s,null}function jd(s){switch(s){case\"cancel\":case\"click\":case\"close\":case\"contextmenu\":case\"copy\":case\"cut\":case\"auxclick\":case\"dblclick\":case\"dragend\":case\"dragstart\":case\"drop\":case\"focusin\":case\"focusout\":case\"input\":case\"invalid\":case\"keydown\":case\"keypress\":case\"keyup\":case\"mousedown\":case\"mouseup\":case\"paste\":case\"pause\":case\"play\":case\"pointercancel\":case\"pointerdown\":case\"pointerup\":case\"ratechange\":case\"reset\":case\"resize\":case\"seeked\":case\"submit\":case\"touchcancel\":case\"touchend\":case\"touchstart\":case\"volumechange\":case\"change\":case\"selectionchange\":case\"textInput\":case\"compositionstart\":case\"compositionend\":case\"compositionupdate\":case\"beforeblur\":case\"afterblur\":case\"beforeinput\":case\"blur\":case\"fullscreenchange\":case\"focus\":case\"hashchange\":case\"popstate\":case\"select\":case\"selectstart\":return 1;case\"drag\":case\"dragenter\":case\"dragexit\":case\"dragleave\":case\"dragover\":case\"mousemove\":case\"mouseout\":case\"mouseover\":case\"pointermove\":case\"pointerout\":case\"pointerover\":case\"scroll\":case\"toggle\":case\"touchmove\":case\"wheel\":case\"mouseenter\":case\"mouseleave\":case\"pointerenter\":case\"pointerleave\":return 4;case\"message\":switch(dt()){case mt:return 1;case gt:return 4;case yt:case vt:return 16;case bt:return 536870912;default:return 16}default:return 16}}var Wt=null,Jt=null,Ht=null;function nd(){if(Ht)return Ht;var s,o,i=Jt,a=i.length,u=\"value\"in Wt?Wt.value:Wt.textContent,_=u.length;for(s=0;s<a&&i[s]===u[s];s++);var w=a-s;for(o=1;o<=w&&i[a-o]===u[_-o];o++);return Ht=u.slice(s,1<o?1-o:void 0)}function od(s){var o=s.keyCode;return\"charCode\"in s?0===(s=s.charCode)&&13===o&&(s=13):s=o,10===s&&(s=13),32<=s||13===s?s:0}function pd(){return!0}function qd(){return!1}function rd(s){function b(o,i,a,u,_){for(var w in this._reactName=o,this._targetInst=a,this.type=i,this.nativeEvent=u,this.target=_,this.currentTarget=null,s)s.hasOwnProperty(w)&&(o=s[w],this[w]=o?o(u):u[w]);return this.isDefaultPrevented=(null!=u.defaultPrevented?u.defaultPrevented:!1===u.returnValue)?pd:qd,this.isPropagationStopped=qd,this}return we(b.prototype,{preventDefault:function(){this.defaultPrevented=!0;var s=this.nativeEvent;s&&(s.preventDefault?s.preventDefault():\"unknown\"!=typeof s.returnValue&&(s.returnValue=!1),this.isDefaultPrevented=pd)},stopPropagation:function(){var s=this.nativeEvent;s&&(s.stopPropagation?s.stopPropagation():\"unknown\"!=typeof s.cancelBubble&&(s.cancelBubble=!0),this.isPropagationStopped=pd)},persist:function(){},isPersistent:pd}),b}var Kt,Gt,Yt,Xt={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(s){return s.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},Qt=rd(Xt),Zt=we({},Xt,{view:0,detail:0}),er=rd(Zt),tr=we({},Zt,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:zd,button:0,buttons:0,relatedTarget:function(s){return void 0===s.relatedTarget?s.fromElement===s.srcElement?s.toElement:s.fromElement:s.relatedTarget},movementX:function(s){return\"movementX\"in s?s.movementX:(s!==Yt&&(Yt&&\"mousemove\"===s.type?(Kt=s.screenX-Yt.screenX,Gt=s.screenY-Yt.screenY):Gt=Kt=0,Yt=s),Kt)},movementY:function(s){return\"movementY\"in s?s.movementY:Gt}}),rr=rd(tr),nr=rd(we({},tr,{dataTransfer:0})),sr=rd(we({},Zt,{relatedTarget:0})),ir=rd(we({},Xt,{animationName:0,elapsedTime:0,pseudoElement:0})),ar=we({},Xt,{clipboardData:function(s){return\"clipboardData\"in s?s.clipboardData:window.clipboardData}}),cr=rd(ar),lr=rd(we({},Xt,{data:0})),ur={Esc:\"Escape\",Spacebar:\" \",Left:\"ArrowLeft\",Up:\"ArrowUp\",Right:\"ArrowRight\",Down:\"ArrowDown\",Del:\"Delete\",Win:\"OS\",Menu:\"ContextMenu\",Apps:\"ContextMenu\",Scroll:\"ScrollLock\",MozPrintableKey:\"Unidentified\"},pr={8:\"Backspace\",9:\"Tab\",12:\"Clear\",13:\"Enter\",16:\"Shift\",17:\"Control\",18:\"Alt\",19:\"Pause\",20:\"CapsLock\",27:\"Escape\",32:\" \",33:\"PageUp\",34:\"PageDown\",35:\"End\",36:\"Home\",37:\"ArrowLeft\",38:\"ArrowUp\",39:\"ArrowRight\",40:\"ArrowDown\",45:\"Insert\",46:\"Delete\",112:\"F1\",113:\"F2\",114:\"F3\",115:\"F4\",116:\"F5\",117:\"F6\",118:\"F7\",119:\"F8\",120:\"F9\",121:\"F10\",122:\"F11\",123:\"F12\",144:\"NumLock\",145:\"ScrollLock\",224:\"Meta\"},dr={Alt:\"altKey\",Control:\"ctrlKey\",Meta:\"metaKey\",Shift:\"shiftKey\"};function Pd(s){var o=this.nativeEvent;return o.getModifierState?o.getModifierState(s):!!(s=dr[s])&&!!o[s]}function zd(){return Pd}var fr=we({},Zt,{key:function(s){if(s.key){var o=ur[s.key]||s.key;if(\"Unidentified\"!==o)return o}return\"keypress\"===s.type?13===(s=od(s))?\"Enter\":String.fromCharCode(s):\"keydown\"===s.type||\"keyup\"===s.type?pr[s.keyCode]||\"Unidentified\":\"\"},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,repeat:0,locale:0,getModifierState:zd,charCode:function(s){return\"keypress\"===s.type?od(s):0},keyCode:function(s){return\"keydown\"===s.type||\"keyup\"===s.type?s.keyCode:0},which:function(s){return\"keypress\"===s.type?od(s):\"keydown\"===s.type||\"keyup\"===s.type?s.keyCode:0}}),mr=rd(fr),gr=rd(we({},tr,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0})),yr=rd(we({},Zt,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,ctrlKey:0,shiftKey:0,getModifierState:zd})),vr=rd(we({},Xt,{propertyName:0,elapsedTime:0,pseudoElement:0})),br=we({},tr,{deltaX:function(s){return\"deltaX\"in s?s.deltaX:\"wheelDeltaX\"in s?-s.wheelDeltaX:0},deltaY:function(s){return\"deltaY\"in s?s.deltaY:\"wheelDeltaY\"in s?-s.wheelDeltaY:\"wheelDelta\"in s?-s.wheelDelta:0},deltaZ:0,deltaMode:0}),_r=rd(br),Sr=[9,13,27,32],Er=x&&\"CompositionEvent\"in window,wr=null;x&&\"documentMode\"in document&&(wr=document.documentMode);var xr=x&&\"TextEvent\"in window&&!wr,kr=x&&(!Er||wr&&8<wr&&11>=wr),Or=String.fromCharCode(32),Ar=!1;function ge(s,o){switch(s){case\"keyup\":return-1!==Sr.indexOf(o.keyCode);case\"keydown\":return 229!==o.keyCode;case\"keypress\":case\"mousedown\":case\"focusout\":return!0;default:return!1}}function he(s){return\"object\"==typeof(s=s.detail)&&\"data\"in s?s.data:null}var Cr=!1;var jr={color:!0,date:!0,datetime:!0,\"datetime-local\":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function me(s){var o=s&&s.nodeName&&s.nodeName.toLowerCase();return\"input\"===o?!!jr[s.type]:\"textarea\"===o}function ne(s,o,i,a){Eb(a),0<(o=oe(o,\"onChange\")).length&&(i=new Qt(\"onChange\",\"change\",null,i,a),s.push({event:i,listeners:o}))}var Pr=null,Ir=null;function re(s){se(s,0)}function te(s){if(Wa(ue(s)))return s}function ve(s,o){if(\"change\"===s)return o}var Tr=!1;if(x){var Nr;if(x){var Mr=\"oninput\"in document;if(!Mr){var Rr=document.createElement(\"div\");Rr.setAttribute(\"oninput\",\"return;\"),Mr=\"function\"==typeof Rr.oninput}Nr=Mr}else Nr=!1;Tr=Nr&&(!document.documentMode||9<document.documentMode)}function Ae(){Pr&&(Pr.detachEvent(\"onpropertychange\",Be),Ir=Pr=null)}function Be(s){if(\"value\"===s.propertyName&&te(Ir)){var o=[];ne(o,Ir,s,xb(s)),Jb(re,o)}}function Ce(s,o,i){\"focusin\"===s?(Ae(),Ir=i,(Pr=o).attachEvent(\"onpropertychange\",Be)):\"focusout\"===s&&Ae()}function De(s){if(\"selectionchange\"===s||\"keyup\"===s||\"keydown\"===s)return te(Ir)}function Ee(s,o){if(\"click\"===s)return te(o)}function Fe(s,o){if(\"input\"===s||\"change\"===s)return te(o)}var Dr=\"function\"==typeof Object.is?Object.is:function Ge(s,o){return s===o&&(0!==s||1/s==1/o)||s!=s&&o!=o};function Ie(s,o){if(Dr(s,o))return!0;if(\"object\"!=typeof s||null===s||\"object\"!=typeof o||null===o)return!1;var i=Object.keys(s),a=Object.keys(o);if(i.length!==a.length)return!1;for(a=0;a<i.length;a++){var u=i[a];if(!C.call(o,u)||!Dr(s[u],o[u]))return!1}return!0}function Je(s){for(;s&&s.firstChild;)s=s.firstChild;return s}function Ke(s,o){var i,a=Je(s);for(s=0;a;){if(3===a.nodeType){if(i=s+a.textContent.length,s<=o&&i>=o)return{node:a,offset:o-s};s=i}e:{for(;a;){if(a.nextSibling){a=a.nextSibling;break e}a=a.parentNode}a=void 0}a=Je(a)}}function Le(s,o){return!(!s||!o)&&(s===o||(!s||3!==s.nodeType)&&(o&&3===o.nodeType?Le(s,o.parentNode):\"contains\"in s?s.contains(o):!!s.compareDocumentPosition&&!!(16&s.compareDocumentPosition(o))))}function Me(){for(var s=window,o=Xa();o instanceof s.HTMLIFrameElement;){try{var i=\"string\"==typeof o.contentWindow.location.href}catch(s){i=!1}if(!i)break;o=Xa((s=o.contentWindow).document)}return o}function Ne(s){var o=s&&s.nodeName&&s.nodeName.toLowerCase();return o&&(\"input\"===o&&(\"text\"===s.type||\"search\"===s.type||\"tel\"===s.type||\"url\"===s.type||\"password\"===s.type)||\"textarea\"===o||\"true\"===s.contentEditable)}function Oe(s){var o=Me(),i=s.focusedElem,a=s.selectionRange;if(o!==i&&i&&i.ownerDocument&&Le(i.ownerDocument.documentElement,i)){if(null!==a&&Ne(i))if(o=a.start,void 0===(s=a.end)&&(s=o),\"selectionStart\"in i)i.selectionStart=o,i.selectionEnd=Math.min(s,i.value.length);else if((s=(o=i.ownerDocument||document)&&o.defaultView||window).getSelection){s=s.getSelection();var u=i.textContent.length,_=Math.min(a.start,u);a=void 0===a.end?_:Math.min(a.end,u),!s.extend&&_>a&&(u=a,a=_,_=u),u=Ke(i,_);var w=Ke(i,a);u&&w&&(1!==s.rangeCount||s.anchorNode!==u.node||s.anchorOffset!==u.offset||s.focusNode!==w.node||s.focusOffset!==w.offset)&&((o=o.createRange()).setStart(u.node,u.offset),s.removeAllRanges(),_>a?(s.addRange(o),s.extend(w.node,w.offset)):(o.setEnd(w.node,w.offset),s.addRange(o)))}for(o=[],s=i;s=s.parentNode;)1===s.nodeType&&o.push({element:s,left:s.scrollLeft,top:s.scrollTop});for(\"function\"==typeof i.focus&&i.focus(),i=0;i<o.length;i++)(s=o[i]).element.scrollLeft=s.left,s.element.scrollTop=s.top}}var Lr=x&&\"documentMode\"in document&&11>=document.documentMode,Fr=null,Br=null,$r=null,qr=!1;function Ue(s,o,i){var a=i.window===i?i.document:9===i.nodeType?i:i.ownerDocument;qr||null==Fr||Fr!==Xa(a)||(\"selectionStart\"in(a=Fr)&&Ne(a)?a={start:a.selectionStart,end:a.selectionEnd}:a={anchorNode:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection()).anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset},$r&&Ie($r,a)||($r=a,0<(a=oe(Br,\"onSelect\")).length&&(o=new Qt(\"onSelect\",\"select\",null,o,i),s.push({event:o,listeners:a}),o.target=Fr)))}function Ve(s,o){var i={};return i[s.toLowerCase()]=o.toLowerCase(),i[\"Webkit\"+s]=\"webkit\"+o,i[\"Moz\"+s]=\"moz\"+o,i}var Ur={animationend:Ve(\"Animation\",\"AnimationEnd\"),animationiteration:Ve(\"Animation\",\"AnimationIteration\"),animationstart:Ve(\"Animation\",\"AnimationStart\"),transitionend:Ve(\"Transition\",\"TransitionEnd\")},Vr={},zr={};function Ze(s){if(Vr[s])return Vr[s];if(!Ur[s])return s;var o,i=Ur[s];for(o in i)if(i.hasOwnProperty(o)&&o in zr)return Vr[s]=i[o];return s}x&&(zr=document.createElement(\"div\").style,\"AnimationEvent\"in window||(delete Ur.animationend.animation,delete Ur.animationiteration.animation,delete Ur.animationstart.animation),\"TransitionEvent\"in window||delete Ur.transitionend.transition);var Wr=Ze(\"animationend\"),Jr=Ze(\"animationiteration\"),Hr=Ze(\"animationstart\"),Kr=Ze(\"transitionend\"),Gr=new Map,Yr=\"abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel\".split(\" \");function ff(s,o){Gr.set(s,o),fa(o,[s])}for(var Xr=0;Xr<Yr.length;Xr++){var Qr=Yr[Xr];ff(Qr.toLowerCase(),\"on\"+(Qr[0].toUpperCase()+Qr.slice(1)))}ff(Wr,\"onAnimationEnd\"),ff(Jr,\"onAnimationIteration\"),ff(Hr,\"onAnimationStart\"),ff(\"dblclick\",\"onDoubleClick\"),ff(\"focusin\",\"onFocus\"),ff(\"focusout\",\"onBlur\"),ff(Kr,\"onTransitionEnd\"),ha(\"onMouseEnter\",[\"mouseout\",\"mouseover\"]),ha(\"onMouseLeave\",[\"mouseout\",\"mouseover\"]),ha(\"onPointerEnter\",[\"pointerout\",\"pointerover\"]),ha(\"onPointerLeave\",[\"pointerout\",\"pointerover\"]),fa(\"onChange\",\"change click focusin focusout input keydown keyup selectionchange\".split(\" \")),fa(\"onSelect\",\"focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange\".split(\" \")),fa(\"onBeforeInput\",[\"compositionend\",\"keypress\",\"textInput\",\"paste\"]),fa(\"onCompositionEnd\",\"compositionend focusout keydown keypress keyup mousedown\".split(\" \")),fa(\"onCompositionStart\",\"compositionstart focusout keydown keypress keyup mousedown\".split(\" \")),fa(\"onCompositionUpdate\",\"compositionupdate focusout keydown keypress keyup mousedown\".split(\" \"));var Zr=\"abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting\".split(\" \"),en=new Set(\"cancel close invalid load scroll toggle\".split(\" \").concat(Zr));function nf(s,o,i){var a=s.type||\"unknown-event\";s.currentTarget=i,function Ub(s,o,i,a,u,_,w,x,C){if(Tb.apply(this,arguments),nt){if(!nt)throw Error(p(198));var j=st;nt=!1,st=null,ot||(ot=!0,it=j)}}(a,o,void 0,s),s.currentTarget=null}function se(s,o){o=!!(4&o);for(var i=0;i<s.length;i++){var a=s[i],u=a.event;a=a.listeners;e:{var _=void 0;if(o)for(var w=a.length-1;0<=w;w--){var x=a[w],C=x.instance,j=x.currentTarget;if(x=x.listener,C!==_&&u.isPropagationStopped())break e;nf(u,x,j),_=C}else for(w=0;w<a.length;w++){if(C=(x=a[w]).instance,j=x.currentTarget,x=x.listener,C!==_&&u.isPropagationStopped())break e;nf(u,x,j),_=C}}}if(ot)throw s=it,ot=!1,it=null,s}function D(s,o){var i=o[mn];void 0===i&&(i=o[mn]=new Set);var a=s+\"__bubble\";i.has(a)||(pf(o,s,2,!1),i.add(a))}function qf(s,o,i){var a=0;o&&(a|=4),pf(i,s,a,o)}var tn=\"_reactListening\"+Math.random().toString(36).slice(2);function sf(s){if(!s[tn]){s[tn]=!0,_.forEach((function(o){\"selectionchange\"!==o&&(en.has(o)||qf(o,!1,s),qf(o,!0,s))}));var o=9===s.nodeType?s:s.ownerDocument;null===o||o[tn]||(o[tn]=!0,qf(\"selectionchange\",!1,o))}}function pf(s,o,i,a){switch(jd(o)){case 1:var u=ed;break;case 4:u=gd;break;default:u=fd}i=u.bind(null,o,i,s),u=void 0,!tt||\"touchstart\"!==o&&\"touchmove\"!==o&&\"wheel\"!==o||(u=!0),a?void 0!==u?s.addEventListener(o,i,{capture:!0,passive:u}):s.addEventListener(o,i,!0):void 0!==u?s.addEventListener(o,i,{passive:u}):s.addEventListener(o,i,!1)}function hd(s,o,i,a,u){var _=a;if(!(1&o||2&o||null===a))e:for(;;){if(null===a)return;var w=a.tag;if(3===w||4===w){var x=a.stateNode.containerInfo;if(x===u||8===x.nodeType&&x.parentNode===u)break;if(4===w)for(w=a.return;null!==w;){var C=w.tag;if((3===C||4===C)&&((C=w.stateNode.containerInfo)===u||8===C.nodeType&&C.parentNode===u))return;w=w.return}for(;null!==x;){if(null===(w=Wc(x)))return;if(5===(C=w.tag)||6===C){a=_=w;continue e}x=x.parentNode}}a=a.return}Jb((function(){var a=_,u=xb(i),w=[];e:{var x=Gr.get(s);if(void 0!==x){var C=Qt,j=s;switch(s){case\"keypress\":if(0===od(i))break e;case\"keydown\":case\"keyup\":C=mr;break;case\"focusin\":j=\"focus\",C=sr;break;case\"focusout\":j=\"blur\",C=sr;break;case\"beforeblur\":case\"afterblur\":C=sr;break;case\"click\":if(2===i.button)break e;case\"auxclick\":case\"dblclick\":case\"mousedown\":case\"mousemove\":case\"mouseup\":case\"mouseout\":case\"mouseover\":case\"contextmenu\":C=rr;break;case\"drag\":case\"dragend\":case\"dragenter\":case\"dragexit\":case\"dragleave\":case\"dragover\":case\"dragstart\":case\"drop\":C=nr;break;case\"touchcancel\":case\"touchend\":case\"touchmove\":case\"touchstart\":C=yr;break;case Wr:case Jr:case Hr:C=ir;break;case Kr:C=vr;break;case\"scroll\":C=er;break;case\"wheel\":C=_r;break;case\"copy\":case\"cut\":case\"paste\":C=cr;break;case\"gotpointercapture\":case\"lostpointercapture\":case\"pointercancel\":case\"pointerdown\":case\"pointermove\":case\"pointerout\":case\"pointerover\":case\"pointerup\":C=gr}var L=!!(4&o),B=!L&&\"scroll\"===s,$=L?null!==x?x+\"Capture\":null:x;L=[];for(var U,V=a;null!==V;){var z=(U=V).stateNode;if(5===U.tag&&null!==z&&(U=z,null!==$&&(null!=(z=Kb(V,$))&&L.push(tf(V,z,U)))),B)break;V=V.return}0<L.length&&(x=new C(x,j,null,i,u),w.push({event:x,listeners:L}))}}if(!(7&o)){if(C=\"mouseout\"===s||\"pointerout\"===s,(!(x=\"mouseover\"===s||\"pointerover\"===s)||i===He||!(j=i.relatedTarget||i.fromElement)||!Wc(j)&&!j[fn])&&(C||x)&&(x=u.window===u?u:(x=u.ownerDocument)?x.defaultView||x.parentWindow:window,C?(C=a,null!==(j=(j=i.relatedTarget||i.toElement)?Wc(j):null)&&(j!==(B=Vb(j))||5!==j.tag&&6!==j.tag)&&(j=null)):(C=null,j=a),C!==j)){if(L=rr,z=\"onMouseLeave\",$=\"onMouseEnter\",V=\"mouse\",\"pointerout\"!==s&&\"pointerover\"!==s||(L=gr,z=\"onPointerLeave\",$=\"onPointerEnter\",V=\"pointer\"),B=null==C?x:ue(C),U=null==j?x:ue(j),(x=new L(z,V+\"leave\",C,i,u)).target=B,x.relatedTarget=U,z=null,Wc(u)===a&&((L=new L($,V+\"enter\",j,i,u)).target=U,L.relatedTarget=B,z=L),B=z,C&&j)e:{for($=j,V=0,U=L=C;U;U=vf(U))V++;for(U=0,z=$;z;z=vf(z))U++;for(;0<V-U;)L=vf(L),V--;for(;0<U-V;)$=vf($),U--;for(;V--;){if(L===$||null!==$&&L===$.alternate)break e;L=vf(L),$=vf($)}L=null}else L=null;null!==C&&wf(w,x,C,L,!1),null!==j&&null!==B&&wf(w,B,j,L,!0)}if(\"select\"===(C=(x=a?ue(a):window).nodeName&&x.nodeName.toLowerCase())||\"input\"===C&&\"file\"===x.type)var Y=ve;else if(me(x))if(Tr)Y=Fe;else{Y=De;var Z=Ce}else(C=x.nodeName)&&\"input\"===C.toLowerCase()&&(\"checkbox\"===x.type||\"radio\"===x.type)&&(Y=Ee);switch(Y&&(Y=Y(s,a))?ne(w,Y,i,u):(Z&&Z(s,x,a),\"focusout\"===s&&(Z=x._wrapperState)&&Z.controlled&&\"number\"===x.type&&cb(x,\"number\",x.value)),Z=a?ue(a):window,s){case\"focusin\":(me(Z)||\"true\"===Z.contentEditable)&&(Fr=Z,Br=a,$r=null);break;case\"focusout\":$r=Br=Fr=null;break;case\"mousedown\":qr=!0;break;case\"contextmenu\":case\"mouseup\":case\"dragend\":qr=!1,Ue(w,i,u);break;case\"selectionchange\":if(Lr)break;case\"keydown\":case\"keyup\":Ue(w,i,u)}var ee;if(Er)e:{switch(s){case\"compositionstart\":var ie=\"onCompositionStart\";break e;case\"compositionend\":ie=\"onCompositionEnd\";break e;case\"compositionupdate\":ie=\"onCompositionUpdate\";break e}ie=void 0}else Cr?ge(s,i)&&(ie=\"onCompositionEnd\"):\"keydown\"===s&&229===i.keyCode&&(ie=\"onCompositionStart\");ie&&(kr&&\"ko\"!==i.locale&&(Cr||\"onCompositionStart\"!==ie?\"onCompositionEnd\"===ie&&Cr&&(ee=nd()):(Jt=\"value\"in(Wt=u)?Wt.value:Wt.textContent,Cr=!0)),0<(Z=oe(a,ie)).length&&(ie=new lr(ie,s,null,i,u),w.push({event:ie,listeners:Z}),ee?ie.data=ee:null!==(ee=he(i))&&(ie.data=ee))),(ee=xr?function je(s,o){switch(s){case\"compositionend\":return he(o);case\"keypress\":return 32!==o.which?null:(Ar=!0,Or);case\"textInput\":return(s=o.data)===Or&&Ar?null:s;default:return null}}(s,i):function ke(s,o){if(Cr)return\"compositionend\"===s||!Er&&ge(s,o)?(s=nd(),Ht=Jt=Wt=null,Cr=!1,s):null;switch(s){case\"paste\":default:return null;case\"keypress\":if(!(o.ctrlKey||o.altKey||o.metaKey)||o.ctrlKey&&o.altKey){if(o.char&&1<o.char.length)return o.char;if(o.which)return String.fromCharCode(o.which)}return null;case\"compositionend\":return kr&&\"ko\"!==o.locale?null:o.data}}(s,i))&&(0<(a=oe(a,\"onBeforeInput\")).length&&(u=new lr(\"onBeforeInput\",\"beforeinput\",null,i,u),w.push({event:u,listeners:a}),u.data=ee))}se(w,o)}))}function tf(s,o,i){return{instance:s,listener:o,currentTarget:i}}function oe(s,o){for(var i=o+\"Capture\",a=[];null!==s;){var u=s,_=u.stateNode;5===u.tag&&null!==_&&(u=_,null!=(_=Kb(s,i))&&a.unshift(tf(s,_,u)),null!=(_=Kb(s,o))&&a.push(tf(s,_,u))),s=s.return}return a}function vf(s){if(null===s)return null;do{s=s.return}while(s&&5!==s.tag);return s||null}function wf(s,o,i,a,u){for(var _=o._reactName,w=[];null!==i&&i!==a;){var x=i,C=x.alternate,j=x.stateNode;if(null!==C&&C===a)break;5===x.tag&&null!==j&&(x=j,u?null!=(C=Kb(i,_))&&w.unshift(tf(i,C,x)):u||null!=(C=Kb(i,_))&&w.push(tf(i,C,x))),i=i.return}0!==w.length&&s.push({event:o,listeners:w})}var rn=/\\r\\n?/g,nn=/\\u0000|\\uFFFD/g;function zf(s){return(\"string\"==typeof s?s:\"\"+s).replace(rn,\"\\n\").replace(nn,\"\")}function Af(s,o,i){if(o=zf(o),zf(s)!==o&&i)throw Error(p(425))}function Bf(){}var sn=null,on=null;function Ef(s,o){return\"textarea\"===s||\"noscript\"===s||\"string\"==typeof o.children||\"number\"==typeof o.children||\"object\"==typeof o.dangerouslySetInnerHTML&&null!==o.dangerouslySetInnerHTML&&null!=o.dangerouslySetInnerHTML.__html}var an=\"function\"==typeof setTimeout?setTimeout:void 0,cn=\"function\"==typeof clearTimeout?clearTimeout:void 0,ln=\"function\"==typeof Promise?Promise:void 0,un=\"function\"==typeof queueMicrotask?queueMicrotask:void 0!==ln?function(s){return ln.resolve(null).then(s).catch(If)}:an;function If(s){setTimeout((function(){throw s}))}function Kf(s,o){var i=o,a=0;do{var u=i.nextSibling;if(s.removeChild(i),u&&8===u.nodeType)if(\"/$\"===(i=u.data)){if(0===a)return s.removeChild(u),void bd(o);a--}else\"$\"!==i&&\"$?\"!==i&&\"$!\"!==i||a++;i=u}while(i);bd(o)}function Lf(s){for(;null!=s;s=s.nextSibling){var o=s.nodeType;if(1===o||3===o)break;if(8===o){if(\"$\"===(o=s.data)||\"$!\"===o||\"$?\"===o)break;if(\"/$\"===o)return null}}return s}function Mf(s){s=s.previousSibling;for(var o=0;s;){if(8===s.nodeType){var i=s.data;if(\"$\"===i||\"$!\"===i||\"$?\"===i){if(0===o)return s;o--}else\"/$\"===i&&o++}s=s.previousSibling}return null}var pn=Math.random().toString(36).slice(2),hn=\"__reactFiber$\"+pn,dn=\"__reactProps$\"+pn,fn=\"__reactContainer$\"+pn,mn=\"__reactEvents$\"+pn,gn=\"__reactListeners$\"+pn,yn=\"__reactHandles$\"+pn;function Wc(s){var o=s[hn];if(o)return o;for(var i=s.parentNode;i;){if(o=i[fn]||i[hn]){if(i=o.alternate,null!==o.child||null!==i&&null!==i.child)for(s=Mf(s);null!==s;){if(i=s[hn])return i;s=Mf(s)}return o}i=(s=i).parentNode}return null}function Cb(s){return!(s=s[hn]||s[fn])||5!==s.tag&&6!==s.tag&&13!==s.tag&&3!==s.tag?null:s}function ue(s){if(5===s.tag||6===s.tag)return s.stateNode;throw Error(p(33))}function Db(s){return s[dn]||null}var vn=[],bn=-1;function Uf(s){return{current:s}}function E(s){0>bn||(s.current=vn[bn],vn[bn]=null,bn--)}function G(s,o){bn++,vn[bn]=s.current,s.current=o}var _n={},Sn=Uf(_n),En=Uf(!1),wn=_n;function Yf(s,o){var i=s.type.contextTypes;if(!i)return _n;var a=s.stateNode;if(a&&a.__reactInternalMemoizedUnmaskedChildContext===o)return a.__reactInternalMemoizedMaskedChildContext;var u,_={};for(u in i)_[u]=o[u];return a&&((s=s.stateNode).__reactInternalMemoizedUnmaskedChildContext=o,s.__reactInternalMemoizedMaskedChildContext=_),_}function Zf(s){return null!=(s=s.childContextTypes)}function $f(){E(En),E(Sn)}function ag(s,o,i){if(Sn.current!==_n)throw Error(p(168));G(Sn,o),G(En,i)}function bg(s,o,i){var a=s.stateNode;if(o=o.childContextTypes,\"function\"!=typeof a.getChildContext)return i;for(var u in a=a.getChildContext())if(!(u in o))throw Error(p(108,Ra(s)||\"Unknown\",u));return we({},i,a)}function cg(s){return s=(s=s.stateNode)&&s.__reactInternalMemoizedMergedChildContext||_n,wn=Sn.current,G(Sn,s),G(En,En.current),!0}function dg(s,o,i){var a=s.stateNode;if(!a)throw Error(p(169));i?(s=bg(s,o,wn),a.__reactInternalMemoizedMergedChildContext=s,E(En),E(Sn),G(Sn,s)):E(En),G(En,i)}var xn=null,kn=!1,On=!1;function hg(s){null===xn?xn=[s]:xn.push(s)}function jg(){if(!On&&null!==xn){On=!0;var s=0,o=At;try{var i=xn;for(At=1;s<i.length;s++){var a=i[s];do{a=a(!0)}while(null!==a)}xn=null,kn=!1}catch(o){throw null!==xn&&(xn=xn.slice(s+1)),ct(mt,jg),o}finally{At=o,On=!1}}return null}var An=[],Cn=0,jn=null,Pn=0,In=[],Tn=0,Nn=null,Mn=1,Rn=\"\";function tg(s,o){An[Cn++]=Pn,An[Cn++]=jn,jn=s,Pn=o}function ug(s,o,i){In[Tn++]=Mn,In[Tn++]=Rn,In[Tn++]=Nn,Nn=s;var a=Mn;s=Rn;var u=32-Et(a)-1;a&=~(1<<u),i+=1;var _=32-Et(o)+u;if(30<_){var w=u-u%5;_=(a&(1<<w)-1).toString(32),a>>=w,u-=w,Mn=1<<32-Et(o)+u|i<<u|a,Rn=_+s}else Mn=1<<_|i<<u|a,Rn=s}function vg(s){null!==s.return&&(tg(s,1),ug(s,1,0))}function wg(s){for(;s===jn;)jn=An[--Cn],An[Cn]=null,Pn=An[--Cn],An[Cn]=null;for(;s===Nn;)Nn=In[--Tn],In[Tn]=null,Rn=In[--Tn],In[Tn]=null,Mn=In[--Tn],In[Tn]=null}var Dn=null,Ln=null,Fn=!1,Bn=null;function Ag(s,o){var i=Bg(5,null,null,0);i.elementType=\"DELETED\",i.stateNode=o,i.return=s,null===(o=s.deletions)?(s.deletions=[i],s.flags|=16):o.push(i)}function Cg(s,o){switch(s.tag){case 5:var i=s.type;return null!==(o=1!==o.nodeType||i.toLowerCase()!==o.nodeName.toLowerCase()?null:o)&&(s.stateNode=o,Dn=s,Ln=Lf(o.firstChild),!0);case 6:return null!==(o=\"\"===s.pendingProps||3!==o.nodeType?null:o)&&(s.stateNode=o,Dn=s,Ln=null,!0);case 13:return null!==(o=8!==o.nodeType?null:o)&&(i=null!==Nn?{id:Mn,overflow:Rn}:null,s.memoizedState={dehydrated:o,treeContext:i,retryLane:1073741824},(i=Bg(18,null,null,0)).stateNode=o,i.return=s,s.child=i,Dn=s,Ln=null,!0);default:return!1}}function Dg(s){return!(!(1&s.mode)||128&s.flags)}function Eg(s){if(Fn){var o=Ln;if(o){var i=o;if(!Cg(s,o)){if(Dg(s))throw Error(p(418));o=Lf(i.nextSibling);var a=Dn;o&&Cg(s,o)?Ag(a,i):(s.flags=-4097&s.flags|2,Fn=!1,Dn=s)}}else{if(Dg(s))throw Error(p(418));s.flags=-4097&s.flags|2,Fn=!1,Dn=s}}}function Fg(s){for(s=s.return;null!==s&&5!==s.tag&&3!==s.tag&&13!==s.tag;)s=s.return;Dn=s}function Gg(s){if(s!==Dn)return!1;if(!Fn)return Fg(s),Fn=!0,!1;var o;if((o=3!==s.tag)&&!(o=5!==s.tag)&&(o=\"head\"!==(o=s.type)&&\"body\"!==o&&!Ef(s.type,s.memoizedProps)),o&&(o=Ln)){if(Dg(s))throw Hg(),Error(p(418));for(;o;)Ag(s,o),o=Lf(o.nextSibling)}if(Fg(s),13===s.tag){if(!(s=null!==(s=s.memoizedState)?s.dehydrated:null))throw Error(p(317));e:{for(s=s.nextSibling,o=0;s;){if(8===s.nodeType){var i=s.data;if(\"/$\"===i){if(0===o){Ln=Lf(s.nextSibling);break e}o--}else\"$\"!==i&&\"$!\"!==i&&\"$?\"!==i||o++}s=s.nextSibling}Ln=null}}else Ln=Dn?Lf(s.stateNode.nextSibling):null;return!0}function Hg(){for(var s=Ln;s;)s=Lf(s.nextSibling)}function Ig(){Ln=Dn=null,Fn=!1}function Jg(s){null===Bn?Bn=[s]:Bn.push(s)}var $n=V.ReactCurrentBatchConfig;function Lg(s,o,i){if(null!==(s=i.ref)&&\"function\"!=typeof s&&\"object\"!=typeof s){if(i._owner){if(i=i._owner){if(1!==i.tag)throw Error(p(309));var a=i.stateNode}if(!a)throw Error(p(147,s));var u=a,_=\"\"+s;return null!==o&&null!==o.ref&&\"function\"==typeof o.ref&&o.ref._stringRef===_?o.ref:(o=function(s){var o=u.refs;null===s?delete o[_]:o[_]=s},o._stringRef=_,o)}if(\"string\"!=typeof s)throw Error(p(284));if(!i._owner)throw Error(p(290,s))}return s}function Mg(s,o){throw s=Object.prototype.toString.call(o),Error(p(31,\"[object Object]\"===s?\"object with keys {\"+Object.keys(o).join(\", \")+\"}\":s))}function Ng(s){return(0,s._init)(s._payload)}function Og(s){function b(o,i){if(s){var a=o.deletions;null===a?(o.deletions=[i],o.flags|=16):a.push(i)}}function c(o,i){if(!s)return null;for(;null!==i;)b(o,i),i=i.sibling;return null}function d(s,o){for(s=new Map;null!==o;)null!==o.key?s.set(o.key,o):s.set(o.index,o),o=o.sibling;return s}function e(s,o){return(s=Pg(s,o)).index=0,s.sibling=null,s}function f(o,i,a){return o.index=a,s?null!==(a=o.alternate)?(a=a.index)<i?(o.flags|=2,i):a:(o.flags|=2,i):(o.flags|=1048576,i)}function g(o){return s&&null===o.alternate&&(o.flags|=2),o}function h(s,o,i,a){return null===o||6!==o.tag?((o=Qg(i,s.mode,a)).return=s,o):((o=e(o,i)).return=s,o)}function k(s,o,i,a){var u=i.type;return u===Z?m(s,o,i.props.children,a,i.key):null!==o&&(o.elementType===u||\"object\"==typeof u&&null!==u&&u.$$typeof===ye&&Ng(u)===o.type)?((a=e(o,i.props)).ref=Lg(s,o,i),a.return=s,a):((a=Rg(i.type,i.key,i.props,null,s.mode,a)).ref=Lg(s,o,i),a.return=s,a)}function l(s,o,i,a){return null===o||4!==o.tag||o.stateNode.containerInfo!==i.containerInfo||o.stateNode.implementation!==i.implementation?((o=Sg(i,s.mode,a)).return=s,o):((o=e(o,i.children||[])).return=s,o)}function m(s,o,i,a,u){return null===o||7!==o.tag?((o=Tg(i,s.mode,a,u)).return=s,o):((o=e(o,i)).return=s,o)}function q(s,o,i){if(\"string\"==typeof o&&\"\"!==o||\"number\"==typeof o)return(o=Qg(\"\"+o,s.mode,i)).return=s,o;if(\"object\"==typeof o&&null!==o){switch(o.$$typeof){case z:return(i=Rg(o.type,o.key,o.props,null,s.mode,i)).ref=Lg(s,null,o),i.return=s,i;case Y:return(o=Sg(o,s.mode,i)).return=s,o;case ye:return q(s,(0,o._init)(o._payload),i)}if(Pe(o)||Ka(o))return(o=Tg(o,s.mode,i,null)).return=s,o;Mg(s,o)}return null}function r(s,o,i,a){var u=null!==o?o.key:null;if(\"string\"==typeof i&&\"\"!==i||\"number\"==typeof i)return null!==u?null:h(s,o,\"\"+i,a);if(\"object\"==typeof i&&null!==i){switch(i.$$typeof){case z:return i.key===u?k(s,o,i,a):null;case Y:return i.key===u?l(s,o,i,a):null;case ye:return r(s,o,(u=i._init)(i._payload),a)}if(Pe(i)||Ka(i))return null!==u?null:m(s,o,i,a,null);Mg(s,i)}return null}function y(s,o,i,a,u){if(\"string\"==typeof a&&\"\"!==a||\"number\"==typeof a)return h(o,s=s.get(i)||null,\"\"+a,u);if(\"object\"==typeof a&&null!==a){switch(a.$$typeof){case z:return k(o,s=s.get(null===a.key?i:a.key)||null,a,u);case Y:return l(o,s=s.get(null===a.key?i:a.key)||null,a,u);case ye:return y(s,o,i,(0,a._init)(a._payload),u)}if(Pe(a)||Ka(a))return m(o,s=s.get(i)||null,a,u,null);Mg(o,a)}return null}function n(o,i,a,u){for(var _=null,w=null,x=i,C=i=0,j=null;null!==x&&C<a.length;C++){x.index>C?(j=x,x=null):j=x.sibling;var L=r(o,x,a[C],u);if(null===L){null===x&&(x=j);break}s&&x&&null===L.alternate&&b(o,x),i=f(L,i,C),null===w?_=L:w.sibling=L,w=L,x=j}if(C===a.length)return c(o,x),Fn&&tg(o,C),_;if(null===x){for(;C<a.length;C++)null!==(x=q(o,a[C],u))&&(i=f(x,i,C),null===w?_=x:w.sibling=x,w=x);return Fn&&tg(o,C),_}for(x=d(o,x);C<a.length;C++)null!==(j=y(x,o,C,a[C],u))&&(s&&null!==j.alternate&&x.delete(null===j.key?C:j.key),i=f(j,i,C),null===w?_=j:w.sibling=j,w=j);return s&&x.forEach((function(s){return b(o,s)})),Fn&&tg(o,C),_}function t(o,i,a,u){var _=Ka(a);if(\"function\"!=typeof _)throw Error(p(150));if(null==(a=_.call(a)))throw Error(p(151));for(var w=_=null,x=i,C=i=0,j=null,L=a.next();null!==x&&!L.done;C++,L=a.next()){x.index>C?(j=x,x=null):j=x.sibling;var B=r(o,x,L.value,u);if(null===B){null===x&&(x=j);break}s&&x&&null===B.alternate&&b(o,x),i=f(B,i,C),null===w?_=B:w.sibling=B,w=B,x=j}if(L.done)return c(o,x),Fn&&tg(o,C),_;if(null===x){for(;!L.done;C++,L=a.next())null!==(L=q(o,L.value,u))&&(i=f(L,i,C),null===w?_=L:w.sibling=L,w=L);return Fn&&tg(o,C),_}for(x=d(o,x);!L.done;C++,L=a.next())null!==(L=y(x,o,C,L.value,u))&&(s&&null!==L.alternate&&x.delete(null===L.key?C:L.key),i=f(L,i,C),null===w?_=L:w.sibling=L,w=L);return s&&x.forEach((function(s){return b(o,s)})),Fn&&tg(o,C),_}return function J(s,o,i,a){if(\"object\"==typeof i&&null!==i&&i.type===Z&&null===i.key&&(i=i.props.children),\"object\"==typeof i&&null!==i){switch(i.$$typeof){case z:e:{for(var u=i.key,_=o;null!==_;){if(_.key===u){if((u=i.type)===Z){if(7===_.tag){c(s,_.sibling),(o=e(_,i.props.children)).return=s,s=o;break e}}else if(_.elementType===u||\"object\"==typeof u&&null!==u&&u.$$typeof===ye&&Ng(u)===_.type){c(s,_.sibling),(o=e(_,i.props)).ref=Lg(s,_,i),o.return=s,s=o;break e}c(s,_);break}b(s,_),_=_.sibling}i.type===Z?((o=Tg(i.props.children,s.mode,a,i.key)).return=s,s=o):((a=Rg(i.type,i.key,i.props,null,s.mode,a)).ref=Lg(s,o,i),a.return=s,s=a)}return g(s);case Y:e:{for(_=i.key;null!==o;){if(o.key===_){if(4===o.tag&&o.stateNode.containerInfo===i.containerInfo&&o.stateNode.implementation===i.implementation){c(s,o.sibling),(o=e(o,i.children||[])).return=s,s=o;break e}c(s,o);break}b(s,o),o=o.sibling}(o=Sg(i,s.mode,a)).return=s,s=o}return g(s);case ye:return J(s,o,(_=i._init)(i._payload),a)}if(Pe(i))return n(s,o,i,a);if(Ka(i))return t(s,o,i,a);Mg(s,i)}return\"string\"==typeof i&&\"\"!==i||\"number\"==typeof i?(i=\"\"+i,null!==o&&6===o.tag?(c(s,o.sibling),(o=e(o,i)).return=s,s=o):(c(s,o),(o=Qg(i,s.mode,a)).return=s,s=o),g(s)):c(s,o)}}var qn=Og(!0),Un=Og(!1),Vn=Uf(null),zn=null,Wn=null,Jn=null;function $g(){Jn=Wn=zn=null}function ah(s){var o=Vn.current;E(Vn),s._currentValue=o}function bh(s,o,i){for(;null!==s;){var a=s.alternate;if((s.childLanes&o)!==o?(s.childLanes|=o,null!==a&&(a.childLanes|=o)):null!==a&&(a.childLanes&o)!==o&&(a.childLanes|=o),s===i)break;s=s.return}}function ch(s,o){zn=s,Jn=Wn=null,null!==(s=s.dependencies)&&null!==s.firstContext&&(!!(s.lanes&o)&&(bs=!0),s.firstContext=null)}function eh(s){var o=s._currentValue;if(Jn!==s)if(s={context:s,memoizedValue:o,next:null},null===Wn){if(null===zn)throw Error(p(308));Wn=s,zn.dependencies={lanes:0,firstContext:s}}else Wn=Wn.next=s;return o}var Hn=null;function gh(s){null===Hn?Hn=[s]:Hn.push(s)}function hh(s,o,i,a){var u=o.interleaved;return null===u?(i.next=i,gh(o)):(i.next=u.next,u.next=i),o.interleaved=i,ih(s,a)}function ih(s,o){s.lanes|=o;var i=s.alternate;for(null!==i&&(i.lanes|=o),i=s,s=s.return;null!==s;)s.childLanes|=o,null!==(i=s.alternate)&&(i.childLanes|=o),i=s,s=s.return;return 3===i.tag?i.stateNode:null}var Kn=!1;function kh(s){s.updateQueue={baseState:s.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function lh(s,o){s=s.updateQueue,o.updateQueue===s&&(o.updateQueue={baseState:s.baseState,firstBaseUpdate:s.firstBaseUpdate,lastBaseUpdate:s.lastBaseUpdate,shared:s.shared,effects:s.effects})}function mh(s,o){return{eventTime:s,lane:o,tag:0,payload:null,callback:null,next:null}}function nh(s,o,i){var a=s.updateQueue;if(null===a)return null;if(a=a.shared,2&Ls){var u=a.pending;return null===u?o.next=o:(o.next=u.next,u.next=o),a.pending=o,ih(s,i)}return null===(u=a.interleaved)?(o.next=o,gh(a)):(o.next=u.next,u.next=o),a.interleaved=o,ih(s,i)}function oh(s,o,i){if(null!==(o=o.updateQueue)&&(o=o.shared,4194240&i)){var a=o.lanes;i|=a&=s.pendingLanes,o.lanes=i,Cc(s,i)}}function ph(s,o){var i=s.updateQueue,a=s.alternate;if(null!==a&&i===(a=a.updateQueue)){var u=null,_=null;if(null!==(i=i.firstBaseUpdate)){do{var w={eventTime:i.eventTime,lane:i.lane,tag:i.tag,payload:i.payload,callback:i.callback,next:null};null===_?u=_=w:_=_.next=w,i=i.next}while(null!==i);null===_?u=_=o:_=_.next=o}else u=_=o;return i={baseState:a.baseState,firstBaseUpdate:u,lastBaseUpdate:_,shared:a.shared,effects:a.effects},void(s.updateQueue=i)}null===(s=i.lastBaseUpdate)?i.firstBaseUpdate=o:s.next=o,i.lastBaseUpdate=o}function qh(s,o,i,a){var u=s.updateQueue;Kn=!1;var _=u.firstBaseUpdate,w=u.lastBaseUpdate,x=u.shared.pending;if(null!==x){u.shared.pending=null;var C=x,j=C.next;C.next=null,null===w?_=j:w.next=j,w=C;var L=s.alternate;null!==L&&((x=(L=L.updateQueue).lastBaseUpdate)!==w&&(null===x?L.firstBaseUpdate=j:x.next=j,L.lastBaseUpdate=C))}if(null!==_){var B=u.baseState;for(w=0,L=j=C=null,x=_;;){var $=x.lane,U=x.eventTime;if((a&$)===$){null!==L&&(L=L.next={eventTime:U,lane:0,tag:x.tag,payload:x.payload,callback:x.callback,next:null});e:{var V=s,z=x;switch($=o,U=i,z.tag){case 1:if(\"function\"==typeof(V=z.payload)){B=V.call(U,B,$);break e}B=V;break e;case 3:V.flags=-65537&V.flags|128;case 0:if(null==($=\"function\"==typeof(V=z.payload)?V.call(U,B,$):V))break e;B=we({},B,$);break e;case 2:Kn=!0}}null!==x.callback&&0!==x.lane&&(s.flags|=64,null===($=u.effects)?u.effects=[x]:$.push(x))}else U={eventTime:U,lane:$,tag:x.tag,payload:x.payload,callback:x.callback,next:null},null===L?(j=L=U,C=B):L=L.next=U,w|=$;if(null===(x=x.next)){if(null===(x=u.shared.pending))break;x=($=x).next,$.next=null,u.lastBaseUpdate=$,u.shared.pending=null}}if(null===L&&(C=B),u.baseState=C,u.firstBaseUpdate=j,u.lastBaseUpdate=L,null!==(o=u.shared.interleaved)){u=o;do{w|=u.lane,u=u.next}while(u!==o)}else null===_&&(u.shared.lanes=0);Ws|=w,s.lanes=w,s.memoizedState=B}}function sh(s,o,i){if(s=o.effects,o.effects=null,null!==s)for(o=0;o<s.length;o++){var a=s[o],u=a.callback;if(null!==u){if(a.callback=null,a=i,\"function\"!=typeof u)throw Error(p(191,u));u.call(a)}}}var Gn={},Yn=Uf(Gn),Xn=Uf(Gn),Qn=Uf(Gn);function xh(s){if(s===Gn)throw Error(p(174));return s}function yh(s,o){switch(G(Qn,o),G(Xn,s),G(Yn,Gn),s=o.nodeType){case 9:case 11:o=(o=o.documentElement)?o.namespaceURI:lb(null,\"\");break;default:o=lb(o=(s=8===s?o.parentNode:o).namespaceURI||null,s=s.tagName)}E(Yn),G(Yn,o)}function zh(){E(Yn),E(Xn),E(Qn)}function Ah(s){xh(Qn.current);var o=xh(Yn.current),i=lb(o,s.type);o!==i&&(G(Xn,s),G(Yn,i))}function Bh(s){Xn.current===s&&(E(Yn),E(Xn))}var Zn=Uf(0);function Ch(s){for(var o=s;null!==o;){if(13===o.tag){var i=o.memoizedState;if(null!==i&&(null===(i=i.dehydrated)||\"$?\"===i.data||\"$!\"===i.data))return o}else if(19===o.tag&&void 0!==o.memoizedProps.revealOrder){if(128&o.flags)return o}else if(null!==o.child){o.child.return=o,o=o.child;continue}if(o===s)break;for(;null===o.sibling;){if(null===o.return||o.return===s)return null;o=o.return}o.sibling.return=o.return,o=o.sibling}return null}var es=[];function Eh(){for(var s=0;s<es.length;s++)es[s]._workInProgressVersionPrimary=null;es.length=0}var ts=V.ReactCurrentDispatcher,rs=V.ReactCurrentBatchConfig,ns=0,ss=null,os=null,as=null,cs=!1,ls=!1,us=0,ps=0;function P(){throw Error(p(321))}function Mh(s,o){if(null===o)return!1;for(var i=0;i<o.length&&i<s.length;i++)if(!Dr(s[i],o[i]))return!1;return!0}function Nh(s,o,i,a,u,_){if(ns=_,ss=o,o.memoizedState=null,o.updateQueue=null,o.lanes=0,ts.current=null===s||null===s.memoizedState?ds:fs,s=i(a,u),ls){_=0;do{if(ls=!1,us=0,25<=_)throw Error(p(301));_+=1,as=os=null,o.updateQueue=null,ts.current=ms,s=i(a,u)}while(ls)}if(ts.current=hs,o=null!==os&&null!==os.next,ns=0,as=os=ss=null,cs=!1,o)throw Error(p(300));return s}function Sh(){var s=0!==us;return us=0,s}function Th(){var s={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return null===as?ss.memoizedState=as=s:as=as.next=s,as}function Uh(){if(null===os){var s=ss.alternate;s=null!==s?s.memoizedState:null}else s=os.next;var o=null===as?ss.memoizedState:as.next;if(null!==o)as=o,os=s;else{if(null===s)throw Error(p(310));s={memoizedState:(os=s).memoizedState,baseState:os.baseState,baseQueue:os.baseQueue,queue:os.queue,next:null},null===as?ss.memoizedState=as=s:as=as.next=s}return as}function Vh(s,o){return\"function\"==typeof o?o(s):o}function Wh(s){var o=Uh(),i=o.queue;if(null===i)throw Error(p(311));i.lastRenderedReducer=s;var a=os,u=a.baseQueue,_=i.pending;if(null!==_){if(null!==u){var w=u.next;u.next=_.next,_.next=w}a.baseQueue=u=_,i.pending=null}if(null!==u){_=u.next,a=a.baseState;var x=w=null,C=null,j=_;do{var L=j.lane;if((ns&L)===L)null!==C&&(C=C.next={lane:0,action:j.action,hasEagerState:j.hasEagerState,eagerState:j.eagerState,next:null}),a=j.hasEagerState?j.eagerState:s(a,j.action);else{var B={lane:L,action:j.action,hasEagerState:j.hasEagerState,eagerState:j.eagerState,next:null};null===C?(x=C=B,w=a):C=C.next=B,ss.lanes|=L,Ws|=L}j=j.next}while(null!==j&&j!==_);null===C?w=a:C.next=x,Dr(a,o.memoizedState)||(bs=!0),o.memoizedState=a,o.baseState=w,o.baseQueue=C,i.lastRenderedState=a}if(null!==(s=i.interleaved)){u=s;do{_=u.lane,ss.lanes|=_,Ws|=_,u=u.next}while(u!==s)}else null===u&&(i.lanes=0);return[o.memoizedState,i.dispatch]}function Xh(s){var o=Uh(),i=o.queue;if(null===i)throw Error(p(311));i.lastRenderedReducer=s;var a=i.dispatch,u=i.pending,_=o.memoizedState;if(null!==u){i.pending=null;var w=u=u.next;do{_=s(_,w.action),w=w.next}while(w!==u);Dr(_,o.memoizedState)||(bs=!0),o.memoizedState=_,null===o.baseQueue&&(o.baseState=_),i.lastRenderedState=_}return[_,a]}function Yh(){}function Zh(s,o){var i=ss,a=Uh(),u=o(),_=!Dr(a.memoizedState,u);if(_&&(a.memoizedState=u,bs=!0),a=a.queue,$h(ai.bind(null,i,a,s),[s]),a.getSnapshot!==o||_||null!==as&&1&as.memoizedState.tag){if(i.flags|=2048,bi(9,ci.bind(null,i,a,u,o),void 0,null),null===Fs)throw Error(p(349));30&ns||di(i,o,u)}return u}function di(s,o,i){s.flags|=16384,s={getSnapshot:o,value:i},null===(o=ss.updateQueue)?(o={lastEffect:null,stores:null},ss.updateQueue=o,o.stores=[s]):null===(i=o.stores)?o.stores=[s]:i.push(s)}function ci(s,o,i,a){o.value=i,o.getSnapshot=a,ei(o)&&fi(s)}function ai(s,o,i){return i((function(){ei(o)&&fi(s)}))}function ei(s){var o=s.getSnapshot;s=s.value;try{var i=o();return!Dr(s,i)}catch(s){return!0}}function fi(s){var o=ih(s,1);null!==o&&gi(o,s,1,-1)}function hi(s){var o=Th();return\"function\"==typeof s&&(s=s()),o.memoizedState=o.baseState=s,s={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:Vh,lastRenderedState:s},o.queue=s,s=s.dispatch=ii.bind(null,ss,s),[o.memoizedState,s]}function bi(s,o,i,a){return s={tag:s,create:o,destroy:i,deps:a,next:null},null===(o=ss.updateQueue)?(o={lastEffect:null,stores:null},ss.updateQueue=o,o.lastEffect=s.next=s):null===(i=o.lastEffect)?o.lastEffect=s.next=s:(a=i.next,i.next=s,s.next=a,o.lastEffect=s),s}function ji(){return Uh().memoizedState}function ki(s,o,i,a){var u=Th();ss.flags|=s,u.memoizedState=bi(1|o,i,void 0,void 0===a?null:a)}function li(s,o,i,a){var u=Uh();a=void 0===a?null:a;var _=void 0;if(null!==os){var w=os.memoizedState;if(_=w.destroy,null!==a&&Mh(a,w.deps))return void(u.memoizedState=bi(o,i,_,a))}ss.flags|=s,u.memoizedState=bi(1|o,i,_,a)}function mi(s,o){return ki(8390656,8,s,o)}function $h(s,o){return li(2048,8,s,o)}function ni(s,o){return li(4,2,s,o)}function oi(s,o){return li(4,4,s,o)}function pi(s,o){return\"function\"==typeof o?(s=s(),o(s),function(){o(null)}):null!=o?(s=s(),o.current=s,function(){o.current=null}):void 0}function qi(s,o,i){return i=null!=i?i.concat([s]):null,li(4,4,pi.bind(null,o,s),i)}function ri(){}function si(s,o){var i=Uh();o=void 0===o?null:o;var a=i.memoizedState;return null!==a&&null!==o&&Mh(o,a[1])?a[0]:(i.memoizedState=[s,o],s)}function ti(s,o){var i=Uh();o=void 0===o?null:o;var a=i.memoizedState;return null!==a&&null!==o&&Mh(o,a[1])?a[0]:(s=s(),i.memoizedState=[s,o],s)}function ui(s,o,i){return 21&ns?(Dr(i,o)||(i=yc(),ss.lanes|=i,Ws|=i,s.baseState=!0),o):(s.baseState&&(s.baseState=!1,bs=!0),s.memoizedState=i)}function vi(s,o){var i=At;At=0!==i&&4>i?i:4,s(!0);var a=rs.transition;rs.transition={};try{s(!1),o()}finally{At=i,rs.transition=a}}function wi(){return Uh().memoizedState}function xi(s,o,i){var a=yi(s);if(i={lane:a,action:i,hasEagerState:!1,eagerState:null,next:null},zi(s))Ai(o,i);else if(null!==(i=hh(s,o,i,a))){gi(i,s,a,R()),Bi(i,o,a)}}function ii(s,o,i){var a=yi(s),u={lane:a,action:i,hasEagerState:!1,eagerState:null,next:null};if(zi(s))Ai(o,u);else{var _=s.alternate;if(0===s.lanes&&(null===_||0===_.lanes)&&null!==(_=o.lastRenderedReducer))try{var w=o.lastRenderedState,x=_(w,i);if(u.hasEagerState=!0,u.eagerState=x,Dr(x,w)){var C=o.interleaved;return null===C?(u.next=u,gh(o)):(u.next=C.next,C.next=u),void(o.interleaved=u)}}catch(s){}null!==(i=hh(s,o,u,a))&&(gi(i,s,a,u=R()),Bi(i,o,a))}}function zi(s){var o=s.alternate;return s===ss||null!==o&&o===ss}function Ai(s,o){ls=cs=!0;var i=s.pending;null===i?o.next=o:(o.next=i.next,i.next=o),s.pending=o}function Bi(s,o,i){if(4194240&i){var a=o.lanes;i|=a&=s.pendingLanes,o.lanes=i,Cc(s,i)}}var hs={readContext:eh,useCallback:P,useContext:P,useEffect:P,useImperativeHandle:P,useInsertionEffect:P,useLayoutEffect:P,useMemo:P,useReducer:P,useRef:P,useState:P,useDebugValue:P,useDeferredValue:P,useTransition:P,useMutableSource:P,useSyncExternalStore:P,useId:P,unstable_isNewReconciler:!1},ds={readContext:eh,useCallback:function(s,o){return Th().memoizedState=[s,void 0===o?null:o],s},useContext:eh,useEffect:mi,useImperativeHandle:function(s,o,i){return i=null!=i?i.concat([s]):null,ki(4194308,4,pi.bind(null,o,s),i)},useLayoutEffect:function(s,o){return ki(4194308,4,s,o)},useInsertionEffect:function(s,o){return ki(4,2,s,o)},useMemo:function(s,o){var i=Th();return o=void 0===o?null:o,s=s(),i.memoizedState=[s,o],s},useReducer:function(s,o,i){var a=Th();return o=void 0!==i?i(o):o,a.memoizedState=a.baseState=o,s={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:s,lastRenderedState:o},a.queue=s,s=s.dispatch=xi.bind(null,ss,s),[a.memoizedState,s]},useRef:function(s){return s={current:s},Th().memoizedState=s},useState:hi,useDebugValue:ri,useDeferredValue:function(s){return Th().memoizedState=s},useTransition:function(){var s=hi(!1),o=s[0];return s=vi.bind(null,s[1]),Th().memoizedState=s,[o,s]},useMutableSource:function(){},useSyncExternalStore:function(s,o,i){var a=ss,u=Th();if(Fn){if(void 0===i)throw Error(p(407));i=i()}else{if(i=o(),null===Fs)throw Error(p(349));30&ns||di(a,o,i)}u.memoizedState=i;var _={value:i,getSnapshot:o};return u.queue=_,mi(ai.bind(null,a,_,s),[s]),a.flags|=2048,bi(9,ci.bind(null,a,_,i,o),void 0,null),i},useId:function(){var s=Th(),o=Fs.identifierPrefix;if(Fn){var i=Rn;o=\":\"+o+\"R\"+(i=(Mn&~(1<<32-Et(Mn)-1)).toString(32)+i),0<(i=us++)&&(o+=\"H\"+i.toString(32)),o+=\":\"}else o=\":\"+o+\"r\"+(i=ps++).toString(32)+\":\";return s.memoizedState=o},unstable_isNewReconciler:!1},fs={readContext:eh,useCallback:si,useContext:eh,useEffect:$h,useImperativeHandle:qi,useInsertionEffect:ni,useLayoutEffect:oi,useMemo:ti,useReducer:Wh,useRef:ji,useState:function(){return Wh(Vh)},useDebugValue:ri,useDeferredValue:function(s){return ui(Uh(),os.memoizedState,s)},useTransition:function(){return[Wh(Vh)[0],Uh().memoizedState]},useMutableSource:Yh,useSyncExternalStore:Zh,useId:wi,unstable_isNewReconciler:!1},ms={readContext:eh,useCallback:si,useContext:eh,useEffect:$h,useImperativeHandle:qi,useInsertionEffect:ni,useLayoutEffect:oi,useMemo:ti,useReducer:Xh,useRef:ji,useState:function(){return Xh(Vh)},useDebugValue:ri,useDeferredValue:function(s){var o=Uh();return null===os?o.memoizedState=s:ui(o,os.memoizedState,s)},useTransition:function(){return[Xh(Vh)[0],Uh().memoizedState]},useMutableSource:Yh,useSyncExternalStore:Zh,useId:wi,unstable_isNewReconciler:!1};function Ci(s,o){if(s&&s.defaultProps){for(var i in o=we({},o),s=s.defaultProps)void 0===o[i]&&(o[i]=s[i]);return o}return o}function Di(s,o,i,a){i=null==(i=i(a,o=s.memoizedState))?o:we({},o,i),s.memoizedState=i,0===s.lanes&&(s.updateQueue.baseState=i)}var gs={isMounted:function(s){return!!(s=s._reactInternals)&&Vb(s)===s},enqueueSetState:function(s,o,i){s=s._reactInternals;var a=R(),u=yi(s),_=mh(a,u);_.payload=o,null!=i&&(_.callback=i),null!==(o=nh(s,_,u))&&(gi(o,s,u,a),oh(o,s,u))},enqueueReplaceState:function(s,o,i){s=s._reactInternals;var a=R(),u=yi(s),_=mh(a,u);_.tag=1,_.payload=o,null!=i&&(_.callback=i),null!==(o=nh(s,_,u))&&(gi(o,s,u,a),oh(o,s,u))},enqueueForceUpdate:function(s,o){s=s._reactInternals;var i=R(),a=yi(s),u=mh(i,a);u.tag=2,null!=o&&(u.callback=o),null!==(o=nh(s,u,a))&&(gi(o,s,a,i),oh(o,s,a))}};function Fi(s,o,i,a,u,_,w){return\"function\"==typeof(s=s.stateNode).shouldComponentUpdate?s.shouldComponentUpdate(a,_,w):!o.prototype||!o.prototype.isPureReactComponent||(!Ie(i,a)||!Ie(u,_))}function Gi(s,o,i){var a=!1,u=_n,_=o.contextType;return\"object\"==typeof _&&null!==_?_=eh(_):(u=Zf(o)?wn:Sn.current,_=(a=null!=(a=o.contextTypes))?Yf(s,u):_n),o=new o(i,_),s.memoizedState=null!==o.state&&void 0!==o.state?o.state:null,o.updater=gs,s.stateNode=o,o._reactInternals=s,a&&((s=s.stateNode).__reactInternalMemoizedUnmaskedChildContext=u,s.__reactInternalMemoizedMaskedChildContext=_),o}function Hi(s,o,i,a){s=o.state,\"function\"==typeof o.componentWillReceiveProps&&o.componentWillReceiveProps(i,a),\"function\"==typeof o.UNSAFE_componentWillReceiveProps&&o.UNSAFE_componentWillReceiveProps(i,a),o.state!==s&&gs.enqueueReplaceState(o,o.state,null)}function Ii(s,o,i,a){var u=s.stateNode;u.props=i,u.state=s.memoizedState,u.refs={},kh(s);var _=o.contextType;\"object\"==typeof _&&null!==_?u.context=eh(_):(_=Zf(o)?wn:Sn.current,u.context=Yf(s,_)),u.state=s.memoizedState,\"function\"==typeof(_=o.getDerivedStateFromProps)&&(Di(s,o,_,i),u.state=s.memoizedState),\"function\"==typeof o.getDerivedStateFromProps||\"function\"==typeof u.getSnapshotBeforeUpdate||\"function\"!=typeof u.UNSAFE_componentWillMount&&\"function\"!=typeof u.componentWillMount||(o=u.state,\"function\"==typeof u.componentWillMount&&u.componentWillMount(),\"function\"==typeof u.UNSAFE_componentWillMount&&u.UNSAFE_componentWillMount(),o!==u.state&&gs.enqueueReplaceState(u,u.state,null),qh(s,i,u,a),u.state=s.memoizedState),\"function\"==typeof u.componentDidMount&&(s.flags|=4194308)}function Ji(s,o){try{var i=\"\",a=o;do{i+=Pa(a),a=a.return}while(a);var u=i}catch(s){u=\"\\nError generating stack: \"+s.message+\"\\n\"+s.stack}return{value:s,source:o,stack:u,digest:null}}function Ki(s,o,i){return{value:s,source:null,stack:null!=i?i:null,digest:null!=o?o:null}}function Li(s,o){try{console.error(o.value)}catch(s){setTimeout((function(){throw s}))}}var ys=\"function\"==typeof WeakMap?WeakMap:Map;function Ni(s,o,i){(i=mh(-1,i)).tag=3,i.payload={element:null};var a=o.value;return i.callback=function(){Zs||(Zs=!0,eo=a),Li(0,o)},i}function Qi(s,o,i){(i=mh(-1,i)).tag=3;var a=s.type.getDerivedStateFromError;if(\"function\"==typeof a){var u=o.value;i.payload=function(){return a(u)},i.callback=function(){Li(0,o)}}var _=s.stateNode;return null!==_&&\"function\"==typeof _.componentDidCatch&&(i.callback=function(){Li(0,o),\"function\"!=typeof a&&(null===to?to=new Set([this]):to.add(this));var s=o.stack;this.componentDidCatch(o.value,{componentStack:null!==s?s:\"\"})}),i}function Si(s,o,i){var a=s.pingCache;if(null===a){a=s.pingCache=new ys;var u=new Set;a.set(o,u)}else void 0===(u=a.get(o))&&(u=new Set,a.set(o,u));u.has(i)||(u.add(i),s=Ti.bind(null,s,o,i),o.then(s,s))}function Ui(s){do{var o;if((o=13===s.tag)&&(o=null===(o=s.memoizedState)||null!==o.dehydrated),o)return s;s=s.return}while(null!==s);return null}function Vi(s,o,i,a,u){return 1&s.mode?(s.flags|=65536,s.lanes=u,s):(s===o?s.flags|=65536:(s.flags|=128,i.flags|=131072,i.flags&=-52805,1===i.tag&&(null===i.alternate?i.tag=17:((o=mh(-1,1)).tag=2,nh(i,o,1))),i.lanes|=1),s)}var vs=V.ReactCurrentOwner,bs=!1;function Xi(s,o,i,a){o.child=null===s?Un(o,null,i,a):qn(o,s.child,i,a)}function Yi(s,o,i,a,u){i=i.render;var _=o.ref;return ch(o,u),a=Nh(s,o,i,a,_,u),i=Sh(),null===s||bs?(Fn&&i&&vg(o),o.flags|=1,Xi(s,o,a,u),o.child):(o.updateQueue=s.updateQueue,o.flags&=-2053,s.lanes&=~u,Zi(s,o,u))}function $i(s,o,i,a,u){if(null===s){var _=i.type;return\"function\"!=typeof _||aj(_)||void 0!==_.defaultProps||null!==i.compare||void 0!==i.defaultProps?((s=Rg(i.type,null,a,o,o.mode,u)).ref=o.ref,s.return=o,o.child=s):(o.tag=15,o.type=_,bj(s,o,_,a,u))}if(_=s.child,!(s.lanes&u)){var w=_.memoizedProps;if((i=null!==(i=i.compare)?i:Ie)(w,a)&&s.ref===o.ref)return Zi(s,o,u)}return o.flags|=1,(s=Pg(_,a)).ref=o.ref,s.return=o,o.child=s}function bj(s,o,i,a,u){if(null!==s){var _=s.memoizedProps;if(Ie(_,a)&&s.ref===o.ref){if(bs=!1,o.pendingProps=a=_,!(s.lanes&u))return o.lanes=s.lanes,Zi(s,o,u);131072&s.flags&&(bs=!0)}}return cj(s,o,i,a,u)}function dj(s,o,i){var a=o.pendingProps,u=a.children,_=null!==s?s.memoizedState:null;if(\"hidden\"===a.mode)if(1&o.mode){if(!(1073741824&i))return s=null!==_?_.baseLanes|i:i,o.lanes=o.childLanes=1073741824,o.memoizedState={baseLanes:s,cachePool:null,transitions:null},o.updateQueue=null,G(Us,qs),qs|=s,null;o.memoizedState={baseLanes:0,cachePool:null,transitions:null},a=null!==_?_.baseLanes:i,G(Us,qs),qs|=a}else o.memoizedState={baseLanes:0,cachePool:null,transitions:null},G(Us,qs),qs|=i;else null!==_?(a=_.baseLanes|i,o.memoizedState=null):a=i,G(Us,qs),qs|=a;return Xi(s,o,u,i),o.child}function gj(s,o){var i=o.ref;(null===s&&null!==i||null!==s&&s.ref!==i)&&(o.flags|=512,o.flags|=2097152)}function cj(s,o,i,a,u){var _=Zf(i)?wn:Sn.current;return _=Yf(o,_),ch(o,u),i=Nh(s,o,i,a,_,u),a=Sh(),null===s||bs?(Fn&&a&&vg(o),o.flags|=1,Xi(s,o,i,u),o.child):(o.updateQueue=s.updateQueue,o.flags&=-2053,s.lanes&=~u,Zi(s,o,u))}function hj(s,o,i,a,u){if(Zf(i)){var _=!0;cg(o)}else _=!1;if(ch(o,u),null===o.stateNode)ij(s,o),Gi(o,i,a),Ii(o,i,a,u),a=!0;else if(null===s){var w=o.stateNode,x=o.memoizedProps;w.props=x;var C=w.context,j=i.contextType;\"object\"==typeof j&&null!==j?j=eh(j):j=Yf(o,j=Zf(i)?wn:Sn.current);var L=i.getDerivedStateFromProps,B=\"function\"==typeof L||\"function\"==typeof w.getSnapshotBeforeUpdate;B||\"function\"!=typeof w.UNSAFE_componentWillReceiveProps&&\"function\"!=typeof w.componentWillReceiveProps||(x!==a||C!==j)&&Hi(o,w,a,j),Kn=!1;var $=o.memoizedState;w.state=$,qh(o,a,w,u),C=o.memoizedState,x!==a||$!==C||En.current||Kn?(\"function\"==typeof L&&(Di(o,i,L,a),C=o.memoizedState),(x=Kn||Fi(o,i,x,a,$,C,j))?(B||\"function\"!=typeof w.UNSAFE_componentWillMount&&\"function\"!=typeof w.componentWillMount||(\"function\"==typeof w.componentWillMount&&w.componentWillMount(),\"function\"==typeof w.UNSAFE_componentWillMount&&w.UNSAFE_componentWillMount()),\"function\"==typeof w.componentDidMount&&(o.flags|=4194308)):(\"function\"==typeof w.componentDidMount&&(o.flags|=4194308),o.memoizedProps=a,o.memoizedState=C),w.props=a,w.state=C,w.context=j,a=x):(\"function\"==typeof w.componentDidMount&&(o.flags|=4194308),a=!1)}else{w=o.stateNode,lh(s,o),x=o.memoizedProps,j=o.type===o.elementType?x:Ci(o.type,x),w.props=j,B=o.pendingProps,$=w.context,\"object\"==typeof(C=i.contextType)&&null!==C?C=eh(C):C=Yf(o,C=Zf(i)?wn:Sn.current);var U=i.getDerivedStateFromProps;(L=\"function\"==typeof U||\"function\"==typeof w.getSnapshotBeforeUpdate)||\"function\"!=typeof w.UNSAFE_componentWillReceiveProps&&\"function\"!=typeof w.componentWillReceiveProps||(x!==B||$!==C)&&Hi(o,w,a,C),Kn=!1,$=o.memoizedState,w.state=$,qh(o,a,w,u);var V=o.memoizedState;x!==B||$!==V||En.current||Kn?(\"function\"==typeof U&&(Di(o,i,U,a),V=o.memoizedState),(j=Kn||Fi(o,i,j,a,$,V,C)||!1)?(L||\"function\"!=typeof w.UNSAFE_componentWillUpdate&&\"function\"!=typeof w.componentWillUpdate||(\"function\"==typeof w.componentWillUpdate&&w.componentWillUpdate(a,V,C),\"function\"==typeof w.UNSAFE_componentWillUpdate&&w.UNSAFE_componentWillUpdate(a,V,C)),\"function\"==typeof w.componentDidUpdate&&(o.flags|=4),\"function\"==typeof w.getSnapshotBeforeUpdate&&(o.flags|=1024)):(\"function\"!=typeof w.componentDidUpdate||x===s.memoizedProps&&$===s.memoizedState||(o.flags|=4),\"function\"!=typeof w.getSnapshotBeforeUpdate||x===s.memoizedProps&&$===s.memoizedState||(o.flags|=1024),o.memoizedProps=a,o.memoizedState=V),w.props=a,w.state=V,w.context=C,a=j):(\"function\"!=typeof w.componentDidUpdate||x===s.memoizedProps&&$===s.memoizedState||(o.flags|=4),\"function\"!=typeof w.getSnapshotBeforeUpdate||x===s.memoizedProps&&$===s.memoizedState||(o.flags|=1024),a=!1)}return jj(s,o,i,a,_,u)}function jj(s,o,i,a,u,_){gj(s,o);var w=!!(128&o.flags);if(!a&&!w)return u&&dg(o,i,!1),Zi(s,o,_);a=o.stateNode,vs.current=o;var x=w&&\"function\"!=typeof i.getDerivedStateFromError?null:a.render();return o.flags|=1,null!==s&&w?(o.child=qn(o,s.child,null,_),o.child=qn(o,null,x,_)):Xi(s,o,x,_),o.memoizedState=a.state,u&&dg(o,i,!0),o.child}function kj(s){var o=s.stateNode;o.pendingContext?ag(0,o.pendingContext,o.pendingContext!==o.context):o.context&&ag(0,o.context,!1),yh(s,o.containerInfo)}function lj(s,o,i,a,u){return Ig(),Jg(u),o.flags|=256,Xi(s,o,i,a),o.child}var _s,Ss,Es,ws,xs={dehydrated:null,treeContext:null,retryLane:0};function nj(s){return{baseLanes:s,cachePool:null,transitions:null}}function oj(s,o,i){var a,u=o.pendingProps,_=Zn.current,w=!1,x=!!(128&o.flags);if((a=x)||(a=(null===s||null!==s.memoizedState)&&!!(2&_)),a?(w=!0,o.flags&=-129):null!==s&&null===s.memoizedState||(_|=1),G(Zn,1&_),null===s)return Eg(o),null!==(s=o.memoizedState)&&null!==(s=s.dehydrated)?(1&o.mode?\"$!\"===s.data?o.lanes=8:o.lanes=1073741824:o.lanes=1,null):(x=u.children,s=u.fallback,w?(u=o.mode,w=o.child,x={mode:\"hidden\",children:x},1&u||null===w?w=pj(x,u,0,null):(w.childLanes=0,w.pendingProps=x),s=Tg(s,u,i,null),w.return=o,s.return=o,w.sibling=s,o.child=w,o.child.memoizedState=nj(i),o.memoizedState=xs,s):qj(o,x));if(null!==(_=s.memoizedState)&&null!==(a=_.dehydrated))return function rj(s,o,i,a,u,_,w){if(i)return 256&o.flags?(o.flags&=-257,sj(s,o,w,a=Ki(Error(p(422))))):null!==o.memoizedState?(o.child=s.child,o.flags|=128,null):(_=a.fallback,u=o.mode,a=pj({mode:\"visible\",children:a.children},u,0,null),(_=Tg(_,u,w,null)).flags|=2,a.return=o,_.return=o,a.sibling=_,o.child=a,1&o.mode&&qn(o,s.child,null,w),o.child.memoizedState=nj(w),o.memoizedState=xs,_);if(!(1&o.mode))return sj(s,o,w,null);if(\"$!\"===u.data){if(a=u.nextSibling&&u.nextSibling.dataset)var x=a.dgst;return a=x,sj(s,o,w,a=Ki(_=Error(p(419)),a,void 0))}if(x=!!(w&s.childLanes),bs||x){if(null!==(a=Fs)){switch(w&-w){case 4:u=2;break;case 16:u=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:u=32;break;case 536870912:u=268435456;break;default:u=0}0!==(u=u&(a.suspendedLanes|w)?0:u)&&u!==_.retryLane&&(_.retryLane=u,ih(s,u),gi(a,s,u,-1))}return tj(),sj(s,o,w,a=Ki(Error(p(421))))}return\"$?\"===u.data?(o.flags|=128,o.child=s.child,o=uj.bind(null,s),u._reactRetry=o,null):(s=_.treeContext,Ln=Lf(u.nextSibling),Dn=o,Fn=!0,Bn=null,null!==s&&(In[Tn++]=Mn,In[Tn++]=Rn,In[Tn++]=Nn,Mn=s.id,Rn=s.overflow,Nn=o),o=qj(o,a.children),o.flags|=4096,o)}(s,o,x,u,a,_,i);if(w){w=u.fallback,x=o.mode,a=(_=s.child).sibling;var C={mode:\"hidden\",children:u.children};return 1&x||o.child===_?(u=Pg(_,C)).subtreeFlags=14680064&_.subtreeFlags:((u=o.child).childLanes=0,u.pendingProps=C,o.deletions=null),null!==a?w=Pg(a,w):(w=Tg(w,x,i,null)).flags|=2,w.return=o,u.return=o,u.sibling=w,o.child=u,u=w,w=o.child,x=null===(x=s.child.memoizedState)?nj(i):{baseLanes:x.baseLanes|i,cachePool:null,transitions:x.transitions},w.memoizedState=x,w.childLanes=s.childLanes&~i,o.memoizedState=xs,u}return s=(w=s.child).sibling,u=Pg(w,{mode:\"visible\",children:u.children}),!(1&o.mode)&&(u.lanes=i),u.return=o,u.sibling=null,null!==s&&(null===(i=o.deletions)?(o.deletions=[s],o.flags|=16):i.push(s)),o.child=u,o.memoizedState=null,u}function qj(s,o){return(o=pj({mode:\"visible\",children:o},s.mode,0,null)).return=s,s.child=o}function sj(s,o,i,a){return null!==a&&Jg(a),qn(o,s.child,null,i),(s=qj(o,o.pendingProps.children)).flags|=2,o.memoizedState=null,s}function vj(s,o,i){s.lanes|=o;var a=s.alternate;null!==a&&(a.lanes|=o),bh(s.return,o,i)}function wj(s,o,i,a,u){var _=s.memoizedState;null===_?s.memoizedState={isBackwards:o,rendering:null,renderingStartTime:0,last:a,tail:i,tailMode:u}:(_.isBackwards=o,_.rendering=null,_.renderingStartTime=0,_.last=a,_.tail=i,_.tailMode=u)}function xj(s,o,i){var a=o.pendingProps,u=a.revealOrder,_=a.tail;if(Xi(s,o,a.children,i),2&(a=Zn.current))a=1&a|2,o.flags|=128;else{if(null!==s&&128&s.flags)e:for(s=o.child;null!==s;){if(13===s.tag)null!==s.memoizedState&&vj(s,i,o);else if(19===s.tag)vj(s,i,o);else if(null!==s.child){s.child.return=s,s=s.child;continue}if(s===o)break e;for(;null===s.sibling;){if(null===s.return||s.return===o)break e;s=s.return}s.sibling.return=s.return,s=s.sibling}a&=1}if(G(Zn,a),1&o.mode)switch(u){case\"forwards\":for(i=o.child,u=null;null!==i;)null!==(s=i.alternate)&&null===Ch(s)&&(u=i),i=i.sibling;null===(i=u)?(u=o.child,o.child=null):(u=i.sibling,i.sibling=null),wj(o,!1,u,i,_);break;case\"backwards\":for(i=null,u=o.child,o.child=null;null!==u;){if(null!==(s=u.alternate)&&null===Ch(s)){o.child=u;break}s=u.sibling,u.sibling=i,i=u,u=s}wj(o,!0,i,null,_);break;case\"together\":wj(o,!1,null,null,void 0);break;default:o.memoizedState=null}else o.memoizedState=null;return o.child}function ij(s,o){!(1&o.mode)&&null!==s&&(s.alternate=null,o.alternate=null,o.flags|=2)}function Zi(s,o,i){if(null!==s&&(o.dependencies=s.dependencies),Ws|=o.lanes,!(i&o.childLanes))return null;if(null!==s&&o.child!==s.child)throw Error(p(153));if(null!==o.child){for(i=Pg(s=o.child,s.pendingProps),o.child=i,i.return=o;null!==s.sibling;)s=s.sibling,(i=i.sibling=Pg(s,s.pendingProps)).return=o;i.sibling=null}return o.child}function Dj(s,o){if(!Fn)switch(s.tailMode){case\"hidden\":o=s.tail;for(var i=null;null!==o;)null!==o.alternate&&(i=o),o=o.sibling;null===i?s.tail=null:i.sibling=null;break;case\"collapsed\":i=s.tail;for(var a=null;null!==i;)null!==i.alternate&&(a=i),i=i.sibling;null===a?o||null===s.tail?s.tail=null:s.tail.sibling=null:a.sibling=null}}function S(s){var o=null!==s.alternate&&s.alternate.child===s.child,i=0,a=0;if(o)for(var u=s.child;null!==u;)i|=u.lanes|u.childLanes,a|=14680064&u.subtreeFlags,a|=14680064&u.flags,u.return=s,u=u.sibling;else for(u=s.child;null!==u;)i|=u.lanes|u.childLanes,a|=u.subtreeFlags,a|=u.flags,u.return=s,u=u.sibling;return s.subtreeFlags|=a,s.childLanes=i,o}function Ej(s,o,i){var a=o.pendingProps;switch(wg(o),o.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return S(o),null;case 1:case 17:return Zf(o.type)&&$f(),S(o),null;case 3:return a=o.stateNode,zh(),E(En),E(Sn),Eh(),a.pendingContext&&(a.context=a.pendingContext,a.pendingContext=null),null!==s&&null!==s.child||(Gg(o)?o.flags|=4:null===s||s.memoizedState.isDehydrated&&!(256&o.flags)||(o.flags|=1024,null!==Bn&&(Fj(Bn),Bn=null))),Ss(s,o),S(o),null;case 5:Bh(o);var u=xh(Qn.current);if(i=o.type,null!==s&&null!=o.stateNode)Es(s,o,i,a,u),s.ref!==o.ref&&(o.flags|=512,o.flags|=2097152);else{if(!a){if(null===o.stateNode)throw Error(p(166));return S(o),null}if(s=xh(Yn.current),Gg(o)){a=o.stateNode,i=o.type;var _=o.memoizedProps;switch(a[hn]=o,a[dn]=_,s=!!(1&o.mode),i){case\"dialog\":D(\"cancel\",a),D(\"close\",a);break;case\"iframe\":case\"object\":case\"embed\":D(\"load\",a);break;case\"video\":case\"audio\":for(u=0;u<Zr.length;u++)D(Zr[u],a);break;case\"source\":D(\"error\",a);break;case\"img\":case\"image\":case\"link\":D(\"error\",a),D(\"load\",a);break;case\"details\":D(\"toggle\",a);break;case\"input\":Za(a,_),D(\"invalid\",a);break;case\"select\":a._wrapperState={wasMultiple:!!_.multiple},D(\"invalid\",a);break;case\"textarea\":hb(a,_),D(\"invalid\",a)}for(var x in ub(i,_),u=null,_)if(_.hasOwnProperty(x)){var C=_[x];\"children\"===x?\"string\"==typeof C?a.textContent!==C&&(!0!==_.suppressHydrationWarning&&Af(a.textContent,C,s),u=[\"children\",C]):\"number\"==typeof C&&a.textContent!==\"\"+C&&(!0!==_.suppressHydrationWarning&&Af(a.textContent,C,s),u=[\"children\",\"\"+C]):w.hasOwnProperty(x)&&null!=C&&\"onScroll\"===x&&D(\"scroll\",a)}switch(i){case\"input\":Va(a),db(a,_,!0);break;case\"textarea\":Va(a),jb(a);break;case\"select\":case\"option\":break;default:\"function\"==typeof _.onClick&&(a.onclick=Bf)}a=u,o.updateQueue=a,null!==a&&(o.flags|=4)}else{x=9===u.nodeType?u:u.ownerDocument,\"http://www.w3.org/1999/xhtml\"===s&&(s=kb(i)),\"http://www.w3.org/1999/xhtml\"===s?\"script\"===i?((s=x.createElement(\"div\")).innerHTML=\"<script><\\/script>\",s=s.removeChild(s.firstChild)):\"string\"==typeof a.is?s=x.createElement(i,{is:a.is}):(s=x.createElement(i),\"select\"===i&&(x=s,a.multiple?x.multiple=!0:a.size&&(x.size=a.size))):s=x.createElementNS(s,i),s[hn]=o,s[dn]=a,_s(s,o,!1,!1),o.stateNode=s;e:{switch(x=vb(i,a),i){case\"dialog\":D(\"cancel\",s),D(\"close\",s),u=a;break;case\"iframe\":case\"object\":case\"embed\":D(\"load\",s),u=a;break;case\"video\":case\"audio\":for(u=0;u<Zr.length;u++)D(Zr[u],s);u=a;break;case\"source\":D(\"error\",s),u=a;break;case\"img\":case\"image\":case\"link\":D(\"error\",s),D(\"load\",s),u=a;break;case\"details\":D(\"toggle\",s),u=a;break;case\"input\":Za(s,a),u=Ya(s,a),D(\"invalid\",s);break;case\"option\":default:u=a;break;case\"select\":s._wrapperState={wasMultiple:!!a.multiple},u=we({},a,{value:void 0}),D(\"invalid\",s);break;case\"textarea\":hb(s,a),u=gb(s,a),D(\"invalid\",s)}for(_ in ub(i,u),C=u)if(C.hasOwnProperty(_)){var j=C[_];\"style\"===_?sb(s,j):\"dangerouslySetInnerHTML\"===_?null!=(j=j?j.__html:void 0)&&$e(s,j):\"children\"===_?\"string\"==typeof j?(\"textarea\"!==i||\"\"!==j)&&ob(s,j):\"number\"==typeof j&&ob(s,\"\"+j):\"suppressContentEditableWarning\"!==_&&\"suppressHydrationWarning\"!==_&&\"autoFocus\"!==_&&(w.hasOwnProperty(_)?null!=j&&\"onScroll\"===_&&D(\"scroll\",s):null!=j&&ta(s,_,j,x))}switch(i){case\"input\":Va(s),db(s,a,!1);break;case\"textarea\":Va(s),jb(s);break;case\"option\":null!=a.value&&s.setAttribute(\"value\",\"\"+Sa(a.value));break;case\"select\":s.multiple=!!a.multiple,null!=(_=a.value)?fb(s,!!a.multiple,_,!1):null!=a.defaultValue&&fb(s,!!a.multiple,a.defaultValue,!0);break;default:\"function\"==typeof u.onClick&&(s.onclick=Bf)}switch(i){case\"button\":case\"input\":case\"select\":case\"textarea\":a=!!a.autoFocus;break e;case\"img\":a=!0;break e;default:a=!1}}a&&(o.flags|=4)}null!==o.ref&&(o.flags|=512,o.flags|=2097152)}return S(o),null;case 6:if(s&&null!=o.stateNode)ws(s,o,s.memoizedProps,a);else{if(\"string\"!=typeof a&&null===o.stateNode)throw Error(p(166));if(i=xh(Qn.current),xh(Yn.current),Gg(o)){if(a=o.stateNode,i=o.memoizedProps,a[hn]=o,(_=a.nodeValue!==i)&&null!==(s=Dn))switch(s.tag){case 3:Af(a.nodeValue,i,!!(1&s.mode));break;case 5:!0!==s.memoizedProps.suppressHydrationWarning&&Af(a.nodeValue,i,!!(1&s.mode))}_&&(o.flags|=4)}else(a=(9===i.nodeType?i:i.ownerDocument).createTextNode(a))[hn]=o,o.stateNode=a}return S(o),null;case 13:if(E(Zn),a=o.memoizedState,null===s||null!==s.memoizedState&&null!==s.memoizedState.dehydrated){if(Fn&&null!==Ln&&1&o.mode&&!(128&o.flags))Hg(),Ig(),o.flags|=98560,_=!1;else if(_=Gg(o),null!==a&&null!==a.dehydrated){if(null===s){if(!_)throw Error(p(318));if(!(_=null!==(_=o.memoizedState)?_.dehydrated:null))throw Error(p(317));_[hn]=o}else Ig(),!(128&o.flags)&&(o.memoizedState=null),o.flags|=4;S(o),_=!1}else null!==Bn&&(Fj(Bn),Bn=null),_=!0;if(!_)return 65536&o.flags?o:null}return 128&o.flags?(o.lanes=i,o):((a=null!==a)!==(null!==s&&null!==s.memoizedState)&&a&&(o.child.flags|=8192,1&o.mode&&(null===s||1&Zn.current?0===Vs&&(Vs=3):tj())),null!==o.updateQueue&&(o.flags|=4),S(o),null);case 4:return zh(),Ss(s,o),null===s&&sf(o.stateNode.containerInfo),S(o),null;case 10:return ah(o.type._context),S(o),null;case 19:if(E(Zn),null===(_=o.memoizedState))return S(o),null;if(a=!!(128&o.flags),null===(x=_.rendering))if(a)Dj(_,!1);else{if(0!==Vs||null!==s&&128&s.flags)for(s=o.child;null!==s;){if(null!==(x=Ch(s))){for(o.flags|=128,Dj(_,!1),null!==(a=x.updateQueue)&&(o.updateQueue=a,o.flags|=4),o.subtreeFlags=0,a=i,i=o.child;null!==i;)s=a,(_=i).flags&=14680066,null===(x=_.alternate)?(_.childLanes=0,_.lanes=s,_.child=null,_.subtreeFlags=0,_.memoizedProps=null,_.memoizedState=null,_.updateQueue=null,_.dependencies=null,_.stateNode=null):(_.childLanes=x.childLanes,_.lanes=x.lanes,_.child=x.child,_.subtreeFlags=0,_.deletions=null,_.memoizedProps=x.memoizedProps,_.memoizedState=x.memoizedState,_.updateQueue=x.updateQueue,_.type=x.type,s=x.dependencies,_.dependencies=null===s?null:{lanes:s.lanes,firstContext:s.firstContext}),i=i.sibling;return G(Zn,1&Zn.current|2),o.child}s=s.sibling}null!==_.tail&&ht()>Xs&&(o.flags|=128,a=!0,Dj(_,!1),o.lanes=4194304)}else{if(!a)if(null!==(s=Ch(x))){if(o.flags|=128,a=!0,null!==(i=s.updateQueue)&&(o.updateQueue=i,o.flags|=4),Dj(_,!0),null===_.tail&&\"hidden\"===_.tailMode&&!x.alternate&&!Fn)return S(o),null}else 2*ht()-_.renderingStartTime>Xs&&1073741824!==i&&(o.flags|=128,a=!0,Dj(_,!1),o.lanes=4194304);_.isBackwards?(x.sibling=o.child,o.child=x):(null!==(i=_.last)?i.sibling=x:o.child=x,_.last=x)}return null!==_.tail?(o=_.tail,_.rendering=o,_.tail=o.sibling,_.renderingStartTime=ht(),o.sibling=null,i=Zn.current,G(Zn,a?1&i|2:1&i),o):(S(o),null);case 22:case 23:return Hj(),a=null!==o.memoizedState,null!==s&&null!==s.memoizedState!==a&&(o.flags|=8192),a&&1&o.mode?!!(1073741824&qs)&&(S(o),6&o.subtreeFlags&&(o.flags|=8192)):S(o),null;case 24:case 25:return null}throw Error(p(156,o.tag))}function Ij(s,o){switch(wg(o),o.tag){case 1:return Zf(o.type)&&$f(),65536&(s=o.flags)?(o.flags=-65537&s|128,o):null;case 3:return zh(),E(En),E(Sn),Eh(),65536&(s=o.flags)&&!(128&s)?(o.flags=-65537&s|128,o):null;case 5:return Bh(o),null;case 13:if(E(Zn),null!==(s=o.memoizedState)&&null!==s.dehydrated){if(null===o.alternate)throw Error(p(340));Ig()}return 65536&(s=o.flags)?(o.flags=-65537&s|128,o):null;case 19:return E(Zn),null;case 4:return zh(),null;case 10:return ah(o.type._context),null;case 22:case 23:return Hj(),null;default:return null}}_s=function(s,o){for(var i=o.child;null!==i;){if(5===i.tag||6===i.tag)s.appendChild(i.stateNode);else if(4!==i.tag&&null!==i.child){i.child.return=i,i=i.child;continue}if(i===o)break;for(;null===i.sibling;){if(null===i.return||i.return===o)return;i=i.return}i.sibling.return=i.return,i=i.sibling}},Ss=function(){},Es=function(s,o,i,a){var u=s.memoizedProps;if(u!==a){s=o.stateNode,xh(Yn.current);var _,x=null;switch(i){case\"input\":u=Ya(s,u),a=Ya(s,a),x=[];break;case\"select\":u=we({},u,{value:void 0}),a=we({},a,{value:void 0}),x=[];break;case\"textarea\":u=gb(s,u),a=gb(s,a),x=[];break;default:\"function\"!=typeof u.onClick&&\"function\"==typeof a.onClick&&(s.onclick=Bf)}for(L in ub(i,a),i=null,u)if(!a.hasOwnProperty(L)&&u.hasOwnProperty(L)&&null!=u[L])if(\"style\"===L){var C=u[L];for(_ in C)C.hasOwnProperty(_)&&(i||(i={}),i[_]=\"\")}else\"dangerouslySetInnerHTML\"!==L&&\"children\"!==L&&\"suppressContentEditableWarning\"!==L&&\"suppressHydrationWarning\"!==L&&\"autoFocus\"!==L&&(w.hasOwnProperty(L)?x||(x=[]):(x=x||[]).push(L,null));for(L in a){var j=a[L];if(C=null!=u?u[L]:void 0,a.hasOwnProperty(L)&&j!==C&&(null!=j||null!=C))if(\"style\"===L)if(C){for(_ in C)!C.hasOwnProperty(_)||j&&j.hasOwnProperty(_)||(i||(i={}),i[_]=\"\");for(_ in j)j.hasOwnProperty(_)&&C[_]!==j[_]&&(i||(i={}),i[_]=j[_])}else i||(x||(x=[]),x.push(L,i)),i=j;else\"dangerouslySetInnerHTML\"===L?(j=j?j.__html:void 0,C=C?C.__html:void 0,null!=j&&C!==j&&(x=x||[]).push(L,j)):\"children\"===L?\"string\"!=typeof j&&\"number\"!=typeof j||(x=x||[]).push(L,\"\"+j):\"suppressContentEditableWarning\"!==L&&\"suppressHydrationWarning\"!==L&&(w.hasOwnProperty(L)?(null!=j&&\"onScroll\"===L&&D(\"scroll\",s),x||C===j||(x=[])):(x=x||[]).push(L,j))}i&&(x=x||[]).push(\"style\",i);var L=x;(o.updateQueue=L)&&(o.flags|=4)}},ws=function(s,o,i,a){i!==a&&(o.flags|=4)};var ks=!1,Os=!1,As=\"function\"==typeof WeakSet?WeakSet:Set,Cs=null;function Lj(s,o){var i=s.ref;if(null!==i)if(\"function\"==typeof i)try{i(null)}catch(i){W(s,o,i)}else i.current=null}function Mj(s,o,i){try{i()}catch(i){W(s,o,i)}}var js=!1;function Pj(s,o,i){var a=o.updateQueue;if(null!==(a=null!==a?a.lastEffect:null)){var u=a=a.next;do{if((u.tag&s)===s){var _=u.destroy;u.destroy=void 0,void 0!==_&&Mj(o,i,_)}u=u.next}while(u!==a)}}function Qj(s,o){if(null!==(o=null!==(o=o.updateQueue)?o.lastEffect:null)){var i=o=o.next;do{if((i.tag&s)===s){var a=i.create;i.destroy=a()}i=i.next}while(i!==o)}}function Rj(s){var o=s.ref;if(null!==o){var i=s.stateNode;s.tag,s=i,\"function\"==typeof o?o(s):o.current=s}}function Sj(s){var o=s.alternate;null!==o&&(s.alternate=null,Sj(o)),s.child=null,s.deletions=null,s.sibling=null,5===s.tag&&(null!==(o=s.stateNode)&&(delete o[hn],delete o[dn],delete o[mn],delete o[gn],delete o[yn])),s.stateNode=null,s.return=null,s.dependencies=null,s.memoizedProps=null,s.memoizedState=null,s.pendingProps=null,s.stateNode=null,s.updateQueue=null}function Tj(s){return 5===s.tag||3===s.tag||4===s.tag}function Uj(s){e:for(;;){for(;null===s.sibling;){if(null===s.return||Tj(s.return))return null;s=s.return}for(s.sibling.return=s.return,s=s.sibling;5!==s.tag&&6!==s.tag&&18!==s.tag;){if(2&s.flags)continue e;if(null===s.child||4===s.tag)continue e;s.child.return=s,s=s.child}if(!(2&s.flags))return s.stateNode}}function Vj(s,o,i){var a=s.tag;if(5===a||6===a)s=s.stateNode,o?8===i.nodeType?i.parentNode.insertBefore(s,o):i.insertBefore(s,o):(8===i.nodeType?(o=i.parentNode).insertBefore(s,i):(o=i).appendChild(s),null!=(i=i._reactRootContainer)||null!==o.onclick||(o.onclick=Bf));else if(4!==a&&null!==(s=s.child))for(Vj(s,o,i),s=s.sibling;null!==s;)Vj(s,o,i),s=s.sibling}function Wj(s,o,i){var a=s.tag;if(5===a||6===a)s=s.stateNode,o?i.insertBefore(s,o):i.appendChild(s);else if(4!==a&&null!==(s=s.child))for(Wj(s,o,i),s=s.sibling;null!==s;)Wj(s,o,i),s=s.sibling}var Ps=null,Is=!1;function Yj(s,o,i){for(i=i.child;null!==i;)Zj(s,o,i),i=i.sibling}function Zj(s,o,i){if(St&&\"function\"==typeof St.onCommitFiberUnmount)try{St.onCommitFiberUnmount(_t,i)}catch(s){}switch(i.tag){case 5:Os||Lj(i,o);case 6:var a=Ps,u=Is;Ps=null,Yj(s,o,i),Is=u,null!==(Ps=a)&&(Is?(s=Ps,i=i.stateNode,8===s.nodeType?s.parentNode.removeChild(i):s.removeChild(i)):Ps.removeChild(i.stateNode));break;case 18:null!==Ps&&(Is?(s=Ps,i=i.stateNode,8===s.nodeType?Kf(s.parentNode,i):1===s.nodeType&&Kf(s,i),bd(s)):Kf(Ps,i.stateNode));break;case 4:a=Ps,u=Is,Ps=i.stateNode.containerInfo,Is=!0,Yj(s,o,i),Ps=a,Is=u;break;case 0:case 11:case 14:case 15:if(!Os&&(null!==(a=i.updateQueue)&&null!==(a=a.lastEffect))){u=a=a.next;do{var _=u,w=_.destroy;_=_.tag,void 0!==w&&(2&_||4&_)&&Mj(i,o,w),u=u.next}while(u!==a)}Yj(s,o,i);break;case 1:if(!Os&&(Lj(i,o),\"function\"==typeof(a=i.stateNode).componentWillUnmount))try{a.props=i.memoizedProps,a.state=i.memoizedState,a.componentWillUnmount()}catch(s){W(i,o,s)}Yj(s,o,i);break;case 21:Yj(s,o,i);break;case 22:1&i.mode?(Os=(a=Os)||null!==i.memoizedState,Yj(s,o,i),Os=a):Yj(s,o,i);break;default:Yj(s,o,i)}}function ak(s){var o=s.updateQueue;if(null!==o){s.updateQueue=null;var i=s.stateNode;null===i&&(i=s.stateNode=new As),o.forEach((function(o){var a=bk.bind(null,s,o);i.has(o)||(i.add(o),o.then(a,a))}))}}function ck(s,o){var i=o.deletions;if(null!==i)for(var a=0;a<i.length;a++){var u=i[a];try{var _=s,w=o,x=w;e:for(;null!==x;){switch(x.tag){case 5:Ps=x.stateNode,Is=!1;break e;case 3:case 4:Ps=x.stateNode.containerInfo,Is=!0;break e}x=x.return}if(null===Ps)throw Error(p(160));Zj(_,w,u),Ps=null,Is=!1;var C=u.alternate;null!==C&&(C.return=null),u.return=null}catch(s){W(u,o,s)}}if(12854&o.subtreeFlags)for(o=o.child;null!==o;)dk(o,s),o=o.sibling}function dk(s,o){var i=s.alternate,a=s.flags;switch(s.tag){case 0:case 11:case 14:case 15:if(ck(o,s),ek(s),4&a){try{Pj(3,s,s.return),Qj(3,s)}catch(o){W(s,s.return,o)}try{Pj(5,s,s.return)}catch(o){W(s,s.return,o)}}break;case 1:ck(o,s),ek(s),512&a&&null!==i&&Lj(i,i.return);break;case 5:if(ck(o,s),ek(s),512&a&&null!==i&&Lj(i,i.return),32&s.flags){var u=s.stateNode;try{ob(u,\"\")}catch(o){W(s,s.return,o)}}if(4&a&&null!=(u=s.stateNode)){var _=s.memoizedProps,w=null!==i?i.memoizedProps:_,x=s.type,C=s.updateQueue;if(s.updateQueue=null,null!==C)try{\"input\"===x&&\"radio\"===_.type&&null!=_.name&&ab(u,_),vb(x,w);var j=vb(x,_);for(w=0;w<C.length;w+=2){var L=C[w],B=C[w+1];\"style\"===L?sb(u,B):\"dangerouslySetInnerHTML\"===L?$e(u,B):\"children\"===L?ob(u,B):ta(u,L,B,j)}switch(x){case\"input\":bb(u,_);break;case\"textarea\":ib(u,_);break;case\"select\":var $=u._wrapperState.wasMultiple;u._wrapperState.wasMultiple=!!_.multiple;var U=_.value;null!=U?fb(u,!!_.multiple,U,!1):$!==!!_.multiple&&(null!=_.defaultValue?fb(u,!!_.multiple,_.defaultValue,!0):fb(u,!!_.multiple,_.multiple?[]:\"\",!1))}u[dn]=_}catch(o){W(s,s.return,o)}}break;case 6:if(ck(o,s),ek(s),4&a){if(null===s.stateNode)throw Error(p(162));u=s.stateNode,_=s.memoizedProps;try{u.nodeValue=_}catch(o){W(s,s.return,o)}}break;case 3:if(ck(o,s),ek(s),4&a&&null!==i&&i.memoizedState.isDehydrated)try{bd(o.containerInfo)}catch(o){W(s,s.return,o)}break;case 4:default:ck(o,s),ek(s);break;case 13:ck(o,s),ek(s),8192&(u=s.child).flags&&(_=null!==u.memoizedState,u.stateNode.isHidden=_,!_||null!==u.alternate&&null!==u.alternate.memoizedState||(Ys=ht())),4&a&&ak(s);break;case 22:if(L=null!==i&&null!==i.memoizedState,1&s.mode?(Os=(j=Os)||L,ck(o,s),Os=j):ck(o,s),ek(s),8192&a){if(j=null!==s.memoizedState,(s.stateNode.isHidden=j)&&!L&&1&s.mode)for(Cs=s,L=s.child;null!==L;){for(B=Cs=L;null!==Cs;){switch(U=($=Cs).child,$.tag){case 0:case 11:case 14:case 15:Pj(4,$,$.return);break;case 1:Lj($,$.return);var V=$.stateNode;if(\"function\"==typeof V.componentWillUnmount){a=$,i=$.return;try{o=a,V.props=o.memoizedProps,V.state=o.memoizedState,V.componentWillUnmount()}catch(s){W(a,i,s)}}break;case 5:Lj($,$.return);break;case 22:if(null!==$.memoizedState){gk(B);continue}}null!==U?(U.return=$,Cs=U):gk(B)}L=L.sibling}e:for(L=null,B=s;;){if(5===B.tag){if(null===L){L=B;try{u=B.stateNode,j?\"function\"==typeof(_=u.style).setProperty?_.setProperty(\"display\",\"none\",\"important\"):_.display=\"none\":(x=B.stateNode,w=null!=(C=B.memoizedProps.style)&&C.hasOwnProperty(\"display\")?C.display:null,x.style.display=rb(\"display\",w))}catch(o){W(s,s.return,o)}}}else if(6===B.tag){if(null===L)try{B.stateNode.nodeValue=j?\"\":B.memoizedProps}catch(o){W(s,s.return,o)}}else if((22!==B.tag&&23!==B.tag||null===B.memoizedState||B===s)&&null!==B.child){B.child.return=B,B=B.child;continue}if(B===s)break e;for(;null===B.sibling;){if(null===B.return||B.return===s)break e;L===B&&(L=null),B=B.return}L===B&&(L=null),B.sibling.return=B.return,B=B.sibling}}break;case 19:ck(o,s),ek(s),4&a&&ak(s);case 21:}}function ek(s){var o=s.flags;if(2&o){try{e:{for(var i=s.return;null!==i;){if(Tj(i)){var a=i;break e}i=i.return}throw Error(p(160))}switch(a.tag){case 5:var u=a.stateNode;32&a.flags&&(ob(u,\"\"),a.flags&=-33),Wj(s,Uj(s),u);break;case 3:case 4:var _=a.stateNode.containerInfo;Vj(s,Uj(s),_);break;default:throw Error(p(161))}}catch(o){W(s,s.return,o)}s.flags&=-3}4096&o&&(s.flags&=-4097)}function hk(s,o,i){Cs=s,ik(s,o,i)}function ik(s,o,i){for(var a=!!(1&s.mode);null!==Cs;){var u=Cs,_=u.child;if(22===u.tag&&a){var w=null!==u.memoizedState||ks;if(!w){var x=u.alternate,C=null!==x&&null!==x.memoizedState||Os;x=ks;var j=Os;if(ks=w,(Os=C)&&!j)for(Cs=u;null!==Cs;)C=(w=Cs).child,22===w.tag&&null!==w.memoizedState?jk(u):null!==C?(C.return=w,Cs=C):jk(u);for(;null!==_;)Cs=_,ik(_,o,i),_=_.sibling;Cs=u,ks=x,Os=j}kk(s)}else 8772&u.subtreeFlags&&null!==_?(_.return=u,Cs=_):kk(s)}}function kk(s){for(;null!==Cs;){var o=Cs;if(8772&o.flags){var i=o.alternate;try{if(8772&o.flags)switch(o.tag){case 0:case 11:case 15:Os||Qj(5,o);break;case 1:var a=o.stateNode;if(4&o.flags&&!Os)if(null===i)a.componentDidMount();else{var u=o.elementType===o.type?i.memoizedProps:Ci(o.type,i.memoizedProps);a.componentDidUpdate(u,i.memoizedState,a.__reactInternalSnapshotBeforeUpdate)}var _=o.updateQueue;null!==_&&sh(o,_,a);break;case 3:var w=o.updateQueue;if(null!==w){if(i=null,null!==o.child)switch(o.child.tag){case 5:case 1:i=o.child.stateNode}sh(o,w,i)}break;case 5:var x=o.stateNode;if(null===i&&4&o.flags){i=x;var C=o.memoizedProps;switch(o.type){case\"button\":case\"input\":case\"select\":case\"textarea\":C.autoFocus&&i.focus();break;case\"img\":C.src&&(i.src=C.src)}}break;case 6:case 4:case 12:case 19:case 17:case 21:case 22:case 23:case 25:break;case 13:if(null===o.memoizedState){var j=o.alternate;if(null!==j){var L=j.memoizedState;if(null!==L){var B=L.dehydrated;null!==B&&bd(B)}}}break;default:throw Error(p(163))}Os||512&o.flags&&Rj(o)}catch(s){W(o,o.return,s)}}if(o===s){Cs=null;break}if(null!==(i=o.sibling)){i.return=o.return,Cs=i;break}Cs=o.return}}function gk(s){for(;null!==Cs;){var o=Cs;if(o===s){Cs=null;break}var i=o.sibling;if(null!==i){i.return=o.return,Cs=i;break}Cs=o.return}}function jk(s){for(;null!==Cs;){var o=Cs;try{switch(o.tag){case 0:case 11:case 15:var i=o.return;try{Qj(4,o)}catch(s){W(o,i,s)}break;case 1:var a=o.stateNode;if(\"function\"==typeof a.componentDidMount){var u=o.return;try{a.componentDidMount()}catch(s){W(o,u,s)}}var _=o.return;try{Rj(o)}catch(s){W(o,_,s)}break;case 5:var w=o.return;try{Rj(o)}catch(s){W(o,w,s)}}}catch(s){W(o,o.return,s)}if(o===s){Cs=null;break}var x=o.sibling;if(null!==x){x.return=o.return,Cs=x;break}Cs=o.return}}var Ts,Ns=Math.ceil,Ms=V.ReactCurrentDispatcher,Rs=V.ReactCurrentOwner,Ds=V.ReactCurrentBatchConfig,Ls=0,Fs=null,Bs=null,$s=0,qs=0,Us=Uf(0),Vs=0,zs=null,Ws=0,Js=0,Hs=0,Ks=null,Gs=null,Ys=0,Xs=1/0,Qs=null,Zs=!1,eo=null,to=null,ro=!1,no=null,so=0,oo=0,io=null,ao=-1,co=0;function R(){return 6&Ls?ht():-1!==ao?ao:ao=ht()}function yi(s){return 1&s.mode?2&Ls&&0!==$s?$s&-$s:null!==$n.transition?(0===co&&(co=yc()),co):0!==(s=At)?s:s=void 0===(s=window.event)?16:jd(s.type):1}function gi(s,o,i,a){if(50<oo)throw oo=0,io=null,Error(p(185));Ac(s,i,a),2&Ls&&s===Fs||(s===Fs&&(!(2&Ls)&&(Js|=i),4===Vs&&Ck(s,$s)),Dk(s,a),1===i&&0===Ls&&!(1&o.mode)&&(Xs=ht()+500,kn&&jg()))}function Dk(s,o){var i=s.callbackNode;!function wc(s,o){for(var i=s.suspendedLanes,a=s.pingedLanes,u=s.expirationTimes,_=s.pendingLanes;0<_;){var w=31-Et(_),x=1<<w,C=u[w];-1===C?x&i&&!(x&a)||(u[w]=vc(x,o)):C<=o&&(s.expiredLanes|=x),_&=~x}}(s,o);var a=uc(s,s===Fs?$s:0);if(0===a)null!==i&&lt(i),s.callbackNode=null,s.callbackPriority=0;else if(o=a&-a,s.callbackPriority!==o){if(null!=i&&lt(i),1===o)0===s.tag?function ig(s){kn=!0,hg(s)}(Ek.bind(null,s)):hg(Ek.bind(null,s)),un((function(){!(6&Ls)&&jg()})),i=null;else{switch(Dc(a)){case 1:i=mt;break;case 4:i=gt;break;case 16:default:i=yt;break;case 536870912:i=bt}i=Fk(i,Gk.bind(null,s))}s.callbackPriority=o,s.callbackNode=i}}function Gk(s,o){if(ao=-1,co=0,6&Ls)throw Error(p(327));var i=s.callbackNode;if(Hk()&&s.callbackNode!==i)return null;var a=uc(s,s===Fs?$s:0);if(0===a)return null;if(30&a||a&s.expiredLanes||o)o=Ik(s,a);else{o=a;var u=Ls;Ls|=2;var _=Jk();for(Fs===s&&$s===o||(Qs=null,Xs=ht()+500,Kk(s,o));;)try{Lk();break}catch(o){Mk(s,o)}$g(),Ms.current=_,Ls=u,null!==Bs?o=0:(Fs=null,$s=0,o=Vs)}if(0!==o){if(2===o&&(0!==(u=xc(s))&&(a=u,o=Nk(s,u))),1===o)throw i=zs,Kk(s,0),Ck(s,a),Dk(s,ht()),i;if(6===o)Ck(s,a);else{if(u=s.current.alternate,!(30&a||function Ok(s){for(var o=s;;){if(16384&o.flags){var i=o.updateQueue;if(null!==i&&null!==(i=i.stores))for(var a=0;a<i.length;a++){var u=i[a],_=u.getSnapshot;u=u.value;try{if(!Dr(_(),u))return!1}catch(s){return!1}}}if(i=o.child,16384&o.subtreeFlags&&null!==i)i.return=o,o=i;else{if(o===s)break;for(;null===o.sibling;){if(null===o.return||o.return===s)return!0;o=o.return}o.sibling.return=o.return,o=o.sibling}}return!0}(u)||(o=Ik(s,a),2===o&&(_=xc(s),0!==_&&(a=_,o=Nk(s,_))),1!==o)))throw i=zs,Kk(s,0),Ck(s,a),Dk(s,ht()),i;switch(s.finishedWork=u,s.finishedLanes=a,o){case 0:case 1:throw Error(p(345));case 2:case 5:Pk(s,Gs,Qs);break;case 3:if(Ck(s,a),(130023424&a)===a&&10<(o=Ys+500-ht())){if(0!==uc(s,0))break;if(((u=s.suspendedLanes)&a)!==a){R(),s.pingedLanes|=s.suspendedLanes&u;break}s.timeoutHandle=an(Pk.bind(null,s,Gs,Qs),o);break}Pk(s,Gs,Qs);break;case 4:if(Ck(s,a),(4194240&a)===a)break;for(o=s.eventTimes,u=-1;0<a;){var w=31-Et(a);_=1<<w,(w=o[w])>u&&(u=w),a&=~_}if(a=u,10<(a=(120>(a=ht()-a)?120:480>a?480:1080>a?1080:1920>a?1920:3e3>a?3e3:4320>a?4320:1960*Ns(a/1960))-a)){s.timeoutHandle=an(Pk.bind(null,s,Gs,Qs),a);break}Pk(s,Gs,Qs);break;default:throw Error(p(329))}}}return Dk(s,ht()),s.callbackNode===i?Gk.bind(null,s):null}function Nk(s,o){var i=Ks;return s.current.memoizedState.isDehydrated&&(Kk(s,o).flags|=256),2!==(s=Ik(s,o))&&(o=Gs,Gs=i,null!==o&&Fj(o)),s}function Fj(s){null===Gs?Gs=s:Gs.push.apply(Gs,s)}function Ck(s,o){for(o&=~Hs,o&=~Js,s.suspendedLanes|=o,s.pingedLanes&=~o,s=s.expirationTimes;0<o;){var i=31-Et(o),a=1<<i;s[i]=-1,o&=~a}}function Ek(s){if(6&Ls)throw Error(p(327));Hk();var o=uc(s,0);if(!(1&o))return Dk(s,ht()),null;var i=Ik(s,o);if(0!==s.tag&&2===i){var a=xc(s);0!==a&&(o=a,i=Nk(s,a))}if(1===i)throw i=zs,Kk(s,0),Ck(s,o),Dk(s,ht()),i;if(6===i)throw Error(p(345));return s.finishedWork=s.current.alternate,s.finishedLanes=o,Pk(s,Gs,Qs),Dk(s,ht()),null}function Qk(s,o){var i=Ls;Ls|=1;try{return s(o)}finally{0===(Ls=i)&&(Xs=ht()+500,kn&&jg())}}function Rk(s){null!==no&&0===no.tag&&!(6&Ls)&&Hk();var o=Ls;Ls|=1;var i=Ds.transition,a=At;try{if(Ds.transition=null,At=1,s)return s()}finally{At=a,Ds.transition=i,!(6&(Ls=o))&&jg()}}function Hj(){qs=Us.current,E(Us)}function Kk(s,o){s.finishedWork=null,s.finishedLanes=0;var i=s.timeoutHandle;if(-1!==i&&(s.timeoutHandle=-1,cn(i)),null!==Bs)for(i=Bs.return;null!==i;){var a=i;switch(wg(a),a.tag){case 1:null!=(a=a.type.childContextTypes)&&$f();break;case 3:zh(),E(En),E(Sn),Eh();break;case 5:Bh(a);break;case 4:zh();break;case 13:case 19:E(Zn);break;case 10:ah(a.type._context);break;case 22:case 23:Hj()}i=i.return}if(Fs=s,Bs=s=Pg(s.current,null),$s=qs=o,Vs=0,zs=null,Hs=Js=Ws=0,Gs=Ks=null,null!==Hn){for(o=0;o<Hn.length;o++)if(null!==(a=(i=Hn[o]).interleaved)){i.interleaved=null;var u=a.next,_=i.pending;if(null!==_){var w=_.next;_.next=u,a.next=w}i.pending=a}Hn=null}return s}function Mk(s,o){for(;;){var i=Bs;try{if($g(),ts.current=hs,cs){for(var a=ss.memoizedState;null!==a;){var u=a.queue;null!==u&&(u.pending=null),a=a.next}cs=!1}if(ns=0,as=os=ss=null,ls=!1,us=0,Rs.current=null,null===i||null===i.return){Vs=1,zs=o,Bs=null;break}e:{var _=s,w=i.return,x=i,C=o;if(o=$s,x.flags|=32768,null!==C&&\"object\"==typeof C&&\"function\"==typeof C.then){var j=C,L=x,B=L.tag;if(!(1&L.mode||0!==B&&11!==B&&15!==B)){var $=L.alternate;$?(L.updateQueue=$.updateQueue,L.memoizedState=$.memoizedState,L.lanes=$.lanes):(L.updateQueue=null,L.memoizedState=null)}var U=Ui(w);if(null!==U){U.flags&=-257,Vi(U,w,x,0,o),1&U.mode&&Si(_,j,o),C=j;var V=(o=U).updateQueue;if(null===V){var z=new Set;z.add(C),o.updateQueue=z}else V.add(C);break e}if(!(1&o)){Si(_,j,o),tj();break e}C=Error(p(426))}else if(Fn&&1&x.mode){var Y=Ui(w);if(null!==Y){!(65536&Y.flags)&&(Y.flags|=256),Vi(Y,w,x,0,o),Jg(Ji(C,x));break e}}_=C=Ji(C,x),4!==Vs&&(Vs=2),null===Ks?Ks=[_]:Ks.push(_),_=w;do{switch(_.tag){case 3:_.flags|=65536,o&=-o,_.lanes|=o,ph(_,Ni(0,C,o));break e;case 1:x=C;var Z=_.type,ee=_.stateNode;if(!(128&_.flags||\"function\"!=typeof Z.getDerivedStateFromError&&(null===ee||\"function\"!=typeof ee.componentDidCatch||null!==to&&to.has(ee)))){_.flags|=65536,o&=-o,_.lanes|=o,ph(_,Qi(_,x,o));break e}}_=_.return}while(null!==_)}Sk(i)}catch(s){o=s,Bs===i&&null!==i&&(Bs=i=i.return);continue}break}}function Jk(){var s=Ms.current;return Ms.current=hs,null===s?hs:s}function tj(){0!==Vs&&3!==Vs&&2!==Vs||(Vs=4),null===Fs||!(268435455&Ws)&&!(268435455&Js)||Ck(Fs,$s)}function Ik(s,o){var i=Ls;Ls|=2;var a=Jk();for(Fs===s&&$s===o||(Qs=null,Kk(s,o));;)try{Tk();break}catch(o){Mk(s,o)}if($g(),Ls=i,Ms.current=a,null!==Bs)throw Error(p(261));return Fs=null,$s=0,Vs}function Tk(){for(;null!==Bs;)Uk(Bs)}function Lk(){for(;null!==Bs&&!ut();)Uk(Bs)}function Uk(s){var o=Ts(s.alternate,s,qs);s.memoizedProps=s.pendingProps,null===o?Sk(s):Bs=o,Rs.current=null}function Sk(s){var o=s;do{var i=o.alternate;if(s=o.return,32768&o.flags){if(null!==(i=Ij(i,o)))return i.flags&=32767,void(Bs=i);if(null===s)return Vs=6,void(Bs=null);s.flags|=32768,s.subtreeFlags=0,s.deletions=null}else if(null!==(i=Ej(i,o,qs)))return void(Bs=i);if(null!==(o=o.sibling))return void(Bs=o);Bs=o=s}while(null!==o);0===Vs&&(Vs=5)}function Pk(s,o,i){var a=At,u=Ds.transition;try{Ds.transition=null,At=1,function Wk(s,o,i,a){do{Hk()}while(null!==no);if(6&Ls)throw Error(p(327));i=s.finishedWork;var u=s.finishedLanes;if(null===i)return null;if(s.finishedWork=null,s.finishedLanes=0,i===s.current)throw Error(p(177));s.callbackNode=null,s.callbackPriority=0;var _=i.lanes|i.childLanes;if(function Bc(s,o){var i=s.pendingLanes&~o;s.pendingLanes=o,s.suspendedLanes=0,s.pingedLanes=0,s.expiredLanes&=o,s.mutableReadLanes&=o,s.entangledLanes&=o,o=s.entanglements;var a=s.eventTimes;for(s=s.expirationTimes;0<i;){var u=31-Et(i),_=1<<u;o[u]=0,a[u]=-1,s[u]=-1,i&=~_}}(s,_),s===Fs&&(Bs=Fs=null,$s=0),!(2064&i.subtreeFlags)&&!(2064&i.flags)||ro||(ro=!0,Fk(yt,(function(){return Hk(),null}))),_=!!(15990&i.flags),!!(15990&i.subtreeFlags)||_){_=Ds.transition,Ds.transition=null;var w=At;At=1;var x=Ls;Ls|=4,Rs.current=null,function Oj(s,o){if(sn=Vt,Ne(s=Me())){if(\"selectionStart\"in s)var i={start:s.selectionStart,end:s.selectionEnd};else e:{var a=(i=(i=s.ownerDocument)&&i.defaultView||window).getSelection&&i.getSelection();if(a&&0!==a.rangeCount){i=a.anchorNode;var u=a.anchorOffset,_=a.focusNode;a=a.focusOffset;try{i.nodeType,_.nodeType}catch(s){i=null;break e}var w=0,x=-1,C=-1,j=0,L=0,B=s,$=null;t:for(;;){for(var U;B!==i||0!==u&&3!==B.nodeType||(x=w+u),B!==_||0!==a&&3!==B.nodeType||(C=w+a),3===B.nodeType&&(w+=B.nodeValue.length),null!==(U=B.firstChild);)$=B,B=U;for(;;){if(B===s)break t;if($===i&&++j===u&&(x=w),$===_&&++L===a&&(C=w),null!==(U=B.nextSibling))break;$=(B=$).parentNode}B=U}i=-1===x||-1===C?null:{start:x,end:C}}else i=null}i=i||{start:0,end:0}}else i=null;for(on={focusedElem:s,selectionRange:i},Vt=!1,Cs=o;null!==Cs;)if(s=(o=Cs).child,1028&o.subtreeFlags&&null!==s)s.return=o,Cs=s;else for(;null!==Cs;){o=Cs;try{var V=o.alternate;if(1024&o.flags)switch(o.tag){case 0:case 11:case 15:case 5:case 6:case 4:case 17:break;case 1:if(null!==V){var z=V.memoizedProps,Y=V.memoizedState,Z=o.stateNode,ee=Z.getSnapshotBeforeUpdate(o.elementType===o.type?z:Ci(o.type,z),Y);Z.__reactInternalSnapshotBeforeUpdate=ee}break;case 3:var ie=o.stateNode.containerInfo;1===ie.nodeType?ie.textContent=\"\":9===ie.nodeType&&ie.documentElement&&ie.removeChild(ie.documentElement);break;default:throw Error(p(163))}}catch(s){W(o,o.return,s)}if(null!==(s=o.sibling)){s.return=o.return,Cs=s;break}Cs=o.return}return V=js,js=!1,V}(s,i),dk(i,s),Oe(on),Vt=!!sn,on=sn=null,s.current=i,hk(i,s,u),pt(),Ls=x,At=w,Ds.transition=_}else s.current=i;if(ro&&(ro=!1,no=s,so=u),_=s.pendingLanes,0===_&&(to=null),function mc(s){if(St&&\"function\"==typeof St.onCommitFiberRoot)try{St.onCommitFiberRoot(_t,s,void 0,!(128&~s.current.flags))}catch(s){}}(i.stateNode),Dk(s,ht()),null!==o)for(a=s.onRecoverableError,i=0;i<o.length;i++)u=o[i],a(u.value,{componentStack:u.stack,digest:u.digest});if(Zs)throw Zs=!1,s=eo,eo=null,s;return!!(1&so)&&0!==s.tag&&Hk(),_=s.pendingLanes,1&_?s===io?oo++:(oo=0,io=s):oo=0,jg(),null}(s,o,i,a)}finally{Ds.transition=u,At=a}return null}function Hk(){if(null!==no){var s=Dc(so),o=Ds.transition,i=At;try{if(Ds.transition=null,At=16>s?16:s,null===no)var a=!1;else{if(s=no,no=null,so=0,6&Ls)throw Error(p(331));var u=Ls;for(Ls|=4,Cs=s.current;null!==Cs;){var _=Cs,w=_.child;if(16&Cs.flags){var x=_.deletions;if(null!==x){for(var C=0;C<x.length;C++){var j=x[C];for(Cs=j;null!==Cs;){var L=Cs;switch(L.tag){case 0:case 11:case 15:Pj(8,L,_)}var B=L.child;if(null!==B)B.return=L,Cs=B;else for(;null!==Cs;){var $=(L=Cs).sibling,U=L.return;if(Sj(L),L===j){Cs=null;break}if(null!==$){$.return=U,Cs=$;break}Cs=U}}}var V=_.alternate;if(null!==V){var z=V.child;if(null!==z){V.child=null;do{var Y=z.sibling;z.sibling=null,z=Y}while(null!==z)}}Cs=_}}if(2064&_.subtreeFlags&&null!==w)w.return=_,Cs=w;else e:for(;null!==Cs;){if(2048&(_=Cs).flags)switch(_.tag){case 0:case 11:case 15:Pj(9,_,_.return)}var Z=_.sibling;if(null!==Z){Z.return=_.return,Cs=Z;break e}Cs=_.return}}var ee=s.current;for(Cs=ee;null!==Cs;){var ie=(w=Cs).child;if(2064&w.subtreeFlags&&null!==ie)ie.return=w,Cs=ie;else e:for(w=ee;null!==Cs;){if(2048&(x=Cs).flags)try{switch(x.tag){case 0:case 11:case 15:Qj(9,x)}}catch(s){W(x,x.return,s)}if(x===w){Cs=null;break e}var ae=x.sibling;if(null!==ae){ae.return=x.return,Cs=ae;break e}Cs=x.return}}if(Ls=u,jg(),St&&\"function\"==typeof St.onPostCommitFiberRoot)try{St.onPostCommitFiberRoot(_t,s)}catch(s){}a=!0}return a}finally{At=i,Ds.transition=o}}return!1}function Xk(s,o,i){s=nh(s,o=Ni(0,o=Ji(i,o),1),1),o=R(),null!==s&&(Ac(s,1,o),Dk(s,o))}function W(s,o,i){if(3===s.tag)Xk(s,s,i);else for(;null!==o;){if(3===o.tag){Xk(o,s,i);break}if(1===o.tag){var a=o.stateNode;if(\"function\"==typeof o.type.getDerivedStateFromError||\"function\"==typeof a.componentDidCatch&&(null===to||!to.has(a))){o=nh(o,s=Qi(o,s=Ji(i,s),1),1),s=R(),null!==o&&(Ac(o,1,s),Dk(o,s));break}}o=o.return}}function Ti(s,o,i){var a=s.pingCache;null!==a&&a.delete(o),o=R(),s.pingedLanes|=s.suspendedLanes&i,Fs===s&&($s&i)===i&&(4===Vs||3===Vs&&(130023424&$s)===$s&&500>ht()-Ys?Kk(s,0):Hs|=i),Dk(s,o)}function Yk(s,o){0===o&&(1&s.mode?(o=Ot,!(130023424&(Ot<<=1))&&(Ot=4194304)):o=1);var i=R();null!==(s=ih(s,o))&&(Ac(s,o,i),Dk(s,i))}function uj(s){var o=s.memoizedState,i=0;null!==o&&(i=o.retryLane),Yk(s,i)}function bk(s,o){var i=0;switch(s.tag){case 13:var a=s.stateNode,u=s.memoizedState;null!==u&&(i=u.retryLane);break;case 19:a=s.stateNode;break;default:throw Error(p(314))}null!==a&&a.delete(o),Yk(s,i)}function Fk(s,o){return ct(s,o)}function $k(s,o,i,a){this.tag=s,this.key=i,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=o,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=a,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Bg(s,o,i,a){return new $k(s,o,i,a)}function aj(s){return!(!(s=s.prototype)||!s.isReactComponent)}function Pg(s,o){var i=s.alternate;return null===i?((i=Bg(s.tag,o,s.key,s.mode)).elementType=s.elementType,i.type=s.type,i.stateNode=s.stateNode,i.alternate=s,s.alternate=i):(i.pendingProps=o,i.type=s.type,i.flags=0,i.subtreeFlags=0,i.deletions=null),i.flags=14680064&s.flags,i.childLanes=s.childLanes,i.lanes=s.lanes,i.child=s.child,i.memoizedProps=s.memoizedProps,i.memoizedState=s.memoizedState,i.updateQueue=s.updateQueue,o=s.dependencies,i.dependencies=null===o?null:{lanes:o.lanes,firstContext:o.firstContext},i.sibling=s.sibling,i.index=s.index,i.ref=s.ref,i}function Rg(s,o,i,a,u,_){var w=2;if(a=s,\"function\"==typeof s)aj(s)&&(w=1);else if(\"string\"==typeof s)w=5;else e:switch(s){case Z:return Tg(i.children,u,_,o);case ee:w=8,u|=8;break;case ie:return(s=Bg(12,i,o,2|u)).elementType=ie,s.lanes=_,s;case pe:return(s=Bg(13,i,o,u)).elementType=pe,s.lanes=_,s;case de:return(s=Bg(19,i,o,u)).elementType=de,s.lanes=_,s;case be:return pj(i,u,_,o);default:if(\"object\"==typeof s&&null!==s)switch(s.$$typeof){case ae:w=10;break e;case ce:w=9;break e;case le:w=11;break e;case fe:w=14;break e;case ye:w=16,a=null;break e}throw Error(p(130,null==s?s:typeof s,\"\"))}return(o=Bg(w,i,o,u)).elementType=s,o.type=a,o.lanes=_,o}function Tg(s,o,i,a){return(s=Bg(7,s,a,o)).lanes=i,s}function pj(s,o,i,a){return(s=Bg(22,s,a,o)).elementType=be,s.lanes=i,s.stateNode={isHidden:!1},s}function Qg(s,o,i){return(s=Bg(6,s,null,o)).lanes=i,s}function Sg(s,o,i){return(o=Bg(4,null!==s.children?s.children:[],s.key,o)).lanes=i,o.stateNode={containerInfo:s.containerInfo,pendingChildren:null,implementation:s.implementation},o}function al(s,o,i,a,u){this.tag=o,this.containerInfo=s,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=zc(0),this.expirationTimes=zc(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=zc(0),this.identifierPrefix=a,this.onRecoverableError=u,this.mutableSourceEagerHydrationData=null}function bl(s,o,i,a,u,_,w,x,C){return s=new al(s,o,i,x,C),1===o?(o=1,!0===_&&(o|=8)):o=0,_=Bg(3,null,null,o),s.current=_,_.stateNode=s,_.memoizedState={element:a,isDehydrated:i,cache:null,transitions:null,pendingSuspenseBoundaries:null},kh(_),s}function dl(s){if(!s)return _n;e:{if(Vb(s=s._reactInternals)!==s||1!==s.tag)throw Error(p(170));var o=s;do{switch(o.tag){case 3:o=o.stateNode.context;break e;case 1:if(Zf(o.type)){o=o.stateNode.__reactInternalMemoizedMergedChildContext;break e}}o=o.return}while(null!==o);throw Error(p(171))}if(1===s.tag){var i=s.type;if(Zf(i))return bg(s,i,o)}return o}function el(s,o,i,a,u,_,w,x,C){return(s=bl(i,a,!0,s,0,_,0,x,C)).context=dl(null),i=s.current,(_=mh(a=R(),u=yi(i))).callback=null!=o?o:null,nh(i,_,u),s.current.lanes=u,Ac(s,u,a),Dk(s,a),s}function fl(s,o,i,a){var u=o.current,_=R(),w=yi(u);return i=dl(i),null===o.context?o.context=i:o.pendingContext=i,(o=mh(_,w)).payload={element:s},null!==(a=void 0===a?null:a)&&(o.callback=a),null!==(s=nh(u,o,w))&&(gi(s,u,w,_),oh(s,u,w)),w}function gl(s){return(s=s.current).child?(s.child.tag,s.child.stateNode):null}function hl(s,o){if(null!==(s=s.memoizedState)&&null!==s.dehydrated){var i=s.retryLane;s.retryLane=0!==i&&i<o?i:o}}function il(s,o){hl(s,o),(s=s.alternate)&&hl(s,o)}Ts=function(s,o,i){if(null!==s)if(s.memoizedProps!==o.pendingProps||En.current)bs=!0;else{if(!(s.lanes&i||128&o.flags))return bs=!1,function yj(s,o,i){switch(o.tag){case 3:kj(o),Ig();break;case 5:Ah(o);break;case 1:Zf(o.type)&&cg(o);break;case 4:yh(o,o.stateNode.containerInfo);break;case 10:var a=o.type._context,u=o.memoizedProps.value;G(Vn,a._currentValue),a._currentValue=u;break;case 13:if(null!==(a=o.memoizedState))return null!==a.dehydrated?(G(Zn,1&Zn.current),o.flags|=128,null):i&o.child.childLanes?oj(s,o,i):(G(Zn,1&Zn.current),null!==(s=Zi(s,o,i))?s.sibling:null);G(Zn,1&Zn.current);break;case 19:if(a=!!(i&o.childLanes),128&s.flags){if(a)return xj(s,o,i);o.flags|=128}if(null!==(u=o.memoizedState)&&(u.rendering=null,u.tail=null,u.lastEffect=null),G(Zn,Zn.current),a)break;return null;case 22:case 23:return o.lanes=0,dj(s,o,i)}return Zi(s,o,i)}(s,o,i);bs=!!(131072&s.flags)}else bs=!1,Fn&&1048576&o.flags&&ug(o,Pn,o.index);switch(o.lanes=0,o.tag){case 2:var a=o.type;ij(s,o),s=o.pendingProps;var u=Yf(o,Sn.current);ch(o,i),u=Nh(null,o,a,s,u,i);var _=Sh();return o.flags|=1,\"object\"==typeof u&&null!==u&&\"function\"==typeof u.render&&void 0===u.$$typeof?(o.tag=1,o.memoizedState=null,o.updateQueue=null,Zf(a)?(_=!0,cg(o)):_=!1,o.memoizedState=null!==u.state&&void 0!==u.state?u.state:null,kh(o),u.updater=gs,o.stateNode=u,u._reactInternals=o,Ii(o,a,s,i),o=jj(null,o,a,!0,_,i)):(o.tag=0,Fn&&_&&vg(o),Xi(null,o,u,i),o=o.child),o;case 16:a=o.elementType;e:{switch(ij(s,o),s=o.pendingProps,a=(u=a._init)(a._payload),o.type=a,u=o.tag=function Zk(s){if(\"function\"==typeof s)return aj(s)?1:0;if(null!=s){if((s=s.$$typeof)===le)return 11;if(s===fe)return 14}return 2}(a),s=Ci(a,s),u){case 0:o=cj(null,o,a,s,i);break e;case 1:o=hj(null,o,a,s,i);break e;case 11:o=Yi(null,o,a,s,i);break e;case 14:o=$i(null,o,a,Ci(a.type,s),i);break e}throw Error(p(306,a,\"\"))}return o;case 0:return a=o.type,u=o.pendingProps,cj(s,o,a,u=o.elementType===a?u:Ci(a,u),i);case 1:return a=o.type,u=o.pendingProps,hj(s,o,a,u=o.elementType===a?u:Ci(a,u),i);case 3:e:{if(kj(o),null===s)throw Error(p(387));a=o.pendingProps,u=(_=o.memoizedState).element,lh(s,o),qh(o,a,null,i);var w=o.memoizedState;if(a=w.element,_.isDehydrated){if(_={element:a,isDehydrated:!1,cache:w.cache,pendingSuspenseBoundaries:w.pendingSuspenseBoundaries,transitions:w.transitions},o.updateQueue.baseState=_,o.memoizedState=_,256&o.flags){o=lj(s,o,a,i,u=Ji(Error(p(423)),o));break e}if(a!==u){o=lj(s,o,a,i,u=Ji(Error(p(424)),o));break e}for(Ln=Lf(o.stateNode.containerInfo.firstChild),Dn=o,Fn=!0,Bn=null,i=Un(o,null,a,i),o.child=i;i;)i.flags=-3&i.flags|4096,i=i.sibling}else{if(Ig(),a===u){o=Zi(s,o,i);break e}Xi(s,o,a,i)}o=o.child}return o;case 5:return Ah(o),null===s&&Eg(o),a=o.type,u=o.pendingProps,_=null!==s?s.memoizedProps:null,w=u.children,Ef(a,u)?w=null:null!==_&&Ef(a,_)&&(o.flags|=32),gj(s,o),Xi(s,o,w,i),o.child;case 6:return null===s&&Eg(o),null;case 13:return oj(s,o,i);case 4:return yh(o,o.stateNode.containerInfo),a=o.pendingProps,null===s?o.child=qn(o,null,a,i):Xi(s,o,a,i),o.child;case 11:return a=o.type,u=o.pendingProps,Yi(s,o,a,u=o.elementType===a?u:Ci(a,u),i);case 7:return Xi(s,o,o.pendingProps,i),o.child;case 8:case 12:return Xi(s,o,o.pendingProps.children,i),o.child;case 10:e:{if(a=o.type._context,u=o.pendingProps,_=o.memoizedProps,w=u.value,G(Vn,a._currentValue),a._currentValue=w,null!==_)if(Dr(_.value,w)){if(_.children===u.children&&!En.current){o=Zi(s,o,i);break e}}else for(null!==(_=o.child)&&(_.return=o);null!==_;){var x=_.dependencies;if(null!==x){w=_.child;for(var C=x.firstContext;null!==C;){if(C.context===a){if(1===_.tag){(C=mh(-1,i&-i)).tag=2;var j=_.updateQueue;if(null!==j){var L=(j=j.shared).pending;null===L?C.next=C:(C.next=L.next,L.next=C),j.pending=C}}_.lanes|=i,null!==(C=_.alternate)&&(C.lanes|=i),bh(_.return,i,o),x.lanes|=i;break}C=C.next}}else if(10===_.tag)w=_.type===o.type?null:_.child;else if(18===_.tag){if(null===(w=_.return))throw Error(p(341));w.lanes|=i,null!==(x=w.alternate)&&(x.lanes|=i),bh(w,i,o),w=_.sibling}else w=_.child;if(null!==w)w.return=_;else for(w=_;null!==w;){if(w===o){w=null;break}if(null!==(_=w.sibling)){_.return=w.return,w=_;break}w=w.return}_=w}Xi(s,o,u.children,i),o=o.child}return o;case 9:return u=o.type,a=o.pendingProps.children,ch(o,i),a=a(u=eh(u)),o.flags|=1,Xi(s,o,a,i),o.child;case 14:return u=Ci(a=o.type,o.pendingProps),$i(s,o,a,u=Ci(a.type,u),i);case 15:return bj(s,o,o.type,o.pendingProps,i);case 17:return a=o.type,u=o.pendingProps,u=o.elementType===a?u:Ci(a,u),ij(s,o),o.tag=1,Zf(a)?(s=!0,cg(o)):s=!1,ch(o,i),Gi(o,a,u),Ii(o,a,u,i),jj(null,o,a,!0,s,i);case 19:return xj(s,o,i);case 22:return dj(s,o,i)}throw Error(p(156,o.tag))};var lo=\"function\"==typeof reportError?reportError:function(s){console.error(s)};function ll(s){this._internalRoot=s}function ml(s){this._internalRoot=s}function nl(s){return!(!s||1!==s.nodeType&&9!==s.nodeType&&11!==s.nodeType)}function ol(s){return!(!s||1!==s.nodeType&&9!==s.nodeType&&11!==s.nodeType&&(8!==s.nodeType||\" react-mount-point-unstable \"!==s.nodeValue))}function pl(){}function rl(s,o,i,a,u){var _=i._reactRootContainer;if(_){var w=_;if(\"function\"==typeof u){var x=u;u=function(){var s=gl(w);x.call(s)}}fl(o,w,s,u)}else w=function ql(s,o,i,a,u){if(u){if(\"function\"==typeof a){var _=a;a=function(){var s=gl(w);_.call(s)}}var w=el(o,a,s,0,null,!1,0,\"\",pl);return s._reactRootContainer=w,s[fn]=w.current,sf(8===s.nodeType?s.parentNode:s),Rk(),w}for(;u=s.lastChild;)s.removeChild(u);if(\"function\"==typeof a){var x=a;a=function(){var s=gl(C);x.call(s)}}var C=bl(s,0,!1,null,0,!1,0,\"\",pl);return s._reactRootContainer=C,s[fn]=C.current,sf(8===s.nodeType?s.parentNode:s),Rk((function(){fl(o,C,i,a)})),C}(i,o,s,u,a);return gl(w)}ml.prototype.render=ll.prototype.render=function(s){var o=this._internalRoot;if(null===o)throw Error(p(409));fl(s,o,null,null)},ml.prototype.unmount=ll.prototype.unmount=function(){var s=this._internalRoot;if(null!==s){this._internalRoot=null;var o=s.containerInfo;Rk((function(){fl(null,s,null,null)})),o[fn]=null}},ml.prototype.unstable_scheduleHydration=function(s){if(s){var o=It();s={blockedOn:null,target:s,priority:o};for(var i=0;i<$t.length&&0!==o&&o<$t[i].priority;i++);$t.splice(i,0,s),0===i&&Vc(s)}},Ct=function(s){switch(s.tag){case 3:var o=s.stateNode;if(o.current.memoizedState.isDehydrated){var i=tc(o.pendingLanes);0!==i&&(Cc(o,1|i),Dk(o,ht()),!(6&Ls)&&(Xs=ht()+500,jg()))}break;case 13:Rk((function(){var o=ih(s,1);if(null!==o){var i=R();gi(o,s,1,i)}})),il(s,1)}},jt=function(s){if(13===s.tag){var o=ih(s,134217728);if(null!==o)gi(o,s,134217728,R());il(s,134217728)}},Pt=function(s){if(13===s.tag){var o=yi(s),i=ih(s,o);if(null!==i)gi(i,s,o,R());il(s,o)}},It=function(){return At},Tt=function(s,o){var i=At;try{return At=s,o()}finally{At=i}},Ye=function(s,o,i){switch(o){case\"input\":if(bb(s,i),o=i.name,\"radio\"===i.type&&null!=o){for(i=s;i.parentNode;)i=i.parentNode;for(i=i.querySelectorAll(\"input[name=\"+JSON.stringify(\"\"+o)+'][type=\"radio\"]'),o=0;o<i.length;o++){var a=i[o];if(a!==s&&a.form===s.form){var u=Db(a);if(!u)throw Error(p(90));Wa(a),bb(a,u)}}}break;case\"textarea\":ib(s,i);break;case\"select\":null!=(o=i.value)&&fb(s,!!i.multiple,o,!1)}},Gb=Qk,Hb=Rk;var uo={usingClientEntryPoint:!1,Events:[Cb,ue,Db,Eb,Fb,Qk]},po={findFiberByHostInstance:Wc,bundleType:0,version:\"18.3.1\",rendererPackageName:\"react-dom\"},ho={bundleType:po.bundleType,version:po.version,rendererPackageName:po.rendererPackageName,rendererConfig:po.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setErrorHandler:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:V.ReactCurrentDispatcher,findHostInstanceByFiber:function(s){return null===(s=Zb(s))?null:s.stateNode},findFiberByHostInstance:po.findFiberByHostInstance||function jl(){return null},findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null,reconcilerVersion:\"18.3.1-next-f1338f8080-20240426\"};if(\"undefined\"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__){var fo=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!fo.isDisabled&&fo.supportsFiber)try{_t=fo.inject(ho),St=fo}catch(Re){}}o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=uo,o.createPortal=function(s,o){var i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(!nl(o))throw Error(p(200));return function cl(s,o,i){var a=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:Y,key:null==a?null:\"\"+a,children:s,containerInfo:o,implementation:i}}(s,o,null,i)},o.createRoot=function(s,o){if(!nl(s))throw Error(p(299));var i=!1,a=\"\",u=lo;return null!=o&&(!0===o.unstable_strictMode&&(i=!0),void 0!==o.identifierPrefix&&(a=o.identifierPrefix),void 0!==o.onRecoverableError&&(u=o.onRecoverableError)),o=bl(s,1,!1,null,0,i,0,a,u),s[fn]=o.current,sf(8===s.nodeType?s.parentNode:s),new ll(o)},o.findDOMNode=function(s){if(null==s)return null;if(1===s.nodeType)return s;var o=s._reactInternals;if(void 0===o){if(\"function\"==typeof s.render)throw Error(p(188));throw s=Object.keys(s).join(\",\"),Error(p(268,s))}return s=null===(s=Zb(o))?null:s.stateNode},o.flushSync=function(s){return Rk(s)},o.hydrate=function(s,o,i){if(!ol(o))throw Error(p(200));return rl(null,s,o,!0,i)},o.hydrateRoot=function(s,o,i){if(!nl(s))throw Error(p(405));var a=null!=i&&i.hydratedSources||null,u=!1,_=\"\",w=lo;if(null!=i&&(!0===i.unstable_strictMode&&(u=!0),void 0!==i.identifierPrefix&&(_=i.identifierPrefix),void 0!==i.onRecoverableError&&(w=i.onRecoverableError)),o=el(o,null,s,1,null!=i?i:null,u,0,_,w),s[fn]=o.current,sf(s),a)for(s=0;s<a.length;s++)u=(u=(i=a[s])._getVersion)(i._source),null==o.mutableSourceEagerHydrationData?o.mutableSourceEagerHydrationData=[i,u]:o.mutableSourceEagerHydrationData.push(i,u);return new ml(o)},o.render=function(s,o,i){if(!ol(o))throw Error(p(200));return rl(null,s,o,!1,i)},o.unmountComponentAtNode=function(s){if(!ol(s))throw Error(p(40));return!!s._reactRootContainer&&(Rk((function(){rl(null,null,s,!1,(function(){s._reactRootContainer=null,s[fn]=null}))})),!0)},o.unstable_batchedUpdates=Qk,o.unstable_renderSubtreeIntoContainer=function(s,o,i,a){if(!ol(i))throw Error(p(200));if(null==s||void 0===s._reactInternals)throw Error(p(38));return rl(s,o,i,!1,a)},o.version=\"18.3.1-next-f1338f8080-20240426\"},22574:(s,o)=>{\"use strict\";var i={}.propertyIsEnumerable,a=Object.getOwnPropertyDescriptor,u=a&&!i.call({1:2},1);o.f=u?function propertyIsEnumerable(s){var o=a(this,s);return!!o&&o.enumerable}:i},23007:s=>{s.exports=function copyArray(s,o){var i=-1,a=s.length;for(o||(o=Array(a));++i<a;)o[i]=s[i];return o}},23034:(s,o,i)=>{\"use strict\";var a=i(88280),u=i(32567),_=Function.prototype;s.exports=function(s){var o=s.bind;return s===_||a(_,s)&&o===_.bind?u:o}},23045:(s,o,i)=>{\"use strict\";var a=i(1907),u=i(49724),_=i(4993),w=i(74436).indexOf,x=i(38530),C=a([].push);s.exports=function(s,o){var i,a=_(s),j=0,L=[];for(i in a)!u(x,i)&&u(a,i)&&C(L,i);for(;o.length>j;)u(a,i=o[j++])&&(~w(L,i)||C(L,i));return L}},23546:(s,o,i)=>{var a=i(72552),u=i(40346),_=i(11331);s.exports=function isError(s){if(!u(s))return!1;var o=a(s);return\"[object Error]\"==o||\"[object DOMException]\"==o||\"string\"==typeof s.message&&\"string\"==typeof s.name&&!_(s)}},23805:s=>{s.exports=function isObject(s){var o=typeof s;return null!=s&&(\"object\"==o||\"function\"==o)}},23888:(s,o,i)=>{\"use strict\";var a=i(98828),u=i(75817);s.exports=!a((function(){var s=new Error(\"a\");return!(\"stack\"in s)||(Object.defineProperty(s,\"stack\",u(1,7)),7!==s.stack)}))},24107:(s,o,i)=>{\"use strict\";var a=i(56698),u=i(90392),_=i(92861).Buffer,w=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],x=new Array(64);function Sha256(){this.init(),this._w=x,u.call(this,64,56)}function ch(s,o,i){return i^s&(o^i)}function maj(s,o,i){return s&o|i&(s|o)}function sigma0(s){return(s>>>2|s<<30)^(s>>>13|s<<19)^(s>>>22|s<<10)}function sigma1(s){return(s>>>6|s<<26)^(s>>>11|s<<21)^(s>>>25|s<<7)}function gamma0(s){return(s>>>7|s<<25)^(s>>>18|s<<14)^s>>>3}a(Sha256,u),Sha256.prototype.init=function(){return this._a=1779033703,this._b=3144134277,this._c=1013904242,this._d=2773480762,this._e=1359893119,this._f=2600822924,this._g=528734635,this._h=1541459225,this},Sha256.prototype._update=function(s){for(var o,i=this._w,a=0|this._a,u=0|this._b,_=0|this._c,x=0|this._d,C=0|this._e,j=0|this._f,L=0|this._g,B=0|this._h,$=0;$<16;++$)i[$]=s.readInt32BE(4*$);for(;$<64;++$)i[$]=0|(((o=i[$-2])>>>17|o<<15)^(o>>>19|o<<13)^o>>>10)+i[$-7]+gamma0(i[$-15])+i[$-16];for(var U=0;U<64;++U){var V=B+sigma1(C)+ch(C,j,L)+w[U]+i[U]|0,z=sigma0(a)+maj(a,u,_)|0;B=L,L=j,j=C,C=x+V|0,x=_,_=u,u=a,a=V+z|0}this._a=a+this._a|0,this._b=u+this._b|0,this._c=_+this._c|0,this._d=x+this._d|0,this._e=C+this._e|0,this._f=j+this._f|0,this._g=L+this._g|0,this._h=B+this._h|0},Sha256.prototype._hash=function(){var s=_.allocUnsafe(32);return s.writeInt32BE(this._a,0),s.writeInt32BE(this._b,4),s.writeInt32BE(this._c,8),s.writeInt32BE(this._d,12),s.writeInt32BE(this._e,16),s.writeInt32BE(this._f,20),s.writeInt32BE(this._g,24),s.writeInt32BE(this._h,28),s},s.exports=Sha256},24168:(s,o,i)=>{var a=i(91033),u=i(82819),_=i(9325);s.exports=function createPartial(s,o,i,w){var x=1&o,C=u(s);return function wrapper(){for(var o=-1,u=arguments.length,j=-1,L=w.length,B=Array(L+u),$=this&&this!==_&&this instanceof wrapper?C:s;++j<L;)B[j]=w[j];for(;u--;)B[j++]=arguments[++o];return a($,x?i:this,B)}}},24443:(s,o,i)=>{\"use strict\";var a=i(23045),u=i(80376).concat(\"length\",\"prototype\");o.f=Object.getOwnPropertyNames||function getOwnPropertyNames(s){return a(s,u)}},24647:(s,o,i)=>{var a=i(54552)({À:\"A\",Á:\"A\",Â:\"A\",Ã:\"A\",Ä:\"A\",Å:\"A\",à:\"a\",á:\"a\",â:\"a\",ã:\"a\",ä:\"a\",å:\"a\",Ç:\"C\",ç:\"c\",Ð:\"D\",ð:\"d\",È:\"E\",É:\"E\",Ê:\"E\",Ë:\"E\",è:\"e\",é:\"e\",ê:\"e\",ë:\"e\",Ì:\"I\",Í:\"I\",Î:\"I\",Ï:\"I\",ì:\"i\",í:\"i\",î:\"i\",ï:\"i\",Ñ:\"N\",ñ:\"n\",Ò:\"O\",Ó:\"O\",Ô:\"O\",Õ:\"O\",Ö:\"O\",Ø:\"O\",ò:\"o\",ó:\"o\",ô:\"o\",õ:\"o\",ö:\"o\",ø:\"o\",Ù:\"U\",Ú:\"U\",Û:\"U\",Ü:\"U\",ù:\"u\",ú:\"u\",û:\"u\",ü:\"u\",Ý:\"Y\",ý:\"y\",ÿ:\"y\",Æ:\"Ae\",æ:\"ae\",Þ:\"Th\",þ:\"th\",ß:\"ss\",Ā:\"A\",Ă:\"A\",Ą:\"A\",ā:\"a\",ă:\"a\",ą:\"a\",Ć:\"C\",Ĉ:\"C\",Ċ:\"C\",Č:\"C\",ć:\"c\",ĉ:\"c\",ċ:\"c\",č:\"c\",Ď:\"D\",Đ:\"D\",ď:\"d\",đ:\"d\",Ē:\"E\",Ĕ:\"E\",Ė:\"E\",Ę:\"E\",Ě:\"E\",ē:\"e\",ĕ:\"e\",ė:\"e\",ę:\"e\",ě:\"e\",Ĝ:\"G\",Ğ:\"G\",Ġ:\"G\",Ģ:\"G\",ĝ:\"g\",ğ:\"g\",ġ:\"g\",ģ:\"g\",Ĥ:\"H\",Ħ:\"H\",ĥ:\"h\",ħ:\"h\",Ĩ:\"I\",Ī:\"I\",Ĭ:\"I\",Į:\"I\",İ:\"I\",ĩ:\"i\",ī:\"i\",ĭ:\"i\",į:\"i\",ı:\"i\",Ĵ:\"J\",ĵ:\"j\",Ķ:\"K\",ķ:\"k\",ĸ:\"k\",Ĺ:\"L\",Ļ:\"L\",Ľ:\"L\",Ŀ:\"L\",Ł:\"L\",ĺ:\"l\",ļ:\"l\",ľ:\"l\",ŀ:\"l\",ł:\"l\",Ń:\"N\",Ņ:\"N\",Ň:\"N\",Ŋ:\"N\",ń:\"n\",ņ:\"n\",ň:\"n\",ŋ:\"n\",Ō:\"O\",Ŏ:\"O\",Ő:\"O\",ō:\"o\",ŏ:\"o\",ő:\"o\",Ŕ:\"R\",Ŗ:\"R\",Ř:\"R\",ŕ:\"r\",ŗ:\"r\",ř:\"r\",Ś:\"S\",Ŝ:\"S\",Ş:\"S\",Š:\"S\",ś:\"s\",ŝ:\"s\",ş:\"s\",š:\"s\",Ţ:\"T\",Ť:\"T\",Ŧ:\"T\",ţ:\"t\",ť:\"t\",ŧ:\"t\",Ũ:\"U\",Ū:\"U\",Ŭ:\"U\",Ů:\"U\",Ű:\"U\",Ų:\"U\",ũ:\"u\",ū:\"u\",ŭ:\"u\",ů:\"u\",ű:\"u\",ų:\"u\",Ŵ:\"W\",ŵ:\"w\",Ŷ:\"Y\",ŷ:\"y\",Ÿ:\"Y\",Ź:\"Z\",Ż:\"Z\",Ž:\"Z\",ź:\"z\",ż:\"z\",ž:\"z\",Ĳ:\"IJ\",ĳ:\"ij\",Œ:\"Oe\",œ:\"oe\",ŉ:\"'n\",ſ:\"s\"});s.exports=a},24677:(s,o,i)=>{\"use strict\";var a=i(81214).DebounceInput;a.DebounceInput=a,s.exports=a},24713:(s,o,i)=>{var a=i(2523),u=i(15389),_=i(61489),w=Math.max;s.exports=function findIndex(s,o,i){var x=null==s?0:s.length;if(!x)return-1;var C=null==i?0:_(i);return C<0&&(C=w(x+C,0)),a(s,u(o,3),C)}},24739:(s,o,i)=>{var a=i(26025);s.exports=function listCacheGet(s){var o=this.__data__,i=a(o,s);return i<0?void 0:o[i][1]}},24823:(s,o,i)=>{\"use strict\";var a=i(28311),u=i(13930),_=i(36624),w=i(4640),x=i(37812),C=i(20575),j=i(88280),L=i(10300),B=i(73448),$=i(40154),U=TypeError,Result=function(s,o){this.stopped=s,this.result=o},V=Result.prototype;s.exports=function(s,o,i){var z,Y,Z,ee,ie,ae,ce,le=i&&i.that,pe=!(!i||!i.AS_ENTRIES),de=!(!i||!i.IS_RECORD),fe=!(!i||!i.IS_ITERATOR),ye=!(!i||!i.INTERRUPTED),be=a(o,le),stop=function(s){return z&&$(z,\"normal\",s),new Result(!0,s)},callFn=function(s){return pe?(_(s),ye?be(s[0],s[1],stop):be(s[0],s[1])):ye?be(s,stop):be(s)};if(de)z=s.iterator;else if(fe)z=s;else{if(!(Y=B(s)))throw new U(w(s)+\" is not iterable\");if(x(Y)){for(Z=0,ee=C(s);ee>Z;Z++)if((ie=callFn(s[Z]))&&j(V,ie))return ie;return new Result(!1)}z=L(s,Y)}for(ae=de?s.next:z.next;!(ce=u(ae,z)).done;){try{ie=callFn(ce.value)}catch(s){$(z,\"throw\",s)}if(\"object\"==typeof ie&&ie&&j(V,ie))return ie}return new Result(!1)}},25160:s=>{s.exports=function baseSlice(s,o,i){var a=-1,u=s.length;o<0&&(o=-o>u?0:u+o),(i=i>u?u:i)<0&&(i+=u),u=o>i?0:i-o>>>0,o>>>=0;for(var _=Array(u);++a<u;)_[a]=s[a+o];return _}},25264:(s,o,i)=>{\"use strict\";function _typeof(s){return _typeof=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(s){return typeof s}:function(s){return s&&\"function\"==typeof Symbol&&s.constructor===Symbol&&s!==Symbol.prototype?\"symbol\":typeof s},_typeof(s)}Object.defineProperty(o,\"__esModule\",{value:!0}),o.CopyToClipboard=void 0;var a=_interopRequireDefault(i(96540)),u=_interopRequireDefault(i(17965)),_=[\"text\",\"onCopy\",\"options\",\"children\"];function _interopRequireDefault(s){return s&&s.__esModule?s:{default:s}}function ownKeys(s,o){var i=Object.keys(s);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(s);o&&(a=a.filter((function(o){return Object.getOwnPropertyDescriptor(s,o).enumerable}))),i.push.apply(i,a)}return i}function _objectSpread(s){for(var o=1;o<arguments.length;o++){var i=null!=arguments[o]?arguments[o]:{};o%2?ownKeys(Object(i),!0).forEach((function(o){_defineProperty(s,o,i[o])})):Object.getOwnPropertyDescriptors?Object.defineProperties(s,Object.getOwnPropertyDescriptors(i)):ownKeys(Object(i)).forEach((function(o){Object.defineProperty(s,o,Object.getOwnPropertyDescriptor(i,o))}))}return s}function _objectWithoutProperties(s,o){if(null==s)return{};var i,a,u=function _objectWithoutPropertiesLoose(s,o){if(null==s)return{};var i,a,u={},_=Object.keys(s);for(a=0;a<_.length;a++)i=_[a],o.indexOf(i)>=0||(u[i]=s[i]);return u}(s,o);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(s);for(a=0;a<_.length;a++)i=_[a],o.indexOf(i)>=0||Object.prototype.propertyIsEnumerable.call(s,i)&&(u[i]=s[i])}return u}function _defineProperties(s,o){for(var i=0;i<o.length;i++){var a=o[i];a.enumerable=a.enumerable||!1,a.configurable=!0,\"value\"in a&&(a.writable=!0),Object.defineProperty(s,a.key,a)}}function _setPrototypeOf(s,o){return _setPrototypeOf=Object.setPrototypeOf||function _setPrototypeOf(s,o){return s.__proto__=o,s},_setPrototypeOf(s,o)}function _createSuper(s){var o=function _isNativeReflectConstruct(){if(\"undefined\"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if(\"function\"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(s){return!1}}();return function _createSuperInternal(){var i,a=_getPrototypeOf(s);if(o){var u=_getPrototypeOf(this).constructor;i=Reflect.construct(a,arguments,u)}else i=a.apply(this,arguments);return function _possibleConstructorReturn(s,o){if(o&&(\"object\"===_typeof(o)||\"function\"==typeof o))return o;if(void 0!==o)throw new TypeError(\"Derived constructors may only return object or undefined\");return _assertThisInitialized(s)}(this,i)}}function _assertThisInitialized(s){if(void 0===s)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return s}function _getPrototypeOf(s){return _getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf:function _getPrototypeOf(s){return s.__proto__||Object.getPrototypeOf(s)},_getPrototypeOf(s)}function _defineProperty(s,o,i){return o in s?Object.defineProperty(s,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):s[o]=i,s}var w=function(s){!function _inherits(s,o){if(\"function\"!=typeof o&&null!==o)throw new TypeError(\"Super expression must either be null or a function\");s.prototype=Object.create(o&&o.prototype,{constructor:{value:s,writable:!0,configurable:!0}}),Object.defineProperty(s,\"prototype\",{writable:!1}),o&&_setPrototypeOf(s,o)}(CopyToClipboard,s);var o=_createSuper(CopyToClipboard);function CopyToClipboard(){var s;!function _classCallCheck(s,o){if(!(s instanceof o))throw new TypeError(\"Cannot call a class as a function\")}(this,CopyToClipboard);for(var i=arguments.length,_=new Array(i),w=0;w<i;w++)_[w]=arguments[w];return _defineProperty(_assertThisInitialized(s=o.call.apply(o,[this].concat(_))),\"onClick\",(function(o){var i=s.props,_=i.text,w=i.onCopy,x=i.children,C=i.options,j=a.default.Children.only(x),L=(0,u.default)(_,C);w&&w(_,L),j&&j.props&&\"function\"==typeof j.props.onClick&&j.props.onClick(o)})),s}return function _createClass(s,o,i){return o&&_defineProperties(s.prototype,o),i&&_defineProperties(s,i),Object.defineProperty(s,\"prototype\",{writable:!1}),s}(CopyToClipboard,[{key:\"render\",value:function render(){var s=this.props,o=(s.text,s.onCopy,s.options,s.children),i=_objectWithoutProperties(s,_),u=a.default.Children.only(o);return a.default.cloneElement(u,_objectSpread(_objectSpread({},i),{},{onClick:this.onClick}))}}]),CopyToClipboard}(a.default.PureComponent);o.CopyToClipboard=w,_defineProperty(w,\"defaultProps\",{onCopy:void 0,options:void 0})},25382:(s,o,i)=>{\"use strict\";var a=i(65606),u=Object.keys||function(s){var o=[];for(var i in s)o.push(i);return o};s.exports=Duplex;var _=i(45412),w=i(16708);i(56698)(Duplex,_);for(var x=u(w.prototype),C=0;C<x.length;C++){var j=x[C];Duplex.prototype[j]||(Duplex.prototype[j]=w.prototype[j])}function Duplex(s){if(!(this instanceof Duplex))return new Duplex(s);_.call(this,s),w.call(this,s),this.allowHalfOpen=!0,s&&(!1===s.readable&&(this.readable=!1),!1===s.writable&&(this.writable=!1),!1===s.allowHalfOpen&&(this.allowHalfOpen=!1,this.once(\"end\",onend)))}function onend(){this._writableState.ended||a.nextTick(onEndNT,this)}function onEndNT(s){s.end()}Object.defineProperty(Duplex.prototype,\"writableHighWaterMark\",{enumerable:!1,get:function get(){return this._writableState.highWaterMark}}),Object.defineProperty(Duplex.prototype,\"writableBuffer\",{enumerable:!1,get:function get(){return this._writableState&&this._writableState.getBuffer()}}),Object.defineProperty(Duplex.prototype,\"writableLength\",{enumerable:!1,get:function get(){return this._writableState.length}}),Object.defineProperty(Duplex.prototype,\"destroyed\",{enumerable:!1,get:function get(){return void 0!==this._readableState&&void 0!==this._writableState&&(this._readableState.destroyed&&this._writableState.destroyed)},set:function set(s){void 0!==this._readableState&&void 0!==this._writableState&&(this._readableState.destroyed=s,this._writableState.destroyed=s)}})},25594:(s,o,i)=>{\"use strict\";var a=i(85582),u=i(62250),_=i(88280),w=i(51175),x=Object;s.exports=w?function(s){return\"symbol\"==typeof s}:function(s){var o=a(\"Symbol\");return u(o)&&_(o.prototype,x(s))}},25767:(s,o,i)=>{\"use strict\";var a=i(82682),u=i(39209),_=i(10487),w=i(36556),x=i(75795),C=w(\"Object.prototype.toString\"),j=i(49092)(),L=\"undefined\"==typeof globalThis?i.g:globalThis,B=u(),$=w(\"String.prototype.slice\"),U=Object.getPrototypeOf,V=w(\"Array.prototype.indexOf\",!0)||function indexOf(s,o){for(var i=0;i<s.length;i+=1)if(s[i]===o)return i;return-1},z={__proto__:null};a(B,j&&x&&U?function(s){var o=new L[s];if(Symbol.toStringTag in o){var i=U(o),a=x(i,Symbol.toStringTag);if(!a){var u=U(i);a=x(u,Symbol.toStringTag)}z[\"$\"+s]=_(a.get)}}:function(s){var o=new L[s],i=o.slice||o.set;i&&(z[\"$\"+s]=_(i))});s.exports=function whichTypedArray(s){if(!s||\"object\"!=typeof s)return!1;if(!j){var o=$(C(s),8,-1);return V(B,o)>-1?o:\"Object\"===o&&function tryAllSlices(s){var o=!1;return a(z,(function(i,a){if(!o)try{i(s),o=$(a,1)}catch(s){}})),o}(s)}return x?function tryAllTypedArrays(s){var o=!1;return a(z,(function(i,a){if(!o)try{\"$\"+i(s)===a&&(o=$(a,1))}catch(s){}})),o}(s):null}},25911:(s,o,i)=>{var a=i(38859),u=i(14248),_=i(19219);s.exports=function equalArrays(s,o,i,w,x,C){var j=1&i,L=s.length,B=o.length;if(L!=B&&!(j&&B>L))return!1;var $=C.get(s),U=C.get(o);if($&&U)return $==o&&U==s;var V=-1,z=!0,Y=2&i?new a:void 0;for(C.set(s,o),C.set(o,s);++V<L;){var Z=s[V],ee=o[V];if(w)var ie=j?w(ee,Z,V,o,s,C):w(Z,ee,V,s,o,C);if(void 0!==ie){if(ie)continue;z=!1;break}if(Y){if(!u(o,(function(s,o){if(!_(Y,o)&&(Z===s||x(Z,s,i,w,C)))return Y.push(o)}))){z=!1;break}}else if(Z!==ee&&!x(Z,ee,i,w,C)){z=!1;break}}return C.delete(s),C.delete(o),z}},26025:(s,o,i)=>{var a=i(75288);s.exports=function assocIndexOf(s,o){for(var i=s.length;i--;)if(a(s[i][0],o))return i;return-1}},26311:s=>{!function(){var o;function format(s){for(var o,i,a,u,_=1,w=[].slice.call(arguments),x=0,C=s.length,j=\"\",L=!1,B=!1,nextArg=function(){return w[_++]},slurpNumber=function(){for(var i=\"\";/\\d/.test(s[x]);)i+=s[x++],o=s[x];return i.length>0?parseInt(i):null};x<C;++x)if(o=s[x],L)switch(L=!1,\".\"==o?(B=!1,o=s[++x]):\"0\"==o&&\".\"==s[x+1]?(B=!0,o=s[x+=2]):B=!0,u=slurpNumber(),o){case\"b\":j+=parseInt(nextArg(),10).toString(2);break;case\"c\":j+=\"string\"==typeof(i=nextArg())||i instanceof String?i:String.fromCharCode(parseInt(i,10));break;case\"d\":j+=parseInt(nextArg(),10);break;case\"f\":a=String(parseFloat(nextArg()).toFixed(u||6)),j+=B?a:a.replace(/^0/,\"\");break;case\"j\":j+=JSON.stringify(nextArg());break;case\"o\":j+=\"0\"+parseInt(nextArg(),10).toString(8);break;case\"s\":j+=nextArg();break;case\"x\":j+=\"0x\"+parseInt(nextArg(),10).toString(16);break;case\"X\":j+=\"0x\"+parseInt(nextArg(),10).toString(16).toUpperCase();break;default:j+=o}else\"%\"===o?L=!0:j+=o;return j}(o=s.exports=format).format=format,o.vsprintf=function vsprintf(s,o){return format.apply(null,[s].concat(o))},\"undefined\"!=typeof console&&\"function\"==typeof console.log&&(o.printf=function printf(){console.log(format.apply(null,arguments))})}()},26571:s=>{s.exports=function powershell(s){const o={$pattern:/-?[A-z\\.\\-]+\\b/,keyword:\"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter\",built_in:\"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write\"},i={begin:\"`[\\\\s\\\\S]\",relevance:0},a={className:\"variable\",variants:[{begin:/\\$\\B/},{className:\"keyword\",begin:/\\$this/},{begin:/\\$[\\w\\d][\\w\\d_:]*/}]},u={className:\"string\",variants:[{begin:/\"/,end:/\"/},{begin:/@\"/,end:/^\"@/}],contains:[i,a,{className:\"variable\",begin:/\\$[A-z]/,end:/[^A-z]/}]},_={className:\"string\",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},w=s.inherit(s.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:\"doctag\",variants:[{begin:/\\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\\s+\\S+/}]}]}),x={className:\"built_in\",variants:[{begin:\"(\".concat(\"Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where\",\")+(-)[\\\\w\\\\d]+\")}]},C={className:\"class\",beginKeywords:\"class enum\",end:/\\s*[{]/,excludeEnd:!0,relevance:0,contains:[s.TITLE_MODE]},j={className:\"function\",begin:/function\\s+/,end:/\\s*\\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:\"function\",relevance:0,className:\"keyword\"},{className:\"title\",begin:/\\w[\\w\\d]*((-)[\\w\\d]+)*/,relevance:0},{begin:/\\(/,end:/\\)/,className:\"params\",relevance:0,contains:[a]}]},L={begin:/using\\s/,end:/$/,returnBegin:!0,contains:[u,_,{className:\"keyword\",begin:/(using|assembly|command|module|namespace|type)/}]},B={variants:[{className:\"operator\",begin:\"(\".concat(\"-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor\",\")\\\\b\")},{className:\"literal\",begin:/(-)[\\w\\d]+/,relevance:0}]},$={className:\"function\",begin:/\\[.*\\]\\s*[\\w]+[ ]??\\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:\"keyword\",begin:\"(\".concat(o.keyword.toString().replace(/\\s/g,\"|\"),\")\\\\b\"),endsParent:!0,relevance:0},s.inherit(s.TITLE_MODE,{endsParent:!0})]},U=[$,w,i,s.NUMBER_MODE,u,_,x,a,{className:\"literal\",begin:/\\$(null|true|false)\\b/},{className:\"selector-tag\",begin:/@\\B/,relevance:0}],V={begin:/\\[/,end:/\\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat(\"self\",U,{begin:\"(\"+[\"string\",\"char\",\"byte\",\"int\",\"long\",\"bool\",\"decimal\",\"single\",\"double\",\"DateTime\",\"xml\",\"array\",\"hashtable\",\"void\"].join(\"|\")+\")\",className:\"built_in\",relevance:0},{className:\"type\",begin:/[\\.\\w\\d]+/,relevance:0})};return $.contains.unshift(V),{name:\"PowerShell\",aliases:[\"ps\",\"ps1\"],case_insensitive:!0,keywords:o,contains:U.concat(C,j,L,B,V)}}},26657:(s,o,i)=>{\"use strict\";var a=i(75208),u=function isClosingTag(s){return/<\\/+[^>]+>/.test(s)},_=function isSelfClosingTag(s){return/<[^>]+\\/>/.test(s)};function getType(s){return u(s)?\"ClosingTag\":function isOpeningTag(s){return function isTag(s){return/<[^>!]+>/.test(s)}(s)&&!u(s)&&!_(s)}(s)?\"OpeningTag\":_(s)?\"SelfClosingTag\":\"Text\"}s.exports=function(s){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=o.indentor,u=o.textNodesOnSameLine,_=0,w=[];i=i||\"    \";var x=function lexer(s){return function splitOnTags(s){return s.split(/(<\\/?[^>]+>)/g).filter((function(s){return\"\"!==s.trim()}))}(s).map((function(s){return{value:s,type:getType(s)}}))}(s).map((function(s,o,x){var C=s.value,j=s.type;\"ClosingTag\"===j&&_--;var L=a(i,_),B=L+C;if(\"OpeningTag\"===j&&_++,u){var $=x[o-1],U=x[o-2];\"ClosingTag\"===j&&\"Text\"===$.type&&\"OpeningTag\"===U.type&&(B=\"\"+L+U.value+$.value+C,w.push(o-2,o-1))}return B}));return w.forEach((function(s){return x[s]=null})),x.filter((function(s){return!!s})).join(\"\\n\")}},26710:(s,o,i)=>{\"use strict\";var a=i(56698),u=i(24107),_=i(90392),w=i(92861).Buffer,x=new Array(64);function Sha224(){this.init(),this._w=x,_.call(this,64,56)}a(Sha224,u),Sha224.prototype.init=function(){return this._a=3238371032,this._b=914150663,this._c=812702999,this._d=4144912697,this._e=4290775857,this._f=1750603025,this._g=1694076839,this._h=3204075428,this},Sha224.prototype._hash=function(){var s=w.allocUnsafe(28);return s.writeInt32BE(this._a,0),s.writeInt32BE(this._b,4),s.writeInt32BE(this._c,8),s.writeInt32BE(this._d,12),s.writeInt32BE(this._e,16),s.writeInt32BE(this._f,20),s.writeInt32BE(this._g,24),s},s.exports=Sha224},27096:(s,o,i)=>{const a=i(87586),u=i(6205),_=i(10023),w=i(8048);s.exports=s=>{var o,i,x=0,C={type:u.ROOT,stack:[]},j=C,L=C.stack,B=[],repeatErr=o=>{a.error(s,\"Nothing to repeat at column \"+(o-1))},$=a.strToChars(s);for(o=$.length;x<o;)switch(i=$[x++]){case\"\\\\\":switch(i=$[x++]){case\"b\":L.push(w.wordBoundary());break;case\"B\":L.push(w.nonWordBoundary());break;case\"w\":L.push(_.words());break;case\"W\":L.push(_.notWords());break;case\"d\":L.push(_.ints());break;case\"D\":L.push(_.notInts());break;case\"s\":L.push(_.whitespace());break;case\"S\":L.push(_.notWhitespace());break;default:/\\d/.test(i)?L.push({type:u.REFERENCE,value:parseInt(i,10)}):L.push({type:u.CHAR,value:i.charCodeAt(0)})}break;case\"^\":L.push(w.begin());break;case\"$\":L.push(w.end());break;case\"[\":var U;\"^\"===$[x]?(U=!0,x++):U=!1;var V=a.tokenizeClass($.slice(x),s);x+=V[1],L.push({type:u.SET,set:V[0],not:U});break;case\".\":L.push(_.anyChar());break;case\"(\":var z={type:u.GROUP,stack:[],remember:!0};\"?\"===(i=$[x])&&(i=$[x+1],x+=2,\"=\"===i?z.followedBy=!0:\"!\"===i?z.notFollowedBy=!0:\":\"!==i&&a.error(s,`Invalid group, character '${i}' after '?' at column `+(x-1)),z.remember=!1),L.push(z),B.push(j),j=z,L=z.stack;break;case\")\":0===B.length&&a.error(s,\"Unmatched ) at column \"+(x-1)),L=(j=B.pop()).options?j.options[j.options.length-1]:j.stack;break;case\"|\":j.options||(j.options=[j.stack],delete j.stack);var Y=[];j.options.push(Y),L=Y;break;case\"{\":var Z,ee,ie=/^(\\d+)(,(\\d+)?)?\\}/.exec($.slice(x));null!==ie?(0===L.length&&repeatErr(x),Z=parseInt(ie[1],10),ee=ie[2]?ie[3]?parseInt(ie[3],10):1/0:Z,x+=ie[0].length,L.push({type:u.REPETITION,min:Z,max:ee,value:L.pop()})):L.push({type:u.CHAR,value:123});break;case\"?\":0===L.length&&repeatErr(x),L.push({type:u.REPETITION,min:0,max:1,value:L.pop()});break;case\"+\":0===L.length&&repeatErr(x),L.push({type:u.REPETITION,min:1,max:1/0,value:L.pop()});break;case\"*\":0===L.length&&repeatErr(x),L.push({type:u.REPETITION,min:0,max:1/0,value:L.pop()});break;default:L.push({type:u.CHAR,value:i.charCodeAt(0)})}return 0!==B.length&&a.error(s,\"Unterminated group\"),C},s.exports.types=u},27301:s=>{s.exports=function baseUnary(s){return function(o){return s(o)}}},27374:(s,o)=>{\"use strict\";Object.defineProperty(o,\"__esModule\",{value:!0}),o.default=function(s,o,i){if(void 0===s)throw new Error('Reducer \"'+o+'\" returned undefined when handling \"'+i.type+'\" action. To ignore an action, you must explicitly return the previous state.')},s.exports=o.default},27534:(s,o,i)=>{var a=i(72552),u=i(40346);s.exports=function baseIsArguments(s){return u(s)&&\"[object Arguments]\"==a(s)}},27816:(s,o,i)=>{\"use strict\";var a=i(56698),u=i(90392),_=i(92861).Buffer,w=[1518500249,1859775393,-1894007588,-899497514],x=new Array(80);function Sha(){this.init(),this._w=x,u.call(this,64,56)}function rotl30(s){return s<<30|s>>>2}function ft(s,o,i,a){return 0===s?o&i|~o&a:2===s?o&i|o&a|i&a:o^i^a}a(Sha,u),Sha.prototype.init=function(){return this._a=1732584193,this._b=4023233417,this._c=2562383102,this._d=271733878,this._e=3285377520,this},Sha.prototype._update=function(s){for(var o,i=this._w,a=0|this._a,u=0|this._b,_=0|this._c,x=0|this._d,C=0|this._e,j=0;j<16;++j)i[j]=s.readInt32BE(4*j);for(;j<80;++j)i[j]=i[j-3]^i[j-8]^i[j-14]^i[j-16];for(var L=0;L<80;++L){var B=~~(L/20),$=0|((o=a)<<5|o>>>27)+ft(B,u,_,x)+C+i[L]+w[B];C=x,x=_,_=rotl30(u),u=a,a=$}this._a=a+this._a|0,this._b=u+this._b|0,this._c=_+this._c|0,this._d=x+this._d|0,this._e=C+this._e|0},Sha.prototype._hash=function(){var s=_.allocUnsafe(20);return s.writeInt32BE(0|this._a,0),s.writeInt32BE(0|this._b,4),s.writeInt32BE(0|this._c,8),s.writeInt32BE(0|this._d,12),s.writeInt32BE(0|this._e,16),s},s.exports=Sha},28077:s=>{s.exports=function baseHasIn(s,o){return null!=s&&o in Object(s)}},28303:(s,o,i)=>{var a=i(56110)(i(9325),\"WeakMap\");s.exports=a},28311:(s,o,i)=>{\"use strict\";var a=i(92361),u=i(82159),_=i(41505),w=a(a.bind);s.exports=function(s,o){return u(s),void 0===o?s:_?w(s,o):function(){return s.apply(o,arguments)}}},28586:(s,o,i)=>{var a=i(56449),u=i(44394),_=/\\.|\\[(?:[^[\\]]*|([\"'])(?:(?!\\1)[^\\\\]|\\\\.)*?\\1)\\]/,w=/^\\w*$/;s.exports=function isKey(s,o){if(a(s))return!1;var i=typeof s;return!(\"number\"!=i&&\"symbol\"!=i&&\"boolean\"!=i&&null!=s&&!u(s))||(w.test(s)||!_.test(s)||null!=o&&s in Object(o))}},28754:(s,o,i)=>{var a=i(25160);s.exports=function castSlice(s,o,i){var u=s.length;return i=void 0===i?u:i,!o&&i>=u?s:a(s,o,i)}},28879:(s,o,i)=>{var a=i(74335)(Object.getPrototypeOf,Object);s.exports=a},29172:(s,o,i)=>{var a=i(5861),u=i(40346);s.exports=function baseIsMap(s){return u(s)&&\"[object Map]\"==a(s)}},29367:(s,o,i)=>{\"use strict\";var a=i(82159),u=i(87136);s.exports=function(s,o){var i=s[o];return u(i)?void 0:a(i)}},29538:(s,o,i)=>{\"use strict\";var a=i(39447),u=i(1907),_=i(13930),w=i(98828),x=i(2875),C=i(87170),j=i(22574),L=i(39298),B=i(16946),$=Object.assign,U=Object.defineProperty,V=u([].concat);s.exports=!$||w((function(){if(a&&1!==$({b:1},$(U({},\"a\",{enumerable:!0,get:function(){U(this,\"b\",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var s={},o={},i=Symbol(\"assign detection\"),u=\"abcdefghijklmnopqrst\";return s[i]=7,u.split(\"\").forEach((function(s){o[s]=s})),7!==$({},s)[i]||x($({},o)).join(\"\")!==u}))?function assign(s,o){for(var i=L(s),u=arguments.length,w=1,$=C.f,U=j.f;u>w;)for(var z,Y=B(arguments[w++]),Z=$?V(x(Y),$(Y)):x(Y),ee=Z.length,ie=0;ee>ie;)z=Z[ie++],a&&!_(U,Y,z)||(i[z]=Y[z]);return i}:$},29817:s=>{s.exports=function stackHas(s){return this.__data__.has(s)}},29844:(s,o)=>{\"use strict\";function f(s,o){var i=s.length;s.push(o);e:for(;0<i;){var a=i-1>>>1,u=s[a];if(!(0<g(u,o)))break e;s[a]=o,s[i]=u,i=a}}function h(s){return 0===s.length?null:s[0]}function k(s){if(0===s.length)return null;var o=s[0],i=s.pop();if(i!==o){s[0]=i;e:for(var a=0,u=s.length,_=u>>>1;a<_;){var w=2*(a+1)-1,x=s[w],C=w+1,j=s[C];if(0>g(x,i))C<u&&0>g(j,x)?(s[a]=j,s[C]=i,a=C):(s[a]=x,s[w]=i,a=w);else{if(!(C<u&&0>g(j,i)))break e;s[a]=j,s[C]=i,a=C}}}return o}function g(s,o){var i=s.sortIndex-o.sortIndex;return 0!==i?i:s.id-o.id}if(\"object\"==typeof performance&&\"function\"==typeof performance.now){var i=performance;o.unstable_now=function(){return i.now()}}else{var a=Date,u=a.now();o.unstable_now=function(){return a.now()-u}}var _=[],w=[],x=1,C=null,j=3,L=!1,B=!1,$=!1,U=\"function\"==typeof setTimeout?setTimeout:null,V=\"function\"==typeof clearTimeout?clearTimeout:null,z=\"undefined\"!=typeof setImmediate?setImmediate:null;function G(s){for(var o=h(w);null!==o;){if(null===o.callback)k(w);else{if(!(o.startTime<=s))break;k(w),o.sortIndex=o.expirationTime,f(_,o)}o=h(w)}}function H(s){if($=!1,G(s),!B)if(null!==h(_))B=!0,I(J);else{var o=h(w);null!==o&&K(H,o.startTime-s)}}function J(s,i){B=!1,$&&($=!1,V(ie),ie=-1),L=!0;var a=j;try{for(G(i),C=h(_);null!==C&&(!(C.expirationTime>i)||s&&!M());){var u=C.callback;if(\"function\"==typeof u){C.callback=null,j=C.priorityLevel;var x=u(C.expirationTime<=i);i=o.unstable_now(),\"function\"==typeof x?C.callback=x:C===h(_)&&k(_),G(i)}else k(_);C=h(_)}if(null!==C)var U=!0;else{var z=h(w);null!==z&&K(H,z.startTime-i),U=!1}return U}finally{C=null,j=a,L=!1}}\"undefined\"!=typeof navigator&&void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var Y,Z=!1,ee=null,ie=-1,ae=5,ce=-1;function M(){return!(o.unstable_now()-ce<ae)}function R(){if(null!==ee){var s=o.unstable_now();ce=s;var i=!0;try{i=ee(!0,s)}finally{i?Y():(Z=!1,ee=null)}}else Z=!1}if(\"function\"==typeof z)Y=function(){z(R)};else if(\"undefined\"!=typeof MessageChannel){var le=new MessageChannel,pe=le.port2;le.port1.onmessage=R,Y=function(){pe.postMessage(null)}}else Y=function(){U(R,0)};function I(s){ee=s,Z||(Z=!0,Y())}function K(s,i){ie=U((function(){s(o.unstable_now())}),i)}o.unstable_IdlePriority=5,o.unstable_ImmediatePriority=1,o.unstable_LowPriority=4,o.unstable_NormalPriority=3,o.unstable_Profiling=null,o.unstable_UserBlockingPriority=2,o.unstable_cancelCallback=function(s){s.callback=null},o.unstable_continueExecution=function(){B||L||(B=!0,I(J))},o.unstable_forceFrameRate=function(s){0>s||125<s?console.error(\"forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported\"):ae=0<s?Math.floor(1e3/s):5},o.unstable_getCurrentPriorityLevel=function(){return j},o.unstable_getFirstCallbackNode=function(){return h(_)},o.unstable_next=function(s){switch(j){case 1:case 2:case 3:var o=3;break;default:o=j}var i=j;j=o;try{return s()}finally{j=i}},o.unstable_pauseExecution=function(){},o.unstable_requestPaint=function(){},o.unstable_runWithPriority=function(s,o){switch(s){case 1:case 2:case 3:case 4:case 5:break;default:s=3}var i=j;j=s;try{return o()}finally{j=i}},o.unstable_scheduleCallback=function(s,i,a){var u=o.unstable_now();switch(\"object\"==typeof a&&null!==a?a=\"number\"==typeof(a=a.delay)&&0<a?u+a:u:a=u,s){case 1:var C=-1;break;case 2:C=250;break;case 5:C=1073741823;break;case 4:C=1e4;break;default:C=5e3}return s={id:x++,callback:i,priorityLevel:s,startTime:a,expirationTime:C=a+C,sortIndex:-1},a>u?(s.sortIndex=a,f(w,s),null===h(_)&&s===h(w)&&($?(V(ie),ie=-1):$=!0,K(H,a-u))):(s.sortIndex=C,f(_,s),B||L||(B=!0,I(J))),s},o.unstable_shouldYield=M,o.unstable_wrapCallback=function(s){var o=j;return function(){var i=j;j=o;try{return s.apply(this,arguments)}finally{j=i}}}},30041:(s,o,i)=>{\"use strict\";var a=i(30655),u=i(58068),_=i(69675),w=i(75795);s.exports=function defineDataProperty(s,o,i){if(!s||\"object\"!=typeof s&&\"function\"!=typeof s)throw new _(\"`obj` must be an object or a function`\");if(\"string\"!=typeof o&&\"symbol\"!=typeof o)throw new _(\"`property` must be a string or a symbol`\");if(arguments.length>3&&\"boolean\"!=typeof arguments[3]&&null!==arguments[3])throw new _(\"`nonEnumerable`, if provided, must be a boolean or null\");if(arguments.length>4&&\"boolean\"!=typeof arguments[4]&&null!==arguments[4])throw new _(\"`nonWritable`, if provided, must be a boolean or null\");if(arguments.length>5&&\"boolean\"!=typeof arguments[5]&&null!==arguments[5])throw new _(\"`nonConfigurable`, if provided, must be a boolean or null\");if(arguments.length>6&&\"boolean\"!=typeof arguments[6])throw new _(\"`loose`, if provided, must be a boolean\");var x=arguments.length>3?arguments[3]:null,C=arguments.length>4?arguments[4]:null,j=arguments.length>5?arguments[5]:null,L=arguments.length>6&&arguments[6],B=!!w&&w(s,o);if(a)a(s,o,{configurable:null===j&&B?B.configurable:!j,enumerable:null===x&&B?B.enumerable:!x,value:i,writable:null===C&&B?B.writable:!C});else{if(!L&&(x||C||j))throw new u(\"This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.\");s[o]=i}}},30294:s=>{s.exports=function isLength(s){return\"number\"==typeof s&&s>-1&&s%1==0&&s<=9007199254740991}},30361:s=>{var o=/^(?:0|[1-9]\\d*)$/;s.exports=function isIndex(s,i){var a=typeof s;return!!(i=null==i?9007199254740991:i)&&(\"number\"==a||\"symbol\"!=a&&o.test(s))&&s>-1&&s%1==0&&s<i}},30592:(s,o,i)=>{\"use strict\";var a=i(30655),u=function hasPropertyDescriptors(){return!!a};u.hasArrayLengthDefineBug=function hasArrayLengthDefineBug(){if(!a)return null;try{return 1!==a([],\"length\",{value:1}).length}catch(s){return!0}},s.exports=u},30641:(s,o,i)=>{var a=i(86649),u=i(95950);s.exports=function baseForOwn(s,o){return s&&a(s,o,u)}},30655:s=>{\"use strict\";var o=Object.defineProperty||!1;if(o)try{o({},\"a\",{value:1})}catch(s){o=!1}s.exports=o},30756:(s,o,i)=>{var a=i(23805);s.exports=function isStrictComparable(s){return s==s&&!a(s)}},30980:(s,o,i)=>{var a=i(39344),u=i(94033);function LazyWrapper(s){this.__wrapped__=s,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=4294967295,this.__views__=[]}LazyWrapper.prototype=a(u.prototype),LazyWrapper.prototype.constructor=LazyWrapper,s.exports=LazyWrapper},31175:(s,o,i)=>{var a=i(26025);s.exports=function listCacheSet(s,o){var i=this.__data__,u=a(i,s);return u<0?(++this.size,i.push([s,o])):i[u][1]=o,this}},31380:s=>{s.exports=function setCacheAdd(s){return this.__data__.set(s,\"__lodash_hash_undefined__\"),this}},31499:s=>{var o={\"&\":\"&amp;\",'\"':\"&quot;\",\"'\":\"&apos;\",\"<\":\"&lt;\",\">\":\"&gt;\"};s.exports=function escapeForXML(s){return s&&s.replace?s.replace(/([&\"<>'])/g,(function(s,i){return o[i]})):s}},31769:(s,o,i)=>{var a=i(56449),u=i(28586),_=i(61802),w=i(13222);s.exports=function castPath(s,o){return a(s)?s:u(s,o)?[s]:_(w(s))}},31800:s=>{var o=/\\s/;s.exports=function trimmedEndIndex(s){for(var i=s.length;i--&&o.test(s.charAt(i)););return i}},32096:(s,o,i)=>{\"use strict\";var a=i(90160);s.exports=function(s,o){return void 0===s?arguments.length<2?\"\":o:a(s)}},32567:(s,o,i)=>{\"use strict\";i(79307);var a=i(61747);s.exports=a(\"Function\",\"bind\")},32629:(s,o,i)=>{var a=i(9999);s.exports=function clone(s){return a(s,4)}},32804:(s,o,i)=>{var a=i(56110)(i(9325),\"Promise\");s.exports=a},32827:(s,o,i)=>{\"use strict\";var a=i(56698),u=i(82890),_=i(90392),w=i(92861).Buffer,x=new Array(160);function Sha384(){this.init(),this._w=x,_.call(this,128,112)}a(Sha384,u),Sha384.prototype.init=function(){return this._ah=3418070365,this._bh=1654270250,this._ch=2438529370,this._dh=355462360,this._eh=1731405415,this._fh=2394180231,this._gh=3675008525,this._hh=1203062813,this._al=3238371032,this._bl=914150663,this._cl=812702999,this._dl=4144912697,this._el=4290775857,this._fl=1750603025,this._gl=1694076839,this._hl=3204075428,this},Sha384.prototype._hash=function(){var s=w.allocUnsafe(48);function writeInt64BE(o,i,a){s.writeInt32BE(o,a),s.writeInt32BE(i,a+4)}return writeInt64BE(this._ah,this._al,0),writeInt64BE(this._bh,this._bl,8),writeInt64BE(this._ch,this._cl,16),writeInt64BE(this._dh,this._dl,24),writeInt64BE(this._eh,this._el,32),writeInt64BE(this._fh,this._fl,40),s},s.exports=Sha384},32865:(s,o,i)=>{var a=i(19570),u=i(51811)(a);s.exports=u},33855:(s,o,i)=>{var a=i(9999),u=i(15389);s.exports=function iteratee(s){return u(\"function\"==typeof s?s:a(s,1))}},34035:(s,o,i)=>{const a=i(3110),u=i(86804);o.g$=a,o.KeyValuePair=i(55973),o.G6=u.ArraySlice,o.ot=u.ObjectSlice,o.Hg=u.Element,o.Om=u.StringElement,o.kT=u.NumberElement,o.bd=u.BooleanElement,o.Os=u.NullElement,o.wE=u.ArrayElement,o.Sh=u.ObjectElement,o.Pr=u.MemberElement,o.sI=u.RefElement,o.Ft=u.LinkElement,o.e=u.refract,i(85105),i(75147)},34084:(s,o,i)=>{\"use strict\";var a=i(62250),u=i(46285),_=i(79192);s.exports=function(s,o,i){var w,x;return _&&a(w=o.constructor)&&w!==i&&u(x=w.prototype)&&x!==i.prototype&&_(s,x),s}},34840:(s,o,i)=>{var a=\"object\"==typeof i.g&&i.g&&i.g.Object===Object&&i.g;s.exports=a},34849:(s,o,i)=>{\"use strict\";var a=i(65482),u=Math.max,_=Math.min;s.exports=function(s,o){var i=a(s);return i<0?u(i+o,0):_(i,o)}},34932:s=>{s.exports=function arrayMap(s,o){for(var i=-1,a=null==s?0:s.length,u=Array(a);++i<a;)u[i]=o(s[i],i,s);return u}},35344:s=>{function concat(...s){return s.map((s=>function source(s){return s?\"string\"==typeof s?s:s.source:null}(s))).join(\"\")}s.exports=function bash(s){const o={},i={begin:/\\$\\{/,end:/\\}/,contains:[\"self\",{begin:/:-/,contains:[o]}]};Object.assign(o,{className:\"variable\",variants:[{begin:concat(/\\$[\\w\\d#@][\\w\\d_]*/,\"(?![\\\\w\\\\d])(?![$])\")},i]});const a={className:\"subst\",begin:/\\$\\(/,end:/\\)/,contains:[s.BACKSLASH_ESCAPE]},u={begin:/<<-?\\s*(?=\\w+)/,starts:{contains:[s.END_SAME_AS_BEGIN({begin:/(\\w+)/,end:/(\\w+)/,className:\"string\"})]}},_={className:\"string\",begin:/\"/,end:/\"/,contains:[s.BACKSLASH_ESCAPE,o,a]};a.contains.push(_);const w={begin:/\\$\\(\\(/,end:/\\)\\)/,contains:[{begin:/\\d+#[0-9a-f]+/,className:\"number\"},s.NUMBER_MODE,o]},x=s.SHEBANG({binary:`(${[\"fish\",\"bash\",\"zsh\",\"sh\",\"csh\",\"ksh\",\"tcsh\",\"dash\",\"scsh\"].join(\"|\")})`,relevance:10}),C={className:\"function\",begin:/\\w[\\w\\d_]*\\s*\\(\\s*\\)\\s*\\{/,returnBegin:!0,contains:[s.inherit(s.TITLE_MODE,{begin:/\\w[\\w\\d_]*/})],relevance:0};return{name:\"Bash\",aliases:[\"sh\",\"zsh\"],keywords:{$pattern:/\\b[a-z._-]+\\b/,keyword:\"if then else elif fi for while in do done case esac function\",literal:\"true false\",built_in:\"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp\"},contains:[x,s.SHEBANG(),C,w,s.HASH_COMMENT_MODE,u,_,{className:\"\",begin:/\\\\\"/},{className:\"string\",begin:/'/,end:/'/},o]}}},35345:s=>{\"use strict\";s.exports=URIError},35529:(s,o,i)=>{var a=i(39344),u=i(28879),_=i(55527);s.exports=function initCloneObject(s){return\"function\"!=typeof s.constructor||_(s)?{}:a(u(s))}},35680:(s,o,i)=>{\"use strict\";var a=i(25767);s.exports=function isTypedArray(s){return!!a(s)}},35749:(s,o,i)=>{var a=i(81042);s.exports=function hashSet(s,o){var i=this.__data__;return this.size+=this.has(s)?0:1,i[s]=a&&void 0===o?\"__lodash_hash_undefined__\":o,this}},35970:(s,o,i)=>{var a=i(83120);s.exports=function flatten(s){return(null==s?0:s.length)?a(s,1):[]}},36128:(s,o,i)=>{\"use strict\";var a=i(7376),u=i(45951),_=i(2532),w=\"__core-js_shared__\",x=s.exports=u[w]||_(w,{});(x.versions||(x.versions=[])).push({version:\"3.40.0\",mode:a?\"pure\":\"global\",copyright:\"© 2014-2025 Denis Pushkarev (zloirock.ru)\",license:\"https://github.com/zloirock/core-js/blob/v3.40.0/LICENSE\",source:\"https://github.com/zloirock/core-js\"})},36306:s=>{var o=\"__lodash_placeholder__\";s.exports=function replaceHolders(s,i){for(var a=-1,u=s.length,_=0,w=[];++a<u;){var x=s[a];x!==i&&x!==o||(s[a]=o,w[_++]=a)}return w}},36371:(s,o,i)=>{\"use strict\";var a=i(11091),u=i(85582),_=i(76024),w=i(98828),x=i(19358),C=\"AggregateError\",j=u(C),L=!w((function(){return 1!==j([1]).errors[0]}))&&w((function(){return 7!==j([1],C,{cause:7}).cause}));a({global:!0,constructor:!0,arity:2,forced:L},{AggregateError:x(C,(function(s){return function AggregateError(o,i){return _(s,this,arguments)}}),L,!0)})},36556:(s,o,i)=>{\"use strict\";var a=i(70453),u=i(73126),_=u([a(\"%String.prototype.indexOf%\")]);s.exports=function callBoundIntrinsic(s,o){var i=a(s,!!o);return\"function\"==typeof i&&_(s,\".prototype.\")>-1?u([i]):i}},36624:(s,o,i)=>{\"use strict\";var a=i(46285),u=String,_=TypeError;s.exports=function(s){if(a(s))return s;throw new _(u(s)+\" is not an object\")}},36800:(s,o,i)=>{var a=i(75288),u=i(64894),_=i(30361),w=i(23805);s.exports=function isIterateeCall(s,o,i){if(!w(i))return!1;var x=typeof o;return!!(\"number\"==x?u(i)&&_(o,i.length):\"string\"==x&&o in i)&&a(i[o],s)}},36833:(s,o,i)=>{\"use strict\";var a=i(39447),u=i(49724),_=Function.prototype,w=a&&Object.getOwnPropertyDescriptor,x=u(_,\"name\"),C=x&&\"something\"===function something(){}.name,j=x&&(!a||a&&w(_,\"name\").configurable);s.exports={EXISTS:x,PROPER:C,CONFIGURABLE:j}},37007:s=>{\"use strict\";var o,i=\"object\"==typeof Reflect?Reflect:null,a=i&&\"function\"==typeof i.apply?i.apply:function ReflectApply(s,o,i){return Function.prototype.apply.call(s,o,i)};o=i&&\"function\"==typeof i.ownKeys?i.ownKeys:Object.getOwnPropertySymbols?function ReflectOwnKeys(s){return Object.getOwnPropertyNames(s).concat(Object.getOwnPropertySymbols(s))}:function ReflectOwnKeys(s){return Object.getOwnPropertyNames(s)};var u=Number.isNaN||function NumberIsNaN(s){return s!=s};function EventEmitter(){EventEmitter.init.call(this)}s.exports=EventEmitter,s.exports.once=function once(s,o){return new Promise((function(i,a){function errorListener(i){s.removeListener(o,resolver),a(i)}function resolver(){\"function\"==typeof s.removeListener&&s.removeListener(\"error\",errorListener),i([].slice.call(arguments))}eventTargetAgnosticAddListener(s,o,resolver,{once:!0}),\"error\"!==o&&function addErrorHandlerIfEventEmitter(s,o,i){\"function\"==typeof s.on&&eventTargetAgnosticAddListener(s,\"error\",o,i)}(s,errorListener,{once:!0})}))},EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var _=10;function checkListener(s){if(\"function\"!=typeof s)throw new TypeError('The \"listener\" argument must be of type Function. Received type '+typeof s)}function _getMaxListeners(s){return void 0===s._maxListeners?EventEmitter.defaultMaxListeners:s._maxListeners}function _addListener(s,o,i,a){var u,_,w;if(checkListener(i),void 0===(_=s._events)?(_=s._events=Object.create(null),s._eventsCount=0):(void 0!==_.newListener&&(s.emit(\"newListener\",o,i.listener?i.listener:i),_=s._events),w=_[o]),void 0===w)w=_[o]=i,++s._eventsCount;else if(\"function\"==typeof w?w=_[o]=a?[i,w]:[w,i]:a?w.unshift(i):w.push(i),(u=_getMaxListeners(s))>0&&w.length>u&&!w.warned){w.warned=!0;var x=new Error(\"Possible EventEmitter memory leak detected. \"+w.length+\" \"+String(o)+\" listeners added. Use emitter.setMaxListeners() to increase limit\");x.name=\"MaxListenersExceededWarning\",x.emitter=s,x.type=o,x.count=w.length,function ProcessEmitWarning(s){console&&console.warn&&console.warn(s)}(x)}return s}function onceWrapper(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function _onceWrap(s,o,i){var a={fired:!1,wrapFn:void 0,target:s,type:o,listener:i},u=onceWrapper.bind(a);return u.listener=i,a.wrapFn=u,u}function _listeners(s,o,i){var a=s._events;if(void 0===a)return[];var u=a[o];return void 0===u?[]:\"function\"==typeof u?i?[u.listener||u]:[u]:i?function unwrapListeners(s){for(var o=new Array(s.length),i=0;i<o.length;++i)o[i]=s[i].listener||s[i];return o}(u):arrayClone(u,u.length)}function listenerCount(s){var o=this._events;if(void 0!==o){var i=o[s];if(\"function\"==typeof i)return 1;if(void 0!==i)return i.length}return 0}function arrayClone(s,o){for(var i=new Array(o),a=0;a<o;++a)i[a]=s[a];return i}function eventTargetAgnosticAddListener(s,o,i,a){if(\"function\"==typeof s.on)a.once?s.once(o,i):s.on(o,i);else{if(\"function\"!=typeof s.addEventListener)throw new TypeError('The \"emitter\" argument must be of type EventEmitter. Received type '+typeof s);s.addEventListener(o,(function wrapListener(u){a.once&&s.removeEventListener(o,wrapListener),i(u)}))}}Object.defineProperty(EventEmitter,\"defaultMaxListeners\",{enumerable:!0,get:function(){return _},set:function(s){if(\"number\"!=typeof s||s<0||u(s))throw new RangeError('The value of \"defaultMaxListeners\" is out of range. It must be a non-negative number. Received '+s+\".\");_=s}}),EventEmitter.init=function(){void 0!==this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},EventEmitter.prototype.setMaxListeners=function setMaxListeners(s){if(\"number\"!=typeof s||s<0||u(s))throw new RangeError('The value of \"n\" is out of range. It must be a non-negative number. Received '+s+\".\");return this._maxListeners=s,this},EventEmitter.prototype.getMaxListeners=function getMaxListeners(){return _getMaxListeners(this)},EventEmitter.prototype.emit=function emit(s){for(var o=[],i=1;i<arguments.length;i++)o.push(arguments[i]);var u=\"error\"===s,_=this._events;if(void 0!==_)u=u&&void 0===_.error;else if(!u)return!1;if(u){var w;if(o.length>0&&(w=o[0]),w instanceof Error)throw w;var x=new Error(\"Unhandled error.\"+(w?\" (\"+w.message+\")\":\"\"));throw x.context=w,x}var C=_[s];if(void 0===C)return!1;if(\"function\"==typeof C)a(C,this,o);else{var j=C.length,L=arrayClone(C,j);for(i=0;i<j;++i)a(L[i],this,o)}return!0},EventEmitter.prototype.addListener=function addListener(s,o){return _addListener(this,s,o,!1)},EventEmitter.prototype.on=EventEmitter.prototype.addListener,EventEmitter.prototype.prependListener=function prependListener(s,o){return _addListener(this,s,o,!0)},EventEmitter.prototype.once=function once(s,o){return checkListener(o),this.on(s,_onceWrap(this,s,o)),this},EventEmitter.prototype.prependOnceListener=function prependOnceListener(s,o){return checkListener(o),this.prependListener(s,_onceWrap(this,s,o)),this},EventEmitter.prototype.removeListener=function removeListener(s,o){var i,a,u,_,w;if(checkListener(o),void 0===(a=this._events))return this;if(void 0===(i=a[s]))return this;if(i===o||i.listener===o)0==--this._eventsCount?this._events=Object.create(null):(delete a[s],a.removeListener&&this.emit(\"removeListener\",s,i.listener||o));else if(\"function\"!=typeof i){for(u=-1,_=i.length-1;_>=0;_--)if(i[_]===o||i[_].listener===o){w=i[_].listener,u=_;break}if(u<0)return this;0===u?i.shift():function spliceOne(s,o){for(;o+1<s.length;o++)s[o]=s[o+1];s.pop()}(i,u),1===i.length&&(a[s]=i[0]),void 0!==a.removeListener&&this.emit(\"removeListener\",s,w||o)}return this},EventEmitter.prototype.off=EventEmitter.prototype.removeListener,EventEmitter.prototype.removeAllListeners=function removeAllListeners(s){var o,i,a;if(void 0===(i=this._events))return this;if(void 0===i.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==i[s]&&(0==--this._eventsCount?this._events=Object.create(null):delete i[s]),this;if(0===arguments.length){var u,_=Object.keys(i);for(a=0;a<_.length;++a)\"removeListener\"!==(u=_[a])&&this.removeAllListeners(u);return this.removeAllListeners(\"removeListener\"),this._events=Object.create(null),this._eventsCount=0,this}if(\"function\"==typeof(o=i[s]))this.removeListener(s,o);else if(void 0!==o)for(a=o.length-1;a>=0;a--)this.removeListener(s,o[a]);return this},EventEmitter.prototype.listeners=function listeners(s){return _listeners(this,s,!0)},EventEmitter.prototype.rawListeners=function rawListeners(s){return _listeners(this,s,!1)},EventEmitter.listenerCount=function(s,o){return\"function\"==typeof s.listenerCount?s.listenerCount(o):listenerCount.call(s,o)},EventEmitter.prototype.listenerCount=listenerCount,EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?o(this._events):[]}},37167:(s,o,i)=>{var a=i(4901),u=i(27301),_=i(86009),w=_&&_.isTypedArray,x=w?u(w):a;s.exports=x},37217:(s,o,i)=>{var a=i(80079),u=i(51420),_=i(90938),w=i(63605),x=i(29817),C=i(80945);function Stack(s){var o=this.__data__=new a(s);this.size=o.size}Stack.prototype.clear=u,Stack.prototype.delete=_,Stack.prototype.get=w,Stack.prototype.has=x,Stack.prototype.set=C,s.exports=Stack},37241:(s,o,i)=>{var a=i(70695),u=i(72903),_=i(64894);s.exports=function keysIn(s){return _(s)?a(s,!0):u(s)}},37257:(s,o,i)=>{\"use strict\";i(96605),i(64502),i(36371),i(99363),i(7057);var a=i(92046);s.exports=a.AggregateError},37334:s=>{s.exports=function constant(s){return function(){return s}}},37381:(s,o,i)=>{var a=i(48152),u=i(63950),_=a?function(s){return a.get(s)}:u;s.exports=_},37471:(s,o,i)=>{var a=i(91596),u=i(53320),_=i(58523),w=i(82819),x=i(18073),C=i(11287),j=i(68294),L=i(36306),B=i(9325);s.exports=function createHybrid(s,o,i,$,U,V,z,Y,Z,ee){var ie=128&o,ae=1&o,ce=2&o,le=24&o,pe=512&o,de=ce?void 0:w(s);return function wrapper(){for(var fe=arguments.length,ye=Array(fe),be=fe;be--;)ye[be]=arguments[be];if(le)var _e=C(wrapper),Se=_(ye,_e);if($&&(ye=a(ye,$,U,le)),V&&(ye=u(ye,V,z,le)),fe-=Se,le&&fe<ee){var we=L(ye,_e);return x(s,o,createHybrid,wrapper.placeholder,i,ye,we,Y,Z,ee-fe)}var xe=ae?i:this,Pe=ce?xe[s]:s;return fe=ye.length,Y?ye=j(ye,Y):pe&&fe>1&&ye.reverse(),ie&&Z<fe&&(ye.length=Z),this&&this!==B&&this instanceof wrapper&&(Pe=de||w(Pe)),Pe.apply(xe,ye)}}},37812:(s,o,i)=>{\"use strict\";var a=i(76264),u=i(93742),_=a(\"iterator\"),w=Array.prototype;s.exports=function(s){return void 0!==s&&(u.Array===s||w[_]===s)}},37828:(s,o,i)=>{var a=i(9325).Uint8Array;s.exports=a},38221:(s,o,i)=>{var a=i(23805),u=i(10124),_=i(99374),w=Math.max,x=Math.min;s.exports=function debounce(s,o,i){var C,j,L,B,$,U,V=0,z=!1,Y=!1,Z=!0;if(\"function\"!=typeof s)throw new TypeError(\"Expected a function\");function invokeFunc(o){var i=C,a=j;return C=j=void 0,V=o,B=s.apply(a,i)}function shouldInvoke(s){var i=s-U;return void 0===U||i>=o||i<0||Y&&s-V>=L}function timerExpired(){var s=u();if(shouldInvoke(s))return trailingEdge(s);$=setTimeout(timerExpired,function remainingWait(s){var i=o-(s-U);return Y?x(i,L-(s-V)):i}(s))}function trailingEdge(s){return $=void 0,Z&&C?invokeFunc(s):(C=j=void 0,B)}function debounced(){var s=u(),i=shouldInvoke(s);if(C=arguments,j=this,U=s,i){if(void 0===$)return function leadingEdge(s){return V=s,$=setTimeout(timerExpired,o),z?invokeFunc(s):B}(U);if(Y)return clearTimeout($),$=setTimeout(timerExpired,o),invokeFunc(U)}return void 0===$&&($=setTimeout(timerExpired,o)),B}return o=_(o)||0,a(i)&&(z=!!i.leading,L=(Y=\"maxWait\"in i)?w(_(i.maxWait)||0,o):L,Z=\"trailing\"in i?!!i.trailing:Z),debounced.cancel=function cancel(){void 0!==$&&clearTimeout($),V=0,C=U=j=$=void 0},debounced.flush=function flush(){return void 0===$?B:trailingEdge(u())},debounced}},38329:(s,o,i)=>{var a=i(64894);s.exports=function createBaseEach(s,o){return function(i,u){if(null==i)return i;if(!a(i))return s(i,u);for(var _=i.length,w=o?_:-1,x=Object(i);(o?w--:++w<_)&&!1!==u(x[w],w,x););return i}}},38440:(s,o,i)=>{var a=i(16038),u=i(27301),_=i(86009),w=_&&_.isSet,x=w?u(w):a;s.exports=x},38530:s=>{\"use strict\";s.exports={}},38816:(s,o,i)=>{var a=i(35970),u=i(56757),_=i(32865);s.exports=function flatRest(s){return _(u(s,void 0,a),s+\"\")}},38859:(s,o,i)=>{var a=i(53661),u=i(31380),_=i(51459);function SetCache(s){var o=-1,i=null==s?0:s.length;for(this.__data__=new a;++o<i;)this.add(s[o])}SetCache.prototype.add=SetCache.prototype.push=u,SetCache.prototype.has=_,s.exports=SetCache},39209:(s,o,i)=>{\"use strict\";var a=i(76578),u=\"undefined\"==typeof globalThis?i.g:globalThis;s.exports=function availableTypedArrays(){for(var s=[],o=0;o<a.length;o++)\"function\"==typeof u[a[o]]&&(s[s.length]=a[o]);return s}},39259:(s,o,i)=>{\"use strict\";var a=i(46285),u=i(61626);s.exports=function(s,o){a(o)&&\"cause\"in o&&u(s,\"cause\",o.cause)}},39298:(s,o,i)=>{\"use strict\";var a=i(74239),u=Object;s.exports=function(s){return u(a(s))}},39344:(s,o,i)=>{var a=i(23805),u=Object.create,_=function(){function object(){}return function(s){if(!a(s))return{};if(u)return u(s);object.prototype=s;var o=new object;return object.prototype=void 0,o}}();s.exports=_},39447:(s,o,i)=>{\"use strict\";var a=i(98828);s.exports=!a((function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]}))},40154:(s,o,i)=>{\"use strict\";var a=i(13930),u=i(36624),_=i(29367);s.exports=function(s,o,i){var w,x;u(s);try{if(!(w=_(s,\"return\"))){if(\"throw\"===o)throw i;return i}w=a(w,s)}catch(s){x=!0,w=s}if(\"throw\"===o)throw i;if(x)throw w;return u(w),i}},40239:(s,o,i)=>{const a=i(10316);s.exports=class NumberElement extends a{constructor(s,o,i){super(s,o,i),this.element=\"number\"}primitive(){return\"number\"}}},40345:(s,o,i)=>{s.exports=i(37007).EventEmitter},40346:s=>{s.exports=function isObjectLike(s){return null!=s&&\"object\"==typeof s}},40551:(s,o,i)=>{\"use strict\";var a=i(45951),u=i(62250),_=a.WeakMap;s.exports=u(_)&&/native code/.test(String(_))},40860:(s,o,i)=>{var a=i(40882),u=i(80909),_=i(15389),w=i(85558),x=i(56449);s.exports=function reduce(s,o,i){var C=x(s)?a:w,j=arguments.length<3;return C(s,_(o,4),i,j,u)}},40882:s=>{s.exports=function arrayReduce(s,o,i,a){var u=-1,_=null==s?0:s.length;for(a&&_&&(i=s[++u]);++u<_;)i=o(i,s[u],u,s);return i}},40961:(s,o,i)=>{\"use strict\";!function checkDCE(){if(\"undefined\"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&\"function\"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE)}catch(s){console.error(s)}}(),s.exports=i(22551)},40975:(s,o,i)=>{\"use strict\";var a=i(9748);s.exports=a},41067:(s,o,i)=>{const a=i(10316);s.exports=class NullElement extends a{constructor(s,o,i){super(s||null,o,i),this.element=\"null\"}primitive(){return\"null\"}set(){return new Error(\"Cannot set the value of null\")}}},41176:s=>{\"use strict\";var o=Math.ceil,i=Math.floor;s.exports=Math.trunc||function trunc(s){var a=+s;return(a>0?i:o)(a)}},41237:s=>{\"use strict\";s.exports=EvalError},41333:s=>{\"use strict\";s.exports=function hasSymbols(){if(\"function\"!=typeof Symbol||\"function\"!=typeof Object.getOwnPropertySymbols)return!1;if(\"symbol\"==typeof Symbol.iterator)return!0;var s={},o=Symbol(\"test\"),i=Object(o);if(\"string\"==typeof o)return!1;if(\"[object Symbol]\"!==Object.prototype.toString.call(o))return!1;if(\"[object Symbol]\"!==Object.prototype.toString.call(i))return!1;for(var a in s[o]=42,s)return!1;if(\"function\"==typeof Object.keys&&0!==Object.keys(s).length)return!1;if(\"function\"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(s).length)return!1;var u=Object.getOwnPropertySymbols(s);if(1!==u.length||u[0]!==o)return!1;if(!Object.prototype.propertyIsEnumerable.call(s,o))return!1;if(\"function\"==typeof Object.getOwnPropertyDescriptor){var _=Object.getOwnPropertyDescriptor(s,o);if(42!==_.value||!0!==_.enumerable)return!1}return!0}},41505:(s,o,i)=>{\"use strict\";var a=i(98828);s.exports=!a((function(){var s=function(){}.bind();return\"function\"!=typeof s||s.hasOwnProperty(\"prototype\")}))},41799:(s,o,i)=>{var a=i(37217),u=i(60270);s.exports=function baseIsMatch(s,o,i,_){var w=i.length,x=w,C=!_;if(null==s)return!x;for(s=Object(s);w--;){var j=i[w];if(C&&j[2]?j[1]!==s[j[0]]:!(j[0]in s))return!1}for(;++w<x;){var L=(j=i[w])[0],B=s[L],$=j[1];if(C&&j[2]){if(void 0===B&&!(L in s))return!1}else{var U=new a;if(_)var V=_(B,$,L,s,o,U);if(!(void 0===V?u($,B,3,_,U):V))return!1}}return!0}},41859:(s,o,i)=>{const a=i(27096),u=i(78004),_=a.types;s.exports=class RandExp{constructor(s,o){if(this._setDefaults(s),s instanceof RegExp)this.ignoreCase=s.ignoreCase,this.multiline=s.multiline,s=s.source;else{if(\"string\"!=typeof s)throw new Error(\"Expected a regexp or string\");this.ignoreCase=o&&-1!==o.indexOf(\"i\"),this.multiline=o&&-1!==o.indexOf(\"m\")}this.tokens=a(s)}_setDefaults(s){this.max=null!=s.max?s.max:null!=RandExp.prototype.max?RandExp.prototype.max:100,this.defaultRange=s.defaultRange?s.defaultRange:this.defaultRange.clone(),s.randInt&&(this.randInt=s.randInt)}gen(){return this._gen(this.tokens,[])}_gen(s,o){var i,a,u,w,x;switch(s.type){case _.ROOT:case _.GROUP:if(s.followedBy||s.notFollowedBy)return\"\";for(s.remember&&void 0===s.groupNumber&&(s.groupNumber=o.push(null)-1),a=\"\",w=0,x=(i=s.options?this._randSelect(s.options):s.stack).length;w<x;w++)a+=this._gen(i[w],o);return s.remember&&(o[s.groupNumber]=a),a;case _.POSITION:return\"\";case _.SET:var C=this._expand(s);return C.length?String.fromCharCode(this._randSelect(C)):\"\";case _.REPETITION:for(u=this.randInt(s.min,s.max===1/0?s.min+this.max:s.max),a=\"\",w=0;w<u;w++)a+=this._gen(s.value,o);return a;case _.REFERENCE:return o[s.value-1]||\"\";case _.CHAR:var j=this.ignoreCase&&this._randBool()?this._toOtherCase(s.value):s.value;return String.fromCharCode(j)}}_toOtherCase(s){return s+(97<=s&&s<=122?-32:65<=s&&s<=90?32:0)}_randBool(){return!this.randInt(0,1)}_randSelect(s){return s instanceof u?s.index(this.randInt(0,s.length-1)):s[this.randInt(0,s.length-1)]}_expand(s){if(s.type===a.types.CHAR)return new u(s.value);if(s.type===a.types.RANGE)return new u(s.from,s.to);{let o=new u;for(let i=0;i<s.set.length;i++){let a=this._expand(s.set[i]);if(o.add(a),this.ignoreCase)for(let s=0;s<a.length;s++){let i=a.index(s),u=this._toOtherCase(i);i!==u&&o.add(u)}}return s.not?this.defaultRange.clone().subtract(o):this.defaultRange.clone().intersect(o)}}randInt(s,o){return s+Math.floor(Math.random()*(1+o-s))}get defaultRange(){return this._range=this._range||new u(32,126)}set defaultRange(s){this._range=s}static randexp(s,o){var i;return\"string\"==typeof s&&(s=new RegExp(s,o)),void 0===s._randexp?(i=new RandExp(s,o),s._randexp=i):(i=s._randexp)._setDefaults(s),i.gen()}static sugar(){RegExp.prototype.gen=function(){return RandExp.randexp(this)}}}},42054:s=>{var o=\"\\\\ud800-\\\\udfff\",i=\"[\"+o+\"]\",a=\"[\\\\u0300-\\\\u036f\\\\ufe20-\\\\ufe2f\\\\u20d0-\\\\u20ff]\",u=\"\\\\ud83c[\\\\udffb-\\\\udfff]\",_=\"[^\"+o+\"]\",w=\"(?:\\\\ud83c[\\\\udde6-\\\\uddff]){2}\",x=\"[\\\\ud800-\\\\udbff][\\\\udc00-\\\\udfff]\",C=\"(?:\"+a+\"|\"+u+\")\"+\"?\",j=\"[\\\\ufe0e\\\\ufe0f]?\",L=j+C+(\"(?:\\\\u200d(?:\"+[_,w,x].join(\"|\")+\")\"+j+C+\")*\"),B=\"(?:\"+[_+a+\"?\",a,w,x,i].join(\"|\")+\")\",$=RegExp(u+\"(?=\"+u+\")|\"+B+L,\"g\");s.exports=function unicodeToArray(s){return s.match($)||[]}},42072:(s,o,i)=>{var a=i(34932),u=i(23007),_=i(56449),w=i(44394),x=i(61802),C=i(77797),j=i(13222);s.exports=function toPath(s){return _(s)?a(s,C):w(s)?[s]:u(x(j(s)))}},42156:s=>{\"use strict\";s.exports=function(){}},42220:(s,o,i)=>{\"use strict\";var a=i(39447),u=i(58661),_=i(74284),w=i(36624),x=i(4993),C=i(2875);o.f=a&&!u?Object.defineProperties:function defineProperties(s,o){w(s);for(var i,a=x(o),u=C(o),j=u.length,L=0;j>L;)_.f(s,i=u[L++],a[i]);return s}},42426:(s,o,i)=>{var a=i(14248),u=i(15389),_=i(90916),w=i(56449),x=i(36800);s.exports=function some(s,o,i){var C=w(s)?a:_;return i&&x(s,o,i)&&(o=void 0),C(s,u(o,3))}},42824:(s,o,i)=>{var a=i(87805),u=i(93290),_=i(71961),w=i(23007),x=i(35529),C=i(72428),j=i(56449),L=i(83693),B=i(3656),$=i(1882),U=i(23805),V=i(11331),z=i(37167),Y=i(14974),Z=i(69884);s.exports=function baseMergeDeep(s,o,i,ee,ie,ae,ce){var le=Y(s,i),pe=Y(o,i),de=ce.get(pe);if(de)a(s,i,de);else{var fe=ae?ae(le,pe,i+\"\",s,o,ce):void 0,ye=void 0===fe;if(ye){var be=j(pe),_e=!be&&B(pe),Se=!be&&!_e&&z(pe);fe=pe,be||_e||Se?j(le)?fe=le:L(le)?fe=w(le):_e?(ye=!1,fe=u(pe,!0)):Se?(ye=!1,fe=_(pe,!0)):fe=[]:V(pe)||C(pe)?(fe=le,C(le)?fe=Z(le):U(le)&&!$(le)||(fe=x(pe))):ye=!1}ye&&(ce.set(pe,fe),ie(fe,pe,ee,ae,ce),ce.delete(pe)),a(s,i,fe)}}},43360:(s,o,i)=>{var a=i(93243);s.exports=function baseAssignValue(s,o,i){\"__proto__\"==o&&a?a(s,o,{configurable:!0,enumerable:!0,value:i,writable:!0}):s[o]=i}},43768:(s,o,i)=>{\"use strict\";var a=i(45981),u=i(85587);o.highlight=highlight,o.highlightAuto=function highlightAuto(s,o){var i,w,x,C,j=o||{},L=j.subset||a.listLanguages(),B=j.prefix,$=L.length,U=-1;null==B&&(B=_);if(\"string\"!=typeof s)throw u(\"Expected `string` for value, got `%s`\",s);w={relevance:0,language:null,value:[]},i={relevance:0,language:null,value:[]};for(;++U<$;)C=L[U],a.getLanguage(C)&&((x=highlight(C,s,o)).language=C,x.relevance>w.relevance&&(w=x),x.relevance>i.relevance&&(w=i,i=x));w.language&&(i.secondBest=w);return i},o.registerLanguage=function registerLanguage(s,o){a.registerLanguage(s,o)},o.listLanguages=function listLanguages(){return a.listLanguages()},o.registerAlias=function registerAlias(s,o){var i,u=s;o&&((u={})[s]=o);for(i in u)a.registerAliases(u[i],{languageName:i})},Emitter.prototype.addText=function text(s){var o,i,a=this.stack;if(\"\"===s)return;o=a[a.length-1],(i=o.children[o.children.length-1])&&\"text\"===i.type?i.value+=s:o.children.push({type:\"text\",value:s})},Emitter.prototype.addKeyword=function addKeyword(s,o){this.openNode(o),this.addText(s),this.closeNode()},Emitter.prototype.addSublanguage=function addSublanguage(s,o){var i=this.stack,a=i[i.length-1],u=s.rootNode.children,_=o?{type:\"element\",tagName:\"span\",properties:{className:[o]},children:u}:u;a.children=a.children.concat(_)},Emitter.prototype.openNode=function open(s){var o=this.stack,i=this.options.classPrefix+s,a=o[o.length-1],u={type:\"element\",tagName:\"span\",properties:{className:[i]},children:[]};a.children.push(u),o.push(u)},Emitter.prototype.closeNode=function close(){this.stack.pop()},Emitter.prototype.closeAllNodes=noop,Emitter.prototype.finalize=noop,Emitter.prototype.toHTML=function toHtmlNoop(){return\"\"};var _=\"hljs-\";function highlight(s,o,i){var w,x=a.configure({}),C=(i||{}).prefix;if(\"string\"!=typeof s)throw u(\"Expected `string` for name, got `%s`\",s);if(!a.getLanguage(s))throw u(\"Unknown language: `%s` is not registered\",s);if(\"string\"!=typeof o)throw u(\"Expected `string` for value, got `%s`\",o);if(null==C&&(C=_),a.configure({__emitter:Emitter,classPrefix:C}),w=a.highlight(o,{language:s,ignoreIllegals:!0}),a.configure(x||{}),w.errorRaised)throw w.errorRaised;return{relevance:w.relevance,language:w.language,value:w.emitter.rootNode.children}}function Emitter(s){this.options=s,this.rootNode={children:[]},this.stack=[this.rootNode]}function noop(){}},43838:(s,o,i)=>{var a=i(21791),u=i(37241);s.exports=function baseAssignIn(s,o){return s&&a(o,u(o),s)}},44394:(s,o,i)=>{var a=i(72552),u=i(40346);s.exports=function isSymbol(s){return\"symbol\"==typeof s||u(s)&&\"[object Symbol]\"==a(s)}},44673:(s,o,i)=>{\"use strict\";var a=i(1907),u=i(82159),_=i(46285),w=i(49724),x=i(93427),C=i(41505),j=Function,L=a([].concat),B=a([].join),$={};s.exports=C?j.bind:function bind(s){var o=u(this),i=o.prototype,a=x(arguments,1),C=function bound(){var i=L(a,x(arguments));return this instanceof C?function(s,o,i){if(!w($,o)){for(var a=[],u=0;u<o;u++)a[u]=\"a[\"+u+\"]\";$[o]=j(\"C,a\",\"return new C(\"+B(a,\",\")+\")\")}return $[o](s,i)}(o,i.length,i):o.apply(s,i)};return _(i)&&(C.prototype=i),C}},45083:(s,o,i)=>{var a=i(1882),u=i(87296),_=i(23805),w=i(47473),x=/^\\[object .+?Constructor\\]$/,C=Function.prototype,j=Object.prototype,L=C.toString,B=j.hasOwnProperty,$=RegExp(\"^\"+L.call(B).replace(/[\\\\^$.*+?()[\\]{}|]/g,\"\\\\$&\").replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g,\"$1.*?\")+\"$\");s.exports=function baseIsNative(s){return!(!_(s)||u(s))&&(a(s)?$:x).test(w(s))}},45412:(s,o,i)=>{\"use strict\";var a,u=i(65606);s.exports=Readable,Readable.ReadableState=ReadableState;i(37007).EventEmitter;var _=function EElistenerCount(s,o){return s.listeners(o).length},w=i(40345),x=i(48287).Buffer,C=(void 0!==i.g?i.g:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:{}).Uint8Array||function(){};var j,L=i(79838);j=L&&L.debuglog?L.debuglog(\"stream\"):function debug(){};var B,$,U,V=i(80345),z=i(75896),Y=i(65291).getHighWaterMark,Z=i(86048).F,ee=Z.ERR_INVALID_ARG_TYPE,ie=Z.ERR_STREAM_PUSH_AFTER_EOF,ae=Z.ERR_METHOD_NOT_IMPLEMENTED,ce=Z.ERR_STREAM_UNSHIFT_AFTER_END_EVENT;i(56698)(Readable,w);var le=z.errorOrDestroy,pe=[\"error\",\"close\",\"destroy\",\"pause\",\"resume\"];function ReadableState(s,o,u){a=a||i(25382),s=s||{},\"boolean\"!=typeof u&&(u=o instanceof a),this.objectMode=!!s.objectMode,u&&(this.objectMode=this.objectMode||!!s.readableObjectMode),this.highWaterMark=Y(this,s,\"readableHighWaterMark\",u),this.buffer=new V,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.paused=!0,this.emitClose=!1!==s.emitClose,this.autoDestroy=!!s.autoDestroy,this.destroyed=!1,this.defaultEncoding=s.defaultEncoding||\"utf8\",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,s.encoding&&(B||(B=i(83141).I),this.decoder=new B(s.encoding),this.encoding=s.encoding)}function Readable(s){if(a=a||i(25382),!(this instanceof Readable))return new Readable(s);var o=this instanceof a;this._readableState=new ReadableState(s,this,o),this.readable=!0,s&&(\"function\"==typeof s.read&&(this._read=s.read),\"function\"==typeof s.destroy&&(this._destroy=s.destroy)),w.call(this)}function readableAddChunk(s,o,i,a,u){j(\"readableAddChunk\",o);var _,w=s._readableState;if(null===o)w.reading=!1,function onEofChunk(s,o){if(j(\"onEofChunk\"),o.ended)return;if(o.decoder){var i=o.decoder.end();i&&i.length&&(o.buffer.push(i),o.length+=o.objectMode?1:i.length)}o.ended=!0,o.sync?emitReadable(s):(o.needReadable=!1,o.emittedReadable||(o.emittedReadable=!0,emitReadable_(s)))}(s,w);else if(u||(_=function chunkInvalid(s,o){var i;(function _isUint8Array(s){return x.isBuffer(s)||s instanceof C})(o)||\"string\"==typeof o||void 0===o||s.objectMode||(i=new ee(\"chunk\",[\"string\",\"Buffer\",\"Uint8Array\"],o));return i}(w,o)),_)le(s,_);else if(w.objectMode||o&&o.length>0)if(\"string\"==typeof o||w.objectMode||Object.getPrototypeOf(o)===x.prototype||(o=function _uint8ArrayToBuffer(s){return x.from(s)}(o)),a)w.endEmitted?le(s,new ce):addChunk(s,w,o,!0);else if(w.ended)le(s,new ie);else{if(w.destroyed)return!1;w.reading=!1,w.decoder&&!i?(o=w.decoder.write(o),w.objectMode||0!==o.length?addChunk(s,w,o,!1):maybeReadMore(s,w)):addChunk(s,w,o,!1)}else a||(w.reading=!1,maybeReadMore(s,w));return!w.ended&&(w.length<w.highWaterMark||0===w.length)}function addChunk(s,o,i,a){o.flowing&&0===o.length&&!o.sync?(o.awaitDrain=0,s.emit(\"data\",i)):(o.length+=o.objectMode?1:i.length,a?o.buffer.unshift(i):o.buffer.push(i),o.needReadable&&emitReadable(s)),maybeReadMore(s,o)}Object.defineProperty(Readable.prototype,\"destroyed\",{enumerable:!1,get:function get(){return void 0!==this._readableState&&this._readableState.destroyed},set:function set(s){this._readableState&&(this._readableState.destroyed=s)}}),Readable.prototype.destroy=z.destroy,Readable.prototype._undestroy=z.undestroy,Readable.prototype._destroy=function(s,o){o(s)},Readable.prototype.push=function(s,o){var i,a=this._readableState;return a.objectMode?i=!0:\"string\"==typeof s&&((o=o||a.defaultEncoding)!==a.encoding&&(s=x.from(s,o),o=\"\"),i=!0),readableAddChunk(this,s,o,!1,i)},Readable.prototype.unshift=function(s){return readableAddChunk(this,s,null,!0,!1)},Readable.prototype.isPaused=function(){return!1===this._readableState.flowing},Readable.prototype.setEncoding=function(s){B||(B=i(83141).I);var o=new B(s);this._readableState.decoder=o,this._readableState.encoding=this._readableState.decoder.encoding;for(var a=this._readableState.buffer.head,u=\"\";null!==a;)u+=o.write(a.data),a=a.next;return this._readableState.buffer.clear(),\"\"!==u&&this._readableState.buffer.push(u),this._readableState.length=u.length,this};var de=1073741824;function howMuchToRead(s,o){return s<=0||0===o.length&&o.ended?0:o.objectMode?1:s!=s?o.flowing&&o.length?o.buffer.head.data.length:o.length:(s>o.highWaterMark&&(o.highWaterMark=function computeNewHighWaterMark(s){return s>=de?s=de:(s--,s|=s>>>1,s|=s>>>2,s|=s>>>4,s|=s>>>8,s|=s>>>16,s++),s}(s)),s<=o.length?s:o.ended?o.length:(o.needReadable=!0,0))}function emitReadable(s){var o=s._readableState;j(\"emitReadable\",o.needReadable,o.emittedReadable),o.needReadable=!1,o.emittedReadable||(j(\"emitReadable\",o.flowing),o.emittedReadable=!0,u.nextTick(emitReadable_,s))}function emitReadable_(s){var o=s._readableState;j(\"emitReadable_\",o.destroyed,o.length,o.ended),o.destroyed||!o.length&&!o.ended||(s.emit(\"readable\"),o.emittedReadable=!1),o.needReadable=!o.flowing&&!o.ended&&o.length<=o.highWaterMark,flow(s)}function maybeReadMore(s,o){o.readingMore||(o.readingMore=!0,u.nextTick(maybeReadMore_,s,o))}function maybeReadMore_(s,o){for(;!o.reading&&!o.ended&&(o.length<o.highWaterMark||o.flowing&&0===o.length);){var i=o.length;if(j(\"maybeReadMore read 0\"),s.read(0),i===o.length)break}o.readingMore=!1}function updateReadableListening(s){var o=s._readableState;o.readableListening=s.listenerCount(\"readable\")>0,o.resumeScheduled&&!o.paused?o.flowing=!0:s.listenerCount(\"data\")>0&&s.resume()}function nReadingNextTick(s){j(\"readable nexttick read 0\"),s.read(0)}function resume_(s,o){j(\"resume\",o.reading),o.reading||s.read(0),o.resumeScheduled=!1,s.emit(\"resume\"),flow(s),o.flowing&&!o.reading&&s.read(0)}function flow(s){var o=s._readableState;for(j(\"flow\",o.flowing);o.flowing&&null!==s.read(););}function fromList(s,o){return 0===o.length?null:(o.objectMode?i=o.buffer.shift():!s||s>=o.length?(i=o.decoder?o.buffer.join(\"\"):1===o.buffer.length?o.buffer.first():o.buffer.concat(o.length),o.buffer.clear()):i=o.buffer.consume(s,o.decoder),i);var i}function endReadable(s){var o=s._readableState;j(\"endReadable\",o.endEmitted),o.endEmitted||(o.ended=!0,u.nextTick(endReadableNT,o,s))}function endReadableNT(s,o){if(j(\"endReadableNT\",s.endEmitted,s.length),!s.endEmitted&&0===s.length&&(s.endEmitted=!0,o.readable=!1,o.emit(\"end\"),s.autoDestroy)){var i=o._writableState;(!i||i.autoDestroy&&i.finished)&&o.destroy()}}function indexOf(s,o){for(var i=0,a=s.length;i<a;i++)if(s[i]===o)return i;return-1}Readable.prototype.read=function(s){j(\"read\",s),s=parseInt(s,10);var o=this._readableState,i=s;if(0!==s&&(o.emittedReadable=!1),0===s&&o.needReadable&&((0!==o.highWaterMark?o.length>=o.highWaterMark:o.length>0)||o.ended))return j(\"read: emitReadable\",o.length,o.ended),0===o.length&&o.ended?endReadable(this):emitReadable(this),null;if(0===(s=howMuchToRead(s,o))&&o.ended)return 0===o.length&&endReadable(this),null;var a,u=o.needReadable;return j(\"need readable\",u),(0===o.length||o.length-s<o.highWaterMark)&&j(\"length less than watermark\",u=!0),o.ended||o.reading?j(\"reading or ended\",u=!1):u&&(j(\"do read\"),o.reading=!0,o.sync=!0,0===o.length&&(o.needReadable=!0),this._read(o.highWaterMark),o.sync=!1,o.reading||(s=howMuchToRead(i,o))),null===(a=s>0?fromList(s,o):null)?(o.needReadable=o.length<=o.highWaterMark,s=0):(o.length-=s,o.awaitDrain=0),0===o.length&&(o.ended||(o.needReadable=!0),i!==s&&o.ended&&endReadable(this)),null!==a&&this.emit(\"data\",a),a},Readable.prototype._read=function(s){le(this,new ae(\"_read()\"))},Readable.prototype.pipe=function(s,o){var i=this,a=this._readableState;switch(a.pipesCount){case 0:a.pipes=s;break;case 1:a.pipes=[a.pipes,s];break;default:a.pipes.push(s)}a.pipesCount+=1,j(\"pipe count=%d opts=%j\",a.pipesCount,o);var w=(!o||!1!==o.end)&&s!==u.stdout&&s!==u.stderr?onend:unpipe;function onunpipe(o,u){j(\"onunpipe\"),o===i&&u&&!1===u.hasUnpiped&&(u.hasUnpiped=!0,function cleanup(){j(\"cleanup\"),s.removeListener(\"close\",onclose),s.removeListener(\"finish\",onfinish),s.removeListener(\"drain\",x),s.removeListener(\"error\",onerror),s.removeListener(\"unpipe\",onunpipe),i.removeListener(\"end\",onend),i.removeListener(\"end\",unpipe),i.removeListener(\"data\",ondata),C=!0,!a.awaitDrain||s._writableState&&!s._writableState.needDrain||x()}())}function onend(){j(\"onend\"),s.end()}a.endEmitted?u.nextTick(w):i.once(\"end\",w),s.on(\"unpipe\",onunpipe);var x=function pipeOnDrain(s){return function pipeOnDrainFunctionResult(){var o=s._readableState;j(\"pipeOnDrain\",o.awaitDrain),o.awaitDrain&&o.awaitDrain--,0===o.awaitDrain&&_(s,\"data\")&&(o.flowing=!0,flow(s))}}(i);s.on(\"drain\",x);var C=!1;function ondata(o){j(\"ondata\");var u=s.write(o);j(\"dest.write\",u),!1===u&&((1===a.pipesCount&&a.pipes===s||a.pipesCount>1&&-1!==indexOf(a.pipes,s))&&!C&&(j(\"false write response, pause\",a.awaitDrain),a.awaitDrain++),i.pause())}function onerror(o){j(\"onerror\",o),unpipe(),s.removeListener(\"error\",onerror),0===_(s,\"error\")&&le(s,o)}function onclose(){s.removeListener(\"finish\",onfinish),unpipe()}function onfinish(){j(\"onfinish\"),s.removeListener(\"close\",onclose),unpipe()}function unpipe(){j(\"unpipe\"),i.unpipe(s)}return i.on(\"data\",ondata),function prependListener(s,o,i){if(\"function\"==typeof s.prependListener)return s.prependListener(o,i);s._events&&s._events[o]?Array.isArray(s._events[o])?s._events[o].unshift(i):s._events[o]=[i,s._events[o]]:s.on(o,i)}(s,\"error\",onerror),s.once(\"close\",onclose),s.once(\"finish\",onfinish),s.emit(\"pipe\",i),a.flowing||(j(\"pipe resume\"),i.resume()),s},Readable.prototype.unpipe=function(s){var o=this._readableState,i={hasUnpiped:!1};if(0===o.pipesCount)return this;if(1===o.pipesCount)return s&&s!==o.pipes||(s||(s=o.pipes),o.pipes=null,o.pipesCount=0,o.flowing=!1,s&&s.emit(\"unpipe\",this,i)),this;if(!s){var a=o.pipes,u=o.pipesCount;o.pipes=null,o.pipesCount=0,o.flowing=!1;for(var _=0;_<u;_++)a[_].emit(\"unpipe\",this,{hasUnpiped:!1});return this}var w=indexOf(o.pipes,s);return-1===w||(o.pipes.splice(w,1),o.pipesCount-=1,1===o.pipesCount&&(o.pipes=o.pipes[0]),s.emit(\"unpipe\",this,i)),this},Readable.prototype.on=function(s,o){var i=w.prototype.on.call(this,s,o),a=this._readableState;return\"data\"===s?(a.readableListening=this.listenerCount(\"readable\")>0,!1!==a.flowing&&this.resume()):\"readable\"===s&&(a.endEmitted||a.readableListening||(a.readableListening=a.needReadable=!0,a.flowing=!1,a.emittedReadable=!1,j(\"on readable\",a.length,a.reading),a.length?emitReadable(this):a.reading||u.nextTick(nReadingNextTick,this))),i},Readable.prototype.addListener=Readable.prototype.on,Readable.prototype.removeListener=function(s,o){var i=w.prototype.removeListener.call(this,s,o);return\"readable\"===s&&u.nextTick(updateReadableListening,this),i},Readable.prototype.removeAllListeners=function(s){var o=w.prototype.removeAllListeners.apply(this,arguments);return\"readable\"!==s&&void 0!==s||u.nextTick(updateReadableListening,this),o},Readable.prototype.resume=function(){var s=this._readableState;return s.flowing||(j(\"resume\"),s.flowing=!s.readableListening,function resume(s,o){o.resumeScheduled||(o.resumeScheduled=!0,u.nextTick(resume_,s,o))}(this,s)),s.paused=!1,this},Readable.prototype.pause=function(){return j(\"call pause flowing=%j\",this._readableState.flowing),!1!==this._readableState.flowing&&(j(\"pause\"),this._readableState.flowing=!1,this.emit(\"pause\")),this._readableState.paused=!0,this},Readable.prototype.wrap=function(s){var o=this,i=this._readableState,a=!1;for(var u in s.on(\"end\",(function(){if(j(\"wrapped end\"),i.decoder&&!i.ended){var s=i.decoder.end();s&&s.length&&o.push(s)}o.push(null)})),s.on(\"data\",(function(u){(j(\"wrapped data\"),i.decoder&&(u=i.decoder.write(u)),i.objectMode&&null==u)||(i.objectMode||u&&u.length)&&(o.push(u)||(a=!0,s.pause()))})),s)void 0===this[u]&&\"function\"==typeof s[u]&&(this[u]=function methodWrap(o){return function methodWrapReturnFunction(){return s[o].apply(s,arguments)}}(u));for(var _=0;_<pe.length;_++)s.on(pe[_],this.emit.bind(this,pe[_]));return this._read=function(o){j(\"wrapped _read\",o),a&&(a=!1,s.resume())},this},\"function\"==typeof Symbol&&(Readable.prototype[Symbol.asyncIterator]=function(){return void 0===$&&($=i(2955)),$(this)}),Object.defineProperty(Readable.prototype,\"readableHighWaterMark\",{enumerable:!1,get:function get(){return this._readableState.highWaterMark}}),Object.defineProperty(Readable.prototype,\"readableBuffer\",{enumerable:!1,get:function get(){return this._readableState&&this._readableState.buffer}}),Object.defineProperty(Readable.prototype,\"readableFlowing\",{enumerable:!1,get:function get(){return this._readableState.flowing},set:function set(s){this._readableState&&(this._readableState.flowing=s)}}),Readable._fromList=fromList,Object.defineProperty(Readable.prototype,\"readableLength\",{enumerable:!1,get:function get(){return this._readableState.length}}),\"function\"==typeof Symbol&&(Readable.from=function(s,o){return void 0===U&&(U=i(55157)),U(Readable,s,o)})},45434:s=>{var o=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;s.exports=function hasUnicodeWord(s){return o.test(s)}},45539:(s,o,i)=>{var a=i(40882),u=i(50828),_=i(66645),w=RegExp(\"['’]\",\"g\");s.exports=function createCompounder(s){return function(o){return a(_(u(o).replace(w,\"\")),s,\"\")}}},45807:(s,o,i)=>{\"use strict\";var a=i(1907),u=a({}.toString),_=a(\"\".slice);s.exports=function(s){return _(u(s),8,-1)}},45891:(s,o,i)=>{var a=i(51873),u=i(72428),_=i(56449),w=a?a.isConcatSpreadable:void 0;s.exports=function isFlattenable(s){return _(s)||u(s)||!!(w&&s&&s[w])}},45951:function(s,o,i){\"use strict\";var check=function(s){return s&&s.Math===Math&&s};s.exports=check(\"object\"==typeof globalThis&&globalThis)||check(\"object\"==typeof window&&window)||check(\"object\"==typeof self&&self)||check(\"object\"==typeof i.g&&i.g)||check(\"object\"==typeof this&&this)||function(){return this}()||Function(\"return this\")()},45981:s=>{function deepFreeze(s){return s instanceof Map?s.clear=s.delete=s.set=function(){throw new Error(\"map is read-only\")}:s instanceof Set&&(s.add=s.clear=s.delete=function(){throw new Error(\"set is read-only\")}),Object.freeze(s),Object.getOwnPropertyNames(s).forEach((function(o){var i=s[o];\"object\"!=typeof i||Object.isFrozen(i)||deepFreeze(i)})),s}var o=deepFreeze,i=deepFreeze;o.default=i;class Response{constructor(s){void 0===s.data&&(s.data={}),this.data=s.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function escapeHTML(s){return s.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\").replace(/'/g,\"&#x27;\")}function inherit(s,...o){const i=Object.create(null);for(const o in s)i[o]=s[o];return o.forEach((function(s){for(const o in s)i[o]=s[o]})),i}const emitsWrappingTags=s=>!!s.kind;class HTMLRenderer{constructor(s,o){this.buffer=\"\",this.classPrefix=o.classPrefix,s.walk(this)}addText(s){this.buffer+=escapeHTML(s)}openNode(s){if(!emitsWrappingTags(s))return;let o=s.kind;s.sublanguage||(o=`${this.classPrefix}${o}`),this.span(o)}closeNode(s){emitsWrappingTags(s)&&(this.buffer+=\"</span>\")}value(){return this.buffer}span(s){this.buffer+=`<span class=\"${s}\">`}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(s){this.top.children.push(s)}openNode(s){const o={kind:s,children:[]};this.add(o),this.stack.push(o)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(s){return this.constructor._walk(s,this.rootNode)}static _walk(s,o){return\"string\"==typeof o?s.addText(o):o.children&&(s.openNode(o),o.children.forEach((o=>this._walk(s,o))),s.closeNode(o)),s}static _collapse(s){\"string\"!=typeof s&&s.children&&(s.children.every((s=>\"string\"==typeof s))?s.children=[s.children.join(\"\")]:s.children.forEach((s=>{TokenTree._collapse(s)})))}}class TokenTreeEmitter extends TokenTree{constructor(s){super(),this.options=s}addKeyword(s,o){\"\"!==s&&(this.openNode(o),this.addText(s),this.closeNode())}addText(s){\"\"!==s&&this.add(s)}addSublanguage(s,o){const i=s.root;i.kind=o,i.sublanguage=!0,this.add(i)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){return!0}}function source(s){return s?\"string\"==typeof s?s:s.source:null}const a=/\\[(?:[^\\\\\\]]|\\\\.)*\\]|\\(\\??|\\\\([1-9][0-9]*)|\\\\./;const u=\"[a-zA-Z]\\\\w*\",_=\"[a-zA-Z_]\\\\w*\",w=\"\\\\b\\\\d+(\\\\.\\\\d+)?\",x=\"(-?)(\\\\b0[xX][a-fA-F0-9]+|(\\\\b\\\\d+(\\\\.\\\\d*)?|\\\\.\\\\d+)([eE][-+]?\\\\d+)?)\",C=\"\\\\b(0b[01]+)\",j={begin:\"\\\\\\\\[\\\\s\\\\S]\",relevance:0},L={className:\"string\",begin:\"'\",end:\"'\",illegal:\"\\\\n\",contains:[j]},B={className:\"string\",begin:'\"',end:'\"',illegal:\"\\\\n\",contains:[j]},$={begin:/\\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\\b/},COMMENT=function(s,o,i={}){const a=inherit({className:\"comment\",begin:s,end:o,contains:[]},i);return a.contains.push($),a.contains.push({className:\"doctag\",begin:\"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):\",relevance:0}),a},U=COMMENT(\"//\",\"$\"),V=COMMENT(\"/\\\\*\",\"\\\\*/\"),z=COMMENT(\"#\",\"$\"),Y={className:\"number\",begin:w,relevance:0},Z={className:\"number\",begin:x,relevance:0},ee={className:\"number\",begin:C,relevance:0},ie={className:\"number\",begin:w+\"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?\",relevance:0},ae={begin:/(?=\\/[^/\\n]*\\/)/,contains:[{className:\"regexp\",begin:/\\//,end:/\\/[gimuy]*/,illegal:/\\n/,contains:[j,{begin:/\\[/,end:/\\]/,relevance:0,contains:[j]}]}]},ce={className:\"title\",begin:u,relevance:0},le={className:\"title\",begin:_,relevance:0},pe={begin:\"\\\\.\\\\s*\"+_,relevance:0};var de=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\\b\\B/,IDENT_RE:u,UNDERSCORE_IDENT_RE:_,NUMBER_RE:w,C_NUMBER_RE:x,BINARY_NUMBER_RE:C,RE_STARTERS_RE:\"!|!=|!==|%|%=|&|&&|&=|\\\\*|\\\\*=|\\\\+|\\\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\\\?|\\\\[|\\\\{|\\\\(|\\\\^|\\\\^=|\\\\||\\\\|=|\\\\|\\\\||~\",SHEBANG:(s={})=>{const o=/^#![ ]*\\//;return s.binary&&(s.begin=function concat(...s){return s.map((s=>source(s))).join(\"\")}(o,/.*\\b/,s.binary,/\\b.*/)),inherit({className:\"meta\",begin:o,end:/$/,relevance:0,\"on:begin\":(s,o)=>{0!==s.index&&o.ignoreMatch()}},s)},BACKSLASH_ESCAPE:j,APOS_STRING_MODE:L,QUOTE_STRING_MODE:B,PHRASAL_WORDS_MODE:$,COMMENT,C_LINE_COMMENT_MODE:U,C_BLOCK_COMMENT_MODE:V,HASH_COMMENT_MODE:z,NUMBER_MODE:Y,C_NUMBER_MODE:Z,BINARY_NUMBER_MODE:ee,CSS_NUMBER_MODE:ie,REGEXP_MODE:ae,TITLE_MODE:ce,UNDERSCORE_TITLE_MODE:le,METHOD_GUARD:pe,END_SAME_AS_BEGIN:function(s){return Object.assign(s,{\"on:begin\":(s,o)=>{o.data._beginMatch=s[1]},\"on:end\":(s,o)=>{o.data._beginMatch!==s[1]&&o.ignoreMatch()}})}});function skipIfhasPrecedingDot(s,o){\".\"===s.input[s.index-1]&&o.ignoreMatch()}function beginKeywords(s,o){o&&s.beginKeywords&&(s.begin=\"\\\\b(\"+s.beginKeywords.split(\" \").join(\"|\")+\")(?!\\\\.)(?=\\\\b|\\\\s)\",s.__beforeBegin=skipIfhasPrecedingDot,s.keywords=s.keywords||s.beginKeywords,delete s.beginKeywords,void 0===s.relevance&&(s.relevance=0))}function compileIllegal(s,o){Array.isArray(s.illegal)&&(s.illegal=function either(...s){return\"(\"+s.map((s=>source(s))).join(\"|\")+\")\"}(...s.illegal))}function compileMatch(s,o){if(s.match){if(s.begin||s.end)throw new Error(\"begin & end are not supported with match\");s.begin=s.match,delete s.match}}function compileRelevance(s,o){void 0===s.relevance&&(s.relevance=1)}const fe=[\"of\",\"and\",\"for\",\"in\",\"not\",\"or\",\"if\",\"then\",\"parent\",\"list\",\"value\"];function compileKeywords(s,o,i=\"keyword\"){const a={};return\"string\"==typeof s?compileList(i,s.split(\" \")):Array.isArray(s)?compileList(i,s):Object.keys(s).forEach((function(i){Object.assign(a,compileKeywords(s[i],o,i))})),a;function compileList(s,i){o&&(i=i.map((s=>s.toLowerCase()))),i.forEach((function(o){const i=o.split(\"|\");a[i[0]]=[s,scoreForKeyword(i[0],i[1])]}))}}function scoreForKeyword(s,o){return o?Number(o):function commonKeyword(s){return fe.includes(s.toLowerCase())}(s)?0:1}function compileLanguage(s,{plugins:o}){function langRe(o,i){return new RegExp(source(o),\"m\"+(s.case_insensitive?\"i\":\"\")+(i?\"g\":\"\"))}class MultiRegex{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(s,o){o.position=this.position++,this.matchIndexes[this.matchAt]=o,this.regexes.push([o,s]),this.matchAt+=function countMatchGroups(s){return new RegExp(s.toString()+\"|\").exec(\"\").length-1}(s)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const s=this.regexes.map((s=>s[1]));this.matcherRe=langRe(function join(s,o=\"|\"){let i=0;return s.map((s=>{i+=1;const o=i;let u=source(s),_=\"\";for(;u.length>0;){const s=a.exec(u);if(!s){_+=u;break}_+=u.substring(0,s.index),u=u.substring(s.index+s[0].length),\"\\\\\"===s[0][0]&&s[1]?_+=\"\\\\\"+String(Number(s[1])+o):(_+=s[0],\"(\"===s[0]&&i++)}return _})).map((s=>`(${s})`)).join(o)}(s),!0),this.lastIndex=0}exec(s){this.matcherRe.lastIndex=this.lastIndex;const o=this.matcherRe.exec(s);if(!o)return null;const i=o.findIndex(((s,o)=>o>0&&void 0!==s)),a=this.matchIndexes[i];return o.splice(0,i),Object.assign(o,a)}}class ResumableMultiRegex{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(s){if(this.multiRegexes[s])return this.multiRegexes[s];const o=new MultiRegex;return this.rules.slice(s).forEach((([s,i])=>o.addRule(s,i))),o.compile(),this.multiRegexes[s]=o,o}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(s,o){this.rules.push([s,o]),\"begin\"===o.type&&this.count++}exec(s){const o=this.getMatcher(this.regexIndex);o.lastIndex=this.lastIndex;let i=o.exec(s);if(this.resumingScanAtSamePosition())if(i&&i.index===this.lastIndex);else{const o=this.getMatcher(0);o.lastIndex=this.lastIndex+1,i=o.exec(s)}return i&&(this.regexIndex+=i.position+1,this.regexIndex===this.count&&this.considerAll()),i}}if(s.compilerExtensions||(s.compilerExtensions=[]),s.contains&&s.contains.includes(\"self\"))throw new Error(\"ERR: contains `self` is not supported at the top-level of a language.  See documentation.\");return s.classNameAliases=inherit(s.classNameAliases||{}),function compileMode(o,i){const a=o;if(o.isCompiled)return a;[compileMatch].forEach((s=>s(o,i))),s.compilerExtensions.forEach((s=>s(o,i))),o.__beforeBegin=null,[beginKeywords,compileIllegal,compileRelevance].forEach((s=>s(o,i))),o.isCompiled=!0;let u=null;if(\"object\"==typeof o.keywords&&(u=o.keywords.$pattern,delete o.keywords.$pattern),o.keywords&&(o.keywords=compileKeywords(o.keywords,s.case_insensitive)),o.lexemes&&u)throw new Error(\"ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) \");return u=u||o.lexemes||/\\w+/,a.keywordPatternRe=langRe(u,!0),i&&(o.begin||(o.begin=/\\B|\\b/),a.beginRe=langRe(o.begin),o.endSameAsBegin&&(o.end=o.begin),o.end||o.endsWithParent||(o.end=/\\B|\\b/),o.end&&(a.endRe=langRe(o.end)),a.terminatorEnd=source(o.end)||\"\",o.endsWithParent&&i.terminatorEnd&&(a.terminatorEnd+=(o.end?\"|\":\"\")+i.terminatorEnd)),o.illegal&&(a.illegalRe=langRe(o.illegal)),o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((function(s){return function expandOrCloneMode(s){s.variants&&!s.cachedVariants&&(s.cachedVariants=s.variants.map((function(o){return inherit(s,{variants:null},o)})));if(s.cachedVariants)return s.cachedVariants;if(dependencyOnParent(s))return inherit(s,{starts:s.starts?inherit(s.starts):null});if(Object.isFrozen(s))return inherit(s);return s}(\"self\"===s?o:s)}))),o.contains.forEach((function(s){compileMode(s,a)})),o.starts&&compileMode(o.starts,i),a.matcher=function buildModeRegex(s){const o=new ResumableMultiRegex;return s.contains.forEach((s=>o.addRule(s.begin,{rule:s,type:\"begin\"}))),s.terminatorEnd&&o.addRule(s.terminatorEnd,{type:\"end\"}),s.illegal&&o.addRule(s.illegal,{type:\"illegal\"}),o}(a),a}(s)}function dependencyOnParent(s){return!!s&&(s.endsWithParent||dependencyOnParent(s.starts))}function BuildVuePlugin(s){const o={props:[\"language\",\"code\",\"autodetect\"],data:function(){return{detectedLanguage:\"\",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?\"\":\"hljs \"+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!s.getLanguage(this.language))return console.warn(`The language \"${this.language}\" you specified could not be found.`),this.unknownLanguage=!0,escapeHTML(this.code);let o={};return this.autoDetect?(o=s.highlightAuto(this.code),this.detectedLanguage=o.language):(o=s.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),o.value},autoDetect(){return!this.language||function hasValueOrEmptyAttribute(s){return Boolean(s||\"\"===s)}(this.autodetect)},ignoreIllegals:()=>!0},render(s){return s(\"pre\",{},[s(\"code\",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:o,VuePlugin:{install(s){s.component(\"highlightjs\",o)}}}}const ye={\"after:highlightElement\":({el:s,result:o,text:i})=>{const a=nodeStream(s);if(!a.length)return;const u=document.createElement(\"div\");u.innerHTML=o.value,o.value=function mergeStreams(s,o,i){let a=0,u=\"\";const _=[];function selectStream(){return s.length&&o.length?s[0].offset!==o[0].offset?s[0].offset<o[0].offset?s:o:\"start\"===o[0].event?s:o:s.length?s:o}function open(s){function attributeString(s){return\" \"+s.nodeName+'=\"'+escapeHTML(s.value)+'\"'}u+=\"<\"+tag(s)+[].map.call(s.attributes,attributeString).join(\"\")+\">\"}function close(s){u+=\"</\"+tag(s)+\">\"}function render(s){(\"start\"===s.event?open:close)(s.node)}for(;s.length||o.length;){let o=selectStream();if(u+=escapeHTML(i.substring(a,o[0].offset)),a=o[0].offset,o===s){_.reverse().forEach(close);do{render(o.splice(0,1)[0]),o=selectStream()}while(o===s&&o.length&&o[0].offset===a);_.reverse().forEach(open)}else\"start\"===o[0].event?_.push(o[0].node):_.pop(),render(o.splice(0,1)[0])}return u+escapeHTML(i.substr(a))}(a,nodeStream(u),i)}};function tag(s){return s.nodeName.toLowerCase()}function nodeStream(s){const o=[];return function _nodeStream(s,i){for(let a=s.firstChild;a;a=a.nextSibling)3===a.nodeType?i+=a.nodeValue.length:1===a.nodeType&&(o.push({event:\"start\",offset:i,node:a}),i=_nodeStream(a,i),tag(a).match(/br|hr|img|input/)||o.push({event:\"stop\",offset:i,node:a}));return i}(s,0),o}const be={},error=s=>{console.error(s)},warn=(s,...o)=>{console.log(`WARN: ${s}`,...o)},deprecated=(s,o)=>{be[`${s}/${o}`]||(console.log(`Deprecated as of ${s}. ${o}`),be[`${s}/${o}`]=!0)},_e=escapeHTML,Se=inherit,we=Symbol(\"nomatch\");var xe=function(s){const i=Object.create(null),a=Object.create(null),u=[];let _=!0;const w=/(^(<[^>]+>|\\t|)+|\\n)/gm,x=\"Could not find the language '{}', did you forget to load/include a language module?\",C={disableAutodetect:!0,name:\"Plain text\",contains:[]};let j={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\\blang(?:uage)?-([\\w-]+)\\b/i,classPrefix:\"hljs-\",tabReplace:null,useBR:!1,languages:null,__emitter:TokenTreeEmitter};function shouldNotHighlight(s){return j.noHighlightRe.test(s)}function highlight(s,o,i,a){let u=\"\",_=\"\";\"object\"==typeof o?(u=s,i=o.ignoreIllegals,_=o.language,a=void 0):(deprecated(\"10.7.0\",\"highlight(lang, code, ...args) has been deprecated.\"),deprecated(\"10.7.0\",\"Please use highlight(code, options) instead.\\nhttps://github.com/highlightjs/highlight.js/issues/2277\"),_=s,u=o);const w={code:u,language:_};fire(\"before:highlight\",w);const x=w.result?w.result:_highlight(w.language,w.code,i,a);return x.code=w.code,fire(\"after:highlight\",x),x}function _highlight(s,o,a,w){function keywordData(s,o){const i=L.case_insensitive?o[0].toLowerCase():o[0];return Object.prototype.hasOwnProperty.call(s.keywords,i)&&s.keywords[i]}function processBuffer(){null!=U.subLanguage?function processSubLanguage(){if(\"\"===Y)return;let s=null;if(\"string\"==typeof U.subLanguage){if(!i[U.subLanguage])return void z.addText(Y);s=_highlight(U.subLanguage,Y,!0,V[U.subLanguage]),V[U.subLanguage]=s.top}else s=highlightAuto(Y,U.subLanguage.length?U.subLanguage:null);U.relevance>0&&(Z+=s.relevance),z.addSublanguage(s.emitter,s.language)}():function processKeywords(){if(!U.keywords)return void z.addText(Y);let s=0;U.keywordPatternRe.lastIndex=0;let o=U.keywordPatternRe.exec(Y),i=\"\";for(;o;){i+=Y.substring(s,o.index);const a=keywordData(U,o);if(a){const[s,u]=a;if(z.addText(i),i=\"\",Z+=u,s.startsWith(\"_\"))i+=o[0];else{const i=L.classNameAliases[s]||s;z.addKeyword(o[0],i)}}else i+=o[0];s=U.keywordPatternRe.lastIndex,o=U.keywordPatternRe.exec(Y)}i+=Y.substr(s),z.addText(i)}(),Y=\"\"}function startNewMode(s){return s.className&&z.openNode(L.classNameAliases[s.className]||s.className),U=Object.create(s,{parent:{value:U}}),U}function endOfMode(s,o,i){let a=function startsWith(s,o){const i=s&&s.exec(o);return i&&0===i.index}(s.endRe,i);if(a){if(s[\"on:end\"]){const i=new Response(s);s[\"on:end\"](o,i),i.isMatchIgnored&&(a=!1)}if(a){for(;s.endsParent&&s.parent;)s=s.parent;return s}}if(s.endsWithParent)return endOfMode(s.parent,o,i)}function doIgnore(s){return 0===U.matcher.regexIndex?(Y+=s[0],1):(ae=!0,0)}function doBeginMatch(s){const o=s[0],i=s.rule,a=new Response(i),u=[i.__beforeBegin,i[\"on:begin\"]];for(const i of u)if(i&&(i(s,a),a.isMatchIgnored))return doIgnore(o);return i&&i.endSameAsBegin&&(i.endRe=function escape(s){return new RegExp(s.replace(/[-/\\\\^$*+?.()|[\\]{}]/g,\"\\\\$&\"),\"m\")}(o)),i.skip?Y+=o:(i.excludeBegin&&(Y+=o),processBuffer(),i.returnBegin||i.excludeBegin||(Y=o)),startNewMode(i),i.returnBegin?0:o.length}function doEndMatch(s){const i=s[0],a=o.substr(s.index),u=endOfMode(U,s,a);if(!u)return we;const _=U;_.skip?Y+=i:(_.returnEnd||_.excludeEnd||(Y+=i),processBuffer(),_.excludeEnd&&(Y=i));do{U.className&&z.closeNode(),U.skip||U.subLanguage||(Z+=U.relevance),U=U.parent}while(U!==u.parent);return u.starts&&(u.endSameAsBegin&&(u.starts.endRe=u.endRe),startNewMode(u.starts)),_.returnEnd?0:i.length}let C={};function processLexeme(i,u){const w=u&&u[0];if(Y+=i,null==w)return processBuffer(),0;if(\"begin\"===C.type&&\"end\"===u.type&&C.index===u.index&&\"\"===w){if(Y+=o.slice(u.index,u.index+1),!_){const o=new Error(\"0 width match regex\");throw o.languageName=s,o.badRule=C.rule,o}return 1}if(C=u,\"begin\"===u.type)return doBeginMatch(u);if(\"illegal\"===u.type&&!a){const s=new Error('Illegal lexeme \"'+w+'\" for mode \"'+(U.className||\"<unnamed>\")+'\"');throw s.mode=U,s}if(\"end\"===u.type){const s=doEndMatch(u);if(s!==we)return s}if(\"illegal\"===u.type&&\"\"===w)return 1;if(ie>1e5&&ie>3*u.index){throw new Error(\"potential infinite loop, way more iterations than matches\")}return Y+=w,w.length}const L=getLanguage(s);if(!L)throw error(x.replace(\"{}\",s)),new Error('Unknown language: \"'+s+'\"');const B=compileLanguage(L,{plugins:u});let $=\"\",U=w||B;const V={},z=new j.__emitter(j);!function processContinuations(){const s=[];for(let o=U;o!==L;o=o.parent)o.className&&s.unshift(o.className);s.forEach((s=>z.openNode(s)))}();let Y=\"\",Z=0,ee=0,ie=0,ae=!1;try{for(U.matcher.considerAll();;){ie++,ae?ae=!1:U.matcher.considerAll(),U.matcher.lastIndex=ee;const s=U.matcher.exec(o);if(!s)break;const i=processLexeme(o.substring(ee,s.index),s);ee=s.index+i}return processLexeme(o.substr(ee)),z.closeAllNodes(),z.finalize(),$=z.toHTML(),{relevance:Math.floor(Z),value:$,language:s,illegal:!1,emitter:z,top:U}}catch(i){if(i.message&&i.message.includes(\"Illegal\"))return{illegal:!0,illegalBy:{msg:i.message,context:o.slice(ee-100,ee+100),mode:i.mode},sofar:$,relevance:0,value:_e(o),emitter:z};if(_)return{illegal:!1,relevance:0,value:_e(o),emitter:z,language:s,top:U,errorRaised:i};throw i}}function highlightAuto(s,o){o=o||j.languages||Object.keys(i);const a=function justTextHighlightResult(s){const o={relevance:0,emitter:new j.__emitter(j),value:_e(s),illegal:!1,top:C};return o.emitter.addText(s),o}(s),u=o.filter(getLanguage).filter(autoDetection).map((o=>_highlight(o,s,!1)));u.unshift(a);const _=u.sort(((s,o)=>{if(s.relevance!==o.relevance)return o.relevance-s.relevance;if(s.language&&o.language){if(getLanguage(s.language).supersetOf===o.language)return 1;if(getLanguage(o.language).supersetOf===s.language)return-1}return 0})),[w,x]=_,L=w;return L.second_best=x,L}const L={\"before:highlightElement\":({el:s})=>{j.useBR&&(s.innerHTML=s.innerHTML.replace(/\\n/g,\"\").replace(/<br[ /]*>/g,\"\\n\"))},\"after:highlightElement\":({result:s})=>{j.useBR&&(s.value=s.value.replace(/\\n/g,\"<br>\"))}},B=/^(<[^>]+>|\\t)+/gm,$={\"after:highlightElement\":({result:s})=>{j.tabReplace&&(s.value=s.value.replace(B,(s=>s.replace(/\\t/g,j.tabReplace))))}};function highlightElement(s){let o=null;const i=function blockLanguage(s){let o=s.className+\" \";o+=s.parentNode?s.parentNode.className:\"\";const i=j.languageDetectRe.exec(o);if(i){const o=getLanguage(i[1]);return o||(warn(x.replace(\"{}\",i[1])),warn(\"Falling back to no-highlight mode for this block.\",s)),o?i[1]:\"no-highlight\"}return o.split(/\\s+/).find((s=>shouldNotHighlight(s)||getLanguage(s)))}(s);if(shouldNotHighlight(i))return;fire(\"before:highlightElement\",{el:s,language:i}),o=s;const u=o.textContent,_=i?highlight(u,{language:i,ignoreIllegals:!0}):highlightAuto(u);fire(\"after:highlightElement\",{el:s,result:_,text:u}),s.innerHTML=_.value,function updateClassName(s,o,i){const u=o?a[o]:i;s.classList.add(\"hljs\"),u&&s.classList.add(u)}(s,i,_.language),s.result={language:_.language,re:_.relevance,relavance:_.relevance},_.second_best&&(s.second_best={language:_.second_best.language,re:_.second_best.relevance,relavance:_.second_best.relevance})}const initHighlighting=()=>{if(initHighlighting.called)return;initHighlighting.called=!0,deprecated(\"10.6.0\",\"initHighlighting() is deprecated.  Use highlightAll() instead.\");document.querySelectorAll(\"pre code\").forEach(highlightElement)};let U=!1;function highlightAll(){if(\"loading\"===document.readyState)return void(U=!0);document.querySelectorAll(\"pre code\").forEach(highlightElement)}function getLanguage(s){return s=(s||\"\").toLowerCase(),i[s]||i[a[s]]}function registerAliases(s,{languageName:o}){\"string\"==typeof s&&(s=[s]),s.forEach((s=>{a[s.toLowerCase()]=o}))}function autoDetection(s){const o=getLanguage(s);return o&&!o.disableAutodetect}function fire(s,o){const i=s;u.forEach((function(s){s[i]&&s[i](o)}))}\"undefined\"!=typeof window&&window.addEventListener&&window.addEventListener(\"DOMContentLoaded\",(function boot(){U&&highlightAll()}),!1),Object.assign(s,{highlight,highlightAuto,highlightAll,fixMarkup:function deprecateFixMarkup(s){return deprecated(\"10.2.0\",\"fixMarkup will be removed entirely in v11.0\"),deprecated(\"10.2.0\",\"Please see https://github.com/highlightjs/highlight.js/issues/2534\"),function fixMarkup(s){return j.tabReplace||j.useBR?s.replace(w,(s=>\"\\n\"===s?j.useBR?\"<br>\":s:j.tabReplace?s.replace(/\\t/g,j.tabReplace):s)):s}(s)},highlightElement,highlightBlock:function deprecateHighlightBlock(s){return deprecated(\"10.7.0\",\"highlightBlock will be removed entirely in v12.0\"),deprecated(\"10.7.0\",\"Please use highlightElement now.\"),highlightElement(s)},configure:function configure(s){s.useBR&&(deprecated(\"10.3.0\",\"'useBR' will be removed entirely in v11.0\"),deprecated(\"10.3.0\",\"Please see https://github.com/highlightjs/highlight.js/issues/2559\")),j=Se(j,s)},initHighlighting,initHighlightingOnLoad:function initHighlightingOnLoad(){deprecated(\"10.6.0\",\"initHighlightingOnLoad() is deprecated.  Use highlightAll() instead.\"),U=!0},registerLanguage:function registerLanguage(o,a){let u=null;try{u=a(s)}catch(s){if(error(\"Language definition for '{}' could not be registered.\".replace(\"{}\",o)),!_)throw s;error(s),u=C}u.name||(u.name=o),i[o]=u,u.rawDefinition=a.bind(null,s),u.aliases&&registerAliases(u.aliases,{languageName:o})},unregisterLanguage:function unregisterLanguage(s){delete i[s];for(const o of Object.keys(a))a[o]===s&&delete a[o]},listLanguages:function listLanguages(){return Object.keys(i)},getLanguage,registerAliases,requireLanguage:function requireLanguage(s){deprecated(\"10.4.0\",\"requireLanguage will be removed entirely in v11.\"),deprecated(\"10.4.0\",\"Please see https://github.com/highlightjs/highlight.js/pull/2844\");const o=getLanguage(s);if(o)return o;throw new Error(\"The '{}' language is required, but not loaded.\".replace(\"{}\",s))},autoDetection,inherit:Se,addPlugin:function addPlugin(s){!function upgradePluginAPI(s){s[\"before:highlightBlock\"]&&!s[\"before:highlightElement\"]&&(s[\"before:highlightElement\"]=o=>{s[\"before:highlightBlock\"](Object.assign({block:o.el},o))}),s[\"after:highlightBlock\"]&&!s[\"after:highlightElement\"]&&(s[\"after:highlightElement\"]=o=>{s[\"after:highlightBlock\"](Object.assign({block:o.el},o))})}(s),u.push(s)},vuePlugin:BuildVuePlugin(s).VuePlugin}),s.debugMode=function(){_=!1},s.safeMode=function(){_=!0},s.versionString=\"10.7.3\";for(const s in de)\"object\"==typeof de[s]&&o(de[s]);return Object.assign(s,de),s.addPlugin(L),s.addPlugin(ye),s.addPlugin($),s}({});s.exports=xe},46028:(s,o,i)=>{\"use strict\";var a=i(13930),u=i(46285),_=i(25594),w=i(29367),x=i(60581),C=i(76264),j=TypeError,L=C(\"toPrimitive\");s.exports=function(s,o){if(!u(s)||_(s))return s;var i,C=w(s,L);if(C){if(void 0===o&&(o=\"default\"),i=a(C,s,o),!u(i)||_(i))return i;throw new j(\"Can't convert object to primitive value\")}return void 0===o&&(o=\"number\"),x(s,o)}},46076:(s,o,i)=>{\"use strict\";i(91599);var a=i(68623);s.exports=a},46285:(s,o,i)=>{\"use strict\";var a=i(62250);s.exports=function(s){return\"object\"==typeof s?null!==s:a(s)}},46942:(s,o)=>{var i;!function(){\"use strict\";var a={}.hasOwnProperty;function classNames(){for(var s=\"\",o=0;o<arguments.length;o++){var i=arguments[o];i&&(s=appendClass(s,parseValue(i)))}return s}function parseValue(s){if(\"string\"==typeof s||\"number\"==typeof s)return s;if(\"object\"!=typeof s)return\"\";if(Array.isArray(s))return classNames.apply(null,s);if(s.toString!==Object.prototype.toString&&!s.toString.toString().includes(\"[native code]\"))return s.toString();var o=\"\";for(var i in s)a.call(s,i)&&s[i]&&(o=appendClass(o,i));return o}function appendClass(s,o){return o?s?s+\" \"+o:s+o:s}s.exports?(classNames.default=classNames,s.exports=classNames):void 0===(i=function(){return classNames}.apply(o,[]))||(s.exports=i)}()},47119:s=>{\"use strict\";s.exports=\"undefined\"!=typeof Reflect&&Reflect&&Reflect.apply},47181:(s,o,i)=>{\"use strict\";var a=i(95116).IteratorPrototype,u=i(58075),_=i(75817),w=i(14840),x=i(93742),returnThis=function(){return this};s.exports=function(s,o,i,C){var j=o+\" Iterator\";return s.prototype=u(a,{next:_(+!C,i)}),w(s,j,!1,!0),x[j]=returnThis,s}},47237:s=>{s.exports=function baseProperty(s){return function(o){return null==o?void 0:o[s]}}},47248:(s,o,i)=>{var a=i(16547),u=i(51234);s.exports=function zipObject(s,o){return u(s||[],o||[],a)}},47422:(s,o,i)=>{var a=i(31769),u=i(77797);s.exports=function baseGet(s,o){for(var i=0,_=(o=a(o,s)).length;null!=s&&i<_;)s=s[u(o[i++])];return i&&i==_?s:void 0}},47473:s=>{var o=Function.prototype.toString;s.exports=function toSource(s){if(null!=s){try{return o.call(s)}catch(s){}try{return s+\"\"}catch(s){}}return\"\"}},47886:(s,o,i)=>{var a=i(5861),u=i(40346);s.exports=function isWeakMap(s){return u(s)&&\"[object WeakMap]\"==a(s)}},47934:(s,o,i)=>{s.exports={ary:i(64626),assign:i(74733),clone:i(32629),curry:i(49747),forEach:i(83729),isArray:i(56449),isError:i(23546),isFunction:i(1882),isWeakMap:i(47886),iteratee:i(33855),keys:i(88984),rearg:i(84195),toInteger:i(61489),toPath:i(42072)}},48152:(s,o,i)=>{var a=i(28303),u=a&&new a;s.exports=u},48287:(s,o,i)=>{\"use strict\";const a=i(67526),u=i(251),_=\"function\"==typeof Symbol&&\"function\"==typeof Symbol.for?Symbol.for(\"nodejs.util.inspect.custom\"):null;o.Buffer=Buffer,o.SlowBuffer=function SlowBuffer(s){+s!=s&&(s=0);return Buffer.alloc(+s)},o.INSPECT_MAX_BYTES=50;const w=2147483647;function createBuffer(s){if(s>w)throw new RangeError('The value \"'+s+'\" is invalid for option \"size\"');const o=new Uint8Array(s);return Object.setPrototypeOf(o,Buffer.prototype),o}function Buffer(s,o,i){if(\"number\"==typeof s){if(\"string\"==typeof o)throw new TypeError('The \"string\" argument must be of type string. Received type number');return allocUnsafe(s)}return from(s,o,i)}function from(s,o,i){if(\"string\"==typeof s)return function fromString(s,o){\"string\"==typeof o&&\"\"!==o||(o=\"utf8\");if(!Buffer.isEncoding(o))throw new TypeError(\"Unknown encoding: \"+o);const i=0|byteLength(s,o);let a=createBuffer(i);const u=a.write(s,o);u!==i&&(a=a.slice(0,u));return a}(s,o);if(ArrayBuffer.isView(s))return function fromArrayView(s){if(isInstance(s,Uint8Array)){const o=new Uint8Array(s);return fromArrayBuffer(o.buffer,o.byteOffset,o.byteLength)}return fromArrayLike(s)}(s);if(null==s)throw new TypeError(\"The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type \"+typeof s);if(isInstance(s,ArrayBuffer)||s&&isInstance(s.buffer,ArrayBuffer))return fromArrayBuffer(s,o,i);if(\"undefined\"!=typeof SharedArrayBuffer&&(isInstance(s,SharedArrayBuffer)||s&&isInstance(s.buffer,SharedArrayBuffer)))return fromArrayBuffer(s,o,i);if(\"number\"==typeof s)throw new TypeError('The \"value\" argument must not be of type number. Received type number');const a=s.valueOf&&s.valueOf();if(null!=a&&a!==s)return Buffer.from(a,o,i);const u=function fromObject(s){if(Buffer.isBuffer(s)){const o=0|checked(s.length),i=createBuffer(o);return 0===i.length||s.copy(i,0,0,o),i}if(void 0!==s.length)return\"number\"!=typeof s.length||numberIsNaN(s.length)?createBuffer(0):fromArrayLike(s);if(\"Buffer\"===s.type&&Array.isArray(s.data))return fromArrayLike(s.data)}(s);if(u)return u;if(\"undefined\"!=typeof Symbol&&null!=Symbol.toPrimitive&&\"function\"==typeof s[Symbol.toPrimitive])return Buffer.from(s[Symbol.toPrimitive](\"string\"),o,i);throw new TypeError(\"The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type \"+typeof s)}function assertSize(s){if(\"number\"!=typeof s)throw new TypeError('\"size\" argument must be of type number');if(s<0)throw new RangeError('The value \"'+s+'\" is invalid for option \"size\"')}function allocUnsafe(s){return assertSize(s),createBuffer(s<0?0:0|checked(s))}function fromArrayLike(s){const o=s.length<0?0:0|checked(s.length),i=createBuffer(o);for(let a=0;a<o;a+=1)i[a]=255&s[a];return i}function fromArrayBuffer(s,o,i){if(o<0||s.byteLength<o)throw new RangeError('\"offset\" is outside of buffer bounds');if(s.byteLength<o+(i||0))throw new RangeError('\"length\" is outside of buffer bounds');let a;return a=void 0===o&&void 0===i?new Uint8Array(s):void 0===i?new Uint8Array(s,o):new Uint8Array(s,o,i),Object.setPrototypeOf(a,Buffer.prototype),a}function checked(s){if(s>=w)throw new RangeError(\"Attempt to allocate Buffer larger than maximum size: 0x\"+w.toString(16)+\" bytes\");return 0|s}function byteLength(s,o){if(Buffer.isBuffer(s))return s.length;if(ArrayBuffer.isView(s)||isInstance(s,ArrayBuffer))return s.byteLength;if(\"string\"!=typeof s)throw new TypeError('The \"string\" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof s);const i=s.length,a=arguments.length>2&&!0===arguments[2];if(!a&&0===i)return 0;let u=!1;for(;;)switch(o){case\"ascii\":case\"latin1\":case\"binary\":return i;case\"utf8\":case\"utf-8\":return utf8ToBytes(s).length;case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return 2*i;case\"hex\":return i>>>1;case\"base64\":return base64ToBytes(s).length;default:if(u)return a?-1:utf8ToBytes(s).length;o=(\"\"+o).toLowerCase(),u=!0}}function slowToString(s,o,i){let a=!1;if((void 0===o||o<0)&&(o=0),o>this.length)return\"\";if((void 0===i||i>this.length)&&(i=this.length),i<=0)return\"\";if((i>>>=0)<=(o>>>=0))return\"\";for(s||(s=\"utf8\");;)switch(s){case\"hex\":return hexSlice(this,o,i);case\"utf8\":case\"utf-8\":return utf8Slice(this,o,i);case\"ascii\":return asciiSlice(this,o,i);case\"latin1\":case\"binary\":return latin1Slice(this,o,i);case\"base64\":return base64Slice(this,o,i);case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return utf16leSlice(this,o,i);default:if(a)throw new TypeError(\"Unknown encoding: \"+s);s=(s+\"\").toLowerCase(),a=!0}}function swap(s,o,i){const a=s[o];s[o]=s[i],s[i]=a}function bidirectionalIndexOf(s,o,i,a,u){if(0===s.length)return-1;if(\"string\"==typeof i?(a=i,i=0):i>2147483647?i=2147483647:i<-2147483648&&(i=-2147483648),numberIsNaN(i=+i)&&(i=u?0:s.length-1),i<0&&(i=s.length+i),i>=s.length){if(u)return-1;i=s.length-1}else if(i<0){if(!u)return-1;i=0}if(\"string\"==typeof o&&(o=Buffer.from(o,a)),Buffer.isBuffer(o))return 0===o.length?-1:arrayIndexOf(s,o,i,a,u);if(\"number\"==typeof o)return o&=255,\"function\"==typeof Uint8Array.prototype.indexOf?u?Uint8Array.prototype.indexOf.call(s,o,i):Uint8Array.prototype.lastIndexOf.call(s,o,i):arrayIndexOf(s,[o],i,a,u);throw new TypeError(\"val must be string, number or Buffer\")}function arrayIndexOf(s,o,i,a,u){let _,w=1,x=s.length,C=o.length;if(void 0!==a&&(\"ucs2\"===(a=String(a).toLowerCase())||\"ucs-2\"===a||\"utf16le\"===a||\"utf-16le\"===a)){if(s.length<2||o.length<2)return-1;w=2,x/=2,C/=2,i/=2}function read(s,o){return 1===w?s[o]:s.readUInt16BE(o*w)}if(u){let a=-1;for(_=i;_<x;_++)if(read(s,_)===read(o,-1===a?0:_-a)){if(-1===a&&(a=_),_-a+1===C)return a*w}else-1!==a&&(_-=_-a),a=-1}else for(i+C>x&&(i=x-C),_=i;_>=0;_--){let i=!0;for(let a=0;a<C;a++)if(read(s,_+a)!==read(o,a)){i=!1;break}if(i)return _}return-1}function hexWrite(s,o,i,a){i=Number(i)||0;const u=s.length-i;a?(a=Number(a))>u&&(a=u):a=u;const _=o.length;let w;for(a>_/2&&(a=_/2),w=0;w<a;++w){const a=parseInt(o.substr(2*w,2),16);if(numberIsNaN(a))return w;s[i+w]=a}return w}function utf8Write(s,o,i,a){return blitBuffer(utf8ToBytes(o,s.length-i),s,i,a)}function asciiWrite(s,o,i,a){return blitBuffer(function asciiToBytes(s){const o=[];for(let i=0;i<s.length;++i)o.push(255&s.charCodeAt(i));return o}(o),s,i,a)}function base64Write(s,o,i,a){return blitBuffer(base64ToBytes(o),s,i,a)}function ucs2Write(s,o,i,a){return blitBuffer(function utf16leToBytes(s,o){let i,a,u;const _=[];for(let w=0;w<s.length&&!((o-=2)<0);++w)i=s.charCodeAt(w),a=i>>8,u=i%256,_.push(u),_.push(a);return _}(o,s.length-i),s,i,a)}function base64Slice(s,o,i){return 0===o&&i===s.length?a.fromByteArray(s):a.fromByteArray(s.slice(o,i))}function utf8Slice(s,o,i){i=Math.min(s.length,i);const a=[];let u=o;for(;u<i;){const o=s[u];let _=null,w=o>239?4:o>223?3:o>191?2:1;if(u+w<=i){let i,a,x,C;switch(w){case 1:o<128&&(_=o);break;case 2:i=s[u+1],128==(192&i)&&(C=(31&o)<<6|63&i,C>127&&(_=C));break;case 3:i=s[u+1],a=s[u+2],128==(192&i)&&128==(192&a)&&(C=(15&o)<<12|(63&i)<<6|63&a,C>2047&&(C<55296||C>57343)&&(_=C));break;case 4:i=s[u+1],a=s[u+2],x=s[u+3],128==(192&i)&&128==(192&a)&&128==(192&x)&&(C=(15&o)<<18|(63&i)<<12|(63&a)<<6|63&x,C>65535&&C<1114112&&(_=C))}}null===_?(_=65533,w=1):_>65535&&(_-=65536,a.push(_>>>10&1023|55296),_=56320|1023&_),a.push(_),u+=w}return function decodeCodePointsArray(s){const o=s.length;if(o<=x)return String.fromCharCode.apply(String,s);let i=\"\",a=0;for(;a<o;)i+=String.fromCharCode.apply(String,s.slice(a,a+=x));return i}(a)}o.kMaxLength=w,Buffer.TYPED_ARRAY_SUPPORT=function typedArraySupport(){try{const s=new Uint8Array(1),o={foo:function(){return 42}};return Object.setPrototypeOf(o,Uint8Array.prototype),Object.setPrototypeOf(s,o),42===s.foo()}catch(s){return!1}}(),Buffer.TYPED_ARRAY_SUPPORT||\"undefined\"==typeof console||\"function\"!=typeof console.error||console.error(\"This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support.\"),Object.defineProperty(Buffer.prototype,\"parent\",{enumerable:!0,get:function(){if(Buffer.isBuffer(this))return this.buffer}}),Object.defineProperty(Buffer.prototype,\"offset\",{enumerable:!0,get:function(){if(Buffer.isBuffer(this))return this.byteOffset}}),Buffer.poolSize=8192,Buffer.from=function(s,o,i){return from(s,o,i)},Object.setPrototypeOf(Buffer.prototype,Uint8Array.prototype),Object.setPrototypeOf(Buffer,Uint8Array),Buffer.alloc=function(s,o,i){return function alloc(s,o,i){return assertSize(s),s<=0?createBuffer(s):void 0!==o?\"string\"==typeof i?createBuffer(s).fill(o,i):createBuffer(s).fill(o):createBuffer(s)}(s,o,i)},Buffer.allocUnsafe=function(s){return allocUnsafe(s)},Buffer.allocUnsafeSlow=function(s){return allocUnsafe(s)},Buffer.isBuffer=function isBuffer(s){return null!=s&&!0===s._isBuffer&&s!==Buffer.prototype},Buffer.compare=function compare(s,o){if(isInstance(s,Uint8Array)&&(s=Buffer.from(s,s.offset,s.byteLength)),isInstance(o,Uint8Array)&&(o=Buffer.from(o,o.offset,o.byteLength)),!Buffer.isBuffer(s)||!Buffer.isBuffer(o))throw new TypeError('The \"buf1\", \"buf2\" arguments must be one of type Buffer or Uint8Array');if(s===o)return 0;let i=s.length,a=o.length;for(let u=0,_=Math.min(i,a);u<_;++u)if(s[u]!==o[u]){i=s[u],a=o[u];break}return i<a?-1:a<i?1:0},Buffer.isEncoding=function isEncoding(s){switch(String(s).toLowerCase()){case\"hex\":case\"utf8\":case\"utf-8\":case\"ascii\":case\"latin1\":case\"binary\":case\"base64\":case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return!0;default:return!1}},Buffer.concat=function concat(s,o){if(!Array.isArray(s))throw new TypeError('\"list\" argument must be an Array of Buffers');if(0===s.length)return Buffer.alloc(0);let i;if(void 0===o)for(o=0,i=0;i<s.length;++i)o+=s[i].length;const a=Buffer.allocUnsafe(o);let u=0;for(i=0;i<s.length;++i){let o=s[i];if(isInstance(o,Uint8Array))u+o.length>a.length?(Buffer.isBuffer(o)||(o=Buffer.from(o)),o.copy(a,u)):Uint8Array.prototype.set.call(a,o,u);else{if(!Buffer.isBuffer(o))throw new TypeError('\"list\" argument must be an Array of Buffers');o.copy(a,u)}u+=o.length}return a},Buffer.byteLength=byteLength,Buffer.prototype._isBuffer=!0,Buffer.prototype.swap16=function swap16(){const s=this.length;if(s%2!=0)throw new RangeError(\"Buffer size must be a multiple of 16-bits\");for(let o=0;o<s;o+=2)swap(this,o,o+1);return this},Buffer.prototype.swap32=function swap32(){const s=this.length;if(s%4!=0)throw new RangeError(\"Buffer size must be a multiple of 32-bits\");for(let o=0;o<s;o+=4)swap(this,o,o+3),swap(this,o+1,o+2);return this},Buffer.prototype.swap64=function swap64(){const s=this.length;if(s%8!=0)throw new RangeError(\"Buffer size must be a multiple of 64-bits\");for(let o=0;o<s;o+=8)swap(this,o,o+7),swap(this,o+1,o+6),swap(this,o+2,o+5),swap(this,o+3,o+4);return this},Buffer.prototype.toString=function toString(){const s=this.length;return 0===s?\"\":0===arguments.length?utf8Slice(this,0,s):slowToString.apply(this,arguments)},Buffer.prototype.toLocaleString=Buffer.prototype.toString,Buffer.prototype.equals=function equals(s){if(!Buffer.isBuffer(s))throw new TypeError(\"Argument must be a Buffer\");return this===s||0===Buffer.compare(this,s)},Buffer.prototype.inspect=function inspect(){let s=\"\";const i=o.INSPECT_MAX_BYTES;return s=this.toString(\"hex\",0,i).replace(/(.{2})/g,\"$1 \").trim(),this.length>i&&(s+=\" ... \"),\"<Buffer \"+s+\">\"},_&&(Buffer.prototype[_]=Buffer.prototype.inspect),Buffer.prototype.compare=function compare(s,o,i,a,u){if(isInstance(s,Uint8Array)&&(s=Buffer.from(s,s.offset,s.byteLength)),!Buffer.isBuffer(s))throw new TypeError('The \"target\" argument must be one of type Buffer or Uint8Array. Received type '+typeof s);if(void 0===o&&(o=0),void 0===i&&(i=s?s.length:0),void 0===a&&(a=0),void 0===u&&(u=this.length),o<0||i>s.length||a<0||u>this.length)throw new RangeError(\"out of range index\");if(a>=u&&o>=i)return 0;if(a>=u)return-1;if(o>=i)return 1;if(this===s)return 0;let _=(u>>>=0)-(a>>>=0),w=(i>>>=0)-(o>>>=0);const x=Math.min(_,w),C=this.slice(a,u),j=s.slice(o,i);for(let s=0;s<x;++s)if(C[s]!==j[s]){_=C[s],w=j[s];break}return _<w?-1:w<_?1:0},Buffer.prototype.includes=function includes(s,o,i){return-1!==this.indexOf(s,o,i)},Buffer.prototype.indexOf=function indexOf(s,o,i){return bidirectionalIndexOf(this,s,o,i,!0)},Buffer.prototype.lastIndexOf=function lastIndexOf(s,o,i){return bidirectionalIndexOf(this,s,o,i,!1)},Buffer.prototype.write=function write(s,o,i,a){if(void 0===o)a=\"utf8\",i=this.length,o=0;else if(void 0===i&&\"string\"==typeof o)a=o,i=this.length,o=0;else{if(!isFinite(o))throw new Error(\"Buffer.write(string, encoding, offset[, length]) is no longer supported\");o>>>=0,isFinite(i)?(i>>>=0,void 0===a&&(a=\"utf8\")):(a=i,i=void 0)}const u=this.length-o;if((void 0===i||i>u)&&(i=u),s.length>0&&(i<0||o<0)||o>this.length)throw new RangeError(\"Attempt to write outside buffer bounds\");a||(a=\"utf8\");let _=!1;for(;;)switch(a){case\"hex\":return hexWrite(this,s,o,i);case\"utf8\":case\"utf-8\":return utf8Write(this,s,o,i);case\"ascii\":case\"latin1\":case\"binary\":return asciiWrite(this,s,o,i);case\"base64\":return base64Write(this,s,o,i);case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return ucs2Write(this,s,o,i);default:if(_)throw new TypeError(\"Unknown encoding: \"+a);a=(\"\"+a).toLowerCase(),_=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:\"Buffer\",data:Array.prototype.slice.call(this._arr||this,0)}};const x=4096;function asciiSlice(s,o,i){let a=\"\";i=Math.min(s.length,i);for(let u=o;u<i;++u)a+=String.fromCharCode(127&s[u]);return a}function latin1Slice(s,o,i){let a=\"\";i=Math.min(s.length,i);for(let u=o;u<i;++u)a+=String.fromCharCode(s[u]);return a}function hexSlice(s,o,i){const a=s.length;(!o||o<0)&&(o=0),(!i||i<0||i>a)&&(i=a);let u=\"\";for(let a=o;a<i;++a)u+=L[s[a]];return u}function utf16leSlice(s,o,i){const a=s.slice(o,i);let u=\"\";for(let s=0;s<a.length-1;s+=2)u+=String.fromCharCode(a[s]+256*a[s+1]);return u}function checkOffset(s,o,i){if(s%1!=0||s<0)throw new RangeError(\"offset is not uint\");if(s+o>i)throw new RangeError(\"Trying to access beyond buffer length\")}function checkInt(s,o,i,a,u,_){if(!Buffer.isBuffer(s))throw new TypeError('\"buffer\" argument must be a Buffer instance');if(o>u||o<_)throw new RangeError('\"value\" argument is out of bounds');if(i+a>s.length)throw new RangeError(\"Index out of range\")}function wrtBigUInt64LE(s,o,i,a,u){checkIntBI(o,a,u,s,i,7);let _=Number(o&BigInt(4294967295));s[i++]=_,_>>=8,s[i++]=_,_>>=8,s[i++]=_,_>>=8,s[i++]=_;let w=Number(o>>BigInt(32)&BigInt(4294967295));return s[i++]=w,w>>=8,s[i++]=w,w>>=8,s[i++]=w,w>>=8,s[i++]=w,i}function wrtBigUInt64BE(s,o,i,a,u){checkIntBI(o,a,u,s,i,7);let _=Number(o&BigInt(4294967295));s[i+7]=_,_>>=8,s[i+6]=_,_>>=8,s[i+5]=_,_>>=8,s[i+4]=_;let w=Number(o>>BigInt(32)&BigInt(4294967295));return s[i+3]=w,w>>=8,s[i+2]=w,w>>=8,s[i+1]=w,w>>=8,s[i]=w,i+8}function checkIEEE754(s,o,i,a,u,_){if(i+a>s.length)throw new RangeError(\"Index out of range\");if(i<0)throw new RangeError(\"Index out of range\")}function writeFloat(s,o,i,a,_){return o=+o,i>>>=0,_||checkIEEE754(s,0,i,4),u.write(s,o,i,a,23,4),i+4}function writeDouble(s,o,i,a,_){return o=+o,i>>>=0,_||checkIEEE754(s,0,i,8),u.write(s,o,i,a,52,8),i+8}Buffer.prototype.slice=function slice(s,o){const i=this.length;(s=~~s)<0?(s+=i)<0&&(s=0):s>i&&(s=i),(o=void 0===o?i:~~o)<0?(o+=i)<0&&(o=0):o>i&&(o=i),o<s&&(o=s);const a=this.subarray(s,o);return Object.setPrototypeOf(a,Buffer.prototype),a},Buffer.prototype.readUintLE=Buffer.prototype.readUIntLE=function readUIntLE(s,o,i){s>>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s],u=1,_=0;for(;++_<o&&(u*=256);)a+=this[s+_]*u;return a},Buffer.prototype.readUintBE=Buffer.prototype.readUIntBE=function readUIntBE(s,o,i){s>>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s+--o],u=1;for(;o>0&&(u*=256);)a+=this[s+--o]*u;return a},Buffer.prototype.readUint8=Buffer.prototype.readUInt8=function readUInt8(s,o){return s>>>=0,o||checkOffset(s,1,this.length),this[s]},Buffer.prototype.readUint16LE=Buffer.prototype.readUInt16LE=function readUInt16LE(s,o){return s>>>=0,o||checkOffset(s,2,this.length),this[s]|this[s+1]<<8},Buffer.prototype.readUint16BE=Buffer.prototype.readUInt16BE=function readUInt16BE(s,o){return s>>>=0,o||checkOffset(s,2,this.length),this[s]<<8|this[s+1]},Buffer.prototype.readUint32LE=Buffer.prototype.readUInt32LE=function readUInt32LE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),(this[s]|this[s+1]<<8|this[s+2]<<16)+16777216*this[s+3]},Buffer.prototype.readUint32BE=Buffer.prototype.readUInt32BE=function readUInt32BE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),16777216*this[s]+(this[s+1]<<16|this[s+2]<<8|this[s+3])},Buffer.prototype.readBigUInt64LE=defineBigIntMethod((function readBigUInt64LE(s){validateNumber(s>>>=0,\"offset\");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=o+256*this[++s]+65536*this[++s]+this[++s]*2**24,u=this[++s]+256*this[++s]+65536*this[++s]+i*2**24;return BigInt(a)+(BigInt(u)<<BigInt(32))})),Buffer.prototype.readBigUInt64BE=defineBigIntMethod((function readBigUInt64BE(s){validateNumber(s>>>=0,\"offset\");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=o*2**24+65536*this[++s]+256*this[++s]+this[++s],u=this[++s]*2**24+65536*this[++s]+256*this[++s]+i;return(BigInt(a)<<BigInt(32))+BigInt(u)})),Buffer.prototype.readIntLE=function readIntLE(s,o,i){s>>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s],u=1,_=0;for(;++_<o&&(u*=256);)a+=this[s+_]*u;return u*=128,a>=u&&(a-=Math.pow(2,8*o)),a},Buffer.prototype.readIntBE=function readIntBE(s,o,i){s>>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=o,u=1,_=this[s+--a];for(;a>0&&(u*=256);)_+=this[s+--a]*u;return u*=128,_>=u&&(_-=Math.pow(2,8*o)),_},Buffer.prototype.readInt8=function readInt8(s,o){return s>>>=0,o||checkOffset(s,1,this.length),128&this[s]?-1*(255-this[s]+1):this[s]},Buffer.prototype.readInt16LE=function readInt16LE(s,o){s>>>=0,o||checkOffset(s,2,this.length);const i=this[s]|this[s+1]<<8;return 32768&i?4294901760|i:i},Buffer.prototype.readInt16BE=function readInt16BE(s,o){s>>>=0,o||checkOffset(s,2,this.length);const i=this[s+1]|this[s]<<8;return 32768&i?4294901760|i:i},Buffer.prototype.readInt32LE=function readInt32LE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),this[s]|this[s+1]<<8|this[s+2]<<16|this[s+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),this[s]<<24|this[s+1]<<16|this[s+2]<<8|this[s+3]},Buffer.prototype.readBigInt64LE=defineBigIntMethod((function readBigInt64LE(s){validateNumber(s>>>=0,\"offset\");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=this[s+4]+256*this[s+5]+65536*this[s+6]+(i<<24);return(BigInt(a)<<BigInt(32))+BigInt(o+256*this[++s]+65536*this[++s]+this[++s]*2**24)})),Buffer.prototype.readBigInt64BE=defineBigIntMethod((function readBigInt64BE(s){validateNumber(s>>>=0,\"offset\");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=(o<<24)+65536*this[++s]+256*this[++s]+this[++s];return(BigInt(a)<<BigInt(32))+BigInt(this[++s]*2**24+65536*this[++s]+256*this[++s]+i)})),Buffer.prototype.readFloatLE=function readFloatLE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),u.read(this,s,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),u.read(this,s,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(s,o){return s>>>=0,o||checkOffset(s,8,this.length),u.read(this,s,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(s,o){return s>>>=0,o||checkOffset(s,8,this.length),u.read(this,s,!1,52,8)},Buffer.prototype.writeUintLE=Buffer.prototype.writeUIntLE=function writeUIntLE(s,o,i,a){if(s=+s,o>>>=0,i>>>=0,!a){checkInt(this,s,o,i,Math.pow(2,8*i)-1,0)}let u=1,_=0;for(this[o]=255&s;++_<i&&(u*=256);)this[o+_]=s/u&255;return o+i},Buffer.prototype.writeUintBE=Buffer.prototype.writeUIntBE=function writeUIntBE(s,o,i,a){if(s=+s,o>>>=0,i>>>=0,!a){checkInt(this,s,o,i,Math.pow(2,8*i)-1,0)}let u=i-1,_=1;for(this[o+u]=255&s;--u>=0&&(_*=256);)this[o+u]=s/_&255;return o+i},Buffer.prototype.writeUint8=Buffer.prototype.writeUInt8=function writeUInt8(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,1,255,0),this[o]=255&s,o+1},Buffer.prototype.writeUint16LE=Buffer.prototype.writeUInt16LE=function writeUInt16LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,65535,0),this[o]=255&s,this[o+1]=s>>>8,o+2},Buffer.prototype.writeUint16BE=Buffer.prototype.writeUInt16BE=function writeUInt16BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,65535,0),this[o]=s>>>8,this[o+1]=255&s,o+2},Buffer.prototype.writeUint32LE=Buffer.prototype.writeUInt32LE=function writeUInt32LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,4294967295,0),this[o+3]=s>>>24,this[o+2]=s>>>16,this[o+1]=s>>>8,this[o]=255&s,o+4},Buffer.prototype.writeUint32BE=Buffer.prototype.writeUInt32BE=function writeUInt32BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,4294967295,0),this[o]=s>>>24,this[o+1]=s>>>16,this[o+2]=s>>>8,this[o+3]=255&s,o+4},Buffer.prototype.writeBigUInt64LE=defineBigIntMethod((function writeBigUInt64LE(s,o=0){return wrtBigUInt64LE(this,s,o,BigInt(0),BigInt(\"0xffffffffffffffff\"))})),Buffer.prototype.writeBigUInt64BE=defineBigIntMethod((function writeBigUInt64BE(s,o=0){return wrtBigUInt64BE(this,s,o,BigInt(0),BigInt(\"0xffffffffffffffff\"))})),Buffer.prototype.writeIntLE=function writeIntLE(s,o,i,a){if(s=+s,o>>>=0,!a){const a=Math.pow(2,8*i-1);checkInt(this,s,o,i,a-1,-a)}let u=0,_=1,w=0;for(this[o]=255&s;++u<i&&(_*=256);)s<0&&0===w&&0!==this[o+u-1]&&(w=1),this[o+u]=(s/_|0)-w&255;return o+i},Buffer.prototype.writeIntBE=function writeIntBE(s,o,i,a){if(s=+s,o>>>=0,!a){const a=Math.pow(2,8*i-1);checkInt(this,s,o,i,a-1,-a)}let u=i-1,_=1,w=0;for(this[o+u]=255&s;--u>=0&&(_*=256);)s<0&&0===w&&0!==this[o+u+1]&&(w=1),this[o+u]=(s/_|0)-w&255;return o+i},Buffer.prototype.writeInt8=function writeInt8(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,1,127,-128),s<0&&(s=255+s+1),this[o]=255&s,o+1},Buffer.prototype.writeInt16LE=function writeInt16LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,32767,-32768),this[o]=255&s,this[o+1]=s>>>8,o+2},Buffer.prototype.writeInt16BE=function writeInt16BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,32767,-32768),this[o]=s>>>8,this[o+1]=255&s,o+2},Buffer.prototype.writeInt32LE=function writeInt32LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,2147483647,-2147483648),this[o]=255&s,this[o+1]=s>>>8,this[o+2]=s>>>16,this[o+3]=s>>>24,o+4},Buffer.prototype.writeInt32BE=function writeInt32BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,2147483647,-2147483648),s<0&&(s=4294967295+s+1),this[o]=s>>>24,this[o+1]=s>>>16,this[o+2]=s>>>8,this[o+3]=255&s,o+4},Buffer.prototype.writeBigInt64LE=defineBigIntMethod((function writeBigInt64LE(s,o=0){return wrtBigUInt64LE(this,s,o,-BigInt(\"0x8000000000000000\"),BigInt(\"0x7fffffffffffffff\"))})),Buffer.prototype.writeBigInt64BE=defineBigIntMethod((function writeBigInt64BE(s,o=0){return wrtBigUInt64BE(this,s,o,-BigInt(\"0x8000000000000000\"),BigInt(\"0x7fffffffffffffff\"))})),Buffer.prototype.writeFloatLE=function writeFloatLE(s,o,i){return writeFloat(this,s,o,!0,i)},Buffer.prototype.writeFloatBE=function writeFloatBE(s,o,i){return writeFloat(this,s,o,!1,i)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(s,o,i){return writeDouble(this,s,o,!0,i)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(s,o,i){return writeDouble(this,s,o,!1,i)},Buffer.prototype.copy=function copy(s,o,i,a){if(!Buffer.isBuffer(s))throw new TypeError(\"argument should be a Buffer\");if(i||(i=0),a||0===a||(a=this.length),o>=s.length&&(o=s.length),o||(o=0),a>0&&a<i&&(a=i),a===i)return 0;if(0===s.length||0===this.length)return 0;if(o<0)throw new RangeError(\"targetStart out of bounds\");if(i<0||i>=this.length)throw new RangeError(\"Index out of range\");if(a<0)throw new RangeError(\"sourceEnd out of bounds\");a>this.length&&(a=this.length),s.length-o<a-i&&(a=s.length-o+i);const u=a-i;return this===s&&\"function\"==typeof Uint8Array.prototype.copyWithin?this.copyWithin(o,i,a):Uint8Array.prototype.set.call(s,this.subarray(i,a),o),u},Buffer.prototype.fill=function fill(s,o,i,a){if(\"string\"==typeof s){if(\"string\"==typeof o?(a=o,o=0,i=this.length):\"string\"==typeof i&&(a=i,i=this.length),void 0!==a&&\"string\"!=typeof a)throw new TypeError(\"encoding must be a string\");if(\"string\"==typeof a&&!Buffer.isEncoding(a))throw new TypeError(\"Unknown encoding: \"+a);if(1===s.length){const o=s.charCodeAt(0);(\"utf8\"===a&&o<128||\"latin1\"===a)&&(s=o)}}else\"number\"==typeof s?s&=255:\"boolean\"==typeof s&&(s=Number(s));if(o<0||this.length<o||this.length<i)throw new RangeError(\"Out of range index\");if(i<=o)return this;let u;if(o>>>=0,i=void 0===i?this.length:i>>>0,s||(s=0),\"number\"==typeof s)for(u=o;u<i;++u)this[u]=s;else{const _=Buffer.isBuffer(s)?s:Buffer.from(s,a),w=_.length;if(0===w)throw new TypeError('The value \"'+s+'\" is invalid for argument \"value\"');for(u=0;u<i-o;++u)this[u+o]=_[u%w]}return this};const C={};function E(s,o,i){C[s]=class NodeError extends i{constructor(){super(),Object.defineProperty(this,\"message\",{value:o.apply(this,arguments),writable:!0,configurable:!0}),this.name=`${this.name} [${s}]`,this.stack,delete this.name}get code(){return s}set code(s){Object.defineProperty(this,\"code\",{configurable:!0,enumerable:!0,value:s,writable:!0})}toString(){return`${this.name} [${s}]: ${this.message}`}}}function addNumericalSeparator(s){let o=\"\",i=s.length;const a=\"-\"===s[0]?1:0;for(;i>=a+4;i-=3)o=`_${s.slice(i-3,i)}${o}`;return`${s.slice(0,i)}${o}`}function checkIntBI(s,o,i,a,u,_){if(s>i||s<o){const a=\"bigint\"==typeof o?\"n\":\"\";let u;throw u=_>3?0===o||o===BigInt(0)?`>= 0${a} and < 2${a} ** ${8*(_+1)}${a}`:`>= -(2${a} ** ${8*(_+1)-1}${a}) and < 2 ** ${8*(_+1)-1}${a}`:`>= ${o}${a} and <= ${i}${a}`,new C.ERR_OUT_OF_RANGE(\"value\",u,s)}!function checkBounds(s,o,i){validateNumber(o,\"offset\"),void 0!==s[o]&&void 0!==s[o+i]||boundsError(o,s.length-(i+1))}(a,u,_)}function validateNumber(s,o){if(\"number\"!=typeof s)throw new C.ERR_INVALID_ARG_TYPE(o,\"number\",s)}function boundsError(s,o,i){if(Math.floor(s)!==s)throw validateNumber(s,i),new C.ERR_OUT_OF_RANGE(i||\"offset\",\"an integer\",s);if(o<0)throw new C.ERR_BUFFER_OUT_OF_BOUNDS;throw new C.ERR_OUT_OF_RANGE(i||\"offset\",`>= ${i?1:0} and <= ${o}`,s)}E(\"ERR_BUFFER_OUT_OF_BOUNDS\",(function(s){return s?`${s} is outside of buffer bounds`:\"Attempt to access memory outside buffer bounds\"}),RangeError),E(\"ERR_INVALID_ARG_TYPE\",(function(s,o){return`The \"${s}\" argument must be of type number. Received type ${typeof o}`}),TypeError),E(\"ERR_OUT_OF_RANGE\",(function(s,o,i){let a=`The value of \"${s}\" is out of range.`,u=i;return Number.isInteger(i)&&Math.abs(i)>2**32?u=addNumericalSeparator(String(i)):\"bigint\"==typeof i&&(u=String(i),(i>BigInt(2)**BigInt(32)||i<-(BigInt(2)**BigInt(32)))&&(u=addNumericalSeparator(u)),u+=\"n\"),a+=` It must be ${o}. Received ${u}`,a}),RangeError);const j=/[^+/0-9A-Za-z-_]/g;function utf8ToBytes(s,o){let i;o=o||1/0;const a=s.length;let u=null;const _=[];for(let w=0;w<a;++w){if(i=s.charCodeAt(w),i>55295&&i<57344){if(!u){if(i>56319){(o-=3)>-1&&_.push(239,191,189);continue}if(w+1===a){(o-=3)>-1&&_.push(239,191,189);continue}u=i;continue}if(i<56320){(o-=3)>-1&&_.push(239,191,189),u=i;continue}i=65536+(u-55296<<10|i-56320)}else u&&(o-=3)>-1&&_.push(239,191,189);if(u=null,i<128){if((o-=1)<0)break;_.push(i)}else if(i<2048){if((o-=2)<0)break;_.push(i>>6|192,63&i|128)}else if(i<65536){if((o-=3)<0)break;_.push(i>>12|224,i>>6&63|128,63&i|128)}else{if(!(i<1114112))throw new Error(\"Invalid code point\");if((o-=4)<0)break;_.push(i>>18|240,i>>12&63|128,i>>6&63|128,63&i|128)}}return _}function base64ToBytes(s){return a.toByteArray(function base64clean(s){if((s=(s=s.split(\"=\")[0]).trim().replace(j,\"\")).length<2)return\"\";for(;s.length%4!=0;)s+=\"=\";return s}(s))}function blitBuffer(s,o,i,a){let u;for(u=0;u<a&&!(u+i>=o.length||u>=s.length);++u)o[u+i]=s[u];return u}function isInstance(s,o){return s instanceof o||null!=s&&null!=s.constructor&&null!=s.constructor.name&&s.constructor.name===o.name}function numberIsNaN(s){return s!=s}const L=function(){const s=\"0123456789abcdef\",o=new Array(256);for(let i=0;i<16;++i){const a=16*i;for(let u=0;u<16;++u)o[a+u]=s[i]+s[u]}return o}();function defineBigIntMethod(s){return\"undefined\"==typeof BigInt?BufferBigIntNotDefined:s}function BufferBigIntNotDefined(){throw new Error(\"BigInt not supported\")}},48590:(s,o)=>{\"use strict\";Object.defineProperty(o,\"__esModule\",{value:!0}),o.default=function(s){return s&&\"@@redux/INIT\"===s.type?\"initialState argument passed to createStore\":\"previous state received by the reducer\"},s.exports=o.default},48648:s=>{\"use strict\";s.exports=\"undefined\"!=typeof Reflect&&Reflect.getPrototypeOf||null},48655:(s,o,i)=>{var a=i(26025);s.exports=function listCacheHas(s){return a(this.__data__,s)>-1}},48675:(s,o,i)=>{s.exports=i(20850)},48948:(s,o,i)=>{var a=i(21791),u=i(86375);s.exports=function copySymbolsIn(s,o){return a(s,u(s),o)}},49092:(s,o,i)=>{\"use strict\";var a=i(41333);s.exports=function hasToStringTagShams(){return a()&&!!Symbol.toStringTag}},49326:(s,o,i)=>{var a=i(31769),u=i(72428),_=i(56449),w=i(30361),x=i(30294),C=i(77797);s.exports=function hasPath(s,o,i){for(var j=-1,L=(o=a(o,s)).length,B=!1;++j<L;){var $=C(o[j]);if(!(B=null!=s&&i(s,$)))break;s=s[$]}return B||++j!=L?B:!!(L=null==s?0:s.length)&&x(L)&&w($,L)&&(_(s)||u(s))}},49552:(s,o,i)=>{\"use strict\";var a=i(45951),u=i(46285),_=a.document,w=u(_)&&u(_.createElement);s.exports=function(s){return w?_.createElement(s):{}}},49653:(s,o,i)=>{var a=i(37828);s.exports=function cloneArrayBuffer(s){var o=new s.constructor(s.byteLength);return new a(o).set(new a(s)),o}},49698:s=>{var o=RegExp(\"[\\\\u200d\\\\ud800-\\\\udfff\\\\u0300-\\\\u036f\\\\ufe20-\\\\ufe2f\\\\u20d0-\\\\u20ff\\\\ufe0e\\\\ufe0f]\");s.exports=function hasUnicode(s){return o.test(s)}},49724:(s,o,i)=>{\"use strict\";var a=i(1907),u=i(39298),_=a({}.hasOwnProperty);s.exports=Object.hasOwn||function hasOwn(s,o){return _(u(s),o)}},49747:(s,o,i)=>{var a=i(66977);function curry(s,o,i){var u=a(s,8,void 0,void 0,void 0,void 0,void 0,o=i?void 0:o);return u.placeholder=curry.placeholder,u}curry.placeholder={},s.exports=curry},50002:(s,o,i)=>{var a=i(82199),u=i(4664),_=i(95950);s.exports=function getAllKeys(s){return a(s,_,u)}},50104:(s,o,i)=>{var a=i(53661);function memoize(s,o){if(\"function\"!=typeof s||null!=o&&\"function\"!=typeof o)throw new TypeError(\"Expected a function\");var memoized=function(){var i=arguments,a=o?o.apply(this,i):i[0],u=memoized.cache;if(u.has(a))return u.get(a);var _=s.apply(this,i);return memoized.cache=u.set(a,_)||u,_};return memoized.cache=new(memoize.Cache||a),memoized}memoize.Cache=a,s.exports=memoize},50583:(s,o,i)=>{var a=i(47237),u=i(17255),_=i(28586),w=i(77797);s.exports=function property(s){return _(s)?a(w(s)):u(s)}},50689:(s,o,i)=>{var a=i(50002),u=Object.prototype.hasOwnProperty;s.exports=function equalObjects(s,o,i,_,w,x){var C=1&i,j=a(s),L=j.length;if(L!=a(o).length&&!C)return!1;for(var B=L;B--;){var $=j[B];if(!(C?$ in o:u.call(o,$)))return!1}var U=x.get(s),V=x.get(o);if(U&&V)return U==o&&V==s;var z=!0;x.set(s,o),x.set(o,s);for(var Y=C;++B<L;){var Z=s[$=j[B]],ee=o[$];if(_)var ie=C?_(ee,Z,$,o,s,x):_(Z,ee,$,s,o,x);if(!(void 0===ie?Z===ee||w(Z,ee,i,_,x):ie)){z=!1;break}Y||(Y=\"constructor\"==$)}if(z&&!Y){var ae=s.constructor,ce=o.constructor;ae==ce||!(\"constructor\"in s)||!(\"constructor\"in o)||\"function\"==typeof ae&&ae instanceof ae&&\"function\"==typeof ce&&ce instanceof ce||(z=!1)}return x.delete(s),x.delete(o),z}},50828:(s,o,i)=>{var a=i(24647),u=i(13222),_=/[\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\xff\\u0100-\\u017f]/g,w=RegExp(\"[\\\\u0300-\\\\u036f\\\\ufe20-\\\\ufe2f\\\\u20d0-\\\\u20ff]\",\"g\");s.exports=function deburr(s){return(s=u(s))&&s.replace(_,a).replace(w,\"\")}},51175:(s,o,i)=>{\"use strict\";var a=i(19846);s.exports=a&&!Symbol.sham&&\"symbol\"==typeof Symbol.iterator},51234:s=>{s.exports=function baseZipObject(s,o,i){for(var a=-1,u=s.length,_=o.length,w={};++a<u;){var x=a<_?o[a]:void 0;i(w,s[a],x)}return w}},51420:(s,o,i)=>{var a=i(80079);s.exports=function stackClear(){this.__data__=new a,this.size=0}},51459:s=>{s.exports=function setCacheHas(s){return this.__data__.has(s)}},51811:s=>{var o=Date.now;s.exports=function shortOut(s){var i=0,a=0;return function(){var u=o(),_=16-(u-a);if(a=u,_>0){if(++i>=800)return arguments[0]}else i=0;return s.apply(void 0,arguments)}}},51871:(s,o,i)=>{\"use strict\";var a=i(1907),u=i(82159);s.exports=function(s,o,i){try{return a(u(Object.getOwnPropertyDescriptor(s,o)[i]))}catch(s){}}},51873:(s,o,i)=>{var a=i(9325).Symbol;s.exports=a},52623:(s,o,i)=>{\"use strict\";var a={};a[i(76264)(\"toStringTag\")]=\"z\",s.exports=\"[object z]\"===String(a)},53138:(s,o,i)=>{var a=i(11331);s.exports=function customOmitClone(s){return a(s)?void 0:s}},53209:(s,o,i)=>{\"use strict\";var a=i(65606),u=65536,_=4294967295;var w=i(92861).Buffer,x=i.g.crypto||i.g.msCrypto;x&&x.getRandomValues?s.exports=function randomBytes(s,o){if(s>_)throw new RangeError(\"requested too many random bytes\");var i=w.allocUnsafe(s);if(s>0)if(s>u)for(var C=0;C<s;C+=u)x.getRandomValues(i.slice(C,C+u));else x.getRandomValues(i);if(\"function\"==typeof o)return a.nextTick((function(){o(null,i)}));return i}:s.exports=function oldBrowser(){throw new Error(\"Secure random number generation is not supported by this browser.\\nUse Chrome, Firefox or Internet Explorer 11\")}},53320:s=>{var o=Math.max;s.exports=function composeArgsRight(s,i,a,u){for(var _=-1,w=s.length,x=-1,C=a.length,j=-1,L=i.length,B=o(w-C,0),$=Array(B+L),U=!u;++_<B;)$[_]=s[_];for(var V=_;++j<L;)$[V+j]=i[j];for(;++x<C;)(U||_<w)&&($[V+a[x]]=s[_++]);return $}},53375:(s,o,i)=>{\"use strict\";var a=i(93700);s.exports=a},53661:(s,o,i)=>{var a=i(63040),u=i(17670),_=i(90289),w=i(4509),x=i(72949);function MapCache(s){var o=-1,i=null==s?0:s.length;for(this.clear();++o<i;){var a=s[o];this.set(a[0],a[1])}}MapCache.prototype.clear=a,MapCache.prototype.delete=u,MapCache.prototype.get=_,MapCache.prototype.has=w,MapCache.prototype.set=x,s.exports=MapCache},53758:(s,o,i)=>{var a=i(30980),u=i(56017),_=i(94033),w=i(56449),x=i(40346),C=i(80257),j=Object.prototype.hasOwnProperty;function lodash(s){if(x(s)&&!w(s)&&!(s instanceof a)){if(s instanceof u)return s;if(j.call(s,\"__wrapped__\"))return C(s)}return new u(s)}lodash.prototype=_.prototype,lodash.prototype.constructor=lodash,s.exports=lodash},53812:(s,o,i)=>{var a=i(72552),u=i(40346);s.exports=function isBoolean(s){return!0===s||!1===s||u(s)&&\"[object Boolean]\"==a(s)}},54018:(s,o,i)=>{\"use strict\";var a=i(46285);s.exports=function(s){return a(s)||null===s}},54128:(s,o,i)=>{var a=i(31800),u=/^\\s+/;s.exports=function baseTrim(s){return s?s.slice(0,a(s)+1).replace(u,\"\"):s}},54552:s=>{s.exports=function basePropertyOf(s){return function(o){return null==s?void 0:s[o]}}},54641:(s,o,i)=>{var a=i(68882),u=i(51811)(a);s.exports=u},54829:(s,o,i)=>{\"use strict\";var a=i(74284).f;s.exports=function(s,o,i){i in s||a(s,i,{configurable:!0,get:function(){return o[i]},set:function(s){o[i]=s}})}},54878:(s,o,i)=>{\"use strict\";var a=i(52623),u=i(73948);s.exports=a?{}.toString:function toString(){return\"[object \"+u(this)+\"]\"}},55157:s=>{s.exports=function(){throw new Error(\"Readable.from is not available in the browser\")}},55364:(s,o,i)=>{var a=i(85250),u=i(20999)((function(s,o,i){a(s,o,i)}));s.exports=u},55481:(s,o,i)=>{var a=i(9325)[\"__core-js_shared__\"];s.exports=a},55527:s=>{var o=Object.prototype;s.exports=function isPrototype(s){var i=s&&s.constructor;return s===(\"function\"==typeof i&&i.prototype||o)}},55580:(s,o,i)=>{var a=i(56110)(i(9325),\"DataView\");s.exports=a},55674:(s,o,i)=>{\"use strict\";Object.defineProperty(o,\"__esModule\",{value:!0}),o.validateNextState=o.getUnexpectedInvocationParameterMessage=o.getStateName=void 0;var a=_interopRequireDefault(i(48590)),u=_interopRequireDefault(i(82261)),_=_interopRequireDefault(i(27374));function _interopRequireDefault(s){return s&&s.__esModule?s:{default:s}}o.getStateName=a.default,o.getUnexpectedInvocationParameterMessage=u.default,o.validateNextState=_.default},55808:(s,o,i)=>{var a=i(12507)(\"toUpperCase\");s.exports=a},55973:s=>{class KeyValuePair{constructor(s,o){this.key=s,this.value=o}clone(){const s=new KeyValuePair;return this.key&&(s.key=this.key.clone()),this.value&&(s.value=this.value.clone()),s}}s.exports=KeyValuePair},56017:(s,o,i)=>{var a=i(39344),u=i(94033);function LodashWrapper(s,o){this.__wrapped__=s,this.__actions__=[],this.__chain__=!!o,this.__index__=0,this.__values__=void 0}LodashWrapper.prototype=a(u.prototype),LodashWrapper.prototype.constructor=LodashWrapper,s.exports=LodashWrapper},56110:(s,o,i)=>{var a=i(45083),u=i(10392);s.exports=function getNative(s,o){var i=u(s,o);return a(i)?i:void 0}},56367:(s,o,i)=>{s.exports=i(77731)},56449:s=>{var o=Array.isArray;s.exports=o},56698:s=>{\"function\"==typeof Object.create?s.exports=function inherits(s,o){o&&(s.super_=o,s.prototype=Object.create(o.prototype,{constructor:{value:s,enumerable:!1,writable:!0,configurable:!0}}))}:s.exports=function inherits(s,o){if(o){s.super_=o;var TempCtor=function(){};TempCtor.prototype=o.prototype,s.prototype=new TempCtor,s.prototype.constructor=s}}},56757:(s,o,i)=>{var a=i(91033),u=Math.max;s.exports=function overRest(s,o,i){return o=u(void 0===o?s.length-1:o,0),function(){for(var _=arguments,w=-1,x=u(_.length-o,0),C=Array(x);++w<x;)C[w]=_[o+w];w=-1;for(var j=Array(o+1);++w<o;)j[w]=_[w];return j[o]=i(C),a(s,this,j)}}},57382:(s,o,i)=>{\"use strict\";var a=i(98828);s.exports=!a((function(){function F(){}return F.prototype.constructor=null,Object.getPrototypeOf(new F)!==F.prototype}))},57758:(s,o,i)=>{\"use strict\";var a;var u=i(86048).F,_=u.ERR_MISSING_ARGS,w=u.ERR_STREAM_DESTROYED;function noop(s){if(s)throw s}function call(s){s()}function pipe(s,o){return s.pipe(o)}s.exports=function pipeline(){for(var s=arguments.length,o=new Array(s),u=0;u<s;u++)o[u]=arguments[u];var x,C=function popCallback(s){return s.length?\"function\"!=typeof s[s.length-1]?noop:s.pop():noop}(o);if(Array.isArray(o[0])&&(o=o[0]),o.length<2)throw new _(\"streams\");var j=o.map((function(s,u){var _=u<o.length-1;return function destroyer(s,o,u,_){_=function once(s){var o=!1;return function(){o||(o=!0,s.apply(void 0,arguments))}}(_);var x=!1;s.on(\"close\",(function(){x=!0})),void 0===a&&(a=i(86238)),a(s,{readable:o,writable:u},(function(s){if(s)return _(s);x=!0,_()}));var C=!1;return function(o){if(!x&&!C)return C=!0,function isRequest(s){return s.setHeader&&\"function\"==typeof s.abort}(s)?s.abort():\"function\"==typeof s.destroy?s.destroy():void _(o||new w(\"pipe\"))}}(s,_,u>0,(function(s){x||(x=s),s&&j.forEach(call),_||(j.forEach(call),C(x))}))}));return o.reduce(pipe)}},58068:s=>{\"use strict\";s.exports=SyntaxError},58075:(s,o,i)=>{\"use strict\";var a,u=i(36624),_=i(42220),w=i(80376),x=i(38530),C=i(62416),j=i(49552),L=i(92522),B=\"prototype\",$=\"script\",U=L(\"IE_PROTO\"),EmptyConstructor=function(){},scriptTag=function(s){return\"<\"+$+\">\"+s+\"</\"+$+\">\"},NullProtoObjectViaActiveX=function(s){s.write(scriptTag(\"\")),s.close();var o=s.parentWindow.Object;return s=null,o},NullProtoObject=function(){try{a=new ActiveXObject(\"htmlfile\")}catch(s){}var s,o,i;NullProtoObject=\"undefined\"!=typeof document?document.domain&&a?NullProtoObjectViaActiveX(a):(o=j(\"iframe\"),i=\"java\"+$+\":\",o.style.display=\"none\",C.appendChild(o),o.src=String(i),(s=o.contentWindow.document).open(),s.write(scriptTag(\"document.F=Object\")),s.close(),s.F):NullProtoObjectViaActiveX(a);for(var u=w.length;u--;)delete NullProtoObject[B][w[u]];return NullProtoObject()};x[U]=!0,s.exports=Object.create||function create(s,o){var i;return null!==s?(EmptyConstructor[B]=u(s),i=new EmptyConstructor,EmptyConstructor[B]=null,i[U]=s):i=NullProtoObject(),void 0===o?i:_.f(i,o)}},58156:(s,o,i)=>{var a=i(47422);s.exports=function get(s,o,i){var u=null==s?void 0:a(s,o);return void 0===u?i:u}},58523:s=>{s.exports=function countHolders(s,o){for(var i=s.length,a=0;i--;)s[i]===o&&++a;return a}},58661:(s,o,i)=>{\"use strict\";var a=i(39447),u=i(98828);s.exports=a&&u((function(){return 42!==Object.defineProperty((function(){}),\"prototype\",{value:42,writable:!1}).prototype}))},58968:s=>{\"use strict\";s.exports=Math.floor},59350:s=>{var o=Object.prototype.toString;s.exports=function objectToString(s){return o.call(s)}},59399:(s,o,i)=>{\"use strict\";var a=i(25264).CopyToClipboard;a.CopyToClipboard=a,s.exports=a},59550:s=>{\"use strict\";s.exports=function(s,o){return{value:s,done:o}}},60183:(s,o,i)=>{\"use strict\";var a=i(11091),u=i(13930),_=i(7376),w=i(36833),x=i(62250),C=i(47181),j=i(15972),L=i(79192),B=i(14840),$=i(61626),U=i(68055),V=i(76264),z=i(93742),Y=i(95116),Z=w.PROPER,ee=w.CONFIGURABLE,ie=Y.IteratorPrototype,ae=Y.BUGGY_SAFARI_ITERATORS,ce=V(\"iterator\"),le=\"keys\",pe=\"values\",de=\"entries\",returnThis=function(){return this};s.exports=function(s,o,i,w,V,Y,fe){C(i,o,w);var ye,be,_e,getIterationMethod=function(s){if(s===V&&Te)return Te;if(!ae&&s&&s in xe)return xe[s];switch(s){case le:return function keys(){return new i(this,s)};case pe:return function values(){return new i(this,s)};case de:return function entries(){return new i(this,s)}}return function(){return new i(this)}},Se=o+\" Iterator\",we=!1,xe=s.prototype,Pe=xe[ce]||xe[\"@@iterator\"]||V&&xe[V],Te=!ae&&Pe||getIterationMethod(V),Re=\"Array\"===o&&xe.entries||Pe;if(Re&&(ye=j(Re.call(new s)))!==Object.prototype&&ye.next&&(_||j(ye)===ie||(L?L(ye,ie):x(ye[ce])||U(ye,ce,returnThis)),B(ye,Se,!0,!0),_&&(z[Se]=returnThis)),Z&&V===pe&&Pe&&Pe.name!==pe&&(!_&&ee?$(xe,\"name\",pe):(we=!0,Te=function values(){return u(Pe,this)})),V)if(be={values:getIterationMethod(pe),keys:Y?Te:getIterationMethod(le),entries:getIterationMethod(de)},fe)for(_e in be)(ae||we||!(_e in xe))&&U(xe,_e,be[_e]);else a({target:o,proto:!0,forced:ae||we},be);return _&&!fe||xe[ce]===Te||U(xe,ce,Te,{name:V}),z[o]=Te,be}},60270:(s,o,i)=>{var a=i(87068),u=i(40346);s.exports=function baseIsEqual(s,o,i,_,w){return s===o||(null==s||null==o||!u(s)&&!u(o)?s!=s&&o!=o:a(s,o,i,_,baseIsEqual,w))}},60581:(s,o,i)=>{\"use strict\";var a=i(13930),u=i(62250),_=i(46285),w=TypeError;s.exports=function(s,o){var i,x;if(\"string\"===o&&u(i=s.toString)&&!_(x=a(i,s)))return x;if(u(i=s.valueOf)&&!_(x=a(i,s)))return x;if(\"string\"!==o&&u(i=s.toString)&&!_(x=a(i,s)))return x;throw new w(\"Can't convert object to primitive value\")}},60680:(s,o,i)=>{var a=i(13222),u=/[\\\\^$.*+?()[\\]{}|]/g,_=RegExp(u.source);s.exports=function escapeRegExp(s){return(s=a(s))&&_.test(s)?s.replace(u,\"\\\\$&\"):s}},61045:(s,o,i)=>{const a=i(6048),u=i(23805),_=i(6233),w=i(87726),x=i(10866);s.exports=class ObjectElement extends _{constructor(s,o,i){super(s||[],o,i),this.element=\"object\"}primitive(){return\"object\"}toValue(){return this.content.reduce(((s,o)=>(s[o.key.toValue()]=o.value?o.value.toValue():void 0,s)),{})}get(s){const o=this.getMember(s);if(o)return o.value}getMember(s){if(void 0!==s)return this.content.find((o=>o.key.toValue()===s))}remove(s){let o=null;return this.content=this.content.filter((i=>i.key.toValue()!==s||(o=i,!1))),o}getKey(s){const o=this.getMember(s);if(o)return o.key}set(s,o){if(u(s))return Object.keys(s).forEach((o=>{this.set(o,s[o])})),this;const i=s,a=this.getMember(i);return a?a.value=o:this.content.push(new w(i,o)),this}keys(){return this.content.map((s=>s.key.toValue()))}values(){return this.content.map((s=>s.value.toValue()))}hasKey(s){return this.content.some((o=>o.key.equals(s)))}items(){return this.content.map((s=>[s.key.toValue(),s.value.toValue()]))}map(s,o){return this.content.map((i=>s.bind(o)(i.value,i.key,i)))}compactMap(s,o){const i=[];return this.forEach(((a,u,_)=>{const w=s.bind(o)(a,u,_);w&&i.push(w)})),i}filter(s,o){return new x(this.content).filter(s,o)}reject(s,o){return this.filter(a(s),o)}forEach(s,o){return this.content.forEach((i=>s.bind(o)(i.value,i.key,i)))}}},61074:s=>{s.exports=function asciiToArray(s){return s.split(\"\")}},61160:(s,o,i)=>{\"use strict\";var a=i(92063),u=i(73992),_=/^[\\x00-\\x20\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+/,w=/[\\n\\r\\t]/g,x=/^[A-Za-z][A-Za-z0-9+-.]*:\\/\\//,C=/:\\d+$/,j=/^([a-z][a-z0-9.+-]*:)?(\\/\\/)?([\\\\/]+)?([\\S\\s]*)/i,L=/^[a-zA-Z]:/;function trimLeft(s){return(s||\"\").toString().replace(_,\"\")}var B=[[\"#\",\"hash\"],[\"?\",\"query\"],function sanitize(s,o){return isSpecial(o.protocol)?s.replace(/\\\\/g,\"/\"):s},[\"/\",\"pathname\"],[\"@\",\"auth\",1],[NaN,\"host\",void 0,1,1],[/:(\\d*)$/,\"port\",void 0,1],[NaN,\"hostname\",void 0,1,1]],$={hash:1,query:1};function lolcation(s){var o,a=(\"undefined\"!=typeof window?window:void 0!==i.g?i.g:\"undefined\"!=typeof self?self:{}).location||{},u={},_=typeof(s=s||a);if(\"blob:\"===s.protocol)u=new Url(unescape(s.pathname),{});else if(\"string\"===_)for(o in u=new Url(s,{}),$)delete u[o];else if(\"object\"===_){for(o in s)o in $||(u[o]=s[o]);void 0===u.slashes&&(u.slashes=x.test(s.href))}return u}function isSpecial(s){return\"file:\"===s||\"ftp:\"===s||\"http:\"===s||\"https:\"===s||\"ws:\"===s||\"wss:\"===s}function extractProtocol(s,o){s=(s=trimLeft(s)).replace(w,\"\"),o=o||{};var i,a=j.exec(s),u=a[1]?a[1].toLowerCase():\"\",_=!!a[2],x=!!a[3],C=0;return _?x?(i=a[2]+a[3]+a[4],C=a[2].length+a[3].length):(i=a[2]+a[4],C=a[2].length):x?(i=a[3]+a[4],C=a[3].length):i=a[4],\"file:\"===u?C>=2&&(i=i.slice(2)):isSpecial(u)?i=a[4]:u?_&&(i=i.slice(2)):C>=2&&isSpecial(o.protocol)&&(i=a[4]),{protocol:u,slashes:_||isSpecial(u),slashesCount:C,rest:i}}function Url(s,o,i){if(s=(s=trimLeft(s)).replace(w,\"\"),!(this instanceof Url))return new Url(s,o,i);var _,x,C,j,$,U,V=B.slice(),z=typeof o,Y=this,Z=0;for(\"object\"!==z&&\"string\"!==z&&(i=o,o=null),i&&\"function\"!=typeof i&&(i=u.parse),_=!(x=extractProtocol(s||\"\",o=lolcation(o))).protocol&&!x.slashes,Y.slashes=x.slashes||_&&o.slashes,Y.protocol=x.protocol||o.protocol||\"\",s=x.rest,(\"file:\"===x.protocol&&(2!==x.slashesCount||L.test(s))||!x.slashes&&(x.protocol||x.slashesCount<2||!isSpecial(Y.protocol)))&&(V[3]=[/(.*)/,\"pathname\"]);Z<V.length;Z++)\"function\"!=typeof(j=V[Z])?(C=j[0],U=j[1],C!=C?Y[U]=s:\"string\"==typeof C?~($=\"@\"===C?s.lastIndexOf(C):s.indexOf(C))&&(\"number\"==typeof j[2]?(Y[U]=s.slice(0,$),s=s.slice($+j[2])):(Y[U]=s.slice($),s=s.slice(0,$))):($=C.exec(s))&&(Y[U]=$[1],s=s.slice(0,$.index)),Y[U]=Y[U]||_&&j[3]&&o[U]||\"\",j[4]&&(Y[U]=Y[U].toLowerCase())):s=j(s,Y);i&&(Y.query=i(Y.query)),_&&o.slashes&&\"/\"!==Y.pathname.charAt(0)&&(\"\"!==Y.pathname||\"\"!==o.pathname)&&(Y.pathname=function resolve(s,o){if(\"\"===s)return o;for(var i=(o||\"/\").split(\"/\").slice(0,-1).concat(s.split(\"/\")),a=i.length,u=i[a-1],_=!1,w=0;a--;)\".\"===i[a]?i.splice(a,1):\"..\"===i[a]?(i.splice(a,1),w++):w&&(0===a&&(_=!0),i.splice(a,1),w--);return _&&i.unshift(\"\"),\".\"!==u&&\"..\"!==u||i.push(\"\"),i.join(\"/\")}(Y.pathname,o.pathname)),\"/\"!==Y.pathname.charAt(0)&&isSpecial(Y.protocol)&&(Y.pathname=\"/\"+Y.pathname),a(Y.port,Y.protocol)||(Y.host=Y.hostname,Y.port=\"\"),Y.username=Y.password=\"\",Y.auth&&(~($=Y.auth.indexOf(\":\"))?(Y.username=Y.auth.slice(0,$),Y.username=encodeURIComponent(decodeURIComponent(Y.username)),Y.password=Y.auth.slice($+1),Y.password=encodeURIComponent(decodeURIComponent(Y.password))):Y.username=encodeURIComponent(decodeURIComponent(Y.auth)),Y.auth=Y.password?Y.username+\":\"+Y.password:Y.username),Y.origin=\"file:\"!==Y.protocol&&isSpecial(Y.protocol)&&Y.host?Y.protocol+\"//\"+Y.host:\"null\",Y.href=Y.toString()}Url.prototype={set:function set(s,o,i){var _=this;switch(s){case\"query\":\"string\"==typeof o&&o.length&&(o=(i||u.parse)(o)),_[s]=o;break;case\"port\":_[s]=o,a(o,_.protocol)?o&&(_.host=_.hostname+\":\"+o):(_.host=_.hostname,_[s]=\"\");break;case\"hostname\":_[s]=o,_.port&&(o+=\":\"+_.port),_.host=o;break;case\"host\":_[s]=o,C.test(o)?(o=o.split(\":\"),_.port=o.pop(),_.hostname=o.join(\":\")):(_.hostname=o,_.port=\"\");break;case\"protocol\":_.protocol=o.toLowerCase(),_.slashes=!i;break;case\"pathname\":case\"hash\":if(o){var w=\"pathname\"===s?\"/\":\"#\";_[s]=o.charAt(0)!==w?w+o:o}else _[s]=o;break;case\"username\":case\"password\":_[s]=encodeURIComponent(o);break;case\"auth\":var x=o.indexOf(\":\");~x?(_.username=o.slice(0,x),_.username=encodeURIComponent(decodeURIComponent(_.username)),_.password=o.slice(x+1),_.password=encodeURIComponent(decodeURIComponent(_.password))):_.username=encodeURIComponent(decodeURIComponent(o))}for(var j=0;j<B.length;j++){var L=B[j];L[4]&&(_[L[1]]=_[L[1]].toLowerCase())}return _.auth=_.password?_.username+\":\"+_.password:_.username,_.origin=\"file:\"!==_.protocol&&isSpecial(_.protocol)&&_.host?_.protocol+\"//\"+_.host:\"null\",_.href=_.toString(),_},toString:function toString(s){s&&\"function\"==typeof s||(s=u.stringify);var o,i=this,a=i.host,_=i.protocol;_&&\":\"!==_.charAt(_.length-1)&&(_+=\":\");var w=_+(i.protocol&&i.slashes||isSpecial(i.protocol)?\"//\":\"\");return i.username?(w+=i.username,i.password&&(w+=\":\"+i.password),w+=\"@\"):i.password?(w+=\":\"+i.password,w+=\"@\"):\"file:\"!==i.protocol&&isSpecial(i.protocol)&&!a&&\"/\"!==i.pathname&&(w+=\"@\"),(\":\"===a[a.length-1]||C.test(i.hostname)&&!i.port)&&(a+=\":\"),w+=a+i.pathname,(o=\"object\"==typeof i.query?s(i.query):i.query)&&(w+=\"?\"!==o.charAt(0)?\"?\"+o:o),i.hash&&(w+=i.hash),w}},Url.extractProtocol=extractProtocol,Url.location=lolcation,Url.trimLeft=trimLeft,Url.qs=u,s.exports=Url},61448:(s,o,i)=>{var a=i(20426),u=i(49326);s.exports=function has(s,o){return null!=s&&u(s,o,a)}},61489:(s,o,i)=>{var a=i(17400);s.exports=function toInteger(s){var o=a(s),i=o%1;return o==o?i?o-i:o:0}},61626:(s,o,i)=>{\"use strict\";var a=i(39447),u=i(74284),_=i(75817);s.exports=a?function(s,o,i){return u.f(s,o,_(1,i))}:function(s,o,i){return s[o]=i,s}},61747:(s,o,i)=>{\"use strict\";var a=i(45951),u=i(92046);s.exports=function(s,o){var i=u[s+\"Prototype\"],_=i&&i[o];if(_)return _;var w=a[s],x=w&&w.prototype;return x&&x[o]}},61802:(s,o,i)=>{var a=i(62224),u=/[^.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))/g,_=/\\\\(\\\\)?/g,w=a((function(s){var o=[];return 46===s.charCodeAt(0)&&o.push(\"\"),s.replace(u,(function(s,i,a,u){o.push(a?u.replace(_,\"$1\"):i||s)})),o}));s.exports=w},62006:(s,o,i)=>{var a=i(15389),u=i(64894),_=i(95950);s.exports=function createFind(s){return function(o,i,w){var x=Object(o);if(!u(o)){var C=a(i,3);o=_(o),i=function(s){return C(x[s],s,x)}}var j=s(o,i,w);return j>-1?x[C?o[j]:j]:void 0}}},62060:s=>{var o=/\\{(?:\\n\\/\\* \\[wrapped with .+\\] \\*\\/)?\\n?/;s.exports=function insertWrapDetails(s,i){var a=i.length;if(!a)return s;var u=a-1;return i[u]=(a>1?\"& \":\"\")+i[u],i=i.join(a>2?\", \":\" \"),s.replace(o,\"{\\n/* [wrapped with \"+i+\"] */\\n\")}},62193:(s,o,i)=>{var a=i(88984),u=i(5861),_=i(72428),w=i(56449),x=i(64894),C=i(3656),j=i(55527),L=i(37167),B=Object.prototype.hasOwnProperty;s.exports=function isEmpty(s){if(null==s)return!0;if(x(s)&&(w(s)||\"string\"==typeof s||\"function\"==typeof s.splice||C(s)||L(s)||_(s)))return!s.length;var o=u(s);if(\"[object Map]\"==o||\"[object Set]\"==o)return!s.size;if(j(s))return!a(s).length;for(var i in s)if(B.call(s,i))return!1;return!0}},62224:(s,o,i)=>{var a=i(50104);s.exports=function memoizeCapped(s){var o=a(s,(function(s){return 500===i.size&&i.clear(),s})),i=o.cache;return o}},62250:s=>{\"use strict\";var o=\"object\"==typeof document&&document.all;s.exports=void 0===o&&void 0!==o?function(s){return\"function\"==typeof s||s===o}:function(s){return\"function\"==typeof s}},62284:(s,o,i)=>{var a=i(84629),u=Object.prototype.hasOwnProperty;s.exports=function getFuncName(s){for(var o=s.name+\"\",i=a[o],_=u.call(a,o)?i.length:0;_--;){var w=i[_],x=w.func;if(null==x||x==s)return w.name}return o}},62416:(s,o,i)=>{\"use strict\";var a=i(85582);s.exports=a(\"document\",\"documentElement\")},62802:(s,o,i)=>{\"use strict\";s.exports=function SHA(o){var i=o.toLowerCase(),a=s.exports[i];if(!a)throw new Error(i+\" is not supported (we accept pull requests)\");return new a},s.exports.sha=i(27816),s.exports.sha1=i(63737),s.exports.sha224=i(26710),s.exports.sha256=i(24107),s.exports.sha384=i(32827),s.exports.sha512=i(82890)},63040:(s,o,i)=>{var a=i(21549),u=i(80079),_=i(68223);s.exports=function mapCacheClear(){this.size=0,this.__data__={hash:new a,map:new(_||u),string:new a}}},63345:s=>{s.exports=function stubArray(){return[]}},63560:(s,o,i)=>{var a=i(73170);s.exports=function set(s,o,i){return null==s?s:a(s,o,i)}},63600:(s,o,i)=>{\"use strict\";s.exports=PassThrough;var a=i(74610);function PassThrough(s){if(!(this instanceof PassThrough))return new PassThrough(s);a.call(this,s)}i(56698)(PassThrough,a),PassThrough.prototype._transform=function(s,o,i){i(null,s)}},63605:s=>{s.exports=function stackGet(s){return this.__data__.get(s)}},63702:s=>{s.exports=function listCacheClear(){this.__data__=[],this.size=0}},63737:(s,o,i)=>{\"use strict\";var a=i(56698),u=i(90392),_=i(92861).Buffer,w=[1518500249,1859775393,-1894007588,-899497514],x=new Array(80);function Sha1(){this.init(),this._w=x,u.call(this,64,56)}function rotl5(s){return s<<5|s>>>27}function rotl30(s){return s<<30|s>>>2}function ft(s,o,i,a){return 0===s?o&i|~o&a:2===s?o&i|o&a|i&a:o^i^a}a(Sha1,u),Sha1.prototype.init=function(){return this._a=1732584193,this._b=4023233417,this._c=2562383102,this._d=271733878,this._e=3285377520,this},Sha1.prototype._update=function(s){for(var o,i=this._w,a=0|this._a,u=0|this._b,_=0|this._c,x=0|this._d,C=0|this._e,j=0;j<16;++j)i[j]=s.readInt32BE(4*j);for(;j<80;++j)i[j]=(o=i[j-3]^i[j-8]^i[j-14]^i[j-16])<<1|o>>>31;for(var L=0;L<80;++L){var B=~~(L/20),$=rotl5(a)+ft(B,u,_,x)+C+i[L]+w[B]|0;C=x,x=_,_=rotl30(u),u=a,a=$}this._a=a+this._a|0,this._b=u+this._b|0,this._c=_+this._c|0,this._d=x+this._d|0,this._e=C+this._e|0},Sha1.prototype._hash=function(){var s=_.allocUnsafe(20);return s.writeInt32BE(0|this._a,0),s.writeInt32BE(0|this._b,4),s.writeInt32BE(0|this._c,8),s.writeInt32BE(0|this._d,12),s.writeInt32BE(0|this._e,16),s},s.exports=Sha1},63862:s=>{s.exports=function hashDelete(s){var o=this.has(s)&&delete this.__data__[s];return this.size-=o?1:0,o}},63912:(s,o,i)=>{var a=i(61074),u=i(49698),_=i(42054);s.exports=function stringToArray(s){return u(s)?_(s):a(s)}},63950:s=>{s.exports=function noop(){}},64039:(s,o,i)=>{\"use strict\";var a=\"undefined\"!=typeof Symbol&&Symbol,u=i(41333);s.exports=function hasNativeSymbols(){return\"function\"==typeof a&&(\"function\"==typeof Symbol&&(\"symbol\"==typeof a(\"foo\")&&(\"symbol\"==typeof Symbol(\"bar\")&&u())))}},64502:(s,o,i)=>{\"use strict\";i(82048)},64626:(s,o,i)=>{var a=i(66977);s.exports=function ary(s,o,i){return o=i?void 0:o,o=s&&null==o?s.length:o,a(s,128,void 0,void 0,void 0,void 0,o)}},64634:s=>{var o={}.toString;s.exports=Array.isArray||function(s){return\"[object Array]\"==o.call(s)}},64894:(s,o,i)=>{var a=i(1882),u=i(30294);s.exports=function isArrayLike(s){return null!=s&&u(s.length)&&!a(s)}},64932:(s,o,i)=>{\"use strict\";var a,u,_,w=i(40551),x=i(45951),C=i(46285),j=i(61626),L=i(49724),B=i(36128),$=i(92522),U=i(38530),V=\"Object already initialized\",z=x.TypeError,Y=x.WeakMap;if(w||B.state){var Z=B.state||(B.state=new Y);Z.get=Z.get,Z.has=Z.has,Z.set=Z.set,a=function(s,o){if(Z.has(s))throw new z(V);return o.facade=s,Z.set(s,o),o},u=function(s){return Z.get(s)||{}},_=function(s){return Z.has(s)}}else{var ee=$(\"state\");U[ee]=!0,a=function(s,o){if(L(s,ee))throw new z(V);return o.facade=s,j(s,ee,o),o},u=function(s){return L(s,ee)?s[ee]:{}},_=function(s){return L(s,ee)}}s.exports={set:a,get:u,has:_,enforce:function(s){return _(s)?u(s):a(s,{})},getterFor:function(s){return function(o){var i;if(!C(o)||(i=u(o)).type!==s)throw new z(\"Incompatible receiver, \"+s+\" required\");return i}}}},65291:(s,o,i)=>{\"use strict\";var a=i(86048).F.ERR_INVALID_OPT_VALUE;s.exports={getHighWaterMark:function getHighWaterMark(s,o,i,u){var _=function highWaterMarkFrom(s,o,i){return null!=s.highWaterMark?s.highWaterMark:o?s[i]:null}(o,u,i);if(null!=_){if(!isFinite(_)||Math.floor(_)!==_||_<0)throw new a(u?i:\"highWaterMark\",_);return Math.floor(_)}return s.objectMode?16:16384}}},65482:(s,o,i)=>{\"use strict\";var a=i(41176);s.exports=function(s){var o=+s;return o!=o||0===o?0:a(o)}},65606:s=>{var o,i,a=s.exports={};function defaultSetTimout(){throw new Error(\"setTimeout has not been defined\")}function defaultClearTimeout(){throw new Error(\"clearTimeout has not been defined\")}function runTimeout(s){if(o===setTimeout)return setTimeout(s,0);if((o===defaultSetTimout||!o)&&setTimeout)return o=setTimeout,setTimeout(s,0);try{return o(s,0)}catch(i){try{return o.call(null,s,0)}catch(i){return o.call(this,s,0)}}}!function(){try{o=\"function\"==typeof setTimeout?setTimeout:defaultSetTimout}catch(s){o=defaultSetTimout}try{i=\"function\"==typeof clearTimeout?clearTimeout:defaultClearTimeout}catch(s){i=defaultClearTimeout}}();var u,_=[],w=!1,x=-1;function cleanUpNextTick(){w&&u&&(w=!1,u.length?_=u.concat(_):x=-1,_.length&&drainQueue())}function drainQueue(){if(!w){var s=runTimeout(cleanUpNextTick);w=!0;for(var o=_.length;o;){for(u=_,_=[];++x<o;)u&&u[x].run();x=-1,o=_.length}u=null,w=!1,function runClearTimeout(s){if(i===clearTimeout)return clearTimeout(s);if((i===defaultClearTimeout||!i)&&clearTimeout)return i=clearTimeout,clearTimeout(s);try{return i(s)}catch(o){try{return i.call(null,s)}catch(o){return i.call(this,s)}}}(s)}}function Item(s,o){this.fun=s,this.array=o}function noop(){}a.nextTick=function(s){var o=new Array(arguments.length-1);if(arguments.length>1)for(var i=1;i<arguments.length;i++)o[i-1]=arguments[i];_.push(new Item(s,o)),1!==_.length||w||runTimeout(drainQueue)},Item.prototype.run=function(){this.fun.apply(null,this.array)},a.title=\"browser\",a.browser=!0,a.env={},a.argv=[],a.version=\"\",a.versions={},a.on=noop,a.addListener=noop,a.once=noop,a.off=noop,a.removeListener=noop,a.removeAllListeners=noop,a.emit=noop,a.prependListener=noop,a.prependOnceListener=noop,a.listeners=function(s){return[]},a.binding=function(s){throw new Error(\"process.binding is not supported\")},a.cwd=function(){return\"/\"},a.chdir=function(s){throw new Error(\"process.chdir is not supported\")},a.umask=function(){return 0}},65772:s=>{s.exports=function json(s){const o={literal:\"true false null\"},i=[s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE],a=[s.QUOTE_STRING_MODE,s.C_NUMBER_MODE],u={end:\",\",endsWithParent:!0,excludeEnd:!0,contains:a,keywords:o},_={begin:/\\{/,end:/\\}/,contains:[{className:\"attr\",begin:/\"/,end:/\"/,contains:[s.BACKSLASH_ESCAPE],illegal:\"\\\\n\"},s.inherit(u,{begin:/:/})].concat(i),illegal:\"\\\\S\"},w={begin:\"\\\\[\",end:\"\\\\]\",contains:[s.inherit(u)],illegal:\"\\\\S\"};return a.push(_,w),i.forEach((function(s){a.push(s)})),{name:\"JSON\",contains:a,keywords:o,illegal:\"\\\\S\"}}},66645:(s,o,i)=>{var a=i(1733),u=i(45434),_=i(13222),w=i(22225);s.exports=function words(s,o,i){return s=_(s),void 0===(o=i?void 0:o)?u(s)?w(s):a(s):s.match(o)||[]}},66721:(s,o,i)=>{var a=i(81042),u=Object.prototype.hasOwnProperty;s.exports=function hashGet(s){var o=this.__data__;if(a){var i=o[s];return\"__lodash_hash_undefined__\"===i?void 0:i}return u.call(o,s)?o[s]:void 0}},66743:(s,o,i)=>{\"use strict\";var a=i(89353);s.exports=Function.prototype.bind||a},66977:(s,o,i)=>{var a=i(68882),u=i(11842),_=i(77078),w=i(37471),x=i(24168),C=i(37381),j=i(3209),L=i(54641),B=i(70981),$=i(61489),U=Math.max;s.exports=function createWrap(s,o,i,V,z,Y,Z,ee){var ie=2&o;if(!ie&&\"function\"!=typeof s)throw new TypeError(\"Expected a function\");var ae=V?V.length:0;if(ae||(o&=-97,V=z=void 0),Z=void 0===Z?Z:U($(Z),0),ee=void 0===ee?ee:$(ee),ae-=z?z.length:0,64&o){var ce=V,le=z;V=z=void 0}var pe=ie?void 0:C(s),de=[s,o,i,V,z,ce,le,Y,Z,ee];if(pe&&j(de,pe),s=de[0],o=de[1],i=de[2],V=de[3],z=de[4],!(ee=de[9]=void 0===de[9]?ie?0:s.length:U(de[9]-ae,0))&&24&o&&(o&=-25),o&&1!=o)fe=8==o||16==o?_(s,o,ee):32!=o&&33!=o||z.length?w.apply(void 0,de):x(s,o,i,V);else var fe=u(s,o,i);return B((pe?a:L)(fe,de),s,o)}},67197:s=>{s.exports=function matchesStrictComparable(s,o){return function(i){return null!=i&&(i[s]===o&&(void 0!==o||s in Object(i)))}}},67526:(s,o)=>{\"use strict\";o.byteLength=function byteLength(s){var o=getLens(s),i=o[0],a=o[1];return 3*(i+a)/4-a},o.toByteArray=function toByteArray(s){var o,i,_=getLens(s),w=_[0],x=_[1],C=new u(function _byteLength(s,o,i){return 3*(o+i)/4-i}(0,w,x)),j=0,L=x>0?w-4:w;for(i=0;i<L;i+=4)o=a[s.charCodeAt(i)]<<18|a[s.charCodeAt(i+1)]<<12|a[s.charCodeAt(i+2)]<<6|a[s.charCodeAt(i+3)],C[j++]=o>>16&255,C[j++]=o>>8&255,C[j++]=255&o;2===x&&(o=a[s.charCodeAt(i)]<<2|a[s.charCodeAt(i+1)]>>4,C[j++]=255&o);1===x&&(o=a[s.charCodeAt(i)]<<10|a[s.charCodeAt(i+1)]<<4|a[s.charCodeAt(i+2)]>>2,C[j++]=o>>8&255,C[j++]=255&o);return C},o.fromByteArray=function fromByteArray(s){for(var o,a=s.length,u=a%3,_=[],w=16383,x=0,C=a-u;x<C;x+=w)_.push(encodeChunk(s,x,x+w>C?C:x+w));1===u?(o=s[a-1],_.push(i[o>>2]+i[o<<4&63]+\"==\")):2===u&&(o=(s[a-2]<<8)+s[a-1],_.push(i[o>>10]+i[o>>4&63]+i[o<<2&63]+\"=\"));return _.join(\"\")};for(var i=[],a=[],u=\"undefined\"!=typeof Uint8Array?Uint8Array:Array,_=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\",w=0;w<64;++w)i[w]=_[w],a[_.charCodeAt(w)]=w;function getLens(s){var o=s.length;if(o%4>0)throw new Error(\"Invalid string. Length must be a multiple of 4\");var i=s.indexOf(\"=\");return-1===i&&(i=o),[i,i===o?0:4-i%4]}function encodeChunk(s,o,a){for(var u,_,w=[],x=o;x<a;x+=3)u=(s[x]<<16&16711680)+(s[x+1]<<8&65280)+(255&s[x+2]),w.push(i[(_=u)>>18&63]+i[_>>12&63]+i[_>>6&63]+i[63&_]);return w.join(\"\")}a[\"-\".charCodeAt(0)]=62,a[\"_\".charCodeAt(0)]=63},68002:s=>{\"use strict\";s.exports=Math.min},68055:(s,o,i)=>{\"use strict\";var a=i(61626);s.exports=function(s,o,i,u){return u&&u.enumerable?s[o]=i:a(s,o,i),s}},68090:s=>{s.exports=function last(s){var o=null==s?0:s.length;return o?s[o-1]:void 0}},68223:(s,o,i)=>{var a=i(56110)(i(9325),\"Map\");s.exports=a},68294:(s,o,i)=>{var a=i(23007),u=i(30361),_=Math.min;s.exports=function reorder(s,o){for(var i=s.length,w=_(o.length,i),x=a(s);w--;){var C=o[w];s[w]=u(C,i)?x[C]:void 0}return s}},68623:(s,o,i)=>{\"use strict\";var a=i(694);s.exports=a},68882:(s,o,i)=>{var a=i(83488),u=i(48152),_=u?function(s,o){return u.set(s,o),s}:a;s.exports=_},68969:(s,o,i)=>{var a=i(47422),u=i(25160);s.exports=function parent(s,o){return o.length<2?s:a(s,u(o,0,-1))}},69302:(s,o,i)=>{var a=i(83488),u=i(56757),_=i(32865);s.exports=function baseRest(s,o){return _(u(s,o,a),s+\"\")}},69383:s=>{\"use strict\";s.exports=Error},69600:s=>{\"use strict\";var o,i,a=Function.prototype.toString,u=\"object\"==typeof Reflect&&null!==Reflect&&Reflect.apply;if(\"function\"==typeof u&&\"function\"==typeof Object.defineProperty)try{o=Object.defineProperty({},\"length\",{get:function(){throw i}}),i={},u((function(){throw 42}),null,o)}catch(s){s!==i&&(u=null)}else u=null;var _=/^\\s*class\\b/,w=function isES6ClassFunction(s){try{var o=a.call(s);return _.test(o)}catch(s){return!1}},x=function tryFunctionToStr(s){try{return!w(s)&&(a.call(s),!0)}catch(s){return!1}},C=Object.prototype.toString,j=\"function\"==typeof Symbol&&!!Symbol.toStringTag,L=!(0 in[,]),B=function isDocumentDotAll(){return!1};if(\"object\"==typeof document){var $=document.all;C.call($)===C.call(document.all)&&(B=function isDocumentDotAll(s){if((L||!s)&&(void 0===s||\"object\"==typeof s))try{var o=C.call(s);return(\"[object HTMLAllCollection]\"===o||\"[object HTML document.all class]\"===o||\"[object HTMLCollection]\"===o||\"[object Object]\"===o)&&null==s(\"\")}catch(s){}return!1})}s.exports=u?function isCallable(s){if(B(s))return!0;if(!s)return!1;if(\"function\"!=typeof s&&\"object\"!=typeof s)return!1;try{u(s,null,o)}catch(s){if(s!==i)return!1}return!w(s)&&x(s)}:function isCallable(s){if(B(s))return!0;if(!s)return!1;if(\"function\"!=typeof s&&\"object\"!=typeof s)return!1;if(j)return x(s);if(w(s))return!1;var o=C.call(s);return!(\"[object Function]\"!==o&&\"[object GeneratorFunction]\"!==o&&!/^\\[object HTML/.test(o))&&x(s)}},69675:s=>{\"use strict\";s.exports=TypeError},69884:(s,o,i)=>{var a=i(21791),u=i(37241);s.exports=function toPlainObject(s){return a(s,u(s))}},69982:(s,o,i)=>{\"use strict\";s.exports=i(29844)},70080:(s,o,i)=>{var a=i(26025),u=Array.prototype.splice;s.exports=function listCacheDelete(s){var o=this.__data__,i=a(o,s);return!(i<0)&&(i==o.length-1?o.pop():u.call(o,i,1),--this.size,!0)}},70414:s=>{\"use strict\";s.exports=Math.round},70453:(s,o,i)=>{\"use strict\";var a,u=i(79612),_=i(69383),w=i(41237),x=i(79290),C=i(79538),j=i(58068),L=i(69675),B=i(35345),$=i(71514),U=i(58968),V=i(6188),z=i(68002),Y=i(75880),Z=i(70414),ee=i(73093),ie=Function,getEvalledConstructor=function(s){try{return ie('\"use strict\"; return ('+s+\").constructor;\")()}catch(s){}},ae=i(75795),ce=i(30655),throwTypeError=function(){throw new L},le=ae?function(){try{return throwTypeError}catch(s){try{return ae(arguments,\"callee\").get}catch(s){return throwTypeError}}}():throwTypeError,pe=i(64039)(),de=i(93628),fe=i(71064),ye=i(48648),be=i(11002),_e=i(10076),Se={},we=\"undefined\"!=typeof Uint8Array&&de?de(Uint8Array):a,xe={__proto__:null,\"%AggregateError%\":\"undefined\"==typeof AggregateError?a:AggregateError,\"%Array%\":Array,\"%ArrayBuffer%\":\"undefined\"==typeof ArrayBuffer?a:ArrayBuffer,\"%ArrayIteratorPrototype%\":pe&&de?de([][Symbol.iterator]()):a,\"%AsyncFromSyncIteratorPrototype%\":a,\"%AsyncFunction%\":Se,\"%AsyncGenerator%\":Se,\"%AsyncGeneratorFunction%\":Se,\"%AsyncIteratorPrototype%\":Se,\"%Atomics%\":\"undefined\"==typeof Atomics?a:Atomics,\"%BigInt%\":\"undefined\"==typeof BigInt?a:BigInt,\"%BigInt64Array%\":\"undefined\"==typeof BigInt64Array?a:BigInt64Array,\"%BigUint64Array%\":\"undefined\"==typeof BigUint64Array?a:BigUint64Array,\"%Boolean%\":Boolean,\"%DataView%\":\"undefined\"==typeof DataView?a:DataView,\"%Date%\":Date,\"%decodeURI%\":decodeURI,\"%decodeURIComponent%\":decodeURIComponent,\"%encodeURI%\":encodeURI,\"%encodeURIComponent%\":encodeURIComponent,\"%Error%\":_,\"%eval%\":eval,\"%EvalError%\":w,\"%Float32Array%\":\"undefined\"==typeof Float32Array?a:Float32Array,\"%Float64Array%\":\"undefined\"==typeof Float64Array?a:Float64Array,\"%FinalizationRegistry%\":\"undefined\"==typeof FinalizationRegistry?a:FinalizationRegistry,\"%Function%\":ie,\"%GeneratorFunction%\":Se,\"%Int8Array%\":\"undefined\"==typeof Int8Array?a:Int8Array,\"%Int16Array%\":\"undefined\"==typeof Int16Array?a:Int16Array,\"%Int32Array%\":\"undefined\"==typeof Int32Array?a:Int32Array,\"%isFinite%\":isFinite,\"%isNaN%\":isNaN,\"%IteratorPrototype%\":pe&&de?de(de([][Symbol.iterator]())):a,\"%JSON%\":\"object\"==typeof JSON?JSON:a,\"%Map%\":\"undefined\"==typeof Map?a:Map,\"%MapIteratorPrototype%\":\"undefined\"!=typeof Map&&pe&&de?de((new Map)[Symbol.iterator]()):a,\"%Math%\":Math,\"%Number%\":Number,\"%Object%\":u,\"%Object.getOwnPropertyDescriptor%\":ae,\"%parseFloat%\":parseFloat,\"%parseInt%\":parseInt,\"%Promise%\":\"undefined\"==typeof Promise?a:Promise,\"%Proxy%\":\"undefined\"==typeof Proxy?a:Proxy,\"%RangeError%\":x,\"%ReferenceError%\":C,\"%Reflect%\":\"undefined\"==typeof Reflect?a:Reflect,\"%RegExp%\":RegExp,\"%Set%\":\"undefined\"==typeof Set?a:Set,\"%SetIteratorPrototype%\":\"undefined\"!=typeof Set&&pe&&de?de((new Set)[Symbol.iterator]()):a,\"%SharedArrayBuffer%\":\"undefined\"==typeof SharedArrayBuffer?a:SharedArrayBuffer,\"%String%\":String,\"%StringIteratorPrototype%\":pe&&de?de(\"\"[Symbol.iterator]()):a,\"%Symbol%\":pe?Symbol:a,\"%SyntaxError%\":j,\"%ThrowTypeError%\":le,\"%TypedArray%\":we,\"%TypeError%\":L,\"%Uint8Array%\":\"undefined\"==typeof Uint8Array?a:Uint8Array,\"%Uint8ClampedArray%\":\"undefined\"==typeof Uint8ClampedArray?a:Uint8ClampedArray,\"%Uint16Array%\":\"undefined\"==typeof Uint16Array?a:Uint16Array,\"%Uint32Array%\":\"undefined\"==typeof Uint32Array?a:Uint32Array,\"%URIError%\":B,\"%WeakMap%\":\"undefined\"==typeof WeakMap?a:WeakMap,\"%WeakRef%\":\"undefined\"==typeof WeakRef?a:WeakRef,\"%WeakSet%\":\"undefined\"==typeof WeakSet?a:WeakSet,\"%Function.prototype.call%\":_e,\"%Function.prototype.apply%\":be,\"%Object.defineProperty%\":ce,\"%Object.getPrototypeOf%\":fe,\"%Math.abs%\":$,\"%Math.floor%\":U,\"%Math.max%\":V,\"%Math.min%\":z,\"%Math.pow%\":Y,\"%Math.round%\":Z,\"%Math.sign%\":ee,\"%Reflect.getPrototypeOf%\":ye};if(de)try{null.error}catch(s){var Pe=de(de(s));xe[\"%Error.prototype%\"]=Pe}var Te=function doEval(s){var o;if(\"%AsyncFunction%\"===s)o=getEvalledConstructor(\"async function () {}\");else if(\"%GeneratorFunction%\"===s)o=getEvalledConstructor(\"function* () {}\");else if(\"%AsyncGeneratorFunction%\"===s)o=getEvalledConstructor(\"async function* () {}\");else if(\"%AsyncGenerator%\"===s){var i=doEval(\"%AsyncGeneratorFunction%\");i&&(o=i.prototype)}else if(\"%AsyncIteratorPrototype%\"===s){var a=doEval(\"%AsyncGenerator%\");a&&de&&(o=de(a.prototype))}return xe[s]=o,o},Re={__proto__:null,\"%ArrayBufferPrototype%\":[\"ArrayBuffer\",\"prototype\"],\"%ArrayPrototype%\":[\"Array\",\"prototype\"],\"%ArrayProto_entries%\":[\"Array\",\"prototype\",\"entries\"],\"%ArrayProto_forEach%\":[\"Array\",\"prototype\",\"forEach\"],\"%ArrayProto_keys%\":[\"Array\",\"prototype\",\"keys\"],\"%ArrayProto_values%\":[\"Array\",\"prototype\",\"values\"],\"%AsyncFunctionPrototype%\":[\"AsyncFunction\",\"prototype\"],\"%AsyncGenerator%\":[\"AsyncGeneratorFunction\",\"prototype\"],\"%AsyncGeneratorPrototype%\":[\"AsyncGeneratorFunction\",\"prototype\",\"prototype\"],\"%BooleanPrototype%\":[\"Boolean\",\"prototype\"],\"%DataViewPrototype%\":[\"DataView\",\"prototype\"],\"%DatePrototype%\":[\"Date\",\"prototype\"],\"%ErrorPrototype%\":[\"Error\",\"prototype\"],\"%EvalErrorPrototype%\":[\"EvalError\",\"prototype\"],\"%Float32ArrayPrototype%\":[\"Float32Array\",\"prototype\"],\"%Float64ArrayPrototype%\":[\"Float64Array\",\"prototype\"],\"%FunctionPrototype%\":[\"Function\",\"prototype\"],\"%Generator%\":[\"GeneratorFunction\",\"prototype\"],\"%GeneratorPrototype%\":[\"GeneratorFunction\",\"prototype\",\"prototype\"],\"%Int8ArrayPrototype%\":[\"Int8Array\",\"prototype\"],\"%Int16ArrayPrototype%\":[\"Int16Array\",\"prototype\"],\"%Int32ArrayPrototype%\":[\"Int32Array\",\"prototype\"],\"%JSONParse%\":[\"JSON\",\"parse\"],\"%JSONStringify%\":[\"JSON\",\"stringify\"],\"%MapPrototype%\":[\"Map\",\"prototype\"],\"%NumberPrototype%\":[\"Number\",\"prototype\"],\"%ObjectPrototype%\":[\"Object\",\"prototype\"],\"%ObjProto_toString%\":[\"Object\",\"prototype\",\"toString\"],\"%ObjProto_valueOf%\":[\"Object\",\"prototype\",\"valueOf\"],\"%PromisePrototype%\":[\"Promise\",\"prototype\"],\"%PromiseProto_then%\":[\"Promise\",\"prototype\",\"then\"],\"%Promise_all%\":[\"Promise\",\"all\"],\"%Promise_reject%\":[\"Promise\",\"reject\"],\"%Promise_resolve%\":[\"Promise\",\"resolve\"],\"%RangeErrorPrototype%\":[\"RangeError\",\"prototype\"],\"%ReferenceErrorPrototype%\":[\"ReferenceError\",\"prototype\"],\"%RegExpPrototype%\":[\"RegExp\",\"prototype\"],\"%SetPrototype%\":[\"Set\",\"prototype\"],\"%SharedArrayBufferPrototype%\":[\"SharedArrayBuffer\",\"prototype\"],\"%StringPrototype%\":[\"String\",\"prototype\"],\"%SymbolPrototype%\":[\"Symbol\",\"prototype\"],\"%SyntaxErrorPrototype%\":[\"SyntaxError\",\"prototype\"],\"%TypedArrayPrototype%\":[\"TypedArray\",\"prototype\"],\"%TypeErrorPrototype%\":[\"TypeError\",\"prototype\"],\"%Uint8ArrayPrototype%\":[\"Uint8Array\",\"prototype\"],\"%Uint8ClampedArrayPrototype%\":[\"Uint8ClampedArray\",\"prototype\"],\"%Uint16ArrayPrototype%\":[\"Uint16Array\",\"prototype\"],\"%Uint32ArrayPrototype%\":[\"Uint32Array\",\"prototype\"],\"%URIErrorPrototype%\":[\"URIError\",\"prototype\"],\"%WeakMapPrototype%\":[\"WeakMap\",\"prototype\"],\"%WeakSetPrototype%\":[\"WeakSet\",\"prototype\"]},$e=i(66743),qe=i(9957),ze=$e.call(_e,Array.prototype.concat),We=$e.call(be,Array.prototype.splice),He=$e.call(_e,String.prototype.replace),Ye=$e.call(_e,String.prototype.slice),Xe=$e.call(_e,RegExp.prototype.exec),Qe=/[^%.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|%$))/g,et=/\\\\(\\\\)?/g,tt=function getBaseIntrinsic(s,o){var i,a=s;if(qe(Re,a)&&(a=\"%\"+(i=Re[a])[0]+\"%\"),qe(xe,a)){var u=xe[a];if(u===Se&&(u=Te(a)),void 0===u&&!o)throw new L(\"intrinsic \"+s+\" exists, but is not available. Please file an issue!\");return{alias:i,name:a,value:u}}throw new j(\"intrinsic \"+s+\" does not exist!\")};s.exports=function GetIntrinsic(s,o){if(\"string\"!=typeof s||0===s.length)throw new L(\"intrinsic name must be a non-empty string\");if(arguments.length>1&&\"boolean\"!=typeof o)throw new L('\"allowMissing\" argument must be a boolean');if(null===Xe(/^%?[^%]*%?$/,s))throw new j(\"`%` may not be present anywhere but at the beginning and end of the intrinsic name\");var i=function stringToPath(s){var o=Ye(s,0,1),i=Ye(s,-1);if(\"%\"===o&&\"%\"!==i)throw new j(\"invalid intrinsic syntax, expected closing `%`\");if(\"%\"===i&&\"%\"!==o)throw new j(\"invalid intrinsic syntax, expected opening `%`\");var a=[];return He(s,Qe,(function(s,o,i,u){a[a.length]=i?He(u,et,\"$1\"):o||s})),a}(s),a=i.length>0?i[0]:\"\",u=tt(\"%\"+a+\"%\",o),_=u.name,w=u.value,x=!1,C=u.alias;C&&(a=C[0],We(i,ze([0,1],C)));for(var B=1,$=!0;B<i.length;B+=1){var U=i[B],V=Ye(U,0,1),z=Ye(U,-1);if(('\"'===V||\"'\"===V||\"`\"===V||'\"'===z||\"'\"===z||\"`\"===z)&&V!==z)throw new j(\"property names with quotes must have matching quotes\");if(\"constructor\"!==U&&$||(x=!0),qe(xe,_=\"%\"+(a+=\".\"+U)+\"%\"))w=xe[_];else if(null!=w){if(!(U in w)){if(!o)throw new L(\"base intrinsic for \"+s+\" exists, but the property is not available.\");return}if(ae&&B+1>=i.length){var Y=ae(w,U);w=($=!!Y)&&\"get\"in Y&&!(\"originalValue\"in Y.get)?Y.get:w[U]}else $=qe(w,U),w=w[U];$&&!x&&(xe[_]=w)}}return w}},70470:(s,o,i)=>{\"use strict\";var a=i(46028),u=i(25594);s.exports=function(s){var o=a(s,\"string\");return u(o)?o:o+\"\"}},70695:(s,o,i)=>{var a=i(78096),u=i(72428),_=i(56449),w=i(3656),x=i(30361),C=i(37167),j=Object.prototype.hasOwnProperty;s.exports=function arrayLikeKeys(s,o){var i=_(s),L=!i&&u(s),B=!i&&!L&&w(s),$=!i&&!L&&!B&&C(s),U=i||L||B||$,V=U?a(s.length,String):[],z=V.length;for(var Y in s)!o&&!j.call(s,Y)||U&&(\"length\"==Y||B&&(\"offset\"==Y||\"parent\"==Y)||$&&(\"buffer\"==Y||\"byteLength\"==Y||\"byteOffset\"==Y)||x(Y,z))||V.push(Y);return V}},70981:(s,o,i)=>{var a=i(75251),u=i(62060),_=i(32865),w=i(75948);s.exports=function setWrapToString(s,o,i){var x=o+\"\";return _(s,u(x,w(a(x),i)))}},71064:(s,o,i)=>{\"use strict\";var a=i(79612);s.exports=a.getPrototypeOf||null},71167:(s,o,i)=>{const a=i(10316);s.exports=class StringElement extends a{constructor(s,o,i){super(s,o,i),this.element=\"string\"}primitive(){return\"string\"}get length(){return this.content.length}}},71340:(s,o,i)=>{\"use strict\";var a=i(11091),u=i(29538);a({target:\"Object\",stat:!0,arity:2,forced:Object.assign!==u},{assign:u})},71514:s=>{\"use strict\";s.exports=Math.abs},71961:(s,o,i)=>{var a=i(49653);s.exports=function cloneTypedArray(s,o){var i=o?a(s.buffer):s.buffer;return new s.constructor(i,s.byteOffset,s.length)}},72428:(s,o,i)=>{var a=i(27534),u=i(40346),_=Object.prototype,w=_.hasOwnProperty,x=_.propertyIsEnumerable,C=a(function(){return arguments}())?a:function(s){return u(s)&&w.call(s,\"callee\")&&!x.call(s,\"callee\")};s.exports=C},72552:(s,o,i)=>{var a=i(51873),u=i(659),_=i(59350),w=a?a.toStringTag:void 0;s.exports=function baseGetTag(s){return null==s?void 0===s?\"[object Undefined]\":\"[object Null]\":w&&w in Object(s)?u(s):_(s)}},72903:(s,o,i)=>{var a=i(23805),u=i(55527),_=i(90181),w=Object.prototype.hasOwnProperty;s.exports=function baseKeysIn(s){if(!a(s))return _(s);var o=u(s),i=[];for(var x in s)(\"constructor\"!=x||!o&&w.call(s,x))&&i.push(x);return i}},72949:(s,o,i)=>{var a=i(12651);s.exports=function mapCacheSet(s,o){var i=a(this,s),u=i.size;return i.set(s,o),this.size+=i.size==u?0:1,this}},73093:(s,o,i)=>{\"use strict\";var a=i(94459);s.exports=function sign(s){return a(s)||0===s?s:s<0?-1:1}},73126:(s,o,i)=>{\"use strict\";var a=i(66743),u=i(69675),_=i(10076),w=i(13144);s.exports=function callBindBasic(s){if(s.length<1||\"function\"!=typeof s[0])throw new u(\"a function is required\");return w(a,_,s)}},73170:(s,o,i)=>{var a=i(16547),u=i(31769),_=i(30361),w=i(23805),x=i(77797);s.exports=function baseSet(s,o,i,C){if(!w(s))return s;for(var j=-1,L=(o=u(o,s)).length,B=L-1,$=s;null!=$&&++j<L;){var U=x(o[j]),V=i;if(\"__proto__\"===U||\"constructor\"===U||\"prototype\"===U)return s;if(j!=B){var z=$[U];void 0===(V=C?C(z,U,$):void 0)&&(V=w(z)?z:_(o[j+1])?[]:{})}a($,U,V),$=$[U]}return s}},73201:s=>{var o=/\\w*$/;s.exports=function cloneRegExp(s){var i=new s.constructor(s.source,o.exec(s));return i.lastIndex=s.lastIndex,i}},73402:s=>{function concat(...s){return s.map((s=>function source(s){return s?\"string\"==typeof s?s:s.source:null}(s))).join(\"\")}s.exports=function http(s){const o=\"HTTP/(2|1\\\\.[01])\",i={className:\"attribute\",begin:concat(\"^\",/[A-Za-z][A-Za-z0-9-]*/,\"(?=\\\\:\\\\s)\"),starts:{contains:[{className:\"punctuation\",begin:/: /,relevance:0,starts:{end:\"$\",relevance:0}}]}},a=[i,{begin:\"\\\\n\\\\n\",starts:{subLanguage:[],endsWithParent:!0}}];return{name:\"HTTP\",aliases:[\"https\"],illegal:/\\S/,contains:[{begin:\"^(?=\"+o+\" \\\\d{3})\",end:/$/,contains:[{className:\"meta\",begin:o},{className:\"number\",begin:\"\\\\b\\\\d{3}\\\\b\"}],starts:{end:/\\b\\B/,illegal:/\\S/,contains:a}},{begin:\"(?=^[A-Z]+ (.*?) \"+o+\"$)\",end:/$/,contains:[{className:\"string\",begin:\" \",end:\" \",excludeBegin:!0,excludeEnd:!0},{className:\"meta\",begin:o},{className:\"keyword\",begin:\"[A-Z]+\"}],starts:{end:/\\b\\B/,illegal:/\\S/,contains:a}},s.inherit(i,{relevance:0})]}}},73424:(s,o,i)=>{var a=i(16962),u=i(2874),_=Array.prototype.push;function baseAry(s,o){return 2==o?function(o,i){return s(o,i)}:function(o){return s(o)}}function cloneArray(s){for(var o=s?s.length:0,i=Array(o);o--;)i[o]=s[o];return i}function wrapImmutable(s,o){return function(){var i=arguments.length;if(i){for(var a=Array(i);i--;)a[i]=arguments[i];var u=a[0]=o.apply(void 0,a);return s.apply(void 0,a),u}}}s.exports=function baseConvert(s,o,i,w){var x=\"function\"==typeof o,C=o===Object(o);if(C&&(w=i,i=o,o=void 0),null==i)throw new TypeError;w||(w={});var j=!(\"cap\"in w)||w.cap,L=!(\"curry\"in w)||w.curry,B=!(\"fixed\"in w)||w.fixed,$=!(\"immutable\"in w)||w.immutable,U=!(\"rearg\"in w)||w.rearg,V=x?i:u,z=\"curry\"in w&&w.curry,Y=\"fixed\"in w&&w.fixed,Z=\"rearg\"in w&&w.rearg,ee=x?i.runInContext():void 0,ie=x?i:{ary:s.ary,assign:s.assign,clone:s.clone,curry:s.curry,forEach:s.forEach,isArray:s.isArray,isError:s.isError,isFunction:s.isFunction,isWeakMap:s.isWeakMap,iteratee:s.iteratee,keys:s.keys,rearg:s.rearg,toInteger:s.toInteger,toPath:s.toPath},ae=ie.ary,ce=ie.assign,le=ie.clone,pe=ie.curry,de=ie.forEach,fe=ie.isArray,ye=ie.isError,be=ie.isFunction,_e=ie.isWeakMap,Se=ie.keys,we=ie.rearg,xe=ie.toInteger,Pe=ie.toPath,Te=Se(a.aryMethod),Re={castArray:function(s){return function(){var o=arguments[0];return fe(o)?s(cloneArray(o)):s.apply(void 0,arguments)}},iteratee:function(s){return function(){var o=arguments[1],i=s(arguments[0],o),a=i.length;return j&&\"number\"==typeof o?(o=o>2?o-2:1,a&&a<=o?i:baseAry(i,o)):i}},mixin:function(s){return function(o){var i=this;if(!be(i))return s(i,Object(o));var a=[];return de(Se(o),(function(s){be(o[s])&&a.push([s,i.prototype[s]])})),s(i,Object(o)),de(a,(function(s){var o=s[1];be(o)?i.prototype[s[0]]=o:delete i.prototype[s[0]]})),i}},nthArg:function(s){return function(o){var i=o<0?1:xe(o)+1;return pe(s(o),i)}},rearg:function(s){return function(o,i){var a=i?i.length:0;return pe(s(o,i),a)}},runInContext:function(o){return function(i){return baseConvert(s,o(i),w)}}};function castCap(s,o){if(j){var i=a.iterateeRearg[s];if(i)return function iterateeRearg(s,o){return overArg(s,(function(s){var i=o.length;return function baseArity(s,o){return 2==o?function(o,i){return s.apply(void 0,arguments)}:function(o){return s.apply(void 0,arguments)}}(we(baseAry(s,i),o),i)}))}(o,i);var u=!x&&a.iterateeAry[s];if(u)return function iterateeAry(s,o){return overArg(s,(function(s){return\"function\"==typeof s?baseAry(s,o):s}))}(o,u)}return o}function castFixed(s,o,i){if(B&&(Y||!a.skipFixed[s])){var u=a.methodSpread[s],w=u&&u.start;return void 0===w?ae(o,i):function flatSpread(s,o){return function(){for(var i=arguments.length,a=i-1,u=Array(i);i--;)u[i]=arguments[i];var w=u[o],x=u.slice(0,o);return w&&_.apply(x,w),o!=a&&_.apply(x,u.slice(o+1)),s.apply(this,x)}}(o,w)}return o}function castRearg(s,o,i){return U&&i>1&&(Z||!a.skipRearg[s])?we(o,a.methodRearg[s]||a.aryRearg[i]):o}function cloneByPath(s,o){for(var i=-1,a=(o=Pe(o)).length,u=a-1,_=le(Object(s)),w=_;null!=w&&++i<a;){var x=o[i],C=w[x];null==C||be(C)||ye(C)||_e(C)||(w[x]=le(i==u?C:Object(C))),w=w[x]}return _}function createConverter(s,o){var i=a.aliasToReal[s]||s,u=a.remap[i]||i,_=w;return function(s){var a=x?ee:ie,w=x?ee[u]:o,C=ce(ce({},_),s);return baseConvert(a,i,w,C)}}function overArg(s,o){return function(){var i=arguments.length;if(!i)return s();for(var a=Array(i);i--;)a[i]=arguments[i];var u=U?0:i-1;return a[u]=o(a[u]),s.apply(void 0,a)}}function wrap(s,o,i){var u,_=a.aliasToReal[s]||s,w=o,x=Re[_];return x?w=x(o):$&&(a.mutate.array[_]?w=wrapImmutable(o,cloneArray):a.mutate.object[_]?w=wrapImmutable(o,function createCloner(s){return function(o){return s({},o)}}(o)):a.mutate.set[_]&&(w=wrapImmutable(o,cloneByPath))),de(Te,(function(s){return de(a.aryMethod[s],(function(o){if(_==o){var i=a.methodSpread[_],x=i&&i.afterRearg;return u=x?castFixed(_,castRearg(_,w,s),s):castRearg(_,castFixed(_,w,s),s),u=function castCurry(s,o,i){return z||L&&i>1?pe(o,i):o}(0,u=castCap(_,u),s),!1}})),!u})),u||(u=w),u==o&&(u=z?pe(u,1):function(){return o.apply(this,arguments)}),u.convert=createConverter(_,o),u.placeholder=o.placeholder=i,u}if(!C)return wrap(o,i,V);var $e=i,qe=[];return de(Te,(function(s){de(a.aryMethod[s],(function(s){var o=$e[a.remap[s]||s];o&&qe.push([s,wrap(s,o,$e)])}))})),de(Se($e),(function(s){var o=$e[s];if(\"function\"==typeof o){for(var i=qe.length;i--;)if(qe[i][0]==s)return;o.convert=createConverter(s,o),qe.push([s,o])}})),de(qe,(function(s){$e[s[0]]=s[1]})),$e.convert=function convertLib(s){return $e.runInContext.convert(s)(void 0)},$e.placeholder=$e,de(Se($e),(function(s){de(a.realToAlias[s]||[],(function(o){$e[o]=$e[s]}))})),$e}},73448:(s,o,i)=>{\"use strict\";var a=i(73948),u=i(29367),_=i(87136),w=i(93742),x=i(76264)(\"iterator\");s.exports=function(s){if(!_(s))return u(s,x)||u(s,\"@@iterator\")||w[a(s)]}},73648:(s,o,i)=>{\"use strict\";var a=i(39447),u=i(98828),_=i(49552);s.exports=!a&&!u((function(){return 7!==Object.defineProperty(_(\"div\"),\"a\",{get:function(){return 7}}).a}))},73948:(s,o,i)=>{\"use strict\";var a=i(52623),u=i(62250),_=i(45807),w=i(76264)(\"toStringTag\"),x=Object,C=\"Arguments\"===_(function(){return arguments}());s.exports=a?_:function(s){var o,i,a;return void 0===s?\"Undefined\":null===s?\"Null\":\"string\"==typeof(i=function(s,o){try{return s[o]}catch(s){}}(o=x(s),w))?i:C?_(o):\"Object\"===(a=_(o))&&u(o.callee)?\"Arguments\":a}},73992:(s,o)=>{\"use strict\";var i=Object.prototype.hasOwnProperty;function decode(s){try{return decodeURIComponent(s.replace(/\\+/g,\" \"))}catch(s){return null}}function encode(s){try{return encodeURIComponent(s)}catch(s){return null}}o.stringify=function querystringify(s,o){o=o||\"\";var a,u,_=[];for(u in\"string\"!=typeof o&&(o=\"?\"),s)if(i.call(s,u)){if((a=s[u])||null!=a&&!isNaN(a)||(a=\"\"),u=encode(u),a=encode(a),null===u||null===a)continue;_.push(u+\"=\"+a)}return _.length?o+_.join(\"&\"):\"\"},o.parse=function querystring(s){for(var o,i=/([^=?#&]+)=?([^&]*)/g,a={};o=i.exec(s);){var u=decode(o[1]),_=decode(o[2]);null===u||null===_||u in a||(a[u]=_)}return a}},74218:s=>{s.exports=function isKeyable(s){var o=typeof s;return\"string\"==o||\"number\"==o||\"symbol\"==o||\"boolean\"==o?\"__proto__\"!==s:null===s}},74239:(s,o,i)=>{\"use strict\";var a=i(87136),u=TypeError;s.exports=function(s){if(a(s))throw new u(\"Can't call method on \"+s);return s}},74284:(s,o,i)=>{\"use strict\";var a=i(39447),u=i(73648),_=i(58661),w=i(36624),x=i(70470),C=TypeError,j=Object.defineProperty,L=Object.getOwnPropertyDescriptor,B=\"enumerable\",$=\"configurable\",U=\"writable\";o.f=a?_?function defineProperty(s,o,i){if(w(s),o=x(o),w(i),\"function\"==typeof s&&\"prototype\"===o&&\"value\"in i&&U in i&&!i[U]){var a=L(s,o);a&&a[U]&&(s[o]=i.value,i={configurable:$ in i?i[$]:a[$],enumerable:B in i?i[B]:a[B],writable:!1})}return j(s,o,i)}:j:function defineProperty(s,o,i){if(w(s),o=x(o),w(i),u)try{return j(s,o,i)}catch(s){}if(\"get\"in i||\"set\"in i)throw new C(\"Accessors not supported\");return\"value\"in i&&(s[o]=i.value),s}},74335:s=>{s.exports=function overArg(s,o){return function(i){return s(o(i))}}},74372:(s,o,i)=>{\"use strict\";var a=i(69675),u=i(36556)(\"TypedArray.prototype.buffer\",!0),_=i(35680);s.exports=u||function typedArrayBuffer(s){if(!_(s))throw new a(\"Not a Typed Array\");return s.buffer}},74436:(s,o,i)=>{\"use strict\";var a=i(4993),u=i(34849),_=i(20575),createMethod=function(s){return function(o,i,w){var x=a(o),C=_(x);if(0===C)return!s&&-1;var j,L=u(w,C);if(s&&i!=i){for(;C>L;)if((j=x[L++])!=j)return!0}else for(;C>L;L++)if((s||L in x)&&x[L]===i)return s||L||0;return!s&&-1}};s.exports={includes:createMethod(!0),indexOf:createMethod(!1)}},74610:(s,o,i)=>{\"use strict\";s.exports=Transform;var a=i(86048).F,u=a.ERR_METHOD_NOT_IMPLEMENTED,_=a.ERR_MULTIPLE_CALLBACK,w=a.ERR_TRANSFORM_ALREADY_TRANSFORMING,x=a.ERR_TRANSFORM_WITH_LENGTH_0,C=i(25382);function afterTransform(s,o){var i=this._transformState;i.transforming=!1;var a=i.writecb;if(null===a)return this.emit(\"error\",new _);i.writechunk=null,i.writecb=null,null!=o&&this.push(o),a(s);var u=this._readableState;u.reading=!1,(u.needReadable||u.length<u.highWaterMark)&&this._read(u.highWaterMark)}function Transform(s){if(!(this instanceof Transform))return new Transform(s);C.call(this,s),this._transformState={afterTransform:afterTransform.bind(this),needTransform:!1,transforming:!1,writecb:null,writechunk:null,writeencoding:null},this._readableState.needReadable=!0,this._readableState.sync=!1,s&&(\"function\"==typeof s.transform&&(this._transform=s.transform),\"function\"==typeof s.flush&&(this._flush=s.flush)),this.on(\"prefinish\",prefinish)}function prefinish(){var s=this;\"function\"!=typeof this._flush||this._readableState.destroyed?done(this,null,null):this._flush((function(o,i){done(s,o,i)}))}function done(s,o,i){if(o)return s.emit(\"error\",o);if(null!=i&&s.push(i),s._writableState.length)throw new x;if(s._transformState.transforming)throw new w;return s.push(null)}i(56698)(Transform,C),Transform.prototype.push=function(s,o){return this._transformState.needTransform=!1,C.prototype.push.call(this,s,o)},Transform.prototype._transform=function(s,o,i){i(new u(\"_transform()\"))},Transform.prototype._write=function(s,o,i){var a=this._transformState;if(a.writecb=i,a.writechunk=s,a.writeencoding=o,!a.transforming){var u=this._readableState;(a.needTransform||u.needReadable||u.length<u.highWaterMark)&&this._read(u.highWaterMark)}},Transform.prototype._read=function(s){var o=this._transformState;null===o.writechunk||o.transforming?o.needTransform=!0:(o.transforming=!0,this._transform(o.writechunk,o.writeencoding,o.afterTransform))},Transform.prototype._destroy=function(s,o){C.prototype._destroy.call(this,s,(function(s){o(s)}))}},74733:(s,o,i)=>{var a=i(21791),u=i(95950);s.exports=function baseAssign(s,o){return s&&a(o,u(o),s)}},75147:(s,o,i)=>{const a=i(85105);s.exports=class JSON06Serialiser extends a{serialise(s){if(!(s instanceof this.namespace.elements.Element))throw new TypeError(`Given element \\`${s}\\` is not an Element instance`);let o;s._attributes&&s.attributes.get(\"variable\")&&(o=s.attributes.get(\"variable\"));const i={element:s.element};s._meta&&s._meta.length>0&&(i.meta=this.serialiseObject(s.meta));const a=\"enum\"===s.element||-1!==s.attributes.keys().indexOf(\"enumerations\");if(a){const o=this.enumSerialiseAttributes(s);o&&(i.attributes=o)}else if(s._attributes&&s._attributes.length>0){let{attributes:a}=s;a.get(\"metadata\")&&(a=a.clone(),a.set(\"meta\",a.get(\"metadata\")),a.remove(\"metadata\")),\"member\"===s.element&&o&&(a=a.clone(),a.remove(\"variable\")),a.length>0&&(i.attributes=this.serialiseObject(a))}if(a)i.content=this.enumSerialiseContent(s,i);else if(this[`${s.element}SerialiseContent`])i.content=this[`${s.element}SerialiseContent`](s,i);else if(void 0!==s.content){let a;o&&s.content.key?(a=s.content.clone(),a.key.attributes.set(\"variable\",o),a=this.serialiseContent(a)):a=this.serialiseContent(s.content),this.shouldSerialiseContent(s,a)&&(i.content=a)}else this.shouldSerialiseContent(s,s.content)&&s instanceof this.namespace.elements.Array&&(i.content=[]);return i}shouldSerialiseContent(s,o){return\"parseResult\"===s.element||\"httpRequest\"===s.element||\"httpResponse\"===s.element||\"category\"===s.element||\"link\"===s.element||void 0!==o&&(!Array.isArray(o)||0!==o.length)}refSerialiseContent(s,o){return delete o.attributes,{href:s.toValue(),path:s.path.toValue()}}sourceMapSerialiseContent(s){return s.toValue()}dataStructureSerialiseContent(s){return[this.serialiseContent(s.content)]}enumSerialiseAttributes(s){const o=s.attributes.clone(),i=o.remove(\"enumerations\")||new this.namespace.elements.Array([]),a=o.get(\"default\");let u=o.get(\"samples\")||new this.namespace.elements.Array([]);if(a&&a.content&&(a.content.attributes&&a.content.attributes.remove(\"typeAttributes\"),o.set(\"default\",new this.namespace.elements.Array([a.content]))),u.forEach((s=>{s.content&&s.content.element&&s.content.attributes.remove(\"typeAttributes\")})),s.content&&0!==i.length&&u.unshift(s.content),u=u.map((s=>s instanceof this.namespace.elements.Array?[s]:new this.namespace.elements.Array([s.content]))),u.length&&o.set(\"samples\",u),o.length>0)return this.serialiseObject(o)}enumSerialiseContent(s){if(s._attributes){const o=s.attributes.get(\"enumerations\");if(o&&o.length>0)return o.content.map((s=>{const o=s.clone();return o.attributes.remove(\"typeAttributes\"),this.serialise(o)}))}if(s.content){const o=s.content.clone();return o.attributes.remove(\"typeAttributes\"),[this.serialise(o)]}return[]}deserialise(s){if(\"string\"==typeof s)return new this.namespace.elements.String(s);if(\"number\"==typeof s)return new this.namespace.elements.Number(s);if(\"boolean\"==typeof s)return new this.namespace.elements.Boolean(s);if(null===s)return new this.namespace.elements.Null;if(Array.isArray(s))return new this.namespace.elements.Array(s.map(this.deserialise,this));const o=this.namespace.getElementClass(s.element),i=new o;i.element!==s.element&&(i.element=s.element),s.meta&&this.deserialiseObject(s.meta,i.meta),s.attributes&&this.deserialiseObject(s.attributes,i.attributes);const a=this.deserialiseContent(s.content);if(void 0===a&&null!==i.content||(i.content=a),\"enum\"===i.element){i.content&&i.attributes.set(\"enumerations\",i.content);let s=i.attributes.get(\"samples\");if(i.attributes.remove(\"samples\"),s){const a=s;s=new this.namespace.elements.Array,a.forEach((a=>{a.forEach((a=>{const u=new o(a);u.element=i.element,s.push(u)}))}));const u=s.shift();i.content=u?u.content:void 0,i.attributes.set(\"samples\",s)}else i.content=void 0;let a=i.attributes.get(\"default\");if(a&&a.length>0){a=a.get(0);const s=new o(a);s.element=i.element,i.attributes.set(\"default\",s)}}else if(\"dataStructure\"===i.element&&Array.isArray(i.content))[i.content]=i.content;else if(\"category\"===i.element){const s=i.attributes.get(\"meta\");s&&(i.attributes.set(\"metadata\",s),i.attributes.remove(\"meta\"))}else\"member\"===i.element&&i.key&&i.key._attributes&&i.key._attributes.getValue(\"variable\")&&(i.attributes.set(\"variable\",i.key.attributes.get(\"variable\")),i.key.attributes.remove(\"variable\"));return i}serialiseContent(s){if(s instanceof this.namespace.elements.Element)return this.serialise(s);if(s instanceof this.namespace.KeyValuePair){const o={key:this.serialise(s.key)};return s.value&&(o.value=this.serialise(s.value)),o}return s&&s.map?s.map(this.serialise,this):s}deserialiseContent(s){if(s){if(s.element)return this.deserialise(s);if(s.key){const o=new this.namespace.KeyValuePair(this.deserialise(s.key));return s.value&&(o.value=this.deserialise(s.value)),o}if(s.map)return s.map(this.deserialise,this)}return s}shouldRefract(s){return!!(s._attributes&&s.attributes.keys().length||s._meta&&s.meta.keys().length)||\"enum\"!==s.element&&(s.element!==s.primitive()||\"member\"===s.element)}convertKeyToRefract(s,o){return this.shouldRefract(o)?this.serialise(o):\"enum\"===o.element?this.serialiseEnum(o):\"array\"===o.element?o.map((o=>this.shouldRefract(o)||\"default\"===s?this.serialise(o):\"array\"===o.element||\"object\"===o.element||\"enum\"===o.element?o.children.map((s=>this.serialise(s))):o.toValue())):\"object\"===o.element?(o.content||[]).map(this.serialise,this):o.toValue()}serialiseEnum(s){return s.children.map((s=>this.serialise(s)))}serialiseObject(s){const o={};return s.forEach(((s,i)=>{if(s){const a=i.toValue();o[a]=this.convertKeyToRefract(a,s)}})),o}deserialiseObject(s,o){Object.keys(s).forEach((i=>{o.set(i,this.deserialise(s[i]))}))}}},75208:s=>{\"use strict\";var o,i=\"\";s.exports=function repeat(s,a){if(\"string\"!=typeof s)throw new TypeError(\"expected a string\");if(1===a)return s;if(2===a)return s+s;var u=s.length*a;if(o!==s||void 0===o)o=s,i=\"\";else if(i.length>=u)return i.substr(0,u);for(;u>i.length&&a>1;)1&a&&(i+=s),a>>=1,s+=s;return i=(i+=s).substr(0,u)}},75251:s=>{var o=/\\{\\n\\/\\* \\[wrapped with (.+)\\] \\*/,i=/,? & /;s.exports=function getWrapDetails(s){var a=s.match(o);return a?a[1].split(i):[]}},75288:s=>{s.exports=function eq(s,o){return s===o||s!=s&&o!=o}},75795:(s,o,i)=>{\"use strict\";var a=i(6549);if(a)try{a([],\"length\")}catch(s){a=null}s.exports=a},75817:s=>{\"use strict\";s.exports=function(s,o){return{enumerable:!(1&s),configurable:!(2&s),writable:!(4&s),value:o}}},75880:s=>{\"use strict\";s.exports=Math.pow},75896:(s,o,i)=>{\"use strict\";var a=i(65606);function emitErrorAndCloseNT(s,o){emitErrorNT(s,o),emitCloseNT(s)}function emitCloseNT(s){s._writableState&&!s._writableState.emitClose||s._readableState&&!s._readableState.emitClose||s.emit(\"close\")}function emitErrorNT(s,o){s.emit(\"error\",o)}s.exports={destroy:function destroy(s,o){var i=this,u=this._readableState&&this._readableState.destroyed,_=this._writableState&&this._writableState.destroyed;return u||_?(o?o(s):s&&(this._writableState?this._writableState.errorEmitted||(this._writableState.errorEmitted=!0,a.nextTick(emitErrorNT,this,s)):a.nextTick(emitErrorNT,this,s)),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(s||null,(function(s){!o&&s?i._writableState?i._writableState.errorEmitted?a.nextTick(emitCloseNT,i):(i._writableState.errorEmitted=!0,a.nextTick(emitErrorAndCloseNT,i,s)):a.nextTick(emitErrorAndCloseNT,i,s):o?(a.nextTick(emitCloseNT,i),o(s)):a.nextTick(emitCloseNT,i)})),this)},undestroy:function undestroy(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finalCalled=!1,this._writableState.prefinished=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)},errorOrDestroy:function errorOrDestroy(s,o){var i=s._readableState,a=s._writableState;i&&i.autoDestroy||a&&a.autoDestroy?s.destroy(o):s.emit(\"error\",o)}}},75948:(s,o,i)=>{var a=i(83729),u=i(15325),_=[[\"ary\",128],[\"bind\",1],[\"bindKey\",2],[\"curry\",8],[\"curryRight\",16],[\"flip\",512],[\"partial\",32],[\"partialRight\",64],[\"rearg\",256]];s.exports=function updateWrapDetails(s,o){return a(_,(function(i){var a=\"_.\"+i[0];o&i[1]&&!u(s,a)&&s.push(a)})),s.sort()}},76024:(s,o,i)=>{\"use strict\";var a=i(41505),u=Function.prototype,_=u.apply,w=u.call;s.exports=\"object\"==typeof Reflect&&Reflect.apply||(a?w.bind(_):function(){return w.apply(_,arguments)})},76169:(s,o,i)=>{var a=i(49653);s.exports=function cloneDataView(s,o){var i=o?a(s.buffer):s.buffer;return new s.constructor(i,s.byteOffset,s.byteLength)}},76189:s=>{var o=Object.prototype.hasOwnProperty;s.exports=function initCloneArray(s){var i=s.length,a=new s.constructor(i);return i&&\"string\"==typeof s[0]&&o.call(s,\"index\")&&(a.index=s.index,a.input=s.input),a}},76264:(s,o,i)=>{\"use strict\";var a=i(45951),u=i(85816),_=i(49724),w=i(6499),x=i(19846),C=i(51175),j=a.Symbol,L=u(\"wks\"),B=C?j.for||j:j&&j.withoutSetter||w;s.exports=function(s){return _(L,s)||(L[s]=x&&_(j,s)?j[s]:B(\"Symbol.\"+s)),L[s]}},76545:(s,o,i)=>{var a=i(56110)(i(9325),\"Set\");s.exports=a},76578:s=>{\"use strict\";s.exports=[\"Float16Array\",\"Float32Array\",\"Float64Array\",\"Int8Array\",\"Int16Array\",\"Int32Array\",\"Uint8Array\",\"Uint8ClampedArray\",\"Uint16Array\",\"Uint32Array\",\"BigInt64Array\",\"BigUint64Array\"]},76959:s=>{s.exports=function strictIndexOf(s,o,i){for(var a=i-1,u=s.length;++a<u;)if(s[a]===o)return a;return-1}},77078:(s,o,i)=>{var a=i(91033),u=i(82819),_=i(37471),w=i(18073),x=i(11287),C=i(36306),j=i(9325);s.exports=function createCurry(s,o,i){var L=u(s);return function wrapper(){for(var u=arguments.length,B=Array(u),$=u,U=x(wrapper);$--;)B[$]=arguments[$];var V=u<3&&B[0]!==U&&B[u-1]!==U?[]:C(B,U);return(u-=V.length)<i?w(s,o,_,wrapper.placeholder,void 0,B,V,void 0,void 0,i-u):a(this&&this!==j&&this instanceof wrapper?L:s,this,B)}}},77199:(s,o,i)=>{var a=i(49653),u=i(76169),_=i(73201),w=i(93736),x=i(71961);s.exports=function initCloneByTag(s,o,i){var C=s.constructor;switch(o){case\"[object ArrayBuffer]\":return a(s);case\"[object Boolean]\":case\"[object Date]\":return new C(+s);case\"[object DataView]\":return u(s,i);case\"[object Float32Array]\":case\"[object Float64Array]\":case\"[object Int8Array]\":case\"[object Int16Array]\":case\"[object Int32Array]\":case\"[object Uint8Array]\":case\"[object Uint8ClampedArray]\":case\"[object Uint16Array]\":case\"[object Uint32Array]\":return x(s,i);case\"[object Map]\":case\"[object Set]\":return new C;case\"[object Number]\":case\"[object String]\":return new C(s);case\"[object RegExp]\":return _(s);case\"[object Symbol]\":return w(s)}}},77556:(s,o,i)=>{var a=i(51873),u=i(34932),_=i(56449),w=i(44394),x=a?a.prototype:void 0,C=x?x.toString:void 0;s.exports=function baseToString(s){if(\"string\"==typeof s)return s;if(_(s))return u(s,baseToString)+\"\";if(w(s))return C?C.call(s):\"\";var o=s+\"\";return\"0\"==o&&1/s==-1/0?\"-0\":o}},77731:(s,o,i)=>{var a=i(79920)(\"set\",i(63560));a.placeholder=i(2874),s.exports=a},77797:(s,o,i)=>{var a=i(44394);s.exports=function toKey(s){if(\"string\"==typeof s||a(s))return s;var o=s+\"\";return\"0\"==o&&1/s==-1/0?\"-0\":o}},78004:s=>{\"use strict\";class SubRange{constructor(s,o){this.low=s,this.high=o,this.length=1+o-s}overlaps(s){return!(this.high<s.low||this.low>s.high)}touches(s){return!(this.high+1<s.low||this.low-1>s.high)}add(s){return new SubRange(Math.min(this.low,s.low),Math.max(this.high,s.high))}subtract(s){return s.low<=this.low&&s.high>=this.high?[]:s.low>this.low&&s.high<this.high?[new SubRange(this.low,s.low-1),new SubRange(s.high+1,this.high)]:s.low<=this.low?[new SubRange(s.high+1,this.high)]:[new SubRange(this.low,s.low-1)]}toString(){return this.low==this.high?this.low.toString():this.low+\"-\"+this.high}}class DRange{constructor(s,o){this.ranges=[],this.length=0,null!=s&&this.add(s,o)}_update_length(){this.length=this.ranges.reduce(((s,o)=>s+o.length),0)}add(s,o){var _add=s=>{for(var o=0;o<this.ranges.length&&!s.touches(this.ranges[o]);)o++;for(var i=this.ranges.slice(0,o);o<this.ranges.length&&s.touches(this.ranges[o]);)s=s.add(this.ranges[o]),o++;i.push(s),this.ranges=i.concat(this.ranges.slice(o)),this._update_length()};return s instanceof DRange?s.ranges.forEach(_add):(null==o&&(o=s),_add(new SubRange(s,o))),this}subtract(s,o){var _subtract=s=>{for(var o=0;o<this.ranges.length&&!s.overlaps(this.ranges[o]);)o++;for(var i=this.ranges.slice(0,o);o<this.ranges.length&&s.overlaps(this.ranges[o]);)i=i.concat(this.ranges[o].subtract(s)),o++;this.ranges=i.concat(this.ranges.slice(o)),this._update_length()};return s instanceof DRange?s.ranges.forEach(_subtract):(null==o&&(o=s),_subtract(new SubRange(s,o))),this}intersect(s,o){var i=[],_intersect=s=>{for(var o=0;o<this.ranges.length&&!s.overlaps(this.ranges[o]);)o++;for(;o<this.ranges.length&&s.overlaps(this.ranges[o]);){var a=Math.max(this.ranges[o].low,s.low),u=Math.min(this.ranges[o].high,s.high);i.push(new SubRange(a,u)),o++}};return s instanceof DRange?s.ranges.forEach(_intersect):(null==o&&(o=s),_intersect(new SubRange(s,o))),this.ranges=i,this._update_length(),this}index(s){for(var o=0;o<this.ranges.length&&this.ranges[o].length<=s;)s-=this.ranges[o].length,o++;return this.ranges[o].low+s}toString(){return\"[ \"+this.ranges.join(\", \")+\" ]\"}clone(){return new DRange(this)}numbers(){return this.ranges.reduce(((s,o)=>{for(var i=o.low;i<=o.high;)s.push(i),i++;return s}),[])}subranges(){return this.ranges.map((s=>({low:s.low,high:s.high,length:1+s.high-s.low})))}}s.exports=DRange},78096:s=>{s.exports=function baseTimes(s,o){for(var i=-1,a=Array(s);++i<s;)a[i]=o(i);return a}},78418:(s,o,i)=>{\"use strict\";i(85160)},79192:(s,o,i)=>{\"use strict\";var a=i(51871),u=i(46285),_=i(74239),w=i(10043);s.exports=Object.setPrototypeOf||(\"__proto__\"in{}?function(){var s,o=!1,i={};try{(s=a(Object.prototype,\"__proto__\",\"set\"))(i,[]),o=i instanceof Array}catch(s){}return function setPrototypeOf(i,a){return _(i),w(a),u(i)?(o?s(i,a):i.__proto__=a,i):i}}():void 0)},79290:s=>{\"use strict\";s.exports=RangeError},79307:(s,o,i)=>{\"use strict\";var a=i(11091),u=i(44673);a({target:\"Function\",proto:!0,forced:Function.bind!==u},{bind:u})},79538:s=>{\"use strict\";s.exports=ReferenceError},79612:s=>{\"use strict\";s.exports=Object},79770:s=>{s.exports=function arrayFilter(s,o){for(var i=-1,a=null==s?0:s.length,u=0,_=[];++i<a;){var w=s[i];o(w,i,s)&&(_[u++]=w)}return _}},79838:()=>{},79920:(s,o,i)=>{var a=i(73424),u=i(47934);s.exports=function convert(s,o,i){return a(u,s,o,i)}},80079:(s,o,i)=>{var a=i(63702),u=i(70080),_=i(24739),w=i(48655),x=i(31175);function ListCache(s){var o=-1,i=null==s?0:s.length;for(this.clear();++o<i;){var a=s[o];this.set(a[0],a[1])}}ListCache.prototype.clear=a,ListCache.prototype.delete=u,ListCache.prototype.get=_,ListCache.prototype.has=w,ListCache.prototype.set=x,s.exports=ListCache},80218:(s,o,i)=>{var a=i(13222);s.exports=function toLower(s){return a(s).toLowerCase()}},80257:(s,o,i)=>{var a=i(30980),u=i(56017),_=i(23007);s.exports=function wrapperClone(s){if(s instanceof a)return s.clone();var o=new u(s.__wrapped__,s.__chain__);return o.__actions__=_(s.__actions__),o.__index__=s.__index__,o.__values__=s.__values__,o}},80345:(s,o,i)=>{\"use strict\";function ownKeys(s,o){var i=Object.keys(s);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(s);o&&(a=a.filter((function(o){return Object.getOwnPropertyDescriptor(s,o).enumerable}))),i.push.apply(i,a)}return i}function _objectSpread(s){for(var o=1;o<arguments.length;o++){var i=null!=arguments[o]?arguments[o]:{};o%2?ownKeys(Object(i),!0).forEach((function(o){_defineProperty(s,o,i[o])})):Object.getOwnPropertyDescriptors?Object.defineProperties(s,Object.getOwnPropertyDescriptors(i)):ownKeys(Object(i)).forEach((function(o){Object.defineProperty(s,o,Object.getOwnPropertyDescriptor(i,o))}))}return s}function _defineProperty(s,o,i){return(o=_toPropertyKey(o))in s?Object.defineProperty(s,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):s[o]=i,s}function _defineProperties(s,o){for(var i=0;i<o.length;i++){var a=o[i];a.enumerable=a.enumerable||!1,a.configurable=!0,\"value\"in a&&(a.writable=!0),Object.defineProperty(s,_toPropertyKey(a.key),a)}}function _toPropertyKey(s){var o=function _toPrimitive(s,o){if(\"object\"!=typeof s||null===s)return s;var i=s[Symbol.toPrimitive];if(void 0!==i){var a=i.call(s,o||\"default\");if(\"object\"!=typeof a)return a;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===o?String:Number)(s)}(s,\"string\");return\"symbol\"==typeof o?o:String(o)}var a=i(48287).Buffer,u=i(15340).inspect,_=u&&u.custom||\"inspect\";s.exports=function(){function BufferList(){!function _classCallCheck(s,o){if(!(s instanceof o))throw new TypeError(\"Cannot call a class as a function\")}(this,BufferList),this.head=null,this.tail=null,this.length=0}return function _createClass(s,o,i){return o&&_defineProperties(s.prototype,o),i&&_defineProperties(s,i),Object.defineProperty(s,\"prototype\",{writable:!1}),s}(BufferList,[{key:\"push\",value:function push(s){var o={data:s,next:null};this.length>0?this.tail.next=o:this.head=o,this.tail=o,++this.length}},{key:\"unshift\",value:function unshift(s){var o={data:s,next:this.head};0===this.length&&(this.tail=o),this.head=o,++this.length}},{key:\"shift\",value:function shift(){if(0!==this.length){var s=this.head.data;return 1===this.length?this.head=this.tail=null:this.head=this.head.next,--this.length,s}}},{key:\"clear\",value:function clear(){this.head=this.tail=null,this.length=0}},{key:\"join\",value:function join(s){if(0===this.length)return\"\";for(var o=this.head,i=\"\"+o.data;o=o.next;)i+=s+o.data;return i}},{key:\"concat\",value:function concat(s){if(0===this.length)return a.alloc(0);for(var o,i,u,_=a.allocUnsafe(s>>>0),w=this.head,x=0;w;)o=w.data,i=_,u=x,a.prototype.copy.call(o,i,u),x+=w.data.length,w=w.next;return _}},{key:\"consume\",value:function consume(s,o){var i;return s<this.head.data.length?(i=this.head.data.slice(0,s),this.head.data=this.head.data.slice(s)):i=s===this.head.data.length?this.shift():o?this._getString(s):this._getBuffer(s),i}},{key:\"first\",value:function first(){return this.head.data}},{key:\"_getString\",value:function _getString(s){var o=this.head,i=1,a=o.data;for(s-=a.length;o=o.next;){var u=o.data,_=s>u.length?u.length:s;if(_===u.length?a+=u:a+=u.slice(0,s),0===(s-=_)){_===u.length?(++i,o.next?this.head=o.next:this.head=this.tail=null):(this.head=o,o.data=u.slice(_));break}++i}return this.length-=i,a}},{key:\"_getBuffer\",value:function _getBuffer(s){var o=a.allocUnsafe(s),i=this.head,u=1;for(i.data.copy(o),s-=i.data.length;i=i.next;){var _=i.data,w=s>_.length?_.length:s;if(_.copy(o,o.length-s,0,w),0===(s-=w)){w===_.length?(++u,i.next?this.head=i.next:this.head=this.tail=null):(this.head=i,i.data=_.slice(w));break}++u}return this.length-=u,o}},{key:_,value:function value(s,o){return u(this,_objectSpread(_objectSpread({},o),{},{depth:0,customInspect:!1}))}}]),BufferList}()},80376:s=>{\"use strict\";s.exports=[\"constructor\",\"hasOwnProperty\",\"isPrototypeOf\",\"propertyIsEnumerable\",\"toLocaleString\",\"toString\",\"valueOf\"]},80631:(s,o,i)=>{var a=i(28077),u=i(49326);s.exports=function hasIn(s,o){return null!=s&&u(s,o,a)}},80909:(s,o,i)=>{var a=i(30641),u=i(38329)(a);s.exports=u},80945:(s,o,i)=>{var a=i(80079),u=i(68223),_=i(53661);s.exports=function stackSet(s,o){var i=this.__data__;if(i instanceof a){var w=i.__data__;if(!u||w.length<199)return w.push([s,o]),this.size=++i.size,this;i=this.__data__=new _(w)}return i.set(s,o),this.size=i.size,this}},81042:(s,o,i)=>{var a=i(56110)(Object,\"create\");s.exports=a},81214:(s,o,i)=>{\"use strict\";function _typeof(s){return _typeof=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(s){return typeof s}:function(s){return s&&\"function\"==typeof Symbol&&s.constructor===Symbol&&s!==Symbol.prototype?\"symbol\":typeof s},_typeof(s)}Object.defineProperty(o,\"__esModule\",{value:!0}),o.DebounceInput=void 0;var a=_interopRequireDefault(i(96540)),u=_interopRequireDefault(i(20181)),_=[\"element\",\"onChange\",\"value\",\"minLength\",\"debounceTimeout\",\"forceNotifyByEnter\",\"forceNotifyOnBlur\",\"onKeyDown\",\"onBlur\",\"inputRef\"];function _interopRequireDefault(s){return s&&s.__esModule?s:{default:s}}function _objectWithoutProperties(s,o){if(null==s)return{};var i,a,u=function _objectWithoutPropertiesLoose(s,o){if(null==s)return{};var i,a,u={},_=Object.keys(s);for(a=0;a<_.length;a++)i=_[a],o.indexOf(i)>=0||(u[i]=s[i]);return u}(s,o);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(s);for(a=0;a<_.length;a++)i=_[a],o.indexOf(i)>=0||Object.prototype.propertyIsEnumerable.call(s,i)&&(u[i]=s[i])}return u}function ownKeys(s,o){var i=Object.keys(s);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(s);o&&(a=a.filter((function(o){return Object.getOwnPropertyDescriptor(s,o).enumerable}))),i.push.apply(i,a)}return i}function _objectSpread(s){for(var o=1;o<arguments.length;o++){var i=null!=arguments[o]?arguments[o]:{};o%2?ownKeys(Object(i),!0).forEach((function(o){_defineProperty(s,o,i[o])})):Object.getOwnPropertyDescriptors?Object.defineProperties(s,Object.getOwnPropertyDescriptors(i)):ownKeys(Object(i)).forEach((function(o){Object.defineProperty(s,o,Object.getOwnPropertyDescriptor(i,o))}))}return s}function _defineProperties(s,o){for(var i=0;i<o.length;i++){var a=o[i];a.enumerable=a.enumerable||!1,a.configurable=!0,\"value\"in a&&(a.writable=!0),Object.defineProperty(s,a.key,a)}}function _setPrototypeOf(s,o){return _setPrototypeOf=Object.setPrototypeOf||function _setPrototypeOf(s,o){return s.__proto__=o,s},_setPrototypeOf(s,o)}function _createSuper(s){var o=function _isNativeReflectConstruct(){if(\"undefined\"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if(\"function\"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(s){return!1}}();return function _createSuperInternal(){var i,a=_getPrototypeOf(s);if(o){var u=_getPrototypeOf(this).constructor;i=Reflect.construct(a,arguments,u)}else i=a.apply(this,arguments);return function _possibleConstructorReturn(s,o){if(o&&(\"object\"===_typeof(o)||\"function\"==typeof o))return o;if(void 0!==o)throw new TypeError(\"Derived constructors may only return object or undefined\");return _assertThisInitialized(s)}(this,i)}}function _assertThisInitialized(s){if(void 0===s)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return s}function _getPrototypeOf(s){return _getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf:function _getPrototypeOf(s){return s.__proto__||Object.getPrototypeOf(s)},_getPrototypeOf(s)}function _defineProperty(s,o,i){return o in s?Object.defineProperty(s,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):s[o]=i,s}var w=function(s){!function _inherits(s,o){if(\"function\"!=typeof o&&null!==o)throw new TypeError(\"Super expression must either be null or a function\");s.prototype=Object.create(o&&o.prototype,{constructor:{value:s,writable:!0,configurable:!0}}),Object.defineProperty(s,\"prototype\",{writable:!1}),o&&_setPrototypeOf(s,o)}(DebounceInput,s);var o=_createSuper(DebounceInput);function DebounceInput(s){var i;!function _classCallCheck(s,o){if(!(s instanceof o))throw new TypeError(\"Cannot call a class as a function\")}(this,DebounceInput),_defineProperty(_assertThisInitialized(i=o.call(this,s)),\"onChange\",(function(s){s.persist();var o=i.state.value,a=i.props.minLength;i.setState({value:s.target.value},(function(){var u=i.state.value;u.length>=a?i.notify(s):o.length>u.length&&i.notify(_objectSpread(_objectSpread({},s),{},{target:_objectSpread(_objectSpread({},s.target),{},{value:\"\"})}))}))})),_defineProperty(_assertThisInitialized(i),\"onKeyDown\",(function(s){\"Enter\"===s.key&&i.forceNotify(s);var o=i.props.onKeyDown;o&&(s.persist(),o(s))})),_defineProperty(_assertThisInitialized(i),\"onBlur\",(function(s){i.forceNotify(s);var o=i.props.onBlur;o&&(s.persist(),o(s))})),_defineProperty(_assertThisInitialized(i),\"createNotifier\",(function(s){if(s<0)i.notify=function(){return null};else if(0===s)i.notify=i.doNotify;else{var o=(0,u.default)((function(s){i.isDebouncing=!1,i.doNotify(s)}),s);i.notify=function(s){i.isDebouncing=!0,o(s)},i.flush=function(){return o.flush()},i.cancel=function(){i.isDebouncing=!1,o.cancel()}}})),_defineProperty(_assertThisInitialized(i),\"doNotify\",(function(){i.props.onChange.apply(void 0,arguments)})),_defineProperty(_assertThisInitialized(i),\"forceNotify\",(function(s){var o=i.props.debounceTimeout;if(i.isDebouncing||!(o>0)){i.cancel&&i.cancel();var a=i.state.value,u=i.props.minLength;a.length>=u?i.doNotify(s):i.doNotify(_objectSpread(_objectSpread({},s),{},{target:_objectSpread(_objectSpread({},s.target),{},{value:a})}))}})),i.isDebouncing=!1,i.state={value:void 0===s.value||null===s.value?\"\":s.value};var a=i.props.debounceTimeout;return i.createNotifier(a),i}return function _createClass(s,o,i){return o&&_defineProperties(s.prototype,o),i&&_defineProperties(s,i),Object.defineProperty(s,\"prototype\",{writable:!1}),s}(DebounceInput,[{key:\"componentDidUpdate\",value:function componentDidUpdate(s){if(!this.isDebouncing){var o=this.props,i=o.value,a=o.debounceTimeout,u=s.debounceTimeout,_=s.value,w=this.state.value;void 0!==i&&_!==i&&w!==i&&this.setState({value:i}),a!==u&&this.createNotifier(a)}}},{key:\"componentWillUnmount\",value:function componentWillUnmount(){this.flush&&this.flush()}},{key:\"render\",value:function render(){var s,o,i=this.props,u=i.element,w=(i.onChange,i.value,i.minLength,i.debounceTimeout,i.forceNotifyByEnter),x=i.forceNotifyOnBlur,C=i.onKeyDown,j=i.onBlur,L=i.inputRef,B=_objectWithoutProperties(i,_),$=this.state.value;s=w?{onKeyDown:this.onKeyDown}:C?{onKeyDown:C}:{},o=x?{onBlur:this.onBlur}:j?{onBlur:j}:{};var U=L?{ref:L}:{};return a.default.createElement(u,_objectSpread(_objectSpread(_objectSpread(_objectSpread({},B),{},{onChange:this.onChange,value:$},s),o),U))}}]),DebounceInput}(a.default.PureComponent);o.DebounceInput=w,_defineProperty(w,\"defaultProps\",{element:\"input\",type:\"text\",onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})},81919:(s,o,i)=>{\"use strict\";var a=i(48287).Buffer;function isSpecificValue(s){return s instanceof a||s instanceof Date||s instanceof RegExp}function cloneSpecificValue(s){if(s instanceof a){var o=a.alloc?a.alloc(s.length):new a(s.length);return s.copy(o),o}if(s instanceof Date)return new Date(s.getTime());if(s instanceof RegExp)return new RegExp(s);throw new Error(\"Unexpected situation\")}function deepCloneArray(s){var o=[];return s.forEach((function(s,i){\"object\"==typeof s&&null!==s?Array.isArray(s)?o[i]=deepCloneArray(s):isSpecificValue(s)?o[i]=cloneSpecificValue(s):o[i]=u({},s):o[i]=s})),o}function safeGetProperty(s,o){return\"__proto__\"===o?void 0:s[o]}var u=s.exports=function(){if(arguments.length<1||\"object\"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var s,o,i=arguments[0];return Array.prototype.slice.call(arguments,1).forEach((function(a){\"object\"!=typeof a||null===a||Array.isArray(a)||Object.keys(a).forEach((function(_){return o=safeGetProperty(i,_),(s=safeGetProperty(a,_))===i?void 0:\"object\"!=typeof s||null===s?void(i[_]=s):Array.isArray(s)?void(i[_]=deepCloneArray(s)):isSpecificValue(s)?void(i[_]=cloneSpecificValue(s)):\"object\"!=typeof o||null===o||Array.isArray(o)?void(i[_]=u({},s)):void(i[_]=u(o,s))}))})),i}},82048:(s,o,i)=>{\"use strict\";var a=i(11091),u=i(88280),_=i(15972),w=i(79192),x=i(19595),C=i(58075),j=i(61626),L=i(75817),B=i(39259),$=i(85884),U=i(24823),V=i(32096),z=i(76264)(\"toStringTag\"),Y=Error,Z=[].push,ee=function AggregateError(s,o){var i,a=u(ie,this);w?i=w(new Y,a?_(this):ie):(i=a?this:C(ie),j(i,z,\"Error\")),void 0!==o&&j(i,\"message\",V(o)),$(i,ee,i.stack,1),arguments.length>2&&B(i,arguments[2]);var x=[];return U(s,Z,{that:x}),j(i,\"errors\",x),i};w?w(ee,Y):x(ee,Y,{name:!0});var ie=ee.prototype=C(Y.prototype,{constructor:L(1,ee),message:L(1,\"\"),name:L(1,\"AggregateError\")});a({global:!0,constructor:!0,arity:2},{AggregateError:ee})},82159:(s,o,i)=>{\"use strict\";var a=i(62250),u=i(4640),_=TypeError;s.exports=function(s){if(a(s))return s;throw new _(u(s)+\" is not a function\")}},82199:(s,o,i)=>{var a=i(14528),u=i(56449);s.exports=function baseGetAllKeys(s,o,i){var _=o(s);return u(s)?_:a(_,i(s))}},82261:(s,o,i)=>{\"use strict\";Object.defineProperty(o,\"__esModule\",{value:!0});var a=_interopRequireDefault(i(9404)),u=_interopRequireDefault(i(48590));function _interopRequireDefault(s){return s&&s.__esModule?s:{default:s}}o.default=function(s,o,i){var _=Object.keys(o);if(!_.length)return\"Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.\";var w=(0,u.default)(i);if(a.default.isImmutable?!a.default.isImmutable(s):!a.default.Iterable.isIterable(s))return\"The \"+w+' is of unexpected type. Expected argument to be an instance of Immutable.Collection or Immutable.Record with the following properties: \"'+_.join('\", \"')+'\".';var x=s.toSeq().keySeq().toArray().filter((function(s){return!o.hasOwnProperty(s)}));return x.length>0?\"Unexpected \"+(1===x.length?\"property\":\"properties\")+' \"'+x.join('\", \"')+'\" found in '+w+'. Expected to find one of the known reducer property names instead: \"'+_.join('\", \"')+'\". Unexpected properties will be ignored.':null},s.exports=o.default},82682:(s,o,i)=>{\"use strict\";var a=i(69600),u=Object.prototype.toString,_=Object.prototype.hasOwnProperty;s.exports=function forEach(s,o,i){if(!a(o))throw new TypeError(\"iterator must be a function\");var w;arguments.length>=3&&(w=i),function isArray(s){return\"[object Array]\"===u.call(s)}(s)?function forEachArray(s,o,i){for(var a=0,u=s.length;a<u;a++)_.call(s,a)&&(null==i?o(s[a],a,s):o.call(i,s[a],a,s))}(s,o,w):\"string\"==typeof s?function forEachString(s,o,i){for(var a=0,u=s.length;a<u;a++)null==i?o(s.charAt(a),a,s):o.call(i,s.charAt(a),a,s)}(s,o,w):function forEachObject(s,o,i){for(var a in s)_.call(s,a)&&(null==i?o(s[a],a,s):o.call(i,s[a],a,s))}(s,o,w)}},82819:(s,o,i)=>{var a=i(39344),u=i(23805);s.exports=function createCtor(s){return function(){var o=arguments;switch(o.length){case 0:return new s;case 1:return new s(o[0]);case 2:return new s(o[0],o[1]);case 3:return new s(o[0],o[1],o[2]);case 4:return new s(o[0],o[1],o[2],o[3]);case 5:return new s(o[0],o[1],o[2],o[3],o[4]);case 6:return new s(o[0],o[1],o[2],o[3],o[4],o[5]);case 7:return new s(o[0],o[1],o[2],o[3],o[4],o[5],o[6])}var i=a(s.prototype),_=s.apply(i,o);return u(_)?_:i}}},82890:(s,o,i)=>{\"use strict\";var a=i(56698),u=i(90392),_=i(92861).Buffer,w=[1116352408,3609767458,1899447441,602891725,3049323471,3964484399,3921009573,2173295548,961987163,4081628472,1508970993,3053834265,2453635748,2937671579,2870763221,3664609560,3624381080,2734883394,310598401,1164996542,607225278,1323610764,1426881987,3590304994,1925078388,4068182383,2162078206,991336113,2614888103,633803317,3248222580,3479774868,3835390401,2666613458,4022224774,944711139,264347078,2341262773,604807628,2007800933,770255983,1495990901,1249150122,1856431235,1555081692,3175218132,1996064986,2198950837,2554220882,3999719339,2821834349,766784016,2952996808,2566594879,3210313671,3203337956,3336571891,1034457026,3584528711,2466948901,113926993,3758326383,338241895,168717936,666307205,1188179964,773529912,1546045734,1294757372,1522805485,1396182291,2643833823,1695183700,2343527390,1986661051,1014477480,2177026350,1206759142,2456956037,344077627,2730485921,1290863460,2820302411,3158454273,3259730800,3505952657,3345764771,106217008,3516065817,3606008344,3600352804,1432725776,4094571909,1467031594,275423344,851169720,430227734,3100823752,506948616,1363258195,659060556,3750685593,883997877,3785050280,958139571,3318307427,1322822218,3812723403,1537002063,2003034995,1747873779,3602036899,1955562222,1575990012,2024104815,1125592928,2227730452,2716904306,2361852424,442776044,2428436474,593698344,2756734187,3733110249,3204031479,2999351573,3329325298,3815920427,3391569614,3928383900,3515267271,566280711,3940187606,3454069534,4118630271,4000239992,116418474,1914138554,174292421,2731055270,289380356,3203993006,460393269,320620315,685471733,587496836,852142971,1086792851,1017036298,365543100,1126000580,2618297676,1288033470,3409855158,1501505948,4234509866,1607167915,987167468,1816402316,1246189591],x=new Array(160);function Sha512(){this.init(),this._w=x,u.call(this,128,112)}function Ch(s,o,i){return i^s&(o^i)}function maj(s,o,i){return s&o|i&(s|o)}function sigma0(s,o){return(s>>>28|o<<4)^(o>>>2|s<<30)^(o>>>7|s<<25)}function sigma1(s,o){return(s>>>14|o<<18)^(s>>>18|o<<14)^(o>>>9|s<<23)}function Gamma0(s,o){return(s>>>1|o<<31)^(s>>>8|o<<24)^s>>>7}function Gamma0l(s,o){return(s>>>1|o<<31)^(s>>>8|o<<24)^(s>>>7|o<<25)}function Gamma1(s,o){return(s>>>19|o<<13)^(o>>>29|s<<3)^s>>>6}function Gamma1l(s,o){return(s>>>19|o<<13)^(o>>>29|s<<3)^(s>>>6|o<<26)}function getCarry(s,o){return s>>>0<o>>>0?1:0}a(Sha512,u),Sha512.prototype.init=function(){return this._ah=1779033703,this._bh=3144134277,this._ch=1013904242,this._dh=2773480762,this._eh=1359893119,this._fh=2600822924,this._gh=528734635,this._hh=1541459225,this._al=4089235720,this._bl=2227873595,this._cl=4271175723,this._dl=1595750129,this._el=2917565137,this._fl=725511199,this._gl=4215389547,this._hl=327033209,this},Sha512.prototype._update=function(s){for(var o=this._w,i=0|this._ah,a=0|this._bh,u=0|this._ch,_=0|this._dh,x=0|this._eh,C=0|this._fh,j=0|this._gh,L=0|this._hh,B=0|this._al,$=0|this._bl,U=0|this._cl,V=0|this._dl,z=0|this._el,Y=0|this._fl,Z=0|this._gl,ee=0|this._hl,ie=0;ie<32;ie+=2)o[ie]=s.readInt32BE(4*ie),o[ie+1]=s.readInt32BE(4*ie+4);for(;ie<160;ie+=2){var ae=o[ie-30],ce=o[ie-30+1],le=Gamma0(ae,ce),pe=Gamma0l(ce,ae),de=Gamma1(ae=o[ie-4],ce=o[ie-4+1]),fe=Gamma1l(ce,ae),ye=o[ie-14],be=o[ie-14+1],_e=o[ie-32],Se=o[ie-32+1],we=pe+be|0,xe=le+ye+getCarry(we,pe)|0;xe=(xe=xe+de+getCarry(we=we+fe|0,fe)|0)+_e+getCarry(we=we+Se|0,Se)|0,o[ie]=xe,o[ie+1]=we}for(var Pe=0;Pe<160;Pe+=2){xe=o[Pe],we=o[Pe+1];var Te=maj(i,a,u),Re=maj(B,$,U),$e=sigma0(i,B),qe=sigma0(B,i),ze=sigma1(x,z),We=sigma1(z,x),He=w[Pe],Ye=w[Pe+1],Xe=Ch(x,C,j),Qe=Ch(z,Y,Z),et=ee+We|0,tt=L+ze+getCarry(et,ee)|0;tt=(tt=(tt=tt+Xe+getCarry(et=et+Qe|0,Qe)|0)+He+getCarry(et=et+Ye|0,Ye)|0)+xe+getCarry(et=et+we|0,we)|0;var rt=qe+Re|0,nt=$e+Te+getCarry(rt,qe)|0;L=j,ee=Z,j=C,Z=Y,C=x,Y=z,x=_+tt+getCarry(z=V+et|0,V)|0,_=u,V=U,u=a,U=$,a=i,$=B,i=tt+nt+getCarry(B=et+rt|0,et)|0}this._al=this._al+B|0,this._bl=this._bl+$|0,this._cl=this._cl+U|0,this._dl=this._dl+V|0,this._el=this._el+z|0,this._fl=this._fl+Y|0,this._gl=this._gl+Z|0,this._hl=this._hl+ee|0,this._ah=this._ah+i+getCarry(this._al,B)|0,this._bh=this._bh+a+getCarry(this._bl,$)|0,this._ch=this._ch+u+getCarry(this._cl,U)|0,this._dh=this._dh+_+getCarry(this._dl,V)|0,this._eh=this._eh+x+getCarry(this._el,z)|0,this._fh=this._fh+C+getCarry(this._fl,Y)|0,this._gh=this._gh+j+getCarry(this._gl,Z)|0,this._hh=this._hh+L+getCarry(this._hl,ee)|0},Sha512.prototype._hash=function(){var s=_.allocUnsafe(64);function writeInt64BE(o,i,a){s.writeInt32BE(o,a),s.writeInt32BE(i,a+4)}return writeInt64BE(this._ah,this._al,0),writeInt64BE(this._bh,this._bl,8),writeInt64BE(this._ch,this._cl,16),writeInt64BE(this._dh,this._dl,24),writeInt64BE(this._eh,this._el,32),writeInt64BE(this._fh,this._fl,40),writeInt64BE(this._gh,this._gl,48),writeInt64BE(this._hh,this._hl,56),s},s.exports=Sha512},83120:(s,o,i)=>{var a=i(14528),u=i(45891);s.exports=function baseFlatten(s,o,i,_,w){var x=-1,C=s.length;for(i||(i=u),w||(w=[]);++x<C;){var j=s[x];o>0&&i(j)?o>1?baseFlatten(j,o-1,i,_,w):a(w,j):_||(w[w.length]=j)}return w}},83141:(s,o,i)=>{\"use strict\";var a=i(92861).Buffer,u=a.isEncoding||function(s){switch((s=\"\"+s)&&s.toLowerCase()){case\"hex\":case\"utf8\":case\"utf-8\":case\"ascii\":case\"binary\":case\"base64\":case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":case\"raw\":return!0;default:return!1}};function StringDecoder(s){var o;switch(this.encoding=function normalizeEncoding(s){var o=function _normalizeEncoding(s){if(!s)return\"utf8\";for(var o;;)switch(s){case\"utf8\":case\"utf-8\":return\"utf8\";case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return\"utf16le\";case\"latin1\":case\"binary\":return\"latin1\";case\"base64\":case\"ascii\":case\"hex\":return s;default:if(o)return;s=(\"\"+s).toLowerCase(),o=!0}}(s);if(\"string\"!=typeof o&&(a.isEncoding===u||!u(s)))throw new Error(\"Unknown encoding: \"+s);return o||s}(s),this.encoding){case\"utf16le\":this.text=utf16Text,this.end=utf16End,o=4;break;case\"utf8\":this.fillLast=utf8FillLast,o=4;break;case\"base64\":this.text=base64Text,this.end=base64End,o=3;break;default:return this.write=simpleWrite,void(this.end=simpleEnd)}this.lastNeed=0,this.lastTotal=0,this.lastChar=a.allocUnsafe(o)}function utf8CheckByte(s){return s<=127?0:s>>5==6?2:s>>4==14?3:s>>3==30?4:s>>6==2?-1:-2}function utf8FillLast(s){var o=this.lastTotal-this.lastNeed,i=function utf8CheckExtraBytes(s,o,i){if(128!=(192&o[0]))return s.lastNeed=0,\"�\";if(s.lastNeed>1&&o.length>1){if(128!=(192&o[1]))return s.lastNeed=1,\"�\";if(s.lastNeed>2&&o.length>2&&128!=(192&o[2]))return s.lastNeed=2,\"�\"}}(this,s);return void 0!==i?i:this.lastNeed<=s.length?(s.copy(this.lastChar,o,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(s.copy(this.lastChar,o,0,s.length),void(this.lastNeed-=s.length))}function utf16Text(s,o){if((s.length-o)%2==0){var i=s.toString(\"utf16le\",o);if(i){var a=i.charCodeAt(i.length-1);if(a>=55296&&a<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=s[s.length-2],this.lastChar[1]=s[s.length-1],i.slice(0,-1)}return i}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=s[s.length-1],s.toString(\"utf16le\",o,s.length-1)}function utf16End(s){var o=s&&s.length?this.write(s):\"\";if(this.lastNeed){var i=this.lastTotal-this.lastNeed;return o+this.lastChar.toString(\"utf16le\",0,i)}return o}function base64Text(s,o){var i=(s.length-o)%3;return 0===i?s.toString(\"base64\",o):(this.lastNeed=3-i,this.lastTotal=3,1===i?this.lastChar[0]=s[s.length-1]:(this.lastChar[0]=s[s.length-2],this.lastChar[1]=s[s.length-1]),s.toString(\"base64\",o,s.length-i))}function base64End(s){var o=s&&s.length?this.write(s):\"\";return this.lastNeed?o+this.lastChar.toString(\"base64\",0,3-this.lastNeed):o}function simpleWrite(s){return s.toString(this.encoding)}function simpleEnd(s){return s&&s.length?this.write(s):\"\"}o.I=StringDecoder,StringDecoder.prototype.write=function(s){if(0===s.length)return\"\";var o,i;if(this.lastNeed){if(void 0===(o=this.fillLast(s)))return\"\";i=this.lastNeed,this.lastNeed=0}else i=0;return i<s.length?o?o+this.text(s,i):this.text(s,i):o||\"\"},StringDecoder.prototype.end=function utf8End(s){var o=s&&s.length?this.write(s):\"\";return this.lastNeed?o+\"�\":o},StringDecoder.prototype.text=function utf8Text(s,o){var i=function utf8CheckIncomplete(s,o,i){var a=o.length-1;if(a<i)return 0;var u=utf8CheckByte(o[a]);if(u>=0)return u>0&&(s.lastNeed=u-1),u;if(--a<i||-2===u)return 0;if(u=utf8CheckByte(o[a]),u>=0)return u>0&&(s.lastNeed=u-2),u;if(--a<i||-2===u)return 0;if(u=utf8CheckByte(o[a]),u>=0)return u>0&&(2===u?u=0:s.lastNeed=u-3),u;return 0}(this,s,o);if(!this.lastNeed)return s.toString(\"utf8\",o);this.lastTotal=i;var a=s.length-(i-this.lastNeed);return s.copy(this.lastChar,0,a),s.toString(\"utf8\",o,a)},StringDecoder.prototype.fillLast=function(s){if(this.lastNeed<=s.length)return s.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);s.copy(this.lastChar,this.lastTotal-this.lastNeed,0,s.length),this.lastNeed-=s.length}},83221:s=>{s.exports=function createBaseFor(s){return function(o,i,a){for(var u=-1,_=Object(o),w=a(o),x=w.length;x--;){var C=w[s?x:++u];if(!1===i(_[C],C,_))break}return o}}},83349:(s,o,i)=>{var a=i(82199),u=i(86375),_=i(37241);s.exports=function getAllKeysIn(s){return a(s,_,u)}},83488:s=>{s.exports=function identity(s){return s}},83693:(s,o,i)=>{var a=i(64894),u=i(40346);s.exports=function isArrayLikeObject(s){return u(s)&&a(s)}},83729:s=>{s.exports=function arrayEach(s,o){for(var i=-1,a=null==s?0:s.length;++i<a&&!1!==o(s[i],i,s););return s}},84058:(s,o,i)=>{var a=i(14792),u=i(45539)((function(s,o,i){return o=o.toLowerCase(),s+(i?a(o):o)}));s.exports=u},84195:(s,o,i)=>{var a=i(66977),u=i(38816),_=u((function(s,o){return a(s,256,void 0,void 0,void 0,o)}));s.exports=_},84247:s=>{s.exports=function setToArray(s){var o=-1,i=Array(s.size);return s.forEach((function(s){i[++o]=s})),i}},84629:s=>{s.exports={}},84851:(s,o,i)=>{\"use strict\";s.exports=i(85401)},84977:(s,o,i)=>{\"use strict\";Object.defineProperty(o,\"__esModule\",{value:!0});var a=function _interopRequireDefault(s){return s&&s.__esModule?s:{default:s}}(i(9404)),u=i(55674);o.default=function(s){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:a.default.Map,i=Object.keys(s);return function(){var a=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o(),_=arguments[1];return a.withMutations((function(o){i.forEach((function(i){var a=(0,s[i])(o.get(i),_);(0,u.validateNextState)(a,i,_),o.set(i,a)}))}))}},s.exports=o.default},85015:(s,o,i)=>{var a=i(72552),u=i(56449),_=i(40346);s.exports=function isString(s){return\"string\"==typeof s||!u(s)&&_(s)&&\"[object String]\"==a(s)}},85087:(s,o,i)=>{var a=i(30980),u=i(37381),_=i(62284),w=i(53758);s.exports=function isLaziable(s){var o=_(s),i=w[o];if(\"function\"!=typeof i||!(o in a.prototype))return!1;if(s===i)return!0;var x=u(i);return!!x&&s===x[0]}},85105:s=>{s.exports=class JSONSerialiser{constructor(s){this.namespace=s||new this.Namespace}serialise(s){if(!(s instanceof this.namespace.elements.Element))throw new TypeError(`Given element \\`${s}\\` is not an Element instance`);const o={element:s.element};s._meta&&s._meta.length>0&&(o.meta=this.serialiseObject(s.meta)),s._attributes&&s._attributes.length>0&&(o.attributes=this.serialiseObject(s.attributes));const i=this.serialiseContent(s.content);return void 0!==i&&(o.content=i),o}deserialise(s){if(!s.element)throw new Error(\"Given value is not an object containing an element name\");const o=new(this.namespace.getElementClass(s.element));o.element!==s.element&&(o.element=s.element),s.meta&&this.deserialiseObject(s.meta,o.meta),s.attributes&&this.deserialiseObject(s.attributes,o.attributes);const i=this.deserialiseContent(s.content);return void 0===i&&null!==o.content||(o.content=i),o}serialiseContent(s){if(s instanceof this.namespace.elements.Element)return this.serialise(s);if(s instanceof this.namespace.KeyValuePair){const o={key:this.serialise(s.key)};return s.value&&(o.value=this.serialise(s.value)),o}if(s&&s.map){if(0===s.length)return;return s.map(this.serialise,this)}return s}deserialiseContent(s){if(s){if(s.element)return this.deserialise(s);if(s.key){const o=new this.namespace.KeyValuePair(this.deserialise(s.key));return s.value&&(o.value=this.deserialise(s.value)),o}if(s.map)return s.map(this.deserialise,this)}return s}serialiseObject(s){const o={};if(s.forEach(((s,i)=>{s&&(o[i.toValue()]=this.serialise(s))})),0!==Object.keys(o).length)return o}deserialiseObject(s,o){Object.keys(s).forEach((i=>{o.set(i,this.deserialise(s[i]))}))}}},85160:(s,o,i)=>{\"use strict\";var a=i(96540);var u=\"function\"==typeof Object.is?Object.is:function is(s,o){return s===o&&(0!==s||1/s==1/o)||s!=s&&o!=o},_=a.useSyncExternalStore,w=a.useRef,x=a.useEffect,C=a.useMemo,j=a.useDebugValue},85250:(s,o,i)=>{var a=i(37217),u=i(87805),_=i(86649),w=i(42824),x=i(23805),C=i(37241),j=i(14974);s.exports=function baseMerge(s,o,i,L,B){s!==o&&_(o,(function(_,C){if(B||(B=new a),x(_))w(s,o,C,i,baseMerge,L,B);else{var $=L?L(j(s,C),_,C+\"\",s,o,B):void 0;void 0===$&&($=_),u(s,C,$)}}),C)}},85401:(s,o,i)=>{\"use strict\";var a=i(462);s.exports=a},85463:s=>{s.exports=function baseIsNaN(s){return s!=s}},85558:s=>{s.exports=function baseReduce(s,o,i,a,u){return u(s,(function(s,u,_){i=a?(a=!1,s):o(i,s,u,_)})),i}},85582:(s,o,i)=>{\"use strict\";var a=i(92046),u=i(45951),_=i(62250),aFunction=function(s){return _(s)?s:void 0};s.exports=function(s,o){return arguments.length<2?aFunction(a[s])||aFunction(u[s]):a[s]&&a[s][o]||u[s]&&u[s][o]}},85587:(s,o,i)=>{\"use strict\";var a=i(26311),u=create(Error);function create(s){return FormattedError.displayName=s.displayName||s.name,FormattedError;function FormattedError(o){return o&&(o=a.apply(null,arguments)),new s(o)}}s.exports=u,u.eval=create(EvalError),u.range=create(RangeError),u.reference=create(ReferenceError),u.syntax=create(SyntaxError),u.type=create(TypeError),u.uri=create(URIError),u.create=create},85762:(s,o,i)=>{\"use strict\";var a=i(1907),u=Error,_=a(\"\".replace),w=String(new u(\"zxcasd\").stack),x=/\\n\\s*at [^:]*:[^\\n]*/,C=x.test(w);s.exports=function(s,o){if(C&&\"string\"==typeof s&&!u.prepareStackTrace)for(;o--;)s=_(s,x,\"\");return s}},85816:(s,o,i)=>{\"use strict\";var a=i(36128);s.exports=function(s,o){return a[s]||(a[s]=o||{})}},85884:(s,o,i)=>{\"use strict\";var a=i(61626),u=i(85762),_=i(23888),w=Error.captureStackTrace;s.exports=function(s,o,i,x){_&&(w?w(s,o):a(s,\"stack\",u(i,x)))}},86009:(s,o,i)=>{s=i.nmd(s);var a=i(34840),u=o&&!o.nodeType&&o,_=u&&s&&!s.nodeType&&s,w=_&&_.exports===u&&a.process,x=function(){try{var s=_&&_.require&&_.require(\"util\").types;return s||w&&w.binding&&w.binding(\"util\")}catch(s){}}();s.exports=x},86048:s=>{\"use strict\";var o={};function createErrorType(s,i,a){a||(a=Error);var u=function(s){function NodeError(o,a,u){return s.call(this,function getMessage(s,o,a){return\"string\"==typeof i?i:i(s,o,a)}(o,a,u))||this}return function _inheritsLoose(s,o){s.prototype=Object.create(o.prototype),s.prototype.constructor=s,s.__proto__=o}(NodeError,s),NodeError}(a);u.prototype.name=a.name,u.prototype.code=s,o[s]=u}function oneOf(s,o){if(Array.isArray(s)){var i=s.length;return s=s.map((function(s){return String(s)})),i>2?\"one of \".concat(o,\" \").concat(s.slice(0,i-1).join(\", \"),\", or \")+s[i-1]:2===i?\"one of \".concat(o,\" \").concat(s[0],\" or \").concat(s[1]):\"of \".concat(o,\" \").concat(s[0])}return\"of \".concat(o,\" \").concat(String(s))}createErrorType(\"ERR_INVALID_OPT_VALUE\",(function(s,o){return'The value \"'+o+'\" is invalid for option \"'+s+'\"'}),TypeError),createErrorType(\"ERR_INVALID_ARG_TYPE\",(function(s,o,i){var a,u;if(\"string\"==typeof o&&function startsWith(s,o,i){return s.substr(!i||i<0?0:+i,o.length)===o}(o,\"not \")?(a=\"must not be\",o=o.replace(/^not /,\"\")):a=\"must be\",function endsWith(s,o,i){return(void 0===i||i>s.length)&&(i=s.length),s.substring(i-o.length,i)===o}(s,\" argument\"))u=\"The \".concat(s,\" \").concat(a,\" \").concat(oneOf(o,\"type\"));else{var _=function includes(s,o,i){return\"number\"!=typeof i&&(i=0),!(i+o.length>s.length)&&-1!==s.indexOf(o,i)}(s,\".\")?\"property\":\"argument\";u='The \"'.concat(s,'\" ').concat(_,\" \").concat(a,\" \").concat(oneOf(o,\"type\"))}return u+=\". Received type \".concat(typeof i)}),TypeError),createErrorType(\"ERR_STREAM_PUSH_AFTER_EOF\",\"stream.push() after EOF\"),createErrorType(\"ERR_METHOD_NOT_IMPLEMENTED\",(function(s){return\"The \"+s+\" method is not implemented\"})),createErrorType(\"ERR_STREAM_PREMATURE_CLOSE\",\"Premature close\"),createErrorType(\"ERR_STREAM_DESTROYED\",(function(s){return\"Cannot call \"+s+\" after a stream was destroyed\"})),createErrorType(\"ERR_MULTIPLE_CALLBACK\",\"Callback called multiple times\"),createErrorType(\"ERR_STREAM_CANNOT_PIPE\",\"Cannot pipe, not readable\"),createErrorType(\"ERR_STREAM_WRITE_AFTER_END\",\"write after end\"),createErrorType(\"ERR_STREAM_NULL_VALUES\",\"May not write null values to stream\",TypeError),createErrorType(\"ERR_UNKNOWN_ENCODING\",(function(s){return\"Unknown encoding: \"+s}),TypeError),createErrorType(\"ERR_STREAM_UNSHIFT_AFTER_END_EVENT\",\"stream.unshift() after end event\"),s.exports.F=o},86215:function(s,o){var i,a,u;a=[],i=function(){\"use strict\";var isNativeSmoothScrollEnabledOn=function(s){return s&&\"getComputedStyle\"in window&&\"smooth\"===window.getComputedStyle(s)[\"scroll-behavior\"]};if(\"undefined\"==typeof window||!(\"document\"in window))return{};var makeScroller=function(s,o,i){var a;o=o||999,i||0===i||(i=9);var setScrollTimeoutId=function(s){a=s},stopScroll=function(){clearTimeout(a),setScrollTimeoutId(0)},getTopWithEdgeOffset=function(o){return Math.max(0,s.getTopOf(o)-i)},scrollToY=function(i,a,u){if(stopScroll(),0===a||a&&a<0||isNativeSmoothScrollEnabledOn(s.body))s.toY(i),u&&u();else{var _=s.getY(),w=Math.max(0,i)-_,x=(new Date).getTime();a=a||Math.min(Math.abs(w),o),function loopScroll(){setScrollTimeoutId(setTimeout((function(){var o=Math.min(1,((new Date).getTime()-x)/a),i=Math.max(0,Math.floor(_+w*(o<.5?2*o*o:o*(4-2*o)-1)));s.toY(i),o<1&&s.getHeight()+i<s.body.scrollHeight?loopScroll():(setTimeout(stopScroll,99),u&&u())}),9))}()}},scrollToElem=function(s,o,i){scrollToY(getTopWithEdgeOffset(s),o,i)},scrollIntoView=function(o,a,u){var _=o.getBoundingClientRect().height,w=s.getTopOf(o)+_,x=s.getHeight(),C=s.getY(),j=C+x;getTopWithEdgeOffset(o)<C||_+i>x?scrollToElem(o,a,u):w+i>j?scrollToY(w-x+i,a,u):u&&u()},scrollToCenterOf=function(o,i,a,u){scrollToY(Math.max(0,s.getTopOf(o)-s.getHeight()/2+(a||o.getBoundingClientRect().height/2)),i,u)};return{setup:function(s,a){return(0===s||s)&&(o=s),(0===a||a)&&(i=a),{defaultDuration:o,edgeOffset:i}},to:scrollToElem,toY:scrollToY,intoView:scrollIntoView,center:scrollToCenterOf,stop:stopScroll,moving:function(){return!!a},getY:s.getY,getTopOf:s.getTopOf}},s=document.documentElement,getDocY=function(){return window.scrollY||s.scrollTop},o=makeScroller({body:document.scrollingElement||document.body,toY:function(s){window.scrollTo(0,s)},getY:getDocY,getHeight:function(){return window.innerHeight||s.clientHeight},getTopOf:function(o){return o.getBoundingClientRect().top+getDocY()-s.offsetTop}});if(o.createScroller=function(o,i,a){return makeScroller({body:o,toY:function(s){o.scrollTop=s},getY:function(){return o.scrollTop},getHeight:function(){return Math.min(o.clientHeight,window.innerHeight||s.clientHeight)},getTopOf:function(s){return s.offsetTop}},i,a)},\"addEventListener\"in window&&!window.noZensmooth&&!isNativeSmoothScrollEnabledOn(document.body)){var i=\"history\"in window&&\"pushState\"in history,a=i&&\"scrollRestoration\"in history;a&&(history.scrollRestoration=\"auto\"),window.addEventListener(\"load\",(function(){a&&(setTimeout((function(){history.scrollRestoration=\"manual\"}),9),window.addEventListener(\"popstate\",(function(s){s.state&&\"zenscrollY\"in s.state&&o.toY(s.state.zenscrollY)}),!1)),window.location.hash&&setTimeout((function(){var s=o.setup().edgeOffset;if(s){var i=document.getElementById(window.location.href.split(\"#\")[1]);if(i){var a=Math.max(0,o.getTopOf(i)-s),u=o.getY()-a;0<=u&&u<9&&window.scrollTo(0,a)}}}),9)}),!1);var u=new RegExp(\"(^|\\\\s)noZensmooth(\\\\s|$)\");window.addEventListener(\"click\",(function(s){for(var _=s.target;_&&\"A\"!==_.tagName;)_=_.parentNode;if(!(!_||1!==s.which||s.shiftKey||s.metaKey||s.ctrlKey||s.altKey)){if(a){var w=history.state&&\"object\"==typeof history.state?history.state:{};w.zenscrollY=o.getY();try{history.replaceState(w,\"\")}catch(s){}}var x=_.getAttribute(\"href\")||\"\";if(0===x.indexOf(\"#\")&&!u.test(_.className)){var C=0,j=document.getElementById(x.substring(1));if(\"#\"!==x){if(!j)return;C=o.getTopOf(j)}s.preventDefault();var onDone=function(){window.location=x},L=o.setup().edgeOffset;L&&(C=Math.max(0,C-L),i&&(onDone=function(){history.pushState({},\"\",x)})),o.toY(C,null,onDone)}}}),!1)}return o}(),void 0===(u=\"function\"==typeof i?i.apply(o,a):i)||(s.exports=u)},86238:(s,o,i)=>{\"use strict\";var a=i(86048).F.ERR_STREAM_PREMATURE_CLOSE;function noop(){}s.exports=function eos(s,o,i){if(\"function\"==typeof o)return eos(s,null,o);o||(o={}),i=function once(s){var o=!1;return function(){if(!o){o=!0;for(var i=arguments.length,a=new Array(i),u=0;u<i;u++)a[u]=arguments[u];s.apply(this,a)}}}(i||noop);var u=o.readable||!1!==o.readable&&s.readable,_=o.writable||!1!==o.writable&&s.writable,w=function onlegacyfinish(){s.writable||C()},x=s._writableState&&s._writableState.finished,C=function onfinish(){_=!1,x=!0,u||i.call(s)},j=s._readableState&&s._readableState.endEmitted,L=function onend(){u=!1,j=!0,_||i.call(s)},B=function onerror(o){i.call(s,o)},$=function onclose(){var o;return u&&!j?(s._readableState&&s._readableState.ended||(o=new a),i.call(s,o)):_&&!x?(s._writableState&&s._writableState.ended||(o=new a),i.call(s,o)):void 0},U=function onrequest(){s.req.on(\"finish\",C)};return!function isRequest(s){return s.setHeader&&\"function\"==typeof s.abort}(s)?_&&!s._writableState&&(s.on(\"end\",w),s.on(\"close\",w)):(s.on(\"complete\",C),s.on(\"abort\",$),s.req?U():s.on(\"request\",U)),s.on(\"end\",L),s.on(\"finish\",C),!1!==o.error&&s.on(\"error\",B),s.on(\"close\",$),function(){s.removeListener(\"complete\",C),s.removeListener(\"abort\",$),s.removeListener(\"request\",U),s.req&&s.req.removeListener(\"finish\",C),s.removeListener(\"end\",w),s.removeListener(\"close\",w),s.removeListener(\"finish\",C),s.removeListener(\"end\",L),s.removeListener(\"error\",B),s.removeListener(\"close\",$)}}},86303:(s,o,i)=>{const a=i(10316);s.exports=class LinkElement extends a{constructor(s,o,i){super(s||[],o,i),this.element=\"link\"}get relation(){return this.attributes.get(\"relation\")}set relation(s){this.attributes.set(\"relation\",s)}get href(){return this.attributes.get(\"href\")}set href(s){this.attributes.set(\"href\",s)}}},86375:(s,o,i)=>{var a=i(14528),u=i(28879),_=i(4664),w=i(63345),x=Object.getOwnPropertySymbols?function(s){for(var o=[];s;)a(o,_(s)),s=u(s);return o}:w;s.exports=x},86649:(s,o,i)=>{var a=i(83221)();s.exports=a},86804:(s,o,i)=>{const a=i(10316),u=i(41067),_=i(71167),w=i(40239),x=i(12242),C=i(6233),j=i(87726),L=i(61045),B=i(86303),$=i(14540),U=i(92340),V=i(10866),z=i(55973);function refract(s){if(s instanceof a)return s;if(\"string\"==typeof s)return new _(s);if(\"number\"==typeof s)return new w(s);if(\"boolean\"==typeof s)return new x(s);if(null===s)return new u;if(Array.isArray(s))return new C(s.map(refract));if(\"object\"==typeof s){return new L(s)}return s}a.prototype.ObjectElement=L,a.prototype.RefElement=$,a.prototype.MemberElement=j,a.prototype.refract=refract,U.prototype.refract=refract,s.exports={Element:a,NullElement:u,StringElement:_,NumberElement:w,BooleanElement:x,ArrayElement:C,MemberElement:j,ObjectElement:L,LinkElement:B,RefElement:$,refract,ArraySlice:U,ObjectSlice:V,KeyValuePair:z}},87068:(s,o,i)=>{var a=i(37217),u=i(25911),_=i(21986),w=i(50689),x=i(5861),C=i(56449),j=i(3656),L=i(37167),B=\"[object Arguments]\",$=\"[object Array]\",U=\"[object Object]\",V=Object.prototype.hasOwnProperty;s.exports=function baseIsEqualDeep(s,o,i,z,Y,Z){var ee=C(s),ie=C(o),ae=ee?$:x(s),ce=ie?$:x(o),le=(ae=ae==B?U:ae)==U,pe=(ce=ce==B?U:ce)==U,de=ae==ce;if(de&&j(s)){if(!j(o))return!1;ee=!0,le=!1}if(de&&!le)return Z||(Z=new a),ee||L(s)?u(s,o,i,z,Y,Z):_(s,o,ae,i,z,Y,Z);if(!(1&i)){var fe=le&&V.call(s,\"__wrapped__\"),ye=pe&&V.call(o,\"__wrapped__\");if(fe||ye){var be=fe?s.value():s,_e=ye?o.value():o;return Z||(Z=new a),Y(be,_e,i,z,Z)}}return!!de&&(Z||(Z=new a),w(s,o,i,z,Y,Z))}},87136:s=>{\"use strict\";s.exports=function(s){return null==s}},87170:(s,o)=>{\"use strict\";o.f=Object.getOwnPropertySymbols},87296:(s,o,i)=>{var a,u=i(55481),_=(a=/[^.]+$/.exec(u&&u.keys&&u.keys.IE_PROTO||\"\"))?\"Symbol(src)_1.\"+a:\"\";s.exports=function isMasked(s){return!!_&&_ in s}},87586:(s,o,i)=>{const a=i(6205),u=i(10023),_={0:0,t:9,n:10,v:11,f:12,r:13};o.strToChars=function(s){return s=s.replace(/(\\[\\\\b\\])|(\\\\)?\\\\(?:u([A-F0-9]{4})|x([A-F0-9]{2})|(0?[0-7]{2})|c([@A-Z[\\\\\\]^?])|([0tnvfr]))/g,(function(s,o,i,a,u,w,x,C){if(i)return s;var j=o?8:a?parseInt(a,16):u?parseInt(u,16):w?parseInt(w,8):x?\"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^ ?\".indexOf(x):_[C],L=String.fromCharCode(j);return/[[\\]{}^$.|?*+()]/.test(L)&&(L=\"\\\\\"+L),L}))},o.tokenizeClass=(s,i)=>{for(var _,w,x=[],C=/\\\\(?:(w)|(d)|(s)|(W)|(D)|(S))|((?:(?:\\\\)(.)|([^\\]\\\\]))-(?:\\\\)?([^\\]]))|(\\])|(?:\\\\)?([^])/g;null!=(_=C.exec(s));)if(_[1])x.push(u.words());else if(_[2])x.push(u.ints());else if(_[3])x.push(u.whitespace());else if(_[4])x.push(u.notWords());else if(_[5])x.push(u.notInts());else if(_[6])x.push(u.notWhitespace());else if(_[7])x.push({type:a.RANGE,from:(_[8]||_[9]).charCodeAt(0),to:_[10].charCodeAt(0)});else{if(!(w=_[12]))return[x,C.lastIndex];x.push({type:a.CHAR,value:w.charCodeAt(0)})}o.error(i,\"Unterminated character class\")},o.error=(s,o)=>{throw new SyntaxError(\"Invalid regular expression: /\"+s+\"/: \"+o)}},87726:(s,o,i)=>{const a=i(55973),u=i(10316);s.exports=class MemberElement extends u{constructor(s,o,i,u){super(new a,i,u),this.element=\"member\",this.key=s,this.value=o}get key(){return this.content.key}set key(s){this.content.key=this.refract(s)}get value(){return this.content.value}set value(s){this.content.value=this.refract(s)}}},87730:(s,o,i)=>{var a=i(29172),u=i(27301),_=i(86009),w=_&&_.isMap,x=w?u(w):a;s.exports=x},87805:(s,o,i)=>{var a=i(43360),u=i(75288);s.exports=function assignMergeValue(s,o,i){(void 0!==i&&!u(s[o],i)||void 0===i&&!(o in s))&&a(s,o,i)}},87978:(s,o,i)=>{var a=i(60270),u=i(58156),_=i(80631),w=i(28586),x=i(30756),C=i(67197),j=i(77797);s.exports=function baseMatchesProperty(s,o){return w(s)&&x(o)?C(j(s),o):function(i){var w=u(i,s);return void 0===w&&w===o?_(i,s):a(o,w,3)}}},88280:(s,o,i)=>{\"use strict\";var a=i(1907);s.exports=a({}.isPrototypeOf)},88310:(s,o,i)=>{s.exports=Stream;var a=i(37007).EventEmitter;function Stream(){a.call(this)}i(56698)(Stream,a),Stream.Readable=i(45412),Stream.Writable=i(16708),Stream.Duplex=i(25382),Stream.Transform=i(74610),Stream.PassThrough=i(63600),Stream.finished=i(86238),Stream.pipeline=i(57758),Stream.Stream=Stream,Stream.prototype.pipe=function(s,o){var i=this;function ondata(o){s.writable&&!1===s.write(o)&&i.pause&&i.pause()}function ondrain(){i.readable&&i.resume&&i.resume()}i.on(\"data\",ondata),s.on(\"drain\",ondrain),s._isStdio||o&&!1===o.end||(i.on(\"end\",onend),i.on(\"close\",onclose));var u=!1;function onend(){u||(u=!0,s.end())}function onclose(){u||(u=!0,\"function\"==typeof s.destroy&&s.destroy())}function onerror(s){if(cleanup(),0===a.listenerCount(this,\"error\"))throw s}function cleanup(){i.removeListener(\"data\",ondata),s.removeListener(\"drain\",ondrain),i.removeListener(\"end\",onend),i.removeListener(\"close\",onclose),i.removeListener(\"error\",onerror),s.removeListener(\"error\",onerror),i.removeListener(\"end\",cleanup),i.removeListener(\"close\",cleanup),s.removeListener(\"close\",cleanup)}return i.on(\"error\",onerror),s.on(\"error\",onerror),i.on(\"end\",cleanup),i.on(\"close\",cleanup),s.on(\"close\",cleanup),s.emit(\"pipe\",i),s}},88984:(s,o,i)=>{var a=i(55527),u=i(3650),_=Object.prototype.hasOwnProperty;s.exports=function baseKeys(s){if(!a(s))return u(s);var o=[];for(var i in Object(s))_.call(s,i)&&\"constructor\"!=i&&o.push(i);return o}},89353:s=>{\"use strict\";var o=Object.prototype.toString,i=Math.max,a=function concatty(s,o){for(var i=[],a=0;a<s.length;a+=1)i[a]=s[a];for(var u=0;u<o.length;u+=1)i[u+s.length]=o[u];return i};s.exports=function bind(s){var u=this;if(\"function\"!=typeof u||\"[object Function]\"!==o.apply(u))throw new TypeError(\"Function.prototype.bind called on incompatible \"+u);for(var _,w=function slicy(s,o){for(var i=[],a=o||0,u=0;a<s.length;a+=1,u+=1)i[u]=s[a];return i}(arguments,1),x=i(0,u.length-w.length),C=[],j=0;j<x;j++)C[j]=\"$\"+j;if(_=Function(\"binder\",\"return function (\"+function(s,o){for(var i=\"\",a=0;a<s.length;a+=1)i+=s[a],a+1<s.length&&(i+=o);return i}(C,\",\")+\"){ return binder.apply(this,arguments); }\")((function(){if(this instanceof _){var o=u.apply(this,a(w,arguments));return Object(o)===o?o:this}return u.apply(s,a(w,arguments))})),u.prototype){var L=function Empty(){};L.prototype=u.prototype,_.prototype=new L,L.prototype=null}return _}},89593:(s,o,i)=>{\"use strict\";o.H=void 0;var a=function _interopRequireDefault(s){return s&&s.__esModule?s:{default:s}}(i(84977));o.H=a.default},89935:s=>{s.exports=function stubFalse(){return!1}},90160:(s,o,i)=>{\"use strict\";var a=i(73948),u=String;s.exports=function(s){if(\"Symbol\"===a(s))throw new TypeError(\"Cannot convert a Symbol value to a string\");return u(s)}},90179:(s,o,i)=>{var a=i(34932),u=i(9999),_=i(19931),w=i(31769),x=i(21791),C=i(53138),j=i(38816),L=i(83349),B=j((function(s,o){var i={};if(null==s)return i;var j=!1;o=a(o,(function(o){return o=w(o,s),j||(j=o.length>1),o})),x(s,L(s),i),j&&(i=u(i,7,C));for(var B=o.length;B--;)_(i,o[B]);return i}));s.exports=B},90181:s=>{s.exports=function nativeKeysIn(s){var o=[];if(null!=s)for(var i in Object(s))o.push(i);return o}},90289:(s,o,i)=>{var a=i(12651);s.exports=function mapCacheGet(s){return a(this,s).get(s)}},90392:(s,o,i)=>{\"use strict\";var a=i(92861).Buffer,u=i(15377);function Hash(s,o){this._block=a.alloc(s),this._finalSize=o,this._blockSize=s,this._len=0}Hash.prototype.update=function(s,o){s=u(s,o||\"utf8\");for(var i=this._block,a=this._blockSize,_=s.length,w=this._len,x=0;x<_;){for(var C=w%a,j=Math.min(_-x,a-C),L=0;L<j;L++)i[C+L]=s[x+L];x+=j,(w+=j)%a==0&&this._update(i)}return this._len+=_,this},Hash.prototype.digest=function(s){var o=this._len%this._blockSize;this._block[o]=128,this._block.fill(0,o+1),o>=this._finalSize&&(this._update(this._block),this._block.fill(0));var i=8*this._len;if(i<=4294967295)this._block.writeUInt32BE(i,this._blockSize-4);else{var a=(4294967295&i)>>>0,u=(i-a)/4294967296;this._block.writeUInt32BE(u,this._blockSize-8),this._block.writeUInt32BE(a,this._blockSize-4)}this._update(this._block);var _=this._hash();return s?_.toString(s):_},Hash.prototype._update=function(){throw new Error(\"_update must be implemented by subclass\")},s.exports=Hash},90916:(s,o,i)=>{var a=i(80909);s.exports=function baseSome(s,o){var i;return a(s,(function(s,a,u){return!(i=o(s,a,u))})),!!i}},90938:s=>{s.exports=function stackDelete(s){var o=this.__data__,i=o.delete(s);return this.size=o.size,i}},91033:s=>{s.exports=function apply(s,o,i){switch(i.length){case 0:return s.call(o);case 1:return s.call(o,i[0]);case 2:return s.call(o,i[0],i[1]);case 3:return s.call(o,i[0],i[1],i[2])}return s.apply(o,i)}},91596:s=>{var o=Math.max;s.exports=function composeArgs(s,i,a,u){for(var _=-1,w=s.length,x=a.length,C=-1,j=i.length,L=o(w-x,0),B=Array(j+L),$=!u;++C<j;)B[C]=i[C];for(;++_<x;)($||_<w)&&(B[a[_]]=s[_]);for(;L--;)B[C++]=s[_++];return B}},91599:(s,o,i)=>{\"use strict\";i(64502)},92046:s=>{\"use strict\";s.exports={}},92063:s=>{\"use strict\";s.exports=function required(s,o){if(o=o.split(\":\")[0],!(s=+s))return!1;switch(o){case\"http\":case\"ws\":return 80!==s;case\"https\":case\"wss\":return 443!==s;case\"ftp\":return 21!==s;case\"gopher\":return 70!==s;case\"file\":return!1}return 0!==s}},92271:(s,o,i)=>{var a=i(21791),u=i(4664);s.exports=function copySymbols(s,o){return a(s,u(s),o)}},92340:(s,o,i)=>{const a=i(6048);function coerceElementMatchingCallback(s){return\"string\"==typeof s?o=>o.element===s:s.constructor&&s.extend?o=>o instanceof s:s}class ArraySlice{constructor(s){this.elements=s||[]}toValue(){return this.elements.map((s=>s.toValue()))}map(s,o){return this.elements.map(s,o)}flatMap(s,o){return this.map(s,o).reduce(((s,o)=>s.concat(o)),[])}compactMap(s,o){const i=[];return this.forEach((a=>{const u=s.bind(o)(a);u&&i.push(u)})),i}filter(s,o){return s=coerceElementMatchingCallback(s),new ArraySlice(this.elements.filter(s,o))}reject(s,o){return s=coerceElementMatchingCallback(s),new ArraySlice(this.elements.filter(a(s),o))}find(s,o){return s=coerceElementMatchingCallback(s),this.elements.find(s,o)}forEach(s,o){this.elements.forEach(s,o)}reduce(s,o){return this.elements.reduce(s,o)}includes(s){return this.elements.some((o=>o.equals(s)))}shift(){return this.elements.shift()}unshift(s){this.elements.unshift(this.refract(s))}push(s){return this.elements.push(this.refract(s)),this}add(s){this.push(s)}get(s){return this.elements[s]}getValue(s){const o=this.elements[s];if(o)return o.toValue()}get length(){return this.elements.length}get isEmpty(){return 0===this.elements.length}get first(){return this.elements[0]}}\"undefined\"!=typeof Symbol&&(ArraySlice.prototype[Symbol.iterator]=function symbol(){return this.elements[Symbol.iterator]()}),s.exports=ArraySlice},92361:(s,o,i)=>{\"use strict\";var a=i(45807),u=i(1907);s.exports=function(s){if(\"Function\"===a(s))return u(s)}},92522:(s,o,i)=>{\"use strict\";var a=i(85816),u=i(6499),_=a(\"keys\");s.exports=function(s){return _[s]||(_[s]=u(s))}},92861:(s,o,i)=>{var a=i(48287),u=a.Buffer;function copyProps(s,o){for(var i in s)o[i]=s[i]}function SafeBuffer(s,o,i){return u(s,o,i)}u.from&&u.alloc&&u.allocUnsafe&&u.allocUnsafeSlow?s.exports=a:(copyProps(a,o),o.Buffer=SafeBuffer),SafeBuffer.prototype=Object.create(u.prototype),copyProps(u,SafeBuffer),SafeBuffer.from=function(s,o,i){if(\"number\"==typeof s)throw new TypeError(\"Argument must not be a number\");return u(s,o,i)},SafeBuffer.alloc=function(s,o,i){if(\"number\"!=typeof s)throw new TypeError(\"Argument must be a number\");var a=u(s);return void 0!==o?\"string\"==typeof i?a.fill(o,i):a.fill(o):a.fill(0),a},SafeBuffer.allocUnsafe=function(s){if(\"number\"!=typeof s)throw new TypeError(\"Argument must be a number\");return u(s)},SafeBuffer.allocUnsafeSlow=function(s){if(\"number\"!=typeof s)throw new TypeError(\"Argument must be a number\");return a.SlowBuffer(s)}},93243:(s,o,i)=>{var a=i(56110),u=function(){try{var s=a(Object,\"defineProperty\");return s({},\"\",{}),s}catch(s){}}();s.exports=u},93290:(s,o,i)=>{s=i.nmd(s);var a=i(9325),u=o&&!o.nodeType&&o,_=u&&s&&!s.nodeType&&s,w=_&&_.exports===u?a.Buffer:void 0,x=w?w.allocUnsafe:void 0;s.exports=function cloneBuffer(s,o){if(o)return s.slice();var i=s.length,a=x?x(i):new s.constructor(i);return s.copy(a),a}},93427:(s,o,i)=>{\"use strict\";var a=i(1907);s.exports=a([].slice)},93628:(s,o,i)=>{\"use strict\";var a=i(48648),u=i(71064),_=i(7176);s.exports=a?function getProto(s){return a(s)}:u?function getProto(s){if(!s||\"object\"!=typeof s&&\"function\"!=typeof s)throw new TypeError(\"getProto: not an object\");return u(s)}:_?function getProto(s){return _(s)}:null},93663:(s,o,i)=>{var a=i(41799),u=i(10776),_=i(67197);s.exports=function baseMatches(s){var o=u(s);return 1==o.length&&o[0][2]?_(o[0][0],o[0][1]):function(i){return i===s||a(i,s,o)}}},93700:(s,o,i)=>{\"use strict\";var a=i(19709);s.exports=a},93736:(s,o,i)=>{var a=i(51873),u=a?a.prototype:void 0,_=u?u.valueOf:void 0;s.exports=function cloneSymbol(s){return _?Object(_.call(s)):{}}},93742:s=>{\"use strict\";s.exports={}},94033:s=>{s.exports=function baseLodash(){}},94459:s=>{\"use strict\";s.exports=Number.isNaN||function isNaN(s){return s!=s}},94643:(s,o,i)=>{function config(s){try{if(!i.g.localStorage)return!1}catch(s){return!1}var o=i.g.localStorage[s];return null!=o&&\"true\"===String(o).toLowerCase()}s.exports=function deprecate(s,o){if(config(\"noDeprecation\"))return s;var i=!1;return function deprecated(){if(!i){if(config(\"throwDeprecation\"))throw new Error(o);config(\"traceDeprecation\")?console.trace(o):console.warn(o),i=!0}return s.apply(this,arguments)}}},95089:s=>{const o=\"[A-Za-z$_][0-9A-Za-z$_]*\",i=[\"as\",\"in\",\"of\",\"if\",\"for\",\"while\",\"finally\",\"var\",\"new\",\"function\",\"do\",\"return\",\"void\",\"else\",\"break\",\"catch\",\"instanceof\",\"with\",\"throw\",\"case\",\"default\",\"try\",\"switch\",\"continue\",\"typeof\",\"delete\",\"let\",\"yield\",\"const\",\"class\",\"debugger\",\"async\",\"await\",\"static\",\"import\",\"from\",\"export\",\"extends\"],a=[\"true\",\"false\",\"null\",\"undefined\",\"NaN\",\"Infinity\"],u=[].concat([\"setInterval\",\"setTimeout\",\"clearInterval\",\"clearTimeout\",\"require\",\"exports\",\"eval\",\"isFinite\",\"isNaN\",\"parseFloat\",\"parseInt\",\"decodeURI\",\"decodeURIComponent\",\"encodeURI\",\"encodeURIComponent\",\"escape\",\"unescape\"],[\"arguments\",\"this\",\"super\",\"console\",\"window\",\"document\",\"localStorage\",\"module\",\"global\"],[\"Intl\",\"DataView\",\"Number\",\"Math\",\"Date\",\"String\",\"RegExp\",\"Object\",\"Function\",\"Boolean\",\"Error\",\"Symbol\",\"Set\",\"Map\",\"WeakSet\",\"WeakMap\",\"Proxy\",\"Reflect\",\"JSON\",\"Promise\",\"Float64Array\",\"Int16Array\",\"Int32Array\",\"Int8Array\",\"Uint16Array\",\"Uint32Array\",\"Float32Array\",\"Array\",\"Uint8Array\",\"Uint8ClampedArray\",\"ArrayBuffer\",\"BigInt64Array\",\"BigUint64Array\",\"BigInt\"],[\"EvalError\",\"InternalError\",\"RangeError\",\"ReferenceError\",\"SyntaxError\",\"TypeError\",\"URIError\"]);function lookahead(s){return concat(\"(?=\",s,\")\")}function concat(...s){return s.map((s=>function source(s){return s?\"string\"==typeof s?s:s.source:null}(s))).join(\"\")}s.exports=function javascript(s){const _=o,w=\"<>\",x=\"</>\",C={begin:/<[A-Za-z0-9\\\\._:-]+/,end:/\\/[A-Za-z0-9\\\\._:-]+>|\\/>/,isTrulyOpeningTag:(s,o)=>{const i=s[0].length+s.index,a=s.input[i];\"<\"!==a?\">\"===a&&(((s,{after:o})=>{const i=\"</\"+s[0].slice(1);return-1!==s.input.indexOf(i,o)})(s,{after:i})||o.ignoreMatch()):o.ignoreMatch()}},j={$pattern:o,keyword:i,literal:a,built_in:u},L=\"[0-9](_?[0-9])*\",B=`\\\\.(${L})`,$=\"0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*\",U={className:\"number\",variants:[{begin:`(\\\\b(${$})((${B})|\\\\.)?|(${B}))[eE][+-]?(${L})\\\\b`},{begin:`\\\\b(${$})\\\\b((${B})\\\\b|\\\\.)?|(${B})\\\\b`},{begin:\"\\\\b(0|[1-9](_?[0-9])*)n\\\\b\"},{begin:\"\\\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\\\b\"},{begin:\"\\\\b0[bB][0-1](_?[0-1])*n?\\\\b\"},{begin:\"\\\\b0[oO][0-7](_?[0-7])*n?\\\\b\"},{begin:\"\\\\b0[0-7]+n?\\\\b\"}],relevance:0},V={className:\"subst\",begin:\"\\\\$\\\\{\",end:\"\\\\}\",keywords:j,contains:[]},z={begin:\"html`\",end:\"\",starts:{end:\"`\",returnEnd:!1,contains:[s.BACKSLASH_ESCAPE,V],subLanguage:\"xml\"}},Y={begin:\"css`\",end:\"\",starts:{end:\"`\",returnEnd:!1,contains:[s.BACKSLASH_ESCAPE,V],subLanguage:\"css\"}},Z={className:\"string\",begin:\"`\",end:\"`\",contains:[s.BACKSLASH_ESCAPE,V]},ee={className:\"comment\",variants:[s.COMMENT(/\\/\\*\\*(?!\\/)/,\"\\\\*/\",{relevance:0,contains:[{className:\"doctag\",begin:\"@[A-Za-z]+\",contains:[{className:\"type\",begin:\"\\\\{\",end:\"\\\\}\",relevance:0},{className:\"variable\",begin:_+\"(?=\\\\s*(-)|$)\",endsParent:!0,relevance:0},{begin:/(?=[^\\n])\\s/,relevance:0}]}]}),s.C_BLOCK_COMMENT_MODE,s.C_LINE_COMMENT_MODE]},ie=[s.APOS_STRING_MODE,s.QUOTE_STRING_MODE,z,Y,Z,U,s.REGEXP_MODE];V.contains=ie.concat({begin:/\\{/,end:/\\}/,keywords:j,contains:[\"self\"].concat(ie)});const ae=[].concat(ee,V.contains),ce=ae.concat([{begin:/\\(/,end:/\\)/,keywords:j,contains:[\"self\"].concat(ae)}]),le={className:\"params\",begin:/\\(/,end:/\\)/,excludeBegin:!0,excludeEnd:!0,keywords:j,contains:ce};return{name:\"Javascript\",aliases:[\"js\",\"jsx\",\"mjs\",\"cjs\"],keywords:j,exports:{PARAMS_CONTAINS:ce},illegal:/#(?![$_A-z])/,contains:[s.SHEBANG({label:\"shebang\",binary:\"node\",relevance:5}),{label:\"use_strict\",className:\"meta\",relevance:10,begin:/^\\s*['\"]use (strict|asm)['\"]/},s.APOS_STRING_MODE,s.QUOTE_STRING_MODE,z,Y,Z,ee,U,{begin:concat(/[{,\\n]\\s*/,lookahead(concat(/(((\\/\\/.*$)|(\\/\\*(\\*[^/]|[^*])*\\*\\/))\\s*)*/,_+\"\\\\s*:\"))),relevance:0,contains:[{className:\"attr\",begin:_+lookahead(\"\\\\s*:\"),relevance:0}]},{begin:\"(\"+s.RE_STARTERS_RE+\"|\\\\b(case|return|throw)\\\\b)\\\\s*\",keywords:\"return throw case\",contains:[ee,s.REGEXP_MODE,{className:\"function\",begin:\"(\\\\([^()]*(\\\\([^()]*(\\\\([^()]*\\\\)[^()]*)*\\\\)[^()]*)*\\\\)|\"+s.UNDERSCORE_IDENT_RE+\")\\\\s*=>\",returnBegin:!0,end:\"\\\\s*=>\",contains:[{className:\"params\",variants:[{begin:s.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\\(\\s*\\)/,skip:!0},{begin:/\\(/,end:/\\)/,excludeBegin:!0,excludeEnd:!0,keywords:j,contains:ce}]}]},{begin:/,/,relevance:0},{className:\"\",begin:/\\s/,end:/\\s*/,skip:!0},{variants:[{begin:w,end:x},{begin:C.begin,\"on:begin\":C.isTrulyOpeningTag,end:C.end}],subLanguage:\"xml\",contains:[{begin:C.begin,end:C.end,skip:!0,contains:[\"self\"]}]}],relevance:0},{className:\"function\",beginKeywords:\"function\",end:/[{;]/,excludeEnd:!0,keywords:j,contains:[\"self\",s.inherit(s.TITLE_MODE,{begin:_}),le],illegal:/%/},{beginKeywords:\"while if switch catch for\"},{className:\"function\",begin:s.UNDERSCORE_IDENT_RE+\"\\\\([^()]*(\\\\([^()]*(\\\\([^()]*\\\\)[^()]*)*\\\\)[^()]*)*\\\\)\\\\s*\\\\{\",returnBegin:!0,contains:[le,s.inherit(s.TITLE_MODE,{begin:_})]},{variants:[{begin:\"\\\\.\"+_},{begin:\"\\\\$\"+_}],relevance:0},{className:\"class\",beginKeywords:\"class\",end:/[{;=]/,excludeEnd:!0,illegal:/[:\"[\\]]/,contains:[{beginKeywords:\"extends\"},s.UNDERSCORE_TITLE_MODE]},{begin:/\\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[s.inherit(s.TITLE_MODE,{begin:_}),\"self\",le]},{begin:\"(get|set)\\\\s+(?=\"+_+\"\\\\()\",end:/\\{/,keywords:\"get set\",contains:[s.inherit(s.TITLE_MODE,{begin:_}),{begin:/\\(\\)/},le]},{begin:/\\$[(.]/}]}}},95116:(s,o,i)=>{\"use strict\";var a,u,_,w=i(98828),x=i(62250),C=i(46285),j=i(58075),L=i(15972),B=i(68055),$=i(76264),U=i(7376),V=$(\"iterator\"),z=!1;[].keys&&(\"next\"in(_=[].keys())?(u=L(L(_)))!==Object.prototype&&(a=u):z=!0),!C(a)||w((function(){var s={};return a[V].call(s)!==s}))?a={}:U&&(a=j(a)),x(a[V])||B(a,V,(function(){return this})),s.exports={IteratorPrototype:a,BUGGY_SAFARI_ITERATORS:z}},95950:(s,o,i)=>{var a=i(70695),u=i(88984),_=i(64894);s.exports=function keys(s){return _(s)?a(s):u(s)}},96131:(s,o,i)=>{var a=i(2523),u=i(85463),_=i(76959);s.exports=function baseIndexOf(s,o,i){return o==o?_(s,o,i):a(s,u,i)}},96540:(s,o,i)=>{\"use strict\";s.exports=i(15287)},96605:(s,o,i)=>{\"use strict\";var a=i(11091),u=i(45951),_=i(76024),w=i(19358),x=\"WebAssembly\",C=u[x],j=7!==new Error(\"e\",{cause:7}).cause,exportGlobalErrorCauseWrapper=function(s,o){var i={};i[s]=w(s,o,j),a({global:!0,constructor:!0,arity:1,forced:j},i)},exportWebAssemblyErrorCauseWrapper=function(s,o){if(C&&C[s]){var i={};i[s]=w(x+\".\"+s,o,j),a({target:x,stat:!0,constructor:!0,arity:1,forced:j},i)}};exportGlobalErrorCauseWrapper(\"Error\",(function(s){return function Error(o){return _(s,this,arguments)}})),exportGlobalErrorCauseWrapper(\"EvalError\",(function(s){return function EvalError(o){return _(s,this,arguments)}})),exportGlobalErrorCauseWrapper(\"RangeError\",(function(s){return function RangeError(o){return _(s,this,arguments)}})),exportGlobalErrorCauseWrapper(\"ReferenceError\",(function(s){return function ReferenceError(o){return _(s,this,arguments)}})),exportGlobalErrorCauseWrapper(\"SyntaxError\",(function(s){return function SyntaxError(o){return _(s,this,arguments)}})),exportGlobalErrorCauseWrapper(\"TypeError\",(function(s){return function TypeError(o){return _(s,this,arguments)}})),exportGlobalErrorCauseWrapper(\"URIError\",(function(s){return function URIError(o){return _(s,this,arguments)}})),exportWebAssemblyErrorCauseWrapper(\"CompileError\",(function(s){return function CompileError(o){return _(s,this,arguments)}})),exportWebAssemblyErrorCauseWrapper(\"LinkError\",(function(s){return function LinkError(o){return _(s,this,arguments)}})),exportWebAssemblyErrorCauseWrapper(\"RuntimeError\",(function(s){return function RuntimeError(o){return _(s,this,arguments)}}))},96794:(s,o,i)=>{\"use strict\";var a=i(45951).navigator,u=a&&a.userAgent;s.exports=u?String(u):\"\"},96897:(s,o,i)=>{\"use strict\";var a=i(70453),u=i(30041),_=i(30592)(),w=i(75795),x=i(69675),C=a(\"%Math.floor%\");s.exports=function setFunctionLength(s,o){if(\"function\"!=typeof s)throw new x(\"`fn` is not a function\");if(\"number\"!=typeof o||o<0||o>4294967295||C(o)!==o)throw new x(\"`length` must be a positive 32-bit integer\");var i=arguments.length>2&&!!arguments[2],a=!0,j=!0;if(\"length\"in s&&w){var L=w(s,\"length\");L&&!L.configurable&&(a=!1),L&&!L.writable&&(j=!1)}return(a||j||!i)&&(_?u(s,\"length\",o,!0,!0):u(s,\"length\",o)),s}},98023:(s,o,i)=>{var a=i(72552),u=i(40346);s.exports=function isNumber(s){return\"number\"==typeof s||u(s)&&\"[object Number]\"==a(s)}},98828:s=>{\"use strict\";s.exports=function(s){try{return!!s()}catch(s){return!0}}},99363:(s,o,i)=>{\"use strict\";var a=i(4993),u=i(42156),_=i(93742),w=i(64932),x=i(74284).f,C=i(60183),j=i(59550),L=i(7376),B=i(39447),$=\"Array Iterator\",U=w.set,V=w.getterFor($);s.exports=C(Array,\"Array\",(function(s,o){U(this,{type:$,target:a(s),index:0,kind:o})}),(function(){var s=V(this),o=s.target,i=s.index++;if(!o||i>=o.length)return s.target=null,j(void 0,!0);switch(s.kind){case\"keys\":return j(i,!1);case\"values\":return j(o[i],!1)}return j([i,o[i]],!1)}),\"values\");var z=_.Arguments=_.Array;if(u(\"keys\"),u(\"values\"),u(\"entries\"),!L&&B&&\"values\"!==z.name)try{x(z,\"name\",{value:\"values\"})}catch(s){}},99374:(s,o,i)=>{var a=i(54128),u=i(23805),_=i(44394),w=/^[-+]0x[0-9a-f]+$/i,x=/^0b[01]+$/i,C=/^0o[0-7]+$/i,j=parseInt;s.exports=function toNumber(s){if(\"number\"==typeof s)return s;if(_(s))return NaN;if(u(s)){var o=\"function\"==typeof s.valueOf?s.valueOf():s;s=u(o)?o+\"\":o}if(\"string\"!=typeof s)return 0===s?s:+s;s=a(s);var i=x.test(s);return i||C.test(s)?j(s.slice(2),i?2:8):w.test(s)?NaN:+s}}},o={};function __webpack_require__(i){var a=o[i];if(void 0!==a)return a.exports;var u=o[i]={id:i,loaded:!1,exports:{}};return s[i].call(u.exports,u,u.exports,__webpack_require__),u.loaded=!0,u.exports}__webpack_require__.n=s=>{var o=s&&s.__esModule?()=>s.default:()=>s;return __webpack_require__.d(o,{a:o}),o},__webpack_require__.d=(s,o)=>{for(var i in o)__webpack_require__.o(o,i)&&!__webpack_require__.o(s,i)&&Object.defineProperty(s,i,{enumerable:!0,get:o[i]})},__webpack_require__.g=function(){if(\"object\"==typeof globalThis)return globalThis;try{return this||new Function(\"return this\")()}catch(s){if(\"object\"==typeof window)return window}}(),__webpack_require__.o=(s,o)=>Object.prototype.hasOwnProperty.call(s,o),__webpack_require__.r=s=>{\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(s,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(s,\"__esModule\",{value:!0})},__webpack_require__.nmd=s=>(s.paths=[],s.children||(s.children=[]),s);var i={};return(()=>{\"use strict\";__webpack_require__.d(i,{default:()=>WT});var s={};__webpack_require__.r(s),__webpack_require__.d(s,{CLEAR:()=>at,CLEAR_BY:()=>ct,NEW_AUTH_ERR:()=>it,NEW_SPEC_ERR:()=>st,NEW_SPEC_ERR_BATCH:()=>ot,NEW_THROWN_ERR:()=>rt,NEW_THROWN_ERR_BATCH:()=>nt,clear:()=>clear,clearBy:()=>clearBy,newAuthErr:()=>newAuthErr,newSpecErr:()=>newSpecErr,newSpecErrBatch:()=>newSpecErrBatch,newThrownErr:()=>newThrownErr,newThrownErrBatch:()=>newThrownErrBatch});var o={};__webpack_require__.r(o),__webpack_require__.d(o,{AUTHORIZE:()=>Rt,AUTHORIZE_OAUTH2:()=>Lt,CONFIGURE_AUTH:()=>Ft,LOGOUT:()=>Dt,RESTORE_AUTHORIZATION:()=>Bt,SHOW_AUTH_POPUP:()=>Mt,authPopup:()=>authPopup,authorize:()=>authorize,authorizeAccessCodeWithBasicAuthentication:()=>authorizeAccessCodeWithBasicAuthentication,authorizeAccessCodeWithFormParams:()=>authorizeAccessCodeWithFormParams,authorizeApplication:()=>authorizeApplication,authorizeOauth2:()=>authorizeOauth2,authorizeOauth2WithPersistOption:()=>authorizeOauth2WithPersistOption,authorizePassword:()=>authorizePassword,authorizeRequest:()=>authorizeRequest,authorizeWithPersistOption:()=>authorizeWithPersistOption,configureAuth:()=>configureAuth,logout:()=>logout,logoutWithPersistOption:()=>logoutWithPersistOption,persistAuthorizationIfNeeded:()=>persistAuthorizationIfNeeded,preAuthorizeImplicit:()=>preAuthorizeImplicit,restoreAuthorization:()=>restoreAuthorization,showDefinitions:()=>showDefinitions});var a={};__webpack_require__.r(a),__webpack_require__.d(a,{authorized:()=>Jt,definitionsForRequirements:()=>definitionsForRequirements,definitionsToAuthorize:()=>Wt,getConfigs:()=>Ht,getDefinitionsByNames:()=>getDefinitionsByNames,isAuthorized:()=>isAuthorized,selectAuthPath:()=>selectAuthPath,shownDefinitions:()=>zt});var u={};__webpack_require__.r(u),__webpack_require__.d(u,{TOGGLE_CONFIGS:()=>gn,UPDATE_CONFIGS:()=>mn,downloadConfig:()=>downloadConfig,getConfigByUrl:()=>getConfigByUrl,loaded:()=>actions_loaded,toggle:()=>toggle,update:()=>update});var _={};__webpack_require__.r(_),__webpack_require__.d(_,{get:()=>get});var w={};__webpack_require__.r(w),__webpack_require__.d(w,{transform:()=>transform});var x={};__webpack_require__.r(x),__webpack_require__.d(x,{transform:()=>parameter_oneof_transform});var C={};__webpack_require__.r(C),__webpack_require__.d(C,{allErrors:()=>In,lastError:()=>Tn});var j={};__webpack_require__.r(j),__webpack_require__.d(j,{SHOW:()=>Fn,UPDATE_FILTER:()=>Dn,UPDATE_LAYOUT:()=>Rn,UPDATE_MODE:()=>Ln,changeMode:()=>changeMode,show:()=>actions_show,updateFilter:()=>updateFilter,updateLayout:()=>updateLayout});var L={};__webpack_require__.r(L),__webpack_require__.d(L,{current:()=>current,currentFilter:()=>currentFilter,isShown:()=>isShown,showSummary:()=>$n,whatMode:()=>whatMode});var B={};__webpack_require__.r(B),__webpack_require__.d(B,{taggedOperations:()=>taggedOperations});var $={};__webpack_require__.r($),__webpack_require__.d($,{getActiveLanguage:()=>Vn,getDefaultExpanded:()=>zn,getGenerators:()=>Un,getSnippetGenerators:()=>getSnippetGenerators});var U={};__webpack_require__.r(U),__webpack_require__.d(U,{JsonSchemaArrayItemFile:()=>JsonSchemaArrayItemFile,JsonSchemaArrayItemText:()=>JsonSchemaArrayItemText,JsonSchemaForm:()=>JsonSchemaForm,JsonSchema_array:()=>JsonSchema_array,JsonSchema_boolean:()=>JsonSchema_boolean,JsonSchema_object:()=>JsonSchema_object,JsonSchema_string:()=>JsonSchema_string});var V={};__webpack_require__.r(V),__webpack_require__.d(V,{allowTryItOutFor:()=>allowTryItOutFor,basePath:()=>Hs,canExecuteScheme:()=>canExecuteScheme,consumes:()=>Us,consumesOptionsFor:()=>consumesOptionsFor,contentTypeValues:()=>contentTypeValues,currentProducesFor:()=>currentProducesFor,definitions:()=>Js,externalDocs:()=>Ds,findDefinition:()=>findDefinition,getOAS3RequiredRequestBodyContentType:()=>getOAS3RequiredRequestBodyContentType,getParameter:()=>getParameter,hasHost:()=>ro,host:()=>Ks,info:()=>Rs,isMediaTypeSchemaPropertiesEqual:()=>isMediaTypeSchemaPropertiesEqual,isOAS3:()=>Ms,lastError:()=>Os,mutatedRequestFor:()=>mutatedRequestFor,mutatedRequests:()=>to,operationScheme:()=>operationScheme,operationWithMeta:()=>operationWithMeta,operations:()=>qs,operationsWithRootInherited:()=>Ys,operationsWithTags:()=>Qs,parameterInclusionSettingFor:()=>parameterInclusionSettingFor,parameterValues:()=>parameterValues,parameterWithMeta:()=>parameterWithMeta,parameterWithMetaByIdentity:()=>parameterWithMetaByIdentity,parametersIncludeIn:()=>parametersIncludeIn,parametersIncludeType:()=>parametersIncludeType,paths:()=>Bs,produces:()=>Vs,producesOptionsFor:()=>producesOptionsFor,requestFor:()=>requestFor,requests:()=>eo,responseFor:()=>responseFor,responses:()=>Zs,schemes:()=>Gs,security:()=>zs,securityDefinitions:()=>Ws,semver:()=>Fs,spec:()=>spec,specJS:()=>Is,specJson:()=>Ps,specJsonWithResolvedSubtrees:()=>Ns,specResolved:()=>Ts,specResolvedSubtree:()=>specResolvedSubtree,specSource:()=>js,specStr:()=>Cs,tagDetails:()=>tagDetails,taggedOperations:()=>selectors_taggedOperations,tags:()=>Xs,url:()=>As,validOperationMethods:()=>$s,validateBeforeExecute:()=>validateBeforeExecute,validationErrors:()=>validationErrors,version:()=>Ls});var z={};__webpack_require__.r(z),__webpack_require__.d(z,{CLEAR_REQUEST:()=>wo,CLEAR_RESPONSE:()=>Eo,CLEAR_VALIDATE_PARAMS:()=>xo,LOG_REQUEST:()=>So,SET_MUTATED_REQUEST:()=>_o,SET_REQUEST:()=>bo,SET_RESPONSE:()=>vo,SET_SCHEME:()=>Co,UPDATE_EMPTY_PARAM_INCLUSION:()=>go,UPDATE_JSON:()=>fo,UPDATE_OPERATION_META_VALUE:()=>ko,UPDATE_PARAM:()=>mo,UPDATE_RESOLVED:()=>Oo,UPDATE_RESOLVED_SUBTREE:()=>Ao,UPDATE_SPEC:()=>po,UPDATE_URL:()=>ho,VALIDATE_PARAMS:()=>yo,changeConsumesValue:()=>changeConsumesValue,changeParam:()=>changeParam,changeParamByIdentity:()=>changeParamByIdentity,changeProducesValue:()=>changeProducesValue,clearRequest:()=>clearRequest,clearResponse:()=>clearResponse,clearValidateParams:()=>clearValidateParams,execute:()=>actions_execute,executeRequest:()=>executeRequest,invalidateResolvedSubtreeCache:()=>invalidateResolvedSubtreeCache,logRequest:()=>logRequest,parseToJson:()=>parseToJson,requestResolvedSubtree:()=>requestResolvedSubtree,resolveSpec:()=>resolveSpec,setMutatedRequest:()=>setMutatedRequest,setRequest:()=>setRequest,setResponse:()=>setResponse,setScheme:()=>setScheme,updateEmptyParamInclusion:()=>updateEmptyParamInclusion,updateJsonSpec:()=>updateJsonSpec,updateResolved:()=>updateResolved,updateResolvedSubtree:()=>updateResolvedSubtree,updateSpec:()=>updateSpec,updateUrl:()=>updateUrl,validateParams:()=>validateParams});var Y={};__webpack_require__.r(Y),__webpack_require__.d(Y,{executeRequest:()=>wrap_actions_executeRequest,updateJsonSpec:()=>wrap_actions_updateJsonSpec,updateSpec:()=>wrap_actions_updateSpec,validateParams:()=>wrap_actions_validateParams});var Z={};__webpack_require__.r(Z),__webpack_require__.d(Z,{JsonPatchError:()=>Do,_areEquals:()=>_areEquals,applyOperation:()=>applyOperation,applyPatch:()=>applyPatch,applyReducer:()=>applyReducer,deepClone:()=>Lo,getValueByPointer:()=>getValueByPointer,validate:()=>validate,validator:()=>validator});var ee={};__webpack_require__.r(ee),__webpack_require__.d(ee,{compare:()=>compare,generate:()=>generate,observe:()=>observe,unobserve:()=>unobserve});var ie={};__webpack_require__.r(ie),__webpack_require__.d(ie,{hasElementSourceMap:()=>hasElementSourceMap,includesClasses:()=>includesClasses,includesSymbols:()=>includesSymbols,isAnnotationElement:()=>Fu,isArrayElement:()=>Mu,isBooleanElement:()=>Tu,isCommentElement:()=>Bu,isElement:()=>Cu,isLinkElement:()=>Du,isMemberElement:()=>Ru,isNullElement:()=>Iu,isNumberElement:()=>Pu,isObjectElement:()=>Nu,isParseResultElement:()=>$u,isPrimitiveElement:()=>isPrimitiveElement,isRefElement:()=>Lu,isStringElement:()=>ju});var ae={};__webpack_require__.r(ae),__webpack_require__.d(ae,{isJSONReferenceElement:()=>Ld,isJSONSchemaElement:()=>Dd,isLinkDescriptionElement:()=>Bd,isMediaElement:()=>Fd});var ce={};__webpack_require__.r(ce),__webpack_require__.d(ce,{isBooleanJsonSchemaElement:()=>isBooleanJsonSchemaElement,isCallbackElement:()=>Tm,isComponentsElement:()=>Nm,isContactElement:()=>Mm,isDiscriminatorElement:()=>og,isExampleElement:()=>Rm,isExternalDocumentationElement:()=>Dm,isHeaderElement:()=>Lm,isInfoElement:()=>Fm,isLicenseElement:()=>Bm,isLinkElement:()=>$m,isMediaTypeElement:()=>ng,isOpenApi3_0Element:()=>Um,isOpenapiElement:()=>qm,isOperationElement:()=>Vm,isParameterElement:()=>zm,isPathItemElement:()=>Wm,isPathsElement:()=>Jm,isReferenceElement:()=>Hm,isRequestBodyElement:()=>Km,isResponseElement:()=>Gm,isResponsesElement:()=>Ym,isSchemaElement:()=>Xm,isSecurityRequirementElement:()=>Qm,isSecuritySchemeElement:()=>Zm,isServerElement:()=>eg,isServerVariableElement:()=>rg,isServersElement:()=>sg});var le={};__webpack_require__.r(le),__webpack_require__.d(le,{isJSONReferenceElement:()=>Ld,isJSONSchemaElement:()=>g_,isLinkDescriptionElement:()=>y_,isMediaElement:()=>Fd});var pe={};__webpack_require__.r(pe),__webpack_require__.d(pe,{isJSONReferenceElement:()=>Ld,isJSONSchemaElement:()=>A_,isLinkDescriptionElement:()=>C_});var de={};__webpack_require__.r(de),__webpack_require__.d(de,{isJSONSchemaElement:()=>K_,isLinkDescriptionElement:()=>G_});var fe={};__webpack_require__.r(fe),__webpack_require__.d(fe,{isJSONSchemaElement:()=>oS,isLinkDescriptionElement:()=>iS});var ye={};__webpack_require__.r(ye),__webpack_require__.d(ye,{isBooleanJsonSchemaElement:()=>predicates_isBooleanJsonSchemaElement,isCallbackElement:()=>zS,isComponentsElement:()=>WS,isContactElement:()=>JS,isExampleElement:()=>HS,isExternalDocumentationElement:()=>KS,isHeaderElement:()=>GS,isInfoElement:()=>YS,isJsonSchemaDialectElement:()=>XS,isLicenseElement:()=>QS,isLinkElement:()=>ZS,isMediaTypeElement:()=>mE,isOpenApi3_1Element:()=>tE,isOpenapiElement:()=>eE,isOperationElement:()=>rE,isParameterElement:()=>nE,isPathItemElement:()=>sE,isPathItemElementExternal:()=>isPathItemElementExternal,isPathsElement:()=>oE,isReferenceElement:()=>iE,isReferenceElementExternal:()=>isReferenceElementExternal,isRequestBodyElement:()=>aE,isResponseElement:()=>cE,isResponsesElement:()=>lE,isSchemaElement:()=>uE,isSecurityRequirementElement:()=>pE,isSecuritySchemeElement:()=>hE,isServerElement:()=>dE,isServerVariableElement:()=>fE});var be={};__webpack_require__.r(be),__webpack_require__.d(be,{cookie:()=>cookie,header:()=>parameter_builders_header,path:()=>parameter_builders_path,query:()=>query});var _e={};__webpack_require__.r(_e),__webpack_require__.d(_e,{Button:()=>Button,Col:()=>Col,Collapse:()=>Collapse,Container:()=>Container,Input:()=>Input,Link:()=>layout_utils_Link,Row:()=>Row,Select:()=>Select,TextArea:()=>TextArea});var Se={};__webpack_require__.r(Se),__webpack_require__.d(Se,{basePath:()=>NP,consumes:()=>MP,definitions:()=>jP,findDefinition:()=>CP,hasHost:()=>PP,host:()=>TP,produces:()=>RP,schemes:()=>DP,securityDefinitions:()=>IP,validOperationMethods:()=>wrap_selectors_validOperationMethods});var we={};__webpack_require__.r(we),__webpack_require__.d(we,{definitionsToAuthorize:()=>LP});var xe={};__webpack_require__.r(xe),__webpack_require__.d(xe,{callbacksOperations:()=>$P,findSchema:()=>findSchema,isOAS3:()=>selectors_isOAS3,isOAS30:()=>selectors_isOAS30,isSwagger2:()=>selectors_isSwagger2,servers:()=>BP});var Pe={};__webpack_require__.r(Pe),__webpack_require__.d(Pe,{CLEAR_REQUEST_BODY_VALIDATE_ERROR:()=>iI,CLEAR_REQUEST_BODY_VALUE:()=>aI,SET_REQUEST_BODY_VALIDATE_ERROR:()=>oI,UPDATE_ACTIVE_EXAMPLES_MEMBER:()=>tI,UPDATE_REQUEST_BODY_INCLUSION:()=>eI,UPDATE_REQUEST_BODY_VALUE:()=>QP,UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG:()=>ZP,UPDATE_REQUEST_CONTENT_TYPE:()=>rI,UPDATE_RESPONSE_CONTENT_TYPE:()=>nI,UPDATE_SELECTED_SERVER:()=>XP,UPDATE_SERVER_VARIABLE_VALUE:()=>sI,clearRequestBodyValidateError:()=>clearRequestBodyValidateError,clearRequestBodyValue:()=>clearRequestBodyValue,initRequestBodyValidateError:()=>initRequestBodyValidateError,setActiveExamplesMember:()=>setActiveExamplesMember,setRequestBodyInclusion:()=>setRequestBodyInclusion,setRequestBodyValidateError:()=>setRequestBodyValidateError,setRequestBodyValue:()=>setRequestBodyValue,setRequestContentType:()=>setRequestContentType,setResponseContentType:()=>setResponseContentType,setRetainRequestBodyValueFlag:()=>setRetainRequestBodyValueFlag,setSelectedServer:()=>setSelectedServer,setServerVariableValue:()=>setServerVariableValue});var Te={};__webpack_require__.r(Te),__webpack_require__.d(Te,{activeExamplesMember:()=>gI,hasUserEditedBody:()=>dI,requestBodyErrors:()=>mI,requestBodyInclusionSetting:()=>fI,requestBodyValue:()=>pI,requestContentType:()=>yI,responseContentType:()=>vI,selectDefaultRequestBodyValue:()=>selectDefaultRequestBodyValue,selectedServer:()=>uI,serverEffectiveValue:()=>SI,serverVariableValue:()=>bI,serverVariables:()=>_I,shouldRetainRequestBodyValue:()=>hI,validOperationMethods:()=>wI,validateBeforeExecute:()=>EI,validateShallowRequired:()=>validateShallowRequired});var Re=__webpack_require__(96540);function formatProdErrorMessage(s){return`Minified Redux error #${s}; visit https://redux.js.org/Errors?code=${s} for the full message or use the non-minified dev environment for full errors. `}var $e=(()=>\"function\"==typeof Symbol&&Symbol.observable||\"@@observable\")(),randomString=()=>Math.random().toString(36).substring(7).split(\"\").join(\".\"),qe={INIT:`@@redux/INIT${randomString()}`,REPLACE:`@@redux/REPLACE${randomString()}`,PROBE_UNKNOWN_ACTION:()=>`@@redux/PROBE_UNKNOWN_ACTION${randomString()}`};function isPlainObject(s){if(\"object\"!=typeof s||null===s)return!1;let o=s;for(;null!==Object.getPrototypeOf(o);)o=Object.getPrototypeOf(o);return Object.getPrototypeOf(s)===o||null===Object.getPrototypeOf(s)}function createStore(s,o,i){if(\"function\"!=typeof s)throw new Error(formatProdErrorMessage(2));if(\"function\"==typeof o&&\"function\"==typeof i||\"function\"==typeof i&&\"function\"==typeof arguments[3])throw new Error(formatProdErrorMessage(0));if(\"function\"==typeof o&&void 0===i&&(i=o,o=void 0),void 0!==i){if(\"function\"!=typeof i)throw new Error(formatProdErrorMessage(1));return i(createStore)(s,o)}let a=s,u=o,_=new Map,w=_,x=0,C=!1;function ensureCanMutateNextListeners(){w===_&&(w=new Map,_.forEach(((s,o)=>{w.set(o,s)})))}function getState(){if(C)throw new Error(formatProdErrorMessage(3));return u}function subscribe(s){if(\"function\"!=typeof s)throw new Error(formatProdErrorMessage(4));if(C)throw new Error(formatProdErrorMessage(5));let o=!0;ensureCanMutateNextListeners();const i=x++;return w.set(i,s),function unsubscribe(){if(o){if(C)throw new Error(formatProdErrorMessage(6));o=!1,ensureCanMutateNextListeners(),w.delete(i),_=null}}}function dispatch(s){if(!isPlainObject(s))throw new Error(formatProdErrorMessage(7));if(void 0===s.type)throw new Error(formatProdErrorMessage(8));if(\"string\"!=typeof s.type)throw new Error(formatProdErrorMessage(17));if(C)throw new Error(formatProdErrorMessage(9));try{C=!0,u=a(u,s)}finally{C=!1}return(_=w).forEach((s=>{s()})),s}dispatch({type:qe.INIT});return{dispatch,subscribe,getState,replaceReducer:function replaceReducer(s){if(\"function\"!=typeof s)throw new Error(formatProdErrorMessage(10));a=s,dispatch({type:qe.REPLACE})},[$e]:function observable(){const s=subscribe;return{subscribe(o){if(\"object\"!=typeof o||null===o)throw new Error(formatProdErrorMessage(11));function observeState(){const s=o;s.next&&s.next(getState())}observeState();return{unsubscribe:s(observeState)}},[$e](){return this}}}}}function bindActionCreator(s,o){return function(...i){return o(s.apply(this,i))}}function compose(...s){return 0===s.length?s=>s:1===s.length?s[0]:s.reduce(((s,o)=>(...i)=>s(o(...i))))}var ze=__webpack_require__(9404),We=__webpack_require__.n(ze),He=__webpack_require__(81919),Ye=__webpack_require__.n(He),Xe=__webpack_require__(89593),Qe=__webpack_require__(20334),et=__webpack_require__(55364),tt=__webpack_require__.n(et);const rt=\"err_new_thrown_err\",nt=\"err_new_thrown_err_batch\",st=\"err_new_spec_err\",ot=\"err_new_spec_err_batch\",it=\"err_new_auth_err\",at=\"err_clear\",ct=\"err_clear_by\";function newThrownErr(s){return{type:rt,payload:(0,Qe.serializeError)(s)}}function newThrownErrBatch(s){return{type:nt,payload:s}}function newSpecErr(s){return{type:st,payload:s}}function newSpecErrBatch(s){return{type:ot,payload:s}}function newAuthErr(s){return{type:it,payload:s}}function clear(s={}){return{type:at,payload:s}}function clearBy(s=()=>!0){return{type:ct,payload:s}}const lt=function makeWindow(){var s={location:{},history:{},open:()=>{},close:()=>{},File:function(){},FormData:function(){}};if(\"undefined\"==typeof window)return s;try{s=window;for(var o of[\"File\",\"Blob\",\"FormData\"])o in window&&(s[o]=window[o])}catch(s){console.error(s)}return s}();__webpack_require__(84058),__webpack_require__(55808);var ut=__webpack_require__(50104),pt=__webpack_require__.n(ut),ht=__webpack_require__(7309),dt=__webpack_require__.n(ht),mt=__webpack_require__(42426),gt=__webpack_require__.n(mt),yt=__webpack_require__(75288),vt=__webpack_require__.n(yt),bt=__webpack_require__(1882),_t=__webpack_require__.n(bt),St=__webpack_require__(2205),Et=__webpack_require__.n(St),wt=__webpack_require__(53209),xt=__webpack_require__.n(wt),kt=__webpack_require__(62802),Ot=__webpack_require__.n(kt);const At=We().Set.of(\"type\",\"format\",\"items\",\"default\",\"maximum\",\"exclusiveMaximum\",\"minimum\",\"exclusiveMinimum\",\"maxLength\",\"minLength\",\"pattern\",\"maxItems\",\"minItems\",\"uniqueItems\",\"enum\",\"multipleOf\");function getParameterSchema(s,{isOAS3:o}={}){if(!We().Map.isMap(s))return{schema:We().Map(),parameterContentMediaType:null};if(!o)return\"body\"===s.get(\"in\")?{schema:s.get(\"schema\",We().Map()),parameterContentMediaType:null}:{schema:s.filter(((s,o)=>At.includes(o))),parameterContentMediaType:null};if(s.get(\"content\")){const o=s.get(\"content\",We().Map({})).keySeq().first();return{schema:s.getIn([\"content\",o,\"schema\"],We().Map()),parameterContentMediaType:o}}return{schema:s.get(\"schema\")?s.get(\"schema\",We().Map()):We().Map(),parameterContentMediaType:null}}var Ct=__webpack_require__(48287).Buffer;const jt=\"default\",isImmutable=s=>We().Iterable.isIterable(s),immutableToJS=s=>isImmutable(s)?s.toJS():s;function objectify(s){return isObject(s)?immutableToJS(s):{}}function fromJSOrdered(s){if(isImmutable(s))return s;if(s instanceof lt.File)return s;if(!isObject(s))return s;if(Array.isArray(s))return We().Seq(s).map(fromJSOrdered).toList();if(_t()(s.entries)){const o=function createObjWithHashedKeys(s){if(!_t()(s.entries))return s;const o={},i=\"_**[]\",a={};for(let u of s.entries())if(o[u[0]]||a[u[0]]&&a[u[0]].containsMultiple){if(!a[u[0]]){a[u[0]]={containsMultiple:!0,length:1},o[`${u[0]}${i}${a[u[0]].length}`]=o[u[0]],delete o[u[0]]}a[u[0]].length+=1,o[`${u[0]}${i}${a[u[0]].length}`]=u[1]}else o[u[0]]=u[1];return o}(s);return We().OrderedMap(o).map(fromJSOrdered)}return We().OrderedMap(s).map(fromJSOrdered)}function normalizeArray(s){return Array.isArray(s)?s:[s]}function isFn(s){return\"function\"==typeof s}function isObject(s){return!!s&&\"object\"==typeof s}function isFunc(s){return\"function\"==typeof s}function isArray(s){return Array.isArray(s)}const Pt=pt();function objMap(s,o){return Object.keys(s).reduce(((i,a)=>(i[a]=o(s[a],a),i)),{})}function objReduce(s,o){return Object.keys(s).reduce(((i,a)=>{let u=o(s[a],a);return u&&\"object\"==typeof u&&Object.assign(i,u),i}),{})}function systemThunkMiddleware(s){return({dispatch:o,getState:i})=>o=>i=>\"function\"==typeof i?i(s()):o(i)}function validateValueBySchema(s,o,i,a,u){if(!o)return[];let _=[],w=o.get(\"nullable\"),x=o.get(\"required\"),C=o.get(\"maximum\"),j=o.get(\"minimum\"),L=o.get(\"type\"),B=o.get(\"format\"),$=o.get(\"maxLength\"),U=o.get(\"minLength\"),V=o.get(\"uniqueItems\"),z=o.get(\"maxItems\"),Y=o.get(\"minItems\"),Z=o.get(\"pattern\");const ee=i||!0===x,ie=null!=s,ae=ee||ie&&\"array\"===L||!(!ee&&!ie),ce=w&&null===s;if(ee&&!ie&&!ce&&!a&&!L)return _.push(\"Required field is not provided\"),_;if(ce||!L||!ae)return[];let le=\"string\"===L&&s,pe=\"array\"===L&&Array.isArray(s)&&s.length,de=\"array\"===L&&We().List.isList(s)&&s.count();const fe=[le,pe,de,\"array\"===L&&\"string\"==typeof s&&s,\"file\"===L&&s instanceof lt.File,\"boolean\"===L&&(s||!1===s),\"number\"===L&&(s||0===s),\"integer\"===L&&(s||0===s),\"object\"===L&&\"object\"==typeof s&&null!==s,\"object\"===L&&\"string\"==typeof s&&s].some((s=>!!s));if(ee&&!fe&&!a)return _.push(\"Required field is not provided\"),_;if(\"object\"===L&&(null===u||\"application/json\"===u)){let i=s;if(\"string\"==typeof s)try{i=JSON.parse(s)}catch(s){return _.push(\"Parameter string value must be valid JSON\"),_}o&&o.has(\"required\")&&isFunc(x.isList)&&x.isList()&&x.forEach((s=>{void 0===i[s]&&_.push({propKey:s,error:\"Required property not found\"})})),o&&o.has(\"properties\")&&o.get(\"properties\").forEach(((s,o)=>{const w=validateValueBySchema(i[o],s,!1,a,u);_.push(...w.map((s=>({propKey:o,error:s}))))}))}if(Z){let o=((s,o)=>{if(!new RegExp(o).test(s))return\"Value must follow pattern \"+o})(s,Z);o&&_.push(o)}if(Y&&\"array\"===L){let o=((s,o)=>{if(!s&&o>=1||s&&s.length<o)return`Array must contain at least ${o} item${1===o?\"\":\"s\"}`})(s,Y);o&&_.push(o)}if(z&&\"array\"===L){let o=((s,o)=>{if(s&&s.length>o)return`Array must not contain more then ${o} item${1===o?\"\":\"s\"}`})(s,z);o&&_.push({needRemove:!0,error:o})}if(V&&\"array\"===L){let o=((s,o)=>{if(s&&(\"true\"===o||!0===o)){const o=(0,ze.fromJS)(s),i=o.toSet();if(s.length>i.size){let s=(0,ze.Set)();if(o.forEach(((i,a)=>{o.filter((s=>isFunc(s.equals)?s.equals(i):s===i)).size>1&&(s=s.add(a))})),0!==s.size)return s.map((s=>({index:s,error:\"No duplicates allowed.\"}))).toArray()}}})(s,V);o&&_.push(...o)}if($||0===$){let o=((s,o)=>{if(s.length>o)return`Value must be no longer than ${o} character${1!==o?\"s\":\"\"}`})(s,$);o&&_.push(o)}if(U){let o=((s,o)=>{if(s.length<o)return`Value must be at least ${o} character${1!==o?\"s\":\"\"}`})(s,U);o&&_.push(o)}if(C||0===C){let o=((s,o)=>{if(s>o)return`Value must be less than or equal to ${o}`})(s,C);o&&_.push(o)}if(j||0===j){let o=((s,o)=>{if(s<o)return`Value must be greater than or equal to ${o}`})(s,j);o&&_.push(o)}if(\"string\"===L){let o;if(o=\"date-time\"===B?(s=>{if(isNaN(Date.parse(s)))return\"Value must be a DateTime\"})(s):\"uuid\"===B?(s=>{if(s=s.toString().toLowerCase(),!/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/.test(s))return\"Value must be a Guid\"})(s):(s=>{if(s&&\"string\"!=typeof s)return\"Value must be a string\"})(s),!o)return _;_.push(o)}else if(\"boolean\"===L){let o=(s=>{if(\"true\"!==s&&\"false\"!==s&&!0!==s&&!1!==s)return\"Value must be a boolean\"})(s);if(!o)return _;_.push(o)}else if(\"number\"===L){let o=(s=>{if(!/^-?\\d+(\\.?\\d+)?$/.test(s))return\"Value must be a number\"})(s);if(!o)return _;_.push(o)}else if(\"integer\"===L){let o=(s=>{if(!/^-?\\d+$/.test(s))return\"Value must be an integer\"})(s);if(!o)return _;_.push(o)}else if(\"array\"===L){if(!pe&&!de)return _;s&&s.forEach(((s,i)=>{const w=validateValueBySchema(s,o.get(\"items\"),!1,a,u);_.push(...w.map((s=>({index:i,error:s}))))}))}else if(\"file\"===L){let o=(s=>{if(s&&!(s instanceof lt.File))return\"Value must be a file\"})(s);if(!o)return _;_.push(o)}return _}const utils_btoa=s=>{let o;return o=s instanceof Ct?s:Ct.from(s.toString(),\"utf-8\"),o.toString(\"base64\")},It={operationsSorter:{alpha:(s,o)=>s.get(\"path\").localeCompare(o.get(\"path\")),method:(s,o)=>s.get(\"method\").localeCompare(o.get(\"method\"))},tagsSorter:{alpha:(s,o)=>s.localeCompare(o)}},buildFormData=s=>{let o=[];for(let i in s){let a=s[i];void 0!==a&&\"\"!==a&&o.push([i,\"=\",encodeURIComponent(a).replace(/%20/g,\"+\")].join(\"\"))}return o.join(\"&\")},shallowEqualKeys=(s,o,i)=>!!dt()(i,(i=>vt()(s[i],o[i])));function requiresValidationURL(s){return!(!s||s.indexOf(\"localhost\")>=0||s.indexOf(\"127.0.0.1\")>=0||\"none\"===s)}const createDeepLinkPath=s=>\"string\"==typeof s||s instanceof String?s.trim().replace(/\\s/g,\"%20\"):\"\",escapeDeepLinkPath=s=>Et()(createDeepLinkPath(s).replace(/%20/g,\"_\")),isExtension=s=>/^x-/.test(s),getExtensions=s=>ze.Map.isMap(s)?s.filter(((s,o)=>isExtension(o))):Object.keys(s).filter((s=>isExtension(s))),getCommonExtensions=s=>s.filter(((s,o)=>/^pattern|maxLength|minLength|maximum|minimum/.test(o)));function deeplyStripKey(s,o,i=()=>!0){if(\"object\"!=typeof s||Array.isArray(s)||null===s||!o)return s;const a=Object.assign({},s);return Object.keys(a).forEach((s=>{s===o&&i(a[s],s)?delete a[s]:a[s]=deeplyStripKey(a[s],o,i)})),a}function stringify(s){if(\"string\"==typeof s)return s;if(s&&s.toJS&&(s=s.toJS()),\"object\"==typeof s&&null!==s)try{return JSON.stringify(s,null,2)}catch(o){return String(s)}return null==s?\"\":s.toString()}function paramToIdentifier(s,{returnAll:o=!1,allowHashes:i=!0}={}){if(!We().Map.isMap(s))throw new Error(\"paramToIdentifier: received a non-Im.Map parameter as input\");const a=s.get(\"name\"),u=s.get(\"in\");let _=[];return s&&s.hashCode&&u&&a&&i&&_.push(`${u}.${a}.hash-${s.hashCode()}`),u&&a&&_.push(`${u}.${a}`),_.push(a),o?_:_[0]||\"\"}function paramToValue(s,o){return paramToIdentifier(s,{returnAll:!0}).map((s=>o[s])).filter((s=>void 0!==s))[0]}function b64toB64UrlEncoded(s){return s.replace(/\\+/g,\"-\").replace(/\\//g,\"_\").replace(/=/g,\"\")}const isEmptyValue=s=>!s||!(!isImmutable(s)||!s.isEmpty()),idFn=s=>s;function createStoreWithMiddleware(s,o,i){let a=[systemThunkMiddleware(i)];return createStore(s,o,(lt.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||compose)(function applyMiddleware(...s){return o=>(i,a)=>{const u=o(i,a);let dispatch=()=>{throw new Error(formatProdErrorMessage(15))};const _={getState:u.getState,dispatch:(s,...o)=>dispatch(s,...o)},w=s.map((s=>s(_)));return dispatch=compose(...w)(u.dispatch),{...u,dispatch}}}(...a)))}class Store{constructor(s={}){Ye()(this,{state:{},plugins:[],system:{configs:{},fn:{},components:{},rootInjects:{},statePlugins:{}},boundSystem:{},toolbox:{}},s),this.getSystem=this._getSystem.bind(this),this.store=function configureStore(s,o,i){return createStoreWithMiddleware(s,o,i)}(idFn,(0,ze.fromJS)(this.state),this.getSystem),this.buildSystem(!1),this.register(this.plugins)}getStore(){return this.store}register(s,o=!0){var i=combinePlugins(s,this.getSystem());systemExtend(this.system,i),o&&this.buildSystem();callAfterLoad.call(this.system,s,this.getSystem())&&this.buildSystem()}buildSystem(s=!0){let o=this.getStore().dispatch,i=this.getStore().getState;this.boundSystem=Object.assign({},this.getRootInjects(),this.getWrappedAndBoundActions(o),this.getWrappedAndBoundSelectors(i,this.getSystem),this.getStateThunks(i),this.getFn(),this.getConfigs()),s&&this.rebuildReducer()}_getSystem(){return this.boundSystem}getRootInjects(){return Object.assign({getSystem:this.getSystem,getStore:this.getStore.bind(this),getComponents:this.getComponents.bind(this),getState:this.getStore().getState,getConfigs:this._getConfigs.bind(this),Im:We(),React:Re},this.system.rootInjects||{})}_getConfigs(){return this.system.configs}getConfigs(){return{configs:this.system.configs}}setConfigs(s){this.system.configs=s}rebuildReducer(){this.store.replaceReducer(function buildReducer(s,o){return function allReducers(s,o){let i=Object.keys(s).reduce(((i,a)=>(i[a]=function makeReducer(s,o){return(i=new ze.Map,a)=>{if(!s)return i;let u=s[a.type];if(u){const s=wrapWithTryCatch(u,o)(i,a);return null===s?i:s}return i}}(s[a],o),i)),{});if(!Object.keys(i).length)return idFn;return(0,Xe.H)(i)}(objMap(s,(s=>s.reducers)),o)}(this.system.statePlugins,this.getSystem))}getType(s){let o=s[0].toUpperCase()+s.slice(1);return objReduce(this.system.statePlugins,((i,a)=>{let u=i[s];if(u)return{[a+o]:u}}))}getSelectors(){return this.getType(\"selectors\")}getActions(){return objMap(this.getType(\"actions\"),(s=>objReduce(s,((s,o)=>{if(isFn(s))return{[o]:s}}))))}getWrappedAndBoundActions(s){return objMap(this.getBoundActions(s),((s,o)=>{let i=this.system.statePlugins[o.slice(0,-7)].wrapActions;return i?objMap(s,((s,o)=>{let a=i[o];return a?(Array.isArray(a)||(a=[a]),a.reduce(((s,o)=>{let newAction=(...i)=>o(s,this.getSystem())(...i);if(!isFn(newAction))throw new TypeError(\"wrapActions needs to return a function that returns a new function (ie the wrapped action)\");return wrapWithTryCatch(newAction,this.getSystem)}),s||Function.prototype)):s})):s}))}getWrappedAndBoundSelectors(s,o){return objMap(this.getBoundSelectors(s,o),((o,i)=>{let a=[i.slice(0,-9)],u=this.system.statePlugins[a].wrapSelectors;return u?objMap(o,((o,i)=>{let _=u[i];return _?(Array.isArray(_)||(_=[_]),_.reduce(((o,i)=>{let wrappedSelector=(...u)=>i(o,this.getSystem())(s().getIn(a),...u);if(!isFn(wrappedSelector))throw new TypeError(\"wrapSelector needs to return a function that returns a new function (ie the wrapped action)\");return wrappedSelector}),o||Function.prototype)):o})):o}))}getStates(s){return Object.keys(this.system.statePlugins).reduce(((o,i)=>(o[i]=s.get(i),o)),{})}getStateThunks(s){return Object.keys(this.system.statePlugins).reduce(((o,i)=>(o[i]=()=>s().get(i),o)),{})}getFn(){return{fn:this.system.fn}}getComponents(s){const o=this.system.components[s];return Array.isArray(o)?o.reduce(((s,o)=>o(s,this.getSystem()))):void 0!==s?this.system.components[s]:this.system.components}getBoundSelectors(s,o){return objMap(this.getSelectors(),((i,a)=>{let u=[a.slice(0,-9)];return objMap(i,(i=>(...a)=>{let _=wrapWithTryCatch(i,this.getSystem).apply(null,[s().getIn(u),...a]);return\"function\"==typeof _&&(_=wrapWithTryCatch(_,this.getSystem)(o())),_}))}))}getBoundActions(s){s=s||this.getStore().dispatch;const o=this.getActions(),process=s=>\"function\"!=typeof s?objMap(s,(s=>process(s))):(...o)=>{var i=null;try{i=s(...o)}catch(s){i={type:rt,error:!0,payload:(0,Qe.serializeError)(s)}}finally{return i}};return objMap(o,(o=>function bindActionCreators(s,o){if(\"function\"==typeof s)return bindActionCreator(s,o);if(\"object\"!=typeof s||null===s)throw new Error(formatProdErrorMessage(16));const i={};for(const a in s){const u=s[a];\"function\"==typeof u&&(i[a]=bindActionCreator(u,o))}return i}(process(o),s)))}getMapStateToProps(){return()=>Object.assign({},this.getSystem())}getMapDispatchToProps(s){return o=>Ye()({},this.getWrappedAndBoundActions(o),this.getFn(),s)}}function combinePlugins(s,o){return isObject(s)&&!isArray(s)?tt()({},s):isFunc(s)?combinePlugins(s(o),o):isArray(s)?s.map((s=>combinePlugins(s,o))).reduce(systemExtend,{components:o.getComponents()}):{}}function callAfterLoad(s,o,{hasLoaded:i}={}){let a=i;return isObject(s)&&!isArray(s)&&\"function\"==typeof s.afterLoad&&(a=!0,wrapWithTryCatch(s.afterLoad,o.getSystem).call(this,o)),isFunc(s)?callAfterLoad.call(this,s(o),o,{hasLoaded:a}):isArray(s)?s.map((s=>callAfterLoad.call(this,s,o,{hasLoaded:a}))):a}function systemExtend(s={},o={}){if(!isObject(s))return{};if(!isObject(o))return s;o.wrapComponents&&(objMap(o.wrapComponents,((i,a)=>{const u=s.components&&s.components[a];u&&Array.isArray(u)?(s.components[a]=u.concat([i]),delete o.wrapComponents[a]):u&&(s.components[a]=[u,i],delete o.wrapComponents[a])})),Object.keys(o.wrapComponents).length||delete o.wrapComponents);const{statePlugins:i}=s;if(isObject(i))for(let s in i){const a=i[s];if(!isObject(a))continue;const{wrapActions:u,wrapSelectors:_}=a;if(isObject(u))for(let i in u){let a=u[i];Array.isArray(a)||(a=[a],u[i]=a),o&&o.statePlugins&&o.statePlugins[s]&&o.statePlugins[s].wrapActions&&o.statePlugins[s].wrapActions[i]&&(o.statePlugins[s].wrapActions[i]=u[i].concat(o.statePlugins[s].wrapActions[i]))}if(isObject(_))for(let i in _){let a=_[i];Array.isArray(a)||(a=[a],_[i]=a),o&&o.statePlugins&&o.statePlugins[s]&&o.statePlugins[s].wrapSelectors&&o.statePlugins[s].wrapSelectors[i]&&(o.statePlugins[s].wrapSelectors[i]=_[i].concat(o.statePlugins[s].wrapSelectors[i]))}}return Ye()(s,o)}function wrapWithTryCatch(s,o,{logErrors:i=!0}={}){return\"function\"!=typeof s?s:function(...a){try{return s.call(this,...a)}catch(s){if(i){const{uncaughtExceptionHandler:i}=o().getConfigs();\"function\"==typeof i?i(s):console.error(s)}return null}}}var Tt=__webpack_require__(61160),Nt=__webpack_require__.n(Tt);const Mt=\"show_popup\",Rt=\"authorize\",Dt=\"logout\",Lt=\"authorize_oauth2\",Ft=\"configure_auth\",Bt=\"restore_authorization\";function showDefinitions(s){return{type:Mt,payload:s}}function authorize(s){return{type:Rt,payload:s}}const authorizeWithPersistOption=s=>({authActions:o})=>{o.authorize(s),o.persistAuthorizationIfNeeded()};function logout(s){return{type:Dt,payload:s}}const logoutWithPersistOption=s=>({authActions:o})=>{o.logout(s),o.persistAuthorizationIfNeeded()},preAuthorizeImplicit=s=>({authActions:o,errActions:i})=>{let{auth:a,token:u,isValid:_}=s,{schema:w,name:x}=a,C=w.get(\"flow\");delete lt.swaggerUIRedirectOauth2,\"accessCode\"===C||_||i.newAuthErr({authId:x,source:\"auth\",level:\"warning\",message:\"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server\"}),u.error?i.newAuthErr({authId:x,source:\"auth\",level:\"error\",message:JSON.stringify(u)}):o.authorizeOauth2WithPersistOption({auth:a,token:u})};function authorizeOauth2(s){return{type:Lt,payload:s}}const authorizeOauth2WithPersistOption=s=>({authActions:o})=>{o.authorizeOauth2(s),o.persistAuthorizationIfNeeded()},authorizePassword=s=>({authActions:o})=>{let{schema:i,name:a,username:u,password:_,passwordType:w,clientId:x,clientSecret:C}=s,j={grant_type:\"password\",scope:s.scopes.join(\" \"),username:u,password:_},L={};switch(w){case\"request-body\":!function setClientIdAndSecret(s,o,i){o&&Object.assign(s,{client_id:o});i&&Object.assign(s,{client_secret:i})}(j,x,C);break;case\"basic\":L.Authorization=\"Basic \"+utils_btoa(x+\":\"+C);break;default:console.warn(`Warning: invalid passwordType ${w} was passed, not including client id and secret`)}return o.authorizeRequest({body:buildFormData(j),url:i.get(\"tokenUrl\"),name:a,headers:L,query:{},auth:s})};const authorizeApplication=s=>({authActions:o})=>{let{schema:i,scopes:a,name:u,clientId:_,clientSecret:w}=s,x={Authorization:\"Basic \"+utils_btoa(_+\":\"+w)},C={grant_type:\"client_credentials\",scope:a.join(\" \")};return o.authorizeRequest({body:buildFormData(C),name:u,url:i.get(\"tokenUrl\"),auth:s,headers:x})},authorizeAccessCodeWithFormParams=({auth:s,redirectUrl:o})=>({authActions:i})=>{let{schema:a,name:u,clientId:_,clientSecret:w,codeVerifier:x}=s,C={grant_type:\"authorization_code\",code:s.code,client_id:_,client_secret:w,redirect_uri:o,code_verifier:x};return i.authorizeRequest({body:buildFormData(C),name:u,url:a.get(\"tokenUrl\"),auth:s})},authorizeAccessCodeWithBasicAuthentication=({auth:s,redirectUrl:o})=>({authActions:i})=>{let{schema:a,name:u,clientId:_,clientSecret:w,codeVerifier:x}=s,C={Authorization:\"Basic \"+utils_btoa(_+\":\"+w)},j={grant_type:\"authorization_code\",code:s.code,client_id:_,redirect_uri:o,code_verifier:x};return i.authorizeRequest({body:buildFormData(j),name:u,url:a.get(\"tokenUrl\"),auth:s,headers:C})},authorizeRequest=s=>({fn:o,getConfigs:i,authActions:a,errActions:u,oas3Selectors:_,specSelectors:w,authSelectors:x})=>{let C,{body:j,query:L={},headers:B={},name:$,url:U,auth:V}=s,{additionalQueryStringParams:z}=x.getConfigs()||{};if(w.isOAS3()){let s=_.serverEffectiveValue(_.selectedServer());C=Nt()(U,s,!0)}else C=Nt()(U,w.url(),!0);\"object\"==typeof z&&(C.query=Object.assign({},C.query,z));const Y=C.toString();let Z=Object.assign({Accept:\"application/json, text/plain, */*\",\"Content-Type\":\"application/x-www-form-urlencoded\",\"X-Requested-With\":\"XMLHttpRequest\"},B);o.fetch({url:Y,method:\"post\",headers:Z,query:L,body:j,requestInterceptor:i().requestInterceptor,responseInterceptor:i().responseInterceptor}).then((function(s){let o=JSON.parse(s.data),i=o&&(o.error||\"\"),_=o&&(o.parseError||\"\");s.ok?i||_?u.newAuthErr({authId:$,level:\"error\",source:\"auth\",message:JSON.stringify(o)}):a.authorizeOauth2WithPersistOption({auth:V,token:o}):u.newAuthErr({authId:$,level:\"error\",source:\"auth\",message:s.statusText})})).catch((s=>{let o=new Error(s).message;if(s.response&&s.response.data){const i=s.response.data;try{const s=\"string\"==typeof i?JSON.parse(i):i;s.error&&(o+=`, error: ${s.error}`),s.error_description&&(o+=`, description: ${s.error_description}`)}catch(s){}}u.newAuthErr({authId:$,level:\"error\",source:\"auth\",message:o})}))};function configureAuth(s){return{type:Ft,payload:s}}function restoreAuthorization(s){return{type:Bt,payload:s}}const persistAuthorizationIfNeeded=()=>({authSelectors:s,getConfigs:o})=>{if(!o().persistAuthorization)return;const i=s.authorized().toJS();localStorage.setItem(\"authorized\",JSON.stringify(i))},authPopup=(s,o)=>()=>{lt.swaggerUIRedirectOauth2=o,lt.open(s)},$t={[Mt]:(s,{payload:o})=>s.set(\"showDefinitions\",o),[Rt]:(s,{payload:o})=>{let i=(0,ze.fromJS)(o),a=s.get(\"authorized\")||(0,ze.Map)();return i.entrySeq().forEach((([o,i])=>{if(!isFunc(i.getIn))return s.set(\"authorized\",a);let u=i.getIn([\"schema\",\"type\"]);if(\"apiKey\"===u||\"http\"===u)a=a.set(o,i);else if(\"basic\"===u){let s=i.getIn([\"value\",\"username\"]),u=i.getIn([\"value\",\"password\"]);a=a.setIn([o,\"value\"],{username:s,header:\"Basic \"+utils_btoa(s+\":\"+u)}),a=a.setIn([o,\"schema\"],i.get(\"schema\"))}})),s.set(\"authorized\",a)},[Lt]:(s,{payload:o})=>{let i,{auth:a,token:u}=o;a.token=Object.assign({},u),i=(0,ze.fromJS)(a);let _=s.get(\"authorized\")||(0,ze.Map)();return _=_.set(i.get(\"name\"),i),s.set(\"authorized\",_)},[Dt]:(s,{payload:o})=>{let i=s.get(\"authorized\").withMutations((s=>{o.forEach((o=>{s.delete(o)}))}));return s.set(\"authorized\",i)},[Ft]:(s,{payload:o})=>s.set(\"configs\",o),[Bt]:(s,{payload:o})=>s.set(\"authorized\",(0,ze.fromJS)(o.authorized))};function assertIsFunction(s,o=\"expected a function, instead received \"+typeof s){if(\"function\"!=typeof s)throw new TypeError(o)}var ensureIsArray=s=>Array.isArray(s)?s:[s];function getDependencies(s){const o=Array.isArray(s[0])?s[0]:s;return function assertIsArrayOfFunctions(s,o=\"expected all items to be functions, instead received the following types: \"){if(!s.every((s=>\"function\"==typeof s))){const i=s.map((s=>\"function\"==typeof s?`function ${s.name||\"unnamed\"}()`:typeof s)).join(\", \");throw new TypeError(`${o}[${i}]`)}}(o,\"createSelector expects all input-selectors to be functions, but received the following types: \"),o}Symbol(),Object.getPrototypeOf({});var qt=\"undefined\"!=typeof WeakRef?WeakRef:class{constructor(s){this.value=s}deref(){return this.value}};function weakMapMemoize(s,o={}){let i={s:0,v:void 0,o:null,p:null};const{resultEqualityCheck:a}=o;let u,_=0;function memoized(){let o=i;const{length:w}=arguments;for(let s=0,i=w;s<i;s++){const i=arguments[s];if(\"function\"==typeof i||\"object\"==typeof i&&null!==i){let s=o.o;null===s&&(o.o=s=new WeakMap);const a=s.get(i);void 0===a?(o={s:0,v:void 0,o:null,p:null},s.set(i,o)):o=a}else{let s=o.p;null===s&&(o.p=s=new Map);const a=s.get(i);void 0===a?(o={s:0,v:void 0,o:null,p:null},s.set(i,o)):o=a}}const x=o;let C;if(1===o.s)C=o.v;else if(C=s.apply(null,arguments),_++,a){const s=u?.deref?.()??u;null!=s&&a(s,C)&&(C=s,0!==_&&_--);u=\"object\"==typeof C&&null!==C||\"function\"==typeof C?new qt(C):C}return x.s=1,x.v=C,C}return memoized.clearCache=()=>{i={s:0,v:void 0,o:null,p:null},memoized.resetResultsCount()},memoized.resultsCount=()=>_,memoized.resetResultsCount=()=>{_=0},memoized}function createSelectorCreator(s,...o){const i=\"function\"==typeof s?{memoize:s,memoizeOptions:o}:s,createSelector2=(...s)=>{let o,a=0,u=0,_={},w=s.pop();\"object\"==typeof w&&(_=w,w=s.pop()),assertIsFunction(w,`createSelector expects an output function after the inputs, but received: [${typeof w}]`);const x={...i,..._},{memoize:C,memoizeOptions:j=[],argsMemoize:L=weakMapMemoize,argsMemoizeOptions:B=[],devModeChecks:$={}}=x,U=ensureIsArray(j),V=ensureIsArray(B),z=getDependencies(s),Y=C((function recomputationWrapper(){return a++,w.apply(null,arguments)}),...U);const Z=L((function dependenciesChecker(){u++;const s=function collectInputSelectorResults(s,o){const i=[],{length:a}=s;for(let u=0;u<a;u++)i.push(s[u].apply(null,o));return i}(z,arguments);return o=Y.apply(null,s),o}),...V);return Object.assign(Z,{resultFunc:w,memoizedResultFunc:Y,dependencies:z,dependencyRecomputations:()=>u,resetDependencyRecomputations:()=>{u=0},lastResult:()=>o,recomputations:()=>a,resetRecomputations:()=>{a=0},memoize:C,argsMemoize:L})};return Object.assign(createSelector2,{withTypes:()=>createSelector2}),createSelector2}var Ut=createSelectorCreator(weakMapMemoize),Vt=Object.assign(((s,o=Ut)=>{!function assertIsObject(s,o=\"expected an object, instead received \"+typeof s){if(\"object\"!=typeof s)throw new TypeError(o)}(s,\"createStructuredSelector expects first argument to be an object where each property is a selector, instead received a \"+typeof s);const i=Object.keys(s);return o(i.map((o=>s[o])),((...s)=>s.reduce(((s,o,a)=>(s[i[a]]=o,s)),{})))}),{withTypes:()=>Vt});const state=s=>s,zt=Ut(state,(s=>s.get(\"showDefinitions\"))),Wt=Ut(state,(()=>({specSelectors:s})=>{let o=s.securityDefinitions()||(0,ze.Map)({}),i=(0,ze.List)();return o.entrySeq().forEach((([s,o])=>{let a=(0,ze.Map)();a=a.set(s,o),i=i.push(a)})),i})),selectAuthPath=(s,o)=>({specSelectors:s})=>(0,ze.List)(s.isOAS3()?[\"components\",\"securitySchemes\",o]:[\"securityDefinitions\",o]),getDefinitionsByNames=(s,o)=>({specSelectors:s})=>{console.warn(\"WARNING: getDefinitionsByNames is deprecated and will be removed in the next major version.\");let i=s.securityDefinitions(),a=(0,ze.List)();return o.valueSeq().forEach((s=>{let o=(0,ze.Map)();s.entrySeq().forEach((([s,a])=>{let u,_=i.get(s);\"oauth2\"===_.get(\"type\")&&a.size&&(u=_.get(\"scopes\"),u.keySeq().forEach((s=>{a.contains(s)||(u=u.delete(s))})),_=_.set(\"allowedScopes\",u)),o=o.set(s,_)})),a=a.push(o)})),a},definitionsForRequirements=(s,o=(0,ze.List)())=>({authSelectors:s})=>{const i=s.definitionsToAuthorize()||(0,ze.List)();let a=(0,ze.List)();return i.forEach((s=>{let i=o.find((o=>o.get(s.keySeq().first())));i&&(s.forEach(((o,a)=>{if(\"oauth2\"===o.get(\"type\")){const u=i.get(a);let _=o.get(\"scopes\");ze.List.isList(u)&&ze.Map.isMap(_)&&(_.keySeq().forEach((s=>{u.contains(s)||(_=_.delete(s))})),s=s.set(a,o.set(\"scopes\",_)))}})),a=a.push(s))})),a},Jt=Ut(state,(s=>s.get(\"authorized\")||(0,ze.Map)())),isAuthorized=(s,o)=>({authSelectors:s})=>{let i=s.authorized();return ze.List.isList(o)?!!o.toJS().filter((s=>-1===Object.keys(s).map((s=>!!i.get(s))).indexOf(!1))).length:null},Ht=Ut(state,(s=>s.get(\"configs\"))),execute=(s,{authSelectors:o,specSelectors:i})=>({path:a,method:u,operation:_,extras:w})=>{let x={authorized:o.authorized()&&o.authorized().toJS(),definitions:i.securityDefinitions()&&i.securityDefinitions().toJS(),specSecurity:i.security()&&i.security().toJS()};return s({path:a,method:u,operation:_,securities:x,...w})},loaded=(s,o)=>i=>{const{getConfigs:a,authActions:u}=o,_=a();if(s(i),_.persistAuthorization){const s=localStorage.getItem(\"authorized\");s&&u.restoreAuthorization({authorized:JSON.parse(s)})}},wrap_actions_authorize=(s,o)=>i=>{s(i);if(o.getConfigs().persistAuthorization)try{const[{schema:s,value:o}]=Object.values(i),a=(0,ze.fromJS)(s),u=\"apiKey\"===a.get(\"type\"),_=\"cookie\"===a.get(\"in\");u&&_&&(document.cookie=`${a.get(\"name\")}=${o}; SameSite=None; Secure`)}catch(s){console.error(\"Error persisting cookie based apiKey in document.cookie.\",s)}},wrap_actions_logout=(s,o)=>i=>{const a=o.getConfigs(),u=o.authSelectors.authorized();try{a.persistAuthorization&&Array.isArray(i)&&i.forEach((s=>{const o=u.get(s,{}),i=\"apiKey\"===o.getIn([\"schema\",\"type\"]),a=\"cookie\"===o.getIn([\"schema\",\"in\"]);if(i&&a){const s=o.getIn([\"schema\",\"name\"]);document.cookie=`${s}=; Max-Age=-99999999`}}))}catch(s){console.error(\"Error deleting cookie based apiKey from document.cookie.\",s)}s(i)};var Kt=__webpack_require__(90179),Gt=__webpack_require__.n(Kt);class LockAuthIcon extends Re.Component{mapStateToProps(s,o){return{state:s,ownProps:Gt()(o,Object.keys(o.getSystem()))}}render(){const{getComponent:s,ownProps:o}=this.props,i=s(\"LockIcon\");return Re.createElement(i,o)}}const Yt=LockAuthIcon;class UnlockAuthIcon extends Re.Component{mapStateToProps(s,o){return{state:s,ownProps:Gt()(o,Object.keys(o.getSystem()))}}render(){const{getComponent:s,ownProps:o}=this.props,i=s(\"UnlockIcon\");return Re.createElement(i,o)}}const Xt=UnlockAuthIcon;function auth(){return{afterLoad(s){this.rootInjects=this.rootInjects||{},this.rootInjects.initOAuth=s.authActions.configureAuth,this.rootInjects.preauthorizeApiKey=preauthorizeApiKey.bind(null,s),this.rootInjects.preauthorizeBasic=preauthorizeBasic.bind(null,s)},components:{LockAuthIcon:Yt,UnlockAuthIcon:Xt,LockAuthOperationIcon:Yt,UnlockAuthOperationIcon:Xt},statePlugins:{auth:{reducers:$t,actions:o,selectors:a,wrapActions:{authorize:wrap_actions_authorize,logout:wrap_actions_logout}},configs:{wrapActions:{loaded}},spec:{wrapActions:{execute}}}}}function preauthorizeBasic(s,o,i,a){const{authActions:{authorize:u},specSelectors:{specJson:_,isOAS3:w}}=s,x=w()?[\"components\",\"securitySchemes\"]:[\"securityDefinitions\"],C=_().getIn([...x,o]);return C?u({[o]:{value:{username:i,password:a},schema:C.toJS()}}):null}function preauthorizeApiKey(s,o,i){const{authActions:{authorize:a},specSelectors:{specJson:u,isOAS3:_}}=s,w=_()?[\"components\",\"securitySchemes\"]:[\"securityDefinitions\"],x=u().getIn([...w,o]);return x?a({[o]:{value:i,schema:x.toJS()}}):null}function isNothing(s){return null==s}var Qt=function repeat(s,o){var i,a=\"\";for(i=0;i<o;i+=1)a+=s;return a},Zt=function isNegativeZero(s){return 0===s&&Number.NEGATIVE_INFINITY===1/s},er={isNothing,isObject:function js_yaml_isObject(s){return\"object\"==typeof s&&null!==s},toArray:function toArray(s){return Array.isArray(s)?s:isNothing(s)?[]:[s]},repeat:Qt,isNegativeZero:Zt,extend:function extend(s,o){var i,a,u,_;if(o)for(i=0,a=(_=Object.keys(o)).length;i<a;i+=1)s[u=_[i]]=o[u];return s}};function formatError(s,o){var i=\"\",a=s.reason||\"(unknown reason)\";return s.mark?(s.mark.name&&(i+='in \"'+s.mark.name+'\" '),i+=\"(\"+(s.mark.line+1)+\":\"+(s.mark.column+1)+\")\",!o&&s.mark.snippet&&(i+=\"\\n\\n\"+s.mark.snippet),a+\" \"+i):a}function YAMLException$1(s,o){Error.call(this),this.name=\"YAMLException\",this.reason=s,this.mark=o,this.message=formatError(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||\"\"}YAMLException$1.prototype=Object.create(Error.prototype),YAMLException$1.prototype.constructor=YAMLException$1,YAMLException$1.prototype.toString=function toString(s){return this.name+\": \"+formatError(this,s)};var tr=YAMLException$1;function getLine(s,o,i,a,u){var _=\"\",w=\"\",x=Math.floor(u/2)-1;return a-o>x&&(o=a-x+(_=\" ... \").length),i-a>x&&(i=a+x-(w=\" ...\").length),{str:_+s.slice(o,i).replace(/\\t/g,\"→\")+w,pos:a-o+_.length}}function padStart(s,o){return er.repeat(\" \",o-s.length)+s}var rr=function makeSnippet(s,o){if(o=Object.create(o||null),!s.buffer)return null;o.maxLength||(o.maxLength=79),\"number\"!=typeof o.indent&&(o.indent=1),\"number\"!=typeof o.linesBefore&&(o.linesBefore=3),\"number\"!=typeof o.linesAfter&&(o.linesAfter=2);for(var i,a=/\\r?\\n|\\r|\\0/g,u=[0],_=[],w=-1;i=a.exec(s.buffer);)_.push(i.index),u.push(i.index+i[0].length),s.position<=i.index&&w<0&&(w=u.length-2);w<0&&(w=u.length-1);var x,C,j=\"\",L=Math.min(s.line+o.linesAfter,_.length).toString().length,B=o.maxLength-(o.indent+L+3);for(x=1;x<=o.linesBefore&&!(w-x<0);x++)C=getLine(s.buffer,u[w-x],_[w-x],s.position-(u[w]-u[w-x]),B),j=er.repeat(\" \",o.indent)+padStart((s.line-x+1).toString(),L)+\" | \"+C.str+\"\\n\"+j;for(C=getLine(s.buffer,u[w],_[w],s.position,B),j+=er.repeat(\" \",o.indent)+padStart((s.line+1).toString(),L)+\" | \"+C.str+\"\\n\",j+=er.repeat(\"-\",o.indent+L+3+C.pos)+\"^\\n\",x=1;x<=o.linesAfter&&!(w+x>=_.length);x++)C=getLine(s.buffer,u[w+x],_[w+x],s.position-(u[w]-u[w+x]),B),j+=er.repeat(\" \",o.indent)+padStart((s.line+x+1).toString(),L)+\" | \"+C.str+\"\\n\";return j.replace(/\\n$/,\"\")},nr=[\"kind\",\"multi\",\"resolve\",\"construct\",\"instanceOf\",\"predicate\",\"represent\",\"representName\",\"defaultStyle\",\"styleAliases\"],sr=[\"scalar\",\"sequence\",\"mapping\"];var ir=function Type$1(s,o){if(o=o||{},Object.keys(o).forEach((function(o){if(-1===nr.indexOf(o))throw new tr('Unknown option \"'+o+'\" is met in definition of \"'+s+'\" YAML type.')})),this.options=o,this.tag=s,this.kind=o.kind||null,this.resolve=o.resolve||function(){return!0},this.construct=o.construct||function(s){return s},this.instanceOf=o.instanceOf||null,this.predicate=o.predicate||null,this.represent=o.represent||null,this.representName=o.representName||null,this.defaultStyle=o.defaultStyle||null,this.multi=o.multi||!1,this.styleAliases=function compileStyleAliases(s){var o={};return null!==s&&Object.keys(s).forEach((function(i){s[i].forEach((function(s){o[String(s)]=i}))})),o}(o.styleAliases||null),-1===sr.indexOf(this.kind))throw new tr('Unknown kind \"'+this.kind+'\" is specified for \"'+s+'\" YAML type.')};function compileList(s,o){var i=[];return s[o].forEach((function(s){var o=i.length;i.forEach((function(i,a){i.tag===s.tag&&i.kind===s.kind&&i.multi===s.multi&&(o=a)})),i[o]=s})),i}function Schema$1(s){return this.extend(s)}Schema$1.prototype.extend=function extend(s){var o=[],i=[];if(s instanceof ir)i.push(s);else if(Array.isArray(s))i=i.concat(s);else{if(!s||!Array.isArray(s.implicit)&&!Array.isArray(s.explicit))throw new tr(\"Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })\");s.implicit&&(o=o.concat(s.implicit)),s.explicit&&(i=i.concat(s.explicit))}o.forEach((function(s){if(!(s instanceof ir))throw new tr(\"Specified list of YAML types (or a single Type object) contains a non-Type object.\");if(s.loadKind&&\"scalar\"!==s.loadKind)throw new tr(\"There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.\");if(s.multi)throw new tr(\"There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.\")})),i.forEach((function(s){if(!(s instanceof ir))throw new tr(\"Specified list of YAML types (or a single Type object) contains a non-Type object.\")}));var a=Object.create(Schema$1.prototype);return a.implicit=(this.implicit||[]).concat(o),a.explicit=(this.explicit||[]).concat(i),a.compiledImplicit=compileList(a,\"implicit\"),a.compiledExplicit=compileList(a,\"explicit\"),a.compiledTypeMap=function compileMap(){var s,o,i={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function collectType(s){s.multi?(i.multi[s.kind].push(s),i.multi.fallback.push(s)):i[s.kind][s.tag]=i.fallback[s.tag]=s}for(s=0,o=arguments.length;s<o;s+=1)arguments[s].forEach(collectType);return i}(a.compiledImplicit,a.compiledExplicit),a};var ar=Schema$1,cr=new ir(\"tag:yaml.org,2002:str\",{kind:\"scalar\",construct:function(s){return null!==s?s:\"\"}}),lr=new ir(\"tag:yaml.org,2002:seq\",{kind:\"sequence\",construct:function(s){return null!==s?s:[]}}),ur=new ir(\"tag:yaml.org,2002:map\",{kind:\"mapping\",construct:function(s){return null!==s?s:{}}}),pr=new ar({explicit:[cr,lr,ur]});var dr=new ir(\"tag:yaml.org,2002:null\",{kind:\"scalar\",resolve:function resolveYamlNull(s){if(null===s)return!0;var o=s.length;return 1===o&&\"~\"===s||4===o&&(\"null\"===s||\"Null\"===s||\"NULL\"===s)},construct:function constructYamlNull(){return null},predicate:function isNull(s){return null===s},represent:{canonical:function(){return\"~\"},lowercase:function(){return\"null\"},uppercase:function(){return\"NULL\"},camelcase:function(){return\"Null\"},empty:function(){return\"\"}},defaultStyle:\"lowercase\"});var fr=new ir(\"tag:yaml.org,2002:bool\",{kind:\"scalar\",resolve:function resolveYamlBoolean(s){if(null===s)return!1;var o=s.length;return 4===o&&(\"true\"===s||\"True\"===s||\"TRUE\"===s)||5===o&&(\"false\"===s||\"False\"===s||\"FALSE\"===s)},construct:function constructYamlBoolean(s){return\"true\"===s||\"True\"===s||\"TRUE\"===s},predicate:function isBoolean(s){return\"[object Boolean]\"===Object.prototype.toString.call(s)},represent:{lowercase:function(s){return s?\"true\":\"false\"},uppercase:function(s){return s?\"TRUE\":\"FALSE\"},camelcase:function(s){return s?\"True\":\"False\"}},defaultStyle:\"lowercase\"});function isOctCode(s){return 48<=s&&s<=55}function isDecCode(s){return 48<=s&&s<=57}var mr=new ir(\"tag:yaml.org,2002:int\",{kind:\"scalar\",resolve:function resolveYamlInteger(s){if(null===s)return!1;var o,i,a=s.length,u=0,_=!1;if(!a)return!1;if(\"-\"!==(o=s[u])&&\"+\"!==o||(o=s[++u]),\"0\"===o){if(u+1===a)return!0;if(\"b\"===(o=s[++u])){for(u++;u<a;u++)if(\"_\"!==(o=s[u])){if(\"0\"!==o&&\"1\"!==o)return!1;_=!0}return _&&\"_\"!==o}if(\"x\"===o){for(u++;u<a;u++)if(\"_\"!==(o=s[u])){if(!(48<=(i=s.charCodeAt(u))&&i<=57||65<=i&&i<=70||97<=i&&i<=102))return!1;_=!0}return _&&\"_\"!==o}if(\"o\"===o){for(u++;u<a;u++)if(\"_\"!==(o=s[u])){if(!isOctCode(s.charCodeAt(u)))return!1;_=!0}return _&&\"_\"!==o}}if(\"_\"===o)return!1;for(;u<a;u++)if(\"_\"!==(o=s[u])){if(!isDecCode(s.charCodeAt(u)))return!1;_=!0}return!(!_||\"_\"===o)},construct:function constructYamlInteger(s){var o,i=s,a=1;if(-1!==i.indexOf(\"_\")&&(i=i.replace(/_/g,\"\")),\"-\"!==(o=i[0])&&\"+\"!==o||(\"-\"===o&&(a=-1),o=(i=i.slice(1))[0]),\"0\"===i)return 0;if(\"0\"===o){if(\"b\"===i[1])return a*parseInt(i.slice(2),2);if(\"x\"===i[1])return a*parseInt(i.slice(2),16);if(\"o\"===i[1])return a*parseInt(i.slice(2),8)}return a*parseInt(i,10)},predicate:function isInteger(s){return\"[object Number]\"===Object.prototype.toString.call(s)&&s%1==0&&!er.isNegativeZero(s)},represent:{binary:function(s){return s>=0?\"0b\"+s.toString(2):\"-0b\"+s.toString(2).slice(1)},octal:function(s){return s>=0?\"0o\"+s.toString(8):\"-0o\"+s.toString(8).slice(1)},decimal:function(s){return s.toString(10)},hexadecimal:function(s){return s>=0?\"0x\"+s.toString(16).toUpperCase():\"-0x\"+s.toString(16).toUpperCase().slice(1)}},defaultStyle:\"decimal\",styleAliases:{binary:[2,\"bin\"],octal:[8,\"oct\"],decimal:[10,\"dec\"],hexadecimal:[16,\"hex\"]}}),gr=new RegExp(\"^(?:[-+]?(?:[0-9][0-9_]*)(?:\\\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\\\.(?:inf|Inf|INF)|\\\\.(?:nan|NaN|NAN))$\");var yr=/^[-+]?[0-9]+e/;var vr=new ir(\"tag:yaml.org,2002:float\",{kind:\"scalar\",resolve:function resolveYamlFloat(s){return null!==s&&!(!gr.test(s)||\"_\"===s[s.length-1])},construct:function constructYamlFloat(s){var o,i;return i=\"-\"===(o=s.replace(/_/g,\"\").toLowerCase())[0]?-1:1,\"+-\".indexOf(o[0])>=0&&(o=o.slice(1)),\".inf\"===o?1===i?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:\".nan\"===o?NaN:i*parseFloat(o,10)},predicate:function isFloat(s){return\"[object Number]\"===Object.prototype.toString.call(s)&&(s%1!=0||er.isNegativeZero(s))},represent:function representYamlFloat(s,o){var i;if(isNaN(s))switch(o){case\"lowercase\":return\".nan\";case\"uppercase\":return\".NAN\";case\"camelcase\":return\".NaN\"}else if(Number.POSITIVE_INFINITY===s)switch(o){case\"lowercase\":return\".inf\";case\"uppercase\":return\".INF\";case\"camelcase\":return\".Inf\"}else if(Number.NEGATIVE_INFINITY===s)switch(o){case\"lowercase\":return\"-.inf\";case\"uppercase\":return\"-.INF\";case\"camelcase\":return\"-.Inf\"}else if(er.isNegativeZero(s))return\"-0.0\";return i=s.toString(10),yr.test(i)?i.replace(\"e\",\".e\"):i},defaultStyle:\"lowercase\"}),br=pr.extend({implicit:[dr,fr,mr,vr]}),_r=br,Sr=new RegExp(\"^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$\"),Er=new RegExp(\"^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\\\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\\\.([0-9]*))?(?:[ \\\\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$\");var wr=new ir(\"tag:yaml.org,2002:timestamp\",{kind:\"scalar\",resolve:function resolveYamlTimestamp(s){return null!==s&&(null!==Sr.exec(s)||null!==Er.exec(s))},construct:function constructYamlTimestamp(s){var o,i,a,u,_,w,x,C,j=0,L=null;if(null===(o=Sr.exec(s))&&(o=Er.exec(s)),null===o)throw new Error(\"Date resolve error\");if(i=+o[1],a=+o[2]-1,u=+o[3],!o[4])return new Date(Date.UTC(i,a,u));if(_=+o[4],w=+o[5],x=+o[6],o[7]){for(j=o[7].slice(0,3);j.length<3;)j+=\"0\";j=+j}return o[9]&&(L=6e4*(60*+o[10]+ +(o[11]||0)),\"-\"===o[9]&&(L=-L)),C=new Date(Date.UTC(i,a,u,_,w,x,j)),L&&C.setTime(C.getTime()-L),C},instanceOf:Date,represent:function representYamlTimestamp(s){return s.toISOString()}});var xr=new ir(\"tag:yaml.org,2002:merge\",{kind:\"scalar\",resolve:function resolveYamlMerge(s){return\"<<\"===s||null===s}}),kr=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\\n\\r\";var Or=new ir(\"tag:yaml.org,2002:binary\",{kind:\"scalar\",resolve:function resolveYamlBinary(s){if(null===s)return!1;var o,i,a=0,u=s.length,_=kr;for(i=0;i<u;i++)if(!((o=_.indexOf(s.charAt(i)))>64)){if(o<0)return!1;a+=6}return a%8==0},construct:function constructYamlBinary(s){var o,i,a=s.replace(/[\\r\\n=]/g,\"\"),u=a.length,_=kr,w=0,x=[];for(o=0;o<u;o++)o%4==0&&o&&(x.push(w>>16&255),x.push(w>>8&255),x.push(255&w)),w=w<<6|_.indexOf(a.charAt(o));return 0===(i=u%4*6)?(x.push(w>>16&255),x.push(w>>8&255),x.push(255&w)):18===i?(x.push(w>>10&255),x.push(w>>2&255)):12===i&&x.push(w>>4&255),new Uint8Array(x)},predicate:function isBinary(s){return\"[object Uint8Array]\"===Object.prototype.toString.call(s)},represent:function representYamlBinary(s){var o,i,a=\"\",u=0,_=s.length,w=kr;for(o=0;o<_;o++)o%3==0&&o&&(a+=w[u>>18&63],a+=w[u>>12&63],a+=w[u>>6&63],a+=w[63&u]),u=(u<<8)+s[o];return 0===(i=_%3)?(a+=w[u>>18&63],a+=w[u>>12&63],a+=w[u>>6&63],a+=w[63&u]):2===i?(a+=w[u>>10&63],a+=w[u>>4&63],a+=w[u<<2&63],a+=w[64]):1===i&&(a+=w[u>>2&63],a+=w[u<<4&63],a+=w[64],a+=w[64]),a}}),Ar=Object.prototype.hasOwnProperty,Cr=Object.prototype.toString;var jr=new ir(\"tag:yaml.org,2002:omap\",{kind:\"sequence\",resolve:function resolveYamlOmap(s){if(null===s)return!0;var o,i,a,u,_,w=[],x=s;for(o=0,i=x.length;o<i;o+=1){if(a=x[o],_=!1,\"[object Object]\"!==Cr.call(a))return!1;for(u in a)if(Ar.call(a,u)){if(_)return!1;_=!0}if(!_)return!1;if(-1!==w.indexOf(u))return!1;w.push(u)}return!0},construct:function constructYamlOmap(s){return null!==s?s:[]}}),Pr=Object.prototype.toString;var Ir=new ir(\"tag:yaml.org,2002:pairs\",{kind:\"sequence\",resolve:function resolveYamlPairs(s){if(null===s)return!0;var o,i,a,u,_,w=s;for(_=new Array(w.length),o=0,i=w.length;o<i;o+=1){if(a=w[o],\"[object Object]\"!==Pr.call(a))return!1;if(1!==(u=Object.keys(a)).length)return!1;_[o]=[u[0],a[u[0]]]}return!0},construct:function constructYamlPairs(s){if(null===s)return[];var o,i,a,u,_,w=s;for(_=new Array(w.length),o=0,i=w.length;o<i;o+=1)a=w[o],u=Object.keys(a),_[o]=[u[0],a[u[0]]];return _}}),Tr=Object.prototype.hasOwnProperty;var Nr=new ir(\"tag:yaml.org,2002:set\",{kind:\"mapping\",resolve:function resolveYamlSet(s){if(null===s)return!0;var o,i=s;for(o in i)if(Tr.call(i,o)&&null!==i[o])return!1;return!0},construct:function constructYamlSet(s){return null!==s?s:{}}}),Mr=_r.extend({implicit:[wr,xr],explicit:[Or,jr,Ir,Nr]}),Rr=Object.prototype.hasOwnProperty,Dr=/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F-\\x84\\x86-\\x9F\\uFFFE\\uFFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]/,Lr=/[\\x85\\u2028\\u2029]/,Fr=/[,\\[\\]\\{\\}]/,Br=/^(?:!|!!|![a-z\\-]+!)$/i,$r=/^(?:!|[^,\\[\\]\\{\\}])(?:%[0-9a-f]{2}|[0-9a-z\\-#;\\/\\?:@&=\\+\\$,_\\.!~\\*'\\(\\)\\[\\]])*$/i;function _class(s){return Object.prototype.toString.call(s)}function is_EOL(s){return 10===s||13===s}function is_WHITE_SPACE(s){return 9===s||32===s}function is_WS_OR_EOL(s){return 9===s||32===s||10===s||13===s}function is_FLOW_INDICATOR(s){return 44===s||91===s||93===s||123===s||125===s}function fromHexCode(s){var o;return 48<=s&&s<=57?s-48:97<=(o=32|s)&&o<=102?o-97+10:-1}function simpleEscapeSequence(s){return 48===s?\"\\0\":97===s?\"\u0007\":98===s?\"\\b\":116===s||9===s?\"\\t\":110===s?\"\\n\":118===s?\"\\v\":102===s?\"\\f\":114===s?\"\\r\":101===s?\"\u001b\":32===s?\" \":34===s?'\"':47===s?\"/\":92===s?\"\\\\\":78===s?\"\":95===s?\" \":76===s?\"\\u2028\":80===s?\"\\u2029\":\"\"}function charFromCodepoint(s){return s<=65535?String.fromCharCode(s):String.fromCharCode(55296+(s-65536>>10),56320+(s-65536&1023))}function setProperty(s,o,i){\"__proto__\"===o?Object.defineProperty(s,o,{configurable:!0,enumerable:!0,writable:!0,value:i}):s[o]=i}for(var qr=new Array(256),Ur=new Array(256),Vr=0;Vr<256;Vr++)qr[Vr]=simpleEscapeSequence(Vr)?1:0,Ur[Vr]=simpleEscapeSequence(Vr);function State$1(s,o){this.input=s,this.filename=o.filename||null,this.schema=o.schema||Mr,this.onWarning=o.onWarning||null,this.legacy=o.legacy||!1,this.json=o.json||!1,this.listener=o.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=s.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function generateError(s,o){var i={name:s.filename,buffer:s.input.slice(0,-1),position:s.position,line:s.line,column:s.position-s.lineStart};return i.snippet=rr(i),new tr(o,i)}function throwError(s,o){throw generateError(s,o)}function throwWarning(s,o){s.onWarning&&s.onWarning.call(null,generateError(s,o))}var zr={YAML:function handleYamlDirective(s,o,i){var a,u,_;null!==s.version&&throwError(s,\"duplication of %YAML directive\"),1!==i.length&&throwError(s,\"YAML directive accepts exactly one argument\"),null===(a=/^([0-9]+)\\.([0-9]+)$/.exec(i[0]))&&throwError(s,\"ill-formed argument of the YAML directive\"),u=parseInt(a[1],10),_=parseInt(a[2],10),1!==u&&throwError(s,\"unacceptable YAML version of the document\"),s.version=i[0],s.checkLineBreaks=_<2,1!==_&&2!==_&&throwWarning(s,\"unsupported YAML version of the document\")},TAG:function handleTagDirective(s,o,i){var a,u;2!==i.length&&throwError(s,\"TAG directive accepts exactly two arguments\"),a=i[0],u=i[1],Br.test(a)||throwError(s,\"ill-formed tag handle (first argument) of the TAG directive\"),Rr.call(s.tagMap,a)&&throwError(s,'there is a previously declared suffix for \"'+a+'\" tag handle'),$r.test(u)||throwError(s,\"ill-formed tag prefix (second argument) of the TAG directive\");try{u=decodeURIComponent(u)}catch(o){throwError(s,\"tag prefix is malformed: \"+u)}s.tagMap[a]=u}};function captureSegment(s,o,i,a){var u,_,w,x;if(o<i){if(x=s.input.slice(o,i),a)for(u=0,_=x.length;u<_;u+=1)9===(w=x.charCodeAt(u))||32<=w&&w<=1114111||throwError(s,\"expected valid JSON character\");else Dr.test(x)&&throwError(s,\"the stream contains non-printable characters\");s.result+=x}}function mergeMappings(s,o,i,a){var u,_,w,x;for(er.isObject(i)||throwError(s,\"cannot merge mappings; the provided source object is unacceptable\"),w=0,x=(u=Object.keys(i)).length;w<x;w+=1)_=u[w],Rr.call(o,_)||(setProperty(o,_,i[_]),a[_]=!0)}function storeMappingPair(s,o,i,a,u,_,w,x,C){var j,L;if(Array.isArray(u))for(j=0,L=(u=Array.prototype.slice.call(u)).length;j<L;j+=1)Array.isArray(u[j])&&throwError(s,\"nested arrays are not supported inside keys\"),\"object\"==typeof u&&\"[object Object]\"===_class(u[j])&&(u[j]=\"[object Object]\");if(\"object\"==typeof u&&\"[object Object]\"===_class(u)&&(u=\"[object Object]\"),u=String(u),null===o&&(o={}),\"tag:yaml.org,2002:merge\"===a)if(Array.isArray(_))for(j=0,L=_.length;j<L;j+=1)mergeMappings(s,o,_[j],i);else mergeMappings(s,o,_,i);else s.json||Rr.call(i,u)||!Rr.call(o,u)||(s.line=w||s.line,s.lineStart=x||s.lineStart,s.position=C||s.position,throwError(s,\"duplicated mapping key\")),setProperty(o,u,_),delete i[u];return o}function readLineBreak(s){var o;10===(o=s.input.charCodeAt(s.position))?s.position++:13===o?(s.position++,10===s.input.charCodeAt(s.position)&&s.position++):throwError(s,\"a line break is expected\"),s.line+=1,s.lineStart=s.position,s.firstTabInLine=-1}function skipSeparationSpace(s,o,i){for(var a=0,u=s.input.charCodeAt(s.position);0!==u;){for(;is_WHITE_SPACE(u);)9===u&&-1===s.firstTabInLine&&(s.firstTabInLine=s.position),u=s.input.charCodeAt(++s.position);if(o&&35===u)do{u=s.input.charCodeAt(++s.position)}while(10!==u&&13!==u&&0!==u);if(!is_EOL(u))break;for(readLineBreak(s),u=s.input.charCodeAt(s.position),a++,s.lineIndent=0;32===u;)s.lineIndent++,u=s.input.charCodeAt(++s.position)}return-1!==i&&0!==a&&s.lineIndent<i&&throwWarning(s,\"deficient indentation\"),a}function testDocumentSeparator(s){var o,i=s.position;return!(45!==(o=s.input.charCodeAt(i))&&46!==o||o!==s.input.charCodeAt(i+1)||o!==s.input.charCodeAt(i+2)||(i+=3,0!==(o=s.input.charCodeAt(i))&&!is_WS_OR_EOL(o)))}function writeFoldedLines(s,o){1===o?s.result+=\" \":o>1&&(s.result+=er.repeat(\"\\n\",o-1))}function readBlockSequence(s,o){var i,a,u=s.tag,_=s.anchor,w=[],x=!1;if(-1!==s.firstTabInLine)return!1;for(null!==s.anchor&&(s.anchorMap[s.anchor]=w),a=s.input.charCodeAt(s.position);0!==a&&(-1!==s.firstTabInLine&&(s.position=s.firstTabInLine,throwError(s,\"tab characters must not be used in indentation\")),45===a)&&is_WS_OR_EOL(s.input.charCodeAt(s.position+1));)if(x=!0,s.position++,skipSeparationSpace(s,!0,-1)&&s.lineIndent<=o)w.push(null),a=s.input.charCodeAt(s.position);else if(i=s.line,composeNode(s,o,3,!1,!0),w.push(s.result),skipSeparationSpace(s,!0,-1),a=s.input.charCodeAt(s.position),(s.line===i||s.lineIndent>o)&&0!==a)throwError(s,\"bad indentation of a sequence entry\");else if(s.lineIndent<o)break;return!!x&&(s.tag=u,s.anchor=_,s.kind=\"sequence\",s.result=w,!0)}function readTagProperty(s){var o,i,a,u,_=!1,w=!1;if(33!==(u=s.input.charCodeAt(s.position)))return!1;if(null!==s.tag&&throwError(s,\"duplication of a tag property\"),60===(u=s.input.charCodeAt(++s.position))?(_=!0,u=s.input.charCodeAt(++s.position)):33===u?(w=!0,i=\"!!\",u=s.input.charCodeAt(++s.position)):i=\"!\",o=s.position,_){do{u=s.input.charCodeAt(++s.position)}while(0!==u&&62!==u);s.position<s.length?(a=s.input.slice(o,s.position),u=s.input.charCodeAt(++s.position)):throwError(s,\"unexpected end of the stream within a verbatim tag\")}else{for(;0!==u&&!is_WS_OR_EOL(u);)33===u&&(w?throwError(s,\"tag suffix cannot contain exclamation marks\"):(i=s.input.slice(o-1,s.position+1),Br.test(i)||throwError(s,\"named tag handle cannot contain such characters\"),w=!0,o=s.position+1)),u=s.input.charCodeAt(++s.position);a=s.input.slice(o,s.position),Fr.test(a)&&throwError(s,\"tag suffix cannot contain flow indicator characters\")}a&&!$r.test(a)&&throwError(s,\"tag name cannot contain such characters: \"+a);try{a=decodeURIComponent(a)}catch(o){throwError(s,\"tag name is malformed: \"+a)}return _?s.tag=a:Rr.call(s.tagMap,i)?s.tag=s.tagMap[i]+a:\"!\"===i?s.tag=\"!\"+a:\"!!\"===i?s.tag=\"tag:yaml.org,2002:\"+a:throwError(s,'undeclared tag handle \"'+i+'\"'),!0}function readAnchorProperty(s){var o,i;if(38!==(i=s.input.charCodeAt(s.position)))return!1;for(null!==s.anchor&&throwError(s,\"duplication of an anchor property\"),i=s.input.charCodeAt(++s.position),o=s.position;0!==i&&!is_WS_OR_EOL(i)&&!is_FLOW_INDICATOR(i);)i=s.input.charCodeAt(++s.position);return s.position===o&&throwError(s,\"name of an anchor node must contain at least one character\"),s.anchor=s.input.slice(o,s.position),!0}function composeNode(s,o,i,a,u){var _,w,x,C,j,L,B,$,U,V=1,z=!1,Y=!1;if(null!==s.listener&&s.listener(\"open\",s),s.tag=null,s.anchor=null,s.kind=null,s.result=null,_=w=x=4===i||3===i,a&&skipSeparationSpace(s,!0,-1)&&(z=!0,s.lineIndent>o?V=1:s.lineIndent===o?V=0:s.lineIndent<o&&(V=-1)),1===V)for(;readTagProperty(s)||readAnchorProperty(s);)skipSeparationSpace(s,!0,-1)?(z=!0,x=_,s.lineIndent>o?V=1:s.lineIndent===o?V=0:s.lineIndent<o&&(V=-1)):x=!1;if(x&&(x=z||u),1!==V&&4!==i||($=1===i||2===i?o:o+1,U=s.position-s.lineStart,1===V?x&&(readBlockSequence(s,U)||function readBlockMapping(s,o,i){var a,u,_,w,x,C,j,L=s.tag,B=s.anchor,$={},U=Object.create(null),V=null,z=null,Y=null,Z=!1,ee=!1;if(-1!==s.firstTabInLine)return!1;for(null!==s.anchor&&(s.anchorMap[s.anchor]=$),j=s.input.charCodeAt(s.position);0!==j;){if(Z||-1===s.firstTabInLine||(s.position=s.firstTabInLine,throwError(s,\"tab characters must not be used in indentation\")),a=s.input.charCodeAt(s.position+1),_=s.line,63!==j&&58!==j||!is_WS_OR_EOL(a)){if(w=s.line,x=s.lineStart,C=s.position,!composeNode(s,i,2,!1,!0))break;if(s.line===_){for(j=s.input.charCodeAt(s.position);is_WHITE_SPACE(j);)j=s.input.charCodeAt(++s.position);if(58===j)is_WS_OR_EOL(j=s.input.charCodeAt(++s.position))||throwError(s,\"a whitespace character is expected after the key-value separator within a block mapping\"),Z&&(storeMappingPair(s,$,U,V,z,null,w,x,C),V=z=Y=null),ee=!0,Z=!1,u=!1,V=s.tag,z=s.result;else{if(!ee)return s.tag=L,s.anchor=B,!0;throwError(s,\"can not read an implicit mapping pair; a colon is missed\")}}else{if(!ee)return s.tag=L,s.anchor=B,!0;throwError(s,\"can not read a block mapping entry; a multiline key may not be an implicit key\")}}else 63===j?(Z&&(storeMappingPair(s,$,U,V,z,null,w,x,C),V=z=Y=null),ee=!0,Z=!0,u=!0):Z?(Z=!1,u=!0):throwError(s,\"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line\"),s.position+=1,j=a;if((s.line===_||s.lineIndent>o)&&(Z&&(w=s.line,x=s.lineStart,C=s.position),composeNode(s,o,4,!0,u)&&(Z?z=s.result:Y=s.result),Z||(storeMappingPair(s,$,U,V,z,Y,w,x,C),V=z=Y=null),skipSeparationSpace(s,!0,-1),j=s.input.charCodeAt(s.position)),(s.line===_||s.lineIndent>o)&&0!==j)throwError(s,\"bad indentation of a mapping entry\");else if(s.lineIndent<o)break}return Z&&storeMappingPair(s,$,U,V,z,null,w,x,C),ee&&(s.tag=L,s.anchor=B,s.kind=\"mapping\",s.result=$),ee}(s,U,$))||function readFlowCollection(s,o){var i,a,u,_,w,x,C,j,L,B,$,U,V=!0,z=s.tag,Y=s.anchor,Z=Object.create(null);if(91===(U=s.input.charCodeAt(s.position)))w=93,j=!1,_=[];else{if(123!==U)return!1;w=125,j=!0,_={}}for(null!==s.anchor&&(s.anchorMap[s.anchor]=_),U=s.input.charCodeAt(++s.position);0!==U;){if(skipSeparationSpace(s,!0,o),(U=s.input.charCodeAt(s.position))===w)return s.position++,s.tag=z,s.anchor=Y,s.kind=j?\"mapping\":\"sequence\",s.result=_,!0;V?44===U&&throwError(s,\"expected the node content, but found ','\"):throwError(s,\"missed comma between flow collection entries\"),$=null,x=C=!1,63===U&&is_WS_OR_EOL(s.input.charCodeAt(s.position+1))&&(x=C=!0,s.position++,skipSeparationSpace(s,!0,o)),i=s.line,a=s.lineStart,u=s.position,composeNode(s,o,1,!1,!0),B=s.tag,L=s.result,skipSeparationSpace(s,!0,o),U=s.input.charCodeAt(s.position),!C&&s.line!==i||58!==U||(x=!0,U=s.input.charCodeAt(++s.position),skipSeparationSpace(s,!0,o),composeNode(s,o,1,!1,!0),$=s.result),j?storeMappingPair(s,_,Z,B,L,$,i,a,u):x?_.push(storeMappingPair(s,null,Z,B,L,$,i,a,u)):_.push(L),skipSeparationSpace(s,!0,o),44===(U=s.input.charCodeAt(s.position))?(V=!0,U=s.input.charCodeAt(++s.position)):V=!1}throwError(s,\"unexpected end of the stream within a flow collection\")}(s,$)?Y=!0:(w&&function readBlockScalar(s,o){var i,a,u,_,w,x=1,C=!1,j=!1,L=o,B=0,$=!1;if(124===(_=s.input.charCodeAt(s.position)))a=!1;else{if(62!==_)return!1;a=!0}for(s.kind=\"scalar\",s.result=\"\";0!==_;)if(43===(_=s.input.charCodeAt(++s.position))||45===_)1===x?x=43===_?3:2:throwError(s,\"repeat of a chomping mode identifier\");else{if(!((u=48<=(w=_)&&w<=57?w-48:-1)>=0))break;0===u?throwError(s,\"bad explicit indentation width of a block scalar; it cannot be less than one\"):j?throwError(s,\"repeat of an indentation width identifier\"):(L=o+u-1,j=!0)}if(is_WHITE_SPACE(_)){do{_=s.input.charCodeAt(++s.position)}while(is_WHITE_SPACE(_));if(35===_)do{_=s.input.charCodeAt(++s.position)}while(!is_EOL(_)&&0!==_)}for(;0!==_;){for(readLineBreak(s),s.lineIndent=0,_=s.input.charCodeAt(s.position);(!j||s.lineIndent<L)&&32===_;)s.lineIndent++,_=s.input.charCodeAt(++s.position);if(!j&&s.lineIndent>L&&(L=s.lineIndent),is_EOL(_))B++;else{if(s.lineIndent<L){3===x?s.result+=er.repeat(\"\\n\",C?1+B:B):1===x&&C&&(s.result+=\"\\n\");break}for(a?is_WHITE_SPACE(_)?($=!0,s.result+=er.repeat(\"\\n\",C?1+B:B)):$?($=!1,s.result+=er.repeat(\"\\n\",B+1)):0===B?C&&(s.result+=\" \"):s.result+=er.repeat(\"\\n\",B):s.result+=er.repeat(\"\\n\",C?1+B:B),C=!0,j=!0,B=0,i=s.position;!is_EOL(_)&&0!==_;)_=s.input.charCodeAt(++s.position);captureSegment(s,i,s.position,!1)}}return!0}(s,$)||function readSingleQuotedScalar(s,o){var i,a,u;if(39!==(i=s.input.charCodeAt(s.position)))return!1;for(s.kind=\"scalar\",s.result=\"\",s.position++,a=u=s.position;0!==(i=s.input.charCodeAt(s.position));)if(39===i){if(captureSegment(s,a,s.position,!0),39!==(i=s.input.charCodeAt(++s.position)))return!0;a=s.position,s.position++,u=s.position}else is_EOL(i)?(captureSegment(s,a,u,!0),writeFoldedLines(s,skipSeparationSpace(s,!1,o)),a=u=s.position):s.position===s.lineStart&&testDocumentSeparator(s)?throwError(s,\"unexpected end of the document within a single quoted scalar\"):(s.position++,u=s.position);throwError(s,\"unexpected end of the stream within a single quoted scalar\")}(s,$)||function readDoubleQuotedScalar(s,o){var i,a,u,_,w,x,C;if(34!==(x=s.input.charCodeAt(s.position)))return!1;for(s.kind=\"scalar\",s.result=\"\",s.position++,i=a=s.position;0!==(x=s.input.charCodeAt(s.position));){if(34===x)return captureSegment(s,i,s.position,!0),s.position++,!0;if(92===x){if(captureSegment(s,i,s.position,!0),is_EOL(x=s.input.charCodeAt(++s.position)))skipSeparationSpace(s,!1,o);else if(x<256&&qr[x])s.result+=Ur[x],s.position++;else if((w=120===(C=x)?2:117===C?4:85===C?8:0)>0){for(u=w,_=0;u>0;u--)(w=fromHexCode(x=s.input.charCodeAt(++s.position)))>=0?_=(_<<4)+w:throwError(s,\"expected hexadecimal character\");s.result+=charFromCodepoint(_),s.position++}else throwError(s,\"unknown escape sequence\");i=a=s.position}else is_EOL(x)?(captureSegment(s,i,a,!0),writeFoldedLines(s,skipSeparationSpace(s,!1,o)),i=a=s.position):s.position===s.lineStart&&testDocumentSeparator(s)?throwError(s,\"unexpected end of the document within a double quoted scalar\"):(s.position++,a=s.position)}throwError(s,\"unexpected end of the stream within a double quoted scalar\")}(s,$)?Y=!0:!function readAlias(s){var o,i,a;if(42!==(a=s.input.charCodeAt(s.position)))return!1;for(a=s.input.charCodeAt(++s.position),o=s.position;0!==a&&!is_WS_OR_EOL(a)&&!is_FLOW_INDICATOR(a);)a=s.input.charCodeAt(++s.position);return s.position===o&&throwError(s,\"name of an alias node must contain at least one character\"),i=s.input.slice(o,s.position),Rr.call(s.anchorMap,i)||throwError(s,'unidentified alias \"'+i+'\"'),s.result=s.anchorMap[i],skipSeparationSpace(s,!0,-1),!0}(s)?function readPlainScalar(s,o,i){var a,u,_,w,x,C,j,L,B=s.kind,$=s.result;if(is_WS_OR_EOL(L=s.input.charCodeAt(s.position))||is_FLOW_INDICATOR(L)||35===L||38===L||42===L||33===L||124===L||62===L||39===L||34===L||37===L||64===L||96===L)return!1;if((63===L||45===L)&&(is_WS_OR_EOL(a=s.input.charCodeAt(s.position+1))||i&&is_FLOW_INDICATOR(a)))return!1;for(s.kind=\"scalar\",s.result=\"\",u=_=s.position,w=!1;0!==L;){if(58===L){if(is_WS_OR_EOL(a=s.input.charCodeAt(s.position+1))||i&&is_FLOW_INDICATOR(a))break}else if(35===L){if(is_WS_OR_EOL(s.input.charCodeAt(s.position-1)))break}else{if(s.position===s.lineStart&&testDocumentSeparator(s)||i&&is_FLOW_INDICATOR(L))break;if(is_EOL(L)){if(x=s.line,C=s.lineStart,j=s.lineIndent,skipSeparationSpace(s,!1,-1),s.lineIndent>=o){w=!0,L=s.input.charCodeAt(s.position);continue}s.position=_,s.line=x,s.lineStart=C,s.lineIndent=j;break}}w&&(captureSegment(s,u,_,!1),writeFoldedLines(s,s.line-x),u=_=s.position,w=!1),is_WHITE_SPACE(L)||(_=s.position+1),L=s.input.charCodeAt(++s.position)}return captureSegment(s,u,_,!1),!!s.result||(s.kind=B,s.result=$,!1)}(s,$,1===i)&&(Y=!0,null===s.tag&&(s.tag=\"?\")):(Y=!0,null===s.tag&&null===s.anchor||throwError(s,\"alias node should not have any properties\")),null!==s.anchor&&(s.anchorMap[s.anchor]=s.result)):0===V&&(Y=x&&readBlockSequence(s,U))),null===s.tag)null!==s.anchor&&(s.anchorMap[s.anchor]=s.result);else if(\"?\"===s.tag){for(null!==s.result&&\"scalar\"!==s.kind&&throwError(s,'unacceptable node kind for !<?> tag; it should be \"scalar\", not \"'+s.kind+'\"'),C=0,j=s.implicitTypes.length;C<j;C+=1)if((B=s.implicitTypes[C]).resolve(s.result)){s.result=B.construct(s.result),s.tag=B.tag,null!==s.anchor&&(s.anchorMap[s.anchor]=s.result);break}}else if(\"!\"!==s.tag){if(Rr.call(s.typeMap[s.kind||\"fallback\"],s.tag))B=s.typeMap[s.kind||\"fallback\"][s.tag];else for(B=null,C=0,j=(L=s.typeMap.multi[s.kind||\"fallback\"]).length;C<j;C+=1)if(s.tag.slice(0,L[C].tag.length)===L[C].tag){B=L[C];break}B||throwError(s,\"unknown tag !<\"+s.tag+\">\"),null!==s.result&&B.kind!==s.kind&&throwError(s,\"unacceptable node kind for !<\"+s.tag+'> tag; it should be \"'+B.kind+'\", not \"'+s.kind+'\"'),B.resolve(s.result,s.tag)?(s.result=B.construct(s.result,s.tag),null!==s.anchor&&(s.anchorMap[s.anchor]=s.result)):throwError(s,\"cannot resolve a node with !<\"+s.tag+\"> explicit tag\")}return null!==s.listener&&s.listener(\"close\",s),null!==s.tag||null!==s.anchor||Y}function readDocument(s){var o,i,a,u,_=s.position,w=!1;for(s.version=null,s.checkLineBreaks=s.legacy,s.tagMap=Object.create(null),s.anchorMap=Object.create(null);0!==(u=s.input.charCodeAt(s.position))&&(skipSeparationSpace(s,!0,-1),u=s.input.charCodeAt(s.position),!(s.lineIndent>0||37!==u));){for(w=!0,u=s.input.charCodeAt(++s.position),o=s.position;0!==u&&!is_WS_OR_EOL(u);)u=s.input.charCodeAt(++s.position);for(a=[],(i=s.input.slice(o,s.position)).length<1&&throwError(s,\"directive name must not be less than one character in length\");0!==u;){for(;is_WHITE_SPACE(u);)u=s.input.charCodeAt(++s.position);if(35===u){do{u=s.input.charCodeAt(++s.position)}while(0!==u&&!is_EOL(u));break}if(is_EOL(u))break;for(o=s.position;0!==u&&!is_WS_OR_EOL(u);)u=s.input.charCodeAt(++s.position);a.push(s.input.slice(o,s.position))}0!==u&&readLineBreak(s),Rr.call(zr,i)?zr[i](s,i,a):throwWarning(s,'unknown document directive \"'+i+'\"')}skipSeparationSpace(s,!0,-1),0===s.lineIndent&&45===s.input.charCodeAt(s.position)&&45===s.input.charCodeAt(s.position+1)&&45===s.input.charCodeAt(s.position+2)?(s.position+=3,skipSeparationSpace(s,!0,-1)):w&&throwError(s,\"directives end mark is expected\"),composeNode(s,s.lineIndent-1,4,!1,!0),skipSeparationSpace(s,!0,-1),s.checkLineBreaks&&Lr.test(s.input.slice(_,s.position))&&throwWarning(s,\"non-ASCII line breaks are interpreted as content\"),s.documents.push(s.result),s.position===s.lineStart&&testDocumentSeparator(s)?46===s.input.charCodeAt(s.position)&&(s.position+=3,skipSeparationSpace(s,!0,-1)):s.position<s.length-1&&throwError(s,\"end of the stream or a document separator is expected\")}function loadDocuments(s,o){o=o||{},0!==(s=String(s)).length&&(10!==s.charCodeAt(s.length-1)&&13!==s.charCodeAt(s.length-1)&&(s+=\"\\n\"),65279===s.charCodeAt(0)&&(s=s.slice(1)));var i=new State$1(s,o),a=s.indexOf(\"\\0\");for(-1!==a&&(i.position=a,throwError(i,\"null byte is not allowed in input\")),i.input+=\"\\0\";32===i.input.charCodeAt(i.position);)i.lineIndent+=1,i.position+=1;for(;i.position<i.length-1;)readDocument(i);return i.documents}var Wr={loadAll:function loadAll$1(s,o,i){null!==o&&\"object\"==typeof o&&void 0===i&&(i=o,o=null);var a=loadDocuments(s,i);if(\"function\"!=typeof o)return a;for(var u=0,_=a.length;u<_;u+=1)o(a[u])},load:function load$1(s,o){var i=loadDocuments(s,o);if(0!==i.length){if(1===i.length)return i[0];throw new tr(\"expected a single document in the stream, but found more\")}}},Jr=Object.prototype.toString,Hr=Object.prototype.hasOwnProperty,Kr=65279,Gr={0:\"\\\\0\",7:\"\\\\a\",8:\"\\\\b\",9:\"\\\\t\",10:\"\\\\n\",11:\"\\\\v\",12:\"\\\\f\",13:\"\\\\r\",27:\"\\\\e\",34:'\\\\\"',92:\"\\\\\\\\\",133:\"\\\\N\",160:\"\\\\_\",8232:\"\\\\L\",8233:\"\\\\P\"},Yr=[\"y\",\"Y\",\"yes\",\"Yes\",\"YES\",\"on\",\"On\",\"ON\",\"n\",\"N\",\"no\",\"No\",\"NO\",\"off\",\"Off\",\"OFF\"],Xr=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\\.[0-9_]*)?$/;function encodeHex(s){var o,i,a;if(o=s.toString(16).toUpperCase(),s<=255)i=\"x\",a=2;else if(s<=65535)i=\"u\",a=4;else{if(!(s<=4294967295))throw new tr(\"code point within a string may not be greater than 0xFFFFFFFF\");i=\"U\",a=8}return\"\\\\\"+i+er.repeat(\"0\",a-o.length)+o}function State(s){this.schema=s.schema||Mr,this.indent=Math.max(1,s.indent||2),this.noArrayIndent=s.noArrayIndent||!1,this.skipInvalid=s.skipInvalid||!1,this.flowLevel=er.isNothing(s.flowLevel)?-1:s.flowLevel,this.styleMap=function compileStyleMap(s,o){var i,a,u,_,w,x,C;if(null===o)return{};for(i={},u=0,_=(a=Object.keys(o)).length;u<_;u+=1)w=a[u],x=String(o[w]),\"!!\"===w.slice(0,2)&&(w=\"tag:yaml.org,2002:\"+w.slice(2)),(C=s.compiledTypeMap.fallback[w])&&Hr.call(C.styleAliases,x)&&(x=C.styleAliases[x]),i[w]=x;return i}(this.schema,s.styles||null),this.sortKeys=s.sortKeys||!1,this.lineWidth=s.lineWidth||80,this.noRefs=s.noRefs||!1,this.noCompatMode=s.noCompatMode||!1,this.condenseFlow=s.condenseFlow||!1,this.quotingType='\"'===s.quotingType?2:1,this.forceQuotes=s.forceQuotes||!1,this.replacer=\"function\"==typeof s.replacer?s.replacer:null,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result=\"\",this.duplicates=[],this.usedDuplicates=null}function indentString(s,o){for(var i,a=er.repeat(\" \",o),u=0,_=-1,w=\"\",x=s.length;u<x;)-1===(_=s.indexOf(\"\\n\",u))?(i=s.slice(u),u=x):(i=s.slice(u,_+1),u=_+1),i.length&&\"\\n\"!==i&&(w+=a),w+=i;return w}function generateNextLine(s,o){return\"\\n\"+er.repeat(\" \",s.indent*o)}function isWhitespace(s){return 32===s||9===s}function isPrintable(s){return 32<=s&&s<=126||161<=s&&s<=55295&&8232!==s&&8233!==s||57344<=s&&s<=65533&&s!==Kr||65536<=s&&s<=1114111}function isNsCharOrWhitespace(s){return isPrintable(s)&&s!==Kr&&13!==s&&10!==s}function isPlainSafe(s,o,i){var a=isNsCharOrWhitespace(s),u=a&&!isWhitespace(s);return(i?a:a&&44!==s&&91!==s&&93!==s&&123!==s&&125!==s)&&35!==s&&!(58===o&&!u)||isNsCharOrWhitespace(o)&&!isWhitespace(o)&&35===s||58===o&&u}function codePointAt(s,o){var i,a=s.charCodeAt(o);return a>=55296&&a<=56319&&o+1<s.length&&(i=s.charCodeAt(o+1))>=56320&&i<=57343?1024*(a-55296)+i-56320+65536:a}function needIndentIndicator(s){return/^\\n* /.test(s)}function chooseScalarStyle(s,o,i,a,u,_,w,x){var C,j=0,L=null,B=!1,$=!1,U=-1!==a,V=-1,z=function isPlainSafeFirst(s){return isPrintable(s)&&s!==Kr&&!isWhitespace(s)&&45!==s&&63!==s&&58!==s&&44!==s&&91!==s&&93!==s&&123!==s&&125!==s&&35!==s&&38!==s&&42!==s&&33!==s&&124!==s&&61!==s&&62!==s&&39!==s&&34!==s&&37!==s&&64!==s&&96!==s}(codePointAt(s,0))&&function isPlainSafeLast(s){return!isWhitespace(s)&&58!==s}(codePointAt(s,s.length-1));if(o||w)for(C=0;C<s.length;j>=65536?C+=2:C++){if(!isPrintable(j=codePointAt(s,C)))return 5;z=z&&isPlainSafe(j,L,x),L=j}else{for(C=0;C<s.length;j>=65536?C+=2:C++){if(10===(j=codePointAt(s,C)))B=!0,U&&($=$||C-V-1>a&&\" \"!==s[V+1],V=C);else if(!isPrintable(j))return 5;z=z&&isPlainSafe(j,L,x),L=j}$=$||U&&C-V-1>a&&\" \"!==s[V+1]}return B||$?i>9&&needIndentIndicator(s)?5:w?2===_?5:2:$?4:3:!z||w||u(s)?2===_?5:2:1}function writeScalar(s,o,i,a,u){s.dump=function(){if(0===o.length)return 2===s.quotingType?'\"\"':\"''\";if(!s.noCompatMode&&(-1!==Yr.indexOf(o)||Xr.test(o)))return 2===s.quotingType?'\"'+o+'\"':\"'\"+o+\"'\";var _=s.indent*Math.max(1,i),w=-1===s.lineWidth?-1:Math.max(Math.min(s.lineWidth,40),s.lineWidth-_),x=a||s.flowLevel>-1&&i>=s.flowLevel;switch(chooseScalarStyle(o,x,s.indent,w,(function testAmbiguity(o){return function testImplicitResolving(s,o){var i,a;for(i=0,a=s.implicitTypes.length;i<a;i+=1)if(s.implicitTypes[i].resolve(o))return!0;return!1}(s,o)}),s.quotingType,s.forceQuotes&&!a,u)){case 1:return o;case 2:return\"'\"+o.replace(/'/g,\"''\")+\"'\";case 3:return\"|\"+blockHeader(o,s.indent)+dropEndingNewline(indentString(o,_));case 4:return\">\"+blockHeader(o,s.indent)+dropEndingNewline(indentString(function foldString(s,o){var i,a,u=/(\\n+)([^\\n]*)/g,_=(x=s.indexOf(\"\\n\"),x=-1!==x?x:s.length,u.lastIndex=x,foldLine(s.slice(0,x),o)),w=\"\\n\"===s[0]||\" \"===s[0];var x;for(;a=u.exec(s);){var C=a[1],j=a[2];i=\" \"===j[0],_+=C+(w||i||\"\"===j?\"\":\"\\n\")+foldLine(j,o),w=i}return _}(o,w),_));case 5:return'\"'+function escapeString(s){for(var o,i=\"\",a=0,u=0;u<s.length;a>=65536?u+=2:u++)a=codePointAt(s,u),!(o=Gr[a])&&isPrintable(a)?(i+=s[u],a>=65536&&(i+=s[u+1])):i+=o||encodeHex(a);return i}(o)+'\"';default:throw new tr(\"impossible error: invalid scalar style\")}}()}function blockHeader(s,o){var i=needIndentIndicator(s)?String(o):\"\",a=\"\\n\"===s[s.length-1];return i+(a&&(\"\\n\"===s[s.length-2]||\"\\n\"===s)?\"+\":a?\"\":\"-\")+\"\\n\"}function dropEndingNewline(s){return\"\\n\"===s[s.length-1]?s.slice(0,-1):s}function foldLine(s,o){if(\"\"===s||\" \"===s[0])return s;for(var i,a,u=/ [^ ]/g,_=0,w=0,x=0,C=\"\";i=u.exec(s);)(x=i.index)-_>o&&(a=w>_?w:x,C+=\"\\n\"+s.slice(_,a),_=a+1),w=x;return C+=\"\\n\",s.length-_>o&&w>_?C+=s.slice(_,w)+\"\\n\"+s.slice(w+1):C+=s.slice(_),C.slice(1)}function writeBlockSequence(s,o,i,a){var u,_,w,x=\"\",C=s.tag;for(u=0,_=i.length;u<_;u+=1)w=i[u],s.replacer&&(w=s.replacer.call(i,String(u),w)),(writeNode(s,o+1,w,!0,!0,!1,!0)||void 0===w&&writeNode(s,o+1,null,!0,!0,!1,!0))&&(a&&\"\"===x||(x+=generateNextLine(s,o)),s.dump&&10===s.dump.charCodeAt(0)?x+=\"-\":x+=\"- \",x+=s.dump);s.tag=C,s.dump=x||\"[]\"}function detectType(s,o,i){var a,u,_,w,x,C;for(_=0,w=(u=i?s.explicitTypes:s.implicitTypes).length;_<w;_+=1)if(((x=u[_]).instanceOf||x.predicate)&&(!x.instanceOf||\"object\"==typeof o&&o instanceof x.instanceOf)&&(!x.predicate||x.predicate(o))){if(i?x.multi&&x.representName?s.tag=x.representName(o):s.tag=x.tag:s.tag=\"?\",x.represent){if(C=s.styleMap[x.tag]||x.defaultStyle,\"[object Function]\"===Jr.call(x.represent))a=x.represent(o,C);else{if(!Hr.call(x.represent,C))throw new tr(\"!<\"+x.tag+'> tag resolver accepts not \"'+C+'\" style');a=x.represent[C](o,C)}s.dump=a}return!0}return!1}function writeNode(s,o,i,a,u,_,w){s.tag=null,s.dump=i,detectType(s,i,!1)||detectType(s,i,!0);var x,C=Jr.call(s.dump),j=a;a&&(a=s.flowLevel<0||s.flowLevel>o);var L,B,$=\"[object Object]\"===C||\"[object Array]\"===C;if($&&(B=-1!==(L=s.duplicates.indexOf(i))),(null!==s.tag&&\"?\"!==s.tag||B||2!==s.indent&&o>0)&&(u=!1),B&&s.usedDuplicates[L])s.dump=\"*ref_\"+L;else{if($&&B&&!s.usedDuplicates[L]&&(s.usedDuplicates[L]=!0),\"[object Object]\"===C)a&&0!==Object.keys(s.dump).length?(!function writeBlockMapping(s,o,i,a){var u,_,w,x,C,j,L=\"\",B=s.tag,$=Object.keys(i);if(!0===s.sortKeys)$.sort();else if(\"function\"==typeof s.sortKeys)$.sort(s.sortKeys);else if(s.sortKeys)throw new tr(\"sortKeys must be a boolean or a function\");for(u=0,_=$.length;u<_;u+=1)j=\"\",a&&\"\"===L||(j+=generateNextLine(s,o)),x=i[w=$[u]],s.replacer&&(x=s.replacer.call(i,w,x)),writeNode(s,o+1,w,!0,!0,!0)&&((C=null!==s.tag&&\"?\"!==s.tag||s.dump&&s.dump.length>1024)&&(s.dump&&10===s.dump.charCodeAt(0)?j+=\"?\":j+=\"? \"),j+=s.dump,C&&(j+=generateNextLine(s,o)),writeNode(s,o+1,x,!0,C)&&(s.dump&&10===s.dump.charCodeAt(0)?j+=\":\":j+=\": \",L+=j+=s.dump));s.tag=B,s.dump=L||\"{}\"}(s,o,s.dump,u),B&&(s.dump=\"&ref_\"+L+s.dump)):(!function writeFlowMapping(s,o,i){var a,u,_,w,x,C=\"\",j=s.tag,L=Object.keys(i);for(a=0,u=L.length;a<u;a+=1)x=\"\",\"\"!==C&&(x+=\", \"),s.condenseFlow&&(x+='\"'),w=i[_=L[a]],s.replacer&&(w=s.replacer.call(i,_,w)),writeNode(s,o,_,!1,!1)&&(s.dump.length>1024&&(x+=\"? \"),x+=s.dump+(s.condenseFlow?'\"':\"\")+\":\"+(s.condenseFlow?\"\":\" \"),writeNode(s,o,w,!1,!1)&&(C+=x+=s.dump));s.tag=j,s.dump=\"{\"+C+\"}\"}(s,o,s.dump),B&&(s.dump=\"&ref_\"+L+\" \"+s.dump));else if(\"[object Array]\"===C)a&&0!==s.dump.length?(s.noArrayIndent&&!w&&o>0?writeBlockSequence(s,o-1,s.dump,u):writeBlockSequence(s,o,s.dump,u),B&&(s.dump=\"&ref_\"+L+s.dump)):(!function writeFlowSequence(s,o,i){var a,u,_,w=\"\",x=s.tag;for(a=0,u=i.length;a<u;a+=1)_=i[a],s.replacer&&(_=s.replacer.call(i,String(a),_)),(writeNode(s,o,_,!1,!1)||void 0===_&&writeNode(s,o,null,!1,!1))&&(\"\"!==w&&(w+=\",\"+(s.condenseFlow?\"\":\" \")),w+=s.dump);s.tag=x,s.dump=\"[\"+w+\"]\"}(s,o,s.dump),B&&(s.dump=\"&ref_\"+L+\" \"+s.dump));else{if(\"[object String]\"!==C){if(\"[object Undefined]\"===C)return!1;if(s.skipInvalid)return!1;throw new tr(\"unacceptable kind of an object to dump \"+C)}\"?\"!==s.tag&&writeScalar(s,s.dump,o,_,j)}null!==s.tag&&\"?\"!==s.tag&&(x=encodeURI(\"!\"===s.tag[0]?s.tag.slice(1):s.tag).replace(/!/g,\"%21\"),x=\"!\"===s.tag[0]?\"!\"+x:\"tag:yaml.org,2002:\"===x.slice(0,18)?\"!!\"+x.slice(18):\"!<\"+x+\">\",s.dump=x+\" \"+s.dump)}return!0}function getDuplicateReferences(s,o){var i,a,u=[],_=[];for(inspectNode(s,u,_),i=0,a=_.length;i<a;i+=1)o.duplicates.push(u[_[i]]);o.usedDuplicates=new Array(a)}function inspectNode(s,o,i){var a,u,_;if(null!==s&&\"object\"==typeof s)if(-1!==(u=o.indexOf(s)))-1===i.indexOf(u)&&i.push(u);else if(o.push(s),Array.isArray(s))for(u=0,_=s.length;u<_;u+=1)inspectNode(s[u],o,i);else for(u=0,_=(a=Object.keys(s)).length;u<_;u+=1)inspectNode(s[a[u]],o,i)}var Qr=function dump$1(s,o){var i=new State(o=o||{});i.noRefs||getDuplicateReferences(s,i);var a=s;return i.replacer&&(a=i.replacer.call({\"\":a},\"\",a)),writeNode(i,0,a,!0,!0)?i.dump+\"\\n\":\"\"};function renamed(s,o){return function(){throw new Error(\"Function yaml.\"+s+\" is removed in js-yaml 4. Use yaml.\"+o+\" instead, which is now safe by default.\")}}var Zr=ir,en=ar,tn=pr,rn=br,nn=_r,sn=Mr,on=Wr.load,an=Wr.loadAll,cn={dump:Qr}.dump,ln=tr,un={binary:Or,float:vr,map:ur,null:dr,pairs:Ir,set:Nr,timestamp:wr,bool:fr,int:mr,merge:xr,omap:jr,seq:lr,str:cr},pn=renamed(\"safeLoad\",\"load\"),hn=renamed(\"safeLoadAll\",\"loadAll\"),dn=renamed(\"safeDump\",\"dump\"),fn={Type:Zr,Schema:en,FAILSAFE_SCHEMA:tn,JSON_SCHEMA:rn,CORE_SCHEMA:nn,DEFAULT_SCHEMA:sn,load:on,loadAll:an,dump:cn,YAMLException:ln,types:un,safeLoad:pn,safeLoadAll:hn,safeDump:dn};const mn=\"configs_update\",gn=\"configs_toggle\";function update(s,o){return{type:mn,payload:{[s]:o}}}function toggle(s){return{type:gn,payload:s}}const actions_loaded=()=>()=>{},downloadConfig=s=>o=>{const{fn:{fetch:i}}=o;return i(s)},getConfigByUrl=(s,o)=>i=>{const{specActions:a,configsActions:u}=i;if(s)return u.downloadConfig(s).then(next,next);function next(u){u instanceof Error||u.status>=400?(a.updateLoadingStatus(\"failedConfig\"),a.updateLoadingStatus(\"failedConfig\"),a.updateUrl(\"\"),console.error(u.statusText+\" \"+s.url),o(null)):o(((s,o)=>{try{return fn.load(s)}catch(s){return o&&o.errActions.newThrownErr(new Error(s)),{}}})(u.text,i))}},get=(s,o)=>s.getIn(Array.isArray(o)?o:[o]),yn={[mn]:(s,o)=>s.merge((0,ze.fromJS)(o.payload)),[gn]:(s,o)=>{const i=o.payload,a=s.get(i);return s.set(i,!a)}};function configsPlugin(){return{statePlugins:{configs:{reducers:yn,actions:u,selectors:_}}}}const setHash=s=>s?history.pushState(null,null,`#${s}`):window.location.hash=\"\";var vn=__webpack_require__(86215),bn=__webpack_require__.n(vn);const _n=\"layout_scroll_to\",Sn=\"layout_clear_scroll\";const En={fn:{getScrollParent:function getScrollParent(s,o){const i=document.documentElement;let a=getComputedStyle(s);const u=\"absolute\"===a.position,_=o?/(auto|scroll|hidden)/:/(auto|scroll)/;if(\"fixed\"===a.position)return i;for(let o=s;o=o.parentElement;)if(a=getComputedStyle(o),(!u||\"static\"!==a.position)&&_.test(a.overflow+a.overflowY+a.overflowX))return o;return i}},statePlugins:{layout:{actions:{scrollToElement:(s,o)=>i=>{try{o=o||i.fn.getScrollParent(s),bn().createScroller(o).to(s)}catch(s){console.error(s)}},scrollTo:s=>({type:_n,payload:Array.isArray(s)?s:[s]}),clearScrollTo:()=>({type:Sn}),readyToScroll:(s,o)=>i=>{const a=i.layoutSelectors.getScrollToKey();We().is(a,(0,ze.fromJS)(s))&&(i.layoutActions.scrollToElement(o),i.layoutActions.clearScrollTo())},parseDeepLinkHash:s=>({layoutActions:o,layoutSelectors:i,getConfigs:a})=>{if(a().deepLinking&&s){let a=s.slice(1);\"!\"===a[0]&&(a=a.slice(1)),\"/\"===a[0]&&(a=a.slice(1));const u=a.split(\"/\").map((s=>s||\"\")),_=i.isShownKeyFromUrlHashArray(u),[w,x=\"\",C=\"\"]=_;if(\"operations\"===w){const s=i.isShownKeyFromUrlHashArray([x]);x.indexOf(\"_\")>-1&&(console.warn(\"Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead.\"),o.show(s.map((s=>s.replace(/_/g,\" \"))),!0)),o.show(s,!0)}(x.indexOf(\"_\")>-1||C.indexOf(\"_\")>-1)&&(console.warn(\"Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead.\"),o.show(_.map((s=>s.replace(/_/g,\" \"))),!0)),o.show(_,!0),o.scrollTo(_)}}},selectors:{getScrollToKey:s=>s.get(\"scrollToKey\"),isShownKeyFromUrlHashArray(s,o){const[i,a]=o;return a?[\"operations\",i,a]:i?[\"operations-tag\",i]:[]},urlHashArrayFromIsShownKey(s,o){let[i,a,u]=o;return\"operations\"==i?[a,u]:\"operations-tag\"==i?[a]:[]}},reducers:{[_n]:(s,o)=>s.set(\"scrollToKey\",We().fromJS(o.payload)),[Sn]:s=>s.delete(\"scrollToKey\")},wrapActions:{show:(s,{getConfigs:o,layoutSelectors:i})=>(...a)=>{if(s(...a),o().deepLinking)try{let[s,o]=a;s=Array.isArray(s)?s:[s];const u=i.urlHashArrayFromIsShownKey(s);if(!u.length)return;const[_,w]=u;if(!o)return setHash(\"/\");2===u.length?setHash(createDeepLinkPath(`/${encodeURIComponent(_)}/${encodeURIComponent(w)}`)):1===u.length&&setHash(createDeepLinkPath(`/${encodeURIComponent(_)}`))}catch(s){console.error(s)}}}}}};var wn=__webpack_require__(2209),xn=__webpack_require__.n(wn);const operation_wrapper=(s,o)=>class OperationWrapper extends Re.Component{onLoad=s=>{const{operation:i}=this.props,{tag:a,operationId:u}=i.toObject();let{isShownKey:_}=i.toObject();_=_||[\"operations\",a,u],o.layoutActions.readyToScroll(_,s)};render(){return Re.createElement(\"span\",{ref:this.onLoad},Re.createElement(s,this.props))}},operation_tag_wrapper=(s,o)=>class OperationTagWrapper extends Re.Component{onLoad=s=>{const{tag:i}=this.props,a=[\"operations-tag\",i];o.layoutActions.readyToScroll(a,s)};render(){return Re.createElement(\"span\",{ref:this.onLoad},Re.createElement(s,this.props))}};function deep_linking(){return[En,{statePlugins:{configs:{wrapActions:{loaded:(s,o)=>(...i)=>{s(...i);const a=decodeURIComponent(window.location.hash);o.layoutActions.parseDeepLinkHash(a)}}}},wrapComponents:{operation:operation_wrapper,OperationTag:operation_tag_wrapper}}]}var kn=__webpack_require__(40860),On=__webpack_require__.n(kn);function transform(s){return s.map((s=>{let o=\"is not of a type(s)\",i=s.get(\"message\").indexOf(o);if(i>-1){let o=s.get(\"message\").slice(i+19).split(\",\");return s.set(\"message\",s.get(\"message\").slice(0,i)+function makeNewMessage(s){return s.reduce(((s,o,i,a)=>i===a.length-1&&a.length>1?s+\"or \"+o:a[i+1]&&a.length>2?s+o+\", \":a[i+1]?s+o+\" \":s+o),\"should be a\")}(o))}return s}))}var An=__webpack_require__(58156),Cn=__webpack_require__.n(An);function parameter_oneof_transform(s,{jsSpec:o}){return s}const jn=[w,x];function transformErrors(s){let o={jsSpec:{}},i=On()(jn,((s,i)=>{try{return i.transform(s,o).filter((s=>!!s))}catch(o){return console.error(\"Transformer error:\",o),s}}),s);return i.filter((s=>!!s)).map((s=>(!s.get(\"line\")&&s.get(\"path\"),s)))}let Pn={line:0,level:\"error\",message:\"Unknown error\"};const In=Ut((s=>s),(s=>s.get(\"errors\",(0,ze.List)()))),Tn=Ut(In,(s=>s.last()));function err(o){return{statePlugins:{err:{reducers:{[rt]:(s,{payload:o})=>{let i=Object.assign(Pn,o,{type:\"thrown\"});return s.update(\"errors\",(s=>(s||(0,ze.List)()).push((0,ze.fromJS)(i)))).update(\"errors\",(s=>transformErrors(s)))},[nt]:(s,{payload:o})=>(o=o.map((s=>(0,ze.fromJS)(Object.assign(Pn,s,{type:\"thrown\"})))),s.update(\"errors\",(s=>(s||(0,ze.List)()).concat((0,ze.fromJS)(o)))).update(\"errors\",(s=>transformErrors(s)))),[st]:(s,{payload:o})=>{let i=(0,ze.fromJS)(o);return i=i.set(\"type\",\"spec\"),s.update(\"errors\",(s=>(s||(0,ze.List)()).push((0,ze.fromJS)(i)).sortBy((s=>s.get(\"line\"))))).update(\"errors\",(s=>transformErrors(s)))},[ot]:(s,{payload:o})=>(o=o.map((s=>(0,ze.fromJS)(Object.assign(Pn,s,{type:\"spec\"})))),s.update(\"errors\",(s=>(s||(0,ze.List)()).concat((0,ze.fromJS)(o)))).update(\"errors\",(s=>transformErrors(s)))),[it]:(s,{payload:o})=>{let i=(0,ze.fromJS)(Object.assign({},o));return i=i.set(\"type\",\"auth\"),s.update(\"errors\",(s=>(s||(0,ze.List)()).push((0,ze.fromJS)(i)))).update(\"errors\",(s=>transformErrors(s)))},[at]:(s,{payload:o})=>{if(!o||!s.get(\"errors\"))return s;let i=s.get(\"errors\").filter((s=>s.keySeq().every((i=>{const a=s.get(i),u=o[i];return!u||a!==u}))));return s.merge({errors:i})},[ct]:(s,{payload:o})=>{if(!o||\"function\"!=typeof o)return s;let i=s.get(\"errors\").filter((s=>o(s)));return s.merge({errors:i})}},actions:s,selectors:C}}}}function opsFilter(s,o){return s.filter(((s,i)=>-1!==i.indexOf(o)))}function filter(){return{fn:{opsFilter}}}var Nn=__webpack_require__(7666),Mn=__webpack_require__.n(Nn);const arrow_up=({className:s=null,width:o=20,height:i=20,...a})=>Re.createElement(\"svg\",Mn()({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 20 20\",className:s,width:o,height:i,\"aria-hidden\":\"true\",focusable:\"false\"},a),Re.createElement(\"path\",{d:\"M 17.418 14.908 C 17.69 15.176 18.127 15.176 18.397 14.908 C 18.667 14.64 18.668 14.207 18.397 13.939 L 10.489 6.109 C 10.219 5.841 9.782 5.841 9.51 6.109 L 1.602 13.939 C 1.332 14.207 1.332 14.64 1.602 14.908 C 1.873 15.176 2.311 15.176 2.581 14.908 L 10 7.767 L 17.418 14.908 Z\"})),arrow_down=({className:s=null,width:o=20,height:i=20,...a})=>Re.createElement(\"svg\",Mn()({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 20 20\",className:s,width:o,height:i,\"aria-hidden\":\"true\",focusable:\"false\"},a),Re.createElement(\"path\",{d:\"M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z\"})),arrow=({className:s=null,width:o=20,height:i=20,...a})=>Re.createElement(\"svg\",Mn()({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 20 20\",className:s,width:o,height:i,\"aria-hidden\":\"true\",focusable:\"false\"},a),Re.createElement(\"path\",{d:\"M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z\"})),components_close=({className:s=null,width:o=20,height:i=20,...a})=>Re.createElement(\"svg\",Mn()({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 20 20\",className:s,width:o,height:i,\"aria-hidden\":\"true\",focusable:\"false\"},a),Re.createElement(\"path\",{d:\"M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z\"})),copy=({className:s=null,width:o=15,height:i=16,...a})=>Re.createElement(\"svg\",Mn()({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 15 16\",className:s,width:o,height:i,\"aria-hidden\":\"true\",focusable:\"false\"},a),Re.createElement(\"g\",{transform:\"translate(2, -1)\"},Re.createElement(\"path\",{fill:\"#ffffff\",fillRule:\"evenodd\",d:\"M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z\"}))),lock=({className:s=null,width:o=20,height:i=20,...a})=>Re.createElement(\"svg\",Mn()({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 20 20\",className:s,width:o,height:i,\"aria-hidden\":\"true\",focusable:\"false\"},a),Re.createElement(\"path\",{d:\"M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z\"})),unlock=({className:s=null,width:o=20,height:i=20,...a})=>Re.createElement(\"svg\",Mn()({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 20 20\",className:s,width:o,height:i,\"aria-hidden\":\"true\",focusable:\"false\"},a),Re.createElement(\"path\",{d:\"M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z\"})),icons=()=>({components:{ArrowUpIcon:arrow_up,ArrowDownIcon:arrow_down,ArrowIcon:arrow,CloseIcon:components_close,CopyIcon:copy,LockIcon:lock,UnlockIcon:unlock}}),Rn=\"layout_update_layout\",Dn=\"layout_update_filter\",Ln=\"layout_update_mode\",Fn=\"layout_show\";function updateLayout(s){return{type:Rn,payload:s}}function updateFilter(s){return{type:Dn,payload:s}}function actions_show(s,o=!0){return s=normalizeArray(s),{type:Fn,payload:{thing:s,shown:o}}}function changeMode(s,o=\"\"){return s=normalizeArray(s),{type:Ln,payload:{thing:s,mode:o}}}const Bn={[Rn]:(s,o)=>s.set(\"layout\",o.payload),[Dn]:(s,o)=>s.set(\"filter\",o.payload),[Fn]:(s,o)=>{const i=o.payload.shown,a=(0,ze.fromJS)(o.payload.thing);return s.update(\"shown\",(0,ze.fromJS)({}),(s=>s.set(a,i)))},[Ln]:(s,o)=>{let i=o.payload.thing,a=o.payload.mode;return s.setIn([\"modes\"].concat(i),(a||\"\")+\"\")}},current=s=>s.get(\"layout\"),currentFilter=s=>s.get(\"filter\"),isShown=(s,o,i)=>(o=normalizeArray(o),s.get(\"shown\",(0,ze.fromJS)({})).get((0,ze.fromJS)(o),i)),whatMode=(s,o,i=\"\")=>(o=normalizeArray(o),s.getIn([\"modes\",...o],i)),$n=Ut((s=>s),(s=>!isShown(s,\"editor\"))),taggedOperations=(s,o)=>(i,...a)=>{let u=s(i,...a);const{fn:_,layoutSelectors:w,getConfigs:x}=o.getSystem(),C=x(),{maxDisplayedTags:j}=C;let L=w.currentFilter();return L&&!0!==L&&(u=_.opsFilter(u,L)),j>=0&&(u=u.slice(0,j)),u};function plugins_layout(){return{statePlugins:{layout:{reducers:Bn,actions:j,selectors:L},spec:{wrapSelectors:B}}}}function logs({configs:s}){const o={debug:0,info:1,log:2,warn:3,error:4},getLevel=s=>o[s]||-1;let{logLevel:i}=s,a=getLevel(i);function log(s,...o){getLevel(s)>=a&&console[s](...o)}return log.warn=log.bind(null,\"warn\"),log.error=log.bind(null,\"error\"),log.info=log.bind(null,\"info\"),log.debug=log.bind(null,\"debug\"),{rootInjects:{log}}}let qn=!1;function on_complete(){return{statePlugins:{spec:{wrapActions:{updateSpec:s=>(...o)=>(qn=!0,s(...o)),updateJsonSpec:(s,o)=>(...i)=>{const a=o.getConfigs().onComplete;return qn&&\"function\"==typeof a&&(setTimeout(a,0),qn=!1),s(...i)}}}}}}const extractKey=s=>{const o=\"_**[]\";return s.indexOf(o)<0?s:s.split(o)[0].trim()},escapeShell=s=>\"-d \"===s||/^[_\\/-]/g.test(s)?s:\"'\"+s.replace(/'/g,\"'\\\\''\")+\"'\",escapeCMD=s=>\"-d \"===(s=s.replace(/\\^/g,\"^^\").replace(/\\\\\"/g,'\\\\\\\\\"').replace(/\"/g,'\"\"').replace(/\\n/g,\"^\\n\"))?s.replace(/-d /g,\"-d ^\\n\"):/^[_\\/-]/g.test(s)?s:'\"'+s+'\"',escapePowershell=s=>{if(\"-d \"===s)return s;if(/\\n/.test(s)){return`@\"\\n${s.replace(/`/g,\"``\").replace(/\\$/g,\"`$\")}\\n\"@`}if(!/^[_\\/-]/.test(s)){return`'${s.replace(/'/g,\"''\")}'`}return s};const curlify=(s,o,i,a=\"\")=>{let u=!1,_=\"\";const addWords=(...s)=>_+=\" \"+s.map(o).join(\" \"),addWordsWithoutLeadingSpace=(...s)=>_+=s.map(o).join(\" \"),addNewLine=()=>_+=` ${i}`,addIndent=(s=1)=>_+=\"  \".repeat(s);let w=s.get(\"headers\");_+=\"curl\"+a;const x=s.get(\"curlOptions\");if(ze.List.isList(x)&&!x.isEmpty()&&addWords(...s.get(\"curlOptions\")),addWords(\"-X\",s.get(\"method\")),addNewLine(),addIndent(),addWordsWithoutLeadingSpace(`${s.get(\"url\")}`),w&&w.size)for(let o of s.get(\"headers\").entries()){addNewLine(),addIndent();let[s,i]=o;addWordsWithoutLeadingSpace(\"-H\",`${s}: ${i}`),u=u||/^content-type$/i.test(s)&&/^multipart\\/form-data$/i.test(i)}const C=s.get(\"body\");if(C)if(u&&[\"POST\",\"PUT\",\"PATCH\"].includes(s.get(\"method\")))for(let[s,o]of C.entrySeq()){let i=extractKey(s);addNewLine(),addIndent(),addWordsWithoutLeadingSpace(\"-F\"),o instanceof lt.File&&\"string\"==typeof o.valueOf()?addWords(`${i}=${o.data}${o.type?`;type=${o.type}`:\"\"}`):o instanceof lt.File?addWords(`${i}=@${o.name}${o.type?`;type=${o.type}`:\"\"}`):addWords(`${i}=${o}`)}else if(C instanceof lt.File)addNewLine(),addIndent(),addWordsWithoutLeadingSpace(`--data-binary '@${C.name}'`);else{addNewLine(),addIndent(),addWordsWithoutLeadingSpace(\"-d \");let o=C;ze.Map.isMap(o)?addWordsWithoutLeadingSpace(function getStringBodyOfMap(s){let o=[];for(let[i,a]of s.get(\"body\").entrySeq()){let s=extractKey(i);a instanceof lt.File?o.push(`  \"${s}\": {\\n    \"name\": \"${a.name}\"${a.type?`,\\n    \"type\": \"${a.type}\"`:\"\"}\\n  }`):o.push(`  \"${s}\": ${JSON.stringify(a,null,2).replace(/(\\r\\n|\\r|\\n)/g,\"\\n  \")}`)}return`{\\n${o.join(\",\\n\")}\\n}`}(s)):(\"string\"!=typeof o&&(o=JSON.stringify(o)),addWordsWithoutLeadingSpace(o))}else C||\"POST\"!==s.get(\"method\")||(addNewLine(),addIndent(),addWordsWithoutLeadingSpace(\"-d ''\"));return _},requestSnippetGenerator_curl_powershell=s=>curlify(s,escapePowershell,\"`\\n\",\".exe\"),requestSnippetGenerator_curl_bash=s=>curlify(s,escapeShell,\"\\\\\\n\"),requestSnippetGenerator_curl_cmd=s=>curlify(s,escapeCMD,\"^\\n\"),request_snippets_selectors_state=s=>s||(0,ze.Map)(),Un=Ut(request_snippets_selectors_state,(s=>{const o=s.get(\"languages\"),i=s.get(\"generators\",(0,ze.Map)());return!o||o.isEmpty()?i:i.filter(((s,i)=>o.includes(i)))})),getSnippetGenerators=s=>({fn:o})=>Un(s).map(((s,i)=>{const a=(s=>o[`requestSnippetGenerator_${s}`])(i);return\"function\"!=typeof a?null:s.set(\"fn\",a)})).filter((s=>s)),Vn=Ut(request_snippets_selectors_state,(s=>s.get(\"activeLanguage\"))),zn=Ut(request_snippets_selectors_state,(s=>s.get(\"defaultExpanded\")));var Wn=__webpack_require__(46942),Jn=__webpack_require__.n(Wn),Hn=__webpack_require__(59399);const Kn={cursor:\"pointer\",lineHeight:1,display:\"inline-flex\",backgroundColor:\"rgb(250, 250, 250)\",paddingBottom:\"0\",paddingTop:\"0\",border:\"1px solid rgb(51, 51, 51)\",borderRadius:\"4px 4px 0 0\",boxShadow:\"none\",borderBottom:\"none\"},Gn={cursor:\"pointer\",lineHeight:1,display:\"inline-flex\",backgroundColor:\"rgb(51, 51, 51)\",boxShadow:\"none\",border:\"1px solid rgb(51, 51, 51)\",paddingBottom:\"0\",paddingTop:\"0\",borderRadius:\"4px 4px 0 0\",marginTop:\"-5px\",marginRight:\"-5px\",marginLeft:\"-5px\",zIndex:\"9999\",borderBottom:\"none\"},request_snippets=({request:s,requestSnippetsSelectors:o,getComponent:i})=>{const a=(0,Re.useRef)(null),u=i(\"ArrowUpIcon\"),_=i(\"ArrowDownIcon\"),w=i(\"SyntaxHighlighter\",!0),[x,C]=(0,Re.useState)(o.getSnippetGenerators()?.keySeq().first()),[j,L]=(0,Re.useState)(o?.getDefaultExpanded()),B=o.getSnippetGenerators(),$=B.get(x),U=$.get(\"fn\")(s),handleSetIsExpanded=()=>{L(!j)},handleGetBtnStyle=s=>s===x?Gn:Kn,handlePreventYScrollingBeyondElement=s=>{const{target:o,deltaY:i}=s,{scrollHeight:a,offsetHeight:u,scrollTop:_}=o;a>u&&(0===_&&i<0||u+_>=a&&i>0)&&s.preventDefault()};return(0,Re.useEffect)((()=>{}),[]),(0,Re.useEffect)((()=>{const s=Array.from(a.current.childNodes).filter((s=>!!s.nodeType&&s.classList?.contains(\"curl-command\")));return s.forEach((s=>s.addEventListener(\"mousewheel\",handlePreventYScrollingBeyondElement,{passive:!1}))),()=>{s.forEach((s=>s.removeEventListener(\"mousewheel\",handlePreventYScrollingBeyondElement)))}}),[s]),Re.createElement(\"div\",{className:\"request-snippets\",ref:a},Re.createElement(\"div\",{style:{width:\"100%\",display:\"flex\",justifyContent:\"flex-start\",alignItems:\"center\",marginBottom:\"15px\"}},Re.createElement(\"h4\",{onClick:()=>handleSetIsExpanded(),style:{cursor:\"pointer\"}},\"Snippets\"),Re.createElement(\"button\",{onClick:()=>handleSetIsExpanded(),style:{border:\"none\",background:\"none\"},title:j?\"Collapse operation\":\"Expand operation\"},j?Re.createElement(_,{className:\"arrow\",width:\"10\",height:\"10\"}):Re.createElement(u,{className:\"arrow\",width:\"10\",height:\"10\"}))),j&&Re.createElement(\"div\",{className:\"curl-command\"},Re.createElement(\"div\",{style:{paddingLeft:\"15px\",paddingRight:\"10px\",width:\"100%\",display:\"flex\"}},B.entrySeq().map((([s,o])=>Re.createElement(\"div\",{className:Jn()(\"btn\",{active:s===x}),style:handleGetBtnStyle(s),key:s,onClick:()=>(s=>{x!==s&&C(s)})(s)},Re.createElement(\"h4\",{style:s===x?{color:\"white\"}:{}},o.get(\"title\")))))),Re.createElement(\"div\",{className:\"copy-to-clipboard\"},Re.createElement(Hn.CopyToClipboard,{text:U},Re.createElement(\"button\",null))),Re.createElement(\"div\",null,Re.createElement(w,{language:$.get(\"syntax\"),className:\"curl microlight\",renderPlainText:({children:s,PlainTextViewer:o})=>Re.createElement(o,{className:\"curl\"},s)},U))))},plugins_request_snippets=()=>({components:{RequestSnippets:request_snippets},fn:{requestSnippetGenerator_curl_bash,requestSnippetGenerator_curl_cmd,requestSnippetGenerator_curl_powershell},statePlugins:{requestSnippets:{selectors:$}}});class ModelCollapse extends Re.Component{static defaultProps={collapsedContent:\"{...}\",expanded:!1,title:null,onToggle:()=>{},hideSelfOnExpand:!1,specPath:We().List([])};constructor(s,o){super(s,o);let{expanded:i,collapsedContent:a}=this.props;this.state={expanded:i,collapsedContent:a||ModelCollapse.defaultProps.collapsedContent}}componentDidMount(){const{hideSelfOnExpand:s,expanded:o,modelName:i}=this.props;s&&o&&this.props.onToggle(i,o)}UNSAFE_componentWillReceiveProps(s){this.props.expanded!==s.expanded&&this.setState({expanded:s.expanded})}toggleCollapsed=()=>{this.props.onToggle&&this.props.onToggle(this.props.modelName,!this.state.expanded),this.setState({expanded:!this.state.expanded})};onLoad=s=>{if(s&&this.props.layoutSelectors){const o=this.props.layoutSelectors.getScrollToKey();We().is(o,this.props.specPath)&&this.toggleCollapsed(),this.props.layoutActions.readyToScroll(this.props.specPath,s.parentElement)}};render(){const{title:s,classes:o}=this.props;return this.state.expanded&&this.props.hideSelfOnExpand?Re.createElement(\"span\",{className:o||\"\"},this.props.children):Re.createElement(\"span\",{className:o||\"\",ref:this.onLoad},Re.createElement(\"button\",{\"aria-expanded\":this.state.expanded,className:\"model-box-control\",onClick:this.toggleCollapsed},s&&Re.createElement(\"span\",{className:\"pointer\"},s),Re.createElement(\"span\",{className:\"model-toggle\"+(this.state.expanded?\"\":\" collapsed\")}),!this.state.expanded&&Re.createElement(\"span\",null,this.state.collapsedContent)),this.state.expanded&&this.props.children)}}const useTabs=({initialTab:s,isExecute:o,schema:i,example:a})=>{const u=(0,Re.useMemo)((()=>({example:\"example\",model:\"model\"})),[]),_=(0,Re.useMemo)((()=>Object.keys(u)),[u]).includes(s)&&i&&!o?s:u.example,w=(s=>{const o=(0,Re.useRef)();return(0,Re.useEffect)((()=>{o.current=s})),o.current})(o),[x,C]=(0,Re.useState)(_),j=(0,Re.useCallback)((s=>{C(s.target.dataset.name)}),[]);return(0,Re.useEffect)((()=>{w&&!o&&a&&C(u.example)}),[w,o,a]),{activeTab:x,onTabChange:j,tabs:u}},model_example=({schema:s,example:o,isExecute:i=!1,specPath:a,includeWriteOnly:u=!1,includeReadOnly:_=!1,getComponent:w,getConfigs:x,specSelectors:C})=>{const{defaultModelRendering:j,defaultModelExpandDepth:L}=x(),B=w(\"ModelWrapper\"),$=w(\"HighlightCode\",!0),U=xt()(5).toString(\"base64\"),V=xt()(5).toString(\"base64\"),z=xt()(5).toString(\"base64\"),Y=xt()(5).toString(\"base64\"),Z=C.isOAS3(),{activeTab:ee,tabs:ie,onTabChange:ae}=useTabs({initialTab:j,isExecute:i,schema:s,example:o});return Re.createElement(\"div\",{className:\"model-example\"},Re.createElement(\"ul\",{className:\"tab\",role:\"tablist\"},Re.createElement(\"li\",{className:Jn()(\"tabitem\",{active:ee===ie.example}),role:\"presentation\"},Re.createElement(\"button\",{\"aria-controls\":V,\"aria-selected\":ee===ie.example,className:\"tablinks\",\"data-name\":\"example\",id:U,onClick:ae,role:\"tab\"},i?\"Edit Value\":\"Example Value\")),s&&Re.createElement(\"li\",{className:Jn()(\"tabitem\",{active:ee===ie.model}),role:\"presentation\"},Re.createElement(\"button\",{\"aria-controls\":Y,\"aria-selected\":ee===ie.model,className:Jn()(\"tablinks\",{inactive:i}),\"data-name\":\"model\",id:z,onClick:ae,role:\"tab\"},Z?\"Schema\":\"Model\"))),ee===ie.example&&Re.createElement(\"div\",{\"aria-hidden\":ee!==ie.example,\"aria-labelledby\":U,\"data-name\":\"examplePanel\",id:V,role:\"tabpanel\",tabIndex:\"0\"},o||Re.createElement($,null,\"(no example available\")),ee===ie.model&&Re.createElement(\"div\",{className:\"model-container\",\"aria-hidden\":ee===ie.example,\"aria-labelledby\":z,\"data-name\":\"modelPanel\",id:Y,role:\"tabpanel\",tabIndex:\"0\"},Re.createElement(B,{schema:s,getComponent:w,getConfigs:x,specSelectors:C,expandDepth:L,specPath:a,includeReadOnly:_,includeWriteOnly:u})))};class ModelWrapper extends Re.Component{onToggle=(s,o)=>{this.props.layoutActions&&this.props.layoutActions.show(this.props.fullPath,o)};render(){let{getComponent:s,getConfigs:o}=this.props;const i=s(\"Model\");let a;return this.props.layoutSelectors&&(a=this.props.layoutSelectors.isShown(this.props.fullPath)),Re.createElement(\"div\",{className:\"model-box\"},Re.createElement(i,Mn()({},this.props,{getConfigs:o,expanded:a,depth:1,onToggle:this.onToggle,expandDepth:this.props.expandDepth||0})))}}function _typeof(s){return _typeof=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(s){return typeof s}:function(s){return s&&\"function\"==typeof Symbol&&s.constructor===Symbol&&s!==Symbol.prototype?\"symbol\":typeof s},_typeof(s)}function _defineProperties(s,o){for(var i=0;i<o.length;i++){var a=o[i];a.enumerable=a.enumerable||!1,a.configurable=!0,\"value\"in a&&(a.writable=!0),Object.defineProperty(s,a.key,a)}}function _defineProperty(s,o,i){return o in s?Object.defineProperty(s,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):s[o]=i,s}function ownKeys(s,o){var i=Object.keys(s);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(s);o&&(a=a.filter((function(o){return Object.getOwnPropertyDescriptor(s,o).enumerable}))),i.push.apply(i,a)}return i}function _getPrototypeOf(s){return _getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf:function _getPrototypeOf(s){return s.__proto__||Object.getPrototypeOf(s)},_getPrototypeOf(s)}function _setPrototypeOf(s,o){return _setPrototypeOf=Object.setPrototypeOf||function _setPrototypeOf(s,o){return s.__proto__=o,s},_setPrototypeOf(s,o)}function _possibleConstructorReturn(s,o){return!o||\"object\"!=typeof o&&\"function\"!=typeof o?function _assertThisInitialized(s){if(void 0===s)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return s}(s):o}var Yn={};function react_immutable_pure_component_es_get(s,o,i){return function isInvalid(s){return null==s}(s)?i:function isMapLike(s){return null!==s&&\"object\"===_typeof(s)&&\"function\"==typeof s.get&&\"function\"==typeof s.has}(s)?s.has(o)?s.get(o):i:hasOwnProperty.call(s,o)?s[o]:i}function getIn(s,o,i){for(var a=0;a!==o.length;)if((s=react_immutable_pure_component_es_get(s,o[a++],Yn))===Yn)return i;return s}function check(s){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},a=function createChecker(s,o){return function(i){if(\"string\"==typeof i)return(0,ze.is)(o[i],s[i]);if(Array.isArray(i))return(0,ze.is)(getIn(o,i),getIn(s,i));throw new TypeError(\"Invalid key: expected Array or string: \"+i)}}(o,i),u=s||Object.keys(function _objectSpread2(s){for(var o=1;o<arguments.length;o++){var i=null!=arguments[o]?arguments[o]:{};o%2?ownKeys(i,!0).forEach((function(o){_defineProperty(s,o,i[o])})):Object.getOwnPropertyDescriptors?Object.defineProperties(s,Object.getOwnPropertyDescriptors(i)):ownKeys(i).forEach((function(o){Object.defineProperty(s,o,Object.getOwnPropertyDescriptor(i,o))}))}return s}({},i,{},o));return u.every(a)}const Xn=function(s){function ImmutablePureComponent(){return function _classCallCheck(s,o){if(!(s instanceof o))throw new TypeError(\"Cannot call a class as a function\")}(this,ImmutablePureComponent),_possibleConstructorReturn(this,_getPrototypeOf(ImmutablePureComponent).apply(this,arguments))}return function _inherits(s,o){if(\"function\"!=typeof o&&null!==o)throw new TypeError(\"Super expression must either be null or a function\");s.prototype=Object.create(o&&o.prototype,{constructor:{value:s,writable:!0,configurable:!0}}),o&&_setPrototypeOf(s,o)}(ImmutablePureComponent,s),function _createClass(s,o,i){return o&&_defineProperties(s.prototype,o),i&&_defineProperties(s,i),s}(ImmutablePureComponent,[{key:\"shouldComponentUpdate\",value:function shouldComponentUpdate(s){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return!check(this.updateOnProps,this.props,s,\"updateOnProps\")||!check(this.updateOnStates,this.state,o,\"updateOnStates\")}}]),ImmutablePureComponent}(Re.Component);var Qn,Zn=__webpack_require__(5556),es=__webpack_require__.n(Zn);function _extends(){return _extends=Object.assign?Object.assign.bind():function(s){for(var o=1;o<arguments.length;o++){var i=arguments[o];for(var a in i)({}).hasOwnProperty.call(i,a)&&(s[a]=i[a])}return s},_extends.apply(null,arguments)}const rolling_load=s=>Re.createElement(\"svg\",_extends({xmlns:\"http://www.w3.org/2000/svg\",width:200,height:200,className:\"rolling-load_svg__lds-rolling\",preserveAspectRatio:\"xMidYMid\",style:{backgroundImage:\"none\",backgroundPosition:\"initial initial\",backgroundRepeat:\"initial initial\"},viewBox:\"0 0 100 100\"},s),Qn||(Qn=Re.createElement(\"circle\",{cx:50,cy:50,r:35,fill:\"none\",stroke:\"#555\",strokeDasharray:\"164.93361431346415 56.97787143782138\",strokeWidth:10},Re.createElement(\"animateTransform\",{attributeName:\"transform\",begin:\"0s\",calcMode:\"linear\",dur:\"1s\",keyTimes:\"0;1\",repeatCount:\"indefinite\",type:\"rotate\",values:\"0 50 50;360 50 50\"})))),decodeRefName=s=>{const o=s.replace(/~1/g,\"/\").replace(/~0/g,\"~\");try{return decodeURIComponent(o)}catch{return o}};class Model extends Xn{static propTypes={schema:xn().map.isRequired,getComponent:es().func.isRequired,getConfigs:es().func.isRequired,specSelectors:es().object.isRequired,name:es().string,displayName:es().string,isRef:es().bool,required:es().bool,expandDepth:es().number,depth:es().number,specPath:xn().list.isRequired,includeReadOnly:es().bool,includeWriteOnly:es().bool};getModelName=s=>-1!==s.indexOf(\"#/definitions/\")?decodeRefName(s.replace(/^.*#\\/definitions\\//,\"\")):-1!==s.indexOf(\"#/components/schemas/\")?decodeRefName(s.replace(/^.*#\\/components\\/schemas\\//,\"\")):void 0;getRefSchema=s=>{let{specSelectors:o}=this.props;return o.findDefinition(s)};render(){let{getComponent:s,getConfigs:o,specSelectors:i,schema:a,required:u,name:_,isRef:w,specPath:x,displayName:C,includeReadOnly:j,includeWriteOnly:L}=this.props;const B=s(\"ObjectModel\"),$=s(\"ArrayModel\"),U=s(\"PrimitiveModel\");let V=\"object\",z=a&&a.get(\"$$ref\"),Y=a&&a.get(\"$ref\");if(!_&&z&&(_=this.getModelName(z)),Y){const s=this.getModelName(Y),o=this.getRefSchema(s);ze.Map.isMap(o)?(a=o.mergeDeep(a),z||(a=a.set(\"$$ref\",Y),z=Y)):ze.Map.isMap(a)&&1===a.size&&(a=null,_=Y)}if(!a)return Re.createElement(\"span\",{className:\"model model-title\"},Re.createElement(\"span\",{className:\"model-title__text\"},C||_),!Y&&Re.createElement(rolling_load,{height:\"20px\",width:\"20px\"}));const Z=i.isOAS3()&&a.get(\"deprecated\");switch(w=void 0!==w?w:!!z,V=a&&a.get(\"type\")||V,V){case\"object\":return Re.createElement(B,Mn()({className:\"object\"},this.props,{specPath:x,getConfigs:o,schema:a,name:_,deprecated:Z,isRef:w,includeReadOnly:j,includeWriteOnly:L}));case\"array\":return Re.createElement($,Mn()({className:\"array\"},this.props,{getConfigs:o,schema:a,name:_,deprecated:Z,required:u,includeReadOnly:j,includeWriteOnly:L}));default:return Re.createElement(U,Mn()({},this.props,{getComponent:s,getConfigs:o,schema:a,name:_,deprecated:Z,required:u}))}}}class Models extends Re.Component{getSchemaBasePath=()=>this.props.specSelectors.isOAS3()?[\"components\",\"schemas\"]:[\"definitions\"];getCollapsedContent=()=>\" \";handleToggle=(s,o)=>{const{layoutActions:i}=this.props;i.show([...this.getSchemaBasePath(),s],o),o&&this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(),s])};onLoadModels=s=>{s&&this.props.layoutActions.readyToScroll(this.getSchemaBasePath(),s)};onLoadModel=s=>{if(s){const o=s.getAttribute(\"data-name\");this.props.layoutActions.readyToScroll([...this.getSchemaBasePath(),o],s)}};render(){let{specSelectors:s,getComponent:o,layoutSelectors:i,layoutActions:a,getConfigs:u}=this.props,_=s.definitions(),{docExpansion:w,defaultModelsExpandDepth:x}=u();if(!_.size||x<0)return null;const C=this.getSchemaBasePath();let j=i.isShown(C,x>0&&\"none\"!==w);const L=s.isOAS3(),B=o(\"ModelWrapper\"),$=o(\"Collapse\"),U=o(\"ModelCollapse\"),V=o(\"JumpToPath\",!0),z=o(\"ArrowUpIcon\"),Y=o(\"ArrowDownIcon\");return Re.createElement(\"section\",{className:j?\"models is-open\":\"models\",ref:this.onLoadModels},Re.createElement(\"h4\",null,Re.createElement(\"button\",{\"aria-expanded\":j,className:\"models-control\",onClick:()=>a.show(C,!j)},Re.createElement(\"span\",null,L?\"Schemas\":\"Models\"),j?Re.createElement(z,null):Re.createElement(Y,null))),Re.createElement($,{isOpened:j},_.entrySeq().map((([_])=>{const w=[...C,_],j=We().List(w),L=s.specResolvedSubtree(w),$=s.specJson().getIn(w),z=ze.Map.isMap(L)?L:We().Map(),Y=ze.Map.isMap($)?$:We().Map(),Z=z.get(\"title\")||Y.get(\"title\")||_,ee=i.isShown(w,!1);ee&&0===z.size&&Y.size>0&&this.props.specActions.requestResolvedSubtree(w);const ie=Re.createElement(B,{name:_,expandDepth:x,schema:z||We().Map(),displayName:Z,fullPath:w,specPath:j,getComponent:o,specSelectors:s,getConfigs:u,layoutSelectors:i,layoutActions:a,includeReadOnly:!0,includeWriteOnly:!0}),ae=Re.createElement(\"span\",{className:\"model-box\"},Re.createElement(\"span\",{className:\"model model-title\"},Z));return Re.createElement(\"div\",{id:`model-${_}`,className:\"model-container\",key:`models-section-${_}`,\"data-name\":_,ref:this.onLoadModel},Re.createElement(\"span\",{className:\"models-jump-to-path\"},Re.createElement(V,{path:j})),Re.createElement(U,{classes:\"model-box\",collapsedContent:this.getCollapsedContent(_),onToggle:this.handleToggle,title:ae,displayName:Z,modelName:_,specPath:j,layoutSelectors:i,layoutActions:a,hideSelfOnExpand:!0,expanded:x>0&&ee},ie))})).toArray()))}}const enum_model=({value:s,getComponent:o})=>{let i=o(\"ModelCollapse\"),a=Re.createElement(\"span\",null,\"Array [ \",s.count(),\" ]\");return Re.createElement(\"span\",{className:\"prop-enum\"},\"Enum:\",Re.createElement(\"br\",null),Re.createElement(i,{collapsedContent:a},\"[ \",s.map(String).join(\", \"),\" ]\"))};function isAbsoluteUrl(s){return s.match(/^(?:[a-z]+:)?\\/\\//i)}function buildBaseUrl(s,o){return s?isAbsoluteUrl(s)?function addProtocol(s){return s.match(/^\\/\\//i)?`${window.location.protocol}${s}`:s}(s):new URL(s,o).href:o}function safeBuildUrl(s,o,{selectedServer:i=\"\"}={}){try{return function buildUrl(s,o,{selectedServer:i=\"\"}={}){if(!s)return;if(isAbsoluteUrl(s))return s;const a=buildBaseUrl(i,o);return isAbsoluteUrl(a)?new URL(s,a).href:new URL(s,window.location.href).href}(s,o,{selectedServer:i})}catch{return}}function sanitizeUrl(s){if(\"string\"!=typeof s||\"\"===s.trim())return\"\";const o=s.trim(),i=\"about:blank\";try{const s=`https://base${String(Math.random()).slice(2)}`,a=new URL(o,s),u=a.protocol.slice(0,-1);if([\"javascript\",\"data\",\"vbscript\"].includes(u.toLowerCase()))return i;if(a.origin===s){if(o.startsWith(\"/\"))return`${a.pathname}${a.search}${a.hash}`;if(o.startsWith(\"./\")||o.startsWith(\"../\")){const s=o.match(/^(\\.\\.?\\/)+/)[0];return`${s}${a.pathname.substring(1)}${a.search}${a.hash}`}return`${a.pathname.substring(1)}${a.search}${a.hash}`}return String(a)}catch{return i}}class ObjectModel extends Re.Component{render(){let{schema:s,name:o,displayName:i,isRef:a,getComponent:u,getConfigs:_,depth:w,onToggle:x,expanded:C,specPath:j,...L}=this.props,{specSelectors:B,expandDepth:$,includeReadOnly:U,includeWriteOnly:V}=L;const{isOAS3:z}=B,Y=w>2||2===w&&\"items\"!==j.last();if(!s)return null;const{showExtensions:Z}=_(),ee=Z?getExtensions(s):(0,ze.List)();let ie=s.get(\"description\"),ae=s.get(\"properties\"),ce=s.get(\"additionalProperties\"),le=s.get(\"title\")||i||o,pe=s.get(\"required\"),de=s.filter(((s,o)=>-1!==[\"maxProperties\",\"minProperties\",\"nullable\",\"example\"].indexOf(o))),fe=s.get(\"deprecated\"),ye=s.getIn([\"externalDocs\",\"url\"]),be=s.getIn([\"externalDocs\",\"description\"]);const _e=u(\"JumpToPath\",!0),Se=u(\"Markdown\",!0),we=u(\"Model\"),xe=u(\"ModelCollapse\"),Pe=u(\"Property\"),Te=u(\"Link\"),$e=u(\"ModelExtensions\"),JumpToPathSection=()=>Re.createElement(\"span\",{className:\"model-jump-to-path\"},Re.createElement(_e,{path:j})),qe=Re.createElement(\"span\",null,Re.createElement(\"span\",null,\"{\"),\"...\",Re.createElement(\"span\",null,\"}\"),a?Re.createElement(JumpToPathSection,null):\"\"),We=B.isOAS3()?s.get(\"allOf\"):null,He=B.isOAS3()?s.get(\"anyOf\"):null,Ye=B.isOAS3()?s.get(\"oneOf\"):null,Xe=B.isOAS3()?s.get(\"not\"):null,Qe=le&&Re.createElement(\"span\",{className:\"model-title\"},a&&s.get(\"$$ref\")&&Re.createElement(\"span\",{className:Jn()(\"model-hint\",{\"model-hint--embedded\":Y})},s.get(\"$$ref\")),Re.createElement(\"span\",{className:\"model-title__text\"},le));return Re.createElement(\"span\",{className:\"model\"},Re.createElement(xe,{modelName:o,title:Qe,onToggle:x,expanded:!!C||w<=$,collapsedContent:qe},Re.createElement(\"span\",{className:\"brace-open object\"},\"{\"),a?Re.createElement(JumpToPathSection,null):null,Re.createElement(\"span\",{className:\"inner-object\"},Re.createElement(\"table\",{className:\"model\"},Re.createElement(\"tbody\",null,ie?Re.createElement(\"tr\",{className:\"description\"},Re.createElement(\"td\",null,\"description:\"),Re.createElement(\"td\",null,Re.createElement(Se,{source:ie}))):null,ye&&Re.createElement(\"tr\",{className:\"external-docs\"},Re.createElement(\"td\",null,\"externalDocs:\"),Re.createElement(\"td\",null,Re.createElement(Te,{target:\"_blank\",href:sanitizeUrl(ye)},be||ye))),fe?Re.createElement(\"tr\",{className:\"property\"},Re.createElement(\"td\",null,\"deprecated:\"),Re.createElement(\"td\",null,\"true\")):null,ae&&ae.size?ae.entrySeq().filter((([,s])=>(!s.get(\"readOnly\")||U)&&(!s.get(\"writeOnly\")||V))).map((([s,i])=>{let a=z()&&i.get(\"deprecated\"),x=ze.List.isList(pe)&&pe.contains(s),C=[\"property-row\"];return a&&C.push(\"deprecated\"),x&&C.push(\"required\"),Re.createElement(\"tr\",{key:s,className:C.join(\" \")},Re.createElement(\"td\",null,s,x&&Re.createElement(\"span\",{className:\"star\"},\"*\")),Re.createElement(\"td\",null,Re.createElement(we,Mn()({key:`object-${o}-${s}_${i}`},L,{required:x,getComponent:u,specPath:j.push(\"properties\",s),getConfigs:_,schema:i,depth:w+1}))))})).toArray():null,0===ee.size?null:Re.createElement(Re.Fragment,null,Re.createElement(\"tr\",null,Re.createElement(\"td\",null,\" \")),Re.createElement($e,{extensions:ee,propClass:\"extension\"})),ce&&ce.size?Re.createElement(\"tr\",null,Re.createElement(\"td\",null,\"< * >:\"),Re.createElement(\"td\",null,Re.createElement(we,Mn()({},L,{required:!1,getComponent:u,specPath:j.push(\"additionalProperties\"),getConfigs:_,schema:ce,depth:w+1})))):null,We?Re.createElement(\"tr\",null,Re.createElement(\"td\",null,\"allOf ->\"),Re.createElement(\"td\",null,We.map(((s,o)=>Re.createElement(\"div\",{key:o},Re.createElement(we,Mn()({},L,{required:!1,getComponent:u,specPath:j.push(\"allOf\",o),getConfigs:_,schema:s,depth:w+1}))))))):null,He?Re.createElement(\"tr\",null,Re.createElement(\"td\",null,\"anyOf ->\"),Re.createElement(\"td\",null,He.map(((s,o)=>Re.createElement(\"div\",{key:o},Re.createElement(we,Mn()({},L,{required:!1,getComponent:u,specPath:j.push(\"anyOf\",o),getConfigs:_,schema:s,depth:w+1}))))))):null,Ye?Re.createElement(\"tr\",null,Re.createElement(\"td\",null,\"oneOf ->\"),Re.createElement(\"td\",null,Ye.map(((s,o)=>Re.createElement(\"div\",{key:o},Re.createElement(we,Mn()({},L,{required:!1,getComponent:u,specPath:j.push(\"oneOf\",o),getConfigs:_,schema:s,depth:w+1}))))))):null,Xe?Re.createElement(\"tr\",null,Re.createElement(\"td\",null,\"not ->\"),Re.createElement(\"td\",null,Re.createElement(\"div\",null,Re.createElement(we,Mn()({},L,{required:!1,getComponent:u,specPath:j.push(\"not\"),getConfigs:_,schema:Xe,depth:w+1}))))):null))),Re.createElement(\"span\",{className:\"brace-close\"},\"}\")),de.size?de.entrySeq().map((([s,o])=>Re.createElement(Pe,{key:`${s}-${o}`,propKey:s,propVal:o,propClass:\"property\"}))):null)}}class ArrayModel extends Re.Component{render(){let{getComponent:s,getConfigs:o,schema:i,depth:a,expandDepth:u,name:_,displayName:w,specPath:x}=this.props,C=i.get(\"description\"),j=i.get(\"items\"),L=i.get(\"title\")||w||_,B=i.filter(((s,o)=>-1===[\"type\",\"items\",\"description\",\"$$ref\",\"externalDocs\"].indexOf(o))),$=i.getIn([\"externalDocs\",\"url\"]),U=i.getIn([\"externalDocs\",\"description\"]);const V=s(\"Markdown\",!0),z=s(\"ModelCollapse\"),Y=s(\"Model\"),Z=s(\"Property\"),ee=s(\"Link\"),ie=L&&Re.createElement(\"span\",{className:\"model-title\"},Re.createElement(\"span\",{className:\"model-title__text\"},L));return Re.createElement(\"span\",{className:\"model\"},Re.createElement(z,{title:ie,expanded:a<=u,collapsedContent:\"[...]\"},\"[\",B.size?B.entrySeq().map((([s,o])=>Re.createElement(Z,{key:`${s}-${o}`,propKey:s,propVal:o,propClass:\"property\"}))):null,C?Re.createElement(V,{source:C}):B.size?Re.createElement(\"div\",{className:\"markdown\"}):null,$&&Re.createElement(\"div\",{className:\"external-docs\"},Re.createElement(ee,{target:\"_blank\",href:sanitizeUrl($)},U||$)),Re.createElement(\"span\",null,Re.createElement(Y,Mn()({},this.props,{getConfigs:o,specPath:x.push(\"items\"),name:null,schema:j,required:!1,depth:a+1}))),\"]\"))}}const ts=\"property primitive\";class Primitive extends Re.Component{render(){let{schema:s,getComponent:o,getConfigs:i,name:a,displayName:u,depth:_,expandDepth:w}=this.props;const{showExtensions:x}=i();if(!s||!s.get)return Re.createElement(\"div\",null);let C=s.get(\"type\"),j=s.get(\"format\"),L=s.get(\"xml\"),B=s.get(\"enum\"),$=s.get(\"title\")||u||a,U=s.get(\"description\");const V=getExtensions(s);let z=s.filter(((s,o)=>-1===[\"enum\",\"type\",\"format\",\"description\",\"$$ref\",\"externalDocs\"].indexOf(o))).filterNot(((s,o)=>V.has(o))),Y=s.getIn([\"externalDocs\",\"url\"]),Z=s.getIn([\"externalDocs\",\"description\"]);const ee=o(\"Markdown\",!0),ie=o(\"EnumModel\"),ae=o(\"Property\"),ce=o(\"ModelCollapse\"),le=o(\"Link\"),pe=o(\"ModelExtensions\"),de=$&&Re.createElement(\"span\",{className:\"model-title\"},Re.createElement(\"span\",{className:\"model-title__text\"},$));return Re.createElement(\"span\",{className:\"model\"},Re.createElement(ce,{title:de,expanded:_<=w,collapsedContent:\"[...]\"},Re.createElement(\"span\",{className:\"prop\"},a&&_>1&&Re.createElement(\"span\",{className:\"prop-name\"},$),Re.createElement(\"span\",{className:\"prop-type\"},C),j&&Re.createElement(\"span\",{className:\"prop-format\"},\"($\",j,\")\"),z.size?z.entrySeq().map((([s,o])=>Re.createElement(ae,{key:`${s}-${o}`,propKey:s,propVal:o,propClass:ts}))):null,x&&V.size>0?Re.createElement(pe,{extensions:V,propClass:`${ts} extension`}):null,U?Re.createElement(ee,{source:U}):null,Y&&Re.createElement(\"div\",{className:\"external-docs\"},Re.createElement(le,{target:\"_blank\",href:sanitizeUrl(Y)},Z||Y)),L&&L.size?Re.createElement(\"span\",null,Re.createElement(\"br\",null),Re.createElement(\"span\",{className:ts},\"xml:\"),L.entrySeq().map((([s,o])=>Re.createElement(\"span\",{key:`${s}-${o}`,className:ts},Re.createElement(\"br\",null),\"   \",s,\": \",String(o)))).toArray()):null,B&&Re.createElement(ie,{value:B,getComponent:o}))))}}class Schemes extends Re.Component{UNSAFE_componentWillMount(){let{schemes:s}=this.props;this.setScheme(s.first())}UNSAFE_componentWillReceiveProps(s){this.props.currentScheme&&s.schemes.includes(this.props.currentScheme)||this.setScheme(s.schemes.first())}onChange=s=>{this.setScheme(s.target.value)};setScheme=s=>{let{path:o,method:i,specActions:a}=this.props;a.setScheme(s,o,i)};render(){let{schemes:s,currentScheme:o}=this.props;return Re.createElement(\"label\",{htmlFor:\"schemes\"},Re.createElement(\"span\",{className:\"schemes-title\"},\"Schemes\"),Re.createElement(\"select\",{onChange:this.onChange,value:o,id:\"schemes\"},s.valueSeq().map((s=>Re.createElement(\"option\",{value:s,key:s},s))).toArray()))}}class SchemesContainer extends Re.Component{render(){const{specActions:s,specSelectors:o,getComponent:i}=this.props,a=o.operationScheme(),u=o.schemes(),_=i(\"schemes\");return u&&u.size?Re.createElement(_,{currentScheme:a,schemes:u,specActions:s}):null}}var rs=__webpack_require__(24677),ns=__webpack_require__.n(rs);const ss={value:\"\",onChange:()=>{},schema:{},keyName:\"\",required:!1,errors:(0,ze.List)()};class JsonSchemaForm extends Re.Component{static defaultProps=ss;componentDidMount(){const{dispatchInitialValue:s,value:o,onChange:i}=this.props;s?i(o):!1===s&&i(\"\")}render(){let{schema:s,errors:o,value:i,onChange:a,getComponent:u,fn:_,disabled:w}=this.props;const x=s&&s.get?s.get(\"format\"):null,C=s&&s.get?s.get(\"type\"):null,j=_.getSchemaObjectType(s),L=_.isFileUploadIntended(s);let getComponentSilently=s=>u(s,!1,{failSilently:!0}),B=C?getComponentSilently(x?`JsonSchema_${C}_${x}`:`JsonSchema_${C}`):u(\"JsonSchema_string\");return L||!ze.List.isList(C)||\"array\"!==j&&\"object\"!==j||(B=u(\"JsonSchema_object\")),B||(B=u(\"JsonSchema_string\")),Re.createElement(B,Mn()({},this.props,{errors:o,fn:_,getComponent:u,value:i,onChange:a,schema:s,disabled:w}))}}class JsonSchema_string extends Re.Component{static defaultProps=ss;onChange=s=>{const o=this.props.schema&&\"file\"===this.props.schema.get(\"type\")?s.target.files[0]:s.target.value;this.props.onChange(o,this.props.keyName)};onEnumChange=s=>this.props.onChange(s);render(){let{getComponent:s,value:o,schema:i,errors:a,required:u,description:_,disabled:w}=this.props;const x=i&&i.get?i.get(\"enum\"):null,C=i&&i.get?i.get(\"format\"):null,j=i&&i.get?i.get(\"type\"):null,L=i&&i.get?i.get(\"in\"):null;if(o?(isImmutable(o)||\"object\"==typeof o)&&(o=stringify(o)):o=\"\",a=a.toJS?a.toJS():[],x){const i=s(\"Select\");return Re.createElement(i,{className:a.length?\"invalid\":\"\",title:a.length?a:\"\",allowedValues:[...x],value:o,allowEmptyValue:!u,disabled:w,onChange:this.onEnumChange})}const B=w||L&&\"formData\"===L&&!(\"FormData\"in window),$=s(\"Input\");return j&&\"file\"===j?Re.createElement($,{type:\"file\",className:a.length?\"invalid\":\"\",title:a.length?a:\"\",onChange:this.onChange,disabled:B}):Re.createElement(ns(),{type:C&&\"password\"===C?\"password\":\"text\",className:a.length?\"invalid\":\"\",title:a.length?a:\"\",value:o,minLength:0,debounceTimeout:350,placeholder:_,onChange:this.onChange,disabled:B})}}class JsonSchema_array extends Re.PureComponent{static defaultProps=ss;constructor(s,o){super(s,o),this.state={value:valueOrEmptyList(s.value),schema:s.schema}}UNSAFE_componentWillReceiveProps(s){const o=valueOrEmptyList(s.value);o!==this.state.value&&this.setState({value:o}),s.schema!==this.state.schema&&this.setState({schema:s.schema})}onChange=()=>{this.props.onChange(this.state.value)};onItemChange=(s,o)=>{this.setState((({value:i})=>({value:i.set(o,s)})),this.onChange)};removeItem=s=>{this.setState((({value:o})=>({value:o.delete(s)})),this.onChange)};addItem=()=>{const{fn:s}=this.props;let o=valueOrEmptyList(this.state.value);this.setState((()=>({value:o.push(s.getSampleSchema(this.state.schema.get(\"items\"),!1,{includeWriteOnly:!0}))})),this.onChange)};onEnumChange=s=>{this.setState((()=>({value:s})),this.onChange)};render(){let{getComponent:s,required:o,schema:i,errors:a,fn:u,disabled:_}=this.props;a=a.toJS?a.toJS():Array.isArray(a)?a:[];const w=a.filter((s=>\"string\"==typeof s)),x=a.filter((s=>void 0!==s.needRemove)).map((s=>s.error)),C=this.state.value,j=!!(C&&C.count&&C.count()>0),L=i.getIn([\"items\",\"enum\"]),B=i.get(\"items\"),$=u.getSchemaObjectType(B),U=u.getSchemaObjectTypeLabel(B),V=i.getIn([\"items\",\"format\"]),z=i.get(\"items\");let Y,Z=!1,ee=\"file\"===$||\"string\"===$&&\"binary\"===V;if($&&V?Y=s(`JsonSchema_${$}_${V}`):\"boolean\"!==$&&\"array\"!==$&&\"object\"!==$||(Y=s(`JsonSchema_${$}`)),!ze.List.isList(B?.get(\"type\"))||\"array\"!==$&&\"object\"!==$||(Y=s(\"JsonSchema_object\")),Y||ee||(Z=!0),L){const i=s(\"Select\");return Re.createElement(i,{className:a.length?\"invalid\":\"\",title:a.length?a:\"\",multiple:!0,value:C,disabled:_,allowedValues:L,allowEmptyValue:!o,onChange:this.onEnumChange})}const ie=s(\"Button\");return Re.createElement(\"div\",{className:\"json-schema-array\"},j?C.map(((o,i)=>{const w=(0,ze.fromJS)([...a.filter((s=>s.index===i)).map((s=>s.error))]);return Re.createElement(\"div\",{key:i,className:\"json-schema-form-item\"},ee?Re.createElement(JsonSchemaArrayItemFile,{value:o,onChange:s=>this.onItemChange(s,i),disabled:_,errors:w,getComponent:s}):Z?Re.createElement(JsonSchemaArrayItemText,{value:o,onChange:s=>this.onItemChange(s,i),disabled:_,errors:w}):Re.createElement(Y,Mn()({},this.props,{value:o,onChange:s=>this.onItemChange(s,i),disabled:_,errors:w,schema:z,getComponent:s,fn:u})),_?null:Re.createElement(ie,{className:`btn btn-sm json-schema-form-item-remove ${x.length?\"invalid\":null}`,title:x.length?x:\"\",onClick:()=>this.removeItem(i)},\" - \"))})):null,_?null:Re.createElement(ie,{className:`btn btn-sm json-schema-form-item-add ${w.length?\"invalid\":null}`,title:w.length?w:\"\",onClick:this.addItem},\"Add \",U,\" item\"))}}class JsonSchemaArrayItemText extends Re.Component{static defaultProps=ss;onChange=s=>{const o=s.target.value;this.props.onChange(o,this.props.keyName)};render(){let{value:s,errors:o,description:i,disabled:a}=this.props;return s?(isImmutable(s)||\"object\"==typeof s)&&(s=stringify(s)):s=\"\",o=o.toJS?o.toJS():[],Re.createElement(ns(),{type:\"text\",className:o.length?\"invalid\":\"\",title:o.length?o:\"\",value:s,minLength:0,debounceTimeout:350,placeholder:i,onChange:this.onChange,disabled:a})}}class JsonSchemaArrayItemFile extends Re.Component{static defaultProps=ss;onFileChange=s=>{const o=s.target.files[0];this.props.onChange(o,this.props.keyName)};render(){let{getComponent:s,errors:o,disabled:i}=this.props;const a=s(\"Input\"),u=i||!(\"FormData\"in window);return Re.createElement(a,{type:\"file\",className:o.length?\"invalid\":\"\",title:o.length?o:\"\",onChange:this.onFileChange,disabled:u})}}class JsonSchema_boolean extends Re.Component{static defaultProps=ss;onEnumChange=s=>this.props.onChange(s);render(){let{getComponent:s,value:o,errors:i,schema:a,required:u,disabled:_}=this.props;i=i.toJS?i.toJS():[];let w=a&&a.get?a.get(\"enum\"):null,x=!w||!u,C=!w&&[\"true\",\"false\"];const j=s(\"Select\");return Re.createElement(j,{className:i.length?\"invalid\":\"\",title:i.length?i:\"\",value:String(o),disabled:_,allowedValues:w?[...w]:C,allowEmptyValue:x,onChange:this.onEnumChange})}}const stringifyObjectErrors=s=>s.map((s=>{const o=void 0!==s.propKey?s.propKey:s.index;let i=\"string\"==typeof s?s:\"string\"==typeof s.error?s.error:null;if(!o&&i)return i;let a=s.error,u=`/${s.propKey}`;for(;\"object\"==typeof a;){const s=void 0!==a.propKey?a.propKey:a.index;if(void 0===s)break;if(u+=`/${s}`,!a.error)break;a=a.error}return`${u}: ${a}`}));class JsonSchema_object extends Re.PureComponent{constructor(){super()}static defaultProps=ss;onChange=s=>{this.props.onChange(s)};handleOnChange=s=>{const o=s.target.value;this.onChange(o)};render(){let{getComponent:s,value:o,errors:i,disabled:a}=this.props;const u=s(\"TextArea\");return i=i.toJS?i.toJS():Array.isArray(i)?i:[],Re.createElement(\"div\",null,Re.createElement(u,{className:Jn()({invalid:i.length}),title:i.length?stringifyObjectErrors(i).join(\", \"):\"\",value:stringify(o),disabled:a,onChange:this.handleOnChange}))}}function valueOrEmptyList(s){return ze.List.isList(s)?s:Array.isArray(s)?(0,ze.fromJS)(s):(0,ze.List)()}const ModelExtensions=({extensions:s,propClass:o=\"\"})=>s.entrySeq().map((([s,i])=>{const a=immutableToJS(i)??null;return Re.createElement(\"tr\",{key:s,className:o},Re.createElement(\"td\",null,s),Re.createElement(\"td\",null,JSON.stringify(a)))})).toArray();var os=__webpack_require__(11331),as=__webpack_require__.n(os);const hasSchemaType=(s,o)=>{const i=ze.Map.isMap(s);if(!i&&!as()(s))return!1;const a=i?s.get(\"type\"):s.type;return o===a||Array.isArray(o)&&o.includes(a)},getType=(s,o=new WeakSet)=>{if(null==s)return\"any\";if(o.has(s))return\"any\";o.add(s);const{type:i,items:a}=s;return Object.hasOwn(s,\"items\")?(()=>{if(a)return`array<${getType(a,o)}>`;return\"array<any>\"})():i},getSchemaObjectTypeLabel=s=>getType(immutableToJS(s)),json_schema_5=()=>({components:{modelExample:model_example,ModelWrapper,ModelCollapse,Model,Models,EnumModel:enum_model,ObjectModel,ArrayModel,PrimitiveModel:Primitive,ModelExtensions,schemes:Schemes,SchemesContainer,...U},fn:{hasSchemaType,getSchemaObjectTypeLabel}});var cs=__webpack_require__(19123),ls=__webpack_require__.n(cs),us=__webpack_require__(41859),ps=__webpack_require__.n(us),hs=__webpack_require__(62193),ds=__webpack_require__.n(hs);const shallowArrayEquals=s=>o=>Array.isArray(s)&&Array.isArray(o)&&s.length===o.length&&s.every(((s,i)=>s===o[i])),list=(...s)=>s;class Cache extends Map{delete(s){const o=Array.from(this.keys()).find(shallowArrayEquals(s));return super.delete(o)}get(s){const o=Array.from(this.keys()).find(shallowArrayEquals(s));return super.get(o)}has(s){return-1!==Array.from(this.keys()).findIndex(shallowArrayEquals(s))}}const utils_memoizeN=(s,o=list)=>{const{Cache:i}=pt();pt().Cache=Cache;const a=pt()(s,o);return pt().Cache=i,a},fs={string:s=>s.pattern?(s=>{try{const o=/(?<=(?<!\\\\)\\{)(\\d{3,})(?=\\})|(?<=(?<!\\\\)\\{\\d*,)(\\d{3,})(?=\\})|(?<=(?<!\\\\)\\{)(\\d{3,})(?=,\\d*\\})/g,i=s.replace(o,\"100\"),a=new(ps())(i);return a.max=100,a.gen()}catch(s){return\"string\"}})(s.pattern):\"string\",string_email:()=>\"user@example.com\",\"string_date-time\":()=>(new Date).toISOString(),string_date:()=>(new Date).toISOString().substring(0,10),string_time:()=>(new Date).toISOString().substring(11),string_uuid:()=>\"3fa85f64-5717-4562-b3fc-2c963f66afa6\",string_hostname:()=>\"example.com\",string_ipv4:()=>\"198.51.100.42\",string_ipv6:()=>\"2001:0db8:5b96:0000:0000:426f:8e17:642a\",number:()=>0,number_float:()=>0,integer:()=>0,boolean:s=>\"boolean\"!=typeof s.default||s.default},primitive=s=>{s=objectify(s);let{type:o,format:i}=s,a=fs[`${o}_${i}`]||fs[o];return isFunc(a)?a(s):\"Unknown Type: \"+s.type},sanitizeRef=s=>deeplyStripKey(s,\"$$ref\",(s=>\"string\"==typeof s&&s.indexOf(\"#\")>-1)),ms=[\"maxProperties\",\"minProperties\"],gs=[\"minItems\",\"maxItems\"],ys=[\"minimum\",\"maximum\",\"exclusiveMinimum\",\"exclusiveMaximum\"],vs=[\"minLength\",\"maxLength\"],mergeJsonSchema=(s,o,i={})=>{const a={...s};if([\"example\",\"default\",\"enum\",\"xml\",\"type\",...ms,...gs,...ys,...vs].forEach((s=>(s=>{void 0===a[s]&&void 0!==o[s]&&(a[s]=o[s])})(s))),void 0!==o.required&&Array.isArray(o.required)&&(void 0!==a.required&&a.required.length||(a.required=[]),o.required.forEach((s=>{a.required.includes(s)||a.required.push(s)}))),o.properties){a.properties||(a.properties={});let s=objectify(o.properties);for(let u in s)Object.prototype.hasOwnProperty.call(s,u)&&(s[u]&&s[u].deprecated||s[u]&&s[u].readOnly&&!i.includeReadOnly||s[u]&&s[u].writeOnly&&!i.includeWriteOnly||a.properties[u]||(a.properties[u]=s[u],!o.required&&Array.isArray(o.required)&&-1!==o.required.indexOf(u)&&(a.required?a.required.push(u):a.required=[u])))}return o.items&&(a.items||(a.items={}),a.items=mergeJsonSchema(a.items,o.items,i)),a},sampleFromSchemaGeneric=(s,o={},i=void 0,a=!1)=>{s&&isFunc(s.toJS)&&(s=s.toJS());let u=void 0!==i||s&&void 0!==s.example||s&&void 0!==s.default;const _=!u&&s&&s.oneOf&&s.oneOf.length>0,w=!u&&s&&s.anyOf&&s.anyOf.length>0;if(!u&&(_||w)){const i=objectify(_?s.oneOf[0]:s.anyOf[0]);if(!(s=mergeJsonSchema(s,i,o)).xml&&i.xml&&(s.xml=i.xml),void 0!==s.example&&void 0!==i.example)u=!0;else if(i.properties){s.properties||(s.properties={});let a=objectify(i.properties);for(let u in a)Object.prototype.hasOwnProperty.call(a,u)&&(a[u]&&a[u].deprecated||a[u]&&a[u].readOnly&&!o.includeReadOnly||a[u]&&a[u].writeOnly&&!o.includeWriteOnly||s.properties[u]||(s.properties[u]=a[u],!i.required&&Array.isArray(i.required)&&-1!==i.required.indexOf(u)&&(s.required?s.required.push(u):s.required=[u])))}}const x={};let{xml:C,type:j,example:L,properties:B,additionalProperties:$,items:U}=s||{},{includeReadOnly:V,includeWriteOnly:z}=o;C=C||{};let Y,{name:Z,prefix:ee,namespace:ie}=C,ae={};if(a&&(Z=Z||\"notagname\",Y=(ee?ee+\":\":\"\")+Z,ie)){x[ee?\"xmlns:\"+ee:\"xmlns\"]=ie}a&&(ae[Y]=[]);const schemaHasAny=o=>o.some((o=>Object.prototype.hasOwnProperty.call(s,o)));s&&!j&&(B||$||schemaHasAny(ms)?j=\"object\":U||schemaHasAny(gs)?j=\"array\":schemaHasAny(ys)?(j=\"number\",s.type=\"number\"):u||s.enum||(j=\"string\",s.type=\"string\"));const handleMinMaxItems=o=>{if(null!=s?.maxItems&&(o=o.slice(0,s?.maxItems)),null!=s?.minItems){let i=0;for(;o.length<s?.minItems;)o.push(o[i++%o.length])}return o},ce=objectify(B);let le,pe=0;const hasExceededMaxProperties=()=>s&&null!==s.maxProperties&&void 0!==s.maxProperties&&pe>=s.maxProperties,canAddProperty=o=>!s||null===s.maxProperties||void 0===s.maxProperties||!hasExceededMaxProperties()&&(!(o=>!(s&&s.required&&s.required.length&&s.required.includes(o)))(o)||s.maxProperties-pe-(()=>{if(!s||!s.required)return 0;let o=0;return a?s.required.forEach((s=>o+=void 0===ae[s]?0:1)):s.required.forEach((s=>o+=void 0===ae[Y]?.find((o=>void 0!==o[s]))?0:1)),s.required.length-o})()>0);if(le=a?(i,u=void 0)=>{if(s&&ce[i]){if(ce[i].xml=ce[i].xml||{},ce[i].xml.attribute){const s=Array.isArray(ce[i].enum)?ce[i].enum[0]:void 0,o=ce[i].example,a=ce[i].default;return void(x[ce[i].xml.name||i]=void 0!==o?o:void 0!==a?a:void 0!==s?s:primitive(ce[i]))}ce[i].xml.name=ce[i].xml.name||i}else ce[i]||!1===$||(ce[i]={xml:{name:i}});let _=sampleFromSchemaGeneric(s&&ce[i]||void 0,o,u,a);canAddProperty(i)&&(pe++,Array.isArray(_)?ae[Y]=ae[Y].concat(_):ae[Y].push(_))}:(i,u)=>{if(canAddProperty(i)){if(Object.prototype.hasOwnProperty.call(s,\"discriminator\")&&s.discriminator&&Object.prototype.hasOwnProperty.call(s.discriminator,\"mapping\")&&s.discriminator.mapping&&Object.prototype.hasOwnProperty.call(s,\"$$ref\")&&s.$$ref&&s.discriminator.propertyName===i){for(let o in s.discriminator.mapping)if(-1!==s.$$ref.search(s.discriminator.mapping[o])){ae[i]=o;break}}else ae[i]=sampleFromSchemaGeneric(ce[i],o,u,a);pe++}},u){let u;if(u=sanitizeRef(void 0!==i?i:void 0!==L?L:s.default),!a){if(\"number\"==typeof u&&\"string\"===j)return`${u}`;if(\"string\"!=typeof u||\"string\"===j)return u;try{return JSON.parse(u)}catch(s){return u}}if(s||(j=Array.isArray(u)?\"array\":typeof u),\"array\"===j){if(!Array.isArray(u)){if(\"string\"==typeof u)return u;u=[u]}const i=s?s.items:void 0;i&&(i.xml=i.xml||C||{},i.xml.name=i.xml.name||C.name);let _=u.map((s=>sampleFromSchemaGeneric(i,o,s,a)));return _=handleMinMaxItems(_),C.wrapped?(ae[Y]=_,ds()(x)||ae[Y].push({_attr:x})):ae=_,ae}if(\"object\"===j){if(\"string\"==typeof u)return u;for(let o in u)Object.prototype.hasOwnProperty.call(u,o)&&(s&&ce[o]&&ce[o].readOnly&&!V||s&&ce[o]&&ce[o].writeOnly&&!z||(s&&ce[o]&&ce[o].xml&&ce[o].xml.attribute?x[ce[o].xml.name||o]=u[o]:le(o,u[o])));return ds()(x)||ae[Y].push({_attr:x}),ae}return ae[Y]=ds()(x)?u:[{_attr:x},u],ae}if(\"object\"===j){for(let s in ce)Object.prototype.hasOwnProperty.call(ce,s)&&(ce[s]&&ce[s].deprecated||ce[s]&&ce[s].readOnly&&!V||ce[s]&&ce[s].writeOnly&&!z||le(s));if(a&&x&&ae[Y].push({_attr:x}),hasExceededMaxProperties())return ae;if(!0===$)a?ae[Y].push({additionalProp:\"Anything can be here\"}):ae.additionalProp1={},pe++;else if($){const i=objectify($),u=sampleFromSchemaGeneric(i,o,void 0,a);if(a&&i.xml&&i.xml.name&&\"notagname\"!==i.xml.name)ae[Y].push(u);else{const o=i[\"x-additionalPropertiesName\"]||\"additionalProp\",_=null!==s.minProperties&&void 0!==s.minProperties&&pe<s.minProperties?s.minProperties-pe:3;for(let s=1;s<=_;s++){if(hasExceededMaxProperties())return ae;if(a){const i={};i[o+s]=u.notagname,ae[Y].push(i)}else ae[o+s]=u;pe++}}}return ae}if(\"array\"===j){if(!U)return;let i;if(a&&(U.xml=U.xml||s?.xml||{},U.xml.name=U.xml.name||C.name),Array.isArray(U.anyOf))i=U.anyOf.map((s=>sampleFromSchemaGeneric(mergeJsonSchema(s,U,o),o,void 0,a)));else if(Array.isArray(U.oneOf))i=U.oneOf.map((s=>sampleFromSchemaGeneric(mergeJsonSchema(s,U,o),o,void 0,a)));else{if(!(!a||a&&C.wrapped))return sampleFromSchemaGeneric(U,o,void 0,a);i=[sampleFromSchemaGeneric(U,o,void 0,a)]}return i=handleMinMaxItems(i),a&&C.wrapped?(ae[Y]=i,ds()(x)||ae[Y].push({_attr:x}),ae):i}let de;if(s&&Array.isArray(s.enum))de=normalizeArray(s.enum)[0];else{if(!s)return;if(de=primitive(s),\"number\"==typeof de){let o=s.minimum;null!=o&&(s.exclusiveMinimum&&o++,de=o);let i=s.maximum;null!=i&&(s.exclusiveMaximum&&i--,de=i)}if(\"string\"==typeof de&&(null!==s.maxLength&&void 0!==s.maxLength&&(de=de.slice(0,s.maxLength)),null!==s.minLength&&void 0!==s.minLength)){let o=0;for(;de.length<s.minLength;)de+=de[o++%de.length]}}if(\"file\"!==j)return a?(ae[Y]=ds()(x)?de:[{_attr:x},de],ae):de},inferSchema=s=>(s.schema&&(s=s.schema),s.properties&&(s.type=\"object\"),s),createXMLExample=(s,o,i)=>{const a=sampleFromSchemaGeneric(s,o,i,!0);if(a)return\"string\"==typeof a?a:ls()(a,{declaration:!0,indent:\"\\t\"})},sampleFromSchema=(s,o,i)=>sampleFromSchemaGeneric(s,o,i,!1),resolver=(s,o,i)=>[s,JSON.stringify(o),JSON.stringify(i)],bs=utils_memoizeN(createXMLExample,resolver),_s=utils_memoizeN(sampleFromSchema,resolver),getSchemaObjectType=s=>immutableToJS(s)?.type??\"string\",Ss=[{when:/json/,shouldStringifyTypes:[\"string\"]}],Es=[\"object\"],get_json_sample_schema=s=>(o,i,a,u)=>{const{fn:_}=s(),w=_.memoizedSampleFromSchema(o,i,u),x=typeof w,C=Ss.reduce(((s,o)=>o.when.test(a)?[...s,...o.shouldStringifyTypes]:s),Es);return gt()(C,(s=>s===x))?JSON.stringify(w,null,2):w},get_yaml_sample_schema=s=>(o,i,a,u)=>{const{fn:_}=s(),w=_.getJsonSampleSchema(o,i,a,u);let x;try{x=fn.dump(fn.load(w),{lineWidth:-1},{schema:rn}),\"\\n\"===x[x.length-1]&&(x=x.slice(0,x.length-1))}catch(s){return console.error(s),\"error: could not generate yaml example\"}return x.replace(/\\t/g,\"  \")},get_xml_sample_schema=s=>(o,i,a)=>{const{fn:u}=s();if(o&&!o.xml&&(o.xml={}),o&&!o.xml.name){if(!o.$$ref&&(o.type||o.items||o.properties||o.additionalProperties))return'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n\\x3c!-- XML example cannot be generated; root element name is undefined --\\x3e';if(o.$$ref){let s=o.$$ref.match(/\\S*\\/(\\S+)$/);o.xml.name=s[1]}}return u.memoizedCreateXMLExample(o,i,a)},get_sample_schema=s=>(o,i=\"\",a={},u=void 0)=>{const{fn:_}=s();return\"function\"==typeof o?.toJS&&(o=o.toJS()),\"function\"==typeof u?.toJS&&(u=u.toJS()),/xml/.test(i)?_.getXmlSampleSchema(o,a,u):/(yaml|yml)/.test(i)?_.getYamlSampleSchema(o,a,i,u):_.getJsonSampleSchema(o,a,i,u)},json_schema_5_samples=({getSystem:s})=>{const o=get_json_sample_schema(s),i=get_yaml_sample_schema(s),a=get_xml_sample_schema(s),u=get_sample_schema(s);return{fn:{jsonSchema5:{inferSchema,sampleFromSchema,sampleFromSchemaGeneric,createXMLExample,memoizedSampleFromSchema:_s,memoizedCreateXMLExample:bs,getJsonSampleSchema:o,getYamlSampleSchema:i,getXmlSampleSchema:a,getSampleSchema:u,mergeJsonSchema},inferSchema,sampleFromSchema,sampleFromSchemaGeneric,createXMLExample,memoizedSampleFromSchema:_s,memoizedCreateXMLExample:bs,getJsonSampleSchema:o,getYamlSampleSchema:i,getXmlSampleSchema:a,getSampleSchema:u,mergeJsonSchema,getSchemaObjectType}}};var ws=__webpack_require__(37334),xs=__webpack_require__.n(ws);const ks=[\"get\",\"put\",\"post\",\"delete\",\"options\",\"head\",\"patch\",\"trace\"],spec_selectors_state=s=>s||(0,ze.Map)(),Os=Ut(spec_selectors_state,(s=>s.get(\"lastError\"))),As=Ut(spec_selectors_state,(s=>s.get(\"url\"))),Cs=Ut(spec_selectors_state,(s=>s.get(\"spec\")||\"\")),js=Ut(spec_selectors_state,(s=>s.get(\"specSource\")||\"not-editor\")),Ps=Ut(spec_selectors_state,(s=>s.get(\"json\",(0,ze.Map)()))),Is=Ut(Ps,(s=>s.toJS())),Ts=Ut(spec_selectors_state,(s=>s.get(\"resolved\",(0,ze.Map)()))),specResolvedSubtree=(s,o)=>s.getIn([\"resolvedSubtrees\",...o],void 0),mergerFn=(s,o)=>ze.Map.isMap(s)&&ze.Map.isMap(o)?o.get(\"$$ref\")?o:(0,ze.OrderedMap)().mergeWith(mergerFn,s,o):o,Ns=Ut(spec_selectors_state,(s=>(0,ze.OrderedMap)().mergeWith(mergerFn,s.get(\"json\"),s.get(\"resolvedSubtrees\")))),spec=s=>Ps(s),Ms=Ut(spec,(()=>!1)),Rs=Ut(spec,(s=>returnSelfOrNewMap(s&&s.get(\"info\")))),Ds=Ut(spec,(s=>returnSelfOrNewMap(s&&s.get(\"externalDocs\")))),Ls=Ut(Rs,(s=>s&&s.get(\"version\"))),Fs=Ut(Ls,(s=>/v?([0-9]*)\\.([0-9]*)\\.([0-9]*)/i.exec(s).slice(1))),Bs=Ut(Ns,(s=>s.get(\"paths\"))),$s=xs()([\"get\",\"put\",\"post\",\"delete\",\"options\",\"head\",\"patch\"]),qs=Ut(Bs,(s=>{let o=(0,ze.List)();return!ze.Map.isMap(s)||s.isEmpty()||s.forEach(((s,i)=>{if(!s||!s.forEach)return{};s.forEach(((s,a)=>{ks.indexOf(a)<0||(o=o.push((0,ze.fromJS)({path:i,method:a,operation:s,id:`${a}-${i}`})))}))})),o})),Us=Ut(spec,(s=>(0,ze.Set)(s.get(\"consumes\")))),Vs=Ut(spec,(s=>(0,ze.Set)(s.get(\"produces\")))),zs=Ut(spec,(s=>s.get(\"security\",(0,ze.List)()))),Ws=Ut(spec,(s=>s.get(\"securityDefinitions\"))),findDefinition=(s,o)=>{const i=s.getIn([\"resolvedSubtrees\",\"definitions\",o],null),a=s.getIn([\"json\",\"definitions\",o],null);return i||a||null},Js=Ut(spec,(s=>{const o=s.get(\"definitions\");return ze.Map.isMap(o)?o:(0,ze.Map)()})),Hs=Ut(spec,(s=>s.get(\"basePath\"))),Ks=Ut(spec,(s=>s.get(\"host\"))),Gs=Ut(spec,(s=>s.get(\"schemes\",(0,ze.Map)()))),Ys=Ut([qs,Us,Vs],((s,o,i)=>s.map((s=>s.update(\"operation\",(s=>ze.Map.isMap(s)?s.withMutations((s=>(s.get(\"consumes\")||s.update(\"consumes\",(s=>(0,ze.Set)(s).merge(o))),s.get(\"produces\")||s.update(\"produces\",(s=>(0,ze.Set)(s).merge(i))),s))):(0,ze.Map)())))))),Xs=Ut(spec,(s=>{const o=s.get(\"tags\",(0,ze.List)());return ze.List.isList(o)?o.filter((s=>ze.Map.isMap(s))):(0,ze.List)()})),tagDetails=(s,o)=>(Xs(s)||(0,ze.List)()).filter(ze.Map.isMap).find((s=>s.get(\"name\")===o),(0,ze.Map)()),Qs=Ut(Ys,Xs,((s,o)=>s.reduce(((s,o)=>{let i=(0,ze.Set)(o.getIn([\"operation\",\"tags\"]));return i.count()<1?s.update(\"default\",(0,ze.List)(),(s=>s.push(o))):i.reduce(((s,i)=>s.update(i,(0,ze.List)(),(s=>s.push(o)))),s)}),o.reduce(((s,o)=>s.set(o.get(\"name\"),(0,ze.List)())),(0,ze.OrderedMap)())))),selectors_taggedOperations=s=>({getConfigs:o})=>{let{tagsSorter:i,operationsSorter:a}=o();return Qs(s).sortBy(((s,o)=>o),((s,o)=>{let a=\"function\"==typeof i?i:It.tagsSorter[i];return a?a(s,o):null})).map(((o,i)=>{let u=\"function\"==typeof a?a:It.operationsSorter[a],_=u?o.sort(u):o;return(0,ze.Map)({tagDetails:tagDetails(s,i),operations:_})}))},Zs=Ut(spec_selectors_state,(s=>s.get(\"responses\",(0,ze.Map)()))),eo=Ut(spec_selectors_state,(s=>s.get(\"requests\",(0,ze.Map)()))),to=Ut(spec_selectors_state,(s=>s.get(\"mutatedRequests\",(0,ze.Map)()))),responseFor=(s,o,i)=>Zs(s).getIn([o,i],null),requestFor=(s,o,i)=>eo(s).getIn([o,i],null),mutatedRequestFor=(s,o,i)=>to(s).getIn([o,i],null),allowTryItOutFor=()=>!0,parameterWithMetaByIdentity=(s,o,i)=>{const a=Ns(s).getIn([\"paths\",...o,\"parameters\"],(0,ze.OrderedMap)()),u=s.getIn([\"meta\",\"paths\",...o,\"parameters\"],(0,ze.OrderedMap)());return a.map((s=>{const o=u.get(`${i.get(\"in\")}.${i.get(\"name\")}`),a=u.get(`${i.get(\"in\")}.${i.get(\"name\")}.hash-${i.hashCode()}`);return(0,ze.OrderedMap)().merge(s,o,a)})).find((s=>s.get(\"in\")===i.get(\"in\")&&s.get(\"name\")===i.get(\"name\")),(0,ze.OrderedMap)())},parameterInclusionSettingFor=(s,o,i,a)=>{const u=`${a}.${i}`;return s.getIn([\"meta\",\"paths\",...o,\"parameter_inclusions\",u],!1)},parameterWithMeta=(s,o,i,a)=>{const u=Ns(s).getIn([\"paths\",...o,\"parameters\"],(0,ze.OrderedMap)()).find((s=>s.get(\"in\")===a&&s.get(\"name\")===i),(0,ze.OrderedMap)());return parameterWithMetaByIdentity(s,o,u)},operationWithMeta=(s,o,i)=>{const a=Ns(s).getIn([\"paths\",o,i],(0,ze.OrderedMap)()),u=s.getIn([\"meta\",\"paths\",o,i],(0,ze.OrderedMap)()),_=a.get(\"parameters\",(0,ze.List)()).map((a=>parameterWithMetaByIdentity(s,[o,i],a)));return(0,ze.OrderedMap)().merge(a,u).set(\"parameters\",_)};function getParameter(s,o,i,a){return o=o||[],s.getIn([\"meta\",\"paths\",...o,\"parameters\"],(0,ze.fromJS)([])).find((s=>ze.Map.isMap(s)&&s.get(\"name\")===i&&s.get(\"in\")===a))||(0,ze.Map)()}const ro=Ut(spec,(s=>{const o=s.get(\"host\");return\"string\"==typeof o&&o.length>0&&\"/\"!==o[0]}));function parameterValues(s,o,i){return o=o||[],operationWithMeta(s,...o).get(\"parameters\",(0,ze.List)()).reduce(((s,o)=>{let a=i&&\"body\"===o.get(\"in\")?o.get(\"value_xml\"):o.get(\"value\");return ze.List.isList(a)&&(a=a.filter((s=>\"\"!==s))),s.set(paramToIdentifier(o,{allowHashes:!1}),a)}),(0,ze.fromJS)({}))}function parametersIncludeIn(s,o=\"\"){if(ze.List.isList(s))return s.some((s=>ze.Map.isMap(s)&&s.get(\"in\")===o))}function parametersIncludeType(s,o=\"\"){if(ze.List.isList(s))return s.some((s=>ze.Map.isMap(s)&&s.get(\"type\")===o))}function contentTypeValues(s,o){o=o||[];let i=Ns(s).getIn([\"paths\",...o],(0,ze.fromJS)({})),a=s.getIn([\"meta\",\"paths\",...o],(0,ze.fromJS)({})),u=currentProducesFor(s,o);const _=i.get(\"parameters\")||new ze.List,w=a.get(\"consumes_value\")?a.get(\"consumes_value\"):parametersIncludeType(_,\"file\")?\"multipart/form-data\":parametersIncludeType(_,\"formData\")?\"application/x-www-form-urlencoded\":void 0;return(0,ze.fromJS)({requestContentType:w,responseContentType:u})}function currentProducesFor(s,o){o=o||[];const i=Ns(s).getIn([\"paths\",...o],null);if(null===i)return;const a=s.getIn([\"meta\",\"paths\",...o,\"produces_value\"],null),u=i.getIn([\"produces\",0],null);return a||u||\"application/json\"}function producesOptionsFor(s,o){o=o||[];const i=Ns(s),a=i.getIn([\"paths\",...o],null);if(null===a)return;const[u]=o,_=a.get(\"produces\",null),w=i.getIn([\"paths\",u,\"produces\"],null),x=i.getIn([\"produces\"],null);return _||w||x}function consumesOptionsFor(s,o){o=o||[];const i=Ns(s),a=i.getIn([\"paths\",...o],null);if(null===a)return;const[u]=o,_=a.get(\"consumes\",null),w=i.getIn([\"paths\",u,\"consumes\"],null),x=i.getIn([\"consumes\"],null);return _||w||x}const operationScheme=(s,o,i)=>{let a=s.get(\"url\").match(/^([a-z][a-z0-9+\\-.]*):/),u=Array.isArray(a)?a[1]:null;return s.getIn([\"scheme\",o,i])||s.getIn([\"scheme\",\"_defaultScheme\"])||u||\"\"},canExecuteScheme=(s,o,i)=>[\"http\",\"https\"].indexOf(operationScheme(s,o,i))>-1,validationErrors=(s,o)=>{o=o||[];const i=s.getIn([\"meta\",\"paths\",...o,\"parameters\"],(0,ze.fromJS)([])),a=[];if(0===i.length)return a;const getErrorsWithPaths=(s,o=[])=>{const getNestedErrorsWithPaths=(s,o)=>{const i=[...o,s.get(\"propKey\")||s.get(\"index\")];return ze.Map.isMap(s.get(\"error\"))?getErrorsWithPaths(s.get(\"error\"),i):{error:s.get(\"error\"),path:i}};return ze.List.isList(s)?s.map((s=>ze.Map.isMap(s)?getNestedErrorsWithPaths(s,o):{error:s,path:o})):getNestedErrorsWithPaths(s,o)};return i.forEach(((s,o)=>{const i=o.split(\".\").slice(1,-1).join(\".\"),u=s.get(\"errors\");if(u&&u.count()){getErrorsWithPaths(u).forEach((({error:s,path:o})=>{a.push(((s,o,i)=>`For '${i}'${(o=o.reduce(((s,o)=>\"number\"==typeof o?`${s}[${o}]`:s?`${s}.${o}`:o),\"\"))?` at path '${o}'`:\"\"}: ${s}.`)(s,o,i))}))}})),a},validateBeforeExecute=(s,o)=>0===validationErrors(s,o).length,getOAS3RequiredRequestBodyContentType=(s,o)=>{let i={requestBody:!1,requestContentType:{}},a=s.getIn([\"resolvedSubtrees\",\"paths\",...o,\"requestBody\"],(0,ze.fromJS)([]));return a.size<1||(a.getIn([\"required\"])&&(i.requestBody=a.getIn([\"required\"])),a.getIn([\"content\"]).entrySeq().forEach((s=>{const o=s[0];if(s[1].getIn([\"schema\",\"required\"])){const a=s[1].getIn([\"schema\",\"required\"]).toJS();i.requestContentType[o]=a}}))),i},isMediaTypeSchemaPropertiesEqual=(s,o,i,a)=>{if((i||a)&&i===a)return!0;let u=s.getIn([\"resolvedSubtrees\",\"paths\",...o,\"requestBody\",\"content\"],(0,ze.fromJS)([]));if(u.size<2||!i||!a)return!1;let _=u.getIn([i,\"schema\",\"properties\"],(0,ze.fromJS)([])),w=u.getIn([a,\"schema\",\"properties\"],(0,ze.fromJS)([]));return!!_.equals(w)};function returnSelfOrNewMap(s){return ze.Map.isMap(s)?s:new ze.Map}var no=__webpack_require__(85015),so=__webpack_require__.n(no),oo=__webpack_require__(38221),io=__webpack_require__.n(oo),ao=__webpack_require__(63560),co=__webpack_require__.n(ao),lo=__webpack_require__(56367),uo=__webpack_require__.n(lo);const po=\"spec_update_spec\",ho=\"spec_update_url\",fo=\"spec_update_json\",mo=\"spec_update_param\",go=\"spec_update_empty_param_inclusion\",yo=\"spec_validate_param\",vo=\"spec_set_response\",bo=\"spec_set_request\",_o=\"spec_set_mutated_request\",So=\"spec_log_request\",Eo=\"spec_clear_response\",wo=\"spec_clear_request\",xo=\"spec_clear_validate_param\",ko=\"spec_update_operation_meta_value\",Oo=\"spec_update_resolved\",Ao=\"spec_update_resolved_subtree\",Co=\"set_scheme\",toStr=s=>so()(s)?s:\"\";function updateSpec(s){const o=toStr(s).replace(/\\t/g,\"  \");if(\"string\"==typeof s)return{type:po,payload:o}}function updateResolved(s){return{type:Oo,payload:s}}function updateUrl(s){return{type:ho,payload:s}}function updateJsonSpec(s){return{type:fo,payload:s}}const parseToJson=s=>({specActions:o,specSelectors:i,errActions:a})=>{let{specStr:u}=i,_=null;try{s=s||u(),a.clear({source:\"parser\"}),_=fn.load(s,{schema:rn})}catch(s){return console.error(s),a.newSpecErr({source:\"parser\",level:\"error\",message:s.reason,line:s.mark&&s.mark.line?s.mark.line+1:void 0})}return _&&\"object\"==typeof _?o.updateJsonSpec(_):o.updateJsonSpec({})};let jo=!1;const resolveSpec=(s,o)=>({specActions:i,specSelectors:a,errActions:u,fn:{fetch:_,resolve:w,AST:x={}},getConfigs:C})=>{jo||(console.warn(\"specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use requestResolvedSubtree instead!\"),jo=!0);const{modelPropertyMacro:j,parameterMacro:L,requestInterceptor:B,responseInterceptor:$}=C();void 0===s&&(s=a.specJson()),void 0===o&&(o=a.url());let U=x.getLineNumberForPath?x.getLineNumberForPath:()=>{},V=a.specStr();return w({fetch:_,spec:s,baseDoc:String(new URL(o,document.baseURI)),modelPropertyMacro:j,parameterMacro:L,requestInterceptor:B,responseInterceptor:$}).then((({spec:s,errors:o})=>{if(u.clear({type:\"thrown\"}),Array.isArray(o)&&o.length>0){let s=o.map((s=>(console.error(s),s.line=s.fullPath?U(V,s.fullPath):null,s.path=s.fullPath?s.fullPath.join(\".\"):null,s.level=\"error\",s.type=\"thrown\",s.source=\"resolver\",Object.defineProperty(s,\"message\",{enumerable:!0,value:s.message}),s)));u.newThrownErrBatch(s)}return i.updateResolved(s)}))};let Po=[];const Io=io()((()=>{const s=Po.reduce(((s,{path:o,system:i})=>(s.has(i)||s.set(i,[]),s.get(i).push(o),s)),new Map);Po=[],s.forEach((async(s,o)=>{if(!o)return void console.error(\"debResolveSubtrees: don't have a system to operate on, aborting.\");if(!o.fn.resolveSubtree)return void console.error(\"Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing.\");const{errActions:i,errSelectors:a,fn:{resolveSubtree:u,fetch:_,AST:w={}},specSelectors:x,specActions:C}=o,j=w.getLineNumberForPath??xs()(void 0),L=x.specStr(),{modelPropertyMacro:B,parameterMacro:$,requestInterceptor:U,responseInterceptor:V}=o.getConfigs();try{const o=await s.reduce((async(s,o)=>{let{resultMap:w,specWithCurrentSubtrees:C}=await s;const{errors:z,spec:Y}=await u(C,o,{baseDoc:String(new URL(x.url(),document.baseURI)),modelPropertyMacro:B,parameterMacro:$,requestInterceptor:U,responseInterceptor:V});if(a.allErrors().size&&i.clearBy((s=>\"thrown\"!==s.get(\"type\")||\"resolver\"!==s.get(\"source\")||!s.get(\"fullPath\")?.every(((s,i)=>s===o[i]||void 0===o[i])))),Array.isArray(z)&&z.length>0){let s=z.map((s=>(s.line=s.fullPath?j(L,s.fullPath):null,s.path=s.fullPath?s.fullPath.join(\".\"):null,s.level=\"error\",s.type=\"thrown\",s.source=\"resolver\",Object.defineProperty(s,\"message\",{enumerable:!0,value:s.message}),s)));i.newThrownErrBatch(s)}return Y&&x.isOAS3()&&\"components\"===o[0]&&\"securitySchemes\"===o[1]&&await Promise.all(Object.values(Y).filter((s=>\"openIdConnect\"===s?.type)).map((async s=>{const o={url:s.openIdConnectUrl,requestInterceptor:U,responseInterceptor:V};try{const i=await _(o);i instanceof Error||i.status>=400?console.error(i.statusText+\" \"+o.url):s.openIdConnectData=JSON.parse(i.text)}catch(s){console.error(s)}}))),co()(w,o,Y),C=uo()(o,Y,C),{resultMap:w,specWithCurrentSubtrees:C}}),Promise.resolve({resultMap:(x.specResolvedSubtree([])||(0,ze.Map)()).toJS(),specWithCurrentSubtrees:x.specJS()}));C.updateResolvedSubtree([],o.resultMap)}catch(s){console.error(s)}}))}),35),requestResolvedSubtree=s=>o=>{Po.find((({path:i,system:a})=>a===o&&i.toString()===s.toString()))||(Po.push({path:s,system:o}),Io())};function changeParam(s,o,i,a,u){return{type:mo,payload:{path:s,value:a,paramName:o,paramIn:i,isXml:u}}}function changeParamByIdentity(s,o,i,a){return{type:mo,payload:{path:s,param:o,value:i,isXml:a}}}const updateResolvedSubtree=(s,o)=>({type:Ao,payload:{path:s,value:o}}),invalidateResolvedSubtreeCache=()=>({type:Ao,payload:{path:[],value:(0,ze.Map)()}}),validateParams=(s,o)=>({type:yo,payload:{pathMethod:s,isOAS3:o}}),updateEmptyParamInclusion=(s,o,i,a)=>({type:go,payload:{pathMethod:s,paramName:o,paramIn:i,includeEmptyValue:a}});function clearValidateParams(s){return{type:xo,payload:{pathMethod:s}}}function changeConsumesValue(s,o){return{type:ko,payload:{path:s,value:o,key:\"consumes_value\"}}}function changeProducesValue(s,o){return{type:ko,payload:{path:s,value:o,key:\"produces_value\"}}}const setResponse=(s,o,i)=>({payload:{path:s,method:o,res:i},type:vo}),setRequest=(s,o,i)=>({payload:{path:s,method:o,req:i},type:bo}),setMutatedRequest=(s,o,i)=>({payload:{path:s,method:o,req:i},type:_o}),logRequest=s=>({payload:s,type:So}),executeRequest=s=>({fn:o,specActions:i,specSelectors:a,getConfigs:u,oas3Selectors:_})=>{let{pathName:w,method:x,operation:C}=s,{requestInterceptor:j,responseInterceptor:L}=u(),B=C.toJS();if(C&&C.get(\"parameters\")&&C.get(\"parameters\").filter((s=>s&&!0===s.get(\"allowEmptyValue\"))).forEach((o=>{if(a.parameterInclusionSettingFor([w,x],o.get(\"name\"),o.get(\"in\"))){s.parameters=s.parameters||{};const i=paramToValue(o,s.parameters);(!i||i&&0===i.size)&&(s.parameters[o.get(\"name\")]=\"\")}})),s.contextUrl=Nt()(a.url()).toString(),B&&B.operationId?s.operationId=B.operationId:B&&w&&x&&(s.operationId=o.opId(B,w,x)),a.isOAS3()){const o=`${w}:${x}`;s.server=_.selectedServer(o)||_.selectedServer();const i=_.serverVariables({server:s.server,namespace:o}).toJS(),a=_.serverVariables({server:s.server}).toJS();s.serverVariables=Object.keys(i).length?i:a,s.requestContentType=_.requestContentType(w,x),s.responseContentType=_.responseContentType(w,x)||\"*/*\";const u=_.requestBodyValue(w,x),C=_.requestBodyInclusionSetting(w,x);u&&u.toJS?s.requestBody=u.map((s=>ze.Map.isMap(s)?s.get(\"value\"):s)).filter(((s,o)=>(Array.isArray(s)?0!==s.length:!isEmptyValue(s))||C.get(o))).toJS():s.requestBody=u}let $=Object.assign({},s);$=o.buildRequest($),i.setRequest(s.pathName,s.method,$);s.requestInterceptor=async o=>{let a=await j.apply(void 0,[o]),u=Object.assign({},a);return i.setMutatedRequest(s.pathName,s.method,u),a},s.responseInterceptor=L;const U=Date.now();return o.execute(s).then((o=>{o.duration=Date.now()-U,i.setResponse(s.pathName,s.method,o)})).catch((o=>{\"Failed to fetch\"===o.message&&(o.name=\"\",o.message='**Failed to fetch.**  \\n**Possible Reasons:** \\n  - CORS \\n  - Network Failure \\n  - URL scheme must be \"http\" or \"https\" for CORS request.'),i.setResponse(s.pathName,s.method,{error:!0,err:o})}))},actions_execute=({path:s,method:o,...i}={})=>a=>{let{fn:{fetch:u},specSelectors:_,specActions:w}=a,x=_.specJsonWithResolvedSubtrees().toJS(),C=_.operationScheme(s,o),{requestContentType:j,responseContentType:L}=_.contentTypeValues([s,o]).toJS(),B=/xml/i.test(j),$=_.parameterValues([s,o],B).toJS();return w.executeRequest({...i,fetch:u,spec:x,pathName:s,method:o,parameters:$,requestContentType:j,scheme:C,responseContentType:L})};function clearResponse(s,o){return{type:Eo,payload:{path:s,method:o}}}function clearRequest(s,o){return{type:wo,payload:{path:s,method:o}}}function setScheme(s,o,i){return{type:Co,payload:{scheme:s,path:o,method:i}}}const To={[po]:(s,o)=>\"string\"==typeof o.payload?s.set(\"spec\",o.payload):s,[ho]:(s,o)=>s.set(\"url\",o.payload+\"\"),[fo]:(s,o)=>s.set(\"json\",fromJSOrdered(o.payload)),[Oo]:(s,o)=>s.setIn([\"resolved\"],fromJSOrdered(o.payload)),[Ao]:(s,o)=>{const{value:i,path:a}=o.payload;return s.setIn([\"resolvedSubtrees\",...a],fromJSOrdered(i))},[mo]:(s,{payload:o})=>{let{path:i,paramName:a,paramIn:u,param:_,value:w,isXml:x}=o,C=_?paramToIdentifier(_):`${u}.${a}`;const j=x?\"value_xml\":\"value\";return s.setIn([\"meta\",\"paths\",...i,\"parameters\",C,j],(0,ze.fromJS)(w))},[go]:(s,{payload:o})=>{let{pathMethod:i,paramName:a,paramIn:u,includeEmptyValue:_}=o;if(!a||!u)return console.warn(\"Warning: UPDATE_EMPTY_PARAM_INCLUSION could not generate a paramKey.\"),s;const w=`${u}.${a}`;return s.setIn([\"meta\",\"paths\",...i,\"parameter_inclusions\",w],_)},[yo]:(s,{payload:{pathMethod:o,isOAS3:i}})=>{const a=Ns(s).getIn([\"paths\",...o]),u=parameterValues(s,o).toJS();return s.updateIn([\"meta\",\"paths\",...o,\"parameters\"],(0,ze.fromJS)({}),(_=>a.get(\"parameters\",(0,ze.List)()).reduce(((a,_)=>{const w=paramToValue(_,u),x=parameterInclusionSettingFor(s,o,_.get(\"name\"),_.get(\"in\")),C=((s,o,{isOAS3:i=!1,bypassRequiredCheck:a=!1}={})=>{let u=s.get(\"required\"),{schema:_,parameterContentMediaType:w}=getParameterSchema(s,{isOAS3:i});return validateValueBySchema(o,_,u,a,w)})(_,w,{bypassRequiredCheck:x,isOAS3:i});return a.setIn([paramToIdentifier(_),\"errors\"],(0,ze.fromJS)(C))}),_)))},[xo]:(s,{payload:{pathMethod:o}})=>s.updateIn([\"meta\",\"paths\",...o,\"parameters\"],(0,ze.fromJS)([]),(s=>s.map((s=>s.set(\"errors\",(0,ze.fromJS)([])))))),[vo]:(s,{payload:{res:o,path:i,method:a}})=>{let u;u=o.error?Object.assign({error:!0,name:o.err.name,message:o.err.message,statusCode:o.err.statusCode},o.err.response):o,u.headers=u.headers||{};let _=s.setIn([\"responses\",i,a],fromJSOrdered(u));return lt.Blob&&u.data instanceof lt.Blob&&(_=_.setIn([\"responses\",i,a,\"text\"],u.data)),_},[bo]:(s,{payload:{req:o,path:i,method:a}})=>s.setIn([\"requests\",i,a],fromJSOrdered(o)),[_o]:(s,{payload:{req:o,path:i,method:a}})=>s.setIn([\"mutatedRequests\",i,a],fromJSOrdered(o)),[ko]:(s,{payload:{path:o,value:i,key:a}})=>{let u=[\"paths\",...o],_=[\"meta\",\"paths\",...o];return s.getIn([\"json\",...u])||s.getIn([\"resolved\",...u])||s.getIn([\"resolvedSubtrees\",...u])?s.setIn([..._,a],(0,ze.fromJS)(i)):s},[Eo]:(s,{payload:{path:o,method:i}})=>s.deleteIn([\"responses\",o,i]),[wo]:(s,{payload:{path:o,method:i}})=>s.deleteIn([\"requests\",o,i]),[Co]:(s,{payload:{scheme:o,path:i,method:a}})=>i&&a?s.setIn([\"scheme\",i,a],o):i||a?void 0:s.setIn([\"scheme\",\"_defaultScheme\"],o)},wrap_actions_updateSpec=(s,{specActions:o})=>(...i)=>{s(...i),o.parseToJson(...i)},wrap_actions_updateJsonSpec=(s,{specActions:o})=>(...i)=>{s(...i),o.invalidateResolvedSubtreeCache();const[a]=i,u=Cn()(a,[\"paths\"])||{};Object.keys(u).forEach((s=>{const i=Cn()(u,[s]);as()(i)&&i.$ref&&o.requestResolvedSubtree([\"paths\",s])})),o.requestResolvedSubtree([\"components\",\"securitySchemes\"])},wrap_actions_executeRequest=(s,{specActions:o})=>i=>(o.logRequest(i),s(i)),wrap_actions_validateParams=(s,{specSelectors:o})=>i=>s(i,o.isOAS3()),plugins_spec=()=>({statePlugins:{spec:{wrapActions:{...Y},reducers:{...To},actions:{...z},selectors:{...V}}}});var No=function(){var extendStatics=function(s,o){return extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,o){s.__proto__=o}||function(s,o){for(var i in o)o.hasOwnProperty(i)&&(s[i]=o[i])},extendStatics(s,o)};return function(s,o){function __(){this.constructor=s}extendStatics(s,o),s.prototype=null===o?Object.create(o):(__.prototype=o.prototype,new __)}}(),Mo=Object.prototype.hasOwnProperty;function module_helpers_hasOwnProperty(s,o){return Mo.call(s,o)}function _objectKeys(s){if(Array.isArray(s)){for(var o=new Array(s.length),i=0;i<o.length;i++)o[i]=\"\"+i;return o}if(Object.keys)return Object.keys(s);var a=[];for(var u in s)module_helpers_hasOwnProperty(s,u)&&a.push(u);return a}function _deepClone(s){switch(typeof s){case\"object\":return JSON.parse(JSON.stringify(s));case\"undefined\":return null;default:return s}}function helpers_isInteger(s){for(var o,i=0,a=s.length;i<a;){if(!((o=s.charCodeAt(i))>=48&&o<=57))return!1;i++}return!0}function escapePathComponent(s){return-1===s.indexOf(\"/\")&&-1===s.indexOf(\"~\")?s:s.replace(/~/g,\"~0\").replace(/\\//g,\"~1\")}function unescapePathComponent(s){return s.replace(/~1/g,\"/\").replace(/~0/g,\"~\")}function hasUndefined(s){if(void 0===s)return!0;if(s)if(Array.isArray(s)){for(var o=0,i=s.length;o<i;o++)if(hasUndefined(s[o]))return!0}else if(\"object\"==typeof s)for(var a=_objectKeys(s),u=a.length,_=0;_<u;_++)if(hasUndefined(s[a[_]]))return!0;return!1}function patchErrorMessageFormatter(s,o){var i=[s];for(var a in o){var u=\"object\"==typeof o[a]?JSON.stringify(o[a],null,2):o[a];void 0!==u&&i.push(a+\": \"+u)}return i.join(\"\\n\")}var Ro=function(s){function PatchError(o,i,a,u,_){var w=this.constructor,x=s.call(this,patchErrorMessageFormatter(o,{name:i,index:a,operation:u,tree:_}))||this;return x.name=i,x.index=a,x.operation=u,x.tree=_,Object.setPrototypeOf(x,w.prototype),x.message=patchErrorMessageFormatter(o,{name:i,index:a,operation:u,tree:_}),x}return No(PatchError,s),PatchError}(Error),Do=Ro,Lo=_deepClone,Fo={add:function(s,o,i){return s[o]=this.value,{newDocument:i}},remove:function(s,o,i){var a=s[o];return delete s[o],{newDocument:i,removed:a}},replace:function(s,o,i){var a=s[o];return s[o]=this.value,{newDocument:i,removed:a}},move:function(s,o,i){var a=getValueByPointer(i,this.path);a&&(a=_deepClone(a));var u=applyOperation(i,{op:\"remove\",path:this.from}).removed;return applyOperation(i,{op:\"add\",path:this.path,value:u}),{newDocument:i,removed:a}},copy:function(s,o,i){var a=getValueByPointer(i,this.from);return applyOperation(i,{op:\"add\",path:this.path,value:_deepClone(a)}),{newDocument:i}},test:function(s,o,i){return{newDocument:i,test:_areEquals(s[o],this.value)}},_get:function(s,o,i){return this.value=s[o],{newDocument:i}}},Bo={add:function(s,o,i){return helpers_isInteger(o)?s.splice(o,0,this.value):s[o]=this.value,{newDocument:i,index:o}},remove:function(s,o,i){return{newDocument:i,removed:s.splice(o,1)[0]}},replace:function(s,o,i){var a=s[o];return s[o]=this.value,{newDocument:i,removed:a}},move:Fo.move,copy:Fo.copy,test:Fo.test,_get:Fo._get};function getValueByPointer(s,o){if(\"\"==o)return s;var i={op:\"_get\",path:o};return applyOperation(s,i),i.value}function applyOperation(s,o,i,a,u,_){if(void 0===i&&(i=!1),void 0===a&&(a=!0),void 0===u&&(u=!0),void 0===_&&(_=0),i&&(\"function\"==typeof i?i(o,0,s,o.path):validator(o,0)),\"\"===o.path){var w={newDocument:s};if(\"add\"===o.op)return w.newDocument=o.value,w;if(\"replace\"===o.op)return w.newDocument=o.value,w.removed=s,w;if(\"move\"===o.op||\"copy\"===o.op)return w.newDocument=getValueByPointer(s,o.from),\"move\"===o.op&&(w.removed=s),w;if(\"test\"===o.op){if(w.test=_areEquals(s,o.value),!1===w.test)throw new Do(\"Test operation failed\",\"TEST_OPERATION_FAILED\",_,o,s);return w.newDocument=s,w}if(\"remove\"===o.op)return w.removed=s,w.newDocument=null,w;if(\"_get\"===o.op)return o.value=s,w;if(i)throw new Do(\"Operation `op` property is not one of operations defined in RFC-6902\",\"OPERATION_OP_INVALID\",_,o,s);return w}a||(s=_deepClone(s));var x=(o.path||\"\").split(\"/\"),C=s,j=1,L=x.length,B=void 0,$=void 0,U=void 0;for(U=\"function\"==typeof i?i:validator;;){if(($=x[j])&&-1!=$.indexOf(\"~\")&&($=unescapePathComponent($)),u&&(\"__proto__\"==$||\"prototype\"==$&&j>0&&\"constructor\"==x[j-1]))throw new TypeError(\"JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README\");if(i&&void 0===B&&(void 0===C[$]?B=x.slice(0,j).join(\"/\"):j==L-1&&(B=o.path),void 0!==B&&U(o,0,s,B)),j++,Array.isArray(C)){if(\"-\"===$)$=C.length;else{if(i&&!helpers_isInteger($))throw new Do(\"Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index\",\"OPERATION_PATH_ILLEGAL_ARRAY_INDEX\",_,o,s);helpers_isInteger($)&&($=~~$)}if(j>=L){if(i&&\"add\"===o.op&&$>C.length)throw new Do(\"The specified index MUST NOT be greater than the number of elements in the array\",\"OPERATION_VALUE_OUT_OF_BOUNDS\",_,o,s);if(!1===(w=Bo[o.op].call(o,C,$,s)).test)throw new Do(\"Test operation failed\",\"TEST_OPERATION_FAILED\",_,o,s);return w}}else if(j>=L){if(!1===(w=Fo[o.op].call(o,C,$,s)).test)throw new Do(\"Test operation failed\",\"TEST_OPERATION_FAILED\",_,o,s);return w}if(C=C[$],i&&j<L&&(!C||\"object\"!=typeof C))throw new Do(\"Cannot perform operation at the desired path\",\"OPERATION_PATH_UNRESOLVABLE\",_,o,s)}}function applyPatch(s,o,i,a,u){if(void 0===a&&(a=!0),void 0===u&&(u=!0),i&&!Array.isArray(o))throw new Do(\"Patch sequence must be an array\",\"SEQUENCE_NOT_AN_ARRAY\");a||(s=_deepClone(s));for(var _=new Array(o.length),w=0,x=o.length;w<x;w++)_[w]=applyOperation(s,o[w],i,!0,u,w),s=_[w].newDocument;return _.newDocument=s,_}function applyReducer(s,o,i){var a=applyOperation(s,o);if(!1===a.test)throw new Do(\"Test operation failed\",\"TEST_OPERATION_FAILED\",i,o,s);return a.newDocument}function validator(s,o,i,a){if(\"object\"!=typeof s||null===s||Array.isArray(s))throw new Do(\"Operation is not an object\",\"OPERATION_NOT_AN_OBJECT\",o,s,i);if(!Fo[s.op])throw new Do(\"Operation `op` property is not one of operations defined in RFC-6902\",\"OPERATION_OP_INVALID\",o,s,i);if(\"string\"!=typeof s.path)throw new Do(\"Operation `path` property is not a string\",\"OPERATION_PATH_INVALID\",o,s,i);if(0!==s.path.indexOf(\"/\")&&s.path.length>0)throw new Do('Operation `path` property must start with \"/\"',\"OPERATION_PATH_INVALID\",o,s,i);if((\"move\"===s.op||\"copy\"===s.op)&&\"string\"!=typeof s.from)throw new Do(\"Operation `from` property is not present (applicable in `move` and `copy` operations)\",\"OPERATION_FROM_REQUIRED\",o,s,i);if((\"add\"===s.op||\"replace\"===s.op||\"test\"===s.op)&&void 0===s.value)throw new Do(\"Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)\",\"OPERATION_VALUE_REQUIRED\",o,s,i);if((\"add\"===s.op||\"replace\"===s.op||\"test\"===s.op)&&hasUndefined(s.value))throw new Do(\"Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)\",\"OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED\",o,s,i);if(i)if(\"add\"==s.op){var u=s.path.split(\"/\").length,_=a.split(\"/\").length;if(u!==_+1&&u!==_)throw new Do(\"Cannot perform an `add` operation at the desired path\",\"OPERATION_PATH_CANNOT_ADD\",o,s,i)}else if(\"replace\"===s.op||\"remove\"===s.op||\"_get\"===s.op){if(s.path!==a)throw new Do(\"Cannot perform the operation at a path that does not exist\",\"OPERATION_PATH_UNRESOLVABLE\",o,s,i)}else if(\"move\"===s.op||\"copy\"===s.op){var w=validate([{op:\"_get\",path:s.from,value:void 0}],i);if(w&&\"OPERATION_PATH_UNRESOLVABLE\"===w.name)throw new Do(\"Cannot perform the operation from a path that does not exist\",\"OPERATION_FROM_UNRESOLVABLE\",o,s,i)}}function validate(s,o,i){try{if(!Array.isArray(s))throw new Do(\"Patch sequence must be an array\",\"SEQUENCE_NOT_AN_ARRAY\");if(o)applyPatch(_deepClone(o),_deepClone(s),i||!0);else{i=i||validator;for(var a=0;a<s.length;a++)i(s[a],a,o,void 0)}}catch(s){if(s instanceof Do)return s;throw s}}function _areEquals(s,o){if(s===o)return!0;if(s&&o&&\"object\"==typeof s&&\"object\"==typeof o){var i,a,u,_=Array.isArray(s),w=Array.isArray(o);if(_&&w){if((a=s.length)!=o.length)return!1;for(i=a;0!=i--;)if(!_areEquals(s[i],o[i]))return!1;return!0}if(_!=w)return!1;var x=Object.keys(s);if((a=x.length)!==Object.keys(o).length)return!1;for(i=a;0!=i--;)if(!o.hasOwnProperty(x[i]))return!1;for(i=a;0!=i--;)if(!_areEquals(s[u=x[i]],o[u]))return!1;return!0}return s!=s&&o!=o}var $o=new WeakMap,qo=function qo(s){this.observers=new Map,this.obj=s},Uo=function Uo(s,o){this.callback=s,this.observer=o};function unobserve(s,o){o.unobserve()}function observe(s,o){var i,a=function getMirror(s){return $o.get(s)}(s);if(a){var u=function getObserverFromMirror(s,o){return s.observers.get(o)}(a,o);i=u&&u.observer}else a=new qo(s),$o.set(s,a);if(i)return i;if(i={},a.value=_deepClone(s),o){i.callback=o,i.next=null;var dirtyCheck=function(){generate(i)},fastCheck=function(){clearTimeout(i.next),i.next=setTimeout(dirtyCheck)};\"undefined\"!=typeof window&&(window.addEventListener(\"mouseup\",fastCheck),window.addEventListener(\"keyup\",fastCheck),window.addEventListener(\"mousedown\",fastCheck),window.addEventListener(\"keydown\",fastCheck),window.addEventListener(\"change\",fastCheck))}return i.patches=[],i.object=s,i.unobserve=function(){generate(i),clearTimeout(i.next),function removeObserverFromMirror(s,o){s.observers.delete(o.callback)}(a,i),\"undefined\"!=typeof window&&(window.removeEventListener(\"mouseup\",fastCheck),window.removeEventListener(\"keyup\",fastCheck),window.removeEventListener(\"mousedown\",fastCheck),window.removeEventListener(\"keydown\",fastCheck),window.removeEventListener(\"change\",fastCheck))},a.observers.set(o,new Uo(o,i)),i}function generate(s,o){void 0===o&&(o=!1);var i=$o.get(s.object);_generate(i.value,s.object,s.patches,\"\",o),s.patches.length&&applyPatch(i.value,s.patches);var a=s.patches;return a.length>0&&(s.patches=[],s.callback&&s.callback(a)),a}function _generate(s,o,i,a,u){if(o!==s){\"function\"==typeof o.toJSON&&(o=o.toJSON());for(var _=_objectKeys(o),w=_objectKeys(s),x=!1,C=w.length-1;C>=0;C--){var j=s[B=w[C]];if(!module_helpers_hasOwnProperty(o,B)||void 0===o[B]&&void 0!==j&&!1===Array.isArray(o))Array.isArray(s)===Array.isArray(o)?(u&&i.push({op:\"test\",path:a+\"/\"+escapePathComponent(B),value:_deepClone(j)}),i.push({op:\"remove\",path:a+\"/\"+escapePathComponent(B)}),x=!0):(u&&i.push({op:\"test\",path:a,value:s}),i.push({op:\"replace\",path:a,value:o}),!0);else{var L=o[B];\"object\"==typeof j&&null!=j&&\"object\"==typeof L&&null!=L&&Array.isArray(j)===Array.isArray(L)?_generate(j,L,i,a+\"/\"+escapePathComponent(B),u):j!==L&&(u&&i.push({op:\"test\",path:a+\"/\"+escapePathComponent(B),value:_deepClone(j)}),i.push({op:\"replace\",path:a+\"/\"+escapePathComponent(B),value:_deepClone(L)}))}}if(x||_.length!=w.length)for(C=0;C<_.length;C++){var B;module_helpers_hasOwnProperty(s,B=_[C])||void 0===o[B]||i.push({op:\"add\",path:a+\"/\"+escapePathComponent(B),value:_deepClone(o[B])})}}}function compare(s,o,i){void 0===i&&(i=!1);var a=[];return _generate(s,o,a,\"\",i),a}Object.assign({},Z,ee,{JsonPatchError:Ro,deepClone:_deepClone,escapePathComponent,unescapePathComponent});var Vo=__webpack_require__(14744),zo=__webpack_require__.n(Vo);const Wo={add:function add(s,o){return{op:\"add\",path:s,value:o}},replace,remove:function remove(s){return{op:\"remove\",path:s}},merge:function lib_merge(s,o){return{type:\"mutation\",op:\"merge\",path:s,value:o}},mergeDeep:function mergeDeep(s,o){return{type:\"mutation\",op:\"mergeDeep\",path:s,value:o}},context:function context(s,o){return{type:\"context\",path:s,value:o}},getIn:function lib_getIn(s,o){return o.reduce(((s,o)=>void 0!==o&&s?s[o]:s),s)},applyPatch:function lib_applyPatch(s,o,i){if(i=i||{},\"merge\"===(o={...o,path:o.path&&normalizeJSONPath(o.path)}).op){const i=getInByJsonPath(s,o.path);Object.assign(i,o.value),applyPatch(s,[replace(o.path,i)])}else if(\"mergeDeep\"===o.op){const i=getInByJsonPath(s,o.path),a=zo()(i,o.value,{customMerge:s=>{if(\"enum\"===s)return(s,o)=>Array.isArray(s)&&Array.isArray(o)?[...new Set([...s,...o])]:zo()(s,o)}});s=applyPatch(s,[replace(o.path,a)]).newDocument}else if(\"add\"===o.op&&\"\"===o.path&&lib_isObject(o.value)){applyPatch(s,Object.keys(o.value).reduce(((s,i)=>(s.push({op:\"add\",path:`/${normalizeJSONPath(i)}`,value:o.value[i]}),s)),[]))}else if(\"replace\"===o.op&&\"\"===o.path){let{value:a}=o;i.allowMetaPatches&&o.meta&&isAdditiveMutation(o)&&(Array.isArray(o.value)||lib_isObject(o.value))&&(a={...a,...o.meta}),s=a}else if(applyPatch(s,[o]),i.allowMetaPatches&&o.meta&&isAdditiveMutation(o)&&(Array.isArray(o.value)||lib_isObject(o.value))){const i={...getInByJsonPath(s,o.path),...o.meta};applyPatch(s,[replace(o.path,i)])}return s},parentPathMatch:function parentPathMatch(s,o){if(!Array.isArray(o))return!1;for(let i=0,a=o.length;i<a;i+=1)if(o[i]!==s[i])return!1;return!0},flatten,fullyNormalizeArray:function fullyNormalizeArray(s){return cleanArray(flatten(lib_normalizeArray(s)))},normalizeArray:lib_normalizeArray,isPromise:function isPromise(s){return lib_isObject(s)&&lib_isFunction(s.then)},forEachNew:function forEachNew(s,o){try{return forEachNewPatch(s,forEach,o)}catch(s){return s}},forEachNewPrimitive:function forEachNewPrimitive(s,o){try{return forEachNewPatch(s,forEachPrimitive,o)}catch(s){return s}},isJsonPatch,isContextPatch:function isContextPatch(s){return isPatch(s)&&\"context\"===s.type},isPatch,isMutation,isAdditiveMutation,isGenerator:function isGenerator(s){return\"[object GeneratorFunction]\"===Object.prototype.toString.call(s)},isFunction:lib_isFunction,isObject:lib_isObject,isError:function lib_isError(s){return s instanceof Error}};function normalizeJSONPath(s){return Array.isArray(s)?s.length<1?\"\":`/${s.map((s=>(s+\"\").replace(/~/g,\"~0\").replace(/\\//g,\"~1\"))).join(\"/\")}`:s}function replace(s,o,i){return{op:\"replace\",path:s,value:o,meta:i}}function forEachNewPatch(s,o,i){return cleanArray(flatten(s.filter(isAdditiveMutation).map((s=>o(s.value,i,s.path)))||[]))}function forEachPrimitive(s,o,i){return i=i||[],Array.isArray(s)?s.map(((s,a)=>forEachPrimitive(s,o,i.concat(a)))):lib_isObject(s)?Object.keys(s).map((a=>forEachPrimitive(s[a],o,i.concat(a)))):o(s,i[i.length-1],i)}function forEach(s,o,i){let a=[];if((i=i||[]).length>0){const u=o(s,i[i.length-1],i);u&&(a=a.concat(u))}if(Array.isArray(s)){const u=s.map(((s,a)=>forEach(s,o,i.concat(a))));u&&(a=a.concat(u))}else if(lib_isObject(s)){const u=Object.keys(s).map((a=>forEach(s[a],o,i.concat(a))));u&&(a=a.concat(u))}return a=flatten(a),a}function lib_normalizeArray(s){return Array.isArray(s)?s:[s]}function flatten(s){return[].concat(...s.map((s=>Array.isArray(s)?flatten(s):s)))}function cleanArray(s){return s.filter((s=>void 0!==s))}function lib_isObject(s){return s&&\"object\"==typeof s}function lib_isFunction(s){return s&&\"function\"==typeof s}function isJsonPatch(s){if(isPatch(s)){const{op:o}=s;return\"add\"===o||\"remove\"===o||\"replace\"===o}return!1}function isMutation(s){return isJsonPatch(s)||isPatch(s)&&\"mutation\"===s.type}function isAdditiveMutation(s){return isMutation(s)&&(\"add\"===s.op||\"replace\"===s.op||\"merge\"===s.op||\"mergeDeep\"===s.op)}function isPatch(s){return s&&\"object\"==typeof s}function getInByJsonPath(s,o){try{return getValueByPointer(s,o)}catch(s){return console.error(s),{}}}var Jo=__webpack_require__(48675);const Ho=class ApiDOMAggregateError extends Jo{constructor(s,o,i){if(super(s,o,i),this.name=this.constructor.name,\"string\"==typeof o&&(this.message=o),\"function\"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(o).stack,null!=i&&\"object\"==typeof i&&Object.hasOwn(i,\"cause\")&&!(\"cause\"in this)){const{cause:s}=i;this.cause=s,s instanceof Error&&\"stack\"in s&&(this.stack=`${this.stack}\\nCAUSE: ${s.stack}`)}}};class ApiDOMError extends Error{static[Symbol.hasInstance](s){return super[Symbol.hasInstance](s)||Function.prototype[Symbol.hasInstance].call(Ho,s)}constructor(s,o){if(super(s,o),this.name=this.constructor.name,\"string\"==typeof s&&(this.message=s),\"function\"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(s).stack,null!=o&&\"object\"==typeof o&&Object.hasOwn(o,\"cause\")&&!(\"cause\"in this)){const{cause:s}=o;this.cause=s,s instanceof Error&&\"stack\"in s&&(this.stack=`${this.stack}\\nCAUSE: ${s.stack}`)}}}const Ko=ApiDOMError;const Go=class ApiDOMStructuredError extends Ko{constructor(s,o){if(super(s,o),null!=o&&\"object\"==typeof o){const{cause:s,...i}=o;Object.assign(this,i)}}};var Yo=__webpack_require__(65606);function _isPlaceholder(s){return null!=s&&\"object\"==typeof s&&!0===s[\"@@functional/placeholder\"]}function _curry1(s){return function f1(o){return 0===arguments.length||_isPlaceholder(o)?f1:s.apply(this,arguments)}}function _curry2(s){return function f2(o,i){switch(arguments.length){case 0:return f2;case 1:return _isPlaceholder(o)?f2:_curry1((function(i){return s(o,i)}));default:return _isPlaceholder(o)&&_isPlaceholder(i)?f2:_isPlaceholder(o)?_curry1((function(o){return s(o,i)})):_isPlaceholder(i)?_curry1((function(i){return s(o,i)})):s(o,i)}}}function _curry3(s){return function f3(o,i,a){switch(arguments.length){case 0:return f3;case 1:return _isPlaceholder(o)?f3:_curry2((function(i,a){return s(o,i,a)}));case 2:return _isPlaceholder(o)&&_isPlaceholder(i)?f3:_isPlaceholder(o)?_curry2((function(o,a){return s(o,i,a)})):_isPlaceholder(i)?_curry2((function(i,a){return s(o,i,a)})):_curry1((function(a){return s(o,i,a)}));default:return _isPlaceholder(o)&&_isPlaceholder(i)&&_isPlaceholder(a)?f3:_isPlaceholder(o)&&_isPlaceholder(i)?_curry2((function(o,i){return s(o,i,a)})):_isPlaceholder(o)&&_isPlaceholder(a)?_curry2((function(o,a){return s(o,i,a)})):_isPlaceholder(i)&&_isPlaceholder(a)?_curry2((function(i,a){return s(o,i,a)})):_isPlaceholder(o)?_curry1((function(o){return s(o,i,a)})):_isPlaceholder(i)?_curry1((function(i){return s(o,i,a)})):_isPlaceholder(a)?_curry1((function(a){return s(o,i,a)})):s(o,i,a)}}}const Xo=Number.isInteger||function _isInteger(s){return(s|0)===s};function _isString(s){return\"[object String]\"===Object.prototype.toString.call(s)}function _nth(s,o){var i=s<0?o.length+s:s;return _isString(o)?o.charAt(i):o[i]}function _path(s,o){for(var i=o,a=0;a<s.length;a+=1){if(null==i)return;var u=s[a];i=Xo(u)?_nth(u,i):i[u]}return i}const Qo=_curry3((function pathSatisfies(s,o,i){return s(_path(o,i))}));function _cloneRegExp(s){return new RegExp(s.source,s.flags?s.flags:(s.global?\"g\":\"\")+(s.ignoreCase?\"i\":\"\")+(s.multiline?\"m\":\"\")+(s.sticky?\"y\":\"\")+(s.unicode?\"u\":\"\")+(s.dotAll?\"s\":\"\"))}function _arrayFromIterator(s){for(var o,i=[];!(o=s.next()).done;)i.push(o.value);return i}function _includesWith(s,o,i){for(var a=0,u=i.length;a<u;){if(s(o,i[a]))return!0;a+=1}return!1}function _has(s,o){return Object.prototype.hasOwnProperty.call(o,s)}const Zo=\"function\"==typeof Object.is?Object.is:function _objectIs(s,o){return s===o?0!==s||1/s==1/o:s!=s&&o!=o};var _i=Object.prototype.toString;const Ei=function(){return\"[object Arguments]\"===_i.call(arguments)?function _isArguments(s){return\"[object Arguments]\"===_i.call(s)}:function _isArguments(s){return _has(\"callee\",s)}}();var Oi=!{toString:null}.propertyIsEnumerable(\"toString\"),Pi=[\"constructor\",\"valueOf\",\"isPrototypeOf\",\"toString\",\"propertyIsEnumerable\",\"hasOwnProperty\",\"toLocaleString\"],Mi=function(){return arguments.propertyIsEnumerable(\"length\")}(),Ri=function contains(s,o){for(var i=0;i<s.length;){if(s[i]===o)return!0;i+=1}return!1},Wi=\"function\"!=typeof Object.keys||Mi?_curry1((function keys(s){if(Object(s)!==s)return[];var o,i,a=[],u=Mi&&Ei(s);for(o in s)!_has(o,s)||u&&\"length\"===o||(a[a.length]=o);if(Oi)for(i=Pi.length-1;i>=0;)_has(o=Pi[i],s)&&!Ri(a,o)&&(a[a.length]=o),i-=1;return a})):_curry1((function keys(s){return Object(s)!==s?[]:Object.keys(s)}));const ea=Wi;const ra=_curry1((function type(s){return null===s?\"Null\":void 0===s?\"Undefined\":Object.prototype.toString.call(s).slice(8,-1)}));function _uniqContentEquals(s,o,i,a){var u=_arrayFromIterator(s);function eq(s,o){return _equals(s,o,i.slice(),a.slice())}return!_includesWith((function(s,o){return!_includesWith(eq,o,s)}),_arrayFromIterator(o),u)}function _equals(s,o,i,a){if(Zo(s,o))return!0;var u=ra(s);if(u!==ra(o))return!1;if(\"function\"==typeof s[\"fantasy-land/equals\"]||\"function\"==typeof o[\"fantasy-land/equals\"])return\"function\"==typeof s[\"fantasy-land/equals\"]&&s[\"fantasy-land/equals\"](o)&&\"function\"==typeof o[\"fantasy-land/equals\"]&&o[\"fantasy-land/equals\"](s);if(\"function\"==typeof s.equals||\"function\"==typeof o.equals)return\"function\"==typeof s.equals&&s.equals(o)&&\"function\"==typeof o.equals&&o.equals(s);switch(u){case\"Arguments\":case\"Array\":case\"Object\":if(\"function\"==typeof s.constructor&&\"Promise\"===function _functionName(s){var o=String(s).match(/^function (\\w*)/);return null==o?\"\":o[1]}(s.constructor))return s===o;break;case\"Boolean\":case\"Number\":case\"String\":if(typeof s!=typeof o||!Zo(s.valueOf(),o.valueOf()))return!1;break;case\"Date\":if(!Zo(s.valueOf(),o.valueOf()))return!1;break;case\"Error\":return s.name===o.name&&s.message===o.message;case\"RegExp\":if(s.source!==o.source||s.global!==o.global||s.ignoreCase!==o.ignoreCase||s.multiline!==o.multiline||s.sticky!==o.sticky||s.unicode!==o.unicode)return!1}for(var _=i.length-1;_>=0;){if(i[_]===s)return a[_]===o;_-=1}switch(u){case\"Map\":return s.size===o.size&&_uniqContentEquals(s.entries(),o.entries(),i.concat([s]),a.concat([o]));case\"Set\":return s.size===o.size&&_uniqContentEquals(s.values(),o.values(),i.concat([s]),a.concat([o]));case\"Arguments\":case\"Array\":case\"Object\":case\"Boolean\":case\"Number\":case\"String\":case\"Date\":case\"Error\":case\"RegExp\":case\"Int8Array\":case\"Uint8Array\":case\"Uint8ClampedArray\":case\"Int16Array\":case\"Uint16Array\":case\"Int32Array\":case\"Uint32Array\":case\"Float32Array\":case\"Float64Array\":case\"ArrayBuffer\":break;default:return!1}var w=ea(s);if(w.length!==ea(o).length)return!1;var x=i.concat([s]),C=a.concat([o]);for(_=w.length-1;_>=0;){var j=w[_];if(!_has(j,o)||!_equals(o[j],s[j],x,C))return!1;_-=1}return!0}const na=_curry2((function equals(s,o){return _equals(s,o,[],[])}));function _includes(s,o){return function _indexOf(s,o,i){var a,u;if(\"function\"==typeof s.indexOf)switch(typeof o){case\"number\":if(0===o){for(a=1/o;i<s.length;){if(0===(u=s[i])&&1/u===a)return i;i+=1}return-1}if(o!=o){for(;i<s.length;){if(\"number\"==typeof(u=s[i])&&u!=u)return i;i+=1}return-1}return s.indexOf(o,i);case\"string\":case\"boolean\":case\"function\":case\"undefined\":return s.indexOf(o,i);case\"object\":if(null===o)return s.indexOf(o,i)}for(;i<s.length;){if(na(s[i],o))return i;i+=1}return-1}(o,s,0)>=0}function _map(s,o){for(var i=0,a=o.length,u=Array(a);i<a;)u[i]=s(o[i]),i+=1;return u}function _quote(s){return'\"'+s.replace(/\\\\/g,\"\\\\\\\\\").replace(/[\\b]/g,\"\\\\b\").replace(/\\f/g,\"\\\\f\").replace(/\\n/g,\"\\\\n\").replace(/\\r/g,\"\\\\r\").replace(/\\t/g,\"\\\\t\").replace(/\\v/g,\"\\\\v\").replace(/\\0/g,\"\\\\0\").replace(/\"/g,'\\\\\"')+'\"'}var ia=function pad(s){return(s<10?\"0\":\"\")+s};const aa=\"function\"==typeof Date.prototype.toISOString?function _toISOString(s){return s.toISOString()}:function _toISOString(s){return s.getUTCFullYear()+\"-\"+ia(s.getUTCMonth()+1)+\"-\"+ia(s.getUTCDate())+\"T\"+ia(s.getUTCHours())+\":\"+ia(s.getUTCMinutes())+\":\"+ia(s.getUTCSeconds())+\".\"+(s.getUTCMilliseconds()/1e3).toFixed(3).slice(2,5)+\"Z\"};function _complement(s){return function(){return!s.apply(this,arguments)}}function _arrayReduce(s,o,i){for(var a=0,u=i.length;a<u;)o=s(o,i[a]),a+=1;return o}const ca=Array.isArray||function _isArray(s){return null!=s&&s.length>=0&&\"[object Array]\"===Object.prototype.toString.call(s)};function _dispatchable(s,o,i){return function(){if(0===arguments.length)return i();var a=arguments[arguments.length-1];if(!ca(a)){for(var u=0;u<s.length;){if(\"function\"==typeof a[s[u]])return a[s[u]].apply(a,Array.prototype.slice.call(arguments,0,-1));u+=1}if(function _isTransformer(s){return null!=s&&\"function\"==typeof s[\"@@transducer/step\"]}(a))return o.apply(null,Array.prototype.slice.call(arguments,0,-1))(a)}return i.apply(this,arguments)}}function _isObject(s){return\"[object Object]\"===Object.prototype.toString.call(s)}const _xfBase_init=function(){return this.xf[\"@@transducer/init\"]()},_xfBase_result=function(s){return this.xf[\"@@transducer/result\"](s)};var la=function(){function XFilter(s,o){this.xf=o,this.f=s}return XFilter.prototype[\"@@transducer/init\"]=_xfBase_init,XFilter.prototype[\"@@transducer/result\"]=_xfBase_result,XFilter.prototype[\"@@transducer/step\"]=function(s,o){return this.f(o)?this.xf[\"@@transducer/step\"](s,o):s},XFilter}();function _xfilter(s){return function(o){return new la(s,o)}}var ua=_curry2(_dispatchable([\"fantasy-land/filter\",\"filter\"],_xfilter,(function(s,o){return _isObject(o)?_arrayReduce((function(i,a){return s(o[a])&&(i[a]=o[a]),i}),{},ea(o)):function _filter(s,o){for(var i=0,a=o.length,u=[];i<a;)s(o[i])&&(u[u.length]=o[i]),i+=1;return u}(s,o)})));const da=ua;const ma=_curry2((function reject(s,o){return da(_complement(s),o)}));function _toString_toString(s,o){var i=function recur(i){var a=o.concat([s]);return _includes(i,a)?\"<Circular>\":_toString_toString(i,a)},mapPairs=function(s,o){return _map((function(o){return _quote(o)+\": \"+i(s[o])}),o.slice().sort())};switch(Object.prototype.toString.call(s)){case\"[object Arguments]\":return\"(function() { return arguments; }(\"+_map(i,s).join(\", \")+\"))\";case\"[object Array]\":return\"[\"+_map(i,s).concat(mapPairs(s,ma((function(s){return/^\\d+$/.test(s)}),ea(s)))).join(\", \")+\"]\";case\"[object Boolean]\":return\"object\"==typeof s?\"new Boolean(\"+i(s.valueOf())+\")\":s.toString();case\"[object Date]\":return\"new Date(\"+(isNaN(s.valueOf())?i(NaN):_quote(aa(s)))+\")\";case\"[object Map]\":return\"new Map(\"+i(Array.from(s))+\")\";case\"[object Null]\":return\"null\";case\"[object Number]\":return\"object\"==typeof s?\"new Number(\"+i(s.valueOf())+\")\":1/s==-1/0?\"-0\":s.toString(10);case\"[object Set]\":return\"new Set(\"+i(Array.from(s).sort())+\")\";case\"[object String]\":return\"object\"==typeof s?\"new String(\"+i(s.valueOf())+\")\":_quote(s);case\"[object Undefined]\":return\"undefined\";default:if(\"function\"==typeof s.toString){var a=s.toString();if(\"[object Object]\"!==a)return a}return\"{\"+mapPairs(s,ea(s)).join(\", \")+\"}\"}}const ga=_curry1((function toString(s){return _toString_toString(s,[])}));var ya=_curry2((function test(s,o){if(!function _isRegExp(s){return\"[object RegExp]\"===Object.prototype.toString.call(s)}(s))throw new TypeError(\"‘test’ requires a value of type RegExp as its first argument; received \"+ga(s));return _cloneRegExp(s).test(o)}));const va=ya;function _arity(s,o){switch(s){case 0:return function(){return o.apply(this,arguments)};case 1:return function(s){return o.apply(this,arguments)};case 2:return function(s,i){return o.apply(this,arguments)};case 3:return function(s,i,a){return o.apply(this,arguments)};case 4:return function(s,i,a,u){return o.apply(this,arguments)};case 5:return function(s,i,a,u,_){return o.apply(this,arguments)};case 6:return function(s,i,a,u,_,w){return o.apply(this,arguments)};case 7:return function(s,i,a,u,_,w,x){return o.apply(this,arguments)};case 8:return function(s,i,a,u,_,w,x,C){return o.apply(this,arguments)};case 9:return function(s,i,a,u,_,w,x,C,j){return o.apply(this,arguments)};case 10:return function(s,i,a,u,_,w,x,C,j,L){return o.apply(this,arguments)};default:throw new Error(\"First argument to _arity must be a non-negative integer no greater than ten\")}}function _pipe(s,o){return function(){return o.call(this,s.apply(this,arguments))}}const ba=_curry1((function isArrayLike(s){return!!ca(s)||!!s&&(\"object\"==typeof s&&(!_isString(s)&&(0===s.length||s.length>0&&(s.hasOwnProperty(0)&&s.hasOwnProperty(s.length-1)))))}));var _a=\"undefined\"!=typeof Symbol?Symbol.iterator:\"@@iterator\";function _createReduce(s,o,i){return function _reduce(a,u,_){if(ba(_))return s(a,u,_);if(null==_)return u;if(\"function\"==typeof _[\"fantasy-land/reduce\"])return o(a,u,_,\"fantasy-land/reduce\");if(null!=_[_a])return i(a,u,_[_a]());if(\"function\"==typeof _.next)return i(a,u,_);if(\"function\"==typeof _.reduce)return o(a,u,_,\"reduce\");throw new TypeError(\"reduce: list must be array or iterable\")}}function _xArrayReduce(s,o,i){for(var a=0,u=i.length;a<u;){if((o=s[\"@@transducer/step\"](o,i[a]))&&o[\"@@transducer/reduced\"]){o=o[\"@@transducer/value\"];break}a+=1}return s[\"@@transducer/result\"](o)}const Ea=_curry2((function bind(s,o){return _arity(s.length,(function(){return s.apply(o,arguments)}))}));function _xIterableReduce(s,o,i){for(var a=i.next();!a.done;){if((o=s[\"@@transducer/step\"](o,a.value))&&o[\"@@transducer/reduced\"]){o=o[\"@@transducer/value\"];break}a=i.next()}return s[\"@@transducer/result\"](o)}function _xMethodReduce(s,o,i,a){return s[\"@@transducer/result\"](i[a](Ea(s[\"@@transducer/step\"],s),o))}const wa=_createReduce(_xArrayReduce,_xMethodReduce,_xIterableReduce);var xa=function(){function XWrap(s){this.f=s}return XWrap.prototype[\"@@transducer/init\"]=function(){throw new Error(\"init not implemented on XWrap\")},XWrap.prototype[\"@@transducer/result\"]=function(s){return s},XWrap.prototype[\"@@transducer/step\"]=function(s,o){return this.f(s,o)},XWrap}();function _xwrap(s){return new xa(s)}var ka=_curry3((function(s,o,i){return wa(\"function\"==typeof s?_xwrap(s):s,o,i)}));const Aa=ka;function _checkForMethod(s,o){return function(){var i=arguments.length;if(0===i)return o();var a=arguments[i-1];return ca(a)||\"function\"!=typeof a[s]?o.apply(this,arguments):a[s].apply(a,Array.prototype.slice.call(arguments,0,i-1))}}var Ca=_curry3(_checkForMethod(\"slice\",(function slice(s,o,i){return Array.prototype.slice.call(i,s,o)})));const ja=Ca;const Ia=_curry1(_checkForMethod(\"tail\",ja(1,1/0)));function pipe(){if(0===arguments.length)throw new Error(\"pipe requires at least one argument\");return _arity(arguments[0].length,Aa(_pipe,arguments[0],Ia(arguments)))}const Na=_curry2((function defaultTo(s,o){return null==o||o!=o?s:o}));const Da=_curry2((function prop(s,o){if(null!=o)return Xo(s)?_nth(s,o):o[s]}));const La=_curry3((function propOr(s,o,i){return Na(s,Da(o,i))}));var Fa=_curry1((function(s){return _nth(-1,s)}));const Ba=Fa;function _curryN(s,o,i){return function(){for(var a=[],u=0,_=s,w=0,x=!1;w<o.length||u<arguments.length;){var C;w<o.length&&(!_isPlaceholder(o[w])||u>=arguments.length)?C=o[w]:(C=arguments[u],u+=1),a[w]=C,_isPlaceholder(C)?x=!0:_-=1,w+=1}return!x&&_<=0?i.apply(this,a):_arity(Math.max(0,_),_curryN(s,a,i))}}const $a=_curry2((function curryN(s,o){return 1===s?_curry1(o):_arity(s,_curryN(s,[],o))}));const za=_curry1((function curry(s){return $a(s.length,s)}));function _isFunction(s){var o=Object.prototype.toString.call(s);return\"[object Function]\"===o||\"[object AsyncFunction]\"===o||\"[object GeneratorFunction]\"===o||\"[object AsyncGeneratorFunction]\"===o}const Ja=_curry2((function invoker(s,o){return $a(s+1,(function(){var i=arguments[s];if(null!=i&&_isFunction(i[o]))return i[o].apply(i,Array.prototype.slice.call(arguments,0,s));throw new TypeError(ga(i)+' does not have a method named \"'+o+'\"')}))}));const Ha=Ja(1,\"split\");function dropLastWhile(s,o){for(var i=o.length-1;i>=0&&s(o[i]);)i-=1;return ja(0,i+1,o)}var Ga=function(){function XDropLastWhile(s,o){this.f=s,this.retained=[],this.xf=o}return XDropLastWhile.prototype[\"@@transducer/init\"]=_xfBase_init,XDropLastWhile.prototype[\"@@transducer/result\"]=function(s){return this.retained=null,this.xf[\"@@transducer/result\"](s)},XDropLastWhile.prototype[\"@@transducer/step\"]=function(s,o){return this.f(o)?this.retain(s,o):this.flush(s,o)},XDropLastWhile.prototype.flush=function(s,o){return s=wa(this.xf,s,this.retained),this.retained=[],this.xf[\"@@transducer/step\"](s,o)},XDropLastWhile.prototype.retain=function(s,o){return this.retained.push(o),s},XDropLastWhile}();function _xdropLastWhile(s){return function(o){return new Ga(s,o)}}const ec=_curry2(_dispatchable([],_xdropLastWhile,dropLastWhile));const rc=Ja(1,\"join\");const sc=_curry1((function flip(s){return $a(s.length,(function(o,i){var a=Array.prototype.slice.call(arguments,0);return a[0]=i,a[1]=o,s.apply(this,a)}))}))(_curry2(_includes));const oc=za((function(s,o){return pipe(Ha(\"\"),ec(sc(s)),rc(\"\"))(o)}));function _iterableReduce(s,o,i){for(var a=i.next();!a.done;)o=s(o,a.value),a=i.next();return o}function _methodReduce(s,o,i,a){return i[a](s,o)}const ic=_createReduce(_arrayReduce,_methodReduce,_iterableReduce);var ac=function(){function XMap(s,o){this.xf=o,this.f=s}return XMap.prototype[\"@@transducer/init\"]=_xfBase_init,XMap.prototype[\"@@transducer/result\"]=_xfBase_result,XMap.prototype[\"@@transducer/step\"]=function(s,o){return this.xf[\"@@transducer/step\"](s,this.f(o))},XMap}();const cc=_curry2(_dispatchable([\"fantasy-land/map\",\"map\"],(function _xmap(s){return function(o){return new ac(s,o)}}),(function map(s,o){switch(Object.prototype.toString.call(o)){case\"[object Function]\":return $a(o.length,(function(){return s.call(this,o.apply(this,arguments))}));case\"[object Object]\":return _arrayReduce((function(i,a){return i[a]=s(o[a]),i}),{},ea(o));default:return _map(s,o)}})));const lc=_curry2((function ap(s,o){return\"function\"==typeof o[\"fantasy-land/ap\"]?o[\"fantasy-land/ap\"](s):\"function\"==typeof s.ap?s.ap(o):\"function\"==typeof s?function(i){return s(i)(o(i))}:ic((function(s,i){return function _concat(s,o){var i;o=o||[];var a=(s=s||[]).length,u=o.length,_=[];for(i=0;i<a;)_[_.length]=s[i],i+=1;for(i=0;i<u;)_[_.length]=o[i],i+=1;return _}(s,cc(i,o))}),[],s)}));const pc=_curry2((function liftN(s,o){var i=$a(s,o);return $a(s,(function(){return _arrayReduce(lc,cc(i,arguments[0]),Array.prototype.slice.call(arguments,1))}))}));const hc=_curry1((function lift(s){return pc(s.length,s)}));const dc=hc(_curry1((function not(s){return!s})));const fc=_curry1((function always(s){return function(){return s}}));const gc=fc(void 0);const bc=na(gc());const _c=dc(bc);const Ec=_curry2((function max(s,o){if(s===o)return o;function safeMax(s,o){if(s>o!=o>s)return o>s?o:s}var i=safeMax(s,o);if(void 0!==i)return i;var a=safeMax(typeof s,typeof o);if(void 0!==a)return a===typeof s?s:o;var u=ga(s),_=safeMax(u,ga(o));return void 0!==_&&_===u?s:o}));var kc=_curry2((function pluck(s,o){return cc(Da(s),o)}));const Oc=kc;const jc=_curry1((function anyPass(s){return $a(Aa(Ec,0,Oc(\"length\",s)),(function(){for(var o=0,i=s.length;o<i;){if(s[o].apply(this,arguments))return!0;o+=1}return!1}))}));var identical=function(s,o){switch(arguments.length){case 0:return identical;case 1:return function unaryIdentical(o){return 0===arguments.length?unaryIdentical:Zo(s,o)};default:return Zo(s,o)}};const Pc=identical;const Ic=$a(1,pipe(ra,Pc(\"GeneratorFunction\")));const Nc=$a(1,pipe(ra,Pc(\"AsyncFunction\")));const Mc=jc([pipe(ra,Pc(\"Function\")),Ic,Nc]);var Rc=_curry3((function replace(s,o,i){return i.replace(s,o)}));const Lc=Rc;const Fc=$a(1,pipe(ra,Pc(\"RegExp\")));const qc=_curry3((function when(s,o,i){return s(i)?o(i):i}));const Jc=$a(1,pipe(ra,Pc(\"String\")));const Hc=qc(Jc,Lc(/[.*+?^${}()|[\\]\\\\-]/g,\"\\\\$&\"));var Kc=function checkValue(s,o){if(\"string\"!=typeof s&&!(s instanceof String))throw TypeError(\"`\".concat(o,\"` must be a string\"))};const Gc=function replaceAll(s,o,i){!function checkArguments(s,o,i){if(null==i||null==s||null==o)throw TypeError(\"Input values must not be `null` or `undefined`\")}(s,o,i),Kc(i,\"str\"),Kc(o,\"replaceValue\"),function checkSearchValue(s){if(!(\"string\"==typeof s||s instanceof String||s instanceof RegExp))throw TypeError(\"`searchValue` must be a string or an regexp\")}(s);var a=new RegExp(Fc(s)?s:Hc(s),\"g\");return Lc(a,o,i)};var Qc=$a(3,Gc),tl=Ja(2,\"replaceAll\");const sl=Mc(String.prototype.replaceAll)?tl:Qc,isWindows=()=>Qo(va(/^win/),[\"platform\"],Yo),getProtocol=s=>{try{const o=new URL(s);return oc(\":\",o.protocol)}catch{return}},ul=(pipe(getProtocol,_c),s=>{if(Yo.browser)return!1;const o=getProtocol(s);return bc(o)||\"file\"===o||/^[a-zA-Z]$/.test(o)}),isHttpUrl=s=>{const o=getProtocol(s);return\"http\"===o||\"https\"===o},toFileSystemPath=(s,o)=>{const i=[/%23/g,\"#\",/%24/g,\"$\",/%26/g,\"&\",/%2C/g,\",\",/%40/g,\"@\"],a=La(!1,\"keepFileProtocol\",o),u=La(isWindows,\"isWindows\",o);let _=decodeURI(s);for(let s=0;s<i.length;s+=2)_=_.replace(i[s],i[s+1]);let w=\"file://\"===_.substring(0,7).toLowerCase();return w&&(_=\"/\"===_[7]?_.substring(8):_.substring(7),u()&&\"/\"===_[1]&&(_=`${_[0]}:${_.substring(1)}`),a?_=`file:///${_}`:(w=!1,_=u()?_:`/${_}`)),u()&&!w&&(_=sl(\"/\",\"\\\\\",_),\":\\\\\"===_.substring(1,3)&&(_=_[0].toUpperCase()+_.substring(1))),_},getHash=s=>{const o=s.indexOf(\"#\");return-1!==o?s.substring(o):\"#\"},stripHash=s=>{const o=s.indexOf(\"#\");let i=s;return o>=0&&(i=s.substring(0,o)),i},url_cwd=()=>{if(Yo.browser)return stripHash(globalThis.location.href);const s=Yo.cwd(),o=Ba(s);return[\"/\",\"\\\\\"].includes(o)?s:s+(isWindows()?\"\\\\\":\"/\")},resolve=(s,o)=>{const i=new URL(o,new URL(s,\"resolve://\"));if(\"resolve:\"===i.protocol){const{pathname:s,search:o,hash:a}=i;return s+o+a}return i.toString()},sanitize=s=>{if(ul(s))return(s=>{const o=[/\\?/g,\"%3F\",/#/g,\"%23\"];let i=s;isWindows()&&(i=i.replace(/\\\\/g,\"/\")),i=encodeURI(i);for(let s=0;s<o.length;s+=2)i=i.replace(o[s],o[s+1]);return i})(toFileSystemPath(s));try{return new URL(s).toString()}catch{return encodeURI(decodeURI(s)).replace(/%5B/g,\"[\").replace(/%5D/g,\"]\")}},unsanitize=s=>ul(s)?toFileSystemPath(s):decodeURI(s),{fetch:yl,Response:vl,Headers:_l,Request:Sl,FormData:El,File:wl,Blob:xl}=globalThis;function _array_like_to_array(s,o){(null==o||o>s.length)&&(o=s.length);for(var i=0,a=new Array(o);i<o;i++)a[i]=s[i];return a}function legacy_defineProperties(s,o){for(var i=0;i<o.length;i++){var a=o[i];a.enumerable=a.enumerable||!1,a.configurable=!0,\"value\"in a&&(a.writable=!0),Object.defineProperty(s,a.key,a)}}function _instanceof(s,o){return null!=o&&\"undefined\"!=typeof Symbol&&o[Symbol.hasInstance]?!!o[Symbol.hasInstance](s):s instanceof o}function _sliced_to_array(s,o){return function _array_with_holes(s){if(Array.isArray(s))return s}(s)||function _iterable_to_array_limit(s,o){var i=null==s?null:\"undefined\"!=typeof Symbol&&s[Symbol.iterator]||s[\"@@iterator\"];if(null!=i){var a,u,_=[],w=!0,x=!1;try{for(i=i.call(s);!(w=(a=i.next()).done)&&(_.push(a.value),!o||_.length!==o);w=!0);}catch(s){x=!0,u=s}finally{try{w||null==i.return||i.return()}finally{if(x)throw u}}return _}}(s,o)||function _unsupported_iterable_to_array(s,o){if(!s)return;if(\"string\"==typeof s)return _array_like_to_array(s,o);var i=Object.prototype.toString.call(s).slice(8,-1);\"Object\"===i&&s.constructor&&(i=s.constructor.name);if(\"Map\"===i||\"Set\"===i)return Array.from(i);if(\"Arguments\"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return _array_like_to_array(s,o)}(s,o)||function _non_iterable_rest(){throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}function _type_of(s){return s&&\"undefined\"!=typeof Symbol&&s.constructor===Symbol?\"symbol\":typeof s}void 0===globalThis.fetch&&(globalThis.fetch=yl),void 0===globalThis.Headers&&(globalThis.Headers=_l),void 0===globalThis.Request&&(globalThis.Request=Sl),void 0===globalThis.Response&&(globalThis.Response=vl),void 0===globalThis.FormData&&(globalThis.FormData=El),void 0===globalThis.File&&(globalThis.File=wl),void 0===globalThis.Blob&&(globalThis.Blob=xl);var __typeError=function(s){throw TypeError(s)},__accessCheck=function(s,o,i){return o.has(s)||__typeError(\"Cannot \"+i)},__privateGet=function(s,o,i){return __accessCheck(s,o,\"read from private field\"),i?i.call(s):o.get(s)},__privateAdd=function(s,o,i){return o.has(s)?__typeError(\"Cannot add the same private member more than once\"):_instanceof(o,WeakSet)?o.add(s):o.set(s,i)},__privateSet=function(s,o,i,a){return __accessCheck(s,o,\"write to private field\"),a?a.call(s,i):o.set(s,i),i},to_string=function(s){return Object.prototype.toString.call(s)},is_typed_array=function(s){return ArrayBuffer.isView(s)&&!_instanceof(s,DataView)},kl=Array.isArray,Ol=Object.getOwnPropertyDescriptor,Al=Object.prototype.propertyIsEnumerable,Cl=Object.getOwnPropertySymbols,Pl=Object.prototype.hasOwnProperty;function own_enumerable_keys(s){for(var o=Object.keys(s),i=Cl(s),a=0;a<i.length;a++)Al.call(s,i[a])&&o.push(i[a]);return o}function is_writable(s,o){var i;return!(null===(i=Ol(s,o))||void 0===i?void 0:i.writable)}function legacy_copy(s,o){if(\"object\"===(void 0===s?\"undefined\":_type_of(s))&&null!==s){var i;if(kl(s))i=[];else if(\"[object Date]\"===to_string(s))i=new Date(s.getTime?s.getTime():s);else if(function(s){return\"[object RegExp]\"===to_string(s)}(s))i=new RegExp(s);else if(function(s){return\"[object Error]\"===to_string(s)}(s))i={message:s.message};else if(function(s){return\"[object Boolean]\"===to_string(s)}(s)||function(s){return\"[object Number]\"===to_string(s)}(s)||function(s){return\"[object String]\"===to_string(s)}(s))i=Object(s);else{if(is_typed_array(s))return s.slice();i=Object.create(Object.getPrototypeOf(s))}var a=o.includeSymbols?own_enumerable_keys:Object.keys,u=!0,_=!1,w=void 0;try{for(var x,C=a(s)[Symbol.iterator]();!(u=(x=C.next()).done);u=!0){var j=x.value;i[j]=s[j]}}catch(s){_=!0,w=s}finally{try{u||null==C.return||C.return()}finally{if(_)throw w}}return i}return s}var Il,Tl,Nl={includeSymbols:!1,immutable:!1};function walk(s,o){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:Nl,a=[],u=[],_=!0,w=i.includeSymbols?own_enumerable_keys:Object.keys,x=!!i.immutable;return function walker(s){var C=x?legacy_copy(s,i):s,j={},L=!0,B={node:C,node_:s,path:[].concat(a),parent:u[u.length-1],parents:u,key:a[a.length-1],isRoot:0===a.length,level:a.length,circular:void 0,isLeaf:!1,notLeaf:!0,notRoot:!0,isFirst:!1,isLast:!1,update:function update(s){var o=arguments.length>1&&void 0!==arguments[1]&&arguments[1];B.isRoot||(B.parent.node[B.key]=s),B.node=s,o&&(L=!1)},delete:function _delete(s){delete B.parent.node[B.key],s&&(L=!1)},remove:function remove(s){kl(B.parent.node)?B.parent.node.splice(B.key,1):delete B.parent.node[B.key],s&&(L=!1)},keys:null,before:function before(s){j.before=s},after:function after(s){j.after=s},pre:function pre(s){j.pre=s},post:function post(s){j.post=s},stop:function stop(){_=!1},block:function block(){L=!1}};if(!_)return B;function update_state(){if(\"object\"===_type_of(B.node)&&null!==B.node){B.keys&&B.node_===B.node||(B.keys=w(B.node)),B.isLeaf=0===B.keys.length;for(var o=0;o<u.length;o++)if(u[o].node_===s){B.circular=u[o];break}}else B.isLeaf=!0,B.keys=null;B.notLeaf=!B.isLeaf,B.notRoot=!B.isRoot}update_state();var $=o.call(B,B.node);if(void 0!==$&&B.update&&B.update($),j.before&&j.before.call(B,B.node),!L)return B;if(\"object\"===_type_of(B.node)&&null!==B.node&&!B.circular){var U;u.push(B),update_state();var V=!0,z=!1,Y=void 0;try{for(var Z,ee=Object.entries(null!==(U=B.keys)&&void 0!==U?U:[])[Symbol.iterator]();!(V=(Z=ee.next()).done);V=!0){var ie,ae=_sliced_to_array(Z.value,2),ce=ae[0],le=ae[1];a.push(le),j.pre&&j.pre.call(B,B.node[le],le);var pe=walker(B.node[le]);x&&Pl.call(B.node,le)&&!is_writable(B.node,le)&&(B.node[le]=pe.node),pe.isLast=!!(null===(ie=B.keys)||void 0===ie?void 0:ie.length)&&+ce==B.keys.length-1,pe.isFirst=0==+ce,j.post&&j.post.call(B,pe),a.pop()}}catch(s){z=!0,Y=s}finally{try{V||null==ee.return||ee.return()}finally{if(z)throw Y}}u.pop()}return j.after&&j.after.call(B,B.node),B}(s).node}var Ml=function(){function Traverse(s){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Nl;!function _class_call_check(s,o){if(!(s instanceof o))throw new TypeError(\"Cannot call a class as a function\")}(this,Traverse),__privateAdd(this,Il),__privateAdd(this,Tl),__privateSet(this,Il,s),__privateSet(this,Tl,o)}return function _create_class(s,o,i){return o&&legacy_defineProperties(s.prototype,o),i&&legacy_defineProperties(s,i),s}(Traverse,[{key:\"get\",value:function get(s){for(var o=__privateGet(this,Il),i=0;o&&i<s.length;i++){var a=s[i];if(!Pl.call(o,a)||!__privateGet(this,Tl).includeSymbols&&\"symbol\"===(void 0===a?\"undefined\":_type_of(a)))return;o=o[a]}return o}},{key:\"has\",value:function has(s){for(var o=__privateGet(this,Il),i=0;o&&i<s.length;i++){var a=s[i];if(!Pl.call(o,a)||!__privateGet(this,Tl).includeSymbols&&\"symbol\"===(void 0===a?\"undefined\":_type_of(a)))return!1;o=o[a]}return!0}},{key:\"set\",value:function set(s,o){var i=__privateGet(this,Il),a=0;for(a=0;a<s.length-1;a++){var u=s[a];Pl.call(i,u)||(i[u]={}),i=i[u]}return i[s[a]]=o,o}},{key:\"map\",value:function map(s){return walk(__privateGet(this,Il),s,{immutable:!0,includeSymbols:!!__privateGet(this,Tl).includeSymbols})}},{key:\"forEach\",value:function forEach(s){return __privateSet(this,Il,walk(__privateGet(this,Il),s,__privateGet(this,Tl))),__privateGet(this,Il)}},{key:\"reduce\",value:function reduce(s,o){var i=1===arguments.length,a=i?__privateGet(this,Il):o;return this.forEach((function(o){this.isRoot&&i||(a=s.call(this,a,o))})),a}},{key:\"paths\",value:function paths(){var s=[];return this.forEach((function(){s.push(this.path)})),s}},{key:\"nodes\",value:function nodes(){var s=[];return this.forEach((function(){s.push(this.node)})),s}},{key:\"clone\",value:function clone(){var s=[],o=[],i=__privateGet(this,Tl);return is_typed_array(__privateGet(this,Il))?__privateGet(this,Il).slice():function clone(a){for(var u=0;u<s.length;u++)if(s[u]===a)return o[u];if(\"object\"===(void 0===a?\"undefined\":_type_of(a))&&null!==a){var _=legacy_copy(a,i);s.push(a),o.push(_);var w=i.includeSymbols?own_enumerable_keys:Object.keys,x=!0,C=!1,j=void 0;try{for(var L,B=w(a)[Symbol.iterator]();!(x=(L=B.next()).done);x=!0){var $=L.value;_[$]=clone(a[$])}}catch(s){C=!0,j=s}finally{try{x||null==B.return||B.return()}finally{if(C)throw j}}return s.pop(),o.pop(),_}return a}(__privateGet(this,Il))}}]),Traverse}();Il=new WeakMap,Tl=new WeakMap;var traverse=function(s,o){return new Ml(s,o)};traverse.get=function(s,o,i){return new Ml(s,i).get(o)},traverse.set=function(s,o,i,a){return new Ml(s,a).set(o,i)},traverse.has=function(s,o,i){return new Ml(s,i).has(o)},traverse.map=function(s,o,i){return new Ml(s,i).map(o)},traverse.forEach=function(s,o,i){return new Ml(s,i).forEach(o)},traverse.reduce=function(s,o,i,a){return new Ml(s,a).reduce(o,i)},traverse.paths=function(s,o){return new Ml(s,o).paths()},traverse.nodes=function(s,o){return new Ml(s,o).nodes()},traverse.clone=function(s,o){return new Ml(s,o).clone()};var Rl=traverse;const Dl=\"application/json, application/yaml\",Ll=\"https://swagger.io\",Fl=Object.freeze({url:\"/\"}),Bl=3e3,$l=[\"properties\"],Ul=[\"properties\"],Vl=[\"definitions\",\"parameters\",\"responses\",\"securityDefinitions\",\"components/schemas\",\"components/responses\",\"components/parameters\",\"components/securitySchemes\"],zl=[\"schema/example\",\"items/example\"];function isFreelyNamed(s){const o=s[s.length-1],i=s[s.length-2],a=s.join(\"/\");return $l.indexOf(o)>-1&&-1===Ul.indexOf(i)||Vl.indexOf(a)>-1||zl.some((s=>a.indexOf(s)>-1))}function absolutifyPointer(s,o){const[i,a]=s.split(\"#\"),u=null!=o?o:\"\",_=null!=i?i:\"\";let w;if(isHttpUrl(u))w=resolve(u,_);else{const s=resolve(Ll,u),o=resolve(s,_).replace(Ll,\"\");w=_.startsWith(\"/\")?o:o.substring(1)}return a?`${w}#${a}`:w}const Wl=/^([a-z]+:\\/\\/|\\/\\/)/i;class JSONRefError extends Go{}const Jl={},Hl=new WeakMap,Kl=[s=>\"paths\"===s[0]&&\"responses\"===s[3]&&\"examples\"===s[5],s=>\"paths\"===s[0]&&\"responses\"===s[3]&&\"content\"===s[5]&&\"example\"===s[7],s=>\"paths\"===s[0]&&\"responses\"===s[3]&&\"content\"===s[5]&&\"examples\"===s[7]&&\"value\"===s[9],s=>\"paths\"===s[0]&&\"requestBody\"===s[3]&&\"content\"===s[4]&&\"example\"===s[6],s=>\"paths\"===s[0]&&\"requestBody\"===s[3]&&\"content\"===s[4]&&\"examples\"===s[6]&&\"value\"===s[8],s=>\"paths\"===s[0]&&\"parameters\"===s[2]&&\"example\"===s[4],s=>\"paths\"===s[0]&&\"parameters\"===s[3]&&\"example\"===s[5],s=>\"paths\"===s[0]&&\"parameters\"===s[2]&&\"examples\"===s[4]&&\"value\"===s[6],s=>\"paths\"===s[0]&&\"parameters\"===s[3]&&\"examples\"===s[5]&&\"value\"===s[7],s=>\"paths\"===s[0]&&\"parameters\"===s[2]&&\"content\"===s[4]&&\"example\"===s[6],s=>\"paths\"===s[0]&&\"parameters\"===s[2]&&\"content\"===s[4]&&\"examples\"===s[6]&&\"value\"===s[8],s=>\"paths\"===s[0]&&\"parameters\"===s[3]&&\"content\"===s[4]&&\"example\"===s[7],s=>\"paths\"===s[0]&&\"parameters\"===s[3]&&\"content\"===s[5]&&\"examples\"===s[7]&&\"value\"===s[9]],Gl={key:\"$ref\",plugin:(s,o,i,a)=>{const u=a.getInstance(),_=i.slice(0,-1);if(isFreelyNamed(_)||(s=>Kl.some((o=>o(s))))(_))return;const{baseDoc:w}=a.getContext(i);if(\"string\"!=typeof s)return new JSONRefError(\"$ref: must be a string (JSON-Ref)\",{$ref:s,baseDoc:w,fullPath:i});const x=refs_split(s),C=x[0],j=x[1]||\"\";let L,B,$;try{L=w||C?absoluteify(C,w):null}catch(o){return wrapError(o,{pointer:j,$ref:s,basePath:L,fullPath:i})}if(function pointerAlreadyInPath(s,o,i,a){let u=Hl.get(a);u||(u={},Hl.set(a,u));const _=function arrayToJsonPointer(s){if(0===s.length)return\"\";return`/${s.map(escapeJsonPointerToken).join(\"/\")}`}(i),w=`${o||\"<specmap-base>\"}#${s}`,x=_.replace(/allOf\\/\\d+\\/?/g,\"\"),C=a.contextTree.get([]).baseDoc;if(o===C&&pointerIsAParent(x,s))return!0;let j=\"\";const L=i.some((s=>(j=`${j}/${escapeJsonPointerToken(s)}`,u[j]&&u[j].some((s=>pointerIsAParent(s,w)||pointerIsAParent(w,s))))));if(L)return!0;return void(u[x]=(u[x]||[]).concat(w))}(j,L,_,a)&&!u.useCircularStructures){const o=absolutifyPointer(s,L);return s===o?null:Wo.replace(i,o)}if(null==L?($=jsonPointerToArray(j),B=a.get($),void 0===B&&(B=new JSONRefError(`Could not resolve reference: ${s}`,{pointer:j,$ref:s,baseDoc:w,fullPath:i}))):(B=extractFromDoc(L,j),B=null!=B.__value?B.__value:B.catch((o=>{throw wrapError(o,{pointer:j,$ref:s,baseDoc:w,fullPath:i})}))),B instanceof Error)return[Wo.remove(i),B];const U=absolutifyPointer(s,L),V=Wo.replace(_,B,{$$ref:U});if(L&&L!==w)return[V,Wo.context(_,{baseDoc:L})];try{if(!function patchValueAlreadyInPath(s,o){const i=[s];return o.path.reduce(((s,o)=>(i.push(s[o]),s[o])),s),pointToAncestor(o.value);function pointToAncestor(s){return Wo.isObject(s)&&(i.indexOf(s)>=0||Object.keys(s).some((o=>pointToAncestor(s[o]))))}}(a.state,V)||u.useCircularStructures)return V}catch(s){return null}}},Yl=Object.assign(Gl,{docCache:Jl,absoluteify,clearCache:function clearCache(s){void 0!==s?delete Jl[s]:Object.keys(Jl).forEach((s=>{delete Jl[s]}))},JSONRefError,wrapError,getDoc,split:refs_split,extractFromDoc,fetchJSON:function fetchJSON(s){return fetch(s,{headers:{Accept:Dl},loadSpec:!0}).then((s=>s.text())).then((s=>fn.load(s)))},extract,jsonPointerToArray,unescapeJsonPointerToken}),Xl=Yl;function absoluteify(s,o){if(!Wl.test(s)){if(!o)throw new JSONRefError(`Tried to resolve a relative URL, without having a basePath. path: '${s}' basePath: '${o}'`);return resolve(o,s)}return s}function wrapError(s,o){let i;return i=s&&s.response&&s.response.body?`${s.response.body.code} ${s.response.body.message}`:s.message,new JSONRefError(`Could not resolve reference: ${i}`,{...o,cause:s})}function refs_split(s){return(s+\"\").split(\"#\")}function extractFromDoc(s,o){const i=Jl[s];if(i&&!Wo.isPromise(i))try{const s=extract(o,i);return Object.assign(Promise.resolve(s),{__value:s})}catch(s){return Promise.reject(s)}return getDoc(s).then((s=>extract(o,s)))}function getDoc(s){const o=Jl[s];return o?Wo.isPromise(o)?o:Promise.resolve(o):(Jl[s]=Yl.fetchJSON(s).then((o=>(Jl[s]=o,o))),Jl[s])}function extract(s,o){const i=jsonPointerToArray(s);if(i.length<1)return o;const a=Wo.getIn(o,i);if(void 0===a)throw new JSONRefError(`Could not resolve pointer: ${s} does not exist in document`,{pointer:s});return a}function jsonPointerToArray(s){if(\"string\"!=typeof s)throw new TypeError(\"Expected a string, got a \"+typeof s);return\"/\"===s[0]&&(s=s.substr(1)),\"\"===s?[]:s.split(\"/\").map(unescapeJsonPointerToken)}function unescapeJsonPointerToken(s){if(\"string\"!=typeof s)return s;return new URLSearchParams(`=${s.replace(/~1/g,\"/\").replace(/~0/g,\"~\")}`).get(\"\")}function escapeJsonPointerToken(s){return new URLSearchParams([[\"\",s.replace(/~/g,\"~0\").replace(/\\//g,\"~1\")]]).toString().slice(1)}const pointerBoundaryChar=s=>!s||\"/\"===s||\"#\"===s;function pointerIsAParent(s,o){if(pointerBoundaryChar(o))return!0;const i=s.charAt(o.length),a=o.slice(-1);return 0===s.indexOf(o)&&(!i||\"/\"===i||\"#\"===i)&&\"#\"!==a}const Ql={key:\"allOf\",plugin:(s,o,i,a,u)=>{if(u.meta&&u.meta.$$ref)return;const _=i.slice(0,-1);if(isFreelyNamed(_))return;if(!Array.isArray(s)){const s=new TypeError(\"allOf must be an array\");return s.fullPath=i,s}let w=!1,x=u.value;if(_.forEach((s=>{x&&(x=x[s])})),x={...x},0===Object.keys(x).length)return;delete x.allOf;const C=[];return C.push(a.replace(_,{})),s.forEach(((s,o)=>{if(!a.isObject(s)){if(w)return null;w=!0;const s=new TypeError(\"Elements in allOf must be objects\");return s.fullPath=i,C.push(s)}C.push(a.mergeDeep(_,s));const u=function generateAbsoluteRefPatches(s,o,{specmap:i,getBaseUrlForNodePath:a=s=>i.getContext([...o,...s]).baseDoc,targetKeys:u=[\"$ref\",\"$$ref\"]}={}){const _=[];return Rl(s).forEach((function callback(){if(u.includes(this.key)&&\"string\"==typeof this.node){const s=this.path,u=o.concat(this.path),w=absolutifyPointer(this.node,a(s));_.push(i.replace(u,w))}})),_}(s,i.slice(0,-1),{getBaseUrlForNodePath:s=>a.getContext([...i,o,...s]).baseDoc,specmap:a});C.push(...u)})),x.example&&C.push(a.remove([].concat(_,\"example\"))),C.push(a.mergeDeep(_,x)),x.$$ref||C.push(a.remove([].concat(_,\"$$ref\"))),C}},Zl={key:\"parameters\",plugin:(s,o,i,a)=>{if(Array.isArray(s)&&s.length){const o=Object.assign([],s),u=i.slice(0,-1),_={...Wo.getIn(a.spec,u)};for(let u=0;u<s.length;u+=1){const w=s[u];try{o[u].default=a.parameterMacro(_,w)}catch(s){const o=new Error(s);return o.fullPath=i,o}}return Wo.replace(i,o)}return Wo.replace(i,s)}},eu={key:\"properties\",plugin:(s,o,i,a)=>{const u={...s};for(const o in s)try{u[o].default=a.modelPropertyMacro(u[o])}catch(s){const o=new Error(s);return o.fullPath=i,o}return Wo.replace(i,u)}};class ContextTree{constructor(s){this.root=context_tree_createNode(s||{})}set(s,o){const i=this.getParent(s,!0);if(!i)return void context_tree_updateNode(this.root,o,null);const a=s[s.length-1],{children:u}=i;u[a]?context_tree_updateNode(u[a],o,i):u[a]=context_tree_createNode(o,i)}get(s){if((s=s||[]).length<1)return this.root.value;let o,i,a=this.root;for(let u=0;u<s.length&&(i=s[u],o=a.children,o[i]);u+=1)a=o[i];return a&&a.protoValue}getParent(s,o){return!s||s.length<1?null:s.length<2?this.root:s.slice(0,-1).reduce(((s,i)=>{if(!s)return s;const{children:a}=s;return!a[i]&&o&&(a[i]=context_tree_createNode(null,s)),a[i]}),this.root)}}function context_tree_createNode(s,o){return context_tree_updateNode({children:{}},s,o)}function context_tree_updateNode(s,o,i){return s.value=o||{},s.protoValue=i?{...i.protoValue,...s.value}:s.value,Object.keys(s.children).forEach((o=>{const i=s.children[o];s.children[o]=context_tree_updateNode(i,i.value,s)})),s}const specmap_noop=()=>{};class SpecMap{static getPluginName(s){return s.pluginName}static getPatchesOfType(s,o){return s.filter(o)}constructor(s){Object.assign(this,{spec:\"\",debugLevel:\"info\",plugins:[],pluginHistory:{},errors:[],mutations:[],promisedPatches:[],state:{},patches:[],context:{},contextTree:new ContextTree,showDebug:!1,allPatches:[],pluginProp:\"specMap\",libMethods:Object.assign(Object.create(this),Wo,{getInstance:()=>this}),allowMetaPatches:!1},s),this.get=this._get.bind(this),this.getContext=this._getContext.bind(this),this.hasRun=this._hasRun.bind(this),this.wrappedPlugins=this.plugins.map(this.wrapPlugin.bind(this)).filter(Wo.isFunction),this.patches.push(Wo.add([],this.spec)),this.patches.push(Wo.context([],this.context)),this.updatePatches(this.patches)}debug(s,...o){this.debugLevel===s&&console.log(...o)}verbose(s,...o){\"verbose\"===this.debugLevel&&console.log(`[${s}]   `,...o)}wrapPlugin(s,o){const{pathDiscriminator:i}=this;let a,u=null;return s[this.pluginProp]?(u=s,a=s[this.pluginProp]):Wo.isFunction(s)?a=s:Wo.isObject(s)&&(a=function createKeyBasedPlugin(s){const isSubPath=(s,o)=>!Array.isArray(s)||s.every(((s,i)=>s===o[i]));return function*generator(o,a){const u={};for(const[s,i]of o.filter(Wo.isAdditiveMutation).entries()){if(!(s<Bl))return;yield*traverse(i.value,i.path,i)}function*traverse(o,_,w){if(Wo.isObject(o)){const x=_.length-1,C=_[x],j=_.indexOf(\"properties\"),L=\"properties\"===C&&x===j,B=a.allowMetaPatches&&u[o.$$ref];for(const x of Object.keys(o)){const C=o[x],j=_.concat(x),$=Wo.isObject(C),U=o.$$ref;if(B||$&&(a.allowMetaPatches&&U&&isSubPath(i,j)&&(u[U]=!0),yield*traverse(C,j,w)),!L&&x===s.key){const o=isSubPath(i,_);i&&!o||(yield s.plugin(C,x,j,a,w))}}}else s.key===_[_.length-1]&&(yield s.plugin(o,s.key,_,a))}}}(s)),Object.assign(a.bind(u),{pluginName:s.name||o,isGenerator:Wo.isGenerator(a)})}nextPlugin(){return this.wrappedPlugins.find((s=>this.getMutationsForPlugin(s).length>0))}nextPromisedPatch(){if(this.promisedPatches.length>0)return Promise.race(this.promisedPatches.map((s=>s.value)))}getPluginHistory(s){const o=this.constructor.getPluginName(s);return this.pluginHistory[o]||[]}getPluginRunCount(s){return this.getPluginHistory(s).length}getPluginHistoryTip(s){const o=this.getPluginHistory(s);return o&&o[o.length-1]||{}}getPluginMutationIndex(s){const o=this.getPluginHistoryTip(s).mutationIndex;return\"number\"!=typeof o?-1:o}updatePluginHistory(s,o){const i=this.constructor.getPluginName(s);this.pluginHistory[i]=this.pluginHistory[i]||[],this.pluginHistory[i].push(o)}updatePatches(s){Wo.normalizeArray(s).forEach((s=>{if(s instanceof Error)this.errors.push(s);else try{if(!Wo.isObject(s))return void this.debug(\"updatePatches\",\"Got a non-object patch\",s);if(this.showDebug&&this.allPatches.push(s),Wo.isPromise(s.value))return this.promisedPatches.push(s),void this.promisedPatchThen(s);if(Wo.isContextPatch(s))return void this.setContext(s.path,s.value);Wo.isMutation(s)&&this.updateMutations(s)}catch(s){console.error(s),this.errors.push(s)}}))}updateMutations(s){\"object\"==typeof s.value&&!Array.isArray(s.value)&&this.allowMetaPatches&&(s.value={...s.value});const o=Wo.applyPatch(this.state,s,{allowMetaPatches:this.allowMetaPatches});o&&(this.mutations.push(s),this.state=o)}removePromisedPatch(s){const o=this.promisedPatches.indexOf(s);o<0?this.debug(\"Tried to remove a promisedPatch that isn't there!\"):this.promisedPatches.splice(o,1)}promisedPatchThen(s){return s.value=s.value.then((o=>{const i={...s,value:o};this.removePromisedPatch(s),this.updatePatches(i)})).catch((o=>{this.removePromisedPatch(s),this.updatePatches(o)})),s.value}getMutations(s,o){return s=s||0,\"number\"!=typeof o&&(o=this.mutations.length),this.mutations.slice(s,o)}getCurrentMutations(){return this.getMutationsForPlugin(this.getCurrentPlugin())}getMutationsForPlugin(s){const o=this.getPluginMutationIndex(s);return this.getMutations(o+1)}getCurrentPlugin(){return this.currentPlugin}getLib(){return this.libMethods}_get(s){return Wo.getIn(this.state,s)}_getContext(s){return this.contextTree.get(s)}setContext(s,o){return this.contextTree.set(s,o)}_hasRun(s){return this.getPluginRunCount(this.getCurrentPlugin())>(s||0)}dispatch(){const s=this,o=this.nextPlugin();if(!o){const s=this.nextPromisedPatch();if(s)return s.then((()=>this.dispatch())).catch((()=>this.dispatch()));const o={spec:this.state,errors:this.errors};return this.showDebug&&(o.patches=this.allPatches),Promise.resolve(o)}if(s.pluginCount=s.pluginCount||new WeakMap,s.pluginCount.set(o,(s.pluginCount.get(o)||0)+1),s.pluginCount[o]>100)return Promise.resolve({spec:s.state,errors:s.errors.concat(new Error(\"We've reached a hard limit of 100 plugin runs\"))});if(o!==this.currentPlugin&&this.promisedPatches.length){const s=this.promisedPatches.map((s=>s.value));return Promise.all(s.map((s=>s.then(specmap_noop,specmap_noop)))).then((()=>this.dispatch()))}return function executePlugin(){s.currentPlugin=o;const i=s.getCurrentMutations(),a=s.mutations.length-1;try{if(o.isGenerator)for(const a of o(i,s.getLib()))updatePatches(a);else{updatePatches(o(i,s.getLib()))}}catch(s){console.error(s),updatePatches([Object.assign(Object.create(s),{plugin:o})])}finally{s.updatePluginHistory(o,{mutationIndex:a})}return s.dispatch()}();function updatePatches(i){i&&(i=Wo.fullyNormalizeArray(i),s.updatePatches(i,o))}}}const tu={refs:Xl,allOf:Ql,parameters:Zl,properties:eu};function makeFetchJSON(s,o={}){const{requestInterceptor:i,responseInterceptor:a}=o,u=s.withCredentials?\"include\":\"same-origin\";return o=>s({url:o,loadSpec:!0,requestInterceptor:i,responseInterceptor:a,headers:{Accept:Dl},credentials:u}).then((s=>s.body))}function isFile(s,o){return o||\"undefined\"==typeof navigator||(o=navigator),o&&\"ReactNative\"===o.product?!(!s||\"object\"!=typeof s||\"string\"!=typeof s.uri):\"undefined\"!=typeof File&&s instanceof File||(\"undefined\"!=typeof Blob&&s instanceof Blob||(!!ArrayBuffer.isView(s)||null!==s&&\"object\"==typeof s&&\"function\"==typeof s.pipe))}function isArrayOfFile(s,o){return Array.isArray(s)&&s.some((s=>isFile(s,o)))}class FileWithData extends File{constructor(s,o=\"\",i={}){super([s],o,i),this.data=s}valueOf(){return this.data}toString(){return this.valueOf()}}const isRfc3986Reserved=s=>\":/?#[]@!$&'()*+,;=\".indexOf(s)>-1,isRfc3986Unreserved=s=>/^[a-z0-9\\-._~]+$/i.test(s);function encodeCharacters(s,o=\"reserved\"){return[...s].map((s=>{if(isRfc3986Unreserved(s))return s;if(isRfc3986Reserved(s)&&\"unsafe\"===o)return s;const i=new TextEncoder;return Array.from(i.encode(s)).map((s=>`0${s.toString(16).toUpperCase()}`.slice(-2))).map((s=>`%${s}`)).join(\"\")})).join(\"\")}function stylize(s){const{value:o}=s;return Array.isArray(o)?function encodeArray({key:s,value:o,style:i,explode:a,escape:u}){if(\"simple\"===i)return o.map((s=>valueEncoder(s,u))).join(\",\");if(\"label\"===i)return`.${o.map((s=>valueEncoder(s,u))).join(\".\")}`;if(\"matrix\"===i)return o.map((s=>valueEncoder(s,u))).reduce(((o,i)=>!o||a?`${o||\"\"};${s}=${i}`:`${o},${i}`),\"\");if(\"form\"===i){const i=a?`&${s}=`:\",\";return o.map((s=>valueEncoder(s,u))).join(i)}if(\"spaceDelimited\"===i){const i=a?`${s}=`:\"\";return o.map((s=>valueEncoder(s,u))).join(` ${i}`)}if(\"pipeDelimited\"===i){const i=a?`${s}=`:\"\";return o.map((s=>valueEncoder(s,u))).join(`|${i}`)}return}(s):\"object\"==typeof o?function encodeObject({key:s,value:o,style:i,explode:a,escape:u}){const _=Object.keys(o);if(\"simple\"===i)return _.reduce(((s,i)=>{const _=valueEncoder(o[i],u);return`${s?`${s},`:\"\"}${i}${a?\"=\":\",\"}${_}`}),\"\");if(\"label\"===i)return _.reduce(((s,i)=>{const _=valueEncoder(o[i],u);return`${s?`${s}.`:\".\"}${i}${a?\"=\":\".\"}${_}`}),\"\");if(\"matrix\"===i&&a)return _.reduce(((s,i)=>`${s?`${s};`:\";\"}${i}=${valueEncoder(o[i],u)}`),\"\");if(\"matrix\"===i)return _.reduce(((i,a)=>{const _=valueEncoder(o[a],u);return`${i?`${i},`:`;${s}=`}${a},${_}`}),\"\");if(\"form\"===i)return _.reduce(((s,i)=>{const _=valueEncoder(o[i],u);return`${s?`${s}${a?\"&\":\",\"}`:\"\"}${i}${a?\"=\":\",\"}${_}`}),\"\");return}(s):function encodePrimitive({key:s,value:o,style:i,escape:a}){if(\"simple\"===i)return valueEncoder(o,a);if(\"label\"===i)return`.${valueEncoder(o,a)}`;if(\"matrix\"===i)return`;${s}=${valueEncoder(o,a)}`;if(\"form\"===i)return valueEncoder(o,a);if(\"deepObject\"===i)return valueEncoder(o,a);return}(s)}function valueEncoder(s,o=!1){return Array.isArray(s)||null!==s&&\"object\"==typeof s?s=JSON.stringify(s):\"number\"!=typeof s&&\"boolean\"!=typeof s||(s=String(s)),o&&\"string\"==typeof s&&s.length>0?encodeCharacters(s,o):null!=s?s:\"\"}const ru={form:\",\",spaceDelimited:\"%20\",pipeDelimited:\"|\"},nu={csv:\",\",ssv:\"%20\",tsv:\"%09\",pipes:\"|\"};function formatKeyValue(s,o,i=!1){const{collectionFormat:a,allowEmptyValue:u,serializationOption:_,encoding:w}=o,x=\"object\"!=typeof o||Array.isArray(o)?o:o.value,C=i?s=>s.toString():s=>encodeURIComponent(s),j=C(s);if(void 0===x&&u)return[[j,\"\"]];if(isFile(x)||isArrayOfFile(x))return[[j,x]];if(_)return formatKeyValueBySerializationOption(s,x,i,_);if(w){if([typeof w.style,typeof w.explode,typeof w.allowReserved].some((s=>\"undefined\"!==s))){const{style:o,explode:a,allowReserved:u}=w;return formatKeyValueBySerializationOption(s,x,i,{style:o,explode:a,allowReserved:u})}if(\"string\"==typeof w.contentType){if(w.contentType.startsWith(\"application/json\")){const s=C(\"string\"==typeof x?x:JSON.stringify(x));return[[j,new FileWithData(s,\"blob\",{type:w.contentType})]]}const s=C(String(x));return[[j,new FileWithData(s,\"blob\",{type:w.contentType})]]}return\"object\"!=typeof x?[[j,C(x)]]:Array.isArray(x)&&x.every((s=>\"object\"!=typeof s))?[[j,x.map(C).join(\",\")]]:[[j,C(JSON.stringify(x))]]}return\"object\"!=typeof x?[[j,C(x)]]:Array.isArray(x)?\"multi\"===a?[[j,x.map(C)]]:[[j,x.map(C).join(nu[a||\"csv\"])]]:[[j,\"\"]]}function formatKeyValueBySerializationOption(s,o,i,a){const u=a.style||\"form\",_=void 0===a.explode?\"form\"===u:a.explode,w=!i&&(a&&a.allowReserved?\"unsafe\":\"reserved\"),encodeFn=s=>valueEncoder(s,w),x=i?s=>s:s=>encodeFn(s);return\"object\"!=typeof o?[[x(s),encodeFn(o)]]:Array.isArray(o)?_?[[x(s),o.map(encodeFn)]]:[[x(s),o.map(encodeFn).join(ru[u])]]:\"deepObject\"===u?Object.keys(o).map((i=>[x(`${s}[${i}]`),encodeFn(o[i])])):_?Object.keys(o).map((s=>[x(s),encodeFn(o[s])])):[[x(s),Object.keys(o).map((s=>[`${x(s)},${encodeFn(o[s])}`])).join(\",\")]]}function encodeFormOrQuery(s){return((s,{encode:o=!0}={})=>{const buildNestedParams=(s,o,i)=>(Array.isArray(i)?i.reduce(((i,a)=>buildNestedParams(s,o,a)),s):i instanceof Date?s.append(o,i.toISOString()):\"object\"==typeof i?Object.entries(i).reduce(((i,[a,u])=>buildNestedParams(s,`${o}[${a}]`,u)),s):s.append(o,i),s),i=Object.entries(s).reduce(((s,[o,i])=>buildNestedParams(s,o,i)),new URLSearchParams),a=String(i);return o?a:decodeURIComponent(a)})(Object.keys(s).reduce(((o,i)=>{for(const[a,u]of formatKeyValue(i,s[i]))o[a]=u instanceof FileWithData?u.valueOf():u;return o}),{}),{encode:!1})}function serializeRequest(s={}){const{url:o=\"\",query:i,form:a}=s;if(a){const o=Object.keys(a).some((s=>{const{value:o}=a[s];return isFile(o)||isArrayOfFile(o)})),i=s.headers[\"content-type\"]||s.headers[\"Content-Type\"];if(o||/multipart\\/form-data/i.test(i)){const o=function request_buildFormData(s){return Object.entries(s).reduce(((s,[o,i])=>{for(const[a,u]of formatKeyValue(o,i,!0))if(Array.isArray(u))for(const o of u)if(ArrayBuffer.isView(o)){const i=new Blob([o]);s.append(a,i)}else s.append(a,o);else if(ArrayBuffer.isView(u)){const o=new Blob([u]);s.append(a,o)}else s.append(a,u);return s}),new FormData)}(s.form);s.formdata=o,s.body=o}else s.body=encodeFormOrQuery(a);delete s.form}if(i){const[a,u]=o.split(\"?\");let _=\"\";if(u){const s=new URLSearchParams(u);Object.keys(i).forEach((o=>s.delete(o))),_=String(s)}const w=((...s)=>{const o=s.filter((s=>s)).join(\"&\");return o?`?${o}`:\"\"})(_,encodeFormOrQuery(i));s.url=a+w,delete s.query}return s}function serializeHeaders(s={}){return\"function\"!=typeof s.entries?{}:Array.from(s.entries()).reduce(((s,[o,i])=>(s[o]=function serializeHeaderValue(s){return s.includes(\", \")?s.split(\", \"):s}(i),s)),{})}function serializeResponse(s,o,{loadSpec:i=!1}={}){const a={ok:s.ok,url:s.url||o,status:s.status,statusText:s.statusText,headers:serializeHeaders(s.headers)},u=a.headers[\"content-type\"],_=i||((s=\"\")=>/(json|xml|yaml|text)\\b/.test(s))(u);return(_?s.text:s.blob||s.buffer).call(s).then((s=>{if(a.text=s,a.data=s,_)try{const o=function parseBody(s,o){if(o){if(0===o.indexOf(\"application/json\")||o.indexOf(\"+json\")>0)return JSON.parse(s);if(0===o.indexOf(\"application/xml\")||o.indexOf(\"+xml\")>0)return s}return fn.load(s)}(s,u);a.body=o,a.obj=o}catch(s){a.parseError=s}return a}))}async function http_http(s,o={}){\"object\"==typeof s&&(s=(o=s).url),o.headers=o.headers||{},(o=serializeRequest(o)).headers&&Object.keys(o.headers).forEach((s=>{const i=o.headers[s];\"string\"==typeof i&&(o.headers[s]=i.replace(/\\n+/g,\" \"))})),o.requestInterceptor&&(o=await o.requestInterceptor(o)||o);const i=o.headers[\"content-type\"]||o.headers[\"Content-Type\"];let a;/multipart\\/form-data/i.test(i)&&(delete o.headers[\"content-type\"],delete o.headers[\"Content-Type\"]);try{a=await(o.userFetch||fetch)(o.url,o),a=await serializeResponse(a,s,o),o.responseInterceptor&&(a=await o.responseInterceptor(a)||a)}catch(s){if(!a)throw s;const o=new Error(a.statusText||`response status is ${a.status}`);throw o.status=a.status,o.statusCode=a.status,o.responseError=s,o}if(!a.ok){const s=new Error(a.statusText||`response status is ${a.status}`);throw s.status=a.status,s.statusCode=a.status,s.response=a,s}return a}const options_retrievalURI=s=>{var o,i;const{baseDoc:a,url:u}=s,_=null!==(o=null!=a?a:u)&&void 0!==o?o:\"\";return\"string\"==typeof(null===(i=globalThis.document)||void 0===i?void 0:i.baseURI)?String(new URL(_,globalThis.document.baseURI)):_},options_httpClient=s=>{const{fetch:o,http:i}=s;return o||i||http_http};async function resolveGenericStrategy(s){const{spec:o,mode:i,allowMetaPatches:a=!0,pathDiscriminator:u,modelPropertyMacro:_,parameterMacro:w,requestInterceptor:x,responseInterceptor:C,skipNormalization:j=!1,useCircularStructures:L,strategies:B}=s,$=options_retrievalURI(s),U=options_httpClient(s),V=B.find((s=>s.match(o)));return async function doResolve(s){$&&(tu.refs.docCache[$]=s);tu.refs.fetchJSON=makeFetchJSON(U,{requestInterceptor:x,responseInterceptor:C});const o=[tu.refs];\"function\"==typeof w&&o.push(tu.parameters);\"function\"==typeof _&&o.push(tu.properties);\"strict\"!==i&&o.push(tu.allOf);const B=await function mapSpec(s){return new SpecMap(s).dispatch()}({spec:s,context:{baseDoc:$},plugins:o,allowMetaPatches:a,pathDiscriminator:u,parameterMacro:w,modelPropertyMacro:_,useCircularStructures:L});j||(B.spec=V.normalize(B.spec));return B}(o)}const su=_curry2((function and(s,o){return s&&o}));const ou=_curry2((function both(s,o){return _isFunction(s)?function _both(){return s.apply(this,arguments)&&o.apply(this,arguments)}:hc(su)(s,o)}));const iu=na(null);const au=dc(iu);function isOfTypeObject_typeof(s){return isOfTypeObject_typeof=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(s){return typeof s}:function(s){return s&&\"function\"==typeof Symbol&&s.constructor===Symbol&&s!==Symbol.prototype?\"symbol\":typeof s},isOfTypeObject_typeof(s)}const cu=function isOfTypeObject(s){return\"object\"===isOfTypeObject_typeof(s)};const lu=$a(1,ou(au,cu));var uu=pipe(ra,Pc(\"Object\")),pu=pipe(ga,na(ga(Object))),hu=Qo(ou(Mc,pu),[\"constructor\"]),du=$a(1,(function(s){if(!lu(s)||!uu(s))return!1;var o=Object.getPrototypeOf(s);return!!iu(o)||hu(o)}));const fu=du,replace_special_chars_with_underscore=s=>s.replace(/\\W/gi,\"_\");function opId(s,o,i=\"\",{v2OperationIdCompatibilityMode:a}={}){if(!s||\"object\"!=typeof s)return null;return(s.operationId||\"\").replace(/\\s/g,\"\").length?replace_special_chars_with_underscore(s.operationId):function idFromPathMethod(s,o,{v2OperationIdCompatibilityMode:i}={}){if(i){let i=`${o.toLowerCase()}_${s}`.replace(/[\\s!@#$%^&*()_+=[{\\]};:<>|./?,\\\\'\"\"-]/g,\"_\");return i=i||`${s.substring(1)}_${o}`,i.replace(/((_){2,})/g,\"_\").replace(/^(_)*/g,\"\").replace(/([_])*$/g,\"\")}return`${o.toLowerCase()}${replace_special_chars_with_underscore(s)}`}(o,i,{v2OperationIdCompatibilityMode:a})}function normalize_normalize(s){const{spec:o}=s,{paths:i}=o,a={};if(!i||o.$$normalized)return s;for(const s in i){const u=i[s];if(null==u||![\"object\",\"function\"].includes(typeof u))continue;const _=u.parameters;for(const i in u){const w=u[i];if(null==w||![\"object\",\"function\"].includes(typeof w))continue;const x=opId(w,s,i);if(x){a[x]?a[x].push(w):a[x]=[w];const s=a[x];if(s.length>1)s.forEach(((s,o)=>{s.__originalOperationId=s.__originalOperationId||s.operationId,s.operationId=`${x}${o+1}`}));else if(void 0!==w.operationId){const o=s[0];o.__originalOperationId=o.__originalOperationId||w.operationId,o.operationId=x}}if(\"parameters\"!==i){const s=[],i={};for(const a in o)\"produces\"!==a&&\"consumes\"!==a&&\"security\"!==a||(i[a]=o[a],s.push(i));if(_&&(i.parameters=_,s.push(i)),s.length)for(const o of s)for(const s in o)if(Array.isArray(w[s])){if(\"parameters\"===s)for(const i of o[s]){w[s].some((s=>!(!fu(s)&&!fu(i))&&(s===i||[\"name\",\"$ref\",\"$$ref\"].some((o=>\"string\"==typeof s[o]&&\"string\"==typeof i[o]&&s[o]===i[o])))))||w[s].push(i)}}else w[s]=o[s]}}}return o.$$normalized=!0,s}const mu={name:\"generic\",match:()=>!0,normalize(s){const{spec:o}=normalize_normalize({spec:s});return o},resolve:async s=>resolveGenericStrategy(s)},gu=mu;const isOpenAPI30=s=>{try{const{openapi:o}=s;return\"string\"==typeof o&&/^3\\.0\\.(?:[1-9]\\d*|0)$/.test(o)}catch{return!1}},isOpenAPI31=s=>{try{const{openapi:o}=s;return\"string\"==typeof o&&/^3\\.1\\.(?:[1-9]\\d*|0)$/.test(o)}catch{return!1}},isOpenAPI3=s=>isOpenAPI30(s)||isOpenAPI31(s),yu={name:\"openapi-2\",match:s=>(s=>{try{const{swagger:o}=s;return\"2.0\"===o}catch{return!1}})(s),normalize(s){const{spec:o}=normalize_normalize({spec:s});return o},resolve:async s=>async function resolveOpenAPI2Strategy(s){return resolveGenericStrategy(s)}(s)},vu=yu;const bu={name:\"openapi-3-0\",match:s=>isOpenAPI30(s),normalize(s){const{spec:o}=normalize_normalize({spec:s});return o},resolve:async s=>async function resolveOpenAPI30Strategy(s){return resolveGenericStrategy(s)}(s)},_u=bu;var Su=__webpack_require__(34035);function _reduced(s){return s&&s[\"@@transducer/reduced\"]?s:{\"@@transducer/value\":s,\"@@transducer/reduced\":!0}}var Eu=function(){function XAll(s,o){this.xf=o,this.f=s,this.all=!0}return XAll.prototype[\"@@transducer/init\"]=_xfBase_init,XAll.prototype[\"@@transducer/result\"]=function(s){return this.all&&(s=this.xf[\"@@transducer/step\"](s,!0)),this.xf[\"@@transducer/result\"](s)},XAll.prototype[\"@@transducer/step\"]=function(s,o){return this.f(o)||(this.all=!1,s=_reduced(this.xf[\"@@transducer/step\"](s,!1))),s},XAll}();function _xall(s){return function(o){return new Eu(s,o)}}var wu=_curry2(_dispatchable([\"all\"],_xall,(function all(s,o){for(var i=0;i<o.length;){if(!s(o[i]))return!1;i+=1}return!0})));const xu=wu;class Annotation extends Su.Om{constructor(s,o,i){super(s,o,i),this.element=\"annotation\"}get code(){return this.attributes.get(\"code\")}set code(s){this.attributes.set(\"code\",s)}}const ku=Annotation;class Comment extends Su.Om{constructor(s,o,i){super(s,o,i),this.element=\"comment\"}}const Ou=Comment;class ParseResult extends Su.wE{constructor(s,o,i){super(s,o,i),this.element=\"parseResult\"}get api(){return this.children.filter((s=>s.classes.contains(\"api\"))).first}get results(){return this.children.filter((s=>s.classes.contains(\"result\")))}get result(){return this.results.first}get annotations(){return this.children.filter((s=>\"annotation\"===s.element))}get warnings(){return this.children.filter((s=>\"annotation\"===s.element&&s.classes.contains(\"warning\")))}get errors(){return this.children.filter((s=>\"annotation\"===s.element&&s.classes.contains(\"error\")))}get isEmpty(){return this.children.reject((s=>\"annotation\"===s.element)).isEmpty}replaceResult(s){const{result:o}=this;if(bc(o))return!1;const i=this.content.findIndex((s=>s===o));return-1!==i&&(this.content[i]=s,!0)}}const Au=ParseResult,hasMethod=(s,o)=>\"object\"==typeof o&&null!==o&&s in o&&\"function\"==typeof o[s],hasBasicElementProps=s=>\"object\"==typeof s&&null!=s&&\"_storedElement\"in s&&\"string\"==typeof s._storedElement&&\"_content\"in s,primitiveEq=(s,o)=>\"object\"==typeof o&&null!==o&&\"primitive\"in o&&(\"function\"==typeof o.primitive&&o.primitive()===s),hasClass=(s,o)=>\"object\"==typeof o&&null!==o&&\"classes\"in o&&(Array.isArray(o.classes)||o.classes instanceof Su.wE)&&o.classes.includes(s),isElementType=(s,o)=>\"object\"==typeof o&&null!==o&&\"element\"in o&&o.element===s,helpers=s=>s({hasMethod,hasBasicElementProps,primitiveEq,isElementType,hasClass}),Cu=helpers((({hasBasicElementProps:s,primitiveEq:o})=>i=>i instanceof Su.Hg||s(i)&&o(void 0,i))),ju=helpers((({hasBasicElementProps:s,primitiveEq:o})=>i=>i instanceof Su.Om||s(i)&&o(\"string\",i))),Pu=helpers((({hasBasicElementProps:s,primitiveEq:o})=>i=>i instanceof Su.kT||s(i)&&o(\"number\",i))),Iu=helpers((({hasBasicElementProps:s,primitiveEq:o})=>i=>i instanceof Su.Os||s(i)&&o(\"null\",i))),Tu=helpers((({hasBasicElementProps:s,primitiveEq:o})=>i=>i instanceof Su.bd||s(i)&&o(\"boolean\",i))),Nu=helpers((({hasBasicElementProps:s,primitiveEq:o,hasMethod:i})=>a=>a instanceof Su.Sh||s(a)&&o(\"object\",a)&&i(\"keys\",a)&&i(\"values\",a)&&i(\"items\",a))),Mu=helpers((({hasBasicElementProps:s,primitiveEq:o,hasMethod:i})=>a=>a instanceof Su.wE&&!(a instanceof Su.Sh)||s(a)&&o(\"array\",a)&&i(\"push\",a)&&i(\"unshift\",a)&&i(\"map\",a)&&i(\"reduce\",a))),Ru=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Su.Pr||s(a)&&o(\"member\",a)&&i(void 0,a))),Du=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Su.Ft||s(a)&&o(\"link\",a)&&i(void 0,a))),Lu=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Su.sI||s(a)&&o(\"ref\",a)&&i(void 0,a))),Fu=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof ku||s(a)&&o(\"annotation\",a)&&i(\"array\",a))),Bu=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Ou||s(a)&&o(\"comment\",a)&&i(\"string\",a))),$u=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Au||s(a)&&o(\"parseResult\",a)&&i(\"array\",a))),isPrimitiveElement=s=>isElementType(\"object\",s)||isElementType(\"array\",s)||isElementType(\"boolean\",s)||isElementType(\"number\",s)||isElementType(\"string\",s)||isElementType(\"null\",s)||isElementType(\"member\",s),hasElementSourceMap=s=>!!Cu(s)&&(Number.isInteger(s.startPositionRow)&&Number.isInteger(s.startPositionColumn)&&Number.isInteger(s.startIndex)&&Number.isInteger(s.endPositionRow)&&Number.isInteger(s.endPositionColumn)&&Number.isInteger(s.endIndex)),includesSymbols=(s,o)=>{if(0===s.length)return!0;const i=o.attributes.get(\"symbols\");return!!Mu(i)&&xu(sc(i.toValue()),s)},includesClasses=(s,o)=>0===s.length||xu(sc(o.classes.toValue()),s);const es_T=function(){return!0};const es_F=function(){return!1},getVisitFn=(s,o,i)=>{const a=s[o];if(null!=a){if(!i&&\"function\"==typeof a)return a;const s=i?a.leave:a.enter;if(\"function\"==typeof s)return s}else{const a=i?s.leave:s.enter;if(null!=a){if(\"function\"==typeof a)return a;const s=a[o];if(\"function\"==typeof s)return s}}return null},qu={},getNodeType=s=>null==s?void 0:s.type,isNode=s=>\"string\"==typeof getNodeType(s),cloneNode=s=>Object.create(Object.getPrototypeOf(s),Object.getOwnPropertyDescriptors(s)),mergeAll=(s,{visitFnGetter:o=getVisitFn,nodeTypeGetter:i=getNodeType,breakSymbol:a=qu,deleteNodeSymbol:u=null,skipVisitingNodeSymbol:_=!1,exposeEdits:w=!1}={})=>{const x=Symbol(\"skip\"),C=new Array(s.length).fill(x);return{enter(j,L,B,$,U,V){let z=j,Y=!1;const Z={...V,replaceWith(s,o){V.replaceWith(s,o),z=s}};for(let j=0;j<s.length;j+=1)if(C[j]===x){const x=o(s[j],i(z),!1);if(\"function\"==typeof x){const o=x.call(s[j],z,L,B,$,U,Z);if(\"function\"==typeof(null==o?void 0:o.then))throw new Go(\"Async visitor not supported in sync mode\",{visitor:s[j],visitFn:x});if(o===_)C[j]=z;else if(o===a)C[j]=a;else{if(o===u)return o;if(void 0!==o){if(!w)return o;z=o,Y=!0}}}}return Y?z:void 0},leave(u,w,j,L,B,$){let U=u;const V={...$,replaceWith(s,o){$.replaceWith(s,o),U=s}};for(let u=0;u<s.length;u+=1)if(C[u]===x){const x=o(s[u],i(U),!0);if(\"function\"==typeof x){const o=x.call(s[u],U,w,j,L,B,V);if(\"function\"==typeof(null==o?void 0:o.then))throw new Go(\"Async visitor not supported in sync mode\",{visitor:s[u],visitFn:x});if(o===a)C[u]=a;else if(void 0!==o&&o!==_)return o}}else C[u]===U&&(C[u]=x)}}};mergeAll[Symbol.for(\"nodejs.util.promisify.custom\")]=(s,{visitFnGetter:o=getVisitFn,nodeTypeGetter:i=getNodeType,breakSymbol:a=qu,deleteNodeSymbol:u=null,skipVisitingNodeSymbol:_=!1,exposeEdits:w=!1}={})=>{const x=Symbol(\"skip\"),C=new Array(s.length).fill(x);return{async enter(j,L,B,$,U,V){let z=j,Y=!1;const Z={...V,replaceWith(s,o){V.replaceWith(s,o),z=s}};for(let j=0;j<s.length;j+=1)if(C[j]===x){const x=o(s[j],i(z),!1);if(\"function\"==typeof x){const o=await x.call(s[j],z,L,B,$,U,Z);if(o===_)C[j]=z;else if(o===a)C[j]=a;else{if(o===u)return o;if(void 0!==o){if(!w)return o;z=o,Y=!0}}}}return Y?z:void 0},async leave(u,w,j,L,B,$){let U=u;const V={...$,replaceWith(s,o){$.replaceWith(s,o),U=s}};for(let u=0;u<s.length;u+=1)if(C[u]===x){const x=o(s[u],i(U),!0);if(\"function\"==typeof x){const o=await x.call(s[u],U,w,j,L,B,V);if(o===a)C[u]=a;else if(void 0!==o&&o!==_)return o}}else C[u]===U&&(C[u]=x)}}};const visit=(s,o,{keyMap:i=null,state:a={},breakSymbol:u=qu,deleteNodeSymbol:_=null,skipVisitingNodeSymbol:w=!1,visitFnGetter:x=getVisitFn,nodeTypeGetter:C=getNodeType,nodePredicate:j=isNode,nodeCloneFn:L=cloneNode,detectCycles:B=!0,detectCyclesCallback:$=null}={})=>{const U=i||{};let V,z,Y=Array.isArray(s),Z=[s],ee=-1,ie=[],ae=s;const ce=[],le=[];do{ee+=1;const s=ee===Z.length;let i;const fe=s&&0!==ie.length;if(s){if(i=0===le.length?void 0:ce.pop(),ae=z,z=le.pop(),fe)if(Y){ae=ae.slice();let s=0;for(const[o,i]of ie){const a=o-s;i===_?(ae.splice(a,1),s+=1):ae[a]=i}}else{ae=L(ae);for(const[s,o]of ie)ae[s]=o}ee=V.index,Z=V.keys,ie=V.edits,Y=V.inArray,V=V.prev}else if(z!==_&&void 0!==z){if(i=Y?ee:Z[ee],ae=z[i],ae===_||void 0===ae)continue;ce.push(i)}let ye;if(!Array.isArray(ae)){var pe;if(!j(ae))throw new Go(`Invalid AST Node:  ${String(ae)}`,{node:ae});if(B&&le.includes(ae)){\"function\"==typeof $&&$(ae,i,z,ce,le),ce.pop();continue}const _=x(o,C(ae),s);if(_){for(const[s,i]of Object.entries(a))o[s]=i;const u={replaceWith(o,a){\"function\"==typeof a?a(o,ae,i,z,ce,le):z&&(z[i]=o),s||(ae=o)}};ye=_.call(o,ae,i,z,ce,le,u)}if(\"function\"==typeof(null===(pe=ye)||void 0===pe?void 0:pe.then))throw new Go(\"Async visitor not supported in sync mode\",{visitor:o,visitFn:_});if(ye===u)break;if(ye===w){if(!s){ce.pop();continue}}else if(void 0!==ye&&(ie.push([i,ye]),!s)){if(!j(ye)){ce.pop();continue}ae=ye}}var de;if(void 0===ye&&fe&&ie.push([i,ae]),!s)V={inArray:Y,index:ee,keys:Z,edits:ie,prev:V},Y=Array.isArray(ae),Z=Y?ae:null!==(de=U[C(ae)])&&void 0!==de?de:[],ee=-1,ie=[],z!==_&&void 0!==z&&le.push(z),z=ae}while(void 0!==V);return 0!==ie.length?ie[ie.length-1][1]:s};visit[Symbol.for(\"nodejs.util.promisify.custom\")]=async(s,o,{keyMap:i=null,state:a={},breakSymbol:u=qu,deleteNodeSymbol:_=null,skipVisitingNodeSymbol:w=!1,visitFnGetter:x=getVisitFn,nodeTypeGetter:C=getNodeType,nodePredicate:j=isNode,nodeCloneFn:L=cloneNode,detectCycles:B=!0,detectCyclesCallback:$=null}={})=>{const U=i||{};let V,z,Y=Array.isArray(s),Z=[s],ee=-1,ie=[],ae=s;const ce=[],le=[];do{ee+=1;const s=ee===Z.length;let i;const de=s&&0!==ie.length;if(s){if(i=0===le.length?void 0:ce.pop(),ae=z,z=le.pop(),de)if(Y){ae=ae.slice();let s=0;for(const[o,i]of ie){const a=o-s;i===_?(ae.splice(a,1),s+=1):ae[a]=i}}else{ae=L(ae);for(const[s,o]of ie)ae[s]=o}ee=V.index,Z=V.keys,ie=V.edits,Y=V.inArray,V=V.prev}else if(z!==_&&void 0!==z){if(i=Y?ee:Z[ee],ae=z[i],ae===_||void 0===ae)continue;ce.push(i)}let fe;if(!Array.isArray(ae)){if(!j(ae))throw new Go(`Invalid AST Node: ${String(ae)}`,{node:ae});if(B&&le.includes(ae)){\"function\"==typeof $&&$(ae,i,z,ce,le),ce.pop();continue}const _=x(o,C(ae),s);if(_){for(const[s,i]of Object.entries(a))o[s]=i;const u={replaceWith(o,a){\"function\"==typeof a?a(o,ae,i,z,ce,le):z&&(z[i]=o),s||(ae=o)}};fe=await _.call(o,ae,i,z,ce,le,u)}if(fe===u)break;if(fe===w){if(!s){ce.pop();continue}}else if(void 0!==fe&&(ie.push([i,fe]),!s)){if(!j(fe)){ce.pop();continue}ae=fe}}var pe;if(void 0===fe&&de&&ie.push([i,ae]),!s)V={inArray:Y,index:ee,keys:Z,edits:ie,prev:V},Y=Array.isArray(ae),Z=Y?ae:null!==(pe=U[C(ae)])&&void 0!==pe?pe:[],ee=-1,ie=[],z!==_&&void 0!==z&&le.push(z),z=ae}while(void 0!==V);return 0!==ie.length?ie[ie.length-1][1]:s};const Uu=class CloneError extends Go{value;constructor(s,o){super(s,o),void 0!==o&&(this.value=o.value)}};const Vu=class DeepCloneError extends Uu{};const zu=class ShallowCloneError extends Uu{};const Wu=_curry2((function mapObjIndexed(s,o){return _arrayReduce((function(i,a){return i[a]=s(o[a],a,o),i}),{},ea(o))}));const Ju=_curry1((function isNil(s){return null==s}));var Hu=_curry2((function hasPath(s,o){if(0===s.length||Ju(o))return!1;for(var i=o,a=0;a<s.length;){if(Ju(i)||!_has(s[a],i))return!1;i=i[s[a]],a+=1}return!0}));const Ku=Hu;var Gu=_curry2((function has(s,o){return Ku([s],o)}));const Yu=Gu;const Xu=_curry3((function propSatisfies(s,o,i){return s(Da(o,i))}));const Qu=_curry2(_path);var Zu=function(){function XDropWhile(s,o){this.xf=o,this.f=s}return XDropWhile.prototype[\"@@transducer/init\"]=_xfBase_init,XDropWhile.prototype[\"@@transducer/result\"]=_xfBase_result,XDropWhile.prototype[\"@@transducer/step\"]=function(s,o){if(this.f){if(this.f(o))return s;this.f=null}return this.xf[\"@@transducer/step\"](s,o)},XDropWhile}();function _xdropWhile(s){return function(o){return new Zu(s,o)}}const ep=_curry2(_dispatchable([\"dropWhile\"],_xdropWhile,(function dropWhile(s,o){for(var i=0,a=o.length;i<a&&s(o[i]);)i+=1;return ja(i,1/0,o)})));const tp=za((function(s,o){return pipe(Ha(\"\"),ep(sc(s)),rc(\"\"))(o)})),dereference=(s,o)=>{const i=Na(s,o);return Wu((s=>{if(fu(s)&&Yu(\"$ref\",s)&&Xu(Jc,\"$ref\",s)){const o=Qu([\"$ref\"],s),a=tp(\"#/\",o);return Qu(a.split(\"/\"),i)}return fu(s)?dereference(s,i):s}),s)},assignSourceMap=(s,o)=>(s.startPositionRow=null==o?void 0:o.startPositionRow,s.startPositionColumn=null==o?void 0:o.startPositionColumn,s.startIndex=null==o?void 0:o.startIndex,s.endPositionRow=null==o?void 0:o.endPositionRow,s.endPositionColumn=null==o?void 0:o.endPositionColumn,s.endIndex=null==o?void 0:o.endIndex,s),cloneDeep=(s,o={})=>{const{visited:i=new WeakMap}=o,a={...o,visited:i};if(i.has(s))return i.get(s);if(s instanceof Su.KeyValuePair){const{key:o,value:u}=s,_=Cu(o)?cloneDeep(o,a):o,w=Cu(u)?cloneDeep(u,a):u,x=new Su.KeyValuePair(_,w);return i.set(s,x),x}if(s instanceof Su.ot){const mapper=s=>cloneDeep(s,a),o=[...s].map(mapper),u=new Su.ot(o);return i.set(s,u),u}if(s instanceof Su.G6){const mapper=s=>cloneDeep(s,a),o=[...s].map(mapper),u=new Su.G6(o);return i.set(s,u),u}if(Cu(s)){const o=cloneShallow(s);if(i.set(s,o),s.content)if(Cu(s.content))o.content=cloneDeep(s.content,a);else if(s.content instanceof Su.KeyValuePair)o.content=cloneDeep(s.content,a);else if(Array.isArray(s.content)){const mapper=s=>cloneDeep(s,a);o.content=s.content.map(mapper)}else o.content=s.content;else o.content=s.content;return o}throw new Vu(\"Value provided to cloneDeep function couldn't be cloned\",{value:s})};cloneDeep.safe=s=>{try{return cloneDeep(s)}catch{return s}};const cloneShallowKeyValuePair=s=>{const{key:o,value:i}=s;return new Su.KeyValuePair(o,i)},cloneShallowElement=s=>{const o=new s.constructor;if(o.element=s.element,hasElementSourceMap(s)&&assignSourceMap(o,s),s.meta.length>0&&(o._meta=cloneDeep(s.meta)),s.attributes.length>0&&(o._attributes=cloneDeep(s.attributes)),Cu(s.content)){const i=s.content;o.content=cloneShallowElement(i)}else Array.isArray(s.content)?o.content=[...s.content]:s.content instanceof Su.KeyValuePair?o.content=cloneShallowKeyValuePair(s.content):o.content=s.content;return o},cloneShallow=s=>{if(s instanceof Su.KeyValuePair)return cloneShallowKeyValuePair(s);if(s instanceof Su.ot)return(s=>{const o=[...s];return new Su.ot(o)})(s);if(s instanceof Su.G6)return(s=>{const o=[...s];return new Su.G6(o)})(s);if(Cu(s))return cloneShallowElement(s);throw new zu(\"Value provided to cloneShallow function couldn't be cloned\",{value:s})};cloneShallow.safe=s=>{try{return cloneShallow(s)}catch{return s}};const visitor_getNodeType=s=>Nu(s)?\"ObjectElement\":Mu(s)?\"ArrayElement\":Ru(s)?\"MemberElement\":ju(s)?\"StringElement\":Tu(s)?\"BooleanElement\":Pu(s)?\"NumberElement\":Iu(s)?\"NullElement\":Du(s)?\"LinkElement\":Lu(s)?\"RefElement\":void 0,visitor_cloneNode=s=>Cu(s)?cloneShallow(s):cloneNode(s),rp=pipe(visitor_getNodeType,Jc),np={ObjectElement:[\"content\"],ArrayElement:[\"content\"],MemberElement:[\"key\",\"value\"],StringElement:[],BooleanElement:[],NumberElement:[],NullElement:[],RefElement:[],LinkElement:[],Annotation:[],Comment:[],ParseResultElement:[\"content\"]};class PredicateVisitor{result;predicate;returnOnTrue;returnOnFalse;constructor({predicate:s=es_F,returnOnTrue:o,returnOnFalse:i}={}){this.result=[],this.predicate=s,this.returnOnTrue=o,this.returnOnFalse=i}enter(s){return this.predicate(s)?(this.result.push(s),this.returnOnTrue):this.returnOnFalse}}const visitor_visit=(s,o,{keyMap:i=np,...a}={})=>visit(s,o,{keyMap:i,nodeTypeGetter:visitor_getNodeType,nodePredicate:rp,nodeCloneFn:visitor_cloneNode,...a});visitor_visit[Symbol.for(\"nodejs.util.promisify.custom\")]=async(s,o,{keyMap:i=np,...a}={})=>visit[Symbol.for(\"nodejs.util.promisify.custom\")](s,o,{keyMap:i,nodeTypeGetter:visitor_getNodeType,nodePredicate:rp,nodeCloneFn:visitor_cloneNode,...a});const nodeTypeGetter=s=>\"string\"==typeof(null==s?void 0:s.type)?s.type:visitor_getNodeType(s),sp={EphemeralObject:[\"content\"],EphemeralArray:[\"content\"],...np},value_visitor_visit=(s,o,{keyMap:i=sp,...a}={})=>visitor_visit(s,o,{keyMap:i,nodeTypeGetter,nodePredicate:es_T,detectCycles:!1,deleteNodeSymbol:Symbol.for(\"delete-node\"),skipVisitingNodeSymbol:Symbol.for(\"skip-visiting-node\"),...a});value_visitor_visit[Symbol.for(\"nodejs.util.promisify.custom\")]=async(s,{keyMap:o=sp,...i}={})=>visitor_visit[Symbol.for(\"nodejs.util.promisify.custom\")](s,visitor,{keyMap:o,nodeTypeGetter,nodePredicate:es_T,detectCycles:!1,deleteNodeSymbol:Symbol.for(\"delete-node\"),skipVisitingNodeSymbol:Symbol.for(\"skip-visiting-node\"),...i});const op=class EphemeralArray{type=\"EphemeralArray\";content=[];reference=void 0;constructor(s){this.content=s,this.reference=[]}toReference(){return this.reference}toArray(){return this.reference.push(...this.content),this.reference}};const ip=class EphemeralObject{type=\"EphemeralObject\";content=[];reference=void 0;constructor(s){this.content=s,this.reference={}}toReference(){return this.reference}toObject(){return Object.assign(this.reference,Object.fromEntries(this.content))}};class Visitor{ObjectElement={enter:s=>{if(this.references.has(s))return this.references.get(s).toReference();const o=new ip(s.content);return this.references.set(s,o),o}};EphemeralObject={leave:s=>s.toObject()};MemberElement={enter:s=>[s.key,s.value]};ArrayElement={enter:s=>{if(this.references.has(s))return this.references.get(s).toReference();const o=new op(s.content);return this.references.set(s,o),o}};EphemeralArray={leave:s=>s.toArray()};references=new WeakMap;BooleanElement(s){return s.toValue()}NumberElement(s){return s.toValue()}StringElement(s){return s.toValue()}NullElement(){return null}RefElement(s,...o){var i;const a=o[3];return\"EphemeralObject\"===(null===(i=a[a.length-1])||void 0===i?void 0:i.type)?Symbol.for(\"delete-node\"):String(s.toValue())}LinkElement(s){return ju(s.href)?s.href.toValue():\"\"}}const serializers_value=s=>Cu(s)?ju(s)||Pu(s)||Tu(s)||Iu(s)?s.toValue():value_visitor_visit(s,new Visitor):s;const cp=_curry3((function mergeWithKey(s,o,i){var a,u={};for(a in i=i||{},o=o||{})_has(a,o)&&(u[a]=_has(a,i)?s(a,o[a],i[a]):o[a]);for(a in i)_has(a,i)&&!_has(a,u)&&(u[a]=i[a]);return u}));const lp=_curry3((function mergeDeepWithKey(s,o,i){return cp((function(o,i,a){return _isObject(i)&&_isObject(a)?mergeDeepWithKey(s,i,a):s(o,i,a)}),o,i)}));const up=_curry2((function mergeDeepRight(s,o){return lp((function(s,o,i){return i}),s,o)}));const pp=ja(0,-1);const hp=_curry2((function apply(s,o){return s.apply(this,o)}));const dp=dc(Mc);var fp=_curry1((function empty(s){return null!=s&&\"function\"==typeof s[\"fantasy-land/empty\"]?s[\"fantasy-land/empty\"]():null!=s&&null!=s.constructor&&\"function\"==typeof s.constructor[\"fantasy-land/empty\"]?s.constructor[\"fantasy-land/empty\"]():null!=s&&\"function\"==typeof s.empty?s.empty():null!=s&&null!=s.constructor&&\"function\"==typeof s.constructor.empty?s.constructor.empty():ca(s)?[]:_isString(s)?\"\":_isObject(s)?{}:Ei(s)?function(){return arguments}():function _isTypedArray(s){var o=Object.prototype.toString.call(s);return\"[object Uint8ClampedArray]\"===o||\"[object Int8Array]\"===o||\"[object Uint8Array]\"===o||\"[object Int16Array]\"===o||\"[object Uint16Array]\"===o||\"[object Int32Array]\"===o||\"[object Uint32Array]\"===o||\"[object Float32Array]\"===o||\"[object Float64Array]\"===o||\"[object BigInt64Array]\"===o||\"[object BigUint64Array]\"===o}(s)?s.constructor.from(\"\"):void 0}));const mp=fp;const gp=_curry1((function isEmpty(s){return null!=s&&na(s,mp(s))}));const yp=$a(1,Mc(Array.isArray)?Array.isArray:pipe(ra,Pc(\"Array\")));const vp=ou(yp,gp);var bp=$a(3,(function(s,o,i){var a=Qu(s,i),u=Qu(pp(s),i);if(!dp(a)&&!vp(s)){var _=Ea(a,u);return hp(_,o)}}));const _p=bp;class Namespace extends Su.g${constructor(){super(),this.register(\"annotation\",ku),this.register(\"comment\",Ou),this.register(\"parseResult\",Au)}}const Sp=new Namespace,createNamespace=s=>{const o=new Namespace;return fu(s)&&o.use(s),o},Ep=Sp,toolbox=()=>({predicates:{...ie},namespace:Ep}),wp={toolboxCreator:toolbox,visitorOptions:{nodeTypeGetter:visitor_getNodeType,exposeEdits:!0}},dispatchPluginsSync=(s,o,i={})=>{if(0===o.length)return s;const a=up(wp,i),{toolboxCreator:u,visitorOptions:_}=a,w=u(),x=o.map((s=>s(w))),C=mergeAll(x.map(La({},\"visitor\")),{..._});x.forEach(_p([\"pre\"],[]));const j=visitor_visit(s,C,_);return x.forEach(_p([\"post\"],[])),j};dispatchPluginsSync[Symbol.for(\"nodejs.util.promisify.custom\")]=async(s,o,i={})=>{if(0===o.length)return s;const a=up(wp,i),{toolboxCreator:u,visitorOptions:_}=a,w=u(),x=o.map((s=>s(w))),C=mergeAll[Symbol.for(\"nodejs.util.promisify.custom\")],j=visitor_visit[Symbol.for(\"nodejs.util.promisify.custom\")],L=C(x.map(La({},\"visitor\")),{..._});await Promise.allSettled(x.map(_p([\"pre\"],[])));const B=await j(s,L,_);return await Promise.allSettled(x.map(_p([\"post\"],[]))),B};const refract=(s,{Type:o,plugins:i=[]})=>{const a=new o(s);return Cu(s)&&(s.meta.length>0&&(a.meta=cloneDeep(s.meta)),s.attributes.length>0&&(a.attributes=cloneDeep(s.attributes))),dispatchPluginsSync(a,i,{toolboxCreator:toolbox,visitorOptions:{nodeTypeGetter:visitor_getNodeType}})},createRefractor=s=>(o,i={})=>refract(o,{...i,Type:s});Su.Sh.refract=createRefractor(Su.Sh),Su.wE.refract=createRefractor(Su.wE),Su.Om.refract=createRefractor(Su.Om),Su.bd.refract=createRefractor(Su.bd),Su.Os.refract=createRefractor(Su.Os),Su.kT.refract=createRefractor(Su.kT),Su.Ft.refract=createRefractor(Su.Ft),Su.sI.refract=createRefractor(Su.sI),ku.refract=createRefractor(ku),Ou.refract=createRefractor(Ou),Au.refract=createRefractor(Au);const computeEdges=(s,o=new WeakMap)=>(Ru(s)?(o.set(s.key,s),computeEdges(s.key,o),o.set(s.value,s),computeEdges(s.value,o)):s.children.forEach((i=>{o.set(i,s),computeEdges(i,o)})),o);const xp=class Transcluder_Transcluder{element;edges;constructor({element:s}){this.element=s}transclude(s,o){var i;if(s===this.element)return o;if(s===o)return this.element;this.edges=null!==(i=this.edges)&&void 0!==i?i:computeEdges(this.element);const a=this.edges.get(s);return bc(a)?void 0:(Nu(a)?((s,o,i)=>{const a=i.get(s);Nu(a)&&(a.content=a.map(((u,_,w)=>w===s?(i.delete(s),i.set(o,a),o):w)))})(s,o,this.edges):Mu(a)?((s,o,i)=>{const a=i.get(s);Mu(a)&&(a.content=a.map((u=>u===s?(i.delete(s),i.set(o,a),o):u)))})(s,o,this.edges):Ru(a)&&((s,o,i)=>{const a=i.get(s);Ru(a)&&(a.key===s&&(a.key=o,i.delete(s),i.set(o,a)),a.value===s&&(a.value=o,i.delete(s),i.set(o,a)))})(s,o,this.edges),this.element)}},fromURIReference=s=>{const o=s.indexOf(\"#\");return(s=>{try{const o=s.startsWith(\"#\")?s.slice(1):s;return decodeURIComponent(o)}catch{return s}})(-1===o?\"#\":s.substring(o))},kp=function fnparser(){const s=Pp,o=jp,i=this,a=\"parser.js: Parser(): \";i.ast=void 0,i.stats=void 0,i.trace=void 0,i.callbacks=[];let u,_,w,x,C,j,L,B=0,$=0,U=0,V=0,z=0,Y=new function systemData(){this.state=s.ACTIVE,this.phraseLength=0,this.refresh=()=>{this.state=s.ACTIVE,this.phraseLength=0}};i.parse=(Z,ee,ie,ae)=>{const ce=`${a}parse(): `;B=0,$=0,U=0,V=0,z=0,u=void 0,_=void 0,w=void 0,x=void 0,Y.refresh(),C=void 0,j=void 0,L=void 0,x=o.stringToChars(ie),u=Z.rules,_=Z.udts;const le=ee.toLowerCase();let pe;for(const s in u)if(u.hasOwnProperty(s)&&le===u[s].lower){pe=u[s].index;break}if(void 0===pe)throw new Error(`${ce}start rule name '${startRule}' not recognized`);(()=>{const s=`${a}initializeCallbacks(): `;let o,w;for(C=[],j=[],o=0;o<u.length;o+=1)C[o]=void 0;for(o=0;o<_.length;o+=1)j[o]=void 0;const x=[];for(o=0;o<u.length;o+=1)x.push(u[o].lower);for(o=0;o<_.length;o+=1)x.push(_[o].lower);for(const a in i.callbacks)if(i.callbacks.hasOwnProperty(a)){if(o=x.indexOf(a.toLowerCase()),o<0)throw new Error(`${s}syntax callback '${a}' not a rule or udt name`);if(w=i.callbacks[a]?i.callbacks[a]:void 0,\"function\"!=typeof w&&void 0!==w)throw new Error(`${s}syntax callback[${a}] must be function reference or falsy)`);o<u.length?C[o]=w:j[o-u.length]=w}})(),i.trace&&i.trace.init(u,_,x),i.stats&&i.stats.init(u,_),i.ast&&i.ast.init(u,_,x),L=ae,w=[{type:s.RNM,index:pe}],opExecute(0,0),w=void 0;let de=!1;switch(Y.state){case s.ACTIVE:throw new Error(`${ce}final state should never be 'ACTIVE'`);case s.NOMATCH:de=!1;break;case s.EMPTY:case s.MATCH:de=Y.phraseLength===x.length;break;default:throw new Error(\"unrecognized state\")}return{success:de,state:Y.state,stateName:s.idName(Y.state),length:x.length,matched:Y.phraseLength,maxMatched:z,maxTreeDepth:U,nodeHits:V}};const validateRnmCallbackResult=(o,i,u,_)=>{if(i.phraseLength>u){let s=`${a}opRNM(${o.name}): callback function error: `;throw s+=`sysData.phraseLength: ${i.phraseLength}`,s+=` must be <= remaining chars: ${u}`,new Error(s)}switch(i.state){case s.ACTIVE:if(!_)throw new Error(`${a}opRNM(${o.name}): callback function return error. ACTIVE state not allowed.`);break;case s.EMPTY:i.phraseLength=0;break;case s.MATCH:0===i.phraseLength&&(i.state=s.EMPTY);break;case s.NOMATCH:i.phraseLength=0;break;default:throw new Error(`${a}opRNM(${o.name}): callback function return error. Unrecognized return state: ${i.state}`)}},opUDT=(o,C)=>{let $,U,V;const z=w[o],Z=_[z.index];Y.UdtIndex=Z.index,B||(V=i.ast&&i.ast.udtDefined(z.index),V&&(U=u.length+z.index,$=i.ast.getLength(),i.ast.down(U,Z.name)));const ee=x.length-C;j[z.index](Y,x,C,L),((o,i,u)=>{if(i.phraseLength>u){let s=`${a}opUDT(${o.name}): callback function error: `;throw s+=`sysData.phraseLength: ${i.phraseLength}`,s+=` must be <= remaining chars: ${u}`,new Error(s)}switch(i.state){case s.ACTIVE:throw new Error(`${a}opUDT(${o.name}) ACTIVE state return not allowed.`);case s.EMPTY:if(!o.empty)throw new Error(`${a}opUDT(${o.name}) may not return EMPTY.`);i.phraseLength=0;break;case s.MATCH:if(0===i.phraseLength){if(!o.empty)throw new Error(`${a}opUDT(${o.name}) may not return EMPTY.`);i.state=s.EMPTY}break;case s.NOMATCH:i.phraseLength=0;break;default:throw new Error(`${a}opUDT(${o.name}): callback function return error. Unrecognized return state: ${i.state}`)}})(Z,Y,ee),B||V&&(Y.state===s.NOMATCH?i.ast.setLength($):i.ast.up(U,Z.name,C,Y.phraseLength))},opExecute=(o,_)=>{const j=`${a}opExecute(): `,Z=w[o];switch(V+=1,$>U&&(U=$),$+=1,Y.refresh(),i.trace&&i.trace.down(Z,_),Z.type){case s.ALT:((o,i)=>{const a=w[o];for(let o=0;o<a.children.length&&(opExecute(a.children[o],i),Y.state===s.NOMATCH);o+=1);})(o,_);break;case s.CAT:((o,a)=>{let u,_,x,C;const j=w[o];i.ast&&(_=i.ast.getLength()),u=!0,x=a,C=0;for(let o=0;o<j.children.length;o+=1){if(opExecute(j.children[o],x),Y.state===s.NOMATCH){u=!1;break}x+=Y.phraseLength,C+=Y.phraseLength}u?(Y.state=0===C?s.EMPTY:s.MATCH,Y.phraseLength=C):(Y.state=s.NOMATCH,Y.phraseLength=0,i.ast&&i.ast.setLength(_))})(o,_);break;case s.REP:((o,a)=>{let u,_,C,j;const L=w[o];if(0===L.max)return Y.state=s.EMPTY,void(Y.phraseLength=0);for(_=a,C=0,j=0,i.ast&&(u=i.ast.getLength());!(_>=x.length)&&(opExecute(o+1,_),Y.state!==s.NOMATCH)&&Y.state!==s.EMPTY&&(j+=1,C+=Y.phraseLength,_+=Y.phraseLength,j!==L.max););Y.state===s.EMPTY||j>=L.min?(Y.state=0===C?s.EMPTY:s.MATCH,Y.phraseLength=C):(Y.state=s.NOMATCH,Y.phraseLength=0,i.ast&&i.ast.setLength(u))})(o,_);break;case s.RNM:((o,a)=>{let _,j,$;const U=w[o],V=u[U.index],z=C[V.index];if(B||(j=i.ast&&i.ast.ruleDefined(U.index),j&&(_=i.ast.getLength(),i.ast.down(U.index,u[U.index].name))),z){const o=x.length-a;z(Y,x,a,L),validateRnmCallbackResult(V,Y,o,!0),Y.state===s.ACTIVE&&($=w,w=V.opcodes,opExecute(0,a),w=$,z(Y,x,a,L),validateRnmCallbackResult(V,Y,o,!1))}else $=w,w=V.opcodes,opExecute(0,a,Y),w=$;B||j&&(Y.state===s.NOMATCH?i.ast.setLength(_):i.ast.up(U.index,V.name,a,Y.phraseLength))})(o,_);break;case s.TRG:((o,i)=>{const a=w[o];Y.state=s.NOMATCH,i<x.length&&a.min<=x[i]&&x[i]<=a.max&&(Y.state=s.MATCH,Y.phraseLength=1)})(o,_);break;case s.TBS:((o,i)=>{const a=w[o],u=a.string.length;if(Y.state=s.NOMATCH,i+u<=x.length){for(let s=0;s<u;s+=1)if(x[i+s]!==a.string[s])return;Y.state=s.MATCH,Y.phraseLength=u}})(o,_);break;case s.TLS:((o,i)=>{let a;const u=w[o];Y.state=s.NOMATCH;const _=u.string.length;if(0!==_){if(i+_<=x.length){for(let s=0;s<_;s+=1)if(a=x[i+s],a>=65&&a<=90&&(a+=32),a!==u.string[s])return;Y.state=s.MATCH,Y.phraseLength=_}}else Y.state=s.EMPTY})(o,_);break;case s.UDT:opUDT(o,_);break;case s.AND:((o,i)=>{switch(B+=1,opExecute(o+1,i),B-=1,Y.phraseLength=0,Y.state){case s.EMPTY:case s.MATCH:Y.state=s.EMPTY;break;case s.NOMATCH:Y.state=s.NOMATCH;break;default:throw new Error(`opAND: invalid state ${Y.state}`)}})(o,_);break;case s.NOT:((o,i)=>{switch(B+=1,opExecute(o+1,i),B-=1,Y.phraseLength=0,Y.state){case s.EMPTY:case s.MATCH:Y.state=s.NOMATCH;break;case s.NOMATCH:Y.state=s.EMPTY;break;default:throw new Error(`opNOT: invalid state ${Y.state}`)}})(o,_);break;default:throw new Error(`${j}unrecognized operator`)}B||_+Y.phraseLength>z&&(z=_+Y.phraseLength),i.stats&&i.stats.collect(Z,Y),i.trace&&i.trace.up(Z,Y.state,_,Y.phraseLength),$-=1}},Op=function fnast(){const s=Pp,o=jp,i=this;let a,u,_,w=0;const x=[],C=[],j=[];function indent(s){let o=\"\";for(;s-- >0;)o+=\" \";return o}i.callbacks=[],i.init=(s,o,L)=>{let B;C.length=0,j.length=0,w=0,a=s,u=o,_=L;const $=[];for(B=0;B<a.length;B+=1)$.push(a[B].lower);for(B=0;B<u.length;B+=1)$.push(u[B].lower);for(w=a.length+u.length,B=0;B<w;B+=1)x[B]=void 0;for(const s in i.callbacks)if(i.callbacks.hasOwnProperty(s)){const o=s.toLowerCase();if(B=$.indexOf(o),B<0)throw new Error(`parser.js: Ast()): init: node '${s}' not a rule or udt name`);x[B]=i.callbacks[s]}},i.ruleDefined=s=>!!x[s],i.udtDefined=s=>!!x[a.length+s],i.down=(o,i)=>{const a=j.length;return C.push(a),j.push({name:i,thisIndex:a,thatIndex:void 0,state:s.SEM_PRE,callbackIndex:o,phraseIndex:void 0,phraseLength:void 0,stack:C.length}),a},i.up=(o,i,a,u)=>{const _=j.length,w=C.pop();return j.push({name:i,thisIndex:_,thatIndex:w,state:s.SEM_POST,callbackIndex:o,phraseIndex:a,phraseLength:u,stack:C.length}),j[w].thatIndex=_,j[w].phraseIndex=a,j[w].phraseLength=u,_},i.translate=o=>{let i,a;for(let u=0;u<j.length;u+=1)a=j[u],i=x[a.callbackIndex],i&&(a.state===s.SEM_PRE?i(s.SEM_PRE,_,a.phraseIndex,a.phraseLength,o):i&&i(s.SEM_POST,_,a.phraseIndex,a.phraseLength,o))},i.setLength=s=>{j.length=s,C.length=s>0?j[s-1].stack:0},i.getLength=()=>j.length,i.toXml=()=>{let i=\"\",a=0;return i+='<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n',i+=`<root nodes=\"${j.length/2}\" characters=\"${_.length}\">\\n`,i+=\"\\x3c!-- input string --\\x3e\\n\",i+=indent(a+2),i+=o.charsToString(_),i+=\"\\n\",j.forEach((u=>{u.state===s.SEM_PRE?(a+=1,i+=indent(a),i+=`<node name=\"${u.name}\" index=\"${u.phraseIndex}\" length=\"${u.phraseLength}\">\\n`,i+=indent(a+2),i+=o.charsToString(_,u.phraseIndex,u.phraseLength),i+=\"\\n\"):(i+=indent(a),i+=`</node>\\x3c!-- name=\"${u.name}\" --\\x3e\\n`,a-=1)})),i+=\"</root>\\n\",i}},Ap=function fntrace(){const s=Pp,o=jp,i=\"parser.js: Trace(): \";let a,u,_,w=\"\",x=0;const C=this,indent=s=>{let o=\"\",i=0;if(s>=0)for(;s--;)i+=1,5===i?(o+=\"|\",i=0):o+=\".\";return o};C.init=(s,o,i)=>{u=s,_=o,a=i};const opName=a=>{let w;switch(a.type){case s.ALT:w=\"ALT\";break;case s.CAT:w=\"CAT\";break;case s.REP:w=a.max===1/0?`REP(${a.min},inf)`:`REP(${a.min},${a.max})`;break;case s.RNM:w=`RNM(${u[a.index].name})`;break;case s.TRG:w=`TRG(${a.min},${a.max})`;break;case s.TBS:w=a.string.length>6?`TBS(${o.charsToString(a.string,0,3)}...)`:`TBS(${o.charsToString(a.string,0,6)})`;break;case s.TLS:w=a.string.length>6?`TLS(${o.charsToString(a.string,0,3)}...)`:`TLS(${o.charsToString(a.string,0,6)})`;break;case s.UDT:w=`UDT(${_[a.index].name})`;break;case s.AND:w=\"AND\";break;case s.NOT:w=\"NOT\";break;default:throw new Error(`${i}Trace: opName: unrecognized opcode`)}return w};C.down=(s,i)=>{const u=indent(x),_=Math.min(100,a.length-i);let C=o.charsToString(a,i,_);_<a.length-i&&(C+=\"...\"),C=`${u}|-|[${opName(s)}]${C}\\n`,w+=C,x+=1},C.up=(u,_,C,j)=>{const L=`${i}trace.up: `;x-=1;const B=indent(x);let $,U,V;switch(_){case s.EMPTY:V=\"|E|\",U=\"''\";break;case s.MATCH:V=\"|M|\",$=Math.min(100,j),U=$<j?`'${o.charsToString(a,C,$)}...'`:`'${o.charsToString(a,C,$)}'`;break;case s.NOMATCH:V=\"|N|\",U=\"\";break;default:throw new Error(`${L} unrecognized state`)}U=`${B}${V}[${opName(u)}]${U}\\n`,w+=U},C.displayTrace=()=>w},Cp=function fnstats(){const s=Pp;let o,i,a;const u=[],_=[],w=[];this.init=(s,a)=>{o=s,i=a,clear()},this.collect=(o,i)=>{incStat(a,i.state,i.phraseLength),incStat(u[o.type],i.state,i.phraseLength),o.type===s.RNM&&incStat(_[o.index],i.state,i.phraseLength),o.type===s.UDT&&incStat(w[o.index],i.state,i.phraseLength)},this.displayStats=()=>{let o=\"\";const i={match:0,empty:0,nomatch:0,total:0},displayRow=(s,o,a,u,_)=>{i.match+=o,i.empty+=a,i.nomatch+=u,i.total+=_;return`${s} | ${normalize(o)} | ${normalize(a)} | ${normalize(u)} | ${normalize(_)} |\\n`};return o+=\"          OPERATOR STATS\\n\",o+=\"      |   MATCH |   EMPTY | NOMATCH |   TOTAL |\\n\",o+=displayRow(\"  ALT\",u[s.ALT].match,u[s.ALT].empty,u[s.ALT].nomatch,u[s.ALT].total),o+=displayRow(\"  CAT\",u[s.CAT].match,u[s.CAT].empty,u[s.CAT].nomatch,u[s.CAT].total),o+=displayRow(\"  REP\",u[s.REP].match,u[s.REP].empty,u[s.REP].nomatch,u[s.REP].total),o+=displayRow(\"  RNM\",u[s.RNM].match,u[s.RNM].empty,u[s.RNM].nomatch,u[s.RNM].total),o+=displayRow(\"  TRG\",u[s.TRG].match,u[s.TRG].empty,u[s.TRG].nomatch,u[s.TRG].total),o+=displayRow(\"  TBS\",u[s.TBS].match,u[s.TBS].empty,u[s.TBS].nomatch,u[s.TBS].total),o+=displayRow(\"  TLS\",u[s.TLS].match,u[s.TLS].empty,u[s.TLS].nomatch,u[s.TLS].total),o+=displayRow(\"  UDT\",u[s.UDT].match,u[s.UDT].empty,u[s.UDT].nomatch,u[s.UDT].total),o+=displayRow(\"  AND\",u[s.AND].match,u[s.AND].empty,u[s.AND].nomatch,u[s.AND].total),o+=displayRow(\"  NOT\",u[s.NOT].match,u[s.NOT].empty,u[s.NOT].nomatch,u[s.NOT].total),o+=displayRow(\"TOTAL\",i.match,i.empty,i.nomatch,i.total),o},this.displayHits=s=>{let o=\"\";const displayRow=(s,o,i,u,_)=>{a.match+=s,a.empty+=o,a.nomatch+=i,a.total+=u;return`| ${normalize(s)} | ${normalize(o)} | ${normalize(i)} | ${normalize(u)} | ${_}\\n`};\"string\"==typeof s&&\"a\"===s.toLowerCase()[0]?(_.sort(sortAlpha),w.sort(sortAlpha),o+=\"    RULES/UDTS ALPHABETICALLY\\n\"):\"string\"==typeof s&&\"i\"===s.toLowerCase()[0]?(_.sort(sortIndex),w.sort(sortIndex),o+=\"    RULES/UDTS BY INDEX\\n\"):(_.sort(sortHits),w.sort(sortHits),o+=\"    RULES/UDTS BY HIT COUNT\\n\"),o+=\"|   MATCH |   EMPTY | NOMATCH |   TOTAL | NAME\\n\";for(let s=0;s<_.length;s+=1){let i=_[s];i.total&&(o+=displayRow(i.match,i.empty,i.nomatch,i.total,i.name))}for(let s=0;s<w.length;s+=1){let i=w[s];i.total&&(o+=displayRow(i.match,i.empty,i.nomatch,i.total,i.name))}return o};const normalize=s=>s<10?`      ${s}`:s<100?`     ${s}`:s<1e3?`    ${s}`:s<1e4?`   ${s}`:s<1e5?`  ${s}`:s<1e6?` ${s}`:`${s}`,sortAlpha=(s,o)=>s.lower<o.lower?-1:s.lower>o.lower?1:0,sortHits=(s,o)=>s.total<o.total?1:s.total>o.total?-1:sortAlpha(s,o),sortIndex=(s,o)=>s.index<o.index?-1:s.index>o.index?1:0,x=function fnempty(){this.empty=0,this.match=0,this.nomatch=0,this.total=0},clear=()=>{u.length=0,a=new x,u[s.ALT]=new x,u[s.CAT]=new x,u[s.REP]=new x,u[s.RNM]=new x,u[s.TRG]=new x,u[s.TBS]=new x,u[s.TLS]=new x,u[s.UDT]=new x,u[s.AND]=new x,u[s.NOT]=new x,_.length=0;for(let s=0;s<o.length;s+=1)_.push({empty:0,match:0,nomatch:0,total:0,name:o[s].name,lower:o[s].lower,index:o[s].index});if(i.length>0){w.length=0;for(let s=0;s<i.length;s+=1)w.push({empty:0,match:0,nomatch:0,total:0,name:i[s].name,lower:i[s].lower,index:i[s].index})}},incStat=(o,i)=>{switch(o.total+=1,i){case s.EMPTY:o.empty+=1;break;case s.MATCH:o.match+=1;break;case s.NOMATCH:o.nomatch+=1;break;default:throw new Error(`parser.js: Stats(): collect(): incStat(): unrecognized state: ${i}`)}}},jp={stringToChars:s=>[...s].map((s=>s.codePointAt(0))),charsToString:(s,o,i)=>{let a=s;for(;!(void 0===o||o<0);){if(void 0===i){a=s.slice(o);break}if(i<=0)return\"\";a=s.slice(o,o+i);break}return String.fromCodePoint(...a)}},Pp={ALT:1,CAT:2,REP:3,RNM:4,TRG:5,TBS:6,TLS:7,UDT:11,AND:12,NOT:13,ACTIVE:100,MATCH:101,EMPTY:102,NOMATCH:103,SEM_PRE:200,SEM_POST:201,SEM_OK:300,idName:s=>{switch(s){case Pp.ALT:return\"ALT\";case Pp.CAT:return\"CAT\";case Pp.REP:return\"REP\";case Pp.RNM:return\"RNM\";case Pp.TRG:return\"TRG\";case Pp.TBS:return\"TBS\";case Pp.TLS:return\"TLS\";case Pp.UDT:return\"UDT\";case Pp.AND:return\"AND\";case Pp.NOT:return\"NOT\";case Pp.ACTIVE:return\"ACTIVE\";case Pp.EMPTY:return\"EMPTY\";case Pp.MATCH:return\"MATCH\";case Pp.NOMATCH:return\"NOMATCH\";case Pp.SEM_PRE:return\"SEM_PRE\";case Pp.SEM_POST:return\"SEM_POST\";case Pp.SEM_OK:return\"SEM_OK\";default:return\"UNRECOGNIZED STATE\"}}};function grammar(){this.grammarObject=\"grammarObject\",this.rules=[],this.rules[0]={name:\"json-pointer\",lower:\"json-pointer\",index:0,isBkr:!1},this.rules[1]={name:\"reference-token\",lower:\"reference-token\",index:1,isBkr:!1},this.rules[2]={name:\"unescaped\",lower:\"unescaped\",index:2,isBkr:!1},this.rules[3]={name:\"escaped\",lower:\"escaped\",index:3,isBkr:!1},this.rules[4]={name:\"array-location\",lower:\"array-location\",index:4,isBkr:!1},this.rules[5]={name:\"array-index\",lower:\"array-index\",index:5,isBkr:!1},this.rules[6]={name:\"array-dash\",lower:\"array-dash\",index:6,isBkr:!1},this.rules[7]={name:\"slash\",lower:\"slash\",index:7,isBkr:!1},this.udts=[],this.rules[0].opcodes=[],this.rules[0].opcodes[0]={type:3,min:0,max:1/0},this.rules[0].opcodes[1]={type:2,children:[2,3]},this.rules[0].opcodes[2]={type:4,index:7},this.rules[0].opcodes[3]={type:4,index:1},this.rules[1].opcodes=[],this.rules[1].opcodes[0]={type:3,min:0,max:1/0},this.rules[1].opcodes[1]={type:1,children:[2,3]},this.rules[1].opcodes[2]={type:4,index:2},this.rules[1].opcodes[3]={type:4,index:3},this.rules[2].opcodes=[],this.rules[2].opcodes[0]={type:1,children:[1,2,3]},this.rules[2].opcodes[1]={type:5,min:0,max:46},this.rules[2].opcodes[2]={type:5,min:48,max:125},this.rules[2].opcodes[3]={type:5,min:127,max:1114111},this.rules[3].opcodes=[],this.rules[3].opcodes[0]={type:2,children:[1,2]},this.rules[3].opcodes[1]={type:7,string:[126]},this.rules[3].opcodes[2]={type:1,children:[3,4]},this.rules[3].opcodes[3]={type:7,string:[48]},this.rules[3].opcodes[4]={type:7,string:[49]},this.rules[4].opcodes=[],this.rules[4].opcodes[0]={type:1,children:[1,2]},this.rules[4].opcodes[1]={type:4,index:5},this.rules[4].opcodes[2]={type:4,index:6},this.rules[5].opcodes=[],this.rules[5].opcodes[0]={type:1,children:[1,2]},this.rules[5].opcodes[1]={type:6,string:[48]},this.rules[5].opcodes[2]={type:2,children:[3,4]},this.rules[5].opcodes[3]={type:5,min:49,max:57},this.rules[5].opcodes[4]={type:3,min:0,max:1/0},this.rules[5].opcodes[5]={type:5,min:48,max:57},this.rules[6].opcodes=[],this.rules[6].opcodes[0]={type:7,string:[45]},this.rules[7].opcodes=[],this.rules[7].opcodes[0]={type:7,string:[47]},this.toString=function toString(){let s=\"\";return s+=\"; JavaScript Object Notation (JSON) Pointer ABNF syntax\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc6901\\n\",s+=\"json-pointer    = *( slash reference-token ) ; MODIFICATION: surrogate text rule used\\n\",s+=\"reference-token = *( unescaped / escaped )\\n\",s+=\"unescaped       = %x00-2E / %x30-7D / %x7F-10FFFF\\n\",s+=\"                ; %x2F ('/') and %x7E ('~') are excluded from 'unescaped'\\n\",s+='escaped         = \"~\" ( \"0\" / \"1\" )\\n',s+=\"                ; representing '~' and '/', respectively\\n\",s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc6901#section-4\\n\",s+=\"array-location  = array-index / array-dash\\n\",s+=\"array-index     = %x30 / ( %x31-39 *(%x30-39) )\\n\",s+='                ; \"0\", or digits without a leading \"0\"\\n',s+='array-dash      = \"-\"\\n',s+=\"\\n\",s+=\"; Surrogate named rules\\n\",s+='slash           = \"/\"\\n','; JavaScript Object Notation (JSON) Pointer ABNF syntax\\n; https://datatracker.ietf.org/doc/html/rfc6901\\njson-pointer    = *( slash reference-token ) ; MODIFICATION: surrogate text rule used\\nreference-token = *( unescaped / escaped )\\nunescaped       = %x00-2E / %x30-7D / %x7F-10FFFF\\n                ; %x2F (\\'/\\') and %x7E (\\'~\\') are excluded from \\'unescaped\\'\\nescaped         = \"~\" ( \"0\" / \"1\" )\\n                ; representing \\'~\\' and \\'/\\', respectively\\n\\n; https://datatracker.ietf.org/doc/html/rfc6901#section-4\\narray-location  = array-index / array-dash\\narray-index     = %x30 / ( %x31-39 *(%x30-39) )\\n                ; \"0\", or digits without a leading \"0\"\\narray-dash      = \"-\"\\n\\n; Surrogate named rules\\nslash           = \"/\"\\n'}}class JSONPointerError extends Error{constructor(s,o=void 0){if(super(s,o),this.name=this.constructor.name,\"string\"==typeof s&&(this.message=s),\"function\"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(s).stack,null!=o&&\"object\"==typeof o&&Object.prototype.hasOwnProperty.call(o,\"cause\")&&!(\"cause\"in this)){const{cause:s}=o;this.cause=s,s instanceof Error&&\"stack\"in s&&(this.stack=`${this.stack}\\nCAUSE: ${s.stack}`)}if(null!=o&&\"object\"==typeof o){const{cause:s,...i}=o;Object.assign(this,i)}}}const Ip=JSONPointerError;const Tp=class JSONPointerParseError extends Ip{},callbacks_cst=s=>(o,i,a,u,_)=>{if(\"object\"!=typeof _||null===_||Array.isArray(_))throw new Tp(\"parser's user data must be an object\");if(o===Pp.SEM_PRE){const o={type:s,text:jp.charsToString(i,a,u),start:a,length:u,children:[]};if(_.stack.length>0){_.stack[_.stack.length-1].children.push(o)}else _.root=o;_.stack.push(o)}o===Pp.SEM_POST&&_.stack.pop()};const Np=class CSTTranslator_CSTTranslator extends Op{constructor(){super(),this.callbacks[\"json-pointer\"]=callbacks_cst(\"json-pointer\"),this.callbacks[\"reference-token\"]=callbacks_cst(\"reference-token\"),this.callbacks.slash=callbacks_cst(\"text\")}getTree(){const s={stack:[],root:null};return this.translate(s),delete s.stack,s}},es_unescape=s=>{if(\"string\"!=typeof s)throw new TypeError(\"Reference token must be a string\");return s.replace(/~1/g,\"/\").replace(/~0/g,\"~\")};const Mp=class ASTTranslator extends Np{getTree(){const{root:s}=super.getTree();return s.children.filter((({type:s})=>\"reference-token\"===s)).map((({text:s})=>es_unescape(s)))}};const Rp=class Expectations extends Array{toString(){return this.map((s=>`\"${String(s)}\"`)).join(\", \")}};const Dp=class Trace extends Ap{inferExpectations(){const s=this.displayTrace().split(\"\\n\"),o=new Set;let i=-1;for(let a=0;a<s.length;a++){const u=s[a];if(u.includes(\"M|\")){const s=u.match(/]'(.*)'$/);s&&s[1]&&(i=a)}if(a>i){const s=u.match(/N\\|\\[TLS\\(([^)]+)\\)]/);s&&o.add(s[1])}}return new Rp(...o)}},Lp=new grammar,es_parse=(s,{translator:o=new Mp,stats:i=!1,trace:a=!1}={})=>{if(\"string\"!=typeof s)throw new TypeError(\"JSON Pointer must be a string\");try{const u=new kp;o&&(u.ast=o),i&&(u.stats=new Cp),a&&(u.trace=new Dp);const _=u.parse(Lp,\"json-pointer\",s);return{result:_,tree:_.success&&o?u.ast.getTree():void 0,stats:u.stats,trace:u.trace}}catch(o){throw new Tp(\"Unexpected error during JSON Pointer parsing\",{cause:o,jsonPointer:s})}};new grammar,new kp,new grammar,new kp;const Fp=new grammar,Bp=new kp,array_index=s=>{if(\"string\"!=typeof s)return!1;try{return Bp.parse(Fp,\"array-index\",s).success}catch{return!1}},$p=new grammar,qp=new kp,array_dash=s=>{if(\"string\"!=typeof s)return!1;try{return qp.parse($p,\"array-dash\",s).success}catch{return!1}},es_escape=s=>{if(\"string\"!=typeof s&&\"number\"!=typeof s)throw new TypeError(\"Reference token must be a string or number\");return String(s).replace(/~/g,\"~0\").replace(/\\//g,\"~1\")};const Up=class JSONPointerCompileError extends Ip{},es_compile=s=>{if(!Array.isArray(s))throw new TypeError(\"Reference tokens must be a list of strings or numbers\");try{return 0===s.length?\"\":`/${s.map((s=>{if(\"string\"!=typeof s&&\"number\"!=typeof s)throw new TypeError(\"Reference token must be a string or number\");return es_escape(String(s))})).join(\"/\")}`}catch(o){throw new Up(\"Unexpected error during JSON Pointer compilation\",{cause:o,referenceTokens:s})}};const Vp=class TraceBuilder{#e;#t;#r;constructor(s,o={}){this.#e=s,this.#e.steps=[],this.#e.failed=!1,this.#e.failedAt=-1,this.#e.message=`JSON Pointer \"${o.jsonPointer}\" was successfully evaluated against the provided value`,this.#e.context={...o,realm:o.realm.name},this.#t=[],this.#r=o.realm}step({referenceToken:s,input:o,output:i,success:a=!0,reason:u}){const _=this.#t.length;this.#t.push(s);const w={referenceToken:s,referenceTokenPosition:_,input:o,inputType:this.#r.isObject(o)?\"object\":this.#r.isArray(o)?\"array\":\"unrecognized\",output:i,success:a};u&&(w.reason=u),this.#e.steps.push(w),a||(this.#e.failed=!0,this.#e.failedAt=_,this.#e.message=u)}};const zp=class EvaluationRealm{name=\"\";isArray(s){throw new Ip(\"Realm.isArray(node) must be implemented in a subclass\")}isObject(s){throw new Ip(\"Realm.isObject(node) must be implemented in a subclass\")}sizeOf(s){throw new Ip(\"Realm.sizeOf(node) must be implemented in a subclass\")}has(s,o){throw new Ip(\"Realm.has(node) must be implemented in a subclass\")}evaluate(s,o){throw new Ip(\"Realm.evaluate(node) must be implemented in a subclass\")}};const Wp=class JSONPointerEvaluateError extends Ip{};const Jp=class JSONPointerIndexError extends Wp{};const Hp=class JSONEvaluationRealm extends zp{name=\"json\";isArray(s){return Array.isArray(s)}isObject(s){return\"object\"==typeof s&&null!==s&&!this.isArray(s)}sizeOf(s){return this.isArray(s)?s.length:this.isObject(s)?Object.keys(s).length:0}has(s,o){if(this.isArray(s)){const i=Number(o),a=i>>>0;if(i!==a)throw new Jp(`Invalid array index \"${o}\": index must be an unsinged 32-bit integer`,{referenceToken:o,currentValue:s,realm:this.name});return a<this.sizeOf(s)&&Object.prototype.hasOwnProperty.call(s,i)}return!!this.isObject(s)&&Object.prototype.hasOwnProperty.call(s,o)}evaluate(s,o){return this.isArray(s)?s[Number(o)]:s[o]}};const Kp=class JSONPointerTypeError extends Wp{};const Gp=class JSONPointerKeyError extends Wp{},es_evaluate=(s,o,{strictArrays:i=!0,strictObjects:a=!0,realm:u=new Hp,trace:_=!0}={})=>{const{result:w,tree:x,trace:C}=es_parse(o,{trace:!!_}),j=\"object\"==typeof _&&null!==_?new Vp(_,{jsonPointer:o,referenceTokens:x,strictArrays:i,strictObjects:a,realm:u,value:s}):null;try{let _;if(!w.success){let i=`Invalid JSON Pointer: \"${o}\". Syntax error at position ${w.maxMatched}`;throw i+=C?`, expected ${C.inferExpectations()}`:\"\",new Wp(i,{jsonPointer:o,currentValue:s,realm:u.name})}return x.reduce(((s,w,C)=>{if(u.isArray(s)){if(array_dash(w)){if(i)throw new Jp(`Invalid array index \"-\" at position ${C} in \"${o}\". The \"-\" token always refers to a nonexistent element during evaluation`,{jsonPointer:o,referenceTokens:x,referenceToken:w,referenceTokenPosition:C,currentValue:s,realm:u.name});return _=u.evaluate(s,String(u.sizeOf(s))),null==j||j.step({referenceToken:w,input:s,output:_}),_}if(!array_index(w))throw new Jp(`Invalid array index \"${w}\" at position ${C} in \"${o}\": index MUST be \"0\", or digits without a leading \"0\"`,{jsonPointer:o,referenceTokens:x,referenceToken:w,referenceTokenPosition:C,currentValue:s,realm:u.name});const a=Number(w);if(!Number.isSafeInteger(a))throw new Jp(`Invalid array index \"${w}\" at position ${C} in \"${o}\": index must be a safe integer`,{jsonPointer:o,referenceTokens:x,referenceToken:w,referenceTokenPosition:C,currentValue:s,realm:u.name});if(!u.has(s,w)&&i)throw new Jp(`Invalid array index \"${w}\" at position ${C} in \"${o}\": index not found in array`,{jsonPointer:o,referenceTokens:x,referenceToken:w,referenceTokenPosition:C,currentValue:s,realm:u.name});return _=u.evaluate(s,w),null==j||j.step({referenceToken:w,input:s,output:_}),_}if(u.isObject(s)){if(!u.has(s,w)&&a)throw new Gp(`Invalid object key \"${w}\" at position ${C} in \"${o}\": key not found in object`,{jsonPointer:o,referenceTokens:x,referenceToken:w,referenceTokenPosition:C,currentValue:s,realm:u.name});return _=u.evaluate(s,w),null==j||j.step({referenceToken:w,input:s,output:_}),_}throw new Kp(`Invalid reference token \"${w}\" at position ${C} in \"${o}\": cannot be applied to a non-object/non-array value`,{jsonPointer:o,referenceTokens:x,referenceToken:w,referenceTokenPosition:C,currentValue:s,realm:u.name})}),s)}catch(s){if(null==j||j.step({referenceToken:s.referenceToken,input:s.currentValue,success:!1,reason:s.message}),s instanceof Wp)throw s;throw new Wp(\"Unexpected error during JSON Pointer evaluation\",{cause:s,jsonPointer:o,referenceTokens:x})}};const Yp=class ApiDOMEvaluationRealm extends zp{name=\"apidom\";isArray(s){return Mu(s)}isObject(s){return Nu(s)}sizeOf(s){return this.isArray(s)||this.isObject(s)?s.length:0}has(s,o){if(this.isArray(s)){const i=Number(o),a=i>>>0;if(i!==a)throw new Jp(`Invalid array index \"${o}\": index must be an unsinged 32-bit integer`,{referenceToken:o,currentValue:s,realm:this.name});return a<this.sizeOf(s)}if(this.isObject(s)){const i=s.keys(),a=new Set(i);if(i.length!==a.size)throw new Gp(`Object key \"${o}\" is not unique — JSON Pointer requires unique member names`,{referenceToken:o,currentValue:s,realm:this.name});return s.hasKey(o)}return!1}evaluate(s,o){return this.isArray(s)?s.get(Number(o)):s.get(o)}},apidom_evaluate=(s,o,i={})=>es_evaluate(s,o,{...i,realm:new Yp});class Callback extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"callback\"}}const Xp=Callback;class Components extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"components\"}get schemas(){return this.get(\"schemas\")}set schemas(s){this.set(\"schemas\",s)}get responses(){return this.get(\"responses\")}set responses(s){this.set(\"responses\",s)}get parameters(){return this.get(\"parameters\")}set parameters(s){this.set(\"parameters\",s)}get examples(){return this.get(\"examples\")}set examples(s){this.set(\"examples\",s)}get requestBodies(){return this.get(\"requestBodies\")}set requestBodies(s){this.set(\"requestBodies\",s)}get headers(){return this.get(\"headers\")}set headers(s){this.set(\"headers\",s)}get securitySchemes(){return this.get(\"securitySchemes\")}set securitySchemes(s){this.set(\"securitySchemes\",s)}get links(){return this.get(\"links\")}set links(s){this.set(\"links\",s)}get callbacks(){return this.get(\"callbacks\")}set callbacks(s){this.set(\"callbacks\",s)}}const Qp=Components;class Contact extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"contact\"}get name(){return this.get(\"name\")}set name(s){this.set(\"name\",s)}get url(){return this.get(\"url\")}set url(s){this.set(\"url\",s)}get email(){return this.get(\"email\")}set email(s){this.set(\"email\",s)}}const Zp=Contact;class Discriminator extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"discriminator\"}get propertyName(){return this.get(\"propertyName\")}set propertyName(s){this.set(\"propertyName\",s)}get mapping(){return this.get(\"mapping\")}set mapping(s){this.set(\"mapping\",s)}}const th=Discriminator;class Encoding extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"encoding\"}get contentType(){return this.get(\"contentType\")}set contentType(s){this.set(\"contentType\",s)}get headers(){return this.get(\"headers\")}set headers(s){this.set(\"headers\",s)}get style(){return this.get(\"style\")}set style(s){this.set(\"style\",s)}get explode(){return this.get(\"explode\")}set explode(s){this.set(\"explode\",s)}get allowedReserved(){return this.get(\"allowedReserved\")}set allowedReserved(s){this.set(\"allowedReserved\",s)}}const rh=Encoding;class Example extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"example\"}get summary(){return this.get(\"summary\")}set summary(s){this.set(\"summary\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get value(){return this.get(\"value\")}set value(s){this.set(\"value\",s)}get externalValue(){return this.get(\"externalValue\")}set externalValue(s){this.set(\"externalValue\",s)}}const uh=Example;class ExternalDocumentation extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"externalDocumentation\"}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get url(){return this.get(\"url\")}set url(s){this.set(\"url\",s)}}const dh=ExternalDocumentation;class Header extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"header\"}get required(){return this.hasKey(\"required\")?this.get(\"required\"):new Su.bd(!1)}set required(s){this.set(\"required\",s)}get deprecated(){return this.hasKey(\"deprecated\")?this.get(\"deprecated\"):new Su.bd(!1)}set deprecated(s){this.set(\"deprecated\",s)}get allowEmptyValue(){return this.get(\"allowEmptyValue\")}set allowEmptyValue(s){this.set(\"allowEmptyValue\",s)}get style(){return this.get(\"style\")}set style(s){this.set(\"style\",s)}get explode(){return this.get(\"explode\")}set explode(s){this.set(\"explode\",s)}get allowReserved(){return this.get(\"allowReserved\")}set allowReserved(s){this.set(\"allowReserved\",s)}get schema(){return this.get(\"schema\")}set schema(s){this.set(\"schema\",s)}get example(){return this.get(\"example\")}set example(s){this.set(\"example\",s)}get examples(){return this.get(\"examples\")}set examples(s){this.set(\"examples\",s)}get contentProp(){return this.get(\"content\")}set contentProp(s){this.set(\"content\",s)}}Object.defineProperty(Header.prototype,\"description\",{get(){return this.get(\"description\")},set(s){this.set(\"description\",s)},enumerable:!0});const fh=Header;class Info extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"info\",this.classes.push(\"info\")}get title(){return this.get(\"title\")}set title(s){this.set(\"title\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get termsOfService(){return this.get(\"termsOfService\")}set termsOfService(s){this.set(\"termsOfService\",s)}get contact(){return this.get(\"contact\")}set contact(s){this.set(\"contact\",s)}get license(){return this.get(\"license\")}set license(s){this.set(\"license\",s)}get version(){return this.get(\"version\")}set version(s){this.set(\"version\",s)}}const vh=Info;class License extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"license\"}get name(){return this.get(\"name\")}set name(s){this.set(\"name\",s)}get url(){return this.get(\"url\")}set url(s){this.set(\"url\",s)}}const _h=License;class Link extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"link\"}get operationRef(){return this.get(\"operationRef\")}set operationRef(s){this.set(\"operationRef\",s)}get operationId(){return this.get(\"operationId\")}set operationId(s){this.set(\"operationId\",s)}get operation(){var s,o;return ju(this.operationRef)?null===(s=this.operationRef)||void 0===s?void 0:s.meta.get(\"operation\"):ju(this.operationId)?null===(o=this.operationId)||void 0===o?void 0:o.meta.get(\"operation\"):void 0}set operation(s){this.set(\"operation\",s)}get parameters(){return this.get(\"parameters\")}set parameters(s){this.set(\"parameters\",s)}get requestBody(){return this.get(\"requestBody\")}set requestBody(s){this.set(\"requestBody\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get server(){return this.get(\"server\")}set server(s){this.set(\"server\",s)}}const wh=Link;class MediaType extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"mediaType\"}get schema(){return this.get(\"schema\")}set schema(s){this.set(\"schema\",s)}get example(){return this.get(\"example\")}set example(s){this.set(\"example\",s)}get examples(){return this.get(\"examples\")}set examples(s){this.set(\"examples\",s)}get encoding(){return this.get(\"encoding\")}set encoding(s){this.set(\"encoding\",s)}}const Oh=MediaType;class OAuthFlow extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"oAuthFlow\"}get authorizationUrl(){return this.get(\"authorizationUrl\")}set authorizationUrl(s){this.set(\"authorizationUrl\",s)}get tokenUrl(){return this.get(\"tokenUrl\")}set tokenUrl(s){this.set(\"tokenUrl\",s)}get refreshUrl(){return this.get(\"refreshUrl\")}set refreshUrl(s){this.set(\"refreshUrl\",s)}get scopes(){return this.get(\"scopes\")}set scopes(s){this.set(\"scopes\",s)}}const jh=OAuthFlow;class OAuthFlows extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"oAuthFlows\"}get implicit(){return this.get(\"implicit\")}set implicit(s){this.set(\"implicit\",s)}get password(){return this.get(\"password\")}set password(s){this.set(\"password\",s)}get clientCredentials(){return this.get(\"clientCredentials\")}set clientCredentials(s){this.set(\"clientCredentials\",s)}get authorizationCode(){return this.get(\"authorizationCode\")}set authorizationCode(s){this.set(\"authorizationCode\",s)}}const Ph=OAuthFlows;class Openapi extends Su.Om{constructor(s,o,i){super(s,o,i),this.element=\"openapi\",this.classes.push(\"spec-version\"),this.classes.push(\"version\")}}const Ih=Openapi;class OpenApi3_0 extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"openApi3_0\",this.classes.push(\"api\")}get openapi(){return this.get(\"openapi\")}set openapi(s){this.set(\"openapi\",s)}get info(){return this.get(\"info\")}set info(s){this.set(\"info\",s)}get servers(){return this.get(\"servers\")}set servers(s){this.set(\"servers\",s)}get paths(){return this.get(\"paths\")}set paths(s){this.set(\"paths\",s)}get components(){return this.get(\"components\")}set components(s){this.set(\"components\",s)}get security(){return this.get(\"security\")}set security(s){this.set(\"security\",s)}get tags(){return this.get(\"tags\")}set tags(s){this.set(\"tags\",s)}get externalDocs(){return this.get(\"externalDocs\")}set externalDocs(s){this.set(\"externalDocs\",s)}}const Rh=OpenApi3_0;class Operation extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"operation\"}get tags(){return this.get(\"tags\")}set tags(s){this.set(\"tags\",s)}get summary(){return this.get(\"summary\")}set summary(s){this.set(\"summary\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}set externalDocs(s){this.set(\"externalDocs\",s)}get externalDocs(){return this.get(\"externalDocs\")}get operationId(){return this.get(\"operationId\")}set operationId(s){this.set(\"operationId\",s)}get parameters(){return this.get(\"parameters\")}set parameters(s){this.set(\"parameters\",s)}get requestBody(){return this.get(\"requestBody\")}set requestBody(s){this.set(\"requestBody\",s)}get responses(){return this.get(\"responses\")}set responses(s){this.set(\"responses\",s)}get callbacks(){return this.get(\"callbacks\")}set callbacks(s){this.set(\"callbacks\",s)}get deprecated(){return this.hasKey(\"deprecated\")?this.get(\"deprecated\"):new Su.bd(!1)}set deprecated(s){this.set(\"deprecated\",s)}get security(){return this.get(\"security\")}set security(s){this.set(\"security\",s)}get servers(){return this.get(\"severs\")}set servers(s){this.set(\"servers\",s)}}const Dh=Operation;class Parameter extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"parameter\"}get name(){return this.get(\"name\")}set name(s){this.set(\"name\",s)}get in(){return this.get(\"in\")}set in(s){this.set(\"in\",s)}get required(){return this.hasKey(\"required\")?this.get(\"required\"):new Su.bd(!1)}set required(s){this.set(\"required\",s)}get deprecated(){return this.hasKey(\"deprecated\")?this.get(\"deprecated\"):new Su.bd(!1)}set deprecated(s){this.set(\"deprecated\",s)}get allowEmptyValue(){return this.get(\"allowEmptyValue\")}set allowEmptyValue(s){this.set(\"allowEmptyValue\",s)}get style(){return this.get(\"style\")}set style(s){this.set(\"style\",s)}get explode(){return this.get(\"explode\")}set explode(s){this.set(\"explode\",s)}get allowReserved(){return this.get(\"allowReserved\")}set allowReserved(s){this.set(\"allowReserved\",s)}get schema(){return this.get(\"schema\")}set schema(s){this.set(\"schema\",s)}get example(){return this.get(\"example\")}set example(s){this.set(\"example\",s)}get examples(){return this.get(\"examples\")}set examples(s){this.set(\"examples\",s)}get contentProp(){return this.get(\"content\")}set contentProp(s){this.set(\"content\",s)}}Object.defineProperty(Parameter.prototype,\"description\",{get(){return this.get(\"description\")},set(s){this.set(\"description\",s)},enumerable:!0});const Lh=Parameter;class PathItem extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"pathItem\"}get $ref(){return this.get(\"$ref\")}set $ref(s){this.set(\"$ref\",s)}get summary(){return this.get(\"summary\")}set summary(s){this.set(\"summary\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get GET(){return this.get(\"get\")}set GET(s){this.set(\"GET\",s)}get PUT(){return this.get(\"put\")}set PUT(s){this.set(\"PUT\",s)}get POST(){return this.get(\"post\")}set POST(s){this.set(\"POST\",s)}get DELETE(){return this.get(\"delete\")}set DELETE(s){this.set(\"DELETE\",s)}get OPTIONS(){return this.get(\"options\")}set OPTIONS(s){this.set(\"OPTIONS\",s)}get HEAD(){return this.get(\"head\")}set HEAD(s){this.set(\"HEAD\",s)}get PATCH(){return this.get(\"patch\")}set PATCH(s){this.set(\"PATCH\",s)}get TRACE(){return this.get(\"trace\")}set TRACE(s){this.set(\"TRACE\",s)}get servers(){return this.get(\"servers\")}set servers(s){this.set(\"servers\",s)}get parameters(){return this.get(\"parameters\")}set parameters(s){this.set(\"parameters\",s)}}const Fh=PathItem;class Paths extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"paths\"}}const Jh=Paths;class Reference extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"reference\",this.classes.push(\"openapi-reference\")}get $ref(){return this.get(\"$ref\")}set $ref(s){this.set(\"$ref\",s)}}const Hh=Reference;class RequestBody extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"requestBody\"}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get contentProp(){return this.get(\"content\")}set contentProp(s){this.set(\"content\",s)}get required(){return this.hasKey(\"required\")?this.get(\"required\"):new Su.bd(!1)}set required(s){this.set(\"required\",s)}}const Kh=RequestBody;class Response_Response extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"response\"}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get headers(){return this.get(\"headers\")}set headers(s){this.set(\"headers\",s)}get contentProp(){return this.get(\"content\")}set contentProp(s){this.set(\"content\",s)}get links(){return this.get(\"links\")}set links(s){this.set(\"links\",s)}}const Gh=Response_Response;class Responses extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"responses\"}get default(){return this.get(\"default\")}set default(s){this.set(\"default\",s)}}const Qh=Responses;const td=class UnsupportedOperationError extends Ko{};class JSONSchema extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"JSONSchemaDraft4\"}get idProp(){return this.get(\"id\")}set idProp(s){this.set(\"id\",s)}get $schema(){return this.get(\"$schema\")}set $schema(s){this.set(\"$schema\",s)}get multipleOf(){return this.get(\"multipleOf\")}set multipleOf(s){this.set(\"multipleOf\",s)}get maximum(){return this.get(\"maximum\")}set maximum(s){this.set(\"maximum\",s)}get exclusiveMaximum(){return this.get(\"exclusiveMaximum\")}set exclusiveMaximum(s){this.set(\"exclusiveMaximum\",s)}get minimum(){return this.get(\"minimum\")}set minimum(s){this.set(\"minimum\",s)}get exclusiveMinimum(){return this.get(\"exclusiveMinimum\")}set exclusiveMinimum(s){this.set(\"exclusiveMinimum\",s)}get maxLength(){return this.get(\"maxLength\")}set maxLength(s){this.set(\"maxLength\",s)}get minLength(){return this.get(\"minLength\")}set minLength(s){this.set(\"minLength\",s)}get pattern(){return this.get(\"pattern\")}set pattern(s){this.set(\"pattern\",s)}get additionalItems(){return this.get(\"additionalItems\")}set additionalItems(s){this.set(\"additionalItems\",s)}get items(){return this.get(\"items\")}set items(s){this.set(\"items\",s)}get maxItems(){return this.get(\"maxItems\")}set maxItems(s){this.set(\"maxItems\",s)}get minItems(){return this.get(\"minItems\")}set minItems(s){this.set(\"minItems\",s)}get uniqueItems(){return this.get(\"uniqueItems\")}set uniqueItems(s){this.set(\"uniqueItems\",s)}get maxProperties(){return this.get(\"maxProperties\")}set maxProperties(s){this.set(\"maxProperties\",s)}get minProperties(){return this.get(\"minProperties\")}set minProperties(s){this.set(\"minProperties\",s)}get required(){return this.get(\"required\")}set required(s){this.set(\"required\",s)}get properties(){return this.get(\"properties\")}set properties(s){this.set(\"properties\",s)}get additionalProperties(){return this.get(\"additionalProperties\")}set additionalProperties(s){this.set(\"additionalProperties\",s)}get patternProperties(){return this.get(\"patternProperties\")}set patternProperties(s){this.set(\"patternProperties\",s)}get dependencies(){return this.get(\"dependencies\")}set dependencies(s){this.set(\"dependencies\",s)}get enum(){return this.get(\"enum\")}set enum(s){this.set(\"enum\",s)}get type(){return this.get(\"type\")}set type(s){this.set(\"type\",s)}get allOf(){return this.get(\"allOf\")}set allOf(s){this.set(\"allOf\",s)}get anyOf(){return this.get(\"anyOf\")}set anyOf(s){this.set(\"anyOf\",s)}get oneOf(){return this.get(\"oneOf\")}set oneOf(s){this.set(\"oneOf\",s)}get not(){return this.get(\"not\")}set not(s){this.set(\"not\",s)}get definitions(){return this.get(\"definitions\")}set definitions(s){this.set(\"definitions\",s)}get title(){return this.get(\"title\")}set title(s){this.set(\"title\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get default(){return this.get(\"default\")}set default(s){this.set(\"default\",s)}get format(){return this.get(\"format\")}set format(s){this.set(\"format\",s)}get base(){return this.get(\"base\")}set base(s){this.set(\"base\",s)}get links(){return this.get(\"links\")}set links(s){this.set(\"links\",s)}get media(){return this.get(\"media\")}set media(s){this.set(\"media\",s)}get readOnly(){return this.get(\"readOnly\")}set readOnly(s){this.set(\"readOnly\",s)}}const sd=JSONSchema;class JSONReference extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"JSONReference\",this.classes.push(\"json-reference\")}get $ref(){return this.get(\"$ref\")}set $ref(s){this.set(\"$ref\",s)}}const id=JSONReference;class Media extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"media\"}get binaryEncoding(){return this.get(\"binaryEncoding\")}set binaryEncoding(s){this.set(\"binaryEncoding\",s)}get type(){return this.get(\"type\")}set type(s){this.set(\"type\",s)}}const cd=Media;class LinkDescription extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"linkDescription\"}get href(){return this.get(\"href\")}set href(s){this.set(\"href\",s)}get rel(){return this.get(\"rel\")}set rel(s){this.set(\"rel\",s)}get title(){return this.get(\"title\")}set title(s){this.set(\"title\",s)}get targetSchema(){return this.get(\"targetSchema\")}set targetSchema(s){this.set(\"targetSchema\",s)}get mediaType(){return this.get(\"mediaType\")}set mediaType(s){this.set(\"mediaType\",s)}get method(){return this.get(\"method\")}set method(s){this.set(\"method\",s)}get encType(){return this.get(\"encType\")}set encType(s){this.set(\"encType\",s)}get schema(){return this.get(\"schema\")}set schema(s){this.set(\"schema\",s)}}const ld=LinkDescription,emptyElement=s=>{const o=s.meta.length>0?cloneDeep(s.meta):void 0,i=s.attributes.length>0?cloneDeep(s.attributes):void 0;return new s.constructor(void 0,o,i)},cloneUnlessOtherwiseSpecified=(s,o)=>o.clone&&o.isMergeableElement(s)?deepmerge(emptyElement(s),s,o):s,ud={clone:!0,isMergeableElement:s=>Nu(s)||Mu(s),arrayElementMerge:(s,o,i)=>s.concat(o)[\"fantasy-land/map\"]((s=>cloneUnlessOtherwiseSpecified(s,i))),objectElementMerge:(s,o,i)=>{const a=Nu(s)?emptyElement(s):emptyElement(o);return Nu(s)&&s.forEach(((s,o,u)=>{const _=cloneShallow(u);_.value=cloneUnlessOtherwiseSpecified(s,i),a.content.push(_)})),o.forEach(((o,u,_)=>{const w=serializers_value(u);let x;if(Nu(s)&&s.hasKey(w)&&i.isMergeableElement(o)){const a=s.get(w);x=cloneShallow(_),x.value=((s,o)=>{if(\"function\"!=typeof o.customMerge)return deepmerge;const i=o.customMerge(s,o);return\"function\"==typeof i?i:deepmerge})(u,i)(a,o,i)}else x=cloneShallow(_),x.value=cloneUnlessOtherwiseSpecified(o,i);a.remove(w),a.content.push(x)})),a},customMerge:void 0,customMetaMerge:void 0,customAttributesMerge:void 0},deepmerge=(s,o,i)=>{var a,u,_;const w={...ud,...i};w.isMergeableElement=null!==(a=w.isMergeableElement)&&void 0!==a?a:ud.isMergeableElement,w.arrayElementMerge=null!==(u=w.arrayElementMerge)&&void 0!==u?u:ud.arrayElementMerge,w.objectElementMerge=null!==(_=w.objectElementMerge)&&void 0!==_?_:ud.objectElementMerge;const x=Mu(o);if(!(x===Mu(s)))return cloneUnlessOtherwiseSpecified(o,w);const C=x&&\"function\"==typeof w.arrayElementMerge?w.arrayElementMerge(s,o,w):w.objectElementMerge(s,o,w);return C.meta=(s=>\"function\"!=typeof s.customMetaMerge?s=>cloneDeep(s):s.customMetaMerge)(w)(s.meta,o.meta),C.attributes=(s=>\"function\"!=typeof s.customAttributesMerge?s=>cloneDeep(s):s.customAttributesMerge)(w)(s.attributes,o.attributes),C};deepmerge.all=(s,o)=>{if(!Array.isArray(s))throw new TypeError(\"First argument of deepmerge should be an array.\");return 0===s.length?new Su.Sh:s.reduce(((s,i)=>deepmerge(s,i,o)),emptyElement(s[0]))};const dd=deepmerge;const md=class Visitor_Visitor{element;constructor(s){Object.assign(this,s)}copyMetaAndAttributes(s,o){(s.meta.length>0||o.meta.length>0)&&(o.meta=dd(o.meta,s.meta)),hasElementSourceMap(s)&&assignSourceMap(o,s),(s.attributes.length>0||s.meta.length>0)&&(o.attributes=dd(o.attributes,s.attributes))}};const yd=class FallbackVisitor extends md{enter(s){return this.element=cloneDeep(s),qu}},copyProps=(s,o,i=[])=>{const a=Object.getOwnPropertyDescriptors(o);for(let s of i)delete a[s];Object.defineProperties(s,a)},protoChain=(s,o=[s])=>{const i=Object.getPrototypeOf(s);return null===i?o:protoChain(i,[...o,i])},hardMixProtos=(s,o,i=[])=>{var a;const u=null!==(a=((...s)=>{if(0===s.length)return;let o;const i=s.map((s=>protoChain(s)));for(;i.every((s=>s.length>0));){const s=i.map((s=>s.pop())),a=s[0];if(!s.every((s=>s===a)))break;o=a}return o})(...s))&&void 0!==a?a:Object.prototype,_=Object.create(u),w=protoChain(u);for(let o of s){let s=protoChain(o);for(let o=s.length-1;o>=0;o--){let a=s[o];-1===w.indexOf(a)&&(copyProps(_,a,[\"constructor\",...i]),w.push(a))}}return _.constructor=o,_},unique=s=>s.filter(((o,i)=>s.indexOf(o)==i)),getIngredientWithProp=(s,o)=>{const i=o.map((s=>protoChain(s)));let a=0,u=!0;for(;u;){u=!1;for(let _=o.length-1;_>=0;_--){const o=i[_][a];if(null!=o&&(u=!0,null!=Object.getOwnPropertyDescriptor(o,s)))return i[_][0]}a++}},proxyMix=(s,o=Object.prototype)=>new Proxy({},{getPrototypeOf:()=>o,setPrototypeOf(){throw Error(\"Cannot set prototype of Proxies created by ts-mixer\")},getOwnPropertyDescriptor:(o,i)=>Object.getOwnPropertyDescriptor(getIngredientWithProp(i,s)||{},i),defineProperty(){throw new Error(\"Cannot define new properties on Proxies created by ts-mixer\")},has:(i,a)=>void 0!==getIngredientWithProp(a,s)||void 0!==o[a],get:(i,a)=>(getIngredientWithProp(a,s)||o)[a],set(o,i,a){const u=getIngredientWithProp(i,s);if(void 0===u)throw new Error(\"Cannot set new properties on Proxies created by ts-mixer\");return u[i]=a,!0},deleteProperty(){throw new Error(\"Cannot delete properties on Proxies created by ts-mixer\")},ownKeys:()=>s.map(Object.getOwnPropertyNames).reduce(((s,o)=>o.concat(s.filter((s=>o.indexOf(s)<0)))))}),vd=null,_d=\"copy\",Sd=\"copy\",Ed=\"deep\",wd=new WeakMap,getMixinsForClass=s=>wd.get(s),mergeObjectsOfDecorators=(s,o)=>{var i,a;const u=unique([...Object.getOwnPropertyNames(s),...Object.getOwnPropertyNames(o)]),_={};for(let w of u)_[w]=unique([...null!==(i=null==s?void 0:s[w])&&void 0!==i?i:[],...null!==(a=null==o?void 0:o[w])&&void 0!==a?a:[]]);return _},mergePropertyAndMethodDecorators=(s,o)=>{var i,a,u,_;return{property:mergeObjectsOfDecorators(null!==(i=null==s?void 0:s.property)&&void 0!==i?i:{},null!==(a=null==o?void 0:o.property)&&void 0!==a?a:{}),method:mergeObjectsOfDecorators(null!==(u=null==s?void 0:s.method)&&void 0!==u?u:{},null!==(_=null==o?void 0:o.method)&&void 0!==_?_:{})}},mergeDecorators=(s,o)=>{var i,a,u,_,w,x;return{class:unique([...null!==(i=null==s?void 0:s.class)&&void 0!==i?i:[],...null!==(a=null==o?void 0:o.class)&&void 0!==a?a:[]]),static:mergePropertyAndMethodDecorators(null!==(u=null==s?void 0:s.static)&&void 0!==u?u:{},null!==(_=null==o?void 0:o.static)&&void 0!==_?_:{}),instance:mergePropertyAndMethodDecorators(null!==(w=null==s?void 0:s.instance)&&void 0!==w?w:{},null!==(x=null==o?void 0:o.instance)&&void 0!==x?x:{})}},xd=new Map,deepDecoratorSearch=(...s)=>{const o=((...s)=>{var o;const i=new Set,a=new Set([...s]);for(;a.size>0;)for(let s of a){const u=protoChain(s.prototype).map((s=>s.constructor)),_=[...u,...null!==(o=getMixinsForClass(s))&&void 0!==o?o:[]].filter((s=>!i.has(s)));for(let s of _)a.add(s);i.add(s),a.delete(s)}return[...i]})(...s).map((s=>xd.get(s))).filter((s=>!!s));return 0==o.length?{}:1==o.length?o[0]:o.reduce(((s,o)=>mergeDecorators(s,o)))},getDecoratorsForClass=s=>{let o=xd.get(s);return o||(o={},xd.set(s,o)),o};function Mixin(...s){var o,i,a;const u=s.map((s=>s.prototype)),_=vd;if(null!==_){const s=u.map((s=>s[_])).filter((s=>\"function\"==typeof s)),combinedInitFunction=function(...o){for(let i of s)i.apply(this,o)},o={[_]:combinedInitFunction};u.push(o)}function MixedClass(...o){for(const i of s)copyProps(this,new i(...o));null!==_&&\"function\"==typeof this[_]&&this[_].apply(this,o)}var w,x;MixedClass.prototype=\"copy\"===Sd?hardMixProtos(u,MixedClass):(w=u,x=MixedClass,proxyMix([...w,{constructor:x}])),Object.setPrototypeOf(MixedClass,\"copy\"===_d?hardMixProtos(s,null,[\"prototype\"]):proxyMix(s,Function.prototype));let C=MixedClass;if(\"none\"!==Ed){const u=\"deep\"===Ed?deepDecoratorSearch(...s):((...s)=>{const o=s.map((s=>getDecoratorsForClass(s)));return 0===o.length?{}:1===o.length?o[0]:o.reduce(((s,o)=>mergeDecorators(s,o)))})(...s);for(let s of null!==(o=null==u?void 0:u.class)&&void 0!==o?o:[]){const o=s(C);o&&(C=o)}applyPropAndMethodDecorators(null!==(i=null==u?void 0:u.static)&&void 0!==i?i:{},C),applyPropAndMethodDecorators(null!==(a=null==u?void 0:u.instance)&&void 0!==a?a:{},C.prototype)}var j,L;return j=C,L=s,wd.set(j,L),C}const applyPropAndMethodDecorators=(s,o)=>{const i=s.property,a=s.method;if(i)for(let s in i)for(let a of i[s])a(o,s);if(a)for(let s in a)for(let i of a[s])i(o,s,Object.getOwnPropertyDescriptor(o,s))};const kd=_curry1((function allPass(s){return $a(Aa(Ec,0,Oc(\"length\",s)),(function(){for(var o=0,i=s.length;o<i;){if(!s[o].apply(this,arguments))return!1;o+=1}return!0}))}));const Od=_curry1((function isNotEmpty(s){return!gp(s)}));const Ad=_curry2((function or(s,o){return s||o}));var Cd=dc($a(1,ou(au,_curry2((function either(s,o){return _isFunction(s)?function _either(){return s.apply(this,arguments)||o.apply(this,arguments)}:hc(Ad)(s,o)}))(cu,Mc))));const Id=kd([Jc,Cd,Od]);const Td=_curry2((function pick(s,o){for(var i={},a=0;a<s.length;)s[a]in o&&(i[s[a]]=o[s[a]]),a+=1;return i}));const Nd=class SpecificationVisitor extends md{specObj;passingOptionsNames=[\"specObj\",\"parent\"];constructor({specObj:s,...o}){super({...o}),this.specObj=s}retrievePassingOptions(){return Td(this.passingOptionsNames,this)}retrieveFixedFields(s){const o=Qu([\"visitors\",...s,\"fixedFields\"],this.specObj);return\"object\"==typeof o&&null!==o?Object.keys(o):[]}retrieveVisitor(s){return Qo(Mc,[\"visitors\",...s],this.specObj)?Qu([\"visitors\",...s],this.specObj):Qu([\"visitors\",...s,\"$visitor\"],this.specObj)}retrieveVisitorInstance(s,o={}){const i=this.retrievePassingOptions();return new(this.retrieveVisitor(s))({...i,...o})}toRefractedElement(s,o,i={}){const a=this.retrieveVisitorInstance(s,i);return a instanceof yd&&(null==a?void 0:a.constructor)===yd?cloneDeep(o):(visitor_visit(o,a,i),a.element)}};const Md=class FixedFieldsVisitor extends Nd{specPath;ignoredFields;constructor({specPath:s,ignoredFields:o,...i}){super({...i}),this.specPath=s,this.ignoredFields=o||[]}ObjectElement(s){const o=this.specPath(s),i=this.retrieveFixedFields(o);return s.forEach(((s,a,u)=>{if(ju(a)&&i.includes(serializers_value(a))&&!this.ignoredFields.includes(serializers_value(a))){const i=this.toRefractedElement([...o,\"fixedFields\",serializers_value(a)],s),_=new Su.Pr(cloneDeep(a),i);this.copyMetaAndAttributes(u,_),_.classes.push(\"fixed-field\"),this.element.content.push(_)}else this.ignoredFields.includes(serializers_value(a))||this.element.content.push(cloneDeep(u))})),this.copyMetaAndAttributes(s,this.element),qu}};const Rd=class ParentSchemaAwareVisitor{parent;constructor({parent:s}){this.parent=s}},Dd=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof sd||s(a)&&o(\"JSONSchemaDraft4\",a)&&i(\"object\",a))),Ld=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof id||s(a)&&o(\"JSONReference\",a)&&i(\"object\",a))),Fd=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof cd||s(a)&&o(\"media\",a)&&i(\"object\",a))),Bd=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof ld||s(a)&&o(\"linkDescription\",a)&&i(\"object\",a)));class JSONSchemaVisitor extends(Mixin(Md,Rd,yd)){constructor(s){super(s),this.element=new sd,this.specPath=fc([\"document\",\"objects\",\"JSONSchema\"])}get defaultDialectIdentifier(){return\"http://json-schema.org/draft-04/schema#\"}ObjectElement(s){return this.handleDialectIdentifier(s),this.handleSchemaIdentifier(s),this.parent=this.element,Md.prototype.ObjectElement.call(this,s)}handleDialectIdentifier(s){if(bc(this.parent)&&!ju(s.get(\"$schema\")))this.element.setMetaProperty(\"inheritedDialectIdentifier\",this.defaultDialectIdentifier);else if(Dd(this.parent)&&!ju(s.get(\"$schema\"))){const s=Na(serializers_value(this.parent.meta.get(\"inheritedDialectIdentifier\")),serializers_value(this.parent.$schema));this.element.setMetaProperty(\"inheritedDialectIdentifier\",s)}}handleSchemaIdentifier(s,o=\"id\"){const i=void 0!==this.parent?cloneDeep(this.parent.getMetaProperty(\"ancestorsSchemaIdentifiers\",[])):new Su.wE,a=serializers_value(s.get(o));Id(a)&&i.push(a),this.element.setMetaProperty(\"ancestorsSchemaIdentifiers\",i)}}const $d=JSONSchemaVisitor,isJSONReferenceLikeElement=s=>Nu(s)&&s.hasKey(\"$ref\");class ItemsVisitor extends(Mixin(Nd,Rd,yd)){ObjectElement(s){const o=isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"];return this.element=this.toRefractedElement(o,s),qu}ArrayElement(s){return this.element=new Su.wE,this.element.classes.push(\"json-schema-items\"),s.forEach((s=>{const o=isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"],i=this.toRefractedElement(o,s);this.element.push(i)})),this.copyMetaAndAttributes(s,this.element),qu}}const Ud=ItemsVisitor;const Vd=class RequiredVisitor extends yd{ArrayElement(s){const o=this.enter(s);return this.element.classes.push(\"json-schema-required\"),o}};const Wd=class PatternedFieldsVisitor extends Nd{specPath;ignoredFields;fieldPatternPredicate=es_F;constructor({specPath:s,ignoredFields:o,fieldPatternPredicate:i,...a}){super({...a}),this.specPath=s,this.ignoredFields=o||[],\"function\"==typeof i&&(this.fieldPatternPredicate=i)}ObjectElement(s){return s.forEach(((s,o,i)=>{if(!this.ignoredFields.includes(serializers_value(o))&&this.fieldPatternPredicate(serializers_value(o))){const a=this.specPath(s),u=this.toRefractedElement(a,s),_=new Su.Pr(cloneDeep(o),u);this.copyMetaAndAttributes(i,_),_.classes.push(\"patterned-field\"),this.element.content.push(_)}else this.ignoredFields.includes(serializers_value(o))||this.element.content.push(cloneDeep(i))})),this.copyMetaAndAttributes(s,this.element),qu}};const Jd=class MapVisitor extends Wd{constructor(s){super(s),this.fieldPatternPredicate=Id}};class PropertiesVisitor extends(Mixin(Jd,Rd,yd)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"json-schema-properties\"),this.specPath=s=>isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"]}}const Hd=PropertiesVisitor;class PatternPropertiesVisitor extends(Mixin(Jd,Rd,yd)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"json-schema-patternProperties\"),this.specPath=s=>isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"]}}const Kd=PatternPropertiesVisitor;class DependenciesVisitor extends(Mixin(Jd,Rd,yd)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"json-schema-dependencies\"),this.specPath=s=>isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"]}}const Gd=DependenciesVisitor;const Yd=class EnumVisitor extends yd{ArrayElement(s){const o=this.enter(s);return this.element.classes.push(\"json-schema-enum\"),o}};const Xd=class TypeVisitor extends yd{StringElement(s){const o=this.enter(s);return this.element.classes.push(\"json-schema-type\"),o}ArrayElement(s){const o=this.enter(s);return this.element.classes.push(\"json-schema-type\"),o}};class AllOfVisitor extends(Mixin(Nd,Rd,yd)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"json-schema-allOf\")}ArrayElement(s){return s.forEach((s=>{const o=isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"],i=this.toRefractedElement(o,s);this.element.push(i)})),this.copyMetaAndAttributes(s,this.element),qu}}const Qd=AllOfVisitor;class AnyOfVisitor extends(Mixin(Nd,Rd,yd)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"json-schema-anyOf\")}ArrayElement(s){return s.forEach((s=>{const o=isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"],i=this.toRefractedElement(o,s);this.element.push(i)})),this.copyMetaAndAttributes(s,this.element),qu}}const Zd=AnyOfVisitor;class OneOfVisitor extends(Mixin(Nd,Rd,yd)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"json-schema-oneOf\")}ArrayElement(s){return s.forEach((s=>{const o=isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"],i=this.toRefractedElement(o,s);this.element.push(i)})),this.copyMetaAndAttributes(s,this.element),qu}}const ef=OneOfVisitor;class DefinitionsVisitor extends(Mixin(Jd,Rd,yd)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"json-schema-definitions\"),this.specPath=s=>isJSONReferenceLikeElement(s)?[\"document\",\"objects\",\"JSONReference\"]:[\"document\",\"objects\",\"JSONSchema\"]}}const rf=DefinitionsVisitor;class LinksVisitor extends(Mixin(Nd,Rd,yd)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"json-schema-links\")}ArrayElement(s){return s.forEach((s=>{const o=this.toRefractedElement([\"document\",\"objects\",\"LinkDescription\"],s);this.element.push(o)})),this.copyMetaAndAttributes(s,this.element),qu}}const of=LinksVisitor;class JSONReferenceVisitor extends(Mixin(Md,yd)){constructor(s){super(s),this.element=new id,this.specPath=fc([\"document\",\"objects\",\"JSONReference\"])}ObjectElement(s){const o=Md.prototype.ObjectElement.call(this,s);return ju(this.element.$ref)&&this.element.classes.push(\"reference-element\"),o}}const af=JSONReferenceVisitor;const cf=class $RefVisitor extends yd{StringElement(s){const o=this.enter(s);return this.element.classes.push(\"reference-value\"),o}};const lf=_curry3((function ifElse(s,o,i){return $a(Math.max(s.length,o.length,i.length),(function _ifElse(){return s.apply(this,arguments)?o.apply(this,arguments):i.apply(this,arguments)}))}));const uf=_curry1((function comparator(s){return function(o,i){return s(o,i)?-1:s(i,o)?1:0}}));var hf=_curry2((function sort(s,o){return Array.prototype.slice.call(o,0).sort(s)}));const df=hf;var mf=_curry1((function(s){return _nth(0,s)}));const gf=mf;const yf=_curry1(_reduced);const bf=dc(Ju);const _f=ou(yp,Od);function _toConsumableArray(s){return function _arrayWithoutHoles(s){if(Array.isArray(s))return _arrayLikeToArray(s)}(s)||function _iterableToArray(s){if(\"undefined\"!=typeof Symbol&&null!=s[Symbol.iterator]||null!=s[\"@@iterator\"])return Array.from(s)}(s)||function _unsupportedIterableToArray(s,o){if(s){if(\"string\"==typeof s)return _arrayLikeToArray(s,o);var i={}.toString.call(s).slice(8,-1);return\"Object\"===i&&s.constructor&&(i=s.constructor.name),\"Map\"===i||\"Set\"===i?Array.from(s):\"Arguments\"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?_arrayLikeToArray(s,o):void 0}}(s)||function _nonIterableSpread(){throw new TypeError(\"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}function _arrayLikeToArray(s,o){(null==o||o>s.length)&&(o=s.length);for(var i=0,a=Array(o);i<o;i++)a[i]=s[i];return a}var Sf=pipe(df(uf((function(s,o){return s.length>o.length}))),gf,Da(\"length\")),xf=za((function(s,o,i){var a=i.apply(void 0,_toConsumableArray(s));return bf(a)?yf(a):o}));const kf=lf(_f,(function dispatchImpl(s){var o=Sf(s);return $a(o,(function(){for(var o=arguments.length,i=new Array(o),a=0;a<o;a++)i[a]=arguments[a];return Aa(xf(i),void 0,s)}))}),gc);const Of=class AlternatingVisitor extends Nd{alternator;constructor({alternator:s,...o}){super({...o}),this.alternator=s}enter(s){const o=this.alternator.map((({predicate:s,specPath:o})=>lf(s,fc(o),gc))),i=kf(o)(s);return this.element=this.toRefractedElement(i,s),qu}};const Cf=class SchemaOrReferenceVisitor extends Of{constructor(s){super(s),this.alternator=[{predicate:isJSONReferenceLikeElement,specPath:[\"document\",\"objects\",\"JSONReference\"]},{predicate:es_T,specPath:[\"document\",\"objects\",\"JSONSchema\"]}]}};class MediaVisitor extends(Mixin(Md,yd)){constructor(s){super(s),this.element=new cd,this.specPath=fc([\"document\",\"objects\",\"Media\"])}}const jf=MediaVisitor;class LinkDescriptionVisitor extends(Mixin(Md,yd)){constructor(s){super(s),this.element=new ld,this.specPath=fc([\"document\",\"objects\",\"LinkDescription\"])}}const Pf=LinkDescriptionVisitor,Tf={visitors:{value:yd,JSONSchemaOrJSONReferenceVisitor:Cf,document:{objects:{JSONSchema:{$visitor:$d,fixedFields:{id:{$ref:\"#/visitors/value\"},$schema:{$ref:\"#/visitors/value\"},multipleOf:{$ref:\"#/visitors/value\"},maximum:{$ref:\"#/visitors/value\"},exclusiveMaximum:{$ref:\"#/visitors/value\"},minimum:{$ref:\"#/visitors/value\"},exclusiveMinimum:{$ref:\"#/visitors/value\"},maxLength:{$ref:\"#/visitors/value\"},minLength:{$ref:\"#/visitors/value\"},pattern:{$ref:\"#/visitors/value\"},additionalItems:Cf,items:Ud,maxItems:{$ref:\"#/visitors/value\"},minItems:{$ref:\"#/visitors/value\"},uniqueItems:{$ref:\"#/visitors/value\"},maxProperties:{$ref:\"#/visitors/value\"},minProperties:{$ref:\"#/visitors/value\"},required:Vd,properties:Hd,additionalProperties:Cf,patternProperties:Kd,dependencies:Gd,enum:Yd,type:Xd,allOf:Qd,anyOf:Zd,oneOf:ef,not:Cf,definitions:rf,title:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},default:{$ref:\"#/visitors/value\"},format:{$ref:\"#/visitors/value\"},base:{$ref:\"#/visitors/value\"},links:of,media:{$ref:\"#/visitors/document/objects/Media\"},readOnly:{$ref:\"#/visitors/value\"}}},JSONReference:{$visitor:af,fixedFields:{$ref:cf}},Media:{$visitor:jf,fixedFields:{binaryEncoding:{$ref:\"#/visitors/value\"},type:{$ref:\"#/visitors/value\"}}},LinkDescription:{$visitor:Pf,fixedFields:{href:{$ref:\"#/visitors/value\"},rel:{$ref:\"#/visitors/value\"},title:{$ref:\"#/visitors/value\"},targetSchema:Cf,mediaType:{$ref:\"#/visitors/value\"},method:{$ref:\"#/visitors/value\"},encType:{$ref:\"#/visitors/value\"},schema:Cf}}}}}},traversal_visitor_getNodeType=s=>{if(Cu(s))return`${s.element.charAt(0).toUpperCase()+s.element.slice(1)}Element`},Nf={JSONSchemaDraft4Element:[\"content\"],JSONReferenceElement:[\"content\"],MediaElement:[\"content\"],LinkDescriptionElement:[\"content\"],...np},Rf={namespace:s=>{const{base:o}=s;return o.register(\"jSONSchemaDraft4\",sd),o.register(\"jSONReference\",id),o.register(\"media\",cd),o.register(\"linkDescription\",ld),o}},Df=Rf,refractor_toolbox=()=>{const s=createNamespace(Df);return{predicates:{...ae,isStringElement:ju},namespace:s}},refractor_refract=(s,{specPath:o=[\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],plugins:i=[],specificationObj:a=Tf}={})=>{const u=(0,Su.e)(s),_=dereference(a),w=new(Qu(o,_))({specObj:_});return visitor_visit(u,w),dispatchPluginsSync(w.element,i,{toolboxCreator:refractor_toolbox,visitorOptions:{keyMap:Nf,nodeTypeGetter:traversal_visitor_getNodeType}})},refractor_createRefractor=s=>(o,i={})=>refractor_refract(o,{specPath:s,...i});sd.refract=refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"]),id.refract=refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"JSONReference\",\"$visitor\"]),cd.refract=refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Media\",\"$visitor\"]),ld.refract=refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"]);const Ff=class Schema_Schema extends sd{constructor(s,o,i){super(s,o,i),this.element=\"schema\",this.classes.push(\"json-schema-draft-4\")}get idProp(){throw new td(\"idProp getter in Schema class is not not supported.\")}set idProp(s){throw new td(\"idProp setter in Schema class is not not supported.\")}get $schema(){throw new td(\"$schema getter in Schema class is not not supported.\")}set $schema(s){throw new td(\"$schema setter in Schema class is not not supported.\")}get additionalItems(){return this.get(\"additionalItems\")}set additionalItems(s){this.set(\"additionalItems\",s)}get items(){return this.get(\"items\")}set items(s){this.set(\"items\",s)}get additionalProperties(){return this.get(\"additionalProperties\")}set additionalProperties(s){this.set(\"additionalProperties\",s)}get patternProperties(){throw new td(\"patternProperties getter in Schema class is not not supported.\")}set patternProperties(s){throw new td(\"patternProperties setter in Schema class is not not supported.\")}get dependencies(){throw new td(\"dependencies getter in Schema class is not not supported.\")}set dependencies(s){throw new td(\"dependencies setter in Schema class is not not supported.\")}get type(){return this.get(\"type\")}set type(s){this.set(\"type\",s)}get not(){return this.get(\"not\")}set not(s){this.set(\"not\",s)}get definitions(){throw new td(\"definitions getter in Schema class is not not supported.\")}set definitions(s){throw new td(\"definitions setter in Schema class is not not supported.\")}get base(){throw new td(\"base getter in Schema class is not not supported.\")}set base(s){throw new td(\"base setter in Schema class is not not supported.\")}get links(){throw new td(\"links getter in Schema class is not not supported.\")}set links(s){throw new td(\"links setter in Schema class is not not supported.\")}get media(){throw new td(\"media getter in Schema class is not not supported.\")}set media(s){throw new td(\"media setter in Schema class is not not supported.\")}get nullable(){return this.get(\"nullable\")}set nullable(s){this.set(\"nullable\",s)}get discriminator(){return this.get(\"discriminator\")}set discriminator(s){this.set(\"discriminator\",s)}get writeOnly(){return this.get(\"writeOnly\")}set writeOnly(s){this.set(\"writeOnly\",s)}get xml(){return this.get(\"xml\")}set xml(s){this.set(\"xml\",s)}get externalDocs(){return this.get(\"externalDocs\")}set externalDocs(s){this.set(\"externalDocs\",s)}get example(){return this.get(\"example\")}set example(s){this.set(\"example\",s)}get deprecated(){return this.get(\"deprecated\")}set deprecated(s){this.set(\"deprecated\",s)}};class SecurityRequirement extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"securityRequirement\"}}const Vf=SecurityRequirement;class SecurityScheme extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"securityScheme\"}get type(){return this.get(\"type\")}set type(s){this.set(\"type\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get name(){return this.get(\"name\")}set name(s){this.set(\"name\",s)}get in(){return this.get(\"in\")}set in(s){this.set(\"in\",s)}get scheme(){return this.get(\"scheme\")}set scheme(s){this.set(\"scheme\",s)}get bearerFormat(){return this.get(\"bearerFormat\")}set bearerFormat(s){this.set(\"bearerFormat\",s)}get flows(){return this.get(\"flows\")}set flows(s){this.set(\"flows\",s)}get openIdConnectUrl(){return this.get(\"openIdConnectUrl\")}set openIdConnectUrl(s){this.set(\"openIdConnectUrl\",s)}}const Wf=SecurityScheme;class Server extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"server\"}get url(){return this.get(\"url\")}set url(s){this.set(\"url\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get variables(){return this.get(\"variables\")}set variables(s){this.set(\"variables\",s)}}const Jf=Server;class ServerVariable extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"serverVariable\"}get enum(){return this.get(\"enum\")}set enum(s){this.set(\"enum\",s)}get default(){return this.get(\"default\")}set default(s){this.set(\"default\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}}const Hf=ServerVariable;class Tag extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"tag\"}get name(){return this.get(\"name\")}set name(s){this.set(\"name\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get externalDocs(){return this.get(\"externalDocs\")}set externalDocs(s){this.set(\"externalDocs\",s)}}const Gf=Tag;class Xml extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"xml\"}get name(){return this.get(\"name\")}set name(s){this.set(\"name\",s)}get namespace(){return this.get(\"namespace\")}set namespace(s){this.set(\"namespace\",s)}get prefix(){return this.get(\"prefix\")}set prefix(s){this.set(\"prefix\",s)}get attribute(){return this.get(\"attribute\")}set attribute(s){this.set(\"attribute\",s)}get wrapped(){return this.get(\"wrapped\")}set wrapped(s){this.set(\"wrapped\",s)}}const Xf=Xml;const Qf=class visitors_Visitor_Visitor{element;constructor(s={}){Object.assign(this,s)}copyMetaAndAttributes(s,o){(s.meta.length>0||o.meta.length>0)&&(o.meta=dd(o.meta,s.meta)),hasElementSourceMap(s)&&assignSourceMap(o,s),(s.attributes.length>0||s.meta.length>0)&&(o.attributes=dd(o.attributes,s.attributes))}};const em=class FallbackVisitor_FallbackVisitor extends Qf{enter(s){return this.element=cloneDeep(s),qu}};const tm=class SpecificationVisitor_SpecificationVisitor extends Qf{specObj;passingOptionsNames=[\"specObj\",\"openApiGenericElement\",\"openApiSemanticElement\"];openApiGenericElement;openApiSemanticElement;constructor({specObj:s,passingOptionsNames:o,openApiGenericElement:i,openApiSemanticElement:a,...u}){super({...u}),this.specObj=s,this.openApiGenericElement=i,this.openApiSemanticElement=a,Array.isArray(o)&&(this.passingOptionsNames=o)}retrievePassingOptions(){return Td(this.passingOptionsNames,this)}retrieveFixedFields(s){const o=Qu([\"visitors\",...s,\"fixedFields\"],this.specObj);return\"object\"==typeof o&&null!==o?Object.keys(o):[]}retrieveVisitor(s){return Qo(Mc,[\"visitors\",...s],this.specObj)?Qu([\"visitors\",...s],this.specObj):Qu([\"visitors\",...s,\"$visitor\"],this.specObj)}retrieveVisitorInstance(s,o={}){const i=this.retrievePassingOptions();return new(this.retrieveVisitor(s))({...i,...o})}toRefractedElement(s,o,i={}){const a=this.retrieveVisitorInstance(s,i);return a instanceof em&&(null==a?void 0:a.constructor)===em?cloneDeep(o):(visitor_visit(o,a,i),a.element)}};var rm=function(){function XTake(s,o){this.xf=o,this.n=s,this.i=0}return XTake.prototype[\"@@transducer/init\"]=_xfBase_init,XTake.prototype[\"@@transducer/result\"]=_xfBase_result,XTake.prototype[\"@@transducer/step\"]=function(s,o){this.i+=1;var i=0===this.n?s:this.xf[\"@@transducer/step\"](s,o);return this.n>=0&&this.i>=this.n?_reduced(i):i},XTake}();function _xtake(s){return function(o){return new rm(s,o)}}const nm=_curry2(_dispatchable([\"take\"],_xtake,(function take(s,o){return ja(0,s<0?1/0:s,o)})));var sm=_curry2((function(s,o){return na(nm(s.length,o),s)}));const om=sm,isReferenceLikeElement=s=>Nu(s)&&s.hasKey(\"$ref\"),im=Nu,am=Nu,isOpenApiExtension=s=>ju(s.key)&&om(\"x-\",serializers_value(s.key));const cm=class FixedFieldsVisitor_FixedFieldsVisitor extends tm{specPath;ignoredFields;canSupportSpecificationExtensions=!0;specificationExtensionPredicate=isOpenApiExtension;constructor({specPath:s,ignoredFields:o,canSupportSpecificationExtensions:i,specificationExtensionPredicate:a,...u}){super({...u}),this.specPath=s,this.ignoredFields=o||[],\"boolean\"==typeof i&&(this.canSupportSpecificationExtensions=i),\"function\"==typeof a&&(this.specificationExtensionPredicate=a)}ObjectElement(s){const o=this.specPath(s),i=this.retrieveFixedFields(o);return s.forEach(((s,a,u)=>{if(ju(a)&&i.includes(serializers_value(a))&&!this.ignoredFields.includes(serializers_value(a))){const i=this.toRefractedElement([...o,\"fixedFields\",serializers_value(a)],s),_=new Su.Pr(cloneDeep(a),i);this.copyMetaAndAttributes(u,_),_.classes.push(\"fixed-field\"),this.element.content.push(_)}else if(this.canSupportSpecificationExtensions&&this.specificationExtensionPredicate(u)){const s=this.toRefractedElement([\"document\",\"extension\"],u);this.element.content.push(s)}else this.ignoredFields.includes(serializers_value(a))||this.element.content.push(cloneDeep(u))})),this.copyMetaAndAttributes(s,this.element),qu}};class OpenApi3_0Visitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Rh,this.specPath=fc([\"document\",\"objects\",\"OpenApi\"]),this.canSupportSpecificationExtensions=!0}ObjectElement(s){return cm.prototype.ObjectElement.call(this,s)}}const lm=OpenApi3_0Visitor;class OpenapiVisitor extends(Mixin(tm,em)){StringElement(s){const o=new Ih(serializers_value(s));return this.copyMetaAndAttributes(s,o),this.element=o,qu}}const um=OpenapiVisitor;const pm=class SpecificationExtensionVisitor extends tm{MemberElement(s){return this.element=cloneDeep(s),this.element.classes.push(\"specification-extension\"),qu}};class InfoVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new vh,this.specPath=fc([\"document\",\"objects\",\"Info\"]),this.canSupportSpecificationExtensions=!0}}const hm=InfoVisitor;const dm=class VersionVisitor extends em{StringElement(s){const o=super.enter(s);return this.element.classes.push(\"api-version\"),this.element.classes.push(\"version\"),o}};class ContactVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Zp,this.specPath=fc([\"document\",\"objects\",\"Contact\"]),this.canSupportSpecificationExtensions=!0}}const fm=ContactVisitor;class LicenseVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new _h,this.specPath=fc([\"document\",\"objects\",\"License\"]),this.canSupportSpecificationExtensions=!0}}const mm=LicenseVisitor;class LinkVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new wh,this.specPath=fc([\"document\",\"objects\",\"Link\"]),this.canSupportSpecificationExtensions=!0}ObjectElement(s){const o=cm.prototype.ObjectElement.call(this,s);return(ju(this.element.operationId)||ju(this.element.operationRef))&&this.element.classes.push(\"reference-element\"),o}}const gm=LinkVisitor;const ym=class OperationRefVisitor extends em{StringElement(s){const o=super.enter(s);return this.element.classes.push(\"reference-value\"),o}};const vm=class OperationIdVisitor extends em{StringElement(s){const o=super.enter(s);return this.element.classes.push(\"reference-value\"),o}};const bm=class PatternedFieldsVisitor_PatternedFieldsVisitor extends tm{specPath;ignoredFields;fieldPatternPredicate=es_F;canSupportSpecificationExtensions=!1;specificationExtensionPredicate=isOpenApiExtension;constructor({specPath:s,ignoredFields:o,fieldPatternPredicate:i,canSupportSpecificationExtensions:a,specificationExtensionPredicate:u,..._}){super({..._}),this.specPath=s,this.ignoredFields=o||[],\"function\"==typeof i&&(this.fieldPatternPredicate=i),\"boolean\"==typeof a&&(this.canSupportSpecificationExtensions=a),\"function\"==typeof u&&(this.specificationExtensionPredicate=u)}ObjectElement(s){return s.forEach(((s,o,i)=>{if(this.canSupportSpecificationExtensions&&this.specificationExtensionPredicate(i)){const s=this.toRefractedElement([\"document\",\"extension\"],i);this.element.content.push(s)}else if(!this.ignoredFields.includes(serializers_value(o))&&this.fieldPatternPredicate(serializers_value(o))){const a=this.specPath(s),u=this.toRefractedElement(a,s),_=new Su.Pr(cloneDeep(o),u);this.copyMetaAndAttributes(i,_),_.classes.push(\"patterned-field\"),this.element.content.push(_)}else this.ignoredFields.includes(serializers_value(o))||this.element.content.push(cloneDeep(i))})),this.copyMetaAndAttributes(s,this.element),qu}};const _m=class MapVisitor_MapVisitor extends bm{constructor(s){super(s),this.fieldPatternPredicate=Id}};class LinkParameters extends Su.Sh{static primaryClass=\"link-parameters\";constructor(s,o,i){super(s,o,i),this.classes.push(LinkParameters.primaryClass)}}const Sm=LinkParameters;class ParametersVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Sm,this.specPath=fc([\"value\"])}}const Em=ParametersVisitor;class ServerVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Jf,this.specPath=fc([\"document\",\"objects\",\"Server\"]),this.canSupportSpecificationExtensions=!0}}const wm=ServerVisitor;const xm=class UrlVisitor extends em{StringElement(s){const o=super.enter(s);return this.element.classes.push(\"server-url\"),o}};class Servers extends Su.wE{static primaryClass=\"servers\";constructor(s,o,i){super(s,o,i),this.classes.push(Servers.primaryClass)}}const km=Servers;class ServersVisitor extends(Mixin(tm,em)){constructor(s){super(s),this.element=new km}ArrayElement(s){return s.forEach((s=>{const o=im(s)?[\"document\",\"objects\",\"Server\"]:[\"value\"],i=this.toRefractedElement(o,s);this.element.push(i)})),this.copyMetaAndAttributes(s,this.element),qu}}const Om=ServersVisitor;class ServerVariableVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Hf,this.specPath=fc([\"document\",\"objects\",\"ServerVariable\"]),this.canSupportSpecificationExtensions=!0}}const Am=ServerVariableVisitor;class ServerVariables extends Su.Sh{static primaryClass=\"server-variables\";constructor(s,o,i){super(s,o,i),this.classes.push(ServerVariables.primaryClass)}}const Cm=ServerVariables;class VariablesVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Cm,this.specPath=fc([\"document\",\"objects\",\"ServerVariable\"])}}const jm=VariablesVisitor;class MediaTypeVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Oh,this.specPath=fc([\"document\",\"objects\",\"MediaType\"]),this.canSupportSpecificationExtensions=!0}}const Pm=MediaTypeVisitor;const Im=class AlternatingVisitor_AlternatingVisitor extends tm{alternator;constructor({alternator:s,...o}){super({...o}),this.alternator=s||[]}enter(s){const o=this.alternator.map((({predicate:s,specPath:o})=>lf(s,fc(o),gc))),i=kf(o)(s);return this.element=this.toRefractedElement(i,s),qu}},Tm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Xp||s(a)&&o(\"callback\",a)&&i(\"object\",a))),Nm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Qp||s(a)&&o(\"components\",a)&&i(\"object\",a))),Mm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Zp||s(a)&&o(\"contact\",a)&&i(\"object\",a))),Rm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof uh||s(a)&&o(\"example\",a)&&i(\"object\",a))),Dm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof dh||s(a)&&o(\"externalDocumentation\",a)&&i(\"object\",a))),Lm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof fh||s(a)&&o(\"header\",a)&&i(\"object\",a))),Fm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof vh||s(a)&&o(\"info\",a)&&i(\"object\",a))),Bm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof _h||s(a)&&o(\"license\",a)&&i(\"object\",a))),$m=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof wh||s(a)&&o(\"link\",a)&&i(\"object\",a))),qm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Ih||s(a)&&o(\"openapi\",a)&&i(\"string\",a))),Um=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i,hasClass:a})=>u=>u instanceof Rh||s(u)&&o(\"openApi3_0\",u)&&i(\"object\",u)&&a(\"api\",u))),Vm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Dh||s(a)&&o(\"operation\",a)&&i(\"object\",a))),zm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Lh||s(a)&&o(\"parameter\",a)&&i(\"object\",a))),Wm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Fh||s(a)&&o(\"pathItem\",a)&&i(\"object\",a))),Jm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Jh||s(a)&&o(\"paths\",a)&&i(\"object\",a))),Hm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Hh||s(a)&&o(\"reference\",a)&&i(\"object\",a))),Km=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Kh||s(a)&&o(\"requestBody\",a)&&i(\"object\",a))),Gm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Gh||s(a)&&o(\"response\",a)&&i(\"object\",a))),Ym=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Qh||s(a)&&o(\"responses\",a)&&i(\"object\",a))),Xm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Ff||s(a)&&o(\"schema\",a)&&i(\"object\",a))),isBooleanJsonSchemaElement=s=>Tu(s)&&s.classes.includes(\"boolean-json-schema\"),Qm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Vf||s(a)&&o(\"securityRequirement\",a)&&i(\"object\",a))),Zm=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Wf||s(a)&&o(\"securityScheme\",a)&&i(\"object\",a))),eg=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Jf||s(a)&&o(\"server\",a)&&i(\"object\",a))),rg=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Hf||s(a)&&o(\"serverVariable\",a)&&i(\"object\",a))),ng=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Oh||s(a)&&o(\"mediaType\",a)&&i(\"object\",a))),sg=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i,hasClass:a})=>u=>u instanceof km||s(u)&&o(\"array\",u)&&i(\"array\",u)&&a(\"servers\",u))),og=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof th||s(a)&&o(\"discriminator\",a)&&i(\"object\",a)));class SchemaVisitor extends(Mixin(Im,em)){constructor(s){super(s),this.alternator=[{predicate:isReferenceLikeElement,specPath:[\"document\",\"objects\",\"Reference\"]},{predicate:es_T,specPath:[\"document\",\"objects\",\"Schema\"]}]}ObjectElement(s){const o=Im.prototype.enter.call(this,s);return Hm(this.element)&&this.element.setMetaProperty(\"referenced-element\",\"schema\"),o}}const lg=SchemaVisitor;class ExamplesVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"examples\"),this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Example\"],this.canSupportSpecificationExtensions=!0}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"example\")})),o}}const pg=ExamplesVisitor;class MediaTypeExamples extends Su.Sh{static primaryClass=\"media-type-examples\";constructor(s,o,i){super(s,o,i),this.classes.push(MediaTypeExamples.primaryClass),this.classes.push(\"examples\")}}const fg=MediaTypeExamples;const mg=class ExamplesVisitor_ExamplesVisitor extends pg{constructor(s){super(s),this.element=new fg}};class MediaTypeEncoding extends Su.Sh{static primaryClass=\"media-type-encoding\";constructor(s,o,i){super(s,o,i),this.classes.push(MediaTypeEncoding.primaryClass)}}const gg=MediaTypeEncoding;class EncodingVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new gg,this.specPath=fc([\"document\",\"objects\",\"Encoding\"])}}const yg=EncodingVisitor;class SecurityRequirementVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Vf,this.specPath=fc([\"value\"])}}const _g=SecurityRequirementVisitor;class Security extends Su.wE{static primaryClass=\"security\";constructor(s,o,i){super(s,o,i),this.classes.push(Security.primaryClass)}}const xg=Security;class SecurityVisitor extends(Mixin(tm,em)){constructor(s){super(s),this.element=new xg}ArrayElement(s){return s.forEach((s=>{if(Nu(s)){const o=this.toRefractedElement([\"document\",\"objects\",\"SecurityRequirement\"],s);this.element.push(o)}else this.element.push(cloneDeep(s))})),this.copyMetaAndAttributes(s,this.element),qu}}const kg=SecurityVisitor;class ComponentsVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Qp,this.specPath=fc([\"document\",\"objects\",\"Components\"]),this.canSupportSpecificationExtensions=!0}}const qg=ComponentsVisitor;class TagVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Gf,this.specPath=fc([\"document\",\"objects\",\"Tag\"]),this.canSupportSpecificationExtensions=!0}}const Ug=TagVisitor;class ReferenceVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Hh,this.specPath=fc([\"document\",\"objects\",\"Reference\"]),this.canSupportSpecificationExtensions=!1}ObjectElement(s){const o=cm.prototype.ObjectElement.call(this,s);return ju(this.element.$ref)&&this.element.classes.push(\"reference-element\"),o}}const Vg=ReferenceVisitor;const zg=class $RefVisitor_$RefVisitor extends em{StringElement(s){const o=super.enter(s);return this.element.classes.push(\"reference-value\"),o}};class ParameterVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Lh,this.specPath=fc([\"document\",\"objects\",\"Parameter\"]),this.canSupportSpecificationExtensions=!0}ObjectElement(s){const o=cm.prototype.ObjectElement.call(this,s);return Nu(this.element.contentProp)&&this.element.contentProp.filter(ng).forEach(((s,o)=>{s.setMetaProperty(\"media-type\",serializers_value(o))})),o}}const Wg=ParameterVisitor;class SchemaVisitor_SchemaVisitor extends(Mixin(Im,em)){constructor(s){super(s),this.alternator=[{predicate:isReferenceLikeElement,specPath:[\"document\",\"objects\",\"Reference\"]},{predicate:es_T,specPath:[\"document\",\"objects\",\"Schema\"]}]}ObjectElement(s){const o=Im.prototype.enter.call(this,s);return Hm(this.element)&&this.element.setMetaProperty(\"referenced-element\",\"schema\"),o}}const Kg=SchemaVisitor_SchemaVisitor;class HeaderVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new fh,this.specPath=fc([\"document\",\"objects\",\"Header\"]),this.canSupportSpecificationExtensions=!0}}const Yg=HeaderVisitor;class header_SchemaVisitor_SchemaVisitor extends(Mixin(Im,em)){constructor(s){super(s),this.alternator=[{predicate:isReferenceLikeElement,specPath:[\"document\",\"objects\",\"Reference\"]},{predicate:es_T,specPath:[\"document\",\"objects\",\"Schema\"]}]}ObjectElement(s){const o=Im.prototype.enter.call(this,s);return Hm(this.element)&&this.element.setMetaProperty(\"referenced-element\",\"schema\"),o}}const Xg=header_SchemaVisitor_SchemaVisitor;class HeaderExamples extends Su.Sh{static primaryClass=\"header-examples\";constructor(s,o,i){super(s,o,i),this.classes.push(HeaderExamples.primaryClass),this.classes.push(\"examples\")}}const Zg=HeaderExamples;const ey=class header_ExamplesVisitor_ExamplesVisitor extends pg{constructor(s){super(s),this.element=new Zg}};class ContentVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"content\"),this.specPath=fc([\"document\",\"objects\",\"MediaType\"])}}const ty=ContentVisitor;class HeaderContent extends Su.Sh{static primaryClass=\"header-content\";constructor(s,o,i){super(s,o,i),this.classes.push(HeaderContent.primaryClass),this.classes.push(\"content\")}}const ry=HeaderContent;const ny=class ContentVisitor_ContentVisitor extends ty{constructor(s){super(s),this.element=new ry}};class schema_SchemaVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Ff,this.specPath=fc([\"document\",\"objects\",\"Schema\"]),this.canSupportSpecificationExtensions=!0}}const sy=schema_SchemaVisitor,oy=Tf.visitors.document.objects.JSONSchema.fixedFields.allOf;const iy=class AllOfVisitor_AllOfVisitor extends oy{ArrayElement(s){const o=oy.prototype.ArrayElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"schema\")})),o}},ay=Tf.visitors.document.objects.JSONSchema.fixedFields.anyOf;const cy=class AnyOfVisitor_AnyOfVisitor extends ay{ArrayElement(s){const o=ay.prototype.ArrayElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"schema\")})),o}},ly=Tf.visitors.document.objects.JSONSchema.fixedFields.oneOf;const uy=class OneOfVisitor_OneOfVisitor extends ly{ArrayElement(s){const o=ly.prototype.ArrayElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"schema\")})),o}},py=Tf.visitors.document.objects.JSONSchema.fixedFields.items;const hy=class ItemsVisitor_ItemsVisitor extends py{ObjectElement(s){const o=py.prototype.ObjectElement.call(this,s);return Hm(this.element)&&this.element.setMetaProperty(\"referenced-element\",\"schema\"),o}ArrayElement(s){return this.enter(s)}},dy=Tf.visitors.document.objects.JSONSchema.fixedFields.properties;const fy=class PropertiesVisitor_PropertiesVisitor extends dy{ObjectElement(s){const o=dy.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"schema\")})),o}},my=Tf.visitors.document.objects.JSONSchema.fixedFields.type;const gy=class TypeVisitor_TypeVisitor extends my{ArrayElement(s){return this.enter(s)}},yy=Tf.visitors.JSONSchemaOrJSONReferenceVisitor;const vy=class SchemaOrReferenceVisitor_SchemaOrReferenceVisitor extends yy{ObjectElement(s){const o=yy.prototype.enter.call(this,s);return Hm(this.element)&&this.element.setMetaProperty(\"referenced-element\",\"schema\"),o}};class DiscriminatorVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new th,this.specPath=fc([\"document\",\"objects\",\"Discriminator\"]),this.canSupportSpecificationExtensions=!1}}const by=DiscriminatorVisitor;class DiscriminatorMapping extends Su.Sh{static primaryClass=\"discriminator-mapping\";constructor(s,o,i){super(s,o,i),this.classes.push(DiscriminatorMapping.primaryClass)}}const _y=DiscriminatorMapping;class MappingVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new _y,this.specPath=fc([\"value\"])}}const Sy=MappingVisitor;class XmlVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Xf,this.specPath=fc([\"document\",\"objects\",\"XML\"]),this.canSupportSpecificationExtensions=!0}}const Ey=XmlVisitor;class ParameterExamples extends Su.Sh{static primaryClass=\"parameter-examples\";constructor(s,o,i){super(s,o,i),this.classes.push(ParameterExamples.primaryClass),this.classes.push(\"examples\")}}const wy=ParameterExamples;const xy=class parameter_ExamplesVisitor_ExamplesVisitor extends pg{constructor(s){super(s),this.element=new wy}};class ParameterContent extends Su.Sh{static primaryClass=\"parameter-content\";constructor(s,o,i){super(s,o,i),this.classes.push(ParameterContent.primaryClass),this.classes.push(\"content\")}}const ky=ParameterContent;const Oy=class parameter_ContentVisitor_ContentVisitor extends ty{constructor(s){super(s),this.element=new ky}};class ComponentsSchemas extends Su.Sh{static primaryClass=\"components-schemas\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsSchemas.primaryClass)}}const Ay=ComponentsSchemas;class SchemasVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Ay,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Schema\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"schema\")})),o}}const Cy=SchemasVisitor;class ComponentsResponses extends Su.Sh{static primaryClass=\"components-responses\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsResponses.primaryClass)}}const jy=ComponentsResponses;class ResponsesVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new jy,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Response\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"response\")})),this.element.filter(Gm).forEach(((s,o)=>{s.setMetaProperty(\"http-status-code\",serializers_value(o))})),o}}const Py=ResponsesVisitor;class ComponentsParameters extends Su.Sh{static primaryClass=\"components-parameters\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsParameters.primaryClass),this.classes.push(\"parameters\")}}const Iy=ComponentsParameters;class ParametersVisitor_ParametersVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Iy,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Parameter\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"parameter\")})),o}}const Ty=ParametersVisitor_ParametersVisitor;class ComponentsExamples extends Su.Sh{static primaryClass=\"components-examples\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsExamples.primaryClass),this.classes.push(\"examples\")}}const Ny=ComponentsExamples;class components_ExamplesVisitor_ExamplesVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Ny,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Example\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"example\")})),o}}const My=components_ExamplesVisitor_ExamplesVisitor;class ComponentsRequestBodies extends Su.Sh{static primaryClass=\"components-request-bodies\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsRequestBodies.primaryClass)}}const Ry=ComponentsRequestBodies;class RequestBodiesVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Ry,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"RequestBody\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"requestBody\")})),o}}const Dy=RequestBodiesVisitor;class ComponentsHeaders extends Su.Sh{static primaryClass=\"components-headers\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsHeaders.primaryClass)}}const Ly=ComponentsHeaders;class HeadersVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Ly,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Header\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"header\")})),this.element.filter(Lm).forEach(((s,o)=>{s.setMetaProperty(\"header-name\",serializers_value(o))})),o}}const Fy=HeadersVisitor;class ComponentsSecuritySchemes extends Su.Sh{static primaryClass=\"components-security-schemes\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsSecuritySchemes.primaryClass)}}const By=ComponentsSecuritySchemes;class SecuritySchemesVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new By,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"SecurityScheme\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"securityScheme\")})),o}}const $y=SecuritySchemesVisitor;class ComponentsLinks extends Su.Sh{static primaryClass=\"components-links\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsLinks.primaryClass)}}const qy=ComponentsLinks;class LinksVisitor_LinksVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new qy,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Link\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"link\")})),o}}const Uy=LinksVisitor_LinksVisitor;class ComponentsCallbacks extends Su.Sh{static primaryClass=\"components-callbacks\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsCallbacks.primaryClass)}}const Vy=ComponentsCallbacks;class CallbacksVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Vy,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Callback\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"callback\")})),o}}const zy=CallbacksVisitor;class ExampleVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new uh,this.specPath=fc([\"document\",\"objects\",\"Example\"]),this.canSupportSpecificationExtensions=!0}ObjectElement(s){const o=cm.prototype.ObjectElement.call(this,s);return ju(this.element.externalValue)&&this.element.classes.push(\"reference-element\"),o}}const Wy=ExampleVisitor;const Jy=class ExternalValueVisitor extends em{StringElement(s){const o=super.enter(s);return this.element.classes.push(\"reference-value\"),o}};class ExternalDocumentationVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new dh,this.specPath=fc([\"document\",\"objects\",\"ExternalDocumentation\"]),this.canSupportSpecificationExtensions=!0}}const Hy=ExternalDocumentationVisitor;class encoding_EncodingVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new rh,this.specPath=fc([\"document\",\"objects\",\"Encoding\"]),this.canSupportSpecificationExtensions=!0}ObjectElement(s){const o=cm.prototype.ObjectElement.call(this,s);return Nu(this.element.headers)&&this.element.headers.filter(Lm).forEach(((s,o)=>{s.setMetaProperty(\"header-name\",serializers_value(o))})),o}}const Ky=encoding_EncodingVisitor;class EncodingHeaders extends Su.Sh{static primaryClass=\"encoding-headers\";constructor(s,o,i){super(s,o,i),this.classes.push(EncodingHeaders.primaryClass)}}const Gy=EncodingHeaders;class HeadersVisitor_HeadersVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Gy,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Header\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"header\")})),this.element.forEach(((s,o)=>{if(!Lm(s))return;const i=serializers_value(o);s.setMetaProperty(\"headerName\",i)})),o}}const Yy=HeadersVisitor_HeadersVisitor;class PathsVisitor extends(Mixin(bm,em)){constructor(s){super(s),this.element=new Jh,this.specPath=fc([\"document\",\"objects\",\"PathItem\"]),this.canSupportSpecificationExtensions=!0,this.fieldPatternPredicate=es_T}ObjectElement(s){const o=bm.prototype.ObjectElement.call(this,s);return this.element.filter(Wm).forEach(((s,o)=>{o.classes.push(\"openapi-path-template\"),o.classes.push(\"path-template\"),s.setMetaProperty(\"path\",cloneDeep(o))})),o}}const Xy=PathsVisitor;class RequestBodyVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Kh,this.specPath=fc([\"document\",\"objects\",\"RequestBody\"])}ObjectElement(s){const o=cm.prototype.ObjectElement.call(this,s);return Nu(this.element.contentProp)&&this.element.contentProp.filter(ng).forEach(((s,o)=>{s.setMetaProperty(\"media-type\",serializers_value(o))})),o}}const Qy=RequestBodyVisitor;class RequestBodyContent extends Su.Sh{static primaryClass=\"request-body-content\";constructor(s,o,i){super(s,o,i),this.classes.push(RequestBodyContent.primaryClass),this.classes.push(\"content\")}}const Zy=RequestBodyContent;const ev=class request_body_ContentVisitor_ContentVisitor extends ty{constructor(s){super(s),this.element=new Zy}};class CallbackVisitor extends(Mixin(bm,em)){constructor(s){super(s),this.element=new Xp,this.specPath=fc([\"document\",\"objects\",\"PathItem\"]),this.canSupportSpecificationExtensions=!0,this.fieldPatternPredicate=s=>/{(?<expression>[^}]{1,2083})}/.test(String(s))}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Wm).forEach(((s,o)=>{s.setMetaProperty(\"runtime-expression\",serializers_value(o))})),o}}const tv=CallbackVisitor;class ResponseVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Gh,this.specPath=fc([\"document\",\"objects\",\"Response\"])}ObjectElement(s){const o=cm.prototype.ObjectElement.call(this,s);return Nu(this.element.contentProp)&&this.element.contentProp.filter(ng).forEach(((s,o)=>{s.setMetaProperty(\"media-type\",serializers_value(o))})),Nu(this.element.headers)&&this.element.headers.filter(Lm).forEach(((s,o)=>{s.setMetaProperty(\"header-name\",serializers_value(o))})),o}}const rv=ResponseVisitor;class ResponseHeaders extends Su.Sh{static primaryClass=\"response-headers\";constructor(s,o,i){super(s,o,i),this.classes.push(ResponseHeaders.primaryClass)}}const nv=ResponseHeaders;class response_HeadersVisitor_HeadersVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new nv,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Header\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"header\")})),this.element.forEach(((s,o)=>{if(!Lm(s))return;const i=serializers_value(o);s.setMetaProperty(\"header-name\",i)})),o}}const sv=response_HeadersVisitor_HeadersVisitor;class ResponseContent extends Su.Sh{static primaryClass=\"response-content\";constructor(s,o,i){super(s,o,i),this.classes.push(ResponseContent.primaryClass),this.classes.push(\"content\")}}const ov=ResponseContent;const iv=class response_ContentVisitor_ContentVisitor extends ty{constructor(s){super(s),this.element=new ov}};class ResponseLinks extends Su.Sh{static primaryClass=\"response-links\";constructor(s,o,i){super(s,o,i),this.classes.push(ResponseLinks.primaryClass)}}const av=ResponseLinks;class response_LinksVisitor_LinksVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new av,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Link\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"link\")})),o}}const cv=response_LinksVisitor_LinksVisitor;function _isNumber(s){return\"[object Number]\"===Object.prototype.toString.call(s)}var lv=_curry2((function range(s,o){if(!_isNumber(s)||!_isNumber(o))throw new TypeError(\"Both arguments to range must be numbers\");for(var i=Array(s<o?o-s:0),a=s<0?o+Math.abs(s):o-s,u=0;u<a;)i[u]=u+s,u+=1;return i}));const uv=lv;function hasOrAdd(s,o,i){var a,u=typeof s;switch(u){case\"string\":case\"number\":return 0===s&&1/s==-1/0?!!i._items[\"-0\"]||(o&&(i._items[\"-0\"]=!0),!1):null!==i._nativeSet?o?(a=i._nativeSet.size,i._nativeSet.add(s),i._nativeSet.size===a):i._nativeSet.has(s):u in i._items?s in i._items[u]||(o&&(i._items[u][s]=!0),!1):(o&&(i._items[u]={},i._items[u][s]=!0),!1);case\"boolean\":if(u in i._items){var _=s?1:0;return!!i._items[u][_]||(o&&(i._items[u][_]=!0),!1)}return o&&(i._items[u]=s?[!1,!0]:[!0,!1]),!1;case\"function\":return null!==i._nativeSet?o?(a=i._nativeSet.size,i._nativeSet.add(s),i._nativeSet.size===a):i._nativeSet.has(s):u in i._items?!!_includes(s,i._items[u])||(o&&i._items[u].push(s),!1):(o&&(i._items[u]=[s]),!1);case\"undefined\":return!!i._items[u]||(o&&(i._items[u]=!0),!1);case\"object\":if(null===s)return!!i._items.null||(o&&(i._items.null=!0),!1);default:return(u=Object.prototype.toString.call(s))in i._items?!!_includes(s,i._items[u])||(o&&i._items[u].push(s),!1):(o&&(i._items[u]=[s]),!1)}}const pv=function(){function _Set(){this._nativeSet=\"function\"==typeof Set?new Set:null,this._items={}}return _Set.prototype.add=function(s){return!hasOrAdd(s,!0,this)},_Set.prototype.has=function(s){return hasOrAdd(s,!1,this)},_Set}();var hv=_curry2((function difference(s,o){for(var i=[],a=0,u=s.length,_=o.length,w=new pv,x=0;x<_;x+=1)w.add(o[x]);for(;a<u;)w.add(s[a])&&(i[i.length]=s[a]),a+=1;return i}));const dv=hv;class MixedFieldsVisitor extends(Mixin(cm,bm)){specPathFixedFields;specPathPatternedFields;constructor({specPathFixedFields:s,specPathPatternedFields:o,...i}){super({...i}),this.specPathFixedFields=s,this.specPathPatternedFields=o}ObjectElement(s){const{specPath:o,ignoredFields:i}=this;try{this.specPath=this.specPathFixedFields;const o=this.retrieveFixedFields(this.specPath(s));this.ignoredFields=[...i,...dv(s.keys(),o)],cm.prototype.ObjectElement.call(this,s),this.specPath=this.specPathPatternedFields,this.ignoredFields=o,bm.prototype.ObjectElement.call(this,s)}catch(s){throw this.specPath=o,s}return qu}}const fv=MixedFieldsVisitor;class responses_ResponsesVisitor extends(Mixin(fv,em)){constructor(s){super(s),this.element=new Qh,this.specPathFixedFields=fc([\"document\",\"objects\",\"Responses\"]),this.canSupportSpecificationExtensions=!0,this.specPathPatternedFields=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Response\"],this.fieldPatternPredicate=s=>new RegExp(`^(1XX|2XX|3XX|4XX|5XX|${uv(100,600).join(\"|\")})$`).test(String(s))}ObjectElement(s){const o=fv.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"response\")})),this.element.filter(Gm).forEach(((s,o)=>{const i=cloneDeep(o);this.fieldPatternPredicate(serializers_value(i))&&s.setMetaProperty(\"http-status-code\",i)})),o}}const mv=responses_ResponsesVisitor;class DefaultVisitor extends(Mixin(Im,em)){constructor(s){super(s),this.alternator=[{predicate:isReferenceLikeElement,specPath:[\"document\",\"objects\",\"Reference\"]},{predicate:es_T,specPath:[\"document\",\"objects\",\"Response\"]}]}ObjectElement(s){const o=Im.prototype.enter.call(this,s);return Hm(this.element)?this.element.setMetaProperty(\"referenced-element\",\"response\"):Gm(this.element)&&this.element.setMetaProperty(\"http-status-code\",\"default\"),o}}const gv=DefaultVisitor;class OperationVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Dh,this.specPath=fc([\"document\",\"objects\",\"Operation\"])}}const yv=OperationVisitor;class OperationTags extends Su.wE{static primaryClass=\"operation-tags\";constructor(s,o,i){super(s,o,i),this.classes.push(OperationTags.primaryClass)}}const vv=OperationTags;const bv=class TagsVisitor extends em{constructor(s){super(s),this.element=new vv}ArrayElement(s){return this.element=this.element.concat(cloneDeep(s)),qu}};class OperationParameters extends Su.wE{static primaryClass=\"operation-parameters\";constructor(s,o,i){super(s,o,i),this.classes.push(OperationParameters.primaryClass),this.classes.push(\"parameters\")}}const _v=OperationParameters;class open_api_3_0_ParametersVisitor_ParametersVisitor extends(Mixin(tm,em)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"parameters\")}ArrayElement(s){return s.forEach((s=>{const o=isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Parameter\"],i=this.toRefractedElement(o,s);Hm(i)&&i.setMetaProperty(\"referenced-element\",\"parameter\"),this.element.push(i)})),this.copyMetaAndAttributes(s,this.element),qu}}const Sv=open_api_3_0_ParametersVisitor_ParametersVisitor;const Ev=class operation_ParametersVisitor_ParametersVisitor extends Sv{constructor(s){super(s),this.element=new _v}};const wv=class RequestBodyVisitor_RequestBodyVisitor extends Im{constructor(s){super(s),this.alternator=[{predicate:isReferenceLikeElement,specPath:[\"document\",\"objects\",\"Reference\"]},{predicate:es_T,specPath:[\"document\",\"objects\",\"RequestBody\"]}]}ObjectElement(s){const o=Im.prototype.enter.call(this,s);return Hm(this.element)&&this.element.setMetaProperty(\"referenced-element\",\"requestBody\"),o}};class OperationCallbacks extends Su.Sh{static primaryClass=\"operation-callbacks\";constructor(s,o,i){super(s,o,i),this.classes.push(OperationCallbacks.primaryClass)}}const xv=OperationCallbacks;class CallbacksVisitor_CallbacksVisitor extends(Mixin(_m,em)){specPath;constructor(s){super(s),this.element=new xv,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"Callback\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(Hm).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"callback\")})),o}}const kv=CallbacksVisitor_CallbacksVisitor;class OperationSecurity extends Su.wE{static primaryClass=\"operation-security\";constructor(s,o,i){super(s,o,i),this.classes.push(OperationSecurity.primaryClass),this.classes.push(\"security\")}}const Ov=OperationSecurity;class SecurityVisitor_SecurityVisitor extends(Mixin(tm,em)){constructor(s){super(s),this.element=new Ov}ArrayElement(s){return s.forEach((s=>{const o=Nu(s)?[\"document\",\"objects\",\"SecurityRequirement\"]:[\"value\"],i=this.toRefractedElement(o,s);this.element.push(i)})),this.copyMetaAndAttributes(s,this.element),qu}}const Av=SecurityVisitor_SecurityVisitor;class OperationServers extends Su.wE{static primaryClass=\"operation-servers\";constructor(s,o,i){super(s,o,i),this.classes.push(OperationServers.primaryClass),this.classes.push(\"servers\")}}const Cv=OperationServers;const jv=class ServersVisitor_ServersVisitor extends Om{constructor(s){super(s),this.element=new Cv}};class PathItemVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Fh,this.specPath=fc([\"document\",\"objects\",\"PathItem\"])}ObjectElement(s){const o=cm.prototype.ObjectElement.call(this,s);return this.element.filter(Vm).forEach(((s,o)=>{const i=cloneDeep(o);i.content=serializers_value(i).toUpperCase(),s.setMetaProperty(\"http-method\",i)})),ju(this.element.$ref)&&this.element.classes.push(\"reference-element\"),o}}const Pv=PathItemVisitor;const Iv=class path_item_$RefVisitor_$RefVisitor extends em{StringElement(s){const o=super.enter(s);return this.element.classes.push(\"reference-value\"),o}};class PathItemServers extends Su.wE{static primaryClass=\"path-item-servers\";constructor(s,o,i){super(s,o,i),this.classes.push(PathItemServers.primaryClass),this.classes.push(\"servers\")}}const Tv=PathItemServers;const Nv=class path_item_ServersVisitor_ServersVisitor extends Om{constructor(s){super(s),this.element=new Tv}};class PathItemParameters extends Su.wE{static primaryClass=\"path-item-parameters\";constructor(s,o,i){super(s,o,i),this.classes.push(PathItemParameters.primaryClass),this.classes.push(\"parameters\")}}const Mv=PathItemParameters;const Rv=class path_item_ParametersVisitor_ParametersVisitor extends Sv{constructor(s){super(s),this.element=new Mv}};class SecuritySchemeVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Wf,this.specPath=fc([\"document\",\"objects\",\"SecurityScheme\"]),this.canSupportSpecificationExtensions=!0}}const Dv=SecuritySchemeVisitor;class OAuthFlowsVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Ph,this.specPath=fc([\"document\",\"objects\",\"OAuthFlows\"]),this.canSupportSpecificationExtensions=!0}}const Lv=OAuthFlowsVisitor;class OAuthFlowVisitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new jh,this.specPath=fc([\"document\",\"objects\",\"OAuthFlow\"]),this.canSupportSpecificationExtensions=!0}}const Fv=OAuthFlowVisitor;class OAuthFlowScopes extends Su.Sh{static primaryClass=\"oauth-flow-scopes\";constructor(s,o,i){super(s,o,i),this.classes.push(OAuthFlowScopes.primaryClass)}}const Bv=OAuthFlowScopes;class ScopesVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Bv,this.specPath=fc([\"value\"])}}const $v=ScopesVisitor;class Tags extends Su.wE{static primaryClass=\"tags\";constructor(s,o,i){super(s,o,i),this.classes.push(Tags.primaryClass)}}const qv=Tags;class TagsVisitor_TagsVisitor extends(Mixin(tm,em)){constructor(s){super(s),this.element=new qv}ArrayElement(s){return s.forEach((s=>{const o=am(s)?[\"document\",\"objects\",\"Tag\"]:[\"value\"],i=this.toRefractedElement(o,s);this.element.push(i)})),this.copyMetaAndAttributes(s,this.element),qu}}const Uv=TagsVisitor_TagsVisitor,{fixedFields:Vv}=Tf.visitors.document.objects.JSONSchema,zv={visitors:{value:em,document:{objects:{OpenApi:{$visitor:lm,fixedFields:{openapi:um,info:{$ref:\"#/visitors/document/objects/Info\"},servers:Om,paths:{$ref:\"#/visitors/document/objects/Paths\"},components:{$ref:\"#/visitors/document/objects/Components\"},security:kg,tags:Uv,externalDocs:{$ref:\"#/visitors/document/objects/ExternalDocumentation\"}}},Info:{$visitor:hm,fixedFields:{title:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},termsOfService:{$ref:\"#/visitors/value\"},contact:{$ref:\"#/visitors/document/objects/Contact\"},license:{$ref:\"#/visitors/document/objects/License\"},version:dm}},Contact:{$visitor:fm,fixedFields:{name:{$ref:\"#/visitors/value\"},url:{$ref:\"#/visitors/value\"},email:{$ref:\"#/visitors/value\"}}},License:{$visitor:mm,fixedFields:{name:{$ref:\"#/visitors/value\"},url:{$ref:\"#/visitors/value\"}}},Server:{$visitor:wm,fixedFields:{url:xm,description:{$ref:\"#/visitors/value\"},variables:jm}},ServerVariable:{$visitor:Am,fixedFields:{enum:{$ref:\"#/visitors/value\"},default:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"}}},Components:{$visitor:qg,fixedFields:{schemas:Cy,responses:Py,parameters:Ty,examples:My,requestBodies:Dy,headers:Fy,securitySchemes:$y,links:Uy,callbacks:zy}},Paths:{$visitor:Xy},PathItem:{$visitor:Pv,fixedFields:{$ref:Iv,summary:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},get:{$ref:\"#/visitors/document/objects/Operation\"},put:{$ref:\"#/visitors/document/objects/Operation\"},post:{$ref:\"#/visitors/document/objects/Operation\"},delete:{$ref:\"#/visitors/document/objects/Operation\"},options:{$ref:\"#/visitors/document/objects/Operation\"},head:{$ref:\"#/visitors/document/objects/Operation\"},patch:{$ref:\"#/visitors/document/objects/Operation\"},trace:{$ref:\"#/visitors/document/objects/Operation\"},servers:Nv,parameters:Rv}},Operation:{$visitor:yv,fixedFields:{tags:bv,summary:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},externalDocs:{$ref:\"#/visitors/document/objects/ExternalDocumentation\"},operationId:{$ref:\"#/visitors/value\"},parameters:Ev,requestBody:wv,responses:{$ref:\"#/visitors/document/objects/Responses\"},callbacks:kv,deprecated:{$ref:\"#/visitors/value\"},security:Av,servers:jv}},ExternalDocumentation:{$visitor:Hy,fixedFields:{description:{$ref:\"#/visitors/value\"},url:{$ref:\"#/visitors/value\"}}},Parameter:{$visitor:Wg,fixedFields:{name:{$ref:\"#/visitors/value\"},in:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},required:{$ref:\"#/visitors/value\"},deprecated:{$ref:\"#/visitors/value\"},allowEmptyValue:{$ref:\"#/visitors/value\"},style:{$ref:\"#/visitors/value\"},explode:{$ref:\"#/visitors/value\"},allowReserved:{$ref:\"#/visitors/value\"},schema:Kg,example:{$ref:\"#/visitors/value\"},examples:xy,content:Oy}},RequestBody:{$visitor:Qy,fixedFields:{description:{$ref:\"#/visitors/value\"},content:ev,required:{$ref:\"#/visitors/value\"}}},MediaType:{$visitor:Pm,fixedFields:{schema:lg,example:{$ref:\"#/visitors/value\"},examples:mg,encoding:yg}},Encoding:{$visitor:Ky,fixedFields:{contentType:{$ref:\"#/visitors/value\"},headers:Yy,style:{$ref:\"#/visitors/value\"},explode:{$ref:\"#/visitors/value\"},allowReserved:{$ref:\"#/visitors/value\"}}},Responses:{$visitor:mv,fixedFields:{default:gv}},Response:{$visitor:rv,fixedFields:{description:{$ref:\"#/visitors/value\"},headers:sv,content:iv,links:cv}},Callback:{$visitor:tv},Example:{$visitor:Wy,fixedFields:{summary:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},value:{$ref:\"#/visitors/value\"},externalValue:Jy}},Link:{$visitor:gm,fixedFields:{operationRef:ym,operationId:vm,parameters:Em,requestBody:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},server:{$ref:\"#/visitors/document/objects/Server\"}}},Header:{$visitor:Yg,fixedFields:{description:{$ref:\"#/visitors/value\"},required:{$ref:\"#/visitors/value\"},deprecated:{$ref:\"#/visitors/value\"},allowEmptyValue:{$ref:\"#/visitors/value\"},style:{$ref:\"#/visitors/value\"},explode:{$ref:\"#/visitors/value\"},allowReserved:{$ref:\"#/visitors/value\"},schema:Xg,example:{$ref:\"#/visitors/value\"},examples:ey,content:ny}},Tag:{$visitor:Ug,fixedFields:{name:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},externalDocs:{$ref:\"#/visitors/document/objects/ExternalDocumentation\"}}},Reference:{$visitor:Vg,fixedFields:{$ref:zg}},JSONSchema:{$ref:\"#/visitors/document/objects/Schema\"},JSONReference:{$ref:\"#/visitors/document/objects/Reference\"},Schema:{$visitor:sy,fixedFields:{title:Vv.title,multipleOf:Vv.multipleOf,maximum:Vv.maximum,exclusiveMaximum:Vv.exclusiveMaximum,minimum:Vv.minimum,exclusiveMinimum:Vv.exclusiveMinimum,maxLength:Vv.maxLength,minLength:Vv.minLength,pattern:Vv.pattern,maxItems:Vv.maxItems,minItems:Vv.minItems,uniqueItems:Vv.uniqueItems,maxProperties:Vv.maxProperties,minProperties:Vv.minProperties,required:Vv.required,enum:Vv.enum,type:gy,allOf:iy,anyOf:cy,oneOf:uy,not:vy,items:hy,properties:fy,additionalProperties:vy,description:Vv.description,format:Vv.format,default:Vv.default,nullable:{$ref:\"#/visitors/value\"},discriminator:{$ref:\"#/visitors/document/objects/Discriminator\"},writeOnly:{$ref:\"#/visitors/value\"},xml:{$ref:\"#/visitors/document/objects/XML\"},externalDocs:{$ref:\"#/visitors/document/objects/ExternalDocumentation\"},example:{$ref:\"#/visitors/value\"},deprecated:{$ref:\"#/visitors/value\"}}},Discriminator:{$visitor:by,fixedFields:{propertyName:{$ref:\"#/visitors/value\"},mapping:Sy}},XML:{$visitor:Ey,fixedFields:{name:{$ref:\"#/visitors/value\"},namespace:{$ref:\"#/visitors/value\"},prefix:{$ref:\"#/visitors/value\"},attribute:{$ref:\"#/visitors/value\"},wrapped:{$ref:\"#/visitors/value\"}}},SecurityScheme:{$visitor:Dv,fixedFields:{type:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"},name:{$ref:\"#/visitors/value\"},in:{$ref:\"#/visitors/value\"},scheme:{$ref:\"#/visitors/value\"},bearerFormat:{$ref:\"#/visitors/value\"},flows:{$ref:\"#/visitors/document/objects/OAuthFlows\"},openIdConnectUrl:{$ref:\"#/visitors/value\"}}},OAuthFlows:{$visitor:Lv,fixedFields:{implicit:{$ref:\"#/visitors/document/objects/OAuthFlow\"},password:{$ref:\"#/visitors/document/objects/OAuthFlow\"},clientCredentials:{$ref:\"#/visitors/document/objects/OAuthFlow\"},authorizationCode:{$ref:\"#/visitors/document/objects/OAuthFlow\"}}},OAuthFlow:{$visitor:Fv,fixedFields:{authorizationUrl:{$ref:\"#/visitors/value\"},tokenUrl:{$ref:\"#/visitors/value\"},refreshUrl:{$ref:\"#/visitors/value\"},scopes:$v}},SecurityRequirement:{$visitor:_g}},extension:{$visitor:pm}}}},src_traversal_visitor_getNodeType=s=>{if(Cu(s))return`${s.element.charAt(0).toUpperCase()+s.element.slice(1)}Element`},Wv={CallbackElement:[\"content\"],ComponentsElement:[\"content\"],ContactElement:[\"content\"],DiscriminatorElement:[\"content\"],Encoding:[\"content\"],Example:[\"content\"],ExternalDocumentationElement:[\"content\"],HeaderElement:[\"content\"],InfoElement:[\"content\"],LicenseElement:[\"content\"],MediaTypeElement:[\"content\"],OAuthFlowElement:[\"content\"],OAuthFlowsElement:[\"content\"],OpenApi3_0Element:[\"content\"],OperationElement:[\"content\"],ParameterElement:[\"content\"],PathItemElement:[\"content\"],PathsElement:[\"content\"],ReferenceElement:[\"content\"],RequestBodyElement:[\"content\"],ResponseElement:[\"content\"],ResponsesElement:[\"content\"],SchemaElement:[\"content\"],SecurityRequirementElement:[\"content\"],SecuritySchemeElement:[\"content\"],ServerElement:[\"content\"],ServerVariableElement:[\"content\"],TagElement:[\"content\"],...np},Jv={namespace:s=>{const{base:o}=s;return o.register(\"callback\",Xp),o.register(\"components\",Qp),o.register(\"contact\",Zp),o.register(\"discriminator\",th),o.register(\"encoding\",rh),o.register(\"example\",uh),o.register(\"externalDocumentation\",dh),o.register(\"header\",fh),o.register(\"info\",vh),o.register(\"license\",_h),o.register(\"link\",wh),o.register(\"mediaType\",Oh),o.register(\"oAuthFlow\",jh),o.register(\"oAuthFlows\",Ph),o.register(\"openapi\",Ih),o.register(\"openApi3_0\",Rh),o.register(\"operation\",Dh),o.register(\"parameter\",Lh),o.register(\"pathItem\",Fh),o.register(\"paths\",Jh),o.register(\"reference\",Hh),o.register(\"requestBody\",Kh),o.register(\"response\",Gh),o.register(\"responses\",Qh),o.register(\"schema\",Ff),o.register(\"securityRequirement\",Vf),o.register(\"securityScheme\",Wf),o.register(\"server\",Jf),o.register(\"serverVariable\",Hf),o.register(\"tag\",Gf),o.register(\"xml\",Xf),o}},Hv=Jv,src_refractor_toolbox=()=>{const s=createNamespace(Hv);return{predicates:{...ce,isElement:Cu,isStringElement:ju,isArrayElement:Mu,isObjectElement:Nu,isMemberElement:Ru,includesClasses,hasElementSourceMap},namespace:s}},src_refractor_refract=(s,{specPath:o=[\"visitors\",\"document\",\"objects\",\"OpenApi\",\"$visitor\"],plugins:i=[]}={})=>{const a=(0,Su.e)(s),u=dereference(zv),_=new(Qu(o,u))({specObj:u});return visitor_visit(a,_),dispatchPluginsSync(_.element,i,{toolboxCreator:src_refractor_toolbox,visitorOptions:{keyMap:Wv,nodeTypeGetter:src_traversal_visitor_getNodeType}})},src_refractor_createRefractor=s=>(o,i={})=>src_refractor_refract(o,{specPath:s,...i});Xp.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Callback\",\"$visitor\"]),Qp.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Components\",\"$visitor\"]),Zp.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Contact\",\"$visitor\"]),uh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Example\",\"$visitor\"]),th.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Discriminator\",\"$visitor\"]),rh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Encoding\",\"$visitor\"]),dh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"ExternalDocumentation\",\"$visitor\"]),fh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Header\",\"$visitor\"]),vh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Info\",\"$visitor\"]),_h.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"License\",\"$visitor\"]),wh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Link\",\"$visitor\"]),Oh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"MediaType\",\"$visitor\"]),jh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OAuthFlow\",\"$visitor\"]),Ph.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OAuthFlows\",\"$visitor\"]),Ih.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OpenApi\",\"fixedFields\",\"openapi\"]),Rh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OpenApi\",\"$visitor\"]),Dh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Operation\",\"$visitor\"]),Lh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Parameter\",\"$visitor\"]),Fh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"PathItem\",\"$visitor\"]),Jh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Paths\",\"$visitor\"]),Hh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Reference\",\"$visitor\"]),Kh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"RequestBody\",\"$visitor\"]),Gh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Response\",\"$visitor\"]),Qh.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Responses\",\"$visitor\"]),Ff.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Schema\",\"$visitor\"]),Vf.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"SecurityRequirement\",\"$visitor\"]),Wf.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"SecurityScheme\",\"$visitor\"]),Jf.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Server\",\"$visitor\"]),Hf.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"ServerVariable\",\"$visitor\"]),Gf.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Tag\",\"$visitor\"]),Xf.refract=src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"XML\",\"$visitor\"]);const Kv=class Callback_Callback extends Xp{};const Gv=class Components_Components extends Qp{get pathItems(){return this.get(\"pathItems\")}set pathItems(s){this.set(\"pathItems\",s)}};const Yv=class Contact_Contact extends Zp{};const Xv=class Discriminator_Discriminator extends th{};const Qv=class Encoding_Encoding extends rh{};const Zv=class Example_Example extends uh{};const eb=class ExternalDocumentation_ExternalDocumentation extends dh{};const tb=class Header_Header extends fh{get schema(){return this.get(\"schema\")}set schema(s){this.set(\"schema\",s)}};const nb=class Info_Info extends vh{get license(){return this.get(\"license\")}set license(s){this.set(\"license\",s)}get summary(){return this.get(\"summary\")}set summary(s){this.set(\"summary\",s)}};class JsonSchemaDialect extends Su.Om{static default=new JsonSchemaDialect(\"https://spec.openapis.org/oas/3.1/dialect/base\");constructor(s,o,i){super(s,o,i),this.element=\"jsonSchemaDialect\"}}const pb=JsonSchemaDialect;const mb=class License_License extends _h{get identifier(){return this.get(\"identifier\")}set identifier(s){this.set(\"identifier\",s)}};const yb=class Link_Link extends wh{};const _b=class MediaType_MediaType extends Oh{get schema(){return this.get(\"schema\")}set schema(s){this.set(\"schema\",s)}};const Sb=class OAuthFlow_OAuthFlow extends jh{};const wb=class OAuthFlows_OAuthFlows extends Ph{};const Ob=class Openapi_Openapi extends Ih{};class OpenApi3_1 extends Su.Sh{constructor(s,o,i){super(s,o,i),this.element=\"openApi3_1\",this.classes.push(\"api\")}get openapi(){return this.get(\"openapi\")}set openapi(s){this.set(\"openapi\",s)}get info(){return this.get(\"info\")}set info(s){this.set(\"info\",s)}get jsonSchemaDialect(){return this.get(\"jsonSchemaDialect\")}set jsonSchemaDialect(s){this.set(\"jsonSchemaDialect\",s)}get servers(){return this.get(\"servers\")}set servers(s){this.set(\"servers\",s)}get paths(){return this.get(\"paths\")}set paths(s){this.set(\"paths\",s)}get components(){return this.get(\"components\")}set components(s){this.set(\"components\",s)}get security(){return this.get(\"security\")}set security(s){this.set(\"security\",s)}get tags(){return this.get(\"tags\")}set tags(s){this.set(\"tags\",s)}get externalDocs(){return this.get(\"externalDocs\")}set externalDocs(s){this.set(\"externalDocs\",s)}get webhooks(){return this.get(\"webhooks\")}set webhooks(s){this.set(\"webhooks\",s)}}const Ab=OpenApi3_1;const Pb=class Operation_Operation extends Dh{get requestBody(){return this.get(\"requestBody\")}set requestBody(s){this.set(\"requestBody\",s)}};const Ib=class Parameter_Parameter extends Lh{get schema(){return this.get(\"schema\")}set schema(s){this.set(\"schema\",s)}};const Mb=class PathItem_PathItem extends Fh{get GET(){return this.get(\"get\")}set GET(s){this.set(\"GET\",s)}get PUT(){return this.get(\"put\")}set PUT(s){this.set(\"PUT\",s)}get POST(){return this.get(\"post\")}set POST(s){this.set(\"POST\",s)}get DELETE(){return this.get(\"delete\")}set DELETE(s){this.set(\"DELETE\",s)}get OPTIONS(){return this.get(\"options\")}set OPTIONS(s){this.set(\"OPTIONS\",s)}get HEAD(){return this.get(\"head\")}set HEAD(s){this.set(\"HEAD\",s)}get PATCH(){return this.get(\"patch\")}set PATCH(s){this.set(\"PATCH\",s)}get TRACE(){return this.get(\"trace\")}set TRACE(s){this.set(\"TRACE\",s)}};const Rb=class Paths_Paths extends Jh{};class Reference_Reference extends Hh{}Object.defineProperty(Reference_Reference.prototype,\"description\",{get(){return this.get(\"description\")},set(s){this.set(\"description\",s)},enumerable:!0}),Object.defineProperty(Reference_Reference.prototype,\"summary\",{get(){return this.get(\"summary\")},set(s){this.set(\"summary\",s)},enumerable:!0});const Lb=Reference_Reference;const qb=class RequestBody_RequestBody extends Kh{};const zb=class elements_Response_Response extends Gh{};const Qb=class Responses_Responses extends Qh{};const e_=class JSONSchema_JSONSchema extends sd{constructor(s,o,i){super(s,o,i),this.element=\"JSONSchemaDraft6\"}get idProp(){throw new td(\"id keyword from Core vocabulary has been renamed to $id.\")}set idProp(s){throw new td(\"id keyword from Core vocabulary has been renamed to $id.\")}get $id(){return this.get(\"$id\")}set $id(s){this.set(\"$id\",s)}get exclusiveMaximum(){return this.get(\"exclusiveMaximum\")}set exclusiveMaximum(s){this.set(\"exclusiveMaximum\",s)}get exclusiveMinimum(){return this.get(\"exclusiveMinimum\")}set exclusiveMinimum(s){this.set(\"exclusiveMinimum\",s)}get containsProp(){return this.get(\"contains\")}set containsProp(s){this.set(\"contains\",s)}get items(){return this.get(\"items\")}set items(s){this.set(\"items\",s)}get propertyNames(){return this.get(\"propertyNames\")}set propertyNames(s){this.set(\"propertyNames\",s)}get const(){return this.get(\"const\")}set const(s){this.set(\"const\",s)}get not(){return this.get(\"not\")}set not(s){this.set(\"not\",s)}get examples(){return this.get(\"examples\")}set examples(s){this.set(\"examples\",s)}};const t_=class LinkDescription_LinkDescription extends ld{get hrefSchema(){return this.get(\"hrefSchema\")}set hrefSchema(s){this.set(\"hrefSchema\",s)}get targetSchema(){return this.get(\"targetSchema\")}set targetSchema(s){this.set(\"targetSchema\",s)}get schema(){throw new td(\"schema keyword from Hyper-Schema vocabulary has been renamed to submissionSchema.\")}set schema(s){throw new td(\"schema keyword from Hyper-Schema vocabulary has been renamed to submissionSchema.\")}get submissionSchema(){return this.get(\"submissionSchema\")}set submissionSchema(s){this.set(\"submissionSchema\",s)}get method(){throw new td(\"method keyword from Hyper-Schema vocabulary has been removed.\")}set method(s){throw new td(\"method keyword from Hyper-Schema vocabulary has been removed.\")}get encType(){throw new td(\"encType keyword from Hyper-Schema vocabulary has been renamed to submissionEncType.\")}set encType(s){throw new td(\"encType keyword from Hyper-Schema vocabulary has been renamed to submissionEncType.\")}get submissionEncType(){return this.get(\"submissionEncType\")}set submissionEncType(s){this.set(\"submissionEncType\",s)}};var r_=_curry3((function assocPath(s,o,i){if(0===s.length)return o;var a=s[0];if(s.length>1){var u=!Ju(i)&&_has(a,i)&&\"object\"==typeof i[a]?i[a]:Xo(s[1])?[]:{};o=assocPath(Array.prototype.slice.call(s,1),o,u)}return function _assoc(s,o,i){if(Xo(s)&&ca(i)){var a=[].concat(i);return a[s]=o,a}var u={};for(var _ in i)u[_]=i[_];return u[s]=o,u}(a,o,i)}));const n_=r_;var s_=_curry3((function remove(s,o,i){var a=Array.prototype.slice.call(i,0);return a.splice(s,o),a}));const o_=s_;var i_=_curry3((function assoc(s,o,i){return n_([s],o,i)}));const a_=i_;var c_=_curry2((function dissocPath(s,o){if(null==o)return o;switch(s.length){case 0:return o;case 1:return function _dissoc(s,o){if(null==o)return o;if(Xo(s)&&ca(o))return o_(s,1,o);var i={};for(var a in o)i[a]=o[a];return delete i[s],i}(s[0],o);default:var i=s[0],a=Array.prototype.slice.call(s,1);return null==o[i]?function _shallowCloneObject(s,o){if(Xo(s)&&ca(o))return[].concat(o);var i={};for(var a in o)i[a]=o[a];return i}(i,o):a_(i,dissocPath(a,o[i]),o)}}));const l_=c_;const u_=class json_schema_JSONSchemaVisitor extends $d{constructor(s){super(s),this.element=new e_}get defaultDialectIdentifier(){return\"http://json-schema.org/draft-06/schema#\"}BooleanElement(s){const o=this.enter(s);return this.element.classes.push(\"boolean-json-schema\"),o}handleSchemaIdentifier(s,o=\"$id\"){return super.handleSchemaIdentifier(s,o)}};const p_=class json_schema_ItemsVisitor_ItemsVisitor extends Ud{BooleanElement(s){return this.element=this.toRefractedElement([\"document\",\"objects\",\"JSONSchema\"],s),qu}};const h_=class json_schema_ExamplesVisitor_ExamplesVisitor extends yd{ArrayElement(s){const o=this.enter(s);return this.element.classes.push(\"json-schema-examples\"),o}};const d_=class link_description_LinkDescriptionVisitor extends Pf{constructor(s){super(s),this.element=new t_}},f_=pipe(n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],u_),l_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"id\"]),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$id\"],Tf.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"contains\"],Tf.visitors.JSONSchemaOrJSONReferenceVisitor),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"items\"],p_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"propertyNames\"],Tf.visitors.JSONSchemaOrJSONReferenceVisitor),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"const\"],Tf.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"examples\"],h_),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"],d_),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"hrefSchema\"],Tf.visitors.JSONSchemaOrJSONReferenceVisitor),l_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"schema\"]),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"submissionSchema\"],Tf.visitors.JSONSchemaOrJSONReferenceVisitor),l_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"method\"]),l_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"encType\"]),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"submissionEncType\"],Tf.visitors.value))(Tf),m_={JSONSchemaDraft6Element:[\"content\"],JSONReferenceElement:[\"content\"],MediaElement:[\"content\"],LinkDescriptionElement:[\"content\"],...np},g_=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof e_||s(a)&&o(\"JSONSchemaDraft6\",a)&&i(\"object\",a))),y_=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof t_||s(a)&&o(\"linkDescription\",a)&&i(\"object\",a))),v_={namespace:s=>{const{base:o}=s;return o.register(\"jSONSchemaDraft6\",e_),o.register(\"jSONReference\",id),o.register(\"media\",cd),o.register(\"linkDescription\",t_),o}},b_=v_,apidom_ns_json_schema_draft_6_src_refractor_toolbox=()=>{const s=createNamespace(b_);return{predicates:{...le,isStringElement:ju},namespace:s}},apidom_ns_json_schema_draft_6_src_refractor_refract=(s,{specPath:o=[\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],plugins:i=[],specificationObj:a=f_}={})=>{const u=(0,Su.e)(s),_=dereference(a),w=new(Qu(o,_))({specObj:_});return visitor_visit(u,w),dispatchPluginsSync(w.element,i,{toolboxCreator:apidom_ns_json_schema_draft_6_src_refractor_toolbox,visitorOptions:{keyMap:m_,nodeTypeGetter:traversal_visitor_getNodeType}})},apidom_ns_json_schema_draft_6_src_refractor_createRefractor=s=>(o,i={})=>apidom_ns_json_schema_draft_6_src_refractor_refract(o,{specPath:s,...i});e_.refract=apidom_ns_json_schema_draft_6_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"]),t_.refract=apidom_ns_json_schema_draft_6_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"]);const S_=class elements_JSONSchema_JSONSchema extends e_{constructor(s,o,i){super(s,o,i),this.element=\"JSONSchemaDraft7\"}get $comment(){return this.get(\"$comment\")}set $comment(s){this.set(\"$comment\",s)}get items(){return this.get(\"items\")}set items(s){this.set(\"items\",s)}get if(){return this.get(\"if\")}set if(s){this.set(\"if\",s)}get then(){return this.get(\"then\")}set then(s){this.set(\"then\",s)}get else(){return this.get(\"else\")}set else(s){this.set(\"else\",s)}get not(){return this.get(\"not\")}set not(s){this.set(\"not\",s)}get contentEncoding(){return this.get(\"contentEncoding\")}set contentEncoding(s){this.set(\"contentEncoding\",s)}get contentMediaType(){return this.get(\"contentMediaType\")}set contentMediaType(s){this.set(\"contentMediaType\",s)}get media(){throw new td('media keyword from Hyper-Schema vocabulary has been moved to validation vocabulary as \"contentMediaType\" / \"contentEncoding\"')}set media(s){throw new td('media keyword from Hyper-Schema vocabulary has been moved to validation vocabulary as \"contentMediaType\" / \"contentEncoding\"')}get writeOnly(){return this.get(\"writeOnly\")}set writeOnly(s){this.set(\"writeOnly\",s)}};const E_=class elements_LinkDescription_LinkDescription extends t_{get anchor(){return this.get(\"anchor\")}set anchor(s){this.set(\"anchor\",s)}get anchorPointer(){return this.get(\"anchorPointer\")}set anchorPointer(s){this.set(\"anchorPointer\",s)}get templatePointers(){return this.get(\"templatePointers\")}set templatePointers(s){this.set(\"templatePointers\",s)}get templateRequired(){return this.get(\"templateRequired\")}set templateRequired(s){this.set(\"templateRequired\",s)}get targetSchema(){return this.get(\"targetSchema\")}set targetSchema(s){this.set(\"targetSchema\",s)}get mediaType(){throw new td(\"mediaType keyword from Hyper-Schema vocabulary has been renamed to targetMediaType.\")}set mediaType(s){throw new td(\"mediaType keyword from Hyper-Schema vocabulary has been renamed to targetMediaType.\")}get targetMediaType(){return this.get(\"targetMediaType\")}set targetMediaType(s){this.set(\"targetMediaType\",s)}get targetHints(){return this.get(\"targetHints\")}set targetHints(s){this.set(\"targetHints\",s)}get description(){return this.get(\"description\")}set description(s){this.set(\"description\",s)}get $comment(){return this.get(\"$comment\")}set $comment(s){this.set(\"$comment\",s)}get hrefSchema(){return this.get(\"hrefSchema\")}set hrefSchema(s){this.set(\"hrefSchema\",s)}get headerSchema(){return this.get(\"headerSchema\")}set headerSchema(s){this.set(\"headerSchema\",s)}get submissionSchema(){return this.get(\"submissionSchema\")}set submissionSchema(s){this.set(\"submissionSchema\",s)}get submissionEncType(){throw new td(\"submissionEncType keyword from Hyper-Schema vocabulary has been renamed to submissionMediaType.\")}set submissionEncType(s){throw new td(\"submissionEncType keyword from Hyper-Schema vocabulary has been renamed to submissionMediaType.\")}get submissionMediaType(){return this.get(\"submissionMediaType\")}set submissionMediaType(s){this.set(\"submissionMediaType\",s)}};const w_=class visitors_json_schema_JSONSchemaVisitor extends u_{constructor(s){super(s),this.element=new S_}get defaultDialectIdentifier(){return\"http://json-schema.org/draft-07/schema#\"}};const x_=class json_schema_link_description_LinkDescriptionVisitor extends d_{constructor(s){super(s),this.element=new E_}},k_=pipe(n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],w_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$comment\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"if\"],f_.visitors.JSONSchemaOrJSONReferenceVisitor),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"then\"],f_.visitors.JSONSchemaOrJSONReferenceVisitor),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"else\"],f_.visitors.JSONSchemaOrJSONReferenceVisitor),l_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"media\"]),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"contentEncoding\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"contentMediaType\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"writeOnly\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"],x_),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"anchor\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"anchorPointer\"],f_.visitors.value),l_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"mediaType\"]),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"targetMediaType\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"targetHints\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"description\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"$comment\"],f_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"headerSchema\"],f_.visitors.JSONSchemaOrJSONReferenceVisitor),l_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"submissionEncType\"]),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"submissionMediaType\"],f_.visitors.value))(f_),O_={JSONSchemaDraft7Element:[\"content\"],JSONReferenceElement:[\"content\"],LinkDescriptionElement:[\"content\"],...np},A_=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof S_||s(a)&&o(\"JSONSchemaDraft7\",a)&&i(\"object\",a))),C_=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof E_||s(a)&&o(\"linkDescription\",a)&&i(\"object\",a))),j_={namespace:s=>{const{base:o}=s;return o.register(\"jSONSchemaDraft7\",S_),o.register(\"jSONReference\",id),o.register(\"linkDescription\",E_),o}},P_=j_,apidom_ns_json_schema_draft_7_src_refractor_toolbox=()=>{const s=createNamespace(P_);return{predicates:{...pe,isStringElement:ju},namespace:s}},apidom_ns_json_schema_draft_7_src_refractor_refract=(s,{specPath:o=[\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],plugins:i=[],specificationObj:a=k_}={})=>{const u=(0,Su.e)(s),_=dereference(a),w=new(Qu(o,_))({specObj:_});return visitor_visit(u,w),dispatchPluginsSync(w.element,i,{toolboxCreator:apidom_ns_json_schema_draft_7_src_refractor_toolbox,visitorOptions:{keyMap:O_,nodeTypeGetter:traversal_visitor_getNodeType}})},apidom_ns_json_schema_draft_7_src_refractor_createRefractor=s=>(o,i={})=>apidom_ns_json_schema_draft_7_src_refractor_refract(o,{specPath:s,...i});S_.refract=apidom_ns_json_schema_draft_7_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"]),E_.refract=apidom_ns_json_schema_draft_7_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"]);const I_=class src_elements_JSONSchema_JSONSchema extends S_{constructor(s,o,i){super(s,o,i),this.element=\"JSONSchema201909\"}get $vocabulary(){return this.get(\"$vocabulary\")}set $vocabulary(s){this.set(\"$vocabulary\",s)}get $anchor(){return this.get(\"$anchor\")}set $anchor(s){this.set(\"$anchor\",s)}get $recursiveAnchor(){return this.get(\"$recursiveAnchor\")}set $recursiveAnchor(s){this.set(\"$recursiveAnchor\",s)}get $recursiveRef(){return this.get(\"$recursiveRef\")}set $recursiveRef(s){this.set(\"$recursiveRef\",s)}get $ref(){return this.get(\"$ref\")}set $ref(s){this.set(\"$ref\",s)}get $defs(){return this.get(\"$defs\")}set $defs(s){this.set(\"$defs\",s)}get definitions(){throw new td(\"definitions keyword from Validation vocabulary has been renamed to $defs.\")}set definitions(s){throw new td(\"definitions keyword from Validation vocabulary has been renamed to $defs.\")}get not(){return this.get(\"not\")}set not(s){this.set(\"not\",s)}get if(){return this.get(\"if\")}set if(s){this.set(\"if\",s)}get then(){return this.get(\"then\")}set then(s){this.set(\"then\",s)}get else(){return this.get(\"else\")}set else(s){this.set(\"else\",s)}get dependentSchemas(){return this.get(\"dependentSchemas\")}set dependentSchemas(s){this.set(\"dependentSchemas\",s)}get dependencies(){throw new td(\"dependencies keyword from Validation vocabulary has been renamed to dependentSchemas.\")}set dependencies(s){throw new td(\"dependencies keyword from Validation vocabulary has been renamed to dependentSchemas.\")}get items(){return this.get(\"items\")}set items(s){this.set(\"items\",s)}get containsProp(){return this.get(\"contains\")}set containsProp(s){this.set(\"contains\",s)}get additionalProperties(){return this.get(\"additionalProperties\")}set additionalProperties(s){this.set(\"additionalProperties\",s)}get additionalItems(){return this.get(\"additionalItems\")}set additionalItems(s){this.set(\"additionalItems\",s)}get propertyNames(){return this.get(\"propertyNames\")}set propertyNames(s){this.set(\"propertyNames\",s)}get unevaluatedItems(){return this.get(\"unevaluatedItems\")}set unevaluatedItems(s){this.set(\"unevaluatedItems\",s)}get unevaluatedProperties(){return this.get(\"unevaluatedProperties\")}set unevaluatedProperties(s){this.set(\"unevaluatedProperties\",s)}get maxContains(){return this.get(\"maxContains\")}set maxContains(s){this.set(\"maxContains\",s)}get minContains(){return this.get(\"minContains\")}set minContains(s){this.set(\"minContains\",s)}get dependentRequired(){return this.get(\"dependentRequired\")}set dependentRequired(s){this.set(\"dependentRequired\",s)}get deprecated(){return this.get(\"deprecated\")}set deprecated(s){this.set(\"deprecated\",s)}get contentSchema(){return this.get(\"contentSchema\")}set contentSchema(s){this.set(\"contentSchema\",s)}};const T_=class src_elements_LinkDescription_LinkDescription extends E_{get targetSchema(){return this.get(\"targetSchema\")}set targetSchema(s){this.set(\"targetSchema\",s)}get hrefSchema(){return this.get(\"hrefSchema\")}set hrefSchema(s){this.set(\"hrefSchema\",s)}get headerSchema(){return this.get(\"headerSchema\")}set headerSchema(s){this.set(\"headerSchema\",s)}get submissionSchema(){return this.get(\"submissionSchema\")}set submissionSchema(s){this.set(\"submissionSchema\",s)}};const N_=class refractor_visitors_json_schema_JSONSchemaVisitor extends w_{constructor(s){super(s),this.element=new I_}get defaultDialectIdentifier(){return\"https://json-schema.org/draft/2019-09/schema\"}ObjectElement(s){this.handleDialectIdentifier(s),this.handleSchemaIdentifier(s),this.parent=this.element;const o=Md.prototype.ObjectElement.call(this,s);return ju(this.element.$ref)&&(this.element.classes.push(\"reference-element\"),this.element.setMetaProperty(\"referenced-element\",\"schema\")),o}};const M_=class $vocabularyVisitor extends yd{ObjectElement(s){const o=super.enter(s);return this.element.classes.push(\"json-schema-$vocabulary\"),o}};const R_=class $refVisitor extends yd{StringElement(s){const o=super.enter(s);return this.element.classes.push(\"reference-value\"),o}};class $defsVisitor extends(Mixin(Jd,Rd,yd)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"json-schema-$defs\"),this.specPath=fc([\"document\",\"objects\",\"JSONSchema\"])}}const D_=$defsVisitor;class json_schema_AllOfVisitor_AllOfVisitor extends(Mixin(Nd,Rd,yd)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"json-schema-allOf\")}ArrayElement(s){return s.forEach((s=>{const o=this.toRefractedElement([\"document\",\"objects\",\"JSONSchema\"],s);this.element.push(o)})),this.copyMetaAndAttributes(s,this.element),qu}}const L_=json_schema_AllOfVisitor_AllOfVisitor;class json_schema_AnyOfVisitor_AnyOfVisitor extends(Mixin(Nd,Rd,yd)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"json-schema-anyOf\")}ArrayElement(s){return s.forEach((s=>{const o=this.toRefractedElement([\"document\",\"objects\",\"JSONSchema\"],s);this.element.push(o)})),this.copyMetaAndAttributes(s,this.element),qu}}const F_=json_schema_AnyOfVisitor_AnyOfVisitor;class json_schema_OneOfVisitor_OneOfVisitor extends(Mixin(Nd,Rd,yd)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"json-schema-oneOf\")}ArrayElement(s){return s.forEach((s=>{const o=this.toRefractedElement([\"document\",\"objects\",\"JSONSchema\"],s);this.element.push(o)})),this.copyMetaAndAttributes(s,this.element),qu}}const B_=json_schema_OneOfVisitor_OneOfVisitor;class DependentSchemasVisitor extends(Mixin(Jd,Rd,yd)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"json-schema-dependentSchemas\"),this.specPath=fc([\"document\",\"objects\",\"JSONSchema\"])}}const $_=DependentSchemasVisitor;class visitors_json_schema_ItemsVisitor_ItemsVisitor extends(Mixin(Nd,Rd,yd)){ObjectElement(s){return this.element=this.toRefractedElement([\"document\",\"objects\",\"JSONSchema\"],s),qu}ArrayElement(s){return this.element=new Su.wE,this.element.classes.push(\"json-schema-items\"),s.forEach((s=>{const o=this.toRefractedElement([\"document\",\"objects\",\"JSONSchema\"],s);this.element.push(o)})),this.copyMetaAndAttributes(s,this.element),qu}BooleanElement(s){return this.element=this.toRefractedElement([\"document\",\"objects\",\"JSONSchema\"],s),qu}}const q_=visitors_json_schema_ItemsVisitor_ItemsVisitor;class json_schema_PropertiesVisitor_PropertiesVisitor extends(Mixin(Jd,Rd,yd)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"json-schema-properties\"),this.specPath=fc([\"document\",\"objects\",\"JSONSchema\"])}}const U_=json_schema_PropertiesVisitor_PropertiesVisitor;class PatternPropertiesVisitor_PatternPropertiesVisitor extends(Mixin(Jd,Rd,yd)){constructor(s){super(s),this.element=new Su.Sh,this.element.classes.push(\"json-schema-patternProperties\"),this.specPath=fc([\"document\",\"objects\",\"JSONSchema\"])}}const V_=PatternPropertiesVisitor_PatternPropertiesVisitor;const z_=class DependentRequiredVisitor extends yd{ObjectElement(s){const o=super.enter(s);return this.element.classes.push(\"json-schema-dependentRequired\"),o}};const W_=class visitors_json_schema_link_description_LinkDescriptionVisitor extends x_{constructor(s){super(s),this.element=new T_}},J_=pipe(n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$vocabulary\"],M_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$anchor\"],k_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$recursiveAnchor\"],k_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$recursiveRef\"],k_.visitors.value),l_([\"visitors\",\"document\",\"objects\",\"JSONReference\",\"$visitor\"]),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$ref\"],R_),l_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"definitions\"]),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$defs\"],D_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"allOf\"],L_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"anyOf\"],F_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"oneOf\"],B_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"not\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"if\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"then\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"else\"],N_),l_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"dependencies\"]),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"dependentSchemas\"],$_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"items\"],q_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"contains\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"properties\"],U_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"patternProperties\"],V_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"additionalProperties\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"additionalItems\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"propertyNames\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"unevaluatedItems\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"unevaluatedProperties\"],N_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"maxContains\"],k_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"minContains\"],k_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"dependentRequired\"],z_),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"deprecated\"],k_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"contentSchema\"],N_),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"],W_),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"targetSchema\"],N_),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"hrefSchema\"],N_),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"headerSchema\"],N_),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"submissionSchema\"],N_))(k_),H_={JSONSchema201909Element:[\"content\"],LinkDescriptionElement:[\"content\"],...np},K_=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof I_||s(a)&&o(\"JSONSchema201909\",a)&&i(\"object\",a))),G_=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof T_||s(a)&&o(\"linkDescription\",a)&&i(\"object\",a))),Y_={namespace:s=>{const{base:o}=s;return o.register(\"jSONSchema201909\",I_),o.register(\"linkDescription\",T_),o}},X_=Y_,apidom_ns_json_schema_2019_09_src_refractor_toolbox=()=>{const s=createNamespace(X_);return{predicates:{...de,isStringElement:ju},namespace:s}},apidom_ns_json_schema_2019_09_src_refractor_refract=(s,{specPath:o=[\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],plugins:i=[],specificationObj:a=J_}={})=>{const u=(0,Su.e)(s),_=dereference(a),w=new(Qu(o,_))({specObj:_});return visitor_visit(u,w),dispatchPluginsSync(w.element,i,{toolboxCreator:apidom_ns_json_schema_2019_09_src_refractor_toolbox,visitorOptions:{keyMap:H_,nodeTypeGetter:traversal_visitor_getNodeType}})},apidom_ns_json_schema_2019_09_src_refractor_createRefractor=s=>(o,i={})=>apidom_ns_json_schema_2019_09_src_refractor_refract(o,{specPath:s,...i});I_.refract=apidom_ns_json_schema_2019_09_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"]),T_.refract=apidom_ns_json_schema_2019_09_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"]);const Q_=class apidom_ns_json_schema_2020_12_src_elements_JSONSchema_JSONSchema extends I_{constructor(s,o,i){super(s,o,i),this.element=\"JSONSchema202012\"}get $dynamicAnchor(){return this.get(\"$dynamicAnchor\")}set $dynamicAnchor(s){this.set(\"$dynamicAnchor\",s)}get $recursiveAnchor(){throw new td(\"$recursiveAnchor keyword from Core vocabulary has been renamed to $dynamicAnchor.\")}set $recursiveAnchor(s){throw new td(\"$recursiveAnchor keyword from Core vocabulary has been renamed to $dynamicAnchor.\")}get $dynamicRef(){return this.get(\"$dynamicRef\")}set $dynamicRef(s){this.set(\"$dynamicRef\",s)}get $recursiveRef(){throw new td(\"$recursiveRef keyword from Core vocabulary has been renamed to $dynamicRef.\")}set $recursiveRef(s){throw new td(\"$recursiveRef keyword from Core vocabulary has been renamed to $dynamicRef.\")}get prefixItems(){return this.get(\"prefixItems\")}set prefixItems(s){this.set(\"prefixItems\",s)}};const Z_=class apidom_ns_json_schema_2020_12_src_elements_LinkDescription_LinkDescription extends T_{get targetSchema(){return this.get(\"targetSchema\")}set targetSchema(s){this.set(\"targetSchema\",s)}get hrefSchema(){return this.get(\"hrefSchema\")}set hrefSchema(s){this.set(\"hrefSchema\",s)}get headerSchema(){return this.get(\"headerSchema\")}set headerSchema(s){this.set(\"headerSchema\",s)}get submissionSchema(){return this.get(\"submissionSchema\")}set submissionSchema(s){this.set(\"submissionSchema\",s)}};const eS=class src_refractor_visitors_json_schema_JSONSchemaVisitor extends N_{constructor(s){super(s),this.element=new Q_}get defaultDialectIdentifier(){return\"https://json-schema.org/draft/2020-12/schema\"}};class PrefixItemsVisitor extends(Mixin(Nd,Rd,yd)){constructor(s){super(s),this.element=new Su.wE,this.element.classes.push(\"json-schema-prefixItems\")}ArrayElement(s){return s.forEach((s=>{const o=this.toRefractedElement([\"document\",\"objects\",\"JSONSchema\"],s);this.element.push(o)})),this.copyMetaAndAttributes(s,this.element),qu}}const tS=PrefixItemsVisitor;const rS=class refractor_visitors_json_schema_link_description_LinkDescriptionVisitor extends W_{constructor(s){super(s),this.element=new Z_}},nS=pipe(n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],eS),l_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$recursiveAnchor\"]),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$dynamicAnchor\"],J_.visitors.value),l_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$recursiveRef\"]),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"$dynamicRef\"],J_.visitors.value),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"not\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"if\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"then\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"else\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"prefixItems\"],tS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"items\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"contains\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"additionalProperties\"],eS),l_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"additionalItems\"]),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"propertyNames\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"unevaluatedItems\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"unevaluatedProperties\"],eS),n_([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"fixedFields\",\"contentSchema\"],eS),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"],rS),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"targetSchema\"],eS),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"hrefSchema\"],eS),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"headerSchema\"],eS),n_([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"fixedFields\",\"submissionSchema\"],eS))(J_),sS={JSONSchema202012Element:[\"content\"],LinkDescriptionElement:[\"content\"],...np},oS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Q_||s(a)&&o(\"JSONSchema202012\",a)&&i(\"object\",a))),iS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Z_||s(a)&&o(\"linkDescription\",a)&&i(\"object\",a))),aS={namespace:s=>{const{base:o}=s;return o.register(\"jSONSchema202012\",Q_),o.register(\"linkDescription\",Z_),o}},cS=aS,apidom_ns_json_schema_2020_12_src_refractor_toolbox=()=>{const s=createNamespace(cS);return{predicates:{...fe,isStringElement:ju},namespace:s}},apidom_ns_json_schema_2020_12_src_refractor_refract=(s,{specPath:o=[\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"],plugins:i=[],specificationObj:a=nS}={})=>{const u=(0,Su.e)(s),_=dereference(a),w=new(Qu(o,_))({specObj:_});return visitor_visit(u,w),dispatchPluginsSync(w.element,i,{toolboxCreator:apidom_ns_json_schema_2020_12_src_refractor_toolbox,visitorOptions:{keyMap:sS,nodeTypeGetter:traversal_visitor_getNodeType}})},apidom_ns_json_schema_2020_12_src_refractor_createRefractor=s=>(o,i={})=>apidom_ns_json_schema_2020_12_src_refractor_refract(o,{specPath:s,...i});Q_.refract=apidom_ns_json_schema_2020_12_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"JSONSchema\",\"$visitor\"]),Z_.refract=apidom_ns_json_schema_2020_12_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"LinkDescription\",\"$visitor\"]);const lS=class elements_Schema_Schema extends Q_{constructor(s,o,i){super(s,o,i),this.element=\"schema\"}get discriminator(){return this.get(\"discriminator\")}set discriminator(s){this.set(\"discriminator\",s)}get xml(){return this.get(\"xml\")}set xml(s){this.set(\"xml\",s)}get externalDocs(){return this.get(\"externalDocs\")}set externalDocs(s){this.set(\"externalDocs\",s)}get example(){return this.get(\"example\")}set example(s){this.set(\"example\",s)}};const uS=class SecurityRequirement_SecurityRequirement extends Vf{};const pS=class SecurityScheme_SecurityScheme extends Wf{};const hS=class Server_Server extends Jf{};const dS=class ServerVariable_ServerVariable extends Hf{};const fS=class Tag_Tag extends Gf{};const mS=class Xml_Xml extends Xf{};class OpenApi3_1Visitor extends(Mixin(cm,em)){constructor(s){super(s),this.element=new Ab,this.specPath=fc([\"document\",\"objects\",\"OpenApi\"]),this.canSupportSpecificationExtensions=!0,this.openApiSemanticElement=this.element}ObjectElement(s){return this.openApiGenericElement=s,cm.prototype.ObjectElement.call(this,s)}}const gS=OpenApi3_1Visitor,yS=zv.visitors.document.objects.Info.$visitor;const vS=class info_InfoVisitor extends yS{constructor(s){super(s),this.element=new nb}},bS=zv.visitors.document.objects.Contact.$visitor;const _S=class contact_ContactVisitor extends bS{constructor(s){super(s),this.element=new Yv}},SS=zv.visitors.document.objects.License.$visitor;const ES=class license_LicenseVisitor extends SS{constructor(s){super(s),this.element=new mb}},wS=zv.visitors.document.objects.Link.$visitor;const xS=class link_LinkVisitor extends wS{constructor(s){super(s),this.element=new yb}};class JsonSchemaDialectVisitor extends(Mixin(tm,em)){StringElement(s){const o=new pb(serializers_value(s));return this.copyMetaAndAttributes(s,o),this.element=o,qu}}const kS=JsonSchemaDialectVisitor,OS=zv.visitors.document.objects.Server.$visitor;const AS=class server_ServerVisitor extends OS{constructor(s){super(s),this.element=new hS}},CS=zv.visitors.document.objects.ServerVariable.$visitor;const jS=class server_variable_ServerVariableVisitor extends CS{constructor(s){super(s),this.element=new dS}},PS=zv.visitors.document.objects.MediaType.$visitor;const IS=class media_type_MediaTypeVisitor extends PS{constructor(s){super(s),this.element=new _b}},TS=zv.visitors.document.objects.SecurityRequirement.$visitor;const NS=class security_requirement_SecurityRequirementVisitor extends TS{constructor(s){super(s),this.element=new uS}},MS=zv.visitors.document.objects.Components.$visitor;const RS=class components_ComponentsVisitor extends MS{constructor(s){super(s),this.element=new Gv}},DS=zv.visitors.document.objects.Tag.$visitor;const LS=class tag_TagVisitor extends DS{constructor(s){super(s),this.element=new fS}},FS=zv.visitors.document.objects.Reference.$visitor;const BS=class reference_ReferenceVisitor extends FS{constructor(s){super(s),this.element=new Lb}},$S=zv.visitors.document.objects.Parameter.$visitor;const qS=class parameter_ParameterVisitor extends $S{constructor(s){super(s),this.element=new Ib}},US=zv.visitors.document.objects.Header.$visitor;const VS=class header_HeaderVisitor extends US{constructor(s){super(s),this.element=new tb}},zS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Kv||s(a)&&o(\"callback\",a)&&i(\"object\",a))),WS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Gv||s(a)&&o(\"components\",a)&&i(\"object\",a))),JS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Yv||s(a)&&o(\"contact\",a)&&i(\"object\",a))),HS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Zv||s(a)&&o(\"example\",a)&&i(\"object\",a))),KS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof eb||s(a)&&o(\"externalDocumentation\",a)&&i(\"object\",a))),GS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof tb||s(a)&&o(\"header\",a)&&i(\"object\",a))),YS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof nb||s(a)&&o(\"info\",a)&&i(\"object\",a))),XS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof pb||s(a)&&o(\"jsonSchemaDialect\",a)&&i(\"string\",a))),QS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof mb||s(a)&&o(\"license\",a)&&i(\"object\",a))),ZS=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof yb||s(a)&&o(\"link\",a)&&i(\"object\",a))),eE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Ob||s(a)&&o(\"openapi\",a)&&i(\"string\",a))),tE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i,hasClass:a})=>u=>u instanceof Ab||s(u)&&o(\"openApi3_1\",u)&&i(\"object\",u)&&a(\"api\",u))),rE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Pb||s(a)&&o(\"operation\",a)&&i(\"object\",a))),nE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Ib||s(a)&&o(\"parameter\",a)&&i(\"object\",a))),sE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Mb||s(a)&&o(\"pathItem\",a)&&i(\"object\",a))),isPathItemElementExternal=s=>{if(!sE(s))return!1;if(!ju(s.$ref))return!1;const o=serializers_value(s.$ref);return\"string\"==typeof o&&o.length>0&&!o.startsWith(\"#\")},oE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Rb||s(a)&&o(\"paths\",a)&&i(\"object\",a))),iE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Lb||s(a)&&o(\"reference\",a)&&i(\"object\",a))),isReferenceElementExternal=s=>{if(!iE(s))return!1;if(!ju(s.$ref))return!1;const o=serializers_value(s.$ref);return\"string\"==typeof o&&o.length>0&&!o.startsWith(\"#\")},aE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof qb||s(a)&&o(\"requestBody\",a)&&i(\"object\",a))),cE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof zb||s(a)&&o(\"response\",a)&&i(\"object\",a))),lE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof Qb||s(a)&&o(\"responses\",a)&&i(\"object\",a))),uE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof lS||s(a)&&o(\"schema\",a)&&i(\"object\",a))),predicates_isBooleanJsonSchemaElement=s=>Tu(s)&&s.classes.includes(\"boolean-json-schema\"),pE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof uS||s(a)&&o(\"securityRequirement\",a)&&i(\"object\",a))),hE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof pS||s(a)&&o(\"securityScheme\",a)&&i(\"object\",a))),dE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof hS||s(a)&&o(\"server\",a)&&i(\"object\",a))),fE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof dS||s(a)&&o(\"serverVariable\",a)&&i(\"object\",a))),mE=helpers((({hasBasicElementProps:s,isElementType:o,primitiveEq:i})=>a=>a instanceof _b||s(a)&&o(\"mediaType\",a)&&i(\"object\",a)));class open_api_3_1_schema_SchemaVisitor extends(Mixin(cm,Rd,em)){constructor(s){super(s),this.element=new lS,this.specPath=fc([\"document\",\"objects\",\"Schema\"]),this.canSupportSpecificationExtensions=!0,this.jsonSchemaDefaultDialect=pb.default,this.passingOptionsNames.push(\"parent\")}ObjectElement(s){this.handleDialectIdentifier(s),this.handleSchemaIdentifier(s),this.parent=this.element;const o=cm.prototype.ObjectElement.call(this,s);return ju(this.element.$ref)&&(this.element.classes.push(\"reference-element\"),this.element.setMetaProperty(\"referenced-element\",\"schema\")),o}BooleanElement(s){return eS.prototype.BooleanElement.call(this,s)}get defaultDialectIdentifier(){let s;return s=void 0!==this.openApiSemanticElement&&XS(this.openApiSemanticElement.jsonSchemaDialect)?serializers_value(this.openApiSemanticElement.jsonSchemaDialect):void 0!==this.openApiGenericElement&&ju(this.openApiGenericElement.get(\"jsonSchemaDialect\"))?serializers_value(this.openApiGenericElement.get(\"jsonSchemaDialect\")):serializers_value(this.jsonSchemaDefaultDialect),s}handleDialectIdentifier(s){return eS.prototype.handleDialectIdentifier.call(this,s)}handleSchemaIdentifier(s){return eS.prototype.handleSchemaIdentifier.call(this,s)}}const gE=open_api_3_1_schema_SchemaVisitor;const yE=class $defsVisitor_$defsVisitor extends D_{constructor(s){super(s),this.passingOptionsNames.push(\"parent\")}};const vE=class schema_AllOfVisitor_AllOfVisitor extends L_{constructor(s){super(s),this.passingOptionsNames.push(\"parent\")}};const bE=class schema_AnyOfVisitor_AnyOfVisitor extends F_{constructor(s){super(s),this.passingOptionsNames.push(\"parent\")}};const _E=class schema_OneOfVisitor_OneOfVisitor extends B_{constructor(s){super(s),this.passingOptionsNames.push(\"parent\")}};const SE=class DependentSchemasVisitor_DependentSchemasVisitor extends $_{constructor(s){super(s),this.passingOptionsNames.push(\"parent\")}};const EE=class PrefixItemsVisitor_PrefixItemsVisitor extends tS{constructor(s){super(s),this.passingOptionsNames.push(\"parent\")}};const wE=class schema_PropertiesVisitor_PropertiesVisitor extends U_{constructor(s){super(s),this.passingOptionsNames.push(\"parent\")}};const xE=class schema_PatternPropertiesVisitor_PatternPropertiesVisitor extends V_{constructor(s){super(s),this.passingOptionsNames.push(\"parent\")}},kE=zv.visitors.document.objects.Discriminator.$visitor;const OE=class distriminator_DiscriminatorVisitor extends kE{constructor(s){super(s),this.element=new Xv,this.canSupportSpecificationExtensions=!0}},AE=zv.visitors.document.objects.XML.$visitor;const CE=class xml_XmlVisitor extends AE{constructor(s){super(s),this.element=new mS}};class SchemasVisitor_SchemasVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new Ay,this.specPath=fc([\"document\",\"objects\",\"Schema\"])}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(uE).forEach(((s,o)=>{s.setMetaProperty(\"schemaName\",serializers_value(o))})),o}}const jE=SchemasVisitor_SchemasVisitor;class ComponentsPathItems extends Su.Sh{static primaryClass=\"components-path-items\";constructor(s,o,i){super(s,o,i),this.classes.push(ComponentsPathItems.primaryClass)}}const PE=ComponentsPathItems;class PathItemsVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new PE,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"PathItem\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(iE).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"pathItem\")})),o}}const IE=PathItemsVisitor,TE=zv.visitors.document.objects.Example.$visitor;const NE=class example_ExampleVisitor extends TE{constructor(s){super(s),this.element=new Zv}},ME=zv.visitors.document.objects.ExternalDocumentation.$visitor;const RE=class external_documentation_ExternalDocumentationVisitor extends ME{constructor(s){super(s),this.element=new eb}},DE=zv.visitors.document.objects.Encoding.$visitor;const LE=class open_api_3_1_encoding_EncodingVisitor extends DE{constructor(s){super(s),this.element=new Qv}},FE=zv.visitors.document.objects.Paths.$visitor;const BE=class paths_PathsVisitor extends FE{constructor(s){super(s),this.element=new Rb}},$E=zv.visitors.document.objects.RequestBody.$visitor;const qE=class request_body_RequestBodyVisitor extends $E{constructor(s){super(s),this.element=new qb}},UE=zv.visitors.document.objects.Callback.$visitor;const VE=class callback_CallbackVisitor extends UE{constructor(s){super(s),this.element=new Kv,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"PathItem\"]}ObjectElement(s){const o=UE.prototype.ObjectElement.call(this,s);return this.element.filter(iE).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"pathItem\")})),o}},zE=zv.visitors.document.objects.Response.$visitor;const WE=class response_ResponseVisitor extends zE{constructor(s){super(s),this.element=new zb}},JE=zv.visitors.document.objects.Responses.$visitor;const HE=class open_api_3_1_responses_ResponsesVisitor extends JE{constructor(s){super(s),this.element=new Qb}},KE=zv.visitors.document.objects.Operation.$visitor;const GE=class operation_OperationVisitor extends KE{constructor(s){super(s),this.element=new Pb}},YE=zv.visitors.document.objects.PathItem.$visitor;const XE=class path_item_PathItemVisitor extends YE{constructor(s){super(s),this.element=new Mb}},QE=zv.visitors.document.objects.SecurityScheme.$visitor;const ZE=class security_scheme_SecuritySchemeVisitor extends QE{constructor(s){super(s),this.element=new pS}},ew=zv.visitors.document.objects.OAuthFlows.$visitor;const tw=class oauth_flows_OAuthFlowsVisitor extends ew{constructor(s){super(s),this.element=new wb}},rw=zv.visitors.document.objects.OAuthFlow.$visitor;const nw=class oauth_flow_OAuthFlowVisitor extends rw{constructor(s){super(s),this.element=new Sb}};class Webhooks extends Su.Sh{static primaryClass=\"webhooks\";constructor(s,o,i){super(s,o,i),this.classes.push(Webhooks.primaryClass)}}const sw=Webhooks;class WebhooksVisitor extends(Mixin(_m,em)){constructor(s){super(s),this.element=new sw,this.specPath=s=>isReferenceLikeElement(s)?[\"document\",\"objects\",\"Reference\"]:[\"document\",\"objects\",\"PathItem\"]}ObjectElement(s){const o=_m.prototype.ObjectElement.call(this,s);return this.element.filter(iE).forEach((s=>{s.setMetaProperty(\"referenced-element\",\"pathItem\")})),this.element.filter(sE).forEach(((s,o)=>{s.setMetaProperty(\"webhook-name\",serializers_value(o))})),o}}const ow=WebhooksVisitor,{JSONSchema:iw,LinkDescription:aw}=nS.visitors.document.objects,cw={visitors:{value:zv.visitors.value,document:{objects:{OpenApi:{$visitor:gS,fixedFields:{openapi:zv.visitors.document.objects.OpenApi.fixedFields.openapi,info:{$ref:\"#/visitors/document/objects/Info\"},jsonSchemaDialect:kS,servers:zv.visitors.document.objects.OpenApi.fixedFields.servers,paths:{$ref:\"#/visitors/document/objects/Paths\"},webhooks:ow,components:{$ref:\"#/visitors/document/objects/Components\"},security:zv.visitors.document.objects.OpenApi.fixedFields.security,tags:zv.visitors.document.objects.OpenApi.fixedFields.tags,externalDocs:{$ref:\"#/visitors/document/objects/ExternalDocumentation\"}}},Info:{$visitor:vS,fixedFields:{title:zv.visitors.document.objects.Info.fixedFields.title,description:zv.visitors.document.objects.Info.fixedFields.description,summary:{$ref:\"#/visitors/value\"},termsOfService:zv.visitors.document.objects.Info.fixedFields.termsOfService,contact:{$ref:\"#/visitors/document/objects/Contact\"},license:{$ref:\"#/visitors/document/objects/License\"},version:zv.visitors.document.objects.Info.fixedFields.version}},Contact:{$visitor:_S,fixedFields:{name:zv.visitors.document.objects.Contact.fixedFields.name,url:zv.visitors.document.objects.Contact.fixedFields.url,email:zv.visitors.document.objects.Contact.fixedFields.email}},License:{$visitor:ES,fixedFields:{name:zv.visitors.document.objects.License.fixedFields.name,identifier:{$ref:\"#/visitors/value\"},url:zv.visitors.document.objects.License.fixedFields.url}},Server:{$visitor:AS,fixedFields:{url:zv.visitors.document.objects.Server.fixedFields.url,description:zv.visitors.document.objects.Server.fixedFields.description,variables:zv.visitors.document.objects.Server.fixedFields.variables}},ServerVariable:{$visitor:jS,fixedFields:{enum:zv.visitors.document.objects.ServerVariable.fixedFields.enum,default:zv.visitors.document.objects.ServerVariable.fixedFields.default,description:zv.visitors.document.objects.ServerVariable.fixedFields.description}},Components:{$visitor:RS,fixedFields:{schemas:jE,responses:zv.visitors.document.objects.Components.fixedFields.responses,parameters:zv.visitors.document.objects.Components.fixedFields.parameters,examples:zv.visitors.document.objects.Components.fixedFields.examples,requestBodies:zv.visitors.document.objects.Components.fixedFields.requestBodies,headers:zv.visitors.document.objects.Components.fixedFields.headers,securitySchemes:zv.visitors.document.objects.Components.fixedFields.securitySchemes,links:zv.visitors.document.objects.Components.fixedFields.links,callbacks:zv.visitors.document.objects.Components.fixedFields.callbacks,pathItems:IE}},Paths:{$visitor:BE},PathItem:{$visitor:XE,fixedFields:{$ref:zv.visitors.document.objects.PathItem.fixedFields.$ref,summary:zv.visitors.document.objects.PathItem.fixedFields.summary,description:zv.visitors.document.objects.PathItem.fixedFields.description,get:{$ref:\"#/visitors/document/objects/Operation\"},put:{$ref:\"#/visitors/document/objects/Operation\"},post:{$ref:\"#/visitors/document/objects/Operation\"},delete:{$ref:\"#/visitors/document/objects/Operation\"},options:{$ref:\"#/visitors/document/objects/Operation\"},head:{$ref:\"#/visitors/document/objects/Operation\"},patch:{$ref:\"#/visitors/document/objects/Operation\"},trace:{$ref:\"#/visitors/document/objects/Operation\"},servers:zv.visitors.document.objects.PathItem.fixedFields.servers,parameters:zv.visitors.document.objects.PathItem.fixedFields.parameters}},Operation:{$visitor:GE,fixedFields:{tags:zv.visitors.document.objects.Operation.fixedFields.tags,summary:zv.visitors.document.objects.Operation.fixedFields.summary,description:zv.visitors.document.objects.Operation.fixedFields.description,externalDocs:{$ref:\"#/visitors/document/objects/ExternalDocumentation\"},operationId:zv.visitors.document.objects.Operation.fixedFields.operationId,parameters:zv.visitors.document.objects.Operation.fixedFields.parameters,requestBody:zv.visitors.document.objects.Operation.fixedFields.requestBody,responses:{$ref:\"#/visitors/document/objects/Responses\"},callbacks:zv.visitors.document.objects.Operation.fixedFields.callbacks,deprecated:zv.visitors.document.objects.Operation.fixedFields.deprecated,security:zv.visitors.document.objects.Operation.fixedFields.security,servers:zv.visitors.document.objects.Operation.fixedFields.servers}},ExternalDocumentation:{$visitor:RE,fixedFields:{description:zv.visitors.document.objects.ExternalDocumentation.fixedFields.description,url:zv.visitors.document.objects.ExternalDocumentation.fixedFields.url}},Parameter:{$visitor:qS,fixedFields:{name:zv.visitors.document.objects.Parameter.fixedFields.name,in:zv.visitors.document.objects.Parameter.fixedFields.in,description:zv.visitors.document.objects.Parameter.fixedFields.description,required:zv.visitors.document.objects.Parameter.fixedFields.required,deprecated:zv.visitors.document.objects.Parameter.fixedFields.deprecated,allowEmptyValue:zv.visitors.document.objects.Parameter.fixedFields.allowEmptyValue,style:zv.visitors.document.objects.Parameter.fixedFields.style,explode:zv.visitors.document.objects.Parameter.fixedFields.explode,allowReserved:zv.visitors.document.objects.Parameter.fixedFields.allowReserved,schema:{$ref:\"#/visitors/document/objects/Schema\"},example:zv.visitors.document.objects.Parameter.fixedFields.example,examples:zv.visitors.document.objects.Parameter.fixedFields.examples,content:zv.visitors.document.objects.Parameter.fixedFields.content}},RequestBody:{$visitor:qE,fixedFields:{description:zv.visitors.document.objects.RequestBody.fixedFields.description,content:zv.visitors.document.objects.RequestBody.fixedFields.content,required:zv.visitors.document.objects.RequestBody.fixedFields.required}},MediaType:{$visitor:IS,fixedFields:{schema:{$ref:\"#/visitors/document/objects/Schema\"},example:zv.visitors.document.objects.MediaType.fixedFields.example,examples:zv.visitors.document.objects.MediaType.fixedFields.examples,encoding:zv.visitors.document.objects.MediaType.fixedFields.encoding}},Encoding:{$visitor:LE,fixedFields:{contentType:zv.visitors.document.objects.Encoding.fixedFields.contentType,headers:zv.visitors.document.objects.Encoding.fixedFields.headers,style:zv.visitors.document.objects.Encoding.fixedFields.style,explode:zv.visitors.document.objects.Encoding.fixedFields.explode,allowReserved:zv.visitors.document.objects.Encoding.fixedFields.allowReserved}},Responses:{$visitor:HE,fixedFields:{default:zv.visitors.document.objects.Responses.fixedFields.default}},Response:{$visitor:WE,fixedFields:{description:zv.visitors.document.objects.Response.fixedFields.description,headers:zv.visitors.document.objects.Response.fixedFields.headers,content:zv.visitors.document.objects.Response.fixedFields.content,links:zv.visitors.document.objects.Response.fixedFields.links}},Callback:{$visitor:VE},Example:{$visitor:NE,fixedFields:{summary:zv.visitors.document.objects.Example.fixedFields.summary,description:zv.visitors.document.objects.Example.fixedFields.description,value:zv.visitors.document.objects.Example.fixedFields.value,externalValue:zv.visitors.document.objects.Example.fixedFields.externalValue}},Link:{$visitor:xS,fixedFields:{operationRef:zv.visitors.document.objects.Link.fixedFields.operationRef,operationId:zv.visitors.document.objects.Link.fixedFields.operationId,parameters:zv.visitors.document.objects.Link.fixedFields.parameters,requestBody:zv.visitors.document.objects.Link.fixedFields.requestBody,description:zv.visitors.document.objects.Link.fixedFields.description,server:{$ref:\"#/visitors/document/objects/Server\"}}},Header:{$visitor:VS,fixedFields:{description:zv.visitors.document.objects.Header.fixedFields.description,required:zv.visitors.document.objects.Header.fixedFields.required,deprecated:zv.visitors.document.objects.Header.fixedFields.deprecated,allowEmptyValue:zv.visitors.document.objects.Header.fixedFields.allowEmptyValue,style:zv.visitors.document.objects.Header.fixedFields.style,explode:zv.visitors.document.objects.Header.fixedFields.explode,allowReserved:zv.visitors.document.objects.Header.fixedFields.allowReserved,schema:{$ref:\"#/visitors/document/objects/Schema\"},example:zv.visitors.document.objects.Header.fixedFields.example,examples:zv.visitors.document.objects.Header.fixedFields.examples,content:zv.visitors.document.objects.Header.fixedFields.content}},Tag:{$visitor:LS,fixedFields:{name:zv.visitors.document.objects.Tag.fixedFields.name,description:zv.visitors.document.objects.Tag.fixedFields.description,externalDocs:{$ref:\"#/visitors/document/objects/ExternalDocumentation\"}}},Reference:{$visitor:BS,fixedFields:{$ref:zv.visitors.document.objects.Reference.fixedFields.$ref,summary:{$ref:\"#/visitors/value\"},description:{$ref:\"#/visitors/value\"}}},JSONSchema:{$ref:\"#/visitors/document/objects/Schema\"},LinkDescription:{...aw},Schema:{$visitor:gE,fixedFields:{...iw.fixedFields,$defs:yE,allOf:vE,anyOf:bE,oneOf:_E,not:{$ref:\"#/visitors/document/objects/Schema\"},if:{$ref:\"#/visitors/document/objects/Schema\"},then:{$ref:\"#/visitors/document/objects/Schema\"},else:{$ref:\"#/visitors/document/objects/Schema\"},dependentSchemas:SE,prefixItems:EE,items:{$ref:\"#/visitors/document/objects/Schema\"},contains:{$ref:\"#/visitors/document/objects/Schema\"},properties:wE,patternProperties:xE,additionalProperties:{$ref:\"#/visitors/document/objects/Schema\"},propertyNames:{$ref:\"#/visitors/document/objects/Schema\"},unevaluatedItems:{$ref:\"#/visitors/document/objects/Schema\"},unevaluatedProperties:{$ref:\"#/visitors/document/objects/Schema\"},contentSchema:{$ref:\"#/visitors/document/objects/Schema\"},discriminator:{$ref:\"#/visitors/document/objects/Discriminator\"},xml:{$ref:\"#/visitors/document/objects/XML\"},externalDocs:{$ref:\"#/visitors/document/objects/ExternalDocumentation\"},example:{$ref:\"#/visitors/value\"}}},Discriminator:{$visitor:OE,fixedFields:{propertyName:zv.visitors.document.objects.Discriminator.fixedFields.propertyName,mapping:zv.visitors.document.objects.Discriminator.fixedFields.mapping}},XML:{$visitor:CE,fixedFields:{name:zv.visitors.document.objects.XML.fixedFields.name,namespace:zv.visitors.document.objects.XML.fixedFields.namespace,prefix:zv.visitors.document.objects.XML.fixedFields.prefix,attribute:zv.visitors.document.objects.XML.fixedFields.attribute,wrapped:zv.visitors.document.objects.XML.fixedFields.wrapped}},SecurityScheme:{$visitor:ZE,fixedFields:{type:zv.visitors.document.objects.SecurityScheme.fixedFields.type,description:zv.visitors.document.objects.SecurityScheme.fixedFields.description,name:zv.visitors.document.objects.SecurityScheme.fixedFields.name,in:zv.visitors.document.objects.SecurityScheme.fixedFields.in,scheme:zv.visitors.document.objects.SecurityScheme.fixedFields.scheme,bearerFormat:zv.visitors.document.objects.SecurityScheme.fixedFields.bearerFormat,flows:{$ref:\"#/visitors/document/objects/OAuthFlows\"},openIdConnectUrl:zv.visitors.document.objects.SecurityScheme.fixedFields.openIdConnectUrl}},OAuthFlows:{$visitor:tw,fixedFields:{implicit:{$ref:\"#/visitors/document/objects/OAuthFlow\"},password:{$ref:\"#/visitors/document/objects/OAuthFlow\"},clientCredentials:{$ref:\"#/visitors/document/objects/OAuthFlow\"},authorizationCode:{$ref:\"#/visitors/document/objects/OAuthFlow\"}}},OAuthFlow:{$visitor:nw,fixedFields:{authorizationUrl:zv.visitors.document.objects.OAuthFlow.fixedFields.authorizationUrl,tokenUrl:zv.visitors.document.objects.OAuthFlow.fixedFields.tokenUrl,refreshUrl:zv.visitors.document.objects.OAuthFlow.fixedFields.refreshUrl,scopes:zv.visitors.document.objects.OAuthFlow.fixedFields.scopes}},SecurityRequirement:{$visitor:NS}},extension:{$visitor:zv.visitors.document.extension.$visitor}}}},apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType=s=>{if(Cu(s))return`${s.element.charAt(0).toUpperCase()+s.element.slice(1)}Element`},lw={CallbackElement:[\"content\"],ComponentsElement:[\"content\"],ContactElement:[\"content\"],DiscriminatorElement:[\"content\"],Encoding:[\"content\"],Example:[\"content\"],ExternalDocumentationElement:[\"content\"],HeaderElement:[\"content\"],InfoElement:[\"content\"],LicenseElement:[\"content\"],MediaTypeElement:[\"content\"],OAuthFlowElement:[\"content\"],OAuthFlowsElement:[\"content\"],OpenApi3_1Element:[\"content\"],OperationElement:[\"content\"],ParameterElement:[\"content\"],PathItemElement:[\"content\"],PathsElement:[\"content\"],ReferenceElement:[\"content\"],RequestBodyElement:[\"content\"],ResponseElement:[\"content\"],ResponsesElement:[\"content\"],SchemaElement:[\"content\"],SecurityRequirementElement:[\"content\"],SecuritySchemeElement:[\"content\"],ServerElement:[\"content\"],ServerVariableElement:[\"content\"],TagElement:[\"content\"],...np},uw={namespace:s=>{const{base:o}=s;return o.register(\"callback\",Kv),o.register(\"components\",Gv),o.register(\"contact\",Yv),o.register(\"discriminator\",Xv),o.register(\"encoding\",Qv),o.register(\"example\",Zv),o.register(\"externalDocumentation\",eb),o.register(\"header\",tb),o.register(\"info\",nb),o.register(\"jsonSchemaDialect\",pb),o.register(\"license\",mb),o.register(\"link\",yb),o.register(\"mediaType\",_b),o.register(\"oAuthFlow\",Sb),o.register(\"oAuthFlows\",wb),o.register(\"openapi\",Ob),o.register(\"openApi3_1\",Ab),o.register(\"operation\",Pb),o.register(\"parameter\",Ib),o.register(\"pathItem\",Mb),o.register(\"paths\",Rb),o.register(\"reference\",Lb),o.register(\"requestBody\",qb),o.register(\"response\",zb),o.register(\"responses\",Qb),o.register(\"schema\",lS),o.register(\"securityRequirement\",uS),o.register(\"securityScheme\",pS),o.register(\"server\",hS),o.register(\"serverVariable\",dS),o.register(\"tag\",fS),o.register(\"xml\",mS),o}},pw=uw,ancestorLineageToJSONPointer=s=>{const o=s.reduce(((o,i,a)=>{if(Ru(i)){const s=String(serializers_value(i.key));o.push(s)}else if(Mu(s[a-2])){const u=String(s[a-2].content.indexOf(i));o.push(u)}return o}),[]);return es_compile(o)},apidom_ns_openapi_3_1_src_refractor_toolbox=()=>{const s=createNamespace(pw);return{predicates:{...ye,isElement:Cu,isStringElement:ju,isArrayElement:Mu,isObjectElement:Nu,isMemberElement:Ru,isServersElement:sg,includesClasses,hasElementSourceMap},ancestorLineageToJSONPointer,namespace:s}},apidom_ns_openapi_3_1_src_refractor_refract=(s,{specPath:o=[\"visitors\",\"document\",\"objects\",\"OpenApi\",\"$visitor\"],plugins:i=[]}={})=>{const a=(0,Su.e)(s),u=dereference(cw),_=new(Qu(o,u))({specObj:u});return visitor_visit(a,_),dispatchPluginsSync(_.element,i,{toolboxCreator:apidom_ns_openapi_3_1_src_refractor_toolbox,visitorOptions:{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType}})},apidom_ns_openapi_3_1_src_refractor_createRefractor=s=>(o,i={})=>apidom_ns_openapi_3_1_src_refractor_refract(o,{specPath:s,...i});Kv.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Callback\",\"$visitor\"]),Gv.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Components\",\"$visitor\"]),Yv.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Contact\",\"$visitor\"]),Zv.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Example\",\"$visitor\"]),Xv.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Discriminator\",\"$visitor\"]),Qv.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Encoding\",\"$visitor\"]),eb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"ExternalDocumentation\",\"$visitor\"]),tb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Header\",\"$visitor\"]),nb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Info\",\"$visitor\"]),pb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OpenApi\",\"fixedFields\",\"jsonSchemaDialect\"]),mb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"License\",\"$visitor\"]),yb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Link\",\"$visitor\"]),_b.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"MediaType\",\"$visitor\"]),Sb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OAuthFlow\",\"$visitor\"]),wb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OAuthFlows\",\"$visitor\"]),Ob.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OpenApi\",\"fixedFields\",\"openapi\"]),Ab.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"OpenApi\",\"$visitor\"]),Pb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Operation\",\"$visitor\"]),Ib.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Parameter\",\"$visitor\"]),Mb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"PathItem\",\"$visitor\"]),Rb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Paths\",\"$visitor\"]),Lb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Reference\",\"$visitor\"]),qb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"RequestBody\",\"$visitor\"]),zb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Response\",\"$visitor\"]),Qb.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Responses\",\"$visitor\"]),lS.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Schema\",\"$visitor\"]),uS.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"SecurityRequirement\",\"$visitor\"]),pS.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"SecurityScheme\",\"$visitor\"]),hS.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Server\",\"$visitor\"]),dS.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"ServerVariable\",\"$visitor\"]),fS.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"Tag\",\"$visitor\"]),mS.refract=apidom_ns_openapi_3_1_src_refractor_createRefractor([\"visitors\",\"document\",\"objects\",\"XML\",\"$visitor\"]);const hw=class NotImplementedError extends td{};const dw=class MediaTypes extends Array{unknownMediaType=\"application/octet-stream\";filterByFormat(){throw new hw(\"filterByFormat method in MediaTypes class is not yet implemented.\")}findBy(){throw new hw(\"findBy method in MediaTypes class is not yet implemented.\")}latest(){throw new hw(\"latest method in MediaTypes class is not yet implemented.\")}};class OpenAPIMediaTypes extends dw{filterByFormat(s=\"generic\"){const o=\"generic\"===s?\"openapi;version\":s;return this.filter((s=>s.includes(o)))}findBy(s=\"3.1.0\",o=\"generic\"){const i=\"generic\"===o?`vnd.oai.openapi;version=${s}`:`vnd.oai.openapi+${o};version=${s}`;return this.find((s=>s.includes(i)))||this.unknownMediaType}latest(s=\"generic\"){return Ba(this.filterByFormat(s))}}const fw=new OpenAPIMediaTypes(\"application/vnd.oai.openapi;version=3.1.0\",\"application/vnd.oai.openapi+json;version=3.1.0\",\"application/vnd.oai.openapi+yaml;version=3.1.0\");const mw=class src_Reference_Reference{uri;depth;value;refSet;errors;constructor({uri:s,depth:o=0,refSet:i,value:a}){this.uri=s,this.value=a,this.depth=o,this.refSet=i,this.errors=[]}};const gw=class ReferenceSet{rootRef;refs;circular;constructor({refs:s=[],circular:o=!1}={}){this.refs=[],this.circular=o,s.forEach(this.add.bind(this))}get size(){return this.refs.length}add(s){return this.has(s)||(this.refs.push(s),this.rootRef=void 0===this.rootRef?s:this.rootRef,s.refSet=this),this}merge(s){for(const o of s.values())this.add(o);return this}has(s){const o=Jc(s)?s:s.uri;return _c(this.find((s=>s.uri===o)))}find(s){return this.refs.find(s)}*values(){yield*this.refs}clean(){this.refs.forEach((s=>{s.refSet=void 0})),this.rootRef=void 0,this.refs.length=0}};function _identity(s){return s}const yw=_curry1(_identity),vw={parse:{mediaType:\"text/plain\",parsers:[],parserOpts:{}},resolve:{baseURI:\"\",resolvers:[],resolverOpts:{},strategies:[],strategyOpts:{},internal:!0,external:!0,maxDepth:1/0},dereference:{strategies:[],strategyOpts:{},refSet:null,maxDepth:1/0,circular:\"ignore\",circularReplacer:yw,immutable:!0},bundle:{strategies:[],refSet:null,maxDepth:1/0}};const bw=_curry2((function lens(s,o){return function(i){return function(a){return cc((function(s){return o(s,a)}),i(s(a)))}}}));var Identity=function(s){return{value:s,map:function(o){return Identity(o(s))}}},_w=_curry3((function over(s,o,i){return s((function(s){return Identity(o(s))}))(i).value}));const Sw=_w;const Ew=na(\"\"),ww=bw(Qu([\"resolve\",\"baseURI\"]),n_([\"resolve\",\"baseURI\"])),baseURIDefault=s=>Ew(s)?url_cwd():s,util_merge=(s,o)=>{const i=up(s,o);return Sw(ww,baseURIDefault,i)};const xw=class File_File{uri;mediaType;data;parseResult;constructor({uri:s,mediaType:o=\"text/plain\",data:i,parseResult:a}){this.uri=s,this.mediaType=o,this.data=i,this.parseResult=a}get extension(){return Jc(this.uri)?(s=>{const o=s.lastIndexOf(\".\");return o>=0?s.substring(o).toLowerCase():\"\"})(this.uri):\"\"}toString(){if(\"string\"==typeof this.data)return this.data;if(this.data instanceof ArrayBuffer||[\"ArrayBuffer\"].includes(ra(this.data))||ArrayBuffer.isView(this.data)){return new TextDecoder(\"utf-8\").decode(this.data)}return String(this.data)}};const kw=class PluginError extends Ko{plugin;constructor(s,o){super(s,{cause:o.cause}),this.plugin=o.plugin}},plugins_filter=async(s,o,i)=>{const a=await Promise.all(i.map(_p([s],o)));return i.filter(((s,o)=>a[o]))},run=async(s,o,i)=>{let a;for(const u of i)try{const i=await u[s].call(u,...o);return{plugin:u,result:i}}catch(s){a=new kw(\"Error while running plugin\",{cause:s,plugin:u})}return Promise.reject(a)};const Ow=class DereferenceError extends Ko{};const Aw=class UnmatchedDereferenceStrategyError extends Ow{},dereferenceApiDOM=async(s,o)=>{let i=s,a=!1;if(!$u(s)){const o=cloneShallow(s);o.classes.push(\"result\"),i=new Au([o]),a=!0}const u=new xw({uri:o.resolve.baseURI,parseResult:i,mediaType:o.parse.mediaType}),_=await plugins_filter(\"canDereference\",[u,o],o.dereference.strategies);if(gp(_))throw new Aw(u.uri);try{const{result:s}=await run(\"dereference\",[u,o],_);return a?s.get(0):s}catch(s){throw new Ow(`Error while dereferencing file \"${u.uri}\"`,{cause:s})}};const Cw=class ParseError extends Ko{};const jw=class ParserError extends Cw{};const Pw=class Parser_Parser{name;allowEmpty;sourceMap;fileExtensions;mediaTypes;constructor({name:s,allowEmpty:o=!0,sourceMap:i=!1,fileExtensions:a=[],mediaTypes:u=[]}){this.name=s,this.allowEmpty=o,this.sourceMap=i,this.fileExtensions=a,this.mediaTypes=u}};const Iw=class BinaryParser extends Pw{constructor(s){super({...null!=s?s:{},name:\"binary\"})}canParse(s){return 0===this.fileExtensions.length||this.fileExtensions.includes(s.extension)}parse(s){try{const o=unescape(encodeURIComponent(s.toString())),i=btoa(o),a=new Au;if(0!==i.length){const s=new Su.Om(i);s.classes.push(\"result\"),a.push(s)}return a}catch(o){throw new jw(`Error parsing \"${s.uri}\"`,{cause:o})}}};const Tw=class ResolveStrategy{name;constructor({name:s}){this.name=s}};const Nw=class OpenAPI3_1ResolveStrategy extends Tw{constructor(s){super({...null!=s?s:{},name:\"openapi-3-1\"})}canResolve(s,o){const i=o.dereference.strategies.find((s=>\"openapi-3-1\"===s.name));return void 0!==i&&i.canDereference(s,o)}async resolve(s,o){const i=o.dereference.strategies.find((s=>\"openapi-3-1\"===s.name));if(void 0===i)throw new Aw('\"openapi-3-1\" dereference strategy is not available.');const a=new gw,u=util_merge(o,{resolve:{internal:!1},dereference:{refSet:a}});return await i.dereference(s,u),a}};const Mw=class Resolver{name;constructor({name:s}){this.name=s}};const Rw=class HTTPResolver extends Mw{timeout;redirects;withCredentials;constructor(s){const{name:o=\"http-resolver\",timeout:i=5e3,redirects:a=5,withCredentials:u=!1}=null!=s?s:{};super({name:o}),this.timeout=i,this.redirects=a,this.withCredentials=u}canRead(s){return isHttpUrl(s.uri)}};const Dw=class ResolveError extends Ko{};const Lw=class ResolverError extends Dw{},{AbortController:Fw,AbortSignal:Bw}=globalThis;void 0===globalThis.AbortController&&(globalThis.AbortController=Fw),void 0===globalThis.AbortSignal&&(globalThis.AbortSignal=Bw);const $w=class HTTPResolverSwaggerClient extends Rw{swaggerHTTPClient=http_http;swaggerHTTPClientConfig;constructor({swaggerHTTPClient:s=http_http,swaggerHTTPClientConfig:o={},...i}={}){super({...i,name:\"http-swagger-client\"}),this.swaggerHTTPClient=s,this.swaggerHTTPClientConfig=o}getHttpClient(){return this.swaggerHTTPClient}async read(s){const o=this.getHttpClient(),i=new AbortController,{signal:a}=i,u=setTimeout((()=>{i.abort()}),this.timeout),_=this.getHttpClient().withCredentials||this.withCredentials?\"include\":\"same-origin\",w=0===this.redirects?\"error\":\"follow\",x=this.redirects>0?this.redirects:void 0;try{return(await o({url:s.uri,signal:a,userFetch:async(s,o)=>{let i=await fetch(s,o);try{i.headers.delete(\"Content-Type\")}catch{i=new Response(i.body,{...i,headers:new Headers(i.headers)}),i.headers.delete(\"Content-Type\")}return i},credentials:_,redirect:w,follow:x,...this.swaggerHTTPClientConfig})).text.arrayBuffer()}catch(o){throw new Lw(`Error downloading \"${s.uri}\"`,{cause:o})}finally{clearTimeout(u)}}},transformers_from=(s,o=Ep)=>{if(Jc(s))try{return o.fromRefract(JSON.parse(s))}catch{}return fu(s)&&Yu(\"element\",s)?o.fromRefract(s):o.toElement(s)};const qw=class JSONParser extends Pw{constructor(s={}){super({name:\"json-swagger-client\",mediaTypes:[\"application/json\"],...s})}async canParse(s){const o=0===this.fileExtensions.length||this.fileExtensions.includes(s.extension),i=this.mediaTypes.includes(s.mediaType);if(!o)return!1;if(i)return!0;if(!i)try{return JSON.parse(s.toString()),!0}catch(s){return!1}return!1}async parse(s){if(this.sourceMap)throw new jw(\"json-swagger-client parser plugin doesn't support sourceMaps option\");const o=new Au,i=s.toString();if(this.allowEmpty&&\"\"===i.trim())return o;try{const s=transformers_from(JSON.parse(i));return s.classes.push(\"result\"),o.push(s),o}catch(o){throw new jw(`Error parsing \"${s.uri}\"`,{cause:o})}}};const Uw=class YAMLParser extends Pw{constructor(s={}){super({name:\"yaml-1-2-swagger-client\",mediaTypes:[\"text/yaml\",\"application/yaml\"],...s})}async canParse(s){const o=0===this.fileExtensions.length||this.fileExtensions.includes(s.extension),i=this.mediaTypes.includes(s.mediaType);if(!o)return!1;if(i)return!0;if(!i)try{return fn.load(s.toString(),{schema:rn}),!0}catch(s){return!1}return!1}async parse(s){if(this.sourceMap)throw new jw(\"yaml-1-2-swagger-client parser plugin doesn't support sourceMaps option\");const o=new Au,i=s.toString();try{const s=fn.load(i,{schema:rn});if(this.allowEmpty&&void 0===s)return o;const a=transformers_from(s);return a.classes.push(\"result\"),o.push(a),o}catch(o){throw new jw(`Error parsing \"${s.uri}\"`,{cause:o})}}};const Vw=class OpenAPIJSON3_1Parser extends Pw{detectionRegExp=/\"openapi\"\\s*:\\s*\"(?<version_json>3\\.1\\.(?:[1-9]\\d*|0))\"/;constructor(s={}){super({name:\"openapi-json-3-1-swagger-client\",mediaTypes:new OpenAPIMediaTypes(...fw.filterByFormat(\"generic\"),...fw.filterByFormat(\"json\")),...s})}async canParse(s){const o=0===this.fileExtensions.length||this.fileExtensions.includes(s.extension),i=this.mediaTypes.includes(s.mediaType);if(!o)return!1;if(i)return!0;if(!i)try{const o=s.toString();return JSON.parse(o),this.detectionRegExp.test(o)}catch(s){return!1}return!1}async parse(s){if(this.sourceMap)throw new jw(\"openapi-json-3-1-swagger-client parser plugin doesn't support sourceMaps option\");const o=new Au,i=s.toString();if(this.allowEmpty&&\"\"===i.trim())return o;try{const s=JSON.parse(i),a=Ab.refract(s,this.refractorOpts);return a.classes.push(\"result\"),o.push(a),o}catch(o){throw new jw(`Error parsing \"${s.uri}\"`,{cause:o})}}};const zw=class OpenAPIYAML31Parser extends Pw{detectionRegExp=/(?<YAML>^([\"']?)openapi\\2\\s*:\\s*([\"']?)(?<version_yaml>3\\.1\\.(?:[1-9]\\d*|0))\\3(?:\\s+|$))|(?<JSON>\"openapi\"\\s*:\\s*\"(?<version_json>3\\.1\\.(?:[1-9]\\d*|0))\")/m;constructor(s={}){super({name:\"openapi-yaml-3-1-swagger-client\",mediaTypes:new OpenAPIMediaTypes(...fw.filterByFormat(\"generic\"),...fw.filterByFormat(\"yaml\")),...s})}async canParse(s){const o=0===this.fileExtensions.length||this.fileExtensions.includes(s.extension),i=this.mediaTypes.includes(s.mediaType);if(!o)return!1;if(i)return!0;if(!i)try{const o=s.toString();return fn.load(o),this.detectionRegExp.test(o)}catch(s){return!1}return!1}async parse(s){if(this.sourceMap)throw new jw(\"openapi-yaml-3-1-swagger-client parser plugin doesn't support sourceMaps option\");const o=new Au,i=s.toString();try{const s=fn.load(i,{schema:rn});if(this.allowEmpty&&void 0===s)return o;const a=Ab.refract(s,this.refractorOpts);return a.classes.push(\"result\"),o.push(a),o}catch(o){throw new jw(`Error parsing \"${s.uri}\"`,{cause:o})}}};const Ww=_curry3((function propEq(s,o,i){return na(s,Da(o,i))}));const Jw=class DereferenceStrategy{name;constructor({name:s}){this.name=s}};const Hw=_curry2((function none(s,o){return xu(_complement(s),o)}));var Kw=__webpack_require__(8068);const Gw=class ElementIdentityError extends Go{value;constructor(s,o){super(s,o),void 0!==o&&(this.value=o.value)}};class IdentityManager{uuid;identityMap;constructor({length:s=6}={}){this.uuid=new Kw({length:s}),this.identityMap=new WeakMap}identify(s){if(!Cu(s))throw new Gw(\"Cannot not identify the element. `element` is neither structurally compatible nor a subclass of an Element class.\",{value:s});if(s.meta.hasKey(\"id\")&&ju(s.meta.get(\"id\"))&&!s.meta.get(\"id\").equals(\"\"))return s.id;if(this.identityMap.has(s))return this.identityMap.get(s);const o=new Su.Om(this.generateId());return this.identityMap.set(s,o),o}forget(s){return!!this.identityMap.has(s)&&(this.identityMap.delete(s),!0)}generateId(){return this.uuid.randomUUID()}}new IdentityManager;const Yw=_curry3((function pathOr(s,o,i){return Na(s,_path(o,i))})),traversal_find=(s,o)=>{const i=new PredicateVisitor({predicate:s,returnOnTrue:qu});return visitor_visit(o,i),Yw(void 0,[0],i.result)};const Xw=class JsonSchema$anchorError extends Ko{};const Qw=class EvaluationJsonSchema$anchorError extends Xw{};const Zw=class InvalidJsonSchema$anchorError extends Xw{constructor(s){super(`Invalid JSON Schema $anchor \"${s}\".`)}},isAnchor=s=>/^[A-Za-z_][A-Za-z_0-9.-]*$/.test(s),uriToAnchor=s=>{const o=getHash(s);return tp(\"#\",o)},$anchor_evaluate=(s,o)=>{const i=(s=>{if(!isAnchor(s))throw new Zw(s);return s})(s),a=traversal_find((s=>uE(s)&&serializers_value(s.$anchor)===i),o);if(bc(a))throw new Qw(`Evaluation failed on token: \"${i}\"`);return a},traversal_filter=(s,o)=>{const i=new PredicateVisitor({predicate:s});return visitor_visit(o,i),new Su.G6(i.result)};const ex=class JsonSchemaUriError extends Ko{};const tx=class EvaluationJsonSchemaUriError extends ex{},resolveSchema$refField=(s,o)=>{if(void 0===o.$ref)return;const i=getHash(serializers_value(o.$ref)),a=serializers_value(o.meta.get(\"ancestorsSchemaIdentifiers\")),u=Aa(((s,o)=>resolve(s,sanitize(stripHash(o)))),s,[...a,serializers_value(o.$ref)]);return`${u}${\"#\"===i?\"\":i}`},refractToSchemaElement=s=>{if(refractToSchemaElement.cache.has(s))return refractToSchemaElement.cache.get(s);const o=lS.refract(s);return refractToSchemaElement.cache.set(s,o),o};refractToSchemaElement.cache=new WeakMap;const maybeRefractToSchemaElement=s=>isPrimitiveElement(s)?refractToSchemaElement(s):s,uri_evaluate=(s,o)=>{const{cache:i}=uri_evaluate,a=stripHash(s),isSchemaElementWith$id=s=>uE(s)&&void 0!==s.$id;if(!i.has(o)){const s=traversal_filter(isSchemaElementWith$id,o);i.set(o,Array.from(s))}const u=i.get(o).find((s=>{const o=((s,o)=>{if(void 0===o.$id)return;const i=serializers_value(o.meta.get(\"ancestorsSchemaIdentifiers\"));return Aa(((s,o)=>resolve(s,sanitize(stripHash(o)))),s,i)})(a,s);return o===a}));if(bc(u))throw new tx(`Evaluation failed on URI: \"${s}\"`);return isAnchor(uriToAnchor(s))?$anchor_evaluate(uriToAnchor(s),u):apidom_evaluate(u,fromURIReference(s))};uri_evaluate.cache=new WeakMap;const rx=class MaximumDereferenceDepthError extends Ow{};const nx=class MaximumResolveDepthError extends Dw{};const sx=class UnmatchedResolverError extends Lw{},apidom_reference_src_parse=async(s,o)=>{const i=new xw({uri:sanitize(stripHash(s)),mediaType:o.parse.mediaType}),a=await(async(s,o)=>{const i=o.resolve.resolvers.map((s=>{const i=Object.create(s);return Object.assign(i,o.resolve.resolverOpts)})),a=await plugins_filter(\"canRead\",[s,o],i);if(gp(a))throw new sx(s.uri);try{const{result:o}=await run(\"read\",[s],a);return o}catch(o){throw new Dw(`Error while reading file \"${s.uri}\"`,{cause:o})}})(i,o);return(async(s,o)=>{const i=o.parse.parsers.map((s=>{const i=Object.create(s);return Object.assign(i,o.parse.parserOpts)})),a=await plugins_filter(\"canParse\",[s,o],i);if(gp(a))throw new sx(s.uri);try{const{plugin:i,result:u}=await run(\"parse\",[s,o],a);return!i.allowEmpty&&u.isEmpty?Promise.reject(new Cw(`Error while parsing file \"${s.uri}\". File is empty.`)):u}catch(o){throw new Cw(`Error while parsing file \"${s.uri}\"`,{cause:o})}})(new xw({...i,data:a}),o)};class AncestorLineage extends Array{includesCycle(s){return this.filter((o=>o.has(s))).length>1}includes(s,o){return s instanceof Set?super.includes(s,o):this.some((o=>o.has(s)))}findItem(s){for(const o of this)for(const i of o)if(Cu(i)&&s(i))return i}}const ox=visitor_visit[Symbol.for(\"nodejs.util.promisify.custom\")],ix=new IdentityManager,mutationReplacer=(s,o,i,a)=>{Ru(a)?a.value=s:Array.isArray(a)&&(a[i]=s)};class OpenAPI3_1DereferenceVisitor{indirections;namespace;reference;options;ancestors;refractCache;allOfDiscriminatorMapping;constructor({reference:s,namespace:o,options:i,indirections:a=[],ancestors:u=new AncestorLineage,refractCache:_=new Map,allOfDiscriminatorMapping:w=new Map}){this.indirections=a,this.namespace=o,this.reference=s,this.options=i,this.ancestors=new AncestorLineage(...u),this.refractCache=_,this.allOfDiscriminatorMapping=w}toBaseURI(s){return resolve(this.reference.uri,sanitize(stripHash(s)))}async toReference(s){if(this.reference.depth>=this.options.resolve.maxDepth)throw new nx(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file \"${this.reference.uri}\"`);const o=this.toBaseURI(s),{refSet:i}=this.reference;if(i.has(o))return i.find(Ww(o,\"uri\"));const a=await apidom_reference_src_parse(unsanitize(o),{...this.options,parse:{...this.options.parse,mediaType:\"text/plain\"}}),u=new mw({uri:o,value:cloneDeep(a),depth:this.reference.depth+1});if(i.add(u),this.options.dereference.immutable){const s=new mw({uri:`immutable://${o}`,value:a,depth:this.reference.depth+1});i.add(s)}return u}toAncestorLineage(s){const o=new Set(s.filter(Cu));return[new AncestorLineage(...this.ancestors,o),o]}OpenApi3_1Element={leave:(s,o,i,a,u,_)=>{var w;if(null===(w=this.options.dereference.strategyOpts[\"openapi-3-1\"])||void 0===w||!w.dereferenceDiscriminatorMapping)return;const x=cloneShallow(s);return x.setMetaProperty(\"allOfDiscriminatorMapping\",Object.fromEntries(this.allOfDiscriminatorMapping)),_.replaceWith(x,mutationReplacer),i?void 0:x}};async ReferenceElement(s,o,i,a,u,_){if(this.indirections.includes(s))return!1;const[w,x]=this.toAncestorLineage([...u,i]),C=this.toBaseURI(serializers_value(s.$ref)),j=stripHash(this.reference.uri)===C,L=!j;if(!this.options.resolve.internal&&j)return!1;if(!this.options.resolve.external&&L)return!1;const B=await this.toReference(serializers_value(s.$ref)),$=resolve(C,serializers_value(s.$ref));this.indirections.push(s);const U=fromURIReference($);let V=apidom_evaluate(B.value.result,U);if(V.id=ix.identify(V),isPrimitiveElement(V)){const o=serializers_value(s.meta.get(\"referenced-element\")),i=`${o}-${serializers_value(ix.identify(V))}`;if(this.refractCache.has(i))V=this.refractCache.get(i);else if(isReferenceLikeElement(V))V=Lb.refract(V),V.setMetaProperty(\"referenced-element\",o),this.refractCache.set(i,V);else{V=this.namespace.getElementClass(o).refract(V),this.refractCache.set(i,V)}}if(s===V)throw new Ko(\"Recursive Reference Object detected\");if(this.indirections.length>this.options.dereference.maxDepth)throw new rx(`Maximum dereference depth of \"${this.options.dereference.maxDepth}\" has been exceeded in file \"${this.reference.uri}\"`);if(w.includes(V)){if(B.refSet.circular=!0,\"error\"===this.options.dereference.circular)throw new Ko(\"Circular reference detected\");if(\"replace\"===this.options.dereference.circular){var z,Y;const o=new Su.sI(V.id,{type:\"reference\",uri:B.uri,$ref:serializers_value(s.$ref)}),a=(null!==(z=null===(Y=this.options.dereference.strategyOpts[\"openapi-3-1\"])||void 0===Y?void 0:Y.circularReplacer)&&void 0!==z?z:this.options.dereference.circularReplacer)(o);return _.replaceWith(a,mutationReplacer),!i&&a}}const Z=stripHash(B.refSet.rootRef.uri)!==B.uri,ee=[\"error\",\"replace\"].includes(this.options.dereference.circular);if((L||Z||iE(V)||ee)&&!w.includesCycle(V)){x.add(s);const o=new OpenAPI3_1DereferenceVisitor({reference:B,namespace:this.namespace,indirections:[...this.indirections],options:this.options,refractCache:this.refractCache,ancestors:w,allOfDiscriminatorMapping:this.allOfDiscriminatorMapping});V=await ox(V,o,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType}),x.delete(s)}this.indirections.pop();const ie=cloneShallow(V);return ie.setMetaProperty(\"id\",ix.generateId()),ie.setMetaProperty(\"ref-fields\",{$ref:serializers_value(s.$ref),description:serializers_value(s.description),summary:serializers_value(s.summary)}),ie.setMetaProperty(\"ref-origin\",B.uri),ie.setMetaProperty(\"ref-referencing-element-id\",cloneDeep(ix.identify(s))),Nu(V)&&Nu(ie)&&(s.hasKey(\"description\")&&\"description\"in V&&(ie.remove(\"description\"),ie.set(\"description\",s.get(\"description\"))),s.hasKey(\"summary\")&&\"summary\"in V&&(ie.remove(\"summary\"),ie.set(\"summary\",s.get(\"summary\")))),_.replaceWith(ie,mutationReplacer),!i&&ie}async PathItemElement(s,o,i,a,u,_){if(!ju(s.$ref))return;if(this.indirections.includes(s))return!1;const[w,x]=this.toAncestorLineage([...u,i]),C=this.toBaseURI(serializers_value(s.$ref)),j=stripHash(this.reference.uri)===C,L=!j;if(!this.options.resolve.internal&&j)return;if(!this.options.resolve.external&&L)return;const B=await this.toReference(serializers_value(s.$ref)),$=resolve(C,serializers_value(s.$ref));this.indirections.push(s);const U=fromURIReference($);let V=apidom_evaluate(B.value.result,U);if(V.id=ix.identify(V),isPrimitiveElement(V)){const s=`path-item-${serializers_value(ix.identify(V))}`;this.refractCache.has(s)?V=this.refractCache.get(s):(V=Mb.refract(V),this.refractCache.set(s,V))}if(s===V)throw new Ko(\"Recursive Path Item Object reference detected\");if(this.indirections.length>this.options.dereference.maxDepth)throw new rx(`Maximum dereference depth of \"${this.options.dereference.maxDepth}\" has been exceeded in file \"${this.reference.uri}\"`);if(w.includes(V)){if(B.refSet.circular=!0,\"error\"===this.options.dereference.circular)throw new Ko(\"Circular reference detected\");if(\"replace\"===this.options.dereference.circular){var z,Y;const o=new Su.sI(V.id,{type:\"path-item\",uri:B.uri,$ref:serializers_value(s.$ref)}),a=(null!==(z=null===(Y=this.options.dereference.strategyOpts[\"openapi-3-1\"])||void 0===Y?void 0:Y.circularReplacer)&&void 0!==z?z:this.options.dereference.circularReplacer)(o);return _.replaceWith(a,mutationReplacer),!i&&a}}const Z=stripHash(B.refSet.rootRef.uri)!==B.uri,ee=[\"error\",\"replace\"].includes(this.options.dereference.circular);if((L||Z||sE(V)&&ju(V.$ref)||ee)&&!w.includesCycle(V)){x.add(s);const o=new OpenAPI3_1DereferenceVisitor({reference:B,namespace:this.namespace,indirections:[...this.indirections],options:this.options,refractCache:this.refractCache,ancestors:w,allOfDiscriminatorMapping:this.allOfDiscriminatorMapping});V=await ox(V,o,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType}),x.delete(s)}if(this.indirections.pop(),sE(V)){const o=new Mb([...V.content],cloneDeep(V.meta),cloneDeep(V.attributes));o.setMetaProperty(\"id\",ix.generateId()),s.forEach(((s,i,a)=>{o.remove(serializers_value(i)),o.content.push(a)})),o.remove(\"$ref\"),o.setMetaProperty(\"ref-fields\",{$ref:serializers_value(s.$ref)}),o.setMetaProperty(\"ref-origin\",B.uri),o.setMetaProperty(\"ref-referencing-element-id\",cloneDeep(ix.identify(s))),V=o}return _.replaceWith(V,mutationReplacer),i?void 0:V}async LinkElement(s,o,i,a,u,_){if(!ju(s.operationRef)&&!ju(s.operationId))return;if(ju(s.operationRef)&&ju(s.operationId))throw new Ko(\"LinkElement operationRef and operationId fields are mutually exclusive.\");let w;if(ju(s.operationRef)){var x;const o=fromURIReference(serializers_value(s.operationRef)),a=this.toBaseURI(serializers_value(s.operationRef)),u=stripHash(this.reference.uri)===a,C=!u;if(!this.options.resolve.internal&&u)return;if(!this.options.resolve.external&&C)return;const j=await this.toReference(serializers_value(s.operationRef));if(w=apidom_evaluate(j.value.result,o),isPrimitiveElement(w)){const s=`operation-${serializers_value(ix.identify(w))}`;this.refractCache.has(s)?w=this.refractCache.get(s):(w=Pb.refract(w),this.refractCache.set(s,w))}w=cloneShallow(w),w.setMetaProperty(\"ref-origin\",j.uri);const L=cloneShallow(s);return null===(x=L.operationRef)||void 0===x||x.meta.set(\"operation\",w),_.replaceWith(L,mutationReplacer),i?void 0:L}if(ju(s.operationId)){var C;const o=serializers_value(s.operationId),a=await this.toReference(unsanitize(this.reference.uri));if(w=traversal_find((s=>rE(s)&&Cu(s.operationId)&&s.operationId.equals(o)),a.value.result),bc(w))throw new Ko(`OperationElement(operationId=${o}) not found.`);const u=cloneShallow(s);return null===(C=u.operationId)||void 0===C||C.meta.set(\"operation\",w),_.replaceWith(u,mutationReplacer),i?void 0:u}}async ExampleElement(s,o,i,a,u,_){if(!ju(s.externalValue))return;if(s.hasKey(\"value\")&&ju(s.externalValue))throw new Ko(\"ExampleElement value and externalValue fields are mutually exclusive.\");const w=this.toBaseURI(serializers_value(s.externalValue)),x=stripHash(this.reference.uri)===w,C=!x;if(!this.options.resolve.internal&&x)return;if(!this.options.resolve.external&&C)return;const j=await this.toReference(serializers_value(s.externalValue)),L=cloneShallow(j.value.result);L.setMetaProperty(\"ref-origin\",j.uri);const B=cloneShallow(s);return B.value=L,_.replaceWith(B,mutationReplacer),i?void 0:B}async MemberElement(s,o,i,a,u,_){var w;const x=u[u.length-1];if(!Nu(x)||!x.classes.contains(\"discriminator-mapping\"))return;if(null===(w=this.options.dereference.strategyOpts[\"openapi-3-1\"])||void 0===w||!w.dereferenceDiscriminatorMapping)return!1;if(!ju(s.key)||!ju(s.value))return!1;if(this.indirections.includes(s))return!1;this.indirections.push(s);const[C,j]=this.toAncestorLineage([...u,i]),L=[...j].findLast(uE),B=cloneDeep(L.getMetaProperty(\"ancestorsSchemaIdentifiers\")),$=serializers_value(s.value),U=/^[a-zA-Z0-9\\\\.\\\\-_]+$/.test($)?`#/components/schemas/${$}`:$,V=new lS({$ref:U});V.setMetaProperty(\"ancestorsSchemaIdentifiers\",B),j.add(V);const z=new OpenAPI3_1DereferenceVisitor({reference:this.reference,namespace:this.namespace,indirections:[...this.indirections],options:this.options,refractCache:this.refractCache,ancestors:C,allOfDiscriminatorMapping:this.allOfDiscriminatorMapping}),Y=await ox(V,z,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType});j.delete(V),this.indirections.pop();const Z=cloneShallow(s);return Z.value.setMetaProperty(\"ref-schema\",Y),_.replaceWith(Z,mutationReplacer),i?void 0:Z}async SchemaElement(s,o,i,a,u,_){if(!ju(s.$ref))return;if(this.indirections.includes(s))return!1;const[w,x]=this.toAncestorLineage([...u,i]);let C=await this.toReference(unsanitize(this.reference.uri)),{uri:j}=C;const L=resolveSchema$refField(j,s),B=stripHash(L),$=new xw({uri:B}),U=Hw((s=>s.canRead($)),this.options.resolve.resolvers),V=!U;let z,Y=stripHash(this.reference.uri)===L,Z=!Y;this.indirections.push(s);try{if(U||V){j=this.toBaseURI(L);const s=L,o=maybeRefractToSchemaElement(C.value.result);if(z=uri_evaluate(s,o),z=maybeRefractToSchemaElement(z),z.id=ix.identify(z),!this.options.resolve.internal&&Y)return;if(!this.options.resolve.external&&Z)return}else{if(j=this.toBaseURI(L),Y=stripHash(this.reference.uri)===j,Z=!Y,!this.options.resolve.internal&&Y)return;if(!this.options.resolve.external&&Z)return;C=await this.toReference(unsanitize(L));const s=fromURIReference(L),o=maybeRefractToSchemaElement(C.value.result);z=apidom_evaluate(o,s),z=maybeRefractToSchemaElement(z),z.id=ix.identify(z)}}catch(s){if(!(V&&s instanceof tx))throw s;if(isAnchor(uriToAnchor(L))){if(Y=stripHash(this.reference.uri)===j,Z=!Y,!this.options.resolve.internal&&Y)return;if(!this.options.resolve.external&&Z)return;C=await this.toReference(unsanitize(L));const s=uriToAnchor(L),o=maybeRefractToSchemaElement(C.value.result);z=$anchor_evaluate(s,o),z=maybeRefractToSchemaElement(z),z.id=ix.identify(z)}else{if(j=this.toBaseURI(L),Y=stripHash(this.reference.uri)===j,Z=!Y,!this.options.resolve.internal&&Y)return;if(!this.options.resolve.external&&Z)return;C=await this.toReference(unsanitize(L));const s=fromURIReference(L),o=maybeRefractToSchemaElement(C.value.result);z=apidom_evaluate(o,s),z=maybeRefractToSchemaElement(z),z.id=ix.identify(z)}}if(s===z)throw new Ko(\"Recursive Schema Object reference detected\");if(this.indirections.length>this.options.dereference.maxDepth)throw new rx(`Maximum dereference depth of \"${this.options.dereference.maxDepth}\" has been exceeded in file \"${this.reference.uri}\"`);if(w.includes(z)){if(C.refSet.circular=!0,\"error\"===this.options.dereference.circular)throw new Ko(\"Circular reference detected\");if(\"replace\"===this.options.dereference.circular){var ee,ie;const o=new Su.sI(z.id,{type:\"json-schema\",uri:C.uri,$ref:serializers_value(s.$ref)}),a=(null!==(ee=null===(ie=this.options.dereference.strategyOpts[\"openapi-3-1\"])||void 0===ie?void 0:ie.circularReplacer)&&void 0!==ee?ee:this.options.dereference.circularReplacer)(o);return _.replaceWith(a,mutationReplacer),!i&&a}}const ae=stripHash(C.refSet.rootRef.uri)!==C.uri,ce=[\"error\",\"replace\"].includes(this.options.dereference.circular);if((Z||ae||uE(z)&&ju(z.$ref)||ce)&&!w.includesCycle(z)){x.add(s);const o=new OpenAPI3_1DereferenceVisitor({reference:C,namespace:this.namespace,indirections:[...this.indirections],options:this.options,refractCache:this.refractCache,ancestors:w,allOfDiscriminatorMapping:this.allOfDiscriminatorMapping});z=await ox(z,o,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType}),x.delete(s)}if(this.indirections.pop(),predicates_isBooleanJsonSchemaElement(z)){const o=cloneDeep(z);return o.setMetaProperty(\"id\",ix.generateId()),o.setMetaProperty(\"ref-fields\",{$ref:serializers_value(s.$ref),$refBaseURI:L}),o.setMetaProperty(\"ref-origin\",C.uri),o.setMetaProperty(\"ref-referencing-element-id\",cloneDeep(ix.identify(s))),_.replaceWith(o,mutationReplacer),!i&&o}if(uE(z)){var le;const o=new lS([...z.content],cloneDeep(z.meta),cloneDeep(z.attributes));if(o.setMetaProperty(\"id\",ix.generateId()),s.forEach(((s,i,a)=>{o.remove(serializers_value(i)),o.content.push(a)})),o.remove(\"$ref\"),o.setMetaProperty(\"ref-fields\",{$ref:serializers_value(s.$ref),$refBaseURI:L}),o.setMetaProperty(\"ref-origin\",C.uri),o.setMetaProperty(\"ref-referencing-element-id\",cloneDeep(ix.identify(s))),null!==(le=this.options.dereference.strategyOpts[\"openapi-3-1\"])&&void 0!==le&&le.dereferenceDiscriminatorMapping){var pe;const s=u[u.length-1],i=[...x].findLast(uE),a=null==i?void 0:i.getMetaProperty(\"schemaName\"),_=serializers_value(o.getMetaProperty(\"schemaName\"));if(_&&a&&null!=s&&null!==(pe=s.classes)&&void 0!==pe&&pe.contains(\"json-schema-allOf\")){var de;const s=null!==(de=this.allOfDiscriminatorMapping.get(_))&&void 0!==de?de:[];s.push(i),this.allOfDiscriminatorMapping.set(_,s)}}z=o}return _.replaceWith(z,mutationReplacer),i?void 0:z}}const ax=OpenAPI3_1DereferenceVisitor,cx=visitor_visit[Symbol.for(\"nodejs.util.promisify.custom\")];const lx=class OpenAPI3_1DereferenceStrategy extends Jw{constructor(s){super({...null!=s?s:{},name:\"openapi-3-1\"})}canDereference(s){var o;return\"text/plain\"!==s.mediaType?fw.includes(s.mediaType):tE(null===(o=s.parseResult)||void 0===o?void 0:o.result)}async dereference(s,o){var i;const a=createNamespace(pw),u=null!==(i=o.dereference.refSet)&&void 0!==i?i:new gw,_=new gw;let w,x=u;u.has(s.uri)?w=u.find(Ww(s.uri,\"uri\")):(w=new mw({uri:s.uri,value:s.parseResult}),u.add(w)),o.dereference.immutable&&(u.refs.map((s=>new mw({...s,value:cloneDeep(s.value)}))).forEach((s=>_.add(s))),w=_.find((o=>o.uri===s.uri)),x=_);const C=new ax({reference:w,namespace:a,options:o}),j=await cx(x.rootRef.value,C,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType});return o.dereference.immutable&&_.refs.filter((s=>s.uri.startsWith(\"immutable://\"))).map((s=>new mw({...s,uri:s.uri.replace(/^immutable:\\/\\//,\"\")}))).forEach((s=>u.add(s))),null===o.dereference.refSet&&u.clean(),_.clean(),j}},to_path=s=>{const o=(s=>s.slice(2))(s);return o.reduce(((s,i,a)=>{if(Ru(i)){const o=String(serializers_value(i.key));s.push(o)}else if(Mu(o[a-2])){const u=o[a-2].content.indexOf(i);s.push(u)}return s}),[])};const ux=class ModelPropertyMacroVisitor{modelPropertyMacro;options;SchemaElement={leave:(s,o,i,a,u)=>{void 0!==s.properties&&Nu(s.properties)&&s.properties.forEach((o=>{if(Nu(o))try{const s=this.modelPropertyMacro(serializers_value(o));o.set(\"default\",s)}catch(o){var a,_;const w=new Error(o,{cause:o});w.fullPath=[...to_path([...u,i,s]),\"properties\"],null===(a=this.options.dereference.dereferenceOpts)||void 0===a||null===(a=a.errors)||void 0===a||null===(_=a.push)||void 0===_||_.call(a,w)}}))}};constructor({modelPropertyMacro:s,options:o}){this.modelPropertyMacro=s,this.options=o}};var px=function(){function XUniqWith(s,o){this.xf=o,this.pred=s,this.items=[]}return XUniqWith.prototype[\"@@transducer/init\"]=_xfBase_init,XUniqWith.prototype[\"@@transducer/result\"]=_xfBase_result,XUniqWith.prototype[\"@@transducer/step\"]=function(s,o){return _includesWith(this.pred,o,this.items)?s:(this.items.push(o),this.xf[\"@@transducer/step\"](s,o))},XUniqWith}();function _xuniqWith(s){return function(o){return new px(s,o)}}var hx=_curry2(_dispatchable([],_xuniqWith,(function(s,o){for(var i,a=0,u=o.length,_=[];a<u;)_includesWith(s,i=o[a],_)||(_[_.length]=i),a+=1;return _})));const dx=hx;const fx=class all_of_AllOfVisitor{options;SchemaElement={leave(s,o,i,a,u){if(void 0===s.allOf)return;if(!Mu(s.allOf)){var _,w;const o=new TypeError(\"allOf must be an array\");return o.fullPath=[...to_path([...u,i,s]),\"allOf\"],void(null===(_=this.options.dereference.dereferenceOpts)||void 0===_||null===(_=_.errors)||void 0===_||null===(w=_.push)||void 0===w||w.call(_,o))}if(s.allOf.isEmpty)return void s.remove(\"allOf\");if(!s.allOf.content.every(uE)){var x,C;const o=new TypeError(\"Elements in allOf must be objects\");return o.fullPath=[...to_path([...u,i,s]),\"allOf\"],void(null===(x=this.options.dereference.dereferenceOpts)||void 0===x||null===(x=x.errors)||void 0===x||null===(C=x.push)||void 0===C||C.call(x,o))}for(;s.hasKey(\"allOf\");){const{allOf:o}=s;s.remove(\"allOf\");const i=dd.all([...o.content,s],{customMerge:s=>\"enum\"===serializers_value(s)?(s,o)=>{if(includesClasses([\"json-schema-enum\"],s)&&includesClasses([\"json-schema-enum\"],o)){const areElementsEqual=(s,o)=>!(Mu(s)||Mu(o)||Nu(s)||Nu(o))&&s.equals(serializers_value(o)),i=cloneShallow(s);return i.content=dx(areElementsEqual)([...s.content,...o.content]),i}return dd(s,o)}:dd});if(s.hasKey(\"$$ref\")||i.remove(\"$$ref\"),s.hasKey(\"example\")){const o=i.getMember(\"example\");o&&(o.value=s.get(\"example\"))}if(s.hasKey(\"examples\")){const o=i.getMember(\"examples\");o&&(o.value=s.get(\"examples\"))}s.content=i.content}}};constructor({options:s}){this.options=s}};const mx=class ParameterMacroVisitor{parameterMacro;options;#n;OperationElement={enter:s=>{this.#n=s},leave:()=>{this.#n=void 0}};ParameterElement={leave:(s,o,i,a,u)=>{const _=this.#n?serializers_value(this.#n):null,w=serializers_value(s);try{const o=this.parameterMacro(_,w);s.set(\"default\",o)}catch(s){var x,C;const o=new Error(s,{cause:s});o.fullPath=to_path([...u,i]),null===(x=this.options.dereference.dereferenceOpts)||void 0===x||null===(x=x.errors)||void 0===x||null===(C=x.push)||void 0===C||C.call(x,o)}}};constructor({parameterMacro:s,options:o}){this.parameterMacro=s,this.options=o}},get_root_cause=s=>{if(null==s.cause)return s;let{cause:o}=s;for(;null!=o.cause;)o=o.cause;return o};const gx=class SchemaRefError extends Go{},{wrapError:yx}=Xl,vx=visitor_visit[Symbol.for(\"nodejs.util.promisify.custom\")],bx=new IdentityManager,dereference_mutationReplacer=(s,o,i,a)=>{Ru(a)?a.value=s:Array.isArray(a)&&(a[i]=s)};class OpenAPI3_1SwaggerClientDereferenceVisitor extends ax{useCircularStructures;allowMetaPatches;basePath;constructor({allowMetaPatches:s=!0,useCircularStructures:o=!1,basePath:i=null,...a}){super(a),this.allowMetaPatches=s,this.useCircularStructures=o,this.basePath=i}async ReferenceElement(s,o,i,a,u,_){try{if(this.indirections.includes(s))return!1;const[o,a]=this.toAncestorLineage([...u,i]),j=this.toBaseURI(serializers_value(s.$ref)),L=stripHash(this.reference.uri)===j,B=!L;if(!this.options.resolve.internal&&L)return!1;if(!this.options.resolve.external&&B)return!1;const $=await this.toReference(serializers_value(s.$ref)),U=resolve(j,serializers_value(s.$ref));this.indirections.push(s);const V=fromURIReference(U);let z=apidom_evaluate($.value.result,V);if(z.id=bx.identify(z),isPrimitiveElement(z)){const o=serializers_value(s.meta.get(\"referenced-element\")),i=`${o}-${serializers_value(bx.identify(z))}`;if(this.refractCache.has(i))z=this.refractCache.get(i);else if(isReferenceLikeElement(z))z=Lb.refract(z),z.setMetaProperty(\"referenced-element\",o),this.refractCache.set(i,z);else{z=this.namespace.getElementClass(o).refract(z),this.refractCache.set(i,z)}}if(s===z)throw new Ko(\"Recursive Reference Object detected\");if(this.indirections.length>this.options.dereference.maxDepth)throw new rx(`Maximum dereference depth of \"${this.options.dereference.maxDepth}\" has been exceeded in file \"${this.reference.uri}\"`);if(o.includes(z)){if($.refSet.circular=!0,\"error\"===this.options.dereference.circular)throw new Ko(\"Circular reference detected\");if(\"replace\"===this.options.dereference.circular){var w,x;const o=new Su.sI(z.id,{type:\"reference\",uri:$.uri,$ref:serializers_value(s.$ref),baseURI:U,referencingElement:s}),a=(null!==(w=null===(x=this.options.dereference.strategyOpts[\"openapi-3-1\"])||void 0===x?void 0:x.circularReplacer)&&void 0!==w?w:this.options.dereference.circularReplacer)(o);return _.replaceWith(o,dereference_mutationReplacer),!i&&a}}const Y=stripHash($.refSet.rootRef.uri)!==$.uri,Z=[\"error\",\"replace\"].includes(this.options.dereference.circular);if((B||Y||iE(z)||Z)&&!o.includesCycle(z)){var C;a.add(s);const _=new OpenAPI3_1SwaggerClientDereferenceVisitor({reference:$,namespace:this.namespace,indirections:[...this.indirections],options:this.options,refractCache:this.refractCache,ancestors:o,allowMetaPatches:this.allowMetaPatches,useCircularStructures:this.useCircularStructures,basePath:null!==(C=this.basePath)&&void 0!==C?C:[...to_path([...u,i,s]),\"$ref\"]});z=await vx(z,_,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType}),a.delete(s)}this.indirections.pop();const ee=cloneShallow(z);if(ee.setMetaProperty(\"ref-fields\",{$ref:serializers_value(s.$ref),description:serializers_value(s.description),summary:serializers_value(s.summary)}),ee.setMetaProperty(\"ref-origin\",$.uri),ee.setMetaProperty(\"ref-referencing-element-id\",cloneDeep(bx.identify(s))),Nu(z)&&(s.hasKey(\"description\")&&\"description\"in z&&(ee.remove(\"description\"),ee.set(\"description\",s.get(\"description\"))),s.hasKey(\"summary\")&&\"summary\"in z&&(ee.remove(\"summary\"),ee.set(\"summary\",s.get(\"summary\")))),this.allowMetaPatches&&Nu(ee)&&!ee.hasKey(\"$$ref\")){const s=resolve(j,U);ee.set(\"$$ref\",s)}return _.replaceWith(ee,dereference_mutationReplacer),!i&&ee}catch(o){var j,L,B;const a=get_root_cause(o),_=yx(a,{baseDoc:this.reference.uri,$ref:serializers_value(s.$ref),pointer:fromURIReference(serializers_value(s.$ref)),fullPath:null!==(j=this.basePath)&&void 0!==j?j:[...to_path([...u,i,s]),\"$ref\"]});return void(null===(L=this.options.dereference.dereferenceOpts)||void 0===L||null===(L=L.errors)||void 0===L||null===(B=L.push)||void 0===B||B.call(L,_))}}async PathItemElement(s,o,i,a,u,_){try{if(!ju(s.$ref))return;if(this.indirections.includes(s))return!1;if(includesClasses([\"cycle\"],s.$ref))return!1;const[o,a]=this.toAncestorLineage([...u,i]),j=this.toBaseURI(serializers_value(s.$ref)),L=stripHash(this.reference.uri)===j,B=!L;if(!this.options.resolve.internal&&L)return;if(!this.options.resolve.external&&B)return;const $=await this.toReference(serializers_value(s.$ref)),U=resolve(j,serializers_value(s.$ref));this.indirections.push(s);const V=fromURIReference(U);let z=apidom_evaluate($.value.result,V);if(z.id=bx.identify(z),isPrimitiveElement(z)){const s=`path-item-${serializers_value(bx.identify(z))}`;this.refractCache.has(s)?z=this.refractCache.get(s):(z=Mb.refract(z),this.refractCache.set(s,z))}if(s===z)throw new Ko(\"Recursive Path Item Object reference detected\");if(this.indirections.length>this.options.dereference.maxDepth)throw new rx(`Maximum dereference depth of \"${this.options.dereference.maxDepth}\" has been exceeded in file \"${this.reference.uri}\"`);if(o.includes(z)){if($.refSet.circular=!0,\"error\"===this.options.dereference.circular)throw new Ko(\"Circular reference detected\");if(\"replace\"===this.options.dereference.circular){var w,x;const o=new Su.sI(z.id,{type:\"path-item\",uri:$.uri,$ref:serializers_value(s.$ref),baseURI:U,referencingElement:s}),a=(null!==(w=null===(x=this.options.dereference.strategyOpts[\"openapi-3-1\"])||void 0===x?void 0:x.circularReplacer)&&void 0!==w?w:this.options.dereference.circularReplacer)(o);return _.replaceWith(o,dereference_mutationReplacer),!i&&a}}const Y=stripHash($.refSet.rootRef.uri)!==$.uri,Z=[\"error\",\"replace\"].includes(this.options.dereference.circular);if((B||Y||sE(z)&&ju(z.$ref)||Z)&&!o.includesCycle(z)){var C;a.add(s);const _=new OpenAPI3_1SwaggerClientDereferenceVisitor({reference:$,namespace:this.namespace,indirections:[...this.indirections],options:this.options,ancestors:o,allowMetaPatches:this.allowMetaPatches,useCircularStructures:this.useCircularStructures,basePath:null!==(C=this.basePath)&&void 0!==C?C:[...to_path([...u,i,s]),\"$ref\"]});z=await vx(z,_,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType}),a.delete(s)}if(this.indirections.pop(),sE(z)){const o=new Mb([...z.content],cloneDeep(z.meta),cloneDeep(z.attributes));if(s.forEach(((s,i,a)=>{o.remove(serializers_value(i)),o.content.push(a)})),o.remove(\"$ref\"),o.setMetaProperty(\"ref-fields\",{$ref:serializers_value(s.$ref)}),o.setMetaProperty(\"ref-origin\",$.uri),o.setMetaProperty(\"ref-referencing-element-id\",cloneDeep(bx.identify(s))),this.allowMetaPatches&&void 0===o.get(\"$$ref\")){const s=resolve(j,U);o.set(\"$$ref\",s)}z=o}return _.replaceWith(z,dereference_mutationReplacer),i?void 0:z}catch(o){var j,L,B;const a=get_root_cause(o),_=yx(a,{baseDoc:this.reference.uri,$ref:serializers_value(s.$ref),pointer:fromURIReference(serializers_value(s.$ref)),fullPath:null!==(j=this.basePath)&&void 0!==j?j:[...to_path([...u,i,s]),\"$ref\"]});return void(null===(L=this.options.dereference.dereferenceOpts)||void 0===L||null===(L=L.errors)||void 0===L||null===(B=L.push)||void 0===B||B.call(L,_))}}async SchemaElement(s,o,i,a,u,_){try{if(!ju(s.$ref))return;if(this.indirections.includes(s))return!1;const[o,a]=this.toAncestorLineage([...u,i]);let j=await this.toReference(unsanitize(this.reference.uri)),{uri:L}=j;const B=resolveSchema$refField(L,s),$=stripHash(B),U=new xw({uri:$}),V=!this.options.resolve.resolvers.some((s=>s.canRead(U))),z=!V;let Y,Z=stripHash(this.reference.uri)===B,ee=!Z;this.indirections.push(s);try{if(V||z){L=this.toBaseURI(B);const s=B,o=maybeRefractToSchemaElement(j.value.result);if(Y=uri_evaluate(s,o),Y=maybeRefractToSchemaElement(Y),Y.id=bx.identify(Y),!this.options.resolve.internal&&Z)return;if(!this.options.resolve.external&&ee)return}else{if(L=this.toBaseURI(B),Z=stripHash(this.reference.uri)===L,ee=!Z,!this.options.resolve.internal&&Z)return;if(!this.options.resolve.external&&ee)return;j=await this.toReference(unsanitize(B));const s=fromURIReference(B),o=maybeRefractToSchemaElement(j.value.result);Y=apidom_evaluate(o,s),Y=maybeRefractToSchemaElement(Y),Y.id=bx.identify(Y)}}catch(s){if(!(z&&s instanceof tx))throw s;if(isAnchor(uriToAnchor(B))){if(Z=stripHash(this.reference.uri)===L,ee=!Z,!this.options.resolve.internal&&Z)return;if(!this.options.resolve.external&&ee)return;j=await this.toReference(unsanitize(B));const s=uriToAnchor(B),o=maybeRefractToSchemaElement(j.value.result);Y=$anchor_evaluate(s,o),Y=maybeRefractToSchemaElement(Y),Y.id=bx.identify(Y)}else{if(L=this.toBaseURI(serializers_value(B)),Z=stripHash(this.reference.uri)===L,ee=!Z,!this.options.resolve.internal&&Z)return;if(!this.options.resolve.external&&ee)return;j=await this.toReference(unsanitize(B));const s=fromURIReference(B),o=maybeRefractToSchemaElement(j.value.result);Y=apidom_evaluate(o,s),Y=maybeRefractToSchemaElement(Y),Y.id=bx.identify(Y)}}if(s===Y)throw new Ko(\"Recursive Schema Object reference detected\");if(this.indirections.length>this.options.dereference.maxDepth)throw new rx(`Maximum dereference depth of \"${this.options.dereference.maxDepth}\" has been exceeded in file \"${this.reference.uri}\"`);if(o.includes(Y)){if(j.refSet.circular=!0,\"error\"===this.options.dereference.circular)throw new Ko(\"Circular reference detected\");if(\"replace\"===this.options.dereference.circular){var w,x;const o=new Su.sI(Y.id,{type:\"json-schema\",uri:j.uri,$ref:serializers_value(s.$ref),baseURI:resolve(L,B),referencingElement:s}),a=(null!==(w=null===(x=this.options.dereference.strategyOpts[\"openapi-3-1\"])||void 0===x?void 0:x.circularReplacer)&&void 0!==w?w:this.options.dereference.circularReplacer)(o);return _.replaceWith(a,dereference_mutationReplacer),!i&&a}}const ie=stripHash(j.refSet.rootRef.uri)!==j.uri,ae=[\"error\",\"replace\"].includes(this.options.dereference.circular);if((ee||ie||uE(Y)&&ju(Y.$ref)||ae)&&!o.includesCycle(Y)){var C;a.add(s);const _=new OpenAPI3_1SwaggerClientDereferenceVisitor({reference:j,namespace:this.namespace,indirections:[...this.indirections],options:this.options,useCircularStructures:this.useCircularStructures,allowMetaPatches:this.allowMetaPatches,ancestors:o,basePath:null!==(C=this.basePath)&&void 0!==C?C:[...to_path([...u,i,s]),\"$ref\"]});Y=await vx(Y,_,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType}),a.delete(s)}if(this.indirections.pop(),predicates_isBooleanJsonSchemaElement(Y)){const o=cloneDeep(Y);return o.setMetaProperty(\"ref-fields\",{$ref:serializers_value(s.$ref)}),o.setMetaProperty(\"ref-origin\",j.uri),o.setMetaProperty(\"ref-referencing-element-id\",cloneDeep(bx.identify(s))),_.replaceWith(o,dereference_mutationReplacer),!i&&o}if(uE(Y)){const o=new lS([...Y.content],cloneDeep(Y.meta),cloneDeep(Y.attributes));if(s.forEach(((s,i,a)=>{o.remove(serializers_value(i)),o.content.push(a)})),o.remove(\"$ref\"),o.setMetaProperty(\"ref-fields\",{$ref:serializers_value(s.$ref)}),o.setMetaProperty(\"ref-origin\",j.uri),o.setMetaProperty(\"ref-referencing-element-id\",cloneDeep(bx.identify(s))),this.allowMetaPatches&&void 0===o.get(\"$$ref\")){const s=resolve(L,B);o.set(\"$$ref\",s)}Y=o}return _.replaceWith(Y,dereference_mutationReplacer),i?void 0:Y}catch(o){var j,L,B;const a=get_root_cause(o),_=new gx(`Could not resolve reference: ${a.message}`,{baseDoc:this.reference.uri,$ref:serializers_value(s.$ref),fullPath:null!==(j=this.basePath)&&void 0!==j?j:[...to_path([...u,i,s]),\"$ref\"],cause:a});return void(null===(L=this.options.dereference.dereferenceOpts)||void 0===L||null===(L=L.errors)||void 0===L||null===(B=L.push)||void 0===B||B.call(L,_))}}async LinkElement(){}async ExampleElement(s,o,i,a,u,_){try{return await super.ExampleElement(s,o,i,a,u,_)}catch(o){var w,x,C;const a=get_root_cause(o),_=yx(a,{baseDoc:this.reference.uri,externalValue:serializers_value(s.externalValue),fullPath:null!==(w=this.basePath)&&void 0!==w?w:[...to_path([...u,i,s]),\"externalValue\"]});return void(null===(x=this.options.dereference.dereferenceOpts)||void 0===x||null===(x=x.errors)||void 0===x||null===(C=x.push)||void 0===C||C.call(x,_))}}}const _x=OpenAPI3_1SwaggerClientDereferenceVisitor,Sx=mergeAll[Symbol.for(\"nodejs.util.promisify.custom\")];const Ex=class RootVisitor{constructor({parameterMacro:s,modelPropertyMacro:o,mode:i,options:a,...u}){const _=[];_.push(new _x({...u,options:a})),\"function\"==typeof o&&_.push(new ux({modelPropertyMacro:o,options:a})),\"strict\"!==i&&_.push(new fx({options:a})),\"function\"==typeof s&&_.push(new mx({parameterMacro:s,options:a}));const w=Sx(_,{nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType});Object.assign(this,w)}},wx=visitor_visit[Symbol.for(\"nodejs.util.promisify.custom\")];const xx=class OpenAPI3_1SwaggerClientDereferenceStrategy extends lx{allowMetaPatches;parameterMacro;modelPropertyMacro;mode;ancestors;constructor({allowMetaPatches:s=!1,parameterMacro:o=null,modelPropertyMacro:i=null,mode:a=\"non-strict\",ancestors:u=[],..._}={}){super({..._}),this.name=\"openapi-3-1-swagger-client\",this.allowMetaPatches=s,this.parameterMacro=o,this.modelPropertyMacro=i,this.mode=a,this.ancestors=[...u]}async dereference(s,o){var i;const a=createNamespace(pw),u=null!==(i=o.dereference.refSet)&&void 0!==i?i:new gw,_=new gw;let w,x=u;u.has(s.uri)?w=u.find((o=>o.uri===s.uri)):(w=new mw({uri:s.uri,value:s.parseResult}),u.add(w)),o.dereference.immutable&&(u.refs.map((s=>new mw({...s,value:cloneDeep(s.value)}))).forEach((s=>_.add(s))),w=_.find((o=>o.uri===s.uri)),x=_);const C=new Ex({reference:w,namespace:a,options:o,allowMetaPatches:this.allowMetaPatches,ancestors:this.ancestors,modelPropertyMacro:this.modelPropertyMacro,mode:this.mode,parameterMacro:this.parameterMacro}),j=await wx(x.rootRef.value,C,{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType});return o.dereference.immutable&&_.refs.filter((s=>s.uri.startsWith(\"immutable://\"))).map((s=>new mw({...s,uri:s.uri.replace(/^immutable:\\/\\//,\"\")}))).forEach((s=>u.add(s))),null===o.dereference.refSet&&u.clean(),_.clean(),j}},circularReplacer=s=>{const o=serializers_value(s.meta.get(\"baseURI\")),i=s.meta.get(\"referencingElement\");return new Su.Sh({$ref:o},cloneDeep(i.meta),cloneDeep(i.attributes))},resolveOpenAPI31Strategy=async s=>{const{spec:o,timeout:i,redirects:a,requestInterceptor:u,responseInterceptor:_,pathDiscriminator:w=[],allowMetaPatches:x=!1,useCircularStructures:C=!1,skipNormalization:j=!1,parameterMacro:L=null,modelPropertyMacro:B=null,mode:$=\"non-strict\",strategies:U}=s;try{const{cache:V}=resolveOpenAPI31Strategy,z=U.find((s=>s.match(o))),Y=isHttpUrl(url_cwd())?url_cwd():Ll,Z=options_retrievalURI(s),ee=resolve(Y,Z);let ie;V.has(o)?ie=V.get(o):(ie=Ab.refract(o),ie.classes.push(\"result\"),V.set(o,ie));const ae=new Au([ie]),ce=es_compile(w),le=\"\"===ce?\"\":`#${ce}`,pe=apidom_evaluate(ie,ce),de=new mw({uri:ee,value:ae}),fe=new gw({refs:[de]});\"\"!==ce&&(fe.rootRef=void 0);const ye=[new Set([pe])],be=[],_e=await(async(s,o={})=>{const i=util_merge(vw,o);return dereferenceApiDOM(s,i)})(pe,{resolve:{baseURI:`${ee}${le}`,resolvers:[new $w({timeout:i||1e4,redirects:a||10})],resolverOpts:{swaggerHTTPClientConfig:{requestInterceptor:u,responseInterceptor:_}},strategies:[new Nw]},parse:{mediaType:fw.latest(),parsers:[new Vw({allowEmpty:!1,sourceMap:!1}),new zw({allowEmpty:!1,sourceMap:!1}),new qw({allowEmpty:!1,sourceMap:!1}),new Uw({allowEmpty:!1,sourceMap:!1}),new Iw({allowEmpty:!1,sourceMap:!1})]},dereference:{maxDepth:100,strategies:[new xx({allowMetaPatches:x,useCircularStructures:C,parameterMacro:L,modelPropertyMacro:B,mode:$,ancestors:ye})],refSet:fe,dereferenceOpts:{errors:be},immutable:!1,circular:C?\"ignore\":\"replace\",circularReplacer:C?vw.dereference.circularReplacer:circularReplacer}}),Se=((s,o,i)=>new xp({element:i}).transclude(s,o))(pe,_e,ie),we=j?Se:z.normalize(Se);return{spec:serializers_value(we),errors:be}}catch(s){if(s instanceof Wp)return{spec:o,errors:[]};throw s}};resolveOpenAPI31Strategy.cache=new WeakMap;const kx=resolveOpenAPI31Strategy;function _clone(s,o,i){if(i||(i=new Ox),function _isPrimitive(s){var o=typeof s;return null==s||\"object\"!=o&&\"function\"!=o}(s))return s;var a=function copy(a){var u=i.get(s);if(u)return u;for(var _ in i.set(s,a),s)Object.prototype.hasOwnProperty.call(s,_)&&(a[_]=o?_clone(s[_],!0,i):s[_]);return a};switch(ra(s)){case\"Object\":return a(Object.create(Object.getPrototypeOf(s)));case\"Array\":return a(Array(s.length));case\"Date\":return new Date(s.valueOf());case\"RegExp\":return _cloneRegExp(s);case\"Int8Array\":case\"Uint8Array\":case\"Uint8ClampedArray\":case\"Int16Array\":case\"Uint16Array\":case\"Int32Array\":case\"Uint32Array\":case\"Float32Array\":case\"Float64Array\":case\"BigInt64Array\":case\"BigUint64Array\":return s.slice();default:return s}}var Ox=function(){function _ObjectMap(){this.map={},this.length=0}return _ObjectMap.prototype.set=function(s,o){var i=this.hash(s),a=this.map[i];a||(this.map[i]=a=[]),a.push([s,o]),this.length+=1},_ObjectMap.prototype.hash=function(s){var o=[];for(var i in s)o.push(Object.prototype.toString.call(s[i]));return o.join()},_ObjectMap.prototype.get=function(s){if(this.length<=180)for(var o in this.map)for(var i=this.map[o],a=0;a<i.length;a+=1){if((_=i[a])[0]===s)return _[1]}else{var u=this.hash(s);if(i=this.map[u])for(a=0;a<i.length;a+=1){var _;if((_=i[a])[0]===s)return _[1]}}},_ObjectMap}(),Ax=function(){function XReduceBy(s,o,i,a){this.valueFn=s,this.valueAcc=o,this.keyFn=i,this.xf=a,this.inputs={}}return XReduceBy.prototype[\"@@transducer/init\"]=_xfBase_init,XReduceBy.prototype[\"@@transducer/result\"]=function(s){var o;for(o in this.inputs)if(_has(o,this.inputs)&&(s=this.xf[\"@@transducer/step\"](s,this.inputs[o]))[\"@@transducer/reduced\"]){s=s[\"@@transducer/value\"];break}return this.inputs=null,this.xf[\"@@transducer/result\"](s)},XReduceBy.prototype[\"@@transducer/step\"]=function(s,o){var i=this.keyFn(o);return this.inputs[i]=this.inputs[i]||[i,_clone(this.valueAcc,!1)],this.inputs[i][1]=this.valueFn(this.inputs[i][1],o),s},XReduceBy}();function _xreduceBy(s,o,i){return function(a){return new Ax(s,o,i,a)}}var Cx=_curryN(4,[],_dispatchable([],_xreduceBy,(function reduceBy(s,o,i,a){var u=_xwrap((function(a,u){var _=i(u),w=s(_has(_,a)?a[_]:_clone(o,!1),u);return w&&w[\"@@transducer/reduced\"]?_reduced(a):(a[_]=w,a)}));return wa(u,{},a)})));const jx=_curry2(_checkForMethod(\"groupBy\",Cx((function(s,o){return s.push(o),s}),[])));const Px=class NormalizeStorage{internalStore;constructor(s,o,i){this.storageElement=s,this.storageField=o,this.storageSubField=i}get store(){if(!this.internalStore){let s=this.storageElement.get(this.storageField);Nu(s)||(s=new Su.Sh,this.storageElement.set(this.storageField,s));let o=s.get(this.storageSubField);Mu(o)||(o=new Su.wE,s.set(this.storageSubField,o)),this.internalStore=o}return this.internalStore}append(s){this.includes(s)||this.store.push(s)}includes(s){return this.store.includes(s)}},removeSpaces=s=>s.replace(/\\s/g,\"\"),normalize_operation_ids_replaceSpecialCharsWithUnderscore=s=>s.replace(/\\W/gi,\"_\"),normalizeOperationId=(s,o,i)=>{const a=removeSpaces(s);return a.length>0?normalize_operation_ids_replaceSpecialCharsWithUnderscore(a):((s,o)=>`${normalize_operation_ids_replaceSpecialCharsWithUnderscore(removeSpaces(o.toLowerCase()))}${normalize_operation_ids_replaceSpecialCharsWithUnderscore(removeSpaces(s))}`)(o,i)},normalize_operation_ids=({storageField:s=\"x-normalized\",operationIdNormalizer:o=normalizeOperationId}={})=>i=>{const{predicates:a,ancestorLineageToJSONPointer:u,namespace:_}=i,w=[],x=[],C=[];let j;return{visitor:{OpenApi3_1Element:{enter(o){j=new Px(o,s,\"operation-ids\")},leave(){const s=jx((s=>serializers_value(s.operationId)),x);Object.entries(s).forEach((([s,o])=>{Array.isArray(o)&&(o.length<=1||o.forEach(((o,i)=>{const a=`${s}${i+1}`;o.operationId=new _.elements.String(a)})))})),C.forEach((s=>{if(void 0===s.operationId)return;const o=String(serializers_value(s.operationId)),i=x.find((s=>serializers_value(s.meta.get(\"originalOperationId\"))===o));void 0!==i&&(s.operationId=cloneDeep.safe(i.operationId),s.meta.set(\"originalOperationId\",o),s.set(\"__originalOperationId\",o))})),x.length=0,C.length=0,j=void 0}},PathItemElement:{enter(s){const o=Na(\"path\",serializers_value(s.meta.get(\"path\")));w.push(o)},leave(){w.pop()}},OperationElement:{enter(s,i,a,C,L){if(void 0===s.operationId)return;const B=u([...L,a,s]);if(j.includes(B))return;const $=String(serializers_value(s.operationId)),U=Ba(w),V=Na(\"method\",serializers_value(s.meta.get(\"http-method\"))),z=o($,U,V);$!==z&&(s.operationId=new _.elements.String(z),s.set(\"__originalOperationId\",$),s.meta.set(\"originalOperationId\",$),x.push(s),j.append(B))}},LinkElement:{leave(s){a.isLinkElement(s)&&void 0!==s.operationId&&C.push(s)}}}}},normalize_parameters=({storageField:s=\"x-normalized\"}={})=>o=>{const{predicates:i,ancestorLineageToJSONPointer:a}=o,parameterEquals=(s,o)=>!!i.isParameterElement(s)&&(!!i.isParameterElement(o)&&(!!i.isStringElement(s.name)&&(!!i.isStringElement(s.in)&&(!!i.isStringElement(o.name)&&(!!i.isStringElement(o.in)&&(serializers_value(s.name)===serializers_value(o.name)&&serializers_value(s.in)===serializers_value(o.in))))))),u=[];let _;return{visitor:{OpenApi3_1Element:{enter(o){_=new Px(o,s,\"parameters\")},leave(){_=void 0}},PathItemElement:{enter(s,o,a,_,w){if(w.some(i.isComponentsElement))return;const{parameters:x}=s;i.isArrayElement(x)?u.push([...x.content]):u.push([])},leave(){u.pop()}},OperationElement:{leave(s,o,i,w,x){const C=Ba(u);if(!Array.isArray(C)||0===C.length)return;const j=a([...x,i,s]);if(_.includes(j))return;const L=Yw([],[\"parameters\",\"content\"],s),B=dx(parameterEquals,[...L,...C]);s.parameters=new _v(B),_.append(j)}}}}},normalize_security_requirements=({storageField:s=\"x-normalized\"}={})=>o=>{const{predicates:i,ancestorLineageToJSONPointer:a}=o;let u,_;return{visitor:{OpenApi3_1Element:{enter(o){_=new Px(o,s,\"security-requirements\"),i.isArrayElement(o.security)&&(u=o.security)},leave(){_=void 0,u=void 0}},OperationElement:{leave(s,o,w,x,C){if(C.some(i.isComponentsElement))return;const j=a([...C,w,s]);if(_.includes(j))return;var L;void 0===s.security&&void 0!==u&&(s.security=new Ov(null===(L=u)||void 0===L?void 0:L.content),_.append(j))}}}}},normalize_parameter_examples=({storageField:s=\"x-normalized\"}={})=>o=>{const{predicates:i,ancestorLineageToJSONPointer:a}=o;let u;return{visitor:{OpenApi3_1Element:{enter(o){u=new Px(o,s,\"parameter-examples\")},leave(){u=void 0}},ParameterElement:{leave(s,o,_,w,x){var C,j;if(x.some(i.isComponentsElement))return;if(void 0===s.schema||!i.isSchemaElement(s.schema))return;if(void 0===(null===(C=s.schema)||void 0===C?void 0:C.example)&&void 0===(null===(j=s.schema)||void 0===j?void 0:j.examples))return;const L=a([...x,_,s]);if(!u.includes(L)){if(void 0!==s.examples&&i.isObjectElement(s.examples)){const o=s.examples.map((s=>cloneDeep.safe(s.value)));return void 0!==s.schema.examples&&(s.schema.set(\"examples\",o),u.append(L)),void(void 0!==s.schema.example&&(s.schema.set(\"example\",o[0]),u.append(L)))}void 0!==s.example&&(void 0!==s.schema.examples&&(s.schema.set(\"examples\",[cloneDeep(s.example)]),u.append(L)),void 0!==s.schema.example&&(s.schema.set(\"example\",cloneDeep(s.example)),u.append(L)))}}}}}},normalize_header_examples=({storageField:s=\"x-normalized\"}={})=>o=>{const{predicates:i,ancestorLineageToJSONPointer:a}=o;let u;return{visitor:{OpenApi3_1Element:{enter(o){u=new Px(o,s,\"header-examples\")},leave(){u=void 0}},HeaderElement:{leave(s,o,_,w,x){var C,j;if(x.some(i.isComponentsElement))return;if(void 0===s.schema||!i.isSchemaElement(s.schema))return;if(void 0===(null===(C=s.schema)||void 0===C?void 0:C.example)&&void 0===(null===(j=s.schema)||void 0===j?void 0:j.examples))return;const L=a([...x,_,s]);if(!u.includes(L)){if(void 0!==s.examples&&i.isObjectElement(s.examples)){const o=s.examples.map((s=>cloneDeep.safe(s.value)));return void 0!==s.schema.examples&&(s.schema.set(\"examples\",o),u.append(L)),void(void 0!==s.schema.example&&(s.schema.set(\"example\",o[0]),u.append(L)))}void 0!==s.example&&(void 0!==s.schema.examples&&(s.schema.set(\"examples\",[cloneDeep(s.example)]),u.append(L)),void 0!==s.schema.example&&(s.schema.set(\"example\",cloneDeep(s.example)),u.append(L)))}}}}}},openapi_3_1_apidom_normalize=s=>{if(!Nu(s))return s;const o=[normalize_operation_ids({operationIdNormalizer:(s,o,i)=>opId({operationId:s},o,i,{v2OperationIdCompatibilityMode:!1})}),normalize_parameters(),normalize_security_requirements(),normalize_parameter_examples(),normalize_header_examples()];return dispatchPluginsSync(s,o,{toolboxCreator:apidom_ns_openapi_3_1_src_refractor_toolbox,visitorOptions:{keyMap:lw,nodeTypeGetter:apidom_ns_openapi_3_1_src_traversal_visitor_getNodeType}})},Ix={name:\"openapi-3-1-apidom\",match:s=>isOpenAPI31(s),normalize(s){if(!Cu(s)&&fu(s)&&!s.$$normalized){const i=(o=openapi_3_1_apidom_normalize,s=>{const i=Ab.refract(s);i.classes.push(\"result\");const a=o(i),u=serializers_value(a);return kx.cache.set(u,a),serializers_value(a)})(s);return i.$$normalized=!0,i}var o;return Cu(s)?openapi_3_1_apidom_normalize(s):s},resolve:async s=>kx(s)},Tx=Ix,makeResolve=s=>async o=>(async s=>{const{spec:o,requestInterceptor:i,responseInterceptor:a}=s,u=options_retrievalURI(s),_=options_httpClient(s),w=o||await makeFetchJSON(_,{requestInterceptor:i,responseInterceptor:a})(u),x={...s,spec:w};return s.strategies.find((s=>s.match(w))).resolve(x)})({...s,...o}),Nx=makeResolve({strategies:[_u,vu,gu]});const server_url_template=(s,o,i,a,u)=>{if(s===Pp.SEM_PRE){if(!1===Array.isArray(u))throw new Error(\"parser's user data must be an array\");u.push([\"server-url-template\",jp.charsToString(o,i,a)])}return Pp.SEM_OK},callbacks_server_variable=(s,o,i,a,u)=>{if(s===Pp.SEM_PRE){if(!1===Array.isArray(u))throw new Error(\"parser's user data must be an array\");u.push([\"server-variable\",jp.charsToString(o,i,a)])}return Pp.SEM_OK},server_variable_name=(s,o,i,a,u)=>{if(s===Pp.SEM_PRE){if(!1===Array.isArray(u))throw new Error(\"parser's user data must be an array\");u.push([\"server-variable-name\",jp.charsToString(o,i,a)])}return Pp.SEM_OK},callbacks_literals=(s,o,i,a,u)=>{if(s===Pp.SEM_PRE){if(!1===Array.isArray(u))throw new Error(\"parser's user data must be an array\");u.push([\"literals\",jp.charsToString(o,i,a)])}return Pp.SEM_OK},Mx=new function server_url_templating_grammar(){this.grammarObject=\"grammarObject\",this.rules=[],this.rules[0]={name:\"server-url-template\",lower:\"server-url-template\",index:0,isBkr:!1},this.rules[1]={name:\"server-variable\",lower:\"server-variable\",index:1,isBkr:!1},this.rules[2]={name:\"server-variable-name\",lower:\"server-variable-name\",index:2,isBkr:!1},this.rules[3]={name:\"literals\",lower:\"literals\",index:3,isBkr:!1},this.rules[4]={name:\"DIGIT\",lower:\"digit\",index:4,isBkr:!1},this.rules[5]={name:\"HEXDIG\",lower:\"hexdig\",index:5,isBkr:!1},this.rules[6]={name:\"pct-encoded\",lower:\"pct-encoded\",index:6,isBkr:!1},this.rules[7]={name:\"ucschar\",lower:\"ucschar\",index:7,isBkr:!1},this.rules[8]={name:\"iprivate\",lower:\"iprivate\",index:8,isBkr:!1},this.udts=[],this.rules[0].opcodes=[],this.rules[0].opcodes[0]={type:3,min:1,max:1/0},this.rules[0].opcodes[1]={type:1,children:[2,3]},this.rules[0].opcodes[2]={type:4,index:3},this.rules[0].opcodes[3]={type:4,index:1},this.rules[1].opcodes=[],this.rules[1].opcodes[0]={type:2,children:[1,2,3]},this.rules[1].opcodes[1]={type:7,string:[123]},this.rules[1].opcodes[2]={type:4,index:2},this.rules[1].opcodes[3]={type:7,string:[125]},this.rules[2].opcodes=[],this.rules[2].opcodes[0]={type:3,min:1,max:1/0},this.rules[2].opcodes[1]={type:1,children:[2,3,4]},this.rules[2].opcodes[2]={type:5,min:0,max:122},this.rules[2].opcodes[3]={type:6,string:[124]},this.rules[2].opcodes[4]={type:5,min:126,max:1114111},this.rules[3].opcodes=[],this.rules[3].opcodes[0]={type:3,min:1,max:1/0},this.rules[3].opcodes[1]={type:1,children:[2,3,4,5,6,7,8,9,10,11,12,13]},this.rules[3].opcodes[2]={type:6,string:[33]},this.rules[3].opcodes[3]={type:5,min:35,max:36},this.rules[3].opcodes[4]={type:5,min:38,max:59},this.rules[3].opcodes[5]={type:6,string:[61]},this.rules[3].opcodes[6]={type:5,min:63,max:91},this.rules[3].opcodes[7]={type:6,string:[93]},this.rules[3].opcodes[8]={type:6,string:[95]},this.rules[3].opcodes[9]={type:5,min:97,max:122},this.rules[3].opcodes[10]={type:6,string:[126]},this.rules[3].opcodes[11]={type:4,index:7},this.rules[3].opcodes[12]={type:4,index:8},this.rules[3].opcodes[13]={type:4,index:6},this.rules[4].opcodes=[],this.rules[4].opcodes[0]={type:5,min:48,max:57},this.rules[5].opcodes=[],this.rules[5].opcodes[0]={type:1,children:[1,2,3,4,5,6,7]},this.rules[5].opcodes[1]={type:4,index:4},this.rules[5].opcodes[2]={type:7,string:[97]},this.rules[5].opcodes[3]={type:7,string:[98]},this.rules[5].opcodes[4]={type:7,string:[99]},this.rules[5].opcodes[5]={type:7,string:[100]},this.rules[5].opcodes[6]={type:7,string:[101]},this.rules[5].opcodes[7]={type:7,string:[102]},this.rules[6].opcodes=[],this.rules[6].opcodes[0]={type:2,children:[1,2,3]},this.rules[6].opcodes[1]={type:7,string:[37]},this.rules[6].opcodes[2]={type:4,index:5},this.rules[6].opcodes[3]={type:4,index:5},this.rules[7].opcodes=[],this.rules[7].opcodes[0]={type:1,children:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]},this.rules[7].opcodes[1]={type:5,min:160,max:55295},this.rules[7].opcodes[2]={type:5,min:63744,max:64975},this.rules[7].opcodes[3]={type:5,min:65008,max:65519},this.rules[7].opcodes[4]={type:5,min:65536,max:131069},this.rules[7].opcodes[5]={type:5,min:131072,max:196605},this.rules[7].opcodes[6]={type:5,min:196608,max:262141},this.rules[7].opcodes[7]={type:5,min:262144,max:327677},this.rules[7].opcodes[8]={type:5,min:327680,max:393213},this.rules[7].opcodes[9]={type:5,min:393216,max:458749},this.rules[7].opcodes[10]={type:5,min:458752,max:524285},this.rules[7].opcodes[11]={type:5,min:524288,max:589821},this.rules[7].opcodes[12]={type:5,min:589824,max:655357},this.rules[7].opcodes[13]={type:5,min:655360,max:720893},this.rules[7].opcodes[14]={type:5,min:720896,max:786429},this.rules[7].opcodes[15]={type:5,min:786432,max:851965},this.rules[7].opcodes[16]={type:5,min:851968,max:917501},this.rules[7].opcodes[17]={type:5,min:921600,max:983037},this.rules[8].opcodes=[],this.rules[8].opcodes[0]={type:1,children:[1,2,3]},this.rules[8].opcodes[1]={type:5,min:57344,max:63743},this.rules[8].opcodes[2]={type:5,min:983040,max:1048573},this.rules[8].opcodes[3]={type:5,min:1048576,max:1114109},this.toString=function toString(){let s=\"\";return s+=\"; OpenAPI Server URL templating ABNF syntax\\n\",s+=\"server-url-template    = 1*( literals / server-variable ) ; variant of https://www.rfc-editor.org/rfc/rfc6570#section-2\\n\",s+='server-variable        = \"{\" server-variable-name \"}\"\\n',s+=\"server-variable-name   = 1*( %x00-7A / %x7C / %x7E-10FFFF ) ; every UTF8 character except { and } (from OpenAPI)\\n\",s+=\"\\n\",s+=\"; https://www.rfc-editor.org/rfc/rfc6570#section-2.1\\n\",s+=\"; https://www.rfc-editor.org/errata/eid6937\\n\",s+=\"literals               = 1*( %x21 / %x23-24 / %x26-3B / %x3D / %x3F-5B\\n\",s+=\"                       / %x5D / %x5F / %x61-7A / %x7E / ucschar / iprivate\\n\",s+=\"                       / pct-encoded)\\n\",s+=\"                            ; any Unicode character except: CTL, SP,\\n\",s+='                            ;  DQUOTE, \"%\" (aside from pct-encoded),\\n',s+='                            ;  \"<\", \">\", \"\\\\\", \"^\", \"`\", \"{\", \"|\", \"}\"\\n',s+=\"\\n\",s+=\"; https://www.rfc-editor.org/rfc/rfc6570#section-1.5\\n\",s+=\"DIGIT          =  %x30-39             ; 0-9\\n\",s+='HEXDIG         =  DIGIT / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\" ; case-insensitive\\n',s+=\"\\n\",s+='pct-encoded    =  \"%\" HEXDIG HEXDIG\\n',s+=\"\\n\",s+=\"ucschar        =  %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF\\n\",s+=\"               /  %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD\\n\",s+=\"               /  %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD\\n\",s+=\"               /  %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD\\n\",s+=\"               /  %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD\\n\",s+=\"               /  %xD0000-DFFFD / %xE1000-EFFFD\\n\",s+=\"\\n\",s+=\"iprivate       =  %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD\\n\",'; OpenAPI Server URL templating ABNF syntax\\nserver-url-template    = 1*( literals / server-variable ) ; variant of https://www.rfc-editor.org/rfc/rfc6570#section-2\\nserver-variable        = \"{\" server-variable-name \"}\"\\nserver-variable-name   = 1*( %x00-7A / %x7C / %x7E-10FFFF ) ; every UTF8 character except { and } (from OpenAPI)\\n\\n; https://www.rfc-editor.org/rfc/rfc6570#section-2.1\\n; https://www.rfc-editor.org/errata/eid6937\\nliterals               = 1*( %x21 / %x23-24 / %x26-3B / %x3D / %x3F-5B\\n                       / %x5D / %x5F / %x61-7A / %x7E / ucschar / iprivate\\n                       / pct-encoded)\\n                            ; any Unicode character except: CTL, SP,\\n                            ;  DQUOTE, \"%\" (aside from pct-encoded),\\n                            ;  \"<\", \">\", \"\\\\\", \"^\", \"`\", \"{\", \"|\", \"}\"\\n\\n; https://www.rfc-editor.org/rfc/rfc6570#section-1.5\\nDIGIT          =  %x30-39             ; 0-9\\nHEXDIG         =  DIGIT / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\" ; case-insensitive\\n\\npct-encoded    =  \"%\" HEXDIG HEXDIG\\n\\nucschar        =  %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF\\n               /  %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD\\n               /  %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD\\n               /  %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD\\n               /  %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD\\n               /  %xD0000-DFFFD / %xE1000-EFFFD\\n\\niprivate       =  %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD\\n'}},openapi_server_url_templating_es_parse=s=>{const o=new kp;o.ast=new Op,o.ast.callbacks[\"server-url-template\"]=server_url_template,o.ast.callbacks[\"server-variable\"]=callbacks_server_variable,o.ast.callbacks[\"server-variable-name\"]=server_variable_name,o.ast.callbacks.literals=callbacks_literals;return{result:o.parse(Mx,\"server-url-template\",s),ast:o.ast}},openapi_server_url_templating_es_test=(s,{strict:o=!1}={})=>{try{const i=openapi_server_url_templating_es_parse(s);if(!i.result.success)return!1;const a=[];i.ast.translate(a);const u=a.some((([s])=>\"server-variable\"===s));if(!o&&!u)try{return new URL(s,\"https://vladimirgorej.com\"),!0}catch{return!1}return!o||u}catch{return!1}},encodeServerVariable=s=>(s=>{try{return\"string\"==typeof s&&decodeURIComponent(s)!==s}catch{return!1}})(s)?s:encodeURIComponent(s).replace(/%5B/g,\"[\").replace(/%5D/g,\"]\"),Rx=[\"literals\",\"server-variable-name\"],es_substitute=(s,o,i={})=>{const a={...{encoder:encodeServerVariable},...i},u=openapi_server_url_templating_es_parse(s);if(!u.result.success)return s;const _=[];u.ast.translate(_);const w=_.filter((([s])=>Rx.includes(s))).map((([s,i])=>\"server-variable-name\"===s?Object.hasOwn(o,i)?a.encoder(o[i],i):`{${i}}`:i));return w.join(\"\")};function path_templating_grammar(){this.grammarObject=\"grammarObject\",this.rules=[],this.rules[0]={name:\"path-template\",lower:\"path-template\",index:0,isBkr:!1},this.rules[1]={name:\"path-segment\",lower:\"path-segment\",index:1,isBkr:!1},this.rules[2]={name:\"slash\",lower:\"slash\",index:2,isBkr:!1},this.rules[3]={name:\"path-literal\",lower:\"path-literal\",index:3,isBkr:!1},this.rules[4]={name:\"template-expression\",lower:\"template-expression\",index:4,isBkr:!1},this.rules[5]={name:\"template-expression-param-name\",lower:\"template-expression-param-name\",index:5,isBkr:!1},this.rules[6]={name:\"pchar\",lower:\"pchar\",index:6,isBkr:!1},this.rules[7]={name:\"unreserved\",lower:\"unreserved\",index:7,isBkr:!1},this.rules[8]={name:\"pct-encoded\",lower:\"pct-encoded\",index:8,isBkr:!1},this.rules[9]={name:\"sub-delims\",lower:\"sub-delims\",index:9,isBkr:!1},this.rules[10]={name:\"ALPHA\",lower:\"alpha\",index:10,isBkr:!1},this.rules[11]={name:\"DIGIT\",lower:\"digit\",index:11,isBkr:!1},this.rules[12]={name:\"HEXDIG\",lower:\"hexdig\",index:12,isBkr:!1},this.udts=[],this.rules[0].opcodes=[],this.rules[0].opcodes[0]={type:2,children:[1,2,6]},this.rules[0].opcodes[1]={type:4,index:2},this.rules[0].opcodes[2]={type:3,min:0,max:1/0},this.rules[0].opcodes[3]={type:2,children:[4,5]},this.rules[0].opcodes[4]={type:4,index:1},this.rules[0].opcodes[5]={type:4,index:2},this.rules[0].opcodes[6]={type:3,min:0,max:1},this.rules[0].opcodes[7]={type:4,index:1},this.rules[1].opcodes=[],this.rules[1].opcodes[0]={type:3,min:1,max:1/0},this.rules[1].opcodes[1]={type:1,children:[2,3]},this.rules[1].opcodes[2]={type:4,index:3},this.rules[1].opcodes[3]={type:4,index:4},this.rules[2].opcodes=[],this.rules[2].opcodes[0]={type:7,string:[47]},this.rules[3].opcodes=[],this.rules[3].opcodes[0]={type:3,min:1,max:1/0},this.rules[3].opcodes[1]={type:4,index:6},this.rules[4].opcodes=[],this.rules[4].opcodes[0]={type:2,children:[1,2,3]},this.rules[4].opcodes[1]={type:7,string:[123]},this.rules[4].opcodes[2]={type:4,index:5},this.rules[4].opcodes[3]={type:7,string:[125]},this.rules[5].opcodes=[],this.rules[5].opcodes[0]={type:3,min:1,max:1/0},this.rules[5].opcodes[1]={type:1,children:[2,3,4]},this.rules[5].opcodes[2]={type:5,min:0,max:122},this.rules[5].opcodes[3]={type:6,string:[124]},this.rules[5].opcodes[4]={type:5,min:126,max:1114111},this.rules[6].opcodes=[],this.rules[6].opcodes[0]={type:1,children:[1,2,3,4,5]},this.rules[6].opcodes[1]={type:4,index:7},this.rules[6].opcodes[2]={type:4,index:8},this.rules[6].opcodes[3]={type:4,index:9},this.rules[6].opcodes[4]={type:7,string:[58]},this.rules[6].opcodes[5]={type:7,string:[64]},this.rules[7].opcodes=[],this.rules[7].opcodes[0]={type:1,children:[1,2,3,4,5,6]},this.rules[7].opcodes[1]={type:4,index:10},this.rules[7].opcodes[2]={type:4,index:11},this.rules[7].opcodes[3]={type:7,string:[45]},this.rules[7].opcodes[4]={type:7,string:[46]},this.rules[7].opcodes[5]={type:7,string:[95]},this.rules[7].opcodes[6]={type:7,string:[126]},this.rules[8].opcodes=[],this.rules[8].opcodes[0]={type:2,children:[1,2,3]},this.rules[8].opcodes[1]={type:7,string:[37]},this.rules[8].opcodes[2]={type:4,index:12},this.rules[8].opcodes[3]={type:4,index:12},this.rules[9].opcodes=[],this.rules[9].opcodes[0]={type:1,children:[1,2,3,4,5,6,7,8,9,10,11]},this.rules[9].opcodes[1]={type:7,string:[33]},this.rules[9].opcodes[2]={type:7,string:[36]},this.rules[9].opcodes[3]={type:7,string:[38]},this.rules[9].opcodes[4]={type:7,string:[39]},this.rules[9].opcodes[5]={type:7,string:[40]},this.rules[9].opcodes[6]={type:7,string:[41]},this.rules[9].opcodes[7]={type:7,string:[42]},this.rules[9].opcodes[8]={type:7,string:[43]},this.rules[9].opcodes[9]={type:7,string:[44]},this.rules[9].opcodes[10]={type:7,string:[59]},this.rules[9].opcodes[11]={type:7,string:[61]},this.rules[10].opcodes=[],this.rules[10].opcodes[0]={type:1,children:[1,2]},this.rules[10].opcodes[1]={type:5,min:65,max:90},this.rules[10].opcodes[2]={type:5,min:97,max:122},this.rules[11].opcodes=[],this.rules[11].opcodes[0]={type:5,min:48,max:57},this.rules[12].opcodes=[],this.rules[12].opcodes[0]={type:1,children:[1,2,3,4,5,6,7]},this.rules[12].opcodes[1]={type:4,index:11},this.rules[12].opcodes[2]={type:7,string:[97]},this.rules[12].opcodes[3]={type:7,string:[98]},this.rules[12].opcodes[4]={type:7,string:[99]},this.rules[12].opcodes[5]={type:7,string:[100]},this.rules[12].opcodes[6]={type:7,string:[101]},this.rules[12].opcodes[7]={type:7,string:[102]},this.toString=function toString(){let s=\"\";return s+=\"; OpenAPI Path Templating ABNF syntax\\n\",s+=\"; variant of https://datatracker.ietf.org/doc/html/rfc3986#section-3.3\\n\",s+=\"path-template                  = slash *( path-segment slash ) [ path-segment ]\\n\",s+=\"path-segment                   = 1*( path-literal / template-expression )\\n\",s+='slash                          = \"/\"\\n',s+=\"path-literal                   = 1*pchar\\n\",s+='template-expression            = \"{\" template-expression-param-name \"}\"\\n',s+=\"template-expression-param-name = 1*( %x00-7A / %x7C / %x7E-10FFFF ) ; every UTF8 character except { and } (from OpenAPI)\\n\",s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc3986#section-3.3\\n\",s+='pchar               = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\\n',s+='unreserved          = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\\n',s+=\"                    ; https://datatracker.ietf.org/doc/html/rfc3986#section-2.3\\n\",s+='pct-encoded         = \"%\" HEXDIG HEXDIG\\n',s+=\"                    ; https://datatracker.ietf.org/doc/html/rfc3986#section-2.1\\n\",s+='sub-delims          = \"!\" / \"$\" / \"&\" / \"\\'\" / \"(\" / \")\"\\n',s+='                    / \"*\" / \"+\" / \",\" / \";\" / \"=\"\\n',s+=\"                    ; https://datatracker.ietf.org/doc/html/rfc3986#section-2.2\\n\",s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1\\n\",s+=\"ALPHA               = %x41-5A / %x61-7A   ; A-Z / a-z\\n\",s+=\"DIGIT               = %x30-39            ; 0-9\\n\",s+='HEXDIG              = DIGIT / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\"\\n','; OpenAPI Path Templating ABNF syntax\\n; variant of https://datatracker.ietf.org/doc/html/rfc3986#section-3.3\\npath-template                  = slash *( path-segment slash ) [ path-segment ]\\npath-segment                   = 1*( path-literal / template-expression )\\nslash                          = \"/\"\\npath-literal                   = 1*pchar\\ntemplate-expression            = \"{\" template-expression-param-name \"}\"\\ntemplate-expression-param-name = 1*( %x00-7A / %x7C / %x7E-10FFFF ) ; every UTF8 character except { and } (from OpenAPI)\\n\\n; https://datatracker.ietf.org/doc/html/rfc3986#section-3.3\\npchar               = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\\nunreserved          = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\\n                    ; https://datatracker.ietf.org/doc/html/rfc3986#section-2.3\\npct-encoded         = \"%\" HEXDIG HEXDIG\\n                    ; https://datatracker.ietf.org/doc/html/rfc3986#section-2.1\\nsub-delims          = \"!\" / \"$\" / \"&\" / \"\\'\" / \"(\" / \")\"\\n                    / \"*\" / \"+\" / \",\" / \";\" / \"=\"\\n                    ; https://datatracker.ietf.org/doc/html/rfc3986#section-2.2\\n\\n; https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1\\nALPHA               = %x41-5A / %x61-7A   ; A-Z / a-z\\nDIGIT               = %x30-39            ; 0-9\\nHEXDIG              = DIGIT / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\"\\n'}}const callbacks_slash=(s,o,i,a,u)=>(s===Pp.SEM_PRE?u.push([\"slash\",jp.charsToString(o,i,a)]):Pp.SEM_POST,Pp.SEM_OK),path_template=(s,o,i,a,u)=>{if(s===Pp.SEM_PRE){if(!1===Array.isArray(u))throw new Error(\"parser's user data must be an array\");u.push([\"path-template\",jp.charsToString(o,i,a)])}return Pp.SEM_OK},path_literal=(s,o,i,a,u)=>(s===Pp.SEM_PRE?u.push([\"path-literal\",jp.charsToString(o,i,a)]):Pp.SEM_POST,Pp.SEM_OK),template_expression=(s,o,i,a,u)=>(s===Pp.SEM_PRE?u.push([\"template-expression\",jp.charsToString(o,i,a)]):Pp.SEM_POST,Pp.SEM_OK),template_expression_param_name=(s,o,i,a,u)=>(s===Pp.SEM_PRE?u.push([\"template-expression-param-name\",jp.charsToString(o,i,a)]):Pp.SEM_POST,Pp.SEM_OK),Dx=new path_templating_grammar,openapi_path_templating_es_parse=s=>{const o=new kp;o.ast=new Op,o.ast.callbacks[\"path-template\"]=path_template,o.ast.callbacks.slash=callbacks_slash,o.ast.callbacks[\"path-literal\"]=path_literal,o.ast.callbacks[\"template-expression\"]=template_expression,o.ast.callbacks[\"template-expression-param-name\"]=template_expression_param_name;return{result:o.parse(Dx,\"path-template\",s),ast:o.ast}},encodePathComponent=s=>(s=>{try{return\"string\"==typeof s&&decodeURIComponent(s)!==s}catch{return!1}})(s)?s:encodeURIComponent(s).replace(/%5B/g,\"[\").replace(/%5D/g,\"]\"),Lx=[\"slash\",\"path-literal\",\"template-expression-param-name\"],es_resolve=(s,o,i={})=>{const a={...{encoder:encodePathComponent},...i},u=openapi_path_templating_es_parse(s);if(!u.result.success)return s;const _=[];u.ast.translate(_);const w=_.filter((([s])=>Lx.includes(s))).map((([s,i])=>\"template-expression-param-name\"===s?Object.prototype.hasOwnProperty.call(o,i)?a.encoder(o[i],i):`{${i}}`:i));return w.join(\"\")},Fx=(new path_templating_grammar,new kp,{body:function bodyBuilder({req:s,value:o}){void 0!==o&&(s.body=o)},header:function headerBuilder({req:s,parameter:o,value:i}){s.headers=s.headers||{},void 0!==i&&(s.headers[o.name]=i)},query:function queryBuilder({req:s,value:o,parameter:i}){s.query=s.query||{},!1===o&&\"boolean\"===i.type&&(o=\"false\");0===o&&[\"number\",\"integer\"].indexOf(i.type)>-1&&(o=\"0\");if(o)s.query[i.name]={collectionFormat:i.collectionFormat,value:o};else if(i.allowEmptyValue&&void 0!==o){const o=i.name;s.query[o]=s.query[o]||{},s.query[o].allowEmptyValue=!0}},path:function pathBuilder({req:s,value:o,parameter:i,baseURL:a}){if(void 0!==o){const u=s.url.replace(a,\"\"),_=es_resolve(u,{[i.name]:o});s.url=a+_}},formData:function formDataBuilder({req:s,value:o,parameter:i}){!1===o&&\"boolean\"===i.type&&(o=\"false\");0===o&&[\"number\",\"integer\"].indexOf(i.type)>-1&&(o=\"0\");if(o)s.form=s.form||{},s.form[i.name]={collectionFormat:i.collectionFormat,value:o};else if(i.allowEmptyValue&&void 0!==o){s.form=s.form||{};const o=i.name;s.form[o]=s.form[o]||{},s.form[o].allowEmptyValue=!0}}});function serialize(s,o){return o.includes(\"application/json\")?\"string\"==typeof s?s:(Array.isArray(s)&&(s=s.map((s=>{try{return JSON.parse(s)}catch(o){return s}}))),JSON.stringify(s)):String(s)}function grammar_grammar(){this.grammarObject=\"grammarObject\",this.rules=[],this.rules[0]={name:\"lenient-cookie-string\",lower:\"lenient-cookie-string\",index:0,isBkr:!1},this.rules[1]={name:\"lenient-cookie-entry\",lower:\"lenient-cookie-entry\",index:1,isBkr:!1},this.rules[2]={name:\"lenient-cookie-pair\",lower:\"lenient-cookie-pair\",index:2,isBkr:!1},this.rules[3]={name:\"lenient-cookie-pair-invalid\",lower:\"lenient-cookie-pair-invalid\",index:3,isBkr:!1},this.rules[4]={name:\"lenient-cookie-name\",lower:\"lenient-cookie-name\",index:4,isBkr:!1},this.rules[5]={name:\"lenient-cookie-value\",lower:\"lenient-cookie-value\",index:5,isBkr:!1},this.rules[6]={name:\"lenient-quoted-value\",lower:\"lenient-quoted-value\",index:6,isBkr:!1},this.rules[7]={name:\"lenient-quoted-char\",lower:\"lenient-quoted-char\",index:7,isBkr:!1},this.rules[8]={name:\"lenient-cookie-octet\",lower:\"lenient-cookie-octet\",index:8,isBkr:!1},this.rules[9]={name:\"cookie-string\",lower:\"cookie-string\",index:9,isBkr:!1},this.rules[10]={name:\"cookie-pair\",lower:\"cookie-pair\",index:10,isBkr:!1},this.rules[11]={name:\"cookie-name\",lower:\"cookie-name\",index:11,isBkr:!1},this.rules[12]={name:\"cookie-value\",lower:\"cookie-value\",index:12,isBkr:!1},this.rules[13]={name:\"cookie-octet\",lower:\"cookie-octet\",index:13,isBkr:!1},this.rules[14]={name:\"OWS\",lower:\"ows\",index:14,isBkr:!1},this.rules[15]={name:\"token\",lower:\"token\",index:15,isBkr:!1},this.rules[16]={name:\"tchar\",lower:\"tchar\",index:16,isBkr:!1},this.rules[17]={name:\"CHAR\",lower:\"char\",index:17,isBkr:!1},this.rules[18]={name:\"CTL\",lower:\"ctl\",index:18,isBkr:!1},this.rules[19]={name:\"separators\",lower:\"separators\",index:19,isBkr:!1},this.rules[20]={name:\"SP\",lower:\"sp\",index:20,isBkr:!1},this.rules[21]={name:\"HT\",lower:\"ht\",index:21,isBkr:!1},this.rules[22]={name:\"ALPHA\",lower:\"alpha\",index:22,isBkr:!1},this.rules[23]={name:\"DIGIT\",lower:\"digit\",index:23,isBkr:!1},this.rules[24]={name:\"DQUOTE\",lower:\"dquote\",index:24,isBkr:!1},this.rules[25]={name:\"WSP\",lower:\"wsp\",index:25,isBkr:!1},this.rules[26]={name:\"HTAB\",lower:\"htab\",index:26,isBkr:!1},this.rules[27]={name:\"CRLF\",lower:\"crlf\",index:27,isBkr:!1},this.rules[28]={name:\"CR\",lower:\"cr\",index:28,isBkr:!1},this.rules[29]={name:\"LF\",lower:\"lf\",index:29,isBkr:!1},this.udts=[],this.rules[0].opcodes=[],this.rules[0].opcodes[0]={type:2,children:[1,2]},this.rules[0].opcodes[1]={type:4,index:1},this.rules[0].opcodes[2]={type:3,min:0,max:1/0},this.rules[0].opcodes[3]={type:2,children:[4,5,6]},this.rules[0].opcodes[4]={type:7,string:[59]},this.rules[0].opcodes[5]={type:4,index:14},this.rules[0].opcodes[6]={type:4,index:1},this.rules[1].opcodes=[],this.rules[1].opcodes[0]={type:1,children:[1,2]},this.rules[1].opcodes[1]={type:4,index:2},this.rules[1].opcodes[2]={type:4,index:3},this.rules[2].opcodes=[],this.rules[2].opcodes[0]={type:2,children:[1,2,3,4,5,6,7]},this.rules[2].opcodes[1]={type:4,index:14},this.rules[2].opcodes[2]={type:4,index:4},this.rules[2].opcodes[3]={type:4,index:14},this.rules[2].opcodes[4]={type:7,string:[61]},this.rules[2].opcodes[5]={type:4,index:14},this.rules[2].opcodes[6]={type:4,index:5},this.rules[2].opcodes[7]={type:4,index:14},this.rules[3].opcodes=[],this.rules[3].opcodes[0]={type:2,children:[1,2,4]},this.rules[3].opcodes[1]={type:4,index:14},this.rules[3].opcodes[2]={type:3,min:1,max:1/0},this.rules[3].opcodes[3]={type:4,index:16},this.rules[3].opcodes[4]={type:4,index:14},this.rules[4].opcodes=[],this.rules[4].opcodes[0]={type:3,min:1,max:1/0},this.rules[4].opcodes[1]={type:1,children:[2,3,4]},this.rules[4].opcodes[2]={type:5,min:33,max:58},this.rules[4].opcodes[3]={type:6,string:[60]},this.rules[4].opcodes[4]={type:5,min:62,max:126},this.rules[5].opcodes=[],this.rules[5].opcodes[0]={type:1,children:[1,6]},this.rules[5].opcodes[1]={type:2,children:[2,3]},this.rules[5].opcodes[2]={type:4,index:6},this.rules[5].opcodes[3]={type:3,min:0,max:1},this.rules[5].opcodes[4]={type:3,min:0,max:1/0},this.rules[5].opcodes[5]={type:4,index:8},this.rules[5].opcodes[6]={type:3,min:0,max:1/0},this.rules[5].opcodes[7]={type:4,index:8},this.rules[6].opcodes=[],this.rules[6].opcodes[0]={type:2,children:[1,2,4]},this.rules[6].opcodes[1]={type:4,index:24},this.rules[6].opcodes[2]={type:3,min:0,max:1/0},this.rules[6].opcodes[3]={type:4,index:7},this.rules[6].opcodes[4]={type:4,index:24},this.rules[7].opcodes=[],this.rules[7].opcodes[0]={type:1,children:[1,2]},this.rules[7].opcodes[1]={type:5,min:32,max:33},this.rules[7].opcodes[2]={type:5,min:35,max:126},this.rules[8].opcodes=[],this.rules[8].opcodes[0]={type:1,children:[1,2,3]},this.rules[8].opcodes[1]={type:5,min:33,max:43},this.rules[8].opcodes[2]={type:5,min:45,max:58},this.rules[8].opcodes[3]={type:5,min:60,max:126},this.rules[9].opcodes=[],this.rules[9].opcodes[0]={type:2,children:[1,2]},this.rules[9].opcodes[1]={type:4,index:10},this.rules[9].opcodes[2]={type:3,min:0,max:1/0},this.rules[9].opcodes[3]={type:2,children:[4,5,6]},this.rules[9].opcodes[4]={type:7,string:[59]},this.rules[9].opcodes[5]={type:4,index:20},this.rules[9].opcodes[6]={type:4,index:10},this.rules[10].opcodes=[],this.rules[10].opcodes[0]={type:2,children:[1,2,3]},this.rules[10].opcodes[1]={type:4,index:11},this.rules[10].opcodes[2]={type:7,string:[61]},this.rules[10].opcodes[3]={type:4,index:12},this.rules[11].opcodes=[],this.rules[11].opcodes[0]={type:4,index:15},this.rules[12].opcodes=[],this.rules[12].opcodes[0]={type:1,children:[1,6]},this.rules[12].opcodes[1]={type:2,children:[2,3,5]},this.rules[12].opcodes[2]={type:4,index:24},this.rules[12].opcodes[3]={type:3,min:0,max:1/0},this.rules[12].opcodes[4]={type:4,index:13},this.rules[12].opcodes[5]={type:4,index:24},this.rules[12].opcodes[6]={type:3,min:0,max:1/0},this.rules[12].opcodes[7]={type:4,index:13},this.rules[13].opcodes=[],this.rules[13].opcodes[0]={type:1,children:[1,2,3,4,5]},this.rules[13].opcodes[1]={type:6,string:[33]},this.rules[13].opcodes[2]={type:5,min:35,max:43},this.rules[13].opcodes[3]={type:5,min:45,max:58},this.rules[13].opcodes[4]={type:5,min:60,max:91},this.rules[13].opcodes[5]={type:5,min:93,max:126},this.rules[14].opcodes=[],this.rules[14].opcodes[0]={type:3,min:0,max:1/0},this.rules[14].opcodes[1]={type:2,children:[2,4]},this.rules[14].opcodes[2]={type:3,min:0,max:1},this.rules[14].opcodes[3]={type:4,index:27},this.rules[14].opcodes[4]={type:4,index:25},this.rules[15].opcodes=[],this.rules[15].opcodes[0]={type:3,min:1,max:1/0},this.rules[15].opcodes[1]={type:4,index:16},this.rules[16].opcodes=[],this.rules[16].opcodes[0]={type:1,children:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]},this.rules[16].opcodes[1]={type:7,string:[33]},this.rules[16].opcodes[2]={type:7,string:[35]},this.rules[16].opcodes[3]={type:7,string:[36]},this.rules[16].opcodes[4]={type:7,string:[37]},this.rules[16].opcodes[5]={type:7,string:[38]},this.rules[16].opcodes[6]={type:7,string:[39]},this.rules[16].opcodes[7]={type:7,string:[42]},this.rules[16].opcodes[8]={type:7,string:[43]},this.rules[16].opcodes[9]={type:7,string:[45]},this.rules[16].opcodes[10]={type:7,string:[46]},this.rules[16].opcodes[11]={type:7,string:[94]},this.rules[16].opcodes[12]={type:7,string:[95]},this.rules[16].opcodes[13]={type:7,string:[96]},this.rules[16].opcodes[14]={type:7,string:[124]},this.rules[16].opcodes[15]={type:7,string:[126]},this.rules[16].opcodes[16]={type:4,index:23},this.rules[16].opcodes[17]={type:4,index:22},this.rules[17].opcodes=[],this.rules[17].opcodes[0]={type:5,min:1,max:127},this.rules[18].opcodes=[],this.rules[18].opcodes[0]={type:1,children:[1,2]},this.rules[18].opcodes[1]={type:5,min:0,max:31},this.rules[18].opcodes[2]={type:6,string:[127]},this.rules[19].opcodes=[],this.rules[19].opcodes[0]={type:1,children:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]},this.rules[19].opcodes[1]={type:7,string:[40]},this.rules[19].opcodes[2]={type:7,string:[41]},this.rules[19].opcodes[3]={type:7,string:[60]},this.rules[19].opcodes[4]={type:7,string:[62]},this.rules[19].opcodes[5]={type:7,string:[64]},this.rules[19].opcodes[6]={type:7,string:[44]},this.rules[19].opcodes[7]={type:7,string:[59]},this.rules[19].opcodes[8]={type:7,string:[58]},this.rules[19].opcodes[9]={type:7,string:[92]},this.rules[19].opcodes[10]={type:6,string:[34]},this.rules[19].opcodes[11]={type:7,string:[47]},this.rules[19].opcodes[12]={type:7,string:[91]},this.rules[19].opcodes[13]={type:7,string:[93]},this.rules[19].opcodes[14]={type:7,string:[63]},this.rules[19].opcodes[15]={type:7,string:[61]},this.rules[19].opcodes[16]={type:7,string:[123]},this.rules[19].opcodes[17]={type:7,string:[125]},this.rules[19].opcodes[18]={type:4,index:20},this.rules[19].opcodes[19]={type:4,index:21},this.rules[20].opcodes=[],this.rules[20].opcodes[0]={type:6,string:[32]},this.rules[21].opcodes=[],this.rules[21].opcodes[0]={type:6,string:[9]},this.rules[22].opcodes=[],this.rules[22].opcodes[0]={type:1,children:[1,2]},this.rules[22].opcodes[1]={type:5,min:65,max:90},this.rules[22].opcodes[2]={type:5,min:97,max:122},this.rules[23].opcodes=[],this.rules[23].opcodes[0]={type:5,min:48,max:57},this.rules[24].opcodes=[],this.rules[24].opcodes[0]={type:6,string:[34]},this.rules[25].opcodes=[],this.rules[25].opcodes[0]={type:1,children:[1,2]},this.rules[25].opcodes[1]={type:4,index:20},this.rules[25].opcodes[2]={type:4,index:26},this.rules[26].opcodes=[],this.rules[26].opcodes[0]={type:6,string:[9]},this.rules[27].opcodes=[],this.rules[27].opcodes[0]={type:2,children:[1,2]},this.rules[27].opcodes[1]={type:4,index:28},this.rules[27].opcodes[2]={type:4,index:29},this.rules[28].opcodes=[],this.rules[28].opcodes[0]={type:6,string:[13]},this.rules[29].opcodes=[],this.rules[29].opcodes[0]={type:6,string:[10]},this.toString=function toString(){let s=\"\";return s+=\"; Lenient version of https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1\\n\",s+='lenient-cookie-string        = lenient-cookie-entry *( \";\" OWS lenient-cookie-entry )\\n',s+=\"lenient-cookie-entry         = lenient-cookie-pair / lenient-cookie-pair-invalid\\n\",s+='lenient-cookie-pair          = OWS lenient-cookie-name OWS \"=\" OWS lenient-cookie-value OWS\\n',s+='lenient-cookie-pair-invalid  = OWS 1*tchar OWS ; Allow for standalone entries like \"fizz\" to be ignored\\n',s+='lenient-cookie-name          = 1*( %x21-3A / %x3C / %x3E-7E ) ; Allow all printable US-ASCII except \"=\"\\n',s+=\"lenient-cookie-value         = lenient-quoted-value [ *lenient-cookie-octet ] / *lenient-cookie-octet\\n\",s+=\"lenient-quoted-value         = DQUOTE *( lenient-quoted-char ) DQUOTE\\n\",s+=\"lenient-quoted-char          = %x20-21 / %x23-7E ; Allow all printable US-ASCII except DQUOTE\\n\",s+=\"lenient-cookie-octet         = %x21-2B / %x2D-3A / %x3C-7E\\n\",s+=\"                             ; Allow all printable characters except CTLs, semicolon and SP\\n\",s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1\\n\",s+='cookie-string     = cookie-pair *( \";\" SP cookie-pair )\\n',s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1\\n\",s+=\"; https://www.rfc-editor.org/errata/eid5518\\n\",s+='cookie-pair       = cookie-name \"=\" cookie-value\\n',s+=\"cookie-name       = token\\n\",s+=\"cookie-value      = ( DQUOTE *cookie-octet DQUOTE ) / *cookie-octet\\n\",s+=\"                  ; https://www.rfc-editor.org/errata/eid8242\\n\",s+=\"cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E\\n\",s+=\"                       ; US-ASCII characters excluding CTLs,\\n\",s+=\"                       ; whitespace, DQUOTE, comma, semicolon,\\n\",s+=\"                       ; and backslash\\n\",s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc6265#section-2.2\\n\",s+='OWS            = *( [ CRLF ] WSP ) ; \"optional\" whitespace\\n',s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2\\n\",s+=\"token          = 1*(tchar)\\n\",s+='tchar          = \"!\" / \"#\" / \"$\" / \"%\" / \"&\" / \"\\'\" / \"*\"\\n',s+='                 / \"+\" / \"-\" / \".\" / \"^\" / \"_\" / \"`\" / \"|\" / \"~\"\\n',s+=\"                 / DIGIT / ALPHA\\n\",s+=\"                 ; any VCHAR, except delimiters\\n\",s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc2616#section-2.2\\n\",s+=\"CHAR           = %x01-7F ; any US-ASCII character (octets 0 - 127)\\n\",s+=\"CTL            = %x00-1F / %x7F ; any US-ASCII control character\\n\",s+='separators     = \"(\" / \")\" / \"<\" / \">\" / \"@\" / \",\" / \";\" / \":\" / \"\\\\\" / %x22 / \"/\" / \"[\" / \"]\" / \"?\" / \"=\" / \"{\" / \"}\" / SP / HT\\n',s+=\"SP             = %x20 ; US-ASCII SP, space (32)\\n\",s+=\"HT             = %x09 ; US-ASCII HT, horizontal-tab (9)\\n\",s+=\"\\n\",s+=\"; https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1\\n\",s+=\"ALPHA          =  %x41-5A / %x61-7A ; A-Z / a-z\\n\",s+=\"DIGIT          =  %x30-39 ; 0-9\\n\",s+='DQUOTE         =  %x22 ; \" (Double Quote)\\n',s+=\"WSP            =  SP / HTAB ; white space\\n\",s+=\"HTAB           =  %x09 ; horizontal tab\\n\",s+=\"CRLF           =  CR LF ; Internet standard newline\\n\",s+=\"CR             =  %x0D ; carriage return\\n\",s+=\"LF             =  %x0A ; linefeed\\n\",'; Lenient version of https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1\\nlenient-cookie-string        = lenient-cookie-entry *( \";\" OWS lenient-cookie-entry )\\nlenient-cookie-entry         = lenient-cookie-pair / lenient-cookie-pair-invalid\\nlenient-cookie-pair          = OWS lenient-cookie-name OWS \"=\" OWS lenient-cookie-value OWS\\nlenient-cookie-pair-invalid  = OWS 1*tchar OWS ; Allow for standalone entries like \"fizz\" to be ignored\\nlenient-cookie-name          = 1*( %x21-3A / %x3C / %x3E-7E ) ; Allow all printable US-ASCII except \"=\"\\nlenient-cookie-value         = lenient-quoted-value [ *lenient-cookie-octet ] / *lenient-cookie-octet\\nlenient-quoted-value         = DQUOTE *( lenient-quoted-char ) DQUOTE\\nlenient-quoted-char          = %x20-21 / %x23-7E ; Allow all printable US-ASCII except DQUOTE\\nlenient-cookie-octet         = %x21-2B / %x2D-3A / %x3C-7E\\n                             ; Allow all printable characters except CTLs, semicolon and SP\\n\\n; https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1\\ncookie-string     = cookie-pair *( \";\" SP cookie-pair )\\n\\n; https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1\\n; https://www.rfc-editor.org/errata/eid5518\\ncookie-pair       = cookie-name \"=\" cookie-value\\ncookie-name       = token\\ncookie-value      = ( DQUOTE *cookie-octet DQUOTE ) / *cookie-octet\\n                  ; https://www.rfc-editor.org/errata/eid8242\\ncookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E\\n                       ; US-ASCII characters excluding CTLs,\\n                       ; whitespace, DQUOTE, comma, semicolon,\\n                       ; and backslash\\n\\n; https://datatracker.ietf.org/doc/html/rfc6265#section-2.2\\nOWS            = *( [ CRLF ] WSP ) ; \"optional\" whitespace\\n\\n; https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2\\ntoken          = 1*(tchar)\\ntchar          = \"!\" / \"#\" / \"$\" / \"%\" / \"&\" / \"\\'\" / \"*\"\\n                 / \"+\" / \"-\" / \".\" / \"^\" / \"_\" / \"`\" / \"|\" / \"~\"\\n                 / DIGIT / ALPHA\\n                 ; any VCHAR, except delimiters\\n\\n; https://datatracker.ietf.org/doc/html/rfc2616#section-2.2\\nCHAR           = %x01-7F ; any US-ASCII character (octets 0 - 127)\\nCTL            = %x00-1F / %x7F ; any US-ASCII control character\\nseparators     = \"(\" / \")\" / \"<\" / \">\" / \"@\" / \",\" / \";\" / \":\" / \"\\\\\" / %x22 / \"/\" / \"[\" / \"]\" / \"?\" / \"=\" / \"{\" / \"}\" / SP / HT\\nSP             = %x20 ; US-ASCII SP, space (32)\\nHT             = %x09 ; US-ASCII HT, horizontal-tab (9)\\n\\n; https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1\\nALPHA          =  %x41-5A / %x61-7A ; A-Z / a-z\\nDIGIT          =  %x30-39 ; 0-9\\nDQUOTE         =  %x22 ; \" (Double Quote)\\nWSP            =  SP / HTAB ; white space\\nHTAB           =  %x09 ; horizontal tab\\nCRLF           =  CR LF ; Internet standard newline\\nCR             =  %x0D ; carriage return\\nLF             =  %x0A ; linefeed\\n'}}new grammar_grammar;const utils_percentEncodeChar=s=>{if(\"string\"!=typeof s||1!==[...s].length)throw new TypeError(\"Input must be a single character string.\");const o=s.codePointAt(0);return o<=127?`%${o.toString(16).toUpperCase().padStart(2,\"0\")}`:encodeURIComponent(s)},utils_isQuoted=s=>s.length>=2&&s.startsWith('\"')&&s.endsWith('\"'),utils_unquote=s=>utils_isQuoted(s)?s.slice(1,-1):s,utils_quote=s=>`\"${s}\"`,utils_identity=s=>s,Bx=new kp,$x=new grammar_grammar,test_cookie_value=(s,{strict:o=!0,quoted:i=null}={})=>{try{const a=o?\"cookie-value\":\"lenient-cookie-value\",u=Bx.parse($x,a,s);return\"boolean\"==typeof i?u.success&&i===utils_isQuoted(s):u.success}catch{return!1}},base64_browser=s=>{const o=(new TextEncoder).encode(s).reduce(((s,o)=>s+String.fromCharCode(o)),\"\");return btoa(o)},cookie_value_strict_base64=(s,o=base64_browser)=>{const i=String(s);if(test_cookie_value(i))return i;const a=utils_isQuoted(i),u=o(a?utils_unquote(i):i);return a?utils_quote(u):u},base64url_browser=s=>(s=>s.replace(/\\+/g,\"-\").replace(/\\//g,\"_\").replace(/=+$/g,\"\"))(base64_browser(s)),cookie_value_strict_base64url=s=>cookie_value_strict_base64(s,base64url_browser),qx=new kp,Ux=new grammar_grammar,test_cookie_name=(s,{strict:o=!0}={})=>{try{const i=o?\"cookie-name\":\"lenient-cookie-name\";return qx.parse(Ux,i,s).success}catch{return!1}},cookie_name_strict=s=>{if(!test_cookie_name(s))throw new TypeError(`Invalid cookie name: ${s}`)},cookie_value_strict=s=>{if(!test_cookie_value(s))throw new TypeError(`Invalid cookie value: ${s}`)},Vx={encoders:{name:utils_identity,value:cookie_value_strict_base64url},validators:{name:cookie_name_strict,value:cookie_value_strict}},set_cookie_serialize=(s,o,i={})=>{const a={...Vx,...i,encoders:{...Vx.encoders,...i.encoders},validators:{...Vx.validators,...i.validators}},u=a.encoders.name(s),_=a.encoders.value(o);return a.validators.name(u),a.validators.value(_),`${u}=${_}`},cookie_serialize=(s,o={})=>(Array.isArray(s)?s:\"object\"==typeof s&&null!==s?Object.entries(s):[]).map((([s,i])=>set_cookie_serialize(s,i,o))).join(\"; \"),zx=new kp,Wx=new grammar_grammar,cookie_value_strict_percent=s=>{const o=String(s);if(test_cookie_value(o))return o;const i=utils_isQuoted(o),a=i?utils_unquote(o):o;let u=\"\";for(const s of a)u+=zx.parse(Wx,\"cookie-octet\",s).success?s:utils_percentEncodeChar(s);return i?utils_quote(u):u},Jx=(new kp,new grammar_grammar,s=>{if(!test_cookie_name(s,{strict:!1}))throw new TypeError(`Invalid cookie name: ${s}`)}),valuePercentEncoder=s=>cookie_value_strict_percent(s).replace(/[=&]/gu,(s=>\"=\"===s?\"%3D\":\"%26\")),helpers_cookie_serialize=(s,o={})=>cookie_serialize(s,up({encoders:{name:utils_identity,value:valuePercentEncoder},validators:{name:Jx,value:cookie_value_strict}},o));function parameter_builders_path({req:s,value:o,parameter:i,baseURL:a}){const{name:u,style:_,explode:w,content:x}=i;if(void 0===o)return;const C=s.url.replace(a,\"\");let j;if(x){const s=Object.keys(x)[0];j=es_resolve(C,{[u]:o},{encoder:o=>encodeCharacters(serialize(o,s))})}else j=es_resolve(C,{[u]:o},{encoder:s=>stylize({key:i.name,value:s,style:_||\"simple\",explode:null!=w&&w,escape:\"reserved\"})});s.url=a+j}function query({req:s,value:o,parameter:i}){if(s.query=s.query||{},void 0!==o&&i.content){const a=serialize(o,Object.keys(i.content)[0]);if(a)s.query[i.name]=a;else if(i.allowEmptyValue){const o=i.name;s.query[o]=s.query[o]||{},s.query[o].allowEmptyValue=!0}}else if(!1===o&&(o=\"false\"),0===o&&(o=\"0\"),o){const{style:a,explode:u,allowReserved:_}=i;s.query[i.name]={value:o,serializationOption:{style:a,explode:u,allowReserved:_}}}else if(i.allowEmptyValue&&void 0!==o){const o=i.name;s.query[o]=s.query[o]||{},s.query[o].allowEmptyValue=!0}}const Hx=[\"accept\",\"authorization\",\"content-type\"];function parameter_builders_header({req:s,parameter:o,value:i}){if(s.headers=s.headers||{},!(Hx.indexOf(o.name.toLowerCase())>-1))if(void 0!==i&&o.content){const a=Object.keys(o.content)[0];s.headers[o.name]=serialize(i,a)}else void 0===i||Array.isArray(i)&&0===i.length||(s.headers[o.name]=stylize({key:o.name,value:i,style:o.style||\"simple\",explode:void 0!==o.explode&&o.explode,escape:!1}))}function cookie({req:s,parameter:o,value:i}){const{name:a}=o;if(s.headers=s.headers||{},void 0!==i&&o.content){const u=serialize(i,Object.keys(o.content)[0]);s.headers.Cookie=helpers_cookie_serialize({[a]:u})}else if(void 0!==i&&(!Array.isArray(i)||0!==i.length)){var u;const _=stylize({key:o.name,value:i,escape:!1,style:o.style||\"form\",explode:null!==(u=o.explode)&&void 0!==u&&u}),w=Array.isArray(i)&&o.explode?`${a}=${_}`:_;s.headers.Cookie=helpers_cookie_serialize({[a]:w})}}const Kx=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof self?self:window,{btoa:Gx}=Kx,Yx=Gx;function buildRequest(s,o){const{operation:i,requestBody:a,securities:u,spec:_,attachContentTypeForEmptyPayload:w}=s;let{requestContentType:x}=s;o=function applySecurities({request:s,securities:o={},operation:i={},spec:a}){var u;const _={...s},{authorized:w={}}=o,x=i.security||a.security||[],C=w&&!!Object.keys(w).length,j=(null==a||null===(u=a.components)||void 0===u?void 0:u.securitySchemes)||{};if(_.headers=_.headers||{},_.query=_.query||{},!Object.keys(o).length||!C||!x||Array.isArray(i.security)&&!i.security.length)return s;return x.forEach((s=>{Object.keys(s).forEach((s=>{const o=w[s],i=j[s];if(!o)return;const a=o.value||o,{type:u}=i;if(o)if(\"apiKey\"===u)\"query\"===i.in&&(_.query[i.name]=a),\"header\"===i.in&&(_.headers[i.name]=a),\"cookie\"===i.in&&(_.cookies[i.name]=a);else if(\"http\"===u){if(/^basic$/i.test(i.scheme)){const s=a.username||\"\",o=a.password||\"\",i=Yx(`${s}:${o}`);_.headers.Authorization=`Basic ${i}`}/^bearer$/i.test(i.scheme)&&(_.headers.Authorization=`Bearer ${a}`)}else if(\"oauth2\"===u||\"openIdConnect\"===u){const s=o.token||{},a=s[i[\"x-tokenName\"]||\"access_token\"];let u=s.token_type;u&&\"bearer\"!==u.toLowerCase()||(u=\"Bearer\"),_.headers.Authorization=`${u} ${a}`}}))})),_}({request:o,securities:u,operation:i,spec:_});const C=i.requestBody||{},j=Object.keys(C.content||{}),L=x&&j.indexOf(x)>-1;if(a||w){if(x&&L)o.headers[\"Content-Type\"]=x;else if(!x){const s=j[0];s&&(o.headers[\"Content-Type\"]=s,x=s)}}else x&&L&&(o.headers[\"Content-Type\"]=x);if(!s.responseContentType&&i.responses){const s=Object.entries(i.responses).filter((([s,o])=>{const i=parseInt(s,10);return i>=200&&i<300&&fu(o.content)})).reduce(((s,[,o])=>s.concat(Object.keys(o.content))),[]);s.length>0&&(o.headers.accept=s.join(\", \"))}if(a)if(x){if(j.indexOf(x)>-1)if(\"application/x-www-form-urlencoded\"===x||\"multipart/form-data\"===x)if(\"object\"==typeof a){var B,$;const s=null!==(B=null===($=C.content[x])||void 0===$?void 0:$.encoding)&&void 0!==B?B:{};o.form={},Object.keys(a).forEach((i=>{let u;try{u=JSON.parse(a[i])}catch{u=a[i]}o.form[i]={value:u,encoding:s[i]||{}}}))}else if(\"string\"==typeof a){var U,V;const s=null!==(U=null===(V=C.content[x])||void 0===V?void 0:V.encoding)&&void 0!==U?U:{};try{o.form={};const i=JSON.parse(a);Object.entries(i).forEach((([i,a])=>{o.form[i]={value:a,encoding:s[i]||{}}}))}catch{o.form=a}}else o.form=a;else o.body=a}else o.body=a;return o}function build_request_buildRequest(s,o){const{spec:i,operation:a,securities:u,requestContentType:_,responseContentType:w,attachContentTypeForEmptyPayload:x}=s;if(o=function build_request_applySecurities({request:s,securities:o={},operation:i={},spec:a}){const u={...s},{authorized:_={},specSecurity:w=[]}=o,x=i.security||w,C=_&&!!Object.keys(_).length,j=a.securityDefinitions;if(u.headers=u.headers||{},u.query=u.query||{},!Object.keys(o).length||!C||!x||Array.isArray(i.security)&&!i.security.length)return s;return x.forEach((s=>{Object.keys(s).forEach((s=>{const o=_[s];if(!o)return;const{token:i}=o,a=o.value||o,w=j[s],{type:x}=w,C=w[\"x-tokenName\"]||\"access_token\",L=i&&i[C];let B=i&&i.token_type;if(o)if(\"apiKey\"===x){const s=\"query\"===w.in?\"query\":\"headers\";u[s]=u[s]||{},u[s][w.name]=a}else if(\"basic\"===x)if(a.header)u.headers.authorization=a.header;else{const s=a.username||\"\",o=a.password||\"\";a.base64=Yx(`${s}:${o}`),u.headers.authorization=`Basic ${a.base64}`}else\"oauth2\"===x&&L&&(B=B&&\"bearer\"!==B.toLowerCase()?B:\"Bearer\",u.headers.authorization=`${B} ${L}`)}))})),u}({request:o,securities:u,operation:a,spec:i}),o.body||o.form||x)_?o.headers[\"Content-Type\"]=_:Array.isArray(a.consumes)?[o.headers[\"Content-Type\"]]=a.consumes:Array.isArray(i.consumes)?[o.headers[\"Content-Type\"]]=i.consumes:a.parameters&&a.parameters.filter((s=>\"file\"===s.type)).length?o.headers[\"Content-Type\"]=\"multipart/form-data\":a.parameters&&a.parameters.filter((s=>\"formData\"===s.in)).length&&(o.headers[\"Content-Type\"]=\"application/x-www-form-urlencoded\");else if(_){const s=a.parameters&&a.parameters.filter((s=>\"body\"===s.in)).length>0,i=a.parameters&&a.parameters.filter((s=>\"formData\"===s.in)).length>0;(s||i)&&(o.headers[\"Content-Type\"]=_)}return!w&&Array.isArray(a.produces)&&a.produces.length>0&&(o.headers.accept=a.produces.join(\", \")),o}function idFromPathMethodLegacy(s,o){return`${o.toLowerCase()}-${s}`}const arrayOrEmpty=s=>Array.isArray(s)?s:[],findObjectOrArraySchema=(s,{recurse:o=!0,depth:i=1}={})=>{if(fu(s)){if(\"object\"===s.type||\"array\"===s.type||Array.isArray(s.type)&&(s.type.includes(\"object\")||s.type.includes(\"array\")))return s;if(!(i>Bl)&&o){const a=Array.isArray(s.oneOf)?s.oneOf.find((s=>findObjectOrArraySchema(s,{recurse:o,depth:i+1}))):void 0;if(a)return a;const u=Array.isArray(s.anyOf)?s.anyOf.find((s=>findObjectOrArraySchema(s,{recurse:o,depth:i+1}))):void 0;if(u)return u}}},parseJsonObjectOrArray=({value:s,silentFail:o=!1})=>{try{const i=JSON.parse(s);if(fu(i)||Array.isArray(i))return i;if(!o)throw new Error(\"Expected JSON serialized object or array\")}catch{if(!o)throw new Error(\"Could not parse parameter value string as JSON Object or JSON Array\")}return s},parseURIReference=s=>{try{return new URL(s)}catch{const o=new URL(s,Ll),i=String(s).startsWith(\"/\")?o.pathname:o.pathname.substring(1);return{hash:o.hash,host:\"\",hostname:\"\",href:\"\",origin:\"\",password:\"\",pathname:i,port:\"\",protocol:\"\",search:o.search,searchParams:o.searchParams}}};class OperationNotFoundError extends Go{}const Xx={buildRequest:execute_buildRequest};function execute_execute({http:s,fetch:o,spec:i,operationId:a,pathName:u,method:_,parameters:w,securities:x,...C}){const j=s||o||http_http;u&&_&&!a&&(a=idFromPathMethodLegacy(u,_));const L=Xx.buildRequest({spec:i,operationId:a,parameters:w,securities:x,http:j,...C});return L.body&&(fu(L.body)||Array.isArray(L.body))&&(L.body=JSON.stringify(L.body)),j(L)}function execute_buildRequest(s){const{spec:o,operationId:i,responseContentType:a,scheme:u,requestInterceptor:_,responseInterceptor:w,contextUrl:x,userFetch:C,server:j,serverVariables:L,http:B,signal:$,serverVariableEncoder:U}=s;let{parameters:V,parameterBuilders:z,baseURL:Y}=s;const Z=isOpenAPI3(o);z||(z=Z?be:Fx);let ee={url:\"\",credentials:B&&B.withCredentials?\"include\":\"same-origin\",headers:{},cookies:{}};$&&(ee.signal=$),_&&(ee.requestInterceptor=_),w&&(ee.responseInterceptor=w),C&&(ee.userFetch=C);const ie=function getOperationRaw(s,o){return s&&s.paths?function findOperation(s,o){return function eachOperation(s,o,i){if(!s||\"object\"!=typeof s||!s.paths||\"object\"!=typeof s.paths)return null;const{paths:a}=s;for(const u in a)for(const _ in a[u]){if(\"PARAMETERS\"===_.toUpperCase())continue;const w=a[u][_];if(!w||\"object\"!=typeof w)continue;const x={spec:s,pathName:u,method:_.toUpperCase(),operation:w},C=o(x);if(i&&C)return x}}(s,o,!0)||null}(s,(({pathName:s,method:i,operation:a})=>{if(!a||\"object\"!=typeof a)return!1;const u=a.operationId;return[opId(a,s,i),idFromPathMethodLegacy(s,i),u].some((s=>s&&s===o))})):null}(o,i);if(!ie)throw new OperationNotFoundError(`Operation ${i} not found`);const{operation:ae={},method:ce,pathName:le}=ie;if(Y=null!=Y?Y:function baseUrl(s){const o=isOpenAPI3(s.spec);return o?function oas3BaseUrl({spec:s,pathName:o,method:i,server:a,contextUrl:u,serverVariables:_={},serverVariableEncoder:w}){var x,C;let j,L=[],B=\"\";const $=null==s||null===(x=s.paths)||void 0===x||null===(x=x[o])||void 0===x||null===(x=x[(i||\"\").toLowerCase()])||void 0===x?void 0:x.servers,U=null==s||null===(C=s.paths)||void 0===C||null===(C=C[o])||void 0===C?void 0:C.servers,V=null==s?void 0:s.servers;L=isNonEmptyServerList($)?$:isNonEmptyServerList(U)?U:isNonEmptyServerList(V)?V:[Fl],a&&(j=L.find((s=>s.url===a)),j&&(B=a));B||([j]=L,B=j.url);if(openapi_server_url_templating_es_test(B,{strict:!0})){const s=Object.entries({...j.variables}).reduce(((s,[o,i])=>(s[o]=i.default,s)),{});B=es_substitute(B,{...s,..._},{encoder:\"function\"==typeof w?w:yw})}return function buildOas3UrlWithContext(s=\"\",o=\"\"){const i=parseURIReference(s&&o?resolve(o,s):s),a=parseURIReference(o),u=stripNonAlpha(i.protocol)||stripNonAlpha(a.protocol),_=i.host||a.host,w=i.pathname;let x;x=u&&_?`${u}://${_+w}`:w;return\"/\"===x[x.length-1]?x.slice(0,-1):x}(B,u)}(s):function swagger2BaseUrl({spec:s,scheme:o,contextUrl:i=\"\"}){const a=parseURIReference(i),u=Array.isArray(s.schemes)?s.schemes[0]:null,_=o||u||stripNonAlpha(a.protocol)||\"http\",w=s.host||a.host||\"\",x=s.basePath||\"\";let C;C=_&&w?`${_}://${w+x}`:x;return\"/\"===C[C.length-1]?C.slice(0,-1):C}(s)}({spec:o,scheme:u,contextUrl:x,server:j,serverVariables:L,pathName:le,method:ce,serverVariableEncoder:U}),ee.url+=Y,!i)return delete ee.cookies,ee;ee.url+=le,ee.method=`${ce}`.toUpperCase(),V=V||{};const pe=o.paths[le]||{};a&&(ee.headers.accept=a);const de=(s=>{const o={};s.forEach((s=>{o[s.in]||(o[s.in]={}),o[s.in][s.name]=s}));const i=[];return Object.keys(o).forEach((s=>{Object.keys(o[s]).forEach((a=>{i.push(o[s][a])}))})),i})([].concat(arrayOrEmpty(ae.parameters)).concat(arrayOrEmpty(pe.parameters)));de.forEach((s=>{const i=z[s.in];let a;if(\"body\"===s.in&&s.schema&&s.schema.properties&&(a=V),a=s&&s.name&&V[s.name],void 0===a?a=s&&s.name&&V[`${s.in}.${s.name}`]:((s,o)=>o.filter((o=>o.name===s)))(s.name,de).length>1&&console.warn(`Parameter '${s.name}' is ambiguous because the defined spec has more than one parameter with the name: '${s.name}' and the passed-in parameter values did not define an 'in' value.`),null!==a){if(void 0!==s.default&&void 0===a&&(a=s.default),void 0===a&&s.required&&!s.allowEmptyValue)throw new Error(`Required parameter ${s.name} is not provided`);Z&&\"string\"==typeof a&&(Yu(\"type\",s.schema)&&\"string\"==typeof s.schema.type&&findObjectOrArraySchema(s.schema,{recurse:!1})?a=parseJsonObjectOrArray({value:a,silentFail:!1}):(Yu(\"type\",s.schema)&&Array.isArray(s.schema.type)&&findObjectOrArraySchema(s.schema,{recurse:!1})||!Yu(\"type\",s.schema)&&findObjectOrArraySchema(s.schema,{recurse:!0}))&&(a=parseJsonObjectOrArray({value:a,silentFail:!0}))),i&&i({req:ee,parameter:s,value:a,operation:ae,spec:o,baseURL:Y})}}));const fe={...s,operation:ae};if(ee=Z?buildRequest(fe,ee):build_request_buildRequest(fe,ee),ee.cookies&&Object.keys(ee.cookies).length>0){const s=helpers_cookie_serialize(ee.cookies);Id(ee.headers.Cookie)?ee.headers.Cookie+=`; ${s}`:ee.headers.Cookie=s}return ee.cookies&&delete ee.cookies,serializeRequest(ee)}const stripNonAlpha=s=>s?s.replace(/\\W/g,\"\"):null;const isNonEmptyServerList=s=>Array.isArray(s)&&s.length>0;const makeResolveSubtree=s=>async(o,i,a={})=>(async(s,o,i={})=>{const{returnEntireTree:a,baseDoc:u,requestInterceptor:_,responseInterceptor:w,parameterMacro:x,modelPropertyMacro:C,useCircularStructures:j,strategies:L}=i,B={spec:s,pathDiscriminator:o,baseDoc:u,requestInterceptor:_,responseInterceptor:w,parameterMacro:x,modelPropertyMacro:C,useCircularStructures:j,strategies:L},$=L.find((o=>o.match(s))).normalize(s),U=await Nx({spec:$,...B,allowMetaPatches:!0,skipNormalization:!isOpenAPI31(s)});return!a&&Array.isArray(o)&&o.length&&(U.spec=o.reduce(((s,o)=>null==s?void 0:s[o]),U.spec)||null),U})(o,i,{...s,...a}),Qx=(makeResolveSubtree({strategies:[_u,vu,gu]}),(s,o)=>(...i)=>{s(...i);const a=o.getConfigs().withCredentials;o.fn.fetch.withCredentials=a});function swagger_client({configs:s,getConfigs:o}){return{fn:{fetch:(i=http_http,a=s.preFetch,u=s.postFetch,u=u||(s=>s),a=a||(s=>s),s=>(\"string\"==typeof s&&(s={url:s}),s=serializeRequest(s),s=a(s),u(i(s)))),buildRequest:execute_buildRequest,execute:execute_execute,resolve:makeResolve({strategies:[Tx,_u,vu,gu]}),resolveSubtree:async(s,i,a={})=>{const u=o(),_={modelPropertyMacro:u.modelPropertyMacro,parameterMacro:u.parameterMacro,requestInterceptor:u.requestInterceptor,responseInterceptor:u.responseInterceptor,strategies:[Tx,_u,vu,gu]};return makeResolveSubtree(_)(s,i,a)},serializeRes:serializeResponse,opId},statePlugins:{configs:{wrapActions:{loaded:Qx}}}};var i,a,u}function util(){return{fn:{shallowEqualKeys,sanitizeUrl}}}var Zx=__webpack_require__(40961),tk=(__webpack_require__(78418),Re.version.startsWith(\"19\")),rk=Symbol.for(tk?\"react.transitional.element\":\"react.element\"),nk=Symbol.for(\"react.portal\"),sk=Symbol.for(\"react.fragment\"),ok=Symbol.for(\"react.strict_mode\"),lk=Symbol.for(\"react.profiler\"),uk=Symbol.for(\"react.consumer\"),pk=Symbol.for(\"react.context\"),fk=Symbol.for(\"react.forward_ref\"),mk=Symbol.for(\"react.suspense\"),yk=Symbol.for(\"react.suspense_list\"),vk=Symbol.for(\"react.memo\"),_k=Symbol.for(\"react.lazy\"),wk=fk,xk=vk;function typeOf(s){if(\"object\"==typeof s&&null!==s){const{$$typeof:o}=s;switch(o){case rk:switch(s=s.type){case sk:case lk:case ok:case mk:case yk:return s;default:switch(s=s&&s.$$typeof){case pk:case fk:case _k:case vk:case uk:return s;default:return o}}case nk:return o}}}function pureFinalPropsSelectorFactory(s,o,i,a,{areStatesEqual:u,areOwnPropsEqual:_,areStatePropsEqual:w}){let x,C,j,L,B,$=!1;function handleSubsequentCalls($,U){const V=!_(U,C),z=!u($,x,U,C);return x=$,C=U,V&&z?function handleNewPropsAndNewState(){return j=s(x,C),o.dependsOnOwnProps&&(L=o(a,C)),B=i(j,L,C),B}():V?function handleNewProps(){return s.dependsOnOwnProps&&(j=s(x,C)),o.dependsOnOwnProps&&(L=o(a,C)),B=i(j,L,C),B}():z?function handleNewState(){const o=s(x,C),a=!w(o,j);return j=o,a&&(B=i(j,L,C)),B}():B}return function pureFinalPropsSelector(u,_){return $?handleSubsequentCalls(u,_):function handleFirstCall(u,_){return x=u,C=_,j=s(x,C),L=o(a,C),B=i(j,L,C),$=!0,B}(u,_)}}function wrapMapToPropsConstant(s){return function initConstantSelector(o){const i=s(o);function constantSelector(){return i}return constantSelector.dependsOnOwnProps=!1,constantSelector}}function getDependsOnOwnProps(s){return s.dependsOnOwnProps?Boolean(s.dependsOnOwnProps):1!==s.length}function wrapMapToPropsFunc(s,o){return function initProxySelector(o,{displayName:i}){const a=function mapToPropsProxy(s,o){return a.dependsOnOwnProps?a.mapToProps(s,o):a.mapToProps(s,void 0)};return a.dependsOnOwnProps=!0,a.mapToProps=function detectFactoryAndVerify(o,i){a.mapToProps=s,a.dependsOnOwnProps=getDependsOnOwnProps(s);let u=a(o,i);return\"function\"==typeof u&&(a.mapToProps=u,a.dependsOnOwnProps=getDependsOnOwnProps(u),u=a(o,i)),u},a}}function createInvalidArgFactory(s,o){return(i,a)=>{throw new Error(`Invalid value of type ${typeof s} for ${o} argument when connecting component ${a.wrappedComponentName}.`)}}function defaultMergeProps(s,o,i){return{...i,...s,...o}}function defaultNoopBatch(s){s()}var Ak={notify(){},get:()=>[]};function createSubscription(s,o){let i,a=Ak,u=0,_=!1;function handleChangeWrapper(){w.onStateChange&&w.onStateChange()}function trySubscribe(){u++,i||(i=o?o.addNestedSub(handleChangeWrapper):s.subscribe(handleChangeWrapper),a=function createListenerCollection(){let s=null,o=null;return{clear(){s=null,o=null},notify(){defaultNoopBatch((()=>{let o=s;for(;o;)o.callback(),o=o.next}))},get(){const o=[];let i=s;for(;i;)o.push(i),i=i.next;return o},subscribe(i){let a=!0;const u=o={callback:i,next:null,prev:o};return u.prev?u.prev.next=u:s=u,function unsubscribe(){a&&null!==s&&(a=!1,u.next?u.next.prev=u.prev:o=u.prev,u.prev?u.prev.next=u.next:s=u.next)}}}}())}function tryUnsubscribe(){u--,i&&0===u&&(i(),i=void 0,a.clear(),a=Ak)}const w={addNestedSub:function addNestedSub(s){trySubscribe();const o=a.subscribe(s);let i=!1;return()=>{i||(i=!0,o(),tryUnsubscribe())}},notifyNestedSubs:function notifyNestedSubs(){a.notify()},handleChangeWrapper,isSubscribed:function isSubscribed(){return _},trySubscribe:function trySubscribeSelf(){_||(_=!0,trySubscribe())},tryUnsubscribe:function tryUnsubscribeSelf(){_&&(_=!1,tryUnsubscribe())},getListeners:()=>a};return w}var Bk=(()=>!(\"undefined\"==typeof window||void 0===window.document||void 0===window.document.createElement))(),qk=(()=>\"undefined\"!=typeof navigator&&\"ReactNative\"===navigator.product)(),Vk=(()=>Bk||qk?Re.useLayoutEffect:Re.useEffect)();function is(s,o){return s===o?0!==s||0!==o||1/s==1/o:s!=s&&o!=o}function shallowEqual(s,o){if(is(s,o))return!0;if(\"object\"!=typeof s||null===s||\"object\"!=typeof o||null===o)return!1;const i=Object.keys(s),a=Object.keys(o);if(i.length!==a.length)return!1;for(let a=0;a<i.length;a++)if(!Object.prototype.hasOwnProperty.call(o,i[a])||!is(s[i[a]],o[i[a]]))return!1;return!0}var zk={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},eO={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},tO={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},rO={[wk]:{$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},[xk]:tO};function getStatics(s){return function isMemo(s){return typeOf(s)===vk}(s)?tO:rO[s.$$typeof]||zk}var nO=Object.defineProperty,sO=Object.getOwnPropertyNames,oO=Object.getOwnPropertySymbols,iO=Object.getOwnPropertyDescriptor,aO=Object.getPrototypeOf,cO=Object.prototype;function hoistNonReactStatics(s,o){if(\"string\"!=typeof o){if(cO){const i=aO(o);i&&i!==cO&&hoistNonReactStatics(s,i)}let i=sO(o);oO&&(i=i.concat(oO(o)));const a=getStatics(s),u=getStatics(o);for(let _=0;_<i.length;++_){const w=i[_];if(!(eO[w]||u&&u[w]||a&&a[w])){const i=iO(o,w);try{nO(s,w,i)}catch(s){}}}}return s}var lO=Symbol.for(\"react-redux-context\"),uO=\"undefined\"!=typeof globalThis?globalThis:{};function getContext(){if(!Re.createContext)return{};const s=uO[lO]??=new Map;let o=s.get(Re.createContext);return o||(o=Re.createContext(null),s.set(Re.createContext,o)),o}var pO=getContext(),hO=[null,null];function captureWrapperProps(s,o,i,a,u,_){s.current=a,i.current=!1,u.current&&(u.current=null,_())}function strictEqual(s,o){return s===o}var dO=function connect(s,o,i,{pure:a,areStatesEqual:u=strictEqual,areOwnPropsEqual:_=shallowEqual,areStatePropsEqual:w=shallowEqual,areMergedPropsEqual:x=shallowEqual,forwardRef:C=!1,context:j=pO}={}){const L=j,B=function mapStateToPropsFactory(s){return s?\"function\"==typeof s?wrapMapToPropsFunc(s):createInvalidArgFactory(s,\"mapStateToProps\"):wrapMapToPropsConstant((()=>({})))}(s),$=function mapDispatchToPropsFactory(s){return s&&\"object\"==typeof s?wrapMapToPropsConstant((o=>function react_redux_bindActionCreators(s,o){const i={};for(const a in s){const u=s[a];\"function\"==typeof u&&(i[a]=(...s)=>o(u(...s)))}return i}(s,o))):s?\"function\"==typeof s?wrapMapToPropsFunc(s):createInvalidArgFactory(s,\"mapDispatchToProps\"):wrapMapToPropsConstant((s=>({dispatch:s})))}(o),U=function mergePropsFactory(s){return s?\"function\"==typeof s?function wrapMergePropsFunc(s){return function initMergePropsProxy(o,{displayName:i,areMergedPropsEqual:a}){let u,_=!1;return function mergePropsProxy(o,i,w){const x=s(o,i,w);return _?a(x,u)||(u=x):(_=!0,u=x),u}}}(s):createInvalidArgFactory(s,\"mergeProps\"):()=>defaultMergeProps}(i),V=Boolean(s);return s=>{const o=s.displayName||s.name||\"Component\",i=`Connect(${o})`,a={shouldHandleStateChanges:V,displayName:i,wrappedComponentName:o,WrappedComponent:s,initMapStateToProps:B,initMapDispatchToProps:$,initMergeProps:U,areStatesEqual:u,areStatePropsEqual:w,areOwnPropsEqual:_,areMergedPropsEqual:x};function ConnectFunction(o){const[i,u,_]=Re.useMemo((()=>{const{reactReduxForwardedRef:s,...i}=o;return[o.context,s,i]}),[o]),w=Re.useMemo((()=>L),[i,L]),x=Re.useContext(w),C=Boolean(o.store)&&Boolean(o.store.getState)&&Boolean(o.store.dispatch),j=Boolean(x)&&Boolean(x.store);const B=C?o.store:x.store,$=j?x.getServerState:B.getState,U=Re.useMemo((()=>function finalPropsSelectorFactory(s,{initMapStateToProps:o,initMapDispatchToProps:i,initMergeProps:a,...u}){return pureFinalPropsSelectorFactory(o(s,u),i(s,u),a(s,u),s,u)}(B.dispatch,a)),[B]),[z,Y]=Re.useMemo((()=>{if(!V)return hO;const s=createSubscription(B,C?void 0:x.subscription),o=s.notifyNestedSubs.bind(s);return[s,o]}),[B,C,x]),Z=Re.useMemo((()=>C?x:{...x,subscription:z}),[C,x,z]),ee=Re.useRef(void 0),ie=Re.useRef(_),ae=Re.useRef(void 0),ce=Re.useRef(!1),le=Re.useRef(!1),pe=Re.useRef(void 0);Vk((()=>(le.current=!0,()=>{le.current=!1})),[]);const de=Re.useMemo((()=>()=>ae.current&&_===ie.current?ae.current:U(B.getState(),_)),[B,_]),fe=Re.useMemo((()=>s=>z?function subscribeUpdates(s,o,i,a,u,_,w,x,C,j,L){if(!s)return()=>{};let B=!1,$=null;const checkForUpdates=()=>{if(B||!x.current)return;const s=o.getState();let i,U;try{i=a(s,u.current)}catch(s){U=s,$=s}U||($=null),i===_.current?w.current||j():(_.current=i,C.current=i,w.current=!0,L())};return i.onStateChange=checkForUpdates,i.trySubscribe(),checkForUpdates(),()=>{if(B=!0,i.tryUnsubscribe(),i.onStateChange=null,$)throw $}}(V,B,z,U,ie,ee,ce,le,ae,Y,s):()=>{}),[z]);let ye;!function useIsomorphicLayoutEffectWithArgs(s,o,i){Vk((()=>s(...o)),i)}(captureWrapperProps,[ie,ee,ce,_,ae,Y]);try{ye=Re.useSyncExternalStore(fe,de,$?()=>U($(),_):de)}catch(s){throw pe.current&&(s.message+=`\\nThe error may be correlated with this previous error:\\n${pe.current.stack}\\n\\n`),s}Vk((()=>{pe.current=void 0,ae.current=void 0,ee.current=ye}));const be=Re.useMemo((()=>Re.createElement(s,{...ye,ref:u})),[u,s,ye]);return Re.useMemo((()=>V?Re.createElement(w.Provider,{value:Z},be):be),[w,be,Z])}const j=Re.memo(ConnectFunction);if(j.WrappedComponent=s,j.displayName=ConnectFunction.displayName=i,C){const o=Re.forwardRef((function forwardConnectRef(s,o){return Re.createElement(j,{...s,reactReduxForwardedRef:o})}));return o.displayName=i,o.WrappedComponent=s,hoistNonReactStatics(o,s)}return hoistNonReactStatics(j,s)}};var fO=function Provider(s){const{children:o,context:i,serverState:a,store:u}=s,_=Re.useMemo((()=>{const s=createSubscription(u);return{store:u,subscription:s,getServerState:a?()=>a:void 0}}),[u,a]),w=Re.useMemo((()=>u.getState()),[u]);Vk((()=>{const{subscription:s}=_;return s.onStateChange=s.notifyNestedSubs,s.trySubscribe(),w!==u.getState()&&s.notifyNestedSubs(),()=>{s.tryUnsubscribe(),s.onStateChange=void 0}}),[_,w]);const x=i||pO;return Re.createElement(x.Provider,{value:_},o)};var mO=__webpack_require__(83488),gO=__webpack_require__.n(mO);const withSystem=s=>o=>{const{fn:i}=s();class WithSystem extends Re.Component{render(){return Re.createElement(o,Mn()({},s(),this.props,this.context))}}return WithSystem.displayName=`WithSystem(${i.getDisplayName(o)})`,WithSystem},withRoot=(s,o)=>i=>{const{fn:a}=s();class WithRoot extends Re.Component{render(){return Re.createElement(fO,{store:o},Re.createElement(i,Mn()({},this.props,this.context)))}}return WithRoot.displayName=`WithRoot(${a.getDisplayName(i)})`,WithRoot},withConnect=(s,o,i)=>compose(i?withRoot(s,i):gO(),dO(((i,a)=>{const u={...a,...s()},_=o.prototype?.mapStateToProps||(s=>({state:s}));return _(i,u)})),withSystem(s))(o),handleProps=(s,o,i,a)=>{for(const u in o){const _=o[u];\"function\"==typeof _&&_(i[u],a[u],s())}},withMappedContainer=(s,o,i)=>(o,a)=>{const{fn:u}=s(),_=i(o,\"root\");class WithMappedContainer extends Re.Component{constructor(o,i){super(o,i),handleProps(s,a,o,{})}UNSAFE_componentWillReceiveProps(o){handleProps(s,a,o,this.props)}render(){const s=Gt()(this.props,a?Object.keys(a):[]);return Re.createElement(_,s)}}return WithMappedContainer.displayName=`WithMappedContainer(${u.getDisplayName(_)})`,WithMappedContainer},render=(s,o,i,a)=>u=>{const _=i(s,o,a)(\"App\",\"root\"),{createRoot:w}=Zx;w(u).render(Re.createElement(_,null))},getComponent=(s,o,i)=>(a,u,_={})=>{if(\"string\"!=typeof a)throw new TypeError(\"Need a string, to fetch a component. Was given a \"+typeof a);const w=i(a);return w?u?\"root\"===u?withConnect(s,w,o()):withConnect(s,w):w:(_.failSilently||s().log.warn(\"Could not find component:\",a),null)},getDisplayName=s=>s.displayName||s.name||\"Component\",view=({getComponents:s,getStore:o,getSystem:i})=>{const a=(u=getComponent(i,o,s),Pt(u,((...s)=>JSON.stringify(s))));var u;const _=(s=>utils_memoizeN(s,((...s)=>s)))(withMappedContainer(i,0,a));return{rootInjects:{getComponent:a,makeMappedContainer:_,render:render(i,o,getComponent,s)},fn:{getDisplayName}}},view_legacy=({React:s,getSystem:o,getStore:i,getComponents:a})=>{const u={},_=parseInt(s?.version,10);return _>=16&&_<18&&(u.render=((s,o,i,a)=>u=>{const _=i(s,o,a)(\"App\",\"root\");Zx.render(Re.createElement(_,null),u)})(o,i,getComponent,a)),{rootInjects:u}};function downloadUrlPlugin(s){let{fn:o}=s;const i={download:s=>({errActions:i,specSelectors:a,specActions:u,getConfigs:_})=>{let{fetch:w}=o;const x=_();function next(o){if(o instanceof Error||o.status>=400)return u.updateLoadingStatus(\"failed\"),i.newThrownErr(Object.assign(new Error((o.message||o.statusText)+\" \"+s),{source:\"fetch\"})),void(!o.status&&o instanceof Error&&function checkPossibleFailReasons(){try{let o;if(\"URL\"in lt?o=new URL(s):(o=document.createElement(\"a\"),o.href=s),\"https:\"!==o.protocol&&\"https:\"===lt.location.protocol){const s=Object.assign(new Error(`Possible mixed-content issue? The page was loaded over https:// but a ${o.protocol}// URL was specified. Check that you are not attempting to load mixed content.`),{source:\"fetch\"});return void i.newThrownErr(s)}if(o.origin!==lt.location.origin){const s=Object.assign(new Error(`Possible cross-origin (CORS) issue? The URL origin (${o.origin}) does not match the page (${lt.location.origin}). Check the server returns the correct 'Access-Control-Allow-*' headers.`),{source:\"fetch\"});i.newThrownErr(s)}}catch(s){return}}());u.updateLoadingStatus(\"success\"),u.updateSpec(o.text),a.url()!==s&&u.updateUrl(s)}s=s||a.url(),u.updateLoadingStatus(\"loading\"),i.clear({source:\"fetch\"}),w({url:s,loadSpec:!0,requestInterceptor:x.requestInterceptor||(s=>s),responseInterceptor:x.responseInterceptor||(s=>s),credentials:\"same-origin\",headers:{Accept:\"application/json,*/*\"}}).then(next,next)},updateLoadingStatus:s=>{let o=[null,\"loading\",\"failed\",\"success\",\"failedConfig\"];return-1===o.indexOf(s)&&console.error(`Error: ${s} is not one of ${JSON.stringify(o)}`),{type:\"spec_update_loading_status\",payload:s}}};let a={loadingStatus:Ut((s=>s||(0,ze.Map)()),(s=>s.get(\"loadingStatus\")||null))};return{statePlugins:{spec:{actions:i,reducers:{spec_update_loading_status:(s,o)=>\"string\"==typeof o.payload?s.set(\"loadingStatus\",o.payload):s},selectors:a}}}}function arrayLikeToArray_arrayLikeToArray(s,o){(null==o||o>s.length)&&(o=s.length);for(var i=0,a=Array(o);i<o;i++)a[i]=s[i];return a}function toConsumableArray_toConsumableArray(s){return function arrayWithoutHoles_arrayWithoutHoles(s){if(Array.isArray(s))return arrayLikeToArray_arrayLikeToArray(s)}(s)||function iterableToArray_iterableToArray(s){if(\"undefined\"!=typeof Symbol&&null!=s[Symbol.iterator]||null!=s[\"@@iterator\"])return Array.from(s)}(s)||function unsupportedIterableToArray_unsupportedIterableToArray(s,o){if(s){if(\"string\"==typeof s)return arrayLikeToArray_arrayLikeToArray(s,o);var i={}.toString.call(s).slice(8,-1);return\"Object\"===i&&s.constructor&&(i=s.constructor.name),\"Map\"===i||\"Set\"===i?Array.from(s):\"Arguments\"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?arrayLikeToArray_arrayLikeToArray(s,o):void 0}}(s)||function nonIterableSpread_nonIterableSpread(){throw new TypeError(\"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}function typeof_typeof(s){return typeof_typeof=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(s){return typeof s}:function(s){return s&&\"function\"==typeof Symbol&&s.constructor===Symbol&&s!==Symbol.prototype?\"symbol\":typeof s},typeof_typeof(s)}function toPropertyKey(s){var o=function toPrimitive(s,o){if(\"object\"!=typeof_typeof(s)||!s)return s;var i=s[Symbol.toPrimitive];if(void 0!==i){var a=i.call(s,o||\"default\");if(\"object\"!=typeof_typeof(a))return a;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===o?String:Number)(s)}(s,\"string\");return\"symbol\"==typeof_typeof(o)?o:o+\"\"}function defineProperty_defineProperty(s,o,i){return(o=toPropertyKey(o))in s?Object.defineProperty(s,o,{value:i,enumerable:!0,configurable:!0,writable:!0}):s[o]=i,s}function extends_extends(){return extends_extends=Object.assign?Object.assign.bind():function(s){for(var o=1;o<arguments.length;o++){var i=arguments[o];for(var a in i)({}).hasOwnProperty.call(i,a)&&(s[a]=i[a])}return s},extends_extends.apply(null,arguments)}function create_element_ownKeys(s,o){var i=Object.keys(s);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(s);o&&(a=a.filter((function(o){return Object.getOwnPropertyDescriptor(s,o).enumerable}))),i.push.apply(i,a)}return i}function _objectSpread(s){for(var o=1;o<arguments.length;o++){var i=null!=arguments[o]?arguments[o]:{};o%2?create_element_ownKeys(Object(i),!0).forEach((function(o){defineProperty_defineProperty(s,o,i[o])})):Object.getOwnPropertyDescriptors?Object.defineProperties(s,Object.getOwnPropertyDescriptors(i)):create_element_ownKeys(Object(i)).forEach((function(o){Object.defineProperty(s,o,Object.getOwnPropertyDescriptor(i,o))}))}return s}var yO={};function createStyleObject(s){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2?arguments[2]:void 0;return function getClassNameCombinations(s){if(0===s.length||1===s.length)return s;var o=s.join(\".\");return yO[o]||(yO[o]=function powerSetPermutations(s){var o=s.length;return 0===o||1===o?s:2===o?[s[0],s[1],\"\".concat(s[0],\".\").concat(s[1]),\"\".concat(s[1],\".\").concat(s[0])]:3===o?[s[0],s[1],s[2],\"\".concat(s[0],\".\").concat(s[1]),\"\".concat(s[0],\".\").concat(s[2]),\"\".concat(s[1],\".\").concat(s[0]),\"\".concat(s[1],\".\").concat(s[2]),\"\".concat(s[2],\".\").concat(s[0]),\"\".concat(s[2],\".\").concat(s[1]),\"\".concat(s[0],\".\").concat(s[1],\".\").concat(s[2]),\"\".concat(s[0],\".\").concat(s[2],\".\").concat(s[1]),\"\".concat(s[1],\".\").concat(s[0],\".\").concat(s[2]),\"\".concat(s[1],\".\").concat(s[2],\".\").concat(s[0]),\"\".concat(s[2],\".\").concat(s[0],\".\").concat(s[1]),\"\".concat(s[2],\".\").concat(s[1],\".\").concat(s[0])]:o>=4?[s[0],s[1],s[2],s[3],\"\".concat(s[0],\".\").concat(s[1]),\"\".concat(s[0],\".\").concat(s[2]),\"\".concat(s[0],\".\").concat(s[3]),\"\".concat(s[1],\".\").concat(s[0]),\"\".concat(s[1],\".\").concat(s[2]),\"\".concat(s[1],\".\").concat(s[3]),\"\".concat(s[2],\".\").concat(s[0]),\"\".concat(s[2],\".\").concat(s[1]),\"\".concat(s[2],\".\").concat(s[3]),\"\".concat(s[3],\".\").concat(s[0]),\"\".concat(s[3],\".\").concat(s[1]),\"\".concat(s[3],\".\").concat(s[2]),\"\".concat(s[0],\".\").concat(s[1],\".\").concat(s[2]),\"\".concat(s[0],\".\").concat(s[1],\".\").concat(s[3]),\"\".concat(s[0],\".\").concat(s[2],\".\").concat(s[1]),\"\".concat(s[0],\".\").concat(s[2],\".\").concat(s[3]),\"\".concat(s[0],\".\").concat(s[3],\".\").concat(s[1]),\"\".concat(s[0],\".\").concat(s[3],\".\").concat(s[2]),\"\".concat(s[1],\".\").concat(s[0],\".\").concat(s[2]),\"\".concat(s[1],\".\").concat(s[0],\".\").concat(s[3]),\"\".concat(s[1],\".\").concat(s[2],\".\").concat(s[0]),\"\".concat(s[1],\".\").concat(s[2],\".\").concat(s[3]),\"\".concat(s[1],\".\").concat(s[3],\".\").concat(s[0]),\"\".concat(s[1],\".\").concat(s[3],\".\").concat(s[2]),\"\".concat(s[2],\".\").concat(s[0],\".\").concat(s[1]),\"\".concat(s[2],\".\").concat(s[0],\".\").concat(s[3]),\"\".concat(s[2],\".\").concat(s[1],\".\").concat(s[0]),\"\".concat(s[2],\".\").concat(s[1],\".\").concat(s[3]),\"\".concat(s[2],\".\").concat(s[3],\".\").concat(s[0]),\"\".concat(s[2],\".\").concat(s[3],\".\").concat(s[1]),\"\".concat(s[3],\".\").concat(s[0],\".\").concat(s[1]),\"\".concat(s[3],\".\").concat(s[0],\".\").concat(s[2]),\"\".concat(s[3],\".\").concat(s[1],\".\").concat(s[0]),\"\".concat(s[3],\".\").concat(s[1],\".\").concat(s[2]),\"\".concat(s[3],\".\").concat(s[2],\".\").concat(s[0]),\"\".concat(s[3],\".\").concat(s[2],\".\").concat(s[1]),\"\".concat(s[0],\".\").concat(s[1],\".\").concat(s[2],\".\").concat(s[3]),\"\".concat(s[0],\".\").concat(s[1],\".\").concat(s[3],\".\").concat(s[2]),\"\".concat(s[0],\".\").concat(s[2],\".\").concat(s[1],\".\").concat(s[3]),\"\".concat(s[0],\".\").concat(s[2],\".\").concat(s[3],\".\").concat(s[1]),\"\".concat(s[0],\".\").concat(s[3],\".\").concat(s[1],\".\").concat(s[2]),\"\".concat(s[0],\".\").concat(s[3],\".\").concat(s[2],\".\").concat(s[1]),\"\".concat(s[1],\".\").concat(s[0],\".\").concat(s[2],\".\").concat(s[3]),\"\".concat(s[1],\".\").concat(s[0],\".\").concat(s[3],\".\").concat(s[2]),\"\".concat(s[1],\".\").concat(s[2],\".\").concat(s[0],\".\").concat(s[3]),\"\".concat(s[1],\".\").concat(s[2],\".\").concat(s[3],\".\").concat(s[0]),\"\".concat(s[1],\".\").concat(s[3],\".\").concat(s[0],\".\").concat(s[2]),\"\".concat(s[1],\".\").concat(s[3],\".\").concat(s[2],\".\").concat(s[0]),\"\".concat(s[2],\".\").concat(s[0],\".\").concat(s[1],\".\").concat(s[3]),\"\".concat(s[2],\".\").concat(s[0],\".\").concat(s[3],\".\").concat(s[1]),\"\".concat(s[2],\".\").concat(s[1],\".\").concat(s[0],\".\").concat(s[3]),\"\".concat(s[2],\".\").concat(s[1],\".\").concat(s[3],\".\").concat(s[0]),\"\".concat(s[2],\".\").concat(s[3],\".\").concat(s[0],\".\").concat(s[1]),\"\".concat(s[2],\".\").concat(s[3],\".\").concat(s[1],\".\").concat(s[0]),\"\".concat(s[3],\".\").concat(s[0],\".\").concat(s[1],\".\").concat(s[2]),\"\".concat(s[3],\".\").concat(s[0],\".\").concat(s[2],\".\").concat(s[1]),\"\".concat(s[3],\".\").concat(s[1],\".\").concat(s[0],\".\").concat(s[2]),\"\".concat(s[3],\".\").concat(s[1],\".\").concat(s[2],\".\").concat(s[0]),\"\".concat(s[3],\".\").concat(s[2],\".\").concat(s[0],\".\").concat(s[1]),\"\".concat(s[3],\".\").concat(s[2],\".\").concat(s[1],\".\").concat(s[0])]:void 0}(s)),yO[o]}(s.filter((function(s){return\"token\"!==s}))).reduce((function(s,o){return _objectSpread(_objectSpread({},s),i[o])}),o)}function createClassNameString(s){return s.join(\" \")}function createElement(s){var o=s.node,i=s.stylesheet,a=s.style,u=void 0===a?{}:a,_=s.useInlineStyles,w=s.key,x=o.properties,C=o.type,j=o.tagName,L=o.value;if(\"text\"===C)return L;if(j){var B,$=function createChildren(s,o){var i=0;return function(a){return i+=1,a.map((function(a,u){return createElement({node:a,stylesheet:s,useInlineStyles:o,key:\"code-segment-\".concat(i,\"-\").concat(u)})}))}}(i,_);if(_){var U=Object.keys(i).reduce((function(s,o){return o.split(\".\").forEach((function(o){s.includes(o)||s.push(o)})),s}),[]),V=x.className&&x.className.includes(\"token\")?[\"token\"]:[],z=x.className&&V.concat(x.className.filter((function(s){return!U.includes(s)})));B=_objectSpread(_objectSpread({},x),{},{className:createClassNameString(z)||void 0,style:createStyleObject(x.className,Object.assign({},x.style,u),i)})}else B=_objectSpread(_objectSpread({},x),{},{className:createClassNameString(x.className)});var Y=$(o.children);return Re.createElement(j,extends_extends({key:w},B),Y)}}var vO=[\"language\",\"children\",\"style\",\"customStyle\",\"codeTagProps\",\"useInlineStyles\",\"showLineNumbers\",\"showInlineLineNumbers\",\"startingLineNumber\",\"lineNumberContainerStyle\",\"lineNumberStyle\",\"wrapLines\",\"wrapLongLines\",\"lineProps\",\"renderer\",\"PreTag\",\"CodeTag\",\"code\",\"astGenerator\"];function highlight_ownKeys(s,o){var i=Object.keys(s);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(s);o&&(a=a.filter((function(o){return Object.getOwnPropertyDescriptor(s,o).enumerable}))),i.push.apply(i,a)}return i}function highlight_objectSpread(s){for(var o=1;o<arguments.length;o++){var i=null!=arguments[o]?arguments[o]:{};o%2?highlight_ownKeys(Object(i),!0).forEach((function(o){defineProperty_defineProperty(s,o,i[o])})):Object.getOwnPropertyDescriptors?Object.defineProperties(s,Object.getOwnPropertyDescriptors(i)):highlight_ownKeys(Object(i)).forEach((function(o){Object.defineProperty(s,o,Object.getOwnPropertyDescriptor(i,o))}))}return s}var bO=/\\n/g;function AllLineNumbers(s){var o=s.codeString,i=s.codeStyle,a=s.containerStyle,u=void 0===a?{float:\"left\",paddingRight:\"10px\"}:a,_=s.numberStyle,w=void 0===_?{}:_,x=s.startingLineNumber;return Re.createElement(\"code\",{style:Object.assign({},i,u)},function getAllLineNumbers(s){var o=s.lines,i=s.startingLineNumber,a=s.style;return o.map((function(s,o){var u=o+i;return Re.createElement(\"span\",{key:\"line-\".concat(o),className:\"react-syntax-highlighter-line-number\",style:\"function\"==typeof a?a(u):a},\"\".concat(u,\"\\n\"))}))}({lines:o.replace(/\\n$/,\"\").split(\"\\n\"),style:w,startingLineNumber:x}))}function getInlineLineNumber(s,o){return{type:\"element\",tagName:\"span\",properties:{key:\"line-number--\".concat(s),className:[\"comment\",\"linenumber\",\"react-syntax-highlighter-line-number\"],style:o},children:[{type:\"text\",value:s}]}}function assembleLineNumberStyles(s,o,i){var a,u={display:\"inline-block\",minWidth:(a=i,\"\".concat(a.toString().length,\".25em\")),paddingRight:\"1em\",textAlign:\"right\",userSelect:\"none\"},_=\"function\"==typeof s?s(o):s;return highlight_objectSpread(highlight_objectSpread({},u),_)}function createLineElement(s){var o=s.children,i=s.lineNumber,a=s.lineNumberStyle,u=s.largestLineNumber,_=s.showInlineLineNumbers,w=s.lineProps,x=void 0===w?{}:w,C=s.className,j=void 0===C?[]:C,L=s.showLineNumbers,B=s.wrapLongLines,$=s.wrapLines,U=void 0!==$&&$?highlight_objectSpread({},\"function\"==typeof x?x(i):x):{};if(U.className=U.className?[].concat(toConsumableArray_toConsumableArray(U.className.trim().split(/\\s+/)),toConsumableArray_toConsumableArray(j)):j,i&&_){var V=assembleLineNumberStyles(a,i,u);o.unshift(getInlineLineNumber(i,V))}return B&L&&(U.style=highlight_objectSpread({display:\"flex\"},U.style)),{type:\"element\",tagName:\"span\",properties:U,children:o}}function flattenCodeTree(s){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[];void 0===s.length&&(s=[s]);for(var a=0;a<s.length;a++){var u=s[a];if(\"text\"===u.type)i.push(createLineElement({children:[u],className:toConsumableArray_toConsumableArray(new Set(o))}));else if(u.children){var _,w=o.concat((null===(_=u.properties)||void 0===_?void 0:_.className)||[]);flattenCodeTree(u.children,w).forEach((function(s){return i.push(s)}))}}return i}function processLines(s,o,i,a,u,_,w,x,C){var j,L=flattenCodeTree(s.value),B=[],$=-1,U=0;function createLine(s,_){var j=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[];return o||j.length>0?function createWrappedLine(s,_){return createLineElement({children:s,lineNumber:_,lineNumberStyle:x,largestLineNumber:w,showInlineLineNumbers:u,lineProps:i,className:arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],showLineNumbers:a,wrapLongLines:C,wrapLines:o})}(s,_,j):function createUnwrappedLine(s,o){if(a&&o&&u){var i=assembleLineNumberStyles(x,o,w);s.unshift(getInlineLineNumber(o,i))}return s}(s,_)}for(var V=function _loop(){var s=L[U],o=s.children[0].value,i=function getNewLines(s){return s.match(bO)}(o);if(i){var u=o.split(\"\\n\");u.forEach((function(o,i){var w=a&&B.length+_,x={type:\"text\",value:\"\".concat(o,\"\\n\")};if(0===i){var C=createLine(L.slice($+1,U).concat(createLineElement({children:[x],className:s.properties.className})),w);B.push(C)}else if(i===u.length-1){var j=L[U+1]&&L[U+1].children&&L[U+1].children[0],V={type:\"text\",value:\"\".concat(o)};if(j){var z=createLineElement({children:[V],className:s.properties.className});L.splice(U+1,0,z)}else{var Y=createLine([V],w,s.properties.className);B.push(Y)}}else{var Z=createLine([x],w,s.properties.className);B.push(Z)}})),$=U}U++};U<L.length;)V();if($!==L.length-1){var z=L.slice($+1,L.length);if(z&&z.length){var Y=createLine(z,a&&B.length+_);B.push(Y)}}return o?B:(j=[]).concat.apply(j,B)}function defaultRenderer(s){var o=s.rows,i=s.stylesheet,a=s.useInlineStyles;return o.map((function(s,o){return createElement({node:s,stylesheet:i,useInlineStyles:a,key:\"code-segment-\".concat(o)})}))}function isHighlightJs(s){return s&&void 0!==s.highlightAuto}var _O=__webpack_require__(43768),SO=function highlight(s,o){return function SyntaxHighlighter(i){var a,u,_=i.language,w=i.children,x=i.style,C=void 0===x?o:x,j=i.customStyle,L=void 0===j?{}:j,B=i.codeTagProps,$=void 0===B?{className:_?\"language-\".concat(_):void 0,style:highlight_objectSpread(highlight_objectSpread({},C['code[class*=\"language-\"]']),C['code[class*=\"language-'.concat(_,'\"]')])}:B,U=i.useInlineStyles,V=void 0===U||U,z=i.showLineNumbers,Y=void 0!==z&&z,Z=i.showInlineLineNumbers,ee=void 0===Z||Z,ie=i.startingLineNumber,ae=void 0===ie?1:ie,ce=i.lineNumberContainerStyle,le=i.lineNumberStyle,pe=void 0===le?{}:le,de=i.wrapLines,fe=i.wrapLongLines,ye=void 0!==fe&&fe,be=i.lineProps,_e=void 0===be?{}:be,Se=i.renderer,we=i.PreTag,xe=void 0===we?\"pre\":we,Pe=i.CodeTag,Te=void 0===Pe?\"code\":Pe,$e=i.code,qe=void 0===$e?(Array.isArray(w)?w[0]:w)||\"\":$e,ze=i.astGenerator,We=function _objectWithoutProperties(s,o){if(null==s)return{};var i,a,u=function _objectWithoutPropertiesLoose(s,o){if(null==s)return{};var i={};for(var a in s)if({}.hasOwnProperty.call(s,a)){if(-1!==o.indexOf(a))continue;i[a]=s[a]}return i}(s,o);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(s);for(a=0;a<_.length;a++)i=_[a],-1===o.indexOf(i)&&{}.propertyIsEnumerable.call(s,i)&&(u[i]=s[i])}return u}(i,vO);ze=ze||s;var He=Y?Re.createElement(AllLineNumbers,{containerStyle:ce,codeStyle:$.style||{},numberStyle:pe,startingLineNumber:ae,codeString:qe}):null,Ye=C.hljs||C['pre[class*=\"language-\"]']||{backgroundColor:\"#fff\"},Xe=isHighlightJs(ze)?\"hljs\":\"prismjs\",Qe=V?Object.assign({},We,{style:Object.assign({},Ye,L)}):Object.assign({},We,{className:We.className?\"\".concat(Xe,\" \").concat(We.className):Xe,style:Object.assign({},L)});if($.style=highlight_objectSpread(ye?{whiteSpace:\"pre-wrap\"}:{whiteSpace:\"pre\"},$.style),!ze)return Re.createElement(xe,Qe,He,Re.createElement(Te,$,qe));(void 0===de&&Se||ye)&&(de=!0),Se=Se||defaultRenderer;var et=[{type:\"text\",value:qe}],tt=function getCodeTree(s){var o=s.astGenerator,i=s.language,a=s.code,u=s.defaultCodeValue;if(isHighlightJs(o)){var _=function(s,o){return-1!==s.listLanguages().indexOf(o)}(o,i);return\"text\"===i?{value:u,language:\"text\"}:_?o.highlight(i,a):o.highlightAuto(a)}try{return i&&\"text\"!==i?{value:o.highlight(a,i)}:{value:u}}catch(s){return{value:u}}}({astGenerator:ze,language:_,code:qe,defaultCodeValue:et});null===tt.language&&(tt.value=et);var rt=processLines(tt,de,_e,Y,ee,ae,ae+(null!==(a=null===(u=qe.match(/\\n/g))||void 0===u?void 0:u.length)&&void 0!==a?a:0),pe,ye);return Re.createElement(xe,Qe,Re.createElement(Te,$,!ee&&He,Se({rows:rt,stylesheet:C,useInlineStyles:V})))}}(_O,{});SO.registerLanguage=_O.registerLanguage;const EO=SO;var wO=__webpack_require__(95089);const xO=__webpack_require__.n(wO)();var kO=__webpack_require__(65772);const OO=__webpack_require__.n(kO)();var AO=__webpack_require__(17285);const CO=__webpack_require__.n(AO)();var jO=__webpack_require__(35344);const PO=__webpack_require__.n(jO)();var IO=__webpack_require__(17533);const TO=__webpack_require__.n(IO)();var NO=__webpack_require__(73402);const MO=__webpack_require__.n(NO)();var RO=__webpack_require__(26571);const DO=__webpack_require__.n(RO)(),after_load=()=>{EO.registerLanguage(\"json\",OO),EO.registerLanguage(\"js\",xO),EO.registerLanguage(\"xml\",CO),EO.registerLanguage(\"yaml\",TO),EO.registerLanguage(\"http\",MO),EO.registerLanguage(\"bash\",PO),EO.registerLanguage(\"powershell\",DO),EO.registerLanguage(\"javascript\",xO)},LO={hljs:{display:\"block\",overflowX:\"auto\",padding:\"0.5em\",background:\"#333\",color:\"white\"},\"hljs-name\":{fontWeight:\"bold\"},\"hljs-strong\":{fontWeight:\"bold\"},\"hljs-code\":{fontStyle:\"italic\",color:\"#888\"},\"hljs-emphasis\":{fontStyle:\"italic\"},\"hljs-tag\":{color:\"#62c8f3\"},\"hljs-variable\":{color:\"#ade5fc\"},\"hljs-template-variable\":{color:\"#ade5fc\"},\"hljs-selector-id\":{color:\"#ade5fc\"},\"hljs-selector-class\":{color:\"#ade5fc\"},\"hljs-string\":{color:\"#a2fca2\"},\"hljs-bullet\":{color:\"#d36363\"},\"hljs-type\":{color:\"#ffa\"},\"hljs-title\":{color:\"#ffa\"},\"hljs-section\":{color:\"#ffa\"},\"hljs-attribute\":{color:\"#ffa\"},\"hljs-quote\":{color:\"#ffa\"},\"hljs-built_in\":{color:\"#ffa\"},\"hljs-builtin-name\":{color:\"#ffa\"},\"hljs-number\":{color:\"#d36363\"},\"hljs-symbol\":{color:\"#d36363\"},\"hljs-keyword\":{color:\"#fcc28c\"},\"hljs-selector-tag\":{color:\"#fcc28c\"},\"hljs-literal\":{color:\"#fcc28c\"},\"hljs-comment\":{color:\"#888\"},\"hljs-deletion\":{color:\"#333\",backgroundColor:\"#fc9b9b\"},\"hljs-regexp\":{color:\"#c6b4f0\"},\"hljs-link\":{color:\"#c6b4f0\"},\"hljs-meta\":{color:\"#fc9b9b\"},\"hljs-addition\":{backgroundColor:\"#a2fca2\",color:\"#333\"}},FO={agate:LO,arta:{hljs:{display:\"block\",overflowX:\"auto\",padding:\"0.5em\",background:\"#222\",color:\"#aaa\"},\"hljs-subst\":{color:\"#aaa\"},\"hljs-section\":{color:\"#fff\",fontWeight:\"bold\"},\"hljs-comment\":{color:\"#444\"},\"hljs-quote\":{color:\"#444\"},\"hljs-meta\":{color:\"#444\"},\"hljs-string\":{color:\"#ffcc33\"},\"hljs-symbol\":{color:\"#ffcc33\"},\"hljs-bullet\":{color:\"#ffcc33\"},\"hljs-regexp\":{color:\"#ffcc33\"},\"hljs-number\":{color:\"#00cc66\"},\"hljs-addition\":{color:\"#00cc66\"},\"hljs-built_in\":{color:\"#32aaee\"},\"hljs-builtin-name\":{color:\"#32aaee\"},\"hljs-literal\":{color:\"#32aaee\"},\"hljs-type\":{color:\"#32aaee\"},\"hljs-template-variable\":{color:\"#32aaee\"},\"hljs-attribute\":{color:\"#32aaee\"},\"hljs-link\":{color:\"#32aaee\"},\"hljs-keyword\":{color:\"#6644aa\"},\"hljs-selector-tag\":{color:\"#6644aa\"},\"hljs-name\":{color:\"#6644aa\"},\"hljs-selector-id\":{color:\"#6644aa\"},\"hljs-selector-class\":{color:\"#6644aa\"},\"hljs-title\":{color:\"#bb1166\"},\"hljs-variable\":{color:\"#bb1166\"},\"hljs-deletion\":{color:\"#bb1166\"},\"hljs-template-tag\":{color:\"#bb1166\"},\"hljs-doctag\":{fontWeight:\"bold\"},\"hljs-strong\":{fontWeight:\"bold\"},\"hljs-emphasis\":{fontStyle:\"italic\"}},monokai:{hljs:{display:\"block\",overflowX:\"auto\",padding:\"0.5em\",background:\"#272822\",color:\"#ddd\"},\"hljs-tag\":{color:\"#f92672\"},\"hljs-keyword\":{color:\"#f92672\",fontWeight:\"bold\"},\"hljs-selector-tag\":{color:\"#f92672\",fontWeight:\"bold\"},\"hljs-literal\":{color:\"#f92672\",fontWeight:\"bold\"},\"hljs-strong\":{color:\"#f92672\"},\"hljs-name\":{color:\"#f92672\"},\"hljs-code\":{color:\"#66d9ef\"},\"hljs-class .hljs-title\":{color:\"white\"},\"hljs-attribute\":{color:\"#bf79db\"},\"hljs-symbol\":{color:\"#bf79db\"},\"hljs-regexp\":{color:\"#bf79db\"},\"hljs-link\":{color:\"#bf79db\"},\"hljs-string\":{color:\"#a6e22e\"},\"hljs-bullet\":{color:\"#a6e22e\"},\"hljs-subst\":{color:\"#a6e22e\"},\"hljs-title\":{color:\"#a6e22e\",fontWeight:\"bold\"},\"hljs-section\":{color:\"#a6e22e\",fontWeight:\"bold\"},\"hljs-emphasis\":{color:\"#a6e22e\"},\"hljs-type\":{color:\"#a6e22e\",fontWeight:\"bold\"},\"hljs-built_in\":{color:\"#a6e22e\"},\"hljs-builtin-name\":{color:\"#a6e22e\"},\"hljs-selector-attr\":{color:\"#a6e22e\"},\"hljs-selector-pseudo\":{color:\"#a6e22e\"},\"hljs-addition\":{color:\"#a6e22e\"},\"hljs-variable\":{color:\"#a6e22e\"},\"hljs-template-tag\":{color:\"#a6e22e\"},\"hljs-template-variable\":{color:\"#a6e22e\"},\"hljs-comment\":{color:\"#75715e\"},\"hljs-quote\":{color:\"#75715e\"},\"hljs-deletion\":{color:\"#75715e\"},\"hljs-meta\":{color:\"#75715e\"},\"hljs-doctag\":{fontWeight:\"bold\"},\"hljs-selector-id\":{fontWeight:\"bold\"}},nord:{hljs:{display:\"block\",overflowX:\"auto\",padding:\"0.5em\",background:\"#2E3440\",color:\"#D8DEE9\"},\"hljs-subst\":{color:\"#D8DEE9\"},\"hljs-selector-tag\":{color:\"#81A1C1\"},\"hljs-selector-id\":{color:\"#8FBCBB\",fontWeight:\"bold\"},\"hljs-selector-class\":{color:\"#8FBCBB\"},\"hljs-selector-attr\":{color:\"#8FBCBB\"},\"hljs-selector-pseudo\":{color:\"#88C0D0\"},\"hljs-addition\":{backgroundColor:\"rgba(163, 190, 140, 0.5)\"},\"hljs-deletion\":{backgroundColor:\"rgba(191, 97, 106, 0.5)\"},\"hljs-built_in\":{color:\"#8FBCBB\"},\"hljs-type\":{color:\"#8FBCBB\"},\"hljs-class\":{color:\"#8FBCBB\"},\"hljs-function\":{color:\"#88C0D0\"},\"hljs-function > .hljs-title\":{color:\"#88C0D0\"},\"hljs-keyword\":{color:\"#81A1C1\"},\"hljs-literal\":{color:\"#81A1C1\"},\"hljs-symbol\":{color:\"#81A1C1\"},\"hljs-number\":{color:\"#B48EAD\"},\"hljs-regexp\":{color:\"#EBCB8B\"},\"hljs-string\":{color:\"#A3BE8C\"},\"hljs-title\":{color:\"#8FBCBB\"},\"hljs-params\":{color:\"#D8DEE9\"},\"hljs-bullet\":{color:\"#81A1C1\"},\"hljs-code\":{color:\"#8FBCBB\"},\"hljs-emphasis\":{fontStyle:\"italic\"},\"hljs-formula\":{color:\"#8FBCBB\"},\"hljs-strong\":{fontWeight:\"bold\"},\"hljs-link:hover\":{textDecoration:\"underline\"},\"hljs-quote\":{color:\"#4C566A\"},\"hljs-comment\":{color:\"#4C566A\"},\"hljs-doctag\":{color:\"#8FBCBB\"},\"hljs-meta\":{color:\"#5E81AC\"},\"hljs-meta-keyword\":{color:\"#5E81AC\"},\"hljs-meta-string\":{color:\"#A3BE8C\"},\"hljs-attr\":{color:\"#8FBCBB\"},\"hljs-attribute\":{color:\"#D8DEE9\"},\"hljs-builtin-name\":{color:\"#81A1C1\"},\"hljs-name\":{color:\"#81A1C1\"},\"hljs-section\":{color:\"#88C0D0\"},\"hljs-tag\":{color:\"#81A1C1\"},\"hljs-variable\":{color:\"#D8DEE9\"},\"hljs-template-variable\":{color:\"#D8DEE9\"},\"hljs-template-tag\":{color:\"#5E81AC\"},\"abnf .hljs-attribute\":{color:\"#88C0D0\"},\"abnf .hljs-symbol\":{color:\"#EBCB8B\"},\"apache .hljs-attribute\":{color:\"#88C0D0\"},\"apache .hljs-section\":{color:\"#81A1C1\"},\"arduino .hljs-built_in\":{color:\"#88C0D0\"},\"aspectj .hljs-meta\":{color:\"#D08770\"},\"aspectj > .hljs-title\":{color:\"#88C0D0\"},\"bnf .hljs-attribute\":{color:\"#8FBCBB\"},\"clojure .hljs-name\":{color:\"#88C0D0\"},\"clojure .hljs-symbol\":{color:\"#EBCB8B\"},\"coq .hljs-built_in\":{color:\"#88C0D0\"},\"cpp .hljs-meta-string\":{color:\"#8FBCBB\"},\"css .hljs-built_in\":{color:\"#88C0D0\"},\"css .hljs-keyword\":{color:\"#D08770\"},\"diff .hljs-meta\":{color:\"#8FBCBB\"},\"ebnf .hljs-attribute\":{color:\"#8FBCBB\"},\"glsl .hljs-built_in\":{color:\"#88C0D0\"},\"groovy .hljs-meta:not(:first-child)\":{color:\"#D08770\"},\"haxe .hljs-meta\":{color:\"#D08770\"},\"java .hljs-meta\":{color:\"#D08770\"},\"ldif .hljs-attribute\":{color:\"#8FBCBB\"},\"lisp .hljs-name\":{color:\"#88C0D0\"},\"lua .hljs-built_in\":{color:\"#88C0D0\"},\"moonscript .hljs-built_in\":{color:\"#88C0D0\"},\"nginx .hljs-attribute\":{color:\"#88C0D0\"},\"nginx .hljs-section\":{color:\"#5E81AC\"},\"pf .hljs-built_in\":{color:\"#88C0D0\"},\"processing .hljs-built_in\":{color:\"#88C0D0\"},\"scss .hljs-keyword\":{color:\"#81A1C1\"},\"stylus .hljs-keyword\":{color:\"#81A1C1\"},\"swift .hljs-meta\":{color:\"#D08770\"},\"vim .hljs-built_in\":{color:\"#88C0D0\",fontStyle:\"italic\"},\"yaml .hljs-meta\":{color:\"#D08770\"}},obsidian:{hljs:{display:\"block\",overflowX:\"auto\",padding:\"0.5em\",background:\"#282b2e\",color:\"#e0e2e4\"},\"hljs-keyword\":{color:\"#93c763\",fontWeight:\"bold\"},\"hljs-selector-tag\":{color:\"#93c763\",fontWeight:\"bold\"},\"hljs-literal\":{color:\"#93c763\",fontWeight:\"bold\"},\"hljs-selector-id\":{color:\"#93c763\"},\"hljs-number\":{color:\"#ffcd22\"},\"hljs-attribute\":{color:\"#668bb0\"},\"hljs-code\":{color:\"white\"},\"hljs-class .hljs-title\":{color:\"white\"},\"hljs-section\":{color:\"white\",fontWeight:\"bold\"},\"hljs-regexp\":{color:\"#d39745\"},\"hljs-link\":{color:\"#d39745\"},\"hljs-meta\":{color:\"#557182\"},\"hljs-tag\":{color:\"#8cbbad\"},\"hljs-name\":{color:\"#8cbbad\",fontWeight:\"bold\"},\"hljs-bullet\":{color:\"#8cbbad\"},\"hljs-subst\":{color:\"#8cbbad\"},\"hljs-emphasis\":{color:\"#8cbbad\"},\"hljs-type\":{color:\"#8cbbad\",fontWeight:\"bold\"},\"hljs-built_in\":{color:\"#8cbbad\"},\"hljs-selector-attr\":{color:\"#8cbbad\"},\"hljs-selector-pseudo\":{color:\"#8cbbad\"},\"hljs-addition\":{color:\"#8cbbad\"},\"hljs-variable\":{color:\"#8cbbad\"},\"hljs-template-tag\":{color:\"#8cbbad\"},\"hljs-template-variable\":{color:\"#8cbbad\"},\"hljs-string\":{color:\"#ec7600\"},\"hljs-symbol\":{color:\"#ec7600\"},\"hljs-comment\":{color:\"#818e96\"},\"hljs-quote\":{color:\"#818e96\"},\"hljs-deletion\":{color:\"#818e96\"},\"hljs-selector-class\":{color:\"#A082BD\"},\"hljs-doctag\":{fontWeight:\"bold\"},\"hljs-title\":{fontWeight:\"bold\"},\"hljs-strong\":{fontWeight:\"bold\"}},\"tomorrow-night\":{\"hljs-comment\":{color:\"#969896\"},\"hljs-quote\":{color:\"#969896\"},\"hljs-variable\":{color:\"#cc6666\"},\"hljs-template-variable\":{color:\"#cc6666\"},\"hljs-tag\":{color:\"#cc6666\"},\"hljs-name\":{color:\"#cc6666\"},\"hljs-selector-id\":{color:\"#cc6666\"},\"hljs-selector-class\":{color:\"#cc6666\"},\"hljs-regexp\":{color:\"#cc6666\"},\"hljs-deletion\":{color:\"#cc6666\"},\"hljs-number\":{color:\"#de935f\"},\"hljs-built_in\":{color:\"#de935f\"},\"hljs-builtin-name\":{color:\"#de935f\"},\"hljs-literal\":{color:\"#de935f\"},\"hljs-type\":{color:\"#de935f\"},\"hljs-params\":{color:\"#de935f\"},\"hljs-meta\":{color:\"#de935f\"},\"hljs-link\":{color:\"#de935f\"},\"hljs-attribute\":{color:\"#f0c674\"},\"hljs-string\":{color:\"#b5bd68\"},\"hljs-symbol\":{color:\"#b5bd68\"},\"hljs-bullet\":{color:\"#b5bd68\"},\"hljs-addition\":{color:\"#b5bd68\"},\"hljs-title\":{color:\"#81a2be\"},\"hljs-section\":{color:\"#81a2be\"},\"hljs-keyword\":{color:\"#b294bb\"},\"hljs-selector-tag\":{color:\"#b294bb\"},hljs:{display:\"block\",overflowX:\"auto\",background:\"#1d1f21\",color:\"#c5c8c6\",padding:\"0.5em\"},\"hljs-emphasis\":{fontStyle:\"italic\"},\"hljs-strong\":{fontWeight:\"bold\"}},idea:{hljs:{display:\"block\",overflowX:\"auto\",padding:\"0.5em\",color:\"#000\",background:\"#fff\"},\"hljs-subst\":{fontWeight:\"normal\",color:\"#000\"},\"hljs-title\":{fontWeight:\"normal\",color:\"#000\"},\"hljs-comment\":{color:\"#808080\",fontStyle:\"italic\"},\"hljs-quote\":{color:\"#808080\",fontStyle:\"italic\"},\"hljs-meta\":{color:\"#808000\"},\"hljs-tag\":{background:\"#efefef\"},\"hljs-section\":{fontWeight:\"bold\",color:\"#000080\"},\"hljs-name\":{fontWeight:\"bold\",color:\"#000080\"},\"hljs-literal\":{fontWeight:\"bold\",color:\"#000080\"},\"hljs-keyword\":{fontWeight:\"bold\",color:\"#000080\"},\"hljs-selector-tag\":{fontWeight:\"bold\",color:\"#000080\"},\"hljs-type\":{fontWeight:\"bold\",color:\"#000080\"},\"hljs-selector-id\":{fontWeight:\"bold\",color:\"#000080\"},\"hljs-selector-class\":{fontWeight:\"bold\",color:\"#000080\"},\"hljs-attribute\":{fontWeight:\"bold\",color:\"#0000ff\"},\"hljs-number\":{fontWeight:\"normal\",color:\"#0000ff\"},\"hljs-regexp\":{fontWeight:\"normal\",color:\"#0000ff\"},\"hljs-link\":{fontWeight:\"normal\",color:\"#0000ff\"},\"hljs-string\":{color:\"#008000\",fontWeight:\"bold\"},\"hljs-symbol\":{color:\"#000\",background:\"#d0eded\",fontStyle:\"italic\"},\"hljs-bullet\":{color:\"#000\",background:\"#d0eded\",fontStyle:\"italic\"},\"hljs-formula\":{color:\"#000\",background:\"#d0eded\",fontStyle:\"italic\"},\"hljs-doctag\":{textDecoration:\"underline\"},\"hljs-variable\":{color:\"#660e7a\"},\"hljs-template-variable\":{color:\"#660e7a\"},\"hljs-addition\":{background:\"#baeeba\"},\"hljs-deletion\":{background:\"#ffc8bd\"},\"hljs-emphasis\":{fontStyle:\"italic\"},\"hljs-strong\":{fontWeight:\"bold\"}}},BO=LO,components_SyntaxHighlighter=({language:s,className:o=\"\",getConfigs:i,syntaxHighlighting:a={},children:u=\"\"})=>{const _=i().syntaxHighlight.theme,{styles:w,defaultStyle:x}=a,C=w?.[_]??x;return Re.createElement(EO,{language:s,className:o,style:C},u)};var $O=__webpack_require__(5419),qO=__webpack_require__.n($O);const components_HighlightCode=({fileName:s=\"response.txt\",className:o,downloadable:i,getComponent:a,canCopy:u,language:_,children:w})=>{const x=(0,Re.useRef)(null),C=a(\"SyntaxHighlighter\",!0),handlePreventYScrollingBeyondElement=s=>{const{target:o,deltaY:i}=s,{scrollHeight:a,offsetHeight:u,scrollTop:_}=o;a>u&&(0===_&&i<0||u+_>=a&&i>0)&&s.preventDefault()};return(0,Re.useEffect)((()=>{const s=Array.from(x.current.childNodes).filter((s=>!!s.nodeType&&s.classList.contains(\"microlight\")));return s.forEach((s=>s.addEventListener(\"mousewheel\",handlePreventYScrollingBeyondElement,{passive:!1}))),()=>{s.forEach((s=>s.removeEventListener(\"mousewheel\",handlePreventYScrollingBeyondElement)))}}),[w,o,_]),Re.createElement(\"div\",{className:\"highlight-code\",ref:x},u&&Re.createElement(\"div\",{className:\"copy-to-clipboard\"},Re.createElement(Hn.CopyToClipboard,{text:w},Re.createElement(\"button\",null))),i?Re.createElement(\"button\",{className:\"download-contents\",onClick:()=>{qO()(w,s)}},\"Download\"):null,Re.createElement(C,{language:_,className:Jn()(o,\"microlight\"),renderPlainText:({children:s,PlainTextViewer:i})=>Re.createElement(i,{className:o},s)},w))},components_PlainTextViewer=({className:s=\"\",children:o})=>Re.createElement(\"pre\",{className:Jn()(\"microlight\",s)},o),wrap_components_SyntaxHighlighter=(s,o)=>({renderPlainText:i,children:a,...u})=>{const _=o.getConfigs().syntaxHighlight.activated,w=o.getComponent(\"PlainTextViewer\");return _||\"function\"!=typeof i?_?Re.createElement(s,u,a):Re.createElement(w,null,a):i({children:a,PlainTextViewer:w})},SyntaxHighlightingPlugin1=()=>({afterLoad:after_load,rootInjects:{syntaxHighlighting:{styles:FO,defaultStyle:BO}},components:{SyntaxHighlighter:components_SyntaxHighlighter,HighlightCode:components_HighlightCode,PlainTextViewer:components_PlainTextViewer}}),SyntaxHighlightingPlugin2=()=>({wrapComponents:{SyntaxHighlighter:wrap_components_SyntaxHighlighter}}),syntax_highlighting=()=>[SyntaxHighlightingPlugin1,SyntaxHighlightingPlugin2],versions_after_load=()=>{const{GIT_DIRTY:s,GIT_COMMIT:o,PACKAGE_VERSION:i,BUILD_TIME:a}={PACKAGE_VERSION:\"5.31.0\",GIT_COMMIT:\"gcf11271c\",GIT_DIRTY:!0,BUILD_TIME:\"Thu, 11 Dec 2025 15:56:57 GMT\"};lt.versions=lt.versions||{},lt.versions.swaggerUI={version:i,gitRevision:o,gitDirty:s,buildTimestamp:a}},versions=()=>({afterLoad:versions_after_load});var UO=__webpack_require__(47248),VO=__webpack_require__.n(UO);const zO=console.error,withErrorBoundary=s=>o=>{const{getComponent:i,fn:a}=s(),u=i(\"ErrorBoundary\"),_=a.getDisplayName(o);class WithErrorBoundary extends Re.Component{render(){return Re.createElement(u,{targetName:_,getComponent:i,fn:a},Re.createElement(o,Mn()({},this.props,this.context)))}}var w;return WithErrorBoundary.displayName=`WithErrorBoundary(${_})`,(w=o).prototype&&w.prototype.isReactComponent&&(WithErrorBoundary.prototype.mapStateToProps=o.prototype.mapStateToProps),WithErrorBoundary},fallback=({name:s})=>Re.createElement(\"div\",{className:\"fallback\"},\"😱 \",Re.createElement(\"i\",null,\"Could not render \",\"t\"===s?\"this component\":s,\", see the console.\"));class ErrorBoundary extends Re.Component{static defaultProps={targetName:\"this component\",getComponent:()=>fallback,fn:{componentDidCatch:zO},children:null};static getDerivedStateFromError(s){return{hasError:!0,error:s}}constructor(...s){super(...s),this.state={hasError:!1,error:null}}componentDidCatch(s,o){this.props.fn.componentDidCatch(s,o)}render(){const{getComponent:s,targetName:o,children:i}=this.props;if(this.state.hasError){const i=s(\"Fallback\");return Re.createElement(i,{name:o})}return i}}const WO=ErrorBoundary,safe_render=({componentList:s=[],fullOverride:o=!1}={})=>({getSystem:i})=>{const a=o?s:[\"App\",\"BaseLayout\",\"VersionPragmaFilter\",\"InfoContainer\",\"ServersContainer\",\"SchemesContainer\",\"AuthorizeBtnContainer\",\"FilterContainer\",\"Operations\",\"OperationContainer\",\"parameters\",\"responses\",\"OperationServers\",\"Models\",\"ModelWrapper\",...s],u=VO()(a,Array(a.length).fill(((s,{fn:o})=>o.withErrorBoundary(s))));return{fn:{componentDidCatch:zO,withErrorBoundary:withErrorBoundary(i)},components:{ErrorBoundary:WO,Fallback:fallback},wrapComponents:u}};class App extends Re.Component{getLayout(){const{getComponent:s,layoutSelectors:o}=this.props,i=o.current(),a=s(i,!0);return a||(()=>Re.createElement(\"h1\",null,' No layout defined for \"',i,'\" '))}render(){const s=this.getLayout();return Re.createElement(s,null)}}const JO=App;class AuthorizationPopup extends Re.Component{close=()=>{let{authActions:s}=this.props;s.showDefinitions(!1)};render(){let{authSelectors:s,authActions:o,getComponent:i,errSelectors:a,specSelectors:u,fn:{AST:_={}}}=this.props,w=s.shownDefinitions();const x=i(\"auths\"),C=i(\"CloseIcon\");return Re.createElement(\"div\",{className:\"dialog-ux\"},Re.createElement(\"div\",{className:\"backdrop-ux\"}),Re.createElement(\"div\",{className:\"modal-ux\"},Re.createElement(\"div\",{className:\"modal-dialog-ux\"},Re.createElement(\"div\",{className:\"modal-ux-inner\"},Re.createElement(\"div\",{className:\"modal-ux-header\"},Re.createElement(\"h3\",null,\"Available authorizations\"),Re.createElement(\"button\",{type:\"button\",className:\"close-modal\",onClick:this.close},Re.createElement(C,null))),Re.createElement(\"div\",{className:\"modal-ux-content\"},w.valueSeq().map(((w,C)=>Re.createElement(x,{key:C,AST:_,definitions:w,getComponent:i,errSelectors:a,authSelectors:s,authActions:o,specSelectors:u}))))))))}}class AuthorizeBtn extends Re.Component{render(){let{isAuthorized:s,showPopup:o,onClick:i,getComponent:a}=this.props;const u=a(\"authorizationPopup\",!0),_=a(\"LockAuthIcon\",!0),w=a(\"UnlockAuthIcon\",!0);return Re.createElement(\"div\",{className:\"auth-wrapper\"},Re.createElement(\"button\",{className:s?\"btn authorize locked\":\"btn authorize unlocked\",onClick:i},Re.createElement(\"span\",null,\"Authorize\"),s?Re.createElement(_,null):Re.createElement(w,null)),o&&Re.createElement(u,null))}}class AuthorizeBtnContainer extends Re.Component{render(){const{authActions:s,authSelectors:o,specSelectors:i,getComponent:a}=this.props,u=i.securityDefinitions(),_=o.definitionsToAuthorize(),w=a(\"authorizeBtn\");return u?Re.createElement(w,{onClick:()=>s.showDefinitions(_),isAuthorized:!!o.authorized().size,showPopup:!!o.shownDefinitions(),getComponent:a}):null}}class AuthorizeOperationBtn extends Re.Component{onClick=s=>{s.stopPropagation();let{onClick:o}=this.props;o&&o()};render(){let{isAuthorized:s,getComponent:o}=this.props;const i=o(\"LockAuthOperationIcon\",!0),a=o(\"UnlockAuthOperationIcon\",!0);return Re.createElement(\"button\",{className:\"authorization__btn\",\"aria-label\":s?\"authorization button locked\":\"authorization button unlocked\",onClick:this.onClick},s?Re.createElement(i,{className:\"locked\"}):Re.createElement(a,{className:\"unlocked\"}))}}class Auths extends Re.Component{constructor(s,o){super(s,o),this.state={}}onAuthChange=s=>{let{name:o}=s;this.setState({[o]:s})};submitAuth=s=>{s.preventDefault();let{authActions:o}=this.props;o.authorizeWithPersistOption(this.state)};logoutClick=s=>{s.preventDefault();let{authActions:o,definitions:i}=this.props,a=i.map(((s,o)=>o)).toArray();this.setState(a.reduce(((s,o)=>(s[o]=\"\",s)),{})),o.logoutWithPersistOption(a)};close=s=>{s.preventDefault();let{authActions:o}=this.props;o.showDefinitions(!1)};render(){let{definitions:s,getComponent:o,authSelectors:i,errSelectors:a}=this.props;const u=o(\"AuthItem\"),_=o(\"oauth2\",!0),w=o(\"Button\");let x=i.authorized(),C=s.filter(((s,o)=>!!x.get(o))),j=s.filter((s=>\"oauth2\"!==s.get(\"type\"))),L=s.filter((s=>\"oauth2\"===s.get(\"type\")));return Re.createElement(\"div\",{className:\"auth-container\"},!!j.size&&Re.createElement(\"form\",{onSubmit:this.submitAuth},j.map(((s,_)=>Re.createElement(u,{key:_,schema:s,name:_,getComponent:o,onAuthChange:this.onAuthChange,authorized:x,errSelectors:a,authSelectors:i}))).toArray(),Re.createElement(\"div\",{className:\"auth-btn-wrapper\"},j.size===C.size?Re.createElement(w,{className:\"btn modal-btn auth\",onClick:this.logoutClick,\"aria-label\":\"Remove authorization\"},\"Logout\"):Re.createElement(w,{type:\"submit\",className:\"btn modal-btn auth authorize\",\"aria-label\":\"Apply credentials\"},\"Authorize\"),Re.createElement(w,{className:\"btn modal-btn auth btn-done\",onClick:this.close},\"Close\"))),L&&L.size?Re.createElement(\"div\",null,Re.createElement(\"div\",{className:\"scope-def\"},Re.createElement(\"p\",null,\"Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.\"),Re.createElement(\"p\",null,\"API requires the following scopes. Select which ones you want to grant to Swagger UI.\")),s.filter((s=>\"oauth2\"===s.get(\"type\"))).map(((s,o)=>Re.createElement(\"div\",{key:o},Re.createElement(_,{authorized:x,schema:s,name:o})))).toArray()):null)}}class auth_item_Auths extends Re.Component{render(){let{schema:s,name:o,getComponent:i,onAuthChange:a,authorized:u,errSelectors:_,authSelectors:w}=this.props;const x=i(\"apiKeyAuth\"),C=i(\"basicAuth\");let j;const L=s.get(\"type\");switch(L){case\"apiKey\":j=Re.createElement(x,{key:o,schema:s,name:o,errSelectors:_,authorized:u,getComponent:i,onChange:a,authSelectors:w});break;case\"basic\":j=Re.createElement(C,{key:o,schema:s,name:o,errSelectors:_,authorized:u,getComponent:i,onChange:a,authSelectors:w});break;default:j=Re.createElement(\"div\",{key:o},\"Unknown security definition type \",L)}return Re.createElement(\"div\",{key:`${o}-jump`},j)}}class AuthError extends Re.Component{render(){let{error:s}=this.props,o=s.get(\"level\"),i=s.get(\"message\"),a=s.get(\"source\");return Re.createElement(\"div\",{className:\"errors\"},Re.createElement(\"b\",null,a,\" \",o),Re.createElement(\"span\",null,i))}}class ApiKeyAuth extends Re.Component{constructor(s,o){super(s,o);let{name:i,schema:a}=this.props,u=this.getValue();this.state={name:i,schema:a,value:u}}getValue(){let{name:s,authorized:o}=this.props;return o&&o.getIn([s,\"value\"])}onChange=s=>{let{onChange:o}=this.props,i=s.target.value,a=Object.assign({},this.state,{value:i});this.setState(a),o(a)};render(){let{schema:s,getComponent:o,errSelectors:i,name:a,authSelectors:u}=this.props;const _=o(\"Input\"),w=o(\"Row\"),x=o(\"Col\"),C=o(\"authError\"),j=o(\"Markdown\",!0),L=o(\"JumpToPath\",!0),B=u.selectAuthPath(a);let $=this.getValue(),U=i.allErrors().filter((s=>s.get(\"authId\")===a));return Re.createElement(\"div\",null,Re.createElement(\"h4\",null,Re.createElement(\"code\",null,a||s.get(\"name\")),\" (apiKey)\",Re.createElement(L,{path:B})),$&&Re.createElement(\"h6\",null,\"Authorized\"),Re.createElement(w,null,Re.createElement(j,{source:s.get(\"description\")})),Re.createElement(w,null,Re.createElement(\"p\",null,\"Name: \",Re.createElement(\"code\",null,s.get(\"name\")))),Re.createElement(w,null,Re.createElement(\"p\",null,\"In: \",Re.createElement(\"code\",null,s.get(\"in\")))),Re.createElement(w,null,Re.createElement(\"label\",{htmlFor:\"api_key_value\"},\"Value:\"),$?Re.createElement(\"code\",null,\" ****** \"):Re.createElement(x,null,Re.createElement(_,{id:\"api_key_value\",type:\"text\",onChange:this.onChange,autoFocus:!0}))),U.valueSeq().map(((s,o)=>Re.createElement(C,{error:s,key:o}))))}}class BasicAuth extends Re.Component{constructor(s,o){super(s,o);let{schema:i,name:a}=this.props,u=this.getValue().username;this.state={name:a,schema:i,value:u?{username:u}:{}}}getValue(){let{authorized:s,name:o}=this.props;return s&&s.getIn([o,\"value\"])||{}}onChange=s=>{let{onChange:o}=this.props,{value:i,name:a}=s.target,u=this.state.value;u[a]=i,this.setState({value:u}),o(this.state)};render(){let{schema:s,getComponent:o,name:i,errSelectors:a,authSelectors:u}=this.props;const _=o(\"Input\"),w=o(\"Row\"),x=o(\"Col\"),C=o(\"authError\"),j=o(\"JumpToPath\",!0),L=o(\"Markdown\",!0),B=u.selectAuthPath(i);let $=this.getValue().username,U=a.allErrors().filter((s=>s.get(\"authId\")===i));return Re.createElement(\"div\",null,Re.createElement(\"h4\",null,\"Basic authorization\",Re.createElement(j,{path:B})),$&&Re.createElement(\"h6\",null,\"Authorized\"),Re.createElement(w,null,Re.createElement(L,{source:s.get(\"description\")})),Re.createElement(w,null,Re.createElement(\"label\",{htmlFor:\"auth_username\"},\"Username:\"),$?Re.createElement(\"code\",null,\" \",$,\" \"):Re.createElement(x,null,Re.createElement(_,{id:\"auth_username\",type:\"text\",required:\"required\",name:\"username\",onChange:this.onChange,autoFocus:!0}))),Re.createElement(w,null,Re.createElement(\"label\",{htmlFor:\"auth_password\"},\"Password:\"),$?Re.createElement(\"code\",null,\" ****** \"):Re.createElement(x,null,Re.createElement(_,{id:\"auth_password\",autoComplete:\"new-password\",name:\"password\",type:\"password\",onChange:this.onChange}))),U.valueSeq().map(((s,o)=>Re.createElement(C,{error:s,key:o}))))}}function example_Example(s){const{example:o,showValue:i,getComponent:a}=s,u=a(\"Markdown\",!0),_=a(\"HighlightCode\",!0);return o&&ze.Map.isMap(o)?Re.createElement(\"div\",{className:\"example\"},o.get(\"description\")?Re.createElement(\"section\",{className:\"example__section\"},Re.createElement(\"div\",{className:\"example__section-header\"},\"Example Description\"),Re.createElement(\"p\",null,Re.createElement(u,{source:o.get(\"description\")}))):null,i&&o.has(\"value\")?Re.createElement(\"section\",{className:\"example__section\"},Re.createElement(\"div\",{className:\"example__section-header\"},\"Example Value\"),Re.createElement(_,null,stringify(o.get(\"value\")))):null):null}class ExamplesSelect extends Re.PureComponent{static defaultProps={examples:(0,ze.Map)({}),onSelect:(...s)=>console.log(\"DEBUG: ExamplesSelect was not given an onSelect callback\",...s),currentExampleKey:null,showLabels:!0};_onSelect=(s,{isSyntheticChange:o=!1}={})=>{\"function\"==typeof this.props.onSelect&&this.props.onSelect(s,{isSyntheticChange:o})};_onDomSelect=s=>{if(\"function\"==typeof this.props.onSelect){const o=s.target.selectedOptions[0].getAttribute(\"value\");this._onSelect(o,{isSyntheticChange:!1})}};getCurrentExample=()=>{const{examples:s,currentExampleKey:o}=this.props,i=s.get(o),a=s.keySeq().first(),u=s.get(a);return i||u||(0,ze.Map)({})};componentDidMount(){const{onSelect:s,examples:o}=this.props;if(\"function\"==typeof s){const s=o.first(),i=o.keyOf(s);this._onSelect(i,{isSyntheticChange:!0})}}UNSAFE_componentWillReceiveProps(s){const{currentExampleKey:o,examples:i}=s;if(i!==this.props.examples&&!i.has(o)){const s=i.first(),o=i.keyOf(s);this._onSelect(o,{isSyntheticChange:!0})}}render(){const{examples:s,currentExampleKey:o,isValueModified:i,isModifiedValueAvailable:a,showLabels:u}=this.props;return Re.createElement(\"div\",{className:\"examples-select\"},u?Re.createElement(\"span\",{className:\"examples-select__section-label\"},\"Examples: \"):null,Re.createElement(\"select\",{className:\"examples-select-element\",onChange:this._onDomSelect,value:a&&i?\"__MODIFIED__VALUE__\":o||\"\"},a?Re.createElement(\"option\",{value:\"__MODIFIED__VALUE__\"},\"[Modified value]\"):null,s.map(((s,o)=>Re.createElement(\"option\",{key:o,value:o},ze.Map.isMap(s)&&s.get(\"summary\")||o))).valueSeq()))}}const stringifyUnlessList=s=>ze.List.isList(s)?s:stringify(s);class ExamplesSelectValueRetainer extends Re.PureComponent{static defaultProps={userHasEditedBody:!1,examples:(0,ze.Map)({}),currentNamespace:\"__DEFAULT__NAMESPACE__\",setRetainRequestBodyValueFlag:()=>{},onSelect:(...s)=>console.log(\"ExamplesSelectValueRetainer: no `onSelect` function was provided\",...s),updateValue:(...s)=>console.log(\"ExamplesSelectValueRetainer: no `updateValue` function was provided\",...s)};constructor(s){super(s);const o=this._getCurrentExampleValue();this.state={[s.currentNamespace]:(0,ze.Map)({lastUserEditedValue:this.props.currentUserInputValue,lastDownstreamValue:o,isModifiedValueSelected:this.props.userHasEditedBody||this.props.currentUserInputValue!==o})}}componentWillUnmount(){this.props.setRetainRequestBodyValueFlag(!1)}_getStateForCurrentNamespace=()=>{const{currentNamespace:s}=this.props;return(this.state[s]||(0,ze.Map)()).toObject()};_setStateForCurrentNamespace=s=>{const{currentNamespace:o}=this.props;return this._setStateForNamespace(o,s)};_setStateForNamespace=(s,o)=>{const i=(this.state[s]||(0,ze.Map)()).mergeDeep(o);return this.setState({[s]:i})};_isCurrentUserInputSameAsExampleValue=()=>{const{currentUserInputValue:s}=this.props;return this._getCurrentExampleValue()===s};_getValueForExample=(s,o)=>{const{examples:i}=o||this.props;return stringifyUnlessList((i||(0,ze.Map)({})).getIn([s,\"value\"]))};_getCurrentExampleValue=s=>{const{currentKey:o}=s||this.props;return this._getValueForExample(o,s||this.props)};_onExamplesSelect=(s,{isSyntheticChange:o}={},...i)=>{const{onSelect:a,updateValue:u,currentUserInputValue:_,userHasEditedBody:w}=this.props,{lastUserEditedValue:x}=this._getStateForCurrentNamespace(),C=this._getValueForExample(s);if(\"__MODIFIED__VALUE__\"===s)return u(stringifyUnlessList(x)),this._setStateForCurrentNamespace({isModifiedValueSelected:!0});\"function\"==typeof a&&a(s,{isSyntheticChange:o},...i),this._setStateForCurrentNamespace({lastDownstreamValue:C,isModifiedValueSelected:o&&w||!!_&&_!==C}),o||\"function\"==typeof u&&u(stringifyUnlessList(C))};UNSAFE_componentWillReceiveProps(s){const{currentUserInputValue:o,examples:i,onSelect:a,userHasEditedBody:u}=s,{lastUserEditedValue:_,lastDownstreamValue:w}=this._getStateForCurrentNamespace(),x=this._getValueForExample(s.currentKey,s),C=i.filter((s=>ze.Map.isMap(s)&&(s.get(\"value\")===o||stringify(s.get(\"value\"))===o)));if(C.size){let o;o=C.has(s.currentKey)?s.currentKey:C.keySeq().first(),a(o,{isSyntheticChange:!0})}else o!==this.props.currentUserInputValue&&o!==_&&o!==w&&(this.props.setRetainRequestBodyValueFlag(!0),this._setStateForNamespace(s.currentNamespace,{lastUserEditedValue:s.currentUserInputValue,isModifiedValueSelected:u||o!==x}))}render(){const{currentUserInputValue:s,examples:o,currentKey:i,getComponent:a,userHasEditedBody:u}=this.props,{lastDownstreamValue:_,lastUserEditedValue:w,isModifiedValueSelected:x}=this._getStateForCurrentNamespace(),C=a(\"ExamplesSelect\");return Re.createElement(C,{examples:o,currentExampleKey:i,onSelect:this._onExamplesSelect,isModifiedValueAvailable:!!w&&w!==_,isValueModified:void 0!==s&&x&&s!==this._getCurrentExampleValue()||u})}}function oauth2_authorize_authorize({auth:s,authActions:o,errActions:i,configs:a,authConfigs:u={},currentServer:_}){let{schema:w,scopes:x,name:C,clientId:j}=s,L=w.get(\"flow\"),B=[];switch(L){case\"password\":return void o.authorizePassword(s);case\"application\":case\"clientCredentials\":case\"client_credentials\":return void o.authorizeApplication(s);case\"accessCode\":case\"authorizationCode\":case\"authorization_code\":B.push(\"response_type=code\");break;case\"implicit\":B.push(\"response_type=token\")}\"string\"==typeof j&&B.push(\"client_id=\"+encodeURIComponent(j));let $=a.oauth2RedirectUrl;if(void 0===$)return void i.newAuthErr({authId:C,source:\"validation\",level:\"error\",message:\"oauth2RedirectUrl configuration is not passed. Oauth2 authorization cannot be performed.\"});B.push(\"redirect_uri=\"+encodeURIComponent($));let U=[];if(Array.isArray(x)?U=x:We().List.isList(x)&&(U=x.toArray()),U.length>0){let s=u.scopeSeparator||\" \";B.push(\"scope=\"+encodeURIComponent(U.join(s)))}let V=utils_btoa(new Date);if(B.push(\"state=\"+encodeURIComponent(V)),void 0!==u.realm&&B.push(\"realm=\"+encodeURIComponent(u.realm)),(\"authorizationCode\"===L||\"authorization_code\"===L||\"accessCode\"===L)&&u.usePkceWithAuthorizationCodeGrant){const o=function generateCodeVerifier(){return b64toB64UrlEncoded(xt()(32).toString(\"base64\"))}(),i=function createCodeChallenge(s){return b64toB64UrlEncoded(Ot()(\"sha256\").update(s).digest(\"base64\"))}(o);B.push(\"code_challenge=\"+i),B.push(\"code_challenge_method=S256\"),s.codeVerifier=o}let{additionalQueryStringParams:z}=u;for(let s in z)void 0!==z[s]&&B.push([s,z[s]].map(encodeURIComponent).join(\"=\"));const Y=w.get(\"authorizationUrl\");let Z;Z=_?Nt()(sanitizeUrl(Y),_,!0).toString():sanitizeUrl(Y);let ee,ie=[Z,B.join(\"&\")].join(\"string\"!=typeof Y||Y.includes(\"?\")?\"&\":\"?\");ee=\"implicit\"===L?o.preAuthorizeImplicit:u.useBasicAuthenticationWithAccessCodeGrant?o.authorizeAccessCodeWithBasicAuthentication:o.authorizeAccessCodeWithFormParams,o.authPopup(ie,{auth:s,state:V,redirectUrl:$,callback:ee,errCb:i.newAuthErr})}class Oauth2 extends Re.Component{constructor(s,o){super(s,o);let{name:i,schema:a,authorized:u,authSelectors:_}=this.props,w=u&&u.get(i),x=_.getConfigs()||{},C=w&&w.get(\"username\")||\"\",j=w&&w.get(\"clientId\")||x.clientId||\"\",L=w&&w.get(\"clientSecret\")||x.clientSecret||\"\",B=w&&w.get(\"passwordType\")||\"basic\",$=w&&w.get(\"scopes\")||x.scopes||[];\"string\"==typeof $&&($=$.split(x.scopeSeparator||\" \")),this.state={appName:x.appName,name:i,schema:a,scopes:$,clientId:j,clientSecret:L,username:C,password:\"\",passwordType:B}}close=s=>{s.preventDefault();let{authActions:o}=this.props;o.showDefinitions(!1)};authorize=()=>{let{authActions:s,errActions:o,getConfigs:i,authSelectors:a,oas3Selectors:u}=this.props,_=i(),w=a.getConfigs();o.clear({authId:name,type:\"auth\",source:\"auth\"}),oauth2_authorize_authorize({auth:this.state,currentServer:u.serverEffectiveValue(u.selectedServer()),authActions:s,errActions:o,configs:_,authConfigs:w})};onScopeChange=s=>{let{target:o}=s,{checked:i}=o,a=o.dataset.value;if(i&&-1===this.state.scopes.indexOf(a)){let s=this.state.scopes.concat([a]);this.setState({scopes:s})}else!i&&this.state.scopes.indexOf(a)>-1&&this.setState({scopes:this.state.scopes.filter((s=>s!==a))})};onInputChange=s=>{let{target:{dataset:{name:o},value:i}}=s,a={[o]:i};this.setState(a)};selectScopes=s=>{s.target.dataset.all?this.setState({scopes:Array.from((this.props.schema.get(\"allowedScopes\")||this.props.schema.get(\"scopes\")).keys())}):this.setState({scopes:[]})};logout=s=>{s.preventDefault();let{authActions:o,errActions:i,name:a}=this.props;i.clear({authId:a,type:\"auth\",source:\"auth\"}),o.logoutWithPersistOption([a])};render(){let{schema:s,getComponent:o,authSelectors:i,errSelectors:a,name:u,specSelectors:_}=this.props;const w=o(\"Input\"),x=o(\"Row\"),C=o(\"Col\"),j=o(\"Button\"),L=o(\"authError\"),B=o(\"JumpToPath\",!0),$=o(\"Markdown\",!0),U=o(\"InitializedInput\"),{isOAS3:V}=_;let z=V()?s.get(\"openIdConnectUrl\"):null;const Y=\"implicit\",Z=\"password\",ee=V()?z?\"authorization_code\":\"authorizationCode\":\"accessCode\",ie=V()?z?\"client_credentials\":\"clientCredentials\":\"application\",ae=i.selectAuthPath(u);let ce=!!(i.getConfigs()||{}).usePkceWithAuthorizationCodeGrant,le=s.get(\"flow\"),pe=le===ee&&ce?le+\" with PKCE\":le,de=s.get(\"allowedScopes\")||s.get(\"scopes\"),fe=!!i.authorized().get(u),ye=a.allErrors().filter((s=>s.get(\"authId\")===u)),be=!ye.filter((s=>\"validation\"===s.get(\"source\"))).size,_e=s.get(\"description\");return Re.createElement(\"div\",null,Re.createElement(\"h4\",null,u,\" (OAuth2, \",pe,\") \",Re.createElement(B,{path:ae})),this.state.appName?Re.createElement(\"h5\",null,\"Application: \",this.state.appName,\" \"):null,_e&&Re.createElement($,{source:s.get(\"description\")}),fe&&Re.createElement(\"h6\",null,\"Authorized\"),z&&Re.createElement(\"p\",null,\"OpenID Connect URL: \",Re.createElement(\"code\",null,z)),(le===Y||le===ee)&&Re.createElement(\"p\",null,\"Authorization URL: \",Re.createElement(\"code\",null,s.get(\"authorizationUrl\"))),(le===Z||le===ee||le===ie)&&Re.createElement(\"p\",null,\"Token URL:\",Re.createElement(\"code\",null,\" \",s.get(\"tokenUrl\"))),Re.createElement(\"p\",{className:\"flow\"},\"Flow: \",Re.createElement(\"code\",null,pe)),le!==Z?null:Re.createElement(x,null,Re.createElement(x,null,Re.createElement(\"label\",{htmlFor:\"oauth_username\"},\"username:\"),fe?Re.createElement(\"code\",null,\" \",this.state.username,\" \"):Re.createElement(C,{tablet:10,desktop:10},Re.createElement(\"input\",{id:\"oauth_username\",type:\"text\",\"data-name\":\"username\",onChange:this.onInputChange,autoFocus:!0}))),Re.createElement(x,null,Re.createElement(\"label\",{htmlFor:\"oauth_password\"},\"password:\"),fe?Re.createElement(\"code\",null,\" ****** \"):Re.createElement(C,{tablet:10,desktop:10},Re.createElement(\"input\",{id:\"oauth_password\",type:\"password\",\"data-name\":\"password\",onChange:this.onInputChange}))),Re.createElement(x,null,Re.createElement(\"label\",{htmlFor:\"password_type\"},\"Client credentials location:\"),fe?Re.createElement(\"code\",null,\" \",this.state.passwordType,\" \"):Re.createElement(C,{tablet:10,desktop:10},Re.createElement(\"select\",{id:\"password_type\",\"data-name\":\"passwordType\",onChange:this.onInputChange},Re.createElement(\"option\",{value:\"basic\"},\"Authorization header\"),Re.createElement(\"option\",{value:\"request-body\"},\"Request body\"))))),(le===ie||le===Y||le===ee||le===Z)&&(!fe||fe&&this.state.clientId)&&Re.createElement(x,null,Re.createElement(\"label\",{htmlFor:`client_id_${le}`},\"client_id:\"),fe?Re.createElement(\"code\",null,\" ****** \"):Re.createElement(C,{tablet:10,desktop:10},Re.createElement(U,{id:`client_id_${le}`,type:\"text\",required:le===Z,initialValue:this.state.clientId,\"data-name\":\"clientId\",onChange:this.onInputChange}))),(le===ie||le===ee||le===Z)&&Re.createElement(x,null,Re.createElement(\"label\",{htmlFor:`client_secret_${le}`},\"client_secret:\"),fe?Re.createElement(\"code\",null,\" ****** \"):Re.createElement(C,{tablet:10,desktop:10},Re.createElement(U,{id:`client_secret_${le}`,initialValue:this.state.clientSecret,type:\"password\",\"data-name\":\"clientSecret\",onChange:this.onInputChange}))),!fe&&de&&de.size?Re.createElement(\"div\",{className:\"scopes\"},Re.createElement(\"h2\",null,\"Scopes:\",Re.createElement(\"a\",{onClick:this.selectScopes,\"data-all\":!0},\"select all\"),Re.createElement(\"a\",{onClick:this.selectScopes},\"select none\")),de.map(((s,o)=>Re.createElement(x,{key:o},Re.createElement(\"div\",{className:\"checkbox\"},Re.createElement(w,{\"data-value\":o,id:`${o}-${le}-checkbox-${this.state.name}`,disabled:fe,checked:this.state.scopes.includes(o),type:\"checkbox\",onChange:this.onScopeChange}),Re.createElement(\"label\",{htmlFor:`${o}-${le}-checkbox-${this.state.name}`},Re.createElement(\"span\",{className:\"item\"}),Re.createElement(\"div\",{className:\"text\"},Re.createElement(\"p\",{className:\"name\"},o),Re.createElement(\"p\",{className:\"description\"},s))))))).toArray()):null,ye.valueSeq().map(((s,o)=>Re.createElement(L,{error:s,key:o}))),Re.createElement(\"div\",{className:\"auth-btn-wrapper\"},be&&(fe?Re.createElement(j,{className:\"btn modal-btn auth authorize\",onClick:this.logout,\"aria-label\":\"Remove authorization\"},\"Logout\"):Re.createElement(j,{className:\"btn modal-btn auth authorize\",onClick:this.authorize,\"aria-label\":\"Apply given OAuth2 credentials\"},\"Authorize\")),Re.createElement(j,{className:\"btn modal-btn auth btn-done\",onClick:this.close},\"Close\")))}}class Clear extends Re.Component{onClick=()=>{let{specActions:s,path:o,method:i}=this.props;s.clearResponse(o,i),s.clearRequest(o,i)};render(){return Re.createElement(\"button\",{className:\"btn btn-clear opblock-control__btn\",onClick:this.onClick},\"Clear\")}}const live_response_Headers=({headers:s})=>Re.createElement(\"div\",null,Re.createElement(\"h5\",null,\"Response headers\"),Re.createElement(\"pre\",{className:\"microlight\"},s)),Duration=({duration:s})=>Re.createElement(\"div\",null,Re.createElement(\"h5\",null,\"Request duration\"),Re.createElement(\"pre\",{className:\"microlight\"},s,\" ms\"));class LiveResponse extends Re.Component{shouldComponentUpdate(s){return this.props.response!==s.response||this.props.path!==s.path||this.props.method!==s.method||this.props.displayRequestDuration!==s.displayRequestDuration}render(){const{response:s,getComponent:o,getConfigs:i,displayRequestDuration:a,specSelectors:u,path:_,method:w}=this.props,{showMutatedRequest:x,requestSnippetsEnabled:C}=i(),j=x?u.mutatedRequestFor(_,w):u.requestFor(_,w),L=s.get(\"status\"),B=j.get(\"url\"),$=s.get(\"headers\").toJS(),U=s.get(\"notDocumented\"),V=s.get(\"error\"),z=s.get(\"text\"),Y=s.get(\"duration\"),Z=Object.keys($),ee=$[\"content-type\"]||$[\"Content-Type\"],ie=o(\"responseBody\"),ae=Z.map((s=>{var o=Array.isArray($[s])?$[s].join():$[s];return Re.createElement(\"span\",{className:\"headerline\",key:s},\" \",s,\": \",o,\" \")})),ce=0!==ae.length,le=o(\"Markdown\",!0),pe=o(\"RequestSnippets\",!0),de=o(\"curl\",!0);return Re.createElement(\"div\",null,j&&C?Re.createElement(pe,{request:j}):Re.createElement(de,{request:j}),B&&Re.createElement(\"div\",null,Re.createElement(\"div\",{className:\"request-url\"},Re.createElement(\"h4\",null,\"Request URL\"),Re.createElement(\"pre\",{className:\"microlight\"},B))),Re.createElement(\"h4\",null,\"Server response\"),Re.createElement(\"table\",{className:\"responses-table live-responses-table\"},Re.createElement(\"thead\",null,Re.createElement(\"tr\",{className:\"responses-header\"},Re.createElement(\"td\",{className:\"col_header response-col_status\"},\"Code\"),Re.createElement(\"td\",{className:\"col_header response-col_description\"},\"Details\"))),Re.createElement(\"tbody\",null,Re.createElement(\"tr\",{className:\"response\"},Re.createElement(\"td\",{className:\"response-col_status\"},L,U?Re.createElement(\"div\",{className:\"response-undocumented\"},Re.createElement(\"i\",null,\" Undocumented \")):null),Re.createElement(\"td\",{className:\"response-col_description\"},V?Re.createElement(le,{source:`${\"\"!==s.get(\"name\")?`${s.get(\"name\")}: `:\"\"}${s.get(\"message\")}`}):null,z?Re.createElement(ie,{content:z,contentType:ee,url:B,headers:$,getConfigs:i,getComponent:o}):null,ce?Re.createElement(live_response_Headers,{headers:ae}):null,a&&Y?Re.createElement(Duration,{duration:Y}):null)))))}}class OnlineValidatorBadge extends Re.Component{constructor(s,o){super(s,o);let{getConfigs:i}=s,{validatorUrl:a}=i();this.state={url:this.getDefinitionUrl(),validatorUrl:void 0===a?\"https://validator.swagger.io/validator\":a}}getDefinitionUrl=()=>{let{specSelectors:s}=this.props;return new(Nt())(s.url(),lt.location).toString()};UNSAFE_componentWillReceiveProps(s){let{getConfigs:o}=s,{validatorUrl:i}=o();this.setState({url:this.getDefinitionUrl(),validatorUrl:void 0===i?\"https://validator.swagger.io/validator\":i})}render(){let{getConfigs:s}=this.props,{spec:o}=s(),i=sanitizeUrl(this.state.validatorUrl);return\"object\"==typeof o&&Object.keys(o).length?null:this.state.url&&requiresValidationURL(this.state.validatorUrl)&&requiresValidationURL(this.state.url)?Re.createElement(\"span\",{className:\"float-right\"},Re.createElement(\"a\",{target:\"_blank\",rel:\"noopener noreferrer\",href:`${i}/debug?url=${encodeURIComponent(this.state.url)}`},Re.createElement(ValidatorImage,{src:`${i}?url=${encodeURIComponent(this.state.url)}`,alt:\"Online validator badge\"}))):null}}class ValidatorImage extends Re.Component{constructor(s){super(s),this.state={loaded:!1,error:!1}}componentDidMount(){const s=new Image;s.onload=()=>{this.setState({loaded:!0})},s.onerror=()=>{this.setState({error:!0})},s.src=this.props.src}UNSAFE_componentWillReceiveProps(s){if(s.src!==this.props.src){const o=new Image;o.onload=()=>{this.setState({loaded:!0})},o.onerror=()=>{this.setState({error:!0})},o.src=s.src}}render(){return this.state.error?Re.createElement(\"img\",{alt:\"Error\"}):this.state.loaded?Re.createElement(\"img\",{src:this.props.src,alt:this.props.alt}):null}}class Operations extends Re.Component{render(){let{specSelectors:s}=this.props;const o=s.taggedOperations();return 0===o.size?Re.createElement(\"h3\",null,\" No operations defined in spec!\"):Re.createElement(\"div\",null,o.map(this.renderOperationTag).toArray(),o.size<1?Re.createElement(\"h3\",null,\" No operations defined in spec! \"):null)}renderOperationTag=(s,o)=>{const{specSelectors:i,getComponent:a,oas3Selectors:u,layoutSelectors:_,layoutActions:w,getConfigs:x}=this.props,C=i.validOperationMethods(),j=a(\"OperationContainer\",!0),L=a(\"OperationTag\"),B=s.get(\"operations\");return Re.createElement(L,{key:\"operation-\"+o,tagObj:s,tag:o,oas3Selectors:u,layoutSelectors:_,layoutActions:w,getConfigs:x,getComponent:a,specUrl:i.url()},Re.createElement(\"div\",{className:\"operation-tag-content\"},B.map((s=>{const i=s.get(\"path\"),a=s.get(\"method\"),u=We().List([\"paths\",i,a]);return-1===C.indexOf(a)?null:Re.createElement(j,{key:`${i}-${a}`,specPath:u,op:s,path:i,method:a,tag:o})})).toArray()))}}class OperationTag extends Re.Component{static defaultProps={tagObj:We().fromJS({}),tag:\"\"};render(){const{tagObj:s,tag:o,children:i,oas3Selectors:a,layoutSelectors:u,layoutActions:_,getConfigs:w,getComponent:x,specUrl:C}=this.props;let{docExpansion:j,deepLinking:L}=w();const B=x(\"Collapse\"),$=x(\"Markdown\",!0),U=x(\"DeepLink\"),V=x(\"Link\"),z=x(\"ArrowUpIcon\"),Y=x(\"ArrowDownIcon\");let Z,ee=s.getIn([\"tagDetails\",\"description\"],null),ie=s.getIn([\"tagDetails\",\"externalDocs\",\"description\"]),ae=s.getIn([\"tagDetails\",\"externalDocs\",\"url\"]);Z=isFunc(a)&&isFunc(a.selectedServer)?safeBuildUrl(ae,C,{selectedServer:a.selectedServer()}):ae;let ce=[\"operations-tag\",o],le=u.isShown(ce,\"full\"===j||\"list\"===j);return Re.createElement(\"div\",{className:le?\"opblock-tag-section is-open\":\"opblock-tag-section\"},Re.createElement(\"h3\",{onClick:()=>_.show(ce,!le),className:ee?\"opblock-tag\":\"opblock-tag no-desc\",id:ce.map((s=>escapeDeepLinkPath(s))).join(\"-\"),\"data-tag\":o,\"data-is-open\":le},Re.createElement(U,{enabled:L,isShown:le,path:createDeepLinkPath(o),text:o}),ee?Re.createElement(\"small\",null,Re.createElement($,{source:ee})):Re.createElement(\"small\",null),Z?Re.createElement(\"div\",{className:\"info__externaldocs\"},Re.createElement(\"small\",null,Re.createElement(V,{href:sanitizeUrl(Z),onClick:s=>s.stopPropagation(),target:\"_blank\"},ie||Z))):null,Re.createElement(\"button\",{\"aria-expanded\":le,className:\"expand-operation\",title:le?\"Collapse operation\":\"Expand operation\",onClick:()=>_.show(ce,!le)},le?Re.createElement(z,{className:\"arrow\"}):Re.createElement(Y,{className:\"arrow\"}))),Re.createElement(B,{isOpened:le},i))}}class operation_Operation extends Re.PureComponent{static defaultProps={operation:null,response:null,request:null,specPath:(0,ze.List)(),summary:\"\"};render(){let{specPath:s,response:o,request:i,toggleShown:a,onTryoutClick:u,onResetClick:_,onCancelClick:w,onExecute:x,fn:C,getComponent:j,getConfigs:L,specActions:B,specSelectors:$,authActions:U,authSelectors:V,oas3Actions:z,oas3Selectors:Y}=this.props,Z=this.props.operation,{deprecated:ee,isShown:ie,path:ae,method:ce,op:le,tag:pe,operationId:de,allowTryItOut:fe,displayRequestDuration:ye,tryItOutEnabled:be,executeInProgress:_e}=Z.toJS(),{description:Se,externalDocs:we,schemes:xe}=le;const Pe=we?safeBuildUrl(we.url,$.url(),{selectedServer:Y.selectedServer()}):\"\";let Te=Z.getIn([\"op\"]),$e=Te.get(\"responses\"),qe=function getList(s,o){if(!We().Iterable.isIterable(s))return We().List();let i=s.getIn(Array.isArray(o)?o:[o]);return We().List.isList(i)?i:We().List()}(Te,[\"parameters\"]),ze=$.operationScheme(ae,ce),He=[\"operations\",pe,de],Ye=getExtensions(Te);const Xe=j(\"responses\"),Qe=j(\"parameters\"),et=j(\"execute\"),tt=j(\"clear\"),rt=j(\"Collapse\"),nt=j(\"Markdown\",!0),st=j(\"schemes\"),ot=j(\"OperationServers\"),it=j(\"OperationExt\"),at=j(\"OperationSummary\"),ct=j(\"Link\"),{showExtensions:lt}=L();if($e&&o&&o.size>0){let s=!$e.get(String(o.get(\"status\")))&&!$e.get(\"default\");o=o.set(\"notDocumented\",s)}let ut=[ae,ce];const pt=$.validationErrors([ae,ce]);return Re.createElement(\"div\",{className:ee?\"opblock opblock-deprecated\":ie?`opblock opblock-${ce} is-open`:`opblock opblock-${ce}`,id:escapeDeepLinkPath(He.join(\"-\"))},Re.createElement(at,{operationProps:Z,isShown:ie,toggleShown:a,getComponent:j,authActions:U,authSelectors:V,specPath:s}),Re.createElement(rt,{isOpened:ie},Re.createElement(\"div\",{className:\"opblock-body\"},Te&&Te.size||null===Te?null:Re.createElement(rolling_load,{height:\"32px\",width:\"32px\",className:\"opblock-loading-animation\"}),ee&&Re.createElement(\"h4\",{className:\"opblock-title_normal\"},\" Warning: Deprecated\"),Se&&Re.createElement(\"div\",{className:\"opblock-description-wrapper\"},Re.createElement(\"div\",{className:\"opblock-description\"},Re.createElement(nt,{source:Se}))),Pe?Re.createElement(\"div\",{className:\"opblock-external-docs-wrapper\"},Re.createElement(\"h4\",{className:\"opblock-title_normal\"},\"Find more details\"),Re.createElement(\"div\",{className:\"opblock-external-docs\"},we.description&&Re.createElement(\"span\",{className:\"opblock-external-docs__description\"},Re.createElement(nt,{source:we.description})),Re.createElement(ct,{target:\"_blank\",className:\"opblock-external-docs__link\",href:sanitizeUrl(Pe)},Pe))):null,Te&&Te.size?Re.createElement(Qe,{parameters:qe,specPath:s.push(\"parameters\"),operation:Te,onChangeKey:ut,onTryoutClick:u,onResetClick:_,onCancelClick:w,tryItOutEnabled:be,allowTryItOut:fe,fn:C,getComponent:j,specActions:B,specSelectors:$,pathMethod:[ae,ce],getConfigs:L,oas3Actions:z,oas3Selectors:Y}):null,be?Re.createElement(ot,{getComponent:j,path:ae,method:ce,operationServers:Te.get(\"servers\"),pathServers:$.paths().getIn([ae,\"servers\"]),getSelectedServer:Y.selectedServer,setSelectedServer:z.setSelectedServer,setServerVariableValue:z.setServerVariableValue,getServerVariable:Y.serverVariableValue,getEffectiveServerValue:Y.serverEffectiveValue}):null,be&&fe&&xe&&xe.size?Re.createElement(\"div\",{className:\"opblock-schemes\"},Re.createElement(st,{schemes:xe,path:ae,method:ce,specActions:B,currentScheme:ze})):null,!be||!fe||pt.length<=0?null:Re.createElement(\"div\",{className:\"validation-errors errors-wrapper\"},\"Please correct the following validation errors and try again.\",Re.createElement(\"ul\",null,pt.map(((s,o)=>Re.createElement(\"li\",{key:o},\" \",s,\" \"))))),Re.createElement(\"div\",{className:be&&o&&fe?\"btn-group\":\"execute-wrapper\"},be&&fe?Re.createElement(et,{operation:Te,specActions:B,specSelectors:$,oas3Selectors:Y,oas3Actions:z,path:ae,method:ce,onExecute:x,disabled:_e}):null,be&&o&&fe?Re.createElement(tt,{specActions:B,path:ae,method:ce}):null),_e?Re.createElement(\"div\",{className:\"loading-container\"},Re.createElement(\"div\",{className:\"loading\"})):null,$e?Re.createElement(Xe,{responses:$e,request:i,tryItOutResponse:o,getComponent:j,getConfigs:L,specSelectors:$,oas3Actions:z,oas3Selectors:Y,specActions:B,produces:$.producesOptionsFor([ae,ce]),producesValue:$.currentProducesFor([ae,ce]),specPath:s.push(\"responses\"),path:ae,method:ce,displayRequestDuration:ye,fn:C}):null,lt&&Ye.size?Re.createElement(it,{extensions:Ye,getComponent:j}):null)))}}class OperationContainer extends Re.PureComponent{constructor(s,o){super(s,o);const{tryItOutEnabled:i}=s.getConfigs();this.state={tryItOutEnabled:i,executeInProgress:!1}}static defaultProps={showSummary:!0,response:null,allowTryItOut:!0,displayOperationId:!1,displayRequestDuration:!1};mapStateToProps(s,o){const{op:i,layoutSelectors:a,getConfigs:u}=o,{docExpansion:_,deepLinking:w,displayOperationId:x,displayRequestDuration:C,supportedSubmitMethods:j}=u(),L=a.showSummary(),B=i.getIn([\"operation\",\"__originalOperationId\"])||i.getIn([\"operation\",\"operationId\"])||opId(i.get(\"operation\"),o.path,o.method)||i.get(\"id\"),$=[\"operations\",o.tag,B],U=j.indexOf(o.method)>=0&&(void 0===o.allowTryItOut?o.specSelectors.allowTryItOutFor(o.path,o.method):o.allowTryItOut),V=i.getIn([\"operation\",\"security\"])||o.specSelectors.security();return{operationId:B,isDeepLinkingEnabled:w,showSummary:L,displayOperationId:x,displayRequestDuration:C,allowTryItOut:U,security:V,isAuthorized:o.authSelectors.isAuthorized(V),isShown:a.isShown($,\"full\"===_),jumpToKey:`paths.${o.path}.${o.method}`,response:o.specSelectors.responseFor(o.path,o.method),request:o.specSelectors.requestFor(o.path,o.method)}}componentDidMount(){const{isShown:s}=this.props,o=this.getResolvedSubtree();s&&void 0===o&&this.requestResolvedSubtree()}componentDidUpdate(s){const{response:o,isShown:i}=this.props,a=this.getResolvedSubtree();o!==s.response&&this.setState({executeInProgress:!1}),i&&void 0===a&&!s.isShown&&this.requestResolvedSubtree()}toggleShown=()=>{let{layoutActions:s,tag:o,operationId:i,isShown:a}=this.props;const u=this.getResolvedSubtree();a||void 0!==u||this.requestResolvedSubtree(),s.show([\"operations\",o,i],!a)};onCancelClick=()=>{this.setState({tryItOutEnabled:!this.state.tryItOutEnabled})};onTryoutClick=()=>{this.setState({tryItOutEnabled:!this.state.tryItOutEnabled})};onResetClick=s=>{const o=this.props.oas3Selectors.selectDefaultRequestBodyValue(...s),i=this.props.oas3Selectors.requestContentType(...s);if(\"application/x-www-form-urlencoded\"===i||\"multipart/form-data\"===i){const i=JSON.parse(o);Object.entries(i).forEach((([s,o])=>{Array.isArray(o)?i[s]=i[s].map((s=>\"object\"==typeof s?JSON.stringify(s,null,2):s)):\"object\"==typeof o&&(i[s]=JSON.stringify(i[s],null,2))})),this.props.oas3Actions.setRequestBodyValue({value:(0,ze.fromJS)(i),pathMethod:s})}else this.props.oas3Actions.setRequestBodyValue({value:o,pathMethod:s})};onExecute=()=>{this.setState({executeInProgress:!0})};getResolvedSubtree=()=>{const{specSelectors:s,path:o,method:i,specPath:a}=this.props;return a?s.specResolvedSubtree(a.toJS()):s.specResolvedSubtree([\"paths\",o,i])};requestResolvedSubtree=()=>{const{specActions:s,path:o,method:i,specPath:a}=this.props;return a?s.requestResolvedSubtree(a.toJS()):s.requestResolvedSubtree([\"paths\",o,i])};render(){let{op:s,tag:o,path:i,method:a,security:u,isAuthorized:_,operationId:w,showSummary:x,isShown:C,jumpToKey:j,allowTryItOut:L,response:B,request:$,displayOperationId:U,displayRequestDuration:V,isDeepLinkingEnabled:z,specPath:Y,specSelectors:Z,specActions:ee,getComponent:ie,getConfigs:ae,layoutSelectors:ce,layoutActions:le,authActions:pe,authSelectors:de,oas3Actions:fe,oas3Selectors:ye,fn:be}=this.props;const _e=ie(\"operation\"),Se=this.getResolvedSubtree()||(0,ze.Map)(),we=(0,ze.fromJS)({op:Se,tag:o,path:i,summary:s.getIn([\"operation\",\"summary\"])||\"\",deprecated:Se.get(\"deprecated\")||s.getIn([\"operation\",\"deprecated\"])||!1,method:a,security:u,isAuthorized:_,operationId:w,originalOperationId:Se.getIn([\"operation\",\"__originalOperationId\"]),showSummary:x,isShown:C,jumpToKey:j,allowTryItOut:L,request:$,displayOperationId:U,displayRequestDuration:V,isDeepLinkingEnabled:z,executeInProgress:this.state.executeInProgress,tryItOutEnabled:this.state.tryItOutEnabled});return Re.createElement(_e,{operation:we,response:B,request:$,isShown:C,toggleShown:this.toggleShown,onTryoutClick:this.onTryoutClick,onResetClick:this.onResetClick,onCancelClick:this.onCancelClick,onExecute:this.onExecute,specPath:Y,specActions:ee,specSelectors:Z,oas3Actions:fe,oas3Selectors:ye,layoutActions:le,layoutSelectors:ce,authActions:pe,authSelectors:de,getComponent:ie,getConfigs:ae,fn:be})}}var HO=__webpack_require__(13222),KO=__webpack_require__.n(HO);class OperationSummary extends Re.PureComponent{static defaultProps={operationProps:null,specPath:(0,ze.List)(),summary:\"\"};render(){let{isShown:s,toggleShown:o,getComponent:i,authActions:a,authSelectors:u,operationProps:_,specPath:w}=this.props,{summary:x,isAuthorized:C,method:j,op:L,showSummary:B,path:$,operationId:U,originalOperationId:V,displayOperationId:z}=_.toJS(),{summary:Y}=L,Z=_.get(\"security\");const ee=i(\"authorizeOperationBtn\",!0),ie=i(\"OperationSummaryMethod\"),ae=i(\"OperationSummaryPath\"),ce=i(\"JumpToPath\",!0),le=i(\"CopyToClipboardBtn\",!0),pe=i(\"ArrowUpIcon\"),de=i(\"ArrowDownIcon\"),fe=Z&&!!Z.count(),ye=fe&&1===Z.size&&Z.first().isEmpty(),be=!fe||ye;return Re.createElement(\"div\",{className:`opblock-summary opblock-summary-${j}`},Re.createElement(\"button\",{\"aria-expanded\":s,className:\"opblock-summary-control\",onClick:o},Re.createElement(ie,{method:j}),Re.createElement(\"div\",{className:\"opblock-summary-path-description-wrapper\"},Re.createElement(ae,{getComponent:i,operationProps:_,specPath:w}),B?Re.createElement(\"div\",{className:\"opblock-summary-description\"},KO()(Y||x)):null),z&&(V||U)?Re.createElement(\"span\",{className:\"opblock-summary-operation-id\"},V||U):null),Re.createElement(le,{textToCopy:`${w.get(1)}`}),be?null:Re.createElement(ee,{isAuthorized:C,onClick:()=>{const s=u.definitionsForRequirements(Z);a.showDefinitions(s)}}),Re.createElement(ce,{path:w}),Re.createElement(\"button\",{\"aria-label\":`${j} ${$.replace(/\\//g,\"​/\")}`,className:\"opblock-control-arrow\",\"aria-expanded\":s,tabIndex:\"-1\",onClick:o},s?Re.createElement(pe,{className:\"arrow\"}):Re.createElement(de,{className:\"arrow\"})))}}class OperationSummaryMethod extends Re.PureComponent{static defaultProps={operationProps:null};render(){let{method:s}=this.props;return Re.createElement(\"span\",{className:\"opblock-summary-method\"},s.toUpperCase())}}class OperationSummaryPath extends Re.PureComponent{render(){let{getComponent:s,operationProps:o}=this.props,{deprecated:i,isShown:a,path:u,tag:_,operationId:w,isDeepLinkingEnabled:x}=o.toJS();const C=u.split(/(?=\\/)/g);for(let s=1;s<C.length;s+=2)C.splice(s,0,Re.createElement(\"wbr\",{key:s}));const j=s(\"DeepLink\");return Re.createElement(\"span\",{className:i?\"opblock-summary-path__deprecated\":\"opblock-summary-path\",\"data-path\":u},Re.createElement(j,{enabled:x,isShown:a,path:createDeepLinkPath(`${_}/${w}`),text:C}))}}const operation_extensions=({extensions:s,getComponent:o})=>{let i=o(\"OperationExtRow\");return Re.createElement(\"div\",{className:\"opblock-section\"},Re.createElement(\"div\",{className:\"opblock-section-header\"},Re.createElement(\"h4\",null,\"Extensions\")),Re.createElement(\"div\",{className:\"table-container\"},Re.createElement(\"table\",null,Re.createElement(\"thead\",null,Re.createElement(\"tr\",null,Re.createElement(\"td\",{className:\"col_header\"},\"Field\"),Re.createElement(\"td\",{className:\"col_header\"},\"Value\"))),Re.createElement(\"tbody\",null,s.entrySeq().map((([s,o])=>Re.createElement(i,{key:`${s}-${o}`,xKey:s,xVal:o})))))))},operation_extension_row=({xKey:s,xVal:o})=>{const i=o?o.toJS?o.toJS():o:null;return Re.createElement(\"tr\",null,Re.createElement(\"td\",null,s),Re.createElement(\"td\",null,JSON.stringify(i)))};function createHtmlReadyId(s,o=\"_\"){return s.replace(/[^\\w-]/g,o)}class responses_Responses extends Re.Component{static defaultProps={tryItOutResponse:null,produces:(0,ze.fromJS)([\"application/json\"]),displayRequestDuration:!1};onChangeProducesWrapper=s=>this.props.specActions.changeProducesValue([this.props.path,this.props.method],s);onResponseContentTypeChange=({controlsAcceptHeader:s,value:o})=>{const{oas3Actions:i,path:a,method:u}=this.props;s&&i.setResponseContentType({value:o,path:a,method:u})};render(){let{responses:s,tryItOutResponse:o,getComponent:i,getConfigs:a,specSelectors:u,fn:_,producesValue:w,displayRequestDuration:x,specPath:C,path:j,method:L,oas3Selectors:B,oas3Actions:$}=this.props,U=function defaultStatusCode(s){let o=s.keySeq();return o.contains(jt)?jt:o.filter((s=>\"2\"===(s+\"\")[0])).sort().first()}(s);const V=i(\"contentType\"),z=i(\"liveResponse\"),Y=i(\"response\");let Z=this.props.produces&&this.props.produces.size?this.props.produces:responses_Responses.defaultProps.produces;const ee=u.isOAS3()?function getAcceptControllingResponse(s){if(!We().OrderedMap.isOrderedMap(s))return null;if(!s.size)return null;const o=s.find(((s,o)=>o.startsWith(\"2\")&&Object.keys(s.get(\"content\")||{}).length>0)),i=s.get(\"default\")||We().OrderedMap(),a=(i.get(\"content\")||We().OrderedMap()).keySeq().toJS().length?i:null;return o||a}(s):null,ie=s.filter(((s,o)=>!isExtension(o))),ae=createHtmlReadyId(`${L}${j}_responses`),ce=`${ae}_select`;return ie&&ie.size?Re.createElement(\"div\",{className:\"responses-wrapper\"},Re.createElement(\"div\",{className:\"opblock-section-header\"},Re.createElement(\"h4\",null,\"Responses\"),u.isOAS3()?null:Re.createElement(\"label\",{htmlFor:ce},Re.createElement(\"span\",null,\"Response content type\"),Re.createElement(V,{value:w,ariaControls:ae,ariaLabel:\"Response content type\",className:\"execute-content-type\",contentTypes:Z,controlId:ce,onChange:this.onChangeProducesWrapper}))),Re.createElement(\"div\",{className:\"responses-inner\"},o?Re.createElement(\"div\",null,Re.createElement(z,{response:o,getComponent:i,getConfigs:a,specSelectors:u,path:this.props.path,method:this.props.method,displayRequestDuration:x}),Re.createElement(\"h4\",null,\"Responses\")):null,Re.createElement(\"table\",{\"aria-live\":\"polite\",className:\"responses-table\",id:ae,role:\"region\"},Re.createElement(\"thead\",null,Re.createElement(\"tr\",{className:\"responses-header\"},Re.createElement(\"td\",{className:\"col_header response-col_status\"},\"Code\"),Re.createElement(\"td\",{className:\"col_header response-col_description\"},\"Description\"),u.isOAS3()?Re.createElement(\"td\",{className:\"col col_header response-col_links\"},\"Links\"):null)),Re.createElement(\"tbody\",null,ie.entrySeq().map((([s,x])=>{let V=o&&o.get(\"status\")==s?\"response_current\":\"\";return Re.createElement(Y,{key:s,path:j,method:L,specPath:C.push(s),isDefault:U===s,fn:_,className:V,code:s,response:x,specSelectors:u,controlsAcceptHeader:x===ee,onContentTypeChange:this.onResponseContentTypeChange,contentType:w,getConfigs:a,activeExamplesKey:B.activeExamplesMember(j,L,\"responses\",s),oas3Actions:$,getComponent:i})})).toArray())))):null}}function getKnownSyntaxHighlighterLanguage(s){const o=function canJsonParse(s){try{return!!JSON.parse(s)}catch(s){return null}}(s);return o?\"json\":null}class response_Response extends Re.Component{constructor(s,o){super(s,o),this.state={responseContentType:\"\"}}static defaultProps={response:(0,ze.fromJS)({}),onContentTypeChange:()=>{}};_onContentTypeChange=s=>{const{onContentTypeChange:o,controlsAcceptHeader:i}=this.props;this.setState({responseContentType:s}),o({value:s,controlsAcceptHeader:i})};getTargetExamplesKey=()=>{const{response:s,contentType:o,activeExamplesKey:i}=this.props,a=this.state.responseContentType||o,u=s.getIn([\"content\",a],(0,ze.Map)({})).get(\"examples\",null).keySeq().first();return i||u};render(){let{path:s,method:o,code:i,response:a,className:u,specPath:_,fn:w,getComponent:x,getConfigs:C,specSelectors:j,contentType:L,controlsAcceptHeader:B,oas3Actions:$}=this.props,{inferSchema:U,getSampleSchema:V}=w,z=j.isOAS3();const{showExtensions:Y}=C();let Z=Y?getExtensions(a):null,ee=a.get(\"headers\"),ie=a.get(\"links\");const ae=x(\"ResponseExtension\"),ce=x(\"headers\"),le=x(\"HighlightCode\",!0),pe=x(\"modelExample\"),de=x(\"Markdown\",!0),fe=x(\"operationLink\"),ye=x(\"contentType\"),be=x(\"ExamplesSelect\"),_e=x(\"Example\");var Se,we;const xe=this.state.responseContentType||L,Pe=a.getIn([\"content\",xe],(0,ze.Map)({})),Te=Pe.get(\"examples\",null);if(z){const s=Pe.get(\"schema\");Se=s?U(s.toJS()):null,we=s?_.push(\"content\",this.state.responseContentType,\"schema\"):_}else Se=a.get(\"schema\"),we=a.has(\"schema\")?_.push(\"schema\"):_;let $e,qe,We=!1,He={includeReadOnly:!0};if(z)if(qe=Pe.get(\"schema\")?.toJS(),ze.Map.isMap(Te)&&!Te.isEmpty()){const s=this.getTargetExamplesKey(),getMediaTypeExample=s=>ze.Map.isMap(s)?s.get(\"value\"):void 0;$e=getMediaTypeExample(Te.get(s,(0,ze.Map)({}))),void 0===$e&&($e=getMediaTypeExample(Te.values().next().value)),We=!0}else void 0!==Pe.get(\"example\")&&($e=Pe.get(\"example\"),We=!0);else{qe=Se,He={...He,includeWriteOnly:!0};const s=a.getIn([\"examples\",xe]);s&&($e=s,We=!0)}const Ye=((s,o)=>{if(null==s)return null;const i=getKnownSyntaxHighlighterLanguage(s)?\"json\":null;return Re.createElement(\"div\",null,Re.createElement(o,{className:\"example\",language:i},stringify(s)))})(V(qe,xe,He,We?$e:void 0),le);return Re.createElement(\"tr\",{className:\"response \"+(u||\"\"),\"data-code\":i},Re.createElement(\"td\",{className:\"response-col_status\"},i),Re.createElement(\"td\",{className:\"response-col_description\"},Re.createElement(\"div\",{className:\"response-col_description__inner\"},Re.createElement(de,{source:a.get(\"description\")})),Y&&Z.size?Z.entrySeq().map((([s,o])=>Re.createElement(ae,{key:`${s}-${o}`,xKey:s,xVal:o}))):null,z&&a.get(\"content\")?Re.createElement(\"section\",{className:\"response-controls\"},Re.createElement(\"div\",{className:Jn()(\"response-control-media-type\",{\"response-control-media-type--accept-controller\":B})},Re.createElement(\"small\",{className:\"response-control-media-type__title\"},\"Media type\"),Re.createElement(ye,{value:this.state.responseContentType,contentTypes:a.get(\"content\")?a.get(\"content\").keySeq():(0,ze.Seq)(),onChange:this._onContentTypeChange,ariaLabel:\"Media Type\"}),B?Re.createElement(\"small\",{className:\"response-control-media-type__accept-message\"},\"Controls \",Re.createElement(\"code\",null,\"Accept\"),\" header.\"):null),ze.Map.isMap(Te)&&!Te.isEmpty()?Re.createElement(\"div\",{className:\"response-control-examples\"},Re.createElement(\"small\",{className:\"response-control-examples__title\"},\"Examples\"),Re.createElement(be,{examples:Te,currentExampleKey:this.getTargetExamplesKey(),onSelect:a=>$.setActiveExamplesMember({name:a,pathMethod:[s,o],contextType:\"responses\",contextName:i}),showLabels:!1})):null):null,Ye||Se?Re.createElement(pe,{specPath:we,getComponent:x,getConfigs:C,specSelectors:j,schema:fromJSOrdered(Se),example:Ye,includeReadOnly:!0}):null,z&&Te?Re.createElement(_e,{example:Te.get(this.getTargetExamplesKey(),(0,ze.Map)({})),getComponent:x,getConfigs:C,omitValue:!0}):null,ee?Re.createElement(ce,{headers:ee,getComponent:x}):null),z?Re.createElement(\"td\",{className:\"response-col_links\"},ie?ie.toSeq().entrySeq().map((([s,o])=>Re.createElement(fe,{key:s,name:s,link:o,getComponent:x}))):Re.createElement(\"i\",null,\"No links\")):null)}}const response_extension=({xKey:s,xVal:o})=>Re.createElement(\"div\",{className:\"response__extension\"},s,\": \",String(o));var GO=__webpack_require__(26657),YO=__webpack_require__.n(GO),XO=__webpack_require__(80218),QO=__webpack_require__.n(XO);class ResponseBody extends Re.PureComponent{state={parsedContent:null};updateParsedContent=s=>{const{content:o}=this.props;if(s!==o)if(o&&o instanceof Blob){var i=new FileReader;i.onload=()=>{this.setState({parsedContent:i.result})},i.readAsText(o)}else this.setState({parsedContent:o.toString()})};componentDidMount(){this.updateParsedContent(null)}componentDidUpdate(s){this.updateParsedContent(s.content)}render(){let{content:s,contentType:o,url:i,headers:a={},getComponent:u}=this.props;const{parsedContent:_}=this.state,w=u(\"HighlightCode\",!0),x=\"response_\"+(new Date).getTime();let C,j;if(i=i||\"\",(/^application\\/octet-stream/i.test(o)||a[\"Content-Disposition\"]&&/attachment/i.test(a[\"Content-Disposition\"])||a[\"content-disposition\"]&&/attachment/i.test(a[\"content-disposition\"])||a[\"Content-Description\"]&&/File Transfer/i.test(a[\"Content-Description\"])||a[\"content-description\"]&&/File Transfer/i.test(a[\"content-description\"]))&&(s.size>0||s.length>0))if(\"Blob\"in window){let u=o||\"text/html\",_=s instanceof Blob?s:new Blob([s],{type:u}),w=window.URL.createObjectURL(_),x=[u,i.substr(i.lastIndexOf(\"/\")+1),w].join(\":\"),C=a[\"content-disposition\"]||a[\"Content-Disposition\"];if(void 0!==C){let s=function extractFileNameFromContentDispositionHeader(s){let o;if([/filename\\*=[^']+'\\w*'\"([^\"]+)\";?/i,/filename\\*=[^']+'\\w*'([^;]+);?/i,/filename=\"([^;]*);?\"/i,/filename=([^;]*);?/i].some((i=>(o=i.exec(s),null!==o))),null!==o&&o.length>1)try{return decodeURIComponent(o[1])}catch(s){console.error(s)}return null}(C);null!==s&&(x=s)}j=lt.navigator&&lt.navigator.msSaveOrOpenBlob?Re.createElement(\"div\",null,Re.createElement(\"a\",{href:w,onClick:()=>lt.navigator.msSaveOrOpenBlob(_,x)},\"Download file\")):Re.createElement(\"div\",null,Re.createElement(\"a\",{href:w,download:x},\"Download file\"))}else j=Re.createElement(\"pre\",{className:\"microlight\"},\"Download headers detected but your browser does not support downloading binary via XHR (Blob).\");else if(/json/i.test(o)){let o=null;getKnownSyntaxHighlighterLanguage(s)&&(o=\"json\");try{C=JSON.stringify(JSON.parse(s),null,\"  \")}catch(o){C=\"can't parse JSON.  Raw result:\\n\\n\"+s}j=Re.createElement(w,{language:o,downloadable:!0,fileName:`${x}.json`,canCopy:!0},C)}else/xml/i.test(o)?(C=YO()(s,{textNodesOnSameLine:!0,indentor:\"  \"}),j=Re.createElement(w,{downloadable:!0,fileName:`${x}.xml`,canCopy:!0},C)):j=\"text/html\"===QO()(o)||/text\\/plain/.test(o)?Re.createElement(w,{downloadable:!0,fileName:`${x}.html`,canCopy:!0},s):\"text/csv\"===QO()(o)||/text\\/csv/.test(o)?Re.createElement(w,{downloadable:!0,fileName:`${x}.csv`,canCopy:!0},s):/^image\\//i.test(o)?o.includes(\"svg\")?Re.createElement(\"div\",null,\" \",s,\" \"):Re.createElement(\"img\",{src:window.URL.createObjectURL(s)}):/^audio\\//i.test(o)?Re.createElement(\"pre\",{className:\"microlight\"},Re.createElement(\"audio\",{controls:!0,key:i},Re.createElement(\"source\",{src:i,type:o}))):\"string\"==typeof s?Re.createElement(w,{downloadable:!0,fileName:`${x}.txt`,canCopy:!0},s):s.size>0?_?Re.createElement(\"div\",null,Re.createElement(\"p\",{className:\"i\"},\"Unrecognized response type; displaying content as text.\"),Re.createElement(w,{downloadable:!0,fileName:`${x}.txt`,canCopy:!0},_)):Re.createElement(\"p\",{className:\"i\"},\"Unrecognized response type; unable to display.\"):null;return j?Re.createElement(\"div\",null,Re.createElement(\"h5\",null,\"Response body\"),j):null}}class Parameters extends Re.Component{constructor(s){super(s),this.state={callbackVisible:!1,parametersVisible:!0}}static defaultProps={onTryoutClick:Function.prototype,onCancelClick:Function.prototype,tryItOutEnabled:!1,allowTryItOut:!0,onChangeKey:[],specPath:[]};onChange=(s,o,i)=>{let{specActions:{changeParamByIdentity:a},onChangeKey:u}=this.props;a(u,s,o,i)};onChangeConsumesWrapper=s=>{let{specActions:{changeConsumesValue:o},onChangeKey:i}=this.props;o(i,s)};toggleTab=s=>\"parameters\"===s?this.setState({parametersVisible:!0,callbackVisible:!1}):\"callbacks\"===s?this.setState({callbackVisible:!0,parametersVisible:!1}):void 0;onChangeMediaType=({value:s,pathMethod:o})=>{let{specActions:i,oas3Selectors:a,oas3Actions:u}=this.props;const _=a.hasUserEditedBody(...o),w=a.shouldRetainRequestBodyValue(...o);u.setRequestContentType({value:s,pathMethod:o}),u.initRequestBodyValidateError({pathMethod:o}),_||(w||u.setRequestBodyValue({value:void 0,pathMethod:o}),i.clearResponse(...o),i.clearRequest(...o),i.clearValidateParams(o))};render(){let{onTryoutClick:s,onResetClick:o,parameters:i,allowTryItOut:a,tryItOutEnabled:u,specPath:_,fn:w,getComponent:x,getConfigs:C,specSelectors:j,specActions:L,pathMethod:B,oas3Actions:$,oas3Selectors:U,operation:V}=this.props;const z=x(\"parameterRow\"),Y=x(\"TryItOutButton\"),Z=x(\"contentType\"),ee=x(\"Callbacks\",!0),ie=x(\"RequestBody\",!0),ae=u&&a,ce=j.isOAS3(),le=`${createHtmlReadyId(`${B[1]}${B[0]}_requests`)}_select`,pe=V.get(\"requestBody\"),de=Object.values(i.reduce(((s,o)=>{if(ze.Map.isMap(o)){const i=o.get(\"in\");s[i]??=[],s[i].push(o)}return s}),{})).reduce(((s,o)=>s.concat(o)),[]);return Re.createElement(\"div\",{className:\"opblock-section\"},Re.createElement(\"div\",{className:\"opblock-section-header\"},ce?Re.createElement(\"div\",{className:\"tab-header\"},Re.createElement(\"div\",{onClick:()=>this.toggleTab(\"parameters\"),className:`tab-item ${this.state.parametersVisible&&\"active\"}`},Re.createElement(\"h4\",{className:\"opblock-title\"},Re.createElement(\"span\",null,\"Parameters\"))),V.get(\"callbacks\")?Re.createElement(\"div\",{onClick:()=>this.toggleTab(\"callbacks\"),className:`tab-item ${this.state.callbackVisible&&\"active\"}`},Re.createElement(\"h4\",{className:\"opblock-title\"},Re.createElement(\"span\",null,\"Callbacks\"))):null):Re.createElement(\"div\",{className:\"tab-header\"},Re.createElement(\"h4\",{className:\"opblock-title\"},\"Parameters\")),a?Re.createElement(Y,{isOAS3:j.isOAS3(),hasUserEditedBody:U.hasUserEditedBody(...B),enabled:u,onCancelClick:this.props.onCancelClick,onTryoutClick:s,onResetClick:()=>o(B)}):null),this.state.parametersVisible?Re.createElement(\"div\",{className:\"parameters-container\"},de.length?Re.createElement(\"div\",{className:\"table-container\"},Re.createElement(\"table\",{className:\"parameters\"},Re.createElement(\"thead\",null,Re.createElement(\"tr\",null,Re.createElement(\"th\",{className:\"col_header parameters-col_name\"},\"Name\"),Re.createElement(\"th\",{className:\"col_header parameters-col_description\"},\"Description\"))),Re.createElement(\"tbody\",null,de.map(((s,o)=>Re.createElement(z,{fn:w,specPath:_.push(o.toString()),getComponent:x,getConfigs:C,rawParam:s,param:j.parameterWithMetaByIdentity(B,s),key:`${s.get(\"in\")}.${s.get(\"name\")}`,onChange:this.onChange,onChangeConsumes:this.onChangeConsumesWrapper,specSelectors:j,specActions:L,oas3Actions:$,oas3Selectors:U,pathMethod:B,isExecute:ae})))))):Re.createElement(\"div\",{className:\"opblock-description-wrapper\"},Re.createElement(\"p\",null,\"No parameters\"))):null,this.state.callbackVisible?Re.createElement(\"div\",{className:\"callbacks-container opblock-description-wrapper\"},Re.createElement(ee,{callbacks:(0,ze.Map)(V.get(\"callbacks\")),specPath:_.slice(0,-1).push(\"callbacks\")})):null,ce&&pe&&this.state.parametersVisible&&Re.createElement(\"div\",{className:\"opblock-section opblock-section-request-body\"},Re.createElement(\"div\",{className:\"opblock-section-header\"},Re.createElement(\"h4\",{className:`opblock-title parameter__name ${pe.get(\"required\")&&\"required\"}`},\"Request body\"),Re.createElement(\"label\",{id:le},Re.createElement(Z,{value:U.requestContentType(...B),contentTypes:pe.get(\"content\",(0,ze.List)()).keySeq(),onChange:s=>{this.onChangeMediaType({value:s,pathMethod:B})},className:\"body-param-content-type\",ariaLabel:\"Request content type\",controlId:le}))),Re.createElement(\"div\",{className:\"opblock-description-wrapper\"},Re.createElement(ie,{setRetainRequestBodyValueFlag:s=>$.setRetainRequestBodyValueFlag({value:s,pathMethod:B}),userHasEditedBody:U.hasUserEditedBody(...B),specPath:_.slice(0,-1).push(\"requestBody\"),requestBody:pe,requestBodyValue:U.requestBodyValue(...B),requestBodyInclusionSetting:U.requestBodyInclusionSetting(...B),requestBodyErrors:U.requestBodyErrors(...B),isExecute:ae,getConfigs:C,activeExamplesKey:U.activeExamplesMember(...B,\"requestBody\",\"requestBody\"),updateActiveExamplesKey:s=>{this.props.oas3Actions.setActiveExamplesMember({name:s,pathMethod:this.props.pathMethod,contextType:\"requestBody\",contextName:\"requestBody\"})},onChange:(s,o)=>{if(o){const i=U.requestBodyValue(...B),a=ze.Map.isMap(i)?i:(0,ze.Map)();return $.setRequestBodyValue({pathMethod:B,value:a.setIn(o,s)})}$.setRequestBodyValue({value:s,pathMethod:B})},onChangeIncludeEmpty:(s,o)=>{$.setRequestBodyInclusion({pathMethod:B,value:o,name:s})},contentType:U.requestContentType(...B)}))))}}const parameter_extension=({xKey:s,xVal:o})=>Re.createElement(\"div\",{className:\"parameter__extension\"},s,\": \",String(o)),ZO={onChange:()=>{},isIncludedOptions:{}};class ParameterIncludeEmpty extends Re.Component{static defaultProps=ZO;componentDidMount(){const{isIncludedOptions:s,onChange:o}=this.props,{shouldDispatchInit:i,defaultValue:a}=s;i&&o(a)}onCheckboxChange=s=>{const{onChange:o}=this.props;o(s.target.checked)};render(){let{isIncluded:s,isDisabled:o}=this.props;return Re.createElement(\"div\",null,Re.createElement(\"label\",{htmlFor:\"include_empty_value\",className:Jn()(\"parameter__empty_value_toggle\",{disabled:o})},Re.createElement(\"input\",{id:\"include_empty_value\",type:\"checkbox\",disabled:o,checked:!o&&s,onChange:this.onCheckboxChange}),\"Send empty value\"))}}class ParameterRow extends Re.Component{constructor(s,o){super(s,o),this.setDefaultValue()}UNSAFE_componentWillReceiveProps(s){let o,{specSelectors:i,pathMethod:a,rawParam:u}=s,_=i.isOAS3(),w=i.parameterWithMetaByIdentity(a,u)||new ze.Map;if(w=w.isEmpty()?u:w,_){let{schema:s}=getParameterSchema(w,{isOAS3:_});o=s?s.get(\"enum\"):void 0}else o=w?w.get(\"enum\"):void 0;let x,C=w?w.get(\"value\"):void 0;void 0!==C?x=C:u.get(\"required\")&&o&&o.size&&(x=o.first()),void 0!==x&&x!==C&&this.onChangeWrapper(function numberToString(s){return\"number\"==typeof s?s.toString():s}(x)),this.setDefaultValue()}onChangeWrapper=(s,o=!1)=>{let i,{onChange:a,rawParam:u}=this.props;return i=\"\"===s||s&&0===s.size?null:s,a(u,i,o)};_onExampleSelect=s=>{this.props.oas3Actions.setActiveExamplesMember({name:s,pathMethod:this.props.pathMethod,contextType:\"parameters\",contextName:this.getParamKey()})};onChangeIncludeEmpty=s=>{let{specActions:o,param:i,pathMethod:a}=this.props;const u=i.get(\"name\"),_=i.get(\"in\");return o.updateEmptyParamInclusion(a,u,_,s)};setDefaultValue=()=>{let{specSelectors:s,pathMethod:o,rawParam:i,oas3Selectors:a,fn:u}=this.props;const _=s.parameterWithMetaByIdentity(o,i)||(0,ze.Map)();let{schema:w}=getParameterSchema(_,{isOAS3:s.isOAS3()});const x=_.get(\"content\",(0,ze.Map)()).keySeq().first(),C=w?u.getSampleSchema(w.toJS(),x,{includeWriteOnly:!0}):null;if(_&&void 0===_.get(\"value\")&&\"body\"!==_.get(\"in\")){let i;if(s.isSwagger2())i=void 0!==_.get(\"x-example\")?_.get(\"x-example\"):void 0!==_.getIn([\"schema\",\"example\"])?_.getIn([\"schema\",\"example\"]):w&&w.getIn([\"default\"]);else if(s.isOAS3()){w=this.composeJsonSchema(w);const s=a.activeExamplesMember(...o,\"parameters\",this.getParamKey());i=void 0!==_.getIn([\"examples\",s,\"value\"])?_.getIn([\"examples\",s,\"value\"]):void 0!==_.getIn([\"content\",x,\"example\"])?_.getIn([\"content\",x,\"example\"]):void 0!==_.get(\"example\")?_.get(\"example\"):void 0!==(w&&w.get(\"example\"))?w&&w.get(\"example\"):void 0!==(w&&w.get(\"default\"))?w&&w.get(\"default\"):_.get(\"default\")}void 0===i||ze.List.isList(i)||(i=stringify(i));const j=u.getSchemaObjectType(w),L=u.getSchemaObjectType(w?.get(\"items\"));void 0!==i?this.onChangeWrapper(i):\"object\"===j&&C&&!_.get(\"examples\")?this.onChangeWrapper(ze.List.isList(C)?C:stringify(C)):\"array\"===j&&\"object\"===L&&C&&!_.get(\"examples\")&&this.onChangeWrapper(ze.List.isList(C)?C:(0,ze.List)(JSON.parse(C)))}};getParamKey(){const{param:s}=this.props;return s?`${s.get(\"name\")}-${s.get(\"in\")}`:null}composeJsonSchema(s){const{fn:o}=this.props,i=s.get(\"oneOf\")?.get(0)?.toJS(),a=s.get(\"anyOf\")?.get(0)?.toJS();return(0,ze.fromJS)(o.mergeJsonSchema(s.toJS(),i??a??{}))}render(){let{param:s,rawParam:o,getComponent:i,getConfigs:a,isExecute:u,fn:_,onChangeConsumes:w,specSelectors:x,pathMethod:C,specPath:j,oas3Selectors:L}=this.props,B=x.isOAS3();const{showExtensions:$,showCommonExtensions:U}=a();if(s||(s=o),!o)return null;const V=i(\"JsonSchemaForm\"),z=i(\"ParamBody\");let Y=s.get(\"in\"),Z=\"body\"!==Y?null:Re.createElement(z,{getComponent:i,getConfigs:a,fn:_,param:s,consumes:x.consumesOptionsFor(C),consumesValue:x.contentTypeValues(C).get(\"requestContentType\"),onChange:this.onChangeWrapper,onChangeConsumes:w,isExecute:u,specSelectors:x,pathMethod:C});const ee=i(\"modelExample\"),ie=i(\"Markdown\",!0),ae=i(\"ParameterExt\"),ce=i(\"ParameterIncludeEmpty\"),le=i(\"ExamplesSelectValueRetainer\"),pe=i(\"Example\");let{schema:de}=getParameterSchema(s,{isOAS3:B}),fe=x.parameterWithMetaByIdentity(C,o)||(0,ze.Map)();const ye=fe.get(\"content\",(0,ze.Map)()).keySeq().first();B&&(de=this.composeJsonSchema(de));let be=de?de.get(\"format\"):null,_e=\"formData\"===Y,Se=\"FormData\"in lt,we=s.get(\"required\");const xe=_.getSchemaObjectType(de),Pe=_.getSchemaObjectType(de?.get(\"items\")),Te=_.getSchemaObjectTypeLabel(de),$e=!Z&&\"object\"===xe,qe=!Z&&\"object\"===Pe;let We,He,Ye,Xe,Qe=fe?fe.get(\"value\"):\"\",et=U?getCommonExtensions(de):null,tt=$?getExtensions(s):null,rt=!1;void 0!==s&&de&&(We=de.get(\"items\")),void 0!==We?(He=We.get(\"enum\"),Ye=We.get(\"default\")):de&&(He=de.get(\"enum\")),He&&He.size&&He.size>0&&(rt=!0),void 0!==s&&(de&&(Ye=de.get(\"default\")),void 0===Ye&&(Ye=s.get(\"default\")),Xe=s.get(\"example\"),void 0===Xe&&(Xe=s.get(\"x-example\")));const nt=Z?null:Re.createElement(V,{fn:_,getComponent:i,value:Qe,required:we,disabled:!u,description:s.get(\"name\"),onChange:this.onChangeWrapper,errors:fe.get(\"errors\"),schema:de});return Re.createElement(\"tr\",{\"data-param-name\":s.get(\"name\"),\"data-param-in\":s.get(\"in\")},Re.createElement(\"td\",{className:\"parameters-col_name\"},Re.createElement(\"div\",{className:we?\"parameter__name required\":\"parameter__name\"},s.get(\"name\"),we?Re.createElement(\"span\",null,\" *\"):null),Re.createElement(\"div\",{className:\"parameter__type\"},Te,be&&Re.createElement(\"span\",{className:\"prop-format\"},\"($\",be,\")\")),Re.createElement(\"div\",{className:\"parameter__deprecated\"},B&&s.get(\"deprecated\")?\"deprecated\":null),Re.createElement(\"div\",{className:\"parameter__in\"},\"(\",s.get(\"in\"),\")\")),Re.createElement(\"td\",{className:\"parameters-col_description\"},s.get(\"description\")?Re.createElement(ie,{source:s.get(\"description\")}):null,!Z&&u||!rt?null:Re.createElement(ie,{className:\"parameter__enum\",source:\"<i>Available values</i> : \"+He.map((function(s){return s})).toArray().map(String).join(\", \")}),!Z&&u||void 0===Ye?null:Re.createElement(ie,{className:\"parameter__default\",source:\"<i>Default value</i> : \"+Ye}),!Z&&u||void 0===Xe?null:Re.createElement(ie,{source:\"<i>Example</i> : \"+Xe}),_e&&!Se&&Re.createElement(\"div\",null,\"Error: your browser does not support FormData\"),B&&s.get(\"examples\")?Re.createElement(\"section\",{className:\"parameter-controls\"},Re.createElement(le,{examples:s.get(\"examples\"),onSelect:this._onExampleSelect,updateValue:this.onChangeWrapper,getComponent:i,defaultToFirstExample:!0,currentKey:L.activeExamplesMember(...C,\"parameters\",this.getParamKey()),currentUserInputValue:Qe})):null,$e||qe?Re.createElement(ee,{getComponent:i,specPath:ye?j.push(\"content\",ye,\"schema\"):j.push(\"schema\"),getConfigs:a,isExecute:u,specSelectors:x,schema:de,example:nt}):nt,Z&&de?Re.createElement(ee,{getComponent:i,specPath:j.push(\"schema\"),getConfigs:a,isExecute:u,specSelectors:x,schema:de,example:Z,includeWriteOnly:!0}):null,!Z&&u&&s.get(\"allowEmptyValue\")?Re.createElement(ce,{onChange:this.onChangeIncludeEmpty,isIncluded:x.parameterInclusionSettingFor(C,s.get(\"name\"),s.get(\"in\")),isDisabled:!isEmptyValue(Qe)}):null,B&&s.get(\"examples\")?Re.createElement(pe,{example:s.getIn([\"examples\",L.activeExamplesMember(...C,\"parameters\",this.getParamKey())]),getComponent:i,getConfigs:a}):null,U&&et.size?et.entrySeq().map((([s,o])=>Re.createElement(ae,{key:`${s}-${o}`,xKey:s,xVal:o}))):null,$&&tt.size?tt.entrySeq().map((([s,o])=>Re.createElement(ae,{key:`${s}-${o}`,xKey:s,xVal:o}))):null))}}class Execute extends Re.Component{handleValidateParameters=()=>{let{specSelectors:s,specActions:o,path:i,method:a}=this.props;return o.validateParams([i,a]),s.validateBeforeExecute([i,a])};handleValidateRequestBody=()=>{let{path:s,method:o,specSelectors:i,oas3Selectors:a,oas3Actions:u}=this.props,_={missingBodyValue:!1,missingRequiredKeys:[]};u.clearRequestBodyValidateError({path:s,method:o});let w=i.getOAS3RequiredRequestBodyContentType([s,o]),x=a.requestBodyValue(s,o),C=a.validateBeforeExecute([s,o]),j=a.requestContentType(s,o);if(!C)return _.missingBodyValue=!0,u.setRequestBodyValidateError({path:s,method:o,validationErrors:_}),!1;if(!w)return!0;let L=a.validateShallowRequired({oas3RequiredRequestBodyContentType:w,oas3RequestContentType:j,oas3RequestBodyValue:x});return!L||L.length<1||(L.forEach((s=>{_.missingRequiredKeys.push(s)})),u.setRequestBodyValidateError({path:s,method:o,validationErrors:_}),!1)};handleValidationResultPass=()=>{let{specActions:s,operation:o,path:i,method:a}=this.props;this.props.onExecute&&this.props.onExecute(),s.execute({operation:o,path:i,method:a})};handleValidationResultFail=()=>{let{specActions:s,path:o,method:i}=this.props;s.clearValidateParams([o,i]),setTimeout((()=>{s.validateParams([o,i])}),40)};handleValidationResult=s=>{s?this.handleValidationResultPass():this.handleValidationResultFail()};onClick=()=>{let s=this.handleValidateParameters(),o=this.handleValidateRequestBody(),i=s&&o;this.handleValidationResult(i)};onChangeProducesWrapper=s=>this.props.specActions.changeProducesValue([this.props.path,this.props.method],s);render(){const{disabled:s}=this.props;return Re.createElement(\"button\",{className:\"btn execute opblock-control__btn\",onClick:this.onClick,disabled:s},\"Execute\")}}class headers_Headers extends Re.Component{render(){let{headers:s,getComponent:o}=this.props;const i=o(\"Property\"),a=o(\"Markdown\",!0);return s&&s.size?Re.createElement(\"div\",{className:\"headers-wrapper\"},Re.createElement(\"h4\",{className:\"headers__title\"},\"Headers:\"),Re.createElement(\"table\",{className:\"headers\"},Re.createElement(\"thead\",null,Re.createElement(\"tr\",{className:\"header-row\"},Re.createElement(\"th\",{className:\"header-col\"},\"Name\"),Re.createElement(\"th\",{className:\"header-col\"},\"Description\"),Re.createElement(\"th\",{className:\"header-col\"},\"Type\"))),Re.createElement(\"tbody\",null,s.entrySeq().map((([s,o])=>{if(!We().Map.isMap(o))return null;const u=o.get(\"description\"),_=o.getIn([\"schema\"])?o.getIn([\"schema\",\"type\"]):o.getIn([\"type\"]),w=o.getIn([\"schema\",\"example\"]);return Re.createElement(\"tr\",{key:s},Re.createElement(\"td\",{className:\"header-col\"},s),Re.createElement(\"td\",{className:\"header-col\"},u?Re.createElement(a,{source:u}):null),Re.createElement(\"td\",{className:\"header-col\"},_,\" \",w?Re.createElement(i,{propKey:\"Example\",propVal:w,propClass:\"header-example\"}):null))})).toArray()))):null}}class Errors extends Re.Component{render(){let{editorActions:s,errSelectors:o,layoutSelectors:i,layoutActions:a,getComponent:u}=this.props;const _=u(\"Collapse\");if(s&&s.jumpToLine)var w=s.jumpToLine;let x=o.allErrors().filter((s=>\"thrown\"===s.get(\"type\")||\"error\"===s.get(\"level\")));if(!x||x.count()<1)return null;let C=i.isShown([\"errorPane\"],!0),j=x.sortBy((s=>s.get(\"line\")));return Re.createElement(\"pre\",{className:\"errors-wrapper\"},Re.createElement(\"hgroup\",{className:\"error\"},Re.createElement(\"h4\",{className:\"errors__title\"},\"Errors\"),Re.createElement(\"button\",{className:\"btn errors__clear-btn\",onClick:()=>a.show([\"errorPane\"],!C)},C?\"Hide\":\"Show\")),Re.createElement(_,{isOpened:C,animated:!0},Re.createElement(\"div\",{className:\"errors\"},j.map(((s,o)=>{let i=s.get(\"type\");return\"thrown\"===i||\"auth\"===i?Re.createElement(ThrownErrorItem,{key:o,error:s.get(\"error\")||s,jumpToLine:w}):\"spec\"===i?Re.createElement(SpecErrorItem,{key:o,error:s,jumpToLine:w}):void 0})))))}}const ThrownErrorItem=({error:s,jumpToLine:o})=>{if(!s)return null;let i=s.get(\"line\");return Re.createElement(\"div\",{className:\"error-wrapper\"},s?Re.createElement(\"div\",null,Re.createElement(\"h4\",null,s.get(\"source\")&&s.get(\"level\")?toTitleCase(s.get(\"source\"))+\" \"+s.get(\"level\"):\"\",s.get(\"path\")?Re.createElement(\"small\",null,\" at \",s.get(\"path\")):null),Re.createElement(\"span\",{className:\"message thrown\"},s.get(\"message\")),Re.createElement(\"div\",{className:\"error-line\"},i&&o?Re.createElement(\"a\",{onClick:o.bind(null,i)},\"Jump to line \",i):null)):null)},SpecErrorItem=({error:s,jumpToLine:o=null})=>{let i=null;return s.get(\"path\")?i=ze.List.isList(s.get(\"path\"))?Re.createElement(\"small\",null,\"at \",s.get(\"path\").join(\".\")):Re.createElement(\"small\",null,\"at \",s.get(\"path\")):s.get(\"line\")&&!o&&(i=Re.createElement(\"small\",null,\"on line \",s.get(\"line\"))),Re.createElement(\"div\",{className:\"error-wrapper\"},s?Re.createElement(\"div\",null,Re.createElement(\"h4\",null,toTitleCase(s.get(\"source\"))+\" \"+s.get(\"level\"),\" \",i),Re.createElement(\"span\",{className:\"message\"},s.get(\"message\")),Re.createElement(\"div\",{className:\"error-line\"},o?Re.createElement(\"a\",{onClick:o.bind(null,s.get(\"line\"))},\"Jump to line \",s.get(\"line\")):null)):null)};function toTitleCase(s){return(s||\"\").split(\" \").map((s=>s[0].toUpperCase()+s.slice(1))).join(\" \")}const content_type_noop=()=>{};class ContentType extends Re.Component{static defaultProps={onChange:content_type_noop,value:null,contentTypes:(0,ze.fromJS)([\"application/json\"])};componentDidMount(){const{contentTypes:s,onChange:o}=this.props;s&&s.size&&o(s.first())}componentDidUpdate(){const{contentTypes:s,value:o,onChange:i}=this.props;s&&s.size&&(s.includes(o)||i(s.first()))}onChangeWrapper=s=>this.props.onChange(s.target.value);render(){let{ariaControls:s,ariaLabel:o,className:i,contentTypes:a,controlId:u,value:_}=this.props;return a&&a.size?Re.createElement(\"div\",{className:\"content-type-wrapper \"+(i||\"\")},Re.createElement(\"select\",{\"aria-controls\":s,\"aria-label\":o,className:\"content-type\",id:u,onChange:this.onChangeWrapper,value:_||\"\"},a.map((s=>Re.createElement(\"option\",{key:s,value:s},s))).toArray())):null}}function xclass(...s){return s.filter((s=>!!s)).join(\" \").trim()}class Container extends Re.Component{render(){let{fullscreen:s,full:o,...i}=this.props;if(s)return Re.createElement(\"section\",i);let a=\"swagger-container\"+(o?\"-full\":\"\");return Re.createElement(\"section\",Mn()({},i,{className:xclass(i.className,a)}))}}const eA={mobile:\"\",tablet:\"-tablet\",desktop:\"-desktop\",large:\"-hd\"};class Col extends Re.Component{render(){const{hide:s,keepContents:o,mobile:i,tablet:a,desktop:u,large:_,...w}=this.props;if(s&&!o)return Re.createElement(\"span\",null);let x=[];for(let s in eA){if(!Object.prototype.hasOwnProperty.call(eA,s))continue;let o=eA[s];if(s in this.props){let i=this.props[s];if(i<1){x.push(\"none\"+o);continue}x.push(\"block\"+o),x.push(\"col-\"+i+o)}}s&&x.push(\"hidden\");let C=xclass(w.className,...x);return Re.createElement(\"section\",Mn()({},w,{className:C}))}}class Row extends Re.Component{render(){return Re.createElement(\"div\",Mn()({},this.props,{className:xclass(this.props.className,\"wrapper\")}))}}class Button extends Re.Component{static defaultProps={className:\"\"};render(){return Re.createElement(\"button\",Mn()({},this.props,{className:xclass(this.props.className,\"button\")}))}}const TextArea=s=>Re.createElement(\"textarea\",s),Input=s=>Re.createElement(\"input\",s);class Select extends Re.Component{static defaultProps={multiple:!1,allowEmptyValue:!0};constructor(s,o){let i;super(s,o),i=s.value?s.value:s.multiple?[\"\"]:\"\",this.state={value:i}}onChange=s=>{let o,{onChange:i,multiple:a}=this.props,u=[].slice.call(s.target.options);o=a?u.filter((function(s){return s.selected})).map((function(s){return s.value})):s.target.value,this.setState({value:o}),i&&i(o)};UNSAFE_componentWillReceiveProps(s){s.value!==this.props.value&&this.setState({value:s.value})}render(){let{allowedValues:s,multiple:o,allowEmptyValue:i,disabled:a}=this.props,u=this.state.value?.toJS?.()||this.state.value;return Re.createElement(\"select\",{className:this.props.className,multiple:o,value:u,onChange:this.onChange,disabled:a},i?Re.createElement(\"option\",{value:\"\"},\"--\"):null,s.map((function(s,o){return Re.createElement(\"option\",{key:o,value:String(s)},String(s))})))}}class layout_utils_Link extends Re.Component{render(){return Re.createElement(\"a\",Mn()({},this.props,{rel:\"noopener noreferrer\",className:xclass(this.props.className,\"link\")}))}}const NoMargin=({children:s})=>Re.createElement(\"div\",{className:\"no-margin\"},\" \",s,\" \");class Collapse extends Re.Component{static defaultProps={isOpened:!1,animated:!1};renderNotAnimated(){return this.props.isOpened?Re.createElement(NoMargin,null,this.props.children):Re.createElement(\"noscript\",null)}render(){let{animated:s,isOpened:o,children:i}=this.props;return s?(i=o?i:null,Re.createElement(NoMargin,null,i)):this.renderNotAnimated()}}class Overview extends Re.Component{constructor(...s){super(...s),this.setTagShown=this._setTagShown.bind(this)}_setTagShown(s,o){this.props.layoutActions.show(s,o)}showOp(s,o){let{layoutActions:i}=this.props;i.show(s,o)}render(){let{specSelectors:s,layoutSelectors:o,layoutActions:i,getComponent:a}=this.props,u=s.taggedOperations();const _=a(\"Collapse\");return Re.createElement(\"div\",null,Re.createElement(\"h4\",{className:\"overview-title\"},\"Overview\"),u.map(((s,a)=>{let u=s.get(\"operations\"),w=[\"overview-tags\",a],x=o.isShown(w,!0);return Re.createElement(\"div\",{key:\"overview-\"+a},Re.createElement(\"h4\",{onClick:()=>i.show(w,!x),className:\"link overview-tag\"},\" \",x?\"-\":\"+\",a),Re.createElement(_,{isOpened:x,animated:!0},u.map((s=>{let{path:a,method:u,id:_}=s.toObject(),w=\"operations\",x=_,C=o.isShown([w,x]);return Re.createElement(OperationLink,{key:_,path:a,method:u,id:a+\"-\"+u,shown:C,showOpId:x,showOpIdPrefix:w,href:`#operation-${x}`,onClick:i.show})})).toArray()))})).toArray(),u.size<1&&Re.createElement(\"h3\",null,\" No operations defined in spec! \"))}}class OperationLink extends Re.Component{constructor(s){super(s),this.onClick=this._onClick.bind(this)}_onClick(){let{showOpId:s,showOpIdPrefix:o,onClick:i,shown:a}=this.props;i([o,s],!a)}render(){let{id:s,method:o,shown:i,href:a}=this.props;return Re.createElement(layout_utils_Link,{href:a,onClick:this.onClick,className:\"block opblock-link \"+(i?\"shown\":\"\")},Re.createElement(\"div\",null,Re.createElement(\"small\",{className:`bold-label-${o}`},o.toUpperCase()),Re.createElement(\"span\",{className:\"bold-label\"},s)))}}class InitializedInput extends Re.Component{componentDidMount(){this.props.initialValue&&(this.inputRef.value=this.props.initialValue)}render(){const{value:s,defaultValue:o,initialValue:i,...a}=this.props;return Re.createElement(\"input\",Mn()({},a,{ref:s=>this.inputRef=s}))}}class InfoBasePath extends Re.Component{render(){const{host:s,basePath:o}=this.props;return Re.createElement(\"pre\",{className:\"base-url\"},\"[ Base URL: \",s,o,\" ]\")}}class InfoUrl extends Re.PureComponent{render(){const{url:s,getComponent:o}=this.props,i=o(\"Link\");return Re.createElement(i,{target:\"_blank\",href:sanitizeUrl(s)},Re.createElement(\"span\",{className:\"url\"},\" \",s))}}class info_Info extends Re.Component{render(){const{info:s,url:o,host:i,basePath:a,getComponent:u,externalDocs:_,selectedServer:w,url:x}=this.props,C=s.get(\"version\"),j=s.get(\"description\"),L=s.get(\"title\"),B=safeBuildUrl(s.get(\"termsOfService\"),x,{selectedServer:w}),$=s.get(\"contact\"),U=s.get(\"license\"),V=safeBuildUrl(_&&_.get(\"url\"),x,{selectedServer:w}),z=_&&_.get(\"description\"),Y=u(\"Markdown\",!0),Z=u(\"Link\"),ee=u(\"VersionStamp\"),ie=u(\"OpenAPIVersion\"),ae=u(\"InfoUrl\"),ce=u(\"InfoBasePath\"),le=u(\"License\"),pe=u(\"Contact\");return Re.createElement(\"div\",{className:\"info\"},Re.createElement(\"hgroup\",{className:\"main\"},Re.createElement(\"h1\",{className:\"title\"},L,Re.createElement(\"span\",null,C&&Re.createElement(ee,{version:C}),Re.createElement(ie,{oasVersion:\"2.0\"}))),i||a?Re.createElement(ce,{host:i,basePath:a}):null,o&&Re.createElement(ae,{getComponent:u,url:o})),Re.createElement(\"div\",{className:\"description\"},Re.createElement(Y,{source:j})),B&&Re.createElement(\"div\",{className:\"info__tos\"},Re.createElement(Z,{target:\"_blank\",href:sanitizeUrl(B)},\"Terms of service\")),$?.size>0&&Re.createElement(pe,{getComponent:u,data:$,selectedServer:w,url:o}),U?.size>0&&Re.createElement(le,{getComponent:u,license:U,selectedServer:w,url:o}),V?Re.createElement(Z,{className:\"info__extdocs\",target:\"_blank\",href:sanitizeUrl(V)},z||V):null)}}const tA=info_Info;class InfoContainer extends Re.Component{render(){const{specSelectors:s,getComponent:o,oas3Selectors:i}=this.props,a=s.info(),u=s.url(),_=s.basePath(),w=s.host(),x=s.externalDocs(),C=i.selectedServer(),j=o(\"info\");return Re.createElement(\"div\",null,a&&a.count()?Re.createElement(j,{info:a,url:u,host:w,basePath:_,externalDocs:x,getComponent:o,selectedServer:C}):null)}}class contact_Contact extends Re.Component{render(){const{data:s,getComponent:o,selectedServer:i,url:a}=this.props,u=s.get(\"name\",\"the developer\"),_=safeBuildUrl(s.get(\"url\"),a,{selectedServer:i}),w=s.get(\"email\"),x=o(\"Link\");return Re.createElement(\"div\",{className:\"info__contact\"},_&&Re.createElement(\"div\",null,Re.createElement(x,{href:sanitizeUrl(_),target:\"_blank\"},u,\" - Website\")),w&&Re.createElement(x,{href:sanitizeUrl(`mailto:${w}`)},_?`Send email to ${u}`:`Contact ${u}`))}}const rA=contact_Contact;class license_License extends Re.Component{render(){const{license:s,getComponent:o,selectedServer:i,url:a}=this.props,u=s.get(\"name\",\"License\"),_=safeBuildUrl(s.get(\"url\"),a,{selectedServer:i}),w=o(\"Link\");return Re.createElement(\"div\",{className:\"info__license\"},_?Re.createElement(\"div\",{className:\"info__license__url\"},Re.createElement(w,{target:\"_blank\",href:sanitizeUrl(_)},u)):Re.createElement(\"span\",null,u))}}const nA=license_License;class JumpToPath extends Re.Component{render(){return null}}class CopyToClipboardBtn extends Re.Component{render(){let{getComponent:s}=this.props;const o=s(\"CopyIcon\");return Re.createElement(\"div\",{className:\"view-line-link copy-to-clipboard\",title:\"Copy to clipboard\"},Re.createElement(Hn.CopyToClipboard,{text:this.props.textToCopy},Re.createElement(o,null)))}}class Footer extends Re.Component{render(){return Re.createElement(\"div\",{className:\"footer\"})}}class FilterContainer extends Re.Component{onFilterChange=s=>{const{target:{value:o}}=s;this.props.layoutActions.updateFilter(o)};render(){const{specSelectors:s,layoutSelectors:o,getComponent:i}=this.props,a=i(\"Col\"),u=\"loading\"===s.loadingStatus(),_=\"failed\"===s.loadingStatus(),w=o.currentFilter(),x=[\"operation-filter-input\"];return _&&x.push(\"failed\"),u&&x.push(\"loading\"),Re.createElement(\"div\",null,!1===w?null:Re.createElement(\"div\",{className:\"filter-container\"},Re.createElement(a,{className:\"filter wrapper\",mobile:12},Re.createElement(\"input\",{className:x.join(\" \"),placeholder:\"Filter by tag\",type:\"text\",onChange:this.onFilterChange,value:\"string\"==typeof w?w:\"\",disabled:u}))))}}const sA=Function.prototype;class ParamBody extends Re.PureComponent{static defaultProp={consumes:(0,ze.fromJS)([\"application/json\"]),param:(0,ze.fromJS)({}),onChange:sA,onChangeConsumes:sA};constructor(s,o){super(s,o),this.state={isEditBox:!1,value:\"\"}}componentDidMount(){this.updateValues.call(this,this.props)}UNSAFE_componentWillReceiveProps(s){this.updateValues.call(this,s)}updateValues=s=>{let{param:o,isExecute:i,consumesValue:a=\"\"}=s,u=/xml/i.test(a),_=/json/i.test(a),w=u?o.get(\"value_xml\"):o.get(\"value\");if(void 0!==w){let s=!w&&_?\"{}\":w;this.setState({value:s}),this.onChange(s,{isXml:u,isEditBox:i})}else u?this.onChange(this.sample(\"xml\"),{isXml:u,isEditBox:i}):this.onChange(this.sample(),{isEditBox:i})};sample=s=>{let{param:o,fn:i}=this.props,a=i.inferSchema(o.toJS());return i.getSampleSchema(a,s,{includeWriteOnly:!0})};onChange=(s,{isEditBox:o,isXml:i})=>{this.setState({value:s,isEditBox:o}),this._onChange(s,i)};_onChange=(s,o)=>{(this.props.onChange||sA)(s,o)};handleOnChange=s=>{const{consumesValue:o}=this.props,i=/xml/i.test(o),a=s.target.value;this.onChange(a,{isXml:i,isEditBox:this.state.isEditBox})};toggleIsEditBox=()=>this.setState((s=>({isEditBox:!s.isEditBox})));render(){let{onChangeConsumes:s,param:o,isExecute:i,specSelectors:a,pathMethod:u,getComponent:_}=this.props;const w=_(\"Button\"),x=_(\"TextArea\"),C=_(\"HighlightCode\",!0),j=_(\"contentType\");let L=(a?a.parameterWithMetaByIdentity(u,o):o).get(\"errors\",(0,ze.List)()),B=a.contentTypeValues(u).get(\"requestContentType\"),$=this.props.consumes&&this.props.consumes.size?this.props.consumes:ParamBody.defaultProp.consumes,{value:U,isEditBox:V}=this.state,z=null;getKnownSyntaxHighlighterLanguage(U)&&(z=\"json\");const Y=`${createHtmlReadyId(`${u[1]}${u[0]}_parameters`)}_select`;return Re.createElement(\"div\",{className:\"body-param\",\"data-param-name\":o.get(\"name\"),\"data-param-in\":o.get(\"in\")},V&&i?Re.createElement(x,{className:\"body-param__text\"+(L.count()?\" invalid\":\"\"),value:U,onChange:this.handleOnChange}):U&&Re.createElement(C,{className:\"body-param__example\",language:z},U),Re.createElement(\"div\",{className:\"body-param-options\"},i?Re.createElement(\"div\",{className:\"body-param-edit\"},Re.createElement(w,{className:V?\"btn cancel body-param__example-edit\":\"btn edit body-param__example-edit\",onClick:this.toggleIsEditBox},V?\"Cancel\":\"Edit\")):null,Re.createElement(\"label\",{htmlFor:Y},Re.createElement(\"span\",null,\"Parameter content type\"),Re.createElement(j,{value:B,contentTypes:$,onChange:s,className:\"body-param-content-type\",ariaLabel:\"Parameter content type\",controlId:Y}))))}}class Curl extends Re.Component{render(){const{request:s,getComponent:o}=this.props,i=requestSnippetGenerator_curl_bash(s),a=o(\"SyntaxHighlighter\",!0);return Re.createElement(\"div\",{className:\"curl-command\"},Re.createElement(\"h4\",null,\"Curl\"),Re.createElement(\"div\",{className:\"copy-to-clipboard\"},Re.createElement(Hn.CopyToClipboard,{text:i},Re.createElement(\"button\",null))),Re.createElement(\"div\",null,Re.createElement(a,{language:\"bash\",className:\"curl microlight\",renderPlainText:({children:s,PlainTextViewer:o})=>Re.createElement(o,{className:\"curl\"},s)},i)))}}const property=({propKey:s,propVal:o,propClass:i})=>Re.createElement(\"span\",{className:i},Re.createElement(\"br\",null),s,\": \",stringify(o));class TryItOutButton extends Re.Component{static defaultProps={onTryoutClick:Function.prototype,onCancelClick:Function.prototype,onResetClick:Function.prototype,enabled:!1,hasUserEditedBody:!1,isOAS3:!1};render(){const{onTryoutClick:s,onCancelClick:o,onResetClick:i,enabled:a,hasUserEditedBody:u,isOAS3:_}=this.props,w=_&&u;return Re.createElement(\"div\",{className:w?\"try-out btn-group\":\"try-out\"},a?Re.createElement(\"button\",{className:\"btn try-out__btn cancel\",onClick:o},\"Cancel\"):Re.createElement(\"button\",{className:\"btn try-out__btn\",onClick:s},\"Try it out \"),w&&Re.createElement(\"button\",{className:\"btn try-out__btn reset\",onClick:i},\"Reset\"))}}class VersionPragmaFilter extends Re.PureComponent{static defaultProps={alsoShow:null,children:null,bypass:!1};render(){const{bypass:s,isSwagger2:o,isOAS3:i,alsoShow:a}=this.props;return s?Re.createElement(\"div\",null,this.props.children):o&&i?Re.createElement(\"div\",{className:\"version-pragma\"},a,Re.createElement(\"div\",{className:\"version-pragma__message version-pragma__message--ambiguous\"},Re.createElement(\"div\",null,Re.createElement(\"h3\",null,\"Unable to render this definition\"),Re.createElement(\"p\",null,Re.createElement(\"code\",null,\"swagger\"),\" and \",Re.createElement(\"code\",null,\"openapi\"),\" fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields.\"),Re.createElement(\"p\",null,\"Supported version fields are \",Re.createElement(\"code\",null,\"swagger: \",'\"2.0\"'),\" and those that match \",Re.createElement(\"code\",null,\"openapi: 3.0.n\"),\" (for example, \",Re.createElement(\"code\",null,\"openapi: 3.0.4\"),\").\")))):o||i?Re.createElement(\"div\",null,this.props.children):Re.createElement(\"div\",{className:\"version-pragma\"},a,Re.createElement(\"div\",{className:\"version-pragma__message version-pragma__message--missing\"},Re.createElement(\"div\",null,Re.createElement(\"h3\",null,\"Unable to render this definition\"),Re.createElement(\"p\",null,\"The provided definition does not specify a valid version field.\"),Re.createElement(\"p\",null,\"Please indicate a valid Swagger or OpenAPI version field. Supported version fields are \",Re.createElement(\"code\",null,\"swagger: \",'\"2.0\"'),\" and those that match \",Re.createElement(\"code\",null,\"openapi: 3.0.n\"),\" (for example, \",Re.createElement(\"code\",null,\"openapi: 3.0.4\"),\").\"))))}}const version_stamp=({version:s})=>Re.createElement(\"small\",null,Re.createElement(\"pre\",{className:\"version\"},\" \",s,\" \")),openapi_version=({oasVersion:s})=>Re.createElement(\"small\",{className:\"version-stamp\"},Re.createElement(\"pre\",{className:\"version\"},\"OAS \",s)),deep_link=({enabled:s,path:o,text:i})=>Re.createElement(\"a\",{className:\"nostyle\",onClick:s?s=>s.preventDefault():null,href:s?`#/${o}`:null},Re.createElement(\"span\",null,i)),svg_assets=()=>Re.createElement(\"div\",null,Re.createElement(\"svg\",{xmlns:\"http://www.w3.org/2000/svg\",xmlnsXlink:\"http://www.w3.org/1999/xlink\",className:\"svg-assets\"},Re.createElement(\"defs\",null,Re.createElement(\"symbol\",{viewBox:\"0 0 20 20\",id:\"unlocked\"},Re.createElement(\"path\",{d:\"M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z\"})),Re.createElement(\"symbol\",{viewBox:\"0 0 20 20\",id:\"locked\"},Re.createElement(\"path\",{d:\"M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z\"})),Re.createElement(\"symbol\",{viewBox:\"0 0 20 20\",id:\"close\"},Re.createElement(\"path\",{d:\"M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z\"})),Re.createElement(\"symbol\",{viewBox:\"0 0 20 20\",id:\"large-arrow\"},Re.createElement(\"path\",{d:\"M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z\"})),Re.createElement(\"symbol\",{viewBox:\"0 0 20 20\",id:\"large-arrow-down\"},Re.createElement(\"path\",{d:\"M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z\"})),Re.createElement(\"symbol\",{viewBox:\"0 0 20 20\",id:\"large-arrow-up\"},Re.createElement(\"path\",{d:\"M 17.418 14.908 C 17.69 15.176 18.127 15.176 18.397 14.908 C 18.667 14.64 18.668 14.207 18.397 13.939 L 10.489 6.109 C 10.219 5.841 9.782 5.841 9.51 6.109 L 1.602 13.939 C 1.332 14.207 1.332 14.64 1.602 14.908 C 1.873 15.176 2.311 15.176 2.581 14.908 L 10 7.767 L 17.418 14.908 Z\"})),Re.createElement(\"symbol\",{viewBox:\"0 0 24 24\",id:\"jump-to\"},Re.createElement(\"path\",{d:\"M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z\"})),Re.createElement(\"symbol\",{viewBox:\"0 0 24 24\",id:\"expand\"},Re.createElement(\"path\",{d:\"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z\"})),Re.createElement(\"symbol\",{viewBox:\"0 0 15 16\",id:\"copy\"},Re.createElement(\"g\",{transform:\"translate(2, -1)\"},Re.createElement(\"path\",{fill:\"#ffffff\",fillRule:\"evenodd\",d:\"M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z\"}))))));var oA;function decodeEntity(s){return(oA=oA||document.createElement(\"textarea\")).innerHTML=\"&\"+s+\";\",oA.value}var iA=Object.prototype.hasOwnProperty;function index_browser_has(s,o){return!!s&&iA.call(s,o)}function index_browser_assign(s){return[].slice.call(arguments,1).forEach((function(o){if(o){if(\"object\"!=typeof o)throw new TypeError(o+\"must be object\");Object.keys(o).forEach((function(i){s[i]=o[i]}))}})),s}var aA=/\\\\([\\\\!\"#$%&'()*+,.\\/:;<=>?@[\\]^_`{|}~-])/g;function unescapeMd(s){return s.indexOf(\"\\\\\")<0?s:s.replace(aA,\"$1\")}function isValidEntityCode(s){return!(s>=55296&&s<=57343)&&(!(s>=64976&&s<=65007)&&(!!(65535&~s&&65534!=(65535&s))&&(!(s>=0&&s<=8)&&(11!==s&&(!(s>=14&&s<=31)&&(!(s>=127&&s<=159)&&!(s>1114111)))))))}function fromCodePoint(s){if(s>65535){var o=55296+((s-=65536)>>10),i=56320+(1023&s);return String.fromCharCode(o,i)}return String.fromCharCode(s)}var cA=/&([a-z#][a-z0-9]{1,31});/gi,lA=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i;function replaceEntityPattern(s,o){var i=0,a=decodeEntity(o);return o!==a?a:35===o.charCodeAt(0)&&lA.test(o)&&isValidEntityCode(i=\"x\"===o[1].toLowerCase()?parseInt(o.slice(2),16):parseInt(o.slice(1),10))?fromCodePoint(i):s}function replaceEntities(s){return s.indexOf(\"&\")<0?s:s.replace(cA,replaceEntityPattern)}var uA=/[&<>\"]/,pA=/[&<>\"]/g,hA={\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\"};function replaceUnsafeChar(s){return hA[s]}function escapeHtml(s){return uA.test(s)?s.replace(pA,replaceUnsafeChar):s}var dA={};function nextToken(s,o){return++o>=s.length-2?o:\"paragraph_open\"===s[o].type&&s[o].tight&&\"inline\"===s[o+1].type&&0===s[o+1].content.length&&\"paragraph_close\"===s[o+2].type&&s[o+2].tight?nextToken(s,o+2):o}dA.blockquote_open=function(){return\"<blockquote>\\n\"},dA.blockquote_close=function(s,o){return\"</blockquote>\"+fA(s,o)},dA.code=function(s,o){return s[o].block?\"<pre><code>\"+escapeHtml(s[o].content)+\"</code></pre>\"+fA(s,o):\"<code>\"+escapeHtml(s[o].content)+\"</code>\"},dA.fence=function(s,o,i,a,u){var _,w,x=s[o],C=\"\",j=i.langPrefix;if(x.params){if(w=(_=x.params.split(/\\s+/g)).join(\" \"),index_browser_has(u.rules.fence_custom,_[0]))return u.rules.fence_custom[_[0]](s,o,i,a,u);C=' class=\"'+j+escapeHtml(replaceEntities(unescapeMd(w)))+'\"'}return\"<pre><code\"+C+\">\"+(i.highlight&&i.highlight.apply(i.highlight,[x.content].concat(_))||escapeHtml(x.content))+\"</code></pre>\"+fA(s,o)},dA.fence_custom={},dA.heading_open=function(s,o){return\"<h\"+s[o].hLevel+\">\"},dA.heading_close=function(s,o){return\"</h\"+s[o].hLevel+\">\\n\"},dA.hr=function(s,o,i){return(i.xhtmlOut?\"<hr />\":\"<hr>\")+fA(s,o)},dA.bullet_list_open=function(){return\"<ul>\\n\"},dA.bullet_list_close=function(s,o){return\"</ul>\"+fA(s,o)},dA.list_item_open=function(){return\"<li>\"},dA.list_item_close=function(){return\"</li>\\n\"},dA.ordered_list_open=function(s,o){var i=s[o];return\"<ol\"+(i.order>1?' start=\"'+i.order+'\"':\"\")+\">\\n\"},dA.ordered_list_close=function(s,o){return\"</ol>\"+fA(s,o)},dA.paragraph_open=function(s,o){return s[o].tight?\"\":\"<p>\"},dA.paragraph_close=function(s,o){var i=!(s[o].tight&&o&&\"inline\"===s[o-1].type&&!s[o-1].content);return(s[o].tight?\"\":\"</p>\")+(i?fA(s,o):\"\")},dA.link_open=function(s,o,i){var a=s[o].title?' title=\"'+escapeHtml(replaceEntities(s[o].title))+'\"':\"\",u=i.linkTarget?' target=\"'+i.linkTarget+'\"':\"\";return'<a href=\"'+escapeHtml(s[o].href)+'\"'+a+u+\">\"},dA.link_close=function(){return\"</a>\"},dA.image=function(s,o,i){var a=' src=\"'+escapeHtml(s[o].src)+'\"',u=s[o].title?' title=\"'+escapeHtml(replaceEntities(s[o].title))+'\"':\"\";return\"<img\"+a+(' alt=\"'+(s[o].alt?escapeHtml(replaceEntities(unescapeMd(s[o].alt))):\"\")+'\"')+u+(i.xhtmlOut?\" /\":\"\")+\">\"},dA.table_open=function(){return\"<table>\\n\"},dA.table_close=function(){return\"</table>\\n\"},dA.thead_open=function(){return\"<thead>\\n\"},dA.thead_close=function(){return\"</thead>\\n\"},dA.tbody_open=function(){return\"<tbody>\\n\"},dA.tbody_close=function(){return\"</tbody>\\n\"},dA.tr_open=function(){return\"<tr>\"},dA.tr_close=function(){return\"</tr>\\n\"},dA.th_open=function(s,o){var i=s[o];return\"<th\"+(i.align?' style=\"text-align:'+i.align+'\"':\"\")+\">\"},dA.th_close=function(){return\"</th>\"},dA.td_open=function(s,o){var i=s[o];return\"<td\"+(i.align?' style=\"text-align:'+i.align+'\"':\"\")+\">\"},dA.td_close=function(){return\"</td>\"},dA.strong_open=function(){return\"<strong>\"},dA.strong_close=function(){return\"</strong>\"},dA.em_open=function(){return\"<em>\"},dA.em_close=function(){return\"</em>\"},dA.del_open=function(){return\"<del>\"},dA.del_close=function(){return\"</del>\"},dA.ins_open=function(){return\"<ins>\"},dA.ins_close=function(){return\"</ins>\"},dA.mark_open=function(){return\"<mark>\"},dA.mark_close=function(){return\"</mark>\"},dA.sub=function(s,o){return\"<sub>\"+escapeHtml(s[o].content)+\"</sub>\"},dA.sup=function(s,o){return\"<sup>\"+escapeHtml(s[o].content)+\"</sup>\"},dA.hardbreak=function(s,o,i){return i.xhtmlOut?\"<br />\\n\":\"<br>\\n\"},dA.softbreak=function(s,o,i){return i.breaks?i.xhtmlOut?\"<br />\\n\":\"<br>\\n\":\"\\n\"},dA.text=function(s,o){return escapeHtml(s[o].content)},dA.htmlblock=function(s,o){return s[o].content},dA.htmltag=function(s,o){return s[o].content},dA.abbr_open=function(s,o){return'<abbr title=\"'+escapeHtml(replaceEntities(s[o].title))+'\">'},dA.abbr_close=function(){return\"</abbr>\"},dA.footnote_ref=function(s,o){var i=Number(s[o].id+1).toString(),a=\"fnref\"+i;return s[o].subId>0&&(a+=\":\"+s[o].subId),'<sup class=\"footnote-ref\"><a href=\"#fn'+i+'\" id=\"'+a+'\">['+i+\"]</a></sup>\"},dA.footnote_block_open=function(s,o,i){return(i.xhtmlOut?'<hr class=\"footnotes-sep\" />\\n':'<hr class=\"footnotes-sep\">\\n')+'<section class=\"footnotes\">\\n<ol class=\"footnotes-list\">\\n'},dA.footnote_block_close=function(){return\"</ol>\\n</section>\\n\"},dA.footnote_open=function(s,o){return'<li id=\"fn'+Number(s[o].id+1).toString()+'\"  class=\"footnote-item\">'},dA.footnote_close=function(){return\"</li>\\n\"},dA.footnote_anchor=function(s,o){var i=\"fnref\"+Number(s[o].id+1).toString();return s[o].subId>0&&(i+=\":\"+s[o].subId),' <a href=\"#'+i+'\" class=\"footnote-backref\">↩</a>'},dA.dl_open=function(){return\"<dl>\\n\"},dA.dt_open=function(){return\"<dt>\"},dA.dd_open=function(){return\"<dd>\"},dA.dl_close=function(){return\"</dl>\\n\"},dA.dt_close=function(){return\"</dt>\\n\"},dA.dd_close=function(){return\"</dd>\\n\"};var fA=dA.getBreak=function getBreak(s,o){return(o=nextToken(s,o))<s.length&&\"list_item_close\"===s[o].type?\"\":\"\\n\"};function Renderer(){this.rules=index_browser_assign({},dA),this.getBreak=dA.getBreak}function Ruler(){this.__rules__=[],this.__cache__=null}function StateInline(s,o,i,a,u){this.src=s,this.env=a,this.options=i,this.parser=o,this.tokens=u,this.pos=0,this.posMax=this.src.length,this.level=0,this.pending=\"\",this.pendingLevel=0,this.cache=[],this.isInLabel=!1,this.linkLevel=0,this.linkContent=\"\",this.labelUnmatchedScopes=0}function parseLinkLabel(s,o){var i,a,u,_=-1,w=s.posMax,x=s.pos,C=s.isInLabel;if(s.isInLabel)return-1;if(s.labelUnmatchedScopes)return s.labelUnmatchedScopes--,-1;for(s.pos=o+1,s.isInLabel=!0,i=1;s.pos<w;){if(91===(u=s.src.charCodeAt(s.pos)))i++;else if(93===u&&0===--i){a=!0;break}s.parser.skipToken(s)}return a?(_=s.pos,s.labelUnmatchedScopes=0):s.labelUnmatchedScopes=i-1,s.pos=x,s.isInLabel=C,_}function parseAbbr(s,o,i,a){var u,_,w,x,C,j;if(42!==s.charCodeAt(0))return-1;if(91!==s.charCodeAt(1))return-1;if(-1===s.indexOf(\"]:\"))return-1;if((_=parseLinkLabel(u=new StateInline(s,o,i,a,[]),1))<0||58!==s.charCodeAt(_+1))return-1;for(x=u.posMax,w=_+2;w<x&&10!==u.src.charCodeAt(w);w++);return C=s.slice(2,_),0===(j=s.slice(_+2,w).trim()).length?-1:(a.abbreviations||(a.abbreviations={}),void 0===a.abbreviations[\":\"+C]&&(a.abbreviations[\":\"+C]=j),w)}function normalizeLink(s){var o=replaceEntities(s);try{o=decodeURI(o)}catch(s){}return encodeURI(o)}function parseLinkDestination(s,o){var i,a,u,_=o,w=s.posMax;if(60===s.src.charCodeAt(o)){for(o++;o<w;){if(10===(i=s.src.charCodeAt(o)))return!1;if(62===i)return u=normalizeLink(unescapeMd(s.src.slice(_+1,o))),!!s.parser.validateLink(u)&&(s.pos=o+1,s.linkContent=u,!0);92===i&&o+1<w?o+=2:o++}return!1}for(a=0;o<w&&32!==(i=s.src.charCodeAt(o))&&!(i<32||127===i);)if(92===i&&o+1<w)o+=2;else{if(40===i&&++a>1)break;if(41===i&&--a<0)break;o++}return _!==o&&(u=unescapeMd(s.src.slice(_,o)),!!s.parser.validateLink(u)&&(s.linkContent=u,s.pos=o,!0))}function parseLinkTitle(s,o){var i,a=o,u=s.posMax,_=s.src.charCodeAt(o);if(34!==_&&39!==_&&40!==_)return!1;for(o++,40===_&&(_=41);o<u;){if((i=s.src.charCodeAt(o))===_)return s.pos=o+1,s.linkContent=unescapeMd(s.src.slice(a+1,o)),!0;92===i&&o+1<u?o+=2:o++}return!1}function normalizeReference(s){return s.trim().replace(/\\s+/g,\" \").toUpperCase()}function parseReference(s,o,i,a){var u,_,w,x,C,j,L,B,$;if(91!==s.charCodeAt(0))return-1;if(-1===s.indexOf(\"]:\"))return-1;if((_=parseLinkLabel(u=new StateInline(s,o,i,a,[]),0))<0||58!==s.charCodeAt(_+1))return-1;for(x=u.posMax,w=_+2;w<x&&(32===(C=u.src.charCodeAt(w))||10===C);w++);if(!parseLinkDestination(u,w))return-1;for(L=u.linkContent,j=w=u.pos,w+=1;w<x&&(32===(C=u.src.charCodeAt(w))||10===C);w++);for(w<x&&j!==w&&parseLinkTitle(u,w)?(B=u.linkContent,w=u.pos):(B=\"\",w=j);w<x&&32===u.src.charCodeAt(w);)w++;return w<x&&10!==u.src.charCodeAt(w)?-1:($=normalizeReference(s.slice(1,_)),void 0===a.references[$]&&(a.references[$]={title:B,href:L}),w)}Renderer.prototype.renderInline=function(s,o,i){for(var a=this.rules,u=s.length,_=0,w=\"\";u--;)w+=a[s[_].type](s,_++,o,i,this);return w},Renderer.prototype.render=function(s,o,i){for(var a=this.rules,u=s.length,_=-1,w=\"\";++_<u;)\"inline\"===s[_].type?w+=this.renderInline(s[_].children,o,i):w+=a[s[_].type](s,_,o,i,this);return w},Ruler.prototype.__find__=function(s){for(var o=this.__rules__.length,i=-1;o--;)if(this.__rules__[++i].name===s)return i;return-1},Ruler.prototype.__compile__=function(){var s=this,o=[\"\"];s.__rules__.forEach((function(s){s.enabled&&s.alt.forEach((function(s){o.indexOf(s)<0&&o.push(s)}))})),s.__cache__={},o.forEach((function(o){s.__cache__[o]=[],s.__rules__.forEach((function(i){i.enabled&&(o&&i.alt.indexOf(o)<0||s.__cache__[o].push(i.fn))}))}))},Ruler.prototype.at=function(s,o,i){var a=this.__find__(s),u=i||{};if(-1===a)throw new Error(\"Parser rule not found: \"+s);this.__rules__[a].fn=o,this.__rules__[a].alt=u.alt||[],this.__cache__=null},Ruler.prototype.before=function(s,o,i,a){var u=this.__find__(s),_=a||{};if(-1===u)throw new Error(\"Parser rule not found: \"+s);this.__rules__.splice(u,0,{name:o,enabled:!0,fn:i,alt:_.alt||[]}),this.__cache__=null},Ruler.prototype.after=function(s,o,i,a){var u=this.__find__(s),_=a||{};if(-1===u)throw new Error(\"Parser rule not found: \"+s);this.__rules__.splice(u+1,0,{name:o,enabled:!0,fn:i,alt:_.alt||[]}),this.__cache__=null},Ruler.prototype.push=function(s,o,i){var a=i||{};this.__rules__.push({name:s,enabled:!0,fn:o,alt:a.alt||[]}),this.__cache__=null},Ruler.prototype.enable=function(s,o){s=Array.isArray(s)?s:[s],o&&this.__rules__.forEach((function(s){s.enabled=!1})),s.forEach((function(s){var o=this.__find__(s);if(o<0)throw new Error(\"Rules manager: invalid rule name \"+s);this.__rules__[o].enabled=!0}),this),this.__cache__=null},Ruler.prototype.disable=function(s){(s=Array.isArray(s)?s:[s]).forEach((function(s){var o=this.__find__(s);if(o<0)throw new Error(\"Rules manager: invalid rule name \"+s);this.__rules__[o].enabled=!1}),this),this.__cache__=null},Ruler.prototype.getRules=function(s){return null===this.__cache__&&this.__compile__(),this.__cache__[s]||[]},StateInline.prototype.pushPending=function(){this.tokens.push({type:\"text\",content:this.pending,level:this.pendingLevel}),this.pending=\"\"},StateInline.prototype.push=function(s){this.pending&&this.pushPending(),this.tokens.push(s),this.pendingLevel=this.level},StateInline.prototype.cacheSet=function(s,o){for(var i=this.cache.length;i<=s;i++)this.cache.push(0);this.cache[s]=o},StateInline.prototype.cacheGet=function(s){return s<this.cache.length?this.cache[s]:0};var mA=\" \\n()[]'\\\".,!?-\";function regEscape(s){return s.replace(/([-()\\[\\]{}+?*.$\\^|,:#<!\\\\])/g,\"\\\\$1\")}var gA=/\\+-|\\.\\.|\\?\\?\\?\\?|!!!!|,,|--/,yA=/\\((c|tm|r|p)\\)/gi,vA={c:\"©\",r:\"®\",p:\"§\",tm:\"™\"};function replaceScopedAbbr(s){return s.indexOf(\"(\")<0?s:s.replace(yA,(function(s,o){return vA[o.toLowerCase()]}))}var bA=/['\"]/,_A=/['\"]/g,SA=/[-\\s()\\[\\]]/;function isLetter(s,o){return!(o<0||o>=s.length)&&!SA.test(s[o])}function replaceAt(s,o,i){return s.substr(0,o)+i+s.substr(o+1)}var EA=[[\"block\",function block(s){s.inlineMode?s.tokens.push({type:\"inline\",content:s.src.replace(/\\n/g,\" \").trim(),level:0,lines:[0,1],children:[]}):s.block.parse(s.src,s.options,s.env,s.tokens)}],[\"abbr\",function abbr(s){var o,i,a,u,_=s.tokens;if(!s.inlineMode)for(o=1,i=_.length-1;o<i;o++)if(\"paragraph_open\"===_[o-1].type&&\"inline\"===_[o].type&&\"paragraph_close\"===_[o+1].type){for(a=_[o].content;a.length&&!((u=parseAbbr(a,s.inline,s.options,s.env))<0);)a=a.slice(u).trim();_[o].content=a,a.length||(_[o-1].tight=!0,_[o+1].tight=!0)}}],[\"references\",function references(s){var o,i,a,u,_=s.tokens;if(s.env.references=s.env.references||{},!s.inlineMode)for(o=1,i=_.length-1;o<i;o++)if(\"inline\"===_[o].type&&\"paragraph_open\"===_[o-1].type&&\"paragraph_close\"===_[o+1].type){for(a=_[o].content;a.length&&!((u=parseReference(a,s.inline,s.options,s.env))<0);)a=a.slice(u).trim();_[o].content=a,a.length||(_[o-1].tight=!0,_[o+1].tight=!0)}}],[\"inline\",function inline(s){var o,i,a,u=s.tokens;for(i=0,a=u.length;i<a;i++)\"inline\"===(o=u[i]).type&&s.inline.parse(o.content,s.options,s.env,o.children)}],[\"footnote_tail\",function footnote_block(s){var o,i,a,u,_,w,x,C,j,L=0,B=!1,$={};if(s.env.footnotes&&(s.tokens=s.tokens.filter((function(s){return\"footnote_reference_open\"===s.type?(B=!0,C=[],j=s.label,!1):\"footnote_reference_close\"===s.type?(B=!1,$[\":\"+j]=C,!1):(B&&C.push(s),!B)})),s.env.footnotes.list)){for(w=s.env.footnotes.list,s.tokens.push({type:\"footnote_block_open\",level:L++}),o=0,i=w.length;o<i;o++){for(s.tokens.push({type:\"footnote_open\",id:o,level:L++}),w[o].tokens?((x=[]).push({type:\"paragraph_open\",tight:!1,level:L++}),x.push({type:\"inline\",content:\"\",level:L,children:w[o].tokens}),x.push({type:\"paragraph_close\",tight:!1,level:--L})):w[o].label&&(x=$[\":\"+w[o].label]),s.tokens=s.tokens.concat(x),_=\"paragraph_close\"===s.tokens[s.tokens.length-1].type?s.tokens.pop():null,u=w[o].count>0?w[o].count:1,a=0;a<u;a++)s.tokens.push({type:\"footnote_anchor\",id:o,subId:a,level:L});_&&s.tokens.push(_),s.tokens.push({type:\"footnote_close\",level:--L})}s.tokens.push({type:\"footnote_block_close\",level:--L})}}],[\"abbr2\",function abbr2(s){var o,i,a,u,_,w,x,C,j,L,B,$,U=s.tokens;if(s.env.abbreviations)for(s.env.abbrRegExp||($=\"(^|[\"+mA.split(\"\").map(regEscape).join(\"\")+\"])(\"+Object.keys(s.env.abbreviations).map((function(s){return s.substr(1)})).sort((function(s,o){return o.length-s.length})).map(regEscape).join(\"|\")+\")($|[\"+mA.split(\"\").map(regEscape).join(\"\")+\"])\",s.env.abbrRegExp=new RegExp($,\"g\")),L=s.env.abbrRegExp,i=0,a=U.length;i<a;i++)if(\"inline\"===U[i].type)for(o=(u=U[i].children).length-1;o>=0;o--)if(\"text\"===(_=u[o]).type){for(C=0,w=_.content,L.lastIndex=0,j=_.level,x=[];B=L.exec(w);)L.lastIndex>C&&x.push({type:\"text\",content:w.slice(C,B.index+B[1].length),level:j}),x.push({type:\"abbr_open\",title:s.env.abbreviations[\":\"+B[2]],level:j++}),x.push({type:\"text\",content:B[2],level:j}),x.push({type:\"abbr_close\",level:--j}),C=L.lastIndex-B[3].length;x.length&&(C<w.length&&x.push({type:\"text\",content:w.slice(C),level:j}),U[i].children=u=[].concat(u.slice(0,o),x,u.slice(o+1)))}}],[\"replacements\",function index_browser_replace(s){var o,i,a,u,_;if(s.options.typographer)for(_=s.tokens.length-1;_>=0;_--)if(\"inline\"===s.tokens[_].type)for(o=(u=s.tokens[_].children).length-1;o>=0;o--)\"text\"===(i=u[o]).type&&(a=replaceScopedAbbr(a=i.content),gA.test(a)&&(a=a.replace(/\\+-/g,\"±\").replace(/\\.{2,}/g,\"…\").replace(/([?!])…/g,\"$1..\").replace(/([?!]){4,}/g,\"$1$1$1\").replace(/,{2,}/g,\",\").replace(/(^|[^-])---([^-]|$)/gm,\"$1—$2\").replace(/(^|\\s)--(\\s|$)/gm,\"$1–$2\").replace(/(^|[^-\\s])--([^-\\s]|$)/gm,\"$1–$2\")),i.content=a)}],[\"smartquotes\",function smartquotes(s){var o,i,a,u,_,w,x,C,j,L,B,$,U,V,z,Y,Z;if(s.options.typographer)for(Z=[],z=s.tokens.length-1;z>=0;z--)if(\"inline\"===s.tokens[z].type)for(Y=s.tokens[z].children,Z.length=0,o=0;o<Y.length;o++)if(\"text\"===(i=Y[o]).type&&!bA.test(i.text)){for(x=Y[o].level,U=Z.length-1;U>=0&&!(Z[U].level<=x);U--);Z.length=U+1,_=0,w=(a=i.content).length;e:for(;_<w&&(_A.lastIndex=_,u=_A.exec(a));)if(C=!isLetter(a,u.index-1),_=u.index+1,V=\"'\"===u[0],(j=!isLetter(a,_))||C){if(B=!j,$=!C)for(U=Z.length-1;U>=0&&(L=Z[U],!(Z[U].level<x));U--)if(L.single===V&&Z[U].level===x){L=Z[U],V?(Y[L.token].content=replaceAt(Y[L.token].content,L.pos,s.options.quotes[2]),i.content=replaceAt(i.content,u.index,s.options.quotes[3])):(Y[L.token].content=replaceAt(Y[L.token].content,L.pos,s.options.quotes[0]),i.content=replaceAt(i.content,u.index,s.options.quotes[1])),Z.length=U;continue e}B?Z.push({token:o,pos:u.index,single:V,level:x}):$&&V&&(i.content=replaceAt(i.content,u.index,\"’\"))}else V&&(i.content=replaceAt(i.content,u.index,\"’\"))}}]];function Core(){this.options={},this.ruler=new Ruler;for(var s=0;s<EA.length;s++)this.ruler.push(EA[s][0],EA[s][1])}function StateBlock(s,o,i,a,u){var _,w,x,C,j,L,B;for(this.src=s,this.parser=o,this.options=i,this.env=a,this.tokens=u,this.bMarks=[],this.eMarks=[],this.tShift=[],this.blkIndent=0,this.line=0,this.lineMax=0,this.tight=!1,this.parentType=\"root\",this.ddIndent=-1,this.level=0,this.result=\"\",L=0,B=!1,x=C=L=0,j=(w=this.src).length;C<j;C++){if(_=w.charCodeAt(C),!B){if(32===_){L++;continue}B=!0}10!==_&&C!==j-1||(10!==_&&C++,this.bMarks.push(x),this.eMarks.push(C),this.tShift.push(L),B=!1,L=0,x=C+1)}this.bMarks.push(w.length),this.eMarks.push(w.length),this.tShift.push(0),this.lineMax=this.bMarks.length-1}function skipBulletListMarker(s,o){var i,a,u;return(a=s.bMarks[o]+s.tShift[o])>=(u=s.eMarks[o])||42!==(i=s.src.charCodeAt(a++))&&45!==i&&43!==i||a<u&&32!==s.src.charCodeAt(a)?-1:a}function skipOrderedListMarker(s,o){var i,a=s.bMarks[o]+s.tShift[o],u=s.eMarks[o];if(a+1>=u)return-1;if((i=s.src.charCodeAt(a++))<48||i>57)return-1;for(;;){if(a>=u)return-1;if(!((i=s.src.charCodeAt(a++))>=48&&i<=57)){if(41===i||46===i)break;return-1}}return a<u&&32!==s.src.charCodeAt(a)?-1:a}Core.prototype.process=function(s){var o,i,a;for(o=0,i=(a=this.ruler.getRules(\"\")).length;o<i;o++)a[o](s)},StateBlock.prototype.isEmpty=function isEmpty(s){return this.bMarks[s]+this.tShift[s]>=this.eMarks[s]},StateBlock.prototype.skipEmptyLines=function skipEmptyLines(s){for(var o=this.lineMax;s<o&&!(this.bMarks[s]+this.tShift[s]<this.eMarks[s]);s++);return s},StateBlock.prototype.skipSpaces=function skipSpaces(s){for(var o=this.src.length;s<o&&32===this.src.charCodeAt(s);s++);return s},StateBlock.prototype.skipChars=function skipChars(s,o){for(var i=this.src.length;s<i&&this.src.charCodeAt(s)===o;s++);return s},StateBlock.prototype.skipCharsBack=function skipCharsBack(s,o,i){if(s<=i)return s;for(;s>i;)if(o!==this.src.charCodeAt(--s))return s+1;return s},StateBlock.prototype.getLines=function getLines(s,o,i,a){var u,_,w,x,C,j=s;if(s>=o)return\"\";if(j+1===o)return _=this.bMarks[j]+Math.min(this.tShift[j],i),w=a?this.eMarks[j]+1:this.eMarks[j],this.src.slice(_,w);for(x=new Array(o-s),u=0;j<o;j++,u++)(C=this.tShift[j])>i&&(C=i),C<0&&(C=0),_=this.bMarks[j]+C,w=j+1<o||a?this.eMarks[j]+1:this.eMarks[j],x[u]=this.src.slice(_,w);return x.join(\"\")};var wA={};[\"article\",\"aside\",\"button\",\"blockquote\",\"body\",\"canvas\",\"caption\",\"col\",\"colgroup\",\"dd\",\"div\",\"dl\",\"dt\",\"embed\",\"fieldset\",\"figcaption\",\"figure\",\"footer\",\"form\",\"h1\",\"h2\",\"h3\",\"h4\",\"h5\",\"h6\",\"header\",\"hgroup\",\"hr\",\"iframe\",\"li\",\"map\",\"object\",\"ol\",\"output\",\"p\",\"pre\",\"progress\",\"script\",\"section\",\"style\",\"table\",\"tbody\",\"td\",\"textarea\",\"tfoot\",\"th\",\"tr\",\"thead\",\"ul\",\"video\"].forEach((function(s){wA[s]=!0}));var xA=/^<([a-zA-Z]{1,15})[\\s\\/>]/,kA=/^<\\/([a-zA-Z]{1,15})[\\s>]/;function index_browser_getLine(s,o){var i=s.bMarks[o]+s.blkIndent,a=s.eMarks[o];return s.src.substr(i,a-i)}function skipMarker(s,o){var i,a,u=s.bMarks[o]+s.tShift[o],_=s.eMarks[o];return u>=_||126!==(a=s.src.charCodeAt(u++))&&58!==a||u===(i=s.skipSpaces(u))||i>=_?-1:i}var OA=[[\"code\",function code(s,o,i){var a,u;if(s.tShift[o]-s.blkIndent<4)return!1;for(u=a=o+1;a<i;)if(s.isEmpty(a))a++;else{if(!(s.tShift[a]-s.blkIndent>=4))break;u=++a}return s.line=a,s.tokens.push({type:\"code\",content:s.getLines(o,u,4+s.blkIndent,!0),block:!0,lines:[o,s.line],level:s.level}),!0}],[\"fences\",function fences(s,o,i,a){var u,_,w,x,C,j=!1,L=s.bMarks[o]+s.tShift[o],B=s.eMarks[o];if(L+3>B)return!1;if(126!==(u=s.src.charCodeAt(L))&&96!==u)return!1;if(C=L,(_=(L=s.skipChars(L,u))-C)<3)return!1;if((w=s.src.slice(L,B).trim()).indexOf(\"`\")>=0)return!1;if(a)return!0;for(x=o;!(++x>=i)&&!((L=C=s.bMarks[x]+s.tShift[x])<(B=s.eMarks[x])&&s.tShift[x]<s.blkIndent);)if(s.src.charCodeAt(L)===u&&!(s.tShift[x]-s.blkIndent>=4||(L=s.skipChars(L,u))-C<_||(L=s.skipSpaces(L))<B)){j=!0;break}return _=s.tShift[o],s.line=x+(j?1:0),s.tokens.push({type:\"fence\",params:w,content:s.getLines(o+1,x,_,!0),lines:[o,s.line],level:s.level}),!0},[\"paragraph\",\"blockquote\",\"list\"]],[\"blockquote\",function blockquote(s,o,i,a){var u,_,w,x,C,j,L,B,$,U,V,z=s.bMarks[o]+s.tShift[o],Y=s.eMarks[o];if(z>Y)return!1;if(62!==s.src.charCodeAt(z++))return!1;if(s.level>=s.options.maxNesting)return!1;if(a)return!0;for(32===s.src.charCodeAt(z)&&z++,C=s.blkIndent,s.blkIndent=0,x=[s.bMarks[o]],s.bMarks[o]=z,_=(z=z<Y?s.skipSpaces(z):z)>=Y,w=[s.tShift[o]],s.tShift[o]=z-s.bMarks[o],B=s.parser.ruler.getRules(\"blockquote\"),u=o+1;u<i&&!((z=s.bMarks[u]+s.tShift[u])>=(Y=s.eMarks[u]));u++)if(62!==s.src.charCodeAt(z++)){if(_)break;for(V=!1,$=0,U=B.length;$<U;$++)if(B[$](s,u,i,!0)){V=!0;break}if(V)break;x.push(s.bMarks[u]),w.push(s.tShift[u]),s.tShift[u]=-1337}else 32===s.src.charCodeAt(z)&&z++,x.push(s.bMarks[u]),s.bMarks[u]=z,_=(z=z<Y?s.skipSpaces(z):z)>=Y,w.push(s.tShift[u]),s.tShift[u]=z-s.bMarks[u];for(j=s.parentType,s.parentType=\"blockquote\",s.tokens.push({type:\"blockquote_open\",lines:L=[o,0],level:s.level++}),s.parser.tokenize(s,o,u),s.tokens.push({type:\"blockquote_close\",level:--s.level}),s.parentType=j,L[1]=s.line,$=0;$<w.length;$++)s.bMarks[$+o]=x[$],s.tShift[$+o]=w[$];return s.blkIndent=C,!0},[\"paragraph\",\"blockquote\",\"list\"]],[\"hr\",function hr(s,o,i,a){var u,_,w,x=s.bMarks[o],C=s.eMarks[o];if((x+=s.tShift[o])>C)return!1;if(42!==(u=s.src.charCodeAt(x++))&&45!==u&&95!==u)return!1;for(_=1;x<C;){if((w=s.src.charCodeAt(x++))!==u&&32!==w)return!1;w===u&&_++}return!(_<3)&&(a||(s.line=o+1,s.tokens.push({type:\"hr\",lines:[o,s.line],level:s.level})),!0)},[\"paragraph\",\"blockquote\",\"list\"]],[\"list\",function index_browser_list(s,o,i,a){var u,_,w,x,C,j,L,B,$,U,V,z,Y,Z,ee,ie,ae,ce,le,pe,de,fe=!0;if((B=skipOrderedListMarker(s,o))>=0)z=!0;else{if(!((B=skipBulletListMarker(s,o))>=0))return!1;z=!1}if(s.level>=s.options.maxNesting)return!1;if(V=s.src.charCodeAt(B-1),a)return!0;for(Z=s.tokens.length,z?(L=s.bMarks[o]+s.tShift[o],U=Number(s.src.substr(L,B-L-1)),s.tokens.push({type:\"ordered_list_open\",order:U,lines:ie=[o,0],level:s.level++})):s.tokens.push({type:\"bullet_list_open\",lines:ie=[o,0],level:s.level++}),u=o,ee=!1,ce=s.parser.ruler.getRules(\"list\");!(!(u<i)||(($=(Y=s.skipSpaces(B))>=s.eMarks[u]?1:Y-B)>4&&($=1),$<1&&($=1),_=B-s.bMarks[u]+$,s.tokens.push({type:\"list_item_open\",lines:ae=[o,0],level:s.level++}),x=s.blkIndent,C=s.tight,w=s.tShift[o],j=s.parentType,s.tShift[o]=Y-s.bMarks[o],s.blkIndent=_,s.tight=!0,s.parentType=\"list\",s.parser.tokenize(s,o,i,!0),s.tight&&!ee||(fe=!1),ee=s.line-o>1&&s.isEmpty(s.line-1),s.blkIndent=x,s.tShift[o]=w,s.tight=C,s.parentType=j,s.tokens.push({type:\"list_item_close\",level:--s.level}),u=o=s.line,ae[1]=u,Y=s.bMarks[o],u>=i)||s.isEmpty(u)||s.tShift[u]<s.blkIndent);){for(de=!1,le=0,pe=ce.length;le<pe;le++)if(ce[le](s,u,i,!0)){de=!0;break}if(de)break;if(z){if((B=skipOrderedListMarker(s,u))<0)break}else if((B=skipBulletListMarker(s,u))<0)break;if(V!==s.src.charCodeAt(B-1))break}return s.tokens.push({type:z?\"ordered_list_close\":\"bullet_list_close\",level:--s.level}),ie[1]=u,s.line=u,fe&&function markTightParagraphs(s,o){var i,a,u=s.level+2;for(i=o+2,a=s.tokens.length-2;i<a;i++)s.tokens[i].level===u&&\"paragraph_open\"===s.tokens[i].type&&(s.tokens[i+2].tight=!0,s.tokens[i].tight=!0,i+=2)}(s,Z),!0},[\"paragraph\",\"blockquote\"]],[\"footnote\",function footnote(s,o,i,a){var u,_,w,x,C,j=s.bMarks[o]+s.tShift[o],L=s.eMarks[o];if(j+4>L)return!1;if(91!==s.src.charCodeAt(j))return!1;if(94!==s.src.charCodeAt(j+1))return!1;if(s.level>=s.options.maxNesting)return!1;for(x=j+2;x<L;x++){if(32===s.src.charCodeAt(x))return!1;if(93===s.src.charCodeAt(x))break}return x!==j+2&&(!(x+1>=L||58!==s.src.charCodeAt(++x))&&(a||(x++,s.env.footnotes||(s.env.footnotes={}),s.env.footnotes.refs||(s.env.footnotes.refs={}),C=s.src.slice(j+2,x-2),s.env.footnotes.refs[\":\"+C]=-1,s.tokens.push({type:\"footnote_reference_open\",label:C,level:s.level++}),u=s.bMarks[o],_=s.tShift[o],w=s.parentType,s.tShift[o]=s.skipSpaces(x)-x,s.bMarks[o]=x,s.blkIndent+=4,s.parentType=\"footnote\",s.tShift[o]<s.blkIndent&&(s.tShift[o]+=s.blkIndent,s.bMarks[o]-=s.blkIndent),s.parser.tokenize(s,o,i,!0),s.parentType=w,s.blkIndent-=4,s.tShift[o]=_,s.bMarks[o]=u,s.tokens.push({type:\"footnote_reference_close\",level:--s.level})),!0))},[\"paragraph\"]],[\"heading\",function heading(s,o,i,a){var u,_,w,x=s.bMarks[o]+s.tShift[o],C=s.eMarks[o];if(x>=C)return!1;if(35!==(u=s.src.charCodeAt(x))||x>=C)return!1;for(_=1,u=s.src.charCodeAt(++x);35===u&&x<C&&_<=6;)_++,u=s.src.charCodeAt(++x);return!(_>6||x<C&&32!==u)&&(a||(C=s.skipCharsBack(C,32,x),(w=s.skipCharsBack(C,35,x))>x&&32===s.src.charCodeAt(w-1)&&(C=w),s.line=o+1,s.tokens.push({type:\"heading_open\",hLevel:_,lines:[o,s.line],level:s.level}),x<C&&s.tokens.push({type:\"inline\",content:s.src.slice(x,C).trim(),level:s.level+1,lines:[o,s.line],children:[]}),s.tokens.push({type:\"heading_close\",hLevel:_,level:s.level})),!0)},[\"paragraph\",\"blockquote\"]],[\"lheading\",function lheading(s,o,i){var a,u,_,w=o+1;return!(w>=i)&&(!(s.tShift[w]<s.blkIndent)&&(!(s.tShift[w]-s.blkIndent>3)&&(!((u=s.bMarks[w]+s.tShift[w])>=(_=s.eMarks[w]))&&((45===(a=s.src.charCodeAt(u))||61===a)&&(u=s.skipChars(u,a),!((u=s.skipSpaces(u))<_)&&(u=s.bMarks[o]+s.tShift[o],s.line=w+1,s.tokens.push({type:\"heading_open\",hLevel:61===a?1:2,lines:[o,s.line],level:s.level}),s.tokens.push({type:\"inline\",content:s.src.slice(u,s.eMarks[o]).trim(),level:s.level+1,lines:[o,s.line-1],children:[]}),s.tokens.push({type:\"heading_close\",hLevel:61===a?1:2,level:s.level}),!0))))))}],[\"htmlblock\",function htmlblock(s,o,i,a){var u,_,w,x=s.bMarks[o],C=s.eMarks[o],j=s.tShift[o];if(x+=j,!s.options.html)return!1;if(j>3||x+2>=C)return!1;if(60!==s.src.charCodeAt(x))return!1;if(33===(u=s.src.charCodeAt(x+1))||63===u){if(a)return!0}else{if(47!==u&&!function isLetter$1(s){var o=32|s;return o>=97&&o<=122}(u))return!1;if(47===u){if(!(_=s.src.slice(x,C).match(kA)))return!1}else if(!(_=s.src.slice(x,C).match(xA)))return!1;if(!0!==wA[_[1].toLowerCase()])return!1;if(a)return!0}for(w=o+1;w<s.lineMax&&!s.isEmpty(w);)w++;return s.line=w,s.tokens.push({type:\"htmlblock\",level:s.level,lines:[o,s.line],content:s.getLines(o,w,0,!0)}),!0},[\"paragraph\",\"blockquote\"]],[\"table\",function table(s,o,i,a){var u,_,w,x,C,j,L,B,$,U,V;if(o+2>i)return!1;if(C=o+1,s.tShift[C]<s.blkIndent)return!1;if((w=s.bMarks[C]+s.tShift[C])>=s.eMarks[C])return!1;if(124!==(u=s.src.charCodeAt(w))&&45!==u&&58!==u)return!1;if(_=index_browser_getLine(s,o+1),!/^[-:| ]+$/.test(_))return!1;if((j=_.split(\"|\"))<=2)return!1;for(B=[],x=0;x<j.length;x++){if(!($=j[x].trim())){if(0===x||x===j.length-1)continue;return!1}if(!/^:?-+:?$/.test($))return!1;58===$.charCodeAt($.length-1)?B.push(58===$.charCodeAt(0)?\"center\":\"right\"):58===$.charCodeAt(0)?B.push(\"left\"):B.push(\"\")}if(-1===(_=index_browser_getLine(s,o).trim()).indexOf(\"|\"))return!1;if(j=_.replace(/^\\||\\|$/g,\"\").split(\"|\"),B.length!==j.length)return!1;if(a)return!0;for(s.tokens.push({type:\"table_open\",lines:U=[o,0],level:s.level++}),s.tokens.push({type:\"thead_open\",lines:[o,o+1],level:s.level++}),s.tokens.push({type:\"tr_open\",lines:[o,o+1],level:s.level++}),x=0;x<j.length;x++)s.tokens.push({type:\"th_open\",align:B[x],lines:[o,o+1],level:s.level++}),s.tokens.push({type:\"inline\",content:j[x].trim(),lines:[o,o+1],level:s.level,children:[]}),s.tokens.push({type:\"th_close\",level:--s.level});for(s.tokens.push({type:\"tr_close\",level:--s.level}),s.tokens.push({type:\"thead_close\",level:--s.level}),s.tokens.push({type:\"tbody_open\",lines:V=[o+2,0],level:s.level++}),C=o+2;C<i&&!(s.tShift[C]<s.blkIndent)&&-1!==(_=index_browser_getLine(s,C).trim()).indexOf(\"|\");C++){for(j=_.replace(/^\\||\\|$/g,\"\").split(\"|\"),s.tokens.push({type:\"tr_open\",level:s.level++}),x=0;x<j.length;x++)s.tokens.push({type:\"td_open\",align:B[x],level:s.level++}),L=j[x].substring(124===j[x].charCodeAt(0)?1:0,124===j[x].charCodeAt(j[x].length-1)?j[x].length-1:j[x].length).trim(),s.tokens.push({type:\"inline\",content:L,level:s.level,children:[]}),s.tokens.push({type:\"td_close\",level:--s.level});s.tokens.push({type:\"tr_close\",level:--s.level})}return s.tokens.push({type:\"tbody_close\",level:--s.level}),s.tokens.push({type:\"table_close\",level:--s.level}),U[1]=V[1]=C,s.line=C,!0},[\"paragraph\"]],[\"deflist\",function deflist(s,o,i,a){var u,_,w,x,C,j,L,B,$,U,V,z,Y,Z;if(a)return!(s.ddIndent<0)&&skipMarker(s,o)>=0;if(L=o+1,s.isEmpty(L)&&++L>i)return!1;if(s.tShift[L]<s.blkIndent)return!1;if((u=skipMarker(s,L))<0)return!1;if(s.level>=s.options.maxNesting)return!1;j=s.tokens.length,s.tokens.push({type:\"dl_open\",lines:C=[o,0],level:s.level++}),w=o,_=L;e:for(;;){for(Z=!0,Y=!1,s.tokens.push({type:\"dt_open\",lines:[w,w],level:s.level++}),s.tokens.push({type:\"inline\",content:s.getLines(w,w+1,s.blkIndent,!1).trim(),level:s.level+1,lines:[w,w],children:[]}),s.tokens.push({type:\"dt_close\",level:--s.level});;){if(s.tokens.push({type:\"dd_open\",lines:x=[L,0],level:s.level++}),z=s.tight,$=s.ddIndent,B=s.blkIndent,V=s.tShift[_],U=s.parentType,s.blkIndent=s.ddIndent=s.tShift[_]+2,s.tShift[_]=u-s.bMarks[_],s.tight=!0,s.parentType=\"deflist\",s.parser.tokenize(s,_,i,!0),s.tight&&!Y||(Z=!1),Y=s.line-_>1&&s.isEmpty(s.line-1),s.tShift[_]=V,s.tight=z,s.parentType=U,s.blkIndent=B,s.ddIndent=$,s.tokens.push({type:\"dd_close\",level:--s.level}),x[1]=L=s.line,L>=i)break e;if(s.tShift[L]<s.blkIndent)break e;if((u=skipMarker(s,L))<0)break;_=L}if(L>=i)break;if(w=L,s.isEmpty(w))break;if(s.tShift[w]<s.blkIndent)break;if((_=w+1)>=i)break;if(s.isEmpty(_)&&_++,_>=i)break;if(s.tShift[_]<s.blkIndent)break;if((u=skipMarker(s,_))<0)break}return s.tokens.push({type:\"dl_close\",level:--s.level}),C[1]=L,s.line=L,Z&&function markTightParagraphs$1(s,o){var i,a,u=s.level+2;for(i=o+2,a=s.tokens.length-2;i<a;i++)s.tokens[i].level===u&&\"paragraph_open\"===s.tokens[i].type&&(s.tokens[i+2].tight=!0,s.tokens[i].tight=!0,i+=2)}(s,j),!0},[\"paragraph\"]],[\"paragraph\",function paragraph(s,o){var i,a,u,_,w,x,C=o+1;if(C<(i=s.lineMax)&&!s.isEmpty(C))for(x=s.parser.ruler.getRules(\"paragraph\");C<i&&!s.isEmpty(C);C++)if(!(s.tShift[C]-s.blkIndent>3)){for(u=!1,_=0,w=x.length;_<w;_++)if(x[_](s,C,i,!0)){u=!0;break}if(u)break}return a=s.getLines(o,C,s.blkIndent,!1).trim(),s.line=C,a.length&&(s.tokens.push({type:\"paragraph_open\",tight:!1,lines:[o,s.line],level:s.level}),s.tokens.push({type:\"inline\",content:a,level:s.level+1,lines:[o,s.line],children:[]}),s.tokens.push({type:\"paragraph_close\",tight:!1,level:s.level})),!0}]];function ParserBlock(){this.ruler=new Ruler;for(var s=0;s<OA.length;s++)this.ruler.push(OA[s][0],OA[s][1],{alt:(OA[s][2]||[]).slice()})}ParserBlock.prototype.tokenize=function(s,o,i){for(var a,u=this.ruler.getRules(\"\"),_=u.length,w=o,x=!1;w<i&&(s.line=w=s.skipEmptyLines(w),!(w>=i))&&!(s.tShift[w]<s.blkIndent);){for(a=0;a<_&&!u[a](s,w,i,!1);a++);if(s.tight=!x,s.isEmpty(s.line-1)&&(x=!0),(w=s.line)<i&&s.isEmpty(w)){if(x=!0,++w<i&&\"list\"===s.parentType&&s.isEmpty(w))break;s.line=w}}};var AA=/[\\n\\t]/g,CA=/\\r[\\n\\u0085]|[\\u2424\\u2028\\u0085]/g,jA=/\\u00a0/g;function isTerminatorChar(s){switch(s){case 10:case 92:case 96:case 42:case 95:case 94:case 91:case 93:case 33:case 38:case 60:case 62:case 123:case 125:case 36:case 37:case 64:case 126:case 43:case 61:case 58:return!0;default:return!1}}ParserBlock.prototype.parse=function(s,o,i,a){var u,_=0,w=0;if(!s)return[];(s=(s=s.replace(jA,\" \")).replace(CA,\"\\n\")).indexOf(\"\\t\")>=0&&(s=s.replace(AA,(function(o,i){var a;return 10===s.charCodeAt(i)?(_=i+1,w=0,o):(a=\"    \".slice((i-_-w)%4),w=i-_+1,a)}))),u=new StateBlock(s,this,o,i,a),this.tokenize(u,u.line,u.lineMax)};for(var PA=[],IA=0;IA<256;IA++)PA.push(0);function isAlphaNum(s){return s>=48&&s<=57||s>=65&&s<=90||s>=97&&s<=122}function scanDelims(s,o){var i,a,u,_=o,w=!0,x=!0,C=s.posMax,j=s.src.charCodeAt(o);for(i=o>0?s.src.charCodeAt(o-1):-1;_<C&&s.src.charCodeAt(_)===j;)_++;return _>=C&&(w=!1),(u=_-o)>=4?w=x=!1:(32!==(a=_<C?s.src.charCodeAt(_):-1)&&10!==a||(w=!1),32!==i&&10!==i||(x=!1),95===j&&(isAlphaNum(i)&&(w=!1),isAlphaNum(a)&&(x=!1))),{can_open:w,can_close:x,delims:u}}\"\\\\!\\\"#$%&'()*+,./:;<=>?@[]^_`{|}~-\".split(\"\").forEach((function(s){PA[s.charCodeAt(0)]=1}));var TA=/\\\\([ \\\\!\"#$%&'()*+,.\\/:;<=>?@[\\]^_`{|}~-])/g;var NA=/\\\\([ \\\\!\"#$%&'()*+,.\\/:;<=>?@[\\]^_`{|}~-])/g;var MA=[\"coap\",\"doi\",\"javascript\",\"aaa\",\"aaas\",\"about\",\"acap\",\"cap\",\"cid\",\"crid\",\"data\",\"dav\",\"dict\",\"dns\",\"file\",\"ftp\",\"geo\",\"go\",\"gopher\",\"h323\",\"http\",\"https\",\"iax\",\"icap\",\"im\",\"imap\",\"info\",\"ipp\",\"iris\",\"iris.beep\",\"iris.xpc\",\"iris.xpcs\",\"iris.lwz\",\"ldap\",\"mailto\",\"mid\",\"msrp\",\"msrps\",\"mtqp\",\"mupdate\",\"news\",\"nfs\",\"ni\",\"nih\",\"nntp\",\"opaquelocktoken\",\"pop\",\"pres\",\"rtsp\",\"service\",\"session\",\"shttp\",\"sieve\",\"sip\",\"sips\",\"sms\",\"snmp\",\"soap.beep\",\"soap.beeps\",\"tag\",\"tel\",\"telnet\",\"tftp\",\"thismessage\",\"tn3270\",\"tip\",\"tv\",\"urn\",\"vemmi\",\"ws\",\"wss\",\"xcon\",\"xcon-userid\",\"xmlrpc.beep\",\"xmlrpc.beeps\",\"xmpp\",\"z39.50r\",\"z39.50s\",\"adiumxtra\",\"afp\",\"afs\",\"aim\",\"apt\",\"attachment\",\"aw\",\"beshare\",\"bitcoin\",\"bolo\",\"callto\",\"chrome\",\"chrome-extension\",\"com-eventbrite-attendee\",\"content\",\"cvs\",\"dlna-playsingle\",\"dlna-playcontainer\",\"dtn\",\"dvb\",\"ed2k\",\"facetime\",\"feed\",\"finger\",\"fish\",\"gg\",\"git\",\"gizmoproject\",\"gtalk\",\"hcp\",\"icon\",\"ipn\",\"irc\",\"irc6\",\"ircs\",\"itms\",\"jar\",\"jms\",\"keyparc\",\"lastfm\",\"ldaps\",\"magnet\",\"maps\",\"market\",\"message\",\"mms\",\"ms-help\",\"msnim\",\"mumble\",\"mvn\",\"notes\",\"oid\",\"palm\",\"paparazzi\",\"platform\",\"proxy\",\"psyc\",\"query\",\"res\",\"resource\",\"rmi\",\"rsync\",\"rtmp\",\"secondlife\",\"sftp\",\"sgn\",\"skype\",\"smb\",\"soldat\",\"spotify\",\"ssh\",\"steam\",\"svn\",\"teamspeak\",\"things\",\"udp\",\"unreal\",\"ut2004\",\"ventrilo\",\"view-source\",\"webcal\",\"wtai\",\"wyciwyg\",\"xfire\",\"xri\",\"ymsgr\"],RA=/^<([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/,DA=/^<([a-zA-Z.\\-]{1,25}):([^<>\\x00-\\x20]*)>/;function replace$1(s,o){return s=s.source,o=o||\"\",function self(i,a){return i?(a=a.source||a,s=s.replace(i,a),self):new RegExp(s,o)}}var LA=replace$1(/(?:unquoted|single_quoted|double_quoted)/)(\"unquoted\",/[^\"'=<>`\\x00-\\x20]+/)(\"single_quoted\",/'[^']*'/)(\"double_quoted\",/\"[^\"]*\"/)(),FA=replace$1(/(?:\\s+attr_name(?:\\s*=\\s*attr_value)?)/)(\"attr_name\",/[a-zA-Z_:][a-zA-Z0-9:._-]*/)(\"attr_value\",LA)(),BA=replace$1(/<[A-Za-z][A-Za-z0-9]*attribute*\\s*\\/?>/)(\"attribute\",FA)(),$A=replace$1(/^(?:open_tag|close_tag|comment|processing|declaration|cdata)/)(\"open_tag\",BA)(\"close_tag\",/<\\/[A-Za-z][A-Za-z0-9]*\\s*>/)(\"comment\",/<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->/)(\"processing\",/<[?].*?[?]>/)(\"declaration\",/<![A-Z]+\\s+[^>]*>/)(\"cdata\",/<!\\[CDATA\\[[\\s\\S]*?\\]\\]>/)();var qA=/^&#((?:x[a-f0-9]{1,8}|[0-9]{1,8}));/i,UA=/^&([a-z][a-z0-9]{1,31});/i;var VA=[[\"text\",function index_browser_text(s,o){for(var i=s.pos;i<s.posMax&&!isTerminatorChar(s.src.charCodeAt(i));)i++;return i!==s.pos&&(o||(s.pending+=s.src.slice(s.pos,i)),s.pos=i,!0)}],[\"newline\",function newline(s,o){var i,a,u=s.pos;if(10!==s.src.charCodeAt(u))return!1;if(i=s.pending.length-1,a=s.posMax,!o)if(i>=0&&32===s.pending.charCodeAt(i))if(i>=1&&32===s.pending.charCodeAt(i-1)){for(var _=i-2;_>=0;_--)if(32!==s.pending.charCodeAt(_)){s.pending=s.pending.substring(0,_+1);break}s.push({type:\"hardbreak\",level:s.level})}else s.pending=s.pending.slice(0,-1),s.push({type:\"softbreak\",level:s.level});else s.push({type:\"softbreak\",level:s.level});for(u++;u<a&&32===s.src.charCodeAt(u);)u++;return s.pos=u,!0}],[\"escape\",function index_browser_escape(s,o){var i,a=s.pos,u=s.posMax;if(92!==s.src.charCodeAt(a))return!1;if(++a<u){if((i=s.src.charCodeAt(a))<256&&0!==PA[i])return o||(s.pending+=s.src[a]),s.pos+=2,!0;if(10===i){for(o||s.push({type:\"hardbreak\",level:s.level}),a++;a<u&&32===s.src.charCodeAt(a);)a++;return s.pos=a,!0}}return o||(s.pending+=\"\\\\\"),s.pos++,!0}],[\"backticks\",function backticks(s,o){var i,a,u,_,w,x=s.pos;if(96!==s.src.charCodeAt(x))return!1;for(i=x,x++,a=s.posMax;x<a&&96===s.src.charCodeAt(x);)x++;for(u=s.src.slice(i,x),_=w=x;-1!==(_=s.src.indexOf(\"`\",w));){for(w=_+1;w<a&&96===s.src.charCodeAt(w);)w++;if(w-_===u.length)return o||s.push({type:\"code\",content:s.src.slice(x,_).replace(/[ \\n]+/g,\" \").trim(),block:!1,level:s.level}),s.pos=w,!0}return o||(s.pending+=u),s.pos+=u.length,!0}],[\"del\",function del(s,o){var i,a,u,_,w,x=s.posMax,C=s.pos;if(126!==s.src.charCodeAt(C))return!1;if(o)return!1;if(C+4>=x)return!1;if(126!==s.src.charCodeAt(C+1))return!1;if(s.level>=s.options.maxNesting)return!1;if(_=C>0?s.src.charCodeAt(C-1):-1,w=s.src.charCodeAt(C+2),126===_)return!1;if(126===w)return!1;if(32===w||10===w)return!1;for(a=C+2;a<x&&126===s.src.charCodeAt(a);)a++;if(a>C+3)return s.pos+=a-C,o||(s.pending+=s.src.slice(C,a)),!0;for(s.pos=C+2,u=1;s.pos+1<x;){if(126===s.src.charCodeAt(s.pos)&&126===s.src.charCodeAt(s.pos+1)&&(_=s.src.charCodeAt(s.pos-1),126!==(w=s.pos+2<x?s.src.charCodeAt(s.pos+2):-1)&&126!==_&&(32!==_&&10!==_?u--:32!==w&&10!==w&&u++,u<=0))){i=!0;break}s.parser.skipToken(s)}return i?(s.posMax=s.pos,s.pos=C+2,o||(s.push({type:\"del_open\",level:s.level++}),s.parser.tokenize(s),s.push({type:\"del_close\",level:--s.level})),s.pos=s.posMax+2,s.posMax=x,!0):(s.pos=C,!1)}],[\"ins\",function ins(s,o){var i,a,u,_,w,x=s.posMax,C=s.pos;if(43!==s.src.charCodeAt(C))return!1;if(o)return!1;if(C+4>=x)return!1;if(43!==s.src.charCodeAt(C+1))return!1;if(s.level>=s.options.maxNesting)return!1;if(_=C>0?s.src.charCodeAt(C-1):-1,w=s.src.charCodeAt(C+2),43===_)return!1;if(43===w)return!1;if(32===w||10===w)return!1;for(a=C+2;a<x&&43===s.src.charCodeAt(a);)a++;if(a!==C+2)return s.pos+=a-C,o||(s.pending+=s.src.slice(C,a)),!0;for(s.pos=C+2,u=1;s.pos+1<x;){if(43===s.src.charCodeAt(s.pos)&&43===s.src.charCodeAt(s.pos+1)&&(_=s.src.charCodeAt(s.pos-1),43!==(w=s.pos+2<x?s.src.charCodeAt(s.pos+2):-1)&&43!==_&&(32!==_&&10!==_?u--:32!==w&&10!==w&&u++,u<=0))){i=!0;break}s.parser.skipToken(s)}return i?(s.posMax=s.pos,s.pos=C+2,o||(s.push({type:\"ins_open\",level:s.level++}),s.parser.tokenize(s),s.push({type:\"ins_close\",level:--s.level})),s.pos=s.posMax+2,s.posMax=x,!0):(s.pos=C,!1)}],[\"mark\",function mark(s,o){var i,a,u,_,w,x=s.posMax,C=s.pos;if(61!==s.src.charCodeAt(C))return!1;if(o)return!1;if(C+4>=x)return!1;if(61!==s.src.charCodeAt(C+1))return!1;if(s.level>=s.options.maxNesting)return!1;if(_=C>0?s.src.charCodeAt(C-1):-1,w=s.src.charCodeAt(C+2),61===_)return!1;if(61===w)return!1;if(32===w||10===w)return!1;for(a=C+2;a<x&&61===s.src.charCodeAt(a);)a++;if(a!==C+2)return s.pos+=a-C,o||(s.pending+=s.src.slice(C,a)),!0;for(s.pos=C+2,u=1;s.pos+1<x;){if(61===s.src.charCodeAt(s.pos)&&61===s.src.charCodeAt(s.pos+1)&&(_=s.src.charCodeAt(s.pos-1),61!==(w=s.pos+2<x?s.src.charCodeAt(s.pos+2):-1)&&61!==_&&(32!==_&&10!==_?u--:32!==w&&10!==w&&u++,u<=0))){i=!0;break}s.parser.skipToken(s)}return i?(s.posMax=s.pos,s.pos=C+2,o||(s.push({type:\"mark_open\",level:s.level++}),s.parser.tokenize(s),s.push({type:\"mark_close\",level:--s.level})),s.pos=s.posMax+2,s.posMax=x,!0):(s.pos=C,!1)}],[\"emphasis\",function emphasis(s,o){var i,a,u,_,w,x,C,j=s.posMax,L=s.pos,B=s.src.charCodeAt(L);if(95!==B&&42!==B)return!1;if(o)return!1;if(i=(C=scanDelims(s,L)).delims,!C.can_open)return s.pos+=i,o||(s.pending+=s.src.slice(L,s.pos)),!0;if(s.level>=s.options.maxNesting)return!1;for(s.pos=L+i,x=[i];s.pos<j;)if(s.src.charCodeAt(s.pos)!==B)s.parser.skipToken(s);else{if(a=(C=scanDelims(s,s.pos)).delims,C.can_close){for(_=x.pop(),w=a;_!==w;){if(w<_){x.push(_-w);break}if(w-=_,0===x.length)break;s.pos+=_,_=x.pop()}if(0===x.length){i=_,u=!0;break}s.pos+=a;continue}C.can_open&&x.push(a),s.pos+=a}return u?(s.posMax=s.pos,s.pos=L+i,o||(2!==i&&3!==i||s.push({type:\"strong_open\",level:s.level++}),1!==i&&3!==i||s.push({type:\"em_open\",level:s.level++}),s.parser.tokenize(s),1!==i&&3!==i||s.push({type:\"em_close\",level:--s.level}),2!==i&&3!==i||s.push({type:\"strong_close\",level:--s.level})),s.pos=s.posMax+i,s.posMax=j,!0):(s.pos=L,!1)}],[\"sub\",function sub(s,o){var i,a,u=s.posMax,_=s.pos;if(126!==s.src.charCodeAt(_))return!1;if(o)return!1;if(_+2>=u)return!1;if(s.level>=s.options.maxNesting)return!1;for(s.pos=_+1;s.pos<u;){if(126===s.src.charCodeAt(s.pos)){i=!0;break}s.parser.skipToken(s)}return i&&_+1!==s.pos?(a=s.src.slice(_+1,s.pos)).match(/(^|[^\\\\])(\\\\\\\\)*\\s/)?(s.pos=_,!1):(s.posMax=s.pos,s.pos=_+1,o||s.push({type:\"sub\",level:s.level,content:a.replace(TA,\"$1\")}),s.pos=s.posMax+1,s.posMax=u,!0):(s.pos=_,!1)}],[\"sup\",function sup(s,o){var i,a,u=s.posMax,_=s.pos;if(94!==s.src.charCodeAt(_))return!1;if(o)return!1;if(_+2>=u)return!1;if(s.level>=s.options.maxNesting)return!1;for(s.pos=_+1;s.pos<u;){if(94===s.src.charCodeAt(s.pos)){i=!0;break}s.parser.skipToken(s)}return i&&_+1!==s.pos?(a=s.src.slice(_+1,s.pos)).match(/(^|[^\\\\])(\\\\\\\\)*\\s/)?(s.pos=_,!1):(s.posMax=s.pos,s.pos=_+1,o||s.push({type:\"sup\",level:s.level,content:a.replace(NA,\"$1\")}),s.pos=s.posMax+1,s.posMax=u,!0):(s.pos=_,!1)}],[\"links\",function links(s,o){var i,a,u,_,w,x,C,j,L=!1,B=s.pos,$=s.posMax,U=s.pos,V=s.src.charCodeAt(U);if(33===V&&(L=!0,V=s.src.charCodeAt(++U)),91!==V)return!1;if(s.level>=s.options.maxNesting)return!1;if(i=U+1,(a=parseLinkLabel(s,U))<0)return!1;if((x=a+1)<$&&40===s.src.charCodeAt(x)){for(x++;x<$&&(32===(j=s.src.charCodeAt(x))||10===j);x++);if(x>=$)return!1;for(U=x,parseLinkDestination(s,x)?(_=s.linkContent,x=s.pos):_=\"\",U=x;x<$&&(32===(j=s.src.charCodeAt(x))||10===j);x++);if(x<$&&U!==x&&parseLinkTitle(s,x))for(w=s.linkContent,x=s.pos;x<$&&(32===(j=s.src.charCodeAt(x))||10===j);x++);else w=\"\";if(x>=$||41!==s.src.charCodeAt(x))return s.pos=B,!1;x++}else{if(s.linkLevel>0)return!1;for(;x<$&&(32===(j=s.src.charCodeAt(x))||10===j);x++);if(x<$&&91===s.src.charCodeAt(x)&&(U=x+1,(x=parseLinkLabel(s,x))>=0?u=s.src.slice(U,x++):x=U-1),u||(void 0===u&&(x=a+1),u=s.src.slice(i,a)),!(C=s.env.references[normalizeReference(u)]))return s.pos=B,!1;_=C.href,w=C.title}return o||(s.pos=i,s.posMax=a,L?s.push({type:\"image\",src:_,title:w,alt:s.src.substr(i,a-i),level:s.level}):(s.push({type:\"link_open\",href:_,title:w,level:s.level++}),s.linkLevel++,s.parser.tokenize(s),s.linkLevel--,s.push({type:\"link_close\",level:--s.level}))),s.pos=x,s.posMax=$,!0}],[\"footnote_inline\",function footnote_inline(s,o){var i,a,u,_,w=s.posMax,x=s.pos;return!(x+2>=w)&&(94===s.src.charCodeAt(x)&&(91===s.src.charCodeAt(x+1)&&(!(s.level>=s.options.maxNesting)&&(i=x+2,!((a=parseLinkLabel(s,x+1))<0)&&(o||(s.env.footnotes||(s.env.footnotes={}),s.env.footnotes.list||(s.env.footnotes.list=[]),u=s.env.footnotes.list.length,s.pos=i,s.posMax=a,s.push({type:\"footnote_ref\",id:u,level:s.level}),s.linkLevel++,_=s.tokens.length,s.parser.tokenize(s),s.env.footnotes.list[u]={tokens:s.tokens.splice(_)},s.linkLevel--),s.pos=a+1,s.posMax=w,!0)))))}],[\"footnote_ref\",function footnote_ref(s,o){var i,a,u,_,w=s.posMax,x=s.pos;if(x+3>w)return!1;if(!s.env.footnotes||!s.env.footnotes.refs)return!1;if(91!==s.src.charCodeAt(x))return!1;if(94!==s.src.charCodeAt(x+1))return!1;if(s.level>=s.options.maxNesting)return!1;for(a=x+2;a<w;a++){if(32===s.src.charCodeAt(a))return!1;if(10===s.src.charCodeAt(a))return!1;if(93===s.src.charCodeAt(a))break}return a!==x+2&&(!(a>=w)&&(a++,i=s.src.slice(x+2,a-1),void 0!==s.env.footnotes.refs[\":\"+i]&&(o||(s.env.footnotes.list||(s.env.footnotes.list=[]),s.env.footnotes.refs[\":\"+i]<0?(u=s.env.footnotes.list.length,s.env.footnotes.list[u]={label:i,count:0},s.env.footnotes.refs[\":\"+i]=u):u=s.env.footnotes.refs[\":\"+i],_=s.env.footnotes.list[u].count,s.env.footnotes.list[u].count++,s.push({type:\"footnote_ref\",id:u,subId:_,level:s.level})),s.pos=a,s.posMax=w,!0)))}],[\"autolink\",function autolink(s,o){var i,a,u,_,w,x=s.pos;return 60===s.src.charCodeAt(x)&&(!((i=s.src.slice(x)).indexOf(\">\")<0)&&((a=i.match(DA))?!(MA.indexOf(a[1].toLowerCase())<0)&&(w=normalizeLink(_=a[0].slice(1,-1)),!!s.parser.validateLink(_)&&(o||(s.push({type:\"link_open\",href:w,level:s.level}),s.push({type:\"text\",content:_,level:s.level+1}),s.push({type:\"link_close\",level:s.level})),s.pos+=a[0].length,!0)):!!(u=i.match(RA))&&(w=normalizeLink(\"mailto:\"+(_=u[0].slice(1,-1))),!!s.parser.validateLink(w)&&(o||(s.push({type:\"link_open\",href:w,level:s.level}),s.push({type:\"text\",content:_,level:s.level+1}),s.push({type:\"link_close\",level:s.level})),s.pos+=u[0].length,!0))))}],[\"htmltag\",function htmltag(s,o){var i,a,u,_=s.pos;return!!s.options.html&&(u=s.posMax,!(60!==s.src.charCodeAt(_)||_+2>=u)&&(!(33!==(i=s.src.charCodeAt(_+1))&&63!==i&&47!==i&&!function isLetter$2(s){var o=32|s;return o>=97&&o<=122}(i))&&(!!(a=s.src.slice(_).match($A))&&(o||s.push({type:\"htmltag\",content:s.src.slice(_,_+a[0].length),level:s.level}),s.pos+=a[0].length,!0))))}],[\"entity\",function entity(s,o){var i,a,u=s.pos,_=s.posMax;if(38!==s.src.charCodeAt(u))return!1;if(u+1<_)if(35===s.src.charCodeAt(u+1)){if(a=s.src.slice(u).match(qA))return o||(i=\"x\"===a[1][0].toLowerCase()?parseInt(a[1].slice(1),16):parseInt(a[1],10),s.pending+=isValidEntityCode(i)?fromCodePoint(i):fromCodePoint(65533)),s.pos+=a[0].length,!0}else if(a=s.src.slice(u).match(UA)){var w=decodeEntity(a[1]);if(a[1]!==w)return o||(s.pending+=w),s.pos+=a[0].length,!0}return o||(s.pending+=\"&\"),s.pos++,!0}]];function ParserInline(){this.ruler=new Ruler;for(var s=0;s<VA.length;s++)this.ruler.push(VA[s][0],VA[s][1]);this.validateLink=validateLink}function validateLink(s){var o=s.trim().toLowerCase();return-1===(o=replaceEntities(o)).indexOf(\":\")||-1===[\"vbscript\",\"javascript\",\"file\",\"data\"].indexOf(o.split(\":\")[0])}ParserInline.prototype.skipToken=function(s){var o,i,a=this.ruler.getRules(\"\"),u=a.length,_=s.pos;if((i=s.cacheGet(_))>0)s.pos=i;else{for(o=0;o<u;o++)if(a[o](s,!0))return void s.cacheSet(_,s.pos);s.pos++,s.cacheSet(_,s.pos)}},ParserInline.prototype.tokenize=function(s){for(var o,i,a=this.ruler.getRules(\"\"),u=a.length,_=s.posMax;s.pos<_;){for(i=0;i<u&&!(o=a[i](s,!1));i++);if(o){if(s.pos>=_)break}else s.pending+=s.src[s.pos++]}s.pending&&s.pushPending()},ParserInline.prototype.parse=function(s,o,i,a){var u=new StateInline(s,this,o,i,a);this.tokenize(u)};var zA={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:\"language-\",linkTarget:\"\",typographer:!1,quotes:\"“”‘’\",highlight:null,maxNesting:20},components:{core:{rules:[\"block\",\"inline\",\"references\",\"replacements\",\"smartquotes\",\"references\",\"abbr2\",\"footnote_tail\"]},block:{rules:[\"blockquote\",\"code\",\"fences\",\"footnote\",\"heading\",\"hr\",\"htmlblock\",\"lheading\",\"list\",\"paragraph\",\"table\"]},inline:{rules:[\"autolink\",\"backticks\",\"del\",\"emphasis\",\"entity\",\"escape\",\"footnote_ref\",\"htmltag\",\"links\",\"newline\",\"text\"]}}},full:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:\"language-\",linkTarget:\"\",typographer:!1,quotes:\"“”‘’\",highlight:null,maxNesting:20},components:{core:{},block:{},inline:{}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:\"language-\",linkTarget:\"\",typographer:!1,quotes:\"“”‘’\",highlight:null,maxNesting:20},components:{core:{rules:[\"block\",\"inline\",\"references\",\"abbr2\"]},block:{rules:[\"blockquote\",\"code\",\"fences\",\"heading\",\"hr\",\"htmlblock\",\"lheading\",\"list\",\"paragraph\"]},inline:{rules:[\"autolink\",\"backticks\",\"emphasis\",\"entity\",\"escape\",\"htmltag\",\"links\",\"newline\",\"text\"]}}}};function StateCore(s,o,i){this.src=o,this.env=i,this.options=s.options,this.tokens=[],this.inlineMode=!1,this.inline=s.inline,this.block=s.block,this.renderer=s.renderer,this.typographer=s.typographer}function Remarkable(s,o){\"string\"!=typeof s&&(o=s,s=\"default\"),o&&null!=o.linkify&&console.warn(\"linkify option is removed. Use linkify plugin instead:\\n\\nimport Remarkable from 'remarkable';\\nimport linkify from 'remarkable/linkify';\\nnew Remarkable().use(linkify)\\n\"),this.inline=new ParserInline,this.block=new ParserBlock,this.core=new Core,this.renderer=new Renderer,this.ruler=new Ruler,this.options={},this.configure(zA[s]),this.set(o||{})}Remarkable.prototype.set=function(s){index_browser_assign(this.options,s)},Remarkable.prototype.configure=function(s){var o=this;if(!s)throw new Error(\"Wrong `remarkable` preset, check name/content\");s.options&&o.set(s.options),s.components&&Object.keys(s.components).forEach((function(i){s.components[i].rules&&o[i].ruler.enable(s.components[i].rules,!0)}))},Remarkable.prototype.use=function(s,o){return s(this,o),this},Remarkable.prototype.parse=function(s,o){var i=new StateCore(this,s,o);return this.core.process(i),i.tokens},Remarkable.prototype.render=function(s,o){return o=o||{},this.renderer.render(this.parse(s,o),this.options,o)},Remarkable.prototype.parseInline=function(s,o){var i=new StateCore(this,s,o);return i.inlineMode=!0,this.core.process(i),i.tokens},Remarkable.prototype.renderInline=function(s,o){return o=o||{},this.renderer.render(this.parseInline(s,o),this.options,o)};function indexOf(s,o){if(Array.prototype.indexOf)return s.indexOf(o);for(var i=0,a=s.length;i<a;i++)if(s[i]===o)return i;return-1}function utils_remove(s,o){for(var i=s.length-1;i>=0;i--)!0===o(s[i])&&s.splice(i,1)}function throwUnhandledCaseError(s){throw new Error(\"Unhandled case for value: '\".concat(s,\"'\"))}var WA=function(){function HtmlTag(s){void 0===s&&(s={}),this.tagName=\"\",this.attrs={},this.innerHTML=\"\",this.whitespaceRegex=/\\s+/,this.tagName=s.tagName||\"\",this.attrs=s.attrs||{},this.innerHTML=s.innerHtml||s.innerHTML||\"\"}return HtmlTag.prototype.setTagName=function(s){return this.tagName=s,this},HtmlTag.prototype.getTagName=function(){return this.tagName||\"\"},HtmlTag.prototype.setAttr=function(s,o){return this.getAttrs()[s]=o,this},HtmlTag.prototype.getAttr=function(s){return this.getAttrs()[s]},HtmlTag.prototype.setAttrs=function(s){return Object.assign(this.getAttrs(),s),this},HtmlTag.prototype.getAttrs=function(){return this.attrs||(this.attrs={})},HtmlTag.prototype.setClass=function(s){return this.setAttr(\"class\",s)},HtmlTag.prototype.addClass=function(s){for(var o,i=this.getClass(),a=this.whitespaceRegex,u=i?i.split(a):[],_=s.split(a);o=_.shift();)-1===indexOf(u,o)&&u.push(o);return this.getAttrs().class=u.join(\" \"),this},HtmlTag.prototype.removeClass=function(s){for(var o,i=this.getClass(),a=this.whitespaceRegex,u=i?i.split(a):[],_=s.split(a);u.length&&(o=_.shift());){var w=indexOf(u,o);-1!==w&&u.splice(w,1)}return this.getAttrs().class=u.join(\" \"),this},HtmlTag.prototype.getClass=function(){return this.getAttrs().class||\"\"},HtmlTag.prototype.hasClass=function(s){return-1!==(\" \"+this.getClass()+\" \").indexOf(\" \"+s+\" \")},HtmlTag.prototype.setInnerHTML=function(s){return this.innerHTML=s,this},HtmlTag.prototype.setInnerHtml=function(s){return this.setInnerHTML(s)},HtmlTag.prototype.getInnerHTML=function(){return this.innerHTML||\"\"},HtmlTag.prototype.getInnerHtml=function(){return this.getInnerHTML()},HtmlTag.prototype.toAnchorString=function(){var s=this.getTagName(),o=this.buildAttrsStr();return[\"<\",s,o=o?\" \"+o:\"\",\">\",this.getInnerHtml(),\"</\",s,\">\"].join(\"\")},HtmlTag.prototype.buildAttrsStr=function(){if(!this.attrs)return\"\";var s=this.getAttrs(),o=[];for(var i in s)s.hasOwnProperty(i)&&o.push(i+'=\"'+s[i]+'\"');return o.join(\" \")},HtmlTag}();var JA=function(){function AnchorTagBuilder(s){void 0===s&&(s={}),this.newWindow=!1,this.truncate={},this.className=\"\",this.newWindow=s.newWindow||!1,this.truncate=s.truncate||{},this.className=s.className||\"\"}return AnchorTagBuilder.prototype.build=function(s){return new WA({tagName:\"a\",attrs:this.createAttrs(s),innerHtml:this.processAnchorText(s.getAnchorText())})},AnchorTagBuilder.prototype.createAttrs=function(s){var o={href:s.getAnchorHref()},i=this.createCssClass(s);return i&&(o.class=i),this.newWindow&&(o.target=\"_blank\",o.rel=\"noopener noreferrer\"),this.truncate&&this.truncate.length&&this.truncate.length<s.getAnchorText().length&&(o.title=s.getAnchorHref()),o},AnchorTagBuilder.prototype.createCssClass=function(s){var o=this.className;if(o){for(var i=[o],a=s.getCssClassSuffixes(),u=0,_=a.length;u<_;u++)i.push(o+\"-\"+a[u]);return i.join(\" \")}return\"\"},AnchorTagBuilder.prototype.processAnchorText=function(s){return s=this.doTruncate(s)},AnchorTagBuilder.prototype.doTruncate=function(s){var o=this.truncate;if(!o||!o.length)return s;var i=o.length,a=o.location;return\"smart\"===a?function truncateSmart(s,o,i){var a,u;null==i?(i=\"&hellip;\",u=3,a=8):(u=i.length,a=i.length);var buildUrl=function(s){var o=\"\";return s.scheme&&s.host&&(o+=s.scheme+\"://\"),s.host&&(o+=s.host),s.path&&(o+=\"/\"+s.path),s.query&&(o+=\"?\"+s.query),s.fragment&&(o+=\"#\"+s.fragment),o},buildSegment=function(s,o){var a=o/2,u=Math.ceil(a),_=-1*Math.floor(a),w=\"\";return _<0&&(w=s.substr(_)),s.substr(0,u)+i+w};if(s.length<=o)return s;var _=o-u,w=function(s){var o={},i=s,a=i.match(/^([a-z]+):\\/\\//i);return a&&(o.scheme=a[1],i=i.substr(a[0].length)),(a=i.match(/^(.*?)(?=(\\?|#|\\/|$))/i))&&(o.host=a[1],i=i.substr(a[0].length)),(a=i.match(/^\\/(.*?)(?=(\\?|#|$))/i))&&(o.path=a[1],i=i.substr(a[0].length)),(a=i.match(/^\\?(.*?)(?=(#|$))/i))&&(o.query=a[1],i=i.substr(a[0].length)),(a=i.match(/^#(.*?)$/i))&&(o.fragment=a[1]),o}(s);if(w.query){var x=w.query.match(/^(.*?)(?=(\\?|\\#))(.*?)$/i);x&&(w.query=w.query.substr(0,x[1].length),s=buildUrl(w))}if(s.length<=o)return s;if(w.host&&(w.host=w.host.replace(/^www\\./,\"\"),s=buildUrl(w)),s.length<=o)return s;var C=\"\";if(w.host&&(C+=w.host),C.length>=_)return w.host.length==o?(w.host.substr(0,o-u)+i).substr(0,_+a):buildSegment(C,_).substr(0,_+a);var j=\"\";if(w.path&&(j+=\"/\"+w.path),w.query&&(j+=\"?\"+w.query),j){if((C+j).length>=_)return(C+j).length==o?(C+j).substr(0,o):(C+buildSegment(j,_-C.length)).substr(0,_+a);C+=j}if(w.fragment){var L=\"#\"+w.fragment;if((C+L).length>=_)return(C+L).length==o?(C+L).substr(0,o):(C+buildSegment(L,_-C.length)).substr(0,_+a);C+=L}if(w.scheme&&w.host){var B=w.scheme+\"://\";if((C+B).length<_)return(B+C).substr(0,o)}if(C.length<=o)return C;var $=\"\";return _>0&&($=C.substr(-1*Math.floor(_/2))),(C.substr(0,Math.ceil(_/2))+i+$).substr(0,_+a)}(s,i):\"middle\"===a?function truncateMiddle(s,o,i){if(s.length<=o)return s;var a,u;null==i?(i=\"&hellip;\",a=8,u=3):(a=i.length,u=i.length);var _=o-u,w=\"\";return _>0&&(w=s.substr(-1*Math.floor(_/2))),(s.substr(0,Math.ceil(_/2))+i+w).substr(0,_+a)}(s,i):function truncateEnd(s,o,i){return function ellipsis(s,o,i){var a;return s.length>o&&(null==i?(i=\"&hellip;\",a=3):a=i.length,s=s.substring(0,o-a)+i),s}(s,o,i)}(s,i)},AnchorTagBuilder}(),HA=function(){function Match(s){this.__jsduckDummyDocProp=null,this.matchedText=\"\",this.offset=0,this.tagBuilder=s.tagBuilder,this.matchedText=s.matchedText,this.offset=s.offset}return Match.prototype.getMatchedText=function(){return this.matchedText},Match.prototype.setOffset=function(s){this.offset=s},Match.prototype.getOffset=function(){return this.offset},Match.prototype.getCssClassSuffixes=function(){return[this.getType()]},Match.prototype.buildTag=function(){return this.tagBuilder.build(this)},Match}(),extendStatics=function(s,o){return extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,o){s.__proto__=o}||function(s,o){for(var i in o)Object.prototype.hasOwnProperty.call(o,i)&&(s[i]=o[i])},extendStatics(s,o)};function tslib_es6_extends(s,o){if(\"function\"!=typeof o&&null!==o)throw new TypeError(\"Class extends value \"+String(o)+\" is not a constructor or null\");function __(){this.constructor=s}extendStatics(s,o),s.prototype=null===o?Object.create(o):(__.prototype=o.prototype,new __)}var __assign=function(){return __assign=Object.assign||function __assign(s){for(var o,i=1,a=arguments.length;i<a;i++)for(var u in o=arguments[i])Object.prototype.hasOwnProperty.call(o,u)&&(s[u]=o[u]);return s},__assign.apply(this,arguments)};Object.create;Object.create;\"function\"==typeof SuppressedError&&SuppressedError;var KA,GA=function(s){function EmailMatch(o){var i=s.call(this,o)||this;return i.email=\"\",i.email=o.email,i}return tslib_es6_extends(EmailMatch,s),EmailMatch.prototype.getType=function(){return\"email\"},EmailMatch.prototype.getEmail=function(){return this.email},EmailMatch.prototype.getAnchorHref=function(){return\"mailto:\"+this.email},EmailMatch.prototype.getAnchorText=function(){return this.email},EmailMatch}(HA),YA=function(s){function HashtagMatch(o){var i=s.call(this,o)||this;return i.serviceName=\"\",i.hashtag=\"\",i.serviceName=o.serviceName,i.hashtag=o.hashtag,i}return tslib_es6_extends(HashtagMatch,s),HashtagMatch.prototype.getType=function(){return\"hashtag\"},HashtagMatch.prototype.getServiceName=function(){return this.serviceName},HashtagMatch.prototype.getHashtag=function(){return this.hashtag},HashtagMatch.prototype.getAnchorHref=function(){var s=this.serviceName,o=this.hashtag;switch(s){case\"twitter\":return\"https://twitter.com/hashtag/\"+o;case\"facebook\":return\"https://www.facebook.com/hashtag/\"+o;case\"instagram\":return\"https://instagram.com/explore/tags/\"+o;case\"tiktok\":return\"https://www.tiktok.com/tag/\"+o;default:throw new Error(\"Unknown service name to point hashtag to: \"+s)}},HashtagMatch.prototype.getAnchorText=function(){return\"#\"+this.hashtag},HashtagMatch}(HA),XA=function(s){function MentionMatch(o){var i=s.call(this,o)||this;return i.serviceName=\"twitter\",i.mention=\"\",i.mention=o.mention,i.serviceName=o.serviceName,i}return tslib_es6_extends(MentionMatch,s),MentionMatch.prototype.getType=function(){return\"mention\"},MentionMatch.prototype.getMention=function(){return this.mention},MentionMatch.prototype.getServiceName=function(){return this.serviceName},MentionMatch.prototype.getAnchorHref=function(){switch(this.serviceName){case\"twitter\":return\"https://twitter.com/\"+this.mention;case\"instagram\":return\"https://instagram.com/\"+this.mention;case\"soundcloud\":return\"https://soundcloud.com/\"+this.mention;case\"tiktok\":return\"https://www.tiktok.com/@\"+this.mention;default:throw new Error(\"Unknown service name to point mention to: \"+this.serviceName)}},MentionMatch.prototype.getAnchorText=function(){return\"@\"+this.mention},MentionMatch.prototype.getCssClassSuffixes=function(){var o=s.prototype.getCssClassSuffixes.call(this),i=this.getServiceName();return i&&o.push(i),o},MentionMatch}(HA),QA=function(s){function PhoneMatch(o){var i=s.call(this,o)||this;return i.number=\"\",i.plusSign=!1,i.number=o.number,i.plusSign=o.plusSign,i}return tslib_es6_extends(PhoneMatch,s),PhoneMatch.prototype.getType=function(){return\"phone\"},PhoneMatch.prototype.getPhoneNumber=function(){return this.number},PhoneMatch.prototype.getNumber=function(){return this.getPhoneNumber()},PhoneMatch.prototype.getAnchorHref=function(){return\"tel:\"+(this.plusSign?\"+\":\"\")+this.number},PhoneMatch.prototype.getAnchorText=function(){return this.matchedText},PhoneMatch}(HA),ZA=function(s){function UrlMatch(o){var i=s.call(this,o)||this;return i.url=\"\",i.urlMatchType=\"scheme\",i.protocolUrlMatch=!1,i.protocolRelativeMatch=!1,i.stripPrefix={scheme:!0,www:!0},i.stripTrailingSlash=!0,i.decodePercentEncoding=!0,i.schemePrefixRegex=/^(https?:\\/\\/)?/i,i.wwwPrefixRegex=/^(https?:\\/\\/)?(www\\.)?/i,i.protocolRelativeRegex=/^\\/\\//,i.protocolPrepended=!1,i.urlMatchType=o.urlMatchType,i.url=o.url,i.protocolUrlMatch=o.protocolUrlMatch,i.protocolRelativeMatch=o.protocolRelativeMatch,i.stripPrefix=o.stripPrefix,i.stripTrailingSlash=o.stripTrailingSlash,i.decodePercentEncoding=o.decodePercentEncoding,i}return tslib_es6_extends(UrlMatch,s),UrlMatch.prototype.getType=function(){return\"url\"},UrlMatch.prototype.getUrlMatchType=function(){return this.urlMatchType},UrlMatch.prototype.getUrl=function(){var s=this.url;return this.protocolRelativeMatch||this.protocolUrlMatch||this.protocolPrepended||(s=this.url=\"http://\"+s,this.protocolPrepended=!0),s},UrlMatch.prototype.getAnchorHref=function(){return this.getUrl().replace(/&amp;/g,\"&\")},UrlMatch.prototype.getAnchorText=function(){var s=this.getMatchedText();return this.protocolRelativeMatch&&(s=this.stripProtocolRelativePrefix(s)),this.stripPrefix.scheme&&(s=this.stripSchemePrefix(s)),this.stripPrefix.www&&(s=this.stripWwwPrefix(s)),this.stripTrailingSlash&&(s=this.removeTrailingSlash(s)),this.decodePercentEncoding&&(s=this.removePercentEncoding(s)),s},UrlMatch.prototype.stripSchemePrefix=function(s){return s.replace(this.schemePrefixRegex,\"\")},UrlMatch.prototype.stripWwwPrefix=function(s){return s.replace(this.wwwPrefixRegex,\"$1\")},UrlMatch.prototype.stripProtocolRelativePrefix=function(s){return s.replace(this.protocolRelativeRegex,\"\")},UrlMatch.prototype.removeTrailingSlash=function(s){return\"/\"===s.charAt(s.length-1)&&(s=s.slice(0,-1)),s},UrlMatch.prototype.removePercentEncoding=function(s){var o=s.replace(/%22/gi,\"&quot;\").replace(/%26/gi,\"&amp;\").replace(/%27/gi,\"&#39;\").replace(/%3C/gi,\"&lt;\").replace(/%3E/gi,\"&gt;\");try{return decodeURIComponent(o)}catch(s){return o}},UrlMatch}(HA),eC=function eC(s){this.__jsduckDummyDocProp=null,this.tagBuilder=s.tagBuilder},tC=/[A-Za-z]/,rC=/[\\d]/,nC=/[\\D]/,sC=/\\s/,oC=/['\"]/,iC=/[\\x00-\\x1F\\x7F]/,aC=/A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0-\\u08B4\\u08B6-\\u08BD\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C60\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C88\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FD5\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7AE\\uA7B0-\\uA7B7\\uA7F7-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB65\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC/.source,cC=aC+/\\u2700-\\u27bf\\udde6-\\uddff\\ud800-\\udbff\\udc00-\\udfff\\ufe0e\\ufe0f\\u0300-\\u036f\\ufe20-\\ufe23\\u20d0-\\u20f0\\ud83c\\udffb-\\udfff\\u200d\\u3299\\u3297\\u303d\\u3030\\u24c2\\ud83c\\udd70-\\udd71\\udd7e-\\udd7f\\udd8e\\udd91-\\udd9a\\udde6-\\uddff\\ude01-\\ude02\\ude1a\\ude2f\\ude32-\\ude3a\\ude50-\\ude51\\u203c\\u2049\\u25aa-\\u25ab\\u25b6\\u25c0\\u25fb-\\u25fe\\u00a9\\u00ae\\u2122\\u2139\\udc04\\u2600-\\u26FF\\u2b05\\u2b06\\u2b07\\u2b1b\\u2b1c\\u2b50\\u2b55\\u231a\\u231b\\u2328\\u23cf\\u23e9-\\u23f3\\u23f8-\\u23fa\\udccf\\u2935\\u2934\\u2190-\\u21ff/.source+/\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07EB-\\u07F3\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0859-\\u085B\\u08D4-\\u08E1\\u08E3-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962\\u0963\\u0981-\\u0983\\u09BC\\u09BE-\\u09C4\\u09C7\\u09C8\\u09CB-\\u09CD\\u09D7\\u09E2\\u09E3\\u0A01-\\u0A03\\u0A3C\\u0A3E-\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A70\\u0A71\\u0A75\\u0A81-\\u0A83\\u0ABC\\u0ABE-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0AE2\\u0AE3\\u0B01-\\u0B03\\u0B3C\\u0B3E-\\u0B44\\u0B47\\u0B48\\u0B4B-\\u0B4D\\u0B56\\u0B57\\u0B62\\u0B63\\u0B82\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD7\\u0C00-\\u0C03\\u0C3E-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C62\\u0C63\\u0C81-\\u0C83\\u0CBC\\u0CBE-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0CE2\\u0CE3\\u0D01-\\u0D03\\u0D3E-\\u0D44\\u0D46-\\u0D48\\u0D4A-\\u0D4D\\u0D57\\u0D62\\u0D63\\u0D82\\u0D83\\u0DCA\\u0DCF-\\u0DD4\\u0DD6\\u0DD8-\\u0DDF\\u0DF2\\u0DF3\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F3E\\u0F3F\\u0F71-\\u0F84\\u0F86\\u0F87\\u0F8D-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102B-\\u103E\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F\\u109A-\\u109D\\u135D-\\u135F\\u1712-\\u1714\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17B4-\\u17D3\\u17DD\\u180B-\\u180D\\u1885\\u1886\\u18A9\\u1920-\\u192B\\u1930-\\u193B\\u1A17-\\u1A1B\\u1A55-\\u1A5E\\u1A60-\\u1A7C\\u1A7F\\u1AB0-\\u1ABE\\u1B00-\\u1B04\\u1B34-\\u1B44\\u1B6B-\\u1B73\\u1B80-\\u1B82\\u1BA1-\\u1BAD\\u1BE6-\\u1BF3\\u1C24-\\u1C37\\u1CD0-\\u1CD2\\u1CD4-\\u1CE8\\u1CED\\u1CF2-\\u1CF4\\u1CF8\\u1CF9\\u1DC0-\\u1DF5\\u1DFB-\\u1DFF\\u20D0-\\u20F0\\u2CEF-\\u2CF1\\u2D7F\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099\\u309A\\uA66F-\\uA672\\uA674-\\uA67D\\uA69E\\uA69F\\uA6F0\\uA6F1\\uA802\\uA806\\uA80B\\uA823-\\uA827\\uA880\\uA881\\uA8B4-\\uA8C5\\uA8E0-\\uA8F1\\uA926-\\uA92D\\uA947-\\uA953\\uA980-\\uA983\\uA9B3-\\uA9C0\\uA9E5\\uAA29-\\uAA36\\uAA43\\uAA4C\\uAA4D\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uAAEB-\\uAAEF\\uAAF5\\uAAF6\\uABE3-\\uABEA\\uABEC\\uABED\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE2F/.source,lC=/0-9\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0BE6-\\u0BEF\\u0C66-\\u0C6F\\u0CE6-\\u0CEF\\u0D66-\\u0D6F\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F29\\u1040-\\u1049\\u1090-\\u1099\\u17E0-\\u17E9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19D9\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\uA620-\\uA629\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19/.source,uC=cC+lC,pC=cC+lC,hC=new RegExp(\"[\".concat(pC,\"]\")),dC=\"(?:[\"+lC+\"]{1,3}\\\\.){3}[\"+lC+\"]{1,3}\",fC=\"[\"+pC+\"](?:[\"+pC+\"\\\\-_]{0,61}[\"+pC+\"])?\",getDomainLabelStr=function(s){return\"(?=(\"+fC+\"))\\\\\"+s},getDomainNameStr=function(s){return\"(?:\"+getDomainLabelStr(s)+\"(?:\\\\.\"+getDomainLabelStr(s+1)+\"){0,126}|\"+dC+\")\"},mC=(new RegExp(\"[\"+pC+\".\\\\-]*[\"+pC+\"\\\\-]\"),hC),gC=/(?:xn--vermgensberatung-pwb|xn--vermgensberater-ctb|xn--clchc0ea0b2g2a9gcd|xn--w4r85el8fhu5dnra|northwesternmutual|travelersinsurance|vermögensberatung|xn--5su34j936bgsg|xn--bck1b9a5dre4c|xn--mgbah1a3hjkrd|xn--mgbai9azgqp6j|xn--mgberp4a5d4ar|xn--xkc2dl3a5ee0h|vermögensberater|xn--fzys8d69uvgm|xn--mgba7c0bbn0a|xn--mgbcpq6gpa1a|xn--xkc2al3hye2a|americanexpress|kerryproperties|sandvikcoromant|xn--i1b6b1a6a2e|xn--kcrx77d1x4a|xn--lgbbat1ad8j|xn--mgba3a4f16a|xn--mgbaakc7dvf|xn--mgbc0a9azcg|xn--nqv7fs00ema|americanfamily|bananarepublic|cancerresearch|cookingchannel|kerrylogistics|weatherchannel|xn--54b7fta0cc|xn--6qq986b3xl|xn--80aqecdr1a|xn--b4w605ferd|xn--fiq228c5hs|xn--h2breg3eve|xn--jlq480n2rg|xn--jlq61u9w7b|xn--mgba3a3ejt|xn--mgbaam7a8h|xn--mgbayh7gpa|xn--mgbbh1a71e|xn--mgbca7dzdo|xn--mgbi4ecexp|xn--mgbx4cd0ab|xn--rvc1e0am3e|international|lifeinsurance|travelchannel|wolterskluwer|xn--cckwcxetd|xn--eckvdtc9d|xn--fpcrj9c3d|xn--fzc2c9e2c|xn--h2brj9c8c|xn--tiq49xqyj|xn--yfro4i67o|xn--ygbi2ammx|construction|lplfinancial|scholarships|versicherung|xn--3e0b707e|xn--45br5cyl|xn--4dbrk0ce|xn--80adxhks|xn--80asehdb|xn--8y0a063a|xn--gckr3f0f|xn--mgb9awbf|xn--mgbab2bd|xn--mgbgu82a|xn--mgbpl2fh|xn--mgbt3dhd|xn--mk1bu44c|xn--ngbc5azd|xn--ngbe9e0a|xn--ogbpf8fl|xn--qcka1pmc|accountants|barclaycard|blackfriday|blockbuster|bridgestone|calvinklein|contractors|creditunion|engineering|enterprises|foodnetwork|investments|kerryhotels|lamborghini|motorcycles|olayangroup|photography|playstation|productions|progressive|redumbrella|williamhill|xn--11b4c3d|xn--1ck2e1b|xn--1qqw23a|xn--2scrj9c|xn--3bst00m|xn--3ds443g|xn--3hcrj9c|xn--42c2d9a|xn--45brj9c|xn--55qw42g|xn--6frz82g|xn--80ao21a|xn--9krt00a|xn--cck2b3b|xn--czr694b|xn--d1acj3b|xn--efvy88h|xn--fct429k|xn--fjq720a|xn--flw351e|xn--g2xx48c|xn--gecrj9c|xn--gk3at1e|xn--h2brj9c|xn--hxt814e|xn--imr513n|xn--j6w193g|xn--jvr189m|xn--kprw13d|xn--kpry57d|xn--mgbbh1a|xn--mgbtx2b|xn--mix891f|xn--nyqy26a|xn--otu796d|xn--pgbs0dh|xn--q9jyb4c|xn--rhqv96g|xn--rovu88b|xn--s9brj9c|xn--ses554g|xn--t60b56a|xn--vuq861b|xn--w4rs40l|xn--xhq521b|xn--zfr164b|சிங்கப்பூர்|accountant|apartments|associates|basketball|bnpparibas|boehringer|capitalone|consulting|creditcard|cuisinella|eurovision|extraspace|foundation|healthcare|immobilien|industries|management|mitsubishi|nextdirect|properties|protection|prudential|realestate|republican|restaurant|schaeffler|tatamotors|technology|university|vlaanderen|volkswagen|xn--30rr7y|xn--3pxu8k|xn--45q11c|xn--4gbrim|xn--55qx5d|xn--5tzm5g|xn--80aswg|xn--90a3ac|xn--9dbq2a|xn--9et52u|xn--c2br7g|xn--cg4bki|xn--czrs0t|xn--czru2d|xn--fiq64b|xn--fiqs8s|xn--fiqz9s|xn--io0a7i|xn--kput3i|xn--mxtq1m|xn--o3cw4h|xn--pssy2u|xn--q7ce6a|xn--unup4y|xn--wgbh1c|xn--wgbl6a|xn--y9a3aq|accenture|alfaromeo|allfinanz|amsterdam|analytics|aquarelle|barcelona|bloomberg|christmas|community|directory|education|equipment|fairwinds|financial|firestone|fresenius|frontdoor|furniture|goldpoint|hisamitsu|homedepot|homegoods|homesense|institute|insurance|kuokgroup|lancaster|landrover|lifestyle|marketing|marshalls|melbourne|microsoft|panasonic|passagens|pramerica|richardli|shangrila|solutions|statebank|statefarm|stockholm|travelers|vacations|xn--90ais|xn--c1avg|xn--d1alf|xn--e1a4c|xn--fhbei|xn--j1aef|xn--j1amh|xn--l1acc|xn--ngbrx|xn--nqv7f|xn--p1acf|xn--qxa6a|xn--tckwe|xn--vhquv|yodobashi|موريتانيا|abudhabi|airforce|allstate|attorney|barclays|barefoot|bargains|baseball|boutique|bradesco|broadway|brussels|builders|business|capetown|catering|catholic|cipriani|cityeats|cleaning|clinique|clothing|commbank|computer|delivery|deloitte|democrat|diamonds|discount|discover|download|engineer|ericsson|etisalat|exchange|feedback|fidelity|firmdale|football|frontier|goodyear|grainger|graphics|guardian|hdfcbank|helsinki|holdings|hospital|infiniti|ipiranga|istanbul|jpmorgan|lighting|lundbeck|marriott|maserati|mckinsey|memorial|merckmsd|mortgage|observer|partners|pharmacy|pictures|plumbing|property|redstone|reliance|saarland|samsclub|security|services|shopping|showtime|softbank|software|stcgroup|supplies|training|vanguard|ventures|verisign|woodside|xn--90ae|xn--node|xn--p1ai|xn--qxam|yokohama|السعودية|abogado|academy|agakhan|alibaba|android|athleta|auction|audible|auspost|avianca|banamex|bauhaus|bentley|bestbuy|booking|brother|bugatti|capital|caravan|careers|channel|charity|chintai|citadel|clubmed|college|cologne|comcast|company|compare|contact|cooking|corsica|country|coupons|courses|cricket|cruises|dentist|digital|domains|exposed|express|farmers|fashion|ferrari|ferrero|finance|fishing|fitness|flights|florist|flowers|forsale|frogans|fujitsu|gallery|genting|godaddy|grocery|guitars|hamburg|hangout|hitachi|holiday|hosting|hoteles|hotmail|hyundai|ismaili|jewelry|juniper|kitchen|komatsu|lacaixa|lanxess|lasalle|latrobe|leclerc|limited|lincoln|markets|monster|netbank|netflix|network|neustar|okinawa|oldnavy|organic|origins|philips|pioneer|politie|realtor|recipes|rentals|reviews|rexroth|samsung|sandvik|schmidt|schwarz|science|shiksha|singles|staples|storage|support|surgery|systems|temasek|theater|theatre|tickets|tiffany|toshiba|trading|walmart|wanggou|watches|weather|website|wedding|whoswho|windows|winners|xfinity|yamaxun|youtube|zuerich|католик|اتصالات|البحرين|الجزائر|العليان|پاکستان|كاثوليك|இந்தியா|abarth|abbott|abbvie|africa|agency|airbus|airtel|alipay|alsace|alstom|amazon|anquan|aramco|author|bayern|beauty|berlin|bharti|bostik|boston|broker|camera|career|casino|center|chanel|chrome|church|circle|claims|clinic|coffee|comsec|condos|coupon|credit|cruise|dating|datsun|dealer|degree|dental|design|direct|doctor|dunlop|dupont|durban|emerck|energy|estate|events|expert|family|flickr|futbol|gallup|garden|george|giving|global|google|gratis|health|hermes|hiphop|hockey|hotels|hughes|imamat|insure|intuit|jaguar|joburg|juegos|kaufen|kinder|kindle|kosher|lancia|latino|lawyer|lefrak|living|locker|london|luxury|madrid|maison|makeup|market|mattel|mobile|monash|mormon|moscow|museum|mutual|nagoya|natura|nissan|nissay|norton|nowruz|office|olayan|online|oracle|orange|otsuka|pfizer|photos|physio|pictet|quebec|racing|realty|reisen|repair|report|review|rocher|rogers|ryukyu|safety|sakura|sanofi|school|schule|search|secure|select|shouji|soccer|social|stream|studio|supply|suzuki|swatch|sydney|taipei|taobao|target|tattoo|tennis|tienda|tjmaxx|tkmaxx|toyota|travel|unicom|viajes|viking|villas|virgin|vision|voting|voyage|vuelos|walter|webcam|xihuan|yachts|yandex|zappos|москва|онлайн|ابوظبي|ارامكو|الاردن|المغرب|امارات|فلسطين|مليسيا|भारतम्|இலங்கை|ファッション|actor|adult|aetna|amfam|amica|apple|archi|audio|autos|azure|baidu|beats|bible|bingo|black|boats|bosch|build|canon|cards|chase|cheap|cisco|citic|click|cloud|coach|codes|crown|cymru|dabur|dance|deals|delta|drive|dubai|earth|edeka|email|epson|faith|fedex|final|forex|forum|gallo|games|gifts|gives|glass|globo|gmail|green|gripe|group|gucci|guide|homes|honda|horse|house|hyatt|ikano|irish|jetzt|koeln|kyoto|lamer|lease|legal|lexus|lilly|linde|lipsy|loans|locus|lotte|lotto|macys|mango|media|miami|money|movie|music|nexus|nikon|ninja|nokia|nowtv|omega|osaka|paris|parts|party|phone|photo|pizza|place|poker|praxi|press|prime|promo|quest|radio|rehab|reise|ricoh|rocks|rodeo|rugby|salon|sener|seven|sharp|shell|shoes|skype|sling|smart|smile|solar|space|sport|stada|store|study|style|sucks|swiss|tatar|tires|tirol|tmall|today|tokyo|tools|toray|total|tours|trade|trust|tunes|tushu|ubank|vegas|video|vodka|volvo|wales|watch|weber|weibo|works|world|xerox|yahoo|ישראל|ایران|بازار|بھارت|سودان|سورية|همراه|भारोत|संगठन|বাংলা|భారత్|ഭാരതം|嘉里大酒店|aarp|able|adac|aero|akdn|ally|amex|arab|army|arpa|arte|asda|asia|audi|auto|baby|band|bank|bbva|beer|best|bike|bing|blog|blue|bofa|bond|book|buzz|cafe|call|camp|care|cars|casa|case|cash|cbre|cern|chat|citi|city|club|cool|coop|cyou|data|date|dclk|deal|dell|desi|diet|dish|docs|dvag|erni|fage|fail|fans|farm|fast|fiat|fido|film|fire|fish|flir|food|ford|free|fund|game|gbiz|gent|ggee|gift|gmbh|gold|golf|goog|guge|guru|hair|haus|hdfc|help|here|hgtv|host|hsbc|icbc|ieee|imdb|immo|info|itau|java|jeep|jobs|jprs|kddi|kids|kiwi|kpmg|kred|land|lego|lgbt|lidl|life|like|limo|link|live|loan|loft|love|ltda|luxe|maif|meet|meme|menu|mini|mint|mobi|moda|moto|name|navy|news|next|nico|nike|ollo|open|page|pars|pccw|pics|ping|pink|play|plus|pohl|porn|post|prod|prof|qpon|read|reit|rent|rest|rich|room|rsvp|ruhr|safe|sale|sarl|save|saxo|scot|seat|seek|sexy|shaw|shia|shop|show|silk|sina|site|skin|sncf|sohu|song|sony|spot|star|surf|talk|taxi|team|tech|teva|tiaa|tips|town|toys|tube|vana|visa|viva|vivo|vote|voto|wang|weir|wien|wiki|wine|work|xbox|yoga|zara|zero|zone|дети|сайт|بارت|بيتك|ڀارت|تونس|شبكة|عراق|عمان|موقع|भारत|ভারত|ভাৰত|ਭਾਰਤ|ભારત|ଭାରତ|ಭಾರತ|ලංකා|アマゾン|グーグル|クラウド|ポイント|组织机构|電訊盈科|香格里拉|aaa|abb|abc|aco|ads|aeg|afl|aig|anz|aol|app|art|aws|axa|bar|bbc|bbt|bcg|bcn|bet|bid|bio|biz|bms|bmw|bom|boo|bot|box|buy|bzh|cab|cal|cam|car|cat|cba|cbn|cbs|ceo|cfa|cfd|com|cpa|crs|dad|day|dds|dev|dhl|diy|dnp|dog|dot|dtv|dvr|eat|eco|edu|esq|eus|fan|fit|fly|foo|fox|frl|ftr|fun|fyi|gal|gap|gay|gdn|gea|gle|gmo|gmx|goo|gop|got|gov|hbo|hiv|hkt|hot|how|ibm|ice|icu|ifm|inc|ing|ink|int|ist|itv|jcb|jio|jll|jmp|jnj|jot|joy|kfh|kia|kim|kpn|krd|lat|law|lds|llc|llp|lol|lpl|ltd|man|map|mba|med|men|mil|mit|mlb|mls|mma|moe|moi|mom|mov|msd|mtn|mtr|nab|nba|nec|net|new|nfl|ngo|nhk|now|nra|nrw|ntt|nyc|obi|one|ong|onl|ooo|org|ott|ovh|pay|pet|phd|pid|pin|pnc|pro|pru|pub|pwc|red|ren|ril|rio|rip|run|rwe|sap|sas|sbi|sbs|sca|scb|ses|sew|sex|sfr|ski|sky|soy|spa|srl|stc|tab|tax|tci|tdk|tel|thd|tjx|top|trv|tui|tvs|ubs|uno|uol|ups|vet|vig|vin|vip|wed|win|wme|wow|wtc|wtf|xin|xxx|xyz|you|yun|zip|бел|ком|қаз|мкд|мон|орг|рус|срб|укр|հայ|קום|عرب|قطر|كوم|مصر|कॉम|नेट|คอม|ไทย|ລາວ|ストア|セール|みんな|中文网|亚马逊|天主教|我爱你|新加坡|淡马锡|诺基亚|飞利浦|ac|ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw|ελ|ευ|бг|ею|рф|გე|닷넷|닷컴|삼성|한국|コム|世界|中信|中国|中國|企业|佛山|信息|健康|八卦|公司|公益|台湾|台灣|商城|商店|商标|嘉里|在线|大拿|娱乐|家電|广东|微博|慈善|手机|招聘|政务|政府|新闻|时尚|書籍|机构|游戏|澳門|点看|移动|网址|网店|网站|网络|联通|谷歌|购物|通販|集团|食品|餐厅|香港)/,yC=new RegExp(\"[\".concat(pC,\"!#$%&'*+/=?^_`{|}~-]\")),vC=new RegExp(\"^\".concat(gC.source,\"$\")),bC=function(s){function EmailMatcher(){var o=null!==s&&s.apply(this,arguments)||this;return o.localPartCharRegex=yC,o.strictTldRegex=vC,o}return tslib_es6_extends(EmailMatcher,s),EmailMatcher.prototype.parseMatches=function(s){for(var o=this.tagBuilder,i=this.localPartCharRegex,a=this.strictTldRegex,u=[],_=s.length,w=new _C,x={m:\"a\",a:\"i\",i:\"l\",l:\"t\",t:\"o\",o:\":\"},C=0,j=0,L=w;C<_;){var B=s.charAt(C);switch(j){case 0:stateNonEmailAddress(B);break;case 1:stateMailTo(s.charAt(C-1),B);break;case 2:stateLocalPart(B);break;case 3:stateLocalPartDot(B);break;case 4:stateAtSign(B);break;case 5:stateDomainChar(B);break;case 6:stateDomainHyphen(B);break;case 7:stateDomainDot(B);break;default:throwUnhandledCaseError(j)}C++}return captureMatchIfValidAndReset(),u;function stateNonEmailAddress(s){\"m\"===s?beginEmailMatch(1):i.test(s)&&beginEmailMatch()}function stateMailTo(s,o){\":\"===s?i.test(o)?(j=2,L=new _C(__assign(__assign({},L),{hasMailtoPrefix:!0}))):resetToNonEmailMatchState():x[s]===o||(i.test(o)?j=2:\".\"===o?j=3:\"@\"===o?j=4:resetToNonEmailMatchState())}function stateLocalPart(s){\".\"===s?j=3:\"@\"===s?j=4:i.test(s)||resetToNonEmailMatchState()}function stateLocalPartDot(s){\".\"===s||\"@\"===s?resetToNonEmailMatchState():i.test(s)?j=2:resetToNonEmailMatchState()}function stateAtSign(s){mC.test(s)?j=5:resetToNonEmailMatchState()}function stateDomainChar(s){\".\"===s?j=7:\"-\"===s?j=6:mC.test(s)||captureMatchIfValidAndReset()}function stateDomainHyphen(s){\"-\"===s||\".\"===s?captureMatchIfValidAndReset():mC.test(s)?j=5:captureMatchIfValidAndReset()}function stateDomainDot(s){\".\"===s||\"-\"===s?captureMatchIfValidAndReset():mC.test(s)?(j=5,L=new _C(__assign(__assign({},L),{hasDomainDot:!0}))):captureMatchIfValidAndReset()}function beginEmailMatch(s){void 0===s&&(s=2),j=s,L=new _C({idx:C})}function resetToNonEmailMatchState(){j=0,L=w}function captureMatchIfValidAndReset(){if(L.hasDomainDot){var i=s.slice(L.idx,C);/[-.]$/.test(i)&&(i=i.slice(0,-1));var _=L.hasMailtoPrefix?i.slice(7):i;(function doesEmailHaveValidTld(s){var o=s.split(\".\").pop()||\"\",i=o.toLowerCase();return a.test(i)})(_)&&u.push(new GA({tagBuilder:o,matchedText:i,offset:L.idx,email:_}))}resetToNonEmailMatchState()}},EmailMatcher}(eC),_C=function _C(s){void 0===s&&(s={}),this.idx=void 0!==s.idx?s.idx:-1,this.hasMailtoPrefix=!!s.hasMailtoPrefix,this.hasDomainDot=!!s.hasDomainDot},SC=function(){function UrlMatchValidator(){}return UrlMatchValidator.isValid=function(s,o){return!(o&&!this.isValidUriScheme(o)||this.urlMatchDoesNotHaveProtocolOrDot(s,o)||this.urlMatchDoesNotHaveAtLeastOneWordChar(s,o)&&!this.isValidIpAddress(s)||this.containsMultipleDots(s))},UrlMatchValidator.isValidIpAddress=function(s){var o=new RegExp(this.hasFullProtocolRegex.source+this.ipRegex.source);return null!==s.match(o)},UrlMatchValidator.containsMultipleDots=function(s){var o=s;return this.hasFullProtocolRegex.test(s)&&(o=s.split(\"://\")[1]),o.split(\"/\")[0].indexOf(\"..\")>-1},UrlMatchValidator.isValidUriScheme=function(s){var o=s.match(this.uriSchemeRegex),i=o&&o[0].toLowerCase();return\"javascript:\"!==i&&\"vbscript:\"!==i},UrlMatchValidator.urlMatchDoesNotHaveProtocolOrDot=function(s,o){return!(!s||o&&this.hasFullProtocolRegex.test(o)||-1!==s.indexOf(\".\"))},UrlMatchValidator.urlMatchDoesNotHaveAtLeastOneWordChar=function(s,o){return!(!s||!o)&&(!this.hasFullProtocolRegex.test(o)&&!this.hasWordCharAfterProtocolRegex.test(s))},UrlMatchValidator.hasFullProtocolRegex=/^[A-Za-z][-.+A-Za-z0-9]*:\\/\\//,UrlMatchValidator.uriSchemeRegex=/^[A-Za-z][-.+A-Za-z0-9]*:/,UrlMatchValidator.hasWordCharAfterProtocolRegex=new RegExp(\":[^\\\\s]*?[\"+aC+\"]\"),UrlMatchValidator.ipRegex=/[0-9][0-9]?[0-9]?\\.[0-9][0-9]?[0-9]?\\.[0-9][0-9]?[0-9]?\\.[0-9][0-9]?[0-9]?(:[0-9]*)?\\/?$/,UrlMatchValidator}(),EC=(KA=new RegExp(\"[/?#](?:[\"+pC+\"\\\\-+&@#/%=~_()|'$*\\\\[\\\\]{}?!:,.;^✓]*[\"+pC+\"\\\\-+&@#/%=~_()|'$*\\\\[\\\\]{}✓])?\"),new RegExp([\"(?:\",\"(\",/(?:[A-Za-z][-.+A-Za-z0-9]{0,63}:(?![A-Za-z][-.+A-Za-z0-9]{0,63}:\\/\\/)(?!\\d+\\/?)(?:\\/\\/)?)/.source,getDomainNameStr(2),\")\",\"|\",\"(\",\"(//)?\",/(?:www\\.)/.source,getDomainNameStr(6),\")\",\"|\",\"(\",\"(//)?\",getDomainNameStr(10)+\"\\\\.\",gC.source,\"(?![-\"+uC+\"])\",\")\",\")\",\"(?::[0-9]+)?\",\"(?:\"+KA.source+\")?\"].join(\"\"),\"gi\")),wC=new RegExp(\"[\"+pC+\"]\"),xC=function(s){function UrlMatcher(o){var i=s.call(this,o)||this;return i.stripPrefix={scheme:!0,www:!0},i.stripTrailingSlash=!0,i.decodePercentEncoding=!0,i.matcherRegex=EC,i.wordCharRegExp=wC,i.stripPrefix=o.stripPrefix,i.stripTrailingSlash=o.stripTrailingSlash,i.decodePercentEncoding=o.decodePercentEncoding,i}return tslib_es6_extends(UrlMatcher,s),UrlMatcher.prototype.parseMatches=function(s){for(var o,i=this.matcherRegex,a=this.stripPrefix,u=this.stripTrailingSlash,_=this.decodePercentEncoding,w=this.tagBuilder,x=[],_loop_1=function(){var i=o[0],j=o[1],L=o[4],B=o[5],$=o[9],U=o.index,V=B||$,z=s.charAt(U-1);if(!SC.isValid(i,j))return\"continue\";if(U>0&&\"@\"===z)return\"continue\";if(U>0&&V&&C.wordCharRegExp.test(z))return\"continue\";if(/\\?$/.test(i)&&(i=i.substr(0,i.length-1)),C.matchHasUnbalancedClosingParen(i))i=i.substr(0,i.length-1);else{var Y=C.matchHasInvalidCharAfterTld(i,j);Y>-1&&(i=i.substr(0,Y))}var Z=[\"http://\",\"https://\"].find((function(s){return!!j&&-1!==j.indexOf(s)}));if(Z){var ee=i.indexOf(Z);i=i.substr(ee),j=j.substr(ee),U+=ee}var ie=j?\"scheme\":L?\"www\":\"tld\",ae=!!j;x.push(new ZA({tagBuilder:w,matchedText:i,offset:U,urlMatchType:ie,url:i,protocolUrlMatch:ae,protocolRelativeMatch:!!V,stripPrefix:a,stripTrailingSlash:u,decodePercentEncoding:_}))},C=this;null!==(o=i.exec(s));)_loop_1();return x},UrlMatcher.prototype.matchHasUnbalancedClosingParen=function(s){var o,i=s.charAt(s.length-1);if(\")\"===i)o=\"(\";else if(\"]\"===i)o=\"[\";else{if(\"}\"!==i)return!1;o=\"{\"}for(var a=0,u=0,_=s.length-1;u<_;u++){var w=s.charAt(u);w===o?a++:w===i&&(a=Math.max(a-1,0))}return 0===a},UrlMatcher.prototype.matchHasInvalidCharAfterTld=function(s,o){if(!s)return-1;var i=0;o&&(i=s.indexOf(\":\"),s=s.slice(i));var a=new RegExp(\"^((.?//)?[-.\"+pC+\"]*[-\"+pC+\"]\\\\.[-\"+pC+\"]+)\").exec(s);return null===a?-1:(i+=a[1].length,s=s.slice(a[1].length),/^[^-.A-Za-z0-9:\\/?#]/.test(s)?i:-1)},UrlMatcher}(eC),kC=new RegExp(\"[_\".concat(pC,\"]\")),OC=function(s){function HashtagMatcher(o){var i=s.call(this,o)||this;return i.serviceName=\"twitter\",i.serviceName=o.serviceName,i}return tslib_es6_extends(HashtagMatcher,s),HashtagMatcher.prototype.parseMatches=function(s){for(var o=this.tagBuilder,i=this.serviceName,a=[],u=s.length,_=0,w=-1,x=0;_<u;){var C=s.charAt(_);switch(x){case 0:stateNone(C);break;case 1:stateNonHashtagWordChar(C);break;case 2:stateHashtagHashChar(C);break;case 3:stateHashtagTextChar(C);break;default:throwUnhandledCaseError(x)}_++}return captureMatchIfValid(),a;function stateNone(s){\"#\"===s?(x=2,w=_):hC.test(s)&&(x=1)}function stateNonHashtagWordChar(s){hC.test(s)||(x=0)}function stateHashtagHashChar(s){x=kC.test(s)?3:hC.test(s)?1:0}function stateHashtagTextChar(s){kC.test(s)||(captureMatchIfValid(),w=-1,x=hC.test(s)?1:0)}function captureMatchIfValid(){if(w>-1&&_-w<=140){var u=s.slice(w,_),x=new YA({tagBuilder:o,matchedText:u,offset:w,serviceName:i,hashtag:u.slice(1)});a.push(x)}}},HashtagMatcher}(eC),AC=[\"twitter\",\"facebook\",\"instagram\",\"tiktok\"],CC=new RegExp(\"\".concat(/(?:(?:(?:(\\+)?\\d{1,3}[-\\040.]?)?\\(?\\d{3}\\)?[-\\040.]?\\d{3}[-\\040.]?\\d{4})|(?:(\\+)(?:9[976]\\d|8[987530]\\d|6[987]\\d|5[90]\\d|42\\d|3[875]\\d|2[98654321]\\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)[-\\040.]?(?:\\d[-\\040.]?){6,12}\\d+))([,;]+[0-9]+#?)*/.source,\"|\").concat(/(0([1-9]{1}-?[1-9]\\d{3}|[1-9]{2}-?\\d{3}|[1-9]{2}\\d{1}-?\\d{2}|[1-9]{2}\\d{2}-?\\d{1})-?\\d{4}|0[789]0-?\\d{4}-?\\d{4}|050-?\\d{4}-?\\d{4})/.source),\"g\"),jC=function(s){function PhoneMatcher(){var o=null!==s&&s.apply(this,arguments)||this;return o.matcherRegex=CC,o}return tslib_es6_extends(PhoneMatcher,s),PhoneMatcher.prototype.parseMatches=function(s){for(var o,i=this.matcherRegex,a=this.tagBuilder,u=[];null!==(o=i.exec(s));){var _=o[0],w=_.replace(/[^0-9,;#]/g,\"\"),x=!(!o[1]&&!o[2]),C=0==o.index?\"\":s.substr(o.index-1,1),j=s.substr(o.index+_.length,1),L=!C.match(/\\d/)&&!j.match(/\\d/);this.testMatch(o[3])&&this.testMatch(_)&&L&&u.push(new QA({tagBuilder:a,matchedText:_,offset:o.index,number:w,plusSign:x}))}return u},PhoneMatcher.prototype.testMatch=function(s){return nC.test(s)},PhoneMatcher}(eC),PC=new RegExp(\"@[_\".concat(pC,\"]{1,50}(?![_\").concat(pC,\"])\"),\"g\"),IC=new RegExp(\"@[_.\".concat(pC,\"]{1,30}(?![_\").concat(pC,\"])\"),\"g\"),TC=new RegExp(\"@[-_.\".concat(pC,\"]{1,50}(?![-_\").concat(pC,\"])\"),\"g\"),NC=new RegExp(\"@[_.\".concat(pC,\"]{1,23}[_\").concat(pC,\"](?![_\").concat(pC,\"])\"),\"g\"),MC=new RegExp(\"[^\"+pC+\"]\"),RC=function(s){function MentionMatcher(o){var i=s.call(this,o)||this;return i.serviceName=\"twitter\",i.matcherRegexes={twitter:PC,instagram:IC,soundcloud:TC,tiktok:NC},i.nonWordCharRegex=MC,i.serviceName=o.serviceName,i}return tslib_es6_extends(MentionMatcher,s),MentionMatcher.prototype.parseMatches=function(s){var o,i=this.serviceName,a=this.matcherRegexes[this.serviceName],u=this.nonWordCharRegex,_=this.tagBuilder,w=[];if(!a)return w;for(;null!==(o=a.exec(s));){var x=o.index,C=s.charAt(x-1);if(0===x||u.test(C)){var j=o[0].replace(/\\.+$/g,\"\"),L=j.slice(1);w.push(new XA({tagBuilder:_,matchedText:j,offset:x,serviceName:i,mention:L}))}}return w},MentionMatcher}(eC);function parseHtml(s,o){for(var i=o.onOpenTag,a=o.onCloseTag,u=o.onText,_=o.onComment,w=o.onDoctype,x=new DC,C=0,j=s.length,L=0,B=0,$=x;C<j;){var U=s.charAt(C);switch(L){case 0:stateData(U);break;case 1:stateTagOpen(U);break;case 2:stateEndTagOpen(U);break;case 3:stateTagName(U);break;case 4:stateBeforeAttributeName(U);break;case 5:stateAttributeName(U);break;case 6:stateAfterAttributeName(U);break;case 7:stateBeforeAttributeValue(U);break;case 8:stateAttributeValueDoubleQuoted(U);break;case 9:stateAttributeValueSingleQuoted(U);break;case 10:stateAttributeValueUnquoted(U);break;case 11:stateAfterAttributeValueQuoted(U);break;case 12:stateSelfClosingStartTag(U);break;case 13:stateMarkupDeclarationOpen(U);break;case 14:stateCommentStart(U);break;case 15:stateCommentStartDash(U);break;case 16:stateComment(U);break;case 17:stateCommentEndDash(U);break;case 18:stateCommentEnd(U);break;case 19:stateCommentEndBang(U);break;case 20:stateDoctype(U);break;default:throwUnhandledCaseError(L)}C++}function stateData(s){\"<\"===s&&startNewTag()}function stateTagOpen(s){\"!\"===s?L=13:\"/\"===s?(L=2,$=new DC(__assign(__assign({},$),{isClosing:!0}))):\"<\"===s?startNewTag():tC.test(s)?(L=3,$=new DC(__assign(__assign({},$),{isOpening:!0}))):(L=0,$=x)}function stateTagName(s){sC.test(s)?($=new DC(__assign(__assign({},$),{name:captureTagName()})),L=4):\"<\"===s?startNewTag():\"/\"===s?($=new DC(__assign(__assign({},$),{name:captureTagName()})),L=12):\">\"===s?($=new DC(__assign(__assign({},$),{name:captureTagName()})),emitTagAndPreviousTextNode()):tC.test(s)||rC.test(s)||\":\"===s||resetToDataState()}function stateEndTagOpen(s){\">\"===s?resetToDataState():tC.test(s)?L=3:resetToDataState()}function stateBeforeAttributeName(s){sC.test(s)||(\"/\"===s?L=12:\">\"===s?emitTagAndPreviousTextNode():\"<\"===s?startNewTag():\"=\"===s||oC.test(s)||iC.test(s)?resetToDataState():L=5)}function stateAttributeName(s){sC.test(s)?L=6:\"/\"===s?L=12:\"=\"===s?L=7:\">\"===s?emitTagAndPreviousTextNode():\"<\"===s?startNewTag():oC.test(s)&&resetToDataState()}function stateAfterAttributeName(s){sC.test(s)||(\"/\"===s?L=12:\"=\"===s?L=7:\">\"===s?emitTagAndPreviousTextNode():\"<\"===s?startNewTag():oC.test(s)?resetToDataState():L=5)}function stateBeforeAttributeValue(s){sC.test(s)||('\"'===s?L=8:\"'\"===s?L=9:/[>=`]/.test(s)?resetToDataState():\"<\"===s?startNewTag():L=10)}function stateAttributeValueDoubleQuoted(s){'\"'===s&&(L=11)}function stateAttributeValueSingleQuoted(s){\"'\"===s&&(L=11)}function stateAttributeValueUnquoted(s){sC.test(s)?L=4:\">\"===s?emitTagAndPreviousTextNode():\"<\"===s&&startNewTag()}function stateAfterAttributeValueQuoted(s){sC.test(s)?L=4:\"/\"===s?L=12:\">\"===s?emitTagAndPreviousTextNode():\"<\"===s?startNewTag():(L=4,function reconsumeCurrentCharacter(){C--}())}function stateSelfClosingStartTag(s){\">\"===s?($=new DC(__assign(__assign({},$),{isClosing:!0})),emitTagAndPreviousTextNode()):L=4}function stateMarkupDeclarationOpen(o){\"--\"===s.substr(C,2)?(C+=2,$=new DC(__assign(__assign({},$),{type:\"comment\"})),L=14):\"DOCTYPE\"===s.substr(C,7).toUpperCase()?(C+=7,$=new DC(__assign(__assign({},$),{type:\"doctype\"})),L=20):resetToDataState()}function stateCommentStart(s){\"-\"===s?L=15:\">\"===s?resetToDataState():L=16}function stateCommentStartDash(s){\"-\"===s?L=18:\">\"===s?resetToDataState():L=16}function stateComment(s){\"-\"===s&&(L=17)}function stateCommentEndDash(s){L=\"-\"===s?18:16}function stateCommentEnd(s){\">\"===s?emitTagAndPreviousTextNode():\"!\"===s?L=19:\"-\"===s||(L=16)}function stateCommentEndBang(s){\"-\"===s?L=17:\">\"===s?emitTagAndPreviousTextNode():L=16}function stateDoctype(s){\">\"===s?emitTagAndPreviousTextNode():\"<\"===s&&startNewTag()}function resetToDataState(){L=0,$=x}function startNewTag(){L=1,$=new DC({idx:C})}function emitTagAndPreviousTextNode(){var o=s.slice(B,$.idx);o&&u(o,B),\"comment\"===$.type?_($.idx):\"doctype\"===$.type?w($.idx):($.isOpening&&i($.name,$.idx),$.isClosing&&a($.name,$.idx)),resetToDataState(),B=C+1}function captureTagName(){var o=$.idx+($.isClosing?2:1);return s.slice(o,C).toLowerCase()}B<C&&function emitText(){var o=s.slice(B,C);u(o,B),B=C+1}()}var DC=function DC(s){void 0===s&&(s={}),this.idx=void 0!==s.idx?s.idx:-1,this.type=s.type||\"tag\",this.name=s.name||\"\",this.isOpening=!!s.isOpening,this.isClosing=!!s.isClosing},LC=function(){function Autolinker(s){void 0===s&&(s={}),this.version=Autolinker.version,this.urls={},this.email=!0,this.phone=!0,this.hashtag=!1,this.mention=!1,this.newWindow=!0,this.stripPrefix={scheme:!0,www:!0},this.stripTrailingSlash=!0,this.decodePercentEncoding=!0,this.truncate={length:0,location:\"end\"},this.className=\"\",this.replaceFn=null,this.context=void 0,this.sanitizeHtml=!1,this.matchers=null,this.tagBuilder=null,this.urls=this.normalizeUrlsCfg(s.urls),this.email=\"boolean\"==typeof s.email?s.email:this.email,this.phone=\"boolean\"==typeof s.phone?s.phone:this.phone,this.hashtag=s.hashtag||this.hashtag,this.mention=s.mention||this.mention,this.newWindow=\"boolean\"==typeof s.newWindow?s.newWindow:this.newWindow,this.stripPrefix=this.normalizeStripPrefixCfg(s.stripPrefix),this.stripTrailingSlash=\"boolean\"==typeof s.stripTrailingSlash?s.stripTrailingSlash:this.stripTrailingSlash,this.decodePercentEncoding=\"boolean\"==typeof s.decodePercentEncoding?s.decodePercentEncoding:this.decodePercentEncoding,this.sanitizeHtml=s.sanitizeHtml||!1;var o=this.mention;if(!1!==o&&-1===[\"twitter\",\"instagram\",\"soundcloud\",\"tiktok\"].indexOf(o))throw new Error(\"invalid `mention` cfg '\".concat(o,\"' - see docs\"));var i=this.hashtag;if(!1!==i&&-1===AC.indexOf(i))throw new Error(\"invalid `hashtag` cfg '\".concat(i,\"' - see docs\"));this.truncate=this.normalizeTruncateCfg(s.truncate),this.className=s.className||this.className,this.replaceFn=s.replaceFn||this.replaceFn,this.context=s.context||this}return Autolinker.link=function(s,o){return new Autolinker(o).link(s)},Autolinker.parse=function(s,o){return new Autolinker(o).parse(s)},Autolinker.prototype.normalizeUrlsCfg=function(s){return null==s&&(s=!0),\"boolean\"==typeof s?{schemeMatches:s,wwwMatches:s,tldMatches:s}:{schemeMatches:\"boolean\"!=typeof s.schemeMatches||s.schemeMatches,wwwMatches:\"boolean\"!=typeof s.wwwMatches||s.wwwMatches,tldMatches:\"boolean\"!=typeof s.tldMatches||s.tldMatches}},Autolinker.prototype.normalizeStripPrefixCfg=function(s){return null==s&&(s=!0),\"boolean\"==typeof s?{scheme:s,www:s}:{scheme:\"boolean\"!=typeof s.scheme||s.scheme,www:\"boolean\"!=typeof s.www||s.www}},Autolinker.prototype.normalizeTruncateCfg=function(s){return\"number\"==typeof s?{length:s,location:\"end\"}:function defaults(s,o){for(var i in o)o.hasOwnProperty(i)&&void 0===s[i]&&(s[i]=o[i]);return s}(s||{},{length:Number.POSITIVE_INFINITY,location:\"end\"})},Autolinker.prototype.parse=function(s){var o=this,i=[\"a\",\"style\",\"script\"],a=0,u=[];return parseHtml(s,{onOpenTag:function(s){i.indexOf(s)>=0&&a++},onText:function(s,i){if(0===a){var _=function splitAndCapture(s,o){if(!o.global)throw new Error(\"`splitRegex` must have the 'g' flag set\");for(var i,a=[],u=0;i=o.exec(s);)a.push(s.substring(u,i.index)),a.push(i[0]),u=i.index+i[0].length;return a.push(s.substring(u)),a}(s,/(&nbsp;|&#160;|&lt;|&#60;|&gt;|&#62;|&quot;|&#34;|&#39;)/gi),w=i;_.forEach((function(s,i){if(i%2==0){var a=o.parseText(s,w);u.push.apply(u,a)}w+=s.length}))}},onCloseTag:function(s){i.indexOf(s)>=0&&(a=Math.max(a-1,0))},onComment:function(s){},onDoctype:function(s){}}),u=this.compactMatches(u),u=this.removeUnwantedMatches(u)},Autolinker.prototype.compactMatches=function(s){s.sort((function(s,o){return s.getOffset()-o.getOffset()}));for(var o=0;o<s.length-1;){var i=s[o],a=i.getOffset(),u=i.getMatchedText().length,_=a+u;if(o+1<s.length){if(s[o+1].getOffset()===a){var w=s[o+1].getMatchedText().length>u?o:o+1;s.splice(w,1);continue}if(s[o+1].getOffset()<_){s.splice(o+1,1);continue}}o++}return s},Autolinker.prototype.removeUnwantedMatches=function(s){return this.hashtag||utils_remove(s,(function(s){return\"hashtag\"===s.getType()})),this.email||utils_remove(s,(function(s){return\"email\"===s.getType()})),this.phone||utils_remove(s,(function(s){return\"phone\"===s.getType()})),this.mention||utils_remove(s,(function(s){return\"mention\"===s.getType()})),this.urls.schemeMatches||utils_remove(s,(function(s){return\"url\"===s.getType()&&\"scheme\"===s.getUrlMatchType()})),this.urls.wwwMatches||utils_remove(s,(function(s){return\"url\"===s.getType()&&\"www\"===s.getUrlMatchType()})),this.urls.tldMatches||utils_remove(s,(function(s){return\"url\"===s.getType()&&\"tld\"===s.getUrlMatchType()})),s},Autolinker.prototype.parseText=function(s,o){void 0===o&&(o=0),o=o||0;for(var i=this.getMatchers(),a=[],u=0,_=i.length;u<_;u++){for(var w=i[u].parseMatches(s),x=0,C=w.length;x<C;x++)w[x].setOffset(o+w[x].getOffset());a.push.apply(a,w)}return a},Autolinker.prototype.link=function(s){if(!s)return\"\";this.sanitizeHtml&&(s=s.replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\"));for(var o=this.parse(s),i=[],a=0,u=0,_=o.length;u<_;u++){var w=o[u];i.push(s.substring(a,w.getOffset())),i.push(this.createMatchReturnVal(w)),a=w.getOffset()+w.getMatchedText().length}return i.push(s.substring(a)),i.join(\"\")},Autolinker.prototype.createMatchReturnVal=function(s){var o;return this.replaceFn&&(o=this.replaceFn.call(this.context,s)),\"string\"==typeof o?o:!1===o?s.getMatchedText():o instanceof WA?o.toAnchorString():s.buildTag().toAnchorString()},Autolinker.prototype.getMatchers=function(){if(this.matchers)return this.matchers;var s=this.getTagBuilder(),o=[new OC({tagBuilder:s,serviceName:this.hashtag}),new bC({tagBuilder:s}),new jC({tagBuilder:s}),new RC({tagBuilder:s,serviceName:this.mention}),new xC({tagBuilder:s,stripPrefix:this.stripPrefix,stripTrailingSlash:this.stripTrailingSlash,decodePercentEncoding:this.decodePercentEncoding})];return this.matchers=o},Autolinker.prototype.getTagBuilder=function(){var s=this.tagBuilder;return s||(s=this.tagBuilder=new JA({newWindow:this.newWindow,truncate:this.truncate,className:this.className})),s},Autolinker.version=\"3.16.2\",Autolinker.AnchorTagBuilder=JA,Autolinker.HtmlTag=WA,Autolinker.matcher={Email:bC,Hashtag:OC,Matcher:eC,Mention:RC,Phone:jC,Url:xC},Autolinker.match={Email:GA,Hashtag:YA,Match:HA,Mention:XA,Phone:QA,Url:ZA},Autolinker}();const FC=LC;var BC=/www|@|\\:\\/\\//;function isLinkOpen(s){return/^<a[>\\s]/i.test(s)}function isLinkClose(s){return/^<\\/a\\s*>/i.test(s)}function createLinkifier(){var s=[],o=new FC({stripPrefix:!1,url:!0,email:!0,replaceFn:function(o){switch(o.getType()){case\"url\":s.push({text:o.matchedText,url:o.getUrl()});break;case\"email\":s.push({text:o.matchedText,url:\"mailto:\"+o.getEmail().replace(/^mailto:/i,\"\")})}return!1}});return{links:s,autolinker:o}}function parseTokens(s){var o,i,a,u,_,w,x,C,j,L,B,$,U,V=s.tokens,z=null;for(i=0,a=V.length;i<a;i++)if(\"inline\"===V[i].type)for(B=0,o=(u=V[i].children).length-1;o>=0;o--)if(\"link_close\"!==(_=u[o]).type){if(\"htmltag\"===_.type&&(isLinkOpen(_.content)&&B>0&&B--,isLinkClose(_.content)&&B++),!(B>0)&&\"text\"===_.type&&BC.test(_.content)){if(z||($=(z=createLinkifier()).links,U=z.autolinker),w=_.content,$.length=0,U.link(w),!$.length)continue;for(x=[],L=_.level,C=0;C<$.length;C++)s.inline.validateLink($[C].url)&&((j=w.indexOf($[C].text))&&x.push({type:\"text\",content:w.slice(0,j),level:L}),x.push({type:\"link_open\",href:$[C].url,title:\"\",level:L++}),x.push({type:\"text\",content:$[C].text,level:L}),x.push({type:\"link_close\",level:--L}),w=w.slice(j+$[C].text.length));w.length&&x.push({type:\"text\",content:w,level:L}),V[i].children=u=[].concat(u.slice(0,o),x,u.slice(o+1))}}else for(o--;u[o].level!==_.level&&\"link_open\"!==u[o].type;)o--}function linkify(s){s.core.ruler.push(\"linkify\",parseTokens)}const{entries:$C,setPrototypeOf:qC,isFrozen:UC,getPrototypeOf:VC,getOwnPropertyDescriptor:zC}=Object;let{freeze:WC,seal:JC,create:HC}=Object,{apply:KC,construct:GC}=\"undefined\"!=typeof Reflect&&Reflect;WC||(WC=function freeze(s){return s}),JC||(JC=function seal(s){return s}),KC||(KC=function apply(s,o,i){return s.apply(o,i)}),GC||(GC=function construct(s,o){return new s(...o)});const YC=unapply(Array.prototype.forEach),XC=unapply(Array.prototype.lastIndexOf),QC=unapply(Array.prototype.pop),ZC=unapply(Array.prototype.push),ej=unapply(Array.prototype.splice),fj=unapply(String.prototype.toLowerCase),mj=unapply(String.prototype.toString),_j=unapply(String.prototype.match),Aj=unapply(String.prototype.replace),Cj=unapply(String.prototype.indexOf),Nj=unapply(String.prototype.trim),Bj=unapply(Object.prototype.hasOwnProperty),$j=unapply(RegExp.prototype.test),zj=function unconstruct(s){return function(){for(var o=arguments.length,i=new Array(o),a=0;a<o;a++)i[a]=arguments[a];return GC(s,i)}}(TypeError);function unapply(s){return function(o){o instanceof RegExp&&(o.lastIndex=0);for(var i=arguments.length,a=new Array(i>1?i-1:0),u=1;u<i;u++)a[u-1]=arguments[u];return KC(s,o,a)}}function addToSet(s,o){let i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:fj;qC&&qC(s,null);let a=o.length;for(;a--;){let u=o[a];if(\"string\"==typeof u){const s=i(u);s!==u&&(UC(o)||(o[a]=s),u=s)}s[u]=!0}return s}function purify_es_cleanArray(s){for(let o=0;o<s.length;o++){Bj(s,o)||(s[o]=null)}return s}function clone(s){const o=HC(null);for(const[i,a]of $C(s)){Bj(s,i)&&(Array.isArray(a)?o[i]=purify_es_cleanArray(a):a&&\"object\"==typeof a&&a.constructor===Object?o[i]=clone(a):o[i]=a)}return o}function lookupGetter(s,o){for(;null!==s;){const i=zC(s,o);if(i){if(i.get)return unapply(i.get);if(\"function\"==typeof i.value)return unapply(i.value)}s=VC(s)}return function fallbackValue(){return null}}const Jj=WC([\"a\",\"abbr\",\"acronym\",\"address\",\"area\",\"article\",\"aside\",\"audio\",\"b\",\"bdi\",\"bdo\",\"big\",\"blink\",\"blockquote\",\"body\",\"br\",\"button\",\"canvas\",\"caption\",\"center\",\"cite\",\"code\",\"col\",\"colgroup\",\"content\",\"data\",\"datalist\",\"dd\",\"decorator\",\"del\",\"details\",\"dfn\",\"dialog\",\"dir\",\"div\",\"dl\",\"dt\",\"element\",\"em\",\"fieldset\",\"figcaption\",\"figure\",\"font\",\"footer\",\"form\",\"h1\",\"h2\",\"h3\",\"h4\",\"h5\",\"h6\",\"head\",\"header\",\"hgroup\",\"hr\",\"html\",\"i\",\"img\",\"input\",\"ins\",\"kbd\",\"label\",\"legend\",\"li\",\"main\",\"map\",\"mark\",\"marquee\",\"menu\",\"menuitem\",\"meter\",\"nav\",\"nobr\",\"ol\",\"optgroup\",\"option\",\"output\",\"p\",\"picture\",\"pre\",\"progress\",\"q\",\"rp\",\"rt\",\"ruby\",\"s\",\"samp\",\"section\",\"select\",\"shadow\",\"small\",\"source\",\"spacer\",\"span\",\"strike\",\"strong\",\"style\",\"sub\",\"summary\",\"sup\",\"table\",\"tbody\",\"td\",\"template\",\"textarea\",\"tfoot\",\"th\",\"thead\",\"time\",\"tr\",\"track\",\"tt\",\"u\",\"ul\",\"var\",\"video\",\"wbr\"]),Kj=WC([\"svg\",\"a\",\"altglyph\",\"altglyphdef\",\"altglyphitem\",\"animatecolor\",\"animatemotion\",\"animatetransform\",\"circle\",\"clippath\",\"defs\",\"desc\",\"ellipse\",\"filter\",\"font\",\"g\",\"glyph\",\"glyphref\",\"hkern\",\"image\",\"line\",\"lineargradient\",\"marker\",\"mask\",\"metadata\",\"mpath\",\"path\",\"pattern\",\"polygon\",\"polyline\",\"radialgradient\",\"rect\",\"stop\",\"style\",\"switch\",\"symbol\",\"text\",\"textpath\",\"title\",\"tref\",\"tspan\",\"view\",\"vkern\"]),Gj=WC([\"feBlend\",\"feColorMatrix\",\"feComponentTransfer\",\"feComposite\",\"feConvolveMatrix\",\"feDiffuseLighting\",\"feDisplacementMap\",\"feDistantLight\",\"feDropShadow\",\"feFlood\",\"feFuncA\",\"feFuncB\",\"feFuncG\",\"feFuncR\",\"feGaussianBlur\",\"feImage\",\"feMerge\",\"feMergeNode\",\"feMorphology\",\"feOffset\",\"fePointLight\",\"feSpecularLighting\",\"feSpotLight\",\"feTile\",\"feTurbulence\"]),Xj=WC([\"animate\",\"color-profile\",\"cursor\",\"discard\",\"font-face\",\"font-face-format\",\"font-face-name\",\"font-face-src\",\"font-face-uri\",\"foreignobject\",\"hatch\",\"hatchpath\",\"mesh\",\"meshgradient\",\"meshpatch\",\"meshrow\",\"missing-glyph\",\"script\",\"set\",\"solidcolor\",\"unknown\",\"use\"]),eP=WC([\"math\",\"menclose\",\"merror\",\"mfenced\",\"mfrac\",\"mglyph\",\"mi\",\"mlabeledtr\",\"mmultiscripts\",\"mn\",\"mo\",\"mover\",\"mpadded\",\"mphantom\",\"mroot\",\"mrow\",\"ms\",\"mspace\",\"msqrt\",\"mstyle\",\"msub\",\"msup\",\"msubsup\",\"mtable\",\"mtd\",\"mtext\",\"mtr\",\"munder\",\"munderover\",\"mprescripts\"]),tP=WC([\"maction\",\"maligngroup\",\"malignmark\",\"mlongdiv\",\"mscarries\",\"mscarry\",\"msgroup\",\"mstack\",\"msline\",\"msrow\",\"semantics\",\"annotation\",\"annotation-xml\",\"mprescripts\",\"none\"]),rP=WC([\"#text\"]),nP=WC([\"accept\",\"action\",\"align\",\"alt\",\"autocapitalize\",\"autocomplete\",\"autopictureinpicture\",\"autoplay\",\"background\",\"bgcolor\",\"border\",\"capture\",\"cellpadding\",\"cellspacing\",\"checked\",\"cite\",\"class\",\"clear\",\"color\",\"cols\",\"colspan\",\"controls\",\"controlslist\",\"coords\",\"crossorigin\",\"datetime\",\"decoding\",\"default\",\"dir\",\"disabled\",\"disablepictureinpicture\",\"disableremoteplayback\",\"download\",\"draggable\",\"enctype\",\"enterkeyhint\",\"face\",\"for\",\"headers\",\"height\",\"hidden\",\"high\",\"href\",\"hreflang\",\"id\",\"inputmode\",\"integrity\",\"ismap\",\"kind\",\"label\",\"lang\",\"list\",\"loading\",\"loop\",\"low\",\"max\",\"maxlength\",\"media\",\"method\",\"min\",\"minlength\",\"multiple\",\"muted\",\"name\",\"nonce\",\"noshade\",\"novalidate\",\"nowrap\",\"open\",\"optimum\",\"pattern\",\"placeholder\",\"playsinline\",\"popover\",\"popovertarget\",\"popovertargetaction\",\"poster\",\"preload\",\"pubdate\",\"radiogroup\",\"readonly\",\"rel\",\"required\",\"rev\",\"reversed\",\"role\",\"rows\",\"rowspan\",\"spellcheck\",\"scope\",\"selected\",\"shape\",\"size\",\"sizes\",\"span\",\"srclang\",\"start\",\"src\",\"srcset\",\"step\",\"style\",\"summary\",\"tabindex\",\"title\",\"translate\",\"type\",\"usemap\",\"valign\",\"value\",\"width\",\"wrap\",\"xmlns\",\"slot\"]),sP=WC([\"accent-height\",\"accumulate\",\"additive\",\"alignment-baseline\",\"amplitude\",\"ascent\",\"attributename\",\"attributetype\",\"azimuth\",\"basefrequency\",\"baseline-shift\",\"begin\",\"bias\",\"by\",\"class\",\"clip\",\"clippathunits\",\"clip-path\",\"clip-rule\",\"color\",\"color-interpolation\",\"color-interpolation-filters\",\"color-profile\",\"color-rendering\",\"cx\",\"cy\",\"d\",\"dx\",\"dy\",\"diffuseconstant\",\"direction\",\"display\",\"divisor\",\"dur\",\"edgemode\",\"elevation\",\"end\",\"exponent\",\"fill\",\"fill-opacity\",\"fill-rule\",\"filter\",\"filterunits\",\"flood-color\",\"flood-opacity\",\"font-family\",\"font-size\",\"font-size-adjust\",\"font-stretch\",\"font-style\",\"font-variant\",\"font-weight\",\"fx\",\"fy\",\"g1\",\"g2\",\"glyph-name\",\"glyphref\",\"gradientunits\",\"gradienttransform\",\"height\",\"href\",\"id\",\"image-rendering\",\"in\",\"in2\",\"intercept\",\"k\",\"k1\",\"k2\",\"k3\",\"k4\",\"kerning\",\"keypoints\",\"keysplines\",\"keytimes\",\"lang\",\"lengthadjust\",\"letter-spacing\",\"kernelmatrix\",\"kernelunitlength\",\"lighting-color\",\"local\",\"marker-end\",\"marker-mid\",\"marker-start\",\"markerheight\",\"markerunits\",\"markerwidth\",\"maskcontentunits\",\"maskunits\",\"max\",\"mask\",\"media\",\"method\",\"mode\",\"min\",\"name\",\"numoctaves\",\"offset\",\"operator\",\"opacity\",\"order\",\"orient\",\"orientation\",\"origin\",\"overflow\",\"paint-order\",\"path\",\"pathlength\",\"patterncontentunits\",\"patterntransform\",\"patternunits\",\"points\",\"preservealpha\",\"preserveaspectratio\",\"primitiveunits\",\"r\",\"rx\",\"ry\",\"radius\",\"refx\",\"refy\",\"repeatcount\",\"repeatdur\",\"restart\",\"result\",\"rotate\",\"scale\",\"seed\",\"shape-rendering\",\"slope\",\"specularconstant\",\"specularexponent\",\"spreadmethod\",\"startoffset\",\"stddeviation\",\"stitchtiles\",\"stop-color\",\"stop-opacity\",\"stroke-dasharray\",\"stroke-dashoffset\",\"stroke-linecap\",\"stroke-linejoin\",\"stroke-miterlimit\",\"stroke-opacity\",\"stroke\",\"stroke-width\",\"style\",\"surfacescale\",\"systemlanguage\",\"tabindex\",\"tablevalues\",\"targetx\",\"targety\",\"transform\",\"transform-origin\",\"text-anchor\",\"text-decoration\",\"text-rendering\",\"textlength\",\"type\",\"u1\",\"u2\",\"unicode\",\"values\",\"viewbox\",\"visibility\",\"version\",\"vert-adv-y\",\"vert-origin-x\",\"vert-origin-y\",\"width\",\"word-spacing\",\"wrap\",\"writing-mode\",\"xchannelselector\",\"ychannelselector\",\"x\",\"x1\",\"x2\",\"xmlns\",\"y\",\"y1\",\"y2\",\"z\",\"zoomandpan\"]),oP=WC([\"accent\",\"accentunder\",\"align\",\"bevelled\",\"close\",\"columnsalign\",\"columnlines\",\"columnspan\",\"denomalign\",\"depth\",\"dir\",\"display\",\"displaystyle\",\"encoding\",\"fence\",\"frame\",\"height\",\"href\",\"id\",\"largeop\",\"length\",\"linethickness\",\"lspace\",\"lquote\",\"mathbackground\",\"mathcolor\",\"mathsize\",\"mathvariant\",\"maxsize\",\"minsize\",\"movablelimits\",\"notation\",\"numalign\",\"open\",\"rowalign\",\"rowlines\",\"rowspacing\",\"rowspan\",\"rspace\",\"rquote\",\"scriptlevel\",\"scriptminsize\",\"scriptsizemultiplier\",\"selection\",\"separator\",\"separators\",\"stretchy\",\"subscriptshift\",\"supscriptshift\",\"symmetric\",\"voffset\",\"width\",\"xmlns\"]),iP=WC([\"xlink:href\",\"xml:id\",\"xlink:title\",\"xml:space\",\"xmlns:xlink\"]),aP=JC(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm),cP=JC(/<%[\\w\\W]*|[\\w\\W]*%>/gm),lP=JC(/\\$\\{[\\w\\W]*/gm),uP=JC(/^data-[\\-\\w.\\u00B7-\\uFFFF]+$/),pP=JC(/^aria-[\\-\\w]+$/),hP=JC(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i),dP=JC(/^(?:\\w+script|data):/i),fP=JC(/[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g),mP=JC(/^html$/i),gP=JC(/^[a-z][.\\w]*(-[.\\w]+)+$/i);var yP=Object.freeze({__proto__:null,ARIA_ATTR:pP,ATTR_WHITESPACE:fP,CUSTOM_ELEMENT:gP,DATA_ATTR:uP,DOCTYPE_NAME:mP,ERB_EXPR:cP,IS_ALLOWED_URI:hP,IS_SCRIPT_OR_DATA:dP,MUSTACHE_EXPR:aP,TMPLIT_EXPR:lP});const vP=1,bP=3,_P=7,SP=8,EP=9,wP=function getGlobal(){return\"undefined\"==typeof window?null:window};var xP=function createDOMPurify(){let s=arguments.length>0&&void 0!==arguments[0]?arguments[0]:wP();const DOMPurify=s=>createDOMPurify(s);if(DOMPurify.version=\"3.2.6\",DOMPurify.removed=[],!s||!s.document||s.document.nodeType!==EP||!s.Element)return DOMPurify.isSupported=!1,DOMPurify;let{document:o}=s;const i=o,a=i.currentScript,{DocumentFragment:u,HTMLTemplateElement:_,Node:w,Element:x,NodeFilter:C,NamedNodeMap:j=s.NamedNodeMap||s.MozNamedAttrMap,HTMLFormElement:L,DOMParser:B,trustedTypes:$}=s,U=x.prototype,V=lookupGetter(U,\"cloneNode\"),z=lookupGetter(U,\"remove\"),Y=lookupGetter(U,\"nextSibling\"),Z=lookupGetter(U,\"childNodes\"),ee=lookupGetter(U,\"parentNode\");if(\"function\"==typeof _){const s=o.createElement(\"template\");s.content&&s.content.ownerDocument&&(o=s.content.ownerDocument)}let ie,ae=\"\";const{implementation:ce,createNodeIterator:le,createDocumentFragment:pe,getElementsByTagName:de}=o,{importNode:fe}=i;let ye={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};DOMPurify.isSupported=\"function\"==typeof $C&&\"function\"==typeof ee&&ce&&void 0!==ce.createHTMLDocument;const{MUSTACHE_EXPR:be,ERB_EXPR:_e,TMPLIT_EXPR:Se,DATA_ATTR:we,ARIA_ATTR:xe,IS_SCRIPT_OR_DATA:Pe,ATTR_WHITESPACE:Te,CUSTOM_ELEMENT:Re}=yP;let{IS_ALLOWED_URI:$e}=yP,qe=null;const ze=addToSet({},[...Jj,...Kj,...Gj,...eP,...rP]);let We=null;const He=addToSet({},[...nP,...sP,...oP,...iP]);let Ye=Object.seal(HC(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Xe=null,Qe=null,et=!0,tt=!0,rt=!1,nt=!0,st=!1,ot=!0,it=!1,at=!1,ct=!1,lt=!1,ut=!1,pt=!1,ht=!0,dt=!1,mt=!0,gt=!1,yt={},vt=null;const bt=addToSet({},[\"annotation-xml\",\"audio\",\"colgroup\",\"desc\",\"foreignobject\",\"head\",\"iframe\",\"math\",\"mi\",\"mn\",\"mo\",\"ms\",\"mtext\",\"noembed\",\"noframes\",\"noscript\",\"plaintext\",\"script\",\"style\",\"svg\",\"template\",\"thead\",\"title\",\"video\",\"xmp\"]);let _t=null;const St=addToSet({},[\"audio\",\"video\",\"img\",\"source\",\"image\",\"track\"]);let Et=null;const wt=addToSet({},[\"alt\",\"class\",\"for\",\"id\",\"label\",\"name\",\"pattern\",\"placeholder\",\"role\",\"summary\",\"title\",\"value\",\"style\",\"xmlns\"]),xt=\"http://www.w3.org/1998/Math/MathML\",kt=\"http://www.w3.org/2000/svg\",Ot=\"http://www.w3.org/1999/xhtml\";let At=Ot,Ct=!1,jt=null;const Pt=addToSet({},[xt,kt,Ot],mj);let It=addToSet({},[\"mi\",\"mo\",\"mn\",\"ms\",\"mtext\"]),Tt=addToSet({},[\"annotation-xml\"]);const Nt=addToSet({},[\"title\",\"style\",\"font\",\"a\",\"script\"]);let Mt=null;const Rt=[\"application/xhtml+xml\",\"text/html\"];let Dt=null,Lt=null;const Ft=o.createElement(\"form\"),Bt=function isRegexOrFunction(s){return s instanceof RegExp||s instanceof Function},$t=function _parseConfig(){let s=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!Lt||Lt!==s){if(s&&\"object\"==typeof s||(s={}),s=clone(s),Mt=-1===Rt.indexOf(s.PARSER_MEDIA_TYPE)?\"text/html\":s.PARSER_MEDIA_TYPE,Dt=\"application/xhtml+xml\"===Mt?mj:fj,qe=Bj(s,\"ALLOWED_TAGS\")?addToSet({},s.ALLOWED_TAGS,Dt):ze,We=Bj(s,\"ALLOWED_ATTR\")?addToSet({},s.ALLOWED_ATTR,Dt):He,jt=Bj(s,\"ALLOWED_NAMESPACES\")?addToSet({},s.ALLOWED_NAMESPACES,mj):Pt,Et=Bj(s,\"ADD_URI_SAFE_ATTR\")?addToSet(clone(wt),s.ADD_URI_SAFE_ATTR,Dt):wt,_t=Bj(s,\"ADD_DATA_URI_TAGS\")?addToSet(clone(St),s.ADD_DATA_URI_TAGS,Dt):St,vt=Bj(s,\"FORBID_CONTENTS\")?addToSet({},s.FORBID_CONTENTS,Dt):bt,Xe=Bj(s,\"FORBID_TAGS\")?addToSet({},s.FORBID_TAGS,Dt):clone({}),Qe=Bj(s,\"FORBID_ATTR\")?addToSet({},s.FORBID_ATTR,Dt):clone({}),yt=!!Bj(s,\"USE_PROFILES\")&&s.USE_PROFILES,et=!1!==s.ALLOW_ARIA_ATTR,tt=!1!==s.ALLOW_DATA_ATTR,rt=s.ALLOW_UNKNOWN_PROTOCOLS||!1,nt=!1!==s.ALLOW_SELF_CLOSE_IN_ATTR,st=s.SAFE_FOR_TEMPLATES||!1,ot=!1!==s.SAFE_FOR_XML,it=s.WHOLE_DOCUMENT||!1,lt=s.RETURN_DOM||!1,ut=s.RETURN_DOM_FRAGMENT||!1,pt=s.RETURN_TRUSTED_TYPE||!1,ct=s.FORCE_BODY||!1,ht=!1!==s.SANITIZE_DOM,dt=s.SANITIZE_NAMED_PROPS||!1,mt=!1!==s.KEEP_CONTENT,gt=s.IN_PLACE||!1,$e=s.ALLOWED_URI_REGEXP||hP,At=s.NAMESPACE||Ot,It=s.MATHML_TEXT_INTEGRATION_POINTS||It,Tt=s.HTML_INTEGRATION_POINTS||Tt,Ye=s.CUSTOM_ELEMENT_HANDLING||{},s.CUSTOM_ELEMENT_HANDLING&&Bt(s.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ye.tagNameCheck=s.CUSTOM_ELEMENT_HANDLING.tagNameCheck),s.CUSTOM_ELEMENT_HANDLING&&Bt(s.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ye.attributeNameCheck=s.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),s.CUSTOM_ELEMENT_HANDLING&&\"boolean\"==typeof s.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ye.allowCustomizedBuiltInElements=s.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),st&&(tt=!1),ut&&(lt=!0),yt&&(qe=addToSet({},rP),We=[],!0===yt.html&&(addToSet(qe,Jj),addToSet(We,nP)),!0===yt.svg&&(addToSet(qe,Kj),addToSet(We,sP),addToSet(We,iP)),!0===yt.svgFilters&&(addToSet(qe,Gj),addToSet(We,sP),addToSet(We,iP)),!0===yt.mathMl&&(addToSet(qe,eP),addToSet(We,oP),addToSet(We,iP))),s.ADD_TAGS&&(qe===ze&&(qe=clone(qe)),addToSet(qe,s.ADD_TAGS,Dt)),s.ADD_ATTR&&(We===He&&(We=clone(We)),addToSet(We,s.ADD_ATTR,Dt)),s.ADD_URI_SAFE_ATTR&&addToSet(Et,s.ADD_URI_SAFE_ATTR,Dt),s.FORBID_CONTENTS&&(vt===bt&&(vt=clone(vt)),addToSet(vt,s.FORBID_CONTENTS,Dt)),mt&&(qe[\"#text\"]=!0),it&&addToSet(qe,[\"html\",\"head\",\"body\"]),qe.table&&(addToSet(qe,[\"tbody\"]),delete Xe.tbody),s.TRUSTED_TYPES_POLICY){if(\"function\"!=typeof s.TRUSTED_TYPES_POLICY.createHTML)throw zj('TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.');if(\"function\"!=typeof s.TRUSTED_TYPES_POLICY.createScriptURL)throw zj('TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.');ie=s.TRUSTED_TYPES_POLICY,ae=ie.createHTML(\"\")}else void 0===ie&&(ie=function _createTrustedTypesPolicy(s,o){if(\"object\"!=typeof s||\"function\"!=typeof s.createPolicy)return null;let i=null;const a=\"data-tt-policy-suffix\";o&&o.hasAttribute(a)&&(i=o.getAttribute(a));const u=\"dompurify\"+(i?\"#\"+i:\"\");try{return s.createPolicy(u,{createHTML:s=>s,createScriptURL:s=>s})}catch(s){return console.warn(\"TrustedTypes policy \"+u+\" could not be created.\"),null}}($,a)),null!==ie&&\"string\"==typeof ae&&(ae=ie.createHTML(\"\"));WC&&WC(s),Lt=s}},qt=addToSet({},[...Kj,...Gj,...Xj]),Ut=addToSet({},[...eP,...tP]),Vt=function _forceRemove(s){ZC(DOMPurify.removed,{element:s});try{ee(s).removeChild(s)}catch(o){z(s)}},zt=function _removeAttribute(s,o){try{ZC(DOMPurify.removed,{attribute:o.getAttributeNode(s),from:o})}catch(s){ZC(DOMPurify.removed,{attribute:null,from:o})}if(o.removeAttribute(s),\"is\"===s)if(lt||ut)try{Vt(o)}catch(s){}else try{o.setAttribute(s,\"\")}catch(s){}},Wt=function _initDocument(s){let i=null,a=null;if(ct)s=\"<remove></remove>\"+s;else{const o=_j(s,/^[\\r\\n\\t ]+/);a=o&&o[0]}\"application/xhtml+xml\"===Mt&&At===Ot&&(s='<html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head><body>'+s+\"</body></html>\");const u=ie?ie.createHTML(s):s;if(At===Ot)try{i=(new B).parseFromString(u,Mt)}catch(s){}if(!i||!i.documentElement){i=ce.createDocument(At,\"template\",null);try{i.documentElement.innerHTML=Ct?ae:u}catch(s){}}const _=i.body||i.documentElement;return s&&a&&_.insertBefore(o.createTextNode(a),_.childNodes[0]||null),At===Ot?de.call(i,it?\"html\":\"body\")[0]:it?i.documentElement:_},Jt=function _createNodeIterator(s){return le.call(s.ownerDocument||s,s,C.SHOW_ELEMENT|C.SHOW_COMMENT|C.SHOW_TEXT|C.SHOW_PROCESSING_INSTRUCTION|C.SHOW_CDATA_SECTION,null)},Ht=function _isClobbered(s){return s instanceof L&&(\"string\"!=typeof s.nodeName||\"string\"!=typeof s.textContent||\"function\"!=typeof s.removeChild||!(s.attributes instanceof j)||\"function\"!=typeof s.removeAttribute||\"function\"!=typeof s.setAttribute||\"string\"!=typeof s.namespaceURI||\"function\"!=typeof s.insertBefore||\"function\"!=typeof s.hasChildNodes)},Kt=function _isNode(s){return\"function\"==typeof w&&s instanceof w};function _executeHooks(s,o,i){YC(s,(s=>{s.call(DOMPurify,o,i,Lt)}))}const Gt=function _sanitizeElements(s){let o=null;if(_executeHooks(ye.beforeSanitizeElements,s,null),Ht(s))return Vt(s),!0;const i=Dt(s.nodeName);if(_executeHooks(ye.uponSanitizeElement,s,{tagName:i,allowedTags:qe}),ot&&s.hasChildNodes()&&!Kt(s.firstElementChild)&&$j(/<[/\\w!]/g,s.innerHTML)&&$j(/<[/\\w!]/g,s.textContent))return Vt(s),!0;if(s.nodeType===_P)return Vt(s),!0;if(ot&&s.nodeType===SP&&$j(/<[/\\w]/g,s.data))return Vt(s),!0;if(!qe[i]||Xe[i]){if(!Xe[i]&&Xt(i)){if(Ye.tagNameCheck instanceof RegExp&&$j(Ye.tagNameCheck,i))return!1;if(Ye.tagNameCheck instanceof Function&&Ye.tagNameCheck(i))return!1}if(mt&&!vt[i]){const o=ee(s)||s.parentNode,i=Z(s)||s.childNodes;if(i&&o){for(let a=i.length-1;a>=0;--a){const u=V(i[a],!0);u.__removalCount=(s.__removalCount||0)+1,o.insertBefore(u,Y(s))}}}return Vt(s),!0}return s instanceof x&&!function _checkValidNamespace(s){let o=ee(s);o&&o.tagName||(o={namespaceURI:At,tagName:\"template\"});const i=fj(s.tagName),a=fj(o.tagName);return!!jt[s.namespaceURI]&&(s.namespaceURI===kt?o.namespaceURI===Ot?\"svg\"===i:o.namespaceURI===xt?\"svg\"===i&&(\"annotation-xml\"===a||It[a]):Boolean(qt[i]):s.namespaceURI===xt?o.namespaceURI===Ot?\"math\"===i:o.namespaceURI===kt?\"math\"===i&&Tt[a]:Boolean(Ut[i]):s.namespaceURI===Ot?!(o.namespaceURI===kt&&!Tt[a])&&!(o.namespaceURI===xt&&!It[a])&&!Ut[i]&&(Nt[i]||!qt[i]):!(\"application/xhtml+xml\"!==Mt||!jt[s.namespaceURI]))}(s)?(Vt(s),!0):\"noscript\"!==i&&\"noembed\"!==i&&\"noframes\"!==i||!$j(/<\\/no(script|embed|frames)/i,s.innerHTML)?(st&&s.nodeType===bP&&(o=s.textContent,YC([be,_e,Se],(s=>{o=Aj(o,s,\" \")})),s.textContent!==o&&(ZC(DOMPurify.removed,{element:s.cloneNode()}),s.textContent=o)),_executeHooks(ye.afterSanitizeElements,s,null),!1):(Vt(s),!0)},Yt=function _isValidAttribute(s,i,a){if(ht&&(\"id\"===i||\"name\"===i)&&(a in o||a in Ft))return!1;if(tt&&!Qe[i]&&$j(we,i));else if(et&&$j(xe,i));else if(!We[i]||Qe[i]){if(!(Xt(s)&&(Ye.tagNameCheck instanceof RegExp&&$j(Ye.tagNameCheck,s)||Ye.tagNameCheck instanceof Function&&Ye.tagNameCheck(s))&&(Ye.attributeNameCheck instanceof RegExp&&$j(Ye.attributeNameCheck,i)||Ye.attributeNameCheck instanceof Function&&Ye.attributeNameCheck(i))||\"is\"===i&&Ye.allowCustomizedBuiltInElements&&(Ye.tagNameCheck instanceof RegExp&&$j(Ye.tagNameCheck,a)||Ye.tagNameCheck instanceof Function&&Ye.tagNameCheck(a))))return!1}else if(Et[i]);else if($j($e,Aj(a,Te,\"\")));else if(\"src\"!==i&&\"xlink:href\"!==i&&\"href\"!==i||\"script\"===s||0!==Cj(a,\"data:\")||!_t[s]){if(rt&&!$j(Pe,Aj(a,Te,\"\")));else if(a)return!1}else;return!0},Xt=function _isBasicCustomElement(s){return\"annotation-xml\"!==s&&_j(s,Re)},Qt=function _sanitizeAttributes(s){_executeHooks(ye.beforeSanitizeAttributes,s,null);const{attributes:o}=s;if(!o||Ht(s))return;const i={attrName:\"\",attrValue:\"\",keepAttr:!0,allowedAttributes:We,forceKeepAttr:void 0};let a=o.length;for(;a--;){const u=o[a],{name:_,namespaceURI:w,value:x}=u,C=Dt(_),j=x;let L=\"value\"===_?j:Nj(j);if(i.attrName=C,i.attrValue=L,i.keepAttr=!0,i.forceKeepAttr=void 0,_executeHooks(ye.uponSanitizeAttribute,s,i),L=i.attrValue,!dt||\"id\"!==C&&\"name\"!==C||(zt(_,s),L=\"user-content-\"+L),ot&&$j(/((--!?|])>)|<\\/(style|title)/i,L)){zt(_,s);continue}if(i.forceKeepAttr)continue;if(!i.keepAttr){zt(_,s);continue}if(!nt&&$j(/\\/>/i,L)){zt(_,s);continue}st&&YC([be,_e,Se],(s=>{L=Aj(L,s,\" \")}));const B=Dt(s.nodeName);if(Yt(B,C,L)){if(ie&&\"object\"==typeof $&&\"function\"==typeof $.getAttributeType)if(w);else switch($.getAttributeType(B,C)){case\"TrustedHTML\":L=ie.createHTML(L);break;case\"TrustedScriptURL\":L=ie.createScriptURL(L)}if(L!==j)try{w?s.setAttributeNS(w,_,L):s.setAttribute(_,L),Ht(s)?Vt(s):QC(DOMPurify.removed)}catch(o){zt(_,s)}}else zt(_,s)}_executeHooks(ye.afterSanitizeAttributes,s,null)},Zt=function _sanitizeShadowDOM(s){let o=null;const i=Jt(s);for(_executeHooks(ye.beforeSanitizeShadowDOM,s,null);o=i.nextNode();)_executeHooks(ye.uponSanitizeShadowNode,o,null),Gt(o),Qt(o),o.content instanceof u&&_sanitizeShadowDOM(o.content);_executeHooks(ye.afterSanitizeShadowDOM,s,null)};return DOMPurify.sanitize=function(s){let o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},a=null,_=null,x=null,C=null;if(Ct=!s,Ct&&(s=\"\\x3c!--\\x3e\"),\"string\"!=typeof s&&!Kt(s)){if(\"function\"!=typeof s.toString)throw zj(\"toString is not a function\");if(\"string\"!=typeof(s=s.toString()))throw zj(\"dirty is not a string, aborting\")}if(!DOMPurify.isSupported)return s;if(at||$t(o),DOMPurify.removed=[],\"string\"==typeof s&&(gt=!1),gt){if(s.nodeName){const o=Dt(s.nodeName);if(!qe[o]||Xe[o])throw zj(\"root node is forbidden and cannot be sanitized in-place\")}}else if(s instanceof w)a=Wt(\"\\x3c!----\\x3e\"),_=a.ownerDocument.importNode(s,!0),_.nodeType===vP&&\"BODY\"===_.nodeName||\"HTML\"===_.nodeName?a=_:a.appendChild(_);else{if(!lt&&!st&&!it&&-1===s.indexOf(\"<\"))return ie&&pt?ie.createHTML(s):s;if(a=Wt(s),!a)return lt?null:pt?ae:\"\"}a&&ct&&Vt(a.firstChild);const j=Jt(gt?s:a);for(;x=j.nextNode();)Gt(x),Qt(x),x.content instanceof u&&Zt(x.content);if(gt)return s;if(lt){if(ut)for(C=pe.call(a.ownerDocument);a.firstChild;)C.appendChild(a.firstChild);else C=a;return(We.shadowroot||We.shadowrootmode)&&(C=fe.call(i,C,!0)),C}let L=it?a.outerHTML:a.innerHTML;return it&&qe[\"!doctype\"]&&a.ownerDocument&&a.ownerDocument.doctype&&a.ownerDocument.doctype.name&&$j(mP,a.ownerDocument.doctype.name)&&(L=\"<!DOCTYPE \"+a.ownerDocument.doctype.name+\">\\n\"+L),st&&YC([be,_e,Se],(s=>{L=Aj(L,s,\" \")})),ie&&pt?ie.createHTML(L):L},DOMPurify.setConfig=function(){$t(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),at=!0},DOMPurify.clearConfig=function(){Lt=null,at=!1},DOMPurify.isValidAttribute=function(s,o,i){Lt||$t({});const a=Dt(s),u=Dt(o);return Yt(a,u,i)},DOMPurify.addHook=function(s,o){\"function\"==typeof o&&ZC(ye[s],o)},DOMPurify.removeHook=function(s,o){if(void 0!==o){const i=XC(ye[s],o);return-1===i?void 0:ej(ye[s],i,1)[0]}return QC(ye[s])},DOMPurify.removeHooks=function(s){ye[s]=[]},DOMPurify.removeAllHooks=function(){ye={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},DOMPurify}();xP.addHook&&xP.addHook(\"beforeSanitizeElements\",(function(s){return s.href&&s.setAttribute(\"rel\",\"noopener noreferrer\"),s}));const kP=function Markdown({source:s,className:o=\"\",getConfigs:i=()=>({useUnsafeMarkdown:!1})}){if(\"string\"!=typeof s)return null;const a=new Remarkable({html:!0,typographer:!0,breaks:!0,linkTarget:\"_blank\"}).use(linkify);a.core.ruler.disable([\"replacements\",\"smartquotes\"]);const{useUnsafeMarkdown:u}=i(),_=a.render(s),w=sanitizer(_,{useUnsafeMarkdown:u});return s&&_&&w?Re.createElement(\"div\",{className:Jn()(o,\"markdown\"),dangerouslySetInnerHTML:{__html:w}}):null};function sanitizer(s,{useUnsafeMarkdown:o=!1}={}){const i=o,a=o?[]:[\"style\",\"class\"];return o&&!sanitizer.hasWarnedAboutDeprecation&&(console.warn(\"useUnsafeMarkdown display configuration parameter is deprecated since >3.26.0 and will be removed in v4.0.0.\"),sanitizer.hasWarnedAboutDeprecation=!0),xP.sanitize(s,{ADD_ATTR:[\"target\"],FORBID_TAGS:[\"style\",\"form\"],ALLOW_DATA_ATTR:i,FORBID_ATTR:a})}sanitizer.hasWarnedAboutDeprecation=!1;class BaseLayout extends Re.Component{render(){const{errSelectors:s,specSelectors:o,getComponent:i}=this.props,a=i(\"SvgAssets\"),u=i(\"InfoContainer\",!0),_=i(\"VersionPragmaFilter\"),w=i(\"operations\",!0),x=i(\"Models\",!0),C=i(\"Webhooks\",!0),j=i(\"Row\"),L=i(\"Col\"),B=i(\"errors\",!0),$=i(\"ServersContainer\",!0),U=i(\"SchemesContainer\",!0),V=i(\"AuthorizeBtnContainer\",!0),z=i(\"FilterContainer\",!0),Y=o.isSwagger2(),Z=o.isOAS3(),ee=o.isOAS31(),ie=!o.specStr(),ae=o.loadingStatus();let ce=null;if(\"loading\"===ae&&(ce=Re.createElement(\"div\",{className:\"info\"},Re.createElement(\"div\",{className:\"loading-container\"},Re.createElement(\"div\",{className:\"loading\"})))),\"failed\"===ae&&(ce=Re.createElement(\"div\",{className:\"info\"},Re.createElement(\"div\",{className:\"loading-container\"},Re.createElement(\"h4\",{className:\"title\"},\"Failed to load API definition.\"),Re.createElement(B,null)))),\"failedConfig\"===ae){const o=s.lastError(),i=o?o.get(\"message\"):\"\";ce=Re.createElement(\"div\",{className:\"info failed-config\"},Re.createElement(\"div\",{className:\"loading-container\"},Re.createElement(\"h4\",{className:\"title\"},\"Failed to load remote configuration.\"),Re.createElement(\"p\",null,i)))}if(!ce&&ie&&(ce=Re.createElement(\"h4\",null,\"No API definition provided.\")),ce)return Re.createElement(\"div\",{className:\"swagger-ui\"},Re.createElement(\"div\",{className:\"loading-container\"},ce));const le=o.servers(),pe=o.schemes(),de=le&&le.size,fe=pe&&pe.size,ye=!!o.securityDefinitions();return Re.createElement(\"div\",{className:\"swagger-ui\"},Re.createElement(a,null),Re.createElement(_,{isSwagger2:Y,isOAS3:Z,alsoShow:Re.createElement(B,null)},Re.createElement(B,null),Re.createElement(j,{className:\"information-container\"},Re.createElement(L,{mobile:12},Re.createElement(u,null))),de||fe||ye?Re.createElement(\"div\",{className:\"scheme-container\"},Re.createElement(L,{className:\"schemes wrapper\",mobile:12},de||fe?Re.createElement(\"div\",{className:\"schemes-server-container\"},de?Re.createElement($,null):null,fe?Re.createElement(U,null):null):null,ye?Re.createElement(V,null):null)):null,Re.createElement(z,null),Re.createElement(j,null,Re.createElement(L,{mobile:12,desktop:12},Re.createElement(w,null))),ee&&Re.createElement(j,{className:\"webhooks-container\"},Re.createElement(L,{mobile:12,desktop:12},Re.createElement(C,null))),Re.createElement(j,null,Re.createElement(L,{mobile:12,desktop:12},Re.createElement(x,null)))))}}const core_components=()=>({components:{App:JO,authorizationPopup:AuthorizationPopup,authorizeBtn:AuthorizeBtn,AuthorizeBtnContainer,authorizeOperationBtn:AuthorizeOperationBtn,auths:Auths,AuthItem:auth_item_Auths,authError:AuthError,oauth2:Oauth2,apiKeyAuth:ApiKeyAuth,basicAuth:BasicAuth,clear:Clear,liveResponse:LiveResponse,InitializedInput,info:tA,InfoContainer,InfoUrl,InfoBasePath,Contact:rA,License:nA,JumpToPath,CopyToClipboardBtn,onlineValidatorBadge:OnlineValidatorBadge,operations:Operations,operation:operation_Operation,OperationSummary,OperationSummaryMethod,OperationSummaryPath,responses:responses_Responses,response:response_Response,ResponseExtension:response_extension,responseBody:ResponseBody,parameters:Parameters,parameterRow:ParameterRow,execute:Execute,headers:headers_Headers,errors:Errors,contentType:ContentType,overview:Overview,footer:Footer,FilterContainer,ParamBody,curl:Curl,Property:property,TryItOutButton,Markdown:kP,BaseLayout,VersionPragmaFilter,VersionStamp:version_stamp,OperationExt:operation_extensions,OperationExtRow:operation_extension_row,ParameterExt:parameter_extension,ParameterIncludeEmpty,OperationTag,OperationContainer,OpenAPIVersion:openapi_version,DeepLink:deep_link,SvgAssets:svg_assets,Example:example_Example,ExamplesSelect,ExamplesSelectValueRetainer}}),form_components=()=>({components:{..._e}}),base=()=>[configsPlugin,util,logs,view,view_legacy,plugins_spec,err,icons,plugins_layout,json_schema_5,json_schema_5_samples,core_components,form_components,swagger_client,auth,downloadUrlPlugin,deep_linking,filter,on_complete,plugins_request_snippets,syntax_highlighting,versions,safe_render()],OP=(0,ze.Map)();function onlyOAS3(s){return(o,i)=>(...a)=>{if(i.getSystem().specSelectors.isOAS3()){const o=s(...a);return\"function\"==typeof o?o(i):o}return o(...a)}}const AP=onlyOAS3(xs()(null)),CP=onlyOAS3(((s,o)=>s=>s.getSystem().specSelectors.findSchema(o))),jP=onlyOAS3((()=>s=>{const o=s.getSystem().specSelectors.specJson().getIn([\"components\",\"schemas\"]);return ze.Map.isMap(o)?o:OP})),PP=onlyOAS3((()=>s=>s.getSystem().specSelectors.specJson().hasIn([\"servers\",0]))),IP=onlyOAS3(Ut(Ns,(s=>s.getIn([\"components\",\"securitySchemes\"])||null))),wrap_selectors_validOperationMethods=(s,o)=>(i,...a)=>o.specSelectors.isOAS3()?o.oas3Selectors.validOperationMethods():s(...a),TP=AP,NP=AP,MP=AP,RP=AP,DP=AP;const LP=function wrap_selectors_onlyOAS3(s){return(o,i)=>(...a)=>{if(i.getSystem().specSelectors.isOAS3()){let o=i.getState().getIn([\"spec\",\"resolvedSubtrees\",\"components\",\"securitySchemes\"]);return s(i,o,...a)}return o(...a)}}(Ut((s=>s),(({specSelectors:s})=>s.securityDefinitions()),((s,o)=>{let i=(0,ze.List)();return o?(o.entrySeq().forEach((([s,o])=>{const a=o?.get(\"type\");if(\"oauth2\"===a&&o.get(\"flows\").entrySeq().forEach((([a,u])=>{let _=(0,ze.fromJS)({flow:a,authorizationUrl:u.get(\"authorizationUrl\"),tokenUrl:u.get(\"tokenUrl\"),scopes:u.get(\"scopes\"),type:o.get(\"type\"),description:o.get(\"description\")});i=i.push(new ze.Map({[s]:_.filter((s=>void 0!==s))}))})),\"http\"!==a&&\"apiKey\"!==a||(i=i.push(new ze.Map({[s]:o}))),\"openIdConnect\"===a&&o.get(\"openIdConnectData\")){let a=o.get(\"openIdConnectData\");(a.get(\"grant_types_supported\")||[\"authorization_code\",\"implicit\"]).forEach((u=>{let _=a.get(\"scopes_supported\")&&a.get(\"scopes_supported\").reduce(((s,o)=>s.set(o,\"\")),new ze.Map),w=(0,ze.fromJS)({flow:u,authorizationUrl:a.get(\"authorization_endpoint\"),tokenUrl:a.get(\"token_endpoint\"),scopes:_,type:\"oauth2\",openIdConnectUrl:o.get(\"openIdConnectUrl\")});i=i.push(new ze.Map({[s]:w.filter((s=>void 0!==s))}))}))}})),i):i})));function OAS3ComponentWrapFactory(s){return(o,i)=>a=>\"function\"==typeof i.specSelectors?.isOAS3?i.specSelectors.isOAS3()?Re.createElement(s,Mn()({},a,i,{Ori:o})):Re.createElement(o,a):(console.warn(\"OAS3 wrapper: couldn't get spec\"),null)}const FP=(0,ze.Map)(),selectors_isSwagger2=()=>s=>function isSwagger2(s){const o=s.get(\"swagger\");return\"string\"==typeof o&&\"2.0\"===o}(s.getSystem().specSelectors.specJson()),selectors_isOAS30=()=>s=>function isOAS30(s){const o=s.get(\"openapi\");return\"string\"==typeof o&&/^3\\.0\\.(?:[1-9]\\d*|0)$/.test(o)}(s.getSystem().specSelectors.specJson()),selectors_isOAS3=()=>s=>s.getSystem().specSelectors.isOAS30();function selectors_onlyOAS3(s){return(o,...i)=>a=>{if(a.specSelectors.isOAS3()){const u=s(o,...i);return\"function\"==typeof u?u(a):u}return null}}const BP=selectors_onlyOAS3((()=>s=>s.specSelectors.specJson().get(\"servers\",FP))),findSchema=(s,o)=>{const i=s.getIn([\"resolvedSubtrees\",\"components\",\"schemas\",o],null),a=s.getIn([\"json\",\"components\",\"schemas\",o],null);return i||a||null},$P=selectors_onlyOAS3(((s,{callbacks:o,specPath:i})=>s=>{const a=s.specSelectors.validOperationMethods();return ze.Map.isMap(o)?o.reduce(((s,o,u)=>{if(!ze.Map.isMap(o))return s;const _=o.reduce(((s,o,_)=>{if(!ze.Map.isMap(o))return s;const w=o.entrySeq().filter((([s])=>a.includes(s))).map((([s,o])=>({operation:(0,ze.Map)({operation:o}),method:s,path:_,callbackName:u,specPath:i.concat([u,_,s])})));return s.concat(w)}),(0,ze.List)());return s.concat(_)}),(0,ze.List)()).groupBy((s=>s.callbackName)).map((s=>s.toArray())).toObject():{}})),callbacks=({callbacks:s,specPath:o,specSelectors:i,getComponent:a})=>{const u=i.callbacksOperations({callbacks:s,specPath:o}),_=Object.keys(u),w=a(\"OperationContainer\",!0);return 0===_.length?Re.createElement(\"span\",null,\"No callbacks\"):Re.createElement(\"div\",null,_.map((s=>Re.createElement(\"div\",{key:`${s}`},Re.createElement(\"h2\",null,s),u[s].map((o=>Re.createElement(w,{key:`${s}-${o.path}-${o.method}`,op:o.operation,tag:\"callbacks\",method:o.method,path:o.path,specPath:o.specPath,allowTryItOut:!1})))))))},getDefaultRequestBodyValue=(s,o,i,a)=>{const u=s.getIn([\"content\",o])??(0,ze.OrderedMap)(),_=u.get(\"schema\",(0,ze.OrderedMap)()).toJS(),w=void 0!==u.get(\"examples\"),x=u.get(\"example\"),C=w?u.getIn([\"examples\",i,\"value\"]):x;return stringify(a.getSampleSchema(_,o,{includeWriteOnly:!0},C))},components_request_body=({userHasEditedBody:s,requestBody:o,requestBodyValue:i,requestBodyInclusionSetting:a,requestBodyErrors:u,getComponent:_,getConfigs:w,specSelectors:x,fn:C,contentType:j,isExecute:L,specPath:B,onChange:$,onChangeIncludeEmpty:U,activeExamplesKey:V,updateActiveExamplesKey:z,setRetainRequestBodyValueFlag:Y})=>{const handleFile=s=>{$(s.target.files[0])},setIsIncludedOptions=s=>{let o={key:s,shouldDispatchInit:!1,defaultValue:!0};return\"no value\"===a.get(s,\"no value\")&&(o.shouldDispatchInit=!0),o},Z=_(\"Markdown\",!0),ee=_(\"modelExample\"),ie=_(\"RequestBodyEditor\"),ae=_(\"HighlightCode\",!0),ce=_(\"ExamplesSelectValueRetainer\"),le=_(\"Example\"),pe=_(\"ParameterIncludeEmpty\"),{showCommonExtensions:de}=w(),fe=o?.get(\"description\")??null,ye=o?.get(\"content\")??new ze.OrderedMap;j=j||ye.keySeq().first()||\"\";const be=ye.get(j)??(0,ze.OrderedMap)(),_e=be.get(\"schema\",(0,ze.OrderedMap)()),Se=be.get(\"examples\",null),we=Se?.map(((s,i)=>{const a=s?.get(\"value\",null);return a&&(s=s.set(\"value\",getDefaultRequestBodyValue(o,j,i,C),a)),s}));u=ze.List.isList(u)?u:(0,ze.List)();if(C.isFileUploadIntended(be?.get(\"schema\"),j)){const s=_(\"Input\");return L?Re.createElement(s,{type:\"file\",onChange:handleFile}):Re.createElement(\"i\",null,\"Example values are not available for \",Re.createElement(\"code\",null,j),\" media types.\")}if(!be.size)return null;if(C.hasSchemaType(be.get(\"schema\"),\"object\")&&(\"application/x-www-form-urlencoded\"===j||0===j.indexOf(\"multipart/\"))&&_e.get(\"properties\",(0,ze.OrderedMap)()).size>0){const s=_(\"JsonSchemaForm\"),o=_(\"ParameterExt\"),j=_e.get(\"properties\",(0,ze.OrderedMap)());return i=ze.Map.isMap(i)?i:(0,ze.OrderedMap)(),Re.createElement(\"div\",{className:\"table-container\"},fe&&Re.createElement(Z,{source:fe}),Re.createElement(\"table\",null,Re.createElement(\"tbody\",null,ze.Map.isMap(j)&&j.entrySeq().map((([j,V])=>{if(V.get(\"readOnly\"))return;const z=V.get(\"oneOf\")?.get(0)?.toJS(),Y=V.get(\"anyOf\")?.get(0)?.toJS();V=(0,ze.fromJS)(C.mergeJsonSchema(V.toJS(),z??Y??{}));let ie=de?getCommonExtensions(V):null;const ae=_e.get(\"required\",(0,ze.List)()).includes(j),ce=C.getSchemaObjectType(V),le=C.getSchemaObjectTypeLabel(V),fe=C.getSchemaObjectType(V?.get(\"items\")),ye=V.get(\"format\"),be=V.get(\"description\"),Se=i.getIn([j,\"value\"]),we=i.getIn([j,\"errors\"])||u,xe=a.get(j)||!1;let Pe=C.getSampleSchema(V,!1,{includeWriteOnly:!0});!1===Pe&&(Pe=\"false\"),0===Pe&&(Pe=\"0\"),\"string\"!=typeof Pe&&\"object\"===ce&&(Pe=stringify(Pe)),\"string\"==typeof Pe&&\"array\"===ce&&(Pe=JSON.parse(Pe));const Te=C.isFileUploadIntended(V),$e=Re.createElement(s,{fn:C,dispatchInitialValue:!Te,schema:V,description:j,getComponent:_,value:void 0===Se?Pe:Se,required:ae,errors:we,onChange:s=>{$(s,[j])}});return Re.createElement(\"tr\",{key:j,className:\"parameters\",\"data-property-name\":j},Re.createElement(\"td\",{className:\"parameters-col_name\"},Re.createElement(\"div\",{className:ae?\"parameter__name required\":\"parameter__name\"},j,ae?Re.createElement(\"span\",null,\" *\"):null),Re.createElement(\"div\",{className:\"parameter__type\"},le,ye&&Re.createElement(\"span\",{className:\"prop-format\"},\"($\",ye,\")\"),de&&ie.size?ie.entrySeq().map((([s,i])=>Re.createElement(o,{key:`${s}-${i}`,xKey:s,xVal:i}))):null),Re.createElement(\"div\",{className:\"parameter__deprecated\"},V.get(\"deprecated\")?\"deprecated\":null)),Re.createElement(\"td\",{className:\"parameters-col_description\"},Re.createElement(Z,{source:be}),L?Re.createElement(\"div\",null,\"object\"===ce||\"object\"===fe?Re.createElement(ee,{getComponent:_,specPath:B.push(\"schema\"),getConfigs:w,isExecute:L,specSelectors:x,schema:V,example:$e}):$e,ae?null:Re.createElement(pe,{onChange:s=>U(j,s),isIncluded:xe,isIncludedOptions:setIsIncludedOptions(j),isDisabled:Array.isArray(Se)?0!==Se.length:!isEmptyValue(Se)})):null))})))))}const xe=getDefaultRequestBodyValue(o,j,V,C);let Pe=null;getKnownSyntaxHighlighterLanguage(xe)&&(Pe=\"json\");const Te=L?Re.createElement(ie,{value:i,errors:u,defaultValue:xe,onChange:$,getComponent:_}):Re.createElement(ae,{className:\"body-param__example\",language:Pe},stringify(i)||xe);return Re.createElement(\"div\",null,fe&&Re.createElement(Z,{source:fe}),we?Re.createElement(ce,{userHasEditedBody:s,examples:we,currentKey:V,currentUserInputValue:i,onSelect:s=>{z(s)},updateValue:$,defaultToFirstExample:!0,getComponent:_,setRetainRequestBodyValueFlag:Y}):null,Re.createElement(ee,{getComponent:_,getConfigs:w,specSelectors:x,expandDepth:1,isExecute:L,schema:be.get(\"schema\"),specPath:B.push(\"content\",j,\"schema\"),example:Te,includeWriteOnly:!0}),we?Re.createElement(le,{example:we.get(V),getComponent:_,getConfigs:w}):null)};class operation_link_OperationLink extends Re.Component{render(){const{link:s,name:o,getComponent:i}=this.props,a=i(\"Markdown\",!0);let u=s.get(\"operationId\")||s.get(\"operationRef\"),_=s.get(\"parameters\")&&s.get(\"parameters\").toJS(),w=s.get(\"description\");return Re.createElement(\"div\",{className:\"operation-link\"},Re.createElement(\"div\",{className:\"description\"},Re.createElement(\"b\",null,Re.createElement(\"code\",null,o)),w?Re.createElement(a,{source:w}):null),Re.createElement(\"pre\",null,\"Operation `\",u,\"`\",Re.createElement(\"br\",null),Re.createElement(\"br\",null),\"Parameters \",function padString(s,o){if(\"string\"!=typeof o)return\"\";return o.split(\"\\n\").map(((o,i)=>i>0?Array(s+1).join(\" \")+o:o)).join(\"\\n\")}(0,JSON.stringify(_,null,2))||\"{}\",Re.createElement(\"br\",null)))}}const qP=operation_link_OperationLink,components_servers=({servers:s,currentServer:o,setSelectedServer:i,setServerVariableValue:a,getServerVariable:u,getEffectiveServerValue:_})=>{const w=(s.find((s=>s.get(\"url\")===o))||(0,ze.OrderedMap)()).get(\"variables\")||(0,ze.OrderedMap)(),x=0!==w.size;(0,Re.useEffect)((()=>{o||i(s.first()?.get(\"url\"))}),[]),(0,Re.useEffect)((()=>{const u=s.find((s=>s.get(\"url\")===o));if(!u)return void i(s.first().get(\"url\"));(u.get(\"variables\")||(0,ze.OrderedMap)()).map(((s,i)=>{a({server:o,key:i,val:s.get(\"default\")||\"\"})}))}),[o,s]);const C=(0,Re.useCallback)((s=>{i(s.target.value)}),[i]),j=(0,Re.useCallback)((s=>{const i=s.target.getAttribute(\"data-variable\"),u=s.target.value;a({server:o,key:i,val:u})}),[a,o]);return Re.createElement(\"div\",{className:\"servers\"},Re.createElement(\"label\",{htmlFor:\"servers\"},Re.createElement(\"select\",{onChange:C,value:o,id:\"servers\"},s.valueSeq().map((s=>Re.createElement(\"option\",{value:s.get(\"url\"),key:s.get(\"url\")},s.get(\"url\"),s.get(\"description\")&&` - ${s.get(\"description\")}`))).toArray())),x&&Re.createElement(\"div\",null,Re.createElement(\"div\",{className:\"computed-url\"},\"Computed URL:\",Re.createElement(\"code\",null,_(o))),Re.createElement(\"h4\",null,\"Server variables\"),Re.createElement(\"table\",null,Re.createElement(\"tbody\",null,w.entrySeq().map((([s,i])=>Re.createElement(\"tr\",{key:s},Re.createElement(\"td\",null,s),Re.createElement(\"td\",null,i.get(\"enum\")?Re.createElement(\"select\",{\"data-variable\":s,onChange:j},i.get(\"enum\").map((i=>Re.createElement(\"option\",{selected:i===u(o,s),key:i,value:i},i)))):Re.createElement(\"input\",{type:\"text\",value:u(o,s)||\"\",onChange:j,\"data-variable\":s})))))))))};class ServersContainer extends Re.Component{render(){const{specSelectors:s,oas3Selectors:o,oas3Actions:i,getComponent:a}=this.props,u=s.servers(),_=a(\"Servers\");return u&&u.size?Re.createElement(\"div\",null,Re.createElement(\"span\",{className:\"servers-title\"},\"Servers\"),Re.createElement(_,{servers:u,currentServer:o.selectedServer(),setSelectedServer:i.setSelectedServer,setServerVariableValue:i.setServerVariableValue,getServerVariable:o.serverVariableValue,getEffectiveServerValue:o.serverEffectiveValue})):null}}const UP=Function.prototype;class RequestBodyEditor extends Re.PureComponent{static defaultProps={onChange:UP,userHasEditedBody:!1};constructor(s,o){super(s,o),this.state={value:stringify(s.value)||s.defaultValue},s.onChange(s.value)}applyDefaultValue=s=>{const{onChange:o,defaultValue:i}=s||this.props;return this.setState({value:i}),o(i)};onChange=s=>{this.props.onChange(stringify(s))};onDomChange=s=>{const o=s.target.value;this.setState({value:o},(()=>this.onChange(o)))};UNSAFE_componentWillReceiveProps(s){this.props.value!==s.value&&s.value!==this.state.value&&this.setState({value:stringify(s.value)}),!s.value&&s.defaultValue&&this.state.value&&this.applyDefaultValue(s)}render(){let{getComponent:s,errors:o}=this.props,{value:i}=this.state,a=o.size>0;const u=s(\"TextArea\");return Re.createElement(\"div\",{className:\"body-param\"},Re.createElement(u,{className:Jn()(\"body-param__text\",{invalid:a}),title:o.size?o.join(\", \"):\"\",value:i,onChange:this.onDomChange}))}}class HttpAuth extends Re.Component{constructor(s,o){super(s,o);let{name:i,schema:a}=this.props,u=this.getValue();this.state={name:i,schema:a,value:u}}getValue(){let{name:s,authorized:o}=this.props;return o&&o.getIn([s,\"value\"])}onChange=s=>{let{onChange:o}=this.props,{value:i,name:a}=s.target,u=Object.assign({},this.state.value);a?u[a]=i:u=i,this.setState({value:u},(()=>o(this.state)))};render(){let{schema:s,getComponent:o,errSelectors:i,name:a,authSelectors:u}=this.props;const _=o(\"Input\"),w=o(\"Row\"),x=o(\"Col\"),C=o(\"authError\"),j=o(\"Markdown\",!0),L=o(\"JumpToPath\",!0),B=(s.get(\"scheme\")||\"\").toLowerCase(),$=u.selectAuthPath(a);let U=this.getValue(),V=i.allErrors().filter((s=>s.get(\"authId\")===a));if(\"basic\"===B){let o=U?U.get(\"username\"):null;return Re.createElement(\"div\",null,Re.createElement(\"h4\",null,Re.createElement(\"code\",null,a),\"  (http, Basic)\",Re.createElement(L,{path:$})),o&&Re.createElement(\"h6\",null,\"Authorized\"),Re.createElement(w,null,Re.createElement(j,{source:s.get(\"description\")})),Re.createElement(w,null,Re.createElement(\"label\",{htmlFor:\"auth-basic-username\"},\"Username:\"),o?Re.createElement(\"code\",null,\" \",o,\" \"):Re.createElement(x,null,Re.createElement(_,{id:\"auth-basic-username\",type:\"text\",required:\"required\",name:\"username\",\"aria-label\":\"auth-basic-username\",onChange:this.onChange,autoFocus:!0}))),Re.createElement(w,null,Re.createElement(\"label\",{htmlFor:\"auth-basic-password\"},\"Password:\"),o?Re.createElement(\"code\",null,\" ****** \"):Re.createElement(x,null,Re.createElement(_,{id:\"auth-basic-password\",autoComplete:\"new-password\",name:\"password\",type:\"password\",\"aria-label\":\"auth-basic-password\",onChange:this.onChange}))),V.valueSeq().map(((s,o)=>Re.createElement(C,{error:s,key:o}))))}return\"bearer\"===B?Re.createElement(\"div\",null,Re.createElement(\"h4\",null,Re.createElement(\"code\",null,a),\"  (http, Bearer)\",Re.createElement(L,{path:$})),U&&Re.createElement(\"h6\",null,\"Authorized\"),Re.createElement(w,null,Re.createElement(j,{source:s.get(\"description\")})),Re.createElement(w,null,Re.createElement(\"label\",{htmlFor:\"auth-bearer-value\"},\"Value:\"),U?Re.createElement(\"code\",null,\" ****** \"):Re.createElement(x,null,Re.createElement(_,{id:\"auth-bearer-value\",type:\"text\",\"aria-label\":\"auth-bearer-value\",onChange:this.onChange,autoFocus:!0}))),V.valueSeq().map(((s,o)=>Re.createElement(C,{error:s,key:o})))):Re.createElement(\"div\",null,Re.createElement(\"em\",null,Re.createElement(\"b\",null,a),\" HTTP authentication: unsupported scheme \",`'${B}'`))}}class operation_servers_OperationServers extends Re.Component{setSelectedServer=s=>{const{path:o,method:i}=this.props;return this.forceUpdate(),this.props.setSelectedServer(s,`${o}:${i}`)};setServerVariableValue=s=>{const{path:o,method:i}=this.props;return this.forceUpdate(),this.props.setServerVariableValue({...s,namespace:`${o}:${i}`})};getSelectedServer=()=>{const{path:s,method:o}=this.props;return this.props.getSelectedServer(`${s}:${o}`)};getServerVariable=(s,o)=>{const{path:i,method:a}=this.props;return this.props.getServerVariable({namespace:`${i}:${a}`,server:s},o)};getEffectiveServerValue=s=>{const{path:o,method:i}=this.props;return this.props.getEffectiveServerValue({server:s,namespace:`${o}:${i}`})};render(){const{operationServers:s,pathServers:o,getComponent:i}=this.props;if(!s&&!o)return null;const a=i(\"Servers\"),u=s||o,_=s?\"operation\":\"path\";return Re.createElement(\"div\",{className:\"opblock-section operation-servers\"},Re.createElement(\"div\",{className:\"opblock-section-header\"},Re.createElement(\"div\",{className:\"tab-header\"},Re.createElement(\"h4\",{className:\"opblock-title\"},\"Servers\"))),Re.createElement(\"div\",{className:\"opblock-description-wrapper\"},Re.createElement(\"h4\",{className:\"message\"},\"These \",_,\"-level options override the global server options.\"),Re.createElement(a,{servers:u,currentServer:this.getSelectedServer(),setSelectedServer:this.setSelectedServer,setServerVariableValue:this.setServerVariableValue,getServerVariable:this.getServerVariable,getEffectiveServerValue:this.getEffectiveServerValue})))}}const VP={Callbacks:callbacks,HttpAuth,RequestBody:components_request_body,Servers:components_servers,ServersContainer,RequestBodyEditor,OperationServers:operation_servers_OperationServers,operationLink:qP},zP=new Remarkable(\"commonmark\");zP.block.ruler.enable([\"table\"]),zP.set({linkTarget:\"_blank\"});const WP=OAS3ComponentWrapFactory((({source:s,className:o=\"\",getConfigs:i=()=>({useUnsafeMarkdown:!1})})=>{if(\"string\"!=typeof s)return null;if(s){const{useUnsafeMarkdown:a}=i(),u=sanitizer(zP.render(s),{useUnsafeMarkdown:a});let _;return\"string\"==typeof u&&(_=u.trim()),Re.createElement(\"div\",{dangerouslySetInnerHTML:{__html:_},className:Jn()(o,\"renderedMarkdown\")})}return null})),JP=OAS3ComponentWrapFactory((({Ori:s,...o})=>{const{schema:i,getComponent:a,errSelectors:u,authorized:_,onAuthChange:w,name:x,authSelectors:C}=o,j=a(\"HttpAuth\");return\"http\"===i.get(\"type\")?Re.createElement(j,{key:x,schema:i,name:x,errSelectors:u,authorized:_,getComponent:a,onChange:w,authSelectors:C}):Re.createElement(s,o)})),HP=OAS3ComponentWrapFactory(OnlineValidatorBadge);class ModelComponent extends Re.Component{render(){let{getConfigs:s,schema:o,Ori:i}=this.props,a=[\"model-box\"],u=null;return!0===o.get(\"deprecated\")&&(a.push(\"deprecated\"),u=Re.createElement(\"span\",{className:\"model-deprecated-warning\"},\"Deprecated:\")),Re.createElement(\"div\",{className:a.join(\" \")},u,Re.createElement(i,Mn()({},this.props,{getConfigs:s,depth:1,expandDepth:this.props.expandDepth||0})))}}const KP=OAS3ComponentWrapFactory(ModelComponent),GP=OAS3ComponentWrapFactory((({Ori:s,...o})=>{const{schema:i,getComponent:a,errors:u,onChange:_,fn:w}=o,x=w.isFileUploadIntended(i),C=a(\"Input\");return x?Re.createElement(C,{type:\"file\",className:u.length?\"invalid\":\"\",title:u.length?u:\"\",onChange:s=>{_(s.target.files[0])},disabled:s.isDisabled}):Re.createElement(s,o)})),YP={Markdown:WP,AuthItem:JP,OpenAPIVersion:function OAS30ComponentWrapFactory(s){return(o,i)=>a=>\"function\"==typeof i.specSelectors?.isOAS30?i.specSelectors.isOAS30()?Re.createElement(s,Mn()({},a,i,{Ori:o})):Re.createElement(o,a):(console.warn(\"OAS30 wrapper: couldn't get spec\"),null)}((s=>{const{Ori:o}=s;return Re.createElement(o,{oasVersion:\"3.0\"})})),JsonSchema_string:GP,model:KP,onlineValidatorBadge:HP},XP=\"oas3_set_servers\",QP=\"oas3_set_request_body_value\",ZP=\"oas3_set_request_body_retain_flag\",eI=\"oas3_set_request_body_inclusion\",tI=\"oas3_set_active_examples_member\",rI=\"oas3_set_request_content_type\",nI=\"oas3_set_response_content_type\",sI=\"oas3_set_server_variable_value\",oI=\"oas3_set_request_body_validate_error\",iI=\"oas3_clear_request_body_validate_error\",aI=\"oas3_clear_request_body_value\";function setSelectedServer(s,o){return{type:XP,payload:{selectedServerUrl:s,namespace:o}}}function setRequestBodyValue({value:s,pathMethod:o}){return{type:QP,payload:{value:s,pathMethod:o}}}const setRetainRequestBodyValueFlag=({value:s,pathMethod:o})=>({type:ZP,payload:{value:s,pathMethod:o}});function setRequestBodyInclusion({value:s,pathMethod:o,name:i}){return{type:eI,payload:{value:s,pathMethod:o,name:i}}}function setActiveExamplesMember({name:s,pathMethod:o,contextType:i,contextName:a}){return{type:tI,payload:{name:s,pathMethod:o,contextType:i,contextName:a}}}function setRequestContentType({value:s,pathMethod:o}){return{type:rI,payload:{value:s,pathMethod:o}}}function setResponseContentType({value:s,path:o,method:i}){return{type:nI,payload:{value:s,path:o,method:i}}}function setServerVariableValue({server:s,namespace:o,key:i,val:a}){return{type:sI,payload:{server:s,namespace:o,key:i,val:a}}}const setRequestBodyValidateError=({path:s,method:o,validationErrors:i})=>({type:oI,payload:{path:s,method:o,validationErrors:i}}),clearRequestBodyValidateError=({path:s,method:o})=>({type:iI,payload:{path:s,method:o}}),initRequestBodyValidateError=({pathMethod:s})=>({type:iI,payload:{path:s[0],method:s[1]}}),clearRequestBodyValue=({pathMethod:s})=>({type:aI,payload:{pathMethod:s}});var cI=__webpack_require__(60680),lI=__webpack_require__.n(cI);const oas3_selectors_onlyOAS3=s=>(o,...i)=>a=>{if(a.getSystem().specSelectors.isOAS3()){const u=s(o,...i);return\"function\"==typeof u?u(a):u}return null};const uI=oas3_selectors_onlyOAS3(((s,o)=>{const i=o?[o,\"selectedServer\"]:[\"selectedServer\"];return s.getIn(i)||\"\"})),pI=oas3_selectors_onlyOAS3(((s,o,i)=>s.getIn([\"requestData\",o,i,\"bodyValue\"])||null)),hI=oas3_selectors_onlyOAS3(((s,o,i)=>s.getIn([\"requestData\",o,i,\"retainBodyValue\"])||!1)),selectDefaultRequestBodyValue=(s,o,i)=>s=>{const{oas3Selectors:a,specSelectors:u,fn:_}=s.getSystem();if(u.isOAS3()){const s=a.requestContentType(o,i);if(s)return getDefaultRequestBodyValue(u.specResolvedSubtree([\"paths\",o,i,\"requestBody\"]),s,a.activeExamplesMember(o,i,\"requestBody\",\"requestBody\"),_)}return null},dI=oas3_selectors_onlyOAS3(((s,o,i)=>s=>{const{oas3Selectors:a,specSelectors:u,fn:_}=s;let w=!1;const x=a.requestContentType(o,i);let C=a.requestBodyValue(o,i);const j=u.specResolvedSubtree([\"paths\",o,i,\"requestBody\"]);if(!j)return!1;if(ze.Map.isMap(C)&&(C=stringify(C.mapEntries((s=>ze.Map.isMap(s[1])?[s[0],s[1].get(\"value\")]:s)).toJS())),ze.List.isList(C)&&(C=stringify(C)),x){const s=getDefaultRequestBodyValue(j,x,a.activeExamplesMember(o,i,\"requestBody\",\"requestBody\"),_);w=!!C&&C!==s}return w})),fI=oas3_selectors_onlyOAS3(((s,o,i)=>s.getIn([\"requestData\",o,i,\"bodyInclusion\"])||(0,ze.Map)())),mI=oas3_selectors_onlyOAS3(((s,o,i)=>s.getIn([\"requestData\",o,i,\"errors\"])||null)),gI=oas3_selectors_onlyOAS3(((s,o,i,a,u)=>s.getIn([\"examples\",o,i,a,u,\"activeExample\"])||null)),yI=oas3_selectors_onlyOAS3(((s,o,i)=>s.getIn([\"requestData\",o,i,\"requestContentType\"])||null)),vI=oas3_selectors_onlyOAS3(((s,o,i)=>s.getIn([\"requestData\",o,i,\"responseContentType\"])||null)),bI=oas3_selectors_onlyOAS3(((s,o,i)=>{let a;if(\"string\"!=typeof o){const{server:s,namespace:u}=o;a=u?[u,\"serverVariableValues\",s,i]:[\"serverVariableValues\",s,i]}else{a=[\"serverVariableValues\",o,i]}return s.getIn(a)||null})),_I=oas3_selectors_onlyOAS3(((s,o)=>{let i;if(\"string\"!=typeof o){const{server:s,namespace:a}=o;i=a?[a,\"serverVariableValues\",s]:[\"serverVariableValues\",s]}else{i=[\"serverVariableValues\",o]}return s.getIn(i)||(0,ze.OrderedMap)()})),SI=oas3_selectors_onlyOAS3(((s,o)=>{var i,a;if(\"string\"!=typeof o){const{server:u,namespace:_}=o;a=u,i=_?s.getIn([_,\"serverVariableValues\",a]):s.getIn([\"serverVariableValues\",a])}else a=o,i=s.getIn([\"serverVariableValues\",a]);i=i||(0,ze.OrderedMap)();let u=a;return i.map(((s,o)=>{u=u.replace(new RegExp(`{${lI()(o)}}`,\"g\"),s)})),u})),EI=function validateRequestBodyIsRequired(s){return(...o)=>i=>{const a=i.getSystem().specSelectors.specJson();let u=[...o][1]||[];return!a.getIn([\"paths\",...u,\"requestBody\",\"required\"])||s(...o)}}(((s,o)=>((s,o)=>(o=o||[],!!s.getIn([\"requestData\",...o,\"bodyValue\"])))(s,o))),validateShallowRequired=(s,{oas3RequiredRequestBodyContentType:o,oas3RequestContentType:i,oas3RequestBodyValue:a})=>{let u=[];if(!ze.Map.isMap(a))return u;let _=[];return Object.keys(o.requestContentType).forEach((s=>{if(s===i){o.requestContentType[s].forEach((s=>{_.indexOf(s)<0&&_.push(s)}))}})),_.forEach((s=>{a.getIn([s,\"value\"])||u.push(s)})),u},wI=xs()([\"get\",\"put\",\"post\",\"delete\",\"options\",\"head\",\"patch\",\"trace\"]),xI={[XP]:(s,{payload:{selectedServerUrl:o,namespace:i}})=>{const a=i?[i,\"selectedServer\"]:[\"selectedServer\"];return s.setIn(a,o)},[QP]:(s,{payload:{value:o,pathMethod:i}})=>{let[a,u]=i;if(!ze.Map.isMap(o))return s.setIn([\"requestData\",a,u,\"bodyValue\"],o);let _=s.getIn([\"requestData\",a,u,\"bodyValue\"])||(0,ze.Map)();ze.Map.isMap(_)||(_=(0,ze.Map)());let w=_;const[...x]=o.keys();return x.forEach((s=>{let i=o.getIn([s]);w.has(s)&&ze.Map.isMap(i)||(w=w.setIn([s,\"value\"],i))})),s.setIn([\"requestData\",a,u,\"bodyValue\"],w)},[ZP]:(s,{payload:{value:o,pathMethod:i}})=>{let[a,u]=i;return s.setIn([\"requestData\",a,u,\"retainBodyValue\"],o)},[eI]:(s,{payload:{value:o,pathMethod:i,name:a}})=>{let[u,_]=i;return s.setIn([\"requestData\",u,_,\"bodyInclusion\",a],o)},[tI]:(s,{payload:{name:o,pathMethod:i,contextType:a,contextName:u}})=>{let[_,w]=i;return s.setIn([\"examples\",_,w,a,u,\"activeExample\"],o)},[rI]:(s,{payload:{value:o,pathMethod:i}})=>{let[a,u]=i;return s.setIn([\"requestData\",a,u,\"requestContentType\"],o)},[nI]:(s,{payload:{value:o,path:i,method:a}})=>s.setIn([\"requestData\",i,a,\"responseContentType\"],o),[sI]:(s,{payload:{server:o,namespace:i,key:a,val:u}})=>{const _=i?[i,\"serverVariableValues\",o,a]:[\"serverVariableValues\",o,a];return s.setIn(_,u)},[oI]:(s,{payload:{path:o,method:i,validationErrors:a}})=>{let u=[];if(u.push(\"Required field is not provided\"),a.missingBodyValue)return s.setIn([\"requestData\",o,i,\"errors\"],(0,ze.fromJS)(u));if(a.missingRequiredKeys&&a.missingRequiredKeys.length>0){const{missingRequiredKeys:_}=a;return s.updateIn([\"requestData\",o,i,\"bodyValue\"],(0,ze.fromJS)({}),(s=>_.reduce(((s,o)=>s.setIn([o,\"errors\"],(0,ze.fromJS)(u))),s)))}return console.warn(\"unexpected result: SET_REQUEST_BODY_VALIDATE_ERROR\"),s},[iI]:(s,{payload:{path:o,method:i}})=>{const a=s.getIn([\"requestData\",o,i,\"bodyValue\"]);if(!ze.Map.isMap(a))return s.setIn([\"requestData\",o,i,\"errors\"],(0,ze.fromJS)([]));const[...u]=a.keys();return u?s.updateIn([\"requestData\",o,i,\"bodyValue\"],(0,ze.fromJS)({}),(s=>u.reduce(((s,o)=>s.setIn([o,\"errors\"],(0,ze.fromJS)([]))),s))):s},[aI]:(s,{payload:{pathMethod:o}})=>{let[i,a]=o;const u=s.getIn([\"requestData\",i,a,\"bodyValue\"]);return u?ze.Map.isMap(u)?s.setIn([\"requestData\",i,a,\"bodyValue\"],(0,ze.Map)()):s.setIn([\"requestData\",i,a,\"bodyValue\"],\"\"):s}};function oas3({getSystem:s}){const o=(s=>(o,i=null)=>{const{getConfigs:a,fn:u}=s(),{fileUploadMediaTypes:_}=a();if(\"string\"==typeof i&&_.some((s=>i.startsWith(s))))return!0;const w=ze.Map.isMap(o);if(!w&&!as()(o))return!1;const x=w?o.get(\"format\"):o.format;return u.hasSchemaType(o,\"string\")&&[\"binary\",\"byte\"].includes(x)})(s);return{components:VP,wrapComponents:YP,statePlugins:{spec:{wrapSelectors:Se,selectors:xe},auth:{wrapSelectors:we},oas3:{actions:{...Pe},reducers:xI,selectors:{...Te}}},fn:{isFileUploadIntended:o,isFileUploadIntendedOAS30:o}}}const webhooks=({specSelectors:s,getComponent:o})=>{const i=s.selectWebhooksOperations();if(!i)return null;const a=Object.keys(i),u=o(\"OperationContainer\",!0);return 0===a.length?null:Re.createElement(\"div\",{className:\"webhooks\"},Re.createElement(\"h2\",null,\"Webhooks\"),a.map((s=>Re.createElement(\"div\",{key:`${s}-webhook`},i[s].map((o=>Re.createElement(u,{key:`${s}-${o.method}-webhook`,op:o.operation,tag:\"webhooks\",method:o.method,path:s,specPath:(0,ze.List)(o.specPath),allowTryItOut:!1})))))))},oas31_components_license=({getComponent:s,specSelectors:o})=>{const i=o.selectLicenseNameField(),a=o.selectLicenseUrl(),u=s(\"Link\");return Re.createElement(\"div\",{className:\"info__license\"},a?Re.createElement(\"div\",{className:\"info__license__url\"},Re.createElement(u,{target:\"_blank\",href:sanitizeUrl(a)},i)):Re.createElement(\"span\",null,i))},oas31_components_contact=({getComponent:s,specSelectors:o})=>{const i=o.selectContactNameField(),a=o.selectContactUrl(),u=o.selectContactEmailField(),_=s(\"Link\");return Re.createElement(\"div\",{className:\"info__contact\"},a&&Re.createElement(\"div\",null,Re.createElement(_,{href:sanitizeUrl(a),target:\"_blank\"},i,\" - Website\")),u&&Re.createElement(_,{href:sanitizeUrl(`mailto:${u}`)},a?`Send email to ${i}`:`Contact ${i}`))},oas31_components_info=({getComponent:s,specSelectors:o})=>{const i=o.version(),a=o.url(),u=o.basePath(),_=o.host(),w=o.selectInfoSummaryField(),x=o.selectInfoDescriptionField(),C=o.selectInfoTitleField(),j=o.selectInfoTermsOfServiceUrl(),L=o.selectExternalDocsUrl(),B=o.selectExternalDocsDescriptionField(),$=o.contact(),U=o.license(),V=s(\"Markdown\",!0),z=s(\"Link\"),Y=s(\"VersionStamp\"),Z=s(\"OpenAPIVersion\"),ee=s(\"InfoUrl\"),ie=s(\"InfoBasePath\"),ae=s(\"License\",!0),ce=s(\"Contact\",!0),le=s(\"JsonSchemaDialect\",!0);return Re.createElement(\"div\",{className:\"info\"},Re.createElement(\"hgroup\",{className:\"main\"},Re.createElement(\"h1\",{className:\"title\"},C,Re.createElement(\"span\",null,i&&Re.createElement(Y,{version:i}),Re.createElement(Z,{oasVersion:\"3.1\"}))),(_||u)&&Re.createElement(ie,{host:_,basePath:u}),a&&Re.createElement(ee,{getComponent:s,url:a})),w&&Re.createElement(\"p\",{className:\"info__summary\"},w),Re.createElement(\"div\",{className:\"info__description description\"},Re.createElement(V,{source:x})),j&&Re.createElement(\"div\",{className:\"info__tos\"},Re.createElement(z,{target:\"_blank\",href:sanitizeUrl(j)},\"Terms of service\")),$.size>0&&Re.createElement(ce,null),U.size>0&&Re.createElement(ae,null),L&&Re.createElement(z,{className:\"info__extdocs\",target:\"_blank\",href:sanitizeUrl(L)},B||L),Re.createElement(le,null))},json_schema_dialect=({getComponent:s,specSelectors:o})=>{const i=o.selectJsonSchemaDialectField(),a=o.selectJsonSchemaDialectDefault(),u=s(\"Link\");return Re.createElement(Re.Fragment,null,i&&i===a&&Re.createElement(\"p\",{className:\"info__jsonschemadialect\"},\"JSON Schema dialect:\",\" \",Re.createElement(u,{target:\"_blank\",href:sanitizeUrl(i)},i)),i&&i!==a&&Re.createElement(\"div\",{className:\"error-wrapper\"},Re.createElement(\"div\",{className:\"no-margin\"},Re.createElement(\"div\",{className:\"errors\"},Re.createElement(\"div\",{className:\"errors-wrapper\"},Re.createElement(\"h4\",{className:\"center\"},\"Warning\"),Re.createElement(\"p\",{className:\"message\"},Re.createElement(\"strong\",null,\"OpenAPI.jsonSchemaDialect\"),\" field contains a value different from the default value of\",\" \",Re.createElement(u,{target:\"_blank\",href:a},a),\". Values different from the default one are currently not supported. Please either omit the field or provide it with the default value.\"))))))},version_pragma_filter=({bypass:s,isSwagger2:o,isOAS3:i,isOAS31:a,alsoShow:u,children:_})=>s?Re.createElement(\"div\",null,_):o&&(i||a)?Re.createElement(\"div\",{className:\"version-pragma\"},u,Re.createElement(\"div\",{className:\"version-pragma__message version-pragma__message--ambiguous\"},Re.createElement(\"div\",null,Re.createElement(\"h3\",null,\"Unable to render this definition\"),Re.createElement(\"p\",null,Re.createElement(\"code\",null,\"swagger\"),\" and \",Re.createElement(\"code\",null,\"openapi\"),\" fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields.\"),Re.createElement(\"p\",null,\"Supported version fields are \",Re.createElement(\"code\",null,'swagger: \"2.0\"'),\" and those that match \",Re.createElement(\"code\",null,\"openapi: 3.x.y\"),\" (for example,\",\" \",Re.createElement(\"code\",null,\"openapi: 3.1.0\"),\").\")))):o||i||a?Re.createElement(\"div\",null,_):Re.createElement(\"div\",{className:\"version-pragma\"},u,Re.createElement(\"div\",{className:\"version-pragma__message version-pragma__message--missing\"},Re.createElement(\"div\",null,Re.createElement(\"h3\",null,\"Unable to render this definition\"),Re.createElement(\"p\",null,\"The provided definition does not specify a valid version field.\"),Re.createElement(\"p\",null,\"Please indicate a valid Swagger or OpenAPI version field. Supported version fields are \",Re.createElement(\"code\",null,'swagger: \"2.0\"'),\" and those that match \",Re.createElement(\"code\",null,\"openapi: 3.x.y\"),\" (for example,\",\" \",Re.createElement(\"code\",null,\"openapi: 3.1.0\"),\").\")))),getModelName=s=>\"string\"==typeof s&&s.includes(\"#/components/schemas/\")?(s=>{const o=s.replace(/~1/g,\"/\").replace(/~0/g,\"~\");try{return decodeURIComponent(o)}catch{return o}})(s.replace(/^.*#\\/components\\/schemas\\//,\"\")):null,kI=(0,Re.forwardRef)((({schema:s,getComponent:o,onToggle:i=()=>{},specPath:a},u)=>{const _=o(\"JSONSchema202012\"),w=getModelName(s.get(\"$$ref\")),x=(0,Re.useCallback)(((s,o)=>{i(w,o)}),[w,i]);return Re.createElement(_,{name:w,schema:s.toJS(),ref:u,onExpand:x,identifier:a.toJS().join(\"_\")})})),OI=kI,models=({specActions:s,specSelectors:o,layoutSelectors:i,layoutActions:a,getComponent:u,getConfigs:_,fn:w})=>{const x=o.selectSchemas(),C=Object.keys(x).length>0,j=[\"components\",\"schemas\"],{docExpansion:L,defaultModelsExpandDepth:B}=_(),$=B>0&&\"none\"!==L,U=i.isShown(j,$),V=u(\"Collapse\"),z=u(\"JSONSchema202012\"),Y=u(\"ArrowUpIcon\"),Z=u(\"ArrowDownIcon\"),{getTitle:ee}=w.jsonSchema202012.useFn();(0,Re.useEffect)((()=>{const a=Object.entries(x).some((([s])=>i.isShown([...j,s],!1))),u=U&&(B>1||a),_=null!=o.specResolvedSubtree(j);u&&!_&&s.requestResolvedSubtree(j)}),[U,B]);const ie=(0,Re.useCallback)((()=>{a.show(j,!U)}),[U]),ae=(0,Re.useCallback)((s=>{null!==s&&a.readyToScroll(j,s)}),[]),handleJSONSchema202012Ref=s=>o=>{null!==o&&a.readyToScroll([...j,s],o)},handleJSONSchema202012Expand=i=>(u,_)=>{const w=[...j,i];if(_){null!=o.specResolvedSubtree(w)||s.requestResolvedSubtree([...j,i]),a.show(w,!0)}else a.show(w,!1)};return!C||B<0?null:Re.createElement(\"section\",{className:Jn()(\"models\",{\"is-open\":U}),ref:ae},Re.createElement(\"h4\",null,Re.createElement(\"button\",{\"aria-expanded\":U,className:\"models-control\",onClick:ie},Re.createElement(\"span\",null,\"Schemas\"),U?Re.createElement(Y,null):Re.createElement(Z,null))),Re.createElement(V,{isOpened:U},Object.entries(x).map((([s,o])=>{const i=ee(o,{lookup:\"basic\"})||s;return Re.createElement(z,{key:s,ref:handleJSONSchema202012Ref(s),schema:o,name:i,onExpand:handleJSONSchema202012Expand(s)})}))))},mutual_tls_auth=({schema:s,getComponent:o,name:i,authSelectors:a})=>{const u=o(\"JumpToPath\",!0),_=a.selectAuthPath(i);return Re.createElement(\"div\",null,Re.createElement(\"h4\",null,i,\" (mutualTLS) \",Re.createElement(u,{path:_})),Re.createElement(\"p\",null,\"Mutual TLS is required by this API/Operation. Certificates are managed via your Operating System and/or your browser.\"),Re.createElement(\"p\",null,s.get(\"description\")))};class auths_Auths extends Re.Component{constructor(s,o){super(s,o),this.state={}}onAuthChange=s=>{let{name:o}=s;this.setState({[o]:s})};submitAuth=s=>{s.preventDefault();let{authActions:o}=this.props;o.authorizeWithPersistOption(this.state)};logoutClick=s=>{s.preventDefault();let{authActions:o,definitions:i}=this.props,a=i.map(((s,o)=>o)).toArray();this.setState(a.reduce(((s,o)=>(s[o]=\"\",s)),{})),o.logoutWithPersistOption(a)};close=s=>{s.preventDefault();let{authActions:o}=this.props;o.showDefinitions(!1)};render(){let{definitions:s,getComponent:o,authSelectors:i,errSelectors:a}=this.props;const u=o(\"AuthItem\"),_=o(\"oauth2\",!0),w=o(\"Button\"),x=i.authorized(),C=s.filter(((s,o)=>!!x.get(o))),j=s.filter((s=>\"oauth2\"!==s.get(\"type\")&&\"mutualTLS\"!==s.get(\"type\"))),L=s.filter((s=>\"oauth2\"===s.get(\"type\"))),B=s.filter((s=>\"mutualTLS\"===s.get(\"type\")));return Re.createElement(\"div\",{className:\"auth-container\"},j.size>0&&Re.createElement(\"form\",{onSubmit:this.submitAuth},j.map(((s,_)=>Re.createElement(u,{key:_,schema:s,name:_,getComponent:o,onAuthChange:this.onAuthChange,authorized:x,errSelectors:a,authSelectors:i}))).toArray(),Re.createElement(\"div\",{className:\"auth-btn-wrapper\"},j.size===C.size?Re.createElement(w,{className:\"btn modal-btn auth\",onClick:this.logoutClick,\"aria-label\":\"Remove authorization\"},\"Logout\"):Re.createElement(w,{type:\"submit\",className:\"btn modal-btn auth authorize\",\"aria-label\":\"Apply credentials\"},\"Authorize\"),Re.createElement(w,{className:\"btn modal-btn auth btn-done\",onClick:this.close},\"Close\"))),L.size>0?Re.createElement(\"div\",null,Re.createElement(\"div\",{className:\"scope-def\"},Re.createElement(\"p\",null,\"Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.\"),Re.createElement(\"p\",null,\"API requires the following scopes. Select which ones you want to grant to Swagger UI.\")),s.filter((s=>\"oauth2\"===s.get(\"type\"))).map(((s,o)=>Re.createElement(\"div\",{key:o},Re.createElement(_,{authorized:x,schema:s,name:o})))).toArray()):null,B.size>0&&Re.createElement(\"div\",null,B.map(((s,_)=>Re.createElement(u,{key:_,schema:s,name:_,getComponent:o,onAuthChange:this.onAuthChange,authorized:x,errSelectors:a,authSelectors:i}))).toArray()))}}const AI=auths_Auths,isOAS31=s=>{const o=s.get(\"openapi\");return\"string\"==typeof o&&/^3\\.1\\.(?:[1-9]\\d*|0)$/.test(o)},fn_createOnlyOAS31Selector=s=>(o,...i)=>a=>{if(a.getSystem().specSelectors.isOAS31()){const u=s(o,...i);return\"function\"==typeof u?u(a):u}return null},createOnlyOAS31SelectorWrapper=s=>(o,i)=>(a,...u)=>{if(i.getSystem().specSelectors.isOAS31()){const _=s(a,...u);return\"function\"==typeof _?_(o,i):_}return o(...u)},fn_createSystemSelector=s=>(o,...i)=>a=>{const u=s(o,a,...i);return\"function\"==typeof u?u(a):u},createOnlyOAS31ComponentWrapper=s=>(o,i)=>a=>i.specSelectors.isOAS31()?Re.createElement(s,Mn()({},a,{originalComponent:o,getSystem:i.getSystem})):Re.createElement(o,a),wrapOAS31Fn=(s,o)=>{const{fn:i,specSelectors:a}=o;return Object.fromEntries(Object.entries(s).map((([s,o])=>{const u=i[s];return[s,(...s)=>a.isOAS31()?o(...s):\"function\"==typeof u?u(...s):void 0]})))},CI=createOnlyOAS31ComponentWrapper((({getSystem:s})=>{const o=s().getComponent(\"OAS31License\",!0);return Re.createElement(o,null)})),jI=createOnlyOAS31ComponentWrapper((({getSystem:s})=>{const o=s().getComponent(\"OAS31Contact\",!0);return Re.createElement(o,null)})),PI=createOnlyOAS31ComponentWrapper((({getSystem:s})=>{const o=s().getComponent(\"OAS31Info\",!0);return Re.createElement(o,null)})),getProperties=(s,{includeReadOnly:o,includeWriteOnly:i})=>{if(!s?.properties)return{};const a=Object.entries(s.properties).filter((([,s])=>(!(!0===s?.readOnly)||o)&&(!(!0===s?.writeOnly)||i)));return Object.fromEntries(a)},makeGetSchemaKeywords=s=>{if(\"function\"!=typeof s)return null;const o=s();return()=>[...o,\"discriminator\",\"xml\",\"externalDocs\",\"example\",\"$$ref\"]},II=createOnlyOAS31ComponentWrapper((({getSystem:s,...o})=>{const i=s(),{getComponent:a,fn:u,getConfigs:_}=i,w=_(),x=a(\"OAS31Model\"),C=a(\"withJSONSchema202012SystemContext\");return II.ModelWithJSONSchemaContext??=C(x,{config:{default$schema:\"https://spec.openapis.org/oas/3.1/dialect/base\",defaultExpandedLevels:w.defaultModelExpandDepth,includeReadOnly:o.includeReadOnly,includeWriteOnly:o.includeWriteOnly},fn:{getProperties:u.jsonSchema202012.getProperties,isExpandable:u.jsonSchema202012.isExpandable,getSchemaKeywords:makeGetSchemaKeywords(u.jsonSchema202012.getSchemaKeywords)}}),Re.createElement(II.ModelWithJSONSchemaContext,o)})),TI=II,NI=createOnlyOAS31ComponentWrapper((({getSystem:s})=>{const{getComponent:o,fn:i,getConfigs:a}=s(),u=a();if(NI.ModelsWithJSONSchemaContext)return Re.createElement(NI.ModelsWithJSONSchemaContext,null);const _=o(\"OAS31Models\",!0),w=o(\"withJSONSchema202012SystemContext\");return NI.ModelsWithJSONSchemaContext??=w(_,{config:{default$schema:\"https://spec.openapis.org/oas/3.1/dialect/base\",defaultExpandedLevels:u.defaultModelsExpandDepth-1,includeReadOnly:!0,includeWriteOnly:!0},fn:{getProperties:i.jsonSchema202012.getProperties,isExpandable:i.jsonSchema202012.isExpandable,getSchemaKeywords:makeGetSchemaKeywords(i.jsonSchema202012.getSchemaKeywords)}}),Re.createElement(NI.ModelsWithJSONSchemaContext,null)}));NI.ModelsWithJSONSchemaContext=null;const MI=NI,wrap_components_version_pragma_filter=(s,o)=>s=>{const i=o.specSelectors.isOAS31(),a=o.getComponent(\"OAS31VersionPragmaFilter\");return Re.createElement(a,Mn()({isOAS31:i},s))},RI=createOnlyOAS31ComponentWrapper((({originalComponent:s,...o})=>{const{getComponent:i,schema:a,name:u}=o,_=i(\"MutualTLSAuth\",!0);return\"mutualTLS\"===a.get(\"type\")?Re.createElement(_,{schema:a,name:u}):Re.createElement(s,o)})),DI=RI,LI=createOnlyOAS31ComponentWrapper((({getSystem:s,...o})=>{const i=s().getComponent(\"OAS31Auths\",!0);return Re.createElement(i,o)})),FI=(0,ze.Map)(),BI=Ut(((s,o)=>o.specSelectors.specJson()),isOAS31),selectors_webhooks=()=>s=>{const o=s.specSelectors.specJson().get(\"webhooks\");return ze.Map.isMap(o)?o:FI},$I=Ut([(s,o)=>o.specSelectors.webhooks(),(s,o)=>o.specSelectors.validOperationMethods(),(s,o)=>o.specSelectors.specResolvedSubtree([\"webhooks\"])],((s,o)=>s.reduce(((s,i,a)=>{if(!ze.Map.isMap(i))return s;const u=i.entrySeq().filter((([s])=>o.includes(s))).map((([s,o])=>({operation:(0,ze.Map)({operation:o}),method:s,path:a,specPath:[\"webhooks\",a,s]})));return s.concat(u)}),(0,ze.List)()).groupBy((s=>s.path)).map((s=>s.toArray())).toObject())),selectors_license=()=>s=>{const o=s.specSelectors.info().get(\"license\");return ze.Map.isMap(o)?o:FI},selectLicenseNameField=()=>s=>s.specSelectors.license().get(\"name\",\"License\"),selectLicenseUrlField=()=>s=>s.specSelectors.license().get(\"url\"),qI=Ut([(s,o)=>o.specSelectors.url(),(s,o)=>o.oas3Selectors.selectedServer(),(s,o)=>o.specSelectors.selectLicenseUrlField()],((s,o,i)=>{if(i)return safeBuildUrl(i,s,{selectedServer:o})})),selectLicenseIdentifierField=()=>s=>s.specSelectors.license().get(\"identifier\"),selectors_contact=()=>s=>{const o=s.specSelectors.info().get(\"contact\");return ze.Map.isMap(o)?o:FI},selectContactNameField=()=>s=>s.specSelectors.contact().get(\"name\",\"the developer\"),selectContactEmailField=()=>s=>s.specSelectors.contact().get(\"email\"),selectContactUrlField=()=>s=>s.specSelectors.contact().get(\"url\"),UI=Ut([(s,o)=>o.specSelectors.url(),(s,o)=>o.oas3Selectors.selectedServer(),(s,o)=>o.specSelectors.selectContactUrlField()],((s,o,i)=>{if(i)return safeBuildUrl(i,s,{selectedServer:o})})),selectInfoTitleField=()=>s=>s.specSelectors.info().get(\"title\"),selectInfoSummaryField=()=>s=>s.specSelectors.info().get(\"summary\"),selectInfoDescriptionField=()=>s=>s.specSelectors.info().get(\"description\"),selectInfoTermsOfServiceField=()=>s=>s.specSelectors.info().get(\"termsOfService\"),VI=Ut([(s,o)=>o.specSelectors.url(),(s,o)=>o.oas3Selectors.selectedServer(),(s,o)=>o.specSelectors.selectInfoTermsOfServiceField()],((s,o,i)=>{if(i)return safeBuildUrl(i,s,{selectedServer:o})})),selectExternalDocsDescriptionField=()=>s=>s.specSelectors.externalDocs().get(\"description\"),selectExternalDocsUrlField=()=>s=>s.specSelectors.externalDocs().get(\"url\"),zI=Ut([(s,o)=>o.specSelectors.url(),(s,o)=>o.oas3Selectors.selectedServer(),(s,o)=>o.specSelectors.selectExternalDocsUrlField()],((s,o,i)=>{if(i)return safeBuildUrl(i,s,{selectedServer:o})})),selectJsonSchemaDialectField=()=>s=>s.specSelectors.specJson().get(\"jsonSchemaDialect\"),selectJsonSchemaDialectDefault=()=>\"https://spec.openapis.org/oas/3.1/dialect/base\",WI=Ut(((s,o)=>o.specSelectors.definitions()),((s,o)=>o.specSelectors.specResolvedSubtree([\"components\",\"schemas\"])),((s,o)=>ze.Map.isMap(s)?ze.Map.isMap(o)?Object.entries(s.toJS()).reduce(((s,[i,a])=>{const u=o.get(i);return s[i]=u?.toJS()||a,s}),{}):s.toJS():{})),wrap_selectors_isOAS3=(s,o)=>(i,...a)=>o.specSelectors.isOAS31()||s(...a),JI=createOnlyOAS31SelectorWrapper((()=>(s,o)=>o.oas31Selectors.selectLicenseUrl())),HI=createOnlyOAS31SelectorWrapper((()=>(s,o)=>{const i=o.specSelectors.securityDefinitions();let a=s();return i?(i.entrySeq().forEach((([s,o])=>{const i=o?.get(\"type\");\"mutualTLS\"===i&&(a=a.push(new ze.Map({[s]:o})))})),a):a})),KI=Ut([(s,o)=>o.specSelectors.url(),(s,o)=>o.oas3Selectors.selectedServer(),(s,o)=>o.specSelectors.selectLicenseUrlField(),(s,o)=>o.specSelectors.selectLicenseIdentifierField()],((s,o,i,a)=>i?safeBuildUrl(i,s,{selectedServer:o}):a?`https://spdx.org/licenses/${a}.html`:void 0)),keywords_Example=({schema:s,getSystem:o})=>{const{fn:i,getComponent:a}=o(),{hasKeyword:u}=i.jsonSchema202012.useFn(),_=a(\"JSONSchema202012JSONViewer\");return u(s,\"example\")?Re.createElement(_,{name:\"Example\",value:s.example,className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--example\"}):null},keywords_Xml=({schema:s,getSystem:o})=>{const i=s?.xml||{},{fn:a,getComponent:u,getConfigs:_}=o(),{showExtensions:w}=_(),{useComponent:x,useIsExpanded:C,usePath:j,useLevel:L}=a.jsonSchema202012,{path:B}=j(\"xml\"),{isExpanded:$,setExpanded:U,setCollapsed:V}=C(\"xml\"),[z,Y]=L(),Z=w?getExtensions(i):[],ee=!!(i.name||i.namespace||i.prefix||Z.length>0),ie=x(\"Accordion\"),ae=x(\"ExpandDeepButton\"),ce=u(\"OpenAPI31Extensions\"),le=u(\"JSONSchema202012PathContext\")(),pe=u(\"JSONSchema202012LevelContext\")(),de=(0,Re.useCallback)((()=>{$?V():U()}),[$,U,V]),fe=(0,Re.useCallback)(((s,o)=>{o?U({deep:!0}):V({deep:!0})}),[U,V]);return 0===Object.keys(i).length?null:Re.createElement(le.Provider,{value:B},Re.createElement(pe.Provider,{value:Y},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--xml\",\"data-json-schema-level\":z},ee?Re.createElement(Re.Fragment,null,Re.createElement(ie,{expanded:$,onChange:de},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"XML\")),Re.createElement(ae,{expanded:$,onClick:fe})):Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"XML\"),!0===i.attribute&&Re.createElement(\"span\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted\"},\"attribute\"),!0===i.wrapped&&Re.createElement(\"span\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted\"},\"wrapped\"),Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},\"object\"),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!$})},$&&Re.createElement(Re.Fragment,null,i.name&&Re.createElement(\"li\",{className:\"json-schema-2020-12-property\"},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"name\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},i.name))),i.namespace&&Re.createElement(\"li\",{className:\"json-schema-2020-12-property\"},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"namespace\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},i.namespace))),i.prefix&&Re.createElement(\"li\",{className:\"json-schema-2020-12-property\"},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"prefix\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},i.prefix)))),Z.length>0&&Re.createElement(ce,{openAPISpecObj:i,openAPIExtensions:Z,getSystem:o})))))},Discriminator_DiscriminatorMapping=({discriminator:s})=>{const o=s?.mapping||{};return 0===Object.keys(o).length?null:Object.entries(o).map((([s,o])=>Re.createElement(\"div\",{key:`${s}-${o}`,className:\"json-schema-2020-12-keyword\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},s),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},o))))},keywords_Discriminator_Discriminator=({schema:s,getSystem:o})=>{const i=s?.discriminator||{},{fn:a,getComponent:u,getConfigs:_}=o(),{showExtensions:w}=_(),{useComponent:x,useIsExpanded:C,usePath:j,useLevel:L}=a.jsonSchema202012,B=\"discriminator\",{path:$}=j(B),{isExpanded:U,setExpanded:V,setCollapsed:z}=C(B),[Y,Z]=L(),ee=w?getExtensions(i):[],ie=!!(i.mapping||ee.length>0),ae=x(\"Accordion\"),ce=x(\"ExpandDeepButton\"),le=u(\"OpenAPI31Extensions\"),pe=u(\"JSONSchema202012PathContext\")(),de=u(\"JSONSchema202012LevelContext\")(),fe=(0,Re.useCallback)((()=>{U?z():V()}),[U,V,z]),ye=(0,Re.useCallback)(((s,o)=>{o?V({deep:!0}):z({deep:!0})}),[V,z]);return 0===Object.keys(i).length?null:Re.createElement(pe.Provider,{value:$},Re.createElement(de.Provider,{value:Z},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--discriminator\",\"data-json-schema-level\":Y},ie?Re.createElement(Re.Fragment,null,Re.createElement(ae,{expanded:U,onChange:fe},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"Discriminator\")),Re.createElement(ce,{expanded:U,onClick:ye})):Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"Discriminator\"),i.propertyName&&Re.createElement(\"span\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted\"},i.propertyName),Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},\"object\"),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!U})},U&&Re.createElement(\"li\",{className:\"json-schema-2020-12-property\"},Re.createElement(Discriminator_DiscriminatorMapping,{discriminator:i})),ee.length>0&&Re.createElement(le,{openAPISpecObj:i,openAPIExtensions:ee,getSystem:o})))))},keywords_OpenAPIExtensions=({openAPISpecObj:s,getSystem:o,openAPIExtensions:i})=>{const{fn:a}=o(),{useComponent:u}=a.jsonSchema202012,_=u(\"JSONViewer\");return i.map((o=>Re.createElement(_,{key:o,name:o,value:s[o],className:\"json-schema-2020-12-json-viewer-extension-keyword\"})))},keywords_ExternalDocs=({schema:s,getSystem:o})=>{const i=s?.externalDocs||{},{fn:a,getComponent:u,getConfigs:_}=o(),{showExtensions:w}=_(),{useComponent:x,useIsExpanded:C,usePath:j,useLevel:L}=a.jsonSchema202012,B=\"externalDocs\",{path:$}=j(B),{isExpanded:U,setExpanded:V,setCollapsed:z}=C(B),[Y,Z]=L(),ee=w?getExtensions(i):[],ie=!!(i.description||i.url||ee.length>0),ae=x(\"Accordion\"),ce=x(\"ExpandDeepButton\"),le=u(\"JSONSchema202012KeywordDescription\"),pe=u(\"Link\"),de=u(\"OpenAPI31Extensions\"),fe=u(\"JSONSchema202012PathContext\")(),ye=u(\"JSONSchema202012LevelContext\")(),be=(0,Re.useCallback)((()=>{U?z():V()}),[U,V,z]),_e=(0,Re.useCallback)(((s,o)=>{o?V({deep:!0}):z({deep:!0})}),[V,z]);return 0===Object.keys(i).length?null:Re.createElement(fe.Provider,{value:$},Re.createElement(ye.Provider,{value:Z},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--externalDocs\",\"data-json-schema-level\":Y},ie?Re.createElement(Re.Fragment,null,Re.createElement(ae,{expanded:U,onChange:be},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"External documentation\")),Re.createElement(ce,{expanded:U,onClick:_e})):Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"External documentation\"),Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},\"object\"),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!U})},U&&Re.createElement(Re.Fragment,null,i.description&&Re.createElement(\"li\",{className:\"json-schema-2020-12-property\"},Re.createElement(le,{schema:i,getSystem:o})),i.url&&Re.createElement(\"li\",{className:\"json-schema-2020-12-property\"},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"url\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},Re.createElement(pe,{target:\"_blank\",href:sanitizeUrl(i.url)},i.url))))),ee.length>0&&Re.createElement(de,{openAPISpecObj:i,openAPIExtensions:ee,getSystem:o})))))},keywords_Description=({schema:s,getSystem:o})=>{if(!s?.description)return null;const{getComponent:i}=o(),a=i(\"Markdown\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--description\"},Re.createElement(\"div\",{className:\"json-schema-2020-12-core-keyword__value json-schema-2020-12-core-keyword__value--secondary\"},Re.createElement(a,{source:s.description})))},GI=createOnlyOAS31ComponentWrapper(keywords_Description),YI=createOnlyOAS31ComponentWrapper((({schema:s,getSystem:o,originalComponent:i})=>{const{getComponent:a}=o(),u=a(\"JSONSchema202012KeywordDiscriminator\"),_=a(\"JSONSchema202012KeywordXml\"),w=a(\"JSONSchema202012KeywordExample\"),x=a(\"JSONSchema202012KeywordExternalDocs\");return Re.createElement(Re.Fragment,null,Re.createElement(i,{schema:s}),Re.createElement(u,{schema:s,getSystem:o}),Re.createElement(_,{schema:s,getSystem:o}),Re.createElement(x,{schema:s,getSystem:o}),Re.createElement(w,{schema:s,getSystem:o}))})),XI=YI,keywords_Properties=({schema:s,getSystem:o})=>{const{fn:i,getComponent:a}=o(),{useComponent:u,usePath:_}=i.jsonSchema202012,{getDependentRequired:w,getProperties:x}=i.jsonSchema202012.useFn(),C=i.jsonSchema202012.useConfig(),j=Array.isArray(s?.required)?s.required:[],{path:L}=_(\"properties\"),B=u(\"JSONSchema\"),$=a(\"JSONSchema202012PathContext\")(),U=x(s,C);return 0===Object.keys(U).length?null:Re.createElement($.Provider,{value:L},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--properties\"},Re.createElement(\"ul\",null,Object.entries(U).map((([o,i])=>{const a=j.includes(o),u=w(o,s);return Re.createElement(\"li\",{key:o,className:Jn()(\"json-schema-2020-12-property\",{\"json-schema-2020-12-property--required\":a})},Re.createElement(B,{name:o,schema:i,dependentRequired:u}))})))))},QI=createOnlyOAS31ComponentWrapper(keywords_Properties);const ZI=function oas31_after_load_afterLoad({fn:s,getSystem:o}){if(s.jsonSchema202012){const i=((s,o)=>{const{fn:i}=o();if(\"function\"!=typeof s)return null;const{hasKeyword:a}=i.jsonSchema202012;return o=>s(o)||a(o,\"example\")||o?.xml||o?.discriminator||o?.externalDocs})(s.jsonSchema202012.isExpandable,o);Object.assign(this.fn.jsonSchema202012,{isExpandable:i,getProperties})}if(\"function\"==typeof s.sampleFromSchema&&s.jsonSchema202012){const i=wrapOAS31Fn({sampleFromSchema:s.jsonSchema202012.sampleFromSchema,sampleFromSchemaGeneric:s.jsonSchema202012.sampleFromSchemaGeneric,createXMLExample:s.jsonSchema202012.createXMLExample,memoizedSampleFromSchema:s.jsonSchema202012.memoizedSampleFromSchema,memoizedCreateXMLExample:s.jsonSchema202012.memoizedCreateXMLExample,getJsonSampleSchema:s.jsonSchema202012.getJsonSampleSchema,getYamlSampleSchema:s.jsonSchema202012.getYamlSampleSchema,getXmlSampleSchema:s.jsonSchema202012.getXmlSampleSchema,getSampleSchema:s.jsonSchema202012.getSampleSchema,mergeJsonSchema:s.jsonSchema202012.mergeJsonSchema,getSchemaObjectTypeLabel:o=>s.jsonSchema202012.getType(immutableToJS(o)),getSchemaObjectType:o=>s.jsonSchema202012.foldType(immutableToJS(o)?.type)},o());Object.assign(this.fn,i)}const i=(s=>(o,i=null)=>{const{fn:a}=s();if(a.isFileUploadIntendedOAS30(o,i))return!0;const u=ze.Map.isMap(o);if(!u&&!as()(o))return!1;const _=u?o.get(\"contentMediaType\"):o.contentMediaType,w=u?o.get(\"contentEncoding\"):o.contentEncoding;return\"string\"==typeof _&&\"\"!==_||\"string\"==typeof w&&\"\"!==w})(o),{isFileUploadIntended:a}=wrapOAS31Fn({isFileUploadIntended:i},o());if(this.fn.isFileUploadIntended=a,this.fn.isFileUploadIntendedOAS31=i,s.jsonSchema202012){const{hasSchemaType:i}=wrapOAS31Fn({hasSchemaType:s.jsonSchema202012.hasSchemaType},o());this.fn.hasSchemaType=i}},oas31=({fn:s})=>{const o=s.createSystemSelector||fn_createSystemSelector,i=s.createOnlyOAS31Selector||fn_createOnlyOAS31Selector;return{afterLoad:ZI,fn:{isOAS31,createSystemSelector:fn_createSystemSelector,createOnlyOAS31Selector:fn_createOnlyOAS31Selector},components:{Webhooks:webhooks,JsonSchemaDialect:json_schema_dialect,MutualTLSAuth:mutual_tls_auth,OAS31Info:oas31_components_info,OAS31License:oas31_components_license,OAS31Contact:oas31_components_contact,OAS31VersionPragmaFilter:version_pragma_filter,OAS31Model:OI,OAS31Models:models,OAS31Auths:AI,JSONSchema202012KeywordExample:keywords_Example,JSONSchema202012KeywordXml:keywords_Xml,JSONSchema202012KeywordDiscriminator:keywords_Discriminator_Discriminator,JSONSchema202012KeywordExternalDocs:keywords_ExternalDocs,OpenAPI31Extensions:keywords_OpenAPIExtensions},wrapComponents:{InfoContainer:PI,License:CI,Contact:jI,VersionPragmaFilter:wrap_components_version_pragma_filter,Model:TI,Models:MI,AuthItem:DI,auths:LI,JSONSchema202012KeywordDescription:GI,JSONSchema202012KeywordExamples:XI,JSONSchema202012KeywordProperties:QI},statePlugins:{auth:{wrapSelectors:{definitionsToAuthorize:HI}},spec:{selectors:{isOAS31:o(BI),license:selectors_license,selectLicenseNameField,selectLicenseUrlField,selectLicenseIdentifierField:i(selectLicenseIdentifierField),selectLicenseUrl:o(qI),contact:selectors_contact,selectContactNameField,selectContactEmailField,selectContactUrlField,selectContactUrl:o(UI),selectInfoTitleField,selectInfoSummaryField:i(selectInfoSummaryField),selectInfoDescriptionField,selectInfoTermsOfServiceField,selectInfoTermsOfServiceUrl:o(VI),selectExternalDocsDescriptionField,selectExternalDocsUrlField,selectExternalDocsUrl:o(zI),webhooks:i(selectors_webhooks),selectWebhooksOperations:i(o($I)),selectJsonSchemaDialectField,selectJsonSchemaDialectDefault,selectSchemas:o(WI)},wrapSelectors:{isOAS3:wrap_selectors_isOAS3,selectLicenseUrl:JI}},oas31:{selectors:{selectLicenseUrl:i(o(KI))}}}}},eT=es().object,tT=es().bool,rT=(es().oneOfType([eT,tT]),(0,Re.createContext)(null));rT.displayName=\"JSONSchemaContext\";const nT=(0,Re.createContext)(0);nT.displayName=\"JSONSchemaLevelContext\";const sT=(0,Re.createContext)(new Set),oT=(0,Re.createContext)([]);class JSONSchemaIsExpandedState{static Collapsed=\"collapsed\";static Expanded=\"expanded\";static DeeplyExpanded=\"deeply-expanded\"}const useConfig=()=>{const{config:s}=(0,Re.useContext)(rT);return s},useComponent=s=>{const{components:o}=(0,Re.useContext)(rT);return o[s]||null},useFn=(s=void 0)=>{const{fn:o}=(0,Re.useContext)(rT);return void 0!==s?o[s]:o},useJSONSchemaContextState=()=>{const[,s]=(0,Re.useState)(null),{state:o}=(0,Re.useContext)(rT);return{state:o,setState:i=>{i(o),s({})}}},useLevel=()=>{const s=(0,Re.useContext)(nT);return[s,s+1]},usePath=s=>{const o=(0,Re.useContext)(oT),{setState:i}=useJSONSchemaContextState(),a=\"string\"==typeof s?[...o,s]:o;return{path:a,pathMutator:(s,o={deep:!1})=>{const u=a.toString(),updateFn=o=>{o.paths[u]=s,s===JSONSchemaIsExpandedState.Collapsed&&Object.keys(o.paths).forEach((s=>{s.startsWith(u)&&o.paths[s]===JSONSchemaIsExpandedState.DeeplyExpanded&&(o.paths[s]=JSONSchemaIsExpandedState.Expanded)}))},updateDeepFn=o=>{Object.keys(o.paths).forEach((i=>{i.startsWith(u)&&(o.paths[i]=s)}))};o.deep?i(updateDeepFn):i(updateFn)}}},useIsExpanded=s=>{const[o]=useLevel(),{defaultExpandedLevels:i}=useConfig(),{path:a,pathMutator:u}=usePath(s),{path:_}=usePath(),{state:w}=useJSONSchemaContextState(),x=w.paths[a.toString()],C=w.paths[_.toString()]??w.paths[_.slice(0,-1).toString()],j=x??(i-o>0?JSONSchemaIsExpandedState.Expanded:JSONSchemaIsExpandedState.Collapsed),L=j!==JSONSchemaIsExpandedState.Collapsed;(0,Re.useEffect)((()=>{u(C===JSONSchemaIsExpandedState.DeeplyExpanded?JSONSchemaIsExpandedState.DeeplyExpanded:j)}),[C]);return{isExpanded:L,setExpanded:(0,Re.useCallback)(((s={deep:!1})=>{u(s.deep?JSONSchemaIsExpandedState.DeeplyExpanded:JSONSchemaIsExpandedState.Expanded)}),[]),setCollapsed:(0,Re.useCallback)(((s={deep:!1})=>{u(JSONSchemaIsExpandedState.Collapsed,s)}),[])}},useRenderedSchemas=(s=void 0)=>{if(void 0===s)return(0,Re.useContext)(sT);const o=(0,Re.useContext)(sT);return new Set([...o,s])},iT=(0,Re.forwardRef)((({schema:s,name:o=\"\",dependentRequired:i=[],onExpand:a=()=>{},identifier:u=\"\"},_)=>{const w=useFn(),x=u||s?.$id||o,{path:C}=usePath(x),{isExpanded:j,setExpanded:L,setCollapsed:B}=useIsExpanded(x),[$,U]=useLevel(),V=(()=>{const[s]=useLevel();return s>0})(),z=w.isExpandable(s)||i.length>0,Y=(s=>useRenderedSchemas().has(s))(s),Z=useRenderedSchemas(s),ee=w.stringifyConstraints(s),ie=useComponent(\"Accordion\"),ae=useComponent(\"Keyword$schema\"),ce=useComponent(\"Keyword$vocabulary\"),le=useComponent(\"Keyword$id\"),pe=useComponent(\"Keyword$anchor\"),de=useComponent(\"Keyword$dynamicAnchor\"),fe=useComponent(\"Keyword$ref\"),ye=useComponent(\"Keyword$dynamicRef\"),be=useComponent(\"Keyword$defs\"),_e=useComponent(\"Keyword$comment\"),Se=useComponent(\"KeywordAllOf\"),we=useComponent(\"KeywordAnyOf\"),xe=useComponent(\"KeywordOneOf\"),Pe=useComponent(\"KeywordNot\"),Te=useComponent(\"KeywordIf\"),$e=useComponent(\"KeywordThen\"),qe=useComponent(\"KeywordElse\"),ze=useComponent(\"KeywordDependentSchemas\"),We=useComponent(\"KeywordPrefixItems\"),He=useComponent(\"KeywordItems\"),Ye=useComponent(\"KeywordContains\"),Xe=useComponent(\"KeywordProperties\"),Qe=useComponent(\"KeywordPatternProperties\"),et=useComponent(\"KeywordAdditionalProperties\"),tt=useComponent(\"KeywordPropertyNames\"),rt=useComponent(\"KeywordUnevaluatedItems\"),nt=useComponent(\"KeywordUnevaluatedProperties\"),st=useComponent(\"KeywordType\"),ot=useComponent(\"KeywordEnum\"),it=useComponent(\"KeywordConst\"),at=useComponent(\"KeywordConstraint\"),ct=useComponent(\"KeywordDependentRequired\"),lt=useComponent(\"KeywordContentSchema\"),ut=useComponent(\"KeywordTitle\"),pt=useComponent(\"KeywordDescription\"),ht=useComponent(\"KeywordDefault\"),dt=useComponent(\"KeywordDeprecated\"),mt=useComponent(\"KeywordReadOnly\"),gt=useComponent(\"KeywordWriteOnly\"),yt=useComponent(\"KeywordExamples\"),vt=useComponent(\"ExtensionKeywords\"),bt=useComponent(\"ExpandDeepButton\"),_t=(0,Re.useCallback)(((s,o)=>{o?L():B(),a(s,o,!1)}),[a,L,B]),St=(0,Re.useCallback)(((s,o)=>{o?L({deep:!0}):B({deep:!0}),a(s,o,!0)}),[a,L,B]);return Re.createElement(oT.Provider,{value:C},Re.createElement(nT.Provider,{value:U},Re.createElement(sT.Provider,{value:Z},Re.createElement(\"article\",{ref:_,\"data-json-schema-level\":$,className:Jn()(\"json-schema-2020-12\",{\"json-schema-2020-12--embedded\":V,\"json-schema-2020-12--circular\":Y})},Re.createElement(\"div\",{className:\"json-schema-2020-12-head\"},z&&!Y?Re.createElement(Re.Fragment,null,Re.createElement(ie,{expanded:j,onChange:_t},Re.createElement(ut,{title:o,schema:s})),Re.createElement(bt,{expanded:j,onClick:St})):Re.createElement(ut,{title:o,schema:s}),Re.createElement(dt,{schema:s}),Re.createElement(mt,{schema:s}),Re.createElement(gt,{schema:s}),Re.createElement(st,{schema:s,isCircular:Y}),ee.length>0&&ee.map((s=>Re.createElement(at,{key:`${s.scope}-${s.value}`,constraint:s})))),Re.createElement(\"div\",{className:Jn()(\"json-schema-2020-12-body\",{\"json-schema-2020-12-body--collapsed\":!j})},j&&Re.createElement(Re.Fragment,null,Re.createElement(pt,{schema:s}),!Y&&z&&Re.createElement(Re.Fragment,null,Re.createElement(Xe,{schema:s}),Re.createElement(Qe,{schema:s}),Re.createElement(et,{schema:s}),Re.createElement(nt,{schema:s}),Re.createElement(tt,{schema:s}),Re.createElement(Se,{schema:s}),Re.createElement(we,{schema:s}),Re.createElement(xe,{schema:s}),Re.createElement(Pe,{schema:s}),Re.createElement(Te,{schema:s}),Re.createElement($e,{schema:s}),Re.createElement(qe,{schema:s}),Re.createElement(ze,{schema:s}),Re.createElement(We,{schema:s}),Re.createElement(He,{schema:s}),Re.createElement(rt,{schema:s}),Re.createElement(Ye,{schema:s}),Re.createElement(lt,{schema:s})),Re.createElement(ot,{schema:s}),Re.createElement(it,{schema:s}),Re.createElement(ct,{schema:s,dependentRequired:i}),Re.createElement(ht,{schema:s}),Re.createElement(yt,{schema:s}),Re.createElement(ae,{schema:s}),Re.createElement(ce,{schema:s}),Re.createElement(le,{schema:s}),Re.createElement(pe,{schema:s}),Re.createElement(de,{schema:s}),Re.createElement(fe,{schema:s}),!Y&&z&&Re.createElement(be,{schema:s}),Re.createElement(ye,{schema:s}),Re.createElement(_e,{schema:s}),Re.createElement(vt,{schema:s})))))))})),aT=iT,keywords_$schema=({schema:s})=>s?.$schema?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$schema\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$schema\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},s.$schema)):null,$vocabulary_$vocabulary=({schema:s})=>{const o=\"$vocabulary\",{path:i}=usePath(o),{isExpanded:a,setExpanded:u,setCollapsed:_}=useIsExpanded(o),w=useComponent(\"Accordion\"),x=(0,Re.useCallback)((()=>{a?_():u()}),[a,u,_]);return s?.$vocabulary?\"object\"!=typeof s.$vocabulary?null:Re.createElement(oT.Provider,{value:i},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$vocabulary\"},Re.createElement(w,{expanded:a,onChange:x},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$vocabulary\")),Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},\"object\"),Re.createElement(\"ul\",null,a&&Object.entries(s.$vocabulary).map((([s,o])=>Re.createElement(\"li\",{key:s,className:Jn()(\"json-schema-2020-12-$vocabulary-uri\",{\"json-schema-2020-12-$vocabulary-uri--disabled\":!o})},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},s))))))):null},keywords_$id=({schema:s})=>s?.$id?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$id\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$id\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},s.$id)):null,keywords_$anchor=({schema:s})=>s?.$anchor?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$anchor\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$anchor\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},s.$anchor)):null,keywords_$dynamicAnchor=({schema:s})=>s?.$dynamicAnchor?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$dynamicAnchor\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$dynamicAnchor\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},s.$dynamicAnchor)):null,keywords_$ref=({schema:s})=>s?.$ref?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$ref\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$ref\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},s.$ref)):null,keywords_$dynamicRef=({schema:s})=>s?.$dynamicRef?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$dynamicRef\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$dynamicRef\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},s.$dynamicRef)):null,keywords_$defs=({schema:s})=>{const o=s?.$defs||{},i=\"$defs\",{path:a}=usePath(i),{isExpanded:u,setExpanded:_,setCollapsed:w}=useIsExpanded(i),[x,C]=useLevel(),j=useComponent(\"Accordion\"),L=useComponent(\"ExpandDeepButton\"),B=useComponent(\"JSONSchema\"),$=(0,Re.useCallback)((()=>{u?w():_()}),[u,_,w]),U=(0,Re.useCallback)(((s,o)=>{o?_({deep:!0}):w({deep:!0})}),[_,w]);return 0===Object.keys(o).length?null:Re.createElement(oT.Provider,{value:a},Re.createElement(nT.Provider,{value:C},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$defs\",\"data-json-schema-level\":x},Re.createElement(j,{expanded:u,onChange:$},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$defs\")),Re.createElement(L,{expanded:u,onClick:U}),Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},\"object\"),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!u})},u&&Re.createElement(Re.Fragment,null,Object.entries(o).map((([s,o])=>Re.createElement(\"li\",{key:s,className:\"json-schema-2020-12-property\"},Re.createElement(B,{name:s,schema:o})))))))))},keywords_$comment=({schema:s})=>s?.$comment?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--$comment\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary\"},\"$comment\"),Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary\"},s.$comment)):null,keywords_AllOf=({schema:s})=>{const o=s?.allOf||[],i=useFn(),a=\"allOf\",{path:u}=usePath(a),{isExpanded:_,setExpanded:w,setCollapsed:x}=useIsExpanded(a),[C,j]=useLevel(),L=useComponent(\"Accordion\"),B=useComponent(\"ExpandDeepButton\"),$=useComponent(\"JSONSchema\"),U=useComponent(\"KeywordType\"),V=(0,Re.useCallback)((()=>{_?x():w()}),[_,w,x]),z=(0,Re.useCallback)(((s,o)=>{o?w({deep:!0}):x({deep:!0})}),[w,x]);return Array.isArray(o)&&0!==o.length?Re.createElement(oT.Provider,{value:u},Re.createElement(nT.Provider,{value:j},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--allOf\",\"data-json-schema-level\":C},Re.createElement(L,{expanded:_,onChange:V},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"All of\")),Re.createElement(B,{expanded:_,onClick:z}),Re.createElement(U,{schema:{allOf:o}}),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!_})},_&&Re.createElement(Re.Fragment,null,o.map(((s,o)=>Re.createElement(\"li\",{key:`#${o}`,className:\"json-schema-2020-12-property\"},Re.createElement($,{name:`#${o} ${i.getTitle(s)}`,schema:s}))))))))):null},keywords_AnyOf=({schema:s})=>{const o=s?.anyOf||[],i=useFn(),a=\"anyOf\",{path:u}=usePath(a),{isExpanded:_,setExpanded:w,setCollapsed:x}=useIsExpanded(a),[C,j]=useLevel(),L=useComponent(\"Accordion\"),B=useComponent(\"ExpandDeepButton\"),$=useComponent(\"JSONSchema\"),U=useComponent(\"KeywordType\"),V=(0,Re.useCallback)((()=>{_?x():w()}),[_,w,x]),z=(0,Re.useCallback)(((s,o)=>{o?w({deep:!0}):x({deep:!0})}),[w,x]);return Array.isArray(o)&&0!==o.length?Re.createElement(oT.Provider,{value:u},Re.createElement(nT.Provider,{value:j},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--anyOf\",\"data-json-schema-level\":C},Re.createElement(L,{expanded:_,onChange:V},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Any of\")),Re.createElement(B,{expanded:_,onClick:z}),Re.createElement(U,{schema:{anyOf:o}}),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!_})},_&&Re.createElement(Re.Fragment,null,o.map(((s,o)=>Re.createElement(\"li\",{key:`#${o}`,className:\"json-schema-2020-12-property\"},Re.createElement($,{name:`#${o} ${i.getTitle(s)}`,schema:s}))))))))):null},keywords_OneOf=({schema:s})=>{const o=s?.oneOf||[],i=useFn(),a=\"oneOf\",{path:u}=usePath(a),{isExpanded:_,setExpanded:w,setCollapsed:x}=useIsExpanded(a),[C,j]=useLevel(),L=useComponent(\"Accordion\"),B=useComponent(\"ExpandDeepButton\"),$=useComponent(\"JSONSchema\"),U=useComponent(\"KeywordType\"),V=(0,Re.useCallback)((()=>{_?x():w()}),[_,w,x]),z=(0,Re.useCallback)(((s,o)=>{o?w({deep:!0}):x({deep:!0})}),[w,x]);return Array.isArray(o)&&0!==o.length?Re.createElement(oT.Provider,{value:u},Re.createElement(nT.Provider,{value:j},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--oneOf\",\"data-json-schema-level\":C},Re.createElement(L,{expanded:_,onChange:V},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"One of\")),Re.createElement(B,{expanded:_,onClick:z}),Re.createElement(U,{schema:{oneOf:o}}),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!_})},_&&Re.createElement(Re.Fragment,null,o.map(((s,o)=>Re.createElement(\"li\",{key:`#${o}`,className:\"json-schema-2020-12-property\"},Re.createElement($,{name:`#${o} ${i.getTitle(s)}`,schema:s}))))))))):null},keywords_Not=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"not\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Not\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--not\"},Re.createElement(i,{name:a,schema:s.not,identifier:\"not\"}))},keywords_If=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"if\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"If\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--if\"},Re.createElement(i,{name:a,schema:s.if,identifier:\"if\"}))},keywords_Then=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"then\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Then\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--then\"},Re.createElement(i,{name:a,schema:s.then,identifier:\"then\"}))},keywords_Else=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"else\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Else\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--if\"},Re.createElement(i,{name:a,schema:s.else,identifier:\"else\"}))},keywords_DependentSchemas=({schema:s})=>{const o=s?.dependentSchemas||[],i=\"dependentSchemas\",{path:a}=usePath(i),{isExpanded:u,setExpanded:_,setCollapsed:w}=useIsExpanded(i),[x,C]=useLevel(),j=useComponent(\"Accordion\"),L=useComponent(\"ExpandDeepButton\"),B=useComponent(\"JSONSchema\"),$=(0,Re.useCallback)((()=>{u?w():_()}),[u,_,w]),U=(0,Re.useCallback)(((s,o)=>{o?_({deep:!0}):w({deep:!0})}),[_,w]);return\"object\"!=typeof o||0===Object.keys(o).length?null:Re.createElement(oT.Provider,{value:a},Re.createElement(nT.Provider,{value:C},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentSchemas\",\"data-json-schema-level\":x},Re.createElement(j,{expanded:u,onChange:$},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Dependent schemas\")),Re.createElement(L,{expanded:u,onClick:U}),Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},\"object\"),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!u})},u&&Re.createElement(Re.Fragment,null,Object.entries(o).map((([s,o])=>Re.createElement(\"li\",{key:s,className:\"json-schema-2020-12-property\"},Re.createElement(B,{name:s,schema:o})))))))))},keywords_PrefixItems=({schema:s})=>{const o=s?.prefixItems||[],i=useFn(),a=\"prefixItems\",{path:u}=usePath(a),{isExpanded:_,setExpanded:w,setCollapsed:x}=useIsExpanded(a),[C,j]=useLevel(),L=useComponent(\"Accordion\"),B=useComponent(\"ExpandDeepButton\"),$=useComponent(\"JSONSchema\"),U=useComponent(\"KeywordType\"),V=(0,Re.useCallback)((()=>{_?x():w()}),[_,w,x]),z=(0,Re.useCallback)(((s,o)=>{o?w({deep:!0}):x({deep:!0})}),[w,x]);return Array.isArray(o)&&0!==o.length?Re.createElement(oT.Provider,{value:u},Re.createElement(nT.Provider,{value:j},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--prefixItems\",\"data-json-schema-level\":C},Re.createElement(L,{expanded:_,onChange:V},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Prefix items\")),Re.createElement(B,{expanded:_,onClick:z}),Re.createElement(U,{schema:{prefixItems:o}}),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!_})},_&&Re.createElement(Re.Fragment,null,o.map(((s,o)=>Re.createElement(\"li\",{key:`#${o}`,className:\"json-schema-2020-12-property\"},Re.createElement($,{name:`#${o} ${i.getTitle(s)}`,schema:s}))))))))):null},keywords_Items=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"items\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Items\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--items\"},Re.createElement(i,{name:a,schema:s.items,identifier:\"items\"}))},keywords_Contains=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"contains\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Contains\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--contains\"},Re.createElement(i,{name:a,schema:s.contains,identifier:\"contains\"}))},keywords_Properties_Properties=({schema:s})=>{const o=useFn(),i=s?.properties||{},a=Array.isArray(s?.required)?s.required:[],u=useComponent(\"JSONSchema\"),{path:_}=usePath(\"properties\");return 0===Object.keys(i).length?null:Re.createElement(oT.Provider,{value:_},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--properties\"},Re.createElement(\"ul\",null,Object.entries(i).map((([i,_])=>{const w=a.includes(i),x=o.getDependentRequired(i,s);return Re.createElement(\"li\",{key:i,className:Jn()(\"json-schema-2020-12-property\",{\"json-schema-2020-12-property--required\":w})},Re.createElement(u,{name:i,schema:_,dependentRequired:x}))})))))},PatternProperties_PatternProperties=({schema:s})=>{const o=s?.patternProperties||{},i=useComponent(\"JSONSchema\"),{path:a}=usePath(\"patternProperties\");return 0===Object.keys(o).length?null:Re.createElement(oT.Provider,{value:a},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--patternProperties\"},Re.createElement(\"ul\",null,Object.entries(o).map((([s,o])=>Re.createElement(\"li\",{key:s,className:\"json-schema-2020-12-property\"},Re.createElement(i,{name:s,schema:o})))))))},keywords_AdditionalProperties=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"additionalProperties\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Additional properties\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--additionalProperties\"},!0===s.additionalProperties?Re.createElement(Re.Fragment,null,a,Re.createElement(\"span\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},\"allowed\")):!1===s.additionalProperties?Re.createElement(Re.Fragment,null,a,Re.createElement(\"span\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},\"forbidden\")):Re.createElement(i,{name:a,schema:s.additionalProperties,identifier:\"additionalProperties\"}))},keywords_PropertyNames=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\"),a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Property names\");return o.hasKeyword(s,\"propertyNames\")?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--propertyNames\"},Re.createElement(i,{name:a,schema:s.propertyNames,identifier:\"propertyNames\"})):null},keywords_UnevaluatedItems=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"unevaluatedItems\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Unevaluated items\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--unevaluatedItems\"},Re.createElement(i,{name:a,schema:s.unevaluatedItems,identifier:\"unevaluatedItems\"}))},keywords_UnevaluatedProperties=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"unevaluatedProperties\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Unevaluated properties\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--unevaluatedProperties\"},Re.createElement(i,{name:a,schema:s.unevaluatedProperties,identifier:\"unevaluatedProperties\"}))},keywords_Type=({schema:s,isCircular:o=!1})=>{const i=useFn().getType(s),a=o?\" [circular]\":\"\";return Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},`${i}${a}`)},Enum_Enum=({schema:s})=>{const o=useComponent(\"JSONViewer\");return Array.isArray(s?.enum)?Re.createElement(o,{name:\"Enum\",value:s.enum,className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--enum\"}):null},Const_Const=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONViewer\");return o.hasKeyword(s,\"const\")?Re.createElement(i,{name:\"Const\",value:s.const,className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--const\"}):null},fn_upperFirst=s=>\"string\"==typeof s?`${s.charAt(0).toUpperCase()}${s.slice(1)}`:s,makeGetTitle=s=>(o,{lookup:i=\"extended\"}={})=>{const a=s();if(null!=o?.title)return a.upperFirst(String(o.title));if(\"extended\"===i){if(null!=o?.$anchor)return a.upperFirst(String(o.$anchor));if(null!=o?.$id)return String(o.$id)}return\"\"},makeGetType=s=>{const getType=(o,i=new WeakSet)=>{const a=s();if(null==o)return\"any\";if(a.isBooleanJSONSchema(o))return o?\"any\":\"never\";if(\"object\"!=typeof o)return\"any\";if(i.has(o))return\"any\";i.add(o);const{type:u,prefixItems:_,items:w}=o,getArrayType=()=>{if(Array.isArray(_)){const s=_.map((s=>getType(s,i))),o=w?getType(w,i):\"any\";return`array<[${s.join(\", \")}], ${o}>`}if(w){return`array<${getType(w,i)}>`}return\"array<any>\"};if(o.not&&\"any\"===getType(o.not))return\"never\";const handleCombiningKeywords=(s,a)=>{if(Array.isArray(o[s])){return`(${o[s].map((s=>getType(s,i))).join(a)})`}return null},x=[Array.isArray(u)?u.map((s=>\"array\"===s?getArrayType():s)).join(\" | \"):\"array\"===u?getArrayType():[\"null\",\"boolean\",\"object\",\"array\",\"number\",\"integer\",\"string\"].includes(u)?u:(()=>{if(Object.hasOwn(o,\"prefixItems\")||Object.hasOwn(o,\"items\")||Object.hasOwn(o,\"contains\"))return getArrayType();if(Object.hasOwn(o,\"properties\")||Object.hasOwn(o,\"additionalProperties\")||Object.hasOwn(o,\"patternProperties\"))return\"object\";if([\"int32\",\"int64\"].includes(o.format))return\"integer\";if([\"float\",\"double\"].includes(o.format))return\"number\";if(Object.hasOwn(o,\"minimum\")||Object.hasOwn(o,\"maximum\")||Object.hasOwn(o,\"exclusiveMinimum\")||Object.hasOwn(o,\"exclusiveMaximum\")||Object.hasOwn(o,\"multipleOf\"))return\"number | integer\";if(Object.hasOwn(o,\"pattern\")||Object.hasOwn(o,\"format\")||Object.hasOwn(o,\"minLength\")||Object.hasOwn(o,\"maxLength\")||Object.hasOwn(o,\"contentEncoding\")||Object.hasOwn(o,\"contentMediaType\"))return\"string\";if(void 0!==o.const){if(null===o.const)return\"null\";if(\"boolean\"==typeof o.const)return\"boolean\";if(\"number\"==typeof o.const)return Number.isInteger(o.const)?\"integer\":\"number\";if(\"string\"==typeof o.const)return\"string\";if(Array.isArray(o.const))return\"array<any>\";if(\"object\"==typeof o.const)return\"object\"}return null})(),handleCombiningKeywords(\"oneOf\",\" | \"),handleCombiningKeywords(\"anyOf\",\" | \"),handleCombiningKeywords(\"allOf\",\" & \")].filter(Boolean).join(\" | \");return i.delete(o),x||\"any\"};return getType},isBooleanJSONSchema=s=>\"boolean\"==typeof s,hasKeyword=(s,o)=>null!==s&&\"object\"==typeof s&&Object.hasOwn(s,o),fn_makeIsExpandable=s=>o=>{const i=s();return o?.$schema||o?.$vocabulary||o?.$id||o?.$anchor||o?.$dynamicAnchor||o?.$ref||o?.$dynamicRef||o?.$defs||o?.$comment||o?.allOf||o?.anyOf||o?.oneOf||i.hasKeyword(o,\"not\")||i.hasKeyword(o,\"if\")||i.hasKeyword(o,\"then\")||i.hasKeyword(o,\"else\")||o?.dependentSchemas||o?.prefixItems||i.hasKeyword(o,\"items\")||i.hasKeyword(o,\"contains\")||o?.properties||o?.patternProperties||i.hasKeyword(o,\"additionalProperties\")||i.hasKeyword(o,\"propertyNames\")||i.hasKeyword(o,\"unevaluatedItems\")||i.hasKeyword(o,\"unevaluatedProperties\")||o?.description||o?.enum||i.hasKeyword(o,\"const\")||i.hasKeyword(o,\"contentSchema\")||i.hasKeyword(o,\"default\")||o?.examples||i.getExtensionKeywords(o).length>0},fn_stringify=s=>null===s||[\"number\",\"bigint\",\"boolean\"].includes(typeof s)?String(s):Array.isArray(s)?`[${s.map(fn_stringify).join(\", \")}]`:JSON.stringify(s),stringifyConstraintRange=(s,o,i)=>{const a=\"number\"==typeof o,u=\"number\"==typeof i;return a&&u?o===i?`${o} ${s}`:`[${o}, ${i}] ${s}`:a?`≥ ${o} ${s}`:u?`≤ ${i} ${s}`:null},stringifyConstraints=s=>{const o=[],i=(s=>{if(\"number\"!=typeof s?.multipleOf)return null;if(s.multipleOf<=0)return null;if(1===s.multipleOf)return null;const{multipleOf:o}=s;if(Number.isInteger(o))return`multiple of ${o}`;const i=10**o.toString().split(\".\")[1].length;return`multiple of ${o*i}/${i}`})(s);null!==i&&o.push({scope:\"number\",value:i});const a=(s=>{const o=s?.minimum,i=s?.maximum,a=s?.exclusiveMinimum,u=s?.exclusiveMaximum,_=\"number\"==typeof o,w=\"number\"==typeof i,x=\"number\"==typeof a,C=\"number\"==typeof u,j=x&&(!_||o<a),L=C&&(!w||i>u);if((_||x)&&(w||C))return`${j?\"(\":\"[\"}${j?a:o}, ${L?u:i}${L?\")\":\"]\"}`;if(_||x)return`${j?\">\":\"≥\"} ${j?a:o}`;if(w||C)return`${L?\"<\":\"≤\"} ${L?u:i}`;return null})(s);null!==a&&o.push({scope:\"number\",value:a}),s?.format&&o.push({scope:\"string\",value:s.format});const u=stringifyConstraintRange(\"characters\",s?.minLength,s?.maxLength);null!==u&&o.push({scope:\"string\",value:u}),s?.pattern&&o.push({scope:\"string\",value:`matches ${s?.pattern}`}),s?.contentMediaType&&o.push({scope:\"string\",value:`media type: ${s.contentMediaType}`}),s?.contentEncoding&&o.push({scope:\"string\",value:`encoding: ${s.contentEncoding}`});const _=stringifyConstraintRange(s?.uniqueItems?\"unique items\":\"items\",s?.minItems,s?.maxItems);null!==_&&o.push({scope:\"array\",value:_}),s?.uniqueItems&&!_&&o.push({scope:\"array\",value:\"unique\"});const w=stringifyConstraintRange(\"contained items\",s?.minContains,s?.maxContains);null!==w&&o.push({scope:\"array\",value:w});const x=stringifyConstraintRange(\"properties\",s?.minProperties,s?.maxProperties);return null!==x&&o.push({scope:\"object\",value:x}),o},getDependentRequired=(s,o)=>o?.dependentRequired?Array.from(Object.entries(o.dependentRequired).reduce(((o,[i,a])=>Array.isArray(a)&&a.includes(s)?(o.add(i),o):o),new Set)):[],fn_isPlainObject=s=>\"object\"==typeof s&&null!==s&&!Array.isArray(s)&&(null===Object.getPrototypeOf(s)||Object.getPrototypeOf(s)===Object.prototype),getSchemaKeywords=()=>[\"$schema\",\"$vocabulary\",\"$id\",\"$anchor\",\"$dynamicAnchor\",\"$dynamicRef\",\"$ref\",\"$defs\",\"$comment\",\"allOf\",\"anyOf\",\"oneOf\",\"not\",\"if\",\"then\",\"else\",\"dependentSchemas\",\"prefixItems\",\"items\",\"contains\",\"properties\",\"patternProperties\",\"additionalProperties\",\"propertyNames\",\"unevaluatedItems\",\"unevaluatedProperties\",\"type\",\"enum\",\"const\",\"multipleOf\",\"maximum\",\"exclusiveMaximum\",\"minimum\",\"exclusiveMinimum\",\"maxLength\",\"minLength\",\"pattern\",\"maxItems\",\"minItems\",\"uniqueItems\",\"maxContains\",\"minContains\",\"maxProperties\",\"minProperties\",\"required\",\"dependentRequired\",\"title\",\"description\",\"default\",\"deprecated\",\"readOnly\",\"writeOnly\",\"examples\",\"format\",\"contentEncoding\",\"contentMediaType\",\"contentSchema\"],makeGetExtensionKeywords=s=>o=>{const i=s().getSchemaKeywords();return fn_isPlainObject(o)?((s,o)=>{const i=new Set(o);return s.filter((s=>!i.has(s)))})(Object.keys(o),i):[]},fn_hasSchemaType=(s,o)=>{const i=ze.Map.isMap(s);if(!i&&!fn_isPlainObject(s))return!1;const hasType=s=>o===s||Array.isArray(o)&&o.includes(s),a=i?s.get(\"type\"):s.type;return ze.List.isList(a)||Array.isArray(a)?a.some((s=>hasType(s))):hasType(a)},Constraint=({constraint:s})=>fn_isPlainObject(s)&&\"string\"==typeof s.scope&&\"string\"==typeof s.value?Re.createElement(\"span\",{className:`json-schema-2020-12__constraint json-schema-2020-12__constraint--${s.scope}`},s.value):null,cT=Re.memo(Constraint),DependentRequired_DependentRequired=({dependentRequired:s})=>Array.isArray(s)&&0!==s.length?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentRequired\"},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Required when defined\"),Re.createElement(\"ul\",null,s.map((s=>Re.createElement(\"li\",{key:s},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--warning\"},s)))))):null,keywords_ContentSchema=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONSchema\");if(!o.hasKeyword(s,\"contentSchema\"))return null;const a=Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary\"},\"Content schema\");return Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--contentSchema\"},Re.createElement(i,{name:a,schema:s.contentSchema,identifier:\"contentSchema\"}))},Title_Title=({title:s=\"\",schema:o})=>{const i=useFn(),a=s||i.getTitle(o);return a?Re.createElement(\"div\",{className:\"json-schema-2020-12__title\"},a):null},keywords_Description_Description=({schema:s})=>s?.description?Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--description\"},Re.createElement(\"div\",{className:\"json-schema-2020-12-core-keyword__value json-schema-2020-12-core-keyword__value--secondary\"},s.description)):null,Default_Default=({schema:s})=>{const o=useFn(),i=useComponent(\"JSONViewer\");return o.hasKeyword(s,\"default\")?Re.createElement(i,{name:\"Default\",value:s.default,className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--default\"}):null},keywords_Deprecated=({schema:s})=>!0!==s?.deprecated?null:Re.createElement(\"span\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--warning\"},\"deprecated\"),keywords_ReadOnly=({schema:s})=>!0!==s?.readOnly?null:Re.createElement(\"span\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted\"},\"read-only\"),keywords_WriteOnly=({schema:s})=>!0!==s?.writeOnly?null:Re.createElement(\"span\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted\"},\"write-only\"),keywords_Examples_Examples=({schema:s})=>{const o=s?.examples||[],i=useComponent(\"JSONViewer\");return Array.isArray(o)&&0!==o.length?Re.createElement(i,{name:\"Examples\",value:s.examples,className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--examples\"}):null},ExtensionKeywords_ExtensionKeywords=({schema:s})=>{const o=useFn(),i=\"ExtensionKeywords\",{path:a}=usePath(i),{isExpanded:u,setExpanded:_,setCollapsed:w}=useIsExpanded(i),[x,C]=useLevel(),j=useComponent(\"Accordion\"),L=useComponent(\"ExpandDeepButton\"),B=useComponent(\"JSONViewer\"),{showExtensionKeywords:$}=useConfig(),U=o.getExtensionKeywords(s),V=(0,Re.useCallback)((()=>{u?w():_()}),[u,_,w]),z=(0,Re.useCallback)(((s,o)=>{o?_({deep:!0}):w({deep:!0})}),[_,w]);return $&&0!==U.length?Re.createElement(oT.Provider,{value:a},Re.createElement(nT.Provider,{value:C},Re.createElement(\"div\",{className:\"json-schema-2020-12-keyword json-schema-2020-12-keyword--extension-keywords\",\"data-json-schema-level\":x},Re.createElement(j,{expanded:u,onChange:V},Re.createElement(\"span\",{className:\"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--extension\"},\"Extension Keywords\")),Re.createElement(L,{expanded:u,onClick:z}),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-keyword__children\",{\"json-schema-2020-12-keyword__children--collapsed\":!u})},u&&Re.createElement(Re.Fragment,null,U.map((o=>Re.createElement(B,{key:o,name:o,value:s[o],className:\"json-schema-2020-12-json-viewer-extension-keyword\"})))))))):null},JSONViewer=({name:s,value:o,className:i})=>{const a=useFn(),{path:u}=usePath(s),{isExpanded:_,setExpanded:w,setCollapsed:x}=useIsExpanded(s),[C,j]=useLevel(),L=useComponent(\"Accordion\"),B=useComponent(\"ExpandDeepButton\"),$=\"string\"==typeof o||\"number\"==typeof o||\"bigint\"==typeof o||\"boolean\"==typeof o||\"symbol\"==typeof o||null==o,U=(s=>fn_isPlainObject(s)&&0===Object.keys(s).length)(o)||(s=>Array.isArray(s)&&0===s.length)(o),V=(0,Re.useCallback)((()=>{_?x():w()}),[_,w,x]),z=(0,Re.useCallback)(((s,o)=>{o?w({deep:!0}):x({deep:!0})}),[w,x]);return $?Re.createElement(\"div\",{className:Jn()(\"json-schema-2020-12-json-viewer\",i)},Re.createElement(\"span\",{className:\"json-schema-2020-12-json-viewer__name json-schema-2020-12-json-viewer__name--secondary\"},s),Re.createElement(\"span\",{className:\"json-schema-2020-12-json-viewer__value json-schema-2020-12-json-viewer__value--secondary\"},a.stringify(o))):U?Re.createElement(\"div\",{className:Jn()(\"json-schema-2020-12-json-viewer\",i)},Re.createElement(\"span\",{className:\"json-schema-2020-12-json-viewer__name json-schema-2020-12-json-viewer__name--secondary\"},s),Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},Array.isArray(o)?\"empty array\":\"empty object\")):Re.createElement(oT.Provider,{value:u},Re.createElement(nT.Provider,{value:j},Re.createElement(\"div\",{className:Jn()(\"json-schema-2020-12-json-viewer\",i),\"data-json-schema-level\":C},Re.createElement(L,{expanded:_,onChange:V},Re.createElement(\"span\",{className:\"json-schema-2020-12-json-viewer__name json-schema-2020-12-json-viewer__name--secondary\"},s)),Re.createElement(B,{expanded:_,onClick:z}),Re.createElement(\"strong\",{className:\"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary\"},Array.isArray(o)?\"array\":\"object\"),Re.createElement(\"ul\",{className:Jn()(\"json-schema-2020-12-json-viewer__children\",{\"json-schema-2020-12-json-viewer__children--collapsed\":!_})},_&&Re.createElement(Re.Fragment,null,Array.isArray(o)?o.map(((s,o)=>Re.createElement(\"li\",{key:`#${o}`,className:\"json-schema-2020-12-property\"},Re.createElement(JSONViewer,{name:`#${o}`,value:s,className:i})))):Object.entries(o).map((([s,o])=>Re.createElement(\"li\",{key:s,className:\"json-schema-2020-12-property\"},Re.createElement(JSONViewer,{name:s,value:o,className:i})))))))))},lT=JSONViewer,Accordion_Accordion=({expanded:s=!1,children:o,onChange:i})=>{const a=useComponent(\"ChevronRightIcon\"),u=(0,Re.useCallback)((o=>{i(o,!s)}),[s,i]);return Re.createElement(\"button\",{type:\"button\",className:\"json-schema-2020-12-accordion\",onClick:u},Re.createElement(\"div\",{className:\"json-schema-2020-12-accordion__children\"},o),Re.createElement(\"span\",{className:Jn()(\"json-schema-2020-12-accordion__icon\",{\"json-schema-2020-12-accordion__icon--expanded\":s,\"json-schema-2020-12-accordion__icon--collapsed\":!s})},Re.createElement(a,null)))},ExpandDeepButton_ExpandDeepButton=({expanded:s,onClick:o})=>{const i=(0,Re.useCallback)((i=>{o(i,!s)}),[s,o]);return Re.createElement(\"button\",{type:\"button\",className:\"json-schema-2020-12-expand-deep-button\",onClick:i},s?\"Collapse all\":\"Expand all\")},icons_ChevronRight=()=>Re.createElement(\"svg\",{xmlns:\"http://www.w3.org/2000/svg\",width:\"24\",height:\"24\",viewBox:\"0 0 24 24\"},Re.createElement(\"path\",{d:\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"})),withJSONSchemaContext=(s,o={})=>{const i={components:{JSONSchema:aT,Keyword$schema:keywords_$schema,Keyword$vocabulary:$vocabulary_$vocabulary,Keyword$id:keywords_$id,Keyword$anchor:keywords_$anchor,Keyword$dynamicAnchor:keywords_$dynamicAnchor,Keyword$ref:keywords_$ref,Keyword$dynamicRef:keywords_$dynamicRef,Keyword$defs:keywords_$defs,Keyword$comment:keywords_$comment,KeywordAllOf:keywords_AllOf,KeywordAnyOf:keywords_AnyOf,KeywordOneOf:keywords_OneOf,KeywordNot:keywords_Not,KeywordIf:keywords_If,KeywordThen:keywords_Then,KeywordElse:keywords_Else,KeywordDependentSchemas:keywords_DependentSchemas,KeywordPrefixItems:keywords_PrefixItems,KeywordItems:keywords_Items,KeywordContains:keywords_Contains,KeywordProperties:keywords_Properties_Properties,KeywordPatternProperties:PatternProperties_PatternProperties,KeywordAdditionalProperties:keywords_AdditionalProperties,KeywordPropertyNames:keywords_PropertyNames,KeywordUnevaluatedItems:keywords_UnevaluatedItems,KeywordUnevaluatedProperties:keywords_UnevaluatedProperties,KeywordType:keywords_Type,KeywordEnum:Enum_Enum,KeywordConst:Const_Const,KeywordConstraint:cT,KeywordDependentRequired:DependentRequired_DependentRequired,KeywordContentSchema:keywords_ContentSchema,KeywordTitle:Title_Title,KeywordDescription:keywords_Description_Description,KeywordDefault:Default_Default,KeywordDeprecated:keywords_Deprecated,KeywordReadOnly:keywords_ReadOnly,KeywordWriteOnly:keywords_WriteOnly,KeywordExamples:keywords_Examples_Examples,ExtensionKeywords:ExtensionKeywords_ExtensionKeywords,JSONViewer:lT,Accordion:Accordion_Accordion,ExpandDeepButton:ExpandDeepButton_ExpandDeepButton,ChevronRightIcon:icons_ChevronRight,...o.components},config:{default$schema:\"https://json-schema.org/draft/2020-12/schema\",defaultExpandedLevels:0,showExtensionKeywords:!0,...o.config},fn:{upperFirst:fn_upperFirst,getTitle:makeGetTitle(useFn),getType:makeGetType(useFn),isBooleanJSONSchema,hasKeyword,isExpandable:fn_makeIsExpandable(useFn),stringify:fn_stringify,stringifyConstraints,getDependentRequired,getSchemaKeywords,getExtensionKeywords:makeGetExtensionKeywords(useFn),...o.fn},state:{paths:{}}},HOC=o=>Re.createElement(rT.Provider,{value:i},Re.createElement(s,o));return HOC.contexts={JSONSchemaContext:rT},HOC.displayName=s.displayName,HOC},makeWithJSONSchemaSystemContext=({getSystem:s})=>(o,i={})=>{const{getComponent:a,getConfigs:u}=s(),_=u(),w=a(\"JSONSchema202012\"),x=a(\"JSONSchema202012Keyword$schema\"),C=a(\"JSONSchema202012Keyword$vocabulary\"),j=a(\"JSONSchema202012Keyword$id\"),L=a(\"JSONSchema202012Keyword$anchor\"),B=a(\"JSONSchema202012Keyword$dynamicAnchor\"),$=a(\"JSONSchema202012Keyword$ref\"),U=a(\"JSONSchema202012Keyword$dynamicRef\"),V=a(\"JSONSchema202012Keyword$defs\"),z=a(\"JSONSchema202012Keyword$comment\"),Y=a(\"JSONSchema202012KeywordAllOf\"),Z=a(\"JSONSchema202012KeywordAnyOf\"),ee=a(\"JSONSchema202012KeywordOneOf\"),ie=a(\"JSONSchema202012KeywordNot\"),ae=a(\"JSONSchema202012KeywordIf\"),ce=a(\"JSONSchema202012KeywordThen\"),le=a(\"JSONSchema202012KeywordElse\"),pe=a(\"JSONSchema202012KeywordDependentSchemas\"),de=a(\"JSONSchema202012KeywordPrefixItems\"),fe=a(\"JSONSchema202012KeywordItems\"),ye=a(\"JSONSchema202012KeywordContains\"),be=a(\"JSONSchema202012KeywordProperties\"),_e=a(\"JSONSchema202012KeywordPatternProperties\"),Se=a(\"JSONSchema202012KeywordAdditionalProperties\"),we=a(\"JSONSchema202012KeywordPropertyNames\"),xe=a(\"JSONSchema202012KeywordUnevaluatedItems\"),Pe=a(\"JSONSchema202012KeywordUnevaluatedProperties\"),Te=a(\"JSONSchema202012KeywordType\"),Re=a(\"JSONSchema202012KeywordEnum\"),$e=a(\"JSONSchema202012KeywordConst\"),qe=a(\"JSONSchema202012KeywordConstraint\"),ze=a(\"JSONSchema202012KeywordDependentRequired\"),We=a(\"JSONSchema202012KeywordContentSchema\"),He=a(\"JSONSchema202012KeywordTitle\"),Ye=a(\"JSONSchema202012KeywordDescription\"),Xe=a(\"JSONSchema202012KeywordDefault\"),Qe=a(\"JSONSchema202012KeywordDeprecated\"),et=a(\"JSONSchema202012KeywordReadOnly\"),tt=a(\"JSONSchema202012KeywordWriteOnly\"),rt=a(\"JSONSchema202012KeywordExamples\"),nt=a(\"JSONSchema202012ExtensionKeywords\"),st=a(\"JSONSchema202012JSONViewer\"),ot=a(\"JSONSchema202012Accordion\"),it=a(\"JSONSchema202012ExpandDeepButton\"),at=a(\"JSONSchema202012ChevronRightIcon\");return withJSONSchemaContext(o,{components:{JSONSchema:w,Keyword$schema:x,Keyword$vocabulary:C,Keyword$id:j,Keyword$anchor:L,Keyword$dynamicAnchor:B,Keyword$ref:$,Keyword$dynamicRef:U,Keyword$defs:V,Keyword$comment:z,KeywordAllOf:Y,KeywordAnyOf:Z,KeywordOneOf:ee,KeywordNot:ie,KeywordIf:ae,KeywordThen:ce,KeywordElse:le,KeywordDependentSchemas:pe,KeywordPrefixItems:de,KeywordItems:fe,KeywordContains:ye,KeywordProperties:be,KeywordPatternProperties:_e,KeywordAdditionalProperties:Se,KeywordPropertyNames:we,KeywordUnevaluatedItems:xe,KeywordUnevaluatedProperties:Pe,KeywordType:Te,KeywordEnum:Re,KeywordConst:$e,KeywordConstraint:qe,KeywordDependentRequired:ze,KeywordContentSchema:We,KeywordTitle:He,KeywordDescription:Ye,KeywordDefault:Xe,KeywordDeprecated:Qe,KeywordReadOnly:et,KeywordWriteOnly:tt,KeywordExamples:rt,ExtensionKeywords:nt,JSONViewer:st,Accordion:ot,ExpandDeepButton:it,ChevronRightIcon:at,...i.components},config:{showExtensionKeywords:_.showExtensions,...i.config},fn:{...i.fn}})},json_schema_2020_12=({getSystem:s,fn:o})=>{const fnAccessor=()=>({upperFirst:o.upperFirst,...o.jsonSchema202012});return{components:{JSONSchema202012:aT,JSONSchema202012Keyword$schema:keywords_$schema,JSONSchema202012Keyword$vocabulary:$vocabulary_$vocabulary,JSONSchema202012Keyword$id:keywords_$id,JSONSchema202012Keyword$anchor:keywords_$anchor,JSONSchema202012Keyword$dynamicAnchor:keywords_$dynamicAnchor,JSONSchema202012Keyword$ref:keywords_$ref,JSONSchema202012Keyword$dynamicRef:keywords_$dynamicRef,JSONSchema202012Keyword$defs:keywords_$defs,JSONSchema202012Keyword$comment:keywords_$comment,JSONSchema202012KeywordAllOf:keywords_AllOf,JSONSchema202012KeywordAnyOf:keywords_AnyOf,JSONSchema202012KeywordOneOf:keywords_OneOf,JSONSchema202012KeywordNot:keywords_Not,JSONSchema202012KeywordIf:keywords_If,JSONSchema202012KeywordThen:keywords_Then,JSONSchema202012KeywordElse:keywords_Else,JSONSchema202012KeywordDependentSchemas:keywords_DependentSchemas,JSONSchema202012KeywordPrefixItems:keywords_PrefixItems,JSONSchema202012KeywordItems:keywords_Items,JSONSchema202012KeywordContains:keywords_Contains,JSONSchema202012KeywordProperties:keywords_Properties_Properties,JSONSchema202012KeywordPatternProperties:PatternProperties_PatternProperties,JSONSchema202012KeywordAdditionalProperties:keywords_AdditionalProperties,JSONSchema202012KeywordPropertyNames:keywords_PropertyNames,JSONSchema202012KeywordUnevaluatedItems:keywords_UnevaluatedItems,JSONSchema202012KeywordUnevaluatedProperties:keywords_UnevaluatedProperties,JSONSchema202012KeywordType:keywords_Type,JSONSchema202012KeywordEnum:Enum_Enum,JSONSchema202012KeywordConst:Const_Const,JSONSchema202012KeywordConstraint:cT,JSONSchema202012KeywordDependentRequired:DependentRequired_DependentRequired,JSONSchema202012KeywordContentSchema:keywords_ContentSchema,JSONSchema202012KeywordTitle:Title_Title,JSONSchema202012KeywordDescription:keywords_Description_Description,JSONSchema202012KeywordDefault:Default_Default,JSONSchema202012KeywordDeprecated:keywords_Deprecated,JSONSchema202012KeywordReadOnly:keywords_ReadOnly,JSONSchema202012KeywordWriteOnly:keywords_WriteOnly,JSONSchema202012KeywordExamples:keywords_Examples_Examples,JSONSchema202012ExtensionKeywords:ExtensionKeywords_ExtensionKeywords,JSONSchema202012JSONViewer:lT,JSONSchema202012Accordion:Accordion_Accordion,JSONSchema202012ExpandDeepButton:ExpandDeepButton_ExpandDeepButton,JSONSchema202012ChevronRightIcon:icons_ChevronRight,withJSONSchema202012Context:withJSONSchemaContext,withJSONSchema202012SystemContext:makeWithJSONSchemaSystemContext(s()),JSONSchema202012PathContext:()=>oT,JSONSchema202012LevelContext:()=>nT},fn:{upperFirst:fn_upperFirst,jsonSchema202012:{getTitle:makeGetTitle(fnAccessor),getType:makeGetType(fnAccessor),isExpandable:fn_makeIsExpandable(fnAccessor),isBooleanJSONSchema,hasKeyword,useFn,useConfig,useComponent,useIsExpanded,usePath,useLevel,getSchemaKeywords,getExtensionKeywords:makeGetExtensionKeywords(fnAccessor),hasSchemaType:fn_hasSchemaType}}}},array=(s,{sample:o=[]}={})=>((s,o={})=>{const{minItems:i,maxItems:a,uniqueItems:u}=o,{contains:_,minContains:w,maxContains:x}=o;let C=[...s];if(null!=_&&\"object\"==typeof _){if(Number.isInteger(w)&&w>1){const s=C.at(0);for(let o=1;o<w;o+=1)C.unshift(s)}Number.isInteger(x)}if(Number.isInteger(a)&&a>0&&(C=s.slice(0,a)),Number.isInteger(i)&&i>0)for(let s=0;C.length<i;s+=1)C.push(C[s%C.length]);return!0===u&&(C=Array.from(new Set(C))),C})(o,s),object=()=>{throw new Error(\"Not implemented\")},bytes=s=>xt()(s),random_pick=s=>s.at(0),predicates_isBooleanJSONSchema=s=>\"boolean\"==typeof s,isJSONSchemaObject=s=>as()(s),isJSONSchema=s=>predicates_isBooleanJSONSchema(s)||isJSONSchemaObject(s);const uT=class Registry{data={};register(s,o){this.data[s]=o}unregister(s){void 0===s?this.data={}:delete this.data[s]}get(s){return this.data[s]}},int32=()=>0,int64=()=>0,generators_float=()=>.1,generators_double=()=>.1,email=()=>\"user@example.com\",idn_email=()=>\"실례@example.com\",hostname=()=>\"example.com\",idn_hostname=()=>\"실례.com\",ipv4=()=>\"198.51.100.42\",ipv6=()=>\"2001:0db8:5b96:0000:0000:426f:8e17:642a\",uri=()=>\"https://example.com/\",uri_reference=()=>\"path/index.html\",iri=()=>\"https://실례.com/\",iri_reference=()=>\"path/실례.html\",uuid=()=>\"3fa85f64-5717-4562-b3fc-2c963f66afa6\",uri_template=()=>\"https://example.com/dictionary/{term:1}/{term}\",generators_json_pointer=()=>\"/a/b/c\",relative_json_pointer=()=>\"1/0\",date_time=()=>(new Date).toISOString(),date=()=>(new Date).toISOString().substring(0,10),time=()=>(new Date).toISOString().substring(11),duration=()=>\"P3D\",generators_password=()=>\"********\",regex=()=>\"^[a-z]+$\";const pT=new class FormatRegistry extends uT{#s={int32,int64,float:generators_float,double:generators_double,email,\"idn-email\":idn_email,hostname,\"idn-hostname\":idn_hostname,ipv4,ipv6,uri,\"uri-reference\":uri_reference,iri,\"iri-reference\":iri_reference,uuid,\"uri-template\":uri_template,\"json-pointer\":generators_json_pointer,\"relative-json-pointer\":relative_json_pointer,\"date-time\":date_time,date,time,duration,password:generators_password,regex};data={...this.#s};get defaults(){return{...this.#s}}},formatAPI=(s,o)=>\"function\"==typeof o?pT.register(s,o):null===o?pT.unregister(s):pT.get(s);formatAPI.getDefaults=()=>pT.defaults;const hT=formatAPI;var dT=__webpack_require__(48287).Buffer;const _7bit=s=>dT.from(s).toString(\"ascii\");var fT=__webpack_require__(48287).Buffer;const _8bit=s=>fT.from(s).toString(\"utf8\");var mT=__webpack_require__(48287).Buffer;const encoders_binary=s=>mT.from(s).toString(\"binary\"),quoted_printable=s=>{let o=\"\";for(let i=0;i<s.length;i++){const a=s.charCodeAt(i);if(61===a)o+=\"=3D\";else if(a>=33&&a<=60||a>=62&&a<=126||9===a||32===a)o+=s.charAt(i);else if(13===a||10===a)o+=\"\\r\\n\";else if(a>126){const a=unescape(encodeURIComponent(s.charAt(i)));for(let s=0;s<a.length;s++)o+=\"=\"+(\"0\"+a.charCodeAt(s).toString(16)).slice(-2).toUpperCase()}else o+=\"=\"+(\"0\"+a.toString(16)).slice(-2).toUpperCase()}return o};var gT=__webpack_require__(48287).Buffer;const base16=s=>gT.from(s).toString(\"hex\");var yT=__webpack_require__(48287).Buffer;const base32=s=>{const o=yT.from(s).toString(\"utf8\"),i=\"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";let a=0,u=\"\",_=0,w=0;for(let s=0;s<o.length;s++)for(_=_<<8|o.charCodeAt(s),w+=8;w>=5;)u+=i.charAt(_>>>w-5&31),w-=5;w>0&&(u+=i.charAt(_<<5-w&31),a=(8-8*o.length%5)%5);for(let s=0;s<a;s++)u+=\"=\";return u};var vT=__webpack_require__(48287).Buffer;const base64=s=>vT.from(s).toString(\"base64\");var bT=__webpack_require__(48287).Buffer;const base64url=s=>bT.from(s).toString(\"base64url\");const _T=new class EncoderRegistry extends uT{#s={\"7bit\":_7bit,\"8bit\":_8bit,binary:encoders_binary,\"quoted-printable\":quoted_printable,base16,base32,base64,base64url};data={...this.#s};get defaults(){return{...this.#s}}},encoderAPI=(s,o)=>\"function\"==typeof o?_T.register(s,o):null===o?_T.unregister(s):_T.get(s);encoderAPI.getDefaults=()=>_T.defaults;const ST=encoderAPI,ET={\"text/plain\":()=>\"string\",\"text/css\":()=>\".selector { border: 1px solid red }\",\"text/csv\":()=>\"value1,value2,value3\",\"text/html\":()=>\"<p>content</p>\",\"text/calendar\":()=>\"BEGIN:VCALENDAR\",\"text/javascript\":()=>\"console.dir('Hello world!');\",\"text/xml\":()=>'<person age=\"30\">John Doe</person>',\"text/*\":()=>\"string\"},wT={\"image/*\":()=>bytes(25).toString(\"binary\")},xT={\"audio/*\":()=>bytes(25).toString(\"binary\")},kT={\"video/*\":()=>bytes(25).toString(\"binary\")},OT={\"application/json\":()=>'{\"key\":\"value\"}',\"application/ld+json\":()=>'{\"name\": \"John Doe\"}',\"application/x-httpd-php\":()=>\"<?php echo '<p>Hello World!</p>'; ?>\",\"application/rtf\":()=>String.raw`{\\rtf1\\adeflang1025\\ansi\\ansicpg1252\\uc1`,\"application/x-sh\":()=>'echo \"Hello World!\"',\"application/xhtml+xml\":()=>\"<p>content</p>\",\"application/*\":()=>bytes(25).toString(\"binary\")};const AT=new class MediaTypeRegistry extends uT{#s={...ET,...wT,...xT,...kT,...OT};data={...this.#s};get defaults(){return{...this.#s}}},mediaTypeAPI=(s,o)=>{if(\"function\"==typeof o)return AT.register(s,o);if(null===o)return AT.unregister(s);const i=s.split(\";\").at(0),a=`${i.split(\"/\").at(0)}/*`;return AT.get(s)||AT.get(i)||AT.get(a)};mediaTypeAPI.getDefaults=()=>AT.defaults;const CT=mediaTypeAPI,applyStringConstraints=(s,o={})=>{const{maxLength:i,minLength:a}=o;let u=s;if(Number.isInteger(i)&&i>0&&(u=u.slice(0,i)),Number.isInteger(a)&&a>0){let s=0;for(;u.length<a;)u+=u[s++%u.length]}return u},types_string=(s,{sample:o}={})=>{const{contentEncoding:i,contentMediaType:a,contentSchema:u}=s,{pattern:_,format:w}=s,x=ST(i)||gO();let C;return C=\"string\"==typeof _?applyStringConstraints((s=>{try{const o=/(?<=(?<!\\\\)\\{)(\\d{3,})(?=\\})|(?<=(?<!\\\\)\\{\\d*,)(\\d{3,})(?=\\})|(?<=(?<!\\\\)\\{)(\\d{3,})(?=,\\d*\\})/g,i=s.replace(o,\"100\"),a=new(ps())(i);return a.max=100,a.gen()}catch{return\"string\"}})(_),s):\"string\"==typeof w?(s=>{const{format:o}=s,i=hT(o);return\"function\"==typeof i?i(s):\"string\"})(s):isJSONSchema(u)&&\"string\"==typeof a&&void 0!==o?Array.isArray(o)||\"object\"==typeof o?JSON.stringify(o):applyStringConstraints(String(o),s):\"string\"==typeof a?(s=>{const{contentMediaType:o}=s,i=CT(o);return\"function\"==typeof i?i(s):\"string\"})(s):applyStringConstraints(\"string\",s),x(C)},applyNumberConstraints=(s,o={})=>{const{minimum:i,maximum:a,exclusiveMinimum:u,exclusiveMaximum:_}=o,{multipleOf:w}=o,x=Number.isInteger(s)?1:Number.EPSILON;let C=\"number\"==typeof i?i:null,j=\"number\"==typeof a?a:null,L=s;if(\"number\"==typeof u&&(C=null!==C?Math.max(C,u+x):u+x),\"number\"==typeof _&&(j=null!==j?Math.min(j,_-x):_-x),L=C>j&&s||C||j||L,\"number\"==typeof w&&w>0){const s=L%w;L=0===s?L:L+w-s}return L},types_number=s=>{const{format:o}=s;let i;return i=\"string\"==typeof o?(s=>{const{format:o}=s,i=hT(o);return\"function\"==typeof i?i(s):0})(s):0,applyNumberConstraints(i,s)},types_integer=s=>{const{format:o}=s;let i;return i=\"string\"==typeof o?(s=>{const{format:o}=s,i=hT(o);if(\"function\"==typeof i)return i(s);switch(o){case\"int32\":return int32();case\"int64\":return int64()}return 0})(s):0,applyNumberConstraints(i,s)},types_boolean=s=>\"boolean\"!=typeof s.default||s.default,jT=new Proxy({array,object,string:types_string,number:types_number,integer:types_integer,boolean:types_boolean,null:()=>null},{get:(s,o)=>\"string\"==typeof o&&Object.hasOwn(s,o)?s[o]:()=>`Unknown Type: ${o}`}),PT=[\"array\",\"object\",\"number\",\"integer\",\"string\",\"boolean\",\"null\"],hasExample=s=>{if(!isJSONSchemaObject(s))return!1;const{examples:o,example:i,default:a}=s;return!!(Array.isArray(o)&&o.length>=1)||(void 0!==a||void 0!==i)},extractExample=s=>{if(!isJSONSchemaObject(s))return null;const{examples:o,example:i,default:a}=s;return Array.isArray(o)&&o.length>=1?o.at(0):void 0!==a?a:void 0!==i?i:void 0},IT={array:[\"items\",\"prefixItems\",\"contains\",\"maxContains\",\"minContains\",\"maxItems\",\"minItems\",\"uniqueItems\",\"unevaluatedItems\"],object:[\"properties\",\"additionalProperties\",\"patternProperties\",\"propertyNames\",\"minProperties\",\"maxProperties\",\"required\",\"dependentSchemas\",\"dependentRequired\",\"unevaluatedProperties\"],string:[\"pattern\",\"format\",\"minLength\",\"maxLength\",\"contentEncoding\",\"contentMediaType\",\"contentSchema\"],integer:[\"minimum\",\"maximum\",\"exclusiveMinimum\",\"exclusiveMaximum\",\"multipleOf\"]};IT.number=IT.integer;const TT=\"string\",inferTypeFromValue=s=>void 0===s?null:null===s?\"null\":Array.isArray(s)?\"array\":Number.isInteger(s)?\"integer\":typeof s,foldType=s=>{if(Array.isArray(s)&&s.length>=1){if(s.includes(\"array\"))return\"array\";if(s.includes(\"object\"))return\"object\";{const o=s.filter((s=>\"null\"!==s)),i=random_pick(o.length>0?o:s);if(PT.includes(i))return i}}return PT.includes(s)?s:null},inferType=(s,o=new WeakSet)=>{if(!isJSONSchemaObject(s))return TT;if(o.has(s))return TT;o.add(s);let{type:i,const:a}=s;if(i=foldType(i),\"string\"!=typeof i){const o=Object.keys(IT);e:for(let a=0;a<o.length;a+=1){const u=o[a],_=IT[u];for(let o=0;o<_.length;o+=1){const a=_[o];if(Object.hasOwn(s,a)){i=u;break e}}}}if(\"string\"!=typeof i&&void 0!==a){const s=inferTypeFromValue(a);i=\"string\"==typeof s?s:i}if(\"string\"!=typeof i){const combineTypes=i=>{if(Array.isArray(s[i])){const a=s[i].map((s=>inferType(s,o)));return foldType(a)}return null},a=combineTypes(\"allOf\"),u=combineTypes(\"anyOf\"),_=combineTypes(\"oneOf\"),w=s.not?inferType(s.not,o):null;(a||u||_||w)&&(i=foldType([a,u,_,w].filter(Boolean)))}if(\"string\"!=typeof i&&hasExample(s)){const o=extractExample(s),a=inferTypeFromValue(o);i=\"string\"==typeof a?a:i}return o.delete(s),i||TT},type_getType=s=>inferType(s),typeCast=s=>predicates_isBooleanJSONSchema(s)?(s=>!1===s?{not:{}}:{})(s):isJSONSchemaObject(s)?s:{},merge_merge=(s,o,i={})=>{if(predicates_isBooleanJSONSchema(s)&&!0===s)return!0;if(predicates_isBooleanJSONSchema(s)&&!1===s)return!1;if(predicates_isBooleanJSONSchema(o)&&!0===o)return!0;if(predicates_isBooleanJSONSchema(o)&&!1===o)return!1;if(!isJSONSchema(s))return o;if(!isJSONSchema(o))return s;const a={...o,...s};if(o.type&&s.type&&Array.isArray(o.type)&&\"string\"==typeof o.type){const i=normalizeArray(o.type).concat(s.type);a.type=Array.from(new Set(i))}if(Array.isArray(o.required)&&Array.isArray(s.required)&&(a.required=[...new Set([...s.required,...o.required])]),o.properties&&s.properties){const u=new Set([...Object.keys(o.properties),...Object.keys(s.properties)]);a.properties={};for(const _ of u){const u=o.properties[_]||{},w=s.properties[_]||{};u.readOnly&&!i.includeReadOnly||u.writeOnly&&!i.includeWriteOnly?a.required=(a.required||[]).filter((s=>s!==_)):a.properties[_]=merge_merge(w,u,i)}}return isJSONSchema(o.items)&&isJSONSchema(s.items)&&(a.items=merge_merge(s.items,o.items,i)),isJSONSchema(o.contains)&&isJSONSchema(s.contains)&&(a.contains=merge_merge(s.contains,o.contains,i)),isJSONSchema(o.contentSchema)&&isJSONSchema(s.contentSchema)&&(a.contentSchema=merge_merge(s.contentSchema,o.contentSchema,i)),a},NT=merge_merge,main_sampleFromSchemaGeneric=(s,o={},i=void 0,a=!1)=>{if(null==s&&void 0===i)return;\"function\"==typeof s?.toJS&&(s=s.toJS()),s=typeCast(s);let u=void 0!==i||hasExample(s);const _=!u&&Array.isArray(s.oneOf)&&s.oneOf.length>0,w=!u&&Array.isArray(s.anyOf)&&s.anyOf.length>0;if(!u&&(_||w)){const i=typeCast(random_pick(_?s.oneOf:s.anyOf));!(s=NT(s,i,o)).xml&&i.xml&&(s.xml=i.xml),hasExample(s)&&hasExample(i)&&(u=!0)}const x={};let{xml:C,properties:j,additionalProperties:L,items:B,contains:$}=s||{},U=type_getType(s),{includeReadOnly:V,includeWriteOnly:z}=o;C=C||{};let Y,{name:Z,prefix:ee,namespace:ie}=C,ae={};if(Object.hasOwn(s,\"type\")||(s.type=U),a&&(Z=Z||\"notagname\",Y=(ee?`${ee}:`:\"\")+Z,ie)){x[ee?`xmlns:${ee}`:\"xmlns\"]=ie}a&&(ae[Y]=[]);const ce=objectify(j);let le,pe=0;const hasExceededMaxProperties=()=>Number.isInteger(s.maxProperties)&&s.maxProperties>0&&pe>=s.maxProperties,canAddProperty=o=>!(Number.isInteger(s.maxProperties)&&s.maxProperties>0)||!hasExceededMaxProperties()&&(!(o=>!Array.isArray(s.required)||0===s.required.length||!s.required.includes(o))(o)||s.maxProperties-pe-(()=>{if(!Array.isArray(s.required)||0===s.required.length)return 0;let o=0;return a?s.required.forEach((s=>o+=void 0===ae[s]?0:1)):s.required.forEach((s=>{o+=void 0===ae[Y]?.find((o=>void 0!==o[s]))?0:1})),s.required.length-o})()>0);if(le=a?(i,u=void 0)=>{if(s&&ce[i]){if(ce[i].xml=ce[i].xml||{},ce[i].xml.attribute){const s=Array.isArray(ce[i].enum)?random_pick(ce[i].enum):void 0;if(hasExample(ce[i]))x[ce[i].xml.name||i]=extractExample(ce[i]);else if(void 0!==s)x[ce[i].xml.name||i]=s;else{const s=typeCast(ce[i]),a=type_getType(s),_=ce[i].xml.name||i;if(\"array\"===a){const s=main_sampleFromSchemaGeneric(ce[i],o,u,!1);x[_]=s.map((s=>as()(s)?\"UnknownTypeObject\":Array.isArray(s)?\"UnknownTypeArray\":s)).join(\" \")}else x[_]=\"object\"===a?\"UnknownTypeObject\":jT[a](s)}return}ce[i].xml.name=ce[i].xml.name||i}else ce[i]||!1===L||(ce[i]={xml:{name:i}});let _=main_sampleFromSchemaGeneric(ce[i],o,u,a);canAddProperty(i)&&(pe++,Array.isArray(_)?ae[Y]=ae[Y].concat(_):ae[Y].push(_))}:(i,u)=>{if(canAddProperty(i)){if(as()(s.discriminator?.mapping)&&s.discriminator.propertyName===i&&\"string\"==typeof s.$$ref){for(const o in s.discriminator.mapping)if(-1!==s.$$ref.search(s.discriminator.mapping[o])){ae[i]=o;break}}else ae[i]=main_sampleFromSchemaGeneric(ce[i],o,u,a);pe++}},u){let u;if(u=void 0!==i?i:extractExample(s),!a){if(\"number\"==typeof u&&\"string\"===U)return`${u}`;if(\"string\"!=typeof u||\"string\"===U)return u;try{return JSON.parse(u)}catch{return u}}if(\"array\"===U){if(!Array.isArray(u)){if(\"string\"==typeof u)return u;u=[u]}let i=[];return isJSONSchemaObject(B)&&(B.xml=B.xml||C||{},B.xml.name=B.xml.name||C.name,i=u.map((s=>main_sampleFromSchemaGeneric(B,o,s,a)))),isJSONSchemaObject($)&&($.xml=$.xml||C||{},$.xml.name=$.xml.name||C.name,i=[main_sampleFromSchemaGeneric($,o,void 0,a),...i]),i=jT.array(s,{sample:i}),C.wrapped?(ae[Y]=i,ds()(x)||ae[Y].push({_attr:x})):ae=i,ae}if(\"object\"===U){if(\"string\"==typeof u)return u;for(const s in u)Object.hasOwn(u,s)&&(ce[s]?.readOnly&&!V||ce[s]?.writeOnly&&!z||(ce[s]?.xml?.attribute?x[ce[s].xml.name||s]=u[s]:le(s,u[s])));return ds()(x)||ae[Y].push({_attr:x}),ae}return ae[Y]=ds()(x)?u:[{_attr:x},u],ae}if(\"array\"===U){let i=[];if(isJSONSchemaObject($))if(a&&($.xml=$.xml||s.xml||{},$.xml.name=$.xml.name||C.name),Array.isArray($.anyOf)){const{anyOf:s,...u}=B;i.push(...$.anyOf.map((s=>main_sampleFromSchemaGeneric(NT(s,u,o),o,void 0,a))))}else if(Array.isArray($.oneOf)){const{oneOf:s,...u}=B;i.push(...$.oneOf.map((s=>main_sampleFromSchemaGeneric(NT(s,u,o),o,void 0,a))))}else{if(!(!a||a&&C.wrapped))return main_sampleFromSchemaGeneric($,o,void 0,a);i.push(main_sampleFromSchemaGeneric($,o,void 0,a))}if(isJSONSchemaObject(B))if(a&&(B.xml=B.xml||s.xml||{},B.xml.name=B.xml.name||C.name),Array.isArray(B.anyOf)){const{anyOf:s,...u}=B;i.push(...B.anyOf.map((s=>main_sampleFromSchemaGeneric(NT(s,u,o),o,void 0,a))))}else if(Array.isArray(B.oneOf)){const{oneOf:s,...u}=B;i.push(...B.oneOf.map((s=>main_sampleFromSchemaGeneric(NT(s,u,o),o,void 0,a))))}else{if(!(!a||a&&C.wrapped))return main_sampleFromSchemaGeneric(B,o,void 0,a);i.push(main_sampleFromSchemaGeneric(B,o,void 0,a))}return i=jT.array(s,{sample:i}),a&&C.wrapped?(ae[Y]=i,ds()(x)||ae[Y].push({_attr:x}),ae):i}if(\"object\"===U){for(let s in ce)Object.hasOwn(ce,s)&&(ce[s]?.deprecated||ce[s]?.readOnly&&!V||ce[s]?.writeOnly&&!z||le(s));if(a&&x&&ae[Y].push({_attr:x}),hasExceededMaxProperties())return ae;if(predicates_isBooleanJSONSchema(L)&&L)a?ae[Y].push({additionalProp:\"Anything can be here\"}):ae.additionalProp1={},pe++;else if(isJSONSchemaObject(L)){const i=L,u=main_sampleFromSchemaGeneric(i,o,void 0,a);if(a&&\"string\"==typeof i?.xml?.name&&\"notagname\"!==i?.xml?.name)ae[Y].push(u);else{const o=i?.[\"x-additionalPropertiesName\"]||\"additionalProp\",_=Number.isInteger(s.minProperties)&&s.minProperties>0&&pe<s.minProperties?s.minProperties-pe:3;for(let s=1;s<=_;s++){if(hasExceededMaxProperties())return ae;if(a){const i={};i[o+s]=u.notagname,ae[Y].push(i)}else ae[o+s]=u;pe++}}}return ae}let de;if(void 0!==s.const)de=s.const;else if(s&&Array.isArray(s.enum))de=random_pick(normalizeArray(s.enum));else{const i=isJSONSchemaObject(s.contentSchema)?main_sampleFromSchemaGeneric(s.contentSchema,o,void 0,a):void 0;de=jT[U](s,{sample:i})}return a?(ae[Y]=ds()(x)?de:[{_attr:x},de],ae):de},main_createXMLExample=(s,o,i)=>{const a=main_sampleFromSchemaGeneric(s,o,i,!0);if(a)return\"string\"==typeof a?a:ls()(a,{declaration:!0,indent:\"\\t\"})},main_sampleFromSchema=(s,o,i)=>main_sampleFromSchemaGeneric(s,o,i,!1),main_resolver=(s,o,i)=>[s,JSON.stringify(o),JSON.stringify(i)],MT=utils_memoizeN(main_createXMLExample,main_resolver),RT=utils_memoizeN(main_sampleFromSchema,main_resolver);const DT=new class OptionRegistry extends uT{#s={};data={...this.#s};get defaults(){return{...this.#s}}},api_optionAPI=(s,o)=>(void 0!==o&&DT.register(s,o),DT.get(s)),LT=[{when:/json/,shouldStringifyTypes:[\"string\"]}],FT=[\"object\"],fn_get_json_sample_schema=s=>(o,i,a,u)=>{const{fn:_}=s(),w=_.jsonSchema202012.memoizedSampleFromSchema(o,i,u),x=typeof w,C=LT.reduce(((s,o)=>o.when.test(a)?[...s,...o.shouldStringifyTypes]:s),FT);return gt()(C,(s=>s===x))?JSON.stringify(w,null,2):w},fn_get_yaml_sample_schema=s=>(o,i,a,u)=>{const{fn:_}=s(),w=_.jsonSchema202012.getJsonSampleSchema(o,i,a,u);let x;try{x=fn.dump(fn.load(w),{lineWidth:-1},{schema:rn}),\"\\n\"===x[x.length-1]&&(x=x.slice(0,x.length-1))}catch(s){return console.error(s),\"error: could not generate yaml example\"}return x.replace(/\\t/g,\"  \")},fn_get_xml_sample_schema=s=>(o,i,a)=>{const{fn:u}=s();if(o&&!o.xml&&(o.xml={}),o&&!o.xml.name){if(!o.$$ref&&(o.type||o.items||o.properties||o.additionalProperties))return'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n\\x3c!-- XML example cannot be generated; root element name is undefined --\\x3e';if(o.$$ref){let s=o.$$ref.match(/\\S*\\/(\\S+)$/);o.xml.name=s[1]}}return u.jsonSchema202012.memoizedCreateXMLExample(o,i,a)},fn_get_sample_schema=s=>(o,i=\"\",a={},u=void 0)=>{const{fn:_}=s();return\"function\"==typeof o?.toJS&&(o=o.toJS()),\"function\"==typeof u?.toJS&&(u=u.toJS()),/xml/.test(i)?_.jsonSchema202012.getXmlSampleSchema(o,a,u):/(yaml|yml)/.test(i)?_.jsonSchema202012.getYamlSampleSchema(o,a,i,u):_.jsonSchema202012.getJsonSampleSchema(o,a,i,u)},json_schema_2020_12_samples=({getSystem:s})=>{const o=fn_get_json_sample_schema(s),i=fn_get_yaml_sample_schema(s),a=fn_get_xml_sample_schema(s),u=fn_get_sample_schema(s);return{fn:{jsonSchema202012:{sampleFromSchema:main_sampleFromSchema,sampleFromSchemaGeneric:main_sampleFromSchemaGeneric,sampleOptionAPI:api_optionAPI,sampleEncoderAPI:ST,sampleFormatAPI:hT,sampleMediaTypeAPI:CT,createXMLExample:main_createXMLExample,memoizedSampleFromSchema:RT,memoizedCreateXMLExample:MT,getJsonSampleSchema:o,getYamlSampleSchema:i,getXmlSampleSchema:a,getSampleSchema:u,mergeJsonSchema:NT,foldType}}}};function PresetApis(){return[base,oas3,json_schema_2020_12,json_schema_2020_12_samples,oas31]}const inline_plugin=s=>()=>({fn:s.fn,components:s.components}),factorization_system=s=>{const o=Ye()({layout:{layout:s.layout,filter:s.filter},spec:{spec:\"\",url:s.url},requestSnippets:s.requestSnippets},s.initialState);if(s.initialState)for(const[i,a]of Object.entries(s.initialState))void 0===a&&delete o[i];return{system:{configs:s.configs},plugins:s.presets,state:o}},sources_query=()=>s=>{const o=s.queryConfigEnabled?(()=>{const s=new URLSearchParams(lt.location.search);return Object.fromEntries(s)})():{};return Object.entries(o).reduce(((s,[o,i])=>(\"config\"===o?s.configUrl=i:\"urls.primaryName\"===o?s[o]=i:s=co()(s,o,i),s)),{})},sources_url=({url:s,system:o})=>async i=>{if(!s)return{};if(\"function\"!=typeof o.configsActions?.getConfigByUrl)return{};const a=(()=>{const s={};return s.promise=new Promise(((o,i)=>{s.resolve=o,s.reject=i})),s})();return o.configsActions.getConfigByUrl({url:s,loadRemoteConfig:!0,requestInterceptor:i.requestInterceptor,responseInterceptor:i.responseInterceptor},(s=>{a.resolve(s)})),a.promise},runtime=()=>()=>{const s={};return globalThis.location&&(s.oauth2RedirectUrl=`${globalThis.location.protocol}//${globalThis.location.host}${globalThis.location.pathname.substring(0,globalThis.location.pathname.lastIndexOf(\"/\"))}/oauth2-redirect.html`),s},BT=Object.freeze({dom_id:null,domNode:null,spec:{},url:\"\",urls:null,configUrl:null,layout:\"BaseLayout\",docExpansion:\"list\",maxDisplayedTags:-1,filter:!1,validatorUrl:\"https://validator.swagger.io/validator\",oauth2RedirectUrl:void 0,persistAuthorization:!1,configs:{},displayOperationId:!1,displayRequestDuration:!1,deepLinking:!1,tryItOutEnabled:!1,requestInterceptor:s=>(s.curlOptions=[],s),responseInterceptor:s=>s,showMutatedRequest:!0,defaultModelRendering:\"example\",defaultModelExpandDepth:1,defaultModelsExpandDepth:1,showExtensions:!1,showCommonExtensions:!1,withCredentials:!1,requestSnippetsEnabled:!1,requestSnippets:{generators:{curl_bash:{title:\"cURL (bash)\",syntax:\"bash\"},curl_powershell:{title:\"cURL (PowerShell)\",syntax:\"powershell\"},curl_cmd:{title:\"cURL (CMD)\",syntax:\"bash\"}},defaultExpanded:!0,languages:null},supportedSubmitMethods:[\"get\",\"put\",\"post\",\"delete\",\"options\",\"head\",\"patch\",\"trace\"],queryConfigEnabled:!1,presets:[PresetApis],plugins:[],initialState:{},fn:{},components:{},syntaxHighlight:{activated:!0,theme:\"agate\"},operationsSorter:null,tagsSorter:null,onComplete:null,modelPropertyMacro:null,parameterMacro:null,fileUploadMediaTypes:[\"application/octet-stream\",\"image/\",\"audio/\",\"video/\"],uncaughtExceptionHandler:null});var $T=__webpack_require__(61448),qT=__webpack_require__.n($T),UT=__webpack_require__(77731),VT=__webpack_require__.n(UT);const type_casters_array=(s,o=[])=>Array.isArray(s)?s:o,type_casters_boolean=(s,o=!1)=>!0===s||\"true\"===s||1===s||\"1\"===s||!1!==s&&\"false\"!==s&&0!==s&&\"0\"!==s&&o,dom_node=s=>null===s||\"null\"===s?null:s,type_casters_filter=s=>{const o=String(s);return type_casters_boolean(s,o)},type_casters_function=(s,o)=>\"function\"==typeof s?s:o,nullable_array=s=>Array.isArray(s)?s:null,nullable_function=s=>\"function\"==typeof s?s:null,nullable_string=s=>null===s||\"null\"===s?null:String(s),type_casters_number=(s,o=-1)=>{const i=parseInt(s,10);return Number.isNaN(i)?o:i},type_casters_object=(s,o={})=>as()(s)?s:o,sorter=s=>\"function\"==typeof s||\"string\"==typeof s?s:null,type_casters_string=s=>String(s),syntax_highlight=(s,o)=>as()(s)?s:!1===s||\"false\"===s||0===s||\"0\"===s?{activated:!1}:o,undefined_string=s=>void 0===s||\"undefined\"===s?void 0:String(s),zT={components:{typeCaster:type_casters_object},configs:{typeCaster:type_casters_object},configUrl:{typeCaster:nullable_string},deepLinking:{typeCaster:type_casters_boolean,defaultValue:BT.deepLinking},defaultModelExpandDepth:{typeCaster:type_casters_number,defaultValue:BT.defaultModelExpandDepth},defaultModelRendering:{typeCaster:type_casters_string},defaultModelsExpandDepth:{typeCaster:type_casters_number,defaultValue:BT.defaultModelsExpandDepth},displayOperationId:{typeCaster:type_casters_boolean,defaultValue:BT.displayOperationId},displayRequestDuration:{typeCaster:type_casters_boolean,defaultValue:BT.displayRequestDuration},docExpansion:{typeCaster:type_casters_string},dom_id:{typeCaster:nullable_string},domNode:{typeCaster:dom_node},fileUploadMediaTypes:{typeCaster:type_casters_array,defaultValue:BT.fileUploadMediaTypes},filter:{typeCaster:type_casters_filter},fn:{typeCaster:type_casters_object},initialState:{typeCaster:type_casters_object},layout:{typeCaster:type_casters_string},maxDisplayedTags:{typeCaster:type_casters_number,defaultValue:BT.maxDisplayedTags},modelPropertyMacro:{typeCaster:nullable_function},oauth2RedirectUrl:{typeCaster:undefined_string},onComplete:{typeCaster:nullable_function},operationsSorter:{typeCaster:sorter},paramaterMacro:{typeCaster:nullable_function},persistAuthorization:{typeCaster:type_casters_boolean,defaultValue:BT.persistAuthorization},plugins:{typeCaster:type_casters_array,defaultValue:BT.plugins},presets:{typeCaster:type_casters_array,defaultValue:BT.presets},requestInterceptor:{typeCaster:type_casters_function,defaultValue:BT.requestInterceptor},requestSnippets:{typeCaster:type_casters_object,defaultValue:BT.requestSnippets},requestSnippetsEnabled:{typeCaster:type_casters_boolean,defaultValue:BT.requestSnippetsEnabled},responseInterceptor:{typeCaster:type_casters_function,defaultValue:BT.responseInterceptor},showCommonExtensions:{typeCaster:type_casters_boolean,defaultValue:BT.showCommonExtensions},showExtensions:{typeCaster:type_casters_boolean,defaultValue:BT.showExtensions},showMutatedRequest:{typeCaster:type_casters_boolean,defaultValue:BT.showMutatedRequest},spec:{typeCaster:type_casters_object,defaultValue:BT.spec},supportedSubmitMethods:{typeCaster:type_casters_array,defaultValue:BT.supportedSubmitMethods},syntaxHighlight:{typeCaster:syntax_highlight,defaultValue:BT.syntaxHighlight},\"syntaxHighlight.activated\":{typeCaster:type_casters_boolean,defaultValue:BT.syntaxHighlight.activated},\"syntaxHighlight.theme\":{typeCaster:type_casters_string},tagsSorter:{typeCaster:sorter},tryItOutEnabled:{typeCaster:type_casters_boolean,defaultValue:BT.tryItOutEnabled},url:{typeCaster:type_casters_string},urls:{typeCaster:nullable_array},\"urls.primaryName\":{typeCaster:type_casters_string},validatorUrl:{typeCaster:nullable_string},withCredentials:{typeCaster:type_casters_boolean,defaultValue:BT.withCredentials},uncaughtExceptionHandler:{typeCaster:nullable_function}},type_cast=s=>Object.entries(zT).reduce(((s,[o,{typeCaster:i,defaultValue:a}])=>{if(qT()(s,o)){const u=i(Cn()(s,o),a);s=VT()(o,u,s)}return s}),{...s}),config_merge=(s,...o)=>{let i=Symbol.for(\"domNode\"),a=Symbol.for(\"primaryName\");const u=[];for(const s of o){const o={...s};Object.hasOwn(o,\"domNode\")&&(i=o.domNode,delete o.domNode),Object.hasOwn(o,\"urls.primaryName\")?(a=o[\"urls.primaryName\"],delete o[\"urls.primaryName\"]):Array.isArray(o.urls)&&Object.hasOwn(o.urls,\"primaryName\")&&(a=o.urls.primaryName,delete o.urls.primaryName),u.push(o)}const _=Ye()(s,...u);return i!==Symbol.for(\"domNode\")&&(_.domNode=i),a!==Symbol.for(\"primaryName\")&&Array.isArray(_.urls)&&(_.urls.primaryName=a),type_cast(_)};function SwaggerUI(s){const o=sources_query()(s),i=runtime()(),a=SwaggerUI.config.merge({},SwaggerUI.config.defaults,i,s,o),u=factorization_system(a),_=inline_plugin(a),w=new Store(u);w.register([a.plugins,_]);const x=w.getSystem(),persistConfigs=s=>{w.setConfigs(s),x.configsActions.loaded()},updateSpec=s=>{!o.url&&\"object\"==typeof s.spec&&Object.keys(s.spec).length>0?(x.specActions.updateUrl(\"\"),x.specActions.updateLoadingStatus(\"success\"),x.specActions.updateSpec(JSON.stringify(s.spec))):\"function\"==typeof x.specActions.download&&s.url&&!s.urls&&(x.specActions.updateUrl(s.url),x.specActions.download(s.url))},render=s=>{if(s.domNode)x.render(s.domNode,\"App\");else if(s.dom_id){const o=document.querySelector(s.dom_id);x.render(o,\"App\")}else null===s.dom_id||null===s.domNode||console.error(\"Skipped rendering: no `dom_id` or `domNode` was specified\")};return a.configUrl?((async()=>{const{configUrl:s}=a,i=await sources_url({url:s,system:x})(a),u=SwaggerUI.config.merge({},a,i,o);persistConfigs(u),null!==i&&updateSpec(u),render(u)})(),x):(persistConfigs(a),updateSpec(a),render(a),x)}SwaggerUI.System=Store,SwaggerUI.config={defaults:BT,merge:config_merge,typeCast:type_cast,typeCastMappings:zT},SwaggerUI.presets={base,apis:PresetApis},SwaggerUI.plugins={Auth:auth,Configs:configsPlugin,DeepLining:deep_linking,Err:err,Filter:filter,Icons:icons,JSONSchema5:json_schema_5,JSONSchema5Samples:json_schema_5_samples,JSONSchema202012:json_schema_2020_12,JSONSchema202012Samples:json_schema_2020_12_samples,Layout:plugins_layout,Logs:logs,OpenAPI30:oas3,OpenAPI31:oas3,OnComplete:on_complete,RequestSnippets:plugins_request_snippets,Spec:plugins_spec,SwaggerClient:swagger_client,Util:util,View:view,ViewLegacy:view_legacy,DownloadUrl:downloadUrlPlugin,SyntaxHighlighting:syntax_highlighting,Versions:versions,SafeRender:safe_render};const WT=SwaggerUI})(),i=i.default})()));"
  },
  {
    "path": "openapi/swagger-ui/swagger-ui-standalone-preset.js",
    "content": "/*! For license information please see swagger-ui-standalone-preset.js.LICENSE.txt */\n!function webpackUniversalModuleDefinition(t,e){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?exports.SwaggerUIStandalonePreset=e():t.SwaggerUIStandalonePreset=e()}(this,(()=>(()=>{var t={2:(t,e,r)=>{var n=r(2199),o=r(4664),i=r(5950);t.exports=function getAllKeys(t){return n(t,i,o)}},41:(t,e,r)=>{\"use strict\";var n=r(655),o=r(8068),i=r(9675),a=r(5795);t.exports=function defineDataProperty(t,e,r){if(!t||\"object\"!=typeof t&&\"function\"!=typeof t)throw new i(\"`obj` must be an object or a function`\");if(\"string\"!=typeof e&&\"symbol\"!=typeof e)throw new i(\"`property` must be a string or a symbol`\");if(arguments.length>3&&\"boolean\"!=typeof arguments[3]&&null!==arguments[3])throw new i(\"`nonEnumerable`, if provided, must be a boolean or null\");if(arguments.length>4&&\"boolean\"!=typeof arguments[4]&&null!==arguments[4])throw new i(\"`nonWritable`, if provided, must be a boolean or null\");if(arguments.length>5&&\"boolean\"!=typeof arguments[5]&&null!==arguments[5])throw new i(\"`nonConfigurable`, if provided, must be a boolean or null\");if(arguments.length>6&&\"boolean\"!=typeof arguments[6])throw new i(\"`loose`, if provided, must be a boolean\");var s=arguments.length>3?arguments[3]:null,u=arguments.length>4?arguments[4]:null,c=arguments.length>5?arguments[5]:null,f=arguments.length>6&&arguments[6],l=!!a&&a(t,e);if(n)n(t,e,{configurable:null===c&&l?l.configurable:!c,enumerable:null===s&&l?l.enumerable:!s,value:r,writable:null===u&&l?l.writable:!u});else{if(!f&&(s||u||c))throw new o(\"This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.\");t[e]=r}}},76:t=>{\"use strict\";t.exports=Function.prototype.call},79:(t,e,r)=>{var n=r(3702),o=r(80),i=r(4739),a=r(8655),s=r(1175);function ListCache(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}ListCache.prototype.clear=n,ListCache.prototype.delete=o,ListCache.prototype.get=i,ListCache.prototype.has=a,ListCache.prototype.set=s,t.exports=ListCache},80:(t,e,r)=>{var n=r(6025),o=Array.prototype.splice;t.exports=function listCacheDelete(t){var e=this.__data__,r=n(e,t);return!(r<0)&&(r==e.length-1?e.pop():o.call(e,r,1),--this.size,!0)}},104:(t,e,r)=>{var n=r(3661);function memoize(t,e){if(\"function\"!=typeof t||null!=e&&\"function\"!=typeof e)throw new TypeError(\"Expected a function\");var memoized=function(){var r=arguments,n=e?e.apply(this,r):r[0],o=memoized.cache;if(o.has(n))return o.get(n);var i=t.apply(this,r);return memoized.cache=o.set(n,i)||o,i};return memoized.cache=new(memoize.Cache||n),memoized}memoize.Cache=n,t.exports=memoize},251:(t,e)=>{e.read=function(t,e,r,n,o){var i,a,s=8*o-n-1,u=(1<<s)-1,c=u>>1,f=-7,l=r?o-1:0,p=r?-1:1,h=t[e+l];for(l+=p,i=h&(1<<-f)-1,h>>=-f,f+=s;f>0;i=256*i+t[e+l],l+=p,f-=8);for(a=i&(1<<-f)-1,i>>=-f,f+=n;f>0;a=256*a+t[e+l],l+=p,f-=8);if(0===i)i=1-c;else{if(i===u)return a?NaN:1/0*(h?-1:1);a+=Math.pow(2,n),i-=c}return(h?-1:1)*a*Math.pow(2,i-n)},e.write=function(t,e,r,n,o,i){var a,s,u,c=8*i-o-1,f=(1<<c)-1,l=f>>1,p=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,h=n?0:i-1,d=n?1:-1,y=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(s=isNaN(e)?1:0,a=f):(a=Math.floor(Math.log(e)/Math.LN2),e*(u=Math.pow(2,-a))<1&&(a--,u*=2),(e+=a+l>=1?p/u:p*Math.pow(2,1-l))*u>=2&&(a++,u/=2),a+l>=f?(s=0,a=f):a+l>=1?(s=(e*u-1)*Math.pow(2,o),a+=l):(s=e*Math.pow(2,l-1)*Math.pow(2,o),a=0));o>=8;t[r+h]=255&s,h+=d,s/=256,o-=8);for(a=a<<o|s,c+=o;c>0;t[r+h]=255&a,h+=d,a/=256,c-=8);t[r+h-d]|=128*y}},270:(t,e,r)=>{var n=r(7068),o=r(346);t.exports=function baseIsEqual(t,e,r,i,a){return t===e||(null==t||null==e||!o(t)&&!o(e)?t!=t&&e!=e:n(t,e,r,i,baseIsEqual,a))}},289:(t,e,r)=>{var n=r(2651);t.exports=function mapCacheGet(t){return n(this,t).get(t)}},294:t=>{t.exports=function isLength(t){return\"number\"==typeof t&&t>-1&&t%1==0&&t<=9007199254740991}},317:t=>{t.exports=function mapToArray(t){var e=-1,r=Array(t.size);return t.forEach((function(t,n){r[++e]=[n,t]})),r}},346:t=>{t.exports=function isObjectLike(t){return null!=t&&\"object\"==typeof t}},361:t=>{var e=/^(?:0|[1-9]\\d*)$/;t.exports=function isIndex(t,r){var n=typeof t;return!!(r=null==r?9007199254740991:r)&&(\"number\"==n||\"symbol\"!=n&&e.test(t))&&t>-1&&t%1==0&&t<r}},376:t=>{\"use strict\";t.exports=[\"constructor\",\"hasOwnProperty\",\"isPrototypeOf\",\"propertyIsEnumerable\",\"toLocaleString\",\"toString\",\"valueOf\"]},392:t=>{t.exports=function getValue(t,e){return null==t?void 0:t[e]}},414:t=>{\"use strict\";t.exports=Math.round},453:(t,e,r)=>{\"use strict\";var n,o=r(9612),i=r(9383),a=r(1237),s=r(9290),u=r(9538),c=r(8068),f=r(9675),l=r(5345),p=r(1514),h=r(8968),d=r(6188),y=r(8002),_=r(5880),g=r(414),m=r(3093),v=Function,getEvalledConstructor=function(t){try{return v('\"use strict\"; return ('+t+\").constructor;\")()}catch(t){}},b=r(5795),w=r(655),throwTypeError=function(){throw new f},I=b?function(){try{return throwTypeError}catch(t){try{return b(arguments,\"callee\").get}catch(t){return throwTypeError}}}():throwTypeError,x=r(4039)(),B=r(3628),k=r(1064),C=r(8648),j=r(1002),q=r(76),L={},P=\"undefined\"!=typeof Uint8Array&&B?B(Uint8Array):n,U={__proto__:null,\"%AggregateError%\":\"undefined\"==typeof AggregateError?n:AggregateError,\"%Array%\":Array,\"%ArrayBuffer%\":\"undefined\"==typeof ArrayBuffer?n:ArrayBuffer,\"%ArrayIteratorPrototype%\":x&&B?B([][Symbol.iterator]()):n,\"%AsyncFromSyncIteratorPrototype%\":n,\"%AsyncFunction%\":L,\"%AsyncGenerator%\":L,\"%AsyncGeneratorFunction%\":L,\"%AsyncIteratorPrototype%\":L,\"%Atomics%\":\"undefined\"==typeof Atomics?n:Atomics,\"%BigInt%\":\"undefined\"==typeof BigInt?n:BigInt,\"%BigInt64Array%\":\"undefined\"==typeof BigInt64Array?n:BigInt64Array,\"%BigUint64Array%\":\"undefined\"==typeof BigUint64Array?n:BigUint64Array,\"%Boolean%\":Boolean,\"%DataView%\":\"undefined\"==typeof DataView?n:DataView,\"%Date%\":Date,\"%decodeURI%\":decodeURI,\"%decodeURIComponent%\":decodeURIComponent,\"%encodeURI%\":encodeURI,\"%encodeURIComponent%\":encodeURIComponent,\"%Error%\":i,\"%eval%\":eval,\"%EvalError%\":a,\"%Float32Array%\":\"undefined\"==typeof Float32Array?n:Float32Array,\"%Float64Array%\":\"undefined\"==typeof Float64Array?n:Float64Array,\"%FinalizationRegistry%\":\"undefined\"==typeof FinalizationRegistry?n:FinalizationRegistry,\"%Function%\":v,\"%GeneratorFunction%\":L,\"%Int8Array%\":\"undefined\"==typeof Int8Array?n:Int8Array,\"%Int16Array%\":\"undefined\"==typeof Int16Array?n:Int16Array,\"%Int32Array%\":\"undefined\"==typeof Int32Array?n:Int32Array,\"%isFinite%\":isFinite,\"%isNaN%\":isNaN,\"%IteratorPrototype%\":x&&B?B(B([][Symbol.iterator]())):n,\"%JSON%\":\"object\"==typeof JSON?JSON:n,\"%Map%\":\"undefined\"==typeof Map?n:Map,\"%MapIteratorPrototype%\":\"undefined\"!=typeof Map&&x&&B?B((new Map)[Symbol.iterator]()):n,\"%Math%\":Math,\"%Number%\":Number,\"%Object%\":o,\"%Object.getOwnPropertyDescriptor%\":b,\"%parseFloat%\":parseFloat,\"%parseInt%\":parseInt,\"%Promise%\":\"undefined\"==typeof Promise?n:Promise,\"%Proxy%\":\"undefined\"==typeof Proxy?n:Proxy,\"%RangeError%\":s,\"%ReferenceError%\":u,\"%Reflect%\":\"undefined\"==typeof Reflect?n:Reflect,\"%RegExp%\":RegExp,\"%Set%\":\"undefined\"==typeof Set?n:Set,\"%SetIteratorPrototype%\":\"undefined\"!=typeof Set&&x&&B?B((new Set)[Symbol.iterator]()):n,\"%SharedArrayBuffer%\":\"undefined\"==typeof SharedArrayBuffer?n:SharedArrayBuffer,\"%String%\":String,\"%StringIteratorPrototype%\":x&&B?B(\"\"[Symbol.iterator]()):n,\"%Symbol%\":x?Symbol:n,\"%SyntaxError%\":c,\"%ThrowTypeError%\":I,\"%TypedArray%\":P,\"%TypeError%\":f,\"%Uint8Array%\":\"undefined\"==typeof Uint8Array?n:Uint8Array,\"%Uint8ClampedArray%\":\"undefined\"==typeof Uint8ClampedArray?n:Uint8ClampedArray,\"%Uint16Array%\":\"undefined\"==typeof Uint16Array?n:Uint16Array,\"%Uint32Array%\":\"undefined\"==typeof Uint32Array?n:Uint32Array,\"%URIError%\":l,\"%WeakMap%\":\"undefined\"==typeof WeakMap?n:WeakMap,\"%WeakRef%\":\"undefined\"==typeof WeakRef?n:WeakRef,\"%WeakSet%\":\"undefined\"==typeof WeakSet?n:WeakSet,\"%Function.prototype.call%\":q,\"%Function.prototype.apply%\":j,\"%Object.defineProperty%\":w,\"%Object.getPrototypeOf%\":k,\"%Math.abs%\":p,\"%Math.floor%\":h,\"%Math.max%\":d,\"%Math.min%\":y,\"%Math.pow%\":_,\"%Math.round%\":g,\"%Math.sign%\":m,\"%Reflect.getPrototypeOf%\":C};if(B)try{null.error}catch(t){var D=B(B(t));U[\"%Error.prototype%\"]=D}var z=function doEval(t){var e;if(\"%AsyncFunction%\"===t)e=getEvalledConstructor(\"async function () {}\");else if(\"%GeneratorFunction%\"===t)e=getEvalledConstructor(\"function* () {}\");else if(\"%AsyncGeneratorFunction%\"===t)e=getEvalledConstructor(\"async function* () {}\");else if(\"%AsyncGenerator%\"===t){var r=doEval(\"%AsyncGeneratorFunction%\");r&&(e=r.prototype)}else if(\"%AsyncIteratorPrototype%\"===t){var n=doEval(\"%AsyncGenerator%\");n&&B&&(e=B(n.prototype))}return U[t]=e,e},W={__proto__:null,\"%ArrayBufferPrototype%\":[\"ArrayBuffer\",\"prototype\"],\"%ArrayPrototype%\":[\"Array\",\"prototype\"],\"%ArrayProto_entries%\":[\"Array\",\"prototype\",\"entries\"],\"%ArrayProto_forEach%\":[\"Array\",\"prototype\",\"forEach\"],\"%ArrayProto_keys%\":[\"Array\",\"prototype\",\"keys\"],\"%ArrayProto_values%\":[\"Array\",\"prototype\",\"values\"],\"%AsyncFunctionPrototype%\":[\"AsyncFunction\",\"prototype\"],\"%AsyncGenerator%\":[\"AsyncGeneratorFunction\",\"prototype\"],\"%AsyncGeneratorPrototype%\":[\"AsyncGeneratorFunction\",\"prototype\",\"prototype\"],\"%BooleanPrototype%\":[\"Boolean\",\"prototype\"],\"%DataViewPrototype%\":[\"DataView\",\"prototype\"],\"%DatePrototype%\":[\"Date\",\"prototype\"],\"%ErrorPrototype%\":[\"Error\",\"prototype\"],\"%EvalErrorPrototype%\":[\"EvalError\",\"prototype\"],\"%Float32ArrayPrototype%\":[\"Float32Array\",\"prototype\"],\"%Float64ArrayPrototype%\":[\"Float64Array\",\"prototype\"],\"%FunctionPrototype%\":[\"Function\",\"prototype\"],\"%Generator%\":[\"GeneratorFunction\",\"prototype\"],\"%GeneratorPrototype%\":[\"GeneratorFunction\",\"prototype\",\"prototype\"],\"%Int8ArrayPrototype%\":[\"Int8Array\",\"prototype\"],\"%Int16ArrayPrototype%\":[\"Int16Array\",\"prototype\"],\"%Int32ArrayPrototype%\":[\"Int32Array\",\"prototype\"],\"%JSONParse%\":[\"JSON\",\"parse\"],\"%JSONStringify%\":[\"JSON\",\"stringify\"],\"%MapPrototype%\":[\"Map\",\"prototype\"],\"%NumberPrototype%\":[\"Number\",\"prototype\"],\"%ObjectPrototype%\":[\"Object\",\"prototype\"],\"%ObjProto_toString%\":[\"Object\",\"prototype\",\"toString\"],\"%ObjProto_valueOf%\":[\"Object\",\"prototype\",\"valueOf\"],\"%PromisePrototype%\":[\"Promise\",\"prototype\"],\"%PromiseProto_then%\":[\"Promise\",\"prototype\",\"then\"],\"%Promise_all%\":[\"Promise\",\"all\"],\"%Promise_reject%\":[\"Promise\",\"reject\"],\"%Promise_resolve%\":[\"Promise\",\"resolve\"],\"%RangeErrorPrototype%\":[\"RangeError\",\"prototype\"],\"%ReferenceErrorPrototype%\":[\"ReferenceError\",\"prototype\"],\"%RegExpPrototype%\":[\"RegExp\",\"prototype\"],\"%SetPrototype%\":[\"Set\",\"prototype\"],\"%SharedArrayBufferPrototype%\":[\"SharedArrayBuffer\",\"prototype\"],\"%StringPrototype%\":[\"String\",\"prototype\"],\"%SymbolPrototype%\":[\"Symbol\",\"prototype\"],\"%SyntaxErrorPrototype%\":[\"SyntaxError\",\"prototype\"],\"%TypedArrayPrototype%\":[\"TypedArray\",\"prototype\"],\"%TypeErrorPrototype%\":[\"TypeError\",\"prototype\"],\"%Uint8ArrayPrototype%\":[\"Uint8Array\",\"prototype\"],\"%Uint8ClampedArrayPrototype%\":[\"Uint8ClampedArray\",\"prototype\"],\"%Uint16ArrayPrototype%\":[\"Uint16Array\",\"prototype\"],\"%Uint32ArrayPrototype%\":[\"Uint32Array\",\"prototype\"],\"%URIErrorPrototype%\":[\"URIError\",\"prototype\"],\"%WeakMapPrototype%\":[\"WeakMap\",\"prototype\"],\"%WeakSetPrototype%\":[\"WeakSet\",\"prototype\"]},V=r(6743),K=r(9957),$=V.call(q,Array.prototype.concat),H=V.call(j,Array.prototype.splice),Y=V.call(q,String.prototype.replace),Z=V.call(q,String.prototype.slice),J=V.call(q,RegExp.prototype.exec),tt=/[^%.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|%$))/g,et=/\\\\(\\\\)?/g,rt=function getBaseIntrinsic(t,e){var r,n=t;if(K(W,n)&&(n=\"%\"+(r=W[n])[0]+\"%\"),K(U,n)){var o=U[n];if(o===L&&(o=z(n)),void 0===o&&!e)throw new f(\"intrinsic \"+t+\" exists, but is not available. Please file an issue!\");return{alias:r,name:n,value:o}}throw new c(\"intrinsic \"+t+\" does not exist!\")};t.exports=function GetIntrinsic(t,e){if(\"string\"!=typeof t||0===t.length)throw new f(\"intrinsic name must be a non-empty string\");if(arguments.length>1&&\"boolean\"!=typeof e)throw new f('\"allowMissing\" argument must be a boolean');if(null===J(/^%?[^%]*%?$/,t))throw new c(\"`%` may not be present anywhere but at the beginning and end of the intrinsic name\");var r=function stringToPath(t){var e=Z(t,0,1),r=Z(t,-1);if(\"%\"===e&&\"%\"!==r)throw new c(\"invalid intrinsic syntax, expected closing `%`\");if(\"%\"===r&&\"%\"!==e)throw new c(\"invalid intrinsic syntax, expected opening `%`\");var n=[];return Y(t,tt,(function(t,e,r,o){n[n.length]=r?Y(o,et,\"$1\"):e||t})),n}(t),n=r.length>0?r[0]:\"\",o=rt(\"%\"+n+\"%\",e),i=o.name,a=o.value,s=!1,u=o.alias;u&&(n=u[0],H(r,$([0,1],u)));for(var l=1,p=!0;l<r.length;l+=1){var h=r[l],d=Z(h,0,1),y=Z(h,-1);if(('\"'===d||\"'\"===d||\"`\"===d||'\"'===y||\"'\"===y||\"`\"===y)&&d!==y)throw new c(\"property names with quotes must have matching quotes\");if(\"constructor\"!==h&&p||(s=!0),K(U,i=\"%\"+(n+=\".\"+h)+\"%\"))a=U[i];else if(null!=a){if(!(h in a)){if(!e)throw new f(\"base intrinsic for \"+t+\" exists, but the property is not available.\");return}if(b&&l+1>=r.length){var _=b(a,h);a=(p=!!_)&&\"get\"in _&&!(\"originalValue\"in _.get)?_.get:a[h]}else p=K(a,h),a=a[h];p&&!s&&(U[i]=a)}}return a}},462:(t,e,r)=>{\"use strict\";var n=r(975);t.exports=n},470:(t,e,r)=>{\"use strict\";var n=r(6028),o=r(5594);t.exports=function(t){var e=n(t,\"string\");return o(e)?e:e+\"\"}},487:(t,e,r)=>{\"use strict\";var n=r(6897),o=r(655),i=r(3126),a=r(4586);t.exports=function callBind(t){var e=i(arguments),r=t.length-(arguments.length-1);return n(e,1+(r>0?r:0),!0)},o?o(t.exports,\"apply\",{value:a}):t.exports.apply=a},575:(t,e,r)=>{\"use strict\";var n=r(3121);t.exports=function(t){return n(t.length)}},581:(t,e,r)=>{\"use strict\";var n=r(3930),o=r(2250),i=r(6285),a=TypeError;t.exports=function(t,e){var r,s;if(\"string\"===e&&o(r=t.toString)&&!i(s=n(r,t)))return s;if(o(r=t.valueOf)&&!i(s=n(r,t)))return s;if(\"string\"!==e&&o(r=t.toString)&&!i(s=n(r,t)))return s;throw new a(\"Can't convert object to primitive value\")}},583:(t,e,r)=>{var n=r(7237),o=r(7255),i=r(8586),a=r(7797);t.exports=function property(t){return i(t)?n(a(t)):o(t)}},592:(t,e,r)=>{\"use strict\";var n=r(655),o=function hasPropertyDescriptors(){return!!n};o.hasArrayLengthDefineBug=function hasArrayLengthDefineBug(){if(!n)return null;try{return 1!==n([],\"length\",{value:1}).length}catch(t){return!0}},t.exports=o},631:(t,e,r)=>{var n=r(8077),o=r(9326);t.exports=function hasIn(t,e){return null!=t&&o(t,e,n)}},641:(t,e,r)=>{var n=r(6649),o=r(5950);t.exports=function baseForOwn(t,e){return t&&n(t,e,o)}},655:t=>{\"use strict\";var e=Object.defineProperty||!1;if(e)try{e({},\"a\",{value:1})}catch(t){e=!1}t.exports=e},659:(t,e,r)=>{var n=r(1873),o=Object.prototype,i=o.hasOwnProperty,a=o.toString,s=n?n.toStringTag:void 0;t.exports=function getRawTag(t){var e=i.call(t,s),r=t[s];try{t[s]=void 0;var n=!0}catch(t){}var o=a.call(t);return n&&(e?t[s]=r:delete t[s]),o}},689:(t,e,r)=>{var n=r(2),o=Object.prototype.hasOwnProperty;t.exports=function equalObjects(t,e,r,i,a,s){var u=1&r,c=n(t),f=c.length;if(f!=n(e).length&&!u)return!1;for(var l=f;l--;){var p=c[l];if(!(u?p in e:o.call(e,p)))return!1}var h=s.get(t),d=s.get(e);if(h&&d)return h==e&&d==t;var y=!0;s.set(t,e),s.set(e,t);for(var _=u;++l<f;){var g=t[p=c[l]],m=e[p];if(i)var v=u?i(m,g,p,e,t,s):i(g,m,p,t,e,s);if(!(void 0===v?g===m||a(g,m,r,i,s):v)){y=!1;break}_||(_=\"constructor\"==p)}if(y&&!_){var b=t.constructor,w=e.constructor;b==w||!(\"constructor\"in t)||!(\"constructor\"in e)||\"function\"==typeof b&&b instanceof b&&\"function\"==typeof w&&w instanceof w||(y=!1)}return s.delete(t),s.delete(e),y}},695:(t,e,r)=>{var n=r(8096),o=r(2428),i=r(6449),a=r(3656),s=r(361),u=r(7167),c=Object.prototype.hasOwnProperty;t.exports=function arrayLikeKeys(t,e){var r=i(t),f=!r&&o(t),l=!r&&!f&&a(t),p=!r&&!f&&!l&&u(t),h=r||f||l||p,d=h?n(t.length,String):[],y=d.length;for(var _ in t)!e&&!c.call(t,_)||h&&(\"length\"==_||l&&(\"offset\"==_||\"parent\"==_)||p&&(\"buffer\"==_||\"byteLength\"==_||\"byteOffset\"==_)||s(_,y))||d.push(_);return d}},756:(t,e,r)=>{var n=r(3805);t.exports=function isStrictComparable(t){return t==t&&!n(t)}},776:(t,e,r)=>{var n=r(756),o=r(5950);t.exports=function getMatchData(t){for(var e=o(t),r=e.length;r--;){var i=e[r],a=t[i];e[r]=[i,a,n(a)]}return e}},798:(t,e,r)=>{\"use strict\";var n,o,i=r(5951),a=r(6794),s=i.process,u=i.Deno,c=s&&s.versions||u&&u.version,f=c&&c.v8;f&&(o=(n=f.split(\".\"))[0]>0&&n[0]<4?1:+(n[0]+n[1])),!o&&a&&(!(n=a.match(/Edge\\/(\\d+)/))||n[1]>=74)&&(n=a.match(/Chrome\\/(\\d+)/))&&(o=+n[1]),t.exports=o},828:(t,e,r)=>{var n=r(4647),o=r(3222),i=/[\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\xff\\u0100-\\u017f]/g,a=RegExp(\"[\\\\u0300-\\\\u036f\\\\ufe20-\\\\ufe2f\\\\u20d0-\\\\u20ff]\",\"g\");t.exports=function deburr(t){return(t=o(t))&&t.replace(i,n).replace(a,\"\")}},882:t=>{t.exports=function arrayReduce(t,e,r,n){var o=-1,i=null==t?0:t.length;for(n&&i&&(r=t[++o]);++o<i;)r=e(r,t[o],o,t);return r}},909:(t,e,r)=>{var n=r(641),o=r(8329)(n);t.exports=o},916:(t,e,r)=>{var n=r(909);t.exports=function baseSome(t,e){var r;return n(t,(function(t,n,o){return!(r=e(t,n,o))})),!!r}},938:t=>{t.exports=function stackDelete(t){var e=this.__data__,r=e.delete(t);return this.size=e.size,r}},945:(t,e,r)=>{var n=r(79),o=r(8223),i=r(3661);t.exports=function stackSet(t,e){var r=this.__data__;if(r instanceof n){var a=r.__data__;if(!o||a.length<199)return a.push([t,e]),this.size=++r.size,this;r=this.__data__=new i(a)}return r.set(t,e),this.size=r.size,this}},953:(t,e,r)=>{\"use strict\";t.exports=r(3375)},975:(t,e,r)=>{\"use strict\";var n=r(9748);t.exports=n},1002:t=>{\"use strict\";t.exports=Function.prototype.apply},1042:(t,e,r)=>{var n=r(6110)(Object,\"create\");t.exports=n},1064:(t,e,r)=>{\"use strict\";var n=r(9612);t.exports=n.getPrototypeOf||null},1074:t=>{t.exports=function asciiToArray(t){return t.split(\"\")}},1091:(t,e,r)=>{\"use strict\";var n=r(5951),o=r(6024),i=r(2361),a=r(2250),s=r(3846).f,u=r(7463),c=r(2046),f=r(8311),l=r(1626),p=r(9724);r(6128);var wrapConstructor=function(t){var Wrapper=function(e,r,n){if(this instanceof Wrapper){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,r)}return new t(e,r,n)}return o(t,this,arguments)};return Wrapper.prototype=t.prototype,Wrapper};t.exports=function(t,e){var r,o,h,d,y,_,g,m,v,b=t.target,w=t.global,I=t.stat,x=t.proto,B=w?n:I?n[b]:n[b]&&n[b].prototype,k=w?c:c[b]||l(c,b,{})[b],C=k.prototype;for(d in e)o=!(r=u(w?d:b+(I?\".\":\"#\")+d,t.forced))&&B&&p(B,d),_=k[d],o&&(g=t.dontCallGetSet?(v=s(B,d))&&v.value:B[d]),y=o&&g?g:e[d],(r||x||typeof _!=typeof y)&&(m=t.bind&&o?f(y,n):t.wrap&&o?wrapConstructor(y):x&&a(y)?i(y):y,(t.sham||y&&y.sham||_&&_.sham)&&l(m,\"sham\",!0),l(k,d,m),x&&(p(c,h=b+\"Prototype\")||l(c,h,{}),l(c[h],d,y),t.real&&C&&(r||!C[d])&&l(C,d,y)))}},1175:(t,e,r)=>{var n=r(6025);t.exports=function listCacheSet(t,e){var r=this.__data__,o=n(r,t);return o<0?(++this.size,r.push([t,e])):r[o][1]=e,this}},1176:t=>{\"use strict\";var e=Math.ceil,r=Math.floor;t.exports=Math.trunc||function trunc(t){var n=+t;return(n>0?r:e)(n)}},1234:t=>{t.exports=function baseZipObject(t,e,r){for(var n=-1,o=t.length,i=e.length,a={};++n<o;){var s=n<i?e[n]:void 0;r(a,t[n],s)}return a}},1237:t=>{\"use strict\";t.exports=EvalError},1333:t=>{\"use strict\";t.exports=function hasSymbols(){if(\"function\"!=typeof Symbol||\"function\"!=typeof Object.getOwnPropertySymbols)return!1;if(\"symbol\"==typeof Symbol.iterator)return!0;var t={},e=Symbol(\"test\"),r=Object(e);if(\"string\"==typeof e)return!1;if(\"[object Symbol]\"!==Object.prototype.toString.call(e))return!1;if(\"[object Symbol]\"!==Object.prototype.toString.call(r))return!1;for(var n in t[e]=42,t)return!1;if(\"function\"==typeof Object.keys&&0!==Object.keys(t).length)return!1;if(\"function\"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(t).length)return!1;var o=Object.getOwnPropertySymbols(t);if(1!==o.length||o[0]!==e)return!1;if(!Object.prototype.propertyIsEnumerable.call(t,e))return!1;if(\"function\"==typeof Object.getOwnPropertyDescriptor){var i=Object.getOwnPropertyDescriptor(t,e);if(42!==i.value||!0!==i.enumerable)return!1}return!0}},1340:(t,e,r)=>{\"use strict\";var n=r(1091),o=r(7157);n({target:\"Object\",stat:!0,arity:2,forced:Object.assign!==o},{assign:o})},1380:t=>{t.exports=function setCacheAdd(t){return this.__data__.set(t,\"__lodash_hash_undefined__\"),this}},1420:(t,e,r)=>{var n=r(79);t.exports=function stackClear(){this.__data__=new n,this.size=0}},1459:t=>{t.exports=function setCacheHas(t){return this.__data__.has(t)}},1489:(t,e,r)=>{var n=r(7400);t.exports=function toInteger(t){var e=n(t),r=e%1;return e==e?r?e-r:e:0}},1505:(t,e,r)=>{\"use strict\";var n=r(8828);t.exports=!n((function(){var t=function(){}.bind();return\"function\"!=typeof t||t.hasOwnProperty(\"prototype\")}))},1514:t=>{\"use strict\";t.exports=Math.abs},1549:(t,e,r)=>{var n=r(2032),o=r(3862),i=r(6721),a=r(2749),s=r(5749);function Hash(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}Hash.prototype.clear=n,Hash.prototype.delete=o,Hash.prototype.get=i,Hash.prototype.has=a,Hash.prototype.set=s,t.exports=Hash},1626:(t,e,r)=>{\"use strict\";var n=r(9447),o=r(4284),i=r(5817);t.exports=n?function(t,e,r){return o.f(t,e,i(1,r))}:function(t,e,r){return t[e]=r,t}},1733:t=>{var e=/[^\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7f]+/g;t.exports=function asciiWords(t){return t.match(e)||[]}},1747:(t,e,r)=>{\"use strict\";var n=r(5951),o=r(2046);t.exports=function(t,e){var r=o[t+\"Prototype\"],i=r&&r[e];if(i)return i;var a=n[t],s=a&&a.prototype;return s&&s[e]}},1769:(t,e,r)=>{var n=r(6449),o=r(8586),i=r(1802),a=r(3222);t.exports=function castPath(t,e){return n(t)?t:o(t,e)?[t]:i(a(t))}},1799:(t,e,r)=>{var n=r(7217),o=r(270);t.exports=function baseIsMatch(t,e,r,i){var a=r.length,s=a,u=!i;if(null==t)return!s;for(t=Object(t);a--;){var c=r[a];if(u&&c[2]?c[1]!==t[c[0]]:!(c[0]in t))return!1}for(;++a<s;){var f=(c=r[a])[0],l=t[f],p=c[1];if(u&&c[2]){if(void 0===l&&!(f in t))return!1}else{var h=new n;if(i)var d=i(l,p,f,t,e,h);if(!(void 0===d?o(p,l,3,i,h):d))return!1}}return!0}},1800:t=>{var e=/\\s/;t.exports=function trimmedEndIndex(t){for(var r=t.length;r--&&e.test(t.charAt(r)););return r}},1802:(t,e,r)=>{var n=r(2224),o=/[^.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))/g,i=/\\\\(\\\\)?/g,a=n((function(t){var e=[];return 46===t.charCodeAt(0)&&e.push(\"\"),t.replace(o,(function(t,r,n,o){e.push(n?o.replace(i,\"$1\"):r||t)})),e}));t.exports=a},1873:(t,e,r)=>{var n=r(9325).Symbol;t.exports=n},1882:(t,e,r)=>{var n=r(2552),o=r(3805);t.exports=function isFunction(t){if(!o(t))return!1;var e=n(t);return\"[object Function]\"==e||\"[object GeneratorFunction]\"==e||\"[object AsyncFunction]\"==e||\"[object Proxy]\"==e}},1907:(t,e,r)=>{\"use strict\";var n=r(1505),o=Function.prototype,i=o.call,a=n&&o.bind.bind(i,i);t.exports=n?a:function(t){return function(){return i.apply(t,arguments)}}},1986:(t,e,r)=>{var n=r(1873),o=r(7828),i=r(5288),a=r(5911),s=r(317),u=r(4247),c=n?n.prototype:void 0,f=c?c.valueOf:void 0;t.exports=function equalByTag(t,e,r,n,c,l,p){switch(r){case\"[object DataView]\":if(t.byteLength!=e.byteLength||t.byteOffset!=e.byteOffset)return!1;t=t.buffer,e=e.buffer;case\"[object ArrayBuffer]\":return!(t.byteLength!=e.byteLength||!l(new o(t),new o(e)));case\"[object Boolean]\":case\"[object Date]\":case\"[object Number]\":return i(+t,+e);case\"[object Error]\":return t.name==e.name&&t.message==e.message;case\"[object RegExp]\":case\"[object String]\":return t==e+\"\";case\"[object Map]\":var h=s;case\"[object Set]\":var d=1&n;if(h||(h=u),t.size!=e.size&&!d)return!1;var y=p.get(t);if(y)return y==e;n|=2,p.set(t,e);var _=a(h(t),h(e),n,c,l,p);return p.delete(t),_;case\"[object Symbol]\":if(f)return f.call(t)==f.call(e)}return!1}},2006:(t,e,r)=>{var n=r(5389),o=r(4894),i=r(5950);t.exports=function createFind(t){return function(e,r,a){var s=Object(e);if(!o(e)){var u=n(r,3);e=i(e),r=function(t){return u(s[t],t,s)}}var c=t(e,r,a);return c>-1?s[u?e[c]:c]:void 0}}},2032:(t,e,r)=>{var n=r(1042);t.exports=function hashClear(){this.__data__=n?n(null):{},this.size=0}},2046:t=>{\"use strict\";t.exports={}},2054:t=>{var e=\"\\\\ud800-\\\\udfff\",r=\"[\"+e+\"]\",n=\"[\\\\u0300-\\\\u036f\\\\ufe20-\\\\ufe2f\\\\u20d0-\\\\u20ff]\",o=\"\\\\ud83c[\\\\udffb-\\\\udfff]\",i=\"[^\"+e+\"]\",a=\"(?:\\\\ud83c[\\\\udde6-\\\\uddff]){2}\",s=\"[\\\\ud800-\\\\udbff][\\\\udc00-\\\\udfff]\",u=\"(?:\"+n+\"|\"+o+\")\"+\"?\",c=\"[\\\\ufe0e\\\\ufe0f]?\",f=c+u+(\"(?:\\\\u200d(?:\"+[i,a,s].join(\"|\")+\")\"+c+u+\")*\"),l=\"(?:\"+[i+n+\"?\",n,a,s,r].join(\"|\")+\")\",p=RegExp(o+\"(?=\"+o+\")|\"+l+f,\"g\");t.exports=function unicodeToArray(t){return t.match(p)||[]}},2159:(t,e,r)=>{\"use strict\";var n=r(2250),o=r(4640),i=TypeError;t.exports=function(t){if(n(t))return t;throw new i(o(t)+\" is not a function\")}},2199:(t,e,r)=>{var n=r(4528),o=r(6449);t.exports=function baseGetAllKeys(t,e,r){var i=e(t);return o(t)?i:n(i,r(t))}},2205:function(t,e,r){var n;n=void 0!==r.g?r.g:this,t.exports=function(t){if(t.CSS&&t.CSS.escape)return t.CSS.escape;var cssEscape=function(t){if(0==arguments.length)throw new TypeError(\"`CSS.escape` requires an argument.\");for(var e,r=String(t),n=r.length,o=-1,i=\"\",a=r.charCodeAt(0);++o<n;)0!=(e=r.charCodeAt(o))?i+=e>=1&&e<=31||127==e||0==o&&e>=48&&e<=57||1==o&&e>=48&&e<=57&&45==a?\"\\\\\"+e.toString(16)+\" \":0==o&&1==n&&45==e||!(e>=128||45==e||95==e||e>=48&&e<=57||e>=65&&e<=90||e>=97&&e<=122)?\"\\\\\"+r.charAt(o):r.charAt(o):i+=\"�\";return i};return t.CSS||(t.CSS={}),t.CSS.escape=cssEscape,cssEscape}(n)},2224:(t,e,r)=>{var n=r(104);t.exports=function memoizeCapped(t){var e=n(t,(function(t){return 500===r.size&&r.clear(),t})),r=e.cache;return e}},2225:t=>{var e=\"\\\\ud800-\\\\udfff\",r=\"\\\\u2700-\\\\u27bf\",n=\"a-z\\\\xdf-\\\\xf6\\\\xf8-\\\\xff\",o=\"A-Z\\\\xc0-\\\\xd6\\\\xd8-\\\\xde\",i=\"\\\\xac\\\\xb1\\\\xd7\\\\xf7\\\\x00-\\\\x2f\\\\x3a-\\\\x40\\\\x5b-\\\\x60\\\\x7b-\\\\xbf\\\\u2000-\\\\u206f \\\\t\\\\x0b\\\\f\\\\xa0\\\\ufeff\\\\n\\\\r\\\\u2028\\\\u2029\\\\u1680\\\\u180e\\\\u2000\\\\u2001\\\\u2002\\\\u2003\\\\u2004\\\\u2005\\\\u2006\\\\u2007\\\\u2008\\\\u2009\\\\u200a\\\\u202f\\\\u205f\\\\u3000\",a=\"[\"+i+\"]\",s=\"\\\\d+\",u=\"[\"+r+\"]\",c=\"[\"+n+\"]\",f=\"[^\"+e+i+s+r+n+o+\"]\",l=\"(?:\\\\ud83c[\\\\udde6-\\\\uddff]){2}\",p=\"[\\\\ud800-\\\\udbff][\\\\udc00-\\\\udfff]\",h=\"[\"+o+\"]\",d=\"(?:\"+c+\"|\"+f+\")\",y=\"(?:\"+h+\"|\"+f+\")\",_=\"(?:['’](?:d|ll|m|re|s|t|ve))?\",g=\"(?:['’](?:D|LL|M|RE|S|T|VE))?\",m=\"(?:[\\\\u0300-\\\\u036f\\\\ufe20-\\\\ufe2f\\\\u20d0-\\\\u20ff]|\\\\ud83c[\\\\udffb-\\\\udfff])?\",v=\"[\\\\ufe0e\\\\ufe0f]?\",b=v+m+(\"(?:\\\\u200d(?:\"+[\"[^\"+e+\"]\",l,p].join(\"|\")+\")\"+v+m+\")*\"),w=\"(?:\"+[u,l,p].join(\"|\")+\")\"+b,I=RegExp([h+\"?\"+c+\"+\"+_+\"(?=\"+[a,h,\"$\"].join(\"|\")+\")\",y+\"+\"+g+\"(?=\"+[a,h+d,\"$\"].join(\"|\")+\")\",h+\"?\"+d+\"+\"+_,h+\"+\"+g,\"\\\\d*(?:1ST|2ND|3RD|(?![123])\\\\dTH)(?=\\\\b|[a-z_])\",\"\\\\d*(?:1st|2nd|3rd|(?![123])\\\\dth)(?=\\\\b|[A-Z_])\",s,w].join(\"|\"),\"g\");t.exports=function unicodeWords(t){return t.match(I)||[]}},2250:t=>{\"use strict\";var e=\"object\"==typeof document&&document.all;t.exports=void 0===e&&void 0!==e?function(t){return\"function\"==typeof t||t===e}:function(t){return\"function\"==typeof t}},2361:(t,e,r)=>{\"use strict\";var n=r(5807),o=r(1907);t.exports=function(t){if(\"Function\"===n(t))return o(t)}},2426:(t,e,r)=>{var n=r(4248),o=r(5389),i=r(916),a=r(6449),s=r(6800);t.exports=function some(t,e,r){var u=a(t)?n:i;return r&&s(t,e,r)&&(e=void 0),u(t,o(e,3))}},2428:(t,e,r)=>{var n=r(7534),o=r(346),i=Object.prototype,a=i.hasOwnProperty,s=i.propertyIsEnumerable,u=n(function(){return arguments}())?n:function(t){return o(t)&&a.call(t,\"callee\")&&!s.call(t,\"callee\")};t.exports=u},2507:(t,e,r)=>{var n=r(8754),o=r(9698),i=r(3912),a=r(3222);t.exports=function createCaseFirst(t){return function(e){e=a(e);var r=o(e)?i(e):void 0,s=r?r[0]:e.charAt(0),u=r?n(r,1).join(\"\"):e.slice(1);return s[t]()+u}}},2523:t=>{t.exports=function baseFindIndex(t,e,r,n){for(var o=t.length,i=r+(n?1:-1);n?i--:++i<o;)if(e(t[i],i,t))return i;return-1}},2532:(t,e,r)=>{\"use strict\";var n=r(5951),o=Object.defineProperty;t.exports=function(t,e){try{o(n,t,{value:e,configurable:!0,writable:!0})}catch(r){n[t]=e}return e}},2552:(t,e,r)=>{var n=r(1873),o=r(659),i=r(9350),a=n?n.toStringTag:void 0;t.exports=function baseGetTag(t){return null==t?void 0===t?\"[object Undefined]\":\"[object Null]\":a&&a in Object(t)?o(t):i(t)}},2567:(t,e,r)=>{\"use strict\";r(9307);var n=r(1747);t.exports=n(\"Function\",\"bind\")},2574:(t,e)=>{\"use strict\";var r={}.propertyIsEnumerable,n=Object.getOwnPropertyDescriptor,o=n&&!r.call({1:2},1);e.f=o?function propertyIsEnumerable(t){var e=n(this,t);return!!e&&e.enumerable}:r},2651:(t,e,r)=>{var n=r(4218);t.exports=function getMapData(t,e){var r=t.__data__;return n(e)?r[\"string\"==typeof e?\"string\":\"hash\"]:r.map}},2682:(t,e,r)=>{\"use strict\";var n=r(9600),o=Object.prototype.toString,i=Object.prototype.hasOwnProperty;t.exports=function forEach(t,e,r){if(!n(e))throw new TypeError(\"iterator must be a function\");var a;arguments.length>=3&&(a=r),function isArray(t){return\"[object Array]\"===o.call(t)}(t)?function forEachArray(t,e,r){for(var n=0,o=t.length;n<o;n++)i.call(t,n)&&(null==r?e(t[n],n,t):e.call(r,t[n],n,t))}(t,e,a):\"string\"==typeof t?function forEachString(t,e,r){for(var n=0,o=t.length;n<o;n++)null==r?e(t.charAt(n),n,t):e.call(r,t.charAt(n),n,t)}(t,e,a):function forEachObject(t,e,r){for(var n in t)i.call(t,n)&&(null==r?e(t[n],n,t):e.call(r,t[n],n,t))}(t,e,a)}},2749:(t,e,r)=>{var n=r(1042),o=Object.prototype.hasOwnProperty;t.exports=function hashHas(t){var e=this.__data__;return n?void 0!==e[t]:o.call(e,t)}},2802:(t,e,r)=>{\"use strict\";t.exports=function SHA(e){var r=e.toLowerCase(),n=t.exports[r];if(!n)throw new Error(r+\" is not supported (we accept pull requests)\");return new n},t.exports.sha=r(7816),t.exports.sha1=r(3737),t.exports.sha224=r(6710),t.exports.sha256=r(4107),t.exports.sha384=r(2827),t.exports.sha512=r(2890)},2804:(t,e,r)=>{var n=r(6110)(r(9325),\"Promise\");t.exports=n},2827:(t,e,r)=>{\"use strict\";var n=r(6698),o=r(2890),i=r(8011),a=r(2861).Buffer,s=new Array(160);function Sha384(){this.init(),this._w=s,i.call(this,128,112)}n(Sha384,o),Sha384.prototype.init=function(){return this._ah=3418070365,this._bh=1654270250,this._ch=2438529370,this._dh=355462360,this._eh=1731405415,this._fh=2394180231,this._gh=3675008525,this._hh=1203062813,this._al=3238371032,this._bl=914150663,this._cl=812702999,this._dl=4144912697,this._el=4290775857,this._fl=1750603025,this._gl=1694076839,this._hl=3204075428,this},Sha384.prototype._hash=function(){var t=a.allocUnsafe(48);function writeInt64BE(e,r,n){t.writeInt32BE(e,n),t.writeInt32BE(r,n+4)}return writeInt64BE(this._ah,this._al,0),writeInt64BE(this._bh,this._bl,8),writeInt64BE(this._ch,this._cl,16),writeInt64BE(this._dh,this._dl,24),writeInt64BE(this._eh,this._el,32),writeInt64BE(this._fh,this._fl,40),t},t.exports=Sha384},2861:(t,e,r)=>{var n=r(8287),o=n.Buffer;function copyProps(t,e){for(var r in t)e[r]=t[r]}function SafeBuffer(t,e,r){return o(t,e,r)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?t.exports=n:(copyProps(n,e),e.Buffer=SafeBuffer),SafeBuffer.prototype=Object.create(o.prototype),copyProps(o,SafeBuffer),SafeBuffer.from=function(t,e,r){if(\"number\"==typeof t)throw new TypeError(\"Argument must not be a number\");return o(t,e,r)},SafeBuffer.alloc=function(t,e,r){if(\"number\"!=typeof t)throw new TypeError(\"Argument must be a number\");var n=o(t);return void 0!==e?\"string\"==typeof r?n.fill(e,r):n.fill(e):n.fill(0),n},SafeBuffer.allocUnsafe=function(t){if(\"number\"!=typeof t)throw new TypeError(\"Argument must be a number\");return o(t)},SafeBuffer.allocUnsafeSlow=function(t){if(\"number\"!=typeof t)throw new TypeError(\"Argument must be a number\");return n.SlowBuffer(t)}},2875:(t,e,r)=>{\"use strict\";var n=r(3045),o=r(376);t.exports=Object.keys||function keys(t){return n(t,o)}},2890:(t,e,r)=>{\"use strict\";var n=r(6698),o=r(8011),i=r(2861).Buffer,a=[1116352408,3609767458,1899447441,602891725,3049323471,3964484399,3921009573,2173295548,961987163,4081628472,1508970993,3053834265,2453635748,2937671579,2870763221,3664609560,3624381080,2734883394,310598401,1164996542,607225278,1323610764,1426881987,3590304994,1925078388,4068182383,2162078206,991336113,2614888103,633803317,3248222580,3479774868,3835390401,2666613458,4022224774,944711139,264347078,2341262773,604807628,2007800933,770255983,1495990901,1249150122,1856431235,1555081692,3175218132,1996064986,2198950837,2554220882,3999719339,2821834349,766784016,2952996808,2566594879,3210313671,3203337956,3336571891,1034457026,3584528711,2466948901,113926993,3758326383,338241895,168717936,666307205,1188179964,773529912,1546045734,1294757372,1522805485,1396182291,2643833823,1695183700,2343527390,1986661051,1014477480,2177026350,1206759142,2456956037,344077627,2730485921,1290863460,2820302411,3158454273,3259730800,3505952657,3345764771,106217008,3516065817,3606008344,3600352804,1432725776,4094571909,1467031594,275423344,851169720,430227734,3100823752,506948616,1363258195,659060556,3750685593,883997877,3785050280,958139571,3318307427,1322822218,3812723403,1537002063,2003034995,1747873779,3602036899,1955562222,1575990012,2024104815,1125592928,2227730452,2716904306,2361852424,442776044,2428436474,593698344,2756734187,3733110249,3204031479,2999351573,3329325298,3815920427,3391569614,3928383900,3515267271,566280711,3940187606,3454069534,4118630271,4000239992,116418474,1914138554,174292421,2731055270,289380356,3203993006,460393269,320620315,685471733,587496836,852142971,1086792851,1017036298,365543100,1126000580,2618297676,1288033470,3409855158,1501505948,4234509866,1607167915,987167468,1816402316,1246189591],s=new Array(160);function Sha512(){this.init(),this._w=s,o.call(this,128,112)}function Ch(t,e,r){return r^t&(e^r)}function maj(t,e,r){return t&e|r&(t|e)}function sigma0(t,e){return(t>>>28|e<<4)^(e>>>2|t<<30)^(e>>>7|t<<25)}function sigma1(t,e){return(t>>>14|e<<18)^(t>>>18|e<<14)^(e>>>9|t<<23)}function Gamma0(t,e){return(t>>>1|e<<31)^(t>>>8|e<<24)^t>>>7}function Gamma0l(t,e){return(t>>>1|e<<31)^(t>>>8|e<<24)^(t>>>7|e<<25)}function Gamma1(t,e){return(t>>>19|e<<13)^(e>>>29|t<<3)^t>>>6}function Gamma1l(t,e){return(t>>>19|e<<13)^(e>>>29|t<<3)^(t>>>6|e<<26)}function getCarry(t,e){return t>>>0<e>>>0?1:0}n(Sha512,o),Sha512.prototype.init=function(){return this._ah=1779033703,this._bh=3144134277,this._ch=1013904242,this._dh=2773480762,this._eh=1359893119,this._fh=2600822924,this._gh=528734635,this._hh=1541459225,this._al=4089235720,this._bl=2227873595,this._cl=4271175723,this._dl=1595750129,this._el=2917565137,this._fl=725511199,this._gl=4215389547,this._hl=327033209,this},Sha512.prototype._update=function(t){for(var e=this._w,r=0|this._ah,n=0|this._bh,o=0|this._ch,i=0|this._dh,s=0|this._eh,u=0|this._fh,c=0|this._gh,f=0|this._hh,l=0|this._al,p=0|this._bl,h=0|this._cl,d=0|this._dl,y=0|this._el,_=0|this._fl,g=0|this._gl,m=0|this._hl,v=0;v<32;v+=2)e[v]=t.readInt32BE(4*v),e[v+1]=t.readInt32BE(4*v+4);for(;v<160;v+=2){var b=e[v-30],w=e[v-30+1],I=Gamma0(b,w),x=Gamma0l(w,b),B=Gamma1(b=e[v-4],w=e[v-4+1]),k=Gamma1l(w,b),C=e[v-14],j=e[v-14+1],q=e[v-32],L=e[v-32+1],P=x+j|0,U=I+C+getCarry(P,x)|0;U=(U=U+B+getCarry(P=P+k|0,k)|0)+q+getCarry(P=P+L|0,L)|0,e[v]=U,e[v+1]=P}for(var D=0;D<160;D+=2){U=e[D],P=e[D+1];var z=maj(r,n,o),W=maj(l,p,h),V=sigma0(r,l),K=sigma0(l,r),$=sigma1(s,y),H=sigma1(y,s),Y=a[D],Z=a[D+1],J=Ch(s,u,c),tt=Ch(y,_,g),et=m+H|0,rt=f+$+getCarry(et,m)|0;rt=(rt=(rt=rt+J+getCarry(et=et+tt|0,tt)|0)+Y+getCarry(et=et+Z|0,Z)|0)+U+getCarry(et=et+P|0,P)|0;var nt=K+W|0,ot=V+z+getCarry(nt,K)|0;f=c,m=g,c=u,g=_,u=s,_=y,s=i+rt+getCarry(y=d+et|0,d)|0,i=o,d=h,o=n,h=p,n=r,p=l,r=rt+ot+getCarry(l=et+nt|0,et)|0}this._al=this._al+l|0,this._bl=this._bl+p|0,this._cl=this._cl+h|0,this._dl=this._dl+d|0,this._el=this._el+y|0,this._fl=this._fl+_|0,this._gl=this._gl+g|0,this._hl=this._hl+m|0,this._ah=this._ah+r+getCarry(this._al,l)|0,this._bh=this._bh+n+getCarry(this._bl,p)|0,this._ch=this._ch+o+getCarry(this._cl,h)|0,this._dh=this._dh+i+getCarry(this._dl,d)|0,this._eh=this._eh+s+getCarry(this._el,y)|0,this._fh=this._fh+u+getCarry(this._fl,_)|0,this._gh=this._gh+c+getCarry(this._gl,g)|0,this._hh=this._hh+f+getCarry(this._hl,m)|0},Sha512.prototype._hash=function(){var t=i.allocUnsafe(64);function writeInt64BE(e,r,n){t.writeInt32BE(e,n),t.writeInt32BE(r,n+4)}return writeInt64BE(this._ah,this._al,0),writeInt64BE(this._bh,this._bl,8),writeInt64BE(this._ch,this._cl,16),writeInt64BE(this._dh,this._dl,24),writeInt64BE(this._eh,this._el,32),writeInt64BE(this._fh,this._fl,40),writeInt64BE(this._gh,this._gl,48),writeInt64BE(this._hh,this._hl,56),t},t.exports=Sha512},2949:(t,e,r)=>{var n=r(2651);t.exports=function mapCacheSet(t,e){var r=n(this,t),o=r.size;return r.set(t,e),this.size+=r.size==o?0:1,this}},3034:(t,e,r)=>{\"use strict\";var n=r(8280),o=r(2567),i=Function.prototype;t.exports=function(t){var e=t.bind;return t===i||n(i,t)&&e===i.bind?o:e}},3040:(t,e,r)=>{var n=r(1549),o=r(79),i=r(8223);t.exports=function mapCacheClear(){this.size=0,this.__data__={hash:new n,map:new(i||o),string:new n}}},3045:(t,e,r)=>{\"use strict\";var n=r(1907),o=r(9724),i=r(7374),a=r(4436).indexOf,s=r(8530),u=n([].push);t.exports=function(t,e){var r,n=i(t),c=0,f=[];for(r in n)!o(s,r)&&o(n,r)&&u(f,r);for(;e.length>c;)o(n,r=e[c++])&&(~a(f,r)||u(f,r));return f}},3093:(t,e,r)=>{\"use strict\";var n=r(4459);t.exports=function sign(t){return n(t)||0===t?t:t<0?-1:1}},3121:(t,e,r)=>{\"use strict\";var n=r(5482),o=Math.min;t.exports=function(t){var e=n(t);return e>0?o(e,9007199254740991):0}},3126:(t,e,r)=>{\"use strict\";var n=r(6743),o=r(9675),i=r(76),a=r(3144);t.exports=function callBindBasic(t){if(t.length<1||\"function\"!=typeof t[0])throw new o(\"a function is required\");return a(n,i,t)}},3144:(t,e,r)=>{\"use strict\";var n=r(6743),o=r(1002),i=r(76),a=r(7119);t.exports=a||n.call(i,o)},3209:(t,e,r)=>{\"use strict\";var n=r(5606),o=65536,i=4294967295;var a=r(2861).Buffer,s=r.g.crypto||r.g.msCrypto;s&&s.getRandomValues?t.exports=function randomBytes(t,e){if(t>i)throw new RangeError(\"requested too many random bytes\");var r=a.allocUnsafe(t);if(t>0)if(t>o)for(var u=0;u<t;u+=o)s.getRandomValues(r.slice(u,u+o));else s.getRandomValues(r);if(\"function\"==typeof e)return n.nextTick((function(){e(null,r)}));return r}:t.exports=function oldBrowser(){throw new Error(\"Secure random number generation is not supported by this browser.\\nUse Chrome, Firefox or Internet Explorer 11\")}},3221:t=>{t.exports=function createBaseFor(t){return function(e,r,n){for(var o=-1,i=Object(e),a=n(e),s=a.length;s--;){var u=a[t?s:++o];if(!1===r(i[u],u,i))break}return e}}},3222:(t,e,r)=>{var n=r(7556);t.exports=function toString(t){return null==t?\"\":n(t)}},3243:(t,e,r)=>{var n=r(6110),o=function(){try{var t=n(Object,\"defineProperty\");return t({},\"\",{}),t}catch(t){}}();t.exports=o},3345:t=>{t.exports=function stubArray(){return[]}},3360:(t,e,r)=>{var n=r(3243);t.exports=function baseAssignValue(t,e,r){\"__proto__\"==e&&n?n(t,e,{configurable:!0,enumerable:!0,value:r,writable:!0}):t[e]=r}},3375:(t,e,r)=>{\"use strict\";var n=r(3700);t.exports=n},3427:(t,e,r)=>{\"use strict\";var n=r(1907);t.exports=n([].slice)},3488:t=>{t.exports=function identity(t){return t}},3556:(t,e,r)=>{\"use strict\";var n=r(9846);t.exports=n&&!Symbol.sham&&\"symbol\"==typeof Symbol.iterator},3605:t=>{t.exports=function stackGet(t){return this.__data__.get(t)}},3628:(t,e,r)=>{\"use strict\";var n=r(8648),o=r(1064),i=r(7176);t.exports=n?function getProto(t){return n(t)}:o?function getProto(t){if(!t||\"object\"!=typeof t&&\"function\"!=typeof t)throw new TypeError(\"getProto: not an object\");return o(t)}:i?function getProto(t){return i(t)}:null},3648:(t,e,r)=>{\"use strict\";var n=r(9447),o=r(8828),i=r(9552);t.exports=!n&&!o((function(){return 7!==Object.defineProperty(i(\"div\"),\"a\",{get:function(){return 7}}).a}))},3650:(t,e,r)=>{var n=r(4335)(Object.keys,Object);t.exports=n},3656:(t,e,r)=>{t=r.nmd(t);var n=r(9325),o=r(9935),i=e&&!e.nodeType&&e,a=i&&t&&!t.nodeType&&t,s=a&&a.exports===i?n.Buffer:void 0,u=(s?s.isBuffer:void 0)||o;t.exports=u},3661:(t,e,r)=>{var n=r(3040),o=r(7670),i=r(289),a=r(4509),s=r(2949);function MapCache(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}MapCache.prototype.clear=n,MapCache.prototype.delete=o,MapCache.prototype.get=i,MapCache.prototype.has=a,MapCache.prototype.set=s,t.exports=MapCache},3663:(t,e,r)=>{var n=r(1799),o=r(776),i=r(7197);t.exports=function baseMatches(t){var e=o(t);return 1==e.length&&e[0][2]?i(e[0][0],e[0][1]):function(r){return r===t||n(r,t,e)}}},3700:(t,e,r)=>{\"use strict\";var n=r(9709);t.exports=n},3702:t=>{t.exports=function listCacheClear(){this.__data__=[],this.size=0}},3737:(t,e,r)=>{\"use strict\";var n=r(6698),o=r(8011),i=r(2861).Buffer,a=[1518500249,1859775393,-1894007588,-899497514],s=new Array(80);function Sha1(){this.init(),this._w=s,o.call(this,64,56)}function rotl5(t){return t<<5|t>>>27}function rotl30(t){return t<<30|t>>>2}function ft(t,e,r,n){return 0===t?e&r|~e&n:2===t?e&r|e&n|r&n:e^r^n}n(Sha1,o),Sha1.prototype.init=function(){return this._a=1732584193,this._b=4023233417,this._c=2562383102,this._d=271733878,this._e=3285377520,this},Sha1.prototype._update=function(t){for(var e,r=this._w,n=0|this._a,o=0|this._b,i=0|this._c,s=0|this._d,u=0|this._e,c=0;c<16;++c)r[c]=t.readInt32BE(4*c);for(;c<80;++c)r[c]=(e=r[c-3]^r[c-8]^r[c-14]^r[c-16])<<1|e>>>31;for(var f=0;f<80;++f){var l=~~(f/20),p=rotl5(n)+ft(l,o,i,s)+u+r[f]+a[l]|0;u=s,s=i,i=rotl30(o),o=n,n=p}this._a=n+this._a|0,this._b=o+this._b|0,this._c=i+this._c|0,this._d=s+this._d|0,this._e=u+this._e|0},Sha1.prototype._hash=function(){var t=i.allocUnsafe(20);return t.writeInt32BE(0|this._a,0),t.writeInt32BE(0|this._b,4),t.writeInt32BE(0|this._c,8),t.writeInt32BE(0|this._d,12),t.writeInt32BE(0|this._e,16),t},t.exports=Sha1},3805:t=>{t.exports=function isObject(t){var e=typeof t;return null!=t&&(\"object\"==e||\"function\"==e)}},3846:(t,e,r)=>{\"use strict\";var n=r(9447),o=r(3930),i=r(2574),a=r(5817),s=r(7374),u=r(470),c=r(9724),f=r(3648),l=Object.getOwnPropertyDescriptor;e.f=n?l:function getOwnPropertyDescriptor(t,e){if(t=s(t),e=u(e),f)try{return l(t,e)}catch(t){}if(c(t,e))return a(!o(i.f,t,e),t[e])}},3862:t=>{t.exports=function hashDelete(t){var e=this.has(t)&&delete this.__data__[t];return this.size-=e?1:0,e}},3912:(t,e,r)=>{var n=r(1074),o=r(9698),i=r(2054);t.exports=function stringToArray(t){return o(t)?i(t):n(t)}},3930:(t,e,r)=>{\"use strict\";var n=r(1505),o=Function.prototype.call;t.exports=n?o.bind(o):function(){return o.apply(o,arguments)}},4039:(t,e,r)=>{\"use strict\";var n=\"undefined\"!=typeof Symbol&&Symbol,o=r(1333);t.exports=function hasNativeSymbols(){return\"function\"==typeof n&&(\"function\"==typeof Symbol&&(\"symbol\"==typeof n(\"foo\")&&(\"symbol\"==typeof Symbol(\"bar\")&&o())))}},4058:(t,e,r)=>{var n=r(4792),o=r(5539)((function(t,e,r){return e=e.toLowerCase(),t+(r?n(e):e)}));t.exports=o},4107:(t,e,r)=>{\"use strict\";var n=r(6698),o=r(8011),i=r(2861).Buffer,a=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],s=new Array(64);function Sha256(){this.init(),this._w=s,o.call(this,64,56)}function ch(t,e,r){return r^t&(e^r)}function maj(t,e,r){return t&e|r&(t|e)}function sigma0(t){return(t>>>2|t<<30)^(t>>>13|t<<19)^(t>>>22|t<<10)}function sigma1(t){return(t>>>6|t<<26)^(t>>>11|t<<21)^(t>>>25|t<<7)}function gamma0(t){return(t>>>7|t<<25)^(t>>>18|t<<14)^t>>>3}n(Sha256,o),Sha256.prototype.init=function(){return this._a=1779033703,this._b=3144134277,this._c=1013904242,this._d=2773480762,this._e=1359893119,this._f=2600822924,this._g=528734635,this._h=1541459225,this},Sha256.prototype._update=function(t){for(var e,r=this._w,n=0|this._a,o=0|this._b,i=0|this._c,s=0|this._d,u=0|this._e,c=0|this._f,f=0|this._g,l=0|this._h,p=0;p<16;++p)r[p]=t.readInt32BE(4*p);for(;p<64;++p)r[p]=0|(((e=r[p-2])>>>17|e<<15)^(e>>>19|e<<13)^e>>>10)+r[p-7]+gamma0(r[p-15])+r[p-16];for(var h=0;h<64;++h){var d=l+sigma1(u)+ch(u,c,f)+a[h]+r[h]|0,y=sigma0(n)+maj(n,o,i)|0;l=f,f=c,c=u,u=s+d|0,s=i,i=o,o=n,n=d+y|0}this._a=n+this._a|0,this._b=o+this._b|0,this._c=i+this._c|0,this._d=s+this._d|0,this._e=u+this._e|0,this._f=c+this._f|0,this._g=f+this._g|0,this._h=l+this._h|0},Sha256.prototype._hash=function(){var t=i.allocUnsafe(32);return t.writeInt32BE(this._a,0),t.writeInt32BE(this._b,4),t.writeInt32BE(this._c,8),t.writeInt32BE(this._d,12),t.writeInt32BE(this._e,16),t.writeInt32BE(this._f,20),t.writeInt32BE(this._g,24),t.writeInt32BE(this._h,28),t},t.exports=Sha256},4128:(t,e,r)=>{var n=r(1800),o=/^\\s+/;t.exports=function baseTrim(t){return t?t.slice(0,n(t)+1).replace(o,\"\"):t}},4218:t=>{t.exports=function isKeyable(t){var e=typeof t;return\"string\"==e||\"number\"==e||\"symbol\"==e||\"boolean\"==e?\"__proto__\"!==t:null===t}},4239:(t,e,r)=>{\"use strict\";var n=r(7136),o=TypeError;t.exports=function(t){if(n(t))throw new o(\"Can't call method on \"+t);return t}},4247:t=>{t.exports=function setToArray(t){var e=-1,r=Array(t.size);return t.forEach((function(t){r[++e]=t})),r}},4248:t=>{t.exports=function arraySome(t,e){for(var r=-1,n=null==t?0:t.length;++r<n;)if(e(t[r],r,t))return!0;return!1}},4284:(t,e,r)=>{\"use strict\";var n=r(9447),o=r(3648),i=r(8661),a=r(6624),s=r(470),u=TypeError,c=Object.defineProperty,f=Object.getOwnPropertyDescriptor,l=\"enumerable\",p=\"configurable\",h=\"writable\";e.f=n?i?function defineProperty(t,e,r){if(a(t),e=s(e),a(r),\"function\"==typeof t&&\"prototype\"===e&&\"value\"in r&&h in r&&!r[h]){var n=f(t,e);n&&n[h]&&(t[e]=r.value,r={configurable:p in r?r[p]:n[p],enumerable:l in r?r[l]:n[l],writable:!1})}return c(t,e,r)}:c:function defineProperty(t,e,r){if(a(t),e=s(e),a(r),o)try{return c(t,e,r)}catch(t){}if(\"get\"in r||\"set\"in r)throw new u(\"Accessors not supported\");return\"value\"in r&&(t[e]=r.value),t}},4335:t=>{t.exports=function overArg(t,e){return function(r){return t(e(r))}}},4372:(t,e,r)=>{\"use strict\";var n=r(9675),o=r(6556)(\"TypedArray.prototype.buffer\",!0),i=r(5680);t.exports=o||function typedArrayBuffer(t){if(!i(t))throw new n(\"Not a Typed Array\");return t.buffer}},4394:(t,e,r)=>{var n=r(2552),o=r(346);t.exports=function isSymbol(t){return\"symbol\"==typeof t||o(t)&&\"[object Symbol]\"==n(t)}},4436:(t,e,r)=>{\"use strict\";var n=r(7374),o=r(4849),i=r(575),createMethod=function(t){return function(e,r,a){var s=n(e),u=i(s);if(0===u)return!t&&-1;var c,f=o(a,u);if(t&&r!=r){for(;u>f;)if((c=s[f++])!=c)return!0}else for(;u>f;f++)if((t||f in s)&&s[f]===r)return t||f||0;return!t&&-1}};t.exports={includes:createMethod(!0),indexOf:createMethod(!1)}},4459:t=>{\"use strict\";t.exports=Number.isNaN||function isNaN(t){return t!=t}},4509:(t,e,r)=>{var n=r(2651);t.exports=function mapCacheHas(t){return n(this,t).has(t)}},4528:t=>{t.exports=function arrayPush(t,e){for(var r=-1,n=e.length,o=t.length;++r<n;)t[o+r]=e[r];return t}},4552:t=>{t.exports=function basePropertyOf(t){return function(e){return null==t?void 0:t[e]}}},4586:(t,e,r)=>{\"use strict\";var n=r(6743),o=r(1002),i=r(3144);t.exports=function applyBind(){return i(n,o,arguments)}},4634:t=>{var e={}.toString;t.exports=Array.isArray||function(t){return\"[object Array]\"==e.call(t)}},4640:t=>{\"use strict\";var e=String;t.exports=function(t){try{return e(t)}catch(t){return\"Object\"}}},4647:(t,e,r)=>{var n=r(4552)({À:\"A\",Á:\"A\",Â:\"A\",Ã:\"A\",Ä:\"A\",Å:\"A\",à:\"a\",á:\"a\",â:\"a\",ã:\"a\",ä:\"a\",å:\"a\",Ç:\"C\",ç:\"c\",Ð:\"D\",ð:\"d\",È:\"E\",É:\"E\",Ê:\"E\",Ë:\"E\",è:\"e\",é:\"e\",ê:\"e\",ë:\"e\",Ì:\"I\",Í:\"I\",Î:\"I\",Ï:\"I\",ì:\"i\",í:\"i\",î:\"i\",ï:\"i\",Ñ:\"N\",ñ:\"n\",Ò:\"O\",Ó:\"O\",Ô:\"O\",Õ:\"O\",Ö:\"O\",Ø:\"O\",ò:\"o\",ó:\"o\",ô:\"o\",õ:\"o\",ö:\"o\",ø:\"o\",Ù:\"U\",Ú:\"U\",Û:\"U\",Ü:\"U\",ù:\"u\",ú:\"u\",û:\"u\",ü:\"u\",Ý:\"Y\",ý:\"y\",ÿ:\"y\",Æ:\"Ae\",æ:\"ae\",Þ:\"Th\",þ:\"th\",ß:\"ss\",Ā:\"A\",Ă:\"A\",Ą:\"A\",ā:\"a\",ă:\"a\",ą:\"a\",Ć:\"C\",Ĉ:\"C\",Ċ:\"C\",Č:\"C\",ć:\"c\",ĉ:\"c\",ċ:\"c\",č:\"c\",Ď:\"D\",Đ:\"D\",ď:\"d\",đ:\"d\",Ē:\"E\",Ĕ:\"E\",Ė:\"E\",Ę:\"E\",Ě:\"E\",ē:\"e\",ĕ:\"e\",ė:\"e\",ę:\"e\",ě:\"e\",Ĝ:\"G\",Ğ:\"G\",Ġ:\"G\",Ģ:\"G\",ĝ:\"g\",ğ:\"g\",ġ:\"g\",ģ:\"g\",Ĥ:\"H\",Ħ:\"H\",ĥ:\"h\",ħ:\"h\",Ĩ:\"I\",Ī:\"I\",Ĭ:\"I\",Į:\"I\",İ:\"I\",ĩ:\"i\",ī:\"i\",ĭ:\"i\",į:\"i\",ı:\"i\",Ĵ:\"J\",ĵ:\"j\",Ķ:\"K\",ķ:\"k\",ĸ:\"k\",Ĺ:\"L\",Ļ:\"L\",Ľ:\"L\",Ŀ:\"L\",Ł:\"L\",ĺ:\"l\",ļ:\"l\",ľ:\"l\",ŀ:\"l\",ł:\"l\",Ń:\"N\",Ņ:\"N\",Ň:\"N\",Ŋ:\"N\",ń:\"n\",ņ:\"n\",ň:\"n\",ŋ:\"n\",Ō:\"O\",Ŏ:\"O\",Ő:\"O\",ō:\"o\",ŏ:\"o\",ő:\"o\",Ŕ:\"R\",Ŗ:\"R\",Ř:\"R\",ŕ:\"r\",ŗ:\"r\",ř:\"r\",Ś:\"S\",Ŝ:\"S\",Ş:\"S\",Š:\"S\",ś:\"s\",ŝ:\"s\",ş:\"s\",š:\"s\",Ţ:\"T\",Ť:\"T\",Ŧ:\"T\",ţ:\"t\",ť:\"t\",ŧ:\"t\",Ũ:\"U\",Ū:\"U\",Ŭ:\"U\",Ů:\"U\",Ű:\"U\",Ų:\"U\",ũ:\"u\",ū:\"u\",ŭ:\"u\",ů:\"u\",ű:\"u\",ų:\"u\",Ŵ:\"W\",ŵ:\"w\",Ŷ:\"Y\",ŷ:\"y\",Ÿ:\"Y\",Ź:\"Z\",Ż:\"Z\",Ž:\"Z\",ź:\"z\",ż:\"z\",ž:\"z\",Ĳ:\"IJ\",ĳ:\"ij\",Œ:\"Oe\",œ:\"oe\",ŉ:\"'n\",ſ:\"s\"});t.exports=n},4664:(t,e,r)=>{var n=r(9770),o=r(3345),i=Object.prototype.propertyIsEnumerable,a=Object.getOwnPropertySymbols,s=a?function(t){return null==t?[]:(t=Object(t),n(a(t),(function(e){return i.call(t,e)})))}:o;t.exports=s},4673:(t,e,r)=>{\"use strict\";var n=r(1907),o=r(2159),i=r(6285),a=r(9724),s=r(3427),u=r(1505),c=Function,f=n([].concat),l=n([].join),p={};t.exports=u?c.bind:function bind(t){var e=o(this),r=e.prototype,n=s(arguments,1),u=function bound(){var r=f(n,s(arguments));return this instanceof u?function(t,e,r){if(!a(p,e)){for(var n=[],o=0;o<e;o++)n[o]=\"a[\"+o+\"]\";p[e]=c(\"C,a\",\"return new C(\"+l(n,\",\")+\")\")}return p[e](t,r)}(e,r.length,r):e.apply(t,r)};return i(r)&&(u.prototype=r),u}},4713:(t,e,r)=>{var n=r(2523),o=r(5389),i=r(1489),a=Math.max;t.exports=function findIndex(t,e,r){var s=null==t?0:t.length;if(!s)return-1;var u=null==r?0:i(r);return u<0&&(u=a(s+u,0)),n(t,o(e,3),u)}},4739:(t,e,r)=>{var n=r(6025);t.exports=function listCacheGet(t){var e=this.__data__,r=n(e,t);return r<0?void 0:e[r][1]}},4792:(t,e,r)=>{var n=r(3222),o=r(5808);t.exports=function capitalize(t){return o(n(t).toLowerCase())}},4840:(t,e,r)=>{var n=\"object\"==typeof r.g&&r.g&&r.g.Object===Object&&r.g;t.exports=n},4849:(t,e,r)=>{\"use strict\";var n=r(5482),o=Math.max,i=Math.min;t.exports=function(t,e){var r=n(t);return r<0?o(r+e,0):i(r,e)}},4851:(t,e,r)=>{\"use strict\";t.exports=r(5401)},4894:(t,e,r)=>{var n=r(1882),o=r(294);t.exports=function isArrayLike(t){return null!=t&&o(t.length)&&!n(t)}},4901:(t,e,r)=>{var n=r(2552),o=r(294),i=r(346),a={};a[\"[object Float32Array]\"]=a[\"[object Float64Array]\"]=a[\"[object Int8Array]\"]=a[\"[object Int16Array]\"]=a[\"[object Int32Array]\"]=a[\"[object Uint8Array]\"]=a[\"[object Uint8ClampedArray]\"]=a[\"[object Uint16Array]\"]=a[\"[object Uint32Array]\"]=!0,a[\"[object Arguments]\"]=a[\"[object Array]\"]=a[\"[object ArrayBuffer]\"]=a[\"[object Boolean]\"]=a[\"[object DataView]\"]=a[\"[object Date]\"]=a[\"[object Error]\"]=a[\"[object Function]\"]=a[\"[object Map]\"]=a[\"[object Number]\"]=a[\"[object Object]\"]=a[\"[object RegExp]\"]=a[\"[object Set]\"]=a[\"[object String]\"]=a[\"[object WeakMap]\"]=!1,t.exports=function baseIsTypedArray(t){return i(t)&&o(t.length)&&!!a[n(t)]}},4932:t=>{t.exports=function arrayMap(t,e){for(var r=-1,n=null==t?0:t.length,o=Array(n);++r<n;)o[r]=e(t[r],r,t);return o}},5083:(t,e,r)=>{var n=r(1882),o=r(7296),i=r(3805),a=r(7473),s=/^\\[object .+?Constructor\\]$/,u=Function.prototype,c=Object.prototype,f=u.toString,l=c.hasOwnProperty,p=RegExp(\"^\"+f.call(l).replace(/[\\\\^$.*+?()[\\]{}|]/g,\"\\\\$&\").replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g,\"$1.*?\")+\"$\");t.exports=function baseIsNative(t){return!(!i(t)||o(t))&&(n(t)?p:s).test(a(t))}},5160:t=>{t.exports=function baseSlice(t,e,r){var n=-1,o=t.length;e<0&&(e=-e>o?0:o+e),(r=r>o?o:r)<0&&(r+=o),o=e>r?0:r-e>>>0,e>>>=0;for(var i=Array(o);++n<o;)i[n]=t[n+e];return i}},5287:(t,e)=>{\"use strict\";var r=Symbol.for(\"react.element\"),n=Symbol.for(\"react.portal\"),o=Symbol.for(\"react.fragment\"),i=Symbol.for(\"react.strict_mode\"),a=Symbol.for(\"react.profiler\"),s=Symbol.for(\"react.provider\"),u=Symbol.for(\"react.context\"),c=Symbol.for(\"react.forward_ref\"),f=Symbol.for(\"react.suspense\"),l=Symbol.for(\"react.memo\"),p=Symbol.for(\"react.lazy\"),h=Symbol.iterator;var d={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},y=Object.assign,_={};function E(t,e,r){this.props=t,this.context=e,this.refs=_,this.updater=r||d}function F(){}function G(t,e,r){this.props=t,this.context=e,this.refs=_,this.updater=r||d}E.prototype.isReactComponent={},E.prototype.setState=function(t,e){if(\"object\"!=typeof t&&\"function\"!=typeof t&&null!=t)throw Error(\"setState(...): takes an object of state variables to update or a function which returns an object of state variables.\");this.updater.enqueueSetState(this,t,e,\"setState\")},E.prototype.forceUpdate=function(t){this.updater.enqueueForceUpdate(this,t,\"forceUpdate\")},F.prototype=E.prototype;var g=G.prototype=new F;g.constructor=G,y(g,E.prototype),g.isPureReactComponent=!0;var m=Array.isArray,v=Object.prototype.hasOwnProperty,b={current:null},w={key:!0,ref:!0,__self:!0,__source:!0};function M(t,e,n){var o,i={},a=null,s=null;if(null!=e)for(o in void 0!==e.ref&&(s=e.ref),void 0!==e.key&&(a=\"\"+e.key),e)v.call(e,o)&&!w.hasOwnProperty(o)&&(i[o]=e[o]);var u=arguments.length-2;if(1===u)i.children=n;else if(1<u){for(var c=Array(u),f=0;f<u;f++)c[f]=arguments[f+2];i.children=c}if(t&&t.defaultProps)for(o in u=t.defaultProps)void 0===i[o]&&(i[o]=u[o]);return{$$typeof:r,type:t,key:a,ref:s,props:i,_owner:b.current}}function O(t){return\"object\"==typeof t&&null!==t&&t.$$typeof===r}var I=/\\/+/g;function Q(t,e){return\"object\"==typeof t&&null!==t&&null!=t.key?function escape(t){var e={\"=\":\"=0\",\":\":\"=2\"};return\"$\"+t.replace(/[=:]/g,(function(t){return e[t]}))}(\"\"+t.key):e.toString(36)}function R(t,e,o,i,a){var s=typeof t;\"undefined\"!==s&&\"boolean\"!==s||(t=null);var u=!1;if(null===t)u=!0;else switch(s){case\"string\":case\"number\":u=!0;break;case\"object\":switch(t.$$typeof){case r:case n:u=!0}}if(u)return a=a(u=t),t=\"\"===i?\".\"+Q(u,0):i,m(a)?(o=\"\",null!=t&&(o=t.replace(I,\"$&/\")+\"/\"),R(a,e,o,\"\",(function(t){return t}))):null!=a&&(O(a)&&(a=function N(t,e){return{$$typeof:r,type:t.type,key:e,ref:t.ref,props:t.props,_owner:t._owner}}(a,o+(!a.key||u&&u.key===a.key?\"\":(\"\"+a.key).replace(I,\"$&/\")+\"/\")+t)),e.push(a)),1;if(u=0,i=\"\"===i?\".\":i+\":\",m(t))for(var c=0;c<t.length;c++){var f=i+Q(s=t[c],c);u+=R(s,e,o,f,a)}else if(f=function A(t){return null===t||\"object\"!=typeof t?null:\"function\"==typeof(t=h&&t[h]||t[\"@@iterator\"])?t:null}(t),\"function\"==typeof f)for(t=f.call(t),c=0;!(s=t.next()).done;)u+=R(s=s.value,e,o,f=i+Q(s,c++),a);else if(\"object\"===s)throw e=String(t),Error(\"Objects are not valid as a React child (found: \"+(\"[object Object]\"===e?\"object with keys {\"+Object.keys(t).join(\", \")+\"}\":e)+\"). If you meant to render a collection of children, use an array instead.\");return u}function S(t,e,r){if(null==t)return t;var n=[],o=0;return R(t,n,\"\",\"\",(function(t){return e.call(r,t,o++)})),n}function T(t){if(-1===t._status){var e=t._result;(e=e()).then((function(e){0!==t._status&&-1!==t._status||(t._status=1,t._result=e)}),(function(e){0!==t._status&&-1!==t._status||(t._status=2,t._result=e)})),-1===t._status&&(t._status=0,t._result=e)}if(1===t._status)return t._result.default;throw t._result}var x={current:null},B={transition:null},k={ReactCurrentDispatcher:x,ReactCurrentBatchConfig:B,ReactCurrentOwner:b};function X(){throw Error(\"act(...) is not supported in production builds of React.\")}e.Children={map:S,forEach:function(t,e,r){S(t,(function(){e.apply(this,arguments)}),r)},count:function(t){var e=0;return S(t,(function(){e++})),e},toArray:function(t){return S(t,(function(t){return t}))||[]},only:function(t){if(!O(t))throw Error(\"React.Children.only expected to receive a single React element child.\");return t}},e.Component=E,e.Fragment=o,e.Profiler=a,e.PureComponent=G,e.StrictMode=i,e.Suspense=f,e.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=k,e.act=X,e.cloneElement=function(t,e,n){if(null==t)throw Error(\"React.cloneElement(...): The argument must be a React element, but you passed \"+t+\".\");var o=y({},t.props),i=t.key,a=t.ref,s=t._owner;if(null!=e){if(void 0!==e.ref&&(a=e.ref,s=b.current),void 0!==e.key&&(i=\"\"+e.key),t.type&&t.type.defaultProps)var u=t.type.defaultProps;for(c in e)v.call(e,c)&&!w.hasOwnProperty(c)&&(o[c]=void 0===e[c]&&void 0!==u?u[c]:e[c])}var c=arguments.length-2;if(1===c)o.children=n;else if(1<c){u=Array(c);for(var f=0;f<c;f++)u[f]=arguments[f+2];o.children=u}return{$$typeof:r,type:t.type,key:i,ref:a,props:o,_owner:s}},e.createContext=function(t){return(t={$$typeof:u,_currentValue:t,_currentValue2:t,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null}).Provider={$$typeof:s,_context:t},t.Consumer=t},e.createElement=M,e.createFactory=function(t){var e=M.bind(null,t);return e.type=t,e},e.createRef=function(){return{current:null}},e.forwardRef=function(t){return{$$typeof:c,render:t}},e.isValidElement=O,e.lazy=function(t){return{$$typeof:p,_payload:{_status:-1,_result:t},_init:T}},e.memo=function(t,e){return{$$typeof:l,type:t,compare:void 0===e?null:e}},e.startTransition=function(t){var e=B.transition;B.transition={};try{t()}finally{B.transition=e}},e.unstable_act=X,e.useCallback=function(t,e){return x.current.useCallback(t,e)},e.useContext=function(t){return x.current.useContext(t)},e.useDebugValue=function(){},e.useDeferredValue=function(t){return x.current.useDeferredValue(t)},e.useEffect=function(t,e){return x.current.useEffect(t,e)},e.useId=function(){return x.current.useId()},e.useImperativeHandle=function(t,e,r){return x.current.useImperativeHandle(t,e,r)},e.useInsertionEffect=function(t,e){return x.current.useInsertionEffect(t,e)},e.useLayoutEffect=function(t,e){return x.current.useLayoutEffect(t,e)},e.useMemo=function(t,e){return x.current.useMemo(t,e)},e.useReducer=function(t,e,r){return x.current.useReducer(t,e,r)},e.useRef=function(t){return x.current.useRef(t)},e.useState=function(t){return x.current.useState(t)},e.useSyncExternalStore=function(t,e,r){return x.current.useSyncExternalStore(t,e,r)},e.useTransition=function(){return x.current.useTransition()},e.version=\"18.3.1\"},5288:t=>{t.exports=function eq(t,e){return t===e||t!=t&&e!=e}},5345:t=>{\"use strict\";t.exports=URIError},5377:(t,e,r)=>{\"use strict\";var n=r(2861).Buffer,o=r(4634),i=r(4372),a=ArrayBuffer.isView||function isView(t){try{return i(t),!0}catch(t){return!1}},s=\"undefined\"!=typeof Uint8Array,u=\"undefined\"!=typeof ArrayBuffer&&\"undefined\"!=typeof Uint8Array,c=u&&(n.prototype instanceof Uint8Array||n.TYPED_ARRAY_SUPPORT);t.exports=function toBuffer(t,e){if(t instanceof n)return t;if(\"string\"==typeof t)return n.from(t,e);if(u&&a(t)){if(0===t.byteLength)return n.alloc(0);if(c){var r=n.from(t.buffer,t.byteOffset,t.byteLength);if(r.byteLength===t.byteLength)return r}var i=t instanceof Uint8Array?t:new Uint8Array(t.buffer,t.byteOffset,t.byteLength),f=n.from(i);if(f.length===t.byteLength)return f}if(s&&t instanceof Uint8Array)return n.from(t);var l=o(t);if(l)for(var p=0;p<t.length;p+=1){var h=t[p];if(\"number\"!=typeof h||h<0||h>255||~~h!==h)throw new RangeError(\"Array items must be numbers in the range 0-255.\")}if(l||n.isBuffer(t)&&t.constructor&&\"function\"==typeof t.constructor.isBuffer&&t.constructor.isBuffer(t))return n.from(t);throw new TypeError('The \"data\" argument must be a string, an Array, a Buffer, a Uint8Array, or a DataView.')}},5389:(t,e,r)=>{var n=r(3663),o=r(7978),i=r(3488),a=r(6449),s=r(583);t.exports=function baseIteratee(t){return\"function\"==typeof t?t:null==t?i:\"object\"==typeof t?a(t)?o(t[0],t[1]):n(t):s(t)}},5401:(t,e,r)=>{\"use strict\";var n=r(462);t.exports=n},5434:t=>{var e=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;t.exports=function hasUnicodeWord(t){return e.test(t)}},5481:(t,e,r)=>{var n=r(9325)[\"__core-js_shared__\"];t.exports=n},5482:(t,e,r)=>{\"use strict\";var n=r(1176);t.exports=function(t){var e=+t;return e!=e||0===e?0:n(e)}},5527:t=>{var e=Object.prototype;t.exports=function isPrototype(t){var r=t&&t.constructor;return t===(\"function\"==typeof r&&r.prototype||e)}},5539:(t,e,r)=>{var n=r(882),o=r(828),i=r(6645),a=RegExp(\"['’]\",\"g\");t.exports=function createCompounder(t){return function(e){return n(i(o(e).replace(a,\"\")),t,\"\")}}},5580:(t,e,r)=>{var n=r(6110)(r(9325),\"DataView\");t.exports=n},5582:(t,e,r)=>{\"use strict\";var n=r(2046),o=r(5951),i=r(2250),aFunction=function(t){return i(t)?t:void 0};t.exports=function(t,e){return arguments.length<2?aFunction(n[t])||aFunction(o[t]):n[t]&&n[t][e]||o[t]&&o[t][e]}},5594:(t,e,r)=>{\"use strict\";var n=r(5582),o=r(2250),i=r(8280),a=r(3556),s=Object;t.exports=a?function(t){return\"symbol\"==typeof t}:function(t){var e=n(\"Symbol\");return o(e)&&i(e.prototype,s(t))}},5606:t=>{var e,r,n=t.exports={};function defaultSetTimout(){throw new Error(\"setTimeout has not been defined\")}function defaultClearTimeout(){throw new Error(\"clearTimeout has not been defined\")}function runTimeout(t){if(e===setTimeout)return setTimeout(t,0);if((e===defaultSetTimout||!e)&&setTimeout)return e=setTimeout,setTimeout(t,0);try{return e(t,0)}catch(r){try{return e.call(null,t,0)}catch(r){return e.call(this,t,0)}}}!function(){try{e=\"function\"==typeof setTimeout?setTimeout:defaultSetTimout}catch(t){e=defaultSetTimout}try{r=\"function\"==typeof clearTimeout?clearTimeout:defaultClearTimeout}catch(t){r=defaultClearTimeout}}();var o,i=[],a=!1,s=-1;function cleanUpNextTick(){a&&o&&(a=!1,o.length?i=o.concat(i):s=-1,i.length&&drainQueue())}function drainQueue(){if(!a){var t=runTimeout(cleanUpNextTick);a=!0;for(var e=i.length;e;){for(o=i,i=[];++s<e;)o&&o[s].run();s=-1,e=i.length}o=null,a=!1,function runClearTimeout(t){if(r===clearTimeout)return clearTimeout(t);if((r===defaultClearTimeout||!r)&&clearTimeout)return r=clearTimeout,clearTimeout(t);try{return r(t)}catch(e){try{return r.call(null,t)}catch(e){return r.call(this,t)}}}(t)}}function Item(t,e){this.fun=t,this.array=e}function noop(){}n.nextTick=function(t){var e=new Array(arguments.length-1);if(arguments.length>1)for(var r=1;r<arguments.length;r++)e[r-1]=arguments[r];i.push(new Item(t,e)),1!==i.length||a||runTimeout(drainQueue)},Item.prototype.run=function(){this.fun.apply(null,this.array)},n.title=\"browser\",n.browser=!0,n.env={},n.argv=[],n.version=\"\",n.versions={},n.on=noop,n.addListener=noop,n.once=noop,n.off=noop,n.removeListener=noop,n.removeAllListeners=noop,n.emit=noop,n.prependListener=noop,n.prependOnceListener=noop,n.listeners=function(t){return[]},n.binding=function(t){throw new Error(\"process.binding is not supported\")},n.cwd=function(){return\"/\"},n.chdir=function(t){throw new Error(\"process.chdir is not supported\")},n.umask=function(){return 0}},5680:(t,e,r)=>{\"use strict\";var n=r(5767);t.exports=function isTypedArray(t){return!!n(t)}},5749:(t,e,r)=>{var n=r(1042);t.exports=function hashSet(t,e){var r=this.__data__;return this.size+=this.has(t)?0:1,r[t]=n&&void 0===e?\"__lodash_hash_undefined__\":e,this}},5767:(t,e,r)=>{\"use strict\";var n=r(2682),o=r(9209),i=r(487),a=r(6556),s=r(5795),u=a(\"Object.prototype.toString\"),c=r(9092)(),f=\"undefined\"==typeof globalThis?r.g:globalThis,l=o(),p=a(\"String.prototype.slice\"),h=Object.getPrototypeOf,d=a(\"Array.prototype.indexOf\",!0)||function indexOf(t,e){for(var r=0;r<t.length;r+=1)if(t[r]===e)return r;return-1},y={__proto__:null};n(l,c&&s&&h?function(t){var e=new f[t];if(Symbol.toStringTag in e){var r=h(e),n=s(r,Symbol.toStringTag);if(!n){var o=h(r);n=s(o,Symbol.toStringTag)}y[\"$\"+t]=i(n.get)}}:function(t){var e=new f[t],r=e.slice||e.set;r&&(y[\"$\"+t]=i(r))});t.exports=function whichTypedArray(t){if(!t||\"object\"!=typeof t)return!1;if(!c){var e=p(u(t),8,-1);return d(l,e)>-1?e:\"Object\"===e&&function tryAllSlices(t){var e=!1;return n(y,(function(r,n){if(!e)try{r(t),e=p(n,1)}catch(t){}})),e}(t)}return s?function tryAllTypedArrays(t){var e=!1;return n(y,(function(r,n){if(!e)try{\"$\"+r(t)===n&&(e=p(n,1))}catch(t){}})),e}(t):null}},5795:(t,e,r)=>{\"use strict\";var n=r(6549);if(n)try{n([],\"length\")}catch(t){n=null}t.exports=n},5807:(t,e,r)=>{\"use strict\";var n=r(1907),o=n({}.toString),i=n(\"\".slice);t.exports=function(t){return i(o(t),8,-1)}},5808:(t,e,r)=>{var n=r(2507)(\"toUpperCase\");t.exports=n},5816:(t,e,r)=>{\"use strict\";var n=r(6128);t.exports=function(t,e){return n[t]||(n[t]=e||{})}},5817:t=>{\"use strict\";t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},5861:(t,e,r)=>{var n=r(5580),o=r(8223),i=r(2804),a=r(6545),s=r(8303),u=r(2552),c=r(7473),f=\"[object Map]\",l=\"[object Promise]\",p=\"[object Set]\",h=\"[object WeakMap]\",d=\"[object DataView]\",y=c(n),_=c(o),g=c(i),m=c(a),v=c(s),b=u;(n&&b(new n(new ArrayBuffer(1)))!=d||o&&b(new o)!=f||i&&b(i.resolve())!=l||a&&b(new a)!=p||s&&b(new s)!=h)&&(b=function(t){var e=u(t),r=\"[object Object]\"==e?t.constructor:void 0,n=r?c(r):\"\";if(n)switch(n){case y:return d;case _:return f;case g:return l;case m:return p;case v:return h}return e}),t.exports=b},5880:t=>{\"use strict\";t.exports=Math.pow},5911:(t,e,r)=>{var n=r(8859),o=r(4248),i=r(9219);t.exports=function equalArrays(t,e,r,a,s,u){var c=1&r,f=t.length,l=e.length;if(f!=l&&!(c&&l>f))return!1;var p=u.get(t),h=u.get(e);if(p&&h)return p==e&&h==t;var d=-1,y=!0,_=2&r?new n:void 0;for(u.set(t,e),u.set(e,t);++d<f;){var g=t[d],m=e[d];if(a)var v=c?a(m,g,d,e,t,u):a(g,m,d,t,e,u);if(void 0!==v){if(v)continue;y=!1;break}if(_){if(!o(e,(function(t,e){if(!i(_,e)&&(g===t||s(g,t,r,a,u)))return _.push(e)}))){y=!1;break}}else if(g!==m&&!s(g,m,r,a,u)){y=!1;break}}return u.delete(t),u.delete(e),y}},5950:(t,e,r)=>{var n=r(695),o=r(8984),i=r(4894);t.exports=function keys(t){return i(t)?n(t):o(t)}},5951:function(t,e,r){\"use strict\";var check=function(t){return t&&t.Math===Math&&t};t.exports=check(\"object\"==typeof globalThis&&globalThis)||check(\"object\"==typeof window&&window)||check(\"object\"==typeof self&&self)||check(\"object\"==typeof r.g&&r.g)||check(\"object\"==typeof this&&this)||function(){return this}()||Function(\"return this\")()},6009:(t,e,r)=>{t=r.nmd(t);var n=r(4840),o=e&&!e.nodeType&&e,i=o&&t&&!t.nodeType&&t,a=i&&i.exports===o&&n.process,s=function(){try{var t=i&&i.require&&i.require(\"util\").types;return t||a&&a.binding&&a.binding(\"util\")}catch(t){}}();t.exports=s},6024:(t,e,r)=>{\"use strict\";var n=r(1505),o=Function.prototype,i=o.apply,a=o.call;t.exports=\"object\"==typeof Reflect&&Reflect.apply||(n?a.bind(i):function(){return a.apply(i,arguments)})},6025:(t,e,r)=>{var n=r(5288);t.exports=function assocIndexOf(t,e){for(var r=t.length;r--;)if(n(t[r][0],e))return r;return-1}},6028:(t,e,r)=>{\"use strict\";var n=r(3930),o=r(6285),i=r(5594),a=r(9367),s=r(581),u=r(6264),c=TypeError,f=u(\"toPrimitive\");t.exports=function(t,e){if(!o(t)||i(t))return t;var r,u=a(t,f);if(u){if(void 0===e&&(e=\"default\"),r=n(u,t,e),!o(r)||i(r))return r;throw new c(\"Can't convert object to primitive value\")}return void 0===e&&(e=\"number\"),s(t,e)}},6110:(t,e,r)=>{var n=r(5083),o=r(392);t.exports=function getNative(t,e){var r=o(t,e);return n(r)?r:void 0}},6128:(t,e,r)=>{\"use strict\";var n=r(7376),o=r(5951),i=r(2532),a=\"__core-js_shared__\",s=t.exports=o[a]||i(a,{});(s.versions||(s.versions=[])).push({version:\"3.40.0\",mode:n?\"pure\":\"global\",copyright:\"© 2014-2025 Denis Pushkarev (zloirock.ru)\",license:\"https://github.com/zloirock/core-js/blob/v3.40.0/LICENSE\",source:\"https://github.com/zloirock/core-js\"})},6188:t=>{\"use strict\";t.exports=Math.max},6264:(t,e,r)=>{\"use strict\";var n=r(5951),o=r(5816),i=r(9724),a=r(6499),s=r(9846),u=r(3556),c=n.Symbol,f=o(\"wks\"),l=u?c.for||c:c&&c.withoutSetter||a;t.exports=function(t){return i(f,t)||(f[t]=s&&i(c,t)?c[t]:l(\"Symbol.\"+t)),f[t]}},6285:(t,e,r)=>{\"use strict\";var n=r(2250);t.exports=function(t){return\"object\"==typeof t?null!==t:n(t)}},6449:t=>{var e=Array.isArray;t.exports=e},6499:(t,e,r)=>{\"use strict\";var n=r(1907),o=0,i=Math.random(),a=n(1..toString);t.exports=function(t){return\"Symbol(\"+(void 0===t?\"\":t)+\")_\"+a(++o+i,36)}},6540:(t,e,r)=>{\"use strict\";t.exports=r(5287)},6545:(t,e,r)=>{var n=r(6110)(r(9325),\"Set\");t.exports=n},6547:(t,e,r)=>{var n=r(3360),o=r(5288),i=Object.prototype.hasOwnProperty;t.exports=function assignValue(t,e,r){var a=t[e];i.call(t,e)&&o(a,r)&&(void 0!==r||e in t)||n(t,e,r)}},6549:t=>{\"use strict\";t.exports=Object.getOwnPropertyDescriptor},6556:(t,e,r)=>{\"use strict\";var n=r(453),o=r(3126),i=o([n(\"%String.prototype.indexOf%\")]);t.exports=function callBoundIntrinsic(t,e){var r=n(t,!!e);return\"function\"==typeof r&&i(t,\".prototype.\")>-1?o([r]):r}},6578:t=>{\"use strict\";t.exports=[\"Float16Array\",\"Float32Array\",\"Float64Array\",\"Int8Array\",\"Int16Array\",\"Int32Array\",\"Uint8Array\",\"Uint8ClampedArray\",\"Uint16Array\",\"Uint32Array\",\"BigInt64Array\",\"BigUint64Array\"]},6624:(t,e,r)=>{\"use strict\";var n=r(6285),o=String,i=TypeError;t.exports=function(t){if(n(t))return t;throw new i(o(t)+\" is not an object\")}},6645:(t,e,r)=>{var n=r(1733),o=r(5434),i=r(3222),a=r(2225);t.exports=function words(t,e,r){return t=i(t),void 0===(e=r?void 0:e)?o(t)?a(t):n(t):t.match(e)||[]}},6649:(t,e,r)=>{var n=r(3221)();t.exports=n},6698:t=>{\"function\"==typeof Object.create?t.exports=function inherits(t,e){e&&(t.super_=e,t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}))}:t.exports=function inherits(t,e){if(e){t.super_=e;var TempCtor=function(){};TempCtor.prototype=e.prototype,t.prototype=new TempCtor,t.prototype.constructor=t}}},6710:(t,e,r)=>{\"use strict\";var n=r(6698),o=r(4107),i=r(8011),a=r(2861).Buffer,s=new Array(64);function Sha224(){this.init(),this._w=s,i.call(this,64,56)}n(Sha224,o),Sha224.prototype.init=function(){return this._a=3238371032,this._b=914150663,this._c=812702999,this._d=4144912697,this._e=4290775857,this._f=1750603025,this._g=1694076839,this._h=3204075428,this},Sha224.prototype._hash=function(){var t=a.allocUnsafe(28);return t.writeInt32BE(this._a,0),t.writeInt32BE(this._b,4),t.writeInt32BE(this._c,8),t.writeInt32BE(this._d,12),t.writeInt32BE(this._e,16),t.writeInt32BE(this._f,20),t.writeInt32BE(this._g,24),t},t.exports=Sha224},6721:(t,e,r)=>{var n=r(1042),o=Object.prototype.hasOwnProperty;t.exports=function hashGet(t){var e=this.__data__;if(n){var r=e[t];return\"__lodash_hash_undefined__\"===r?void 0:r}return o.call(e,t)?e[t]:void 0}},6743:(t,e,r)=>{\"use strict\";var n=r(9353);t.exports=Function.prototype.bind||n},6794:(t,e,r)=>{\"use strict\";var n=r(5951).navigator,o=n&&n.userAgent;t.exports=o?String(o):\"\"},6800:(t,e,r)=>{var n=r(5288),o=r(4894),i=r(361),a=r(3805);t.exports=function isIterateeCall(t,e,r){if(!a(r))return!1;var s=typeof e;return!!(\"number\"==s?o(r)&&i(e,r.length):\"string\"==s&&e in r)&&n(r[e],t)}},6897:(t,e,r)=>{\"use strict\";var n=r(453),o=r(41),i=r(592)(),a=r(5795),s=r(9675),u=n(\"%Math.floor%\");t.exports=function setFunctionLength(t,e){if(\"function\"!=typeof t)throw new s(\"`fn` is not a function\");if(\"number\"!=typeof e||e<0||e>4294967295||u(e)!==e)throw new s(\"`length` must be a positive 32-bit integer\");var r=arguments.length>2&&!!arguments[2],n=!0,c=!0;if(\"length\"in t&&a){var f=a(t,\"length\");f&&!f.configurable&&(n=!1),f&&!f.writable&&(c=!1)}return(n||c||!r)&&(i?o(t,\"length\",e,!0,!0):o(t,\"length\",e)),t}},6946:(t,e,r)=>{\"use strict\";var n=r(1907),o=r(8828),i=r(5807),a=Object,s=n(\"\".split);t.exports=o((function(){return!a(\"z\").propertyIsEnumerable(0)}))?function(t){return\"String\"===i(t)?s(t,\"\"):a(t)}:a},7068:(t,e,r)=>{var n=r(7217),o=r(5911),i=r(1986),a=r(689),s=r(5861),u=r(6449),c=r(3656),f=r(7167),l=\"[object Arguments]\",p=\"[object Array]\",h=\"[object Object]\",d=Object.prototype.hasOwnProperty;t.exports=function baseIsEqualDeep(t,e,r,y,_,g){var m=u(t),v=u(e),b=m?p:s(t),w=v?p:s(e),I=(b=b==l?h:b)==h,x=(w=w==l?h:w)==h,B=b==w;if(B&&c(t)){if(!c(e))return!1;m=!0,I=!1}if(B&&!I)return g||(g=new n),m||f(t)?o(t,e,r,y,_,g):i(t,e,b,r,y,_,g);if(!(1&r)){var k=I&&d.call(t,\"__wrapped__\"),C=x&&d.call(e,\"__wrapped__\");if(k||C){var j=k?t.value():t,q=C?e.value():e;return g||(g=new n),_(j,q,r,y,g)}}return!!B&&(g||(g=new n),a(t,e,r,y,_,g))}},7119:t=>{\"use strict\";t.exports=\"undefined\"!=typeof Reflect&&Reflect&&Reflect.apply},7136:t=>{\"use strict\";t.exports=function(t){return null==t}},7157:(t,e,r)=>{\"use strict\";var n=r(9447),o=r(1907),i=r(3930),a=r(8828),s=r(2875),u=r(7170),c=r(2574),f=r(9298),l=r(6946),p=Object.assign,h=Object.defineProperty,d=o([].concat);t.exports=!p||a((function(){if(n&&1!==p({b:1},p(h({},\"a\",{enumerable:!0,get:function(){h(this,\"b\",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var t={},e={},r=Symbol(\"assign detection\"),o=\"abcdefghijklmnopqrst\";return t[r]=7,o.split(\"\").forEach((function(t){e[t]=t})),7!==p({},t)[r]||s(p({},e)).join(\"\")!==o}))?function assign(t,e){for(var r=f(t),o=arguments.length,a=1,p=u.f,h=c.f;o>a;)for(var y,_=l(arguments[a++]),g=p?d(s(_),p(_)):s(_),m=g.length,v=0;m>v;)y=g[v++],n&&!i(h,_,y)||(r[y]=_[y]);return r}:p},7167:(t,e,r)=>{var n=r(4901),o=r(7301),i=r(6009),a=i&&i.isTypedArray,s=a?o(a):n;t.exports=s},7170:(t,e)=>{\"use strict\";e.f=Object.getOwnPropertySymbols},7176:(t,e,r)=>{\"use strict\";var n,o=r(3126),i=r(5795);try{n=[].__proto__===Array.prototype}catch(t){if(!t||\"object\"!=typeof t||!(\"code\"in t)||\"ERR_PROTO_ACCESS\"!==t.code)throw t}var a=!!n&&i&&i(Object.prototype,\"__proto__\"),s=Object,u=s.getPrototypeOf;t.exports=a&&\"function\"==typeof a.get?o([a.get]):\"function\"==typeof u&&function getDunder(t){return u(null==t?t:s(t))}},7197:t=>{t.exports=function matchesStrictComparable(t,e){return function(r){return null!=r&&(r[t]===e&&(void 0!==e||t in Object(r)))}}},7217:(t,e,r)=>{var n=r(79),o=r(1420),i=r(938),a=r(3605),s=r(9817),u=r(945);function Stack(t){var e=this.__data__=new n(t);this.size=e.size}Stack.prototype.clear=o,Stack.prototype.delete=i,Stack.prototype.get=a,Stack.prototype.has=s,Stack.prototype.set=u,t.exports=Stack},7237:t=>{t.exports=function baseProperty(t){return function(e){return null==e?void 0:e[t]}}},7248:(t,e,r)=>{var n=r(6547),o=r(1234);t.exports=function zipObject(t,e){return o(t||[],e||[],n)}},7255:(t,e,r)=>{var n=r(7422);t.exports=function basePropertyDeep(t){return function(e){return n(e,t)}}},7296:(t,e,r)=>{var n,o=r(5481),i=(n=/[^.]+$/.exec(o&&o.keys&&o.keys.IE_PROTO||\"\"))?\"Symbol(src)_1.\"+n:\"\";t.exports=function isMasked(t){return!!i&&i in t}},7301:t=>{t.exports=function baseUnary(t){return function(e){return t(e)}}},7309:(t,e,r)=>{var n=r(2006)(r(4713));t.exports=n},7374:(t,e,r)=>{\"use strict\";var n=r(6946),o=r(4239);t.exports=function(t){return n(o(t))}},7376:t=>{\"use strict\";t.exports=!0},7400:(t,e,r)=>{var n=r(9374),o=1/0;t.exports=function toFinite(t){return t?(t=n(t))===o||t===-1/0?17976931348623157e292*(t<0?-1:1):t==t?t:0:0===t?t:0}},7422:(t,e,r)=>{var n=r(1769),o=r(7797);t.exports=function baseGet(t,e){for(var r=0,i=(e=n(e,t)).length;null!=t&&r<i;)t=t[o(e[r++])];return r&&r==i?t:void 0}},7463:(t,e,r)=>{\"use strict\";var n=r(8828),o=r(2250),i=/#|\\.prototype\\./,isForced=function(t,e){var r=s[a(t)];return r===c||r!==u&&(o(e)?n(e):!!e)},a=isForced.normalize=function(t){return String(t).replace(i,\".\").toLowerCase()},s=isForced.data={},u=isForced.NATIVE=\"N\",c=isForced.POLYFILL=\"P\";t.exports=isForced},7473:t=>{var e=Function.prototype.toString;t.exports=function toSource(t){if(null!=t){try{return e.call(t)}catch(t){}try{return t+\"\"}catch(t){}}return\"\"}},7526:(t,e)=>{\"use strict\";e.byteLength=function byteLength(t){var e=getLens(t),r=e[0],n=e[1];return 3*(r+n)/4-n},e.toByteArray=function toByteArray(t){var e,r,i=getLens(t),a=i[0],s=i[1],u=new o(function _byteLength(t,e,r){return 3*(e+r)/4-r}(0,a,s)),c=0,f=s>0?a-4:a;for(r=0;r<f;r+=4)e=n[t.charCodeAt(r)]<<18|n[t.charCodeAt(r+1)]<<12|n[t.charCodeAt(r+2)]<<6|n[t.charCodeAt(r+3)],u[c++]=e>>16&255,u[c++]=e>>8&255,u[c++]=255&e;2===s&&(e=n[t.charCodeAt(r)]<<2|n[t.charCodeAt(r+1)]>>4,u[c++]=255&e);1===s&&(e=n[t.charCodeAt(r)]<<10|n[t.charCodeAt(r+1)]<<4|n[t.charCodeAt(r+2)]>>2,u[c++]=e>>8&255,u[c++]=255&e);return u},e.fromByteArray=function fromByteArray(t){for(var e,n=t.length,o=n%3,i=[],a=16383,s=0,u=n-o;s<u;s+=a)i.push(encodeChunk(t,s,s+a>u?u:s+a));1===o?(e=t[n-1],i.push(r[e>>2]+r[e<<4&63]+\"==\")):2===o&&(e=(t[n-2]<<8)+t[n-1],i.push(r[e>>10]+r[e>>4&63]+r[e<<2&63]+\"=\"));return i.join(\"\")};for(var r=[],n=[],o=\"undefined\"!=typeof Uint8Array?Uint8Array:Array,i=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\",a=0;a<64;++a)r[a]=i[a],n[i.charCodeAt(a)]=a;function getLens(t){var e=t.length;if(e%4>0)throw new Error(\"Invalid string. Length must be a multiple of 4\");var r=t.indexOf(\"=\");return-1===r&&(r=e),[r,r===e?0:4-r%4]}function encodeChunk(t,e,n){for(var o,i,a=[],s=e;s<n;s+=3)o=(t[s]<<16&16711680)+(t[s+1]<<8&65280)+(255&t[s+2]),a.push(r[(i=o)>>18&63]+r[i>>12&63]+r[i>>6&63]+r[63&i]);return a.join(\"\")}n[\"-\".charCodeAt(0)]=62,n[\"_\".charCodeAt(0)]=63},7534:(t,e,r)=>{var n=r(2552),o=r(346);t.exports=function baseIsArguments(t){return o(t)&&\"[object Arguments]\"==n(t)}},7556:(t,e,r)=>{var n=r(1873),o=r(4932),i=r(6449),a=r(4394),s=n?n.prototype:void 0,u=s?s.toString:void 0;t.exports=function baseToString(t){if(\"string\"==typeof t)return t;if(i(t))return o(t,baseToString)+\"\";if(a(t))return u?u.call(t):\"\";var e=t+\"\";return\"0\"==e&&1/t==-1/0?\"-0\":e}},7666:(t,e,r)=>{var n=r(4851),o=r(953);function _extends(){var e;return t.exports=_extends=n?o(e=n).call(e):function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var n in r)({}).hasOwnProperty.call(r,n)&&(t[n]=r[n])}return t},t.exports.__esModule=!0,t.exports.default=t.exports,_extends.apply(null,arguments)}t.exports=_extends,t.exports.__esModule=!0,t.exports.default=t.exports},7670:(t,e,r)=>{var n=r(2651);t.exports=function mapCacheDelete(t){var e=n(this,t).delete(t);return this.size-=e?1:0,e}},7797:(t,e,r)=>{var n=r(4394);t.exports=function toKey(t){if(\"string\"==typeof t||n(t))return t;var e=t+\"\";return\"0\"==e&&1/t==-1/0?\"-0\":e}},7816:(t,e,r)=>{\"use strict\";var n=r(6698),o=r(8011),i=r(2861).Buffer,a=[1518500249,1859775393,-1894007588,-899497514],s=new Array(80);function Sha(){this.init(),this._w=s,o.call(this,64,56)}function rotl30(t){return t<<30|t>>>2}function ft(t,e,r,n){return 0===t?e&r|~e&n:2===t?e&r|e&n|r&n:e^r^n}n(Sha,o),Sha.prototype.init=function(){return this._a=1732584193,this._b=4023233417,this._c=2562383102,this._d=271733878,this._e=3285377520,this},Sha.prototype._update=function(t){for(var e,r=this._w,n=0|this._a,o=0|this._b,i=0|this._c,s=0|this._d,u=0|this._e,c=0;c<16;++c)r[c]=t.readInt32BE(4*c);for(;c<80;++c)r[c]=r[c-3]^r[c-8]^r[c-14]^r[c-16];for(var f=0;f<80;++f){var l=~~(f/20),p=0|((e=n)<<5|e>>>27)+ft(l,o,i,s)+u+r[f]+a[l];u=s,s=i,i=rotl30(o),o=n,n=p}this._a=n+this._a|0,this._b=o+this._b|0,this._c=i+this._c|0,this._d=s+this._d|0,this._e=u+this._e|0},Sha.prototype._hash=function(){var t=i.allocUnsafe(20);return t.writeInt32BE(0|this._a,0),t.writeInt32BE(0|this._b,4),t.writeInt32BE(0|this._c,8),t.writeInt32BE(0|this._d,12),t.writeInt32BE(0|this._e,16),t},t.exports=Sha},7828:(t,e,r)=>{var n=r(9325).Uint8Array;t.exports=n},7978:(t,e,r)=>{var n=r(270),o=r(8156),i=r(631),a=r(8586),s=r(756),u=r(7197),c=r(7797);t.exports=function baseMatchesProperty(t,e){return a(t)&&s(e)?u(c(t),e):function(r){var a=o(r,t);return void 0===a&&a===e?i(r,t):n(e,a,3)}}},8002:t=>{\"use strict\";t.exports=Math.min},8011:(t,e,r)=>{\"use strict\";var n=r(2861).Buffer,o=r(5377);function Hash(t,e){this._block=n.alloc(t),this._finalSize=e,this._blockSize=t,this._len=0}Hash.prototype.update=function(t,e){t=o(t,e||\"utf8\");for(var r=this._block,n=this._blockSize,i=t.length,a=this._len,s=0;s<i;){for(var u=a%n,c=Math.min(i-s,n-u),f=0;f<c;f++)r[u+f]=t[s+f];s+=c,(a+=c)%n==0&&this._update(r)}return this._len+=i,this},Hash.prototype.digest=function(t){var e=this._len%this._blockSize;this._block[e]=128,this._block.fill(0,e+1),e>=this._finalSize&&(this._update(this._block),this._block.fill(0));var r=8*this._len;if(r<=4294967295)this._block.writeUInt32BE(r,this._blockSize-4);else{var n=(4294967295&r)>>>0,o=(r-n)/4294967296;this._block.writeUInt32BE(o,this._blockSize-8),this._block.writeUInt32BE(n,this._blockSize-4)}this._update(this._block);var i=this._hash();return t?i.toString(t):i},Hash.prototype._update=function(){throw new Error(\"_update must be implemented by subclass\")},t.exports=Hash},8068:t=>{\"use strict\";t.exports=SyntaxError},8077:t=>{t.exports=function baseHasIn(t,e){return null!=t&&e in Object(t)}},8096:t=>{t.exports=function baseTimes(t,e){for(var r=-1,n=Array(t);++r<t;)n[r]=e(r);return n}},8156:(t,e,r)=>{var n=r(7422);t.exports=function get(t,e,r){var o=null==t?void 0:n(t,e);return void 0===o?r:o}},8223:(t,e,r)=>{var n=r(6110)(r(9325),\"Map\");t.exports=n},8280:(t,e,r)=>{\"use strict\";var n=r(1907);t.exports=n({}.isPrototypeOf)},8287:(t,e,r)=>{\"use strict\";const n=r(7526),o=r(251),i=\"function\"==typeof Symbol&&\"function\"==typeof Symbol.for?Symbol.for(\"nodejs.util.inspect.custom\"):null;e.Buffer=Buffer,e.SlowBuffer=function SlowBuffer(t){+t!=t&&(t=0);return Buffer.alloc(+t)},e.INSPECT_MAX_BYTES=50;const a=2147483647;function createBuffer(t){if(t>a)throw new RangeError('The value \"'+t+'\" is invalid for option \"size\"');const e=new Uint8Array(t);return Object.setPrototypeOf(e,Buffer.prototype),e}function Buffer(t,e,r){if(\"number\"==typeof t){if(\"string\"==typeof e)throw new TypeError('The \"string\" argument must be of type string. Received type number');return allocUnsafe(t)}return from(t,e,r)}function from(t,e,r){if(\"string\"==typeof t)return function fromString(t,e){\"string\"==typeof e&&\"\"!==e||(e=\"utf8\");if(!Buffer.isEncoding(e))throw new TypeError(\"Unknown encoding: \"+e);const r=0|byteLength(t,e);let n=createBuffer(r);const o=n.write(t,e);o!==r&&(n=n.slice(0,o));return n}(t,e);if(ArrayBuffer.isView(t))return function fromArrayView(t){if(isInstance(t,Uint8Array)){const e=new Uint8Array(t);return fromArrayBuffer(e.buffer,e.byteOffset,e.byteLength)}return fromArrayLike(t)}(t);if(null==t)throw new TypeError(\"The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type \"+typeof t);if(isInstance(t,ArrayBuffer)||t&&isInstance(t.buffer,ArrayBuffer))return fromArrayBuffer(t,e,r);if(\"undefined\"!=typeof SharedArrayBuffer&&(isInstance(t,SharedArrayBuffer)||t&&isInstance(t.buffer,SharedArrayBuffer)))return fromArrayBuffer(t,e,r);if(\"number\"==typeof t)throw new TypeError('The \"value\" argument must not be of type number. Received type number');const n=t.valueOf&&t.valueOf();if(null!=n&&n!==t)return Buffer.from(n,e,r);const o=function fromObject(t){if(Buffer.isBuffer(t)){const e=0|checked(t.length),r=createBuffer(e);return 0===r.length||t.copy(r,0,0,e),r}if(void 0!==t.length)return\"number\"!=typeof t.length||numberIsNaN(t.length)?createBuffer(0):fromArrayLike(t);if(\"Buffer\"===t.type&&Array.isArray(t.data))return fromArrayLike(t.data)}(t);if(o)return o;if(\"undefined\"!=typeof Symbol&&null!=Symbol.toPrimitive&&\"function\"==typeof t[Symbol.toPrimitive])return Buffer.from(t[Symbol.toPrimitive](\"string\"),e,r);throw new TypeError(\"The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type \"+typeof t)}function assertSize(t){if(\"number\"!=typeof t)throw new TypeError('\"size\" argument must be of type number');if(t<0)throw new RangeError('The value \"'+t+'\" is invalid for option \"size\"')}function allocUnsafe(t){return assertSize(t),createBuffer(t<0?0:0|checked(t))}function fromArrayLike(t){const e=t.length<0?0:0|checked(t.length),r=createBuffer(e);for(let n=0;n<e;n+=1)r[n]=255&t[n];return r}function fromArrayBuffer(t,e,r){if(e<0||t.byteLength<e)throw new RangeError('\"offset\" is outside of buffer bounds');if(t.byteLength<e+(r||0))throw new RangeError('\"length\" is outside of buffer bounds');let n;return n=void 0===e&&void 0===r?new Uint8Array(t):void 0===r?new Uint8Array(t,e):new Uint8Array(t,e,r),Object.setPrototypeOf(n,Buffer.prototype),n}function checked(t){if(t>=a)throw new RangeError(\"Attempt to allocate Buffer larger than maximum size: 0x\"+a.toString(16)+\" bytes\");return 0|t}function byteLength(t,e){if(Buffer.isBuffer(t))return t.length;if(ArrayBuffer.isView(t)||isInstance(t,ArrayBuffer))return t.byteLength;if(\"string\"!=typeof t)throw new TypeError('The \"string\" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof t);const r=t.length,n=arguments.length>2&&!0===arguments[2];if(!n&&0===r)return 0;let o=!1;for(;;)switch(e){case\"ascii\":case\"latin1\":case\"binary\":return r;case\"utf8\":case\"utf-8\":return utf8ToBytes(t).length;case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return 2*r;case\"hex\":return r>>>1;case\"base64\":return base64ToBytes(t).length;default:if(o)return n?-1:utf8ToBytes(t).length;e=(\"\"+e).toLowerCase(),o=!0}}function slowToString(t,e,r){let n=!1;if((void 0===e||e<0)&&(e=0),e>this.length)return\"\";if((void 0===r||r>this.length)&&(r=this.length),r<=0)return\"\";if((r>>>=0)<=(e>>>=0))return\"\";for(t||(t=\"utf8\");;)switch(t){case\"hex\":return hexSlice(this,e,r);case\"utf8\":case\"utf-8\":return utf8Slice(this,e,r);case\"ascii\":return asciiSlice(this,e,r);case\"latin1\":case\"binary\":return latin1Slice(this,e,r);case\"base64\":return base64Slice(this,e,r);case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return utf16leSlice(this,e,r);default:if(n)throw new TypeError(\"Unknown encoding: \"+t);t=(t+\"\").toLowerCase(),n=!0}}function swap(t,e,r){const n=t[e];t[e]=t[r],t[r]=n}function bidirectionalIndexOf(t,e,r,n,o){if(0===t.length)return-1;if(\"string\"==typeof r?(n=r,r=0):r>2147483647?r=2147483647:r<-2147483648&&(r=-2147483648),numberIsNaN(r=+r)&&(r=o?0:t.length-1),r<0&&(r=t.length+r),r>=t.length){if(o)return-1;r=t.length-1}else if(r<0){if(!o)return-1;r=0}if(\"string\"==typeof e&&(e=Buffer.from(e,n)),Buffer.isBuffer(e))return 0===e.length?-1:arrayIndexOf(t,e,r,n,o);if(\"number\"==typeof e)return e&=255,\"function\"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(t,e,r):Uint8Array.prototype.lastIndexOf.call(t,e,r):arrayIndexOf(t,[e],r,n,o);throw new TypeError(\"val must be string, number or Buffer\")}function arrayIndexOf(t,e,r,n,o){let i,a=1,s=t.length,u=e.length;if(void 0!==n&&(\"ucs2\"===(n=String(n).toLowerCase())||\"ucs-2\"===n||\"utf16le\"===n||\"utf-16le\"===n)){if(t.length<2||e.length<2)return-1;a=2,s/=2,u/=2,r/=2}function read(t,e){return 1===a?t[e]:t.readUInt16BE(e*a)}if(o){let n=-1;for(i=r;i<s;i++)if(read(t,i)===read(e,-1===n?0:i-n)){if(-1===n&&(n=i),i-n+1===u)return n*a}else-1!==n&&(i-=i-n),n=-1}else for(r+u>s&&(r=s-u),i=r;i>=0;i--){let r=!0;for(let n=0;n<u;n++)if(read(t,i+n)!==read(e,n)){r=!1;break}if(r)return i}return-1}function hexWrite(t,e,r,n){r=Number(r)||0;const o=t.length-r;n?(n=Number(n))>o&&(n=o):n=o;const i=e.length;let a;for(n>i/2&&(n=i/2),a=0;a<n;++a){const n=parseInt(e.substr(2*a,2),16);if(numberIsNaN(n))return a;t[r+a]=n}return a}function utf8Write(t,e,r,n){return blitBuffer(utf8ToBytes(e,t.length-r),t,r,n)}function asciiWrite(t,e,r,n){return blitBuffer(function asciiToBytes(t){const e=[];for(let r=0;r<t.length;++r)e.push(255&t.charCodeAt(r));return e}(e),t,r,n)}function base64Write(t,e,r,n){return blitBuffer(base64ToBytes(e),t,r,n)}function ucs2Write(t,e,r,n){return blitBuffer(function utf16leToBytes(t,e){let r,n,o;const i=[];for(let a=0;a<t.length&&!((e-=2)<0);++a)r=t.charCodeAt(a),n=r>>8,o=r%256,i.push(o),i.push(n);return i}(e,t.length-r),t,r,n)}function base64Slice(t,e,r){return 0===e&&r===t.length?n.fromByteArray(t):n.fromByteArray(t.slice(e,r))}function utf8Slice(t,e,r){r=Math.min(t.length,r);const n=[];let o=e;for(;o<r;){const e=t[o];let i=null,a=e>239?4:e>223?3:e>191?2:1;if(o+a<=r){let r,n,s,u;switch(a){case 1:e<128&&(i=e);break;case 2:r=t[o+1],128==(192&r)&&(u=(31&e)<<6|63&r,u>127&&(i=u));break;case 3:r=t[o+1],n=t[o+2],128==(192&r)&&128==(192&n)&&(u=(15&e)<<12|(63&r)<<6|63&n,u>2047&&(u<55296||u>57343)&&(i=u));break;case 4:r=t[o+1],n=t[o+2],s=t[o+3],128==(192&r)&&128==(192&n)&&128==(192&s)&&(u=(15&e)<<18|(63&r)<<12|(63&n)<<6|63&s,u>65535&&u<1114112&&(i=u))}}null===i?(i=65533,a=1):i>65535&&(i-=65536,n.push(i>>>10&1023|55296),i=56320|1023&i),n.push(i),o+=a}return function decodeCodePointsArray(t){const e=t.length;if(e<=s)return String.fromCharCode.apply(String,t);let r=\"\",n=0;for(;n<e;)r+=String.fromCharCode.apply(String,t.slice(n,n+=s));return r}(n)}e.kMaxLength=a,Buffer.TYPED_ARRAY_SUPPORT=function typedArraySupport(){try{const t=new Uint8Array(1),e={foo:function(){return 42}};return Object.setPrototypeOf(e,Uint8Array.prototype),Object.setPrototypeOf(t,e),42===t.foo()}catch(t){return!1}}(),Buffer.TYPED_ARRAY_SUPPORT||\"undefined\"==typeof console||\"function\"!=typeof console.error||console.error(\"This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support.\"),Object.defineProperty(Buffer.prototype,\"parent\",{enumerable:!0,get:function(){if(Buffer.isBuffer(this))return this.buffer}}),Object.defineProperty(Buffer.prototype,\"offset\",{enumerable:!0,get:function(){if(Buffer.isBuffer(this))return this.byteOffset}}),Buffer.poolSize=8192,Buffer.from=function(t,e,r){return from(t,e,r)},Object.setPrototypeOf(Buffer.prototype,Uint8Array.prototype),Object.setPrototypeOf(Buffer,Uint8Array),Buffer.alloc=function(t,e,r){return function alloc(t,e,r){return assertSize(t),t<=0?createBuffer(t):void 0!==e?\"string\"==typeof r?createBuffer(t).fill(e,r):createBuffer(t).fill(e):createBuffer(t)}(t,e,r)},Buffer.allocUnsafe=function(t){return allocUnsafe(t)},Buffer.allocUnsafeSlow=function(t){return allocUnsafe(t)},Buffer.isBuffer=function isBuffer(t){return null!=t&&!0===t._isBuffer&&t!==Buffer.prototype},Buffer.compare=function compare(t,e){if(isInstance(t,Uint8Array)&&(t=Buffer.from(t,t.offset,t.byteLength)),isInstance(e,Uint8Array)&&(e=Buffer.from(e,e.offset,e.byteLength)),!Buffer.isBuffer(t)||!Buffer.isBuffer(e))throw new TypeError('The \"buf1\", \"buf2\" arguments must be one of type Buffer or Uint8Array');if(t===e)return 0;let r=t.length,n=e.length;for(let o=0,i=Math.min(r,n);o<i;++o)if(t[o]!==e[o]){r=t[o],n=e[o];break}return r<n?-1:n<r?1:0},Buffer.isEncoding=function isEncoding(t){switch(String(t).toLowerCase()){case\"hex\":case\"utf8\":case\"utf-8\":case\"ascii\":case\"latin1\":case\"binary\":case\"base64\":case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return!0;default:return!1}},Buffer.concat=function concat(t,e){if(!Array.isArray(t))throw new TypeError('\"list\" argument must be an Array of Buffers');if(0===t.length)return Buffer.alloc(0);let r;if(void 0===e)for(e=0,r=0;r<t.length;++r)e+=t[r].length;const n=Buffer.allocUnsafe(e);let o=0;for(r=0;r<t.length;++r){let e=t[r];if(isInstance(e,Uint8Array))o+e.length>n.length?(Buffer.isBuffer(e)||(e=Buffer.from(e)),e.copy(n,o)):Uint8Array.prototype.set.call(n,e,o);else{if(!Buffer.isBuffer(e))throw new TypeError('\"list\" argument must be an Array of Buffers');e.copy(n,o)}o+=e.length}return n},Buffer.byteLength=byteLength,Buffer.prototype._isBuffer=!0,Buffer.prototype.swap16=function swap16(){const t=this.length;if(t%2!=0)throw new RangeError(\"Buffer size must be a multiple of 16-bits\");for(let e=0;e<t;e+=2)swap(this,e,e+1);return this},Buffer.prototype.swap32=function swap32(){const t=this.length;if(t%4!=0)throw new RangeError(\"Buffer size must be a multiple of 32-bits\");for(let e=0;e<t;e+=4)swap(this,e,e+3),swap(this,e+1,e+2);return this},Buffer.prototype.swap64=function swap64(){const t=this.length;if(t%8!=0)throw new RangeError(\"Buffer size must be a multiple of 64-bits\");for(let e=0;e<t;e+=8)swap(this,e,e+7),swap(this,e+1,e+6),swap(this,e+2,e+5),swap(this,e+3,e+4);return this},Buffer.prototype.toString=function toString(){const t=this.length;return 0===t?\"\":0===arguments.length?utf8Slice(this,0,t):slowToString.apply(this,arguments)},Buffer.prototype.toLocaleString=Buffer.prototype.toString,Buffer.prototype.equals=function equals(t){if(!Buffer.isBuffer(t))throw new TypeError(\"Argument must be a Buffer\");return this===t||0===Buffer.compare(this,t)},Buffer.prototype.inspect=function inspect(){let t=\"\";const r=e.INSPECT_MAX_BYTES;return t=this.toString(\"hex\",0,r).replace(/(.{2})/g,\"$1 \").trim(),this.length>r&&(t+=\" ... \"),\"<Buffer \"+t+\">\"},i&&(Buffer.prototype[i]=Buffer.prototype.inspect),Buffer.prototype.compare=function compare(t,e,r,n,o){if(isInstance(t,Uint8Array)&&(t=Buffer.from(t,t.offset,t.byteLength)),!Buffer.isBuffer(t))throw new TypeError('The \"target\" argument must be one of type Buffer or Uint8Array. Received type '+typeof t);if(void 0===e&&(e=0),void 0===r&&(r=t?t.length:0),void 0===n&&(n=0),void 0===o&&(o=this.length),e<0||r>t.length||n<0||o>this.length)throw new RangeError(\"out of range index\");if(n>=o&&e>=r)return 0;if(n>=o)return-1;if(e>=r)return 1;if(this===t)return 0;let i=(o>>>=0)-(n>>>=0),a=(r>>>=0)-(e>>>=0);const s=Math.min(i,a),u=this.slice(n,o),c=t.slice(e,r);for(let t=0;t<s;++t)if(u[t]!==c[t]){i=u[t],a=c[t];break}return i<a?-1:a<i?1:0},Buffer.prototype.includes=function includes(t,e,r){return-1!==this.indexOf(t,e,r)},Buffer.prototype.indexOf=function indexOf(t,e,r){return bidirectionalIndexOf(this,t,e,r,!0)},Buffer.prototype.lastIndexOf=function lastIndexOf(t,e,r){return bidirectionalIndexOf(this,t,e,r,!1)},Buffer.prototype.write=function write(t,e,r,n){if(void 0===e)n=\"utf8\",r=this.length,e=0;else if(void 0===r&&\"string\"==typeof e)n=e,r=this.length,e=0;else{if(!isFinite(e))throw new Error(\"Buffer.write(string, encoding, offset[, length]) is no longer supported\");e>>>=0,isFinite(r)?(r>>>=0,void 0===n&&(n=\"utf8\")):(n=r,r=void 0)}const o=this.length-e;if((void 0===r||r>o)&&(r=o),t.length>0&&(r<0||e<0)||e>this.length)throw new RangeError(\"Attempt to write outside buffer bounds\");n||(n=\"utf8\");let i=!1;for(;;)switch(n){case\"hex\":return hexWrite(this,t,e,r);case\"utf8\":case\"utf-8\":return utf8Write(this,t,e,r);case\"ascii\":case\"latin1\":case\"binary\":return asciiWrite(this,t,e,r);case\"base64\":return base64Write(this,t,e,r);case\"ucs2\":case\"ucs-2\":case\"utf16le\":case\"utf-16le\":return ucs2Write(this,t,e,r);default:if(i)throw new TypeError(\"Unknown encoding: \"+n);n=(\"\"+n).toLowerCase(),i=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:\"Buffer\",data:Array.prototype.slice.call(this._arr||this,0)}};const s=4096;function asciiSlice(t,e,r){let n=\"\";r=Math.min(t.length,r);for(let o=e;o<r;++o)n+=String.fromCharCode(127&t[o]);return n}function latin1Slice(t,e,r){let n=\"\";r=Math.min(t.length,r);for(let o=e;o<r;++o)n+=String.fromCharCode(t[o]);return n}function hexSlice(t,e,r){const n=t.length;(!e||e<0)&&(e=0),(!r||r<0||r>n)&&(r=n);let o=\"\";for(let n=e;n<r;++n)o+=f[t[n]];return o}function utf16leSlice(t,e,r){const n=t.slice(e,r);let o=\"\";for(let t=0;t<n.length-1;t+=2)o+=String.fromCharCode(n[t]+256*n[t+1]);return o}function checkOffset(t,e,r){if(t%1!=0||t<0)throw new RangeError(\"offset is not uint\");if(t+e>r)throw new RangeError(\"Trying to access beyond buffer length\")}function checkInt(t,e,r,n,o,i){if(!Buffer.isBuffer(t))throw new TypeError('\"buffer\" argument must be a Buffer instance');if(e>o||e<i)throw new RangeError('\"value\" argument is out of bounds');if(r+n>t.length)throw new RangeError(\"Index out of range\")}function wrtBigUInt64LE(t,e,r,n,o){checkIntBI(e,n,o,t,r,7);let i=Number(e&BigInt(4294967295));t[r++]=i,i>>=8,t[r++]=i,i>>=8,t[r++]=i,i>>=8,t[r++]=i;let a=Number(e>>BigInt(32)&BigInt(4294967295));return t[r++]=a,a>>=8,t[r++]=a,a>>=8,t[r++]=a,a>>=8,t[r++]=a,r}function wrtBigUInt64BE(t,e,r,n,o){checkIntBI(e,n,o,t,r,7);let i=Number(e&BigInt(4294967295));t[r+7]=i,i>>=8,t[r+6]=i,i>>=8,t[r+5]=i,i>>=8,t[r+4]=i;let a=Number(e>>BigInt(32)&BigInt(4294967295));return t[r+3]=a,a>>=8,t[r+2]=a,a>>=8,t[r+1]=a,a>>=8,t[r]=a,r+8}function checkIEEE754(t,e,r,n,o,i){if(r+n>t.length)throw new RangeError(\"Index out of range\");if(r<0)throw new RangeError(\"Index out of range\")}function writeFloat(t,e,r,n,i){return e=+e,r>>>=0,i||checkIEEE754(t,0,r,4),o.write(t,e,r,n,23,4),r+4}function writeDouble(t,e,r,n,i){return e=+e,r>>>=0,i||checkIEEE754(t,0,r,8),o.write(t,e,r,n,52,8),r+8}Buffer.prototype.slice=function slice(t,e){const r=this.length;(t=~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),(e=void 0===e?r:~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),e<t&&(e=t);const n=this.subarray(t,e);return Object.setPrototypeOf(n,Buffer.prototype),n},Buffer.prototype.readUintLE=Buffer.prototype.readUIntLE=function readUIntLE(t,e,r){t>>>=0,e>>>=0,r||checkOffset(t,e,this.length);let n=this[t],o=1,i=0;for(;++i<e&&(o*=256);)n+=this[t+i]*o;return n},Buffer.prototype.readUintBE=Buffer.prototype.readUIntBE=function readUIntBE(t,e,r){t>>>=0,e>>>=0,r||checkOffset(t,e,this.length);let n=this[t+--e],o=1;for(;e>0&&(o*=256);)n+=this[t+--e]*o;return n},Buffer.prototype.readUint8=Buffer.prototype.readUInt8=function readUInt8(t,e){return t>>>=0,e||checkOffset(t,1,this.length),this[t]},Buffer.prototype.readUint16LE=Buffer.prototype.readUInt16LE=function readUInt16LE(t,e){return t>>>=0,e||checkOffset(t,2,this.length),this[t]|this[t+1]<<8},Buffer.prototype.readUint16BE=Buffer.prototype.readUInt16BE=function readUInt16BE(t,e){return t>>>=0,e||checkOffset(t,2,this.length),this[t]<<8|this[t+1]},Buffer.prototype.readUint32LE=Buffer.prototype.readUInt32LE=function readUInt32LE(t,e){return t>>>=0,e||checkOffset(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},Buffer.prototype.readUint32BE=Buffer.prototype.readUInt32BE=function readUInt32BE(t,e){return t>>>=0,e||checkOffset(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},Buffer.prototype.readBigUInt64LE=defineBigIntMethod((function readBigUInt64LE(t){validateNumber(t>>>=0,\"offset\");const e=this[t],r=this[t+7];void 0!==e&&void 0!==r||boundsError(t,this.length-8);const n=e+256*this[++t]+65536*this[++t]+this[++t]*2**24,o=this[++t]+256*this[++t]+65536*this[++t]+r*2**24;return BigInt(n)+(BigInt(o)<<BigInt(32))})),Buffer.prototype.readBigUInt64BE=defineBigIntMethod((function readBigUInt64BE(t){validateNumber(t>>>=0,\"offset\");const e=this[t],r=this[t+7];void 0!==e&&void 0!==r||boundsError(t,this.length-8);const n=e*2**24+65536*this[++t]+256*this[++t]+this[++t],o=this[++t]*2**24+65536*this[++t]+256*this[++t]+r;return(BigInt(n)<<BigInt(32))+BigInt(o)})),Buffer.prototype.readIntLE=function readIntLE(t,e,r){t>>>=0,e>>>=0,r||checkOffset(t,e,this.length);let n=this[t],o=1,i=0;for(;++i<e&&(o*=256);)n+=this[t+i]*o;return o*=128,n>=o&&(n-=Math.pow(2,8*e)),n},Buffer.prototype.readIntBE=function readIntBE(t,e,r){t>>>=0,e>>>=0,r||checkOffset(t,e,this.length);let n=e,o=1,i=this[t+--n];for(;n>0&&(o*=256);)i+=this[t+--n]*o;return o*=128,i>=o&&(i-=Math.pow(2,8*e)),i},Buffer.prototype.readInt8=function readInt8(t,e){return t>>>=0,e||checkOffset(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},Buffer.prototype.readInt16LE=function readInt16LE(t,e){t>>>=0,e||checkOffset(t,2,this.length);const r=this[t]|this[t+1]<<8;return 32768&r?4294901760|r:r},Buffer.prototype.readInt16BE=function readInt16BE(t,e){t>>>=0,e||checkOffset(t,2,this.length);const r=this[t+1]|this[t]<<8;return 32768&r?4294901760|r:r},Buffer.prototype.readInt32LE=function readInt32LE(t,e){return t>>>=0,e||checkOffset(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(t,e){return t>>>=0,e||checkOffset(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},Buffer.prototype.readBigInt64LE=defineBigIntMethod((function readBigInt64LE(t){validateNumber(t>>>=0,\"offset\");const e=this[t],r=this[t+7];void 0!==e&&void 0!==r||boundsError(t,this.length-8);const n=this[t+4]+256*this[t+5]+65536*this[t+6]+(r<<24);return(BigInt(n)<<BigInt(32))+BigInt(e+256*this[++t]+65536*this[++t]+this[++t]*2**24)})),Buffer.prototype.readBigInt64BE=defineBigIntMethod((function readBigInt64BE(t){validateNumber(t>>>=0,\"offset\");const e=this[t],r=this[t+7];void 0!==e&&void 0!==r||boundsError(t,this.length-8);const n=(e<<24)+65536*this[++t]+256*this[++t]+this[++t];return(BigInt(n)<<BigInt(32))+BigInt(this[++t]*2**24+65536*this[++t]+256*this[++t]+r)})),Buffer.prototype.readFloatLE=function readFloatLE(t,e){return t>>>=0,e||checkOffset(t,4,this.length),o.read(this,t,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(t,e){return t>>>=0,e||checkOffset(t,4,this.length),o.read(this,t,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(t,e){return t>>>=0,e||checkOffset(t,8,this.length),o.read(this,t,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(t,e){return t>>>=0,e||checkOffset(t,8,this.length),o.read(this,t,!1,52,8)},Buffer.prototype.writeUintLE=Buffer.prototype.writeUIntLE=function writeUIntLE(t,e,r,n){if(t=+t,e>>>=0,r>>>=0,!n){checkInt(this,t,e,r,Math.pow(2,8*r)-1,0)}let o=1,i=0;for(this[e]=255&t;++i<r&&(o*=256);)this[e+i]=t/o&255;return e+r},Buffer.prototype.writeUintBE=Buffer.prototype.writeUIntBE=function writeUIntBE(t,e,r,n){if(t=+t,e>>>=0,r>>>=0,!n){checkInt(this,t,e,r,Math.pow(2,8*r)-1,0)}let o=r-1,i=1;for(this[e+o]=255&t;--o>=0&&(i*=256);)this[e+o]=t/i&255;return e+r},Buffer.prototype.writeUint8=Buffer.prototype.writeUInt8=function writeUInt8(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,1,255,0),this[e]=255&t,e+1},Buffer.prototype.writeUint16LE=Buffer.prototype.writeUInt16LE=function writeUInt16LE(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,2,65535,0),this[e]=255&t,this[e+1]=t>>>8,e+2},Buffer.prototype.writeUint16BE=Buffer.prototype.writeUInt16BE=function writeUInt16BE(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,2,65535,0),this[e]=t>>>8,this[e+1]=255&t,e+2},Buffer.prototype.writeUint32LE=Buffer.prototype.writeUInt32LE=function writeUInt32LE(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,4,4294967295,0),this[e+3]=t>>>24,this[e+2]=t>>>16,this[e+1]=t>>>8,this[e]=255&t,e+4},Buffer.prototype.writeUint32BE=Buffer.prototype.writeUInt32BE=function writeUInt32BE(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,4,4294967295,0),this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t,e+4},Buffer.prototype.writeBigUInt64LE=defineBigIntMethod((function writeBigUInt64LE(t,e=0){return wrtBigUInt64LE(this,t,e,BigInt(0),BigInt(\"0xffffffffffffffff\"))})),Buffer.prototype.writeBigUInt64BE=defineBigIntMethod((function writeBigUInt64BE(t,e=0){return wrtBigUInt64BE(this,t,e,BigInt(0),BigInt(\"0xffffffffffffffff\"))})),Buffer.prototype.writeIntLE=function writeIntLE(t,e,r,n){if(t=+t,e>>>=0,!n){const n=Math.pow(2,8*r-1);checkInt(this,t,e,r,n-1,-n)}let o=0,i=1,a=0;for(this[e]=255&t;++o<r&&(i*=256);)t<0&&0===a&&0!==this[e+o-1]&&(a=1),this[e+o]=(t/i|0)-a&255;return e+r},Buffer.prototype.writeIntBE=function writeIntBE(t,e,r,n){if(t=+t,e>>>=0,!n){const n=Math.pow(2,8*r-1);checkInt(this,t,e,r,n-1,-n)}let o=r-1,i=1,a=0;for(this[e+o]=255&t;--o>=0&&(i*=256);)t<0&&0===a&&0!==this[e+o+1]&&(a=1),this[e+o]=(t/i|0)-a&255;return e+r},Buffer.prototype.writeInt8=function writeInt8(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,1,127,-128),t<0&&(t=255+t+1),this[e]=255&t,e+1},Buffer.prototype.writeInt16LE=function writeInt16LE(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,2,32767,-32768),this[e]=255&t,this[e+1]=t>>>8,e+2},Buffer.prototype.writeInt16BE=function writeInt16BE(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,2,32767,-32768),this[e]=t>>>8,this[e+1]=255&t,e+2},Buffer.prototype.writeInt32LE=function writeInt32LE(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,4,2147483647,-2147483648),this[e]=255&t,this[e+1]=t>>>8,this[e+2]=t>>>16,this[e+3]=t>>>24,e+4},Buffer.prototype.writeInt32BE=function writeInt32BE(t,e,r){return t=+t,e>>>=0,r||checkInt(this,t,e,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t,e+4},Buffer.prototype.writeBigInt64LE=defineBigIntMethod((function writeBigInt64LE(t,e=0){return wrtBigUInt64LE(this,t,e,-BigInt(\"0x8000000000000000\"),BigInt(\"0x7fffffffffffffff\"))})),Buffer.prototype.writeBigInt64BE=defineBigIntMethod((function writeBigInt64BE(t,e=0){return wrtBigUInt64BE(this,t,e,-BigInt(\"0x8000000000000000\"),BigInt(\"0x7fffffffffffffff\"))})),Buffer.prototype.writeFloatLE=function writeFloatLE(t,e,r){return writeFloat(this,t,e,!0,r)},Buffer.prototype.writeFloatBE=function writeFloatBE(t,e,r){return writeFloat(this,t,e,!1,r)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(t,e,r){return writeDouble(this,t,e,!0,r)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(t,e,r){return writeDouble(this,t,e,!1,r)},Buffer.prototype.copy=function copy(t,e,r,n){if(!Buffer.isBuffer(t))throw new TypeError(\"argument should be a Buffer\");if(r||(r=0),n||0===n||(n=this.length),e>=t.length&&(e=t.length),e||(e=0),n>0&&n<r&&(n=r),n===r)return 0;if(0===t.length||0===this.length)return 0;if(e<0)throw new RangeError(\"targetStart out of bounds\");if(r<0||r>=this.length)throw new RangeError(\"Index out of range\");if(n<0)throw new RangeError(\"sourceEnd out of bounds\");n>this.length&&(n=this.length),t.length-e<n-r&&(n=t.length-e+r);const o=n-r;return this===t&&\"function\"==typeof Uint8Array.prototype.copyWithin?this.copyWithin(e,r,n):Uint8Array.prototype.set.call(t,this.subarray(r,n),e),o},Buffer.prototype.fill=function fill(t,e,r,n){if(\"string\"==typeof t){if(\"string\"==typeof e?(n=e,e=0,r=this.length):\"string\"==typeof r&&(n=r,r=this.length),void 0!==n&&\"string\"!=typeof n)throw new TypeError(\"encoding must be a string\");if(\"string\"==typeof n&&!Buffer.isEncoding(n))throw new TypeError(\"Unknown encoding: \"+n);if(1===t.length){const e=t.charCodeAt(0);(\"utf8\"===n&&e<128||\"latin1\"===n)&&(t=e)}}else\"number\"==typeof t?t&=255:\"boolean\"==typeof t&&(t=Number(t));if(e<0||this.length<e||this.length<r)throw new RangeError(\"Out of range index\");if(r<=e)return this;let o;if(e>>>=0,r=void 0===r?this.length:r>>>0,t||(t=0),\"number\"==typeof t)for(o=e;o<r;++o)this[o]=t;else{const i=Buffer.isBuffer(t)?t:Buffer.from(t,n),a=i.length;if(0===a)throw new TypeError('The value \"'+t+'\" is invalid for argument \"value\"');for(o=0;o<r-e;++o)this[o+e]=i[o%a]}return this};const u={};function E(t,e,r){u[t]=class NodeError extends r{constructor(){super(),Object.defineProperty(this,\"message\",{value:e.apply(this,arguments),writable:!0,configurable:!0}),this.name=`${this.name} [${t}]`,this.stack,delete this.name}get code(){return t}set code(t){Object.defineProperty(this,\"code\",{configurable:!0,enumerable:!0,value:t,writable:!0})}toString(){return`${this.name} [${t}]: ${this.message}`}}}function addNumericalSeparator(t){let e=\"\",r=t.length;const n=\"-\"===t[0]?1:0;for(;r>=n+4;r-=3)e=`_${t.slice(r-3,r)}${e}`;return`${t.slice(0,r)}${e}`}function checkIntBI(t,e,r,n,o,i){if(t>r||t<e){const n=\"bigint\"==typeof e?\"n\":\"\";let o;throw o=i>3?0===e||e===BigInt(0)?`>= 0${n} and < 2${n} ** ${8*(i+1)}${n}`:`>= -(2${n} ** ${8*(i+1)-1}${n}) and < 2 ** ${8*(i+1)-1}${n}`:`>= ${e}${n} and <= ${r}${n}`,new u.ERR_OUT_OF_RANGE(\"value\",o,t)}!function checkBounds(t,e,r){validateNumber(e,\"offset\"),void 0!==t[e]&&void 0!==t[e+r]||boundsError(e,t.length-(r+1))}(n,o,i)}function validateNumber(t,e){if(\"number\"!=typeof t)throw new u.ERR_INVALID_ARG_TYPE(e,\"number\",t)}function boundsError(t,e,r){if(Math.floor(t)!==t)throw validateNumber(t,r),new u.ERR_OUT_OF_RANGE(r||\"offset\",\"an integer\",t);if(e<0)throw new u.ERR_BUFFER_OUT_OF_BOUNDS;throw new u.ERR_OUT_OF_RANGE(r||\"offset\",`>= ${r?1:0} and <= ${e}`,t)}E(\"ERR_BUFFER_OUT_OF_BOUNDS\",(function(t){return t?`${t} is outside of buffer bounds`:\"Attempt to access memory outside buffer bounds\"}),RangeError),E(\"ERR_INVALID_ARG_TYPE\",(function(t,e){return`The \"${t}\" argument must be of type number. Received type ${typeof e}`}),TypeError),E(\"ERR_OUT_OF_RANGE\",(function(t,e,r){let n=`The value of \"${t}\" is out of range.`,o=r;return Number.isInteger(r)&&Math.abs(r)>2**32?o=addNumericalSeparator(String(r)):\"bigint\"==typeof r&&(o=String(r),(r>BigInt(2)**BigInt(32)||r<-(BigInt(2)**BigInt(32)))&&(o=addNumericalSeparator(o)),o+=\"n\"),n+=` It must be ${e}. Received ${o}`,n}),RangeError);const c=/[^+/0-9A-Za-z-_]/g;function utf8ToBytes(t,e){let r;e=e||1/0;const n=t.length;let o=null;const i=[];for(let a=0;a<n;++a){if(r=t.charCodeAt(a),r>55295&&r<57344){if(!o){if(r>56319){(e-=3)>-1&&i.push(239,191,189);continue}if(a+1===n){(e-=3)>-1&&i.push(239,191,189);continue}o=r;continue}if(r<56320){(e-=3)>-1&&i.push(239,191,189),o=r;continue}r=65536+(o-55296<<10|r-56320)}else o&&(e-=3)>-1&&i.push(239,191,189);if(o=null,r<128){if((e-=1)<0)break;i.push(r)}else if(r<2048){if((e-=2)<0)break;i.push(r>>6|192,63&r|128)}else if(r<65536){if((e-=3)<0)break;i.push(r>>12|224,r>>6&63|128,63&r|128)}else{if(!(r<1114112))throw new Error(\"Invalid code point\");if((e-=4)<0)break;i.push(r>>18|240,r>>12&63|128,r>>6&63|128,63&r|128)}}return i}function base64ToBytes(t){return n.toByteArray(function base64clean(t){if((t=(t=t.split(\"=\")[0]).trim().replace(c,\"\")).length<2)return\"\";for(;t.length%4!=0;)t+=\"=\";return t}(t))}function blitBuffer(t,e,r,n){let o;for(o=0;o<n&&!(o+r>=e.length||o>=t.length);++o)e[o+r]=t[o];return o}function isInstance(t,e){return t instanceof e||null!=t&&null!=t.constructor&&null!=t.constructor.name&&t.constructor.name===e.name}function numberIsNaN(t){return t!=t}const f=function(){const t=\"0123456789abcdef\",e=new Array(256);for(let r=0;r<16;++r){const n=16*r;for(let o=0;o<16;++o)e[n+o]=t[r]+t[o]}return e}();function defineBigIntMethod(t){return\"undefined\"==typeof BigInt?BufferBigIntNotDefined:t}function BufferBigIntNotDefined(){throw new Error(\"BigInt not supported\")}},8303:(t,e,r)=>{var n=r(6110)(r(9325),\"WeakMap\");t.exports=n},8311:(t,e,r)=>{\"use strict\";var n=r(2361),o=r(2159),i=r(1505),a=n(n.bind);t.exports=function(t,e){return o(t),void 0===e?t:i?a(t,e):function(){return t.apply(e,arguments)}}},8329:(t,e,r)=>{var n=r(4894);t.exports=function createBaseEach(t,e){return function(r,o){if(null==r)return r;if(!n(r))return t(r,o);for(var i=r.length,a=e?i:-1,s=Object(r);(e?a--:++a<i)&&!1!==o(s[a],a,s););return r}}},8530:t=>{\"use strict\";t.exports={}},8586:(t,e,r)=>{var n=r(6449),o=r(4394),i=/\\.|\\[(?:[^[\\]]*|([\"'])(?:(?!\\1)[^\\\\]|\\\\.)*?\\1)\\]/,a=/^\\w*$/;t.exports=function isKey(t,e){if(n(t))return!1;var r=typeof t;return!(\"number\"!=r&&\"symbol\"!=r&&\"boolean\"!=r&&null!=t&&!o(t))||(a.test(t)||!i.test(t)||null!=e&&t in Object(e))}},8648:t=>{\"use strict\";t.exports=\"undefined\"!=typeof Reflect&&Reflect.getPrototypeOf||null},8655:(t,e,r)=>{var n=r(6025);t.exports=function listCacheHas(t){return n(this.__data__,t)>-1}},8661:(t,e,r)=>{\"use strict\";var n=r(9447),o=r(8828);t.exports=n&&o((function(){return 42!==Object.defineProperty((function(){}),\"prototype\",{value:42,writable:!1}).prototype}))},8754:(t,e,r)=>{var n=r(5160);t.exports=function castSlice(t,e,r){var o=t.length;return r=void 0===r?o:r,!e&&r>=o?t:n(t,e,r)}},8828:t=>{\"use strict\";t.exports=function(t){try{return!!t()}catch(t){return!0}}},8859:(t,e,r)=>{var n=r(3661),o=r(1380),i=r(1459);function SetCache(t){var e=-1,r=null==t?0:t.length;for(this.__data__=new n;++e<r;)this.add(t[e])}SetCache.prototype.add=SetCache.prototype.push=o,SetCache.prototype.has=i,t.exports=SetCache},8968:t=>{\"use strict\";t.exports=Math.floor},8984:(t,e,r)=>{var n=r(5527),o=r(3650),i=Object.prototype.hasOwnProperty;t.exports=function baseKeys(t){if(!n(t))return o(t);var e=[];for(var r in Object(t))i.call(t,r)&&\"constructor\"!=r&&e.push(r);return e}},9092:(t,e,r)=>{\"use strict\";var n=r(1333);t.exports=function hasToStringTagShams(){return n()&&!!Symbol.toStringTag}},9209:(t,e,r)=>{\"use strict\";var n=r(6578),o=\"undefined\"==typeof globalThis?r.g:globalThis;t.exports=function availableTypedArrays(){for(var t=[],e=0;e<n.length;e++)\"function\"==typeof o[n[e]]&&(t[t.length]=n[e]);return t}},9219:t=>{t.exports=function cacheHas(t,e){return t.has(e)}},9290:t=>{\"use strict\";t.exports=RangeError},9298:(t,e,r)=>{\"use strict\";var n=r(4239),o=Object;t.exports=function(t){return o(n(t))}},9307:(t,e,r)=>{\"use strict\";var n=r(1091),o=r(4673);n({target:\"Function\",proto:!0,forced:Function.bind!==o},{bind:o})},9325:(t,e,r)=>{var n=r(4840),o=\"object\"==typeof self&&self&&self.Object===Object&&self,i=n||o||Function(\"return this\")();t.exports=i},9326:(t,e,r)=>{var n=r(1769),o=r(2428),i=r(6449),a=r(361),s=r(294),u=r(7797);t.exports=function hasPath(t,e,r){for(var c=-1,f=(e=n(e,t)).length,l=!1;++c<f;){var p=u(e[c]);if(!(l=null!=t&&r(t,p)))break;t=t[p]}return l||++c!=f?l:!!(f=null==t?0:t.length)&&s(f)&&a(p,f)&&(i(t)||o(t))}},9350:t=>{var e=Object.prototype.toString;t.exports=function objectToString(t){return e.call(t)}},9353:t=>{\"use strict\";var e=Object.prototype.toString,r=Math.max,n=function concatty(t,e){for(var r=[],n=0;n<t.length;n+=1)r[n]=t[n];for(var o=0;o<e.length;o+=1)r[o+t.length]=e[o];return r};t.exports=function bind(t){var o=this;if(\"function\"!=typeof o||\"[object Function]\"!==e.apply(o))throw new TypeError(\"Function.prototype.bind called on incompatible \"+o);for(var i,a=function slicy(t,e){for(var r=[],n=e||0,o=0;n<t.length;n+=1,o+=1)r[o]=t[n];return r}(arguments,1),s=r(0,o.length-a.length),u=[],c=0;c<s;c++)u[c]=\"$\"+c;if(i=Function(\"binder\",\"return function (\"+function(t,e){for(var r=\"\",n=0;n<t.length;n+=1)r+=t[n],n+1<t.length&&(r+=e);return r}(u,\",\")+\"){ return binder.apply(this,arguments); }\")((function(){if(this instanceof i){var e=o.apply(this,n(a,arguments));return Object(e)===e?e:this}return o.apply(t,n(a,arguments))})),o.prototype){var f=function Empty(){};f.prototype=o.prototype,i.prototype=new f,f.prototype=null}return i}},9367:(t,e,r)=>{\"use strict\";var n=r(2159),o=r(7136);t.exports=function(t,e){var r=t[e];return o(r)?void 0:n(r)}},9374:(t,e,r)=>{var n=r(4128),o=r(3805),i=r(4394),a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,u=/^0o[0-7]+$/i,c=parseInt;t.exports=function toNumber(t){if(\"number\"==typeof t)return t;if(i(t))return NaN;if(o(t)){var e=\"function\"==typeof t.valueOf?t.valueOf():t;t=o(e)?e+\"\":e}if(\"string\"!=typeof t)return 0===t?t:+t;t=n(t);var r=s.test(t);return r||u.test(t)?c(t.slice(2),r?2:8):a.test(t)?NaN:+t}},9383:t=>{\"use strict\";t.exports=Error},9404:function(t){t.exports=function(){\"use strict\";var t=Array.prototype.slice;function createClass(t,e){e&&(t.prototype=Object.create(e.prototype)),t.prototype.constructor=t}function Iterable(t){return isIterable(t)?t:Seq(t)}function KeyedIterable(t){return isKeyed(t)?t:KeyedSeq(t)}function IndexedIterable(t){return isIndexed(t)?t:IndexedSeq(t)}function SetIterable(t){return isIterable(t)&&!isAssociative(t)?t:SetSeq(t)}function isIterable(t){return!(!t||!t[e])}function isKeyed(t){return!(!t||!t[r])}function isIndexed(t){return!(!t||!t[n])}function isAssociative(t){return isKeyed(t)||isIndexed(t)}function isOrdered(t){return!(!t||!t[o])}createClass(KeyedIterable,Iterable),createClass(IndexedIterable,Iterable),createClass(SetIterable,Iterable),Iterable.isIterable=isIterable,Iterable.isKeyed=isKeyed,Iterable.isIndexed=isIndexed,Iterable.isAssociative=isAssociative,Iterable.isOrdered=isOrdered,Iterable.Keyed=KeyedIterable,Iterable.Indexed=IndexedIterable,Iterable.Set=SetIterable;var e=\"@@__IMMUTABLE_ITERABLE__@@\",r=\"@@__IMMUTABLE_KEYED__@@\",n=\"@@__IMMUTABLE_INDEXED__@@\",o=\"@@__IMMUTABLE_ORDERED__@@\",i=\"delete\",a=5,s=1<<a,u=s-1,c={},f={value:!1},l={value:!1};function MakeRef(t){return t.value=!1,t}function SetRef(t){t&&(t.value=!0)}function OwnerID(){}function arrCopy(t,e){e=e||0;for(var r=Math.max(0,t.length-e),n=new Array(r),o=0;o<r;o++)n[o]=t[o+e];return n}function ensureSize(t){return void 0===t.size&&(t.size=t.__iterate(returnTrue)),t.size}function wrapIndex(t,e){if(\"number\"!=typeof e){var r=e>>>0;if(\"\"+r!==e||4294967295===r)return NaN;e=r}return e<0?ensureSize(t)+e:e}function returnTrue(){return!0}function wholeSlice(t,e,r){return(0===t||void 0!==r&&t<=-r)&&(void 0===e||void 0!==r&&e>=r)}function resolveBegin(t,e){return resolveIndex(t,e,0)}function resolveEnd(t,e){return resolveIndex(t,e,e)}function resolveIndex(t,e,r){return void 0===t?r:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}var p=0,h=1,d=2,y=\"function\"==typeof Symbol&&Symbol.iterator,_=\"@@iterator\",g=y||_;function Iterator(t){this.next=t}function iteratorValue(t,e,r,n){var o=0===t?e:1===t?r:[e,r];return n?n.value=o:n={value:o,done:!1},n}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(t){return!!getIteratorFn(t)}function isIterator(t){return t&&\"function\"==typeof t.next}function getIterator(t){var e=getIteratorFn(t);return e&&e.call(t)}function getIteratorFn(t){var e=t&&(y&&t[y]||t[_]);if(\"function\"==typeof e)return e}function isArrayLike(t){return t&&\"number\"==typeof t.length}function Seq(t){return null==t?emptySequence():isIterable(t)?t.toSeq():seqFromValue(t)}function KeyedSeq(t){return null==t?emptySequence().toKeyedSeq():isIterable(t)?isKeyed(t)?t.toSeq():t.fromEntrySeq():keyedSeqFromValue(t)}function IndexedSeq(t){return null==t?emptySequence():isIterable(t)?isKeyed(t)?t.entrySeq():t.toIndexedSeq():indexedSeqFromValue(t)}function SetSeq(t){return(null==t?emptySequence():isIterable(t)?isKeyed(t)?t.entrySeq():t:indexedSeqFromValue(t)).toSetSeq()}Iterator.prototype.toString=function(){return\"[Iterator]\"},Iterator.KEYS=p,Iterator.VALUES=h,Iterator.ENTRIES=d,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[g]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString(\"Seq {\",\"}\")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(t,e){return seqIterate(this,t,e,!0)},Seq.prototype.__iterator=function(t,e){return seqIterator(this,t,e,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString(\"Seq [\",\"]\")},IndexedSeq.prototype.__iterate=function(t,e){return seqIterate(this,t,e,!1)},IndexedSeq.prototype.__iterator=function(t,e){return seqIterator(this,t,e,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var m,v,b,w=\"@@__IMMUTABLE_SEQ__@@\";function ArraySeq(t){this._array=t,this.size=t.length}function ObjectSeq(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function IterableSeq(t){this._iterable=t,this.size=t.length||t.size}function IteratorSeq(t){this._iterator=t,this._iteratorCache=[]}function isSeq(t){return!(!t||!t[w])}function emptySequence(){return m||(m=new ArraySeq([]))}function keyedSeqFromValue(t){var e=Array.isArray(t)?new ArraySeq(t).fromEntrySeq():isIterator(t)?new IteratorSeq(t).fromEntrySeq():hasIterator(t)?new IterableSeq(t).fromEntrySeq():\"object\"==typeof t?new ObjectSeq(t):void 0;if(!e)throw new TypeError(\"Expected Array or iterable object of [k, v] entries, or keyed object: \"+t);return e}function indexedSeqFromValue(t){var e=maybeIndexedSeqFromValue(t);if(!e)throw new TypeError(\"Expected Array or iterable object of values: \"+t);return e}function seqFromValue(t){var e=maybeIndexedSeqFromValue(t)||\"object\"==typeof t&&new ObjectSeq(t);if(!e)throw new TypeError(\"Expected Array or iterable object of values, or keyed object: \"+t);return e}function maybeIndexedSeqFromValue(t){return isArrayLike(t)?new ArraySeq(t):isIterator(t)?new IteratorSeq(t):hasIterator(t)?new IterableSeq(t):void 0}function seqIterate(t,e,r,n){var o=t._cache;if(o){for(var i=o.length-1,a=0;a<=i;a++){var s=o[r?i-a:a];if(!1===e(s[1],n?s[0]:a,t))return a+1}return a}return t.__iterateUncached(e,r)}function seqIterator(t,e,r,n){var o=t._cache;if(o){var i=o.length-1,a=0;return new Iterator((function(){var t=o[r?i-a:a];return a++>i?iteratorDone():iteratorValue(e,n?t[0]:a-1,t[1])}))}return t.__iteratorUncached(e,r)}function fromJS(t,e){return e?fromJSWith(e,t,\"\",{\"\":t}):fromJSDefault(t)}function fromJSWith(t,e,r,n){return Array.isArray(e)?t.call(n,r,IndexedSeq(e).map((function(r,n){return fromJSWith(t,r,n,e)}))):isPlainObj(e)?t.call(n,r,KeyedSeq(e).map((function(r,n){return fromJSWith(t,r,n,e)}))):e}function fromJSDefault(t){return Array.isArray(t)?IndexedSeq(t).map(fromJSDefault).toList():isPlainObj(t)?KeyedSeq(t).map(fromJSDefault).toMap():t}function isPlainObj(t){return t&&(t.constructor===Object||void 0===t.constructor)}function is(t,e){if(t===e||t!=t&&e!=e)return!0;if(!t||!e)return!1;if(\"function\"==typeof t.valueOf&&\"function\"==typeof e.valueOf){if((t=t.valueOf())===(e=e.valueOf())||t!=t&&e!=e)return!0;if(!t||!e)return!1}return!(\"function\"!=typeof t.equals||\"function\"!=typeof e.equals||!t.equals(e))}function deepEqual(t,e){if(t===e)return!0;if(!isIterable(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||isKeyed(t)!==isKeyed(e)||isIndexed(t)!==isIndexed(e)||isOrdered(t)!==isOrdered(e))return!1;if(0===t.size&&0===e.size)return!0;var r=!isAssociative(t);if(isOrdered(t)){var n=t.entries();return e.every((function(t,e){var o=n.next().value;return o&&is(o[1],t)&&(r||is(o[0],e))}))&&n.next().done}var o=!1;if(void 0===t.size)if(void 0===e.size)\"function\"==typeof t.cacheResult&&t.cacheResult();else{o=!0;var i=t;t=e,e=i}var a=!0,s=e.__iterate((function(e,n){if(r?!t.has(e):o?!is(e,t.get(n,c)):!is(t.get(n,c),e))return a=!1,!1}));return a&&t.size===s}function Repeat(t,e){if(!(this instanceof Repeat))return new Repeat(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(v)return v;v=this}}function invariant(t,e){if(!t)throw new Error(e)}function Range(t,e,r){if(!(this instanceof Range))return new Range(t,e,r);if(invariant(0!==r,\"Cannot step a Range by 0\"),t=t||0,void 0===e&&(e=1/0),r=void 0===r?1:Math.abs(r),e<t&&(r=-r),this._start=t,this._end=e,this._step=r,this.size=Math.max(0,Math.ceil((e-t)/r-1)+1),0===this.size){if(b)return b;b=this}}function Collection(){throw TypeError(\"Abstract\")}function KeyedCollection(){}function IndexedCollection(){}function SetCollection(){}Seq.prototype[w]=!0,createClass(ArraySeq,IndexedSeq),ArraySeq.prototype.get=function(t,e){return this.has(t)?this._array[wrapIndex(this,t)]:e},ArraySeq.prototype.__iterate=function(t,e){for(var r=this._array,n=r.length-1,o=0;o<=n;o++)if(!1===t(r[e?n-o:o],o,this))return o+1;return o},ArraySeq.prototype.__iterator=function(t,e){var r=this._array,n=r.length-1,o=0;return new Iterator((function(){return o>n?iteratorDone():iteratorValue(t,o,r[e?n-o++:o++])}))},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},ObjectSeq.prototype.has=function(t){return this._object.hasOwnProperty(t)},ObjectSeq.prototype.__iterate=function(t,e){for(var r=this._object,n=this._keys,o=n.length-1,i=0;i<=o;i++){var a=n[e?o-i:i];if(!1===t(r[a],a,this))return i+1}return i},ObjectSeq.prototype.__iterator=function(t,e){var r=this._object,n=this._keys,o=n.length-1,i=0;return new Iterator((function(){var a=n[e?o-i:i];return i++>o?iteratorDone():iteratorValue(t,a,r[a])}))},ObjectSeq.prototype[o]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);var r=getIterator(this._iterable),n=0;if(isIterator(r))for(var o;!(o=r.next()).done&&!1!==t(o.value,n++,this););return n},IterableSeq.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var r=getIterator(this._iterable);if(!isIterator(r))return new Iterator(iteratorDone);var n=0;return new Iterator((function(){var e=r.next();return e.done?e:iteratorValue(t,n++,e.value)}))},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);for(var r,n=this._iterator,o=this._iteratorCache,i=0;i<o.length;)if(!1===t(o[i],i++,this))return i;for(;!(r=n.next()).done;){var a=r.value;if(o[i]=a,!1===t(a,i++,this))break}return i},IteratorSeq.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var r=this._iterator,n=this._iteratorCache,o=0;return new Iterator((function(){if(o>=n.length){var e=r.next();if(e.done)return e;n[o]=e.value}return iteratorValue(t,o,n[o++])}))},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?\"Repeat []\":\"Repeat [ \"+this._value+\" \"+this.size+\" times ]\"},Repeat.prototype.get=function(t,e){return this.has(t)?this._value:e},Repeat.prototype.includes=function(t){return is(this._value,t)},Repeat.prototype.slice=function(t,e){var r=this.size;return wholeSlice(t,e,r)?this:new Repeat(this._value,resolveEnd(e,r)-resolveBegin(t,r))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(t){return is(this._value,t)?0:-1},Repeat.prototype.lastIndexOf=function(t){return is(this._value,t)?this.size:-1},Repeat.prototype.__iterate=function(t,e){for(var r=0;r<this.size;r++)if(!1===t(this._value,r,this))return r+1;return r},Repeat.prototype.__iterator=function(t,e){var r=this,n=0;return new Iterator((function(){return n<r.size?iteratorValue(t,n++,r._value):iteratorDone()}))},Repeat.prototype.equals=function(t){return t instanceof Repeat?is(this._value,t._value):deepEqual(t)},createClass(Range,IndexedSeq),Range.prototype.toString=function(){return 0===this.size?\"Range []\":\"Range [ \"+this._start+\"...\"+this._end+(1!==this._step?\" by \"+this._step:\"\")+\" ]\"},Range.prototype.get=function(t,e){return this.has(t)?this._start+wrapIndex(this,t)*this._step:e},Range.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&e<this.size&&e===Math.floor(e)},Range.prototype.slice=function(t,e){return wholeSlice(t,e,this.size)?this:(t=resolveBegin(t,this.size),(e=resolveEnd(e,this.size))<=t?new Range(0,0):new Range(this.get(t,this._end),this.get(e,this._end),this._step))},Range.prototype.indexOf=function(t){var e=t-this._start;if(e%this._step==0){var r=e/this._step;if(r>=0&&r<this.size)return r}return-1},Range.prototype.lastIndexOf=function(t){return this.indexOf(t)},Range.prototype.__iterate=function(t,e){for(var r=this.size-1,n=this._step,o=e?this._start+r*n:this._start,i=0;i<=r;i++){if(!1===t(o,i,this))return i+1;o+=e?-n:n}return i},Range.prototype.__iterator=function(t,e){var r=this.size-1,n=this._step,o=e?this._start+r*n:this._start,i=0;return new Iterator((function(){var a=o;return o+=e?-n:n,i>r?iteratorDone():iteratorValue(t,i++,a)}))},Range.prototype.equals=function(t){return t instanceof Range?this._start===t._start&&this._end===t._end&&this._step===t._step:deepEqual(this,t)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var I=\"function\"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(t,e){var r=65535&(t|=0),n=65535&(e|=0);return r*n+((t>>>16)*n+r*(e>>>16)<<16>>>0)|0};function smi(t){return t>>>1&1073741824|3221225471&t}function hash(t){if(!1===t||null==t)return 0;if(\"function\"==typeof t.valueOf&&(!1===(t=t.valueOf())||null==t))return 0;if(!0===t)return 1;var e=typeof t;if(\"number\"===e){if(t!=t||t===1/0)return 0;var r=0|t;for(r!==t&&(r^=4294967295*t);t>4294967295;)r^=t/=4294967295;return smi(r)}if(\"string\"===e)return t.length>L?cachedHashString(t):hashString(t);if(\"function\"==typeof t.hashCode)return t.hashCode();if(\"object\"===e)return hashJSObj(t);if(\"function\"==typeof t.toString)return hashString(t.toString());throw new Error(\"Value type \"+e+\" cannot be hashed.\")}function cachedHashString(t){var e=D[t];return void 0===e&&(e=hashString(t),U===P&&(U=0,D={}),U++,D[t]=e),e}function hashString(t){for(var e=0,r=0;r<t.length;r++)e=31*e+t.charCodeAt(r)|0;return smi(e)}function hashJSObj(t){var e;if(C&&void 0!==(e=k.get(t)))return e;if(void 0!==(e=t[q]))return e;if(!B){if(void 0!==(e=t.propertyIsEnumerable&&t.propertyIsEnumerable[q]))return e;if(void 0!==(e=getIENodeHash(t)))return e}if(e=++j,1073741824&j&&(j=0),C)k.set(t,e);else{if(void 0!==x&&!1===x(t))throw new Error(\"Non-extensible objects are not allowed as keys.\");if(B)Object.defineProperty(t,q,{enumerable:!1,configurable:!1,writable:!1,value:e});else if(void 0!==t.propertyIsEnumerable&&t.propertyIsEnumerable===t.constructor.prototype.propertyIsEnumerable)t.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},t.propertyIsEnumerable[q]=e;else{if(void 0===t.nodeType)throw new Error(\"Unable to set a non-enumerable property on object.\");t[q]=e}}return e}var x=Object.isExtensible,B=function(){try{return Object.defineProperty({},\"@\",{}),!0}catch(t){return!1}}();function getIENodeHash(t){if(t&&t.nodeType>0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}var k,C=\"function\"==typeof WeakMap;C&&(k=new WeakMap);var j=0,q=\"__immutablehash__\";\"function\"==typeof Symbol&&(q=Symbol(q));var L=16,P=255,U=0,D={};function assertNotInfinite(t){invariant(t!==1/0,\"Cannot perform this action with an infinite size.\")}function Map(t){return null==t?emptyMap():isMap(t)&&!isOrdered(t)?t:emptyMap().withMutations((function(e){var r=KeyedIterable(t);assertNotInfinite(r.size),r.forEach((function(t,r){return e.set(r,t)}))}))}function isMap(t){return!(!t||!t[W])}createClass(Map,KeyedCollection),Map.of=function(){var e=t.call(arguments,0);return emptyMap().withMutations((function(t){for(var r=0;r<e.length;r+=2){if(r+1>=e.length)throw new Error(\"Missing value for key: \"+e[r]);t.set(e[r],e[r+1])}}))},Map.prototype.toString=function(){return this.__toString(\"Map {\",\"}\")},Map.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},Map.prototype.set=function(t,e){return updateMap(this,t,e)},Map.prototype.setIn=function(t,e){return this.updateIn(t,c,(function(){return e}))},Map.prototype.remove=function(t){return updateMap(this,t,c)},Map.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return c}))},Map.prototype.update=function(t,e,r){return 1===arguments.length?t(this):this.updateIn([t],e,r)},Map.prototype.updateIn=function(t,e,r){r||(r=e,e=void 0);var n=updateInDeepMap(this,forceIterator(t),e,r);return n===c?void 0:n},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(e){return mergeIntoMapWith(this,e,t.call(arguments,1))},Map.prototype.mergeIn=function(e){var r=t.call(arguments,1);return this.updateIn(e,emptyMap(),(function(t){return\"function\"==typeof t.merge?t.merge.apply(t,r):r[r.length-1]}))},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(e){var r=t.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(e),r)},Map.prototype.mergeDeepIn=function(e){var r=t.call(arguments,1);return this.updateIn(e,emptyMap(),(function(t){return\"function\"==typeof t.mergeDeep?t.mergeDeep.apply(t,r):r[r.length-1]}))},Map.prototype.sort=function(t){return OrderedMap(sortFactory(this,t))},Map.prototype.sortBy=function(t,e){return OrderedMap(sortFactory(this,e,t))},Map.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(t,e){return new MapIterator(this,t,e)},Map.prototype.__iterate=function(t,e){var r=this,n=0;return this._root&&this._root.iterate((function(e){return n++,t(e[1],e[0],r)}),e),n},Map.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?makeMap(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Map.isMap=isMap;var z,W=\"@@__IMMUTABLE_MAP__@@\",V=Map.prototype;function ArrayMapNode(t,e){this.ownerID=t,this.entries=e}function BitmapIndexedNode(t,e,r){this.ownerID=t,this.bitmap=e,this.nodes=r}function HashArrayMapNode(t,e,r){this.ownerID=t,this.count=e,this.nodes=r}function HashCollisionNode(t,e,r){this.ownerID=t,this.keyHash=e,this.entries=r}function ValueNode(t,e,r){this.ownerID=t,this.keyHash=e,this.entry=r}function MapIterator(t,e,r){this._type=e,this._reverse=r,this._stack=t._root&&mapIteratorFrame(t._root)}function mapIteratorValue(t,e){return iteratorValue(t,e[0],e[1])}function mapIteratorFrame(t,e){return{node:t,index:0,__prev:e}}function makeMap(t,e,r,n){var o=Object.create(V);return o.size=t,o._root=e,o.__ownerID=r,o.__hash=n,o.__altered=!1,o}function emptyMap(){return z||(z=makeMap(0))}function updateMap(t,e,r){var n,o;if(t._root){var i=MakeRef(f),a=MakeRef(l);if(n=updateNode(t._root,t.__ownerID,0,void 0,e,r,i,a),!a.value)return t;o=t.size+(i.value?r===c?-1:1:0)}else{if(r===c)return t;o=1,n=new ArrayMapNode(t.__ownerID,[[e,r]])}return t.__ownerID?(t.size=o,t._root=n,t.__hash=void 0,t.__altered=!0,t):n?makeMap(o,n):emptyMap()}function updateNode(t,e,r,n,o,i,a,s){return t?t.update(e,r,n,o,i,a,s):i===c?t:(SetRef(s),SetRef(a),new ValueNode(e,n,[o,i]))}function isLeafNode(t){return t.constructor===ValueNode||t.constructor===HashCollisionNode}function mergeIntoNode(t,e,r,n,o){if(t.keyHash===n)return new HashCollisionNode(e,n,[t.entry,o]);var i,s=(0===r?t.keyHash:t.keyHash>>>r)&u,c=(0===r?n:n>>>r)&u;return new BitmapIndexedNode(e,1<<s|1<<c,s===c?[mergeIntoNode(t,e,r+a,n,o)]:(i=new ValueNode(e,n,o),s<c?[t,i]:[i,t]))}function createNodes(t,e,r,n){t||(t=new OwnerID);for(var o=new ValueNode(t,hash(r),[r,n]),i=0;i<e.length;i++){var a=e[i];o=o.update(t,0,void 0,a[0],a[1])}return o}function packNodes(t,e,r,n){for(var o=0,i=0,a=new Array(r),s=0,u=1,c=e.length;s<c;s++,u<<=1){var f=e[s];void 0!==f&&s!==n&&(o|=u,a[i++]=f)}return new BitmapIndexedNode(t,o,a)}function expandNodes(t,e,r,n,o){for(var i=0,a=new Array(s),u=0;0!==r;u++,r>>>=1)a[u]=1&r?e[i++]:void 0;return a[n]=o,new HashArrayMapNode(t,i+1,a)}function mergeIntoMapWith(t,e,r){for(var n=[],o=0;o<r.length;o++){var i=r[o],a=KeyedIterable(i);isIterable(i)||(a=a.map((function(t){return fromJS(t)}))),n.push(a)}return mergeIntoCollectionWith(t,e,n)}function deepMerger(t,e,r){return t&&t.mergeDeep&&isIterable(e)?t.mergeDeep(e):is(t,e)?t:e}function deepMergerWith(t){return function(e,r,n){if(e&&e.mergeDeepWith&&isIterable(r))return e.mergeDeepWith(t,r);var o=t(e,r,n);return is(e,o)?e:o}}function mergeIntoCollectionWith(t,e,r){return 0===(r=r.filter((function(t){return 0!==t.size}))).length?t:0!==t.size||t.__ownerID||1!==r.length?t.withMutations((function(t){for(var n=e?function(r,n){t.update(n,c,(function(t){return t===c?r:e(t,r,n)}))}:function(e,r){t.set(r,e)},o=0;o<r.length;o++)r[o].forEach(n)})):t.constructor(r[0])}function updateInDeepMap(t,e,r,n){var o=t===c,i=e.next();if(i.done){var a=o?r:t,s=n(a);return s===a?t:s}invariant(o||t&&t.set,\"invalid keyPath\");var u=i.value,f=o?c:t.get(u,c),l=updateInDeepMap(f,e,r,n);return l===f?t:l===c?t.remove(u):(o?emptyMap():t).set(u,l)}function popCount(t){return t=(t=(858993459&(t-=t>>1&1431655765))+(t>>2&858993459))+(t>>4)&252645135,t+=t>>8,127&(t+=t>>16)}function setIn(t,e,r,n){var o=n?t:arrCopy(t);return o[e]=r,o}function spliceIn(t,e,r,n){var o=t.length+1;if(n&&e+1===o)return t[e]=r,t;for(var i=new Array(o),a=0,s=0;s<o;s++)s===e?(i[s]=r,a=-1):i[s]=t[s+a];return i}function spliceOut(t,e,r){var n=t.length-1;if(r&&e===n)return t.pop(),t;for(var o=new Array(n),i=0,a=0;a<n;a++)a===e&&(i=1),o[a]=t[a+i];return o}V[W]=!0,V[i]=V.remove,V.removeIn=V.deleteIn,ArrayMapNode.prototype.get=function(t,e,r,n){for(var o=this.entries,i=0,a=o.length;i<a;i++)if(is(r,o[i][0]))return o[i][1];return n},ArrayMapNode.prototype.update=function(t,e,r,n,o,i,a){for(var s=o===c,u=this.entries,f=0,l=u.length;f<l&&!is(n,u[f][0]);f++);var p=f<l;if(p?u[f][1]===o:s)return this;if(SetRef(a),(s||!p)&&SetRef(i),!s||1!==u.length){if(!p&&!s&&u.length>=K)return createNodes(t,u,n,o);var h=t&&t===this.ownerID,d=h?u:arrCopy(u);return p?s?f===l-1?d.pop():d[f]=d.pop():d[f]=[n,o]:d.push([n,o]),h?(this.entries=d,this):new ArrayMapNode(t,d)}},BitmapIndexedNode.prototype.get=function(t,e,r,n){void 0===e&&(e=hash(r));var o=1<<((0===t?e:e>>>t)&u),i=this.bitmap;return i&o?this.nodes[popCount(i&o-1)].get(t+a,e,r,n):n},BitmapIndexedNode.prototype.update=function(t,e,r,n,o,i,s){void 0===r&&(r=hash(n));var f=(0===e?r:r>>>e)&u,l=1<<f,p=this.bitmap,h=!!(p&l);if(!h&&o===c)return this;var d=popCount(p&l-1),y=this.nodes,_=h?y[d]:void 0,g=updateNode(_,t,e+a,r,n,o,i,s);if(g===_)return this;if(!h&&g&&y.length>=$)return expandNodes(t,y,p,f,g);if(h&&!g&&2===y.length&&isLeafNode(y[1^d]))return y[1^d];if(h&&g&&1===y.length&&isLeafNode(g))return g;var m=t&&t===this.ownerID,v=h?g?p:p^l:p|l,b=h?g?setIn(y,d,g,m):spliceOut(y,d,m):spliceIn(y,d,g,m);return m?(this.bitmap=v,this.nodes=b,this):new BitmapIndexedNode(t,v,b)},HashArrayMapNode.prototype.get=function(t,e,r,n){void 0===e&&(e=hash(r));var o=(0===t?e:e>>>t)&u,i=this.nodes[o];return i?i.get(t+a,e,r,n):n},HashArrayMapNode.prototype.update=function(t,e,r,n,o,i,s){void 0===r&&(r=hash(n));var f=(0===e?r:r>>>e)&u,l=o===c,p=this.nodes,h=p[f];if(l&&!h)return this;var d=updateNode(h,t,e+a,r,n,o,i,s);if(d===h)return this;var y=this.count;if(h){if(!d&&--y<H)return packNodes(t,p,y,f)}else y++;var _=t&&t===this.ownerID,g=setIn(p,f,d,_);return _?(this.count=y,this.nodes=g,this):new HashArrayMapNode(t,y,g)},HashCollisionNode.prototype.get=function(t,e,r,n){for(var o=this.entries,i=0,a=o.length;i<a;i++)if(is(r,o[i][0]))return o[i][1];return n},HashCollisionNode.prototype.update=function(t,e,r,n,o,i,a){void 0===r&&(r=hash(n));var s=o===c;if(r!==this.keyHash)return s?this:(SetRef(a),SetRef(i),mergeIntoNode(this,t,e,r,[n,o]));for(var u=this.entries,f=0,l=u.length;f<l&&!is(n,u[f][0]);f++);var p=f<l;if(p?u[f][1]===o:s)return this;if(SetRef(a),(s||!p)&&SetRef(i),s&&2===l)return new ValueNode(t,this.keyHash,u[1^f]);var h=t&&t===this.ownerID,d=h?u:arrCopy(u);return p?s?f===l-1?d.pop():d[f]=d.pop():d[f]=[n,o]:d.push([n,o]),h?(this.entries=d,this):new HashCollisionNode(t,this.keyHash,d)},ValueNode.prototype.get=function(t,e,r,n){return is(r,this.entry[0])?this.entry[1]:n},ValueNode.prototype.update=function(t,e,r,n,o,i,a){var s=o===c,u=is(n,this.entry[0]);return(u?o===this.entry[1]:s)?this:(SetRef(a),s?void SetRef(i):u?t&&t===this.ownerID?(this.entry[1]=o,this):new ValueNode(t,this.keyHash,[n,o]):(SetRef(i),mergeIntoNode(this,t,e,hash(n),[n,o])))},ArrayMapNode.prototype.iterate=HashCollisionNode.prototype.iterate=function(t,e){for(var r=this.entries,n=0,o=r.length-1;n<=o;n++)if(!1===t(r[e?o-n:n]))return!1},BitmapIndexedNode.prototype.iterate=HashArrayMapNode.prototype.iterate=function(t,e){for(var r=this.nodes,n=0,o=r.length-1;n<=o;n++){var i=r[e?o-n:n];if(i&&!1===i.iterate(t,e))return!1}},ValueNode.prototype.iterate=function(t,e){return t(this.entry)},createClass(MapIterator,Iterator),MapIterator.prototype.next=function(){for(var t=this._type,e=this._stack;e;){var r,n=e.node,o=e.index++;if(n.entry){if(0===o)return mapIteratorValue(t,n.entry)}else if(n.entries){if(o<=(r=n.entries.length-1))return mapIteratorValue(t,n.entries[this._reverse?r-o:o])}else if(o<=(r=n.nodes.length-1)){var i=n.nodes[this._reverse?r-o:o];if(i){if(i.entry)return mapIteratorValue(t,i.entry);e=this._stack=mapIteratorFrame(i,e)}continue}e=this._stack=this._stack.__prev}return iteratorDone()};var K=s/4,$=s/2,H=s/4;function List(t){var e=emptyList();if(null==t)return e;if(isList(t))return t;var r=IndexedIterable(t),n=r.size;return 0===n?e:(assertNotInfinite(n),n>0&&n<s?makeList(0,n,a,null,new VNode(r.toArray())):e.withMutations((function(t){t.setSize(n),r.forEach((function(e,r){return t.set(r,e)}))})))}function isList(t){return!(!t||!t[Y])}createClass(List,IndexedCollection),List.of=function(){return this(arguments)},List.prototype.toString=function(){return this.__toString(\"List [\",\"]\")},List.prototype.get=function(t,e){if((t=wrapIndex(this,t))>=0&&t<this.size){var r=listNodeFor(this,t+=this._origin);return r&&r.array[t&u]}return e},List.prototype.set=function(t,e){return updateList(this,t,e)},List.prototype.remove=function(t){return this.has(t)?0===t?this.shift():t===this.size-1?this.pop():this.splice(t,1):this},List.prototype.insert=function(t,e){return this.splice(t,0,e)},List.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=a,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):emptyList()},List.prototype.push=function(){var t=arguments,e=this.size;return this.withMutations((function(r){setListBounds(r,0,e+t.length);for(var n=0;n<t.length;n++)r.set(e+n,t[n])}))},List.prototype.pop=function(){return setListBounds(this,0,-1)},List.prototype.unshift=function(){var t=arguments;return this.withMutations((function(e){setListBounds(e,-t.length);for(var r=0;r<t.length;r++)e.set(r,t[r])}))},List.prototype.shift=function(){return setListBounds(this,1)},List.prototype.merge=function(){return mergeIntoListWith(this,void 0,arguments)},List.prototype.mergeWith=function(e){return mergeIntoListWith(this,e,t.call(arguments,1))},List.prototype.mergeDeep=function(){return mergeIntoListWith(this,deepMerger,arguments)},List.prototype.mergeDeepWith=function(e){var r=t.call(arguments,1);return mergeIntoListWith(this,deepMergerWith(e),r)},List.prototype.setSize=function(t){return setListBounds(this,0,t)},List.prototype.slice=function(t,e){var r=this.size;return wholeSlice(t,e,r)?this:setListBounds(this,resolveBegin(t,r),resolveEnd(e,r))},List.prototype.__iterator=function(t,e){var r=0,n=iterateList(this,e);return new Iterator((function(){var e=n();return e===et?iteratorDone():iteratorValue(t,r++,e)}))},List.prototype.__iterate=function(t,e){for(var r,n=0,o=iterateList(this,e);(r=o())!==et&&!1!==t(r,n++,this););return n},List.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?makeList(this._origin,this._capacity,this._level,this._root,this._tail,t,this.__hash):(this.__ownerID=t,this)},List.isList=isList;var Y=\"@@__IMMUTABLE_LIST__@@\",Z=List.prototype;function VNode(t,e){this.array=t,this.ownerID=e}Z[Y]=!0,Z[i]=Z.remove,Z.setIn=V.setIn,Z.deleteIn=Z.removeIn=V.removeIn,Z.update=V.update,Z.updateIn=V.updateIn,Z.mergeIn=V.mergeIn,Z.mergeDeepIn=V.mergeDeepIn,Z.withMutations=V.withMutations,Z.asMutable=V.asMutable,Z.asImmutable=V.asImmutable,Z.wasAltered=V.wasAltered,VNode.prototype.removeBefore=function(t,e,r){if(r===e?1<<e:0===this.array.length)return this;var n=r>>>e&u;if(n>=this.array.length)return new VNode([],t);var o,i=0===n;if(e>0){var s=this.array[n];if((o=s&&s.removeBefore(t,e-a,r))===s&&i)return this}if(i&&!o)return this;var c=editableVNode(this,t);if(!i)for(var f=0;f<n;f++)c.array[f]=void 0;return o&&(c.array[n]=o),c},VNode.prototype.removeAfter=function(t,e,r){if(r===(e?1<<e:0)||0===this.array.length)return this;var n,o=r-1>>>e&u;if(o>=this.array.length)return this;if(e>0){var i=this.array[o];if((n=i&&i.removeAfter(t,e-a,r))===i&&o===this.array.length-1)return this}var s=editableVNode(this,t);return s.array.splice(o+1),n&&(s.array[o]=n),s};var J,tt,et={};function iterateList(t,e){var r=t._origin,n=t._capacity,o=getTailOffset(n),i=t._tail;return iterateNodeOrLeaf(t._root,t._level,0);function iterateNodeOrLeaf(t,e,r){return 0===e?iterateLeaf(t,r):iterateNode(t,e,r)}function iterateLeaf(t,a){var u=a===o?i&&i.array:t&&t.array,c=a>r?0:r-a,f=n-a;return f>s&&(f=s),function(){if(c===f)return et;var t=e?--f:c++;return u&&u[t]}}function iterateNode(t,o,i){var u,c=t&&t.array,f=i>r?0:r-i>>o,l=1+(n-i>>o);return l>s&&(l=s),function(){for(;;){if(u){var t=u();if(t!==et)return t;u=null}if(f===l)return et;var r=e?--l:f++;u=iterateNodeOrLeaf(c&&c[r],o-a,i+(r<<o))}}}}function makeList(t,e,r,n,o,i,a){var s=Object.create(Z);return s.size=e-t,s._origin=t,s._capacity=e,s._level=r,s._root=n,s._tail=o,s.__ownerID=i,s.__hash=a,s.__altered=!1,s}function emptyList(){return J||(J=makeList(0,0,a))}function updateList(t,e,r){if((e=wrapIndex(t,e))!=e)return t;if(e>=t.size||e<0)return t.withMutations((function(t){e<0?setListBounds(t,e).set(0,r):setListBounds(t,0,e+1).set(e,r)}));e+=t._origin;var n=t._tail,o=t._root,i=MakeRef(l);return e>=getTailOffset(t._capacity)?n=updateVNode(n,t.__ownerID,0,e,r,i):o=updateVNode(o,t.__ownerID,t._level,e,r,i),i.value?t.__ownerID?(t._root=o,t._tail=n,t.__hash=void 0,t.__altered=!0,t):makeList(t._origin,t._capacity,t._level,o,n):t}function updateVNode(t,e,r,n,o,i){var s,c=n>>>r&u,f=t&&c<t.array.length;if(!f&&void 0===o)return t;if(r>0){var l=t&&t.array[c],p=updateVNode(l,e,r-a,n,o,i);return p===l?t:((s=editableVNode(t,e)).array[c]=p,s)}return f&&t.array[c]===o?t:(SetRef(i),s=editableVNode(t,e),void 0===o&&c===s.array.length-1?s.array.pop():s.array[c]=o,s)}function editableVNode(t,e){return e&&t&&e===t.ownerID?t:new VNode(t?t.array.slice():[],e)}function listNodeFor(t,e){if(e>=getTailOffset(t._capacity))return t._tail;if(e<1<<t._level+a){for(var r=t._root,n=t._level;r&&n>0;)r=r.array[e>>>n&u],n-=a;return r}}function setListBounds(t,e,r){void 0!==e&&(e|=0),void 0!==r&&(r|=0);var n=t.__ownerID||new OwnerID,o=t._origin,i=t._capacity,s=o+e,c=void 0===r?i:r<0?i+r:o+r;if(s===o&&c===i)return t;if(s>=c)return t.clear();for(var f=t._level,l=t._root,p=0;s+p<0;)l=new VNode(l&&l.array.length?[void 0,l]:[],n),p+=1<<(f+=a);p&&(s+=p,o+=p,c+=p,i+=p);for(var h=getTailOffset(i),d=getTailOffset(c);d>=1<<f+a;)l=new VNode(l&&l.array.length?[l]:[],n),f+=a;var y=t._tail,_=d<h?listNodeFor(t,c-1):d>h?new VNode([],n):y;if(y&&d>h&&s<i&&y.array.length){for(var g=l=editableVNode(l,n),m=f;m>a;m-=a){var v=h>>>m&u;g=g.array[v]=editableVNode(g.array[v],n)}g.array[h>>>a&u]=y}if(c<i&&(_=_&&_.removeAfter(n,0,c)),s>=d)s-=d,c-=d,f=a,l=null,_=_&&_.removeBefore(n,0,s);else if(s>o||d<h){for(p=0;l;){var b=s>>>f&u;if(b!==d>>>f&u)break;b&&(p+=(1<<f)*b),f-=a,l=l.array[b]}l&&s>o&&(l=l.removeBefore(n,f,s-p)),l&&d<h&&(l=l.removeAfter(n,f,d-p)),p&&(s-=p,c-=p)}return t.__ownerID?(t.size=c-s,t._origin=s,t._capacity=c,t._level=f,t._root=l,t._tail=_,t.__hash=void 0,t.__altered=!0,t):makeList(s,c,f,l,_)}function mergeIntoListWith(t,e,r){for(var n=[],o=0,i=0;i<r.length;i++){var a=r[i],s=IndexedIterable(a);s.size>o&&(o=s.size),isIterable(a)||(s=s.map((function(t){return fromJS(t)}))),n.push(s)}return o>t.size&&(t=t.setSize(o)),mergeIntoCollectionWith(t,e,n)}function getTailOffset(t){return t<s?0:t-1>>>a<<a}function OrderedMap(t){return null==t?emptyOrderedMap():isOrderedMap(t)?t:emptyOrderedMap().withMutations((function(e){var r=KeyedIterable(t);assertNotInfinite(r.size),r.forEach((function(t,r){return e.set(r,t)}))}))}function isOrderedMap(t){return isMap(t)&&isOrdered(t)}function makeOrderedMap(t,e,r,n){var o=Object.create(OrderedMap.prototype);return o.size=t?t.size:0,o._map=t,o._list=e,o.__ownerID=r,o.__hash=n,o}function emptyOrderedMap(){return tt||(tt=makeOrderedMap(emptyMap(),emptyList()))}function updateOrderedMap(t,e,r){var n,o,i=t._map,a=t._list,u=i.get(e),f=void 0!==u;if(r===c){if(!f)return t;a.size>=s&&a.size>=2*i.size?(n=(o=a.filter((function(t,e){return void 0!==t&&u!==e}))).toKeyedSeq().map((function(t){return t[0]})).flip().toMap(),t.__ownerID&&(n.__ownerID=o.__ownerID=t.__ownerID)):(n=i.remove(e),o=u===a.size-1?a.pop():a.set(u,void 0))}else if(f){if(r===a.get(u)[1])return t;n=i,o=a.set(u,[e,r])}else n=i.set(e,a.size),o=a.set(a.size,[e,r]);return t.__ownerID?(t.size=n.size,t._map=n,t._list=o,t.__hash=void 0,t):makeOrderedMap(n,o)}function ToKeyedSequence(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ToIndexedSequence(t){this._iter=t,this.size=t.size}function ToSetSequence(t){this._iter=t,this.size=t.size}function FromEntriesSequence(t){this._iter=t,this.size=t.size}function flipFactory(t){var e=makeSequence(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=cacheResultThrough,e.__iterateUncached=function(e,r){var n=this;return t.__iterate((function(t,r){return!1!==e(r,t,n)}),r)},e.__iteratorUncached=function(e,r){if(e===d){var n=t.__iterator(e,r);return new Iterator((function(){var t=n.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t}))}return t.__iterator(e===h?p:h,r)},e}function mapFactory(t,e,r){var n=makeSequence(t);return n.size=t.size,n.has=function(e){return t.has(e)},n.get=function(n,o){var i=t.get(n,c);return i===c?o:e.call(r,i,n,t)},n.__iterateUncached=function(n,o){var i=this;return t.__iterate((function(t,o,a){return!1!==n(e.call(r,t,o,a),o,i)}),o)},n.__iteratorUncached=function(n,o){var i=t.__iterator(d,o);return new Iterator((function(){var o=i.next();if(o.done)return o;var a=o.value,s=a[0];return iteratorValue(n,s,e.call(r,a[1],s,t),o)}))},n}function reverseFactory(t,e){var r=makeSequence(t);return r._iter=t,r.size=t.size,r.reverse=function(){return t},t.flip&&(r.flip=function(){var e=flipFactory(t);return e.reverse=function(){return t.flip()},e}),r.get=function(r,n){return t.get(e?r:-1-r,n)},r.has=function(r){return t.has(e?r:-1-r)},r.includes=function(e){return t.includes(e)},r.cacheResult=cacheResultThrough,r.__iterate=function(e,r){var n=this;return t.__iterate((function(t,r){return e(t,r,n)}),!r)},r.__iterator=function(e,r){return t.__iterator(e,!r)},r}function filterFactory(t,e,r,n){var o=makeSequence(t);return n&&(o.has=function(n){var o=t.get(n,c);return o!==c&&!!e.call(r,o,n,t)},o.get=function(n,o){var i=t.get(n,c);return i!==c&&e.call(r,i,n,t)?i:o}),o.__iterateUncached=function(o,i){var a=this,s=0;return t.__iterate((function(t,i,u){if(e.call(r,t,i,u))return s++,o(t,n?i:s-1,a)}),i),s},o.__iteratorUncached=function(o,i){var a=t.__iterator(d,i),s=0;return new Iterator((function(){for(;;){var i=a.next();if(i.done)return i;var u=i.value,c=u[0],f=u[1];if(e.call(r,f,c,t))return iteratorValue(o,n?c:s++,f,i)}}))},o}function countByFactory(t,e,r){var n=Map().asMutable();return t.__iterate((function(o,i){n.update(e.call(r,o,i,t),0,(function(t){return t+1}))})),n.asImmutable()}function groupByFactory(t,e,r){var n=isKeyed(t),o=(isOrdered(t)?OrderedMap():Map()).asMutable();t.__iterate((function(i,a){o.update(e.call(r,i,a,t),(function(t){return(t=t||[]).push(n?[a,i]:i),t}))}));var i=iterableClass(t);return o.map((function(e){return reify(t,i(e))}))}function sliceFactory(t,e,r,n){var o=t.size;if(void 0!==e&&(e|=0),void 0!==r&&(r===1/0?r=o:r|=0),wholeSlice(e,r,o))return t;var i=resolveBegin(e,o),a=resolveEnd(r,o);if(i!=i||a!=a)return sliceFactory(t.toSeq().cacheResult(),e,r,n);var s,u=a-i;u==u&&(s=u<0?0:u);var c=makeSequence(t);return c.size=0===s?s:t.size&&s||void 0,!n&&isSeq(t)&&s>=0&&(c.get=function(e,r){return(e=wrapIndex(this,e))>=0&&e<s?t.get(e+i,r):r}),c.__iterateUncached=function(e,r){var o=this;if(0===s)return 0;if(r)return this.cacheResult().__iterate(e,r);var a=0,u=!0,c=0;return t.__iterate((function(t,r){if(!u||!(u=a++<i))return c++,!1!==e(t,n?r:c-1,o)&&c!==s})),c},c.__iteratorUncached=function(e,r){if(0!==s&&r)return this.cacheResult().__iterator(e,r);var o=0!==s&&t.__iterator(e,r),a=0,u=0;return new Iterator((function(){for(;a++<i;)o.next();if(++u>s)return iteratorDone();var t=o.next();return n||e===h?t:iteratorValue(e,u-1,e===p?void 0:t.value[1],t)}))},c}function takeWhileFactory(t,e,r){var n=makeSequence(t);return n.__iterateUncached=function(n,o){var i=this;if(o)return this.cacheResult().__iterate(n,o);var a=0;return t.__iterate((function(t,o,s){return e.call(r,t,o,s)&&++a&&n(t,o,i)})),a},n.__iteratorUncached=function(n,o){var i=this;if(o)return this.cacheResult().__iterator(n,o);var a=t.__iterator(d,o),s=!0;return new Iterator((function(){if(!s)return iteratorDone();var t=a.next();if(t.done)return t;var o=t.value,u=o[0],c=o[1];return e.call(r,c,u,i)?n===d?t:iteratorValue(n,u,c,t):(s=!1,iteratorDone())}))},n}function skipWhileFactory(t,e,r,n){var o=makeSequence(t);return o.__iterateUncached=function(o,i){var a=this;if(i)return this.cacheResult().__iterate(o,i);var s=!0,u=0;return t.__iterate((function(t,i,c){if(!s||!(s=e.call(r,t,i,c)))return u++,o(t,n?i:u-1,a)})),u},o.__iteratorUncached=function(o,i){var a=this;if(i)return this.cacheResult().__iterator(o,i);var s=t.__iterator(d,i),u=!0,c=0;return new Iterator((function(){var t,i,f;do{if((t=s.next()).done)return n||o===h?t:iteratorValue(o,c++,o===p?void 0:t.value[1],t);var l=t.value;i=l[0],f=l[1],u&&(u=e.call(r,f,i,a))}while(u);return o===d?t:iteratorValue(o,i,f,t)}))},o}function concatFactory(t,e){var r=isKeyed(t),n=[t].concat(e).map((function(t){return isIterable(t)?r&&(t=KeyedIterable(t)):t=r?keyedSeqFromValue(t):indexedSeqFromValue(Array.isArray(t)?t:[t]),t})).filter((function(t){return 0!==t.size}));if(0===n.length)return t;if(1===n.length){var o=n[0];if(o===t||r&&isKeyed(o)||isIndexed(t)&&isIndexed(o))return o}var i=new ArraySeq(n);return r?i=i.toKeyedSeq():isIndexed(t)||(i=i.toSetSeq()),(i=i.flatten(!0)).size=n.reduce((function(t,e){if(void 0!==t){var r=e.size;if(void 0!==r)return t+r}}),0),i}function flattenFactory(t,e,r){var n=makeSequence(t);return n.__iterateUncached=function(n,o){var i=0,a=!1;function flatDeep(t,s){var u=this;t.__iterate((function(t,o){return(!e||s<e)&&isIterable(t)?flatDeep(t,s+1):!1===n(t,r?o:i++,u)&&(a=!0),!a}),o)}return flatDeep(t,0),i},n.__iteratorUncached=function(n,o){var i=t.__iterator(n,o),a=[],s=0;return new Iterator((function(){for(;i;){var t=i.next();if(!1===t.done){var u=t.value;if(n===d&&(u=u[1]),e&&!(a.length<e)||!isIterable(u))return r?t:iteratorValue(n,s++,u,t);a.push(i),i=u.__iterator(n,o)}else i=a.pop()}return iteratorDone()}))},n}function flatMapFactory(t,e,r){var n=iterableClass(t);return t.toSeq().map((function(o,i){return n(e.call(r,o,i,t))})).flatten(!0)}function interposeFactory(t,e){var r=makeSequence(t);return r.size=t.size&&2*t.size-1,r.__iterateUncached=function(r,n){var o=this,i=0;return t.__iterate((function(t,n){return(!i||!1!==r(e,i++,o))&&!1!==r(t,i++,o)}),n),i},r.__iteratorUncached=function(r,n){var o,i=t.__iterator(h,n),a=0;return new Iterator((function(){return(!o||a%2)&&(o=i.next()).done?o:a%2?iteratorValue(r,a++,e):iteratorValue(r,a++,o.value,o)}))},r}function sortFactory(t,e,r){e||(e=defaultComparator);var n=isKeyed(t),o=0,i=t.toSeq().map((function(e,n){return[n,e,o++,r?r(e,n,t):e]})).toArray();return i.sort((function(t,r){return e(t[3],r[3])||t[2]-r[2]})).forEach(n?function(t,e){i[e].length=2}:function(t,e){i[e]=t[1]}),n?KeyedSeq(i):isIndexed(t)?IndexedSeq(i):SetSeq(i)}function maxFactory(t,e,r){if(e||(e=defaultComparator),r){var n=t.toSeq().map((function(e,n){return[e,r(e,n,t)]})).reduce((function(t,r){return maxCompare(e,t[1],r[1])?r:t}));return n&&n[0]}return t.reduce((function(t,r){return maxCompare(e,t,r)?r:t}))}function maxCompare(t,e,r){var n=t(r,e);return 0===n&&r!==e&&(null==r||r!=r)||n>0}function zipWithFactory(t,e,r){var n=makeSequence(t);return n.size=new ArraySeq(r).map((function(t){return t.size})).min(),n.__iterate=function(t,e){for(var r,n=this.__iterator(h,e),o=0;!(r=n.next()).done&&!1!==t(r.value,o++,this););return o},n.__iteratorUncached=function(t,n){var o=r.map((function(t){return t=Iterable(t),getIterator(n?t.reverse():t)})),i=0,a=!1;return new Iterator((function(){var r;return a||(r=o.map((function(t){return t.next()})),a=r.some((function(t){return t.done}))),a?iteratorDone():iteratorValue(t,i++,e.apply(null,r.map((function(t){return t.value}))))}))},n}function reify(t,e){return isSeq(t)?e:t.constructor(e)}function validateEntry(t){if(t!==Object(t))throw new TypeError(\"Expected [K, V] tuple: \"+t)}function resolveSize(t){return assertNotInfinite(t.size),ensureSize(t)}function iterableClass(t){return isKeyed(t)?KeyedIterable:isIndexed(t)?IndexedIterable:SetIterable}function makeSequence(t){return Object.create((isKeyed(t)?KeyedSeq:isIndexed(t)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(t,e){return t>e?1:t<e?-1:0}function forceIterator(t){var e=getIterator(t);if(!e){if(!isArrayLike(t))throw new TypeError(\"Expected iterable or array-like: \"+t);e=getIterator(Iterable(t))}return e}function Record(t,e){var r,n=function Record(i){if(i instanceof n)return i;if(!(this instanceof n))return new n(i);if(!r){r=!0;var a=Object.keys(t);setProps(o,a),o.size=a.length,o._name=e,o._keys=a,o._defaultValues=t}this._map=Map(i)},o=n.prototype=Object.create(rt);return o.constructor=n,n}createClass(OrderedMap,Map),OrderedMap.of=function(){return this(arguments)},OrderedMap.prototype.toString=function(){return this.__toString(\"OrderedMap {\",\"}\")},OrderedMap.prototype.get=function(t,e){var r=this._map.get(t);return void 0!==r?this._list.get(r)[1]:e},OrderedMap.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):emptyOrderedMap()},OrderedMap.prototype.set=function(t,e){return updateOrderedMap(this,t,e)},OrderedMap.prototype.remove=function(t){return updateOrderedMap(this,t,c)},OrderedMap.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},OrderedMap.prototype.__iterate=function(t,e){var r=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],r)}),e)},OrderedMap.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},OrderedMap.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),r=this._list.__ensureOwner(t);return t?makeOrderedMap(e,r,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=r,this)},OrderedMap.isOrderedMap=isOrderedMap,OrderedMap.prototype[o]=!0,OrderedMap.prototype[i]=OrderedMap.prototype.remove,createClass(ToKeyedSequence,KeyedSeq),ToKeyedSequence.prototype.get=function(t,e){return this._iter.get(t,e)},ToKeyedSequence.prototype.has=function(t){return this._iter.has(t)},ToKeyedSequence.prototype.valueSeq=function(){return this._iter.valueSeq()},ToKeyedSequence.prototype.reverse=function(){var t=this,e=reverseFactory(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},ToKeyedSequence.prototype.map=function(t,e){var r=this,n=mapFactory(this,t,e);return this._useKeys||(n.valueSeq=function(){return r._iter.toSeq().map(t,e)}),n},ToKeyedSequence.prototype.__iterate=function(t,e){var r,n=this;return this._iter.__iterate(this._useKeys?function(e,r){return t(e,r,n)}:(r=e?resolveSize(this):0,function(o){return t(o,e?--r:r++,n)}),e)},ToKeyedSequence.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var r=this._iter.__iterator(h,e),n=e?resolveSize(this):0;return new Iterator((function(){var o=r.next();return o.done?o:iteratorValue(t,e?--n:n++,o.value,o)}))},ToKeyedSequence.prototype[o]=!0,createClass(ToIndexedSequence,IndexedSeq),ToIndexedSequence.prototype.includes=function(t){return this._iter.includes(t)},ToIndexedSequence.prototype.__iterate=function(t,e){var r=this,n=0;return this._iter.__iterate((function(e){return t(e,n++,r)}),e)},ToIndexedSequence.prototype.__iterator=function(t,e){var r=this._iter.__iterator(h,e),n=0;return new Iterator((function(){var e=r.next();return e.done?e:iteratorValue(t,n++,e.value,e)}))},createClass(ToSetSequence,SetSeq),ToSetSequence.prototype.has=function(t){return this._iter.includes(t)},ToSetSequence.prototype.__iterate=function(t,e){var r=this;return this._iter.__iterate((function(e){return t(e,e,r)}),e)},ToSetSequence.prototype.__iterator=function(t,e){var r=this._iter.__iterator(h,e);return new Iterator((function(){var e=r.next();return e.done?e:iteratorValue(t,e.value,e.value,e)}))},createClass(FromEntriesSequence,KeyedSeq),FromEntriesSequence.prototype.entrySeq=function(){return this._iter.toSeq()},FromEntriesSequence.prototype.__iterate=function(t,e){var r=this;return this._iter.__iterate((function(e){if(e){validateEntry(e);var n=isIterable(e);return t(n?e.get(1):e[1],n?e.get(0):e[0],r)}}),e)},FromEntriesSequence.prototype.__iterator=function(t,e){var r=this._iter.__iterator(h,e);return new Iterator((function(){for(;;){var e=r.next();if(e.done)return e;var n=e.value;if(n){validateEntry(n);var o=isIterable(n);return iteratorValue(t,o?n.get(0):n[0],o?n.get(1):n[1],e)}}}))},ToIndexedSequence.prototype.cacheResult=ToKeyedSequence.prototype.cacheResult=ToSetSequence.prototype.cacheResult=FromEntriesSequence.prototype.cacheResult=cacheResultThrough,createClass(Record,KeyedCollection),Record.prototype.toString=function(){return this.__toString(recordName(this)+\" {\",\"}\")},Record.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Record.prototype.get=function(t,e){if(!this.has(t))return e;var r=this._defaultValues[t];return this._map?this._map.get(t,r):r},Record.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=makeRecord(this,emptyMap()))},Record.prototype.set=function(t,e){if(!this.has(t))throw new Error('Cannot set unknown key \"'+t+'\" on '+recordName(this));if(this._map&&!this._map.has(t)&&e===this._defaultValues[t])return this;var r=this._map&&this._map.set(t,e);return this.__ownerID||r===this._map?this:makeRecord(this,r)},Record.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:makeRecord(this,e)},Record.prototype.wasAltered=function(){return this._map.wasAltered()},Record.prototype.__iterator=function(t,e){var r=this;return KeyedIterable(this._defaultValues).map((function(t,e){return r.get(e)})).__iterator(t,e)},Record.prototype.__iterate=function(t,e){var r=this;return KeyedIterable(this._defaultValues).map((function(t,e){return r.get(e)})).__iterate(t,e)},Record.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?makeRecord(this,e,t):(this.__ownerID=t,this._map=e,this)};var rt=Record.prototype;function makeRecord(t,e,r){var n=Object.create(Object.getPrototypeOf(t));return n._map=e,n.__ownerID=r,n}function recordName(t){return t._name||t.constructor.name||\"Record\"}function setProps(t,e){try{e.forEach(setProp.bind(void 0,t))}catch(t){}}function setProp(t,e){Object.defineProperty(t,e,{get:function(){return this.get(e)},set:function(t){invariant(this.__ownerID,\"Cannot set on an immutable record.\"),this.set(e,t)}})}function Set(t){return null==t?emptySet():isSet(t)&&!isOrdered(t)?t:emptySet().withMutations((function(e){var r=SetIterable(t);assertNotInfinite(r.size),r.forEach((function(t){return e.add(t)}))}))}function isSet(t){return!(!t||!t[ot])}rt[i]=rt.remove,rt.deleteIn=rt.removeIn=V.removeIn,rt.merge=V.merge,rt.mergeWith=V.mergeWith,rt.mergeIn=V.mergeIn,rt.mergeDeep=V.mergeDeep,rt.mergeDeepWith=V.mergeDeepWith,rt.mergeDeepIn=V.mergeDeepIn,rt.setIn=V.setIn,rt.update=V.update,rt.updateIn=V.updateIn,rt.withMutations=V.withMutations,rt.asMutable=V.asMutable,rt.asImmutable=V.asImmutable,createClass(Set,SetCollection),Set.of=function(){return this(arguments)},Set.fromKeys=function(t){return this(KeyedIterable(t).keySeq())},Set.prototype.toString=function(){return this.__toString(\"Set {\",\"}\")},Set.prototype.has=function(t){return this._map.has(t)},Set.prototype.add=function(t){return updateSet(this,this._map.set(t,!0))},Set.prototype.remove=function(t){return updateSet(this,this._map.remove(t))},Set.prototype.clear=function(){return updateSet(this,this._map.clear())},Set.prototype.union=function(){var e=t.call(arguments,0);return 0===(e=e.filter((function(t){return 0!==t.size}))).length?this:0!==this.size||this.__ownerID||1!==e.length?this.withMutations((function(t){for(var r=0;r<e.length;r++)SetIterable(e[r]).forEach((function(e){return t.add(e)}))})):this.constructor(e[0])},Set.prototype.intersect=function(){var e=t.call(arguments,0);if(0===e.length)return this;e=e.map((function(t){return SetIterable(t)}));var r=this;return this.withMutations((function(t){r.forEach((function(r){e.every((function(t){return t.includes(r)}))||t.remove(r)}))}))},Set.prototype.subtract=function(){var e=t.call(arguments,0);if(0===e.length)return this;e=e.map((function(t){return SetIterable(t)}));var r=this;return this.withMutations((function(t){r.forEach((function(r){e.some((function(t){return t.includes(r)}))&&t.remove(r)}))}))},Set.prototype.merge=function(){return this.union.apply(this,arguments)},Set.prototype.mergeWith=function(e){var r=t.call(arguments,1);return this.union.apply(this,r)},Set.prototype.sort=function(t){return OrderedSet(sortFactory(this,t))},Set.prototype.sortBy=function(t,e){return OrderedSet(sortFactory(this,e,t))},Set.prototype.wasAltered=function(){return this._map.wasAltered()},Set.prototype.__iterate=function(t,e){var r=this;return this._map.__iterate((function(e,n){return t(n,n,r)}),e)},Set.prototype.__iterator=function(t,e){return this._map.map((function(t,e){return e})).__iterator(t,e)},Set.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t);return t?this.__make(e,t):(this.__ownerID=t,this._map=e,this)},Set.isSet=isSet;var nt,ot=\"@@__IMMUTABLE_SET__@@\",it=Set.prototype;function updateSet(t,e){return t.__ownerID?(t.size=e.size,t._map=e,t):e===t._map?t:0===e.size?t.__empty():t.__make(e)}function makeSet(t,e){var r=Object.create(it);return r.size=t?t.size:0,r._map=t,r.__ownerID=e,r}function emptySet(){return nt||(nt=makeSet(emptyMap()))}function OrderedSet(t){return null==t?emptyOrderedSet():isOrderedSet(t)?t:emptyOrderedSet().withMutations((function(e){var r=SetIterable(t);assertNotInfinite(r.size),r.forEach((function(t){return e.add(t)}))}))}function isOrderedSet(t){return isSet(t)&&isOrdered(t)}it[ot]=!0,it[i]=it.remove,it.mergeDeep=it.merge,it.mergeDeepWith=it.mergeWith,it.withMutations=V.withMutations,it.asMutable=V.asMutable,it.asImmutable=V.asImmutable,it.__empty=emptySet,it.__make=makeSet,createClass(OrderedSet,Set),OrderedSet.of=function(){return this(arguments)},OrderedSet.fromKeys=function(t){return this(KeyedIterable(t).keySeq())},OrderedSet.prototype.toString=function(){return this.__toString(\"OrderedSet {\",\"}\")},OrderedSet.isOrderedSet=isOrderedSet;var at,st=OrderedSet.prototype;function makeOrderedSet(t,e){var r=Object.create(st);return r.size=t?t.size:0,r._map=t,r.__ownerID=e,r}function emptyOrderedSet(){return at||(at=makeOrderedSet(emptyOrderedMap()))}function Stack(t){return null==t?emptyStack():isStack(t)?t:emptyStack().unshiftAll(t)}function isStack(t){return!(!t||!t[ct])}st[o]=!0,st.__empty=emptyOrderedSet,st.__make=makeOrderedSet,createClass(Stack,IndexedCollection),Stack.of=function(){return this(arguments)},Stack.prototype.toString=function(){return this.__toString(\"Stack [\",\"]\")},Stack.prototype.get=function(t,e){var r=this._head;for(t=wrapIndex(this,t);r&&t--;)r=r.next;return r?r.value:e},Stack.prototype.peek=function(){return this._head&&this._head.value},Stack.prototype.push=function(){if(0===arguments.length)return this;for(var t=this.size+arguments.length,e=this._head,r=arguments.length-1;r>=0;r--)e={value:arguments[r],next:e};return this.__ownerID?(this.size=t,this._head=e,this.__hash=void 0,this.__altered=!0,this):makeStack(t,e)},Stack.prototype.pushAll=function(t){if(0===(t=IndexedIterable(t)).size)return this;assertNotInfinite(t.size);var e=this.size,r=this._head;return t.reverse().forEach((function(t){e++,r={value:t,next:r}})),this.__ownerID?(this.size=e,this._head=r,this.__hash=void 0,this.__altered=!0,this):makeStack(e,r)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(t){return this.pushAll(t)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(t,e){if(wholeSlice(t,e,this.size))return this;var r=resolveBegin(t,this.size);if(resolveEnd(e,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,t,e);for(var n=this.size-r,o=this._head;r--;)o=o.next;return this.__ownerID?(this.size=n,this._head=o,this.__hash=void 0,this.__altered=!0,this):makeStack(n,o)},Stack.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?makeStack(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Stack.prototype.__iterate=function(t,e){if(e)return this.reverse().__iterate(t);for(var r=0,n=this._head;n&&!1!==t(n.value,r++,this);)n=n.next;return r},Stack.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var r=0,n=this._head;return new Iterator((function(){if(n){var e=n.value;return n=n.next,iteratorValue(t,r++,e)}return iteratorDone()}))},Stack.isStack=isStack;var ut,ct=\"@@__IMMUTABLE_STACK__@@\",lt=Stack.prototype;function makeStack(t,e,r,n){var o=Object.create(lt);return o.size=t,o._head=e,o.__ownerID=r,o.__hash=n,o.__altered=!1,o}function emptyStack(){return ut||(ut=makeStack(0))}function mixin(t,e){var keyCopier=function(r){t.prototype[r]=e[r]};return Object.keys(e).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(e).forEach(keyCopier),t}lt[ct]=!0,lt.withMutations=V.withMutations,lt.asMutable=V.asMutable,lt.asImmutable=V.asImmutable,lt.wasAltered=V.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var t=new Array(this.size||0);return this.valueSeq().__iterate((function(e,r){t[r]=e})),t},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map((function(t){return t&&\"function\"==typeof t.toJS?t.toJS():t})).__toJS()},toJSON:function(){return this.toSeq().map((function(t){return t&&\"function\"==typeof t.toJSON?t.toJSON():t})).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var t={};return this.__iterate((function(e,r){t[r]=e})),t},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return\"[Iterable]\"},__toString:function(t,e){return 0===this.size?t+e:t+\" \"+this.toSeq().map(this.__toStringMapper).join(\", \")+\" \"+e},concat:function(){return reify(this,concatFactory(this,t.call(arguments,0)))},includes:function(t){return this.some((function(e){return is(e,t)}))},entries:function(){return this.__iterator(d)},every:function(t,e){assertNotInfinite(this.size);var r=!0;return this.__iterate((function(n,o,i){if(!t.call(e,n,o,i))return r=!1,!1})),r},filter:function(t,e){return reify(this,filterFactory(this,t,e,!0))},find:function(t,e,r){var n=this.findEntry(t,e);return n?n[1]:r},forEach:function(t,e){return assertNotInfinite(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){assertNotInfinite(this.size),t=void 0!==t?\"\"+t:\",\";var e=\"\",r=!0;return this.__iterate((function(n){r?r=!1:e+=t,e+=null!=n?n.toString():\"\"})),e},keys:function(){return this.__iterator(p)},map:function(t,e){return reify(this,mapFactory(this,t,e))},reduce:function(t,e,r){var n,o;return assertNotInfinite(this.size),arguments.length<2?o=!0:n=e,this.__iterate((function(e,i,a){o?(o=!1,n=e):n=t.call(r,n,e,i,a)})),n},reduceRight:function(t,e,r){var n=this.toKeyedSeq().reverse();return n.reduce.apply(n,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(t,e){return reify(this,sliceFactory(this,t,e,!0))},some:function(t,e){return!this.every(not(t),e)},sort:function(t){return reify(this,sortFactory(this,t))},values:function(){return this.__iterator(h)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(t,e){return ensureSize(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return countByFactory(this,t,e)},equals:function(t){return deepEqual(this,t)},entrySeq:function(){var t=this;if(t._cache)return new ArraySeq(t._cache);var e=t.toSeq().map(entryMapper).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){return this.filter(not(t),e)},findEntry:function(t,e,r){var n=r;return this.__iterate((function(r,o,i){if(t.call(e,r,o,i))return n=[o,r],!1})),n},findKey:function(t,e){var r=this.findEntry(t,e);return r&&r[0]},findLast:function(t,e,r){return this.toKeyedSeq().reverse().find(t,e,r)},findLastEntry:function(t,e,r){return this.toKeyedSeq().reverse().findEntry(t,e,r)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(returnTrue)},flatMap:function(t,e){return reify(this,flatMapFactory(this,t,e))},flatten:function(t){return reify(this,flattenFactory(this,t,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(t,e){return this.find((function(e,r){return is(r,t)}),void 0,e)},getIn:function(t,e){for(var r,n=this,o=forceIterator(t);!(r=o.next()).done;){var i=r.value;if((n=n&&n.get?n.get(i,c):c)===c)return e}return n},groupBy:function(t,e){return groupByFactory(this,t,e)},has:function(t){return this.get(t,c)!==c},hasIn:function(t){return this.getIn(t,c)!==c},isSubset:function(t){return t=\"function\"==typeof t.includes?t:Iterable(t),this.every((function(e){return t.includes(e)}))},isSuperset:function(t){return(t=\"function\"==typeof t.isSubset?t:Iterable(t)).isSubset(this)},keyOf:function(t){return this.findKey((function(e){return is(e,t)}))},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return maxFactory(this,t)},maxBy:function(t,e){return maxFactory(this,e,t)},min:function(t){return maxFactory(this,t?neg(t):defaultNegComparator)},minBy:function(t,e){return maxFactory(this,e?neg(e):defaultNegComparator,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return reify(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return reify(this,skipWhileFactory(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile(not(t),e)},sortBy:function(t,e){return reify(this,sortFactory(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return reify(this,this.toSeq().reverse().take(t).reverse())},takeWhile:function(t,e){return reify(this,takeWhileFactory(this,t,e))},takeUntil:function(t,e){return this.takeWhile(not(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=hashIterable(this))}});var pt=Iterable.prototype;pt[e]=!0,pt[g]=pt.values,pt.__toJS=pt.toArray,pt.__toStringMapper=quoteString,pt.inspect=pt.toSource=function(){return this.toString()},pt.chain=pt.flatMap,pt.contains=pt.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(t,e){var r=this,n=0;return reify(this,this.toSeq().map((function(o,i){return t.call(e,[i,o],n++,r)})).fromEntrySeq())},mapKeys:function(t,e){var r=this;return reify(this,this.toSeq().flip().map((function(n,o){return t.call(e,n,o,r)})).flip())}});var ht=KeyedIterable.prototype;function keyMapper(t,e){return e}function entryMapper(t,e){return[e,t]}function not(t){return function(){return!t.apply(this,arguments)}}function neg(t){return function(){return-t.apply(this,arguments)}}function quoteString(t){return\"string\"==typeof t?JSON.stringify(t):String(t)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(t,e){return t<e?1:t>e?-1:0}function hashIterable(t){if(t.size===1/0)return 0;var e=isOrdered(t),r=isKeyed(t),n=e?1:0;return murmurHashOfSize(t.__iterate(r?e?function(t,e){n=31*n+hashMerge(hash(t),hash(e))|0}:function(t,e){n=n+hashMerge(hash(t),hash(e))|0}:e?function(t){n=31*n+hash(t)|0}:function(t){n=n+hash(t)|0}),n)}function murmurHashOfSize(t,e){return e=I(e,3432918353),e=I(e<<15|e>>>-15,461845907),e=I(e<<13|e>>>-13,5),e=I((e=e+3864292196^t)^e>>>16,2246822507),e=smi((e=I(e^e>>>13,3266489909))^e>>>16)}function hashMerge(t,e){return t^e+2654435769+(t<<6)+(t>>2)}return ht[r]=!0,ht[g]=pt.entries,ht.__toJS=pt.toObject,ht.__toStringMapper=function(t,e){return JSON.stringify(e)+\": \"+quoteString(t)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(t,e){return reify(this,filterFactory(this,t,e,!1))},findIndex:function(t,e){var r=this.findEntry(t,e);return r?r[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(t,e){return reify(this,sliceFactory(this,t,e,!1))},splice:function(t,e){var r=arguments.length;if(e=Math.max(0|e,0),0===r||2===r&&!e)return this;t=resolveBegin(t,t<0?this.count():this.size);var n=this.slice(0,t);return reify(this,1===r?n:n.concat(arrCopy(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var r=this.findLastEntry(t,e);return r?r[0]:-1},first:function(){return this.get(0)},flatten:function(t){return reify(this,flattenFactory(this,t,!1))},get:function(t,e){return(t=wrapIndex(this,t))<0||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find((function(e,r){return r===t}),void 0,e)},has:function(t){return(t=wrapIndex(this,t))>=0&&(void 0!==this.size?this.size===1/0||t<this.size:-1!==this.indexOf(t))},interpose:function(t){return reify(this,interposeFactory(this,t))},interleave:function(){var t=[this].concat(arrCopy(arguments)),e=zipWithFactory(this.toSeq(),IndexedSeq.of,t),r=e.flatten(!0);return e.size&&(r.size=e.size*t.length),reify(this,r)},keySeq:function(){return Range(0,this.size)},last:function(){return this.get(-1)},skipWhile:function(t,e){return reify(this,skipWhileFactory(this,t,e,!1))},zip:function(){return reify(this,zipWithFactory(this,defaultZipper,[this].concat(arrCopy(arguments))))},zipWith:function(t){var e=arrCopy(arguments);return e[0]=this,reify(this,zipWithFactory(this,t,e))}}),IndexedIterable.prototype[n]=!0,IndexedIterable.prototype[o]=!0,mixin(SetIterable,{get:function(t,e){return this.has(t)?t:e},includes:function(t){return this.has(t)},keySeq:function(){return this.valueSeq()}}),SetIterable.prototype.has=pt.includes,SetIterable.prototype.contains=SetIterable.prototype.includes,mixin(KeyedSeq,KeyedIterable.prototype),mixin(IndexedSeq,IndexedIterable.prototype),mixin(SetSeq,SetIterable.prototype),mixin(KeyedCollection,KeyedIterable.prototype),mixin(IndexedCollection,IndexedIterable.prototype),mixin(SetCollection,SetIterable.prototype),{Iterable,Seq,Collection,Map,OrderedMap,List,Stack,Set,OrderedSet,Record,Range,Repeat,is,fromJS}}()},9447:(t,e,r)=>{\"use strict\";var n=r(8828);t.exports=!n((function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]}))},9538:t=>{\"use strict\";t.exports=ReferenceError},9552:(t,e,r)=>{\"use strict\";var n=r(5951),o=r(6285),i=n.document,a=o(i)&&o(i.createElement);t.exports=function(t){return a?i.createElement(t):{}}},9600:t=>{\"use strict\";var e,r,n=Function.prototype.toString,o=\"object\"==typeof Reflect&&null!==Reflect&&Reflect.apply;if(\"function\"==typeof o&&\"function\"==typeof Object.defineProperty)try{e=Object.defineProperty({},\"length\",{get:function(){throw r}}),r={},o((function(){throw 42}),null,e)}catch(t){t!==r&&(o=null)}else o=null;var i=/^\\s*class\\b/,a=function isES6ClassFunction(t){try{var e=n.call(t);return i.test(e)}catch(t){return!1}},s=function tryFunctionToStr(t){try{return!a(t)&&(n.call(t),!0)}catch(t){return!1}},u=Object.prototype.toString,c=\"function\"==typeof Symbol&&!!Symbol.toStringTag,f=!(0 in[,]),l=function isDocumentDotAll(){return!1};if(\"object\"==typeof document){var p=document.all;u.call(p)===u.call(document.all)&&(l=function isDocumentDotAll(t){if((f||!t)&&(void 0===t||\"object\"==typeof t))try{var e=u.call(t);return(\"[object HTMLAllCollection]\"===e||\"[object HTML document.all class]\"===e||\"[object HTMLCollection]\"===e||\"[object Object]\"===e)&&null==t(\"\")}catch(t){}return!1})}t.exports=o?function isCallable(t){if(l(t))return!0;if(!t)return!1;if(\"function\"!=typeof t&&\"object\"!=typeof t)return!1;try{o(t,null,e)}catch(t){if(t!==r)return!1}return!a(t)&&s(t)}:function isCallable(t){if(l(t))return!0;if(!t)return!1;if(\"function\"!=typeof t&&\"object\"!=typeof t)return!1;if(c)return s(t);if(a(t))return!1;var e=u.call(t);return!(\"[object Function]\"!==e&&\"[object GeneratorFunction]\"!==e&&!/^\\[object HTML/.test(e))&&s(t)}},9612:t=>{\"use strict\";t.exports=Object},9675:t=>{\"use strict\";t.exports=TypeError},9698:t=>{var e=RegExp(\"[\\\\u200d\\\\ud800-\\\\udfff\\\\u0300-\\\\u036f\\\\ufe20-\\\\ufe2f\\\\u20d0-\\\\u20ff\\\\ufe0e\\\\ufe0f]\");t.exports=function hasUnicode(t){return e.test(t)}},9709:(t,e,r)=>{\"use strict\";var n=r(3034);t.exports=n},9724:(t,e,r)=>{\"use strict\";var n=r(1907),o=r(9298),i=n({}.hasOwnProperty);t.exports=Object.hasOwn||function hasOwn(t,e){return i(o(t),e)}},9748:(t,e,r)=>{\"use strict\";r(1340);var n=r(2046);t.exports=n.Object.assign},9770:t=>{t.exports=function arrayFilter(t,e){for(var r=-1,n=null==t?0:t.length,o=0,i=[];++r<n;){var a=t[r];e(a,r,t)&&(i[o++]=a)}return i}},9817:t=>{t.exports=function stackHas(t){return this.__data__.has(t)}},9846:(t,e,r)=>{\"use strict\";var n=r(798),o=r(8828),i=r(5951).String;t.exports=!!Object.getOwnPropertySymbols&&!o((function(){var t=Symbol(\"symbol detection\");return!i(t)||!(Object(t)instanceof Symbol)||!Symbol.sham&&n&&n<41}))},9935:t=>{t.exports=function stubFalse(){return!1}},9957:(t,e,r)=>{\"use strict\";var n=Function.prototype.call,o=Object.prototype.hasOwnProperty,i=r(6743);t.exports=i.call(n,o)}},e={};function __webpack_require__(r){var n=e[r];if(void 0!==n)return n.exports;var o=e[r]={id:r,loaded:!1,exports:{}};return t[r].call(o.exports,o,o.exports,__webpack_require__),o.loaded=!0,o.exports}__webpack_require__.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return __webpack_require__.d(e,{a:e}),e},__webpack_require__.d=(t,e)=>{for(var r in e)__webpack_require__.o(e,r)&&!__webpack_require__.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},__webpack_require__.g=function(){if(\"object\"==typeof globalThis)return globalThis;try{return this||new Function(\"return this\")()}catch(t){if(\"object\"==typeof window)return window}}(),__webpack_require__.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),__webpack_require__.r=t=>{\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})},__webpack_require__.nmd=t=>(t.paths=[],t.children||(t.children=[]),t);var r={};return(()=>{\"use strict\";__webpack_require__.d(r,{default:()=>ce});var t={};__webpack_require__.r(t),__webpack_require__.d(t,{TOGGLE_CONFIGS:()=>ee,UPDATE_CONFIGS:()=>te,downloadConfig:()=>downloadConfig,getConfigByUrl:()=>getConfigByUrl,loaded:()=>loaded,toggle:()=>toggle,update:()=>update});var e={};__webpack_require__.r(e),__webpack_require__.d(e,{get:()=>get});var n=__webpack_require__(6540);class StandaloneLayout extends n.Component{render(){const{getComponent:t}=this.props,e=t(\"Container\"),r=t(\"Row\"),o=t(\"Col\"),i=t(\"Topbar\",!0),a=t(\"BaseLayout\",!0),s=t(\"onlineValidatorBadge\",!0);return n.createElement(e,{className:\"swagger-ui\"},i?n.createElement(i,null):null,n.createElement(a,null),n.createElement(r,null,n.createElement(o,null,n.createElement(s,null))))}}const o=StandaloneLayout,stadalone_layout=()=>({components:{StandaloneLayout:o}});var i=__webpack_require__(9404),a=__webpack_require__.n(i);__webpack_require__(4058),__webpack_require__(5808),__webpack_require__(104),__webpack_require__(7309),__webpack_require__(2426),__webpack_require__(5288),__webpack_require__(1882),__webpack_require__(2205),__webpack_require__(3209),__webpack_require__(2802);const s=function makeWindow(){var t={location:{},history:{},open:()=>{},close:()=>{},File:function(){},FormData:function(){}};if(\"undefined\"==typeof window)return t;try{t=window;for(var e of[\"File\",\"Blob\",\"FormData\"])e in window&&(t[e]=window[e])}catch(t){console.error(t)}return t}();a().Set.of(\"type\",\"format\",\"items\",\"default\",\"maximum\",\"exclusiveMaximum\",\"minimum\",\"exclusiveMinimum\",\"maxLength\",\"minLength\",\"pattern\",\"maxItems\",\"minItems\",\"uniqueItems\",\"enum\",\"multipleOf\");__webpack_require__(8287).Buffer;const parseSearch=()=>{const t=new URLSearchParams(s.location.search);return Object.fromEntries(t)};class TopBar extends n.Component{constructor(t,e){super(t,e),this.state={url:t.specSelectors.url(),selectedIndex:0}}UNSAFE_componentWillReceiveProps(t){this.setState({url:t.specSelectors.url()})}onUrlChange=t=>{let{target:{value:e}}=t;this.setState({url:e})};flushAuthData(){const{persistAuthorization:t}=this.props.getConfigs();t||this.props.authActions.restoreAuthorization({authorized:{}})}loadSpec=t=>{this.flushAuthData(),this.props.specActions.updateUrl(t),this.props.specActions.download(t)};onUrlSelect=t=>{let e=t.target.value||t.target.href;this.loadSpec(e),this.setSelectedUrl(e),t.preventDefault()};downloadUrl=t=>{this.loadSpec(this.state.url),t.preventDefault()};setSearch=t=>{let e=parseSearch();e[\"urls.primaryName\"]=t.name;const r=`${window.location.protocol}//${window.location.host}${window.location.pathname}`;window&&window.history&&window.history.pushState&&window.history.replaceState(null,\"\",`${r}?${(t=>{const e=new URLSearchParams(Object.entries(t));return String(e)})(e)}`)};setSelectedUrl=t=>{const e=this.props.getConfigs().urls||[];e&&e.length&&t&&e.forEach(((e,r)=>{e.url===t&&(this.setState({selectedIndex:r}),this.setSearch(e))}))};componentDidMount(){const t=this.props.getConfigs(),e=t.urls||[];if(e&&e.length){var r=this.state.selectedIndex;let n=parseSearch()[\"urls.primaryName\"]||t.urls.primaryName;n&&e.forEach(((t,e)=>{t.name===n&&(this.setState({selectedIndex:e}),r=e)})),this.loadSpec(e[r].url)}}onFilterChange=t=>{let{target:{value:e}}=t;this.props.layoutActions.updateFilter(e)};render(){let{getComponent:t,specSelectors:e,getConfigs:r}=this.props;const o=t(\"Button\"),i=t(\"Link\"),a=t(\"Logo\"),s=t(\"DarkModeToggle\");let u=\"loading\"===e.loadingStatus();const c=[\"download-url-input\"];\"failed\"===e.loadingStatus()&&c.push(\"failed\"),u&&c.push(\"loading\");const{urls:f}=r();let l=[],p=null;if(f){let t=[];f.forEach(((e,r)=>{t.push(n.createElement(\"option\",{key:r,value:e.url},e.name))})),l.push(n.createElement(\"label\",{className:\"select-label\",htmlFor:\"select\"},n.createElement(\"span\",null,\"Select a definition\"),n.createElement(\"select\",{id:\"select\",disabled:u,onChange:this.onUrlSelect,value:f[this.state.selectedIndex].url},t)))}else p=this.downloadUrl,l.push(n.createElement(\"input\",{className:c.join(\" \"),type:\"text\",onChange:this.onUrlChange,value:this.state.url,disabled:u,id:\"download-url-input\"})),l.push(n.createElement(o,{className:\"download-url-button\",onClick:this.downloadUrl},\"Explore\"));return n.createElement(\"div\",{className:\"topbar\"},n.createElement(\"div\",{className:\"wrapper\"},n.createElement(\"div\",{className:\"topbar-wrapper\"},n.createElement(i,null,n.createElement(a,null)),n.createElement(\"form\",{className:\"download-url-wrapper\",onSubmit:p},l.map(((t,e)=>(0,n.cloneElement)(t,{key:e})))),n.createElement(s,null))))}}const u=TopBar;var c,f,l,p,h,d,y,_,g,m,v,b,w,I,x,B,k,C,j,q,L,P,U,D,z,W,V,K,$,H,Y,Z;function _extends(){return _extends=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var n in r)({}).hasOwnProperty.call(r,n)&&(t[n]=r[n])}return t},_extends.apply(null,arguments)}const logo_small=t=>n.createElement(\"svg\",_extends({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 407 116\"},t),c||(c=n.createElement(\"defs\",null,n.createElement(\"clipPath\",{id:\"logo_small_svg__clip-SW_TM-logo-on-dark\"},n.createElement(\"path\",{d:\"M0 0h407v116H0z\"})),n.createElement(\"style\",null,\".logo_small_svg__cls-2{fill:#fff}.logo_small_svg__cls-3{fill:#85ea2d}\"))),n.createElement(\"g\",{id:\"logo_small_svg__SW_TM-logo-on-dark\",style:{clipPath:\"url(#logo_small_svg__clip-SW_TM-logo-on-dark)\"}},n.createElement(\"g\",{id:\"logo_small_svg__SW_In-Product\",transform:\"translate(-.301)\"},f||(f=n.createElement(\"path\",{id:\"logo_small_svg__Path_2936\",d:\"M359.15 70.674h-.7v-3.682h-1.26v-.6h3.219v.6h-1.259Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2936\"})),l||(l=n.createElement(\"path\",{id:\"logo_small_svg__Path_2937\",d:\"m363.217 70.674-1.242-3.574h-.023q.05.8.05 1.494v2.083h-.636v-4.286h.987l1.19 3.407h.017l1.225-3.407h.99v4.283h-.675v-2.118a30 30 0 0 1 .044-1.453h-.023l-1.286 3.571Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2937\"})),p||(p=n.createElement(\"path\",{id:\"logo_small_svg__Path_2938\",d:\"M50.328 97.669a47.642 47.642 0 1 1 47.643-47.642 47.64 47.64 0 0 1-47.643 47.642\",className:\"logo_small_svg__cls-3\",\"data-name\":\"Path 2938\"})),h||(h=n.createElement(\"path\",{id:\"logo_small_svg__Path_2939\",d:\"M50.328 4.769A45.258 45.258 0 1 1 5.07 50.027 45.26 45.26 0 0 1 50.328 4.769m0-4.769a50.027 50.027 0 1 0 50.027 50.027A50.027 50.027 0 0 0 50.328 0\",className:\"logo_small_svg__cls-3\",\"data-name\":\"Path 2939\"})),n.createElement(\"path\",{id:\"logo_small_svg__Path_2940\",d:\"M31.8 33.854c-.154 1.712.058 3.482-.057 5.213a43 43 0 0 1-.693 5.156 9.53 9.53 0 0 1-4.1 5.829c4.079 2.654 4.54 6.771 4.81 10.946.135 2.25.077 4.52.308 6.752.173 1.731.846 2.174 2.636 2.231.73.02 1.48 0 2.327 0v5.349c-5.29.9-9.657-.6-10.734-5.079a31 31 0 0 1-.654-5c-.117-1.789.076-3.578-.058-5.367-.386-4.906-1.02-6.56-5.713-6.791v-6.1a9 9 0 0 1 1.028-.173c2.577-.135 3.674-.924 4.231-3.463a29 29 0 0 0 .481-4.329 82 82 0 0 1 .6-8.406c.673-3.982 3.136-5.906 7.234-6.137 1.154-.057 2.327 0 3.655 0v5.464c-.558.038-1.039.115-1.539.115-3.336-.115-3.51 1.02-3.762 3.79m6.406 12.658h-.077a3.515 3.515 0 1 0-.346 7.021h.231a3.46 3.46 0 0 0 3.655-3.251v-.192a3.523 3.523 0 0 0-3.461-3.578Zm12.062 0a3.373 3.373 0 0 0-3.482 3.251 2 2 0 0 0 .02.327 3.3 3.3 0 0 0 3.578 3.443 3.263 3.263 0 0 0 3.443-3.558 3.308 3.308 0 0 0-3.557-3.463Zm12.351 0a3.59 3.59 0 0 0-3.655 3.482 3.53 3.53 0 0 0 3.536 3.539h.039c1.769.309 3.559-1.4 3.674-3.462a3.57 3.57 0 0 0-3.6-3.559Zm16.948.288c-2.232-.1-3.348-.846-3.9-2.962a21.5 21.5 0 0 1-.635-4.136c-.154-2.578-.135-5.175-.308-7.753-.4-6.117-4.828-8.252-11.254-7.195v5.31c1.019 0 1.808 0 2.6.019 1.366.019 2.4.539 2.539 2.059.135 1.385.135 2.789.27 4.193.269 2.79.422 5.618.9 8.369a8.72 8.72 0 0 0 3.921 5.348c-3.4 2.289-4.406 5.559-4.578 9.234-.1 2.52-.154 5.059-.289 7.6-.115 2.308-.923 3.058-3.251 3.116-.654.019-1.289.077-2.019.115v5.445c1.365 0 2.616.077 3.866 0 3.886-.231 6.233-2.117 7-5.887A49 49 0 0 0 75 63.4c.135-1.923.116-3.866.308-5.771.289-2.982 1.655-4.213 4.636-4.4a4 4 0 0 0 .828-.192v-6.1c-.5-.058-.843-.115-1.208-.135Z\",\"data-name\":\"Path 2940\",style:{fill:\"#173647\"}}),d||(d=n.createElement(\"path\",{id:\"logo_small_svg__Path_2941\",d:\"M152.273 58.122a11.23 11.23 0 0 1-4.384 9.424q-4.383 3.382-11.9 3.382-8.14 0-12.524-2.1V63.7a33 33 0 0 0 6.137 1.879 32.3 32.3 0 0 0 6.575.689q5.322 0 8.015-2.02a6.63 6.63 0 0 0 2.692-5.62 7.2 7.2 0 0 0-.954-3.9 8.9 8.9 0 0 0-3.194-2.8 44.6 44.6 0 0 0-6.81-2.911q-6.387-2.286-9.126-5.417a11.96 11.96 0 0 1-2.74-8.172A10.16 10.16 0 0 1 128.039 27q3.977-3.131 10.52-3.131a31 31 0 0 1 12.555 2.5L149.455 31a28.4 28.4 0 0 0-11.021-2.38 10.67 10.67 0 0 0-6.606 1.816 5.98 5.98 0 0 0-2.38 5.041 7.7 7.7 0 0 0 .877 3.9 8.24 8.24 0 0 0 2.959 2.786 36.7 36.7 0 0 0 6.371 2.8q7.2 2.566 9.91 5.51a10.84 10.84 0 0 1 2.708 7.649\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2941\"})),y||(y=n.createElement(\"path\",{id:\"logo_small_svg__Path_2942\",d:\"M185.288 70.3 179 50.17q-.594-1.848-2.222-8.391h-.251q-1.252 5.479-2.192 8.453L167.849 70.3h-6.011l-9.361-34.315h5.447q3.318 12.931 5.057 19.693a80 80 0 0 1 1.988 9.111h.25q.345-1.785 1.112-4.618t1.33-4.493l6.294-19.693h5.635l6.137 19.693a66 66 0 0 1 2.379 9.048h.251a33 33 0 0 1 .673-3.475q.548-2.347 6.528-25.266h5.385L191.456 70.3Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2942\"})),_||(_=n.createElement(\"path\",{id:\"logo_small_svg__Path_2943\",d:\"m225.115 70.3-1.033-4.885h-.25a14.45 14.45 0 0 1-5.119 4.368 15.6 15.6 0 0 1-6.372 1.143q-5.1 0-8-2.63t-2.9-7.483q0-10.4 16.626-10.9l5.823-.188V47.6q0-4.038-1.738-5.964t-5.552-1.923a22.6 22.6 0 0 0-9.706 2.63l-1.6-3.977a24.4 24.4 0 0 1 5.557-2.16 24 24 0 0 1 6.058-.783q6.136 0 9.1 2.724t2.959 8.735V70.3Zm-11.741-3.663a10.55 10.55 0 0 0 7.626-2.66 9.85 9.85 0 0 0 2.771-7.451v-3.1l-5.2.219q-6.2.219-8.939 1.926a5.8 5.8 0 0 0-2.74 5.306 5.35 5.35 0 0 0 1.707 4.29 7.08 7.08 0 0 0 4.775 1.472Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2943\"})),g||(g=n.createElement(\"path\",{id:\"logo_small_svg__Path_2944\",d:\"M264.6 35.987v3.287l-6.356.752a11.16 11.16 0 0 1 2.255 6.856 10.15 10.15 0 0 1-3.444 8.047q-3.444 3-9.456 3a15.7 15.7 0 0 1-2.88-.25Q241.4 59.438 241.4 62.1a2.24 2.24 0 0 0 1.159 2.082 8.46 8.46 0 0 0 3.976.673h6.074q5.573 0 8.563 2.348a8.16 8.16 0 0 1 2.99 6.825 9.74 9.74 0 0 1-4.571 8.688q-4.572 2.989-13.338 2.99-6.732 0-10.379-2.5a8.09 8.09 0 0 1-3.647-7.076 7.95 7.95 0 0 1 2-5.417 10.2 10.2 0 0 1 5.636-3.1 5.43 5.43 0 0 1-2.207-1.847 4.9 4.9 0 0 1-.893-2.912 5.53 5.53 0 0 1 1-3.288 10.5 10.5 0 0 1 3.162-2.723 9.28 9.28 0 0 1-4.336-3.726 10.95 10.95 0 0 1-1.675-6.012q0-5.634 3.382-8.688t9.58-3.052a17.4 17.4 0 0 1 4.853.626Zm-27.367 40.075a4.66 4.66 0 0 0 2.348 4.227 12.97 12.97 0 0 0 6.732 1.44q6.543 0 9.69-1.956a5.99 5.99 0 0 0 3.147-5.307q0-2.787-1.723-3.867t-6.481-1.08h-6.23a8.2 8.2 0 0 0-5.51 1.69 6.04 6.04 0 0 0-1.973 4.853m2.818-29.086a6.98 6.98 0 0 0 2.035 5.448 8.12 8.12 0 0 0 5.667 1.847q7.608 0 7.608-7.389 0-7.733-7.7-7.733a7.63 7.63 0 0 0-5.635 1.972q-1.976 1.973-1.975 5.855\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2944\"})),m||(m=n.createElement(\"path\",{id:\"logo_small_svg__Path_2945\",d:\"M299.136 35.987v3.287l-6.356.752a11.17 11.17 0 0 1 2.254 6.856 10.15 10.15 0 0 1-3.444 8.047q-3.444 3-9.455 3a15.7 15.7 0 0 1-2.88-.25q-3.32 1.754-3.319 4.415a2.24 2.24 0 0 0 1.158 2.082 8.46 8.46 0 0 0 3.976.673h6.074q5.574 0 8.563 2.348a8.16 8.16 0 0 1 2.99 6.825 9.74 9.74 0 0 1-4.571 8.688q-4.57 2.989-13.337 2.99-6.732 0-10.379-2.5a8.09 8.09 0 0 1-3.648-7.076 7.95 7.95 0 0 1 2-5.417 10.2 10.2 0 0 1 5.636-3.1 5.43 5.43 0 0 1-2.208-1.847 4.9 4.9 0 0 1-.892-2.912 5.53 5.53 0 0 1 1-3.288 10.5 10.5 0 0 1 3.162-2.723 9.27 9.27 0 0 1-4.336-3.726 10.95 10.95 0 0 1-1.675-6.012q0-5.634 3.381-8.688t9.581-3.052a17.4 17.4 0 0 1 4.853.626Zm-27.364 40.075a4.66 4.66 0 0 0 2.348 4.227 12.97 12.97 0 0 0 6.731 1.44q6.544 0 9.691-1.956a5.99 5.99 0 0 0 3.146-5.307q0-2.787-1.722-3.867t-6.481-1.08h-6.23a8.2 8.2 0 0 0-5.511 1.69 6.04 6.04 0 0 0-1.972 4.853m2.818-29.086a6.98 6.98 0 0 0 2.035 5.448 8.12 8.12 0 0 0 5.667 1.847q7.607 0 7.608-7.389 0-7.733-7.7-7.733a7.63 7.63 0 0 0-5.635 1.972q-1.975 1.973-1.975 5.855\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2945\"})),v||(v=n.createElement(\"path\",{id:\"logo_small_svg__Path_2946\",d:\"M316.778 70.928q-7.608 0-12.007-4.634t-4.4-12.868q0-8.3 4.086-13.181a13.57 13.57 0 0 1 10.974-4.884 12.94 12.94 0 0 1 10.207 4.239q3.762 4.247 3.762 11.2v3.287h-23.643q.156 6.044 3.053 9.174t8.156 3.131a27.6 27.6 0 0 0 10.958-2.317v4.634a27.5 27.5 0 0 1-5.213 1.706 29.3 29.3 0 0 1-5.933.513m-1.409-31.215a8.49 8.49 0 0 0-6.591 2.692 12.4 12.4 0 0 0-2.9 7.452h17.94q0-4.916-2.191-7.53a7.71 7.71 0 0 0-6.258-2.614\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2946\"})),b||(b=n.createElement(\"path\",{id:\"logo_small_svg__Path_2947\",d:\"M350.9 35.361a20.4 20.4 0 0 1 4.1.375l-.721 4.822a17.7 17.7 0 0 0-3.757-.47 9.14 9.14 0 0 0-7.122 3.382 12.33 12.33 0 0 0-2.959 8.422V70.3h-5.2V35.987h4.29l.6 6.356h.25a15.1 15.1 0 0 1 4.6-5.166 10.36 10.36 0 0 1 5.919-1.816\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2947\"})),w||(w=n.createElement(\"path\",{id:\"logo_small_svg__Path_2948\",d:\"M255.857 96.638s-3.43-.391-4.85-.391c-2.058 0-3.111.735-3.111 2.18 0 1.568.882 1.935 3.748 2.719 3.527.98 4.8 1.911 4.8 4.777 0 3.675-2.3 5.267-5.61 5.267a36 36 0 0 1-5.487-.662l.27-2.18s3.306.441 5.046.441c2.082 0 3.037-.931 3.037-2.7 0-1.421-.759-1.91-3.331-2.523-3.626-.93-5.193-2.033-5.193-4.948 0-3.381 2.229-4.776 5.585-4.776a37 37 0 0 1 5.315.587Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2948\"})),I||(I=n.createElement(\"path\",{id:\"logo_small_svg__Path_2949\",d:\"M262.967 94.14h4.733l3.748 13.106L275.2 94.14h4.752v16.78H277.2v-14.5h-.145l-4.191 13.816h-2.842l-4.191-13.816h-.145v14.5h-2.719Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2949\"})),x||(x=n.createElement(\"path\",{id:\"logo_small_svg__Path_2950\",d:\"M322.057 94.14H334.3v2.425h-4.728v14.355h-2.743V96.565h-4.777Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2950\"})),B||(B=n.createElement(\"path\",{id:\"logo_small_svg__Path_2951\",d:\"M346.137 94.14c3.332 0 5.12 1.249 5.12 4.361 0 2.033-.637 3.037-1.984 3.772 1.445.563 2.4 1.592 2.4 3.9 0 3.43-2.081 4.752-5.339 4.752h-6.566V94.14Zm-3.65 2.352v4.8h3.6c1.666 0 2.4-.832 2.4-2.474 0-1.617-.833-2.327-2.5-2.327Zm0 7.1v4.973h3.7c1.689 0 2.694-.539 2.694-2.548 0-1.911-1.421-2.425-2.744-2.425Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2951\"})),k||(k=n.createElement(\"path\",{id:\"logo_small_svg__Path_2952\",d:\"M358.414 94.14H369v2.377h-7.864v4.751h6.394v2.332h-6.394v4.924H369v2.4h-10.586Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2952\"})),C||(C=n.createElement(\"path\",{id:\"logo_small_svg__Path_2953\",d:\"M378.747 94.14h5.414l4.164 16.78h-2.744l-1.239-4.92h-5.777l-1.239 4.923h-2.719Zm.361 9.456h4.708l-1.737-7.178h-1.225Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2953\"})),j||(j=n.createElement(\"path\",{id:\"logo_small_svg__Path_2954\",d:\"M397.1 105.947v4.973h-2.719V94.14h6.37c3.7 0 5.683 2.12 5.683 5.843 0 2.376-.956 4.519-2.744 5.352l2.769 5.585h-2.989l-2.426-4.973Zm3.651-9.455H397.1v7.1h3.7c2.057 0 2.841-1.85 2.841-3.589 0-1.9-.934-3.511-2.894-3.511Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2954\"})),q||(q=n.createElement(\"path\",{id:\"logo_small_svg__Path_2955\",d:\"M290.013 94.14h5.413l4.164 16.78h-2.743l-1.239-4.92h-5.777l-1.239 4.923h-2.719Zm.361 9.456h4.707l-1.737-7.178h-1.225Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2955\"})),L||(L=n.createElement(\"path\",{id:\"logo_small_svg__Path_2956\",d:\"M308.362 105.947v4.973h-2.719V94.14h6.369c3.7 0 5.683 2.12 5.683 5.843 0 2.376-.955 4.519-2.743 5.352l2.768 5.585h-2.989l-2.425-4.973Zm3.65-9.455h-3.65v7.1h3.7c2.058 0 2.841-1.85 2.841-3.589-.003-1.903-.931-3.511-2.891-3.511\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2956\"})),P||(P=n.createElement(\"path\",{id:\"logo_small_svg__Path_2957\",d:\"M130.606 107.643a3.02 3.02 0 0 1-1.18 2.537 5.1 5.1 0 0 1-3.2.91 8 8 0 0 1-3.371-.564v-1.383a9 9 0 0 0 1.652.506 8.7 8.7 0 0 0 1.77.186 3.57 3.57 0 0 0 2.157-.544 1.78 1.78 0 0 0 .725-1.512 1.95 1.95 0 0 0-.257-1.05 2.4 2.4 0 0 0-.86-.754 12 12 0 0 0-1.833-.784 5.84 5.84 0 0 1-2.456-1.458 3.2 3.2 0 0 1-.738-2.2 2.74 2.74 0 0 1 1.071-2.267 4.44 4.44 0 0 1 2.831-.843 8.3 8.3 0 0 1 3.38.675l-.447 1.247a7.6 7.6 0 0 0-2.966-.641 2.88 2.88 0 0 0-1.779.489 1.61 1.61 0 0 0-.64 1.357 2.1 2.1 0 0 0 .236 1.049 2.2 2.2 0 0 0 .8.75 10 10 0 0 0 1.715.754 6.8 6.8 0 0 1 2.667 1.483 2.92 2.92 0 0 1 .723 2.057\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2957\"})),U||(U=n.createElement(\"path\",{id:\"logo_small_svg__Path_2958\",d:\"M134.447 101.686v5.991a2.4 2.4 0 0 0 .515 1.686 2.1 2.1 0 0 0 1.609.556 2.63 2.63 0 0 0 2.12-.792 4 4 0 0 0 .67-2.587v-4.854h1.4v9.236H139.6l-.2-1.239h-.075a2.8 2.8 0 0 1-1.193 1.045 4 4 0 0 1-1.74.362 3.53 3.53 0 0 1-2.524-.8 3.4 3.4 0 0 1-.839-2.562v-6.042Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2958\"})),D||(D=n.createElement(\"path\",{id:\"logo_small_svg__Path_2959\",d:\"M148.206 111.09a4 4 0 0 1-1.647-.333 3.1 3.1 0 0 1-1.252-1.023h-.1a12 12 0 0 1 .1 1.533v3.8h-1.4v-13.381h1.137l.194 1.264h.067a3.26 3.26 0 0 1 1.256-1.1 3.8 3.8 0 0 1 1.643-.337 3.41 3.41 0 0 1 2.836 1.256 6.68 6.68 0 0 1-.017 7.057 3.42 3.42 0 0 1-2.817 1.264m-.2-8.385a2.48 2.48 0 0 0-2.048.784 4.04 4.04 0 0 0-.649 2.494v.312a4.63 4.63 0 0 0 .649 2.785 2.47 2.47 0 0 0 2.082.839 2.16 2.16 0 0 0 1.875-.969 4.6 4.6 0 0 0 .678-2.671 4.43 4.43 0 0 0-.678-2.651 2.23 2.23 0 0 0-1.915-.923Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2959\"})),z||(z=n.createElement(\"path\",{id:\"logo_small_svg__Path_2960\",d:\"M159.039 111.09a4 4 0 0 1-1.647-.333 3.1 3.1 0 0 1-1.252-1.023h-.1a12 12 0 0 1 .1 1.533v3.8h-1.4v-13.381h1.137l.194 1.264h.067a3.26 3.26 0 0 1 1.256-1.1 3.8 3.8 0 0 1 1.643-.337 3.41 3.41 0 0 1 2.836 1.256 6.68 6.68 0 0 1-.017 7.057 3.42 3.42 0 0 1-2.817 1.264m-.2-8.385a2.48 2.48 0 0 0-2.048.784 4.04 4.04 0 0 0-.649 2.494v.312a4.63 4.63 0 0 0 .649 2.785 2.47 2.47 0 0 0 2.082.839 2.16 2.16 0 0 0 1.875-.969 4.6 4.6 0 0 0 .678-2.671 4.43 4.43 0 0 0-.678-2.651 2.23 2.23 0 0 0-1.911-.923Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2960\"})),W||(W=n.createElement(\"path\",{id:\"logo_small_svg__Path_2961\",d:\"M173.612 106.3a5.1 5.1 0 0 1-1.137 3.527 4 4 0 0 1-3.143 1.268 4.17 4.17 0 0 1-2.2-.581 3.84 3.84 0 0 1-1.483-1.669 5.8 5.8 0 0 1-.522-2.545 5.1 5.1 0 0 1 1.129-3.518 4 4 0 0 1 3.135-1.26 3.9 3.9 0 0 1 3.08 1.29 5.07 5.07 0 0 1 1.141 3.488m-7.036 0a4.4 4.4 0 0 0 .708 2.7 2.81 2.81 0 0 0 4.167 0 4.37 4.37 0 0 0 .712-2.7 4.3 4.3 0 0 0-.712-2.675 2.5 2.5 0 0 0-2.1-.915 2.46 2.46 0 0 0-2.072.9 4.33 4.33 0 0 0-.7 2.69Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2961\"})),V||(V=n.createElement(\"path\",{id:\"logo_small_svg__Path_2962\",d:\"M180.525 101.517a5.5 5.5 0 0 1 1.1.1l-.194 1.3a4.8 4.8 0 0 0-1.011-.127 2.46 2.46 0 0 0-1.917.911 3.32 3.32 0 0 0-.8 2.267v4.955h-1.4v-9.236h1.154l.16 1.71h.068a4.05 4.05 0 0 1 1.238-1.39 2.8 2.8 0 0 1 1.6-.49Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2962\"})),K||(K=n.createElement(\"path\",{id:\"logo_small_svg__Path_2963\",d:\"M187.363 109.936a4.5 4.5 0 0 0 .716-.055 4 4 0 0 0 .548-.114v1.07a2.5 2.5 0 0 1-.67.181 5 5 0 0 1-.8.072q-2.68 0-2.68-2.823v-5.494h-1.323v-.673l1.323-.582.59-1.972h.809v2.141h2.68v1.087h-2.68v5.435a1.87 1.87 0 0 0 .4 1.281 1.38 1.38 0 0 0 1.087.446\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2963\"})),$||($=n.createElement(\"path\",{id:\"logo_small_svg__Path_2964\",d:\"M194.538 111.09a4.24 4.24 0 0 1-3.231-1.247 4.82 4.82 0 0 1-1.184-3.463 5.36 5.36 0 0 1 1.1-3.548 3.65 3.65 0 0 1 2.954-1.315 3.48 3.48 0 0 1 2.747 1.142 4.38 4.38 0 0 1 1.011 3.013v.885h-6.362a3.66 3.66 0 0 0 .822 2.469 2.84 2.84 0 0 0 2.2.843 7.4 7.4 0 0 0 2.949-.624v1.247a7.4 7.4 0 0 1-1.4.459 8 8 0 0 1-1.6.139Zm-.379-8.4a2.29 2.29 0 0 0-1.774.725 3.34 3.34 0 0 0-.779 2.006h4.828a3.07 3.07 0 0 0-.59-2.027 2.08 2.08 0 0 0-1.685-.706Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2964\"})),H||(H=n.createElement(\"path\",{id:\"logo_small_svg__Path_2965\",d:\"M206.951 109.683h-.076a3.29 3.29 0 0 1-2.9 1.407 3.43 3.43 0 0 1-2.819-1.239 5.45 5.45 0 0 1-1.006-3.522 5.54 5.54 0 0 1 1.011-3.548 3.4 3.4 0 0 1 2.814-1.264 3.36 3.36 0 0 1 2.883 1.365h.109l-.059-.665-.034-.649v-3.759h1.4v13.113h-1.138Zm-2.8.236a2.55 2.55 0 0 0 2.078-.779 3.95 3.95 0 0 0 .644-2.516v-.3a4.64 4.64 0 0 0-.653-2.8 2.48 2.48 0 0 0-2.086-.839 2.14 2.14 0 0 0-1.883.957 4.76 4.76 0 0 0-.653 2.7 4.55 4.55 0 0 0 .649 2.671 2.2 2.2 0 0 0 1.906.906Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2965\"})),Y||(Y=n.createElement(\"path\",{id:\"logo_small_svg__Path_2966\",d:\"M220.712 101.534a3.44 3.44 0 0 1 2.827 1.243 6.65 6.65 0 0 1-.009 7.053 3.42 3.42 0 0 1-2.818 1.26 4 4 0 0 1-1.648-.333 3.1 3.1 0 0 1-1.251-1.023h-.1l-.295 1.188h-1V97.809h1.4V101q0 1.069-.068 1.921h.068a3.32 3.32 0 0 1 2.894-1.387m-.2 1.171a2.44 2.44 0 0 0-2.064.822 6.34 6.34 0 0 0 .017 5.553 2.46 2.46 0 0 0 2.081.839 2.16 2.16 0 0 0 1.922-.94 4.83 4.83 0 0 0 .632-2.7 4.64 4.64 0 0 0-.632-2.689 2.24 2.24 0 0 0-1.959-.885Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2966\"})),Z||(Z=n.createElement(\"path\",{id:\"logo_small_svg__Path_2967\",d:\"M225.758 101.686h1.5l2.023 5.267a20 20 0 0 1 .826 2.6h.067q.109-.431.459-1.471t2.288-6.4h1.5l-3.969 10.518a5.25 5.25 0 0 1-1.378 2.212 2.93 2.93 0 0 1-1.934.653 5.7 5.7 0 0 1-1.264-.143V113.8a5 5 0 0 0 1.037.1 2.136 2.136 0 0 0 2.056-1.618l.514-1.314Z\",className:\"logo_small_svg__cls-2\",\"data-name\":\"Path 2967\"}))))),components_Logo=()=>n.createElement(logo_small,{height:\"40\"});var J;function lightbulb_extends(){return lightbulb_extends=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var n in r)({}).hasOwnProperty.call(r,n)&&(t[n]=r[n])}return t},lightbulb_extends.apply(null,arguments)}const lightbulb=t=>n.createElement(\"svg\",lightbulb_extends({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 24 24\"},t),J||(J=n.createElement(\"path\",{d:\"M12 2a7 7 0 0 0-7 7c0 2.38 1.19 4.47 3 5.74V17a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-2.26c1.81-1.27 3-3.36 3-5.74a7 7 0 0 0-7-7M9 21a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1H9z\"})));var tt;function lightbulb_off_extends(){return lightbulb_off_extends=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var n in r)({}).hasOwnProperty.call(r,n)&&(t[n]=r[n])}return t},lightbulb_off_extends.apply(null,arguments)}const lightbulb_off=t=>n.createElement(\"svg\",lightbulb_off_extends({xmlns:\"http://www.w3.org/2000/svg\",viewBox:\"0 0 24 24\"},t),tt||(tt=n.createElement(\"path\",{d:\"M12 2C9.76 2 7.78 3.05 6.5 4.68l9.81 9.82C17.94 13.21 19 11.24 19 9a7 7 0 0 0-7-7M3.28 4 2 5.27 5.04 8.3C5 8.53 5 8.76 5 9c0 2.38 1.19 4.47 3 5.74V17a1 1 0 0 0 1 1h5.73l4 4L20 20.72zM9 20v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1z\"})));class DarkModeToggle extends n.Component{constructor(t){super(t),this.state={isDarkMode:!1},this.toggleIsDarkMode=this.toggleIsDarkMode.bind(this)}componentDidMount(){window.matchMedia(\"(prefers-color-scheme: dark)\").matches&&(document.documentElement.classList.add(\"dark-mode\"),this.setState({isDarkMode:!0}))}toggleIsDarkMode(){document.documentElement.classList.toggle(\"dark-mode\"),this.setState((t=>({isDarkMode:!t.isDarkMode})))}render(){const{isDarkMode:t}=this.state;return n.createElement(\"div\",{className:\"dark-mode-toggle\"},n.createElement(\"button\",{onClick:this.toggleIsDarkMode},t?n.createElement(lightbulb,{height:\"24\"}):n.createElement(lightbulb_off,{height:\"24\"})))}}const et=DarkModeToggle,top_bar=()=>({components:{Topbar:u,Logo:components_Logo,DarkModeToggle:et}});function isNothing(t){return null==t}var rt={isNothing,isObject:function js_yaml_isObject(t){return\"object\"==typeof t&&null!==t},toArray:function toArray(t){return Array.isArray(t)?t:isNothing(t)?[]:[t]},repeat:function repeat(t,e){var r,n=\"\";for(r=0;r<e;r+=1)n+=t;return n},isNegativeZero:function isNegativeZero(t){return 0===t&&Number.NEGATIVE_INFINITY===1/t},extend:function extend(t,e){var r,n,o,i;if(e)for(r=0,n=(i=Object.keys(e)).length;r<n;r+=1)t[o=i[r]]=e[o];return t}};function formatError(t,e){var r=\"\",n=t.reason||\"(unknown reason)\";return t.mark?(t.mark.name&&(r+='in \"'+t.mark.name+'\" '),r+=\"(\"+(t.mark.line+1)+\":\"+(t.mark.column+1)+\")\",!e&&t.mark.snippet&&(r+=\"\\n\\n\"+t.mark.snippet),n+\" \"+r):n}function YAMLException$1(t,e){Error.call(this),this.name=\"YAMLException\",this.reason=t,this.mark=e,this.message=formatError(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||\"\"}YAMLException$1.prototype=Object.create(Error.prototype),YAMLException$1.prototype.constructor=YAMLException$1,YAMLException$1.prototype.toString=function toString(t){return this.name+\": \"+formatError(this,t)};var nt=YAMLException$1;function getLine(t,e,r,n,o){var i=\"\",a=\"\",s=Math.floor(o/2)-1;return n-e>s&&(e=n-s+(i=\" ... \").length),r-n>s&&(r=n+s-(a=\" ...\").length),{str:i+t.slice(e,r).replace(/\\t/g,\"→\")+a,pos:n-e+i.length}}function padStart(t,e){return rt.repeat(\" \",e-t.length)+t}var ot=function makeSnippet(t,e){if(e=Object.create(e||null),!t.buffer)return null;e.maxLength||(e.maxLength=79),\"number\"!=typeof e.indent&&(e.indent=1),\"number\"!=typeof e.linesBefore&&(e.linesBefore=3),\"number\"!=typeof e.linesAfter&&(e.linesAfter=2);for(var r,n=/\\r?\\n|\\r|\\0/g,o=[0],i=[],a=-1;r=n.exec(t.buffer);)i.push(r.index),o.push(r.index+r[0].length),t.position<=r.index&&a<0&&(a=o.length-2);a<0&&(a=o.length-1);var s,u,c=\"\",f=Math.min(t.line+e.linesAfter,i.length).toString().length,l=e.maxLength-(e.indent+f+3);for(s=1;s<=e.linesBefore&&!(a-s<0);s++)u=getLine(t.buffer,o[a-s],i[a-s],t.position-(o[a]-o[a-s]),l),c=rt.repeat(\" \",e.indent)+padStart((t.line-s+1).toString(),f)+\" | \"+u.str+\"\\n\"+c;for(u=getLine(t.buffer,o[a],i[a],t.position,l),c+=rt.repeat(\" \",e.indent)+padStart((t.line+1).toString(),f)+\" | \"+u.str+\"\\n\",c+=rt.repeat(\"-\",e.indent+f+3+u.pos)+\"^\\n\",s=1;s<=e.linesAfter&&!(a+s>=i.length);s++)u=getLine(t.buffer,o[a+s],i[a+s],t.position-(o[a]-o[a+s]),l),c+=rt.repeat(\" \",e.indent)+padStart((t.line+s+1).toString(),f)+\" | \"+u.str+\"\\n\";return c.replace(/\\n$/,\"\")},it=[\"kind\",\"multi\",\"resolve\",\"construct\",\"instanceOf\",\"predicate\",\"represent\",\"representName\",\"defaultStyle\",\"styleAliases\"],at=[\"scalar\",\"sequence\",\"mapping\"];var st=function Type$1(t,e){if(e=e||{},Object.keys(e).forEach((function(e){if(-1===it.indexOf(e))throw new nt('Unknown option \"'+e+'\" is met in definition of \"'+t+'\" YAML type.')})),this.options=e,this.tag=t,this.kind=e.kind||null,this.resolve=e.resolve||function(){return!0},this.construct=e.construct||function(t){return t},this.instanceOf=e.instanceOf||null,this.predicate=e.predicate||null,this.represent=e.represent||null,this.representName=e.representName||null,this.defaultStyle=e.defaultStyle||null,this.multi=e.multi||!1,this.styleAliases=function compileStyleAliases(t){var e={};return null!==t&&Object.keys(t).forEach((function(r){t[r].forEach((function(t){e[String(t)]=r}))})),e}(e.styleAliases||null),-1===at.indexOf(this.kind))throw new nt('Unknown kind \"'+this.kind+'\" is specified for \"'+t+'\" YAML type.')};function compileList(t,e){var r=[];return t[e].forEach((function(t){var e=r.length;r.forEach((function(r,n){r.tag===t.tag&&r.kind===t.kind&&r.multi===t.multi&&(e=n)})),r[e]=t})),r}function Schema$1(t){return this.extend(t)}Schema$1.prototype.extend=function extend(t){var e=[],r=[];if(t instanceof st)r.push(t);else if(Array.isArray(t))r=r.concat(t);else{if(!t||!Array.isArray(t.implicit)&&!Array.isArray(t.explicit))throw new nt(\"Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })\");t.implicit&&(e=e.concat(t.implicit)),t.explicit&&(r=r.concat(t.explicit))}e.forEach((function(t){if(!(t instanceof st))throw new nt(\"Specified list of YAML types (or a single Type object) contains a non-Type object.\");if(t.loadKind&&\"scalar\"!==t.loadKind)throw new nt(\"There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.\");if(t.multi)throw new nt(\"There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.\")})),r.forEach((function(t){if(!(t instanceof st))throw new nt(\"Specified list of YAML types (or a single Type object) contains a non-Type object.\")}));var n=Object.create(Schema$1.prototype);return n.implicit=(this.implicit||[]).concat(e),n.explicit=(this.explicit||[]).concat(r),n.compiledImplicit=compileList(n,\"implicit\"),n.compiledExplicit=compileList(n,\"explicit\"),n.compiledTypeMap=function compileMap(){var t,e,r={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function collectType(t){t.multi?(r.multi[t.kind].push(t),r.multi.fallback.push(t)):r[t.kind][t.tag]=r.fallback[t.tag]=t}for(t=0,e=arguments.length;t<e;t+=1)arguments[t].forEach(collectType);return r}(n.compiledImplicit,n.compiledExplicit),n};var ut=Schema$1,ct=new st(\"tag:yaml.org,2002:str\",{kind:\"scalar\",construct:function(t){return null!==t?t:\"\"}}),lt=new st(\"tag:yaml.org,2002:seq\",{kind:\"sequence\",construct:function(t){return null!==t?t:[]}}),pt=new st(\"tag:yaml.org,2002:map\",{kind:\"mapping\",construct:function(t){return null!==t?t:{}}}),ht=new ut({explicit:[ct,lt,pt]});var dt=new st(\"tag:yaml.org,2002:null\",{kind:\"scalar\",resolve:function resolveYamlNull(t){if(null===t)return!0;var e=t.length;return 1===e&&\"~\"===t||4===e&&(\"null\"===t||\"Null\"===t||\"NULL\"===t)},construct:function constructYamlNull(){return null},predicate:function isNull(t){return null===t},represent:{canonical:function(){return\"~\"},lowercase:function(){return\"null\"},uppercase:function(){return\"NULL\"},camelcase:function(){return\"Null\"},empty:function(){return\"\"}},defaultStyle:\"lowercase\"});var yt=new st(\"tag:yaml.org,2002:bool\",{kind:\"scalar\",resolve:function resolveYamlBoolean(t){if(null===t)return!1;var e=t.length;return 4===e&&(\"true\"===t||\"True\"===t||\"TRUE\"===t)||5===e&&(\"false\"===t||\"False\"===t||\"FALSE\"===t)},construct:function constructYamlBoolean(t){return\"true\"===t||\"True\"===t||\"TRUE\"===t},predicate:function isBoolean(t){return\"[object Boolean]\"===Object.prototype.toString.call(t)},represent:{lowercase:function(t){return t?\"true\":\"false\"},uppercase:function(t){return t?\"TRUE\":\"FALSE\"},camelcase:function(t){return t?\"True\":\"False\"}},defaultStyle:\"lowercase\"});function isOctCode(t){return 48<=t&&t<=55}function isDecCode(t){return 48<=t&&t<=57}var _t=new st(\"tag:yaml.org,2002:int\",{kind:\"scalar\",resolve:function resolveYamlInteger(t){if(null===t)return!1;var e,r,n=t.length,o=0,i=!1;if(!n)return!1;if(\"-\"!==(e=t[o])&&\"+\"!==e||(e=t[++o]),\"0\"===e){if(o+1===n)return!0;if(\"b\"===(e=t[++o])){for(o++;o<n;o++)if(\"_\"!==(e=t[o])){if(\"0\"!==e&&\"1\"!==e)return!1;i=!0}return i&&\"_\"!==e}if(\"x\"===e){for(o++;o<n;o++)if(\"_\"!==(e=t[o])){if(!(48<=(r=t.charCodeAt(o))&&r<=57||65<=r&&r<=70||97<=r&&r<=102))return!1;i=!0}return i&&\"_\"!==e}if(\"o\"===e){for(o++;o<n;o++)if(\"_\"!==(e=t[o])){if(!isOctCode(t.charCodeAt(o)))return!1;i=!0}return i&&\"_\"!==e}}if(\"_\"===e)return!1;for(;o<n;o++)if(\"_\"!==(e=t[o])){if(!isDecCode(t.charCodeAt(o)))return!1;i=!0}return!(!i||\"_\"===e)},construct:function constructYamlInteger(t){var e,r=t,n=1;if(-1!==r.indexOf(\"_\")&&(r=r.replace(/_/g,\"\")),\"-\"!==(e=r[0])&&\"+\"!==e||(\"-\"===e&&(n=-1),e=(r=r.slice(1))[0]),\"0\"===r)return 0;if(\"0\"===e){if(\"b\"===r[1])return n*parseInt(r.slice(2),2);if(\"x\"===r[1])return n*parseInt(r.slice(2),16);if(\"o\"===r[1])return n*parseInt(r.slice(2),8)}return n*parseInt(r,10)},predicate:function isInteger(t){return\"[object Number]\"===Object.prototype.toString.call(t)&&t%1==0&&!rt.isNegativeZero(t)},represent:{binary:function(t){return t>=0?\"0b\"+t.toString(2):\"-0b\"+t.toString(2).slice(1)},octal:function(t){return t>=0?\"0o\"+t.toString(8):\"-0o\"+t.toString(8).slice(1)},decimal:function(t){return t.toString(10)},hexadecimal:function(t){return t>=0?\"0x\"+t.toString(16).toUpperCase():\"-0x\"+t.toString(16).toUpperCase().slice(1)}},defaultStyle:\"decimal\",styleAliases:{binary:[2,\"bin\"],octal:[8,\"oct\"],decimal:[10,\"dec\"],hexadecimal:[16,\"hex\"]}}),gt=new RegExp(\"^(?:[-+]?(?:[0-9][0-9_]*)(?:\\\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\\\.(?:inf|Inf|INF)|\\\\.(?:nan|NaN|NAN))$\");var mt=/^[-+]?[0-9]+e/;var vt=new st(\"tag:yaml.org,2002:float\",{kind:\"scalar\",resolve:function resolveYamlFloat(t){return null!==t&&!(!gt.test(t)||\"_\"===t[t.length-1])},construct:function constructYamlFloat(t){var e,r;return r=\"-\"===(e=t.replace(/_/g,\"\").toLowerCase())[0]?-1:1,\"+-\".indexOf(e[0])>=0&&(e=e.slice(1)),\".inf\"===e?1===r?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:\".nan\"===e?NaN:r*parseFloat(e,10)},predicate:function isFloat(t){return\"[object Number]\"===Object.prototype.toString.call(t)&&(t%1!=0||rt.isNegativeZero(t))},represent:function representYamlFloat(t,e){var r;if(isNaN(t))switch(e){case\"lowercase\":return\".nan\";case\"uppercase\":return\".NAN\";case\"camelcase\":return\".NaN\"}else if(Number.POSITIVE_INFINITY===t)switch(e){case\"lowercase\":return\".inf\";case\"uppercase\":return\".INF\";case\"camelcase\":return\".Inf\"}else if(Number.NEGATIVE_INFINITY===t)switch(e){case\"lowercase\":return\"-.inf\";case\"uppercase\":return\"-.INF\";case\"camelcase\":return\"-.Inf\"}else if(rt.isNegativeZero(t))return\"-0.0\";return r=t.toString(10),mt.test(r)?r.replace(\"e\",\".e\"):r},defaultStyle:\"lowercase\"}),bt=ht.extend({implicit:[dt,yt,_t,vt]}),St=bt,wt=new RegExp(\"^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$\"),It=new RegExp(\"^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\\\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\\\.([0-9]*))?(?:[ \\\\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$\");var xt=new st(\"tag:yaml.org,2002:timestamp\",{kind:\"scalar\",resolve:function resolveYamlTimestamp(t){return null!==t&&(null!==wt.exec(t)||null!==It.exec(t))},construct:function constructYamlTimestamp(t){var e,r,n,o,i,a,s,u,c=0,f=null;if(null===(e=wt.exec(t))&&(e=It.exec(t)),null===e)throw new Error(\"Date resolve error\");if(r=+e[1],n=+e[2]-1,o=+e[3],!e[4])return new Date(Date.UTC(r,n,o));if(i=+e[4],a=+e[5],s=+e[6],e[7]){for(c=e[7].slice(0,3);c.length<3;)c+=\"0\";c=+c}return e[9]&&(f=6e4*(60*+e[10]+ +(e[11]||0)),\"-\"===e[9]&&(f=-f)),u=new Date(Date.UTC(r,n,o,i,a,s,c)),f&&u.setTime(u.getTime()-f),u},instanceOf:Date,represent:function representYamlTimestamp(t){return t.toISOString()}});var Et=new st(\"tag:yaml.org,2002:merge\",{kind:\"scalar\",resolve:function resolveYamlMerge(t){return\"<<\"===t||null===t}}),At=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\\n\\r\";var Ot=new st(\"tag:yaml.org,2002:binary\",{kind:\"scalar\",resolve:function resolveYamlBinary(t){if(null===t)return!1;var e,r,n=0,o=t.length,i=At;for(r=0;r<o;r++)if(!((e=i.indexOf(t.charAt(r)))>64)){if(e<0)return!1;n+=6}return n%8==0},construct:function constructYamlBinary(t){var e,r,n=t.replace(/[\\r\\n=]/g,\"\"),o=n.length,i=At,a=0,s=[];for(e=0;e<o;e++)e%4==0&&e&&(s.push(a>>16&255),s.push(a>>8&255),s.push(255&a)),a=a<<6|i.indexOf(n.charAt(e));return 0===(r=o%4*6)?(s.push(a>>16&255),s.push(a>>8&255),s.push(255&a)):18===r?(s.push(a>>10&255),s.push(a>>2&255)):12===r&&s.push(a>>4&255),new Uint8Array(s)},predicate:function isBinary(t){return\"[object Uint8Array]\"===Object.prototype.toString.call(t)},represent:function representYamlBinary(t){var e,r,n=\"\",o=0,i=t.length,a=At;for(e=0;e<i;e++)e%3==0&&e&&(n+=a[o>>18&63],n+=a[o>>12&63],n+=a[o>>6&63],n+=a[63&o]),o=(o<<8)+t[e];return 0===(r=i%3)?(n+=a[o>>18&63],n+=a[o>>12&63],n+=a[o>>6&63],n+=a[63&o]):2===r?(n+=a[o>>10&63],n+=a[o>>4&63],n+=a[o<<2&63],n+=a[64]):1===r&&(n+=a[o>>2&63],n+=a[o<<4&63],n+=a[64],n+=a[64]),n}}),Bt=Object.prototype.hasOwnProperty,kt=Object.prototype.toString;var Mt=new st(\"tag:yaml.org,2002:omap\",{kind:\"sequence\",resolve:function resolveYamlOmap(t){if(null===t)return!0;var e,r,n,o,i,a=[],s=t;for(e=0,r=s.length;e<r;e+=1){if(n=s[e],i=!1,\"[object Object]\"!==kt.call(n))return!1;for(o in n)if(Bt.call(n,o)){if(i)return!1;i=!0}if(!i)return!1;if(-1!==a.indexOf(o))return!1;a.push(o)}return!0},construct:function constructYamlOmap(t){return null!==t?t:[]}}),Ct=Object.prototype.toString;var jt=new st(\"tag:yaml.org,2002:pairs\",{kind:\"sequence\",resolve:function resolveYamlPairs(t){if(null===t)return!0;var e,r,n,o,i,a=t;for(i=new Array(a.length),e=0,r=a.length;e<r;e+=1){if(n=a[e],\"[object Object]\"!==Ct.call(n))return!1;if(1!==(o=Object.keys(n)).length)return!1;i[e]=[o[0],n[o[0]]]}return!0},construct:function constructYamlPairs(t){if(null===t)return[];var e,r,n,o,i,a=t;for(i=new Array(a.length),e=0,r=a.length;e<r;e+=1)n=a[e],o=Object.keys(n),i[e]=[o[0],n[o[0]]];return i}}),qt=Object.prototype.hasOwnProperty;var Lt=new st(\"tag:yaml.org,2002:set\",{kind:\"mapping\",resolve:function resolveYamlSet(t){if(null===t)return!0;var e,r=t;for(e in r)if(qt.call(r,e)&&null!==r[e])return!1;return!0},construct:function constructYamlSet(t){return null!==t?t:{}}}),Nt=St.extend({implicit:[xt,Et],explicit:[Ot,Mt,jt,Lt]}),Tt=Object.prototype.hasOwnProperty,Pt=/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F-\\x84\\x86-\\x9F\\uFFFE\\uFFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]/,Rt=/[\\x85\\u2028\\u2029]/,Ft=/[,\\[\\]\\{\\}]/,Ut=/^(?:!|!!|![a-z\\-]+!)$/i,Dt=/^(?:!|[^,\\[\\]\\{\\}])(?:%[0-9a-f]{2}|[0-9a-z\\-#;\\/\\?:@&=\\+\\$,_\\.!~\\*'\\(\\)\\[\\]])*$/i;function _class(t){return Object.prototype.toString.call(t)}function is_EOL(t){return 10===t||13===t}function is_WHITE_SPACE(t){return 9===t||32===t}function is_WS_OR_EOL(t){return 9===t||32===t||10===t||13===t}function is_FLOW_INDICATOR(t){return 44===t||91===t||93===t||123===t||125===t}function fromHexCode(t){var e;return 48<=t&&t<=57?t-48:97<=(e=32|t)&&e<=102?e-97+10:-1}function simpleEscapeSequence(t){return 48===t?\"\\0\":97===t?\"\u0007\":98===t?\"\\b\":116===t||9===t?\"\\t\":110===t?\"\\n\":118===t?\"\\v\":102===t?\"\\f\":114===t?\"\\r\":101===t?\"\u001b\":32===t?\" \":34===t?'\"':47===t?\"/\":92===t?\"\\\\\":78===t?\"\":95===t?\" \":76===t?\"\\u2028\":80===t?\"\\u2029\":\"\"}function charFromCodepoint(t){return t<=65535?String.fromCharCode(t):String.fromCharCode(55296+(t-65536>>10),56320+(t-65536&1023))}function setProperty(t,e,r){\"__proto__\"===e?Object.defineProperty(t,e,{configurable:!0,enumerable:!0,writable:!0,value:r}):t[e]=r}for(var zt=new Array(256),Wt=new Array(256),Vt=0;Vt<256;Vt++)zt[Vt]=simpleEscapeSequence(Vt)?1:0,Wt[Vt]=simpleEscapeSequence(Vt);function State$1(t,e){this.input=t,this.filename=e.filename||null,this.schema=e.schema||Nt,this.onWarning=e.onWarning||null,this.legacy=e.legacy||!1,this.json=e.json||!1,this.listener=e.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=t.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function generateError(t,e){var r={name:t.filename,buffer:t.input.slice(0,-1),position:t.position,line:t.line,column:t.position-t.lineStart};return r.snippet=ot(r),new nt(e,r)}function throwError(t,e){throw generateError(t,e)}function throwWarning(t,e){t.onWarning&&t.onWarning.call(null,generateError(t,e))}var Kt={YAML:function handleYamlDirective(t,e,r){var n,o,i;null!==t.version&&throwError(t,\"duplication of %YAML directive\"),1!==r.length&&throwError(t,\"YAML directive accepts exactly one argument\"),null===(n=/^([0-9]+)\\.([0-9]+)$/.exec(r[0]))&&throwError(t,\"ill-formed argument of the YAML directive\"),o=parseInt(n[1],10),i=parseInt(n[2],10),1!==o&&throwError(t,\"unacceptable YAML version of the document\"),t.version=r[0],t.checkLineBreaks=i<2,1!==i&&2!==i&&throwWarning(t,\"unsupported YAML version of the document\")},TAG:function handleTagDirective(t,e,r){var n,o;2!==r.length&&throwError(t,\"TAG directive accepts exactly two arguments\"),n=r[0],o=r[1],Ut.test(n)||throwError(t,\"ill-formed tag handle (first argument) of the TAG directive\"),Tt.call(t.tagMap,n)&&throwError(t,'there is a previously declared suffix for \"'+n+'\" tag handle'),Dt.test(o)||throwError(t,\"ill-formed tag prefix (second argument) of the TAG directive\");try{o=decodeURIComponent(o)}catch(e){throwError(t,\"tag prefix is malformed: \"+o)}t.tagMap[n]=o}};function captureSegment(t,e,r,n){var o,i,a,s;if(e<r){if(s=t.input.slice(e,r),n)for(o=0,i=s.length;o<i;o+=1)9===(a=s.charCodeAt(o))||32<=a&&a<=1114111||throwError(t,\"expected valid JSON character\");else Pt.test(s)&&throwError(t,\"the stream contains non-printable characters\");t.result+=s}}function mergeMappings(t,e,r,n){var o,i,a,s;for(rt.isObject(r)||throwError(t,\"cannot merge mappings; the provided source object is unacceptable\"),a=0,s=(o=Object.keys(r)).length;a<s;a+=1)i=o[a],Tt.call(e,i)||(setProperty(e,i,r[i]),n[i]=!0)}function storeMappingPair(t,e,r,n,o,i,a,s,u){var c,f;if(Array.isArray(o))for(c=0,f=(o=Array.prototype.slice.call(o)).length;c<f;c+=1)Array.isArray(o[c])&&throwError(t,\"nested arrays are not supported inside keys\"),\"object\"==typeof o&&\"[object Object]\"===_class(o[c])&&(o[c]=\"[object Object]\");if(\"object\"==typeof o&&\"[object Object]\"===_class(o)&&(o=\"[object Object]\"),o=String(o),null===e&&(e={}),\"tag:yaml.org,2002:merge\"===n)if(Array.isArray(i))for(c=0,f=i.length;c<f;c+=1)mergeMappings(t,e,i[c],r);else mergeMappings(t,e,i,r);else t.json||Tt.call(r,o)||!Tt.call(e,o)||(t.line=a||t.line,t.lineStart=s||t.lineStart,t.position=u||t.position,throwError(t,\"duplicated mapping key\")),setProperty(e,o,i),delete r[o];return e}function readLineBreak(t){var e;10===(e=t.input.charCodeAt(t.position))?t.position++:13===e?(t.position++,10===t.input.charCodeAt(t.position)&&t.position++):throwError(t,\"a line break is expected\"),t.line+=1,t.lineStart=t.position,t.firstTabInLine=-1}function skipSeparationSpace(t,e,r){for(var n=0,o=t.input.charCodeAt(t.position);0!==o;){for(;is_WHITE_SPACE(o);)9===o&&-1===t.firstTabInLine&&(t.firstTabInLine=t.position),o=t.input.charCodeAt(++t.position);if(e&&35===o)do{o=t.input.charCodeAt(++t.position)}while(10!==o&&13!==o&&0!==o);if(!is_EOL(o))break;for(readLineBreak(t),o=t.input.charCodeAt(t.position),n++,t.lineIndent=0;32===o;)t.lineIndent++,o=t.input.charCodeAt(++t.position)}return-1!==r&&0!==n&&t.lineIndent<r&&throwWarning(t,\"deficient indentation\"),n}function testDocumentSeparator(t){var e,r=t.position;return!(45!==(e=t.input.charCodeAt(r))&&46!==e||e!==t.input.charCodeAt(r+1)||e!==t.input.charCodeAt(r+2)||(r+=3,0!==(e=t.input.charCodeAt(r))&&!is_WS_OR_EOL(e)))}function writeFoldedLines(t,e){1===e?t.result+=\" \":e>1&&(t.result+=rt.repeat(\"\\n\",e-1))}function readBlockSequence(t,e){var r,n,o=t.tag,i=t.anchor,a=[],s=!1;if(-1!==t.firstTabInLine)return!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=a),n=t.input.charCodeAt(t.position);0!==n&&(-1!==t.firstTabInLine&&(t.position=t.firstTabInLine,throwError(t,\"tab characters must not be used in indentation\")),45===n)&&is_WS_OR_EOL(t.input.charCodeAt(t.position+1));)if(s=!0,t.position++,skipSeparationSpace(t,!0,-1)&&t.lineIndent<=e)a.push(null),n=t.input.charCodeAt(t.position);else if(r=t.line,composeNode(t,e,3,!1,!0),a.push(t.result),skipSeparationSpace(t,!0,-1),n=t.input.charCodeAt(t.position),(t.line===r||t.lineIndent>e)&&0!==n)throwError(t,\"bad indentation of a sequence entry\");else if(t.lineIndent<e)break;return!!s&&(t.tag=o,t.anchor=i,t.kind=\"sequence\",t.result=a,!0)}function readTagProperty(t){var e,r,n,o,i=!1,a=!1;if(33!==(o=t.input.charCodeAt(t.position)))return!1;if(null!==t.tag&&throwError(t,\"duplication of a tag property\"),60===(o=t.input.charCodeAt(++t.position))?(i=!0,o=t.input.charCodeAt(++t.position)):33===o?(a=!0,r=\"!!\",o=t.input.charCodeAt(++t.position)):r=\"!\",e=t.position,i){do{o=t.input.charCodeAt(++t.position)}while(0!==o&&62!==o);t.position<t.length?(n=t.input.slice(e,t.position),o=t.input.charCodeAt(++t.position)):throwError(t,\"unexpected end of the stream within a verbatim tag\")}else{for(;0!==o&&!is_WS_OR_EOL(o);)33===o&&(a?throwError(t,\"tag suffix cannot contain exclamation marks\"):(r=t.input.slice(e-1,t.position+1),Ut.test(r)||throwError(t,\"named tag handle cannot contain such characters\"),a=!0,e=t.position+1)),o=t.input.charCodeAt(++t.position);n=t.input.slice(e,t.position),Ft.test(n)&&throwError(t,\"tag suffix cannot contain flow indicator characters\")}n&&!Dt.test(n)&&throwError(t,\"tag name cannot contain such characters: \"+n);try{n=decodeURIComponent(n)}catch(e){throwError(t,\"tag name is malformed: \"+n)}return i?t.tag=n:Tt.call(t.tagMap,r)?t.tag=t.tagMap[r]+n:\"!\"===r?t.tag=\"!\"+n:\"!!\"===r?t.tag=\"tag:yaml.org,2002:\"+n:throwError(t,'undeclared tag handle \"'+r+'\"'),!0}function readAnchorProperty(t){var e,r;if(38!==(r=t.input.charCodeAt(t.position)))return!1;for(null!==t.anchor&&throwError(t,\"duplication of an anchor property\"),r=t.input.charCodeAt(++t.position),e=t.position;0!==r&&!is_WS_OR_EOL(r)&&!is_FLOW_INDICATOR(r);)r=t.input.charCodeAt(++t.position);return t.position===e&&throwError(t,\"name of an anchor node must contain at least one character\"),t.anchor=t.input.slice(e,t.position),!0}function composeNode(t,e,r,n,o){var i,a,s,u,c,f,l,p,h,d=1,y=!1,_=!1;if(null!==t.listener&&t.listener(\"open\",t),t.tag=null,t.anchor=null,t.kind=null,t.result=null,i=a=s=4===r||3===r,n&&skipSeparationSpace(t,!0,-1)&&(y=!0,t.lineIndent>e?d=1:t.lineIndent===e?d=0:t.lineIndent<e&&(d=-1)),1===d)for(;readTagProperty(t)||readAnchorProperty(t);)skipSeparationSpace(t,!0,-1)?(y=!0,s=i,t.lineIndent>e?d=1:t.lineIndent===e?d=0:t.lineIndent<e&&(d=-1)):s=!1;if(s&&(s=y||o),1!==d&&4!==r||(p=1===r||2===r?e:e+1,h=t.position-t.lineStart,1===d?s&&(readBlockSequence(t,h)||function readBlockMapping(t,e,r){var n,o,i,a,s,u,c,f=t.tag,l=t.anchor,p={},h=Object.create(null),d=null,y=null,_=null,g=!1,m=!1;if(-1!==t.firstTabInLine)return!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=p),c=t.input.charCodeAt(t.position);0!==c;){if(g||-1===t.firstTabInLine||(t.position=t.firstTabInLine,throwError(t,\"tab characters must not be used in indentation\")),n=t.input.charCodeAt(t.position+1),i=t.line,63!==c&&58!==c||!is_WS_OR_EOL(n)){if(a=t.line,s=t.lineStart,u=t.position,!composeNode(t,r,2,!1,!0))break;if(t.line===i){for(c=t.input.charCodeAt(t.position);is_WHITE_SPACE(c);)c=t.input.charCodeAt(++t.position);if(58===c)is_WS_OR_EOL(c=t.input.charCodeAt(++t.position))||throwError(t,\"a whitespace character is expected after the key-value separator within a block mapping\"),g&&(storeMappingPair(t,p,h,d,y,null,a,s,u),d=y=_=null),m=!0,g=!1,o=!1,d=t.tag,y=t.result;else{if(!m)return t.tag=f,t.anchor=l,!0;throwError(t,\"can not read an implicit mapping pair; a colon is missed\")}}else{if(!m)return t.tag=f,t.anchor=l,!0;throwError(t,\"can not read a block mapping entry; a multiline key may not be an implicit key\")}}else 63===c?(g&&(storeMappingPair(t,p,h,d,y,null,a,s,u),d=y=_=null),m=!0,g=!0,o=!0):g?(g=!1,o=!0):throwError(t,\"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line\"),t.position+=1,c=n;if((t.line===i||t.lineIndent>e)&&(g&&(a=t.line,s=t.lineStart,u=t.position),composeNode(t,e,4,!0,o)&&(g?y=t.result:_=t.result),g||(storeMappingPair(t,p,h,d,y,_,a,s,u),d=y=_=null),skipSeparationSpace(t,!0,-1),c=t.input.charCodeAt(t.position)),(t.line===i||t.lineIndent>e)&&0!==c)throwError(t,\"bad indentation of a mapping entry\");else if(t.lineIndent<e)break}return g&&storeMappingPair(t,p,h,d,y,null,a,s,u),m&&(t.tag=f,t.anchor=l,t.kind=\"mapping\",t.result=p),m}(t,h,p))||function readFlowCollection(t,e){var r,n,o,i,a,s,u,c,f,l,p,h,d=!0,y=t.tag,_=t.anchor,g=Object.create(null);if(91===(h=t.input.charCodeAt(t.position)))a=93,c=!1,i=[];else{if(123!==h)return!1;a=125,c=!0,i={}}for(null!==t.anchor&&(t.anchorMap[t.anchor]=i),h=t.input.charCodeAt(++t.position);0!==h;){if(skipSeparationSpace(t,!0,e),(h=t.input.charCodeAt(t.position))===a)return t.position++,t.tag=y,t.anchor=_,t.kind=c?\"mapping\":\"sequence\",t.result=i,!0;d?44===h&&throwError(t,\"expected the node content, but found ','\"):throwError(t,\"missed comma between flow collection entries\"),p=null,s=u=!1,63===h&&is_WS_OR_EOL(t.input.charCodeAt(t.position+1))&&(s=u=!0,t.position++,skipSeparationSpace(t,!0,e)),r=t.line,n=t.lineStart,o=t.position,composeNode(t,e,1,!1,!0),l=t.tag,f=t.result,skipSeparationSpace(t,!0,e),h=t.input.charCodeAt(t.position),!u&&t.line!==r||58!==h||(s=!0,h=t.input.charCodeAt(++t.position),skipSeparationSpace(t,!0,e),composeNode(t,e,1,!1,!0),p=t.result),c?storeMappingPair(t,i,g,l,f,p,r,n,o):s?i.push(storeMappingPair(t,null,g,l,f,p,r,n,o)):i.push(f),skipSeparationSpace(t,!0,e),44===(h=t.input.charCodeAt(t.position))?(d=!0,h=t.input.charCodeAt(++t.position)):d=!1}throwError(t,\"unexpected end of the stream within a flow collection\")}(t,p)?_=!0:(a&&function readBlockScalar(t,e){var r,n,o,i,a,s=1,u=!1,c=!1,f=e,l=0,p=!1;if(124===(i=t.input.charCodeAt(t.position)))n=!1;else{if(62!==i)return!1;n=!0}for(t.kind=\"scalar\",t.result=\"\";0!==i;)if(43===(i=t.input.charCodeAt(++t.position))||45===i)1===s?s=43===i?3:2:throwError(t,\"repeat of a chomping mode identifier\");else{if(!((o=48<=(a=i)&&a<=57?a-48:-1)>=0))break;0===o?throwError(t,\"bad explicit indentation width of a block scalar; it cannot be less than one\"):c?throwError(t,\"repeat of an indentation width identifier\"):(f=e+o-1,c=!0)}if(is_WHITE_SPACE(i)){do{i=t.input.charCodeAt(++t.position)}while(is_WHITE_SPACE(i));if(35===i)do{i=t.input.charCodeAt(++t.position)}while(!is_EOL(i)&&0!==i)}for(;0!==i;){for(readLineBreak(t),t.lineIndent=0,i=t.input.charCodeAt(t.position);(!c||t.lineIndent<f)&&32===i;)t.lineIndent++,i=t.input.charCodeAt(++t.position);if(!c&&t.lineIndent>f&&(f=t.lineIndent),is_EOL(i))l++;else{if(t.lineIndent<f){3===s?t.result+=rt.repeat(\"\\n\",u?1+l:l):1===s&&u&&(t.result+=\"\\n\");break}for(n?is_WHITE_SPACE(i)?(p=!0,t.result+=rt.repeat(\"\\n\",u?1+l:l)):p?(p=!1,t.result+=rt.repeat(\"\\n\",l+1)):0===l?u&&(t.result+=\" \"):t.result+=rt.repeat(\"\\n\",l):t.result+=rt.repeat(\"\\n\",u?1+l:l),u=!0,c=!0,l=0,r=t.position;!is_EOL(i)&&0!==i;)i=t.input.charCodeAt(++t.position);captureSegment(t,r,t.position,!1)}}return!0}(t,p)||function readSingleQuotedScalar(t,e){var r,n,o;if(39!==(r=t.input.charCodeAt(t.position)))return!1;for(t.kind=\"scalar\",t.result=\"\",t.position++,n=o=t.position;0!==(r=t.input.charCodeAt(t.position));)if(39===r){if(captureSegment(t,n,t.position,!0),39!==(r=t.input.charCodeAt(++t.position)))return!0;n=t.position,t.position++,o=t.position}else is_EOL(r)?(captureSegment(t,n,o,!0),writeFoldedLines(t,skipSeparationSpace(t,!1,e)),n=o=t.position):t.position===t.lineStart&&testDocumentSeparator(t)?throwError(t,\"unexpected end of the document within a single quoted scalar\"):(t.position++,o=t.position);throwError(t,\"unexpected end of the stream within a single quoted scalar\")}(t,p)||function readDoubleQuotedScalar(t,e){var r,n,o,i,a,s,u;if(34!==(s=t.input.charCodeAt(t.position)))return!1;for(t.kind=\"scalar\",t.result=\"\",t.position++,r=n=t.position;0!==(s=t.input.charCodeAt(t.position));){if(34===s)return captureSegment(t,r,t.position,!0),t.position++,!0;if(92===s){if(captureSegment(t,r,t.position,!0),is_EOL(s=t.input.charCodeAt(++t.position)))skipSeparationSpace(t,!1,e);else if(s<256&&zt[s])t.result+=Wt[s],t.position++;else if((a=120===(u=s)?2:117===u?4:85===u?8:0)>0){for(o=a,i=0;o>0;o--)(a=fromHexCode(s=t.input.charCodeAt(++t.position)))>=0?i=(i<<4)+a:throwError(t,\"expected hexadecimal character\");t.result+=charFromCodepoint(i),t.position++}else throwError(t,\"unknown escape sequence\");r=n=t.position}else is_EOL(s)?(captureSegment(t,r,n,!0),writeFoldedLines(t,skipSeparationSpace(t,!1,e)),r=n=t.position):t.position===t.lineStart&&testDocumentSeparator(t)?throwError(t,\"unexpected end of the document within a double quoted scalar\"):(t.position++,n=t.position)}throwError(t,\"unexpected end of the stream within a double quoted scalar\")}(t,p)?_=!0:!function readAlias(t){var e,r,n;if(42!==(n=t.input.charCodeAt(t.position)))return!1;for(n=t.input.charCodeAt(++t.position),e=t.position;0!==n&&!is_WS_OR_EOL(n)&&!is_FLOW_INDICATOR(n);)n=t.input.charCodeAt(++t.position);return t.position===e&&throwError(t,\"name of an alias node must contain at least one character\"),r=t.input.slice(e,t.position),Tt.call(t.anchorMap,r)||throwError(t,'unidentified alias \"'+r+'\"'),t.result=t.anchorMap[r],skipSeparationSpace(t,!0,-1),!0}(t)?function readPlainScalar(t,e,r){var n,o,i,a,s,u,c,f,l=t.kind,p=t.result;if(is_WS_OR_EOL(f=t.input.charCodeAt(t.position))||is_FLOW_INDICATOR(f)||35===f||38===f||42===f||33===f||124===f||62===f||39===f||34===f||37===f||64===f||96===f)return!1;if((63===f||45===f)&&(is_WS_OR_EOL(n=t.input.charCodeAt(t.position+1))||r&&is_FLOW_INDICATOR(n)))return!1;for(t.kind=\"scalar\",t.result=\"\",o=i=t.position,a=!1;0!==f;){if(58===f){if(is_WS_OR_EOL(n=t.input.charCodeAt(t.position+1))||r&&is_FLOW_INDICATOR(n))break}else if(35===f){if(is_WS_OR_EOL(t.input.charCodeAt(t.position-1)))break}else{if(t.position===t.lineStart&&testDocumentSeparator(t)||r&&is_FLOW_INDICATOR(f))break;if(is_EOL(f)){if(s=t.line,u=t.lineStart,c=t.lineIndent,skipSeparationSpace(t,!1,-1),t.lineIndent>=e){a=!0,f=t.input.charCodeAt(t.position);continue}t.position=i,t.line=s,t.lineStart=u,t.lineIndent=c;break}}a&&(captureSegment(t,o,i,!1),writeFoldedLines(t,t.line-s),o=i=t.position,a=!1),is_WHITE_SPACE(f)||(i=t.position+1),f=t.input.charCodeAt(++t.position)}return captureSegment(t,o,i,!1),!!t.result||(t.kind=l,t.result=p,!1)}(t,p,1===r)&&(_=!0,null===t.tag&&(t.tag=\"?\")):(_=!0,null===t.tag&&null===t.anchor||throwError(t,\"alias node should not have any properties\")),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):0===d&&(_=s&&readBlockSequence(t,h))),null===t.tag)null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);else if(\"?\"===t.tag){for(null!==t.result&&\"scalar\"!==t.kind&&throwError(t,'unacceptable node kind for !<?> tag; it should be \"scalar\", not \"'+t.kind+'\"'),u=0,c=t.implicitTypes.length;u<c;u+=1)if((l=t.implicitTypes[u]).resolve(t.result)){t.result=l.construct(t.result),t.tag=l.tag,null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);break}}else if(\"!\"!==t.tag){if(Tt.call(t.typeMap[t.kind||\"fallback\"],t.tag))l=t.typeMap[t.kind||\"fallback\"][t.tag];else for(l=null,u=0,c=(f=t.typeMap.multi[t.kind||\"fallback\"]).length;u<c;u+=1)if(t.tag.slice(0,f[u].tag.length)===f[u].tag){l=f[u];break}l||throwError(t,\"unknown tag !<\"+t.tag+\">\"),null!==t.result&&l.kind!==t.kind&&throwError(t,\"unacceptable node kind for !<\"+t.tag+'> tag; it should be \"'+l.kind+'\", not \"'+t.kind+'\"'),l.resolve(t.result,t.tag)?(t.result=l.construct(t.result,t.tag),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):throwError(t,\"cannot resolve a node with !<\"+t.tag+\"> explicit tag\")}return null!==t.listener&&t.listener(\"close\",t),null!==t.tag||null!==t.anchor||_}function readDocument(t){var e,r,n,o,i=t.position,a=!1;for(t.version=null,t.checkLineBreaks=t.legacy,t.tagMap=Object.create(null),t.anchorMap=Object.create(null);0!==(o=t.input.charCodeAt(t.position))&&(skipSeparationSpace(t,!0,-1),o=t.input.charCodeAt(t.position),!(t.lineIndent>0||37!==o));){for(a=!0,o=t.input.charCodeAt(++t.position),e=t.position;0!==o&&!is_WS_OR_EOL(o);)o=t.input.charCodeAt(++t.position);for(n=[],(r=t.input.slice(e,t.position)).length<1&&throwError(t,\"directive name must not be less than one character in length\");0!==o;){for(;is_WHITE_SPACE(o);)o=t.input.charCodeAt(++t.position);if(35===o){do{o=t.input.charCodeAt(++t.position)}while(0!==o&&!is_EOL(o));break}if(is_EOL(o))break;for(e=t.position;0!==o&&!is_WS_OR_EOL(o);)o=t.input.charCodeAt(++t.position);n.push(t.input.slice(e,t.position))}0!==o&&readLineBreak(t),Tt.call(Kt,r)?Kt[r](t,r,n):throwWarning(t,'unknown document directive \"'+r+'\"')}skipSeparationSpace(t,!0,-1),0===t.lineIndent&&45===t.input.charCodeAt(t.position)&&45===t.input.charCodeAt(t.position+1)&&45===t.input.charCodeAt(t.position+2)?(t.position+=3,skipSeparationSpace(t,!0,-1)):a&&throwError(t,\"directives end mark is expected\"),composeNode(t,t.lineIndent-1,4,!1,!0),skipSeparationSpace(t,!0,-1),t.checkLineBreaks&&Rt.test(t.input.slice(i,t.position))&&throwWarning(t,\"non-ASCII line breaks are interpreted as content\"),t.documents.push(t.result),t.position===t.lineStart&&testDocumentSeparator(t)?46===t.input.charCodeAt(t.position)&&(t.position+=3,skipSeparationSpace(t,!0,-1)):t.position<t.length-1&&throwError(t,\"end of the stream or a document separator is expected\")}function loadDocuments(t,e){e=e||{},0!==(t=String(t)).length&&(10!==t.charCodeAt(t.length-1)&&13!==t.charCodeAt(t.length-1)&&(t+=\"\\n\"),65279===t.charCodeAt(0)&&(t=t.slice(1)));var r=new State$1(t,e),n=t.indexOf(\"\\0\");for(-1!==n&&(r.position=n,throwError(r,\"null byte is not allowed in input\")),r.input+=\"\\0\";32===r.input.charCodeAt(r.position);)r.lineIndent+=1,r.position+=1;for(;r.position<r.length-1;)readDocument(r);return r.documents}var $t={loadAll:function loadAll$1(t,e,r){null!==e&&\"object\"==typeof e&&void 0===r&&(r=e,e=null);var n=loadDocuments(t,r);if(\"function\"!=typeof e)return n;for(var o=0,i=n.length;o<i;o+=1)e(n[o])},load:function load$1(t,e){var r=loadDocuments(t,e);if(0!==r.length){if(1===r.length)return r[0];throw new nt(\"expected a single document in the stream, but found more\")}}},Ht=Object.prototype.toString,Yt=Object.prototype.hasOwnProperty,Gt=65279,Zt={0:\"\\\\0\",7:\"\\\\a\",8:\"\\\\b\",9:\"\\\\t\",10:\"\\\\n\",11:\"\\\\v\",12:\"\\\\f\",13:\"\\\\r\",27:\"\\\\e\",34:'\\\\\"',92:\"\\\\\\\\\",133:\"\\\\N\",160:\"\\\\_\",8232:\"\\\\L\",8233:\"\\\\P\"},Jt=[\"y\",\"Y\",\"yes\",\"Yes\",\"YES\",\"on\",\"On\",\"ON\",\"n\",\"N\",\"no\",\"No\",\"NO\",\"off\",\"Off\",\"OFF\"],Qt=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\\.[0-9_]*)?$/;function encodeHex(t){var e,r,n;if(e=t.toString(16).toUpperCase(),t<=255)r=\"x\",n=2;else if(t<=65535)r=\"u\",n=4;else{if(!(t<=4294967295))throw new nt(\"code point within a string may not be greater than 0xFFFFFFFF\");r=\"U\",n=8}return\"\\\\\"+r+rt.repeat(\"0\",n-e.length)+e}function State(t){this.schema=t.schema||Nt,this.indent=Math.max(1,t.indent||2),this.noArrayIndent=t.noArrayIndent||!1,this.skipInvalid=t.skipInvalid||!1,this.flowLevel=rt.isNothing(t.flowLevel)?-1:t.flowLevel,this.styleMap=function compileStyleMap(t,e){var r,n,o,i,a,s,u;if(null===e)return{};for(r={},o=0,i=(n=Object.keys(e)).length;o<i;o+=1)a=n[o],s=String(e[a]),\"!!\"===a.slice(0,2)&&(a=\"tag:yaml.org,2002:\"+a.slice(2)),(u=t.compiledTypeMap.fallback[a])&&Yt.call(u.styleAliases,s)&&(s=u.styleAliases[s]),r[a]=s;return r}(this.schema,t.styles||null),this.sortKeys=t.sortKeys||!1,this.lineWidth=t.lineWidth||80,this.noRefs=t.noRefs||!1,this.noCompatMode=t.noCompatMode||!1,this.condenseFlow=t.condenseFlow||!1,this.quotingType='\"'===t.quotingType?2:1,this.forceQuotes=t.forceQuotes||!1,this.replacer=\"function\"==typeof t.replacer?t.replacer:null,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result=\"\",this.duplicates=[],this.usedDuplicates=null}function indentString(t,e){for(var r,n=rt.repeat(\" \",e),o=0,i=-1,a=\"\",s=t.length;o<s;)-1===(i=t.indexOf(\"\\n\",o))?(r=t.slice(o),o=s):(r=t.slice(o,i+1),o=i+1),r.length&&\"\\n\"!==r&&(a+=n),a+=r;return a}function generateNextLine(t,e){return\"\\n\"+rt.repeat(\" \",t.indent*e)}function isWhitespace(t){return 32===t||9===t}function isPrintable(t){return 32<=t&&t<=126||161<=t&&t<=55295&&8232!==t&&8233!==t||57344<=t&&t<=65533&&t!==Gt||65536<=t&&t<=1114111}function isNsCharOrWhitespace(t){return isPrintable(t)&&t!==Gt&&13!==t&&10!==t}function isPlainSafe(t,e,r){var n=isNsCharOrWhitespace(t),o=n&&!isWhitespace(t);return(r?n:n&&44!==t&&91!==t&&93!==t&&123!==t&&125!==t)&&35!==t&&!(58===e&&!o)||isNsCharOrWhitespace(e)&&!isWhitespace(e)&&35===t||58===e&&o}function codePointAt(t,e){var r,n=t.charCodeAt(e);return n>=55296&&n<=56319&&e+1<t.length&&(r=t.charCodeAt(e+1))>=56320&&r<=57343?1024*(n-55296)+r-56320+65536:n}function needIndentIndicator(t){return/^\\n* /.test(t)}function chooseScalarStyle(t,e,r,n,o,i,a,s){var u,c=0,f=null,l=!1,p=!1,h=-1!==n,d=-1,y=function isPlainSafeFirst(t){return isPrintable(t)&&t!==Gt&&!isWhitespace(t)&&45!==t&&63!==t&&58!==t&&44!==t&&91!==t&&93!==t&&123!==t&&125!==t&&35!==t&&38!==t&&42!==t&&33!==t&&124!==t&&61!==t&&62!==t&&39!==t&&34!==t&&37!==t&&64!==t&&96!==t}(codePointAt(t,0))&&function isPlainSafeLast(t){return!isWhitespace(t)&&58!==t}(codePointAt(t,t.length-1));if(e||a)for(u=0;u<t.length;c>=65536?u+=2:u++){if(!isPrintable(c=codePointAt(t,u)))return 5;y=y&&isPlainSafe(c,f,s),f=c}else{for(u=0;u<t.length;c>=65536?u+=2:u++){if(10===(c=codePointAt(t,u)))l=!0,h&&(p=p||u-d-1>n&&\" \"!==t[d+1],d=u);else if(!isPrintable(c))return 5;y=y&&isPlainSafe(c,f,s),f=c}p=p||h&&u-d-1>n&&\" \"!==t[d+1]}return l||p?r>9&&needIndentIndicator(t)?5:a?2===i?5:2:p?4:3:!y||a||o(t)?2===i?5:2:1}function writeScalar(t,e,r,n,o){t.dump=function(){if(0===e.length)return 2===t.quotingType?'\"\"':\"''\";if(!t.noCompatMode&&(-1!==Jt.indexOf(e)||Qt.test(e)))return 2===t.quotingType?'\"'+e+'\"':\"'\"+e+\"'\";var i=t.indent*Math.max(1,r),a=-1===t.lineWidth?-1:Math.max(Math.min(t.lineWidth,40),t.lineWidth-i),s=n||t.flowLevel>-1&&r>=t.flowLevel;switch(chooseScalarStyle(e,s,t.indent,a,(function testAmbiguity(e){return function testImplicitResolving(t,e){var r,n;for(r=0,n=t.implicitTypes.length;r<n;r+=1)if(t.implicitTypes[r].resolve(e))return!0;return!1}(t,e)}),t.quotingType,t.forceQuotes&&!n,o)){case 1:return e;case 2:return\"'\"+e.replace(/'/g,\"''\")+\"'\";case 3:return\"|\"+blockHeader(e,t.indent)+dropEndingNewline(indentString(e,i));case 4:return\">\"+blockHeader(e,t.indent)+dropEndingNewline(indentString(function foldString(t,e){var r,n,o=/(\\n+)([^\\n]*)/g,i=(s=t.indexOf(\"\\n\"),s=-1!==s?s:t.length,o.lastIndex=s,foldLine(t.slice(0,s),e)),a=\"\\n\"===t[0]||\" \"===t[0];var s;for(;n=o.exec(t);){var u=n[1],c=n[2];r=\" \"===c[0],i+=u+(a||r||\"\"===c?\"\":\"\\n\")+foldLine(c,e),a=r}return i}(e,a),i));case 5:return'\"'+function escapeString(t){for(var e,r=\"\",n=0,o=0;o<t.length;n>=65536?o+=2:o++)n=codePointAt(t,o),!(e=Zt[n])&&isPrintable(n)?(r+=t[o],n>=65536&&(r+=t[o+1])):r+=e||encodeHex(n);return r}(e)+'\"';default:throw new nt(\"impossible error: invalid scalar style\")}}()}function blockHeader(t,e){var r=needIndentIndicator(t)?String(e):\"\",n=\"\\n\"===t[t.length-1];return r+(n&&(\"\\n\"===t[t.length-2]||\"\\n\"===t)?\"+\":n?\"\":\"-\")+\"\\n\"}function dropEndingNewline(t){return\"\\n\"===t[t.length-1]?t.slice(0,-1):t}function foldLine(t,e){if(\"\"===t||\" \"===t[0])return t;for(var r,n,o=/ [^ ]/g,i=0,a=0,s=0,u=\"\";r=o.exec(t);)(s=r.index)-i>e&&(n=a>i?a:s,u+=\"\\n\"+t.slice(i,n),i=n+1),a=s;return u+=\"\\n\",t.length-i>e&&a>i?u+=t.slice(i,a)+\"\\n\"+t.slice(a+1):u+=t.slice(i),u.slice(1)}function writeBlockSequence(t,e,r,n){var o,i,a,s=\"\",u=t.tag;for(o=0,i=r.length;o<i;o+=1)a=r[o],t.replacer&&(a=t.replacer.call(r,String(o),a)),(writeNode(t,e+1,a,!0,!0,!1,!0)||void 0===a&&writeNode(t,e+1,null,!0,!0,!1,!0))&&(n&&\"\"===s||(s+=generateNextLine(t,e)),t.dump&&10===t.dump.charCodeAt(0)?s+=\"-\":s+=\"- \",s+=t.dump);t.tag=u,t.dump=s||\"[]\"}function detectType(t,e,r){var n,o,i,a,s,u;for(i=0,a=(o=r?t.explicitTypes:t.implicitTypes).length;i<a;i+=1)if(((s=o[i]).instanceOf||s.predicate)&&(!s.instanceOf||\"object\"==typeof e&&e instanceof s.instanceOf)&&(!s.predicate||s.predicate(e))){if(r?s.multi&&s.representName?t.tag=s.representName(e):t.tag=s.tag:t.tag=\"?\",s.represent){if(u=t.styleMap[s.tag]||s.defaultStyle,\"[object Function]\"===Ht.call(s.represent))n=s.represent(e,u);else{if(!Yt.call(s.represent,u))throw new nt(\"!<\"+s.tag+'> tag resolver accepts not \"'+u+'\" style');n=s.represent[u](e,u)}t.dump=n}return!0}return!1}function writeNode(t,e,r,n,o,i,a){t.tag=null,t.dump=r,detectType(t,r,!1)||detectType(t,r,!0);var s,u=Ht.call(t.dump),c=n;n&&(n=t.flowLevel<0||t.flowLevel>e);var f,l,p=\"[object Object]\"===u||\"[object Array]\"===u;if(p&&(l=-1!==(f=t.duplicates.indexOf(r))),(null!==t.tag&&\"?\"!==t.tag||l||2!==t.indent&&e>0)&&(o=!1),l&&t.usedDuplicates[f])t.dump=\"*ref_\"+f;else{if(p&&l&&!t.usedDuplicates[f]&&(t.usedDuplicates[f]=!0),\"[object Object]\"===u)n&&0!==Object.keys(t.dump).length?(!function writeBlockMapping(t,e,r,n){var o,i,a,s,u,c,f=\"\",l=t.tag,p=Object.keys(r);if(!0===t.sortKeys)p.sort();else if(\"function\"==typeof t.sortKeys)p.sort(t.sortKeys);else if(t.sortKeys)throw new nt(\"sortKeys must be a boolean or a function\");for(o=0,i=p.length;o<i;o+=1)c=\"\",n&&\"\"===f||(c+=generateNextLine(t,e)),s=r[a=p[o]],t.replacer&&(s=t.replacer.call(r,a,s)),writeNode(t,e+1,a,!0,!0,!0)&&((u=null!==t.tag&&\"?\"!==t.tag||t.dump&&t.dump.length>1024)&&(t.dump&&10===t.dump.charCodeAt(0)?c+=\"?\":c+=\"? \"),c+=t.dump,u&&(c+=generateNextLine(t,e)),writeNode(t,e+1,s,!0,u)&&(t.dump&&10===t.dump.charCodeAt(0)?c+=\":\":c+=\": \",f+=c+=t.dump));t.tag=l,t.dump=f||\"{}\"}(t,e,t.dump,o),l&&(t.dump=\"&ref_\"+f+t.dump)):(!function writeFlowMapping(t,e,r){var n,o,i,a,s,u=\"\",c=t.tag,f=Object.keys(r);for(n=0,o=f.length;n<o;n+=1)s=\"\",\"\"!==u&&(s+=\", \"),t.condenseFlow&&(s+='\"'),a=r[i=f[n]],t.replacer&&(a=t.replacer.call(r,i,a)),writeNode(t,e,i,!1,!1)&&(t.dump.length>1024&&(s+=\"? \"),s+=t.dump+(t.condenseFlow?'\"':\"\")+\":\"+(t.condenseFlow?\"\":\" \"),writeNode(t,e,a,!1,!1)&&(u+=s+=t.dump));t.tag=c,t.dump=\"{\"+u+\"}\"}(t,e,t.dump),l&&(t.dump=\"&ref_\"+f+\" \"+t.dump));else if(\"[object Array]\"===u)n&&0!==t.dump.length?(t.noArrayIndent&&!a&&e>0?writeBlockSequence(t,e-1,t.dump,o):writeBlockSequence(t,e,t.dump,o),l&&(t.dump=\"&ref_\"+f+t.dump)):(!function writeFlowSequence(t,e,r){var n,o,i,a=\"\",s=t.tag;for(n=0,o=r.length;n<o;n+=1)i=r[n],t.replacer&&(i=t.replacer.call(r,String(n),i)),(writeNode(t,e,i,!1,!1)||void 0===i&&writeNode(t,e,null,!1,!1))&&(\"\"!==a&&(a+=\",\"+(t.condenseFlow?\"\":\" \")),a+=t.dump);t.tag=s,t.dump=\"[\"+a+\"]\"}(t,e,t.dump),l&&(t.dump=\"&ref_\"+f+\" \"+t.dump));else{if(\"[object String]\"!==u){if(\"[object Undefined]\"===u)return!1;if(t.skipInvalid)return!1;throw new nt(\"unacceptable kind of an object to dump \"+u)}\"?\"!==t.tag&&writeScalar(t,t.dump,e,i,c)}null!==t.tag&&\"?\"!==t.tag&&(s=encodeURI(\"!\"===t.tag[0]?t.tag.slice(1):t.tag).replace(/!/g,\"%21\"),s=\"!\"===t.tag[0]?\"!\"+s:\"tag:yaml.org,2002:\"===s.slice(0,18)?\"!!\"+s.slice(18):\"!<\"+s+\">\",t.dump=s+\" \"+t.dump)}return!0}function getDuplicateReferences(t,e){var r,n,o=[],i=[];for(inspectNode(t,o,i),r=0,n=i.length;r<n;r+=1)e.duplicates.push(o[i[r]]);e.usedDuplicates=new Array(n)}function inspectNode(t,e,r){var n,o,i;if(null!==t&&\"object\"==typeof t)if(-1!==(o=e.indexOf(t)))-1===r.indexOf(o)&&r.push(o);else if(e.push(t),Array.isArray(t))for(o=0,i=t.length;o<i;o+=1)inspectNode(t[o],e,r);else for(o=0,i=(n=Object.keys(t)).length;o<i;o+=1)inspectNode(t[n[o]],e,r)}function renamed(t,e){return function(){throw new Error(\"Function yaml.\"+t+\" is removed in js-yaml 4. Use yaml.\"+e+\" instead, which is now safe by default.\")}}var Xt={Type:st,Schema:ut,FAILSAFE_SCHEMA:ht,JSON_SCHEMA:bt,CORE_SCHEMA:St,DEFAULT_SCHEMA:Nt,load:$t.load,loadAll:$t.loadAll,dump:{dump:function dump$1(t,e){var r=new State(e=e||{});r.noRefs||getDuplicateReferences(t,r);var n=t;return r.replacer&&(n=r.replacer.call({\"\":n},\"\",n)),writeNode(r,0,n,!0,!0)?r.dump+\"\\n\":\"\"}}.dump,YAMLException:nt,types:{binary:Ot,float:vt,map:pt,null:dt,pairs:jt,set:Lt,timestamp:xt,bool:yt,int:_t,merge:Et,omap:Mt,seq:lt,str:ct},safeLoad:renamed(\"safeLoad\",\"load\"),safeLoadAll:renamed(\"safeLoadAll\",\"loadAll\"),safeDump:renamed(\"safeDump\",\"dump\")};const te=\"configs_update\",ee=\"configs_toggle\";function update(t,e){return{type:te,payload:{[t]:e}}}function toggle(t){return{type:ee,payload:t}}const loaded=()=>()=>{},downloadConfig=t=>e=>{const{fn:{fetch:r}}=e;return r(t)},getConfigByUrl=(t,e)=>r=>{const{specActions:n,configsActions:o}=r;if(t)return o.downloadConfig(t).then(next,next);function next(o){o instanceof Error||o.status>=400?(n.updateLoadingStatus(\"failedConfig\"),n.updateLoadingStatus(\"failedConfig\"),n.updateUrl(\"\"),console.error(o.statusText+\" \"+t.url),e(null)):e(((t,e)=>{try{return Xt.load(t)}catch(t){return e&&e.errActions.newThrownErr(new Error(t)),{}}})(o.text,r))}},get=(t,e)=>t.getIn(Array.isArray(e)?e:[e]),re={[te]:(t,e)=>t.merge((0,i.fromJS)(e.payload)),[ee]:(t,e)=>{const r=e.payload,n=t.get(r);return t.set(r,!n)}};var ne=__webpack_require__(7248),oe=__webpack_require__.n(ne),ie=__webpack_require__(7666),ae=__webpack_require__.n(ie);const se=console.error,withErrorBoundary=t=>e=>{const{getComponent:r,fn:o}=t(),i=r(\"ErrorBoundary\"),a=o.getDisplayName(e);class WithErrorBoundary extends n.Component{render(){return n.createElement(i,{targetName:a,getComponent:r,fn:o},n.createElement(e,ae()({},this.props,this.context)))}}var s;return WithErrorBoundary.displayName=`WithErrorBoundary(${a})`,(s=e).prototype&&s.prototype.isReactComponent&&(WithErrorBoundary.prototype.mapStateToProps=e.prototype.mapStateToProps),WithErrorBoundary},fallback=({name:t})=>n.createElement(\"div\",{className:\"fallback\"},\"😱 \",n.createElement(\"i\",null,\"Could not render \",\"t\"===t?\"this component\":t,\", see the console.\"));class ErrorBoundary extends n.Component{static defaultProps={targetName:\"this component\",getComponent:()=>fallback,fn:{componentDidCatch:se},children:null};static getDerivedStateFromError(t){return{hasError:!0,error:t}}constructor(...t){super(...t),this.state={hasError:!1,error:null}}componentDidCatch(t,e){this.props.fn.componentDidCatch(t,e)}render(){const{getComponent:t,targetName:e,children:r}=this.props;if(this.state.hasError){const r=t(\"Fallback\");return n.createElement(r,{name:e})}return r}}const ue=ErrorBoundary,ce=[top_bar,function configsPlugin(){return{statePlugins:{configs:{reducers:re,actions:t,selectors:e}}}},stadalone_layout,(({componentList:t=[],fullOverride:e=!1}={})=>({getSystem:r})=>{const n=e?t:[\"App\",\"BaseLayout\",\"VersionPragmaFilter\",\"InfoContainer\",\"ServersContainer\",\"SchemesContainer\",\"AuthorizeBtnContainer\",\"FilterContainer\",\"Operations\",\"OperationContainer\",\"parameters\",\"responses\",\"OperationServers\",\"Models\",\"ModelWrapper\",...t],o=oe()(n,Array(n.length).fill(((t,{fn:e})=>e.withErrorBoundary(t))));return{fn:{componentDidCatch:se,withErrorBoundary:withErrorBoundary(r)},components:{ErrorBoundary:ue,Fallback:fallback},wrapComponents:o}})({fullOverride:!0,componentList:[\"Topbar\",\"StandaloneLayout\",\"onlineValidatorBadge\"]})]})(),r=r.default})()));"
  },
  {
    "path": "openapi/swagger-ui/swagger-ui.css",
    "content": ".swagger-ui{color:#3b4151;font-family:sans-serif}.swagger-ui html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.swagger-ui body{margin:0}.swagger-ui article,.swagger-ui aside,.swagger-ui footer,.swagger-ui header,.swagger-ui nav,.swagger-ui section{display:block}.swagger-ui h1{font-size:2em;margin:.67em 0}.swagger-ui figcaption,.swagger-ui figure,.swagger-ui main{display:block}.swagger-ui figure{margin:1em 40px}.swagger-ui hr{box-sizing:content-box;height:0;overflow:visible}.swagger-ui pre{font-family:monospace,monospace;font-size:1em}.swagger-ui a{background-color:transparent;-webkit-text-decoration-skip:objects}.swagger-ui abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.swagger-ui b,.swagger-ui strong{font-weight:inherit;font-weight:bolder}.swagger-ui code,.swagger-ui kbd,.swagger-ui samp{font-family:monospace,monospace;font-size:1em}.swagger-ui dfn{font-style:italic}.swagger-ui mark{background-color:#ff0;color:#000}.swagger-ui small{font-size:80%}.swagger-ui sub,.swagger-ui sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.swagger-ui sub{bottom:-.25em}.swagger-ui sup{top:-.5em}.swagger-ui audio,.swagger-ui video{display:inline-block}.swagger-ui audio:not([controls]){display:none;height:0}.swagger-ui img{border-style:none}.swagger-ui svg:not(:root){overflow:hidden}.swagger-ui button,.swagger-ui input,.swagger-ui optgroup,.swagger-ui select,.swagger-ui textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}.swagger-ui button,.swagger-ui input{overflow:visible}.swagger-ui button,.swagger-ui select{text-transform:none}.swagger-ui [type=reset],.swagger-ui [type=submit],.swagger-ui button,.swagger-ui html [type=button]{-webkit-appearance:button}.swagger-ui [type=button]::-moz-focus-inner,.swagger-ui [type=reset]::-moz-focus-inner,.swagger-ui [type=submit]::-moz-focus-inner,.swagger-ui button::-moz-focus-inner{border-style:none;padding:0}.swagger-ui [type=button]:-moz-focusring,.swagger-ui [type=reset]:-moz-focusring,.swagger-ui [type=submit]:-moz-focusring,.swagger-ui button:-moz-focusring{outline:1px dotted ButtonText}.swagger-ui fieldset{padding:.35em .75em .625em}.swagger-ui legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}.swagger-ui progress{display:inline-block;vertical-align:baseline}.swagger-ui textarea{overflow:auto}.swagger-ui [type=checkbox],.swagger-ui [type=radio]{box-sizing:border-box;padding:0}.swagger-ui [type=number]::-webkit-inner-spin-button,.swagger-ui [type=number]::-webkit-outer-spin-button{height:auto}.swagger-ui [type=search]{-webkit-appearance:textfield;outline-offset:-2px}.swagger-ui [type=search]::-webkit-search-cancel-button,.swagger-ui [type=search]::-webkit-search-decoration{-webkit-appearance:none}.swagger-ui ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.swagger-ui details,.swagger-ui menu{display:block}.swagger-ui summary{display:list-item}.swagger-ui canvas{display:inline-block}.swagger-ui [hidden],.swagger-ui template{display:none}.swagger-ui .debug *{outline:1px solid gold}.swagger-ui .debug-white *{outline:1px solid #fff}.swagger-ui .debug-black *{outline:1px solid #000}.swagger-ui .debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) repeat 0 0}.swagger-ui .debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODYyRjhERDU2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODYyRjhERDQ2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QTY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3QjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvCS01IAAABMSURBVHjaYmR4/5+BFPBfAMFm/MBgx8RAGWCn1AAmSg34Q6kBDKMGMDCwICeMIemF/5QawEipAWwUhwEjMDvbAWlWkvVBwu8vQIABAEwBCph8U6c0AAAAAElFTkSuQmCC) repeat 0 0}.swagger-ui .debug-grid-8-solid{background:#fff url(data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAAAAD/4QMxaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzExMSA3OS4xNTgzMjUsIDIwMTUvMDkvMTAtMDE6MTA6MjAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIxMjI0OTczNjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIxMjI0OTc0NjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjEyMjQ5NzE2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjEyMjQ5NzI2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAbGhopHSlBJiZBQi8vL0JHPz4+P0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHAR0pKTQmND8oKD9HPzU/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0f/wAARCAAIAAgDASIAAhEBAxEB/8QAWQABAQAAAAAAAAAAAAAAAAAAAAYBAQEAAAAAAAAAAAAAAAAAAAIEEAEBAAMBAAAAAAAAAAAAAAABADECA0ERAAEDBQAAAAAAAAAAAAAAAAARITFBUWESIv/aAAwDAQACEQMRAD8AoOnTV1QTD7JJshP3vSM3P//Z) repeat 0 0}.swagger-ui .debug-grid-16-solid{background:#fff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzY3MkJEN0U2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzY3MkJEN0Y2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3RDY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pve6J3kAAAAzSURBVHjaYvz//z8D0UDsMwMjSRoYP5Gq4SPNbRjVMEQ1fCRDg+in/6+J1AJUxsgAEGAA31BAJMS0GYEAAAAASUVORK5CYII=) repeat 0 0}.swagger-ui .border-box,.swagger-ui a,.swagger-ui article,.swagger-ui body,.swagger-ui code,.swagger-ui dd,.swagger-ui div,.swagger-ui dl,.swagger-ui dt,.swagger-ui fieldset,.swagger-ui footer,.swagger-ui form,.swagger-ui h1,.swagger-ui h2,.swagger-ui h3,.swagger-ui h4,.swagger-ui h5,.swagger-ui h6,.swagger-ui header,.swagger-ui html,.swagger-ui input[type=email],.swagger-ui input[type=number],.swagger-ui input[type=password],.swagger-ui input[type=tel],.swagger-ui input[type=text],.swagger-ui input[type=url],.swagger-ui legend,.swagger-ui li,.swagger-ui main,.swagger-ui ol,.swagger-ui p,.swagger-ui pre,.swagger-ui section,.swagger-ui table,.swagger-ui td,.swagger-ui textarea,.swagger-ui th,.swagger-ui tr,.swagger-ui ul{box-sizing:border-box}.swagger-ui .aspect-ratio{height:0;position:relative}.swagger-ui .aspect-ratio--16x9{padding-bottom:56.25%}.swagger-ui .aspect-ratio--9x16{padding-bottom:177.77%}.swagger-ui .aspect-ratio--4x3{padding-bottom:75%}.swagger-ui .aspect-ratio--3x4{padding-bottom:133.33%}.swagger-ui .aspect-ratio--6x4{padding-bottom:66.6%}.swagger-ui .aspect-ratio--4x6{padding-bottom:150%}.swagger-ui .aspect-ratio--8x5{padding-bottom:62.5%}.swagger-ui .aspect-ratio--5x8{padding-bottom:160%}.swagger-ui .aspect-ratio--7x5{padding-bottom:71.42%}.swagger-ui .aspect-ratio--5x7{padding-bottom:140%}.swagger-ui .aspect-ratio--1x1{padding-bottom:100%}.swagger-ui .aspect-ratio--object{bottom:0;height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:100}@media screen and (min-width:30em){.swagger-ui .aspect-ratio-ns{height:0;position:relative}.swagger-ui .aspect-ratio--16x9-ns{padding-bottom:56.25%}.swagger-ui .aspect-ratio--9x16-ns{padding-bottom:177.77%}.swagger-ui .aspect-ratio--4x3-ns{padding-bottom:75%}.swagger-ui .aspect-ratio--3x4-ns{padding-bottom:133.33%}.swagger-ui .aspect-ratio--6x4-ns{padding-bottom:66.6%}.swagger-ui .aspect-ratio--4x6-ns{padding-bottom:150%}.swagger-ui .aspect-ratio--8x5-ns{padding-bottom:62.5%}.swagger-ui .aspect-ratio--5x8-ns{padding-bottom:160%}.swagger-ui .aspect-ratio--7x5-ns{padding-bottom:71.42%}.swagger-ui .aspect-ratio--5x7-ns{padding-bottom:140%}.swagger-ui .aspect-ratio--1x1-ns{padding-bottom:100%}.swagger-ui .aspect-ratio--object-ns{bottom:0;height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:100}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .aspect-ratio-m{height:0;position:relative}.swagger-ui .aspect-ratio--16x9-m{padding-bottom:56.25%}.swagger-ui .aspect-ratio--9x16-m{padding-bottom:177.77%}.swagger-ui .aspect-ratio--4x3-m{padding-bottom:75%}.swagger-ui .aspect-ratio--3x4-m{padding-bottom:133.33%}.swagger-ui .aspect-ratio--6x4-m{padding-bottom:66.6%}.swagger-ui .aspect-ratio--4x6-m{padding-bottom:150%}.swagger-ui .aspect-ratio--8x5-m{padding-bottom:62.5%}.swagger-ui .aspect-ratio--5x8-m{padding-bottom:160%}.swagger-ui .aspect-ratio--7x5-m{padding-bottom:71.42%}.swagger-ui .aspect-ratio--5x7-m{padding-bottom:140%}.swagger-ui .aspect-ratio--1x1-m{padding-bottom:100%}.swagger-ui .aspect-ratio--object-m{bottom:0;height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:100}}@media screen and (min-width:60em){.swagger-ui .aspect-ratio-l{height:0;position:relative}.swagger-ui .aspect-ratio--16x9-l{padding-bottom:56.25%}.swagger-ui .aspect-ratio--9x16-l{padding-bottom:177.77%}.swagger-ui .aspect-ratio--4x3-l{padding-bottom:75%}.swagger-ui .aspect-ratio--3x4-l{padding-bottom:133.33%}.swagger-ui .aspect-ratio--6x4-l{padding-bottom:66.6%}.swagger-ui .aspect-ratio--4x6-l{padding-bottom:150%}.swagger-ui .aspect-ratio--8x5-l{padding-bottom:62.5%}.swagger-ui .aspect-ratio--5x8-l{padding-bottom:160%}.swagger-ui .aspect-ratio--7x5-l{padding-bottom:71.42%}.swagger-ui .aspect-ratio--5x7-l{padding-bottom:140%}.swagger-ui .aspect-ratio--1x1-l{padding-bottom:100%}.swagger-ui .aspect-ratio--object-l{bottom:0;height:100%;left:0;position:absolute;right:0;top:0;width:100%;z-index:100}}.swagger-ui img{max-width:100%}.swagger-ui .cover{background-size:cover!important}.swagger-ui .contain{background-size:contain!important}@media screen and (min-width:30em){.swagger-ui .cover-ns{background-size:cover!important}.swagger-ui .contain-ns{background-size:contain!important}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .cover-m{background-size:cover!important}.swagger-ui .contain-m{background-size:contain!important}}@media screen and (min-width:60em){.swagger-ui .cover-l{background-size:cover!important}.swagger-ui .contain-l{background-size:contain!important}}.swagger-ui .bg-center{background-position:50%;background-repeat:no-repeat}.swagger-ui .bg-top{background-position:top;background-repeat:no-repeat}.swagger-ui .bg-right{background-position:100%;background-repeat:no-repeat}.swagger-ui .bg-bottom{background-position:bottom;background-repeat:no-repeat}.swagger-ui .bg-left{background-position:0;background-repeat:no-repeat}@media screen and (min-width:30em){.swagger-ui .bg-center-ns{background-position:50%;background-repeat:no-repeat}.swagger-ui .bg-top-ns{background-position:top;background-repeat:no-repeat}.swagger-ui .bg-right-ns{background-position:100%;background-repeat:no-repeat}.swagger-ui .bg-bottom-ns{background-position:bottom;background-repeat:no-repeat}.swagger-ui .bg-left-ns{background-position:0;background-repeat:no-repeat}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .bg-center-m{background-position:50%;background-repeat:no-repeat}.swagger-ui .bg-top-m{background-position:top;background-repeat:no-repeat}.swagger-ui .bg-right-m{background-position:100%;background-repeat:no-repeat}.swagger-ui .bg-bottom-m{background-position:bottom;background-repeat:no-repeat}.swagger-ui .bg-left-m{background-position:0;background-repeat:no-repeat}}@media screen and (min-width:60em){.swagger-ui .bg-center-l{background-position:50%;background-repeat:no-repeat}.swagger-ui .bg-top-l{background-position:top;background-repeat:no-repeat}.swagger-ui .bg-right-l{background-position:100%;background-repeat:no-repeat}.swagger-ui .bg-bottom-l{background-position:bottom;background-repeat:no-repeat}.swagger-ui .bg-left-l{background-position:0;background-repeat:no-repeat}}.swagger-ui .outline{outline:1px solid}.swagger-ui .outline-transparent{outline:1px solid transparent}.swagger-ui .outline-0{outline:0}@media screen and (min-width:30em){.swagger-ui .outline-ns{outline:1px solid}.swagger-ui .outline-transparent-ns{outline:1px solid transparent}.swagger-ui .outline-0-ns{outline:0}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .outline-m{outline:1px solid}.swagger-ui .outline-transparent-m{outline:1px solid transparent}.swagger-ui .outline-0-m{outline:0}}@media screen and (min-width:60em){.swagger-ui .outline-l{outline:1px solid}.swagger-ui .outline-transparent-l{outline:1px solid transparent}.swagger-ui .outline-0-l{outline:0}}.swagger-ui .ba{border-style:solid;border-width:1px}.swagger-ui .bt{border-top-style:solid;border-top-width:1px}.swagger-ui .br{border-right-style:solid;border-right-width:1px}.swagger-ui .bb{border-bottom-style:solid;border-bottom-width:1px}.swagger-ui .bl{border-left-style:solid;border-left-width:1px}.swagger-ui .bn{border-style:none;border-width:0}@media screen and (min-width:30em){.swagger-ui .ba-ns{border-style:solid;border-width:1px}.swagger-ui .bt-ns{border-top-style:solid;border-top-width:1px}.swagger-ui .br-ns{border-right-style:solid;border-right-width:1px}.swagger-ui .bb-ns{border-bottom-style:solid;border-bottom-width:1px}.swagger-ui .bl-ns{border-left-style:solid;border-left-width:1px}.swagger-ui .bn-ns{border-style:none;border-width:0}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .ba-m{border-style:solid;border-width:1px}.swagger-ui .bt-m{border-top-style:solid;border-top-width:1px}.swagger-ui .br-m{border-right-style:solid;border-right-width:1px}.swagger-ui .bb-m{border-bottom-style:solid;border-bottom-width:1px}.swagger-ui .bl-m{border-left-style:solid;border-left-width:1px}.swagger-ui .bn-m{border-style:none;border-width:0}}@media screen and (min-width:60em){.swagger-ui .ba-l{border-style:solid;border-width:1px}.swagger-ui .bt-l{border-top-style:solid;border-top-width:1px}.swagger-ui .br-l{border-right-style:solid;border-right-width:1px}.swagger-ui .bb-l{border-bottom-style:solid;border-bottom-width:1px}.swagger-ui .bl-l{border-left-style:solid;border-left-width:1px}.swagger-ui .bn-l{border-style:none;border-width:0}}.swagger-ui .b--black{border-color:#000}.swagger-ui .b--near-black{border-color:#111}.swagger-ui .b--dark-gray{border-color:#333}.swagger-ui .b--mid-gray{border-color:#555}.swagger-ui .b--gray{border-color:#777}.swagger-ui .b--silver{border-color:#999}.swagger-ui .b--light-silver{border-color:#aaa}.swagger-ui .b--moon-gray{border-color:#ccc}.swagger-ui .b--light-gray{border-color:#eee}.swagger-ui .b--near-white{border-color:#f4f4f4}.swagger-ui .b--white{border-color:#fff}.swagger-ui .b--white-90{border-color:hsla(0,0%,100%,.9)}.swagger-ui .b--white-80{border-color:hsla(0,0%,100%,.8)}.swagger-ui .b--white-70{border-color:hsla(0,0%,100%,.7)}.swagger-ui .b--white-60{border-color:hsla(0,0%,100%,.6)}.swagger-ui .b--white-50{border-color:hsla(0,0%,100%,.5)}.swagger-ui .b--white-40{border-color:hsla(0,0%,100%,.4)}.swagger-ui .b--white-30{border-color:hsla(0,0%,100%,.3)}.swagger-ui .b--white-20{border-color:hsla(0,0%,100%,.2)}.swagger-ui .b--white-10{border-color:hsla(0,0%,100%,.1)}.swagger-ui .b--white-05{border-color:hsla(0,0%,100%,.05)}.swagger-ui .b--white-025{border-color:hsla(0,0%,100%,.025)}.swagger-ui .b--white-0125{border-color:hsla(0,0%,100%,.013)}.swagger-ui .b--black-90{border-color:rgba(0,0,0,.9)}.swagger-ui .b--black-80{border-color:rgba(0,0,0,.8)}.swagger-ui .b--black-70{border-color:rgba(0,0,0,.7)}.swagger-ui .b--black-60{border-color:rgba(0,0,0,.6)}.swagger-ui .b--black-50{border-color:rgba(0,0,0,.5)}.swagger-ui .b--black-40{border-color:rgba(0,0,0,.4)}.swagger-ui .b--black-30{border-color:rgba(0,0,0,.3)}.swagger-ui .b--black-20{border-color:rgba(0,0,0,.2)}.swagger-ui .b--black-10{border-color:rgba(0,0,0,.1)}.swagger-ui .b--black-05{border-color:rgba(0,0,0,.05)}.swagger-ui .b--black-025{border-color:rgba(0,0,0,.025)}.swagger-ui .b--black-0125{border-color:rgba(0,0,0,.013)}.swagger-ui .b--dark-red{border-color:#e7040f}.swagger-ui .b--red{border-color:#ff4136}.swagger-ui .b--light-red{border-color:#ff725c}.swagger-ui .b--orange{border-color:#ff6300}.swagger-ui .b--gold{border-color:#ffb700}.swagger-ui .b--yellow{border-color:gold}.swagger-ui .b--light-yellow{border-color:#fbf1a9}.swagger-ui .b--purple{border-color:#5e2ca5}.swagger-ui .b--light-purple{border-color:#a463f2}.swagger-ui .b--dark-pink{border-color:#d5008f}.swagger-ui .b--hot-pink{border-color:#ff41b4}.swagger-ui .b--pink{border-color:#ff80cc}.swagger-ui .b--light-pink{border-color:#ffa3d7}.swagger-ui .b--dark-green{border-color:#137752}.swagger-ui .b--green{border-color:#19a974}.swagger-ui .b--light-green{border-color:#9eebcf}.swagger-ui .b--navy{border-color:#001b44}.swagger-ui .b--dark-blue{border-color:#00449e}.swagger-ui .b--blue{border-color:#357edd}.swagger-ui .b--light-blue{border-color:#96ccff}.swagger-ui .b--lightest-blue{border-color:#cdecff}.swagger-ui .b--washed-blue{border-color:#f6fffe}.swagger-ui .b--washed-green{border-color:#e8fdf5}.swagger-ui .b--washed-yellow{border-color:#fffceb}.swagger-ui .b--washed-red{border-color:#ffdfdf}.swagger-ui .b--transparent{border-color:transparent}.swagger-ui .b--inherit{border-color:inherit}.swagger-ui .br0{border-radius:0}.swagger-ui .br1{border-radius:.125rem}.swagger-ui .br2{border-radius:.25rem}.swagger-ui .br3{border-radius:.5rem}.swagger-ui .br4{border-radius:1rem}.swagger-ui .br-100{border-radius:100%}.swagger-ui .br-pill{border-radius:9999px}.swagger-ui .br--bottom{border-top-left-radius:0;border-top-right-radius:0}.swagger-ui .br--top{border-bottom-left-radius:0;border-bottom-right-radius:0}.swagger-ui .br--right{border-bottom-left-radius:0;border-top-left-radius:0}.swagger-ui .br--left{border-bottom-right-radius:0;border-top-right-radius:0}@media screen and (min-width:30em){.swagger-ui .br0-ns{border-radius:0}.swagger-ui .br1-ns{border-radius:.125rem}.swagger-ui .br2-ns{border-radius:.25rem}.swagger-ui .br3-ns{border-radius:.5rem}.swagger-ui .br4-ns{border-radius:1rem}.swagger-ui .br-100-ns{border-radius:100%}.swagger-ui .br-pill-ns{border-radius:9999px}.swagger-ui .br--bottom-ns{border-top-left-radius:0;border-top-right-radius:0}.swagger-ui .br--top-ns{border-bottom-left-radius:0;border-bottom-right-radius:0}.swagger-ui .br--right-ns{border-bottom-left-radius:0;border-top-left-radius:0}.swagger-ui .br--left-ns{border-bottom-right-radius:0;border-top-right-radius:0}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .br0-m{border-radius:0}.swagger-ui .br1-m{border-radius:.125rem}.swagger-ui .br2-m{border-radius:.25rem}.swagger-ui .br3-m{border-radius:.5rem}.swagger-ui .br4-m{border-radius:1rem}.swagger-ui .br-100-m{border-radius:100%}.swagger-ui .br-pill-m{border-radius:9999px}.swagger-ui .br--bottom-m{border-top-left-radius:0;border-top-right-radius:0}.swagger-ui .br--top-m{border-bottom-left-radius:0;border-bottom-right-radius:0}.swagger-ui .br--right-m{border-bottom-left-radius:0;border-top-left-radius:0}.swagger-ui .br--left-m{border-bottom-right-radius:0;border-top-right-radius:0}}@media screen and (min-width:60em){.swagger-ui .br0-l{border-radius:0}.swagger-ui .br1-l{border-radius:.125rem}.swagger-ui .br2-l{border-radius:.25rem}.swagger-ui .br3-l{border-radius:.5rem}.swagger-ui .br4-l{border-radius:1rem}.swagger-ui .br-100-l{border-radius:100%}.swagger-ui .br-pill-l{border-radius:9999px}.swagger-ui .br--bottom-l{border-top-left-radius:0;border-top-right-radius:0}.swagger-ui .br--top-l{border-bottom-left-radius:0;border-bottom-right-radius:0}.swagger-ui .br--right-l{border-bottom-left-radius:0;border-top-left-radius:0}.swagger-ui .br--left-l{border-bottom-right-radius:0;border-top-right-radius:0}}.swagger-ui .b--dotted{border-style:dotted}.swagger-ui .b--dashed{border-style:dashed}.swagger-ui .b--solid{border-style:solid}.swagger-ui .b--none{border-style:none}@media screen and (min-width:30em){.swagger-ui .b--dotted-ns{border-style:dotted}.swagger-ui .b--dashed-ns{border-style:dashed}.swagger-ui .b--solid-ns{border-style:solid}.swagger-ui .b--none-ns{border-style:none}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .b--dotted-m{border-style:dotted}.swagger-ui .b--dashed-m{border-style:dashed}.swagger-ui .b--solid-m{border-style:solid}.swagger-ui .b--none-m{border-style:none}}@media screen and (min-width:60em){.swagger-ui .b--dotted-l{border-style:dotted}.swagger-ui .b--dashed-l{border-style:dashed}.swagger-ui .b--solid-l{border-style:solid}.swagger-ui .b--none-l{border-style:none}}.swagger-ui .bw0{border-width:0}.swagger-ui .bw1{border-width:.125rem}.swagger-ui .bw2{border-width:.25rem}.swagger-ui .bw3{border-width:.5rem}.swagger-ui .bw4{border-width:1rem}.swagger-ui .bw5{border-width:2rem}.swagger-ui .bt-0{border-top-width:0}.swagger-ui .br-0{border-right-width:0}.swagger-ui .bb-0{border-bottom-width:0}.swagger-ui .bl-0{border-left-width:0}@media screen and (min-width:30em){.swagger-ui .bw0-ns{border-width:0}.swagger-ui .bw1-ns{border-width:.125rem}.swagger-ui .bw2-ns{border-width:.25rem}.swagger-ui .bw3-ns{border-width:.5rem}.swagger-ui .bw4-ns{border-width:1rem}.swagger-ui .bw5-ns{border-width:2rem}.swagger-ui .bt-0-ns{border-top-width:0}.swagger-ui .br-0-ns{border-right-width:0}.swagger-ui .bb-0-ns{border-bottom-width:0}.swagger-ui .bl-0-ns{border-left-width:0}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .bw0-m{border-width:0}.swagger-ui .bw1-m{border-width:.125rem}.swagger-ui .bw2-m{border-width:.25rem}.swagger-ui .bw3-m{border-width:.5rem}.swagger-ui .bw4-m{border-width:1rem}.swagger-ui .bw5-m{border-width:2rem}.swagger-ui .bt-0-m{border-top-width:0}.swagger-ui .br-0-m{border-right-width:0}.swagger-ui .bb-0-m{border-bottom-width:0}.swagger-ui .bl-0-m{border-left-width:0}}@media screen and (min-width:60em){.swagger-ui .bw0-l{border-width:0}.swagger-ui .bw1-l{border-width:.125rem}.swagger-ui .bw2-l{border-width:.25rem}.swagger-ui .bw3-l{border-width:.5rem}.swagger-ui .bw4-l{border-width:1rem}.swagger-ui .bw5-l{border-width:2rem}.swagger-ui .bt-0-l{border-top-width:0}.swagger-ui .br-0-l{border-right-width:0}.swagger-ui .bb-0-l{border-bottom-width:0}.swagger-ui .bl-0-l{border-left-width:0}}.swagger-ui .shadow-1{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-2{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-3{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-4{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.swagger-ui .shadow-5{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}@media screen and (min-width:30em){.swagger-ui .shadow-1-ns{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-2-ns{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-3-ns{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-4-ns{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.swagger-ui .shadow-5-ns{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .shadow-1-m{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-2-m{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-3-m{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-4-m{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.swagger-ui .shadow-5-m{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}}@media screen and (min-width:60em){.swagger-ui .shadow-1-l{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-2-l{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-3-l{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.swagger-ui .shadow-4-l{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.swagger-ui .shadow-5-l{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}}.swagger-ui .pre{overflow-x:auto;overflow-y:hidden;overflow:scroll}.swagger-ui .top-0{top:0}.swagger-ui .right-0{right:0}.swagger-ui .bottom-0{bottom:0}.swagger-ui .left-0{left:0}.swagger-ui .top-1{top:1rem}.swagger-ui .right-1{right:1rem}.swagger-ui .bottom-1{bottom:1rem}.swagger-ui .left-1{left:1rem}.swagger-ui .top-2{top:2rem}.swagger-ui .right-2{right:2rem}.swagger-ui .bottom-2{bottom:2rem}.swagger-ui .left-2{left:2rem}.swagger-ui .top--1{top:-1rem}.swagger-ui .right--1{right:-1rem}.swagger-ui .bottom--1{bottom:-1rem}.swagger-ui .left--1{left:-1rem}.swagger-ui .top--2{top:-2rem}.swagger-ui .right--2{right:-2rem}.swagger-ui .bottom--2{bottom:-2rem}.swagger-ui .left--2{left:-2rem}.swagger-ui .absolute--fill{bottom:0;left:0;right:0;top:0}@media screen and (min-width:30em){.swagger-ui .top-0-ns{top:0}.swagger-ui .left-0-ns{left:0}.swagger-ui .right-0-ns{right:0}.swagger-ui .bottom-0-ns{bottom:0}.swagger-ui .top-1-ns{top:1rem}.swagger-ui .left-1-ns{left:1rem}.swagger-ui .right-1-ns{right:1rem}.swagger-ui .bottom-1-ns{bottom:1rem}.swagger-ui .top-2-ns{top:2rem}.swagger-ui .left-2-ns{left:2rem}.swagger-ui .right-2-ns{right:2rem}.swagger-ui .bottom-2-ns{bottom:2rem}.swagger-ui .top--1-ns{top:-1rem}.swagger-ui .right--1-ns{right:-1rem}.swagger-ui .bottom--1-ns{bottom:-1rem}.swagger-ui .left--1-ns{left:-1rem}.swagger-ui .top--2-ns{top:-2rem}.swagger-ui .right--2-ns{right:-2rem}.swagger-ui .bottom--2-ns{bottom:-2rem}.swagger-ui .left--2-ns{left:-2rem}.swagger-ui .absolute--fill-ns{bottom:0;left:0;right:0;top:0}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .top-0-m{top:0}.swagger-ui .left-0-m{left:0}.swagger-ui .right-0-m{right:0}.swagger-ui .bottom-0-m{bottom:0}.swagger-ui .top-1-m{top:1rem}.swagger-ui .left-1-m{left:1rem}.swagger-ui .right-1-m{right:1rem}.swagger-ui .bottom-1-m{bottom:1rem}.swagger-ui .top-2-m{top:2rem}.swagger-ui .left-2-m{left:2rem}.swagger-ui .right-2-m{right:2rem}.swagger-ui .bottom-2-m{bottom:2rem}.swagger-ui .top--1-m{top:-1rem}.swagger-ui .right--1-m{right:-1rem}.swagger-ui .bottom--1-m{bottom:-1rem}.swagger-ui .left--1-m{left:-1rem}.swagger-ui .top--2-m{top:-2rem}.swagger-ui .right--2-m{right:-2rem}.swagger-ui .bottom--2-m{bottom:-2rem}.swagger-ui .left--2-m{left:-2rem}.swagger-ui .absolute--fill-m{bottom:0;left:0;right:0;top:0}}@media screen and (min-width:60em){.swagger-ui .top-0-l{top:0}.swagger-ui .left-0-l{left:0}.swagger-ui .right-0-l{right:0}.swagger-ui .bottom-0-l{bottom:0}.swagger-ui .top-1-l{top:1rem}.swagger-ui .left-1-l{left:1rem}.swagger-ui .right-1-l{right:1rem}.swagger-ui .bottom-1-l{bottom:1rem}.swagger-ui .top-2-l{top:2rem}.swagger-ui .left-2-l{left:2rem}.swagger-ui .right-2-l{right:2rem}.swagger-ui .bottom-2-l{bottom:2rem}.swagger-ui .top--1-l{top:-1rem}.swagger-ui .right--1-l{right:-1rem}.swagger-ui .bottom--1-l{bottom:-1rem}.swagger-ui .left--1-l{left:-1rem}.swagger-ui .top--2-l{top:-2rem}.swagger-ui .right--2-l{right:-2rem}.swagger-ui .bottom--2-l{bottom:-2rem}.swagger-ui .left--2-l{left:-2rem}.swagger-ui .absolute--fill-l{bottom:0;left:0;right:0;top:0}}.swagger-ui .cf:after,.swagger-ui .cf:before{content:\" \";display:table}.swagger-ui .cf:after{clear:both}.swagger-ui .cf{zoom:1}.swagger-ui .cl{clear:left}.swagger-ui .cr{clear:right}.swagger-ui .cb{clear:both}.swagger-ui .cn{clear:none}@media screen and (min-width:30em){.swagger-ui .cl-ns{clear:left}.swagger-ui .cr-ns{clear:right}.swagger-ui .cb-ns{clear:both}.swagger-ui .cn-ns{clear:none}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .cl-m{clear:left}.swagger-ui .cr-m{clear:right}.swagger-ui .cb-m{clear:both}.swagger-ui .cn-m{clear:none}}@media screen and (min-width:60em){.swagger-ui .cl-l{clear:left}.swagger-ui .cr-l{clear:right}.swagger-ui .cb-l{clear:both}.swagger-ui .cn-l{clear:none}}.swagger-ui .flex{display:flex}.swagger-ui .inline-flex{display:inline-flex}.swagger-ui .flex-auto{flex:1 1 auto;min-height:0;min-width:0}.swagger-ui .flex-none{flex:none}.swagger-ui .flex-column{flex-direction:column}.swagger-ui .flex-row{flex-direction:row}.swagger-ui .flex-wrap{flex-wrap:wrap}.swagger-ui .flex-nowrap{flex-wrap:nowrap}.swagger-ui .flex-wrap-reverse{flex-wrap:wrap-reverse}.swagger-ui .flex-column-reverse{flex-direction:column-reverse}.swagger-ui .flex-row-reverse{flex-direction:row-reverse}.swagger-ui .items-start{align-items:flex-start}.swagger-ui .items-end{align-items:flex-end}.swagger-ui .items-center{align-items:center}.swagger-ui .items-baseline{align-items:baseline}.swagger-ui .items-stretch{align-items:stretch}.swagger-ui .self-start{align-self:flex-start}.swagger-ui .self-end{align-self:flex-end}.swagger-ui .self-center{align-self:center}.swagger-ui .self-baseline{align-self:baseline}.swagger-ui .self-stretch{align-self:stretch}.swagger-ui .justify-start{justify-content:flex-start}.swagger-ui .justify-end{justify-content:flex-end}.swagger-ui .justify-center{justify-content:center}.swagger-ui .justify-between{justify-content:space-between}.swagger-ui .justify-around{justify-content:space-around}.swagger-ui .content-start{align-content:flex-start}.swagger-ui .content-end{align-content:flex-end}.swagger-ui .content-center{align-content:center}.swagger-ui .content-between{align-content:space-between}.swagger-ui .content-around{align-content:space-around}.swagger-ui .content-stretch{align-content:stretch}.swagger-ui .order-0{order:0}.swagger-ui .order-1{order:1}.swagger-ui .order-2{order:2}.swagger-ui .order-3{order:3}.swagger-ui .order-4{order:4}.swagger-ui .order-5{order:5}.swagger-ui .order-6{order:6}.swagger-ui .order-7{order:7}.swagger-ui .order-8{order:8}.swagger-ui .order-last{order:99999}.swagger-ui .flex-grow-0{flex-grow:0}.swagger-ui .flex-grow-1{flex-grow:1}.swagger-ui .flex-shrink-0{flex-shrink:0}.swagger-ui .flex-shrink-1{flex-shrink:1}@media screen and (min-width:30em){.swagger-ui .flex-ns{display:flex}.swagger-ui .inline-flex-ns{display:inline-flex}.swagger-ui .flex-auto-ns{flex:1 1 auto;min-height:0;min-width:0}.swagger-ui .flex-none-ns{flex:none}.swagger-ui .flex-column-ns{flex-direction:column}.swagger-ui .flex-row-ns{flex-direction:row}.swagger-ui .flex-wrap-ns{flex-wrap:wrap}.swagger-ui .flex-nowrap-ns{flex-wrap:nowrap}.swagger-ui .flex-wrap-reverse-ns{flex-wrap:wrap-reverse}.swagger-ui .flex-column-reverse-ns{flex-direction:column-reverse}.swagger-ui .flex-row-reverse-ns{flex-direction:row-reverse}.swagger-ui .items-start-ns{align-items:flex-start}.swagger-ui .items-end-ns{align-items:flex-end}.swagger-ui .items-center-ns{align-items:center}.swagger-ui .items-baseline-ns{align-items:baseline}.swagger-ui .items-stretch-ns{align-items:stretch}.swagger-ui .self-start-ns{align-self:flex-start}.swagger-ui .self-end-ns{align-self:flex-end}.swagger-ui .self-center-ns{align-self:center}.swagger-ui .self-baseline-ns{align-self:baseline}.swagger-ui .self-stretch-ns{align-self:stretch}.swagger-ui .justify-start-ns{justify-content:flex-start}.swagger-ui .justify-end-ns{justify-content:flex-end}.swagger-ui .justify-center-ns{justify-content:center}.swagger-ui .justify-between-ns{justify-content:space-between}.swagger-ui .justify-around-ns{justify-content:space-around}.swagger-ui .content-start-ns{align-content:flex-start}.swagger-ui .content-end-ns{align-content:flex-end}.swagger-ui .content-center-ns{align-content:center}.swagger-ui .content-between-ns{align-content:space-between}.swagger-ui .content-around-ns{align-content:space-around}.swagger-ui .content-stretch-ns{align-content:stretch}.swagger-ui .order-0-ns{order:0}.swagger-ui .order-1-ns{order:1}.swagger-ui .order-2-ns{order:2}.swagger-ui .order-3-ns{order:3}.swagger-ui .order-4-ns{order:4}.swagger-ui .order-5-ns{order:5}.swagger-ui .order-6-ns{order:6}.swagger-ui .order-7-ns{order:7}.swagger-ui .order-8-ns{order:8}.swagger-ui .order-last-ns{order:99999}.swagger-ui .flex-grow-0-ns{flex-grow:0}.swagger-ui .flex-grow-1-ns{flex-grow:1}.swagger-ui .flex-shrink-0-ns{flex-shrink:0}.swagger-ui .flex-shrink-1-ns{flex-shrink:1}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .flex-m{display:flex}.swagger-ui .inline-flex-m{display:inline-flex}.swagger-ui .flex-auto-m{flex:1 1 auto;min-height:0;min-width:0}.swagger-ui .flex-none-m{flex:none}.swagger-ui .flex-column-m{flex-direction:column}.swagger-ui .flex-row-m{flex-direction:row}.swagger-ui .flex-wrap-m{flex-wrap:wrap}.swagger-ui .flex-nowrap-m{flex-wrap:nowrap}.swagger-ui .flex-wrap-reverse-m{flex-wrap:wrap-reverse}.swagger-ui .flex-column-reverse-m{flex-direction:column-reverse}.swagger-ui .flex-row-reverse-m{flex-direction:row-reverse}.swagger-ui .items-start-m{align-items:flex-start}.swagger-ui .items-end-m{align-items:flex-end}.swagger-ui .items-center-m{align-items:center}.swagger-ui .items-baseline-m{align-items:baseline}.swagger-ui .items-stretch-m{align-items:stretch}.swagger-ui .self-start-m{align-self:flex-start}.swagger-ui .self-end-m{align-self:flex-end}.swagger-ui .self-center-m{align-self:center}.swagger-ui .self-baseline-m{align-self:baseline}.swagger-ui .self-stretch-m{align-self:stretch}.swagger-ui .justify-start-m{justify-content:flex-start}.swagger-ui .justify-end-m{justify-content:flex-end}.swagger-ui .justify-center-m{justify-content:center}.swagger-ui .justify-between-m{justify-content:space-between}.swagger-ui .justify-around-m{justify-content:space-around}.swagger-ui .content-start-m{align-content:flex-start}.swagger-ui .content-end-m{align-content:flex-end}.swagger-ui .content-center-m{align-content:center}.swagger-ui .content-between-m{align-content:space-between}.swagger-ui .content-around-m{align-content:space-around}.swagger-ui .content-stretch-m{align-content:stretch}.swagger-ui .order-0-m{order:0}.swagger-ui .order-1-m{order:1}.swagger-ui .order-2-m{order:2}.swagger-ui .order-3-m{order:3}.swagger-ui .order-4-m{order:4}.swagger-ui .order-5-m{order:5}.swagger-ui .order-6-m{order:6}.swagger-ui .order-7-m{order:7}.swagger-ui .order-8-m{order:8}.swagger-ui .order-last-m{order:99999}.swagger-ui .flex-grow-0-m{flex-grow:0}.swagger-ui .flex-grow-1-m{flex-grow:1}.swagger-ui .flex-shrink-0-m{flex-shrink:0}.swagger-ui .flex-shrink-1-m{flex-shrink:1}}@media screen and (min-width:60em){.swagger-ui .flex-l{display:flex}.swagger-ui .inline-flex-l{display:inline-flex}.swagger-ui .flex-auto-l{flex:1 1 auto;min-height:0;min-width:0}.swagger-ui .flex-none-l{flex:none}.swagger-ui .flex-column-l{flex-direction:column}.swagger-ui .flex-row-l{flex-direction:row}.swagger-ui .flex-wrap-l{flex-wrap:wrap}.swagger-ui .flex-nowrap-l{flex-wrap:nowrap}.swagger-ui .flex-wrap-reverse-l{flex-wrap:wrap-reverse}.swagger-ui .flex-column-reverse-l{flex-direction:column-reverse}.swagger-ui .flex-row-reverse-l{flex-direction:row-reverse}.swagger-ui .items-start-l{align-items:flex-start}.swagger-ui .items-end-l{align-items:flex-end}.swagger-ui .items-center-l{align-items:center}.swagger-ui .items-baseline-l{align-items:baseline}.swagger-ui .items-stretch-l{align-items:stretch}.swagger-ui .self-start-l{align-self:flex-start}.swagger-ui .self-end-l{align-self:flex-end}.swagger-ui .self-center-l{align-self:center}.swagger-ui .self-baseline-l{align-self:baseline}.swagger-ui .self-stretch-l{align-self:stretch}.swagger-ui .justify-start-l{justify-content:flex-start}.swagger-ui .justify-end-l{justify-content:flex-end}.swagger-ui .justify-center-l{justify-content:center}.swagger-ui .justify-between-l{justify-content:space-between}.swagger-ui .justify-around-l{justify-content:space-around}.swagger-ui .content-start-l{align-content:flex-start}.swagger-ui .content-end-l{align-content:flex-end}.swagger-ui .content-center-l{align-content:center}.swagger-ui .content-between-l{align-content:space-between}.swagger-ui .content-around-l{align-content:space-around}.swagger-ui .content-stretch-l{align-content:stretch}.swagger-ui .order-0-l{order:0}.swagger-ui .order-1-l{order:1}.swagger-ui .order-2-l{order:2}.swagger-ui .order-3-l{order:3}.swagger-ui .order-4-l{order:4}.swagger-ui .order-5-l{order:5}.swagger-ui .order-6-l{order:6}.swagger-ui .order-7-l{order:7}.swagger-ui .order-8-l{order:8}.swagger-ui .order-last-l{order:99999}.swagger-ui .flex-grow-0-l{flex-grow:0}.swagger-ui .flex-grow-1-l{flex-grow:1}.swagger-ui .flex-shrink-0-l{flex-shrink:0}.swagger-ui .flex-shrink-1-l{flex-shrink:1}}.swagger-ui .dn{display:none}.swagger-ui .di{display:inline}.swagger-ui .db{display:block}.swagger-ui .dib{display:inline-block}.swagger-ui .dit{display:inline-table}.swagger-ui .dt{display:table}.swagger-ui .dtc{display:table-cell}.swagger-ui .dt-row{display:table-row}.swagger-ui .dt-row-group{display:table-row-group}.swagger-ui .dt-column{display:table-column}.swagger-ui .dt-column-group{display:table-column-group}.swagger-ui .dt--fixed{table-layout:fixed;width:100%}@media screen and (min-width:30em){.swagger-ui .dn-ns{display:none}.swagger-ui .di-ns{display:inline}.swagger-ui .db-ns{display:block}.swagger-ui .dib-ns{display:inline-block}.swagger-ui .dit-ns{display:inline-table}.swagger-ui .dt-ns{display:table}.swagger-ui .dtc-ns{display:table-cell}.swagger-ui .dt-row-ns{display:table-row}.swagger-ui .dt-row-group-ns{display:table-row-group}.swagger-ui .dt-column-ns{display:table-column}.swagger-ui .dt-column-group-ns{display:table-column-group}.swagger-ui .dt--fixed-ns{table-layout:fixed;width:100%}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .dn-m{display:none}.swagger-ui .di-m{display:inline}.swagger-ui .db-m{display:block}.swagger-ui .dib-m{display:inline-block}.swagger-ui .dit-m{display:inline-table}.swagger-ui .dt-m{display:table}.swagger-ui .dtc-m{display:table-cell}.swagger-ui .dt-row-m{display:table-row}.swagger-ui .dt-row-group-m{display:table-row-group}.swagger-ui .dt-column-m{display:table-column}.swagger-ui .dt-column-group-m{display:table-column-group}.swagger-ui .dt--fixed-m{table-layout:fixed;width:100%}}@media screen and (min-width:60em){.swagger-ui .dn-l{display:none}.swagger-ui .di-l{display:inline}.swagger-ui .db-l{display:block}.swagger-ui .dib-l{display:inline-block}.swagger-ui .dit-l{display:inline-table}.swagger-ui .dt-l{display:table}.swagger-ui .dtc-l{display:table-cell}.swagger-ui .dt-row-l{display:table-row}.swagger-ui .dt-row-group-l{display:table-row-group}.swagger-ui .dt-column-l{display:table-column}.swagger-ui .dt-column-group-l{display:table-column-group}.swagger-ui .dt--fixed-l{table-layout:fixed;width:100%}}.swagger-ui .fl{_display:inline;float:left}.swagger-ui .fr{_display:inline;float:right}.swagger-ui .fn{float:none}@media screen and (min-width:30em){.swagger-ui .fl-ns{_display:inline;float:left}.swagger-ui .fr-ns{_display:inline;float:right}.swagger-ui .fn-ns{float:none}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .fl-m{_display:inline;float:left}.swagger-ui .fr-m{_display:inline;float:right}.swagger-ui .fn-m{float:none}}@media screen and (min-width:60em){.swagger-ui .fl-l{_display:inline;float:left}.swagger-ui .fr-l{_display:inline;float:right}.swagger-ui .fn-l{float:none}}.swagger-ui .sans-serif{font-family:-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.swagger-ui .serif{font-family:georgia,serif}.swagger-ui .system-sans-serif{font-family:sans-serif}.swagger-ui .system-serif{font-family:serif}.swagger-ui .code,.swagger-ui code{font-family:Consolas,monaco,monospace}.swagger-ui .courier{font-family:Courier Next,courier,monospace}.swagger-ui .helvetica{font-family:helvetica neue,helvetica,sans-serif}.swagger-ui .avenir{font-family:avenir next,avenir,sans-serif}.swagger-ui .athelas{font-family:athelas,georgia,serif}.swagger-ui .georgia{font-family:georgia,serif}.swagger-ui .times{font-family:times,serif}.swagger-ui .bodoni{font-family:Bodoni MT,serif}.swagger-ui .calisto{font-family:Calisto MT,serif}.swagger-ui .garamond{font-family:garamond,serif}.swagger-ui .baskerville{font-family:baskerville,serif}.swagger-ui .i{font-style:italic}.swagger-ui .fs-normal{font-style:normal}@media screen and (min-width:30em){.swagger-ui .i-ns{font-style:italic}.swagger-ui .fs-normal-ns{font-style:normal}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .i-m{font-style:italic}.swagger-ui .fs-normal-m{font-style:normal}}@media screen and (min-width:60em){.swagger-ui .i-l{font-style:italic}.swagger-ui .fs-normal-l{font-style:normal}}.swagger-ui .normal{font-weight:400}.swagger-ui .b{font-weight:700}.swagger-ui .fw1{font-weight:100}.swagger-ui .fw2{font-weight:200}.swagger-ui .fw3{font-weight:300}.swagger-ui .fw4{font-weight:400}.swagger-ui .fw5{font-weight:500}.swagger-ui .fw6{font-weight:600}.swagger-ui .fw7{font-weight:700}.swagger-ui .fw8{font-weight:800}.swagger-ui .fw9{font-weight:900}@media screen and (min-width:30em){.swagger-ui .normal-ns{font-weight:400}.swagger-ui .b-ns{font-weight:700}.swagger-ui .fw1-ns{font-weight:100}.swagger-ui .fw2-ns{font-weight:200}.swagger-ui .fw3-ns{font-weight:300}.swagger-ui .fw4-ns{font-weight:400}.swagger-ui .fw5-ns{font-weight:500}.swagger-ui .fw6-ns{font-weight:600}.swagger-ui .fw7-ns{font-weight:700}.swagger-ui .fw8-ns{font-weight:800}.swagger-ui .fw9-ns{font-weight:900}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .normal-m{font-weight:400}.swagger-ui .b-m{font-weight:700}.swagger-ui .fw1-m{font-weight:100}.swagger-ui .fw2-m{font-weight:200}.swagger-ui .fw3-m{font-weight:300}.swagger-ui .fw4-m{font-weight:400}.swagger-ui .fw5-m{font-weight:500}.swagger-ui .fw6-m{font-weight:600}.swagger-ui .fw7-m{font-weight:700}.swagger-ui .fw8-m{font-weight:800}.swagger-ui .fw9-m{font-weight:900}}@media screen and (min-width:60em){.swagger-ui .normal-l{font-weight:400}.swagger-ui .b-l{font-weight:700}.swagger-ui .fw1-l{font-weight:100}.swagger-ui .fw2-l{font-weight:200}.swagger-ui .fw3-l{font-weight:300}.swagger-ui .fw4-l{font-weight:400}.swagger-ui .fw5-l{font-weight:500}.swagger-ui .fw6-l{font-weight:600}.swagger-ui .fw7-l{font-weight:700}.swagger-ui .fw8-l{font-weight:800}.swagger-ui .fw9-l{font-weight:900}}.swagger-ui .input-reset{-webkit-appearance:none;-moz-appearance:none}.swagger-ui .button-reset::-moz-focus-inner,.swagger-ui .input-reset::-moz-focus-inner{border:0;padding:0}.swagger-ui .h1{height:1rem}.swagger-ui .h2{height:2rem}.swagger-ui .h3{height:4rem}.swagger-ui .h4{height:8rem}.swagger-ui .h5{height:16rem}.swagger-ui .h-25{height:25%}.swagger-ui .h-50{height:50%}.swagger-ui .h-75{height:75%}.swagger-ui .h-100{height:100%}.swagger-ui .min-h-100{min-height:100%}.swagger-ui .vh-25{height:25vh}.swagger-ui .vh-50{height:50vh}.swagger-ui .vh-75{height:75vh}.swagger-ui .vh-100{height:100vh}.swagger-ui .min-vh-100{min-height:100vh}.swagger-ui .h-auto{height:auto}.swagger-ui .h-inherit{height:inherit}@media screen and (min-width:30em){.swagger-ui .h1-ns{height:1rem}.swagger-ui .h2-ns{height:2rem}.swagger-ui .h3-ns{height:4rem}.swagger-ui .h4-ns{height:8rem}.swagger-ui .h5-ns{height:16rem}.swagger-ui .h-25-ns{height:25%}.swagger-ui .h-50-ns{height:50%}.swagger-ui .h-75-ns{height:75%}.swagger-ui .h-100-ns{height:100%}.swagger-ui .min-h-100-ns{min-height:100%}.swagger-ui .vh-25-ns{height:25vh}.swagger-ui .vh-50-ns{height:50vh}.swagger-ui .vh-75-ns{height:75vh}.swagger-ui .vh-100-ns{height:100vh}.swagger-ui .min-vh-100-ns{min-height:100vh}.swagger-ui .h-auto-ns{height:auto}.swagger-ui .h-inherit-ns{height:inherit}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .h1-m{height:1rem}.swagger-ui .h2-m{height:2rem}.swagger-ui .h3-m{height:4rem}.swagger-ui .h4-m{height:8rem}.swagger-ui .h5-m{height:16rem}.swagger-ui .h-25-m{height:25%}.swagger-ui .h-50-m{height:50%}.swagger-ui .h-75-m{height:75%}.swagger-ui .h-100-m{height:100%}.swagger-ui .min-h-100-m{min-height:100%}.swagger-ui .vh-25-m{height:25vh}.swagger-ui .vh-50-m{height:50vh}.swagger-ui .vh-75-m{height:75vh}.swagger-ui .vh-100-m{height:100vh}.swagger-ui .min-vh-100-m{min-height:100vh}.swagger-ui .h-auto-m{height:auto}.swagger-ui .h-inherit-m{height:inherit}}@media screen and (min-width:60em){.swagger-ui .h1-l{height:1rem}.swagger-ui .h2-l{height:2rem}.swagger-ui .h3-l{height:4rem}.swagger-ui .h4-l{height:8rem}.swagger-ui .h5-l{height:16rem}.swagger-ui .h-25-l{height:25%}.swagger-ui .h-50-l{height:50%}.swagger-ui .h-75-l{height:75%}.swagger-ui .h-100-l{height:100%}.swagger-ui .min-h-100-l{min-height:100%}.swagger-ui .vh-25-l{height:25vh}.swagger-ui .vh-50-l{height:50vh}.swagger-ui .vh-75-l{height:75vh}.swagger-ui .vh-100-l{height:100vh}.swagger-ui .min-vh-100-l{min-height:100vh}.swagger-ui .h-auto-l{height:auto}.swagger-ui .h-inherit-l{height:inherit}}.swagger-ui .tracked{letter-spacing:.1em}.swagger-ui .tracked-tight{letter-spacing:-.05em}.swagger-ui .tracked-mega{letter-spacing:.25em}@media screen and (min-width:30em){.swagger-ui .tracked-ns{letter-spacing:.1em}.swagger-ui .tracked-tight-ns{letter-spacing:-.05em}.swagger-ui .tracked-mega-ns{letter-spacing:.25em}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .tracked-m{letter-spacing:.1em}.swagger-ui .tracked-tight-m{letter-spacing:-.05em}.swagger-ui .tracked-mega-m{letter-spacing:.25em}}@media screen and (min-width:60em){.swagger-ui .tracked-l{letter-spacing:.1em}.swagger-ui .tracked-tight-l{letter-spacing:-.05em}.swagger-ui .tracked-mega-l{letter-spacing:.25em}}.swagger-ui .lh-solid{line-height:1}.swagger-ui .lh-title{line-height:1.25}.swagger-ui .lh-copy{line-height:1.5}@media screen and (min-width:30em){.swagger-ui .lh-solid-ns{line-height:1}.swagger-ui .lh-title-ns{line-height:1.25}.swagger-ui .lh-copy-ns{line-height:1.5}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .lh-solid-m{line-height:1}.swagger-ui .lh-title-m{line-height:1.25}.swagger-ui .lh-copy-m{line-height:1.5}}@media screen and (min-width:60em){.swagger-ui .lh-solid-l{line-height:1}.swagger-ui .lh-title-l{line-height:1.25}.swagger-ui .lh-copy-l{line-height:1.5}}.swagger-ui .link{-webkit-text-decoration:none;text-decoration:none}.swagger-ui .link,.swagger-ui .link:active,.swagger-ui .link:focus,.swagger-ui .link:hover,.swagger-ui .link:link,.swagger-ui .link:visited{transition:color .15s ease-in}.swagger-ui .link:focus{outline:1px dotted currentColor}.swagger-ui .list{list-style-type:none}.swagger-ui .mw-100{max-width:100%}.swagger-ui .mw1{max-width:1rem}.swagger-ui .mw2{max-width:2rem}.swagger-ui .mw3{max-width:4rem}.swagger-ui .mw4{max-width:8rem}.swagger-ui .mw5{max-width:16rem}.swagger-ui .mw6{max-width:32rem}.swagger-ui .mw7{max-width:48rem}.swagger-ui .mw8{max-width:64rem}.swagger-ui .mw9{max-width:96rem}.swagger-ui .mw-none{max-width:none}@media screen and (min-width:30em){.swagger-ui .mw-100-ns{max-width:100%}.swagger-ui .mw1-ns{max-width:1rem}.swagger-ui .mw2-ns{max-width:2rem}.swagger-ui .mw3-ns{max-width:4rem}.swagger-ui .mw4-ns{max-width:8rem}.swagger-ui .mw5-ns{max-width:16rem}.swagger-ui .mw6-ns{max-width:32rem}.swagger-ui .mw7-ns{max-width:48rem}.swagger-ui .mw8-ns{max-width:64rem}.swagger-ui .mw9-ns{max-width:96rem}.swagger-ui .mw-none-ns{max-width:none}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .mw-100-m{max-width:100%}.swagger-ui .mw1-m{max-width:1rem}.swagger-ui .mw2-m{max-width:2rem}.swagger-ui .mw3-m{max-width:4rem}.swagger-ui .mw4-m{max-width:8rem}.swagger-ui .mw5-m{max-width:16rem}.swagger-ui .mw6-m{max-width:32rem}.swagger-ui .mw7-m{max-width:48rem}.swagger-ui .mw8-m{max-width:64rem}.swagger-ui .mw9-m{max-width:96rem}.swagger-ui .mw-none-m{max-width:none}}@media screen and (min-width:60em){.swagger-ui .mw-100-l{max-width:100%}.swagger-ui .mw1-l{max-width:1rem}.swagger-ui .mw2-l{max-width:2rem}.swagger-ui .mw3-l{max-width:4rem}.swagger-ui .mw4-l{max-width:8rem}.swagger-ui .mw5-l{max-width:16rem}.swagger-ui .mw6-l{max-width:32rem}.swagger-ui .mw7-l{max-width:48rem}.swagger-ui .mw8-l{max-width:64rem}.swagger-ui .mw9-l{max-width:96rem}.swagger-ui .mw-none-l{max-width:none}}.swagger-ui .w1{width:1rem}.swagger-ui .w2{width:2rem}.swagger-ui .w3{width:4rem}.swagger-ui .w4{width:8rem}.swagger-ui .w5{width:16rem}.swagger-ui .w-10{width:10%}.swagger-ui .w-20{width:20%}.swagger-ui .w-25{width:25%}.swagger-ui .w-30{width:30%}.swagger-ui .w-33{width:33%}.swagger-ui .w-34{width:34%}.swagger-ui .w-40{width:40%}.swagger-ui .w-50{width:50%}.swagger-ui .w-60{width:60%}.swagger-ui .w-70{width:70%}.swagger-ui .w-75{width:75%}.swagger-ui .w-80{width:80%}.swagger-ui .w-90{width:90%}.swagger-ui .w-100{width:100%}.swagger-ui .w-third{width:33.3333333333%}.swagger-ui .w-two-thirds{width:66.6666666667%}.swagger-ui .w-auto{width:auto}@media screen and (min-width:30em){.swagger-ui .w1-ns{width:1rem}.swagger-ui .w2-ns{width:2rem}.swagger-ui .w3-ns{width:4rem}.swagger-ui .w4-ns{width:8rem}.swagger-ui .w5-ns{width:16rem}.swagger-ui .w-10-ns{width:10%}.swagger-ui .w-20-ns{width:20%}.swagger-ui .w-25-ns{width:25%}.swagger-ui .w-30-ns{width:30%}.swagger-ui .w-33-ns{width:33%}.swagger-ui .w-34-ns{width:34%}.swagger-ui .w-40-ns{width:40%}.swagger-ui .w-50-ns{width:50%}.swagger-ui .w-60-ns{width:60%}.swagger-ui .w-70-ns{width:70%}.swagger-ui .w-75-ns{width:75%}.swagger-ui .w-80-ns{width:80%}.swagger-ui .w-90-ns{width:90%}.swagger-ui .w-100-ns{width:100%}.swagger-ui .w-third-ns{width:33.3333333333%}.swagger-ui .w-two-thirds-ns{width:66.6666666667%}.swagger-ui .w-auto-ns{width:auto}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .w1-m{width:1rem}.swagger-ui .w2-m{width:2rem}.swagger-ui .w3-m{width:4rem}.swagger-ui .w4-m{width:8rem}.swagger-ui .w5-m{width:16rem}.swagger-ui .w-10-m{width:10%}.swagger-ui .w-20-m{width:20%}.swagger-ui .w-25-m{width:25%}.swagger-ui .w-30-m{width:30%}.swagger-ui .w-33-m{width:33%}.swagger-ui .w-34-m{width:34%}.swagger-ui .w-40-m{width:40%}.swagger-ui .w-50-m{width:50%}.swagger-ui .w-60-m{width:60%}.swagger-ui .w-70-m{width:70%}.swagger-ui .w-75-m{width:75%}.swagger-ui .w-80-m{width:80%}.swagger-ui .w-90-m{width:90%}.swagger-ui .w-100-m{width:100%}.swagger-ui .w-third-m{width:33.3333333333%}.swagger-ui .w-two-thirds-m{width:66.6666666667%}.swagger-ui .w-auto-m{width:auto}}@media screen and (min-width:60em){.swagger-ui .w1-l{width:1rem}.swagger-ui .w2-l{width:2rem}.swagger-ui .w3-l{width:4rem}.swagger-ui .w4-l{width:8rem}.swagger-ui .w5-l{width:16rem}.swagger-ui .w-10-l{width:10%}.swagger-ui .w-20-l{width:20%}.swagger-ui .w-25-l{width:25%}.swagger-ui .w-30-l{width:30%}.swagger-ui .w-33-l{width:33%}.swagger-ui .w-34-l{width:34%}.swagger-ui .w-40-l{width:40%}.swagger-ui .w-50-l{width:50%}.swagger-ui .w-60-l{width:60%}.swagger-ui .w-70-l{width:70%}.swagger-ui .w-75-l{width:75%}.swagger-ui .w-80-l{width:80%}.swagger-ui .w-90-l{width:90%}.swagger-ui .w-100-l{width:100%}.swagger-ui .w-third-l{width:33.3333333333%}.swagger-ui .w-two-thirds-l{width:66.6666666667%}.swagger-ui .w-auto-l{width:auto}}.swagger-ui .overflow-visible{overflow:visible}.swagger-ui .overflow-hidden{overflow:hidden}.swagger-ui .overflow-scroll{overflow:scroll}.swagger-ui .overflow-auto{overflow:auto}.swagger-ui .overflow-x-visible{overflow-x:visible}.swagger-ui .overflow-x-hidden{overflow-x:hidden}.swagger-ui .overflow-x-scroll{overflow-x:scroll}.swagger-ui .overflow-x-auto{overflow-x:auto}.swagger-ui .overflow-y-visible{overflow-y:visible}.swagger-ui .overflow-y-hidden{overflow-y:hidden}.swagger-ui .overflow-y-scroll{overflow-y:scroll}.swagger-ui .overflow-y-auto{overflow-y:auto}@media screen and (min-width:30em){.swagger-ui .overflow-visible-ns{overflow:visible}.swagger-ui .overflow-hidden-ns{overflow:hidden}.swagger-ui .overflow-scroll-ns{overflow:scroll}.swagger-ui .overflow-auto-ns{overflow:auto}.swagger-ui .overflow-x-visible-ns{overflow-x:visible}.swagger-ui .overflow-x-hidden-ns{overflow-x:hidden}.swagger-ui .overflow-x-scroll-ns{overflow-x:scroll}.swagger-ui .overflow-x-auto-ns{overflow-x:auto}.swagger-ui .overflow-y-visible-ns{overflow-y:visible}.swagger-ui .overflow-y-hidden-ns{overflow-y:hidden}.swagger-ui .overflow-y-scroll-ns{overflow-y:scroll}.swagger-ui .overflow-y-auto-ns{overflow-y:auto}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .overflow-visible-m{overflow:visible}.swagger-ui .overflow-hidden-m{overflow:hidden}.swagger-ui .overflow-scroll-m{overflow:scroll}.swagger-ui .overflow-auto-m{overflow:auto}.swagger-ui .overflow-x-visible-m{overflow-x:visible}.swagger-ui .overflow-x-hidden-m{overflow-x:hidden}.swagger-ui .overflow-x-scroll-m{overflow-x:scroll}.swagger-ui .overflow-x-auto-m{overflow-x:auto}.swagger-ui .overflow-y-visible-m{overflow-y:visible}.swagger-ui .overflow-y-hidden-m{overflow-y:hidden}.swagger-ui .overflow-y-scroll-m{overflow-y:scroll}.swagger-ui .overflow-y-auto-m{overflow-y:auto}}@media screen and (min-width:60em){.swagger-ui .overflow-visible-l{overflow:visible}.swagger-ui .overflow-hidden-l{overflow:hidden}.swagger-ui .overflow-scroll-l{overflow:scroll}.swagger-ui .overflow-auto-l{overflow:auto}.swagger-ui .overflow-x-visible-l{overflow-x:visible}.swagger-ui .overflow-x-hidden-l{overflow-x:hidden}.swagger-ui .overflow-x-scroll-l{overflow-x:scroll}.swagger-ui .overflow-x-auto-l{overflow-x:auto}.swagger-ui .overflow-y-visible-l{overflow-y:visible}.swagger-ui .overflow-y-hidden-l{overflow-y:hidden}.swagger-ui .overflow-y-scroll-l{overflow-y:scroll}.swagger-ui .overflow-y-auto-l{overflow-y:auto}}.swagger-ui .static{position:static}.swagger-ui .relative{position:relative}.swagger-ui .absolute{position:absolute}.swagger-ui .fixed{position:fixed}@media screen and (min-width:30em){.swagger-ui .static-ns{position:static}.swagger-ui .relative-ns{position:relative}.swagger-ui .absolute-ns{position:absolute}.swagger-ui .fixed-ns{position:fixed}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .static-m{position:static}.swagger-ui .relative-m{position:relative}.swagger-ui .absolute-m{position:absolute}.swagger-ui .fixed-m{position:fixed}}@media screen and (min-width:60em){.swagger-ui .static-l{position:static}.swagger-ui .relative-l{position:relative}.swagger-ui .absolute-l{position:absolute}.swagger-ui .fixed-l{position:fixed}}.swagger-ui .o-100{opacity:1}.swagger-ui .o-90{opacity:.9}.swagger-ui .o-80{opacity:.8}.swagger-ui .o-70{opacity:.7}.swagger-ui .o-60{opacity:.6}.swagger-ui .o-50{opacity:.5}.swagger-ui .o-40{opacity:.4}.swagger-ui .o-30{opacity:.3}.swagger-ui .o-20{opacity:.2}.swagger-ui .o-10{opacity:.1}.swagger-ui .o-05{opacity:.05}.swagger-ui .o-025{opacity:.025}.swagger-ui .o-0{opacity:0}.swagger-ui .rotate-45{transform:rotate(45deg)}.swagger-ui .rotate-90{transform:rotate(90deg)}.swagger-ui .rotate-135{transform:rotate(135deg)}.swagger-ui .rotate-180{transform:rotate(180deg)}.swagger-ui .rotate-225{transform:rotate(225deg)}.swagger-ui .rotate-270{transform:rotate(270deg)}.swagger-ui .rotate-315{transform:rotate(315deg)}@media screen and (min-width:30em){.swagger-ui .rotate-45-ns{transform:rotate(45deg)}.swagger-ui .rotate-90-ns{transform:rotate(90deg)}.swagger-ui .rotate-135-ns{transform:rotate(135deg)}.swagger-ui .rotate-180-ns{transform:rotate(180deg)}.swagger-ui .rotate-225-ns{transform:rotate(225deg)}.swagger-ui .rotate-270-ns{transform:rotate(270deg)}.swagger-ui .rotate-315-ns{transform:rotate(315deg)}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .rotate-45-m{transform:rotate(45deg)}.swagger-ui .rotate-90-m{transform:rotate(90deg)}.swagger-ui .rotate-135-m{transform:rotate(135deg)}.swagger-ui .rotate-180-m{transform:rotate(180deg)}.swagger-ui .rotate-225-m{transform:rotate(225deg)}.swagger-ui .rotate-270-m{transform:rotate(270deg)}.swagger-ui .rotate-315-m{transform:rotate(315deg)}}@media screen and (min-width:60em){.swagger-ui .rotate-45-l{transform:rotate(45deg)}.swagger-ui .rotate-90-l{transform:rotate(90deg)}.swagger-ui .rotate-135-l{transform:rotate(135deg)}.swagger-ui .rotate-180-l{transform:rotate(180deg)}.swagger-ui .rotate-225-l{transform:rotate(225deg)}.swagger-ui .rotate-270-l{transform:rotate(270deg)}.swagger-ui .rotate-315-l{transform:rotate(315deg)}}.swagger-ui .black-90{color:rgba(0,0,0,.9)}.swagger-ui .black-80{color:rgba(0,0,0,.8)}.swagger-ui .black-70{color:rgba(0,0,0,.7)}.swagger-ui .black-60{color:rgba(0,0,0,.6)}.swagger-ui .black-50{color:rgba(0,0,0,.5)}.swagger-ui .black-40{color:rgba(0,0,0,.4)}.swagger-ui .black-30{color:rgba(0,0,0,.3)}.swagger-ui .black-20{color:rgba(0,0,0,.2)}.swagger-ui .black-10{color:rgba(0,0,0,.1)}.swagger-ui .black-05{color:rgba(0,0,0,.05)}.swagger-ui .white-90{color:hsla(0,0%,100%,.9)}.swagger-ui .white-80{color:hsla(0,0%,100%,.8)}.swagger-ui .white-70{color:hsla(0,0%,100%,.7)}.swagger-ui .white-60{color:hsla(0,0%,100%,.6)}.swagger-ui .white-50{color:hsla(0,0%,100%,.5)}.swagger-ui .white-40{color:hsla(0,0%,100%,.4)}.swagger-ui .white-30{color:hsla(0,0%,100%,.3)}.swagger-ui .white-20{color:hsla(0,0%,100%,.2)}.swagger-ui .white-10{color:hsla(0,0%,100%,.1)}.swagger-ui .black{color:#000}.swagger-ui .near-black{color:#111}.swagger-ui .dark-gray{color:#333}.swagger-ui .mid-gray{color:#555}.swagger-ui .gray{color:#777}.swagger-ui .silver{color:#999}.swagger-ui .light-silver{color:#aaa}.swagger-ui .moon-gray{color:#ccc}.swagger-ui .light-gray{color:#eee}.swagger-ui .near-white{color:#f4f4f4}.swagger-ui .white{color:#fff}.swagger-ui .dark-red{color:#e7040f}.swagger-ui .red{color:#ff4136}.swagger-ui .light-red{color:#ff725c}.swagger-ui .orange{color:#ff6300}.swagger-ui .gold{color:#ffb700}.swagger-ui .yellow{color:gold}.swagger-ui .light-yellow{color:#fbf1a9}.swagger-ui .purple{color:#5e2ca5}.swagger-ui .light-purple{color:#a463f2}.swagger-ui .dark-pink{color:#d5008f}.swagger-ui .hot-pink{color:#ff41b4}.swagger-ui .pink{color:#ff80cc}.swagger-ui .light-pink{color:#ffa3d7}.swagger-ui .dark-green{color:#137752}.swagger-ui .green{color:#19a974}.swagger-ui .light-green{color:#9eebcf}.swagger-ui .navy{color:#001b44}.swagger-ui .dark-blue{color:#00449e}.swagger-ui .blue{color:#357edd}.swagger-ui .light-blue{color:#96ccff}.swagger-ui .lightest-blue{color:#cdecff}.swagger-ui .washed-blue{color:#f6fffe}.swagger-ui .washed-green{color:#e8fdf5}.swagger-ui .washed-yellow{color:#fffceb}.swagger-ui .washed-red{color:#ffdfdf}.swagger-ui .color-inherit{color:inherit}.swagger-ui .bg-black-90{background-color:rgba(0,0,0,.9)}.swagger-ui .bg-black-80{background-color:rgba(0,0,0,.8)}.swagger-ui .bg-black-70{background-color:rgba(0,0,0,.7)}.swagger-ui .bg-black-60{background-color:rgba(0,0,0,.6)}.swagger-ui .bg-black-50{background-color:rgba(0,0,0,.5)}.swagger-ui .bg-black-40{background-color:rgba(0,0,0,.4)}.swagger-ui .bg-black-30{background-color:rgba(0,0,0,.3)}.swagger-ui .bg-black-20{background-color:rgba(0,0,0,.2)}.swagger-ui .bg-black-10{background-color:rgba(0,0,0,.1)}.swagger-ui .bg-black-05{background-color:rgba(0,0,0,.05)}.swagger-ui .bg-white-90{background-color:hsla(0,0%,100%,.9)}.swagger-ui .bg-white-80{background-color:hsla(0,0%,100%,.8)}.swagger-ui .bg-white-70{background-color:hsla(0,0%,100%,.7)}.swagger-ui .bg-white-60{background-color:hsla(0,0%,100%,.6)}.swagger-ui .bg-white-50{background-color:hsla(0,0%,100%,.5)}.swagger-ui .bg-white-40{background-color:hsla(0,0%,100%,.4)}.swagger-ui .bg-white-30{background-color:hsla(0,0%,100%,.3)}.swagger-ui .bg-white-20{background-color:hsla(0,0%,100%,.2)}.swagger-ui .bg-white-10{background-color:hsla(0,0%,100%,.1)}.swagger-ui .bg-black{background-color:#000}.swagger-ui .bg-near-black{background-color:#111}.swagger-ui .bg-dark-gray{background-color:#333}.swagger-ui .bg-mid-gray{background-color:#555}.swagger-ui .bg-gray{background-color:#777}.swagger-ui .bg-silver{background-color:#999}.swagger-ui .bg-light-silver{background-color:#aaa}.swagger-ui .bg-moon-gray{background-color:#ccc}.swagger-ui .bg-light-gray{background-color:#eee}.swagger-ui .bg-near-white{background-color:#f4f4f4}.swagger-ui .bg-white{background-color:#fff}.swagger-ui .bg-transparent{background-color:transparent}.swagger-ui .bg-dark-red{background-color:#e7040f}.swagger-ui .bg-red{background-color:#ff4136}.swagger-ui .bg-light-red{background-color:#ff725c}.swagger-ui .bg-orange{background-color:#ff6300}.swagger-ui .bg-gold{background-color:#ffb700}.swagger-ui .bg-yellow{background-color:gold}.swagger-ui .bg-light-yellow{background-color:#fbf1a9}.swagger-ui .bg-purple{background-color:#5e2ca5}.swagger-ui .bg-light-purple{background-color:#a463f2}.swagger-ui .bg-dark-pink{background-color:#d5008f}.swagger-ui .bg-hot-pink{background-color:#ff41b4}.swagger-ui .bg-pink{background-color:#ff80cc}.swagger-ui .bg-light-pink{background-color:#ffa3d7}.swagger-ui .bg-dark-green{background-color:#137752}.swagger-ui .bg-green{background-color:#19a974}.swagger-ui .bg-light-green{background-color:#9eebcf}.swagger-ui .bg-navy{background-color:#001b44}.swagger-ui .bg-dark-blue{background-color:#00449e}.swagger-ui .bg-blue{background-color:#357edd}.swagger-ui .bg-light-blue{background-color:#96ccff}.swagger-ui .bg-lightest-blue{background-color:#cdecff}.swagger-ui .bg-washed-blue{background-color:#f6fffe}.swagger-ui .bg-washed-green{background-color:#e8fdf5}.swagger-ui .bg-washed-yellow{background-color:#fffceb}.swagger-ui .bg-washed-red{background-color:#ffdfdf}.swagger-ui .bg-inherit{background-color:inherit}.swagger-ui .hover-black:focus,.swagger-ui .hover-black:hover{color:#000}.swagger-ui .hover-near-black:focus,.swagger-ui .hover-near-black:hover{color:#111}.swagger-ui .hover-dark-gray:focus,.swagger-ui .hover-dark-gray:hover{color:#333}.swagger-ui .hover-mid-gray:focus,.swagger-ui .hover-mid-gray:hover{color:#555}.swagger-ui .hover-gray:focus,.swagger-ui .hover-gray:hover{color:#777}.swagger-ui .hover-silver:focus,.swagger-ui .hover-silver:hover{color:#999}.swagger-ui .hover-light-silver:focus,.swagger-ui .hover-light-silver:hover{color:#aaa}.swagger-ui .hover-moon-gray:focus,.swagger-ui .hover-moon-gray:hover{color:#ccc}.swagger-ui .hover-light-gray:focus,.swagger-ui .hover-light-gray:hover{color:#eee}.swagger-ui .hover-near-white:focus,.swagger-ui .hover-near-white:hover{color:#f4f4f4}.swagger-ui .hover-white:focus,.swagger-ui .hover-white:hover{color:#fff}.swagger-ui .hover-black-90:focus,.swagger-ui .hover-black-90:hover{color:rgba(0,0,0,.9)}.swagger-ui .hover-black-80:focus,.swagger-ui .hover-black-80:hover{color:rgba(0,0,0,.8)}.swagger-ui .hover-black-70:focus,.swagger-ui .hover-black-70:hover{color:rgba(0,0,0,.7)}.swagger-ui .hover-black-60:focus,.swagger-ui .hover-black-60:hover{color:rgba(0,0,0,.6)}.swagger-ui .hover-black-50:focus,.swagger-ui .hover-black-50:hover{color:rgba(0,0,0,.5)}.swagger-ui .hover-black-40:focus,.swagger-ui .hover-black-40:hover{color:rgba(0,0,0,.4)}.swagger-ui .hover-black-30:focus,.swagger-ui .hover-black-30:hover{color:rgba(0,0,0,.3)}.swagger-ui .hover-black-20:focus,.swagger-ui .hover-black-20:hover{color:rgba(0,0,0,.2)}.swagger-ui .hover-black-10:focus,.swagger-ui .hover-black-10:hover{color:rgba(0,0,0,.1)}.swagger-ui .hover-white-90:focus,.swagger-ui .hover-white-90:hover{color:hsla(0,0%,100%,.9)}.swagger-ui .hover-white-80:focus,.swagger-ui .hover-white-80:hover{color:hsla(0,0%,100%,.8)}.swagger-ui .hover-white-70:focus,.swagger-ui .hover-white-70:hover{color:hsla(0,0%,100%,.7)}.swagger-ui .hover-white-60:focus,.swagger-ui .hover-white-60:hover{color:hsla(0,0%,100%,.6)}.swagger-ui .hover-white-50:focus,.swagger-ui .hover-white-50:hover{color:hsla(0,0%,100%,.5)}.swagger-ui .hover-white-40:focus,.swagger-ui .hover-white-40:hover{color:hsla(0,0%,100%,.4)}.swagger-ui .hover-white-30:focus,.swagger-ui .hover-white-30:hover{color:hsla(0,0%,100%,.3)}.swagger-ui .hover-white-20:focus,.swagger-ui .hover-white-20:hover{color:hsla(0,0%,100%,.2)}.swagger-ui .hover-white-10:focus,.swagger-ui .hover-white-10:hover{color:hsla(0,0%,100%,.1)}.swagger-ui .hover-inherit:focus,.swagger-ui .hover-inherit:hover{color:inherit}.swagger-ui .hover-bg-black:focus,.swagger-ui .hover-bg-black:hover{background-color:#000}.swagger-ui .hover-bg-near-black:focus,.swagger-ui .hover-bg-near-black:hover{background-color:#111}.swagger-ui .hover-bg-dark-gray:focus,.swagger-ui .hover-bg-dark-gray:hover{background-color:#333}.swagger-ui .hover-bg-mid-gray:focus,.swagger-ui .hover-bg-mid-gray:hover{background-color:#555}.swagger-ui .hover-bg-gray:focus,.swagger-ui .hover-bg-gray:hover{background-color:#777}.swagger-ui .hover-bg-silver:focus,.swagger-ui .hover-bg-silver:hover{background-color:#999}.swagger-ui .hover-bg-light-silver:focus,.swagger-ui .hover-bg-light-silver:hover{background-color:#aaa}.swagger-ui .hover-bg-moon-gray:focus,.swagger-ui .hover-bg-moon-gray:hover{background-color:#ccc}.swagger-ui .hover-bg-light-gray:focus,.swagger-ui .hover-bg-light-gray:hover{background-color:#eee}.swagger-ui .hover-bg-near-white:focus,.swagger-ui .hover-bg-near-white:hover{background-color:#f4f4f4}.swagger-ui .hover-bg-white:focus,.swagger-ui .hover-bg-white:hover{background-color:#fff}.swagger-ui .hover-bg-transparent:focus,.swagger-ui .hover-bg-transparent:hover{background-color:transparent}.swagger-ui .hover-bg-black-90:focus,.swagger-ui .hover-bg-black-90:hover{background-color:rgba(0,0,0,.9)}.swagger-ui .hover-bg-black-80:focus,.swagger-ui .hover-bg-black-80:hover{background-color:rgba(0,0,0,.8)}.swagger-ui .hover-bg-black-70:focus,.swagger-ui .hover-bg-black-70:hover{background-color:rgba(0,0,0,.7)}.swagger-ui .hover-bg-black-60:focus,.swagger-ui .hover-bg-black-60:hover{background-color:rgba(0,0,0,.6)}.swagger-ui .hover-bg-black-50:focus,.swagger-ui .hover-bg-black-50:hover{background-color:rgba(0,0,0,.5)}.swagger-ui .hover-bg-black-40:focus,.swagger-ui .hover-bg-black-40:hover{background-color:rgba(0,0,0,.4)}.swagger-ui .hover-bg-black-30:focus,.swagger-ui .hover-bg-black-30:hover{background-color:rgba(0,0,0,.3)}.swagger-ui .hover-bg-black-20:focus,.swagger-ui .hover-bg-black-20:hover{background-color:rgba(0,0,0,.2)}.swagger-ui .hover-bg-black-10:focus,.swagger-ui .hover-bg-black-10:hover{background-color:rgba(0,0,0,.1)}.swagger-ui .hover-bg-white-90:focus,.swagger-ui .hover-bg-white-90:hover{background-color:hsla(0,0%,100%,.9)}.swagger-ui .hover-bg-white-80:focus,.swagger-ui .hover-bg-white-80:hover{background-color:hsla(0,0%,100%,.8)}.swagger-ui .hover-bg-white-70:focus,.swagger-ui .hover-bg-white-70:hover{background-color:hsla(0,0%,100%,.7)}.swagger-ui .hover-bg-white-60:focus,.swagger-ui .hover-bg-white-60:hover{background-color:hsla(0,0%,100%,.6)}.swagger-ui .hover-bg-white-50:focus,.swagger-ui .hover-bg-white-50:hover{background-color:hsla(0,0%,100%,.5)}.swagger-ui .hover-bg-white-40:focus,.swagger-ui .hover-bg-white-40:hover{background-color:hsla(0,0%,100%,.4)}.swagger-ui .hover-bg-white-30:focus,.swagger-ui .hover-bg-white-30:hover{background-color:hsla(0,0%,100%,.3)}.swagger-ui .hover-bg-white-20:focus,.swagger-ui .hover-bg-white-20:hover{background-color:hsla(0,0%,100%,.2)}.swagger-ui .hover-bg-white-10:focus,.swagger-ui .hover-bg-white-10:hover{background-color:hsla(0,0%,100%,.1)}.swagger-ui .hover-dark-red:focus,.swagger-ui .hover-dark-red:hover{color:#e7040f}.swagger-ui .hover-red:focus,.swagger-ui .hover-red:hover{color:#ff4136}.swagger-ui .hover-light-red:focus,.swagger-ui .hover-light-red:hover{color:#ff725c}.swagger-ui .hover-orange:focus,.swagger-ui .hover-orange:hover{color:#ff6300}.swagger-ui .hover-gold:focus,.swagger-ui .hover-gold:hover{color:#ffb700}.swagger-ui .hover-yellow:focus,.swagger-ui .hover-yellow:hover{color:gold}.swagger-ui .hover-light-yellow:focus,.swagger-ui .hover-light-yellow:hover{color:#fbf1a9}.swagger-ui .hover-purple:focus,.swagger-ui .hover-purple:hover{color:#5e2ca5}.swagger-ui .hover-light-purple:focus,.swagger-ui .hover-light-purple:hover{color:#a463f2}.swagger-ui .hover-dark-pink:focus,.swagger-ui .hover-dark-pink:hover{color:#d5008f}.swagger-ui .hover-hot-pink:focus,.swagger-ui .hover-hot-pink:hover{color:#ff41b4}.swagger-ui .hover-pink:focus,.swagger-ui .hover-pink:hover{color:#ff80cc}.swagger-ui .hover-light-pink:focus,.swagger-ui .hover-light-pink:hover{color:#ffa3d7}.swagger-ui .hover-dark-green:focus,.swagger-ui .hover-dark-green:hover{color:#137752}.swagger-ui .hover-green:focus,.swagger-ui .hover-green:hover{color:#19a974}.swagger-ui .hover-light-green:focus,.swagger-ui .hover-light-green:hover{color:#9eebcf}.swagger-ui .hover-navy:focus,.swagger-ui .hover-navy:hover{color:#001b44}.swagger-ui .hover-dark-blue:focus,.swagger-ui .hover-dark-blue:hover{color:#00449e}.swagger-ui .hover-blue:focus,.swagger-ui .hover-blue:hover{color:#357edd}.swagger-ui .hover-light-blue:focus,.swagger-ui .hover-light-blue:hover{color:#96ccff}.swagger-ui .hover-lightest-blue:focus,.swagger-ui .hover-lightest-blue:hover{color:#cdecff}.swagger-ui .hover-washed-blue:focus,.swagger-ui .hover-washed-blue:hover{color:#f6fffe}.swagger-ui .hover-washed-green:focus,.swagger-ui .hover-washed-green:hover{color:#e8fdf5}.swagger-ui .hover-washed-yellow:focus,.swagger-ui .hover-washed-yellow:hover{color:#fffceb}.swagger-ui .hover-washed-red:focus,.swagger-ui .hover-washed-red:hover{color:#ffdfdf}.swagger-ui .hover-bg-dark-red:focus,.swagger-ui .hover-bg-dark-red:hover{background-color:#e7040f}.swagger-ui .hover-bg-red:focus,.swagger-ui .hover-bg-red:hover{background-color:#ff4136}.swagger-ui .hover-bg-light-red:focus,.swagger-ui .hover-bg-light-red:hover{background-color:#ff725c}.swagger-ui .hover-bg-orange:focus,.swagger-ui .hover-bg-orange:hover{background-color:#ff6300}.swagger-ui .hover-bg-gold:focus,.swagger-ui .hover-bg-gold:hover{background-color:#ffb700}.swagger-ui .hover-bg-yellow:focus,.swagger-ui .hover-bg-yellow:hover{background-color:gold}.swagger-ui .hover-bg-light-yellow:focus,.swagger-ui .hover-bg-light-yellow:hover{background-color:#fbf1a9}.swagger-ui .hover-bg-purple:focus,.swagger-ui .hover-bg-purple:hover{background-color:#5e2ca5}.swagger-ui .hover-bg-light-purple:focus,.swagger-ui .hover-bg-light-purple:hover{background-color:#a463f2}.swagger-ui .hover-bg-dark-pink:focus,.swagger-ui .hover-bg-dark-pink:hover{background-color:#d5008f}.swagger-ui .hover-bg-hot-pink:focus,.swagger-ui .hover-bg-hot-pink:hover{background-color:#ff41b4}.swagger-ui .hover-bg-pink:focus,.swagger-ui .hover-bg-pink:hover{background-color:#ff80cc}.swagger-ui .hover-bg-light-pink:focus,.swagger-ui .hover-bg-light-pink:hover{background-color:#ffa3d7}.swagger-ui .hover-bg-dark-green:focus,.swagger-ui .hover-bg-dark-green:hover{background-color:#137752}.swagger-ui .hover-bg-green:focus,.swagger-ui .hover-bg-green:hover{background-color:#19a974}.swagger-ui .hover-bg-light-green:focus,.swagger-ui .hover-bg-light-green:hover{background-color:#9eebcf}.swagger-ui .hover-bg-navy:focus,.swagger-ui .hover-bg-navy:hover{background-color:#001b44}.swagger-ui .hover-bg-dark-blue:focus,.swagger-ui .hover-bg-dark-blue:hover{background-color:#00449e}.swagger-ui .hover-bg-blue:focus,.swagger-ui .hover-bg-blue:hover{background-color:#357edd}.swagger-ui .hover-bg-light-blue:focus,.swagger-ui .hover-bg-light-blue:hover{background-color:#96ccff}.swagger-ui .hover-bg-lightest-blue:focus,.swagger-ui .hover-bg-lightest-blue:hover{background-color:#cdecff}.swagger-ui .hover-bg-washed-blue:focus,.swagger-ui .hover-bg-washed-blue:hover{background-color:#f6fffe}.swagger-ui .hover-bg-washed-green:focus,.swagger-ui .hover-bg-washed-green:hover{background-color:#e8fdf5}.swagger-ui .hover-bg-washed-yellow:focus,.swagger-ui .hover-bg-washed-yellow:hover{background-color:#fffceb}.swagger-ui .hover-bg-washed-red:focus,.swagger-ui .hover-bg-washed-red:hover{background-color:#ffdfdf}.swagger-ui .hover-bg-inherit:focus,.swagger-ui .hover-bg-inherit:hover{background-color:inherit}.swagger-ui .pa0{padding:0}.swagger-ui .pa1{padding:.25rem}.swagger-ui .pa2{padding:.5rem}.swagger-ui .pa3{padding:1rem}.swagger-ui .pa4{padding:2rem}.swagger-ui .pa5{padding:4rem}.swagger-ui .pa6{padding:8rem}.swagger-ui .pa7{padding:16rem}.swagger-ui .pl0{padding-left:0}.swagger-ui .pl1{padding-left:.25rem}.swagger-ui .pl2{padding-left:.5rem}.swagger-ui .pl3{padding-left:1rem}.swagger-ui .pl4{padding-left:2rem}.swagger-ui .pl5{padding-left:4rem}.swagger-ui .pl6{padding-left:8rem}.swagger-ui .pl7{padding-left:16rem}.swagger-ui .pr0{padding-right:0}.swagger-ui .pr1{padding-right:.25rem}.swagger-ui .pr2{padding-right:.5rem}.swagger-ui .pr3{padding-right:1rem}.swagger-ui .pr4{padding-right:2rem}.swagger-ui .pr5{padding-right:4rem}.swagger-ui .pr6{padding-right:8rem}.swagger-ui .pr7{padding-right:16rem}.swagger-ui .pb0{padding-bottom:0}.swagger-ui .pb1{padding-bottom:.25rem}.swagger-ui .pb2{padding-bottom:.5rem}.swagger-ui .pb3{padding-bottom:1rem}.swagger-ui .pb4{padding-bottom:2rem}.swagger-ui .pb5{padding-bottom:4rem}.swagger-ui .pb6{padding-bottom:8rem}.swagger-ui .pb7{padding-bottom:16rem}.swagger-ui .pt0{padding-top:0}.swagger-ui .pt1{padding-top:.25rem}.swagger-ui .pt2{padding-top:.5rem}.swagger-ui .pt3{padding-top:1rem}.swagger-ui .pt4{padding-top:2rem}.swagger-ui .pt5{padding-top:4rem}.swagger-ui .pt6{padding-top:8rem}.swagger-ui .pt7{padding-top:16rem}.swagger-ui .pv0{padding-bottom:0;padding-top:0}.swagger-ui .pv1{padding-bottom:.25rem;padding-top:.25rem}.swagger-ui .pv2{padding-bottom:.5rem;padding-top:.5rem}.swagger-ui .pv3{padding-bottom:1rem;padding-top:1rem}.swagger-ui .pv4{padding-bottom:2rem;padding-top:2rem}.swagger-ui .pv5{padding-bottom:4rem;padding-top:4rem}.swagger-ui .pv6{padding-bottom:8rem;padding-top:8rem}.swagger-ui .pv7{padding-bottom:16rem;padding-top:16rem}.swagger-ui .ph0{padding-left:0;padding-right:0}.swagger-ui .ph1{padding-left:.25rem;padding-right:.25rem}.swagger-ui .ph2{padding-left:.5rem;padding-right:.5rem}.swagger-ui .ph3{padding-left:1rem;padding-right:1rem}.swagger-ui .ph4{padding-left:2rem;padding-right:2rem}.swagger-ui .ph5{padding-left:4rem;padding-right:4rem}.swagger-ui .ph6{padding-left:8rem;padding-right:8rem}.swagger-ui .ph7{padding-left:16rem;padding-right:16rem}.swagger-ui .ma0{margin:0}.swagger-ui .ma1{margin:.25rem}.swagger-ui .ma2{margin:.5rem}.swagger-ui .ma3{margin:1rem}.swagger-ui .ma4{margin:2rem}.swagger-ui .ma5{margin:4rem}.swagger-ui .ma6{margin:8rem}.swagger-ui .ma7{margin:16rem}.swagger-ui .ml0{margin-left:0}.swagger-ui .ml1{margin-left:.25rem}.swagger-ui .ml2{margin-left:.5rem}.swagger-ui .ml3{margin-left:1rem}.swagger-ui .ml4{margin-left:2rem}.swagger-ui .ml5{margin-left:4rem}.swagger-ui .ml6{margin-left:8rem}.swagger-ui .ml7{margin-left:16rem}.swagger-ui .mr0{margin-right:0}.swagger-ui .mr1{margin-right:.25rem}.swagger-ui .mr2{margin-right:.5rem}.swagger-ui .mr3{margin-right:1rem}.swagger-ui .mr4{margin-right:2rem}.swagger-ui .mr5{margin-right:4rem}.swagger-ui .mr6{margin-right:8rem}.swagger-ui .mr7{margin-right:16rem}.swagger-ui .mb0{margin-bottom:0}.swagger-ui .mb1{margin-bottom:.25rem}.swagger-ui .mb2{margin-bottom:.5rem}.swagger-ui .mb3{margin-bottom:1rem}.swagger-ui .mb4{margin-bottom:2rem}.swagger-ui .mb5{margin-bottom:4rem}.swagger-ui .mb6{margin-bottom:8rem}.swagger-ui .mb7{margin-bottom:16rem}.swagger-ui .mt0{margin-top:0}.swagger-ui .mt1{margin-top:.25rem}.swagger-ui .mt2{margin-top:.5rem}.swagger-ui .mt3{margin-top:1rem}.swagger-ui .mt4{margin-top:2rem}.swagger-ui .mt5{margin-top:4rem}.swagger-ui .mt6{margin-top:8rem}.swagger-ui .mt7{margin-top:16rem}.swagger-ui .mv0{margin-bottom:0;margin-top:0}.swagger-ui .mv1{margin-bottom:.25rem;margin-top:.25rem}.swagger-ui .mv2{margin-bottom:.5rem;margin-top:.5rem}.swagger-ui .mv3{margin-bottom:1rem;margin-top:1rem}.swagger-ui .mv4{margin-bottom:2rem;margin-top:2rem}.swagger-ui .mv5{margin-bottom:4rem;margin-top:4rem}.swagger-ui .mv6{margin-bottom:8rem;margin-top:8rem}.swagger-ui .mv7{margin-bottom:16rem;margin-top:16rem}.swagger-ui .mh0{margin-left:0;margin-right:0}.swagger-ui .mh1{margin-left:.25rem;margin-right:.25rem}.swagger-ui .mh2{margin-left:.5rem;margin-right:.5rem}.swagger-ui .mh3{margin-left:1rem;margin-right:1rem}.swagger-ui .mh4{margin-left:2rem;margin-right:2rem}.swagger-ui .mh5{margin-left:4rem;margin-right:4rem}.swagger-ui .mh6{margin-left:8rem;margin-right:8rem}.swagger-ui .mh7{margin-left:16rem;margin-right:16rem}@media screen and (min-width:30em){.swagger-ui .pa0-ns{padding:0}.swagger-ui .pa1-ns{padding:.25rem}.swagger-ui .pa2-ns{padding:.5rem}.swagger-ui .pa3-ns{padding:1rem}.swagger-ui .pa4-ns{padding:2rem}.swagger-ui .pa5-ns{padding:4rem}.swagger-ui .pa6-ns{padding:8rem}.swagger-ui .pa7-ns{padding:16rem}.swagger-ui .pl0-ns{padding-left:0}.swagger-ui .pl1-ns{padding-left:.25rem}.swagger-ui .pl2-ns{padding-left:.5rem}.swagger-ui .pl3-ns{padding-left:1rem}.swagger-ui .pl4-ns{padding-left:2rem}.swagger-ui .pl5-ns{padding-left:4rem}.swagger-ui .pl6-ns{padding-left:8rem}.swagger-ui .pl7-ns{padding-left:16rem}.swagger-ui .pr0-ns{padding-right:0}.swagger-ui .pr1-ns{padding-right:.25rem}.swagger-ui .pr2-ns{padding-right:.5rem}.swagger-ui .pr3-ns{padding-right:1rem}.swagger-ui .pr4-ns{padding-right:2rem}.swagger-ui .pr5-ns{padding-right:4rem}.swagger-ui .pr6-ns{padding-right:8rem}.swagger-ui .pr7-ns{padding-right:16rem}.swagger-ui .pb0-ns{padding-bottom:0}.swagger-ui .pb1-ns{padding-bottom:.25rem}.swagger-ui .pb2-ns{padding-bottom:.5rem}.swagger-ui .pb3-ns{padding-bottom:1rem}.swagger-ui .pb4-ns{padding-bottom:2rem}.swagger-ui .pb5-ns{padding-bottom:4rem}.swagger-ui .pb6-ns{padding-bottom:8rem}.swagger-ui .pb7-ns{padding-bottom:16rem}.swagger-ui .pt0-ns{padding-top:0}.swagger-ui .pt1-ns{padding-top:.25rem}.swagger-ui .pt2-ns{padding-top:.5rem}.swagger-ui .pt3-ns{padding-top:1rem}.swagger-ui .pt4-ns{padding-top:2rem}.swagger-ui .pt5-ns{padding-top:4rem}.swagger-ui .pt6-ns{padding-top:8rem}.swagger-ui .pt7-ns{padding-top:16rem}.swagger-ui .pv0-ns{padding-bottom:0;padding-top:0}.swagger-ui .pv1-ns{padding-bottom:.25rem;padding-top:.25rem}.swagger-ui .pv2-ns{padding-bottom:.5rem;padding-top:.5rem}.swagger-ui .pv3-ns{padding-bottom:1rem;padding-top:1rem}.swagger-ui .pv4-ns{padding-bottom:2rem;padding-top:2rem}.swagger-ui .pv5-ns{padding-bottom:4rem;padding-top:4rem}.swagger-ui .pv6-ns{padding-bottom:8rem;padding-top:8rem}.swagger-ui .pv7-ns{padding-bottom:16rem;padding-top:16rem}.swagger-ui .ph0-ns{padding-left:0;padding-right:0}.swagger-ui .ph1-ns{padding-left:.25rem;padding-right:.25rem}.swagger-ui .ph2-ns{padding-left:.5rem;padding-right:.5rem}.swagger-ui .ph3-ns{padding-left:1rem;padding-right:1rem}.swagger-ui .ph4-ns{padding-left:2rem;padding-right:2rem}.swagger-ui .ph5-ns{padding-left:4rem;padding-right:4rem}.swagger-ui .ph6-ns{padding-left:8rem;padding-right:8rem}.swagger-ui .ph7-ns{padding-left:16rem;padding-right:16rem}.swagger-ui .ma0-ns{margin:0}.swagger-ui .ma1-ns{margin:.25rem}.swagger-ui .ma2-ns{margin:.5rem}.swagger-ui .ma3-ns{margin:1rem}.swagger-ui .ma4-ns{margin:2rem}.swagger-ui .ma5-ns{margin:4rem}.swagger-ui .ma6-ns{margin:8rem}.swagger-ui .ma7-ns{margin:16rem}.swagger-ui .ml0-ns{margin-left:0}.swagger-ui .ml1-ns{margin-left:.25rem}.swagger-ui .ml2-ns{margin-left:.5rem}.swagger-ui .ml3-ns{margin-left:1rem}.swagger-ui .ml4-ns{margin-left:2rem}.swagger-ui .ml5-ns{margin-left:4rem}.swagger-ui .ml6-ns{margin-left:8rem}.swagger-ui .ml7-ns{margin-left:16rem}.swagger-ui .mr0-ns{margin-right:0}.swagger-ui .mr1-ns{margin-right:.25rem}.swagger-ui .mr2-ns{margin-right:.5rem}.swagger-ui .mr3-ns{margin-right:1rem}.swagger-ui .mr4-ns{margin-right:2rem}.swagger-ui .mr5-ns{margin-right:4rem}.swagger-ui .mr6-ns{margin-right:8rem}.swagger-ui .mr7-ns{margin-right:16rem}.swagger-ui .mb0-ns{margin-bottom:0}.swagger-ui .mb1-ns{margin-bottom:.25rem}.swagger-ui .mb2-ns{margin-bottom:.5rem}.swagger-ui .mb3-ns{margin-bottom:1rem}.swagger-ui .mb4-ns{margin-bottom:2rem}.swagger-ui .mb5-ns{margin-bottom:4rem}.swagger-ui .mb6-ns{margin-bottom:8rem}.swagger-ui .mb7-ns{margin-bottom:16rem}.swagger-ui .mt0-ns{margin-top:0}.swagger-ui .mt1-ns{margin-top:.25rem}.swagger-ui .mt2-ns{margin-top:.5rem}.swagger-ui .mt3-ns{margin-top:1rem}.swagger-ui .mt4-ns{margin-top:2rem}.swagger-ui .mt5-ns{margin-top:4rem}.swagger-ui .mt6-ns{margin-top:8rem}.swagger-ui .mt7-ns{margin-top:16rem}.swagger-ui .mv0-ns{margin-bottom:0;margin-top:0}.swagger-ui .mv1-ns{margin-bottom:.25rem;margin-top:.25rem}.swagger-ui .mv2-ns{margin-bottom:.5rem;margin-top:.5rem}.swagger-ui .mv3-ns{margin-bottom:1rem;margin-top:1rem}.swagger-ui .mv4-ns{margin-bottom:2rem;margin-top:2rem}.swagger-ui .mv5-ns{margin-bottom:4rem;margin-top:4rem}.swagger-ui .mv6-ns{margin-bottom:8rem;margin-top:8rem}.swagger-ui .mv7-ns{margin-bottom:16rem;margin-top:16rem}.swagger-ui .mh0-ns{margin-left:0;margin-right:0}.swagger-ui .mh1-ns{margin-left:.25rem;margin-right:.25rem}.swagger-ui .mh2-ns{margin-left:.5rem;margin-right:.5rem}.swagger-ui .mh3-ns{margin-left:1rem;margin-right:1rem}.swagger-ui .mh4-ns{margin-left:2rem;margin-right:2rem}.swagger-ui .mh5-ns{margin-left:4rem;margin-right:4rem}.swagger-ui .mh6-ns{margin-left:8rem;margin-right:8rem}.swagger-ui .mh7-ns{margin-left:16rem;margin-right:16rem}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .pa0-m{padding:0}.swagger-ui .pa1-m{padding:.25rem}.swagger-ui .pa2-m{padding:.5rem}.swagger-ui .pa3-m{padding:1rem}.swagger-ui .pa4-m{padding:2rem}.swagger-ui .pa5-m{padding:4rem}.swagger-ui .pa6-m{padding:8rem}.swagger-ui .pa7-m{padding:16rem}.swagger-ui .pl0-m{padding-left:0}.swagger-ui .pl1-m{padding-left:.25rem}.swagger-ui .pl2-m{padding-left:.5rem}.swagger-ui .pl3-m{padding-left:1rem}.swagger-ui .pl4-m{padding-left:2rem}.swagger-ui .pl5-m{padding-left:4rem}.swagger-ui .pl6-m{padding-left:8rem}.swagger-ui .pl7-m{padding-left:16rem}.swagger-ui .pr0-m{padding-right:0}.swagger-ui .pr1-m{padding-right:.25rem}.swagger-ui .pr2-m{padding-right:.5rem}.swagger-ui .pr3-m{padding-right:1rem}.swagger-ui .pr4-m{padding-right:2rem}.swagger-ui .pr5-m{padding-right:4rem}.swagger-ui .pr6-m{padding-right:8rem}.swagger-ui .pr7-m{padding-right:16rem}.swagger-ui .pb0-m{padding-bottom:0}.swagger-ui .pb1-m{padding-bottom:.25rem}.swagger-ui .pb2-m{padding-bottom:.5rem}.swagger-ui .pb3-m{padding-bottom:1rem}.swagger-ui .pb4-m{padding-bottom:2rem}.swagger-ui .pb5-m{padding-bottom:4rem}.swagger-ui .pb6-m{padding-bottom:8rem}.swagger-ui .pb7-m{padding-bottom:16rem}.swagger-ui .pt0-m{padding-top:0}.swagger-ui .pt1-m{padding-top:.25rem}.swagger-ui .pt2-m{padding-top:.5rem}.swagger-ui .pt3-m{padding-top:1rem}.swagger-ui .pt4-m{padding-top:2rem}.swagger-ui .pt5-m{padding-top:4rem}.swagger-ui .pt6-m{padding-top:8rem}.swagger-ui .pt7-m{padding-top:16rem}.swagger-ui .pv0-m{padding-bottom:0;padding-top:0}.swagger-ui .pv1-m{padding-bottom:.25rem;padding-top:.25rem}.swagger-ui .pv2-m{padding-bottom:.5rem;padding-top:.5rem}.swagger-ui .pv3-m{padding-bottom:1rem;padding-top:1rem}.swagger-ui .pv4-m{padding-bottom:2rem;padding-top:2rem}.swagger-ui .pv5-m{padding-bottom:4rem;padding-top:4rem}.swagger-ui .pv6-m{padding-bottom:8rem;padding-top:8rem}.swagger-ui .pv7-m{padding-bottom:16rem;padding-top:16rem}.swagger-ui .ph0-m{padding-left:0;padding-right:0}.swagger-ui .ph1-m{padding-left:.25rem;padding-right:.25rem}.swagger-ui .ph2-m{padding-left:.5rem;padding-right:.5rem}.swagger-ui .ph3-m{padding-left:1rem;padding-right:1rem}.swagger-ui .ph4-m{padding-left:2rem;padding-right:2rem}.swagger-ui .ph5-m{padding-left:4rem;padding-right:4rem}.swagger-ui .ph6-m{padding-left:8rem;padding-right:8rem}.swagger-ui .ph7-m{padding-left:16rem;padding-right:16rem}.swagger-ui .ma0-m{margin:0}.swagger-ui .ma1-m{margin:.25rem}.swagger-ui .ma2-m{margin:.5rem}.swagger-ui .ma3-m{margin:1rem}.swagger-ui .ma4-m{margin:2rem}.swagger-ui .ma5-m{margin:4rem}.swagger-ui .ma6-m{margin:8rem}.swagger-ui .ma7-m{margin:16rem}.swagger-ui .ml0-m{margin-left:0}.swagger-ui .ml1-m{margin-left:.25rem}.swagger-ui .ml2-m{margin-left:.5rem}.swagger-ui .ml3-m{margin-left:1rem}.swagger-ui .ml4-m{margin-left:2rem}.swagger-ui .ml5-m{margin-left:4rem}.swagger-ui .ml6-m{margin-left:8rem}.swagger-ui .ml7-m{margin-left:16rem}.swagger-ui .mr0-m{margin-right:0}.swagger-ui .mr1-m{margin-right:.25rem}.swagger-ui .mr2-m{margin-right:.5rem}.swagger-ui .mr3-m{margin-right:1rem}.swagger-ui .mr4-m{margin-right:2rem}.swagger-ui .mr5-m{margin-right:4rem}.swagger-ui .mr6-m{margin-right:8rem}.swagger-ui .mr7-m{margin-right:16rem}.swagger-ui .mb0-m{margin-bottom:0}.swagger-ui .mb1-m{margin-bottom:.25rem}.swagger-ui .mb2-m{margin-bottom:.5rem}.swagger-ui .mb3-m{margin-bottom:1rem}.swagger-ui .mb4-m{margin-bottom:2rem}.swagger-ui .mb5-m{margin-bottom:4rem}.swagger-ui .mb6-m{margin-bottom:8rem}.swagger-ui .mb7-m{margin-bottom:16rem}.swagger-ui .mt0-m{margin-top:0}.swagger-ui .mt1-m{margin-top:.25rem}.swagger-ui .mt2-m{margin-top:.5rem}.swagger-ui .mt3-m{margin-top:1rem}.swagger-ui .mt4-m{margin-top:2rem}.swagger-ui .mt5-m{margin-top:4rem}.swagger-ui .mt6-m{margin-top:8rem}.swagger-ui .mt7-m{margin-top:16rem}.swagger-ui .mv0-m{margin-bottom:0;margin-top:0}.swagger-ui .mv1-m{margin-bottom:.25rem;margin-top:.25rem}.swagger-ui .mv2-m{margin-bottom:.5rem;margin-top:.5rem}.swagger-ui .mv3-m{margin-bottom:1rem;margin-top:1rem}.swagger-ui .mv4-m{margin-bottom:2rem;margin-top:2rem}.swagger-ui .mv5-m{margin-bottom:4rem;margin-top:4rem}.swagger-ui .mv6-m{margin-bottom:8rem;margin-top:8rem}.swagger-ui .mv7-m{margin-bottom:16rem;margin-top:16rem}.swagger-ui .mh0-m{margin-left:0;margin-right:0}.swagger-ui .mh1-m{margin-left:.25rem;margin-right:.25rem}.swagger-ui .mh2-m{margin-left:.5rem;margin-right:.5rem}.swagger-ui .mh3-m{margin-left:1rem;margin-right:1rem}.swagger-ui .mh4-m{margin-left:2rem;margin-right:2rem}.swagger-ui .mh5-m{margin-left:4rem;margin-right:4rem}.swagger-ui .mh6-m{margin-left:8rem;margin-right:8rem}.swagger-ui .mh7-m{margin-left:16rem;margin-right:16rem}}@media screen and (min-width:60em){.swagger-ui .pa0-l{padding:0}.swagger-ui .pa1-l{padding:.25rem}.swagger-ui .pa2-l{padding:.5rem}.swagger-ui .pa3-l{padding:1rem}.swagger-ui .pa4-l{padding:2rem}.swagger-ui .pa5-l{padding:4rem}.swagger-ui .pa6-l{padding:8rem}.swagger-ui .pa7-l{padding:16rem}.swagger-ui .pl0-l{padding-left:0}.swagger-ui .pl1-l{padding-left:.25rem}.swagger-ui .pl2-l{padding-left:.5rem}.swagger-ui .pl3-l{padding-left:1rem}.swagger-ui .pl4-l{padding-left:2rem}.swagger-ui .pl5-l{padding-left:4rem}.swagger-ui .pl6-l{padding-left:8rem}.swagger-ui .pl7-l{padding-left:16rem}.swagger-ui .pr0-l{padding-right:0}.swagger-ui .pr1-l{padding-right:.25rem}.swagger-ui .pr2-l{padding-right:.5rem}.swagger-ui .pr3-l{padding-right:1rem}.swagger-ui .pr4-l{padding-right:2rem}.swagger-ui .pr5-l{padding-right:4rem}.swagger-ui .pr6-l{padding-right:8rem}.swagger-ui .pr7-l{padding-right:16rem}.swagger-ui .pb0-l{padding-bottom:0}.swagger-ui .pb1-l{padding-bottom:.25rem}.swagger-ui .pb2-l{padding-bottom:.5rem}.swagger-ui .pb3-l{padding-bottom:1rem}.swagger-ui .pb4-l{padding-bottom:2rem}.swagger-ui .pb5-l{padding-bottom:4rem}.swagger-ui .pb6-l{padding-bottom:8rem}.swagger-ui .pb7-l{padding-bottom:16rem}.swagger-ui .pt0-l{padding-top:0}.swagger-ui .pt1-l{padding-top:.25rem}.swagger-ui .pt2-l{padding-top:.5rem}.swagger-ui .pt3-l{padding-top:1rem}.swagger-ui .pt4-l{padding-top:2rem}.swagger-ui .pt5-l{padding-top:4rem}.swagger-ui .pt6-l{padding-top:8rem}.swagger-ui .pt7-l{padding-top:16rem}.swagger-ui .pv0-l{padding-bottom:0;padding-top:0}.swagger-ui .pv1-l{padding-bottom:.25rem;padding-top:.25rem}.swagger-ui .pv2-l{padding-bottom:.5rem;padding-top:.5rem}.swagger-ui .pv3-l{padding-bottom:1rem;padding-top:1rem}.swagger-ui .pv4-l{padding-bottom:2rem;padding-top:2rem}.swagger-ui .pv5-l{padding-bottom:4rem;padding-top:4rem}.swagger-ui .pv6-l{padding-bottom:8rem;padding-top:8rem}.swagger-ui .pv7-l{padding-bottom:16rem;padding-top:16rem}.swagger-ui .ph0-l{padding-left:0;padding-right:0}.swagger-ui .ph1-l{padding-left:.25rem;padding-right:.25rem}.swagger-ui .ph2-l{padding-left:.5rem;padding-right:.5rem}.swagger-ui .ph3-l{padding-left:1rem;padding-right:1rem}.swagger-ui .ph4-l{padding-left:2rem;padding-right:2rem}.swagger-ui .ph5-l{padding-left:4rem;padding-right:4rem}.swagger-ui .ph6-l{padding-left:8rem;padding-right:8rem}.swagger-ui .ph7-l{padding-left:16rem;padding-right:16rem}.swagger-ui .ma0-l{margin:0}.swagger-ui .ma1-l{margin:.25rem}.swagger-ui .ma2-l{margin:.5rem}.swagger-ui .ma3-l{margin:1rem}.swagger-ui .ma4-l{margin:2rem}.swagger-ui .ma5-l{margin:4rem}.swagger-ui .ma6-l{margin:8rem}.swagger-ui .ma7-l{margin:16rem}.swagger-ui .ml0-l{margin-left:0}.swagger-ui .ml1-l{margin-left:.25rem}.swagger-ui .ml2-l{margin-left:.5rem}.swagger-ui .ml3-l{margin-left:1rem}.swagger-ui .ml4-l{margin-left:2rem}.swagger-ui .ml5-l{margin-left:4rem}.swagger-ui .ml6-l{margin-left:8rem}.swagger-ui .ml7-l{margin-left:16rem}.swagger-ui .mr0-l{margin-right:0}.swagger-ui .mr1-l{margin-right:.25rem}.swagger-ui .mr2-l{margin-right:.5rem}.swagger-ui .mr3-l{margin-right:1rem}.swagger-ui .mr4-l{margin-right:2rem}.swagger-ui .mr5-l{margin-right:4rem}.swagger-ui .mr6-l{margin-right:8rem}.swagger-ui .mr7-l{margin-right:16rem}.swagger-ui .mb0-l{margin-bottom:0}.swagger-ui .mb1-l{margin-bottom:.25rem}.swagger-ui .mb2-l{margin-bottom:.5rem}.swagger-ui .mb3-l{margin-bottom:1rem}.swagger-ui .mb4-l{margin-bottom:2rem}.swagger-ui .mb5-l{margin-bottom:4rem}.swagger-ui .mb6-l{margin-bottom:8rem}.swagger-ui .mb7-l{margin-bottom:16rem}.swagger-ui .mt0-l{margin-top:0}.swagger-ui .mt1-l{margin-top:.25rem}.swagger-ui .mt2-l{margin-top:.5rem}.swagger-ui .mt3-l{margin-top:1rem}.swagger-ui .mt4-l{margin-top:2rem}.swagger-ui .mt5-l{margin-top:4rem}.swagger-ui .mt6-l{margin-top:8rem}.swagger-ui .mt7-l{margin-top:16rem}.swagger-ui .mv0-l{margin-bottom:0;margin-top:0}.swagger-ui .mv1-l{margin-bottom:.25rem;margin-top:.25rem}.swagger-ui .mv2-l{margin-bottom:.5rem;margin-top:.5rem}.swagger-ui .mv3-l{margin-bottom:1rem;margin-top:1rem}.swagger-ui .mv4-l{margin-bottom:2rem;margin-top:2rem}.swagger-ui .mv5-l{margin-bottom:4rem;margin-top:4rem}.swagger-ui .mv6-l{margin-bottom:8rem;margin-top:8rem}.swagger-ui .mv7-l{margin-bottom:16rem;margin-top:16rem}.swagger-ui .mh0-l{margin-left:0;margin-right:0}.swagger-ui .mh1-l{margin-left:.25rem;margin-right:.25rem}.swagger-ui .mh2-l{margin-left:.5rem;margin-right:.5rem}.swagger-ui .mh3-l{margin-left:1rem;margin-right:1rem}.swagger-ui .mh4-l{margin-left:2rem;margin-right:2rem}.swagger-ui .mh5-l{margin-left:4rem;margin-right:4rem}.swagger-ui .mh6-l{margin-left:8rem;margin-right:8rem}.swagger-ui .mh7-l{margin-left:16rem;margin-right:16rem}}.swagger-ui .na1{margin:-.25rem}.swagger-ui .na2{margin:-.5rem}.swagger-ui .na3{margin:-1rem}.swagger-ui .na4{margin:-2rem}.swagger-ui .na5{margin:-4rem}.swagger-ui .na6{margin:-8rem}.swagger-ui .na7{margin:-16rem}.swagger-ui .nl1{margin-left:-.25rem}.swagger-ui .nl2{margin-left:-.5rem}.swagger-ui .nl3{margin-left:-1rem}.swagger-ui .nl4{margin-left:-2rem}.swagger-ui .nl5{margin-left:-4rem}.swagger-ui .nl6{margin-left:-8rem}.swagger-ui .nl7{margin-left:-16rem}.swagger-ui .nr1{margin-right:-.25rem}.swagger-ui .nr2{margin-right:-.5rem}.swagger-ui .nr3{margin-right:-1rem}.swagger-ui .nr4{margin-right:-2rem}.swagger-ui .nr5{margin-right:-4rem}.swagger-ui .nr6{margin-right:-8rem}.swagger-ui .nr7{margin-right:-16rem}.swagger-ui .nb1{margin-bottom:-.25rem}.swagger-ui .nb2{margin-bottom:-.5rem}.swagger-ui .nb3{margin-bottom:-1rem}.swagger-ui .nb4{margin-bottom:-2rem}.swagger-ui .nb5{margin-bottom:-4rem}.swagger-ui .nb6{margin-bottom:-8rem}.swagger-ui .nb7{margin-bottom:-16rem}.swagger-ui .nt1{margin-top:-.25rem}.swagger-ui .nt2{margin-top:-.5rem}.swagger-ui .nt3{margin-top:-1rem}.swagger-ui .nt4{margin-top:-2rem}.swagger-ui .nt5{margin-top:-4rem}.swagger-ui .nt6{margin-top:-8rem}.swagger-ui .nt7{margin-top:-16rem}@media screen and (min-width:30em){.swagger-ui .na1-ns{margin:-.25rem}.swagger-ui .na2-ns{margin:-.5rem}.swagger-ui .na3-ns{margin:-1rem}.swagger-ui .na4-ns{margin:-2rem}.swagger-ui .na5-ns{margin:-4rem}.swagger-ui .na6-ns{margin:-8rem}.swagger-ui .na7-ns{margin:-16rem}.swagger-ui .nl1-ns{margin-left:-.25rem}.swagger-ui .nl2-ns{margin-left:-.5rem}.swagger-ui .nl3-ns{margin-left:-1rem}.swagger-ui .nl4-ns{margin-left:-2rem}.swagger-ui .nl5-ns{margin-left:-4rem}.swagger-ui .nl6-ns{margin-left:-8rem}.swagger-ui .nl7-ns{margin-left:-16rem}.swagger-ui .nr1-ns{margin-right:-.25rem}.swagger-ui .nr2-ns{margin-right:-.5rem}.swagger-ui .nr3-ns{margin-right:-1rem}.swagger-ui .nr4-ns{margin-right:-2rem}.swagger-ui .nr5-ns{margin-right:-4rem}.swagger-ui .nr6-ns{margin-right:-8rem}.swagger-ui .nr7-ns{margin-right:-16rem}.swagger-ui .nb1-ns{margin-bottom:-.25rem}.swagger-ui .nb2-ns{margin-bottom:-.5rem}.swagger-ui .nb3-ns{margin-bottom:-1rem}.swagger-ui .nb4-ns{margin-bottom:-2rem}.swagger-ui .nb5-ns{margin-bottom:-4rem}.swagger-ui .nb6-ns{margin-bottom:-8rem}.swagger-ui .nb7-ns{margin-bottom:-16rem}.swagger-ui .nt1-ns{margin-top:-.25rem}.swagger-ui .nt2-ns{margin-top:-.5rem}.swagger-ui .nt3-ns{margin-top:-1rem}.swagger-ui .nt4-ns{margin-top:-2rem}.swagger-ui .nt5-ns{margin-top:-4rem}.swagger-ui .nt6-ns{margin-top:-8rem}.swagger-ui .nt7-ns{margin-top:-16rem}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .na1-m{margin:-.25rem}.swagger-ui .na2-m{margin:-.5rem}.swagger-ui .na3-m{margin:-1rem}.swagger-ui .na4-m{margin:-2rem}.swagger-ui .na5-m{margin:-4rem}.swagger-ui .na6-m{margin:-8rem}.swagger-ui .na7-m{margin:-16rem}.swagger-ui .nl1-m{margin-left:-.25rem}.swagger-ui .nl2-m{margin-left:-.5rem}.swagger-ui .nl3-m{margin-left:-1rem}.swagger-ui .nl4-m{margin-left:-2rem}.swagger-ui .nl5-m{margin-left:-4rem}.swagger-ui .nl6-m{margin-left:-8rem}.swagger-ui .nl7-m{margin-left:-16rem}.swagger-ui .nr1-m{margin-right:-.25rem}.swagger-ui .nr2-m{margin-right:-.5rem}.swagger-ui .nr3-m{margin-right:-1rem}.swagger-ui .nr4-m{margin-right:-2rem}.swagger-ui .nr5-m{margin-right:-4rem}.swagger-ui .nr6-m{margin-right:-8rem}.swagger-ui .nr7-m{margin-right:-16rem}.swagger-ui .nb1-m{margin-bottom:-.25rem}.swagger-ui .nb2-m{margin-bottom:-.5rem}.swagger-ui .nb3-m{margin-bottom:-1rem}.swagger-ui .nb4-m{margin-bottom:-2rem}.swagger-ui .nb5-m{margin-bottom:-4rem}.swagger-ui .nb6-m{margin-bottom:-8rem}.swagger-ui .nb7-m{margin-bottom:-16rem}.swagger-ui .nt1-m{margin-top:-.25rem}.swagger-ui .nt2-m{margin-top:-.5rem}.swagger-ui .nt3-m{margin-top:-1rem}.swagger-ui .nt4-m{margin-top:-2rem}.swagger-ui .nt5-m{margin-top:-4rem}.swagger-ui .nt6-m{margin-top:-8rem}.swagger-ui .nt7-m{margin-top:-16rem}}@media screen and (min-width:60em){.swagger-ui .na1-l{margin:-.25rem}.swagger-ui .na2-l{margin:-.5rem}.swagger-ui .na3-l{margin:-1rem}.swagger-ui .na4-l{margin:-2rem}.swagger-ui .na5-l{margin:-4rem}.swagger-ui .na6-l{margin:-8rem}.swagger-ui .na7-l{margin:-16rem}.swagger-ui .nl1-l{margin-left:-.25rem}.swagger-ui .nl2-l{margin-left:-.5rem}.swagger-ui .nl3-l{margin-left:-1rem}.swagger-ui .nl4-l{margin-left:-2rem}.swagger-ui .nl5-l{margin-left:-4rem}.swagger-ui .nl6-l{margin-left:-8rem}.swagger-ui .nl7-l{margin-left:-16rem}.swagger-ui .nr1-l{margin-right:-.25rem}.swagger-ui .nr2-l{margin-right:-.5rem}.swagger-ui .nr3-l{margin-right:-1rem}.swagger-ui .nr4-l{margin-right:-2rem}.swagger-ui .nr5-l{margin-right:-4rem}.swagger-ui .nr6-l{margin-right:-8rem}.swagger-ui .nr7-l{margin-right:-16rem}.swagger-ui .nb1-l{margin-bottom:-.25rem}.swagger-ui .nb2-l{margin-bottom:-.5rem}.swagger-ui .nb3-l{margin-bottom:-1rem}.swagger-ui .nb4-l{margin-bottom:-2rem}.swagger-ui .nb5-l{margin-bottom:-4rem}.swagger-ui .nb6-l{margin-bottom:-8rem}.swagger-ui .nb7-l{margin-bottom:-16rem}.swagger-ui .nt1-l{margin-top:-.25rem}.swagger-ui .nt2-l{margin-top:-.5rem}.swagger-ui .nt3-l{margin-top:-1rem}.swagger-ui .nt4-l{margin-top:-2rem}.swagger-ui .nt5-l{margin-top:-4rem}.swagger-ui .nt6-l{margin-top:-8rem}.swagger-ui .nt7-l{margin-top:-16rem}}.swagger-ui .collapse{border-collapse:collapse;border-spacing:0}.swagger-ui .striped--light-silver:nth-child(odd){background-color:#aaa}.swagger-ui .striped--moon-gray:nth-child(odd){background-color:#ccc}.swagger-ui .striped--light-gray:nth-child(odd){background-color:#eee}.swagger-ui .striped--near-white:nth-child(odd){background-color:#f4f4f4}.swagger-ui .stripe-light:nth-child(odd){background-color:hsla(0,0%,100%,.1)}.swagger-ui .stripe-dark:nth-child(odd){background-color:rgba(0,0,0,.1)}.swagger-ui .strike{-webkit-text-decoration:line-through;text-decoration:line-through}.swagger-ui .underline{-webkit-text-decoration:underline;text-decoration:underline}.swagger-ui .no-underline{-webkit-text-decoration:none;text-decoration:none}@media screen and (min-width:30em){.swagger-ui .strike-ns{-webkit-text-decoration:line-through;text-decoration:line-through}.swagger-ui .underline-ns{-webkit-text-decoration:underline;text-decoration:underline}.swagger-ui .no-underline-ns{-webkit-text-decoration:none;text-decoration:none}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .strike-m{-webkit-text-decoration:line-through;text-decoration:line-through}.swagger-ui .underline-m{-webkit-text-decoration:underline;text-decoration:underline}.swagger-ui .no-underline-m{-webkit-text-decoration:none;text-decoration:none}}@media screen and (min-width:60em){.swagger-ui .strike-l{-webkit-text-decoration:line-through;text-decoration:line-through}.swagger-ui .underline-l{-webkit-text-decoration:underline;text-decoration:underline}.swagger-ui .no-underline-l{-webkit-text-decoration:none;text-decoration:none}}.swagger-ui .tl{text-align:left}.swagger-ui .tr{text-align:right}.swagger-ui .tc{text-align:center}.swagger-ui .tj{text-align:justify}@media screen and (min-width:30em){.swagger-ui .tl-ns{text-align:left}.swagger-ui .tr-ns{text-align:right}.swagger-ui .tc-ns{text-align:center}.swagger-ui .tj-ns{text-align:justify}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .tl-m{text-align:left}.swagger-ui .tr-m{text-align:right}.swagger-ui .tc-m{text-align:center}.swagger-ui .tj-m{text-align:justify}}@media screen and (min-width:60em){.swagger-ui .tl-l{text-align:left}.swagger-ui .tr-l{text-align:right}.swagger-ui .tc-l{text-align:center}.swagger-ui .tj-l{text-align:justify}}.swagger-ui .ttc{text-transform:capitalize}.swagger-ui .ttl{text-transform:lowercase}.swagger-ui .ttu{text-transform:uppercase}.swagger-ui .ttn{text-transform:none}@media screen and (min-width:30em){.swagger-ui .ttc-ns{text-transform:capitalize}.swagger-ui .ttl-ns{text-transform:lowercase}.swagger-ui .ttu-ns{text-transform:uppercase}.swagger-ui .ttn-ns{text-transform:none}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .ttc-m{text-transform:capitalize}.swagger-ui .ttl-m{text-transform:lowercase}.swagger-ui .ttu-m{text-transform:uppercase}.swagger-ui .ttn-m{text-transform:none}}@media screen and (min-width:60em){.swagger-ui .ttc-l{text-transform:capitalize}.swagger-ui .ttl-l{text-transform:lowercase}.swagger-ui .ttu-l{text-transform:uppercase}.swagger-ui .ttn-l{text-transform:none}}.swagger-ui .f-6,.swagger-ui .f-headline{font-size:6rem}.swagger-ui .f-5,.swagger-ui .f-subheadline{font-size:5rem}.swagger-ui .f1{font-size:3rem}.swagger-ui .f2{font-size:2.25rem}.swagger-ui .f3{font-size:1.5rem}.swagger-ui .f4{font-size:1.25rem}.swagger-ui .f5{font-size:1rem}.swagger-ui .f6{font-size:.875rem}.swagger-ui .f7{font-size:.75rem}@media screen and (min-width:30em){.swagger-ui .f-6-ns,.swagger-ui .f-headline-ns{font-size:6rem}.swagger-ui .f-5-ns,.swagger-ui .f-subheadline-ns{font-size:5rem}.swagger-ui .f1-ns{font-size:3rem}.swagger-ui .f2-ns{font-size:2.25rem}.swagger-ui .f3-ns{font-size:1.5rem}.swagger-ui .f4-ns{font-size:1.25rem}.swagger-ui .f5-ns{font-size:1rem}.swagger-ui .f6-ns{font-size:.875rem}.swagger-ui .f7-ns{font-size:.75rem}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .f-6-m,.swagger-ui .f-headline-m{font-size:6rem}.swagger-ui .f-5-m,.swagger-ui .f-subheadline-m{font-size:5rem}.swagger-ui .f1-m{font-size:3rem}.swagger-ui .f2-m{font-size:2.25rem}.swagger-ui .f3-m{font-size:1.5rem}.swagger-ui .f4-m{font-size:1.25rem}.swagger-ui .f5-m{font-size:1rem}.swagger-ui .f6-m{font-size:.875rem}.swagger-ui .f7-m{font-size:.75rem}}@media screen and (min-width:60em){.swagger-ui .f-6-l,.swagger-ui .f-headline-l{font-size:6rem}.swagger-ui .f-5-l,.swagger-ui .f-subheadline-l{font-size:5rem}.swagger-ui .f1-l{font-size:3rem}.swagger-ui .f2-l{font-size:2.25rem}.swagger-ui .f3-l{font-size:1.5rem}.swagger-ui .f4-l{font-size:1.25rem}.swagger-ui .f5-l{font-size:1rem}.swagger-ui .f6-l{font-size:.875rem}.swagger-ui .f7-l{font-size:.75rem}}.swagger-ui .measure{max-width:30em}.swagger-ui .measure-wide{max-width:34em}.swagger-ui .measure-narrow{max-width:20em}.swagger-ui .indent{margin-bottom:0;margin-top:0;text-indent:1em}.swagger-ui .small-caps{font-feature-settings:\"smcp\";font-variant:small-caps}.swagger-ui .truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media screen and (min-width:30em){.swagger-ui .measure-ns{max-width:30em}.swagger-ui .measure-wide-ns{max-width:34em}.swagger-ui .measure-narrow-ns{max-width:20em}.swagger-ui .indent-ns{margin-bottom:0;margin-top:0;text-indent:1em}.swagger-ui .small-caps-ns{font-feature-settings:\"smcp\";font-variant:small-caps}.swagger-ui .truncate-ns{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .measure-m{max-width:30em}.swagger-ui .measure-wide-m{max-width:34em}.swagger-ui .measure-narrow-m{max-width:20em}.swagger-ui .indent-m{margin-bottom:0;margin-top:0;text-indent:1em}.swagger-ui .small-caps-m{font-feature-settings:\"smcp\";font-variant:small-caps}.swagger-ui .truncate-m{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}@media screen and (min-width:60em){.swagger-ui .measure-l{max-width:30em}.swagger-ui .measure-wide-l{max-width:34em}.swagger-ui .measure-narrow-l{max-width:20em}.swagger-ui .indent-l{margin-bottom:0;margin-top:0;text-indent:1em}.swagger-ui .small-caps-l{font-feature-settings:\"smcp\";font-variant:small-caps}.swagger-ui .truncate-l{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}.swagger-ui .overflow-container{overflow-y:scroll}.swagger-ui .center{margin-left:auto;margin-right:auto}.swagger-ui .mr-auto{margin-right:auto}.swagger-ui .ml-auto{margin-left:auto}@media screen and (min-width:30em){.swagger-ui .center-ns{margin-left:auto;margin-right:auto}.swagger-ui .mr-auto-ns{margin-right:auto}.swagger-ui .ml-auto-ns{margin-left:auto}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .center-m{margin-left:auto;margin-right:auto}.swagger-ui .mr-auto-m{margin-right:auto}.swagger-ui .ml-auto-m{margin-left:auto}}@media screen and (min-width:60em){.swagger-ui .center-l{margin-left:auto;margin-right:auto}.swagger-ui .mr-auto-l{margin-right:auto}.swagger-ui .ml-auto-l{margin-left:auto}}.swagger-ui .clip{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}@media screen and (min-width:30em){.swagger-ui .clip-ns{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .clip-m{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}}@media screen and (min-width:60em){.swagger-ui .clip-l{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}}.swagger-ui .ws-normal{white-space:normal}.swagger-ui .nowrap{white-space:nowrap}.swagger-ui .pre{white-space:pre}@media screen and (min-width:30em){.swagger-ui .ws-normal-ns{white-space:normal}.swagger-ui .nowrap-ns{white-space:nowrap}.swagger-ui .pre-ns{white-space:pre}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .ws-normal-m{white-space:normal}.swagger-ui .nowrap-m{white-space:nowrap}.swagger-ui .pre-m{white-space:pre}}@media screen and (min-width:60em){.swagger-ui .ws-normal-l{white-space:normal}.swagger-ui .nowrap-l{white-space:nowrap}.swagger-ui .pre-l{white-space:pre}}.swagger-ui .v-base{vertical-align:baseline}.swagger-ui .v-mid{vertical-align:middle}.swagger-ui .v-top{vertical-align:top}.swagger-ui .v-btm{vertical-align:bottom}@media screen and (min-width:30em){.swagger-ui .v-base-ns{vertical-align:baseline}.swagger-ui .v-mid-ns{vertical-align:middle}.swagger-ui .v-top-ns{vertical-align:top}.swagger-ui .v-btm-ns{vertical-align:bottom}}@media screen and (min-width:30em)and (max-width:60em){.swagger-ui .v-base-m{vertical-align:baseline}.swagger-ui .v-mid-m{vertical-align:middle}.swagger-ui .v-top-m{vertical-align:top}.swagger-ui .v-btm-m{vertical-align:bottom}}@media screen and (min-width:60em){.swagger-ui .v-base-l{vertical-align:baseline}.swagger-ui .v-mid-l{vertical-align:middle}.swagger-ui .v-top-l{vertical-align:top}.swagger-ui .v-btm-l{vertical-align:bottom}}.swagger-ui .dim{opacity:1;transition:opacity .15s ease-in}.swagger-ui .dim:focus,.swagger-ui .dim:hover{opacity:.5;transition:opacity .15s ease-in}.swagger-ui .dim:active{opacity:.8;transition:opacity .15s ease-out}.swagger-ui .glow{transition:opacity .15s ease-in}.swagger-ui .glow:focus,.swagger-ui .glow:hover{opacity:1;transition:opacity .15s ease-in}.swagger-ui .hide-child .child{opacity:0;transition:opacity .15s ease-in}.swagger-ui .hide-child:active .child,.swagger-ui .hide-child:focus .child,.swagger-ui .hide-child:hover .child{opacity:1;transition:opacity .15s ease-in}.swagger-ui .underline-hover:focus,.swagger-ui .underline-hover:hover{-webkit-text-decoration:underline;text-decoration:underline}.swagger-ui .grow{-moz-osx-font-smoothing:grayscale;backface-visibility:hidden;transform:translateZ(0);transition:transform .25s ease-out}.swagger-ui .grow:focus,.swagger-ui .grow:hover{transform:scale(1.05)}.swagger-ui .grow:active{transform:scale(.9)}.swagger-ui .grow-large{-moz-osx-font-smoothing:grayscale;backface-visibility:hidden;transform:translateZ(0);transition:transform .25s ease-in-out}.swagger-ui .grow-large:focus,.swagger-ui .grow-large:hover{transform:scale(1.2)}.swagger-ui .grow-large:active{transform:scale(.95)}.swagger-ui .pointer:hover{cursor:pointer}.swagger-ui .shadow-hover{cursor:pointer;position:relative;transition:all .5s cubic-bezier(.165,.84,.44,1)}.swagger-ui .shadow-hover:after{border-radius:inherit;box-shadow:0 0 16px 2px rgba(0,0,0,.2);content:\"\";height:100%;left:0;opacity:0;position:absolute;top:0;transition:opacity .5s cubic-bezier(.165,.84,.44,1);width:100%;z-index:-1}.swagger-ui .shadow-hover:focus:after,.swagger-ui .shadow-hover:hover:after{opacity:1}.swagger-ui .bg-animate,.swagger-ui .bg-animate:focus,.swagger-ui .bg-animate:hover{transition:background-color .15s ease-in-out}.swagger-ui .z-0{z-index:0}.swagger-ui .z-1{z-index:1}.swagger-ui .z-2{z-index:2}.swagger-ui .z-3{z-index:3}.swagger-ui .z-4{z-index:4}.swagger-ui .z-5{z-index:5}.swagger-ui .z-999{z-index:999}.swagger-ui .z-9999{z-index:9999}.swagger-ui .z-max{z-index:2147483647}.swagger-ui .z-inherit{z-index:inherit}.swagger-ui .z-initial,.swagger-ui .z-unset{z-index:auto}.swagger-ui .nested-copy-line-height ol,.swagger-ui .nested-copy-line-height p,.swagger-ui .nested-copy-line-height ul{line-height:1.5}.swagger-ui .nested-headline-line-height h1,.swagger-ui .nested-headline-line-height h2,.swagger-ui .nested-headline-line-height h3,.swagger-ui .nested-headline-line-height h4,.swagger-ui .nested-headline-line-height h5,.swagger-ui .nested-headline-line-height h6{line-height:1.25}.swagger-ui .nested-list-reset ol,.swagger-ui .nested-list-reset ul{list-style-type:none;margin-left:0;padding-left:0}.swagger-ui .nested-copy-indent p+p{margin-bottom:0;margin-top:0;text-indent:.1em}.swagger-ui .nested-copy-seperator p+p{margin-top:1.5em}.swagger-ui .nested-img img{display:block;max-width:100%;width:100%}.swagger-ui .nested-links a{color:#357edd;transition:color .15s ease-in}.swagger-ui .nested-links a:focus,.swagger-ui .nested-links a:hover{color:#96ccff;transition:color .15s ease-in}.swagger-ui .wrapper{box-sizing:border-box;margin:0 auto;max-width:1460px;padding:0 20px;width:100%}.swagger-ui .opblock-tag-section{display:flex;flex-direction:column}.swagger-ui .try-out.btn-group{display:flex;flex:.1 2 auto;padding:0}.swagger-ui .try-out__btn{margin-left:1.25rem}.swagger-ui .opblock-tag{align-items:center;border-bottom:1px solid rgba(59,65,81,.3);cursor:pointer;display:flex;padding:10px 20px 10px 10px;transition:all .2s}.swagger-ui .opblock-tag:hover{background:rgba(0,0,0,.02)}.swagger-ui .opblock-tag{color:#3b4151;font-family:sans-serif;font-size:24px;margin:0 0 5px}.swagger-ui .opblock-tag.no-desc span{flex:1}.swagger-ui .opblock-tag svg{transition:all .4s}.swagger-ui .opblock-tag small{color:#3b4151;flex:2;font-family:sans-serif;font-size:14px;font-weight:400;padding:0 10px}.swagger-ui .opblock-tag>div{flex:1 1 150px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media(max-width:640px){.swagger-ui .opblock-tag small,.swagger-ui .opblock-tag>div{flex:1}}.swagger-ui .opblock-tag .info__externaldocs{text-align:right}.swagger-ui .parameter__type{color:#3b4151;font-family:monospace;font-size:12px;font-weight:600;padding:5px 0}.swagger-ui .parameter-controls{margin-top:.75em}.swagger-ui .examples__title{display:block;font-size:1.1em;font-weight:700;margin-bottom:.75em}.swagger-ui .examples__section{margin-top:1.5em}.swagger-ui .examples__section-header{font-size:.9rem;font-weight:700;margin-bottom:.5rem}.swagger-ui .examples-select{display:inline-block;margin-bottom:.75em}.swagger-ui .examples-select .examples-select-element{width:100%}.swagger-ui .examples-select__section-label{font-size:.9rem;font-weight:700;margin-right:.5rem}.swagger-ui .example__section{margin-top:1.5em}.swagger-ui .example__section-header{font-size:.9rem;font-weight:700;margin-bottom:.5rem}.swagger-ui .view-line-link{cursor:pointer;margin:0 5px;position:relative;top:3px;transition:all .5s;width:20px}.swagger-ui .opblock{border:1px solid #000;border-radius:4px;box-shadow:0 0 3px rgba(0,0,0,.19);margin:0 0 15px}.swagger-ui .opblock .tab-header{display:flex;flex:1}.swagger-ui .opblock .tab-header .tab-item{cursor:pointer;padding:0 40px}.swagger-ui .opblock .tab-header .tab-item:first-of-type{padding:0 40px 0 0}.swagger-ui .opblock .tab-header .tab-item.active h4 span{position:relative}.swagger-ui .opblock .tab-header .tab-item.active h4 span:after{background:grey;bottom:-15px;content:\"\";height:4px;left:50%;position:absolute;transform:translateX(-50%);width:120%}.swagger-ui .opblock.is-open .opblock-summary{border-bottom:1px solid #000}.swagger-ui .opblock .opblock-section-header{align-items:center;background:hsla(0,0%,100%,.8);box-shadow:0 1px 2px rgba(0,0,0,.1);display:flex;min-height:50px;padding:8px 20px}.swagger-ui .opblock .opblock-section-header>label{align-items:center;color:#3b4151;display:flex;font-family:sans-serif;font-size:12px;font-weight:700;margin:0 0 0 auto}.swagger-ui .opblock .opblock-section-header>label>span{padding:0 10px 0 0}.swagger-ui .opblock .opblock-section-header h4{color:#3b4151;flex:1;font-family:sans-serif;font-size:14px;margin:0}.swagger-ui .opblock .opblock-summary-method{background:#000;border-radius:3px;color:#fff;font-family:sans-serif;font-size:14px;font-weight:700;min-width:80px;padding:6px 0;text-align:center;text-shadow:0 1px 0 rgba(0,0,0,.1)}@media(max-width:768px){.swagger-ui .opblock .opblock-summary-method{font-size:12px}}.swagger-ui .opblock .opblock-summary-operation-id,.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{align-items:center;color:#3b4151;display:flex;font-family:monospace;font-size:16px;font-weight:600;word-break:break-word}@media(max-width:768px){.swagger-ui .opblock .opblock-summary-operation-id,.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{font-size:12px}}.swagger-ui .opblock .opblock-summary-path{flex-shrink:1}@media(max-width:640px){.swagger-ui .opblock .opblock-summary-path{max-width:100%}}.swagger-ui .opblock .opblock-summary-path__deprecated{-webkit-text-decoration:line-through;text-decoration:line-through}.swagger-ui .opblock .opblock-summary-operation-id{font-size:14px}.swagger-ui .opblock .opblock-summary-description{color:#3b4151;font-family:sans-serif;font-size:13px;word-break:break-word}.swagger-ui .opblock .opblock-summary-path-description-wrapper{align-items:center;display:flex;flex-direction:row;flex-grow:1;flex-wrap:wrap;gap:0 10px;padding:0 10px}@media(max-width:550px){.swagger-ui .opblock .opblock-summary-path-description-wrapper{align-items:flex-start;flex-direction:column}}.swagger-ui .opblock .opblock-summary{align-items:center;cursor:pointer;display:flex;padding:5px}.swagger-ui .opblock .opblock-summary .view-line-link{cursor:pointer;margin:0;position:relative;top:2px;transition:all .5s;width:0}.swagger-ui .opblock .opblock-summary:hover .view-line-link{margin:0 5px;width:18px}.swagger-ui .opblock .opblock-summary:hover .view-line-link.copy-to-clipboard{width:24px}.swagger-ui .opblock.opblock-post{background:rgba(73,204,144,.1);border-color:#49cc90}.swagger-ui .opblock.opblock-post .opblock-summary-method{background:#49cc90}.swagger-ui .opblock.opblock-post .opblock-summary{border-color:#49cc90}.swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span:after{background:#49cc90}.swagger-ui .opblock.opblock-put{background:rgba(252,161,48,.1);border-color:#fca130}.swagger-ui .opblock.opblock-put .opblock-summary-method{background:#fca130}.swagger-ui .opblock.opblock-put .opblock-summary{border-color:#fca130}.swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span:after{background:#fca130}.swagger-ui .opblock.opblock-delete{background:rgba(249,62,62,.1);border-color:#f93e3e}.swagger-ui .opblock.opblock-delete .opblock-summary-method{background:#f93e3e}.swagger-ui .opblock.opblock-delete .opblock-summary{border-color:#f93e3e}.swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span:after{background:#f93e3e}.swagger-ui .opblock.opblock-get{background:rgba(97,175,254,.1);border-color:#61affe}.swagger-ui .opblock.opblock-get .opblock-summary-method{background:#61affe}.swagger-ui .opblock.opblock-get .opblock-summary{border-color:#61affe}.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span:after{background:#61affe}.swagger-ui .opblock.opblock-patch{background:rgba(80,227,194,.1);border-color:#50e3c2}.swagger-ui .opblock.opblock-patch .opblock-summary-method{background:#50e3c2}.swagger-ui .opblock.opblock-patch .opblock-summary{border-color:#50e3c2}.swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span:after{background:#50e3c2}.swagger-ui .opblock.opblock-head{background:rgba(144,18,254,.1);border-color:#9012fe}.swagger-ui .opblock.opblock-head .opblock-summary-method{background:#9012fe}.swagger-ui .opblock.opblock-head .opblock-summary{border-color:#9012fe}.swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span:after{background:#9012fe}.swagger-ui .opblock.opblock-options{background:rgba(13,90,167,.1);border-color:#0d5aa7}.swagger-ui .opblock.opblock-options .opblock-summary-method{background:#0d5aa7}.swagger-ui .opblock.opblock-options .opblock-summary{border-color:#0d5aa7}.swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span:after{background:#0d5aa7}.swagger-ui .opblock.opblock-deprecated{background:hsla(0,0%,92%,.1);border-color:#ebebeb;opacity:.6}.swagger-ui .opblock.opblock-deprecated .opblock-summary-method{background:#ebebeb}.swagger-ui .opblock.opblock-deprecated .opblock-summary{border-color:#ebebeb}.swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span:after{background:#ebebeb}.swagger-ui .opblock .opblock-schemes{padding:8px 20px}.swagger-ui .opblock .opblock-schemes .schemes-title{padding:0 10px 0 0}.swagger-ui .filter .operation-filter-input{border:2px solid #d8dde7;margin:20px 0;padding:10px;width:100%}.swagger-ui .download-url-wrapper .failed,.swagger-ui .filter .failed{color:red}.swagger-ui .download-url-wrapper .loading,.swagger-ui .filter .loading{color:#aaa}.swagger-ui .model-example{margin-top:1em}.swagger-ui .model-example .model-container{overflow-x:auto;width:100%}.swagger-ui .model-example .model-container .model-hint:not(.model-hint--embedded){top:-1.15em}.swagger-ui .tab{display:flex;list-style:none;padding:0}.swagger-ui .tab li{color:#3b4151;cursor:pointer;font-family:sans-serif;font-size:12px;min-width:60px;padding:0}.swagger-ui .tab li:first-of-type{padding-left:0;padding-right:12px;position:relative}.swagger-ui .tab li:first-of-type:after{background:rgba(0,0,0,.2);content:\"\";height:100%;position:absolute;right:6px;top:0;width:1px}.swagger-ui .tab li.active{font-weight:700}.swagger-ui .tab li button.tablinks{background:none;border:0;color:inherit;font-family:inherit;font-weight:inherit;padding:0}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-external-docs-wrapper,.swagger-ui .opblock-title_normal{color:#3b4151;font-family:sans-serif;font-size:12px;margin:0 0 5px;padding:15px 20px}.swagger-ui .opblock-description-wrapper h4,.swagger-ui .opblock-external-docs-wrapper h4,.swagger-ui .opblock-title_normal h4{color:#3b4151;font-family:sans-serif;font-size:12px;margin:0 0 5px}.swagger-ui .opblock-description-wrapper p,.swagger-ui .opblock-external-docs-wrapper p,.swagger-ui .opblock-title_normal p{color:#3b4151;font-family:sans-serif;font-size:14px;margin:0}.swagger-ui .opblock-external-docs-wrapper h4{padding-left:0}.swagger-ui .execute-wrapper{padding:20px;text-align:right}.swagger-ui .execute-wrapper .btn{padding:8px 40px;width:100%}.swagger-ui .body-param-options{display:flex;flex-direction:column}.swagger-ui .body-param-options .body-param-edit{padding:10px 0}.swagger-ui .body-param-options label{padding:8px 0}.swagger-ui .body-param-options label select{margin:3px 0 0}.swagger-ui .responses-inner{padding:20px}.swagger-ui .responses-inner h4,.swagger-ui .responses-inner h5{color:#3b4151;font-family:sans-serif;font-size:12px;margin:10px 0 5px}.swagger-ui .responses-inner .curl{max-height:400px;min-height:6em;overflow-y:auto}.swagger-ui .response-col_status{color:#3b4151;font-family:sans-serif;font-size:14px}.swagger-ui .response-col_status .response-undocumented{color:#909090;font-family:monospace;font-size:11px;font-weight:600}.swagger-ui .response-col_links{color:#3b4151;font-family:sans-serif;font-size:14px;max-width:40em;padding-left:2em}.swagger-ui .response-col_links .response-undocumented{color:#909090;font-family:monospace;font-size:11px;font-weight:600}.swagger-ui .response-col_links .operation-link{margin-bottom:1.5em}.swagger-ui .response-col_links .operation-link .description{margin-bottom:.5em}.swagger-ui .opblock-body .opblock-loading-animation{display:block;margin:3em auto}.swagger-ui .opblock-body pre.microlight{background:#333;border-radius:4px;font-size:12px;hyphens:auto;margin:0;padding:10px;white-space:pre-wrap;word-break:break-all;word-break:break-word;word-wrap:break-word;color:#fff;font-family:monospace;font-weight:600}.swagger-ui .opblock-body pre.microlight .headerline{display:block}.swagger-ui .highlight-code{position:relative}.swagger-ui .highlight-code>.microlight{max-height:400px;min-height:6em;overflow-y:auto}.swagger-ui .highlight-code>.microlight code{white-space:pre-wrap!important;word-break:break-all}.swagger-ui .curl-command{position:relative}.swagger-ui .download-contents{align-items:center;background:#7d8293;border:none;border-radius:4px;bottom:10px;color:#fff;display:flex;font-family:sans-serif;font-size:14px;font-weight:600;height:30px;justify-content:center;padding:5px;position:absolute;right:10px;text-align:center}.swagger-ui .scheme-container{background:#fff;box-shadow:0 1px 2px 0 rgba(0,0,0,.15);margin:0 0 20px;padding:30px 0}.swagger-ui .scheme-container .schemes{align-items:flex-end;display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between}.swagger-ui .scheme-container .schemes>.schemes-server-container{display:flex;flex-wrap:wrap;gap:10px}.swagger-ui .scheme-container .schemes>.schemes-server-container>label{color:#3b4151;display:flex;flex-direction:column;font-family:sans-serif;font-size:12px;font-weight:700;margin:-20px 15px 0 0}.swagger-ui .scheme-container .schemes>.schemes-server-container>label select{min-width:130px;text-transform:uppercase}.swagger-ui .scheme-container .schemes:not(:has(.schemes-server-container)){justify-content:flex-end}.swagger-ui .scheme-container .schemes .auth-wrapper{flex:none;justify-content:start}.swagger-ui .scheme-container .schemes .auth-wrapper .authorize{display:flex;flex-wrap:nowrap;margin:0;padding-right:20px}.swagger-ui .loading-container{align-items:center;display:flex;flex-direction:column;justify-content:center;margin-top:1em;min-height:1px;padding:40px 0 60px}.swagger-ui .loading-container .loading{position:relative}.swagger-ui .loading-container .loading:after{color:#3b4151;content:\"loading\";font-family:sans-serif;font-size:10px;font-weight:700;left:50%;position:absolute;text-transform:uppercase;top:50%;transform:translate(-50%,-50%)}.swagger-ui .loading-container .loading:before{animation:rotation 1s linear infinite,opacity .5s;backface-visibility:hidden;border:2px solid rgba(85,85,85,.1);border-radius:100%;border-top-color:rgba(0,0,0,.6);content:\"\";display:block;height:60px;left:50%;margin:-30px;opacity:1;position:absolute;top:50%;width:60px}@keyframes rotation{to{transform:rotate(1turn)}}.swagger-ui .response-controls{display:flex;padding-top:1em}.swagger-ui .response-control-media-type{margin-right:1em}.swagger-ui .response-control-media-type--accept-controller select{border-color:green}.swagger-ui .response-control-media-type__accept-message{color:green;font-size:.7em}.swagger-ui .response-control-examples__title,.swagger-ui .response-control-media-type__title{display:block;font-size:.7em;margin-bottom:.2em}@keyframes blinker{50%{opacity:0}}.swagger-ui .hidden{display:none}.swagger-ui .no-margin{border:none;height:auto;margin:0;padding:0}.swagger-ui .float-right{float:right}.swagger-ui .svg-assets{height:0;position:absolute;width:0}.swagger-ui section h3{color:#3b4151;font-family:sans-serif}.swagger-ui a.nostyle{display:inline}.swagger-ui a.nostyle,.swagger-ui a.nostyle:visited{color:inherit;cursor:pointer;text-decoration:inherit}.swagger-ui .fallback{color:#aaa;padding:1em}.swagger-ui .version-pragma{height:100%;padding:5em 0}.swagger-ui .version-pragma__message{display:flex;font-size:1.2em;height:100%;justify-content:center;line-height:1.5em;padding:0 .6em;text-align:center}.swagger-ui .version-pragma__message>div{flex:1;max-width:55ch}.swagger-ui .version-pragma__message code{background-color:#dedede;padding:4px 4px 2px;white-space:pre}.swagger-ui .opblock-link{font-weight:400}.swagger-ui .opblock-link.shown{font-weight:700}.swagger-ui span.token-string{color:#555}.swagger-ui span.token-not-formatted{color:#555;font-weight:700}.swagger-ui .btn{background:transparent;border:2px solid grey;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.1);color:#3b4151;font-family:sans-serif;font-size:14px;font-weight:700;padding:5px 23px;transition:all .3s}.swagger-ui .btn.btn-sm{font-size:12px;padding:4px 23px}.swagger-ui .btn[disabled]{cursor:not-allowed;opacity:.3}.swagger-ui .btn:hover{box-shadow:0 0 5px rgba(0,0,0,.3)}.swagger-ui .btn.cancel{background-color:transparent;border-color:#ff6060;color:#ff6060;font-family:sans-serif}.swagger-ui .btn.authorize{background-color:transparent;border-color:#49cc90;color:#49cc90;display:inline;line-height:1}.swagger-ui .btn.authorize span{float:left;padding:4px 20px 0 0}.swagger-ui .btn.authorize svg{fill:#49cc90}.swagger-ui .btn.execute{background-color:#4990e2;border-color:#4990e2;color:#fff}.swagger-ui .btn-group{display:flex;padding:30px}.swagger-ui .btn-group .btn{flex:1}.swagger-ui .btn-group .btn:first-child{border-radius:4px 0 0 4px}.swagger-ui .btn-group .btn:last-child{border-radius:0 4px 4px 0}.swagger-ui .authorization__btn{background:none;border:none;padding:0 0 0 10px}.swagger-ui .authorization__btn .locked{opacity:1}.swagger-ui .authorization__btn .unlocked{opacity:.4}.swagger-ui .model-box-control,.swagger-ui .models-control,.swagger-ui .opblock-summary-control{all:inherit;border-bottom:0;cursor:pointer;flex:1;padding:0}.swagger-ui .model-box-control:focus,.swagger-ui .models-control:focus,.swagger-ui .opblock-summary-control:focus{outline:auto}.swagger-ui .expand-methods,.swagger-ui .expand-operation{background:none;border:none}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{height:20px;width:20px}.swagger-ui .expand-methods{padding:0 10px}.swagger-ui .expand-methods:hover svg{fill:#404040}.swagger-ui .expand-methods svg{transition:all .3s;fill:#707070}.swagger-ui button{cursor:pointer}.swagger-ui button.invalid{animation:shake .4s 1;background:#feebeb;border-color:#f93e3e}.swagger-ui .copy-to-clipboard{align-items:center;background:#7d8293;border:none;border-radius:4px;bottom:10px;display:flex;height:30px;justify-content:center;position:absolute;right:100px;width:30px}.swagger-ui .copy-to-clipboard button{background:url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"16\\\" height=\\\"15\\\" aria-hidden=\\\"true\\\"><path fill=\\\"%23fff\\\" fill-rule=\\\"evenodd\\\" d=\\\"M4 12h4v1H4zm5-6H4v1h5zm2 3V7l-3 3 3 3v-2h5V9zM6.5 8H4v1h2.5zM4 11h2.5v-1H4zm9 1h1v2c-.02.28-.11.52-.3.7s-.42.28-.7.3H3c-.55 0-1-.45-1-1V3c0-.55.45-1 1-1h3c0-1.11.89-2 2-2s2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V5H3v9h10zM4 4h8c0-.55-.45-1-1-1h-1c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H5c-.55 0-1 .45-1 1\\\"/></svg>\") 50% no-repeat;border:none;flex-grow:1;flex-shrink:1;height:25px}.swagger-ui .copy-to-clipboard:active{background:#5e626f}.swagger-ui .opblock-control-arrow{background:none;border:none;text-align:center}.swagger-ui .curl-command .copy-to-clipboard{bottom:5px;height:20px;right:10px;width:20px}.swagger-ui .curl-command .copy-to-clipboard button{height:18px}.swagger-ui .opblock .opblock-summary .view-line-link.copy-to-clipboard{height:26px;position:static}.swagger-ui select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#f7f7f7 url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 20 20\\\"><path d=\\\"M13.418 7.859a.695.695 0 0 1 .978 0 .68.68 0 0 1 0 .969l-3.908 3.83a.697.697 0 0 1-.979 0l-3.908-3.83a.68.68 0 0 1 0-.969.695.695 0 0 1 .978 0L10 11z\\\"/></svg>\") right 10px center no-repeat;background-size:20px;border:2px solid #41444e;border-radius:4px;box-shadow:0 1px 2px 0 rgba(0,0,0,.25);color:#3b4151;font-family:sans-serif;font-size:14px;font-weight:700;padding:5px 40px 5px 10px}.swagger-ui select[multiple]{background:#f7f7f7;margin:5px 0;padding:5px}.swagger-ui select.invalid{animation:shake .4s 1;background:#feebeb;border-color:#f93e3e}.swagger-ui .opblock-body select{min-width:230px}@media(max-width:768px){.swagger-ui .opblock-body select{min-width:180px}}@media(max-width:640px){.swagger-ui .opblock-body select{min-width:100%;width:100%}}.swagger-ui label{color:#3b4151;font-family:sans-serif;font-size:12px;font-weight:700;margin:0 0 5px}.swagger-ui input[type=email],.swagger-ui input[type=file],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{line-height:1}@media(max-width:768px){.swagger-ui input[type=email],.swagger-ui input[type=file],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{max-width:175px}}.swagger-ui input[type=email],.swagger-ui input[type=file],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text],.swagger-ui textarea{background:#fff;border:1px solid #d9d9d9;border-radius:4px;margin:5px 0;min-width:100px;padding:8px 10px}.swagger-ui input[type=email].invalid,.swagger-ui input[type=file].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid,.swagger-ui textarea.invalid{animation:shake .4s 1;background:#feebeb;border-color:#f93e3e}.swagger-ui input[disabled],.swagger-ui select[disabled],.swagger-ui textarea[disabled]{background-color:#fafafa;color:#888;cursor:not-allowed}.swagger-ui select[disabled]{border-color:#888}.swagger-ui textarea[disabled]{background-color:#41444e;color:#fff}@keyframes shake{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}.swagger-ui textarea{background:hsla(0,0%,100%,.8);border:none;border-radius:4px;color:#3b4151;font-family:monospace;font-size:12px;font-weight:600;min-height:280px;outline:none;padding:10px;width:100%}.swagger-ui textarea:focus{border:2px solid #61affe}.swagger-ui textarea.curl{background:#41444e;border-radius:4px;color:#fff;font-family:monospace;font-size:12px;font-weight:600;margin:0;min-height:100px;padding:10px;resize:none}.swagger-ui .checkbox{color:#303030;padding:5px 0 10px;transition:opacity .5s}.swagger-ui .checkbox label{display:flex}.swagger-ui .checkbox p{color:#3b4151;font-family:monospace;font-style:italic;font-weight:400!important;font-weight:600;margin:0!important}.swagger-ui .checkbox input[type=checkbox]{display:none}.swagger-ui .checkbox input[type=checkbox]+label>.item{background:#e8e8e8;border-radius:1px;box-shadow:0 0 0 2px #e8e8e8;cursor:pointer;display:inline-block;flex:none;height:16px;margin:0 8px 0 0;padding:5px;position:relative;top:3px;width:16px}.swagger-ui .checkbox input[type=checkbox]+label>.item:active{transform:scale(.9)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:#e8e8e8 url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"10\\\" height=\\\"8\\\" viewBox=\\\"3 7 10 8\\\"><path fill=\\\"%2341474E\\\" fill-rule=\\\"evenodd\\\" d=\\\"M6.333 15 3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z\\\"/></svg>\") 50% no-repeat}.swagger-ui .dialog-ux{bottom:0;left:0;position:fixed;right:0;top:0;z-index:9999}.swagger-ui .dialog-ux .backdrop-ux{background:rgba(0,0,0,.8);bottom:0;left:0;position:fixed;right:0;top:0}.swagger-ui .dialog-ux .modal-ux{background:#fff;border:1px solid #ebebeb;border-radius:4px;box-shadow:0 10px 30px 0 rgba(0,0,0,.2);left:50%;max-width:650px;min-width:300px;position:absolute;top:50%;transform:translate(-50%,-50%);width:100%;z-index:9999}.swagger-ui .dialog-ux .modal-ux-content{max-height:540px;overflow-y:auto;padding:20px}.swagger-ui .dialog-ux .modal-ux-content p{color:#41444e;color:#3b4151;font-family:sans-serif;font-size:12px;margin:0 0 5px}.swagger-ui .dialog-ux .modal-ux-content h4{color:#3b4151;font-family:sans-serif;font-size:18px;font-weight:600;margin:15px 0 0}.swagger-ui .dialog-ux .modal-ux-header{align-items:center;border-bottom:1px solid #ebebeb;display:flex;padding:12px 0}.swagger-ui .dialog-ux .modal-ux-header .close-modal{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:none;padding:0 10px}.swagger-ui .dialog-ux .modal-ux-header h3{color:#3b4151;flex:1;font-family:sans-serif;font-size:20px;font-weight:600;margin:0;padding:0 20px}.swagger-ui .model{color:#3b4151;font-family:monospace;font-size:12px;font-weight:300;font-weight:600}.swagger-ui .model .deprecated span,.swagger-ui .model .deprecated td{color:#a0a0a0!important}.swagger-ui .model .deprecated>td:first-of-type{-webkit-text-decoration:line-through;text-decoration:line-through}.swagger-ui .model-toggle{cursor:pointer;display:inline-block;font-size:10px;margin:auto .3em;position:relative;top:6px;transform:rotate(90deg);transform-origin:50% 50%;transition:transform .15s ease-in}.swagger-ui .model-toggle.collapsed{transform:rotate(0deg)}.swagger-ui .model-toggle:after{background:url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"24\\\" height=\\\"24\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\\\"/></svg>\") 50% no-repeat;background-size:100%;content:\"\";display:block;height:20px;width:20px}.swagger-ui .model-jump-to-path{cursor:pointer;position:relative}.swagger-ui .model-jump-to-path .view-line-link{cursor:pointer;position:absolute;top:-.4em}.swagger-ui .model-title{position:relative}.swagger-ui .model-title:hover .model-hint{display:block}.swagger-ui .model-hint{background:rgba(0,0,0,.7);border-radius:4px;color:#ebebeb;display:none;padding:.1em .5em;position:absolute;top:-1.8em;white-space:nowrap}.swagger-ui .model p{margin:0 0 1em}.swagger-ui .model .property{color:#999;font-style:italic}.swagger-ui .model .property.primitive{color:#6b6b6b}.swagger-ui .model .property.primitive.extension{display:block}.swagger-ui .model .property.primitive.extension>td:first-child{padding-left:0;padding-right:0;width:auto}.swagger-ui .model .property.primitive.extension>td:first-child:after{content:\": \"}.swagger-ui .model .external-docs,.swagger-ui table.model tr.description{color:#666;font-weight:400}.swagger-ui table.model tr.description td:first-child,.swagger-ui table.model tr.property-row.required td:first-child{font-weight:700}.swagger-ui table.model tr.property-row td{vertical-align:top}.swagger-ui table.model tr.property-row td:first-child{padding-right:.2em}.swagger-ui table.model tr.property-row .star{color:red}.swagger-ui table.model tr.extension{color:#777}.swagger-ui table.model tr.extension td:last-child{vertical-align:top}.swagger-ui table.model tr.external-docs td:first-child{font-weight:700}.swagger-ui table.model tr .renderedMarkdown p:first-child{margin-top:0}.swagger-ui section.models{border:1px solid rgba(59,65,81,.3);border-radius:4px;margin:30px 0}.swagger-ui section.models .pointer{cursor:pointer}.swagger-ui section.models.is-open{padding:0 0 20px}.swagger-ui section.models.is-open h4{border-bottom:1px solid rgba(59,65,81,.3);margin:0 0 5px}.swagger-ui section.models h4{align-items:center;color:#606060;cursor:pointer;display:flex;font-family:sans-serif;font-size:16px;margin:0;padding:10px 20px 10px 10px;transition:all .2s}.swagger-ui section.models h4 svg{transition:all .4s}.swagger-ui section.models h4 span{flex:1}.swagger-ui section.models h4:hover{background:rgba(0,0,0,.02)}.swagger-ui section.models h5{color:#707070;font-family:sans-serif;font-size:16px;margin:0 0 10px}.swagger-ui section.models .model-jump-to-path{position:relative;top:5px}.swagger-ui section.models .model-container{background:rgba(0,0,0,.05);border-radius:4px;margin:0 20px 15px;position:relative;transition:all .5s}.swagger-ui section.models .model-container:hover{background:rgba(0,0,0,.07)}.swagger-ui section.models .model-container:first-of-type{margin:20px}.swagger-ui section.models .model-container:last-of-type{margin:0 20px}.swagger-ui section.models .model-container .models-jump-to-path{opacity:.65;position:absolute;right:5px;top:8px}.swagger-ui section.models .model-box{background:none}.swagger-ui section.models .model-box:has(.model-box){overflow-x:auto;width:100%}.swagger-ui .model-box{background:rgba(0,0,0,.1);border-radius:4px;display:inline-block;padding:10px}.swagger-ui .model-box .model-jump-to-path{position:relative;top:4px}.swagger-ui .model-box.deprecated{opacity:.5}.swagger-ui .model-title{color:#505050;font-family:sans-serif;font-size:16px}.swagger-ui .model-title img{bottom:0;margin-left:1em;position:relative}.swagger-ui .model-deprecated-warning{color:#f93e3e;font-family:sans-serif;font-size:16px;font-weight:600;margin-right:1em}.swagger-ui span>span.model .brace-close{padding:0 0 0 10px}.swagger-ui .prop-name{display:inline-block;margin-right:1em}.swagger-ui .prop-type{color:#55a}.swagger-ui .prop-enum{display:block}.swagger-ui .prop-format{color:#606060}.swagger-ui .servers>label{color:#3b4151;font-family:sans-serif;font-size:12px;margin:-20px 15px 0 0}.swagger-ui .servers>label select{max-width:100%;min-width:130px;width:100%}.swagger-ui .servers h4.message{padding-bottom:2em}.swagger-ui .servers table tr{width:30em}.swagger-ui .servers table td{display:inline-block;max-width:15em;padding-bottom:10px;padding-top:10px;vertical-align:middle}.swagger-ui .servers table td:first-of-type{padding-right:1em}.swagger-ui .servers table td input{height:100%;width:100%}.swagger-ui .servers .computed-url{margin:2em 0}.swagger-ui .servers .computed-url code{display:inline-block;font-size:16px;margin:0 1em;padding:4px}.swagger-ui .servers-title{font-size:12px;font-weight:700}.swagger-ui .operation-servers h4.message{margin-bottom:2em}.swagger-ui table{border-collapse:collapse;padding:0 10px;width:100%}.swagger-ui table.model tbody tr td{padding:0 0 0 1em;vertical-align:top}.swagger-ui table.model tbody tr td:first-of-type{padding:0 0 0 2em;width:174px}.swagger-ui table.headers td{color:#3b4151;font-family:monospace;font-size:12px;font-weight:300;font-weight:600;vertical-align:middle}.swagger-ui table.headers .header-example{color:#999;font-style:italic}.swagger-ui table tbody tr td{padding:10px 0 0;vertical-align:top}.swagger-ui table tbody tr td:first-of-type{min-width:6em;padding:10px 0}.swagger-ui table tbody tr td:has(.model-box){max-width:1px}.swagger-ui table thead tr td,.swagger-ui table thead tr th{border-bottom:1px solid rgba(59,65,81,.2);color:#3b4151;font-family:sans-serif;font-size:12px;font-weight:700;padding:12px 0;text-align:left}.swagger-ui .parameters-col_description{margin-bottom:2em;width:99%}.swagger-ui .parameters-col_description input{max-width:340px;width:100%}.swagger-ui .parameters-col_description select{border-width:1px}.swagger-ui .parameters-col_description .markdown:first-child p:first-child,.swagger-ui .parameters-col_description .renderedMarkdown:first-child p:first-child{margin:0}.swagger-ui .parameter__name{color:#3b4151;font-family:sans-serif;font-size:16px;font-weight:400;margin-right:.75em}.swagger-ui .parameter__name.required{font-weight:700}.swagger-ui .parameter__name.required span{color:red}.swagger-ui .parameter__name.required:after{color:rgba(255,0,0,.6);content:\"required\";font-size:10px;padding:5px;position:relative;top:-6px}.swagger-ui .parameter__extension,.swagger-ui .parameter__in{color:grey;font-family:monospace;font-size:12px;font-style:italic;font-weight:600}.swagger-ui .parameter__deprecated{color:red;font-family:monospace;font-size:12px;font-style:italic;font-weight:600}.swagger-ui .parameter__empty_value_toggle{display:block;font-size:13px;padding-bottom:12px;padding-top:5px}.swagger-ui .parameter__empty_value_toggle input{margin-right:7px;width:auto}.swagger-ui .parameter__empty_value_toggle.disabled{opacity:.7}.swagger-ui .table-container{padding:20px}.swagger-ui .response-col_description{width:99%}.swagger-ui .response-col_description .markdown p:first-child,.swagger-ui .response-col_description .renderedMarkdown p:first-child{margin:0}.swagger-ui .response-col_description .markdown p:last-child,.swagger-ui .response-col_description .renderedMarkdown p:last-child{margin-bottom:0}.swagger-ui .response-col_links{min-width:6em}.swagger-ui .response__extension{color:grey;font-family:monospace;font-size:12px;font-style:italic;font-weight:600}.swagger-ui .topbar{background-color:#1b1b1b;padding:10px 0}.swagger-ui .topbar .topbar-wrapper{align-items:center;display:flex;flex-wrap:wrap;gap:10px}@media(max-width:550px){.swagger-ui .topbar .topbar-wrapper{align-items:start;flex-direction:column}}.swagger-ui .topbar a{align-items:center;color:#fff;display:flex;flex:1;font-family:sans-serif;font-size:1.5em;font-weight:700;max-width:300px;-webkit-text-decoration:none;text-decoration:none}.swagger-ui .topbar a span{margin:0;padding:0 10px}.swagger-ui .topbar .download-url-wrapper{display:flex;flex:3;justify-content:flex-end;margin-left:auto;max-width:600px}.swagger-ui .topbar .download-url-wrapper input[type=text]{border:2px solid #62a03f;border-radius:4px 0 0 4px;margin:0;max-width:100%;outline:none;width:100%}.swagger-ui .topbar .download-url-wrapper .select-label{align-items:center;color:#f0f0f0;display:flex;margin:0;max-width:600px;width:100%}.swagger-ui .topbar .download-url-wrapper .select-label span{flex:1;font-size:16px;padding:0 10px 0 0;text-align:right}.swagger-ui .topbar .download-url-wrapper .select-label select{border:2px solid #62a03f;box-shadow:none;flex:2;outline:none;width:100%}.swagger-ui .topbar .download-url-wrapper .download-url-button{background:#62a03f;border:none;border-radius:0 4px 4px 0;color:#fff;font-family:sans-serif;font-size:16px;font-weight:700;padding:4px 30px}@media(max-width:550px){.swagger-ui .topbar .download-url-wrapper{width:100%}}.swagger-ui .topbar .dark-mode-toggle{cursor:pointer;margin-left:10px;opacity:.8;transition:all .2s}.swagger-ui .topbar .dark-mode-toggle button{background:none;border:none;padding:0}.swagger-ui .topbar .dark-mode-toggle button svg{fill:#e4e6e6}.swagger-ui .topbar .dark-mode-toggle:hover{opacity:1}.swagger-ui .info{margin:50px 0}.swagger-ui .info.failed-config{margin-left:auto;margin-right:auto;max-width:880px;text-align:center}.swagger-ui .info hgroup.main{margin:0 0 20px}.swagger-ui .info hgroup.main a{font-size:12px}.swagger-ui .info li,.swagger-ui .info p,.swagger-ui .info pre,.swagger-ui .info table{font-size:14px}.swagger-ui .info h1,.swagger-ui .info h2,.swagger-ui .info h3,.swagger-ui .info h4,.swagger-ui .info h5,.swagger-ui .info li,.swagger-ui .info p,.swagger-ui .info table{color:#3b4151;font-family:sans-serif}.swagger-ui .info a{color:#4990e2;font-family:sans-serif;font-size:14px;transition:all .4s}.swagger-ui .info a:hover{color:#1f69c0}.swagger-ui .info>div{margin:0 0 5px}.swagger-ui .info .base-url{color:#3b4151;font-family:monospace;font-size:12px;font-weight:300!important;font-weight:600;margin:0}.swagger-ui .info .title{color:#3b4151;font-family:sans-serif;font-size:36px;margin:0}.swagger-ui .info .title small{background:#7d8492;border-radius:57px;display:inline-block;font-size:10px;margin:0 0 0 5px;padding:2px 4px;position:relative;top:-5px;vertical-align:super}.swagger-ui .info .title small.version-stamp{background-color:#89bf04}.swagger-ui .info .title small pre{color:#fff;font-family:sans-serif;margin:0;padding:0}.swagger-ui .auth-btn-wrapper{display:flex;justify-content:center;padding:10px 0}.swagger-ui .auth-btn-wrapper .btn-done{margin-right:1em}.swagger-ui .auth-wrapper{display:flex;flex:1;justify-content:flex-end}.swagger-ui .auth-wrapper .authorize{margin-left:10px;margin-right:10px;padding-right:20px}.swagger-ui .auth-container{border-bottom:1px solid #ebebeb;margin:0 0 10px;padding:10px 20px}.swagger-ui .auth-container:last-of-type{border:0;margin:0;padding:10px 20px}.swagger-ui .auth-container h4{margin:5px 0 15px!important}.swagger-ui .auth-container .wrapper{margin:0;padding:0}.swagger-ui .auth-container input[type=password],.swagger-ui .auth-container input[type=text]{min-width:230px}.swagger-ui .auth-container .errors{background-color:#fee;border-radius:4px;color:red;color:#3b4151;font-family:monospace;font-size:12px;font-weight:600;margin:1em;padding:10px}.swagger-ui .auth-container .errors b{margin-right:1em;text-transform:capitalize}.swagger-ui .scopes h2{color:#3b4151;font-family:sans-serif;font-size:14px}.swagger-ui .scopes h2 a{color:#4990e2;cursor:pointer;font-size:12px;padding-left:10px;-webkit-text-decoration:underline;text-decoration:underline}.swagger-ui .scope-def{padding:0 0 20px}.swagger-ui .errors-wrapper{animation:scaleUp .5s;background:rgba(249,62,62,.1);border:2px solid #f93e3e;border-radius:4px;margin:20px;padding:10px 20px}.swagger-ui .errors-wrapper .error-wrapper{margin:0 0 10px}.swagger-ui .errors-wrapper .errors h4{color:#3b4151;font-family:monospace;font-size:14px;font-weight:600;margin:0}.swagger-ui .errors-wrapper .errors small{color:#606060}.swagger-ui .errors-wrapper .errors .message{white-space:pre-line}.swagger-ui .errors-wrapper .errors .message.thrown{max-width:100%}.swagger-ui .errors-wrapper .errors .error-line{cursor:pointer;-webkit-text-decoration:underline;text-decoration:underline}.swagger-ui .errors-wrapper hgroup{align-items:center;display:flex}.swagger-ui .errors-wrapper hgroup h4{color:#3b4151;flex:1;font-family:sans-serif;font-size:20px;margin:0}@keyframes scaleUp{0%{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}.swagger-ui .Resizer.vertical.disabled{display:none}.swagger-ui .markdown p,.swagger-ui .markdown pre,.swagger-ui .renderedMarkdown p,.swagger-ui .renderedMarkdown pre{margin:1em auto;word-break:break-all;word-break:break-word}.swagger-ui .markdown pre,.swagger-ui .renderedMarkdown pre{background:none;color:#000;font-weight:400;padding:0;white-space:pre-wrap}.swagger-ui .markdown code,.swagger-ui .renderedMarkdown code{background:rgba(0,0,0,.05);border-radius:4px;color:#9012fe;font-family:monospace;font-size:14px;font-weight:600;padding:5px 7px}.swagger-ui .markdown pre>code,.swagger-ui .renderedMarkdown pre>code{display:block}.swagger-ui .json-schema-2020-12-keyword--\\$vocabulary ul{border-left:1px dashed rgba(0,0,0,.1);margin:0 0 0 20px}.swagger-ui .json-schema-2020-12-\\$vocabulary-uri{margin-left:35px}.swagger-ui .json-schema-2020-12-\\$vocabulary-uri--disabled{-webkit-text-decoration:line-through;text-decoration:line-through}.swagger-ui .json-schema-2020-12-keyword--const .json-schema-2020-12-json-viewer__name,.swagger-ui .json-schema-2020-12-keyword--const .json-schema-2020-12-json-viewer__value{color:#3b4151;font-style:normal}.swagger-ui .json-schema-2020-12__constraint{background-color:#805ad5;border-radius:4px;color:#3b4151;color:#fff;font-family:monospace;font-weight:600;line-height:1.5;margin-left:10px;padding:1px 3px}.swagger-ui .json-schema-2020-12__constraint--string{background-color:#d69e2e;color:#fff}.swagger-ui .json-schema-2020-12-keyword--default .json-schema-2020-12-json-viewer__name,.swagger-ui .json-schema-2020-12-keyword--default .json-schema-2020-12-json-viewer__value{color:#3b4151;font-style:normal}.swagger-ui .json-schema-2020-12-keyword--dependentRequired>ul{display:inline-block;margin:0;padding:0}.swagger-ui .json-schema-2020-12-keyword--dependentRequired>ul li{display:inline;list-style-type:none}.swagger-ui .json-schema-2020-12-keyword--description{color:#6b6b6b;font-size:12px;margin-left:20px}.swagger-ui .json-schema-2020-12-keyword--description p{margin:0}.swagger-ui .json-schema-2020-12-keyword--enum .json-schema-2020-12-json-viewer__name,.swagger-ui .json-schema-2020-12-keyword--enum .json-schema-2020-12-json-viewer__value,.swagger-ui .json-schema-2020-12-keyword--examples .json-schema-2020-12-json-viewer__name,.swagger-ui .json-schema-2020-12-keyword--examples .json-schema-2020-12-json-viewer__value{color:#3b4151;font-style:normal}.swagger-ui .json-schema-2020-12-json-viewer-extension-keyword .json-schema-2020-12-json-viewer__name,.swagger-ui .json-schema-2020-12-json-viewer-extension-keyword .json-schema-2020-12-json-viewer__value{color:#929292;font-style:italic}.swagger-ui .json-schema-2020-12-keyword--patternProperties ul{border:none;margin:0;padding:0}.swagger-ui .json-schema-2020-12-keyword--patternProperties .json-schema-2020-12__title:first-of-type:after,.swagger-ui .json-schema-2020-12-keyword--patternProperties .json-schema-2020-12__title:first-of-type:before{color:#55a;content:\"/\"}.swagger-ui .json-schema-2020-12-keyword--properties>ul{border:none;margin:0;padding:0}.swagger-ui .json-schema-2020-12-property{list-style-type:none}.swagger-ui .json-schema-2020-12-property--required>.json-schema-2020-12:first-of-type>.json-schema-2020-12-head .json-schema-2020-12__title:after{color:red;content:\"*\";font-weight:700}.swagger-ui .json-schema-2020-12__title{color:#505050;display:inline-block;font-family:sans-serif;font-size:12px;font-weight:700;line-height:normal}.swagger-ui .json-schema-2020-12__title .json-schema-2020-12-keyword__name{margin:0}.swagger-ui .json-schema-2020-12-property{margin:7px 0}.swagger-ui .json-schema-2020-12-property .json-schema-2020-12__title{color:#3b4151;font-family:monospace;font-size:12px;font-weight:600;vertical-align:middle}.swagger-ui .json-schema-2020-12-keyword{margin:5px 0}.swagger-ui .json-schema-2020-12-keyword__children{border-left:1px dashed rgba(0,0,0,.1);margin:0 0 0 20px;padding:0}.swagger-ui .json-schema-2020-12-keyword__children--collapsed{display:none}.swagger-ui .json-schema-2020-12-keyword__name{font-size:12px;font-weight:700;margin-left:20px}.swagger-ui .json-schema-2020-12-keyword__name--primary{color:#3b4151;font-style:normal}.swagger-ui .json-schema-2020-12-keyword__name--secondary{color:#6b6b6b;font-style:italic}.swagger-ui .json-schema-2020-12-keyword__name--extension{color:#929292;font-style:italic}.swagger-ui .json-schema-2020-12-keyword__value{color:#6b6b6b;font-size:12px;font-style:italic;font-weight:400}.swagger-ui .json-schema-2020-12-keyword__value--primary{color:#3b4151;font-style:normal}.swagger-ui .json-schema-2020-12-keyword__value--secondary{color:#6b6b6b;font-style:italic}.swagger-ui .json-schema-2020-12-keyword__value--extension{color:#929292;font-style:italic}.swagger-ui .json-schema-2020-12-keyword__value--warning{border:1px dashed red;border-radius:4px;color:#3b4151;color:red;display:inline-block;font-family:monospace;font-style:normal;font-weight:600;line-height:1.5;margin-left:10px;padding:1px 4px}.swagger-ui .json-schema-2020-12-keyword__name--secondary+.json-schema-2020-12-keyword__value--secondary:before{content:\"=\"}.swagger-ui .json-schema-2020-12__attribute{color:#3b4151;font-family:monospace;font-size:12px;padding-left:10px;text-transform:lowercase}.swagger-ui .json-schema-2020-12__attribute--primary{color:#55a}.swagger-ui .json-schema-2020-12__attribute--muted{color:gray}.swagger-ui .json-schema-2020-12__attribute--warning{color:red}.swagger-ui .json-schema-2020-12-json-viewer{margin:5px 0}.swagger-ui .json-schema-2020-12-json-viewer__children{border-left:1px dashed rgba(0,0,0,.1);margin:0 0 0 20px;padding:0}.swagger-ui .json-schema-2020-12-json-viewer__children--collapsed{display:none}.swagger-ui .json-schema-2020-12-json-viewer__name{font-size:12px;font-weight:700;margin-left:20px}.swagger-ui .json-schema-2020-12-json-viewer__name--primary{color:#3b4151;font-style:normal}.swagger-ui .json-schema-2020-12-json-viewer__name--secondary{color:#6b6b6b;font-style:italic}.swagger-ui .json-schema-2020-12-json-viewer__name--extension{color:#929292;font-style:italic}.swagger-ui .json-schema-2020-12-json-viewer__value{color:#6b6b6b;font-size:12px;font-style:italic;font-weight:400}.swagger-ui .json-schema-2020-12-json-viewer__value--primary{color:#3b4151;font-style:normal}.swagger-ui .json-schema-2020-12-json-viewer__value--secondary{color:#6b6b6b;font-style:italic}.swagger-ui .json-schema-2020-12-json-viewer__value--extension{color:#929292;font-style:italic}.swagger-ui .json-schema-2020-12-json-viewer__value--warning{border:1px dashed red;border-radius:4px;color:#3b4151;color:red;display:inline-block;font-family:monospace;font-style:normal;font-weight:600;line-height:1.5;margin-left:10px;padding:1px 4px}.swagger-ui .json-schema-2020-12-json-viewer__name--secondary+.json-schema-2020-12-json-viewer__value--secondary:before{content:\"=\"}.swagger-ui .json-schema-2020-12{background-color:rgba(0,0,0,.05);border-radius:4px;margin:0 20px 15px;padding:12px 0 12px 20px}.swagger-ui .json-schema-2020-12:first-of-type{margin:20px}.swagger-ui .json-schema-2020-12:last-of-type{margin:0 20px}.swagger-ui .json-schema-2020-12--embedded{background-color:inherit;padding-bottom:0;padding-left:inherit;padding-right:inherit;padding-top:0}.swagger-ui .json-schema-2020-12-body{border-left:1px dashed rgba(0,0,0,.1);margin:2px 0}.swagger-ui .json-schema-2020-12-body--collapsed{display:none}.swagger-ui .json-schema-2020-12-accordion{border:none;outline:none;padding-left:0}.swagger-ui .json-schema-2020-12-accordion__children{display:inline-block}.swagger-ui .json-schema-2020-12-accordion__icon{display:inline-block;height:18px;vertical-align:bottom;width:18px}.swagger-ui .json-schema-2020-12-accordion__icon--expanded{transform:rotate(-90deg);transform-origin:50% 50%;transition:transform .15s ease-in}.swagger-ui .json-schema-2020-12-accordion__icon--collapsed{transform:rotate(0deg);transform-origin:50% 50%;transition:transform .15s ease-in}.swagger-ui .json-schema-2020-12-accordion__icon svg{height:20px;width:20px}.swagger-ui .json-schema-2020-12-expand-deep-button{border:none;color:#505050;color:#afaeae;font-family:sans-serif;font-size:12px;padding-right:0}.swagger-ui .model-box .json-schema-2020-12:not(.json-schema-2020-12--embedded)>.json-schema-2020-12-head .json-schema-2020-12__title:first-of-type{font-size:16px}.swagger-ui .model-box>.json-schema-2020-12{margin:0}.swagger-ui .model-box .json-schema-2020-12{background-color:transparent;padding:0}.swagger-ui .model-box .json-schema-2020-12-accordion,.swagger-ui .model-box .json-schema-2020-12-expand-deep-button{background-color:transparent}.swagger-ui .models .json-schema-2020-12:not(.json-schema-2020-12--embedded)>.json-schema-2020-12-head .json-schema-2020-12__title:first-of-type{font-size:16px}.swagger-ui .models .json-schema-2020-12:not(.json-schema-2020-12--embedded){overflow-x:auto;width:calc(100% - 40px)}html.dark-mode{background:#1c2022}html.dark-mode .swagger-ui{background:#1c2022;color:#e4e6e6}html.dark-mode .swagger-ui .authorization__btn svg,html.dark-mode .swagger-ui .expand-operation svg,html.dark-mode .swagger-ui .opblock-control-arrow svg{fill:#b7bcbf;opacity:1}html.dark-mode .swagger-ui .markdown p,html.dark-mode .swagger-ui .markdown pre,html.dark-mode .swagger-ui .renderedMarkdown p,html.dark-mode .swagger-ui .renderedMarkdown pre,html.dark-mode .swagger-ui section h3,html.dark-mode .swagger-ui table thead tr td,html.dark-mode .swagger-ui table thead tr th{color:#e4e6e6}html.dark-mode .swagger-ui .markdown code,html.dark-mode .swagger-ui .renderedMarkdown code{background:#080a0b;color:#b68ae1}html.dark-mode .swagger-ui input{background:#1c2022;border-color:#b7bcbf;color:#f0f1f1}html.dark-mode .swagger-ui input:focus:not(.download-url-input){border-color:#51a8ff!important;box-shadow:none;outline:none}html.dark-mode .swagger-ui textarea{background:#0d1014;border:1px solid #0d1014;color:#f0f1f1}html.dark-mode .swagger-ui textarea:focus{border-color:#51a8ff}html.dark-mode .swagger-ui textarea[disabled]{background-color:#202225;border-color:#202225;color:#8c969a}html.dark-mode .swagger-ui select{background:#1c2022 url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 20 20\\\"><path fill=\\\"%23B7BCBF\\\" d=\\\"M13.418 7.859a.695.695 0 0 1 .978 0 .68.68 0 0 1 0 .969l-3.908 3.83a.697.697 0 0 1-.979 0l-3.908-3.83a.68.68 0 0 1 0-.969.695.695 0 0 1 .978 0L10 11z\\\"/></svg>\") right 10px center no-repeat;border-color:#b7bcbf;box-shadow:none;color:#f0f1f1;outline:none}html.dark-mode .swagger-ui select[multiple]{background:#1c2022}html.dark-mode .swagger-ui select:focus{border-color:#51a8ff}html.dark-mode .swagger-ui input::-moz-placeholder, html.dark-mode .swagger-ui textarea::-moz-placeholder{color:#f0f1f1;opacity:.5}html.dark-mode .swagger-ui input::placeholder,html.dark-mode .swagger-ui textarea::placeholder{color:#f0f1f1;opacity:.5}html.dark-mode .swagger-ui input.invalid,html.dark-mode .swagger-ui select.invalid,html.dark-mode .swagger-ui textarea.invalid{background:#1c2022;border-color:#ff5f5f}html.dark-mode .swagger-ui .topbar{background:#2a2e30}html.dark-mode .swagger-ui .topbar .download-url-wrapper .download-url-button{background:#1d632e;color:#e4e6e6}html.dark-mode .swagger-ui .topbar .download-url-wrapper .download-url-input{border-color:#1d632e}html.dark-mode .swagger-ui .topbar .download-url-wrapper .download-url-input.failed{color:#ff5f5f}html.dark-mode .swagger-ui .dialog-ux .modal-ux{background-color:#2a2e30;border:none;color:#e4e6e6}html.dark-mode .swagger-ui .dialog-ux .modal-ux-header{border-color:#545d61}html.dark-mode .swagger-ui .dialog-ux .modal-ux-header .close-modal svg{fill:#e4e6e6}html.dark-mode .swagger-ui .dialog-ux .modal-ux h2,html.dark-mode .swagger-ui .dialog-ux .modal-ux h3,html.dark-mode .swagger-ui .dialog-ux .modal-ux h4,html.dark-mode .swagger-ui .dialog-ux .modal-ux h5,html.dark-mode .swagger-ui .dialog-ux .modal-ux label,html.dark-mode .swagger-ui .dialog-ux .modal-ux p{color:#e4e6e6}html.dark-mode .swagger-ui .dialog-ux .modal-ux .scopes a{color:#51a8ff}html.dark-mode .swagger-ui .dialog-ux .modal-ux .btn.modal-btn{border-color:#3ece90;color:#3ece90}html.dark-mode .swagger-ui .dialog-ux .modal-ux .btn.modal-btn.btn-done{border-color:#e4e6e6;color:#e4e6e6}html.dark-mode .swagger-ui .dialog-ux .modal-ux .auth-container{border-color:#545d61}html.dark-mode .swagger-ui .dialog-ux .modal-ux .checkbox input[type=checkbox]+label>.item{background:#545d61;box-shadow:none;color:#f0f1f1!important}html.dark-mode .swagger-ui .dialog-ux .modal-ux .checkbox input[type=checkbox]:checked+label>.item{background:#545d61 url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"10\\\" height=\\\"8\\\" viewBox=\\\"3 7 10 8\\\"><path fill=\\\"%23E4E6E6\\\" fill-rule=\\\"evenodd\\\" d=\\\"M6.333 15 3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z\\\"/></svg>\") 50% no-repeat}html.dark-mode .swagger-ui .loading-container .loading:before{border-color:#e4e6e6 #545d61 #545d61}html.dark-mode .swagger-ui .loading-container .loading:after{color:#e4e6e6}html.dark-mode .swagger-ui .scheme-container{background:#1c2022;box-shadow:0 1px 2px 0 #545d61}html.dark-mode .swagger-ui .scheme-container .schemes>.schemes-server-container>label{color:#e4e6e6}html.dark-mode .swagger-ui .scheme-container .btn.authorize{border-color:#3ece90;color:#3ece90}html.dark-mode .swagger-ui .scheme-container .btn.authorize svg{fill:#3ece90}html.dark-mode .swagger-ui .info .title,html.dark-mode .swagger-ui .info h1,html.dark-mode .swagger-ui .info h2,html.dark-mode .swagger-ui .info h3,html.dark-mode .swagger-ui .info h4,html.dark-mode .swagger-ui .info h5{color:#d2d6d7}html.dark-mode .swagger-ui .info .base-url,html.dark-mode .swagger-ui .info li,html.dark-mode .swagger-ui .info p,html.dark-mode .swagger-ui .info table{color:#e4e6e6}html.dark-mode .swagger-ui .info a{color:#51a8ff}html.dark-mode .swagger-ui .info .title small{background:#434b4f}html.dark-mode .swagger-ui .info .title small.version-stamp{background:#1d632e}html.dark-mode .swagger-ui .info .errors-wrapper{background:#434b4f;border-color:#ff5f5f}html.dark-mode .swagger-ui .info .errors-wrapper h4,html.dark-mode .swagger-ui .info .errors-wrapper span{color:#e4e6e6}html.dark-mode .swagger-ui .info .errors-wrapper .btn.errors__clear-btn{border-color:#e4e6e6;color:#e4e6e6}html.dark-mode .swagger-ui .copy-to-clipboard,html.dark-mode .swagger-ui .download-contents{background:#545d61;color:#e4e6e6}html.dark-mode .swagger-ui .copy-to-clipboard button,html.dark-mode .swagger-ui .download-contents button{background:url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"16\\\" height=\\\"15\\\" aria-hidden=\\\"true\\\"><path fill=\\\"%23E4E6E6\\\" fill-rule=\\\"evenodd\\\" d=\\\"M4 12h4v1H4zm5-6H4v1h5zm2 3V7l-3 3 3 3v-2h5V9zM6.5 8H4v1h2.5zM4 11h2.5v-1H4zm9 1h1v2c-.02.28-.11.52-.3.7s-.42.28-.7.3H3c-.55 0-1-.45-1-1V3c0-.55.45-1 1-1h3c0-1.11.89-2 2-2s2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V5H3v9h10zM4 4h8c0-.55-.45-1-1-1h-1c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H5c-.55 0-1 .45-1 1\\\"/></svg>\") 50% no-repeat}html.dark-mode .swagger-ui .opblock-tag{border-bottom-color:#545d61;color:#e4e6e6}html.dark-mode .swagger-ui .opblock-tag small{color:#e4e6e6}html.dark-mode .swagger-ui .opblock-tag a.link{color:#51a8ff}html.dark-mode .swagger-ui .opblock.opblock-post{background:#112929;border-color:#104834}html.dark-mode .swagger-ui .opblock.opblock-post thead tr td,html.dark-mode .swagger-ui .opblock.opblock-post thead tr th{border-color:#104834;opacity:1}html.dark-mode .swagger-ui .opblock.opblock-post .opblock-section-header{background:#14392c;border-bottom:1px solid #104834;border-top:1px solid #104834}html.dark-mode .swagger-ui .opblock.opblock-post .opblock-section-header .tab-header .tab-item .opblock-title span:after{background:#00b572}html.dark-mode .swagger-ui .opblock.opblock-post .opblock-summary{border-bottom:none;border-color:#104834}html.dark-mode .swagger-ui .opblock.opblock-post .opblock-summary-control:focus{outline:none}html.dark-mode .swagger-ui .opblock.opblock-post .opblock-summary-method{background:#00b572;color:#080a0b;text-shadow:none}html.dark-mode .swagger-ui .opblock.opblock-post .opblock-body>.opblock-description-wrapper,html.dark-mode .swagger-ui .opblock.opblock-post .opblock-body>.opblock-title_normal{border-top:1px solid #104834}html.dark-mode .swagger-ui .opblock.opblock-deprecated{background:#272c34;border-color:#495361}html.dark-mode .swagger-ui .opblock.opblock-deprecated thead tr td,html.dark-mode .swagger-ui .opblock.opblock-deprecated thead tr th{border-color:#495361;opacity:1}html.dark-mode .swagger-ui .opblock.opblock-deprecated .opblock-section-header{background:#262e36;border-bottom:1px solid #495361;border-top:1px solid #495361}html.dark-mode .swagger-ui .opblock.opblock-deprecated .opblock-section-header .tab-header .tab-item .opblock-title span:after{background:#6a6a6a}html.dark-mode .swagger-ui .opblock.opblock-deprecated .opblock-summary{border-bottom:none;border-color:#495361}html.dark-mode .swagger-ui .opblock.opblock-deprecated .opblock-summary-control:focus{outline:none}html.dark-mode .swagger-ui .opblock.opblock-deprecated .opblock-summary-method{background:#6a6a6a;color:#080a0b;text-shadow:none}html.dark-mode .swagger-ui .opblock.opblock-deprecated .opblock-body>.opblock-description-wrapper,html.dark-mode .swagger-ui .opblock.opblock-deprecated .opblock-body>.opblock-title_normal{border-top:1px solid #495361}html.dark-mode .swagger-ui .opblock.opblock-put{background:#27201e;border-color:#523524}html.dark-mode .swagger-ui .opblock.opblock-put thead tr td,html.dark-mode .swagger-ui .opblock.opblock-put thead tr th{border-color:#523524;opacity:1}html.dark-mode .swagger-ui .opblock.opblock-put .opblock-section-header{background:#9a5b3e;border-bottom:1px solid #523524;border-top:1px solid #523524}html.dark-mode .swagger-ui .opblock.opblock-put .opblock-section-header .tab-header .tab-item .opblock-title span:after{background:#ff7d35}html.dark-mode .swagger-ui .opblock.opblock-put .opblock-summary{border-bottom:none;border-color:#523524}html.dark-mode .swagger-ui .opblock.opblock-put .opblock-summary-control:focus{outline:none}html.dark-mode .swagger-ui .opblock.opblock-put .opblock-summary-method{background:#ff7d35;color:#080a0b;text-shadow:none}html.dark-mode .swagger-ui .opblock.opblock-put .opblock-body>.opblock-description-wrapper,html.dark-mode .swagger-ui .opblock.opblock-put .opblock-body>.opblock-title_normal{border-top:1px solid #523524}html.dark-mode .swagger-ui .opblock.opblock-get{background:#182536;border-color:#294262}html.dark-mode .swagger-ui .opblock.opblock-get thead tr td,html.dark-mode .swagger-ui .opblock.opblock-get thead tr th{border-color:#294262;opacity:1}html.dark-mode .swagger-ui .opblock.opblock-get .opblock-section-header{background:#1c3043;border-bottom:1px solid #294262;border-top:1px solid #294262}html.dark-mode .swagger-ui .opblock.opblock-get .opblock-section-header .tab-header .tab-item .opblock-title span:after{background:#55a1ff}html.dark-mode .swagger-ui .opblock.opblock-get .opblock-summary{border-bottom:none;border-color:#294262}html.dark-mode .swagger-ui .opblock.opblock-get .opblock-summary-control:focus{outline:none}html.dark-mode .swagger-ui .opblock.opblock-get .opblock-summary-method{background:#55a1ff;color:#080a0b;text-shadow:none}html.dark-mode .swagger-ui .opblock.opblock-get .opblock-body>.opblock-description-wrapper,html.dark-mode .swagger-ui .opblock.opblock-get .opblock-body>.opblock-title_normal{border-top:1px solid #294262}html.dark-mode .swagger-ui .opblock.opblock-delete{background:#241a20;border-color:#4b2420}html.dark-mode .swagger-ui .opblock.opblock-delete thead tr td,html.dark-mode .swagger-ui .opblock.opblock-delete thead tr th{border-color:#4b2420;opacity:1}html.dark-mode .swagger-ui .opblock.opblock-delete .opblock-section-header{background:#2f2020;border-bottom:1px solid #4b2420;border-top:1px solid #4b2420}html.dark-mode .swagger-ui .opblock.opblock-delete .opblock-section-header .tab-header .tab-item .opblock-title span:after{background:#eb6156}html.dark-mode .swagger-ui .opblock.opblock-delete .opblock-summary{border-bottom:none;border-color:#4b2420}html.dark-mode .swagger-ui .opblock.opblock-delete .opblock-summary-control:focus{outline:none}html.dark-mode .swagger-ui .opblock.opblock-delete .opblock-summary-method{background:#eb6156;color:#080a0b;text-shadow:none}html.dark-mode .swagger-ui .opblock.opblock-delete .opblock-body>.opblock-description-wrapper,html.dark-mode .swagger-ui .opblock.opblock-delete .opblock-body>.opblock-title_normal{border-top:1px solid #4b2420}html.dark-mode .swagger-ui .opblock.opblock-patch{background:#11282f;border-color:#16494b}html.dark-mode .swagger-ui .opblock.opblock-patch thead tr td,html.dark-mode .swagger-ui .opblock.opblock-patch thead tr th{border-color:#16494b;opacity:1}html.dark-mode .swagger-ui .opblock.opblock-patch .opblock-section-header{background:#113239;border-bottom:1px solid #16494b;border-top:1px solid #16494b}html.dark-mode .swagger-ui .opblock.opblock-patch .opblock-section-header .tab-header .tab-item .opblock-title span:after{background:#03b7bf}html.dark-mode .swagger-ui .opblock.opblock-patch .opblock-summary{border-bottom:none;border-color:#16494b}html.dark-mode .swagger-ui .opblock.opblock-patch .opblock-summary-control:focus{outline:none}html.dark-mode .swagger-ui .opblock.opblock-patch .opblock-summary-method{background:#03b7bf;color:#080a0b;text-shadow:none}html.dark-mode .swagger-ui .opblock.opblock-patch .opblock-body>.opblock-description-wrapper,html.dark-mode .swagger-ui .opblock.opblock-patch .opblock-body>.opblock-title_normal{border-top:1px solid #16494b}html.dark-mode .swagger-ui .opblock.opblock-head{background:#282231;border-color:#44336a}html.dark-mode .swagger-ui .opblock.opblock-head thead tr td,html.dark-mode .swagger-ui .opblock.opblock-head thead tr th{border-color:#44336a;opacity:1}html.dark-mode .swagger-ui .opblock.opblock-head .opblock-section-header{background:#352c45;border-bottom:1px solid #44336a;border-top:1px solid #44336a}html.dark-mode .swagger-ui .opblock.opblock-head .opblock-section-header .tab-header .tab-item .opblock-title span:after{background:#b889ff}html.dark-mode .swagger-ui .opblock.opblock-head .opblock-summary{border-bottom:none;border-color:#44336a}html.dark-mode .swagger-ui .opblock.opblock-head .opblock-summary-control:focus{outline:none}html.dark-mode .swagger-ui .opblock.opblock-head .opblock-summary-method{background:#b889ff;color:#080a0b;text-shadow:none}html.dark-mode .swagger-ui .opblock.opblock-head .opblock-body>.opblock-description-wrapper,html.dark-mode .swagger-ui .opblock.opblock-head .opblock-body>.opblock-title_normal{border-top:1px solid #44336a}html.dark-mode .swagger-ui .opblock.opblock-options{background:#202c3c;border-color:#33465e}html.dark-mode .swagger-ui .opblock.opblock-options thead tr td,html.dark-mode .swagger-ui .opblock.opblock-options thead tr th{border-color:#33465e;opacity:1}html.dark-mode .swagger-ui .opblock.opblock-options .opblock-section-header{background:#314558;border-bottom:1px solid #33465e;border-top:1px solid #33465e}html.dark-mode .swagger-ui .opblock.opblock-options .opblock-section-header .tab-header .tab-item .opblock-title span:after{background:#6895c8}html.dark-mode .swagger-ui .opblock.opblock-options .opblock-summary{border-bottom:none;border-color:#33465e}html.dark-mode .swagger-ui .opblock.opblock-options .opblock-summary-control:focus{outline:none}html.dark-mode .swagger-ui .opblock.opblock-options .opblock-summary-method{background:#6895c8;color:#080a0b;text-shadow:none}html.dark-mode .swagger-ui .opblock.opblock-options .opblock-body>.opblock-description-wrapper,html.dark-mode .swagger-ui .opblock.opblock-options .opblock-body>.opblock-title_normal{border-top:1px solid #33465e}html.dark-mode .swagger-ui .opblock .opblock-section-header{box-shadow:none}html.dark-mode .swagger-ui .opblock .opblock-section-header h4,html.dark-mode .swagger-ui .opblock .opblock-section-header label{color:#e4e6e6}html.dark-mode .swagger-ui .opblock .opblock-section-header .try-out__btn{border-color:#b7bcbf;box-shadow:none;color:#e4e6e6}html.dark-mode .swagger-ui .opblock .opblock-section-header .try-out__btn.cancel{border-color:#ff5f5f;color:#ff5f5f}html.dark-mode .swagger-ui .opblock .btn.json-schema-form-item-add,html.dark-mode .swagger-ui .opblock .btn.json-schema-form-item-remove{border-color:#e4e6e6;color:#e4e6e6}html.dark-mode .swagger-ui .opblock .validation-errors.errors-wrapper{background:#434b4f;border-color:#ff5f5f;color:#e4e6e6}html.dark-mode .swagger-ui .opblock .body-param-options label span,html.dark-mode .swagger-ui .opblock .opblock-description-wrapper i,html.dark-mode .swagger-ui .opblock .opblock-description-wrapper p,html.dark-mode .swagger-ui .opblock .opblock-external-docs-wrapper,html.dark-mode .swagger-ui .opblock .opblock-summary-description,html.dark-mode .swagger-ui .opblock .opblock-summary-operation-id,html.dark-mode .swagger-ui .opblock .opblock-summary-path,html.dark-mode .swagger-ui .opblock .opblock-summary-path__deprecated,html.dark-mode .swagger-ui .opblock .opblock-title_normal,html.dark-mode .swagger-ui .opblock .parameter__in,html.dark-mode .swagger-ui .opblock .parameter__name,html.dark-mode .swagger-ui .opblock .parameter__type,html.dark-mode .swagger-ui .opblock .parameter__type .prop-format,html.dark-mode .swagger-ui .opblock .response-col_links,html.dark-mode .swagger-ui .opblock .response-col_status,html.dark-mode .swagger-ui .opblock .response-col_undocumented{color:#e4e6e6}html.dark-mode .swagger-ui .opblock .opblock-external-docs a.link{color:#51a8ff}html.dark-mode .swagger-ui .opblock .parameter__name.required span,html.dark-mode .swagger-ui .opblock .parameter__name.required:after{color:#ff5f5f}html.dark-mode .swagger-ui .opblock .parameter__empty_value_toggle{color:#e4e6e6}html.dark-mode .swagger-ui .opblock .btn.execute{background:#51a8ff;border-color:#51a8ff;color:#080a0b}html.dark-mode .swagger-ui .opblock .btn.btn-clear{border-color:#e4e6e6;color:#e4e6e6}html.dark-mode .swagger-ui .opblock .highlight-code pre.microlight{background:#2a2e30!important;color:#f0f1f1}html.dark-mode .swagger-ui .opblock .curl-command .btn{background:#3b424d!important;border-color:#2a2e30!important;color:#ebebeb!important}html.dark-mode .swagger-ui .opblock .curl-command .btn.active{background:#2a2e30!important;color:#e4e6e6!important}html.dark-mode .swagger-ui .opblock pre.microlight{background:#2a2e30!important;color:#f0f1f1}html.dark-mode .swagger-ui .opblock .model-example .tab button{color:#e4e6e6}html.dark-mode .swagger-ui .opblock .model-example .tabitem:after{background:#6b757a}html.dark-mode .swagger-ui .opblock .responses-inner h4,html.dark-mode .swagger-ui .opblock .responses-inner h5{color:#e4e6e6}html.dark-mode .swagger-ui .opblock .response-control-media-type--accept-controller select.content-type{border-color:#4ac966}html.dark-mode .swagger-ui .opblock .response-control-media-type--accept-controller .response-control-media-type__accept-message{color:#4ac966}html.dark-mode .swagger-ui .model-toggle:after{background:url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"24\\\" height=\\\"24\\\" viewBox=\\\"0 0 24 24\\\"><path fill=\\\"%23e4e6e6\\\" d=\\\"M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\\\"/></svg>\") 50% no-repeat;background-size:100%}html.dark-mode .swagger-ui .model .prop-type{color:#b68ae1}html.dark-mode .swagger-ui .model .brace-close,html.dark-mode .swagger-ui .model .brace-open,html.dark-mode .swagger-ui .model .description,html.dark-mode .swagger-ui .model .prop-format,html.dark-mode .swagger-ui .model .property,html.dark-mode .swagger-ui .model .property-row{color:#e4e6e6}html.dark-mode .swagger-ui .model .property-row.required .star{color:#ff5f5f}html.dark-mode .swagger-ui .model-box{background:#2a2e30}html.dark-mode .swagger-ui .model-box .model,html.dark-mode .swagger-ui .model-box .model-title{color:#e4e6e6}html.dark-mode .swagger-ui .model-box-control:focus{outline:none}html.dark-mode .swagger-ui .model-box-control:not(.prop){color:#e4e6e6}html.dark-mode .swagger-ui .json-schema-2020-12,html.dark-mode .swagger-ui .json-schema-2020-12 button{background:#2a2e30}html.dark-mode .swagger-ui .json-schema-2020-12 button svg{fill:#e4e6e6}html.dark-mode .swagger-ui .json-schema-2020-12 a{color:#51a8ff}html.dark-mode .swagger-ui .json-schema-2020-12__title{color:#e4e6e6}html.dark-mode .swagger-ui .json-schema-2020-12-property--required>.json-schema-2020-12:first-of-type>.json-schema-2020-12-head .json-schema-2020-12__title:after{color:#ff5f5f}html.dark-mode .swagger-ui .json-schema-2020-12-expand-deep-button{color:#b7bcbf}html.dark-mode .swagger-ui .json-schema-2020-12-body{border-color:#b7bcbf}html.dark-mode .swagger-ui .json-schema-2020-12-keyword__name--primary{color:#e4e6e6}html.dark-mode .swagger-ui .json-schema-2020-12-keyword__name--secondary,html.dark-mode .swagger-ui .json-schema-2020-12-keyword__value--secondary{color:#b7bcbf}html.dark-mode .swagger-ui .json-schema-2020-12-keyword__value--warning{border-color:#ff5f5f;color:#ff5f5f}html.dark-mode .swagger-ui .json-schema-2020-12-keyword--\\$vocabulary ul{border-color:#b7bcbf}html.dark-mode .swagger-ui .json-schema-2020-12-keyword--patternProperties .json-schema-2020-12__title:after,html.dark-mode .swagger-ui .json-schema-2020-12-keyword--patternProperties .json-schema-2020-12__title:before,html.dark-mode .swagger-ui .json-schema-2020-12__attribute--primary{color:#9898ff}html.dark-mode .swagger-ui .json-schema-2020-12__attribute--muted{color:#b7bcbf}html.dark-mode .swagger-ui .json-schema-2020-12__attribute--warning{color:#ff5f5f}html.dark-mode .swagger-ui .json-schema-2020-12-json-viewer__name--secondary,html.dark-mode .swagger-ui .json-schema-2020-12-json-viewer__value--secondary{color:#b7bcbf}html.dark-mode .swagger-ui .json-schema-2020-12__constraint{background:#9898ff;color:#080a0b}html.dark-mode .swagger-ui .json-schema-2020-12__constraint--string{background:#d4aa53}html.dark-mode .swagger-ui section.models,html.dark-mode .swagger-ui section.models h4{border-color:#545d61}html.dark-mode .swagger-ui section.models h4 span{color:#e4e6e6}html.dark-mode .swagger-ui section.models .model-container{background:#2a2e30}html.dark-mode .swagger-ui section.models .models-control:focus{outline:none}html.dark-mode .swagger-ui section.models .models-control svg{fill:#b7bcbf}\n\n/*# sourceMappingURL=swagger-ui.css.map*/"
  },
  {
    "path": "pkgs/build.sh",
    "content": "#!/bin/bash\n\nNFPM_VERSION=2.45.1\nNFPM_ARCH=${NFPM_ARCH:-amd64}\nif [ -z ${SFTPGO_VERSION} ]\nthen\n  LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))\n  NUM_COMMITS_FROM_TAG=$(git rev-list ${LATEST_TAG}.. --count)\n  VERSION=$(echo \"${LATEST_TAG}\" | awk -F. -v OFS=. '{$NF++;print}')-dev.${NUM_COMMITS_FROM_TAG}\nelse\n  VERSION=${SFTPGO_VERSION}\nfi\n\nmkdir dist\necho -n ${VERSION} > dist/version\ncd dist\nBASE_DIR=\"../..\"\n\nif [ -f \"${BASE_DIR}/output/bash_completion/sftpgo\" ]\nthen\n  cp ${BASE_DIR}/output/bash_completion/sftpgo sftpgo-completion.bash\nelse\n  $BASE_DIR/sftpgo gen completion bash > sftpgo-completion.bash\nfi\n\nif [ -d \"${BASE_DIR}/output/man/man1\" ]\nthen\n  cp -r ${BASE_DIR}/output/man/man1 .\nelse\n  $BASE_DIR/sftpgo gen man -d man1\nfi\n\nif [ ! -f ${BASE_DIR}/sftpgo ]\nthen\n  cp ${BASE_DIR}/output/sftpgo ${BASE_DIR}/sftpgo\n  chmod 755 ${BASE_DIR}/sftpgo\nfi\n\ncp ${BASE_DIR}/sftpgo.json .\nsed -i \"s|sftpgo.db|/var/lib/sftpgo/sftpgo.db|\" sftpgo.json\nsed -i \"s|\\\"users_base_dir\\\": \\\"\\\",|\\\"users_base_dir\\\": \\\"/srv/sftpgo/data\\\",|\" sftpgo.json\nsed -i \"s|\\\"backups\\\"|\\\"/srv/sftpgo/backups\\\"|\" sftpgo.json\nsed -i \"s|\\\"certs\\\"|\\\"/var/lib/sftpgo/certs\\\"|\" sftpgo.json\n\ncat >nfpm.yaml <<EOF\nname: \"sftpgo\"\narch: \"${NFPM_ARCH}\"\nplatform: \"linux\"\nversion: ${VERSION}\nrelease: 1\nsection: \"net\"\npriority: \"optional\"\nmaintainer: \"Nicola Murino <nicola.murino@gmail.com>\"\ndescription: |\n  Full-featured and highly configurable SFTP server\n  SFTPGo has optional HTTP, FTP/S and WebDAV support.\n  It can serve local filesystem, S3 (Compatible) Object Storage,\n  Google Cloud Storage, Azure Blob Storage, SFTP.\nvendor: \"SFTPGo\"\nhomepage: \"https://github.com/drakkan/sftpgo\"\nlicense: \"AGPL-3.0\"\nprovides:\n  - sftpgo\ncontents:\n  - src: \"${BASE_DIR}/sftpgo${BIN_SUFFIX}\"\n    dst: \"/usr/bin/sftpgo\"\n\n  - src: \"./sftpgo-completion.bash\"\n    dst: \"/usr/share/bash-completion/completions/sftpgo\"\n\n  - src: \"./man1/*\"\n    dst: \"/usr/share/man/man1/\"\n\n  - src: \"${BASE_DIR}/init/sftpgo.service\"\n    dst: \"/lib/systemd/system/sftpgo.service\"\n\n  - src: \"${BASE_DIR}/templates/*\"\n    dst: \"/usr/share/sftpgo/templates\"\n\n  - src: \"${BASE_DIR}/static/*\"\n    dst: \"/usr/share/sftpgo/static\"\n\n  - src: \"${BASE_DIR}/openapi/*\"\n    dst: \"/usr/share/sftpgo/openapi\"\n\n  - src: \"${BASE_DIR}/LICENSE\"\n    dst: \"/usr/share/licenses/sftpgo/LICENSE\"\n\n  - src: \"${BASE_DIR}/NOTICE\"\n    dst: \"/usr/share/licenses/sftpgo/NOTICE\"\n\n  - src: \"./sftpgo.json\"\n    dst: \"/etc/sftpgo/sftpgo.json\"\n    type: \"config|noreplace\"\n\n  - dst: \"/srv/sftpgo\"\n    type: dir\n\n  - dst: \"/var/lib/sftpgo\"\n    type: dir\n\n  - dst: \"/etc/sftpgo/env.d\"\n    type: dir\n\noverrides:\n  deb:\n    recommends:\n      - bash-completion\n      - mime-support\n    scripts:\n      postinstall: ../scripts/deb/postinstall.sh\n      preremove: ../scripts/deb/preremove.sh\n      postremove: ../scripts/deb/postremove.sh\n  rpm:\n    recommends:\n      - bash-completion\n      - mailcap\n    scripts:\n      postinstall: ../scripts/rpm/postinstall\n      preremove: ../scripts/rpm/preremove\n      postremove: ../scripts/rpm/postremove\n\nrpm:\n  compression: xz\n\ndeb:\n  compression: xz\n\nEOF\n\ncurl --retry 5 --retry-delay 2 --connect-timeout 10 -L -O \\\n  https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz\ntar xvf nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz nfpm\nchmod 755 nfpm\nmkdir rpm\n./nfpm -f nfpm.yaml pkg -p rpm -t rpm\nmkdir deb\n./nfpm -f nfpm.yaml pkg -p deb -t deb\n"
  },
  {
    "path": "pkgs/choco/sftpgo.nuspec",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one.-->\n<package xmlns=\"http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd\">\n\t<metadata>\n\t\t<id>sftpgo</id>\n\t\t<version>2.7.1</version>\n\t\t<packageSourceUrl>https://github.com/drakkan/sftpgo/tree/main/pkgs/choco</packageSourceUrl>\n\t\t<owners>asheroto</owners>\n\t\t<title>SFTPGo</title>\n\t\t<authors>Nicola Murino</authors>\n\t\t<projectUrl>https://sftpgo.com</projectUrl>\n\t\t<iconUrl>https://cdn.statically.io/gh/drakkan/sftpgo/v2.7.1/static/img/logo.png</iconUrl>\n\t\t<licenseUrl>https://github.com/drakkan/sftpgo/blob/main/LICENSE</licenseUrl>\n\t\t<requireLicenseAcceptance>false</requireLicenseAcceptance>\n\t\t<projectSourceUrl>https://github.com/drakkan/sftpgo</projectSourceUrl>\n\t\t<docsUrl>https://docs.sftpgo.com/2.7/</docsUrl>\n\t\t<bugTrackerUrl>https://github.com/drakkan/sftpgo/issues</bugTrackerUrl>\n\t\t<tags>sftp sftp-server ftp webdav s3 azure-blob google-cloud-storage cloud-storage scp data-at-rest-encryption multi-factor-authentication multi-step-authentication</tags>\n\t\t<summary>Full-featured and highly configurable file transfer server: SFTP, HTTP/S,FTP/S, WebDAV.</summary>\n\t\t<description>SFTPGo allows you to securely share your files over SFTP and optionally over HTTP/S, FTP/S and WebDAV as well.\nSeveral storage backends are supported and they are configurable per-user, so you can serve a local directory for a user and an S3 bucket (or part of it) for another one.\n\nSFTPGo also supports virtual folders. A virtual folder can use any of the supported storage backends. So you can have, for example, a user with the S3 backend mapping a GCS bucket (or part of it) on a specified path and an encrypted local filesystem on another one. Virtual folders can be private or shared among multiple users, for shared virtual folders you can define different quota limits for each user.\n\nSFTPGo allows to create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.\n\nSFTPGo is highly customizable and extensible to suit your needs.\n\nYou can find more info [here](https://docs.sftpgo.com/2.6/).\n\n### Notes\n\n* This package installs SFTPGo as Windows Service.\n* After the first installation please take a look at the [Getting Started Guide](https://docs.sftpgo.com/2.7/initial-configuration/).</description>\n\t\t<releaseNotes>https://github.com/drakkan/sftpgo/releases/tag/v2.7.1</releaseNotes>\n\t</metadata>\n\t<files>\n\t\t<file src=\"**\" exclude=\"**\\*.md;**\\icon.png;**\\icon.jpg;**\\icon.svg\" />\n\t</files>\n</package>"
  },
  {
    "path": "pkgs/choco/tools/ChocolateyInstall.ps1",
    "content": "﻿$ErrorActionPreference  = 'Stop'\n$packageName    = 'sftpgo'\n$softwareName   = 'SFTPGo'\n$url            = 'https://github.com/drakkan/sftpgo/releases/download/v2.7.1/sftpgo_v2.7.1_windows_x86_64.exe'\n$checksum       = '5187F2DAA58987EAE1760B6383095EF8B7E18ED1A872F2514ACB767D781D1883'\n$silentArgs     = '/VERYSILENT'\n$validExitCodes = @(0)\n\n$packageArgs = @{\n  packageName   = $packageName\n  fileType      = 'exe'\n  file          = $fileLocation\n  url           = $url\n  checksum      = $checksum\n  checksumType  = 'sha256'\n  silentArgs    = $silentArgs\n  validExitCodes= $validExitCodes\n  softwareName  = $softwareName\n}\n\nInstall-ChocolateyPackage @packageArgs\n\n$DefaultDataPath = Join-Path -Path $ENV:ProgramData -ChildPath \"SFTPGo\"\n$DefaultConfigurationFilePath = Join-Path -Path $DefaultDataPath -ChildPath \"sftpgo.json\"\n$EnvDirPath = Join-Path -Path $DefaultDataPath -ChildPath \"env.d\"\n\n# `t = tab\nWrite-Output \"---------------------------\"\nWrite-Output \"\"\nWrite-Output \"If you have never used SFTPGo before, the web administration panel is located here:\"\nWrite-Output \"`thttp://localhost:8080/web/admin\"\nWrite-Output \"\"\nWrite-Output \"Default web administration port:\"\nWrite-Output \"`t8080\"\nWrite-Output \"Default SFTP port:\"\nWrite-Output \"`t2022\"\nWrite-Output \"\"\nWrite-Output \"Default data location:\"\nWrite-Output \"`t$DefaultDataPath\"\nWrite-Output \"Default configuration file location:\"\nWrite-Output \"`t$DefaultConfigurationFilePath\"\nWrite-Output \"Directory to create environment variable files to set custom configurations:\"\nWrite-Output \"`t$EnvDirPath\"\nWrite-Output \"If the SFTPGo service does not start, make sure that TCP ports 2022 and 8080 are\"\nWrite-Output \"not used by other services or change the SFTPGo configuration to suit your needs.\"\nWrite-Output \"\"\nWrite-Output \"General information:\"\nWrite-Output \"`thttps://sftpgo.com\"\nWrite-Output \"Documentation location:\"\nWrite-Output \"`thttps://docs.sftpgo.com/latest/\"\nWrite-Output \"\"\nWrite-Output \"---------------------------\""
  },
  {
    "path": "pkgs/debian/changelog",
    "content": "sftpgo (2.7.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 13 Mar 2026 21:05:12 +0100\n\nsftpgo (2.7.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 23 Oct 2025 20:05:08 +0200\n\nsftpgo (2.6.6-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Mon, 24 Feb 2025 20:19:49 +0100\n\nsftpgo (2.6.5-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 07 Feb 2025 19:22:24 +0100\n\nsftpgo (2.6.4-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 28 Nov 2024 07:39:39 +0100\n\nsftpgo (2.6.3-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 15 Nov 2024 18:35:41 +0100\n\nsftpgo (2.6.2-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 21 Jun 2024 19:47:58 +0200\n\nsftpgo (2.6.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Wed, 19 Jun 2024 10:17:49 +0200\n\nsftpgo (2.6.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Wed, 15 May 2024 19:12:01 +0200\n\nsftpgo (2.5.6-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Mon, 18 Dec 2023 19:17:56 +0100\n\nsftpgo (2.5.5-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 10 Nov 2023 19:31:37 +0100\n\nsftpgo (2.5.4-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 15 Jul 2023 10:37:49 +0200\n\nsftpgo (2.5.3-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 29 Jun 2023 13:43:13 +0200\n\nsftpgo (2.5.2-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 17 Jun 2023 19:49:31 +0200\n\nsftpgo (2.5.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 20 May 2023 19:45:01 +0200\n\nsftpgo (2.5.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Wed, 03 May 2023 18:01:02 +0200\n\nsftpgo (2.4.5-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 08 Apr 2023 17:26:36 +0200\n\nsftpgo (2.4.4-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 04 Feb 2023 17:29:22 +0100\n\nsftpgo (2.4.3-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 07 Jan 2023 10:52:01 +0100\n\nsftpgo (2.4.2-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 25 Nov 2022 17:54:12 +0100\n\nsftpgo (2.4.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sun, 13 Nov 2022 07:34:04 +0100\n\nsftpgo (2.4.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sun, 23 Oct 2022 07:40:29 +0200\n\nsftpgo (2.3.6-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Wed, 12 Oct 2022 19:45:32 +0200\n\nsftpgo (2.3.5-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 17 Sep 2022 18:01:55 +0200\n\nsftpgo (2.3.4-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 01 Sep 2022 16:56:20 +0200\n\nsftpgo (2.3.3-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 05 Aug 2022 11:56:23 +0200\n\nsftpgo (2.3.2-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 29 Jul 2022 18:50:40 +0200\n\nsftpgo (2.3.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 10 Jun 2022 19:48:21 +0200\n\nsftpgo (2.3.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 04 Jun 2022 10:22:53 +0200\n\nsftpgo (2.2.3-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 30 Apr 2022 17:18:25 +0200\n\nsftpgo (2.2.2-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sun, 06 Feb 2022 15:48:39 +0100\n\nsftpgo (2.2.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Fri, 31 Dec 2021 15:44:15 +0100\n\nsftpgo (2.2.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 27 Nov 2021 13:41:17 +0100\n\nsftpgo (2.1.2-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 11 Sep 2021 13:35:21 +0200\n\nsftpgo (2.1.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 11 Sep 2021 07:12:40 +0200\n\nsftpgo (2.1.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Wed, 16 Jun 2021 18:20:12 +0200\n\nsftpgo (2.0.4-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 10 Apr 2021 09:03:47 +0200\n\nsftpgo (2.0.3-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sun, 28 Mar 2021 21:51:32 +0200\n\nsftpgo (2.0.2-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Wed, 17 Feb 2021 08:24:29 +0100\n\nsftpgo (2.0.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 06 Feb 2021 15:25:19 +0100\n\nsftpgo (2.0.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 06 Feb 2021 11:05:48 +0100\n\nsftpgo (1.2.2-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Wed, 18 Nov 2020 19:49:28 +0100\n\nsftpgo (1.2.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sat, 14 Nov 2020 10:19:02 +0100\n\nsftpgo (1.2.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sun, 08 Nov 2020 06:59:19 +0100\n\nsftpgo (1.1.1-1ppa2) bionic; urgency=medium\n\n  * Fix sftpgo data dir.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 18 Oct 2020 09:18:43 +0200\n\nsftpgo (1.1.1-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 17 Oct 2020 22:06:43 +0200\n\nsftpgo (1.1.0-1ppa3) bionic; urgency=medium\n\n  * Fix packaging errors\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sun, 11 Oct 2020 15:13:41 +0200\n\nsftpgo (1.1.0-1ppa2) bionic; urgency=medium\n\n  * Add mime-support to recommends\n\n -- Nicola Murino <nicola.murino@gmail.com>  Sun, 11 Oct 2020 12:18:14 +0200\n\nsftpgo (1.1.0-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 10 Oct 2020 22:09:55 +0200\n\nsftpgo (1.0.1-dev.105-1ppa1) bionic; urgency=medium\n\n  * New upstream release.\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 08 Oct 2020 16:36:55 +0200\n\nsftpgo (1.0.1-dev.102-1ppa1) bionic; urgency=medium\n\n  * Initial release\n\n -- Nicola Murino <nicola.murino@gmail.com>  Thu, 08 Oct 2020 13:15:04 +0200\n"
  },
  {
    "path": "pkgs/debian/compat",
    "content": "10\n"
  },
  {
    "path": "pkgs/debian/control",
    "content": "Source: sftpgo\nSection: net\nPriority: optional\nMaintainer: Nicola Murino <nicola.murino@gmail.com>\nBuild-Depends: debhelper (>= 10)\nStandards-Version: 4.1.2\nHomepage: https://github.com/drakkan/sftpgo\nVcs-Git: https://github.com/drakkan/sftpgo.git\n\nPackage: sftpgo\nArchitecture: any\nDepends: ${shlibs:Depends}, ${misc:Depends}\nRecommends: bash-completion, mime-support\nDescription: Full-featured and highly configurable SFTP server\n SFTPGo has optional FTP/S and WebDAV support.\n It can serve local filesystem, S3 (Compatible) Object Storage,\n Google Cloud Storage, Azure Blob Storage, SFTP.\n"
  },
  {
    "path": "pkgs/debian/copyright",
    "content": "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: SFTPGo\nSource: https://github.com/drakkan/sftpgo\n\nFiles: *\nCopyright: 2019-2025 Nicola Murino <nicola.murino@gmail.com>\nLicense: AGPL-3 with additional terms\n\nLicense: AGPL-3\n                    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\nADDITIONAL TERMS\n\nAdditional terms under GNU AGPL version 3 section 7.3(b) and 13.1:\n\nIf you have included SFTPGo so that it is offered through any network\ninteractions, including by means of an external user interface, or\nany other integration, even without modifying its source code and then\nSFTPGo is partially, fully or optionally configured via your frontend,\nyou must provide reasonable but clear attribution to the SFTPGo project\nand its author(s), not imply any endorsement by or affiliation with the\nSFTPGo project, and you must prominently offer all users interacting\nwith it remotely through a computer network an opportunity to receive\nthe Corresponding Source of the SFTPGo version you include by providing\na link to the Corresponding Source in the SFTPGo source code repository.\n"
  },
  {
    "path": "pkgs/debian/patches/config.diff",
    "content": "Index: sftpgo/sftpgo.json\n===================================================================\n--- sftpgo.orig/sftpgo.json\n+++ sftpgo/sftpgo.json\n@@ -70,7 +70,7 @@\n     \"domains\": [],\n     \"email\": \"\",\n     \"key_type\": \"4096\",\n-    \"certs_path\": \"certs\",\n+    \"certs_path\": \"/var/lib/sftpgo/certs\",\n     \"ca_endpoint\": \"https://acme-v02.api.letsencrypt.org/directory\",\n     \"renew_days\": 30,\n     \"http01_challenge\": {\n@@ -200,7 +200,7 @@\n   },\n   \"data_provider\": {\n     \"driver\": \"sqlite\",\n-    \"name\": \"sftpgo.db\",\n+    \"name\": \"/var/lib/sftpgo/sftpgo.db\",\n     \"host\": \"\",\n     \"port\": 0,\n     \"username\": \"\",\n@@ -216,7 +216,7 @@\n     \"track_quota\": 2,\n     \"delayed_quota_update\": 0,\n     \"pool_size\": 0,\n-    \"users_base_dir\": \"\",\n+    \"users_base_dir\": \"/srv/sftpgo/data\",\n     \"actions\": {\n       \"execute_on\": [],\n       \"execute_for\": [],\n@@ -258,7 +258,7 @@\n       \"port\": 0,\n       \"proto\": \"http\"\n     },\n-    \"backups_path\": \"backups\"\n+    \"backups_path\": \"/srv/sftpgo/backups\"\n   },\n   \"httpd\": {\n     \"bindings\": [\n"
  },
  {
    "path": "pkgs/debian/patches/series",
    "content": "config.diff\n"
  },
  {
    "path": "pkgs/debian/postinst",
    "content": "#!/bin/sh\nset -e\n\nif [ \"$1\" = \"configure\" ]; then\n  # Add user and group\n  if ! getent group sftpgo >/dev/null; then\n    groupadd --system sftpgo\n  fi\n  if ! getent passwd sftpgo >/dev/null; then\n    useradd --system \\\n      --gid sftpgo \\\n      --no-create-home \\\n      --home-dir /var/lib/sftpgo \\\n      --shell /usr/sbin/nologin \\\n      --comment \"SFTPGo user\" \\\n      sftpgo\n  fi\n\n  if [ -z \"$2\" ]; then\n    # if configure has no args this is the first installation\n    # for upgrades the second arg is the previously installed version\n    #\n    # initialize data provider\n    sftpgo initprovider -c /etc/sftpgo\n    # ensure files and folders have the appropriate permissions\n    chown -R sftpgo:sftpgo /etc/sftpgo /var/lib/sftpgo /srv/sftpgo\n    chmod 750 /etc/sftpgo /etc/sftpgo/env.d /var/lib/sftpgo /srv/sftpgo\n    chmod 640 /etc/sftpgo/sftpgo.json\n  fi\n\n  # we added /etc/sftpgo/env.d in v2.4.0, we should check if we are upgrading\n  # from a previous version but a non-recursive chmod/chown shouldn't hurt\n  if [ -d /etc/sftpgo/env.d ]; then\n    chown sftpgo:sftpgo /etc/sftpgo/env.d\n    chmod 750 /etc/sftpgo/env.d\n  fi\n\n  # set the cap_net_bind_service capability so the service can bind to privileged ports\n  setcap cap_net_bind_service=+ep /usr/bin/sftpgo || true\nfi\n\n#DEBHELPER#\n"
  },
  {
    "path": "pkgs/debian/rules",
    "content": "#!/usr/bin/make -f\n# See debhelper(7) (uncomment to enable)\n# output every command that modifies files on the build system.\nexport DH_VERBOSE = 1\n\n%:\n\tdh $@\n"
  },
  {
    "path": "pkgs/debian/sftpgo-docs.docs",
    "content": ""
  },
  {
    "path": "pkgs/debian/sftpgo.dirs",
    "content": "/var/lib/sftpgo\n/srv/sftpgo\n/etc/sftpgo/env.d\n"
  },
  {
    "path": "pkgs/debian/sftpgo.install",
    "content": "sftpgo usr/bin\nsftpgo.json etc/sftpgo\ninit/sftpgo.service lib/systemd/system\nbash_completion/sftpgo usr/share/bash-completion/completions\nman/man1/* usr/share/man/man1\ntemplates usr/share/sftpgo\nstatic usr/share/sftpgo\nopenapi usr/share/sftpgo\n"
  },
  {
    "path": "pkgs/debian/sftpgo.install.arm64",
    "content": "arm64/sftpgo usr/bin\nsftpgo.json etc/sftpgo\ninit/sftpgo.service lib/systemd/system\nbash_completion/sftpgo usr/share/bash-completion/completions\nman/man1/* usr/share/man/man1\ntemplates usr/share/sftpgo\nstatic usr/share/sftpgo\nopenapi usr/share/sftpgo\n"
  },
  {
    "path": "pkgs/debian/sftpgo.install.armhf",
    "content": "armv7/sftpgo usr/bin\nsftpgo.json etc/sftpgo\ninit/sftpgo.service lib/systemd/system\nbash_completion/sftpgo usr/share/bash-completion/completions\nman/man1/* usr/share/man/man1\ntemplates usr/share/sftpgo\nstatic usr/share/sftpgo\nopenapi usr/share/sftpgo\n"
  },
  {
    "path": "pkgs/debian/sftpgo.install.ppc64el",
    "content": "ppc64le/sftpgo usr/bin\nsftpgo.json etc/sftpgo\ninit/sftpgo.service lib/systemd/system\nbash_completion/sftpgo usr/share/bash-completion/completions\nman/man1/* usr/share/man/man1\ntemplates usr/share/sftpgo\nstatic usr/share/sftpgo\nopenapi usr/share/sftpgo\n"
  },
  {
    "path": "pkgs/debian/source/format",
    "content": "3.0 (quilt)\n"
  },
  {
    "path": "pkgs/scripts/deb/postinstall.sh",
    "content": "#!/bin/sh\nset -e\n\nif [ \"$1\" = \"configure\" ]; then\n  # Add user and group\n  if ! getent group sftpgo >/dev/null; then\n    groupadd --system sftpgo\n  fi\n  if ! getent passwd sftpgo >/dev/null; then\n    useradd --system \\\n      --gid sftpgo \\\n      --no-create-home \\\n      --home-dir /var/lib/sftpgo \\\n      --shell /usr/sbin/nologin \\\n      --comment \"SFTPGo user\" \\\n      sftpgo\n  fi\n\n  if [ -z \"$2\" ]; then\n    # if configure has no args this is the first installation\n    # for upgrades the second arg is the previously installed version\n    #\n    # initialize data provider\n    sftpgo initprovider -c /etc/sftpgo\n    # ensure files and folders have the appropriate permissions\n    chown -R sftpgo:sftpgo /etc/sftpgo /var/lib/sftpgo /srv/sftpgo\n    chmod 750 /etc/sftpgo /etc/sftpgo/env.d /var/lib/sftpgo /srv/sftpgo\n    chmod 640 /etc/sftpgo/sftpgo.json\n  fi\n\n  # we added /etc/sftpgo/env.d in v2.4.0, we should check if we are upgrading\n  # from a previous version but a non-recursive chmod/chown shouldn't hurt\n  if [ -d /etc/sftpgo/env.d ]; then\n    chown sftpgo:sftpgo /etc/sftpgo/env.d\n    chmod 750 /etc/sftpgo/env.d\n  fi\n\n  # set the cap_net_bind_service capability so the service can bind to privileged ports\n  setcap cap_net_bind_service=+ep /usr/bin/sftpgo || true\n\nfi\n\nif [ \"$1\" = \"configure\" ] || [ \"$1\" = \"abort-upgrade\" ] || [ \"$1\" = \"abort-deconfigure\" ] || [ \"$1\" = \"abort-remove\" ] ; then\n\t# This will only remove masks created by d-s-h on package removal.\n\tdeb-systemd-helper unmask 'sftpgo.service' >/dev/null || true\n\n\t# was-enabled defaults to true, so new installations run enable.\n\tif deb-systemd-helper --quiet was-enabled 'sftpgo.service'; then\n\t\t# Enables the unit on first installation, creates new\n\t\t# symlinks on upgrades if the unit file has changed.\n\t\tdeb-systemd-helper enable 'sftpgo.service' >/dev/null || true\n\telse\n\t\t# Update the statefile to add new symlinks (if any), which need to be\n\t\t# cleaned up on purge. Also remove old symlinks.\n\t\tdeb-systemd-helper update-state 'sftpgo.service' >/dev/null || true\n\tfi\nfi\n\nif [ \"$1\" = \"configure\" ] || [ \"$1\" = \"abort-upgrade\" ] || [ \"$1\" = \"abort-deconfigure\" ] || [ \"$1\" = \"abort-remove\" ] ; then\n\tif [ -d /run/systemd/system ]; then\n\t\tsystemctl --system daemon-reload >/dev/null || true\n\t\tif [ -n \"$2\" ]; then\n\t\t\t_dh_action=restart\n\t\telse\n\t\t\t_dh_action=start\n\t\tfi\n\t\tdeb-systemd-invoke $_dh_action 'sftpgo.service' >/dev/null || true\n\tfi\nfi"
  },
  {
    "path": "pkgs/scripts/deb/postremove.sh",
    "content": "#!/bin/sh\nset -e\n\nif [ -d /run/systemd/system ]; then\n\tsystemctl --system daemon-reload >/dev/null || true\nfi\n\nif [ \"$1\" = \"remove\" ]; then\n\tif [ -x \"/usr/bin/deb-systemd-helper\" ]; then\n\t\tdeb-systemd-helper mask 'sftpgo.service' >/dev/null || true\n\tfi\nfi\n\nif [ \"$1\" = \"purge\" ]; then\n\tif [ -x \"/usr/bin/deb-systemd-helper\" ]; then\n\t\tdeb-systemd-helper purge 'sftpgo.service' >/dev/null || true\n\t\tdeb-systemd-helper unmask 'sftpgo.service' >/dev/null || true\n\tfi\nfi\n"
  },
  {
    "path": "pkgs/scripts/deb/preremove.sh",
    "content": "#!/bin/sh\nset -e\n\nif [ -d /run/systemd/system ] && [ \"$1\" = remove ]; then\n  deb-systemd-invoke stop sftpgo.service >/dev/null || true\nfi\n"
  },
  {
    "path": "pkgs/scripts/rpm/postinstall",
    "content": "if [ $1 -eq 1 ]; then\n  # Initial installation\n  # Add user and group\n  if ! getent group sftpgo >/dev/null; then\n    /usr/sbin/groupadd --system sftpgo\n  fi\n  if ! getent passwd sftpgo >/dev/null; then\n    /usr/sbin/useradd --system \\\n      --gid sftpgo \\\n      --no-create-home \\\n      --home-dir /var/lib/sftpgo \\\n      --shell /sbin/nologin \\\n      --comment \"SFTPGo user\" \\\n      sftpgo\n  fi\n  # initialize data provider\n  /usr/bin/sftpgo initprovider -c /etc/sftpgo\n  # ensure files and folders have the appropriate permissions\n  /usr/bin/chown -R sftpgo:sftpgo /etc/sftpgo /var/lib/sftpgo /srv/sftpgo\n  /usr/bin/chmod 750 /etc/sftpgo /etc/sftpgo/env.d /var/lib/sftpgo /srv/sftpgo\n  /usr/bin/chmod 640 /etc/sftpgo/sftpgo.json\nfi\n\n# adjust permissions for /srv/sftpgo, /etc/sftpgo/env.d and /var/lib/sftpgo\nif [ -d /srv/sftpgo ]; then\n    /usr/bin/chown sftpgo:sftpgo /srv/sftpgo\n    /usr/bin/chmod 750 /srv/sftpgo\nfi\n\nif [ -d /etc/sftpgo/env.d ]; then\n    /usr/bin/chown sftpgo:sftpgo /etc/sftpgo/env.d\n    /usr/bin/chmod 750 /etc/sftpgo/env.d\nfi\n\nif [ -d /var/lib/sftpgo ]; then\n    /usr/bin/chown sftpgo:sftpgo /var/lib/sftpgo\n    /usr/bin/chmod 750 /var/lib/sftpgo\nfi\n\n# set the cap_net_bind_service capability so the service can bind to privileged ports\nsetcap cap_net_bind_service=+ep /usr/bin/sftpgo || :\n\n# reload to pick up any changes to systemd files\n/bin/systemctl daemon-reload >/dev/null 2>&1 || :\n"
  },
  {
    "path": "pkgs/scripts/rpm/postremove",
    "content": "/bin/systemctl daemon-reload >/dev/null 2>&1 || :\nif [ $1 -ge 1 ]; then\n  # Package upgrade, not uninstall\n  /bin/systemctl try-restart sftpgo.service >/dev/null 2>&1 || :\nfi\n"
  },
  {
    "path": "pkgs/scripts/rpm/preremove",
    "content": "if [ $1 -eq 0 ]; then\n  # Package removal, not upgrade\n  /bin/systemctl --no-reload disable sftpgo.service >/dev/null 2>&1 || :\n  /bin/systemctl stop sftpgo.service >/dev/null 2>&1 || :\nfi\n"
  },
  {
    "path": "sftpgo.json",
    "content": "{\n  \"common\": {\n    \"idle_timeout\": 15,\n    \"upload_mode\": 0,\n    \"actions\": {\n      \"execute_on\": [],\n      \"execute_sync\": [],\n      \"hook\": \"\"\n    },\n    \"setstat_mode\": 0,\n    \"rename_mode\": 0,\n    \"resume_max_size\": 0,\n    \"temp_path\": \"\",\n    \"proxy_protocol\": 0,\n    \"proxy_allowed\": [],\n    \"proxy_skipped\": [],\n    \"startup_hook\": \"\",\n    \"post_connect_hook\": \"\",\n    \"post_disconnect_hook\": \"\",\n    \"max_total_connections\": 0,\n    \"max_per_host_connections\": 20,\n    \"allowlist_status\": 0,\n    \"allow_self_connections\": 0,\n    \"umask\": \"\",\n    \"server_version\": \"\",\n    \"tz\": \"\",\n    \"metadata\": {\n      \"read\": 0\n    },\n    \"defender\": {\n      \"enabled\": false,\n      \"driver\": \"memory\",\n      \"ban_time\": 30,\n      \"ban_time_increment\": 50,\n      \"threshold\": 15,\n      \"score_invalid\": 2,\n      \"score_valid\": 1,\n      \"score_limit_exceeded\": 3,\n      \"score_no_auth\": 0,\n      \"observation_time\": 30,\n      \"entries_soft_limit\": 100,\n      \"entries_hard_limit\": 150,\n      \"login_delay\": {\n        \"success\": 0,\n        \"password_failed\": 1000\n      }\n    },\n    \"rate_limiters\": [\n      {\n        \"average\": 0,\n        \"period\": 1000,\n        \"burst\": 1,\n        \"type\": 2,\n        \"protocols\": [\n          \"SSH\",\n          \"FTP\",\n          \"DAV\",\n          \"HTTP\"\n        ],\n        \"generate_defender_events\": false,\n        \"entries_soft_limit\": 100,\n        \"entries_hard_limit\": 150\n      }\n    ],\n    \"event_manager\": {\n      \"enabled_commands\": []\n    }\n  },\n  \"acme\": {\n    \"domains\": [],\n    \"email\": \"\",\n    \"key_type\": \"4096\",\n    \"certs_path\": \"certs\",\n    \"ca_endpoint\": \"https://acme-v02.api.letsencrypt.org/directory\",\n    \"renew_days\": 30,\n    \"http01_challenge\": {\n      \"port\": 80,\n      \"proxy_header\": \"\",\n      \"webroot\": \"\"\n    },\n    \"tls_alpn01_challenge\": {\n      \"port\": 0\n    }\n  },\n  \"sftpd\": {\n    \"bindings\": [\n      {\n        \"port\": 2022,\n        \"address\": \"\",\n        \"apply_proxy_config\": true\n      }\n    ],\n    \"max_auth_tries\": 0,\n    \"host_keys\": [],\n    \"host_certificates\": [],\n    \"host_key_algorithms\": [],\n    \"kex_algorithms\": [],\n    \"ciphers\": [],\n    \"macs\": [],\n    \"public_key_algorithms\": [],\n    \"trusted_user_ca_keys\": [],\n    \"revoked_user_certs_file\": \"\",\n    \"opkssh_path\": \"\",\n    \"opkssh_checksum\": \"\",\n    \"login_banner_file\": \"\",\n    \"enabled_ssh_commands\": [\n      \"md5sum\",\n      \"sha1sum\",\n      \"sha256sum\",\n      \"cd\",\n      \"pwd\",\n      \"scp\"\n    ],\n    \"keyboard_interactive_authentication\": true,\n    \"keyboard_interactive_auth_hook\": \"\",\n    \"password_authentication\": true\n  },\n  \"ftpd\": {\n    \"bindings\": [\n      {\n        \"port\": 0,\n        \"address\": \"\",\n        \"apply_proxy_config\": true,\n        \"tls_mode\": 0,\n        \"certificate_file\": \"\",\n        \"certificate_key_file\": \"\",\n        \"min_tls_version\": 12,\n        \"force_passive_ip\": \"\",\n        \"passive_ip_overrides\": [],\n        \"passive_host\": \"\",\n        \"client_auth_type\": 0,\n        \"tls_cipher_suites\": [],\n        \"passive_connections_security\": 0,\n        \"active_connections_security\": 0,\n        \"debug\": false\n      }\n    ],\n    \"banner_file\": \"\",\n    \"active_transfers_port_non_20\": true,\n    \"passive_port_range\": {\n      \"start\": 50000,\n      \"end\": 50100\n    },\n    \"disable_active_mode\": false,\n    \"enable_site\": false,\n    \"hash_support\": 0,\n    \"combine_support\": 0,\n    \"certificate_file\": \"\",\n    \"certificate_key_file\": \"\",\n    \"ca_certificates\": [],\n    \"ca_revocation_lists\": []\n  },\n  \"webdavd\": {\n    \"bindings\": [\n      {\n        \"port\": 0,\n        \"address\": \"\",\n        \"enable_https\": false,\n        \"certificate_file\": \"\",\n        \"certificate_key_file\": \"\",\n        \"min_tls_version\": 12,\n        \"client_auth_type\": 0,\n        \"tls_cipher_suites\": [],\n        \"tls_protocols\": [],\n        \"prefix\": \"\",\n        \"proxy_mode\": 0,\n        \"proxy_allowed\": [],\n        \"client_ip_proxy_header\": \"\",\n        \"client_ip_header_depth\": 0,\n        \"disable_www_auth_header\": false\n      }\n    ],\n    \"certificate_file\": \"\",\n    \"certificate_key_file\": \"\",\n    \"ca_certificates\": [],\n    \"ca_revocation_lists\": [],\n    \"cors\": {\n      \"enabled\": false,\n      \"allowed_origins\": [],\n      \"allowed_methods\": [],\n      \"allowed_headers\": [],\n      \"exposed_headers\": [],\n      \"allow_credentials\": false,\n      \"max_age\": 0,\n      \"options_passthrough\": false,\n      \"options_success_status\": 0,\n      \"allow_private_network\": false\n    },\n    \"cache\": {\n      \"users\": {\n        \"expiration_time\": 0,\n        \"max_size\": 50\n      },\n      \"mime_types\": {\n        \"enabled\": true,\n        \"max_size\": 1000,\n        \"custom_mappings\": []\n      }\n    }\n  },\n  \"data_provider\": {\n    \"driver\": \"sqlite\",\n    \"name\": \"sftpgo.db\",\n    \"host\": \"\",\n    \"port\": 0,\n    \"username\": \"\",\n    \"password\": \"\",\n    \"sslmode\": 0,\n    \"disable_sni\": false,\n    \"target_session_attrs\": \"\",\n    \"root_cert\": \"\",\n    \"client_cert\": \"\",\n    \"client_key\": \"\",\n    \"connection_string\": \"\",\n    \"sql_tables_prefix\": \"\",\n    \"track_quota\": 2,\n    \"delayed_quota_update\": 0,\n    \"pool_size\": 0,\n    \"users_base_dir\": \"\",\n    \"actions\": {\n      \"execute_on\": [],\n      \"execute_for\": [],\n      \"hook\": \"\"\n    },\n    \"external_auth_hook\": \"\",\n    \"external_auth_scope\": 0,\n    \"pre_login_hook\": \"\",\n    \"post_login_hook\": \"\",\n    \"post_login_scope\": 0,\n    \"check_password_hook\": \"\",\n    \"check_password_scope\": 0,\n    \"password_hashing\": {\n      \"bcrypt_options\": {\n        \"cost\": 10\n      },\n      \"argon2_options\": {\n        \"memory\": 65536,\n        \"iterations\": 1,\n        \"parallelism\": 2\n      },\n      \"algo\": \"bcrypt\"\n    },\n    \"password_validation\": {\n      \"admins\": {\n        \"min_entropy\": 0\n      },\n      \"users\": {\n        \"min_entropy\": 0\n      }\n    },\n    \"password_caching\": true,\n    \"update_mode\": 0,\n    \"create_default_admin\": false,\n    \"naming_rules\": 5,\n    \"is_shared\": 0,\n    \"node\": {\n      \"host\": \"\",\n      \"port\": 0,\n      \"proto\": \"http\"\n    },\n    \"backups_path\": \"backups\"\n  },\n  \"httpd\": {\n    \"bindings\": [\n      {\n        \"port\": 8080,\n        \"address\": \"\",\n        \"enable_web_admin\": true,\n        \"enable_web_client\": true,\n        \"enable_rest_api\": true,\n        \"enabled_login_methods\": 0,\n        \"disabled_login_methods\": 0,\n        \"enable_https\": false,\n        \"certificate_file\": \"\",\n        \"certificate_key_file\": \"\",\n        \"min_tls_version\": 12,\n        \"client_auth_type\": 0,\n        \"tls_cipher_suites\": [],\n        \"tls_protocols\": [],\n        \"proxy_mode\": 0,\n        \"proxy_allowed\": [],\n        \"client_ip_proxy_header\": \"\",\n        \"client_ip_header_depth\": 0,\n        \"hide_login_url\": 0,\n        \"render_openapi\": true,\n        \"base_url\": \"\",\n        \"languages\": [\n          \"en\"\n        ],\n        \"oidc\": {\n          \"client_id\": \"\",\n          \"client_secret\": \"\",\n          \"client_secret_file\": \"\",\n          \"config_url\": \"\",\n          \"redirect_base_url\": \"\",\n          \"scopes\": [\n            \"openid\",\n            \"profile\",\n            \"email\"\n          ],\n          \"username_field\": \"\",\n          \"role_field\": \"\",\n          \"implicit_roles\": false,\n          \"custom_fields\": [],\n          \"insecure_skip_signature_check\": false,\n          \"debug\": false\n        },\n        \"security\": {\n          \"enabled\": false,\n          \"allowed_hosts\": [],\n          \"allowed_hosts_are_regex\": false,\n          \"hosts_proxy_headers\": [],\n          \"https_redirect\": false,\n          \"https_host\": \"\",\n          \"https_proxy_headers\": [],\n          \"sts_seconds\": 0,\n          \"sts_include_subdomains\": false,\n          \"sts_preload\": false,\n          \"content_type_nosniff\": false,\n          \"content_security_policy\": \"\",\n          \"permissions_policy\": \"\",\n          \"cross_origin_opener_policy\": \"\",\n          \"cross_origin_resource_policy\": \"\",\n          \"cross_origin_embedder_policy\": \"\",\n          \"referrer_policy\": \"\",\n          \"cache_control\": \"\"\n        },\n        \"branding\": {\n          \"web_admin\": {\n            \"name\": \"\",\n            \"short_name\": \"\",\n            \"favicon_path\": \"\",\n            \"logo_path\": \"\",\n            \"disclaimer_name\": \"\",\n            \"disclaimer_path\": \"\",\n            \"default_css\": [],\n            \"extra_css\": []\n          },\n          \"web_client\": {\n            \"name\": \"\",\n            \"short_name\": \"\",\n            \"favicon_path\": \"\",\n            \"logo_path\": \"\",\n            \"disclaimer_name\": \"\",\n            \"disclaimer_path\": \"\",\n            \"default_css\": [],\n            \"extra_css\": []\n          }\n        }\n      }\n    ],\n    \"templates_path\": \"templates\",\n    \"static_files_path\": \"static\",\n    \"openapi_path\": \"openapi\",\n    \"web_root\": \"\",\n    \"certificate_file\": \"\",\n    \"certificate_key_file\": \"\",\n    \"ca_certificates\": [],\n    \"ca_revocation_lists\": [],\n    \"signing_passphrase\": \"\",\n    \"signing_passphrase_file\": \"\",\n    \"token_validation\": 0,\n    \"cookie_lifetime\": 20,\n    \"share_cookie_lifetime\": 120,\n    \"jwt_lifetime\": 20,\n    \"max_upload_file_size\": 0,\n    \"cors\": {\n      \"enabled\": false,\n      \"allowed_origins\": [],\n      \"allowed_methods\": [],\n      \"allowed_headers\": [],\n      \"exposed_headers\": [],\n      \"allow_credentials\": false,\n      \"max_age\": 0,\n      \"options_passthrough\": false,\n      \"options_success_status\": 0,\n      \"allow_private_network\": false\n    },\n    \"setup\": {\n      \"installation_code\": \"\",\n      \"installation_code_hint\": \"Installation code\"\n    },\n    \"hide_support_link\": false\n  },\n  \"telemetry\": {\n    \"bind_port\": 0,\n    \"bind_address\": \"127.0.0.1\",\n    \"enable_profiler\": false,\n    \"auth_user_file\": \"\",\n    \"certificate_file\": \"\",\n    \"certificate_key_file\": \"\",\n    \"min_tls_version\": 12,\n    \"tls_cipher_suites\": [],\n    \"tls_protocols\": []\n  },\n  \"http\": {\n    \"timeout\": 20,\n    \"retry_wait_min\": 2,\n    \"retry_wait_max\": 30,\n    \"retry_max\": 3,\n    \"ca_certificates\": [],\n    \"certificates\": [],\n    \"skip_tls_verify\": false,\n    \"headers\": []\n  },\n  \"command\": {\n    \"timeout\": 30,\n    \"env\": [],\n    \"commands\": []\n  },\n  \"kms\": {\n    \"secrets\": {\n      \"url\": \"\",\n      \"master_key\": \"\",\n      \"master_key_path\": \"\"\n    }\n  },\n  \"mfa\": {\n    \"totp\": [\n      {\n        \"name\": \"Default\",\n        \"issuer\": \"SFTPGo\",\n        \"algo\": \"sha1\"\n      }\n    ]\n  },\n  \"smtp\": {\n    \"host\": \"\",\n    \"port\": 587,\n    \"from\": \"\",\n    \"user\": \"\",\n    \"password\": \"\",\n    \"auth_type\": 0,\n    \"encryption\": 0,\n    \"domain\": \"\",\n    \"templates_path\": \"templates\",\n    \"debug\": 0,\n    \"oauth2\": {\n      \"provider\": 0,\n      \"tenant\": \"\",\n      \"client_id\": \"\",\n      \"client_secret\": \"\",\n      \"refresh_token\": \"\"\n    }\n  },\n  \"plugins\": []\n}"
  },
  {
    "path": "static/assets/css/style.bundle.css",
    "content": "@charset \"UTF-8\";/*!\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 */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000000;--bs-white:#ffffff;--bs-gray:#78829D;--bs-gray-dark:#252F4A;--bs-gray-100:#F9F9F9;--bs-gray-200:#F1F1F4;--bs-gray-300:#DBDFE9;--bs-gray-400:#C4CADA;--bs-gray-500:#6D6D97;--bs-gray-600:#78829D;--bs-gray-700:#4B5675;--bs-gray-800:#252F4A;--bs-gray-900:#071437;--bs-light:#F9F9F9;--bs-primary:#0069E0;--bs-secondary:#F1F1F4;--bs-success:#17C653;--bs-info:#7239EA;--bs-warning:#F6C000;--bs-danger:#F8285A;--bs-dark:#1E2129;--bs-light-rgb:249,249,249;--bs-primary-rgb:0,105,224;--bs-secondary-rgb:241,241,244;--bs-success-rgb:23,198,83;--bs-info-rgb:114,57,234;--bs-warning-rgb:246,192,0;--bs-danger-rgb:248,40,90;--bs-dark-rgb:30,33,41;--bs-primary-text-emphasis:#002a5a;--bs-secondary-text-emphasis:#606062;--bs-success-text-emphasis:#094f21;--bs-info-text-emphasis:#2e175e;--bs-warning-text-emphasis:#624d00;--bs-danger-text-emphasis:#631024;--bs-light-text-emphasis:#4B5675;--bs-dark-text-emphasis:#4B5675;--bs-primary-bg-subtle:#cce1f9;--bs-secondary-bg-subtle:#fcfcfd;--bs-success-bg-subtle:#d1f4dd;--bs-info-bg-subtle:#e3d7fb;--bs-warning-bg-subtle:#fdf2cc;--bs-danger-bg-subtle:#fed4de;--bs-light-bg-subtle:#fcfcfc;--bs-dark-bg-subtle:#C4CADA;--bs-primary-border-subtle:#99c3f3;--bs-secondary-border-subtle:#f9f9fb;--bs-success-border-subtle:#a2e8ba;--bs-info-border-subtle:#c7b0f7;--bs-warning-border-subtle:#fbe699;--bs-danger-border-subtle:#fca9bd;--bs-light-border-subtle:#F1F1F4;--bs-dark-border-subtle:#6D6D97;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:Inter,Helvetica,\"sans-serif\";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#071437;--bs-body-color-rgb:7,20,55;--bs-body-bg:#ffffff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(7, 20, 55, 0.75);--bs-secondary-color-rgb:7,20,55;--bs-secondary-bg:#F1F1F4;--bs-secondary-bg-rgb:241,241,244;--bs-tertiary-color:rgba(7, 20, 55, 0.5);--bs-tertiary-color-rgb:7,20,55;--bs-tertiary-bg:#F9F9F9;--bs-tertiary-bg-rgb:249,249,249;--bs-heading-color:#071437;--bs-link-color:#0069E0;--bs-link-color-rgb:0,105,224;--bs-link-decoration:none;--bs-link-hover-color:#056EE9;--bs-link-hover-color-rgb:5,110,233;--bs-link-hover-decoration:none;--bs-code-color:#b93993;--bs-highlight-color:#071437;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#F1F1F4;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.475rem;--bs-border-radius-sm:0.425rem;--bs-border-radius-lg:0.625rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1.5rem 0.5rem rgba(0, 0, 0, 0.075);--bs-box-shadow-sm:0 0.1rem 1rem 0.25rem rgba(0, 0, 0, 0.05);--bs-box-shadow-lg:0 1rem 2rem 1rem rgba(0, 0, 0, 0.1);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(0, 105, 224, 0.25);--bs-form-valid-color:#17C653;--bs-form-valid-border-color:#17C653;--bs-form-invalid-color:#F8285A;--bs-form-invalid-border-color:#F8285A}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#F5F5F5;--bs-body-color-rgb:245,245,245;--bs-body-bg:#15171C;--bs-body-bg-rgb:21,23,28;--bs-emphasis-color:#ffffff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(245, 245, 245, 0.75);--bs-secondary-color-rgb:245,245,245;--bs-secondary-bg:#252F4A;--bs-secondary-bg-rgb:37,47,74;--bs-tertiary-color:rgba(245, 245, 245, 0.5);--bs-tertiary-color-rgb:245,245,245;--bs-tertiary-bg:#162241;--bs-tertiary-bg-rgb:22,34,65;--bs-primary-text-emphasis:#66a5ec;--bs-secondary-text-emphasis:#f7f7f8;--bs-success-text-emphasis:#74dd98;--bs-info-text-emphasis:#aa88f2;--bs-warning-text-emphasis:#fad966;--bs-danger-text-emphasis:#fb7e9c;--bs-light-text-emphasis:#F9F9F9;--bs-dark-text-emphasis:#DBDFE9;--bs-primary-bg-subtle:#00152d;--bs-secondary-bg-subtle:#303031;--bs-success-bg-subtle:#052811;--bs-info-bg-subtle:#170b2f;--bs-warning-bg-subtle:#312600;--bs-danger-bg-subtle:#320812;--bs-light-bg-subtle:#252F4A;--bs-dark-bg-subtle:#131825;--bs-primary-border-subtle:#003f86;--bs-secondary-border-subtle:#919192;--bs-success-border-subtle:#0e7732;--bs-info-border-subtle:#44228c;--bs-warning-border-subtle:#947300;--bs-danger-border-subtle:#951836;--bs-light-border-subtle:#4B5675;--bs-dark-border-subtle:#252F4A;--bs-heading-color:#F5F5F5;--bs-link-color:#1F87FF;--bs-link-hover-color:#4c9fff;--bs-link-color-rgb:31,135,255;--bs-link-hover-color-rgb:76,159,255;--bs-code-color:#b93993;--bs-highlight-color:#F5F5F5;--bs-highlight-bg:#664d03;--bs-border-color:#26272F;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:600;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h1,h1{font-size:1.75rem}}.h2,h2{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h2,h2{font-size:1.5rem}}.h3,h3{font-size:calc(1.26rem + .12vw)}@media (min-width:1200px){.h3,h3{font-size:1.35rem}}.h4,h4{font-size:1.25rem}.h5,h5{font-size:1.15rem}.h6,h6{font-size:1.075rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:600}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:700}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:none}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb);text-decoration:none}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:1rem}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:1rem;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:1rem;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.425rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6d6d97;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;line-height:inherit;font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-weight:700;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:700;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:700;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:700;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:700;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:700;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#78829d}.blockquote-footer::before{content:\"— \"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);box-shadow:var(--bs-box-shadow-sm);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-gray-600)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0rem}.g-0,.gy-0{--bs-gutter-y:0rem}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:0.75rem}.g-3,.gy-3{--bs-gutter-y:0.75rem}.g-4,.gx-4{--bs-gutter-x:1rem}.g-4,.gy-4{--bs-gutter-y:1rem}.g-5,.gx-5{--bs-gutter-x:1.25rem}.g-5,.gy-5{--bs-gutter-y:1.25rem}.g-6,.gx-6{--bs-gutter-x:1.5rem}.g-6,.gy-6{--bs-gutter-y:1.5rem}.g-7,.gx-7{--bs-gutter-x:1.75rem}.g-7,.gy-7{--bs-gutter-y:1.75rem}.g-8,.gx-8{--bs-gutter-x:2rem}.g-8,.gy-8{--bs-gutter-y:2rem}.g-9,.gx-9{--bs-gutter-x:2.25rem}.g-9,.gy-9{--bs-gutter-y:2.25rem}.g-10,.gx-10{--bs-gutter-x:2.5rem}.g-10,.gy-10{--bs-gutter-y:2.5rem}@media (min-width:576px){.col-sm{flex:1 0 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0rem}.g-sm-0,.gy-sm-0{--bs-gutter-y:0rem}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:0.75rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:0.75rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:1.25rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:1.25rem}.g-sm-6,.gx-sm-6{--bs-gutter-x:1.5rem}.g-sm-6,.gy-sm-6{--bs-gutter-y:1.5rem}.g-sm-7,.gx-sm-7{--bs-gutter-x:1.75rem}.g-sm-7,.gy-sm-7{--bs-gutter-y:1.75rem}.g-sm-8,.gx-sm-8{--bs-gutter-x:2rem}.g-sm-8,.gy-sm-8{--bs-gutter-y:2rem}.g-sm-9,.gx-sm-9{--bs-gutter-x:2.25rem}.g-sm-9,.gy-sm-9{--bs-gutter-y:2.25rem}.g-sm-10,.gx-sm-10{--bs-gutter-x:2.5rem}.g-sm-10,.gy-sm-10{--bs-gutter-y:2.5rem}}@media (min-width:768px){.col-md{flex:1 0 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0rem}.g-md-0,.gy-md-0{--bs-gutter-y:0rem}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:0.75rem}.g-md-3,.gy-md-3{--bs-gutter-y:0.75rem}.g-md-4,.gx-md-4{--bs-gutter-x:1rem}.g-md-4,.gy-md-4{--bs-gutter-y:1rem}.g-md-5,.gx-md-5{--bs-gutter-x:1.25rem}.g-md-5,.gy-md-5{--bs-gutter-y:1.25rem}.g-md-6,.gx-md-6{--bs-gutter-x:1.5rem}.g-md-6,.gy-md-6{--bs-gutter-y:1.5rem}.g-md-7,.gx-md-7{--bs-gutter-x:1.75rem}.g-md-7,.gy-md-7{--bs-gutter-y:1.75rem}.g-md-8,.gx-md-8{--bs-gutter-x:2rem}.g-md-8,.gy-md-8{--bs-gutter-y:2rem}.g-md-9,.gx-md-9{--bs-gutter-x:2.25rem}.g-md-9,.gy-md-9{--bs-gutter-y:2.25rem}.g-md-10,.gx-md-10{--bs-gutter-x:2.5rem}.g-md-10,.gy-md-10{--bs-gutter-y:2.5rem}}@media (min-width:992px){.col-lg{flex:1 0 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0rem}.g-lg-0,.gy-lg-0{--bs-gutter-y:0rem}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:0.75rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:0.75rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:1.25rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:1.25rem}.g-lg-6,.gx-lg-6{--bs-gutter-x:1.5rem}.g-lg-6,.gy-lg-6{--bs-gutter-y:1.5rem}.g-lg-7,.gx-lg-7{--bs-gutter-x:1.75rem}.g-lg-7,.gy-lg-7{--bs-gutter-y:1.75rem}.g-lg-8,.gx-lg-8{--bs-gutter-x:2rem}.g-lg-8,.gy-lg-8{--bs-gutter-y:2rem}.g-lg-9,.gx-lg-9{--bs-gutter-x:2.25rem}.g-lg-9,.gy-lg-9{--bs-gutter-y:2.25rem}.g-lg-10,.gx-lg-10{--bs-gutter-x:2.5rem}.g-lg-10,.gy-lg-10{--bs-gutter-y:2.5rem}}@media (min-width:1200px){.col-xl{flex:1 0 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0rem}.g-xl-0,.gy-xl-0{--bs-gutter-y:0rem}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:0.75rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:0.75rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:1.25rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:1.25rem}.g-xl-6,.gx-xl-6{--bs-gutter-x:1.5rem}.g-xl-6,.gy-xl-6{--bs-gutter-y:1.5rem}.g-xl-7,.gx-xl-7{--bs-gutter-x:1.75rem}.g-xl-7,.gy-xl-7{--bs-gutter-y:1.75rem}.g-xl-8,.gx-xl-8{--bs-gutter-x:2rem}.g-xl-8,.gy-xl-8{--bs-gutter-y:2rem}.g-xl-9,.gx-xl-9{--bs-gutter-x:2.25rem}.g-xl-9,.gy-xl-9{--bs-gutter-y:2.25rem}.g-xl-10,.gx-xl-10{--bs-gutter-x:2.5rem}.g-xl-10,.gy-xl-10{--bs-gutter-y:2.5rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0rem}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0rem}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:0.75rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:0.75rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:1.25rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:1.25rem}.g-xxl-6,.gx-xxl-6{--bs-gutter-x:1.5rem}.g-xxl-6,.gy-xxl-6{--bs-gutter-y:1.5rem}.g-xxl-7,.gx-xxl-7{--bs-gutter-x:1.75rem}.g-xxl-7,.gy-xxl-7{--bs-gutter-y:1.75rem}.g-xxl-8,.gx-xxl-8{--bs-gutter-x:2rem}.g-xxl-8,.gy-xxl-8{--bs-gutter-y:2rem}.g-xxl-9,.gx-xxl-9{--bs-gutter-x:2.25rem}.g-xxl-9,.gy-xxl-9{--bs-gutter-y:2.25rem}.g-xxl-10,.gx-xxl-10{--bs-gutter-x:2.5rem}.g-xxl-10,.gy-xxl-10{--bs-gutter-y:2.5rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(var(--bs-gray-100-rgb), 0.75);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:var(--bs-gray-100);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:var(--bs-gray-100);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.75rem .75rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(1px * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.5rem .5rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000000;--bs-table-bg:#cce1f9;--bs-table-border-color:#b8cbe0;--bs-table-striped-bg:#c2d6ed;--bs-table-striped-color:#000000;--bs-table-active-bg:#b8cbe0;--bs-table-active-color:#000000;--bs-table-hover-bg:#bdd0e6;--bs-table-hover-color:#000000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000000;--bs-table-bg:#fcfcfd;--bs-table-border-color:#e3e3e4;--bs-table-striped-bg:#efeff0;--bs-table-striped-color:#000000;--bs-table-active-bg:#e3e3e4;--bs-table-active-color:#000000;--bs-table-hover-bg:#e9e9ea;--bs-table-hover-color:#000000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000000;--bs-table-bg:#d1f4dd;--bs-table-border-color:#bcdcc7;--bs-table-striped-bg:#c7e8d2;--bs-table-striped-color:#000000;--bs-table-active-bg:#bcdcc7;--bs-table-active-color:#000000;--bs-table-hover-bg:#c1e2cc;--bs-table-hover-color:#000000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000000;--bs-table-bg:#e3d7fb;--bs-table-border-color:#ccc2e2;--bs-table-striped-bg:#d8ccee;--bs-table-striped-color:#000000;--bs-table-active-bg:#ccc2e2;--bs-table-active-color:#000000;--bs-table-hover-bg:#d2c7e8;--bs-table-hover-color:#000000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000000;--bs-table-bg:#fdf2cc;--bs-table-border-color:#e4dab8;--bs-table-striped-bg:#f0e6c2;--bs-table-striped-color:#000000;--bs-table-active-bg:#e4dab8;--bs-table-active-color:#000000;--bs-table-hover-bg:#eae0bd;--bs-table-hover-color:#000000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000000;--bs-table-bg:#fed4de;--bs-table-border-color:#e5bfc8;--bs-table-striped-bg:#f1c9d3;--bs-table-striped-color:#000000;--bs-table-active-bg:#e5bfc8;--bs-table-active-color:#000000;--bs-table-hover-bg:#ebc4cd;--bs-table-hover-color:#000000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000000;--bs-table-bg:#F9F9F9;--bs-table-border-color:#e0e0e0;--bs-table-striped-bg:#ededed;--bs-table-striped-color:#000000;--bs-table-active-bg:#e0e0e0;--bs-table-active-color:#000000;--bs-table-hover-bg:#e6e6e6;--bs-table-hover-color:#000000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#ffffff;--bs-table-bg:#1E2129;--bs-table-border-color:#35373e;--bs-table-striped-bg:#292c34;--bs-table-striped-color:#ffffff;--bs-table-active-bg:#35373e;--bs-table-active-color:#ffffff;--bs-table-hover-bg:#2f3239;--bs-table-hover-color:#ffffff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem;font-size:1.05rem;font-weight:500;color:var(--bs-gray-800)}.col-form-label{padding-top:calc(.775rem + 1px);padding-bottom:calc(.775rem + 1px);margin-bottom:0;font-size:inherit;font-weight:500;line-height:1.5;color:var(--bs-gray-800)}.col-form-label-lg{padding-top:calc(1rem + 1px);padding-bottom:calc(1rem + 1px);font-size:1.15rem}.col-form-label-sm{padding-top:calc(.55rem + 1px);padding-bottom:calc(.55rem + 1px);font-size:.95rem}.form-text{margin-top:.5rem;font-size:.95rem;color:var(--bs-text-muted)}.form-control{display:block;width:100%;padding:.775rem 1rem;font-size:1.1rem;font-weight:500;line-height:1.5;color:var(--bs-gray-700);appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:1px solid var(--bs-gray-300);border-radius:.475rem;box-shadow:false;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-gray-700);background-color:var(--bs-body-bg);border-color:var(--bs-gray-400);outline:0;box-shadow:false,0 0 0 .25rem rgba(0,105,224,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:var(--bs-gray-500);opacity:1}.form-control:disabled{color:var(--bs-gray-500);background-color:var(--bs-gray-200);border-color:var(--bs-gray-300);opacity:1}.form-control::file-selector-button{padding:.775rem 1rem;margin:-.775rem -1rem;margin-inline-end:1rem;color:var(--bs-gray-700);background-color:var(--bs-gray-100);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:shade-color(var(--bs-gray-100),5%)}.form-control-plaintext{display:block;width:100%;padding:.775rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-gray-700);background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + 1.1rem + 2px);padding:.55rem .75rem;font-size:.95rem;border-radius:.425rem}.form-control-sm::file-selector-button{padding:.55rem .75rem;margin:-.55rem -.75rem;margin-inline-end:.75rem}.form-control-lg{min-height:calc(1.5em + 2rem + 2px);padding:1rem 1.5rem;font-size:1.15rem;border-radius:.625rem}.form-control-lg::file-selector-button{padding:1rem 1.5rem;margin:-1rem -1.5rem;margin-inline-end:1.5rem}textarea.form-control{min-height:calc(1.5em + 1.55rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + 1.1rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 2rem + 2px)}.form-control-color{width:3rem;height:calc(1.5em + 1.55rem + 2px);padding:.775rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:.475rem}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:.475rem}.form-control-color.form-control-sm{height:calc(1.5em + 1.1rem + 2px)}.form-control-color.form-control-lg{height:calc(1.5em + 2rem + 2px)}.form-select{--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='%2378829D' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");display:block;width:100%;padding:.775rem 3rem .775rem 1rem;font-size:1.1rem;font-weight:500;line-height:1.5;color:var(--bs-gray-700);appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right 1rem center;background-size:16px 12px;border:1px solid var(--bs-gray-300);border-radius:.475rem;box-shadow:false;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:var(--bs-gray-400);outline:0;box-shadow:false,0 0 0 .25rem rgba(var(--bs-component-active-bg),.25)}.form-select[multiple],.form-select[size]:not([size=\"1\"]){padding-right:1rem;background-image:none}.form-select:disabled{color:var(--bs-gray-500);background-color:var(--bs-gray-200);border-color:var(--bs-gray-300)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-gray-700)}.form-select-sm{padding-top:.55rem;padding-bottom:.55rem;padding-left:.75rem;font-size:.95rem;border-radius:.425rem}.form-select-lg{padding-top:1rem;padding-bottom:1rem;padding-left:1.5rem;font-size:1.15rem;border-radius:.625rem}[data-bs-theme=dark] .form-select{--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='%23808290' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\")}.form-check{display:block;min-height:1.5rem;padding-left:2.25rem;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-2.25rem}.form-check-reverse{padding-right:2.25rem;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-2.25rem;margin-left:0}.form-check-input{--bs-form-check-bg:transparent;flex-shrink:0;width:1.75rem;height:1.75rem;margin-top:-.125rem;vertical-align:top;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid var(--bs-gray-300);print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.45em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:var(--bs-gray-400);outline:0;box-shadow:none}.form-check-input:checked{background-color:#0069e0;border-color:#0069e0}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 13 11' width='13' height='11' fill='none'%3e%3cpath d='M11.0426 1.02893C11.3258 0.695792 11.8254 0.655283 12.1585 0.938451C12.4917 1.22162 12.5322 1.72124 12.249 2.05437L5.51985 9.97104C5.23224 10.3094 4.72261 10.3451 4.3907 10.05L0.828197 6.88335C0.50141 6.59288 0.471975 6.09249 0.762452 5.7657C1.05293 5.43891 1.55332 5.40948 1.88011 5.69995L4.83765 8.32889L11.0426 1.02893Z' fill='%23ffffff'/%3e%3c/svg%3e\")}.form-check-input:checked[type=radio]{--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='%23ffffff'/%3e%3c/svg%3e\")}.form-check-input[type=checkbox]:indeterminate{background-color:#0069e0;border-color:#0069e0;--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='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e\")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-check-label{color:var(--bs-gray-500)}.form-switch{padding-left:3.75rem}.form-switch .form-check-input{--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\");width:3.25rem;margin-left:-3.75rem;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:3.25rem;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--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\")}.form-switch .form-check-input:checked{background-position:right center;--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='%23ffffff'/%3e%3c/svg%3e\")}.form-switch.form-check-reverse{padding-right:3.75rem;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-3.75rem;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--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\")}.form-range{width:100%;height:1.5rem;padding:0;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(0,105,224,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(0,105,224,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;appearance:none;background-color:#0069e0;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b3d2f6}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-gray-300);border-color:transparent;border-radius:.475rem;box-shadow:var(--bs-box-shadow-inset)}.form-range::-moz-range-thumb{width:1rem;height:1rem;appearance:none;background-color:#0069e0;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:#b3d2f6}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-gray-300);border-color:transparent;border-radius:.475rem;box-shadow:var(--bs-box-shadow-inset)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-gray-500)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-gray-500)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.75rem + 2px);min-height:calc(3.75rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem 1rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem 1rem}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.85rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.85rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.85rem;padding-bottom:.625rem;padding-left:1rem}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem .5rem;z-index:-1;height:1.5em;content:\"\";background-color:var(--bs-body-bg);border-radius:.475rem}.form-floating>textarea:disabled~label::after{background-color:var(--bs-gray-200)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#78829d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.775rem 1rem;font-size:1.1rem;font-weight:500;line-height:1.5;color:var(--bs-gray-700);text-align:center;white-space:nowrap;background-color:var(--bs-gray-100);border:1px solid var(--bs-gray-300);border-radius:.475rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:1rem 1.5rem;font-size:1.15rem;border-radius:.625rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.55rem .75rem;font-size:.95rem;border-radius:.425rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:4rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1 * 1px);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.5rem;font-size:.95rem;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.75rem 1rem;margin-top:.1rem;font-size:1rem;color:#fff;background-color:var(--bs-success);border-radius:.475rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + 1.55rem);background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2317C653' 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\");background-repeat:no-repeat;background-position:right calc(.375em + .3875rem) center;background-size:calc(.75em + .775rem) calc(.75em + .775rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:false,0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 1.55rem);background-position:top calc(.375em + .3875rem) right calc(.375em + .3875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size=\"1\"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size=\"1\"]{--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='%2317C653' 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\");padding-right:5.5rem;background-position:right 1rem center,center right 3rem;background-size:16px 12px,calc(.75em + .775rem) calc(.75em + .775rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:false,0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + 1.55rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.5rem;font-size:.95rem;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.75rem 1rem;margin-top:.1rem;font-size:1rem;color:#fff;background-color:var(--bs-danger);border-radius:.475rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + 1.55rem);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='%23F8285A'%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='%23F8285A' stroke='none'/%3e%3c/svg%3e\");background-repeat:no-repeat;background-position:right calc(.375em + .3875rem) center;background-size:calc(.75em + .775rem) calc(.75em + .775rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:false,0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 1.55rem);background-position:top calc(.375em + .3875rem) right calc(.375em + .3875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size=\"1\"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size=\"1\"]{--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='%23F8285A'%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='%23F8285A' stroke='none'/%3e%3c/svg%3e\");padding-right:5.5rem;background-position:right 1rem center,center right 3rem;background-size:16px 12px,calc(.75em + .775rem) calc(.75em + .775rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:false,0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + 1.55rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:1.5rem;--bs-btn-padding-y:0.775rem;--bs-btn-font-size:1.1rem;--bs-btn-font-weight:500;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.475rem;--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:none;--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;vertical-align:middle;cursor:pointer;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);box-shadow:var(--bs-btn-box-shadow);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-box-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-box-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color);box-shadow:var(--bs-btn-active-shadow)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-active-shadow),var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-active-shadow),var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity);box-shadow:none}.btn-light{--bs-btn-color:#000000;--bs-btn-bg:#F9F9F9;--bs-btn-border-color:#F9F9F9;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#d4d4d4;--bs-btn-hover-border-color:#c7c7c7;--bs-btn-focus-shadow-rgb:212,212,212;--bs-btn-active-color:#000000;--bs-btn-active-bg:#c7c7c7;--bs-btn-active-border-color:#bbbbbb;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#000000;--bs-btn-disabled-bg:#F9F9F9;--bs-btn-disabled-border-color:#F9F9F9}.btn-primary{--bs-btn-color:#ffffff;--bs-btn-bg:#0069E0;--bs-btn-border-color:#0069E0;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#0059be;--bs-btn-hover-border-color:#0054b3;--bs-btn-focus-shadow-rgb:38,128,229;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#0054b3;--bs-btn-active-border-color:#004fa8;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#ffffff;--bs-btn-disabled-bg:#0069E0;--bs-btn-disabled-border-color:#0069E0}.btn-secondary{--bs-btn-color:#000000;--bs-btn-bg:#F1F1F4;--bs-btn-border-color:#F1F1F4;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#f3f3f6;--bs-btn-hover-border-color:#f2f2f5;--bs-btn-focus-shadow-rgb:205,205,207;--bs-btn-active-color:#000000;--bs-btn-active-bg:#f4f4f6;--bs-btn-active-border-color:#f2f2f5;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#000000;--bs-btn-disabled-bg:#F1F1F4;--bs-btn-disabled-border-color:#F1F1F4}.btn-success{--bs-btn-color:#000000;--bs-btn-bg:#17C653;--bs-btn-border-color:#17C653;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#3acf6d;--bs-btn-hover-border-color:#2ecc64;--bs-btn-focus-shadow-rgb:20,168,71;--bs-btn-active-color:#000000;--bs-btn-active-bg:#45d175;--bs-btn-active-border-color:#2ecc64;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#000000;--bs-btn-disabled-bg:#17C653;--bs-btn-disabled-border-color:#17C653}.btn-info{--bs-btn-color:#ffffff;--bs-btn-bg:#7239EA;--bs-btn-border-color:#7239EA;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#6130c7;--bs-btn-hover-border-color:#5b2ebb;--bs-btn-focus-shadow-rgb:135,87,237;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#5b2ebb;--bs-btn-active-border-color:#562bb0;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#ffffff;--bs-btn-disabled-bg:#7239EA;--bs-btn-disabled-border-color:#7239EA}.btn-warning{--bs-btn-color:#000000;--bs-btn-bg:#F6C000;--bs-btn-border-color:#F6C000;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#f7c926;--bs-btn-hover-border-color:#f7c61a;--bs-btn-focus-shadow-rgb:209,163,0;--bs-btn-active-color:#000000;--bs-btn-active-bg:#f8cd33;--bs-btn-active-border-color:#f7c61a;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#000000;--bs-btn-disabled-bg:#F6C000;--bs-btn-disabled-border-color:#F6C000}.btn-danger{--bs-btn-color:#000000;--bs-btn-bg:#F8285A;--bs-btn-border-color:#F8285A;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#f94873;--bs-btn-hover-border-color:#f93e6b;--bs-btn-focus-shadow-rgb:211,34,77;--bs-btn-active-color:#000000;--bs-btn-active-bg:#f9537b;--bs-btn-active-border-color:#f93e6b;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#000000;--bs-btn-disabled-bg:#F8285A;--bs-btn-disabled-border-color:#F8285A}.btn-dark{--bs-btn-color:#ffffff;--bs-btn-bg:#1E2129;--bs-btn-border-color:#1E2129;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#404249;--bs-btn-hover-border-color:#35373e;--bs-btn-focus-shadow-rgb:64,66,73;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#4b4d54;--bs-btn-active-border-color:#35373e;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#ffffff;--bs-btn-disabled-bg:#1E2129;--bs-btn-disabled-border-color:#1E2129}.btn-outline-light{--bs-btn-color:#F9F9F9;--bs-btn-border-color:#F9F9F9;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#F9F9F9;--bs-btn-hover-border-color:#F9F9F9;--bs-btn-focus-shadow-rgb:249,249,249;--bs-btn-active-color:#000000;--bs-btn-active-bg:#F9F9F9;--bs-btn-active-border-color:#F9F9F9;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#F9F9F9;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#F9F9F9;--bs-gradient:none}.btn-outline-primary{--bs-btn-color:#0069E0;--bs-btn-border-color:#0069E0;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#0069E0;--bs-btn-hover-border-color:#0069E0;--bs-btn-focus-shadow-rgb:0,105,224;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#0069E0;--bs-btn-active-border-color:#0069E0;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#0069E0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0069E0;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#F1F1F4;--bs-btn-border-color:#F1F1F4;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#F1F1F4;--bs-btn-hover-border-color:#F1F1F4;--bs-btn-focus-shadow-rgb:241,241,244;--bs-btn-active-color:#000000;--bs-btn-active-bg:#F1F1F4;--bs-btn-active-border-color:#F1F1F4;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#F1F1F4;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#F1F1F4;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#17C653;--bs-btn-border-color:#17C653;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#17C653;--bs-btn-hover-border-color:#17C653;--bs-btn-focus-shadow-rgb:23,198,83;--bs-btn-active-color:#000000;--bs-btn-active-bg:#17C653;--bs-btn-active-border-color:#17C653;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#17C653;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#17C653;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#7239EA;--bs-btn-border-color:#7239EA;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#7239EA;--bs-btn-hover-border-color:#7239EA;--bs-btn-focus-shadow-rgb:114,57,234;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#7239EA;--bs-btn-active-border-color:#7239EA;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#7239EA;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#7239EA;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#F6C000;--bs-btn-border-color:#F6C000;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#F6C000;--bs-btn-hover-border-color:#F6C000;--bs-btn-focus-shadow-rgb:246,192,0;--bs-btn-active-color:#000000;--bs-btn-active-bg:#F6C000;--bs-btn-active-border-color:#F6C000;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#F6C000;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#F6C000;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#F8285A;--bs-btn-border-color:#F8285A;--bs-btn-hover-color:#000000;--bs-btn-hover-bg:#F8285A;--bs-btn-hover-border-color:#F8285A;--bs-btn-focus-shadow-rgb:248,40,90;--bs-btn-active-color:#000000;--bs-btn-active-bg:#F8285A;--bs-btn-active-border-color:#F8285A;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#F8285A;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#F8285A;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#1E2129;--bs-btn-border-color:#1E2129;--bs-btn-hover-color:#ffffff;--bs-btn-hover-bg:#1E2129;--bs-btn-hover-border-color:#1E2129;--bs-btn-focus-shadow-rgb:30,33,41;--bs-btn-active-color:#ffffff;--bs-btn-active-bg:#1E2129;--bs-btn-active-border-color:#1E2129;--bs-btn-active-shadow:none;--bs-btn-disabled-color:#1E2129;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#1E2129;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:var(--bs-gray-600);--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:38,128,229;text-decoration:none}.btn-link:focus-visible,.btn-link:hover{text-decoration:none}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:1rem;--bs-btn-padding-x:1.75rem;--bs-btn-font-size:1.15rem;--bs-btn-border-radius:0.625rem}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.55rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:0.95rem;--bs-btn-border-radius:0.425rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:\"\";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:0.475rem;--bs-dropdown-border-width:0rem;--bs-dropdown-inner-border-radius:calc(0.475rem - 0rem);--bs-dropdown-divider-bg:var(--bs-gray-100);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:0px 0px 50px 0px rgba(82, 63, 105, 0.15);--bs-dropdown-link-color:var(--bs-gray-900);--bs-dropdown-link-hover-color:var(--bs-gray-900);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:var(--bs-component-hover-color);--bs-dropdown-link-active-bg:var(--bs-component-hover-bg);--bs-dropdown-link-disabled-color:var(--bs-gray-500);--bs-dropdown-item-padding-x:0.85rem;--bs-dropdown-item-padding-y:0.65rem;--bs-dropdown-header-color:var(--bs-gray-600);--bs-dropdown-header-padding-x:0.85rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius);box-shadow:var(--bs-dropdown-box-shadow)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:\"\";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:\"\";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:\"\"}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:\"\";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.95rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#DBDFE9;--bs-dropdown-bg:#252F4A;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-link-color:#DBDFE9;--bs-dropdown-link-hover-color:#ffffff;--bs-dropdown-divider-bg:var(--bs-gray-100);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:var(--bs-component-hover-color);--bs-dropdown-link-active-bg:var(--bs-component-hover-bg);--bs-dropdown-link-disabled-color:#6D6D97;--bs-dropdown-header-color:#6D6D97}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.475rem}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * 1px)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.3125rem;padding-left:1.3125rem}.btn-group.show .dropdown-toggle{box-shadow:none}.btn-group.show .dropdown-toggle.btn-link{box-shadow:none}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(-1 * 1px)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);background:0 0;border:0;transition:color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(0,105,224,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#ffffff;--bs-nav-pills-link-active-bg:#0069E0}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:600;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.44375rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.075rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.075rem;--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%287, 20, 55, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:0.475rem;--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#ffffff;--bs-navbar-brand-color:#ffffff;--bs-navbar-brand-hover-color:#ffffff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--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\")}[data-bs-theme=dark] .navbar-toggler-icon{--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\")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color:var(--bs-gray-900);--bs-card-border-width:1px;--bs-card-border-color:#F1F1F4;--bs-card-border-radius:0.625rem;--bs-card-box-shadow:0px 3px 4px 0px rgba(0, 0, 0, 0.03);--bs-card-inner-border-radius:calc(0.625rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:transparent;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius);box-shadow:var(--bs-card-box-shadow)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child)>.card-header,.card-group>.card:not(:last-child)>.card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-footer,.card-group>.card:not(:last-child)>.card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child)>.card-header,.card-group>.card:not(:first-child)>.card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-footer,.card-group>.card:not(:first-child)>.card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--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;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:0.475rem;--bs-accordion-inner-border-radius:calc(0.475rem - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.5rem;--bs-accordion-btn-padding-y:1.5rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-body-bg);--bs-accordion-btn-icon:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23071437'%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-.708z'/%3e%3c/svg%3e\");--bs-accordion-btn-icon-width:1.15rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230069E0'%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-.708z'/%3e%3c/svg%3e\");--bs-accordion-btn-focus-box-shadow:none;--bs-accordion-body-padding-x:1.5rem;--bs-accordion-body-padding-y:1.5rem;--bs-accordion-active-color:var(--bs-primary);--bs-accordion-active-bg:var(--bs-gray-100)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:\"\";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23F5F5F5'%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-.708z'/%3e%3c/svg%3e\");--bs-accordion-btn-active-icon:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%231F87FF'%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-.708z'/%3e%3c/svg%3e\")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-divider-color:var(--bs-gray-600);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-primary);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, \"/\")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1.075rem;--bs-pagination-color:var(--bs-gray-700);--bs-pagination-bg:transparent;--bs-pagination-border-width:0;--bs-pagination-border-color:transparent;--bs-pagination-border-radius:0.475rem;--bs-pagination-hover-color:var(--bs-component-hover-color);--bs-pagination-hover-bg:var(--bs-component-hover-bg);--bs-pagination-hover-border-color:transparent;--bs-pagination-focus-color:var(--bs-component-hover-color);--bs-pagination-focus-bg:var(--bs-component-hover-bg);--bs-pagination-focus-box-shadow:none;--bs-pagination-active-color:var(--bs-component-active-color);--bs-pagination-active-bg:var(--bs-component-active-bg);--bs-pagination-active-border-color:transparent;--bs-pagination-disabled-color:var(--bs-gray-400);--bs-pagination-disabled-bg:transparent;--bs-pagination-disabled-border-color:transparent;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(-1 * 0)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.075rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.95rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.5rem;--bs-badge-padding-y:0.325rem;--bs-badge-font-size:0.85rem;--bs-badge-font-weight:600;--bs-badge-color:var(--bs-body-color);--bs-badge-border-radius:0.425rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:600;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-gray-100);--bs-progress-border-radius:6px;--bs-progress-box-shadow:none;--bs-progress-bar-color:#ffffff;--bs-progress-bar-bg:#0069E0;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius);box-shadow:var(--bs-progress-box-shadow)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-gray-900);--bs-list-group-bg:var(--bs-light);--bs-list-group-border-color:var(--bs-gray-200);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-gray-700);--bs-list-group-action-hover-color:var(--bs-gray-700);--bs-list-group-action-hover-bg:var(--bs-gray-100);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-gray-200);--bs-list-group-disabled-color:var(--bs-gray-600);--bs-list-group-disabled-bg:var(--bs-light);--bs-list-group-active-color:var(--bs-component-active-color);--bs-list-group-active-bg:var(--bs-component-active-bg);--bs-list-group-active-border-color:var(--bs-component-active-bg);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, \".\") \". \";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000000;--bs-btn-close-bg:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000000'%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.414z'/%3e%3c/svg%3e\");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:none;--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:.75rem;height:.75rem;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/.75rem auto no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:.475rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color:var(--bs-gray-700);--bs-toast-bg:var(--bs-body-bg);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:transparent;--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-gray-700);--bs-toast-header-bg:var(--bs-body-bg);--bs-toast-header-border-color:var(--bs-border-color);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1.75rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:0;--bs-modal-border-radius:0.475rem;--bs-modal-box-shadow:0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);--bs-modal-inner-border-radius:0.475rem;--bs-modal-header-padding-x:1.75rem;--bs-modal-header-padding-y:1.75rem;--bs-modal-header-padding:1.75rem 1.75rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:1px;--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);box-shadow:var(--bs-modal-box-shadow);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000000;--bs-backdrop-opacity:0.4;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.1)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:1rem;--bs-tooltip-padding-y:0.75rem;--bs-tooltip-margin:0;--bs-tooltip-font-size:1rem;--bs-tooltip-color:var(--bs-gray-800);--bs-tooltip-bg:var(--bs-body-bg);--bs-tooltip-border-radius:0.475rem;--bs-tooltip-opacity:1;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:\"\";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:1rem;--bs-popover-bg:#ffffff;--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:#ffffff;--bs-popover-border-radius:0.475rem;--bs-popover-inner-border-radius:0.475rem;--bs-popover-box-shadow:0px 0px 50px 0px rgba(82, 63, 105, 0.15);--bs-popover-header-padding-x:1.25rem;--bs-popover-header-padding-y:1rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:var(--bs-gray-800);--bs-popover-header-bg:#ffffff;--bs-popover-body-padding-x:1.25rem;--bs-popover-body-padding-y:1.25rem;--bs-popover-body-color:var(--bs-gray-800);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius);box-shadow:var(--bs-popover-box-shadow)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:\"\";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:\"\";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:\"\"}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;filter:var(--bs-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'%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\")}.carousel-control-next-icon{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'%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\")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:var(--bs-carousel-indicator-active-bg);background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000000;--bs-carousel-caption-color:#000000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#ffffff;--bs-carousel-caption-color:#ffffff}[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000000;--bs-carousel-caption-color:#000000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;flex-shrink:0;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.185rem;--bs-spinner-animation-speed:0.65s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.145em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.65s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.3s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1.75rem;--bs-offcanvas-padding-y:1.75rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:0;--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--bs-offcanvas-box-shadow);transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.4}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:\"\"}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{mask-position:-200% 0}}.clearfix::after{display:block;clear:both;content:\"\"}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#000!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#000!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#fff!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#000!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(250,250,250,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(250,250,250,var(--bs-link-underline-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(0,84,179,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(0,84,179,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(244,244,246,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(244,244,246,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(69,209,117,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(69,209,117,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(91,46,187,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(91,46,187,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(248,205,51,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(248,205,51,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(249,83,123,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(249,83,123,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(24,26,33,var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(24,26,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,.75))!important}.focus-ring:focus{outline:0;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)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,.5));text-underline-offset:.25em;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:\"\"}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:\"\"}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{object-fit:contain!important}.object-fit-cover{object-fit:cover!important}.object-fit-fill{object-fit:fill!important}.object-fit-scale{object-fit:scale-down!important}.object-fit-none{object-fit:none!important}.opacity-0{opacity:0!important}.opacity-5{opacity:.05!important}.opacity-10{opacity:.1!important}.opacity-15{opacity:.15!important}.opacity-20{opacity:.2!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.top-0{top:0!important}.top-25{top:25%!important}.top-50{top:50%!important}.top-75{top:75%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-25{bottom:25%!important}.bottom-50{bottom:50%!important}.bottom-75{bottom:75%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-25{left:25%!important}.start-50{left:50%!important}.start-75{left:75%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-25{right:25%!important}.end-50{right:50%!important}.end-75{right:75%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-0{border-width:0!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-unset{width:unset!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.w-1px{width:1px!important}.w-2px{width:2px!important}.w-3px{width:3px!important}.w-4px{width:4px!important}.w-5px{width:5px!important}.w-6px{width:6px!important}.w-7px{width:7px!important}.w-8px{width:8px!important}.w-9px{width:9px!important}.w-10px{width:10px!important}.w-15px{width:15px!important}.w-20px{width:20px!important}.w-25px{width:25px!important}.w-30px{width:30px!important}.w-35px{width:35px!important}.w-40px{width:40px!important}.w-45px{width:45px!important}.w-50px{width:50px!important}.w-55px{width:55px!important}.w-60px{width:60px!important}.w-65px{width:65px!important}.w-70px{width:70px!important}.w-75px{width:75px!important}.w-80px{width:80px!important}.w-85px{width:85px!important}.w-90px{width:90px!important}.w-95px{width:95px!important}.w-100px{width:100px!important}.w-125px{width:125px!important}.w-150px{width:150px!important}.w-175px{width:175px!important}.w-200px{width:200px!important}.w-225px{width:225px!important}.w-250px{width:250px!important}.w-275px{width:275px!important}.w-300px{width:300px!important}.w-325px{width:325px!important}.w-350px{width:350px!important}.w-375px{width:375px!important}.w-400px{width:400px!important}.w-425px{width:425px!important}.w-450px{width:450px!important}.w-475px{width:475px!important}.w-500px{width:500px!important}.w-550px{width:550px!important}.w-600px{width:600px!important}.w-650px{width:650px!important}.w-700px{width:700px!important}.w-750px{width:750px!important}.w-800px{width:800px!important}.w-850px{width:850px!important}.w-900px{width:900px!important}.w-950px{width:950px!important}.w-1000px{width:1000px!important}.mw-unset{max-width:unset!important}.mw-25{max-width:25%!important}.mw-50{max-width:50%!important}.mw-75{max-width:75%!important}.mw-100{max-width:100%!important}.mw-auto{max-width:auto!important}.mw-1px{max-width:1px!important}.mw-2px{max-width:2px!important}.mw-3px{max-width:3px!important}.mw-4px{max-width:4px!important}.mw-5px{max-width:5px!important}.mw-6px{max-width:6px!important}.mw-7px{max-width:7px!important}.mw-8px{max-width:8px!important}.mw-9px{max-width:9px!important}.mw-10px{max-width:10px!important}.mw-15px{max-width:15px!important}.mw-20px{max-width:20px!important}.mw-25px{max-width:25px!important}.mw-30px{max-width:30px!important}.mw-35px{max-width:35px!important}.mw-40px{max-width:40px!important}.mw-45px{max-width:45px!important}.mw-50px{max-width:50px!important}.mw-55px{max-width:55px!important}.mw-60px{max-width:60px!important}.mw-65px{max-width:65px!important}.mw-70px{max-width:70px!important}.mw-75px{max-width:75px!important}.mw-80px{max-width:80px!important}.mw-85px{max-width:85px!important}.mw-90px{max-width:90px!important}.mw-95px{max-width:95px!important}.mw-100px{max-width:100px!important}.mw-125px{max-width:125px!important}.mw-150px{max-width:150px!important}.mw-175px{max-width:175px!important}.mw-200px{max-width:200px!important}.mw-225px{max-width:225px!important}.mw-250px{max-width:250px!important}.mw-275px{max-width:275px!important}.mw-300px{max-width:300px!important}.mw-325px{max-width:325px!important}.mw-350px{max-width:350px!important}.mw-375px{max-width:375px!important}.mw-400px{max-width:400px!important}.mw-425px{max-width:425px!important}.mw-450px{max-width:450px!important}.mw-475px{max-width:475px!important}.mw-500px{max-width:500px!important}.mw-550px{max-width:550px!important}.mw-600px{max-width:600px!important}.mw-650px{max-width:650px!important}.mw-700px{max-width:700px!important}.mw-750px{max-width:750px!important}.mw-800px{max-width:800px!important}.mw-850px{max-width:850px!important}.mw-900px{max-width:900px!important}.mw-950px{max-width:950px!important}.mw-1000px{max-width:1000px!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-unset{height:unset!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.h-1px{height:1px!important}.h-2px{height:2px!important}.h-3px{height:3px!important}.h-4px{height:4px!important}.h-5px{height:5px!important}.h-6px{height:6px!important}.h-7px{height:7px!important}.h-8px{height:8px!important}.h-9px{height:9px!important}.h-10px{height:10px!important}.h-15px{height:15px!important}.h-20px{height:20px!important}.h-25px{height:25px!important}.h-30px{height:30px!important}.h-35px{height:35px!important}.h-40px{height:40px!important}.h-45px{height:45px!important}.h-50px{height:50px!important}.h-55px{height:55px!important}.h-60px{height:60px!important}.h-65px{height:65px!important}.h-70px{height:70px!important}.h-75px{height:75px!important}.h-80px{height:80px!important}.h-85px{height:85px!important}.h-90px{height:90px!important}.h-95px{height:95px!important}.h-100px{height:100px!important}.h-125px{height:125px!important}.h-150px{height:150px!important}.h-175px{height:175px!important}.h-200px{height:200px!important}.h-225px{height:225px!important}.h-250px{height:250px!important}.h-275px{height:275px!important}.h-300px{height:300px!important}.h-325px{height:325px!important}.h-350px{height:350px!important}.h-375px{height:375px!important}.h-400px{height:400px!important}.h-425px{height:425px!important}.h-450px{height:450px!important}.h-475px{height:475px!important}.h-500px{height:500px!important}.h-550px{height:550px!important}.h-600px{height:600px!important}.h-650px{height:650px!important}.h-700px{height:700px!important}.h-750px{height:750px!important}.h-800px{height:800px!important}.h-850px{height:850px!important}.h-900px{height:900px!important}.h-950px{height:950px!important}.h-1000px{height:1000px!important}.mh-unset{max-height:unset!important}.mh-25{max-height:25%!important}.mh-50{max-height:50%!important}.mh-75{max-height:75%!important}.mh-100{max-height:100%!important}.mh-auto{max-height:auto!important}.mh-1px{max-height:1px!important}.mh-2px{max-height:2px!important}.mh-3px{max-height:3px!important}.mh-4px{max-height:4px!important}.mh-5px{max-height:5px!important}.mh-6px{max-height:6px!important}.mh-7px{max-height:7px!important}.mh-8px{max-height:8px!important}.mh-9px{max-height:9px!important}.mh-10px{max-height:10px!important}.mh-15px{max-height:15px!important}.mh-20px{max-height:20px!important}.mh-25px{max-height:25px!important}.mh-30px{max-height:30px!important}.mh-35px{max-height:35px!important}.mh-40px{max-height:40px!important}.mh-45px{max-height:45px!important}.mh-50px{max-height:50px!important}.mh-55px{max-height:55px!important}.mh-60px{max-height:60px!important}.mh-65px{max-height:65px!important}.mh-70px{max-height:70px!important}.mh-75px{max-height:75px!important}.mh-80px{max-height:80px!important}.mh-85px{max-height:85px!important}.mh-90px{max-height:90px!important}.mh-95px{max-height:95px!important}.mh-100px{max-height:100px!important}.mh-125px{max-height:125px!important}.mh-150px{max-height:150px!important}.mh-175px{max-height:175px!important}.mh-200px{max-height:200px!important}.mh-225px{max-height:225px!important}.mh-250px{max-height:250px!important}.mh-275px{max-height:275px!important}.mh-300px{max-height:300px!important}.mh-325px{max-height:325px!important}.mh-350px{max-height:350px!important}.mh-375px{max-height:375px!important}.mh-400px{max-height:400px!important}.mh-425px{max-height:425px!important}.mh-450px{max-height:450px!important}.mh-475px{max-height:475px!important}.mh-500px{max-height:500px!important}.mh-550px{max-height:550px!important}.mh-600px{max-height:600px!important}.mh-650px{max-height:650px!important}.mh-700px{max-height:700px!important}.mh-750px{max-height:750px!important}.mh-800px{max-height:800px!important}.mh-850px{max-height:850px!important}.mh-900px{max-height:900px!important}.mh-950px{max-height:950px!important}.mh-1000px{max-height:1000px!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:.75rem!important}.m-4{margin:1rem!important}.m-5{margin:1.25rem!important}.m-6{margin:1.5rem!important}.m-7{margin:1.75rem!important}.m-8{margin:2rem!important}.m-9{margin:2.25rem!important}.m-10{margin:2.5rem!important}.m-11{margin:2.75rem!important}.m-12{margin:3rem!important}.m-13{margin:3.25rem!important}.m-14{margin:3.5rem!important}.m-15{margin:3.75rem!important}.m-16{margin:4rem!important}.m-17{margin:4.25rem!important}.m-18{margin:4.5rem!important}.m-19{margin:4.75rem!important}.m-20{margin:5rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:.75rem!important;margin-left:.75rem!important}.mx-4{margin-right:1rem!important;margin-left:1rem!important}.mx-5{margin-right:1.25rem!important;margin-left:1.25rem!important}.mx-6{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-7{margin-right:1.75rem!important;margin-left:1.75rem!important}.mx-8{margin-right:2rem!important;margin-left:2rem!important}.mx-9{margin-right:2.25rem!important;margin-left:2.25rem!important}.mx-10{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-11{margin-right:2.75rem!important;margin-left:2.75rem!important}.mx-12{margin-right:3rem!important;margin-left:3rem!important}.mx-13{margin-right:3.25rem!important;margin-left:3.25rem!important}.mx-14{margin-right:3.5rem!important;margin-left:3.5rem!important}.mx-15{margin-right:3.75rem!important;margin-left:3.75rem!important}.mx-16{margin-right:4rem!important;margin-left:4rem!important}.mx-17{margin-right:4.25rem!important;margin-left:4.25rem!important}.mx-18{margin-right:4.5rem!important;margin-left:4.5rem!important}.mx-19{margin-right:4.75rem!important;margin-left:4.75rem!important}.mx-20{margin-right:5rem!important;margin-left:5rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:.75rem!important;margin-bottom:.75rem!important}.my-4{margin-top:1rem!important;margin-bottom:1rem!important}.my-5{margin-top:1.25rem!important;margin-bottom:1.25rem!important}.my-6{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-7{margin-top:1.75rem!important;margin-bottom:1.75rem!important}.my-8{margin-top:2rem!important;margin-bottom:2rem!important}.my-9{margin-top:2.25rem!important;margin-bottom:2.25rem!important}.my-10{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-11{margin-top:2.75rem!important;margin-bottom:2.75rem!important}.my-12{margin-top:3rem!important;margin-bottom:3rem!important}.my-13{margin-top:3.25rem!important;margin-bottom:3.25rem!important}.my-14{margin-top:3.5rem!important;margin-bottom:3.5rem!important}.my-15{margin-top:3.75rem!important;margin-bottom:3.75rem!important}.my-16{margin-top:4rem!important;margin-bottom:4rem!important}.my-17{margin-top:4.25rem!important;margin-bottom:4.25rem!important}.my-18{margin-top:4.5rem!important;margin-bottom:4.5rem!important}.my-19{margin-top:4.75rem!important;margin-bottom:4.75rem!important}.my-20{margin-top:5rem!important;margin-bottom:5rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:.75rem!important}.mt-4{margin-top:1rem!important}.mt-5{margin-top:1.25rem!important}.mt-6{margin-top:1.5rem!important}.mt-7{margin-top:1.75rem!important}.mt-8{margin-top:2rem!important}.mt-9{margin-top:2.25rem!important}.mt-10{margin-top:2.5rem!important}.mt-11{margin-top:2.75rem!important}.mt-12{margin-top:3rem!important}.mt-13{margin-top:3.25rem!important}.mt-14{margin-top:3.5rem!important}.mt-15{margin-top:3.75rem!important}.mt-16{margin-top:4rem!important}.mt-17{margin-top:4.25rem!important}.mt-18{margin-top:4.5rem!important}.mt-19{margin-top:4.75rem!important}.mt-20{margin-top:5rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:.75rem!important}.me-4{margin-right:1rem!important}.me-5{margin-right:1.25rem!important}.me-6{margin-right:1.5rem!important}.me-7{margin-right:1.75rem!important}.me-8{margin-right:2rem!important}.me-9{margin-right:2.25rem!important}.me-10{margin-right:2.5rem!important}.me-11{margin-right:2.75rem!important}.me-12{margin-right:3rem!important}.me-13{margin-right:3.25rem!important}.me-14{margin-right:3.5rem!important}.me-15{margin-right:3.75rem!important}.me-16{margin-right:4rem!important}.me-17{margin-right:4.25rem!important}.me-18{margin-right:4.5rem!important}.me-19{margin-right:4.75rem!important}.me-20{margin-right:5rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:.75rem!important}.mb-4{margin-bottom:1rem!important}.mb-5{margin-bottom:1.25rem!important}.mb-6{margin-bottom:1.5rem!important}.mb-7{margin-bottom:1.75rem!important}.mb-8{margin-bottom:2rem!important}.mb-9{margin-bottom:2.25rem!important}.mb-10{margin-bottom:2.5rem!important}.mb-11{margin-bottom:2.75rem!important}.mb-12{margin-bottom:3rem!important}.mb-13{margin-bottom:3.25rem!important}.mb-14{margin-bottom:3.5rem!important}.mb-15{margin-bottom:3.75rem!important}.mb-16{margin-bottom:4rem!important}.mb-17{margin-bottom:4.25rem!important}.mb-18{margin-bottom:4.5rem!important}.mb-19{margin-bottom:4.75rem!important}.mb-20{margin-bottom:5rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:.75rem!important}.ms-4{margin-left:1rem!important}.ms-5{margin-left:1.25rem!important}.ms-6{margin-left:1.5rem!important}.ms-7{margin-left:1.75rem!important}.ms-8{margin-left:2rem!important}.ms-9{margin-left:2.25rem!important}.ms-10{margin-left:2.5rem!important}.ms-11{margin-left:2.75rem!important}.ms-12{margin-left:3rem!important}.ms-13{margin-left:3.25rem!important}.ms-14{margin-left:3.5rem!important}.ms-15{margin-left:3.75rem!important}.ms-16{margin-left:4rem!important}.ms-17{margin-left:4.25rem!important}.ms-18{margin-left:4.5rem!important}.ms-19{margin-left:4.75rem!important}.ms-20{margin-left:5rem!important}.ms-auto{margin-left:auto!important}.m-n1{margin:-.25rem!important}.m-n2{margin:-.5rem!important}.m-n3{margin:-.75rem!important}.m-n4{margin:-1rem!important}.m-n5{margin:-1.25rem!important}.m-n6{margin:-1.5rem!important}.m-n7{margin:-1.75rem!important}.m-n8{margin:-2rem!important}.m-n9{margin:-2.25rem!important}.m-n10{margin:-2.5rem!important}.m-n11{margin:-2.75rem!important}.m-n12{margin:-3rem!important}.m-n13{margin:-3.25rem!important}.m-n14{margin:-3.5rem!important}.m-n15{margin:-3.75rem!important}.m-n16{margin:-4rem!important}.m-n17{margin:-4.25rem!important}.m-n18{margin:-4.5rem!important}.m-n19{margin:-4.75rem!important}.m-n20{margin:-5rem!important}.mx-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-n3{margin-right:-.75rem!important;margin-left:-.75rem!important}.mx-n4{margin-right:-1rem!important;margin-left:-1rem!important}.mx-n5{margin-right:-1.25rem!important;margin-left:-1.25rem!important}.mx-n6{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-n7{margin-right:-1.75rem!important;margin-left:-1.75rem!important}.mx-n8{margin-right:-2rem!important;margin-left:-2rem!important}.mx-n9{margin-right:-2.25rem!important;margin-left:-2.25rem!important}.mx-n10{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.mx-n11{margin-right:-2.75rem!important;margin-left:-2.75rem!important}.mx-n12{margin-right:-3rem!important;margin-left:-3rem!important}.mx-n13{margin-right:-3.25rem!important;margin-left:-3.25rem!important}.mx-n14{margin-right:-3.5rem!important;margin-left:-3.5rem!important}.mx-n15{margin-right:-3.75rem!important;margin-left:-3.75rem!important}.mx-n16{margin-right:-4rem!important;margin-left:-4rem!important}.mx-n17{margin-right:-4.25rem!important;margin-left:-4.25rem!important}.mx-n18{margin-right:-4.5rem!important;margin-left:-4.5rem!important}.mx-n19{margin-right:-4.75rem!important;margin-left:-4.75rem!important}.mx-n20{margin-right:-5rem!important;margin-left:-5rem!important}.my-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-n3{margin-top:-.75rem!important;margin-bottom:-.75rem!important}.my-n4{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-n5{margin-top:-1.25rem!important;margin-bottom:-1.25rem!important}.my-n6{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-n7{margin-top:-1.75rem!important;margin-bottom:-1.75rem!important}.my-n8{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-n9{margin-top:-2.25rem!important;margin-bottom:-2.25rem!important}.my-n10{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.my-n11{margin-top:-2.75rem!important;margin-bottom:-2.75rem!important}.my-n12{margin-top:-3rem!important;margin-bottom:-3rem!important}.my-n13{margin-top:-3.25rem!important;margin-bottom:-3.25rem!important}.my-n14{margin-top:-3.5rem!important;margin-bottom:-3.5rem!important}.my-n15{margin-top:-3.75rem!important;margin-bottom:-3.75rem!important}.my-n16{margin-top:-4rem!important;margin-bottom:-4rem!important}.my-n17{margin-top:-4.25rem!important;margin-bottom:-4.25rem!important}.my-n18{margin-top:-4.5rem!important;margin-bottom:-4.5rem!important}.my-n19{margin-top:-4.75rem!important;margin-bottom:-4.75rem!important}.my-n20{margin-top:-5rem!important;margin-bottom:-5rem!important}.mt-n1{margin-top:-.25rem!important}.mt-n2{margin-top:-.5rem!important}.mt-n3{margin-top:-.75rem!important}.mt-n4{margin-top:-1rem!important}.mt-n5{margin-top:-1.25rem!important}.mt-n6{margin-top:-1.5rem!important}.mt-n7{margin-top:-1.75rem!important}.mt-n8{margin-top:-2rem!important}.mt-n9{margin-top:-2.25rem!important}.mt-n10{margin-top:-2.5rem!important}.mt-n11{margin-top:-2.75rem!important}.mt-n12{margin-top:-3rem!important}.mt-n13{margin-top:-3.25rem!important}.mt-n14{margin-top:-3.5rem!important}.mt-n15{margin-top:-3.75rem!important}.mt-n16{margin-top:-4rem!important}.mt-n17{margin-top:-4.25rem!important}.mt-n18{margin-top:-4.5rem!important}.mt-n19{margin-top:-4.75rem!important}.mt-n20{margin-top:-5rem!important}.me-n1{margin-right:-.25rem!important}.me-n2{margin-right:-.5rem!important}.me-n3{margin-right:-.75rem!important}.me-n4{margin-right:-1rem!important}.me-n5{margin-right:-1.25rem!important}.me-n6{margin-right:-1.5rem!important}.me-n7{margin-right:-1.75rem!important}.me-n8{margin-right:-2rem!important}.me-n9{margin-right:-2.25rem!important}.me-n10{margin-right:-2.5rem!important}.me-n11{margin-right:-2.75rem!important}.me-n12{margin-right:-3rem!important}.me-n13{margin-right:-3.25rem!important}.me-n14{margin-right:-3.5rem!important}.me-n15{margin-right:-3.75rem!important}.me-n16{margin-right:-4rem!important}.me-n17{margin-right:-4.25rem!important}.me-n18{margin-right:-4.5rem!important}.me-n19{margin-right:-4.75rem!important}.me-n20{margin-right:-5rem!important}.mb-n1{margin-bottom:-.25rem!important}.mb-n2{margin-bottom:-.5rem!important}.mb-n3{margin-bottom:-.75rem!important}.mb-n4{margin-bottom:-1rem!important}.mb-n5{margin-bottom:-1.25rem!important}.mb-n6{margin-bottom:-1.5rem!important}.mb-n7{margin-bottom:-1.75rem!important}.mb-n8{margin-bottom:-2rem!important}.mb-n9{margin-bottom:-2.25rem!important}.mb-n10{margin-bottom:-2.5rem!important}.mb-n11{margin-bottom:-2.75rem!important}.mb-n12{margin-bottom:-3rem!important}.mb-n13{margin-bottom:-3.25rem!important}.mb-n14{margin-bottom:-3.5rem!important}.mb-n15{margin-bottom:-3.75rem!important}.mb-n16{margin-bottom:-4rem!important}.mb-n17{margin-bottom:-4.25rem!important}.mb-n18{margin-bottom:-4.5rem!important}.mb-n19{margin-bottom:-4.75rem!important}.mb-n20{margin-bottom:-5rem!important}.ms-n1{margin-left:-.25rem!important}.ms-n2{margin-left:-.5rem!important}.ms-n3{margin-left:-.75rem!important}.ms-n4{margin-left:-1rem!important}.ms-n5{margin-left:-1.25rem!important}.ms-n6{margin-left:-1.5rem!important}.ms-n7{margin-left:-1.75rem!important}.ms-n8{margin-left:-2rem!important}.ms-n9{margin-left:-2.25rem!important}.ms-n10{margin-left:-2.5rem!important}.ms-n11{margin-left:-2.75rem!important}.ms-n12{margin-left:-3rem!important}.ms-n13{margin-left:-3.25rem!important}.ms-n14{margin-left:-3.5rem!important}.ms-n15{margin-left:-3.75rem!important}.ms-n16{margin-left:-4rem!important}.ms-n17{margin-left:-4.25rem!important}.ms-n18{margin-left:-4.5rem!important}.ms-n19{margin-left:-4.75rem!important}.ms-n20{margin-left:-5rem!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:.75rem!important}.p-4{padding:1rem!important}.p-5{padding:1.25rem!important}.p-6{padding:1.5rem!important}.p-7{padding:1.75rem!important}.p-8{padding:2rem!important}.p-9{padding:2.25rem!important}.p-10{padding:2.5rem!important}.p-11{padding:2.75rem!important}.p-12{padding:3rem!important}.p-13{padding:3.25rem!important}.p-14{padding:3.5rem!important}.p-15{padding:3.75rem!important}.p-16{padding:4rem!important}.p-17{padding:4.25rem!important}.p-18{padding:4.5rem!important}.p-19{padding:4.75rem!important}.p-20{padding:5rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:.75rem!important;padding-left:.75rem!important}.px-4{padding-right:1rem!important;padding-left:1rem!important}.px-5{padding-right:1.25rem!important;padding-left:1.25rem!important}.px-6{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-7{padding-right:1.75rem!important;padding-left:1.75rem!important}.px-8{padding-right:2rem!important;padding-left:2rem!important}.px-9{padding-right:2.25rem!important;padding-left:2.25rem!important}.px-10{padding-right:2.5rem!important;padding-left:2.5rem!important}.px-11{padding-right:2.75rem!important;padding-left:2.75rem!important}.px-12{padding-right:3rem!important;padding-left:3rem!important}.px-13{padding-right:3.25rem!important;padding-left:3.25rem!important}.px-14{padding-right:3.5rem!important;padding-left:3.5rem!important}.px-15{padding-right:3.75rem!important;padding-left:3.75rem!important}.px-16{padding-right:4rem!important;padding-left:4rem!important}.px-17{padding-right:4.25rem!important;padding-left:4.25rem!important}.px-18{padding-right:4.5rem!important;padding-left:4.5rem!important}.px-19{padding-right:4.75rem!important;padding-left:4.75rem!important}.px-20{padding-right:5rem!important;padding-left:5rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:.75rem!important;padding-bottom:.75rem!important}.py-4{padding-top:1rem!important;padding-bottom:1rem!important}.py-5{padding-top:1.25rem!important;padding-bottom:1.25rem!important}.py-6{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-7{padding-top:1.75rem!important;padding-bottom:1.75rem!important}.py-8{padding-top:2rem!important;padding-bottom:2rem!important}.py-9{padding-top:2.25rem!important;padding-bottom:2.25rem!important}.py-10{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.py-11{padding-top:2.75rem!important;padding-bottom:2.75rem!important}.py-12{padding-top:3rem!important;padding-bottom:3rem!important}.py-13{padding-top:3.25rem!important;padding-bottom:3.25rem!important}.py-14{padding-top:3.5rem!important;padding-bottom:3.5rem!important}.py-15{padding-top:3.75rem!important;padding-bottom:3.75rem!important}.py-16{padding-top:4rem!important;padding-bottom:4rem!important}.py-17{padding-top:4.25rem!important;padding-bottom:4.25rem!important}.py-18{padding-top:4.5rem!important;padding-bottom:4.5rem!important}.py-19{padding-top:4.75rem!important;padding-bottom:4.75rem!important}.py-20{padding-top:5rem!important;padding-bottom:5rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:.75rem!important}.pt-4{padding-top:1rem!important}.pt-5{padding-top:1.25rem!important}.pt-6{padding-top:1.5rem!important}.pt-7{padding-top:1.75rem!important}.pt-8{padding-top:2rem!important}.pt-9{padding-top:2.25rem!important}.pt-10{padding-top:2.5rem!important}.pt-11{padding-top:2.75rem!important}.pt-12{padding-top:3rem!important}.pt-13{padding-top:3.25rem!important}.pt-14{padding-top:3.5rem!important}.pt-15{padding-top:3.75rem!important}.pt-16{padding-top:4rem!important}.pt-17{padding-top:4.25rem!important}.pt-18{padding-top:4.5rem!important}.pt-19{padding-top:4.75rem!important}.pt-20{padding-top:5rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:.75rem!important}.pe-4{padding-right:1rem!important}.pe-5{padding-right:1.25rem!important}.pe-6{padding-right:1.5rem!important}.pe-7{padding-right:1.75rem!important}.pe-8{padding-right:2rem!important}.pe-9{padding-right:2.25rem!important}.pe-10{padding-right:2.5rem!important}.pe-11{padding-right:2.75rem!important}.pe-12{padding-right:3rem!important}.pe-13{padding-right:3.25rem!important}.pe-14{padding-right:3.5rem!important}.pe-15{padding-right:3.75rem!important}.pe-16{padding-right:4rem!important}.pe-17{padding-right:4.25rem!important}.pe-18{padding-right:4.5rem!important}.pe-19{padding-right:4.75rem!important}.pe-20{padding-right:5rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:.75rem!important}.pb-4{padding-bottom:1rem!important}.pb-5{padding-bottom:1.25rem!important}.pb-6{padding-bottom:1.5rem!important}.pb-7{padding-bottom:1.75rem!important}.pb-8{padding-bottom:2rem!important}.pb-9{padding-bottom:2.25rem!important}.pb-10{padding-bottom:2.5rem!important}.pb-11{padding-bottom:2.75rem!important}.pb-12{padding-bottom:3rem!important}.pb-13{padding-bottom:3.25rem!important}.pb-14{padding-bottom:3.5rem!important}.pb-15{padding-bottom:3.75rem!important}.pb-16{padding-bottom:4rem!important}.pb-17{padding-bottom:4.25rem!important}.pb-18{padding-bottom:4.5rem!important}.pb-19{padding-bottom:4.75rem!important}.pb-20{padding-bottom:5rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:.75rem!important}.ps-4{padding-left:1rem!important}.ps-5{padding-left:1.25rem!important}.ps-6{padding-left:1.5rem!important}.ps-7{padding-left:1.75rem!important}.ps-8{padding-left:2rem!important}.ps-9{padding-left:2.25rem!important}.ps-10{padding-left:2.5rem!important}.ps-11{padding-left:2.75rem!important}.ps-12{padding-left:3rem!important}.ps-13{padding-left:3.25rem!important}.ps-14{padding-left:3.5rem!important}.ps-15{padding-left:3.75rem!important}.ps-16{padding-left:4rem!important}.ps-17{padding-left:4.25rem!important}.ps-18{padding-left:4.5rem!important}.ps-19{padding-left:4.75rem!important}.ps-20{padding-left:5rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:.75rem!important}.gap-4{gap:1rem!important}.gap-5{gap:1.25rem!important}.gap-6{gap:1.5rem!important}.gap-7{gap:1.75rem!important}.gap-8{gap:2rem!important}.gap-9{gap:2.25rem!important}.gap-10{gap:2.5rem!important}.gap-11{gap:2.75rem!important}.gap-12{gap:3rem!important}.gap-13{gap:3.25rem!important}.gap-14{gap:3.5rem!important}.gap-15{gap:3.75rem!important}.gap-16{gap:4rem!important}.gap-17{gap:4.25rem!important}.gap-18{gap:4.5rem!important}.gap-19{gap:4.75rem!important}.gap-20{gap:5rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:.75rem!important}.row-gap-4{row-gap:1rem!important}.row-gap-5{row-gap:1.25rem!important}.row-gap-6{row-gap:1.5rem!important}.row-gap-7{row-gap:1.75rem!important}.row-gap-8{row-gap:2rem!important}.row-gap-9{row-gap:2.25rem!important}.row-gap-10{row-gap:2.5rem!important}.row-gap-11{row-gap:2.75rem!important}.row-gap-12{row-gap:3rem!important}.row-gap-13{row-gap:3.25rem!important}.row-gap-14{row-gap:3.5rem!important}.row-gap-15{row-gap:3.75rem!important}.row-gap-16{row-gap:4rem!important}.row-gap-17{row-gap:4.25rem!important}.row-gap-18{row-gap:4.5rem!important}.row-gap-19{row-gap:4.75rem!important}.row-gap-20{row-gap:5rem!important}.column-gap-0{column-gap:0!important}.column-gap-1{column-gap:.25rem!important}.column-gap-2{column-gap:.5rem!important}.column-gap-3{column-gap:.75rem!important}.column-gap-4{column-gap:1rem!important}.column-gap-5{column-gap:1.25rem!important}.column-gap-6{column-gap:1.5rem!important}.column-gap-7{column-gap:1.75rem!important}.column-gap-8{column-gap:2rem!important}.column-gap-9{column-gap:2.25rem!important}.column-gap-10{column-gap:2.5rem!important}.column-gap-11{column-gap:2.75rem!important}.column-gap-12{column-gap:3rem!important}.column-gap-13{column-gap:3.25rem!important}.column-gap-14{column-gap:3.5rem!important}.column-gap-15{column-gap:3.75rem!important}.column-gap-16{column-gap:4rem!important}.column-gap-17{column-gap:4.25rem!important}.column-gap-18{column-gap:4.5rem!important}.column-gap-19{column-gap:4.75rem!important}.column-gap-20{column-gap:5rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.3rem + .6vw)!important}.fs-2{font-size:calc(1.275rem + .3vw)!important}.fs-3{font-size:calc(1.26rem + .12vw)!important}.fs-4{font-size:1.25rem!important}.fs-5{font-size:1.15rem!important}.fs-6{font-size:1.075rem!important}.fs-7{font-size:.95rem!important}.fs-8{font-size:.85rem!important}.fs-9{font-size:.75rem!important}.fs-10{font-size:.5rem!important}.fs-sm{font-size:.95rem!important}.fs-base{font-size:1rem!important}.fs-lg{font-size:1.075rem!important}.fs-xl{font-size:1.21rem!important}.fs-fluid{font-size:100%!important}.fs-2x{font-size:calc(1.325rem + .9vw)!important}.fs-2qx{font-size:calc(1.35rem + 1.2vw)!important}.fs-2hx{font-size:calc(1.375rem + 1.5vw)!important}.fs-2tx{font-size:calc(1.4rem + 1.8vw)!important}.fs-3x{font-size:calc(1.425rem + 2.1vw)!important}.fs-3qx{font-size:calc(1.45rem + 2.4vw)!important}.fs-3hx{font-size:calc(1.475rem + 2.7vw)!important}.fs-3tx{font-size:calc(1.5rem + 3vw)!important}.fs-4x{font-size:calc(1.525rem + 3.3vw)!important}.fs-4qx{font-size:calc(1.55rem + 3.6vw)!important}.fs-4hx{font-size:calc(1.575rem + 3.9vw)!important}.fs-4tx{font-size:calc(1.6rem + 4.2vw)!important}.fs-5x{font-size:calc(1.625rem + 4.5vw)!important}.fs-5qx{font-size:calc(1.65rem + 4.8vw)!important}.fs-5hx{font-size:calc(1.675rem + 5.1vw)!important}.fs-5tx{font-size:calc(1.7rem + 5.4vw)!important}.fs-6x{font-size:calc(1.725rem + 5.7vw)!important}.fs-6qx{font-size:calc(1.75rem + 6vw)!important}.fs-6hx{font-size:calc(1.775rem + 6.3vw)!important}.fs-6tx{font-size:calc(1.8rem + 6.6vw)!important}.fs-7x{font-size:calc(1.825rem + 6.9vw)!important}.fs-7qx{font-size:calc(1.85rem + 7.2vw)!important}.fs-7hx{font-size:calc(1.875rem + 7.5vw)!important}.fs-7tx{font-size:calc(1.9rem + 7.8vw)!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:500!important}.fw-bold{font-weight:600!important}.fw-bolder{font-weight:700!important}.lh-0{line-height:0!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:1.75!important}.lh-xl{line-height:2!important}.lh-xxl{line-height:2.25!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:.125em!important}.link-offset-1-hover:hover{text-underline-offset:.125em!important}.link-offset-2{text-underline-offset:.25em!important}.link-offset-2-hover:hover{text-underline-offset:.25em!important}.link-offset-3{text-underline-offset:.375em!important}.link-offset-3-hover:hover{text-underline-offset:.375em!important}.link-underline-light{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-primary{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{user-select:all!important}.user-select-auto{user-select:auto!important}.user-select-none{user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.475rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.425rem!important}.rounded-2{border-radius:.475rem!important}.rounded-3{border-radius:.625rem!important}.rounded-4{border-radius:1rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-index-n1{z-index:-1!important}.z-index-n2{z-index:-2!important}.z-index-0{z-index:0!important}.z-index-1{z-index:1!important}.z-index-2{z-index:2!important}.z-index-3{z-index:3!important}.cursor-help{cursor:help!important}.cursor-wait{cursor:wait!important}.cursor-crosshair{cursor:crosshair!important}.cursor-not-allowed{cursor:not-allowed!important}.cursor-zoom-in{cursor:zoom-in!important}.cursor-grab{cursor:grab!important}.cursor-pointer{cursor:pointer!important}.opacity-0{opacity:0!important}.opacity-0-hover:hover{opacity:0!important}.opacity-5{opacity:.05!important}.opacity-5-hover:hover{opacity:.05!important}.opacity-10{opacity:.1!important}.opacity-10-hover:hover{opacity:.1!important}.opacity-15{opacity:.15!important}.opacity-15-hover:hover{opacity:.15!important}.opacity-20{opacity:.2!important}.opacity-20-hover:hover{opacity:.2!important}.opacity-25{opacity:.25!important}.opacity-25-hover:hover{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-50-hover:hover{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-75-hover:hover{opacity:.75!important}.opacity-100{opacity:1!important}.opacity-100-hover:hover{opacity:1!important}.min-w-unset{min-width:unset!important}.min-w-25{min-width:25%!important}.min-w-50{min-width:50%!important}.min-w-75{min-width:75%!important}.min-w-100{min-width:100%!important}.min-w-auto{min-width:auto!important}.min-w-1px{min-width:1px!important}.min-w-2px{min-width:2px!important}.min-w-3px{min-width:3px!important}.min-w-4px{min-width:4px!important}.min-w-5px{min-width:5px!important}.min-w-6px{min-width:6px!important}.min-w-7px{min-width:7px!important}.min-w-8px{min-width:8px!important}.min-w-9px{min-width:9px!important}.min-w-10px{min-width:10px!important}.min-w-15px{min-width:15px!important}.min-w-20px{min-width:20px!important}.min-w-25px{min-width:25px!important}.min-w-30px{min-width:30px!important}.min-w-35px{min-width:35px!important}.min-w-40px{min-width:40px!important}.min-w-45px{min-width:45px!important}.min-w-50px{min-width:50px!important}.min-w-55px{min-width:55px!important}.min-w-60px{min-width:60px!important}.min-w-65px{min-width:65px!important}.min-w-70px{min-width:70px!important}.min-w-75px{min-width:75px!important}.min-w-80px{min-width:80px!important}.min-w-85px{min-width:85px!important}.min-w-90px{min-width:90px!important}.min-w-95px{min-width:95px!important}.min-w-100px{min-width:100px!important}.min-w-125px{min-width:125px!important}.min-w-150px{min-width:150px!important}.min-w-175px{min-width:175px!important}.min-w-200px{min-width:200px!important}.min-w-225px{min-width:225px!important}.min-w-250px{min-width:250px!important}.min-w-275px{min-width:275px!important}.min-w-300px{min-width:300px!important}.min-w-325px{min-width:325px!important}.min-w-350px{min-width:350px!important}.min-w-375px{min-width:375px!important}.min-w-400px{min-width:400px!important}.min-w-425px{min-width:425px!important}.min-w-450px{min-width:450px!important}.min-w-475px{min-width:475px!important}.min-w-500px{min-width:500px!important}.min-w-550px{min-width:550px!important}.min-w-600px{min-width:600px!important}.min-w-650px{min-width:650px!important}.min-w-700px{min-width:700px!important}.min-w-750px{min-width:750px!important}.min-w-800px{min-width:800px!important}.min-w-850px{min-width:850px!important}.min-w-900px{min-width:900px!important}.min-w-950px{min-width:950px!important}.min-w-1000px{min-width:1000px!important}.min-h-unset{min-height:unset!important}.min-h-25{min-height:25%!important}.min-h-50{min-height:50%!important}.min-h-75{min-height:75%!important}.min-h-100{min-height:100%!important}.min-h-auto{min-height:auto!important}.min-h-1px{min-height:1px!important}.min-h-2px{min-height:2px!important}.min-h-3px{min-height:3px!important}.min-h-4px{min-height:4px!important}.min-h-5px{min-height:5px!important}.min-h-6px{min-height:6px!important}.min-h-7px{min-height:7px!important}.min-h-8px{min-height:8px!important}.min-h-9px{min-height:9px!important}.min-h-10px{min-height:10px!important}.min-h-15px{min-height:15px!important}.min-h-20px{min-height:20px!important}.min-h-25px{min-height:25px!important}.min-h-30px{min-height:30px!important}.min-h-35px{min-height:35px!important}.min-h-40px{min-height:40px!important}.min-h-45px{min-height:45px!important}.min-h-50px{min-height:50px!important}.min-h-55px{min-height:55px!important}.min-h-60px{min-height:60px!important}.min-h-65px{min-height:65px!important}.min-h-70px{min-height:70px!important}.min-h-75px{min-height:75px!important}.min-h-80px{min-height:80px!important}.min-h-85px{min-height:85px!important}.min-h-90px{min-height:90px!important}.min-h-95px{min-height:95px!important}.min-h-100px{min-height:100px!important}.min-h-125px{min-height:125px!important}.min-h-150px{min-height:150px!important}.min-h-175px{min-height:175px!important}.min-h-200px{min-height:200px!important}.min-h-225px{min-height:225px!important}.min-h-250px{min-height:250px!important}.min-h-275px{min-height:275px!important}.min-h-300px{min-height:300px!important}.min-h-325px{min-height:325px!important}.min-h-350px{min-height:350px!important}.min-h-375px{min-height:375px!important}.min-h-400px{min-height:400px!important}.min-h-425px{min-height:425px!important}.min-h-450px{min-height:450px!important}.min-h-475px{min-height:475px!important}.min-h-500px{min-height:500px!important}.min-h-550px{min-height:550px!important}.min-h-600px{min-height:600px!important}.min-h-650px{min-height:650px!important}.min-h-700px{min-height:700px!important}.min-h-750px{min-height:750px!important}.min-h-800px{min-height:800px!important}.min-h-850px{min-height:850px!important}.min-h-900px{min-height:900px!important}.min-h-950px{min-height:950px!important}.min-h-1000px{min-height:1000px!important}.border-top-0{border-top-width:0!important}.border-top-1{border-top-width:1px!important}.border-top-2{border-top-width:2px!important}.border-top-3{border-top-width:3px!important}.border-top-4{border-top-width:4px!important}.border-top-5{border-top-width:5px!important}.border-bottom-0{border-bottom-width:0!important}.border-bottom-1{border-bottom-width:1px!important}.border-bottom-2{border-bottom-width:2px!important}.border-bottom-3{border-bottom-width:3px!important}.border-bottom-4{border-bottom-width:4px!important}.border-bottom-5{border-bottom-width:5px!important}.border-right-0{border-right-width:0!important}.border-right-1{border-right-width:1px!important}.border-right-2{border-right-width:2px!important}.border-right-3{border-right-width:3px!important}.border-right-4{border-right-width:4px!important}.border-right-5{border-right-width:5px!important}.border-left-0{border-left-width:0!important}.border-left-1{border-left-width:1px!important}.border-left-2{border-left-width:2px!important}.border-left-3{border-left-width:3px!important}.border-left-4{border-left-width:4px!important}.border-left-5{border-left-width:5px!important}.ls-1{letter-spacing:.1rem!important}.ls-2{letter-spacing:.115rem!important}.ls-3{letter-spacing:.125rem!important}.ls-4{letter-spacing:.25rem!important}.ls-5{letter-spacing:.5rem!important}.ls-n1{letter-spacing:-.1rem!important}.ls-n2{letter-spacing:-.115rem!important}.ls-n3{letter-spacing:-.125rem!important}.ls-n4{letter-spacing:-.25rem!important}.ls-n5{letter-spacing:-.5rem!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{object-fit:contain!important}.object-fit-sm-cover{object-fit:cover!important}.object-fit-sm-fill{object-fit:fill!important}.object-fit-sm-scale{object-fit:scale-down!important}.object-fit-sm-none{object-fit:none!important}.overflow-sm-auto{overflow:auto!important}.overflow-sm-hidden{overflow:hidden!important}.overflow-sm-visible{overflow:visible!important}.overflow-sm-scroll{overflow:scroll!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.position-sm-static{position:static!important}.position-sm-relative{position:relative!important}.position-sm-absolute{position:absolute!important}.position-sm-fixed{position:fixed!important}.position-sm-sticky{position:sticky!important}.w-sm-unset{width:unset!important}.w-sm-25{width:25%!important}.w-sm-50{width:50%!important}.w-sm-75{width:75%!important}.w-sm-100{width:100%!important}.w-sm-auto{width:auto!important}.w-sm-1px{width:1px!important}.w-sm-2px{width:2px!important}.w-sm-3px{width:3px!important}.w-sm-4px{width:4px!important}.w-sm-5px{width:5px!important}.w-sm-6px{width:6px!important}.w-sm-7px{width:7px!important}.w-sm-8px{width:8px!important}.w-sm-9px{width:9px!important}.w-sm-10px{width:10px!important}.w-sm-15px{width:15px!important}.w-sm-20px{width:20px!important}.w-sm-25px{width:25px!important}.w-sm-30px{width:30px!important}.w-sm-35px{width:35px!important}.w-sm-40px{width:40px!important}.w-sm-45px{width:45px!important}.w-sm-50px{width:50px!important}.w-sm-55px{width:55px!important}.w-sm-60px{width:60px!important}.w-sm-65px{width:65px!important}.w-sm-70px{width:70px!important}.w-sm-75px{width:75px!important}.w-sm-80px{width:80px!important}.w-sm-85px{width:85px!important}.w-sm-90px{width:90px!important}.w-sm-95px{width:95px!important}.w-sm-100px{width:100px!important}.w-sm-125px{width:125px!important}.w-sm-150px{width:150px!important}.w-sm-175px{width:175px!important}.w-sm-200px{width:200px!important}.w-sm-225px{width:225px!important}.w-sm-250px{width:250px!important}.w-sm-275px{width:275px!important}.w-sm-300px{width:300px!important}.w-sm-325px{width:325px!important}.w-sm-350px{width:350px!important}.w-sm-375px{width:375px!important}.w-sm-400px{width:400px!important}.w-sm-425px{width:425px!important}.w-sm-450px{width:450px!important}.w-sm-475px{width:475px!important}.w-sm-500px{width:500px!important}.w-sm-550px{width:550px!important}.w-sm-600px{width:600px!important}.w-sm-650px{width:650px!important}.w-sm-700px{width:700px!important}.w-sm-750px{width:750px!important}.w-sm-800px{width:800px!important}.w-sm-850px{width:850px!important}.w-sm-900px{width:900px!important}.w-sm-950px{width:950px!important}.w-sm-1000px{width:1000px!important}.mw-sm-unset{max-width:unset!important}.mw-sm-25{max-width:25%!important}.mw-sm-50{max-width:50%!important}.mw-sm-75{max-width:75%!important}.mw-sm-100{max-width:100%!important}.mw-sm-auto{max-width:auto!important}.mw-sm-1px{max-width:1px!important}.mw-sm-2px{max-width:2px!important}.mw-sm-3px{max-width:3px!important}.mw-sm-4px{max-width:4px!important}.mw-sm-5px{max-width:5px!important}.mw-sm-6px{max-width:6px!important}.mw-sm-7px{max-width:7px!important}.mw-sm-8px{max-width:8px!important}.mw-sm-9px{max-width:9px!important}.mw-sm-10px{max-width:10px!important}.mw-sm-15px{max-width:15px!important}.mw-sm-20px{max-width:20px!important}.mw-sm-25px{max-width:25px!important}.mw-sm-30px{max-width:30px!important}.mw-sm-35px{max-width:35px!important}.mw-sm-40px{max-width:40px!important}.mw-sm-45px{max-width:45px!important}.mw-sm-50px{max-width:50px!important}.mw-sm-55px{max-width:55px!important}.mw-sm-60px{max-width:60px!important}.mw-sm-65px{max-width:65px!important}.mw-sm-70px{max-width:70px!important}.mw-sm-75px{max-width:75px!important}.mw-sm-80px{max-width:80px!important}.mw-sm-85px{max-width:85px!important}.mw-sm-90px{max-width:90px!important}.mw-sm-95px{max-width:95px!important}.mw-sm-100px{max-width:100px!important}.mw-sm-125px{max-width:125px!important}.mw-sm-150px{max-width:150px!important}.mw-sm-175px{max-width:175px!important}.mw-sm-200px{max-width:200px!important}.mw-sm-225px{max-width:225px!important}.mw-sm-250px{max-width:250px!important}.mw-sm-275px{max-width:275px!important}.mw-sm-300px{max-width:300px!important}.mw-sm-325px{max-width:325px!important}.mw-sm-350px{max-width:350px!important}.mw-sm-375px{max-width:375px!important}.mw-sm-400px{max-width:400px!important}.mw-sm-425px{max-width:425px!important}.mw-sm-450px{max-width:450px!important}.mw-sm-475px{max-width:475px!important}.mw-sm-500px{max-width:500px!important}.mw-sm-550px{max-width:550px!important}.mw-sm-600px{max-width:600px!important}.mw-sm-650px{max-width:650px!important}.mw-sm-700px{max-width:700px!important}.mw-sm-750px{max-width:750px!important}.mw-sm-800px{max-width:800px!important}.mw-sm-850px{max-width:850px!important}.mw-sm-900px{max-width:900px!important}.mw-sm-950px{max-width:950px!important}.mw-sm-1000px{max-width:1000px!important}.h-sm-unset{height:unset!important}.h-sm-25{height:25%!important}.h-sm-50{height:50%!important}.h-sm-75{height:75%!important}.h-sm-100{height:100%!important}.h-sm-auto{height:auto!important}.h-sm-1px{height:1px!important}.h-sm-2px{height:2px!important}.h-sm-3px{height:3px!important}.h-sm-4px{height:4px!important}.h-sm-5px{height:5px!important}.h-sm-6px{height:6px!important}.h-sm-7px{height:7px!important}.h-sm-8px{height:8px!important}.h-sm-9px{height:9px!important}.h-sm-10px{height:10px!important}.h-sm-15px{height:15px!important}.h-sm-20px{height:20px!important}.h-sm-25px{height:25px!important}.h-sm-30px{height:30px!important}.h-sm-35px{height:35px!important}.h-sm-40px{height:40px!important}.h-sm-45px{height:45px!important}.h-sm-50px{height:50px!important}.h-sm-55px{height:55px!important}.h-sm-60px{height:60px!important}.h-sm-65px{height:65px!important}.h-sm-70px{height:70px!important}.h-sm-75px{height:75px!important}.h-sm-80px{height:80px!important}.h-sm-85px{height:85px!important}.h-sm-90px{height:90px!important}.h-sm-95px{height:95px!important}.h-sm-100px{height:100px!important}.h-sm-125px{height:125px!important}.h-sm-150px{height:150px!important}.h-sm-175px{height:175px!important}.h-sm-200px{height:200px!important}.h-sm-225px{height:225px!important}.h-sm-250px{height:250px!important}.h-sm-275px{height:275px!important}.h-sm-300px{height:300px!important}.h-sm-325px{height:325px!important}.h-sm-350px{height:350px!important}.h-sm-375px{height:375px!important}.h-sm-400px{height:400px!important}.h-sm-425px{height:425px!important}.h-sm-450px{height:450px!important}.h-sm-475px{height:475px!important}.h-sm-500px{height:500px!important}.h-sm-550px{height:550px!important}.h-sm-600px{height:600px!important}.h-sm-650px{height:650px!important}.h-sm-700px{height:700px!important}.h-sm-750px{height:750px!important}.h-sm-800px{height:800px!important}.h-sm-850px{height:850px!important}.h-sm-900px{height:900px!important}.h-sm-950px{height:950px!important}.h-sm-1000px{height:1000px!important}.mh-sm-unset{max-height:unset!important}.mh-sm-25{max-height:25%!important}.mh-sm-50{max-height:50%!important}.mh-sm-75{max-height:75%!important}.mh-sm-100{max-height:100%!important}.mh-sm-auto{max-height:auto!important}.mh-sm-1px{max-height:1px!important}.mh-sm-2px{max-height:2px!important}.mh-sm-3px{max-height:3px!important}.mh-sm-4px{max-height:4px!important}.mh-sm-5px{max-height:5px!important}.mh-sm-6px{max-height:6px!important}.mh-sm-7px{max-height:7px!important}.mh-sm-8px{max-height:8px!important}.mh-sm-9px{max-height:9px!important}.mh-sm-10px{max-height:10px!important}.mh-sm-15px{max-height:15px!important}.mh-sm-20px{max-height:20px!important}.mh-sm-25px{max-height:25px!important}.mh-sm-30px{max-height:30px!important}.mh-sm-35px{max-height:35px!important}.mh-sm-40px{max-height:40px!important}.mh-sm-45px{max-height:45px!important}.mh-sm-50px{max-height:50px!important}.mh-sm-55px{max-height:55px!important}.mh-sm-60px{max-height:60px!important}.mh-sm-65px{max-height:65px!important}.mh-sm-70px{max-height:70px!important}.mh-sm-75px{max-height:75px!important}.mh-sm-80px{max-height:80px!important}.mh-sm-85px{max-height:85px!important}.mh-sm-90px{max-height:90px!important}.mh-sm-95px{max-height:95px!important}.mh-sm-100px{max-height:100px!important}.mh-sm-125px{max-height:125px!important}.mh-sm-150px{max-height:150px!important}.mh-sm-175px{max-height:175px!important}.mh-sm-200px{max-height:200px!important}.mh-sm-225px{max-height:225px!important}.mh-sm-250px{max-height:250px!important}.mh-sm-275px{max-height:275px!important}.mh-sm-300px{max-height:300px!important}.mh-sm-325px{max-height:325px!important}.mh-sm-350px{max-height:350px!important}.mh-sm-375px{max-height:375px!important}.mh-sm-400px{max-height:400px!important}.mh-sm-425px{max-height:425px!important}.mh-sm-450px{max-height:450px!important}.mh-sm-475px{max-height:475px!important}.mh-sm-500px{max-height:500px!important}.mh-sm-550px{max-height:550px!important}.mh-sm-600px{max-height:600px!important}.mh-sm-650px{max-height:650px!important}.mh-sm-700px{max-height:700px!important}.mh-sm-750px{max-height:750px!important}.mh-sm-800px{max-height:800px!important}.mh-sm-850px{max-height:850px!important}.mh-sm-900px{max-height:900px!important}.mh-sm-950px{max-height:950px!important}.mh-sm-1000px{max-height:1000px!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:.75rem!important}.m-sm-4{margin:1rem!important}.m-sm-5{margin:1.25rem!important}.m-sm-6{margin:1.5rem!important}.m-sm-7{margin:1.75rem!important}.m-sm-8{margin:2rem!important}.m-sm-9{margin:2.25rem!important}.m-sm-10{margin:2.5rem!important}.m-sm-11{margin:2.75rem!important}.m-sm-12{margin:3rem!important}.m-sm-13{margin:3.25rem!important}.m-sm-14{margin:3.5rem!important}.m-sm-15{margin:3.75rem!important}.m-sm-16{margin:4rem!important}.m-sm-17{margin:4.25rem!important}.m-sm-18{margin:4.5rem!important}.m-sm-19{margin:4.75rem!important}.m-sm-20{margin:5rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:.75rem!important;margin-left:.75rem!important}.mx-sm-4{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-5{margin-right:1.25rem!important;margin-left:1.25rem!important}.mx-sm-6{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-7{margin-right:1.75rem!important;margin-left:1.75rem!important}.mx-sm-8{margin-right:2rem!important;margin-left:2rem!important}.mx-sm-9{margin-right:2.25rem!important;margin-left:2.25rem!important}.mx-sm-10{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-sm-11{margin-right:2.75rem!important;margin-left:2.75rem!important}.mx-sm-12{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-13{margin-right:3.25rem!important;margin-left:3.25rem!important}.mx-sm-14{margin-right:3.5rem!important;margin-left:3.5rem!important}.mx-sm-15{margin-right:3.75rem!important;margin-left:3.75rem!important}.mx-sm-16{margin-right:4rem!important;margin-left:4rem!important}.mx-sm-17{margin-right:4.25rem!important;margin-left:4.25rem!important}.mx-sm-18{margin-right:4.5rem!important;margin-left:4.5rem!important}.mx-sm-19{margin-right:4.75rem!important;margin-left:4.75rem!important}.mx-sm-20{margin-right:5rem!important;margin-left:5rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:.75rem!important;margin-bottom:.75rem!important}.my-sm-4{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-5{margin-top:1.25rem!important;margin-bottom:1.25rem!important}.my-sm-6{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-7{margin-top:1.75rem!important;margin-bottom:1.75rem!important}.my-sm-8{margin-top:2rem!important;margin-bottom:2rem!important}.my-sm-9{margin-top:2.25rem!important;margin-bottom:2.25rem!important}.my-sm-10{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-sm-11{margin-top:2.75rem!important;margin-bottom:2.75rem!important}.my-sm-12{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-13{margin-top:3.25rem!important;margin-bottom:3.25rem!important}.my-sm-14{margin-top:3.5rem!important;margin-bottom:3.5rem!important}.my-sm-15{margin-top:3.75rem!important;margin-bottom:3.75rem!important}.my-sm-16{margin-top:4rem!important;margin-bottom:4rem!important}.my-sm-17{margin-top:4.25rem!important;margin-bottom:4.25rem!important}.my-sm-18{margin-top:4.5rem!important;margin-bottom:4.5rem!important}.my-sm-19{margin-top:4.75rem!important;margin-bottom:4.75rem!important}.my-sm-20{margin-top:5rem!important;margin-bottom:5rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:.75rem!important}.mt-sm-4{margin-top:1rem!important}.mt-sm-5{margin-top:1.25rem!important}.mt-sm-6{margin-top:1.5rem!important}.mt-sm-7{margin-top:1.75rem!important}.mt-sm-8{margin-top:2rem!important}.mt-sm-9{margin-top:2.25rem!important}.mt-sm-10{margin-top:2.5rem!important}.mt-sm-11{margin-top:2.75rem!important}.mt-sm-12{margin-top:3rem!important}.mt-sm-13{margin-top:3.25rem!important}.mt-sm-14{margin-top:3.5rem!important}.mt-sm-15{margin-top:3.75rem!important}.mt-sm-16{margin-top:4rem!important}.mt-sm-17{margin-top:4.25rem!important}.mt-sm-18{margin-top:4.5rem!important}.mt-sm-19{margin-top:4.75rem!important}.mt-sm-20{margin-top:5rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:.75rem!important}.me-sm-4{margin-right:1rem!important}.me-sm-5{margin-right:1.25rem!important}.me-sm-6{margin-right:1.5rem!important}.me-sm-7{margin-right:1.75rem!important}.me-sm-8{margin-right:2rem!important}.me-sm-9{margin-right:2.25rem!important}.me-sm-10{margin-right:2.5rem!important}.me-sm-11{margin-right:2.75rem!important}.me-sm-12{margin-right:3rem!important}.me-sm-13{margin-right:3.25rem!important}.me-sm-14{margin-right:3.5rem!important}.me-sm-15{margin-right:3.75rem!important}.me-sm-16{margin-right:4rem!important}.me-sm-17{margin-right:4.25rem!important}.me-sm-18{margin-right:4.5rem!important}.me-sm-19{margin-right:4.75rem!important}.me-sm-20{margin-right:5rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:.75rem!important}.mb-sm-4{margin-bottom:1rem!important}.mb-sm-5{margin-bottom:1.25rem!important}.mb-sm-6{margin-bottom:1.5rem!important}.mb-sm-7{margin-bottom:1.75rem!important}.mb-sm-8{margin-bottom:2rem!important}.mb-sm-9{margin-bottom:2.25rem!important}.mb-sm-10{margin-bottom:2.5rem!important}.mb-sm-11{margin-bottom:2.75rem!important}.mb-sm-12{margin-bottom:3rem!important}.mb-sm-13{margin-bottom:3.25rem!important}.mb-sm-14{margin-bottom:3.5rem!important}.mb-sm-15{margin-bottom:3.75rem!important}.mb-sm-16{margin-bottom:4rem!important}.mb-sm-17{margin-bottom:4.25rem!important}.mb-sm-18{margin-bottom:4.5rem!important}.mb-sm-19{margin-bottom:4.75rem!important}.mb-sm-20{margin-bottom:5rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:.75rem!important}.ms-sm-4{margin-left:1rem!important}.ms-sm-5{margin-left:1.25rem!important}.ms-sm-6{margin-left:1.5rem!important}.ms-sm-7{margin-left:1.75rem!important}.ms-sm-8{margin-left:2rem!important}.ms-sm-9{margin-left:2.25rem!important}.ms-sm-10{margin-left:2.5rem!important}.ms-sm-11{margin-left:2.75rem!important}.ms-sm-12{margin-left:3rem!important}.ms-sm-13{margin-left:3.25rem!important}.ms-sm-14{margin-left:3.5rem!important}.ms-sm-15{margin-left:3.75rem!important}.ms-sm-16{margin-left:4rem!important}.ms-sm-17{margin-left:4.25rem!important}.ms-sm-18{margin-left:4.5rem!important}.ms-sm-19{margin-left:4.75rem!important}.ms-sm-20{margin-left:5rem!important}.ms-sm-auto{margin-left:auto!important}.m-sm-n1{margin:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.m-sm-n3{margin:-.75rem!important}.m-sm-n4{margin:-1rem!important}.m-sm-n5{margin:-1.25rem!important}.m-sm-n6{margin:-1.5rem!important}.m-sm-n7{margin:-1.75rem!important}.m-sm-n8{margin:-2rem!important}.m-sm-n9{margin:-2.25rem!important}.m-sm-n10{margin:-2.5rem!important}.m-sm-n11{margin:-2.75rem!important}.m-sm-n12{margin:-3rem!important}.m-sm-n13{margin:-3.25rem!important}.m-sm-n14{margin:-3.5rem!important}.m-sm-n15{margin:-3.75rem!important}.m-sm-n16{margin:-4rem!important}.m-sm-n17{margin:-4.25rem!important}.m-sm-n18{margin:-4.5rem!important}.m-sm-n19{margin:-4.75rem!important}.m-sm-n20{margin:-5rem!important}.mx-sm-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-sm-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-sm-n3{margin-right:-.75rem!important;margin-left:-.75rem!important}.mx-sm-n4{margin-right:-1rem!important;margin-left:-1rem!important}.mx-sm-n5{margin-right:-1.25rem!important;margin-left:-1.25rem!important}.mx-sm-n6{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-sm-n7{margin-right:-1.75rem!important;margin-left:-1.75rem!important}.mx-sm-n8{margin-right:-2rem!important;margin-left:-2rem!important}.mx-sm-n9{margin-right:-2.25rem!important;margin-left:-2.25rem!important}.mx-sm-n10{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.mx-sm-n11{margin-right:-2.75rem!important;margin-left:-2.75rem!important}.mx-sm-n12{margin-right:-3rem!important;margin-left:-3rem!important}.mx-sm-n13{margin-right:-3.25rem!important;margin-left:-3.25rem!important}.mx-sm-n14{margin-right:-3.5rem!important;margin-left:-3.5rem!important}.mx-sm-n15{margin-right:-3.75rem!important;margin-left:-3.75rem!important}.mx-sm-n16{margin-right:-4rem!important;margin-left:-4rem!important}.mx-sm-n17{margin-right:-4.25rem!important;margin-left:-4.25rem!important}.mx-sm-n18{margin-right:-4.5rem!important;margin-left:-4.5rem!important}.mx-sm-n19{margin-right:-4.75rem!important;margin-left:-4.75rem!important}.mx-sm-n20{margin-right:-5rem!important;margin-left:-5rem!important}.my-sm-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-sm-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-sm-n3{margin-top:-.75rem!important;margin-bottom:-.75rem!important}.my-sm-n4{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-sm-n5{margin-top:-1.25rem!important;margin-bottom:-1.25rem!important}.my-sm-n6{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-sm-n7{margin-top:-1.75rem!important;margin-bottom:-1.75rem!important}.my-sm-n8{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-sm-n9{margin-top:-2.25rem!important;margin-bottom:-2.25rem!important}.my-sm-n10{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.my-sm-n11{margin-top:-2.75rem!important;margin-bottom:-2.75rem!important}.my-sm-n12{margin-top:-3rem!important;margin-bottom:-3rem!important}.my-sm-n13{margin-top:-3.25rem!important;margin-bottom:-3.25rem!important}.my-sm-n14{margin-top:-3.5rem!important;margin-bottom:-3.5rem!important}.my-sm-n15{margin-top:-3.75rem!important;margin-bottom:-3.75rem!important}.my-sm-n16{margin-top:-4rem!important;margin-bottom:-4rem!important}.my-sm-n17{margin-top:-4.25rem!important;margin-bottom:-4.25rem!important}.my-sm-n18{margin-top:-4.5rem!important;margin-bottom:-4.5rem!important}.my-sm-n19{margin-top:-4.75rem!important;margin-bottom:-4.75rem!important}.my-sm-n20{margin-top:-5rem!important;margin-bottom:-5rem!important}.mt-sm-n1{margin-top:-.25rem!important}.mt-sm-n2{margin-top:-.5rem!important}.mt-sm-n3{margin-top:-.75rem!important}.mt-sm-n4{margin-top:-1rem!important}.mt-sm-n5{margin-top:-1.25rem!important}.mt-sm-n6{margin-top:-1.5rem!important}.mt-sm-n7{margin-top:-1.75rem!important}.mt-sm-n8{margin-top:-2rem!important}.mt-sm-n9{margin-top:-2.25rem!important}.mt-sm-n10{margin-top:-2.5rem!important}.mt-sm-n11{margin-top:-2.75rem!important}.mt-sm-n12{margin-top:-3rem!important}.mt-sm-n13{margin-top:-3.25rem!important}.mt-sm-n14{margin-top:-3.5rem!important}.mt-sm-n15{margin-top:-3.75rem!important}.mt-sm-n16{margin-top:-4rem!important}.mt-sm-n17{margin-top:-4.25rem!important}.mt-sm-n18{margin-top:-4.5rem!important}.mt-sm-n19{margin-top:-4.75rem!important}.mt-sm-n20{margin-top:-5rem!important}.me-sm-n1{margin-right:-.25rem!important}.me-sm-n2{margin-right:-.5rem!important}.me-sm-n3{margin-right:-.75rem!important}.me-sm-n4{margin-right:-1rem!important}.me-sm-n5{margin-right:-1.25rem!important}.me-sm-n6{margin-right:-1.5rem!important}.me-sm-n7{margin-right:-1.75rem!important}.me-sm-n8{margin-right:-2rem!important}.me-sm-n9{margin-right:-2.25rem!important}.me-sm-n10{margin-right:-2.5rem!important}.me-sm-n11{margin-right:-2.75rem!important}.me-sm-n12{margin-right:-3rem!important}.me-sm-n13{margin-right:-3.25rem!important}.me-sm-n14{margin-right:-3.5rem!important}.me-sm-n15{margin-right:-3.75rem!important}.me-sm-n16{margin-right:-4rem!important}.me-sm-n17{margin-right:-4.25rem!important}.me-sm-n18{margin-right:-4.5rem!important}.me-sm-n19{margin-right:-4.75rem!important}.me-sm-n20{margin-right:-5rem!important}.mb-sm-n1{margin-bottom:-.25rem!important}.mb-sm-n2{margin-bottom:-.5rem!important}.mb-sm-n3{margin-bottom:-.75rem!important}.mb-sm-n4{margin-bottom:-1rem!important}.mb-sm-n5{margin-bottom:-1.25rem!important}.mb-sm-n6{margin-bottom:-1.5rem!important}.mb-sm-n7{margin-bottom:-1.75rem!important}.mb-sm-n8{margin-bottom:-2rem!important}.mb-sm-n9{margin-bottom:-2.25rem!important}.mb-sm-n10{margin-bottom:-2.5rem!important}.mb-sm-n11{margin-bottom:-2.75rem!important}.mb-sm-n12{margin-bottom:-3rem!important}.mb-sm-n13{margin-bottom:-3.25rem!important}.mb-sm-n14{margin-bottom:-3.5rem!important}.mb-sm-n15{margin-bottom:-3.75rem!important}.mb-sm-n16{margin-bottom:-4rem!important}.mb-sm-n17{margin-bottom:-4.25rem!important}.mb-sm-n18{margin-bottom:-4.5rem!important}.mb-sm-n19{margin-bottom:-4.75rem!important}.mb-sm-n20{margin-bottom:-5rem!important}.ms-sm-n1{margin-left:-.25rem!important}.ms-sm-n2{margin-left:-.5rem!important}.ms-sm-n3{margin-left:-.75rem!important}.ms-sm-n4{margin-left:-1rem!important}.ms-sm-n5{margin-left:-1.25rem!important}.ms-sm-n6{margin-left:-1.5rem!important}.ms-sm-n7{margin-left:-1.75rem!important}.ms-sm-n8{margin-left:-2rem!important}.ms-sm-n9{margin-left:-2.25rem!important}.ms-sm-n10{margin-left:-2.5rem!important}.ms-sm-n11{margin-left:-2.75rem!important}.ms-sm-n12{margin-left:-3rem!important}.ms-sm-n13{margin-left:-3.25rem!important}.ms-sm-n14{margin-left:-3.5rem!important}.ms-sm-n15{margin-left:-3.75rem!important}.ms-sm-n16{margin-left:-4rem!important}.ms-sm-n17{margin-left:-4.25rem!important}.ms-sm-n18{margin-left:-4.5rem!important}.ms-sm-n19{margin-left:-4.75rem!important}.ms-sm-n20{margin-left:-5rem!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:.75rem!important}.p-sm-4{padding:1rem!important}.p-sm-5{padding:1.25rem!important}.p-sm-6{padding:1.5rem!important}.p-sm-7{padding:1.75rem!important}.p-sm-8{padding:2rem!important}.p-sm-9{padding:2.25rem!important}.p-sm-10{padding:2.5rem!important}.p-sm-11{padding:2.75rem!important}.p-sm-12{padding:3rem!important}.p-sm-13{padding:3.25rem!important}.p-sm-14{padding:3.5rem!important}.p-sm-15{padding:3.75rem!important}.p-sm-16{padding:4rem!important}.p-sm-17{padding:4.25rem!important}.p-sm-18{padding:4.5rem!important}.p-sm-19{padding:4.75rem!important}.p-sm-20{padding:5rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:.75rem!important;padding-left:.75rem!important}.px-sm-4{padding-right:1rem!important;padding-left:1rem!important}.px-sm-5{padding-right:1.25rem!important;padding-left:1.25rem!important}.px-sm-6{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-7{padding-right:1.75rem!important;padding-left:1.75rem!important}.px-sm-8{padding-right:2rem!important;padding-left:2rem!important}.px-sm-9{padding-right:2.25rem!important;padding-left:2.25rem!important}.px-sm-10{padding-right:2.5rem!important;padding-left:2.5rem!important}.px-sm-11{padding-right:2.75rem!important;padding-left:2.75rem!important}.px-sm-12{padding-right:3rem!important;padding-left:3rem!important}.px-sm-13{padding-right:3.25rem!important;padding-left:3.25rem!important}.px-sm-14{padding-right:3.5rem!important;padding-left:3.5rem!important}.px-sm-15{padding-right:3.75rem!important;padding-left:3.75rem!important}.px-sm-16{padding-right:4rem!important;padding-left:4rem!important}.px-sm-17{padding-right:4.25rem!important;padding-left:4.25rem!important}.px-sm-18{padding-right:4.5rem!important;padding-left:4.5rem!important}.px-sm-19{padding-right:4.75rem!important;padding-left:4.75rem!important}.px-sm-20{padding-right:5rem!important;padding-left:5rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:.75rem!important;padding-bottom:.75rem!important}.py-sm-4{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-5{padding-top:1.25rem!important;padding-bottom:1.25rem!important}.py-sm-6{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-7{padding-top:1.75rem!important;padding-bottom:1.75rem!important}.py-sm-8{padding-top:2rem!important;padding-bottom:2rem!important}.py-sm-9{padding-top:2.25rem!important;padding-bottom:2.25rem!important}.py-sm-10{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.py-sm-11{padding-top:2.75rem!important;padding-bottom:2.75rem!important}.py-sm-12{padding-top:3rem!important;padding-bottom:3rem!important}.py-sm-13{padding-top:3.25rem!important;padding-bottom:3.25rem!important}.py-sm-14{padding-top:3.5rem!important;padding-bottom:3.5rem!important}.py-sm-15{padding-top:3.75rem!important;padding-bottom:3.75rem!important}.py-sm-16{padding-top:4rem!important;padding-bottom:4rem!important}.py-sm-17{padding-top:4.25rem!important;padding-bottom:4.25rem!important}.py-sm-18{padding-top:4.5rem!important;padding-bottom:4.5rem!important}.py-sm-19{padding-top:4.75rem!important;padding-bottom:4.75rem!important}.py-sm-20{padding-top:5rem!important;padding-bottom:5rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:.75rem!important}.pt-sm-4{padding-top:1rem!important}.pt-sm-5{padding-top:1.25rem!important}.pt-sm-6{padding-top:1.5rem!important}.pt-sm-7{padding-top:1.75rem!important}.pt-sm-8{padding-top:2rem!important}.pt-sm-9{padding-top:2.25rem!important}.pt-sm-10{padding-top:2.5rem!important}.pt-sm-11{padding-top:2.75rem!important}.pt-sm-12{padding-top:3rem!important}.pt-sm-13{padding-top:3.25rem!important}.pt-sm-14{padding-top:3.5rem!important}.pt-sm-15{padding-top:3.75rem!important}.pt-sm-16{padding-top:4rem!important}.pt-sm-17{padding-top:4.25rem!important}.pt-sm-18{padding-top:4.5rem!important}.pt-sm-19{padding-top:4.75rem!important}.pt-sm-20{padding-top:5rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:.75rem!important}.pe-sm-4{padding-right:1rem!important}.pe-sm-5{padding-right:1.25rem!important}.pe-sm-6{padding-right:1.5rem!important}.pe-sm-7{padding-right:1.75rem!important}.pe-sm-8{padding-right:2rem!important}.pe-sm-9{padding-right:2.25rem!important}.pe-sm-10{padding-right:2.5rem!important}.pe-sm-11{padding-right:2.75rem!important}.pe-sm-12{padding-right:3rem!important}.pe-sm-13{padding-right:3.25rem!important}.pe-sm-14{padding-right:3.5rem!important}.pe-sm-15{padding-right:3.75rem!important}.pe-sm-16{padding-right:4rem!important}.pe-sm-17{padding-right:4.25rem!important}.pe-sm-18{padding-right:4.5rem!important}.pe-sm-19{padding-right:4.75rem!important}.pe-sm-20{padding-right:5rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:.75rem!important}.pb-sm-4{padding-bottom:1rem!important}.pb-sm-5{padding-bottom:1.25rem!important}.pb-sm-6{padding-bottom:1.5rem!important}.pb-sm-7{padding-bottom:1.75rem!important}.pb-sm-8{padding-bottom:2rem!important}.pb-sm-9{padding-bottom:2.25rem!important}.pb-sm-10{padding-bottom:2.5rem!important}.pb-sm-11{padding-bottom:2.75rem!important}.pb-sm-12{padding-bottom:3rem!important}.pb-sm-13{padding-bottom:3.25rem!important}.pb-sm-14{padding-bottom:3.5rem!important}.pb-sm-15{padding-bottom:3.75rem!important}.pb-sm-16{padding-bottom:4rem!important}.pb-sm-17{padding-bottom:4.25rem!important}.pb-sm-18{padding-bottom:4.5rem!important}.pb-sm-19{padding-bottom:4.75rem!important}.pb-sm-20{padding-bottom:5rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:.75rem!important}.ps-sm-4{padding-left:1rem!important}.ps-sm-5{padding-left:1.25rem!important}.ps-sm-6{padding-left:1.5rem!important}.ps-sm-7{padding-left:1.75rem!important}.ps-sm-8{padding-left:2rem!important}.ps-sm-9{padding-left:2.25rem!important}.ps-sm-10{padding-left:2.5rem!important}.ps-sm-11{padding-left:2.75rem!important}.ps-sm-12{padding-left:3rem!important}.ps-sm-13{padding-left:3.25rem!important}.ps-sm-14{padding-left:3.5rem!important}.ps-sm-15{padding-left:3.75rem!important}.ps-sm-16{padding-left:4rem!important}.ps-sm-17{padding-left:4.25rem!important}.ps-sm-18{padding-left:4.5rem!important}.ps-sm-19{padding-left:4.75rem!important}.ps-sm-20{padding-left:5rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:.75rem!important}.gap-sm-4{gap:1rem!important}.gap-sm-5{gap:1.25rem!important}.gap-sm-6{gap:1.5rem!important}.gap-sm-7{gap:1.75rem!important}.gap-sm-8{gap:2rem!important}.gap-sm-9{gap:2.25rem!important}.gap-sm-10{gap:2.5rem!important}.gap-sm-11{gap:2.75rem!important}.gap-sm-12{gap:3rem!important}.gap-sm-13{gap:3.25rem!important}.gap-sm-14{gap:3.5rem!important}.gap-sm-15{gap:3.75rem!important}.gap-sm-16{gap:4rem!important}.gap-sm-17{gap:4.25rem!important}.gap-sm-18{gap:4.5rem!important}.gap-sm-19{gap:4.75rem!important}.gap-sm-20{gap:5rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:.75rem!important}.row-gap-sm-4{row-gap:1rem!important}.row-gap-sm-5{row-gap:1.25rem!important}.row-gap-sm-6{row-gap:1.5rem!important}.row-gap-sm-7{row-gap:1.75rem!important}.row-gap-sm-8{row-gap:2rem!important}.row-gap-sm-9{row-gap:2.25rem!important}.row-gap-sm-10{row-gap:2.5rem!important}.row-gap-sm-11{row-gap:2.75rem!important}.row-gap-sm-12{row-gap:3rem!important}.row-gap-sm-13{row-gap:3.25rem!important}.row-gap-sm-14{row-gap:3.5rem!important}.row-gap-sm-15{row-gap:3.75rem!important}.row-gap-sm-16{row-gap:4rem!important}.row-gap-sm-17{row-gap:4.25rem!important}.row-gap-sm-18{row-gap:4.5rem!important}.row-gap-sm-19{row-gap:4.75rem!important}.row-gap-sm-20{row-gap:5rem!important}.column-gap-sm-0{column-gap:0!important}.column-gap-sm-1{column-gap:.25rem!important}.column-gap-sm-2{column-gap:.5rem!important}.column-gap-sm-3{column-gap:.75rem!important}.column-gap-sm-4{column-gap:1rem!important}.column-gap-sm-5{column-gap:1.25rem!important}.column-gap-sm-6{column-gap:1.5rem!important}.column-gap-sm-7{column-gap:1.75rem!important}.column-gap-sm-8{column-gap:2rem!important}.column-gap-sm-9{column-gap:2.25rem!important}.column-gap-sm-10{column-gap:2.5rem!important}.column-gap-sm-11{column-gap:2.75rem!important}.column-gap-sm-12{column-gap:3rem!important}.column-gap-sm-13{column-gap:3.25rem!important}.column-gap-sm-14{column-gap:3.5rem!important}.column-gap-sm-15{column-gap:3.75rem!important}.column-gap-sm-16{column-gap:4rem!important}.column-gap-sm-17{column-gap:4.25rem!important}.column-gap-sm-18{column-gap:4.5rem!important}.column-gap-sm-19{column-gap:4.75rem!important}.column-gap-sm-20{column-gap:5rem!important}.fs-sm-1{font-size:calc(1.3rem + .6vw)!important}.fs-sm-2{font-size:calc(1.275rem + .3vw)!important}.fs-sm-3{font-size:calc(1.26rem + .12vw)!important}.fs-sm-4{font-size:1.25rem!important}.fs-sm-5{font-size:1.15rem!important}.fs-sm-6{font-size:1.075rem!important}.fs-sm-7{font-size:.95rem!important}.fs-sm-8{font-size:.85rem!important}.fs-sm-9{font-size:.75rem!important}.fs-sm-10{font-size:.5rem!important}.fs-sm-sm{font-size:.95rem!important}.fs-sm-base{font-size:1rem!important}.fs-sm-lg{font-size:1.075rem!important}.fs-sm-xl{font-size:1.21rem!important}.fs-sm-fluid{font-size:100%!important}.fs-sm-2x{font-size:calc(1.325rem + .9vw)!important}.fs-sm-2qx{font-size:calc(1.35rem + 1.2vw)!important}.fs-sm-2hx{font-size:calc(1.375rem + 1.5vw)!important}.fs-sm-2tx{font-size:calc(1.4rem + 1.8vw)!important}.fs-sm-3x{font-size:calc(1.425rem + 2.1vw)!important}.fs-sm-3qx{font-size:calc(1.45rem + 2.4vw)!important}.fs-sm-3hx{font-size:calc(1.475rem + 2.7vw)!important}.fs-sm-3tx{font-size:calc(1.5rem + 3vw)!important}.fs-sm-4x{font-size:calc(1.525rem + 3.3vw)!important}.fs-sm-4qx{font-size:calc(1.55rem + 3.6vw)!important}.fs-sm-4hx{font-size:calc(1.575rem + 3.9vw)!important}.fs-sm-4tx{font-size:calc(1.6rem + 4.2vw)!important}.fs-sm-5x{font-size:calc(1.625rem + 4.5vw)!important}.fs-sm-5qx{font-size:calc(1.65rem + 4.8vw)!important}.fs-sm-5hx{font-size:calc(1.675rem + 5.1vw)!important}.fs-sm-5tx{font-size:calc(1.7rem + 5.4vw)!important}.fs-sm-6x{font-size:calc(1.725rem + 5.7vw)!important}.fs-sm-6qx{font-size:calc(1.75rem + 6vw)!important}.fs-sm-6hx{font-size:calc(1.775rem + 6.3vw)!important}.fs-sm-6tx{font-size:calc(1.8rem + 6.6vw)!important}.fs-sm-7x{font-size:calc(1.825rem + 6.9vw)!important}.fs-sm-7qx{font-size:calc(1.85rem + 7.2vw)!important}.fs-sm-7hx{font-size:calc(1.875rem + 7.5vw)!important}.fs-sm-7tx{font-size:calc(1.9rem + 7.8vw)!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}.min-w-sm-unset{min-width:unset!important}.min-w-sm-25{min-width:25%!important}.min-w-sm-50{min-width:50%!important}.min-w-sm-75{min-width:75%!important}.min-w-sm-100{min-width:100%!important}.min-w-sm-auto{min-width:auto!important}.min-w-sm-1px{min-width:1px!important}.min-w-sm-2px{min-width:2px!important}.min-w-sm-3px{min-width:3px!important}.min-w-sm-4px{min-width:4px!important}.min-w-sm-5px{min-width:5px!important}.min-w-sm-6px{min-width:6px!important}.min-w-sm-7px{min-width:7px!important}.min-w-sm-8px{min-width:8px!important}.min-w-sm-9px{min-width:9px!important}.min-w-sm-10px{min-width:10px!important}.min-w-sm-15px{min-width:15px!important}.min-w-sm-20px{min-width:20px!important}.min-w-sm-25px{min-width:25px!important}.min-w-sm-30px{min-width:30px!important}.min-w-sm-35px{min-width:35px!important}.min-w-sm-40px{min-width:40px!important}.min-w-sm-45px{min-width:45px!important}.min-w-sm-50px{min-width:50px!important}.min-w-sm-55px{min-width:55px!important}.min-w-sm-60px{min-width:60px!important}.min-w-sm-65px{min-width:65px!important}.min-w-sm-70px{min-width:70px!important}.min-w-sm-75px{min-width:75px!important}.min-w-sm-80px{min-width:80px!important}.min-w-sm-85px{min-width:85px!important}.min-w-sm-90px{min-width:90px!important}.min-w-sm-95px{min-width:95px!important}.min-w-sm-100px{min-width:100px!important}.min-w-sm-125px{min-width:125px!important}.min-w-sm-150px{min-width:150px!important}.min-w-sm-175px{min-width:175px!important}.min-w-sm-200px{min-width:200px!important}.min-w-sm-225px{min-width:225px!important}.min-w-sm-250px{min-width:250px!important}.min-w-sm-275px{min-width:275px!important}.min-w-sm-300px{min-width:300px!important}.min-w-sm-325px{min-width:325px!important}.min-w-sm-350px{min-width:350px!important}.min-w-sm-375px{min-width:375px!important}.min-w-sm-400px{min-width:400px!important}.min-w-sm-425px{min-width:425px!important}.min-w-sm-450px{min-width:450px!important}.min-w-sm-475px{min-width:475px!important}.min-w-sm-500px{min-width:500px!important}.min-w-sm-550px{min-width:550px!important}.min-w-sm-600px{min-width:600px!important}.min-w-sm-650px{min-width:650px!important}.min-w-sm-700px{min-width:700px!important}.min-w-sm-750px{min-width:750px!important}.min-w-sm-800px{min-width:800px!important}.min-w-sm-850px{min-width:850px!important}.min-w-sm-900px{min-width:900px!important}.min-w-sm-950px{min-width:950px!important}.min-w-sm-1000px{min-width:1000px!important}.min-h-sm-unset{min-height:unset!important}.min-h-sm-25{min-height:25%!important}.min-h-sm-50{min-height:50%!important}.min-h-sm-75{min-height:75%!important}.min-h-sm-100{min-height:100%!important}.min-h-sm-auto{min-height:auto!important}.min-h-sm-1px{min-height:1px!important}.min-h-sm-2px{min-height:2px!important}.min-h-sm-3px{min-height:3px!important}.min-h-sm-4px{min-height:4px!important}.min-h-sm-5px{min-height:5px!important}.min-h-sm-6px{min-height:6px!important}.min-h-sm-7px{min-height:7px!important}.min-h-sm-8px{min-height:8px!important}.min-h-sm-9px{min-height:9px!important}.min-h-sm-10px{min-height:10px!important}.min-h-sm-15px{min-height:15px!important}.min-h-sm-20px{min-height:20px!important}.min-h-sm-25px{min-height:25px!important}.min-h-sm-30px{min-height:30px!important}.min-h-sm-35px{min-height:35px!important}.min-h-sm-40px{min-height:40px!important}.min-h-sm-45px{min-height:45px!important}.min-h-sm-50px{min-height:50px!important}.min-h-sm-55px{min-height:55px!important}.min-h-sm-60px{min-height:60px!important}.min-h-sm-65px{min-height:65px!important}.min-h-sm-70px{min-height:70px!important}.min-h-sm-75px{min-height:75px!important}.min-h-sm-80px{min-height:80px!important}.min-h-sm-85px{min-height:85px!important}.min-h-sm-90px{min-height:90px!important}.min-h-sm-95px{min-height:95px!important}.min-h-sm-100px{min-height:100px!important}.min-h-sm-125px{min-height:125px!important}.min-h-sm-150px{min-height:150px!important}.min-h-sm-175px{min-height:175px!important}.min-h-sm-200px{min-height:200px!important}.min-h-sm-225px{min-height:225px!important}.min-h-sm-250px{min-height:250px!important}.min-h-sm-275px{min-height:275px!important}.min-h-sm-300px{min-height:300px!important}.min-h-sm-325px{min-height:325px!important}.min-h-sm-350px{min-height:350px!important}.min-h-sm-375px{min-height:375px!important}.min-h-sm-400px{min-height:400px!important}.min-h-sm-425px{min-height:425px!important}.min-h-sm-450px{min-height:450px!important}.min-h-sm-475px{min-height:475px!important}.min-h-sm-500px{min-height:500px!important}.min-h-sm-550px{min-height:550px!important}.min-h-sm-600px{min-height:600px!important}.min-h-sm-650px{min-height:650px!important}.min-h-sm-700px{min-height:700px!important}.min-h-sm-750px{min-height:750px!important}.min-h-sm-800px{min-height:800px!important}.min-h-sm-850px{min-height:850px!important}.min-h-sm-900px{min-height:900px!important}.min-h-sm-950px{min-height:950px!important}.min-h-sm-1000px{min-height:1000px!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{object-fit:contain!important}.object-fit-md-cover{object-fit:cover!important}.object-fit-md-fill{object-fit:fill!important}.object-fit-md-scale{object-fit:scale-down!important}.object-fit-md-none{object-fit:none!important}.overflow-md-auto{overflow:auto!important}.overflow-md-hidden{overflow:hidden!important}.overflow-md-visible{overflow:visible!important}.overflow-md-scroll{overflow:scroll!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.position-md-static{position:static!important}.position-md-relative{position:relative!important}.position-md-absolute{position:absolute!important}.position-md-fixed{position:fixed!important}.position-md-sticky{position:sticky!important}.w-md-unset{width:unset!important}.w-md-25{width:25%!important}.w-md-50{width:50%!important}.w-md-75{width:75%!important}.w-md-100{width:100%!important}.w-md-auto{width:auto!important}.w-md-1px{width:1px!important}.w-md-2px{width:2px!important}.w-md-3px{width:3px!important}.w-md-4px{width:4px!important}.w-md-5px{width:5px!important}.w-md-6px{width:6px!important}.w-md-7px{width:7px!important}.w-md-8px{width:8px!important}.w-md-9px{width:9px!important}.w-md-10px{width:10px!important}.w-md-15px{width:15px!important}.w-md-20px{width:20px!important}.w-md-25px{width:25px!important}.w-md-30px{width:30px!important}.w-md-35px{width:35px!important}.w-md-40px{width:40px!important}.w-md-45px{width:45px!important}.w-md-50px{width:50px!important}.w-md-55px{width:55px!important}.w-md-60px{width:60px!important}.w-md-65px{width:65px!important}.w-md-70px{width:70px!important}.w-md-75px{width:75px!important}.w-md-80px{width:80px!important}.w-md-85px{width:85px!important}.w-md-90px{width:90px!important}.w-md-95px{width:95px!important}.w-md-100px{width:100px!important}.w-md-125px{width:125px!important}.w-md-150px{width:150px!important}.w-md-175px{width:175px!important}.w-md-200px{width:200px!important}.w-md-225px{width:225px!important}.w-md-250px{width:250px!important}.w-md-275px{width:275px!important}.w-md-300px{width:300px!important}.w-md-325px{width:325px!important}.w-md-350px{width:350px!important}.w-md-375px{width:375px!important}.w-md-400px{width:400px!important}.w-md-425px{width:425px!important}.w-md-450px{width:450px!important}.w-md-475px{width:475px!important}.w-md-500px{width:500px!important}.w-md-550px{width:550px!important}.w-md-600px{width:600px!important}.w-md-650px{width:650px!important}.w-md-700px{width:700px!important}.w-md-750px{width:750px!important}.w-md-800px{width:800px!important}.w-md-850px{width:850px!important}.w-md-900px{width:900px!important}.w-md-950px{width:950px!important}.w-md-1000px{width:1000px!important}.mw-md-unset{max-width:unset!important}.mw-md-25{max-width:25%!important}.mw-md-50{max-width:50%!important}.mw-md-75{max-width:75%!important}.mw-md-100{max-width:100%!important}.mw-md-auto{max-width:auto!important}.mw-md-1px{max-width:1px!important}.mw-md-2px{max-width:2px!important}.mw-md-3px{max-width:3px!important}.mw-md-4px{max-width:4px!important}.mw-md-5px{max-width:5px!important}.mw-md-6px{max-width:6px!important}.mw-md-7px{max-width:7px!important}.mw-md-8px{max-width:8px!important}.mw-md-9px{max-width:9px!important}.mw-md-10px{max-width:10px!important}.mw-md-15px{max-width:15px!important}.mw-md-20px{max-width:20px!important}.mw-md-25px{max-width:25px!important}.mw-md-30px{max-width:30px!important}.mw-md-35px{max-width:35px!important}.mw-md-40px{max-width:40px!important}.mw-md-45px{max-width:45px!important}.mw-md-50px{max-width:50px!important}.mw-md-55px{max-width:55px!important}.mw-md-60px{max-width:60px!important}.mw-md-65px{max-width:65px!important}.mw-md-70px{max-width:70px!important}.mw-md-75px{max-width:75px!important}.mw-md-80px{max-width:80px!important}.mw-md-85px{max-width:85px!important}.mw-md-90px{max-width:90px!important}.mw-md-95px{max-width:95px!important}.mw-md-100px{max-width:100px!important}.mw-md-125px{max-width:125px!important}.mw-md-150px{max-width:150px!important}.mw-md-175px{max-width:175px!important}.mw-md-200px{max-width:200px!important}.mw-md-225px{max-width:225px!important}.mw-md-250px{max-width:250px!important}.mw-md-275px{max-width:275px!important}.mw-md-300px{max-width:300px!important}.mw-md-325px{max-width:325px!important}.mw-md-350px{max-width:350px!important}.mw-md-375px{max-width:375px!important}.mw-md-400px{max-width:400px!important}.mw-md-425px{max-width:425px!important}.mw-md-450px{max-width:450px!important}.mw-md-475px{max-width:475px!important}.mw-md-500px{max-width:500px!important}.mw-md-550px{max-width:550px!important}.mw-md-600px{max-width:600px!important}.mw-md-650px{max-width:650px!important}.mw-md-700px{max-width:700px!important}.mw-md-750px{max-width:750px!important}.mw-md-800px{max-width:800px!important}.mw-md-850px{max-width:850px!important}.mw-md-900px{max-width:900px!important}.mw-md-950px{max-width:950px!important}.mw-md-1000px{max-width:1000px!important}.h-md-unset{height:unset!important}.h-md-25{height:25%!important}.h-md-50{height:50%!important}.h-md-75{height:75%!important}.h-md-100{height:100%!important}.h-md-auto{height:auto!important}.h-md-1px{height:1px!important}.h-md-2px{height:2px!important}.h-md-3px{height:3px!important}.h-md-4px{height:4px!important}.h-md-5px{height:5px!important}.h-md-6px{height:6px!important}.h-md-7px{height:7px!important}.h-md-8px{height:8px!important}.h-md-9px{height:9px!important}.h-md-10px{height:10px!important}.h-md-15px{height:15px!important}.h-md-20px{height:20px!important}.h-md-25px{height:25px!important}.h-md-30px{height:30px!important}.h-md-35px{height:35px!important}.h-md-40px{height:40px!important}.h-md-45px{height:45px!important}.h-md-50px{height:50px!important}.h-md-55px{height:55px!important}.h-md-60px{height:60px!important}.h-md-65px{height:65px!important}.h-md-70px{height:70px!important}.h-md-75px{height:75px!important}.h-md-80px{height:80px!important}.h-md-85px{height:85px!important}.h-md-90px{height:90px!important}.h-md-95px{height:95px!important}.h-md-100px{height:100px!important}.h-md-125px{height:125px!important}.h-md-150px{height:150px!important}.h-md-175px{height:175px!important}.h-md-200px{height:200px!important}.h-md-225px{height:225px!important}.h-md-250px{height:250px!important}.h-md-275px{height:275px!important}.h-md-300px{height:300px!important}.h-md-325px{height:325px!important}.h-md-350px{height:350px!important}.h-md-375px{height:375px!important}.h-md-400px{height:400px!important}.h-md-425px{height:425px!important}.h-md-450px{height:450px!important}.h-md-475px{height:475px!important}.h-md-500px{height:500px!important}.h-md-550px{height:550px!important}.h-md-600px{height:600px!important}.h-md-650px{height:650px!important}.h-md-700px{height:700px!important}.h-md-750px{height:750px!important}.h-md-800px{height:800px!important}.h-md-850px{height:850px!important}.h-md-900px{height:900px!important}.h-md-950px{height:950px!important}.h-md-1000px{height:1000px!important}.mh-md-unset{max-height:unset!important}.mh-md-25{max-height:25%!important}.mh-md-50{max-height:50%!important}.mh-md-75{max-height:75%!important}.mh-md-100{max-height:100%!important}.mh-md-auto{max-height:auto!important}.mh-md-1px{max-height:1px!important}.mh-md-2px{max-height:2px!important}.mh-md-3px{max-height:3px!important}.mh-md-4px{max-height:4px!important}.mh-md-5px{max-height:5px!important}.mh-md-6px{max-height:6px!important}.mh-md-7px{max-height:7px!important}.mh-md-8px{max-height:8px!important}.mh-md-9px{max-height:9px!important}.mh-md-10px{max-height:10px!important}.mh-md-15px{max-height:15px!important}.mh-md-20px{max-height:20px!important}.mh-md-25px{max-height:25px!important}.mh-md-30px{max-height:30px!important}.mh-md-35px{max-height:35px!important}.mh-md-40px{max-height:40px!important}.mh-md-45px{max-height:45px!important}.mh-md-50px{max-height:50px!important}.mh-md-55px{max-height:55px!important}.mh-md-60px{max-height:60px!important}.mh-md-65px{max-height:65px!important}.mh-md-70px{max-height:70px!important}.mh-md-75px{max-height:75px!important}.mh-md-80px{max-height:80px!important}.mh-md-85px{max-height:85px!important}.mh-md-90px{max-height:90px!important}.mh-md-95px{max-height:95px!important}.mh-md-100px{max-height:100px!important}.mh-md-125px{max-height:125px!important}.mh-md-150px{max-height:150px!important}.mh-md-175px{max-height:175px!important}.mh-md-200px{max-height:200px!important}.mh-md-225px{max-height:225px!important}.mh-md-250px{max-height:250px!important}.mh-md-275px{max-height:275px!important}.mh-md-300px{max-height:300px!important}.mh-md-325px{max-height:325px!important}.mh-md-350px{max-height:350px!important}.mh-md-375px{max-height:375px!important}.mh-md-400px{max-height:400px!important}.mh-md-425px{max-height:425px!important}.mh-md-450px{max-height:450px!important}.mh-md-475px{max-height:475px!important}.mh-md-500px{max-height:500px!important}.mh-md-550px{max-height:550px!important}.mh-md-600px{max-height:600px!important}.mh-md-650px{max-height:650px!important}.mh-md-700px{max-height:700px!important}.mh-md-750px{max-height:750px!important}.mh-md-800px{max-height:800px!important}.mh-md-850px{max-height:850px!important}.mh-md-900px{max-height:900px!important}.mh-md-950px{max-height:950px!important}.mh-md-1000px{max-height:1000px!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:.75rem!important}.m-md-4{margin:1rem!important}.m-md-5{margin:1.25rem!important}.m-md-6{margin:1.5rem!important}.m-md-7{margin:1.75rem!important}.m-md-8{margin:2rem!important}.m-md-9{margin:2.25rem!important}.m-md-10{margin:2.5rem!important}.m-md-11{margin:2.75rem!important}.m-md-12{margin:3rem!important}.m-md-13{margin:3.25rem!important}.m-md-14{margin:3.5rem!important}.m-md-15{margin:3.75rem!important}.m-md-16{margin:4rem!important}.m-md-17{margin:4.25rem!important}.m-md-18{margin:4.5rem!important}.m-md-19{margin:4.75rem!important}.m-md-20{margin:5rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:.75rem!important;margin-left:.75rem!important}.mx-md-4{margin-right:1rem!important;margin-left:1rem!important}.mx-md-5{margin-right:1.25rem!important;margin-left:1.25rem!important}.mx-md-6{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-7{margin-right:1.75rem!important;margin-left:1.75rem!important}.mx-md-8{margin-right:2rem!important;margin-left:2rem!important}.mx-md-9{margin-right:2.25rem!important;margin-left:2.25rem!important}.mx-md-10{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-md-11{margin-right:2.75rem!important;margin-left:2.75rem!important}.mx-md-12{margin-right:3rem!important;margin-left:3rem!important}.mx-md-13{margin-right:3.25rem!important;margin-left:3.25rem!important}.mx-md-14{margin-right:3.5rem!important;margin-left:3.5rem!important}.mx-md-15{margin-right:3.75rem!important;margin-left:3.75rem!important}.mx-md-16{margin-right:4rem!important;margin-left:4rem!important}.mx-md-17{margin-right:4.25rem!important;margin-left:4.25rem!important}.mx-md-18{margin-right:4.5rem!important;margin-left:4.5rem!important}.mx-md-19{margin-right:4.75rem!important;margin-left:4.75rem!important}.mx-md-20{margin-right:5rem!important;margin-left:5rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:.75rem!important;margin-bottom:.75rem!important}.my-md-4{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-5{margin-top:1.25rem!important;margin-bottom:1.25rem!important}.my-md-6{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-7{margin-top:1.75rem!important;margin-bottom:1.75rem!important}.my-md-8{margin-top:2rem!important;margin-bottom:2rem!important}.my-md-9{margin-top:2.25rem!important;margin-bottom:2.25rem!important}.my-md-10{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-md-11{margin-top:2.75rem!important;margin-bottom:2.75rem!important}.my-md-12{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-13{margin-top:3.25rem!important;margin-bottom:3.25rem!important}.my-md-14{margin-top:3.5rem!important;margin-bottom:3.5rem!important}.my-md-15{margin-top:3.75rem!important;margin-bottom:3.75rem!important}.my-md-16{margin-top:4rem!important;margin-bottom:4rem!important}.my-md-17{margin-top:4.25rem!important;margin-bottom:4.25rem!important}.my-md-18{margin-top:4.5rem!important;margin-bottom:4.5rem!important}.my-md-19{margin-top:4.75rem!important;margin-bottom:4.75rem!important}.my-md-20{margin-top:5rem!important;margin-bottom:5rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:.75rem!important}.mt-md-4{margin-top:1rem!important}.mt-md-5{margin-top:1.25rem!important}.mt-md-6{margin-top:1.5rem!important}.mt-md-7{margin-top:1.75rem!important}.mt-md-8{margin-top:2rem!important}.mt-md-9{margin-top:2.25rem!important}.mt-md-10{margin-top:2.5rem!important}.mt-md-11{margin-top:2.75rem!important}.mt-md-12{margin-top:3rem!important}.mt-md-13{margin-top:3.25rem!important}.mt-md-14{margin-top:3.5rem!important}.mt-md-15{margin-top:3.75rem!important}.mt-md-16{margin-top:4rem!important}.mt-md-17{margin-top:4.25rem!important}.mt-md-18{margin-top:4.5rem!important}.mt-md-19{margin-top:4.75rem!important}.mt-md-20{margin-top:5rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:.75rem!important}.me-md-4{margin-right:1rem!important}.me-md-5{margin-right:1.25rem!important}.me-md-6{margin-right:1.5rem!important}.me-md-7{margin-right:1.75rem!important}.me-md-8{margin-right:2rem!important}.me-md-9{margin-right:2.25rem!important}.me-md-10{margin-right:2.5rem!important}.me-md-11{margin-right:2.75rem!important}.me-md-12{margin-right:3rem!important}.me-md-13{margin-right:3.25rem!important}.me-md-14{margin-right:3.5rem!important}.me-md-15{margin-right:3.75rem!important}.me-md-16{margin-right:4rem!important}.me-md-17{margin-right:4.25rem!important}.me-md-18{margin-right:4.5rem!important}.me-md-19{margin-right:4.75rem!important}.me-md-20{margin-right:5rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:.75rem!important}.mb-md-4{margin-bottom:1rem!important}.mb-md-5{margin-bottom:1.25rem!important}.mb-md-6{margin-bottom:1.5rem!important}.mb-md-7{margin-bottom:1.75rem!important}.mb-md-8{margin-bottom:2rem!important}.mb-md-9{margin-bottom:2.25rem!important}.mb-md-10{margin-bottom:2.5rem!important}.mb-md-11{margin-bottom:2.75rem!important}.mb-md-12{margin-bottom:3rem!important}.mb-md-13{margin-bottom:3.25rem!important}.mb-md-14{margin-bottom:3.5rem!important}.mb-md-15{margin-bottom:3.75rem!important}.mb-md-16{margin-bottom:4rem!important}.mb-md-17{margin-bottom:4.25rem!important}.mb-md-18{margin-bottom:4.5rem!important}.mb-md-19{margin-bottom:4.75rem!important}.mb-md-20{margin-bottom:5rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:.75rem!important}.ms-md-4{margin-left:1rem!important}.ms-md-5{margin-left:1.25rem!important}.ms-md-6{margin-left:1.5rem!important}.ms-md-7{margin-left:1.75rem!important}.ms-md-8{margin-left:2rem!important}.ms-md-9{margin-left:2.25rem!important}.ms-md-10{margin-left:2.5rem!important}.ms-md-11{margin-left:2.75rem!important}.ms-md-12{margin-left:3rem!important}.ms-md-13{margin-left:3.25rem!important}.ms-md-14{margin-left:3.5rem!important}.ms-md-15{margin-left:3.75rem!important}.ms-md-16{margin-left:4rem!important}.ms-md-17{margin-left:4.25rem!important}.ms-md-18{margin-left:4.5rem!important}.ms-md-19{margin-left:4.75rem!important}.ms-md-20{margin-left:5rem!important}.ms-md-auto{margin-left:auto!important}.m-md-n1{margin:-.25rem!important}.m-md-n2{margin:-.5rem!important}.m-md-n3{margin:-.75rem!important}.m-md-n4{margin:-1rem!important}.m-md-n5{margin:-1.25rem!important}.m-md-n6{margin:-1.5rem!important}.m-md-n7{margin:-1.75rem!important}.m-md-n8{margin:-2rem!important}.m-md-n9{margin:-2.25rem!important}.m-md-n10{margin:-2.5rem!important}.m-md-n11{margin:-2.75rem!important}.m-md-n12{margin:-3rem!important}.m-md-n13{margin:-3.25rem!important}.m-md-n14{margin:-3.5rem!important}.m-md-n15{margin:-3.75rem!important}.m-md-n16{margin:-4rem!important}.m-md-n17{margin:-4.25rem!important}.m-md-n18{margin:-4.5rem!important}.m-md-n19{margin:-4.75rem!important}.m-md-n20{margin:-5rem!important}.mx-md-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-md-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-md-n3{margin-right:-.75rem!important;margin-left:-.75rem!important}.mx-md-n4{margin-right:-1rem!important;margin-left:-1rem!important}.mx-md-n5{margin-right:-1.25rem!important;margin-left:-1.25rem!important}.mx-md-n6{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-md-n7{margin-right:-1.75rem!important;margin-left:-1.75rem!important}.mx-md-n8{margin-right:-2rem!important;margin-left:-2rem!important}.mx-md-n9{margin-right:-2.25rem!important;margin-left:-2.25rem!important}.mx-md-n10{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.mx-md-n11{margin-right:-2.75rem!important;margin-left:-2.75rem!important}.mx-md-n12{margin-right:-3rem!important;margin-left:-3rem!important}.mx-md-n13{margin-right:-3.25rem!important;margin-left:-3.25rem!important}.mx-md-n14{margin-right:-3.5rem!important;margin-left:-3.5rem!important}.mx-md-n15{margin-right:-3.75rem!important;margin-left:-3.75rem!important}.mx-md-n16{margin-right:-4rem!important;margin-left:-4rem!important}.mx-md-n17{margin-right:-4.25rem!important;margin-left:-4.25rem!important}.mx-md-n18{margin-right:-4.5rem!important;margin-left:-4.5rem!important}.mx-md-n19{margin-right:-4.75rem!important;margin-left:-4.75rem!important}.mx-md-n20{margin-right:-5rem!important;margin-left:-5rem!important}.my-md-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-md-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-md-n3{margin-top:-.75rem!important;margin-bottom:-.75rem!important}.my-md-n4{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-md-n5{margin-top:-1.25rem!important;margin-bottom:-1.25rem!important}.my-md-n6{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-md-n7{margin-top:-1.75rem!important;margin-bottom:-1.75rem!important}.my-md-n8{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-md-n9{margin-top:-2.25rem!important;margin-bottom:-2.25rem!important}.my-md-n10{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.my-md-n11{margin-top:-2.75rem!important;margin-bottom:-2.75rem!important}.my-md-n12{margin-top:-3rem!important;margin-bottom:-3rem!important}.my-md-n13{margin-top:-3.25rem!important;margin-bottom:-3.25rem!important}.my-md-n14{margin-top:-3.5rem!important;margin-bottom:-3.5rem!important}.my-md-n15{margin-top:-3.75rem!important;margin-bottom:-3.75rem!important}.my-md-n16{margin-top:-4rem!important;margin-bottom:-4rem!important}.my-md-n17{margin-top:-4.25rem!important;margin-bottom:-4.25rem!important}.my-md-n18{margin-top:-4.5rem!important;margin-bottom:-4.5rem!important}.my-md-n19{margin-top:-4.75rem!important;margin-bottom:-4.75rem!important}.my-md-n20{margin-top:-5rem!important;margin-bottom:-5rem!important}.mt-md-n1{margin-top:-.25rem!important}.mt-md-n2{margin-top:-.5rem!important}.mt-md-n3{margin-top:-.75rem!important}.mt-md-n4{margin-top:-1rem!important}.mt-md-n5{margin-top:-1.25rem!important}.mt-md-n6{margin-top:-1.5rem!important}.mt-md-n7{margin-top:-1.75rem!important}.mt-md-n8{margin-top:-2rem!important}.mt-md-n9{margin-top:-2.25rem!important}.mt-md-n10{margin-top:-2.5rem!important}.mt-md-n11{margin-top:-2.75rem!important}.mt-md-n12{margin-top:-3rem!important}.mt-md-n13{margin-top:-3.25rem!important}.mt-md-n14{margin-top:-3.5rem!important}.mt-md-n15{margin-top:-3.75rem!important}.mt-md-n16{margin-top:-4rem!important}.mt-md-n17{margin-top:-4.25rem!important}.mt-md-n18{margin-top:-4.5rem!important}.mt-md-n19{margin-top:-4.75rem!important}.mt-md-n20{margin-top:-5rem!important}.me-md-n1{margin-right:-.25rem!important}.me-md-n2{margin-right:-.5rem!important}.me-md-n3{margin-right:-.75rem!important}.me-md-n4{margin-right:-1rem!important}.me-md-n5{margin-right:-1.25rem!important}.me-md-n6{margin-right:-1.5rem!important}.me-md-n7{margin-right:-1.75rem!important}.me-md-n8{margin-right:-2rem!important}.me-md-n9{margin-right:-2.25rem!important}.me-md-n10{margin-right:-2.5rem!important}.me-md-n11{margin-right:-2.75rem!important}.me-md-n12{margin-right:-3rem!important}.me-md-n13{margin-right:-3.25rem!important}.me-md-n14{margin-right:-3.5rem!important}.me-md-n15{margin-right:-3.75rem!important}.me-md-n16{margin-right:-4rem!important}.me-md-n17{margin-right:-4.25rem!important}.me-md-n18{margin-right:-4.5rem!important}.me-md-n19{margin-right:-4.75rem!important}.me-md-n20{margin-right:-5rem!important}.mb-md-n1{margin-bottom:-.25rem!important}.mb-md-n2{margin-bottom:-.5rem!important}.mb-md-n3{margin-bottom:-.75rem!important}.mb-md-n4{margin-bottom:-1rem!important}.mb-md-n5{margin-bottom:-1.25rem!important}.mb-md-n6{margin-bottom:-1.5rem!important}.mb-md-n7{margin-bottom:-1.75rem!important}.mb-md-n8{margin-bottom:-2rem!important}.mb-md-n9{margin-bottom:-2.25rem!important}.mb-md-n10{margin-bottom:-2.5rem!important}.mb-md-n11{margin-bottom:-2.75rem!important}.mb-md-n12{margin-bottom:-3rem!important}.mb-md-n13{margin-bottom:-3.25rem!important}.mb-md-n14{margin-bottom:-3.5rem!important}.mb-md-n15{margin-bottom:-3.75rem!important}.mb-md-n16{margin-bottom:-4rem!important}.mb-md-n17{margin-bottom:-4.25rem!important}.mb-md-n18{margin-bottom:-4.5rem!important}.mb-md-n19{margin-bottom:-4.75rem!important}.mb-md-n20{margin-bottom:-5rem!important}.ms-md-n1{margin-left:-.25rem!important}.ms-md-n2{margin-left:-.5rem!important}.ms-md-n3{margin-left:-.75rem!important}.ms-md-n4{margin-left:-1rem!important}.ms-md-n5{margin-left:-1.25rem!important}.ms-md-n6{margin-left:-1.5rem!important}.ms-md-n7{margin-left:-1.75rem!important}.ms-md-n8{margin-left:-2rem!important}.ms-md-n9{margin-left:-2.25rem!important}.ms-md-n10{margin-left:-2.5rem!important}.ms-md-n11{margin-left:-2.75rem!important}.ms-md-n12{margin-left:-3rem!important}.ms-md-n13{margin-left:-3.25rem!important}.ms-md-n14{margin-left:-3.5rem!important}.ms-md-n15{margin-left:-3.75rem!important}.ms-md-n16{margin-left:-4rem!important}.ms-md-n17{margin-left:-4.25rem!important}.ms-md-n18{margin-left:-4.5rem!important}.ms-md-n19{margin-left:-4.75rem!important}.ms-md-n20{margin-left:-5rem!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:.75rem!important}.p-md-4{padding:1rem!important}.p-md-5{padding:1.25rem!important}.p-md-6{padding:1.5rem!important}.p-md-7{padding:1.75rem!important}.p-md-8{padding:2rem!important}.p-md-9{padding:2.25rem!important}.p-md-10{padding:2.5rem!important}.p-md-11{padding:2.75rem!important}.p-md-12{padding:3rem!important}.p-md-13{padding:3.25rem!important}.p-md-14{padding:3.5rem!important}.p-md-15{padding:3.75rem!important}.p-md-16{padding:4rem!important}.p-md-17{padding:4.25rem!important}.p-md-18{padding:4.5rem!important}.p-md-19{padding:4.75rem!important}.p-md-20{padding:5rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:.75rem!important;padding-left:.75rem!important}.px-md-4{padding-right:1rem!important;padding-left:1rem!important}.px-md-5{padding-right:1.25rem!important;padding-left:1.25rem!important}.px-md-6{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-7{padding-right:1.75rem!important;padding-left:1.75rem!important}.px-md-8{padding-right:2rem!important;padding-left:2rem!important}.px-md-9{padding-right:2.25rem!important;padding-left:2.25rem!important}.px-md-10{padding-right:2.5rem!important;padding-left:2.5rem!important}.px-md-11{padding-right:2.75rem!important;padding-left:2.75rem!important}.px-md-12{padding-right:3rem!important;padding-left:3rem!important}.px-md-13{padding-right:3.25rem!important;padding-left:3.25rem!important}.px-md-14{padding-right:3.5rem!important;padding-left:3.5rem!important}.px-md-15{padding-right:3.75rem!important;padding-left:3.75rem!important}.px-md-16{padding-right:4rem!important;padding-left:4rem!important}.px-md-17{padding-right:4.25rem!important;padding-left:4.25rem!important}.px-md-18{padding-right:4.5rem!important;padding-left:4.5rem!important}.px-md-19{padding-right:4.75rem!important;padding-left:4.75rem!important}.px-md-20{padding-right:5rem!important;padding-left:5rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:.75rem!important;padding-bottom:.75rem!important}.py-md-4{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-5{padding-top:1.25rem!important;padding-bottom:1.25rem!important}.py-md-6{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-7{padding-top:1.75rem!important;padding-bottom:1.75rem!important}.py-md-8{padding-top:2rem!important;padding-bottom:2rem!important}.py-md-9{padding-top:2.25rem!important;padding-bottom:2.25rem!important}.py-md-10{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.py-md-11{padding-top:2.75rem!important;padding-bottom:2.75rem!important}.py-md-12{padding-top:3rem!important;padding-bottom:3rem!important}.py-md-13{padding-top:3.25rem!important;padding-bottom:3.25rem!important}.py-md-14{padding-top:3.5rem!important;padding-bottom:3.5rem!important}.py-md-15{padding-top:3.75rem!important;padding-bottom:3.75rem!important}.py-md-16{padding-top:4rem!important;padding-bottom:4rem!important}.py-md-17{padding-top:4.25rem!important;padding-bottom:4.25rem!important}.py-md-18{padding-top:4.5rem!important;padding-bottom:4.5rem!important}.py-md-19{padding-top:4.75rem!important;padding-bottom:4.75rem!important}.py-md-20{padding-top:5rem!important;padding-bottom:5rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:.75rem!important}.pt-md-4{padding-top:1rem!important}.pt-md-5{padding-top:1.25rem!important}.pt-md-6{padding-top:1.5rem!important}.pt-md-7{padding-top:1.75rem!important}.pt-md-8{padding-top:2rem!important}.pt-md-9{padding-top:2.25rem!important}.pt-md-10{padding-top:2.5rem!important}.pt-md-11{padding-top:2.75rem!important}.pt-md-12{padding-top:3rem!important}.pt-md-13{padding-top:3.25rem!important}.pt-md-14{padding-top:3.5rem!important}.pt-md-15{padding-top:3.75rem!important}.pt-md-16{padding-top:4rem!important}.pt-md-17{padding-top:4.25rem!important}.pt-md-18{padding-top:4.5rem!important}.pt-md-19{padding-top:4.75rem!important}.pt-md-20{padding-top:5rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:.75rem!important}.pe-md-4{padding-right:1rem!important}.pe-md-5{padding-right:1.25rem!important}.pe-md-6{padding-right:1.5rem!important}.pe-md-7{padding-right:1.75rem!important}.pe-md-8{padding-right:2rem!important}.pe-md-9{padding-right:2.25rem!important}.pe-md-10{padding-right:2.5rem!important}.pe-md-11{padding-right:2.75rem!important}.pe-md-12{padding-right:3rem!important}.pe-md-13{padding-right:3.25rem!important}.pe-md-14{padding-right:3.5rem!important}.pe-md-15{padding-right:3.75rem!important}.pe-md-16{padding-right:4rem!important}.pe-md-17{padding-right:4.25rem!important}.pe-md-18{padding-right:4.5rem!important}.pe-md-19{padding-right:4.75rem!important}.pe-md-20{padding-right:5rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:.75rem!important}.pb-md-4{padding-bottom:1rem!important}.pb-md-5{padding-bottom:1.25rem!important}.pb-md-6{padding-bottom:1.5rem!important}.pb-md-7{padding-bottom:1.75rem!important}.pb-md-8{padding-bottom:2rem!important}.pb-md-9{padding-bottom:2.25rem!important}.pb-md-10{padding-bottom:2.5rem!important}.pb-md-11{padding-bottom:2.75rem!important}.pb-md-12{padding-bottom:3rem!important}.pb-md-13{padding-bottom:3.25rem!important}.pb-md-14{padding-bottom:3.5rem!important}.pb-md-15{padding-bottom:3.75rem!important}.pb-md-16{padding-bottom:4rem!important}.pb-md-17{padding-bottom:4.25rem!important}.pb-md-18{padding-bottom:4.5rem!important}.pb-md-19{padding-bottom:4.75rem!important}.pb-md-20{padding-bottom:5rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:.75rem!important}.ps-md-4{padding-left:1rem!important}.ps-md-5{padding-left:1.25rem!important}.ps-md-6{padding-left:1.5rem!important}.ps-md-7{padding-left:1.75rem!important}.ps-md-8{padding-left:2rem!important}.ps-md-9{padding-left:2.25rem!important}.ps-md-10{padding-left:2.5rem!important}.ps-md-11{padding-left:2.75rem!important}.ps-md-12{padding-left:3rem!important}.ps-md-13{padding-left:3.25rem!important}.ps-md-14{padding-left:3.5rem!important}.ps-md-15{padding-left:3.75rem!important}.ps-md-16{padding-left:4rem!important}.ps-md-17{padding-left:4.25rem!important}.ps-md-18{padding-left:4.5rem!important}.ps-md-19{padding-left:4.75rem!important}.ps-md-20{padding-left:5rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:.75rem!important}.gap-md-4{gap:1rem!important}.gap-md-5{gap:1.25rem!important}.gap-md-6{gap:1.5rem!important}.gap-md-7{gap:1.75rem!important}.gap-md-8{gap:2rem!important}.gap-md-9{gap:2.25rem!important}.gap-md-10{gap:2.5rem!important}.gap-md-11{gap:2.75rem!important}.gap-md-12{gap:3rem!important}.gap-md-13{gap:3.25rem!important}.gap-md-14{gap:3.5rem!important}.gap-md-15{gap:3.75rem!important}.gap-md-16{gap:4rem!important}.gap-md-17{gap:4.25rem!important}.gap-md-18{gap:4.5rem!important}.gap-md-19{gap:4.75rem!important}.gap-md-20{gap:5rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:.75rem!important}.row-gap-md-4{row-gap:1rem!important}.row-gap-md-5{row-gap:1.25rem!important}.row-gap-md-6{row-gap:1.5rem!important}.row-gap-md-7{row-gap:1.75rem!important}.row-gap-md-8{row-gap:2rem!important}.row-gap-md-9{row-gap:2.25rem!important}.row-gap-md-10{row-gap:2.5rem!important}.row-gap-md-11{row-gap:2.75rem!important}.row-gap-md-12{row-gap:3rem!important}.row-gap-md-13{row-gap:3.25rem!important}.row-gap-md-14{row-gap:3.5rem!important}.row-gap-md-15{row-gap:3.75rem!important}.row-gap-md-16{row-gap:4rem!important}.row-gap-md-17{row-gap:4.25rem!important}.row-gap-md-18{row-gap:4.5rem!important}.row-gap-md-19{row-gap:4.75rem!important}.row-gap-md-20{row-gap:5rem!important}.column-gap-md-0{column-gap:0!important}.column-gap-md-1{column-gap:.25rem!important}.column-gap-md-2{column-gap:.5rem!important}.column-gap-md-3{column-gap:.75rem!important}.column-gap-md-4{column-gap:1rem!important}.column-gap-md-5{column-gap:1.25rem!important}.column-gap-md-6{column-gap:1.5rem!important}.column-gap-md-7{column-gap:1.75rem!important}.column-gap-md-8{column-gap:2rem!important}.column-gap-md-9{column-gap:2.25rem!important}.column-gap-md-10{column-gap:2.5rem!important}.column-gap-md-11{column-gap:2.75rem!important}.column-gap-md-12{column-gap:3rem!important}.column-gap-md-13{column-gap:3.25rem!important}.column-gap-md-14{column-gap:3.5rem!important}.column-gap-md-15{column-gap:3.75rem!important}.column-gap-md-16{column-gap:4rem!important}.column-gap-md-17{column-gap:4.25rem!important}.column-gap-md-18{column-gap:4.5rem!important}.column-gap-md-19{column-gap:4.75rem!important}.column-gap-md-20{column-gap:5rem!important}.fs-md-1{font-size:calc(1.3rem + .6vw)!important}.fs-md-2{font-size:calc(1.275rem + .3vw)!important}.fs-md-3{font-size:calc(1.26rem + .12vw)!important}.fs-md-4{font-size:1.25rem!important}.fs-md-5{font-size:1.15rem!important}.fs-md-6{font-size:1.075rem!important}.fs-md-7{font-size:.95rem!important}.fs-md-8{font-size:.85rem!important}.fs-md-9{font-size:.75rem!important}.fs-md-10{font-size:.5rem!important}.fs-md-sm{font-size:.95rem!important}.fs-md-base{font-size:1rem!important}.fs-md-lg{font-size:1.075rem!important}.fs-md-xl{font-size:1.21rem!important}.fs-md-fluid{font-size:100%!important}.fs-md-2x{font-size:calc(1.325rem + .9vw)!important}.fs-md-2qx{font-size:calc(1.35rem + 1.2vw)!important}.fs-md-2hx{font-size:calc(1.375rem + 1.5vw)!important}.fs-md-2tx{font-size:calc(1.4rem + 1.8vw)!important}.fs-md-3x{font-size:calc(1.425rem + 2.1vw)!important}.fs-md-3qx{font-size:calc(1.45rem + 2.4vw)!important}.fs-md-3hx{font-size:calc(1.475rem + 2.7vw)!important}.fs-md-3tx{font-size:calc(1.5rem + 3vw)!important}.fs-md-4x{font-size:calc(1.525rem + 3.3vw)!important}.fs-md-4qx{font-size:calc(1.55rem + 3.6vw)!important}.fs-md-4hx{font-size:calc(1.575rem + 3.9vw)!important}.fs-md-4tx{font-size:calc(1.6rem + 4.2vw)!important}.fs-md-5x{font-size:calc(1.625rem + 4.5vw)!important}.fs-md-5qx{font-size:calc(1.65rem + 4.8vw)!important}.fs-md-5hx{font-size:calc(1.675rem + 5.1vw)!important}.fs-md-5tx{font-size:calc(1.7rem + 5.4vw)!important}.fs-md-6x{font-size:calc(1.725rem + 5.7vw)!important}.fs-md-6qx{font-size:calc(1.75rem + 6vw)!important}.fs-md-6hx{font-size:calc(1.775rem + 6.3vw)!important}.fs-md-6tx{font-size:calc(1.8rem + 6.6vw)!important}.fs-md-7x{font-size:calc(1.825rem + 6.9vw)!important}.fs-md-7qx{font-size:calc(1.85rem + 7.2vw)!important}.fs-md-7hx{font-size:calc(1.875rem + 7.5vw)!important}.fs-md-7tx{font-size:calc(1.9rem + 7.8vw)!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}.min-w-md-unset{min-width:unset!important}.min-w-md-25{min-width:25%!important}.min-w-md-50{min-width:50%!important}.min-w-md-75{min-width:75%!important}.min-w-md-100{min-width:100%!important}.min-w-md-auto{min-width:auto!important}.min-w-md-1px{min-width:1px!important}.min-w-md-2px{min-width:2px!important}.min-w-md-3px{min-width:3px!important}.min-w-md-4px{min-width:4px!important}.min-w-md-5px{min-width:5px!important}.min-w-md-6px{min-width:6px!important}.min-w-md-7px{min-width:7px!important}.min-w-md-8px{min-width:8px!important}.min-w-md-9px{min-width:9px!important}.min-w-md-10px{min-width:10px!important}.min-w-md-15px{min-width:15px!important}.min-w-md-20px{min-width:20px!important}.min-w-md-25px{min-width:25px!important}.min-w-md-30px{min-width:30px!important}.min-w-md-35px{min-width:35px!important}.min-w-md-40px{min-width:40px!important}.min-w-md-45px{min-width:45px!important}.min-w-md-50px{min-width:50px!important}.min-w-md-55px{min-width:55px!important}.min-w-md-60px{min-width:60px!important}.min-w-md-65px{min-width:65px!important}.min-w-md-70px{min-width:70px!important}.min-w-md-75px{min-width:75px!important}.min-w-md-80px{min-width:80px!important}.min-w-md-85px{min-width:85px!important}.min-w-md-90px{min-width:90px!important}.min-w-md-95px{min-width:95px!important}.min-w-md-100px{min-width:100px!important}.min-w-md-125px{min-width:125px!important}.min-w-md-150px{min-width:150px!important}.min-w-md-175px{min-width:175px!important}.min-w-md-200px{min-width:200px!important}.min-w-md-225px{min-width:225px!important}.min-w-md-250px{min-width:250px!important}.min-w-md-275px{min-width:275px!important}.min-w-md-300px{min-width:300px!important}.min-w-md-325px{min-width:325px!important}.min-w-md-350px{min-width:350px!important}.min-w-md-375px{min-width:375px!important}.min-w-md-400px{min-width:400px!important}.min-w-md-425px{min-width:425px!important}.min-w-md-450px{min-width:450px!important}.min-w-md-475px{min-width:475px!important}.min-w-md-500px{min-width:500px!important}.min-w-md-550px{min-width:550px!important}.min-w-md-600px{min-width:600px!important}.min-w-md-650px{min-width:650px!important}.min-w-md-700px{min-width:700px!important}.min-w-md-750px{min-width:750px!important}.min-w-md-800px{min-width:800px!important}.min-w-md-850px{min-width:850px!important}.min-w-md-900px{min-width:900px!important}.min-w-md-950px{min-width:950px!important}.min-w-md-1000px{min-width:1000px!important}.min-h-md-unset{min-height:unset!important}.min-h-md-25{min-height:25%!important}.min-h-md-50{min-height:50%!important}.min-h-md-75{min-height:75%!important}.min-h-md-100{min-height:100%!important}.min-h-md-auto{min-height:auto!important}.min-h-md-1px{min-height:1px!important}.min-h-md-2px{min-height:2px!important}.min-h-md-3px{min-height:3px!important}.min-h-md-4px{min-height:4px!important}.min-h-md-5px{min-height:5px!important}.min-h-md-6px{min-height:6px!important}.min-h-md-7px{min-height:7px!important}.min-h-md-8px{min-height:8px!important}.min-h-md-9px{min-height:9px!important}.min-h-md-10px{min-height:10px!important}.min-h-md-15px{min-height:15px!important}.min-h-md-20px{min-height:20px!important}.min-h-md-25px{min-height:25px!important}.min-h-md-30px{min-height:30px!important}.min-h-md-35px{min-height:35px!important}.min-h-md-40px{min-height:40px!important}.min-h-md-45px{min-height:45px!important}.min-h-md-50px{min-height:50px!important}.min-h-md-55px{min-height:55px!important}.min-h-md-60px{min-height:60px!important}.min-h-md-65px{min-height:65px!important}.min-h-md-70px{min-height:70px!important}.min-h-md-75px{min-height:75px!important}.min-h-md-80px{min-height:80px!important}.min-h-md-85px{min-height:85px!important}.min-h-md-90px{min-height:90px!important}.min-h-md-95px{min-height:95px!important}.min-h-md-100px{min-height:100px!important}.min-h-md-125px{min-height:125px!important}.min-h-md-150px{min-height:150px!important}.min-h-md-175px{min-height:175px!important}.min-h-md-200px{min-height:200px!important}.min-h-md-225px{min-height:225px!important}.min-h-md-250px{min-height:250px!important}.min-h-md-275px{min-height:275px!important}.min-h-md-300px{min-height:300px!important}.min-h-md-325px{min-height:325px!important}.min-h-md-350px{min-height:350px!important}.min-h-md-375px{min-height:375px!important}.min-h-md-400px{min-height:400px!important}.min-h-md-425px{min-height:425px!important}.min-h-md-450px{min-height:450px!important}.min-h-md-475px{min-height:475px!important}.min-h-md-500px{min-height:500px!important}.min-h-md-550px{min-height:550px!important}.min-h-md-600px{min-height:600px!important}.min-h-md-650px{min-height:650px!important}.min-h-md-700px{min-height:700px!important}.min-h-md-750px{min-height:750px!important}.min-h-md-800px{min-height:800px!important}.min-h-md-850px{min-height:850px!important}.min-h-md-900px{min-height:900px!important}.min-h-md-950px{min-height:950px!important}.min-h-md-1000px{min-height:1000px!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{object-fit:contain!important}.object-fit-lg-cover{object-fit:cover!important}.object-fit-lg-fill{object-fit:fill!important}.object-fit-lg-scale{object-fit:scale-down!important}.object-fit-lg-none{object-fit:none!important}.overflow-lg-auto{overflow:auto!important}.overflow-lg-hidden{overflow:hidden!important}.overflow-lg-visible{overflow:visible!important}.overflow-lg-scroll{overflow:scroll!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.position-lg-static{position:static!important}.position-lg-relative{position:relative!important}.position-lg-absolute{position:absolute!important}.position-lg-fixed{position:fixed!important}.position-lg-sticky{position:sticky!important}.w-lg-unset{width:unset!important}.w-lg-25{width:25%!important}.w-lg-50{width:50%!important}.w-lg-75{width:75%!important}.w-lg-100{width:100%!important}.w-lg-auto{width:auto!important}.w-lg-1px{width:1px!important}.w-lg-2px{width:2px!important}.w-lg-3px{width:3px!important}.w-lg-4px{width:4px!important}.w-lg-5px{width:5px!important}.w-lg-6px{width:6px!important}.w-lg-7px{width:7px!important}.w-lg-8px{width:8px!important}.w-lg-9px{width:9px!important}.w-lg-10px{width:10px!important}.w-lg-15px{width:15px!important}.w-lg-20px{width:20px!important}.w-lg-25px{width:25px!important}.w-lg-30px{width:30px!important}.w-lg-35px{width:35px!important}.w-lg-40px{width:40px!important}.w-lg-45px{width:45px!important}.w-lg-50px{width:50px!important}.w-lg-55px{width:55px!important}.w-lg-60px{width:60px!important}.w-lg-65px{width:65px!important}.w-lg-70px{width:70px!important}.w-lg-75px{width:75px!important}.w-lg-80px{width:80px!important}.w-lg-85px{width:85px!important}.w-lg-90px{width:90px!important}.w-lg-95px{width:95px!important}.w-lg-100px{width:100px!important}.w-lg-125px{width:125px!important}.w-lg-150px{width:150px!important}.w-lg-175px{width:175px!important}.w-lg-200px{width:200px!important}.w-lg-225px{width:225px!important}.w-lg-250px{width:250px!important}.w-lg-275px{width:275px!important}.w-lg-300px{width:300px!important}.w-lg-325px{width:325px!important}.w-lg-350px{width:350px!important}.w-lg-375px{width:375px!important}.w-lg-400px{width:400px!important}.w-lg-425px{width:425px!important}.w-lg-450px{width:450px!important}.w-lg-475px{width:475px!important}.w-lg-500px{width:500px!important}.w-lg-550px{width:550px!important}.w-lg-600px{width:600px!important}.w-lg-650px{width:650px!important}.w-lg-700px{width:700px!important}.w-lg-750px{width:750px!important}.w-lg-800px{width:800px!important}.w-lg-850px{width:850px!important}.w-lg-900px{width:900px!important}.w-lg-950px{width:950px!important}.w-lg-1000px{width:1000px!important}.mw-lg-unset{max-width:unset!important}.mw-lg-25{max-width:25%!important}.mw-lg-50{max-width:50%!important}.mw-lg-75{max-width:75%!important}.mw-lg-100{max-width:100%!important}.mw-lg-auto{max-width:auto!important}.mw-lg-1px{max-width:1px!important}.mw-lg-2px{max-width:2px!important}.mw-lg-3px{max-width:3px!important}.mw-lg-4px{max-width:4px!important}.mw-lg-5px{max-width:5px!important}.mw-lg-6px{max-width:6px!important}.mw-lg-7px{max-width:7px!important}.mw-lg-8px{max-width:8px!important}.mw-lg-9px{max-width:9px!important}.mw-lg-10px{max-width:10px!important}.mw-lg-15px{max-width:15px!important}.mw-lg-20px{max-width:20px!important}.mw-lg-25px{max-width:25px!important}.mw-lg-30px{max-width:30px!important}.mw-lg-35px{max-width:35px!important}.mw-lg-40px{max-width:40px!important}.mw-lg-45px{max-width:45px!important}.mw-lg-50px{max-width:50px!important}.mw-lg-55px{max-width:55px!important}.mw-lg-60px{max-width:60px!important}.mw-lg-65px{max-width:65px!important}.mw-lg-70px{max-width:70px!important}.mw-lg-75px{max-width:75px!important}.mw-lg-80px{max-width:80px!important}.mw-lg-85px{max-width:85px!important}.mw-lg-90px{max-width:90px!important}.mw-lg-95px{max-width:95px!important}.mw-lg-100px{max-width:100px!important}.mw-lg-125px{max-width:125px!important}.mw-lg-150px{max-width:150px!important}.mw-lg-175px{max-width:175px!important}.mw-lg-200px{max-width:200px!important}.mw-lg-225px{max-width:225px!important}.mw-lg-250px{max-width:250px!important}.mw-lg-275px{max-width:275px!important}.mw-lg-300px{max-width:300px!important}.mw-lg-325px{max-width:325px!important}.mw-lg-350px{max-width:350px!important}.mw-lg-375px{max-width:375px!important}.mw-lg-400px{max-width:400px!important}.mw-lg-425px{max-width:425px!important}.mw-lg-450px{max-width:450px!important}.mw-lg-475px{max-width:475px!important}.mw-lg-500px{max-width:500px!important}.mw-lg-550px{max-width:550px!important}.mw-lg-600px{max-width:600px!important}.mw-lg-650px{max-width:650px!important}.mw-lg-700px{max-width:700px!important}.mw-lg-750px{max-width:750px!important}.mw-lg-800px{max-width:800px!important}.mw-lg-850px{max-width:850px!important}.mw-lg-900px{max-width:900px!important}.mw-lg-950px{max-width:950px!important}.mw-lg-1000px{max-width:1000px!important}.h-lg-unset{height:unset!important}.h-lg-25{height:25%!important}.h-lg-50{height:50%!important}.h-lg-75{height:75%!important}.h-lg-100{height:100%!important}.h-lg-auto{height:auto!important}.h-lg-1px{height:1px!important}.h-lg-2px{height:2px!important}.h-lg-3px{height:3px!important}.h-lg-4px{height:4px!important}.h-lg-5px{height:5px!important}.h-lg-6px{height:6px!important}.h-lg-7px{height:7px!important}.h-lg-8px{height:8px!important}.h-lg-9px{height:9px!important}.h-lg-10px{height:10px!important}.h-lg-15px{height:15px!important}.h-lg-20px{height:20px!important}.h-lg-25px{height:25px!important}.h-lg-30px{height:30px!important}.h-lg-35px{height:35px!important}.h-lg-40px{height:40px!important}.h-lg-45px{height:45px!important}.h-lg-50px{height:50px!important}.h-lg-55px{height:55px!important}.h-lg-60px{height:60px!important}.h-lg-65px{height:65px!important}.h-lg-70px{height:70px!important}.h-lg-75px{height:75px!important}.h-lg-80px{height:80px!important}.h-lg-85px{height:85px!important}.h-lg-90px{height:90px!important}.h-lg-95px{height:95px!important}.h-lg-100px{height:100px!important}.h-lg-125px{height:125px!important}.h-lg-150px{height:150px!important}.h-lg-175px{height:175px!important}.h-lg-200px{height:200px!important}.h-lg-225px{height:225px!important}.h-lg-250px{height:250px!important}.h-lg-275px{height:275px!important}.h-lg-300px{height:300px!important}.h-lg-325px{height:325px!important}.h-lg-350px{height:350px!important}.h-lg-375px{height:375px!important}.h-lg-400px{height:400px!important}.h-lg-425px{height:425px!important}.h-lg-450px{height:450px!important}.h-lg-475px{height:475px!important}.h-lg-500px{height:500px!important}.h-lg-550px{height:550px!important}.h-lg-600px{height:600px!important}.h-lg-650px{height:650px!important}.h-lg-700px{height:700px!important}.h-lg-750px{height:750px!important}.h-lg-800px{height:800px!important}.h-lg-850px{height:850px!important}.h-lg-900px{height:900px!important}.h-lg-950px{height:950px!important}.h-lg-1000px{height:1000px!important}.mh-lg-unset{max-height:unset!important}.mh-lg-25{max-height:25%!important}.mh-lg-50{max-height:50%!important}.mh-lg-75{max-height:75%!important}.mh-lg-100{max-height:100%!important}.mh-lg-auto{max-height:auto!important}.mh-lg-1px{max-height:1px!important}.mh-lg-2px{max-height:2px!important}.mh-lg-3px{max-height:3px!important}.mh-lg-4px{max-height:4px!important}.mh-lg-5px{max-height:5px!important}.mh-lg-6px{max-height:6px!important}.mh-lg-7px{max-height:7px!important}.mh-lg-8px{max-height:8px!important}.mh-lg-9px{max-height:9px!important}.mh-lg-10px{max-height:10px!important}.mh-lg-15px{max-height:15px!important}.mh-lg-20px{max-height:20px!important}.mh-lg-25px{max-height:25px!important}.mh-lg-30px{max-height:30px!important}.mh-lg-35px{max-height:35px!important}.mh-lg-40px{max-height:40px!important}.mh-lg-45px{max-height:45px!important}.mh-lg-50px{max-height:50px!important}.mh-lg-55px{max-height:55px!important}.mh-lg-60px{max-height:60px!important}.mh-lg-65px{max-height:65px!important}.mh-lg-70px{max-height:70px!important}.mh-lg-75px{max-height:75px!important}.mh-lg-80px{max-height:80px!important}.mh-lg-85px{max-height:85px!important}.mh-lg-90px{max-height:90px!important}.mh-lg-95px{max-height:95px!important}.mh-lg-100px{max-height:100px!important}.mh-lg-125px{max-height:125px!important}.mh-lg-150px{max-height:150px!important}.mh-lg-175px{max-height:175px!important}.mh-lg-200px{max-height:200px!important}.mh-lg-225px{max-height:225px!important}.mh-lg-250px{max-height:250px!important}.mh-lg-275px{max-height:275px!important}.mh-lg-300px{max-height:300px!important}.mh-lg-325px{max-height:325px!important}.mh-lg-350px{max-height:350px!important}.mh-lg-375px{max-height:375px!important}.mh-lg-400px{max-height:400px!important}.mh-lg-425px{max-height:425px!important}.mh-lg-450px{max-height:450px!important}.mh-lg-475px{max-height:475px!important}.mh-lg-500px{max-height:500px!important}.mh-lg-550px{max-height:550px!important}.mh-lg-600px{max-height:600px!important}.mh-lg-650px{max-height:650px!important}.mh-lg-700px{max-height:700px!important}.mh-lg-750px{max-height:750px!important}.mh-lg-800px{max-height:800px!important}.mh-lg-850px{max-height:850px!important}.mh-lg-900px{max-height:900px!important}.mh-lg-950px{max-height:950px!important}.mh-lg-1000px{max-height:1000px!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:.75rem!important}.m-lg-4{margin:1rem!important}.m-lg-5{margin:1.25rem!important}.m-lg-6{margin:1.5rem!important}.m-lg-7{margin:1.75rem!important}.m-lg-8{margin:2rem!important}.m-lg-9{margin:2.25rem!important}.m-lg-10{margin:2.5rem!important}.m-lg-11{margin:2.75rem!important}.m-lg-12{margin:3rem!important}.m-lg-13{margin:3.25rem!important}.m-lg-14{margin:3.5rem!important}.m-lg-15{margin:3.75rem!important}.m-lg-16{margin:4rem!important}.m-lg-17{margin:4.25rem!important}.m-lg-18{margin:4.5rem!important}.m-lg-19{margin:4.75rem!important}.m-lg-20{margin:5rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:.75rem!important;margin-left:.75rem!important}.mx-lg-4{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-5{margin-right:1.25rem!important;margin-left:1.25rem!important}.mx-lg-6{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-7{margin-right:1.75rem!important;margin-left:1.75rem!important}.mx-lg-8{margin-right:2rem!important;margin-left:2rem!important}.mx-lg-9{margin-right:2.25rem!important;margin-left:2.25rem!important}.mx-lg-10{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-lg-11{margin-right:2.75rem!important;margin-left:2.75rem!important}.mx-lg-12{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-13{margin-right:3.25rem!important;margin-left:3.25rem!important}.mx-lg-14{margin-right:3.5rem!important;margin-left:3.5rem!important}.mx-lg-15{margin-right:3.75rem!important;margin-left:3.75rem!important}.mx-lg-16{margin-right:4rem!important;margin-left:4rem!important}.mx-lg-17{margin-right:4.25rem!important;margin-left:4.25rem!important}.mx-lg-18{margin-right:4.5rem!important;margin-left:4.5rem!important}.mx-lg-19{margin-right:4.75rem!important;margin-left:4.75rem!important}.mx-lg-20{margin-right:5rem!important;margin-left:5rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:.75rem!important;margin-bottom:.75rem!important}.my-lg-4{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-5{margin-top:1.25rem!important;margin-bottom:1.25rem!important}.my-lg-6{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-7{margin-top:1.75rem!important;margin-bottom:1.75rem!important}.my-lg-8{margin-top:2rem!important;margin-bottom:2rem!important}.my-lg-9{margin-top:2.25rem!important;margin-bottom:2.25rem!important}.my-lg-10{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-lg-11{margin-top:2.75rem!important;margin-bottom:2.75rem!important}.my-lg-12{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-13{margin-top:3.25rem!important;margin-bottom:3.25rem!important}.my-lg-14{margin-top:3.5rem!important;margin-bottom:3.5rem!important}.my-lg-15{margin-top:3.75rem!important;margin-bottom:3.75rem!important}.my-lg-16{margin-top:4rem!important;margin-bottom:4rem!important}.my-lg-17{margin-top:4.25rem!important;margin-bottom:4.25rem!important}.my-lg-18{margin-top:4.5rem!important;margin-bottom:4.5rem!important}.my-lg-19{margin-top:4.75rem!important;margin-bottom:4.75rem!important}.my-lg-20{margin-top:5rem!important;margin-bottom:5rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:.75rem!important}.mt-lg-4{margin-top:1rem!important}.mt-lg-5{margin-top:1.25rem!important}.mt-lg-6{margin-top:1.5rem!important}.mt-lg-7{margin-top:1.75rem!important}.mt-lg-8{margin-top:2rem!important}.mt-lg-9{margin-top:2.25rem!important}.mt-lg-10{margin-top:2.5rem!important}.mt-lg-11{margin-top:2.75rem!important}.mt-lg-12{margin-top:3rem!important}.mt-lg-13{margin-top:3.25rem!important}.mt-lg-14{margin-top:3.5rem!important}.mt-lg-15{margin-top:3.75rem!important}.mt-lg-16{margin-top:4rem!important}.mt-lg-17{margin-top:4.25rem!important}.mt-lg-18{margin-top:4.5rem!important}.mt-lg-19{margin-top:4.75rem!important}.mt-lg-20{margin-top:5rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:.75rem!important}.me-lg-4{margin-right:1rem!important}.me-lg-5{margin-right:1.25rem!important}.me-lg-6{margin-right:1.5rem!important}.me-lg-7{margin-right:1.75rem!important}.me-lg-8{margin-right:2rem!important}.me-lg-9{margin-right:2.25rem!important}.me-lg-10{margin-right:2.5rem!important}.me-lg-11{margin-right:2.75rem!important}.me-lg-12{margin-right:3rem!important}.me-lg-13{margin-right:3.25rem!important}.me-lg-14{margin-right:3.5rem!important}.me-lg-15{margin-right:3.75rem!important}.me-lg-16{margin-right:4rem!important}.me-lg-17{margin-right:4.25rem!important}.me-lg-18{margin-right:4.5rem!important}.me-lg-19{margin-right:4.75rem!important}.me-lg-20{margin-right:5rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:.75rem!important}.mb-lg-4{margin-bottom:1rem!important}.mb-lg-5{margin-bottom:1.25rem!important}.mb-lg-6{margin-bottom:1.5rem!important}.mb-lg-7{margin-bottom:1.75rem!important}.mb-lg-8{margin-bottom:2rem!important}.mb-lg-9{margin-bottom:2.25rem!important}.mb-lg-10{margin-bottom:2.5rem!important}.mb-lg-11{margin-bottom:2.75rem!important}.mb-lg-12{margin-bottom:3rem!important}.mb-lg-13{margin-bottom:3.25rem!important}.mb-lg-14{margin-bottom:3.5rem!important}.mb-lg-15{margin-bottom:3.75rem!important}.mb-lg-16{margin-bottom:4rem!important}.mb-lg-17{margin-bottom:4.25rem!important}.mb-lg-18{margin-bottom:4.5rem!important}.mb-lg-19{margin-bottom:4.75rem!important}.mb-lg-20{margin-bottom:5rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:.75rem!important}.ms-lg-4{margin-left:1rem!important}.ms-lg-5{margin-left:1.25rem!important}.ms-lg-6{margin-left:1.5rem!important}.ms-lg-7{margin-left:1.75rem!important}.ms-lg-8{margin-left:2rem!important}.ms-lg-9{margin-left:2.25rem!important}.ms-lg-10{margin-left:2.5rem!important}.ms-lg-11{margin-left:2.75rem!important}.ms-lg-12{margin-left:3rem!important}.ms-lg-13{margin-left:3.25rem!important}.ms-lg-14{margin-left:3.5rem!important}.ms-lg-15{margin-left:3.75rem!important}.ms-lg-16{margin-left:4rem!important}.ms-lg-17{margin-left:4.25rem!important}.ms-lg-18{margin-left:4.5rem!important}.ms-lg-19{margin-left:4.75rem!important}.ms-lg-20{margin-left:5rem!important}.ms-lg-auto{margin-left:auto!important}.m-lg-n1{margin:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.m-lg-n3{margin:-.75rem!important}.m-lg-n4{margin:-1rem!important}.m-lg-n5{margin:-1.25rem!important}.m-lg-n6{margin:-1.5rem!important}.m-lg-n7{margin:-1.75rem!important}.m-lg-n8{margin:-2rem!important}.m-lg-n9{margin:-2.25rem!important}.m-lg-n10{margin:-2.5rem!important}.m-lg-n11{margin:-2.75rem!important}.m-lg-n12{margin:-3rem!important}.m-lg-n13{margin:-3.25rem!important}.m-lg-n14{margin:-3.5rem!important}.m-lg-n15{margin:-3.75rem!important}.m-lg-n16{margin:-4rem!important}.m-lg-n17{margin:-4.25rem!important}.m-lg-n18{margin:-4.5rem!important}.m-lg-n19{margin:-4.75rem!important}.m-lg-n20{margin:-5rem!important}.mx-lg-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-lg-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-lg-n3{margin-right:-.75rem!important;margin-left:-.75rem!important}.mx-lg-n4{margin-right:-1rem!important;margin-left:-1rem!important}.mx-lg-n5{margin-right:-1.25rem!important;margin-left:-1.25rem!important}.mx-lg-n6{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-lg-n7{margin-right:-1.75rem!important;margin-left:-1.75rem!important}.mx-lg-n8{margin-right:-2rem!important;margin-left:-2rem!important}.mx-lg-n9{margin-right:-2.25rem!important;margin-left:-2.25rem!important}.mx-lg-n10{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.mx-lg-n11{margin-right:-2.75rem!important;margin-left:-2.75rem!important}.mx-lg-n12{margin-right:-3rem!important;margin-left:-3rem!important}.mx-lg-n13{margin-right:-3.25rem!important;margin-left:-3.25rem!important}.mx-lg-n14{margin-right:-3.5rem!important;margin-left:-3.5rem!important}.mx-lg-n15{margin-right:-3.75rem!important;margin-left:-3.75rem!important}.mx-lg-n16{margin-right:-4rem!important;margin-left:-4rem!important}.mx-lg-n17{margin-right:-4.25rem!important;margin-left:-4.25rem!important}.mx-lg-n18{margin-right:-4.5rem!important;margin-left:-4.5rem!important}.mx-lg-n19{margin-right:-4.75rem!important;margin-left:-4.75rem!important}.mx-lg-n20{margin-right:-5rem!important;margin-left:-5rem!important}.my-lg-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-lg-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-lg-n3{margin-top:-.75rem!important;margin-bottom:-.75rem!important}.my-lg-n4{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-lg-n5{margin-top:-1.25rem!important;margin-bottom:-1.25rem!important}.my-lg-n6{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-lg-n7{margin-top:-1.75rem!important;margin-bottom:-1.75rem!important}.my-lg-n8{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-lg-n9{margin-top:-2.25rem!important;margin-bottom:-2.25rem!important}.my-lg-n10{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.my-lg-n11{margin-top:-2.75rem!important;margin-bottom:-2.75rem!important}.my-lg-n12{margin-top:-3rem!important;margin-bottom:-3rem!important}.my-lg-n13{margin-top:-3.25rem!important;margin-bottom:-3.25rem!important}.my-lg-n14{margin-top:-3.5rem!important;margin-bottom:-3.5rem!important}.my-lg-n15{margin-top:-3.75rem!important;margin-bottom:-3.75rem!important}.my-lg-n16{margin-top:-4rem!important;margin-bottom:-4rem!important}.my-lg-n17{margin-top:-4.25rem!important;margin-bottom:-4.25rem!important}.my-lg-n18{margin-top:-4.5rem!important;margin-bottom:-4.5rem!important}.my-lg-n19{margin-top:-4.75rem!important;margin-bottom:-4.75rem!important}.my-lg-n20{margin-top:-5rem!important;margin-bottom:-5rem!important}.mt-lg-n1{margin-top:-.25rem!important}.mt-lg-n2{margin-top:-.5rem!important}.mt-lg-n3{margin-top:-.75rem!important}.mt-lg-n4{margin-top:-1rem!important}.mt-lg-n5{margin-top:-1.25rem!important}.mt-lg-n6{margin-top:-1.5rem!important}.mt-lg-n7{margin-top:-1.75rem!important}.mt-lg-n8{margin-top:-2rem!important}.mt-lg-n9{margin-top:-2.25rem!important}.mt-lg-n10{margin-top:-2.5rem!important}.mt-lg-n11{margin-top:-2.75rem!important}.mt-lg-n12{margin-top:-3rem!important}.mt-lg-n13{margin-top:-3.25rem!important}.mt-lg-n14{margin-top:-3.5rem!important}.mt-lg-n15{margin-top:-3.75rem!important}.mt-lg-n16{margin-top:-4rem!important}.mt-lg-n17{margin-top:-4.25rem!important}.mt-lg-n18{margin-top:-4.5rem!important}.mt-lg-n19{margin-top:-4.75rem!important}.mt-lg-n20{margin-top:-5rem!important}.me-lg-n1{margin-right:-.25rem!important}.me-lg-n2{margin-right:-.5rem!important}.me-lg-n3{margin-right:-.75rem!important}.me-lg-n4{margin-right:-1rem!important}.me-lg-n5{margin-right:-1.25rem!important}.me-lg-n6{margin-right:-1.5rem!important}.me-lg-n7{margin-right:-1.75rem!important}.me-lg-n8{margin-right:-2rem!important}.me-lg-n9{margin-right:-2.25rem!important}.me-lg-n10{margin-right:-2.5rem!important}.me-lg-n11{margin-right:-2.75rem!important}.me-lg-n12{margin-right:-3rem!important}.me-lg-n13{margin-right:-3.25rem!important}.me-lg-n14{margin-right:-3.5rem!important}.me-lg-n15{margin-right:-3.75rem!important}.me-lg-n16{margin-right:-4rem!important}.me-lg-n17{margin-right:-4.25rem!important}.me-lg-n18{margin-right:-4.5rem!important}.me-lg-n19{margin-right:-4.75rem!important}.me-lg-n20{margin-right:-5rem!important}.mb-lg-n1{margin-bottom:-.25rem!important}.mb-lg-n2{margin-bottom:-.5rem!important}.mb-lg-n3{margin-bottom:-.75rem!important}.mb-lg-n4{margin-bottom:-1rem!important}.mb-lg-n5{margin-bottom:-1.25rem!important}.mb-lg-n6{margin-bottom:-1.5rem!important}.mb-lg-n7{margin-bottom:-1.75rem!important}.mb-lg-n8{margin-bottom:-2rem!important}.mb-lg-n9{margin-bottom:-2.25rem!important}.mb-lg-n10{margin-bottom:-2.5rem!important}.mb-lg-n11{margin-bottom:-2.75rem!important}.mb-lg-n12{margin-bottom:-3rem!important}.mb-lg-n13{margin-bottom:-3.25rem!important}.mb-lg-n14{margin-bottom:-3.5rem!important}.mb-lg-n15{margin-bottom:-3.75rem!important}.mb-lg-n16{margin-bottom:-4rem!important}.mb-lg-n17{margin-bottom:-4.25rem!important}.mb-lg-n18{margin-bottom:-4.5rem!important}.mb-lg-n19{margin-bottom:-4.75rem!important}.mb-lg-n20{margin-bottom:-5rem!important}.ms-lg-n1{margin-left:-.25rem!important}.ms-lg-n2{margin-left:-.5rem!important}.ms-lg-n3{margin-left:-.75rem!important}.ms-lg-n4{margin-left:-1rem!important}.ms-lg-n5{margin-left:-1.25rem!important}.ms-lg-n6{margin-left:-1.5rem!important}.ms-lg-n7{margin-left:-1.75rem!important}.ms-lg-n8{margin-left:-2rem!important}.ms-lg-n9{margin-left:-2.25rem!important}.ms-lg-n10{margin-left:-2.5rem!important}.ms-lg-n11{margin-left:-2.75rem!important}.ms-lg-n12{margin-left:-3rem!important}.ms-lg-n13{margin-left:-3.25rem!important}.ms-lg-n14{margin-left:-3.5rem!important}.ms-lg-n15{margin-left:-3.75rem!important}.ms-lg-n16{margin-left:-4rem!important}.ms-lg-n17{margin-left:-4.25rem!important}.ms-lg-n18{margin-left:-4.5rem!important}.ms-lg-n19{margin-left:-4.75rem!important}.ms-lg-n20{margin-left:-5rem!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:.75rem!important}.p-lg-4{padding:1rem!important}.p-lg-5{padding:1.25rem!important}.p-lg-6{padding:1.5rem!important}.p-lg-7{padding:1.75rem!important}.p-lg-8{padding:2rem!important}.p-lg-9{padding:2.25rem!important}.p-lg-10{padding:2.5rem!important}.p-lg-11{padding:2.75rem!important}.p-lg-12{padding:3rem!important}.p-lg-13{padding:3.25rem!important}.p-lg-14{padding:3.5rem!important}.p-lg-15{padding:3.75rem!important}.p-lg-16{padding:4rem!important}.p-lg-17{padding:4.25rem!important}.p-lg-18{padding:4.5rem!important}.p-lg-19{padding:4.75rem!important}.p-lg-20{padding:5rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:.75rem!important;padding-left:.75rem!important}.px-lg-4{padding-right:1rem!important;padding-left:1rem!important}.px-lg-5{padding-right:1.25rem!important;padding-left:1.25rem!important}.px-lg-6{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-7{padding-right:1.75rem!important;padding-left:1.75rem!important}.px-lg-8{padding-right:2rem!important;padding-left:2rem!important}.px-lg-9{padding-right:2.25rem!important;padding-left:2.25rem!important}.px-lg-10{padding-right:2.5rem!important;padding-left:2.5rem!important}.px-lg-11{padding-right:2.75rem!important;padding-left:2.75rem!important}.px-lg-12{padding-right:3rem!important;padding-left:3rem!important}.px-lg-13{padding-right:3.25rem!important;padding-left:3.25rem!important}.px-lg-14{padding-right:3.5rem!important;padding-left:3.5rem!important}.px-lg-15{padding-right:3.75rem!important;padding-left:3.75rem!important}.px-lg-16{padding-right:4rem!important;padding-left:4rem!important}.px-lg-17{padding-right:4.25rem!important;padding-left:4.25rem!important}.px-lg-18{padding-right:4.5rem!important;padding-left:4.5rem!important}.px-lg-19{padding-right:4.75rem!important;padding-left:4.75rem!important}.px-lg-20{padding-right:5rem!important;padding-left:5rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:.75rem!important;padding-bottom:.75rem!important}.py-lg-4{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-5{padding-top:1.25rem!important;padding-bottom:1.25rem!important}.py-lg-6{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-7{padding-top:1.75rem!important;padding-bottom:1.75rem!important}.py-lg-8{padding-top:2rem!important;padding-bottom:2rem!important}.py-lg-9{padding-top:2.25rem!important;padding-bottom:2.25rem!important}.py-lg-10{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.py-lg-11{padding-top:2.75rem!important;padding-bottom:2.75rem!important}.py-lg-12{padding-top:3rem!important;padding-bottom:3rem!important}.py-lg-13{padding-top:3.25rem!important;padding-bottom:3.25rem!important}.py-lg-14{padding-top:3.5rem!important;padding-bottom:3.5rem!important}.py-lg-15{padding-top:3.75rem!important;padding-bottom:3.75rem!important}.py-lg-16{padding-top:4rem!important;padding-bottom:4rem!important}.py-lg-17{padding-top:4.25rem!important;padding-bottom:4.25rem!important}.py-lg-18{padding-top:4.5rem!important;padding-bottom:4.5rem!important}.py-lg-19{padding-top:4.75rem!important;padding-bottom:4.75rem!important}.py-lg-20{padding-top:5rem!important;padding-bottom:5rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:.75rem!important}.pt-lg-4{padding-top:1rem!important}.pt-lg-5{padding-top:1.25rem!important}.pt-lg-6{padding-top:1.5rem!important}.pt-lg-7{padding-top:1.75rem!important}.pt-lg-8{padding-top:2rem!important}.pt-lg-9{padding-top:2.25rem!important}.pt-lg-10{padding-top:2.5rem!important}.pt-lg-11{padding-top:2.75rem!important}.pt-lg-12{padding-top:3rem!important}.pt-lg-13{padding-top:3.25rem!important}.pt-lg-14{padding-top:3.5rem!important}.pt-lg-15{padding-top:3.75rem!important}.pt-lg-16{padding-top:4rem!important}.pt-lg-17{padding-top:4.25rem!important}.pt-lg-18{padding-top:4.5rem!important}.pt-lg-19{padding-top:4.75rem!important}.pt-lg-20{padding-top:5rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:.75rem!important}.pe-lg-4{padding-right:1rem!important}.pe-lg-5{padding-right:1.25rem!important}.pe-lg-6{padding-right:1.5rem!important}.pe-lg-7{padding-right:1.75rem!important}.pe-lg-8{padding-right:2rem!important}.pe-lg-9{padding-right:2.25rem!important}.pe-lg-10{padding-right:2.5rem!important}.pe-lg-11{padding-right:2.75rem!important}.pe-lg-12{padding-right:3rem!important}.pe-lg-13{padding-right:3.25rem!important}.pe-lg-14{padding-right:3.5rem!important}.pe-lg-15{padding-right:3.75rem!important}.pe-lg-16{padding-right:4rem!important}.pe-lg-17{padding-right:4.25rem!important}.pe-lg-18{padding-right:4.5rem!important}.pe-lg-19{padding-right:4.75rem!important}.pe-lg-20{padding-right:5rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:.75rem!important}.pb-lg-4{padding-bottom:1rem!important}.pb-lg-5{padding-bottom:1.25rem!important}.pb-lg-6{padding-bottom:1.5rem!important}.pb-lg-7{padding-bottom:1.75rem!important}.pb-lg-8{padding-bottom:2rem!important}.pb-lg-9{padding-bottom:2.25rem!important}.pb-lg-10{padding-bottom:2.5rem!important}.pb-lg-11{padding-bottom:2.75rem!important}.pb-lg-12{padding-bottom:3rem!important}.pb-lg-13{padding-bottom:3.25rem!important}.pb-lg-14{padding-bottom:3.5rem!important}.pb-lg-15{padding-bottom:3.75rem!important}.pb-lg-16{padding-bottom:4rem!important}.pb-lg-17{padding-bottom:4.25rem!important}.pb-lg-18{padding-bottom:4.5rem!important}.pb-lg-19{padding-bottom:4.75rem!important}.pb-lg-20{padding-bottom:5rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:.75rem!important}.ps-lg-4{padding-left:1rem!important}.ps-lg-5{padding-left:1.25rem!important}.ps-lg-6{padding-left:1.5rem!important}.ps-lg-7{padding-left:1.75rem!important}.ps-lg-8{padding-left:2rem!important}.ps-lg-9{padding-left:2.25rem!important}.ps-lg-10{padding-left:2.5rem!important}.ps-lg-11{padding-left:2.75rem!important}.ps-lg-12{padding-left:3rem!important}.ps-lg-13{padding-left:3.25rem!important}.ps-lg-14{padding-left:3.5rem!important}.ps-lg-15{padding-left:3.75rem!important}.ps-lg-16{padding-left:4rem!important}.ps-lg-17{padding-left:4.25rem!important}.ps-lg-18{padding-left:4.5rem!important}.ps-lg-19{padding-left:4.75rem!important}.ps-lg-20{padding-left:5rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:.75rem!important}.gap-lg-4{gap:1rem!important}.gap-lg-5{gap:1.25rem!important}.gap-lg-6{gap:1.5rem!important}.gap-lg-7{gap:1.75rem!important}.gap-lg-8{gap:2rem!important}.gap-lg-9{gap:2.25rem!important}.gap-lg-10{gap:2.5rem!important}.gap-lg-11{gap:2.75rem!important}.gap-lg-12{gap:3rem!important}.gap-lg-13{gap:3.25rem!important}.gap-lg-14{gap:3.5rem!important}.gap-lg-15{gap:3.75rem!important}.gap-lg-16{gap:4rem!important}.gap-lg-17{gap:4.25rem!important}.gap-lg-18{gap:4.5rem!important}.gap-lg-19{gap:4.75rem!important}.gap-lg-20{gap:5rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:.75rem!important}.row-gap-lg-4{row-gap:1rem!important}.row-gap-lg-5{row-gap:1.25rem!important}.row-gap-lg-6{row-gap:1.5rem!important}.row-gap-lg-7{row-gap:1.75rem!important}.row-gap-lg-8{row-gap:2rem!important}.row-gap-lg-9{row-gap:2.25rem!important}.row-gap-lg-10{row-gap:2.5rem!important}.row-gap-lg-11{row-gap:2.75rem!important}.row-gap-lg-12{row-gap:3rem!important}.row-gap-lg-13{row-gap:3.25rem!important}.row-gap-lg-14{row-gap:3.5rem!important}.row-gap-lg-15{row-gap:3.75rem!important}.row-gap-lg-16{row-gap:4rem!important}.row-gap-lg-17{row-gap:4.25rem!important}.row-gap-lg-18{row-gap:4.5rem!important}.row-gap-lg-19{row-gap:4.75rem!important}.row-gap-lg-20{row-gap:5rem!important}.column-gap-lg-0{column-gap:0!important}.column-gap-lg-1{column-gap:.25rem!important}.column-gap-lg-2{column-gap:.5rem!important}.column-gap-lg-3{column-gap:.75rem!important}.column-gap-lg-4{column-gap:1rem!important}.column-gap-lg-5{column-gap:1.25rem!important}.column-gap-lg-6{column-gap:1.5rem!important}.column-gap-lg-7{column-gap:1.75rem!important}.column-gap-lg-8{column-gap:2rem!important}.column-gap-lg-9{column-gap:2.25rem!important}.column-gap-lg-10{column-gap:2.5rem!important}.column-gap-lg-11{column-gap:2.75rem!important}.column-gap-lg-12{column-gap:3rem!important}.column-gap-lg-13{column-gap:3.25rem!important}.column-gap-lg-14{column-gap:3.5rem!important}.column-gap-lg-15{column-gap:3.75rem!important}.column-gap-lg-16{column-gap:4rem!important}.column-gap-lg-17{column-gap:4.25rem!important}.column-gap-lg-18{column-gap:4.5rem!important}.column-gap-lg-19{column-gap:4.75rem!important}.column-gap-lg-20{column-gap:5rem!important}.fs-lg-1{font-size:calc(1.3rem + .6vw)!important}.fs-lg-2{font-size:calc(1.275rem + .3vw)!important}.fs-lg-3{font-size:calc(1.26rem + .12vw)!important}.fs-lg-4{font-size:1.25rem!important}.fs-lg-5{font-size:1.15rem!important}.fs-lg-6{font-size:1.075rem!important}.fs-lg-7{font-size:.95rem!important}.fs-lg-8{font-size:.85rem!important}.fs-lg-9{font-size:.75rem!important}.fs-lg-10{font-size:.5rem!important}.fs-lg-sm{font-size:.95rem!important}.fs-lg-base{font-size:1rem!important}.fs-lg-lg{font-size:1.075rem!important}.fs-lg-xl{font-size:1.21rem!important}.fs-lg-fluid{font-size:100%!important}.fs-lg-2x{font-size:calc(1.325rem + .9vw)!important}.fs-lg-2qx{font-size:calc(1.35rem + 1.2vw)!important}.fs-lg-2hx{font-size:calc(1.375rem + 1.5vw)!important}.fs-lg-2tx{font-size:calc(1.4rem + 1.8vw)!important}.fs-lg-3x{font-size:calc(1.425rem + 2.1vw)!important}.fs-lg-3qx{font-size:calc(1.45rem + 2.4vw)!important}.fs-lg-3hx{font-size:calc(1.475rem + 2.7vw)!important}.fs-lg-3tx{font-size:calc(1.5rem + 3vw)!important}.fs-lg-4x{font-size:calc(1.525rem + 3.3vw)!important}.fs-lg-4qx{font-size:calc(1.55rem + 3.6vw)!important}.fs-lg-4hx{font-size:calc(1.575rem + 3.9vw)!important}.fs-lg-4tx{font-size:calc(1.6rem + 4.2vw)!important}.fs-lg-5x{font-size:calc(1.625rem + 4.5vw)!important}.fs-lg-5qx{font-size:calc(1.65rem + 4.8vw)!important}.fs-lg-5hx{font-size:calc(1.675rem + 5.1vw)!important}.fs-lg-5tx{font-size:calc(1.7rem + 5.4vw)!important}.fs-lg-6x{font-size:calc(1.725rem + 5.7vw)!important}.fs-lg-6qx{font-size:calc(1.75rem + 6vw)!important}.fs-lg-6hx{font-size:calc(1.775rem + 6.3vw)!important}.fs-lg-6tx{font-size:calc(1.8rem + 6.6vw)!important}.fs-lg-7x{font-size:calc(1.825rem + 6.9vw)!important}.fs-lg-7qx{font-size:calc(1.85rem + 7.2vw)!important}.fs-lg-7hx{font-size:calc(1.875rem + 7.5vw)!important}.fs-lg-7tx{font-size:calc(1.9rem + 7.8vw)!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}.min-w-lg-unset{min-width:unset!important}.min-w-lg-25{min-width:25%!important}.min-w-lg-50{min-width:50%!important}.min-w-lg-75{min-width:75%!important}.min-w-lg-100{min-width:100%!important}.min-w-lg-auto{min-width:auto!important}.min-w-lg-1px{min-width:1px!important}.min-w-lg-2px{min-width:2px!important}.min-w-lg-3px{min-width:3px!important}.min-w-lg-4px{min-width:4px!important}.min-w-lg-5px{min-width:5px!important}.min-w-lg-6px{min-width:6px!important}.min-w-lg-7px{min-width:7px!important}.min-w-lg-8px{min-width:8px!important}.min-w-lg-9px{min-width:9px!important}.min-w-lg-10px{min-width:10px!important}.min-w-lg-15px{min-width:15px!important}.min-w-lg-20px{min-width:20px!important}.min-w-lg-25px{min-width:25px!important}.min-w-lg-30px{min-width:30px!important}.min-w-lg-35px{min-width:35px!important}.min-w-lg-40px{min-width:40px!important}.min-w-lg-45px{min-width:45px!important}.min-w-lg-50px{min-width:50px!important}.min-w-lg-55px{min-width:55px!important}.min-w-lg-60px{min-width:60px!important}.min-w-lg-65px{min-width:65px!important}.min-w-lg-70px{min-width:70px!important}.min-w-lg-75px{min-width:75px!important}.min-w-lg-80px{min-width:80px!important}.min-w-lg-85px{min-width:85px!important}.min-w-lg-90px{min-width:90px!important}.min-w-lg-95px{min-width:95px!important}.min-w-lg-100px{min-width:100px!important}.min-w-lg-125px{min-width:125px!important}.min-w-lg-150px{min-width:150px!important}.min-w-lg-175px{min-width:175px!important}.min-w-lg-200px{min-width:200px!important}.min-w-lg-225px{min-width:225px!important}.min-w-lg-250px{min-width:250px!important}.min-w-lg-275px{min-width:275px!important}.min-w-lg-300px{min-width:300px!important}.min-w-lg-325px{min-width:325px!important}.min-w-lg-350px{min-width:350px!important}.min-w-lg-375px{min-width:375px!important}.min-w-lg-400px{min-width:400px!important}.min-w-lg-425px{min-width:425px!important}.min-w-lg-450px{min-width:450px!important}.min-w-lg-475px{min-width:475px!important}.min-w-lg-500px{min-width:500px!important}.min-w-lg-550px{min-width:550px!important}.min-w-lg-600px{min-width:600px!important}.min-w-lg-650px{min-width:650px!important}.min-w-lg-700px{min-width:700px!important}.min-w-lg-750px{min-width:750px!important}.min-w-lg-800px{min-width:800px!important}.min-w-lg-850px{min-width:850px!important}.min-w-lg-900px{min-width:900px!important}.min-w-lg-950px{min-width:950px!important}.min-w-lg-1000px{min-width:1000px!important}.min-h-lg-unset{min-height:unset!important}.min-h-lg-25{min-height:25%!important}.min-h-lg-50{min-height:50%!important}.min-h-lg-75{min-height:75%!important}.min-h-lg-100{min-height:100%!important}.min-h-lg-auto{min-height:auto!important}.min-h-lg-1px{min-height:1px!important}.min-h-lg-2px{min-height:2px!important}.min-h-lg-3px{min-height:3px!important}.min-h-lg-4px{min-height:4px!important}.min-h-lg-5px{min-height:5px!important}.min-h-lg-6px{min-height:6px!important}.min-h-lg-7px{min-height:7px!important}.min-h-lg-8px{min-height:8px!important}.min-h-lg-9px{min-height:9px!important}.min-h-lg-10px{min-height:10px!important}.min-h-lg-15px{min-height:15px!important}.min-h-lg-20px{min-height:20px!important}.min-h-lg-25px{min-height:25px!important}.min-h-lg-30px{min-height:30px!important}.min-h-lg-35px{min-height:35px!important}.min-h-lg-40px{min-height:40px!important}.min-h-lg-45px{min-height:45px!important}.min-h-lg-50px{min-height:50px!important}.min-h-lg-55px{min-height:55px!important}.min-h-lg-60px{min-height:60px!important}.min-h-lg-65px{min-height:65px!important}.min-h-lg-70px{min-height:70px!important}.min-h-lg-75px{min-height:75px!important}.min-h-lg-80px{min-height:80px!important}.min-h-lg-85px{min-height:85px!important}.min-h-lg-90px{min-height:90px!important}.min-h-lg-95px{min-height:95px!important}.min-h-lg-100px{min-height:100px!important}.min-h-lg-125px{min-height:125px!important}.min-h-lg-150px{min-height:150px!important}.min-h-lg-175px{min-height:175px!important}.min-h-lg-200px{min-height:200px!important}.min-h-lg-225px{min-height:225px!important}.min-h-lg-250px{min-height:250px!important}.min-h-lg-275px{min-height:275px!important}.min-h-lg-300px{min-height:300px!important}.min-h-lg-325px{min-height:325px!important}.min-h-lg-350px{min-height:350px!important}.min-h-lg-375px{min-height:375px!important}.min-h-lg-400px{min-height:400px!important}.min-h-lg-425px{min-height:425px!important}.min-h-lg-450px{min-height:450px!important}.min-h-lg-475px{min-height:475px!important}.min-h-lg-500px{min-height:500px!important}.min-h-lg-550px{min-height:550px!important}.min-h-lg-600px{min-height:600px!important}.min-h-lg-650px{min-height:650px!important}.min-h-lg-700px{min-height:700px!important}.min-h-lg-750px{min-height:750px!important}.min-h-lg-800px{min-height:800px!important}.min-h-lg-850px{min-height:850px!important}.min-h-lg-900px{min-height:900px!important}.min-h-lg-950px{min-height:950px!important}.min-h-lg-1000px{min-height:1000px!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{object-fit:contain!important}.object-fit-xl-cover{object-fit:cover!important}.object-fit-xl-fill{object-fit:fill!important}.object-fit-xl-scale{object-fit:scale-down!important}.object-fit-xl-none{object-fit:none!important}.overflow-xl-auto{overflow:auto!important}.overflow-xl-hidden{overflow:hidden!important}.overflow-xl-visible{overflow:visible!important}.overflow-xl-scroll{overflow:scroll!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.position-xl-static{position:static!important}.position-xl-relative{position:relative!important}.position-xl-absolute{position:absolute!important}.position-xl-fixed{position:fixed!important}.position-xl-sticky{position:sticky!important}.w-xl-unset{width:unset!important}.w-xl-25{width:25%!important}.w-xl-50{width:50%!important}.w-xl-75{width:75%!important}.w-xl-100{width:100%!important}.w-xl-auto{width:auto!important}.w-xl-1px{width:1px!important}.w-xl-2px{width:2px!important}.w-xl-3px{width:3px!important}.w-xl-4px{width:4px!important}.w-xl-5px{width:5px!important}.w-xl-6px{width:6px!important}.w-xl-7px{width:7px!important}.w-xl-8px{width:8px!important}.w-xl-9px{width:9px!important}.w-xl-10px{width:10px!important}.w-xl-15px{width:15px!important}.w-xl-20px{width:20px!important}.w-xl-25px{width:25px!important}.w-xl-30px{width:30px!important}.w-xl-35px{width:35px!important}.w-xl-40px{width:40px!important}.w-xl-45px{width:45px!important}.w-xl-50px{width:50px!important}.w-xl-55px{width:55px!important}.w-xl-60px{width:60px!important}.w-xl-65px{width:65px!important}.w-xl-70px{width:70px!important}.w-xl-75px{width:75px!important}.w-xl-80px{width:80px!important}.w-xl-85px{width:85px!important}.w-xl-90px{width:90px!important}.w-xl-95px{width:95px!important}.w-xl-100px{width:100px!important}.w-xl-125px{width:125px!important}.w-xl-150px{width:150px!important}.w-xl-175px{width:175px!important}.w-xl-200px{width:200px!important}.w-xl-225px{width:225px!important}.w-xl-250px{width:250px!important}.w-xl-275px{width:275px!important}.w-xl-300px{width:300px!important}.w-xl-325px{width:325px!important}.w-xl-350px{width:350px!important}.w-xl-375px{width:375px!important}.w-xl-400px{width:400px!important}.w-xl-425px{width:425px!important}.w-xl-450px{width:450px!important}.w-xl-475px{width:475px!important}.w-xl-500px{width:500px!important}.w-xl-550px{width:550px!important}.w-xl-600px{width:600px!important}.w-xl-650px{width:650px!important}.w-xl-700px{width:700px!important}.w-xl-750px{width:750px!important}.w-xl-800px{width:800px!important}.w-xl-850px{width:850px!important}.w-xl-900px{width:900px!important}.w-xl-950px{width:950px!important}.w-xl-1000px{width:1000px!important}.mw-xl-unset{max-width:unset!important}.mw-xl-25{max-width:25%!important}.mw-xl-50{max-width:50%!important}.mw-xl-75{max-width:75%!important}.mw-xl-100{max-width:100%!important}.mw-xl-auto{max-width:auto!important}.mw-xl-1px{max-width:1px!important}.mw-xl-2px{max-width:2px!important}.mw-xl-3px{max-width:3px!important}.mw-xl-4px{max-width:4px!important}.mw-xl-5px{max-width:5px!important}.mw-xl-6px{max-width:6px!important}.mw-xl-7px{max-width:7px!important}.mw-xl-8px{max-width:8px!important}.mw-xl-9px{max-width:9px!important}.mw-xl-10px{max-width:10px!important}.mw-xl-15px{max-width:15px!important}.mw-xl-20px{max-width:20px!important}.mw-xl-25px{max-width:25px!important}.mw-xl-30px{max-width:30px!important}.mw-xl-35px{max-width:35px!important}.mw-xl-40px{max-width:40px!important}.mw-xl-45px{max-width:45px!important}.mw-xl-50px{max-width:50px!important}.mw-xl-55px{max-width:55px!important}.mw-xl-60px{max-width:60px!important}.mw-xl-65px{max-width:65px!important}.mw-xl-70px{max-width:70px!important}.mw-xl-75px{max-width:75px!important}.mw-xl-80px{max-width:80px!important}.mw-xl-85px{max-width:85px!important}.mw-xl-90px{max-width:90px!important}.mw-xl-95px{max-width:95px!important}.mw-xl-100px{max-width:100px!important}.mw-xl-125px{max-width:125px!important}.mw-xl-150px{max-width:150px!important}.mw-xl-175px{max-width:175px!important}.mw-xl-200px{max-width:200px!important}.mw-xl-225px{max-width:225px!important}.mw-xl-250px{max-width:250px!important}.mw-xl-275px{max-width:275px!important}.mw-xl-300px{max-width:300px!important}.mw-xl-325px{max-width:325px!important}.mw-xl-350px{max-width:350px!important}.mw-xl-375px{max-width:375px!important}.mw-xl-400px{max-width:400px!important}.mw-xl-425px{max-width:425px!important}.mw-xl-450px{max-width:450px!important}.mw-xl-475px{max-width:475px!important}.mw-xl-500px{max-width:500px!important}.mw-xl-550px{max-width:550px!important}.mw-xl-600px{max-width:600px!important}.mw-xl-650px{max-width:650px!important}.mw-xl-700px{max-width:700px!important}.mw-xl-750px{max-width:750px!important}.mw-xl-800px{max-width:800px!important}.mw-xl-850px{max-width:850px!important}.mw-xl-900px{max-width:900px!important}.mw-xl-950px{max-width:950px!important}.mw-xl-1000px{max-width:1000px!important}.h-xl-unset{height:unset!important}.h-xl-25{height:25%!important}.h-xl-50{height:50%!important}.h-xl-75{height:75%!important}.h-xl-100{height:100%!important}.h-xl-auto{height:auto!important}.h-xl-1px{height:1px!important}.h-xl-2px{height:2px!important}.h-xl-3px{height:3px!important}.h-xl-4px{height:4px!important}.h-xl-5px{height:5px!important}.h-xl-6px{height:6px!important}.h-xl-7px{height:7px!important}.h-xl-8px{height:8px!important}.h-xl-9px{height:9px!important}.h-xl-10px{height:10px!important}.h-xl-15px{height:15px!important}.h-xl-20px{height:20px!important}.h-xl-25px{height:25px!important}.h-xl-30px{height:30px!important}.h-xl-35px{height:35px!important}.h-xl-40px{height:40px!important}.h-xl-45px{height:45px!important}.h-xl-50px{height:50px!important}.h-xl-55px{height:55px!important}.h-xl-60px{height:60px!important}.h-xl-65px{height:65px!important}.h-xl-70px{height:70px!important}.h-xl-75px{height:75px!important}.h-xl-80px{height:80px!important}.h-xl-85px{height:85px!important}.h-xl-90px{height:90px!important}.h-xl-95px{height:95px!important}.h-xl-100px{height:100px!important}.h-xl-125px{height:125px!important}.h-xl-150px{height:150px!important}.h-xl-175px{height:175px!important}.h-xl-200px{height:200px!important}.h-xl-225px{height:225px!important}.h-xl-250px{height:250px!important}.h-xl-275px{height:275px!important}.h-xl-300px{height:300px!important}.h-xl-325px{height:325px!important}.h-xl-350px{height:350px!important}.h-xl-375px{height:375px!important}.h-xl-400px{height:400px!important}.h-xl-425px{height:425px!important}.h-xl-450px{height:450px!important}.h-xl-475px{height:475px!important}.h-xl-500px{height:500px!important}.h-xl-550px{height:550px!important}.h-xl-600px{height:600px!important}.h-xl-650px{height:650px!important}.h-xl-700px{height:700px!important}.h-xl-750px{height:750px!important}.h-xl-800px{height:800px!important}.h-xl-850px{height:850px!important}.h-xl-900px{height:900px!important}.h-xl-950px{height:950px!important}.h-xl-1000px{height:1000px!important}.mh-xl-unset{max-height:unset!important}.mh-xl-25{max-height:25%!important}.mh-xl-50{max-height:50%!important}.mh-xl-75{max-height:75%!important}.mh-xl-100{max-height:100%!important}.mh-xl-auto{max-height:auto!important}.mh-xl-1px{max-height:1px!important}.mh-xl-2px{max-height:2px!important}.mh-xl-3px{max-height:3px!important}.mh-xl-4px{max-height:4px!important}.mh-xl-5px{max-height:5px!important}.mh-xl-6px{max-height:6px!important}.mh-xl-7px{max-height:7px!important}.mh-xl-8px{max-height:8px!important}.mh-xl-9px{max-height:9px!important}.mh-xl-10px{max-height:10px!important}.mh-xl-15px{max-height:15px!important}.mh-xl-20px{max-height:20px!important}.mh-xl-25px{max-height:25px!important}.mh-xl-30px{max-height:30px!important}.mh-xl-35px{max-height:35px!important}.mh-xl-40px{max-height:40px!important}.mh-xl-45px{max-height:45px!important}.mh-xl-50px{max-height:50px!important}.mh-xl-55px{max-height:55px!important}.mh-xl-60px{max-height:60px!important}.mh-xl-65px{max-height:65px!important}.mh-xl-70px{max-height:70px!important}.mh-xl-75px{max-height:75px!important}.mh-xl-80px{max-height:80px!important}.mh-xl-85px{max-height:85px!important}.mh-xl-90px{max-height:90px!important}.mh-xl-95px{max-height:95px!important}.mh-xl-100px{max-height:100px!important}.mh-xl-125px{max-height:125px!important}.mh-xl-150px{max-height:150px!important}.mh-xl-175px{max-height:175px!important}.mh-xl-200px{max-height:200px!important}.mh-xl-225px{max-height:225px!important}.mh-xl-250px{max-height:250px!important}.mh-xl-275px{max-height:275px!important}.mh-xl-300px{max-height:300px!important}.mh-xl-325px{max-height:325px!important}.mh-xl-350px{max-height:350px!important}.mh-xl-375px{max-height:375px!important}.mh-xl-400px{max-height:400px!important}.mh-xl-425px{max-height:425px!important}.mh-xl-450px{max-height:450px!important}.mh-xl-475px{max-height:475px!important}.mh-xl-500px{max-height:500px!important}.mh-xl-550px{max-height:550px!important}.mh-xl-600px{max-height:600px!important}.mh-xl-650px{max-height:650px!important}.mh-xl-700px{max-height:700px!important}.mh-xl-750px{max-height:750px!important}.mh-xl-800px{max-height:800px!important}.mh-xl-850px{max-height:850px!important}.mh-xl-900px{max-height:900px!important}.mh-xl-950px{max-height:950px!important}.mh-xl-1000px{max-height:1000px!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:.75rem!important}.m-xl-4{margin:1rem!important}.m-xl-5{margin:1.25rem!important}.m-xl-6{margin:1.5rem!important}.m-xl-7{margin:1.75rem!important}.m-xl-8{margin:2rem!important}.m-xl-9{margin:2.25rem!important}.m-xl-10{margin:2.5rem!important}.m-xl-11{margin:2.75rem!important}.m-xl-12{margin:3rem!important}.m-xl-13{margin:3.25rem!important}.m-xl-14{margin:3.5rem!important}.m-xl-15{margin:3.75rem!important}.m-xl-16{margin:4rem!important}.m-xl-17{margin:4.25rem!important}.m-xl-18{margin:4.5rem!important}.m-xl-19{margin:4.75rem!important}.m-xl-20{margin:5rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:.75rem!important;margin-left:.75rem!important}.mx-xl-4{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-5{margin-right:1.25rem!important;margin-left:1.25rem!important}.mx-xl-6{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-7{margin-right:1.75rem!important;margin-left:1.75rem!important}.mx-xl-8{margin-right:2rem!important;margin-left:2rem!important}.mx-xl-9{margin-right:2.25rem!important;margin-left:2.25rem!important}.mx-xl-10{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-xl-11{margin-right:2.75rem!important;margin-left:2.75rem!important}.mx-xl-12{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-13{margin-right:3.25rem!important;margin-left:3.25rem!important}.mx-xl-14{margin-right:3.5rem!important;margin-left:3.5rem!important}.mx-xl-15{margin-right:3.75rem!important;margin-left:3.75rem!important}.mx-xl-16{margin-right:4rem!important;margin-left:4rem!important}.mx-xl-17{margin-right:4.25rem!important;margin-left:4.25rem!important}.mx-xl-18{margin-right:4.5rem!important;margin-left:4.5rem!important}.mx-xl-19{margin-right:4.75rem!important;margin-left:4.75rem!important}.mx-xl-20{margin-right:5rem!important;margin-left:5rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:.75rem!important;margin-bottom:.75rem!important}.my-xl-4{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-5{margin-top:1.25rem!important;margin-bottom:1.25rem!important}.my-xl-6{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-7{margin-top:1.75rem!important;margin-bottom:1.75rem!important}.my-xl-8{margin-top:2rem!important;margin-bottom:2rem!important}.my-xl-9{margin-top:2.25rem!important;margin-bottom:2.25rem!important}.my-xl-10{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-xl-11{margin-top:2.75rem!important;margin-bottom:2.75rem!important}.my-xl-12{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-13{margin-top:3.25rem!important;margin-bottom:3.25rem!important}.my-xl-14{margin-top:3.5rem!important;margin-bottom:3.5rem!important}.my-xl-15{margin-top:3.75rem!important;margin-bottom:3.75rem!important}.my-xl-16{margin-top:4rem!important;margin-bottom:4rem!important}.my-xl-17{margin-top:4.25rem!important;margin-bottom:4.25rem!important}.my-xl-18{margin-top:4.5rem!important;margin-bottom:4.5rem!important}.my-xl-19{margin-top:4.75rem!important;margin-bottom:4.75rem!important}.my-xl-20{margin-top:5rem!important;margin-bottom:5rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:.75rem!important}.mt-xl-4{margin-top:1rem!important}.mt-xl-5{margin-top:1.25rem!important}.mt-xl-6{margin-top:1.5rem!important}.mt-xl-7{margin-top:1.75rem!important}.mt-xl-8{margin-top:2rem!important}.mt-xl-9{margin-top:2.25rem!important}.mt-xl-10{margin-top:2.5rem!important}.mt-xl-11{margin-top:2.75rem!important}.mt-xl-12{margin-top:3rem!important}.mt-xl-13{margin-top:3.25rem!important}.mt-xl-14{margin-top:3.5rem!important}.mt-xl-15{margin-top:3.75rem!important}.mt-xl-16{margin-top:4rem!important}.mt-xl-17{margin-top:4.25rem!important}.mt-xl-18{margin-top:4.5rem!important}.mt-xl-19{margin-top:4.75rem!important}.mt-xl-20{margin-top:5rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:.75rem!important}.me-xl-4{margin-right:1rem!important}.me-xl-5{margin-right:1.25rem!important}.me-xl-6{margin-right:1.5rem!important}.me-xl-7{margin-right:1.75rem!important}.me-xl-8{margin-right:2rem!important}.me-xl-9{margin-right:2.25rem!important}.me-xl-10{margin-right:2.5rem!important}.me-xl-11{margin-right:2.75rem!important}.me-xl-12{margin-right:3rem!important}.me-xl-13{margin-right:3.25rem!important}.me-xl-14{margin-right:3.5rem!important}.me-xl-15{margin-right:3.75rem!important}.me-xl-16{margin-right:4rem!important}.me-xl-17{margin-right:4.25rem!important}.me-xl-18{margin-right:4.5rem!important}.me-xl-19{margin-right:4.75rem!important}.me-xl-20{margin-right:5rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:.75rem!important}.mb-xl-4{margin-bottom:1rem!important}.mb-xl-5{margin-bottom:1.25rem!important}.mb-xl-6{margin-bottom:1.5rem!important}.mb-xl-7{margin-bottom:1.75rem!important}.mb-xl-8{margin-bottom:2rem!important}.mb-xl-9{margin-bottom:2.25rem!important}.mb-xl-10{margin-bottom:2.5rem!important}.mb-xl-11{margin-bottom:2.75rem!important}.mb-xl-12{margin-bottom:3rem!important}.mb-xl-13{margin-bottom:3.25rem!important}.mb-xl-14{margin-bottom:3.5rem!important}.mb-xl-15{margin-bottom:3.75rem!important}.mb-xl-16{margin-bottom:4rem!important}.mb-xl-17{margin-bottom:4.25rem!important}.mb-xl-18{margin-bottom:4.5rem!important}.mb-xl-19{margin-bottom:4.75rem!important}.mb-xl-20{margin-bottom:5rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:.75rem!important}.ms-xl-4{margin-left:1rem!important}.ms-xl-5{margin-left:1.25rem!important}.ms-xl-6{margin-left:1.5rem!important}.ms-xl-7{margin-left:1.75rem!important}.ms-xl-8{margin-left:2rem!important}.ms-xl-9{margin-left:2.25rem!important}.ms-xl-10{margin-left:2.5rem!important}.ms-xl-11{margin-left:2.75rem!important}.ms-xl-12{margin-left:3rem!important}.ms-xl-13{margin-left:3.25rem!important}.ms-xl-14{margin-left:3.5rem!important}.ms-xl-15{margin-left:3.75rem!important}.ms-xl-16{margin-left:4rem!important}.ms-xl-17{margin-left:4.25rem!important}.ms-xl-18{margin-left:4.5rem!important}.ms-xl-19{margin-left:4.75rem!important}.ms-xl-20{margin-left:5rem!important}.ms-xl-auto{margin-left:auto!important}.m-xl-n1{margin:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.m-xl-n3{margin:-.75rem!important}.m-xl-n4{margin:-1rem!important}.m-xl-n5{margin:-1.25rem!important}.m-xl-n6{margin:-1.5rem!important}.m-xl-n7{margin:-1.75rem!important}.m-xl-n8{margin:-2rem!important}.m-xl-n9{margin:-2.25rem!important}.m-xl-n10{margin:-2.5rem!important}.m-xl-n11{margin:-2.75rem!important}.m-xl-n12{margin:-3rem!important}.m-xl-n13{margin:-3.25rem!important}.m-xl-n14{margin:-3.5rem!important}.m-xl-n15{margin:-3.75rem!important}.m-xl-n16{margin:-4rem!important}.m-xl-n17{margin:-4.25rem!important}.m-xl-n18{margin:-4.5rem!important}.m-xl-n19{margin:-4.75rem!important}.m-xl-n20{margin:-5rem!important}.mx-xl-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-xl-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-xl-n3{margin-right:-.75rem!important;margin-left:-.75rem!important}.mx-xl-n4{margin-right:-1rem!important;margin-left:-1rem!important}.mx-xl-n5{margin-right:-1.25rem!important;margin-left:-1.25rem!important}.mx-xl-n6{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-xl-n7{margin-right:-1.75rem!important;margin-left:-1.75rem!important}.mx-xl-n8{margin-right:-2rem!important;margin-left:-2rem!important}.mx-xl-n9{margin-right:-2.25rem!important;margin-left:-2.25rem!important}.mx-xl-n10{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.mx-xl-n11{margin-right:-2.75rem!important;margin-left:-2.75rem!important}.mx-xl-n12{margin-right:-3rem!important;margin-left:-3rem!important}.mx-xl-n13{margin-right:-3.25rem!important;margin-left:-3.25rem!important}.mx-xl-n14{margin-right:-3.5rem!important;margin-left:-3.5rem!important}.mx-xl-n15{margin-right:-3.75rem!important;margin-left:-3.75rem!important}.mx-xl-n16{margin-right:-4rem!important;margin-left:-4rem!important}.mx-xl-n17{margin-right:-4.25rem!important;margin-left:-4.25rem!important}.mx-xl-n18{margin-right:-4.5rem!important;margin-left:-4.5rem!important}.mx-xl-n19{margin-right:-4.75rem!important;margin-left:-4.75rem!important}.mx-xl-n20{margin-right:-5rem!important;margin-left:-5rem!important}.my-xl-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-xl-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-xl-n3{margin-top:-.75rem!important;margin-bottom:-.75rem!important}.my-xl-n4{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-xl-n5{margin-top:-1.25rem!important;margin-bottom:-1.25rem!important}.my-xl-n6{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-xl-n7{margin-top:-1.75rem!important;margin-bottom:-1.75rem!important}.my-xl-n8{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-xl-n9{margin-top:-2.25rem!important;margin-bottom:-2.25rem!important}.my-xl-n10{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.my-xl-n11{margin-top:-2.75rem!important;margin-bottom:-2.75rem!important}.my-xl-n12{margin-top:-3rem!important;margin-bottom:-3rem!important}.my-xl-n13{margin-top:-3.25rem!important;margin-bottom:-3.25rem!important}.my-xl-n14{margin-top:-3.5rem!important;margin-bottom:-3.5rem!important}.my-xl-n15{margin-top:-3.75rem!important;margin-bottom:-3.75rem!important}.my-xl-n16{margin-top:-4rem!important;margin-bottom:-4rem!important}.my-xl-n17{margin-top:-4.25rem!important;margin-bottom:-4.25rem!important}.my-xl-n18{margin-top:-4.5rem!important;margin-bottom:-4.5rem!important}.my-xl-n19{margin-top:-4.75rem!important;margin-bottom:-4.75rem!important}.my-xl-n20{margin-top:-5rem!important;margin-bottom:-5rem!important}.mt-xl-n1{margin-top:-.25rem!important}.mt-xl-n2{margin-top:-.5rem!important}.mt-xl-n3{margin-top:-.75rem!important}.mt-xl-n4{margin-top:-1rem!important}.mt-xl-n5{margin-top:-1.25rem!important}.mt-xl-n6{margin-top:-1.5rem!important}.mt-xl-n7{margin-top:-1.75rem!important}.mt-xl-n8{margin-top:-2rem!important}.mt-xl-n9{margin-top:-2.25rem!important}.mt-xl-n10{margin-top:-2.5rem!important}.mt-xl-n11{margin-top:-2.75rem!important}.mt-xl-n12{margin-top:-3rem!important}.mt-xl-n13{margin-top:-3.25rem!important}.mt-xl-n14{margin-top:-3.5rem!important}.mt-xl-n15{margin-top:-3.75rem!important}.mt-xl-n16{margin-top:-4rem!important}.mt-xl-n17{margin-top:-4.25rem!important}.mt-xl-n18{margin-top:-4.5rem!important}.mt-xl-n19{margin-top:-4.75rem!important}.mt-xl-n20{margin-top:-5rem!important}.me-xl-n1{margin-right:-.25rem!important}.me-xl-n2{margin-right:-.5rem!important}.me-xl-n3{margin-right:-.75rem!important}.me-xl-n4{margin-right:-1rem!important}.me-xl-n5{margin-right:-1.25rem!important}.me-xl-n6{margin-right:-1.5rem!important}.me-xl-n7{margin-right:-1.75rem!important}.me-xl-n8{margin-right:-2rem!important}.me-xl-n9{margin-right:-2.25rem!important}.me-xl-n10{margin-right:-2.5rem!important}.me-xl-n11{margin-right:-2.75rem!important}.me-xl-n12{margin-right:-3rem!important}.me-xl-n13{margin-right:-3.25rem!important}.me-xl-n14{margin-right:-3.5rem!important}.me-xl-n15{margin-right:-3.75rem!important}.me-xl-n16{margin-right:-4rem!important}.me-xl-n17{margin-right:-4.25rem!important}.me-xl-n18{margin-right:-4.5rem!important}.me-xl-n19{margin-right:-4.75rem!important}.me-xl-n20{margin-right:-5rem!important}.mb-xl-n1{margin-bottom:-.25rem!important}.mb-xl-n2{margin-bottom:-.5rem!important}.mb-xl-n3{margin-bottom:-.75rem!important}.mb-xl-n4{margin-bottom:-1rem!important}.mb-xl-n5{margin-bottom:-1.25rem!important}.mb-xl-n6{margin-bottom:-1.5rem!important}.mb-xl-n7{margin-bottom:-1.75rem!important}.mb-xl-n8{margin-bottom:-2rem!important}.mb-xl-n9{margin-bottom:-2.25rem!important}.mb-xl-n10{margin-bottom:-2.5rem!important}.mb-xl-n11{margin-bottom:-2.75rem!important}.mb-xl-n12{margin-bottom:-3rem!important}.mb-xl-n13{margin-bottom:-3.25rem!important}.mb-xl-n14{margin-bottom:-3.5rem!important}.mb-xl-n15{margin-bottom:-3.75rem!important}.mb-xl-n16{margin-bottom:-4rem!important}.mb-xl-n17{margin-bottom:-4.25rem!important}.mb-xl-n18{margin-bottom:-4.5rem!important}.mb-xl-n19{margin-bottom:-4.75rem!important}.mb-xl-n20{margin-bottom:-5rem!important}.ms-xl-n1{margin-left:-.25rem!important}.ms-xl-n2{margin-left:-.5rem!important}.ms-xl-n3{margin-left:-.75rem!important}.ms-xl-n4{margin-left:-1rem!important}.ms-xl-n5{margin-left:-1.25rem!important}.ms-xl-n6{margin-left:-1.5rem!important}.ms-xl-n7{margin-left:-1.75rem!important}.ms-xl-n8{margin-left:-2rem!important}.ms-xl-n9{margin-left:-2.25rem!important}.ms-xl-n10{margin-left:-2.5rem!important}.ms-xl-n11{margin-left:-2.75rem!important}.ms-xl-n12{margin-left:-3rem!important}.ms-xl-n13{margin-left:-3.25rem!important}.ms-xl-n14{margin-left:-3.5rem!important}.ms-xl-n15{margin-left:-3.75rem!important}.ms-xl-n16{margin-left:-4rem!important}.ms-xl-n17{margin-left:-4.25rem!important}.ms-xl-n18{margin-left:-4.5rem!important}.ms-xl-n19{margin-left:-4.75rem!important}.ms-xl-n20{margin-left:-5rem!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:.75rem!important}.p-xl-4{padding:1rem!important}.p-xl-5{padding:1.25rem!important}.p-xl-6{padding:1.5rem!important}.p-xl-7{padding:1.75rem!important}.p-xl-8{padding:2rem!important}.p-xl-9{padding:2.25rem!important}.p-xl-10{padding:2.5rem!important}.p-xl-11{padding:2.75rem!important}.p-xl-12{padding:3rem!important}.p-xl-13{padding:3.25rem!important}.p-xl-14{padding:3.5rem!important}.p-xl-15{padding:3.75rem!important}.p-xl-16{padding:4rem!important}.p-xl-17{padding:4.25rem!important}.p-xl-18{padding:4.5rem!important}.p-xl-19{padding:4.75rem!important}.p-xl-20{padding:5rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:.75rem!important;padding-left:.75rem!important}.px-xl-4{padding-right:1rem!important;padding-left:1rem!important}.px-xl-5{padding-right:1.25rem!important;padding-left:1.25rem!important}.px-xl-6{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-7{padding-right:1.75rem!important;padding-left:1.75rem!important}.px-xl-8{padding-right:2rem!important;padding-left:2rem!important}.px-xl-9{padding-right:2.25rem!important;padding-left:2.25rem!important}.px-xl-10{padding-right:2.5rem!important;padding-left:2.5rem!important}.px-xl-11{padding-right:2.75rem!important;padding-left:2.75rem!important}.px-xl-12{padding-right:3rem!important;padding-left:3rem!important}.px-xl-13{padding-right:3.25rem!important;padding-left:3.25rem!important}.px-xl-14{padding-right:3.5rem!important;padding-left:3.5rem!important}.px-xl-15{padding-right:3.75rem!important;padding-left:3.75rem!important}.px-xl-16{padding-right:4rem!important;padding-left:4rem!important}.px-xl-17{padding-right:4.25rem!important;padding-left:4.25rem!important}.px-xl-18{padding-right:4.5rem!important;padding-left:4.5rem!important}.px-xl-19{padding-right:4.75rem!important;padding-left:4.75rem!important}.px-xl-20{padding-right:5rem!important;padding-left:5rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:.75rem!important;padding-bottom:.75rem!important}.py-xl-4{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-5{padding-top:1.25rem!important;padding-bottom:1.25rem!important}.py-xl-6{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-7{padding-top:1.75rem!important;padding-bottom:1.75rem!important}.py-xl-8{padding-top:2rem!important;padding-bottom:2rem!important}.py-xl-9{padding-top:2.25rem!important;padding-bottom:2.25rem!important}.py-xl-10{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.py-xl-11{padding-top:2.75rem!important;padding-bottom:2.75rem!important}.py-xl-12{padding-top:3rem!important;padding-bottom:3rem!important}.py-xl-13{padding-top:3.25rem!important;padding-bottom:3.25rem!important}.py-xl-14{padding-top:3.5rem!important;padding-bottom:3.5rem!important}.py-xl-15{padding-top:3.75rem!important;padding-bottom:3.75rem!important}.py-xl-16{padding-top:4rem!important;padding-bottom:4rem!important}.py-xl-17{padding-top:4.25rem!important;padding-bottom:4.25rem!important}.py-xl-18{padding-top:4.5rem!important;padding-bottom:4.5rem!important}.py-xl-19{padding-top:4.75rem!important;padding-bottom:4.75rem!important}.py-xl-20{padding-top:5rem!important;padding-bottom:5rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:.75rem!important}.pt-xl-4{padding-top:1rem!important}.pt-xl-5{padding-top:1.25rem!important}.pt-xl-6{padding-top:1.5rem!important}.pt-xl-7{padding-top:1.75rem!important}.pt-xl-8{padding-top:2rem!important}.pt-xl-9{padding-top:2.25rem!important}.pt-xl-10{padding-top:2.5rem!important}.pt-xl-11{padding-top:2.75rem!important}.pt-xl-12{padding-top:3rem!important}.pt-xl-13{padding-top:3.25rem!important}.pt-xl-14{padding-top:3.5rem!important}.pt-xl-15{padding-top:3.75rem!important}.pt-xl-16{padding-top:4rem!important}.pt-xl-17{padding-top:4.25rem!important}.pt-xl-18{padding-top:4.5rem!important}.pt-xl-19{padding-top:4.75rem!important}.pt-xl-20{padding-top:5rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:.75rem!important}.pe-xl-4{padding-right:1rem!important}.pe-xl-5{padding-right:1.25rem!important}.pe-xl-6{padding-right:1.5rem!important}.pe-xl-7{padding-right:1.75rem!important}.pe-xl-8{padding-right:2rem!important}.pe-xl-9{padding-right:2.25rem!important}.pe-xl-10{padding-right:2.5rem!important}.pe-xl-11{padding-right:2.75rem!important}.pe-xl-12{padding-right:3rem!important}.pe-xl-13{padding-right:3.25rem!important}.pe-xl-14{padding-right:3.5rem!important}.pe-xl-15{padding-right:3.75rem!important}.pe-xl-16{padding-right:4rem!important}.pe-xl-17{padding-right:4.25rem!important}.pe-xl-18{padding-right:4.5rem!important}.pe-xl-19{padding-right:4.75rem!important}.pe-xl-20{padding-right:5rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:.75rem!important}.pb-xl-4{padding-bottom:1rem!important}.pb-xl-5{padding-bottom:1.25rem!important}.pb-xl-6{padding-bottom:1.5rem!important}.pb-xl-7{padding-bottom:1.75rem!important}.pb-xl-8{padding-bottom:2rem!important}.pb-xl-9{padding-bottom:2.25rem!important}.pb-xl-10{padding-bottom:2.5rem!important}.pb-xl-11{padding-bottom:2.75rem!important}.pb-xl-12{padding-bottom:3rem!important}.pb-xl-13{padding-bottom:3.25rem!important}.pb-xl-14{padding-bottom:3.5rem!important}.pb-xl-15{padding-bottom:3.75rem!important}.pb-xl-16{padding-bottom:4rem!important}.pb-xl-17{padding-bottom:4.25rem!important}.pb-xl-18{padding-bottom:4.5rem!important}.pb-xl-19{padding-bottom:4.75rem!important}.pb-xl-20{padding-bottom:5rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:.75rem!important}.ps-xl-4{padding-left:1rem!important}.ps-xl-5{padding-left:1.25rem!important}.ps-xl-6{padding-left:1.5rem!important}.ps-xl-7{padding-left:1.75rem!important}.ps-xl-8{padding-left:2rem!important}.ps-xl-9{padding-left:2.25rem!important}.ps-xl-10{padding-left:2.5rem!important}.ps-xl-11{padding-left:2.75rem!important}.ps-xl-12{padding-left:3rem!important}.ps-xl-13{padding-left:3.25rem!important}.ps-xl-14{padding-left:3.5rem!important}.ps-xl-15{padding-left:3.75rem!important}.ps-xl-16{padding-left:4rem!important}.ps-xl-17{padding-left:4.25rem!important}.ps-xl-18{padding-left:4.5rem!important}.ps-xl-19{padding-left:4.75rem!important}.ps-xl-20{padding-left:5rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:.75rem!important}.gap-xl-4{gap:1rem!important}.gap-xl-5{gap:1.25rem!important}.gap-xl-6{gap:1.5rem!important}.gap-xl-7{gap:1.75rem!important}.gap-xl-8{gap:2rem!important}.gap-xl-9{gap:2.25rem!important}.gap-xl-10{gap:2.5rem!important}.gap-xl-11{gap:2.75rem!important}.gap-xl-12{gap:3rem!important}.gap-xl-13{gap:3.25rem!important}.gap-xl-14{gap:3.5rem!important}.gap-xl-15{gap:3.75rem!important}.gap-xl-16{gap:4rem!important}.gap-xl-17{gap:4.25rem!important}.gap-xl-18{gap:4.5rem!important}.gap-xl-19{gap:4.75rem!important}.gap-xl-20{gap:5rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:.75rem!important}.row-gap-xl-4{row-gap:1rem!important}.row-gap-xl-5{row-gap:1.25rem!important}.row-gap-xl-6{row-gap:1.5rem!important}.row-gap-xl-7{row-gap:1.75rem!important}.row-gap-xl-8{row-gap:2rem!important}.row-gap-xl-9{row-gap:2.25rem!important}.row-gap-xl-10{row-gap:2.5rem!important}.row-gap-xl-11{row-gap:2.75rem!important}.row-gap-xl-12{row-gap:3rem!important}.row-gap-xl-13{row-gap:3.25rem!important}.row-gap-xl-14{row-gap:3.5rem!important}.row-gap-xl-15{row-gap:3.75rem!important}.row-gap-xl-16{row-gap:4rem!important}.row-gap-xl-17{row-gap:4.25rem!important}.row-gap-xl-18{row-gap:4.5rem!important}.row-gap-xl-19{row-gap:4.75rem!important}.row-gap-xl-20{row-gap:5rem!important}.column-gap-xl-0{column-gap:0!important}.column-gap-xl-1{column-gap:.25rem!important}.column-gap-xl-2{column-gap:.5rem!important}.column-gap-xl-3{column-gap:.75rem!important}.column-gap-xl-4{column-gap:1rem!important}.column-gap-xl-5{column-gap:1.25rem!important}.column-gap-xl-6{column-gap:1.5rem!important}.column-gap-xl-7{column-gap:1.75rem!important}.column-gap-xl-8{column-gap:2rem!important}.column-gap-xl-9{column-gap:2.25rem!important}.column-gap-xl-10{column-gap:2.5rem!important}.column-gap-xl-11{column-gap:2.75rem!important}.column-gap-xl-12{column-gap:3rem!important}.column-gap-xl-13{column-gap:3.25rem!important}.column-gap-xl-14{column-gap:3.5rem!important}.column-gap-xl-15{column-gap:3.75rem!important}.column-gap-xl-16{column-gap:4rem!important}.column-gap-xl-17{column-gap:4.25rem!important}.column-gap-xl-18{column-gap:4.5rem!important}.column-gap-xl-19{column-gap:4.75rem!important}.column-gap-xl-20{column-gap:5rem!important}.fs-xl-1{font-size:calc(1.3rem + .6vw)!important}.fs-xl-2{font-size:calc(1.275rem + .3vw)!important}.fs-xl-3{font-size:calc(1.26rem + .12vw)!important}.fs-xl-4{font-size:1.25rem!important}.fs-xl-5{font-size:1.15rem!important}.fs-xl-6{font-size:1.075rem!important}.fs-xl-7{font-size:.95rem!important}.fs-xl-8{font-size:.85rem!important}.fs-xl-9{font-size:.75rem!important}.fs-xl-10{font-size:.5rem!important}.fs-xl-sm{font-size:.95rem!important}.fs-xl-base{font-size:1rem!important}.fs-xl-lg{font-size:1.075rem!important}.fs-xl-xl{font-size:1.21rem!important}.fs-xl-fluid{font-size:100%!important}.fs-xl-2x{font-size:calc(1.325rem + .9vw)!important}.fs-xl-2qx{font-size:calc(1.35rem + 1.2vw)!important}.fs-xl-2hx{font-size:calc(1.375rem + 1.5vw)!important}.fs-xl-2tx{font-size:calc(1.4rem + 1.8vw)!important}.fs-xl-3x{font-size:calc(1.425rem + 2.1vw)!important}.fs-xl-3qx{font-size:calc(1.45rem + 2.4vw)!important}.fs-xl-3hx{font-size:calc(1.475rem + 2.7vw)!important}.fs-xl-3tx{font-size:calc(1.5rem + 3vw)!important}.fs-xl-4x{font-size:calc(1.525rem + 3.3vw)!important}.fs-xl-4qx{font-size:calc(1.55rem + 3.6vw)!important}.fs-xl-4hx{font-size:calc(1.575rem + 3.9vw)!important}.fs-xl-4tx{font-size:calc(1.6rem + 4.2vw)!important}.fs-xl-5x{font-size:calc(1.625rem + 4.5vw)!important}.fs-xl-5qx{font-size:calc(1.65rem + 4.8vw)!important}.fs-xl-5hx{font-size:calc(1.675rem + 5.1vw)!important}.fs-xl-5tx{font-size:calc(1.7rem + 5.4vw)!important}.fs-xl-6x{font-size:calc(1.725rem + 5.7vw)!important}.fs-xl-6qx{font-size:calc(1.75rem + 6vw)!important}.fs-xl-6hx{font-size:calc(1.775rem + 6.3vw)!important}.fs-xl-6tx{font-size:calc(1.8rem + 6.6vw)!important}.fs-xl-7x{font-size:calc(1.825rem + 6.9vw)!important}.fs-xl-7qx{font-size:calc(1.85rem + 7.2vw)!important}.fs-xl-7hx{font-size:calc(1.875rem + 7.5vw)!important}.fs-xl-7tx{font-size:calc(1.9rem + 7.8vw)!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}.min-w-xl-unset{min-width:unset!important}.min-w-xl-25{min-width:25%!important}.min-w-xl-50{min-width:50%!important}.min-w-xl-75{min-width:75%!important}.min-w-xl-100{min-width:100%!important}.min-w-xl-auto{min-width:auto!important}.min-w-xl-1px{min-width:1px!important}.min-w-xl-2px{min-width:2px!important}.min-w-xl-3px{min-width:3px!important}.min-w-xl-4px{min-width:4px!important}.min-w-xl-5px{min-width:5px!important}.min-w-xl-6px{min-width:6px!important}.min-w-xl-7px{min-width:7px!important}.min-w-xl-8px{min-width:8px!important}.min-w-xl-9px{min-width:9px!important}.min-w-xl-10px{min-width:10px!important}.min-w-xl-15px{min-width:15px!important}.min-w-xl-20px{min-width:20px!important}.min-w-xl-25px{min-width:25px!important}.min-w-xl-30px{min-width:30px!important}.min-w-xl-35px{min-width:35px!important}.min-w-xl-40px{min-width:40px!important}.min-w-xl-45px{min-width:45px!important}.min-w-xl-50px{min-width:50px!important}.min-w-xl-55px{min-width:55px!important}.min-w-xl-60px{min-width:60px!important}.min-w-xl-65px{min-width:65px!important}.min-w-xl-70px{min-width:70px!important}.min-w-xl-75px{min-width:75px!important}.min-w-xl-80px{min-width:80px!important}.min-w-xl-85px{min-width:85px!important}.min-w-xl-90px{min-width:90px!important}.min-w-xl-95px{min-width:95px!important}.min-w-xl-100px{min-width:100px!important}.min-w-xl-125px{min-width:125px!important}.min-w-xl-150px{min-width:150px!important}.min-w-xl-175px{min-width:175px!important}.min-w-xl-200px{min-width:200px!important}.min-w-xl-225px{min-width:225px!important}.min-w-xl-250px{min-width:250px!important}.min-w-xl-275px{min-width:275px!important}.min-w-xl-300px{min-width:300px!important}.min-w-xl-325px{min-width:325px!important}.min-w-xl-350px{min-width:350px!important}.min-w-xl-375px{min-width:375px!important}.min-w-xl-400px{min-width:400px!important}.min-w-xl-425px{min-width:425px!important}.min-w-xl-450px{min-width:450px!important}.min-w-xl-475px{min-width:475px!important}.min-w-xl-500px{min-width:500px!important}.min-w-xl-550px{min-width:550px!important}.min-w-xl-600px{min-width:600px!important}.min-w-xl-650px{min-width:650px!important}.min-w-xl-700px{min-width:700px!important}.min-w-xl-750px{min-width:750px!important}.min-w-xl-800px{min-width:800px!important}.min-w-xl-850px{min-width:850px!important}.min-w-xl-900px{min-width:900px!important}.min-w-xl-950px{min-width:950px!important}.min-w-xl-1000px{min-width:1000px!important}.min-h-xl-unset{min-height:unset!important}.min-h-xl-25{min-height:25%!important}.min-h-xl-50{min-height:50%!important}.min-h-xl-75{min-height:75%!important}.min-h-xl-100{min-height:100%!important}.min-h-xl-auto{min-height:auto!important}.min-h-xl-1px{min-height:1px!important}.min-h-xl-2px{min-height:2px!important}.min-h-xl-3px{min-height:3px!important}.min-h-xl-4px{min-height:4px!important}.min-h-xl-5px{min-height:5px!important}.min-h-xl-6px{min-height:6px!important}.min-h-xl-7px{min-height:7px!important}.min-h-xl-8px{min-height:8px!important}.min-h-xl-9px{min-height:9px!important}.min-h-xl-10px{min-height:10px!important}.min-h-xl-15px{min-height:15px!important}.min-h-xl-20px{min-height:20px!important}.min-h-xl-25px{min-height:25px!important}.min-h-xl-30px{min-height:30px!important}.min-h-xl-35px{min-height:35px!important}.min-h-xl-40px{min-height:40px!important}.min-h-xl-45px{min-height:45px!important}.min-h-xl-50px{min-height:50px!important}.min-h-xl-55px{min-height:55px!important}.min-h-xl-60px{min-height:60px!important}.min-h-xl-65px{min-height:65px!important}.min-h-xl-70px{min-height:70px!important}.min-h-xl-75px{min-height:75px!important}.min-h-xl-80px{min-height:80px!important}.min-h-xl-85px{min-height:85px!important}.min-h-xl-90px{min-height:90px!important}.min-h-xl-95px{min-height:95px!important}.min-h-xl-100px{min-height:100px!important}.min-h-xl-125px{min-height:125px!important}.min-h-xl-150px{min-height:150px!important}.min-h-xl-175px{min-height:175px!important}.min-h-xl-200px{min-height:200px!important}.min-h-xl-225px{min-height:225px!important}.min-h-xl-250px{min-height:250px!important}.min-h-xl-275px{min-height:275px!important}.min-h-xl-300px{min-height:300px!important}.min-h-xl-325px{min-height:325px!important}.min-h-xl-350px{min-height:350px!important}.min-h-xl-375px{min-height:375px!important}.min-h-xl-400px{min-height:400px!important}.min-h-xl-425px{min-height:425px!important}.min-h-xl-450px{min-height:450px!important}.min-h-xl-475px{min-height:475px!important}.min-h-xl-500px{min-height:500px!important}.min-h-xl-550px{min-height:550px!important}.min-h-xl-600px{min-height:600px!important}.min-h-xl-650px{min-height:650px!important}.min-h-xl-700px{min-height:700px!important}.min-h-xl-750px{min-height:750px!important}.min-h-xl-800px{min-height:800px!important}.min-h-xl-850px{min-height:850px!important}.min-h-xl-900px{min-height:900px!important}.min-h-xl-950px{min-height:950px!important}.min-h-xl-1000px{min-height:1000px!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{object-fit:contain!important}.object-fit-xxl-cover{object-fit:cover!important}.object-fit-xxl-fill{object-fit:fill!important}.object-fit-xxl-scale{object-fit:scale-down!important}.object-fit-xxl-none{object-fit:none!important}.overflow-xxl-auto{overflow:auto!important}.overflow-xxl-hidden{overflow:hidden!important}.overflow-xxl-visible{overflow:visible!important}.overflow-xxl-scroll{overflow:scroll!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.position-xxl-static{position:static!important}.position-xxl-relative{position:relative!important}.position-xxl-absolute{position:absolute!important}.position-xxl-fixed{position:fixed!important}.position-xxl-sticky{position:sticky!important}.w-xxl-unset{width:unset!important}.w-xxl-25{width:25%!important}.w-xxl-50{width:50%!important}.w-xxl-75{width:75%!important}.w-xxl-100{width:100%!important}.w-xxl-auto{width:auto!important}.w-xxl-1px{width:1px!important}.w-xxl-2px{width:2px!important}.w-xxl-3px{width:3px!important}.w-xxl-4px{width:4px!important}.w-xxl-5px{width:5px!important}.w-xxl-6px{width:6px!important}.w-xxl-7px{width:7px!important}.w-xxl-8px{width:8px!important}.w-xxl-9px{width:9px!important}.w-xxl-10px{width:10px!important}.w-xxl-15px{width:15px!important}.w-xxl-20px{width:20px!important}.w-xxl-25px{width:25px!important}.w-xxl-30px{width:30px!important}.w-xxl-35px{width:35px!important}.w-xxl-40px{width:40px!important}.w-xxl-45px{width:45px!important}.w-xxl-50px{width:50px!important}.w-xxl-55px{width:55px!important}.w-xxl-60px{width:60px!important}.w-xxl-65px{width:65px!important}.w-xxl-70px{width:70px!important}.w-xxl-75px{width:75px!important}.w-xxl-80px{width:80px!important}.w-xxl-85px{width:85px!important}.w-xxl-90px{width:90px!important}.w-xxl-95px{width:95px!important}.w-xxl-100px{width:100px!important}.w-xxl-125px{width:125px!important}.w-xxl-150px{width:150px!important}.w-xxl-175px{width:175px!important}.w-xxl-200px{width:200px!important}.w-xxl-225px{width:225px!important}.w-xxl-250px{width:250px!important}.w-xxl-275px{width:275px!important}.w-xxl-300px{width:300px!important}.w-xxl-325px{width:325px!important}.w-xxl-350px{width:350px!important}.w-xxl-375px{width:375px!important}.w-xxl-400px{width:400px!important}.w-xxl-425px{width:425px!important}.w-xxl-450px{width:450px!important}.w-xxl-475px{width:475px!important}.w-xxl-500px{width:500px!important}.w-xxl-550px{width:550px!important}.w-xxl-600px{width:600px!important}.w-xxl-650px{width:650px!important}.w-xxl-700px{width:700px!important}.w-xxl-750px{width:750px!important}.w-xxl-800px{width:800px!important}.w-xxl-850px{width:850px!important}.w-xxl-900px{width:900px!important}.w-xxl-950px{width:950px!important}.w-xxl-1000px{width:1000px!important}.mw-xxl-unset{max-width:unset!important}.mw-xxl-25{max-width:25%!important}.mw-xxl-50{max-width:50%!important}.mw-xxl-75{max-width:75%!important}.mw-xxl-100{max-width:100%!important}.mw-xxl-auto{max-width:auto!important}.mw-xxl-1px{max-width:1px!important}.mw-xxl-2px{max-width:2px!important}.mw-xxl-3px{max-width:3px!important}.mw-xxl-4px{max-width:4px!important}.mw-xxl-5px{max-width:5px!important}.mw-xxl-6px{max-width:6px!important}.mw-xxl-7px{max-width:7px!important}.mw-xxl-8px{max-width:8px!important}.mw-xxl-9px{max-width:9px!important}.mw-xxl-10px{max-width:10px!important}.mw-xxl-15px{max-width:15px!important}.mw-xxl-20px{max-width:20px!important}.mw-xxl-25px{max-width:25px!important}.mw-xxl-30px{max-width:30px!important}.mw-xxl-35px{max-width:35px!important}.mw-xxl-40px{max-width:40px!important}.mw-xxl-45px{max-width:45px!important}.mw-xxl-50px{max-width:50px!important}.mw-xxl-55px{max-width:55px!important}.mw-xxl-60px{max-width:60px!important}.mw-xxl-65px{max-width:65px!important}.mw-xxl-70px{max-width:70px!important}.mw-xxl-75px{max-width:75px!important}.mw-xxl-80px{max-width:80px!important}.mw-xxl-85px{max-width:85px!important}.mw-xxl-90px{max-width:90px!important}.mw-xxl-95px{max-width:95px!important}.mw-xxl-100px{max-width:100px!important}.mw-xxl-125px{max-width:125px!important}.mw-xxl-150px{max-width:150px!important}.mw-xxl-175px{max-width:175px!important}.mw-xxl-200px{max-width:200px!important}.mw-xxl-225px{max-width:225px!important}.mw-xxl-250px{max-width:250px!important}.mw-xxl-275px{max-width:275px!important}.mw-xxl-300px{max-width:300px!important}.mw-xxl-325px{max-width:325px!important}.mw-xxl-350px{max-width:350px!important}.mw-xxl-375px{max-width:375px!important}.mw-xxl-400px{max-width:400px!important}.mw-xxl-425px{max-width:425px!important}.mw-xxl-450px{max-width:450px!important}.mw-xxl-475px{max-width:475px!important}.mw-xxl-500px{max-width:500px!important}.mw-xxl-550px{max-width:550px!important}.mw-xxl-600px{max-width:600px!important}.mw-xxl-650px{max-width:650px!important}.mw-xxl-700px{max-width:700px!important}.mw-xxl-750px{max-width:750px!important}.mw-xxl-800px{max-width:800px!important}.mw-xxl-850px{max-width:850px!important}.mw-xxl-900px{max-width:900px!important}.mw-xxl-950px{max-width:950px!important}.mw-xxl-1000px{max-width:1000px!important}.h-xxl-unset{height:unset!important}.h-xxl-25{height:25%!important}.h-xxl-50{height:50%!important}.h-xxl-75{height:75%!important}.h-xxl-100{height:100%!important}.h-xxl-auto{height:auto!important}.h-xxl-1px{height:1px!important}.h-xxl-2px{height:2px!important}.h-xxl-3px{height:3px!important}.h-xxl-4px{height:4px!important}.h-xxl-5px{height:5px!important}.h-xxl-6px{height:6px!important}.h-xxl-7px{height:7px!important}.h-xxl-8px{height:8px!important}.h-xxl-9px{height:9px!important}.h-xxl-10px{height:10px!important}.h-xxl-15px{height:15px!important}.h-xxl-20px{height:20px!important}.h-xxl-25px{height:25px!important}.h-xxl-30px{height:30px!important}.h-xxl-35px{height:35px!important}.h-xxl-40px{height:40px!important}.h-xxl-45px{height:45px!important}.h-xxl-50px{height:50px!important}.h-xxl-55px{height:55px!important}.h-xxl-60px{height:60px!important}.h-xxl-65px{height:65px!important}.h-xxl-70px{height:70px!important}.h-xxl-75px{height:75px!important}.h-xxl-80px{height:80px!important}.h-xxl-85px{height:85px!important}.h-xxl-90px{height:90px!important}.h-xxl-95px{height:95px!important}.h-xxl-100px{height:100px!important}.h-xxl-125px{height:125px!important}.h-xxl-150px{height:150px!important}.h-xxl-175px{height:175px!important}.h-xxl-200px{height:200px!important}.h-xxl-225px{height:225px!important}.h-xxl-250px{height:250px!important}.h-xxl-275px{height:275px!important}.h-xxl-300px{height:300px!important}.h-xxl-325px{height:325px!important}.h-xxl-350px{height:350px!important}.h-xxl-375px{height:375px!important}.h-xxl-400px{height:400px!important}.h-xxl-425px{height:425px!important}.h-xxl-450px{height:450px!important}.h-xxl-475px{height:475px!important}.h-xxl-500px{height:500px!important}.h-xxl-550px{height:550px!important}.h-xxl-600px{height:600px!important}.h-xxl-650px{height:650px!important}.h-xxl-700px{height:700px!important}.h-xxl-750px{height:750px!important}.h-xxl-800px{height:800px!important}.h-xxl-850px{height:850px!important}.h-xxl-900px{height:900px!important}.h-xxl-950px{height:950px!important}.h-xxl-1000px{height:1000px!important}.mh-xxl-unset{max-height:unset!important}.mh-xxl-25{max-height:25%!important}.mh-xxl-50{max-height:50%!important}.mh-xxl-75{max-height:75%!important}.mh-xxl-100{max-height:100%!important}.mh-xxl-auto{max-height:auto!important}.mh-xxl-1px{max-height:1px!important}.mh-xxl-2px{max-height:2px!important}.mh-xxl-3px{max-height:3px!important}.mh-xxl-4px{max-height:4px!important}.mh-xxl-5px{max-height:5px!important}.mh-xxl-6px{max-height:6px!important}.mh-xxl-7px{max-height:7px!important}.mh-xxl-8px{max-height:8px!important}.mh-xxl-9px{max-height:9px!important}.mh-xxl-10px{max-height:10px!important}.mh-xxl-15px{max-height:15px!important}.mh-xxl-20px{max-height:20px!important}.mh-xxl-25px{max-height:25px!important}.mh-xxl-30px{max-height:30px!important}.mh-xxl-35px{max-height:35px!important}.mh-xxl-40px{max-height:40px!important}.mh-xxl-45px{max-height:45px!important}.mh-xxl-50px{max-height:50px!important}.mh-xxl-55px{max-height:55px!important}.mh-xxl-60px{max-height:60px!important}.mh-xxl-65px{max-height:65px!important}.mh-xxl-70px{max-height:70px!important}.mh-xxl-75px{max-height:75px!important}.mh-xxl-80px{max-height:80px!important}.mh-xxl-85px{max-height:85px!important}.mh-xxl-90px{max-height:90px!important}.mh-xxl-95px{max-height:95px!important}.mh-xxl-100px{max-height:100px!important}.mh-xxl-125px{max-height:125px!important}.mh-xxl-150px{max-height:150px!important}.mh-xxl-175px{max-height:175px!important}.mh-xxl-200px{max-height:200px!important}.mh-xxl-225px{max-height:225px!important}.mh-xxl-250px{max-height:250px!important}.mh-xxl-275px{max-height:275px!important}.mh-xxl-300px{max-height:300px!important}.mh-xxl-325px{max-height:325px!important}.mh-xxl-350px{max-height:350px!important}.mh-xxl-375px{max-height:375px!important}.mh-xxl-400px{max-height:400px!important}.mh-xxl-425px{max-height:425px!important}.mh-xxl-450px{max-height:450px!important}.mh-xxl-475px{max-height:475px!important}.mh-xxl-500px{max-height:500px!important}.mh-xxl-550px{max-height:550px!important}.mh-xxl-600px{max-height:600px!important}.mh-xxl-650px{max-height:650px!important}.mh-xxl-700px{max-height:700px!important}.mh-xxl-750px{max-height:750px!important}.mh-xxl-800px{max-height:800px!important}.mh-xxl-850px{max-height:850px!important}.mh-xxl-900px{max-height:900px!important}.mh-xxl-950px{max-height:950px!important}.mh-xxl-1000px{max-height:1000px!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:.75rem!important}.m-xxl-4{margin:1rem!important}.m-xxl-5{margin:1.25rem!important}.m-xxl-6{margin:1.5rem!important}.m-xxl-7{margin:1.75rem!important}.m-xxl-8{margin:2rem!important}.m-xxl-9{margin:2.25rem!important}.m-xxl-10{margin:2.5rem!important}.m-xxl-11{margin:2.75rem!important}.m-xxl-12{margin:3rem!important}.m-xxl-13{margin:3.25rem!important}.m-xxl-14{margin:3.5rem!important}.m-xxl-15{margin:3.75rem!important}.m-xxl-16{margin:4rem!important}.m-xxl-17{margin:4.25rem!important}.m-xxl-18{margin:4.5rem!important}.m-xxl-19{margin:4.75rem!important}.m-xxl-20{margin:5rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:.75rem!important;margin-left:.75rem!important}.mx-xxl-4{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-5{margin-right:1.25rem!important;margin-left:1.25rem!important}.mx-xxl-6{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-7{margin-right:1.75rem!important;margin-left:1.75rem!important}.mx-xxl-8{margin-right:2rem!important;margin-left:2rem!important}.mx-xxl-9{margin-right:2.25rem!important;margin-left:2.25rem!important}.mx-xxl-10{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-xxl-11{margin-right:2.75rem!important;margin-left:2.75rem!important}.mx-xxl-12{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-13{margin-right:3.25rem!important;margin-left:3.25rem!important}.mx-xxl-14{margin-right:3.5rem!important;margin-left:3.5rem!important}.mx-xxl-15{margin-right:3.75rem!important;margin-left:3.75rem!important}.mx-xxl-16{margin-right:4rem!important;margin-left:4rem!important}.mx-xxl-17{margin-right:4.25rem!important;margin-left:4.25rem!important}.mx-xxl-18{margin-right:4.5rem!important;margin-left:4.5rem!important}.mx-xxl-19{margin-right:4.75rem!important;margin-left:4.75rem!important}.mx-xxl-20{margin-right:5rem!important;margin-left:5rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:.75rem!important;margin-bottom:.75rem!important}.my-xxl-4{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-5{margin-top:1.25rem!important;margin-bottom:1.25rem!important}.my-xxl-6{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-7{margin-top:1.75rem!important;margin-bottom:1.75rem!important}.my-xxl-8{margin-top:2rem!important;margin-bottom:2rem!important}.my-xxl-9{margin-top:2.25rem!important;margin-bottom:2.25rem!important}.my-xxl-10{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-xxl-11{margin-top:2.75rem!important;margin-bottom:2.75rem!important}.my-xxl-12{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-13{margin-top:3.25rem!important;margin-bottom:3.25rem!important}.my-xxl-14{margin-top:3.5rem!important;margin-bottom:3.5rem!important}.my-xxl-15{margin-top:3.75rem!important;margin-bottom:3.75rem!important}.my-xxl-16{margin-top:4rem!important;margin-bottom:4rem!important}.my-xxl-17{margin-top:4.25rem!important;margin-bottom:4.25rem!important}.my-xxl-18{margin-top:4.5rem!important;margin-bottom:4.5rem!important}.my-xxl-19{margin-top:4.75rem!important;margin-bottom:4.75rem!important}.my-xxl-20{margin-top:5rem!important;margin-bottom:5rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:.75rem!important}.mt-xxl-4{margin-top:1rem!important}.mt-xxl-5{margin-top:1.25rem!important}.mt-xxl-6{margin-top:1.5rem!important}.mt-xxl-7{margin-top:1.75rem!important}.mt-xxl-8{margin-top:2rem!important}.mt-xxl-9{margin-top:2.25rem!important}.mt-xxl-10{margin-top:2.5rem!important}.mt-xxl-11{margin-top:2.75rem!important}.mt-xxl-12{margin-top:3rem!important}.mt-xxl-13{margin-top:3.25rem!important}.mt-xxl-14{margin-top:3.5rem!important}.mt-xxl-15{margin-top:3.75rem!important}.mt-xxl-16{margin-top:4rem!important}.mt-xxl-17{margin-top:4.25rem!important}.mt-xxl-18{margin-top:4.5rem!important}.mt-xxl-19{margin-top:4.75rem!important}.mt-xxl-20{margin-top:5rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:.75rem!important}.me-xxl-4{margin-right:1rem!important}.me-xxl-5{margin-right:1.25rem!important}.me-xxl-6{margin-right:1.5rem!important}.me-xxl-7{margin-right:1.75rem!important}.me-xxl-8{margin-right:2rem!important}.me-xxl-9{margin-right:2.25rem!important}.me-xxl-10{margin-right:2.5rem!important}.me-xxl-11{margin-right:2.75rem!important}.me-xxl-12{margin-right:3rem!important}.me-xxl-13{margin-right:3.25rem!important}.me-xxl-14{margin-right:3.5rem!important}.me-xxl-15{margin-right:3.75rem!important}.me-xxl-16{margin-right:4rem!important}.me-xxl-17{margin-right:4.25rem!important}.me-xxl-18{margin-right:4.5rem!important}.me-xxl-19{margin-right:4.75rem!important}.me-xxl-20{margin-right:5rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:.75rem!important}.mb-xxl-4{margin-bottom:1rem!important}.mb-xxl-5{margin-bottom:1.25rem!important}.mb-xxl-6{margin-bottom:1.5rem!important}.mb-xxl-7{margin-bottom:1.75rem!important}.mb-xxl-8{margin-bottom:2rem!important}.mb-xxl-9{margin-bottom:2.25rem!important}.mb-xxl-10{margin-bottom:2.5rem!important}.mb-xxl-11{margin-bottom:2.75rem!important}.mb-xxl-12{margin-bottom:3rem!important}.mb-xxl-13{margin-bottom:3.25rem!important}.mb-xxl-14{margin-bottom:3.5rem!important}.mb-xxl-15{margin-bottom:3.75rem!important}.mb-xxl-16{margin-bottom:4rem!important}.mb-xxl-17{margin-bottom:4.25rem!important}.mb-xxl-18{margin-bottom:4.5rem!important}.mb-xxl-19{margin-bottom:4.75rem!important}.mb-xxl-20{margin-bottom:5rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:.75rem!important}.ms-xxl-4{margin-left:1rem!important}.ms-xxl-5{margin-left:1.25rem!important}.ms-xxl-6{margin-left:1.5rem!important}.ms-xxl-7{margin-left:1.75rem!important}.ms-xxl-8{margin-left:2rem!important}.ms-xxl-9{margin-left:2.25rem!important}.ms-xxl-10{margin-left:2.5rem!important}.ms-xxl-11{margin-left:2.75rem!important}.ms-xxl-12{margin-left:3rem!important}.ms-xxl-13{margin-left:3.25rem!important}.ms-xxl-14{margin-left:3.5rem!important}.ms-xxl-15{margin-left:3.75rem!important}.ms-xxl-16{margin-left:4rem!important}.ms-xxl-17{margin-left:4.25rem!important}.ms-xxl-18{margin-left:4.5rem!important}.ms-xxl-19{margin-left:4.75rem!important}.ms-xxl-20{margin-left:5rem!important}.ms-xxl-auto{margin-left:auto!important}.m-xxl-n1{margin:-.25rem!important}.m-xxl-n2{margin:-.5rem!important}.m-xxl-n3{margin:-.75rem!important}.m-xxl-n4{margin:-1rem!important}.m-xxl-n5{margin:-1.25rem!important}.m-xxl-n6{margin:-1.5rem!important}.m-xxl-n7{margin:-1.75rem!important}.m-xxl-n8{margin:-2rem!important}.m-xxl-n9{margin:-2.25rem!important}.m-xxl-n10{margin:-2.5rem!important}.m-xxl-n11{margin:-2.75rem!important}.m-xxl-n12{margin:-3rem!important}.m-xxl-n13{margin:-3.25rem!important}.m-xxl-n14{margin:-3.5rem!important}.m-xxl-n15{margin:-3.75rem!important}.m-xxl-n16{margin:-4rem!important}.m-xxl-n17{margin:-4.25rem!important}.m-xxl-n18{margin:-4.5rem!important}.m-xxl-n19{margin:-4.75rem!important}.m-xxl-n20{margin:-5rem!important}.mx-xxl-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-xxl-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-xxl-n3{margin-right:-.75rem!important;margin-left:-.75rem!important}.mx-xxl-n4{margin-right:-1rem!important;margin-left:-1rem!important}.mx-xxl-n5{margin-right:-1.25rem!important;margin-left:-1.25rem!important}.mx-xxl-n6{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-xxl-n7{margin-right:-1.75rem!important;margin-left:-1.75rem!important}.mx-xxl-n8{margin-right:-2rem!important;margin-left:-2rem!important}.mx-xxl-n9{margin-right:-2.25rem!important;margin-left:-2.25rem!important}.mx-xxl-n10{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.mx-xxl-n11{margin-right:-2.75rem!important;margin-left:-2.75rem!important}.mx-xxl-n12{margin-right:-3rem!important;margin-left:-3rem!important}.mx-xxl-n13{margin-right:-3.25rem!important;margin-left:-3.25rem!important}.mx-xxl-n14{margin-right:-3.5rem!important;margin-left:-3.5rem!important}.mx-xxl-n15{margin-right:-3.75rem!important;margin-left:-3.75rem!important}.mx-xxl-n16{margin-right:-4rem!important;margin-left:-4rem!important}.mx-xxl-n17{margin-right:-4.25rem!important;margin-left:-4.25rem!important}.mx-xxl-n18{margin-right:-4.5rem!important;margin-left:-4.5rem!important}.mx-xxl-n19{margin-right:-4.75rem!important;margin-left:-4.75rem!important}.mx-xxl-n20{margin-right:-5rem!important;margin-left:-5rem!important}.my-xxl-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-xxl-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-xxl-n3{margin-top:-.75rem!important;margin-bottom:-.75rem!important}.my-xxl-n4{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-xxl-n5{margin-top:-1.25rem!important;margin-bottom:-1.25rem!important}.my-xxl-n6{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-xxl-n7{margin-top:-1.75rem!important;margin-bottom:-1.75rem!important}.my-xxl-n8{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-xxl-n9{margin-top:-2.25rem!important;margin-bottom:-2.25rem!important}.my-xxl-n10{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.my-xxl-n11{margin-top:-2.75rem!important;margin-bottom:-2.75rem!important}.my-xxl-n12{margin-top:-3rem!important;margin-bottom:-3rem!important}.my-xxl-n13{margin-top:-3.25rem!important;margin-bottom:-3.25rem!important}.my-xxl-n14{margin-top:-3.5rem!important;margin-bottom:-3.5rem!important}.my-xxl-n15{margin-top:-3.75rem!important;margin-bottom:-3.75rem!important}.my-xxl-n16{margin-top:-4rem!important;margin-bottom:-4rem!important}.my-xxl-n17{margin-top:-4.25rem!important;margin-bottom:-4.25rem!important}.my-xxl-n18{margin-top:-4.5rem!important;margin-bottom:-4.5rem!important}.my-xxl-n19{margin-top:-4.75rem!important;margin-bottom:-4.75rem!important}.my-xxl-n20{margin-top:-5rem!important;margin-bottom:-5rem!important}.mt-xxl-n1{margin-top:-.25rem!important}.mt-xxl-n2{margin-top:-.5rem!important}.mt-xxl-n3{margin-top:-.75rem!important}.mt-xxl-n4{margin-top:-1rem!important}.mt-xxl-n5{margin-top:-1.25rem!important}.mt-xxl-n6{margin-top:-1.5rem!important}.mt-xxl-n7{margin-top:-1.75rem!important}.mt-xxl-n8{margin-top:-2rem!important}.mt-xxl-n9{margin-top:-2.25rem!important}.mt-xxl-n10{margin-top:-2.5rem!important}.mt-xxl-n11{margin-top:-2.75rem!important}.mt-xxl-n12{margin-top:-3rem!important}.mt-xxl-n13{margin-top:-3.25rem!important}.mt-xxl-n14{margin-top:-3.5rem!important}.mt-xxl-n15{margin-top:-3.75rem!important}.mt-xxl-n16{margin-top:-4rem!important}.mt-xxl-n17{margin-top:-4.25rem!important}.mt-xxl-n18{margin-top:-4.5rem!important}.mt-xxl-n19{margin-top:-4.75rem!important}.mt-xxl-n20{margin-top:-5rem!important}.me-xxl-n1{margin-right:-.25rem!important}.me-xxl-n2{margin-right:-.5rem!important}.me-xxl-n3{margin-right:-.75rem!important}.me-xxl-n4{margin-right:-1rem!important}.me-xxl-n5{margin-right:-1.25rem!important}.me-xxl-n6{margin-right:-1.5rem!important}.me-xxl-n7{margin-right:-1.75rem!important}.me-xxl-n8{margin-right:-2rem!important}.me-xxl-n9{margin-right:-2.25rem!important}.me-xxl-n10{margin-right:-2.5rem!important}.me-xxl-n11{margin-right:-2.75rem!important}.me-xxl-n12{margin-right:-3rem!important}.me-xxl-n13{margin-right:-3.25rem!important}.me-xxl-n14{margin-right:-3.5rem!important}.me-xxl-n15{margin-right:-3.75rem!important}.me-xxl-n16{margin-right:-4rem!important}.me-xxl-n17{margin-right:-4.25rem!important}.me-xxl-n18{margin-right:-4.5rem!important}.me-xxl-n19{margin-right:-4.75rem!important}.me-xxl-n20{margin-right:-5rem!important}.mb-xxl-n1{margin-bottom:-.25rem!important}.mb-xxl-n2{margin-bottom:-.5rem!important}.mb-xxl-n3{margin-bottom:-.75rem!important}.mb-xxl-n4{margin-bottom:-1rem!important}.mb-xxl-n5{margin-bottom:-1.25rem!important}.mb-xxl-n6{margin-bottom:-1.5rem!important}.mb-xxl-n7{margin-bottom:-1.75rem!important}.mb-xxl-n8{margin-bottom:-2rem!important}.mb-xxl-n9{margin-bottom:-2.25rem!important}.mb-xxl-n10{margin-bottom:-2.5rem!important}.mb-xxl-n11{margin-bottom:-2.75rem!important}.mb-xxl-n12{margin-bottom:-3rem!important}.mb-xxl-n13{margin-bottom:-3.25rem!important}.mb-xxl-n14{margin-bottom:-3.5rem!important}.mb-xxl-n15{margin-bottom:-3.75rem!important}.mb-xxl-n16{margin-bottom:-4rem!important}.mb-xxl-n17{margin-bottom:-4.25rem!important}.mb-xxl-n18{margin-bottom:-4.5rem!important}.mb-xxl-n19{margin-bottom:-4.75rem!important}.mb-xxl-n20{margin-bottom:-5rem!important}.ms-xxl-n1{margin-left:-.25rem!important}.ms-xxl-n2{margin-left:-.5rem!important}.ms-xxl-n3{margin-left:-.75rem!important}.ms-xxl-n4{margin-left:-1rem!important}.ms-xxl-n5{margin-left:-1.25rem!important}.ms-xxl-n6{margin-left:-1.5rem!important}.ms-xxl-n7{margin-left:-1.75rem!important}.ms-xxl-n8{margin-left:-2rem!important}.ms-xxl-n9{margin-left:-2.25rem!important}.ms-xxl-n10{margin-left:-2.5rem!important}.ms-xxl-n11{margin-left:-2.75rem!important}.ms-xxl-n12{margin-left:-3rem!important}.ms-xxl-n13{margin-left:-3.25rem!important}.ms-xxl-n14{margin-left:-3.5rem!important}.ms-xxl-n15{margin-left:-3.75rem!important}.ms-xxl-n16{margin-left:-4rem!important}.ms-xxl-n17{margin-left:-4.25rem!important}.ms-xxl-n18{margin-left:-4.5rem!important}.ms-xxl-n19{margin-left:-4.75rem!important}.ms-xxl-n20{margin-left:-5rem!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:.75rem!important}.p-xxl-4{padding:1rem!important}.p-xxl-5{padding:1.25rem!important}.p-xxl-6{padding:1.5rem!important}.p-xxl-7{padding:1.75rem!important}.p-xxl-8{padding:2rem!important}.p-xxl-9{padding:2.25rem!important}.p-xxl-10{padding:2.5rem!important}.p-xxl-11{padding:2.75rem!important}.p-xxl-12{padding:3rem!important}.p-xxl-13{padding:3.25rem!important}.p-xxl-14{padding:3.5rem!important}.p-xxl-15{padding:3.75rem!important}.p-xxl-16{padding:4rem!important}.p-xxl-17{padding:4.25rem!important}.p-xxl-18{padding:4.5rem!important}.p-xxl-19{padding:4.75rem!important}.p-xxl-20{padding:5rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:.75rem!important;padding-left:.75rem!important}.px-xxl-4{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-5{padding-right:1.25rem!important;padding-left:1.25rem!important}.px-xxl-6{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-7{padding-right:1.75rem!important;padding-left:1.75rem!important}.px-xxl-8{padding-right:2rem!important;padding-left:2rem!important}.px-xxl-9{padding-right:2.25rem!important;padding-left:2.25rem!important}.px-xxl-10{padding-right:2.5rem!important;padding-left:2.5rem!important}.px-xxl-11{padding-right:2.75rem!important;padding-left:2.75rem!important}.px-xxl-12{padding-right:3rem!important;padding-left:3rem!important}.px-xxl-13{padding-right:3.25rem!important;padding-left:3.25rem!important}.px-xxl-14{padding-right:3.5rem!important;padding-left:3.5rem!important}.px-xxl-15{padding-right:3.75rem!important;padding-left:3.75rem!important}.px-xxl-16{padding-right:4rem!important;padding-left:4rem!important}.px-xxl-17{padding-right:4.25rem!important;padding-left:4.25rem!important}.px-xxl-18{padding-right:4.5rem!important;padding-left:4.5rem!important}.px-xxl-19{padding-right:4.75rem!important;padding-left:4.75rem!important}.px-xxl-20{padding-right:5rem!important;padding-left:5rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:.75rem!important;padding-bottom:.75rem!important}.py-xxl-4{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-5{padding-top:1.25rem!important;padding-bottom:1.25rem!important}.py-xxl-6{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-7{padding-top:1.75rem!important;padding-bottom:1.75rem!important}.py-xxl-8{padding-top:2rem!important;padding-bottom:2rem!important}.py-xxl-9{padding-top:2.25rem!important;padding-bottom:2.25rem!important}.py-xxl-10{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.py-xxl-11{padding-top:2.75rem!important;padding-bottom:2.75rem!important}.py-xxl-12{padding-top:3rem!important;padding-bottom:3rem!important}.py-xxl-13{padding-top:3.25rem!important;padding-bottom:3.25rem!important}.py-xxl-14{padding-top:3.5rem!important;padding-bottom:3.5rem!important}.py-xxl-15{padding-top:3.75rem!important;padding-bottom:3.75rem!important}.py-xxl-16{padding-top:4rem!important;padding-bottom:4rem!important}.py-xxl-17{padding-top:4.25rem!important;padding-bottom:4.25rem!important}.py-xxl-18{padding-top:4.5rem!important;padding-bottom:4.5rem!important}.py-xxl-19{padding-top:4.75rem!important;padding-bottom:4.75rem!important}.py-xxl-20{padding-top:5rem!important;padding-bottom:5rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:.75rem!important}.pt-xxl-4{padding-top:1rem!important}.pt-xxl-5{padding-top:1.25rem!important}.pt-xxl-6{padding-top:1.5rem!important}.pt-xxl-7{padding-top:1.75rem!important}.pt-xxl-8{padding-top:2rem!important}.pt-xxl-9{padding-top:2.25rem!important}.pt-xxl-10{padding-top:2.5rem!important}.pt-xxl-11{padding-top:2.75rem!important}.pt-xxl-12{padding-top:3rem!important}.pt-xxl-13{padding-top:3.25rem!important}.pt-xxl-14{padding-top:3.5rem!important}.pt-xxl-15{padding-top:3.75rem!important}.pt-xxl-16{padding-top:4rem!important}.pt-xxl-17{padding-top:4.25rem!important}.pt-xxl-18{padding-top:4.5rem!important}.pt-xxl-19{padding-top:4.75rem!important}.pt-xxl-20{padding-top:5rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:.75rem!important}.pe-xxl-4{padding-right:1rem!important}.pe-xxl-5{padding-right:1.25rem!important}.pe-xxl-6{padding-right:1.5rem!important}.pe-xxl-7{padding-right:1.75rem!important}.pe-xxl-8{padding-right:2rem!important}.pe-xxl-9{padding-right:2.25rem!important}.pe-xxl-10{padding-right:2.5rem!important}.pe-xxl-11{padding-right:2.75rem!important}.pe-xxl-12{padding-right:3rem!important}.pe-xxl-13{padding-right:3.25rem!important}.pe-xxl-14{padding-right:3.5rem!important}.pe-xxl-15{padding-right:3.75rem!important}.pe-xxl-16{padding-right:4rem!important}.pe-xxl-17{padding-right:4.25rem!important}.pe-xxl-18{padding-right:4.5rem!important}.pe-xxl-19{padding-right:4.75rem!important}.pe-xxl-20{padding-right:5rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:.75rem!important}.pb-xxl-4{padding-bottom:1rem!important}.pb-xxl-5{padding-bottom:1.25rem!important}.pb-xxl-6{padding-bottom:1.5rem!important}.pb-xxl-7{padding-bottom:1.75rem!important}.pb-xxl-8{padding-bottom:2rem!important}.pb-xxl-9{padding-bottom:2.25rem!important}.pb-xxl-10{padding-bottom:2.5rem!important}.pb-xxl-11{padding-bottom:2.75rem!important}.pb-xxl-12{padding-bottom:3rem!important}.pb-xxl-13{padding-bottom:3.25rem!important}.pb-xxl-14{padding-bottom:3.5rem!important}.pb-xxl-15{padding-bottom:3.75rem!important}.pb-xxl-16{padding-bottom:4rem!important}.pb-xxl-17{padding-bottom:4.25rem!important}.pb-xxl-18{padding-bottom:4.5rem!important}.pb-xxl-19{padding-bottom:4.75rem!important}.pb-xxl-20{padding-bottom:5rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:.75rem!important}.ps-xxl-4{padding-left:1rem!important}.ps-xxl-5{padding-left:1.25rem!important}.ps-xxl-6{padding-left:1.5rem!important}.ps-xxl-7{padding-left:1.75rem!important}.ps-xxl-8{padding-left:2rem!important}.ps-xxl-9{padding-left:2.25rem!important}.ps-xxl-10{padding-left:2.5rem!important}.ps-xxl-11{padding-left:2.75rem!important}.ps-xxl-12{padding-left:3rem!important}.ps-xxl-13{padding-left:3.25rem!important}.ps-xxl-14{padding-left:3.5rem!important}.ps-xxl-15{padding-left:3.75rem!important}.ps-xxl-16{padding-left:4rem!important}.ps-xxl-17{padding-left:4.25rem!important}.ps-xxl-18{padding-left:4.5rem!important}.ps-xxl-19{padding-left:4.75rem!important}.ps-xxl-20{padding-left:5rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:.75rem!important}.gap-xxl-4{gap:1rem!important}.gap-xxl-5{gap:1.25rem!important}.gap-xxl-6{gap:1.5rem!important}.gap-xxl-7{gap:1.75rem!important}.gap-xxl-8{gap:2rem!important}.gap-xxl-9{gap:2.25rem!important}.gap-xxl-10{gap:2.5rem!important}.gap-xxl-11{gap:2.75rem!important}.gap-xxl-12{gap:3rem!important}.gap-xxl-13{gap:3.25rem!important}.gap-xxl-14{gap:3.5rem!important}.gap-xxl-15{gap:3.75rem!important}.gap-xxl-16{gap:4rem!important}.gap-xxl-17{gap:4.25rem!important}.gap-xxl-18{gap:4.5rem!important}.gap-xxl-19{gap:4.75rem!important}.gap-xxl-20{gap:5rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:.75rem!important}.row-gap-xxl-4{row-gap:1rem!important}.row-gap-xxl-5{row-gap:1.25rem!important}.row-gap-xxl-6{row-gap:1.5rem!important}.row-gap-xxl-7{row-gap:1.75rem!important}.row-gap-xxl-8{row-gap:2rem!important}.row-gap-xxl-9{row-gap:2.25rem!important}.row-gap-xxl-10{row-gap:2.5rem!important}.row-gap-xxl-11{row-gap:2.75rem!important}.row-gap-xxl-12{row-gap:3rem!important}.row-gap-xxl-13{row-gap:3.25rem!important}.row-gap-xxl-14{row-gap:3.5rem!important}.row-gap-xxl-15{row-gap:3.75rem!important}.row-gap-xxl-16{row-gap:4rem!important}.row-gap-xxl-17{row-gap:4.25rem!important}.row-gap-xxl-18{row-gap:4.5rem!important}.row-gap-xxl-19{row-gap:4.75rem!important}.row-gap-xxl-20{row-gap:5rem!important}.column-gap-xxl-0{column-gap:0!important}.column-gap-xxl-1{column-gap:.25rem!important}.column-gap-xxl-2{column-gap:.5rem!important}.column-gap-xxl-3{column-gap:.75rem!important}.column-gap-xxl-4{column-gap:1rem!important}.column-gap-xxl-5{column-gap:1.25rem!important}.column-gap-xxl-6{column-gap:1.5rem!important}.column-gap-xxl-7{column-gap:1.75rem!important}.column-gap-xxl-8{column-gap:2rem!important}.column-gap-xxl-9{column-gap:2.25rem!important}.column-gap-xxl-10{column-gap:2.5rem!important}.column-gap-xxl-11{column-gap:2.75rem!important}.column-gap-xxl-12{column-gap:3rem!important}.column-gap-xxl-13{column-gap:3.25rem!important}.column-gap-xxl-14{column-gap:3.5rem!important}.column-gap-xxl-15{column-gap:3.75rem!important}.column-gap-xxl-16{column-gap:4rem!important}.column-gap-xxl-17{column-gap:4.25rem!important}.column-gap-xxl-18{column-gap:4.5rem!important}.column-gap-xxl-19{column-gap:4.75rem!important}.column-gap-xxl-20{column-gap:5rem!important}.fs-xxl-1{font-size:calc(1.3rem + .6vw)!important}.fs-xxl-2{font-size:calc(1.275rem + .3vw)!important}.fs-xxl-3{font-size:calc(1.26rem + .12vw)!important}.fs-xxl-4{font-size:1.25rem!important}.fs-xxl-5{font-size:1.15rem!important}.fs-xxl-6{font-size:1.075rem!important}.fs-xxl-7{font-size:.95rem!important}.fs-xxl-8{font-size:.85rem!important}.fs-xxl-9{font-size:.75rem!important}.fs-xxl-10{font-size:.5rem!important}.fs-xxl-sm{font-size:.95rem!important}.fs-xxl-base{font-size:1rem!important}.fs-xxl-lg{font-size:1.075rem!important}.fs-xxl-xl{font-size:1.21rem!important}.fs-xxl-fluid{font-size:100%!important}.fs-xxl-2x{font-size:calc(1.325rem + .9vw)!important}.fs-xxl-2qx{font-size:calc(1.35rem + 1.2vw)!important}.fs-xxl-2hx{font-size:calc(1.375rem + 1.5vw)!important}.fs-xxl-2tx{font-size:calc(1.4rem + 1.8vw)!important}.fs-xxl-3x{font-size:calc(1.425rem + 2.1vw)!important}.fs-xxl-3qx{font-size:calc(1.45rem + 2.4vw)!important}.fs-xxl-3hx{font-size:calc(1.475rem + 2.7vw)!important}.fs-xxl-3tx{font-size:calc(1.5rem + 3vw)!important}.fs-xxl-4x{font-size:calc(1.525rem + 3.3vw)!important}.fs-xxl-4qx{font-size:calc(1.55rem + 3.6vw)!important}.fs-xxl-4hx{font-size:calc(1.575rem + 3.9vw)!important}.fs-xxl-4tx{font-size:calc(1.6rem + 4.2vw)!important}.fs-xxl-5x{font-size:calc(1.625rem + 4.5vw)!important}.fs-xxl-5qx{font-size:calc(1.65rem + 4.8vw)!important}.fs-xxl-5hx{font-size:calc(1.675rem + 5.1vw)!important}.fs-xxl-5tx{font-size:calc(1.7rem + 5.4vw)!important}.fs-xxl-6x{font-size:calc(1.725rem + 5.7vw)!important}.fs-xxl-6qx{font-size:calc(1.75rem + 6vw)!important}.fs-xxl-6hx{font-size:calc(1.775rem + 6.3vw)!important}.fs-xxl-6tx{font-size:calc(1.8rem + 6.6vw)!important}.fs-xxl-7x{font-size:calc(1.825rem + 6.9vw)!important}.fs-xxl-7qx{font-size:calc(1.85rem + 7.2vw)!important}.fs-xxl-7hx{font-size:calc(1.875rem + 7.5vw)!important}.fs-xxl-7tx{font-size:calc(1.9rem + 7.8vw)!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}.min-w-xxl-unset{min-width:unset!important}.min-w-xxl-25{min-width:25%!important}.min-w-xxl-50{min-width:50%!important}.min-w-xxl-75{min-width:75%!important}.min-w-xxl-100{min-width:100%!important}.min-w-xxl-auto{min-width:auto!important}.min-w-xxl-1px{min-width:1px!important}.min-w-xxl-2px{min-width:2px!important}.min-w-xxl-3px{min-width:3px!important}.min-w-xxl-4px{min-width:4px!important}.min-w-xxl-5px{min-width:5px!important}.min-w-xxl-6px{min-width:6px!important}.min-w-xxl-7px{min-width:7px!important}.min-w-xxl-8px{min-width:8px!important}.min-w-xxl-9px{min-width:9px!important}.min-w-xxl-10px{min-width:10px!important}.min-w-xxl-15px{min-width:15px!important}.min-w-xxl-20px{min-width:20px!important}.min-w-xxl-25px{min-width:25px!important}.min-w-xxl-30px{min-width:30px!important}.min-w-xxl-35px{min-width:35px!important}.min-w-xxl-40px{min-width:40px!important}.min-w-xxl-45px{min-width:45px!important}.min-w-xxl-50px{min-width:50px!important}.min-w-xxl-55px{min-width:55px!important}.min-w-xxl-60px{min-width:60px!important}.min-w-xxl-65px{min-width:65px!important}.min-w-xxl-70px{min-width:70px!important}.min-w-xxl-75px{min-width:75px!important}.min-w-xxl-80px{min-width:80px!important}.min-w-xxl-85px{min-width:85px!important}.min-w-xxl-90px{min-width:90px!important}.min-w-xxl-95px{min-width:95px!important}.min-w-xxl-100px{min-width:100px!important}.min-w-xxl-125px{min-width:125px!important}.min-w-xxl-150px{min-width:150px!important}.min-w-xxl-175px{min-width:175px!important}.min-w-xxl-200px{min-width:200px!important}.min-w-xxl-225px{min-width:225px!important}.min-w-xxl-250px{min-width:250px!important}.min-w-xxl-275px{min-width:275px!important}.min-w-xxl-300px{min-width:300px!important}.min-w-xxl-325px{min-width:325px!important}.min-w-xxl-350px{min-width:350px!important}.min-w-xxl-375px{min-width:375px!important}.min-w-xxl-400px{min-width:400px!important}.min-w-xxl-425px{min-width:425px!important}.min-w-xxl-450px{min-width:450px!important}.min-w-xxl-475px{min-width:475px!important}.min-w-xxl-500px{min-width:500px!important}.min-w-xxl-550px{min-width:550px!important}.min-w-xxl-600px{min-width:600px!important}.min-w-xxl-650px{min-width:650px!important}.min-w-xxl-700px{min-width:700px!important}.min-w-xxl-750px{min-width:750px!important}.min-w-xxl-800px{min-width:800px!important}.min-w-xxl-850px{min-width:850px!important}.min-w-xxl-900px{min-width:900px!important}.min-w-xxl-950px{min-width:950px!important}.min-w-xxl-1000px{min-width:1000px!important}.min-h-xxl-unset{min-height:unset!important}.min-h-xxl-25{min-height:25%!important}.min-h-xxl-50{min-height:50%!important}.min-h-xxl-75{min-height:75%!important}.min-h-xxl-100{min-height:100%!important}.min-h-xxl-auto{min-height:auto!important}.min-h-xxl-1px{min-height:1px!important}.min-h-xxl-2px{min-height:2px!important}.min-h-xxl-3px{min-height:3px!important}.min-h-xxl-4px{min-height:4px!important}.min-h-xxl-5px{min-height:5px!important}.min-h-xxl-6px{min-height:6px!important}.min-h-xxl-7px{min-height:7px!important}.min-h-xxl-8px{min-height:8px!important}.min-h-xxl-9px{min-height:9px!important}.min-h-xxl-10px{min-height:10px!important}.min-h-xxl-15px{min-height:15px!important}.min-h-xxl-20px{min-height:20px!important}.min-h-xxl-25px{min-height:25px!important}.min-h-xxl-30px{min-height:30px!important}.min-h-xxl-35px{min-height:35px!important}.min-h-xxl-40px{min-height:40px!important}.min-h-xxl-45px{min-height:45px!important}.min-h-xxl-50px{min-height:50px!important}.min-h-xxl-55px{min-height:55px!important}.min-h-xxl-60px{min-height:60px!important}.min-h-xxl-65px{min-height:65px!important}.min-h-xxl-70px{min-height:70px!important}.min-h-xxl-75px{min-height:75px!important}.min-h-xxl-80px{min-height:80px!important}.min-h-xxl-85px{min-height:85px!important}.min-h-xxl-90px{min-height:90px!important}.min-h-xxl-95px{min-height:95px!important}.min-h-xxl-100px{min-height:100px!important}.min-h-xxl-125px{min-height:125px!important}.min-h-xxl-150px{min-height:150px!important}.min-h-xxl-175px{min-height:175px!important}.min-h-xxl-200px{min-height:200px!important}.min-h-xxl-225px{min-height:225px!important}.min-h-xxl-250px{min-height:250px!important}.min-h-xxl-275px{min-height:275px!important}.min-h-xxl-300px{min-height:300px!important}.min-h-xxl-325px{min-height:325px!important}.min-h-xxl-350px{min-height:350px!important}.min-h-xxl-375px{min-height:375px!important}.min-h-xxl-400px{min-height:400px!important}.min-h-xxl-425px{min-height:425px!important}.min-h-xxl-450px{min-height:450px!important}.min-h-xxl-475px{min-height:475px!important}.min-h-xxl-500px{min-height:500px!important}.min-h-xxl-550px{min-height:550px!important}.min-h-xxl-600px{min-height:600px!important}.min-h-xxl-650px{min-height:650px!important}.min-h-xxl-700px{min-height:700px!important}.min-h-xxl-750px{min-height:750px!important}.min-h-xxl-800px{min-height:800px!important}.min-h-xxl-850px{min-height:850px!important}.min-h-xxl-900px{min-height:900px!important}.min-h-xxl-950px{min-height:950px!important}.min-h-xxl-1000px{min-height:1000px!important}}@media (min-width:1200px){.fs-1{font-size:1.75rem!important}.fs-2{font-size:1.5rem!important}.fs-3{font-size:1.35rem!important}.fs-2x{font-size:2rem!important}.fs-2qx{font-size:2.25rem!important}.fs-2hx{font-size:2.5rem!important}.fs-2tx{font-size:2.75rem!important}.fs-3x{font-size:3rem!important}.fs-3qx{font-size:3.25rem!important}.fs-3hx{font-size:3.5rem!important}.fs-3tx{font-size:3.75rem!important}.fs-4x{font-size:4rem!important}.fs-4qx{font-size:4.25rem!important}.fs-4hx{font-size:4.5rem!important}.fs-4tx{font-size:4.75rem!important}.fs-5x{font-size:5rem!important}.fs-5qx{font-size:5.25rem!important}.fs-5hx{font-size:5.5rem!important}.fs-5tx{font-size:5.75rem!important}.fs-6x{font-size:6rem!important}.fs-6qx{font-size:6.25rem!important}.fs-6hx{font-size:6.5rem!important}.fs-6tx{font-size:6.75rem!important}.fs-7x{font-size:7rem!important}.fs-7qx{font-size:7.25rem!important}.fs-7hx{font-size:7.5rem!important}.fs-7tx{font-size:7.75rem!important}.fs-sm-1{font-size:1.75rem!important}.fs-sm-2{font-size:1.5rem!important}.fs-sm-3{font-size:1.35rem!important}.fs-sm-2x{font-size:2rem!important}.fs-sm-2qx{font-size:2.25rem!important}.fs-sm-2hx{font-size:2.5rem!important}.fs-sm-2tx{font-size:2.75rem!important}.fs-sm-3x{font-size:3rem!important}.fs-sm-3qx{font-size:3.25rem!important}.fs-sm-3hx{font-size:3.5rem!important}.fs-sm-3tx{font-size:3.75rem!important}.fs-sm-4x{font-size:4rem!important}.fs-sm-4qx{font-size:4.25rem!important}.fs-sm-4hx{font-size:4.5rem!important}.fs-sm-4tx{font-size:4.75rem!important}.fs-sm-5x{font-size:5rem!important}.fs-sm-5qx{font-size:5.25rem!important}.fs-sm-5hx{font-size:5.5rem!important}.fs-sm-5tx{font-size:5.75rem!important}.fs-sm-6x{font-size:6rem!important}.fs-sm-6qx{font-size:6.25rem!important}.fs-sm-6hx{font-size:6.5rem!important}.fs-sm-6tx{font-size:6.75rem!important}.fs-sm-7x{font-size:7rem!important}.fs-sm-7qx{font-size:7.25rem!important}.fs-sm-7hx{font-size:7.5rem!important}.fs-sm-7tx{font-size:7.75rem!important}.fs-md-1{font-size:1.75rem!important}.fs-md-2{font-size:1.5rem!important}.fs-md-3{font-size:1.35rem!important}.fs-md-2x{font-size:2rem!important}.fs-md-2qx{font-size:2.25rem!important}.fs-md-2hx{font-size:2.5rem!important}.fs-md-2tx{font-size:2.75rem!important}.fs-md-3x{font-size:3rem!important}.fs-md-3qx{font-size:3.25rem!important}.fs-md-3hx{font-size:3.5rem!important}.fs-md-3tx{font-size:3.75rem!important}.fs-md-4x{font-size:4rem!important}.fs-md-4qx{font-size:4.25rem!important}.fs-md-4hx{font-size:4.5rem!important}.fs-md-4tx{font-size:4.75rem!important}.fs-md-5x{font-size:5rem!important}.fs-md-5qx{font-size:5.25rem!important}.fs-md-5hx{font-size:5.5rem!important}.fs-md-5tx{font-size:5.75rem!important}.fs-md-6x{font-size:6rem!important}.fs-md-6qx{font-size:6.25rem!important}.fs-md-6hx{font-size:6.5rem!important}.fs-md-6tx{font-size:6.75rem!important}.fs-md-7x{font-size:7rem!important}.fs-md-7qx{font-size:7.25rem!important}.fs-md-7hx{font-size:7.5rem!important}.fs-md-7tx{font-size:7.75rem!important}.fs-lg-1{font-size:1.75rem!important}.fs-lg-2{font-size:1.5rem!important}.fs-lg-3{font-size:1.35rem!important}.fs-lg-2x{font-size:2rem!important}.fs-lg-2qx{font-size:2.25rem!important}.fs-lg-2hx{font-size:2.5rem!important}.fs-lg-2tx{font-size:2.75rem!important}.fs-lg-3x{font-size:3rem!important}.fs-lg-3qx{font-size:3.25rem!important}.fs-lg-3hx{font-size:3.5rem!important}.fs-lg-3tx{font-size:3.75rem!important}.fs-lg-4x{font-size:4rem!important}.fs-lg-4qx{font-size:4.25rem!important}.fs-lg-4hx{font-size:4.5rem!important}.fs-lg-4tx{font-size:4.75rem!important}.fs-lg-5x{font-size:5rem!important}.fs-lg-5qx{font-size:5.25rem!important}.fs-lg-5hx{font-size:5.5rem!important}.fs-lg-5tx{font-size:5.75rem!important}.fs-lg-6x{font-size:6rem!important}.fs-lg-6qx{font-size:6.25rem!important}.fs-lg-6hx{font-size:6.5rem!important}.fs-lg-6tx{font-size:6.75rem!important}.fs-lg-7x{font-size:7rem!important}.fs-lg-7qx{font-size:7.25rem!important}.fs-lg-7hx{font-size:7.5rem!important}.fs-lg-7tx{font-size:7.75rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}:root{--bs-xs:0;--bs-sm:576px;--bs-md:768px;--bs-lg:992px;--bs-xl:1200px;--bs-xxl:1400px;--bs-scrollbar-size:5px;--bs-scrollbar-overlay-size:19px;--bs-scrollbar-overlay-space:7px;--bs-white-bg-rgb:255,255,255;--bs-black-bg-rgb:0,0,0}[data-bs-theme=light]{--bs-text-muted:#6D6D97;--bs-gray-100:#F9F9F9;--bs-gray-100-rgb:249,249,249;--bs-gray-200:#F1F1F4;--bs-gray-200-rgb:241,241,244;--bs-gray-300:#DBDFE9;--bs-gray-300-rgb:219,223,233;--bs-gray-400:#C4CADA;--bs-gray-400-rgb:196,202,218;--bs-gray-500:#6D6D97;--bs-gray-500-rgb:109,109,151;--bs-gray-600:#78829D;--bs-gray-600-rgb:120,130,157;--bs-gray-700:#4B5675;--bs-gray-700-rgb:75,86,117;--bs-gray-800:#252F4A;--bs-gray-800-rgb:37,47,74;--bs-gray-900:#071437;--bs-gray-900-rgb:7,20,55;--bs-light:#F9F9F9;--bs-primary:#0069E0;--bs-secondary:#F1F1F4;--bs-success:#17C653;--bs-info:#7239EA;--bs-warning:#F6C000;--bs-danger:#F8285A;--bs-dark:#1E2129;--bs-primary-active:#056EE9;--bs-secondary-active:#C4CADA;--bs-light-active:#F1F1F4;--bs-success-active:#04B440;--bs-info-active:#5014D0;--bs-warning-active:#DEAD00;--bs-danger-active:#D81A48;--bs-dark-active:#111318;--bs-primary-light:#E9F3FF;--bs-secondary-light:#F9F9F9;--bs-success-light:#DFFFEA;--bs-info-light:#F8F5FF;--bs-warning-light:#FFF8DD;--bs-danger-light:#FFEEF3;--bs-dark-light:#F9F9F9;--bs-light-light:#ffffff;--bs-primary-inverse:#ffffff;--bs-secondary-inverse:#252F4A;--bs-light-inverse:#252F4A;--bs-success-inverse:#ffffff;--bs-info-inverse:#ffffff;--bs-warning-inverse:#ffffff;--bs-danger-inverse:#ffffff;--bs-dark-inverse:#ffffff;--bs-primary-clarity:rgba(0, 105, 224, 0.2);--bs-secondary-clarity:rgba(249, 249, 249, 0.2);--bs-success-clarity:rgba(23, 198, 83, 0.2);--bs-info-clarity:rgba(114, 57, 234, 0.2);--bs-warning-clarity:rgba(246, 192, 0, 0.2);--bs-danger-clarity:rgba(248, 40, 90, 0.2);--bs-dark-clarity:rgba(30, 33, 41, 0.2);--bs-light-clarity:rgba(255, 255, 255, 0.2);--bs-light-rgb:249,249,249;--bs-primary-rgb:0,105,224;--bs-secondary-rgb:241,241,244;--bs-success-rgb:23,198,83;--bs-info-rgb:114,57,234;--bs-warning-rgb:246,192,0;--bs-danger-rgb:248,40,90;--bs-dark-rgb:30,33,41;--bs-text-white:#ffffff;--bs-text-primary:#0069E0;--bs-text-secondary:#F1F1F4;--bs-text-light:#F9F9F9;--bs-text-success:#17C653;--bs-text-info:#7239EA;--bs-text-warning:#F6C000;--bs-text-danger:#F8285A;--bs-text-dark:#1E2129;--bs-text-muted:#6D6D97;--bs-text-gray-100:#F9F9F9;--bs-text-gray-200:#F1F1F4;--bs-text-gray-300:#DBDFE9;--bs-text-gray-400:#C4CADA;--bs-text-gray-500:#6D6D97;--bs-text-gray-600:#78829D;--bs-text-gray-700:#4B5675;--bs-text-gray-800:#252F4A;--bs-text-gray-900:#071437;--bs-border-color:#F1F1F4;--bs-border-dashed-color:#DBDFE9;--bs-component-active-color:#ffffff;--bs-component-active-bg:#0069E0;--bs-component-hover-color:#0069E0;--bs-component-hover-bg:#F9F9F9;--bs-component-checked-color:#ffffff;--bs-component-checked-bg:#0069E0;--bs-box-shadow-xs:0 0.1rem 0.75rem 0.25rem rgba(0, 0, 0, 0.05);--bs-box-shadow-sm:0 0.1rem 1rem 0.25rem rgba(0, 0, 0, 0.05);--bs-box-shadow:0 0.5rem 1.5rem 0.5rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 2rem 1rem rgba(0, 0, 0, 0.1);--bs-input-bg:var(--bs-body-bg);--bs-input-color:var(--bs-gray-700);--bs-input-solid-color:var(--bs-gray-700);--bs-input-solid-bg:var(--bs-gray-100);--bs-input-solid-bg-focus:var(--bs-gray-200);--bs-input-solid-placeholder-color:var(--bs-gray-500);--bs-root-card-box-shadow:0px 3px 4px 0px rgba(0, 0, 0, 0.03);--bs-root-card-border-color:#F1F1F4;--bs-tooltip-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.15);--bs-table-striped-bg:rgba(var(--bs-gray-100-rgb), 0.75);--bs-table-loading-message-box-shadow:0px 0px 50px 0px rgba(82, 63, 105, 0.15);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-box-shadow:0px 0px 50px 0px rgba(82, 63, 105, 0.15);--bs-code-bg:#f1f3f8;--bs-code-shadow:0px 3px 9px rgba(0, 0, 0, 0.08);--bs-code-border-color:transparent;--bs-code-color:#b93993;--bs-symbol-label-color:var(--bs-gray-800);--bs-symbol-label-bg:var(--bs-gray-100);--bs-symbol-border-color:rgba(var(--bs-body-bg), 0.5);--bs-bullet-bg-color:var(--bs-gray-400);--bs-scrolltop-opacity:0;--bs-scrolltop-opacity-on:0.3;--bs-scrolltop-opacity-hover:1;--bs-scrolltop-box-shadow:var(--bs-box-shadow);--bs-scrolltop-bg-color:var(--bs-primary);--bs-scrolltop-bg-color-hover:var(--bs-primary);--bs-scrolltop-icon-color:var(--bs-primary-inverse);--bs-scrolltop-icon-color-hover:var(--bs-primary-inverse);--bs-drawer-box-shadow:0px 1px 9px -3px rgba(0, 0, 0, 0.05);--bs-drawer-bg-color:#ffffff;--bs-drawer-overlay-bg-color:rgba(0, 0, 0, 0.2);--bs-menu-dropdown-box-shadow:0px 0px 50px 0px rgba(82, 63, 105, 0.15);--bs-menu-dropdown-bg-color:var(--bs-body-bg);--bs-menu-heading-color:#6D6D97;--bs-menu-link-color-hover:#0069E0;--bs-menu-link-color-show:#0069E0;--bs-menu-link-color-here:#0069E0;--bs-menu-link-color-active:#0069E0;--bs-menu-link-bg-color-hover:#F9F9F9;--bs-menu-link-bg-color-show:#F9F9F9;--bs-menu-link-bg-color-here:#F9F9F9;--bs-menu-link-bg-color-active:#F9F9F9;--bs-scrollbar-color:#F1F1F4;--bs-scrollbar-hover-color:#DBDFE9;--bs-overlay-bg:rgba(0, 0, 0, 0.05);--bs-blockui-overlay-bg:rgba(0, 0, 0, 0.05);--bs-rating-color-default:#C4CADA;--bs-rating-color-active:#FFAD0F;--bs-ribbon-label-box-shadow:0px -1px 5px 0px rgba(30, 33, 41, 0.1);--bs-ribbon-label-bg:#0069E0;--bs-ribbon-label-border-color:#00397a;--bs-ribbon-clip-bg:#1E2129;--bs-engage-btn-bg:#ffffff;--bs-engage-btn-box-shadow:0px 0px 22px #E0E0E0;--bs-engage-btn-border-color:#E8E8E8;--bs-engage-btn-color:#252F4A;--bs-engage-btn-icon-color:#78829D;--bs-engage-btn-color-active:#252F4A}[data-bs-theme=dark]{--bs-text-muted:#9A9CAE;--bs-gray-100:#1B1C22;--bs-gray-100-rgb:27,28,34;--bs-gray-200:#26272F;--bs-gray-200-rgb:38,39,47;--bs-gray-300:#363843;--bs-gray-300-rgb:54,56,67;--bs-gray-400:#464852;--bs-gray-400-rgb:70,72,82;--bs-gray-500:#9A9CAE;--bs-gray-500-rgb:154,156,174;--bs-gray-600:#808290;--bs-gray-600-rgb:128,130,144;--bs-gray-700:#9A9CAE;--bs-gray-700-rgb:154,156,174;--bs-gray-800:#B5B7C8;--bs-gray-800-rgb:181,183,200;--bs-gray-900:#F5F5F5;--bs-gray-900-rgb:245,245,245;--bs-light:#1B1C22;--bs-primary:#1F87FF;--bs-secondary:#363843;--bs-success:#00A261;--bs-info:#883FFF;--bs-warning:#C59A00;--bs-danger:#E42855;--bs-dark:#F9F9F9;--bs-primary-active:#107EFF;--bs-secondary-active:#464852;--bs-light-active:#1F212A;--bs-success-active:#01BF73;--bs-info-active:#9E63FF;--bs-warning-active:#D9AA00;--bs-danger-active:#FF3767;--bs-dark-active:#F9F9F9;--bs-primary-light:#172331;--bs-secondary-light:#363843;--bs-success-light:#1F212A;--bs-info-light:#272134;--bs-warning-light:#242320;--bs-danger-light:#302024;--bs-dark-light:#1E2027;--bs-light-light:#1F212A;--bs-primary-inverse:#ffffff;--bs-secondary-inverse:#ffffff;--bs-light-inverse:#808290;--bs-success-inverse:#ffffff;--bs-info-inverse:#ffffff;--bs-warning-inverse:#ffffff;--bs-danger-inverse:#ffffff;--bs-dark-inverse:#1E2129;--bs-primary-clarity:rgba(31, 135, 255, 0.2);--bs-secondary-clarity:rgba(54, 56, 67, 0.2);--bs-success-clarity:rgba(0, 162, 97, 0.2);--bs-info-clarity:rgba(136, 63, 255, 0.2);--bs-warning-clarity:rgba(197, 154, 0, 0.2);--bs-danger-clarity:rgba(228, 40, 85, 0.2);--bs-dark-clarity:rgba(39, 42, 52, 0.2);--bs-light-clarity:rgba(31, 33, 42, 0.2);--bs-light-rgb:27,28,34;--bs-primary-rgb:31,135,255;--bs-secondary-rgb:54,56,67;--bs-success-rgb:0,162,97;--bs-info-rgb:136,63,255;--bs-warning-rgb:197,154,0;--bs-danger-rgb:228,40,85;--bs-dark-rgb:249,249,249;--bs-text-white:#ffffff;--bs-text-primary:#1F87FF;--bs-text-secondary:#363843;--bs-text-light:#1B1C22;--bs-text-success:#00A261;--bs-text-info:#883FFF;--bs-text-warning:#C59A00;--bs-text-danger:#E42855;--bs-text-dark:#F9F9F9;--bs-text-muted:#9A9CAE;--bs-text-gray-100:#1B1C22;--bs-text-gray-200:#26272F;--bs-text-gray-300:#363843;--bs-text-gray-400:#464852;--bs-text-gray-500:#9A9CAE;--bs-text-gray-600:#808290;--bs-text-gray-700:#9A9CAE;--bs-text-gray-800:#B5B7C8;--bs-text-gray-900:#F5F5F5;--bs-border-color:#26272F;--bs-border-dashed-color:#363843;--bs-component-active-color:#ffffff;--bs-component-active-bg:#1F87FF;--bs-component-hover-color:#1F87FF;--bs-component-hover-bg:#1B1C22;--bs-component-checked-color:#ffffff;--bs-component-checked-bg:#1F87FF;--bs-box-shadow-xs:0 0.1rem 0.75rem 0.25rem rgba(0, 0, 0, 0.05);--bs-box-shadow-sm:0 0.1rem 1rem 0.25rem rgba(0, 0, 0, 0.05);--bs-box-shadow:0 0.5rem 1.5rem 0.5rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 2rem 1rem rgba(0, 0, 0, 0.1);--bs-input-color:var(--bs-gray-700);--bs-input-bg:var(--bs-body-bg);--bs-input-solid-color:var(--bs-gray-700);--bs-input-solid-bg:var(--bs-gray-100);--bs-input-solid-bg-focus:var(--bs-gray-200);--bs-input-solid-placeholder-color:var(--bs-gray-500);--bs-tooltip-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.15);--bs-root-card-box-shadow:none;--bs-root-card-border-color:#1E2027;--bs-table-striped-bg:rgba(27, 28, 34, 0.75);--bs-table-loading-message-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.3);--bs-dropdown-bg:#1C1D22;--bs-dropdown-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.3);--bs-code-bg:#2b2b40;--bs-code-shadow:rgba(0, 0, 0, 0.08) 0px 3px 9px 0px;--bs-code-border-color:transparent;--bs-code-color:#b93993;--bs-symbol-label-color:#B5B7C8;--bs-symbol-label-bg:#1B1C22;--bs-symbol-border-color:rgba(255, 255, 255, 0.5);--bs-bullet-bg-color:#464852;--bs-scrolltop-opacity:0;--bs-scrolltop-opacity-on:0.3;--bs-scrolltop-opacity-hover:1;--bs-scrolltop-box-shadow:0 0.5rem 1.5rem 0.5rem rgba(0, 0, 0, 0.075);--bs-scrolltop-bg-color:#0069E0;--bs-scrolltop-bg-color-hover:#0069E0;--bs-scrolltop-icon-color:#ffffff;--bs-scrolltop-icon-color-hover:#ffffff;--bs-drawer-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.1);--bs-drawer-bg-color:#1C1D22;--bs-drawer-overlay-bg-color:rgba(0, 0, 0, 0.4);--bs-menu-dropdown-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.3);--bs-menu-dropdown-bg-color:#1C1D22;--bs-menu-heading-color:#9A9CAE;--bs-menu-link-color-hover:#1F87FF;--bs-menu-link-color-show:#1F87FF;--bs-menu-link-color-here:#1F87FF;--bs-menu-link-color-active:#1F87FF;--bs-menu-link-bg-color-hover:#1B1C22;--bs-menu-link-bg-color-show:#1B1C22;--bs-menu-link-bg-color-here:#1B1C22;--bs-menu-link-bg-color-active:#1B1C22;--bs-scrollbar-color:#26272F;--bs-scrollbar-hover-color:#363843;--bs-overlay-bg:rgba(255, 255, 255, 0.05);--bs-blockui-overlay-bg:rgba(255, 255, 255, 0.05);--bs-rating-color-default:#464852;--bs-rating-color-active:#FFAD0F;--bs-ribbon-label-box-shadow:0px -1px 5px 0px rgba(255, 255, 255, 0.1);--bs-ribbon-label-bg:#0069E0;--bs-ribbon-label-border-color:#00397a;--bs-ribbon-clip-bg:#F9F9F9;--bs-engage-btn-bg:#26272F;--bs-engage-btn-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.3);--bs-engage-btn-border-color:#26272F;--bs-engage-btn-color:#B5B7C8;--bs-engage-btn-icon-color:#808290;--bs-engage-btn-color-active:#B5B7C8}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{outline:0}.blockquote-footer{color:var(--bs-blockquote-footer-color)}.list-style-none{list-style:none}[data-kt-theme-mode-switching=true] *{transition:none!important}[data-bs-theme=light] .theme-dark-show{display:none!important}[data-bs-theme=light] .theme-light-bg-transparent{background-color:transparent!important}[data-bs-theme=light] .theme-light-bg-body{background-color:var(--bs-body-bg)!important}[data-bs-theme=dark] .theme-light-show{display:none!important}[data-bs-theme=dark] .theme-dark-bg-transparent{background-color:transparent!important}[data-bs-theme=dark] .theme-dark-bg-body{background-color:var(--bs-body-bg)!important}.animation{animation-duration:1s;animation-fill-mode:both}@keyframes animationSlideInDown{from{transform:translate3d(0,-100%,0);visibility:visible}to{transform:translate3d(0,0,0)}}.animation-slide-in-down{animation-name:animationSlideInDown}@keyframes animationSlideInUp{from{transform:translate3d(0,100%,0);visibility:visible}to{transform:translate3d(0,0,0)}}.animation-slide-in-up{animation-name:animationSlideInUp}@keyframes animationFadeIn{from{opacity:0}to{opacity:1}}.animation-fade-in{animation-name:animationFadeIn}@keyframes animationFadeOut{from{opacity:1}to{opacity:0}}.animation-fade-out{animation-name:animationFadeOut}.animation-blink{animation:animationBlink 1s steps(5,start) infinite}@keyframes animationBlink{to{visibility:hidden}}.alert-light{color:var(--bs-light);border-color:var(--bs-light);background-color:var(--bs-light-light)}.alert-light .alert-link{color:var(--bs-light)}.alert-primary{color:var(--bs-primary);border-color:var(--bs-primary);background-color:var(--bs-primary-light)}.alert-primary .alert-link{color:var(--bs-primary)}.alert-secondary{color:var(--bs-secondary);border-color:var(--bs-secondary);background-color:var(--bs-secondary-light)}.alert-secondary .alert-link{color:var(--bs-secondary)}.alert-success{color:var(--bs-success);border-color:var(--bs-success);background-color:var(--bs-success-light)}.alert-success .alert-link{color:var(--bs-success)}.alert-info{color:var(--bs-info);border-color:var(--bs-info);background-color:var(--bs-info-light)}.alert-info .alert-link{color:var(--bs-info)}.alert-warning{color:var(--bs-warning);border-color:var(--bs-warning);background-color:var(--bs-warning-light)}.alert-warning .alert-link{color:var(--bs-warning)}.alert-danger{color:var(--bs-danger);border-color:var(--bs-danger);background-color:var(--bs-danger-light)}.alert-danger .alert-link{color:var(--bs-danger)}.alert-dark{color:var(--bs-dark);border-color:var(--bs-dark);background-color:var(--bs-dark-light)}.alert-dark .alert-link{color:var(--bs-dark)}[data-bs-theme=dark] .dropdown-menu{box-shadow:0 0 30px rgba(0,0,0,.3)}.toast .toast-header .btn-close{margin-right:0}[data-bs-theme=dark] .toast{--bs-toast-bg:#26272F;--bs-toast-header-bg:#26272F;--bs-toast-header-border-color:#363843}.nav-pills .nav-item{margin-right:.5rem}.nav-pills .nav-item:last-child{margin-right:0}.nav-stretch{align-items:stretch;padding-top:0!important;padding-bottom:0!important}.nav-stretch .nav-item{display:flex;align-items:stretch;padding-top:0!important;padding-bottom:0!important}.nav-stretch .nav-link{display:flex;align-items:center}.nav-group{padding:.35rem;border-radius:.475rem;background-color:var(--bs-gray-100)}.nav-group.nav-group-outline{background-color:transparent;border:1px solid var(--bs-border-color)}.nav-group.nav-group-fluid{display:flex}.nav-group.nav-group-fluid>.btn,.nav-group.nav-group-fluid>label{position:relative;flex-shrink:0;flex-grow:1;flex-basis:0}.nav-group.nav-group-fluid>label{margin-right:.1rem}.nav-group.nav-group-fluid>label>.btn{width:100%}.nav-group.nav-group-fluid>label:last-child{margin-right:0}.nav-line-tabs{border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:var(--bs-border-color)}.nav-line-tabs .nav-item{margin-bottom:-1px}.nav-line-tabs .nav-item .nav-link{color:var(--bs-gray-700);border:0;border-bottom:1px solid transparent;transition:color .2s ease;padding:.5rem 0;margin:0 1rem}.nav-line-tabs .nav-item:first-child .nav-link{margin-left:0}.nav-line-tabs .nav-item:last-child .nav-link{margin-right:0}.nav-line-tabs .nav-item .nav-link.active,.nav-line-tabs .nav-item .nav-link:hover:not(.disabled),.nav-line-tabs .nav-item.show .nav-link{background-color:transparent;border:0;border-bottom:1px solid var(--bs-primary);color:var(--bs-primary);transition:color .2s ease}.nav-line-tabs.nav-line-tabs-2x{border-bottom-width:2px}.nav-line-tabs.nav-line-tabs-2x .nav-item{margin-bottom:-2px}.nav-line-tabs.nav-line-tabs-2x .nav-item .nav-link{border-bottom-width:2px}.nav-line-tabs.nav-line-tabs-2x .nav-item .nav-link.active,.nav-line-tabs.nav-line-tabs-2x .nav-item .nav-link:hover:not(.disabled),.nav-line-tabs.nav-line-tabs-2x .nav-item.show .nav-link{color:var(--bs-primary);border-bottom-width:2px}.nav.nav-pills.nav-pills-custom .nav-link,.nav.nav-pills.nav-pills-custom .show>.nav-link{border:1px dashed var(--bs-border-dashed-color);border-radius:.625rem}.nav.nav-pills.nav-pills-custom .nav-link.nav-link-border-solid,.nav.nav-pills.nav-pills-custom .show>.nav-link.nav-link-border-solid{border:3px solid var(--bs-border-dashed-color)}.nav.nav-pills.nav-pills-custom .nav-link.nav-link-border-solid.active,.nav.nav-pills.nav-pills-custom .show>.nav-link.nav-link-border-solid.active{border:3px solid var(--bs-primary)}.nav.nav-pills.nav-pills-custom .nav-link .nav-icon img,.nav.nav-pills.nav-pills-custom .show>.nav-link .nav-icon img{width:30px;transition:color .2s ease}.nav.nav-pills.nav-pills-custom .nav-link .nav-icon img.default,.nav.nav-pills.nav-pills-custom .show>.nav-link .nav-icon img.default{display:inline-block}.nav.nav-pills.nav-pills-custom .nav-link .nav-icon img.active,.nav.nav-pills.nav-pills-custom .show>.nav-link .nav-icon img.active{display:none}.nav.nav-pills.nav-pills-custom .nav-link.active,.nav.nav-pills.nav-pills-custom .show>.nav-link.active{background-color:transparent;border:1px solid var(--bs-border-dashed-color);transition-duration:1ms;position:relative}.nav.nav-pills.nav-pills-custom .nav-link.active .nav-text,.nav.nav-pills.nav-pills-custom .show>.nav-link.active .nav-text{color:var(--bs-gray-800)!important;transition:color .2s ease}.nav.nav-pills.nav-pills-custom .nav-link.active .bullet-custom,.nav.nav-pills.nav-pills-custom .show>.nav-link.active .bullet-custom{display:block}.nav.nav-pills.nav-pills-custom .nav-link .bullet-custom,.nav.nav-pills.nav-pills-custom .show>.nav-link .bullet-custom{display:none}.nav.nav-pills.nav-pills-custom.nav-pills-active-custom .nav-item .nav-link:not(:active) span:nth-child(1){color:#b5b5c3}.nav.nav-pills.nav-pills-custom.nav-pills-active-custom .nav-item .nav-link:not(:active) span:nth-child(2){color:#3f4254}.nav.nav-pills.nav-pills-custom.nav-pills-active-custom .nav-item .nav-link:hover span:nth-child(1){color:#fff!important}.nav.nav-pills.nav-pills-custom.nav-pills-active-custom .nav-item .nav-link:hover span:nth-child(2){color:#fff!important}.nav.nav-pills.nav-pills-custom.nav-pills-active-custom .nav-item .nav-link.active span:nth-child(1){color:#fff!important}.nav.nav-pills.nav-pills-custom.nav-pills-active-custom .nav-item .nav-link.active span:nth-child(2){color:#fff!important}.pagination{display:flex;flex-wrap:wrap;justify-content:center;margin:0}.pagination.pagination-circle .page-link{border-radius:50%}.pagination.pagination-outline .page-link{border:1px solid var(--bs-border-color)}.pagination.pagination-outline .page-item.active .page-link,.pagination.pagination-outline .page-item:hover:not(.disabled) .page-link{border-color:var(--bs-primary-light)}.page-item{margin-right:.5rem}.page-item:last-child{margin-right:0}.page-item .page-link{display:flex;justify-content:center;align-items:center;border-radius:.475rem;height:2.5rem;min-width:2.5rem;font-weight:500;font-size:1.075rem}.page-item .page-link i{font-size:.85rem}.page-item .page-link .next,.page-item .page-link .previous{display:block;height:.875rem;width:.875rem}.page-item .page-link .first{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\")}.page-item .page-link .previous{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\")}.page-item .page-link .next{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.page-item .page-link .last{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.page-item:focus .page-link{color:var(--bs-pagination-focus-color)}.page-item:focus .page-link .svg-icon,.page-item:focus .page-link i{color:var(--bs-pagination-focus-color)}.page-item:focus .page-link .previous{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-pagination-focus-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-focus-color%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-focus-color%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\")}.page-item:focus .page-link .next{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-pagination-focus-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-focus-color%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-focus-color%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.page-item:hover:not(.active):not(.offset):not(.disabled) .page-link{color:var(--bs-pagination-hover-color)}.page-item:hover:not(.active):not(.offset):not(.disabled) .page-link.page-text{background-color:transparent}.page-item:hover:not(.active):not(.offset):not(.disabled) .page-link .svg-icon,.page-item:hover:not(.active):not(.offset):not(.disabled) .page-link i{color:var(--bs-pagination-hover-color)}.page-item:hover:not(.active):not(.offset):not(.disabled) .page-link .previous{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-pagination-hover-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-hover-color%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-hover-color%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\")}.page-item:hover:not(.active):not(.offset):not(.disabled) .page-link .next{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-pagination-hover-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-hover-color%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-hover-color%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.page-item.active .page-link{color:var(--bs-pagination-active-color)}.page-item.active .page-link.page-text{background-color:transparent}.page-item.active .page-link .svg-icon,.page-item.active .page-link i{color:var(--bs-pagination-active-color)}.page-item.active .page-link .previous{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-pagination-active-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-active-color%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-active-color%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\")}.page-item.active .page-link .next{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-pagination-active-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-active-color%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-active-color%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.page-item.disabled .page-link{color:var(--bs-pagination-disabled-color)}.page-item.disabled .page-link .svg-icon,.page-item.disabled .page-link i{color:var(--bs-pagination-disabled-color)}.page-item.disabled .page-link .previous{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-pagination-disabled-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-disabled-color%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-disabled-color%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\")}.page-item.disabled .page-link .next{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-pagination-disabled-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-disabled-color%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-pagination-disabled-color%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}@media (max-width:991.98px){.page-item{margin-right:.25rem}.page-item:last-child{margin-right:0}}.separator{display:block;height:0;border-bottom:1px solid var(--bs-border-color)}.separator.separator-dotted{border-bottom-style:dotted;border-bottom-color:var(--bs-border-dashed-color)}.separator.separator-dashed{border-bottom-style:dashed;border-bottom-color:var(--bs-border-dashed-color)}.separator.separator-content{display:flex;align-items:center;border-bottom:0;text-align:center}.separator.separator-content::after,.separator.separator-content::before{content:\" \";width:50%;border-bottom:1px solid var(--bs-border-color)}.separator.separator-content::before{margin-right:1.25rem}.separator.separator-content::after{margin-left:1.25rem}.separator.separator-content.separator-dotted::after,.separator.separator-content.separator-dotted::before{border-bottom-style:dotted;border-bottom-color:var(--bs-border-dashed-color)}.separator.separator-content.separator-dashed::after,.separator.separator-content.separator-dashed::before{border-bottom-style:dashed;border-bottom-color:var(--bs-border-dashed-color)}.separator.separator-content.border-light::after,.separator.separator-content.border-light::before{border-color:#f9f9f9!important}.separator.separator-content.border-primary::after,.separator.separator-content.border-primary::before{border-color:#0069e0!important}.separator.separator-content.border-secondary::after,.separator.separator-content.border-secondary::before{border-color:#f1f1f4!important}.separator.separator-content.border-success::after,.separator.separator-content.border-success::before{border-color:#17c653!important}.separator.separator-content.border-info::after,.separator.separator-content.border-info::before{border-color:#7239ea!important}.separator.separator-content.border-warning::after,.separator.separator-content.border-warning::before{border-color:#f6c000!important}.separator.separator-content.border-danger::after,.separator.separator-content.border-danger::before{border-color:#f8285a!important}.separator.separator-content.border-dark::after,.separator.separator-content.border-dark::before{border-color:#1e2129!important}.carousel-custom .carousel-indicators{align-items:center;position:static;z-index:auto;margin:0;padding:0;list-style:none}.carousel-custom .carousel-indicators li{transform:none;opacity:1}.carousel-custom .carousel-indicators li.active{transform:none;opacity:1}.carousel-custom .carousel-indicators.carousel-indicators-dots li{border-radius:0;background-color:transparent!important;height:13px;width:13px;display:flex;align-items:center;justify-content:center;text-align:center}.carousel-custom .carousel-indicators.carousel-indicators-dots li:after{display:inline-block;content:\" \";border-radius:50%;transition:all .3s ease;background-color:var(--bs-gray-200);height:9px;width:9px}.carousel-custom .carousel-indicators.carousel-indicators-dots li.active{background-color:transparent}.carousel-custom .carousel-indicators.carousel-indicators-dots li.active:after{transition:all .3s ease;height:13px;width:13px;background-color:var(--bs-gray-400)}.carousel-custom .carousel-indicators.carousel-indicators-bullet li{transition:all .3s ease;background-color:transparent!important;border-radius:6px;height:6px;width:6px;display:flex;align-items:center;justify-content:center;text-align:center}.carousel-custom .carousel-indicators.carousel-indicators-bullet li:after{display:inline-block;content:\" \";transition:all .3s ease;background-color:var(--bs-gray-400);border-radius:6px;height:6px;width:6px}.carousel-custom .carousel-indicators.carousel-indicators-bullet li.active{transition:all .3s ease;background-color:transparent;height:6px;width:16px}.carousel-custom .carousel-indicators.carousel-indicators-bullet li.active:after{transition:all .3s ease;height:6px;width:16px;background-color:var(--bs-gray-600)}.carousel-custom .carousel-indicators-active-light li.active:after{background-color:var(--bs-light)!important}.carousel-custom .carousel-indicators-active-primary li.active:after{background-color:var(--bs-primary)!important}.carousel-custom .carousel-indicators-active-secondary li.active:after{background-color:var(--bs-secondary)!important}.carousel-custom .carousel-indicators-active-success li.active:after{background-color:var(--bs-success)!important}.carousel-custom .carousel-indicators-active-info li.active:after{background-color:var(--bs-info)!important}.carousel-custom .carousel-indicators-active-warning li.active:after{background-color:var(--bs-warning)!important}.carousel-custom .carousel-indicators-active-danger li.active:after{background-color:var(--bs-danger)!important}.carousel-custom .carousel-indicators-active-dark li.active:after{background-color:var(--bs-dark)!important}.carousel-custom.carousel-stretch{height:100%;display:flex;flex-direction:column}.carousel-custom.carousel-stretch .carousel-inner{flex-grow:1}.carousel-custom.carousel-stretch .carousel-item{height:100%}.carousel-custom.carousel-stretch .carousel-wrapper{display:flex;flex-direction:column;height:100%}.menu-group{display:flex}.menu,.menu-wrapper{display:flex;padding:0;margin:0;list-style:none}.menu-inner{padding:0;margin:0;list-style:none}.menu-sub{display:none;padding:0;margin:0;list-style:none;flex-direction:column}.menu-item{display:block;padding:.15rem 0}.menu-item .menu-link{cursor:pointer;display:flex;align-items:center;padding:0;flex:0 0 100%;padding:.65rem 1rem;transition:none;outline:0!important}.menu-item .menu-link .menu-icon{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:2rem;margin-right:.5rem}.menu-item .menu-link .menu-icon .svg-icon,.menu-item .menu-link .menu-icon i{line-height:1}.menu-item .menu-link .menu-bullet{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.25rem;margin-right:.5rem}.menu-item .menu-link .menu-title{display:flex;align-items:center;flex-grow:1}.menu-item .menu-link .menu-badge{display:flex;align-items:center;flex-shrink:0;margin-left:.5rem}.menu-item .menu-link .menu-arrow{display:flex;align-items:stretch;position:relative;overflow:hidden;flex-shrink:0;margin-left:5px;width:9px;height:9px}.menu-item .menu-link .menu-arrow:after{display:block;width:100%;content:\" \";will-change:transform;background-size:100% 100%;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-item .menu-content{padding:.65rem 1rem}.menu-item.show .menu-link .menu-arrow:after{backface-visibility:hidden;transition:transform .3s ease}.menu-nowrap .menu-link,.menu-nowrap .menu-title{flex-wrap:nowrap;flex-shrink:0}.menu-center{justify-content:center}.menu-heading{color:var(--bs-menu-heading-color)}.menu-item.menu-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-dropdown.menu.show,.menu-sub-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-accordion{display:none}.menu-sub-accordion.show,.show:not(.menu-dropdown)>.menu-sub-accordion{display:flex}.menu-sub-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-inline{display:flex}.menu-fit>.menu-item>.menu-content,.menu-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-column{flex-direction:column;width:100%}.menu-row{flex-direction:row}.menu-row>.menu-item{display:flex;align-items:center}.menu-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-rounded .menu-link{border-radius:.475rem}.menu-pill .menu-link{border-radius:50px}.menu-rounded-0 .menu-link{border-radius:0!important}@media (min-width:576px){.menu-item.menu-sm-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-sm-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-sm-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-sm-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-sm-dropdown.menu.show,.menu-sub-sm-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-sm-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-sm-accordion{display:none}.menu-sub-sm-accordion.show,.show:not(.menu-dropdown)>.menu-sub-sm-accordion{display:flex}.menu-sub-sm-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-sm-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-sm-inline{display:flex}.menu-sm-fit>.menu-item>.menu-content,.menu-sm-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-sm-column{flex-direction:column;width:100%}.menu-sm-row{flex-direction:row}.menu-sm-row>.menu-item{display:flex;align-items:center}.menu-sm-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-sm-rounded .menu-link{border-radius:.475rem}.menu-sm-pill .menu-link{border-radius:50px}.menu-sm-rounded-0 .menu-link{border-radius:0!important}}@media (min-width:768px){.menu-item.menu-md-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-md-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-md-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-md-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-md-dropdown.menu.show,.menu-sub-md-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-md-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-md-accordion{display:none}.menu-sub-md-accordion.show,.show:not(.menu-dropdown)>.menu-sub-md-accordion{display:flex}.menu-sub-md-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-md-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-md-inline{display:flex}.menu-md-fit>.menu-item>.menu-content,.menu-md-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-md-column{flex-direction:column;width:100%}.menu-md-row{flex-direction:row}.menu-md-row>.menu-item{display:flex;align-items:center}.menu-md-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-md-rounded .menu-link{border-radius:.475rem}.menu-md-pill .menu-link{border-radius:50px}.menu-md-rounded-0 .menu-link{border-radius:0!important}}@media (min-width:992px){.menu-item.menu-lg-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-lg-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-lg-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-lg-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-lg-dropdown.menu.show,.menu-sub-lg-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-lg-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-lg-accordion{display:none}.menu-sub-lg-accordion.show,.show:not(.menu-dropdown)>.menu-sub-lg-accordion{display:flex}.menu-sub-lg-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-lg-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-lg-inline{display:flex}.menu-lg-fit>.menu-item>.menu-content,.menu-lg-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-lg-column{flex-direction:column;width:100%}.menu-lg-row{flex-direction:row}.menu-lg-row>.menu-item{display:flex;align-items:center}.menu-lg-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-lg-rounded .menu-link{border-radius:.475rem}.menu-lg-pill .menu-link{border-radius:50px}.menu-lg-rounded-0 .menu-link{border-radius:0!important}}@media (min-width:1200px){.menu-item.menu-xl-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-xl-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-xl-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-xl-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-xl-dropdown.menu.show,.menu-sub-xl-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-xl-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-xl-accordion{display:none}.menu-sub-xl-accordion.show,.show:not(.menu-dropdown)>.menu-sub-xl-accordion{display:flex}.menu-sub-xl-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-xl-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-xl-inline{display:flex}.menu-xl-fit>.menu-item>.menu-content,.menu-xl-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-xl-column{flex-direction:column;width:100%}.menu-xl-row{flex-direction:row}.menu-xl-row>.menu-item{display:flex;align-items:center}.menu-xl-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-xl-rounded .menu-link{border-radius:.475rem}.menu-xl-pill .menu-link{border-radius:50px}.menu-xl-rounded-0 .menu-link{border-radius:0!important}}@media (min-width:1400px){.menu-item.menu-xxl-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-xxl-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-xxl-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-xxl-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-xxl-dropdown.menu.show,.menu-sub-xxl-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-xxl-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-xxl-accordion{display:none}.menu-sub-xxl-accordion.show,.show:not(.menu-dropdown)>.menu-sub-xxl-accordion{display:flex}.menu-sub-xxl-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-xxl-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-xxl-inline{display:flex}.menu-xxl-fit>.menu-item>.menu-content,.menu-xxl-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-xxl-column{flex-direction:column;width:100%}.menu-xxl-row{flex-direction:row}.menu-xxl-row>.menu-item{display:flex;align-items:center}.menu-xxl-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-xxl-rounded .menu-link{border-radius:.475rem}.menu-xxl-pill .menu-link{border-radius:50px}.menu-xxl-rounded-0 .menu-link{border-radius:0!important}}@media (max-width:575.98px){.menu-item.menu-sm-down-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-sm-down-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-sm-down-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-sm-down-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-sm-down-dropdown.menu.show,.menu-sub-sm-down-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-sm-down-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-sm-down-accordion{display:none}.menu-sub-sm-down-accordion.show,.show:not(.menu-dropdown)>.menu-sub-sm-down-accordion{display:flex}.menu-sub-sm-down-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-sm-down-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-sm-down-inline{display:flex}.menu-sm-down-fit>.menu-item>.menu-content,.menu-sm-down-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-sm-down-column{flex-direction:column;width:100%}.menu-sm-down-row{flex-direction:row}.menu-sm-down-row>.menu-item{display:flex;align-items:center}.menu-sm-down-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-sm-down-rounded .menu-link{border-radius:.475rem}.menu-sm-down-pill .menu-link{border-radius:50px}.menu-sm-down-rounded-0 .menu-link{border-radius:0!important}}@media (max-width:767.98px){.menu-item.menu-md-down-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-md-down-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-md-down-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-md-down-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-md-down-dropdown.menu.show,.menu-sub-md-down-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-md-down-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-md-down-accordion{display:none}.menu-sub-md-down-accordion.show,.show:not(.menu-dropdown)>.menu-sub-md-down-accordion{display:flex}.menu-sub-md-down-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-md-down-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-md-down-inline{display:flex}.menu-md-down-fit>.menu-item>.menu-content,.menu-md-down-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-md-down-column{flex-direction:column;width:100%}.menu-md-down-row{flex-direction:row}.menu-md-down-row>.menu-item{display:flex;align-items:center}.menu-md-down-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-md-down-rounded .menu-link{border-radius:.475rem}.menu-md-down-pill .menu-link{border-radius:50px}.menu-md-down-rounded-0 .menu-link{border-radius:0!important}}@media (max-width:991.98px){.menu-item.menu-lg-down-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-lg-down-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-lg-down-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-lg-down-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-lg-down-dropdown.menu.show,.menu-sub-lg-down-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-lg-down-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-lg-down-accordion{display:none}.menu-sub-lg-down-accordion.show,.show:not(.menu-dropdown)>.menu-sub-lg-down-accordion{display:flex}.menu-sub-lg-down-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-lg-down-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-lg-down-inline{display:flex}.menu-lg-down-fit>.menu-item>.menu-content,.menu-lg-down-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-lg-down-column{flex-direction:column;width:100%}.menu-lg-down-row{flex-direction:row}.menu-lg-down-row>.menu-item{display:flex;align-items:center}.menu-lg-down-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-lg-down-rounded .menu-link{border-radius:.475rem}.menu-lg-down-pill .menu-link{border-radius:50px}.menu-lg-down-rounded-0 .menu-link{border-radius:0!important}}@media (max-width:1199.98px){.menu-item.menu-xl-down-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-xl-down-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-xl-down-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-xl-down-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-xl-down-dropdown.menu.show,.menu-sub-xl-down-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-xl-down-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-xl-down-accordion{display:none}.menu-sub-xl-down-accordion.show,.show:not(.menu-dropdown)>.menu-sub-xl-down-accordion{display:flex}.menu-sub-xl-down-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-xl-down-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-xl-down-inline{display:flex}.menu-xl-down-fit>.menu-item>.menu-content,.menu-xl-down-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-xl-down-column{flex-direction:column;width:100%}.menu-xl-down-row{flex-direction:row}.menu-xl-down-row>.menu-item{display:flex;align-items:center}.menu-xl-down-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-xl-down-rounded .menu-link{border-radius:.475rem}.menu-xl-down-pill .menu-link{border-radius:50px}.menu-xl-down-rounded-0 .menu-link{border-radius:0!important}}@media (max-width:1399.98px){.menu-item.menu-xxl-down-accordion .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-item.menu-xxl-down-accordion.show:not(.hiding):not(.menu-dropdown)>.menu-link .menu-arrow:after,.menu-item.menu-xxl-down-accordion.showing:not(.menu-dropdown)>.menu-link .menu-arrow:after{transform:rotateZ(90deg);transform:rotateZ(-90deg);transition:transform .3s ease}.menu-sub-xxl-down-dropdown{display:none;border-radius:.475rem;background-color:var(--bs-menu-dropdown-bg-color);box-shadow:var(--bs-menu-dropdown-box-shadow);z-index:107}.menu-sub-xxl-down-dropdown.menu.show,.menu-sub-xxl-down-dropdown.show[data-popper-placement],.show.menu-dropdown>.menu-sub-xxl-down-dropdown{display:flex;will-change:transform;animation:menu-sub-dropdown-animation-fade-in .3s ease 1,menu-sub-dropdown-animation-move-up .3s ease 1}.menu-sub-xxl-down-accordion{display:none}.menu-sub-xxl-down-accordion.show,.show:not(.menu-dropdown)>.menu-sub-xxl-down-accordion{display:flex}.menu-sub-xxl-down-indention .menu-sub:not([data-popper-placement]){margin-left:1rem}.menu-sub-xxl-down-indention .menu-item .menu-item .menu-link.active{margin-right:1rem}.menu-xxl-down-inline{display:flex}.menu-xxl-down-fit>.menu-item>.menu-content,.menu-xxl-down-fit>.menu-item>.menu-link{padding-left:0!important;padding-right:0!important}.menu-xxl-down-column{flex-direction:column;width:100%}.menu-xxl-down-row{flex-direction:row}.menu-xxl-down-row>.menu-item{display:flex;align-items:center}.menu-xxl-down-row>.menu-item>.menu-link .menu-arrow:after{transform:rotateZ(-90deg);transform:rotateZ(90deg);transition:transform .3s ease}.menu-xxl-down-rounded .menu-link{border-radius:.475rem}.menu-xxl-down-pill .menu-link{border-radius:50px}.menu-xxl-down-rounded-0 .menu-link{border-radius:0!important}}.menu-link-indention .menu-item{padding-top:0;padding-bottom:0}.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-link{padding-left:calc(1rem + 1rem)}.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link{padding-left:calc(2rem + 1rem)}.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link{padding-left:calc(3rem + 1rem)}.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link{padding-left:calc(4rem + 1rem)}.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-link{padding-left:1rem;padding-right:0}.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link{padding-left:calc(2rem);padding-right:0}.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link{padding-left:calc(3rem);padding-right:0}.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-inner>.menu-item>.menu-link,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-content,.menu-link-indention.menu-fit .menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-sub:not([data-popper-placement])>.menu-item>.menu-link{padding-left:calc(4rem);padding-right:0}@keyframes menu-sub-dropdown-animation-fade-in{from{opacity:0}to{opacity:1}}@keyframes menu-sub-dropdown-animation-move-up{from{margin-top:.75rem}to{margin-top:0}}@keyframes menu-sub-dropdown-animation-move-down{from{margin-bottom:.75rem}to{margin-bottom:0}}.menu-white .menu-item .menu-link{color:var(--bs-white)}.menu-white .menu-item .menu-link .menu-title{color:var(--bs-white)}.menu-white .menu-item .menu-link .menu-icon,.menu-white .menu-item .menu-link .menu-icon .svg-icon,.menu-white .menu-item .menu-link .menu-icon i{color:var(--bs-white)}.menu-white .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-white)}.menu-white .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-white);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-white%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-white%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-white);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-white%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-white%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-white .menu-item .menu-link{color:var(--bs-text-white)}.menu-title-white .menu-item .menu-link .menu-title{color:var(--bs-text-white)}.menu-icon-white .menu-item .menu-link .menu-icon,.menu-icon-white .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-white .menu-item .menu-link .menu-icon i{color:var(--bs-text-white)}.menu-bullet-white .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-white)}.menu-arrow-white .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-white);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-white%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-white%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-white);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-white%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-white%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-primary .menu-item .menu-link{color:var(--bs-primary)}.menu-primary .menu-item .menu-link .menu-title{color:var(--bs-primary)}.menu-primary .menu-item .menu-link .menu-icon,.menu-primary .menu-item .menu-link .menu-icon .svg-icon,.menu-primary .menu-item .menu-link .menu-icon i{color:var(--bs-primary)}.menu-primary .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-primary .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-primary .menu-item .menu-link{color:var(--bs-text-primary)}.menu-title-primary .menu-item .menu-link .menu-title{color:var(--bs-text-primary)}.menu-icon-primary .menu-item .menu-link .menu-icon,.menu-icon-primary .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-primary .menu-item .menu-link .menu-icon i{color:var(--bs-text-primary)}.menu-bullet-primary .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-primary)}.menu-arrow-primary .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-secondary .menu-item .menu-link{color:var(--bs-secondary)}.menu-secondary .menu-item .menu-link .menu-title{color:var(--bs-secondary)}.menu-secondary .menu-item .menu-link .menu-icon,.menu-secondary .menu-item .menu-link .menu-icon .svg-icon,.menu-secondary .menu-item .menu-link .menu-icon i{color:var(--bs-secondary)}.menu-secondary .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-secondary)}.menu-secondary .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-secondary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-secondary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-secondary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-secondary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-secondary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-secondary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-secondary .menu-item .menu-link{color:var(--bs-text-secondary)}.menu-title-secondary .menu-item .menu-link .menu-title{color:var(--bs-text-secondary)}.menu-icon-secondary .menu-item .menu-link .menu-icon,.menu-icon-secondary .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-secondary .menu-item .menu-link .menu-icon i{color:var(--bs-text-secondary)}.menu-bullet-secondary .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-secondary)}.menu-arrow-secondary .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-secondary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-secondary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-secondary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-secondary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-secondary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-secondary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-light .menu-item .menu-link{color:var(--bs-light)}.menu-light .menu-item .menu-link .menu-title{color:var(--bs-light)}.menu-light .menu-item .menu-link .menu-icon,.menu-light .menu-item .menu-link .menu-icon .svg-icon,.menu-light .menu-item .menu-link .menu-icon i{color:var(--bs-light)}.menu-light .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-light)}.menu-light .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-light);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-light%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-light%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-light);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-light%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-light%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-light .menu-item .menu-link{color:var(--bs-text-light)}.menu-title-light .menu-item .menu-link .menu-title{color:var(--bs-text-light)}.menu-icon-light .menu-item .menu-link .menu-icon,.menu-icon-light .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-light .menu-item .menu-link .menu-icon i{color:var(--bs-text-light)}.menu-bullet-light .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-light)}.menu-arrow-light .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-light);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-light%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-light%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-light);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-light%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-light%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-success .menu-item .menu-link{color:var(--bs-success)}.menu-success .menu-item .menu-link .menu-title{color:var(--bs-success)}.menu-success .menu-item .menu-link .menu-icon,.menu-success .menu-item .menu-link .menu-icon .svg-icon,.menu-success .menu-item .menu-link .menu-icon i{color:var(--bs-success)}.menu-success .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-success)}.menu-success .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-success);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-success%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-success%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-success);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-success%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-success%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-success .menu-item .menu-link{color:var(--bs-text-success)}.menu-title-success .menu-item .menu-link .menu-title{color:var(--bs-text-success)}.menu-icon-success .menu-item .menu-link .menu-icon,.menu-icon-success .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-success .menu-item .menu-link .menu-icon i{color:var(--bs-text-success)}.menu-bullet-success .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-success)}.menu-arrow-success .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-success);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-success%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-success%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-success);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-success%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-success%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-info .menu-item .menu-link{color:var(--bs-info)}.menu-info .menu-item .menu-link .menu-title{color:var(--bs-info)}.menu-info .menu-item .menu-link .menu-icon,.menu-info .menu-item .menu-link .menu-icon .svg-icon,.menu-info .menu-item .menu-link .menu-icon i{color:var(--bs-info)}.menu-info .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-info)}.menu-info .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-info);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-info%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-info%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-info);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-info%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-info%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-info .menu-item .menu-link{color:var(--bs-text-info)}.menu-title-info .menu-item .menu-link .menu-title{color:var(--bs-text-info)}.menu-icon-info .menu-item .menu-link .menu-icon,.menu-icon-info .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-info .menu-item .menu-link .menu-icon i{color:var(--bs-text-info)}.menu-bullet-info .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-info)}.menu-arrow-info .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-info);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-info%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-info%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-info);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-info%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-info%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-warning .menu-item .menu-link{color:var(--bs-warning)}.menu-warning .menu-item .menu-link .menu-title{color:var(--bs-warning)}.menu-warning .menu-item .menu-link .menu-icon,.menu-warning .menu-item .menu-link .menu-icon .svg-icon,.menu-warning .menu-item .menu-link .menu-icon i{color:var(--bs-warning)}.menu-warning .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-warning)}.menu-warning .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-warning);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-warning%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-warning%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-warning);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-warning%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-warning%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-warning .menu-item .menu-link{color:var(--bs-text-warning)}.menu-title-warning .menu-item .menu-link .menu-title{color:var(--bs-text-warning)}.menu-icon-warning .menu-item .menu-link .menu-icon,.menu-icon-warning .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-warning .menu-item .menu-link .menu-icon i{color:var(--bs-text-warning)}.menu-bullet-warning .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-warning)}.menu-arrow-warning .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-warning);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-warning%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-warning%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-warning);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-warning%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-warning%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-danger .menu-item .menu-link{color:var(--bs-danger)}.menu-danger .menu-item .menu-link .menu-title{color:var(--bs-danger)}.menu-danger .menu-item .menu-link .menu-icon,.menu-danger .menu-item .menu-link .menu-icon .svg-icon,.menu-danger .menu-item .menu-link .menu-icon i{color:var(--bs-danger)}.menu-danger .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-danger)}.menu-danger .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-danger);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-danger%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-danger%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-danger);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-danger%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-danger%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-danger .menu-item .menu-link{color:var(--bs-text-danger)}.menu-title-danger .menu-item .menu-link .menu-title{color:var(--bs-text-danger)}.menu-icon-danger .menu-item .menu-link .menu-icon,.menu-icon-danger .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-danger .menu-item .menu-link .menu-icon i{color:var(--bs-text-danger)}.menu-bullet-danger .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-danger)}.menu-arrow-danger .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-danger);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-danger%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-danger%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-danger);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-danger%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-danger%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-dark .menu-item .menu-link{color:var(--bs-dark)}.menu-dark .menu-item .menu-link .menu-title{color:var(--bs-dark)}.menu-dark .menu-item .menu-link .menu-icon,.menu-dark .menu-item .menu-link .menu-icon .svg-icon,.menu-dark .menu-item .menu-link .menu-icon i{color:var(--bs-dark)}.menu-dark .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-dark)}.menu-dark .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-dark .menu-item .menu-link{color:var(--bs-text-dark)}.menu-title-dark .menu-item .menu-link .menu-title{color:var(--bs-text-dark)}.menu-icon-dark .menu-item .menu-link .menu-icon,.menu-icon-dark .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-dark .menu-item .menu-link .menu-icon i{color:var(--bs-text-dark)}.menu-bullet-dark .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-dark)}.menu-arrow-dark .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-muted .menu-item .menu-link{color:var(--bs-muted)}.menu-muted .menu-item .menu-link .menu-title{color:var(--bs-muted)}.menu-muted .menu-item .menu-link .menu-icon,.menu-muted .menu-item .menu-link .menu-icon .svg-icon,.menu-muted .menu-item .menu-link .menu-icon i{color:var(--bs-muted)}.menu-muted .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-muted)}.menu-muted .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-muted%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-muted%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-muted%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-muted%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-muted .menu-item .menu-link{color:var(--bs-text-muted)}.menu-title-muted .menu-item .menu-link .menu-title{color:var(--bs-text-muted)}.menu-icon-muted .menu-item .menu-link .menu-icon,.menu-icon-muted .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-muted .menu-item .menu-link .menu-icon i{color:var(--bs-text-muted)}.menu-bullet-muted .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-muted)}.menu-arrow-muted .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-100 .menu-item .menu-link{color:var(--bs-gray-100)}.menu-gray-100 .menu-item .menu-link .menu-title{color:var(--bs-gray-100)}.menu-gray-100 .menu-item .menu-link .menu-icon,.menu-gray-100 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-100 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-100)}.menu-gray-100 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-100)}.menu-gray-100 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-100);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-100%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-100%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-100);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-100%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-100%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-100 .menu-item .menu-link{color:var(--bs-text-gray-100)}.menu-title-gray-100 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-100)}.menu-icon-gray-100 .menu-item .menu-link .menu-icon,.menu-icon-gray-100 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-100 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-100)}.menu-bullet-gray-100 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-100)}.menu-arrow-gray-100 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-100);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-100%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-100%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-100);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-100%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-100%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-200 .menu-item .menu-link{color:var(--bs-gray-200)}.menu-gray-200 .menu-item .menu-link .menu-title{color:var(--bs-gray-200)}.menu-gray-200 .menu-item .menu-link .menu-icon,.menu-gray-200 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-200 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-200)}.menu-gray-200 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-200)}.menu-gray-200 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-200);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-200%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-200%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-200);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-200%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-200%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-200 .menu-item .menu-link{color:var(--bs-text-gray-200)}.menu-title-gray-200 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-200)}.menu-icon-gray-200 .menu-item .menu-link .menu-icon,.menu-icon-gray-200 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-200 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-200)}.menu-bullet-gray-200 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-200)}.menu-arrow-gray-200 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-200);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-200%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-200%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-200);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-200%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-200%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-300 .menu-item .menu-link{color:var(--bs-gray-300)}.menu-gray-300 .menu-item .menu-link .menu-title{color:var(--bs-gray-300)}.menu-gray-300 .menu-item .menu-link .menu-icon,.menu-gray-300 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-300 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-300)}.menu-gray-300 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-300)}.menu-gray-300 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-300);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-300%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-300%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-300);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-300%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-300%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-300 .menu-item .menu-link{color:var(--bs-text-gray-300)}.menu-title-gray-300 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-300)}.menu-icon-gray-300 .menu-item .menu-link .menu-icon,.menu-icon-gray-300 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-300 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-300)}.menu-bullet-gray-300 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-300)}.menu-arrow-gray-300 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-300);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-300%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-300%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-300);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-300%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-300%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-400 .menu-item .menu-link{color:var(--bs-gray-400)}.menu-gray-400 .menu-item .menu-link .menu-title{color:var(--bs-gray-400)}.menu-gray-400 .menu-item .menu-link .menu-icon,.menu-gray-400 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-400 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-400)}.menu-gray-400 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-400)}.menu-gray-400 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-400);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-400%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-400%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-400);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-400%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-400%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-400 .menu-item .menu-link{color:var(--bs-text-gray-400)}.menu-title-gray-400 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-400)}.menu-icon-gray-400 .menu-item .menu-link .menu-icon,.menu-icon-gray-400 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-400 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-400)}.menu-bullet-gray-400 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-400)}.menu-arrow-gray-400 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-400);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-400%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-400%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-400);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-400%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-400%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-500 .menu-item .menu-link{color:var(--bs-gray-500)}.menu-gray-500 .menu-item .menu-link .menu-title{color:var(--bs-gray-500)}.menu-gray-500 .menu-item .menu-link .menu-icon,.menu-gray-500 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-500 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-500)}.menu-gray-500 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-500)}.menu-gray-500 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-500);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-500%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-500%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-500);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-500%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-500%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-500 .menu-item .menu-link{color:var(--bs-text-gray-500)}.menu-title-gray-500 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-500)}.menu-icon-gray-500 .menu-item .menu-link .menu-icon,.menu-icon-gray-500 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-500 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-500)}.menu-bullet-gray-500 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-500)}.menu-arrow-gray-500 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-500);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-500%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-500%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-500);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-500%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-500%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-600 .menu-item .menu-link{color:var(--bs-gray-600)}.menu-gray-600 .menu-item .menu-link .menu-title{color:var(--bs-gray-600)}.menu-gray-600 .menu-item .menu-link .menu-icon,.menu-gray-600 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-600 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-600)}.menu-gray-600 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-600)}.menu-gray-600 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-600);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-600%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-600%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-600);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-600%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-600%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-600 .menu-item .menu-link{color:var(--bs-text-gray-600)}.menu-title-gray-600 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-600)}.menu-icon-gray-600 .menu-item .menu-link .menu-icon,.menu-icon-gray-600 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-600 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-600)}.menu-bullet-gray-600 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-600)}.menu-arrow-gray-600 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-600);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-600%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-600%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-600);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-600%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-600%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-700 .menu-item .menu-link{color:var(--bs-gray-700)}.menu-gray-700 .menu-item .menu-link .menu-title{color:var(--bs-gray-700)}.menu-gray-700 .menu-item .menu-link .menu-icon,.menu-gray-700 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-700 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-700)}.menu-gray-700 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-700)}.menu-gray-700 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-700%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-700 .menu-item .menu-link{color:var(--bs-text-gray-700)}.menu-title-gray-700 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-700)}.menu-icon-gray-700 .menu-item .menu-link .menu-icon,.menu-icon-gray-700 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-700 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-700)}.menu-bullet-gray-700 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-700)}.menu-arrow-gray-700 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-700%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-700%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-700%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-700%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-800 .menu-item .menu-link{color:var(--bs-gray-800)}.menu-gray-800 .menu-item .menu-link .menu-title{color:var(--bs-gray-800)}.menu-gray-800 .menu-item .menu-link .menu-icon,.menu-gray-800 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-800 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-800)}.menu-gray-800 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-800)}.menu-gray-800 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-800);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-800%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-800%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-800);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-800%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-800%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-800 .menu-item .menu-link{color:var(--bs-text-gray-800)}.menu-title-gray-800 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-800)}.menu-icon-gray-800 .menu-item .menu-link .menu-icon,.menu-icon-gray-800 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-800 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-800)}.menu-bullet-gray-800 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-800)}.menu-arrow-gray-800 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-800);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-800%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-800%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-800);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-800%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-800%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-gray-900 .menu-item .menu-link{color:var(--bs-gray-900)}.menu-gray-900 .menu-item .menu-link .menu-title{color:var(--bs-gray-900)}.menu-gray-900 .menu-item .menu-link .menu-icon,.menu-gray-900 .menu-item .menu-link .menu-icon .svg-icon,.menu-gray-900 .menu-item .menu-link .menu-icon i{color:var(--bs-gray-900)}.menu-gray-900 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-gray-900)}.menu-gray-900 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-title-gray-900 .menu-item .menu-link{color:var(--bs-text-gray-900)}.menu-title-gray-900 .menu-item .menu-link .menu-title{color:var(--bs-text-gray-900)}.menu-icon-gray-900 .menu-item .menu-link .menu-icon,.menu-icon-gray-900 .menu-item .menu-link .menu-icon .svg-icon,.menu-icon-gray-900 .menu-item .menu-link .menu-icon i{color:var(--bs-text-gray-900)}.menu-bullet-gray-900 .menu-item .menu-link .menu-bullet .bullet{background-color:var(--bs-text-gray-900)}.menu-arrow-gray-900 .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-hover-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-hover-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-hover);color:var(--bs-menu-link-color-hover)}.menu-hover-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-hover-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-menu-link-color-hover)}.menu-hover-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-hover-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-menu-link-color-hover)}.menu-hover-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-hover-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-menu-link-color-hover)}.menu-hover-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-hover-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-hover);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-hover);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-here-bg .menu-item.here>.menu-link{transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-here);color:var(--bs-menu-link-color-here)}.menu-here-bg .menu-item.here>.menu-link .menu-title{color:var(--bs-menu-link-color-here)}.menu-here-bg .menu-item.here>.menu-link .menu-icon,.menu-here-bg .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-here-bg .menu-item.here>.menu-link .menu-icon i{color:var(--bs-menu-link-color-here)}.menu-here-bg .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-menu-link-color-here)}.menu-here-bg .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-root-here-bg>.menu-item.here>.menu-link{transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-here);color:var(--bs-menu-link-color-here)}.menu-root-here-bg>.menu-item.here>.menu-link .menu-title{color:var(--bs-menu-link-color-here)}.menu-root-here-bg>.menu-item.here>.menu-link .menu-icon,.menu-root-here-bg>.menu-item.here>.menu-link .menu-icon .svg-icon,.menu-root-here-bg>.menu-item.here>.menu-link .menu-icon i{color:var(--bs-menu-link-color-here)}.menu-root-here-bg>.menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-menu-link-color-here)}.menu-root-here-bg>.menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}@media (min-width:992px){.menu-root-here-bg-desktop>.menu-item.here>.menu-link{transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-here);color:var(--bs-menu-link-color-here)}.menu-root-here-bg-desktop>.menu-item.here>.menu-link .menu-title{color:var(--bs-menu-link-color-here)}.menu-root-here-bg-desktop>.menu-item.here>.menu-link .menu-icon,.menu-root-here-bg-desktop>.menu-item.here>.menu-link .menu-icon .svg-icon,.menu-root-here-bg-desktop>.menu-item.here>.menu-link .menu-icon i{color:var(--bs-menu-link-color-here)}.menu-root-here-bg-desktop>.menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-menu-link-color-here)}.menu-root-here-bg-desktop>.menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}}.menu-show-bg .menu-item.show>.menu-link{transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-show);color:var(--bs-menu-link-color-show)}.menu-show-bg .menu-item.show>.menu-link .menu-title{color:var(--bs-menu-link-color-show)}.menu-show-bg .menu-item.show>.menu-link .menu-icon,.menu-show-bg .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-show-bg .menu-item.show>.menu-link .menu-icon i{color:var(--bs-menu-link-color-show)}.menu-show-bg .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-menu-link-color-show)}.menu-show-bg .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-show);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-show);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-active-bg .menu-item .menu-link.active{transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-active);color:var(--bs-menu-link-color-active)}.menu-active-bg .menu-item .menu-link.active .menu-title{color:var(--bs-menu-link-color-active)}.menu-active-bg .menu-item .menu-link.active .menu-icon,.menu-active-bg .menu-item .menu-link.active .menu-icon .svg-icon,.menu-active-bg .menu-item .menu-link.active .menu-icon i{color:var(--bs-menu-link-color-active)}.menu-active-bg .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-menu-link-color-active)}.menu-active-bg .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-active);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-active);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-hover);color:var(--bs-menu-link-color-hover)}.menu-state-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-state-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-menu-link-color-hover)}.menu-state-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-state-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-menu-link-color-hover)}.menu-state-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-state-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-menu-link-color-hover)}.menu-state-bg .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-state-bg .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-hover);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-hover);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg .menu-item.here>.menu-link{transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-here);color:var(--bs-menu-link-color-here)}.menu-state-bg .menu-item.here>.menu-link .menu-title{color:var(--bs-menu-link-color-here)}.menu-state-bg .menu-item.here>.menu-link .menu-icon,.menu-state-bg .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-state-bg .menu-item.here>.menu-link .menu-icon i{color:var(--bs-menu-link-color-here)}.menu-state-bg .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-menu-link-color-here)}.menu-state-bg .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg .menu-item.show>.menu-link{transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-show);color:var(--bs-menu-link-color-show)}.menu-state-bg .menu-item.show>.menu-link .menu-title{color:var(--bs-menu-link-color-show)}.menu-state-bg .menu-item.show>.menu-link .menu-icon,.menu-state-bg .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-bg .menu-item.show>.menu-link .menu-icon i{color:var(--bs-menu-link-color-show)}.menu-state-bg .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-menu-link-color-show)}.menu-state-bg .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-show);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-show);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg .menu-item .menu-link.active{transition:color .2s ease;background-color:var(--bs-menu-link-bg-color-active);color:var(--bs-menu-link-color-active)}.menu-state-bg .menu-item .menu-link.active .menu-title{color:var(--bs-menu-link-color-active)}.menu-state-bg .menu-item .menu-link.active .menu-icon,.menu-state-bg .menu-item .menu-link.active .menu-icon .svg-icon,.menu-state-bg .menu-item .menu-link.active .menu-icon i{color:var(--bs-menu-link-color-active)}.menu-state-bg .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-menu-link-color-active)}.menu-state-bg .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-active);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-active);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-color .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-color .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;color:var(--bs-menu-link-color-hover)}.menu-state-color .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-state-color .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-menu-link-color-hover)}.menu-state-color .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-color .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-color .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-state-color .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-color .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-color .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-menu-link-color-hover)}.menu-state-color .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-state-color .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-menu-link-color-hover)}.menu-state-color .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-state-color .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-hover);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-hover);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-hover%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-color .menu-item.here>.menu-link{transition:color .2s ease;color:var(--bs-menu-link-color-here)}.menu-state-color .menu-item.here>.menu-link .menu-title{color:var(--bs-menu-link-color-here)}.menu-state-color .menu-item.here>.menu-link .menu-icon,.menu-state-color .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-state-color .menu-item.here>.menu-link .menu-icon i{color:var(--bs-menu-link-color-here)}.menu-state-color .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-menu-link-color-here)}.menu-state-color .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-here);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-here%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-color .menu-item.show>.menu-link{transition:color .2s ease;color:var(--bs-menu-link-color-show)}.menu-state-color .menu-item.show>.menu-link .menu-title{color:var(--bs-menu-link-color-show)}.menu-state-color .menu-item.show>.menu-link .menu-icon,.menu-state-color .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-color .menu-item.show>.menu-link .menu-icon i{color:var(--bs-menu-link-color-show)}.menu-state-color .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-menu-link-color-show)}.menu-state-color .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-show);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-show);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-show%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-color .menu-item .menu-link.active{transition:color .2s ease;color:var(--bs-menu-link-color-active)}.menu-state-color .menu-item .menu-link.active .menu-title{color:var(--bs-menu-link-color-active)}.menu-state-color .menu-item .menu-link.active .menu-icon,.menu-state-color .menu-item .menu-link.active .menu-icon .svg-icon,.menu-state-color .menu-item .menu-link.active .menu-icon i{color:var(--bs-menu-link-color-active)}.menu-state-color .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-menu-link-color-active)}.menu-state-color .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-active);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-menu-link-color-active);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-menu-link-color-active%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-hover-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-hover-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-hover-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-hover-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-primary-inverse)}.menu-hover-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-hover-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-primary-inverse)}.menu-hover-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-hover-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-hover-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-hover-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-show-bg-primary .menu-item.show>.menu-link{transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-show-bg-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary-inverse)}.menu-show-bg-primary .menu-item.show>.menu-link .menu-icon,.menu-show-bg-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-show-bg-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary-inverse)}.menu-show-bg-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-show-bg-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-here-bg-primary .menu-item.here>.menu-link{transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-here-bg-primary .menu-item.here>.menu-link .menu-title{color:var(--bs-primary-inverse)}.menu-here-bg-primary .menu-item.here>.menu-link .menu-icon,.menu-here-bg-primary .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-here-bg-primary .menu-item.here>.menu-link .menu-icon i{color:var(--bs-primary-inverse)}.menu-here-bg-primary .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-here-bg-primary .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-active-bg-primary .menu-item .menu-link.active{transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-active-bg-primary .menu-item .menu-link.active .menu-title{color:var(--bs-primary-inverse)}.menu-active-bg-primary .menu-item .menu-link.active .menu-icon,.menu-active-bg-primary .menu-item .menu-link.active .menu-icon .svg-icon,.menu-active-bg-primary .menu-item .menu-link.active .menu-icon i{color:var(--bs-primary-inverse)}.menu-active-bg-primary .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-active-bg-primary .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-state-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-state-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-state-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-state-bg-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-primary .menu-item.show>.menu-link{transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.show>.menu-link .menu-icon,.menu-state-bg-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-bg-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-primary .menu-item.here>.menu-link{transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.here>.menu-link .menu-title{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.here>.menu-link .menu-icon,.menu-state-bg-primary .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-state-bg-primary .menu-item.here>.menu-link .menu-icon i{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-primary .menu-item .menu-link.active{transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item .menu-link.active .menu-title{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item .menu-link.active .menu-icon,.menu-state-bg-primary .menu-item .menu-link.active .menu-icon .svg-icon,.menu-state-bg-primary .menu-item .menu-link.active .menu-icon i{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-primary .menu-item.show>.menu-link{transition:color .2s ease;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.show>.menu-link .menu-icon,.menu-state-bg-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-bg-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary-inverse)}.menu-state-bg-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary-inverse%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-show-bg-light-primary .menu-item.show>.menu-link{transition:color .2s ease;background-color:var(--bs-primary-light);color:var(--bs-primary)}.menu-show-bg-light-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary)}.menu-show-bg-light-primary .menu-item.show>.menu-link .menu-icon,.menu-show-bg-light-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-show-bg-light-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-show-bg-light-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-show-bg-light-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-here-bg-light-primary .menu-item.here>.menu-link{transition:color .2s ease;background-color:var(--bs-primary-light);color:var(--bs-primary)}.menu-here-bg-light-primary .menu-item.here>.menu-link .menu-title{color:var(--bs-primary)}.menu-here-bg-light-primary .menu-item.here>.menu-link .menu-icon,.menu-here-bg-light-primary .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-here-bg-light-primary .menu-item.here>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-here-bg-light-primary .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-here-bg-light-primary .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-hover-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-hover-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;background-color:var(--bs-primary-light);color:var(--bs-primary)}.menu-hover-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-hover-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-primary)}.menu-hover-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-hover-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-primary)}.menu-hover-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-hover-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-hover-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-hover-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-active-bg-light-primary .menu-item .menu-link.active{transition:color .2s ease;background-color:var(--bs-primary-light);color:var(--bs-primary)}.menu-active-bg-light-primary .menu-item .menu-link.active .menu-title{color:var(--bs-primary)}.menu-active-bg-light-primary .menu-item .menu-link.active .menu-icon,.menu-active-bg-light-primary .menu-item .menu-link.active .menu-icon .svg-icon,.menu-active-bg-light-primary .menu-item .menu-link.active .menu-icon i{color:var(--bs-primary)}.menu-active-bg-light-primary .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-active-bg-light-primary .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-light-primary .menu-item.show>.menu-link{transition:color .2s ease;background-color:var(--bs-primary-light);color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.show>.menu-link .menu-icon,.menu-state-bg-light-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-bg-light-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-light-primary .menu-item.here>.menu-link{transition:color .2s ease;background-color:var(--bs-primary-light);color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.here>.menu-link .menu-title{color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.here>.menu-link .menu-icon,.menu-state-bg-light-primary .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-state-bg-light-primary .menu-item.here>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;background-color:var(--bs-primary-light);color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-state-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-state-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-state-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-state-bg-light-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-bg-light-primary .menu-item .menu-link.active{transition:color .2s ease;background-color:var(--bs-primary-light);color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item .menu-link.active .menu-title{color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item .menu-link.active .menu-icon,.menu-state-bg-light-primary .menu-item .menu-link.active .menu-icon .svg-icon,.menu-state-bg-light-primary .menu-item .menu-link.active .menu-icon i{color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-bg-light-primary .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-hover-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-hover-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;color:var(--bs-primary)}.menu-hover-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-hover-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-primary)}.menu-hover-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-hover-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-primary)}.menu-hover-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-hover-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-hover-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-hover-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-show-primary .menu-item.show>.menu-link{transition:color .2s ease;color:var(--bs-primary)}.menu-show-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary)}.menu-show-primary .menu-item.show>.menu-link .menu-icon,.menu-show-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-show-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-show-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-show-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-here-primary .menu-item.here>.menu-link{transition:color .2s ease;color:var(--bs-primary)}.menu-here-primary .menu-item.here>.menu-link .menu-title{color:var(--bs-primary)}.menu-here-primary .menu-item.here>.menu-link .menu-icon,.menu-here-primary .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-here-primary .menu-item.here>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-here-primary .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-here-primary .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-active-primary .menu-item .menu-link.active{transition:color .2s ease;color:var(--bs-primary)}.menu-active-primary .menu-item .menu-link.active .menu-title{color:var(--bs-primary)}.menu-active-primary .menu-item .menu-link.active .menu-icon,.menu-active-primary .menu-item .menu-link.active .menu-icon .svg-icon,.menu-active-primary .menu-item .menu-link.active .menu-icon i{color:var(--bs-primary)}.menu-active-primary .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-active-primary .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;color:var(--bs-primary)}.menu-state-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-state-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-primary)}.menu-state-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-state-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-primary)}.menu-state-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-state-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-state-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-primary .menu-item.show>.menu-link{transition:color .2s ease;color:var(--bs-primary)}.menu-state-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary)}.menu-state-primary .menu-item.show>.menu-link .menu-icon,.menu-state-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-state-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-primary .menu-item.here>.menu-link{transition:color .2s ease;color:var(--bs-primary)}.menu-state-primary .menu-item.here>.menu-link .menu-title{color:var(--bs-primary)}.menu-state-primary .menu-item.here>.menu-link .menu-icon,.menu-state-primary .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-state-primary .menu-item.here>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-state-primary .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-primary .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-primary .menu-item .menu-link.active{transition:color .2s ease;color:var(--bs-primary)}.menu-state-primary .menu-item .menu-link.active .menu-title{color:var(--bs-primary)}.menu-state-primary .menu-item .menu-link.active .menu-icon,.menu-state-primary .menu-item .menu-link.active .menu-icon .svg-icon,.menu-state-primary .menu-item .menu-link.active .menu-icon i{color:var(--bs-primary)}.menu-state-primary .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-primary .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-dark .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-dark .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;color:var(--bs-dark)}.menu-state-dark .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-state-dark .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-dark)}.menu-state-dark .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-dark .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-dark .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-state-dark .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-dark .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-dark .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-dark)}.menu-state-dark .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-state-dark .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-dark)}.menu-state-dark .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-state-dark .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-dark .menu-item.show>.menu-link{transition:color .2s ease;color:var(--bs-dark)}.menu-state-dark .menu-item.show>.menu-link .menu-title{color:var(--bs-dark)}.menu-state-dark .menu-item.show>.menu-link .menu-icon,.menu-state-dark .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-dark .menu-item.show>.menu-link .menu-icon i{color:var(--bs-dark)}.menu-state-dark .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-dark)}.menu-state-dark .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-dark .menu-item.here>.menu-link{transition:color .2s ease;color:var(--bs-dark)}.menu-state-dark .menu-item.here>.menu-link .menu-title{color:var(--bs-dark)}.menu-state-dark .menu-item.here>.menu-link .menu-icon,.menu-state-dark .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-state-dark .menu-item.here>.menu-link .menu-icon i{color:var(--bs-dark)}.menu-state-dark .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-dark)}.menu-state-dark .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-dark .menu-item .menu-link.active{transition:color .2s ease;color:var(--bs-dark)}.menu-state-dark .menu-item .menu-link.active .menu-title{color:var(--bs-dark)}.menu-state-dark .menu-item .menu-link.active .menu-icon,.menu-state-dark .menu-item .menu-link.active .menu-icon .svg-icon,.menu-state-dark .menu-item .menu-link.active .menu-icon i{color:var(--bs-dark)}.menu-state-dark .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-dark)}.menu-state-dark .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-dark);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-dark%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-gray-900 .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-gray-900 .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-state-gray-900 .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-gray-900 .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-gray-900 .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-state-gray-900 .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-gray-900 .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-gray-900 .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-state-gray-900 .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-state-gray-900 .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-gray-900 .menu-item.show>.menu-link{transition:color .2s ease;color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.show>.menu-link .menu-title{color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.show>.menu-link .menu-icon,.menu-state-gray-900 .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-gray-900 .menu-item.show>.menu-link .menu-icon i{color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-gray-900 .menu-item.here>.menu-link{transition:color .2s ease;color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.here>.menu-link .menu-title{color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.here>.menu-link .menu-icon,.menu-state-gray-900 .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-state-gray-900 .menu-item.here>.menu-link .menu-icon i{color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-gray-900 .menu-item .menu-link.active{transition:color .2s ease;color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item .menu-link.active .menu-title{color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item .menu-link.active .menu-icon,.menu-state-gray-900 .menu-item .menu-link.active .menu-icon .svg-icon,.menu-state-gray-900 .menu-item .menu-link.active .menu-icon i{color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-gray-900)}.menu-state-gray-900 .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-900);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-gray-900%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-hover-title-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-hover-title-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;color:var(--bs-primary)}.menu-hover-title-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-hover-title-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-primary)}.menu-here-title-primary .menu-item.here>.menu-link{transition:color .2s ease;color:var(--bs-primary)}.menu-here-title-primary .menu-item.here>.menu-link .menu-title{color:var(--bs-primary)}.menu-show-title-primary .menu-item.show>.menu-link{transition:color .2s ease;color:var(--bs-primary)}.menu-show-title-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary)}.menu-active-title-primary .menu-item .menu-link.active{transition:color .2s ease;color:var(--bs-primary)}.menu-active-title-primary .menu-item .menu-link.active .menu-title{color:var(--bs-primary)}.menu-state-title-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-title-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;color:var(--bs-primary)}.menu-state-title-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.menu-state-title-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:var(--bs-primary)}.menu-state-title-primary .menu-item.here>.menu-link{transition:color .2s ease;color:var(--bs-primary)}.menu-state-title-primary .menu-item.here>.menu-link .menu-title{color:var(--bs-primary)}.menu-state-title-primary .menu-item.show>.menu-link{transition:color .2s ease;color:var(--bs-primary)}.menu-state-title-primary .menu-item.show>.menu-link .menu-title{color:var(--bs-primary)}.menu-state-title-primary .menu-item .menu-link.active{transition:color .2s ease;color:var(--bs-primary)}.menu-state-title-primary .menu-item .menu-link.active .menu-title{color:var(--bs-primary)}.menu-hover-icon-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-hover-icon-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease}.menu-hover-icon-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-icon-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-icon-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-hover-icon-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-hover-icon-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-hover-icon-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-primary)}.menu-here-icon-primary .menu-item.here>.menu-link{transition:color .2s ease}.menu-here-icon-primary .menu-item.here>.menu-link .menu-icon,.menu-here-icon-primary .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-here-icon-primary .menu-item.here>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-show-icon-primary .menu-item.show>.menu-link{transition:color .2s ease}.menu-show-icon-primary .menu-item.show>.menu-link .menu-icon,.menu-show-icon-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-show-icon-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-active-icon-primary .menu-item .menu-link.active{transition:color .2s ease}.menu-active-icon-primary .menu-item .menu-link.active .menu-icon,.menu-active-icon-primary .menu-item .menu-link.active .menu-icon .svg-icon,.menu-active-icon-primary .menu-item .menu-link.active .menu-icon i{color:var(--bs-primary)}.menu-state-icon-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-icon-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease}.menu-state-icon-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-icon-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-icon-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.menu-state-icon-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.menu-state-icon-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.menu-state-icon-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:var(--bs-primary)}.menu-state-icon-primary .menu-item.here>.menu-link{transition:color .2s ease}.menu-state-icon-primary .menu-item.here>.menu-link .menu-icon,.menu-state-icon-primary .menu-item.here>.menu-link .menu-icon .svg-icon,.menu-state-icon-primary .menu-item.here>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-state-icon-primary .menu-item.show>.menu-link{transition:color .2s ease}.menu-state-icon-primary .menu-item.show>.menu-link .menu-icon,.menu-state-icon-primary .menu-item.show>.menu-link .menu-icon .svg-icon,.menu-state-icon-primary .menu-item.show>.menu-link .menu-icon i{color:var(--bs-primary)}.menu-state-icon-primary .menu-item .menu-link.active{transition:color .2s ease}.menu-state-icon-primary .menu-item .menu-link.active .menu-icon,.menu-state-icon-primary .menu-item .menu-link.active .menu-icon .svg-icon,.menu-state-icon-primary .menu-item .menu-link.active .menu-icon i{color:var(--bs-primary)}.menu-hover-bullet-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-hover-bullet-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease}.menu-hover-bullet-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-hover-bullet-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-show-bullet-primary .menu-item.show>.menu-link{transition:color .2s ease}.menu-show-bullet-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-here-bullet-primary .menu-item.here>.menu-link{transition:color .2s ease}.menu-here-bullet-primary .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-active-bullet-primary .menu-item .menu-link.active{transition:color .2s ease}.menu-active-bullet-primary .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-bullet-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-bullet-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease}.menu-state-bullet-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.menu-state-bullet-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-bullet-primary .menu-item.here>.menu-link{transition:color .2s ease}.menu-state-bullet-primary .menu-item.here>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-bullet-primary .menu-item.show>.menu-link{transition:color .2s ease}.menu-state-bullet-primary .menu-item.show>.menu-link .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-state-bullet-primary .menu-item .menu-link.active{transition:color .2s ease}.menu-state-bullet-primary .menu-item .menu-link.active .menu-bullet .bullet{background-color:var(--bs-primary)}.menu-hover-arrow-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-hover-arrow-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease}.menu-hover-arrow-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-hover-arrow-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-here-arrow-primary .menu-item.here>.menu-link{transition:color .2s ease}.menu-here-arrow-primary .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-show-arrow-primary .menu-item.show>.menu-link{transition:color .2s ease}.menu-show-arrow-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-active-arrow-primary .menu-item .menu-link.active{transition:color .2s ease}.menu-active-arrow-primary .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-arrow-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.menu-state-arrow-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease}.menu-state-arrow-primary .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.menu-state-arrow-primary .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-arrow-primary .menu-item.here>.menu-link{transition:color .2s ease}.menu-state-arrow-primary .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-arrow-primary .menu-item.show>.menu-link{transition:color .2s ease}.menu-state-arrow-primary .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.menu-state-arrow-primary .menu-item .menu-link.active{transition:color .2s ease}.menu-state-arrow-primary .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-primary%29'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.anchor{display:flex;align-items:center}.anchor a{position:relative;display:none;align-items:center;justify-content:flex-start;height:1em;width:1.25em;margin-left:-1.25em;font-weight:500;font-size:.8em;color:var(--bs-text-muted);transition:all .2s ease-in-out}.anchor a:before{content:\"#\"}.anchor:hover a{display:flex}.anchor:hover a:hover{color:var(--bs-primary);transition:all .2s ease-in-out}.card{--bs-card-box-shadow:var(--bs-root-card-box-shadow);--bs-card-border-color:var(--bs-root-card-border-color);border:1px solid var(--bs-card-border-color)}.card .card-header{display:flex;justify-content:space-between;align-items:stretch;flex-wrap:wrap;min-height:70px;padding:0 2.25rem;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:1px solid var(--bs-card-border-color)}.card .card-header .card-title{display:flex;align-items:center;margin:.5rem;margin-left:0}.card .card-header .card-title.flex-column{align-items:flex-start;justify-content:center}.card .card-header .card-title .card-icon{margin-right:.75rem;line-height:0}.card .card-header .card-title .card-icon i{font-size:1.25rem;color:var(--bs-gray-600);line-height:0}.card .card-header .card-title .card-icon i:after,.card .card-header .card-title .card-icon i:before{line-height:0}.card .card-header .card-title .card-icon .svg-icon{color:var(--bs-gray-600)}.card .card-header .card-title .card-icon .svg-icon svg{height:24px;width:24px}.card .card-header .card-title,.card .card-header .card-title .card-label{font-weight:500;font-size:1.275rem;color:var(--bs-text-gray-900)}.card .card-header .card-title .card-label{margin:0 .75rem 0 0;flex-wrap:wrap}.card .card-header .card-title .small,.card .card-header .card-title small{color:var(--bs-text-muted);font-size:1rem}.card .card-header .card-title .h1,.card .card-header .card-title .h2,.card .card-header .card-title .h3,.card .card-header .card-title .h4,.card .card-header .card-title .h5,.card .card-header .card-title .h6,.card .card-header .card-title h1,.card .card-header .card-title h2,.card .card-header .card-title h3,.card .card-header .card-title h4,.card .card-header .card-title h5,.card .card-header .card-title h6{margin-bottom:0}.card .card-header .card-toolbar{display:flex;align-items:center;margin:.5rem 0;flex-wrap:wrap}.card .card-body{padding:2rem 2.25rem;color:var(--bs-card-color)}.card .card-footer{padding:2rem 2.25rem;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:1px solid var(--bs-card-border-color)}.card .card-scroll{position:relative;overflow:auto}.card.card-px-0 .card-body,.card.card-px-0 .card-footer,.card.card-px-0 .card-header{padding-left:0;padding-right:0}.card.card-py-0 .card-body,.card.card-py-0 .card-footer,.card.card-py-0 .card-header{padding-top:0;padding-bottom:0}.card.card-p-0 .card-body,.card.card-p-0 .card-footer,.card.card-p-0 .card-header{padding:0}.card.card-dashed{box-shadow:none;border:1px dashed var(--bs-border-dashed-color)}.card.card-dashed>.card-header{border-bottom:1px dashed var(--bs-border-dashed-color)}.card.card-dashed>.card-footer{border-top:1px dashed var(--bs-border-dashed-color)}.card.card-bordered{box-shadow:none;border:1px solid #f1f1f4}.card.card-flush>.card-header{border-bottom:0!important}.card.card-flush>.card-footer{border-top:0!important}.card.card-shadow{box-shadow:var(--bs-card-box-shadow);border:0}.card.card-reset{border:0!important;box-shadow:none!important;background-color:transparent!important}.card.card-reset>.card-header{border-bottom:0!important}.card.card-reset>.card-footer{border-top:0!important}.card.card-borderless{border:0!important}.card.card-borderless>.card-header{border-bottom:0!important}.card.card-borderless>.card-footer{border-top:0!important}.card.card-border-0{border:0!important}.card.card-stretch{height:calc(100% - var(--bs-gutter-y))}.card.card-stretch-75{height:calc(75% - var(--bs-gutter-y))}.card.card-stretch-50{height:calc(50% - var(--bs-gutter-y))}.card.card-stretch-33{height:calc(33.333% - var(--bs-gutter-y))}.card.card-stretch-25{height:calc(25% - var(--bs-gutter-y))}.card .card-header-stretch{padding-top:0!important;padding-bottom:0!important;align-items:stretch}.card .card-header-stretch .card-toolbar{margin:0;align-items:stretch}@media (min-width:576px){.card.card-sm-stretch{height:calc(100% - var(--bs-gutter-y))}.card.card-sm-stretch-75{height:calc(75% - var(--bs-gutter-y))}.card.card-sm-stretch-50{height:calc(50% - var(--bs-gutter-y))}.card.card-sm-stretch-33{height:calc(33.333% - var(--bs-gutter-y))}.card.card-sm-stretch-25{height:calc(25% - var(--bs-gutter-y))}.card .card-header-sm-stretch{padding-top:0!important;padding-bottom:0!important;align-items:stretch}.card .card-header-sm-stretch .card-toolbar{margin:0;align-items:stretch}}@media (min-width:768px){.card.card-md-stretch{height:calc(100% - var(--bs-gutter-y))}.card.card-md-stretch-75{height:calc(75% - var(--bs-gutter-y))}.card.card-md-stretch-50{height:calc(50% - var(--bs-gutter-y))}.card.card-md-stretch-33{height:calc(33.333% - var(--bs-gutter-y))}.card.card-md-stretch-25{height:calc(25% - var(--bs-gutter-y))}.card .card-header-md-stretch{padding-top:0!important;padding-bottom:0!important;align-items:stretch}.card .card-header-md-stretch .card-toolbar{margin:0;align-items:stretch}}@media (min-width:992px){.card.card-lg-stretch{height:calc(100% - var(--bs-gutter-y))}.card.card-lg-stretch-75{height:calc(75% - var(--bs-gutter-y))}.card.card-lg-stretch-50{height:calc(50% - var(--bs-gutter-y))}.card.card-lg-stretch-33{height:calc(33.333% - var(--bs-gutter-y))}.card.card-lg-stretch-25{height:calc(25% - var(--bs-gutter-y))}.card .card-header-lg-stretch{padding-top:0!important;padding-bottom:0!important;align-items:stretch}.card .card-header-lg-stretch .card-toolbar{margin:0;align-items:stretch}}@media (min-width:1200px){.card.card-xl-stretch{height:calc(100% - var(--bs-gutter-y))}.card.card-xl-stretch-75{height:calc(75% - var(--bs-gutter-y))}.card.card-xl-stretch-50{height:calc(50% - var(--bs-gutter-y))}.card.card-xl-stretch-33{height:calc(33.333% - var(--bs-gutter-y))}.card.card-xl-stretch-25{height:calc(25% - var(--bs-gutter-y))}.card .card-header-xl-stretch{padding-top:0!important;padding-bottom:0!important;align-items:stretch}.card .card-header-xl-stretch .card-toolbar{margin:0;align-items:stretch}}@media (min-width:1400px){.card.card-xxl-stretch{height:calc(100% - var(--bs-gutter-y))}.card.card-xxl-stretch-75{height:calc(75% - var(--bs-gutter-y))}.card.card-xxl-stretch-50{height:calc(50% - var(--bs-gutter-y))}.card.card-xxl-stretch-33{height:calc(33.333% - var(--bs-gutter-y))}.card.card-xxl-stretch-25{height:calc(25% - var(--bs-gutter-y))}.card .card-header-xxl-stretch{padding-top:0!important;padding-bottom:0!important;align-items:stretch}.card .card-header-xxl-stretch .card-toolbar{margin:0;align-items:stretch}}.card-p{padding:2rem 2.25rem!important}.card-border{border:1px solid var(--bs-root-card-border-color)!important}.card-px{padding-left:2.25rem!important;padding-right:2.25rem!important}.card-shadow{box-shadow:var(--bs-card-box-shadow)}.card-py{padding-top:2rem!important;padding-bottom:2rem!important}.card-rounded{border-radius:.625rem}.card-rounded-start{border-top-left-radius:.625rem;border-bottom-left-radius:.625rem}.card-rounded-end{border-top-right-radius:.625rem;border-bottom-right-radius:.625rem}.card-rounded-top{border-top-left-radius:.625rem;border-top-right-radius:.625rem}.card-rounded-bottom{border-bottom-left-radius:.625rem;border-bottom-right-radius:.625rem}@media (max-width:767.98px){.card>.card-header:not(.flex-nowrap){padding-top:.5rem;padding-bottom:.5rem}}[data-bs-theme=dark] .card{--bs-card-box-shadow:none}.breadcrumb{display:flex;align-items:center;background-color:transparent;padding:0;margin:0}.breadcrumb .breadcrumb-item{display:flex;align-items:center;padding-left:.5rem}.breadcrumb .breadcrumb-item:last-child{padding-right:0}.breadcrumb .breadcrumb-item:after{padding-left:.5rem;content:\"/\"}.breadcrumb .breadcrumb-item:before{display:none}.breadcrumb .breadcrumb-item:first-child{padding-left:0}.breadcrumb .breadcrumb-item:last-child:after{display:none}.breadcrumb-line .breadcrumb-item:after{content:\"-\"}.breadcrumb-dot .breadcrumb-item:after{content:\"•\"}.breadcrumb-separatorless .breadcrumb-item:after{display:none}.btn{--bs-btn-color:var(--bs-body-color);outline:0!important}.btn:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg):not(.shadow-xs){box-shadow:none}.btn:not(.btn-outline):not(.btn-dashed):not(.btn-bordered):not(.border-hover):not(.border-active):not(.btn-flush):not(.btn-icon):not(.btn-hover-outline){border:0;padding:calc(.775rem + 1px) calc(1.5rem + 1px)}.btn-group-lg>.btn:not(.btn-outline):not(.btn-dashed):not(.btn-bordered):not(.border-hover):not(.border-active):not(.btn-flush):not(.btn-icon):not(.btn-hover-outline),.btn:not(.btn-outline):not(.btn-dashed):not(.btn-bordered):not(.border-hover):not(.border-active):not(.btn-flush):not(.btn-icon):not(.btn-hover-outline).btn-lg{padding:calc(1rem + 1px) calc(1.75rem + 1px)}.btn-group-sm>.btn:not(.btn-outline):not(.btn-dashed):not(.btn-bordered):not(.border-hover):not(.border-active):not(.btn-flush):not(.btn-icon):not(.btn-hover-outline),.btn:not(.btn-outline):not(.btn-dashed):not(.btn-bordered):not(.border-hover):not(.border-active):not(.btn-flush):not(.btn-icon):not(.btn-hover-outline).btn-sm{padding:calc(.55rem + 1px) calc(1rem + 1px)}.btn.btn-link{border:0;border-radius:0;padding-left:0!important;padding-right:0!important;text-decoration:none;font-weight:500}.btn.btn-outline:not(.btn-outline-dashed){border:1px solid var(--bs-gray-300)}.btn.btn-outline-dashed{border:1px dashed var(--bs-gray-300)}.btn.btn-flush{appearance:none;box-shadow:none;border-radius:0;border:none;cursor:pointer;background-color:transparent;outline:0!important;margin:0;padding:0}.btn.btn-flex{display:inline-flex;align-items:center}.btn.btn-trim-start{justify-content:flex-start!important;padding-left:0!important}.btn.btn-trim-end{justify-content:flex-end!important;padding-right:0!important}.btn-reset{background-color:transparent;border:0;box-shadow:none;user-select:none;outline:0}.btn>i{display:inline-flex;font-size:1rem;padding-right:.35rem;vertical-align:middle}.btn.btn-icon{display:inline-flex;align-items:center;justify-content:center;padding:0;height:calc(1.5em + 1.55rem + 2px);width:calc(1.5em + 1.55rem + 2px);line-height:1}.btn.btn-icon i{padding-right:0}.btn.btn-icon:not(.btn-outline):not(.btn-dashed):not(.border-hover):not(.border-active):not(.btn-flush){border:0}.btn-group-sm>.btn.btn-icon,.btn.btn-icon.btn-sm{height:calc(1.5em + 1.1rem + 2px);width:calc(1.5em + 1.1rem + 2px)}.btn-group-lg>.btn.btn-icon,.btn.btn-icon.btn-lg{height:calc(1.5em + 2rem + 2px);width:calc(1.5em + 2rem + 2px)}.btn.btn-icon.btn-circle{border-radius:50%}.btn.btn-outline.btn-outline-dashed{border-width:1px;border-style:dashed}.btn-check:active+.btn.btn-outline.btn-outline-dashed,.btn-check:checked+.btn.btn-outline.btn-outline-dashed,.btn.btn-outline.btn-outline-dashed.active,.btn.btn-outline.btn-outline-dashed.show,.btn.btn-outline.btn-outline-dashed:active:not(.btn-active),.btn.btn-outline.btn-outline-dashed:focus:not(.btn-active),.btn.btn-outline.btn-outline-dashed:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-dashed{border-color:var(--bs-primary)}.btn.btn-hover-outline{border-width:1px;border-style:solid}.btn-check:active+.btn.btn-hover-outline,.btn-check:checked+.btn.btn-hover-outline,.btn.btn-hover-outline.active,.btn.btn-hover-outline.show,.btn.btn-hover-outline:active:not(.btn-active),.btn.btn-hover-outline:focus:not(.btn-active),.btn.btn-hover-outline:hover:not(.btn-active),.show>.btn.btn-hover-outline{border-color:var(--bs-gray-300)}.btn.btn-light{color:var(--bs-light-inverse);border-color:var(--bs-light);background-color:var(--bs-light)}.btn.btn-light .svg-icon,.btn.btn-light i{color:var(--bs-light-inverse)}.btn.btn-light.dropdown-toggle:after{color:var(--bs-light-inverse)}.btn-check:active+.btn.btn-light,.btn-check:checked+.btn.btn-light,.btn.btn-light.active,.btn.btn-light.show,.btn.btn-light:active:not(.btn-active),.btn.btn-light:focus:not(.btn-active),.btn.btn-light:hover:not(.btn-active),.show>.btn.btn-light{color:var(--bs-light-inverse);border-color:var(--bs-light-active);background-color:var(--bs-light-active)!important}.btn-check:active+.btn.btn-light .svg-icon,.btn-check:active+.btn.btn-light i,.btn-check:checked+.btn.btn-light .svg-icon,.btn-check:checked+.btn.btn-light i,.btn.btn-light.active .svg-icon,.btn.btn-light.active i,.btn.btn-light.show .svg-icon,.btn.btn-light.show i,.btn.btn-light:active:not(.btn-active) .svg-icon,.btn.btn-light:active:not(.btn-active) i,.btn.btn-light:focus:not(.btn-active) .svg-icon,.btn.btn-light:focus:not(.btn-active) i,.btn.btn-light:hover:not(.btn-active) .svg-icon,.btn.btn-light:hover:not(.btn-active) i,.show>.btn.btn-light .svg-icon,.show>.btn.btn-light i{color:var(--bs-light-inverse)}.btn-check:active+.btn.btn-light.dropdown-toggle:after,.btn-check:checked+.btn.btn-light.dropdown-toggle:after,.btn.btn-light.active.dropdown-toggle:after,.btn.btn-light.show.dropdown-toggle:after,.btn.btn-light:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light.dropdown-toggle:after{color:var(--bs-light-inverse)}.btn.btn-light-light{color:var(--bs-light);border-color:var(--bs-light-light);background-color:var(--bs-light-light)}.btn.btn-light-light .svg-icon,.btn.btn-light-light i{color:var(--bs-light)}.btn.btn-light-light.dropdown-toggle:after{color:var(--bs-light)}.btn-check:active+.btn.btn-light-light,.btn-check:checked+.btn.btn-light-light,.btn.btn-light-light.active,.btn.btn-light-light.show,.btn.btn-light-light:active:not(.btn-active),.btn.btn-light-light:focus:not(.btn-active),.btn.btn-light-light:hover:not(.btn-active),.show>.btn.btn-light-light{color:var(--bs-light-inverse);border-color:var(--bs-light);background-color:var(--bs-light)!important}.btn-check:active+.btn.btn-light-light .svg-icon,.btn-check:active+.btn.btn-light-light i,.btn-check:checked+.btn.btn-light-light .svg-icon,.btn-check:checked+.btn.btn-light-light i,.btn.btn-light-light.active .svg-icon,.btn.btn-light-light.active i,.btn.btn-light-light.show .svg-icon,.btn.btn-light-light.show i,.btn.btn-light-light:active:not(.btn-active) .svg-icon,.btn.btn-light-light:active:not(.btn-active) i,.btn.btn-light-light:focus:not(.btn-active) .svg-icon,.btn.btn-light-light:focus:not(.btn-active) i,.btn.btn-light-light:hover:not(.btn-active) .svg-icon,.btn.btn-light-light:hover:not(.btn-active) i,.show>.btn.btn-light-light .svg-icon,.show>.btn.btn-light-light i{color:var(--bs-light-inverse)}.btn-check:active+.btn.btn-light-light.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-light.dropdown-toggle:after,.btn.btn-light-light.active.dropdown-toggle:after,.btn.btn-light-light.show.dropdown-toggle:after,.btn.btn-light-light:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-light:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-light:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-light.dropdown-toggle:after{color:var(--bs-light-inverse)}.btn.btn-bg-light{border-color:var(--bs-light);background-color:var(--bs-light)}.btn-check:active+.btn.btn-active-light,.btn-check:checked+.btn.btn-active-light,.btn.btn-active-light.active,.btn.btn-active-light.show,.btn.btn-active-light:active:not(.btn-active),.btn.btn-active-light:focus:not(.btn-active),.btn.btn-active-light:hover:not(.btn-active),.show>.btn.btn-active-light{color:var(--bs-light-inverse);border-color:var(--bs-light);background-color:var(--bs-light)!important}.btn-check:active+.btn.btn-active-light .svg-icon,.btn-check:active+.btn.btn-active-light i,.btn-check:checked+.btn.btn-active-light .svg-icon,.btn-check:checked+.btn.btn-active-light i,.btn.btn-active-light.active .svg-icon,.btn.btn-active-light.active i,.btn.btn-active-light.show .svg-icon,.btn.btn-active-light.show i,.btn.btn-active-light:active:not(.btn-active) .svg-icon,.btn.btn-active-light:active:not(.btn-active) i,.btn.btn-active-light:focus:not(.btn-active) .svg-icon,.btn.btn-active-light:focus:not(.btn-active) i,.btn.btn-active-light:hover:not(.btn-active) .svg-icon,.btn.btn-active-light:hover:not(.btn-active) i,.show>.btn.btn-active-light .svg-icon,.show>.btn.btn-active-light i{color:var(--bs-light-inverse)}.btn-check:active+.btn.btn-active-light.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light.dropdown-toggle:after,.btn.btn-active-light.active.dropdown-toggle:after,.btn.btn-active-light.show.dropdown-toggle:after,.btn.btn-active-light:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light.dropdown-toggle:after{color:var(--bs-light-inverse)}.btn-check:active+.btn.btn-active-light-light,.btn-check:checked+.btn.btn-active-light-light,.btn.btn-active-light-light.active,.btn.btn-active-light-light.show,.btn.btn-active-light-light:active:not(.btn-active),.btn.btn-active-light-light:focus:not(.btn-active),.btn.btn-active-light-light:hover:not(.btn-active),.show>.btn.btn-active-light-light{color:var(--bs-light);border-color:var(--bs-light-light);background-color:var(--bs-light-light)!important}.btn-check:active+.btn.btn-active-light-light .svg-icon,.btn-check:active+.btn.btn-active-light-light i,.btn-check:checked+.btn.btn-active-light-light .svg-icon,.btn-check:checked+.btn.btn-active-light-light i,.btn.btn-active-light-light.active .svg-icon,.btn.btn-active-light-light.active i,.btn.btn-active-light-light.show .svg-icon,.btn.btn-active-light-light.show i,.btn.btn-active-light-light:active:not(.btn-active) .svg-icon,.btn.btn-active-light-light:active:not(.btn-active) i,.btn.btn-active-light-light:focus:not(.btn-active) .svg-icon,.btn.btn-active-light-light:focus:not(.btn-active) i,.btn.btn-active-light-light:hover:not(.btn-active) .svg-icon,.btn.btn-active-light-light:hover:not(.btn-active) i,.show>.btn.btn-active-light-light .svg-icon,.show>.btn.btn-active-light-light i{color:var(--bs-light)}.btn-check:active+.btn.btn-active-light-light.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light-light.dropdown-toggle:after,.btn.btn-active-light-light.active.dropdown-toggle:after,.btn.btn-active-light-light.show.dropdown-toggle:after,.btn.btn-active-light-light:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-light:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-light:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light-light.dropdown-toggle:after{color:var(--bs-light)}.btn.btn-outline.btn-outline-light{color:var(--bs-light);border-color:var(--bs-light);background-color:transparent}.btn.btn-outline.btn-outline-light .svg-icon,.btn.btn-outline.btn-outline-light i{color:var(--bs-light)}.btn.btn-outline.btn-outline-light.dropdown-toggle:after{color:var(--bs-light)}.btn-check:active+.btn.btn-outline.btn-outline-light,.btn-check:checked+.btn.btn-outline.btn-outline-light,.btn.btn-outline.btn-outline-light.active,.btn.btn-outline.btn-outline-light.show,.btn.btn-outline.btn-outline-light:active:not(.btn-active),.btn.btn-outline.btn-outline-light:focus:not(.btn-active),.btn.btn-outline.btn-outline-light:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-light{color:var(--bs-light-active);border-color:var(--bs-light);background-color:var(--bs-light-light)!important}.btn-check:active+.btn.btn-outline.btn-outline-light .svg-icon,.btn-check:active+.btn.btn-outline.btn-outline-light i,.btn-check:checked+.btn.btn-outline.btn-outline-light .svg-icon,.btn-check:checked+.btn.btn-outline.btn-outline-light i,.btn.btn-outline.btn-outline-light.active .svg-icon,.btn.btn-outline.btn-outline-light.active i,.btn.btn-outline.btn-outline-light.show .svg-icon,.btn.btn-outline.btn-outline-light.show i,.btn.btn-outline.btn-outline-light:active:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-light:active:not(.btn-active) i,.btn.btn-outline.btn-outline-light:focus:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-light:focus:not(.btn-active) i,.btn.btn-outline.btn-outline-light:hover:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-light:hover:not(.btn-active) i,.show>.btn.btn-outline.btn-outline-light .svg-icon,.show>.btn.btn-outline.btn-outline-light i{color:var(--bs-light-active)}.btn-check:active+.btn.btn-outline.btn-outline-light.dropdown-toggle:after,.btn-check:checked+.btn.btn-outline.btn-outline-light.dropdown-toggle:after,.btn.btn-outline.btn-outline-light.active.dropdown-toggle:after,.btn.btn-outline.btn-outline-light.show.dropdown-toggle:after,.btn.btn-outline.btn-outline-light:active:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-light:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-light:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-outline.btn-outline-light.dropdown-toggle:after{color:var(--bs-light-active)}.btn.btn-primary{color:var(--bs-primary-inverse);border-color:var(--bs-primary);background-color:var(--bs-primary)}.btn.btn-primary .svg-icon,.btn.btn-primary i{color:var(--bs-primary-inverse)}.btn.btn-primary.dropdown-toggle:after{color:var(--bs-primary-inverse)}.btn-check:active+.btn.btn-primary,.btn-check:checked+.btn.btn-primary,.btn.btn-primary.active,.btn.btn-primary.show,.btn.btn-primary:active:not(.btn-active),.btn.btn-primary:focus:not(.btn-active),.btn.btn-primary:hover:not(.btn-active),.show>.btn.btn-primary{color:var(--bs-primary-inverse);border-color:var(--bs-primary-active);background-color:var(--bs-primary-active)!important}.btn-check:active+.btn.btn-primary .svg-icon,.btn-check:active+.btn.btn-primary i,.btn-check:checked+.btn.btn-primary .svg-icon,.btn-check:checked+.btn.btn-primary i,.btn.btn-primary.active .svg-icon,.btn.btn-primary.active i,.btn.btn-primary.show .svg-icon,.btn.btn-primary.show i,.btn.btn-primary:active:not(.btn-active) .svg-icon,.btn.btn-primary:active:not(.btn-active) i,.btn.btn-primary:focus:not(.btn-active) .svg-icon,.btn.btn-primary:focus:not(.btn-active) i,.btn.btn-primary:hover:not(.btn-active) .svg-icon,.btn.btn-primary:hover:not(.btn-active) i,.show>.btn.btn-primary .svg-icon,.show>.btn.btn-primary i{color:var(--bs-primary-inverse)}.btn-check:active+.btn.btn-primary.dropdown-toggle:after,.btn-check:checked+.btn.btn-primary.dropdown-toggle:after,.btn.btn-primary.active.dropdown-toggle:after,.btn.btn-primary.show.dropdown-toggle:after,.btn.btn-primary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-primary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-primary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-primary.dropdown-toggle:after{color:var(--bs-primary-inverse)}.btn.btn-light-primary{color:var(--bs-primary);border-color:var(--bs-primary-light);background-color:var(--bs-primary-light)}.btn.btn-light-primary .svg-icon,.btn.btn-light-primary i{color:var(--bs-primary)}.btn.btn-light-primary.dropdown-toggle:after{color:var(--bs-primary)}.btn-check:active+.btn.btn-light-primary,.btn-check:checked+.btn.btn-light-primary,.btn.btn-light-primary.active,.btn.btn-light-primary.show,.btn.btn-light-primary:active:not(.btn-active),.btn.btn-light-primary:focus:not(.btn-active),.btn.btn-light-primary:hover:not(.btn-active),.show>.btn.btn-light-primary{color:var(--bs-primary-inverse);border-color:var(--bs-primary);background-color:var(--bs-primary)!important}.btn-check:active+.btn.btn-light-primary .svg-icon,.btn-check:active+.btn.btn-light-primary i,.btn-check:checked+.btn.btn-light-primary .svg-icon,.btn-check:checked+.btn.btn-light-primary i,.btn.btn-light-primary.active .svg-icon,.btn.btn-light-primary.active i,.btn.btn-light-primary.show .svg-icon,.btn.btn-light-primary.show i,.btn.btn-light-primary:active:not(.btn-active) .svg-icon,.btn.btn-light-primary:active:not(.btn-active) i,.btn.btn-light-primary:focus:not(.btn-active) .svg-icon,.btn.btn-light-primary:focus:not(.btn-active) i,.btn.btn-light-primary:hover:not(.btn-active) .svg-icon,.btn.btn-light-primary:hover:not(.btn-active) i,.show>.btn.btn-light-primary .svg-icon,.show>.btn.btn-light-primary i{color:var(--bs-primary-inverse)}.btn-check:active+.btn.btn-light-primary.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-primary.dropdown-toggle:after,.btn.btn-light-primary.active.dropdown-toggle:after,.btn.btn-light-primary.show.dropdown-toggle:after,.btn.btn-light-primary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-primary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-primary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-primary.dropdown-toggle:after{color:var(--bs-primary-inverse)}.btn.btn-bg-primary{border-color:var(--bs-primary);background-color:var(--bs-primary)}.btn-check:active+.btn.btn-active-primary,.btn-check:checked+.btn.btn-active-primary,.btn.btn-active-primary.active,.btn.btn-active-primary.show,.btn.btn-active-primary:active:not(.btn-active),.btn.btn-active-primary:focus:not(.btn-active),.btn.btn-active-primary:hover:not(.btn-active),.show>.btn.btn-active-primary{color:var(--bs-primary-inverse);border-color:var(--bs-primary);background-color:var(--bs-primary)!important}.btn-check:active+.btn.btn-active-primary .svg-icon,.btn-check:active+.btn.btn-active-primary i,.btn-check:checked+.btn.btn-active-primary .svg-icon,.btn-check:checked+.btn.btn-active-primary i,.btn.btn-active-primary.active .svg-icon,.btn.btn-active-primary.active i,.btn.btn-active-primary.show .svg-icon,.btn.btn-active-primary.show i,.btn.btn-active-primary:active:not(.btn-active) .svg-icon,.btn.btn-active-primary:active:not(.btn-active) i,.btn.btn-active-primary:focus:not(.btn-active) .svg-icon,.btn.btn-active-primary:focus:not(.btn-active) i,.btn.btn-active-primary:hover:not(.btn-active) .svg-icon,.btn.btn-active-primary:hover:not(.btn-active) i,.show>.btn.btn-active-primary .svg-icon,.show>.btn.btn-active-primary i{color:var(--bs-primary-inverse)}.btn-check:active+.btn.btn-active-primary.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-primary.dropdown-toggle:after,.btn.btn-active-primary.active.dropdown-toggle:after,.btn.btn-active-primary.show.dropdown-toggle:after,.btn.btn-active-primary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-primary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-primary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-primary.dropdown-toggle:after{color:var(--bs-primary-inverse)}.btn-check:active+.btn.btn-active-light-primary,.btn-check:checked+.btn.btn-active-light-primary,.btn.btn-active-light-primary.active,.btn.btn-active-light-primary.show,.btn.btn-active-light-primary:active:not(.btn-active),.btn.btn-active-light-primary:focus:not(.btn-active),.btn.btn-active-light-primary:hover:not(.btn-active),.show>.btn.btn-active-light-primary{color:var(--bs-primary);border-color:var(--bs-primary-light);background-color:var(--bs-primary-light)!important}.btn-check:active+.btn.btn-active-light-primary .svg-icon,.btn-check:active+.btn.btn-active-light-primary i,.btn-check:checked+.btn.btn-active-light-primary .svg-icon,.btn-check:checked+.btn.btn-active-light-primary i,.btn.btn-active-light-primary.active .svg-icon,.btn.btn-active-light-primary.active i,.btn.btn-active-light-primary.show .svg-icon,.btn.btn-active-light-primary.show i,.btn.btn-active-light-primary:active:not(.btn-active) .svg-icon,.btn.btn-active-light-primary:active:not(.btn-active) i,.btn.btn-active-light-primary:focus:not(.btn-active) .svg-icon,.btn.btn-active-light-primary:focus:not(.btn-active) i,.btn.btn-active-light-primary:hover:not(.btn-active) .svg-icon,.btn.btn-active-light-primary:hover:not(.btn-active) i,.show>.btn.btn-active-light-primary .svg-icon,.show>.btn.btn-active-light-primary i{color:var(--bs-primary)}.btn-check:active+.btn.btn-active-light-primary.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light-primary.dropdown-toggle:after,.btn.btn-active-light-primary.active.dropdown-toggle:after,.btn.btn-active-light-primary.show.dropdown-toggle:after,.btn.btn-active-light-primary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-primary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-primary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light-primary.dropdown-toggle:after{color:var(--bs-primary)}.btn.btn-outline.btn-outline-primary{color:var(--bs-primary);border-color:var(--bs-primary);background-color:transparent}.btn.btn-outline.btn-outline-primary .svg-icon,.btn.btn-outline.btn-outline-primary i{color:var(--bs-primary)}.btn.btn-outline.btn-outline-primary.dropdown-toggle:after{color:var(--bs-primary)}.btn-check:active+.btn.btn-outline.btn-outline-primary,.btn-check:checked+.btn.btn-outline.btn-outline-primary,.btn.btn-outline.btn-outline-primary.active,.btn.btn-outline.btn-outline-primary.show,.btn.btn-outline.btn-outline-primary:active:not(.btn-active),.btn.btn-outline.btn-outline-primary:focus:not(.btn-active),.btn.btn-outline.btn-outline-primary:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-primary{color:var(--bs-primary-active);border-color:var(--bs-primary);background-color:var(--bs-primary-light)!important}.btn-check:active+.btn.btn-outline.btn-outline-primary .svg-icon,.btn-check:active+.btn.btn-outline.btn-outline-primary i,.btn-check:checked+.btn.btn-outline.btn-outline-primary .svg-icon,.btn-check:checked+.btn.btn-outline.btn-outline-primary i,.btn.btn-outline.btn-outline-primary.active .svg-icon,.btn.btn-outline.btn-outline-primary.active i,.btn.btn-outline.btn-outline-primary.show .svg-icon,.btn.btn-outline.btn-outline-primary.show i,.btn.btn-outline.btn-outline-primary:active:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-primary:active:not(.btn-active) i,.btn.btn-outline.btn-outline-primary:focus:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-primary:focus:not(.btn-active) i,.btn.btn-outline.btn-outline-primary:hover:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-primary:hover:not(.btn-active) i,.show>.btn.btn-outline.btn-outline-primary .svg-icon,.show>.btn.btn-outline.btn-outline-primary i{color:var(--bs-primary-active)}.btn-check:active+.btn.btn-outline.btn-outline-primary.dropdown-toggle:after,.btn-check:checked+.btn.btn-outline.btn-outline-primary.dropdown-toggle:after,.btn.btn-outline.btn-outline-primary.active.dropdown-toggle:after,.btn.btn-outline.btn-outline-primary.show.dropdown-toggle:after,.btn.btn-outline.btn-outline-primary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-primary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-primary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-outline.btn-outline-primary.dropdown-toggle:after{color:var(--bs-primary-active)}.btn.btn-secondary{color:var(--bs-secondary-inverse);border-color:var(--bs-secondary);background-color:var(--bs-secondary)}.btn.btn-secondary .svg-icon,.btn.btn-secondary i{color:var(--bs-secondary-inverse)}.btn.btn-secondary.dropdown-toggle:after{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-secondary,.btn-check:checked+.btn.btn-secondary,.btn.btn-secondary.active,.btn.btn-secondary.show,.btn.btn-secondary:active:not(.btn-active),.btn.btn-secondary:focus:not(.btn-active),.btn.btn-secondary:hover:not(.btn-active),.show>.btn.btn-secondary{color:var(--bs-secondary-inverse);border-color:var(--bs-secondary-active);background-color:var(--bs-secondary-active)!important}.btn-check:active+.btn.btn-secondary .svg-icon,.btn-check:active+.btn.btn-secondary i,.btn-check:checked+.btn.btn-secondary .svg-icon,.btn-check:checked+.btn.btn-secondary i,.btn.btn-secondary.active .svg-icon,.btn.btn-secondary.active i,.btn.btn-secondary.show .svg-icon,.btn.btn-secondary.show i,.btn.btn-secondary:active:not(.btn-active) .svg-icon,.btn.btn-secondary:active:not(.btn-active) i,.btn.btn-secondary:focus:not(.btn-active) .svg-icon,.btn.btn-secondary:focus:not(.btn-active) i,.btn.btn-secondary:hover:not(.btn-active) .svg-icon,.btn.btn-secondary:hover:not(.btn-active) i,.show>.btn.btn-secondary .svg-icon,.show>.btn.btn-secondary i{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-secondary.dropdown-toggle:after,.btn-check:checked+.btn.btn-secondary.dropdown-toggle:after,.btn.btn-secondary.active.dropdown-toggle:after,.btn.btn-secondary.show.dropdown-toggle:after,.btn.btn-secondary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-secondary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-secondary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-secondary.dropdown-toggle:after{color:var(--bs-secondary-inverse)}.btn.btn-light-secondary{color:var(--bs-secondary-inverse);border-color:var(--bs-secondary-light);background-color:var(--bs-secondary-light)}.btn.btn-light-secondary .svg-icon,.btn.btn-light-secondary i{color:var(--bs-secondary-inverse)}.btn.btn-light-secondary.dropdown-toggle:after{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-light-secondary,.btn-check:checked+.btn.btn-light-secondary,.btn.btn-light-secondary.active,.btn.btn-light-secondary.show,.btn.btn-light-secondary:active:not(.btn-active),.btn.btn-light-secondary:focus:not(.btn-active),.btn.btn-light-secondary:hover:not(.btn-active),.show>.btn.btn-light-secondary{color:var(--bs-secondary-inverse);border-color:var(--bs-secondary);background-color:var(--bs-secondary)!important}.btn-check:active+.btn.btn-light-secondary .svg-icon,.btn-check:active+.btn.btn-light-secondary i,.btn-check:checked+.btn.btn-light-secondary .svg-icon,.btn-check:checked+.btn.btn-light-secondary i,.btn.btn-light-secondary.active .svg-icon,.btn.btn-light-secondary.active i,.btn.btn-light-secondary.show .svg-icon,.btn.btn-light-secondary.show i,.btn.btn-light-secondary:active:not(.btn-active) .svg-icon,.btn.btn-light-secondary:active:not(.btn-active) i,.btn.btn-light-secondary:focus:not(.btn-active) .svg-icon,.btn.btn-light-secondary:focus:not(.btn-active) i,.btn.btn-light-secondary:hover:not(.btn-active) .svg-icon,.btn.btn-light-secondary:hover:not(.btn-active) i,.show>.btn.btn-light-secondary .svg-icon,.show>.btn.btn-light-secondary i{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-light-secondary.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-secondary.dropdown-toggle:after,.btn.btn-light-secondary.active.dropdown-toggle:after,.btn.btn-light-secondary.show.dropdown-toggle:after,.btn.btn-light-secondary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-secondary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-secondary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-secondary.dropdown-toggle:after{color:var(--bs-secondary-inverse)}.btn.btn-bg-secondary{border-color:var(--bs-secondary);background-color:var(--bs-secondary)}.btn-check:active+.btn.btn-active-secondary,.btn-check:checked+.btn.btn-active-secondary,.btn.btn-active-secondary.active,.btn.btn-active-secondary.show,.btn.btn-active-secondary:active:not(.btn-active),.btn.btn-active-secondary:focus:not(.btn-active),.btn.btn-active-secondary:hover:not(.btn-active),.show>.btn.btn-active-secondary{color:var(--bs-secondary-inverse);border-color:var(--bs-secondary);background-color:var(--bs-secondary)!important}.btn-check:active+.btn.btn-active-secondary .svg-icon,.btn-check:active+.btn.btn-active-secondary i,.btn-check:checked+.btn.btn-active-secondary .svg-icon,.btn-check:checked+.btn.btn-active-secondary i,.btn.btn-active-secondary.active .svg-icon,.btn.btn-active-secondary.active i,.btn.btn-active-secondary.show .svg-icon,.btn.btn-active-secondary.show i,.btn.btn-active-secondary:active:not(.btn-active) .svg-icon,.btn.btn-active-secondary:active:not(.btn-active) i,.btn.btn-active-secondary:focus:not(.btn-active) .svg-icon,.btn.btn-active-secondary:focus:not(.btn-active) i,.btn.btn-active-secondary:hover:not(.btn-active) .svg-icon,.btn.btn-active-secondary:hover:not(.btn-active) i,.show>.btn.btn-active-secondary .svg-icon,.show>.btn.btn-active-secondary i{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-active-secondary.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-secondary.dropdown-toggle:after,.btn.btn-active-secondary.active.dropdown-toggle:after,.btn.btn-active-secondary.show.dropdown-toggle:after,.btn.btn-active-secondary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-secondary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-secondary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-secondary.dropdown-toggle:after{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-active-light-secondary,.btn-check:checked+.btn.btn-active-light-secondary,.btn.btn-active-light-secondary.active,.btn.btn-active-light-secondary.show,.btn.btn-active-light-secondary:active:not(.btn-active),.btn.btn-active-light-secondary:focus:not(.btn-active),.btn.btn-active-light-secondary:hover:not(.btn-active),.show>.btn.btn-active-light-secondary{color:var(--bs-secondary-inverse);border-color:var(--bs-secondary-light);background-color:var(--bs-secondary-light)!important}.btn-check:active+.btn.btn-active-light-secondary .svg-icon,.btn-check:active+.btn.btn-active-light-secondary i,.btn-check:checked+.btn.btn-active-light-secondary .svg-icon,.btn-check:checked+.btn.btn-active-light-secondary i,.btn.btn-active-light-secondary.active .svg-icon,.btn.btn-active-light-secondary.active i,.btn.btn-active-light-secondary.show .svg-icon,.btn.btn-active-light-secondary.show i,.btn.btn-active-light-secondary:active:not(.btn-active) .svg-icon,.btn.btn-active-light-secondary:active:not(.btn-active) i,.btn.btn-active-light-secondary:focus:not(.btn-active) .svg-icon,.btn.btn-active-light-secondary:focus:not(.btn-active) i,.btn.btn-active-light-secondary:hover:not(.btn-active) .svg-icon,.btn.btn-active-light-secondary:hover:not(.btn-active) i,.show>.btn.btn-active-light-secondary .svg-icon,.show>.btn.btn-active-light-secondary i{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-active-light-secondary.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light-secondary.dropdown-toggle:after,.btn.btn-active-light-secondary.active.dropdown-toggle:after,.btn.btn-active-light-secondary.show.dropdown-toggle:after,.btn.btn-active-light-secondary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-secondary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-secondary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light-secondary.dropdown-toggle:after{color:var(--bs-secondary-inverse)}.btn.btn-outline.btn-outline-secondary{color:var(--bs-secondary-inverse);border-color:var(--bs-secondary-inverse);background-color:transparent}.btn.btn-outline.btn-outline-secondary .svg-icon,.btn.btn-outline.btn-outline-secondary i{color:var(--bs-secondary-inverse)}.btn.btn-outline.btn-outline-secondary.dropdown-toggle:after{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-outline.btn-outline-secondary,.btn-check:checked+.btn.btn-outline.btn-outline-secondary,.btn.btn-outline.btn-outline-secondary.active,.btn.btn-outline.btn-outline-secondary.show,.btn.btn-outline.btn-outline-secondary:active:not(.btn-active),.btn.btn-outline.btn-outline-secondary:focus:not(.btn-active),.btn.btn-outline.btn-outline-secondary:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-secondary{color:var(--bs-secondary-inverse);border-color:var(--bs-secondary-inverse);background-color:var(--bs-secondary-light)!important}.btn-check:active+.btn.btn-outline.btn-outline-secondary .svg-icon,.btn-check:active+.btn.btn-outline.btn-outline-secondary i,.btn-check:checked+.btn.btn-outline.btn-outline-secondary .svg-icon,.btn-check:checked+.btn.btn-outline.btn-outline-secondary i,.btn.btn-outline.btn-outline-secondary.active .svg-icon,.btn.btn-outline.btn-outline-secondary.active i,.btn.btn-outline.btn-outline-secondary.show .svg-icon,.btn.btn-outline.btn-outline-secondary.show i,.btn.btn-outline.btn-outline-secondary:active:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-secondary:active:not(.btn-active) i,.btn.btn-outline.btn-outline-secondary:focus:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-secondary:focus:not(.btn-active) i,.btn.btn-outline.btn-outline-secondary:hover:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-secondary:hover:not(.btn-active) i,.show>.btn.btn-outline.btn-outline-secondary .svg-icon,.show>.btn.btn-outline.btn-outline-secondary i{color:var(--bs-secondary-inverse)}.btn-check:active+.btn.btn-outline.btn-outline-secondary.dropdown-toggle:after,.btn-check:checked+.btn.btn-outline.btn-outline-secondary.dropdown-toggle:after,.btn.btn-outline.btn-outline-secondary.active.dropdown-toggle:after,.btn.btn-outline.btn-outline-secondary.show.dropdown-toggle:after,.btn.btn-outline.btn-outline-secondary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-secondary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-secondary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-outline.btn-outline-secondary.dropdown-toggle:after{color:var(--bs-secondary-inverse)}.btn.btn-success{color:var(--bs-success-inverse);border-color:var(--bs-success);background-color:var(--bs-success)}.btn.btn-success .svg-icon,.btn.btn-success i{color:var(--bs-success-inverse)}.btn.btn-success.dropdown-toggle:after{color:var(--bs-success-inverse)}.btn-check:active+.btn.btn-success,.btn-check:checked+.btn.btn-success,.btn.btn-success.active,.btn.btn-success.show,.btn.btn-success:active:not(.btn-active),.btn.btn-success:focus:not(.btn-active),.btn.btn-success:hover:not(.btn-active),.show>.btn.btn-success{color:var(--bs-success-inverse);border-color:var(--bs-success-active);background-color:var(--bs-success-active)!important}.btn-check:active+.btn.btn-success .svg-icon,.btn-check:active+.btn.btn-success i,.btn-check:checked+.btn.btn-success .svg-icon,.btn-check:checked+.btn.btn-success i,.btn.btn-success.active .svg-icon,.btn.btn-success.active i,.btn.btn-success.show .svg-icon,.btn.btn-success.show i,.btn.btn-success:active:not(.btn-active) .svg-icon,.btn.btn-success:active:not(.btn-active) i,.btn.btn-success:focus:not(.btn-active) .svg-icon,.btn.btn-success:focus:not(.btn-active) i,.btn.btn-success:hover:not(.btn-active) .svg-icon,.btn.btn-success:hover:not(.btn-active) i,.show>.btn.btn-success .svg-icon,.show>.btn.btn-success i{color:var(--bs-success-inverse)}.btn-check:active+.btn.btn-success.dropdown-toggle:after,.btn-check:checked+.btn.btn-success.dropdown-toggle:after,.btn.btn-success.active.dropdown-toggle:after,.btn.btn-success.show.dropdown-toggle:after,.btn.btn-success:active:not(.btn-active).dropdown-toggle:after,.btn.btn-success:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-success:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-success.dropdown-toggle:after{color:var(--bs-success-inverse)}.btn.btn-light-success{color:var(--bs-success);border-color:var(--bs-success-light);background-color:var(--bs-success-light)}.btn.btn-light-success .svg-icon,.btn.btn-light-success i{color:var(--bs-success)}.btn.btn-light-success.dropdown-toggle:after{color:var(--bs-success)}.btn-check:active+.btn.btn-light-success,.btn-check:checked+.btn.btn-light-success,.btn.btn-light-success.active,.btn.btn-light-success.show,.btn.btn-light-success:active:not(.btn-active),.btn.btn-light-success:focus:not(.btn-active),.btn.btn-light-success:hover:not(.btn-active),.show>.btn.btn-light-success{color:var(--bs-success-inverse);border-color:var(--bs-success);background-color:var(--bs-success)!important}.btn-check:active+.btn.btn-light-success .svg-icon,.btn-check:active+.btn.btn-light-success i,.btn-check:checked+.btn.btn-light-success .svg-icon,.btn-check:checked+.btn.btn-light-success i,.btn.btn-light-success.active .svg-icon,.btn.btn-light-success.active i,.btn.btn-light-success.show .svg-icon,.btn.btn-light-success.show i,.btn.btn-light-success:active:not(.btn-active) .svg-icon,.btn.btn-light-success:active:not(.btn-active) i,.btn.btn-light-success:focus:not(.btn-active) .svg-icon,.btn.btn-light-success:focus:not(.btn-active) i,.btn.btn-light-success:hover:not(.btn-active) .svg-icon,.btn.btn-light-success:hover:not(.btn-active) i,.show>.btn.btn-light-success .svg-icon,.show>.btn.btn-light-success i{color:var(--bs-success-inverse)}.btn-check:active+.btn.btn-light-success.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-success.dropdown-toggle:after,.btn.btn-light-success.active.dropdown-toggle:after,.btn.btn-light-success.show.dropdown-toggle:after,.btn.btn-light-success:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-success:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-success:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-success.dropdown-toggle:after{color:var(--bs-success-inverse)}.btn.btn-bg-success{border-color:var(--bs-success);background-color:var(--bs-success)}.btn-check:active+.btn.btn-active-success,.btn-check:checked+.btn.btn-active-success,.btn.btn-active-success.active,.btn.btn-active-success.show,.btn.btn-active-success:active:not(.btn-active),.btn.btn-active-success:focus:not(.btn-active),.btn.btn-active-success:hover:not(.btn-active),.show>.btn.btn-active-success{color:var(--bs-success-inverse);border-color:var(--bs-success);background-color:var(--bs-success)!important}.btn-check:active+.btn.btn-active-success .svg-icon,.btn-check:active+.btn.btn-active-success i,.btn-check:checked+.btn.btn-active-success .svg-icon,.btn-check:checked+.btn.btn-active-success i,.btn.btn-active-success.active .svg-icon,.btn.btn-active-success.active i,.btn.btn-active-success.show .svg-icon,.btn.btn-active-success.show i,.btn.btn-active-success:active:not(.btn-active) .svg-icon,.btn.btn-active-success:active:not(.btn-active) i,.btn.btn-active-success:focus:not(.btn-active) .svg-icon,.btn.btn-active-success:focus:not(.btn-active) i,.btn.btn-active-success:hover:not(.btn-active) .svg-icon,.btn.btn-active-success:hover:not(.btn-active) i,.show>.btn.btn-active-success .svg-icon,.show>.btn.btn-active-success i{color:var(--bs-success-inverse)}.btn-check:active+.btn.btn-active-success.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-success.dropdown-toggle:after,.btn.btn-active-success.active.dropdown-toggle:after,.btn.btn-active-success.show.dropdown-toggle:after,.btn.btn-active-success:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-success:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-success:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-success.dropdown-toggle:after{color:var(--bs-success-inverse)}.btn-check:active+.btn.btn-active-light-success,.btn-check:checked+.btn.btn-active-light-success,.btn.btn-active-light-success.active,.btn.btn-active-light-success.show,.btn.btn-active-light-success:active:not(.btn-active),.btn.btn-active-light-success:focus:not(.btn-active),.btn.btn-active-light-success:hover:not(.btn-active),.show>.btn.btn-active-light-success{color:var(--bs-success);border-color:var(--bs-success-light);background-color:var(--bs-success-light)!important}.btn-check:active+.btn.btn-active-light-success .svg-icon,.btn-check:active+.btn.btn-active-light-success i,.btn-check:checked+.btn.btn-active-light-success .svg-icon,.btn-check:checked+.btn.btn-active-light-success i,.btn.btn-active-light-success.active .svg-icon,.btn.btn-active-light-success.active i,.btn.btn-active-light-success.show .svg-icon,.btn.btn-active-light-success.show i,.btn.btn-active-light-success:active:not(.btn-active) .svg-icon,.btn.btn-active-light-success:active:not(.btn-active) i,.btn.btn-active-light-success:focus:not(.btn-active) .svg-icon,.btn.btn-active-light-success:focus:not(.btn-active) i,.btn.btn-active-light-success:hover:not(.btn-active) .svg-icon,.btn.btn-active-light-success:hover:not(.btn-active) i,.show>.btn.btn-active-light-success .svg-icon,.show>.btn.btn-active-light-success i{color:var(--bs-success)}.btn-check:active+.btn.btn-active-light-success.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light-success.dropdown-toggle:after,.btn.btn-active-light-success.active.dropdown-toggle:after,.btn.btn-active-light-success.show.dropdown-toggle:after,.btn.btn-active-light-success:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-success:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-success:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light-success.dropdown-toggle:after{color:var(--bs-success)}.btn.btn-outline.btn-outline-success{color:var(--bs-success);border-color:var(--bs-success);background-color:transparent}.btn.btn-outline.btn-outline-success .svg-icon,.btn.btn-outline.btn-outline-success i{color:var(--bs-success)}.btn.btn-outline.btn-outline-success.dropdown-toggle:after{color:var(--bs-success)}.btn-check:active+.btn.btn-outline.btn-outline-success,.btn-check:checked+.btn.btn-outline.btn-outline-success,.btn.btn-outline.btn-outline-success.active,.btn.btn-outline.btn-outline-success.show,.btn.btn-outline.btn-outline-success:active:not(.btn-active),.btn.btn-outline.btn-outline-success:focus:not(.btn-active),.btn.btn-outline.btn-outline-success:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-success{color:var(--bs-success-active);border-color:var(--bs-success);background-color:var(--bs-success-light)!important}.btn-check:active+.btn.btn-outline.btn-outline-success .svg-icon,.btn-check:active+.btn.btn-outline.btn-outline-success i,.btn-check:checked+.btn.btn-outline.btn-outline-success .svg-icon,.btn-check:checked+.btn.btn-outline.btn-outline-success i,.btn.btn-outline.btn-outline-success.active .svg-icon,.btn.btn-outline.btn-outline-success.active i,.btn.btn-outline.btn-outline-success.show .svg-icon,.btn.btn-outline.btn-outline-success.show i,.btn.btn-outline.btn-outline-success:active:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-success:active:not(.btn-active) i,.btn.btn-outline.btn-outline-success:focus:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-success:focus:not(.btn-active) i,.btn.btn-outline.btn-outline-success:hover:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-success:hover:not(.btn-active) i,.show>.btn.btn-outline.btn-outline-success .svg-icon,.show>.btn.btn-outline.btn-outline-success i{color:var(--bs-success-active)}.btn-check:active+.btn.btn-outline.btn-outline-success.dropdown-toggle:after,.btn-check:checked+.btn.btn-outline.btn-outline-success.dropdown-toggle:after,.btn.btn-outline.btn-outline-success.active.dropdown-toggle:after,.btn.btn-outline.btn-outline-success.show.dropdown-toggle:after,.btn.btn-outline.btn-outline-success:active:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-success:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-success:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-outline.btn-outline-success.dropdown-toggle:after{color:var(--bs-success-active)}.btn.btn-info{color:var(--bs-info-inverse);border-color:var(--bs-info);background-color:var(--bs-info)}.btn.btn-info .svg-icon,.btn.btn-info i{color:var(--bs-info-inverse)}.btn.btn-info.dropdown-toggle:after{color:var(--bs-info-inverse)}.btn-check:active+.btn.btn-info,.btn-check:checked+.btn.btn-info,.btn.btn-info.active,.btn.btn-info.show,.btn.btn-info:active:not(.btn-active),.btn.btn-info:focus:not(.btn-active),.btn.btn-info:hover:not(.btn-active),.show>.btn.btn-info{color:var(--bs-info-inverse);border-color:var(--bs-info-active);background-color:var(--bs-info-active)!important}.btn-check:active+.btn.btn-info .svg-icon,.btn-check:active+.btn.btn-info i,.btn-check:checked+.btn.btn-info .svg-icon,.btn-check:checked+.btn.btn-info i,.btn.btn-info.active .svg-icon,.btn.btn-info.active i,.btn.btn-info.show .svg-icon,.btn.btn-info.show i,.btn.btn-info:active:not(.btn-active) .svg-icon,.btn.btn-info:active:not(.btn-active) i,.btn.btn-info:focus:not(.btn-active) .svg-icon,.btn.btn-info:focus:not(.btn-active) i,.btn.btn-info:hover:not(.btn-active) .svg-icon,.btn.btn-info:hover:not(.btn-active) i,.show>.btn.btn-info .svg-icon,.show>.btn.btn-info i{color:var(--bs-info-inverse)}.btn-check:active+.btn.btn-info.dropdown-toggle:after,.btn-check:checked+.btn.btn-info.dropdown-toggle:after,.btn.btn-info.active.dropdown-toggle:after,.btn.btn-info.show.dropdown-toggle:after,.btn.btn-info:active:not(.btn-active).dropdown-toggle:after,.btn.btn-info:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-info:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-info.dropdown-toggle:after{color:var(--bs-info-inverse)}.btn.btn-light-info{color:var(--bs-info);border-color:var(--bs-info-light);background-color:var(--bs-info-light)}.btn.btn-light-info .svg-icon,.btn.btn-light-info i{color:var(--bs-info)}.btn.btn-light-info.dropdown-toggle:after{color:var(--bs-info)}.btn-check:active+.btn.btn-light-info,.btn-check:checked+.btn.btn-light-info,.btn.btn-light-info.active,.btn.btn-light-info.show,.btn.btn-light-info:active:not(.btn-active),.btn.btn-light-info:focus:not(.btn-active),.btn.btn-light-info:hover:not(.btn-active),.show>.btn.btn-light-info{color:var(--bs-info-inverse);border-color:var(--bs-info);background-color:var(--bs-info)!important}.btn-check:active+.btn.btn-light-info .svg-icon,.btn-check:active+.btn.btn-light-info i,.btn-check:checked+.btn.btn-light-info .svg-icon,.btn-check:checked+.btn.btn-light-info i,.btn.btn-light-info.active .svg-icon,.btn.btn-light-info.active i,.btn.btn-light-info.show .svg-icon,.btn.btn-light-info.show i,.btn.btn-light-info:active:not(.btn-active) .svg-icon,.btn.btn-light-info:active:not(.btn-active) i,.btn.btn-light-info:focus:not(.btn-active) .svg-icon,.btn.btn-light-info:focus:not(.btn-active) i,.btn.btn-light-info:hover:not(.btn-active) .svg-icon,.btn.btn-light-info:hover:not(.btn-active) i,.show>.btn.btn-light-info .svg-icon,.show>.btn.btn-light-info i{color:var(--bs-info-inverse)}.btn-check:active+.btn.btn-light-info.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-info.dropdown-toggle:after,.btn.btn-light-info.active.dropdown-toggle:after,.btn.btn-light-info.show.dropdown-toggle:after,.btn.btn-light-info:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-info:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-info:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-info.dropdown-toggle:after{color:var(--bs-info-inverse)}.btn.btn-bg-info{border-color:var(--bs-info);background-color:var(--bs-info)}.btn-check:active+.btn.btn-active-info,.btn-check:checked+.btn.btn-active-info,.btn.btn-active-info.active,.btn.btn-active-info.show,.btn.btn-active-info:active:not(.btn-active),.btn.btn-active-info:focus:not(.btn-active),.btn.btn-active-info:hover:not(.btn-active),.show>.btn.btn-active-info{color:var(--bs-info-inverse);border-color:var(--bs-info);background-color:var(--bs-info)!important}.btn-check:active+.btn.btn-active-info .svg-icon,.btn-check:active+.btn.btn-active-info i,.btn-check:checked+.btn.btn-active-info .svg-icon,.btn-check:checked+.btn.btn-active-info i,.btn.btn-active-info.active .svg-icon,.btn.btn-active-info.active i,.btn.btn-active-info.show .svg-icon,.btn.btn-active-info.show i,.btn.btn-active-info:active:not(.btn-active) .svg-icon,.btn.btn-active-info:active:not(.btn-active) i,.btn.btn-active-info:focus:not(.btn-active) .svg-icon,.btn.btn-active-info:focus:not(.btn-active) i,.btn.btn-active-info:hover:not(.btn-active) .svg-icon,.btn.btn-active-info:hover:not(.btn-active) i,.show>.btn.btn-active-info .svg-icon,.show>.btn.btn-active-info i{color:var(--bs-info-inverse)}.btn-check:active+.btn.btn-active-info.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-info.dropdown-toggle:after,.btn.btn-active-info.active.dropdown-toggle:after,.btn.btn-active-info.show.dropdown-toggle:after,.btn.btn-active-info:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-info:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-info:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-info.dropdown-toggle:after{color:var(--bs-info-inverse)}.btn-check:active+.btn.btn-active-light-info,.btn-check:checked+.btn.btn-active-light-info,.btn.btn-active-light-info.active,.btn.btn-active-light-info.show,.btn.btn-active-light-info:active:not(.btn-active),.btn.btn-active-light-info:focus:not(.btn-active),.btn.btn-active-light-info:hover:not(.btn-active),.show>.btn.btn-active-light-info{color:var(--bs-info);border-color:var(--bs-info-light);background-color:var(--bs-info-light)!important}.btn-check:active+.btn.btn-active-light-info .svg-icon,.btn-check:active+.btn.btn-active-light-info i,.btn-check:checked+.btn.btn-active-light-info .svg-icon,.btn-check:checked+.btn.btn-active-light-info i,.btn.btn-active-light-info.active .svg-icon,.btn.btn-active-light-info.active i,.btn.btn-active-light-info.show .svg-icon,.btn.btn-active-light-info.show i,.btn.btn-active-light-info:active:not(.btn-active) .svg-icon,.btn.btn-active-light-info:active:not(.btn-active) i,.btn.btn-active-light-info:focus:not(.btn-active) .svg-icon,.btn.btn-active-light-info:focus:not(.btn-active) i,.btn.btn-active-light-info:hover:not(.btn-active) .svg-icon,.btn.btn-active-light-info:hover:not(.btn-active) i,.show>.btn.btn-active-light-info .svg-icon,.show>.btn.btn-active-light-info i{color:var(--bs-info)}.btn-check:active+.btn.btn-active-light-info.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light-info.dropdown-toggle:after,.btn.btn-active-light-info.active.dropdown-toggle:after,.btn.btn-active-light-info.show.dropdown-toggle:after,.btn.btn-active-light-info:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-info:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-info:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light-info.dropdown-toggle:after{color:var(--bs-info)}.btn.btn-outline.btn-outline-info{color:var(--bs-info);border-color:var(--bs-info);background-color:transparent}.btn.btn-outline.btn-outline-info .svg-icon,.btn.btn-outline.btn-outline-info i{color:var(--bs-info)}.btn.btn-outline.btn-outline-info.dropdown-toggle:after{color:var(--bs-info)}.btn-check:active+.btn.btn-outline.btn-outline-info,.btn-check:checked+.btn.btn-outline.btn-outline-info,.btn.btn-outline.btn-outline-info.active,.btn.btn-outline.btn-outline-info.show,.btn.btn-outline.btn-outline-info:active:not(.btn-active),.btn.btn-outline.btn-outline-info:focus:not(.btn-active),.btn.btn-outline.btn-outline-info:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-info{color:var(--bs-info-active);border-color:var(--bs-info);background-color:var(--bs-info-light)!important}.btn-check:active+.btn.btn-outline.btn-outline-info .svg-icon,.btn-check:active+.btn.btn-outline.btn-outline-info i,.btn-check:checked+.btn.btn-outline.btn-outline-info .svg-icon,.btn-check:checked+.btn.btn-outline.btn-outline-info i,.btn.btn-outline.btn-outline-info.active .svg-icon,.btn.btn-outline.btn-outline-info.active i,.btn.btn-outline.btn-outline-info.show .svg-icon,.btn.btn-outline.btn-outline-info.show i,.btn.btn-outline.btn-outline-info:active:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-info:active:not(.btn-active) i,.btn.btn-outline.btn-outline-info:focus:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-info:focus:not(.btn-active) i,.btn.btn-outline.btn-outline-info:hover:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-info:hover:not(.btn-active) i,.show>.btn.btn-outline.btn-outline-info .svg-icon,.show>.btn.btn-outline.btn-outline-info i{color:var(--bs-info-active)}.btn-check:active+.btn.btn-outline.btn-outline-info.dropdown-toggle:after,.btn-check:checked+.btn.btn-outline.btn-outline-info.dropdown-toggle:after,.btn.btn-outline.btn-outline-info.active.dropdown-toggle:after,.btn.btn-outline.btn-outline-info.show.dropdown-toggle:after,.btn.btn-outline.btn-outline-info:active:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-info:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-info:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-outline.btn-outline-info.dropdown-toggle:after{color:var(--bs-info-active)}.btn.btn-warning{color:var(--bs-warning-inverse);border-color:var(--bs-warning);background-color:var(--bs-warning)}.btn.btn-warning .svg-icon,.btn.btn-warning i{color:var(--bs-warning-inverse)}.btn.btn-warning.dropdown-toggle:after{color:var(--bs-warning-inverse)}.btn-check:active+.btn.btn-warning,.btn-check:checked+.btn.btn-warning,.btn.btn-warning.active,.btn.btn-warning.show,.btn.btn-warning:active:not(.btn-active),.btn.btn-warning:focus:not(.btn-active),.btn.btn-warning:hover:not(.btn-active),.show>.btn.btn-warning{color:var(--bs-warning-inverse);border-color:var(--bs-warning-active);background-color:var(--bs-warning-active)!important}.btn-check:active+.btn.btn-warning .svg-icon,.btn-check:active+.btn.btn-warning i,.btn-check:checked+.btn.btn-warning .svg-icon,.btn-check:checked+.btn.btn-warning i,.btn.btn-warning.active .svg-icon,.btn.btn-warning.active i,.btn.btn-warning.show .svg-icon,.btn.btn-warning.show i,.btn.btn-warning:active:not(.btn-active) .svg-icon,.btn.btn-warning:active:not(.btn-active) i,.btn.btn-warning:focus:not(.btn-active) .svg-icon,.btn.btn-warning:focus:not(.btn-active) i,.btn.btn-warning:hover:not(.btn-active) .svg-icon,.btn.btn-warning:hover:not(.btn-active) i,.show>.btn.btn-warning .svg-icon,.show>.btn.btn-warning i{color:var(--bs-warning-inverse)}.btn-check:active+.btn.btn-warning.dropdown-toggle:after,.btn-check:checked+.btn.btn-warning.dropdown-toggle:after,.btn.btn-warning.active.dropdown-toggle:after,.btn.btn-warning.show.dropdown-toggle:after,.btn.btn-warning:active:not(.btn-active).dropdown-toggle:after,.btn.btn-warning:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-warning:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-warning.dropdown-toggle:after{color:var(--bs-warning-inverse)}.btn.btn-light-warning{color:var(--bs-warning);border-color:var(--bs-warning-light);background-color:var(--bs-warning-light)}.btn.btn-light-warning .svg-icon,.btn.btn-light-warning i{color:var(--bs-warning)}.btn.btn-light-warning.dropdown-toggle:after{color:var(--bs-warning)}.btn-check:active+.btn.btn-light-warning,.btn-check:checked+.btn.btn-light-warning,.btn.btn-light-warning.active,.btn.btn-light-warning.show,.btn.btn-light-warning:active:not(.btn-active),.btn.btn-light-warning:focus:not(.btn-active),.btn.btn-light-warning:hover:not(.btn-active),.show>.btn.btn-light-warning{color:var(--bs-warning-inverse);border-color:var(--bs-warning);background-color:var(--bs-warning)!important}.btn-check:active+.btn.btn-light-warning .svg-icon,.btn-check:active+.btn.btn-light-warning i,.btn-check:checked+.btn.btn-light-warning .svg-icon,.btn-check:checked+.btn.btn-light-warning i,.btn.btn-light-warning.active .svg-icon,.btn.btn-light-warning.active i,.btn.btn-light-warning.show .svg-icon,.btn.btn-light-warning.show i,.btn.btn-light-warning:active:not(.btn-active) .svg-icon,.btn.btn-light-warning:active:not(.btn-active) i,.btn.btn-light-warning:focus:not(.btn-active) .svg-icon,.btn.btn-light-warning:focus:not(.btn-active) i,.btn.btn-light-warning:hover:not(.btn-active) .svg-icon,.btn.btn-light-warning:hover:not(.btn-active) i,.show>.btn.btn-light-warning .svg-icon,.show>.btn.btn-light-warning i{color:var(--bs-warning-inverse)}.btn-check:active+.btn.btn-light-warning.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-warning.dropdown-toggle:after,.btn.btn-light-warning.active.dropdown-toggle:after,.btn.btn-light-warning.show.dropdown-toggle:after,.btn.btn-light-warning:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-warning:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-warning:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-warning.dropdown-toggle:after{color:var(--bs-warning-inverse)}.btn.btn-bg-warning{border-color:var(--bs-warning);background-color:var(--bs-warning)}.btn-check:active+.btn.btn-active-warning,.btn-check:checked+.btn.btn-active-warning,.btn.btn-active-warning.active,.btn.btn-active-warning.show,.btn.btn-active-warning:active:not(.btn-active),.btn.btn-active-warning:focus:not(.btn-active),.btn.btn-active-warning:hover:not(.btn-active),.show>.btn.btn-active-warning{color:var(--bs-warning-inverse);border-color:var(--bs-warning);background-color:var(--bs-warning)!important}.btn-check:active+.btn.btn-active-warning .svg-icon,.btn-check:active+.btn.btn-active-warning i,.btn-check:checked+.btn.btn-active-warning .svg-icon,.btn-check:checked+.btn.btn-active-warning i,.btn.btn-active-warning.active .svg-icon,.btn.btn-active-warning.active i,.btn.btn-active-warning.show .svg-icon,.btn.btn-active-warning.show i,.btn.btn-active-warning:active:not(.btn-active) .svg-icon,.btn.btn-active-warning:active:not(.btn-active) i,.btn.btn-active-warning:focus:not(.btn-active) .svg-icon,.btn.btn-active-warning:focus:not(.btn-active) i,.btn.btn-active-warning:hover:not(.btn-active) .svg-icon,.btn.btn-active-warning:hover:not(.btn-active) i,.show>.btn.btn-active-warning .svg-icon,.show>.btn.btn-active-warning i{color:var(--bs-warning-inverse)}.btn-check:active+.btn.btn-active-warning.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-warning.dropdown-toggle:after,.btn.btn-active-warning.active.dropdown-toggle:after,.btn.btn-active-warning.show.dropdown-toggle:after,.btn.btn-active-warning:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-warning:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-warning:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-warning.dropdown-toggle:after{color:var(--bs-warning-inverse)}.btn-check:active+.btn.btn-active-light-warning,.btn-check:checked+.btn.btn-active-light-warning,.btn.btn-active-light-warning.active,.btn.btn-active-light-warning.show,.btn.btn-active-light-warning:active:not(.btn-active),.btn.btn-active-light-warning:focus:not(.btn-active),.btn.btn-active-light-warning:hover:not(.btn-active),.show>.btn.btn-active-light-warning{color:var(--bs-warning);border-color:var(--bs-warning-light);background-color:var(--bs-warning-light)!important}.btn-check:active+.btn.btn-active-light-warning .svg-icon,.btn-check:active+.btn.btn-active-light-warning i,.btn-check:checked+.btn.btn-active-light-warning .svg-icon,.btn-check:checked+.btn.btn-active-light-warning i,.btn.btn-active-light-warning.active .svg-icon,.btn.btn-active-light-warning.active i,.btn.btn-active-light-warning.show .svg-icon,.btn.btn-active-light-warning.show i,.btn.btn-active-light-warning:active:not(.btn-active) .svg-icon,.btn.btn-active-light-warning:active:not(.btn-active) i,.btn.btn-active-light-warning:focus:not(.btn-active) .svg-icon,.btn.btn-active-light-warning:focus:not(.btn-active) i,.btn.btn-active-light-warning:hover:not(.btn-active) .svg-icon,.btn.btn-active-light-warning:hover:not(.btn-active) i,.show>.btn.btn-active-light-warning .svg-icon,.show>.btn.btn-active-light-warning i{color:var(--bs-warning)}.btn-check:active+.btn.btn-active-light-warning.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light-warning.dropdown-toggle:after,.btn.btn-active-light-warning.active.dropdown-toggle:after,.btn.btn-active-light-warning.show.dropdown-toggle:after,.btn.btn-active-light-warning:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-warning:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-warning:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light-warning.dropdown-toggle:after{color:var(--bs-warning)}.btn.btn-outline.btn-outline-warning{color:var(--bs-warning);border-color:var(--bs-warning);background-color:transparent}.btn.btn-outline.btn-outline-warning .svg-icon,.btn.btn-outline.btn-outline-warning i{color:var(--bs-warning)}.btn.btn-outline.btn-outline-warning.dropdown-toggle:after{color:var(--bs-warning)}.btn-check:active+.btn.btn-outline.btn-outline-warning,.btn-check:checked+.btn.btn-outline.btn-outline-warning,.btn.btn-outline.btn-outline-warning.active,.btn.btn-outline.btn-outline-warning.show,.btn.btn-outline.btn-outline-warning:active:not(.btn-active),.btn.btn-outline.btn-outline-warning:focus:not(.btn-active),.btn.btn-outline.btn-outline-warning:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-warning{color:var(--bs-warning-active);border-color:var(--bs-warning);background-color:var(--bs-warning-light)!important}.btn-check:active+.btn.btn-outline.btn-outline-warning .svg-icon,.btn-check:active+.btn.btn-outline.btn-outline-warning i,.btn-check:checked+.btn.btn-outline.btn-outline-warning .svg-icon,.btn-check:checked+.btn.btn-outline.btn-outline-warning i,.btn.btn-outline.btn-outline-warning.active .svg-icon,.btn.btn-outline.btn-outline-warning.active i,.btn.btn-outline.btn-outline-warning.show .svg-icon,.btn.btn-outline.btn-outline-warning.show i,.btn.btn-outline.btn-outline-warning:active:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-warning:active:not(.btn-active) i,.btn.btn-outline.btn-outline-warning:focus:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-warning:focus:not(.btn-active) i,.btn.btn-outline.btn-outline-warning:hover:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-warning:hover:not(.btn-active) i,.show>.btn.btn-outline.btn-outline-warning .svg-icon,.show>.btn.btn-outline.btn-outline-warning i{color:var(--bs-warning-active)}.btn-check:active+.btn.btn-outline.btn-outline-warning.dropdown-toggle:after,.btn-check:checked+.btn.btn-outline.btn-outline-warning.dropdown-toggle:after,.btn.btn-outline.btn-outline-warning.active.dropdown-toggle:after,.btn.btn-outline.btn-outline-warning.show.dropdown-toggle:after,.btn.btn-outline.btn-outline-warning:active:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-warning:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-warning:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-outline.btn-outline-warning.dropdown-toggle:after{color:var(--bs-warning-active)}.btn.btn-danger{color:var(--bs-danger-inverse);border-color:var(--bs-danger);background-color:var(--bs-danger)}.btn.btn-danger .svg-icon,.btn.btn-danger i{color:var(--bs-danger-inverse)}.btn.btn-danger.dropdown-toggle:after{color:var(--bs-danger-inverse)}.btn-check:active+.btn.btn-danger,.btn-check:checked+.btn.btn-danger,.btn.btn-danger.active,.btn.btn-danger.show,.btn.btn-danger:active:not(.btn-active),.btn.btn-danger:focus:not(.btn-active),.btn.btn-danger:hover:not(.btn-active),.show>.btn.btn-danger{color:var(--bs-danger-inverse);border-color:var(--bs-danger-active);background-color:var(--bs-danger-active)!important}.btn-check:active+.btn.btn-danger .svg-icon,.btn-check:active+.btn.btn-danger i,.btn-check:checked+.btn.btn-danger .svg-icon,.btn-check:checked+.btn.btn-danger i,.btn.btn-danger.active .svg-icon,.btn.btn-danger.active i,.btn.btn-danger.show .svg-icon,.btn.btn-danger.show i,.btn.btn-danger:active:not(.btn-active) .svg-icon,.btn.btn-danger:active:not(.btn-active) i,.btn.btn-danger:focus:not(.btn-active) .svg-icon,.btn.btn-danger:focus:not(.btn-active) i,.btn.btn-danger:hover:not(.btn-active) .svg-icon,.btn.btn-danger:hover:not(.btn-active) i,.show>.btn.btn-danger .svg-icon,.show>.btn.btn-danger i{color:var(--bs-danger-inverse)}.btn-check:active+.btn.btn-danger.dropdown-toggle:after,.btn-check:checked+.btn.btn-danger.dropdown-toggle:after,.btn.btn-danger.active.dropdown-toggle:after,.btn.btn-danger.show.dropdown-toggle:after,.btn.btn-danger:active:not(.btn-active).dropdown-toggle:after,.btn.btn-danger:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-danger:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-danger.dropdown-toggle:after{color:var(--bs-danger-inverse)}.btn.btn-light-danger{color:var(--bs-danger);border-color:var(--bs-danger-light);background-color:var(--bs-danger-light)}.btn.btn-light-danger .svg-icon,.btn.btn-light-danger i{color:var(--bs-danger)}.btn.btn-light-danger.dropdown-toggle:after{color:var(--bs-danger)}.btn-check:active+.btn.btn-light-danger,.btn-check:checked+.btn.btn-light-danger,.btn.btn-light-danger.active,.btn.btn-light-danger.show,.btn.btn-light-danger:active:not(.btn-active),.btn.btn-light-danger:focus:not(.btn-active),.btn.btn-light-danger:hover:not(.btn-active),.show>.btn.btn-light-danger{color:var(--bs-danger-inverse);border-color:var(--bs-danger);background-color:var(--bs-danger)!important}.btn-check:active+.btn.btn-light-danger .svg-icon,.btn-check:active+.btn.btn-light-danger i,.btn-check:checked+.btn.btn-light-danger .svg-icon,.btn-check:checked+.btn.btn-light-danger i,.btn.btn-light-danger.active .svg-icon,.btn.btn-light-danger.active i,.btn.btn-light-danger.show .svg-icon,.btn.btn-light-danger.show i,.btn.btn-light-danger:active:not(.btn-active) .svg-icon,.btn.btn-light-danger:active:not(.btn-active) i,.btn.btn-light-danger:focus:not(.btn-active) .svg-icon,.btn.btn-light-danger:focus:not(.btn-active) i,.btn.btn-light-danger:hover:not(.btn-active) .svg-icon,.btn.btn-light-danger:hover:not(.btn-active) i,.show>.btn.btn-light-danger .svg-icon,.show>.btn.btn-light-danger i{color:var(--bs-danger-inverse)}.btn-check:active+.btn.btn-light-danger.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-danger.dropdown-toggle:after,.btn.btn-light-danger.active.dropdown-toggle:after,.btn.btn-light-danger.show.dropdown-toggle:after,.btn.btn-light-danger:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-danger:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-danger:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-danger.dropdown-toggle:after{color:var(--bs-danger-inverse)}.btn.btn-bg-danger{border-color:var(--bs-danger);background-color:var(--bs-danger)}.btn-check:active+.btn.btn-active-danger,.btn-check:checked+.btn.btn-active-danger,.btn.btn-active-danger.active,.btn.btn-active-danger.show,.btn.btn-active-danger:active:not(.btn-active),.btn.btn-active-danger:focus:not(.btn-active),.btn.btn-active-danger:hover:not(.btn-active),.show>.btn.btn-active-danger{color:var(--bs-danger-inverse);border-color:var(--bs-danger);background-color:var(--bs-danger)!important}.btn-check:active+.btn.btn-active-danger .svg-icon,.btn-check:active+.btn.btn-active-danger i,.btn-check:checked+.btn.btn-active-danger .svg-icon,.btn-check:checked+.btn.btn-active-danger i,.btn.btn-active-danger.active .svg-icon,.btn.btn-active-danger.active i,.btn.btn-active-danger.show .svg-icon,.btn.btn-active-danger.show i,.btn.btn-active-danger:active:not(.btn-active) .svg-icon,.btn.btn-active-danger:active:not(.btn-active) i,.btn.btn-active-danger:focus:not(.btn-active) .svg-icon,.btn.btn-active-danger:focus:not(.btn-active) i,.btn.btn-active-danger:hover:not(.btn-active) .svg-icon,.btn.btn-active-danger:hover:not(.btn-active) i,.show>.btn.btn-active-danger .svg-icon,.show>.btn.btn-active-danger i{color:var(--bs-danger-inverse)}.btn-check:active+.btn.btn-active-danger.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-danger.dropdown-toggle:after,.btn.btn-active-danger.active.dropdown-toggle:after,.btn.btn-active-danger.show.dropdown-toggle:after,.btn.btn-active-danger:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-danger:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-danger:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-danger.dropdown-toggle:after{color:var(--bs-danger-inverse)}.btn-check:active+.btn.btn-active-light-danger,.btn-check:checked+.btn.btn-active-light-danger,.btn.btn-active-light-danger.active,.btn.btn-active-light-danger.show,.btn.btn-active-light-danger:active:not(.btn-active),.btn.btn-active-light-danger:focus:not(.btn-active),.btn.btn-active-light-danger:hover:not(.btn-active),.show>.btn.btn-active-light-danger{color:var(--bs-danger);border-color:var(--bs-danger-light);background-color:var(--bs-danger-light)!important}.btn-check:active+.btn.btn-active-light-danger .svg-icon,.btn-check:active+.btn.btn-active-light-danger i,.btn-check:checked+.btn.btn-active-light-danger .svg-icon,.btn-check:checked+.btn.btn-active-light-danger i,.btn.btn-active-light-danger.active .svg-icon,.btn.btn-active-light-danger.active i,.btn.btn-active-light-danger.show .svg-icon,.btn.btn-active-light-danger.show i,.btn.btn-active-light-danger:active:not(.btn-active) .svg-icon,.btn.btn-active-light-danger:active:not(.btn-active) i,.btn.btn-active-light-danger:focus:not(.btn-active) .svg-icon,.btn.btn-active-light-danger:focus:not(.btn-active) i,.btn.btn-active-light-danger:hover:not(.btn-active) .svg-icon,.btn.btn-active-light-danger:hover:not(.btn-active) i,.show>.btn.btn-active-light-danger .svg-icon,.show>.btn.btn-active-light-danger i{color:var(--bs-danger)}.btn-check:active+.btn.btn-active-light-danger.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light-danger.dropdown-toggle:after,.btn.btn-active-light-danger.active.dropdown-toggle:after,.btn.btn-active-light-danger.show.dropdown-toggle:after,.btn.btn-active-light-danger:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-danger:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-danger:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light-danger.dropdown-toggle:after{color:var(--bs-danger)}.btn.btn-outline.btn-outline-danger{color:var(--bs-danger);border-color:var(--bs-danger);background-color:transparent}.btn.btn-outline.btn-outline-danger .svg-icon,.btn.btn-outline.btn-outline-danger i{color:var(--bs-danger)}.btn.btn-outline.btn-outline-danger.dropdown-toggle:after{color:var(--bs-danger)}.btn-check:active+.btn.btn-outline.btn-outline-danger,.btn-check:checked+.btn.btn-outline.btn-outline-danger,.btn.btn-outline.btn-outline-danger.active,.btn.btn-outline.btn-outline-danger.show,.btn.btn-outline.btn-outline-danger:active:not(.btn-active),.btn.btn-outline.btn-outline-danger:focus:not(.btn-active),.btn.btn-outline.btn-outline-danger:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-danger{color:var(--bs-danger-active);border-color:var(--bs-danger);background-color:var(--bs-danger-light)!important}.btn-check:active+.btn.btn-outline.btn-outline-danger .svg-icon,.btn-check:active+.btn.btn-outline.btn-outline-danger i,.btn-check:checked+.btn.btn-outline.btn-outline-danger .svg-icon,.btn-check:checked+.btn.btn-outline.btn-outline-danger i,.btn.btn-outline.btn-outline-danger.active .svg-icon,.btn.btn-outline.btn-outline-danger.active i,.btn.btn-outline.btn-outline-danger.show .svg-icon,.btn.btn-outline.btn-outline-danger.show i,.btn.btn-outline.btn-outline-danger:active:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-danger:active:not(.btn-active) i,.btn.btn-outline.btn-outline-danger:focus:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-danger:focus:not(.btn-active) i,.btn.btn-outline.btn-outline-danger:hover:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-danger:hover:not(.btn-active) i,.show>.btn.btn-outline.btn-outline-danger .svg-icon,.show>.btn.btn-outline.btn-outline-danger i{color:var(--bs-danger-active)}.btn-check:active+.btn.btn-outline.btn-outline-danger.dropdown-toggle:after,.btn-check:checked+.btn.btn-outline.btn-outline-danger.dropdown-toggle:after,.btn.btn-outline.btn-outline-danger.active.dropdown-toggle:after,.btn.btn-outline.btn-outline-danger.show.dropdown-toggle:after,.btn.btn-outline.btn-outline-danger:active:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-danger:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-danger:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-outline.btn-outline-danger.dropdown-toggle:after{color:var(--bs-danger-active)}.btn.btn-dark{color:var(--bs-dark-inverse);border-color:var(--bs-dark);background-color:var(--bs-dark)}.btn.btn-dark .svg-icon,.btn.btn-dark i{color:var(--bs-dark-inverse)}.btn.btn-dark.dropdown-toggle:after{color:var(--bs-dark-inverse)}.btn-check:active+.btn.btn-dark,.btn-check:checked+.btn.btn-dark,.btn.btn-dark.active,.btn.btn-dark.show,.btn.btn-dark:active:not(.btn-active),.btn.btn-dark:focus:not(.btn-active),.btn.btn-dark:hover:not(.btn-active),.show>.btn.btn-dark{color:var(--bs-dark-inverse);border-color:var(--bs-dark-active);background-color:var(--bs-dark-active)!important}.btn-check:active+.btn.btn-dark .svg-icon,.btn-check:active+.btn.btn-dark i,.btn-check:checked+.btn.btn-dark .svg-icon,.btn-check:checked+.btn.btn-dark i,.btn.btn-dark.active .svg-icon,.btn.btn-dark.active i,.btn.btn-dark.show .svg-icon,.btn.btn-dark.show i,.btn.btn-dark:active:not(.btn-active) .svg-icon,.btn.btn-dark:active:not(.btn-active) i,.btn.btn-dark:focus:not(.btn-active) .svg-icon,.btn.btn-dark:focus:not(.btn-active) i,.btn.btn-dark:hover:not(.btn-active) .svg-icon,.btn.btn-dark:hover:not(.btn-active) i,.show>.btn.btn-dark .svg-icon,.show>.btn.btn-dark i{color:var(--bs-dark-inverse)}.btn-check:active+.btn.btn-dark.dropdown-toggle:after,.btn-check:checked+.btn.btn-dark.dropdown-toggle:after,.btn.btn-dark.active.dropdown-toggle:after,.btn.btn-dark.show.dropdown-toggle:after,.btn.btn-dark:active:not(.btn-active).dropdown-toggle:after,.btn.btn-dark:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-dark:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-dark.dropdown-toggle:after{color:var(--bs-dark-inverse)}.btn.btn-light-dark{color:var(--bs-dark);border-color:var(--bs-dark-light);background-color:var(--bs-dark-light)}.btn.btn-light-dark .svg-icon,.btn.btn-light-dark i{color:var(--bs-dark)}.btn.btn-light-dark.dropdown-toggle:after{color:var(--bs-dark)}.btn-check:active+.btn.btn-light-dark,.btn-check:checked+.btn.btn-light-dark,.btn.btn-light-dark.active,.btn.btn-light-dark.show,.btn.btn-light-dark:active:not(.btn-active),.btn.btn-light-dark:focus:not(.btn-active),.btn.btn-light-dark:hover:not(.btn-active),.show>.btn.btn-light-dark{color:var(--bs-dark-inverse);border-color:var(--bs-dark);background-color:var(--bs-dark)!important}.btn-check:active+.btn.btn-light-dark .svg-icon,.btn-check:active+.btn.btn-light-dark i,.btn-check:checked+.btn.btn-light-dark .svg-icon,.btn-check:checked+.btn.btn-light-dark i,.btn.btn-light-dark.active .svg-icon,.btn.btn-light-dark.active i,.btn.btn-light-dark.show .svg-icon,.btn.btn-light-dark.show i,.btn.btn-light-dark:active:not(.btn-active) .svg-icon,.btn.btn-light-dark:active:not(.btn-active) i,.btn.btn-light-dark:focus:not(.btn-active) .svg-icon,.btn.btn-light-dark:focus:not(.btn-active) i,.btn.btn-light-dark:hover:not(.btn-active) .svg-icon,.btn.btn-light-dark:hover:not(.btn-active) i,.show>.btn.btn-light-dark .svg-icon,.show>.btn.btn-light-dark i{color:var(--bs-dark-inverse)}.btn-check:active+.btn.btn-light-dark.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-dark.dropdown-toggle:after,.btn.btn-light-dark.active.dropdown-toggle:after,.btn.btn-light-dark.show.dropdown-toggle:after,.btn.btn-light-dark:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-dark:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-dark:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-dark.dropdown-toggle:after{color:var(--bs-dark-inverse)}.btn.btn-bg-dark{border-color:var(--bs-dark);background-color:var(--bs-dark)}.btn-check:active+.btn.btn-active-dark,.btn-check:checked+.btn.btn-active-dark,.btn.btn-active-dark.active,.btn.btn-active-dark.show,.btn.btn-active-dark:active:not(.btn-active),.btn.btn-active-dark:focus:not(.btn-active),.btn.btn-active-dark:hover:not(.btn-active),.show>.btn.btn-active-dark{color:var(--bs-dark-inverse);border-color:var(--bs-dark);background-color:var(--bs-dark)!important}.btn-check:active+.btn.btn-active-dark .svg-icon,.btn-check:active+.btn.btn-active-dark i,.btn-check:checked+.btn.btn-active-dark .svg-icon,.btn-check:checked+.btn.btn-active-dark i,.btn.btn-active-dark.active .svg-icon,.btn.btn-active-dark.active i,.btn.btn-active-dark.show .svg-icon,.btn.btn-active-dark.show i,.btn.btn-active-dark:active:not(.btn-active) .svg-icon,.btn.btn-active-dark:active:not(.btn-active) i,.btn.btn-active-dark:focus:not(.btn-active) .svg-icon,.btn.btn-active-dark:focus:not(.btn-active) i,.btn.btn-active-dark:hover:not(.btn-active) .svg-icon,.btn.btn-active-dark:hover:not(.btn-active) i,.show>.btn.btn-active-dark .svg-icon,.show>.btn.btn-active-dark i{color:var(--bs-dark-inverse)}.btn-check:active+.btn.btn-active-dark.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-dark.dropdown-toggle:after,.btn.btn-active-dark.active.dropdown-toggle:after,.btn.btn-active-dark.show.dropdown-toggle:after,.btn.btn-active-dark:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-dark:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-dark:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-dark.dropdown-toggle:after{color:var(--bs-dark-inverse)}.btn-check:active+.btn.btn-active-light-dark,.btn-check:checked+.btn.btn-active-light-dark,.btn.btn-active-light-dark.active,.btn.btn-active-light-dark.show,.btn.btn-active-light-dark:active:not(.btn-active),.btn.btn-active-light-dark:focus:not(.btn-active),.btn.btn-active-light-dark:hover:not(.btn-active),.show>.btn.btn-active-light-dark{color:var(--bs-dark);border-color:var(--bs-dark-light);background-color:var(--bs-dark-light)!important}.btn-check:active+.btn.btn-active-light-dark .svg-icon,.btn-check:active+.btn.btn-active-light-dark i,.btn-check:checked+.btn.btn-active-light-dark .svg-icon,.btn-check:checked+.btn.btn-active-light-dark i,.btn.btn-active-light-dark.active .svg-icon,.btn.btn-active-light-dark.active i,.btn.btn-active-light-dark.show .svg-icon,.btn.btn-active-light-dark.show i,.btn.btn-active-light-dark:active:not(.btn-active) .svg-icon,.btn.btn-active-light-dark:active:not(.btn-active) i,.btn.btn-active-light-dark:focus:not(.btn-active) .svg-icon,.btn.btn-active-light-dark:focus:not(.btn-active) i,.btn.btn-active-light-dark:hover:not(.btn-active) .svg-icon,.btn.btn-active-light-dark:hover:not(.btn-active) i,.show>.btn.btn-active-light-dark .svg-icon,.show>.btn.btn-active-light-dark i{color:var(--bs-dark)}.btn-check:active+.btn.btn-active-light-dark.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-light-dark.dropdown-toggle:after,.btn.btn-active-light-dark.active.dropdown-toggle:after,.btn.btn-active-light-dark.show.dropdown-toggle:after,.btn.btn-active-light-dark:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-dark:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-light-dark:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-light-dark.dropdown-toggle:after{color:var(--bs-dark)}.btn.btn-outline.btn-outline-dark{color:var(--bs-dark);border-color:var(--bs-dark);background-color:transparent}.btn.btn-outline.btn-outline-dark .svg-icon,.btn.btn-outline.btn-outline-dark i{color:var(--bs-dark)}.btn.btn-outline.btn-outline-dark.dropdown-toggle:after{color:var(--bs-dark)}.btn-check:active+.btn.btn-outline.btn-outline-dark,.btn-check:checked+.btn.btn-outline.btn-outline-dark,.btn.btn-outline.btn-outline-dark.active,.btn.btn-outline.btn-outline-dark.show,.btn.btn-outline.btn-outline-dark:active:not(.btn-active),.btn.btn-outline.btn-outline-dark:focus:not(.btn-active),.btn.btn-outline.btn-outline-dark:hover:not(.btn-active),.show>.btn.btn-outline.btn-outline-dark{color:var(--bs-dark-active);border-color:var(--bs-dark);background-color:var(--bs-dark-light)!important}.btn-check:active+.btn.btn-outline.btn-outline-dark .svg-icon,.btn-check:active+.btn.btn-outline.btn-outline-dark i,.btn-check:checked+.btn.btn-outline.btn-outline-dark .svg-icon,.btn-check:checked+.btn.btn-outline.btn-outline-dark i,.btn.btn-outline.btn-outline-dark.active .svg-icon,.btn.btn-outline.btn-outline-dark.active i,.btn.btn-outline.btn-outline-dark.show .svg-icon,.btn.btn-outline.btn-outline-dark.show i,.btn.btn-outline.btn-outline-dark:active:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-dark:active:not(.btn-active) i,.btn.btn-outline.btn-outline-dark:focus:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-dark:focus:not(.btn-active) i,.btn.btn-outline.btn-outline-dark:hover:not(.btn-active) .svg-icon,.btn.btn-outline.btn-outline-dark:hover:not(.btn-active) i,.show>.btn.btn-outline.btn-outline-dark .svg-icon,.show>.btn.btn-outline.btn-outline-dark i{color:var(--bs-dark-active)}.btn-check:active+.btn.btn-outline.btn-outline-dark.dropdown-toggle:after,.btn-check:checked+.btn.btn-outline.btn-outline-dark.dropdown-toggle:after,.btn.btn-outline.btn-outline-dark.active.dropdown-toggle:after,.btn.btn-outline.btn-outline-dark.show.dropdown-toggle:after,.btn.btn-outline.btn-outline-dark:active:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-dark:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-outline.btn-outline-dark:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-outline.btn-outline-dark.dropdown-toggle:after{color:var(--bs-dark-active)}.btn.btn-color-white{color:var(--bs-text-white)}.btn.btn-color-white .svg-icon,.btn.btn-color-white i{color:var(--bs-text-white)}.btn.btn-color-white.dropdown-toggle:after{color:var(--bs-text-white)}.btn-check:active+.btn.btn-active-color-white,.btn-check:checked+.btn.btn-active-color-white,.btn.btn-active-color-white.active,.btn.btn-active-color-white.show,.btn.btn-active-color-white:active:not(.btn-active),.btn.btn-active-color-white:focus:not(.btn-active),.btn.btn-active-color-white:hover:not(.btn-active),.show>.btn.btn-active-color-white{color:var(--bs-text-white)}.btn-check:active+.btn.btn-active-color-white .svg-icon,.btn-check:active+.btn.btn-active-color-white i,.btn-check:checked+.btn.btn-active-color-white .svg-icon,.btn-check:checked+.btn.btn-active-color-white i,.btn.btn-active-color-white.active .svg-icon,.btn.btn-active-color-white.active i,.btn.btn-active-color-white.show .svg-icon,.btn.btn-active-color-white.show i,.btn.btn-active-color-white:active:not(.btn-active) .svg-icon,.btn.btn-active-color-white:active:not(.btn-active) i,.btn.btn-active-color-white:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-white:focus:not(.btn-active) i,.btn.btn-active-color-white:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-white:hover:not(.btn-active) i,.show>.btn.btn-active-color-white .svg-icon,.show>.btn.btn-active-color-white i{color:var(--bs-text-white)}.btn-check:active+.btn.btn-active-color-white.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-white.dropdown-toggle:after,.btn.btn-active-color-white.active.dropdown-toggle:after,.btn.btn-active-color-white.show.dropdown-toggle:after,.btn.btn-active-color-white:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-white:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-white:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-white.dropdown-toggle:after{color:var(--bs-text-white)}.btn.btn-icon-white .svg-icon,.btn.btn-icon-white i{color:var(--bs-text-white)}.btn.btn-icon-white.dropdown-toggle:after{color:var(--bs-text-white)}.btn-check:active+.btn.btn-active-icon-white .svg-icon,.btn-check:active+.btn.btn-active-icon-white i,.btn-check:checked+.btn.btn-active-icon-white .svg-icon,.btn-check:checked+.btn.btn-active-icon-white i,.btn.btn-active-icon-white.active .svg-icon,.btn.btn-active-icon-white.active i,.btn.btn-active-icon-white.show .svg-icon,.btn.btn-active-icon-white.show i,.btn.btn-active-icon-white:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-white:active:not(.btn-active) i,.btn.btn-active-icon-white:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-white:focus:not(.btn-active) i,.btn.btn-active-icon-white:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-white:hover:not(.btn-active) i,.show>.btn.btn-active-icon-white .svg-icon,.show>.btn.btn-active-icon-white i{color:var(--bs-text-white)}.btn-check:active+.btn.btn-active-icon-white.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-white.dropdown-toggle:after,.btn.btn-active-icon-white.active.dropdown-toggle:after,.btn.btn-active-icon-white.show.dropdown-toggle:after,.btn.btn-active-icon-white:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-white:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-white:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-white.dropdown-toggle:after{color:var(--bs-text-white)}.btn.btn-text-white{color:var(--bs-text-white)}.btn-check:active+.btn.btn-active-text-white,.btn-check:checked+.btn.btn-active-text-white,.btn.btn-active-text-white.active,.btn.btn-active-text-white.show,.btn.btn-active-text-white:active:not(.btn-active),.btn.btn-active-text-white:focus:not(.btn-active),.btn.btn-active-text-white:hover:not(.btn-active),.show>.btn.btn-active-text-white{color:var(--bs-text-white)}.btn.btn-color-primary{color:var(--bs-text-primary)}.btn.btn-color-primary .svg-icon,.btn.btn-color-primary i{color:var(--bs-text-primary)}.btn.btn-color-primary.dropdown-toggle:after{color:var(--bs-text-primary)}.btn-check:active+.btn.btn-active-color-primary,.btn-check:checked+.btn.btn-active-color-primary,.btn.btn-active-color-primary.active,.btn.btn-active-color-primary.show,.btn.btn-active-color-primary:active:not(.btn-active),.btn.btn-active-color-primary:focus:not(.btn-active),.btn.btn-active-color-primary:hover:not(.btn-active),.show>.btn.btn-active-color-primary{color:var(--bs-text-primary)}.btn-check:active+.btn.btn-active-color-primary .svg-icon,.btn-check:active+.btn.btn-active-color-primary i,.btn-check:checked+.btn.btn-active-color-primary .svg-icon,.btn-check:checked+.btn.btn-active-color-primary i,.btn.btn-active-color-primary.active .svg-icon,.btn.btn-active-color-primary.active i,.btn.btn-active-color-primary.show .svg-icon,.btn.btn-active-color-primary.show i,.btn.btn-active-color-primary:active:not(.btn-active) .svg-icon,.btn.btn-active-color-primary:active:not(.btn-active) i,.btn.btn-active-color-primary:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-primary:focus:not(.btn-active) i,.btn.btn-active-color-primary:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-primary:hover:not(.btn-active) i,.show>.btn.btn-active-color-primary .svg-icon,.show>.btn.btn-active-color-primary i{color:var(--bs-text-primary)}.btn-check:active+.btn.btn-active-color-primary.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-primary.dropdown-toggle:after,.btn.btn-active-color-primary.active.dropdown-toggle:after,.btn.btn-active-color-primary.show.dropdown-toggle:after,.btn.btn-active-color-primary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-primary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-primary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-primary.dropdown-toggle:after{color:var(--bs-text-primary)}.btn.btn-icon-primary .svg-icon,.btn.btn-icon-primary i{color:var(--bs-text-primary)}.btn.btn-icon-primary.dropdown-toggle:after{color:var(--bs-text-primary)}.btn-check:active+.btn.btn-active-icon-primary .svg-icon,.btn-check:active+.btn.btn-active-icon-primary i,.btn-check:checked+.btn.btn-active-icon-primary .svg-icon,.btn-check:checked+.btn.btn-active-icon-primary i,.btn.btn-active-icon-primary.active .svg-icon,.btn.btn-active-icon-primary.active i,.btn.btn-active-icon-primary.show .svg-icon,.btn.btn-active-icon-primary.show i,.btn.btn-active-icon-primary:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-primary:active:not(.btn-active) i,.btn.btn-active-icon-primary:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-primary:focus:not(.btn-active) i,.btn.btn-active-icon-primary:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-primary:hover:not(.btn-active) i,.show>.btn.btn-active-icon-primary .svg-icon,.show>.btn.btn-active-icon-primary i{color:var(--bs-text-primary)}.btn-check:active+.btn.btn-active-icon-primary.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-primary.dropdown-toggle:after,.btn.btn-active-icon-primary.active.dropdown-toggle:after,.btn.btn-active-icon-primary.show.dropdown-toggle:after,.btn.btn-active-icon-primary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-primary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-primary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-primary.dropdown-toggle:after{color:var(--bs-text-primary)}.btn.btn-text-primary{color:var(--bs-text-primary)}.btn-check:active+.btn.btn-active-text-primary,.btn-check:checked+.btn.btn-active-text-primary,.btn.btn-active-text-primary.active,.btn.btn-active-text-primary.show,.btn.btn-active-text-primary:active:not(.btn-active),.btn.btn-active-text-primary:focus:not(.btn-active),.btn.btn-active-text-primary:hover:not(.btn-active),.show>.btn.btn-active-text-primary{color:var(--bs-text-primary)}.btn.btn-color-secondary{color:var(--bs-text-secondary)}.btn.btn-color-secondary .svg-icon,.btn.btn-color-secondary i{color:var(--bs-text-secondary)}.btn.btn-color-secondary.dropdown-toggle:after{color:var(--bs-text-secondary)}.btn-check:active+.btn.btn-active-color-secondary,.btn-check:checked+.btn.btn-active-color-secondary,.btn.btn-active-color-secondary.active,.btn.btn-active-color-secondary.show,.btn.btn-active-color-secondary:active:not(.btn-active),.btn.btn-active-color-secondary:focus:not(.btn-active),.btn.btn-active-color-secondary:hover:not(.btn-active),.show>.btn.btn-active-color-secondary{color:var(--bs-text-secondary)}.btn-check:active+.btn.btn-active-color-secondary .svg-icon,.btn-check:active+.btn.btn-active-color-secondary i,.btn-check:checked+.btn.btn-active-color-secondary .svg-icon,.btn-check:checked+.btn.btn-active-color-secondary i,.btn.btn-active-color-secondary.active .svg-icon,.btn.btn-active-color-secondary.active i,.btn.btn-active-color-secondary.show .svg-icon,.btn.btn-active-color-secondary.show i,.btn.btn-active-color-secondary:active:not(.btn-active) .svg-icon,.btn.btn-active-color-secondary:active:not(.btn-active) i,.btn.btn-active-color-secondary:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-secondary:focus:not(.btn-active) i,.btn.btn-active-color-secondary:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-secondary:hover:not(.btn-active) i,.show>.btn.btn-active-color-secondary .svg-icon,.show>.btn.btn-active-color-secondary i{color:var(--bs-text-secondary)}.btn-check:active+.btn.btn-active-color-secondary.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-secondary.dropdown-toggle:after,.btn.btn-active-color-secondary.active.dropdown-toggle:after,.btn.btn-active-color-secondary.show.dropdown-toggle:after,.btn.btn-active-color-secondary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-secondary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-secondary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-secondary.dropdown-toggle:after{color:var(--bs-text-secondary)}.btn.btn-icon-secondary .svg-icon,.btn.btn-icon-secondary i{color:var(--bs-text-secondary)}.btn.btn-icon-secondary.dropdown-toggle:after{color:var(--bs-text-secondary)}.btn-check:active+.btn.btn-active-icon-secondary .svg-icon,.btn-check:active+.btn.btn-active-icon-secondary i,.btn-check:checked+.btn.btn-active-icon-secondary .svg-icon,.btn-check:checked+.btn.btn-active-icon-secondary i,.btn.btn-active-icon-secondary.active .svg-icon,.btn.btn-active-icon-secondary.active i,.btn.btn-active-icon-secondary.show .svg-icon,.btn.btn-active-icon-secondary.show i,.btn.btn-active-icon-secondary:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-secondary:active:not(.btn-active) i,.btn.btn-active-icon-secondary:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-secondary:focus:not(.btn-active) i,.btn.btn-active-icon-secondary:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-secondary:hover:not(.btn-active) i,.show>.btn.btn-active-icon-secondary .svg-icon,.show>.btn.btn-active-icon-secondary i{color:var(--bs-text-secondary)}.btn-check:active+.btn.btn-active-icon-secondary.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-secondary.dropdown-toggle:after,.btn.btn-active-icon-secondary.active.dropdown-toggle:after,.btn.btn-active-icon-secondary.show.dropdown-toggle:after,.btn.btn-active-icon-secondary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-secondary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-secondary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-secondary.dropdown-toggle:after{color:var(--bs-text-secondary)}.btn.btn-text-secondary{color:var(--bs-text-secondary)}.btn-check:active+.btn.btn-active-text-secondary,.btn-check:checked+.btn.btn-active-text-secondary,.btn.btn-active-text-secondary.active,.btn.btn-active-text-secondary.show,.btn.btn-active-text-secondary:active:not(.btn-active),.btn.btn-active-text-secondary:focus:not(.btn-active),.btn.btn-active-text-secondary:hover:not(.btn-active),.show>.btn.btn-active-text-secondary{color:var(--bs-text-secondary)}.btn.btn-color-light{color:var(--bs-text-light)}.btn.btn-color-light .svg-icon,.btn.btn-color-light i{color:var(--bs-text-light)}.btn.btn-color-light.dropdown-toggle:after{color:var(--bs-text-light)}.btn-check:active+.btn.btn-active-color-light,.btn-check:checked+.btn.btn-active-color-light,.btn.btn-active-color-light.active,.btn.btn-active-color-light.show,.btn.btn-active-color-light:active:not(.btn-active),.btn.btn-active-color-light:focus:not(.btn-active),.btn.btn-active-color-light:hover:not(.btn-active),.show>.btn.btn-active-color-light{color:var(--bs-text-light)}.btn-check:active+.btn.btn-active-color-light .svg-icon,.btn-check:active+.btn.btn-active-color-light i,.btn-check:checked+.btn.btn-active-color-light .svg-icon,.btn-check:checked+.btn.btn-active-color-light i,.btn.btn-active-color-light.active .svg-icon,.btn.btn-active-color-light.active i,.btn.btn-active-color-light.show .svg-icon,.btn.btn-active-color-light.show i,.btn.btn-active-color-light:active:not(.btn-active) .svg-icon,.btn.btn-active-color-light:active:not(.btn-active) i,.btn.btn-active-color-light:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-light:focus:not(.btn-active) i,.btn.btn-active-color-light:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-light:hover:not(.btn-active) i,.show>.btn.btn-active-color-light .svg-icon,.show>.btn.btn-active-color-light i{color:var(--bs-text-light)}.btn-check:active+.btn.btn-active-color-light.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-light.dropdown-toggle:after,.btn.btn-active-color-light.active.dropdown-toggle:after,.btn.btn-active-color-light.show.dropdown-toggle:after,.btn.btn-active-color-light:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-light:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-light:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-light.dropdown-toggle:after{color:var(--bs-text-light)}.btn.btn-icon-light .svg-icon,.btn.btn-icon-light i{color:var(--bs-text-light)}.btn.btn-icon-light.dropdown-toggle:after{color:var(--bs-text-light)}.btn-check:active+.btn.btn-active-icon-light .svg-icon,.btn-check:active+.btn.btn-active-icon-light i,.btn-check:checked+.btn.btn-active-icon-light .svg-icon,.btn-check:checked+.btn.btn-active-icon-light i,.btn.btn-active-icon-light.active .svg-icon,.btn.btn-active-icon-light.active i,.btn.btn-active-icon-light.show .svg-icon,.btn.btn-active-icon-light.show i,.btn.btn-active-icon-light:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-light:active:not(.btn-active) i,.btn.btn-active-icon-light:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-light:focus:not(.btn-active) i,.btn.btn-active-icon-light:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-light:hover:not(.btn-active) i,.show>.btn.btn-active-icon-light .svg-icon,.show>.btn.btn-active-icon-light i{color:var(--bs-text-light)}.btn-check:active+.btn.btn-active-icon-light.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-light.dropdown-toggle:after,.btn.btn-active-icon-light.active.dropdown-toggle:after,.btn.btn-active-icon-light.show.dropdown-toggle:after,.btn.btn-active-icon-light:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-light:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-light:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-light.dropdown-toggle:after{color:var(--bs-text-light)}.btn.btn-text-light{color:var(--bs-text-light)}.btn-check:active+.btn.btn-active-text-light,.btn-check:checked+.btn.btn-active-text-light,.btn.btn-active-text-light.active,.btn.btn-active-text-light.show,.btn.btn-active-text-light:active:not(.btn-active),.btn.btn-active-text-light:focus:not(.btn-active),.btn.btn-active-text-light:hover:not(.btn-active),.show>.btn.btn-active-text-light{color:var(--bs-text-light)}.btn.btn-color-success{color:var(--bs-text-success)}.btn.btn-color-success .svg-icon,.btn.btn-color-success i{color:var(--bs-text-success)}.btn.btn-color-success.dropdown-toggle:after{color:var(--bs-text-success)}.btn-check:active+.btn.btn-active-color-success,.btn-check:checked+.btn.btn-active-color-success,.btn.btn-active-color-success.active,.btn.btn-active-color-success.show,.btn.btn-active-color-success:active:not(.btn-active),.btn.btn-active-color-success:focus:not(.btn-active),.btn.btn-active-color-success:hover:not(.btn-active),.show>.btn.btn-active-color-success{color:var(--bs-text-success)}.btn-check:active+.btn.btn-active-color-success .svg-icon,.btn-check:active+.btn.btn-active-color-success i,.btn-check:checked+.btn.btn-active-color-success .svg-icon,.btn-check:checked+.btn.btn-active-color-success i,.btn.btn-active-color-success.active .svg-icon,.btn.btn-active-color-success.active i,.btn.btn-active-color-success.show .svg-icon,.btn.btn-active-color-success.show i,.btn.btn-active-color-success:active:not(.btn-active) .svg-icon,.btn.btn-active-color-success:active:not(.btn-active) i,.btn.btn-active-color-success:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-success:focus:not(.btn-active) i,.btn.btn-active-color-success:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-success:hover:not(.btn-active) i,.show>.btn.btn-active-color-success .svg-icon,.show>.btn.btn-active-color-success i{color:var(--bs-text-success)}.btn-check:active+.btn.btn-active-color-success.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-success.dropdown-toggle:after,.btn.btn-active-color-success.active.dropdown-toggle:after,.btn.btn-active-color-success.show.dropdown-toggle:after,.btn.btn-active-color-success:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-success:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-success:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-success.dropdown-toggle:after{color:var(--bs-text-success)}.btn.btn-icon-success .svg-icon,.btn.btn-icon-success i{color:var(--bs-text-success)}.btn.btn-icon-success.dropdown-toggle:after{color:var(--bs-text-success)}.btn-check:active+.btn.btn-active-icon-success .svg-icon,.btn-check:active+.btn.btn-active-icon-success i,.btn-check:checked+.btn.btn-active-icon-success .svg-icon,.btn-check:checked+.btn.btn-active-icon-success i,.btn.btn-active-icon-success.active .svg-icon,.btn.btn-active-icon-success.active i,.btn.btn-active-icon-success.show .svg-icon,.btn.btn-active-icon-success.show i,.btn.btn-active-icon-success:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-success:active:not(.btn-active) i,.btn.btn-active-icon-success:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-success:focus:not(.btn-active) i,.btn.btn-active-icon-success:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-success:hover:not(.btn-active) i,.show>.btn.btn-active-icon-success .svg-icon,.show>.btn.btn-active-icon-success i{color:var(--bs-text-success)}.btn-check:active+.btn.btn-active-icon-success.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-success.dropdown-toggle:after,.btn.btn-active-icon-success.active.dropdown-toggle:after,.btn.btn-active-icon-success.show.dropdown-toggle:after,.btn.btn-active-icon-success:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-success:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-success:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-success.dropdown-toggle:after{color:var(--bs-text-success)}.btn.btn-text-success{color:var(--bs-text-success)}.btn-check:active+.btn.btn-active-text-success,.btn-check:checked+.btn.btn-active-text-success,.btn.btn-active-text-success.active,.btn.btn-active-text-success.show,.btn.btn-active-text-success:active:not(.btn-active),.btn.btn-active-text-success:focus:not(.btn-active),.btn.btn-active-text-success:hover:not(.btn-active),.show>.btn.btn-active-text-success{color:var(--bs-text-success)}.btn.btn-color-info{color:var(--bs-text-info)}.btn.btn-color-info .svg-icon,.btn.btn-color-info i{color:var(--bs-text-info)}.btn.btn-color-info.dropdown-toggle:after{color:var(--bs-text-info)}.btn-check:active+.btn.btn-active-color-info,.btn-check:checked+.btn.btn-active-color-info,.btn.btn-active-color-info.active,.btn.btn-active-color-info.show,.btn.btn-active-color-info:active:not(.btn-active),.btn.btn-active-color-info:focus:not(.btn-active),.btn.btn-active-color-info:hover:not(.btn-active),.show>.btn.btn-active-color-info{color:var(--bs-text-info)}.btn-check:active+.btn.btn-active-color-info .svg-icon,.btn-check:active+.btn.btn-active-color-info i,.btn-check:checked+.btn.btn-active-color-info .svg-icon,.btn-check:checked+.btn.btn-active-color-info i,.btn.btn-active-color-info.active .svg-icon,.btn.btn-active-color-info.active i,.btn.btn-active-color-info.show .svg-icon,.btn.btn-active-color-info.show i,.btn.btn-active-color-info:active:not(.btn-active) .svg-icon,.btn.btn-active-color-info:active:not(.btn-active) i,.btn.btn-active-color-info:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-info:focus:not(.btn-active) i,.btn.btn-active-color-info:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-info:hover:not(.btn-active) i,.show>.btn.btn-active-color-info .svg-icon,.show>.btn.btn-active-color-info i{color:var(--bs-text-info)}.btn-check:active+.btn.btn-active-color-info.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-info.dropdown-toggle:after,.btn.btn-active-color-info.active.dropdown-toggle:after,.btn.btn-active-color-info.show.dropdown-toggle:after,.btn.btn-active-color-info:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-info:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-info:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-info.dropdown-toggle:after{color:var(--bs-text-info)}.btn.btn-icon-info .svg-icon,.btn.btn-icon-info i{color:var(--bs-text-info)}.btn.btn-icon-info.dropdown-toggle:after{color:var(--bs-text-info)}.btn-check:active+.btn.btn-active-icon-info .svg-icon,.btn-check:active+.btn.btn-active-icon-info i,.btn-check:checked+.btn.btn-active-icon-info .svg-icon,.btn-check:checked+.btn.btn-active-icon-info i,.btn.btn-active-icon-info.active .svg-icon,.btn.btn-active-icon-info.active i,.btn.btn-active-icon-info.show .svg-icon,.btn.btn-active-icon-info.show i,.btn.btn-active-icon-info:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-info:active:not(.btn-active) i,.btn.btn-active-icon-info:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-info:focus:not(.btn-active) i,.btn.btn-active-icon-info:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-info:hover:not(.btn-active) i,.show>.btn.btn-active-icon-info .svg-icon,.show>.btn.btn-active-icon-info i{color:var(--bs-text-info)}.btn-check:active+.btn.btn-active-icon-info.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-info.dropdown-toggle:after,.btn.btn-active-icon-info.active.dropdown-toggle:after,.btn.btn-active-icon-info.show.dropdown-toggle:after,.btn.btn-active-icon-info:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-info:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-info:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-info.dropdown-toggle:after{color:var(--bs-text-info)}.btn.btn-text-info{color:var(--bs-text-info)}.btn-check:active+.btn.btn-active-text-info,.btn-check:checked+.btn.btn-active-text-info,.btn.btn-active-text-info.active,.btn.btn-active-text-info.show,.btn.btn-active-text-info:active:not(.btn-active),.btn.btn-active-text-info:focus:not(.btn-active),.btn.btn-active-text-info:hover:not(.btn-active),.show>.btn.btn-active-text-info{color:var(--bs-text-info)}.btn.btn-color-warning{color:var(--bs-text-warning)}.btn.btn-color-warning .svg-icon,.btn.btn-color-warning i{color:var(--bs-text-warning)}.btn.btn-color-warning.dropdown-toggle:after{color:var(--bs-text-warning)}.btn-check:active+.btn.btn-active-color-warning,.btn-check:checked+.btn.btn-active-color-warning,.btn.btn-active-color-warning.active,.btn.btn-active-color-warning.show,.btn.btn-active-color-warning:active:not(.btn-active),.btn.btn-active-color-warning:focus:not(.btn-active),.btn.btn-active-color-warning:hover:not(.btn-active),.show>.btn.btn-active-color-warning{color:var(--bs-text-warning)}.btn-check:active+.btn.btn-active-color-warning .svg-icon,.btn-check:active+.btn.btn-active-color-warning i,.btn-check:checked+.btn.btn-active-color-warning .svg-icon,.btn-check:checked+.btn.btn-active-color-warning i,.btn.btn-active-color-warning.active .svg-icon,.btn.btn-active-color-warning.active i,.btn.btn-active-color-warning.show .svg-icon,.btn.btn-active-color-warning.show i,.btn.btn-active-color-warning:active:not(.btn-active) .svg-icon,.btn.btn-active-color-warning:active:not(.btn-active) i,.btn.btn-active-color-warning:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-warning:focus:not(.btn-active) i,.btn.btn-active-color-warning:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-warning:hover:not(.btn-active) i,.show>.btn.btn-active-color-warning .svg-icon,.show>.btn.btn-active-color-warning i{color:var(--bs-text-warning)}.btn-check:active+.btn.btn-active-color-warning.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-warning.dropdown-toggle:after,.btn.btn-active-color-warning.active.dropdown-toggle:after,.btn.btn-active-color-warning.show.dropdown-toggle:after,.btn.btn-active-color-warning:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-warning:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-warning:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-warning.dropdown-toggle:after{color:var(--bs-text-warning)}.btn.btn-icon-warning .svg-icon,.btn.btn-icon-warning i{color:var(--bs-text-warning)}.btn.btn-icon-warning.dropdown-toggle:after{color:var(--bs-text-warning)}.btn-check:active+.btn.btn-active-icon-warning .svg-icon,.btn-check:active+.btn.btn-active-icon-warning i,.btn-check:checked+.btn.btn-active-icon-warning .svg-icon,.btn-check:checked+.btn.btn-active-icon-warning i,.btn.btn-active-icon-warning.active .svg-icon,.btn.btn-active-icon-warning.active i,.btn.btn-active-icon-warning.show .svg-icon,.btn.btn-active-icon-warning.show i,.btn.btn-active-icon-warning:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-warning:active:not(.btn-active) i,.btn.btn-active-icon-warning:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-warning:focus:not(.btn-active) i,.btn.btn-active-icon-warning:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-warning:hover:not(.btn-active) i,.show>.btn.btn-active-icon-warning .svg-icon,.show>.btn.btn-active-icon-warning i{color:var(--bs-text-warning)}.btn-check:active+.btn.btn-active-icon-warning.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-warning.dropdown-toggle:after,.btn.btn-active-icon-warning.active.dropdown-toggle:after,.btn.btn-active-icon-warning.show.dropdown-toggle:after,.btn.btn-active-icon-warning:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-warning:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-warning:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-warning.dropdown-toggle:after{color:var(--bs-text-warning)}.btn.btn-text-warning{color:var(--bs-text-warning)}.btn-check:active+.btn.btn-active-text-warning,.btn-check:checked+.btn.btn-active-text-warning,.btn.btn-active-text-warning.active,.btn.btn-active-text-warning.show,.btn.btn-active-text-warning:active:not(.btn-active),.btn.btn-active-text-warning:focus:not(.btn-active),.btn.btn-active-text-warning:hover:not(.btn-active),.show>.btn.btn-active-text-warning{color:var(--bs-text-warning)}.btn.btn-color-danger{color:var(--bs-text-danger)}.btn.btn-color-danger .svg-icon,.btn.btn-color-danger i{color:var(--bs-text-danger)}.btn.btn-color-danger.dropdown-toggle:after{color:var(--bs-text-danger)}.btn-check:active+.btn.btn-active-color-danger,.btn-check:checked+.btn.btn-active-color-danger,.btn.btn-active-color-danger.active,.btn.btn-active-color-danger.show,.btn.btn-active-color-danger:active:not(.btn-active),.btn.btn-active-color-danger:focus:not(.btn-active),.btn.btn-active-color-danger:hover:not(.btn-active),.show>.btn.btn-active-color-danger{color:var(--bs-text-danger)}.btn-check:active+.btn.btn-active-color-danger .svg-icon,.btn-check:active+.btn.btn-active-color-danger i,.btn-check:checked+.btn.btn-active-color-danger .svg-icon,.btn-check:checked+.btn.btn-active-color-danger i,.btn.btn-active-color-danger.active .svg-icon,.btn.btn-active-color-danger.active i,.btn.btn-active-color-danger.show .svg-icon,.btn.btn-active-color-danger.show i,.btn.btn-active-color-danger:active:not(.btn-active) .svg-icon,.btn.btn-active-color-danger:active:not(.btn-active) i,.btn.btn-active-color-danger:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-danger:focus:not(.btn-active) i,.btn.btn-active-color-danger:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-danger:hover:not(.btn-active) i,.show>.btn.btn-active-color-danger .svg-icon,.show>.btn.btn-active-color-danger i{color:var(--bs-text-danger)}.btn-check:active+.btn.btn-active-color-danger.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-danger.dropdown-toggle:after,.btn.btn-active-color-danger.active.dropdown-toggle:after,.btn.btn-active-color-danger.show.dropdown-toggle:after,.btn.btn-active-color-danger:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-danger:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-danger:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-danger.dropdown-toggle:after{color:var(--bs-text-danger)}.btn.btn-icon-danger .svg-icon,.btn.btn-icon-danger i{color:var(--bs-text-danger)}.btn.btn-icon-danger.dropdown-toggle:after{color:var(--bs-text-danger)}.btn-check:active+.btn.btn-active-icon-danger .svg-icon,.btn-check:active+.btn.btn-active-icon-danger i,.btn-check:checked+.btn.btn-active-icon-danger .svg-icon,.btn-check:checked+.btn.btn-active-icon-danger i,.btn.btn-active-icon-danger.active .svg-icon,.btn.btn-active-icon-danger.active i,.btn.btn-active-icon-danger.show .svg-icon,.btn.btn-active-icon-danger.show i,.btn.btn-active-icon-danger:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-danger:active:not(.btn-active) i,.btn.btn-active-icon-danger:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-danger:focus:not(.btn-active) i,.btn.btn-active-icon-danger:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-danger:hover:not(.btn-active) i,.show>.btn.btn-active-icon-danger .svg-icon,.show>.btn.btn-active-icon-danger i{color:var(--bs-text-danger)}.btn-check:active+.btn.btn-active-icon-danger.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-danger.dropdown-toggle:after,.btn.btn-active-icon-danger.active.dropdown-toggle:after,.btn.btn-active-icon-danger.show.dropdown-toggle:after,.btn.btn-active-icon-danger:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-danger:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-danger:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-danger.dropdown-toggle:after{color:var(--bs-text-danger)}.btn.btn-text-danger{color:var(--bs-text-danger)}.btn-check:active+.btn.btn-active-text-danger,.btn-check:checked+.btn.btn-active-text-danger,.btn.btn-active-text-danger.active,.btn.btn-active-text-danger.show,.btn.btn-active-text-danger:active:not(.btn-active),.btn.btn-active-text-danger:focus:not(.btn-active),.btn.btn-active-text-danger:hover:not(.btn-active),.show>.btn.btn-active-text-danger{color:var(--bs-text-danger)}.btn.btn-color-dark{color:var(--bs-text-dark)}.btn.btn-color-dark .svg-icon,.btn.btn-color-dark i{color:var(--bs-text-dark)}.btn.btn-color-dark.dropdown-toggle:after{color:var(--bs-text-dark)}.btn-check:active+.btn.btn-active-color-dark,.btn-check:checked+.btn.btn-active-color-dark,.btn.btn-active-color-dark.active,.btn.btn-active-color-dark.show,.btn.btn-active-color-dark:active:not(.btn-active),.btn.btn-active-color-dark:focus:not(.btn-active),.btn.btn-active-color-dark:hover:not(.btn-active),.show>.btn.btn-active-color-dark{color:var(--bs-text-dark)}.btn-check:active+.btn.btn-active-color-dark .svg-icon,.btn-check:active+.btn.btn-active-color-dark i,.btn-check:checked+.btn.btn-active-color-dark .svg-icon,.btn-check:checked+.btn.btn-active-color-dark i,.btn.btn-active-color-dark.active .svg-icon,.btn.btn-active-color-dark.active i,.btn.btn-active-color-dark.show .svg-icon,.btn.btn-active-color-dark.show i,.btn.btn-active-color-dark:active:not(.btn-active) .svg-icon,.btn.btn-active-color-dark:active:not(.btn-active) i,.btn.btn-active-color-dark:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-dark:focus:not(.btn-active) i,.btn.btn-active-color-dark:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-dark:hover:not(.btn-active) i,.show>.btn.btn-active-color-dark .svg-icon,.show>.btn.btn-active-color-dark i{color:var(--bs-text-dark)}.btn-check:active+.btn.btn-active-color-dark.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-dark.dropdown-toggle:after,.btn.btn-active-color-dark.active.dropdown-toggle:after,.btn.btn-active-color-dark.show.dropdown-toggle:after,.btn.btn-active-color-dark:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-dark:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-dark:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-dark.dropdown-toggle:after{color:var(--bs-text-dark)}.btn.btn-icon-dark .svg-icon,.btn.btn-icon-dark i{color:var(--bs-text-dark)}.btn.btn-icon-dark.dropdown-toggle:after{color:var(--bs-text-dark)}.btn-check:active+.btn.btn-active-icon-dark .svg-icon,.btn-check:active+.btn.btn-active-icon-dark i,.btn-check:checked+.btn.btn-active-icon-dark .svg-icon,.btn-check:checked+.btn.btn-active-icon-dark i,.btn.btn-active-icon-dark.active .svg-icon,.btn.btn-active-icon-dark.active i,.btn.btn-active-icon-dark.show .svg-icon,.btn.btn-active-icon-dark.show i,.btn.btn-active-icon-dark:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-dark:active:not(.btn-active) i,.btn.btn-active-icon-dark:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-dark:focus:not(.btn-active) i,.btn.btn-active-icon-dark:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-dark:hover:not(.btn-active) i,.show>.btn.btn-active-icon-dark .svg-icon,.show>.btn.btn-active-icon-dark i{color:var(--bs-text-dark)}.btn-check:active+.btn.btn-active-icon-dark.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-dark.dropdown-toggle:after,.btn.btn-active-icon-dark.active.dropdown-toggle:after,.btn.btn-active-icon-dark.show.dropdown-toggle:after,.btn.btn-active-icon-dark:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-dark:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-dark:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-dark.dropdown-toggle:after{color:var(--bs-text-dark)}.btn.btn-text-dark{color:var(--bs-text-dark)}.btn-check:active+.btn.btn-active-text-dark,.btn-check:checked+.btn.btn-active-text-dark,.btn.btn-active-text-dark.active,.btn.btn-active-text-dark.show,.btn.btn-active-text-dark:active:not(.btn-active),.btn.btn-active-text-dark:focus:not(.btn-active),.btn.btn-active-text-dark:hover:not(.btn-active),.show>.btn.btn-active-text-dark{color:var(--bs-text-dark)}.btn.btn-color-muted{color:var(--bs-text-muted)}.btn.btn-color-muted .svg-icon,.btn.btn-color-muted i{color:var(--bs-text-muted)}.btn.btn-color-muted.dropdown-toggle:after{color:var(--bs-text-muted)}.btn-check:active+.btn.btn-active-color-muted,.btn-check:checked+.btn.btn-active-color-muted,.btn.btn-active-color-muted.active,.btn.btn-active-color-muted.show,.btn.btn-active-color-muted:active:not(.btn-active),.btn.btn-active-color-muted:focus:not(.btn-active),.btn.btn-active-color-muted:hover:not(.btn-active),.show>.btn.btn-active-color-muted{color:var(--bs-text-muted)}.btn-check:active+.btn.btn-active-color-muted .svg-icon,.btn-check:active+.btn.btn-active-color-muted i,.btn-check:checked+.btn.btn-active-color-muted .svg-icon,.btn-check:checked+.btn.btn-active-color-muted i,.btn.btn-active-color-muted.active .svg-icon,.btn.btn-active-color-muted.active i,.btn.btn-active-color-muted.show .svg-icon,.btn.btn-active-color-muted.show i,.btn.btn-active-color-muted:active:not(.btn-active) .svg-icon,.btn.btn-active-color-muted:active:not(.btn-active) i,.btn.btn-active-color-muted:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-muted:focus:not(.btn-active) i,.btn.btn-active-color-muted:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-muted:hover:not(.btn-active) i,.show>.btn.btn-active-color-muted .svg-icon,.show>.btn.btn-active-color-muted i{color:var(--bs-text-muted)}.btn-check:active+.btn.btn-active-color-muted.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-muted.dropdown-toggle:after,.btn.btn-active-color-muted.active.dropdown-toggle:after,.btn.btn-active-color-muted.show.dropdown-toggle:after,.btn.btn-active-color-muted:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-muted:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-muted:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-muted.dropdown-toggle:after{color:var(--bs-text-muted)}.btn.btn-icon-muted .svg-icon,.btn.btn-icon-muted i{color:var(--bs-text-muted)}.btn.btn-icon-muted.dropdown-toggle:after{color:var(--bs-text-muted)}.btn-check:active+.btn.btn-active-icon-muted .svg-icon,.btn-check:active+.btn.btn-active-icon-muted i,.btn-check:checked+.btn.btn-active-icon-muted .svg-icon,.btn-check:checked+.btn.btn-active-icon-muted i,.btn.btn-active-icon-muted.active .svg-icon,.btn.btn-active-icon-muted.active i,.btn.btn-active-icon-muted.show .svg-icon,.btn.btn-active-icon-muted.show i,.btn.btn-active-icon-muted:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-muted:active:not(.btn-active) i,.btn.btn-active-icon-muted:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-muted:focus:not(.btn-active) i,.btn.btn-active-icon-muted:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-muted:hover:not(.btn-active) i,.show>.btn.btn-active-icon-muted .svg-icon,.show>.btn.btn-active-icon-muted i{color:var(--bs-text-muted)}.btn-check:active+.btn.btn-active-icon-muted.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-muted.dropdown-toggle:after,.btn.btn-active-icon-muted.active.dropdown-toggle:after,.btn.btn-active-icon-muted.show.dropdown-toggle:after,.btn.btn-active-icon-muted:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-muted:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-muted:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-muted.dropdown-toggle:after{color:var(--bs-text-muted)}.btn.btn-text-muted{color:var(--bs-text-muted)}.btn-check:active+.btn.btn-active-text-muted,.btn-check:checked+.btn.btn-active-text-muted,.btn.btn-active-text-muted.active,.btn.btn-active-text-muted.show,.btn.btn-active-text-muted:active:not(.btn-active),.btn.btn-active-text-muted:focus:not(.btn-active),.btn.btn-active-text-muted:hover:not(.btn-active),.show>.btn.btn-active-text-muted{color:var(--bs-text-muted)}.btn.btn-color-gray-100{color:var(--bs-text-gray-100)}.btn.btn-color-gray-100 .svg-icon,.btn.btn-color-gray-100 i{color:var(--bs-text-gray-100)}.btn.btn-color-gray-100.dropdown-toggle:after{color:var(--bs-text-gray-100)}.btn-check:active+.btn.btn-active-color-gray-100,.btn-check:checked+.btn.btn-active-color-gray-100,.btn.btn-active-color-gray-100.active,.btn.btn-active-color-gray-100.show,.btn.btn-active-color-gray-100:active:not(.btn-active),.btn.btn-active-color-gray-100:focus:not(.btn-active),.btn.btn-active-color-gray-100:hover:not(.btn-active),.show>.btn.btn-active-color-gray-100{color:var(--bs-text-gray-100)}.btn-check:active+.btn.btn-active-color-gray-100 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-100 i,.btn-check:checked+.btn.btn-active-color-gray-100 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-100 i,.btn.btn-active-color-gray-100.active .svg-icon,.btn.btn-active-color-gray-100.active i,.btn.btn-active-color-gray-100.show .svg-icon,.btn.btn-active-color-gray-100.show i,.btn.btn-active-color-gray-100:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-100:active:not(.btn-active) i,.btn.btn-active-color-gray-100:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-100:focus:not(.btn-active) i,.btn.btn-active-color-gray-100:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-100:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-100 .svg-icon,.show>.btn.btn-active-color-gray-100 i{color:var(--bs-text-gray-100)}.btn-check:active+.btn.btn-active-color-gray-100.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-100.dropdown-toggle:after,.btn.btn-active-color-gray-100.active.dropdown-toggle:after,.btn.btn-active-color-gray-100.show.dropdown-toggle:after,.btn.btn-active-color-gray-100:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-100:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-100:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-100.dropdown-toggle:after{color:var(--bs-text-gray-100)}.btn.btn-icon-gray-100 .svg-icon,.btn.btn-icon-gray-100 i{color:var(--bs-text-gray-100)}.btn.btn-icon-gray-100.dropdown-toggle:after{color:var(--bs-text-gray-100)}.btn-check:active+.btn.btn-active-icon-gray-100 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-100 i,.btn-check:checked+.btn.btn-active-icon-gray-100 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-100 i,.btn.btn-active-icon-gray-100.active .svg-icon,.btn.btn-active-icon-gray-100.active i,.btn.btn-active-icon-gray-100.show .svg-icon,.btn.btn-active-icon-gray-100.show i,.btn.btn-active-icon-gray-100:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-100:active:not(.btn-active) i,.btn.btn-active-icon-gray-100:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-100:focus:not(.btn-active) i,.btn.btn-active-icon-gray-100:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-100:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-100 .svg-icon,.show>.btn.btn-active-icon-gray-100 i{color:var(--bs-text-gray-100)}.btn-check:active+.btn.btn-active-icon-gray-100.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-100.dropdown-toggle:after,.btn.btn-active-icon-gray-100.active.dropdown-toggle:after,.btn.btn-active-icon-gray-100.show.dropdown-toggle:after,.btn.btn-active-icon-gray-100:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-100:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-100:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-100.dropdown-toggle:after{color:var(--bs-text-gray-100)}.btn.btn-text-gray-100{color:var(--bs-text-gray-100)}.btn-check:active+.btn.btn-active-text-gray-100,.btn-check:checked+.btn.btn-active-text-gray-100,.btn.btn-active-text-gray-100.active,.btn.btn-active-text-gray-100.show,.btn.btn-active-text-gray-100:active:not(.btn-active),.btn.btn-active-text-gray-100:focus:not(.btn-active),.btn.btn-active-text-gray-100:hover:not(.btn-active),.show>.btn.btn-active-text-gray-100{color:var(--bs-text-gray-100)}.btn.btn-color-gray-200{color:var(--bs-text-gray-200)}.btn.btn-color-gray-200 .svg-icon,.btn.btn-color-gray-200 i{color:var(--bs-text-gray-200)}.btn.btn-color-gray-200.dropdown-toggle:after{color:var(--bs-text-gray-200)}.btn-check:active+.btn.btn-active-color-gray-200,.btn-check:checked+.btn.btn-active-color-gray-200,.btn.btn-active-color-gray-200.active,.btn.btn-active-color-gray-200.show,.btn.btn-active-color-gray-200:active:not(.btn-active),.btn.btn-active-color-gray-200:focus:not(.btn-active),.btn.btn-active-color-gray-200:hover:not(.btn-active),.show>.btn.btn-active-color-gray-200{color:var(--bs-text-gray-200)}.btn-check:active+.btn.btn-active-color-gray-200 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-200 i,.btn-check:checked+.btn.btn-active-color-gray-200 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-200 i,.btn.btn-active-color-gray-200.active .svg-icon,.btn.btn-active-color-gray-200.active i,.btn.btn-active-color-gray-200.show .svg-icon,.btn.btn-active-color-gray-200.show i,.btn.btn-active-color-gray-200:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-200:active:not(.btn-active) i,.btn.btn-active-color-gray-200:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-200:focus:not(.btn-active) i,.btn.btn-active-color-gray-200:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-200:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-200 .svg-icon,.show>.btn.btn-active-color-gray-200 i{color:var(--bs-text-gray-200)}.btn-check:active+.btn.btn-active-color-gray-200.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-200.dropdown-toggle:after,.btn.btn-active-color-gray-200.active.dropdown-toggle:after,.btn.btn-active-color-gray-200.show.dropdown-toggle:after,.btn.btn-active-color-gray-200:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-200:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-200:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-200.dropdown-toggle:after{color:var(--bs-text-gray-200)}.btn.btn-icon-gray-200 .svg-icon,.btn.btn-icon-gray-200 i{color:var(--bs-text-gray-200)}.btn.btn-icon-gray-200.dropdown-toggle:after{color:var(--bs-text-gray-200)}.btn-check:active+.btn.btn-active-icon-gray-200 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-200 i,.btn-check:checked+.btn.btn-active-icon-gray-200 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-200 i,.btn.btn-active-icon-gray-200.active .svg-icon,.btn.btn-active-icon-gray-200.active i,.btn.btn-active-icon-gray-200.show .svg-icon,.btn.btn-active-icon-gray-200.show i,.btn.btn-active-icon-gray-200:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-200:active:not(.btn-active) i,.btn.btn-active-icon-gray-200:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-200:focus:not(.btn-active) i,.btn.btn-active-icon-gray-200:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-200:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-200 .svg-icon,.show>.btn.btn-active-icon-gray-200 i{color:var(--bs-text-gray-200)}.btn-check:active+.btn.btn-active-icon-gray-200.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-200.dropdown-toggle:after,.btn.btn-active-icon-gray-200.active.dropdown-toggle:after,.btn.btn-active-icon-gray-200.show.dropdown-toggle:after,.btn.btn-active-icon-gray-200:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-200:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-200:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-200.dropdown-toggle:after{color:var(--bs-text-gray-200)}.btn.btn-text-gray-200{color:var(--bs-text-gray-200)}.btn-check:active+.btn.btn-active-text-gray-200,.btn-check:checked+.btn.btn-active-text-gray-200,.btn.btn-active-text-gray-200.active,.btn.btn-active-text-gray-200.show,.btn.btn-active-text-gray-200:active:not(.btn-active),.btn.btn-active-text-gray-200:focus:not(.btn-active),.btn.btn-active-text-gray-200:hover:not(.btn-active),.show>.btn.btn-active-text-gray-200{color:var(--bs-text-gray-200)}.btn.btn-color-gray-300{color:var(--bs-text-gray-300)}.btn.btn-color-gray-300 .svg-icon,.btn.btn-color-gray-300 i{color:var(--bs-text-gray-300)}.btn.btn-color-gray-300.dropdown-toggle:after{color:var(--bs-text-gray-300)}.btn-check:active+.btn.btn-active-color-gray-300,.btn-check:checked+.btn.btn-active-color-gray-300,.btn.btn-active-color-gray-300.active,.btn.btn-active-color-gray-300.show,.btn.btn-active-color-gray-300:active:not(.btn-active),.btn.btn-active-color-gray-300:focus:not(.btn-active),.btn.btn-active-color-gray-300:hover:not(.btn-active),.show>.btn.btn-active-color-gray-300{color:var(--bs-text-gray-300)}.btn-check:active+.btn.btn-active-color-gray-300 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-300 i,.btn-check:checked+.btn.btn-active-color-gray-300 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-300 i,.btn.btn-active-color-gray-300.active .svg-icon,.btn.btn-active-color-gray-300.active i,.btn.btn-active-color-gray-300.show .svg-icon,.btn.btn-active-color-gray-300.show i,.btn.btn-active-color-gray-300:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-300:active:not(.btn-active) i,.btn.btn-active-color-gray-300:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-300:focus:not(.btn-active) i,.btn.btn-active-color-gray-300:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-300:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-300 .svg-icon,.show>.btn.btn-active-color-gray-300 i{color:var(--bs-text-gray-300)}.btn-check:active+.btn.btn-active-color-gray-300.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-300.dropdown-toggle:after,.btn.btn-active-color-gray-300.active.dropdown-toggle:after,.btn.btn-active-color-gray-300.show.dropdown-toggle:after,.btn.btn-active-color-gray-300:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-300:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-300:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-300.dropdown-toggle:after{color:var(--bs-text-gray-300)}.btn.btn-icon-gray-300 .svg-icon,.btn.btn-icon-gray-300 i{color:var(--bs-text-gray-300)}.btn.btn-icon-gray-300.dropdown-toggle:after{color:var(--bs-text-gray-300)}.btn-check:active+.btn.btn-active-icon-gray-300 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-300 i,.btn-check:checked+.btn.btn-active-icon-gray-300 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-300 i,.btn.btn-active-icon-gray-300.active .svg-icon,.btn.btn-active-icon-gray-300.active i,.btn.btn-active-icon-gray-300.show .svg-icon,.btn.btn-active-icon-gray-300.show i,.btn.btn-active-icon-gray-300:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-300:active:not(.btn-active) i,.btn.btn-active-icon-gray-300:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-300:focus:not(.btn-active) i,.btn.btn-active-icon-gray-300:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-300:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-300 .svg-icon,.show>.btn.btn-active-icon-gray-300 i{color:var(--bs-text-gray-300)}.btn-check:active+.btn.btn-active-icon-gray-300.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-300.dropdown-toggle:after,.btn.btn-active-icon-gray-300.active.dropdown-toggle:after,.btn.btn-active-icon-gray-300.show.dropdown-toggle:after,.btn.btn-active-icon-gray-300:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-300:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-300:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-300.dropdown-toggle:after{color:var(--bs-text-gray-300)}.btn.btn-text-gray-300{color:var(--bs-text-gray-300)}.btn-check:active+.btn.btn-active-text-gray-300,.btn-check:checked+.btn.btn-active-text-gray-300,.btn.btn-active-text-gray-300.active,.btn.btn-active-text-gray-300.show,.btn.btn-active-text-gray-300:active:not(.btn-active),.btn.btn-active-text-gray-300:focus:not(.btn-active),.btn.btn-active-text-gray-300:hover:not(.btn-active),.show>.btn.btn-active-text-gray-300{color:var(--bs-text-gray-300)}.btn.btn-color-gray-400{color:var(--bs-text-gray-400)}.btn.btn-color-gray-400 .svg-icon,.btn.btn-color-gray-400 i{color:var(--bs-text-gray-400)}.btn.btn-color-gray-400.dropdown-toggle:after{color:var(--bs-text-gray-400)}.btn-check:active+.btn.btn-active-color-gray-400,.btn-check:checked+.btn.btn-active-color-gray-400,.btn.btn-active-color-gray-400.active,.btn.btn-active-color-gray-400.show,.btn.btn-active-color-gray-400:active:not(.btn-active),.btn.btn-active-color-gray-400:focus:not(.btn-active),.btn.btn-active-color-gray-400:hover:not(.btn-active),.show>.btn.btn-active-color-gray-400{color:var(--bs-text-gray-400)}.btn-check:active+.btn.btn-active-color-gray-400 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-400 i,.btn-check:checked+.btn.btn-active-color-gray-400 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-400 i,.btn.btn-active-color-gray-400.active .svg-icon,.btn.btn-active-color-gray-400.active i,.btn.btn-active-color-gray-400.show .svg-icon,.btn.btn-active-color-gray-400.show i,.btn.btn-active-color-gray-400:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-400:active:not(.btn-active) i,.btn.btn-active-color-gray-400:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-400:focus:not(.btn-active) i,.btn.btn-active-color-gray-400:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-400:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-400 .svg-icon,.show>.btn.btn-active-color-gray-400 i{color:var(--bs-text-gray-400)}.btn-check:active+.btn.btn-active-color-gray-400.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-400.dropdown-toggle:after,.btn.btn-active-color-gray-400.active.dropdown-toggle:after,.btn.btn-active-color-gray-400.show.dropdown-toggle:after,.btn.btn-active-color-gray-400:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-400:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-400:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-400.dropdown-toggle:after{color:var(--bs-text-gray-400)}.btn.btn-icon-gray-400 .svg-icon,.btn.btn-icon-gray-400 i{color:var(--bs-text-gray-400)}.btn.btn-icon-gray-400.dropdown-toggle:after{color:var(--bs-text-gray-400)}.btn-check:active+.btn.btn-active-icon-gray-400 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-400 i,.btn-check:checked+.btn.btn-active-icon-gray-400 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-400 i,.btn.btn-active-icon-gray-400.active .svg-icon,.btn.btn-active-icon-gray-400.active i,.btn.btn-active-icon-gray-400.show .svg-icon,.btn.btn-active-icon-gray-400.show i,.btn.btn-active-icon-gray-400:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-400:active:not(.btn-active) i,.btn.btn-active-icon-gray-400:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-400:focus:not(.btn-active) i,.btn.btn-active-icon-gray-400:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-400:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-400 .svg-icon,.show>.btn.btn-active-icon-gray-400 i{color:var(--bs-text-gray-400)}.btn-check:active+.btn.btn-active-icon-gray-400.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-400.dropdown-toggle:after,.btn.btn-active-icon-gray-400.active.dropdown-toggle:after,.btn.btn-active-icon-gray-400.show.dropdown-toggle:after,.btn.btn-active-icon-gray-400:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-400:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-400:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-400.dropdown-toggle:after{color:var(--bs-text-gray-400)}.btn.btn-text-gray-400{color:var(--bs-text-gray-400)}.btn-check:active+.btn.btn-active-text-gray-400,.btn-check:checked+.btn.btn-active-text-gray-400,.btn.btn-active-text-gray-400.active,.btn.btn-active-text-gray-400.show,.btn.btn-active-text-gray-400:active:not(.btn-active),.btn.btn-active-text-gray-400:focus:not(.btn-active),.btn.btn-active-text-gray-400:hover:not(.btn-active),.show>.btn.btn-active-text-gray-400{color:var(--bs-text-gray-400)}.btn.btn-color-gray-500{color:var(--bs-text-gray-500)}.btn.btn-color-gray-500 .svg-icon,.btn.btn-color-gray-500 i{color:var(--bs-text-gray-500)}.btn.btn-color-gray-500.dropdown-toggle:after{color:var(--bs-text-gray-500)}.btn-check:active+.btn.btn-active-color-gray-500,.btn-check:checked+.btn.btn-active-color-gray-500,.btn.btn-active-color-gray-500.active,.btn.btn-active-color-gray-500.show,.btn.btn-active-color-gray-500:active:not(.btn-active),.btn.btn-active-color-gray-500:focus:not(.btn-active),.btn.btn-active-color-gray-500:hover:not(.btn-active),.show>.btn.btn-active-color-gray-500{color:var(--bs-text-gray-500)}.btn-check:active+.btn.btn-active-color-gray-500 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-500 i,.btn-check:checked+.btn.btn-active-color-gray-500 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-500 i,.btn.btn-active-color-gray-500.active .svg-icon,.btn.btn-active-color-gray-500.active i,.btn.btn-active-color-gray-500.show .svg-icon,.btn.btn-active-color-gray-500.show i,.btn.btn-active-color-gray-500:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-500:active:not(.btn-active) i,.btn.btn-active-color-gray-500:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-500:focus:not(.btn-active) i,.btn.btn-active-color-gray-500:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-500:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-500 .svg-icon,.show>.btn.btn-active-color-gray-500 i{color:var(--bs-text-gray-500)}.btn-check:active+.btn.btn-active-color-gray-500.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-500.dropdown-toggle:after,.btn.btn-active-color-gray-500.active.dropdown-toggle:after,.btn.btn-active-color-gray-500.show.dropdown-toggle:after,.btn.btn-active-color-gray-500:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-500:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-500:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-500.dropdown-toggle:after{color:var(--bs-text-gray-500)}.btn.btn-icon-gray-500 .svg-icon,.btn.btn-icon-gray-500 i{color:var(--bs-text-gray-500)}.btn.btn-icon-gray-500.dropdown-toggle:after{color:var(--bs-text-gray-500)}.btn-check:active+.btn.btn-active-icon-gray-500 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-500 i,.btn-check:checked+.btn.btn-active-icon-gray-500 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-500 i,.btn.btn-active-icon-gray-500.active .svg-icon,.btn.btn-active-icon-gray-500.active i,.btn.btn-active-icon-gray-500.show .svg-icon,.btn.btn-active-icon-gray-500.show i,.btn.btn-active-icon-gray-500:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-500:active:not(.btn-active) i,.btn.btn-active-icon-gray-500:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-500:focus:not(.btn-active) i,.btn.btn-active-icon-gray-500:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-500:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-500 .svg-icon,.show>.btn.btn-active-icon-gray-500 i{color:var(--bs-text-gray-500)}.btn-check:active+.btn.btn-active-icon-gray-500.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-500.dropdown-toggle:after,.btn.btn-active-icon-gray-500.active.dropdown-toggle:after,.btn.btn-active-icon-gray-500.show.dropdown-toggle:after,.btn.btn-active-icon-gray-500:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-500:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-500:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-500.dropdown-toggle:after{color:var(--bs-text-gray-500)}.btn.btn-text-gray-500{color:var(--bs-text-gray-500)}.btn-check:active+.btn.btn-active-text-gray-500,.btn-check:checked+.btn.btn-active-text-gray-500,.btn.btn-active-text-gray-500.active,.btn.btn-active-text-gray-500.show,.btn.btn-active-text-gray-500:active:not(.btn-active),.btn.btn-active-text-gray-500:focus:not(.btn-active),.btn.btn-active-text-gray-500:hover:not(.btn-active),.show>.btn.btn-active-text-gray-500{color:var(--bs-text-gray-500)}.btn.btn-color-gray-600{color:var(--bs-text-gray-600)}.btn.btn-color-gray-600 .svg-icon,.btn.btn-color-gray-600 i{color:var(--bs-text-gray-600)}.btn.btn-color-gray-600.dropdown-toggle:after{color:var(--bs-text-gray-600)}.btn-check:active+.btn.btn-active-color-gray-600,.btn-check:checked+.btn.btn-active-color-gray-600,.btn.btn-active-color-gray-600.active,.btn.btn-active-color-gray-600.show,.btn.btn-active-color-gray-600:active:not(.btn-active),.btn.btn-active-color-gray-600:focus:not(.btn-active),.btn.btn-active-color-gray-600:hover:not(.btn-active),.show>.btn.btn-active-color-gray-600{color:var(--bs-text-gray-600)}.btn-check:active+.btn.btn-active-color-gray-600 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-600 i,.btn-check:checked+.btn.btn-active-color-gray-600 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-600 i,.btn.btn-active-color-gray-600.active .svg-icon,.btn.btn-active-color-gray-600.active i,.btn.btn-active-color-gray-600.show .svg-icon,.btn.btn-active-color-gray-600.show i,.btn.btn-active-color-gray-600:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-600:active:not(.btn-active) i,.btn.btn-active-color-gray-600:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-600:focus:not(.btn-active) i,.btn.btn-active-color-gray-600:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-600:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-600 .svg-icon,.show>.btn.btn-active-color-gray-600 i{color:var(--bs-text-gray-600)}.btn-check:active+.btn.btn-active-color-gray-600.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-600.dropdown-toggle:after,.btn.btn-active-color-gray-600.active.dropdown-toggle:after,.btn.btn-active-color-gray-600.show.dropdown-toggle:after,.btn.btn-active-color-gray-600:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-600:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-600:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-600.dropdown-toggle:after{color:var(--bs-text-gray-600)}.btn.btn-icon-gray-600 .svg-icon,.btn.btn-icon-gray-600 i{color:var(--bs-text-gray-600)}.btn.btn-icon-gray-600.dropdown-toggle:after{color:var(--bs-text-gray-600)}.btn-check:active+.btn.btn-active-icon-gray-600 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-600 i,.btn-check:checked+.btn.btn-active-icon-gray-600 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-600 i,.btn.btn-active-icon-gray-600.active .svg-icon,.btn.btn-active-icon-gray-600.active i,.btn.btn-active-icon-gray-600.show .svg-icon,.btn.btn-active-icon-gray-600.show i,.btn.btn-active-icon-gray-600:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-600:active:not(.btn-active) i,.btn.btn-active-icon-gray-600:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-600:focus:not(.btn-active) i,.btn.btn-active-icon-gray-600:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-600:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-600 .svg-icon,.show>.btn.btn-active-icon-gray-600 i{color:var(--bs-text-gray-600)}.btn-check:active+.btn.btn-active-icon-gray-600.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-600.dropdown-toggle:after,.btn.btn-active-icon-gray-600.active.dropdown-toggle:after,.btn.btn-active-icon-gray-600.show.dropdown-toggle:after,.btn.btn-active-icon-gray-600:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-600:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-600:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-600.dropdown-toggle:after{color:var(--bs-text-gray-600)}.btn.btn-text-gray-600{color:var(--bs-text-gray-600)}.btn-check:active+.btn.btn-active-text-gray-600,.btn-check:checked+.btn.btn-active-text-gray-600,.btn.btn-active-text-gray-600.active,.btn.btn-active-text-gray-600.show,.btn.btn-active-text-gray-600:active:not(.btn-active),.btn.btn-active-text-gray-600:focus:not(.btn-active),.btn.btn-active-text-gray-600:hover:not(.btn-active),.show>.btn.btn-active-text-gray-600{color:var(--bs-text-gray-600)}.btn.btn-color-gray-700{color:var(--bs-text-gray-700)}.btn.btn-color-gray-700 .svg-icon,.btn.btn-color-gray-700 i{color:var(--bs-text-gray-700)}.btn.btn-color-gray-700.dropdown-toggle:after{color:var(--bs-text-gray-700)}.btn-check:active+.btn.btn-active-color-gray-700,.btn-check:checked+.btn.btn-active-color-gray-700,.btn.btn-active-color-gray-700.active,.btn.btn-active-color-gray-700.show,.btn.btn-active-color-gray-700:active:not(.btn-active),.btn.btn-active-color-gray-700:focus:not(.btn-active),.btn.btn-active-color-gray-700:hover:not(.btn-active),.show>.btn.btn-active-color-gray-700{color:var(--bs-text-gray-700)}.btn-check:active+.btn.btn-active-color-gray-700 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-700 i,.btn-check:checked+.btn.btn-active-color-gray-700 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-700 i,.btn.btn-active-color-gray-700.active .svg-icon,.btn.btn-active-color-gray-700.active i,.btn.btn-active-color-gray-700.show .svg-icon,.btn.btn-active-color-gray-700.show i,.btn.btn-active-color-gray-700:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-700:active:not(.btn-active) i,.btn.btn-active-color-gray-700:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-700:focus:not(.btn-active) i,.btn.btn-active-color-gray-700:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-700:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-700 .svg-icon,.show>.btn.btn-active-color-gray-700 i{color:var(--bs-text-gray-700)}.btn-check:active+.btn.btn-active-color-gray-700.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-700.dropdown-toggle:after,.btn.btn-active-color-gray-700.active.dropdown-toggle:after,.btn.btn-active-color-gray-700.show.dropdown-toggle:after,.btn.btn-active-color-gray-700:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-700:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-700:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-700.dropdown-toggle:after{color:var(--bs-text-gray-700)}.btn.btn-icon-gray-700 .svg-icon,.btn.btn-icon-gray-700 i{color:var(--bs-text-gray-700)}.btn.btn-icon-gray-700.dropdown-toggle:after{color:var(--bs-text-gray-700)}.btn-check:active+.btn.btn-active-icon-gray-700 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-700 i,.btn-check:checked+.btn.btn-active-icon-gray-700 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-700 i,.btn.btn-active-icon-gray-700.active .svg-icon,.btn.btn-active-icon-gray-700.active i,.btn.btn-active-icon-gray-700.show .svg-icon,.btn.btn-active-icon-gray-700.show i,.btn.btn-active-icon-gray-700:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-700:active:not(.btn-active) i,.btn.btn-active-icon-gray-700:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-700:focus:not(.btn-active) i,.btn.btn-active-icon-gray-700:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-700:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-700 .svg-icon,.show>.btn.btn-active-icon-gray-700 i{color:var(--bs-text-gray-700)}.btn-check:active+.btn.btn-active-icon-gray-700.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-700.dropdown-toggle:after,.btn.btn-active-icon-gray-700.active.dropdown-toggle:after,.btn.btn-active-icon-gray-700.show.dropdown-toggle:after,.btn.btn-active-icon-gray-700:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-700:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-700:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-700.dropdown-toggle:after{color:var(--bs-text-gray-700)}.btn.btn-text-gray-700{color:var(--bs-text-gray-700)}.btn-check:active+.btn.btn-active-text-gray-700,.btn-check:checked+.btn.btn-active-text-gray-700,.btn.btn-active-text-gray-700.active,.btn.btn-active-text-gray-700.show,.btn.btn-active-text-gray-700:active:not(.btn-active),.btn.btn-active-text-gray-700:focus:not(.btn-active),.btn.btn-active-text-gray-700:hover:not(.btn-active),.show>.btn.btn-active-text-gray-700{color:var(--bs-text-gray-700)}.btn.btn-color-gray-800{color:var(--bs-text-gray-800)}.btn.btn-color-gray-800 .svg-icon,.btn.btn-color-gray-800 i{color:var(--bs-text-gray-800)}.btn.btn-color-gray-800.dropdown-toggle:after{color:var(--bs-text-gray-800)}.btn-check:active+.btn.btn-active-color-gray-800,.btn-check:checked+.btn.btn-active-color-gray-800,.btn.btn-active-color-gray-800.active,.btn.btn-active-color-gray-800.show,.btn.btn-active-color-gray-800:active:not(.btn-active),.btn.btn-active-color-gray-800:focus:not(.btn-active),.btn.btn-active-color-gray-800:hover:not(.btn-active),.show>.btn.btn-active-color-gray-800{color:var(--bs-text-gray-800)}.btn-check:active+.btn.btn-active-color-gray-800 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-800 i,.btn-check:checked+.btn.btn-active-color-gray-800 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-800 i,.btn.btn-active-color-gray-800.active .svg-icon,.btn.btn-active-color-gray-800.active i,.btn.btn-active-color-gray-800.show .svg-icon,.btn.btn-active-color-gray-800.show i,.btn.btn-active-color-gray-800:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-800:active:not(.btn-active) i,.btn.btn-active-color-gray-800:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-800:focus:not(.btn-active) i,.btn.btn-active-color-gray-800:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-800:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-800 .svg-icon,.show>.btn.btn-active-color-gray-800 i{color:var(--bs-text-gray-800)}.btn-check:active+.btn.btn-active-color-gray-800.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-800.dropdown-toggle:after,.btn.btn-active-color-gray-800.active.dropdown-toggle:after,.btn.btn-active-color-gray-800.show.dropdown-toggle:after,.btn.btn-active-color-gray-800:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-800:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-800:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-800.dropdown-toggle:after{color:var(--bs-text-gray-800)}.btn.btn-icon-gray-800 .svg-icon,.btn.btn-icon-gray-800 i{color:var(--bs-text-gray-800)}.btn.btn-icon-gray-800.dropdown-toggle:after{color:var(--bs-text-gray-800)}.btn-check:active+.btn.btn-active-icon-gray-800 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-800 i,.btn-check:checked+.btn.btn-active-icon-gray-800 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-800 i,.btn.btn-active-icon-gray-800.active .svg-icon,.btn.btn-active-icon-gray-800.active i,.btn.btn-active-icon-gray-800.show .svg-icon,.btn.btn-active-icon-gray-800.show i,.btn.btn-active-icon-gray-800:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-800:active:not(.btn-active) i,.btn.btn-active-icon-gray-800:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-800:focus:not(.btn-active) i,.btn.btn-active-icon-gray-800:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-800:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-800 .svg-icon,.show>.btn.btn-active-icon-gray-800 i{color:var(--bs-text-gray-800)}.btn-check:active+.btn.btn-active-icon-gray-800.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-800.dropdown-toggle:after,.btn.btn-active-icon-gray-800.active.dropdown-toggle:after,.btn.btn-active-icon-gray-800.show.dropdown-toggle:after,.btn.btn-active-icon-gray-800:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-800:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-800:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-800.dropdown-toggle:after{color:var(--bs-text-gray-800)}.btn.btn-text-gray-800{color:var(--bs-text-gray-800)}.btn-check:active+.btn.btn-active-text-gray-800,.btn-check:checked+.btn.btn-active-text-gray-800,.btn.btn-active-text-gray-800.active,.btn.btn-active-text-gray-800.show,.btn.btn-active-text-gray-800:active:not(.btn-active),.btn.btn-active-text-gray-800:focus:not(.btn-active),.btn.btn-active-text-gray-800:hover:not(.btn-active),.show>.btn.btn-active-text-gray-800{color:var(--bs-text-gray-800)}.btn.btn-color-gray-900{color:var(--bs-text-gray-900)}.btn.btn-color-gray-900 .svg-icon,.btn.btn-color-gray-900 i{color:var(--bs-text-gray-900)}.btn.btn-color-gray-900.dropdown-toggle:after{color:var(--bs-text-gray-900)}.btn-check:active+.btn.btn-active-color-gray-900,.btn-check:checked+.btn.btn-active-color-gray-900,.btn.btn-active-color-gray-900.active,.btn.btn-active-color-gray-900.show,.btn.btn-active-color-gray-900:active:not(.btn-active),.btn.btn-active-color-gray-900:focus:not(.btn-active),.btn.btn-active-color-gray-900:hover:not(.btn-active),.show>.btn.btn-active-color-gray-900{color:var(--bs-text-gray-900)}.btn-check:active+.btn.btn-active-color-gray-900 .svg-icon,.btn-check:active+.btn.btn-active-color-gray-900 i,.btn-check:checked+.btn.btn-active-color-gray-900 .svg-icon,.btn-check:checked+.btn.btn-active-color-gray-900 i,.btn.btn-active-color-gray-900.active .svg-icon,.btn.btn-active-color-gray-900.active i,.btn.btn-active-color-gray-900.show .svg-icon,.btn.btn-active-color-gray-900.show i,.btn.btn-active-color-gray-900:active:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-900:active:not(.btn-active) i,.btn.btn-active-color-gray-900:focus:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-900:focus:not(.btn-active) i,.btn.btn-active-color-gray-900:hover:not(.btn-active) .svg-icon,.btn.btn-active-color-gray-900:hover:not(.btn-active) i,.show>.btn.btn-active-color-gray-900 .svg-icon,.show>.btn.btn-active-color-gray-900 i{color:var(--bs-text-gray-900)}.btn-check:active+.btn.btn-active-color-gray-900.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-color-gray-900.dropdown-toggle:after,.btn.btn-active-color-gray-900.active.dropdown-toggle:after,.btn.btn-active-color-gray-900.show.dropdown-toggle:after,.btn.btn-active-color-gray-900:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-900:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-color-gray-900:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-color-gray-900.dropdown-toggle:after{color:var(--bs-text-gray-900)}.btn.btn-icon-gray-900 .svg-icon,.btn.btn-icon-gray-900 i{color:var(--bs-text-gray-900)}.btn.btn-icon-gray-900.dropdown-toggle:after{color:var(--bs-text-gray-900)}.btn-check:active+.btn.btn-active-icon-gray-900 .svg-icon,.btn-check:active+.btn.btn-active-icon-gray-900 i,.btn-check:checked+.btn.btn-active-icon-gray-900 .svg-icon,.btn-check:checked+.btn.btn-active-icon-gray-900 i,.btn.btn-active-icon-gray-900.active .svg-icon,.btn.btn-active-icon-gray-900.active i,.btn.btn-active-icon-gray-900.show .svg-icon,.btn.btn-active-icon-gray-900.show i,.btn.btn-active-icon-gray-900:active:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-900:active:not(.btn-active) i,.btn.btn-active-icon-gray-900:focus:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-900:focus:not(.btn-active) i,.btn.btn-active-icon-gray-900:hover:not(.btn-active) .svg-icon,.btn.btn-active-icon-gray-900:hover:not(.btn-active) i,.show>.btn.btn-active-icon-gray-900 .svg-icon,.show>.btn.btn-active-icon-gray-900 i{color:var(--bs-text-gray-900)}.btn-check:active+.btn.btn-active-icon-gray-900.dropdown-toggle:after,.btn-check:checked+.btn.btn-active-icon-gray-900.dropdown-toggle:after,.btn.btn-active-icon-gray-900.active.dropdown-toggle:after,.btn.btn-active-icon-gray-900.show.dropdown-toggle:after,.btn.btn-active-icon-gray-900:active:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-900:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-active-icon-gray-900:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-active-icon-gray-900.dropdown-toggle:after{color:var(--bs-text-gray-900)}.btn.btn-text-gray-900{color:var(--bs-text-gray-900)}.btn-check:active+.btn.btn-active-text-gray-900,.btn-check:checked+.btn.btn-active-text-gray-900,.btn.btn-active-text-gray-900.active,.btn.btn-active-text-gray-900.show,.btn.btn-active-text-gray-900:active:not(.btn-active),.btn.btn-active-text-gray-900:focus:not(.btn-active),.btn.btn-active-text-gray-900:hover:not(.btn-active),.show>.btn.btn-active-text-gray-900{color:var(--bs-text-gray-900)}.btn.btn-facebook{color:#fff;border-color:#3b5998;background-color:#3b5998}.btn.btn-facebook .svg-icon,.btn.btn-facebook i{color:#fff}.btn.btn-facebook.dropdown-toggle:after{color:#fff}.btn-check:active+.btn.btn-facebook,.btn-check:checked+.btn.btn-facebook,.btn.btn-facebook.active,.btn.btn-facebook.show,.btn.btn-facebook:active:not(.btn-active),.btn.btn-facebook:focus:not(.btn-active),.btn.btn-facebook:hover:not(.btn-active),.show>.btn.btn-facebook{border-color:#30497c;background-color:#30497c!important}.btn.btn-light-facebook{color:var(--bs-facebook);color:#3b5998;border-color:rgba(59,89,152,.1);background-color:rgba(59,89,152,.1)}.btn.btn-light-facebook .svg-icon,.btn.btn-light-facebook i{color:#3b5998}.btn.btn-light-facebook.dropdown-toggle:after{color:#3b5998}.btn-check:active+.btn.btn-light-facebook,.btn-check:checked+.btn.btn-light-facebook,.btn.btn-light-facebook.active,.btn.btn-light-facebook.show,.btn.btn-light-facebook:active:not(.btn-active),.btn.btn-light-facebook:focus:not(.btn-active),.btn.btn-light-facebook:hover:not(.btn-active),.show>.btn.btn-light-facebook{color:#fff;border-color:#3b5998;background-color:#3b5998!important}.btn-check:active+.btn.btn-light-facebook .svg-icon,.btn-check:active+.btn.btn-light-facebook i,.btn-check:checked+.btn.btn-light-facebook .svg-icon,.btn-check:checked+.btn.btn-light-facebook i,.btn.btn-light-facebook.active .svg-icon,.btn.btn-light-facebook.active i,.btn.btn-light-facebook.show .svg-icon,.btn.btn-light-facebook.show i,.btn.btn-light-facebook:active:not(.btn-active) .svg-icon,.btn.btn-light-facebook:active:not(.btn-active) i,.btn.btn-light-facebook:focus:not(.btn-active) .svg-icon,.btn.btn-light-facebook:focus:not(.btn-active) i,.btn.btn-light-facebook:hover:not(.btn-active) .svg-icon,.btn.btn-light-facebook:hover:not(.btn-active) i,.show>.btn.btn-light-facebook .svg-icon,.show>.btn.btn-light-facebook i{color:#fff}.btn-check:active+.btn.btn-light-facebook.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-facebook.dropdown-toggle:after,.btn.btn-light-facebook.active.dropdown-toggle:after,.btn.btn-light-facebook.show.dropdown-toggle:after,.btn.btn-light-facebook:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-facebook:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-facebook:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-facebook.dropdown-toggle:after{color:#fff}.btn.btn-google{color:#fff;border-color:#dd4b39;background-color:#dd4b39}.btn.btn-google .svg-icon,.btn.btn-google i{color:#fff}.btn.btn-google.dropdown-toggle:after{color:#fff}.btn-check:active+.btn.btn-google,.btn-check:checked+.btn.btn-google,.btn.btn-google.active,.btn.btn-google.show,.btn.btn-google:active:not(.btn-active),.btn.btn-google:focus:not(.btn-active),.btn.btn-google:hover:not(.btn-active),.show>.btn.btn-google{border-color:#cd3623;background-color:#cd3623!important}.btn.btn-light-google{color:var(--bs-google);color:#dd4b39;border-color:rgba(221,75,57,.1);background-color:rgba(221,75,57,.1)}.btn.btn-light-google .svg-icon,.btn.btn-light-google i{color:#dd4b39}.btn.btn-light-google.dropdown-toggle:after{color:#dd4b39}.btn-check:active+.btn.btn-light-google,.btn-check:checked+.btn.btn-light-google,.btn.btn-light-google.active,.btn.btn-light-google.show,.btn.btn-light-google:active:not(.btn-active),.btn.btn-light-google:focus:not(.btn-active),.btn.btn-light-google:hover:not(.btn-active),.show>.btn.btn-light-google{color:#fff;border-color:#dd4b39;background-color:#dd4b39!important}.btn-check:active+.btn.btn-light-google .svg-icon,.btn-check:active+.btn.btn-light-google i,.btn-check:checked+.btn.btn-light-google .svg-icon,.btn-check:checked+.btn.btn-light-google i,.btn.btn-light-google.active .svg-icon,.btn.btn-light-google.active i,.btn.btn-light-google.show .svg-icon,.btn.btn-light-google.show i,.btn.btn-light-google:active:not(.btn-active) .svg-icon,.btn.btn-light-google:active:not(.btn-active) i,.btn.btn-light-google:focus:not(.btn-active) .svg-icon,.btn.btn-light-google:focus:not(.btn-active) i,.btn.btn-light-google:hover:not(.btn-active) .svg-icon,.btn.btn-light-google:hover:not(.btn-active) i,.show>.btn.btn-light-google .svg-icon,.show>.btn.btn-light-google i{color:#fff}.btn-check:active+.btn.btn-light-google.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-google.dropdown-toggle:after,.btn.btn-light-google.active.dropdown-toggle:after,.btn.btn-light-google.show.dropdown-toggle:after,.btn.btn-light-google:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-google:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-google:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-google.dropdown-toggle:after{color:#fff}.btn.btn-twitter{color:#fff;border-color:#1da1f2;background-color:#1da1f2}.btn.btn-twitter .svg-icon,.btn.btn-twitter i{color:#fff}.btn.btn-twitter.dropdown-toggle:after{color:#fff}.btn-check:active+.btn.btn-twitter,.btn-check:checked+.btn.btn-twitter,.btn.btn-twitter.active,.btn.btn-twitter.show,.btn.btn-twitter:active:not(.btn-active),.btn.btn-twitter:focus:not(.btn-active),.btn.btn-twitter:hover:not(.btn-active),.show>.btn.btn-twitter{border-color:#0d8ddc;background-color:#0d8ddc!important}.btn.btn-light-twitter{color:var(--bs-twitter);color:#1da1f2;border-color:rgba(29,161,242,.1);background-color:rgba(29,161,242,.1)}.btn.btn-light-twitter .svg-icon,.btn.btn-light-twitter i{color:#1da1f2}.btn.btn-light-twitter.dropdown-toggle:after{color:#1da1f2}.btn-check:active+.btn.btn-light-twitter,.btn-check:checked+.btn.btn-light-twitter,.btn.btn-light-twitter.active,.btn.btn-light-twitter.show,.btn.btn-light-twitter:active:not(.btn-active),.btn.btn-light-twitter:focus:not(.btn-active),.btn.btn-light-twitter:hover:not(.btn-active),.show>.btn.btn-light-twitter{color:#fff;border-color:#1da1f2;background-color:#1da1f2!important}.btn-check:active+.btn.btn-light-twitter .svg-icon,.btn-check:active+.btn.btn-light-twitter i,.btn-check:checked+.btn.btn-light-twitter .svg-icon,.btn-check:checked+.btn.btn-light-twitter i,.btn.btn-light-twitter.active .svg-icon,.btn.btn-light-twitter.active i,.btn.btn-light-twitter.show .svg-icon,.btn.btn-light-twitter.show i,.btn.btn-light-twitter:active:not(.btn-active) .svg-icon,.btn.btn-light-twitter:active:not(.btn-active) i,.btn.btn-light-twitter:focus:not(.btn-active) .svg-icon,.btn.btn-light-twitter:focus:not(.btn-active) i,.btn.btn-light-twitter:hover:not(.btn-active) .svg-icon,.btn.btn-light-twitter:hover:not(.btn-active) i,.show>.btn.btn-light-twitter .svg-icon,.show>.btn.btn-light-twitter i{color:#fff}.btn-check:active+.btn.btn-light-twitter.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-twitter.dropdown-toggle:after,.btn.btn-light-twitter.active.dropdown-toggle:after,.btn.btn-light-twitter.show.dropdown-toggle:after,.btn.btn-light-twitter:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-twitter:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-twitter:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-twitter.dropdown-toggle:after{color:#fff}.btn.btn-instagram{color:#fff;border-color:#e1306c;background-color:#e1306c}.btn.btn-instagram .svg-icon,.btn.btn-instagram i{color:#fff}.btn.btn-instagram.dropdown-toggle:after{color:#fff}.btn-check:active+.btn.btn-instagram,.btn-check:checked+.btn.btn-instagram,.btn.btn-instagram.active,.btn.btn-instagram.show,.btn.btn-instagram:active:not(.btn-active),.btn.btn-instagram:focus:not(.btn-active),.btn.btn-instagram:hover:not(.btn-active),.show>.btn.btn-instagram{border-color:#cd1e59;background-color:#cd1e59!important}.btn.btn-light-instagram{color:var(--bs-instagram);color:#e1306c;border-color:rgba(225,48,108,.1);background-color:rgba(225,48,108,.1)}.btn.btn-light-instagram .svg-icon,.btn.btn-light-instagram i{color:#e1306c}.btn.btn-light-instagram.dropdown-toggle:after{color:#e1306c}.btn-check:active+.btn.btn-light-instagram,.btn-check:checked+.btn.btn-light-instagram,.btn.btn-light-instagram.active,.btn.btn-light-instagram.show,.btn.btn-light-instagram:active:not(.btn-active),.btn.btn-light-instagram:focus:not(.btn-active),.btn.btn-light-instagram:hover:not(.btn-active),.show>.btn.btn-light-instagram{color:#fff;border-color:#e1306c;background-color:#e1306c!important}.btn-check:active+.btn.btn-light-instagram .svg-icon,.btn-check:active+.btn.btn-light-instagram i,.btn-check:checked+.btn.btn-light-instagram .svg-icon,.btn-check:checked+.btn.btn-light-instagram i,.btn.btn-light-instagram.active .svg-icon,.btn.btn-light-instagram.active i,.btn.btn-light-instagram.show .svg-icon,.btn.btn-light-instagram.show i,.btn.btn-light-instagram:active:not(.btn-active) .svg-icon,.btn.btn-light-instagram:active:not(.btn-active) i,.btn.btn-light-instagram:focus:not(.btn-active) .svg-icon,.btn.btn-light-instagram:focus:not(.btn-active) i,.btn.btn-light-instagram:hover:not(.btn-active) .svg-icon,.btn.btn-light-instagram:hover:not(.btn-active) i,.show>.btn.btn-light-instagram .svg-icon,.show>.btn.btn-light-instagram i{color:#fff}.btn-check:active+.btn.btn-light-instagram.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-instagram.dropdown-toggle:after,.btn.btn-light-instagram.active.dropdown-toggle:after,.btn.btn-light-instagram.show.dropdown-toggle:after,.btn.btn-light-instagram:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-instagram:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-instagram:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-instagram.dropdown-toggle:after{color:#fff}.btn.btn-youtube{color:#fff;border-color:red;background-color:red}.btn.btn-youtube .svg-icon,.btn.btn-youtube i{color:#fff}.btn.btn-youtube.dropdown-toggle:after{color:#fff}.btn-check:active+.btn.btn-youtube,.btn-check:checked+.btn.btn-youtube,.btn.btn-youtube.active,.btn.btn-youtube.show,.btn.btn-youtube:active:not(.btn-active),.btn.btn-youtube:focus:not(.btn-active),.btn.btn-youtube:hover:not(.btn-active),.show>.btn.btn-youtube{border-color:#d90000;background-color:#d90000!important}.btn.btn-light-youtube{color:var(--bs-youtube);color:red;border-color:rgba(255,0,0,.1);background-color:rgba(255,0,0,.1)}.btn.btn-light-youtube .svg-icon,.btn.btn-light-youtube i{color:red}.btn.btn-light-youtube.dropdown-toggle:after{color:red}.btn-check:active+.btn.btn-light-youtube,.btn-check:checked+.btn.btn-light-youtube,.btn.btn-light-youtube.active,.btn.btn-light-youtube.show,.btn.btn-light-youtube:active:not(.btn-active),.btn.btn-light-youtube:focus:not(.btn-active),.btn.btn-light-youtube:hover:not(.btn-active),.show>.btn.btn-light-youtube{color:#fff;border-color:red;background-color:red!important}.btn-check:active+.btn.btn-light-youtube .svg-icon,.btn-check:active+.btn.btn-light-youtube i,.btn-check:checked+.btn.btn-light-youtube .svg-icon,.btn-check:checked+.btn.btn-light-youtube i,.btn.btn-light-youtube.active .svg-icon,.btn.btn-light-youtube.active i,.btn.btn-light-youtube.show .svg-icon,.btn.btn-light-youtube.show i,.btn.btn-light-youtube:active:not(.btn-active) .svg-icon,.btn.btn-light-youtube:active:not(.btn-active) i,.btn.btn-light-youtube:focus:not(.btn-active) .svg-icon,.btn.btn-light-youtube:focus:not(.btn-active) i,.btn.btn-light-youtube:hover:not(.btn-active) .svg-icon,.btn.btn-light-youtube:hover:not(.btn-active) i,.show>.btn.btn-light-youtube .svg-icon,.show>.btn.btn-light-youtube i{color:#fff}.btn-check:active+.btn.btn-light-youtube.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-youtube.dropdown-toggle:after,.btn.btn-light-youtube.active.dropdown-toggle:after,.btn.btn-light-youtube.show.dropdown-toggle:after,.btn.btn-light-youtube:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-youtube:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-youtube:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-youtube.dropdown-toggle:after{color:#fff}.btn.btn-linkedin{color:#fff;border-color:#0077b5;background-color:#0077b5}.btn.btn-linkedin .svg-icon,.btn.btn-linkedin i{color:#fff}.btn.btn-linkedin.dropdown-toggle:after{color:#fff}.btn-check:active+.btn.btn-linkedin,.btn-check:checked+.btn.btn-linkedin,.btn.btn-linkedin.active,.btn.btn-linkedin.show,.btn.btn-linkedin:active:not(.btn-active),.btn.btn-linkedin:focus:not(.btn-active),.btn.btn-linkedin:hover:not(.btn-active),.show>.btn.btn-linkedin{border-color:#005e8f;background-color:#005e8f!important}.btn.btn-light-linkedin{color:var(--bs-linkedin);color:#0077b5;border-color:rgba(0,119,181,.1);background-color:rgba(0,119,181,.1)}.btn.btn-light-linkedin .svg-icon,.btn.btn-light-linkedin i{color:#0077b5}.btn.btn-light-linkedin.dropdown-toggle:after{color:#0077b5}.btn-check:active+.btn.btn-light-linkedin,.btn-check:checked+.btn.btn-light-linkedin,.btn.btn-light-linkedin.active,.btn.btn-light-linkedin.show,.btn.btn-light-linkedin:active:not(.btn-active),.btn.btn-light-linkedin:focus:not(.btn-active),.btn.btn-light-linkedin:hover:not(.btn-active),.show>.btn.btn-light-linkedin{color:#fff;border-color:#0077b5;background-color:#0077b5!important}.btn-check:active+.btn.btn-light-linkedin .svg-icon,.btn-check:active+.btn.btn-light-linkedin i,.btn-check:checked+.btn.btn-light-linkedin .svg-icon,.btn-check:checked+.btn.btn-light-linkedin i,.btn.btn-light-linkedin.active .svg-icon,.btn.btn-light-linkedin.active i,.btn.btn-light-linkedin.show .svg-icon,.btn.btn-light-linkedin.show i,.btn.btn-light-linkedin:active:not(.btn-active) .svg-icon,.btn.btn-light-linkedin:active:not(.btn-active) i,.btn.btn-light-linkedin:focus:not(.btn-active) .svg-icon,.btn.btn-light-linkedin:focus:not(.btn-active) i,.btn.btn-light-linkedin:hover:not(.btn-active) .svg-icon,.btn.btn-light-linkedin:hover:not(.btn-active) i,.show>.btn.btn-light-linkedin .svg-icon,.show>.btn.btn-light-linkedin i{color:#fff}.btn-check:active+.btn.btn-light-linkedin.dropdown-toggle:after,.btn-check:checked+.btn.btn-light-linkedin.dropdown-toggle:after,.btn.btn-light-linkedin.active.dropdown-toggle:after,.btn.btn-light-linkedin.show.dropdown-toggle:after,.btn.btn-light-linkedin:active:not(.btn-active).dropdown-toggle:after,.btn.btn-light-linkedin:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-light-linkedin:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-light-linkedin.dropdown-toggle:after{color:#fff}[data-bs-theme=dark] .btn.btn-dark{color:#b5b7c8;background-color:#464852}[data-bs-theme=dark] .btn.btn-dark .svg-icon,[data-bs-theme=dark] .btn.btn-dark i{color:#b5b7c8}[data-bs-theme=dark] .btn.btn-dark.dropdown-toggle:after{color:#b5b7c8}.btn-check:active+[data-bs-theme=dark] .btn.btn-dark,.btn-check:checked+[data-bs-theme=dark] .btn.btn-dark,.show>[data-bs-theme=dark] .btn.btn-dark,[data-bs-theme=dark] .btn.btn-dark.active,[data-bs-theme=dark] .btn.btn-dark.show,[data-bs-theme=dark] .btn.btn-dark:active:not(.btn-active),[data-bs-theme=dark] .btn.btn-dark:focus:not(.btn-active),[data-bs-theme=dark] .btn.btn-dark:hover:not(.btn-active){color:#bec0cf;background-color:#4d4f5a!important}.btn-check:active+[data-bs-theme=dark] .btn.btn-dark .svg-icon,.btn-check:active+[data-bs-theme=dark] .btn.btn-dark i,.btn-check:checked+[data-bs-theme=dark] .btn.btn-dark .svg-icon,.btn-check:checked+[data-bs-theme=dark] .btn.btn-dark i,.show>[data-bs-theme=dark] .btn.btn-dark .svg-icon,.show>[data-bs-theme=dark] .btn.btn-dark i,[data-bs-theme=dark] .btn.btn-dark.active .svg-icon,[data-bs-theme=dark] .btn.btn-dark.active i,[data-bs-theme=dark] .btn.btn-dark.show .svg-icon,[data-bs-theme=dark] .btn.btn-dark.show i,[data-bs-theme=dark] .btn.btn-dark:active:not(.btn-active) .svg-icon,[data-bs-theme=dark] .btn.btn-dark:active:not(.btn-active) i,[data-bs-theme=dark] .btn.btn-dark:focus:not(.btn-active) .svg-icon,[data-bs-theme=dark] .btn.btn-dark:focus:not(.btn-active) i,[data-bs-theme=dark] .btn.btn-dark:hover:not(.btn-active) .svg-icon,[data-bs-theme=dark] .btn.btn-dark:hover:not(.btn-active) i{color:#bec0cf}.btn-check:active+[data-bs-theme=dark] .btn.btn-dark.dropdown-toggle:after,.btn-check:checked+[data-bs-theme=dark] .btn.btn-dark.dropdown-toggle:after,.show>[data-bs-theme=dark] .btn.btn-dark.dropdown-toggle:after,[data-bs-theme=dark] .btn.btn-dark.active.dropdown-toggle:after,[data-bs-theme=dark] .btn.btn-dark.show.dropdown-toggle:after,[data-bs-theme=dark] .btn.btn-dark:active:not(.btn-active).dropdown-toggle:after,[data-bs-theme=dark] .btn.btn-dark:focus:not(.btn-active).dropdown-toggle:after,[data-bs-theme=dark] .btn.btn-dark:hover:not(.btn-active).dropdown-toggle:after{color:#bec0cf}.modal-rounded{border-radius:.475rem!important}.modal-header{justify-content:space-between}code:not([class*=language-]){font-weight:400;color:var(--bs-code-color);border:1px solid var(--bs-code-border-color);background-color:var(--bs-code-bg);border-radius:.3rem;line-height:inherit;font-size:1rem;padding:.1rem .4rem;margin:0 .5rem;box-shadow:0 3px 9px rgba(0,0,0,.08)}code:not([class*=language-]).code-light{color:var(--bs-light);background-color:var(--bs-light-light);border:1px solid var(--bs-light)}code:not([class*=language-]).code-primary{color:var(--bs-primary);background-color:var(--bs-primary-light);border:1px solid var(--bs-primary)}code:not([class*=language-]).code-secondary{color:var(--bs-secondary);background-color:var(--bs-secondary-light);border:1px solid var(--bs-secondary)}code:not([class*=language-]).code-success{color:var(--bs-success);background-color:var(--bs-success-light);border:1px solid var(--bs-success)}code:not([class*=language-]).code-info{color:var(--bs-info);background-color:var(--bs-info-light);border:1px solid var(--bs-info)}code:not([class*=language-]).code-warning{color:var(--bs-warning);background-color:var(--bs-warning-light);border:1px solid var(--bs-warning)}code:not([class*=language-]).code-danger{color:var(--bs-danger);background-color:var(--bs-danger-light);border:1px solid var(--bs-danger)}code:not([class*=language-]).code-dark{color:var(--bs-dark);background-color:var(--bs-dark-light);border:1px solid var(--bs-dark)}.col-form-label{font-size:1.05rem}.form-control.form-control-transparent{background-color:transparent;border-color:transparent}.dropdown.show>.form-control.form-control-transparent,.form-control.form-control-transparent.active,.form-control.form-control-transparent.focus,.form-control.form-control-transparent:active,.form-control.form-control-transparent:focus{background-color:transparent;border-color:transparent}.form-control.form-control-flush{border:0;background-color:transparent;outline:0!important;box-shadow:none;border-radius:0}.form-control.form-control-solid{background-color:var(--bs-gray-100);border-color:var(--bs-gray-100);color:var(--bs-gray-700);transition:color .2s ease}.form-control.form-control-solid::placeholder{color:var(--bs-gray-500)}.form-control.form-control-solid::-moz-placeholder{color:var(--bs-gray-500);opacity:1}.dropdown.show>.form-control.form-control-solid,.form-control.form-control-solid.active,.form-control.form-control-solid.focus,.form-control.form-control-solid:active,.form-control.form-control-solid:focus{background-color:var(--bs-gray-200);border-color:var(--bs-gray-200);color:var(--bs-gray-700);transition:color .2s ease}.form-control-solid-bg{background-color:var(--bs-gray-100)}.form-control-plaintext{color:var(--bs-gray-700)}.placeholder-gray-500::placeholder{color:var(--bs-gray-500)}.placeholder-gray-500::-moz-placeholder{color:var(--bs-gray-500);opacity:1}.placeholder-white::placeholder{color:#fff}.placeholder-white::-moz-placeholder{color:#fff;opacity:1}.resize-none{resize:none}.form-floating .form-control.form-control-solid::placeholder{color:transparent}.form-floating.form-control-solid-bg label::after,.form-floating>:disabled~label::after,.form-floating>:focus~label::after{background-color:transparent!important}.form-select{appearance:none}.form-select:focus{border-color:var(--bs-gray-400);box-shadow:false,0 0 0 .25rem rgba(var(--bs-component-active-bg),.25)}.form-select:disabled{color:var(--bs-gray-500);background-color:var(--bs-gray-200);border-color:var(--bs-gray-300)}.form-select:-moz-focusring{text-shadow:0 0 0 var(--bs-gray-700)}.form-select.form-select-solid{background-color:var(--bs-gray-100);border-color:var(--bs-gray-100);color:var(--bs-gray-700);transition:color .2s ease}.form-select.form-select-solid::placeholder{color:var(--bs-gray-500)}.form-select.form-select-solid::-moz-placeholder{color:var(--bs-gray-500);opacity:1}.dropdown.show>.form-select.form-select-solid,.form-select.form-select-solid.active,.form-select.form-select-solid.focus,.form-select.form-select-solid:active,.form-select.form-select-solid:focus{background-color:var(--bs-gray-200);border-color:var(--bs-gray-200)!important;color:var(--bs-gray-700);transition:color .2s ease}.form-select.form-select-transparent{background-color:transparent;border-color:transparent;color:var(--bs-gray-700)}.form-select.form-select-transparent::placeholder{color:var(--bs-gray-500)}.form-select.form-select-transparent::-moz-placeholder{color:var(--bs-gray-500);opacity:1}.dropdown.show>.form-select.form-select-transparent,.form-select.form-select-transparent.active,.form-select.form-select-transparent.focus,.form-select.form-select-transparent:active,.form-select.form-select-transparent:focus{background-color:transparent;border-color:transparent!important;color:var(--bs-gray-700)}.form-check:not(.form-switch) .form-check-input[type=checkbox]{background-size:60% 60%}.form-check.form-check-sm .form-check-input{height:1.55rem;width:1.55rem}.form-check.form-check-lg .form-check-input{height:2.25rem;width:2.25rem}.form-check.form-check-inline{display:inline-block;margin-right:1rem}.form-check.form-check-solid .form-check-input{border:0}.form-check.form-check-solid .form-check-input:not(:checked){background-color:var(--bs-gray-200)}.form-check.form-check-solid .form-check-input[type=checkbox]:indeterminate{background-color:#0069e0}.form-check.form-check-success .form-check-input:checked{background-color:var(--bs-success)}.form-check.form-check-danger .form-check-input:checked{background-color:var(--bs-danger)}.form-check.form-check-warning .form-check-input:checked{background-color:var(--bs-warning)}.form-check-custom{display:flex;align-items:center;padding-left:0;margin:0}.form-check-custom .form-check-input{margin:0;float:none;flex-shrink:0}.form-check-custom .form-check-label{margin-left:.55rem}.form-switch.form-check-custom .form-check-input{height:2.25rem}.form-switch.form-check-custom.form-switch-sm .form-check-input{height:1.5rem;width:2.5rem}.form-switch.form-check-custom.form-switch-lg .form-check-input{height:2.75rem;width:3.75rem}.form-switch.form-check-solid .form-check-input{--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='%23ffffff'/%3e%3c/svg%3e\")}.form-switch.form-check-solid .form-check-input:not(:checked){background-color:var(--bs-gray-200)}[data-bs-theme=dark] .form-switch .form-check-input:focus:not(:checked){--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\")}.form-check-image{position:relative;overflow:hidden}.form-check-image img{max-width:100%}.form-check-image .form-check-wrapper{border-radius:.625rem;border:2px solid transparent;transition:all .2s ease-in-out;cursor:pointer;overflow:hidden;margin-bottom:.75rem}.form-check-image .form-check-rounded{border-radius:.625rem}.form-check-image .form-check-label{font-weight:600;margin-left:.5rem}.form-check-image.active:not(.form-check-success):not(.form-check-danger) .form-check-wrapper{border-color:var(--bs-primary)!important}.form-check-image.form-check-success.active .form-check-wrapper{border-color:var(--bs-success)!important}.form-check-image.form-check-danger.active .form-check-wrapper{border-color:var(--bs-danger)!important}.form-check-image.disabled{opacity:.65}.input-group.input-group-solid{border-radius:.475rem}.input-group.input-group-solid.input-group-sm{border-radius:.425rem}.input-group.input-group-solid.input-group-lg{border-radius:.625rem}.input-group.input-group-solid .input-group-text{background-color:var(--bs-gray-100);border-color:var(--bs-gray-100)}.input-group.input-group-solid .input-group-text+.form-control{border-left-color:var(--bs-gray-300)}.input-group.input-group-solid .form-control{background-color:var(--bs-gray-100);border-color:var(--bs-gray-100)}.input-group.input-group-solid .form-control+.input-group-text{border-left-color:var(--bs-gray-300)}.form-floating .form-control.form-control-solid::placeholder{color:transparent}.form-floating.form-control-solid-bg label::after,.form-floating>:disabled~label::after,.form-floating>:focus~label::after{background-color:transparent!important}.required:after{content:\"*\";position:relative;font-size:inherit;color:var(--bs-danger);padding-left:.25rem;font-weight:600}.progress-vertical{display:flex;align-items:stretch;justify-content:space-between}.progress-vertical .progress{height:100%;border-radius:.475rem;display:flex;align-items:flex-end;margin-right:1rem}.progress-vertical .progress:last-child{margin-right:0}.progress-vertical .progress .progress-bar{width:8px;border-radius:.475rem}.table:not(.table-bordered)>:not(:first-child){border-color:transparent;border-width:0;border-style:none}.table:not(.table-bordered)>:not(:last-child)>:last-child>*{border-bottom-color:inherit}.table:not(.table-bordered) td,.table:not(.table-bordered) th,.table:not(.table-bordered) tr{border-color:inherit;border-width:inherit;border-style:inherit;text-transform:inherit;font-weight:inherit;font-size:inherit;color:inherit;height:inherit;min-height:inherit}.table:not(.table-bordered) td:first-child,.table:not(.table-bordered) th:first-child,.table:not(.table-bordered) tr:first-child{padding-left:0}.table:not(.table-bordered) td:last-child,.table:not(.table-bordered) th:last-child,.table:not(.table-bordered) tr:last-child{padding-right:0}.table:not(.table-bordered) tbody tr:last-child,.table:not(.table-bordered) tfoot tr:last-child{border-bottom:0!important}.table:not(.table-bordered) tbody tr:last-child td,.table:not(.table-bordered) tbody tr:last-child th,.table:not(.table-bordered) tfoot tr:last-child td,.table:not(.table-bordered) tfoot tr:last-child th{border-bottom:0!important}.table:not(.table-bordered) tfoot td,.table:not(.table-bordered) tfoot th{border-top:inherit}.table:not(.table-bordered).table-rounded{border-radius:.475rem;border-spacing:0;border-collapse:separate}.table:not(.table-bordered).table-rows-rounded td:first-child,.table:not(.table-bordered).table-rows-rounded th:first-child{border-top-left-radius:6px;border-bottom-left-radius:6px}.table:not(.table-bordered).table-rows-rounded td:last-child,.table:not(.table-bordered).table-rows-rounded th:last-child{border-top-right-radius:6px;border-bottom-right-radius:6px}.table:not(.table-bordered).table-flush td,.table:not(.table-bordered).table-flush th,.table:not(.table-bordered).table-flush tr{padding:inherit}.table:not(.table-bordered).table-row-bordered tr{border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:var(--bs-border-color)}.table:not(.table-bordered).table-row-bordered tfoot td,.table:not(.table-bordered).table-row-bordered tfoot th{border-top-width:1px!important}.table:not(.table-bordered).table-row-dashed tr{border-bottom-width:1px;border-bottom-style:dashed;border-bottom-color:var(--bs-border-color)}.table:not(.table-bordered).table-row-dashed tfoot td,.table:not(.table-bordered).table-row-dashed tfoot th{border-top-width:1px!important}.table:not(.table-bordered).table-row-gray-100 tr{border-bottom-color:var(--bs-gray-100)}.table:not(.table-bordered).table-row-gray-200 tr{border-bottom-color:var(--bs-gray-200)}.table:not(.table-bordered).table-row-gray-300 tr{border-bottom-color:var(--bs-gray-300)}.table:not(.table-bordered).table-row-gray-400 tr{border-bottom-color:var(--bs-gray-400)}.table:not(.table-bordered).table-row-gray-500 tr{border-bottom-color:var(--bs-gray-500)}.table:not(.table-bordered).table-row-gray-600 tr{border-bottom-color:var(--bs-gray-600)}.table:not(.table-bordered).table-row-gray-700 tr{border-bottom-color:var(--bs-gray-700)}.table:not(.table-bordered).table-row-gray-800 tr{border-bottom-color:var(--bs-gray-800)}.table:not(.table-bordered).table-row-gray-900 tr{border-bottom-color:var(--bs-gray-900)}.table-layout-fixed{table-layout:fixed}.table-sort:after{opacity:0}.table-sort,.table-sort-asc,.table-sort-desc{vertical-align:middle}.table-sort-asc:after,.table-sort-desc:after,.table-sort:after{position:relative;display:inline-block;width:.75rem;height:.75rem;content:\" \";bottom:auto;right:auto;left:auto;margin-left:.5rem}.table-sort-asc:after{opacity:1;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M3.23571 2.72011L4.97917 4.46358C5.15176 4.63618 5.43158 4.63617 5.60417 4.46358C5.77676 4.29099 5.77676 4.01118 5.60417 3.83861L3.29463 1.52904C3.13192 1.36629 2.86809 1.36629 2.70538 1.52904L0.395812 3.83861C0.22325 4.01117 0.22325 4.29099 0.395812 4.46358C0.568437 4.63617 0.84825 4.63617 1.02081 4.46358L2.76429 2.72011C2.89446 2.58994 3.10554 2.58994 3.23571 2.72011Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M3.23571 2.72011L4.97917 4.46358C5.15176 4.63618 5.43158 4.63617 5.60417 4.46358C5.77676 4.29099 5.77676 4.01118 5.60417 3.83861L3.29463 1.52904C3.13192 1.36629 2.86809 1.36629 2.70538 1.52904L0.395812 3.83861C0.22325 4.01117 0.22325 4.29099 0.395812 4.46358C0.568437 4.63617 0.84825 4.63617 1.02081 4.46358L2.76429 2.72011C2.89446 2.58994 3.10554 2.58994 3.23571 2.72011Z'/%3e%3c/svg%3e\")}.table-sort-desc:after{opacity:1;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M2.76429 3.27989L1.02083 1.53642C0.848244 1.36382 0.568419 1.36383 0.395831 1.53642C0.223244 1.70901 0.223244 1.98882 0.395831 2.16139L2.70537 4.47096C2.86808 4.63371 3.13191 4.63371 3.29462 4.47096L5.60419 2.16139C5.77675 1.98883 5.77675 1.70901 5.60419 1.53642C5.43156 1.36383 5.15175 1.36383 4.97919 1.53642L3.23571 3.27989C3.10554 3.41006 2.89446 3.41006 2.76429 3.27989Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M2.76429 3.27989L1.02083 1.53642C0.848244 1.36382 0.568419 1.36383 0.395831 1.53642C0.223244 1.70901 0.223244 1.98882 0.395831 2.16139L2.70537 4.47096C2.86808 4.63371 3.13191 4.63371 3.29462 4.47096L5.60419 2.16139C5.77675 1.98883 5.77675 1.70901 5.60419 1.53642C5.43156 1.36383 5.15175 1.36383 4.97919 1.53642L3.23571 3.27989C3.10554 3.41006 2.89446 3.41006 2.76429 3.27989Z'/%3e%3c/svg%3e\")}.table-loading-message{display:none;position:absolute;top:50%;left:50%;border-radius:.475rem;box-shadow:var(--bs-table-loading-message-box-shadow);background-color:var(--bs-body-bg);color:var(--bs-gray-700);font-weight:600;margin:0!important;width:auto;padding:.85rem 2rem!important;transform:translateX(-50%) translateY(-50%)}.table-loading{position:relative}.table-loading .table-loading-message{display:block}.table.g-0 td,.table.g-0 th{padding:0}.table.g-0 td.dtr-control,.table.g-0 th.dtr-control{padding-left:0!important}.table.gy-0 td,.table.gy-0 th{padding-top:0;padding-bottom:0}.table.gx-0 td,.table.gx-0 th{padding-left:0;padding-right:0}.table.gx-0 td.dtr-control,.table.gx-0 th.dtr-control{padding-left:0!important}.table.gs-0 td:first-child,.table.gs-0 th:first-child{padding-left:0}.table.gs-0 td:last-child,.table.gs-0 th:last-child{padding-right:0}.table.gs-0 td.dtr-control:first-child,.table.gs-0 th.dtr-control:first-child{padding-left:0!important}.table.g-1 td,.table.g-1 th{padding:.25rem}.table.g-1 td.dtr-control,.table.g-1 th.dtr-control{padding-left:.25rem!important}.table.gy-1 td,.table.gy-1 th{padding-top:.25rem;padding-bottom:.25rem}.table.gx-1 td,.table.gx-1 th{padding-left:.25rem;padding-right:.25rem}.table.gx-1 td.dtr-control,.table.gx-1 th.dtr-control{padding-left:.25rem!important}.table.gs-1 td:first-child,.table.gs-1 th:first-child{padding-left:.25rem}.table.gs-1 td:last-child,.table.gs-1 th:last-child{padding-right:.25rem}.table.gs-1 td.dtr-control:first-child,.table.gs-1 th.dtr-control:first-child{padding-left:.25rem!important}.table.g-2 td,.table.g-2 th{padding:.5rem}.table.g-2 td.dtr-control,.table.g-2 th.dtr-control{padding-left:.5rem!important}.table.gy-2 td,.table.gy-2 th{padding-top:.5rem;padding-bottom:.5rem}.table.gx-2 td,.table.gx-2 th{padding-left:.5rem;padding-right:.5rem}.table.gx-2 td.dtr-control,.table.gx-2 th.dtr-control{padding-left:.5rem!important}.table.gs-2 td:first-child,.table.gs-2 th:first-child{padding-left:.5rem}.table.gs-2 td:last-child,.table.gs-2 th:last-child{padding-right:.5rem}.table.gs-2 td.dtr-control:first-child,.table.gs-2 th.dtr-control:first-child{padding-left:.5rem!important}.table.g-3 td,.table.g-3 th{padding:.75rem}.table.g-3 td.dtr-control,.table.g-3 th.dtr-control{padding-left:.75rem!important}.table.gy-3 td,.table.gy-3 th{padding-top:.75rem;padding-bottom:.75rem}.table.gx-3 td,.table.gx-3 th{padding-left:.75rem;padding-right:.75rem}.table.gx-3 td.dtr-control,.table.gx-3 th.dtr-control{padding-left:.75rem!important}.table.gs-3 td:first-child,.table.gs-3 th:first-child{padding-left:.75rem}.table.gs-3 td:last-child,.table.gs-3 th:last-child{padding-right:.75rem}.table.gs-3 td.dtr-control:first-child,.table.gs-3 th.dtr-control:first-child{padding-left:.75rem!important}.table.g-4 td,.table.g-4 th{padding:1rem}.table.g-4 td.dtr-control,.table.g-4 th.dtr-control{padding-left:1rem!important}.table.gy-4 td,.table.gy-4 th{padding-top:1rem;padding-bottom:1rem}.table.gx-4 td,.table.gx-4 th{padding-left:1rem;padding-right:1rem}.table.gx-4 td.dtr-control,.table.gx-4 th.dtr-control{padding-left:1rem!important}.table.gs-4 td:first-child,.table.gs-4 th:first-child{padding-left:1rem}.table.gs-4 td:last-child,.table.gs-4 th:last-child{padding-right:1rem}.table.gs-4 td.dtr-control:first-child,.table.gs-4 th.dtr-control:first-child{padding-left:1rem!important}.table.g-5 td,.table.g-5 th{padding:1.25rem}.table.g-5 td.dtr-control,.table.g-5 th.dtr-control{padding-left:1.25rem!important}.table.gy-5 td,.table.gy-5 th{padding-top:1.25rem;padding-bottom:1.25rem}.table.gx-5 td,.table.gx-5 th{padding-left:1.25rem;padding-right:1.25rem}.table.gx-5 td.dtr-control,.table.gx-5 th.dtr-control{padding-left:1.25rem!important}.table.gs-5 td:first-child,.table.gs-5 th:first-child{padding-left:1.25rem}.table.gs-5 td:last-child,.table.gs-5 th:last-child{padding-right:1.25rem}.table.gs-5 td.dtr-control:first-child,.table.gs-5 th.dtr-control:first-child{padding-left:1.25rem!important}.table.g-6 td,.table.g-6 th{padding:1.5rem}.table.g-6 td.dtr-control,.table.g-6 th.dtr-control{padding-left:1.5rem!important}.table.gy-6 td,.table.gy-6 th{padding-top:1.5rem;padding-bottom:1.5rem}.table.gx-6 td,.table.gx-6 th{padding-left:1.5rem;padding-right:1.5rem}.table.gx-6 td.dtr-control,.table.gx-6 th.dtr-control{padding-left:1.5rem!important}.table.gs-6 td:first-child,.table.gs-6 th:first-child{padding-left:1.5rem}.table.gs-6 td:last-child,.table.gs-6 th:last-child{padding-right:1.5rem}.table.gs-6 td.dtr-control:first-child,.table.gs-6 th.dtr-control:first-child{padding-left:1.5rem!important}.table.g-7 td,.table.g-7 th{padding:1.75rem}.table.g-7 td.dtr-control,.table.g-7 th.dtr-control{padding-left:1.75rem!important}.table.gy-7 td,.table.gy-7 th{padding-top:1.75rem;padding-bottom:1.75rem}.table.gx-7 td,.table.gx-7 th{padding-left:1.75rem;padding-right:1.75rem}.table.gx-7 td.dtr-control,.table.gx-7 th.dtr-control{padding-left:1.75rem!important}.table.gs-7 td:first-child,.table.gs-7 th:first-child{padding-left:1.75rem}.table.gs-7 td:last-child,.table.gs-7 th:last-child{padding-right:1.75rem}.table.gs-7 td.dtr-control:first-child,.table.gs-7 th.dtr-control:first-child{padding-left:1.75rem!important}.table.g-8 td,.table.g-8 th{padding:2rem}.table.g-8 td.dtr-control,.table.g-8 th.dtr-control{padding-left:2rem!important}.table.gy-8 td,.table.gy-8 th{padding-top:2rem;padding-bottom:2rem}.table.gx-8 td,.table.gx-8 th{padding-left:2rem;padding-right:2rem}.table.gx-8 td.dtr-control,.table.gx-8 th.dtr-control{padding-left:2rem!important}.table.gs-8 td:first-child,.table.gs-8 th:first-child{padding-left:2rem}.table.gs-8 td:last-child,.table.gs-8 th:last-child{padding-right:2rem}.table.gs-8 td.dtr-control:first-child,.table.gs-8 th.dtr-control:first-child{padding-left:2rem!important}.table.g-9 td,.table.g-9 th{padding:2.25rem}.table.g-9 td.dtr-control,.table.g-9 th.dtr-control{padding-left:2.25rem!important}.table.gy-9 td,.table.gy-9 th{padding-top:2.25rem;padding-bottom:2.25rem}.table.gx-9 td,.table.gx-9 th{padding-left:2.25rem;padding-right:2.25rem}.table.gx-9 td.dtr-control,.table.gx-9 th.dtr-control{padding-left:2.25rem!important}.table.gs-9 td:first-child,.table.gs-9 th:first-child{padding-left:2.25rem}.table.gs-9 td:last-child,.table.gs-9 th:last-child{padding-right:2.25rem}.table.gs-9 td.dtr-control:first-child,.table.gs-9 th.dtr-control:first-child{padding-left:2.25rem!important}.table.g-10 td,.table.g-10 th{padding:2.5rem}.table.g-10 td.dtr-control,.table.g-10 th.dtr-control{padding-left:2.5rem!important}.table.gy-10 td,.table.gy-10 th{padding-top:2.5rem;padding-bottom:2.5rem}.table.gx-10 td,.table.gx-10 th{padding-left:2.5rem;padding-right:2.5rem}.table.gx-10 td.dtr-control,.table.gx-10 th.dtr-control{padding-left:2.5rem!important}.table.gs-10 td:first-child,.table.gs-10 th:first-child{padding-left:2.5rem}.table.gs-10 td:last-child,.table.gs-10 th:last-child{padding-right:2.5rem}.table.gs-10 td.dtr-control:first-child,.table.gs-10 th.dtr-control:first-child{padding-left:2.5rem!important}@media (min-width:576px){.table.g-sm-0 td,.table.g-sm-0 th{padding:0}.table.g-sm-0 td.dtr-control,.table.g-sm-0 th.dtr-control{padding-left:0!important}.table.gy-sm-0 td,.table.gy-sm-0 th{padding-top:0;padding-bottom:0}.table.gx-sm-0 td,.table.gx-sm-0 th{padding-left:0;padding-right:0}.table.gx-sm-0 td.dtr-control,.table.gx-sm-0 th.dtr-control{padding-left:0!important}.table.gs-sm-0 td:first-child,.table.gs-sm-0 th:first-child{padding-left:0}.table.gs-sm-0 td:last-child,.table.gs-sm-0 th:last-child{padding-right:0}.table.gs-sm-0 td.dtr-control:first-child,.table.gs-sm-0 th.dtr-control:first-child{padding-left:0!important}.table.g-sm-1 td,.table.g-sm-1 th{padding:.25rem}.table.g-sm-1 td.dtr-control,.table.g-sm-1 th.dtr-control{padding-left:.25rem!important}.table.gy-sm-1 td,.table.gy-sm-1 th{padding-top:.25rem;padding-bottom:.25rem}.table.gx-sm-1 td,.table.gx-sm-1 th{padding-left:.25rem;padding-right:.25rem}.table.gx-sm-1 td.dtr-control,.table.gx-sm-1 th.dtr-control{padding-left:.25rem!important}.table.gs-sm-1 td:first-child,.table.gs-sm-1 th:first-child{padding-left:.25rem}.table.gs-sm-1 td:last-child,.table.gs-sm-1 th:last-child{padding-right:.25rem}.table.gs-sm-1 td.dtr-control:first-child,.table.gs-sm-1 th.dtr-control:first-child{padding-left:.25rem!important}.table.g-sm-2 td,.table.g-sm-2 th{padding:.5rem}.table.g-sm-2 td.dtr-control,.table.g-sm-2 th.dtr-control{padding-left:.5rem!important}.table.gy-sm-2 td,.table.gy-sm-2 th{padding-top:.5rem;padding-bottom:.5rem}.table.gx-sm-2 td,.table.gx-sm-2 th{padding-left:.5rem;padding-right:.5rem}.table.gx-sm-2 td.dtr-control,.table.gx-sm-2 th.dtr-control{padding-left:.5rem!important}.table.gs-sm-2 td:first-child,.table.gs-sm-2 th:first-child{padding-left:.5rem}.table.gs-sm-2 td:last-child,.table.gs-sm-2 th:last-child{padding-right:.5rem}.table.gs-sm-2 td.dtr-control:first-child,.table.gs-sm-2 th.dtr-control:first-child{padding-left:.5rem!important}.table.g-sm-3 td,.table.g-sm-3 th{padding:.75rem}.table.g-sm-3 td.dtr-control,.table.g-sm-3 th.dtr-control{padding-left:.75rem!important}.table.gy-sm-3 td,.table.gy-sm-3 th{padding-top:.75rem;padding-bottom:.75rem}.table.gx-sm-3 td,.table.gx-sm-3 th{padding-left:.75rem;padding-right:.75rem}.table.gx-sm-3 td.dtr-control,.table.gx-sm-3 th.dtr-control{padding-left:.75rem!important}.table.gs-sm-3 td:first-child,.table.gs-sm-3 th:first-child{padding-left:.75rem}.table.gs-sm-3 td:last-child,.table.gs-sm-3 th:last-child{padding-right:.75rem}.table.gs-sm-3 td.dtr-control:first-child,.table.gs-sm-3 th.dtr-control:first-child{padding-left:.75rem!important}.table.g-sm-4 td,.table.g-sm-4 th{padding:1rem}.table.g-sm-4 td.dtr-control,.table.g-sm-4 th.dtr-control{padding-left:1rem!important}.table.gy-sm-4 td,.table.gy-sm-4 th{padding-top:1rem;padding-bottom:1rem}.table.gx-sm-4 td,.table.gx-sm-4 th{padding-left:1rem;padding-right:1rem}.table.gx-sm-4 td.dtr-control,.table.gx-sm-4 th.dtr-control{padding-left:1rem!important}.table.gs-sm-4 td:first-child,.table.gs-sm-4 th:first-child{padding-left:1rem}.table.gs-sm-4 td:last-child,.table.gs-sm-4 th:last-child{padding-right:1rem}.table.gs-sm-4 td.dtr-control:first-child,.table.gs-sm-4 th.dtr-control:first-child{padding-left:1rem!important}.table.g-sm-5 td,.table.g-sm-5 th{padding:1.25rem}.table.g-sm-5 td.dtr-control,.table.g-sm-5 th.dtr-control{padding-left:1.25rem!important}.table.gy-sm-5 td,.table.gy-sm-5 th{padding-top:1.25rem;padding-bottom:1.25rem}.table.gx-sm-5 td,.table.gx-sm-5 th{padding-left:1.25rem;padding-right:1.25rem}.table.gx-sm-5 td.dtr-control,.table.gx-sm-5 th.dtr-control{padding-left:1.25rem!important}.table.gs-sm-5 td:first-child,.table.gs-sm-5 th:first-child{padding-left:1.25rem}.table.gs-sm-5 td:last-child,.table.gs-sm-5 th:last-child{padding-right:1.25rem}.table.gs-sm-5 td.dtr-control:first-child,.table.gs-sm-5 th.dtr-control:first-child{padding-left:1.25rem!important}.table.g-sm-6 td,.table.g-sm-6 th{padding:1.5rem}.table.g-sm-6 td.dtr-control,.table.g-sm-6 th.dtr-control{padding-left:1.5rem!important}.table.gy-sm-6 td,.table.gy-sm-6 th{padding-top:1.5rem;padding-bottom:1.5rem}.table.gx-sm-6 td,.table.gx-sm-6 th{padding-left:1.5rem;padding-right:1.5rem}.table.gx-sm-6 td.dtr-control,.table.gx-sm-6 th.dtr-control{padding-left:1.5rem!important}.table.gs-sm-6 td:first-child,.table.gs-sm-6 th:first-child{padding-left:1.5rem}.table.gs-sm-6 td:last-child,.table.gs-sm-6 th:last-child{padding-right:1.5rem}.table.gs-sm-6 td.dtr-control:first-child,.table.gs-sm-6 th.dtr-control:first-child{padding-left:1.5rem!important}.table.g-sm-7 td,.table.g-sm-7 th{padding:1.75rem}.table.g-sm-7 td.dtr-control,.table.g-sm-7 th.dtr-control{padding-left:1.75rem!important}.table.gy-sm-7 td,.table.gy-sm-7 th{padding-top:1.75rem;padding-bottom:1.75rem}.table.gx-sm-7 td,.table.gx-sm-7 th{padding-left:1.75rem;padding-right:1.75rem}.table.gx-sm-7 td.dtr-control,.table.gx-sm-7 th.dtr-control{padding-left:1.75rem!important}.table.gs-sm-7 td:first-child,.table.gs-sm-7 th:first-child{padding-left:1.75rem}.table.gs-sm-7 td:last-child,.table.gs-sm-7 th:last-child{padding-right:1.75rem}.table.gs-sm-7 td.dtr-control:first-child,.table.gs-sm-7 th.dtr-control:first-child{padding-left:1.75rem!important}.table.g-sm-8 td,.table.g-sm-8 th{padding:2rem}.table.g-sm-8 td.dtr-control,.table.g-sm-8 th.dtr-control{padding-left:2rem!important}.table.gy-sm-8 td,.table.gy-sm-8 th{padding-top:2rem;padding-bottom:2rem}.table.gx-sm-8 td,.table.gx-sm-8 th{padding-left:2rem;padding-right:2rem}.table.gx-sm-8 td.dtr-control,.table.gx-sm-8 th.dtr-control{padding-left:2rem!important}.table.gs-sm-8 td:first-child,.table.gs-sm-8 th:first-child{padding-left:2rem}.table.gs-sm-8 td:last-child,.table.gs-sm-8 th:last-child{padding-right:2rem}.table.gs-sm-8 td.dtr-control:first-child,.table.gs-sm-8 th.dtr-control:first-child{padding-left:2rem!important}.table.g-sm-9 td,.table.g-sm-9 th{padding:2.25rem}.table.g-sm-9 td.dtr-control,.table.g-sm-9 th.dtr-control{padding-left:2.25rem!important}.table.gy-sm-9 td,.table.gy-sm-9 th{padding-top:2.25rem;padding-bottom:2.25rem}.table.gx-sm-9 td,.table.gx-sm-9 th{padding-left:2.25rem;padding-right:2.25rem}.table.gx-sm-9 td.dtr-control,.table.gx-sm-9 th.dtr-control{padding-left:2.25rem!important}.table.gs-sm-9 td:first-child,.table.gs-sm-9 th:first-child{padding-left:2.25rem}.table.gs-sm-9 td:last-child,.table.gs-sm-9 th:last-child{padding-right:2.25rem}.table.gs-sm-9 td.dtr-control:first-child,.table.gs-sm-9 th.dtr-control:first-child{padding-left:2.25rem!important}.table.g-sm-10 td,.table.g-sm-10 th{padding:2.5rem}.table.g-sm-10 td.dtr-control,.table.g-sm-10 th.dtr-control{padding-left:2.5rem!important}.table.gy-sm-10 td,.table.gy-sm-10 th{padding-top:2.5rem;padding-bottom:2.5rem}.table.gx-sm-10 td,.table.gx-sm-10 th{padding-left:2.5rem;padding-right:2.5rem}.table.gx-sm-10 td.dtr-control,.table.gx-sm-10 th.dtr-control{padding-left:2.5rem!important}.table.gs-sm-10 td:first-child,.table.gs-sm-10 th:first-child{padding-left:2.5rem}.table.gs-sm-10 td:last-child,.table.gs-sm-10 th:last-child{padding-right:2.5rem}.table.gs-sm-10 td.dtr-control:first-child,.table.gs-sm-10 th.dtr-control:first-child{padding-left:2.5rem!important}}@media (min-width:768px){.table.g-md-0 td,.table.g-md-0 th{padding:0}.table.g-md-0 td.dtr-control,.table.g-md-0 th.dtr-control{padding-left:0!important}.table.gy-md-0 td,.table.gy-md-0 th{padding-top:0;padding-bottom:0}.table.gx-md-0 td,.table.gx-md-0 th{padding-left:0;padding-right:0}.table.gx-md-0 td.dtr-control,.table.gx-md-0 th.dtr-control{padding-left:0!important}.table.gs-md-0 td:first-child,.table.gs-md-0 th:first-child{padding-left:0}.table.gs-md-0 td:last-child,.table.gs-md-0 th:last-child{padding-right:0}.table.gs-md-0 td.dtr-control:first-child,.table.gs-md-0 th.dtr-control:first-child{padding-left:0!important}.table.g-md-1 td,.table.g-md-1 th{padding:.25rem}.table.g-md-1 td.dtr-control,.table.g-md-1 th.dtr-control{padding-left:.25rem!important}.table.gy-md-1 td,.table.gy-md-1 th{padding-top:.25rem;padding-bottom:.25rem}.table.gx-md-1 td,.table.gx-md-1 th{padding-left:.25rem;padding-right:.25rem}.table.gx-md-1 td.dtr-control,.table.gx-md-1 th.dtr-control{padding-left:.25rem!important}.table.gs-md-1 td:first-child,.table.gs-md-1 th:first-child{padding-left:.25rem}.table.gs-md-1 td:last-child,.table.gs-md-1 th:last-child{padding-right:.25rem}.table.gs-md-1 td.dtr-control:first-child,.table.gs-md-1 th.dtr-control:first-child{padding-left:.25rem!important}.table.g-md-2 td,.table.g-md-2 th{padding:.5rem}.table.g-md-2 td.dtr-control,.table.g-md-2 th.dtr-control{padding-left:.5rem!important}.table.gy-md-2 td,.table.gy-md-2 th{padding-top:.5rem;padding-bottom:.5rem}.table.gx-md-2 td,.table.gx-md-2 th{padding-left:.5rem;padding-right:.5rem}.table.gx-md-2 td.dtr-control,.table.gx-md-2 th.dtr-control{padding-left:.5rem!important}.table.gs-md-2 td:first-child,.table.gs-md-2 th:first-child{padding-left:.5rem}.table.gs-md-2 td:last-child,.table.gs-md-2 th:last-child{padding-right:.5rem}.table.gs-md-2 td.dtr-control:first-child,.table.gs-md-2 th.dtr-control:first-child{padding-left:.5rem!important}.table.g-md-3 td,.table.g-md-3 th{padding:.75rem}.table.g-md-3 td.dtr-control,.table.g-md-3 th.dtr-control{padding-left:.75rem!important}.table.gy-md-3 td,.table.gy-md-3 th{padding-top:.75rem;padding-bottom:.75rem}.table.gx-md-3 td,.table.gx-md-3 th{padding-left:.75rem;padding-right:.75rem}.table.gx-md-3 td.dtr-control,.table.gx-md-3 th.dtr-control{padding-left:.75rem!important}.table.gs-md-3 td:first-child,.table.gs-md-3 th:first-child{padding-left:.75rem}.table.gs-md-3 td:last-child,.table.gs-md-3 th:last-child{padding-right:.75rem}.table.gs-md-3 td.dtr-control:first-child,.table.gs-md-3 th.dtr-control:first-child{padding-left:.75rem!important}.table.g-md-4 td,.table.g-md-4 th{padding:1rem}.table.g-md-4 td.dtr-control,.table.g-md-4 th.dtr-control{padding-left:1rem!important}.table.gy-md-4 td,.table.gy-md-4 th{padding-top:1rem;padding-bottom:1rem}.table.gx-md-4 td,.table.gx-md-4 th{padding-left:1rem;padding-right:1rem}.table.gx-md-4 td.dtr-control,.table.gx-md-4 th.dtr-control{padding-left:1rem!important}.table.gs-md-4 td:first-child,.table.gs-md-4 th:first-child{padding-left:1rem}.table.gs-md-4 td:last-child,.table.gs-md-4 th:last-child{padding-right:1rem}.table.gs-md-4 td.dtr-control:first-child,.table.gs-md-4 th.dtr-control:first-child{padding-left:1rem!important}.table.g-md-5 td,.table.g-md-5 th{padding:1.25rem}.table.g-md-5 td.dtr-control,.table.g-md-5 th.dtr-control{padding-left:1.25rem!important}.table.gy-md-5 td,.table.gy-md-5 th{padding-top:1.25rem;padding-bottom:1.25rem}.table.gx-md-5 td,.table.gx-md-5 th{padding-left:1.25rem;padding-right:1.25rem}.table.gx-md-5 td.dtr-control,.table.gx-md-5 th.dtr-control{padding-left:1.25rem!important}.table.gs-md-5 td:first-child,.table.gs-md-5 th:first-child{padding-left:1.25rem}.table.gs-md-5 td:last-child,.table.gs-md-5 th:last-child{padding-right:1.25rem}.table.gs-md-5 td.dtr-control:first-child,.table.gs-md-5 th.dtr-control:first-child{padding-left:1.25rem!important}.table.g-md-6 td,.table.g-md-6 th{padding:1.5rem}.table.g-md-6 td.dtr-control,.table.g-md-6 th.dtr-control{padding-left:1.5rem!important}.table.gy-md-6 td,.table.gy-md-6 th{padding-top:1.5rem;padding-bottom:1.5rem}.table.gx-md-6 td,.table.gx-md-6 th{padding-left:1.5rem;padding-right:1.5rem}.table.gx-md-6 td.dtr-control,.table.gx-md-6 th.dtr-control{padding-left:1.5rem!important}.table.gs-md-6 td:first-child,.table.gs-md-6 th:first-child{padding-left:1.5rem}.table.gs-md-6 td:last-child,.table.gs-md-6 th:last-child{padding-right:1.5rem}.table.gs-md-6 td.dtr-control:first-child,.table.gs-md-6 th.dtr-control:first-child{padding-left:1.5rem!important}.table.g-md-7 td,.table.g-md-7 th{padding:1.75rem}.table.g-md-7 td.dtr-control,.table.g-md-7 th.dtr-control{padding-left:1.75rem!important}.table.gy-md-7 td,.table.gy-md-7 th{padding-top:1.75rem;padding-bottom:1.75rem}.table.gx-md-7 td,.table.gx-md-7 th{padding-left:1.75rem;padding-right:1.75rem}.table.gx-md-7 td.dtr-control,.table.gx-md-7 th.dtr-control{padding-left:1.75rem!important}.table.gs-md-7 td:first-child,.table.gs-md-7 th:first-child{padding-left:1.75rem}.table.gs-md-7 td:last-child,.table.gs-md-7 th:last-child{padding-right:1.75rem}.table.gs-md-7 td.dtr-control:first-child,.table.gs-md-7 th.dtr-control:first-child{padding-left:1.75rem!important}.table.g-md-8 td,.table.g-md-8 th{padding:2rem}.table.g-md-8 td.dtr-control,.table.g-md-8 th.dtr-control{padding-left:2rem!important}.table.gy-md-8 td,.table.gy-md-8 th{padding-top:2rem;padding-bottom:2rem}.table.gx-md-8 td,.table.gx-md-8 th{padding-left:2rem;padding-right:2rem}.table.gx-md-8 td.dtr-control,.table.gx-md-8 th.dtr-control{padding-left:2rem!important}.table.gs-md-8 td:first-child,.table.gs-md-8 th:first-child{padding-left:2rem}.table.gs-md-8 td:last-child,.table.gs-md-8 th:last-child{padding-right:2rem}.table.gs-md-8 td.dtr-control:first-child,.table.gs-md-8 th.dtr-control:first-child{padding-left:2rem!important}.table.g-md-9 td,.table.g-md-9 th{padding:2.25rem}.table.g-md-9 td.dtr-control,.table.g-md-9 th.dtr-control{padding-left:2.25rem!important}.table.gy-md-9 td,.table.gy-md-9 th{padding-top:2.25rem;padding-bottom:2.25rem}.table.gx-md-9 td,.table.gx-md-9 th{padding-left:2.25rem;padding-right:2.25rem}.table.gx-md-9 td.dtr-control,.table.gx-md-9 th.dtr-control{padding-left:2.25rem!important}.table.gs-md-9 td:first-child,.table.gs-md-9 th:first-child{padding-left:2.25rem}.table.gs-md-9 td:last-child,.table.gs-md-9 th:last-child{padding-right:2.25rem}.table.gs-md-9 td.dtr-control:first-child,.table.gs-md-9 th.dtr-control:first-child{padding-left:2.25rem!important}.table.g-md-10 td,.table.g-md-10 th{padding:2.5rem}.table.g-md-10 td.dtr-control,.table.g-md-10 th.dtr-control{padding-left:2.5rem!important}.table.gy-md-10 td,.table.gy-md-10 th{padding-top:2.5rem;padding-bottom:2.5rem}.table.gx-md-10 td,.table.gx-md-10 th{padding-left:2.5rem;padding-right:2.5rem}.table.gx-md-10 td.dtr-control,.table.gx-md-10 th.dtr-control{padding-left:2.5rem!important}.table.gs-md-10 td:first-child,.table.gs-md-10 th:first-child{padding-left:2.5rem}.table.gs-md-10 td:last-child,.table.gs-md-10 th:last-child{padding-right:2.5rem}.table.gs-md-10 td.dtr-control:first-child,.table.gs-md-10 th.dtr-control:first-child{padding-left:2.5rem!important}}@media (min-width:992px){.table.g-lg-0 td,.table.g-lg-0 th{padding:0}.table.g-lg-0 td.dtr-control,.table.g-lg-0 th.dtr-control{padding-left:0!important}.table.gy-lg-0 td,.table.gy-lg-0 th{padding-top:0;padding-bottom:0}.table.gx-lg-0 td,.table.gx-lg-0 th{padding-left:0;padding-right:0}.table.gx-lg-0 td.dtr-control,.table.gx-lg-0 th.dtr-control{padding-left:0!important}.table.gs-lg-0 td:first-child,.table.gs-lg-0 th:first-child{padding-left:0}.table.gs-lg-0 td:last-child,.table.gs-lg-0 th:last-child{padding-right:0}.table.gs-lg-0 td.dtr-control:first-child,.table.gs-lg-0 th.dtr-control:first-child{padding-left:0!important}.table.g-lg-1 td,.table.g-lg-1 th{padding:.25rem}.table.g-lg-1 td.dtr-control,.table.g-lg-1 th.dtr-control{padding-left:.25rem!important}.table.gy-lg-1 td,.table.gy-lg-1 th{padding-top:.25rem;padding-bottom:.25rem}.table.gx-lg-1 td,.table.gx-lg-1 th{padding-left:.25rem;padding-right:.25rem}.table.gx-lg-1 td.dtr-control,.table.gx-lg-1 th.dtr-control{padding-left:.25rem!important}.table.gs-lg-1 td:first-child,.table.gs-lg-1 th:first-child{padding-left:.25rem}.table.gs-lg-1 td:last-child,.table.gs-lg-1 th:last-child{padding-right:.25rem}.table.gs-lg-1 td.dtr-control:first-child,.table.gs-lg-1 th.dtr-control:first-child{padding-left:.25rem!important}.table.g-lg-2 td,.table.g-lg-2 th{padding:.5rem}.table.g-lg-2 td.dtr-control,.table.g-lg-2 th.dtr-control{padding-left:.5rem!important}.table.gy-lg-2 td,.table.gy-lg-2 th{padding-top:.5rem;padding-bottom:.5rem}.table.gx-lg-2 td,.table.gx-lg-2 th{padding-left:.5rem;padding-right:.5rem}.table.gx-lg-2 td.dtr-control,.table.gx-lg-2 th.dtr-control{padding-left:.5rem!important}.table.gs-lg-2 td:first-child,.table.gs-lg-2 th:first-child{padding-left:.5rem}.table.gs-lg-2 td:last-child,.table.gs-lg-2 th:last-child{padding-right:.5rem}.table.gs-lg-2 td.dtr-control:first-child,.table.gs-lg-2 th.dtr-control:first-child{padding-left:.5rem!important}.table.g-lg-3 td,.table.g-lg-3 th{padding:.75rem}.table.g-lg-3 td.dtr-control,.table.g-lg-3 th.dtr-control{padding-left:.75rem!important}.table.gy-lg-3 td,.table.gy-lg-3 th{padding-top:.75rem;padding-bottom:.75rem}.table.gx-lg-3 td,.table.gx-lg-3 th{padding-left:.75rem;padding-right:.75rem}.table.gx-lg-3 td.dtr-control,.table.gx-lg-3 th.dtr-control{padding-left:.75rem!important}.table.gs-lg-3 td:first-child,.table.gs-lg-3 th:first-child{padding-left:.75rem}.table.gs-lg-3 td:last-child,.table.gs-lg-3 th:last-child{padding-right:.75rem}.table.gs-lg-3 td.dtr-control:first-child,.table.gs-lg-3 th.dtr-control:first-child{padding-left:.75rem!important}.table.g-lg-4 td,.table.g-lg-4 th{padding:1rem}.table.g-lg-4 td.dtr-control,.table.g-lg-4 th.dtr-control{padding-left:1rem!important}.table.gy-lg-4 td,.table.gy-lg-4 th{padding-top:1rem;padding-bottom:1rem}.table.gx-lg-4 td,.table.gx-lg-4 th{padding-left:1rem;padding-right:1rem}.table.gx-lg-4 td.dtr-control,.table.gx-lg-4 th.dtr-control{padding-left:1rem!important}.table.gs-lg-4 td:first-child,.table.gs-lg-4 th:first-child{padding-left:1rem}.table.gs-lg-4 td:last-child,.table.gs-lg-4 th:last-child{padding-right:1rem}.table.gs-lg-4 td.dtr-control:first-child,.table.gs-lg-4 th.dtr-control:first-child{padding-left:1rem!important}.table.g-lg-5 td,.table.g-lg-5 th{padding:1.25rem}.table.g-lg-5 td.dtr-control,.table.g-lg-5 th.dtr-control{padding-left:1.25rem!important}.table.gy-lg-5 td,.table.gy-lg-5 th{padding-top:1.25rem;padding-bottom:1.25rem}.table.gx-lg-5 td,.table.gx-lg-5 th{padding-left:1.25rem;padding-right:1.25rem}.table.gx-lg-5 td.dtr-control,.table.gx-lg-5 th.dtr-control{padding-left:1.25rem!important}.table.gs-lg-5 td:first-child,.table.gs-lg-5 th:first-child{padding-left:1.25rem}.table.gs-lg-5 td:last-child,.table.gs-lg-5 th:last-child{padding-right:1.25rem}.table.gs-lg-5 td.dtr-control:first-child,.table.gs-lg-5 th.dtr-control:first-child{padding-left:1.25rem!important}.table.g-lg-6 td,.table.g-lg-6 th{padding:1.5rem}.table.g-lg-6 td.dtr-control,.table.g-lg-6 th.dtr-control{padding-left:1.5rem!important}.table.gy-lg-6 td,.table.gy-lg-6 th{padding-top:1.5rem;padding-bottom:1.5rem}.table.gx-lg-6 td,.table.gx-lg-6 th{padding-left:1.5rem;padding-right:1.5rem}.table.gx-lg-6 td.dtr-control,.table.gx-lg-6 th.dtr-control{padding-left:1.5rem!important}.table.gs-lg-6 td:first-child,.table.gs-lg-6 th:first-child{padding-left:1.5rem}.table.gs-lg-6 td:last-child,.table.gs-lg-6 th:last-child{padding-right:1.5rem}.table.gs-lg-6 td.dtr-control:first-child,.table.gs-lg-6 th.dtr-control:first-child{padding-left:1.5rem!important}.table.g-lg-7 td,.table.g-lg-7 th{padding:1.75rem}.table.g-lg-7 td.dtr-control,.table.g-lg-7 th.dtr-control{padding-left:1.75rem!important}.table.gy-lg-7 td,.table.gy-lg-7 th{padding-top:1.75rem;padding-bottom:1.75rem}.table.gx-lg-7 td,.table.gx-lg-7 th{padding-left:1.75rem;padding-right:1.75rem}.table.gx-lg-7 td.dtr-control,.table.gx-lg-7 th.dtr-control{padding-left:1.75rem!important}.table.gs-lg-7 td:first-child,.table.gs-lg-7 th:first-child{padding-left:1.75rem}.table.gs-lg-7 td:last-child,.table.gs-lg-7 th:last-child{padding-right:1.75rem}.table.gs-lg-7 td.dtr-control:first-child,.table.gs-lg-7 th.dtr-control:first-child{padding-left:1.75rem!important}.table.g-lg-8 td,.table.g-lg-8 th{padding:2rem}.table.g-lg-8 td.dtr-control,.table.g-lg-8 th.dtr-control{padding-left:2rem!important}.table.gy-lg-8 td,.table.gy-lg-8 th{padding-top:2rem;padding-bottom:2rem}.table.gx-lg-8 td,.table.gx-lg-8 th{padding-left:2rem;padding-right:2rem}.table.gx-lg-8 td.dtr-control,.table.gx-lg-8 th.dtr-control{padding-left:2rem!important}.table.gs-lg-8 td:first-child,.table.gs-lg-8 th:first-child{padding-left:2rem}.table.gs-lg-8 td:last-child,.table.gs-lg-8 th:last-child{padding-right:2rem}.table.gs-lg-8 td.dtr-control:first-child,.table.gs-lg-8 th.dtr-control:first-child{padding-left:2rem!important}.table.g-lg-9 td,.table.g-lg-9 th{padding:2.25rem}.table.g-lg-9 td.dtr-control,.table.g-lg-9 th.dtr-control{padding-left:2.25rem!important}.table.gy-lg-9 td,.table.gy-lg-9 th{padding-top:2.25rem;padding-bottom:2.25rem}.table.gx-lg-9 td,.table.gx-lg-9 th{padding-left:2.25rem;padding-right:2.25rem}.table.gx-lg-9 td.dtr-control,.table.gx-lg-9 th.dtr-control{padding-left:2.25rem!important}.table.gs-lg-9 td:first-child,.table.gs-lg-9 th:first-child{padding-left:2.25rem}.table.gs-lg-9 td:last-child,.table.gs-lg-9 th:last-child{padding-right:2.25rem}.table.gs-lg-9 td.dtr-control:first-child,.table.gs-lg-9 th.dtr-control:first-child{padding-left:2.25rem!important}.table.g-lg-10 td,.table.g-lg-10 th{padding:2.5rem}.table.g-lg-10 td.dtr-control,.table.g-lg-10 th.dtr-control{padding-left:2.5rem!important}.table.gy-lg-10 td,.table.gy-lg-10 th{padding-top:2.5rem;padding-bottom:2.5rem}.table.gx-lg-10 td,.table.gx-lg-10 th{padding-left:2.5rem;padding-right:2.5rem}.table.gx-lg-10 td.dtr-control,.table.gx-lg-10 th.dtr-control{padding-left:2.5rem!important}.table.gs-lg-10 td:first-child,.table.gs-lg-10 th:first-child{padding-left:2.5rem}.table.gs-lg-10 td:last-child,.table.gs-lg-10 th:last-child{padding-right:2.5rem}.table.gs-lg-10 td.dtr-control:first-child,.table.gs-lg-10 th.dtr-control:first-child{padding-left:2.5rem!important}}@media (min-width:1200px){.table.g-xl-0 td,.table.g-xl-0 th{padding:0}.table.g-xl-0 td.dtr-control,.table.g-xl-0 th.dtr-control{padding-left:0!important}.table.gy-xl-0 td,.table.gy-xl-0 th{padding-top:0;padding-bottom:0}.table.gx-xl-0 td,.table.gx-xl-0 th{padding-left:0;padding-right:0}.table.gx-xl-0 td.dtr-control,.table.gx-xl-0 th.dtr-control{padding-left:0!important}.table.gs-xl-0 td:first-child,.table.gs-xl-0 th:first-child{padding-left:0}.table.gs-xl-0 td:last-child,.table.gs-xl-0 th:last-child{padding-right:0}.table.gs-xl-0 td.dtr-control:first-child,.table.gs-xl-0 th.dtr-control:first-child{padding-left:0!important}.table.g-xl-1 td,.table.g-xl-1 th{padding:.25rem}.table.g-xl-1 td.dtr-control,.table.g-xl-1 th.dtr-control{padding-left:.25rem!important}.table.gy-xl-1 td,.table.gy-xl-1 th{padding-top:.25rem;padding-bottom:.25rem}.table.gx-xl-1 td,.table.gx-xl-1 th{padding-left:.25rem;padding-right:.25rem}.table.gx-xl-1 td.dtr-control,.table.gx-xl-1 th.dtr-control{padding-left:.25rem!important}.table.gs-xl-1 td:first-child,.table.gs-xl-1 th:first-child{padding-left:.25rem}.table.gs-xl-1 td:last-child,.table.gs-xl-1 th:last-child{padding-right:.25rem}.table.gs-xl-1 td.dtr-control:first-child,.table.gs-xl-1 th.dtr-control:first-child{padding-left:.25rem!important}.table.g-xl-2 td,.table.g-xl-2 th{padding:.5rem}.table.g-xl-2 td.dtr-control,.table.g-xl-2 th.dtr-control{padding-left:.5rem!important}.table.gy-xl-2 td,.table.gy-xl-2 th{padding-top:.5rem;padding-bottom:.5rem}.table.gx-xl-2 td,.table.gx-xl-2 th{padding-left:.5rem;padding-right:.5rem}.table.gx-xl-2 td.dtr-control,.table.gx-xl-2 th.dtr-control{padding-left:.5rem!important}.table.gs-xl-2 td:first-child,.table.gs-xl-2 th:first-child{padding-left:.5rem}.table.gs-xl-2 td:last-child,.table.gs-xl-2 th:last-child{padding-right:.5rem}.table.gs-xl-2 td.dtr-control:first-child,.table.gs-xl-2 th.dtr-control:first-child{padding-left:.5rem!important}.table.g-xl-3 td,.table.g-xl-3 th{padding:.75rem}.table.g-xl-3 td.dtr-control,.table.g-xl-3 th.dtr-control{padding-left:.75rem!important}.table.gy-xl-3 td,.table.gy-xl-3 th{padding-top:.75rem;padding-bottom:.75rem}.table.gx-xl-3 td,.table.gx-xl-3 th{padding-left:.75rem;padding-right:.75rem}.table.gx-xl-3 td.dtr-control,.table.gx-xl-3 th.dtr-control{padding-left:.75rem!important}.table.gs-xl-3 td:first-child,.table.gs-xl-3 th:first-child{padding-left:.75rem}.table.gs-xl-3 td:last-child,.table.gs-xl-3 th:last-child{padding-right:.75rem}.table.gs-xl-3 td.dtr-control:first-child,.table.gs-xl-3 th.dtr-control:first-child{padding-left:.75rem!important}.table.g-xl-4 td,.table.g-xl-4 th{padding:1rem}.table.g-xl-4 td.dtr-control,.table.g-xl-4 th.dtr-control{padding-left:1rem!important}.table.gy-xl-4 td,.table.gy-xl-4 th{padding-top:1rem;padding-bottom:1rem}.table.gx-xl-4 td,.table.gx-xl-4 th{padding-left:1rem;padding-right:1rem}.table.gx-xl-4 td.dtr-control,.table.gx-xl-4 th.dtr-control{padding-left:1rem!important}.table.gs-xl-4 td:first-child,.table.gs-xl-4 th:first-child{padding-left:1rem}.table.gs-xl-4 td:last-child,.table.gs-xl-4 th:last-child{padding-right:1rem}.table.gs-xl-4 td.dtr-control:first-child,.table.gs-xl-4 th.dtr-control:first-child{padding-left:1rem!important}.table.g-xl-5 td,.table.g-xl-5 th{padding:1.25rem}.table.g-xl-5 td.dtr-control,.table.g-xl-5 th.dtr-control{padding-left:1.25rem!important}.table.gy-xl-5 td,.table.gy-xl-5 th{padding-top:1.25rem;padding-bottom:1.25rem}.table.gx-xl-5 td,.table.gx-xl-5 th{padding-left:1.25rem;padding-right:1.25rem}.table.gx-xl-5 td.dtr-control,.table.gx-xl-5 th.dtr-control{padding-left:1.25rem!important}.table.gs-xl-5 td:first-child,.table.gs-xl-5 th:first-child{padding-left:1.25rem}.table.gs-xl-5 td:last-child,.table.gs-xl-5 th:last-child{padding-right:1.25rem}.table.gs-xl-5 td.dtr-control:first-child,.table.gs-xl-5 th.dtr-control:first-child{padding-left:1.25rem!important}.table.g-xl-6 td,.table.g-xl-6 th{padding:1.5rem}.table.g-xl-6 td.dtr-control,.table.g-xl-6 th.dtr-control{padding-left:1.5rem!important}.table.gy-xl-6 td,.table.gy-xl-6 th{padding-top:1.5rem;padding-bottom:1.5rem}.table.gx-xl-6 td,.table.gx-xl-6 th{padding-left:1.5rem;padding-right:1.5rem}.table.gx-xl-6 td.dtr-control,.table.gx-xl-6 th.dtr-control{padding-left:1.5rem!important}.table.gs-xl-6 td:first-child,.table.gs-xl-6 th:first-child{padding-left:1.5rem}.table.gs-xl-6 td:last-child,.table.gs-xl-6 th:last-child{padding-right:1.5rem}.table.gs-xl-6 td.dtr-control:first-child,.table.gs-xl-6 th.dtr-control:first-child{padding-left:1.5rem!important}.table.g-xl-7 td,.table.g-xl-7 th{padding:1.75rem}.table.g-xl-7 td.dtr-control,.table.g-xl-7 th.dtr-control{padding-left:1.75rem!important}.table.gy-xl-7 td,.table.gy-xl-7 th{padding-top:1.75rem;padding-bottom:1.75rem}.table.gx-xl-7 td,.table.gx-xl-7 th{padding-left:1.75rem;padding-right:1.75rem}.table.gx-xl-7 td.dtr-control,.table.gx-xl-7 th.dtr-control{padding-left:1.75rem!important}.table.gs-xl-7 td:first-child,.table.gs-xl-7 th:first-child{padding-left:1.75rem}.table.gs-xl-7 td:last-child,.table.gs-xl-7 th:last-child{padding-right:1.75rem}.table.gs-xl-7 td.dtr-control:first-child,.table.gs-xl-7 th.dtr-control:first-child{padding-left:1.75rem!important}.table.g-xl-8 td,.table.g-xl-8 th{padding:2rem}.table.g-xl-8 td.dtr-control,.table.g-xl-8 th.dtr-control{padding-left:2rem!important}.table.gy-xl-8 td,.table.gy-xl-8 th{padding-top:2rem;padding-bottom:2rem}.table.gx-xl-8 td,.table.gx-xl-8 th{padding-left:2rem;padding-right:2rem}.table.gx-xl-8 td.dtr-control,.table.gx-xl-8 th.dtr-control{padding-left:2rem!important}.table.gs-xl-8 td:first-child,.table.gs-xl-8 th:first-child{padding-left:2rem}.table.gs-xl-8 td:last-child,.table.gs-xl-8 th:last-child{padding-right:2rem}.table.gs-xl-8 td.dtr-control:first-child,.table.gs-xl-8 th.dtr-control:first-child{padding-left:2rem!important}.table.g-xl-9 td,.table.g-xl-9 th{padding:2.25rem}.table.g-xl-9 td.dtr-control,.table.g-xl-9 th.dtr-control{padding-left:2.25rem!important}.table.gy-xl-9 td,.table.gy-xl-9 th{padding-top:2.25rem;padding-bottom:2.25rem}.table.gx-xl-9 td,.table.gx-xl-9 th{padding-left:2.25rem;padding-right:2.25rem}.table.gx-xl-9 td.dtr-control,.table.gx-xl-9 th.dtr-control{padding-left:2.25rem!important}.table.gs-xl-9 td:first-child,.table.gs-xl-9 th:first-child{padding-left:2.25rem}.table.gs-xl-9 td:last-child,.table.gs-xl-9 th:last-child{padding-right:2.25rem}.table.gs-xl-9 td.dtr-control:first-child,.table.gs-xl-9 th.dtr-control:first-child{padding-left:2.25rem!important}.table.g-xl-10 td,.table.g-xl-10 th{padding:2.5rem}.table.g-xl-10 td.dtr-control,.table.g-xl-10 th.dtr-control{padding-left:2.5rem!important}.table.gy-xl-10 td,.table.gy-xl-10 th{padding-top:2.5rem;padding-bottom:2.5rem}.table.gx-xl-10 td,.table.gx-xl-10 th{padding-left:2.5rem;padding-right:2.5rem}.table.gx-xl-10 td.dtr-control,.table.gx-xl-10 th.dtr-control{padding-left:2.5rem!important}.table.gs-xl-10 td:first-child,.table.gs-xl-10 th:first-child{padding-left:2.5rem}.table.gs-xl-10 td:last-child,.table.gs-xl-10 th:last-child{padding-right:2.5rem}.table.gs-xl-10 td.dtr-control:first-child,.table.gs-xl-10 th.dtr-control:first-child{padding-left:2.5rem!important}}@media (min-width:1400px){.table.g-xxl-0 td,.table.g-xxl-0 th{padding:0}.table.g-xxl-0 td.dtr-control,.table.g-xxl-0 th.dtr-control{padding-left:0!important}.table.gy-xxl-0 td,.table.gy-xxl-0 th{padding-top:0;padding-bottom:0}.table.gx-xxl-0 td,.table.gx-xxl-0 th{padding-left:0;padding-right:0}.table.gx-xxl-0 td.dtr-control,.table.gx-xxl-0 th.dtr-control{padding-left:0!important}.table.gs-xxl-0 td:first-child,.table.gs-xxl-0 th:first-child{padding-left:0}.table.gs-xxl-0 td:last-child,.table.gs-xxl-0 th:last-child{padding-right:0}.table.gs-xxl-0 td.dtr-control:first-child,.table.gs-xxl-0 th.dtr-control:first-child{padding-left:0!important}.table.g-xxl-1 td,.table.g-xxl-1 th{padding:.25rem}.table.g-xxl-1 td.dtr-control,.table.g-xxl-1 th.dtr-control{padding-left:.25rem!important}.table.gy-xxl-1 td,.table.gy-xxl-1 th{padding-top:.25rem;padding-bottom:.25rem}.table.gx-xxl-1 td,.table.gx-xxl-1 th{padding-left:.25rem;padding-right:.25rem}.table.gx-xxl-1 td.dtr-control,.table.gx-xxl-1 th.dtr-control{padding-left:.25rem!important}.table.gs-xxl-1 td:first-child,.table.gs-xxl-1 th:first-child{padding-left:.25rem}.table.gs-xxl-1 td:last-child,.table.gs-xxl-1 th:last-child{padding-right:.25rem}.table.gs-xxl-1 td.dtr-control:first-child,.table.gs-xxl-1 th.dtr-control:first-child{padding-left:.25rem!important}.table.g-xxl-2 td,.table.g-xxl-2 th{padding:.5rem}.table.g-xxl-2 td.dtr-control,.table.g-xxl-2 th.dtr-control{padding-left:.5rem!important}.table.gy-xxl-2 td,.table.gy-xxl-2 th{padding-top:.5rem;padding-bottom:.5rem}.table.gx-xxl-2 td,.table.gx-xxl-2 th{padding-left:.5rem;padding-right:.5rem}.table.gx-xxl-2 td.dtr-control,.table.gx-xxl-2 th.dtr-control{padding-left:.5rem!important}.table.gs-xxl-2 td:first-child,.table.gs-xxl-2 th:first-child{padding-left:.5rem}.table.gs-xxl-2 td:last-child,.table.gs-xxl-2 th:last-child{padding-right:.5rem}.table.gs-xxl-2 td.dtr-control:first-child,.table.gs-xxl-2 th.dtr-control:first-child{padding-left:.5rem!important}.table.g-xxl-3 td,.table.g-xxl-3 th{padding:.75rem}.table.g-xxl-3 td.dtr-control,.table.g-xxl-3 th.dtr-control{padding-left:.75rem!important}.table.gy-xxl-3 td,.table.gy-xxl-3 th{padding-top:.75rem;padding-bottom:.75rem}.table.gx-xxl-3 td,.table.gx-xxl-3 th{padding-left:.75rem;padding-right:.75rem}.table.gx-xxl-3 td.dtr-control,.table.gx-xxl-3 th.dtr-control{padding-left:.75rem!important}.table.gs-xxl-3 td:first-child,.table.gs-xxl-3 th:first-child{padding-left:.75rem}.table.gs-xxl-3 td:last-child,.table.gs-xxl-3 th:last-child{padding-right:.75rem}.table.gs-xxl-3 td.dtr-control:first-child,.table.gs-xxl-3 th.dtr-control:first-child{padding-left:.75rem!important}.table.g-xxl-4 td,.table.g-xxl-4 th{padding:1rem}.table.g-xxl-4 td.dtr-control,.table.g-xxl-4 th.dtr-control{padding-left:1rem!important}.table.gy-xxl-4 td,.table.gy-xxl-4 th{padding-top:1rem;padding-bottom:1rem}.table.gx-xxl-4 td,.table.gx-xxl-4 th{padding-left:1rem;padding-right:1rem}.table.gx-xxl-4 td.dtr-control,.table.gx-xxl-4 th.dtr-control{padding-left:1rem!important}.table.gs-xxl-4 td:first-child,.table.gs-xxl-4 th:first-child{padding-left:1rem}.table.gs-xxl-4 td:last-child,.table.gs-xxl-4 th:last-child{padding-right:1rem}.table.gs-xxl-4 td.dtr-control:first-child,.table.gs-xxl-4 th.dtr-control:first-child{padding-left:1rem!important}.table.g-xxl-5 td,.table.g-xxl-5 th{padding:1.25rem}.table.g-xxl-5 td.dtr-control,.table.g-xxl-5 th.dtr-control{padding-left:1.25rem!important}.table.gy-xxl-5 td,.table.gy-xxl-5 th{padding-top:1.25rem;padding-bottom:1.25rem}.table.gx-xxl-5 td,.table.gx-xxl-5 th{padding-left:1.25rem;padding-right:1.25rem}.table.gx-xxl-5 td.dtr-control,.table.gx-xxl-5 th.dtr-control{padding-left:1.25rem!important}.table.gs-xxl-5 td:first-child,.table.gs-xxl-5 th:first-child{padding-left:1.25rem}.table.gs-xxl-5 td:last-child,.table.gs-xxl-5 th:last-child{padding-right:1.25rem}.table.gs-xxl-5 td.dtr-control:first-child,.table.gs-xxl-5 th.dtr-control:first-child{padding-left:1.25rem!important}.table.g-xxl-6 td,.table.g-xxl-6 th{padding:1.5rem}.table.g-xxl-6 td.dtr-control,.table.g-xxl-6 th.dtr-control{padding-left:1.5rem!important}.table.gy-xxl-6 td,.table.gy-xxl-6 th{padding-top:1.5rem;padding-bottom:1.5rem}.table.gx-xxl-6 td,.table.gx-xxl-6 th{padding-left:1.5rem;padding-right:1.5rem}.table.gx-xxl-6 td.dtr-control,.table.gx-xxl-6 th.dtr-control{padding-left:1.5rem!important}.table.gs-xxl-6 td:first-child,.table.gs-xxl-6 th:first-child{padding-left:1.5rem}.table.gs-xxl-6 td:last-child,.table.gs-xxl-6 th:last-child{padding-right:1.5rem}.table.gs-xxl-6 td.dtr-control:first-child,.table.gs-xxl-6 th.dtr-control:first-child{padding-left:1.5rem!important}.table.g-xxl-7 td,.table.g-xxl-7 th{padding:1.75rem}.table.g-xxl-7 td.dtr-control,.table.g-xxl-7 th.dtr-control{padding-left:1.75rem!important}.table.gy-xxl-7 td,.table.gy-xxl-7 th{padding-top:1.75rem;padding-bottom:1.75rem}.table.gx-xxl-7 td,.table.gx-xxl-7 th{padding-left:1.75rem;padding-right:1.75rem}.table.gx-xxl-7 td.dtr-control,.table.gx-xxl-7 th.dtr-control{padding-left:1.75rem!important}.table.gs-xxl-7 td:first-child,.table.gs-xxl-7 th:first-child{padding-left:1.75rem}.table.gs-xxl-7 td:last-child,.table.gs-xxl-7 th:last-child{padding-right:1.75rem}.table.gs-xxl-7 td.dtr-control:first-child,.table.gs-xxl-7 th.dtr-control:first-child{padding-left:1.75rem!important}.table.g-xxl-8 td,.table.g-xxl-8 th{padding:2rem}.table.g-xxl-8 td.dtr-control,.table.g-xxl-8 th.dtr-control{padding-left:2rem!important}.table.gy-xxl-8 td,.table.gy-xxl-8 th{padding-top:2rem;padding-bottom:2rem}.table.gx-xxl-8 td,.table.gx-xxl-8 th{padding-left:2rem;padding-right:2rem}.table.gx-xxl-8 td.dtr-control,.table.gx-xxl-8 th.dtr-control{padding-left:2rem!important}.table.gs-xxl-8 td:first-child,.table.gs-xxl-8 th:first-child{padding-left:2rem}.table.gs-xxl-8 td:last-child,.table.gs-xxl-8 th:last-child{padding-right:2rem}.table.gs-xxl-8 td.dtr-control:first-child,.table.gs-xxl-8 th.dtr-control:first-child{padding-left:2rem!important}.table.g-xxl-9 td,.table.g-xxl-9 th{padding:2.25rem}.table.g-xxl-9 td.dtr-control,.table.g-xxl-9 th.dtr-control{padding-left:2.25rem!important}.table.gy-xxl-9 td,.table.gy-xxl-9 th{padding-top:2.25rem;padding-bottom:2.25rem}.table.gx-xxl-9 td,.table.gx-xxl-9 th{padding-left:2.25rem;padding-right:2.25rem}.table.gx-xxl-9 td.dtr-control,.table.gx-xxl-9 th.dtr-control{padding-left:2.25rem!important}.table.gs-xxl-9 td:first-child,.table.gs-xxl-9 th:first-child{padding-left:2.25rem}.table.gs-xxl-9 td:last-child,.table.gs-xxl-9 th:last-child{padding-right:2.25rem}.table.gs-xxl-9 td.dtr-control:first-child,.table.gs-xxl-9 th.dtr-control:first-child{padding-left:2.25rem!important}.table.g-xxl-10 td,.table.g-xxl-10 th{padding:2.5rem}.table.g-xxl-10 td.dtr-control,.table.g-xxl-10 th.dtr-control{padding-left:2.5rem!important}.table.gy-xxl-10 td,.table.gy-xxl-10 th{padding-top:2.5rem;padding-bottom:2.5rem}.table.gx-xxl-10 td,.table.gx-xxl-10 th{padding-left:2.5rem;padding-right:2.5rem}.table.gx-xxl-10 td.dtr-control,.table.gx-xxl-10 th.dtr-control{padding-left:2.5rem!important}.table.gs-xxl-10 td:first-child,.table.gs-xxl-10 th:first-child{padding-left:2.5rem}.table.gs-xxl-10 td:last-child,.table.gs-xxl-10 th:last-child{padding-right:2.5rem}.table.gs-xxl-10 td.dtr-control:first-child,.table.gs-xxl-10 th.dtr-control:first-child{padding-left:2.5rem!important}}.popover{--bs-popover-header-border-color:#F1F1F4}.popover .popover-header{font-size:1rem;font-weight:500;border-bottom:1px solid var(--bs-popover-header-border-color)}.popover .popover-dismiss{position:absolute;top:.85rem;right:.85rem;height:1.25rem;width:1.25rem;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-500);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-500%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-500%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-size:50%;-webkit-mask-size:50%}.popover .popover-dismiss:hover{background-color:var(--bs-primary)}.popover .popover-dismiss+.popover-header{padding-right:2.5rem}.popover-inverse{--bs-popover-bg:var(--bs-gray-900);--bs-popover-border-color:var(--bs-gray-900);--bs-popover-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.3);--bs-popover-header-color:var(--bs-gray-200);--bs-popover-header-bg:var(--bs-gray-900);--bs-popover-body-color:var(--bs-gray-400);--bs-popover-arrow-border:transparent;--bs-popover-header-border-color:var(--bs-gray-800)}[data-bs-theme=dark] .popover:not(.popover-inverse){--bs-popover-bg:#26272F;--bs-popover-border-color:#26272F;--bs-popover-header-bg:#26272F;--bs-popover-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.3);--bs-popover-header-border-color:#363843}.tooltip .tooltip-inner{box-shadow:var(--bs-tooltip-box-shadow)}.tooltip.tooltop-auto-width .tooltip-inner{white-space:nowrap;max-width:none}.tooltip.tooltip-inverse{--bs-tooltip-color:var(--bs-dark-inverse);--bs-tooltip-bg:var(--bs-dark)}[data-bs-theme=dark] .tooltip:not(.tooltip-inverse){--bs-tooltip-bg:#26272F;--bs-tooltip-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.15)}.accordion .accordion-header{cursor:pointer}.accordion.accordion-icon-toggle .accordion-icon{display:flex;flex-shrink:0;transition:all .2s ease-in-out;transform:rotate(90deg);align-items:center;justify-content:center}.accordion.accordion-icon-toggle .accordion-icon .svg-icon,.accordion.accordion-icon-toggle .accordion-icon i{color:var(--bs-primary)}.accordion.accordion-icon-toggle .collapsed .accordion-icon{transition:all .2s ease-in-out;transform:rotate(0)}.accordion.accordion-icon-toggle .collapsed .accordion-icon .svg-icon,.accordion.accordion-icon-toggle .collapsed .accordion-icon i{color:var(--bs-text-muted)}.accordion.accordion-icon-collapse .accordion-icon{display:flex;flex-shrink:0;align-items:center;justify-content:center;transition:all .2s ease-in-out}.accordion.accordion-icon-collapse .accordion-icon .accordion-icon-on{display:inline-flex}.accordion.accordion-icon-collapse .accordion-icon .accordion-icon-off{display:none}.accordion.accordion-icon-collapse .accordion-icon .svg-icon,.accordion.accordion-icon-collapse .accordion-icon i{color:var(--bs-primary)}.accordion.accordion-icon-collapse .collapsed .accordion-icon{transition:all .2s ease-in-out}.accordion.accordion-icon-collapse .collapsed .accordion-icon .accordion-icon-on{display:none}.accordion.accordion-icon-collapse .collapsed .accordion-icon .accordion-icon-off{display:inline-flex}.accordion.accordion-icon-collapse .collapsed .accordion-icon .svg-icon,.accordion.accordion-icon-collapse .collapsed .accordion-icon i{color:var(--bs-text-muted)}.accordion.accordion-borderless .accordion-item{border:0}.accordion.accordion-flush .accordion-item{background-color:transparent;border:0;border-radius:0;padding-left:0;padding-right:0}.image-input{position:relative;display:inline-block;border-radius:.475rem;background-repeat:no-repeat;background-size:cover}.image-input:not(.image-input-empty){background-image:none!important}.image-input .image-input-wrapper{width:120px;height:120px;border-radius:.475rem;background-repeat:no-repeat;background-size:cover}.image-input [data-kt-image-input-action]{cursor:pointer;position:absolute;transform:translate(-50%,-50%)}.image-input [data-kt-image-input-action=change]{left:100%;top:0}.image-input [data-kt-image-input-action=change] input{width:0!important;height:0!important;overflow:hidden;opacity:0}.image-input [data-kt-image-input-action=cancel],.image-input [data-kt-image-input-action=remove]{position:absolute;left:100%;top:100%}.image-input [data-kt-image-input-action=cancel]{display:none}.image-input.image-input-changed [data-kt-image-input-action=cancel]{display:flex}.image-input.image-input-changed [data-kt-image-input-action=remove]{display:none}.image-input.image-input-empty [data-kt-image-input-action=cancel],.image-input.image-input-empty [data-kt-image-input-action=remove]{display:none}.image-input.image-input-circle{border-radius:50%}.image-input.image-input-circle .image-input-wrapper{border-radius:50%}.image-input.image-input-circle [data-kt-image-input-action=change]{left:100%;top:0;transform:translate(-100%,0)}.image-input.image-input-circle [data-kt-image-input-action=cancel],.image-input.image-input-circle [data-kt-image-input-action=remove]{left:100%;top:100%;transform:translate(-100%,-100%)}.image-input.image-input-outline .image-input-wrapper{border:3px solid var(--bs-body-bg);box-shadow:var(--bs-box-shadow)}.symbol{display:inline-block;flex-shrink:0;position:relative;border-radius:.475rem}.symbol .symbol-label{display:flex;align-items:center;justify-content:center;font-weight:500;color:var(--bs-symbol-label-color);background-color:var(--bs-symbol-label-bg);background-repeat:no-repeat;background-position:center center;background-size:cover;border-radius:.475rem}.symbol .symbol-label:after{border-radius:.475rem}.symbol .symbol-badge{position:absolute;border-radius:100%;top:0;left:50%;transform:translateX(-50%) translateY(-50%)!important}.symbol>img{width:100%;flex-shrink:0;display:inline-block;border-radius:.475rem}.symbol.symbol-square,.symbol.symbol-square .symbol-label,.symbol.symbol-square>img{border-radius:0!important}.symbol.symbol-circle,.symbol.symbol-circle .symbol-label,.symbol.symbol-circle>img{border-radius:50%}.symbol.symbol-circle .symbol-label:after,.symbol.symbol-circle:after,.symbol.symbol-circle>img:after{border-radius:50%}.symbol>img{width:50px;height:50px}.symbol .symbol-label{width:50px;height:50px}.symbol.symbol-fixed .symbol-label{width:50px;height:50px}.symbol.symbol-fixed>img{width:50px;height:50px;max-width:none}.symbol.symbol-2by3 .symbol-label{height:50px;width:75px}.symbol.symbol-2by3>img{height:50px;width:75px;max-width:none}.symbol.symbol-20px>img{width:20px;height:20px}.symbol.symbol-20px .symbol-label{width:20px;height:20px}.symbol.symbol-20px.symbol-fixed .symbol-label{width:20px;height:20px}.symbol.symbol-20px.symbol-fixed>img{width:20px;height:20px;max-width:none}.symbol.symbol-20px.symbol-2by3 .symbol-label{height:20px;width:30px}.symbol.symbol-20px.symbol-2by3>img{height:20px;width:30px;max-width:none}.symbol.symbol-25px>img{width:25px;height:25px}.symbol.symbol-25px .symbol-label{width:25px;height:25px}.symbol.symbol-25px.symbol-fixed .symbol-label{width:25px;height:25px}.symbol.symbol-25px.symbol-fixed>img{width:25px;height:25px;max-width:none}.symbol.symbol-25px.symbol-2by3 .symbol-label{height:25px;width:37.5px}.symbol.symbol-25px.symbol-2by3>img{height:25px;width:37.5px;max-width:none}.symbol.symbol-30px>img{width:30px;height:30px}.symbol.symbol-30px .symbol-label{width:30px;height:30px}.symbol.symbol-30px.symbol-fixed .symbol-label{width:30px;height:30px}.symbol.symbol-30px.symbol-fixed>img{width:30px;height:30px;max-width:none}.symbol.symbol-30px.symbol-2by3 .symbol-label{height:30px;width:45px}.symbol.symbol-30px.symbol-2by3>img{height:30px;width:45px;max-width:none}.symbol.symbol-35px>img{width:35px;height:35px}.symbol.symbol-35px .symbol-label{width:35px;height:35px}.symbol.symbol-35px.symbol-fixed .symbol-label{width:35px;height:35px}.symbol.symbol-35px.symbol-fixed>img{width:35px;height:35px;max-width:none}.symbol.symbol-35px.symbol-2by3 .symbol-label{height:35px;width:52.5px}.symbol.symbol-35px.symbol-2by3>img{height:35px;width:52.5px;max-width:none}.symbol.symbol-40px>img{width:40px;height:40px}.symbol.symbol-40px .symbol-label{width:40px;height:40px}.symbol.symbol-40px.symbol-fixed .symbol-label{width:40px;height:40px}.symbol.symbol-40px.symbol-fixed>img{width:40px;height:40px;max-width:none}.symbol.symbol-40px.symbol-2by3 .symbol-label{height:40px;width:60px}.symbol.symbol-40px.symbol-2by3>img{height:40px;width:60px;max-width:none}.symbol.symbol-45px>img{width:45px;height:45px}.symbol.symbol-45px .symbol-label{width:45px;height:45px}.symbol.symbol-45px.symbol-fixed .symbol-label{width:45px;height:45px}.symbol.symbol-45px.symbol-fixed>img{width:45px;height:45px;max-width:none}.symbol.symbol-45px.symbol-2by3 .symbol-label{height:45px;width:67.5px}.symbol.symbol-45px.symbol-2by3>img{height:45px;width:67.5px;max-width:none}.symbol.symbol-50px>img{width:50px;height:50px}.symbol.symbol-50px .symbol-label{width:50px;height:50px}.symbol.symbol-50px.symbol-fixed .symbol-label{width:50px;height:50px}.symbol.symbol-50px.symbol-fixed>img{width:50px;height:50px;max-width:none}.symbol.symbol-50px.symbol-2by3 .symbol-label{height:50px;width:75px}.symbol.symbol-50px.symbol-2by3>img{height:50px;width:75px;max-width:none}.symbol.symbol-55px>img{width:55px;height:55px}.symbol.symbol-55px .symbol-label{width:55px;height:55px}.symbol.symbol-55px.symbol-fixed .symbol-label{width:55px;height:55px}.symbol.symbol-55px.symbol-fixed>img{width:55px;height:55px;max-width:none}.symbol.symbol-55px.symbol-2by3 .symbol-label{height:55px;width:82.5px}.symbol.symbol-55px.symbol-2by3>img{height:55px;width:82.5px;max-width:none}.symbol.symbol-60px>img{width:60px;height:60px}.symbol.symbol-60px .symbol-label{width:60px;height:60px}.symbol.symbol-60px.symbol-fixed .symbol-label{width:60px;height:60px}.symbol.symbol-60px.symbol-fixed>img{width:60px;height:60px;max-width:none}.symbol.symbol-60px.symbol-2by3 .symbol-label{height:60px;width:90px}.symbol.symbol-60px.symbol-2by3>img{height:60px;width:90px;max-width:none}.symbol.symbol-65px>img{width:65px;height:65px}.symbol.symbol-65px .symbol-label{width:65px;height:65px}.symbol.symbol-65px.symbol-fixed .symbol-label{width:65px;height:65px}.symbol.symbol-65px.symbol-fixed>img{width:65px;height:65px;max-width:none}.symbol.symbol-65px.symbol-2by3 .symbol-label{height:65px;width:97.5px}.symbol.symbol-65px.symbol-2by3>img{height:65px;width:97.5px;max-width:none}.symbol.symbol-70px>img{width:70px;height:70px}.symbol.symbol-70px .symbol-label{width:70px;height:70px}.symbol.symbol-70px.symbol-fixed .symbol-label{width:70px;height:70px}.symbol.symbol-70px.symbol-fixed>img{width:70px;height:70px;max-width:none}.symbol.symbol-70px.symbol-2by3 .symbol-label{height:70px;width:105px}.symbol.symbol-70px.symbol-2by3>img{height:70px;width:105px;max-width:none}.symbol.symbol-75px>img{width:75px;height:75px}.symbol.symbol-75px .symbol-label{width:75px;height:75px}.symbol.symbol-75px.symbol-fixed .symbol-label{width:75px;height:75px}.symbol.symbol-75px.symbol-fixed>img{width:75px;height:75px;max-width:none}.symbol.symbol-75px.symbol-2by3 .symbol-label{height:75px;width:112.5px}.symbol.symbol-75px.symbol-2by3>img{height:75px;width:112.5px;max-width:none}.symbol.symbol-90px>img{width:90px;height:90px}.symbol.symbol-90px .symbol-label{width:90px;height:90px}.symbol.symbol-90px.symbol-fixed .symbol-label{width:90px;height:90px}.symbol.symbol-90px.symbol-fixed>img{width:90px;height:90px;max-width:none}.symbol.symbol-90px.symbol-2by3 .symbol-label{height:90px;width:135px}.symbol.symbol-90px.symbol-2by3>img{height:90px;width:135px;max-width:none}.symbol.symbol-100px>img{width:100px;height:100px}.symbol.symbol-100px .symbol-label{width:100px;height:100px}.symbol.symbol-100px.symbol-fixed .symbol-label{width:100px;height:100px}.symbol.symbol-100px.symbol-fixed>img{width:100px;height:100px;max-width:none}.symbol.symbol-100px.symbol-2by3 .symbol-label{height:100px;width:150px}.symbol.symbol-100px.symbol-2by3>img{height:100px;width:150px;max-width:none}.symbol.symbol-125px>img{width:125px;height:125px}.symbol.symbol-125px .symbol-label{width:125px;height:125px}.symbol.symbol-125px.symbol-fixed .symbol-label{width:125px;height:125px}.symbol.symbol-125px.symbol-fixed>img{width:125px;height:125px;max-width:none}.symbol.symbol-125px.symbol-2by3 .symbol-label{height:125px;width:187.5px}.symbol.symbol-125px.symbol-2by3>img{height:125px;width:187.5px;max-width:none}.symbol.symbol-150px>img{width:150px;height:150px}.symbol.symbol-150px .symbol-label{width:150px;height:150px}.symbol.symbol-150px.symbol-fixed .symbol-label{width:150px;height:150px}.symbol.symbol-150px.symbol-fixed>img{width:150px;height:150px;max-width:none}.symbol.symbol-150px.symbol-2by3 .symbol-label{height:150px;width:225px}.symbol.symbol-150px.symbol-2by3>img{height:150px;width:225px;max-width:none}.symbol.symbol-160px>img{width:160px;height:160px}.symbol.symbol-160px .symbol-label{width:160px;height:160px}.symbol.symbol-160px.symbol-fixed .symbol-label{width:160px;height:160px}.symbol.symbol-160px.symbol-fixed>img{width:160px;height:160px;max-width:none}.symbol.symbol-160px.symbol-2by3 .symbol-label{height:160px;width:240px}.symbol.symbol-160px.symbol-2by3>img{height:160px;width:240px;max-width:none}.symbol.symbol-175px>img{width:175px;height:175px}.symbol.symbol-175px .symbol-label{width:175px;height:175px}.symbol.symbol-175px.symbol-fixed .symbol-label{width:175px;height:175px}.symbol.symbol-175px.symbol-fixed>img{width:175px;height:175px;max-width:none}.symbol.symbol-175px.symbol-2by3 .symbol-label{height:175px;width:262.5px}.symbol.symbol-175px.symbol-2by3>img{height:175px;width:262.5px;max-width:none}.symbol.symbol-200px>img{width:200px;height:200px}.symbol.symbol-200px .symbol-label{width:200px;height:200px}.symbol.symbol-200px.symbol-fixed .symbol-label{width:200px;height:200px}.symbol.symbol-200px.symbol-fixed>img{width:200px;height:200px;max-width:none}.symbol.symbol-200px.symbol-2by3 .symbol-label{height:200px;width:300px}.symbol.symbol-200px.symbol-2by3>img{height:200px;width:300px;max-width:none}@media (min-width:576px){.symbol.symbol-sm-20px>img{width:20px;height:20px}.symbol.symbol-sm-20px .symbol-label{width:20px;height:20px}.symbol.symbol-sm-20px.symbol-fixed .symbol-label{width:20px;height:20px}.symbol.symbol-sm-20px.symbol-fixed>img{width:20px;height:20px;max-width:none}.symbol.symbol-sm-20px.symbol-2by3 .symbol-label{height:20px;width:30px}.symbol.symbol-sm-20px.symbol-2by3>img{height:20px;width:30px;max-width:none}.symbol.symbol-sm-25px>img{width:25px;height:25px}.symbol.symbol-sm-25px .symbol-label{width:25px;height:25px}.symbol.symbol-sm-25px.symbol-fixed .symbol-label{width:25px;height:25px}.symbol.symbol-sm-25px.symbol-fixed>img{width:25px;height:25px;max-width:none}.symbol.symbol-sm-25px.symbol-2by3 .symbol-label{height:25px;width:37.5px}.symbol.symbol-sm-25px.symbol-2by3>img{height:25px;width:37.5px;max-width:none}.symbol.symbol-sm-30px>img{width:30px;height:30px}.symbol.symbol-sm-30px .symbol-label{width:30px;height:30px}.symbol.symbol-sm-30px.symbol-fixed .symbol-label{width:30px;height:30px}.symbol.symbol-sm-30px.symbol-fixed>img{width:30px;height:30px;max-width:none}.symbol.symbol-sm-30px.symbol-2by3 .symbol-label{height:30px;width:45px}.symbol.symbol-sm-30px.symbol-2by3>img{height:30px;width:45px;max-width:none}.symbol.symbol-sm-35px>img{width:35px;height:35px}.symbol.symbol-sm-35px .symbol-label{width:35px;height:35px}.symbol.symbol-sm-35px.symbol-fixed .symbol-label{width:35px;height:35px}.symbol.symbol-sm-35px.symbol-fixed>img{width:35px;height:35px;max-width:none}.symbol.symbol-sm-35px.symbol-2by3 .symbol-label{height:35px;width:52.5px}.symbol.symbol-sm-35px.symbol-2by3>img{height:35px;width:52.5px;max-width:none}.symbol.symbol-sm-40px>img{width:40px;height:40px}.symbol.symbol-sm-40px .symbol-label{width:40px;height:40px}.symbol.symbol-sm-40px.symbol-fixed .symbol-label{width:40px;height:40px}.symbol.symbol-sm-40px.symbol-fixed>img{width:40px;height:40px;max-width:none}.symbol.symbol-sm-40px.symbol-2by3 .symbol-label{height:40px;width:60px}.symbol.symbol-sm-40px.symbol-2by3>img{height:40px;width:60px;max-width:none}.symbol.symbol-sm-45px>img{width:45px;height:45px}.symbol.symbol-sm-45px .symbol-label{width:45px;height:45px}.symbol.symbol-sm-45px.symbol-fixed .symbol-label{width:45px;height:45px}.symbol.symbol-sm-45px.symbol-fixed>img{width:45px;height:45px;max-width:none}.symbol.symbol-sm-45px.symbol-2by3 .symbol-label{height:45px;width:67.5px}.symbol.symbol-sm-45px.symbol-2by3>img{height:45px;width:67.5px;max-width:none}.symbol.symbol-sm-50px>img{width:50px;height:50px}.symbol.symbol-sm-50px .symbol-label{width:50px;height:50px}.symbol.symbol-sm-50px.symbol-fixed .symbol-label{width:50px;height:50px}.symbol.symbol-sm-50px.symbol-fixed>img{width:50px;height:50px;max-width:none}.symbol.symbol-sm-50px.symbol-2by3 .symbol-label{height:50px;width:75px}.symbol.symbol-sm-50px.symbol-2by3>img{height:50px;width:75px;max-width:none}.symbol.symbol-sm-55px>img{width:55px;height:55px}.symbol.symbol-sm-55px .symbol-label{width:55px;height:55px}.symbol.symbol-sm-55px.symbol-fixed .symbol-label{width:55px;height:55px}.symbol.symbol-sm-55px.symbol-fixed>img{width:55px;height:55px;max-width:none}.symbol.symbol-sm-55px.symbol-2by3 .symbol-label{height:55px;width:82.5px}.symbol.symbol-sm-55px.symbol-2by3>img{height:55px;width:82.5px;max-width:none}.symbol.symbol-sm-60px>img{width:60px;height:60px}.symbol.symbol-sm-60px .symbol-label{width:60px;height:60px}.symbol.symbol-sm-60px.symbol-fixed .symbol-label{width:60px;height:60px}.symbol.symbol-sm-60px.symbol-fixed>img{width:60px;height:60px;max-width:none}.symbol.symbol-sm-60px.symbol-2by3 .symbol-label{height:60px;width:90px}.symbol.symbol-sm-60px.symbol-2by3>img{height:60px;width:90px;max-width:none}.symbol.symbol-sm-65px>img{width:65px;height:65px}.symbol.symbol-sm-65px .symbol-label{width:65px;height:65px}.symbol.symbol-sm-65px.symbol-fixed .symbol-label{width:65px;height:65px}.symbol.symbol-sm-65px.symbol-fixed>img{width:65px;height:65px;max-width:none}.symbol.symbol-sm-65px.symbol-2by3 .symbol-label{height:65px;width:97.5px}.symbol.symbol-sm-65px.symbol-2by3>img{height:65px;width:97.5px;max-width:none}.symbol.symbol-sm-70px>img{width:70px;height:70px}.symbol.symbol-sm-70px .symbol-label{width:70px;height:70px}.symbol.symbol-sm-70px.symbol-fixed .symbol-label{width:70px;height:70px}.symbol.symbol-sm-70px.symbol-fixed>img{width:70px;height:70px;max-width:none}.symbol.symbol-sm-70px.symbol-2by3 .symbol-label{height:70px;width:105px}.symbol.symbol-sm-70px.symbol-2by3>img{height:70px;width:105px;max-width:none}.symbol.symbol-sm-75px>img{width:75px;height:75px}.symbol.symbol-sm-75px .symbol-label{width:75px;height:75px}.symbol.symbol-sm-75px.symbol-fixed .symbol-label{width:75px;height:75px}.symbol.symbol-sm-75px.symbol-fixed>img{width:75px;height:75px;max-width:none}.symbol.symbol-sm-75px.symbol-2by3 .symbol-label{height:75px;width:112.5px}.symbol.symbol-sm-75px.symbol-2by3>img{height:75px;width:112.5px;max-width:none}.symbol.symbol-sm-90px>img{width:90px;height:90px}.symbol.symbol-sm-90px .symbol-label{width:90px;height:90px}.symbol.symbol-sm-90px.symbol-fixed .symbol-label{width:90px;height:90px}.symbol.symbol-sm-90px.symbol-fixed>img{width:90px;height:90px;max-width:none}.symbol.symbol-sm-90px.symbol-2by3 .symbol-label{height:90px;width:135px}.symbol.symbol-sm-90px.symbol-2by3>img{height:90px;width:135px;max-width:none}.symbol.symbol-sm-100px>img{width:100px;height:100px}.symbol.symbol-sm-100px .symbol-label{width:100px;height:100px}.symbol.symbol-sm-100px.symbol-fixed .symbol-label{width:100px;height:100px}.symbol.symbol-sm-100px.symbol-fixed>img{width:100px;height:100px;max-width:none}.symbol.symbol-sm-100px.symbol-2by3 .symbol-label{height:100px;width:150px}.symbol.symbol-sm-100px.symbol-2by3>img{height:100px;width:150px;max-width:none}.symbol.symbol-sm-125px>img{width:125px;height:125px}.symbol.symbol-sm-125px .symbol-label{width:125px;height:125px}.symbol.symbol-sm-125px.symbol-fixed .symbol-label{width:125px;height:125px}.symbol.symbol-sm-125px.symbol-fixed>img{width:125px;height:125px;max-width:none}.symbol.symbol-sm-125px.symbol-2by3 .symbol-label{height:125px;width:187.5px}.symbol.symbol-sm-125px.symbol-2by3>img{height:125px;width:187.5px;max-width:none}.symbol.symbol-sm-150px>img{width:150px;height:150px}.symbol.symbol-sm-150px .symbol-label{width:150px;height:150px}.symbol.symbol-sm-150px.symbol-fixed .symbol-label{width:150px;height:150px}.symbol.symbol-sm-150px.symbol-fixed>img{width:150px;height:150px;max-width:none}.symbol.symbol-sm-150px.symbol-2by3 .symbol-label{height:150px;width:225px}.symbol.symbol-sm-150px.symbol-2by3>img{height:150px;width:225px;max-width:none}.symbol.symbol-sm-160px>img{width:160px;height:160px}.symbol.symbol-sm-160px .symbol-label{width:160px;height:160px}.symbol.symbol-sm-160px.symbol-fixed .symbol-label{width:160px;height:160px}.symbol.symbol-sm-160px.symbol-fixed>img{width:160px;height:160px;max-width:none}.symbol.symbol-sm-160px.symbol-2by3 .symbol-label{height:160px;width:240px}.symbol.symbol-sm-160px.symbol-2by3>img{height:160px;width:240px;max-width:none}.symbol.symbol-sm-175px>img{width:175px;height:175px}.symbol.symbol-sm-175px .symbol-label{width:175px;height:175px}.symbol.symbol-sm-175px.symbol-fixed .symbol-label{width:175px;height:175px}.symbol.symbol-sm-175px.symbol-fixed>img{width:175px;height:175px;max-width:none}.symbol.symbol-sm-175px.symbol-2by3 .symbol-label{height:175px;width:262.5px}.symbol.symbol-sm-175px.symbol-2by3>img{height:175px;width:262.5px;max-width:none}.symbol.symbol-sm-200px>img{width:200px;height:200px}.symbol.symbol-sm-200px .symbol-label{width:200px;height:200px}.symbol.symbol-sm-200px.symbol-fixed .symbol-label{width:200px;height:200px}.symbol.symbol-sm-200px.symbol-fixed>img{width:200px;height:200px;max-width:none}.symbol.symbol-sm-200px.symbol-2by3 .symbol-label{height:200px;width:300px}.symbol.symbol-sm-200px.symbol-2by3>img{height:200px;width:300px;max-width:none}}@media (min-width:768px){.symbol.symbol-md-20px>img{width:20px;height:20px}.symbol.symbol-md-20px .symbol-label{width:20px;height:20px}.symbol.symbol-md-20px.symbol-fixed .symbol-label{width:20px;height:20px}.symbol.symbol-md-20px.symbol-fixed>img{width:20px;height:20px;max-width:none}.symbol.symbol-md-20px.symbol-2by3 .symbol-label{height:20px;width:30px}.symbol.symbol-md-20px.symbol-2by3>img{height:20px;width:30px;max-width:none}.symbol.symbol-md-25px>img{width:25px;height:25px}.symbol.symbol-md-25px .symbol-label{width:25px;height:25px}.symbol.symbol-md-25px.symbol-fixed .symbol-label{width:25px;height:25px}.symbol.symbol-md-25px.symbol-fixed>img{width:25px;height:25px;max-width:none}.symbol.symbol-md-25px.symbol-2by3 .symbol-label{height:25px;width:37.5px}.symbol.symbol-md-25px.symbol-2by3>img{height:25px;width:37.5px;max-width:none}.symbol.symbol-md-30px>img{width:30px;height:30px}.symbol.symbol-md-30px .symbol-label{width:30px;height:30px}.symbol.symbol-md-30px.symbol-fixed .symbol-label{width:30px;height:30px}.symbol.symbol-md-30px.symbol-fixed>img{width:30px;height:30px;max-width:none}.symbol.symbol-md-30px.symbol-2by3 .symbol-label{height:30px;width:45px}.symbol.symbol-md-30px.symbol-2by3>img{height:30px;width:45px;max-width:none}.symbol.symbol-md-35px>img{width:35px;height:35px}.symbol.symbol-md-35px .symbol-label{width:35px;height:35px}.symbol.symbol-md-35px.symbol-fixed .symbol-label{width:35px;height:35px}.symbol.symbol-md-35px.symbol-fixed>img{width:35px;height:35px;max-width:none}.symbol.symbol-md-35px.symbol-2by3 .symbol-label{height:35px;width:52.5px}.symbol.symbol-md-35px.symbol-2by3>img{height:35px;width:52.5px;max-width:none}.symbol.symbol-md-40px>img{width:40px;height:40px}.symbol.symbol-md-40px .symbol-label{width:40px;height:40px}.symbol.symbol-md-40px.symbol-fixed .symbol-label{width:40px;height:40px}.symbol.symbol-md-40px.symbol-fixed>img{width:40px;height:40px;max-width:none}.symbol.symbol-md-40px.symbol-2by3 .symbol-label{height:40px;width:60px}.symbol.symbol-md-40px.symbol-2by3>img{height:40px;width:60px;max-width:none}.symbol.symbol-md-45px>img{width:45px;height:45px}.symbol.symbol-md-45px .symbol-label{width:45px;height:45px}.symbol.symbol-md-45px.symbol-fixed .symbol-label{width:45px;height:45px}.symbol.symbol-md-45px.symbol-fixed>img{width:45px;height:45px;max-width:none}.symbol.symbol-md-45px.symbol-2by3 .symbol-label{height:45px;width:67.5px}.symbol.symbol-md-45px.symbol-2by3>img{height:45px;width:67.5px;max-width:none}.symbol.symbol-md-50px>img{width:50px;height:50px}.symbol.symbol-md-50px .symbol-label{width:50px;height:50px}.symbol.symbol-md-50px.symbol-fixed .symbol-label{width:50px;height:50px}.symbol.symbol-md-50px.symbol-fixed>img{width:50px;height:50px;max-width:none}.symbol.symbol-md-50px.symbol-2by3 .symbol-label{height:50px;width:75px}.symbol.symbol-md-50px.symbol-2by3>img{height:50px;width:75px;max-width:none}.symbol.symbol-md-55px>img{width:55px;height:55px}.symbol.symbol-md-55px .symbol-label{width:55px;height:55px}.symbol.symbol-md-55px.symbol-fixed .symbol-label{width:55px;height:55px}.symbol.symbol-md-55px.symbol-fixed>img{width:55px;height:55px;max-width:none}.symbol.symbol-md-55px.symbol-2by3 .symbol-label{height:55px;width:82.5px}.symbol.symbol-md-55px.symbol-2by3>img{height:55px;width:82.5px;max-width:none}.symbol.symbol-md-60px>img{width:60px;height:60px}.symbol.symbol-md-60px .symbol-label{width:60px;height:60px}.symbol.symbol-md-60px.symbol-fixed .symbol-label{width:60px;height:60px}.symbol.symbol-md-60px.symbol-fixed>img{width:60px;height:60px;max-width:none}.symbol.symbol-md-60px.symbol-2by3 .symbol-label{height:60px;width:90px}.symbol.symbol-md-60px.symbol-2by3>img{height:60px;width:90px;max-width:none}.symbol.symbol-md-65px>img{width:65px;height:65px}.symbol.symbol-md-65px .symbol-label{width:65px;height:65px}.symbol.symbol-md-65px.symbol-fixed .symbol-label{width:65px;height:65px}.symbol.symbol-md-65px.symbol-fixed>img{width:65px;height:65px;max-width:none}.symbol.symbol-md-65px.symbol-2by3 .symbol-label{height:65px;width:97.5px}.symbol.symbol-md-65px.symbol-2by3>img{height:65px;width:97.5px;max-width:none}.symbol.symbol-md-70px>img{width:70px;height:70px}.symbol.symbol-md-70px .symbol-label{width:70px;height:70px}.symbol.symbol-md-70px.symbol-fixed .symbol-label{width:70px;height:70px}.symbol.symbol-md-70px.symbol-fixed>img{width:70px;height:70px;max-width:none}.symbol.symbol-md-70px.symbol-2by3 .symbol-label{height:70px;width:105px}.symbol.symbol-md-70px.symbol-2by3>img{height:70px;width:105px;max-width:none}.symbol.symbol-md-75px>img{width:75px;height:75px}.symbol.symbol-md-75px .symbol-label{width:75px;height:75px}.symbol.symbol-md-75px.symbol-fixed .symbol-label{width:75px;height:75px}.symbol.symbol-md-75px.symbol-fixed>img{width:75px;height:75px;max-width:none}.symbol.symbol-md-75px.symbol-2by3 .symbol-label{height:75px;width:112.5px}.symbol.symbol-md-75px.symbol-2by3>img{height:75px;width:112.5px;max-width:none}.symbol.symbol-md-90px>img{width:90px;height:90px}.symbol.symbol-md-90px .symbol-label{width:90px;height:90px}.symbol.symbol-md-90px.symbol-fixed .symbol-label{width:90px;height:90px}.symbol.symbol-md-90px.symbol-fixed>img{width:90px;height:90px;max-width:none}.symbol.symbol-md-90px.symbol-2by3 .symbol-label{height:90px;width:135px}.symbol.symbol-md-90px.symbol-2by3>img{height:90px;width:135px;max-width:none}.symbol.symbol-md-100px>img{width:100px;height:100px}.symbol.symbol-md-100px .symbol-label{width:100px;height:100px}.symbol.symbol-md-100px.symbol-fixed .symbol-label{width:100px;height:100px}.symbol.symbol-md-100px.symbol-fixed>img{width:100px;height:100px;max-width:none}.symbol.symbol-md-100px.symbol-2by3 .symbol-label{height:100px;width:150px}.symbol.symbol-md-100px.symbol-2by3>img{height:100px;width:150px;max-width:none}.symbol.symbol-md-125px>img{width:125px;height:125px}.symbol.symbol-md-125px .symbol-label{width:125px;height:125px}.symbol.symbol-md-125px.symbol-fixed .symbol-label{width:125px;height:125px}.symbol.symbol-md-125px.symbol-fixed>img{width:125px;height:125px;max-width:none}.symbol.symbol-md-125px.symbol-2by3 .symbol-label{height:125px;width:187.5px}.symbol.symbol-md-125px.symbol-2by3>img{height:125px;width:187.5px;max-width:none}.symbol.symbol-md-150px>img{width:150px;height:150px}.symbol.symbol-md-150px .symbol-label{width:150px;height:150px}.symbol.symbol-md-150px.symbol-fixed .symbol-label{width:150px;height:150px}.symbol.symbol-md-150px.symbol-fixed>img{width:150px;height:150px;max-width:none}.symbol.symbol-md-150px.symbol-2by3 .symbol-label{height:150px;width:225px}.symbol.symbol-md-150px.symbol-2by3>img{height:150px;width:225px;max-width:none}.symbol.symbol-md-160px>img{width:160px;height:160px}.symbol.symbol-md-160px .symbol-label{width:160px;height:160px}.symbol.symbol-md-160px.symbol-fixed .symbol-label{width:160px;height:160px}.symbol.symbol-md-160px.symbol-fixed>img{width:160px;height:160px;max-width:none}.symbol.symbol-md-160px.symbol-2by3 .symbol-label{height:160px;width:240px}.symbol.symbol-md-160px.symbol-2by3>img{height:160px;width:240px;max-width:none}.symbol.symbol-md-175px>img{width:175px;height:175px}.symbol.symbol-md-175px .symbol-label{width:175px;height:175px}.symbol.symbol-md-175px.symbol-fixed .symbol-label{width:175px;height:175px}.symbol.symbol-md-175px.symbol-fixed>img{width:175px;height:175px;max-width:none}.symbol.symbol-md-175px.symbol-2by3 .symbol-label{height:175px;width:262.5px}.symbol.symbol-md-175px.symbol-2by3>img{height:175px;width:262.5px;max-width:none}.symbol.symbol-md-200px>img{width:200px;height:200px}.symbol.symbol-md-200px .symbol-label{width:200px;height:200px}.symbol.symbol-md-200px.symbol-fixed .symbol-label{width:200px;height:200px}.symbol.symbol-md-200px.symbol-fixed>img{width:200px;height:200px;max-width:none}.symbol.symbol-md-200px.symbol-2by3 .symbol-label{height:200px;width:300px}.symbol.symbol-md-200px.symbol-2by3>img{height:200px;width:300px;max-width:none}}@media (min-width:992px){.symbol.symbol-lg-20px>img{width:20px;height:20px}.symbol.symbol-lg-20px .symbol-label{width:20px;height:20px}.symbol.symbol-lg-20px.symbol-fixed .symbol-label{width:20px;height:20px}.symbol.symbol-lg-20px.symbol-fixed>img{width:20px;height:20px;max-width:none}.symbol.symbol-lg-20px.symbol-2by3 .symbol-label{height:20px;width:30px}.symbol.symbol-lg-20px.symbol-2by3>img{height:20px;width:30px;max-width:none}.symbol.symbol-lg-25px>img{width:25px;height:25px}.symbol.symbol-lg-25px .symbol-label{width:25px;height:25px}.symbol.symbol-lg-25px.symbol-fixed .symbol-label{width:25px;height:25px}.symbol.symbol-lg-25px.symbol-fixed>img{width:25px;height:25px;max-width:none}.symbol.symbol-lg-25px.symbol-2by3 .symbol-label{height:25px;width:37.5px}.symbol.symbol-lg-25px.symbol-2by3>img{height:25px;width:37.5px;max-width:none}.symbol.symbol-lg-30px>img{width:30px;height:30px}.symbol.symbol-lg-30px .symbol-label{width:30px;height:30px}.symbol.symbol-lg-30px.symbol-fixed .symbol-label{width:30px;height:30px}.symbol.symbol-lg-30px.symbol-fixed>img{width:30px;height:30px;max-width:none}.symbol.symbol-lg-30px.symbol-2by3 .symbol-label{height:30px;width:45px}.symbol.symbol-lg-30px.symbol-2by3>img{height:30px;width:45px;max-width:none}.symbol.symbol-lg-35px>img{width:35px;height:35px}.symbol.symbol-lg-35px .symbol-label{width:35px;height:35px}.symbol.symbol-lg-35px.symbol-fixed .symbol-label{width:35px;height:35px}.symbol.symbol-lg-35px.symbol-fixed>img{width:35px;height:35px;max-width:none}.symbol.symbol-lg-35px.symbol-2by3 .symbol-label{height:35px;width:52.5px}.symbol.symbol-lg-35px.symbol-2by3>img{height:35px;width:52.5px;max-width:none}.symbol.symbol-lg-40px>img{width:40px;height:40px}.symbol.symbol-lg-40px .symbol-label{width:40px;height:40px}.symbol.symbol-lg-40px.symbol-fixed .symbol-label{width:40px;height:40px}.symbol.symbol-lg-40px.symbol-fixed>img{width:40px;height:40px;max-width:none}.symbol.symbol-lg-40px.symbol-2by3 .symbol-label{height:40px;width:60px}.symbol.symbol-lg-40px.symbol-2by3>img{height:40px;width:60px;max-width:none}.symbol.symbol-lg-45px>img{width:45px;height:45px}.symbol.symbol-lg-45px .symbol-label{width:45px;height:45px}.symbol.symbol-lg-45px.symbol-fixed .symbol-label{width:45px;height:45px}.symbol.symbol-lg-45px.symbol-fixed>img{width:45px;height:45px;max-width:none}.symbol.symbol-lg-45px.symbol-2by3 .symbol-label{height:45px;width:67.5px}.symbol.symbol-lg-45px.symbol-2by3>img{height:45px;width:67.5px;max-width:none}.symbol.symbol-lg-50px>img{width:50px;height:50px}.symbol.symbol-lg-50px .symbol-label{width:50px;height:50px}.symbol.symbol-lg-50px.symbol-fixed .symbol-label{width:50px;height:50px}.symbol.symbol-lg-50px.symbol-fixed>img{width:50px;height:50px;max-width:none}.symbol.symbol-lg-50px.symbol-2by3 .symbol-label{height:50px;width:75px}.symbol.symbol-lg-50px.symbol-2by3>img{height:50px;width:75px;max-width:none}.symbol.symbol-lg-55px>img{width:55px;height:55px}.symbol.symbol-lg-55px .symbol-label{width:55px;height:55px}.symbol.symbol-lg-55px.symbol-fixed .symbol-label{width:55px;height:55px}.symbol.symbol-lg-55px.symbol-fixed>img{width:55px;height:55px;max-width:none}.symbol.symbol-lg-55px.symbol-2by3 .symbol-label{height:55px;width:82.5px}.symbol.symbol-lg-55px.symbol-2by3>img{height:55px;width:82.5px;max-width:none}.symbol.symbol-lg-60px>img{width:60px;height:60px}.symbol.symbol-lg-60px .symbol-label{width:60px;height:60px}.symbol.symbol-lg-60px.symbol-fixed .symbol-label{width:60px;height:60px}.symbol.symbol-lg-60px.symbol-fixed>img{width:60px;height:60px;max-width:none}.symbol.symbol-lg-60px.symbol-2by3 .symbol-label{height:60px;width:90px}.symbol.symbol-lg-60px.symbol-2by3>img{height:60px;width:90px;max-width:none}.symbol.symbol-lg-65px>img{width:65px;height:65px}.symbol.symbol-lg-65px .symbol-label{width:65px;height:65px}.symbol.symbol-lg-65px.symbol-fixed .symbol-label{width:65px;height:65px}.symbol.symbol-lg-65px.symbol-fixed>img{width:65px;height:65px;max-width:none}.symbol.symbol-lg-65px.symbol-2by3 .symbol-label{height:65px;width:97.5px}.symbol.symbol-lg-65px.symbol-2by3>img{height:65px;width:97.5px;max-width:none}.symbol.symbol-lg-70px>img{width:70px;height:70px}.symbol.symbol-lg-70px .symbol-label{width:70px;height:70px}.symbol.symbol-lg-70px.symbol-fixed .symbol-label{width:70px;height:70px}.symbol.symbol-lg-70px.symbol-fixed>img{width:70px;height:70px;max-width:none}.symbol.symbol-lg-70px.symbol-2by3 .symbol-label{height:70px;width:105px}.symbol.symbol-lg-70px.symbol-2by3>img{height:70px;width:105px;max-width:none}.symbol.symbol-lg-75px>img{width:75px;height:75px}.symbol.symbol-lg-75px .symbol-label{width:75px;height:75px}.symbol.symbol-lg-75px.symbol-fixed .symbol-label{width:75px;height:75px}.symbol.symbol-lg-75px.symbol-fixed>img{width:75px;height:75px;max-width:none}.symbol.symbol-lg-75px.symbol-2by3 .symbol-label{height:75px;width:112.5px}.symbol.symbol-lg-75px.symbol-2by3>img{height:75px;width:112.5px;max-width:none}.symbol.symbol-lg-90px>img{width:90px;height:90px}.symbol.symbol-lg-90px .symbol-label{width:90px;height:90px}.symbol.symbol-lg-90px.symbol-fixed .symbol-label{width:90px;height:90px}.symbol.symbol-lg-90px.symbol-fixed>img{width:90px;height:90px;max-width:none}.symbol.symbol-lg-90px.symbol-2by3 .symbol-label{height:90px;width:135px}.symbol.symbol-lg-90px.symbol-2by3>img{height:90px;width:135px;max-width:none}.symbol.symbol-lg-100px>img{width:100px;height:100px}.symbol.symbol-lg-100px .symbol-label{width:100px;height:100px}.symbol.symbol-lg-100px.symbol-fixed .symbol-label{width:100px;height:100px}.symbol.symbol-lg-100px.symbol-fixed>img{width:100px;height:100px;max-width:none}.symbol.symbol-lg-100px.symbol-2by3 .symbol-label{height:100px;width:150px}.symbol.symbol-lg-100px.symbol-2by3>img{height:100px;width:150px;max-width:none}.symbol.symbol-lg-125px>img{width:125px;height:125px}.symbol.symbol-lg-125px .symbol-label{width:125px;height:125px}.symbol.symbol-lg-125px.symbol-fixed .symbol-label{width:125px;height:125px}.symbol.symbol-lg-125px.symbol-fixed>img{width:125px;height:125px;max-width:none}.symbol.symbol-lg-125px.symbol-2by3 .symbol-label{height:125px;width:187.5px}.symbol.symbol-lg-125px.symbol-2by3>img{height:125px;width:187.5px;max-width:none}.symbol.symbol-lg-150px>img{width:150px;height:150px}.symbol.symbol-lg-150px .symbol-label{width:150px;height:150px}.symbol.symbol-lg-150px.symbol-fixed .symbol-label{width:150px;height:150px}.symbol.symbol-lg-150px.symbol-fixed>img{width:150px;height:150px;max-width:none}.symbol.symbol-lg-150px.symbol-2by3 .symbol-label{height:150px;width:225px}.symbol.symbol-lg-150px.symbol-2by3>img{height:150px;width:225px;max-width:none}.symbol.symbol-lg-160px>img{width:160px;height:160px}.symbol.symbol-lg-160px .symbol-label{width:160px;height:160px}.symbol.symbol-lg-160px.symbol-fixed .symbol-label{width:160px;height:160px}.symbol.symbol-lg-160px.symbol-fixed>img{width:160px;height:160px;max-width:none}.symbol.symbol-lg-160px.symbol-2by3 .symbol-label{height:160px;width:240px}.symbol.symbol-lg-160px.symbol-2by3>img{height:160px;width:240px;max-width:none}.symbol.symbol-lg-175px>img{width:175px;height:175px}.symbol.symbol-lg-175px .symbol-label{width:175px;height:175px}.symbol.symbol-lg-175px.symbol-fixed .symbol-label{width:175px;height:175px}.symbol.symbol-lg-175px.symbol-fixed>img{width:175px;height:175px;max-width:none}.symbol.symbol-lg-175px.symbol-2by3 .symbol-label{height:175px;width:262.5px}.symbol.symbol-lg-175px.symbol-2by3>img{height:175px;width:262.5px;max-width:none}.symbol.symbol-lg-200px>img{width:200px;height:200px}.symbol.symbol-lg-200px .symbol-label{width:200px;height:200px}.symbol.symbol-lg-200px.symbol-fixed .symbol-label{width:200px;height:200px}.symbol.symbol-lg-200px.symbol-fixed>img{width:200px;height:200px;max-width:none}.symbol.symbol-lg-200px.symbol-2by3 .symbol-label{height:200px;width:300px}.symbol.symbol-lg-200px.symbol-2by3>img{height:200px;width:300px;max-width:none}}@media (min-width:1200px){.symbol.symbol-xl-20px>img{width:20px;height:20px}.symbol.symbol-xl-20px .symbol-label{width:20px;height:20px}.symbol.symbol-xl-20px.symbol-fixed .symbol-label{width:20px;height:20px}.symbol.symbol-xl-20px.symbol-fixed>img{width:20px;height:20px;max-width:none}.symbol.symbol-xl-20px.symbol-2by3 .symbol-label{height:20px;width:30px}.symbol.symbol-xl-20px.symbol-2by3>img{height:20px;width:30px;max-width:none}.symbol.symbol-xl-25px>img{width:25px;height:25px}.symbol.symbol-xl-25px .symbol-label{width:25px;height:25px}.symbol.symbol-xl-25px.symbol-fixed .symbol-label{width:25px;height:25px}.symbol.symbol-xl-25px.symbol-fixed>img{width:25px;height:25px;max-width:none}.symbol.symbol-xl-25px.symbol-2by3 .symbol-label{height:25px;width:37.5px}.symbol.symbol-xl-25px.symbol-2by3>img{height:25px;width:37.5px;max-width:none}.symbol.symbol-xl-30px>img{width:30px;height:30px}.symbol.symbol-xl-30px .symbol-label{width:30px;height:30px}.symbol.symbol-xl-30px.symbol-fixed .symbol-label{width:30px;height:30px}.symbol.symbol-xl-30px.symbol-fixed>img{width:30px;height:30px;max-width:none}.symbol.symbol-xl-30px.symbol-2by3 .symbol-label{height:30px;width:45px}.symbol.symbol-xl-30px.symbol-2by3>img{height:30px;width:45px;max-width:none}.symbol.symbol-xl-35px>img{width:35px;height:35px}.symbol.symbol-xl-35px .symbol-label{width:35px;height:35px}.symbol.symbol-xl-35px.symbol-fixed .symbol-label{width:35px;height:35px}.symbol.symbol-xl-35px.symbol-fixed>img{width:35px;height:35px;max-width:none}.symbol.symbol-xl-35px.symbol-2by3 .symbol-label{height:35px;width:52.5px}.symbol.symbol-xl-35px.symbol-2by3>img{height:35px;width:52.5px;max-width:none}.symbol.symbol-xl-40px>img{width:40px;height:40px}.symbol.symbol-xl-40px .symbol-label{width:40px;height:40px}.symbol.symbol-xl-40px.symbol-fixed .symbol-label{width:40px;height:40px}.symbol.symbol-xl-40px.symbol-fixed>img{width:40px;height:40px;max-width:none}.symbol.symbol-xl-40px.symbol-2by3 .symbol-label{height:40px;width:60px}.symbol.symbol-xl-40px.symbol-2by3>img{height:40px;width:60px;max-width:none}.symbol.symbol-xl-45px>img{width:45px;height:45px}.symbol.symbol-xl-45px .symbol-label{width:45px;height:45px}.symbol.symbol-xl-45px.symbol-fixed .symbol-label{width:45px;height:45px}.symbol.symbol-xl-45px.symbol-fixed>img{width:45px;height:45px;max-width:none}.symbol.symbol-xl-45px.symbol-2by3 .symbol-label{height:45px;width:67.5px}.symbol.symbol-xl-45px.symbol-2by3>img{height:45px;width:67.5px;max-width:none}.symbol.symbol-xl-50px>img{width:50px;height:50px}.symbol.symbol-xl-50px .symbol-label{width:50px;height:50px}.symbol.symbol-xl-50px.symbol-fixed .symbol-label{width:50px;height:50px}.symbol.symbol-xl-50px.symbol-fixed>img{width:50px;height:50px;max-width:none}.symbol.symbol-xl-50px.symbol-2by3 .symbol-label{height:50px;width:75px}.symbol.symbol-xl-50px.symbol-2by3>img{height:50px;width:75px;max-width:none}.symbol.symbol-xl-55px>img{width:55px;height:55px}.symbol.symbol-xl-55px .symbol-label{width:55px;height:55px}.symbol.symbol-xl-55px.symbol-fixed .symbol-label{width:55px;height:55px}.symbol.symbol-xl-55px.symbol-fixed>img{width:55px;height:55px;max-width:none}.symbol.symbol-xl-55px.symbol-2by3 .symbol-label{height:55px;width:82.5px}.symbol.symbol-xl-55px.symbol-2by3>img{height:55px;width:82.5px;max-width:none}.symbol.symbol-xl-60px>img{width:60px;height:60px}.symbol.symbol-xl-60px .symbol-label{width:60px;height:60px}.symbol.symbol-xl-60px.symbol-fixed .symbol-label{width:60px;height:60px}.symbol.symbol-xl-60px.symbol-fixed>img{width:60px;height:60px;max-width:none}.symbol.symbol-xl-60px.symbol-2by3 .symbol-label{height:60px;width:90px}.symbol.symbol-xl-60px.symbol-2by3>img{height:60px;width:90px;max-width:none}.symbol.symbol-xl-65px>img{width:65px;height:65px}.symbol.symbol-xl-65px .symbol-label{width:65px;height:65px}.symbol.symbol-xl-65px.symbol-fixed .symbol-label{width:65px;height:65px}.symbol.symbol-xl-65px.symbol-fixed>img{width:65px;height:65px;max-width:none}.symbol.symbol-xl-65px.symbol-2by3 .symbol-label{height:65px;width:97.5px}.symbol.symbol-xl-65px.symbol-2by3>img{height:65px;width:97.5px;max-width:none}.symbol.symbol-xl-70px>img{width:70px;height:70px}.symbol.symbol-xl-70px .symbol-label{width:70px;height:70px}.symbol.symbol-xl-70px.symbol-fixed .symbol-label{width:70px;height:70px}.symbol.symbol-xl-70px.symbol-fixed>img{width:70px;height:70px;max-width:none}.symbol.symbol-xl-70px.symbol-2by3 .symbol-label{height:70px;width:105px}.symbol.symbol-xl-70px.symbol-2by3>img{height:70px;width:105px;max-width:none}.symbol.symbol-xl-75px>img{width:75px;height:75px}.symbol.symbol-xl-75px .symbol-label{width:75px;height:75px}.symbol.symbol-xl-75px.symbol-fixed .symbol-label{width:75px;height:75px}.symbol.symbol-xl-75px.symbol-fixed>img{width:75px;height:75px;max-width:none}.symbol.symbol-xl-75px.symbol-2by3 .symbol-label{height:75px;width:112.5px}.symbol.symbol-xl-75px.symbol-2by3>img{height:75px;width:112.5px;max-width:none}.symbol.symbol-xl-90px>img{width:90px;height:90px}.symbol.symbol-xl-90px .symbol-label{width:90px;height:90px}.symbol.symbol-xl-90px.symbol-fixed .symbol-label{width:90px;height:90px}.symbol.symbol-xl-90px.symbol-fixed>img{width:90px;height:90px;max-width:none}.symbol.symbol-xl-90px.symbol-2by3 .symbol-label{height:90px;width:135px}.symbol.symbol-xl-90px.symbol-2by3>img{height:90px;width:135px;max-width:none}.symbol.symbol-xl-100px>img{width:100px;height:100px}.symbol.symbol-xl-100px .symbol-label{width:100px;height:100px}.symbol.symbol-xl-100px.symbol-fixed .symbol-label{width:100px;height:100px}.symbol.symbol-xl-100px.symbol-fixed>img{width:100px;height:100px;max-width:none}.symbol.symbol-xl-100px.symbol-2by3 .symbol-label{height:100px;width:150px}.symbol.symbol-xl-100px.symbol-2by3>img{height:100px;width:150px;max-width:none}.symbol.symbol-xl-125px>img{width:125px;height:125px}.symbol.symbol-xl-125px .symbol-label{width:125px;height:125px}.symbol.symbol-xl-125px.symbol-fixed .symbol-label{width:125px;height:125px}.symbol.symbol-xl-125px.symbol-fixed>img{width:125px;height:125px;max-width:none}.symbol.symbol-xl-125px.symbol-2by3 .symbol-label{height:125px;width:187.5px}.symbol.symbol-xl-125px.symbol-2by3>img{height:125px;width:187.5px;max-width:none}.symbol.symbol-xl-150px>img{width:150px;height:150px}.symbol.symbol-xl-150px .symbol-label{width:150px;height:150px}.symbol.symbol-xl-150px.symbol-fixed .symbol-label{width:150px;height:150px}.symbol.symbol-xl-150px.symbol-fixed>img{width:150px;height:150px;max-width:none}.symbol.symbol-xl-150px.symbol-2by3 .symbol-label{height:150px;width:225px}.symbol.symbol-xl-150px.symbol-2by3>img{height:150px;width:225px;max-width:none}.symbol.symbol-xl-160px>img{width:160px;height:160px}.symbol.symbol-xl-160px .symbol-label{width:160px;height:160px}.symbol.symbol-xl-160px.symbol-fixed .symbol-label{width:160px;height:160px}.symbol.symbol-xl-160px.symbol-fixed>img{width:160px;height:160px;max-width:none}.symbol.symbol-xl-160px.symbol-2by3 .symbol-label{height:160px;width:240px}.symbol.symbol-xl-160px.symbol-2by3>img{height:160px;width:240px;max-width:none}.symbol.symbol-xl-175px>img{width:175px;height:175px}.symbol.symbol-xl-175px .symbol-label{width:175px;height:175px}.symbol.symbol-xl-175px.symbol-fixed .symbol-label{width:175px;height:175px}.symbol.symbol-xl-175px.symbol-fixed>img{width:175px;height:175px;max-width:none}.symbol.symbol-xl-175px.symbol-2by3 .symbol-label{height:175px;width:262.5px}.symbol.symbol-xl-175px.symbol-2by3>img{height:175px;width:262.5px;max-width:none}.symbol.symbol-xl-200px>img{width:200px;height:200px}.symbol.symbol-xl-200px .symbol-label{width:200px;height:200px}.symbol.symbol-xl-200px.symbol-fixed .symbol-label{width:200px;height:200px}.symbol.symbol-xl-200px.symbol-fixed>img{width:200px;height:200px;max-width:none}.symbol.symbol-xl-200px.symbol-2by3 .symbol-label{height:200px;width:300px}.symbol.symbol-xl-200px.symbol-2by3>img{height:200px;width:300px;max-width:none}}@media (min-width:1400px){.symbol.symbol-xxl-20px>img{width:20px;height:20px}.symbol.symbol-xxl-20px .symbol-label{width:20px;height:20px}.symbol.symbol-xxl-20px.symbol-fixed .symbol-label{width:20px;height:20px}.symbol.symbol-xxl-20px.symbol-fixed>img{width:20px;height:20px;max-width:none}.symbol.symbol-xxl-20px.symbol-2by3 .symbol-label{height:20px;width:30px}.symbol.symbol-xxl-20px.symbol-2by3>img{height:20px;width:30px;max-width:none}.symbol.symbol-xxl-25px>img{width:25px;height:25px}.symbol.symbol-xxl-25px .symbol-label{width:25px;height:25px}.symbol.symbol-xxl-25px.symbol-fixed .symbol-label{width:25px;height:25px}.symbol.symbol-xxl-25px.symbol-fixed>img{width:25px;height:25px;max-width:none}.symbol.symbol-xxl-25px.symbol-2by3 .symbol-label{height:25px;width:37.5px}.symbol.symbol-xxl-25px.symbol-2by3>img{height:25px;width:37.5px;max-width:none}.symbol.symbol-xxl-30px>img{width:30px;height:30px}.symbol.symbol-xxl-30px .symbol-label{width:30px;height:30px}.symbol.symbol-xxl-30px.symbol-fixed .symbol-label{width:30px;height:30px}.symbol.symbol-xxl-30px.symbol-fixed>img{width:30px;height:30px;max-width:none}.symbol.symbol-xxl-30px.symbol-2by3 .symbol-label{height:30px;width:45px}.symbol.symbol-xxl-30px.symbol-2by3>img{height:30px;width:45px;max-width:none}.symbol.symbol-xxl-35px>img{width:35px;height:35px}.symbol.symbol-xxl-35px .symbol-label{width:35px;height:35px}.symbol.symbol-xxl-35px.symbol-fixed .symbol-label{width:35px;height:35px}.symbol.symbol-xxl-35px.symbol-fixed>img{width:35px;height:35px;max-width:none}.symbol.symbol-xxl-35px.symbol-2by3 .symbol-label{height:35px;width:52.5px}.symbol.symbol-xxl-35px.symbol-2by3>img{height:35px;width:52.5px;max-width:none}.symbol.symbol-xxl-40px>img{width:40px;height:40px}.symbol.symbol-xxl-40px .symbol-label{width:40px;height:40px}.symbol.symbol-xxl-40px.symbol-fixed .symbol-label{width:40px;height:40px}.symbol.symbol-xxl-40px.symbol-fixed>img{width:40px;height:40px;max-width:none}.symbol.symbol-xxl-40px.symbol-2by3 .symbol-label{height:40px;width:60px}.symbol.symbol-xxl-40px.symbol-2by3>img{height:40px;width:60px;max-width:none}.symbol.symbol-xxl-45px>img{width:45px;height:45px}.symbol.symbol-xxl-45px .symbol-label{width:45px;height:45px}.symbol.symbol-xxl-45px.symbol-fixed .symbol-label{width:45px;height:45px}.symbol.symbol-xxl-45px.symbol-fixed>img{width:45px;height:45px;max-width:none}.symbol.symbol-xxl-45px.symbol-2by3 .symbol-label{height:45px;width:67.5px}.symbol.symbol-xxl-45px.symbol-2by3>img{height:45px;width:67.5px;max-width:none}.symbol.symbol-xxl-50px>img{width:50px;height:50px}.symbol.symbol-xxl-50px .symbol-label{width:50px;height:50px}.symbol.symbol-xxl-50px.symbol-fixed .symbol-label{width:50px;height:50px}.symbol.symbol-xxl-50px.symbol-fixed>img{width:50px;height:50px;max-width:none}.symbol.symbol-xxl-50px.symbol-2by3 .symbol-label{height:50px;width:75px}.symbol.symbol-xxl-50px.symbol-2by3>img{height:50px;width:75px;max-width:none}.symbol.symbol-xxl-55px>img{width:55px;height:55px}.symbol.symbol-xxl-55px .symbol-label{width:55px;height:55px}.symbol.symbol-xxl-55px.symbol-fixed .symbol-label{width:55px;height:55px}.symbol.symbol-xxl-55px.symbol-fixed>img{width:55px;height:55px;max-width:none}.symbol.symbol-xxl-55px.symbol-2by3 .symbol-label{height:55px;width:82.5px}.symbol.symbol-xxl-55px.symbol-2by3>img{height:55px;width:82.5px;max-width:none}.symbol.symbol-xxl-60px>img{width:60px;height:60px}.symbol.symbol-xxl-60px .symbol-label{width:60px;height:60px}.symbol.symbol-xxl-60px.symbol-fixed .symbol-label{width:60px;height:60px}.symbol.symbol-xxl-60px.symbol-fixed>img{width:60px;height:60px;max-width:none}.symbol.symbol-xxl-60px.symbol-2by3 .symbol-label{height:60px;width:90px}.symbol.symbol-xxl-60px.symbol-2by3>img{height:60px;width:90px;max-width:none}.symbol.symbol-xxl-65px>img{width:65px;height:65px}.symbol.symbol-xxl-65px .symbol-label{width:65px;height:65px}.symbol.symbol-xxl-65px.symbol-fixed .symbol-label{width:65px;height:65px}.symbol.symbol-xxl-65px.symbol-fixed>img{width:65px;height:65px;max-width:none}.symbol.symbol-xxl-65px.symbol-2by3 .symbol-label{height:65px;width:97.5px}.symbol.symbol-xxl-65px.symbol-2by3>img{height:65px;width:97.5px;max-width:none}.symbol.symbol-xxl-70px>img{width:70px;height:70px}.symbol.symbol-xxl-70px .symbol-label{width:70px;height:70px}.symbol.symbol-xxl-70px.symbol-fixed .symbol-label{width:70px;height:70px}.symbol.symbol-xxl-70px.symbol-fixed>img{width:70px;height:70px;max-width:none}.symbol.symbol-xxl-70px.symbol-2by3 .symbol-label{height:70px;width:105px}.symbol.symbol-xxl-70px.symbol-2by3>img{height:70px;width:105px;max-width:none}.symbol.symbol-xxl-75px>img{width:75px;height:75px}.symbol.symbol-xxl-75px .symbol-label{width:75px;height:75px}.symbol.symbol-xxl-75px.symbol-fixed .symbol-label{width:75px;height:75px}.symbol.symbol-xxl-75px.symbol-fixed>img{width:75px;height:75px;max-width:none}.symbol.symbol-xxl-75px.symbol-2by3 .symbol-label{height:75px;width:112.5px}.symbol.symbol-xxl-75px.symbol-2by3>img{height:75px;width:112.5px;max-width:none}.symbol.symbol-xxl-90px>img{width:90px;height:90px}.symbol.symbol-xxl-90px .symbol-label{width:90px;height:90px}.symbol.symbol-xxl-90px.symbol-fixed .symbol-label{width:90px;height:90px}.symbol.symbol-xxl-90px.symbol-fixed>img{width:90px;height:90px;max-width:none}.symbol.symbol-xxl-90px.symbol-2by3 .symbol-label{height:90px;width:135px}.symbol.symbol-xxl-90px.symbol-2by3>img{height:90px;width:135px;max-width:none}.symbol.symbol-xxl-100px>img{width:100px;height:100px}.symbol.symbol-xxl-100px .symbol-label{width:100px;height:100px}.symbol.symbol-xxl-100px.symbol-fixed .symbol-label{width:100px;height:100px}.symbol.symbol-xxl-100px.symbol-fixed>img{width:100px;height:100px;max-width:none}.symbol.symbol-xxl-100px.symbol-2by3 .symbol-label{height:100px;width:150px}.symbol.symbol-xxl-100px.symbol-2by3>img{height:100px;width:150px;max-width:none}.symbol.symbol-xxl-125px>img{width:125px;height:125px}.symbol.symbol-xxl-125px .symbol-label{width:125px;height:125px}.symbol.symbol-xxl-125px.symbol-fixed .symbol-label{width:125px;height:125px}.symbol.symbol-xxl-125px.symbol-fixed>img{width:125px;height:125px;max-width:none}.symbol.symbol-xxl-125px.symbol-2by3 .symbol-label{height:125px;width:187.5px}.symbol.symbol-xxl-125px.symbol-2by3>img{height:125px;width:187.5px;max-width:none}.symbol.symbol-xxl-150px>img{width:150px;height:150px}.symbol.symbol-xxl-150px .symbol-label{width:150px;height:150px}.symbol.symbol-xxl-150px.symbol-fixed .symbol-label{width:150px;height:150px}.symbol.symbol-xxl-150px.symbol-fixed>img{width:150px;height:150px;max-width:none}.symbol.symbol-xxl-150px.symbol-2by3 .symbol-label{height:150px;width:225px}.symbol.symbol-xxl-150px.symbol-2by3>img{height:150px;width:225px;max-width:none}.symbol.symbol-xxl-160px>img{width:160px;height:160px}.symbol.symbol-xxl-160px .symbol-label{width:160px;height:160px}.symbol.symbol-xxl-160px.symbol-fixed .symbol-label{width:160px;height:160px}.symbol.symbol-xxl-160px.symbol-fixed>img{width:160px;height:160px;max-width:none}.symbol.symbol-xxl-160px.symbol-2by3 .symbol-label{height:160px;width:240px}.symbol.symbol-xxl-160px.symbol-2by3>img{height:160px;width:240px;max-width:none}.symbol.symbol-xxl-175px>img{width:175px;height:175px}.symbol.symbol-xxl-175px .symbol-label{width:175px;height:175px}.symbol.symbol-xxl-175px.symbol-fixed .symbol-label{width:175px;height:175px}.symbol.symbol-xxl-175px.symbol-fixed>img{width:175px;height:175px;max-width:none}.symbol.symbol-xxl-175px.symbol-2by3 .symbol-label{height:175px;width:262.5px}.symbol.symbol-xxl-175px.symbol-2by3>img{height:175px;width:262.5px;max-width:none}.symbol.symbol-xxl-200px>img{width:200px;height:200px}.symbol.symbol-xxl-200px .symbol-label{width:200px;height:200px}.symbol.symbol-xxl-200px.symbol-fixed .symbol-label{width:200px;height:200px}.symbol.symbol-xxl-200px.symbol-fixed>img{width:200px;height:200px;max-width:none}.symbol.symbol-xxl-200px.symbol-2by3 .symbol-label{height:200px;width:300px}.symbol.symbol-xxl-200px.symbol-2by3>img{height:200px;width:300px;max-width:none}}.symbol-group{display:flex;flex-wrap:wrap;align-items:center;margin-left:10px}.symbol-group .symbol{position:relative;z-index:0;margin-left:-10px;transition:all .3s ease}.symbol-group .symbol:hover{transition:all .3s ease;z-index:1}.symbol-group .symbol-badge{border:2px solid var(--bs-body-bg)}.symbol-group .symbol-label{position:relative}.symbol-group .symbol-label:after{display:block;content:\" \";border-radius:inherit;position:absolute;top:0;right:0;left:0;bottom:0;border:2px solid var(--bs-symbol-border-color);-webkit-background-clip:padding-box;background-clip:padding-box}.symbol-group.symbol-hover .symbol{cursor:pointer}.pulse{position:relative}.pulse.pulse-light .pulse-ring{border-color:var(--bs-light)}.pulse.pulse-primary .pulse-ring{border-color:var(--bs-primary)}.pulse.pulse-secondary .pulse-ring{border-color:var(--bs-secondary)}.pulse.pulse-success .pulse-ring{border-color:var(--bs-success)}.pulse.pulse-info .pulse-ring{border-color:var(--bs-info)}.pulse.pulse-warning .pulse-ring{border-color:var(--bs-warning)}.pulse.pulse-danger .pulse-ring{border-color:var(--bs-danger)}.pulse.pulse-dark .pulse-ring{border-color:var(--bs-dark)}.pulse-ring{display:block;border-radius:40px;height:40px;width:40px;position:absolute;animation:animation-pulse 3.5s ease-out;animation-iteration-count:infinite;opacity:0;border-width:3px;border-style:solid;border-color:var(--bs-gray-500)}@keyframes animation-pulse{0%{-webkit-transform:scale(.1,.1);opacity:0}60%{-webkit-transform:scale(.1,.1);opacity:0}65%{opacity:1}100%{-webkit-transform:scale(1.2,1.2);opacity:0}}.page-loading *,[data-kt-app-page-loading=on] *{transition:none!important}.page-loader{background-color:var(--bs-body-bg);position:fixed;top:0;bottom:0;left:0;right:0;z-index:10000;display:none}.page-loading .page-loader,[data-kt-app-page-loading=on] .page-loader{display:flex;justify-content:center;align-items:center}.scrolltop{position:fixed;display:none;cursor:pointer;z-index:105;justify-content:center;align-items:center;width:36px;height:36px;bottom:43px;right:7px;background-color:var(--bs-scrolltop-bg-color);box-shadow:var(--bs-scrolltop-box-shadow);opacity:0;transition:color .2s ease;border-radius:.475rem}.scrolltop .svg-icon{color:var(--bs-scrolltop-icon-color)}.scrolltop .svg-icon svg{height:24px;width:24px}.scrolltop>i{font-size:1.3rem;color:var(--bs-scrolltop-icon-color)}.scrolltop:hover{background-color:var(--bs-scrolltop-bg-color-hover)}.scrolltop:hover .svg-icon,.scrolltop:hover i{color:var(--bs-scrolltop-icon-color-hover)}[data-kt-scrolltop=on] .scrolltop{opacity:var(--bs-scrolltop-opacity-on);animation:animation-scrolltop .4s ease-out 1;display:flex}[data-kt-scrolltop=on] .scrolltop:hover{transition:color .2s ease;opacity:var(--bs-scrolltop-opacity-hover)}@media (max-width:991.98px){.scrolltop{bottom:23px;right:5px;width:30px;height:30px}}@keyframes animation-scrolltop{from{margin-bottom:-15px}to{margin-bottom:0}}.svg-icon{line-height:1;color:var(--bs-text-muted)}.svg-icon svg{height:1.15rem;width:1.15rem}.svg-icon.svg-icon-white{color:var(--bs-text-white)}.svg-icon.svg-icon-primary{color:var(--bs-text-primary)}.svg-icon.svg-icon-secondary{color:var(--bs-text-secondary)}.svg-icon.svg-icon-light{color:var(--bs-text-light)}.svg-icon.svg-icon-success{color:var(--bs-text-success)}.svg-icon.svg-icon-info{color:var(--bs-text-info)}.svg-icon.svg-icon-warning{color:var(--bs-text-warning)}.svg-icon.svg-icon-danger{color:var(--bs-text-danger)}.svg-icon.svg-icon-dark{color:var(--bs-text-dark)}.svg-icon.svg-icon-muted{color:var(--bs-text-muted)}.svg-icon.svg-icon-gray-100{color:var(--bs-text-gray-100)}.svg-icon.svg-icon-gray-200{color:var(--bs-text-gray-200)}.svg-icon.svg-icon-gray-300{color:var(--bs-text-gray-300)}.svg-icon.svg-icon-gray-400{color:var(--bs-text-gray-400)}.svg-icon.svg-icon-gray-500{color:var(--bs-text-gray-500)}.svg-icon.svg-icon-gray-600{color:var(--bs-text-gray-600)}.svg-icon.svg-icon-gray-700{color:var(--bs-text-gray-700)}.svg-icon.svg-icon-gray-800{color:var(--bs-text-gray-800)}.svg-icon.svg-icon-gray-900{color:var(--bs-text-gray-900)}.svg-icon.svg-icon-1 svg{height:1.75rem!important;width:1.75rem!important}.svg-icon.svg-icon-2 svg{height:1.5rem!important;width:1.5rem!important}.svg-icon.svg-icon-3 svg{height:1.35rem!important;width:1.35rem!important}.svg-icon.svg-icon-4 svg{height:1.25rem!important;width:1.25rem!important}.svg-icon.svg-icon-5 svg{height:1.15rem!important;width:1.15rem!important}.svg-icon.svg-icon-6 svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-7 svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-8 svg{height:.85rem!important;width:.85rem!important}.svg-icon.svg-icon-9 svg{height:.75rem!important;width:.75rem!important}.svg-icon.svg-icon-10 svg{height:.5rem!important;width:.5rem!important}.svg-icon.svg-icon-sm svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-base svg{height:1rem!important;width:1rem!important}.svg-icon.svg-icon-lg svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-xl svg{height:1.21rem!important;width:1.21rem!important}.svg-icon.svg-icon-fluid svg{height:100%!important;width:100%!important}.svg-icon.svg-icon-2x svg{height:2rem!important;width:2rem!important}.svg-icon.svg-icon-2qx svg{height:2.25rem!important;width:2.25rem!important}.svg-icon.svg-icon-2hx svg{height:2.5rem!important;width:2.5rem!important}.svg-icon.svg-icon-2tx svg{height:2.75rem!important;width:2.75rem!important}.svg-icon.svg-icon-3x svg{height:3rem!important;width:3rem!important}.svg-icon.svg-icon-3qx svg{height:3.25rem!important;width:3.25rem!important}.svg-icon.svg-icon-3hx svg{height:3.5rem!important;width:3.5rem!important}.svg-icon.svg-icon-3tx svg{height:3.75rem!important;width:3.75rem!important}.svg-icon.svg-icon-4x svg{height:4rem!important;width:4rem!important}.svg-icon.svg-icon-4qx svg{height:4.25rem!important;width:4.25rem!important}.svg-icon.svg-icon-4hx svg{height:4.5rem!important;width:4.5rem!important}.svg-icon.svg-icon-4tx svg{height:4.75rem!important;width:4.75rem!important}.svg-icon.svg-icon-5x svg{height:5rem!important;width:5rem!important}.svg-icon.svg-icon-5qx svg{height:5.25rem!important;width:5.25rem!important}.svg-icon.svg-icon-5hx svg{height:5.5rem!important;width:5.5rem!important}.svg-icon.svg-icon-5tx svg{height:5.75rem!important;width:5.75rem!important}.svg-icon.svg-icon-6x svg{height:6rem!important;width:6rem!important}.svg-icon.svg-icon-6qx svg{height:6.25rem!important;width:6.25rem!important}.svg-icon.svg-icon-6hx svg{height:6.5rem!important;width:6.5rem!important}.svg-icon.svg-icon-6tx svg{height:6.75rem!important;width:6.75rem!important}.svg-icon.svg-icon-7x svg{height:7rem!important;width:7rem!important}.svg-icon.svg-icon-7qx svg{height:7.25rem!important;width:7.25rem!important}.svg-icon.svg-icon-7hx svg{height:7.5rem!important;width:7.5rem!important}.svg-icon.svg-icon-7tx svg{height:7.75rem!important;width:7.75rem!important}@media (min-width:576px){.svg-icon.svg-icon-sm-1 svg{height:1.75rem!important;width:1.75rem!important}.svg-icon.svg-icon-sm-2 svg{height:1.5rem!important;width:1.5rem!important}.svg-icon.svg-icon-sm-3 svg{height:1.35rem!important;width:1.35rem!important}.svg-icon.svg-icon-sm-4 svg{height:1.25rem!important;width:1.25rem!important}.svg-icon.svg-icon-sm-5 svg{height:1.15rem!important;width:1.15rem!important}.svg-icon.svg-icon-sm-6 svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-sm-7 svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-sm-8 svg{height:.85rem!important;width:.85rem!important}.svg-icon.svg-icon-sm-9 svg{height:.75rem!important;width:.75rem!important}.svg-icon.svg-icon-sm-10 svg{height:.5rem!important;width:.5rem!important}.svg-icon.svg-icon-sm-sm svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-sm-base svg{height:1rem!important;width:1rem!important}.svg-icon.svg-icon-sm-lg svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-sm-xl svg{height:1.21rem!important;width:1.21rem!important}.svg-icon.svg-icon-sm-fluid svg{height:100%!important;width:100%!important}.svg-icon.svg-icon-sm-2x svg{height:2rem!important;width:2rem!important}.svg-icon.svg-icon-sm-2qx svg{height:2.25rem!important;width:2.25rem!important}.svg-icon.svg-icon-sm-2hx svg{height:2.5rem!important;width:2.5rem!important}.svg-icon.svg-icon-sm-2tx svg{height:2.75rem!important;width:2.75rem!important}.svg-icon.svg-icon-sm-3x svg{height:3rem!important;width:3rem!important}.svg-icon.svg-icon-sm-3qx svg{height:3.25rem!important;width:3.25rem!important}.svg-icon.svg-icon-sm-3hx svg{height:3.5rem!important;width:3.5rem!important}.svg-icon.svg-icon-sm-3tx svg{height:3.75rem!important;width:3.75rem!important}.svg-icon.svg-icon-sm-4x svg{height:4rem!important;width:4rem!important}.svg-icon.svg-icon-sm-4qx svg{height:4.25rem!important;width:4.25rem!important}.svg-icon.svg-icon-sm-4hx svg{height:4.5rem!important;width:4.5rem!important}.svg-icon.svg-icon-sm-4tx svg{height:4.75rem!important;width:4.75rem!important}.svg-icon.svg-icon-sm-5x svg{height:5rem!important;width:5rem!important}.svg-icon.svg-icon-sm-5qx svg{height:5.25rem!important;width:5.25rem!important}.svg-icon.svg-icon-sm-5hx svg{height:5.5rem!important;width:5.5rem!important}.svg-icon.svg-icon-sm-5tx svg{height:5.75rem!important;width:5.75rem!important}.svg-icon.svg-icon-sm-6x svg{height:6rem!important;width:6rem!important}.svg-icon.svg-icon-sm-6qx svg{height:6.25rem!important;width:6.25rem!important}.svg-icon.svg-icon-sm-6hx svg{height:6.5rem!important;width:6.5rem!important}.svg-icon.svg-icon-sm-6tx svg{height:6.75rem!important;width:6.75rem!important}.svg-icon.svg-icon-sm-7x svg{height:7rem!important;width:7rem!important}.svg-icon.svg-icon-sm-7qx svg{height:7.25rem!important;width:7.25rem!important}.svg-icon.svg-icon-sm-7hx svg{height:7.5rem!important;width:7.5rem!important}.svg-icon.svg-icon-sm-7tx svg{height:7.75rem!important;width:7.75rem!important}}@media (min-width:768px){.svg-icon.svg-icon-md-1 svg{height:1.75rem!important;width:1.75rem!important}.svg-icon.svg-icon-md-2 svg{height:1.5rem!important;width:1.5rem!important}.svg-icon.svg-icon-md-3 svg{height:1.35rem!important;width:1.35rem!important}.svg-icon.svg-icon-md-4 svg{height:1.25rem!important;width:1.25rem!important}.svg-icon.svg-icon-md-5 svg{height:1.15rem!important;width:1.15rem!important}.svg-icon.svg-icon-md-6 svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-md-7 svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-md-8 svg{height:.85rem!important;width:.85rem!important}.svg-icon.svg-icon-md-9 svg{height:.75rem!important;width:.75rem!important}.svg-icon.svg-icon-md-10 svg{height:.5rem!important;width:.5rem!important}.svg-icon.svg-icon-md-sm svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-md-base svg{height:1rem!important;width:1rem!important}.svg-icon.svg-icon-md-lg svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-md-xl svg{height:1.21rem!important;width:1.21rem!important}.svg-icon.svg-icon-md-fluid svg{height:100%!important;width:100%!important}.svg-icon.svg-icon-md-2x svg{height:2rem!important;width:2rem!important}.svg-icon.svg-icon-md-2qx svg{height:2.25rem!important;width:2.25rem!important}.svg-icon.svg-icon-md-2hx svg{height:2.5rem!important;width:2.5rem!important}.svg-icon.svg-icon-md-2tx svg{height:2.75rem!important;width:2.75rem!important}.svg-icon.svg-icon-md-3x svg{height:3rem!important;width:3rem!important}.svg-icon.svg-icon-md-3qx svg{height:3.25rem!important;width:3.25rem!important}.svg-icon.svg-icon-md-3hx svg{height:3.5rem!important;width:3.5rem!important}.svg-icon.svg-icon-md-3tx svg{height:3.75rem!important;width:3.75rem!important}.svg-icon.svg-icon-md-4x svg{height:4rem!important;width:4rem!important}.svg-icon.svg-icon-md-4qx svg{height:4.25rem!important;width:4.25rem!important}.svg-icon.svg-icon-md-4hx svg{height:4.5rem!important;width:4.5rem!important}.svg-icon.svg-icon-md-4tx svg{height:4.75rem!important;width:4.75rem!important}.svg-icon.svg-icon-md-5x svg{height:5rem!important;width:5rem!important}.svg-icon.svg-icon-md-5qx svg{height:5.25rem!important;width:5.25rem!important}.svg-icon.svg-icon-md-5hx svg{height:5.5rem!important;width:5.5rem!important}.svg-icon.svg-icon-md-5tx svg{height:5.75rem!important;width:5.75rem!important}.svg-icon.svg-icon-md-6x svg{height:6rem!important;width:6rem!important}.svg-icon.svg-icon-md-6qx svg{height:6.25rem!important;width:6.25rem!important}.svg-icon.svg-icon-md-6hx svg{height:6.5rem!important;width:6.5rem!important}.svg-icon.svg-icon-md-6tx svg{height:6.75rem!important;width:6.75rem!important}.svg-icon.svg-icon-md-7x svg{height:7rem!important;width:7rem!important}.svg-icon.svg-icon-md-7qx svg{height:7.25rem!important;width:7.25rem!important}.svg-icon.svg-icon-md-7hx svg{height:7.5rem!important;width:7.5rem!important}.svg-icon.svg-icon-md-7tx svg{height:7.75rem!important;width:7.75rem!important}}@media (min-width:992px){.svg-icon.svg-icon-lg-1 svg{height:1.75rem!important;width:1.75rem!important}.svg-icon.svg-icon-lg-2 svg{height:1.5rem!important;width:1.5rem!important}.svg-icon.svg-icon-lg-3 svg{height:1.35rem!important;width:1.35rem!important}.svg-icon.svg-icon-lg-4 svg{height:1.25rem!important;width:1.25rem!important}.svg-icon.svg-icon-lg-5 svg{height:1.15rem!important;width:1.15rem!important}.svg-icon.svg-icon-lg-6 svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-lg-7 svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-lg-8 svg{height:.85rem!important;width:.85rem!important}.svg-icon.svg-icon-lg-9 svg{height:.75rem!important;width:.75rem!important}.svg-icon.svg-icon-lg-10 svg{height:.5rem!important;width:.5rem!important}.svg-icon.svg-icon-lg-sm svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-lg-base svg{height:1rem!important;width:1rem!important}.svg-icon.svg-icon-lg-lg svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-lg-xl svg{height:1.21rem!important;width:1.21rem!important}.svg-icon.svg-icon-lg-fluid svg{height:100%!important;width:100%!important}.svg-icon.svg-icon-lg-2x svg{height:2rem!important;width:2rem!important}.svg-icon.svg-icon-lg-2qx svg{height:2.25rem!important;width:2.25rem!important}.svg-icon.svg-icon-lg-2hx svg{height:2.5rem!important;width:2.5rem!important}.svg-icon.svg-icon-lg-2tx svg{height:2.75rem!important;width:2.75rem!important}.svg-icon.svg-icon-lg-3x svg{height:3rem!important;width:3rem!important}.svg-icon.svg-icon-lg-3qx svg{height:3.25rem!important;width:3.25rem!important}.svg-icon.svg-icon-lg-3hx svg{height:3.5rem!important;width:3.5rem!important}.svg-icon.svg-icon-lg-3tx svg{height:3.75rem!important;width:3.75rem!important}.svg-icon.svg-icon-lg-4x svg{height:4rem!important;width:4rem!important}.svg-icon.svg-icon-lg-4qx svg{height:4.25rem!important;width:4.25rem!important}.svg-icon.svg-icon-lg-4hx svg{height:4.5rem!important;width:4.5rem!important}.svg-icon.svg-icon-lg-4tx svg{height:4.75rem!important;width:4.75rem!important}.svg-icon.svg-icon-lg-5x svg{height:5rem!important;width:5rem!important}.svg-icon.svg-icon-lg-5qx svg{height:5.25rem!important;width:5.25rem!important}.svg-icon.svg-icon-lg-5hx svg{height:5.5rem!important;width:5.5rem!important}.svg-icon.svg-icon-lg-5tx svg{height:5.75rem!important;width:5.75rem!important}.svg-icon.svg-icon-lg-6x svg{height:6rem!important;width:6rem!important}.svg-icon.svg-icon-lg-6qx svg{height:6.25rem!important;width:6.25rem!important}.svg-icon.svg-icon-lg-6hx svg{height:6.5rem!important;width:6.5rem!important}.svg-icon.svg-icon-lg-6tx svg{height:6.75rem!important;width:6.75rem!important}.svg-icon.svg-icon-lg-7x svg{height:7rem!important;width:7rem!important}.svg-icon.svg-icon-lg-7qx svg{height:7.25rem!important;width:7.25rem!important}.svg-icon.svg-icon-lg-7hx svg{height:7.5rem!important;width:7.5rem!important}.svg-icon.svg-icon-lg-7tx svg{height:7.75rem!important;width:7.75rem!important}}@media (min-width:1200px){.svg-icon.svg-icon-xl-1 svg{height:1.75rem!important;width:1.75rem!important}.svg-icon.svg-icon-xl-2 svg{height:1.5rem!important;width:1.5rem!important}.svg-icon.svg-icon-xl-3 svg{height:1.35rem!important;width:1.35rem!important}.svg-icon.svg-icon-xl-4 svg{height:1.25rem!important;width:1.25rem!important}.svg-icon.svg-icon-xl-5 svg{height:1.15rem!important;width:1.15rem!important}.svg-icon.svg-icon-xl-6 svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-xl-7 svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-xl-8 svg{height:.85rem!important;width:.85rem!important}.svg-icon.svg-icon-xl-9 svg{height:.75rem!important;width:.75rem!important}.svg-icon.svg-icon-xl-10 svg{height:.5rem!important;width:.5rem!important}.svg-icon.svg-icon-xl-sm svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-xl-base svg{height:1rem!important;width:1rem!important}.svg-icon.svg-icon-xl-lg svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-xl-xl svg{height:1.21rem!important;width:1.21rem!important}.svg-icon.svg-icon-xl-fluid svg{height:100%!important;width:100%!important}.svg-icon.svg-icon-xl-2x svg{height:2rem!important;width:2rem!important}.svg-icon.svg-icon-xl-2qx svg{height:2.25rem!important;width:2.25rem!important}.svg-icon.svg-icon-xl-2hx svg{height:2.5rem!important;width:2.5rem!important}.svg-icon.svg-icon-xl-2tx svg{height:2.75rem!important;width:2.75rem!important}.svg-icon.svg-icon-xl-3x svg{height:3rem!important;width:3rem!important}.svg-icon.svg-icon-xl-3qx svg{height:3.25rem!important;width:3.25rem!important}.svg-icon.svg-icon-xl-3hx svg{height:3.5rem!important;width:3.5rem!important}.svg-icon.svg-icon-xl-3tx svg{height:3.75rem!important;width:3.75rem!important}.svg-icon.svg-icon-xl-4x svg{height:4rem!important;width:4rem!important}.svg-icon.svg-icon-xl-4qx svg{height:4.25rem!important;width:4.25rem!important}.svg-icon.svg-icon-xl-4hx svg{height:4.5rem!important;width:4.5rem!important}.svg-icon.svg-icon-xl-4tx svg{height:4.75rem!important;width:4.75rem!important}.svg-icon.svg-icon-xl-5x svg{height:5rem!important;width:5rem!important}.svg-icon.svg-icon-xl-5qx svg{height:5.25rem!important;width:5.25rem!important}.svg-icon.svg-icon-xl-5hx svg{height:5.5rem!important;width:5.5rem!important}.svg-icon.svg-icon-xl-5tx svg{height:5.75rem!important;width:5.75rem!important}.svg-icon.svg-icon-xl-6x svg{height:6rem!important;width:6rem!important}.svg-icon.svg-icon-xl-6qx svg{height:6.25rem!important;width:6.25rem!important}.svg-icon.svg-icon-xl-6hx svg{height:6.5rem!important;width:6.5rem!important}.svg-icon.svg-icon-xl-6tx svg{height:6.75rem!important;width:6.75rem!important}.svg-icon.svg-icon-xl-7x svg{height:7rem!important;width:7rem!important}.svg-icon.svg-icon-xl-7qx svg{height:7.25rem!important;width:7.25rem!important}.svg-icon.svg-icon-xl-7hx svg{height:7.5rem!important;width:7.5rem!important}.svg-icon.svg-icon-xl-7tx svg{height:7.75rem!important;width:7.75rem!important}}@media (min-width:1400px){.svg-icon.svg-icon-xxl-1 svg{height:1.75rem!important;width:1.75rem!important}.svg-icon.svg-icon-xxl-2 svg{height:1.5rem!important;width:1.5rem!important}.svg-icon.svg-icon-xxl-3 svg{height:1.35rem!important;width:1.35rem!important}.svg-icon.svg-icon-xxl-4 svg{height:1.25rem!important;width:1.25rem!important}.svg-icon.svg-icon-xxl-5 svg{height:1.15rem!important;width:1.15rem!important}.svg-icon.svg-icon-xxl-6 svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-xxl-7 svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-xxl-8 svg{height:.85rem!important;width:.85rem!important}.svg-icon.svg-icon-xxl-9 svg{height:.75rem!important;width:.75rem!important}.svg-icon.svg-icon-xxl-10 svg{height:.5rem!important;width:.5rem!important}.svg-icon.svg-icon-xxl-sm svg{height:.95rem!important;width:.95rem!important}.svg-icon.svg-icon-xxl-base svg{height:1rem!important;width:1rem!important}.svg-icon.svg-icon-xxl-lg svg{height:1.075rem!important;width:1.075rem!important}.svg-icon.svg-icon-xxl-xl svg{height:1.21rem!important;width:1.21rem!important}.svg-icon.svg-icon-xxl-fluid svg{height:100%!important;width:100%!important}.svg-icon.svg-icon-xxl-2x svg{height:2rem!important;width:2rem!important}.svg-icon.svg-icon-xxl-2qx svg{height:2.25rem!important;width:2.25rem!important}.svg-icon.svg-icon-xxl-2hx svg{height:2.5rem!important;width:2.5rem!important}.svg-icon.svg-icon-xxl-2tx svg{height:2.75rem!important;width:2.75rem!important}.svg-icon.svg-icon-xxl-3x svg{height:3rem!important;width:3rem!important}.svg-icon.svg-icon-xxl-3qx svg{height:3.25rem!important;width:3.25rem!important}.svg-icon.svg-icon-xxl-3hx svg{height:3.5rem!important;width:3.5rem!important}.svg-icon.svg-icon-xxl-3tx svg{height:3.75rem!important;width:3.75rem!important}.svg-icon.svg-icon-xxl-4x svg{height:4rem!important;width:4rem!important}.svg-icon.svg-icon-xxl-4qx svg{height:4.25rem!important;width:4.25rem!important}.svg-icon.svg-icon-xxl-4hx svg{height:4.5rem!important;width:4.5rem!important}.svg-icon.svg-icon-xxl-4tx svg{height:4.75rem!important;width:4.75rem!important}.svg-icon.svg-icon-xxl-5x svg{height:5rem!important;width:5rem!important}.svg-icon.svg-icon-xxl-5qx svg{height:5.25rem!important;width:5.25rem!important}.svg-icon.svg-icon-xxl-5hx svg{height:5.5rem!important;width:5.5rem!important}.svg-icon.svg-icon-xxl-5tx svg{height:5.75rem!important;width:5.75rem!important}.svg-icon.svg-icon-xxl-6x svg{height:6rem!important;width:6rem!important}.svg-icon.svg-icon-xxl-6qx svg{height:6.25rem!important;width:6.25rem!important}.svg-icon.svg-icon-xxl-6hx svg{height:6.5rem!important;width:6.5rem!important}.svg-icon.svg-icon-xxl-6tx svg{height:6.75rem!important;width:6.75rem!important}.svg-icon.svg-icon-xxl-7x svg{height:7rem!important;width:7rem!important}.svg-icon.svg-icon-xxl-7qx svg{height:7.25rem!important;width:7.25rem!important}.svg-icon.svg-icon-xxl-7hx svg{height:7.5rem!important;width:7.5rem!important}.svg-icon.svg-icon-xxl-7tx svg{height:7.75rem!important;width:7.75rem!important}}.fixed-top{position:fixed;z-index:101;top:0;left:0;right:0}@media (min-width:576px){.fixed-top-sm{position:fixed;z-index:101;top:0;left:0;right:0}}@media (min-width:768px){.fixed-top-md{position:fixed;z-index:101;top:0;left:0;right:0}}@media (min-width:992px){.fixed-top-lg{position:fixed;z-index:101;top:0;left:0;right:0}}@media (min-width:1200px){.fixed-top-xl{position:fixed;z-index:101;top:0;left:0;right:0}}@media (min-width:1400px){.fixed-top-xxl{position:fixed;z-index:101;top:0;left:0;right:0}}.timeline{--bs-timeline-icon-size:38px;--bs-timeline-icon-space:0.35rem}.timeline .timeline-item{position:relative;padding:0;margin:0;display:flex;align-items:flex-start}.timeline .timeline-item:last-child .timeline-line{bottom:100%}.timeline .timeline-line{display:block;content:\" \";justify-content:center;position:absolute;z-index:0;left:0;top:var(--bs-timeline-icon-size);bottom:0;transform:translate(50%);border-left-width:1px;border-left-style:solid;border-left-color:var(--bs-gray-300);width:var(--bs-timeline-icon-size);margin-top:var(--bs-timeline-icon-space);margin-bottom:var(--bs-timeline-icon-space)}.timeline .timeline-icon{z-index:1;flex-shrink:0;margin-right:1rem;width:var(--bs-timeline-icon-size);height:var(--bs-timeline-icon-size);display:flex;text-align:center;align-items:center;justify-content:center;border:1px solid var(--bs-gray-300);border-radius:50%}.timeline .timeline-content{width:100%;overflow:auto;margin-bottom:1.5rem}.timeline.timeline-center .timeline-item{align-items:center}.timeline.timeline-center .timeline-item:first-child .timeline-line{top:50%}.timeline.timeline-center .timeline-item:last-child .timeline-line{bottom:50%}.timeline.timeline-border-dashed .timeline-line{border-left-style:dashed!important}.timeline.timeline-border-dashed .timeline-icon{border-style:dashed!important}.timeline-label{position:relative}.timeline-label:before{content:\"\";position:absolute;left:51px;width:3px;top:0;bottom:0;background-color:var(--bs-gray-200)}.timeline-label .timeline-item{display:flex;align-items:flex-start;position:relative;margin-bottom:1.7rem}.timeline-label .timeline-item:last-child{margin-bottom:0}.timeline-label .timeline-label{width:50px;flex-shrink:0;position:relative;color:var(--bs-gray-800)}.timeline-label .timeline-badge{flex-shrink:0;background-color:var(--bs-body-bg);width:1rem;height:1rem;display:flex;justify-content:center;align-items:center;z-index:1;position:relative;margin-top:1px;margin-left:-.5rem;padding:3px!important;border:6px solid var(--bs-body-bg)!important}.timeline-label .timeline-content{flex-grow:1}.overlay{position:relative}.overlay .overlay-layer{position:absolute;top:0;bottom:0;left:0;right:0;display:flex;justify-content:center;align-items:center;background-color:var(--bs-overlay-bg);transition:all .3s ease;opacity:0}.overlay.overlay-block .overlay-layer,.overlay.overlay-show .overlay-layer,.overlay:hover .overlay-layer{transition:all .3s ease;opacity:1}.overlay.overlay-block{cursor:wait}.bullet{display:inline-block;background-color:var(--bs-bullet-bg-color);border-radius:6px;width:8px;height:4px;flex-shrink:0}.bullet-dot{width:4px;height:4px;border-radius:100%!important}.bullet-vertical{width:4px;height:8px}.bullet-line{width:5px;height:1px;border-radius:0}.drawer{display:flex!important;overflow:auto;z-index:110;position:fixed;top:0;bottom:0;background-color:var(--bs-drawer-bg-color);transition:transform .3s ease-in-out!important}.drawer.drawer-start{left:0;transform:translateX(-100%)}.drawer.drawer-end{right:0;transform:translateX(100%)}.drawer.drawer-bottom{bottom:0;top:auto;left:0;right:0;transform:translateY(100%)}.drawer.drawer-top{top:0;bottom:auto;left:0;right:0;transform:translateY(-100%)}.drawer.drawer-on{transform:none;box-shadow:var(--bs-drawer-box-shadow);transition:transform .3s ease-in-out!important}.drawer-overlay{position:fixed;top:0;left:0;bottom:0;right:0;overflow:hidden;z-index:109;background-color:var(--bs-drawer-overlay-bg-color);animation:animation-drawer-fade-in .3s ease-in-out 1}[data-kt-drawer=true]{display:none}@keyframes animation-drawer-fade-in{from{opacity:0}to{opacity:1}}@media (max-width:991.98px){body[data-kt-drawer=on]{overflow:hidden}}.badge{display:inline-flex;align-items:center}.badge.badge-circle,.badge.badge-square{display:inline-flex;align-items:center;justify-content:center;height:1.75rem;min-width:1.75rem;padding:0 .1rem;line-height:0}.badge.badge-circle{border-radius:50%;padding:0;min-width:unset;width:1.75rem}.badge.badge-sm{min-width:1.5rem;font-size:.8rem}.badge.badge-sm.badge-square{height:1.5rem}.badge.badge-sm.badge-circle{width:1.5rem;height:1.5rem}.badge.badge-lg{min-width:2rem;font-size:1rem}.badge.badge-lg.badge-square{height:2rem}.badge.badge-lg.badge-circle{width:2rem;height:2rem}.badge-light{color:var(--bs-light-inverse);background-color:var(--bs-light)}.badge-light.badge-outline{border:1px solid var(--bs-light);background-color:transparent;color:var(--bs-light)}.badge-light-light{background-color:var(--bs-light-light);color:var(--bs-light)}.badge-primary{color:var(--bs-primary-inverse);background-color:var(--bs-primary)}.badge-primary.badge-outline{border:1px solid var(--bs-primary);background-color:transparent;color:var(--bs-primary)}.badge-light-primary{background-color:var(--bs-primary-light);color:var(--bs-primary)}.badge-secondary{color:var(--bs-secondary-inverse);background-color:var(--bs-secondary)}.badge-secondary.badge-outline{border:1px solid var(--bs-secondary);background-color:transparent;color:var(--bs-secondary-inverse)}.badge-light-secondary{background-color:var(--bs-secondary-light);color:var(--bs-secondary-inverse)}.badge-success{color:var(--bs-success-inverse);background-color:var(--bs-success)}.badge-success.badge-outline{border:1px solid var(--bs-success);background-color:transparent;color:var(--bs-success)}.badge-light-success{background-color:var(--bs-success-light);color:var(--bs-success)}.badge-info{color:var(--bs-info-inverse);background-color:var(--bs-info)}.badge-info.badge-outline{border:1px solid var(--bs-info);background-color:transparent;color:var(--bs-info)}.badge-light-info{background-color:var(--bs-info-light);color:var(--bs-info)}.badge-warning{color:var(--bs-warning-inverse);background-color:var(--bs-warning)}.badge-warning.badge-outline{border:1px solid var(--bs-warning);background-color:transparent;color:var(--bs-warning)}.badge-light-warning{background-color:var(--bs-warning-light);color:var(--bs-warning)}.badge-danger{color:var(--bs-danger-inverse);background-color:var(--bs-danger)}.badge-danger.badge-outline{border:1px solid var(--bs-danger);background-color:transparent;color:var(--bs-danger)}.badge-light-danger{background-color:var(--bs-danger-light);color:var(--bs-danger)}.badge-dark{color:var(--bs-dark-inverse);background-color:var(--bs-dark)}.badge-dark.badge-outline{border:1px solid var(--bs-dark);background-color:transparent;color:var(--bs-dark)}.badge-light-dark{background-color:var(--bs-dark-light);color:var(--bs-dark)}.indicator-progress{display:none}[data-kt-indicator=on]>.indicator-progress{display:inline-block}[data-kt-indicator=on]>.indicator-label{display:none}.hover-elevate-up{transition:transform .3s ease}.hover-elevate-up:hover{transform:translateY(-2.5%);transition:transform .3s ease;will-change:transform}.hover-elevate-down{transition:transform .3s ease}.hover-elevate-down:hover{transform:translateY(2.5%);transition:transform .3s ease;will-change:transform}.hover-scale{transition:transform .3s ease}.hover-scale:hover{transform:scale(1.1);transition:transform .3s ease;will-change:transform}.hover-rotate-end{transition:transform .3s ease}.hover-rotate-end:hover{transform:rotate(4deg);transition:transform .3s ease;will-change:transform}.hover-rotate-start{transition:transform .3s ease}.hover-rotate-start:hover{transform:rotate(-4deg);transition:transform .3s ease;will-change:transform}.rotate{display:inline-flex;align-items:center}.rotate-90{transition:transform .3s ease;backface-visibility:hidden;will-change:transform}.active>.rotate-90,.collapsible:not(.collapsed)>.rotate-90,.show>.rotate-90{transform:rotateZ(90deg);transition:transform .3s ease}[direction=rtl] .active>.rotate-90,[direction=rtl] .collapsible:not(.collapsed)>.rotate-90,[direction=rtl] .show>.rotate-90{transform:rotateZ(-90deg)}.rotate-n90{transition:transform .3s ease;backface-visibility:hidden;will-change:transform}.active>.rotate-n90,.collapsible:not(.collapsed)>.rotate-n90,.show>.rotate-n90{transform:rotateZ(-90deg);transition:transform .3s ease}[direction=rtl] .active>.rotate-n90,[direction=rtl] .collapsible:not(.collapsed)>.rotate-n90,[direction=rtl] .show>.rotate-n90{transform:rotateZ(90deg)}.rotate-180{transition:transform .3s ease;backface-visibility:hidden;will-change:transform}.active>.rotate-180,.collapsible:not(.collapsed)>.rotate-180,.show>.rotate-180{transform:rotateZ(180deg);transition:transform .3s ease}[direction=rtl] .active>.rotate-180,[direction=rtl] .collapsible:not(.collapsed)>.rotate-180,[direction=rtl] .show>.rotate-180{transform:rotateZ(-180deg)}.rotate-n180{transition:transform .3s ease;backface-visibility:hidden;will-change:transform}.active>.rotate-n180,.collapsible:not(.collapsed)>.rotate-n180,.show>.rotate-n180{transform:rotateZ(-180deg);transition:transform .3s ease}[direction=rtl] .active>.rotate-n180,[direction=rtl] .collapsible:not(.collapsed)>.rotate-n180,[direction=rtl] .show>.rotate-n180{transform:rotateZ(180deg)}.rotate-270{transition:transform .3s ease;backface-visibility:hidden;will-change:transform}.active>.rotate-270,.collapsible:not(.collapsed)>.rotate-270,.show>.rotate-270{transform:rotateZ(270deg);transition:transform .3s ease}[direction=rtl] .active>.rotate-270,[direction=rtl] .collapsible:not(.collapsed)>.rotate-270,[direction=rtl] .show>.rotate-270{transform:rotateZ(-270deg)}.rotate-n270{transition:transform .3s ease;backface-visibility:hidden;will-change:transform}.active>.rotate-n270,.collapsible:not(.collapsed)>.rotate-n270,.show>.rotate-n270{transform:rotateZ(-270deg);transition:transform .3s ease}[direction=rtl] .active>.rotate-n270,[direction=rtl] .collapsible:not(.collapsed)>.rotate-n270,[direction=rtl] .show>.rotate-n270{transform:rotateZ(270deg)}@media (min-width:992px){div,main,ol,pre,span,ul{scrollbar-width:thin;scrollbar-color:var(--bs-scrollbar-color) transparent}div::-webkit-scrollbar,main::-webkit-scrollbar,ol::-webkit-scrollbar,pre::-webkit-scrollbar,span::-webkit-scrollbar,ul::-webkit-scrollbar{width:var(--bs-scrollbar-size);height:var(--bs-scrollbar-size)}div ::-webkit-scrollbar-track,main ::-webkit-scrollbar-track,ol ::-webkit-scrollbar-track,pre ::-webkit-scrollbar-track,span ::-webkit-scrollbar-track,ul ::-webkit-scrollbar-track{background-color:transparent}div ::-webkit-scrollbar-thumb,main ::-webkit-scrollbar-thumb,ol ::-webkit-scrollbar-thumb,pre ::-webkit-scrollbar-thumb,span ::-webkit-scrollbar-thumb,ul ::-webkit-scrollbar-thumb{border-radius:var(--bs-scrollbar-size)}div::-webkit-scrollbar-thumb,main::-webkit-scrollbar-thumb,ol::-webkit-scrollbar-thumb,pre::-webkit-scrollbar-thumb,span::-webkit-scrollbar-thumb,ul::-webkit-scrollbar-thumb{background-color:var(--bs-scrollbar-color)}div::-webkit-scrollbar-corner,main::-webkit-scrollbar-corner,ol::-webkit-scrollbar-corner,pre::-webkit-scrollbar-corner,span::-webkit-scrollbar-corner,ul::-webkit-scrollbar-corner{background-color:transparent}div:hover,main:hover,ol:hover,pre:hover,span:hover,ul:hover{scrollbar-color:var(--bs-scrollbar-hover-color) transparent}div:hover::-webkit-scrollbar-thumb,main:hover::-webkit-scrollbar-thumb,ol:hover::-webkit-scrollbar-thumb,pre:hover::-webkit-scrollbar-thumb,span:hover::-webkit-scrollbar-thumb,ul:hover::-webkit-scrollbar-thumb{background-color:var(--bs-scrollbar-hover-color)}div:hover::-webkit-scrollbar-corner,main:hover::-webkit-scrollbar-corner,ol:hover::-webkit-scrollbar-corner,pre:hover::-webkit-scrollbar-corner,span:hover::-webkit-scrollbar-corner,ul:hover::-webkit-scrollbar-corner{background-color:transparent}}.hover-scroll,.hover-scroll-overlay,.scroll{overflow:scroll;position:relative}@media (max-width:991.98px){.hover-scroll,.hover-scroll-overlay,.scroll{overflow:auto}}.hover-scroll-overlay-x,.hover-scroll-x,.scroll-x{overflow-x:scroll;position:relative}@media (max-width:991.98px){.hover-scroll-overlay-x,.hover-scroll-x,.scroll-x{overflow-x:auto}}.hover-scroll-overlay-y,.hover-scroll-y,.scroll-y{overflow-y:scroll;position:relative}@media (max-width:991.98px){.hover-scroll-overlay-y,.hover-scroll-y,.scroll-y{overflow-y:auto}}.hover-scroll,.hover-scroll-overlay,.hover-scroll-overlay-x,.hover-scroll-overlay-y,.hover-scroll-x,.hover-scroll-y{scrollbar-color:transparent transparent}.hover-scroll-overlay-x::-webkit-scrollbar-thumb,.hover-scroll-overlay-y::-webkit-scrollbar-thumb,.hover-scroll-overlay::-webkit-scrollbar-thumb,.hover-scroll-x::-webkit-scrollbar-thumb,.hover-scroll-y::-webkit-scrollbar-thumb,.hover-scroll::-webkit-scrollbar-thumb{background-color:transparent}.hover-scroll-overlay-x::-webkit-scrollbar-corner,.hover-scroll-overlay-y::-webkit-scrollbar-corner,.hover-scroll-overlay::-webkit-scrollbar-corner,.hover-scroll-x::-webkit-scrollbar-corner,.hover-scroll-y::-webkit-scrollbar-corner,.hover-scroll::-webkit-scrollbar-corner{background-color:transparent}.hover-scroll-overlay-x:hover,.hover-scroll-overlay-y:hover,.hover-scroll-overlay:hover,.hover-scroll-x:hover,.hover-scroll-y:hover,.hover-scroll:hover{scrollbar-color:var(--bs-scrollbar-color) transparent}.hover-scroll-overlay-x:hover::-webkit-scrollbar-thumb,.hover-scroll-overlay-y:hover::-webkit-scrollbar-thumb,.hover-scroll-overlay:hover::-webkit-scrollbar-thumb,.hover-scroll-x:hover::-webkit-scrollbar-thumb,.hover-scroll-y:hover::-webkit-scrollbar-thumb,.hover-scroll:hover::-webkit-scrollbar-thumb{background-color:var(--bs-scrollbar-color)}.hover-scroll-overlay-x:hover::-webkit-scrollbar-corner,.hover-scroll-overlay-y:hover::-webkit-scrollbar-corner,.hover-scroll-overlay:hover::-webkit-scrollbar-corner,.hover-scroll-x:hover::-webkit-scrollbar-corner,.hover-scroll-y:hover::-webkit-scrollbar-corner,.hover-scroll:hover::-webkit-scrollbar-corner{background-color:transparent}@supports (-webkit-hyphens:none){.hover-scroll,.hover-scroll-overlay,.scroll{overflow:auto}.hover-scroll-overlay-x,.hover-scroll-x,.scroll-x{overflow-x:auto}.hover-scroll-overlay-y,.hover-scroll-y,.scroll-y{overflow-y:auto}}.scroll-ps{padding-left:var(--bs-scrollbar-size)!important}.scroll-ms{margin-left:var(--bs-scrollbar-size)!important}.scroll-mb{margin-bottom:var(--bs-scrollbar-size)!important}.scroll-pe{padding-right:var(--bs-scrollbar-size)!important}.scroll-me{margin-right:var(--bs-scrollbar-size)!important}.scroll-px{padding-left:var(--bs-scrollbar-size)!important;padding-right:var(--bs-scrollbar-size)!important}.scroll-mx{margin-left:var(--bs-scrollbar-size)!important;margin-right:var(--bs-scrollbar-size)!important}@media (min-width:576px){.scroll-sm-ps{padding-left:var(--bs-scrollbar-size)!important}.scroll-sm-ms{margin-left:var(--bs-scrollbar-size)!important}.scroll-sm-mb{margin-bottom:var(--bs-scrollbar-size)!important}.scroll-sm-pe{padding-right:var(--bs-scrollbar-size)!important}.scroll-sm-me{margin-right:var(--bs-scrollbar-size)!important}.scroll-sm-px{padding-left:var(--bs-scrollbar-size)!important;padding-right:var(--bs-scrollbar-size)!important}.scroll-sm-mx{margin-left:var(--bs-scrollbar-size)!important;margin-right:var(--bs-scrollbar-size)!important}}@media (min-width:768px){.scroll-md-ps{padding-left:var(--bs-scrollbar-size)!important}.scroll-md-ms{margin-left:var(--bs-scrollbar-size)!important}.scroll-md-mb{margin-bottom:var(--bs-scrollbar-size)!important}.scroll-md-pe{padding-right:var(--bs-scrollbar-size)!important}.scroll-md-me{margin-right:var(--bs-scrollbar-size)!important}.scroll-md-px{padding-left:var(--bs-scrollbar-size)!important;padding-right:var(--bs-scrollbar-size)!important}.scroll-md-mx{margin-left:var(--bs-scrollbar-size)!important;margin-right:var(--bs-scrollbar-size)!important}}@media (min-width:992px){.scroll-lg-ps{padding-left:var(--bs-scrollbar-size)!important}.scroll-lg-ms{margin-left:var(--bs-scrollbar-size)!important}.scroll-lg-mb{margin-bottom:var(--bs-scrollbar-size)!important}.scroll-lg-pe{padding-right:var(--bs-scrollbar-size)!important}.scroll-lg-me{margin-right:var(--bs-scrollbar-size)!important}.scroll-lg-px{padding-left:var(--bs-scrollbar-size)!important;padding-right:var(--bs-scrollbar-size)!important}.scroll-lg-mx{margin-left:var(--bs-scrollbar-size)!important;margin-right:var(--bs-scrollbar-size)!important}}@media (min-width:1200px){.scroll-xl-ps{padding-left:var(--bs-scrollbar-size)!important}.scroll-xl-ms{margin-left:var(--bs-scrollbar-size)!important}.scroll-xl-mb{margin-bottom:var(--bs-scrollbar-size)!important}.scroll-xl-pe{padding-right:var(--bs-scrollbar-size)!important}.scroll-xl-me{margin-right:var(--bs-scrollbar-size)!important}.scroll-xl-px{padding-left:var(--bs-scrollbar-size)!important;padding-right:var(--bs-scrollbar-size)!important}.scroll-xl-mx{margin-left:var(--bs-scrollbar-size)!important;margin-right:var(--bs-scrollbar-size)!important}}@media (min-width:1400px){.scroll-xxl-ps{padding-left:var(--bs-scrollbar-size)!important}.scroll-xxl-ms{margin-left:var(--bs-scrollbar-size)!important}.scroll-xxl-mb{margin-bottom:var(--bs-scrollbar-size)!important}.scroll-xxl-pe{padding-right:var(--bs-scrollbar-size)!important}.scroll-xxl-me{margin-right:var(--bs-scrollbar-size)!important}.scroll-xxl-px{padding-left:var(--bs-scrollbar-size)!important;padding-right:var(--bs-scrollbar-size)!important}.scroll-xxl-mx{margin-left:var(--bs-scrollbar-size)!important;margin-right:var(--bs-scrollbar-size)!important}}.rating{display:flex;align-items:center}.rating-input{position:absolute!important;left:-9999px!important}.rating-input[disabled]{display:none}.rating-label{padding:0;margin:0}.rating-label>.svg-icon,.rating-label>i{line-height:1;color:var(--bs-rating-color-default)}label.rating-label{cursor:pointer}div.rating-label.checked>.svg-icon,div.rating-label.checked>i,label.rating-label>.svg-icon,label.rating-label>i{color:var(--bs-rating-color-active)}.rating-input:checked~.rating-label>.svg-icon,.rating-input:checked~.rating-label>i{color:var(--bs-rating-color-default)}.rating:hover label.rating-label>.svg-icon,.rating:hover label.rating-label>i{color:var(--bs-rating-color-active)}label.rating-label:hover~.rating-label{color:var(--bs-rating-color-default)}label.rating-label:hover~.rating-label>.svg-icon,label.rating-label:hover~.rating-label>i{color:var(--bs-rating-color-default)}.stepper [data-kt-stepper-element=content],.stepper [data-kt-stepper-element=info]{display:none}.stepper [data-kt-stepper-element=content].current,.stepper [data-kt-stepper-element=info].current{display:flex}.stepper .stepper-item[data-kt-stepper-action=step]{cursor:pointer}.stepper [data-kt-stepper-action=final]{display:none}.stepper [data-kt-stepper-action=previous]{display:none}.stepper [data-kt-stepper-action=next]{display:inline-block}.stepper [data-kt-stepper-action=submit]{display:none}.stepper.first [data-kt-stepper-action=previous]{display:none}.stepper.first [data-kt-stepper-action=next]{display:inline-block}.stepper.first [data-kt-stepper-action=submit]{display:none}.stepper.between [data-kt-stepper-action=previous]{display:inline-block}.stepper.between [data-kt-stepper-action=next]{display:inline-block}.stepper.between [data-kt-stepper-action=submit]{display:none}.stepper.last [data-kt-stepper-action=final]{display:inline-block}.stepper.last [data-kt-stepper-action=final].btn-flex{display:flex}.stepper.last [data-kt-stepper-action=previous]{display:inline-block}.stepper.last [data-kt-stepper-action=previous].btn-flex{display:flex}.stepper.last [data-kt-stepper-action=previous][data-kt-stepper-state=hide-on-last-step]{display:none!important}.stepper.last [data-kt-stepper-action=next]{display:none}.stepper.last [data-kt-stepper-action=next].btn-flex{display:flex}.stepper.last [data-kt-stepper-action=submit]{display:inline-block}.stepper.last [data-kt-stepper-action=submit].btn-flex{display:flex}.stepper.stepper-pills{--bs-stepper-pills-size:40px;--bs-stepper-icon-border-radius:9px;--bs-stepper-icon-check-size:1rem;--bs-stepper-icon-bg-color:var(--bs-primary-light);--bs-stepper-icon-bg-color-current:var(--bs-primary);--bs-stepper-icon-bg-color-completed:var(--bs-primary-light);--bs-stepper-icon-border:0;--bs-stepper-icon-border-current:0;--bs-stepper-icon-border-completed:0;--bs-stepper-icon-number-color:var(--bs-primary);--bs-stepper-icon-number-color-current:var(--bs-white);--bs-stepper-icon-check-color-completed:var(--bs-primary);--bs-stepper-label-title-opacity:1;--bs-stepper-label-title-opacity-current:1;--bs-stepper-label-title-opacity-completed:1;--bs-stepper-label-title-color:var(--bs-gray-800);--bs-stepper-label-title-color-current:var(--bs-gray-600);--bs-stepper-label-title-color-completed:var(--bs-text-muted);--bs-stepper-label-desc-opacity:1;--bs-stepper-label-desc-opacity-current:1;--bs-stepper-label-desc-opacity-completed:1;--bs-stepper-label-desc-color:var(--bs-text-muted);--bs-stepper-label-desc-color-current:var(--bs-gray-500);--bs-stepper-label-desc-color-completed:var(--bs-gray-500);--bs-stepper-line-border:1px dashed var(--bs-gray-300)}.stepper.stepper-pills .stepper-nav{display:flex}.stepper.stepper-pills .stepper-item{display:flex;align-items:center;transition:color .2s ease}.stepper.stepper-pills .stepper-item .stepper-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s ease;width:var(--bs-stepper-pills-size);height:var(--bs-stepper-pills-size);border-radius:var(--bs-stepper-icon-border-radius);background-color:var(--bs-stepper-icon-bg-color);border:var(--bs-stepper-icon-border);margin-right:1.5rem}.stepper.stepper-pills .stepper-item .stepper-icon .stepper-check{display:none;font-size:var(--bs-stepper-icon-check-size)}.stepper.stepper-pills .stepper-item .stepper-icon .stepper-number{font-weight:600;color:var(--bs-stepper-icon-number-color);font-size:1.25rem}.stepper.stepper-pills .stepper-item .stepper-label{display:flex;flex-direction:column;justify-content:center}.stepper.stepper-pills .stepper-item .stepper-label .stepper-title{color:var(--bs-stepper-label-title-color);opacity:var(--bs-stepper-label-title-opacity);font-weight:600;font-size:1.25rem;margin-bottom:.3rem}.stepper.stepper-pills .stepper-item .stepper-label .stepper-desc{opacity:var(--bs-stepper-label-desc-opacity);color:var(--bs-stepper-label-desc-color)}.stepper.stepper-pills .stepper-item.current{transition:color .2s ease}.stepper.stepper-pills .stepper-item.current .stepper-icon{transition:color .2s ease;background-color:var(--bs-stepper-icon-bg-color-current);border:var(--bs-stepper-icon-border-current)}.stepper.stepper-pills .stepper-item.current .stepper-icon .stepper-check{display:none}.stepper.stepper-pills .stepper-item.current .stepper-icon .stepper-number{color:var(--bs-stepper-icon-number-color-current);font-size:1.35rem}.stepper.stepper-pills .stepper-item.current .stepper-label .stepper-title{opacity:var(--bs-stepper-label-title-opacity-current);color:var(--bs-stepper-label-title-color-current)}.stepper.stepper-pills .stepper-item.current .stepper-label .stepper-desc{opacity:var(--bs-stepper-label-desc-opacity-current);color:var(--bs-stepper-label-desc-color-current)}.stepper.stepper-pills .stepper-item.completed .stepper-icon,.stepper.stepper-pills .stepper-item.current.mark-completed:last-child .stepper-icon{transition:color .2s ease;background-color:var(--bs-stepper-icon-bg-color-completed);border:var(--bs-stepper-icon-border-completed)}.stepper.stepper-pills .stepper-item.completed .stepper-icon .stepper-check,.stepper.stepper-pills .stepper-item.current.mark-completed:last-child .stepper-icon .stepper-check{color:var(--bs-stepper-icon-check-color-completed);display:inline-block}.stepper.stepper-pills .stepper-item.completed .stepper-icon .stepper-number,.stepper.stepper-pills .stepper-item.current.mark-completed:last-child .stepper-icon .stepper-number{display:none}.stepper.stepper-pills .stepper-item.completed .stepper-label .stepper-title,.stepper.stepper-pills .stepper-item.current.mark-completed:last-child .stepper-label .stepper-title{opacity:var(--bs-stepper-label-title-opacity-completed);color:var(--bs-stepper-label-title-color-completed)}.stepper.stepper-pills .stepper-item.completed .stepper-label .stepper-desc,.stepper.stepper-pills .stepper-item.current.mark-completed:last-child .stepper-label .stepper-desc{opacity:var(--bs-stepper-label-desc-opacity-completed);color:var(--bs-stepper-label-desc-color-completed)}.stepper.stepper-pills.stepper-column .stepper-nav{flex-direction:column;align-items:start}.stepper.stepper-pills.stepper-column .stepper-item{flex-direction:column;justify-content:start;align-items:stretch;padding:0;margin:0}.stepper.stepper-pills.stepper-column .stepper-wrapper{display:flex;align-items:center}.stepper.stepper-pills.stepper-column .stepper-icon{z-index:1}.stepper.stepper-pills.stepper-column .stepper-line{display:block;flex-grow:1;margin-left:calc(var(--bs-stepper-pills-size)/ 2);border-left:var(--bs-stepper-line-border);margin-top:2px;margin-bottom:2px}.stepper.stepper-links .stepper-nav{display:flex;margin:0 auto;justify-content:center;align-items:center;flex-wrap:wrap}.stepper.stepper-links .stepper-nav .stepper-item{position:relative;flex-shrink:0;margin:1rem 1.5rem}.stepper.stepper-links .stepper-nav .stepper-item:after{content:\" \";position:absolute;top:2.3rem;left:0;height:2px;width:100%;background-color:transparent;transition:color .2s ease}.stepper.stepper-links .stepper-nav .stepper-item .stepper-title{color:var(--bs-gray-900);font-weight:600;font-size:1.25rem}.stepper.stepper-links .stepper-nav .stepper-item.current{transition:color .2s ease}.stepper.stepper-links .stepper-nav .stepper-item.current .stepper-title{color:var(--bs-primary)}.stepper.stepper-links .stepper-nav .stepper-item.current:after{background-color:var(--bs-primary)}.stepper.stepper-links .stepper-nav .stepper-item.completed .stepper-title,.stepper.stepper-links .stepper-nav .stepper-item.current.mark-completed:last-child .stepper-title{color:var(--bs-gray-500)}.toggle.active .toggle-off,.toggle.collapsible:not(.collapsed) .toggle-off{display:none}.toggle.collapsible.collapsed .toggle-on,.toggle:not(.collapsible):not(.active) .toggle-on{display:none}.xehagon{clip-path:polygon(45% 1.3397459622%,46.5797985667% .6030737921%,48.2635182233% .1519224699%,50% 0,51.7364817767% .1519224699%,53.4202014333% .6030737921%,55% 1.3397459622%,89.6410161514% 21.3397459622%,91.0688922482% 22.3395555688%,92.3014605826% 23.5721239031%,93.3012701892% 25%,94.0379423592% 26.5797985667%,94.4890936815% 28.2635182233%,94.6410161514% 30%,94.6410161514% 70%,94.4890936815% 71.7364817767%,94.0379423592% 73.4202014333%,93.3012701892% 75%,92.3014605826% 76.4278760969%,91.0688922482% 77.6604444312%,89.6410161514% 78.6602540378%,55% 98.6602540378%,53.4202014333% 99.3969262079%,51.7364817767% 99.8480775301%,50% 100%,48.2635182233% 99.8480775301%,46.5797985667% 99.3969262079%,45% 98.6602540378%,10.3589838486% 78.6602540378%,8.9311077518% 77.6604444312%,7.6985394174% 76.4278760969%,6.6987298108% 75%,5.9620576408% 73.4202014333%,5.5109063185% 71.7364817767%,5.3589838486% 70%,5.3589838486% 30%,5.5109063185% 28.2635182233%,5.9620576408% 26.5797985667%,6.6987298108% 25%,7.6985394174% 23.5721239031%,8.9311077518% 22.3395555688%,10.3589838486% 21.3397459622%)}.octagon{clip-path:polygon(46.1731656763% .7612046749%,47.411809549% .3407417371%,48.6947380778% .0855513863%,50% 0,51.3052619222% .0855513863%,52.588190451% .3407417371%,53.8268343237% .7612046749%,82.1111055711% 12.4769334274%,83.2842712475% 13.0554747147%,84.3718855375% 13.7821953496%,85.3553390593% 14.6446609407%,86.2178046504% 15.6281144625%,86.9445252853% 16.7157287525%,87.5230665726% 17.8888944289%,99.2387953251% 46.1731656763%,99.6592582629% 47.411809549%,99.9144486137% 48.6947380778%,100% 50%,99.9144486137% 51.3052619222%,99.6592582629% 52.588190451%,99.2387953251% 53.8268343237%,87.5230665726% 82.1111055711%,86.9445252853% 83.2842712475%,86.2178046504% 84.3718855375%,85.3553390593% 85.3553390593%,84.3718855375% 86.2178046504%,83.2842712475% 86.9445252853%,82.1111055711% 87.5230665726%,53.8268343237% 99.2387953251%,52.588190451% 99.6592582629%,51.3052619222% 99.9144486137%,50% 100%,48.6947380778% 99.9144486137%,47.411809549% 99.6592582629%,46.1731656763% 99.2387953251%,17.8888944289% 87.5230665726%,16.7157287525% 86.9445252853%,15.6281144625% 86.2178046504%,14.6446609407% 85.3553390593%,13.7821953496% 84.3718855375%,13.0554747147% 83.2842712475%,12.4769334274% 82.1111055711%,.7612046749% 53.8268343237%,.3407417371% 52.588190451%,.0855513863% 51.3052619222%,0 50%,.0855513863% 48.6947380778%,.3407417371% 47.411809549%,.7612046749% 46.1731656763%,12.4769334274% 17.8888944289%,13.0554747147% 16.7157287525%,13.7821953496% 15.6281144625%,14.6446609407% 14.6446609407%,15.6281144625% 13.7821953496%,16.7157287525% 13.0554747147%,17.8888944289% 12.4769334274%)}.ribbon{position:relative}.ribbon .ribbon-label{display:flex;justify-content:center;align-items:center;padding:5px 10px;position:absolute;z-index:1;background-color:var(--bs-ribbon-label-bg);box-shadow:var(--bs-ribbon-label-box-shadow);color:var(--bs-primary-inverse);top:50%;right:0;transform:translateX(5px) translateY(-50%)}.ribbon .ribbon-label>.ribbon-inner{z-index:-1;position:absolute;padding:0;width:100%;height:100%;top:0;left:0}.ribbon .ribbon-label:after{border-color:var(--bs-ribbon-label-border-color)}.ribbon-vertical .ribbon-label{padding:5px 10px;min-width:36px;min-height:46px;text-align:center}.ribbon.ribbon-top .ribbon-label{top:0;transform:translateX(-15px) translateY(-4px);border-bottom-right-radius:.475rem;border-bottom-left-radius:.475rem}.ribbon.ribbon-bottom .ribbon-label{border-top-right-radius:.475rem;border-top-left-radius:.475rem}.ribbon.ribbon-start .ribbon-label{top:50%;left:0;right:auto;transform:translateX(-5px) translateY(-50%);border-top-right-radius:.475rem;border-bottom-right-radius:.475rem}.ribbon.ribbon-end .ribbon-label{border-top-left-radius:.475rem;border-bottom-left-radius:.475rem}.ribbon.ribbon-clip.ribbon-start .ribbon-label{left:-5px}.ribbon.ribbon-clip.ribbon-start .ribbon-label .ribbon-inner{border-top-right-radius:.475rem;border-bottom-right-radius:.475rem}.ribbon.ribbon-clip.ribbon-start .ribbon-label .ribbon-inner:after,.ribbon.ribbon-clip.ribbon-start .ribbon-label .ribbon-inner:before{content:\"\";position:absolute;border-style:solid;border-color:transparent!important;bottom:-10px}.ribbon.ribbon-clip.ribbon-start .ribbon-label .ribbon-inner:before{border-width:0 10px 10px 0;border-right-color:var(--bs-ribbon-clip-bg)!important;left:0}.ribbon.ribbon-clip.ribbon-end .ribbon-label{right:-5px}.ribbon.ribbon-clip.ribbon-end .ribbon-label .ribbon-inner{border-top-left-radius:.475rem;border-bottom-left-radius:.475rem}.ribbon.ribbon-clip.ribbon-end .ribbon-label .ribbon-inner:after,.ribbon.ribbon-clip.ribbon-end .ribbon-label .ribbon-inner:before{content:\"\";position:absolute;border-style:solid;border-color:transparent!important;bottom:-10px}.ribbon.ribbon-clip.ribbon-end .ribbon-label .ribbon-inner:before{border-width:0 0 10px 10px;border-left-color:var(--bs-ribbon-clip-bg)!important;right:0}.ribbon.ribbon-triangle{position:absolute;z-index:1;display:flex;align-items:flex-start;justify-content:flex-start}.ribbon.ribbon-triangle.ribbon-top-start{top:0;left:0;width:4rem;height:4rem;border-bottom:solid 2rem transparent!important;border-left:solid 2rem transparent;border-right:solid 2rem transparent!important;border-top:solid 2rem transparent}.ribbon.ribbon-triangle.ribbon-top-end{top:0;right:0;width:4rem;height:4rem;border-bottom:solid 2rem transparent!important;border-left:solid 2rem transparent!important;border-right:solid 2rem transparent;border-top:solid 2rem transparent}.ribbon.ribbon-triangle.ribbon-bottom-start{bottom:0;left:0;width:4rem;height:4rem;border-bottom:solid 2rem transparent;border-left:solid 2rem transparent;border-right:solid 2rem transparent!important;border-top:solid 2rem transparent!important}.ribbon.ribbon-triangle.ribbon-bottom-end{bottom:0;right:0;width:4rem;height:4rem;border-bottom:solid 2rem transparent;border-right:solid 2rem transparent;border-left:solid 2rem transparent!important;border-top:solid 2rem transparent!important}.blockui{position:relative}.blockui .blockui-overlay{transition:all .3s ease;position:absolute;top:0;bottom:0;left:0;right:0;display:flex;justify-content:center;align-items:center;background-color:var(--bs-blockui-overlay-bg)}.blockui .blockui-overlay .spinner-border{height:1.35rem;width:1.35rem}.blockui .blockui-message{display:flex;align-items:center;border-radius:.475rem;box-shadow:var(--bs-blockui-message-box-shadow);background-color:var(--bs-blockui-message-bg);color:var(--bs-gray-700);font-weight:600;margin:0!important;width:auto;padding:.85rem 1.75rem!important}.blockui .blockui-message .spinner-border{margin-right:.65rem}[data-bs-theme=dark] .blockui{--bs-blockui-overlay-bg:rgba(255, 255, 255, 0.05);--bs-blockui-message-bg:#26272F;--bs-blockui-message-box-shadow:0px 0px 30px rgba(0, 0, 0, 0.15)}.explore-btn-toggle{color:var(--bs-gray-600);background-color:#fff}.explore-btn-toggle:active,.explore-btn-toggle:focus,.explore-btn-toggle:hover{color:#fff;background-color:#00b2ff}.explore-btn-dismiss{border:0}.explore-btn-dismiss:hover .svg-icon,.explore-btn-dismiss:hover i{color:#00b2ff}.explore-btn-primary{border:0;color:#fff;background-color:#00b2ff}.explore-btn-primary:hover{color:#fff;background-color:#0098da}.explore-btn-secondary{border:0;color:var(--bs-gray-600);background-color:var(--bs-gray-100)}.explore-btn-secondary:hover{color:var(--bs-gray-800);background-color:var(--bs-gray-200)}.explore-btn-outline{border:1px dashed var(--bs-gray-300)!important}.explore-btn-outline.active,.explore-btn-outline:hover{border:1px dashed #50cd89!important;background-color:#e8fff3}.explore-link{color:#00b2ff}.explore-link:hover{color:#0098da}.explore-link-hover:hover{color:#00b2ff!important}.explore-icon-success{color:#50cd89}.explore-icon-danger{color:#f1416c}.explore-label-free{color:#fff;background-color:#ffc700}.explore-label-pro{color:#fff;background-color:#50cd89}.app-engage{position:fixed;right:0;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;align-items:flex-end;z-index:5;padding-right:12px}.app-engage .app-engage-btn{display:flex;align-items:center;justify-content:center;flex-direction:column;box-shadow:var(--bs-engage-btn-box-shadow);border-width:1px;border-style:solid;font-size:12px;font-weight:600;margin-bottom:8px;border-radius:6px;width:66px;height:70px;color:var(--bs-engage-btn-color);border-color:var(--bs-engage-btn-border-color);background-color:var(--bs-engage-btn-bg)}.app-engage .app-engage-btn .svg-icon,.app-engage .app-engage-btn i{color:var(--bs-engage-btn-icon-color)}.app-engage .app-engage-btn.dropdown-toggle:after{color:var(--bs-engage-btn-icon-color)}.app-engage .app-engage-btn.hover-dark:hover{color:var(--bs-dark-inverse);border-color:var(--bs-dark);background-color:var(--bs-dark)}.app-engage .app-engage-btn.hover-dark:hover .svg-icon,.app-engage .app-engage-btn.hover-dark:hover i{color:var(--bs-dark-inverse)}.app-engage .app-engage-btn.hover-primary:hover{color:var(--bs-primary-inverse);border-color:var(--bs-primary);background-color:var(--bs-primary)}.app-engage .app-engage-btn.hover-primary:hover .svg-icon,.app-engage .app-engage-btn.hover-primary:hover i{color:var(--bs-primary-inverse)}.app-engage .app-engage-btn.hover-success:hover{color:var(--bs-success-inverse);border-color:var(--bs-success);background-color:var(--bs-success)}.app-engage .app-engage-btn.hover-success:hover .svg-icon,.app-engage .app-engage-btn.hover-success:hover i{color:var(--bs-success-inverse)}.app-engage .app-engage-btn.app-engage-btn-toggle-off{width:35px;height:35px}.app-engage .app-engage-btn.app-engage-btn-toggle-on{width:35px;height:35px;display:none}.app-engage.app-engage-hide .app-engage-btn{visibility:hidden}.app-engage.app-engage-hide .app-engage-btn.app-engage-btn-toggle-off{display:none}.app-engage.app-engage-hide .app-engage-btn.app-engage-btn-toggle-on{visibility:visible;display:flex}.engage-btn{display:flex;align-items:center;height:35px!important;color:var(--bs-engage-btn-color);border-color:var(--bs-engage-btn-bg);background-color:var(--bs-engage-btn-bg)}.engage-btn .svg-icon,.engage-btn i{color:var(--bs-engage-btn-color)}.engage-btn.dropdown-toggle:after{color:var(--bs-engage-btn-color)}.btn-check:active+.engage-btn,.btn-check:checked+.engage-btn,.engage-btn.active,.engage-btn.show,.engage-btn:active:not(.btn-active),.engage-btn:focus:not(.btn-active),.engage-btn:hover:not(.btn-active),.show>.engage-btn{color:var(--bs-engage-btn-color-active);border-color:var(--bs-engage-btn-bg);background-color:var(--bs-engage-btn-bg)!important}.btn-check:active+.engage-btn .svg-icon,.btn-check:active+.engage-btn i,.btn-check:checked+.engage-btn .svg-icon,.btn-check:checked+.engage-btn i,.engage-btn.active .svg-icon,.engage-btn.active i,.engage-btn.show .svg-icon,.engage-btn.show i,.engage-btn:active:not(.btn-active) .svg-icon,.engage-btn:active:not(.btn-active) i,.engage-btn:focus:not(.btn-active) .svg-icon,.engage-btn:focus:not(.btn-active) i,.engage-btn:hover:not(.btn-active) .svg-icon,.engage-btn:hover:not(.btn-active) i,.show>.engage-btn .svg-icon,.show>.engage-btn i{color:var(--bs-engage-btn-color-active)}.btn-check:active+.engage-btn.dropdown-toggle:after,.btn-check:checked+.engage-btn.dropdown-toggle:after,.engage-btn.active.dropdown-toggle:after,.engage-btn.show.dropdown-toggle:after,.engage-btn:active:not(.btn-active).dropdown-toggle:after,.engage-btn:focus:not(.btn-active).dropdown-toggle:after,.engage-btn:hover:not(.btn-active).dropdown-toggle:after,.show>.engage-btn.dropdown-toggle:after{color:var(--bs-engage-btn-color-active)}.cookiealert{background:inherit;color:inherit}@media print{.print-content-only{padding:0!important;background:0 0!important}.print-content-only .container,.print-content-only .container-fluid,.print-content-only .container-lg,.print-content-only .container-md,.print-content-only .container-sm,.print-content-only .container-xl,.print-content-only .container-xxl,.print-content-only .page,.print-content-only .page-title .content,.print-content-only .wrapper{background:0 0!important;padding:0!important;margin:0!important}.print-content-only .aside,.print-content-only .btn,.print-content-only .drawer,.print-content-only .footer,.print-content-only .header,.print-content-only .scrolltop,.print-content-only .sidebar,.print-content-only .toolbar{display:none!important}}.bg-white{--bs-bg-rgb-color:var(--bs-white-bg-rgb);background-color:#fff!important}.bg-black{--bs-bg-rgb-color:var(--bs-black-bg-rgb);background-color:#000!important}.bg-body{--bs-bg-rgb-color:var(--bs-body-bg-rgb);background-color:var(--bs-body-bg)!important}.bg-hover-body{cursor:pointer}.bg-hover-body:hover{background-color:var(--bs-body-bg)!important}.bg-active-body.active{background-color:var(--bs-body-bg)!important}.bg-state-body{cursor:pointer}.bg-state-body.active,.bg-state-body:hover{background-color:var(--bs-body-bg)!important}.bg-hover-white{cursor:pointer}.bg-hover-white:hover{--bs-bg-rgb-color:var(--bs-white-bg-rgb);background-color:#fff!important}.bg-active-white.active{--bs-bg-rgb-color:var(--bs-white-bg-rgb);background-color:#fff!important}.bg-state-white{cursor:pointer}.bg-state-white.active,.bg-state-white:hover{--bs-bg-rgb-color:var(--bs-white-bg-rgb);background-color:#fff!important}.bg-light-light{background-color:var(--bs-light-light)!important}.bg-light{--bs-bg-rgb-color:var(--bs-light-rgb);background-color:var(--bs-light)!important}.bg-light-active{--bs-bg-rgb-color:var(--bs-light-active-rgb);background-color:var(--bs-light-active)!important}.bg-hover-light-light{cursor:pointer}.bg-hover-light-light:hover{background-color:var(--bs-light-light)!important}.bg-state-light-light{cursor:pointer}.bg-state-light-light.active,.bg-state-light-light:hover{background-color:var(--bs-light-light)!important}.bg-hover-light{cursor:pointer}.bg-hover-light:hover{background-color:var(--bs-light)!important}.bg-active-light.active{background-color:var(--bs-light)!important}.bg-state-light{cursor:pointer}.bg-state-light.active,.bg-state-light:hover{background-color:var(--bs-light)!important}.bg-light-primary{background-color:var(--bs-primary-light)!important}.bg-primary{--bs-bg-rgb-color:var(--bs-primary-rgb);background-color:var(--bs-primary)!important}.bg-primary-active{--bs-bg-rgb-color:var(--bs-primary-active-rgb);background-color:var(--bs-primary-active)!important}.bg-hover-light-primary{cursor:pointer}.bg-hover-light-primary:hover{background-color:var(--bs-primary-light)!important}.bg-state-light-primary{cursor:pointer}.bg-state-light-primary.active,.bg-state-light-primary:hover{background-color:var(--bs-primary-light)!important}.bg-hover-primary{cursor:pointer}.bg-hover-primary:hover{background-color:var(--bs-primary)!important}.bg-active-primary.active{background-color:var(--bs-primary)!important}.bg-state-primary{cursor:pointer}.bg-state-primary.active,.bg-state-primary:hover{background-color:var(--bs-primary)!important}.bg-light-secondary{background-color:var(--bs-secondary-light)!important}.bg-secondary{--bs-bg-rgb-color:var(--bs-secondary-rgb);background-color:var(--bs-secondary)!important}.bg-secondary-active{--bs-bg-rgb-color:var(--bs-secondary-active-rgb);background-color:var(--bs-secondary-active)!important}.bg-hover-light-secondary{cursor:pointer}.bg-hover-light-secondary:hover{background-color:var(--bs-secondary-light)!important}.bg-state-light-secondary{cursor:pointer}.bg-state-light-secondary.active,.bg-state-light-secondary:hover{background-color:var(--bs-secondary-light)!important}.bg-hover-secondary{cursor:pointer}.bg-hover-secondary:hover{background-color:var(--bs-secondary)!important}.bg-active-secondary.active{background-color:var(--bs-secondary)!important}.bg-state-secondary{cursor:pointer}.bg-state-secondary.active,.bg-state-secondary:hover{background-color:var(--bs-secondary)!important}.bg-light-success{background-color:var(--bs-success-light)!important}.bg-success{--bs-bg-rgb-color:var(--bs-success-rgb);background-color:var(--bs-success)!important}.bg-success-active{--bs-bg-rgb-color:var(--bs-success-active-rgb);background-color:var(--bs-success-active)!important}.bg-hover-light-success{cursor:pointer}.bg-hover-light-success:hover{background-color:var(--bs-success-light)!important}.bg-state-light-success{cursor:pointer}.bg-state-light-success.active,.bg-state-light-success:hover{background-color:var(--bs-success-light)!important}.bg-hover-success{cursor:pointer}.bg-hover-success:hover{background-color:var(--bs-success)!important}.bg-active-success.active{background-color:var(--bs-success)!important}.bg-state-success{cursor:pointer}.bg-state-success.active,.bg-state-success:hover{background-color:var(--bs-success)!important}.bg-light-info{background-color:var(--bs-info-light)!important}.bg-info{--bs-bg-rgb-color:var(--bs-info-rgb);background-color:var(--bs-info)!important}.bg-info-active{--bs-bg-rgb-color:var(--bs-info-active-rgb);background-color:var(--bs-info-active)!important}.bg-hover-light-info{cursor:pointer}.bg-hover-light-info:hover{background-color:var(--bs-info-light)!important}.bg-state-light-info{cursor:pointer}.bg-state-light-info.active,.bg-state-light-info:hover{background-color:var(--bs-info-light)!important}.bg-hover-info{cursor:pointer}.bg-hover-info:hover{background-color:var(--bs-info)!important}.bg-active-info.active{background-color:var(--bs-info)!important}.bg-state-info{cursor:pointer}.bg-state-info.active,.bg-state-info:hover{background-color:var(--bs-info)!important}.bg-light-warning{background-color:var(--bs-warning-light)!important}.bg-warning{--bs-bg-rgb-color:var(--bs-warning-rgb);background-color:var(--bs-warning)!important}.bg-warning-active{--bs-bg-rgb-color:var(--bs-warning-active-rgb);background-color:var(--bs-warning-active)!important}.bg-hover-light-warning{cursor:pointer}.bg-hover-light-warning:hover{background-color:var(--bs-warning-light)!important}.bg-state-light-warning{cursor:pointer}.bg-state-light-warning.active,.bg-state-light-warning:hover{background-color:var(--bs-warning-light)!important}.bg-hover-warning{cursor:pointer}.bg-hover-warning:hover{background-color:var(--bs-warning)!important}.bg-active-warning.active{background-color:var(--bs-warning)!important}.bg-state-warning{cursor:pointer}.bg-state-warning.active,.bg-state-warning:hover{background-color:var(--bs-warning)!important}.bg-light-danger{background-color:var(--bs-danger-light)!important}.bg-danger{--bs-bg-rgb-color:var(--bs-danger-rgb);background-color:var(--bs-danger)!important}.bg-danger-active{--bs-bg-rgb-color:var(--bs-danger-active-rgb);background-color:var(--bs-danger-active)!important}.bg-hover-light-danger{cursor:pointer}.bg-hover-light-danger:hover{background-color:var(--bs-danger-light)!important}.bg-state-light-danger{cursor:pointer}.bg-state-light-danger.active,.bg-state-light-danger:hover{background-color:var(--bs-danger-light)!important}.bg-hover-danger{cursor:pointer}.bg-hover-danger:hover{background-color:var(--bs-danger)!important}.bg-active-danger.active{background-color:var(--bs-danger)!important}.bg-state-danger{cursor:pointer}.bg-state-danger.active,.bg-state-danger:hover{background-color:var(--bs-danger)!important}.bg-light-dark{background-color:var(--bs-dark-light)!important}.bg-dark{--bs-bg-rgb-color:var(--bs-dark-rgb);background-color:var(--bs-dark)!important}.bg-dark-active{--bs-bg-rgb-color:var(--bs-dark-active-rgb);background-color:var(--bs-dark-active)!important}.bg-hover-light-dark{cursor:pointer}.bg-hover-light-dark:hover{background-color:var(--bs-dark-light)!important}.bg-state-light-dark{cursor:pointer}.bg-state-light-dark.active,.bg-state-light-dark:hover{background-color:var(--bs-dark-light)!important}.bg-hover-dark{cursor:pointer}.bg-hover-dark:hover{background-color:var(--bs-dark)!important}.bg-active-dark.active{background-color:var(--bs-dark)!important}.bg-state-dark{cursor:pointer}.bg-state-dark.active,.bg-state-dark:hover{background-color:var(--bs-dark)!important}.bg-gray-100{--bs-bg-rgb-color:var(--bs-gray-100-rgb);background-color:var(--bs-gray-100)}.bg-hover-gray-100:hover{--bs-bg-rgb-color:var(--bs-gray-100-rgb);background-color:var(--bs-gray-100)}.bg-gray-100i{--bs-bg-rgb-color:var(--bs-gray-100-rgb);background-color:var(--bs-gray-100)!important}.bg-gray-200{--bs-bg-rgb-color:var(--bs-gray-200-rgb);background-color:var(--bs-gray-200)}.bg-hover-gray-200:hover{--bs-bg-rgb-color:var(--bs-gray-200-rgb);background-color:var(--bs-gray-200)}.bg-gray-200i{--bs-bg-rgb-color:var(--bs-gray-200-rgb);background-color:var(--bs-gray-200)!important}.bg-gray-300{--bs-bg-rgb-color:var(--bs-gray-300-rgb);background-color:var(--bs-gray-300)}.bg-hover-gray-300:hover{--bs-bg-rgb-color:var(--bs-gray-300-rgb);background-color:var(--bs-gray-300)}.bg-gray-300i{--bs-bg-rgb-color:var(--bs-gray-300-rgb);background-color:var(--bs-gray-300)!important}.bg-gray-400{--bs-bg-rgb-color:var(--bs-gray-400-rgb);background-color:var(--bs-gray-400)}.bg-hover-gray-400:hover{--bs-bg-rgb-color:var(--bs-gray-400-rgb);background-color:var(--bs-gray-400)}.bg-gray-400i{--bs-bg-rgb-color:var(--bs-gray-400-rgb);background-color:var(--bs-gray-400)!important}.bg-gray-500{--bs-bg-rgb-color:var(--bs-gray-500-rgb);background-color:var(--bs-gray-500)}.bg-hover-gray-500:hover{--bs-bg-rgb-color:var(--bs-gray-500-rgb);background-color:var(--bs-gray-500)}.bg-gray-500i{--bs-bg-rgb-color:var(--bs-gray-500-rgb);background-color:var(--bs-gray-500)!important}.bg-gray-600{--bs-bg-rgb-color:var(--bs-gray-600-rgb);background-color:var(--bs-gray-600)}.bg-hover-gray-600:hover{--bs-bg-rgb-color:var(--bs-gray-600-rgb);background-color:var(--bs-gray-600)}.bg-gray-600i{--bs-bg-rgb-color:var(--bs-gray-600-rgb);background-color:var(--bs-gray-600)!important}.bg-gray-700{--bs-bg-rgb-color:var(--bs-gray-700-rgb);background-color:var(--bs-gray-700)}.bg-hover-gray-700:hover{--bs-bg-rgb-color:var(--bs-gray-700-rgb);background-color:var(--bs-gray-700)}.bg-gray-700i{--bs-bg-rgb-color:var(--bs-gray-700-rgb);background-color:var(--bs-gray-700)!important}.bg-gray-800{--bs-bg-rgb-color:var(--bs-gray-800-rgb);background-color:var(--bs-gray-800)}.bg-hover-gray-800:hover{--bs-bg-rgb-color:var(--bs-gray-800-rgb);background-color:var(--bs-gray-800)}.bg-gray-800i{--bs-bg-rgb-color:var(--bs-gray-800-rgb);background-color:var(--bs-gray-800)!important}.bg-gray-900{--bs-bg-rgb-color:var(--bs-gray-900-rgb);background-color:var(--bs-gray-900)}.bg-hover-gray-900:hover{--bs-bg-rgb-color:var(--bs-gray-900-rgb);background-color:var(--bs-gray-900)}.bg-gray-900i{--bs-bg-rgb-color:var(--bs-gray-900-rgb);background-color:var(--bs-gray-900)!important}.bg-opacity-0{background-color:rgba(var(--bs-bg-rgb-color),0)!important}.bg-hover-opacity-0:hover{background-color:rgba(var(--bs-bg-rgb-color),0)!important}.bg-active-opacity-0.active{background-color:rgba(var(--bs-bg-rgb-color),0)!important}.bg-state-opacity-0 .active,.bg-state-opacity-0:hover{background-color:rgba(var(--bs-bg-rgb-color),0)!important}.bg-opacity-5{background-color:rgba(var(--bs-bg-rgb-color),.05)!important}.bg-hover-opacity-5:hover{background-color:rgba(var(--bs-bg-rgb-color),.05)!important}.bg-active-opacity-5.active{background-color:rgba(var(--bs-bg-rgb-color),.05)!important}.bg-state-opacity-5 .active,.bg-state-opacity-5:hover{background-color:rgba(var(--bs-bg-rgb-color),.05)!important}.bg-opacity-10{background-color:rgba(var(--bs-bg-rgb-color),.1)!important}.bg-hover-opacity-10:hover{background-color:rgba(var(--bs-bg-rgb-color),.1)!important}.bg-active-opacity-10.active{background-color:rgba(var(--bs-bg-rgb-color),.1)!important}.bg-state-opacity-10 .active,.bg-state-opacity-10:hover{background-color:rgba(var(--bs-bg-rgb-color),.1)!important}.bg-opacity-15{background-color:rgba(var(--bs-bg-rgb-color),.15)!important}.bg-hover-opacity-15:hover{background-color:rgba(var(--bs-bg-rgb-color),.15)!important}.bg-active-opacity-15.active{background-color:rgba(var(--bs-bg-rgb-color),.15)!important}.bg-state-opacity-15 .active,.bg-state-opacity-15:hover{background-color:rgba(var(--bs-bg-rgb-color),.15)!important}.bg-opacity-20{background-color:rgba(var(--bs-bg-rgb-color),.2)!important}.bg-hover-opacity-20:hover{background-color:rgba(var(--bs-bg-rgb-color),.2)!important}.bg-active-opacity-20.active{background-color:rgba(var(--bs-bg-rgb-color),.2)!important}.bg-state-opacity-20 .active,.bg-state-opacity-20:hover{background-color:rgba(var(--bs-bg-rgb-color),.2)!important}.bg-opacity-25{background-color:rgba(var(--bs-bg-rgb-color),.25)!important}.bg-hover-opacity-25:hover{background-color:rgba(var(--bs-bg-rgb-color),.25)!important}.bg-active-opacity-25.active{background-color:rgba(var(--bs-bg-rgb-color),.25)!important}.bg-state-opacity-25 .active,.bg-state-opacity-25:hover{background-color:rgba(var(--bs-bg-rgb-color),.25)!important}.bg-opacity-50{background-color:rgba(var(--bs-bg-rgb-color),.5)!important}.bg-hover-opacity-50:hover{background-color:rgba(var(--bs-bg-rgb-color),.5)!important}.bg-active-opacity-50.active{background-color:rgba(var(--bs-bg-rgb-color),.5)!important}.bg-state-opacity-50 .active,.bg-state-opacity-50:hover{background-color:rgba(var(--bs-bg-rgb-color),.5)!important}.bg-opacity-75{background-color:rgba(var(--bs-bg-rgb-color),.75)!important}.bg-hover-opacity-75:hover{background-color:rgba(var(--bs-bg-rgb-color),.75)!important}.bg-active-opacity-75.active{background-color:rgba(var(--bs-bg-rgb-color),.75)!important}.bg-state-opacity-75 .active,.bg-state-opacity-75:hover{background-color:rgba(var(--bs-bg-rgb-color),.75)!important}.bg-opacity-100{background-color:rgba(var(--bs-bg-rgb-color),1)!important}.bg-hover-opacity-100:hover{background-color:rgba(var(--bs-bg-rgb-color),1)!important}.bg-active-opacity-100.active{background-color:rgba(var(--bs-bg-rgb-color),1)!important}.bg-state-opacity-100 .active,.bg-state-opacity-100:hover{background-color:rgba(var(--bs-bg-rgb-color),1)!important}.bgi-no-repeat{background-repeat:no-repeat}.bgi-position-y-top{background-position-y:top}.bgi-position-y-bottom{background-position-y:bottom}.bgi-position-y-center{background-position-y:center}.bgi-position-x-start{background-position-x:left}.bgi-position-x-end{background-position-x:right}.bgi-position-x-center{background-position-x:center}.bgi-position-top{background-position:0 top}.bgi-position-bottom{background-position:0 bottom}.bgi-position-center{background-position:center}.bgi-size-auto{background-size:auto}.bgi-size-cover{background-size:cover}.bgi-size-contain{background-size:contain}.bgi-attachment-fixed{background-attachment:fixed}.bgi-attachment-scroll{background-attachment:scroll}@media (min-width:576px){.bgi-size-sm-auto{background-size:auto}.bgi-size-sm-cover{background-size:cover}.bgi-size-sm-contain{background-size:contain}.bgi-attachment-sm-fixed{background-attachment:fixed}.bgi-attachment-sm-scroll{background-attachment:scroll}}@media (min-width:768px){.bgi-size-md-auto{background-size:auto}.bgi-size-md-cover{background-size:cover}.bgi-size-md-contain{background-size:contain}.bgi-attachment-md-fixed{background-attachment:fixed}.bgi-attachment-md-scroll{background-attachment:scroll}}@media (min-width:992px){.bgi-size-lg-auto{background-size:auto}.bgi-size-lg-cover{background-size:cover}.bgi-size-lg-contain{background-size:contain}.bgi-attachment-lg-fixed{background-attachment:fixed}.bgi-attachment-lg-scroll{background-attachment:scroll}}@media (min-width:1200px){.bgi-size-xl-auto{background-size:auto}.bgi-size-xl-cover{background-size:cover}.bgi-size-xl-contain{background-size:contain}.bgi-attachment-xl-fixed{background-attachment:fixed}.bgi-attachment-xl-scroll{background-attachment:scroll}}@media (min-width:1400px){.bgi-size-xxl-auto{background-size:auto}.bgi-size-xxl-cover{background-size:cover}.bgi-size-xxl-contain{background-size:contain}.bgi-attachment-xxl-fixed{background-attachment:fixed}.bgi-attachment-xxl-scroll{background-attachment:scroll}}.border-active:not(.active):not(:active):not(:hover):not(:focus){border-color:transparent!important}.border-hover:not(:hover):not(:focus):not(.active):not(:active){cursor:pointer;border-color:transparent!important}.border-gray-100{border-color:var(--bs-gray-100)!important}.border-gray-200{border-color:var(--bs-gray-200)!important}.border-gray-300{border-color:var(--bs-gray-300)!important}.border-gray-400{border-color:var(--bs-gray-400)!important}.border-gray-500{border-color:var(--bs-gray-500)!important}.border-gray-600{border-color:var(--bs-gray-600)!important}.border-gray-700{border-color:var(--bs-gray-700)!important}.border-gray-800{border-color:var(--bs-gray-800)!important}.border-gray-900{border-color:var(--bs-gray-900)!important}.border-light-clarity{border-color:var(--bs-light-clarity)!important}.border-hover-light:hover{border-color:var(--bs-light)!important}.border-active-light.active{border-color:var(--bs-light)!important}.border-primary-clarity{border-color:var(--bs-primary-clarity)!important}.border-hover-primary:hover{border-color:var(--bs-primary)!important}.border-active-primary.active{border-color:var(--bs-primary)!important}.border-secondary-clarity{border-color:var(--bs-secondary-clarity)!important}.border-hover-secondary:hover{border-color:var(--bs-secondary)!important}.border-active-secondary.active{border-color:var(--bs-secondary)!important}.border-success-clarity{border-color:var(--bs-success-clarity)!important}.border-hover-success:hover{border-color:var(--bs-success)!important}.border-active-success.active{border-color:var(--bs-success)!important}.border-info-clarity{border-color:var(--bs-info-clarity)!important}.border-hover-info:hover{border-color:var(--bs-info)!important}.border-active-info.active{border-color:var(--bs-info)!important}.border-warning-clarity{border-color:var(--bs-warning-clarity)!important}.border-hover-warning:hover{border-color:var(--bs-warning)!important}.border-active-warning.active{border-color:var(--bs-warning)!important}.border-danger-clarity{border-color:var(--bs-danger-clarity)!important}.border-hover-danger:hover{border-color:var(--bs-danger)!important}.border-active-danger.active{border-color:var(--bs-danger)!important}.border-dark-clarity{border-color:var(--bs-dark-clarity)!important}.border-hover-dark:hover{border-color:var(--bs-dark)!important}.border-active-dark.active{border-color:var(--bs-dark)!important}.border-active-primary-clarity.active,.border-hover-primary-clarity:hover{border-color:var(--bs-primary-clarity)!important}.border-hover-transparent:hover{border-color:transparent!important}.border-dashed{border-style:dashed!important;border-color:var(--bs-border-dashed-color)}.border-top-dashed{border-top-style:dashed!important}.border-bottom-dashed{border-bottom-style:dashed!important}.border-start-dashed{border-left-style:dashed!important}.border-end-dashed{border-right-style:dashed!important}.border-dotted{border-style:dotted!important}.border-top-dotted{border-top-style:dotted!important}.border-bottom-dotted{border-bottom-style:dotted!important}.border-start-dotted{border-left-style:dotted!important}.border-end-dotted{border-right-style:dotted!important}.border-transparent{border-color:transparent!important}.border-body{border-color:var(--bs-body-bg)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-bottom-0{border-bottom-left-radius:0!important;border-bottom-right-radius:0!important}.rounded-start-0{border-top-left-radius:0!important;border-bottom-left-radius:0!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-circle{border-radius:50%!important}.flex-root{flex:1}.flex-column-auto{flex:none}.flex-column-fluid{flex:1 0 auto}.flex-row-auto{flex:0 0 auto}.flex-row-fluid{flex:1 auto;min-width:0}.flex-center{justify-content:center;align-items:center}.flex-start{justify-content:start;align-items:start}.flex-end{justify-content:flex-end;align-items:flex-end}.flex-stack{justify-content:space-between;align-items:center}@media (min-width:576px){.flex-sm-root{flex:1}.flex-sm-column-auto{flex:none}.flex-sm-column-fluid{flex:1 0 auto}.flex-sm-row-auto{flex:0 0 auto}.flex-sm-row-fluid{flex:1 auto;min-width:0}.flex-sm-center{justify-content:center;align-items:center}.flex-sm-start{justify-content:start;align-items:start}.flex-sm-end{justify-content:flex-end;align-items:flex-end}.flex-sm-stack{justify-content:space-between;align-items:center}}@media (min-width:768px){.flex-md-root{flex:1}.flex-md-column-auto{flex:none}.flex-md-column-fluid{flex:1 0 auto}.flex-md-row-auto{flex:0 0 auto}.flex-md-row-fluid{flex:1 auto;min-width:0}.flex-md-center{justify-content:center;align-items:center}.flex-md-start{justify-content:start;align-items:start}.flex-md-end{justify-content:flex-end;align-items:flex-end}.flex-md-stack{justify-content:space-between;align-items:center}}@media (min-width:992px){.flex-lg-root{flex:1}.flex-lg-column-auto{flex:none}.flex-lg-column-fluid{flex:1 0 auto}.flex-lg-row-auto{flex:0 0 auto}.flex-lg-row-fluid{flex:1 auto;min-width:0}.flex-lg-center{justify-content:center;align-items:center}.flex-lg-start{justify-content:start;align-items:start}.flex-lg-end{justify-content:flex-end;align-items:flex-end}.flex-lg-stack{justify-content:space-between;align-items:center}}@media (min-width:1200px){.flex-xl-root{flex:1}.flex-xl-column-auto{flex:none}.flex-xl-column-fluid{flex:1 0 auto}.flex-xl-row-auto{flex:0 0 auto}.flex-xl-row-fluid{flex:1 auto;min-width:0}.flex-xl-center{justify-content:center;align-items:center}.flex-xl-start{justify-content:start;align-items:start}.flex-xl-end{justify-content:flex-end;align-items:flex-end}.flex-xl-stack{justify-content:space-between;align-items:center}}@media (min-width:1400px){.flex-xxl-root{flex:1}.flex-xxl-column-auto{flex:none}.flex-xxl-column-fluid{flex:1 0 auto}.flex-xxl-row-auto{flex:0 0 auto}.flex-xxl-row-fluid{flex:1 auto;min-width:0}.flex-xxl-center{justify-content:center;align-items:center}.flex-xxl-start{justify-content:start;align-items:start}.flex-xxl-end{justify-content:flex-end;align-items:flex-end}.flex-xxl-stack{justify-content:space-between;align-items:center}}.flex-equal{flex-grow:1;flex-basis:0;flex-shrink:0}.shadow-xs{box-shadow:var(--bs-box-shadow-xs)}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)}.shadow{box-shadow:var(--bs-box-shadow)}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)}.text-white{color:var(--bs-text-white)!important}.text-hover-white{transition:color .2s ease}.text-hover-white i{transition:color .2s ease}.text-hover-white:hover{transition:color .2s ease;color:var(--bs-text-white)!important}.text-hover-white:hover i{transition:color .2s ease;color:var(--bs-text-white)!important}.text-hover-white:hover .svg-icon{color:var(--bs-text-white)!important}.text-active-white{transition:color .2s ease}.text-active-white i{transition:color .2s ease}.text-active-white.active{transition:color .2s ease;color:var(--bs-text-white)!important}.text-active-white.active i{transition:color .2s ease;color:var(--bs-text-white)!important}.text-active-white.active .svg-icon{color:var(--bs-text-white)!important}.text-primary{color:var(--bs-text-primary)!important}.text-inverse-primary{color:var(--bs-primary-inverse)!important}.text-light-primary{color:var(--bs-primary-light)!important}.text-hover-primary{transition:color .2s ease}.text-hover-primary i{transition:color .2s ease}.text-hover-primary:hover{transition:color .2s ease;color:var(--bs-text-primary)!important}.text-hover-primary:hover i{transition:color .2s ease;color:var(--bs-text-primary)!important}.text-hover-primary:hover .svg-icon{color:var(--bs-text-primary)!important}.text-active-primary{transition:color .2s ease}.text-active-primary i{transition:color .2s ease}.text-active-primary.active{transition:color .2s ease;color:var(--bs-text-primary)!important}.text-active-primary.active i{transition:color .2s ease;color:var(--bs-text-primary)!important}.text-active-primary.active .svg-icon{color:var(--bs-text-primary)!important}.text-secondary{color:var(--bs-text-secondary)!important}.text-inverse-secondary{color:var(--bs-secondary-inverse)!important}.text-light-secondary{color:var(--bs-secondary-light)!important}.text-hover-secondary{transition:color .2s ease}.text-hover-secondary i{transition:color .2s ease}.text-hover-secondary:hover{transition:color .2s ease;color:var(--bs-text-secondary)!important}.text-hover-secondary:hover i{transition:color .2s ease;color:var(--bs-text-secondary)!important}.text-hover-secondary:hover .svg-icon{color:var(--bs-text-secondary)!important}.text-active-secondary{transition:color .2s ease}.text-active-secondary i{transition:color .2s ease}.text-active-secondary.active{transition:color .2s ease;color:var(--bs-text-secondary)!important}.text-active-secondary.active i{transition:color .2s ease;color:var(--bs-text-secondary)!important}.text-active-secondary.active .svg-icon{color:var(--bs-text-secondary)!important}.text-light{color:var(--bs-text-light)!important}.text-inverse-light{color:var(--bs-light-inverse)!important}.text-light-light{color:var(--bs-light-light)!important}.text-hover-light{transition:color .2s ease}.text-hover-light i{transition:color .2s ease}.text-hover-light:hover{transition:color .2s ease;color:var(--bs-text-light)!important}.text-hover-light:hover i{transition:color .2s ease;color:var(--bs-text-light)!important}.text-hover-light:hover .svg-icon{color:var(--bs-text-light)!important}.text-active-light{transition:color .2s ease}.text-active-light i{transition:color .2s ease}.text-active-light.active{transition:color .2s ease;color:var(--bs-text-light)!important}.text-active-light.active i{transition:color .2s ease;color:var(--bs-text-light)!important}.text-active-light.active .svg-icon{color:var(--bs-text-light)!important}.text-success{color:var(--bs-text-success)!important}.text-inverse-success{color:var(--bs-success-inverse)!important}.text-light-success{color:var(--bs-success-light)!important}.text-hover-success{transition:color .2s ease}.text-hover-success i{transition:color .2s ease}.text-hover-success:hover{transition:color .2s ease;color:var(--bs-text-success)!important}.text-hover-success:hover i{transition:color .2s ease;color:var(--bs-text-success)!important}.text-hover-success:hover .svg-icon{color:var(--bs-text-success)!important}.text-active-success{transition:color .2s ease}.text-active-success i{transition:color .2s ease}.text-active-success.active{transition:color .2s ease;color:var(--bs-text-success)!important}.text-active-success.active i{transition:color .2s ease;color:var(--bs-text-success)!important}.text-active-success.active .svg-icon{color:var(--bs-text-success)!important}.text-info{color:var(--bs-text-info)!important}.text-inverse-info{color:var(--bs-info-inverse)!important}.text-light-info{color:var(--bs-info-light)!important}.text-hover-info{transition:color .2s ease}.text-hover-info i{transition:color .2s ease}.text-hover-info:hover{transition:color .2s ease;color:var(--bs-text-info)!important}.text-hover-info:hover i{transition:color .2s ease;color:var(--bs-text-info)!important}.text-hover-info:hover .svg-icon{color:var(--bs-text-info)!important}.text-active-info{transition:color .2s ease}.text-active-info i{transition:color .2s ease}.text-active-info.active{transition:color .2s ease;color:var(--bs-text-info)!important}.text-active-info.active i{transition:color .2s ease;color:var(--bs-text-info)!important}.text-active-info.active .svg-icon{color:var(--bs-text-info)!important}.text-warning{color:var(--bs-text-warning)!important}.text-inverse-warning{color:var(--bs-warning-inverse)!important}.text-light-warning{color:var(--bs-warning-light)!important}.text-hover-warning{transition:color .2s ease}.text-hover-warning i{transition:color .2s ease}.text-hover-warning:hover{transition:color .2s ease;color:var(--bs-text-warning)!important}.text-hover-warning:hover i{transition:color .2s ease;color:var(--bs-text-warning)!important}.text-hover-warning:hover .svg-icon{color:var(--bs-text-warning)!important}.text-active-warning{transition:color .2s ease}.text-active-warning i{transition:color .2s ease}.text-active-warning.active{transition:color .2s ease;color:var(--bs-text-warning)!important}.text-active-warning.active i{transition:color .2s ease;color:var(--bs-text-warning)!important}.text-active-warning.active .svg-icon{color:var(--bs-text-warning)!important}.text-danger{color:var(--bs-text-danger)!important}.text-inverse-danger{color:var(--bs-danger-inverse)!important}.text-light-danger{color:var(--bs-danger-light)!important}.text-hover-danger{transition:color .2s ease}.text-hover-danger i{transition:color .2s ease}.text-hover-danger:hover{transition:color .2s ease;color:var(--bs-text-danger)!important}.text-hover-danger:hover i{transition:color .2s ease;color:var(--bs-text-danger)!important}.text-hover-danger:hover .svg-icon{color:var(--bs-text-danger)!important}.text-active-danger{transition:color .2s ease}.text-active-danger i{transition:color .2s ease}.text-active-danger.active{transition:color .2s ease;color:var(--bs-text-danger)!important}.text-active-danger.active i{transition:color .2s ease;color:var(--bs-text-danger)!important}.text-active-danger.active .svg-icon{color:var(--bs-text-danger)!important}.text-dark{color:var(--bs-text-dark)!important}.text-inverse-dark{color:var(--bs-dark-inverse)!important}.text-light-dark{color:var(--bs-dark-light)!important}.text-hover-dark{transition:color .2s ease}.text-hover-dark i{transition:color .2s ease}.text-hover-dark:hover{transition:color .2s ease;color:var(--bs-text-dark)!important}.text-hover-dark:hover i{transition:color .2s ease;color:var(--bs-text-dark)!important}.text-hover-dark:hover .svg-icon{color:var(--bs-text-dark)!important}.text-active-dark{transition:color .2s ease}.text-active-dark i{transition:color .2s ease}.text-active-dark.active{transition:color .2s ease;color:var(--bs-text-dark)!important}.text-active-dark.active i{transition:color .2s ease;color:var(--bs-text-dark)!important}.text-active-dark.active .svg-icon{color:var(--bs-text-dark)!important}.text-muted{color:var(--bs-text-muted)!important}.text-hover-muted{transition:color .2s ease}.text-hover-muted i{transition:color .2s ease}.text-hover-muted:hover{transition:color .2s ease;color:var(--bs-text-muted)!important}.text-hover-muted:hover i{transition:color .2s ease;color:var(--bs-text-muted)!important}.text-hover-muted:hover .svg-icon{color:var(--bs-text-muted)!important}.text-active-muted{transition:color .2s ease}.text-active-muted i{transition:color .2s ease}.text-active-muted.active{transition:color .2s ease;color:var(--bs-text-muted)!important}.text-active-muted.active i{transition:color .2s ease;color:var(--bs-text-muted)!important}.text-active-muted.active .svg-icon{color:var(--bs-text-muted)!important}.text-gray-100{color:var(--bs-text-gray-100)!important}.text-hover-gray-100{transition:color .2s ease}.text-hover-gray-100 i{transition:color .2s ease}.text-hover-gray-100:hover{transition:color .2s ease;color:var(--bs-text-gray-100)!important}.text-hover-gray-100:hover i{transition:color .2s ease;color:var(--bs-text-gray-100)!important}.text-hover-gray-100:hover .svg-icon{color:var(--bs-text-gray-100)!important}.text-active-gray-100{transition:color .2s ease}.text-active-gray-100 i{transition:color .2s ease}.text-active-gray-100.active{transition:color .2s ease;color:var(--bs-text-gray-100)!important}.text-active-gray-100.active i{transition:color .2s ease;color:var(--bs-text-gray-100)!important}.text-active-gray-100.active .svg-icon{color:var(--bs-text-gray-100)!important}.text-gray-200{color:var(--bs-text-gray-200)!important}.text-hover-gray-200{transition:color .2s ease}.text-hover-gray-200 i{transition:color .2s ease}.text-hover-gray-200:hover{transition:color .2s ease;color:var(--bs-text-gray-200)!important}.text-hover-gray-200:hover i{transition:color .2s ease;color:var(--bs-text-gray-200)!important}.text-hover-gray-200:hover .svg-icon{color:var(--bs-text-gray-200)!important}.text-active-gray-200{transition:color .2s ease}.text-active-gray-200 i{transition:color .2s ease}.text-active-gray-200.active{transition:color .2s ease;color:var(--bs-text-gray-200)!important}.text-active-gray-200.active i{transition:color .2s ease;color:var(--bs-text-gray-200)!important}.text-active-gray-200.active .svg-icon{color:var(--bs-text-gray-200)!important}.text-gray-300{color:var(--bs-text-gray-300)!important}.text-hover-gray-300{transition:color .2s ease}.text-hover-gray-300 i{transition:color .2s ease}.text-hover-gray-300:hover{transition:color .2s ease;color:var(--bs-text-gray-300)!important}.text-hover-gray-300:hover i{transition:color .2s ease;color:var(--bs-text-gray-300)!important}.text-hover-gray-300:hover .svg-icon{color:var(--bs-text-gray-300)!important}.text-active-gray-300{transition:color .2s ease}.text-active-gray-300 i{transition:color .2s ease}.text-active-gray-300.active{transition:color .2s ease;color:var(--bs-text-gray-300)!important}.text-active-gray-300.active i{transition:color .2s ease;color:var(--bs-text-gray-300)!important}.text-active-gray-300.active .svg-icon{color:var(--bs-text-gray-300)!important}.text-gray-400{color:var(--bs-text-gray-400)!important}.text-hover-gray-400{transition:color .2s ease}.text-hover-gray-400 i{transition:color .2s ease}.text-hover-gray-400:hover{transition:color .2s ease;color:var(--bs-text-gray-400)!important}.text-hover-gray-400:hover i{transition:color .2s ease;color:var(--bs-text-gray-400)!important}.text-hover-gray-400:hover .svg-icon{color:var(--bs-text-gray-400)!important}.text-active-gray-400{transition:color .2s ease}.text-active-gray-400 i{transition:color .2s ease}.text-active-gray-400.active{transition:color .2s ease;color:var(--bs-text-gray-400)!important}.text-active-gray-400.active i{transition:color .2s ease;color:var(--bs-text-gray-400)!important}.text-active-gray-400.active .svg-icon{color:var(--bs-text-gray-400)!important}.text-gray-500{color:var(--bs-text-gray-500)!important}.text-hover-gray-500{transition:color .2s ease}.text-hover-gray-500 i{transition:color .2s ease}.text-hover-gray-500:hover{transition:color .2s ease;color:var(--bs-text-gray-500)!important}.text-hover-gray-500:hover i{transition:color .2s ease;color:var(--bs-text-gray-500)!important}.text-hover-gray-500:hover .svg-icon{color:var(--bs-text-gray-500)!important}.text-active-gray-500{transition:color .2s ease}.text-active-gray-500 i{transition:color .2s ease}.text-active-gray-500.active{transition:color .2s ease;color:var(--bs-text-gray-500)!important}.text-active-gray-500.active i{transition:color .2s ease;color:var(--bs-text-gray-500)!important}.text-active-gray-500.active .svg-icon{color:var(--bs-text-gray-500)!important}.text-gray-600{color:var(--bs-text-gray-600)!important}.text-hover-gray-600{transition:color .2s ease}.text-hover-gray-600 i{transition:color .2s ease}.text-hover-gray-600:hover{transition:color .2s ease;color:var(--bs-text-gray-600)!important}.text-hover-gray-600:hover i{transition:color .2s ease;color:var(--bs-text-gray-600)!important}.text-hover-gray-600:hover .svg-icon{color:var(--bs-text-gray-600)!important}.text-active-gray-600{transition:color .2s ease}.text-active-gray-600 i{transition:color .2s ease}.text-active-gray-600.active{transition:color .2s ease;color:var(--bs-text-gray-600)!important}.text-active-gray-600.active i{transition:color .2s ease;color:var(--bs-text-gray-600)!important}.text-active-gray-600.active .svg-icon{color:var(--bs-text-gray-600)!important}.text-gray-700{color:var(--bs-text-gray-700)!important}.text-hover-gray-700{transition:color .2s ease}.text-hover-gray-700 i{transition:color .2s ease}.text-hover-gray-700:hover{transition:color .2s ease;color:var(--bs-text-gray-700)!important}.text-hover-gray-700:hover i{transition:color .2s ease;color:var(--bs-text-gray-700)!important}.text-hover-gray-700:hover .svg-icon{color:var(--bs-text-gray-700)!important}.text-active-gray-700{transition:color .2s ease}.text-active-gray-700 i{transition:color .2s ease}.text-active-gray-700.active{transition:color .2s ease;color:var(--bs-text-gray-700)!important}.text-active-gray-700.active i{transition:color .2s ease;color:var(--bs-text-gray-700)!important}.text-active-gray-700.active .svg-icon{color:var(--bs-text-gray-700)!important}.text-gray-800{color:var(--bs-text-gray-800)!important}.text-hover-gray-800{transition:color .2s ease}.text-hover-gray-800 i{transition:color .2s ease}.text-hover-gray-800:hover{transition:color .2s ease;color:var(--bs-text-gray-800)!important}.text-hover-gray-800:hover i{transition:color .2s ease;color:var(--bs-text-gray-800)!important}.text-hover-gray-800:hover .svg-icon{color:var(--bs-text-gray-800)!important}.text-active-gray-800{transition:color .2s ease}.text-active-gray-800 i{transition:color .2s ease}.text-active-gray-800.active{transition:color .2s ease;color:var(--bs-text-gray-800)!important}.text-active-gray-800.active i{transition:color .2s ease;color:var(--bs-text-gray-800)!important}.text-active-gray-800.active .svg-icon{color:var(--bs-text-gray-800)!important}.text-gray-900{color:var(--bs-text-gray-900)!important}.text-hover-gray-900{transition:color .2s ease}.text-hover-gray-900 i{transition:color .2s ease}.text-hover-gray-900:hover{transition:color .2s ease;color:var(--bs-text-gray-900)!important}.text-hover-gray-900:hover i{transition:color .2s ease;color:var(--bs-text-gray-900)!important}.text-hover-gray-900:hover .svg-icon{color:var(--bs-text-gray-900)!important}.text-active-gray-900{transition:color .2s ease}.text-active-gray-900 i{transition:color .2s ease}.text-active-gray-900.active{transition:color .2s ease;color:var(--bs-text-gray-900)!important}.text-active-gray-900.active i{transition:color .2s ease;color:var(--bs-text-gray-900)!important}.text-active-gray-900.active .svg-icon{color:var(--bs-text-gray-900)!important}.parent-hover .parent-hover-white{transition:color .2s ease}.parent-hover:hover .parent-hover-white{transition:color .2s ease;color:var(--bs-text-white)!important}.parent-hover .parent-hover-primary{transition:color .2s ease}.parent-hover:hover .parent-hover-primary{transition:color .2s ease;color:var(--bs-text-primary)!important}.parent-hover .parent-hover-secondary{transition:color .2s ease}.parent-hover:hover .parent-hover-secondary{transition:color .2s ease;color:var(--bs-text-secondary)!important}.parent-hover .parent-hover-light{transition:color .2s ease}.parent-hover:hover .parent-hover-light{transition:color .2s ease;color:var(--bs-text-light)!important}.parent-hover .parent-hover-success{transition:color .2s ease}.parent-hover:hover .parent-hover-success{transition:color .2s ease;color:var(--bs-text-success)!important}.parent-hover .parent-hover-info{transition:color .2s ease}.parent-hover:hover .parent-hover-info{transition:color .2s ease;color:var(--bs-text-info)!important}.parent-hover .parent-hover-warning{transition:color .2s ease}.parent-hover:hover .parent-hover-warning{transition:color .2s ease;color:var(--bs-text-warning)!important}.parent-hover .parent-hover-danger{transition:color .2s ease}.parent-hover:hover .parent-hover-danger{transition:color .2s ease;color:var(--bs-text-danger)!important}.parent-hover .parent-hover-dark{transition:color .2s ease}.parent-hover:hover .parent-hover-dark{transition:color .2s ease;color:var(--bs-text-dark)!important}.parent-hover .parent-hover-muted{transition:color .2s ease}.parent-hover:hover .parent-hover-muted{transition:color .2s ease;color:var(--bs-text-muted)!important}.parent-hover .parent-hover-gray-100{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-100{transition:color .2s ease;color:var(--bs-text-gray-100)!important}.parent-hover .parent-hover-gray-200{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-200{transition:color .2s ease;color:var(--bs-text-gray-200)!important}.parent-hover .parent-hover-gray-300{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-300{transition:color .2s ease;color:var(--bs-text-gray-300)!important}.parent-hover .parent-hover-gray-400{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-400{transition:color .2s ease;color:var(--bs-text-gray-400)!important}.parent-hover .parent-hover-gray-500{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-500{transition:color .2s ease;color:var(--bs-text-gray-500)!important}.parent-hover .parent-hover-gray-600{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-600{transition:color .2s ease;color:var(--bs-text-gray-600)!important}.parent-hover .parent-hover-gray-700{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-700{transition:color .2s ease;color:var(--bs-text-gray-700)!important}.parent-hover .parent-hover-gray-800{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-800{transition:color .2s ease;color:var(--bs-text-gray-800)!important}.parent-hover .parent-hover-gray-900{transition:color .2s ease}.parent-hover:hover .parent-hover-gray-900{transition:color .2s ease;color:var(--bs-text-gray-900)!important}.parent-active .parent-active-white{transition:color .2s ease}.parent-active.active .parent-active-white{transition:color .2s ease;color:var(--bs-text-white)!important}.parent-active .parent-active-primary{transition:color .2s ease}.parent-active.active .parent-active-primary{transition:color .2s ease;color:var(--bs-text-primary)!important}.parent-active .parent-active-secondary{transition:color .2s ease}.parent-active.active .parent-active-secondary{transition:color .2s ease;color:var(--bs-text-secondary)!important}.parent-active .parent-active-light{transition:color .2s ease}.parent-active.active .parent-active-light{transition:color .2s ease;color:var(--bs-text-light)!important}.parent-active .parent-active-success{transition:color .2s ease}.parent-active.active .parent-active-success{transition:color .2s ease;color:var(--bs-text-success)!important}.parent-active .parent-active-info{transition:color .2s ease}.parent-active.active .parent-active-info{transition:color .2s ease;color:var(--bs-text-info)!important}.parent-active .parent-active-warning{transition:color .2s ease}.parent-active.active .parent-active-warning{transition:color .2s ease;color:var(--bs-text-warning)!important}.parent-active .parent-active-danger{transition:color .2s ease}.parent-active.active .parent-active-danger{transition:color .2s ease;color:var(--bs-text-danger)!important}.parent-active .parent-active-dark{transition:color .2s ease}.parent-active.active .parent-active-dark{transition:color .2s ease;color:var(--bs-text-dark)!important}.parent-active .parent-active-muted{transition:color .2s ease}.parent-active.active .parent-active-muted{transition:color .2s ease;color:var(--bs-text-muted)!important}.parent-active .parent-active-gray-100{transition:color .2s ease}.parent-active.active .parent-active-gray-100{transition:color .2s ease;color:var(--bs-text-gray-100)!important}.parent-active .parent-active-gray-200{transition:color .2s ease}.parent-active.active .parent-active-gray-200{transition:color .2s ease;color:var(--bs-text-gray-200)!important}.parent-active .parent-active-gray-300{transition:color .2s ease}.parent-active.active .parent-active-gray-300{transition:color .2s ease;color:var(--bs-text-gray-300)!important}.parent-active .parent-active-gray-400{transition:color .2s ease}.parent-active.active .parent-active-gray-400{transition:color .2s ease;color:var(--bs-text-gray-400)!important}.parent-active .parent-active-gray-500{transition:color .2s ease}.parent-active.active .parent-active-gray-500{transition:color .2s ease;color:var(--bs-text-gray-500)!important}.parent-active .parent-active-gray-600{transition:color .2s ease}.parent-active.active .parent-active-gray-600{transition:color .2s ease;color:var(--bs-text-gray-600)!important}.parent-active .parent-active-gray-700{transition:color .2s ease}.parent-active.active .parent-active-gray-700{transition:color .2s ease;color:var(--bs-text-gray-700)!important}.parent-active .parent-active-gray-800{transition:color .2s ease}.parent-active.active .parent-active-gray-800{transition:color .2s ease;color:var(--bs-text-gray-800)!important}.parent-active .parent-active-gray-900{transition:color .2s ease}.parent-active.active .parent-active-gray-900{transition:color .2s ease;color:var(--bs-text-gray-900)!important}.text-transparent{color:transparent}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-move{cursor:move}i.bi,i[class*=\" fonticon-\"],i[class*=\" la-\"],i[class^=fonticon-],i[class^=la-]{line-height:1;font-size:1rem;color:var(--bs-text-muted)}a{transition:color .2s ease}a:hover{transition:color .2s ease}.opacity-active-0.active{opacity:0!important}.opacity-state-0.active,.opacity-state-0:hover{opacity:0!important}.opacity-active-5.active{opacity:.05!important}.opacity-state-5.active,.opacity-state-5:hover{opacity:.05!important}.opacity-active-10.active{opacity:.1!important}.opacity-state-10.active,.opacity-state-10:hover{opacity:.1!important}.opacity-active-15.active{opacity:.15!important}.opacity-state-15.active,.opacity-state-15:hover{opacity:.15!important}.opacity-active-20.active{opacity:.2!important}.opacity-state-20.active,.opacity-state-20:hover{opacity:.2!important}.opacity-active-25.active{opacity:.25!important}.opacity-state-25.active,.opacity-state-25:hover{opacity:.25!important}.opacity-active-50.active{opacity:.5!important}.opacity-state-50.active,.opacity-state-50:hover{opacity:.5!important}.opacity-active-75.active{opacity:.75!important}.opacity-state-75.active,.opacity-state-75:hover{opacity:.75!important}.opacity-active-100.active{opacity:1!important}.opacity-state-100.active,.opacity-state-100:hover{opacity:1!important}.transform-90{transform:rotate(90deg);transform-origin:right top}.tree{--bs-tree-icon-size:16px;--bs-tree-icon-gap:14px;--bs-tree-icon-color-open:var(--bs-success);--bs-tree-icon-color-default:var(--bs-gray-500);--bs-tree-icon-color-close:var(--bs-gray-500);--bs-tree-line-color:var(--bs-gray-200)}.tree .tree-node{padding-left:1.25rem;display:flex;flex-direction:column;align-items:start}.tree .tree-sub{padding:.35rem 0}.tree .tree-content{display:flex;align-items:center;padding:.35rem 0}.tree .tree-wrapper{padding-left:calc(var(--bs-tree-icon-size) + var(--bs-tree-icon-size))}.tree .tree-section{display:flex;align-items:baseline;padding-left:var(--bs-tree-icon-gap)}.tree .tree-toggle{display:flex;align-items:center;width:var(--bs-tree-icon-size)}.tree .tree-toggle .tree-icon-default{font-size:1.5rem;color:var(--bs-tree-icon-color-default)}.tree .tree-toggle .tree-icon-open{font-size:1.5rem;color:var(--bs-tree-icon-color-open)}.tree .tree-toggle .tree-icon-close{font-size:1.5rem;color:var(--bs-tree-icon-color-close)}.tree .tree-toggle.collapsed .tree-icon-close{display:flex}.tree .tree-toggle.collapsed .tree-icon-open{display:none}.tree .tree-toggle:not(.collapsed) .tree-icon-close{display:none}.tree .tree-toggle:not(.collapsed) .tree-icon-open{display:flex}.tree>.tree-node{padding-left:0!important}.tree.tree-line .tree-sub{border-left:1px solid var(--bs-tree-line-color);margin-left:calc(var(--bs-tree-icon-size)/ 2)}@media (min-width:992px){.testimonials-slider-highlight{transition:all ease .3}.testimonials-slider-highlight .testimonials-author,.testimonials-slider-highlight .testimonials-body,.testimonials-slider-highlight .testimonials-photo,.testimonials-slider-highlight .testimonials-positon,.testimonials-slider-highlight .testimonials-title{transition:all ease .3s}.testimonials-slider-highlight .tns-item:not(.tns-slide-active)+.tns-item.tns-slide-active .testimonials-photo{height:200px;width:200px;transition:all ease .3s}.testimonials-slider-highlight .tns-item:not(.tns-slide-active)+.tns-item.tns-slide-active .testimonials-title{color:var(--bs-gray-900)!important;font-size:1.54rem!important;transition:all ease .3s;margin-bottom:1.3rem!important}.testimonials-slider-highlight .tns-item:not(.tns-slide-active)+.tns-item.tns-slide-active .testimonials-description{color:var(--bs-gray-700)!important;font-size:1.38rem!important;transition:all ease .3s;margin-bottom:1.7rem!important}.testimonials-slider-highlight .tns-item:not(.tns-slide-active)+.tns-item.tns-slide-active .testimonials-author{color:var(--bs-primary)!important;font-size:1.1rem!important;transition:all ease .3s}.testimonials-slider-highlight .tns-item:not(.tns-slide-active)+.tns-item.tns-slide-active .testimonials-positon{color:var(--bs-gray-500)!important;font-size:.9rem!important;transition:all ease .3s}}.testimonials-slider-quote .testimonials-quote{opacity:.2}.testimonials-slider-quote .tns-item:not(.tns-slide-active)+.tns-slide-active+.tns-slide-active .testimonials-quote{opacity:.5;color:var(--bs-primary)!important}@media (min-width:992px){.btn.btn-secondary{color:var(--bs-body-bg);background-color:var(--bs-gray-800)}.btn.btn-secondary .svg-icon,.btn.btn-secondary i{color:var(--bs-body-bg)}.btn.btn-secondary.dropdown-toggle:after{color:var(--bs-body-bg)}.btn-check:active+.btn.btn-secondary,.btn-check:checked+.btn.btn-secondary,.btn.btn-secondary.active,.btn.btn-secondary.show,.btn.btn-secondary:active:not(.btn-active),.btn.btn-secondary:focus:not(.btn-active),.btn.btn-secondary:hover:not(.btn-active),.show>.btn.btn-secondary{color:var(--bs-body-bg);background-color:var(--bs-gray-700)!important}.btn-check:active+.btn.btn-secondary .svg-icon,.btn-check:active+.btn.btn-secondary i,.btn-check:checked+.btn.btn-secondary .svg-icon,.btn-check:checked+.btn.btn-secondary i,.btn.btn-secondary.active .svg-icon,.btn.btn-secondary.active i,.btn.btn-secondary.show .svg-icon,.btn.btn-secondary.show i,.btn.btn-secondary:active:not(.btn-active) .svg-icon,.btn.btn-secondary:active:not(.btn-active) i,.btn.btn-secondary:focus:not(.btn-active) .svg-icon,.btn.btn-secondary:focus:not(.btn-active) i,.btn.btn-secondary:hover:not(.btn-active) .svg-icon,.btn.btn-secondary:hover:not(.btn-active) i,.show>.btn.btn-secondary .svg-icon,.show>.btn.btn-secondary i{color:var(--bs-body-bg)}.btn-check:active+.btn.btn-secondary.dropdown-toggle:after,.btn-check:checked+.btn.btn-secondary.dropdown-toggle:after,.btn.btn-secondary.active.dropdown-toggle:after,.btn.btn-secondary.show.dropdown-toggle:after,.btn.btn-secondary:active:not(.btn-active).dropdown-toggle:after,.btn.btn-secondary:focus:not(.btn-active).dropdown-toggle:after,.btn.btn-secondary:hover:not(.btn-active).dropdown-toggle:after,.show>.btn.btn-secondary.dropdown-toggle:after{color:var(--bs-body-bg)}}[data-bs-theme=dark] .btn.btn-secondary{color:var(--bs-gray-700);background-color:#2a2a3c}[data-bs-theme=dark] .btn.btn-secondary .svg-icon,[data-bs-theme=dark] .btn.btn-secondary i{color:#fff}[data-bs-theme=dark] .btn.btn-secondary.dropdown-toggle:after{color:#fff}.btn-check:active+[data-bs-theme=dark] .btn.btn-secondary,.btn-check:checked+[data-bs-theme=dark] .btn.btn-secondary,.show>[data-bs-theme=dark] .btn.btn-secondary,[data-bs-theme=dark] .btn.btn-secondary.active,[data-bs-theme=dark] .btn.btn-secondary.show,[data-bs-theme=dark] .btn.btn-secondary:active:not(.btn-active),[data-bs-theme=dark] .btn.btn-secondary:focus:not(.btn-active),[data-bs-theme=dark] .btn.btn-secondary:hover:not(.btn-active){color:var(--bs-gray-700);background-color:#282838!important}[data-bs-theme=light]{--bs-app-bg-color:#ffffff;--bs-app-blank-bg-color:#ffffff;--bs-app-header-base-bg-color:#ffffff;--bs-app-header-minimize-bg-color:rgba(255, 255, 255, 0.5);--bs-app-sidebar-base-bg-color:#0B0C10;--bs-app-sidebar-base-box-shadow:none}[data-bs-theme=dark]{--bs-app-bg-color:#0F1014;--bs-app-blank-bg-color:#151521;--bs-app-header-base-bg-color:#0F1014;--bs-app-header-minimize-bg-color:rgba(21, 23, 28, 0.5);--bs-app-sidebar-base-bg-color:#0B0C10;--bs-app-sidebar-base-box-shadow:none;--bs-app-sidebar-base-border-end:1px solid #1B1C22}html{font-family:sans-serif;text-size-adjust:100%}body,html{height:100%;margin:0;padding:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:13px!important;font-weight:400;font-family:Inter,Helvetica,sans-serif}@media (max-width:991.98px){body,html{font-size:12px!important}}@media (max-width:767.98px){body,html{font-size:12px!important}}body{display:flex;flex-direction:column}body a:active,body a:focus,body a:hover{text-decoration:none!important}canvas{user-select:none}router-outlet{display:none}.app-default,body{background-color:var(--bs-app-bg-color)}.app-blank{background-color:var(--bs-app-blank-bg-color)}[data-kt-app-reset-transition=true] *{transition:none!important}.app-page{display:flex}[data-kt-app-page-loading=on]{overflow:hidden}[data-kt-app-page-loading=on] *{transition:none!important}.app-page-loader{background:var(--bs-body-bg);color:var(--bs-body-color);position:fixed;top:0;bottom:0;left:0;right:0;z-index:10000;display:none}[data-kt-app-page-loading=on] .app-page-loader{display:flex;justify-content:center;align-items:center}@media (min-width:992px){.app-container{padding-left:30px!important;padding-right:30px!important}.app-container-fit-desktop{padding-left:0!important;padding-right:0!important}}@media (max-width:991.98px){.app-container{max-width:none;padding-left:20px!important;padding-right:20px!important}.app-container-fit-mobile{padding-left:0!important;padding-right:0!important}}@media print{.app-print-content-only{padding:0!important;background:0 0!important}.app-print-content-only .app-container,.app-print-content-only .app-content,.app-print-content-only .app-page,.app-print-content-only .app-page-title,.app-print-content-only .app-wrapper{background:0 0!important;padding:0!important;margin:0!important}.app-print-content-only .app-aside,.app-print-content-only .app-aside-panel,.app-print-content-only .app-footer,.app-print-content-only .app-header,.app-print-content-only .app-sidebar,.app-print-content-only .app-sidebar-panel,.app-print-content-only .app-toolbar,.app-print-content-only .btn,.app-print-content-only .drawer,.app-print-content-only .scrolltop{display:none!important}}.app-navbar{display:flex;align-items:stretch}.app-navbar .app-navbar-item{display:flex;align-items:center}.app-navbar.app-navbar-stretch .app-navbar-item{align-items:stretch}.app-header{transition:none;display:flex;align-items:stretch;background-color:var(--bs-app-header-base-bg-color);box-shadow:var(--bs-app-header-base-box-shadow);border-bottom:var(--bs-app-header-base-border-bottom)}@media (min-width:992px){:root{--bs-app-header-height:90px;--bs-app-header-height-actual:90px}[data-kt-app-header-fixed=true][data-kt-app-header-stacked=true]{--bs-app-header-height:calc(var(--bs-app-header-primary-height, 0px) + var(--bs-app-header-secondary-height, 0px) + var(--bs-app-header-tertiary-height, 0px));--bs-app-header-height-actual:calc(70px + 70px + 70px)}[data-kt-app-header-sticky=on]{--bs-app-header-height:70px;--bs-app-header-height-actual:90px}[data-kt-app-header-sticky=on][data-kt-app-header-stacked=true]{--bs-app-header-height:calc(var(--bs-app-header-primary-height, 0px) + var(--bs-app-header-secondary-height, 0px) + var(--bs-app-header-tertiary-height, 0px));--bs-app-header-height-actual:calc(70px + 70px + 70px)}[data-kt-app-header-minimize=on]{--bs-app-header-height:90px}.app-header{height:var(--bs-app-header-height)}[data-kt-app-header-fixed=true] .app-header{z-index:100;position:fixed;left:0;right:0;top:0}[data-kt-app-header-static=true] .app-header{position:relative}[data-kt-app-header-stacked=true] .app-header{flex-direction:column;height:calc(var(--bs-app-header-primary-height) + var(--bs-app-header-secondary-height,0px) + var(--bs-app-header-tertiary-height,0px))}[data-kt-app-header-sticky=on] .app-header{position:fixed;left:0;right:0;top:0;z-index:100;background-color:var(--bs-app-header-sticky-bg-color);box-shadow:var(--bs-app-header-sticky-box-shadow);border-bottom:var(--bs-app-header-sticky-border-bottom)}[data-kt-app-header-minimize=on] .app-header{transition:none;z-index:100;backdrop-filter:blur(6px);background-color:var(--bs-app-header-minimize-bg-color);box-shadow:var(--bs-app-header-minimize-box-shadow);border-bottom:var(--bs-app-header-minimize-border-bottom)}.app-header .app-header-mobile-drawer{display:flex}[data-kt-app-header-fixed=true][data-kt-app-sidebar-fixed=true][data-kt-app-sidebar-push-header=true] .app-header,[data-kt-app-header-fixed=true][data-kt-app-sidebar-sticky=on][data-kt-app-sidebar-push-header=true] .app-header{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}body:not([data-kt-app-header-fixed=true])[data-kt-app-sidebar-fixed=true][data-kt-app-sidebar-push-header=true] .app-header,body:not([data-kt-app-header-fixed=true])[data-kt-app-sidebar-sticky=on][data-kt-app-sidebar-push-header=true] .app-header{margin-left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}[data-kt-app-header-fixed=true][data-kt-app-sidebar-panel-fixed=true][data-kt-app-sidebar-panel-push-header=true] .app-header,[data-kt-app-header-fixed=true][data-kt-app-sidebar-panel-sticky=on][data-kt-app-sidebar-panel-push-header=true] .app-header{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}body:not([data-kt-app-header-fixed=true])[data-kt-app-sidebar-panel-fixed=true][data-kt-app-sidebar-panel-push-header=true] .app-header,body:not([data-kt-app-header-fixed=true])[data-kt-app-sidebar-panel-sticky=on][data-kt-app-sidebar-panel-push-header=true] .app-header{margin-left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}[data-kt-app-header-fixed=true][data-kt-app-aside-fixed=true][data-kt-app-aside-push-header=true] .app-header,[data-kt-app-header-fixed=true][data-kt-app-aside-sticky=on][data-kt-app-aside-push-header=true] .app-header{right:calc(var(--bs-app-aside-width) + var(--bs-app-aside-gap-start,0px) + var(--bs-app-aside-gap-end,0px))}[data-kt-app-header-fixed=true][data-kt-app-toolbar-fixed=true] .app-header{box-shadow:none}}@media (max-width:991.98px){:root{--bs-app-header-height:60px}[data-kt-app-header-sticky=on]{--bs-app-header-height:70px;--bs-app-header-height-actual:70px}[data-kt-app-header-minimize=on]{--bs-app-header-height:60px;--bs-app-header-height-actual:60px}.app-header{height:var(--bs-app-header-height);align-items:stretch}.app-header .app-header-mobile-drawer{display:none}[data-kt-app-header-stacked=true] .app-header{flex-direction:column;height:calc(var(--bs-app-header-primary-height) + var(--bs-app-header-secondary-height,0px) + var(--bs-app-header-tertiary-height,0px))}[data-kt-app-header-fixed-mobile=true] .app-header{z-index:100;transition:none;position:fixed;left:0;right:0;top:0}[data-kt-app-header-sticky=on] .app-header{position:fixed;left:0;right:0;top:0;z-index:100;background-color:var(--bs-app-header-sticky-bg-color);box-shadow:var(--bs-app-header-sticky-box-shadow);border-bottom:var(--bs-app-header-sticky-border-bottom)}[data-kt-app-header-minimize=on] .app-header{z-index:100;backdrop-filter:blur(6px);transition:none;background-color:var(--bs-app-header-minimize-bg-color);box-shadow:var(--bs-app-header-minimize-box-shadow);border-bottom:var(--bs-app-header-minimize-border-bottom)}[data-kt-app-header-fixed-mobile=true][data-kt-app-toolbar-fixed-mobile=true] .app-header{box-shadow:none}[data-kt-app-header-fixed-mobile=true][data-kt-app-toolbar-sticky=on] .app-header{box-shadow:none}}.app-header-primary{transition:none;display:flex;align-items:stretch}@media (min-width:992px){.app-header-primary{background-color:var(--bs-app-header-primary-base-bg-color);box-shadow:var(--bs-app-header-primary-base-box-shadow);border-bottom:var(--bs-app-header-primary-base-border-bottom)}[data-kt-app-header-primary-enabled=true]{--bs-app-header-primary-height:70px}[data-kt-app-header-primary-enabled=true][data-kt-app-header-sticky=on]{--bs-app-header-primary-height:70px}[data-kt-app-header-primary-enabled=true][data-kt-app-header-minimize=on]{--bs-app-header-primary-height:70px}[data-kt-app-header-primary-enabled=true][data-kt-app-header-sticky=on][data-kt-app-header-primary-sticky-hide=true]{--bs-app-header-primary-height:0}.app-header-primary{height:var(--bs-app-header-primary-height)}[data-kt-app-header-primary-fixed=true] .app-header-primary{z-index:100;position:fixed;left:0;right:0;top:0}[data-kt-app-header-primary-static=true] .app-header-primary{position:relative}[data-kt-app-header-primary-sticky=on] .app-header-primary{position:fixed;left:0;right:0;top:0;height:70px;z-index:100;background-color:var(--bs-app-header-primary-sticky-bg-color);box-shadow:var(--bs-app-header-primary-sticky-box-shadow);border-bottom:var(--bs-app-header-primary-sticky-border-bottom)}[data-kt-app-header-primary-minimize=on] .app-header-primary{transition:none;height:70px;z-index:100;background-color:var(--bs-app-header-primary-minimize-bg-color);box-shadow:var(--bs-app-header-primary-minimize-box-shadow);border-bottom:var(--bs-app-header-primary-minimize-border-bottom)}[data-kt-app-header-sticky=on][data-kt-app-header-primary-sticky-hide=true] .app-header-primary{display:none!important}[data-kt-app-header-primary-enabled=true][data-kt-app-sidebar-fixed=true][data-kt-app-sidebar-push-header=true] .app-header-primary{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}[data-kt-app-header-primary-enabled=true][data-kt-app-sidebar-panel-fixed=true][data-kt-app-sidebar-panel-push-header=true] .app-header-primary{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}}@media (max-width:991.98px){.app-header .app-header-primary{flex-grow:1;height:var(--bs-app-header-primary-height);border-bottom:var(--bs-app-header-primary-base-border-bottom);box-shadow:var(--bs-app-header-primary-base-box-shadow)}.app-header-primary{background-color:var(--bs-app-header-primary-base-bg-color)}[data-kt-app-header-primary-enabled=true]{--bs-app-header-primary-height:60px}[data-kt-app-header-primary-enabled=true][data-kt-app-header-sticky=on]{--bs-app-header-primary-height:60px}[data-kt-app-header-primary-enabled=true][data-kt-app-header-minimize=on]{--bs-app-header-primary-height:60px}}.app-header-secondary{transition:none;display:flex;align-items:stretch}@media (min-width:992px){.app-header-secondary{background-color:var(--bs-app-header-secondary-base-bg-color);box-shadow:var(--bs-app-header-secondary-base-box-shadow);border-top:var(--bs-app-header-secondary-base-border-top);border-bottom:var(--bs-app-header-secondary-base-border-bottom)}[data-kt-app-header-secondary-enabled=true]{--bs-app-header-secondary-height:70px}[data-kt-app-header-secondary-enabled=true][data-kt-app-header-sticky=on]{--bs-app-header-secondary-height:70px}[data-kt-app-header-secondary-enabled=true][data-kt-app-header-minimize=on]{--bs-app-header-secondary-height:70px}[data-kt-app-header-secondary-enabled=true][data-kt-app-header-sticky=on][data-kt-app-header-secondary-sticky-hide=true]{--bs-app-header-secondary-height:0}.app-header-secondary{height:var(--bs-app-header-secondary-height)}[data-kt-app-header-secondary-fixed=true] .app-header-secondary{z-index:100;position:fixed;left:0;right:0;top:0}[data-kt-app-header-secondary-static=true] .app-header-secondary{position:static}[data-kt-app-header-secondary-sticky=on] .app-header-secondary{transition:none;position:fixed;left:0;right:0;top:0;height:70px;z-index:100;background-color:var(--bs-app-header-secondary-sticky-bg-color);box-shadow:var(--bs-app-header-secondary-sticky-box-shadow);border-bottom:var(--bs-app-header-secondary-sticky-border-bottom)}[data-kt-app-header-secondary-minimize=on] .app-header-secondary{transition:none;height:70px;z-index:100;background-color:var(--bs-app-header-secondary-minimize-bg-color);box-shadow:var(--bs-app-header-secondary-minimize-box-shadow);border-bottom:var(--bs-app-header-secondary-minimize-border-bottom)}[data-kt-app-header-sticky=on][data-kt-app-header-secondary-sticky-hide=true] .app-header-secondary{display:none!important}[data-kt-app-header-secondary-enabled=true][data-kt-app-sidebar-fixed=true][data-kt-app-sidebar-push-header=true] .app-header-secondary{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}[data-kt-app-header-secondary-enabled=true][data-kt-app-sidebar-panel-fixed=true][data-kt-app-sidebar-panel-push-header=true] .app-header-secondary{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}}@media (max-width:991.98px){.app-header .app-header-secondary{flex-grow:1;height:var(--bs-app-header-secondary-height);box-shadow:var(--bs-app-header-secondary-base-box-shadow);border-top:var(--bs-app-header-secondary-base-border-top);border-bottom:var(--bs-app-header-secondary-base-border-bottom)}.app-header-secondary{background-color:var(--bs-app-header-secondary-base-bg-color)}[data-kt-app-header-secondary-enabled=true]{--bs-app-header-secondary-height:60px}[data-kt-app-header-secondary-enabled=true][data-kt-app-header-sticky=on]{--bs-app-header-secondary-height:60px}[data-kt-app-header-secondary-enabled=true][data-kt-app-header-minimize=on]{--bs-app-header-secondary-height:60px}}.app-header-tertiary{transition:none;display:flex;align-items:stretch}@media (min-width:992px){.app-header-tertiary{background-color:var(--bs-app-header-tertiary-base-bg-color);box-shadow:var(--bs-app-header-tertiary-base-box-shadow);border-top:var(--bs-app-header-tertiary-base-border-top);border-bottom:var(--bs-app-header-tertiary-base-border-bottom)}[data-kt-app-header-tertiary-enabled=true]{--bs-app-header-tertiary-height:70px}[data-kt-app-header-tertiary-enabled=true][data-kt-app-header-sticky=on]{--bs-app-header-tertiary-height:70px}[data-kt-app-header-tertiary-enabled=true][data-kt-app-header-minimize=on]{--bs-app-header-tertiary-height:70px}[data-kt-app-header-tertiary-enabled=true][data-kt-app-header-sticky=on][data-kt-app-header-tertiary-sticky-hide=true]{--bs-app-header-tertiary-height:0}.app-header-tertiary{height:var(--bs-app-header-tertiary-height)}[data-kt-app-header-tertiary-fixed=true] .app-header-tertiary{z-index:100;position:fixed;left:0;right:0;top:0}[data-kt-app-header-tertiary-static=true] .app-header-tertiary{position:static}[data-kt-app-header-tertiary-sticky=on] .app-header-tertiary{transition:none;position:fixed;left:0;right:0;top:0;height:70px;z-index:100;background-color:var(--bs-app-header-tertiary-sticky-bg-color);box-shadow:var(--bs-app-header-tertiary-sticky-box-shadow);border-bottom:var(--bs-app-header-tertiary-sticky-border-bottom)}[data-kt-app-header-tertiary-minimize=on] .app-header-tertiary{transition:none;height:70px;z-index:100;background-color:var(--bs-app-header-tertiary-minimize-bg-color);box-shadow:var(--bs-app-header-tertiary-minimize-box-shadow);border-bottom:var(--bs-app-header-tertiary-minimize-border-bottom)}[data-kt-app-header-sticky=on][data-kt-app-header-tertiary-sticky-hide=true] .app-header-tertiary{display:none!important}[data-kt-app-header-tertiary-enabled=true][data-kt-app-sidebar-fixed=true][data-kt-app-sidebar-push-header=true] .app-header-tertiary{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}[data-kt-app-header-tertiary-enabled=true][data-kt-app-sidebar-panel-fixed=true][data-kt-app-sidebar-panel-push-header=true] .app-header-tertiary{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}}@media (max-width:991.98px){.app-header .app-header-tertiary{flex-grow:1;height:var(--bs-app-header-tertiary-height);border-top:var(--bs-app-header-tertiary-base-border-top);box-shadow:var(--bs-app-header-tertiary-base-box-shadow)}.app-header-tertiary{background-color:var(--bs-app-header-tertiary-base-bg-color)}[data-kt-app-header-tertiary-enabled=true]{--bs-app-header-tertiary-height:70px}[data-kt-app-header-tertiary-enabled=true][data-kt-app-header-sticky=on]{--bs-app-header-tertiary-height:60px}[data-kt-app-header-secondary-enabled=true][data-kt-app-header-minimize=on]{--bs-app-header-tertiary-height:60px}}.app-toolbar{display:flex;align-items:stretch;background-color:var(--bs-app-toolbar-base-bg-color);box-shadow:var(--bs-app-toolbar-base-box-shadow);border-top:var(--bs-app-toolbar-base-border-top);border-bottom:var(--bs-app-toolbar-base-border-bottom)}.app-toolbar.app-toolbar-minimize{transition:none}@media (min-width:992px){:root{--bs-app-toolbar-height:auto;--bs-app-toolbar-height-actual:auto}[data-kt-app-toolbar-sticky=on]{--bs-app-toolbar-height:70px}[data-kt-app-toolbar-minimize=on]{--bs-app-toolbar-height:70px}.app-toolbar{height:var(--bs-app-toolbar-height)}[data-kt-app-header-fixed=true][data-kt-app-toolbar-fixed=true] .app-toolbar{z-index:99;position:fixed;left:0;right:0;top:0}[data-kt-app-toolbar-sticky=on] .app-toolbar{position:fixed;left:0;right:0;top:0;box-shadow:var(--bs-app-toolbar-sticky-box-shadow);background-color:var(--bs-app-toolbar-sticky-bg-color);border-top:var(--bs-app-toolbar-sticky-border-top);border-bottom:var(--bs-app-toolbar-sticky-border-bottom);z-index:99}[data-kt-app-toolbar-minimize=on] .app-toolbar{transition:none;box-shadow:var(--bs-app-toolbar-minimize-box-shadow);background-color:var(--bs-app-toolbar-minimize-bg-color);border-top:var(--bs-app-toolbar-minimize-border-top);border-bottom:var(--bs-app-toolbar-minimize-border-bottom);z-index:99}[data-kt-app-toolbar-fixed=true][data-kt-app-header-fixed=true] .app-toolbar{top:var(--bs-app-header-height)}[data-kt-app-toolbar-fixed=true][data-kt-app-sidebar-fixed=true][data-kt-app-sidebar-push-toolbar=true] .app-toolbar,[data-kt-app-toolbar-sticky=on][data-kt-app-sidebar-fixed=true][data-kt-app-sidebar-push-toolbar=true] .app-toolbar{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}[data-kt-app-toolbar-fixed=true][data-kt-app-sidebar-panel-fixed=true][data-kt-app-sidebar-panel-push-toolbar=true] .app-toolbar,[data-kt-app-toolbar-sticky=on][data-kt-app-sidebar-panel-fixed=true][data-kt-app-sidebar-panel-push-toolbar=true] .app-toolbar{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}[data-kt-app-toolbar-fixed=true][data-kt-app-aside-fixed=true][data-kt-app-aside-push-toolbar=true] .app-toolbar,[data-kt-app-toolbar-sticky=on][data-kt-app-aside-fixed=true][data-kt-app-aside-push-toolbar=true] .app-toolbar{right:calc(var(--bs-app-aside-width) + var(--bs-app-aside-gap-start,0px) + var(--bs-app-aside-gap-end,0px))}}@media (max-width:991.98px){:root{--bs-app-toolbar-height:auto}[data-kt-app-toolbar-sticky=on]{--bs-app-toolbar-height:70px}[data-kt-app-toolbar-minimize=on]{--bs-app-toolbar-height:70px}.app-toolbar{height:var(--bs-app-toolbar-height)}[data-kt-app-header-fixed-mobile=true][data-kt-app-toolbar-fixed-mobile=true] .app-toolbar{z-index:99;position:fixed;top:calc(var(--bs-app-header-height,0px) + var(--bs-app-header-mobile-height,0px));left:0;right:0}[data-kt-app-toolbar-sticky=on] .app-toolbar{position:fixed;left:0;right:0;top:var(--bs-app-header-height,0);box-shadow:var(--bs-app-toolbar-sticky-box-shadow);background-color:var(--bs-app-toolbar-sticky-bg-color);border-top:var(--bs-app-toolbar-sticky-border-top);border-bottom:var(--bs-app-toolbar-sticky-border-bottom);z-index:99}[data-kt-app-toolbar-minimize=on] .app-toolbar{transition:none;box-shadow:var(--bs-app-toolbar-minimize-box-shadow);background-color:var(--bs-app-toolbar-minimize-bg-color);border-top:var(--bs-app-toolbar-minimize-border-top);border-bottom:var(--bs-app-toolbar-minimize-border-bottom);z-index:99}}.app-hero{display:flex;align-items:stretch;background-color:var(--bs-app-hero-bg-color);box-shadow:var(--bs-app-hero-box-shadow);border-top:var(--bs-app-hero-border-top);border-bottom:var(--bs-app-hero-border-bottom)}.app-sidebar{transition:width .3s ease;background-color:var(--bs-app-sidebar-base-bg-color);box-shadow:var(--bs-app-sidebar-base-box-shadow);border-left:var(--bs-app-sidebar-base-border-start);border-right:var(--bs-app-sidebar-base-border-end)}.app-sidebar-collapse-d-flex,.app-sidebar-collapse-mobile-d-flex,.app-sidebar-minimize-d-flex,.app-sidebar-minimize-mobile-d-flex,.app-sidebar-sticky-d-flex{display:none}@media (min-width:992px){.app-sidebar{display:flex;flex-shrink:0;width:var(--bs-app-sidebar-width);margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}:root{--bs-app-sidebar-width:300px;--bs-app-sidebar-width-actual:300px;--bs-app-sidebar-gap-start:0px;--bs-app-sidebar-gap-end:0px;--bs-app-sidebar-gap-top:0px;--bs-app-sidebar-gap-bottom:0px}[data-kt-app-sidebar-stacked=true]{--bs-app-sidebar-width:calc(var(--bs-app-sidebar-primary-width) + var(--bs-app-sidebar-secondary-width, 0px))}[data-kt-app-sidebar-minimize=on]{--bs-app-sidebar-width:75px;--bs-app-sidebar-gap-start:0px;--bs-app-sidebar-gap-end:0px;--bs-app-sidebar-gap-top:0px;--bs-app-sidebar-gap-bottom:0px}[data-kt-app-sidebar-sticky=on]{--bs-app-sidebar-width:300px;--bs-app-sidebar-gap-start:0px;--bs-app-sidebar-gap-end:0px;--bs-app-sidebar-gap-top:0px;--bs-app-sidebar-gap-bottom:0px}[data-kt-app-sidebar-collapse=on]{--bs-app-sidebar-width:0px}[data-kt-app-sidebar-static=true] .app-sidebar{position:relative}[data-kt-app-sidebar-offcanvas=true] .app-sidebar{display:none}[data-kt-app-sidebar-fixed=true] .app-sidebar{position:fixed;z-index:105;top:0;bottom:0;left:0}[data-kt-app-sidebar-stacked=true] .app-sidebar{align-items:stretch}[data-kt-app-sidebar-sticky=on] .app-sidebar{position:fixed;transition:width .3s ease;top:auto;bottom:auto;left:auto;z-index:105;box-shadow:var(--bs-app-sidebar-sticky-box-shadow);border-left:var(--bs-app-sidebar-sticky-border-start);border-right:var(--bs-app-sidebar-sticky-border-end);margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-minimize=on] .app-sidebar{transition:width .3s ease;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-hoverable=true] .app-sidebar .app-sidebar-wrapper{width:var(--bs-app-sidebar-width-actual)}[data-kt-app-sidebar-hoverable=true][data-kt-app-sidebar-minimize=on] .app-sidebar:hover:not(.animating){transition:width .3s ease;width:var(--bs-app-sidebar-width-actual)}[data-kt-app-sidebar-collapse=on] .app-sidebar{transition:width .3s ease;width:var(--bs-app-sidebar-width-actual);margin-left:calc(-1 * var(--bs-app-sidebar-width-actual))}[data-kt-app-sidebar-minimize=on] .app-sidebar-minimize-d-none{display:none!important}[data-kt-app-sidebar-minimize=on] .app-sidebar-minimize-d-flex{display:flex!important}[data-kt-app-sidebar-sticky=on] .app-sidebar-sticky-d-none{display:none!important}[data-kt-app-sidebar-sticky=on] .app-sidebar-sticky-d-flex{display:flex!important}[data-kt-app-sidebar-collapse=on] .app-sidebar-collapse-d-none{display:none!important}[data-kt-app-sidebar-collapse=on] .app-sidebar-collapse-d-flex{display:flex!important}[data-kt-app-sidebar-fixed=true][data-kt-app-header-fixed=true]:not([data-kt-app-sidebar-push-header=true]) .app-sidebar{top:var(--bs-app-header-height)}[data-kt-app-sidebar-fixed=true][data-kt-app-header-fixed=true][data-kt-app-toolbar-fixed=true]:not([data-kt-app-sidebar-push-toolbar=true]) .app-sidebar{top:calc(var(--bs-app-header-height) + var(--bs-app-toolbar-height,0px))}}@media (max-width:991.98px){.app-sidebar{display:none;width:var(--bs-app-sidebar-width);z-index:106;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}:root{--bs-app-sidebar-width:275px;--bs-app-sidebar-width-actual:275px;--bs-app-sidebar-gap-start:0px;--bs-app-sidebar-gap-end:0px;--bs-app-sidebar-gap-top:0px;--bs-app-sidebar-gap-bottom:0px}[data-kt-app-sidebar-stacked=true]{--bs-app-sidebar-width:calc(var(--bs-app-sidebar-primary-width) + var(--bs-app-sidebar-secondary-width, 0))}[data-kt-app-sidebar-minimize-mobile=on]{--bs-app-sidebar-width:75px;--bs-app-sidebar-gap-start:0px;--bs-app-sidebar-gap-end:0px;--bs-app-sidebar-gap-top:0px;--bs-app-sidebar-gap-bottom:0px}[data-kt-app-sidebar-collapse-mobile=on]{--bs-app-sidebar-width:0px}[data-kt-app-sidebar-stacked=true] .app-sidebar{align-items:stretch}[data-kt-app-sidebar-minimize-mobile=on] .app-sidebar{transition:width .3s ease;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-hoverable-mobile=true] .app-sidebar .app-sidebar-wrapper{width:var(--bs-app-sidebar-width-actual)}[data-kt-app-sidebar-hoverable-mobile=true][data-kt-app-sidebar-minimize-mobile=on] .app-sidebar:hover:not(.animating){transition:width .3s ease;width:var(--bs-app-sidebar-width-actual);box-shadow:var(--bs-app-sidebar-minimize-hover-box-shadow-mobile)}[data-kt-app-sidebar-collapse-mobile=on] .app-sidebar{transition:width .3s ease;width:var(--bs-app-sidebar-width-actual);margin-left:calc(-1 * var(--bs-app-sidebar-width-actual))}[data-kt-app-sidebar-minimize-mobile=on] .app-sidebar-minimize-mobile-d-none{display:none!important}[data-kt-app-sidebar-minimize-mobile=on] .app-sidebar-minimize-mobile-d-flex{display:flex!important}[data-kt-app-sidebar-collapse-mobile=on] .app-sidebar-collapse-mobile-d-none{display:none!important}[data-kt-app-sidebar-collapse-mobile=on] .app-sidebar-collapse-mobile-d-flex{display:flex!important}}.app-sidebar-primary{transition:none;position:relative;flex-shrink:0;background-color:var(--bs-app-sidebar-primary-base-bg-color);box-shadow:var(--bs-app-sidebar-primary-base-box-shadow);border-left:var(--bs-app-sidebar-primary-base-border-start);border-right:var(--bs-app-sidebar-primary-base-border-end)}.app-sidebar-primary-collapse-d-flex,.app-sidebar-primary-collapse-mobile-d-flex,.app-sidebar-primary-minimize-d-flex,.app-sidebar-primary-minimize-mobile-d-flex{display:none}@media (min-width:992px){.app-sidebar-primary{z-index:1;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}:root{--bs-app-sidebar-primary-width-actual:100px}[data-kt-app-sidebar-stacked=true]{--bs-app-sidebar-primary-width:100px;--bs-app-sidebar-primary-gap-start:0px;--bs-app-sidebar-primary-gap-end:0px;--bs-app-sidebar-primary-gap-top:0px;--bs-app-sidebar-primary-gap-bottom:0px}[data-kt-app-sidebar-primary-minimize=on]{--bs-app-sidebar-primary-width:75px;--bs-app-sidebar-primary-gap-start:0px;--bs-app-sidebar-primary-gap-end:0px;--bs-app-sidebar-primary-gap-top:0px;--bs-app-sidebar-primary-gap-bottom:0px}[data-kt-app-sidebar-primary-collapse=on]{--bs-app-sidebar-primary-width:0px}.app-sidebar-primary{width:var(--bs-app-sidebar-primary-width)}[data-kt-app-sidebar-primary-collapse=on] .app-sidebar-primary{transition:none;width:var(--bs-app-sidebar-primary-width-actual);margin-left:calc(-1 * var(--bs-app-sidebar-primary-width-actual))}[data-kt-app-sidebar-primary-minimize=on] .app-sidebar-primary{transition:none;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-primary-hoverable=true] .app-sidebar-primary .app-sidebar-primary-hoverable{width:var(--bs-app-sidebar-primary-width-actual)}[data-kt-app-sidebar-primary-hoverable=true][data-kt-app-sidebar-primary-minimize=on] .app-sidebar-primary:hover:not(.animating){transition:none;width:var(--bs-app-sidebar-primary-width-actual);box-shadow:var(--bs-app-sidebar-primary-minimize-hover-box-shadow)}[data-kt-app-sidebar-fixed=true][data-kt-app-header-fixed=true][data-kt-app-sidebar-primary-below-header=true] .app-sidebar-primary{top:var(--bs-app-header-height)}[data-kt-app-sidebar-fixed=true][data-kt-app-header-fixed=true][data-kt-app-toolbar-fixed=true][data-kt-app-sidebar-primary-below-toolbar=true] .app-sidebar-primary{top:calc(var(--bs-app-header-height) + var(--bs-app-toolbar-height,0))}[data-kt-app-sidebar-primary-minimize=on] .app-sidebar-primary-minimize-d-none{display:none!important}[data-kt-app-sidebar-primary-minimize=on] .app-sidebar-primary-minimize-d-flex{display:flex!important}[data-kt-app-sidebar-primary-collapse=on] .app-sidebar-primary-collapse-d-none{display:none!important}[data-kt-app-sidebar-primary-collapse=on] .app-sidebar-primary-collapse-d-flex{display:flex!important}}@media (max-width:991.98px){.app-sidebar-primary{z-index:1;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}:root{--bs-app-sidebar-primary-width-actual:100px}[data-kt-app-sidebar-stacked=true]{--bs-app-sidebar-primary-width:100px;--bs-app-sidebar-primary-gap-start:0px;--bs-app-sidebar-primary-gap-end:0px;--bs-app-sidebar-primary-gap-top:0px;--bs-app-sidebar-primary-gap-bottom:0px}[data-kt-app-sidebar-primary-minimize-mobile=on]{--bs-app-sidebar-primary-width:75px;--bs-app-sidebar-primary-gap-start:0px;--bs-app-sidebar-primary-gap-end:0px;--bs-app-sidebar-primary-gap-top:0px;--bs-app-sidebar-primary-gap-bottom:0px}[data-kt-app-sidebar-primary-collapse-mobile=on]{--bs-app-sidebar-primary-width:0px}.app-sidebar-primary{width:var(--bs-app-sidebar-primary-width)}[data-kt-app-sidebar-primary-collapse-mobile=on] .app-sidebar-primary{transition:none;width:var(--bs-app-sidebar-primary-width-actual);margin-left:calc(-1 * var(--bs-app-sidebar-primary-width-actual))}[data-kt-app-sidebar-primary-minimize-mobile=on] .app-sidebar-primary{transition:none;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-primary-hoverable-mobile=true] .app-sidebar-primary .app-sidebar-primary-hoverable{width:var(--bs-app-sidebar-primary-width-actual)}[data-kt-app-sidebar-primary-hoverable-mobile=true][data-kt-app-sidebar-primary-minimize-mobile=on] .app-sidebar-primary:hover:not(.animating){transition:none;width:var(--bs-app-sidebar-primary-width-actual);box-shadow:var(--bs-app-sidebar-primary-minimize-hover-box-shadow-mobile)}[data-kt-app-sidebar-primary-minimize-mobile=on] .app-sidebar-primary-minimize-mobile-d-none{display:none!important}[data-kt-app-sidebar-primary-minimize-mobile=on] .app-sidebar-primary-minimize-mobile-d-flex{display:flex!important}[data-kt-app-sidebar-primary-collapse-mobile=on] .app-sidebar-primary-collapse-mobile-d-none{display:none!important}[data-kt-app-sidebar-primary-collapse-mobile=on] .app-sidebar-primary-collapse-mobile-d-flex{display:flex!important}}.app-sidebar-secondary{transition:width .3s ease,margin .3s ease;position:relative;flex-shrink:0;background-color:var(--bs-app-sidebar-secondary-base-bg-color);box-shadow:var(--bs-app-sidebar-secondary-base-box-shadow);border-left:var(--bs-app-sidebar-secondary-base-border-start);border-right:var(--bs-app-sidebar-secondary-base-border-end)}.app-sidebar-secondary-collapse-d-flex,.app-sidebar-secondary-collapse-mobile-d-flex,.app-sidebar-secondary-minimize-d-flex,.app-sidebar-secondary-minimize-mobile-d-flex{display:none}@media (min-width:992px){.app-sidebar-secondary{margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-secondary-enabled=true]{--bs-app-sidebar-secondary-width-actual:calc(\n        300px -\n        100px -\n        0px -\n        0px -\n        0px -\n        0px\n    )}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-stacked=true]{--bs-app-sidebar-secondary-width:calc(\n        300px -\n        100px -\n        0px -\n        0px -\n        0px -\n        0px\n    );--bs-app-sidebar-secondary-gap-start:0px;--bs-app-sidebar-secondary-gap-end:0px;--bs-app-sidebar-secondary-gap-top:0px;--bs-app-sidebar-secondary-gap-bottom:0px}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-minimize=on]{--bs-app-sidebar-secondary-width:75px;--bs-app-sidebar-secondary-gap-start:0px;--bs-app-sidebar-secondary-gap-end:0px;--bs-app-sidebar-secondary-gap-top:0px;--bs-app-sidebar-secondary-gap-bottom:0px}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-collapse=on]{--bs-app-sidebar-secondary-width-actual:calc(\n        300px -\n        100px -\n        0px -\n        0px -\n        0px -\n        0px\n    );--bs-app-sidebar-secondary-width:0px}.app-sidebar-secondary{width:var(--bs-app-sidebar-secondary-width)}[data-kt-app-sidebar-secondary-collapse=on] .app-sidebar-secondary{transition:width .3s ease,margin .3s ease;width:var(--bs-app-sidebar-secondary-width-actual);margin-left:calc(-1 * var(--bs-app-sidebar-secondary-width-actual))}[data-kt-app-sidebar-secondary-minimize=on] .app-sidebar-secondary{transition:width .3s ease,margin .3s ease;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-secondary-hoverable=true] .app-sidebar-secondary .app-sidebar-secondary-hoverable{width:var(--bs-app-sidebar-secondary-width-actual)}[data-kt-app-sidebar-secondary-hoverable=true][data-kt-app-sidebar-secondary-minimize=on] .app-sidebar-secondary:hover:not(.animating){transition:width .3s ease,margin .3s ease;width:var(--bs-app-sidebar-secondary-width-actual);box-shadow:var(--bs-app-sidebar-secondary-minimize-hover-box-shadow)}[data-kt-app-sidebar-fixed=true][data-kt-app-header-fixed=true][data-kt-app-sidebar-secondary-below-header=true] .app-sidebar-secondary{top:var(--bs-app-header-height)}[data-kt-app-sidebar-fixed=true][data-kt-app-header-fixed=true][data-kt-app-toolbar-fixed=true][data-kt-app-sidebar-secondary-below-toolbar=true] .app-sidebar-secondary{top:calc(var(--bs-app-header-height) + var(--bs-app-toolbar-height,0))}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-minimize=on] .app-sidebar-secondary-minimize-d-none{display:none!important}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-minimize=on] .app-sidebar-secondary-minimize-d-flex{display:flex!important}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-collapse=on] .app-sidebar-secondary-collapse-d-none{display:none!important}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-collapse=on] .app-sidebar-secondary-collapse-d-flex{display:flex!important}}@media (max-width:991.98px){.app-sidebar-secondary{margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-secondary-enabled=true]{--bs-app-sidebar-secondary-width-actual:calc(\n        275px -\n        100px -\n        0px -\n        0px -\n        0px -\n        0px\n    )}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-stacked=true]{--bs-app-sidebar-secondary-width:calc(\n        275px -\n        100px -\n        0px -\n        0px -\n        0px -\n        0px\n    );--bs-app-sidebar-secondary-gap-start:0px;--bs-app-sidebar-secondary-gap-end:0px;--bs-app-sidebar-secondary-gap-top:0px;--bs-app-sidebar-secondary-gap-bottom:0px}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-minimize-mobile=on]{--bs-app-sidebar-secondary-width:75px;--bs-app-sidebar-secondary-gap-start:0px;--bs-app-sidebar-secondary-gap-end:0px;--bs-app-sidebar-secondary-gap-top:0px;--bs-app-sidebar-secondary-gap-bottom:0px}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-collapse-mobile=on]{--bs-app-sidebar-secondary-width-actual:calc(\n        275px -\n        100px -\n        0px -\n        0px -\n        0px -\n        0px\n    );--bs-app-sidebar-secondary-width:0px}.app-sidebar-secondary{width:var(--bs-app-sidebar-secondary-width)}[data-kt-app-sidebar-secondary-collapse-mobile=on] .app-sidebar-secondary{transition:width .3s ease,margin .3s ease;width:var(--bs-app-sidebar-secondary-width-actual);margin-left:calc(-1 * var(--bs-app-sidebar-secondary-width-actual))}[data-kt-app-sidebar-secondary-minimize-mobile=on] .app-sidebar-secondary{transition:width .3s ease,margin .3s ease;background-color:var(--bs-app-sidebar-secondary-minimize-bg-color-mobile);box-shadow:var(--bs-app-sidebar-secondary-minimize-box-shadow-mobile);border-left:var(--bs-app-sidebar-secondary-minimize-border-start-mobile);border-right:var(--bs-app-sidebar-secondary-minimize-border-end-mobile)}[data-kt-app-sidebar-secondary-hoverable-mobile=true] .app-sidebar-secondary .app-sidebar-secondary-hoverable{width:var(--bs-app-sidebar-secondary-width-actual)}[data-kt-app-sidebar-secondary-hoverable-mobile=true][data-kt-app-sidebar-secondary-minimize-mobile=on] .app-sidebar-secondary:hover:not(.animating){transition:width .3s ease,margin .3s ease;width:var(--bs-app-sidebar-secondary-width-actual);box-shadow:var(--bs-app-sidebar-secondary-minimize-hover-box-shadow-mobile)}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-minimize-mobile=on] .app-sidebar-secondary-minimize-mobile-d-none{display:none!important}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-minimize-mobile=on] .app-sidebar-secondary-minimize-mobile-d-flex{display:flex!important}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-collapse=on] .app-sidebar-secondary-collapse-mobile-d-none{display:none!important}[data-kt-app-sidebar-secondary-enabled=true][data-kt-app-sidebar-secondary-collapse=on] .app-sidebar-secondary-collapse-mobile-d-flex{display:flex!important}}.app-sidebar-panel{transition:none;background-color:var(--bs-app-sidebar-panel-base-bg-color);box-shadow:var(--bs-app-sidebar-panel-base-box-shadow);border-left:var(--bs-app-sidebar-panel-base-border-start);border-right:var(--bs-app-sidebar-panel-base-border-end)}.app-sidebar-panel-collapse-d-flex,.app-sidebar-panel-collapse-mobile-d-flex,.app-sidebar-panel-minimize-d-flex,.app-sidebar-panel-minimize-mobile-d-flex,.app-sidebar-panel-sticky-d-flex{display:none}@media (min-width:992px){.app-sidebar-panel{display:flex;flex-shrink:0;width:var(--bs-app-sidebar-panel-width);margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}:root{--bs-app-sidebar-panel-width:300px;--bs-app-sidebar-panel-width-actual:300px;--bs-app-sidebar-panel-gap-start:0px;--bs-app-sidebar-panel-gap-end:0px;--bs-app-sidebar-panel-gap-top:0px;--bs-app-sidebar-panel-gap-bottom:0px}[data-kt-app-sidebar-panel-minimize=on]{--bs-app-sidebar-panel-width:75px;--bs-app-sidebar-panel-gap-start:0px;--bs-app-sidebar-panel-gap-end:0px;--bs-app-sidebar-panel-gap-top:0px;--bs-app-sidebar-panel-gap-bottom:0px}[data-kt-app-sidebar-panel-sticky=on]{--bs-app-sidebar-panel-width:300px;--bs-app-sidebar-panel-gap-start:0px;--bs-app-sidebar-panel-gap-end:0px;--bs-app-sidebar-panel-gap-top:0px;--bs-app-sidebar-panel-gap-bottom:0px}[data-kt-app-sidebar-panel-collapse=on]{--bs-app-sidebar-panel-width-actual:300px;--bs-app-sidebar-panel-width:0px}[data-kt-app-sidebar-panel-static=true] .app-sidebar-panel{position:relative}[data-kt-app-sidebar-panel-offcanvas=true] .app-sidebar-panel{display:none}[data-kt-app-sidebar-panel-fixed=true] .app-sidebar-panel{z-index:104;position:fixed;left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px));top:0;bottom:0}[data-kt-app-sidebar-panel-sticky=on] .app-sidebar-panel{position:fixed;left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px));top:0;bottom:0;transition:none;z-index:104;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-panel-minimize=on] .app-sidebar-panel{transition:none;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-panel-hoverable=true] .app-sidebar-panel .app-sidebar-panel-hoverable{width:var(--bs-app-sidebar-panel-width-actual)}[data-kt-app-sidebar-panel-hoverable=true][data-kt-app-sidebar-panel-minimize=on] .app-sidebar-panel:hover:not(.animating){transition:none;width:var(--bs-app-sidebar-panel-width-actual);box-shadow:var(--bs-app-sidebar-panel-minimize-hover-box-shadow)}[data-kt-app-sidebar-panel-collapse=on] .app-sidebar-panel{transition:none;width:var(--bs-app-sidebar-panel-width-actual);margin-left:calc(-1 * (var(--bs-app-sidebar-panel-width-actual) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px)))}[data-kt-app-sidebar-panel-fixed=true][data-kt-app-header-fixed=true]:not([data-kt-app-sidebar-panel-push-header=true]) .app-sidebar-panel{top:var(--bs-app-header-height)}[data-kt-app-sidebar-panel-fixed=true][data-kt-app-header-fixed=true][data-kt-app-toolbar-fixed=true]:not([data-kt-app-sidebar-panel-push-toolbar=true]) .app-sidebar-panel{top:calc(var(--bs-app-header-height) + var(--bs-app-toolbar-height,0))}[data-kt-app-sidebar-panel-minimize=on] .app-sidebar-panel-minimize-d-none{display:none!important}[data-kt-app-sidebar-panel-minimize=on] .app-sidebar-panel-minimize-d-flex{display:flex!important}[data-kt-app-sidebar-panel-sticky=on] .app-sidebar-panel-sticky-d-none{display:none!important}[data-kt-app-sidebar-panel-sticky=on] .app-sidebar-panel-sticky-d-flex{display:flex!important}[data-kt-app-sidebar-panel-collapse=on] .app-sidebar-panel-collapse-d-none{display:none!important}[data-kt-app-sidebar-panel-collapse=on] .app-sidebar-panel-collapse-d-flex{display:flex!important}}@media (max-width:991.98px){.app-sidebar-panel{display:none;width:var(--bs-app-sidebar-panel-width);margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}:root{--bs-app-sidebar-panel-width:300px;--bs-app-sidebar-panel-width-actual:300px;--bs-app-sidebar-panel-gap-start:0px;--bs-app-sidebar-panel-gap-end:0px;--bs-app-sidebar-panel-gap-top:0px;--bs-app-sidebar-panel-gap-bottom:0px}[data-kt-app-sidebar-panel-minimize-mobile=on]{--bs-app-sidebar-panel-width:75px;--bs-app-sidebar-panel-gap-start:0px;--bs-app-sidebar-panel-gap-end:0px;--bs-app-sidebar-panel-gap-top:0px;--bs-app-sidebar-panel-gap-bottom:0px}[data-kt-app-sidebar-panel-collapse-mobile=on]{--bs-app-sidebar-panel-width-actual:300px;--bs-app-sidebar-panel-width:0px}[data-kt-app-sidebar-panel-minimize-mobile=on] .app-sidebar-panel{transition:none;background-color:var(--bs-app-sidebar-panel-minimize-bg-color-mobile);box-shadow:var(--bs-app-sidebar-panel-minimize-box-shadow-mobile);border-left:var(--bs-app-sidebar-panel-minimize-border-start-mobile);border-right:var(--bs-app-sidebar-panel-minimize-border-end-mobile);margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-sidebar-panel-hoverable-mobile=true] .app-sidebar-panel .app-sidebar-panel-hoverable{width:var(--bs-app-sidebar-panel-width-actual)}[data-kt-app-sidebar-panel-hoverable-mobile=true][data-kt-app-sidebar-panel-minimize-mobile=on] .app-sidebar-panel:hover:not(.animating){transition:none;width:var(--bs-app-sidebar-panel-width-actual);box-shadow:var(--bs-app-sidebar-panel-minimize-hover-box-shadow-mobile)}[data-kt-app-sidebar-panel-collapse-mobile=on] .app-sidebar-panel{transition:none;width:var(--bs-app-sidebar-panel-width-actual);margin-left:calc(-1 * var(--bs-app-sidebar-panel-width-actual))}[data-kt-app-sidebar-panel-minimize-mobile=on] .app-sidebar-panel-minimize-mobile-d-none{display:none!important}[data-kt-app-sidebar-panel-minimize-mobile=on] .app-sidebar-panel-minimize-mobile-d-flex{display:flex!important}[data-kt-app-sidebar-panel-collapse-mobile=on] .app-sidebar-panel-collapse-mobile-d-none{display:none!important}[data-kt-app-sidebar-panel-collapse-mobile=on] .app-sidebar-panel-collapse-mobile-d-flex{display:flex!important}}.app-aside{transition:none;background-color:var(--bs-app-aside-base-bg-color);box-shadow:var(--bs-app-aside-base-box-shadow)}.app-aside-collapse-d-flex,.app-aside-collapse-mobile-d-flex,.app-aside-minimize-d-flex,.app-aside-minimize-mobile-d-flex,.app-aside-sticky-d-flex{display:none}@media (min-width:992px){.app-aside{display:flex;flex-shrink:0;width:var(--bs-app-aside-width);margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}:root{--bs-app-aside-width:300px;--bs-app-aside-width-actual:300px;--bs-app-aside-gap-start:0px;--bs-app-aside-gap-end:0px;--bs-app-aside-gap-top:0px;--bs-app-aside-gap-bottom:0px}[data-kt-app-aside-stacked=true]{--bs-app-aside-width:calc(var(--bs-app-aside-primary-width) + var(--bs-app-aside-secondary-width))}[data-kt-app-aside-minimize=on]{--bs-app-aside-width:75px;--bs-app-aside-gap-start:0px;--bs-app-aside-gap-end:0px;--bs-app-aside-gap-top:0px;--bs-app-aside-gap-bottom:0px}[data-kt-app-aside-sticky=on]{--bs-app-aside-width:300px;--bs-app-aside-gap-start:0px;--bs-app-aside-gap-end:0px;--bs-app-aside-gap-top:0px;--bs-app-aside-gap-bottom:0px}[data-kt-app-aside-collapse=on]{--bs-app-aside-width:0px}[data-kt-app-aside-static=true] .app-aside{position:relative}[data-kt-app-aside-offcanvas=true] .app-aside{display:none}[data-kt-app-aside-fixed=true] .app-aside{position:fixed;z-index:99;top:0;bottom:0;right:0}[data-kt-app-aside-stacked=true] .app-aside{align-items:stretch}[data-kt-app-aside-sticky=on] .app-aside{position:fixed;transition:none;box-shadow:var(--bs-app-aside-sticky-box-shadow);border-left:var(--bs-aside-sticky-border-start);border-right:var(--bs-app-aside-sticky-border-end);top:auto;bottom:auto;left:auto;z-index:99;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-aside-minimize=on] .app-aside{transition:none;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-aside-hoverable=true] .app-aside .app-aside-wrapper{width:var(--bs-app-aside-width-actual)}[data-kt-app-aside-hoverable=true][data-kt-app-aside-minimize=on] .app-aside:hover:not(.animating){transition:none;width:var(--bs-app-aside-width-actual);box-shadow:var(--bs-app-aside-minimize-hover-box-shadow)}[data-kt-app-aside-collapse=on] .app-aside{transition:none;width:var(--bs-app-aside-width-actual);margin-right:calc(-1 * var(--bs-app-aside-width-actual))}[data-kt-app-aside-minimize=on] .app-aside-minimize-d-none{display:none!important}[data-kt-app-aside-minimize=on] .app-aside-minimize-d-flex{display:flex!important}[data-kt-app-aside-sticky=on] .app-aside-sticky-d-none{display:none!important}[data-kt-app-aside-sticky=on] .app-aside-sticky-d-flex{display:flex!important}[data-kt-app-aside-collapse=on] .app-aside-collapse-d-none{display:none!important}[data-kt-app-aside-collapse=on] .app-aside-collapse-d-flex{display:flex!important}[data-kt-app-aside-fixed=true][data-kt-app-header-fixed=true]:not([data-kt-app-aside-push-header=true]) .app-aside{top:var(--bs-app-header-height)}[data-kt-app-aside-fixed=true][data-kt-app-header-fixed=true][data-kt-app-toolbar-fixed=true]:not([data-kt-app-aside-push-toolbar=true]) .app-aside{top:calc(var(--bs-app-header-height) + var(--bs-app-toolbar-height,0px))}}@media (max-width:991.98px){.app-aside{display:none;width:var(--bs-app-aside-width);z-index:106;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}:root{--bs-app-aside-width:275px;--bs-app-aside-width-actual:275px;--bs-app-aside-gap-start:0px;--bs-app-aside-gap-end:0px;--bs-app-aside-gap-top:0px;--bs-app-aside-gap-bottom:0px}[data-kt-app-aside-minimize-mobile=on]{--bs-app-aside-width:75px;--bs-app-aside-gap-start:0px;--bs-app-aside-gap-end:0px;--bs-app-aside-gap-top:0px;--bs-app-aside-gap-bottom:0px}[data-kt-app-aside-collapse-mobile=on]{--bs-app-aside-width:0px}[data-kt-app-aside-stacked=true] .app-aside{align-items:stretch}[data-kt-app-aside-minimize-mobile=on] .app-aside{transition:none;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}[data-kt-app-aside-hoverable-mobile=true] .app-aside .app-aside-wrapper{width:var(--bs-app-aside-width-actual)}[data-kt-app-aside-hoverable-mobile=true][data-kt-app-aside-minimize-mobile=on] .app-aside:hover:not(.animating){transition:none;width:var(--bs-app-aside-width-actual);box-shadow:var(--bs-app-aside-minimize-hover-box-shadow-mobile)}[data-kt-app-aside-collapse-mobile=on] .app-aside{transition:none;width:var(--bs-app-aside-width-actual);margin-right:calc(-1 * var(--bs-app-aside-width-actual))}[data-kt-app-aside-minimize-mobile=on] .app-aside-minimize-mobile-d-none{display:none!important}[data-kt-app-aside-minimize-mobile=on] .app-aside-minimize-mobile-d-flex{display:flex!important}[data-kt-app-aside-collapse-mobile=on] .app-aside-collapse-mobile-d-none{display:none!important}[data-kt-app-aside-collapse-mobile=on] .app-aside-collapse-mobile-d-flex{display:flex!important}}.app-wrapper{display:flex}@media (min-width:992px){.app-wrapper{transition:margin-left .3s ease,margin-right .3s ease}[data-kt-app-header-sticky=on] .app-wrapper{margin-top:var(--bs-app-header-height-actual)}[data-kt-app-header-fixed=true] .app-wrapper{margin-top:var(--bs-app-header-height)}[data-kt-app-toolbar-sticky=on] .app-wrapper{margin-top:var(--bs-app-toolbar-height)}[data-kt-app-header-fixed=true][data-kt-app-toolbar-sticky=on] .app-wrapper{margin-top:calc(var(--bs-app-header-height-actual) + var(--bs-app-toolbar-height-actual))}[data-kt-app-header-fixed=true][data-kt-app-toolbar-fixed=true] .app-wrapper{margin-top:calc(var(--bs-app-header-height) + var(--bs-app-toolbar-height))}[data-kt-app-sidebar-fixed=true] .app-wrapper{margin-left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}[data-kt-app-sidebar-panel-fixed=true] .app-wrapper{margin-left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}[data-kt-app-aside-fixed=true] .app-wrapper{margin-right:calc(var(--bs-app-aside-width) + var(--bs-app-aside-gap-start,0px) + var(--bs-app-aside-gap-end,0px))}[data-kt-app-footer-fixed=true] .app-wrapper{margin-bottom:var(--bs-app-footer-height)}}@media (max-width:991.98px){.app-wrapper{transition:margin-left .3s ease,margin-right .3s ease}[data-kt-app-header-sticky=on] .app-wrapper{margin-top:var(--bs-app-header-height-actual)}[data-kt-app-header-fixed-mobile=true] .app-wrapper{margin-top:var(--bs-app-header-height)}[data-kt-app-header-fixed-mobile=true][data-kt-app-toolbar-sticky=on] .app-wrapper{margin-top:calc(var(--bs-app-header-height-actual) + var(--bs-app-toolbar-height-actual))}[data-kt-app-footer-fixed-mobile=true] .app-wrapper{margin-bottom:var(--bs-app-footer-height)}}.app-main{display:flex}@media (min-width:992px){.app-main{transition:margin .3s ease}[data-kt-app-sidebar-sticky=true] .app-main{margin-left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}[data-kt-app-sidebar-panel-sticky=true] .app-main{margin-left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}[data-kt-app-aside-sticky=true] .app-main{margin-right:calc(var(--bs-app-aside-width) + var(--bs-app-aside-gap-start,0px) + var(--bs-app-aside-gap-end,0px))}}@media (max-width:991.98px){.app-main{padding-left:0;padding-right:0}}@media (min-width:992px){.app-content{padding-top:20px;padding-bottom:20px;padding-left:0;padding-right:0}}@media (max-width:991.98px){.app-content{max-width:none;padding-top:20px;padding-bottom:20px;padding-left:0;padding-right:0}}.app-footer{transition:left .3s ease,right .3s ease;display:flex;align-items:center;background-color:var(--bs-app-footer-bg-color);border-top:var(--bs-app-footer-border-top)}@media (min-width:992px){:root{--bs-app-footer-height:auto}.app-footer{height:var(--bs-app-footer-height)}[data-kt-app-footer-fixed=true] .app-footer{z-index:100;box-shadow:var(--bs-app-footer-box-shadow);position:fixed;left:0;right:0;bottom:0}[data-kt-app-sidebar-fixed=true][data-kt-app-sidebar-push-footer=true] .app-footer{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px))}[data-kt-app-sidebar-panel-fixed=true][data-kt-app-sidebar-panel-push-footer=true] .app-footer{left:calc(var(--bs-app-sidebar-width) + var(--bs-app-sidebar-gap-start,0px) + var(--bs-app-sidebar-gap-end,0px) + var(--bs-app-sidebar-panel-width) + var(--bs-app-sidebar-panel-gap-start,0px) + var(--bs-app-sidebar-panel-gap-end,0px))}[data-kt-app-aside-fixed=true][data-kt-app-aside-push-footer=true] .app-footer{right:calc(var(--bs-app-aside-width) + var(--bs-app-aside-gap-start,0px) + var(--bs-app-aside-gap-end,0px))}}@media (max-width:991.98px){body{--bs-app-footer-height:auto}.app-footer{height:var(--bs-app-footer-height)}[data-kt-app-footer-fixed-mobile=true] .app-footer{z-index:100;box-shadow:var(--bs-app-footer-box-shadow);position:fixed;left:0;right:0;bottom:0}}.app-layout-builder-toggle{position:fixed;z-index:105;bottom:40px;right:50px}@media (max-width:991.98px){.app-layout-builder-toggle{bottom:20px;right:40px}}@media (min-width:992px){[data-kt-app-header-sticky=on] .app-header .app-container{margin-top:0}}.app-sidebar .hover-scroll-y{scrollbar-color:transparent transparent}.app-sidebar .hover-scroll-y::-webkit-scrollbar-thumb{background-color:transparent}.app-sidebar .hover-scroll-y::-webkit-scrollbar-corner{background-color:transparent}.app-sidebar .hover-scroll-y:hover{scrollbar-color:#343641 transparent}.app-sidebar .hover-scroll-y:hover::-webkit-scrollbar-thumb{background-color:#343641}.app-sidebar .hover-scroll-y:hover::-webkit-scrollbar-corner{background-color:transparent}.app-sidebar-header .btn.btn-custom{border:1px solid rgba(255,255,255,.1)}.app-sidebar-header .btn.btn-custom .btn-title{color:#fff}.app-sidebar-header .btn.btn-custom .btn-desc{color:rgba(255,255,255,.4)}.app-sidebar-header .btn.btn-custom .btn-icon{color:rgba(255,255,255,.6)}.app-sidebar-header .btn.btn-custom.active .btn-title,.app-sidebar-header .btn.btn-custom.show .btn-title,.app-sidebar-header .btn.btn-custom:active:not(.btn-active) .btn-title,.app-sidebar-header .btn.btn-custom:focus:not(.btn-active) .btn-title,.app-sidebar-header .btn.btn-custom:hover:not(.btn-active) .btn-title,.btn-check:active+.app-sidebar-header .btn.btn-custom .btn-title,.btn-check:checked+.app-sidebar-header .btn.btn-custom .btn-title,.show>.app-sidebar-header .btn.btn-custom .btn-title{color:#fff}.app-sidebar-header .btn.btn-custom.active .btn-desc,.app-sidebar-header .btn.btn-custom.show .btn-desc,.app-sidebar-header .btn.btn-custom:active:not(.btn-active) .btn-desc,.app-sidebar-header .btn.btn-custom:focus:not(.btn-active) .btn-desc,.app-sidebar-header .btn.btn-custom:hover:not(.btn-active) .btn-desc,.btn-check:active+.app-sidebar-header .btn.btn-custom .btn-desc,.btn-check:checked+.app-sidebar-header .btn.btn-custom .btn-desc,.show>.app-sidebar-header .btn.btn-custom .btn-desc{color:rgba(255,255,255,.5)}.app-sidebar-header .btn.btn-custom.active .btn-icon,.app-sidebar-header .btn.btn-custom.show .btn-icon,.app-sidebar-header .btn.btn-custom:active:not(.btn-active) .btn-icon,.app-sidebar-header .btn.btn-custom:focus:not(.btn-active) .btn-icon,.app-sidebar-header .btn.btn-custom:hover:not(.btn-active) .btn-icon,.btn-check:active+.app-sidebar-header .btn.btn-custom .btn-icon,.btn-check:checked+.app-sidebar-header .btn.btn-custom .btn-icon,.show>.app-sidebar-header .btn.btn-custom .btn-icon{color:rgba(255,255,255,.8)}.app-sidebar-navs .menu{padding:0 1.5rem}.app-sidebar-navs .menu>.menu-item>.menu-link{padding:13px 10px}.app-sidebar-navs .menu>.menu-item>.menu-link .menu-title{font-size:1.05rem;font-weight:400}.app-sidebar-navs .menu>.menu-item>.menu-heading,.app-sidebar-navs .menu>.menu-item>.menu-link>.menu-title{font-size:1.15rem;font-weight:500}.app-sidebar-navs .menu .menu-item .menu-heading{color:#363843}.app-sidebar-navs .menu .menu-item .menu-link .menu-icon{margin-right:.9rem}.app-sidebar-navs .menu .menu-item .menu-link .menu-icon i{font-size:1.5rem!important;line-height:0}.app-sidebar-navs .menu .menu-item .menu-link .menu-icon .svg-icon svg{height:1.75rem!important;width:1.75rem!important}.app-sidebar-navs .menu .menu-item .menu-link .menu-badge .badge{font-weight:600;font-size:.8rem}.app-sidebar-navs .menu .menu-item .menu-link .menu-badge .badge.badge-custom{background-color:#262626;color:#78829d}.app-sidebar-navs .menu .menu-item .menu-link .menu-arrow{width:11px;height:11px}.app-sidebar-navs .menu .menu-item .menu-link{color:#dbdfe9}.app-sidebar-navs .menu .menu-item .menu-link .menu-title{color:#dbdfe9}.app-sidebar-navs .menu .menu-item .menu-link .menu-icon,.app-sidebar-navs .menu .menu-item .menu-link .menu-icon .svg-icon,.app-sidebar-navs .menu .menu-item .menu-link .menu-icon i{color:#78829d}.app-sidebar-navs .menu .menu-item .menu-link .menu-bullet .bullet{background-color:#78829d}.app-sidebar-navs .menu .menu-item .menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#78829d;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%2378829D'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%2378829D'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#78829d;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%2378829D'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%2378829D'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.app-sidebar-navs .menu .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here),.app-sidebar-navs .menu .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here){transition:color .2s ease;color:#fff}.app-sidebar-navs .menu .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-title,.app-sidebar-navs .menu .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-title{color:#fff}.app-sidebar-navs .menu .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon,.app-sidebar-navs .menu .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.app-sidebar-navs .menu .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-icon i,.app-sidebar-navs .menu .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon,.app-sidebar-navs .menu .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon .svg-icon,.app-sidebar-navs .menu .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-icon i{color:#fff}.app-sidebar-navs .menu .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-bullet .bullet,.app-sidebar-navs .menu .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-bullet .bullet{background-color:#fff}.app-sidebar-navs .menu .menu-item.hover:not(.here)>.menu-link:not(.disabled):not(.active):not(.here) .menu-arrow:after,.app-sidebar-navs .menu .menu-item:not(.here) .menu-link:hover:not(.disabled):not(.active):not(.here) .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#fff;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#fff;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.app-sidebar-navs .menu .menu-item.show>.menu-link{transition:color .2s ease;color:#fff}.app-sidebar-navs .menu .menu-item.show>.menu-link .menu-title{color:#fff}.app-sidebar-navs .menu .menu-item.show>.menu-link .menu-icon,.app-sidebar-navs .menu .menu-item.show>.menu-link .menu-icon .svg-icon,.app-sidebar-navs .menu .menu-item.show>.menu-link .menu-icon i{color:#fff}.app-sidebar-navs .menu .menu-item.show>.menu-link .menu-bullet .bullet{background-color:#fff}.app-sidebar-navs .menu .menu-item.show>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#fff;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#fff;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.app-sidebar-navs .menu .menu-item.here>.menu-link{transition:color .2s ease;color:#fff}.app-sidebar-navs .menu .menu-item.here>.menu-link .menu-title{color:#fff}.app-sidebar-navs .menu .menu-item.here>.menu-link .menu-icon,.app-sidebar-navs .menu .menu-item.here>.menu-link .menu-icon .svg-icon,.app-sidebar-navs .menu .menu-item.here>.menu-link .menu-icon i{color:#fff}.app-sidebar-navs .menu .menu-item.here>.menu-link .menu-bullet .bullet{background-color:#fff}.app-sidebar-navs .menu .menu-item.here>.menu-link .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#fff;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#fff;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.app-sidebar-navs .menu .menu-item .menu-link.active{transition:color .2s ease;background-color:#15171c;color:#fff}.app-sidebar-navs .menu .menu-item .menu-link.active .menu-title{color:#fff}.app-sidebar-navs .menu .menu-item .menu-link.active .menu-bullet .bullet{background-color:#fff}.app-sidebar-navs .menu .menu-item .menu-link.active .menu-arrow:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#fff;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M2.72011 2.76429L4.46358 1.02083C4.63618 0.848244 4.63617 0.568419 4.46358 0.395831C4.29099 0.223244 4.01118 0.223244 3.83861 0.395831L1.52904 2.70537C1.36629 2.86808 1.36629 3.13191 1.52904 3.29462L3.83861 5.60419C4.01117 5.77675 4.29099 5.77675 4.46358 5.60419C4.63617 5.43156 4.63617 5.15175 4.46358 4.97919L2.72011 3.23571C2.58994 3.10554 2.58994 2.89446 2.72011 2.76429Z'/%3e%3c/svg%3e\");mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#fff;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='%23ffffff'%3e%3cpath d='M3.27989 3.23571L1.53642 4.97917C1.36382 5.15176 1.36383 5.43158 1.53642 5.60417C1.70901 5.77676 1.98882 5.77676 2.16139 5.60417L4.47096 3.29463C4.63371 3.13192 4.63371 2.86809 4.47096 2.70538L2.16139 0.395812C1.98883 0.22325 1.70901 0.22325 1.53642 0.395812C1.36383 0.568437 1.36383 0.84825 1.53642 1.02081L3.27989 2.76429C3.41006 2.89446 3.41006 3.10554 3.27989 3.23571Z'/%3e%3c/svg%3e\")}.app-sidebar-navs .menu .menu-collapse-toggle{background-color:transparent!important}.app-sidebar-navs .menu .menu-collapse-toggle .menu-title{font-size:1rem!important;color:#78829d!important}.app-sidebar-navs .menu .menu-collapse-toggle .svg-color{color:#c4cada!important}@media (min-width:992px){.app-header .page-heading{color:#1e2129!important}}@media (max-width:991.98px){.app-header .page-title{display:none!important}}@media (min-width:992px){.app-toolbar .app-toolbar-container{border-bottom:var(--bs-app-separator-border)}}@media (max-width:991.98px){.app-toolbar{border-bottom:var(--bs-app-separator-border)}}"
  },
  {
    "path": "static/assets/js/scripts.bundle.js",
    "content": "var KTComponents={init:function(){KTApp.init(),KTDrawer.init(),KTMenu.init(),KTScroll.init(),KTSticky.init(),KTSwapper.init(),KTToggle.init(),KTScrolltop.init(),KTDialer.init(),KTImageInput.init(),KTPasswordMeter.init()}};\"loading\"===document.readyState?document.addEventListener(\"DOMContentLoaded\",function(){KTComponents.init()}):KTComponents.init(),window.addEventListener(\"load\",function(){KTApp.hidePageLoading()}),\"undefined\"!=typeof module&&void 0!==module.exports&&(window.KTComponents=module.exports=KTComponents);var KTApp=function(){var e=!1,t=!1,n=function(e,t){if(\"1\"!==e.getAttribute(\"data-kt-initialized\")){var n={};e.hasAttribute(\"data-bs-delay-hide\")&&(n.hide=e.getAttribute(\"data-bs-delay-hide\")),e.hasAttribute(\"data-bs-delay-show\")&&(n.show=e.getAttribute(\"data-bs-delay-show\")),n&&(t.delay=n),e.hasAttribute(\"data-bs-dismiss\")&&\"click\"==e.getAttribute(\"data-bs-dismiss\")&&(t.dismiss=\"click\");var i=new bootstrap.Tooltip(e,t);return t.dismiss&&\"click\"===t.dismiss&&e.addEventListener(\"click\",function(t){e.blur(),i.hide()}),e.setAttribute(\"data-kt-initialized\",\"1\"),i}},i=function(e,t){if(\"1\"!==e.getAttribute(\"data-kt-initialized\")){var n={};e.hasAttribute(\"data-bs-delay-hide\")&&(n.hide=e.getAttribute(\"data-bs-delay-hide\")),e.hasAttribute(\"data-bs-delay-show\")&&(n.show=e.getAttribute(\"data-bs-delay-show\")),n&&(t.delay=n),\"true\"==e.getAttribute(\"data-bs-dismiss\")&&(t.dismiss=!0),!0===t.dismiss&&(t.template='<div class=\"popover\" role=\"tooltip\"><div class=\"popover-arrow\"></div><span class=\"popover-dismiss btn btn-icon\"></span><h3 class=\"popover-header\"></h3><div class=\"popover-body\"></div></div>');var i=new bootstrap.Popover(e,t);if(!0===t.dismiss){var r=function(e){i.hide()};e.addEventListener(\"shown.bs.popover\",function(){document.getElementById(e.getAttribute(\"aria-describedby\")).addEventListener(\"click\",r)}),e.addEventListener(\"hide.bs.popover\",function(){document.getElementById(e.getAttribute(\"aria-describedby\")).removeEventListener(\"click\",r)})}return e.setAttribute(\"data-kt-initialized\",\"1\"),i}},r=function(){\"undefined\"!=typeof countUp&&[].slice.call(document.querySelectorAll('[data-kt-countup=\"true\"]:not(.counted)')).map(function(e){if(KTUtil.isInViewport(e)&&KTUtil.visible(e)){if(\"1\"===e.getAttribute(\"data-kt-initialized\"))return;var t={},n=e.getAttribute(\"data-kt-countup-value\");n=parseFloat(n.replace(/,/g,\"\")),e.hasAttribute(\"data-kt-countup-start-val\")&&(t.startVal=parseFloat(e.getAttribute(\"data-kt-countup-start-val\"))),e.hasAttribute(\"data-kt-countup-duration\")&&(t.duration=parseInt(e.getAttribute(\"data-kt-countup-duration\"))),e.hasAttribute(\"data-kt-countup-decimal-places\")&&(t.decimalPlaces=parseInt(e.getAttribute(\"data-kt-countup-decimal-places\"))),e.hasAttribute(\"data-kt-countup-prefix\")&&(t.prefix=e.getAttribute(\"data-kt-countup-prefix\")),e.hasAttribute(\"data-kt-countup-separator\")&&(t.separator=e.getAttribute(\"data-kt-countup-separator\")),e.hasAttribute(\"data-kt-countup-suffix\")&&(t.suffix=e.getAttribute(\"data-kt-countup-suffix\")),new countUp.CountUp(e,n,t).start(),e.classList.add(\"counted\"),e.setAttribute(\"data-kt-initialized\",\"1\")}})},o=function(e){if(!e)return;const t={};e.getAttributeNames().forEach(function(n){if(/^data-tns-.*/g.test(n)){let r=n.replace(\"data-tns-\",\"\").toLowerCase().replace(/(?:[\\s-])\\w/g,function(e){return e.replace(\"-\",\"\").toUpperCase()});if(\"data-tns-responsive\"===n){const i=e.getAttribute(n).replace(/(\\w+:)|(\\w+ :)/g,function(e){return'\"'+e.substring(0,e.length-1)+'\":'});try{t[r]=JSON.parse(i)}catch(e){}}else t[r]=\"true\"===(i=e.getAttribute(n))||\"false\"!==i&&i}var i});const n=Object.assign({},{container:e,slideBy:\"page\",autoplay:!0,center:!0,autoplayButtonOutput:!1},t);return e.closest(\".tns\")&&KTUtil.addClass(e.closest(\".tns\"),\"tns-initiazlied\"),tns(n)};return{init:function(){var a;!function(){if(\"undefined\"==typeof lozad)return;lozad().observe()}(),!0!==e&&\"undefined\"!=typeof SmoothScroll&&new SmoothScroll('a[data-kt-scroll-toggle][href*=\"#\"]',{speed:1e3,speedAsDuration:!0,offset:function(e,t){return e.hasAttribute(\"data-kt-scroll-offset\")?KTUtil.getResponsiveValue(e.getAttribute(\"data-kt-scroll-offset\")):0}}),KTUtil.on(document.body,'[data-kt-card-action=\"remove\"]',\"click\",function(e){e.preventDefault();const t=this.closest(\".card\");if(!t)return;const n=this.getAttribute(\"data-kt-card-confirm-message\");\"true\"===this.getAttribute(\"data-kt-card-confirm\")?Swal.fire({text:n||\"Are you sure to remove ?\",icon:\"warning\",buttonsStyling:!1,confirmButtonText:\"Confirm\",denyButtonText:\"Cancel\",customClass:{confirmButton:\"btn btn-primary\",denyButton:\"btn btn-danger\"}}).then(function(e){e.isConfirmed&&t.remove()}):t.remove()}),(a=Array.prototype.slice.call(document.querySelectorAll(\"[data-bs-stacked-modal]\")))&&a.length>0&&a.forEach(e=>{\"1\"!==e.getAttribute(\"data-kt-initialized\")&&(e.setAttribute(\"data-kt-initialized\",\"1\"),e.addEventListener(\"click\",function(e){e.preventDefault();const t=document.querySelector(this.getAttribute(\"data-bs-stacked-modal\"));t&&new bootstrap.Modal(t,{backdrop:!1}).show()}))}),!0!==e&&KTUtil.on(document.body,'[data-kt-check=\"true\"]',\"change\",function(e){var t=this,n=document.querySelectorAll(t.getAttribute(\"data-kt-check-target\"));KTUtil.each(n,function(e){\"checkbox\"==e.type?e.checked=t.checked:e.classList.toggle(\"active\")})}),!0!==e&&KTUtil.on(document.body,'.collapsible[data-bs-toggle=\"collapse\"]',\"click\",function(e){if(this.classList.contains(\"collapsed\")?(this.classList.remove(\"active\"),this.blur()):this.classList.add(\"active\"),this.hasAttribute(\"data-kt-toggle-text\")){var t=this.getAttribute(\"data-kt-toggle-text\"),n=(n=this.querySelector('[data-kt-toggle-text-target=\"true\"]'))||this;this.setAttribute(\"data-kt-toggle-text\",n.innerText),n.innerText=t}}),!0!==e&&KTUtil.on(document.body,'[data-kt-rotate=\"true\"]',\"click\",function(e){this.classList.contains(\"active\")?(this.classList.remove(\"active\"),this.blur()):this.classList.add(\"active\")}),[].slice.call(document.querySelectorAll('[data-bs-toggle=\"tooltip\"]')).map(function(e){n(e,{})}),[].slice.call(document.querySelectorAll('[data-bs-toggle=\"popover\"]')).map(function(e){i(e,{})}),[].slice.call(document.querySelectorAll(\".toast\")).map(function(e){if(\"1\"!==e.getAttribute(\"data-kt-initialized\"))return e.setAttribute(\"data-kt-initialized\",\"1\"),new bootstrap.Toast(e,{})}),function(){if(\"undefined\"!=typeof jQuery&&void 0!==$.fn.daterangepicker){var e=[].slice.call(document.querySelectorAll('[data-kt-daterangepicker=\"true\"]')),t=moment().subtract(29,\"days\"),n=moment();e.map(function(e){if(\"1\"!==e.getAttribute(\"data-kt-initialized\")){var i=e.querySelector(\"div\"),r=e.hasAttribute(\"data-kt-daterangepicker-opens\")?e.getAttribute(\"data-kt-daterangepicker-opens\"):\"left\",o=function(e,t){var n=moment();i&&(n.isSame(e,\"day\")&&n.isSame(t,\"day\")?i.innerHTML=e.format(\"D MMM YYYY\"):i.innerHTML=e.format(\"D MMM YYYY\")+\" - \"+t.format(\"D MMM YYYY\"))};\"today\"===e.getAttribute(\"data-kt-daterangepicker-range\")&&(t=moment(),n=moment()),$(e).daterangepicker({startDate:t,endDate:n,opens:r,ranges:{Today:[moment(),moment()],Yesterday:[moment().subtract(1,\"days\"),moment().subtract(1,\"days\")],\"Last 7 Days\":[moment().subtract(6,\"days\"),moment()],\"Last 30 Days\":[moment().subtract(29,\"days\"),moment()],\"This Month\":[moment().startOf(\"month\"),moment().endOf(\"month\")],\"Last Month\":[moment().subtract(1,\"month\").startOf(\"month\"),moment().subtract(1,\"month\").endOf(\"month\")]}},o),o(t,n),e.setAttribute(\"data-kt-initialized\",\"1\")}})}}(),[].slice.call(document.querySelectorAll('[data-kt-buttons=\"true\"]')).map(function(e){if(\"1\"!==e.getAttribute(\"data-kt-initialized\")){var t=e.hasAttribute(\"data-kt-buttons-target\")?e.getAttribute(\"data-kt-buttons-target\"):\".btn\",n=[].slice.call(e.querySelectorAll(t));KTUtil.on(e,t,\"click\",function(e){n.map(function(e){e.classList.remove(\"active\")}),this.classList.add(\"active\")}),e.setAttribute(\"data-kt-initialized\",\"1\")}}),\"undefined\"!=typeof jQuery&&void 0!==$.fn.select2&&[].slice.call(document.querySelectorAll('[data-control=\"select2\"], [data-kt-select2=\"true\"]')).map(function(e){if(\"1\"!==e.getAttribute(\"data-kt-initialized\")){var t={dir:document.body.getAttribute(\"direction\")};if(\"true\"==e.getAttribute(\"data-hide-search\")&&(t.minimumResultsForSearch=1/0),$(e).select2(t),e.hasAttribute(\"data-dropdown-parent\")&&e.hasAttribute(\"multiple\")){var n=document.querySelector(e.getAttribute(\"data-dropdown-parent\"));if(n&&n.hasAttribute(\"data-kt-menu\")){var i=KTMenu.getInstance(n);i||(i=new KTMenu(n)),i&&($(e).on(\"select2:unselect\",function(t){e.setAttribute(\"data-multiple-unselect\",\"1\")}),i.on(\"kt.menu.dropdown.hide\",function(t){if(\"1\"===e.getAttribute(\"data-multiple-unselect\"))return e.removeAttribute(\"data-multiple-unselect\"),!1}))}}e.setAttribute(\"data-kt-initialized\",\"1\")}}),r(),\"undefined\"!=typeof countUp&&(!1===t&&(r(),window.addEventListener(\"scroll\",r)),[].slice.call(document.querySelectorAll('[data-kt-countup-tabs=\"true\"][data-bs-toggle=\"tab\"]')).map(function(e){\"1\"!==e.getAttribute(\"data-kt-initialized\")&&(e.addEventListener(\"shown.bs.tab\",r),e.setAttribute(\"data-kt-initialized\",\"1\"))}),t=!0),\"undefined\"!=typeof autosize&&[].slice.call(document.querySelectorAll('[data-kt-autosize=\"true\"]')).map(function(e){\"1\"!==e.getAttribute(\"data-kt-initialized\")&&(autosize(e),e.setAttribute(\"data-kt-initialized\",\"1\"))}),function(){if(\"undefined\"==typeof tns)return;const e=Array.prototype.slice.call(document.querySelectorAll('[data-tns=\"true\"]'),0);(e||0!==e.length)&&e.forEach(function(e){\"1\"!==e.getAttribute(\"data-kt-initialized\")&&(o(e),KTUtil.data(e).set(\"tns\",tns),e.setAttribute(\"data-kt-initialized\",\"1\"))})}(),e=!0},initTinySlider:function(e){o(e)},showPageLoading:function(){document.body.classList.add(\"page-loading\"),document.body.setAttribute(\"data-kt-app-page-loading\",\"on\")},hidePageLoading:function(){document.body.classList.remove(\"page-loading\"),document.body.removeAttribute(\"data-kt-app-page-loading\")},createBootstrapPopover:function(e,t){return i(e,t)},createBootstrapTooltip:function(e,t){return n(e,t)}}}();\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTApp);var KTBlockUI=function(e,t){var n=this;if(null!=e){var i={zIndex:!1,overlayClass:\"\",overflow:\"hidden\",message:'<span class=\"spinner-border text-primary\"></span>'},r=function(){n.options=KTUtil.deepExtend({},i,t),n.element=e,n.overlayElement=null,n.blocked=!1,n.positionChanged=!1,n.overflowChanged=!1,KTUtil.data(n.element).set(\"blockui\",n)};KTUtil.data(e).has(\"blockui\")?n=KTUtil.data(e).get(\"blockui\"):r(),n.block=function(){!function(){if(!1!==KTEventHandler.trigger(n.element,\"kt.blockui.block\",n)){var e=\"BODY\"===n.element.tagName,t=KTUtil.css(n.element,\"position\"),i=KTUtil.css(n.element,\"overflow\"),r=e?1e4:1;n.options.zIndex>0?r=n.options.zIndex:\"auto\"!=KTUtil.css(n.element,\"z-index\")&&(r=KTUtil.css(n.element,\"z-index\")),n.element.classList.add(\"blockui\"),\"absolute\"!==t&&\"relative\"!==t&&\"fixed\"!==t||(KTUtil.css(n.element,\"position\",\"relative\"),n.positionChanged=!0),\"hidden\"===n.options.overflow&&\"visible\"===i&&(KTUtil.css(n.element,\"overflow\",\"hidden\"),n.overflowChanged=!0),n.overlayElement=document.createElement(\"DIV\"),n.overlayElement.setAttribute(\"class\",\"blockui-overlay \"+n.options.overlayClass),n.overlayElement.innerHTML=n.options.message,KTUtil.css(n.overlayElement,\"z-index\",r),n.element.append(n.overlayElement),n.blocked=!0,KTEventHandler.trigger(n.element,\"kt.blockui.after.blocked\",n)}}()},n.release=function(){!1!==KTEventHandler.trigger(n.element,\"kt.blockui.release\",n)&&(n.element.classList.add(\"blockui\"),n.positionChanged&&KTUtil.css(n.element,\"position\",\"\"),n.overflowChanged&&KTUtil.css(n.element,\"overflow\",\"\"),n.overlayElement&&KTUtil.remove(n.overlayElement),n.blocked=!1,KTEventHandler.trigger(n.element,\"kt.blockui.released\",n))},n.isBlocked=function(){return n.blocked},n.destroy=function(){KTUtil.data(n.element).remove(\"blockui\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)},n.trigger=function(e,t){return KTEventHandler.trigger(n.element,e,t,n,t)}}};KTBlockUI.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"blockui\")?KTUtil.data(e).get(\"blockui\"):null},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTBlockUI);var KTCookie={get:function(e){var t=document.cookie.match(new RegExp(\"(?:^|; )\"+e.replace(/([\\.$?*|{}\\(\\)\\[\\]\\\\\\/\\+^])/g,\"\\\\$1\")+\"=([^;]*)\"));return t?decodeURIComponent(t[1]):null},set:function(e,t,n){null==n&&(n={}),(n=Object.assign({},{path:\"/\"},n)).expires instanceof Date&&(n.expires=n.expires.toUTCString());var i=encodeURIComponent(e)+\"=\"+encodeURIComponent(t);for(var r in n)if(!1!==n.hasOwnProperty(r)){i+=\"; \"+r;var o=n[r];!0!==o&&(i+=\"=\"+o)}document.cookie=i},remove:function(e){this.set(e,\"\",{\"max-age\":-1})}};\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTCookie);var KTDialer=function(e,t){var n=this;if(e){var i={min:null,max:null,step:1,currency:!1,decimals:0,prefix:\"\",suffix:\"\"},r=function(){n.options=KTUtil.deepExtend({},i,t),n.element=e,n.incElement=n.element.querySelector('[data-kt-dialer-control=\"increase\"]'),n.decElement=n.element.querySelector('[data-kt-dialer-control=\"decrease\"]'),n.inputElement=n.element.querySelector(\"input[type]\"),\"true\"===c(\"currency\")&&(n.options.currency=!0),c(\"decimals\")&&(n.options.decimals=parseInt(c(\"decimals\"))),c(\"prefix\")&&(n.options.prefix=c(\"prefix\")),c(\"suffix\")&&(n.options.suffix=c(\"suffix\")),c(\"step\")&&(n.options.step=parseFloat(c(\"step\"))),c(\"min\")&&(n.options.min=parseFloat(c(\"min\"))),c(\"max\")&&(n.options.max=parseFloat(c(\"max\"))),n.value=parseFloat(n.inputElement.value.replace(/[^\\d.]/g,\"\")),s(),o(),KTUtil.data(n.element).set(\"dialer\",n)},o=function(){KTUtil.addEvent(n.incElement,\"click\",function(e){e.preventDefault(),a()}),KTUtil.addEvent(n.decElement,\"click\",function(e){e.preventDefault(),l()}),KTUtil.addEvent(n.inputElement,\"input\",function(e){e.preventDefault(),s()})},a=function(){return KTEventHandler.trigger(n.element,\"kt.dialer.increase\",n),n.inputElement.value=n.value+n.options.step,s(),KTEventHandler.trigger(n.element,\"kt.dialer.increased\",n),n},l=function(){return KTEventHandler.trigger(n.element,\"kt.dialer.decrease\",n),n.inputElement.value=n.value-n.options.step,s(),KTEventHandler.trigger(n.element,\"kt.dialer.decreased\",n),n},s=function(e){KTEventHandler.trigger(n.element,\"kt.dialer.change\",n),n.value=void 0!==e?e:u(n.inputElement.value),null!==n.options.min&&n.value<n.options.min&&(n.value=n.options.min),null!==n.options.max&&n.value>n.options.max&&(n.value=n.options.max),n.inputElement.value=d(n.value),n.inputElement.dispatchEvent(new Event(\"change\")),KTEventHandler.trigger(n.element,\"kt.dialer.changed\",n)},u=function(e){return e=e.replace(/[^0-9.-]/g,\"\").replace(/(\\..*)\\./g,\"$1\").replace(/(?!^)-/g,\"\").replace(/^0+(\\d)/gm,\"$1\"),e=parseFloat(e),isNaN(e)&&(e=0),e},d=function(e){return e=parseFloat(e).toFixed(n.options.decimals),n.options.currency&&(e=e.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g,\",\")),n.options.prefix+e+n.options.suffix},c=function(e){return!0===n.element.hasAttribute(\"data-kt-dialer-\"+e)?n.element.getAttribute(\"data-kt-dialer-\"+e):null};!0===KTUtil.data(e).has(\"dialer\")?n=KTUtil.data(e).get(\"dialer\"):r(),n.setMinValue=function(e){n.options.min=e},n.setMaxValue=function(e){n.options.max=e},n.setValue=function(e){s(e)},n.getValue=function(){return n.inputElement.value},n.update=function(){s()},n.increase=function(){return a()},n.decrease=function(){return l()},n.getElement=function(){return n.element},n.destroy=function(){KTUtil.data(n.element).remove(\"dialer\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)},n.trigger=function(e,t){return KTEventHandler.trigger(n.element,e,t,n,t)}}};KTDialer.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"dialer\")?KTUtil.data(e).get(\"dialer\"):null},KTDialer.createInstances=function(e='[data-kt-dialer=\"true\"]'){var t=document.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTDialer(t[n])},KTDialer.init=function(){KTDialer.createInstances()},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTDialer);var KTDrawerHandlersInitialized=!1,KTDrawer=function(e,t){var n=this;if(null!=e){var i={overlay:!0,direction:\"end\",baseClass:\"drawer\",overlayClass:\"drawer-overlay\"},r=function(){n.options=KTUtil.deepExtend({},i,t),n.uid=KTUtil.getUniqueId(\"drawer\"),n.element=e,n.overlayElement=null,n.name=n.element.getAttribute(\"data-kt-drawer-name\"),n.shown=!1,n.lastWidth,n.lastHeight,n.toggleElement=null,n.element.setAttribute(\"data-kt-drawer\",\"true\"),o(),u(),KTUtil.data(n.element).set(\"drawer\",n)},o=function(){var e=m(\"toggle\"),t=m(\"close\");null!==e&&e.length>0&&KTUtil.on(document.body,e,\"click\",function(e){e.preventDefault(),n.toggleElement=this,a()}),null!==t&&t.length>0&&KTUtil.on(document.body,t,\"click\",function(e){e.preventDefault(),n.closeElement=this,l()})},a=function(){!1!==KTEventHandler.trigger(n.element,\"kt.drawer.toggle\",n)&&!1!==m(\"activate\")&&(!0===n.shown?l():s(),KTEventHandler.trigger(n.element,\"kt.drawer.toggled\",n))},l=function(){!1!==KTEventHandler.trigger(n.element,\"kt.drawer.hide\",n)&&(n.shown=!1,c(),document.body.removeAttribute(\"data-kt-drawer-\"+n.name,\"on\"),document.body.removeAttribute(\"data-kt-drawer\"),KTUtil.removeClass(n.element,n.options.baseClass+\"-on\"),null!==n.toggleElement&&KTUtil.removeClass(n.toggleElement,\"active\"),KTEventHandler.trigger(n.element,\"kt.drawer.after.hidden\",n))},s=function(){!1!==KTEventHandler.trigger(n.element,\"kt.drawer.show\",n)&&(n.shown=!0,d(),document.body.setAttribute(\"data-kt-drawer-\"+n.name,\"on\"),document.body.setAttribute(\"data-kt-drawer\",\"on\"),KTUtil.addClass(n.element,n.options.baseClass+\"-on\"),null!==n.toggleElement&&KTUtil.addClass(n.toggleElement,\"active\"),KTEventHandler.trigger(n.element,\"kt.drawer.shown\",n))},u=function(){var e=f(),t=p(),i=m(\"direction\"),r=m(\"top\"),o=m(\"bottom\"),a=m(\"start\"),s=m(\"end\");!0===KTUtil.hasClass(n.element,n.options.baseClass+\"-on\")&&\"on\"===String(document.body.getAttribute(\"data-kt-drawer-\"+n.name+\"-\"))?n.shown=!0:n.shown=!1,!0===m(\"activate\")?(KTUtil.addClass(n.element,n.options.baseClass),KTUtil.addClass(n.element,n.options.baseClass+\"-\"+i),e&&(KTUtil.css(n.element,\"width\",e,!0),n.lastWidth=e),t&&(KTUtil.css(n.element,\"height\",t,!0),n.lastHeight=t),r&&KTUtil.css(n.element,\"top\",r),o&&KTUtil.css(n.element,\"bottom\",o),a&&(KTUtil.isRTL()?KTUtil.css(n.element,\"right\",a):KTUtil.css(n.element,\"left\",a)),s&&(KTUtil.isRTL()?KTUtil.css(n.element,\"left\",s):KTUtil.css(n.element,\"right\",s))):(KTUtil.removeClass(n.element,n.options.baseClass),KTUtil.removeClass(n.element,n.options.baseClass+\"-\"+i),KTUtil.css(n.element,\"width\",\"\"),KTUtil.css(n.element,\"height\",\"\"),r&&KTUtil.css(n.element,\"top\",\"\"),o&&KTUtil.css(n.element,\"bottom\",\"\"),a&&(KTUtil.isRTL()?KTUtil.css(n.element,\"right\",\"\"):KTUtil.css(n.element,\"left\",\"\")),s&&(KTUtil.isRTL()?KTUtil.css(n.element,\"left\",\"\"):KTUtil.css(n.element,\"right\",\"\")),l())},d=function(){!0===m(\"overlay\")&&(n.overlayElement=document.createElement(\"DIV\"),KTUtil.css(n.overlayElement,\"z-index\",KTUtil.css(n.element,\"z-index\")-1),document.body.append(n.overlayElement),KTUtil.addClass(n.overlayElement,m(\"overlay-class\")),KTUtil.addEvent(n.overlayElement,\"click\",function(e){e.preventDefault(),!0!==m(\"permanent\")&&l()}))},c=function(){null!==n.overlayElement&&KTUtil.remove(n.overlayElement)},m=function(e){if(!0===n.element.hasAttribute(\"data-kt-drawer-\"+e)){var t=n.element.getAttribute(\"data-kt-drawer-\"+e),i=KTUtil.getResponsiveValue(t);return null!==i&&\"true\"===String(i)?i=!0:null!==i&&\"false\"===String(i)&&(i=!1),i}var r=KTUtil.snakeToCamel(e);return n.options[r]?KTUtil.getResponsiveValue(n.options[r]):null},f=function(){var e=m(\"width\");return\"auto\"===e&&(e=KTUtil.css(n.element,\"width\")),e},p=function(){var e=m(\"height\");return\"auto\"===e&&(e=KTUtil.css(n.element,\"height\")),e};KTUtil.data(e).has(\"drawer\")?n=KTUtil.data(e).get(\"drawer\"):r(),n.toggle=function(){return a()},n.show=function(){return s()},n.hide=function(){return l()},n.isShown=function(){return n.shown},n.update=function(){u()},n.goElement=function(){return n.element},n.destroy=function(){KTUtil.data(n.element).remove(\"drawer\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)},n.trigger=function(e,t){return KTEventHandler.trigger(n.element,e,t,n,t)}}};KTDrawer.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"drawer\")?KTUtil.data(e).get(\"drawer\"):null},KTDrawer.hideAll=function(e=null,t='[data-kt-drawer=\"true\"]'){var n=document.querySelectorAll(t);if(n&&n.length>0)for(var i=0,r=n.length;i<r;i++){var o=n[i],a=KTDrawer.getInstance(o);a&&(e?o!==e&&a.hide():a.hide())}},KTDrawer.updateAll=function(e='[data-kt-drawer=\"true\"]'){var t=document.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++){var r=KTDrawer.getInstance(t[n]);r&&r.update()}},KTDrawer.createInstances=function(e='[data-kt-drawer=\"true\"]'){var t=document.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTDrawer(t[n])},KTDrawer.handleShow=function(){KTUtil.on(document.body,'[data-kt-drawer-show=\"true\"][data-kt-drawer-target]',\"click\",function(e){e.preventDefault();var t=document.querySelector(this.getAttribute(\"data-kt-drawer-target\"));t&&KTDrawer.getInstance(t).show()})},KTDrawer.handleEscapeKey=function(){document.addEventListener(\"keydown\",e=>{if(\"Escape\"===e.key){if(!(e.ctrlKey||e.altKey||e.shiftKey)){var t,n=document.querySelectorAll('.drawer-on[data-kt-drawer=\"true\"]:not([data-kt-drawer-escape=\"false\"])');if(n&&n.length>0)for(var i=0,r=n.length;i<r;i++)(t=KTDrawer.getInstance(n[i])).isShown()&&t.hide()}}})},KTDrawer.handleDismiss=function(){KTUtil.on(document.body,'[data-kt-drawer-dismiss=\"true\"]',\"click\",function(e){var t=this.closest('[data-kt-drawer=\"true\"]');if(t){var n=KTDrawer.getInstance(t);n.isShown()&&n.hide()}})},KTDrawer.handleResize=function(){window.addEventListener(\"resize\",function(){KTUtil.throttle(undefined,function(){var e=document.querySelectorAll('[data-kt-drawer=\"true\"]');if(e&&e.length>0)for(var t=0,n=e.length;t<n;t++){var i=KTDrawer.getInstance(e[t]);i&&i.update()}},200)})},KTDrawer.init=function(){KTDrawer.createInstances(),!1===KTDrawerHandlersInitialized&&(KTDrawer.handleResize(),KTDrawer.handleShow(),KTDrawer.handleDismiss(),KTDrawer.handleEscapeKey(),KTDrawerHandlersInitialized=!0)},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTDrawer);var KTEventHandler=function(){var e={},t=function(t,n,i,r){var o=KTUtil.getUniqueId(\"event\"),a=KTUtil.data(t).get(n);return a||(a=[]),a.push(o),KTUtil.data(t).set(n,a),e[n]||(e[n]={}),e[n][o]={name:n,callback:i,one:r,fired:!1},o};return{trigger:function(t,n,i){return function(t,n,i){var r,o=!0;if(!0===KTUtil.data(t).has(n))for(var a,l=KTUtil.data(t).get(n),s=0;s<l.length;s++)if(a=l[s],e[n]&&e[n][a]){var u=e[n][a];u.name===n&&(1==u.one?0==u.fired&&(e[n][a].fired=!0,r=u.callback.call(this,i)):r=u.callback.call(this,i),!1===r&&(o=!1))}return o}(t,n,i)},on:function(e,n,i){return t(e,n,i)},one:function(e,n,i){return t(e,n,i,!0)},off:function(t,n,i){return function(t,n,i){var r=KTUtil.data(t).get(n),o=r&&r.indexOf(i);-1!==o&&(r.splice(o,1),KTUtil.data(t).set(n,r)),e[n]&&e[n][i]&&delete e[n][i]}(t,n,i)},debug:function(){for(var t in e)e.hasOwnProperty(t)&&console.log(t)}}}();\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTEventHandler);var KTFeedback=function(e){var t=this,n={width:100,placement:\"top-center\",content:\"\",type:\"popup\"},i=function(){t.options=KTUtil.deepExtend({},n,e),t.uid=KTUtil.getUniqueId(\"feedback\"),t.element,t.shown=!1,r(),KTUtil.data(t.element).set(\"feedback\",t)},r=function(){KTUtil.addEvent(t.element,\"click\",function(e){e.preventDefault(),_go()})},o=function(){t.element=document.createElement(\"DIV\"),KTUtil.addClass(t.element,\"feedback feedback-popup\"),KTUtil.setHTML(t.element,t.options.content),\"top-center\"==t.options.placement&&a(),document.body.appendChild(t.element),KTUtil.addClass(t.element,\"feedback-shown\"),t.shown=!0},a=function(){var e=KTUtil.getResponsiveValue(t.options.width),n=KTUtil.css(t.element,\"height\");KTUtil.addClass(t.element,\"feedback-top-center\"),KTUtil.css(t.element,\"width\",e),KTUtil.css(t.element,\"left\",\"50%\"),KTUtil.css(t.element,\"top\",\"-\"+n)},l=function(){t.element.remove()};i(),t.show=function(){return function(){if(!1!==KTEventHandler.trigger(t.element,\"kt.feedback.show\",t))return\"popup\"===t.options.type&&o(),KTEventHandler.trigger(t.element,\"kt.feedback.shown\",t),t}()},t.hide=function(){return function(){if(!1!==KTEventHandler.trigger(t.element,\"kt.feedback.hide\",t))return\"popup\"===t.options.type&&l(),t.shown=!1,KTEventHandler.trigger(t.element,\"kt.feedback.hidden\",t),t}()},t.isShown=function(){return t.shown},t.getElement=function(){return t.element},t.destroy=function(){KTUtil.data(t.element).remove(\"feedback\")},t.on=function(e,n){return KTEventHandler.on(t.element,e,n)},t.one=function(e,n){return KTEventHandler.one(t.element,e,n)},t.off=function(e,n){return KTEventHandler.off(t.element,e,n)},t.trigger=function(e,n){return KTEventHandler.trigger(t.element,e,n,t,n)}};\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTFeedback);var KTImageInput=function(e,t){var n=this;if(null!=e){var i={},r=function(){n.options=KTUtil.deepExtend({},i,t),n.uid=KTUtil.getUniqueId(\"image-input\"),n.element=e,n.inputElement=KTUtil.find(e,'input[type=\"file\"]'),n.wrapperElement=KTUtil.find(e,\".image-input-wrapper\"),n.cancelElement=KTUtil.find(e,'[data-kt-image-input-action=\"cancel\"]'),n.removeElement=KTUtil.find(e,'[data-kt-image-input-action=\"remove\"]'),n.hiddenElement=KTUtil.find(e,'input[type=\"hidden\"]'),n.src=KTUtil.css(n.wrapperElement,\"backgroundImage\"),n.element.setAttribute(\"data-kt-image-input\",\"true\"),o(),KTUtil.data(n.element).set(\"image-input\",n)},o=function(){KTUtil.addEvent(n.inputElement,\"change\",a),KTUtil.addEvent(n.cancelElement,\"click\",l),KTUtil.addEvent(n.removeElement,\"click\",s)},a=function(e){if(e.preventDefault(),null!==n.inputElement&&n.inputElement.files&&n.inputElement.files[0]){if(!1===KTEventHandler.trigger(n.element,\"kt.imageinput.change\",n))return;var t=new FileReader;t.onload=function(e){KTUtil.css(n.wrapperElement,\"background-image\",\"url(\"+e.target.result+\")\")},t.readAsDataURL(n.inputElement.files[0]),n.element.classList.add(\"image-input-changed\"),n.element.classList.remove(\"image-input-empty\"),KTEventHandler.trigger(n.element,\"kt.imageinput.changed\",n)}},l=function(e){e.preventDefault(),!1!==KTEventHandler.trigger(n.element,\"kt.imageinput.cancel\",n)&&(n.element.classList.remove(\"image-input-changed\"),n.element.classList.remove(\"image-input-empty\"),\"none\"===n.src?(KTUtil.css(n.wrapperElement,\"background-image\",\"\"),n.element.classList.add(\"image-input-empty\")):KTUtil.css(n.wrapperElement,\"background-image\",n.src),n.inputElement.value=\"\",null!==n.hiddenElement&&(n.hiddenElement.value=\"0\"),KTEventHandler.trigger(n.element,\"kt.imageinput.canceled\",n))},s=function(e){e.preventDefault(),!1!==KTEventHandler.trigger(n.element,\"kt.imageinput.remove\",n)&&(n.element.classList.remove(\"image-input-changed\"),n.element.classList.add(\"image-input-empty\"),KTUtil.css(n.wrapperElement,\"background-image\",\"none\"),n.inputElement.value=\"\",null!==n.hiddenElement&&(n.hiddenElement.value=\"1\"),KTEventHandler.trigger(n.element,\"kt.imageinput.removed\",n))};!0===KTUtil.data(e).has(\"image-input\")?n=KTUtil.data(e).get(\"image-input\"):r(),n.getInputElement=function(){return n.inputElement},n.getElement=function(){return n.element},n.destroy=function(){KTUtil.data(n.element).remove(\"image-input\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)},n.trigger=function(e,t){return KTEventHandler.trigger(n.element,e,t,n,t)}}};KTImageInput.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"image-input\")?KTUtil.data(e).get(\"image-input\"):null},KTImageInput.createInstances=function(e=\"[data-kt-image-input]\"){var t=document.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTImageInput(t[n])},KTImageInput.init=function(){KTImageInput.createInstances()},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTImageInput);var KTMenuHandlersInitialized=!1,KTMenu=function(e,t){var n=this;if(null!=e){var i={dropdown:{hoverTimeout:200,zindex:107},accordion:{slideSpeed:250,expand:!1}},r=function(){n.options=KTUtil.deepExtend({},i,t),n.uid=KTUtil.getUniqueId(\"menu\"),n.element=e,n.triggerElement,n.disabled=!1,n.element.setAttribute(\"data-kt-menu\",\"true\"),d(),u(),KTUtil.data(n.element).set(\"menu\",n)},o=function(e){e||(e=n.triggerElement),!0===m(e)?l(e):a(e)},a=function(e){e||(e=n.triggerElement),!0!==m(e)&&(\"dropdown\"===v(e)?y(e):\"accordion\"===v(e)&&A(e),KTUtil.data(e).set(\"type\",v(e)))},l=function(e){e||(e=n.triggerElement),!1!==m(e)&&(\"dropdown\"===v(e)?w(e):\"accordion\"===v(e)&&x(e))},s=function(e){if(!1!==f(e)){var t=g(e);KTUtil.data(e).has(\"type\")&&KTUtil.data(e).get(\"type\")!==v(e)&&(KTUtil.removeClass(e,\"hover\"),KTUtil.removeClass(e,\"show\"),KTUtil.removeClass(t,\"show\"))}},u=function(){var e=n.element.querySelectorAll(\".menu-item[data-kt-menu-trigger]\");if(e&&e.length>0)for(var t=0,i=e.length;t<i;t++)s(e[t])},d=function(){var e=document.querySelector('[data-kt-menu-target=\"#'+n.element.getAttribute(\"id\")+'\"]');null!==e?n.triggerElement=e:n.element.closest(\"[data-kt-menu-trigger]\")?n.triggerElement=n.element.closest(\"[data-kt-menu-trigger]\"):n.element.parentNode&&KTUtil.child(n.element.parentNode,\"[data-kt-menu-trigger]\")&&(n.triggerElement=KTUtil.child(n.element.parentNode,\"[data-kt-menu-trigger]\")),n.triggerElement&&KTUtil.data(n.triggerElement).set(\"menu\",n)},c=function(e){return n.triggerElement===e},m=function(e){var t=g(e);return null!==t&&(\"dropdown\"===v(e)?!0===KTUtil.hasClass(t,\"show\")&&!0===t.hasAttribute(\"data-popper-placement\"):KTUtil.hasClass(e,\"show\"))},f=function(e){return KTUtil.hasClass(e,\"menu-item\")&&e.hasAttribute(\"data-kt-menu-trigger\")},p=function(e){return KTUtil.child(e,\".menu-link\")},g=function(e){return!0===c(e)?n.element:!0===e.classList.contains(\"menu-sub\")?e:KTUtil.data(e).has(\"sub\")?KTUtil.data(e).get(\"sub\"):KTUtil.child(e,\".menu-sub\")},v=function(e){var t=g(e);return t&&parseInt(KTUtil.css(t,\"z-index\"))>0?\"dropdown\":\"accordion\"},T=function(e){var t,n;return c(e)||e.hasAttribute(\"data-kt-menu-trigger\")?e:KTUtil.data(e).has(\"item\")?KTUtil.data(e).get(\"item\"):(t=e.closest(\".menu-item\"))?t:(n=e.closest(\".menu-sub\"))&&!0===KTUtil.data(n).has(\"item\")?KTUtil.data(n).get(\"item\"):void 0},h=function(e){var t,n=e.closest(\".menu-sub\");return n&&KTUtil.data(n).has(\"item\")?KTUtil.data(n).get(\"item\"):n&&(t=n.closest(\".menu-item[data-kt-menu-trigger]\"))?t:null},K=function(e){var t,i=[],r=0;do{(t=h(e))&&(i.push(t),e=t),r++}while(null!==t&&r<20);return n.triggerElement&&i.unshift(n.triggerElement),i},b=function(e){var t=e;return KTUtil.data(e).get(\"sub\")&&(t=KTUtil.data(e).get(\"sub\")),null!==t&&t.querySelector(\".menu-item[data-kt-menu-trigger]\")||null},k=function(e){var t,n=[],i=0;do{(t=b(e))&&(n.push(t),e=t),i++}while(null!==t&&i<20);return n},y=function(e){if(!1!==KTEventHandler.trigger(n.element,\"kt.menu.dropdown.show\",e)){KTMenu.hideDropdowns(e);c(e)||p(e);var t=g(e),i=L(e,\"width\"),r=L(e,\"height\"),o=n.options.dropdown.zindex,a=KTUtil.getHighestZindex(e);null!==a&&a>=o&&(o=a+1),o>0&&KTUtil.css(t,\"z-index\",o),null!==i&&KTUtil.css(t,\"width\",i),null!==r&&KTUtil.css(t,\"height\",r),KTUtil.css(t,\"display\",\"\"),KTUtil.css(t,\"overflow\",\"\"),U(e,t),KTUtil.addClass(e,\"show\"),KTUtil.addClass(e,\"menu-dropdown\"),KTUtil.addClass(t,\"show\"),!0===L(e,\"overflow\")?(document.body.appendChild(t),KTUtil.data(e).set(\"sub\",t),KTUtil.data(t).set(\"item\",e),KTUtil.data(t).set(\"menu\",n)):KTUtil.data(t).set(\"item\",e),KTEventHandler.trigger(n.element,\"kt.menu.dropdown.shown\",e)}},w=function(e){if(!1!==KTEventHandler.trigger(n.element,\"kt.menu.dropdown.hide\",e)){var t=g(e);KTUtil.css(t,\"z-index\",\"\"),KTUtil.css(t,\"width\",\"\"),KTUtil.css(t,\"height\",\"\"),KTUtil.removeClass(e,\"show\"),KTUtil.removeClass(e,\"menu-dropdown\"),KTUtil.removeClass(t,\"show\"),!0===L(e,\"overflow\")&&(e.classList.contains(\"menu-item\")?e.appendChild(t):KTUtil.insertAfter(n.element,e),KTUtil.data(e).remove(\"sub\"),KTUtil.data(t).remove(\"item\"),KTUtil.data(t).remove(\"menu\")),E(e),KTEventHandler.trigger(n.element,\"kt.menu.dropdown.hidden\",e)}},U=function(e,t){var n,i=L(e,\"attach\");n=i?\"parent\"===i?e.parentNode:document.querySelector(i):e;var r=Popper.createPopper(n,t,S(e));KTUtil.data(e).set(\"popper\",r)},E=function(e){!0===KTUtil.data(e).has(\"popper\")&&(KTUtil.data(e).get(\"popper\").destroy(),KTUtil.data(e).remove(\"popper\"))},S=function(e){var t=L(e,\"placement\");t||(t=\"right\");var n=L(e,\"offset\"),i=n?n.split(\",\"):[];return 2===i.length&&(i[0]=parseInt(i[0]),i[1]=parseInt(i[1])),{placement:t,strategy:!0===L(e,\"overflow\")?\"absolute\":\"fixed\",modifiers:[{name:\"offset\",options:{offset:i}},{name:\"preventOverflow\",options:{altAxis:!1!==L(e,\"flip\")}},{name:\"flip\",options:{enabled:!1,flipVariations:!1}}]}},A=function(e){if(!1!==KTEventHandler.trigger(n.element,\"kt.menu.accordion.show\",e)){var t=g(e),i=n.options.accordion.expand;!0===L(e,\"expand\")?i=!0:!1===L(e,\"expand\")?i=!1:!0===L(n.element,\"expand\")&&(i=!0),!1===i&&I(e),!0===KTUtil.data(e).has(\"popper\")&&w(e),KTUtil.addClass(e,\"hover\"),KTUtil.addClass(e,\"showing\"),KTUtil.slideDown(t,n.options.accordion.slideSpeed,function(){KTUtil.removeClass(e,\"showing\"),KTUtil.addClass(e,\"show\"),KTUtil.addClass(t,\"show\"),KTEventHandler.trigger(n.element,\"kt.menu.accordion.shown\",e)})}},x=function(e){if(!1!==KTEventHandler.trigger(n.element,\"kt.menu.accordion.hide\",e)){var t=g(e);KTUtil.addClass(e,\"hiding\"),KTUtil.slideUp(t,n.options.accordion.slideSpeed,function(){KTUtil.removeClass(e,\"hiding\"),KTUtil.removeClass(e,\"show\"),KTUtil.removeClass(t,\"show\"),KTUtil.removeClass(e,\"hover\"),KTEventHandler.trigger(n.element,\"kt.menu.accordion.hidden\",e)})}},I=function(e){var t,i=KTUtil.findAll(n.element,\".show[data-kt-menu-trigger]\");if(i&&i.length>0)for(var r=0,o=i.length;r<o;r++)t=i[r],\"accordion\"===v(t)&&t!==e&&!1===e.contains(t)&&!1===t.contains(e)&&x(t)},L=function(e,t){var n,i=null;return e&&e.hasAttribute(\"data-kt-menu-\"+t)&&(n=e.getAttribute(\"data-kt-menu-\"+t),null!==(i=KTUtil.getResponsiveValue(n))&&\"true\"===String(i)?i=!0:null!==i&&\"false\"===String(i)&&(i=!1)),i};!0===KTUtil.data(e).has(\"menu\")?n=KTUtil.data(e).get(\"menu\"):r(),n.click=function(e,t){return function(e,t){if((!e.hasAttribute(\"href\")||\"#\"===e.getAttribute(\"href\"))&&(t.preventDefault(),!0!==n.disabled)){var i=T(e);\"click\"===L(i,\"trigger\")&&(!1===L(i,\"toggle\")?a(i):o(i))}}(e,t)},n.link=function(e,t){return function(e){!0!==n.disabled&&!1!==KTEventHandler.trigger(n.element,\"kt.menu.link.click\",e)&&(KTMenu.hideDropdowns(),KTEventHandler.trigger(n.element,\"kt.menu.link.clicked\",e))}(e)},n.dismiss=function(e,t){return function(e){var t=T(e),n=k(t);if(null!==t&&\"dropdown\"===v(t)&&(l(t),n.length>0))for(var i=0,r=n.length;i<r;i++)null!==n[i]&&\"dropdown\"===v(n[i])&&l(tems[i])}(e)},n.mouseover=function(e,t){return function(e){var t=T(e);!0!==n.disabled&&null!==t&&\"hover\"===L(t,\"trigger\")&&(\"1\"===KTUtil.data(t).get(\"hover\")&&(clearTimeout(KTUtil.data(t).get(\"timeout\")),KTUtil.data(t).remove(\"hover\"),KTUtil.data(t).remove(\"timeout\")),a(t))}(e)},n.mouseout=function(e,t){return function(e){var t=T(e);if(!0!==n.disabled&&null!==t&&\"hover\"===L(t,\"trigger\")){var i=setTimeout(function(){\"1\"===KTUtil.data(t).get(\"hover\")&&l(t)},n.options.dropdown.hoverTimeout);KTUtil.data(t).set(\"hover\",\"1\"),KTUtil.data(t).set(\"timeout\",i)}}(e)},n.getItemTriggerType=function(e){return L(e,\"trigger\")},n.getItemSubType=function(e){return v(e)},n.show=function(e){return a(e)},n.hide=function(e){return l(e)},n.toggle=function(e){return o(e)},n.reset=function(e){return s(e)},n.update=function(){return u()},n.getElement=function(){return n.element},n.setActiveLink=function(e){return function(e){var t=T(e);if(t){var i=K(t),r=e.closest(\".tab-pane\"),o=[].slice.call(n.element.querySelectorAll(\".menu-link.active\")),a=[].slice.call(n.element.querySelectorAll(\".menu-item.here, .menu-item.show\"));if(\"accordion\"===v(t)?A(t):t.classList.add(\"here\"),i&&i.length>0)for(var l=0,s=i.length;l<s;l++){var u=i[l];\"accordion\"===v(u)?A(u):u.classList.add(\"here\")}if(o.map(function(e){e.classList.remove(\"active\")}),a.map(function(e){!1===e.contains(t)&&(e.classList.remove(\"here\"),e.classList.remove(\"show\"))}),r&&bootstrap.Tab){var d=n.element.querySelector('[data-bs-target=\"#'+r.getAttribute(\"id\")+'\"]'),c=new bootstrap.Tab(d);c&&c.show()}e.classList.add(\"active\")}}(e)},n.getLinkByAttribute=function(e,t=\"href\"){return function(e,t=\"href\"){var i=n.element.querySelector(\".menu-link[\"+t+'=\"'+e+'\"]');if(i)return i}(e,t)},n.getItemLinkElement=function(e){return p(e)},n.getItemToggleElement=function(e){return function(e){return n.triggerElement?n.triggerElement:p(e)}(e)},n.getItemSubElement=function(e){return g(e)},n.getItemParentElements=function(e){return K(e)},n.isItemSubShown=function(e){return m(e)},n.isItemParentShown=function(e){return function(e){return KTUtil.parents(e,\".menu-item.show\").length>0}(e)},n.getTriggerElement=function(){return n.triggerElement},n.isItemDropdownPermanent=function(e){return function(e){return!0===L(e,\"permanent\")}(e)},n.destroy=function(){KTUtil.data(n.element).remove(\"menu\")},n.disable=function(){n.disabled=!0},n.enable=function(){n.disabled=!1},n.hideAccordions=function(e){return I(e)},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)}}};KTMenu.getInstance=function(e){var t;if(!e)return null;if(KTUtil.data(e).has(\"menu\"))return KTUtil.data(e).get(\"menu\");if((t=e.closest(\".menu\"))&&KTUtil.data(t).has(\"menu\"))return KTUtil.data(t).get(\"menu\");if(KTUtil.hasClass(e,\"menu-link\")){var n=e.closest(\".menu-sub\");if(KTUtil.data(n).has(\"menu\"))return KTUtil.data(n).get(\"menu\")}return null},KTMenu.hideDropdowns=function(e){var t=document.querySelectorAll(\".show.menu-dropdown[data-kt-menu-trigger]\");if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++){var r=t[n],o=KTMenu.getInstance(r);o&&\"dropdown\"===o.getItemSubType(r)&&(e?!1===o.getItemSubElement(r).contains(e)&&!1===r.contains(e)&&r!==e&&o.hide(r):o.hide(r))}},KTMenu.updateDropdowns=function(){var e=document.querySelectorAll(\".show.menu-dropdown[data-kt-menu-trigger]\");if(e&&e.length>0)for(var t=0,n=e.length;t<n;t++){var i=e[t];KTUtil.data(i).has(\"popper\")&&KTUtil.data(i).get(\"popper\").forceUpdate()}},KTMenu.initHandlers=function(){document.addEventListener(\"click\",function(e){var t,n,i,r=document.querySelectorAll('.show.menu-dropdown[data-kt-menu-trigger]:not([data-kt-menu-static=\"true\"])');if(r&&r.length>0)for(var o=0,a=r.length;o<a;o++)if(t=r[o],(i=KTMenu.getInstance(t))&&\"dropdown\"===i.getItemSubType(t)){if(i.getElement(),n=i.getItemSubElement(t),t===e.target||t.contains(e.target))continue;if(n===e.target||n.contains(e.target))continue;i.hide(t)}}),KTUtil.on(document.body,'.menu-item[data-kt-menu-trigger] > .menu-link, [data-kt-menu-trigger]:not(.menu-item):not([data-kt-menu-trigger=\"auto\"])',\"click\",function(e){var t=KTMenu.getInstance(this);if(null!==t)return t.click(this,e)}),KTUtil.on(document.body,\".menu-item:not([data-kt-menu-trigger]) > .menu-link\",\"click\",function(e){var t=KTMenu.getInstance(this);if(null!==t)return t.link(this,e)}),KTUtil.on(document.body,'[data-kt-menu-dismiss=\"true\"]',\"click\",function(e){var t=KTMenu.getInstance(this);if(null!==t)return t.dismiss(this,e)}),KTUtil.on(document.body,\"[data-kt-menu-trigger], .menu-sub\",\"mouseover\",function(e){var t=KTMenu.getInstance(this);if(null!==t&&\"dropdown\"===t.getItemSubType(this))return t.mouseover(this,e)}),KTUtil.on(document.body,\"[data-kt-menu-trigger], .menu-sub\",\"mouseout\",function(e){var t=KTMenu.getInstance(this);if(null!==t&&\"dropdown\"===t.getItemSubType(this))return t.mouseout(this,e)}),window.addEventListener(\"resize\",function(){var e;KTUtil.throttle(undefined,function(){var t=document.querySelectorAll('[data-kt-menu=\"true\"]');if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)(e=KTMenu.getInstance(t[n]))&&e.update()},200)})},KTMenu.updateByLinkAttribute=function(e,t=\"href\"){var n=document.querySelectorAll('[data-kt-menu=\"true\"]');if(n&&n.length>0)for(var i=0,r=n.length;i<r;i++){var o=KTMenu.getInstance(n[i]);if(o){var a=o.getLinkByAttribute(e,t);a&&o.setActiveLink(a)}}},KTMenu.createInstances=function(e='[data-kt-menu=\"true\"]'){var t=document.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTMenu(t[n])},KTMenu.init=function(){KTMenu.createInstances(),!1===KTMenuHandlersInitialized&&(KTMenu.initHandlers(),KTMenuHandlersInitialized=!0)},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTMenu);var KTPasswordMeter=function(e,t){var n=this;if(e){var i={minLength:8,checkUppercase:!0,checkLowercase:!0,checkDigit:!0,checkChar:!0,scoreHighlightClass:\"active\"},r=function(){n.options=KTUtil.deepExtend({},i,t),n.score=0,n.checkSteps=5,n.element=e,n.inputElement=n.element.querySelector(\"input[type]\"),n.visibilityElement=n.element.querySelector('[data-kt-password-meter-control=\"visibility\"]'),n.highlightElement=n.element.querySelector('[data-kt-password-meter-control=\"highlight\"]'),n.element.setAttribute(\"data-kt-password-meter\",\"true\"),o(),KTUtil.data(n.element).set(\"password-meter\",n)},o=function(){n.highlightElement&&n.inputElement.addEventListener(\"input\",function(){a()}),n.visibilityElement&&n.visibilityElement.addEventListener(\"click\",function(){p()})},a=function(){var e=0,t=m();!0===l()&&(e+=t),!0===n.options.checkUppercase&&!0===s()&&(e+=t),!0===n.options.checkLowercase&&!0===u()&&(e+=t),!0===n.options.checkDigit&&!0===d()&&(e+=t),!0===n.options.checkChar&&!0===c()&&(e+=t),n.score=e,f()},l=function(){return n.inputElement.value.length>=n.options.minLength},s=function(){return/[a-z]/.test(n.inputElement.value)},u=function(){return/[A-Z]/.test(n.inputElement.value)},d=function(){return/[0-9]/.test(n.inputElement.value)},c=function(){return/[~`!#@$%\\^&*+=\\-\\[\\]\\\\';,/{}|\\\\\":<>\\?]/g.test(n.inputElement.value)},m=function(){var e=1;return!0===n.options.checkUppercase&&e++,!0===n.options.checkLowercase&&e++,!0===n.options.checkDigit&&e++,!0===n.options.checkChar&&e++,n.checkSteps=e,100/n.checkSteps},f=function(){var e=[].slice.call(n.highlightElement.querySelectorAll(\"div\")),t=e.length,i=0,r=m(),o=g();e.map(function(e){i++,r*i*(n.checkSteps/t)<=o?e.classList.add(\"active\"):e.classList.remove(\"active\")})},p=function(){var e=n.visibilityElement.querySelector(\":scope > i:not(.d-none)\"),t=n.visibilityElement.querySelector(\":scope > i.d-none\");\"password\"===n.inputElement.getAttribute(\"type\").toLowerCase()?n.inputElement.setAttribute(\"type\",\"text\"):n.inputElement.setAttribute(\"type\",\"password\"),e.classList.add(\"d-none\"),t.classList.remove(\"d-none\"),n.inputElement.focus()},g=function(){return n.score};!0===KTUtil.data(e).has(\"password-meter\")?n=KTUtil.data(e).get(\"password-meter\"):r(),n.check=function(){return a()},n.getScore=function(){return g()},n.reset=function(){return n.score=0,void f()},n.destroy=function(){KTUtil.data(n.element).remove(\"password-meter\")}}};KTPasswordMeter.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"password-meter\")?KTUtil.data(e).get(\"password-meter\"):null},KTPasswordMeter.createInstances=function(e=\"[data-kt-password-meter]\"){var t=document.body.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTPasswordMeter(t[n])},KTPasswordMeter.init=function(){KTPasswordMeter.createInstances()},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTPasswordMeter);var KTScrollHandlersInitialized=!1,KTScroll=function(e,t){var n=this;if(e){var i={saveState:!0},r=function(){n.options=KTUtil.deepExtend({},i,t),n.element=e,n.id=n.element.getAttribute(\"id\"),n.element.setAttribute(\"data-kt-scroll\",\"true\"),l(),KTUtil.data(n.element).set(\"scroll\",n)},o=function(e){return document.body.hasAttribute(\"data-kt-name\")?document.body.getAttribute(\"data-kt-name\")+\"_\":\"\"},a=function(){var e=o();localStorage.setItem(e+n.id+\"st\",n.element.scrollTop)},l=function(){var e,t;!0===f(\"activate\")||!1===n.element.hasAttribute(\"data-kt-scroll-activate\")?(e=p(),null!==(t=u())&&t.length>0?KTUtil.css(n.element,e,t):KTUtil.css(n.element,e,\"\"),s(),!0===f(\"save-state\")&&n.id?n.element.addEventListener(\"scroll\",a):n.element.removeEventListener(\"scroll\",a),function(){var e=o();if(!0===f(\"save-state\")&&n.id&&localStorage.getItem(e+n.id+\"st\")){var t=parseInt(localStorage.getItem(e+n.id+\"st\"));t>0&&n.element.scroll({top:t,behavior:\"instant\"})}}()):(KTUtil.css(n.element,p(),\"\"),n.element.removeEventListener(\"scroll\",a))},s=function(){var e=f(\"stretch\");if(null!==e){var t=document.querySelectorAll(e);if(t&&2==t.length){var i=t[0],r=t[1],o=c(r)-c(i);if(o>0){var a=parseInt(KTUtil.css(n.element,p()))+o;KTUtil.css(n.element,p(),String(a)+\"px\")}}}},u=function(){var e=f(p());return e instanceof Function?e.call():null!==e&&\"string\"==typeof e&&\"auto\"===e.toLowerCase()?d():e},d=function(){var e,t=KTUtil.getViewPort().height,i=f(\"dependencies\"),r=f(\"wrappers\"),o=f(\"offset\");if((t-=m(n.element),null!==i)&&((e=document.querySelectorAll(i))&&e.length>0))for(var a=0,l=e.length;a<l;a++)!1!==KTUtil.visible(e[a])&&(t-=c(e[a]));if(null!==r&&((e=document.querySelectorAll(r))&&e.length>0))for(a=0,l=e.length;a<l;a++)!1!==KTUtil.visible(e[a])&&(t-=m(e[a]));return null!==o&&\"object\"!=typeof o&&(t-=parseInt(o)),String(t)+\"px\"},c=function(e){var t=0;return null!==e&&(t+=parseInt(KTUtil.css(e,\"height\")),t+=parseInt(KTUtil.css(e,\"margin-top\")),t+=parseInt(KTUtil.css(e,\"margin-bottom\")),KTUtil.css(e,\"border-top\")&&(t+=parseInt(KTUtil.css(e,\"border-top\"))),KTUtil.css(e,\"border-bottom\")&&(t+=parseInt(KTUtil.css(e,\"border-bottom\")))),t},m=function(e){var t=0;return null!==e&&(t+=parseInt(KTUtil.css(e,\"margin-top\")),t+=parseInt(KTUtil.css(e,\"margin-bottom\")),t+=parseInt(KTUtil.css(e,\"padding-top\")),t+=parseInt(KTUtil.css(e,\"padding-bottom\")),KTUtil.css(e,\"border-top\")&&(t+=parseInt(KTUtil.css(e,\"border-top\"))),KTUtil.css(e,\"border-bottom\")&&(t+=parseInt(KTUtil.css(e,\"border-bottom\")))),t},f=function(e){if(!0===n.element.hasAttribute(\"data-kt-scroll-\"+e)){var t=n.element.getAttribute(\"data-kt-scroll-\"+e),i=KTUtil.getResponsiveValue(t);return null!==i&&\"true\"===String(i)?i=!0:null!==i&&\"false\"===String(i)&&(i=!1),i}var r=KTUtil.snakeToCamel(e);return n.options[r]?KTUtil.getResponsiveValue(n.options[r]):null},p=function(){return f(\"height\")?\"height\":f(\"min-height\")?\"min-height\":f(\"max-height\")?\"max-height\":void 0};KTUtil.data(e).has(\"scroll\")?n=KTUtil.data(e).get(\"scroll\"):r(),n.update=function(){return l()},n.getHeight=function(){return u()},n.getElement=function(){return n.element},n.destroy=function(){KTUtil.data(n.element).remove(\"scroll\")}}};KTScroll.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"scroll\")?KTUtil.data(e).get(\"scroll\"):null},KTScroll.createInstances=function(e='[data-kt-scroll=\"true\"]'){var t=document.body.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTScroll(t[n])},KTScroll.handleResize=function(){window.addEventListener(\"resize\",function(){KTUtil.throttle(undefined,function(){var e=document.body.querySelectorAll('[data-kt-scroll=\"true\"]');if(e&&e.length>0)for(var t=0,n=e.length;t<n;t++){var i=KTScroll.getInstance(e[t]);i&&i.update()}},200)})},KTScroll.init=function(){KTScroll.createInstances(),!1===KTScrollHandlersInitialized&&(KTScroll.handleResize(),KTScrollHandlersInitialized=!0)},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTScroll);var KTScrolltop=function(e,t){var n=this;if(null!=e){var i={offset:300,speed:600},r=function(){n.options=KTUtil.deepExtend({},i,t),n.uid=KTUtil.getUniqueId(\"scrolltop\"),n.element=e,n.element.setAttribute(\"data-kt-scrolltop\",\"true\"),o(),KTUtil.data(n.element).set(\"scrolltop\",n)},o=function(){window.addEventListener(\"scroll\",function(){KTUtil.throttle(undefined,function(){a()},200)}),KTUtil.addEvent(n.element,\"click\",function(e){e.preventDefault(),l()})},a=function(){var e=parseInt(s(\"offset\"));KTUtil.getScrollTop()>e?!1===document.body.hasAttribute(\"data-kt-scrolltop\")&&document.body.setAttribute(\"data-kt-scrolltop\",\"on\"):!0===document.body.hasAttribute(\"data-kt-scrolltop\")&&document.body.removeAttribute(\"data-kt-scrolltop\")},l=function(){parseInt(s(\"speed\"));window.scrollTo({top:0,behavior:\"smooth\"})},s=function(e){if(!0===n.element.hasAttribute(\"data-kt-scrolltop-\"+e)){var t=n.element.getAttribute(\"data-kt-scrolltop-\"+e),i=KTUtil.getResponsiveValue(t);return null!==i&&\"true\"===String(i)?i=!0:null!==i&&\"false\"===String(i)&&(i=!1),i}var r=KTUtil.snakeToCamel(e);return n.options[r]?KTUtil.getResponsiveValue(n.options[r]):null};KTUtil.data(e).has(\"scrolltop\")?n=KTUtil.data(e).get(\"scrolltop\"):r(),n.go=function(){return l()},n.getElement=function(){return n.element},n.destroy=function(){KTUtil.data(n.element).remove(\"scrolltop\")}}};KTScrolltop.getInstance=function(e){return e&&KTUtil.data(e).has(\"scrolltop\")?KTUtil.data(e).get(\"scrolltop\"):null},KTScrolltop.createInstances=function(e='[data-kt-scrolltop=\"true\"]'){var t=document.body.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTScrolltop(t[n])},KTScrolltop.init=function(){KTScrolltop.createInstances()},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTScrolltop);var KTSearch=function(e,t){var n=this;if(e){var i={minLength:2,keypress:!0,enter:!0,layout:\"menu\",responsive:null,showOnFocus:!0},r=function(){n.options=KTUtil.deepExtend({},i,t),n.processing=!1,n.element=e,n.contentElement=v(\"content\"),n.formElement=v(\"form\"),n.inputElement=v(\"input\"),n.spinnerElement=v(\"spinner\"),n.clearElement=v(\"clear\"),n.toggleElement=v(\"toggle\"),n.submitElement=v(\"submit\"),n.toolbarElement=v(\"toolbar\"),n.minLength=parseInt(g(\"min-length\")),n.resultsElement=v(\"results\"),n.suggestionElement=v(\"suggestion\"),n.emptyElement=v(\"empty\"),n.element.setAttribute(\"data-kt-search\",\"true\"),n.layout=g(\"layout\"),\"menu\"===n.layout?n.menuObject=new KTMenu(n.contentElement):n.menuObject=null,m(),o(),KTUtil.data(n.element).set(\"search\",n)},o=function(){n.inputElement.addEventListener(\"focus\",a),n.inputElement.addEventListener(\"blur\",l),!0===g(\"keypress\")&&n.inputElement.addEventListener(\"input\",u),n.submitElement&&n.submitElement.addEventListener(\"click\",d),!0===g(\"enter\")&&n.inputElement.addEventListener(\"keypress\",s),n.clearElement&&n.clearElement.addEventListener(\"click\",c),n.menuObject&&(n.toggleElement&&(n.toggleElement.addEventListener(\"click\",f),n.menuObject.on(\"kt.menu.dropdown.show\",function(e){KTUtil.visible(n.toggleElement)&&(n.toggleElement.classList.add(\"active\"),n.toggleElement.classList.add(\"show\"))}),n.menuObject.on(\"kt.menu.dropdown.hide\",function(e){KTUtil.visible(n.toggleElement)&&(n.toggleElement.classList.remove(\"active\"),n.toggleElement.classList.remove(\"show\"))})),n.menuObject.on(\"kt.menu.dropdown.shown\",function(){n.inputElement.focus()})),window.addEventListener(\"resize\",function(){KTUtil.throttle(undefined,function(){m()},200)})},a=function(){n.element.classList.add(\"focus\"),(!0===g(\"show-on-focus\")||n.inputElement.value.length>=n.minLength)&&f()},l=function(){n.element.classList.remove(\"focus\")},s=function(e){13==(e.charCode||e.keyCode||0)&&(e.preventDefault(),d())},u=function(){g(\"min-length\")&&(n.inputElement.value.length>=n.minLength?d():0===n.inputElement.value.length&&c())},d=function(){!1===n.processing&&(n.spinnerElement&&n.spinnerElement.classList.remove(\"d-none\"),n.clearElement&&n.clearElement.classList.add(\"d-none\"),n.toolbarElement&&n.formElement.contains(n.toolbarElement)&&n.toolbarElement.classList.add(\"d-none\"),n.inputElement.focus(),n.processing=!0,KTEventHandler.trigger(n.element,\"kt.search.process\",n))},c=function(){!1!==KTEventHandler.trigger(n.element,\"kt.search.clear\",n)&&(n.inputElement.value=\"\",n.inputElement.focus(),n.clearElement&&n.clearElement.classList.add(\"d-none\"),n.toolbarElement&&n.formElement.contains(n.toolbarElement)&&n.toolbarElement.classList.remove(\"d-none\"),!1===g(\"show-on-focus\")&&p(),KTEventHandler.trigger(n.element,\"kt.search.cleared\",n))},m=function(){if(\"menu\"===n.layout){var e=T();\"on\"===e&&!1===n.contentElement.contains(n.formElement)?(n.contentElement.prepend(n.formElement),n.formElement.classList.remove(\"d-none\")):\"off\"===e&&!0===n.contentElement.contains(n.formElement)&&(n.element.prepend(n.formElement),n.formElement.classList.add(\"d-none\"))}},f=function(){n.menuObject&&(m(),n.menuObject.show(n.element))},p=function(){n.menuObject&&(m(),n.menuObject.hide(n.element))},g=function(e){if(!0===n.element.hasAttribute(\"data-kt-search-\"+e)){var t=n.element.getAttribute(\"data-kt-search-\"+e),i=KTUtil.getResponsiveValue(t);return null!==i&&\"true\"===String(i)?i=!0:null!==i&&\"false\"===String(i)&&(i=!1),i}var r=KTUtil.snakeToCamel(e);return null!=n.options[r]?KTUtil.getResponsiveValue(n.options[r]):null},v=function(e){return n.element.querySelector('[data-kt-search-element=\"'+e+'\"]')},T=function(){var e=g(\"responsive\"),t=KTUtil.getViewPort().width;if(!e)return null;var n=KTUtil.getBreakpoint(e);return n||(n=parseInt(e)),t<n?\"on\":\"off\"};!0===KTUtil.data(e).has(\"search\")?n=KTUtil.data(e).get(\"search\"):r(),n.show=function(){return f()},n.hide=function(){return p()},n.update=function(){return m()},n.search=function(){return d()},n.complete=function(){return n.spinnerElement&&n.spinnerElement.classList.add(\"d-none\"),n.clearElement&&n.clearElement.classList.remove(\"d-none\"),0===n.inputElement.value.length&&c(),n.inputElement.focus(),f(),void(n.processing=!1)},n.clear=function(){return c()},n.isProcessing=function(){return n.processing},n.getQuery=function(){return n.inputElement.value},n.getMenu=function(){return n.menuObject},n.getFormElement=function(){return n.formElement},n.getInputElement=function(){return n.inputElement},n.getContentElement=function(){return n.contentElement},n.getElement=function(){return n.element},n.destroy=function(){KTUtil.data(n.element).remove(\"search\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)}}};KTSearch.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"search\")?KTUtil.data(e).get(\"search\"):null},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTSearch);var KTStepper=function(e,t){var n=this;if(null!=e){var i={startIndex:1,animation:!1,animationSpeed:\"0.3s\",animationNextClass:\"animate__animated animate__slideInRight animate__fast\",animationPreviousClass:\"animate__animated animate__slideInLeft animate__fast\"},r=function(){n.options=KTUtil.deepExtend({},i,t),n.uid=KTUtil.getUniqueId(\"stepper\"),n.element=e,n.element.setAttribute(\"data-kt-stepper\",\"true\"),n.steps=KTUtil.findAll(n.element,'[data-kt-stepper-element=\"nav\"]'),n.btnNext=KTUtil.find(n.element,'[data-kt-stepper-action=\"next\"]'),n.btnPrevious=KTUtil.find(n.element,'[data-kt-stepper-action=\"previous\"]'),n.btnSubmit=KTUtil.find(n.element,'[data-kt-stepper-action=\"submit\"]'),n.totalStepsNumber=n.steps.length,n.passedStepIndex=0,n.currentStepIndex=1,n.clickedStepIndex=0,n.options.startIndex>1&&o(n.options.startIndex),n.nextListener=function(e){e.preventDefault(),KTEventHandler.trigger(n.element,\"kt.stepper.next\",n)},n.previousListener=function(e){e.preventDefault(),KTEventHandler.trigger(n.element,\"kt.stepper.previous\",n)},n.submitListener=function(e){e.preventDefault(),KTEventHandler.trigger(n.element,\"kt.stepper.submit\",n)},n.stepListener=function(e){if(e.preventDefault(),n.steps&&n.steps.length>0)for(var t=0,i=n.steps.length;t<i;t++)if(n.steps[t]===this)return n.clickedStepIndex=t+1,void KTEventHandler.trigger(n.element,\"kt.stepper.click\",n)},KTUtil.addEvent(n.btnNext,\"click\",n.nextListener),KTUtil.addEvent(n.btnPrevious,\"click\",n.previousListener),KTUtil.addEvent(n.btnSubmit,\"click\",n.submitListener),n.stepListenerId=KTUtil.on(n.element,'[data-kt-stepper-action=\"step\"]',\"click\",n.stepListener),KTUtil.data(n.element).set(\"stepper\",n)},o=function(e){if(KTEventHandler.trigger(n.element,\"kt.stepper.change\",n),!(e===n.currentStepIndex||e>n.totalStepsNumber||e<0))return e=parseInt(e),n.passedStepIndex=n.currentStepIndex,n.currentStepIndex=e,a(),KTEventHandler.trigger(n.element,\"kt.stepper.changed\",n),n},a=function(){var e=\"\";e=l()?\"last\":s()?\"first\":\"between\",KTUtil.removeClass(n.element,\"last\"),KTUtil.removeClass(n.element,\"first\"),KTUtil.removeClass(n.element,\"between\"),KTUtil.addClass(n.element,e);var t=KTUtil.findAll(n.element,'[data-kt-stepper-element=\"nav\"], [data-kt-stepper-element=\"content\"], [data-kt-stepper-element=\"info\"]');if(t&&t.length>0)for(var i=0,r=t.length;i<r;i++){var o=t[i],a=KTUtil.index(o)+1;if(KTUtil.removeClass(o,\"current\"),KTUtil.removeClass(o,\"completed\"),KTUtil.removeClass(o,\"pending\"),a==n.currentStepIndex){if(KTUtil.addClass(o,\"current\"),!1!==n.options.animation&&\"content\"==o.getAttribute(\"data-kt-stepper-element\")){KTUtil.css(o,\"animationDuration\",n.options.animationSpeed);var u=\"previous\"===f(n.passedStepIndex)?n.options.animationPreviousClass:n.options.animationNextClass;KTUtil.animateClass(o,u)}}else a<n.currentStepIndex?KTUtil.addClass(o,\"completed\"):KTUtil.addClass(o,\"pending\")}},l=function(){return n.currentStepIndex===n.totalStepsNumber},s=function(){return 1===n.currentStepIndex},u=function(){return n.totalStepsNumber>=n.currentStepIndex+1?n.currentStepIndex+1:n.totalStepsNumber},d=function(){return n.currentStepIndex-1>1?n.currentStepIndex-1:1},c=function(){return 1},m=function(){return n.totalStepsNumber},f=function(e){return e>n.currentStepIndex?\"next\":\"previous\"};!0===KTUtil.data(e).has(\"stepper\")?n=KTUtil.data(e).get(\"stepper\"):r(),n.getElement=function(e){return n.element},n.goTo=function(e){return o(e)},n.goPrevious=function(){return o(d())},n.goNext=function(){return o(u())},n.goFirst=function(){return o(c())},n.goLast=function(){return o(m())},n.getCurrentStepIndex=function(){return n.currentStepIndex},n.getNextStepIndex=function(){return u()},n.getPassedStepIndex=function(){return n.passedStepIndex},n.getClickedStepIndex=function(){return n.clickedStepIndex},n.getPreviousStepIndex=function(){return d()},n.destroy=function(){return KTUtil.removeEvent(n.btnNext,\"click\",n.nextListener),KTUtil.removeEvent(n.btnPrevious,\"click\",n.previousListener),KTUtil.off(n.element,\"click\",n.stepListenerId),void KTUtil.data(n.element).remove(\"stepper\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)},n.trigger=function(e,t){return KTEventHandler.trigger(n.element,e,t,n,t)}}};KTStepper.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"stepper\")?KTUtil.data(e).get(\"stepper\"):null},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTStepper);var KTStickyHandlersInitialized=!1,KTSticky=function(e,t){var n=this;if(null!=e){var i={offset:200,reverse:!1,release:null,animation:!0,animationSpeed:\"0.3s\",animationClass:\"animation-slide-in-down\"},r=function(){n.element=e,n.options=KTUtil.deepExtend({},i,t),n.uid=KTUtil.getUniqueId(\"sticky\"),n.name=n.element.getAttribute(\"data-kt-sticky-name\"),n.attributeName=\"data-kt-sticky-\"+n.name,n.attributeName2=\"data-kt-\"+n.name,n.eventTriggerState=!0,n.lastScrollTop=0,n.scrollHandler,n.element.setAttribute(\"data-kt-sticky\",\"true\"),window.addEventListener(\"scroll\",o),o(),KTUtil.data(n.element).set(\"sticky\",n)},o=function(e){var t,i=u(\"offset\"),r=u(\"release\"),o=u(\"reverse\");if(!1!==i){i=parseInt(i),r=r?document.querySelector(r):null,t=KTUtil.getScrollTop(),document.documentElement.scrollHeight-window.innerHeight-KTUtil.getScrollTop();var s=!r||r.offsetTop-r.clientHeight>t;if(!0===o){if(t>i&&s){if(!1===document.body.hasAttribute(n.attributeName)){if(!1===a())return;document.body.setAttribute(n.attributeName,\"on\"),document.body.setAttribute(n.attributeName2,\"on\"),n.element.setAttribute(\"data-kt-sticky-enabled\",\"true\")}!0===n.eventTriggerState&&(KTEventHandler.trigger(n.element,\"kt.sticky.on\",n),KTEventHandler.trigger(n.element,\"kt.sticky.change\",n),n.eventTriggerState=!1)}else!0===document.body.hasAttribute(n.attributeName)&&(l(),document.body.removeAttribute(n.attributeName),document.body.removeAttribute(n.attributeName2),n.element.removeAttribute(\"data-kt-sticky-enabled\")),!1===n.eventTriggerState&&(KTEventHandler.trigger(n.element,\"kt.sticky.off\",n),KTEventHandler.trigger(n.element,\"kt.sticky.change\",n),n.eventTriggerState=!0);n.lastScrollTop=t}else if(t>i&&s){if(!1===document.body.hasAttribute(n.attributeName)){if(!1===a())return;document.body.setAttribute(n.attributeName,\"on\"),document.body.setAttribute(n.attributeName2,\"on\"),n.element.setAttribute(\"data-kt-sticky-enabled\",\"true\")}!0===n.eventTriggerState&&(KTEventHandler.trigger(n.element,\"kt.sticky.on\",n),KTEventHandler.trigger(n.element,\"kt.sticky.change\",n),n.eventTriggerState=!1)}else!0===document.body.hasAttribute(n.attributeName)&&(l(),document.body.removeAttribute(n.attributeName),document.body.removeAttribute(n.attributeName2),n.element.removeAttribute(\"data-kt-sticky-enabled\")),!1===n.eventTriggerState&&(KTEventHandler.trigger(n.element,\"kt.sticky.off\",n),KTEventHandler.trigger(n.element,\"kt.sticky.change\",n),n.eventTriggerState=!0);r&&(r.offsetTop-r.clientHeight>t?n.element.setAttribute(\"data-kt-sticky-released\",\"true\"):n.element.removeAttribute(\"data-kt-sticky-released\"))}else l()},a=function(e){var t=u(\"top\");t=t?parseInt(t):0;var i=u(\"left\"),r=u(\"right\"),o=u(\"width\"),a=u(\"zindex\"),l=u(\"dependencies\"),d=u(\"class\"),c=s(),m=u(\"height-offset\");if(c+(m=m?parseInt(m):0)+t>KTUtil.getViewPort().height)return!1;if(!0!==e&&!0===u(\"animation\")&&(KTUtil.css(n.element,\"animationDuration\",u(\"animationSpeed\")),KTUtil.animateClass(n.element,\"animation \"+u(\"animationClass\"))),null!==d&&KTUtil.addClass(n.element,d),null!==a&&(KTUtil.css(n.element,\"z-index\",a),KTUtil.css(n.element,\"position\",\"fixed\")),t>=0&&KTUtil.css(n.element,\"top\",String(t)+\"px\"),null!==o){if(o.target){var f=document.querySelector(o.target);f&&(o=KTUtil.css(f,\"width\"))}KTUtil.css(n.element,\"width\",o)}if(null!==i)if(\"auto\"===String(i).toLowerCase()){var p=KTUtil.offset(n.element).left;p>=0&&KTUtil.css(n.element,\"left\",String(p)+\"px\")}else KTUtil.css(n.element,\"left\",i);if(null!==r&&KTUtil.css(n.element,\"right\",r),null!==l){var g=document.querySelectorAll(l);if(g&&g.length>0)for(var v=0,T=g.length;v<T;v++)KTUtil.css(g[v],\"padding-top\",String(c)+\"px\")}},l=function(){KTUtil.css(n.element,\"top\",\"\"),KTUtil.css(n.element,\"width\",\"\"),KTUtil.css(n.element,\"left\",\"\"),KTUtil.css(n.element,\"right\",\"\"),KTUtil.css(n.element,\"z-index\",\"\"),KTUtil.css(n.element,\"position\",\"\");var e=u(\"dependencies\"),t=u(\"class\");if(null!==t&&KTUtil.removeClass(n.element,t),null!==e){var i=document.querySelectorAll(e);if(i&&i.length>0)for(var r=0,o=i.length;r<o;r++)KTUtil.css(i[r],\"padding-top\",\"\")}},s=function(){var t=parseFloat(KTUtil.css(n.element,\"height\"));return t+=parseFloat(KTUtil.css(n.element,\"margin-top\")),t+=parseFloat(KTUtil.css(n.element,\"margin-bottom\")),KTUtil.css(e,\"border-top\")&&(t+=parseFloat(KTUtil.css(n.element,\"border-top\"))),KTUtil.css(e,\"border-bottom\")&&(t+=parseFloat(KTUtil.css(n.element,\"border-bottom\"))),t},u=function(e){if(!0===n.element.hasAttribute(\"data-kt-sticky-\"+e)){var t=n.element.getAttribute(\"data-kt-sticky-\"+e),i=KTUtil.getResponsiveValue(t);return null!==i&&\"true\"===String(i)?i=!0:null!==i&&\"false\"===String(i)&&(i=!1),i}var r=KTUtil.snakeToCamel(e);return n.options[r]?KTUtil.getResponsiveValue(n.options[r]):null};!0===KTUtil.data(e).has(\"sticky\")?n=KTUtil.data(e).get(\"sticky\"):r(),n.update=function(){!0===document.body.hasAttribute(n.attributeName)&&(l(),document.body.removeAttribute(n.attributeName),document.body.removeAttribute(n.attributeName2),a(!0),document.body.setAttribute(n.attributeName,\"on\"),document.body.setAttribute(n.attributeName2,\"on\"))},n.destroy=function(){return window.removeEventListener(\"scroll\",o),void KTUtil.data(n.element).remove(\"sticky\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)},n.trigger=function(e,t){return KTEventHandler.trigger(n.element,e,t,n,t)}}};KTSticky.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"sticky\")?KTUtil.data(e).get(\"sticky\"):null},KTSticky.createInstances=function(e='[data-kt-sticky=\"true\"]'){var t=document.body.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTSticky(t[n])},KTSticky.handleResize=function(){window.addEventListener(\"resize\",function(){KTUtil.throttle(undefined,function(){var e=document.body.querySelectorAll('[data-kt-sticky=\"true\"]');if(e&&e.length>0)for(var t=0,n=e.length;t<n;t++){var i=KTSticky.getInstance(e[t]);i&&i.update()}},200)})},KTSticky.init=function(){KTSticky.createInstances(),!1===KTStickyHandlersInitialized&&(KTSticky.handleResize(),KTStickyHandlersInitialized=!0)},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTSticky);var KTSwapperHandlersInitialized=!1,KTSwapper=function(e,t){var n=this;if(null!=e){var i={mode:\"append\"},r=function(){n.element=e,n.options=KTUtil.deepExtend({},i,t),n.element.setAttribute(\"data-kt-swapper\",\"true\"),o(),KTUtil.data(n.element).set(\"swapper\",n)},o=function(t){var n=a(\"parent\"),i=a(\"mode\"),r=n?document.querySelector(n):null;r&&e.parentNode!==r&&(\"prepend\"===i?r.prepend(e):\"append\"===i&&r.append(e))},a=function(e){if(!0===n.element.hasAttribute(\"data-kt-swapper-\"+e)){var t=n.element.getAttribute(\"data-kt-swapper-\"+e),i=KTUtil.getResponsiveValue(t);return null!==i&&\"true\"===String(i)?i=!0:null!==i&&\"false\"===String(i)&&(i=!1),i}var r=KTUtil.snakeToCamel(e);return n.options[r]?KTUtil.getResponsiveValue(n.options[r]):null};!0===KTUtil.data(e).has(\"swapper\")?n=KTUtil.data(e).get(\"swapper\"):r(),n.update=function(){o()},n.destroy=function(){KTUtil.data(n.element).remove(\"swapper\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)},n.trigger=function(e,t){return KTEventHandler.trigger(n.element,e,t,n,t)}}};KTSwapper.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"swapper\")?KTUtil.data(e).get(\"swapper\"):null},KTSwapper.createInstances=function(e='[data-kt-swapper=\"true\"]'){var t=document.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTSwapper(t[n])},KTSwapper.handleResize=function(){window.addEventListener(\"resize\",function(){KTUtil.throttle(undefined,function(){var e=document.querySelectorAll('[data-kt-swapper=\"true\"]');if(e&&e.length>0)for(var t=0,n=e.length;t<n;t++){var i=KTSwapper.getInstance(e[t]);i&&i.update()}},200)})},KTSwapper.init=function(){KTSwapper.createInstances(),!1===KTSwapperHandlersInitialized&&(KTSwapper.handleResize(),KTSwapperHandlersInitialized=!0)},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTSwapper);var KTToggle=function(e,t){var n=this;if(e){var i={saveState:!0},r=function(){n.options=KTUtil.deepExtend({},i,t),n.uid=KTUtil.getUniqueId(\"toggle\"),n.element=e,n.target=document.querySelector(n.element.getAttribute(\"data-kt-toggle-target\"))?document.querySelector(n.element.getAttribute(\"data-kt-toggle-target\")):n.element,n.state=n.element.hasAttribute(\"data-kt-toggle-state\")?n.element.getAttribute(\"data-kt-toggle-state\"):\"\",n.mode=n.element.hasAttribute(\"data-kt-toggle-mode\")?n.element.getAttribute(\"data-kt-toggle-mode\"):\"\",n.attribute=\"data-kt-\"+n.element.getAttribute(\"data-kt-toggle-name\"),o(),KTUtil.data(n.element).set(\"toggle\",n)},o=function(){KTUtil.addEvent(n.element,\"click\",function(e){e.preventDefault(),\"\"!==n.mode?(\"off\"===n.mode&&!1===u()||\"on\"===n.mode&&!0===u())&&a():a()})},a=function(){return KTEventHandler.trigger(n.element,\"kt.toggle.change\",n),u()?s():l(),KTEventHandler.trigger(n.element,\"kt.toggle.changed\",n),n},l=function(){if(!0!==u())return KTEventHandler.trigger(n.element,\"kt.toggle.enable\",n),n.target.setAttribute(n.attribute,\"on\"),n.state.length>0&&n.element.classList.add(n.state),void 0!==KTCookie&&!0===n.options.saveState&&KTCookie.set(n.attribute,\"on\"),KTEventHandler.trigger(n.element,\"kt.toggle.enabled\",n),n},s=function(){if(!1!==u())return KTEventHandler.trigger(n.element,\"kt.toggle.disable\",n),n.target.removeAttribute(n.attribute),n.state.length>0&&n.element.classList.remove(n.state),void 0!==KTCookie&&!0===n.options.saveState&&KTCookie.remove(n.attribute),KTEventHandler.trigger(n.element,\"kt.toggle.disabled\",n),n},u=function(){return\"on\"===String(n.target.getAttribute(n.attribute)).toLowerCase()};!0===KTUtil.data(e).has(\"toggle\")?n=KTUtil.data(e).get(\"toggle\"):r(),n.toggle=function(){return a()},n.enable=function(){return l()},n.disable=function(){return s()},n.isEnabled=function(){return u()},n.goElement=function(){return n.element},n.destroy=function(){KTUtil.data(n.element).remove(\"toggle\")},n.on=function(e,t){return KTEventHandler.on(n.element,e,t)},n.one=function(e,t){return KTEventHandler.one(n.element,e,t)},n.off=function(e,t){return KTEventHandler.off(n.element,e,t)},n.trigger=function(e,t){return KTEventHandler.trigger(n.element,e,t,n,t)}}};KTToggle.getInstance=function(e){return null!==e&&KTUtil.data(e).has(\"toggle\")?KTUtil.data(e).get(\"toggle\"):null},KTToggle.createInstances=function(e=\"[data-kt-toggle]\"){var t=document.body.querySelectorAll(e);if(t&&t.length>0)for(var n=0,i=t.length;n<i;n++)new KTToggle(t[n])},KTToggle.init=function(){KTToggle.createInstances()},\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTToggle),Element.prototype.matches||(Element.prototype.matches=function(e){for(var t=(this.document||this.ownerDocument).querySelectorAll(e),n=t.length;--n>=0&&t.item(n)!==this;);return n>-1}),Element.prototype.closest||(Element.prototype.closest=function(e){var t=this;if(!document.documentElement.contains(this))return null;do{if(t.matches(e))return t;t=t.parentElement}while(null!==t);return null})\n/**\n * ChildNode.remove() polyfill\n * https://gomakethings.com/removing-an-element-from-the-dom-the-es6-way/\n * @author Chris Ferdinandi\n * @license MIT\n */,function(e){for(var t=0;t<e.length;t++)window[e[t]]&&!(\"remove\"in window[e[t]].prototype)&&(window[e[t]].prototype.remove=function(){this.parentNode.removeChild(this)})}([\"Element\",\"CharacterData\",\"DocumentType\"]),function(){for(var e=0,t=[\"webkit\",\"moz\"],n=0;n<t.length&&!window.requestAnimationFrame;++n)window.requestAnimationFrame=window[t[n]+\"RequestAnimationFrame\"],window.cancelAnimationFrame=window[t[n]+\"CancelAnimationFrame\"]||window[t[n]+\"CancelRequestAnimationFrame\"];window.requestAnimationFrame||(window.requestAnimationFrame=function(t){var n=(new Date).getTime(),i=Math.max(0,16-(n-e)),r=window.setTimeout(function(){t(n+i)},i);return e=n+i,r}),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(e){clearTimeout(e)})}(),[Element.prototype,Document.prototype,DocumentFragment.prototype].forEach(function(e){e.hasOwnProperty(\"prepend\")||Object.defineProperty(e,\"prepend\",{configurable:!0,enumerable:!0,writable:!0,value:function(){var e=Array.prototype.slice.call(arguments),t=document.createDocumentFragment();e.forEach(function(e){var n=e instanceof Node;t.appendChild(n?e:document.createTextNode(String(e)))}),this.insertBefore(t,this.firstChild)}})}),null==Element.prototype.getAttributeNames&&(Element.prototype.getAttributeNames=function(){for(var e=this.attributes,t=e.length,n=new Array(t),i=0;i<t;i++)n[i]=e[i].name;return n}),window.KTUtilElementDataStore={},window.KTUtilElementDataStoreID=0,window.KTUtilDelegatedEventHandlers={};var KTUtil=function(){var e=[],t=function(){window.addEventListener(\"resize\",function(){KTUtil.throttle(undefined,function(){!function(){for(var t=0;t<e.length;t++)e[t].call()}()},200)})};return{init:function(e){t()},addResizeHandler:function(t){e.push(t)},removeResizeHandler:function(t){for(var n=0;n<e.length;n++)t===e[n]&&delete e[n]},runResizeHandlers:function(){_runResizeHandlers()},resize:function(){if(\"function\"==typeof Event)window.dispatchEvent(new Event(\"resize\"));else{var e=window.document.createEvent(\"UIEvents\");e.initUIEvent(\"resize\",!0,!1,window,0),window.dispatchEvent(e)}},getURLParam:function(e){var t,n,i=window.location.search.substring(1).split(\"&\");for(t=0;t<i.length;t++)if((n=i[t].split(\"=\"))[0]==e)return unescape(n[1]);return null},isMobileDevice:function(){var e=this.getViewPort().width<this.getBreakpoint(\"lg\");return!1===e&&(e=null!=navigator.userAgent.match(/iPad/i)),e},isDesktopDevice:function(){return!KTUtil.isMobileDevice()},getViewPort:function(){var e=window,t=\"inner\";return\"innerWidth\"in window||(t=\"client\",e=document.documentElement||document.body),{width:e[t+\"Width\"],height:e[t+\"Height\"]}},isBreakpointUp:function(e){return this.getViewPort().width>=this.getBreakpoint(e)},isBreakpointDown:function(e){return this.getViewPort().width<this.getBreakpoint(e)},getViewportWidth:function(){return this.getViewPort().width},getUniqueId:function(e){return e+Math.floor(Math.random()*(new Date).getTime())},getBreakpoint:function(e){var t=this.getCssVariableValue(\"--bs-\"+e);return t&&(t=parseInt(t.trim())),t},isset:function(e,t){var n;if(-1!==(t=t||\"\").indexOf(\"[\"))throw new Error(\"Unsupported object path notation.\");t=t.split(\".\");do{if(void 0===e)return!1;if(n=t.shift(),!e.hasOwnProperty(n))return!1;e=e[n]}while(t.length);return!0},getHighestZindex:function(e){for(var t,n;e&&e!==document;){if((\"absolute\"===(t=KTUtil.css(e,\"position\"))||\"relative\"===t||\"fixed\"===t)&&(n=parseInt(KTUtil.css(e,\"z-index\")),!isNaN(n)&&0!==n))return n;e=e.parentNode}return 1},hasFixedPositionedParent:function(e){for(;e&&e!==document;){if(\"fixed\"===KTUtil.css(e,\"position\"))return!0;e=e.parentNode}return!1},sleep:function(e){for(var t=(new Date).getTime(),n=0;n<1e7&&!((new Date).getTime()-t>e);n++);},getRandomInt:function(e,t){return Math.floor(Math.random()*(t-e+1))+e},isAngularVersion:function(){return void 0!==window.Zone},deepExtend:function(e){e=e||{};for(var t=1;t<arguments.length;t++){var n=arguments[t];if(n)for(var i in n)n.hasOwnProperty(i)&&(\"[object Object]\"!==Object.prototype.toString.call(n[i])?e[i]=n[i]:e[i]=KTUtil.deepExtend(e[i],n[i]))}return e},extend:function(e){e=e||{};for(var t=1;t<arguments.length;t++)if(arguments[t])for(var n in arguments[t])arguments[t].hasOwnProperty(n)&&(e[n]=arguments[t][n]);return e},getBody:function(){return document.getElementsByTagName(\"body\")[0]},hasClasses:function(e,t){if(e){for(var n=t.split(\" \"),i=0;i<n.length;i++)if(0==KTUtil.hasClass(e,KTUtil.trim(n[i])))return!1;return!0}},hasClass:function(e,t){if(e)return e.classList?e.classList.contains(t):new RegExp(\"\\\\b\"+t+\"\\\\b\").test(e.className)},addClass:function(e,t){if(e&&void 0!==t){var n=t.split(\" \");if(e.classList)for(var i=0;i<n.length;i++)n[i]&&n[i].length>0&&e.classList.add(KTUtil.trim(n[i]));else if(!KTUtil.hasClass(e,t))for(var r=0;r<n.length;r++)e.className+=\" \"+KTUtil.trim(n[r])}},removeClass:function(e,t){if(e&&void 0!==t){var n=t.split(\" \");if(e.classList)for(var i=0;i<n.length;i++)e.classList.remove(KTUtil.trim(n[i]));else if(KTUtil.hasClass(e,t))for(var r=0;r<n.length;r++)e.className=e.className.replace(new RegExp(\"\\\\b\"+KTUtil.trim(n[r])+\"\\\\b\",\"g\"),\"\")}},triggerCustomEvent:function(e,t,n){var i;window.CustomEvent?i=new CustomEvent(t,{detail:n}):(i=document.createEvent(\"CustomEvent\")).initCustomEvent(t,!0,!0,n),e.dispatchEvent(i)},triggerEvent:function(e,t){var n;if(e.ownerDocument)n=e.ownerDocument;else{if(9!=e.nodeType)throw new Error(\"Invalid node passed to fireEvent: \"+e.id);n=e}if(e.dispatchEvent){var i=\"\";switch(t){case\"click\":case\"mouseenter\":case\"mouseleave\":case\"mousedown\":case\"mouseup\":i=\"MouseEvents\";break;case\"focus\":case\"change\":case\"blur\":case\"select\":i=\"HTMLEvents\";break;default:throw\"fireEvent: Couldn't find an event class for event '\"+t+\"'.\"}var r=\"change\"!=t;(o=n.createEvent(i)).initEvent(t,r,!0),o.synthetic=!0,e.dispatchEvent(o,!0)}else if(e.fireEvent){var o;(o=n.createEventObject()).synthetic=!0,e.fireEvent(\"on\"+t,o)}},index:function(e){for(var t=e.parentNode.children,n=0;n<t.length;n++)if(t[n]==e)return n},trim:function(e){return e.trim()},eventTriggered:function(e){return!!e.currentTarget.dataset.triggered||(e.currentTarget.dataset.triggered=!0,!1)},remove:function(e){e&&e.parentNode&&e.parentNode.removeChild(e)},find:function(e,t){return null!==e?e.querySelector(t):null},findAll:function(e,t){return null!==e?e.querySelectorAll(t):null},insertAfter:function(e,t){return t.parentNode.insertBefore(e,t.nextSibling)},parents:function(e,t){for(var n=[];e&&e!==document;e=e.parentNode)t?e.matches(t)&&n.push(e):n.push(e);return n},children:function(e,t,n){if(!e||!e.childNodes)return null;for(var i=[],r=0,o=e.childNodes.length;r<o;++r)1==e.childNodes[r].nodeType&&KTUtil.matches(e.childNodes[r],t,n)&&i.push(e.childNodes[r]);return i},child:function(e,t,n){var i=KTUtil.children(e,t,n);return i?i[0]:null},matches:function(e,t,n){var i=Element.prototype,r=i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(e){return-1!==[].indexOf.call(document.querySelectorAll(e),this)};return!(!e||!e.tagName)&&r.call(e,t)},data:function(e){return{set:function(t,n){e&&(void 0===e.customDataTag&&(window.KTUtilElementDataStoreID++,e.customDataTag=window.KTUtilElementDataStoreID),void 0===window.KTUtilElementDataStore[e.customDataTag]&&(window.KTUtilElementDataStore[e.customDataTag]={}),window.KTUtilElementDataStore[e.customDataTag][t]=n)},get:function(t){if(e)return void 0===e.customDataTag?null:this.has(t)?window.KTUtilElementDataStore[e.customDataTag][t]:null},has:function(t){return!!e&&(void 0!==e.customDataTag&&!(!window.KTUtilElementDataStore[e.customDataTag]||!window.KTUtilElementDataStore[e.customDataTag][t]))},remove:function(t){e&&this.has(t)&&delete window.KTUtilElementDataStore[e.customDataTag][t]}}},outerWidth:function(e,t){var n;return!0===t?(n=parseFloat(e.offsetWidth),n+=parseFloat(KTUtil.css(e,\"margin-left\"))+parseFloat(KTUtil.css(e,\"margin-right\")),parseFloat(n)):n=parseFloat(e.offsetWidth)},offset:function(e){var t,n;if(e)return e.getClientRects().length?(t=e.getBoundingClientRect(),n=e.ownerDocument.defaultView,{top:t.top+n.pageYOffset,left:t.left+n.pageXOffset,right:window.innerWidth-(e.offsetLeft+e.offsetWidth)}):{top:0,left:0}},height:function(e){return KTUtil.css(e,\"height\")},outerHeight:function(e,t){var n,i=e.offsetHeight;return void 0!==t&&!0===t?(n=getComputedStyle(e),i+=parseInt(n.marginTop)+parseInt(n.marginBottom)):i},visible:function(e){return!(0===e.offsetWidth&&0===e.offsetHeight)},isVisibleInContainer:function(e,t,n=0){const i=e.offsetTop,r=i+e.clientHeight+n,o=t.scrollTop,a=o+t.clientHeight;return i>=o&&r<=a},getRelativeTopPosition:function(e,t){return e.offsetTop-t.offsetTop},attr:function(e,t,n){if(null!=e)return void 0===n?e.getAttribute(t):void e.setAttribute(t,n)},hasAttr:function(e,t){if(null!=e)return!!e.getAttribute(t)},removeAttr:function(e,t){null!=e&&e.removeAttribute(t)},animate:function(e,t,n,i,r,o){var a={};if(a.linear=function(e,t,n,i){return n*e/i+t},r=a.linear,\"number\"==typeof e&&\"number\"==typeof t&&\"number\"==typeof n&&\"function\"==typeof i){\"function\"!=typeof o&&(o=function(){});var l=window.requestAnimationFrame||function(e){window.setTimeout(e,20)},s=t-e;i(e);var u=window.performance&&window.performance.now?window.performance.now():+new Date;l(function a(d){var c=(d||+new Date)-u;c>=0&&i(r(c,e,s,n)),c>=0&&c>=n?(i(t),o()):l(a)})}},actualCss:function(e,t,n){var i,r=\"\";if(e instanceof HTMLElement!=!1)return e.getAttribute(\"kt-hidden-\"+t)&&!1!==n?parseFloat(e.getAttribute(\"kt-hidden-\"+t)):(r=e.style.cssText,e.style.cssText=\"position: absolute; visibility: hidden; display: block;\",\"width\"==t?i=e.offsetWidth:\"height\"==t&&(i=e.offsetHeight),e.style.cssText=r,e.setAttribute(\"kt-hidden-\"+t,i),parseFloat(i))},actualHeight:function(e,t){return KTUtil.actualCss(e,\"height\",t)},actualWidth:function(e,t){return KTUtil.actualCss(e,\"width\",t)},getScroll:function(e,t){return t=\"scroll\"+t,e==window||e==document?self[\"scrollTop\"==t?\"pageYOffset\":\"pageXOffset\"]||browserSupportsBoxModel&&document.documentElement[t]||document.body[t]:e[t]},css:function(e,t,n,i){if(e)if(void 0!==n)!0===i?e.style.setProperty(t,n,\"important\"):e.style[t]=n;else{var r=(e.ownerDocument||document).defaultView;if(r&&r.getComputedStyle)return t=t.replace(/([A-Z])/g,\"-$1\").toLowerCase(),r.getComputedStyle(e,null).getPropertyValue(t);if(e.currentStyle)return t=t.replace(/\\-(\\w)/g,function(e,t){return t.toUpperCase()}),n=e.currentStyle[t],/^\\d+(em|pt|%|ex)?$/i.test(n)?function(t){var n=e.style.left,i=e.runtimeStyle.left;return e.runtimeStyle.left=e.currentStyle.left,e.style.left=t||0,t=e.style.pixelLeft+\"px\",e.style.left=n,e.runtimeStyle.left=i,t}(n):n}},slide:function(e,t,n,i,r){if(!(!e||\"up\"==t&&!1===KTUtil.visible(e)||\"down\"==t&&!0===KTUtil.visible(e))){n=n||600;var o=KTUtil.actualHeight(e),a=!1,l=!1;KTUtil.css(e,\"padding-top\")&&!0!==KTUtil.data(e).has(\"slide-padding-top\")&&KTUtil.data(e).set(\"slide-padding-top\",KTUtil.css(e,\"padding-top\")),KTUtil.css(e,\"padding-bottom\")&&!0!==KTUtil.data(e).has(\"slide-padding-bottom\")&&KTUtil.data(e).set(\"slide-padding-bottom\",KTUtil.css(e,\"padding-bottom\")),KTUtil.data(e).has(\"slide-padding-top\")&&(a=parseInt(KTUtil.data(e).get(\"slide-padding-top\"))),KTUtil.data(e).has(\"slide-padding-bottom\")&&(l=parseInt(KTUtil.data(e).get(\"slide-padding-bottom\"))),\"up\"==t?(e.style.cssText=\"display: block; overflow: hidden;\",a&&KTUtil.animate(0,a,n,function(t){e.style.paddingTop=a-t+\"px\"},\"linear\"),l&&KTUtil.animate(0,l,n,function(t){e.style.paddingBottom=l-t+\"px\"},\"linear\"),KTUtil.animate(0,o,n,function(t){e.style.height=o-t+\"px\"},\"linear\",function(){e.style.height=\"\",e.style.display=\"none\",\"function\"==typeof i&&i()})):\"down\"==t&&(e.style.cssText=\"display: block; overflow: hidden;\",a&&KTUtil.animate(0,a,n,function(t){e.style.paddingTop=t+\"px\"},\"linear\",function(){e.style.paddingTop=\"\"}),l&&KTUtil.animate(0,l,n,function(t){e.style.paddingBottom=t+\"px\"},\"linear\",function(){e.style.paddingBottom=\"\"}),KTUtil.animate(0,o,n,function(t){e.style.height=t+\"px\"},\"linear\",function(){e.style.height=\"\",e.style.display=\"\",e.style.overflow=\"\",\"function\"==typeof i&&i()}))}},slideUp:function(e,t,n){KTUtil.slide(e,\"up\",t,n)},slideDown:function(e,t,n){KTUtil.slide(e,\"down\",t,n)},show:function(e,t){void 0!==e&&(e.style.display=t||\"block\")},hide:function(e){void 0!==e&&(e.style.display=\"none\")},addEvent:function(e,t,n,i){null!=e&&e.addEventListener(t,n)},removeEvent:function(e,t,n){null!==e&&e.removeEventListener(t,n)},on:function(e,t,n,i){if(null!==e){var r=KTUtil.getUniqueId(\"event\");return window.KTUtilDelegatedEventHandlers[r]=function(n){for(var r=e.querySelectorAll(t),o=n.target;o&&o!==e;){for(var a=0,l=r.length;a<l;a++)o===r[a]&&i.call(o,n);o=o.parentNode}},KTUtil.addEvent(e,n,window.KTUtilDelegatedEventHandlers[r]),r}},off:function(e,t,n){e&&window.KTUtilDelegatedEventHandlers[n]&&(KTUtil.removeEvent(e,t,window.KTUtilDelegatedEventHandlers[n]),delete window.KTUtilDelegatedEventHandlers[n])},one:function(e,t,n){e.addEventListener(t,function t(i){return i.target&&i.target.removeEventListener&&i.target.removeEventListener(i.type,t),e&&e.removeEventListener&&i.currentTarget.removeEventListener(i.type,t),n(i)})},hash:function(e){var t,n=0;if(0===e.length)return n;for(t=0;t<e.length;t++)n=(n<<5)-n+e.charCodeAt(t),n|=0;return n},animateClass:function(e,t,n){var i,r={animation:\"animationend\",OAnimation:\"oAnimationEnd\",MozAnimation:\"mozAnimationEnd\",WebkitAnimation:\"webkitAnimationEnd\",msAnimation:\"msAnimationEnd\"};for(var o in r)void 0!==e.style[o]&&(i=r[o]);KTUtil.addClass(e,t),KTUtil.one(e,i,function(){KTUtil.removeClass(e,t)}),n&&KTUtil.one(e,i,n)},transitionEnd:function(e,t){var n,i={transition:\"transitionend\",OTransition:\"oTransitionEnd\",MozTransition:\"mozTransitionEnd\",WebkitTransition:\"webkitTransitionEnd\",msTransition:\"msTransitionEnd\"};for(var r in i)void 0!==e.style[r]&&(n=i[r]);KTUtil.one(e,n,t)},animationEnd:function(e,t){var n,i={animation:\"animationend\",OAnimation:\"oAnimationEnd\",MozAnimation:\"mozAnimationEnd\",WebkitAnimation:\"webkitAnimationEnd\",msAnimation:\"msAnimationEnd\"};for(var r in i)void 0!==e.style[r]&&(n=i[r]);KTUtil.one(e,n,t)},animateDelay:function(e,t){for(var n=[\"webkit-\",\"moz-\",\"ms-\",\"o-\",\"\"],i=0;i<n.length;i++)KTUtil.css(e,n[i]+\"animation-delay\",t)},animateDuration:function(e,t){for(var n=[\"webkit-\",\"moz-\",\"ms-\",\"o-\",\"\"],i=0;i<n.length;i++)KTUtil.css(e,n[i]+\"animation-duration\",t)},scrollTo:function(e,t,n){n=n||500;var i,r,o=e?KTUtil.offset(e).top:0;t&&(o-=t),i=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,r=o,KTUtil.animate(i,r,n,function(e){document.documentElement.scrollTop=e,document.body.parentNode.scrollTop=e,document.body.scrollTop=e})},scrollTop:function(e,t){KTUtil.scrollTo(null,e,t)},isArray:function(e){return e&&Array.isArray(e)},isEmpty:function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},numberString:function(e){for(var t=(e+=\"\").split(\".\"),n=t[0],i=t.length>1?\".\"+t[1]:\"\",r=/(\\d+)(\\d{3})/;r.test(n);)n=n.replace(r,\"$1,$2\");return n+i},isRTL:function(){return\"rtl\"===document.querySelector(\"html\").getAttribute(\"direction\")},snakeToCamel:function(e){return e.replace(/(\\-\\w)/g,function(e){return e[1].toUpperCase()})},filterBoolean:function(e){return!0===e||\"true\"===e||!1!==e&&\"false\"!==e&&e},setHTML:function(e,t){e.innerHTML=t},getHTML:function(e){if(e)return e.innerHTML},getDocumentHeight:function(){var e=document.body,t=document.documentElement;return Math.max(e.scrollHeight,e.offsetHeight,t.clientHeight,t.scrollHeight,t.offsetHeight)},getScrollTop:function(){return(document.scrollingElement||document.documentElement).scrollTop},colorLighten:function(e,t){const n=function(e,t){let n=parseInt(e,16)+t,i=n>255?255:n;return i=i.toString(16).length>1?i.toString(16):`0${i.toString(16)}`,i};return e=e.indexOf(\"#\")>=0?e.substring(1,e.length):e,t=parseInt(255*t/100),`#${n(e.substring(0,2),t)}${n(e.substring(2,4),t)}${n(e.substring(4,6),t)}`},colorDarken:function(e,t){const n=function(e,t){let n=parseInt(e,16)-t,i=n<0?0:n;return i=i.toString(16).length>1?i.toString(16):`0${i.toString(16)}`,i};return e=e.indexOf(\"#\")>=0?e.substring(1,e.length):e,t=parseInt(255*t/100),`#${n(e.substring(0,2),t)}${n(e.substring(2,4),t)}${n(e.substring(4,6),t)}`},throttle:function(e,t,n){e||(e=setTimeout(function(){t(),e=void 0},n))},debounce:function(e,t,n){clearTimeout(e),e=setTimeout(t,n)},parseJson:function(e){if(\"string\"==typeof e){var t=(e=e.replace(/'/g,'\"')).replace(/(\\w+:)|(\\w+ :)/g,function(e){return'\"'+e.substring(0,e.length-1)+'\":'});try{e=JSON.parse(t)}catch(e){}}return e},getResponsiveValue:function(e,t){var n=this.getViewPort().width,i=null;if(\"object\"==typeof(e=KTUtil.parseJson(e))){var r,o,a=-1;for(var l in e)(o=\"default\"===l?0:this.getBreakpoint(l)?this.getBreakpoint(l):parseInt(l))<=n&&o>a&&(r=l,a=o);i=r?e[r]:e}else i=e;return i},each:function(e,t){return[].slice.call(e).map(t)},getSelectorMatchValue:function(e){var t=null;if(\"object\"==typeof(e=KTUtil.parseJson(e))){if(void 0!==e.match){var n=Object.keys(e.match)[0];e=Object.values(e.match)[0],null!==document.querySelector(n)&&(t=e)}}else t=e;return t},getConditionalValue:function(e){e=KTUtil.parseJson(e);var t=KTUtil.getResponsiveValue(e);return null!==t&&void 0!==t.match&&(t=KTUtil.getSelectorMatchValue(t)),null===t&&null!==e&&void 0!==e.default&&(t=e.default),t},getCssVariableValue:function(e){var t=getComputedStyle(document.documentElement).getPropertyValue(e);return t&&t.length>0&&(t=t.trim()),t},isInViewport:function(e){var t=e.getBoundingClientRect();return t.top>=0&&t.left>=0&&t.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&t.right<=(window.innerWidth||document.documentElement.clientWidth)},isPartiallyInViewport:function(e){let t=e.getBoundingClientRect().left,n=e.getBoundingClientRect().top,i=Math.max(document.documentElement.clientWidth,window.innerWidth||0),r=Math.max(document.documentElement.clientHeight,window.innerHeight||0),o=e.clientWidth,a=e.clientHeight;return n<r&&n+a>0&&t<i&&t+o>0},onDOMContentLoaded:function(e){\"loading\"===document.readyState?(document.addEventListener(\"DOMContentLoaded\",e),document.addEventListener(\"livewire:navigated\",e)):e()},inIframe:function(){try{return window.self!==window.top}catch(e){return!0}},isHexColor:e=>/^#[0-9A-F]{6}$/i.test(e)}}();\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTUtil);var KTAppLayoutBuilder=function(){var e,t,n,i,r,o,a,l,s,u;return{init:function(){var d,c,m;(a=document.querySelector(\"#kt_app_engage\"),s=document.querySelector(\"#kt_app_engage_toggle_on\"),l=document.querySelector(\"#kt_app_engage_toggle_off\"),u=document.querySelector(\"#kt_app_engage_prebuilts_modal\"),a&&u&&(null!==u&&\"1\"!==KTCookie.get(\"app_engage_prebuilts_modal_displayed\")&&setTimeout(function(){new bootstrap.Modal(u).show();const e=new Date(Date.now()+2592e6);KTCookie.set(\"app_engage_prebuilts_modal_displayed\",\"1\",{expires:e})},3e3),function(){u.querySelector('[data-kt-element=\"selected\"]');const e=u.querySelector('[data-kt-element=\"title\"]'),t=u.querySelector('[data-kt-menu=\"true\"]');KTUtil.on(u,\"[data-kt-mode]\",\"click\",function(n){const i=this.innerText,r=this.getAttribute(\"data-kt-mode\"),o=t.querySelector(\".menu-link.active\"),a=document.querySelector(\"#kt_app_engage_prebuilts_view_image\"),l=document.querySelector(\"#kt_app_engage_prebuilts_view_text\");e.innerText=i,o&&o.classList.remove(\"active\"),this.classList.add(\"active\"),\"image\"===r?(a.classList.remove(\"d-none\"),a.classList.add(\"d-block\"),l.classList.remove(\"d-block\"),l.classList.add(\"d-none\")):(l.classList.remove(\"d-none\"),l.classList.add(\"d-block\"),a.classList.remove(\"d-block\"),a.classList.add(\"d-none\"))})}()),a&&s&&l&&(l.addEventListener(\"click\",function(e){e.preventDefault();const t=new Date(Date.now()+864e5);KTCookie.set(\"app_engage_hide\",\"1\",{expires:t}),a.classList.add(\"app-engage-hide\")}),s.addEventListener(\"click\",function(e){e.preventDefault(),KTCookie.remove(\"app_engage_hide\"),a.classList.remove(\"app-engage-hide\")})),e=document.querySelector(\"#kt_app_layout_builder_form\"))&&(n=e.getAttribute(\"action\"),t=document.querySelector(\"#kt_app_layout_builder_action\"),i=document.querySelector(\"#kt_app_layout_builder_preview\"),r=document.querySelector(\"#kt_app_layout_builder_export\"),o=document.querySelector(\"#kt_app_layout_builder_reset\"),i&&i.addEventListener(\"click\",function(r){r.preventDefault(),t.value=\"preview\",i.setAttribute(\"data-kt-indicator\",\"on\");var o=$(e).serialize();$.ajax({type:\"POST\",dataType:\"html\",url:n,data:o,success:function(e,t,n){history.scrollRestoration&&(history.scrollRestoration=\"manual\"),location.reload()},error:function(e){toastr.error(\"Please try it again later.\",\"Something went wrong!\",{timeOut:0,extendedTimeOut:0,closeButton:!0,closeDuration:0})},complete:function(){i.removeAttribute(\"data-kt-indicator\")}})}),r&&r.addEventListener(\"click\",function(i){i.preventDefault(),toastr.success(\"Process has been started and it may take a while.\",\"Generating HTML!\",{timeOut:0,extendedTimeOut:0,closeButton:!0,closeDuration:0}),r.setAttribute(\"data-kt-indicator\",\"on\"),t.value=\"export\";var o=$(e).serialize();$.ajax({type:\"POST\",dataType:\"html\",url:n,data:o,success:function(e,t,i){var o=setInterval(function(){$(\"<iframe/>\").attr({src:n+\"?layout-builder[action]=export&download=1&output=\"+e,style:\"visibility:hidden;display:none\"}).ready(function(){clearInterval(o),r.removeAttribute(\"data-kt-indicator\")}).appendTo(\"body\")},3e3)},error:function(e){toastr.error(\"Please try it again later.\",\"Something went wrong!\",{timeOut:0,extendedTimeOut:0,closeButton:!0,closeDuration:0}),r.removeAttribute(\"data-kt-indicator\")}})}),o&&o.addEventListener(\"click\",function(i){i.preventDefault(),o.setAttribute(\"data-kt-indicator\",\"on\"),t.value=\"reset\";var r=$(e).serialize();$.ajax({type:\"POST\",dataType:\"html\",url:n,data:r,success:function(e,t,n){history.scrollRestoration&&(history.scrollRestoration=\"manual\"),location.reload()},error:function(e){toastr.error(\"Please try it again later.\",\"Something went wrong!\",{timeOut:0,extendedTimeOut:0,closeButton:!0,closeDuration:0})},complete:function(){o.removeAttribute(\"data-kt-indicator\")}})}),d=document.querySelector(\"#kt_layout_builder_theme_mode_light\"),c=document.querySelector(\"#kt_layout_builder_theme_mode_dark\"),m=document.querySelector(\"#kt_layout_builder_theme_mode_\"+KTThemeMode.getMode()),d&&d.addEventListener(\"click\",function(){this.checked=!0,this.closest('[data-kt-buttons=\"true\"]').querySelector(\".form-check-image.active\").classList.remove(\"active\"),this.closest(\".form-check-image\").classList.add(\"active\"),KTThemeMode.setMode(\"light\")}),c&&c.addEventListener(\"click\",function(){this.checked=!0,this.closest('[data-kt-buttons=\"true\"]').querySelector(\".form-check-image.active\").classList.remove(\"active\"),this.closest(\".form-check-image\").classList.add(\"active\"),KTThemeMode.setMode(\"dark\")}),m&&(m.closest(\".form-check-image\").classList.add(\"active\"),m.checked=!0))}}}();KTUtil.onDOMContentLoaded(function(){KTAppLayoutBuilder.init()});var KTLayoutSearch=function(){var e,t,n,i,r,o,a,l,s,u,d,c,m=function(e){setTimeout(function(){var i=KTUtil.getRandomInt(1,3);t.classList.add(\"d-none\"),3===i?(n.classList.add(\"d-none\"),r.classList.remove(\"d-none\")):(n.classList.remove(\"d-none\"),r.classList.add(\"d-none\")),e.complete()},1500)},f=function(e){t.classList.remove(\"d-none\"),n.classList.add(\"d-none\"),r.classList.add(\"d-none\")};return{init:function(){(e=document.querySelector(\"#kt_header_search\"))&&(i=e.querySelector('[data-kt-search-element=\"wrapper\"]'),e.querySelector('[data-kt-search-element=\"form\"]'),t=e.querySelector('[data-kt-search-element=\"main\"]'),n=e.querySelector('[data-kt-search-element=\"results\"]'),r=e.querySelector('[data-kt-search-element=\"empty\"]'),o=e.querySelector('[data-kt-search-element=\"preferences\"]'),a=e.querySelector('[data-kt-search-element=\"preferences-show\"]'),l=e.querySelector('[data-kt-search-element=\"preferences-dismiss\"]'),s=e.querySelector('[data-kt-search-element=\"advanced-options-form\"]'),u=e.querySelector('[data-kt-search-element=\"advanced-options-form-show\"]'),d=e.querySelector('[data-kt-search-element=\"advanced-options-form-cancel\"]'),e.querySelector('[data-kt-search-element=\"advanced-options-form-search\"]'),(c=new KTSearch(e)).on(\"kt.search.process\",m),c.on(\"kt.search.clear\",f),o&&(a&&a.addEventListener(\"click\",function(){i.classList.add(\"d-none\"),o.classList.remove(\"d-none\")}),l&&l.addEventListener(\"click\",function(){i.classList.remove(\"d-none\"),o.classList.add(\"d-none\")})),s&&(u&&u.addEventListener(\"click\",function(){i.classList.add(\"d-none\"),s.classList.remove(\"d-none\")}),d&&d.addEventListener(\"click\",function(){i.classList.remove(\"d-none\"),s.classList.add(\"d-none\")})))}}}();KTUtil.onDOMContentLoaded(function(){KTLayoutSearch.init()});var KTAppSidebar=function(){var e,t,n;return{init:function(){var i;e=document.querySelector(\"#kt_app_sidebar_navs_wrappers\"),t=document.querySelector(\"#kt_app_sidebar_menu_projects_collapse\"),n=document.querySelector(\"#kt_app_sidebar_header\"),e&&(i=e.querySelector(\".menu-link.active\"))&&!0!==KTUtil.isVisibleInContainer(i,e)&&e.scroll({top:KTUtil.getRelativeTopPosition(i,e),behavior:\"smooth\"}),t&&t.addEventListener(\"shown.bs.collapse\",t=>{const n=parseInt(KTUtil.css(e,\"height\"));e.scroll({top:n,behavior:\"smooth\"})}),n&&function(){const e=n.querySelector('[data-kt-element=\"selected\"]'),t=n.querySelector('[data-kt-menu=\"true\"]');KTUtil.on(n,'[data-kt-element=\"project\"]',\"click\",function(n){const i=this.querySelector('[data-kt-element=\"logo\"]'),r=this.querySelector('[data-kt-element=\"title\"]'),o=this.querySelector('[data-kt-element=\"desc\"]'),a=t.querySelector(\".menu-link.active\");e.querySelector('[data-kt-element=\"logo\"]').setAttribute(\"src\",i.getAttribute(\"src\")),e.querySelector('[data-kt-element=\"title\"]').innerText=r.innerText,e.querySelector('[data-kt-element=\"desc\"]').innerText=o.innerText,a&&a.classList.remove(\"active\"),this.classList.add(\"active\")})}()}}}();KTUtil.onDOMContentLoaded(function(){KTAppSidebar.init()});var KTThemeModeUser={init:function(){KTThemeMode.on(\"kt.thememode.change\",function(){var e=KTThemeMode.getMenuMode(),t=KTThemeMode.getMode();console.log(\"user selected theme mode:\"+e),console.log(\"theme mode:\"+t)})}};KTUtil.onDOMContentLoaded(function(){KTThemeModeUser.init()}),\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTThemeModeUser);var KTThemeMode=function(){var e,t=this,n=function(){return document.documentElement.hasAttribute(\"data-bs-theme\")?document.documentElement.getAttribute(\"data-bs-theme\"):null!==localStorage.getItem(\"data-bs-theme\")?localStorage.getItem(\"data-bs-theme\"):\"system\"===r()?o():\"light\"},i=function(i,r){var l=n();\"system\"===r?o()!==i&&(i=o()):i!==r&&(r=i);var s=e?e.querySelector('[data-kt-element=\"mode\"][data-kt-value=\"'+r+'\"]'):null;document.documentElement.setAttribute(\"data-kt-theme-mode-switching\",\"true\"),document.documentElement.setAttribute(\"data-bs-theme\",i),setTimeout(function(){document.documentElement.removeAttribute(\"data-kt-theme-mode-switching\")},300),localStorage.setItem(\"data-bs-theme\",i),s&&(localStorage.setItem(\"data-bs-theme-mode\",r),a(s)),i!==l&&KTEventHandler.trigger(document.documentElement,\"kt.thememode.change\",t)},r=function(){if(!e)return null;var t=e?e.querySelector('.active[data-kt-element=\"mode\"]'):null;return t&&t.getAttribute(\"data-kt-value\")?t.getAttribute(\"data-kt-value\"):document.documentElement.hasAttribute(\"data-bs-theme-mode\")?document.documentElement.getAttribute(\"data-bs-theme-mode\"):null!==localStorage.getItem(\"data-bs-theme-mode\")?localStorage.getItem(\"data-bs-theme-mode\"):\"undefined\"!=typeof defaultThemeMode?defaultThemeMode:\"light\"},o=function(){return window.matchMedia(\"(prefers-color-scheme: dark)\").matches?\"dark\":\"light\"},a=function(t){var n=t.getAttribute(\"data-kt-value\"),i=e.querySelector('.active[data-kt-element=\"mode\"]');i&&i.classList.remove(\"active\"),t.classList.add(\"active\"),localStorage.setItem(\"data-bs-theme-mode\",n)};return{init:function(){e=document.querySelector('[data-kt-element=\"theme-mode-menu\"]'),i(n(),r()),KTEventHandler.trigger(document.documentElement,\"kt.thememode.init\",t),e&&[].slice.call(e.querySelectorAll('[data-kt-element=\"mode\"]')).map(function(e){e.addEventListener(\"click\",function(t){t.preventDefault();var n=e.getAttribute(\"data-kt-value\"),r=n;\"system\"===n&&(r=o()),i(r,n)})})},getMode:function(){return n()},getMenuMode:function(){return r()},getSystemMode:function(){return o()},setMode:function(e){return i(e)},on:function(e,t){return KTEventHandler.on(document.documentElement,e,t)},off:function(e,t){return KTEventHandler.off(document.documentElement,e,t)}}}();KTUtil.onDOMContentLoaded(function(){KTThemeMode.init()}),\"undefined\"!=typeof module&&void 0!==module.exports&&(module.exports=KTThemeMode);"
  },
  {
    "path": "static/assets/plugins/custom/datatables/datatables.bundle.css",
    "content": "@charset \"UTF-8\";:root{--dt-row-selected:13,110,253;--dt-row-selected-text:255,255,255;--dt-row-selected-link:228,228,228;--dt-row-stripe:0,0,0;--dt-row-hover:0,0,0;--dt-column-ordering:0,0,0;--dt-header-align-items:center;--dt-header-vertical-align:middle;--dt-html-background:white}:root.dark{--dt-html-background:rgb(33, 37, 41)}table.dataTable tbody td.dt-control{text-align:center;cursor:pointer}table.dataTable tbody td.dt-control:before{display:inline-block;box-sizing:border-box;content:\"\";border-top:5px solid transparent;border-left:10px solid rgba(0,0,0,.5);border-bottom:5px solid transparent;border-right:0 solid transparent}table.dataTable tbody tr.dt-hasChild td.dt-control:before{border-top:10px solid rgba(0,0,0,.5);border-left:5px solid transparent;border-bottom:0 solid transparent;border-right:5px solid transparent}table.dataTable tfoot:empty{display:none}:root[data-bs-theme=dark] table.dataTable td.dt-control:before,:root[data-theme=dark] table.dataTable td.dt-control:before,html.dark table.dataTable td.dt-control:before{border-left-color:rgba(255,255,255,.5)}:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before,html.dark table.dataTable tr.dt-hasChild td.dt-control:before{border-top-color:rgba(255,255,255,.5);border-left-color:transparent}div.dt-scroll{width:100%}div.dt-scroll-body tfoot tr,div.dt-scroll-body thead tr{height:0}div.dt-scroll-body tfoot tr td,div.dt-scroll-body tfoot tr th,div.dt-scroll-body thead tr td,div.dt-scroll-body thead tr th{height:0!important;padding-top:0!important;padding-bottom:0!important;border-top-width:0!important;border-bottom-width:0!important}div.dt-scroll-body tfoot tr td div.dt-scroll-sizing,div.dt-scroll-body tfoot tr th div.dt-scroll-sizing,div.dt-scroll-body thead tr td div.dt-scroll-sizing,div.dt-scroll-body thead tr th div.dt-scroll-sizing{height:0!important;overflow:hidden!important}table.dataTable thead>tr>td:active,table.dataTable thead>tr>th:active{outline:0}table.dataTable thead>tr>td.dt-orderable-asc .dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc .dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-asc .dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc .dt-column-order:before{position:absolute;display:block;bottom:50%;content:\"▲\";content:\"▲\"/\"\"}table.dataTable thead>tr>td.dt-orderable-desc .dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc .dt-column-order:after,table.dataTable thead>tr>th.dt-orderable-desc .dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc .dt-column-order:after{position:absolute;display:block;top:50%;content:\"▼\";content:\"▼\"/\"\"}table.dataTable thead>tr>td.dt-orderable-asc .dt-column-order,table.dataTable thead>tr>td.dt-orderable-desc .dt-column-order,table.dataTable thead>tr>td.dt-ordering-asc .dt-column-order,table.dataTable thead>tr>td.dt-ordering-desc .dt-column-order,table.dataTable thead>tr>th.dt-orderable-asc .dt-column-order,table.dataTable thead>tr>th.dt-orderable-desc .dt-column-order,table.dataTable thead>tr>th.dt-ordering-asc .dt-column-order,table.dataTable thead>tr>th.dt-ordering-desc .dt-column-order{position:relative;width:12px;height:20px}table.dataTable thead>tr>td.dt-orderable-asc .dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-asc .dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-desc .dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc .dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc .dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc .dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc .dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc .dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-asc .dt-column-order:after,table.dataTable thead>tr>th.dt-orderable-asc .dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-desc .dt-column-order:after,table.dataTable thead>tr>th.dt-orderable-desc .dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc .dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-asc .dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc .dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc .dt-column-order:before{left:0;opacity:.125;line-height:9px;font-size:.8em}table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc,table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc{cursor:pointer}table.dataTable thead>tr>td.dt-orderable-asc:hover,table.dataTable thead>tr>td.dt-orderable-desc:hover,table.dataTable thead>tr>th.dt-orderable-asc:hover,table.dataTable thead>tr>th.dt-orderable-desc:hover{outline:2px solid rgba(0,0,0,.05);outline-offset:-2px}table.dataTable thead>tr>td.dt-ordering-asc .dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc .dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-asc .dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc .dt-column-order:after{opacity:.6}table.dataTable thead>tr>td.dt-orderable-none:not(.dt-ordering-asc,.dt-ordering-desc) .dt-column-order:empty,table.dataTable thead>tr>td.sorting_asc_disabled .dt-column-order:before,table.dataTable thead>tr>td.sorting_desc_disabled .dt-column-order:after,table.dataTable thead>tr>th.dt-orderable-none:not(.dt-ordering-asc,.dt-ordering-desc) .dt-column-order:empty,table.dataTable thead>tr>th.sorting_asc_disabled .dt-column-order:before,table.dataTable thead>tr>th.sorting_desc_disabled .dt-column-order:after{display:none}table.dataTable thead>tr>td:active,table.dataTable thead>tr>th:active{outline:0}table.dataTable tfoot>tr>td div.dt-column-footer,table.dataTable tfoot>tr>td div.dt-column-header,table.dataTable tfoot>tr>th div.dt-column-footer,table.dataTable tfoot>tr>th div.dt-column-header,table.dataTable thead>tr>td div.dt-column-footer,table.dataTable thead>tr>td div.dt-column-header,table.dataTable thead>tr>th div.dt-column-footer,table.dataTable thead>tr>th div.dt-column-header{display:flex;justify-content:space-between;align-items:var(--dt-header-align-items);gap:4px}table.dataTable tfoot>tr>td div.dt-column-footer .dt-column-title,table.dataTable tfoot>tr>td div.dt-column-header .dt-column-title,table.dataTable tfoot>tr>th div.dt-column-footer .dt-column-title,table.dataTable tfoot>tr>th div.dt-column-header .dt-column-title,table.dataTable thead>tr>td div.dt-column-footer .dt-column-title,table.dataTable thead>tr>td div.dt-column-header .dt-column-title,table.dataTable thead>tr>th div.dt-column-footer .dt-column-title,table.dataTable thead>tr>th div.dt-column-header .dt-column-title{flex-grow:1}table.dataTable tfoot>tr>td div.dt-column-footer .dt-column-title:empty,table.dataTable tfoot>tr>td div.dt-column-header .dt-column-title:empty,table.dataTable tfoot>tr>th div.dt-column-footer .dt-column-title:empty,table.dataTable tfoot>tr>th div.dt-column-header .dt-column-title:empty,table.dataTable thead>tr>td div.dt-column-footer .dt-column-title:empty,table.dataTable thead>tr>td div.dt-column-header .dt-column-title:empty,table.dataTable thead>tr>th div.dt-column-footer .dt-column-title:empty,table.dataTable thead>tr>th div.dt-column-header .dt-column-title:empty{display:none}div.dt-scroll-body>table.dataTable>thead>tr>td,div.dt-scroll-body>table.dataTable>thead>tr>th{overflow:hidden}:root.dark table.dataTable thead>tr>td.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-desc:hover,:root.dark table.dataTable thead>tr>th.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>th.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-desc:hover{outline:2px solid rgba(255,255,255,.05)}div.dt-processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-22px;text-align:center;padding:2px;z-index:10}div.dt-processing>div:last-child{position:relative;width:80px;height:15px;margin:1em auto}div.dt-processing>div:last-child>div{position:absolute;top:0;width:13px;height:13px;border-radius:50%;background:#0d6efd;background:rgb(var(--dt-row-selected));animation-timing-function:cubic-bezier(0,1,1,0)}div.dt-processing>div:last-child>div:nth-child(1){left:8px;animation:datatables-loader-1 .6s infinite}div.dt-processing>div:last-child>div:nth-child(2){left:8px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(3){left:32px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(4){left:56px;animation:datatables-loader-3 .6s infinite}@keyframes datatables-loader-1{0%{transform:scale(0)}100%{transform:scale(1)}}@keyframes datatables-loader-3{0%{transform:scale(1)}100%{transform:scale(0)}}@keyframes datatables-loader-2{0%{transform:translate(0,0)}100%{transform:translate(24px,0)}}table.dataTable.nowrap td,table.dataTable.nowrap th{white-space:nowrap}table.dataTable td,table.dataTable th{box-sizing:border-box}table.dataTable td.dt-type-date,table.dataTable td.dt-type-numeric,table.dataTable th.dt-type-date,table.dataTable th.dt-type-numeric{text-align:right}table.dataTable td.dt-type-date div.dt-column-footer,table.dataTable td.dt-type-date div.dt-column-header,table.dataTable td.dt-type-numeric div.dt-column-footer,table.dataTable td.dt-type-numeric div.dt-column-header,table.dataTable th.dt-type-date div.dt-column-footer,table.dataTable th.dt-type-date div.dt-column-header,table.dataTable th.dt-type-numeric div.dt-column-footer,table.dataTable th.dt-type-numeric div.dt-column-header{flex-direction:row-reverse}table.dataTable td.dt-left,table.dataTable th.dt-left{text-align:left}table.dataTable td.dt-left div.dt-column-footer,table.dataTable td.dt-left div.dt-column-header,table.dataTable th.dt-left div.dt-column-footer,table.dataTable th.dt-left div.dt-column-header{flex-direction:row}table.dataTable td.dt-center,table.dataTable th.dt-center{text-align:center}table.dataTable td.dt-right,table.dataTable th.dt-right{text-align:right}table.dataTable td.dt-right div.dt-column-footer,table.dataTable td.dt-right div.dt-column-header,table.dataTable th.dt-right div.dt-column-footer,table.dataTable th.dt-right div.dt-column-header{flex-direction:row-reverse}table.dataTable td.dt-justify,table.dataTable th.dt-justify{text-align:justify}table.dataTable td.dt-justify div.dt-column-footer,table.dataTable td.dt-justify div.dt-column-header,table.dataTable th.dt-justify div.dt-column-footer,table.dataTable th.dt-justify div.dt-column-header{flex-direction:row}table.dataTable td.dt-nowrap,table.dataTable th.dt-nowrap{white-space:nowrap}table.dataTable td.dt-empty,table.dataTable th.dt-empty{text-align:center;vertical-align:top}table.dataTable tfoot td,table.dataTable tfoot th,table.dataTable thead td,table.dataTable thead th{text-align:left;vertical-align:var(--dt-header-vertical-align)}table.dataTable tfoot td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable thead th.dt-head-left{text-align:left}table.dataTable tfoot td.dt-head-left div.dt-column-footer,table.dataTable tfoot td.dt-head-left div.dt-column-header,table.dataTable tfoot th.dt-head-left div.dt-column-footer,table.dataTable tfoot th.dt-head-left div.dt-column-header,table.dataTable thead td.dt-head-left div.dt-column-footer,table.dataTable thead td.dt-head-left div.dt-column-header,table.dataTable thead th.dt-head-left div.dt-column-footer,table.dataTable thead th.dt-head-left div.dt-column-header{flex-direction:row}table.dataTable tfoot td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable thead th.dt-head-center{text-align:center}table.dataTable tfoot td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable thead th.dt-head-right{text-align:right}table.dataTable tfoot td.dt-head-right div.dt-column-footer,table.dataTable tfoot td.dt-head-right div.dt-column-header,table.dataTable tfoot th.dt-head-right div.dt-column-footer,table.dataTable tfoot th.dt-head-right div.dt-column-header,table.dataTable thead td.dt-head-right div.dt-column-footer,table.dataTable thead td.dt-head-right div.dt-column-header,table.dataTable thead th.dt-head-right div.dt-column-footer,table.dataTable thead th.dt-head-right div.dt-column-header{flex-direction:row-reverse}table.dataTable tfoot td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable thead th.dt-head-justify{text-align:justify}table.dataTable tfoot td.dt-head-justify div.dt-column-footer,table.dataTable tfoot td.dt-head-justify div.dt-column-header,table.dataTable tfoot th.dt-head-justify div.dt-column-footer,table.dataTable tfoot th.dt-head-justify div.dt-column-header,table.dataTable thead td.dt-head-justify div.dt-column-footer,table.dataTable thead td.dt-head-justify div.dt-column-header,table.dataTable thead th.dt-head-justify div.dt-column-footer,table.dataTable thead th.dt-head-justify div.dt-column-header{flex-direction:row}table.dataTable tfoot td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable thead th.dt-head-nowrap{white-space:nowrap}table.dataTable tbody td.dt-body-left,table.dataTable tbody th.dt-body-left{text-align:left}table.dataTable tbody td.dt-body-center,table.dataTable tbody th.dt-body-center{text-align:center}table.dataTable tbody td.dt-body-right,table.dataTable tbody th.dt-body-right{text-align:right}table.dataTable tbody td.dt-body-justify,table.dataTable tbody th.dt-body-justify{text-align:justify}table.dataTable tbody td.dt-body-nowrap,table.dataTable tbody th.dt-body-nowrap{white-space:nowrap}/*! Bootstrap 5 integration for DataTables\n *\n * ©2020 SpryMedia Ltd, all rights reserved.\n * License: MIT datatables.net/license/mit\n */table.table.dataTable{clear:both;margin-bottom:0;max-width:none;border-spacing:0}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:none}table.table.dataTable>:not(caption)>*>*{background-color:var(--bs-table-bg)}table.table.dataTable>tbody>tr{background-color:transparent}table.table.dataTable>tbody>tr.selected>*{box-shadow:inset 0 0 0 9999px #0d6efd;box-shadow:inset 0 0 0 9999px rgb(var(--dt-row-selected));color:#fff;color:rgb(var(--dt-row-selected-text))}table.table.dataTable>tbody>tr.selected a{color:#e4e4e4;color:rgb(var(--dt-row-selected-link))}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-stripe),.05)}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1).selected>*{box-shadow:inset 0 0 0 9999px rgba(13,110,253,.95);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected),.95)}table.table.dataTable.table-hover>tbody>tr:hover>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover),.075)}table.table.dataTable.table-hover>tbody>tr.selected:hover>*{box-shadow:inset 0 0 0 9999px rgba(13,110,253,.975);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected),.975)}div.dt-container div.dt-layout-start>:not(:last-child){margin-right:1em}div.dt-container div.dt-layout-end>:not(:first-child){margin-left:1em}div.dt-container div.dt-layout-full{width:100%}div.dt-container div.dt-layout-full>:only-child{margin-left:auto;margin-right:auto}div.dt-container div.dt-layout-table>div{display:block!important}@media screen and (max-width:767px){div.dt-container div.dt-layout-start>:not(:last-child){margin-right:0}div.dt-container div.dt-layout-end>:not(:first-child){margin-left:0}}div.dt-container{position:relative}div.dt-container div.dt-length label{font-weight:400;text-align:left;white-space:nowrap}div.dt-container div.dt-length select{width:auto;display:inline-block;margin-right:.5em}div.dt-container div.dt-search{text-align:right}div.dt-container div.dt-search label{font-weight:400;white-space:nowrap;text-align:left}div.dt-container div.dt-search input{margin-left:.5em;display:inline-block;width:auto}div.dt-container div.dt-paging{margin:0}div.dt-container div.dt-paging ul.pagination{margin:2px 0;flex-wrap:wrap}div.dt-container div.dt-row{position:relative}div.dt-scroll-head table.dataTable{margin-bottom:0!important}div.dt-scroll-body{border-bottom-color:var(--bs-border-color);border-bottom-width:var(--bs-border-width);border-bottom-style:solid}div.dt-scroll-body>table{border-top:none;margin-top:0!important;margin-bottom:0!important}div.dt-scroll-body>table>tbody>tr:first-child{border-top-width:0}div.dt-scroll-body>table>thead>tr{border-width:0!important}div.dt-scroll-body>table>tbody>tr:last-child>*{border-bottom:none}div.dt-scroll-foot>.dt-scroll-footInner{box-sizing:content-box}div.dt-scroll-foot>.dt-scroll-footInner>table{margin-top:0!important;border-top:none}div.dt-scroll-foot>.dt-scroll-footInner>table>tfoot>tr:first-child{border-top-width:0!important}@media screen and (max-width:767px){div.dt-container div.dt-info,div.dt-container div.dt-length,div.dt-container div.dt-paging,div.dt-container div.dt-search{text-align:center}div.dt-container .row{--bs-gutter-y:0.5rem}div.dt-container div.dt-paging ul.pagination{justify-content:center!important}}table.dataTable.table-sm>thead>tr td.dt-orderable-asc,table.dataTable.table-sm>thead>tr td.dt-orderable-desc,table.dataTable.table-sm>thead>tr td.dt-ordering-asc,table.dataTable.table-sm>thead>tr td.dt-ordering-desc,table.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{padding-right:.25rem}table.dataTable.table-sm>thead>tr td.dt-orderable-asc .dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-desc .dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-asc .dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-desc .dt-column-order,table.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{right:.25rem}table.dataTable.table-sm>thead>tr td.dt-type-date .dt-column-order,table.dataTable.table-sm>thead>tr td.dt-type-numeric .dt-column-order,table.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{left:.25rem}div.dt-scroll-head table.table-bordered{border-bottom-width:0}div.table-responsive>div.dt-container>div.row{margin-left:0;margin-right:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:first-child{padding-left:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:last-child{padding-right:0}:root[data-bs-theme=dark]{--dt-row-hover:255,255,255;--dt-row-stripe:255,255,255;--dt-column-ordering:255,255,255}body.dtcr-dragging{overflow-x:hidden}table.dtcr-cloned.dataTable{position:absolute!important;background-color:rgba(255,255,255,.7);z-index:202;border-radius:4px}table.dataTable tbody tr td.dtcr-moving{background-color:rgba(127,127,127,.15)}table.dataTable tbody tr td.dtcr-moving-first{border-left:1px solid #0d6efd}table.dataTable tbody tr td.dtcr-moving-last{border-right:1px solid #0d6efd}html.dark table.dtcr-cloned.dataTable{background-color:rgba(33,33,33,.9)}:root{--dtfc_background:white;--dtfc-thead-cell_background:var(--dtfc_background);--dtfc-tbody-cell_background:var(--dtfc_background)}:root.dark{--dtfc_background:rgb(33, 37, 41)}table.dataTable tfoot tr>.dtfc-fixed-end,table.dataTable tfoot tr>.dtfc-fixed-start,table.dataTable thead tr>.dtfc-fixed-end,table.dataTable thead tr>.dtfc-fixed-start{top:0;bottom:0;z-index:3;background-color:var(--dtfc-thead-cell_background)}table.dataTable tbody tr>.dtfc-fixed-end,table.dataTable tbody tr>.dtfc-fixed-start{z-index:1;background-color:var(--dtfc-tbody-cell_background)}table.dataTable tr>.dtfc-fixed-left::after,table.dataTable tr>.dtfc-fixed-right::after{position:absolute;top:0;bottom:0;width:10px;transition:box-shadow .3s;content:\"\";pointer-events:none}table.dataTable tr>.dtfc-fixed-left::after{right:0;transform:translateX(100%)}table.dataTable tr>.dtfc-fixed-right::after{left:0;transform:translateX(-80%)}table.dataTable.dtfc-scrolling-left tr>.dtfc-fixed-left::after{box-shadow:inset 10px 0 8px -8px rgba(0,0,0,.2)}table.dataTable.dtfc-scrolling-right tr>.dtfc-fixed-right::after{box-shadow:inset -10px 0 8px -8px rgba(0,0,0,.2)}table.dataTable.dtfc-scrolling-right tr>.dtfc-fixed-right+.dtfc-fixed-right::after{box-shadow:none}div.dt-scroll,div.dtfh-floatingparent{position:relative}div.dt-scroll div.dtfc-bottom-blocker,div.dt-scroll div.dtfc-top-blocker,div.dtfh-floatingparent div.dtfc-bottom-blocker,div.dtfh-floatingparent div.dtfc-top-blocker{position:absolute;background-color:var(--dtfc-thead-cell_background)}html.dark table.dataTable.dtfc-scrolling-left tbody>tr>.dtfc-fixed-left::after{box-shadow:inset 10px 0 8px -8px rgba(0,0,0,.3)}html.dark table.dataTable.dtfc-scrolling-right tbody>tr>.dtfc-fixed-right::after{box-shadow:inset -10px 0 8px -8px rgba(0,0,0,.3)}html.dark table.dataTable.dtfc-scrolling-right tbody>tr>.dtfc-fixed-right+.dtfc-fixed-right::after{box-shadow:none}div.dtfc-top-blocker{border-bottom:0 solid #ddd!important}table.dataTable{border-collapse:separate}table.dataTable.table-bordered{border-left-width:0;border-right-width:0}table.dataTable.table-bordered td,table.dataTable.table-bordered th{border-right-width:0;border-top-width:0}table.dataTable.table-bordered td:last-child,table.dataTable.table-bordered th:last-child{border-right:1px solid #dee2e6}table.dataTable.table-bordered tr:last-child td,table.dataTable.table-bordered tr:last-child th{border-bottom-width:0}table.dataTable tfoot tr>.dtfc-fixed-end,table.dataTable tfoot tr>.dtfc-fixed-start,table.dataTable thead tr>.dtfc-fixed-end,table.dataTable thead tr>.dtfc-fixed-start{background-color:var(--bs-table-bg)}table.dataTable tbody tr>.dtfc-fixed-end,table.dataTable tbody tr>.dtfc-fixed-start{background-color:var(--bs-table-bg)}div.dtfc-bottom-blocker,div.dtfc-top-blocker{background-color:var(--bs-body-bg)}div.dt-scroll-body{border-left-color:var(--bs-table-color)!important}div.dt-scroll-footInner table.table-bordered tr th:first-child,div.dt-scroll-headInner table.table-bordered tr th:first-child{border-left-color:var(--bs-border-color)!important}table.dataTable.fixedHeader-floating,table.dataTable.fixedHeader-locked{position:relative!important;background-color:var(--bs-body-bg);margin-top:0!important;margin-bottom:0!important}div.dtfh-floatingparent-foot table{border-top-color:var(--bs-border-color);border-top-width:var(--bs-border-width);border-top-style:solid}@media print{table.fixedHeader-floating,table.fixedHeader-locked{display:none}}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child{cursor:default!important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before{display:none!important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control{cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before{margin-right:.5em;display:inline-block;box-sizing:border-box;content:\"\";border-top:5px solid transparent;border-left:10px solid rgba(0,0,0,.5);border-bottom:5px solid transparent;border-right:0 solid transparent}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control.arrow-right::before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control.arrow-right::before{border-top:5px solid transparent;border-left:0 solid transparent;border-bottom:5px solid transparent;border-right:10px solid rgba(0,0,0,.5)}table.dataTable.dtr-inline.collapsed>tbody>tr.dtr-expanded>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr.dtr-expanded>th.dtr-control:before{border-top:10px solid rgba(0,0,0,.5);border-left:5px solid transparent;border-bottom:0 solid transparent;border-right:5px solid transparent}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control{padding-left:.333em}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>td.dtr-control,table.dataTable.dtr-column>tbody>tr>th.control,table.dataTable.dtr-column>tbody>tr>th.dtr-control{cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>td.dtr-control:before,table.dataTable.dtr-column>tbody>tr>th.control:before,table.dataTable.dtr-column>tbody>tr>th.dtr-control:before{display:inline-block;box-sizing:border-box;content:\"\";border-top:5px solid transparent;border-left:10px solid rgba(0,0,0,.5);border-bottom:5px solid transparent;border-right:0 solid transparent}table.dataTable.dtr-column>tbody>tr>td.control.arrow-right::before,table.dataTable.dtr-column>tbody>tr>td.dtr-control.arrow-right::before,table.dataTable.dtr-column>tbody>tr>th.control.arrow-right::before,table.dataTable.dtr-column>tbody>tr>th.dtr-control.arrow-right::before{border-top:5px solid transparent;border-left:0 solid transparent;border-bottom:5px solid transparent;border-right:10px solid rgba(0,0,0,.5)}table.dataTable.dtr-column>tbody>tr.dtr-expanded td.control:before,table.dataTable.dtr-column>tbody>tr.dtr-expanded td.dtr-control:before,table.dataTable.dtr-column>tbody>tr.dtr-expanded th.control:before,table.dataTable.dtr-column>tbody>tr.dtr-expanded th.dtr-control:before{border-top:10px solid rgba(0,0,0,.5);border-left:5px solid transparent;border-bottom:0 solid transparent;border-right:5px solid transparent}table.dataTable>tbody>tr.child{padding:.5em 1em}table.dataTable>tbody>tr.child:hover{background:0 0!important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details>li{border-bottom:1px solid #efefef;padding:.5em 0}table.dataTable>tbody>tr.child ul.dtr-details>li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details>li:last-child{padding-bottom:0;border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:700}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:fit-content;max-height:75%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid #000;border-radius:.5em;box-shadow:0 12px 30px rgba(0,0,0,.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:2.5em}div.dtr-modal div.dtr-modal-content h2{margin-top:0}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,.6)}@media screen and (max-width:767px){div.dtr-modal div.dtr-modal-display{width:95%}}html.dark table.dataTable>tbody>tr>td.dtr-control:before,html[data-bs-theme=dark] table.dataTable>tbody>tr>td.dtr-control:before{border-left-color:rgba(255,255,255,.5)!important}html.dark table.dataTable>tbody>tr>td.dtr-control.arrow-right::before,html[data-bs-theme=dark] table.dataTable>tbody>tr>td.dtr-control.arrow-right::before{border-right-color:rgba(255,255,255,.5)!important}html.dark table.dataTable>tbody>tr.dtr-expanded>td.dtr-control:before,html.dark table.dataTable>tbody>tr.dtr-expanded>th.dtr-control:before,html[data-bs-theme=dark] table.dataTable>tbody>tr.dtr-expanded>td.dtr-control:before,html[data-bs-theme=dark] table.dataTable>tbody>tr.dtr-expanded>th.dtr-control:before{border-top-color:rgba(255,255,255,.5)!important;border-left-color:transparent!important;border-right-color:transparent!important}html.dark table.dataTable>tbody>tr.child ul.dtr-details>li,html[data-bs-theme=dark] table.dataTable>tbody>tr.child ul.dtr-details>li{border-bottom-color:#404346}html.dark div.dtr-modal div.dtr-modal-display,html[data-bs-theme=dark] div.dtr-modal div.dtr-modal-display{background-color:#212529;border:1px solid rgba(255,255,255,.15)}div.dtr-bs-modal table.table tr:first-child td{border-top:none}table.dataTable.table-bordered td.dtr-control.dtr-hidden+*,table.dataTable.table-bordered th.dtr-control.dtr-hidden+*{border-left-width:1px}div.dts{display:block!important}div.dts tbody td,div.dts tbody th{white-space:nowrap}div.dts div.dts_loading{z-index:1}div.dts div.dts_label{position:absolute;right:20px;background:rgba(0,0,0,.8);color:#fff;box-shadow:3px 3px 10px rgba(0,0,0,.5);text-align:right;border-radius:3px;padding:.4em;z-index:2;display:none}div.dts div.dataTables_scrollBody,div.dts div.dt-scroll-body{background:repeating-linear-gradient(45deg,rgba(0,0,0,.025),rgba(0,0,0,.025) 10px,rgba(0,0,0,0) 10px,rgba(0,0,0,0) 20px)}div.dts div.dataTables_scrollBody table,div.dts div.dt-scroll-body table{background-color:#fff;z-index:2}div.dts div.dataTables_length,div.dts div.dataTables_paginate,div.dts div.dt-length,div.dts div.dt-paging{display:none}html.dark div.dts div.dts_label{background:rgba(255,255,255,.8);color:#000}html.dark div.dts div.dataTables_scrollBody,html.dark div.dts div.dt-scroll-body{background:repeating-linear-gradient(45deg,rgba(255,255,255,.025),rgba(255,255,255,.025) 10px,rgba(255,255,255,0) 10px,rgba(255,255,255,0) 20px)}html.dark div.dts div.dataTables_scrollBody table,html.dark div.dts div.dt-scroll-body table{background-color:var(--dt-html-background);z-index:2}div.DTS div.dataTables_scrollBody table{background-color:#fff}html[data-bs-theme=dark] div.DTS div.dataTables_scrollBody table{background-color:var(--bs-body-bg)}table.dataTable>tbody>tr>.selected{background-color:#0d6efd;color:rgb(var(--dt-row-selected-text))}div.dt-container span.select-info,div.dt-container span.select-item{margin-left:.5em}@media screen and (max-width:640px){div.dt-container span.select-info,div.dt-container span.select-item{margin-left:0;display:block}}table.dataTable.table-sm tbody td.select-checkbox::before{margin-top:-9px}:root{--dt-datetime_background:white;--dt-datetime_zindex:2050;--dt-datetime_border:1px solid #ccc;--dt-datetime_box-shadow:0 5px 15px -5px rgba(0,0,0,.5);--dt-datetime_padding:6px 20px;--dt-datetime_width:275px;--dt-datetime_border-radius:5px;--dt-datetime-inline_padding:6px 0;--dt-datetime-inline_background:transparent;--dt-datetime-title_padding:5px 0px 3px;--dt-datetime-buttons-link_padding:0 0.5em 0.5em 0.5em;--dt-datetime-buttons-link_font-size:0.9em;--dt-datetime-table-header_font-size:0.8em;--dt-datetime-table-header_color:#777;--dt-datetime-table-header_padding:0 0 4px 0;--dt-datetime-table-body_font-size:0.9em;--dt-datetime-table-body_color:#444;--dt-datetime-table-body_padding:0;--dt-datetime-table-selectable_background:#f5f5f5;--dt-datetime-table-selectable_disabled-color:#aaa;--dt-datetime-table-selectable_disabled-background:white;--dt-datetime-table-selectable_disabled-hover-color:#aaa;--dt-datetime-table-selectable_disabled-hover-background:white;--dt-datetime-table_hover-background:#ff8000;--dt-datetime-table_hover-color:white;--dt-datetime-table-now_background:#ddd;--dt-datetime-table-selected_background:#4E6CA3;--dt-datetime-table-selected_color:white;--dt-datetime-label_height:30px;--dt-datetime-label_border:1px solid transparent;--dt-datetime-label_padding:5px 6px;--dt-datetime-label_hover-border:1px solid #ddd;--dt-datetime-label_hover-background:#f5f5f5;--dt-datetime-icon_border:1px solid transparent;--dt-datetime-icon-opacity:0.3;--dt-datetime-icon_hover-border:1px solid #ccc;--dt-datetime-icon_hover-background:#f0f0f0;--dt-datetime-icon_hover-opacity:0.6;--dt-datetime-icon-triangle_border-color:black;--dt-datetime-error_color:#b11f1f}:root[data-bs-theme=dark],:root[data-theme=dark],html.dark{--dt-datetime_background:rgb(33, 37, 41);--dt-datetime_border:1px solid rgb(89, 91, 94);--dt-datetime_box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.8);--dt-datetime-table-header_color:#ccc;--dt-datetime-table-body_color:#eee;--dt-datetime-table-selectable_background:rgb(55, 60, 65);--dt-datetime-table-selectable_disabled-color:#aaa;--dt-datetime-table-selectable_disabled-background:rgb(23, 27, 31);--dt-datetime-table-selectable_disabled-hover-color:#aaa;--dt-datetime-table-selectable_disabled-hover-background:rgb(23, 27, 31);--dt-datetime-table_hover-background:#ff8000;--dt-datetime-table_hover-color:black;--dt-datetime-table-now_background:rgb(75, 80, 85);--dt-datetime-table-selected_background:#6ea8fe;--dt-datetime-table-selected_color:black;--dt-datetime-label_border:1px solid transparent;--dt-datetime-label_hover-border:1px solid transparent;--dt-datetime-label_hover-background:rgba(255, 255, 255, 0.1);--dt-datetime-icon_border:1px solid transparent;--dt-datetime-icon_hover-border:1px solid transparent;--dt-datetime-icon_hover-background:rgba(255, 255, 255, 0.1);--dt-datetime-icon-triangle_border-color:white;--dt-datetime-error_color:#b11f1f}div.dt-datetime{position:absolute;background-color:var(--dt-datetime_background);z-index:var(--dt-datetime_zindex);border:var(--dt-datetime_border);box-shadow:var(--dt-datetime_box-shadow);padding:var(--dt-datetime_padding);width:var(--dt-datetime_width);border-radius:var(--dt-datetime_border-radius)}div.dt-datetime.inline{position:relative;box-shadow:none;border:none;z-index:inherit;padding:var(--dt-datetime-inline_padding);background-color:var(--dt-datetime-inline_background)}div.dt-datetime div.dt-datetime-title{text-align:center;padding:var(--dt-datetime-title_padding)}div.dt-datetime div.dt-datetime-buttons{text-align:center}div.dt-datetime div.dt-datetime-buttons a{display:inline-block;padding:var(--dt-datetime-buttons-link_padding);margin:0;font-size:var(--dt-datetime-buttons-link_font-size)}div.dt-datetime div.dt-datetime-buttons a:hover{text-decoration:underline}div.dt-datetime table{border-spacing:0;margin:12px 0;width:100%}div.dt-datetime table.dt-datetime-table-nospace{margin-top:-12px}div.dt-datetime table th{font-size:var(--dt-datetime-table-header_font-size);color:var(--dt-datetime-table-header_color);font-weight:400;width:14.285714286%;padding:var(--dt-datetime-table-header_padding);text-align:center}div.dt-datetime table td{font-size:var(--dt-datetime-table-body_font-size);color:var(--dt-datetime-table-body_color);padding:var(--dt-datetime-table-body_padding)}div.dt-datetime table td.selectable{text-align:center;background:var(--dt-datetime-table-selectable_background)}div.dt-datetime table td.selectable.disabled{color:var(--dt-datetime-table-selectable_disabled-color);background:var(--dt-datetime-table-selectable_disabled-background)}div.dt-datetime table td.selectable.disabled button:hover{color:var(--dt-datetime-table-selectable_disabled-hover-color);background:var(--dt-datetime-table-selectable_disabled-hover-background)}div.dt-datetime table td.selectable.now{background-color:var(--dt-datetime-table-now_background)}div.dt-datetime table td.selectable.now button{font-weight:700}div.dt-datetime table td.selectable.selected button{background:var(--dt-datetime-table-selected_background);color:var(--dt-datetime-table-selected_color);border-radius:2px}div.dt-datetime table td.selectable button:hover{background:var(--dt-datetime-table_hover-background);color:var(--dt-datetime-table_hover-color);border-radius:2px}div.dt-datetime table td.dt-datetime-week{font-size:.7em}div.dt-datetime table button{width:100%;box-sizing:border-box;border:none;background:0 0;font-size:inherit;color:inherit;text-align:center;padding:4px 0;cursor:pointer;margin:0}div.dt-datetime table button span{display:inline-block;min-width:14px;text-align:right}div.dt-datetime table.weekNumber th{width:12.5%}div.dt-datetime div.dt-datetime-calendar table{margin-top:0}div.dt-datetime div.dt-datetime-label{position:relative;display:inline-block;height:var(--dt-datetime-label_height);padding:var(--dt-datetime-label_padding);border:var(--dt-datetime-label_border);box-sizing:border-box;cursor:pointer}div.dt-datetime div.dt-datetime-label:hover{border:var(--dt-datetime-label_hover-border);border-radius:2px;background-color:var(--dt-datetime-label_hover-background)}div.dt-datetime div.dt-datetime-label select{position:absolute;top:6px;left:0;cursor:pointer;opacity:0}div.dt-datetime.horizontal{width:550px}div.dt-datetime.horizontal div.dt-datetime-date,div.dt-datetime.horizontal div.dt-datetime-time{width:48%}div.dt-datetime.horizontal div.dt-datetime-time{margin-left:4%}div.dt-datetime div.dt-datetime-date{position:relative;float:left;width:100%}div.dt-datetime div.dt-datetime-time{position:relative;float:left;width:100%;text-align:center}div.dt-datetime div.dt-datetime-time>span{vertical-align:middle}div.dt-datetime div.dt-datetime-time th{text-align:left}div.dt-datetime div.dt-datetime-time div.dt-datetime-timeblock{display:inline-block;vertical-align:middle}div.dt-datetime div.dt-datetime-iconLeft,div.dt-datetime div.dt-datetime-iconRight{width:30px;height:30px;background-position:center;background-repeat:no-repeat;opacity:var(--dt-datetime-icon-opacity);overflow:hidden;box-sizing:border-box;border:var(--dt-datetime-icon_border)}div.dt-datetime div.dt-datetime-iconLeft:hover,div.dt-datetime div.dt-datetime-iconRight:hover{border:var(--dt-datetime-icon_hover-border);border-radius:2px;background-color:var(--dt-datetime-icon_hover-background);opacity:var(--dt-datetime-icon_hover-opacity)}div.dt-datetime div.dt-datetime-iconLeft button,div.dt-datetime div.dt-datetime-iconRight button{border:none;background:0 0;text-indent:30px;height:100%;width:100%;cursor:pointer}div.dt-datetime div.dt-datetime-iconLeft{position:absolute;top:5px;left:5px}div.dt-datetime div.dt-datetime-iconLeft button{position:relative;z-index:1}div.dt-datetime div.dt-datetime-iconLeft:after{position:absolute;top:7px;left:10px;display:block;content:\"\";border-top:7px solid transparent;border-right:7px solid var(--dt-datetime-icon-triangle_border-color);border-bottom:7px solid transparent}div.dt-datetime div.dt-datetime-iconRight{position:absolute;top:5px;right:5px}div.dt-datetime div.dt-datetime-iconRight button{position:relative;z-index:1}div.dt-datetime div.dt-datetime-iconRight:after{position:absolute;top:7px;left:12px;display:block;content:\"\";border-top:7px solid transparent;border-left:7px solid var(--dt-datetime-icon-triangle_border-color);border-bottom:7px solid transparent}div.dt-datetime-error{clear:both;padding:0 1em;max-width:240px;font-size:11px;line-height:1.25em;text-align:center;color:var(--dt-datetime-error_color)}:root[data-bs-theme=dark] input.dt-datetime,:root[data-bs-theme=dark] select,:root[data-theme=dark] input.dt-datetime,:root[data-theme=dark] select,html.dark input.dt-datetime,html.dark select{color-scheme:dark}:root[data-bs-theme=dark].inline,:root[data-theme=dark].inline,html.dark.inline{box-shadow:none;border:none}"
  },
  {
    "path": "static/assets/plugins/custom/datatables/datatables.bundle.js",
    "content": "/*! DataTables 2.3.7\n * © SpryMedia Ltd - datatables.net/license\n */\n!function(e){\"use strict\";if(\"function\"==typeof define&&define.amd)define([\"jquery\"],function(t){return e(t,window,document)});else if(\"object\"==typeof exports){var t=require(\"jquery\");\"undefined\"==typeof window?module.exports=function(n,o){return n||(n=window),o||(o=t(n)),e(o,n,n.document)}:module.exports=e(t,window,window.document)}else window.DataTable=e(jQuery,window,document)}(function(e,t,n){\"use strict\";var o,r,i,a,s=function(n,o){if(s.factory(n,o))return s;if(this instanceof s)return e(n).DataTable(o);var i=this,a=void 0===(o=n),l=this.length;return a&&(o={}),this.api=function(){return new r(this)},this.each(function(){var n,c=l>1?Ge({},o,!0):o,d=0,u=this.getAttribute(\"id\"),f=s.defaults,h=e(this);if(\"table\"==this.nodeName.toLowerCase()){c.on&&c.on.options&&lt(h,\"options\",c.on.options),h.trigger(\"options.dt\",c),F(f),j(f.column),N(f,f,!0),N(f.column,f.column,!0),N(f,e.extend(c,ct(h.data())),!0);var p=s.settings;for(d=0,n=p.length;d<n;d++){var m=p[d];if(m.nTable==this||m.nTHead&&m.nTHead.parentNode==this||m.nTFoot&&m.nTFoot.parentNode==this){var g=void 0!==c.bRetrieve?c.bRetrieve:f.bRetrieve,v=void 0!==c.bDestroy?c.bDestroy:f.bDestroy;if(a||g)return m.oInstance;if(v){new s.Api(m).destroy();break}return void Qe(m,0,\"Cannot reinitialise DataTable\",3)}if(m.sTableId==this.id){p.splice(d,1);break}}null!==u&&\"\"!==u||(u=\"DataTables_Table_\"+s.ext._unique++,this.id=u),h.children(\"colgroup\").remove();var b=e.extend(!0,{},s.models.oSettings,{sDestroyWidth:h[0].style.width,sInstance:u,sTableId:u,colgroup:e(\"<colgroup>\"),fastData:function(e,t,n){return J(b,e,t,n)}});b.nTable=this,b.oInit=c,p.push(b),b.api=new r(b),b.oInstance=1===i.length?i:h.dataTable(),F(c),c.aLengthMenu&&!c.iDisplayLength&&(c.iDisplayLength=Array.isArray(c.aLengthMenu[0])?c.aLengthMenu[0][0]:e.isPlainObject(c.aLengthMenu[0])?c.aLengthMenu[0].value:c.aLengthMenu[0]),c=Ge(e.extend(!0,{},f),c),Ke(b.oFeatures,c,[\"bPaginate\",\"bLengthChange\",\"bFilter\",\"bSort\",\"bSortMulti\",\"bInfo\",\"bProcessing\",\"bAutoWidth\",\"bSortClasses\",\"bServerSide\",\"bDeferRender\"]),Ke(b,c,[\"ajax\",\"fnFormatNumber\",\"sServerMethod\",\"aaSorting\",\"aaSortingFixed\",\"aLengthMenu\",\"sPaginationType\",\"iStateDuration\",\"bSortCellsTop\",\"iTabIndex\",\"sDom\",\"fnStateLoadCallback\",\"fnStateSaveCallback\",\"renderer\",\"searchDelay\",\"rowId\",\"caption\",\"layout\",\"orderDescReverse\",\"orderIndicators\",\"orderHandler\",\"titleRow\",\"typeDetect\",\"columnTitleTag\",[\"iCookieDuration\",\"iStateDuration\"],[\"oSearch\",\"oPreviousSearch\"],[\"aoSearchCols\",\"aoPreSearchCols\"],[\"iDisplayLength\",\"_iDisplayLength\"]]),Ke(b.oScroll,c,[[\"sScrollX\",\"sX\"],[\"sScrollXInner\",\"sXInner\"],[\"sScrollY\",\"sY\"],[\"bScrollCollapse\",\"bCollapse\"]]),Ke(b.oLanguage,c,\"fnInfoCallback\"),tt(b,\"aoDrawCallback\",c.fnDrawCallback),tt(b,\"aoStateSaveParams\",c.fnStateSaveParams),tt(b,\"aoStateLoadParams\",c.fnStateLoadParams),tt(b,\"aoStateLoaded\",c.fnStateLoaded),tt(b,\"aoRowCallback\",c.fnRowCallback),tt(b,\"aoRowCreatedCallback\",c.fnCreatedRow),tt(b,\"aoHeaderCallback\",c.fnHeaderCallback),tt(b,\"aoFooterCallback\",c.fnFooterCallback),tt(b,\"aoInitComplete\",c.fnInitComplete),tt(b,\"aoPreDrawCallback\",c.fnPreDrawCallback),b.rowIdFn=te(c.rowId),c.on&&Object.keys(c.on).forEach(function(e){lt(h,e,c.on[e])}),function(n){if(!s.__browser){var o={};s.__browser=o;var r=e(\"<div/>\").css({position:\"fixed\",top:0,left:-1*t.pageXOffset,height:1,width:1,overflow:\"hidden\"}).append(e(\"<div/>\").css({position:\"absolute\",top:1,left:1,width:100,overflow:\"scroll\"}).append(e(\"<div/>\").css({width:\"100%\",height:10}))).appendTo(\"body\"),i=r.children(),a=i.children();o.barWidth=i[0].offsetWidth-i[0].clientWidth,o.bScrollbarLeft=1!==Math.round(a.offset().left),r.remove()}e.extend(n.oBrowser,s.__browser),n.oScroll.iBarWidth=s.__browser.barWidth}(b);var y=b.oClasses;e.extend(y,s.ext.classes,c.oClasses),h.addClass(y.table),b.oFeatures.bPaginate||(c.iDisplayStart=0),void 0===b.iInitDisplayStart&&(b.iInitDisplayStart=c.iDisplayStart,b._iDisplayStart=c.iDisplayStart);var w=c.iDeferLoading;if(null!==w){b.deferLoading=!0;var _=Array.isArray(w);b._iRecordsDisplay=_?w[0]:w,b._iRecordsTotal=_?w[1]:w}var x=[],T=this.getElementsByTagName(\"thead\"),D=we(b,T[0]);if(c.aoColumns)x=c.aoColumns;else if(D.length)for(d=0,n=D[0].length;d<n;d++)x.push(null);for(d=0,n=x.length;d<n;d++)O(b);!function(t,n,o,r,i){var a,s,l,c,d,u,f,h=t.aoColumns;if(o)for(a=0,s=o.length;a<s;a++)o[a]&&o[a].name&&(h[a].sName=o[a].name);if(n)for(a=n.length-1;a>=0;a--){var p=void 0!==(f=n[a]).target?f.target:void 0!==f.targets?f.targets:f.aTargets;for(Array.isArray(p)||(p=[p]),l=0,c=p.length;l<c;l++){var m=p[l];if(\"number\"==typeof m&&m>=0){for(;h.length<=m;)O(t);i(m,f)}else if(\"number\"==typeof m&&m<0)i(h.length+m,f);else if(\"string\"==typeof m)for(d=0,u=h.length;d<u;d++)\"_all\"===m?i(d,f):-1!==m.indexOf(\":name\")?h[d].sName===m.replace(\":name\",\"\")&&i(d,f):r.forEach(function(t){if(t[d]){var n=e(t[d].cell);m.match(/^[a-z][\\w-]*$/i)&&(m=\".\"+m),n.is(m)&&i(d,f)}})}}if(o)for(a=0,s=o.length;a<s;a++)i(a,o[a])}(b,c.aoColumnDefs,x,D,function(e,t){P(b,e,t)});var C=h.children(\"tbody\").find(\"tr:first-child\").eq(0);if(C.length){var S=function(e,t){return null!==e.getAttribute(\"data-\"+t)?t:null};e(C[0]).children(\"th, td\").each(function(e,t){var n=b.aoColumns[e];if(n||Qe(b,0,\"Incorrect column count\",18),n.mData===e){var o=S(t,\"sort\")||S(t,\"order\"),r=S(t,\"filter\")||S(t,\"search\");null===o&&null===r||(n.mData={_:e+\".display\",sort:null!==o?e+\".@data-\"+o:void 0,type:null!==o?e+\".@data-\"+o:void 0,filter:null!==r?e+\".@data-\"+r:void 0},n._isArrayHost=!0,P(b,e))}})}tt(b,\"aoDrawCallback\",Ze);var A=b.oFeatures;if(c.bStateSave&&(A.bStateSave=!0),void 0===c.aaSorting){var R=b.aaSorting;for(d=0,n=R.length;d<n;d++)R[d][1]=b.aoColumns[d].asSorting[0]}Ye(b),tt(b,\"aoDrawCallback\",function(){(b.bSorted||\"ssp\"===it(b)||A.bDeferRender)&&Ye(b)});var I=h.children(\"caption\");b.caption&&(0===I.length&&(I=e(\"<caption/>\").prependTo(h)),I.html(b.caption)),I.length&&(I[0]._captionSide=I.css(\"caption-side\"),b.captionNode=I[0]),I.length?b.colgroup.insertAfter(I):b.colgroup.prependTo(b.nTable),0===T.length&&(T=e(\"<thead/>\").appendTo(h)),b.nTHead=T[0];var k=h.children(\"tbody\");0===k.length&&(k=e(\"<tbody/>\").insertAfter(T)),b.nTBody=k[0];var L=h.children(\"tfoot\");0===L.length&&(L=e(\"<tfoot/>\").appendTo(h)),b.nTFoot=L[0],b.aiDisplay=b.aiDisplayMaster.slice(),b.bInitialised=!0;var H=b.oLanguage;e.extend(!0,H,c.oLanguage),H.sUrl?e.ajax({dataType:\"json\",url:H.sUrl,success:function(t){N(f.oLanguage,t),e.extend(!0,H,t,b.oInit.oLanguage),nt(b,null,\"i18n\",[b],!0),Ie(b)},error:function(){Qe(b,0,\"i18n file loading error\",21),Ie(b)}}):(nt(b,null,\"i18n\",[b],!0),Ie(b))}else Qe(null,0,\"Non-table node initialisation (\"+this.nodeName+\")\",2)}),i=null,this};s.ext=o={builder:\"-source-\",buttons:{},ccContent:{},classes:{},errMode:\"alert\",escape:{attributes:!1},feature:[],features:{},search:[],selector:{cell:[],column:[],row:[]},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{className:{},detect:[],render:{},search:{},order:{}},_unique:0,fnVersionCheck:s.fnVersionCheck,iApiIndex:0,sVersion:s.version},e.extend(o,{afnFiltering:o.search,aTypes:o.type.detect,ofnSearch:o.type.search,oSort:o.type.order,afnSortData:o.order,aoFeatures:o.feature,oStdClasses:o.classes,oPagination:o.pager}),e.extend(s.ext.classes,{container:\"dt-container\",empty:{row:\"dt-empty\"},info:{container:\"dt-info\"},layout:{row:\"dt-layout-row\",cell:\"dt-layout-cell\",tableRow:\"dt-layout-table\",tableCell:\"\",start:\"dt-layout-start\",end:\"dt-layout-end\",full:\"dt-layout-full\"},length:{container:\"dt-length\",select:\"dt-input\"},order:{canAsc:\"dt-orderable-asc\",canDesc:\"dt-orderable-desc\",isAsc:\"dt-ordering-asc\",isDesc:\"dt-ordering-desc\",none:\"dt-orderable-none\",position:\"sorting_\"},processing:{container:\"dt-processing\"},scrolling:{body:\"dt-scroll-body\",container:\"dt-scroll\",footer:{self:\"dt-scroll-foot\",inner:\"dt-scroll-footInner\"},header:{self:\"dt-scroll-head\",inner:\"dt-scroll-headInner\"}},search:{container:\"dt-search\",input:\"dt-input\"},table:\"dataTable\",tbody:{cell:\"\",row:\"\"},thead:{cell:\"\",row:\"\"},tfoot:{cell:\"\",row:\"\"},paging:{active:\"current\",button:\"dt-paging-button\",container:\"dt-paging\",disabled:\"disabled\",nav:\"\"}});var l={},c=/[\\r\\n\\u2028]/g,d=/<([^>]*>)/g,u=Math.pow(2,28),f=/^\\d{2,4}[./-]\\d{1,2}[./-]\\d{1,2}([T ]{1}\\d{1,2}[:.]\\d{2}([.:]\\d{2})?)?$/,h=new RegExp(\"(\\\\\"+[\"/\",\".\",\"*\",\"+\",\"?\",\"|\",\"(\",\")\",\"[\",\"]\",\"{\",\"}\",\"\\\\\",\"$\",\"^\",\"-\"].join(\"|\\\\\")+\")\",\"g\"),p=/['\\u00A0,$£€¥%\\u2009\\u202F\\u20BD\\u20a9\\u20BArfkɃΞ]/gi,m=function(e){return!e||!0===e||\"-\"===e},g=function(e){var t=parseInt(e,10);return!isNaN(t)&&isFinite(e)?t:null},v=function(e,t){return l[t]||(l[t]=new RegExp(Se(t),\"g\")),\"string\"==typeof e&&\".\"!==t?e.replace(/\\./g,\"\").replace(l[t],\".\"):e},b=function(e,t,n,o){var r=typeof e,i=\"string\"===r;return\"number\"===r||\"bigint\"===r||(!(!o||!m(e))||(t&&i&&(e=v(e,t)),n&&i&&(e=e.replace(p,\"\")),!isNaN(parseFloat(e))&&isFinite(e)))},y=function(e,t,n,o){if(o&&m(e))return!0;if(\"string\"==typeof e&&e.match(/<(input|select)/i))return null;var r=function(e){return m(e)||\"string\"==typeof e}(e);return r&&!!b(D(e),t,n,o)||null},w=function(e,t,n){var o=[],r=0,i=e.length;if(void 0!==n)for(;r<i;r++)e[r]&&e[r][t]&&o.push(e[r][t][n]);else for(;r<i;r++)e[r]&&o.push(e[r][t]);return o},_=function(e,t,n,o){var r=[],i=0,a=t.length;if(void 0!==o)for(;i<a;i++)e[t[i]]&&e[t[i]][n]&&r.push(e[t[i]][n][o]);else for(;i<a;i++)e[t[i]]&&r.push(e[t[i]][n]);return r},x=function(e,t){var n,o=[];void 0===t?(t=0,n=e):(n=t,t=e);for(var r=t;r<n;r++)o.push(r);return o},T=function(e){for(var t=[],n=0,o=e.length;n<o;n++)e[n]&&t.push(e[n]);return t},D=function(e,t){if(!e||\"string\"!=typeof e)return e;if(e.length>u)throw new Error(\"Exceeded max str len\");var n;e=e.replace(d,t||\"\");do{n=e,e=e.replace(/<script/i,\"\")}while(e!==n);return n},C=function(e){return Array.isArray(e)&&(e=e.join(\",\")),\"string\"==typeof e?e.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\"):e},S=function(e,t){if(\"string\"!=typeof e)return e;var n=e.normalize?e.normalize(\"NFD\"):e;return n.length!==e.length?(!0===t?e+\" \":\"\")+n.replace(/[\\u0300-\\u036f]/g,\"\"):n},A=function(e){if(Array.from&&Set)return Array.from(new Set(e));if(function(e){if(e.length<2)return!0;for(var t=e.slice().sort(),n=t[0],o=1,r=t.length;o<r;o++){if(t[o]===n)return!1;n=t[o]}return!0}(e))return e.slice();var t,n,o,r=[],i=e.length,a=0;e:for(n=0;n<i;n++){for(t=e[n],o=0;o<a;o++)if(r[o]===t)continue e;r.push(t),a++}return r},R=function(e,t){if(Array.isArray(t))for(var n=0;n<t.length;n++)R(e,t[n]);else e.push(t);return e};function I(e,t){t&&t.split(\" \").forEach(function(t){t&&e.classList.add(t)})}function k(t){var n,o,r={};e.each(t,function(e){(n=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!==\"a aa ai ao as b fn i m o s \".indexOf(n[1]+\" \")&&(o=e.replace(n[0],n[2].toLowerCase()),r[o]=e,\"o\"===n[1]&&k(t[e]))}),t._hungarianMap=r}function N(t,n,o){var r;t._hungarianMap||k(t),e.each(n,function(i){void 0===(r=t._hungarianMap[i])||!o&&void 0!==n[r]||(\"o\"===r.charAt(0)?(n[r]||(n[r]={}),e.extend(!0,n[r],n[i]),N(t[r],n[r],o)):n[r]=n[i])})}s.util={diacritics:function(e,t){if(\"function\"!==typeof e)return S(e,t);S=e},debounce:function(e,t){var n;return function(){var o=this,r=arguments;clearTimeout(n),n=setTimeout(function(){e.apply(o,r)},t||250)}},throttle:function(e,t){var n,o,r=void 0!==t?t:200;return function(){var t=this,i=+new Date,a=arguments;n&&i<n+r?(clearTimeout(o),o=setTimeout(function(){n=void 0,e.apply(t,a)},r)):(n=i,e.apply(t,a))}},escapeRegex:function(e){return e.replace(h,\"\\\\$1\")},set:function(t){if(e.isPlainObject(t))return s.util.set(t._);if(null===t)return function(){};if(\"function\"==typeof t)return function(e,n,o){t(e,\"set\",n,o)};if(\"string\"!=typeof t||-1===t.indexOf(\".\")&&-1===t.indexOf(\"[\")&&-1===t.indexOf(\"(\"))return function(e,n){e[t]=n};var n=function(e,t,o){for(var r,i,a,s,l,c=ee(o),d=c[c.length-1],u=0,f=c.length-1;u<f;u++){if(\"__proto__\"===c[u]||\"constructor\"===c[u])throw new Error(\"Cannot set prototype values\");if(i=c[u].match(K),a=c[u].match(G),i){if(c[u]=c[u].replace(K,\"\"),e[c[u]]=[],(r=c.slice()).splice(0,u+1),l=r.join(\".\"),Array.isArray(t))for(var h=0,p=t.length;h<p;h++)n(s={},t[h],l),e[c[u]].push(s);else e[c[u]]=t;return}a&&(c[u]=c[u].replace(G,\"\"),e=e[c[u]](t)),null!==e[c[u]]&&void 0!==e[c[u]]||(e[c[u]]={}),e=e[c[u]]}d.match(G)?e=e[d.replace(G,\"\")](t):e[d.replace(K,\"\")]=t};return function(e,o){return n(e,o,t)}},get:function(t){if(e.isPlainObject(t)){var n={};return e.each(t,function(e,t){t&&(n[e]=s.util.get(t))}),function(e,t,o,r){var i=n[t]||n._;return void 0!==i?i(e,t,o,r):e}}if(null===t)return function(e){return e};if(\"function\"==typeof t)return function(e,n,o,r){return t(e,n,o,r)};if(\"string\"!=typeof t||-1===t.indexOf(\".\")&&-1===t.indexOf(\"[\")&&-1===t.indexOf(\"(\"))return function(e){return e[t]};var o=function(e,t,n){var r,i,a,s;if(\"\"!==n)for(var l=ee(n),c=0,d=l.length;c<d;c++){if(r=l[c].match(K),i=l[c].match(G),r){if(l[c]=l[c].replace(K,\"\"),\"\"!==l[c]&&(e=e[l[c]]),a=[],l.splice(0,c+1),s=l.join(\".\"),Array.isArray(e))for(var u=0,f=e.length;u<f;u++)a.push(o(e[u],t,s));var h=r[0].substring(1,r[0].length-1);e=\"\"===h?a:a.join(h);break}if(i)l[c]=l[c].replace(G,\"\"),e=e[l[c]]();else{if(null===e||null===e[l[c]])return null;if(void 0===e||void 0===e[l[c]])return;e=e[l[c]]}}return e};return function(e,n){return o(e,n,t)}},stripHtml:function(e,t){var n=typeof e;if(\"function\"!==n)return\"string\"===n?D(e,t):e;D=e},escapeHtml:function(e){var t=typeof e;if(\"function\"!==t)return\"string\"===t||Array.isArray(e)?C(e):e;C=e},unique:A};var L=function(e,t,n){void 0!==e[t]&&(e[n]=e[t])};function F(e){L(e,\"ordering\",\"bSort\"),L(e,\"orderMulti\",\"bSortMulti\"),L(e,\"orderClasses\",\"bSortClasses\"),L(e,\"orderCellsTop\",\"bSortCellsTop\"),L(e,\"order\",\"aaSorting\"),L(e,\"orderFixed\",\"aaSortingFixed\"),L(e,\"paging\",\"bPaginate\"),L(e,\"pagingType\",\"sPaginationType\"),L(e,\"pageLength\",\"iDisplayLength\"),L(e,\"searching\",\"bFilter\"),\"boolean\"==typeof e.sScrollX&&(e.sScrollX=e.sScrollX?\"100%\":\"\"),\"boolean\"==typeof e.scrollX&&(e.scrollX=e.scrollX?\"100%\":\"\"),\"object\"==typeof e.bSort?(e.orderIndicators=void 0===e.bSort.indicators||e.bSort.indicators,e.orderHandler=void 0===e.bSort.handler||e.bSort.handler,e.bSort=!0):!1===e.bSort?(e.orderIndicators=!1,e.orderHandler=!1):!0===e.bSort&&(e.orderIndicators=!0,e.orderHandler=!0),\"boolean\"==typeof e.bSortCellsTop&&(e.titleRow=e.bSortCellsTop);var t=e.aoSearchCols;if(t)for(var n=0,o=t.length;n<o;n++)t[n]&&N(s.models.oSearch,t[n]);e.serverSide&&!e.searchDelay&&(e.searchDelay=400)}function j(e){L(e,\"orderable\",\"bSortable\"),L(e,\"orderData\",\"aDataSort\"),L(e,\"orderSequence\",\"asSorting\"),L(e,\"orderDataType\",\"sortDataType\");var t=e.aDataSort;\"number\"!=typeof t||Array.isArray(t)||(e.aDataSort=[t])}function O(t){var n=s.defaults.column,o=t.aoColumns.length,r=e.extend({},s.models.oColumn,n,{aDataSort:n.aDataSort?n.aDataSort:[o],mData:n.mData?n.mData:o,idx:o,searchFixed:{},colEl:e(\"<col>\").attr(\"data-dt-column\",o)});t.aoColumns.push(r);var i=t.aoPreSearchCols;i[o]=e.extend({},s.models.oSearch,i[o])}function P(n,o,r){var i=n.aoColumns[o];if(null!=r){j(r),N(s.defaults.column,r,!0),void 0===r.mDataProp||r.mData||(r.mData=r.mDataProp),r.sType&&(i._sManualType=r.sType),r.className&&!r.sClass&&(r.sClass=r.className);var a=i.sClass;e.extend(i,r),Ke(i,r,\"sWidth\",\"sWidthOrig\"),a!==i.sClass&&(i.sClass=a+\" \"+i.sClass),void 0!==r.iDataSort&&(i.aDataSort=[r.iDataSort]),Ke(i,r,\"aDataSort\")}var l=i.mData,c=te(l);if(i.mRender&&Array.isArray(i.mRender)){var d=i.mRender.slice(),u=d.shift();i.mRender=s.render[u].apply(t,d)}i._render=i.mRender?te(i.mRender):null;var f=function(e){return\"string\"==typeof e&&-1!==e.indexOf(\"@\")};i._bAttrSrc=e.isPlainObject(l)&&(f(l.sort)||f(l.type)||f(l.filter)),i._setter=null,i.fnGetData=function(e,t,n){var o=c(e,t,void 0,n);return i._render&&t?i._render(o,t,e,n):o},i.fnSetData=function(e,t,n){return ne(l)(e,t,n)},\"number\"==typeof l||i._isArrayHost||(n._rowReadObject=!0),n.oFeatures.bSort||(i.bSortable=!1)}function H(r){!function(r){if(!r.oFeatures.bAutoWidth)return;var i,a,l,c=r.nTable,d=r.aoColumns,u=r.oScroll,f=u.sY,h=u.sX,p=u.sXInner,m=q(r,\"bVisible\"),g=c.getAttribute(\"width\"),v=c.parentNode,b=c.style.width,y=He(r);if(y===r.containerWidth)return!1;r.containerWidth=y,b||g||(c.style.width=\"100%\",b=\"100%\");b&&-1!==b.indexOf(\"%\")&&(g=b);nt(r,null,\"column-calc\",{visible:m},!1);var w=e(c.cloneNode()).css(\"visibility\",\"hidden\").css(\"margin\",0).removeAttr(\"id\");w.append(\"<tbody/>\"),w.append(e(r.nTHead).clone()).append(e(r.nTFoot).clone()),w.find(\"tfoot th, tfoot td\").css(\"width\",\"\"),w.find(\"thead th, thead td\").each(function(){var t=X(r,this,!0,!1);t?(this.style.width=t,h&&(this.style.minWidth=t,e(this).append(e(\"<div/>\").css({width:t,margin:0,padding:0,border:0,height:1})))):this.style.width=\"\"});var _=[];for(i=0;i<m.length;i++)_.push(Ee(r,m[i]));if(_.length)for(i=0;i<_[0].length;i++){var x=e(\"<tr/>\").appendTo(w.find(\"tbody\"));for(a=0;a<m.length;a++){l=d[m[a]];var T=_[a][i]||\"\",D=o.type.className[l.sType],C=T+(l.sContentPadding||(h?\"-\":\"\")),S=-1===T.indexOf(\"<\")&&-1===T.indexOf(\"&\")?n.createTextNode(C):C;e(\"<td/>\").addClass(D).addClass(l.sClass).append(S).appendTo(x)}}e(\"[name]\",w).removeAttr(\"name\");var A=e(\"<div/>\").css(h||f?{position:\"absolute\",top:0,left:0,height:1,right:0,overflow:\"hidden\"}:{}).append(w).appendTo(v);h&&p?w.width(p):h?(w.css(\"width\",\"auto\"),w.removeAttr(\"width\"),w.outerWidth()<v.clientWidth&&g&&w.outerWidth(v.clientWidth)):f?w.outerWidth(v.clientWidth):g&&w.outerWidth(g);var R=0,I=w.find(\"tbody tr\").eq(0).children();for(i=0;i<m.length;i++){var k=I[i].getBoundingClientRect().width;R+=k,d[m[i]].sWidth=Me(k)}c.style.width=Me(R),A.remove(),g&&(c.style.width=Me(g));if((g||h)&&!r._reszEvt){var N=s.util.throttle(function(){var e=He(r);r.bDestroying||0===e||H(r)});if(t.ResizeObserver){var L=e(r.nTableWrapper).is(\":visible\"),F=e(\"<div>\").css({width:\"100%\",height:0}).addClass(\"dt-autosize\").appendTo(r.nTableWrapper);r.resizeObserver=new ResizeObserver(function(e){L?L=!1:N()}),r.resizeObserver.observe(F[0])}else e(t).on(\"resize.DT-\"+r.sInstance,N);r._reszEvt=!0}}(r),function(e){for(var t=e.aoColumns,n=0;n<t.length;n++){var o=X(e,[n],!1,!1);t[n].colEl.css(\"width\",o),e.oScroll.sX&&t[n].colEl.css(\"min-width\",o)}}(r);var i=r.oScroll;\"\"===i.sY&&\"\"===i.sX||Pe(r),nt(r,null,\"column-sizing\",[r])}function E(e,t){var n=q(e,\"bVisible\");return\"number\"==typeof n[t]?n[t]:null}function M(e,t){var n=q(e,\"bVisible\").indexOf(t);return-1!==n?n:null}function W(t){var n=t.aoHeader,o=t.aoColumns,r=0;if(n.length)for(var i=0,a=n[0].length;i<a;i++)o[i].bVisible&&\"none\"!==e(n[0][i].cell).css(\"display\")&&r++;return r}function q(e,t){var n=[];return e.aoColumns.map(function(e,o){e[t]&&n.push(o)}),n}function U(e,t){return!0===t?e._name:t}function B(e){var t,n,r,i,a,l,c,d,u,f=e.aoColumns,h=e.aoData,p=s.ext.type.detect;for(t=0,n=f.length;t<n;t++){if(u=[],!(c=f[t]).sType&&c._sManualType)c.sType=c._sManualType;else if(!c.sType){if(!e.typeDetect)return;for(r=0,i=p.length;r<i;r++){var g=p[r],v=g.oneOf,b=g.allOf||g,y=g.init,w=!1;if(d=null,y&&(d=U(g,y(e,c,t)))){c.sType=d;break}for(a=0,l=h.length;a<l;a++)if(h[a]){if(void 0===u[a]&&(u[a]=J(e,a,t,\"type\")),v&&!w&&(w=U(g,v(u[a],e))),!(d=U(g,b(u[a],e)))&&r!==p.length-3)break;if(\"html\"===d&&!m(u[a]))break}if(v&&w&&d||!v&&d){c.sType=d;break}}c.sType||(c.sType=\"string\")}var _=o.type.className[c.sType];_&&(z(e.aoHeader,t,_),z(e.aoFooter,t,_));var x=o.type.render[c.sType];x&&!c._render&&(c._render=s.util.get(x),V(e,t))}}function V(e,t){for(var n=e.aoData,o=0;o<n.length;o++)if(n[o].nTr){var r=J(e,o,t,\"display\");n[o].displayData[t]=r,Q(n[o].anCells[t],r)}}function z(e,t,n){e.forEach(function(e){e[t]&&e[t].unique&&I(e[t].cell,n)})}function X(e,t,n,o){Array.isArray(t)||(t=Y(t));for(var r,i=0,a=e.aoColumns,s=0,l=t.length;s<l;s++){var c=a[t[s]],d=n?c.sWidthOrig:c.sWidth;if(o||!1!==c.bVisible){if(null==d)return null;if(\"number\"==typeof d)r=\"px\",i+=d;else{var u=d.match(/([\\d\\.]+)([^\\d]*)/);u&&(i+=1*u[1],r=3===u.length?u[2]:\"px\")}}}return i+r}function Y(t){var n=e(t).closest(\"[data-dt-column]\").attr(\"data-dt-column\");return n?n.split(\",\").map(function(e){return 1*e}):[]}function $(t,n,o,r){var i=t.aoData.length,a=e.extend(!0,{},s.models.oRow,{src:o?\"dom\":\"data\",idx:i});a._aData=n,t.aoData.push(a);for(var l=t.aoColumns,c=0,d=l.length;c<d;c++)l[c].sType=null;t.aiDisplayMaster.push(i);var u=t.rowIdFn(n);return void 0!==u&&(t.aIds[u]=a),!o&&t.oFeatures.bDeferRender||le(t,i,o,r),i}function Z(t,n){var o;return n instanceof e||(n=e(n)),n.map(function(e,n){return o=ae(t,n),$(t,o.data,n,o.cells)})}function J(e,t,n,o){\"search\"===o?o=\"filter\":\"order\"===o&&(o=\"sort\");var r=e.aoData[t];if(r){var i=e.iDraw,a=e.aoColumns[n],l=r._aData,c=a.sDefaultContent,d=a.fnGetData(l,o,{settings:e,row:t,col:n});if(\"display\"!==o&&d&&\"object\"==typeof d&&d.nodeName&&(d=d.innerHTML),void 0===d)return e.iDrawError!=i&&null===c&&(Qe(e,0,\"Requested unknown parameter \"+(\"function\"==typeof a.mData?\"{function}\":\"'\"+a.mData+\"'\")+\" for row \"+t+\", column \"+n,4),e.iDrawError=i),c;if(d!==l&&null!==d||null===c||void 0===o){if(\"function\"==typeof d)return d.call(l)}else d=c;if(null===d&&\"display\"===o)return\"\";if(\"filter\"===o){var u=s.ext.type.search;u[a.sType]&&(d=u[a.sType](d))}return d}}function Q(t,n){n&&\"object\"==typeof n&&n.nodeName?e(t).empty().append(n):t.innerHTML=n}var K=/\\[.*?\\]$/,G=/\\(\\)$/;function ee(e){return(e.match(/(\\\\.|[^.])+/g)||[\"\"]).map(function(e){return e.replace(/\\\\\\./g,\".\")})}var te=s.util.get,ne=s.util.set;function oe(e){return w(e.aoData,\"_aData\")}function re(e){e.aoData.length=0,e.aiDisplayMaster.length=0,e.aiDisplay.length=0,e.aIds={}}function ie(e,t,n,o){var r,i,a=e.aoData[t];if(a._aSortData=null,a._aFilterData=null,a.displayData=null,\"dom\"!==n&&(n&&\"auto\"!==n||\"dom\"!==a.src)){var s=a.anCells,l=se(e,t);if(s)if(void 0!==o)Q(s[o],l[o]);else for(r=0,i=s.length;r<i;r++)Q(s[r],l[r])}else a._aData=ae(e,a,o,void 0===o?void 0:a._aData).data;var c=e.aoColumns;if(void 0!==o)c[o].sType=null,c[o].wideStrings=null;else{for(r=0,i=c.length;r<i;r++)c[r].sType=null,c[r].wideStrings=null;ce(e,a)}}function ae(e,t,n,o){var r,i,a,s=[],l=t.firstChild,c=0,d=e.aoColumns,u=e._rowReadObject;o=void 0!==o?o:u?{}:[];var f=function(e,t){if(\"string\"==typeof e){var n=e.indexOf(\"@\");if(-1!==n){var r=e.substring(n+1);ne(e)(o,t.getAttribute(r))}}},h=function(e){void 0!==n&&n!==c||(i=d[c],a=e.innerHTML.trim(),i&&i._bAttrSrc?(ne(i.mData._)(o,a),f(i.mData.sort,e),f(i.mData.type,e),f(i.mData.filter,e)):u?(i._setter||(i._setter=ne(i.mData)),i._setter(o,a)):o[c]=a);c++};if(l)for(;l;)\"TD\"!=(r=l.nodeName.toUpperCase())&&\"TH\"!=r||(h(l),s.push(l)),l=l.nextSibling;else for(var p=0,m=(s=t.anCells).length;p<m;p++)h(s[p]);var g=t.firstChild?t:t.nTr;if(g){var v=g.getAttribute(\"id\");v&&ne(e.rowId)(o,v)}return{data:o,cells:s}}function se(e,t){var n=e.aoData[t],o=e.aoColumns;if(!n.displayData){n.displayData=[];for(var r=0,i=o.length;r<i;r++)n.displayData.push(J(e,t,r,\"display\"))}return n.displayData}function le(t,o,r,i){var a,s,l,c,d,u,f=t.aoData[o],h=f._aData,p=[],m=t.oClasses.tbody.row;if(null===f.nTr){for(a=r||n.createElement(\"tr\"),f.nTr=a,f.anCells=p,I(a,m),a._DT_RowIndex=o,ce(t,f),c=0,d=t.aoColumns.length;c<d;c++){l=t.aoColumns[c],(s=(u=!r||!i[c])?n.createElement(l.sCellType):i[c])||Qe(t,0,\"Incorrect column count\",18),s._DT_CellIndex={row:o,column:c},p.push(s);var g=se(t,o);!u&&(!l.mRender&&l.mData===c||e.isPlainObject(l.mData)&&l.mData._===c+\".display\")||Q(s,g[c]),I(s,l.sClass),l.bVisible&&u?a.appendChild(s):l.bVisible||u||s.parentNode.removeChild(s),l.fnCreatedCell&&l.fnCreatedCell.call(t.oInstance,s,J(t,o,c),h,o,c)}nt(t,\"aoRowCreatedCallback\",\"row-created\",[a,h,o,p])}else I(f.nTr,m)}function ce(t,n){var o=n.nTr,r=n._aData;if(o){var i=t.rowIdFn(r);if(i&&(o.id=i),r.DT_RowClass){var a=r.DT_RowClass.split(\" \");n.__rowc=n.__rowc?A(n.__rowc.concat(a)):a,e(o).removeClass(n.__rowc.join(\" \")).addClass(r.DT_RowClass)}r.DT_RowAttr&&e(o).attr(r.DT_RowAttr),r.DT_RowData&&e(o).data(r.DT_RowData)}}function de(t,n){var o,r,i,a=t.oClasses,s=t.aoColumns,l=\"header\"===n?t.nTHead:t.nTFoot,c=\"header\"===n?\"sTitle\":n;if(l){if((\"header\"===n||w(t.aoColumns,c).join(\"\"))&&((i=e(\"tr\",l)).length||(i=e(\"<tr/>\").appendTo(l)),1===i.length)){var d=0;for(e(\"td, th\",i).each(function(){d+=this.colSpan}),o=d,r=s.length;o<r;o++)e(\"<th/>\").html(s[o][c]||\"\").appendTo(i)}var u=we(t,l,!0);\"header\"===n?(t.aoHeader=u,e(\"tr\",l).addClass(a.thead.row)):(t.aoFooter=u,e(\"tr\",l).addClass(a.tfoot.row)),e(l).children(\"tr\").children(\"th, td\").each(function(){rt(t,n)(t,e(this),a)})}}function ue(t,n,o){var r,i,a,s,l,c=[],d=[],u=t.aoColumns,f=u.length;if(n){for(o||(o=x(f).filter(function(e){return u[e].bVisible})),r=0;r<n.length;r++)c[r]=n[r].slice().filter(function(e,t){return o.includes(t)}),d.push([]);for(r=0;r<c.length;r++)for(i=0;i<c[r].length;i++)if(s=1,l=1,void 0===d[r][i]){for(a=c[r][i].cell;void 0!==c[r+s]&&c[r][i].cell==c[r+s][i].cell;)d[r+s][i]=null,s++;for(;void 0!==c[r][i+l]&&c[r][i].cell==c[r][i+l].cell;){for(var h=0;h<s;h++)d[r+h][i+l]=null;l++}var p=e(\".dt-column-title\",a);d[r][i]={cell:a,colspan:l,rowspan:s,title:p.length?p.html():e(a).html()}}return d}}function fe(t,n){for(var o,r,i=ue(t,n),a=0;a<n.length;a++){if(o=n[a].row)for(;r=o.firstChild;)o.removeChild(r);for(var s=0;s<i[a].length;s++){var l=i[a][s];l&&e(l.cell).appendTo(o).attr(\"rowspan\",l.rowspan).attr(\"colspan\",l.colspan)}}}function he(t,n){if(function(e){var t=\"ssp\"==it(e),n=e.iInitDisplayStart;void 0!==n&&-1!==n&&(e._iDisplayStart=t?n:n>=e.fnRecordsDisplay()?0:n,e.iInitDisplayStart=-1)}(t),-1===nt(t,\"aoPreDrawCallback\",\"preDraw\",[t]).indexOf(!1)){var r,i=[],a=0,s=\"ssp\"==it(t),l=t.aiDisplay,c=t._iDisplayStart,d=t.fnDisplayEnd(),u=t.aoColumns,f=e(t.nTBody);if(t.bDrawing=!0,t.deferLoading)t.deferLoading=!1,t.iDraw++,Fe(t,!1);else if(s){if(!t.bDestroying&&!n)return 0===t.iDraw&&f.empty().append(me(t)),(r=t).iDraw++,Fe(r,!0),void _e(r,function(e){var t=e.aoColumns,n=e.oFeatures,o=e.oPreviousSearch,r=e.aoPreSearchCols,i=function(e,n){return\"function\"==typeof t[e][n]?\"function\":t[e][n]};return{draw:e.iDraw,columns:t.map(function(e,t){return{data:i(t,\"mData\"),name:e.sName,searchable:e.bSearchable,orderable:e.bSortable,search:{value:r[t].search,regex:r[t].regex,fixed:Object.keys(e.searchFixed).map(function(t){return{name:t,term:\"function\"!=typeof e.searchFixed[t]?e.searchFixed[t].toString():\"function\"}})}}}),order:Ve(e).map(function(e){return{column:e.col,dir:e.dir,name:i(e.col,\"sName\")}}),start:e._iDisplayStart,length:n.bPaginate?e._iDisplayLength:-1,search:{value:o.search,regex:o.regex,fixed:Object.keys(e.searchFixed).map(function(t){return{name:t,term:\"function\"!=typeof e.searchFixed[t]?e.searchFixed[t].toString():\"function\"}})}}}(r),function(e){!function(e,t){var n=xe(e,t),o=Te(e,\"draw\",t),r=Te(e,\"recordsTotal\",t),i=Te(e,\"recordsFiltered\",t);if(void 0!==o){if(1*o<e.iDraw)return;e.iDraw=1*o}n||(n=[]),re(e),e._iRecordsTotal=parseInt(r,10),e._iRecordsDisplay=parseInt(i,10);for(var a=0,s=n.length;a<s;a++)$(e,n[a]);e.aiDisplay=e.aiDisplayMaster.slice(),B(e),he(e,!0),ke(e),Fe(e,!1)}(r,e)})}else t.iDraw++;if(0!==l.length)for(var h=s?0:c,p=s?t.aoData.length:d,m=h;m<p;m++){var g=l[m],v=t.aoData[g];if(null!==v){null===v.nTr&&le(t,g);for(var b=v.nTr,y=0;y<u.length;y++){var w=u[y],_=v.anCells[y];I(_,o.type.className[w.sType]),I(_,t.oClasses.tbody.cell)}nt(t,\"aoRowCallback\",null,[b,v._aData,a,m,g]),i.push(b),a++}}else i[0]=me(t);nt(t,\"aoHeaderCallback\",\"header\",[e(t.nTHead).children(\"tr\")[0],oe(t),c,d,l]),nt(t,\"aoFooterCallback\",\"footer\",[e(t.nTFoot).children(\"tr\")[0],oe(t),c,d,l]),f[0].replaceChildren?f[0].replaceChildren.apply(f[0],i):(f.children().detach(),f.append(e(i))),e(t.nTableWrapper).toggleClass(\"dt-empty-footer\",0===e(\"tr\",t.nTFoot).length),nt(t,\"aoDrawCallback\",\"draw\",[t],!0),t.bSorted=!1,t.bFiltered=!1,t.bDrawing=!1}else Fe(t,!1)}function pe(e,t,n){var o=e.oFeatures,r=o.bSort,i=o.bFilter;void 0!==n&&!0!==n||(B(e),r&&ze(e),i?De(e,e.oPreviousSearch):e.aiDisplay=e.aiDisplayMaster.slice()),!0!==t&&(e._iDisplayStart=0),e._drawHold=t,he(e),e.api.one(\"draw\",function(){e._drawHold=!1})}function me(t){var n=t.oLanguage,o=n.sZeroRecords,r=it(t);return\"ssp\"!==r&&\"ajax\"!==r||t.json?n.sEmptyTable&&0===t.fnRecordsTotal()&&(o=n.sEmptyTable):o=n.sLoadingRecords,e(\"<tr/>\").append(e(\"<td />\",{colSpan:W(t),class:t.oClasses.empty.row}).html(o))[0]}function ge(t,n,o){if(Array.isArray(o))for(var r=0;r<o.length;r++)ge(t,n,o[r]);else{var i=t[n];e.isPlainObject(o)?o.features?(o.rowId&&(t.id=o.rowId),o.rowClass&&(t.className=o.rowClass),i.id=o.id,i.className=o.className,ge(t,n,o.features)):Object.keys(o).map(function(e){i.contents.push({feature:e,opts:o[e]})}):i.contents.push(o)}}function ve(t,n,o){var r=[];e.each(n,function(e,t){if(null!==t){var n=e.match(/^([a-z]+)([0-9]*)([A-Za-z]*)$/),i=n[2]?1*n[2]:0,a=n[3]?n[3].toLowerCase():\"full\";if(n[1]===o){var s=function(e,t,n){for(var o,r=0;r<e.length;r++)if((o=e[r]).rowNum===t&&(\"full\"===n&&o.full||(\"start\"===n||\"end\"===n)&&(o.start||o.end)))return o[n]||(o[n]={contents:[]}),o;return(o={rowNum:t})[n]={contents:[]},e.push(o),o}(r,i,a);ge(s,a,t)}}}),r.sort(function(e,t){var n=e.rowNum,r=t.rowNum;if(n===r){var i=e.full&&!t.full?-1:1;return\"bottom\"===o?-1*i:i}return r-n}),\"bottom\"===o&&r.reverse();for(var i=0;i<r.length;i++)delete r[i].rowNum,be(t,r[i]);return r}function be(t,n){var r=function(e,n){return o.features[e]||Qe(t,0,\"Unknown feature: \"+e),o.features[e].apply(this,[t,n])},i=function(o){if(n[o])for(var i=n[o].contents,a=0,s=i.length;a<s;a++)if(i[a])if(\"string\"==typeof i[a])i[a]=r(i[a],null);else if(e.isPlainObject(i[a]))i[a]=r(i[a].feature,i[a].opts);else if(\"function\"==typeof i[a].node)i[a]=i[a].node(t);else if(\"function\"==typeof i[a]){var l=i[a](t);i[a]=\"function\"==typeof l.node?l.node():l}};i(\"start\"),i(\"end\"),i(\"full\")}function ye(t){var n=t.oClasses,o=e(t.nTable),r=e(\"<div/>\").attr({id:t.sTableId+\"_wrapper\",class:n.container}).insertBefore(o);if(t.nTableWrapper=r[0],t.sDom)!function(t,n,o){for(var r,i,a,l,c,d=n.match(/(\".*?\")|('.*?')|./g),u=0;u<d.length;u++){if(r=null,\"<\"==(i=d[u])){if(a=e(\"<div/>\"),\"'\"==(l=d[u+1])[0]||'\"'==l[0]){var f,h=\"\";if(-1!=(c=l.replace(/['\"]/g,\"\")).indexOf(\".\")){var p=c.split(\".\");h=p[0],f=p[1]}else\"#\"==c[0]?h=c:f=c;a.attr(\"id\",h.substring(1)).addClass(f),u++}o.append(a),o=a}else\">\"==i?o=o.parent():\"t\"==i?r=Oe(t):s.ext.feature.forEach(function(e){i==e.cFeature&&(r=e.fnInit(t))});r&&o.append(r)}}(t,t.sDom,r);else{var i=ve(t,t.layout,\"top\"),a=ve(t,t.layout,\"bottom\"),l=rt(t,\"layout\");i.forEach(function(e){l(t,r,e)}),l(t,r,{full:{table:!0,contents:[Oe(t)]}}),a.forEach(function(e){l(t,r,e)})}!function(t){var n=t.nTable,o=\"\"!==t.oScroll.sX||\"\"!==t.oScroll.sY;if(t.oFeatures.bProcessing){var r=e(\"<div/>\",{id:t.sTableId+\"_processing\",class:t.oClasses.processing.container,role:\"status\"}).html(t.oLanguage.sProcessing).append(\"<div><div></div><div></div><div></div><div></div></div>\");o?r.prependTo(e(\"div.dt-scroll\",t.nTableWrapper)):r.insertBefore(n),e(n).on(\"processing.dt.DT\",function(e,t,n){r.css(\"display\",n?\"block\":\"none\")})}}(t)}function we(t,o,r){var i,a,s,l,c,d,u,f,h,p,m=t.aoColumns,g=e(o).children(\"tr\"),v=t.titleRow,b=o&&\"thead\"===o.nodeName.toLowerCase(),y=[],w=function(e,t,n){for(var o=e[t];o[n];)n++;return n};for(s=0,d=g.length;s<d;s++)y.push([]);for(s=0,d=g.length;s<d;s++)for(a=(i=g[s]).firstChild;a;){if(\"TD\"==a.nodeName.toUpperCase()||\"TH\"==a.nodeName.toUpperCase()){var _=[],x=e(a);if(f=(f=1*a.getAttribute(\"colspan\"))&&0!==f&&1!==f?f:1,h=(h=1*a.getAttribute(\"rowspan\"))&&0!==h&&1!==h?h:1,u=w(y,s,0),p=1===f,r){if(p){P(t,u,ct(x.data()));var T=m[u],C=a.getAttribute(\"width\")||null,S=a.style.width.match(/width:\\s*(\\d+[pxem%]+)/);S&&(C=S[1]),T.sWidthOrig=T.sWidth||C,b?(null===T.sTitle||T.autoTitle||(!0===v&&0===s||!1===v&&s===g.length-1||v===s||null===v)&&(a.innerHTML=T.sTitle),!T.sTitle&&p&&(T.sTitle=D(a.innerHTML),T.autoTitle=!0)):T.footer&&(a.innerHTML=T.footer),T.ariaTitle||(T.ariaTitle=x.attr(\"aria-label\")||T.sTitle),T.className&&x.addClass(T.className)}0===e(\".dt-column-title\",a).length&&e(n.createElement(t.columnTitleTag)).addClass(\"dt-column-title\").append(a.childNodes).appendTo(a),t.orderIndicators&&b&&0!==x.filter(\":not([data-dt-order=disable])\").length&&0!==x.parent(\":not([data-dt-order=disable])\").length&&0===e(\".dt-column-order\",a).length&&e(n.createElement(t.columnTitleTag)).addClass(\"dt-column-order\").appendTo(a);var R=b?\"header\":\"footer\";0===e(\"div.dt-column-\"+R,a).length&&e(\"<div>\").addClass(\"dt-column-\"+R).append(a.childNodes).appendTo(a)}for(c=0;c<f;c++){for(l=0;l<h;l++)y[s+l][u+c]={cell:a,unique:p},y[s+l].row=i;_.push(u+c)}a.setAttribute(\"data-dt-column\",A(_).join(\",\"))}a=a.nextSibling}return y}function _e(t,n,o){var r,i=t.ajax,a=t.oInstance,s=function(e){var n=t.jqXHR?t.jqXHR.status:null;(null===e||\"number\"==typeof n&&204==n)&&xe(t,e={},[]);var r=e.error||e.sError;if(r&&Qe(t,0,r),e.d&&\"string\"==typeof e.d)try{e=JSON.parse(e.d)}catch(e){}t.json=e,nt(t,null,\"xhr\",[t,e,t.jqXHR],!0),o(e)};if(e.isPlainObject(i)&&i.data){var l=\"function\"==typeof(r=i.data)?r(n,t):r;n=\"function\"==typeof r&&l?l:e.extend(!0,n,l),delete i.data}var c={url:\"string\"==typeof i?i:\"\",data:n,success:s,dataType:\"json\",cache:!1,type:t.sServerMethod,error:function(e,n){-1===nt(t,null,\"xhr\",[t,null,t.jqXHR],!0).indexOf(!0)&&(\"parsererror\"==n?Qe(t,0,\"Invalid JSON response\",1):4===e.readyState&&Qe(t,0,\"Ajax error\",7)),Fe(t,!1)}};if(e.isPlainObject(i)&&e.extend(c,i),t.oAjaxData=n,nt(t,null,\"preXhr\",[t,n,c],!0),\"json\"===c.submitAs&&\"object\"==typeof n&&(c.data=JSON.stringify(n),c.contentType||(c.contentType=\"application/json; charset=utf-8\")),\"function\"==typeof i)t.jqXHR=i.call(a,n,s,t);else if(\"\"===i.url){var d={};xe(t,d,[]),s(d)}else t.jqXHR=e.ajax(c);r&&(i.data=r)}function xe(t,n,o){var r=\"data\";if(e.isPlainObject(t.ajax)&&void 0!==t.ajax.dataSrc){var i=t.ajax.dataSrc;\"string\"==typeof i||\"function\"==typeof i?r=i:void 0!==i.data&&(r=i.data)}if(!o)return\"data\"===r?n.aaData||n[r]:\"\"!==r?te(r)(n):n;ne(r)(n,o)}function Te(t,n,o){var r=e.isPlainObject(t.ajax)?t.ajax.dataSrc:null;if(r&&r[n])return te(r[n])(o);var i=\"\";return\"draw\"===n?i=\"sEcho\":\"recordsTotal\"===n?i=\"iTotalRecords\":\"recordsFiltered\"===n&&(i=\"iTotalDisplayRecords\"),void 0!==o[i]?o[i]:o[n]}function De(t,n){var o=t.aoPreSearchCols;if(\"ssp\"!=it(t)){!function(e){for(var t,n,o,r,i,a=e.aoColumns,s=e.aoData,l=!1,c=0;c<s.length;c++)if(s[c]&&!(i=s[c])._aFilterData){for(o=[],t=0,n=a.length;t<n;t++)a[t].bSearchable?(null===(r=J(e,c,t,\"filter\"))&&(r=\"\"),\"string\"!=typeof r&&r.toString&&(r=r.toString())):r=\"\",r.indexOf&&-1!==r.indexOf(\"&\")&&(Ae.innerHTML=r,r=Re?Ae.textContent:Ae.innerText),r.replace&&(r=r.replace(/[\\r\\n\\u2028]/g,\"\")),o.push(r);i._aFilterData=o,i._sFilterRow=o.join(\"  \"),l=!0}}(t),t.aiDisplay=t.aiDisplayMaster.slice(),Ce(t.aiDisplay,t,n.search,n),e.each(t.searchFixed,function(e,n){Ce(t.aiDisplay,t,n,{})});for(var r=0;r<o.length;r++){var i=o[r];Ce(t.aiDisplay,t,i.search,i,r),e.each(t.aoColumns[r].searchFixed,function(e,n){Ce(t.aiDisplay,t,n,{},r)})}!function(e){for(var t,n,o=s.ext.search,r=e.aiDisplay,i=0,a=o.length;i<a;i++){for(var l=[],c=0,d=r.length;c<d;c++)n=r[c],t=e.aoData[n],o[i](e,t._aFilterData,n,t._aData,c)&&l.push(n);r.length=0,st(r,l)}}(t)}t.bFiltered=!0,nt(t,null,\"search\",[t])}function Ce(t,n,o,r,i){if(\"\"!==o){var a=0,s=[],l=\"function\"==typeof o?o:null,c=o instanceof RegExp?o:l?null:function(t,n){var o=[],r=e.extend({},{boundary:!1,caseInsensitive:!0,exact:!1,regex:!1,smart:!0},n);\"string\"!=typeof t&&(t=t.toString());if(t=S(t),r.exact)return new RegExp(\"^\"+Se(t)+\"$\",r.caseInsensitive?\"i\":\"\");if(t=r.regex?t:Se(t),r.smart){var i=(t.match(/!?[\"\\u201C][^\"\\u201D]+[\"\\u201D]|[^ ]+/g)||[\"\"]).map(function(e){var t,n=!1;return\"!\"===e.charAt(0)&&(n=!0,e=e.substring(1)),'\"'===e.charAt(0)?(t=e.match(/^\"(.*)\"$/),e=t?t[1]:e):\"“\"===e.charAt(0)&&(t=e.match(/^\\u201C(.*)\\u201D$/),e=t?t[1]:e),n&&(e.length>1&&o.push(\"(?!\"+e+\")\"),e=\"\"),e.replace(/\"/g,\"\")}),a=o.length?o.join(\"\"):\"\",s=r.boundary?\"\\\\b\":\"\";t=\"^(?=.*?\"+s+i.join(\")(?=.*?\"+s)+\")(\"+a+\".)*$\"}return new RegExp(t,r.caseInsensitive?\"i\":\"\")}(o,r);for(a=0;a<t.length;a++){var d=n.aoData[t[a]],u=void 0===i?d._sFilterRow:d._aFilterData[i];(l&&l(u,d._aData,t[a],i)||c&&c.test(u))&&s.push(t[a])}for(t.length=s.length,a=0;a<s.length;a++)t[a]=s[a]}}var Se=s.util.escapeRegex,Ae=e(\"<div>\")[0],Re=void 0!==Ae.textContent;function Ie(t){var n,o=t.oInit,r=t.deferLoading,i=it(t);t.bInitialised?(de(t,\"header\"),de(t,\"footer\"),function(e,t,n){if(!e.oFeatures.bStateSave)return void n();var o=function(t){Je(e,t,n)},r=e.fnStateLoadCallback.call(e.oInstance,e,o);void 0!==r&&Je(e,r,n)}(t,0,function(){fe(t,t.aoHeader),fe(t,t.aoFooter);var a=t.iInitDisplayStart;if(o.aaData)for(n=0;n<o.aaData.length;n++)$(t,o.aaData[n]);else(r||\"dom\"==i)&&Z(t,e(t.nTBody).children(\"tr\"));t.aiDisplay=t.aiDisplayMaster.slice(),ye(t),function(e){var t=e.nTHead,n=t.querySelectorAll(\"tr\"),o=e.titleRow,r=':not([data-dt-order=\"disable\"]):not([data-dt-order=\"icon-only\"])';!0===o?t=n[0]:!1===o?t=n[n.length-1]:null!==o&&(t=n[o]);e.orderHandler&&qe(e,t,t===e.nTHead?\"tr\"+r+\" th\"+r+\", tr\"+r+\" td\"+r:\"th\"+r+\", td\"+r);var i=[];Be(e,i,e.aaSorting),e.aaSorting=i}(t),We(t),Fe(t,!0),nt(t,null,\"preInit\",[t],!0),pe(t),(\"ssp\"!=i||r)&&(\"ajax\"==i?_e(t,{},function(e){var o=xe(t,e);for(n=0;n<o.length;n++)$(t,o[n]);t.iInitDisplayStart=a,pe(t),Fe(t,!1),ke(t)}):(ke(t),Fe(t,!1)))})):setTimeout(function(){Ie(t)},200)}function ke(e){if(!e._bInitComplete){var t=[e,e.json];e._bInitComplete=!0,H(e),nt(e,null,\"plugin-init\",t,!0),nt(e,\"aoInitComplete\",\"init\",t,!0)}}function Ne(e,t){var n=parseInt(t,10);e._iDisplayLength=n,ot(e),nt(e,null,\"length\",[e,n])}function Le(e,t,n){var o=e._iDisplayStart,r=e._iDisplayLength,i=e.fnRecordsDisplay();if(0===i||-1===r)o=0;else if(\"number\"==typeof t)(o=t*r)>i&&(o=0);else if(\"first\"==t)o=0;else if(\"previous\"==t)(o=r>=0?o-r:0)<0&&(o=0);else if(\"next\"==t)o+r<i&&(o+=r);else if(\"last\"==t)o=Math.floor((i-1)/r)*r;else{if(\"ellipsis\"===t)return;Qe(e,0,\"Unknown paging action: \"+t,5)}var a=e._iDisplayStart!==o;return e._iDisplayStart=o,nt(e,null,a?\"page\":\"page-nc\",[e]),a&&n&&he(e),a}function Fe(e,t){e.bDrawing&&!1===t||nt(e,null,\"processing\",[e,t])}function je(e,t,n){t?(Fe(e,!0),setTimeout(function(){n(),Fe(e,!1)},0)):n()}function Oe(t){var n=e(t.nTable),o=t.oScroll;if(\"\"===o.sX&&\"\"===o.sY)return t.nTable;var r=o.sX,i=o.sY,a=t.oClasses.scrolling,s=t.captionNode,l=s?s._captionSide:null,c=e(n[0].cloneNode(!1)),d=e(n[0].cloneNode(!1)),u=n.children(\"tfoot\"),f=\"<div/>\",h=function(e){return e?Me(e):null};u.length||(u=null);var p=e(f,{class:a.container}).append(e(f,{class:a.header.self}).css({overflow:\"hidden\",position:\"relative\",border:0,width:r?h(r):\"100%\"}).append(e(f,{class:a.header.inner}).css({\"box-sizing\":\"content-box\",width:o.sXInner||\"100%\"}).append(c.removeAttr(\"id\").css(\"margin-left\",0).append(\"top\"===l?s:null).append(n.children(\"thead\"))))).append(e(f,{class:a.body}).css({position:\"relative\",overflow:\"auto\",width:h(r)}).append(n));u&&p.append(e(f,{class:a.footer.self}).css({overflow:\"hidden\",border:0,width:r?h(r):\"100%\"}).append(e(f,{class:a.footer.inner}).append(d.removeAttr(\"id\").css(\"margin-left\",0).append(\"bottom\"===l?s:null).append(n.children(\"tfoot\")))));var m=p.children(),g=m[0],v=m[1],b=u?m[2]:null;return e(v).on(\"scroll.DT\",function(){var e=this.scrollLeft;g.scrollLeft=e,u&&(b.scrollLeft=e)}),e(\"th, td\",g).on(\"focus\",function(){var e=g.scrollLeft;v.scrollLeft=e,u&&(v.scrollLeft=e)}),e(v).css(\"max-height\",i),o.bCollapse||e(v).css(\"height\",i),t.nScrollHead=g,t.nScrollBody=v,t.nScrollFoot=b,t.aoDrawCallback.push(Pe),p[0]}function Pe(t){var n,o,r=t.oScroll,i=r.iBarWidth,a=e(t.nScrollHead).children(\"div\"),s=a.children(\"table\"),l=t.nScrollBody,c=e(l),d=e(t.nScrollFoot).children(\"div\"),u=d.children(\"table\"),f=e(t.nTHead),h=e(t.nTable),p=t.nTFoot&&e(\"th, td\",t.nTFoot).length?e(t.nTFoot):null,m=t.oBrowser,g=l.scrollHeight>l.clientHeight;if(t.scrollBarVis!==g&&void 0!==t.scrollBarVis)return t.scrollBarVis=g,void H(t);if(t.scrollBarVis=g,h.children(\"thead, tfoot\").remove(),(n=f.clone().prependTo(h)).find(\"th, td\").removeAttr(\"tabindex\"),n.find(\"[id]\").removeAttr(\"id\"),p&&(o=p.clone().prependTo(h)).find(\"[id]\").removeAttr(\"id\"),t.aiDisplay.length){var v=null,b=\"ssp\"!==it(t)?t._iDisplayStart:0;for(x=b;x<b+t.aiDisplay.length;x++){var y=t.aiDisplay[x],w=t.aoData[y].nTr;if(w){v=w;break}}if(v)for(var _=e(v).children(\"th, td\").map(function(n){return{idx:E(t,n),width:e(this).outerWidth()}}),x=0;x<_.length;x++){var T=t.aoColumns[_[x].idx].colEl[0];T.style.width.replace(\"px\",\"\")!==_[x].width&&(T.style.width=_[x].width+\"px\",r.sX&&(T.style.minWidth=_[x].width+\"px\"))}}s.find(\"colgroup\").remove(),s.append(t.colgroup.clone()),p&&(u.find(\"colgroup\").remove(),u.append(t.colgroup.clone())),e(\"th, td\",n).each(function(){e(this.childNodes).wrapAll('<div class=\"dt-scroll-sizing\">')}),p&&e(\"th, td\",o).each(function(){e(this.childNodes).wrapAll('<div class=\"dt-scroll-sizing\">')});var D=Math.floor(h.height())>l.clientHeight||\"scroll\"==c.css(\"overflow-y\"),C=\"padding\"+(m.bScrollbarLeft?\"Left\":\"Right\"),S=h.outerWidth();s.css(\"width\",Me(S)),a.css(\"width\",Me(S)).css(C,D?i+\"px\":\"0px\"),p&&(u.css(\"width\",Me(S)),d.css(\"width\",Me(S)).css(C,D?i+\"px\":\"0px\")),h.children(\"colgroup\").prependTo(h),c.trigger(\"scroll\"),!t.bSorted&&!t.bFiltered||t._drawHold||(l.scrollTop=0)}function He(t){return e(t.nTableWrapper).is(\":visible\")?e(t.nTableWrapper).width():0}function Ee(e,t){var n=e.aoColumns[t];if(!n.wideStrings){for(var o=[],r=[],i=0,a=e.aiDisplayMaster.length;i<a;i++){var s=se(e,e.aiDisplayMaster[i])[t],l=s&&\"object\"==typeof s&&s.nodeType?s.innerHTML:s+\"\";l=(l=l.replace(/id=\".*?\"/g,\"\").replace(/name=\".*?\"/g,\"\")).replace(/<script.*?<\\/script>/gi,\" \");var c=D(l,\" \").replace(/&nbsp;/g,\" \");r.push({str:l,len:c.length}),o.push(c)}r.sort(function(e,t){return t.len-e.len}).splice(3),n.wideStrings=r.map(function(e){return e.str});let d=o.join(\" \").split(\" \");d.sort(function(e,t){return t.length-e.length}),d.length&&n.wideStrings.push(d[0]),d.length>1&&n.wideStrings.push(d[1]),d.length>2&&n.wideStrings.push(d[3])}return n.wideStrings}function Me(e){return null===e?\"0px\":\"number\"==typeof e?e<0?\"0px\":e+\"px\":e.match(/\\d$/)?e+\"px\":e}function We(e){var t=e.aoColumns;for(e.colgroup.empty(),Vt=0;Vt<t.length;Vt++)t[Vt].bVisible&&e.colgroup.append(t[Vt].colEl)}function qe(e,t,n,o,r){et(t,n,function(t){var n=!1,i=void 0===o?Y(t.target):\"function\"==typeof o?o():Array.isArray(o)?o:[o];if(i.length){for(var a=0,s=i.length;a<s;a++){if(!1!==Xe(e,i[a],a,t.shiftKey)&&(n=!0),1===e.aaSorting.length&&\"\"===e.aaSorting[0][1])break}n&&je(e,!0,function(){ze(e),Ue(e,e.aiDisplay),pe(e,!1,!1),r&&r()})}})}function Ue(e,t){if(!(t.length<2)){var n,o=e.aiDisplayMaster,r={},i={};for(n=0;n<o.length;n++)r[o[n]]=n;for(n=0;n<t.length;n++)i[t[n]]=r[t[n]];t.sort(function(e,t){return i[e]-i[t]})}}function Be(t,n,o){var r=function(o){if(e.isPlainObject(o)){if(void 0!==o.idx)n.push([o.idx,o.dir]);else if(o.name){var r=w(t.aoColumns,\"sName\").indexOf(o.name);-1!==r&&n.push([r,o.dir])}}else n.push(o)};if(e.isPlainObject(o))r(o);else if(o.length&&\"number\"==typeof o[0])r(o);else if(o.length)for(var i=0;i<o.length;i++)r(o[i])}function Ve(t){var n,o,r,i,a,l,c,d=[],u=s.ext.type.order,f=t.aoColumns,h=t.aaSortingFixed,p=e.isPlainObject(h),m=[];if(!t.oFeatures.bSort)return d;for(Array.isArray(h)&&Be(t,m,h),p&&h.pre&&Be(t,m,h.pre),Be(t,m,t.aaSorting),p&&h.post&&Be(t,m,h.post),n=0;n<m.length;n++)if(f[c=m[n][0]])for(o=0,r=(i=f[c].aDataSort).length;o<r;o++)l=f[a=i[o]].sType||\"string\",void 0===m[n]._idx&&(m[n]._idx=f[a].asSorting.indexOf(m[n][1])),m[n][1]&&d.push({src:c,col:a,dir:m[n][1],index:m[n]._idx,type:l,formatter:u[l+\"-pre\"],sorter:u[l+\"-\"+m[n][1]]});return d}function ze(e,t,n){var o,r,i,a=[],l=s.ext.type.order,c=e.aoData,d=e.aiDisplayMaster;if(B(e),void 0!==t){var u=e.aoColumns[t];i=[{src:t,col:t,dir:n,index:0,type:u.sType,formatter:l[u.sType+\"-pre\"],sorter:l[u.sType+\"-\"+n]}],d=d.slice()}else i=Ve(e);for(o=0,r=i.length;o<r;o++)$e(e,i[o].col);if(\"ssp\"!=it(e)&&0!==i.length){for(o=0,r=d.length;o<r;o++)a[o]=o;i.length&&\"desc\"===i[0].dir&&e.orderDescReverse&&a.reverse(),d.sort(function(e,t){var n,o,r,s,l,d=i.length,u=c[e]._aSortData,f=c[t]._aSortData;for(r=0;r<d;r++)if(n=u[(l=i[r]).col],o=f[l.col],l.sorter){if(0!==(s=l.sorter(n,o)))return s}else if(0!==(s=n<o?-1:n>o?1:0))return\"asc\"===l.dir?s:-s;return(n=a[e])<(o=a[t])?-1:n>o?1:0})}else 0===i.length&&d.sort(function(e,t){return e<t?-1:e>t?1:0});return void 0===t&&(e.bSorted=!0,e.sortDetails=i,nt(e,null,\"order\",[e,i])),d}function Xe(e,t,n,o){var r,i=e.aoColumns[t],a=e.aaSorting,s=i.asSorting,l=function(e,t){var n=e._idx;return void 0===n&&(n=s.indexOf(e[1])),n+1<s.length?n+1:t?null:0};if(!i.bSortable)return!1;if(\"number\"==typeof a[0]&&(a=e.aaSorting=[a]),(o||n)&&e.oFeatures.bSortMulti){var c=w(a,\"0\").indexOf(t);-1!==c?(null===(r=l(a[c],!0))&&1===a.length&&(r=0),null===r||\"\"===s[r]?a.splice(c,1):(a[c][1]=s[r],a[c]._idx=r)):o?(a.push([t,s[0],0]),a[a.length-1]._idx=0):(a.push([t,a[0][1],0]),a[a.length-1]._idx=0)}else a.length&&a[0][0]==t?(r=l(a[0]),a.length=1,a[0][1]=s[r],a[0]._idx=r):(a.length=0,a.push([t,s[0]]),a[0]._idx=0)}function Ye(t){var n,o,r,i=t.aLastSort,a=t.oClasses.order.position,s=Ve(t),l=t.oFeatures;if(l.bSort&&l.bSortClasses){for(n=0,o=i.length;n<o;n++)r=i[n].src,e(w(t.aoData,\"anCells\",r)).removeClass(a+(n<2?n+1:3));for(n=0,o=s.length;n<o;n++)r=s[n].src,e(w(t.aoData,\"anCells\",r)).addClass(a+(n<2?n+1:3))}t.aLastSort=s}function $e(e,t){var n,o,r,i=e.aoColumns[t],a=s.ext.order[i.sSortDataType];a&&(n=a.call(e.oInstance,e,t,M(e,t)));for(var l=s.ext.type.order[i.sType+\"-pre\"],c=e.aoData,d=0;d<c.length;d++)c[d]&&((o=c[d])._aSortData||(o._aSortData=[]),o._aSortData[t]&&!a||(r=a?n[d]:J(e,d,t,\"sort\"),o._aSortData[t]=l?l(r,e):r))}function Ze(t){if(!t._bLoadingState){var n=[];Be(t,n,t.aaSorting);var o=t.aoColumns,r={time:+new Date,start:t._iDisplayStart,length:t._iDisplayLength,order:n.map(function(e){return o[e[0]]&&o[e[0]].sName?[o[e[0]].sName,e[1]]:e.slice()}),search:e.extend({},t.oPreviousSearch),columns:t.aoColumns.map(function(n,o){return{name:n.sName,visible:n.bVisible,search:e.extend({},t.aoPreSearchCols[o])}})};t.oSavedState=r,nt(t,\"aoStateSaveParams\",\"stateSaveParams\",[t,r]),t.oFeatures.bStateSave&&!t.bDestroying&&t.fnStateSaveCallback.call(t.oInstance,t,r)}}function Je(t,n,o){var r,i,a=t.aoColumns,l=w(t.aoColumns,\"sName\");t._bLoadingState=!0;var c=t._bInitComplete?new s.Api(t):null;if(!n||!n.time)return t._bLoadingState=!1,void o();var d=t.iStateDuration;if(d>0&&n.time<+new Date-1e3*d)return t._bLoadingState=!1,void o();if(-1!==nt(t,\"aoStateLoadParams\",\"stateLoadParams\",[t,n]).indexOf(!1))return t._bLoadingState=!1,void o();if(t.oLoadedState=e.extend(!0,{},n),nt(t,null,\"stateLoadInit\",[t,n],!0),void 0!==n.length&&(c?c.page.len(n.length):t._iDisplayLength=n.length),void 0!==n.start&&(null===c?(t._iDisplayStart=n.start,t.iInitDisplayStart=n.start):Le(t,n.start/t._iDisplayLength)),void 0!==n.order&&(t.aaSorting=[],e.each(n.order,function(e,n){var o=[n[0],n[1]];if(\"string\"==typeof n[0]){var r=l.indexOf(n[0]);if(r<0)return;o[0]=r}else if(o[0]>=a.length)return;t.aaSorting.push(o)})),void 0!==n.search&&e.extend(t.oPreviousSearch,n.search),n.columns){var u=n.columns,f=w(n.columns,\"name\");if(f.join(\"\").length&&f.join(\"\")!==l.join(\"\"))for(u=[],r=0;r<l.length;r++)if(\"\"!=l[r]){var h=f.indexOf(l[r]);h>=0?u.push(n.columns[h]):u.push({})}else u.push({});if(u.length===a.length){for(r=0,i=u.length;r<i;r++){var p=u[r];void 0!==p.visible&&(c?c.column(r).visible(p.visible,!1):a[r].bVisible=p.visible),void 0!==p.search&&e.extend(t.aoPreSearchCols[r],p.search)}c&&c.one(\"draw\",function(){c.columns.adjust()})}}t._bLoadingState=!1,nt(t,\"aoStateLoaded\",\"stateLoaded\",[t,n]),o()}function Qe(e,n,o,r){if(o=\"DataTables warning: \"+(e?\"table id=\"+e.sTableId+\" - \":\"\")+o,r&&(o+=\". For more information about this error, please see https://datatables.net/tn/\"+r),n)t.console&&console.log&&console.log(o);else{var i=s.ext,a=i.sErrMode||i.errMode;if(e&&nt(e,null,\"dt-error\",[e,r,o],!0),\"alert\"==a)alert(o);else{if(\"throw\"==a)throw new Error(o);\"function\"==typeof a&&a(e,r,o)}}}function Ke(t,n,o,r){Array.isArray(o)?e.each(o,function(e,o){Array.isArray(o)?Ke(t,n,o[0],o[1]):Ke(t,n,o)}):(void 0===r&&(r=o),void 0!==n[o]&&(t[r]=n[o]))}function Ge(t,n,o){var r;for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(r=n[i],e.isPlainObject(r)?(e.isPlainObject(t[i])||(t[i]={}),e.extend(!0,t[i],r)):o&&\"data\"!==i&&\"aaData\"!==i&&Array.isArray(r)?t[i]=r.slice():t[i]=r);return t}function et(t,n,o){e(t).on(\"click.DT\",n,function(e){o(e)}).on(\"keypress.DT\",n,function(e){13===e.which&&(e.preventDefault(),o(e))}).on(\"selectstart.DT\",n,function(){return!1})}function tt(e,t,n){n&&e[t].push(n)}function nt(t,n,o,r,i){var a=[];if(n&&(a=t[n].slice().reverse().map(function(e){return e.apply(t.oInstance,r)})),null!==o){var s=e.Event(o+\".dt\"),l=e(t.nTable);s.dt=t.api,l[i?\"trigger\":\"triggerHandler\"](s,r),i&&0===l.parents(\"body\").length&&e(\"body\").trigger(s,r),a.push(s.result)}return a}function ot(e){var t=e._iDisplayStart,n=e.fnDisplayEnd(),o=e._iDisplayLength;t>=n&&(t=n-o),t-=t%o,(-1===o||t<0)&&(t=0),e._iDisplayStart=t}function rt(t,n){var o=t.renderer,r=s.ext.renderer[n];return e.isPlainObject(o)&&o[n]?r[o[n]]||r._:\"string\"==typeof o&&r[o]||r._}function it(e){return e.oFeatures.bServerSide?\"ssp\":e.ajax?\"ajax\":\"dom\"}function at(e,t,n){var o=e.fnFormatNumber,r=e._iDisplayStart+1,i=e._iDisplayLength,a=e.fnRecordsDisplay(),s=e.fnRecordsTotal(),l=-1===i;return t.replace(/_START_/g,o.call(e,r)).replace(/_END_/g,o.call(e,e.fnDisplayEnd())).replace(/_MAX_/g,o.call(e,s)).replace(/_TOTAL_/g,o.call(e,a)).replace(/_PAGE_/g,o.call(e,l?1:Math.ceil(r/i))).replace(/_PAGES_/g,o.call(e,l?1:Math.ceil(a/i))).replace(/_ENTRIES_/g,e.api.i18n(\"entries\",\"\",n)).replace(/_ENTRIES-MAX_/g,e.api.i18n(\"entries\",\"\",s)).replace(/_ENTRIES-TOTAL_/g,e.api.i18n(\"entries\",\"\",a))}function st(e,t){if(t)if(t.length<1e4)e.push.apply(e,t);else for(Vt=0;Vt<t.length;Vt++)e.push(t[Vt])}function lt(e,t,n){for(Array.isArray(n)||(n=[n]),Vt=0;Vt<n.length;Vt++)e.on(t+\".dt\",n[Vt])}function ct(t){return s.ext.escape.attributes&&e.each(t,function(e,n){t[e]=C(n)}),t}var dt=[],ut=Array.prototype;function ft(e,t,n){return function(){var o=t.apply(e||this,arguments);return r.extend(o,o,n.methodExt),o}}function ht(e,t){for(var n=0,o=e.length;n<o;n++)if(e[n].name===t)return e[n];return null}r=function(t,n){if(!(this instanceof r))return new r(t,n);var o,i=[],a=function(t){var n=function(t){var n,o,r=s.settings,i=w(r,\"nTable\");return t?t.nTable&&t.oFeatures?[t]:t.nodeName&&\"table\"===t.nodeName.toLowerCase()?-1!==(n=i.indexOf(t))?[r[n]]:null:t&&\"function\"==typeof t.settings?t.settings().toArray():(\"string\"==typeof t?o=e(t).get():t instanceof e&&(o=t.get()),o?r.filter(function(e,t){return o.includes(i[t])}):void 0):[]}(t);n&&i.push.apply(i,n)};if(Array.isArray(t))for(o=0;o<t.length;o++)a(t[o]);else a(t);this.context=i.length>1?A(i):i,st(this,n),this.selector={rows:null,cols:null,opts:null},r.extend(this,this,dt)},s.Api=r,e.extend(r.prototype,{any:function(){return 0!==this.count()},context:[],count:function(){return this.flatten().length},each:function(e){for(var t=0,n=this.length;t<n;t++)e.call(this,this[t],t,this);return this},eq:function(e){var t=this.context;return t.length>e?new r(t[e],this[e]):null},filter:function(e){var t=ut.filter.call(this,e,this);return new r(this.context,t)},flatten:function(){var e=[];return new r(this.context,e.concat.apply(e,this.toArray()))},get:function(e){return this[e]},join:ut.join,includes:function(e){return-1!==this.indexOf(e)},indexOf:ut.indexOf,iterator:function(e,t,n,o){var i,a,s,l,c,d,u,f,h=[],p=this.context,m=this.selector;for(\"string\"==typeof e&&(o=n,n=t,t=e,e=!1),a=0,s=p.length;a<s;a++){var g=new r(p[a]);if(\"table\"===t)void 0!==(i=n.call(g,p[a],a))&&h.push(i);else if(\"columns\"===t||\"rows\"===t)void 0!==(i=n.call(g,p[a],this[a],a))&&h.push(i);else if(\"every\"===t||\"column\"===t||\"column-rows\"===t||\"row\"===t||\"cell\"===t)for(u=this[a],\"column-rows\"===t&&(d=yt(p[a],m.opts)),l=0,c=u.length;l<c;l++)f=u[l],void 0!==(i=\"cell\"===t?n.call(g,p[a],f.row,f.column,a,l):n.call(g,p[a],f,a,l,d))&&h.push(i)}if(h.length||o){var v=new r(p,e?h.concat.apply([],h):h),b=v.selector;return b.rows=m.rows,b.cols=m.cols,b.opts=m.opts,v}return this},lastIndexOf:ut.lastIndexOf,length:0,map:function(e){var t=ut.map.call(this,e,this);return new r(this.context,t)},pluck:function(e){var t=s.util.get(e);return this.map(function(e){return t(e)})},pop:ut.pop,push:ut.push,reduce:ut.reduce,reduceRight:ut.reduceRight,reverse:ut.reverse,selector:null,shift:ut.shift,slice:function(){return new r(this.context,this)},sort:ut.sort,splice:ut.splice,toArray:function(){return ut.slice.call(this)},to$:function(){return e(this)},toJQuery:function(){return e(this)},unique:function(){return new r(this.context,A(this.toArray()))},unshift:ut.unshift}),t.__apiStruct=dt,r.extend=function(e,t,n){var o,i,a;if(n.length&&t&&(t instanceof r||t.__dt_wrapper))for(o=0,i=n.length;o<i;o++)\"__proto__\"!==(a=n[o]).name&&(t[a.name]=\"function\"===a.type?ft(e,a.val,a):\"object\"===a.type?{}:a.val,t[a.name].__dt_wrapper=!0,r.extend(e,t[a.name],a.propExt))},r.register=i=function(t,n){if(Array.isArray(t))for(var o=0,i=t.length;o<i;o++)r.register(t[o],n);else{var a,s,l,c,d=t.split(\".\"),u=dt;for(a=0,s=d.length;a<s;a++){var f=ht(u,l=(c=-1!==d[a].indexOf(\"()\"))?d[a].replace(\"()\",\"\"):d[a]);f||(f={name:l,val:{},methodExt:[],propExt:[],type:\"object\"},u.push(f)),a===s-1?(f.val=n,f.type=\"function\"==typeof n?\"function\":e.isPlainObject(n)?\"object\":\"other\"):u=c?f.methodExt:f.propExt}}},r.registerPlural=a=function(e,t,n){r.register(e,n),r.register(t,function(){var e=n.apply(this,arguments);return e===this?this:e instanceof r?e.length?Array.isArray(e[0])?new r(e.context,e[0]):e[0]:void 0:e})};var pt=function(t,n){if(Array.isArray(t)){var o=[];return t.forEach(function(e){var t=pt(e,n);st(o,t)}),o.filter(function(e){return e})}if(\"number\"==typeof t)return[n[t]];var r=n.map(function(e){return e.nTable});return e(r).filter(t).map(function(){var e=r.indexOf(this);return n[e]}).toArray()};i(\"tables()\",function(e){return null!=e?new r(pt(e,this.context)):this}),i(\"table()\",function(e){var t=this.tables(e),n=t.context;return n.length?new r(n[0]):t}),[[\"nodes\",\"node\",\"nTable\"],[\"body\",\"body\",\"nTBody\"],[\"header\",\"header\",\"nTHead\"],[\"footer\",\"footer\",\"nTFoot\"]].forEach(function(e){a(\"tables().\"+e[0]+\"()\",\"table().\"+e[1]+\"()\",function(){return this.iterator(\"table\",function(t){return t[e[2]]},1)})}),[[\"header\",\"aoHeader\"],[\"footer\",\"aoFooter\"]].forEach(function(e){i(\"table().\"+e[0]+\".structure()\",function(t){var n=this.columns(t).indexes().flatten().toArray(),o=this.context[0],r=ue(o,o[e[1]],n),i=n.slice().sort(function(e,t){return e-t});return r.map(function(e){return n.map(function(t){return e[i.indexOf(t)]})})})}),a(\"tables().containers()\",\"table().container()\",function(){return this.iterator(\"table\",function(e){return e.nTableWrapper},1)}),i(\"tables().every()\",function(e){var t=this;return this.iterator(\"table\",function(n,o){e.call(t.table(o),o)})}),i(\"caption()\",function(t,n){var o=this.context;if(void 0===t){var r=o[0].captionNode;return r&&o.length?r.innerHTML:null}return this.iterator(\"table\",function(o){var r=e(o.nTable),i=e(o.captionNode),a=e(o.nTableWrapper);if(i.length||(i=e(\"<caption/>\").html(t),o.captionNode=i[0],n||(r.prepend(i),n=i.css(\"caption-side\"))),i.html(t),n&&(i.css(\"caption-side\",n),i[0]._captionSide=n),a.find(\"div.dataTables_scroll\").length){var s=\"top\"===n?\"Head\":\"Foot\";a.find(\"div.dataTables_scroll\"+s+\" table\").prepend(i)}else r.prepend(i)},1)}),i(\"caption.node()\",function(){var e=this.context;return e.length?e[0].captionNode:null}),i(\"draw()\",function(e){return this.iterator(\"table\",function(t){\"page\"===e?he(t):(\"string\"==typeof e&&(e=\"full-hold\"!==e),pe(t,!1===e))})}),i(\"page()\",function(e){return void 0===e?this.page.info().page:this.iterator(\"table\",function(t){Le(t,e)})}),i(\"page.info()\",function(){if(0!==this.context.length){var e=this.context[0],t=e._iDisplayStart,n=e.oFeatures.bPaginate?e._iDisplayLength:-1,o=e.fnRecordsDisplay(),r=-1===n;return{page:r?0:Math.floor(t/n),pages:r?1:Math.ceil(o/n),start:t,end:e.fnDisplayEnd(),length:n,recordsTotal:e.fnRecordsTotal(),recordsDisplay:o,serverSide:\"ssp\"===it(e)}}}),i(\"page.len()\",function(e){return void 0===e?0!==this.context.length?this.context[0]._iDisplayLength:void 0:this.iterator(\"table\",function(t){Ne(t,e)})});var mt=function(e,t,n){if(n){var o=new r(e);o.one(\"draw\",function(){n(o.ajax.json())})}if(\"ssp\"==it(e))pe(e,t);else{Fe(e,!0);var i=e.jqXHR;i&&4!==i.readyState&&i.abort(),_e(e,{},function(n){re(e);for(var o=xe(e,n),r=0,i=o.length;r<i;r++)$(e,o[r]);pe(e,t),ke(e),Fe(e,!1)})}};i(\"ajax.json()\",function(){var e=this.context;if(e.length>0)return e[0].json}),i(\"ajax.params()\",function(){var e=this.context;if(e.length>0)return e[0].oAjaxData}),i(\"ajax.reload()\",function(e,t){return this.iterator(\"table\",function(n){mt(n,!1===t,e)})}),i(\"ajax.url()\",function(t){var n=this.context;if(void 0===t){if(0===n.length)return;return n=n[0],e.isPlainObject(n.ajax)?n.ajax.url:n.ajax}return this.iterator(\"table\",function(n){e.isPlainObject(n.ajax)?n.ajax.url=t:n.ajax=t})}),i(\"ajax.url().load()\",function(e,t){return this.iterator(\"table\",function(n){mt(n,!1===t,e)})});var gt=function(e,t,n,r,i){var a,s,l,c=[],d=typeof t;for(t&&\"string\"!==d&&\"function\"!==d&&void 0!==t.length||(t=[t]),s=0,l=t.length;s<l;s++)(a=(a=n(\"string\"==typeof t[s]?t[s].trim():t[s])).filter(function(e){return null!=e}))&&a.length&&(c=c.concat(a));var u=o.selector[e];if(u.length)for(s=0,l=u.length;s<l;s++)c=u[s](r,i,c);return A(c)},vt=function(t){return t||(t={}),t.filter&&void 0===t.search&&(t.search=t.filter),e.extend({columnOrder:\"implied\",search:\"none\",order:\"current\",page:\"all\"},t)},bt=function(e){var t=new r(e.context[0]);return e.length&&t.push(e[0]),t.selector=e.selector,t.length&&t[0].length>1&&t[0].splice(1),t},yt=function(e,t){var n,o,r,i=[],a=e.aiDisplay,s=e.aiDisplayMaster,l=t.search,c=t.order,d=t.page;if(\"ssp\"==it(e))return\"removed\"===l?[]:x(0,s.length);if(\"current\"==d)for(n=e._iDisplayStart,o=e.fnDisplayEnd();n<o;n++)i.push(a[n]);else if(\"current\"==c||\"applied\"==c){if(\"none\"==l)i=s.slice();else if(\"applied\"==l)i=a.slice();else if(\"removed\"==l){var u={};for(n=0,o=a.length;n<o;n++)u[a[n]]=null;s.forEach(function(e){Object.prototype.hasOwnProperty.call(u,e)||i.push(e)})}}else if(\"index\"==c||\"original\"==c)for(n=0,o=e.aoData.length;n<o;n++)e.aoData[n]&&(\"none\"==l||-1===(r=a.indexOf(n))&&\"removed\"==l||r>=0&&\"applied\"==l)&&i.push(n);else if(\"number\"==typeof c){var f=ze(e,c,\"asc\");if(\"none\"===l)i=f;else for(n=0;n<f.length;n++)(-1===(r=a.indexOf(f[n]))&&\"removed\"==l||r>=0&&\"applied\"==l)&&i.push(f[n])}return i};i(\"rows()\",function(t,n){void 0===t?t=\"\":e.isPlainObject(t)&&(n=t,t=\"\"),n=vt(n);var o=this.iterator(\"table\",function(o){return function(t,n,o){var r,i=gt(\"row\",n,function(n){var i=g(n),a=t.aoData;if(null!==i&&!o)return[i];if(r||(r=yt(t,o)),null!==i&&-1!==r.indexOf(i))return[i];if(null==n||\"\"===n)return r;if(\"function\"==typeof n)return r.map(function(e){var t=a[e];return n(e,t._aData,t.nTr)?e:null});if(n.nodeName){var s=n._DT_RowIndex,l=n._DT_CellIndex;if(void 0!==s)return a[s]&&a[s].nTr===n?[s]:[];if(l)return a[l.row]&&a[l.row].nTr===n.parentNode?[l.row]:[];var c=e(n).closest(\"*[data-dt-row]\");return c.length?[c.data(\"dt-row\")]:[]}if(\"string\"==typeof n&&\"#\"===n.charAt(0)){var d=t.aIds[n.replace(/^#/,\"\")];if(void 0!==d)return[d.idx]}var u=T(_(t.aoData,r,\"nTr\"));return e(u).filter(n).map(function(){return this._DT_RowIndex}).toArray()},t,o);return\"current\"!==o.order&&\"applied\"!==o.order||Ue(t,i),i}(o,t,n)},1);return o.selector.rows=t,o.selector.opts=n,o}),i(\"rows().nodes()\",function(){return this.iterator(\"row\",function(e,t){return e.aoData[t].nTr||void 0},1)}),i(\"rows().data()\",function(){return this.iterator(!0,\"rows\",function(e,t){return _(e.aoData,t,\"_aData\")},1)}),a(\"rows().cache()\",\"row().cache()\",function(e){return this.iterator(\"row\",function(t,n){var o=t.aoData[n];return\"search\"===e?o._aFilterData:o._aSortData},1)}),a(\"rows().invalidate()\",\"row().invalidate()\",function(e){return this.iterator(\"row\",function(t,n){ie(t,n,e)})}),a(\"rows().indexes()\",\"row().index()\",function(){return this.iterator(\"row\",function(e,t){return t},1)}),a(\"rows().ids()\",\"row().id()\",function(e){for(var t=[],n=this.context,o=0,i=n.length;o<i;o++)for(var a=0,s=this[o].length;a<s;a++){var l=n[o].rowIdFn(n[o].aoData[this[o][a]]._aData);t.push((!0===e?\"#\":\"\")+l)}return new r(n,t)}),a(\"rows().remove()\",\"row().remove()\",function(){return this.iterator(\"row\",function(e,t){var n=e.aoData,o=n[t],r=e.aiDisplayMaster.indexOf(t);-1!==r&&e.aiDisplayMaster.splice(r,1),e._iRecordsDisplay>0&&e._iRecordsDisplay--,ot(e);var i=e.rowIdFn(o._aData);void 0!==i&&delete e.aIds[i],n[t]=null}),this}),i(\"rows.add()\",function(e){var t=this.iterator(\"table\",function(t){var n,o,r,i=[];for(o=0,r=e.length;o<r;o++)(n=e[o]).nodeName&&\"TR\"===n.nodeName.toUpperCase()?i.push(Z(t,n)[0]):i.push($(t,n));return i},1),n=this.rows(-1);return n.pop(),st(n,t),n}),i(\"row()\",function(e,t){return bt(this.rows(e,t))}),i(\"row().data()\",function(e){var t=this.context;if(void 0===e)return t.length&&this.length&&this[0].length?t[0].aoData[this[0]]._aData:void 0;var n=t[0].aoData[this[0]];return n._aData=e,Array.isArray(e)&&n.nTr&&n.nTr.id&&ne(t[0].rowId)(e,n.nTr.id),ie(t[0],this[0],\"data\"),this}),i(\"row().node()\",function(){var e=this.context;if(e.length&&this.length&&this[0].length){var t=e[0].aoData[this[0]];if(t&&t.nTr)return t.nTr}return null}),i(\"row.add()\",function(t){t instanceof e&&t.length&&(t=t[0]);var n=this.iterator(\"table\",function(e){return t.nodeName&&\"TR\"===t.nodeName.toUpperCase()?Z(e,t)[0]:$(e,t)});return this.row(n[0])}),e(n).on(\"plugin-init.dt\",function(e,t){var n=new r(t);n.on(\"stateSaveParams.DT\",function(e,t,n){for(var o=t.rowIdFn,r=t.aiDisplayMaster,i=[],a=0;a<r.length;a++){var s=r[a],l=t.aoData[s];l._detailsShow&&i.push(\"#\"+o(l._aData))}n.childRows=i}),n.on(\"stateLoaded.DT\",function(e,t,o){wt(n,o)}),wt(n,n.state.loaded())});var wt=function(e,t){t&&t.childRows&&e.rows(t.childRows.map(function(e){return e.replace(/([^:\\\\]*(?:\\\\.[^:\\\\]*)*):/g,\"$1\\\\:\")})).every(function(){nt(e.settings()[0],null,\"requestChild\",[this])})},_t=s.util.throttle(function(e){Ze(e[0])},500),xt=function(t,n){var o=t.context;if(o.length){var r=o[0].aoData[void 0!==n?n:t[0]];r&&r._details&&(r._details.detach(),r._detailsShow=void 0,r._details=void 0,e(r.nTr).removeClass(\"dt-hasChild\"),_t(o))}},Tt=function(t,n){var o=t.context;if(o.length&&t.length){var r=o[0].aoData[t[0]];r._details&&(r._detailsShow=n,n?(r._details.insertAfter(r.nTr),e(r.nTr).addClass(\"dt-hasChild\")):(r._details.detach(),e(r.nTr).removeClass(\"dt-hasChild\")),nt(o[0],null,\"childRow\",[n,t.row(t[0])]),Dt(o[0]),_t(o))}},Dt=function(t){var n=new r(t),o=\".dt.DT_details\",i=\"draw\"+o,a=\"column-sizing\"+o,s=\"destroy\"+o,l=t.aoData;n.off(i+\" \"+a+\" \"+s),w(l,\"_details\").length>0&&(n.on(i,function(e,o){t===o&&n.rows({page:\"current\"}).eq(0).each(function(e){var t=l[e];t._detailsShow&&t._details.insertAfter(t.nTr)})}),n.on(a,function(n,o){if(t===o)for(var r,i=W(o),a=0,s=l.length;a<s;a++)(r=l[a])&&r._details&&r._details.each(function(){var t=e(this).children(\"td\");1==t.length&&t.attr(\"colspan\",i)})}),n.on(s,function(e,o){if(t===o)for(var r=0,i=l.length;r<i;r++)l[r]&&l[r]._details&&xt(n,r)}))},Ct=\"row().child\",St=Ct+\"()\";i(St,function(t,n){var o=this.context;return void 0===t?o.length&&this.length&&o[0].aoData[this[0]]?o[0].aoData[this[0]]._details:void 0:(!0===t?this.child.show():!1===t?xt(this):o.length&&this.length&&function(t,n,o,r){var i=[],a=function(o,r){if(Array.isArray(o)||o instanceof e)for(var s=0,l=o.length;s<l;s++)a(o[s],r);else if(o.nodeName&&\"tr\"===o.nodeName.toLowerCase())o.setAttribute(\"data-dt-row\",n.idx),i.push(o);else{var c=e(\"<tr><td></td></tr>\").attr(\"data-dt-row\",n.idx).addClass(r);e(\"td\",c).addClass(r).html(o)[0].colSpan=W(t),i.push(c[0])}};a(o,r),n._details&&n._details.detach(),n._details=e(i),n._detailsShow&&n._details.insertAfter(n.nTr)}(o[0],o[0].aoData[this[0]],t,n),this)}),i([Ct+\".show()\",St+\".show()\"],function(){return Tt(this,!0),this}),i([Ct+\".hide()\",St+\".hide()\"],function(){return Tt(this,!1),this}),i([Ct+\".remove()\",St+\".remove()\"],function(){return xt(this),this}),i(Ct+\".isShown()\",function(){var e=this.context;return e.length&&this.length&&e[0].aoData[this[0]]&&e[0].aoData[this[0]]._detailsShow||!1});var At=/^([^:]+)?:(name|title|visIdx|visible)$/,Rt=function(e,t,n,o,r,i){for(var a=[],s=0,l=r.length;s<l;s++)a.push(J(e,r[s],t,i));return a},It=function(t,n,o){var r=t.aoHeader,i=t.titleRow,a=null;if(void 0!==o)a=o;else if(!0===i)a=0;else if(!1===i)a=r.length-1;else if(null!==i)a=i;else{for(var s=0;s<r.length;s++)r[s][n].unique&&e(\".dt-column-title\",r[s][n].cell).text()&&(a=s);null===a&&(a=0)}return r[a][n].cell};i(\"columns()\",function(t,n){void 0===t?t=\"\":e.isPlainObject(t)&&(n=t,t=\"\"),n=vt(n);var o=this.iterator(\"table\",function(o){return function(t,n,o){var r,i,a=t.aoColumns,s=function(e){for(var t=[],n=0;n<e.length;n++)for(var o=0;o<e[n].length;o++){var r=e[n][o].cell;t.includes(r)||t.push(r)}return t}(t.aoHeader),l=gt(\"column\",n,function(n){var l=g(n);if(\"\"===n)return x(a.length);if(null!==l)return[l>=0?l:a.length+l];if(\"function\"==typeof n){var c=yt(t,o);return a.map(function(e,o){return n(o,Rt(t,o,0,0,c),It(t,o))?o:null})}var d=\"string\"==typeof n?n.match(At):\"\";if(d)switch(d[2]){case\"visIdx\":case\"visible\":if(d[1]&&d[1].match(/^\\d+$/)){var u=parseInt(d[1],10);if(u<0){var f=a.map(function(e,t){return e.bVisible?t:null});return[f[f.length+u]]}return[E(t,u)]}return a.map(function(t,n){return t.bVisible?!1===t.responsiveVisible?null:d[1]?e(s[n]).filter(d[1]).length>0?n:null:n:null});case\"name\":return r||(r=w(a,\"sName\")),r.map(function(e,t){return e===d[1]?t:null});case\"title\":return i||(i=w(a,\"sTitle\")),i.map(function(e,t){return e===d[1]?t:null});default:return[]}if(n.nodeName&&n._DT_CellIndex)return[n._DT_CellIndex.column];var h=e(s).filter(n).map(function(){return Y(this)}).toArray().sort(function(e,t){return e-t});if(h.length||!n.nodeName)return h;var p=e(n).closest(\"*[data-dt-column]\");return p.length?[p.data(\"dt-column\")]:[]},t,o);return o.columnOrder&&\"index\"===o.columnOrder?l.sort(function(e,t){return e-t}):l}(o,t,n)},1);return o.selector.cols=t,o.selector.opts=n,o}),a(\"columns().header()\",\"column().header()\",function(e){return this.iterator(\"column\",function(t,n){return It(t,n,e)},1)}),a(\"columns().footer()\",\"column().footer()\",function(e){return this.iterator(\"column\",function(t,n){return t.aoFooter.length?t.aoFooter[void 0!==e?e:0][n].cell:null},1)}),a(\"columns().data()\",\"column().data()\",function(){return this.iterator(\"column-rows\",Rt,1)}),a(\"columns().render()\",\"column().render()\",function(e){return this.iterator(\"column-rows\",function(t,n,o,r,i){return Rt(t,n,0,0,i,e)},1)}),a(\"columns().dataSrc()\",\"column().dataSrc()\",function(){return this.iterator(\"column\",function(e,t){return e.aoColumns[t].mData},1)}),a(\"columns().cache()\",\"column().cache()\",function(e){return this.iterator(\"column-rows\",function(t,n,o,r,i){return _(t.aoData,i,\"search\"===e?\"_aFilterData\":\"_aSortData\",n)},1)}),a(\"columns().init()\",\"column().init()\",function(){return this.iterator(\"column\",function(e,t){return e.aoColumns[t]},1)}),a(\"columns().names()\",\"column().name()\",function(){return this.iterator(\"column\",function(e,t){return e.aoColumns[t].sName},1)}),a(\"columns().nodes()\",\"column().nodes()\",function(){return this.iterator(\"column-rows\",function(e,t,n,o,r){return _(e.aoData,r,\"anCells\",t)},1)}),a(\"columns().titles()\",\"column().title()\",function(t,n){return this.iterator(\"column\",function(o,r){\"number\"==typeof t&&(n=t,t=void 0);var i=e(\".dt-column-title\",this.column(r).header(n));return void 0!==t?(i.html(t),this):i.html()},1)}),a(\"columns().types()\",\"column().type()\",function(){return this.iterator(\"column\",function(e,t){var n=e.aoColumns[t],o=n.sType;return o||(B(e),o=n.sType),o},1)}),a(\"columns().visible()\",\"column().visible()\",function(t,n){var o=this,r=[],i=this.iterator(\"column\",function(n,o){if(void 0===t)return n.aoColumns[o].bVisible;(function(t,n,o){var r,i,a,s,l=t.aoColumns,c=l[n],d=t.aoData;if(void 0===o)return c.bVisible;if(c.bVisible===o)return!1;if(o){var u=w(l,\"bVisible\").indexOf(!0,n+1);for(i=0,a=d.length;i<a;i++)d[i]&&(s=d[i].nTr,r=d[i].anCells,s&&s.insertBefore(r[n],r[u]||null))}else e(w(t.aoData,\"anCells\",n)).detach();return c.bVisible=o,We(t),!0})(n,o,t)&&r.push(o)});return void 0!==t&&this.iterator(\"table\",function(i){fe(i,i.aoHeader),fe(i,i.aoFooter),i.aiDisplay.length||e(i.nTBody).find(\"td[colspan]\").attr(\"colspan\",W(i)),Ze(i),o.iterator(\"column\",function(e,o){r.includes(o)&&nt(e,null,\"column-visibility\",[e,o,t,n])}),r.length&&(void 0===n||n)&&o.columns.adjust()}),i}),a(\"columns().widths()\",\"column().width()\",function(){var t=this.columns(\":visible\").count(),n=e(\"<tr>\").html(\"<td>\"+Array(t).join(\"</td><td>\")+\"</td>\");e(this.table().body()).append(n);var o=n.children().map(function(){return e(this).outerWidth()});return n.remove(),this.iterator(\"column\",function(e,t){var n=M(e,t);return null!==n?o[n]:0},1)}),a(\"columns().indexes()\",\"column().index()\",function(e){return this.iterator(\"column\",function(t,n){return\"visible\"===e?M(t,n):n},1)}),i(\"columns.adjust()\",function(){return this.iterator(\"table\",function(e){e.containerWidth=-1,H(e)},1)}),i(\"column.index()\",function(e,t){if(0!==this.context.length){var n=this.context[0];if(\"fromVisible\"===e||\"toData\"===e)return E(n,t);if(\"fromData\"===e||\"toVisible\"===e)return M(n,t)}}),i(\"column()\",function(e,t){return bt(this.columns(e,t))});var kt,Nt;function Lt(t,n){e(t).find(\".dt-column-order\").remove(),e(t).find(\".dt-column-title\").each(function(){var t=e(this).html();e(this).parent().parent().append(t),e(this).remove()}),e(t).find(\"div.dt-column-\"+n).remove(),e(\"th, td\",t).removeAttr(\"data-dt-column\")}i(\"cells()\",function(t,n,o){if(e.isPlainObject(t)&&(void 0===t.row?(o=t,t=null):(o=n,n=null)),e.isPlainObject(n)&&(o=n,n=null),null==n)return this.iterator(\"table\",function(n){return function(t,n,o){var r,i,a,s,l,c,d,u=t.aoData,f=yt(t,o),h=T(_(u,f,\"anCells\")),p=e(R([],h)),m=t.aoColumns.length;return gt(\"cell\",n,function(n){var o=\"function\"==typeof n;if(null==n||o){for(i=[],a=0,s=f.length;a<s;a++)for(r=f[a],l=0;l<m;l++)c={row:r,column:l},o?(d=u[r],n(c,J(t,r,l),d.anCells?d.anCells[l]:null)&&i.push(c)):i.push(c);return i}if(e.isPlainObject(n))return void 0!==n.column&&void 0!==n.row&&-1!==f.indexOf(n.row)?[n]:[];var h=p.filter(n).map(function(e,t){return{row:t._DT_CellIndex.row,column:t._DT_CellIndex.column}}).toArray();return h.length||!n.nodeName?h:(d=e(n).closest(\"*[data-dt-row]\")).length?[{row:d.data(\"dt-row\"),column:d.data(\"dt-column\")}]:[]},t,o)}(n,t,vt(o))});var r,i,a,s,l=o?{page:o.page,order:o.order,search:o.search}:{},c=this.columns(n,l),d=this.rows(t,l),u=this.iterator(\"table\",function(e,t){var n=[];for(r=0,i=d[t].length;r<i;r++)for(a=0,s=c[t].length;a<s;a++)n.push({row:d[t][r],column:c[t][a]});return n},1),f=o&&o.selected?this.cells(u,o):u;return e.extend(f.selector,{cols:n,rows:t,opts:o}),f}),a(\"cells().nodes()\",\"cell().node()\",function(){return this.iterator(\"cell\",function(e,t,n){var o=e.aoData[t];return o&&o.anCells?o.anCells[n]:void 0},1)}),i(\"cells().data()\",function(){return this.iterator(\"cell\",function(e,t,n){return J(e,t,n)},1)}),a(\"cells().cache()\",\"cell().cache()\",function(e){return e=\"search\"===e?\"_aFilterData\":\"_aSortData\",this.iterator(\"cell\",function(t,n,o){return t.aoData[n][e][o]},1)}),a(\"cells().render()\",\"cell().render()\",function(e){return this.iterator(\"cell\",function(t,n,o){return J(t,n,o,e)},1)}),a(\"cells().indexes()\",\"cell().index()\",function(){return this.iterator(\"cell\",function(e,t,n){return{row:t,column:n,columnVisible:M(e,n)}},1)}),a(\"cells().invalidate()\",\"cell().invalidate()\",function(e){return this.iterator(\"cell\",function(t,n,o){ie(t,n,e,o)})}),i(\"cell()\",function(e,t,n){return bt(this.cells(e,t,n))}),i(\"cell().data()\",function(e){var t,n,o,r,i,a,s=this.context,l=this[0];return void 0===e?s.length&&l.length?J(s[0],l[0].row,l[0].column):void 0:(t=s[0],n=l[0].row,o=l[0].column,r=e,i=t.aoColumns[o],a=t.aoData[n]._aData,i.fnSetData(a,r,{settings:t,row:n,col:o}),ie(s[0],l[0].row,\"data\",l[0].column),this)}),i(\"order()\",function(e,t){var n=this.context,o=Array.prototype.slice.call(arguments);return void 0===e?0!==n.length?n[0].aaSorting:void 0:(\"number\"==typeof e?e=[[e,t]]:o.length>1&&(e=o),this.iterator(\"table\",function(t){var n=[];Be(t,n,e),t.aaSorting=n}))}),i(\"order.listener()\",function(e,t,n){return this.iterator(\"table\",function(o){qe(o,e,{},t,n)})}),i(\"order.fixed()\",function(t){if(!t){var n=this.context,o=n.length?n[0].aaSortingFixed:void 0;return Array.isArray(o)?{pre:o}:o}return this.iterator(\"table\",function(n){n.aaSortingFixed=e.extend(!0,{},t)})}),i([\"columns().order()\",\"column().order()\"],function(e){var t=this;return e?this.iterator(\"table\",function(n,o){n.aaSorting=t[o].map(function(t){return[t,e]})}):this.iterator(\"column\",function(e,t){for(var n=Ve(e),o=0,r=n.length;o<r;o++)if(n[o].col===t)return n[o].dir;return null},1)}),a(\"columns().orderable()\",\"column().orderable()\",function(e){return this.iterator(\"column\",function(t,n){var o=t.aoColumns[n];return e?o.asSorting:o.bSortable},1)}),i(\"processing()\",function(e){return this.iterator(\"table\",function(t){Fe(t,e)})}),i(\"search()\",function(t,n,o,r){var i=this.context;return void 0===t?0!==i.length?i[0].oPreviousSearch.search:void 0:this.iterator(\"table\",function(i){i.oFeatures.bFilter&&De(i,\"object\"==typeof n?e.extend(i.oPreviousSearch,n,{search:t}):e.extend(i.oPreviousSearch,{search:t,regex:null!==n&&n,smart:null===o||o,caseInsensitive:null===r||r}))})}),i(\"search.fixed()\",function(e,t){var n=this.iterator(!0,\"table\",function(n){var o=n.searchFixed;return e?void 0===t?o[e]:(null===t?delete o[e]:o[e]=t,this):Object.keys(o)});return void 0!==e&&void 0===t?n[0]:n}),a(\"columns().search()\",\"column().search()\",function(t,n,o,r){return this.iterator(\"column\",function(i,a){var s=i.aoPreSearchCols;if(void 0===t)return s[a].search;i.oFeatures.bFilter&&(\"object\"==typeof n?e.extend(s[a],n,{search:t}):e.extend(s[a],{search:t,regex:null!==n&&n,smart:null===o||o,caseInsensitive:null===r||r}),De(i,i.oPreviousSearch))})}),i([\"columns().search.fixed()\",\"column().search.fixed()\"],function(e,t){var n=this.iterator(!0,\"column\",function(n,o){var r=n.aoColumns[o].searchFixed;return e?void 0===t?r[e]||null:(null===t?delete r[e]:r[e]=t,this):Object.keys(r)});return void 0!==e&&void 0===t?n[0]:n}),i(\"state()\",function(t,n){if(!t)return this.context.length?this.context[0].oSavedState:null;var o=e.extend(!0,{},t);return this.iterator(\"table\",function(e){!1!==n&&(o.time=+new Date+100),Je(e,o,function(){})})}),i(\"state.clear()\",function(){return this.iterator(\"table\",function(e){e.fnStateSaveCallback.call(e.oInstance,e,{})})}),i(\"state.loaded()\",function(){return this.context.length?this.context[0].oLoadedState:null}),i(\"state.save()\",function(){return this.iterator(\"table\",function(e){Ze(e)})}),s.use=function(o,r){var i=\"string\"==typeof o?r:o,a=\"string\"==typeof r?r:o;if(void 0===i&&\"string\"==typeof a)switch(a){case\"lib\":case\"jq\":return e;case\"win\":return t;case\"datetime\":return s.DateTime;case\"luxon\":return Pt;case\"moment\":return Ht;case\"bootstrap\":return kt||t.bootstrap;case\"foundation\":return Nt||t.Foundation;default:return null}\"lib\"===a||\"jq\"===a||i&&i.fn&&i.fn.jquery?e=i:\"win\"===a||i&&i.document?(t=i,n=i.document):\"datetime\"===a||i&&\"DateTime\"===i.type?s.DateTime=i:\"luxon\"===a||i&&i.FixedOffsetZone?Pt=i:\"moment\"===a||i&&i.isMoment?Ht=i:\"bootstrap\"===a||i&&i.Modal&&\"modal\"===i.Modal.NAME?kt=i:(\"foundation\"===a||i&&i.Reveal)&&(Nt=i)},s.factory=function(o,r){var i=!1;return o&&o.document&&(t=o,n=o.document),r&&r.fn&&r.fn.jquery&&(e=r,i=!0),i},s.versionCheck=function(e,t){for(var n,o,r=t?t.split(\".\"):s.version.split(\".\"),i=e.split(\".\"),a=0,l=i.length;a<l;a++)if((n=parseInt(r[a],10)||0)!==(o=parseInt(i[a],10)||0))return n>o;return!0},s.isDataTable=function(t){var n=e(t).get(0),o=!1;return t instanceof s.Api||(e.each(s.settings,function(t,r){var i=r.nScrollHead?e(\"table\",r.nScrollHead)[0]:null,a=r.nScrollFoot?e(\"table\",r.nScrollFoot)[0]:null;r.nTable!==n&&i!==n&&a!==n||(o=!0)}),o)},s.tables=function(t){var n=!1;e.isPlainObject(t)&&(n=t.api,t=t.visible);var o=s.settings.filter(function(n){return!!(!t||t&&e(n.nTable).is(\":visible\"))}).map(function(e){return e.nTable});return n?new r(o):o},s.camelToHungarian=N,i(\"$()\",function(t,n){var o=this.rows(n).nodes(),r=e(o);return e([].concat(r.filter(t).toArray(),r.find(t).toArray()))}),e.each([\"on\",\"one\",\"off\"],function(t,n){i(n+\"()\",function(){var t=Array.prototype.slice.call(arguments);t[0]=t[0].split(/\\s/).map(function(e){return e.match(/\\.dt\\b/)?e:e+\".dt\"}).join(\" \");var o=e(this.tables().nodes());return o[n].apply(o,t),this})}),i(\"clear()\",function(){return this.iterator(\"table\",function(e){re(e)})}),i(\"error()\",function(e){return this.iterator(\"table\",function(t){Qe(t,0,e)})}),i(\"settings()\",function(){return new r(this.context,this.context)}),i(\"init()\",function(){var e=this.context;return e.length?e[0].oInit:null}),i(\"data()\",function(){return this.iterator(\"table\",function(e){return w(e.aoData,\"_aData\")}).flatten()}),i(\"trigger()\",function(e,t,n){return this.iterator(\"table\",function(o){return nt(o,null,e,t,n)}).flatten()}),i(\"ready()\",function(e){var t=this.context;return e?this.tables().every(function(){var t=this;this.context[0]._bInitComplete?e.call(t):this.on(\"init.dt.DT\",function(){e.call(t)})}):t.length?t[0]._bInitComplete||!1:null}),i(\"destroy()\",function(n){return n=n||!1,this.iterator(\"table\",function(o){var i=o.oClasses,a=o.nTable,l=o.nTBody,c=o.nTHead,d=o.nTFoot,u=e(a),f=e(l),h=e(o.nTableWrapper),p=o.aoData.map(function(e){return e?e.nTr:null}),m=i.order;o.bDestroying=!0,nt(o,\"aoDestroyCallback\",\"destroy\",[o],!0),n||new r(o).columns().visible(!0),o.resizeObserver&&o.resizeObserver.disconnect(),h.off(\".DT\").find(\":not(tbody *)\").off(\".DT\"),e(t).off(\".DT-\"+o.sInstance),a!=c.parentNode&&(u.children(\"thead\").detach(),u.append(c)),d&&a!=d.parentNode&&(u.children(\"tfoot\").detach(),u.append(d)),Lt(c,\"header\"),Lt(d,\"footer\"),o.colgroup.remove(),o.aaSorting=[],o.aaSortingFixed=[],Ye(o),e(u).find(\"th, td\").removeClass(e.map(s.ext.type.className,function(e){return e}).join(\" \")),e(\"th, td\",c).removeClass(m.none+\" \"+m.canAsc+\" \"+m.canDesc+\" \"+m.isAsc+\" \"+m.isDesc).css(\"width\",\"\").removeAttr(\"aria-sort\"),f.children().detach(),f.append(p);var g=o.nTableWrapper.parentNode,v=o.nTableWrapper.nextSibling,b=n?\"remove\":\"detach\";u[b](),h[b](),!n&&g&&(g.insertBefore(a,v),u.css(\"width\",o.sDestroyWidth).removeClass(i.table));var y=s.settings.indexOf(o);-1!==y&&s.settings.splice(y,1)})}),e.each([\"column\",\"row\",\"cell\"],function(e,t){i(t+\"s().every()\",function(e){var n,o=this.selector.opts,r=this,i=0;return this.iterator(\"every\",function(a,s,l){n=r[t](s,o),\"cell\"===t?e.call(n,n[0][0].row,n[0][0].column,l,i):e.call(n,s,l,i),i++})})}),i(\"i18n()\",function(t,n,o){var r=this.context[0],i=te(t)(r.oLanguage);return void 0===i&&(i=n),e.isPlainObject(i)&&(i=void 0!==o&&void 0!==i[o]?i[o]:!1===o?i:i._),\"string\"==typeof i?i.replace(\"%d\",o):i}),s.version=\"2.3.7\",s.settings=[],s.models={},s.models.oSearch={caseInsensitive:!0,search:\"\",regex:!1,smart:!0,return:!1},s.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,src:null,idx:-1,displayData:null},s.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:\"std\",sSortingClass:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null,wideStrings:null,searchFixed:null},s.defaults={aaData:null,aaSorting:[[0,\"asc\"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],bAutoWidth:!0,bDeferRender:!0,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:null,titleRow:null,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(e){return e.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnStateLoadCallback:function(e){try{return JSON.parse((-1===e.iStateDuration?sessionStorage:localStorage).getItem(\"DataTables_\"+e.sInstance+\"_\"+location.pathname))}catch(e){return{}}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(e,t){try{(-1===e.iStateDuration?sessionStorage:localStorage).setItem(\"DataTables_\"+e.sInstance+\"_\"+location.pathname,JSON.stringify(t))}catch(e){}},fnStateSaveParams:null,iStateDuration:7200,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{orderable:\": Activate to sort\",orderableReverse:\": Activate to invert sorting\",orderableRemove:\": Activate to remove sorting\",paginate:{first:\"First\",last:\"Last\",next:\"Next\",previous:\"Previous\",number:\"\"}},oPaginate:{sFirst:\"«\",sLast:\"»\",sNext:\"›\",sPrevious:\"‹\"},entries:{_:\"entries\",1:\"entry\"},lengthLabels:{\"-1\":\"All\"},sEmptyTable:\"No data available in table\",sInfo:\"Showing _START_ to _END_ of _TOTAL_ _ENTRIES-TOTAL_\",sInfoEmpty:\"Showing 0 to 0 of 0 _ENTRIES-TOTAL_\",sInfoFiltered:\"(filtered from _MAX_ total _ENTRIES-MAX_)\",sInfoPostFix:\"\",sDecimal:\"\",sThousands:\",\",sLengthMenu:\"_MENU_ _ENTRIES_ per page\",sLoadingRecords:\"Loading...\",sProcessing:\"\",sSearch:\"Search:\",sSearchPlaceholder:\"\",sUrl:\"\",sZeroRecords:\"No matching records found\"},orderDescReverse:!0,oSearch:e.extend({},s.models.oSearch),layout:{topStart:\"pageLength\",topEnd:\"search\",bottomStart:\"info\",bottomEnd:\"paging\"},sDom:null,searchDelay:null,sPaginationType:\"\",sScrollX:\"\",sScrollXInner:\"\",sScrollY:\"\",sServerMethod:\"GET\",renderer:null,rowId:\"DT_RowId\",caption:null,iDeferLoading:null,on:null,columnTitleTag:\"span\"},k(s.defaults),s.defaults.column={aDataSort:null,iDataSort:-1,ariaTitle:\"\",asSorting:[\"asc\",\"desc\",\"\"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:\"td\",sClass:\"\",sContentPadding:\"\",sDefaultContent:null,sName:\"\",sSortDataType:\"std\",sTitle:null,sType:null,sWidth:null},k(s.defaults.column),s.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:!0,bLengthChange:!0,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollbarLeft:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},searchFixed:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:\"\",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:\"two_button\",pagingControls:0,iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,bAjaxDataGet:!0,jqXHR:null,json:void 0,oAjaxData:void 0,sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return\"ssp\"==it(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return\"ssp\"==it(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var e=this._iDisplayLength,t=this._iDisplayStart,n=t+e,o=this.aiDisplay.length,r=this.oFeatures,i=r.bPaginate;return r.bServerSide?!1===i||-1===e?t+o:Math.min(t+e,this._iRecordsDisplay):!i||n>o||-1===e?o:n},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null,caption:\"\",captionNode:null,colgroup:null,deferLoading:null,typeDetect:!0,resizeObserver:null,containerWidth:-1,orderDescReverse:null,orderIndicators:!0,orderHandler:!0,titleRow:null,columnTitleTag:\"span\"};var Ft=s.ext.pager;e.extend(Ft,{simple:function(){return[\"previous\",\"next\"]},full:function(){return[\"first\",\"previous\",\"next\",\"last\"]},numbers:function(){return[\"numbers\"]},simple_numbers:function(){return[\"previous\",\"numbers\",\"next\"]},full_numbers:function(){return[\"first\",\"previous\",\"numbers\",\"next\",\"last\"]},first_last:function(){return[\"first\",\"last\"]},first_last_numbers:function(){return[\"first\",\"numbers\",\"last\"]},_numbers:en,numbers_length:7}),e.extend(!0,s.ext.renderer,{pagingButton:{_:function(t,n,o,r,i){var a,s=t.oClasses.paging,l=[s.button];return r&&l.push(s.active),i&&l.push(s.disabled),{display:a=\"ellipsis\"===n?e('<span class=\"ellipsis\"></span>').html(o)[0]:e(\"<button>\",{class:l.join(\" \"),role:\"link\",type:\"button\"}).html(o),clicker:a}}},pagingContainer:{_:function(e,t){return t}}});var jt=function(e,t){return function(n){return m(n)||\"string\"!=typeof n||(n=n.replace(c,\" \"),e&&(n=D(n)),t&&(n=S(n,!1))),n}};function Ot(e,t,n,o,r){return Ht?e[t](r):Pt?e[n](r):o?e[o](r):e}var Pt,Ht,Et=!1;function Mt(e,n,o){var r;if(t.luxon&&!Pt&&(Pt=t.luxon),t.moment&&!Ht&&(Ht=t.moment),Ht){if(!(r=Ht.utc(e,n,o,!0)).isValid())return null}else if(Pt){if(!(r=n&&\"string\"==typeof e?Pt.DateTime.fromFormat(e,n):Pt.DateTime.fromISO(e)).isValid)return null;r=r.setLocale(o)}else n?(Et||alert(\"DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17\"),Et=!0):r=new Date(e);return r}function Wt(e){return function(t,n,o,r){0===arguments.length?(o=\"en\",n=null,t=null):1===arguments.length?(o=\"en\",n=t,t=null):2===arguments.length&&(o=n,n=t,t=null);var i=\"datetime\"+(n?\"-\"+n:\"\");return s.ext.type.order[i+\"-pre\"]||s.type(i,{detect:function(e){return e===i&&i},order:{pre:function(e){return e.valueOf()}},className:\"dt-right\"}),function(a,s){if(null==a)if(\"--now\"===r){var l=new Date;a=new Date(Date.UTC(l.getFullYear(),l.getMonth(),l.getDate(),l.getHours(),l.getMinutes(),l.getSeconds()))}else a=\"\";if(\"type\"===s)return i;if(\"\"===a)return\"sort\"!==s?\"\":Mt(\"0000-01-01 00:00:00\",null,o);if(null!==n&&t===n&&\"sort\"!==s&&\"type\"!==s&&!(a instanceof Date))return a;var c=Mt(a,t,o);if(null===c)return a;if(\"sort\"===s)return c;var d=null===n?Ot(c,\"toDate\",\"toJSDate\",\"\")[e](navigator.language,{timeZone:\"UTC\"}):Ot(c,\"format\",\"toFormat\",\"toISOString\",n);return\"display\"===s?C(d):d}}}var qt=\",\",Ut=\".\";if(void 0!==t.Intl)try{for(var Bt=(new Intl.NumberFormat).formatToParts(100000.1),Vt=0;Vt<Bt.length;Vt++)\"group\"===Bt[Vt].type?qt=Bt[Vt].value:\"decimal\"===Bt[Vt].type&&(Ut=Bt[Vt].value)}catch(e){}s.datetime=function(e,t){var n=\"datetime-\"+e;t||(t=\"en\"),s.ext.type.order[n]||s.type(n,{detect:function(o){var r=Mt(o,e,t);return!(\"\"!==o&&!r)&&n},order:{pre:function(n){return Mt(n,e,t)||0}},className:\"dt-right\"})},s.render={date:Wt(\"toLocaleDateString\"),datetime:Wt(\"toLocaleString\"),time:Wt(\"toLocaleTimeString\"),number:function(e,t,n,o,r){return null==e&&(e=qt),null==t&&(t=Ut),{display:function(i){if(\"number\"!=typeof i&&\"string\"!=typeof i)return i;if(\"\"===i||null===i)return i;var a=i<0?\"-\":\"\",s=parseFloat(i),l=Math.abs(s);if(l>=1e11||l<1e-4&&0!==l){var c=s.toExponential(n).split(/e\\+?/);return c[0]+\" x 10<sup>\"+c[1]+\"</sup>\"}if(isNaN(s))return C(i);s=s.toFixed(n),i=Math.abs(s);var d=parseInt(i,10),u=n?t+(i-d).toFixed(n).substring(2):\"\";return 0===d&&0===parseFloat(u)&&(a=\"\"),a+(o||\"\")+d.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g,e)+u+(r||\"\")}}},text:function(){return{display:C,filter:C}}};var zt=s.ext.type;s.type=function(e,t,n){if(!t)return{className:zt.className[e],detect:zt.detect.find(function(t){return t._name===e}),order:{pre:zt.order[e+\"-pre\"],asc:zt.order[e+\"-asc\"],desc:zt.order[e+\"-desc\"]},render:zt.render[e],search:zt.search[e]};var o=function(t,n){zt[t][e]=n},r=function(t){Object.defineProperty(t,\"_name\",{value:e});var n=zt.detect.findIndex(function(t){return t._name===e});-1===n?zt.detect.unshift(t):zt.detect.splice(n,1,t)},i=function(t){zt.order[e+\"-pre\"]=t.pre,zt.order[e+\"-asc\"]=t.asc,zt.order[e+\"-desc\"]=t.desc};void 0===n&&(n=t,t=null),\"className\"===t?o(\"className\",n):\"detect\"===t?r(n):\"order\"===t?i(n):\"render\"===t?o(\"render\",n):\"search\"===t?o(\"search\",n):t||(n.className&&o(\"className\",n.className),void 0!==n.detect&&r(n.detect),n.order&&i(n.order),void 0!==n.render&&o(\"render\",n.render),void 0!==n.search&&o(\"search\",n.search))},s.types=function(){return zt.detect.map(function(e){return e._name})};var Xt=function(e,t){return e=null!=e?e.toString().toLowerCase():\"\",t=null!=t?t.toString().toLowerCase():\"\",e.localeCompare(t,navigator.languages[0]||navigator.language,{numeric:!0,ignorePunctuation:!0})},Yt=function(e,t){return e=D(e),t=D(t),Xt(e,t)};s.type(\"string\",{detect:function(){return\"string\"},order:{pre:function(e){return m(e)&&\"boolean\"!=typeof e?\"\":\"string\"==typeof e?e.toLowerCase():e.toString?e.toString():\"\"}},search:jt(!1,!0)}),s.type(\"string-utf8\",{detect:{allOf:function(e){return!0},oneOf:function(e){return!m(e)&&navigator.languages&&\"string\"==typeof e&&e.match(/[^\\x00-\\x7F]/)}},order:{asc:Xt,desc:function(e,t){return-1*Xt(e,t)}},search:jt(!1,!0)}),s.type(\"html\",{detect:{allOf:function(e){return m(e)||\"string\"==typeof e&&-1!==e.indexOf(\"<\")},oneOf:function(e){return!m(e)&&\"string\"==typeof e&&-1!==e.indexOf(\"<\")}},order:{pre:function(e){return m(e)?\"\":e.replace?D(e).trim().toLowerCase():e+\"\"}},search:jt(!0,!0)}),s.type(\"html-utf8\",{detect:{allOf:function(e){return m(e)||\"string\"==typeof e&&-1!==e.indexOf(\"<\")},oneOf:function(e){return navigator.languages&&!m(e)&&\"string\"==typeof e&&-1!==e.indexOf(\"<\")&&\"string\"==typeof e&&e.match(/[^\\x00-\\x7F]/)}},order:{asc:Yt,desc:function(e,t){return-1*Yt(e,t)}},search:jt(!0,!0)}),s.type(\"date\",{className:\"dt-type-date\",detect:{allOf:function(e){if(e&&!(e instanceof Date)&&!f.test(e))return null;var t=Date.parse(e);return null!==t&&!isNaN(t)||m(e)},oneOf:function(e){return e instanceof Date||\"string\"==typeof e&&f.test(e)}},order:{pre:function(e){var t=Date.parse(e);return isNaN(t)?-1/0:t}}}),s.type(\"html-num-fmt\",{className:\"dt-type-numeric\",detect:{allOf:function(e,t){var n=t.oLanguage.sDecimal;return y(e,n,!0,!1)},oneOf:function(e,t){var n=t.oLanguage.sDecimal;return y(e,n,!0,!1)}},order:{pre:function(e,t){var n=t.oLanguage.sDecimal;return $t(e,n,d,p)}},search:jt(!0,!0)}),s.type(\"html-num\",{className:\"dt-type-numeric\",detect:{allOf:function(e,t){var n=t.oLanguage.sDecimal;return y(e,n,!1,!0)},oneOf:function(e,t){var n=t.oLanguage.sDecimal;return y(e,n,!1,!1)}},order:{pre:function(e,t){var n=t.oLanguage.sDecimal;return $t(e,n,d)}},search:jt(!0,!0)}),s.type(\"num-fmt\",{className:\"dt-type-numeric\",detect:{allOf:function(e,t){var n=t.oLanguage.sDecimal;return b(e,n,!0,!0)},oneOf:function(e,t){var n=t.oLanguage.sDecimal;return b(e,n,!0,!1)}},order:{pre:function(e,t){var n=t.oLanguage.sDecimal;return $t(e,n,p)}}}),s.type(\"num\",{className:\"dt-type-numeric\",detect:{allOf:function(e,t){var n=t.oLanguage.sDecimal;return b(e,n,!1,!0)},oneOf:function(e,t){var n=t.oLanguage.sDecimal;return b(e,n,!1,!1)}},order:{pre:function(e,t){var n=t.oLanguage.sDecimal;return $t(e,n)}}});var $t=function(e,t,n,o){if(0!==e&&(!e||\"-\"===e))return-1/0;var r=typeof e;return\"number\"===r||\"bigint\"===r?e:(t&&(e=v(e,t)),e.replace&&(n&&(e=e.replace(n,\"\")),o&&(e=e.replace(o,\"\"))),1*e)};function Zt(e,t,n){n&&(e[t]=n)}e.extend(!0,s.ext.renderer,{footer:{_:function(e,t,n){t.addClass(n.tfoot.cell)}},header:{_:function(t,n,o){n.addClass(o.thead.cell),t.oFeatures.bSort||n.addClass(o.order.none);var r=t.titleRow,i=n.closest(\"thead\").find(\"tr\"),a=n.parent().index();\"disable\"===n.attr(\"data-dt-order\")||\"disable\"===n.parent().attr(\"data-dt-order\")||!0===r&&0!==a||!1===r&&a!==i.length-1||\"number\"==typeof r&&a!==r||e(t.nTable).on(\"order.dt.DT column-visibility.dt.DT\",function(e,r,i){if(t===r){var a=r.sortDetails;if(a){var s=w(a,\"col\");if(\"column-visibility\"!==e.type||s.includes(i)){var l,c=o.order,d=r.api.columns(n),u=t.aoColumns[d.flatten()[0]],f=d.orderable().includes(!0),h=\"\",p=d.indexes(),m=d.orderable(!0).flatten(),g=t.iTabIndex,v=r.orderHandler&&f;n.removeClass(c.isAsc+\" \"+c.isDesc).toggleClass(c.none,!f).toggleClass(c.canAsc,v&&m.includes(\"asc\")).toggleClass(c.canDesc,v&&m.includes(\"desc\"));var b=!0;for(l=0;l<p.length;l++)s.includes(p[l])||(b=!1);if(b){var y=d.order();n.addClass(y.includes(\"asc\")?c.isAsc:\"\"+y.includes(\"desc\")?c.isDesc:\"\")}var _=-1;for(l=0;l<s.length;l++)if(t.aoColumns[s[l]].bVisible){_=s[l];break}if(p[0]==_){var x=a[0],T=u.asSorting;n.attr(\"aria-sort\",\"asc\"===x.dir?\"ascending\":\"descending\"),h=T[x.index+1]?\"Reverse\":\"Remove\"}else n.removeAttr(\"aria-sort\");if(f){var D=n.find(\".dt-column-order\");D.attr(\"role\",\"button\").attr(\"aria-label\",f?u.ariaTitle+r.api.i18n(\"oAria.orderable\"+h):u.ariaTitle),-1!==g&&D.attr(\"tabindex\",g)}}}}})}},layout:{_:function(t,n,o){var r=t.oClasses.layout,i=e(\"<div/>\").attr(\"id\",o.id||null).addClass(o.className||r.row).appendTo(n);s.ext.renderer.layout._forLayoutRow(o,function(t,n){if(\"id\"!==t&&\"className\"!==t){var o=\"\";n.table&&(i.addClass(r.tableRow),o+=r.tableCell+\" \"),o+=\"start\"===t?r.start:\"end\"===t?r.end:r.full,e(\"<div/>\").attr({id:n.id||null,class:n.className?n.className:r.cell+\" \"+o}).append(n.contents).appendTo(i)}})},_forLayoutRow:function(e,t){var n=function(e){switch(e){case\"\":return 0;case\"start\":return 1;case\"end\":return 2;default:return 3}};Object.keys(e).sort(function(e,t){return n(e)-n(t)}).forEach(function(n){t(n,e[n])})}}}),s.feature={},s.feature.register=function(e,t,n){s.ext.features[e]=t,n&&o.feature.push({cFeature:n,fnInit:t})},s.feature.register(\"div\",function(t,n){var o=e(\"<div>\")[0];return n&&(Zt(o,\"className\",n.className),Zt(o,\"id\",n.id),Zt(o,\"innerHTML\",n.html),Zt(o,\"textContent\",n.text)),o}),s.feature.register(\"info\",function(t,n){if(!t.oFeatures.bInfo)return null;var o=t.oLanguage,r=t.sTableId,i=e(\"<div/>\",{class:t.oClasses.info.container});return n=e.extend({callback:o.fnInfoCallback,empty:o.sInfoEmpty,postfix:o.sInfoPostFix,search:o.sInfoFiltered,text:o.sInfo},n),t.aoDrawCallback.push(function(e){!function(e,t,n){var o=e._iDisplayStart+1,r=e.fnDisplayEnd(),i=e.fnRecordsTotal(),a=e.fnRecordsDisplay(),s=a?t.text:t.empty;a!==i&&(s+=\" \"+t.search);s+=t.postfix,s=at(e,s),t.callback&&(s=t.callback.call(e.oInstance,e,o,r,i,a,s));n.html(s),nt(e,null,\"info\",[e,n[0],s])}(e,n,i)}),t._infoEl||(i.attr({\"aria-live\":\"polite\",id:r+\"_info\",role:\"status\"}),e(t.nTable).attr(\"aria-describedby\",r+\"_info\"),t._infoEl=i),i},\"i\");var Jt=0;function Qt(e){var t=[];return e.numbers&&t.push(\"numbers\"),e.previousNext&&(t.unshift(\"previous\"),t.push(\"next\")),e.firstLast&&(t.unshift(\"first\"),t.push(\"last\")),t}function Kt(t,o,r){if(t._bInitComplete){var i=r.type?s.ext.pager[r.type]:Qt,a=t.oLanguage.oAria.paginate||{},l=t._iDisplayStart,c=t._iDisplayLength,d=t.fnRecordsDisplay(),u=-1===c,f=u?0:Math.ceil(l/c),h=u?1:Math.ceil(d/c),p=[],m=[],g=i(r).map(function(e){return\"numbers\"===e?en(f,h,r.buttons,r.boundaryNumbers):e});p=p.concat.apply(p,g);for(var v=0;v<p.length;v++){var b=p[v],y=Gt(t,b,f,h),w=rt(t,\"pagingButton\")(t,b,y.display,y.active,y.disabled),_=\"string\"==typeof b?a[b]:a.number?a.number+(b+1):null;e(w.clicker).attr({\"aria-controls\":t.sTableId,\"aria-disabled\":y.disabled?\"true\":null,\"aria-current\":y.active?\"page\":null,\"aria-label\":_,\"data-dt-idx\":b,tabIndex:y.disabled?-1:t.iTabIndex&&\"span\"!==w.clicker[0].nodeName.toLowerCase()?t.iTabIndex:null}),\"number\"!=typeof b&&e(w.clicker).addClass(b),et(w.clicker,{action:b},function(e){e.preventDefault(),Le(t,e.data.action,!0)}),m.push(w.display)}var x=rt(t,\"pagingContainer\")(t,m),T=o.find(n.activeElement).data(\"dt-idx\");if(o.empty().append(x),void 0!==T&&o.find(\"[data-dt-idx=\"+T+\"]\").trigger(\"focus\"),m.length){var D=e(m[0]).outerHeight();r.buttons>1&&D>0&&e(o).height()>=2*D-10&&Kt(t,o,e.extend({},r,{buttons:r.buttons-2}))}}}function Gt(e,t,n,o){var r=e.oLanguage.oPaginate,i={display:\"\",active:!1,disabled:!1};switch(t){case\"ellipsis\":i.display=\"&#x2026;\";break;case\"first\":i.display=r.sFirst,0===n&&(i.disabled=!0);break;case\"previous\":i.display=r.sPrevious,0===n&&(i.disabled=!0);break;case\"next\":i.display=r.sNext,0!==o&&n!==o-1||(i.disabled=!0);break;case\"last\":i.display=r.sLast,0!==o&&n!==o-1||(i.disabled=!0);break;default:\"number\"==typeof t&&(i.display=e.fnFormatNumber(t+1),n===t&&(i.active=!0))}return i}function en(e,t,n,o){var r=[],i=Math.floor(n/2),a=o?2:1,s=o?1:0;return t<=n?r=x(0,t):1===n?r=[e]:3===n?e<=1?r=[0,1,\"ellipsis\"]:e>=t-2?(r=x(t-2,t)).unshift(\"ellipsis\"):r=[\"ellipsis\",e,\"ellipsis\"]:e<=i?((r=x(0,n-a)).push(\"ellipsis\"),o&&r.push(t-1)):e>=t-1-i?((r=x(t-(n-a),t)).unshift(\"ellipsis\"),o&&r.unshift(0)):((r=x(e-i+a,e+i-s)).push(\"ellipsis\"),r.unshift(\"ellipsis\"),o&&(r.push(t-1),r.unshift(0))),r}s.feature.register(\"search\",function(t,o){if(!t.oFeatures.bFilter)return null;var r=t.oClasses.search,i=t.sTableId,a=t.oLanguage,l=t.oPreviousSearch,c='<input type=\"search\" class=\"'+r.input+'\"/>';-1===(o=e.extend({placeholder:a.sSearchPlaceholder,processing:!1,text:a.sSearch},o)).text.indexOf(\"_INPUT_\")&&(o.text+=\"_INPUT_\"),o.text=at(t,o.text);var d=o.text.match(/_INPUT_$/),u=o.text.match(/^_INPUT_/),f=o.text.replace(/_INPUT_/,\"\"),h=\"<label>\"+o.text+\"</label>\";u?h=\"_INPUT_<label>\"+f+\"</label>\":d&&(h=\"<label>\"+f+\"</label>_INPUT_\");var p=e(\"<div>\").addClass(r.container).append(h.replace(/_INPUT_/,c));p.find(\"label\").attr(\"for\",\"dt-search-\"+Jt),p.find(\"input\").attr(\"id\",\"dt-search-\"+Jt),Jt++;var m=function(e){var n=this.value;l.return&&\"Enter\"!==e.key||n!=l.search&&je(t,o.processing,function(){l.search=n,De(t,l),t._iDisplayStart=0,he(t)})},g=null!==t.searchDelay?t.searchDelay:0,v=e(\"input\",p).val(l.search).attr(\"placeholder\",o.placeholder).on(\"keyup.DT search.DT input.DT paste.DT cut.DT\",g?s.util.debounce(m,g):m).on(\"mouseup.DT\",function(e){setTimeout(function(){m.call(v[0],e)},10)}).on(\"keypress.DT\",function(e){if(13==e.keyCode)return!1}).attr(\"aria-controls\",i);return e(t.nTable).on(\"search.dt.DT\",function(e,o){t===o&&v[0]!==n.activeElement&&v.val(\"function\"!=typeof l.search?l.search:\"\")}),p},\"f\"),s.feature.register(\"paging\",function(t,n){if(!t.oFeatures.bPaginate)return null;n=e.extend({buttons:s.ext.pager.numbers_length,type:t.sPaginationType,boundaryNumbers:!0,firstLast:!0,previousNext:!0,numbers:!0},n);var o=e(\"<div/>\").addClass(t.oClasses.paging.container+(n.type?\" paging_\"+n.type:\"\")).append(e(\"<nav>\").attr(\"aria-label\",\"pagination\").addClass(t.oClasses.paging.nav)),r=function(){Kt(t,o.children(),n)};return t.aoDrawCallback.push(r),e(t.nTable).on(\"column-sizing.dt.DT\",r),o},\"p\");var tn=0;return s.feature.register(\"pageLength\",function(t,n){var o=t.oFeatures;if(!o.bPaginate||!o.bLengthChange)return null;n=e.extend({menu:t.aLengthMenu,text:t.oLanguage.sLengthMenu},n);var r,i=t.oClasses.length,a=t.sTableId,s=n.menu,l=[],c=[];if(Array.isArray(s[0]))l=s[0],c=s[1];else for(r=0;r<s.length;r++)e.isPlainObject(s[r])?(l.push(s[r].value),c.push(s[r].label)):(l.push(s[r]),c.push(s[r]));var d=n.text.match(/_MENU_$/),u=n.text.match(/^_MENU_/),f=n.text.replace(/_MENU_/,\"\"),h=\"<label>\"+n.text+\"</label>\";u?h=\"_MENU_<label>\"+f+\"</label>\":d&&(h=\"<label>\"+f+\"</label>_MENU_\");var p=\"tmp-\"+ +new Date,m=e(\"<div/>\").addClass(i.container).append(h.replace(\"_MENU_\",'<span id=\"'+p+'\"></span>')),g=[];Array.prototype.slice.call(m.find(\"label\")[0].childNodes).forEach(function(e){e.nodeType===Node.TEXT_NODE&&g.push({el:e,text:e.textContent})});var v=function(e){g.forEach(function(n){n.el.textContent=at(t,n.text,e)})},b=e(\"<select/>\",{\"aria-controls\":a,class:i.select});for(r=0;r<l.length;r++){var y=t.api.i18n(\"lengthLabels.\"+l[r],null);null===y&&(y=\"number\"==typeof c[r]?t.fnFormatNumber(c[r]):c[r]),b[0][r]=new Option(y,l[r])}return m.find(\"label\").attr(\"for\",\"dt-length-\"+tn),b.attr(\"id\",\"dt-length-\"+tn),tn++,m.find(\"#\"+p).replaceWith(b),e(\"select\",m).val(t._iDisplayLength).on(\"change.DT\",function(){Ne(t,e(this).val()),he(t)}),e(t.nTable).on(\"length.dt.DT\",function(n,o,r){t===o&&(e(\"select\",m).val(r),v(r))}),v(t._iDisplayLength),m},\"l\"),e.fn.dataTable=s,s.$=e,e.fn.dataTableSettings=s.settings,e.fn.dataTableExt=s.ext,e.fn.DataTable=function(t){return e(this).dataTable(t).api()},e.each(s,function(t,n){e.fn.DataTable[t]=n}),s}),\n/*! DataTables Bootstrap 5 integration\n * © SpryMedia Ltd - datatables.net/license\n */\nfunction(e){if(\"function\"==typeof define&&define.amd)define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)});else if(\"object\"==typeof exports){var t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net\")(e,t)};\"undefined\"==typeof window?module.exports=function(o,r){return o||(o=window),r||(r=t(o)),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))}else e(jQuery,window,document)}(function(e,t,n){\"use strict\";var o=e.fn.dataTable;return e.extend(!0,o.defaults,{renderer:\"bootstrap\"}),e.extend(!0,o.ext.classes,{container:\"dt-container dt-bootstrap5\",search:{input:\"form-control form-control-sm\"},length:{select:\"form-select form-select-sm\"},processing:{container:\"dt-processing card\"},layout:{row:\"row mt-2 justify-content-between\",cell:\"d-md-flex justify-content-between align-items-center\",tableCell:\"col-12\",start:\"dt-layout-start col-md-auto me-auto\",end:\"dt-layout-end col-md-auto ms-auto\",full:\"dt-layout-full col-md\"}}),o.ext.renderer.pagingButton.bootstrap=function(t,n,o,r,i){var a=[\"dt-paging-button\",\"page-item\"];r&&a.push(\"active\"),i&&a.push(\"disabled\");var s=e(\"<li>\").addClass(a.join(\" \"));return{display:s,clicker:e(\"<button>\",{class:\"page-link\",role:\"link\",type:\"button\"}).html(o).appendTo(s)}},o.ext.renderer.pagingContainer.bootstrap=function(t,n){return e(\"<ul/>\").addClass(\"pagination\").append(n)},o});var defaults={language:{info:\"Showing _START_ to _END_ of _TOTAL_ records\",infoEmpty:\"Showing no records\",lengthMenu:\"_MENU_\",processing:'<span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span> <span class=\"text-gray-600\">Loading...</span>',paginate:{first:'<i class=\"kt-outline kt-double-left\"></i>',last:'<i class=\"kt-outline kt-double-right\"></i>',next:'<i class=\"next\"></i>',previous:'<i class=\"previous\"></i>'}}};$.extend(!0,$.fn.dataTable.defaults,defaults),\n/*! DataTables Bootstrap 4 integration\n * ©2011-2017 SpryMedia Ltd - datatables.net/license\n */\nfunction(e){\"function\"==typeof define&&define.amd?define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?module.exports=function(t,n){return t||(t=window),n&&n.fn.dataTable||(n=require(\"datatables.net\")(t,n).$),e(n,t,t.document)}:e(jQuery,window,document)}(function(e,t,n,o){\"use strict\";var r=e.fn.dataTable;return e.extend(!0,r.defaults,{pagingType:\"simple_numbers\",dom:\"<'table-responsive'tr><'row'<'col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start dt-toolbar'li><'col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end'p>>\",renderer:\"bootstrap\"}),e.extend(r.ext.classes,{sWrapper:\"dataTables_wrapper dt-bootstrap4\",sFilterInput:\"form-control form-control-sm form-control-solid\",sLengthSelect:\"form-select form-select-sm form-select-solid\",sProcessing:\"dataTables_processing\",sPageButton:\"paginate_button page-item\",search:{input:\"form-control form-control-solid form-control-sm\"},length:{select:\"form-select form-select-solid form-select-sm\"}}),r.ext.renderer.pageButton.bootstrap=function(t,i,a,s,l,c){var d,u,f,h=new r.Api(t),p=t.oClasses,m=t.oLanguage.oPaginate,g=t.oLanguage.oAria.paginate||{},v=0,b=function(n,o){var r,i,s,f,y=function(t){t.preventDefault(),e(t.currentTarget).hasClass(\"disabled\")||h.page()==t.data.action||h.page(t.data.action).draw(\"page\")};for(r=0,i=o.length;r<i;r++)if(f=o[r],Array.isArray(f))b(n,f);else{switch(d=\"\",u=\"\",f){case\"ellipsis\":d=\"&#x2026;\",u=\"disabled\";break;case\"first\":d=m.sFirst,u=f+(l>0?\"\":\" disabled\");break;case\"previous\":d=m.sPrevious,u=f+(l>0?\"\":\" disabled\");break;case\"next\":d=m.sNext,u=f+(l<c-1?\"\":\" disabled\");break;case\"last\":d=m.sLast,u=f+(l<c-1?\"\":\" disabled\");break;default:d=f+1,u=l===f?\"active\":\"\"}d&&(s=e(\"<li>\",{class:p.sPageButton+\" \"+u,id:0===a&&\"string\"==typeof f?t.sTableId+\"_\"+f:null}).append(e(\"<a>\",{href:\"#\",\"aria-controls\":t.sTableId,\"aria-label\":g[f],\"data-dt-idx\":v,tabindex:t.iTabIndex,class:\"page-link\"}).html(d)).appendTo(n),t.oApi._fnBindAction(s,{action:f},y),v++)}};try{f=e(i).find(n.activeElement).data(\"dt-idx\")}catch(e){}b(e(i).empty().html('<ul class=\"pagination\"/>').children(\"ul\"),s),f!==o&&e(i).find(\"[data-dt-idx=\"+f+\"]\").trigger(\"focus\")},r}),\n/*! ColReorder 2.1.2\n * © SpryMedia Ltd - datatables.net/license\n */\n(e=>{var t,n;\"function\"==typeof define&&define.amd?define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?(t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net\")(e,t)},\"undefined\"==typeof window?module.exports=function(o,r){return o=o||window,r=r||t(o),n(o,r),e(r,0,o.document)}:(n(window,t),module.exports=e(t,window,window.document))):e(jQuery,window,document)})(function(e,t,n){var o=e.fn.dataTable;function r(e,t,n,o){var r=e.splice(t,n);r.unshift(0),r.unshift(o<t?o:o-n+1),e.splice.apply(e,r)}function i(e){e.rows().invalidate(\"data\"),e.column(0).visible(e.column(0).visible()),e.columns.adjust();var t=e.colReorder.order();e.trigger(\"columns-reordered\",[{order:t,mapping:c(t)}])}function a(e){return e.settings()[0].aoColumns.map(function(e){return e._crOriginalIdx})}function s(e,t,n,o){for(var i=[],a=0;a<e.length;a++){var s=e[a];r(s,n[0],n.length,o);for(var l=0;l<s.length;l++){var c,d=s[l].cell;i.includes(d)||(c=d.getAttribute(\"data-dt-column\").split(\",\").map(function(e){return t[e]}).join(\",\"),d.setAttribute(\"data-dt-column\",c),i.push(d))}}}function l(e){e.columns().iterator(\"column\",function(e,t){void 0===(e=e.aoColumns)[t]._crOriginalIdx&&(e[t]._crOriginalIdx=t)})}function c(e){for(var t=[],n=0;n<e.length;n++)t[e[n]]=n;return t}function d(e,t,n){var o,i=e.settings()[0],a=i.aoColumns,l=a.map(function(e,t){return t});if(!t.includes(n)){r(l,t[0],t.length,n);var d=c(l);for(r(a,t[0],t.length,n),o=0;o<i.aoData.length;o++){var f=i.aoData[o];if(f){var h=f.anCells;if(h)for(r(h,t[0],t.length,n),m=0;m<h.length;m++)f.nTr&&h[m]&&a[m].bVisible&&f.nTr.appendChild(h[m]),h[m]&&h[m]._DT_CellIndex&&(h[m]._DT_CellIndex.column=m)}}for(o=0;o<a.length;o++){for(var p=a[o],m=0;m<p.aDataSort.length;m++)p.aDataSort[m]=d[p.aDataSort[m]];p.idx=d[p.idx],p.bVisible&&i.colgroup.append(p.colEl)}s(i.aoHeader,d,t,n),s(i.aoFooter,d,t,n),r(i.aoPreSearchCols,t[0],t.length,n),u(d,i.aaSorting),Array.isArray(i.aaSortingFixed)?u(d,i.aaSortingFixed):(i.aaSortingFixed.pre||i.aaSortingFixed.post)&&u(d,i.aaSortingFixed.pre),i.aLastSort.forEach(function(e){e.src=d[e.src]}),e.trigger(\"column-reorder\",[e.settings()[0],{from:t,to:n,mapping:d}])}}function u(t,n){if(n)for(var o=0;o<n.length;o++){var r=n[o];\"number\"==typeof r?n[o]=t[r]:e.isPlainObject(r)&&void 0!==r.idx?r.idx=t[r.idx]:Array.isArray(r)&&\"number\"==typeof r[0]&&(r[0]=t[r[0]])}}function f(e,t,n){var o=!1;if(t.length!==e.columns().count())e.error(\"ColReorder - column count mismatch\");else{for(var a=c(t=n?h(e,t,\"toCurrent\"):t),s=0;s<a.length;s++){var l=a.indexOf(s);s!==l&&(r(a,l,1,s),d(e,[l],s),o=!0)}o&&i(e)}}function h(e,t,n){var o=e.colReorder.order(),r=e.settings()[0].aoColumns;return\"toCurrent\"===n||\"fromOriginal\"===n?Array.isArray(t)?t.map(function(e){return o.indexOf(e)}):o.indexOf(t):Array.isArray(t)?t.map(function(e){return r[e]._crOriginalIdx}):r[t]._crOriginalIdx}function p(e,t,n){var o=e.columns().count();return!(t[0]<n&&n<t[t.length]||t[0]<0&&t[t.length-1]>o||n<0&&o<n||!t.includes(n)&&(!m(e.table().header.structure(),t,n)||!m(e.table().footer.structure(),t,n)))}function m(e,t,n){for(var o=(e=>{for(var t=[],n=0;n<e.length;n++){t.push([]);for(var o=0;o<e[n].length;o++){var r=e[n][o];if(r)for(var i=0;i<r.rowspan;i++){t[n+i]||(t[n+i]=[]);for(var a=0;a<r.colspan;a++)t[n+i][o+a]=r.cell}}}return t})(e),i=0;i<o.length;i++)r(o[i],t[0],t.length,n);for(i=0;i<o.length;i++)for(var a=[],s=0;s<o[i].length;s++){var l=o[i][s];if(a.includes(l)){if(a[a.length-1]!==l)return}else a.push(l)}return 1}v.prototype.disable=function(){return this.c.enable=!1,this},v.prototype.enable=function(e){return!1===(e=void 0===e||e)?this.disable():(this.c.enable=!0,this)},v.prototype._addListener=function(t){var n=this;e(t).on(\"selectstart.colReorder\",function(){return!1}).on(\"mousedown.colReorder touchstart.colReorder\",function(t){var o;\"mousedown\"===t.type&&1!==t.which||!n.c.enable||(o=e(\"button.dtcc-button_reorder\",this)).length&&t.target!==o[0]&&0===o.find(t.target).length||n._mouseDown(t,this)})},v.prototype._createDragNode=function(){var t=this.s.mouse.target,n=t.parent(),o=n.parent(),r=o.parent(),i=t.clone();this.dom.drag=e(r[0].cloneNode(!1)).addClass(\"dtcr-cloned\").append(e(o[0].cloneNode(!1)).append(e(n[0].cloneNode(!1)).append(i[0]))).css({position:\"absolute\",top:0,left:0,width:e(t).outerWidth(),height:e(t).outerHeight()}).appendTo(\"body\")},v.prototype._cursorPosition=function(e,t){return(-1!==e.type.indexOf(\"touch\")?e.originalEvent.touches[0]:e)[t]},v.prototype._mouseDown=function(t,o){for(var r=this,i=e(t.target).closest(\"th, td\"),a=i.offset(),s=this.dt.columns(this.c.columns).indexes().toArray(),l=e(o).attr(\"data-dt-column\").split(\",\").map(function(e){return parseInt(e,10)}),c=0;c<l.length;c++)if(!s.includes(l[c]))return!1;this.s.mouse.start.x=this._cursorPosition(t,\"pageX\"),this.s.mouse.start.y=this._cursorPosition(t,\"pageY\"),this.s.mouse.offset.x=this._cursorPosition(t,\"pageX\")-a.left,this.s.mouse.offset.y=this._cursorPosition(t,\"pageY\")-a.top,this.s.mouse.target=i,this.s.mouse.targets=l;for(var d=0;d<l.length;d++){var u=this.dt.cells(null,l[d],{page:\"current\"}).nodes().to$(),f=\"dtcr-moving\";0===d&&(f+=\" dtcr-moving-first\"),d===l.length-1&&(f+=\" dtcr-moving-last\"),u.addClass(f)}this._regions(l),this._scrollRegions(),e(n).on(\"mousemove.colReorder touchmove.colReorder\",function(e){r._mouseMove(e)}).on(\"mouseup.colReorder touchend.colReorder\",function(e){r._mouseUp(e)})},v.prototype._mouseMove=function(t){if(null===this.dom.drag){if(Math.pow(Math.pow(this._cursorPosition(t,\"pageX\")-this.s.mouse.start.x,2)+Math.pow(this._cursorPosition(t,\"pageY\")-this.s.mouse.start.y,2),.5)<5)return;e(n.body).addClass(\"dtcr-dragging\"),this._createDragNode()}this.dom.drag.css({left:this._cursorPosition(t,\"pageX\")-this.s.mouse.offset.x,top:this._cursorPosition(t,\"pageY\")-this.s.mouse.offset.y});var o,r=this.dt.table().node(),i=e(r).offset().left;i=this._cursorPosition(t,\"pageX\")-i,o=this._isRtl()?r.clientWidth-i:i,r=this.s.dropZones.find(function(e){return e.inlineStart<=o&&o<=e.inlineStart+e.width});this.s.mouse.absLeft=this._cursorPosition(t,\"pageX\"),r&&!r.self&&this._move(r,o)},v.prototype._mouseUp=function(t){var o=this;e(n).off(\".colReorder\"),e(n.body).removeClass(\"dtcr-dragging\"),this.dom.drag&&(this.dom.drag.remove(),this.dom.drag=null,this.s.mouse.target.on(\"click.dtcr\",function(e){return!1}),setTimeout(function(){o.s.mouse.target.off(\".dtcr\")},10)),this.s.scrollInterval&&clearInterval(this.s.scrollInterval),this.dt.cells(\".dtcr-moving\").nodes().to$().removeClass(\"dtcr-moving dtcr-moving-first dtcr-moving-last\")},v.prototype._move=function(t,n){var o,r,i=this,a=(this.dt.colReorder.move(this.s.mouse.targets,t.colIdx),this.s.mouse.targets=e(this.s.mouse.target).attr(\"data-dt-column\").split(\",\").map(function(e){return parseInt(e,10)}),this._regions(this.s.mouse.targets),this.s.mouse.targets.filter(function(e){return i.dt.column(e).visible()})),s=(t=this.s.dropZones.find(function(e){return e.colIdx===a[0]}),this.s.dropZones.indexOf(t));t.inlineStart>n&&(r=t.inlineStart-n,o=this.s.dropZones[s-1],t.inlineStart-=r,t.width+=r,o)&&(o.width-=r),(t=this.s.dropZones.find(function(e){return e.colIdx===a[a.length-1]})).inlineStart+t.width<n&&(o=n-(t.inlineStart+t.width),r=this.s.dropZones[s+1],t.width+=o,r)&&(r.inlineStart+=o,r.width-=o)},v.prototype._regions=function(e){var t=this,n=[],o=0,r=0,i=this.dt.columns(this.c.columns).indexes().toArray(),a=this.dt.columns().widths();this.dt.columns().every(function(s,l,c){var d;this.visible()&&(d=a[s],i.includes(s)&&(p(t.dt,e,s)?n.push({colIdx:s,inlineStart:o-r,self:e[0]<=s&&s<=e[e.length-1],width:d+r}):s<e[0]?n.length&&(n[n.length-1].width+=d):s>e[e.length-1]&&(r+=d)),o+=d)}),this.s.dropZones=n},v.prototype._isScrolling=function(){return this.dt.table().body().parentNode!==this.dt.table().header().parentNode},v.prototype._scrollRegions=function(){var t,n,o,r;this._isScrolling()&&(n=e((t=this).dt.table().container()).offset().left,o=e(this.dt.table().container()).outerWidth(),r=this.dt.table().body().parentElement.parentElement,this.s.scrollInterval=setInterval(function(){var e=t.s.mouse.absLeft;-1!==e&&(e<n+75&&r.scrollLeft?r.scrollLeft-=5:n+o-75<e&&r.scrollLeft<r.scrollWidth&&(r.scrollLeft+=5))},25))},v.prototype._isRtl=function(){return\"rtl\"===e(this.dt.table().node()).css(\"direction\")},v.defaults={columns:\"\",enable:!0,headerRows:null,order:null},v.version=\"2.1.2\";\n/*! ColReorder 2.1.2\n * © SpryMedia Ltd - datatables.net/license\n */\nvar g=v;function v(t,n){this.dom={drag:null},this.c={columns:null,enable:null,headerRows:null,order:null},this.s={dropZones:[],mouse:{absLeft:-1,offset:{x:-1,y:-1},start:{x:-1,y:-1},target:null,targets:[]},scrollInterval:null};var o,r,i=this;t.settings()[0]._colReorder||((t.settings()[0]._colReorder=this).dt=t,e.extend(this.c,v.defaults,n),l(t),t.on(\"stateSaveParams\",function(e,n,o){o.colReorder=a(t)}),t.on(\"destroy\",function(){t.off(\".colReorder\"),t.colReorder.reset()}),o=t.state.loaded(),r=this.c.order,(r=o&&o.colReorder&&t.columns().count()===o.colReorder.length?o.colReorder:r)&&t.ready(function(){f(t,r,!0)}),t.table().header.structure().forEach(function(e,t){for(var o=n.headerRows,r=0;r<e.length;r++)o&&!o.includes(t)||e[r]&&e[r].cell&&i._addListener(e[r].cell)}))}return o.Api.register(\"colReorder.enable()\",function(e){return this.iterator(\"table\",function(t){t._colReorder&&t._colReorder.enable(e)})}),o.Api.register(\"colReorder.disable()\",function(){return this.iterator(\"table\",function(e){e._colReorder&&e._colReorder.disable()})}),o.Api.register(\"colReorder.move()\",function(e,t){return l(this),p(this,e=Array.isArray(e)?e:[e],t)?this.tables().every(function(){d(this,e,t),i(this)}):(this.error(\"ColReorder - invalid move\"),this)}),o.Api.register(\"colReorder.order()\",function(e,t){return l(this),e?this.tables().every(function(){f(this,e,t)}):this.context.length?a(this):null}),o.Api.register(\"colReorder.reset()\",function(){return l(this),this.tables().every(function(){f(this,this.columns().every(function(e){return e}).flatten().toArray(),!0)})}),o.Api.register(\"colReorder.transpose()\",function(e,t){return l(this),h(this,e,t=t||\"toCurrent\")}),o.ColReorder=g,e(n).on(\"stateLoadInit.dt\",function(e,t,n){if(\"dt\"===e.namespace&&(e=new o.Api(t),n.colReorder&&e.columns().count()===n.colReorder.length))if(e.ready())f(e,n.colReorder,!0);else if(u(n.colReorder,n.order),n.columns){for(var r=0;r<n.columns.length;r++)n.columns[r]._cr_sort=n.colReorder[r];n.columns.sort(function(e,t){return e._cr_sort-t._cr_sort})}}),e(n).on(\"preInit.dt\",function(t,n){var r,i;\"dt\"===t.namespace&&(t=n.oInit.colReorder,i=o.defaults.colReorder,t||i)&&(r=e.extend({},i,t),!1!==t)&&(i=new o.Api(n),new g(i,r))}),o}),\n/*! Bootstrap 5 styling wrapper for ColReorder\n * © SpryMedia Ltd - datatables.net/license\n */\nfunction(e){if(\"function\"==typeof define&&define.amd)define([\"jquery\",\"datatables.net-bs5\",\"datatables.net-colreorder\"],function(t){return e(t,window,document)});else if(\"object\"==typeof exports){var t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net-bs5\")(e,t),t.fn.dataTable.ColReorder||require(\"datatables.net-colreorder\")(e,t)};\"undefined\"==typeof window?module.exports=function(o,r){return o||(o=window),r||(r=t(o)),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))}else e(jQuery,window,document)}(function(e,t,n){\"use strict\";return e.fn.dataTable}),\n/*! FixedColumns 5.0.5\n * © SpryMedia Ltd - datatables.net/license\n */\n(e=>{var t,n;\"function\"==typeof define&&define.amd?define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?(t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net\")(e,t)},\"undefined\"==typeof window?module.exports=function(o,r){return o=o||window,r=r||t(o),n(o,r),e(r,0,o.document)}:(n(window,t),module.exports=e(t,window,window.document))):e(jQuery,window,document)})(function(e,t,n){var o,r,i,a,s=e.fn.dataTable;function l(e,t){var n,i=this;if(r&&r.versionCheck&&r.versionCheck(\"2\"))return e=new r.Api(e),this.classes=o.extend(!0,{},l.classes),this.c=o.extend(!0,{},l.defaults,t),this.s={dt:e,rtl:\"rtl\"===o(e.table().node()).css(\"direction\")},t&&void 0!==t.leftColumns&&(t.left=t.leftColumns),t&&void 0!==t.left&&(this.c[this.s.rtl?\"end\":\"start\"]=t.left),t&&void 0!==t.rightColumns&&(t.right=t.rightColumns),t&&void 0!==t.right&&(this.c[this.s.rtl?\"start\":\"end\"]=t.right),this.dom={bottomBlocker:o(\"<div>\").addClass(this.classes.bottomBlocker),topBlocker:o(\"<div>\").addClass(this.classes.topBlocker),scroller:o(\"div.dt-scroll-body\",this.s.dt.table().container())},this.s.dt.settings()[0]._bInitComplete?(this._addStyles(),this._setKeyTableListener()):e.one(\"init.dt.dtfc\",function(){i._addStyles(),i._setKeyTableListener()}),e.on(\"column-sizing.dt.dtfc column-reorder.dt.dtfc draw.dt.dtfc\",function(){return i._addStyles()}),n=r.util.debounce(function(){i._addStyles()},50),e.on(\"column-visibility.dt.dtfc\",function(){n()}),this.dom.scroller.on(\"scroll.dtfc\",function(){return i._scroll()}),this._scroll(),e.settings()[0]._fixedColumns=this,e.on(\"destroy\",function(){return i._destroy()}),this;throw new Error(\"FixedColumns requires DataTables 2 or newer\")}function c(e,t){void 0===t&&(t=null),e=new s.Api(e),t=t||e.init().fixedColumns||s.defaults.fixedColumns,new i(e,t)}return l.prototype.end=function(e){return void 0!==e?(0<=e&&e<=this.s.dt.columns().count()&&(this.c.end=e,this._addStyles()),this):this.c.end},l.prototype.left=function(e){return this.s.rtl?this.end(e):this.start(e)},l.prototype.right=function(e){return this.s.rtl?this.start(e):this.end(e)},l.prototype.start=function(e){return void 0!==e?(0<=e&&e<=this.s.dt.columns().count()&&(this.c.start=e,this._addStyles()),this):this.c.start},l.prototype._addStyles=function(){var e=this.s.dt,t=this,n=this.s.dt.columns(\":visible\").count(),r=e.table().header.structure(\":visible\"),i=e.table().footer.structure(\":visible\"),a=e.columns(\":visible\").widths().toArray(),s=o(e.table().node()).closest(\"div.dt-scroll\"),l=o(e.table().node()).closest(\"div.dt-scroll-body\")[0],c=this.s.rtl,d=this.c.start,u=this.c.end,f=c?u:d,h=(c=c?d:u,e.settings()[0].oBrowser.barWidth);if(0===s.length)return this;l.offsetWidth===l.clientWidth&&(h=0),e.columns().every(function(o){var s;null!==(o=e.column.index(\"toVisible\",o))&&(o<d?(s=t._sum(a,o),t._fixColumn(o,s,\"start\",r,i,h)):n-u<=o?(s=t._sum(a,n-o-1,!0),t._fixColumn(o,s,\"end\",r,i,h)):t._fixColumn(o,0,\"none\",r,i,h))}),o(e.table().node()).toggleClass(t.classes.tableFixedStart,0<d).toggleClass(t.classes.tableFixedEnd,0<u).toggleClass(t.classes.tableFixedLeft,0<f).toggleClass(t.classes.tableFixedRight,0<c),l=e.table().header(),f=e.table().footer(),c=o(l).outerHeight(),l=o(f).outerHeight(),this.dom.topBlocker.appendTo(s).css(\"top\",0).css(this.s.rtl?\"left\":\"right\",0).css(\"height\",c).css(\"width\",h+1).css(\"display\",h?\"block\":\"none\"),f&&this.dom.bottomBlocker.appendTo(s).css(\"bottom\",0).css(this.s.rtl?\"left\":\"right\",0).css(\"height\",l).css(\"width\",h+1).css(\"display\",h?\"block\":\"none\")},l.prototype._destroy=function(){this.s.dt.off(\".dtfc\"),this.dom.scroller.off(\".dtfc\"),o(this.s.dt.table().node()).removeClass(this.classes.tableScrollingEnd+\" \"+this.classes.tableScrollingLeft+\" \"+this.classes.tableScrollingStart+\" \"+this.classes.tableScrollingRight),this.dom.bottomBlocker.remove(),this.dom.topBlocker.remove()},l.prototype._fixColumn=function(e,t,n,r,i,a){function s(e,o){var r,i;\"none\"===n?e.css(\"position\",\"\").css(\"left\",\"\").css(\"right\",\"\").removeClass(l.classes.fixedEnd+\" \"+l.classes.fixedLeft+\" \"+l.classes.fixedRight+\" \"+l.classes.fixedStart):(r=\"start\"===n?\"left\":\"right\",l.s.rtl&&(r=\"start\"===n?\"right\":\"left\"),i=t,\"end\"!==n||\"header\"!==o&&\"footer\"!==o||(i+=a),e.css(\"position\",\"sticky\").css(r,i).addClass(\"start\"===n?l.classes.fixedStart:l.classes.fixedEnd).addClass(\"left\"===r?l.classes.fixedLeft:l.classes.fixedRight))}var l=this,c=this.s.dt;r.forEach(function(t){t[e]&&s(o(t[e].cell),\"header\")}),s(c.column(e+\":visible\",{page:\"current\"}).nodes().to$(),\"body\"),i&&i.forEach(function(t){t[e]&&s(o(t[e].cell),\"footer\")})},l.prototype._scroll=function(){var e,t,n,r,i=this.dom.scroller[0];i&&(e=o(this.s.dt.table().node()).add(this.s.dt.table().header().parentNode).add(this.s.dt.table().footer().parentNode).add(\"div.dt-scroll-headInner table\",this.s.dt.table().container()).add(\"div.dt-scroll-footInner table\",this.s.dt.table().container()),t=i.scrollLeft,n=!this.s.rtl,r=0!==t,i=i.scrollWidth>i.clientWidth+Math.abs(t)+1,e.toggleClass(this.classes.tableScrollingStart,r),e.toggleClass(this.classes.tableScrollingEnd,i),e.toggleClass(this.classes.tableScrollingLeft,r&&n||i&&!n),e.toggleClass(this.classes.tableScrollingRight,i&&n||r&&!n))},l.prototype._setKeyTableListener=function(){var e=this;this.s.dt.on(\"key-focus.dt.dtfc\",function(t,n,r){var i,a,s,l=o(r.node()).offset(),c=e.dom.scroller[0],d=o(o(e.s.dt.table().node()).closest(\"div.dt-scroll-body\"));0<e.c.start&&(s=(a=o(e.s.dt.column(e.c.start-1).header())).offset(),a=a.outerWidth(),o(r.node()).hasClass(e.classes.fixedLeft)?d.scrollLeft(0):l.left<s.left+a&&(i=d.scrollLeft(),d.scrollLeft(i-(s.left+a-l.left)))),0<e.c.end&&(s=e.s.dt.columns().data().toArray().length,a=o(r.node()).outerWidth(),s=o(e.s.dt.column(s-e.c.end).header()).offset(),o(r.node()).hasClass(e.classes.fixedRight)?d.scrollLeft(c.scrollWidth-c.clientWidth):l.left+a>s.left&&(i=d.scrollLeft(),d.scrollLeft(i-(s.left-(l.left+a)))))})},l.prototype._sum=function(e,t,n){return(e=(n=void 0!==n&&n)?e.slice().reverse():e).slice(0,t).reduce(function(e,t){return e+t},0)},l.version=\"5.0.5\",l.classes={bottomBlocker:\"dtfc-bottom-blocker\",fixedEnd:\"dtfc-fixed-end\",fixedLeft:\"dtfc-fixed-left\",fixedRight:\"dtfc-fixed-right\",fixedStart:\"dtfc-fixed-start\",tableFixedEnd:\"dtfc-has-end\",tableFixedLeft:\"dtfc-has-left\",tableFixedRight:\"dtfc-has-right\",tableFixedStart:\"dtfc-has-start\",tableScrollingEnd:\"dtfc-scrolling-end\",tableScrollingLeft:\"dtfc-scrolling-left\",tableScrollingRight:\"dtfc-scrolling-right\",tableScrollingStart:\"dtfc-scrolling-start\",topBlocker:\"dtfc-top-blocker\"},l.defaults={i18n:{button:\"FixedColumns\"},start:1,end:0},i=l,r=(o=e).fn.dataTable,e.fn.dataTable.FixedColumns=i,e.fn.DataTable.FixedColumns=i,(a=s.Api.register)(\"fixedColumns()\",function(){return this}),a(\"fixedColumns().start()\",function(e){var t=this.context[0];return void 0!==e?(t._fixedColumns.start(e),this):t._fixedColumns.start()}),a(\"fixedColumns().end()\",function(e){var t=this.context[0];return void 0!==e?(t._fixedColumns.end(e),this):t._fixedColumns.end()}),a(\"fixedColumns().left()\",function(e){var t=this.context[0];return void 0!==e?(t._fixedColumns.left(e),this):t._fixedColumns.left()}),a(\"fixedColumns().right()\",function(e){var t=this.context[0];return void 0!==e?(t._fixedColumns.right(e),this):t._fixedColumns.right()}),s.ext.buttons.fixedColumns={action:function(t,n,o,r){e(o).attr(\"active\")?(e(o).removeAttr(\"active\").removeClass(\"active\"),n.fixedColumns().start(0),n.fixedColumns().end(0)):(e(o).attr(\"active\",\"true\").addClass(\"active\"),n.fixedColumns().start(r.config.start),n.fixedColumns().end(r.config.end))},config:{start:1,end:0},init:function(t,n,o){void 0===t.settings()[0]._fixedColumns&&c(t.settings(),o.config),e(n).attr(\"active\",\"true\").addClass(\"active\"),t.button(n).text(o.text||t.i18n(\"buttons.fixedColumns\",t.settings()[0]._fixedColumns.c.i18n.button))},text:null},e(n).on(\"plugin-init.dt\",function(e,t){\"dt\"!==e.namespace||!t.oInit.fixedColumns&&!s.defaults.fixedColumns||t._fixedColumns||c(t,null)}),s}),\n/*! Bootstrap 5 integration for DataTables' FixedColumns\n * © SpryMedia Ltd - datatables.net/license\n */\nfunction(e){if(\"function\"==typeof define&&define.amd)define([\"jquery\",\"datatables.net-bs5\",\"datatables.net-fixedcolumns\"],function(t){return e(t,window,document)});else if(\"object\"==typeof exports){var t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net-bs5\")(e,t),t.fn.dataTable.FixedColumns||require(\"datatables.net-fixedcolumns\")(e,t)};\"undefined\"==typeof window?module.exports=function(o,r){return o||(o=window),r||(r=t(o)),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))}else e(jQuery,window,document)}(function(e,t,n){\"use strict\";return e.fn.dataTable}),\n/*! FixedHeader 4.0.6\n * © SpryMedia Ltd - datatables.net/license\n */\n(e=>{var t,n;\"function\"==typeof define&&define.amd?define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?(t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net\")(e,t)},\"undefined\"==typeof window?module.exports=function(o,r){return o=o||window,r=r||t(o),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))):e(jQuery,window,document)})(function(e,t,n){function o(n,a){if(!r.versionCheck(\"2\"))throw\"Warning: FixedHeader requires DataTables 2 or newer\";if(!(this instanceof o))throw\"FixedHeader must be initialised with the 'new' keyword.\";if(!0===a&&(a={}),n=new r.Api(n),this.c=e.extend(!0,{},o.defaults,a),this.s={dt:n,position:{theadTop:0,tbodyTop:0,tfootTop:0,tfootBottom:0,width:0,left:0,tfootHeight:0,theadHeight:0,windowHeight:e(t).height(),visible:!0},headerMode:null,footerMode:null,autoWidth:n.settings()[0].oFeatures.bAutoWidth,namespace:\".dtfc\"+i++,scrollLeft:{header:-1,footer:-1},enable:!0,autoDisable:!1},this.dom={floatingHeader:null,thead:e(n.table().header()),tbody:e(n.table().body()),tfoot:e(n.table().footer()),header:{host:null,scrollAdjust:null,floating:null,floatingParent:e('<div class=\"dtfh-floatingparent\"><div class=\"dtfh-floating-limiter\"><div></div></div></div>'),limiter:null,placeholder:null},footer:{host:null,scrollAdjust:null,floating:null,floatingParent:e('<div class=\"dtfh-floatingparent\"><div class=\"dtfh-floating-limiter\"><div></div></div></div>'),limiter:null,placeholder:null}},(a=this.dom).header.host=a.thead.parent(),a.header.limiter=a.header.floatingParent.children(),a.header.scrollAdjust=a.header.limiter.children(),a.footer.host=a.tfoot.parent(),a.footer.limiter=a.footer.floatingParent.children(),a.footer.scrollAdjust=a.footer.limiter.children(),(a=n.settings()[0])._fixedHeader)throw\"FixedHeader already initialised on table \"+a.nTable.id;(a._fixedHeader=this)._constructor()}var r=e.fn.dataTable,i=0;return e.extend(o.prototype,{destroy:function(){var n=this.dom;this.s.dt.off(\".dtfc\"),e(\"body\").off(\".dtfc\"),e(t).off(this.s.namespace),n.header.rightBlocker&&n.header.rightBlocker.remove(),n.header.leftBlocker&&n.header.leftBlocker.remove(),n.footer.rightBlocker&&n.footer.rightBlocker.remove(),n.footer.leftBlocker&&n.footer.leftBlocker.remove(),this.c.header&&this._modeChange(\"in-place\",\"header\",!0),this.c.footer&&n.tfoot.length&&this._modeChange(\"in-place\",\"footer\",!0)},enable:function(e,t,n){this.s.enable=e,this.s.enableType=n,!t&&void 0!==t||(this._positions(),this._scroll(!0))},enabled:function(){return this.s.enable},headerOffset:function(e){return void 0!==e&&(this.c.headerOffset=e,this.update()),this.c.headerOffset},footerOffset:function(e){return void 0!==e&&(this.c.footerOffset=e,this.update()),this.c.footerOffset},update:function(t){var n=this.s.dt.table().node();(this.s.enable||this.s.autoDisable)&&(e(n).is(\":visible\")?(this.s.autoDisable=!1,this.enable(!0,!1)):(this.s.autoDisable=!0,this.enable(!1,!1)),0!==e(n).children(\"thead\").length)&&(this._positions(),this._scroll(void 0===t||t),this._widths(this.dom.header),this._widths(this.dom.footer))},_constructor:function(){var n=this,o=this.s.dt,i=(e(t).on(\"scroll\"+this.s.namespace,function(){n._scroll()}).on(\"resize\"+this.s.namespace,r.util.throttle(function(){n.s.position.windowHeight=e(t).height(),n.update()},50)),e(\".fh-fixedHeader\"));!this.c.headerOffset&&i.length&&(this.c.headerOffset=i.outerHeight()),i=e(\".fh-fixedFooter\");!this.c.footerOffset&&i.length&&(this.c.footerOffset=i.outerHeight()),o.on(\"column-reorder.dt.dtfc column-visibility.dt.dtfc column-sizing.dt.dtfc responsive-display.dt.dtfc\",function(e,t){n.update()}),e(\"body\").on(\"draw.dt.dtfc\",function(e,t){n.update(t!==o.settings()[0])}),o.on(\"destroy.dtfc\",function(){n.destroy()}),this._positions(),this._scroll()},_clone:function(t,n){var o,r,i=this,a=this.s.dt,s=this.dom[t],l=\"header\"===t?this.dom.thead:this.dom.tfoot;if(\"footer\"!==t||!this._scrollEnabled())return n||!s.floating?(s.floating&&(null!==s.placeholder&&s.placeholder.detach(),s.floating.detach()),n=e(a.table().node()),o=e(n.parent()),r=this._scrollEnabled(),s.floating=e(a.table().node().cloneNode(!1)).attr(\"aria-hidden\",\"true\").css({top:0,left:0}).removeAttr(\"id\"),s.floatingParent.css({width:o[0].offsetWidth,position:\"fixed\",left:r?n.offset().left+o.scrollLeft():0}).css(\"header\"===t?{top:this.c.headerOffset,bottom:\"\"}:{top:\"\",bottom:this.c.footerOffset}).addClass(\"footer\"===t?\"dtfh-floatingparent-foot\":\"dtfh-floatingparent-head\").appendTo(\"body\").children().eq(0),s.limiter.css({width:\"100%\",overflow:\"hidden\",height:\"fit-content\"}),s.scrollAdjust.append(s.floating),this._stickyPosition(s.floating,\"-\"),(r=function(){var e=o.scrollLeft();i.s.scrollLeft={footer:e,header:e},s.limiter.scrollLeft(i.s.scrollLeft.header)})(),o.off(\"scroll.dtfh\").on(\"scroll.dtfh\",r),s.scrollAdjust.css({width:\"fit-content\",paddingRight:i.s.dt.settings()[0].oBrowser.barWidth}),(n=e(\"footer\"===t?\"div.dtfc-bottom-blocker\":\"div.dtfc-top-blocker\",a.table().container())).length&&n.clone().appendTo(s.floatingParent).css({position:\"fixed\",right:n.width()}),s.placeholder=l.clone(!1),s.placeholder.find(\"*[id]\").removeAttr(\"id\"),e(s.placeholder).insertAfter(e(\"header\"===t?\"colgroup\":\"tbody\",s.host)),s.floating.append(l),this._widths(s),r):void s.floating.removeClass(\"fixedHeader-floating fixedHeader-locked\")},_stickyPosition:function(t,n){var o;this._scrollEnabled()&&(o=\"rtl\"===e(this.s.dt.table().node()).css(\"direction\"),t.find(\"th\").each(function(){var t,n,r;\"sticky\"===e(this).css(\"position\")&&(t=e(this).css(\"right\"),n=e(this).css(\"left\"),\"auto\"===t||o?\"auto\"!==n&&o&&(r=+n.replace(/px/g,\"\"),e(this).css(\"left\",0<r?r:0)):(r=+t.replace(/px/g,\"\"),e(this).css(\"right\",0<r?r:0)))}))},_horizontal:function(t,n){var o,r=this.dom[t],i=this.s.scrollLeft;r.floating&&i[t]!==n&&(this._scrollEnabled()&&(o=e(e(this.s.dt.table().node()).parent()).scrollLeft(),r.floating.scrollLeft(o),r.floatingParent.scrollLeft(o)),i[t]=n)},_modeChange:function(o,r,i){var a,s,l,c,d,u,f,h,p=this.s.dt,m=this.dom[r],g=this.s.position,v=this._scrollEnabled();\"footer\"===r&&v||(a=function(e){m.floating[0].style.setProperty(\"width\",e+\"px\",\"important\"),v||m.floatingParent[0].style.setProperty(\"width\",e+\"px\",\"important\")},l=this.dom[\"footer\"===r?\"tfoot\":\"thead\"],s=e.contains(l[0],n.activeElement)?n.activeElement:null,u=e(e(this.s.dt.table().node()).parent()),\"in-place\"===o?(m.placeholder&&(m.placeholder.remove(),m.placeholder=null),e.contains(m.host[0],l[0])||(\"header\"===r?l.insertAfter(e(\"colgroup\",m.host)):m.host.append(l)),m.floating&&(m.floating.remove(),m.floating=null,this._stickyPosition(m.host,\"+\")),m.floatingParent&&(m.floatingParent.find(\"div.dtfc-top-blocker\").remove(),m.floatingParent.remove()),e(e(m.host.parent()).parent()).scrollLeft(u.scrollLeft())):\"in\"===o?(l=this._clone(r,i),d=u.offset(),h=(c=e(n).scrollTop())+e(t).height(),f=v?d.top:g.tbodyTop,d=v?d.top+u.outerHeight():g.tfootTop,u=\"footer\"===r?h<f?g.tfootHeight:f+g.tfootHeight-h:c+this.c.headerOffset+g.theadHeight-d,f=\"header\"===r?\"top\":\"bottom\",h=this.c[r+\"Offset\"]-(0<u?u:0),m.floating.addClass(\"fixedHeader-floating\"),m.floatingParent.css(f,h).css({left:g.left,\"z-index\":3}),a(g.width),l&&l(),\"footer\"===r&&m.floating.css(\"top\",\"\")):\"below\"===o?(this._clone(r,i),m.floating.addClass(\"fixedHeader-locked\"),m.floatingParent.css({position:\"absolute\",top:g.tfootTop-g.theadHeight,left:g.left+\"px\"}),a(g.width)):\"above\"===o&&(this._clone(r,i),m.floating.addClass(\"fixedHeader-locked\"),m.floatingParent.css({position:\"absolute\",top:g.tbodyTop,left:g.left+\"px\"}),a(g.width)),s&&s!==n.activeElement&&setTimeout(function(){s.focus()},10),this.s.scrollLeft.header=-1,this.s.scrollLeft.footer=-1,this.s[r+\"Mode\"]=o,p.trigger(\"fixedheader-mode\",[o,r]))},_positions:function(){var t=(a=this.s.dt).table(),n=this.s.position,o=this.dom,r=(t=e(t.node()),this._scrollEnabled()),i=e(a.table().header()),a=e(a.table().footer()),s=(o=o.tbody,t.parent());n.visible=t.is(\":visible\"),n.width=t.outerWidth(),n.left=t.offset().left,n.theadTop=i.offset().top,n.tbodyTop=(r?s:o).offset().top,n.tbodyHeight=(r?s:o).outerHeight(),n.theadHeight=i.outerHeight(),n.theadBottom=n.theadTop+n.theadHeight,n.tfootTop=n.tbodyTop+n.tbodyHeight,a.length?(n.tfootBottom=n.tfootTop+a.outerHeight(),n.tfootHeight=a.outerHeight()):(n.tfootBottom=n.tfootTop,n.tfootHeight=0)},_scroll:function(o){var r,i,a,s,l,c,d,u,f,h,p,m,g,v,b,y;this.s.dt.settings()[0].bDestroying||(r=this._scrollEnabled(),a=(i=e(this.s.dt.table().node()).parent()).offset(),m=i.outerHeight(),s=e(n).scrollLeft(),l=e(n).scrollTop(),c=e(t).height()+l,g=this.s.position,y=r?a.top:g.tbodyTop,u=(r?a:g).left,m=r?a.top+m:g.tfootTop,f=r?i.outerWidth():g.tbodyWidth,this.c.header&&(!this.s.enable||!g.visible||l+this.c.headerOffset+g.theadHeight<=y?h=\"in-place\":l+this.c.headerOffset+g.theadHeight>y&&l+this.c.headerOffset+g.theadHeight<m?(h=\"in\",l+this.c.headerOffset+g.theadHeight>m||void 0===this.dom.header.floatingParent?o=!0:0===(p=this.dom.header.floatingParent.css({top:this.c.headerOffset,position:\"fixed\"}).children().eq(0)).find(this.dom.header.floating).length&&p.append(this.dom.header.floating)):h=\"below\",!o&&h===this.s.headerMode||this._modeChange(h,\"header\",o),this._horizontal(\"header\",s)),v={offset:{top:0,left:0},height:0},b={offset:{top:0,left:0},height:0},this.c.footer&&this.dom.tfoot.length&&this.dom.tfoot.find(\"th, td\").length&&(!this.s.enable||!g.visible||g.tfootBottom+this.c.footerOffset<=c?d=\"in-place\":m+g.tfootHeight+this.c.footerOffset>c&&y+this.c.footerOffset<c?(d=\"in\",o=!0):d=\"above\",!o&&d===this.s.footerMode||this._modeChange(d,\"footer\",o),this._horizontal(\"footer\",s),p=function(e){return{offset:e.offset(),height:e.outerHeight()}},v=this.dom.header.floating?p(this.dom.header.floating):p(this.dom.thead),b=this.dom.footer.floating?p(this.dom.footer.floating):p(this.dom.tfoot),r)&&b.offset.top>l&&(g=c+((m=l-a.top)>-v.height?m:0)-(v.offset.top+(m<-v.height?v.height:0)+b.height),i.outerHeight(g=g<0?0:g),Math.round(i.outerHeight())>=Math.round(g)?e(this.dom.tfoot.parent()).addClass(\"fixedHeader-floating\"):e(this.dom.tfoot.parent()).removeClass(\"fixedHeader-floating\")),this.dom.header.floating&&this.dom.header.floatingParent.css(\"left\",u-s),this.dom.footer.floating&&this.dom.footer.floatingParent.css(\"left\",u-s),void 0!==this.s.dt.settings()[0]._fixedColumns&&(this.dom.header.rightBlocker=(y=function(t,n,o){var r;return null!==(o=void 0===o?0===(r=e(\"div.dtfc-\"+t+\"-\"+n+\"-blocker\")).length?null:r.clone().css(\"z-index\",1):o)&&(\"in\"===h||\"below\"===h?o.appendTo(\"body\").css({top:(\"top\"===n?v:b).offset.top,left:\"right\"===t?u+f-o.width():u}):o.detach()),o})(\"right\",\"top\",this.dom.header.rightBlocker),this.dom.header.leftBlocker=y(\"left\",\"top\",this.dom.header.leftBlocker),this.dom.footer.rightBlocker=y(\"right\",\"bottom\",this.dom.footer.rightBlocker),this.dom.footer.leftBlocker=y(\"left\",\"bottom\",this.dom.footer.leftBlocker)))},_scrollEnabled:function(){var e=this.s.dt.settings()[0].oScroll;return\"\"!==e.sY||\"\"!==e.sX},_widths:function(t){if(t&&t.placeholder)for(var n=e(this.s.dt.table().node()),o=e(n.parent()),r=(t.floatingParent.css(\"width\",o[0].offsetWidth),t.floating.css(\"width\",n[0].offsetWidth),e(\"colgroup\",t.floating).remove(),t.placeholder.parent().find(\"colgroup\").clone().appendTo(t.floating).find(\"col\")),i=this.s.dt.columns(\":visible\").widths(),a=0;a<i.length;a++)r.eq(a).css(\"width\",i[a])}}),o.version=\"4.0.6\",o.defaults={header:!0,footer:!1,headerOffset:0,footerOffset:0},e.fn.dataTable.FixedHeader=o,e.fn.DataTable.FixedHeader=o,e(n).on(\"init.dt.dtfh\",function(t,n,i){var a;\"dt\"===t.namespace&&(t=n.oInit.fixedHeader,a=r.defaults.fixedHeader,t||a)&&!n._fixedHeader&&(a=e.extend({},a,t),!1!==t)&&new o(n,a)}),r.Api.register(\"fixedHeader()\",function(){}),r.Api.register(\"fixedHeader.adjust()\",function(){return this.iterator(\"table\",function(e){(e=e._fixedHeader)&&e.update()})}),r.Api.register(\"fixedHeader.enable()\",function(e){return this.iterator(\"table\",function(t){t=t._fixedHeader,e=void 0===e||e,t&&e!==t.enabled()&&t.enable(e)})}),r.Api.register(\"fixedHeader.enabled()\",function(){if(this.context.length){var e=this.context[0]._fixedHeader;if(e)return e.enabled()}return!1}),r.Api.register(\"fixedHeader.disable()\",function(){return this.iterator(\"table\",function(e){(e=e._fixedHeader)&&e.enabled()&&e.enable(!1)})}),e.each([\"header\",\"footer\"],function(e,t){r.Api.register(\"fixedHeader.\"+t+\"Offset()\",function(e){var n=this.context;return void 0===e?n.length&&n[0]._fixedHeader?n[0]._fixedHeader[t+\"Offset\"]():void 0:this.iterator(\"table\",function(n){(n=n._fixedHeader)&&n[t+\"Offset\"](e)})})}),r}),\n/*! Bootstrap 5 styling wrapper for FixedHeader\n * © SpryMedia Ltd - datatables.net/license\n */\nfunction(e){if(\"function\"==typeof define&&define.amd)define([\"jquery\",\"datatables.net-bs5\",\"datatables.net-fixedheader\"],function(t){return e(t,window,document)});else if(\"object\"==typeof exports){var t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net-bs5\")(e,t),t.fn.dataTable.FixedHeader||require(\"datatables.net-fixedheader\")(e,t)};\"undefined\"==typeof window?module.exports=function(o,r){return o||(o=window),r||(r=t(o)),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))}else e(jQuery,window,document)}(function(e,t,n){\"use strict\";return e.fn.dataTable}),\n/*! Responsive 3.0.8\n * © SpryMedia Ltd - datatables.net/license\n */\n(e=>{var t,n;\"function\"==typeof define&&define.amd?define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?(t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net\")(e,t)},\"undefined\"==typeof window?module.exports=function(o,r){return o=o||window,r=r||t(o),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))):e(jQuery,window,document)})(function(e,t,n){function o(t,n){if(!r.versionCheck||!r.versionCheck(\"2\"))throw\"DataTables Responsive requires DataTables 2 or newer\";this.s={childNodeStore:{},columns:[],current:[],dt:new r.Api(t)},this.s.dt.settings()[0].responsive||(n&&\"string\"==typeof n.details?n.details={type:n.details}:n&&!1===n.details?n.details={type:!1}:n&&!0===n.details&&(n.details={type:\"inline\"}),this.c=e.extend(!0,{},o.defaults,r.defaults.responsive,n),(t.responsive=this)._constructor())}var r=e.fn.dataTable,i=(e.extend(o.prototype,{_constructor:function(){var n=this,o=this.s.dt,i=e(t).innerWidth(),a=(o.settings()[0]._responsive=this,e(t).on(\"orientationchange.dtr\",r.util.throttle(function(){var o=e(t).innerWidth();o!==i&&(n._resize(),i=o)})),o.on(\"row-created.dtr\",function(t,r,i,a){-1!==e.inArray(!1,n.s.current)&&e(\">td, >th\",r).each(function(t){t=o.column.index(\"toData\",t),!1===n.s.current[t]&&e(this).css(\"display\",\"none\").addClass(\"dtr-hidden\")})}),o.on(\"destroy.dtr\",function(){o.off(\".dtr\"),e(o.table().body()).off(\".dtr\"),e(t).off(\"resize.dtr orientationchange.dtr\"),o.cells(\".dtr-control\").nodes().to$().removeClass(\"dtr-control\"),e(o.table().node()).removeClass(\"dtr-inline collapsed\"),e.each(n.s.current,function(e,t){!1===t&&n._setColumnVis(e,!0)})}),this.c.breakpoints.sort(function(e,t){return e.width<t.width?1:e.width>t.width?-1:0}),this._classLogic(),this.c.details);!1!==a.type&&(n._detailsInit(),o.on(\"column-visibility.dtr\",function(){n._timer&&clearTimeout(n._timer),n._timer=setTimeout(function(){n._timer=null,n._classLogic(),n._resizeAuto(),n._resize(!0),n._redrawChildren()},100)}),o.on(\"draw.dtr\",function(){n._redrawChildren()}),e(o.table().node()).addClass(\"dtr-\"+a.type)),o.on(\"column-calc.dt\",function(e,t){for(var o=n.s.current,r=0;r<o.length;r++){var i=t.visible.indexOf(r);!1===o[r]&&0<=i&&t.visible.splice(i,1)}}),o.on(\"preXhr.dtr\",function(){var e=[];o.rows().every(function(){this.child.isShown()&&e.push(this.id(!0))}),o.one(\"draw.dtr\",function(){n._resizeAuto(),n._resize(),o.rows(e).every(function(){n._detailsDisplay(this,!1)})})}),o.on(\"draw.dtr\",function(){o.page.info().serverSide&&(n.s.childNodeStore={}),n._controlClass()}).ready(function(){n._resizeAuto(),n._resize(),o.on(\"column-sizing.dtr\",function(){n._resizeAuto(),n._resize()})}),o.on(\"column-reorder.dtr\",function(e,t,o){n._classLogic(),n._resizeAuto(),n._resize(!0)})},_colGroupAttach:function(e,t,n){var o=null;if(t[n].get(0).parentNode!==e[0]){for(var r=n+1;r<t.length;r++)if(e[0]===t[r].get(0).parentNode){o=r;break}null!==o?t[n].insertBefore(t[o][0]):e.append(t[n])}},_childNodes:function(e,t,n){var o=t+\"-\"+n;if(this.s.childNodeStore[o])return this.s.childNodeStore[o];for(var r=[],i=e.cell(t,n).node().childNodes,a=0,s=i.length;a<s;a++)r.push(i[a]);return this.s.childNodeStore[o]=r},_childNodesRestore:function(e,t,n){var o=t+\"-\"+n;if(this.s.childNodeStore[o]){var r=e.cell(t,n).node();if(0<(e=this.s.childNodeStore[o]).length){for(var i=e[0].parentNode.childNodes,a=[],s=0,l=i.length;s<l;s++)a.push(i[s]);for(var c=0,d=a.length;c<d;c++)r.appendChild(a[c])}this.s.childNodeStore[o]=void 0}},_columnsVisibility:function(t){for(var n=this.s.dt,o=this.s.columns,r=o.map(function(e,t){return{columnIdx:t,priority:e.priority}}).sort(function(e,t){return e.priority!==t.priority?e.priority-t.priority:e.columnIdx-t.columnIdx}),i=e.map(o,function(o,r){return!1===n.column(r).visible()?\"not-visible\":(!o.auto||null!==o.minWidth)&&(!0===o.auto?\"-\":-1!==e.inArray(t,o.includeIn))}),a=0,s=0,l=i.length;s<l;s++)!0===i[s]&&(a+=o[s].minWidth);var c=(c=n.settings()[0].oScroll).sY||c.sX?c.iBarWidth:0,d=n.table().container().offsetWidth-c-a;for(s=0,l=i.length;s<l;s++)o[s].control&&(d-=o[s].minWidth);var u=!1;for(s=0,l=r.length;s<l;s++){var f=r[s].columnIdx;\"-\"===i[f]&&!o[f].control&&o[f].minWidth&&(u||d-o[f].minWidth<0?i[f]=!(u=!0):i[f]=!0,d-=o[f].minWidth)}var h=!1;for(s=0,l=o.length;s<l;s++)if(!o[s].control&&!o[s].never&&!1===i[s]){h=!0;break}for(s=0,l=o.length;s<l;s++)o[s].control&&(i[s]=h),\"not-visible\"===i[s]&&(i[s]=!1);return-1===e.inArray(!0,i)&&(i[0]=!0),i},_classLogic:function(){function t(e,t,a,s){var l,c,d;if(a){if(\"max-\"===a)for(l=n._find(t).width,c=0,d=o.length;c<d;c++)o[c].width<=l&&i(e,o[c].name);else if(\"min-\"===a)for(l=n._find(t).width,c=0,d=o.length;c<d;c++)o[c].width>=l&&i(e,o[c].name);else if(\"not-\"===a)for(c=0,d=o.length;c<d;c++)-1===o[c].name.indexOf(s)&&i(e,o[c].name)}else r[e].includeIn.push(t)}var n=this,o=this.c.breakpoints,r=this.s.dt.columns().eq(0).map(function(e){var t=(e=this.column(e)).header().className,n=e.init().responsivePriority;e=e.header().getAttribute(\"data-priority\");return void 0===n&&(n=null==e?1e4:+e),{className:t,includeIn:[],auto:!1,control:!1,never:!!t.match(/\\b(dtr\\-)?never\\b/),priority:n}}),i=function(t,n){t=r[t].includeIn,-1===e.inArray(n,t)&&t.push(n)};r.each(function(n,r){for(var i=n.className.split(\" \"),a=!1,s=0,l=i.length;s<l;s++){var c=i[s].trim();if(\"all\"===c||\"dtr-all\"===c)return a=!0,void(n.includeIn=e.map(o,function(e){return e.name}));if(\"none\"===c||\"dtr-none\"===c||n.never)return void(a=!0);if(\"control\"===c||\"dtr-control\"===c)return a=!0,void(n.control=!0);e.each(o,function(e,n){var o=n.name.split(\"-\"),i=new RegExp(\"(min\\\\-|max\\\\-|not\\\\-)?(\"+o[0]+\")(\\\\-[_a-zA-Z0-9])?\");(i=c.match(i))&&(a=!0,i[2]===o[0]&&i[3]===\"-\"+o[1]?t(r,n.name,i[1],i[2]+i[3]):i[2]!==o[0]||i[3]||t(r,n.name,i[1],i[2]))})}a||(n.auto=!0)}),this.s.columns=r},_controlClass:function(){var t,n,o;\"inline\"===this.c.details.type&&(t=this.s.dt,n=this.s.current,o=e.inArray(!0,n),t.cells(null,function(e){return e!==o},{page:\"current\"}).nodes().to$().filter(\".dtr-control\").removeClass(\"dtr-control\"),0<=o)&&t.cells(null,o,{page:\"current\"}).nodes().to$().addClass(\"dtr-control\"),this._tabIndexes()},_detailsDisplay:function(t,n){function r(o){e(t.node()).toggleClass(\"dtr-expanded\",!1!==o),e(s.table().node()).triggerHandler(\"responsive-display.dt\",[s,t,o,n])}var i,a=this,s=this.s.dt,l=this.c.details;l&&!1!==l.type&&(i=\"string\"==typeof l.renderer?o.renderer[l.renderer]():l.renderer,\"boolean\"==typeof(l=l.display(t,n,function(){return i.call(a,s,t[0][0],a._detailsObj(t[0]))},function(){r(!1)})))&&r(l)},_detailsInit:function(){var t=this,o=this.s.dt,r=(\"inline\"===(i=this.c.details).type&&(i.target=\"td.dtr-control, th.dtr-control\"),e(o.table().body()).on(\"keyup.dtr\",\"td, th\",function(t){var o=n.activeElement.nodeName.toLowerCase();13!==t.keyCode||!e(this).data(\"dtr-keyboard\")||\"td\"!==o&&\"th\"!==o||e(this).click()}),i.target),i=\"string\"==typeof r?r:\"td, th\";void 0===r&&null===r||e(o.table().body()).on(\"click.dtr mousedown.dtr mouseup.dtr\",i,function(n){if(e(o.table().node()).hasClass(\"collapsed\")&&-1!==e.inArray(e(this).closest(\"tr\").get(0),o.rows().nodes().toArray())){if(\"number\"==typeof r){var i=r<0?o.columns().eq(0).length+r:r;if(o.cell(this).index().column!==i)return}i=o.row(e(this).closest(\"tr\")),\"click\"===n.type?t._detailsDisplay(i,!1):\"mousedown\"===n.type?e(this).css(\"outline\",\"none\"):\"mouseup\"===n.type&&e(this).trigger(\"blur\").css(\"outline\",\"\")}})},_detailsObj:function(t){var n=this,o=this.s.dt,r=[];let i=o.settings()[0];return e.map(this.s.columns,function(e,a){if(!e.never&&!e.control)return e=i.aoColumns[a],r[a]||(r[a]=o.column(a)),{className:e.sClass,columnIndex:a,data:i.fastData(t,a,n.c.orthogonal),hidden:r[a].visible()&&!n.s.current[a],rowIndex:t,title:r[a].title()}})},_find:function(e){for(var t=this.c.breakpoints,n=0,o=t.length;n<o;n++)if(t[n].name===e)return t[n]},_redrawChildren:function(){var e=this,t=this.s.dt;t.rows({page:\"current\"}).iterator(\"row\",function(n,o){e._detailsDisplay(t.row(o),!0)})},_resize:function(n){for(var o,r=this,i=this.s.dt,a=e(t).innerWidth(),s=this.c.breakpoints,l=s[0].name,c=this.s.columns,d=this.s.current.slice(),u=s.length-1;0<=u;u--)if(a<=s[u].width){l=s[u].name;break}var f=this._columnsVisibility(l),h=(this.s.current=f,!1);for(u=0,o=c.length;u<o;u++)if(!1===f[u]&&!c[u].never&&!c[u].control&&0==!i.column(u).visible()){h=!0;break}e(i.table().node()).toggleClass(\"collapsed\",h);var p=!1,m=0,g=i.settings()[0],v=e(i.table().node()).children(\"colgroup\"),b=g.aoColumns.map(function(e){return e.colEl});i.columns().eq(0).each(function(e,t){i.column(e).visible()&&(!0===f[t]&&m++,!n&&f[t]===d[t]||(p=!0,r._setColumnVis(e,f[t])),f[t]?r._colGroupAttach(v,b,t):b[t].detach())}),p&&(i.columns.adjust(),this._redrawChildren(),e(i.table().node()).trigger(\"responsive-resize.dt\",[i,this._responsiveOnlyHidden()]),0===i.page.info().recordsDisplay)&&e(\"td\",i.table().body()).eq(0).attr(\"colspan\",m),r._controlClass()},_resizeAuto:function(){var t=this.s.dt,n=this.s.columns,o=this,r=t.columns().indexes().filter(function(e){return t.column(e).visible()});if(this.c.auto&&-1!==e.inArray(!0,e.map(n,function(e){return e.auto}))){for(var i=t.table().node().cloneNode(!1),a=e(t.table().header().cloneNode(!1)).appendTo(i),s=e(t.table().footer().cloneNode(!1)).appendTo(i),l=e(t.table().body()).clone(!1,!1).empty().appendTo(i),c=(i.style.width=\"auto\",t.table().header.structure(r).forEach(t=>{t=t.filter(function(e){return!!e}).map(function(t){return e(t.cell).clone(!1).css(\"display\",\"table-cell\").css(\"width\",\"auto\").css(\"min-width\",0)}),e(\"<tr/>\").append(t).appendTo(a)}),e(\"<tr/>\").appendTo(l)),d=0;d<r.count();d++)c.append(\"<td/>\");this.c.details.renderer._responsiveMovesNodes?t.rows({page:\"current\"}).every(function(n){var i,a=this.node();a&&(i=a.cloneNode(!1),t.cells(n,r).every(function(t,r){((r=o.s.childNodeStore[n+\"-\"+r])?e(this.node().cloneNode(!1)).append(e(r).clone()):e(this.node()).clone(!1)).appendTo(i)}),l.append(i))}):e(l).append(e(t.rows({page:\"current\"}).nodes()).clone(!1)).find(\"th, td\").css(\"display\",\"\"),l.find(\"th, td\").css(\"display\",\"\"),t.table().footer.structure(r).forEach(t=>{t=t.filter(function(e){return!!e}).map(function(t){return e(t.cell).clone(!1).css(\"display\",\"table-cell\").css(\"width\",\"auto\").css(\"min-width\",0)}),e(\"<tr/>\").append(t).appendTo(s)}),\"inline\"===this.c.details.type&&e(i).addClass(\"dtr-inline collapsed\"),e(i).find(\"[name]\").removeAttr(\"name\"),e(i).css(\"position\",\"relative\"),(i=e(\"<div/>\").css({width:1,height:1,overflow:\"hidden\",clear:\"both\"}).append(i)).insertBefore(t.table().node()),c.children().each(function(e){e=t.column.index(\"fromVisible\",e),n[e].minWidth=this.offsetWidth||0}),i.remove()}},_responsiveOnlyHidden:function(){var t=this.s.dt;return e.map(this.s.current,function(e,n){return!1===t.column(n).visible()||e})},_setColumnVis:function(t,n){var o=this,r=this.s.dt,i=n?\"\":\"none\";this._setHeaderVis(t,n,r.table().header.structure()),this._setHeaderVis(t,n,r.table().footer.structure()),r.column(t).nodes().to$().css(\"display\",i).toggleClass(\"dtr-hidden\",!n),r.settings()[0].aoColumns[t].responsiveVisible=n,e.isEmptyObject(this.s.childNodeStore)||r.cells(null,t).indexes().each(function(e){o._childNodesRestore(r,e.row,e.column)})},_setHeaderVis:function(t,n,o){var r=this,i=n?\"\":\"none\";o.forEach(function(e,t){for(var n=0;n<e.length;n++)if(e[n]&&1<e[n].rowspan)for(var r=e[n].rowspan,i=1;i<r;i++)o[t+i][n]={}}),o.forEach(function(o){if(o[t]&&o[t].cell)e(o[t].cell).css(\"display\",i).toggleClass(\"dtr-hidden\",!n);else for(var a=t;0<=a;){if(o[a]&&o[a].cell){o[a].cell.colSpan=r._colspan(o,a);break}a--}})},_colspan:function(e,t){for(var n=1,o=t+1;o<e.length;o++)if(null===e[o]&&this.s.current[o])n++;else if(e[o])break;return n},_tabIndexes:function(){var t=this.s.dt,n=t.cells({page:\"current\"}).nodes().to$(),o=t.settings()[0],r=this.c.details.target;n.filter(\"[data-dtr-keyboard]\").removeData(\"[data-dtr-keyboard]\"),(\"number\"==typeof r?t.cells(null,r,{page:\"current\"}).nodes().to$():(\"td:first-child, th:first-child\"===r&&(r=\">td:first-child, >th:first-child\"),n=t.rows({page:\"current\"}).nodes(),\"tr\"===r?e(n):e(r,n))).attr(\"tabIndex\",o.iTabIndex).data(\"dtr-keyboard\",1)}}),o.defaults={breakpoints:o.breakpoints=[{name:\"desktop\",width:1/0},{name:\"tablet-l\",width:1024},{name:\"tablet-p\",width:768},{name:\"mobile-l\",width:480},{name:\"mobile-p\",width:320}],auto:!0,details:{display:(o.display={childRow:function(t,n,o){var r=e(t.node());return n?r.hasClass(\"dtr-expanded\")?(t.child(o(),\"child\").show(),!0):void 0:r.hasClass(\"dtr-expanded\")?(t.child(!1),!1):!1!==(n=o())&&(t.child(n,\"child\").show(),!0)},childRowImmediate:function(t,n,o){var r=e(t.node());return!n&&r.hasClass(\"dtr-expanded\")||!t.responsive.hasHidden()?(t.child(!1),!1):!1!==(n=o())&&(t.child(n,\"child\").show(),!0)},modal:function(t){return function(o,r,i,a){if(!1===(i=i()))return!1;if(r){if(!(l=e(\"div.dtr-modal-content\")).length||o.index()!==l.data(\"dtr-row-idx\"))return null;l.empty().append(i)}else{var s=function(){l.remove(),e(n).off(\"keypress.dtr\"),e(o.node()).removeClass(\"dtr-expanded\"),a()},l=e('<div class=\"dtr-modal\"/>').append(e('<div class=\"dtr-modal-display\"/>').append(e('<div class=\"dtr-modal-content\"/>').data(\"dtr-row-idx\",o.index()).append(i)).append(e('<div class=\"dtr-modal-close\">&times;</div>').click(function(){s()}))).append(e('<div class=\"dtr-modal-background\"/>').click(function(){s()})).appendTo(\"body\");e(o.node()).addClass(\"dtr-expanded\"),e(n).on(\"keyup.dtr\",function(e){27===e.keyCode&&(e.stopPropagation(),s())})}return t&&t.header&&e(\"div.dtr-modal-content\").prepend(\"<h2>\"+t.header(o)+\"</h2>\"),!0}}}).childRow,renderer:(o.renderer={listHiddenNodes:function(){function t(t,n,o){var r=this,i=e('<ul data-dtr-index=\"'+n+'\" class=\"dtr-details\"/>'),a=!1;return e.each(o,function(n,o){var s;o.hidden&&(s=o.className?'class=\"'+o.className+'\"':\"\",e(\"<li \"+s+' data-dtr-index=\"'+o.columnIndex+'\" data-dt-row=\"'+o.rowIndex+'\" data-dt-column=\"'+o.columnIndex+'\"><span class=\"dtr-title\">'+o.title+\"</span> </li>\").append(e('<span class=\"dtr-data\"/>').append(r._childNodes(t,o.rowIndex,o.columnIndex))).appendTo(i),a=!0)}),!!a&&i}return t._responsiveMovesNodes=!0,t},listHidden:function(){return function(t,n,o){return o=e.map(o,function(e){var t=e.className?'class=\"'+e.className+'\"':\"\";return e.hidden?\"<li \"+t+' data-dtr-index=\"'+e.columnIndex+'\" data-dt-row=\"'+e.rowIndex+'\" data-dt-column=\"'+e.columnIndex+'\"><span class=\"dtr-title\">'+e.title+'</span> <span class=\"dtr-data\">'+e.data+\"</span></li>\":\"\"}).join(\"\"),!!o&&e('<ul data-dtr-index=\"'+n+'\" class=\"dtr-details\"/>').append(o)}},tableAll:function(t){return t=e.extend({tableClass:\"\"},t),function(n,o,r){return r=e.map(r,function(e){return\"<tr \"+(e.className?'class=\"'+e.className+'\"':\"\")+' data-dt-row=\"'+e.rowIndex+'\" data-dt-column=\"'+e.columnIndex+'\"><td>'+(\"\"!==e.title?e.title+\":\":\"\")+\"</td> <td>\"+e.data+\"</td></tr>\"}).join(\"\"),e('<table class=\"'+t.tableClass+' dtr-details\" width=\"100%\"/>').append(r)}}}).listHidden(),target:0,type:\"inline\"},orthogonal:\"display\"},e.fn.dataTable.Api);return i.register(\"responsive()\",function(){return this}),i.register(\"responsive.index()\",function(t){return{column:(t=e(t)).data(\"dtr-index\"),row:t.parent().data(\"dtr-index\")}}),i.register(\"responsive.rebuild()\",function(){return this.iterator(\"table\",function(e){e._responsive&&e._responsive._classLogic()})}),i.register(\"responsive.recalc()\",function(){return this.iterator(\"table\",function(e){e._responsive&&(e._responsive._resizeAuto(),e._responsive._resize())})}),i.register(\"responsive.hasHidden()\",function(){var t=this.context[0];return!!t._responsive&&-1!==e.inArray(!1,t._responsive._responsiveOnlyHidden())}),i.registerPlural(\"columns().responsiveHidden()\",\"column().responsiveHidden()\",function(){return this.iterator(\"column\",function(e,t){return!!e._responsive&&e._responsive._responsiveOnlyHidden()[t]},1)}),o.version=\"3.0.8\",e.fn.dataTable.Responsive=o,e.fn.DataTable.Responsive=o,e(n).on(\"preInit.dt.dtr\",function(t,n,i){\"dt\"===t.namespace&&(e(n.nTable).hasClass(\"responsive\")||e(n.nTable).hasClass(\"dt-responsive\")||n.oInit.responsive||r.defaults.responsive)&&!1!==(t=n.oInit.responsive)&&new o(n,e.isPlainObject(t)?t:{})}),r}),\n/*! Bootstrap 5 integration for DataTables' Responsive\n * © SpryMedia Ltd - datatables.net/license\n */\n(e=>{var t,n;\"function\"==typeof define&&define.amd?define([\"jquery\",\"datatables.net-bs5\",\"datatables.net-responsive\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?(t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net-bs5\")(e,t),t.fn.dataTable.Responsive||require(\"datatables.net-responsive\")(e,t)},\"undefined\"==typeof window?module.exports=function(o,r){return o=o||window,r=r||t(o),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))):e(jQuery,window,document)})(function(e,t,n){var o,r=e.fn.dataTable,i=r.Responsive.display,a=i.modal,s=e('<div class=\"modal fade dtr-bs-modal\" role=\"dialog\"><div class=\"modal-dialog\" role=\"document\"><div class=\"modal-content\"><div class=\"modal-header\"><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"/></div></div></div>'),l=t.bootstrap;return r.Responsive.bootstrap=function(e){l=e},i.modal=function(t){var i;return!o&&l.Modal&&(i=(()=>{var e=r.use(\"bootstrap\");if(e)return e;if(l)return l;throw new Error(\"No Bootstrap library. Set it with `DataTable.use(bootstrap);`\")})(),o=new i.Modal(s[0])),function(r,i,l,c){if(o){var d,u,f=l();if(!1===f)return!1;if(i){if(!e.contains(n,s[0])||r.index()!==s.data(\"dtr-row-idx\"))return null;s.find(\"div.modal-body\").empty().append(f)}else t&&t.header&&(u=(d=s.find(\"div.modal-header\")).find(\"button\").detach(),d.empty().append('<h4 class=\"modal-title\">'+t.header(r)+\"</h4>\").append(u)),s.find(\"div.modal-body\").empty().append(f),s.data(\"dtr-row-idx\",r.index()).one(\"hidden.bs.modal\",c).appendTo(\"body\"),o.show();return!0}return a(r,i,l,c)}},r}),function(e){var t,n;\"function\"==typeof define&&define.amd?define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?(t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net\")(e,t)},\"undefined\"==typeof window?module.exports=function(o,r){return o=o||window,r=r||t(o),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))):e(jQuery,window,document)}(function(e,t,n){\"use strict\";function o(t,r){this instanceof o?(void 0===r&&(r={}),t=e.fn.dataTable.Api(t),this.s={dt:t.settings()[0],dtApi:t,tableTop:0,tableBottom:0,redrawTop:0,redrawBottom:0,autoHeight:!0,viewportRows:0,stateTO:null,stateSaveThrottle:function(){},drawTO:null,heights:{jump:null,page:null,virtual:null,scroll:null,row:null,viewport:null,labelHeight:0,xbar:0},topRowFloat:0,scrollDrawDiff:null,loaderVisible:!1,forceReposition:!1,baseRowTop:0,baseScrollTop:0,mousedown:!1,lastScrollTop:0},this.s=e.extend(this.s,o.oDefaults,r),this.s.heights.row=this.s.rowHeight,this.dom={force:n.createElement(\"div\"),label:e('<div class=\"dts_label\">0</div>'),scroller:null,table:null,loader:null},this.s.dt.oScroller||(this.s.dt.oScroller=this).construct()):alert(\"Scroller warning: Scroller must be initialised with the 'new' keyword.\")}var r=e.fn.dataTable,i=(e.extend(o.prototype,{measure:function(t){this.s.autoHeight&&this._calcRowHeight();var n=this.s.heights,o=(n.row&&(n.viewport=this._parseHeight(e(this.dom.scroller).css(\"max-height\")),this.s.viewportRows=parseInt(n.viewport/n.row,10)+1,this.s.dt._iDisplayLength=this.s.viewportRows*this.s.displayBuffer),this.dom.label.outerHeight());n.xbar=this.dom.scroller.offsetHeight-this.dom.scroller.clientHeight,n.labelHeight=o,void 0!==t&&!t||this.s.dtApi.draw(!1)},pageInfo:function(){var e=this.s.dt,t=this.dom.scroller.scrollTop,n=(e=e.fnRecordsDisplay(),Math.ceil(this.pixelsToRow(t+this.s.heights.viewport,!1,this.s.ani)));return{start:Math.floor(this.pixelsToRow(t,!1,this.s.ani)),end:e<n?e-1:n-1}},pixelsToRow:function(e,t,n){return e-=this.s.baseScrollTop,n=n?(this._domain(\"physicalToVirtual\",this.s.baseScrollTop)+e)/this.s.heights.row:e/this.s.heights.row+this.s.baseRowTop,t||void 0===t?parseInt(n,10):n},rowToPixels:function(e,t,n){return e-=this.s.baseRowTop,n=n?this._domain(\"virtualToPhysical\",this.s.baseScrollTop):this.s.baseScrollTop,n+=e*this.s.heights.row,t||void 0===t?parseInt(n,10):n},scrollToRow:function(t,n){var o=this,r=!1,i=this.rowToPixels(t),a=t-(this.s.displayBuffer-1)/2*this.s.viewportRows;a<0&&(a=0),void 0===(n=(i>this.s.redrawBottom||i<this.s.redrawTop)&&this.s.dt._iDisplayStart!==a&&(r=!0,i=this._domain(\"virtualToPhysical\",t*this.s.heights.row),this.s.redrawTop<i)&&i<this.s.redrawBottom?!(this.s.forceReposition=!0):n)||n?(this.s.ani=r,e(this.dom.scroller).animate({scrollTop:i},function(){setTimeout(function(){o.s.ani=!1},250)})):e(this.dom.scroller).scrollTop(i)},construct:function(){var n=this,o=this.s.dtApi;if(!this.s.dt.oFeatures.bPaginate)throw new Error(\"Pagination must be enabled for Scroller to operate\");this.dom.force.style.position=\"relative\",this.dom.force.style.top=\"0px\",this.dom.force.style.left=\"0px\",this.dom.force.style.width=\"1px\",this.dom.scroller=o.table().node().parentNode,this.dom.scroller.appendChild(this.dom.force),this.dom.scroller.style.position=\"relative\",this.dom.table=e(\">table\",this.dom.scroller)[0],this.dom.table.style.position=\"absolute\",this.dom.table.style.top=\"0px\",this.dom.table.style.left=\"0px\",e(o.table().container()).addClass(\"dts DTS\"),this.dom.label.appendTo(this.dom.scroller),this.s.heights.row&&\"auto\"!=this.s.heights.row&&(this.s.autoHeight=!1),this.s.ingnoreScroll=!0,e(this.dom.scroller).on(\"scroll.dt-scroller\",function(e){n._scroll.call(n)}),e(this.dom.scroller).on(\"touchstart.dt-scroller\",function(){n._scroll.call(n)}),e(this.dom.scroller).on(\"mousedown.dt-scroller\",function(){n.s.mousedown=!0}).on(\"mouseup.dt-scroller\",function(){n.s.labelVisible=!1,n.s.mousedown=!1,n.dom.label.css(\"display\",\"none\")}),e(t).on(\"resize.dt-scroller\",function(){n.measure(!1),n._info()});var i=!0,a=o.state.loaded();o.on(\"stateSaveParams.scroller\",function(e,t,o){i&&a?(o.scroller=a.scroller,i=!1,o.scroller&&(n.s.lastScrollTop=o.scroller.scrollTop)):o.scroller={topRow:n.s.topRowFloat,baseRowTop:n.s.baseRowTop}}),o.on(\"stateLoadParams.scroller\",function(e,t,o){void 0!==o.scroller&&n.scrollToRow(o.scroller.topRow)}),this.measure(!1),a&&a.scroller&&(this.s.topRowFloat=a.scroller.topRow,this.s.baseRowTop=a.scroller.baseRowTop,this.s.baseScrollTop=this.s.baseRowTop*this.s.heights.row,a.scroller.scrollTop=this._domain(\"physicalToVirtual\",this.s.topRowFloat*this.s.heights.row)),n.s.stateSaveThrottle=r.util.throttle(function(){n.s.dtApi.state.save()},500),o.on(\"init.scroller\",function(){n.measure(!1),n.s.scrollType=\"jump\",n._draw(),o.on(\"draw.scroller\",function(){n._draw()})}),o.on(\"preDraw.dt.scroller\",function(){n._scrollForce()}),o.on(\"destroy.scroller\",function(){e(t).off(\"resize.dt-scroller\"),e(n.dom.scroller).off(\".dt-scroller\"),e(n.s.dt.nTable).off(\".scroller\"),e(n.s.dt.nTableWrapper).removeClass(\"DTS\"),e(\"div.DTS_Loading\",n.dom.scroller.parentNode).remove(),n.dom.table.style.position=\"\",n.dom.table.style.top=\"\",n.dom.table.style.left=\"\"})},_calcRowHeight:function(){var t=(i=this.s.dt).nTable,n=t.cloneNode(!1),o=e(\"<tbody/>\").appendTo(n),i=i.oClasses,a=(i=r.versionCheck(\"2\")?{container:i.container,scroller:i.scrolling.container,body:i.scrolling.body}:{container:i.sWrapper,scroller:i.sScrollWrapper,body:i.sScrollBody},e('<div class=\"'+i.container+' DTS\"><div class=\"'+i.scroller+'\"><div class=\"'+i.body+'\"></div></div></div>')),s=(e(\"tbody tr:lt(4)\",t).clone().appendTo(o),e(\"tr\",o).length);if(1===s)o.prepend(\"<tr><td>&#160;</td></tr>\"),o.append(\"<tr><td>&#160;</td></tr>\");else for(;s<3;s++)o.append(\"<tr><td>&#160;</td></tr>\");e(\"div.\"+i.body,a).append(n),i=this.s.dt.nHolding||t.parentNode,e(i).is(\":visible\")||(i=\"body\"),a.find(\"input\").removeAttr(\"name\"),a.appendTo(i),this.s.heights.row=e(\"tr\",o).eq(1).outerHeight(),a.remove()},_draw:function(){var t=this,n=this.s.heights,o=this.dom.scroller.scrollTop,r=e(this.s.dt.nTable).height(),i=this.s.dt._iDisplayStart,a=this.s.dt._iDisplayLength,s=this.s.dt.fnRecordsDisplay(),l=o+n.viewport,c=(this.s.skip=!0,!this.s.dt.bSorted&&!this.s.dt.bFiltered||0!==i||this.s.dt._drawHold||(this.s.topRowFloat=0),o=\"jump\"===this.s.scrollType?this._domain(\"virtualToPhysical\",this.s.topRowFloat*n.row):o,this.s.baseScrollTop=o,this.s.baseRowTop=this.s.topRowFloat,o-(this.s.topRowFloat-i)*n.row);0===i?c=0:s<=i+a?c=n.scroll-r:c+r<l&&(this.s.baseScrollTop+=(s=l-r)-c+1,c=s),this.dom.table.style.top=c+\"px\",this.s.tableTop=c,this.s.tableBottom=r+this.s.tableTop,i=(o-this.s.tableTop)*this.s.boundaryScale;this.s.redrawTop=o-i,this.s.redrawBottom=o+i>n.scroll-n.viewport-n.row?n.scroll-n.viewport-n.row:o+i,this.s.skip=!1,t.s.ingnoreScroll&&(this.s.dt.oFeatures.bStateSave&&null!==this.s.dt.oLoadedState&&void 0!==this.s.dt.oLoadedState.scroller?((a=!(!this.s.dt.sAjaxSource&&!t.s.dt.ajax||this.s.dt.oFeatures.bServerSide))&&2<=this.s.dt.iDraw||!a&&1<=this.s.dt.iDraw)&&setTimeout(function(){e(t.dom.scroller).scrollTop(t.s.dt.oLoadedState.scroller.scrollTop),setTimeout(function(){t.s.ingnoreScroll=!1},0)},0):t.s.ingnoreScroll=!1),this.s.dt.oFeatures.bInfo&&setTimeout(function(){t._info.call(t)},0),e(this.s.dt.nTable).triggerHandler(\"position.dts.dt\",c)},_domain:function(e,t){var n,o=this.s.heights,r=1e4;return o.virtual===o.scroll||t<r?t:\"virtualToPhysical\"===e&&t>=o.virtual-r?(n=o.virtual-t,o.scroll-n):\"physicalToVirtual\"===e&&t>=o.scroll-r?(n=o.scroll-t,o.virtual-n):(o=r-(n=(o.virtual-r-r)/(o.scroll-r-r))*r,\"virtualToPhysical\"===e?(t-o)/n:n*t+o)},_info:function(){if(this.s.dt.oFeatures.bInfo){var t=this.s.dt,n=this.s.dtApi,o=t.oLanguage,r=(i=n.page.info()).recordsDisplay,i=i.recordsTotal,a=(this.s.lastScrollTop-this.s.baseScrollTop)/this.s.heights.row,s=(a=Math.floor(this.s.baseRowTop+a)+1,r<(s=(a=\"jump\"===this.s.scrollType?Math.floor(this.s.topRowFloat)+1:a)+Math.floor(this.s.heights.viewport/this.s.heights.row))?r:s),l=0===r&&r==i?o.sInfoEmpty+o.sInfoPostFix:0===r?o.sInfoEmpty+\" \"+o.sInfoFiltered+o.sInfoPostFix:r==i?o.sInfo+o.sInfoPostFix:o.sInfo+\" \"+o.sInfoFiltered+o.sInfoPostFix,c=((o=(l=this._macros(l,a,s,i,r),o.fnInfoCallback))&&(l=o.call(t.oInstance,t,a,s,i,r,l)),t.aanFeatures.i);if(void 0!==c){for(var d=0,u=c.length;d<u;d++)e(c[d]).html(l);e(t.nTable).triggerHandler(\"info.dt\")}e(\"div.dt-info\",n.table().container()).each(function(){e(this).html(l),n.trigger(\"info\",[n.settings()[0],this,l])})}},_macros:function(e,t,n,o,r){var i=this.s.dtApi,a=this.s.dt,s=a.fnFormatNumber;return e.replace(/_START_/g,s.call(a,t)).replace(/_END_/g,s.call(a,n)).replace(/_MAX_/g,s.call(a,o)).replace(/_TOTAL_/g,s.call(a,r)).replace(/_ENTRIES_/g,i.i18n(\"entries\",\"\")).replace(/_ENTRIES-MAX_/g,i.i18n(\"entries\",\"\",o)).replace(/_ENTRIES-TOTAL_/g,i.i18n(\"entries\",\"\",r))},_parseHeight:function(n){var o,r;return null!==(n=/^([+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+))(px|em|rem|vh)$/.exec(n))&&(r=parseFloat(n[1]),\"px\"===(n=n[2])?o=r:\"vh\"===n?o=r/100*e(t).height():\"rem\"===n?o=r*parseFloat(e(\":root\").css(\"font-size\")):\"em\"===n&&(o=r*parseFloat(e(\"body\").css(\"font-size\"))),o)||0},_scroll:function(){var t,n=this,o=this.s.heights,i=this.dom.scroller.scrollTop;this.s.skip||this.s.ingnoreScroll||i!==this.s.lastScrollTop&&(this.s.dt.bFiltered||this.s.dt.bSorted?this.s.lastScrollTop=0:(clearTimeout(this.s.stateTO),this.s.stateTO=setTimeout(function(){n.s.dtApi.state.save(),n._info()},250),this.s.scrollType=Math.abs(i-this.s.lastScrollTop)>o.viewport?\"jump\":\"cont\",this.s.topRowFloat=\"cont\"===this.s.scrollType?this.pixelsToRow(i,!1,!1):this._domain(\"physicalToVirtual\",i)/o.row,this.s.topRowFloat<0&&(this.s.topRowFloat=0),this.s.forceReposition||i<this.s.redrawTop||i>this.s.redrawBottom?(t=Math.ceil((this.s.displayBuffer-1)/2*this.s.viewportRows),t=parseInt(this.s.topRowFloat,10)-t,this.s.forceReposition=!1,t<=0?t=0:t+this.s.dt._iDisplayLength>this.s.dt.fnRecordsDisplay()?(t=this.s.dt.fnRecordsDisplay()-this.s.dt._iDisplayLength)<0&&(t=0):t%2!=0&&t++,(this.s.targetTop=t)!=this.s.dt._iDisplayStart&&(this.s.tableTop=e(this.s.dt.nTable).offset().top,this.s.tableBottom=e(this.s.dt.nTable).height()+this.s.tableTop,t=function(){n.s.dt._iDisplayStart=n.s.targetTop,n.s.dtApi.draw(\"page\")},this.s.dt.oFeatures.bServerSide?(this.s.forceReposition=!0,e(this.s.dt.nTable).triggerHandler(\"scroller-will-draw.dt\"),r.versionCheck(\"2\")?n.s.dtApi.processing(!0):this.s.dt.oApi._fnProcessingDisplay(this.s.dt,!0),clearTimeout(this.s.drawTO),this.s.drawTO=setTimeout(t,this.s.serverWait)):t())):this.s.topRowFloat=this.pixelsToRow(i,!1,!0),this._info(),this.s.lastScrollTop=i,this.s.stateSaveThrottle(),\"jump\"===this.s.scrollType&&this.s.mousedown&&(this.s.labelVisible=!0),this.s.labelVisible&&(t=(o.viewport-o.labelHeight-o.xbar)/o.scroll,this.dom.label.html(this.s.dt.fnFormatNumber(parseInt(this.s.topRowFloat,10)+1)).css(\"top\",i+i*t).css(\"display\",\"block\"))))},_scrollForce:function(){var e=this.s.heights;e.virtual=e.row*this.s.dt.fnRecordsDisplay(),e.scroll=e.virtual,1e6<e.scroll&&(e.scroll=1e6),this.dom.force.style.height=e.scroll>this.s.heights.row?e.scroll+\"px\":this.s.heights.row+\"px\"}}),o.oDefaults=o.defaults={boundaryScale:.5,displayBuffer:9,rowHeight:\"auto\",serverWait:200},o.version=\"2.4.3\",e(n).on(\"preInit.dt.dtscroller\",function(t,n){var i,a;\"dt\"===t.namespace&&(t=n.oInit.scroller,i=r.defaults.scroller,t||i)&&(a=e.extend({},t,i),!1!==t)&&new o(n,a)}),e.fn.dataTable.Scroller=o,e.fn.DataTable.Scroller=o,e.fn.dataTable.Api);return i.register(\"scroller()\",function(){return this}),i.register(\"scroller().rowToPixels()\",function(e,t,n){var o=this.context;if(o.length&&o[0].oScroller)return o[0].oScroller.rowToPixels(e,t,n)}),i.register(\"scroller().pixelsToRow()\",function(e,t,n){var o=this.context;if(o.length&&o[0].oScroller)return o[0].oScroller.pixelsToRow(e,t,n)}),i.register([\"scroller().scrollToRow()\",\"scroller.toPosition()\"],function(e,t){return this.iterator(\"table\",function(n){n.oScroller&&n.oScroller.scrollToRow(e,t)}),this}),i.register(\"row().scrollTo()\",function(e){var t=this;return this.iterator(\"row\",function(n,o){n.oScroller&&(o=t.rows({order:\"applied\",search:\"applied\"}).indexes().indexOf(o),n.oScroller.scrollToRow(o,e))}),this}),i.register(\"scroller.measure()\",function(e){return this.iterator(\"table\",function(t){t.oScroller&&t.oScroller.measure(e)}),this}),i.register(\"scroller.page()\",function(){var e=this.context;if(e.length&&e[0].oScroller)return e[0].oScroller.pageInfo()}),r}),\n/*! Select for DataTables 3.1.3\n * © SpryMedia Ltd - datatables.net/license/mit\n */\n(e=>{var t,n;\"function\"==typeof define&&define.amd?define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?(t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net\")(e,t)},\"undefined\"==typeof window?module.exports=function(o,r){return o=o||window,r=r||t(o),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))):e(jQuery,window,document)})(function(e,t,n){var o=e.fn.dataTable;function r(e,t,n){function o(t,n){n<t&&(o=n,n=t,t=o);var o,r=!1;return e.columns(\":visible\").indexes().filter(function(e){return e===t&&(r=!0),e===n?!(r=!1):r})}function r(t,n){var o,r=e.rows({search:\"applied\"}).indexes(),i=(r.indexOf(t)>r.indexOf(n)&&(o=n,n=t,t=o),!1);return r.filter(function(e){return e===t&&(i=!0),e===n?!(i=!1):i})}var i;n=e.cells({selected:!0}).any()||n?(i=o(n.column,t.column),r(n.row,t.row)):(i=o(0,t.column),r(0,t.row)),n=e.cells(n,i).flatten();e.cells(t,{selected:!0}).any()?e.cells(n).deselect():e.cells(n).select()}function i(e){var t=o.select.classes.checkbox;return e?t.replace(/ /g,\".\"):t}function a(t){var n=t.settings()[0]._select.selector;e(t.table().container()).off(\"mousedown.dtSelect\",n).off(\"mouseup.dtSelect\",n).off(\"click.dtSelect\",n),e(\"body\").off(\"click.dtSelect\"+m(t.table().node()))}function s(n){var o,r=e(n.table().container()),i=n.settings()[0],a=i._select.selector;r.on(\"mousedown.dtSelect\",a,function(e){(e.shiftKey||e.metaKey||e.ctrlKey)&&r.css(\"-moz-user-select\",\"none\").one(\"selectstart.dtSelect\",a,function(){return!1}),t.getSelection&&(o=t.getSelection())}).on(\"mouseup.dtSelect\",a,function(){r.css(\"-moz-user-select\",\"\")}).on(\"click.dtSelect\",a,function(r){var i,a=n.select.items();if(o&&((!(c=t.getSelection()).anchorNode||e(c.anchorNode).closest(\"table\")[0]===n.table().node())&&c!==o))return;var s,c=n.settings()[0],d=n.table().container();e(r.target).closest(\"div.dt-container\")[0]==d&&(d=n.cell(e(r.target).closest(\"td, th\"))).any()&&(s=e.Event(\"user-select.dt\"),l(n,s,[a,d,r]),s.isDefaultPrevented()||(s=d.index(),\"row\"===a?(i=s.row,p(r,n,c,\"row\",i)):\"column\"===a?(i=d.index().column,p(r,n,c,\"column\",i)):\"cell\"===a&&(i=d.index(),p(r,n,c,\"cell\",i)),c._select_lastCell=s))}),e(\"body\").on(\"click.dtSelect\"+m(n.table().node()),function(t){var o;!i._select.blurable||e(t.target).parents().filter(n.table().container()).length||0===e(t.target).parents(\"html\").length||e(t.target).parents(\"div.DTE\").length||(o=e.Event(\"select-blur.dt\"),l(n,o,[t.target,t]),o.isDefaultPrevented())||h(i,!0)})}function l(t,n,o,r){r&&!t.flatten().length||(\"string\"==typeof n&&(n+=\".dt\"),o.unshift(t),e(t.table().node()).trigger(n,o))}function c(e){return e.mRender&&\"selectCheckbox\"===e.mRender._name}function d(t,n){if(\"api\"!==t.select.style()&&!1!==t.select.info()){var o=t.settings()[0],r=o._select_set;if(!t.page.info().serverSide)for(var i=r.length-1;0<=i;i--)o.aIds[r[i]]||r.splice(i,1);var a=r.length||t.rows({selected:!0}).count(),s=t.columns({selected:!0}).count(),l=t.cells({selected:!0}).count(),c=(\"subtractive\"===o._select_mode&&(a=t.page.info().recordsDisplay-r.length),function(n,o,r){n.append(e('<span class=\"select-item\"/>').append(t.i18n(\"select.\"+o+\"s\",{_:\"%d \"+o+\"s selected\",0:\"\",1:\"1 \"+o+\" selected\"},r)))}),d=(n=e(n),e('<span class=\"select-info\"/>'));(a=(c(d,\"row\",a),c(d,\"column\",s),c(d,\"cell\",l),n.children(\"span.select-info\"))).length&&a.remove(),\"\"!==d.text()&&n.append(d)}}function u(e,t,n){e.one(\"draw\",function(){e.row(n).node().focus()}).page(t).draw(!1)}function f(e,t,n,o){var r,i=e[t+\"s\"]({search:\"applied\"}).indexes(),a=(o=i.indexOf(o),i.indexOf(n));e[t+\"s\"]({selected:!0}).any()||-1!==o?(a<o&&(r=a,a=o,o=r),i.splice(a+1,i.length),i.splice(0,o)):i.splice(i.indexOf(n)+1,i.length),e[t](n,{selected:!0}).any()?(i.splice(i.indexOf(n),1),e[t+\"s\"](i).deselect()):e[t+\"s\"](i).select()}function h(e,t){!t&&\"single\"!==e._select.style||((t=new o.Api(e)).rows({selected:!0}).deselect(),t.columns({selected:!0}).deselect(),t.cells({selected:!0}).deselect())}function p(e,t,n,o,i){var a=t.select.style(),s=t.select.toggleable(),l=t[o](i,{selected:!0}).any();l&&!s||(\"os\"===a?e.ctrlKey||e.metaKey?t[o](i).select(!l):e.shiftKey?\"cell\"===o?r(t,i,n._select_lastCell||null):f(t,o,i,n._select_lastCell?n._select_lastCell[o]:null):(s=t[o+\"s\"]({selected:!0}),l&&1===s.flatten().length?t[o](i).deselect():(s.deselect(),t[o](i).select())):\"multi+shift\"==a&&e.shiftKey?\"cell\"===o?r(t,i,n._select_lastCell||null):f(t,o,i,n._select_lastCell?n._select_lastCell[o]:null):t[o](i).select(!l))}function m(e){return e.id.replace(/[^a-zA-Z0-9\\-\\_]/g,\"-\")}function g(e,t,n){for(var o=0;o<n.length;o++){var r=e.row(n[o]).id();r&&\"undefined\"!==r&&!t.includes(r)&&t.push(r)}}function v(e,t,n){for(var o=0;o<n.length;o++){var r=e.row(n[o]).id();-1!==(r=t.indexOf(r))&&t.splice(r,1)}}o.select={},o.select.classes={checkbox:\"dt-select-checkbox\"},o.select.version=\"3.1.3\",o.select.init=function(t){var n,r,a,s,l,d,u,f,h,p,m,g,v,b,y,w=t.settings()[0];if(!o.versionCheck(\"2\"))throw\"Warning: Select requires DataTables 2 or newer\";!w._select&&(n=t.state.loaded(),r=function(e,n,o){if(null!==o&&void 0!==o.select){if(t.rows({selected:!0}).any()&&t.rows().deselect(),void 0!==o.select.rows&&t.rows(o.select.rows).select(),t.columns({selected:!0}).any()&&t.columns().deselect(),void 0!==o.select.columns&&t.columns(o.select.columns).select(),t.cells({selected:!0}).any()&&t.cells().deselect(),void 0!==o.select.cells)for(var r=0;r<o.select.cells.length;r++)t.cell(o.select.cells[r].row,o.select.cells[r].column).select();t.state.save()}},t.on(\"stateSaveParams\",function(e,n,o){o.select={},o.select.rows=t.rows({selected:!0}).ids(!0).toArray(),o.select.columns=t.columns({selected:!0})[0],o.select.cells=t.cells({selected:!0})[0].map(function(e){return{row:t.row(e.row).id(!0),column:e.column}})}).on(\"stateLoadParams\",r).one(\"init\",function(){r(0,0,n)}),s=w.oInit.select,a=o.defaults.select,a=void 0===s?a:s,s=\"row\",u=!(d=!(l=\"api\")),p=\"td, th\",m=\"selected\",y=b=v=!(g=h=!(f=null)),w._select={infoEls:[]},!0===a?(l=\"os\",v=!0):\"string\"==typeof a?(l=a,v=!0):e.isPlainObject(a)&&(void 0!==a.blurable&&(d=a.blurable),void 0!==a.toggleable&&(u=a.toggleable),void 0!==a.info&&(h=a.info),void 0!==a.items&&(s=a.items),l=void 0!==a.style?a.style:\"os\",v=!0,void 0!==a.selector&&(p=a.selector),void 0!==a.className&&(m=a.className),void 0!==a.headerCheckbox&&(g=a.headerCheckbox),void 0!==a.selectable&&(f=a.selectable),void 0!==a.keys&&(b=a.keys),void 0!==a.keysWrap)&&(y=a.keysWrap),t.select.selector(p),t.select.items(s),t.select.style(l),t.select.blurable(d),t.select.toggleable(u),t.select.info(h),t.select.keys(b,y),t.select.selectable(f),w._select.className=m,!v&&e(t.table().node()).hasClass(\"selectable\")&&t.select.style(\"os\"),g||\"select-page\"===g||\"select-all\"===g)&&t.ready(function(){var n,o,r;o=g,r=(n=t).settings()[0].aoColumns,n.columns().iterator(\"column\",function(t,a){var s,l;c(r[a])&&(a=n.column(a).header(),(s=e(\"div.dt-column-header\",a)).length&&(a=s),e(\"input\",a).length||(l=e(\"<input>\").attr({class:i(!1),type:\"checkbox\",\"aria-label\":n.i18n(\"select.aria.headerCheckbox\")||\"Select all rows\"}).appendTo(a).on(\"change\",function(){this.checked?(\"select-page\"==o?n.rows({page:\"current\"}):n.rows({search:\"applied\"})).select():(\"select-page\"==o?n.rows({page:\"current\",selected:!0}):n.rows({selected:!0})).deselect()}).on(\"click\",function(e){e.stopPropagation()}),n.on(\"draw select deselect\",function(e,t,r){\"row\"!==r&&r||((r=((e,t)=>{var n=e.settings()[0],o=n._select.selectable,r=0,i=(\"select-page\"==t?e.rows({page:\"current\",selected:!0}):e.rows({selected:!0})).count(),a=(\"select-page\"==t?e.rows({page:\"current\",selected:!0}):e.rows({search:\"applied\",selected:!0})).count();if(o)for(var s=(\"select-page\"==t?e.rows({page:\"current\"}):e.rows({search:\"applied\"})).indexes(),l=0;l<s.length;l++){var c=n.aoData[s[l]];o(c._aData,c.nTr,s[l])&&r++}else r=(\"select-page\"==t?e.rows({page:\"current\"}):e.rows({search:\"applied\"})).count();return{available:r,count:i,search:a}})(n,o)).search&&r.search<=r.count&&r.search===r.available?l.prop(\"checked\",!0).prop(\"indeterminate\",!1):0===r.search&&0===r.count?l.prop(\"checked\",!1).prop(\"indeterminate\",!1):l.prop(\"checked\",!1).prop(\"indeterminate\",!0))})))})})},e.each([{type:\"row\",prop:\"aoData\"},{type:\"column\",prop:\"aoColumns\"}],function(e,t){o.ext.selector[t.type].push(function(e,n,o){var r,i=n.selected,a=[];if(!0!==i&&!1!==i)return o;for(var s=0,l=o.length;s<l;s++)(r=e[t.prop][o[s]])&&(!0===i&&!0===r._select_selected||!1===i&&!r._select_selected)&&a.push(o[s]);return a})}),o.ext.selector.cell.push(function(e,t,n){var o,r=t.selected,i=[];if(void 0===r)return n;for(var a=0,s=n.length;a<s;a++)(o=e.aoData[n[a].row])&&(!0===r&&o._selected_cells&&!0===o._selected_cells[n[a].column]||!1===r&&(!o._selected_cells||!o._selected_cells[n[a].column]))&&i.push(n[a]);return i});var b=o.Api.register,y=o.Api.registerPlural;function w(e,t){return function(n){return n.i18n(\"buttons.\"+e,t)}}function _(e){return\"draw.dt.DT\"+(e=e._eventNamespace)+\" select.dt.DT\"+e+\" deselect.dt.DT\"+e}b(\"select()\",function(){return this.iterator(\"table\",function(e){o.select.init(new o.Api(e))})}),b(\"select.blurable()\",function(e){return void 0===e?this.context[0]._select.blurable:this.iterator(\"table\",function(t){t._select.blurable=e})}),b(\"select.toggleable()\",function(e){return void 0===e?this.context[0]._select.toggleable:this.iterator(\"table\",function(t){t._select.toggleable=e})}),b(\"select.info()\",function(e){return void 0===e?this.context[0]._select.info:this.iterator(\"table\",function(t){t._select.info=e})}),b(\"select.items()\",function(e){return void 0===e?this.context[0]._select.items:this.iterator(\"table\",function(t){t._select.items=e,l(new o.Api(t),\"selectItems\",[e])})}),b(\"select.keys()\",function(t,r){return void 0===t?this.context[0]._select.keys:this.iterator(\"table\",function(i){var a,s,l;i._select||o.select.init(new o.Api(i)),i._select.keys=t,i._select.keysWrap=r,s=(i=(a=new o.Api(i)).settings()[0])._select.keys,l=i._select.keysWrap,i=\"dts-keys-\"+i.sTableId,s?(e(a.rows({page:\"current\"}).nodes()).attr(\"tabindex\",0),a.on(\"draw.\"+i,function(){e(a.rows({page:\"current\"}).nodes()).attr(\"tabindex\",0)}),e(n).on(\"keydown.\"+i,function(e){var t,o,r,i,s=e.keyCode,c=n.activeElement;[9,13,32,38,40].includes(s)&&(o=(t=a.rows({page:\"current\"}).nodes().toArray()).indexOf(c),r=!0,i=a.page.info(),-1!==o)&&(9===s?!1===e.shift&&o===t.length-1?u(a,\"next\",\":first-child\"):!0===e.shift&&0===o?u(a,\"previous\",\":last-child\"):r=!1:13===s||32===s?(c=a.row(c)).selected()?c.deselect():c.select():38===s?0<o?t[o-1].focus():0<i.start?u(a,\"previous\",\":last-child\"):l&&u(a,\"last\",\":last-child\"):o<t.length-1?t[o+1].focus():i.page<i.pages-1?u(a,\"next\",\":first-child\"):l&&u(a,\"first\",\":first-child\"),r)&&(e.stopPropagation(),e.preventDefault())})):(e(a.rows().nodes()).removeAttr(\"tabindex\"),a.off(\"draw.\"+i),e(n).off(\"keydown.\"+i))})}),b(\"select.style()\",function(t){return void 0===t?this.context[0]._select.style:this.iterator(\"table\",function(n){n._select||o.select.init(new o.Api(n)),n._select_init||function(t){var n,r=new o.Api(t);t._select_init=!0,t._select_mode=\"additive\",t._select_set=[],t.aoRowCreatedCallback.push(function(n,o,a){var s,l,c=t.aoData[a];for(a=r.row(a).id(),(c._select_selected||\"additive\"===t._select_mode&&t._select_set.includes(a)||\"subtractive\"===t._select_mode&&!t._select_set.includes(a))&&(c._select_selected=!0,e(n).addClass(t._select.className).find(\"input.\"+i(!0)).prop(\"checked\",!0)),s=0,l=t.aoColumns.length;s<l;s++)(t.aoColumns[s]._select_selected||c._selected_cells&&c._selected_cells[s])&&e(c.anCells[s]).addClass(t._select.className)}),(n=r).on(\"select\",function(e,t,o,r){\"row\"===o&&(\"additive\"===(o=n.settings()[0])._select_mode?g:v)(n,o._select_set,r)}),n.on(\"deselect\",function(e,t,o,r){\"row\"===o&&(\"additive\"===(o=n.settings()[0])._select_mode?v:g)(n,o._select_set,r)}),r.on(\"info.dt\",function(e,t,n){t._select.infoEls.includes(n)||t._select.infoEls.push(n),d(r,n)}),r.on(\"select.dtSelect.dt deselect.dtSelect.dt\",function(){t._select.infoEls.forEach(function(e){d(r,e)}),r.state.save()}),r.on(\"destroy.dtSelect\",function(){e(r.rows({selected:!0}).nodes()).removeClass(r.settings()[0]._select.className),e(\"input.\"+i(!0),r.table().header()).remove(),a(r),r.off(\".dtSelect\"),e(\"body\").off(\".dtSelect\"+m(r.table().node()))})}(n),n._select.style=t;var r=new o.Api(n);\"api\"!==t?r.ready(function(){a(r),s(r)}):a(r),l(new o.Api(n),\"selectStyle\",[t])})}),b(\"select.selector()\",function(e){return void 0===e?this.context[0]._select.selector:this.iterator(\"table\",function(t){var n=new o.Api(t),r=t._select.style;a(n),t._select.selector=e,r&&\"api\"!==r?n.ready(function(){a(n),s(n)}):a(n)})}),b(\"select.selectable()\",function(e){var t=this.context[0];return e?(t._select.selectable=e,this):t._select.selectable}),b(\"select.last()\",function(e){var t=this.context[0];return e?(t._select_lastCell=e,this):t._select_lastCell}),b(\"select.cumulative()\",function(e){var t;return e?this.iterator(\"table\",function(t){var n,r;t._select_mode!==e&&(n=new o.Api(t),\"subtractive\"===e?(r=n.rows({selected:!1}).ids().toArray(),t._select_mode=e,t._select_set.length=0,t._select_set.push.apply(t._select_set,r)):(r=n.rows({selected:!0}).ids().toArray(),t._select_mode=e,t._select_set.length=0,t._select_set.push.apply(t._select_set,r)))}).draw(!1):(t=this.context[0])&&t._select_set?{mode:t._select_mode,rows:t._select_set}:null}),y(\"rows().select()\",\"row().select()\",function(t){var n=this,o=[];return!1===t?this.deselect():(this.iterator(\"row\",function(t,r){h(t);var a=t.aoData[r],s=t.aoColumns;if(!t._select.selectable||!1!==t._select.selectable(a._aData,a.nTr,r)){e(a.nTr).addClass(t._select.className),a._select_selected=!0,o.push(r);for(var l=0;l<s.length;l++){var d=s[l];null===d.sType&&n.columns().types(),c(d)&&((d=a.anCells)&&d[l]&&e(\"input.\"+i(!0),d[l]).prop(\"checked\",!0),null!==a._aSortData)&&(a._aSortData[l]=null)}}}),this.iterator(\"table\",function(e){l(n,\"select\",[\"row\",o],!0)}),this)}),b(\"row().selected()\",function(){var e=this.context[0];return!!(e&&this.length&&e.aoData[this[0]]&&e.aoData[this[0]]._select_selected)}),b(\"row().focus()\",function(){var e=this.context[0];e&&this.length&&e.aoData[this[0]]&&e.aoData[this[0]].nTr&&e.aoData[this[0]].nTr.focus()}),b(\"row().blur()\",function(){var e=this.context[0];e&&this.length&&e.aoData[this[0]]&&e.aoData[this[0]].nTr&&e.aoData[this[0]].nTr.blur()}),y(\"columns().select()\",\"column().select()\",function(t){var n=this;return!1===t?this.deselect():(this.iterator(\"column\",function(t,n){h(t),t.aoColumns[n]._select_selected=!0,n=new o.Api(t).column(n),e(n.header()).addClass(t._select.className),e(n.footer()).addClass(t._select.className),n.nodes().to$().addClass(t._select.className)}),this.iterator(\"table\",function(e,t){l(n,\"select\",[\"column\",n[t]],!0)}),this)}),b(\"column().selected()\",function(){var e=this.context[0];return!!(e&&this.length&&e.aoColumns[this[0]]&&e.aoColumns[this[0]]._select_selected)}),y(\"cells().select()\",\"cell().select()\",function(t){var n=this;return!1===t?this.deselect():(this.iterator(\"cell\",function(t,n,o){h(t),void 0===(n=t.aoData[n])._selected_cells&&(n._selected_cells=[]),n._selected_cells[o]=!0,n.anCells&&e(n.anCells[o]).addClass(t._select.className)}),this.iterator(\"table\",function(e,t){l(n,\"select\",[\"cell\",n.cells(n[t]).indexes().toArray()],!0)}),this)}),b(\"cell().selected()\",function(){var e=this.context[0];return!!(e&&this.length&&(e=e.aoData[this[0][0].row])&&e._selected_cells&&e._selected_cells[this[0][0].column])}),y(\"rows().deselect()\",\"row().deselect()\",function(){var t=this;return this.iterator(\"row\",function(n,o){var r=n.aoData[o],a=n.aoColumns;e(r.nTr).removeClass(n._select.className),r._select_selected=!1,n._select_lastCell=null;for(var s=0;s<a.length;s++){var l=a[s];null===l.sType&&t.columns().types(),c(l)&&((l=r.anCells)&&l[s]&&e(\"input.\"+i(!0),r.anCells[s]).prop(\"checked\",!1),null!==r._aSortData)&&(r._aSortData[s]=null)}}),this.iterator(\"table\",function(e,n){l(t,\"deselect\",[\"row\",t[n]],!0)}),this}),y(\"columns().deselect()\",\"column().deselect()\",function(){var t=this;return this.iterator(\"column\",function(t,n){t.aoColumns[n]._select_selected=!1;var r=new o.Api(t),i=r.column(n);e(i.header()).removeClass(t._select.className),e(i.footer()).removeClass(t._select.className),r.cells(null,n).indexes().each(function(n){var o=t.aoData[n.row],r=o._selected_cells;!o.anCells||r&&r[n.column]||e(o.anCells[n.column]).removeClass(t._select.className)})}),this.iterator(\"table\",function(e,n){l(t,\"deselect\",[\"column\",t[n]],!0)}),this}),y(\"cells().deselect()\",\"cell().deselect()\",function(){var t=this;return this.iterator(\"cell\",function(t,n,o){void 0!==(n=t.aoData[n])._selected_cells&&(n._selected_cells[o]=!1),n.anCells&&!t.aoColumns[o]._select_selected&&e(n.anCells[o]).removeClass(t._select.className)}),this.iterator(\"table\",function(e,n){l(t,\"deselect\",[\"cell\",t[n]],!0)}),this});var x=0;return e.extend(o.ext.buttons,{selected:{text:w(\"selected\",\"Selected\"),className:\"buttons-selected\",limitTo:[\"rows\",\"columns\",\"cells\"],init:function(e,t,n){var o=this;n._eventNamespace=\".select\"+x++,e.on(_(n),function(){var t,r;o.enable((t=e,!(-1===(r=n).limitTo.indexOf(\"rows\")||!t.rows({selected:!0}).any())||!(-1===r.limitTo.indexOf(\"columns\")||!t.columns({selected:!0}).any())||!(-1===r.limitTo.indexOf(\"cells\")||!t.cells({selected:!0}).any())))}),this.disable()},destroy:function(e,t,n){e.off(n._eventNamespace)}},selectedSingle:{text:w(\"selectedSingle\",\"Selected single\"),className:\"buttons-selected-single\",init:function(e,t,n){var o=this;n._eventNamespace=\".select\"+x++,e.on(_(n),function(){var t=e.rows({selected:!0}).flatten().length+e.columns({selected:!0}).flatten().length+e.cells({selected:!0}).flatten().length;o.enable(1===t)}),this.disable()},destroy:function(e,t,n){e.off(n._eventNamespace)}},selectAll:{text:w(\"selectAll\",\"Select all\"),className:\"buttons-select-all\",action:function(e,t,n,o){var r=this.select.items(),i=o.selectorModifier;(i?(\"function\"==typeof i&&(i=i.call(t,e,t,n,o)),this[r+\"s\"](i)):this[r+\"s\"]()).select()}},selectNone:{text:w(\"selectNone\",\"Deselect all\"),className:\"buttons-select-none\",action:function(){h(this.settings()[0],!0)},init:function(e,t,n){var o=this;n._eventNamespace=\".select\"+x++,e.on(_(n),function(){var t=e.rows({selected:!0}).flatten().length+e.columns({selected:!0}).flatten().length+e.cells({selected:!0}).flatten().length;o.enable(0<t)}),this.disable()},destroy:function(e,t,n){e.off(n._eventNamespace)}},showSelected:{text:w(\"showSelected\",\"Show only selected\"),className:\"buttons-show-selected\",action:function(e,t){var n;t.search.fixed(\"dt-select\")?(t.search.fixed(\"dt-select\",null),this.active(!1)):(n=t.settings()[0].aoData,t.search.fixed(\"dt-select\",function(e,t,o){return n[o]._select_selected}),this.active(!0)),t.draw()}}}),e.each([\"Row\",\"Column\",\"Cell\"],function(e,t){var n=t.toLowerCase();o.ext.buttons[\"select\"+t+\"s\"]={text:w(\"select\"+t+\"s\",\"Select \"+n+\"s\"),className:\"buttons-select-\"+n+\"s\",action:function(){this.select.items(n)},init:function(e){var t=this;this.active(e.select.items()===n),e.on(\"selectItems.dt.DT\",function(e,o,r){t.active(r===n)})}}}),o.type(\"select-checkbox\",{className:\"dt-select\",detect:o.versionCheck(\"2.1\")?{oneOf:function(){return!1},allOf:function(){return!1},init:function(e,t,n){return c(t)}}:function(e){return\"select-checkbox\"===e&&e},order:{pre:function(e){return\"X\"===e?-1:0}}}),e.extend(!0,o.defaults.oLanguage,{select:{aria:{rowCheckbox:\"Select row\"}}}),o.render.select=function(t,n){function r(t,n,o,r){var l=r.settings.aoData[r.row],c=l._select_selected,d=r.settings.oLanguage.select.aria.rowCheckbox,u=r.settings._select.selectable;return\"display\"!==n?\"type\"===n?\"select-checkbox\":\"filter\"!==n&&c?\"X\":\"\":u&&!1===u(o,l.nTr,r.row)?\"\":e(\"<input>\").attr({\"aria-label\":d,class:i(),name:s?s(o):null,type:\"checkbox\",value:a?a(o):null,checked:c}).on(\"input\",function(t){t.preventDefault(),this.checked=e(this).closest(\"tr\").hasClass(\"selected\")})[0]}var a=t?o.util.get(t):null,s=n?o.util.get(n):null;return r._name=\"selectCheckbox\",r},o.ext.order[\"select-checkbox\"]=function(t,n){return this.api().column(n,{order:\"index\"}).nodes().map(function(n){return\"row\"===t._select.items?e(n).parent().hasClass(t._select.className).toString():\"cell\"===t._select.items&&e(n).hasClass(t._select.className).toString()})},e.fn.DataTable.select=o.select,e(n).on(\"i18n.dt.dtSelect preInit.dt.dtSelect\",function(e,t){\"dt\"===e.namespace&&o.select.init(new o.Api(t))}),o}),\n/*! DateTime picker for DataTables.net v1.6.3\n *\n * © SpryMedia Ltd, all rights reserved.\n * License: MIT datatables.net/license/mit\n */\n(e=>{var t;\"function\"==typeof define&&define.amd?define([\"jquery\"],function(t){return e(t,window,document)}):\"object\"==typeof exports?(t=require(\"jquery\"),\"undefined\"==typeof window?module.exports=function(n,o){return n=n||window,o=o||t(n),e(o,n,n.document)}:module.exports=e(t,window,window.document)):e(jQuery,window,document)})(function(e,t,n){function o(n,i){if(o.factory(n,i))return o;if(void 0===r&&(r=t.moment||t.dayjs||t.luxon||null),this.c=e.extend(!0,{},o.defaults,i),i=this.c.classPrefix,!r&&\"YYYY-MM-DD\"!==this.c.format)throw\"DateTime: Without momentjs, dayjs or luxon only the format 'YYYY-MM-DD' can be used\";this._isLuxon()&&\"YYYY-MM-DD\"==this.c.format&&(this.c.format=\"yyyy-MM-dd\"),\"string\"==typeof this.c.minDate&&(this.c.minDate=new Date(this.c.minDate)),\"string\"==typeof this.c.maxDate&&(this.c.maxDate=new Date(this.c.maxDate));var a=e('<div class=\"'+i+'\"><div class=\"'+i+'-date\"><div class=\"'+i+'-title\"><div class=\"'+i+'-iconLeft\"><button type=\"button\"></button></div><div class=\"'+i+'-iconRight\"><button type=\"button\"></button></div><div class=\"'+i+'-label\"><span></span><select class=\"'+i+'-month\"></select></div><div class=\"'+i+'-label\"><span></span><select class=\"'+i+'-year\"></select></div></div><div class=\"'+i+'-buttons\"><a class=\"'+i+'-clear\"></a><a class=\"'+i+'-today\"></a><a class=\"'+i+'-selected\"></a></div><div class=\"'+i+'-calendar\"></div></div><div class=\"'+i+'-time\"><div class=\"'+i+'-hours\"></div><div class=\"'+i+'-minutes\"></div><div class=\"'+i+'-seconds\"></div></div><div class=\"'+i+'-error\"></div></div>');this.dom={container:a,date:a.find(\".\"+i+\"-date\"),title:a.find(\".\"+i+\"-title\"),calendar:a.find(\".\"+i+\"-calendar\"),time:a.find(\".\"+i+\"-time\"),error:a.find(\".\"+i+\"-error\"),buttons:a.find(\".\"+i+\"-buttons\"),clear:a.find(\".\"+i+\"-clear\"),today:a.find(\".\"+i+\"-today\"),selected:a.find(\".\"+i+\"-selected\"),previous:a.find(\".\"+i+\"-iconLeft\"),next:a.find(\".\"+i+\"-iconRight\"),input:e(n)},this.s={d:null,display:null,minutesRange:null,secondsRange:null,namespace:\"datetime-\"+o._instance++,parts:{date:null!==this.c.format.match(/[yYMDd]|L(?!T)|l/),time:null!==this.c.format.match(/[Hhm]|LT|LTS/),seconds:-1!==this.c.format.indexOf(\"s\"),hours12:null!==this.c.format.match(/[haA]/)},showTo:null},this.dom.container.append(this.dom.date).append(this.dom.time).append(this.dom.error),this.dom.date.append(this.dom.title).append(this.dom.buttons).append(this.dom.calendar),this.dom.input.addClass(\"dt-datetime\"),this._constructor()}var r;return e.extend(o.prototype,{destroy:function(){clearTimeout(this.s.showTo),this._hide(!0),this.dom.container.off().empty(),this.dom.input.removeClass(\"dt-datetime\").removeAttr(\"autocomplete\").off(\".datetime\")},display:function(e,t){return void 0!==e&&this.s.display.setUTCFullYear(e),void 0!==t&&this.s.display.setUTCMonth(t-1),void 0!==e||void 0!==t?(this._setTitle(),this._setCalander(),this):this.s.display?{month:this.s.display.getUTCMonth()+1,year:this.s.display.getUTCFullYear()}:{month:null,year:null}},errorMsg:function(e){var t=this.dom.error;return e?t.html(e):t.empty(),this},hide:function(){return this._hide(),this},max:function(e){return this.c.maxDate=\"string\"==typeof e?new Date(e):e,this._optionsTitle(),this._setCalander(),this},min:function(e){return this.c.minDate=\"string\"==typeof e?new Date(e):e,this._optionsTitle(),this._setCalander(),this},owns:function(t){return 0<e(t).parents().filter(this.dom.container).length},val:function(e,t){var n;return void 0===e?this.s.d:(n=this.s.d,e instanceof Date?this.s.d=this._dateToUtc(e):null===e||\"\"===e?this.s.d=null:\"--now\"===e?this.s.d=this._dateToUtc(new Date):\"string\"==typeof e&&(this.s.d=this._dateToUtc(this._convert(e,this.c.format,null))),!t&&void 0!==t||(this.s.d?this._writeOutput(!1,null===n&&null!==this.s.d||null!==n&&null===this.s.d||n.toString()!==this.s.d.toString()):this.dom.input.val(e)),this.s.d?this.s.display=new Date(this.s.d.toString()):this.c.display?(this.s.display=new Date,this.s.display.setUTCDate(1),this.display(this.c.display.year,this.c.display.month)):this.s.display=new Date,this.s.display.setUTCDate(1),this._setTitle(),this._setCalander(),this._setTime(),this)},valFormat:function(e,t){return t?(this.val(this._convert(t,e,null)),this):this._convert(this.val(),null,e)},_constructor:function(){function t(){var e=o.dom.input.val();e!==i&&(o.c.onChange.call(o,e,o.s.d,o.dom.input),i=e)}var o=this,r=this.c.classPrefix,i=this.dom.input.val();this.s.parts.date||this.dom.date.css(\"display\",\"none\"),this.s.parts.time||this.dom.time.css(\"display\",\"none\"),this.s.parts.seconds||(this.dom.time.children(\"div.\"+r+\"-seconds\").remove(),this.dom.time.children(\"span\").eq(1).remove()),this.c.buttons.clear||this.dom.clear.css(\"display\",\"none\"),this.c.buttons.today||this.dom.today.css(\"display\",\"none\"),this.c.buttons.selected||this.dom.selected.css(\"display\",\"none\"),this._optionsTitle(),e(n).on(\"i18n.dt\",function(t,n){n.oLanguage.datetime&&(e.extend(!0,o.c.i18n,n.oLanguage.datetime),o._optionsTitle())}),\"hidden\"!==this.dom.input.attr(\"type\")&&!this.c.alwaysVisible||(this.dom.container.addClass(\"inline\"),this.c.attachTo=\"input\",this.val(this.dom.input.val(),!1),this._show()),i&&this.val(i,!1),this.dom.input.attr(\"autocomplete\",\"off\").on(\"focus.datetime click.datetime\",function(){o.dom.container.is(\":visible\")||o.dom.input.is(\":disabled\")||(i=o.dom.input.val(),o.val(i,!1),o._show())}).on(\"keyup.datetime\",function(){o.val(o.dom.input.val(),!1)}),this.dom.container[0].addEventListener(\"focusin\",function(e){e.stopPropagation()}),this.dom.container.on(\"change\",\"select\",function(){var n,i,a=e(this),s=a.val();a.hasClass(r+\"-month\")?(o._correctMonth(o.s.display,s),o._setTitle(),o._setCalander()):a.hasClass(r+\"-year\")?(o.s.display.setUTCFullYear(s),o._setTitle(),o._setCalander()):a.hasClass(r+\"-hours\")||a.hasClass(r+\"-ampm\")?(o.s.parts.hours12?(n=+e(o.dom.container).find(\".\"+r+\"-hours\").val(),i=\"pm\"===e(o.dom.container).find(\".\"+r+\"-ampm\").val(),o.s.d.setUTCHours(12!=n||i?i&&12!=n?12+n:n:0)):o.s.d.setUTCHours(s),o._setTime(),o._writeOutput(!0),t()):a.hasClass(r+\"-minutes\")?(o.s.d.setUTCMinutes(s),o._setTime(),o._writeOutput(!0),t()):a.hasClass(r+\"-seconds\")&&(o.s.d.setSeconds(s),o._setTime(),o._writeOutput(!0),t()),o.dom.input.focus(),o._position()}).on(\"click\",function(n){var i=o.s.d,a=(s=\"span\"===n.target.nodeName.toLowerCase()?n.target.parentNode:n.target).nodeName.toLowerCase();if(\"select\"!==a)if(n.stopPropagation(),\"a\"===a&&(n.preventDefault(),e(s).hasClass(r+\"-clear\")?(o.s.d=null,o.dom.input.val(\"\"),o._writeOutput(),o._setCalander(),o._setTime(),t()):e(s).hasClass(r+\"-today\")?(o.s.display=new Date,o._setTitle(),o._setCalander()):e(s).hasClass(r+\"-selected\")&&(o.s.display=new Date(o.s.d.getTime()),o._setTitle(),o._setCalander())),\"button\"===a)if((a=(n=e(s)).parent()).hasClass(\"disabled\")&&!a.hasClass(\"range\"))n.blur();else if(a.hasClass(r+\"-iconLeft\"))o.s.display.setUTCMonth(o.s.display.getUTCMonth()-1),o._setTitle(),o._setCalander(),o.dom.input.focus();else if(a.hasClass(r+\"-iconRight\"))o._correctMonth(o.s.display,o.s.display.getUTCMonth()+1),o._setTitle(),o._setCalander(),o.dom.input.focus();else{if(n.parents(\".\"+r+\"-time\").length){var s=n.data(\"value\"),l=n.data(\"unit\");i=o._needValue();if(\"minutes\"===l){if(a.hasClass(\"disabled\")&&a.hasClass(\"range\"))return o.s.minutesRange=s,void o._setTime();o.s.minutesRange=null}if(\"seconds\"===l){if(a.hasClass(\"disabled\")&&a.hasClass(\"range\"))return o.s.secondsRange=s,void o._setTime();o.s.secondsRange=null}if(\"am\"===s){if(!(12<=i.getUTCHours()))return;s=i.getUTCHours()-12}else if(\"pm\"===s){if(!(i.getUTCHours()<12))return;s=i.getUTCHours()+12}i[\"hours\"===l?\"setUTCHours\":\"minutes\"===l?\"setUTCMinutes\":\"setSeconds\"](s),o._setCalander(),o._setTime(),o._writeOutput(!0)}else(i=o._needValue()).setUTCDate(1),i.setUTCFullYear(n.data(\"year\")),i.setUTCMonth(n.data(\"month\")),i.setUTCDate(n.data(\"day\")),o._writeOutput(!0),o.s.parts.time?(o._setCalander(),o._setTime()):setTimeout(function(){o._hide()},10);t()}else o.dom.input.focus()})},_compareDates:function(e,t){return this._isLuxon()?r.DateTime.fromJSDate(e).toUTC().toISODate()===r.DateTime.fromJSDate(t).toUTC().toISODate():this._dateToUtcString(e)===this._dateToUtcString(t)},_convert:function(e,t,n){var o;return e&&(r?this._isLuxon()?(o=e instanceof Date?r.DateTime.fromJSDate(e).toUTC():r.DateTime.fromFormat(e,t)).isValid?n?o.toFormat(n):o.toJSDate():null:(o=e instanceof Date?r.utc(e,void 0,this.c.locale,this.c.strict):r(e,t,this.c.locale,this.c.strict)).isValid()?n?o.format(n):o.toDate():null:!t&&!n||t&&n?e:t?(o=e.match(/(\\d{4})\\-(\\d{2})\\-(\\d{2})/))?new Date(o[1],o[2]-1,o[3]):null:e.getUTCFullYear()+\"-\"+this._pad(e.getUTCMonth()+1)+\"-\"+this._pad(e.getUTCDate()))},_correctMonth:function(e,t){var n=this._daysInMonth(e.getUTCFullYear(),t),o=e.getUTCDate()>n;e.setUTCMonth(t),o&&(e.setUTCDate(n),e.setUTCMonth(t))},_daysInMonth:function(e,t){return[31,e%4!=0||e%100==0&&e%400!=0?28:29,31,30,31,30,31,31,30,31,30,31][t]},_dateToUtc:function(e){return e&&new Date(Date.UTC(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds()))},_dateToUtcString:function(e){return this._isLuxon()?r.DateTime.fromJSDate(e).toUTC().toISODate():e.getUTCFullYear()+\"-\"+this._pad(e.getUTCMonth()+1)+\"-\"+this._pad(e.getUTCDate())},_hide:function(o){o||\"hidden\"!==this.dom.input.attr(\"type\")&&!this.c.alwaysVisible?(o=this.s.namespace,this.dom.container.detach(),e(t).off(\".\"+o),e(n).off(\"keydown.\"+o).off(\"keyup.\"+o).off(\"click.\"+o),e(\"div.dataTables_scrollBody\").off(\"scroll.\"+o),e(\"div.DTE_Body_Content\").off(\"scroll.\"+o),e(this.dom.input[0].offsetParent).off(\".\"+o)):(this._setCalander(),this._setTime())},_hours24To12:function(e){return 0===e?12:12<e?e-12:e},_htmlDay:function(e){var t,n=this.c.classPrefix;return e.empty?'<td class=\"'+n+'-empty\"></td>':(t=[\"selectable\"],e.disabled&&t.push(\"disabled\"),e.today&&t.push(\"now\"),e.selected&&t.push(\"selected\"),'<td data-day=\"'+e.day+'\" class=\"'+t.join(\" \")+'\"><button class=\"'+n+\"-button \"+n+'-day\" type=\"button\" data-year=\"'+e.year+'\" data-month=\"'+e.month+'\" data-day=\"'+e.day+'\"><span>'+e.day+\"</span></button></td>\")},_htmlMonth:function(t,n){for(var o=this._dateToUtc(new Date),r=this._daysInMonth(t,n),i=new Date(Date.UTC(t,n,1)).getUTCDay(),a=[],s=[],l=(0<this.c.firstDay&&(i-=this.c.firstDay)<0&&(i+=7),r+i),c=l;7<c;)c-=7;l+=7-c;var d=this.c.minDate,u=this.c.maxDate;d&&(d.setUTCHours(0),d.setUTCMinutes(0),d.setSeconds(0)),u&&(u.setUTCHours(23),u.setUTCMinutes(59),u.setSeconds(59));for(var f=0,h=0;f<l;f++){var p=new Date(Date.UTC(t,n,f-i+1)),m=!!this.s.d&&this._compareDates(p,this.s.d),g=this._compareDates(p,o),v=f<i||r+i<=f,b=d&&p<d||u&&u<p,y=this.c.disableDays;m={day:f-i+1,month:n,year:t,selected:m,today:g,disabled:b=!!(Array.isArray(y)&&-1!==e.inArray(p.getUTCDay(),y)||\"function\"==typeof y&&!0===y(p))||b,empty:v};s.push(this._htmlDay(m)),7==++h&&(this.c.showWeekNumber&&s.unshift(this._htmlWeekOfYear(f-i,n,t)),a.push(\"<tr>\"+s.join(\"\")+\"</tr>\"),s=[],h=0)}var w,_=this.c.classPrefix,x=_+\"-table\";return this.c.showWeekNumber&&(x+=\" weekNumber\"),d&&(w=d>=new Date(Date.UTC(t,n,1,0,0,0)),this.dom.title.find(\"div.\"+_+\"-iconLeft\").css(\"display\",w?\"none\":\"block\")),u&&(w=u<new Date(Date.UTC(t,n+1,1,0,0,0)),this.dom.title.find(\"div.\"+_+\"-iconRight\").css(\"display\",w?\"none\":\"block\")),'<table class=\"'+x+'\"><thead>'+this._htmlMonthHead()+\"</thead><tbody>\"+a.join(\"\")+\"</tbody></table>\"},_htmlMonthHead:function(){var e=[],t=this.c.firstDay,n=this.c.i18n;this.c.showWeekNumber&&e.push(\"<th></th>\");for(var o=0;o<7;o++)e.push(\"<th>\"+(e=>{for(e+=t;7<=e;)e-=7;return n.weekdays[e]})(o)+\"</th>\");return e.join(\"\")},_htmlWeekOfYear:function(e,t,n){return(t=new Date(n,t,e,0,0,0,0)).setDate(t.getDate()+4-(t.getDay()||7)),e=new Date(n,0,1),n=Math.ceil(((t-e)/864e5+1)/7),'<td class=\"'+this.c.classPrefix+'-week\">'+n+\"</td>\"},_isLuxon:function(){return!!(r&&r.DateTime&&r.Duration&&r.Settings)},_needValue:function(){return this.s.d||(this.s.d=this._dateToUtc(new Date),this.s.parts.time)||(this.s.d.setUTCHours(0),this.s.d.setUTCMinutes(0),this.s.d.setSeconds(0),this.s.d.setMilliseconds(0)),this.s.d},_options:function(e,t,n){n=n||t;var o=this.dom.container.find(\"select.\"+this.c.classPrefix+\"-\"+e);o.empty();for(var r=0,i=t.length;r<i;r++)o.append('<option value=\"'+t[r]+'\">'+n[r]+\"</option>\")},_optionSet:function(e,t){var n=(e=this.dom.container.find(\"select.\"+this.c.classPrefix+\"-\"+e)).parent().children(\"span\");e.val(t),t=e.find(\"option:selected\");n.html(0!==t.length?t.text():this.c.i18n.unknown)},_optionsTime:function(t,n,o,r,i){var a,s=this.c.classPrefix,l=this.dom.container.find(\"div.\"+s+\"-\"+t),c=12===n?function(e){return e}:this._pad,d=s+\"-table\",u=this.c.i18n;if(l.length){var f=\"\",h=10,p=function(i,a,l){12===n&&\"number\"==typeof i&&(12<=o&&(i+=12),12==i?i=0:24==i&&(i=12));var c=o===i||\"am\"===i&&o<12||\"pm\"===i&&12<=o?\"selected\":\"\";return\"number\"==typeof i&&r&&-1===e.inArray(i,r)&&(c+=\" disabled\"),l&&(c+=\" \"+l),'<td class=\"selectable '+c+'\"><button class=\"'+s+\"-button \"+s+'-day\" type=\"button\" data-unit=\"'+t+'\" data-value=\"'+i+'\"><span>'+a+\"</span></button></td>\"};if(12===n){for(f+=\"<tr>\",a=1;a<=6;a++)f+=p(a,c(a));for(f=(f+=p(\"am\",u.amPm[0]))+\"</tr><tr>\",a=7;a<=12;a++)f+=p(a,c(a));f=f+p(\"pm\",u.amPm[1])+\"</tr>\",h=7}else{if(24===n)for(var m=0,g=0;g<4;g++){for(f+=\"<tr>\",a=0;a<6;a++)f+=p(m,c(m)),m++;f+=\"</tr>\"}else{for(f+=\"<tr>\",g=0;g<60;g+=10)f+=p(g,c(g),\"range\");f=f+'</tr></tbody></thead><table class=\"'+d+\" \"+d+'-nospace\"><tbody>';var v=null!==i?i:-1===o?0:10*Math.floor(o/10);for(f+=\"<tr>\",g=v+1;g<v+10;g++)f+=p(g,c(g));f+=\"</tr>\"}h=6}l.empty().append('<table class=\"'+d+'\"><thead><tr><th colspan=\"'+h+'\">'+u[t]+\"</th></tr></thead><tbody>\"+f+\"</tbody></table>\")}},_optionsTitle:function(){var e=this.c.i18n,t=this.c.minDate,n=this.c.maxDate;t=t?t.getFullYear():null,n=n?n.getFullYear():null,t=null!==t?t:(new Date).getFullYear()-this.c.yearRange,n=null!==n?n:(new Date).getFullYear()+this.c.yearRange;this._options(\"month\",this._range(0,11),e.months),this._options(\"year\",this._range(t,n)),this.dom.today.text(e.today).text(e.today),this.dom.selected.text(e.selected).text(e.selected),this.dom.clear.text(e.clear).text(e.clear),this.dom.previous.attr(\"title\",e.previous).children(\"button\").text(e.previous),this.dom.next.attr(\"title\",e.next).children(\"button\").text(e.next)},_pad:function(e){return e<10?\"0\"+e:e},_position:function(){var n,o,r,i=\"input\"===this.c.attachTo?this.dom.input.position():this.dom.input.offset(),a=this.dom.container,s=this.dom.input.outerHeight();a.hasClass(\"inline\")?a.insertAfter(this.dom.input):(this.s.parts.date&&this.s.parts.time&&550<e(t).width()?a.addClass(\"horizontal\"):a.removeClass(\"horizontal\"),\"input\"===this.c.attachTo?a.css({top:i.top+s,left:i.left}).insertAfter(this.dom.input):a.css({top:i.top+s,left:i.left}).appendTo(\"body\"),n=a.outerHeight(),o=a.outerWidth(),r=e(t).scrollTop(),i.top+s+n-r>e(t).height()&&(s=i.top-n,a.css(\"top\",s<0?0:s)),o+i.left>e(t).width()&&(r=e(t).width()-o-5,\"input\"===this.c.attachTo&&(r-=e(a).offsetParent().offset().left),a.css(\"left\",r<0?0:r)))},_range:function(e,t,n){var o=[];n=n||1;for(var r=e;r<=t;r+=n)o.push(r);return o},_setCalander:function(){this.s.display&&this.dom.calendar.empty().append(this._htmlMonth(this.s.display.getUTCFullYear(),this.s.display.getUTCMonth()))},_setTitle:function(){this._optionSet(\"month\",this.s.display.getUTCMonth()),this._optionSet(\"year\",this.s.display.getUTCFullYear())},_setTime:function(){function e(e){return t.c[e+\"Available\"]||t._range(0,59,t.c[e+\"Increment\"])}var t=this,n=this.s.d,o=null,i=null!=(o=this._isLuxon()?r.DateTime.fromJSDate(n).toUTC():o)?o.hour:n?n.getUTCHours():-1;this._optionsTime(\"hours\",this.s.parts.hours12?12:24,i,this.c.hoursAvailable),this._optionsTime(\"minutes\",60,null!=o?o.minute:n?n.getUTCMinutes():-1,e(\"minutes\"),this.s.minutesRange),this._optionsTime(\"seconds\",60,null!=o?o.second:n?n.getSeconds():-1,e(\"seconds\"),this.s.secondsRange)},_show:function(){var o=this,r=this.s.namespace,i=(this._position(),e(t).on(\"scroll.\"+r+\" resize.\"+r,function(){o._position()}),e(\"div.DTE_Body_Content\").on(\"scroll.\"+r,function(){o._position()}),e(\"div.dataTables_scrollBody\").on(\"scroll.\"+r,function(){o._position()}),this.dom.input[0].offsetParent);i!==n.body&&e(i).on(\"scroll.\"+r,function(){o._position()}),e(n).on(\"keydown.\"+r,function(e){!o.dom.container.is(\":visible\")||9!==e.keyCode&&13!==e.keyCode||o._hide()}),e(n).on(\"keyup.\"+r,function(e){o.dom.container.is(\":visible\")&&27===e.keyCode&&(e.preventDefault(),o._hide())}),clearTimeout(this.s.showTo),this.dom.input.on(\"blur\",function(e){o.s.showTo=setTimeout(function(){var e=n.activeElement.tagName.toLowerCase();n.activeElement===o.dom.input[0]||o.dom.container.find(n.activeElement).length||[\"input\",\"select\",\"button\"].includes(e)&&o.hide()},10)}),setTimeout(function(){e(n).on(\"click.\"+r,function(t){e(t.target).parents().filter(o.dom.container).length||t.target===o.dom.input[0]||o._hide()})},10)},_writeOutput:function(e,t){var n=this.s.d,o=\"\",r=this.dom.input;n&&(o=this._convert(n,null,this.c.format)),r.val(o),void 0!==t&&!t||(n=new Event(\"change\",{bubbles:!0}),r[0].dispatchEvent(n)),\"hidden\"===r.attr(\"type\")&&this.val(o,!1),e&&r.focus()}}),o.use=function(e){r=e},o._instance=0,o.type=\"DateTime\",o.defaults={alwaysVisible:!1,attachTo:\"body\",buttons:{clear:!1,selected:!1,today:!1},classPrefix:\"dt-datetime\",disableDays:null,firstDay:1,format:\"YYYY-MM-DD\",hoursAvailable:null,i18n:{clear:\"Clear\",previous:\"Previous\",next:\"Next\",months:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],weekdays:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],amPm:[\"am\",\"pm\"],hours:\"Hour\",minutes:\"Minute\",seconds:\"Second\",unknown:\"-\",today:\"Today\",selected:\"Selected\"},maxDate:null,minDate:null,minutesAvailable:null,minutesIncrement:1,strict:!0,locale:\"en\",onChange:function(){},secondsAvailable:null,secondsIncrement:1,showWeekNumber:!1,yearRange:25},o.version=\"1.6.3\",o.factory=function(o,r){var i=!1;return o&&o.document&&(n=(t=o).document),r&&r.fn&&r.fn.jquery&&(e=r,i=!0),i},t.DateTime||(t.DateTime=o),t.DataTable&&(t.DataTable.DateTime=o,DataTable.use(\"datetime\",o)),e.fn.dtDateTime=function(e){return this.each(function(){new o(this,e)})},e.fn.dataTable&&(e.fn.dataTable.DateTime=o,e.fn.DataTable.DateTime=o,e.fn.dataTable.Editor)&&(e.fn.dataTable.Editor.DateTime=o),o}),\n/*! © SpryMedia Ltd, Matthew Hasbach - datatables.net/license */\nfunction(e){if(\"function\"==typeof define&&define.amd)define([\"jquery\",\"datatables.net\"],function(t){return e(t,window,document)});else if(\"object\"==typeof exports){var t=require(\"jquery\"),n=function(e,t){t.fn.dataTable||require(\"datatables.net\")(e,t)};\"undefined\"==typeof window?module.exports=function(o,r){return o||(o=window),r||(r=t(o)),n(o,r),e(r,o,o.document)}:(n(window,t),module.exports=e(t,window,window.document))}else e(jQuery,window,document)}(function(e,t,n){\"use strict\";var o=e.fn.dataTable;\n/**\n * @summary     ConditionalPaging\n * @description Hide paging controls when the amount of pages is <= 1\n * @version     1.0.0\n * @author      Matthew Hasbach (https://github.com/mjhasbach)\n * @copyright   Copyright 2015 Matthew Hasbach\n *\n * License      MIT - http://datatables.net/license/mit\n *\n * This feature plugin for DataTables hides paging controls when the amount\n * of pages is <= 1. The controls can either appear / disappear or fade in / out\n *\n * @example\n *    $('#myTable').DataTable({\n *        conditionalPaging: true\n *    });\n *\n * @example\n *    $('#myTable').DataTable({\n *        conditionalPaging: {\n *            style: 'fade',\n *            speed: 500 // optional\n *        }\n *    });\n */return e(n).on(\"init.dt\",function(t,n){if(\"dt\"===t.namespace){var r=n.oInit.conditionalPaging||o.defaults.conditionalPaging;if(e.isPlainObject(r)||!0===r){var i=e.isPlainObject(r)?r:{},a=new o.Api(n),s=500,l=function(t){var n=e(a.table().container()).find(\"div.dt-paging\"),o=a.page.info().pages;t instanceof e.Event?o<=1?\"fade\"===i.style?n.stop().fadeTo(s,0):n.css(\"visibility\",\"hidden\"):\"fade\"===i.style?n.stop().fadeTo(s,1):n.css(\"visibility\",\"\"):o<=1&&(\"fade\"===i.style?n.css(\"opacity\",0):n.css(\"visibility\",\"hidden\"))};void 0!==i.speed&&(s=i.speed),l(null),a.on(\"draw.dt\",l)}}}),o});"
  },
  {
    "path": "static/assets/plugins/custom/flatpickr/l10n/de.js",
    "content": "(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.de = {}));\n}(this, (function (exports) { 'use strict';\n\n  var fp = typeof window !== \"undefined\" && window.flatpickr !== undefined\n      ? window.flatpickr\n      : {\n          l10ns: {},\n      };\n  var German = {\n      weekdays: {\n          shorthand: [\"So\", \"Mo\", \"Di\", \"Mi\", \"Do\", \"Fr\", \"Sa\"],\n          longhand: [\n              \"Sonntag\",\n              \"Montag\",\n              \"Dienstag\",\n              \"Mittwoch\",\n              \"Donnerstag\",\n              \"Freitag\",\n              \"Samstag\",\n          ],\n      },\n      months: {\n          shorthand: [\n              \"Jan\",\n              \"Feb\",\n              \"Mär\",\n              \"Apr\",\n              \"Mai\",\n              \"Jun\",\n              \"Jul\",\n              \"Aug\",\n              \"Sep\",\n              \"Okt\",\n              \"Nov\",\n              \"Dez\",\n          ],\n          longhand: [\n              \"Januar\",\n              \"Februar\",\n              \"März\",\n              \"April\",\n              \"Mai\",\n              \"Juni\",\n              \"Juli\",\n              \"August\",\n              \"September\",\n              \"Oktober\",\n              \"November\",\n              \"Dezember\",\n          ],\n      },\n      firstDayOfWeek: 1,\n      weekAbbreviation: \"KW\",\n      rangeSeparator: \" bis \",\n      scrollTitle: \"Zum Ändern scrollen\",\n      toggleTitle: \"Zum Umschalten klicken\",\n      time_24hr: true,\n  };\n  fp.l10ns.de = German;\n  var de = fp.l10ns;\n\n  exports.German = German;\n  exports.default = de;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n})));\n"
  },
  {
    "path": "static/assets/plugins/custom/flatpickr/l10n/es.js",
    "content": "(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.es = {}));\n}(this, (function (exports) { 'use strict';\n\n  var fp = typeof window !== \"undefined\" && window.flatpickr !== undefined\n      ? window.flatpickr\n      : {\n          l10ns: {},\n      };\n  var Spanish = {\n      weekdays: {\n          shorthand: [\"Dom\", \"Lun\", \"Mar\", \"Mié\", \"Jue\", \"Vie\", \"Sáb\"],\n          longhand: [\n              \"Domingo\",\n              \"Lunes\",\n              \"Martes\",\n              \"Miércoles\",\n              \"Jueves\",\n              \"Viernes\",\n              \"Sábado\",\n          ],\n      },\n      months: {\n          shorthand: [\n              \"Ene\",\n              \"Feb\",\n              \"Mar\",\n              \"Abr\",\n              \"May\",\n              \"Jun\",\n              \"Jul\",\n              \"Ago\",\n              \"Sep\",\n              \"Oct\",\n              \"Nov\",\n              \"Dic\",\n          ],\n          longhand: [\n              \"Enero\",\n              \"Febrero\",\n              \"Marzo\",\n              \"Abril\",\n              \"Mayo\",\n              \"Junio\",\n              \"Julio\",\n              \"Agosto\",\n              \"Septiembre\",\n              \"Octubre\",\n              \"Noviembre\",\n              \"Diciembre\",\n          ],\n      },\n      ordinal: function () {\n          return \"º\";\n      },\n      firstDayOfWeek: 1,\n      rangeSeparator: \" a \",\n      time_24hr: true,\n  };\n  fp.l10ns.es = Spanish;\n  var es = fp.l10ns;\n\n  exports.Spanish = Spanish;\n  exports.default = es;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n})));\n"
  },
  {
    "path": "static/assets/plugins/custom/flatpickr/l10n/fr.js",
    "content": "(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.fr = {}));\n}(this, (function (exports) { 'use strict';\n\n  var fp = typeof window !== \"undefined\" && window.flatpickr !== undefined\n      ? window.flatpickr\n      : {\n          l10ns: {},\n      };\n  var French = {\n      firstDayOfWeek: 1,\n      weekdays: {\n          shorthand: [\"dim\", \"lun\", \"mar\", \"mer\", \"jeu\", \"ven\", \"sam\"],\n          longhand: [\n              \"dimanche\",\n              \"lundi\",\n              \"mardi\",\n              \"mercredi\",\n              \"jeudi\",\n              \"vendredi\",\n              \"samedi\",\n          ],\n      },\n      months: {\n          shorthand: [\n              \"janv\",\n              \"févr\",\n              \"mars\",\n              \"avr\",\n              \"mai\",\n              \"juin\",\n              \"juil\",\n              \"août\",\n              \"sept\",\n              \"oct\",\n              \"nov\",\n              \"déc\",\n          ],\n          longhand: [\n              \"janvier\",\n              \"février\",\n              \"mars\",\n              \"avril\",\n              \"mai\",\n              \"juin\",\n              \"juillet\",\n              \"août\",\n              \"septembre\",\n              \"octobre\",\n              \"novembre\",\n              \"décembre\",\n          ],\n      },\n      ordinal: function (nth) {\n          if (nth > 1)\n              return \"\";\n          return \"er\";\n      },\n      rangeSeparator: \" au \",\n      weekAbbreviation: \"Sem\",\n      scrollTitle: \"Défiler pour augmenter la valeur\",\n      toggleTitle: \"Cliquer pour basculer\",\n      time_24hr: true,\n  };\n  fp.l10ns.fr = French;\n  var fr = fp.l10ns;\n\n  exports.French = French;\n  exports.default = fr;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n})));\n"
  },
  {
    "path": "static/assets/plugins/custom/flatpickr/l10n/it.js",
    "content": "(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.it = {}));\n}(this, (function (exports) { 'use strict';\n\n  var fp = typeof window !== \"undefined\" && window.flatpickr !== undefined\n      ? window.flatpickr\n      : {\n          l10ns: {},\n      };\n  var Italian = {\n      weekdays: {\n          shorthand: [\"Dom\", \"Lun\", \"Mar\", \"Mer\", \"Gio\", \"Ven\", \"Sab\"],\n          longhand: [\n              \"Domenica\",\n              \"Lunedì\",\n              \"Martedì\",\n              \"Mercoledì\",\n              \"Giovedì\",\n              \"Venerdì\",\n              \"Sabato\",\n          ],\n      },\n      months: {\n          shorthand: [\n              \"Gen\",\n              \"Feb\",\n              \"Mar\",\n              \"Apr\",\n              \"Mag\",\n              \"Giu\",\n              \"Lug\",\n              \"Ago\",\n              \"Set\",\n              \"Ott\",\n              \"Nov\",\n              \"Dic\",\n          ],\n          longhand: [\n              \"Gennaio\",\n              \"Febbraio\",\n              \"Marzo\",\n              \"Aprile\",\n              \"Maggio\",\n              \"Giugno\",\n              \"Luglio\",\n              \"Agosto\",\n              \"Settembre\",\n              \"Ottobre\",\n              \"Novembre\",\n              \"Dicembre\",\n          ],\n      },\n      firstDayOfWeek: 1,\n      ordinal: function () { return \"°\"; },\n      rangeSeparator: \" al \",\n      weekAbbreviation: \"Se\",\n      scrollTitle: \"Scrolla per aumentare\",\n      toggleTitle: \"Clicca per cambiare\",\n      time_24hr: true,\n  };\n  fp.l10ns.it = Italian;\n  var it = fp.l10ns;\n\n  exports.Italian = Italian;\n  exports.default = it;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n})));\n"
  },
  {
    "path": "static/assets/plugins/custom/flatpickr/l10n/zh.js",
    "content": "(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.zh = {}));\n}(this, (function (exports) { 'use strict';\n\n  var fp = typeof window !== \"undefined\" && window.flatpickr !== undefined\n      ? window.flatpickr\n      : {\n          l10ns: {},\n      };\n  var Mandarin = {\n      weekdays: {\n          shorthand: [\"周日\", \"周一\", \"周二\", \"周三\", \"周四\", \"周五\", \"周六\"],\n          longhand: [\n              \"星期日\",\n              \"星期一\",\n              \"星期二\",\n              \"星期三\",\n              \"星期四\",\n              \"星期五\",\n              \"星期六\",\n          ],\n      },\n      months: {\n          shorthand: [\n              \"一月\",\n              \"二月\",\n              \"三月\",\n              \"四月\",\n              \"五月\",\n              \"六月\",\n              \"七月\",\n              \"八月\",\n              \"九月\",\n              \"十月\",\n              \"十一月\",\n              \"十二月\",\n          ],\n          longhand: [\n              \"一月\",\n              \"二月\",\n              \"三月\",\n              \"四月\",\n              \"五月\",\n              \"六月\",\n              \"七月\",\n              \"八月\",\n              \"九月\",\n              \"十月\",\n              \"十一月\",\n              \"十二月\",\n          ],\n      },\n      rangeSeparator: \" 至 \",\n      weekAbbreviation: \"周\",\n      scrollTitle: \"滚动切换\",\n      toggleTitle: \"点击切换 12/24 小时时制\",\n  };\n  fp.l10ns.zh = Mandarin;\n  var zh = fp.l10ns;\n\n  exports.Mandarin = Mandarin;\n  exports.default = zh;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n})));\n"
  },
  {
    "path": "static/assets/plugins/custom/formrepeater/formrepeater.bundle.js",
    "content": "var identity=function(t){return t},isArray=function(t){return $.isArray(t)},isObject=function(t){return!isArray(t)&&t instanceof Object},isNumber=function(t){return t instanceof Number},isFunction=function(t){return t instanceof Function},indexOf=function(t,e){return $.inArray(e,t)},inArray=function(t,e){return-1!==indexOf(t,e)},foreach=function(t,e){for(var n in t)t.hasOwnProperty(n)&&e(t[n],n,t)},last=function(t){return t[t.length-1]},argumentsToArray=function(t){return Array.prototype.slice.call(t)},extend=function(){var t={};return foreach(argumentsToArray(arguments),function(e){foreach(e,function(e,n){t[n]=e})}),t},mapToArray=function(t,e){var n=[];return foreach(t,function(t,r,i){n.push(e(t,r,i))}),n},mapToObject=function(t,e,n){var r={};return foreach(t,function(t,i,a){i=n?n(i,t):i,r[i]=e(t,i,a)}),r},map=function(t,e,n){return isArray(t)?mapToArray(t,e):mapToObject(t,e,n)},pluck=function(t,e){return map(t,function(t){return t[e]})},filter=function(t,e){var n;return isArray(t)?(n=[],foreach(t,function(t,r,i){e(t,r,i)&&n.push(t)})):(n={},foreach(t,function(t,r,i){e(t,r,i)&&(n[r]=t)})),n},call=function(t,e,n){return map(t,function(t,r){return t[e].apply(t,n||[])})},throttle=function(t,e){var n=null;return function(){var r=arguments;null===n&&(n=setTimeout(function(){n=null},t),e.apply(this,r))}},mixinPubSub=function(t){var e={};return(t=t||{}).publish=function(t,n){foreach(e[t],function(t){t(n)})},t.subscribe=function(t,n){e[t]=e[t]||[],e[t].push(n)},t.unsubscribe=function(t){foreach(e,function(e){var n=indexOf(e,t);-1!==n&&e.splice(n,1)})},t};!function(t){\"use strict\";var e=function(t,e){var n,r=mixinPubSub(),i=t.$;return r.getType=function(){throw'implement me (return type. \"text\", \"radio\", etc.)'},r.$=function(t){return t?i.find(t):i},r.disable=function(){r.$().prop(\"disabled\",!0),r.publish(\"isEnabled\",!1)},r.enable=function(){r.$().prop(\"disabled\",!1),r.publish(\"isEnabled\",!0)},e.equalTo=function(t,e){return t===e},e.publishChange=function(t,i){var a=r.get();e.equalTo(a,n)||r.publish(\"change\",{e:t,domElement:i}),n=a},r},n=function(t,n){var r=e(t,n);return r.get=function(){return r.$().val()},r.set=function(t){r.$().val(t)},r.clear=function(){r.set(\"\")},n.buildSetter=function(t){return function(e){t.call(r,e)}},r},r=function(t,e){t=isArray(t)?t:[t],e=isArray(e)?e:[e];var n=!0;return t.length!==e.length?n=!1:foreach(t,function(t){inArray(e,t)||(n=!1)}),n},i=function(t){var e={},r=n(t,e);return r.getType=function(){return\"button\"},r.$().on(\"change\",function(t){e.publishChange(t,this)}),r},a=function(e){var i={},a=n(e,i);return a.getType=function(){return\"checkbox\"},a.get=function(){var e=[];return a.$().filter(\":checked\").each(function(){e.push(t(this).val())}),e},a.set=function(e){e=isArray(e)?e:[e],a.$().each(function(){t(this).prop(\"checked\",!1)}),foreach(e,function(t){a.$().filter('[value=\"'+t+'\"]').prop(\"checked\",!0)})},i.equalTo=r,a.$().change(function(t){i.publishChange(t,this)}),a},u=function(t){var e=m(t,{});return e.getType=function(){return\"email\"},e},c=function(n){var r={},i=e(n,r);return i.getType=function(){return\"file\"},i.get=function(){return last(i.$().val().split(\"\\\\\"))},i.clear=function(){this.$().each(function(){t(this).wrap(\"<form>\").closest(\"form\").get(0).reset(),t(this).unwrap()})},i.$().change(function(t){r.publishChange(t,this)}),i},o=function(t){var e={},r=n(t,e);return r.getType=function(){return\"hidden\"},r.$().change(function(t){e.publishChange(t,this)}),r},f=function(n){var r={},i=e(n,r);return i.getType=function(){return\"file[multiple]\"},i.get=function(){var t,e=i.$().get(0).files||[],n=[];for(t=0;t<(e.length||0);t+=1)n.push(e[t].name);return n},i.clear=function(){this.$().each(function(){t(this).wrap(\"<form>\").closest(\"form\").get(0).reset(),t(this).unwrap()})},i.$().change(function(t){r.publishChange(t,this)}),i},s=function(t){var e={},i=n(t,e);return i.getType=function(){return\"select[multiple]\"},i.get=function(){return i.$().val()||[]},i.set=function(t){i.$().val(\"\"===t?[]:isArray(t)?t:[t])},e.equalTo=r,i.$().change(function(t){e.publishChange(t,this)}),i},l=function(t){var e=m(t,{});return e.getType=function(){return\"password\"},e},p=function(e){var r={},i=n(e,r);return i.getType=function(){return\"radio\"},i.get=function(){return i.$().filter(\":checked\").val()||null},i.set=function(e){e?i.$().filter('[value=\"'+e+'\"]').prop(\"checked\",!0):i.$().each(function(){t(this).prop(\"checked\",!1)})},i.$().change(function(t){r.publishChange(t,this)}),i},h=function(t){var e={},r=n(t,e);return r.getType=function(){return\"range\"},r.$().change(function(t){e.publishChange(t,this)}),r},d=function(t){var e={},r=n(t,e);return r.getType=function(){return\"select\"},r.$().change(function(t){e.publishChange(t,this)}),r},m=function(t){var e={},r=n(t,e);return r.getType=function(){return\"text\"},r.$().on(\"change keyup keydown\",function(t){e.publishChange(t,this)}),r},v=function(t){var e={},r=n(t,e);return r.getType=function(){return\"textarea\"},r.$().on(\"change keyup keydown\",function(t){e.publishChange(t,this)}),r},y=function(t){var e=m(t,{});return e.getType=function(){return\"url\"},e},g=function(e){var n={},r=e.$,g=e.constructorOverride||{button:i,text:m,url:y,email:u,password:l,range:h,textarea:v,select:d,\"select[multiple]\":s,radio:p,checkbox:a,file:c,\"file[multiple]\":f,hidden:o},$=function(e,i){(isObject(i)?i:r.find(i)).each(function(){var r=t(this).attr(\"name\");n[r]=g[e]({$:t(this)})})},b=function(e,i){var a=[],u=isObject(i)?i:r.find(i);isObject(i)?n[u.attr(\"name\")]=g[e]({$:u}):(u.each(function(){-1===indexOf(a,t(this).attr(\"name\"))&&a.push(t(this).attr(\"name\"))}),foreach(a,function(t){n[t]=g[e]({$:r.find('input[name=\"'+t+'\"]')})}))};return r.is(\"input, select, textarea\")?r.is('input[type=\"button\"], button, input[type=\"submit\"]')?$(\"button\",r):r.is(\"textarea\")?$(\"textarea\",r):r.is('input[type=\"text\"]')||r.is(\"input\")&&!r.attr(\"type\")?$(\"text\",r):r.is('input[type=\"password\"]')?$(\"password\",r):r.is('input[type=\"email\"]')?$(\"email\",r):r.is('input[type=\"url\"]')?$(\"url\",r):r.is('input[type=\"range\"]')?$(\"range\",r):r.is(\"select\")?r.is(\"[multiple]\")?$(\"select[multiple]\",r):$(\"select\",r):r.is('input[type=\"file\"]')?r.is(\"[multiple]\")?$(\"file[multiple]\",r):$(\"file\",r):r.is('input[type=\"hidden\"]')?$(\"hidden\",r):r.is('input[type=\"radio\"]')?b(\"radio\",r):r.is('input[type=\"checkbox\"]')?b(\"checkbox\",r):$(\"text\",r):($(\"button\",'input[type=\"button\"], button, input[type=\"submit\"]'),$(\"text\",'input[type=\"text\"]'),$(\"password\",'input[type=\"password\"]'),$(\"email\",'input[type=\"email\"]'),$(\"url\",'input[type=\"url\"]'),$(\"range\",'input[type=\"range\"]'),$(\"textarea\",\"textarea\"),$(\"select\",\"select:not([multiple])\"),$(\"select[multiple]\",\"select[multiple]\"),$(\"file\",'input[type=\"file\"]:not([multiple])'),$(\"file[multiple]\",'input[type=\"file\"][multiple]'),$(\"hidden\",'input[type=\"hidden\"]'),b(\"radio\",'input[type=\"radio\"]'),b(\"checkbox\",'input[type=\"checkbox\"]')),n};t.fn.inputVal=function(e){var n=t(this),r=g({$:n});return n.is(\"input, textarea, select\")?void 0===e?r[n.attr(\"name\")].get():(r[n.attr(\"name\")].set(e),n):void 0===e?call(r,\"get\"):(foreach(e,function(t,e){r[e].set(t)}),n)},t.fn.inputOnChange=function(e){var n=t(this),r=g({$:n});return foreach(r,function(t){t.subscribe(\"change\",function(t){e.call(t.domElement,t.e)})}),n},t.fn.inputDisable=function(){var e=t(this);return call(g({$:e}),\"disable\"),e},t.fn.inputEnable=function(){var e=t(this);return call(g({$:e}),\"enable\"),e},t.fn.inputClear=function(){var e=t(this);return call(g({$:e}),\"clear\"),e}}(jQuery),$.fn.repeaterVal=function(){var t,e,n=function(t){if(1===t.length&&(0===t[0].key.length||1===t[0].key.length&&!t[0].key[0]))return t[0].val;foreach(t,function(t){t.head=t.key.shift()});var e,r=function(){var e={};return foreach(t,function(t){e[t.head]||(e[t.head]=[]),e[t.head].push(t)}),e}();return/^[0-9]+$/.test(t[0].head)?(e=[],foreach(r,function(t){e.push(n(t))})):(e={},foreach(r,function(t,r){e[r]=n(t)})),e};return n((t=$(this).inputVal(),e=[],foreach(t,function(t,n){var r=[];\"undefined\"!==n&&(r.push(n.match(/^[^\\[]*/)[0]),r=r.concat(map(n.match(/\\[[^\\]]*\\]/g),function(t){return t.replace(/[\\[\\]]/g,\"\")})),e.push({val:t,key:r}))}),e))},$.fn.repeater=function(t){var e;return t=t||{},$(this).each(function(){var n=$(this),r=t.show||function(){$(this).show()},i=t.hide||function(t){t()},a=n.find(\"[data-repeater-list]\").first(),u=function(t,e){return t.filter(function(){return!e||0===$(this).closest(pluck(e,\"selector\").join(\",\")).length})},c=function(){return u(a.find(\"[data-repeater-item]\"),t.repeaters)},o=a.find(\"[data-repeater-item]\").first().clone().hide(),f=u(u($(this).find(\"[data-repeater-item]\"),t.repeaters).first().find(\"[data-repeater-delete]\"),t.repeaters);t.isFirstItemUndeletable&&f&&f.remove();var s=function(){var e=a.data(\"repeater-list\");return t.$parent?t.$parent.data(\"item-name\")+\"[\"+e+\"]\":e},l=function(e){t.repeaters&&e.each(function(){var e=$(this);foreach(t.repeaters,function(t){e.find(t.selector).repeater(extend(t,{$parent:e}))})})},p=function(t,e,n){t&&foreach(t,function(t){n.call(e.find(t.selector)[0],t)})},h=function(t,e,n){t.each(function(t){var r=$(this);r.data(\"item-name\",e+\"[\"+t+\"]\"),u(r.find(\"[name]\"),n).each(function(){var i=$(this),a=i.attr(\"name\").match(/\\[[^\\]]+\\]/g),c=a?last(a).replace(/\\[|\\]/g,\"\"):i.attr(\"name\"),o=e+\"[\"+t+\"][\"+c+\"]\"+(i.is(\":checkbox\")||i.attr(\"multiple\")?\"[]\":\"\");i.attr(\"name\",o),p(n,r,function(n){var r=$(this);h(u(r.find(\"[data-repeater-item]\"),n.repeaters||[]),e+\"[\"+t+\"][\"+r.find(\"[data-repeater-list]\").first().data(\"repeater-list\")+\"]\",n.repeaters)})})}),a.find(\"input[name][checked]\").removeAttr(\"checked\").prop(\"checked\",!0)};h(c(),s(),t.repeaters),l(c()),t.initEmpty&&c().remove(),t.ready&&t.ready(function(){h(c(),s(),t.repeaters)});var d,m=(d=function(e,n,r){if(n||t.defaultValues){var i={};u(e.find(\"[name]\"),r).each(function(){var t=$(this).attr(\"name\").match(/\\[([^\\]]*)(\\]|\\]\\[\\])$/)[1];i[t]=$(this).attr(\"name\")}),e.inputVal(map(filter(n||t.defaultValues,function(t,e){return i[e]}),identity,function(t){return i[t]}))}p(r,e,function(t){var e=$(this);u(e.find(\"[data-repeater-item]\"),t.repeaters).each(function(){var r=e.find(\"[data-repeater-list]\").data(\"repeater-list\");if(n&&n[r]){var i=$(this).clone();e.find(\"[data-repeater-item]\").remove(),foreach(n[r],function(n){var r=i.clone();d(r,n,t.repeaters||[]),e.find(\"[data-repeater-list]\").append(r)})}else d($(this),t.defaultValues,t.repeaters||[])})})},function(e,n){a.append(e),h(c(),s(),t.repeaters),e.find(\"[name]\").each(function(){$(this).inputClear()}),d(e,n||t.defaultValues,t.repeaters)}),v=function(e){var n=o.clone();m(n,e),t.repeaters&&l(n),r.call(n.get(0))};e=function(t){c().remove(),foreach(t,v)},u(n.find(\"[data-repeater-create]\"),t.repeaters).click(function(){v()}),a.on(\"click\",\"[data-repeater-delete]\",function(){var e=$(this).closest(\"[data-repeater-item]\").get(0);i.call(e,function(){$(e).remove(),h(c(),s(),t.repeaters)})})}),this.setList=e,this};"
  },
  {
    "path": "static/assets/plugins/global/plugins.bundle.css",
    "content": ".select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container[dir=rtl] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline;list-style:none;padding:0}.select2-container .select2-selection--multiple .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;margin-left:5px;padding:0;max-width:100%;resize:none;height:18px;vertical-align:bottom;font-family:sans-serif;overflow:hidden;word-break:keep-all}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:#fff;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option--selectable{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff}.select2-hidden-accessible{border:0!important;clip:rect(0 0 0 0)!important;-webkit-clip-path:inset(50%)!important;clip-path:inset(50%)!important;height:1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;white-space:nowrap!important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:700;height:26px;margin-right:20px;padding-right:0}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir=rtl] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir=rtl] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:#fff;border:1px solid #aaa;border-radius:4px;cursor:text;padding-bottom:5px;padding-right:5px;position:relative}.select2-container--default .select2-selection--multiple.select2-selection--clearable{padding-right:25px}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;font-weight:700;height:20px;margin-right:10px;margin-top:5px;position:absolute;right:0;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:inline-block;margin-left:5px;margin-top:5px;padding:0;padding-left:20px;position:relative;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap}.select2-container--default .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-right:1px solid #aaa;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#999;cursor:pointer;font-size:1em;font-weight:700;padding:0 4px;position:absolute;left:0;top:0}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus,.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{background-color:#f1f1f1;color:#333;outline:0}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice__remove{border-left:1px solid #aaa;border-right:none;border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__clear{float:left;margin-left:10px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid #000 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple,.select2-container--default.select2-container--open.select2-container--above .select2-selection--single{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple,.select2-container--default.select2-container--open.select2-container--below .select2-selection--single{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:0 0;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--group{padding:0}.select2-container--default .select2-results__option--disabled{color:#999}.select2-container--default .select2-results__option--selected{background-color:#ddd}.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable{background-color:#5897fb;color:#fff}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top,#fff 50%,#eee 100%);background-image:-o-linear-gradient(top,#fff 50%,#eee 100%);background-image:linear-gradient(to bottom,#fff 50%,#eee 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:700;height:26px;margin-right:20px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top,#eee 50%,#ccc 100%);background-image:-o-linear-gradient(top,#eee 50%,#ccc 100%);background-image:linear-gradient(to bottom,#eee 50%,#ccc 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir=rtl] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir=rtl] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:0 0;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top,#fff 0,#eee 50%);background-image:-o-linear-gradient(top,#fff 0,#eee 50%);background-image:linear-gradient(to bottom,#fff 0,#eee 50%);background-repeat:repeat-x}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top,#eee 50%,#fff 100%);background-image:-o-linear-gradient(top,#eee 50%,#fff 100%);background-image:linear-gradient(to bottom,#eee 50%,#fff 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--multiple{background-color:#fff;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0;padding-bottom:5px;padding-right:5px}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;display:inline-block;margin-left:5px;margin-top:5px;padding:0}.select2-container--classic .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#888;cursor:pointer;font-size:1em;font-weight:700;padding:0 4px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555;outline:0}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice__remove{border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option--group{padding:0}.select2-container--classic .select2-results__option--disabled{color:grey}.select2-container--classic .select2-results__option--highlighted.select2-results__option--selectable{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb}.flatpickr-calendar{background:0 0;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,.08)}.flatpickr-calendar.inline,.flatpickr-calendar.open{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1);animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px)}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none!important;box-shadow:none!important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasTime .dayContainer,.flatpickr-calendar .hasWeeks .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:after,.flatpickr-calendar:before{position:absolute;display:block;pointer-events:none;border:solid transparent;content:\"\";height:0;width:0;left:22px}.flatpickr-calendar.arrowRight:after,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.rightMost:before{left:auto;right:22px}.flatpickr-calendar.arrowCenter:after,.flatpickr-calendar.arrowCenter:before{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:after,.flatpickr-calendar.arrowTop:before{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:after,.flatpickr-calendar.arrowBottom:before{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-months .flatpickr-month{background:0 0;color:rgba(0,0,0,.9);fill:rgba(0,0,0,.9);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-next-month,.flatpickr-months .flatpickr-prev-month{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:rgba(0,0,0,.9);fill:rgba(0,0,0,.9)}.flatpickr-months .flatpickr-next-month.flatpickr-disabled,.flatpickr-months .flatpickr-prev-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-next-month i,.flatpickr-months .flatpickr-prev-month i{position:relative}.flatpickr-months .flatpickr-next-month.flatpickr-prev-month,.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month{left:0}.flatpickr-months .flatpickr-next-month.flatpickr-next-month,.flatpickr-months .flatpickr-prev-month.flatpickr-next-month{right:0}.flatpickr-months .flatpickr-next-month:hover,.flatpickr-months .flatpickr-prev-month:hover{color:#959ea9}.flatpickr-months .flatpickr-next-month:hover svg,.flatpickr-months .flatpickr-prev-month:hover svg{fill:#f64747}.flatpickr-months .flatpickr-next-month svg,.flatpickr-months .flatpickr-prev-month svg{width:14px;height:14px}.flatpickr-months .flatpickr-next-month svg path,.flatpickr-months .flatpickr-prev-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%}.numInputWrapper input::-ms-clear{display:none}.numInputWrapper input::-webkit-inner-spin-button,.numInputWrapper input::-webkit-outer-spin-button{margin:0;-webkit-appearance:none}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,.15);-webkit-box-sizing:border-box;box-sizing:border-box}.numInputWrapper span:hover{background:rgba(0,0,0,.1)}.numInputWrapper span:active{background:rgba(0,0,0,.2)}.numInputWrapper span:after{display:block;content:\"\";position:absolute}.numInputWrapper span.arrowUp{top:0;border-bottom:0}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,.6);top:26%}.numInputWrapper span.arrowDown{top:50%}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,.6);top:40%}.numInputWrapper span svg{width:inherit;height:auto}.numInputWrapper span svg path{fill:rgba(0,0,0,.5)}.numInputWrapper:hover{background:rgba(0,0,0,.05)}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{font-size:135%;line-height:inherit;font-weight:300;color:inherit;position:absolute;width:75%;left:12.5%;padding:7.48px 0 0 0;line-height:1;height:34px;display:inline-block;text-align:center;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0}.flatpickr-current-month span.cur-month:hover{background:rgba(0,0,0,.05)}.flatpickr-current-month .numInputWrapper{width:6ch;display:inline-block}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:rgba(0,0,0,.9)}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:rgba(0,0,0,.9)}.flatpickr-current-month input.cur-year{background:0 0;-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;cursor:text;padding:0 0 0 .5ch;margin:0;display:inline-block;font-size:inherit;font-family:inherit;font-weight:300;line-height:inherit;height:auto;border:0;border-radius:0;vertical-align:initial;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{font-size:100%;color:rgba(0,0,0,.5);background:0 0;pointer-events:none}.flatpickr-current-month .flatpickr-monthDropdown-months{appearance:menulist;background:0 0;border:none;border-radius:0;box-sizing:border-box;color:inherit;cursor:pointer;font-size:inherit;font-family:inherit;font-weight:300;height:auto;line-height:inherit;margin:-1px 0 0 0;outline:0;padding:0 0 0 .5ch;position:relative;vertical-align:initial;-webkit-box-sizing:border-box;-webkit-appearance:menulist;-moz-appearance:menulist;width:auto}.flatpickr-current-month .flatpickr-monthDropdown-months:active,.flatpickr-current-month .flatpickr-monthDropdown-months:focus{outline:0}.flatpickr-current-month .flatpickr-monthDropdown-months:hover{background:rgba(0,0,0,.05)}.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{background-color:transparent;outline:0;padding:0}.flatpickr-weekdays{background:0 0;text-align:center;overflow:hidden;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:28px}.flatpickr-weekdays .flatpickr-weekdaycontainer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}span.flatpickr-weekday{cursor:default;font-size:90%;background:0 0;color:rgba(0,0,0,.54);line-height:1;margin:0;text-align:center;display:block;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;font-weight:bolder}.dayContainer,.flatpickr-weeks{padding:1px 0 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;width:307.875px}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:307.875px;min-width:307.875px;max-width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;display:inline-block;display:-ms-flexbox;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-wrap:wrap;-ms-flex-pack:justify;-webkit-justify-content:space-around;justify-content:space-around;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}.dayContainer+.dayContainer{-webkit-box-shadow:-1px 0 0 #e6e6e6;box-shadow:-1px 0 0 #e6e6e6}.flatpickr-day{background:0 0;border:1px solid transparent;border-radius:150px;-webkit-box-sizing:border-box;box-sizing:border-box;color:#393939;cursor:pointer;font-weight:400;width:14.2857143%;-webkit-flex-basis:14.2857143%;-ms-flex-preferred-size:14.2857143%;flex-basis:14.2857143%;max-width:39px;height:39px;line-height:39px;margin:0;display:inline-block;position:relative;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center}.flatpickr-day.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day.nextMonthDay:focus,.flatpickr-day.nextMonthDay:hover,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.today.inRange,.flatpickr-day:focus,.flatpickr-day:hover{cursor:pointer;outline:0;background:#e6e6e6;border-color:#e6e6e6}.flatpickr-day.today{border-color:#959ea9}.flatpickr-day.today:focus,.flatpickr-day.today:hover{border-color:#959ea9;background:#959ea9;color:#fff}.flatpickr-day.endRange,.flatpickr-day.endRange.inRange,.flatpickr-day.endRange.nextMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.endRange:focus,.flatpickr-day.endRange:hover,.flatpickr-day.selected,.flatpickr-day.selected.inRange,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.selected:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange,.flatpickr-day.startRange.inRange,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.startRange:focus,.flatpickr-day.startRange:hover{background:#569ff7;-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:#569ff7}.flatpickr-day.endRange.startRange,.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.endRange.endRange,.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.endRange.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.selected.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange+.endRange:not(:nth-child(7n+1)){-webkit-box-shadow:-10px 0 0 #569ff7;box-shadow:-10px 0 0 #569ff7}.flatpickr-day.endRange.startRange.endRange,.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;-webkit-box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.nextMonthDay,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.prevMonthDay{color:rgba(57,57,57,.3);background:0 0;border-color:transparent;cursor:default}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{cursor:not-allowed;color:rgba(57,57,57,.1)}.flatpickr-day.week.selected{border-radius:0;-webkit-box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7;box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7}.flatpickr-day.hidden{visibility:hidden}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{float:left}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 #e6e6e6;box-shadow:1px 0 0 #e6e6e6}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day,.flatpickr-weekwrapper span.flatpickr-day:hover{display:block;width:100%;max-width:none;color:rgba(57,57,57,.3);background:0 0;cursor:default;border:none}.flatpickr-innerContainer{display:block;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden}.flatpickr-rContainer{display:inline-block;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-time:after{content:\"\";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;width:40%;height:40px;float:left}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:#393939}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:#393939}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:0 0;-webkit-box-shadow:none;box-shadow:none;border:0;border-radius:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;color:#393939;font-size:14px;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-time input.flatpickr-hour{font-weight:700}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-am-pm,.flatpickr-time .flatpickr-time-separator{height:inherit;float:left;line-height:inherit;color:#393939;font-weight:700;width:2%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time .flatpickr-am-pm:focus,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time input:hover{background:#eee}.flatpickr-input[readonly]{cursor:pointer}@-webkit-keyframes fpFadeInDown{from{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@keyframes fpFadeInDown{from{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.fv-sr-only{display:none}.fv-plugins-framework input::-ms-clear,.fv-plugins-framework textarea::-ms-clear{display:none;height:0;width:0}.fv-plugins-icon-container{position:relative}.fv-plugins-icon{position:absolute;right:0;text-align:center;top:0}.fv-plugins-icon--enabled{visibility:visible}.fv-plugins-icon--disabled{visibility:hidden}.fv-plugins-message-container--enabled{display:block}.fv-plugins-message-container--disabled{display:none}.fv-plugins-tooltip{max-width:256px;position:absolute;text-align:center;z-index:10000}.fv-plugins-tooltip .fv-plugins-tooltip__content{background:#000;border-radius:3px;color:#eee;padding:8px;position:relative}.fv-plugins-tooltip .fv-plugins-tooltip__content:before{border:8px solid transparent;content:\"\";position:absolute}.fv-plugins-tooltip--hide{display:none}.fv-plugins-tooltip--top-left{transform:translateY(-8px)}.fv-plugins-tooltip--top-left .fv-plugins-tooltip__content:before{border-top-color:#000;left:8px;top:100%}.fv-plugins-tooltip--top{transform:translateY(-8px)}.fv-plugins-tooltip--top .fv-plugins-tooltip__content:before{border-top-color:#000;left:50%;margin-left:-8px;top:100%}.fv-plugins-tooltip--top-right{transform:translateY(-8px)}.fv-plugins-tooltip--top-right .fv-plugins-tooltip__content:before{border-top-color:#000;right:8px;top:100%}.fv-plugins-tooltip--right{transform:translateX(8px)}.fv-plugins-tooltip--right .fv-plugins-tooltip__content:before{border-right-color:#000;margin-top:-8px;right:100%;top:50%}.fv-plugins-tooltip--bottom-right{transform:translateY(8px)}.fv-plugins-tooltip--bottom-right .fv-plugins-tooltip__content:before{border-bottom-color:#000;bottom:100%;right:8px}.fv-plugins-tooltip--bottom{transform:translateY(8px)}.fv-plugins-tooltip--bottom .fv-plugins-tooltip__content:before{border-bottom-color:#000;bottom:100%;left:50%;margin-left:-8px}.fv-plugins-tooltip--bottom-left{transform:translateY(8px)}.fv-plugins-tooltip--bottom-left .fv-plugins-tooltip__content:before{border-bottom-color:#000;bottom:100%;left:8px}.fv-plugins-tooltip--left{transform:translateX(-8px)}.fv-plugins-tooltip--left .fv-plugins-tooltip__content:before{border-left-color:#000;left:100%;margin-top:-8px;top:50%}.fv-plugins-tooltip-icon{cursor:pointer;pointer-events:inherit}.fv-plugins-bootstrap .fv-help-block{color:#dc3545;font-size:80%;margin-top:.25rem}.fv-plugins-bootstrap .is-invalid~.form-check-label,.fv-plugins-bootstrap .is-valid~.form-check-label{color:inherit}.fv-plugins-bootstrap .has-danger .fv-plugins-icon{color:#dc3545}.fv-plugins-bootstrap .has-success .fv-plugins-icon{color:#28a745}.fv-plugins-bootstrap .fv-plugins-icon{height:38px;line-height:38px;width:38px}.fv-plugins-bootstrap .input-group~.fv-plugins-icon{z-index:3}.fv-plugins-bootstrap .form-group.row .fv-plugins-icon{right:15px}.fv-plugins-bootstrap .form-group.row .fv-plugins-icon-check{top:-7px}.fv-plugins-bootstrap:not(.form-inline) label~.fv-plugins-icon{top:32px}.fv-plugins-bootstrap:not(.form-inline) label~.fv-plugins-icon-check{top:25px}.fv-plugins-bootstrap:not(.form-inline) label.sr-only~.fv-plugins-icon-check{top:-7px}.fv-plugins-bootstrap.form-inline .form-group{align-items:flex-start;flex-direction:column;margin-bottom:auto}.fv-plugins-bootstrap .form-control.is-invalid,.fv-plugins-bootstrap .form-control.is-valid{background-image:none}.fv-plugins-bootstrap3 .help-block{margin-bottom:0}.fv-plugins-bootstrap3 .input-group~.form-control-feedback{z-index:4}.fv-plugins-bootstrap3.form-inline .form-group{vertical-align:top}.fv-plugins-bootstrap5 .fv-plugins-bootstrap5-row-invalid .fv-plugins-icon{color:#dc3545}.fv-plugins-bootstrap5 .fv-plugins-bootstrap5-row-valid .fv-plugins-icon{color:#198754}.fv-plugins-bootstrap5 .fv-plugins-icon{align-items:center;display:flex;justify-content:center;height:38px;width:38px}.fv-plugins-bootstrap5 .input-group~.fv-plugins-icon{z-index:3}.fv-plugins-bootstrap5 .fv-plugins-icon-input-group{right:-38px}.fv-plugins-bootstrap5 .form-floating .fv-plugins-icon{height:58px}.fv-plugins-bootstrap5 .row .fv-plugins-icon{right:12px}.fv-plugins-bootstrap5 .row .fv-plugins-icon-check{top:-7px}.fv-plugins-bootstrap5:not(.fv-plugins-bootstrap5-form-inline) label~.fv-plugins-icon{top:32px}.fv-plugins-bootstrap5:not(.fv-plugins-bootstrap5-form-inline) label~.fv-plugins-icon-check{top:25px}.fv-plugins-bootstrap5:not(.fv-plugins-bootstrap5-form-inline) label.sr-only~.fv-plugins-icon-check{top:-7px}.fv-plugins-bootstrap5.fv-plugins-bootstrap5-form-inline .fv-plugins-icon{right:calc(var(--bs-gutter-x,1.5rem)/ 2)}.fv-plugins-bootstrap5 .form-control.fv-plugins-icon-input.is-invalid,.fv-plugins-bootstrap5 .form-control.fv-plugins-icon-input.is-valid,.fv-plugins-bootstrap5 .form-select.fv-plugins-icon-input.is-invalid,.fv-plugins-bootstrap5 .form-select.fv-plugins-icon-input.is-valid{background-image:none}.fv-plugins-bulma .field.has-addons{flex-wrap:wrap}.fv-plugins-bulma .field.has-addons::after{content:\"\";width:100%}.fv-plugins-bulma .field.has-addons .fv-plugins-message-container{order:1}.fv-plugins-bulma .icon.fv-plugins-icon-check{top:-4px}.fv-plugins-bulma .fv-has-error .input,.fv-plugins-bulma .fv-has-error .select select,.fv-plugins-bulma .fv-has-error .textarea{border:1px solid #ff3860}.fv-plugins-bulma .fv-has-success .input,.fv-plugins-bulma .fv-has-success .select select,.fv-plugins-bulma .fv-has-success .textarea{border:1px solid #23d160}.fv-plugins-foundation .fv-plugins-icon{height:39px;line-height:39px;right:0;width:39px}.fv-plugins-foundation .grid-padding-x .fv-plugins-icon{right:15px}.fv-plugins-foundation .fv-plugins-icon-container .cell{position:relative}.fv-plugins-foundation [type=checkbox]~.fv-plugins-icon{top:-7px}.fv-plugins-foundation.fv-stacked-form .fv-plugins-message-container{width:100%}.fv-plugins-foundation.fv-stacked-form fieldset [type=checkbox]~.fv-plugins-icon,.fv-plugins-foundation.fv-stacked-form fieldset [type=radio]~.fv-plugins-icon,.fv-plugins-foundation.fv-stacked-form label .fv-plugins-icon{top:25px}.fv-plugins-foundation .form-error{display:block}.fv-plugins-foundation .fv-row__success .fv-plugins-icon{color:#3adb76}.fv-plugins-foundation .fv-row__error .fv-plugins-icon,.fv-plugins-foundation .fv-row__error fieldset legend,.fv-plugins-foundation .fv-row__error label{color:#cc4b37}.fv-plugins-materialize .fv-plugins-icon{height:42px;line-height:42px;width:42px}.fv-plugins-materialize .fv-plugins-icon-check{top:-10px}.fv-plugins-materialize .fv-invalid-row .fv-plugins-icon,.fv-plugins-materialize .fv-invalid-row .helper-text{color:#f44336}.fv-plugins-materialize .fv-valid-row .fv-plugins-icon,.fv-plugins-materialize .fv-valid-row .helper-text{color:#4caf50}.fv-plugins-milligram .fv-plugins-icon{height:38px;line-height:38px;width:38px}.fv-plugins-milligram .column{position:relative}.fv-plugins-milligram .column .fv-plugins-icon{right:10px}.fv-plugins-milligram .fv-plugins-icon-check{top:-6px}.fv-plugins-milligram .fv-plugins-message-container{margin-bottom:15px}.fv-plugins-milligram.fv-stacked-form .fv-plugins-icon{top:30px}.fv-plugins-milligram.fv-stacked-form .fv-plugins-icon-check{top:24px}.fv-plugins-milligram .fv-invalid-row .fv-help-block,.fv-plugins-milligram .fv-invalid-row .fv-plugins-icon{color:red}.fv-plugins-milligram .fv-valid-row .fv-help-block,.fv-plugins-milligram .fv-valid-row .fv-plugins-icon{color:green}.fv-plugins-mini .fv-plugins-icon{height:42px;line-height:42px;width:42px;top:4px}.fv-plugins-mini .fv-plugins-icon-check{top:-8px}.fv-plugins-mini.fv-stacked-form .fv-plugins-icon{top:28px}.fv-plugins-mini.fv-stacked-form .fv-plugins-icon-check{top:20px}.fv-plugins-mini .fv-plugins-message-container{margin:calc(var(--universal-margin)/ 2)}.fv-plugins-mini .fv-invalid-row .fv-help-block,.fv-plugins-mini .fv-invalid-row .fv-plugins-icon{color:var(--input-invalid-color)}.fv-plugins-mini .fv-valid-row .fv-help-block,.fv-plugins-mini .fv-valid-row .fv-plugins-icon{color:#308732}.fv-plugins-mui .fv-plugins-icon{height:32px;line-height:32px;width:32px;top:15px;right:4px}.fv-plugins-mui .fv-plugins-icon-check{top:-6px;right:-10px}.fv-plugins-mui .fv-plugins-message-container{margin:8px 0}.fv-plugins-mui .fv-invalid-row .fv-help-block,.fv-plugins-mui .fv-invalid-row .fv-plugins-icon{color:#f44336}.fv-plugins-mui .fv-valid-row .fv-help-block,.fv-plugins-mui .fv-valid-row .fv-plugins-icon{color:#4caf50}.fv-plugins-pure .fv-plugins-icon{height:36px;line-height:36px;width:36px}.fv-plugins-pure .fv-has-error .fv-help-block,.fv-plugins-pure .fv-has-error .fv-plugins-icon,.fv-plugins-pure .fv-has-error label{color:#ca3c3c}.fv-plugins-pure .fv-has-success .fv-help-block,.fv-plugins-pure .fv-has-success .fv-plugins-icon,.fv-plugins-pure .fv-has-success label{color:#1cb841}.fv-plugins-pure.pure-form-aligned .fv-help-block{margin-top:5px;margin-left:180px}.fv-plugins-pure.pure-form-aligned .fv-plugins-icon-check{top:-9px}.fv-plugins-pure.pure-form-stacked .pure-control-group{margin-bottom:8px}.fv-plugins-pure.pure-form-stacked .fv-plugins-icon{top:22px}.fv-plugins-pure.pure-form-stacked .fv-plugins-icon-check{top:13px}.fv-plugins-pure.pure-form-stacked .fv-sr-only~.fv-plugins-icon{top:-9px}.fv-plugins-semantic .error .fv-plugins-icon,.fv-plugins-semantic.ui.form .fields.error label{color:#9f3a38}.fv-plugins-semantic .fv-plugins-icon-check{right:7px}.fv-plugins-shoelace .input-group{margin-bottom:0}.fv-plugins-shoelace .fv-plugins-icon{height:32px;line-height:32px;width:32px;top:28px}.fv-plugins-shoelace .row .fv-plugins-icon{right:16px;top:0}.fv-plugins-shoelace .fv-plugins-icon-check{top:24px}.fv-plugins-shoelace .fv-sr-only~.fv-plugins-icon,.fv-plugins-shoelace .fv-sr-only~div .fv-plugins-icon{top:-4px}.fv-plugins-shoelace .input-valid .fv-help-block,.fv-plugins-shoelace .input-valid .fv-plugins-icon{color:#2ecc40}.fv-plugins-shoelace .input-invalid .fv-help-block,.fv-plugins-shoelace .input-invalid .fv-plugins-icon{color:#ff4136}.fv-plugins-spectre .input-group .fv-plugins-icon{z-index:2}.fv-plugins-spectre .form-group .fv-plugins-icon-check{right:6px;top:10px}.fv-plugins-spectre:not(.form-horizontal) .form-group .fv-plugins-icon-check{right:6px;top:45px}.fv-plugins-tachyons .fv-plugins-icon{height:36px;line-height:36px;width:36px}.fv-plugins-tachyons .fv-plugins-icon-check{top:-7px}.fv-plugins-tachyons.fv-stacked-form .fv-plugins-icon{top:34px}.fv-plugins-tachyons.fv-stacked-form .fv-plugins-icon-check{top:24px}.fv-plugins-turret .fv-plugins-icon{height:40px;line-height:40px;width:40px}.fv-plugins-turret.fv-stacked-form .fv-plugins-icon{top:29px}.fv-plugins-turret.fv-stacked-form .fv-plugins-icon-check{top:17px}.fv-plugins-turret .fv-invalid-row .form-message,.fv-plugins-turret .fv-invalid-row .fv-plugins-icon{color:#c00}.fv-plugins-turret .fv-valid-row .form-message,.fv-plugins-turret .fv-valid-row .fv-plugins-icon{color:#00b300}.fv-plugins-uikit .fv-plugins-icon{height:40px;line-height:40px;top:25px;width:40px}.fv-plugins-uikit.uk-form-horizontal .fv-plugins-icon{top:0}.fv-plugins-uikit.uk-form-horizontal .fv-plugins-icon-check{top:-11px}.fv-plugins-uikit.uk-form-stacked .fv-plugins-icon-check{top:15px}.fv-plugins-uikit.uk-form-stacked .fv-no-label .fv-plugins-icon{top:0}.fv-plugins-uikit.uk-form-stacked .fv-no-label .fv-plugins-icon-check{top:-11px}.fv-plugins-wizard--step{display:none}.fv-plugins-wizard--active{display:block}.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;right:0;height:100%;width:100%;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;-webkit-transform-style:preserve-3d;transform-origin:0 0;transform-style:flat}.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin{left:0;right:auto}.noUi-vertical .noUi-origin{top:-100%;width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:absolute}.noUi-touch-area{height:100%;width:100%}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;right:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;right:-6px;bottom:-17px}.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle{left:-17px;right:auto}.noUi-target{background:#fafafa;border-radius:4px;border:1px solid #d3d3d3;box-shadow:inset 0 1px 1px #f0f0f0,0 3px 6px -5px #bbb}.noUi-connects{border-radius:3px}.noUi-connect{background:#3fb8af}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #d9d9d9;border-radius:3px;background:#fff;cursor:default;box-shadow:inset 0 0 1px #fff,inset 0 1px 7px #ebebeb,0 3px 6px -3px #bbb}.noUi-active{box-shadow:inset 0 0 1px #fff,inset 0 1px 7px #ddd,0 3px 6px -3px #bbb}.noUi-handle:after,.noUi-handle:before{content:\"\";display:block;position:absolute;height:14px;width:1px;background:#e8e7e6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#b8b8b8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#ccc}.noUi-marker-sub{background:#aaa}.noUi-marker-large{background:#aaa}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #d9d9d9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%}.noUi-horizontal .noUi-origin>.noUi-tooltip{-webkit-transform:translate(50%,0);transform:translate(50%,0);left:auto;bottom:10px}.noUi-vertical .noUi-origin>.noUi-tooltip{-webkit-transform:translate(0,-18px);transform:translate(0,-18px);top:auto;right:28px}@-webkit-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%,70%{opacity:1;-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-moz-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%,70%{opacity:1;-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%,70%{opacity:1;-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-webkit-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}}@-moz-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}}@keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@-moz-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:2px solid rgba(0,0,0,.3);background:#fff;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:.5}.dropzone .dz-message{text-align:center;margin:2em 0}.dropzone .dz-message .dz-button{background:0 0;color:inherit;border:none;padding:0;font:inherit;cursor:pointer;outline:inherit}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom,#eee,#ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:#fff}.dropzone .dz-preview.dz-image-preview .dz-details{-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;-ms-transition:opacity .2s linear;-o-transition:opacity .2s linear;transition:opacity .2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,.8);background-color:rgba(255,255,255,.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,.4);padding:0 .4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{-webkit-transform:scale(1.05,1.05);-moz-transform:scale(1.05,1.05);-ms-transform:scale(1.05,1.05);-o-transform:scale(1.05,1.05);transform:scale(1.05,1.05);-webkit-filter:blur(8px);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{-webkit-animation:passing-through 3s cubic-bezier(.77,0,.175,1);-moz-animation:passing-through 3s cubic-bezier(.77,0,.175,1);-ms-animation:passing-through 3s cubic-bezier(.77,0,.175,1);-o-animation:passing-through 3s cubic-bezier(.77,0,.175,1);animation:passing-through 3s cubic-bezier(.77,0,.175,1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;-webkit-animation:slide-in 3s cubic-bezier(.77,0,.175,1);-moz-animation:slide-in 3s cubic-bezier(.77,0,.175,1);-ms-animation:slide-in 3s cubic-bezier(.77,0,.175,1);-o-animation:slide-in 3s cubic-bezier(.77,0,.175,1);animation:slide-in 3s cubic-bezier(.77,0,.175,1)}.dropzone .dz-preview .dz-error-mark,.dropzone .dz-preview .dz-success-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px}.dropzone .dz-preview .dz-error-mark svg,.dropzone .dz-preview .dz-success-mark svg{display:block;width:54px;height:54px}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;-webkit-transition:all .2s linear;-moz-transition:all .2s linear;-ms-transition:all .2s linear;-o-transition:all .2s linear;transition:all .2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;-webkit-transition:opacity .4s ease-in;-moz-transition:opacity .4s ease-in;-ms-transition:opacity .4s ease-in;-o-transition:opacity .4s ease-in;transition:opacity .4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{-webkit-animation:pulse 6s ease infinite;-moz-animation:pulse 6s ease infinite;-ms-animation:pulse 6s ease infinite;-o-animation:pulse 6s ease infinite;animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:16px;left:50%;top:50%;margin-top:-8px;width:80px;margin-left:-40px;background:rgba(255,255,255,.9);-webkit-transform:scale(1);border-radius:8px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#333;background:linear-gradient(to bottom,#666,#444);position:absolute;top:0;left:0;bottom:0;width:0;-webkit-transition:width .3s ease-in-out;-moz-transition:width .3s ease-in-out;-ms-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;-webkit-transition:opacity .3s ease;-moz-transition:opacity .3s ease;-ms-transition:opacity .3s ease;-o-transition:opacity .3s ease;transition:opacity .3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#be2626;background:linear-gradient(to bottom,#be2626,#a92222);padding:.5em 1.2em;color:#fff}.dropzone .dz-preview .dz-error-message:after{content:\"\";position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #be2626}@font-face{font-family:keenicons-duotone;src:url(fonts/keenicons/keenicons-duotone.eot?eut7fk);src:url(fonts/keenicons/keenicons-duotone.eot?eut7fk#iefix) format(\"embedded-opentype\"),url(fonts/keenicons/keenicons-duotone.ttf?eut7fk) format(\"truetype\"),url(fonts/keenicons/keenicons-duotone.woff?eut7fk) format(\"woff\"),url(fonts/keenicons/keenicons-duotone.svg?eut7fk#keenicons-duotone) format(\"svg\");font-weight:400;font-style:normal;font-display:block}.ki-duotone{font-family:keenicons-duotone!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;display:inline-flex;direction:ltr;position:relative;display:inline-flex;direction:ltr;position:relative;display:inline-flex;direction:ltr;position:relative;display:inline-flex;direction:ltr;position:relative;display:inline-flex;direction:ltr;position:relative;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ki-abstract-1 .path1:before{content:\"\\e900\";opacity:.3}.ki-abstract-1 .path2:before{content:\"\\e901\";position:absolute;left:0}.ki-abstract-2 .path1:before{content:\"\\e902\";opacity:.3}.ki-abstract-2 .path2:before{content:\"\\e903\";position:absolute;left:0}.ki-abstract-3 .path1:before{content:\"\\e904\";opacity:.3}.ki-abstract-3 .path2:before{content:\"\\e905\";position:absolute;left:0}.ki-abstract-4 .path1:before{content:\"\\e906\";opacity:.3}.ki-abstract-4 .path2:before{content:\"\\e907\";position:absolute;left:0}.ki-abstract-5 .path1:before{content:\"\\e908\";opacity:.3}.ki-abstract-5 .path2:before{content:\"\\e909\";position:absolute;left:0}.ki-abstract-6:before{content:\"\\e90a\"}.ki-abstract-7 .path1:before{content:\"\\e90b\";opacity:.3}.ki-abstract-7 .path2:before{content:\"\\e90c\";position:absolute;left:0}.ki-abstract-8 .path1:before{content:\"\\e90d\";opacity:.3}.ki-abstract-8 .path2:before{content:\"\\e90e\";position:absolute;left:0}.ki-abstract-9 .path1:before{content:\"\\e90f\";opacity:.3}.ki-abstract-9 .path2:before{content:\"\\e910\";position:absolute;left:0}.ki-abstract-10 .path1:before{content:\"\\e911\";opacity:.3}.ki-abstract-10 .path2:before{content:\"\\e912\";position:absolute;left:0}.ki-abstract-11 .path1:before{content:\"\\e913\";opacity:.3}.ki-abstract-11 .path2:before{content:\"\\e914\";position:absolute;left:0}.ki-abstract-12 .path1:before{content:\"\\e915\";opacity:.3}.ki-abstract-12 .path2:before{content:\"\\e916\";position:absolute;left:0}.ki-abstract-13 .path1:before{content:\"\\e917\";opacity:.3}.ki-abstract-13 .path2:before{content:\"\\e918\";position:absolute;left:0}.ki-abstract-14 .path1:before{content:\"\\e919\"}.ki-abstract-14 .path2:before{content:\"\\e91a\";position:absolute;left:0;opacity:.3}.ki-abstract-15 .path1:before{content:\"\\e91b\";opacity:.3}.ki-abstract-15 .path2:before{content:\"\\e91c\";position:absolute;left:0}.ki-abstract-16 .path1:before{content:\"\\e91d\";opacity:.3}.ki-abstract-16 .path2:before{content:\"\\e91e\";position:absolute;left:0}.ki-abstract-17 .path1:before{content:\"\\e91f\"}.ki-abstract-17 .path2:before{content:\"\\e920\";position:absolute;left:0;opacity:.3}.ki-abstract-18 .path1:before{content:\"\\e921\"}.ki-abstract-18 .path2:before{content:\"\\e922\";position:absolute;left:0;opacity:.3}.ki-abstract-19 .path1:before{content:\"\\e923\"}.ki-abstract-19 .path2:before{content:\"\\e924\";position:absolute;left:0;opacity:.3}.ki-abstract-20 .path1:before{content:\"\\e925\"}.ki-abstract-20 .path2:before{content:\"\\e926\";position:absolute;left:0;opacity:.3}.ki-abstract-21 .path1:before{content:\"\\e927\";opacity:.3}.ki-abstract-21 .path2:before{content:\"\\e928\";position:absolute;left:0}.ki-abstract-22 .path1:before{content:\"\\e929\";opacity:.3}.ki-abstract-22 .path2:before{content:\"\\e92a\";position:absolute;left:0}.ki-abstract-23 .path1:before{content:\"\\e92b\";opacity:.3}.ki-abstract-23 .path2:before{content:\"\\e92c\";position:absolute;left:0}.ki-abstract-24 .path1:before{content:\"\\e92d\"}.ki-abstract-24 .path2:before{content:\"\\e92e\";position:absolute;left:0;opacity:.3}.ki-abstract-25 .path1:before{content:\"\\e92f\";opacity:.3}.ki-abstract-25 .path2:before{content:\"\\e930\";position:absolute;left:0}.ki-abstract-26 .path1:before{content:\"\\e931\";opacity:.3}.ki-abstract-26 .path2:before{content:\"\\e932\";position:absolute;left:0}.ki-abstract-27 .path1:before{content:\"\\e933\"}.ki-abstract-27 .path2:before{content:\"\\e934\";position:absolute;left:0;opacity:.3}.ki-abstract-28 .path1:before{content:\"\\e935\"}.ki-abstract-28 .path2:before{content:\"\\e936\";position:absolute;left:0;opacity:.3}.ki-abstract-29 .path1:before{content:\"\\e937\"}.ki-abstract-29 .path2:before{content:\"\\e938\";position:absolute;left:0;opacity:.3}.ki-abstract-30 .path1:before{content:\"\\e939\"}.ki-abstract-30 .path2:before{content:\"\\e93a\";position:absolute;left:0;opacity:.3}.ki-abstract-31 .path1:before{content:\"\\e93b\"}.ki-abstract-31 .path2:before{content:\"\\e93c\";position:absolute;left:0;opacity:.3}.ki-abstract-32 .path1:before{content:\"\\e93d\"}.ki-abstract-32 .path2:before{content:\"\\e93e\";position:absolute;left:0;opacity:.3}.ki-abstract-33 .path1:before{content:\"\\e93f\";opacity:.3}.ki-abstract-33 .path2:before{content:\"\\e940\";position:absolute;left:0}.ki-abstract-34 .path1:before{content:\"\\e941\";opacity:.3}.ki-abstract-34 .path2:before{content:\"\\e942\";position:absolute;left:0}.ki-abstract-35 .path1:before{content:\"\\e943\"}.ki-abstract-35 .path2:before{content:\"\\e944\";position:absolute;left:0;opacity:.3}.ki-abstract-36 .path1:before{content:\"\\e945\"}.ki-abstract-36 .path2:before{content:\"\\e946\";position:absolute;left:0;opacity:.3}.ki-abstract-37 .path1:before{content:\"\\e947\";opacity:.3}.ki-abstract-37 .path2:before{content:\"\\e948\";position:absolute;left:0}.ki-abstract-38 .path1:before{content:\"\\e949\"}.ki-abstract-38 .path2:before{content:\"\\e94a\";position:absolute;left:0;opacity:.3}.ki-abstract-39 .path1:before{content:\"\\e94b\";opacity:.3}.ki-abstract-39 .path2:before{content:\"\\e94c\";position:absolute;left:0}.ki-abstract-40 .path1:before{content:\"\\e94d\"}.ki-abstract-40 .path2:before{content:\"\\e94e\";position:absolute;left:0;opacity:.3}.ki-abstract-41 .path1:before{content:\"\\e94f\"}.ki-abstract-41 .path2:before{content:\"\\e950\";position:absolute;left:0;opacity:.3}.ki-abstract-42 .path1:before{content:\"\\e951\";opacity:.3}.ki-abstract-42 .path2:before{content:\"\\e952\";position:absolute;left:0}.ki-abstract-43 .path1:before{content:\"\\e953\";opacity:.3}.ki-abstract-43 .path2:before{content:\"\\e954\";position:absolute;left:0}.ki-abstract-44 .path1:before{content:\"\\e955\"}.ki-abstract-44 .path2:before{content:\"\\e956\";position:absolute;left:0;opacity:.3}.ki-abstract-45 .path1:before{content:\"\\e957\"}.ki-abstract-45 .path2:before{content:\"\\e958\";position:absolute;left:0;opacity:.3}.ki-abstract-46 .path1:before{content:\"\\e959\";opacity:.3}.ki-abstract-46 .path2:before{content:\"\\e95a\";position:absolute;left:0}.ki-abstract-47 .path1:before{content:\"\\e95b\";opacity:.3}.ki-abstract-47 .path2:before{content:\"\\e95c\";position:absolute;left:0}.ki-abstract-48 .path1:before{content:\"\\e95d\";opacity:.3}.ki-abstract-48 .path2:before{content:\"\\e95e\";position:absolute;left:0}.ki-abstract-48 .path3:before{content:\"\\e95f\";position:absolute;left:0}.ki-abstract-49 .path1:before{content:\"\\e960\";opacity:.3}.ki-abstract-49 .path2:before{content:\"\\e961\";position:absolute;left:0;opacity:.3}.ki-abstract-49 .path3:before{content:\"\\e962\";position:absolute;left:0}.ki-abstract .path1:before{content:\"\\e963\";opacity:.3}.ki-abstract .path2:before{content:\"\\e964\";position:absolute;left:0}.ki-add-files .path1:before{content:\"\\e965\"}.ki-add-files .path2:before{content:\"\\e966\";position:absolute;left:0}.ki-add-files .path3:before{content:\"\\e967\";position:absolute;left:0;opacity:.3}.ki-add-folder .path1:before{content:\"\\e968\";opacity:.3}.ki-add-folder .path2:before{content:\"\\e969\";position:absolute;left:0}.ki-add-item .path1:before{content:\"\\e96a\";opacity:.3}.ki-add-item .path2:before{content:\"\\e96b\";position:absolute;left:0}.ki-add-item .path3:before{content:\"\\e96c\";position:absolute;left:0}.ki-add-notepad .path1:before{content:\"\\e96d\";opacity:.3}.ki-add-notepad .path2:before{content:\"\\e96e\";position:absolute;left:0}.ki-add-notepad .path3:before{content:\"\\e96f\";position:absolute;left:0}.ki-add-notepad .path4:before{content:\"\\e970\";position:absolute;left:0}.ki-address-book .path1:before{content:\"\\e971\"}.ki-address-book .path2:before{content:\"\\e972\";position:absolute;left:0}.ki-address-book .path3:before{content:\"\\e973\";position:absolute;left:0;opacity:.3}.ki-airplane-square .path1:before{content:\"\\e974\";opacity:.3}.ki-airplane-square .path2:before{content:\"\\e975\";position:absolute;left:0}.ki-airplane .path1:before{content:\"\\e976\";opacity:.3}.ki-airplane .path2:before{content:\"\\e977\";position:absolute;left:0}.ki-airpod .path1:before{content:\"\\e978\";opacity:.3}.ki-airpod .path2:before{content:\"\\e979\";position:absolute;left:0}.ki-airpod .path3:before{content:\"\\e97a\";position:absolute;left:0}.ki-android .path1:before{content:\"\\e97b\";opacity:.3}.ki-android .path2:before{content:\"\\e97c\";position:absolute;left:0}.ki-android .path3:before{content:\"\\e97d\";position:absolute;left:0}.ki-android .path4:before{content:\"\\e97e\";position:absolute;left:0}.ki-android .path5:before{content:\"\\e97f\";position:absolute;left:0}.ki-android .path6:before{content:\"\\e980\";position:absolute;left:0;opacity:.3}.ki-angular .path1:before{content:\"\\e981\";opacity:.3}.ki-angular .path2:before{content:\"\\e982\";position:absolute;left:0}.ki-angular .path3:before{content:\"\\e983\";position:absolute;left:0;opacity:.3}.ki-apple .path1:before{content:\"\\e984\"}.ki-apple .path2:before{content:\"\\e985\";position:absolute;left:0;opacity:.3}.ki-archive-tick .path1:before{content:\"\\e986\";opacity:.3}.ki-archive-tick .path2:before{content:\"\\e987\";position:absolute;left:0}.ki-archive .path1:before{content:\"\\e988\";opacity:.3}.ki-archive .path2:before{content:\"\\e989\";position:absolute;left:0}.ki-archive .path3:before{content:\"\\e98a\";position:absolute;left:0}.ki-arrow-circle-left .path1:before{content:\"\\e98b\";opacity:.3}.ki-arrow-circle-left .path2:before{content:\"\\e98c\";position:absolute;left:0}.ki-arrow-circle-right .path1:before{content:\"\\e98d\";opacity:.3}.ki-arrow-circle-right .path2:before{content:\"\\e98e\";position:absolute;left:0}.ki-arrow-diagonal .path1:before{content:\"\\e98f\";opacity:.3}.ki-arrow-diagonal .path2:before{content:\"\\e990\";position:absolute;left:0}.ki-arrow-diagonal .path3:before{content:\"\\e991\";position:absolute;left:0}.ki-arrow-down-left .path1:before{content:\"\\e992\"}.ki-arrow-down-left .path2:before{content:\"\\e993\";position:absolute;left:0;opacity:.3}.ki-arrow-down-refraction .path1:before{content:\"\\e994\";opacity:.3}.ki-arrow-down-refraction .path2:before{content:\"\\e995\";position:absolute;left:0}.ki-arrow-down-right .path1:before{content:\"\\e996\"}.ki-arrow-down-right .path2:before{content:\"\\e997\";position:absolute;left:0;opacity:.3}.ki-arrow-down .path1:before{content:\"\\e998\"}.ki-arrow-down .path2:before{content:\"\\e999\";position:absolute;left:0;opacity:.3}.ki-arrow-left .path1:before{content:\"\\e99a\"}.ki-arrow-left .path2:before{content:\"\\e99b\";position:absolute;left:0;opacity:.3}.ki-arrow-mix .path1:before{content:\"\\e99c\"}.ki-arrow-mix .path2:before{content:\"\\e99d\";position:absolute;left:0;opacity:.3}.ki-arrow-right-left .path1:before{content:\"\\e99e\";opacity:.3}.ki-arrow-right-left .path2:before{content:\"\\e99f\";position:absolute;left:0}.ki-arrow-right .path1:before{content:\"\\e9a0\"}.ki-arrow-right .path2:before{content:\"\\e9a1\";position:absolute;left:0;opacity:.3}.ki-arrow-two-diagonals .path1:before{content:\"\\e9a2\";opacity:.3}.ki-arrow-two-diagonals .path2:before{content:\"\\e9a3\";position:absolute;left:0}.ki-arrow-two-diagonals .path3:before{content:\"\\e9a4\";position:absolute;left:0}.ki-arrow-two-diagonals .path4:before{content:\"\\e9a5\";position:absolute;left:0}.ki-arrow-two-diagonals .path5:before{content:\"\\e9a6\";position:absolute;left:0}.ki-arrow-up-down .path1:before{content:\"\\e9a7\";opacity:.3}.ki-arrow-up-down .path2:before{content:\"\\e9a8\";position:absolute;left:0}.ki-arrow-up-left .path1:before{content:\"\\e9a9\"}.ki-arrow-up-left .path2:before{content:\"\\e9aa\";position:absolute;left:0;opacity:.3}.ki-arrow-up-refraction .path1:before{content:\"\\e9ab\";opacity:.3}.ki-arrow-up-refraction .path2:before{content:\"\\e9ac\";position:absolute;left:0}.ki-arrow-up-right .path1:before{content:\"\\e9ad\"}.ki-arrow-up-right .path2:before{content:\"\\e9ae\";position:absolute;left:0;opacity:.3}.ki-arrow-up .path1:before{content:\"\\e9af\"}.ki-arrow-up .path2:before{content:\"\\e9b0\";position:absolute;left:0;opacity:.3}.ki-arrow-zigzag .path1:before{content:\"\\e9b1\"}.ki-arrow-zigzag .path2:before{content:\"\\e9b2\";position:absolute;left:0;opacity:.3}.ki-arrows-circle .path1:before{content:\"\\e9b3\";opacity:.3}.ki-arrows-circle .path2:before{content:\"\\e9b4\";position:absolute;left:0}.ki-arrows-loop .path1:before{content:\"\\e9b5\";opacity:.3}.ki-arrows-loop .path2:before{content:\"\\e9b6\";position:absolute;left:0}.ki-artificial-intelligence .path1:before{content:\"\\e9b7\";opacity:.3}.ki-artificial-intelligence .path2:before{content:\"\\e9b8\";position:absolute;left:0}.ki-artificial-intelligence .path3:before{content:\"\\e9b9\";position:absolute;left:0}.ki-artificial-intelligence .path4:before{content:\"\\e9ba\";position:absolute;left:0}.ki-artificial-intelligence .path5:before{content:\"\\e9bb\";position:absolute;left:0}.ki-artificial-intelligence .path6:before{content:\"\\e9bc\";position:absolute;left:0}.ki-artificial-intelligence .path7:before{content:\"\\e9bd\";position:absolute;left:0}.ki-artificial-intelligence .path8:before{content:\"\\e9be\";position:absolute;left:0}.ki-auto-brightness .path1:before{content:\"\\e9bf\";opacity:.3}.ki-auto-brightness .path2:before{content:\"\\e9c0\";position:absolute;left:0}.ki-auto-brightness .path3:before{content:\"\\e9c1\";position:absolute;left:0}.ki-avalanche .path1:before{content:\"\\e9c2\";opacity:.3}.ki-avalanche .path2:before{content:\"\\e9c3\";position:absolute;left:0}.ki-award .path1:before{content:\"\\e9c4\";opacity:.3}.ki-award .path2:before{content:\"\\e9c5\";position:absolute;left:0}.ki-award .path3:before{content:\"\\e9c6\";position:absolute;left:0}.ki-badge .path1:before{content:\"\\e9c7\";opacity:.3}.ki-badge .path2:before{content:\"\\e9c8\";position:absolute;left:0}.ki-badge .path3:before{content:\"\\e9c9\";position:absolute;left:0}.ki-badge .path4:before{content:\"\\e9ca\";position:absolute;left:0}.ki-badge .path5:before{content:\"\\e9cb\";position:absolute;left:0}.ki-bandage .path1:before{content:\"\\e9cc\";opacity:.3}.ki-bandage .path2:before{content:\"\\e9cd\";position:absolute;left:0}.ki-bank .path1:before{content:\"\\e9ce\";opacity:.3}.ki-bank .path2:before{content:\"\\e9cf\";position:absolute;left:0}.ki-barcode .path1:before{content:\"\\e9d0\"}.ki-barcode .path2:before{content:\"\\e9d1\";position:absolute;left:0}.ki-barcode .path3:before{content:\"\\e9d2\";position:absolute;left:0}.ki-barcode .path4:before{content:\"\\e9d3\";position:absolute;left:0}.ki-barcode .path5:before{content:\"\\e9d4\";position:absolute;left:0}.ki-barcode .path6:before{content:\"\\e9d5\";position:absolute;left:0}.ki-barcode .path7:before{content:\"\\e9d6\";position:absolute;left:0}.ki-barcode .path8:before{content:\"\\e9d7\";position:absolute;left:0;opacity:.3}.ki-basket-ok .path1:before{content:\"\\e9d8\";opacity:.3}.ki-basket-ok .path2:before{content:\"\\e9d9\";position:absolute;left:0}.ki-basket-ok .path3:before{content:\"\\e9da\";position:absolute;left:0}.ki-basket-ok .path4:before{content:\"\\e9db\";position:absolute;left:0}.ki-basket .path1:before{content:\"\\e9dc\";opacity:.3}.ki-basket .path2:before{content:\"\\e9dd\";position:absolute;left:0}.ki-basket .path3:before{content:\"\\e9de\";position:absolute;left:0}.ki-basket .path4:before{content:\"\\e9df\";position:absolute;left:0}.ki-behance .path1:before{content:\"\\e9e0\"}.ki-behance .path2:before{content:\"\\e9e1\";position:absolute;left:0;opacity:.3}.ki-bill .path1:before{content:\"\\e9e2\";opacity:.3}.ki-bill .path2:before{content:\"\\e9e3\";position:absolute;left:0}.ki-bill .path3:before{content:\"\\e9e4\";position:absolute;left:0}.ki-bill .path4:before{content:\"\\e9e5\";position:absolute;left:0}.ki-bill .path5:before{content:\"\\e9e6\";position:absolute;left:0}.ki-bill .path6:before{content:\"\\e9e7\";position:absolute;left:0}.ki-binance-usd .path1:before{content:\"\\e9e8\"}.ki-binance-usd .path2:before{content:\"\\e9e9\";position:absolute;left:0;opacity:.3}.ki-binance-usd .path3:before{content:\"\\e9ea\";position:absolute;left:0}.ki-binance-usd .path4:before{content:\"\\e9eb\";position:absolute;left:0;opacity:.3}.ki-binance .path1:before{content:\"\\e9ec\";opacity:.3}.ki-binance .path2:before{content:\"\\e9ed\";position:absolute;left:0;opacity:.3}.ki-binance .path3:before{content:\"\\e9ee\";position:absolute;left:0;opacity:.3}.ki-binance .path4:before{content:\"\\e9ef\";position:absolute;left:0}.ki-binance .path5:before{content:\"\\e9f0\";position:absolute;left:0}.ki-bitcoin .path1:before{content:\"\\e9f1\";opacity:.3}.ki-bitcoin .path2:before{content:\"\\e9f2\";position:absolute;left:0}.ki-black-down:before{content:\"\\e9f3\"}.ki-black-left-line .path1:before{content:\"\\e9f4\"}.ki-black-left-line .path2:before{content:\"\\e9f5\";position:absolute;left:0;opacity:.3}.ki-black-left:before{content:\"\\e9f6\"}.ki-black-right-line .path1:before{content:\"\\e9f7\"}.ki-black-right-line .path2:before{content:\"\\e9f8\";position:absolute;left:0;opacity:.3}.ki-black-right:before{content:\"\\e9f9\"}.ki-black-up:before{content:\"\\e9fa\"}.ki-bluetooth .path1:before{content:\"\\e9fb\"}.ki-bluetooth .path2:before{content:\"\\e9fc\";position:absolute;left:0;opacity:.3}.ki-book-open .path1:before{content:\"\\e9fd\";opacity:.3}.ki-book-open .path2:before{content:\"\\e9fe\";position:absolute;left:0}.ki-book-open .path3:before{content:\"\\e9ff\";position:absolute;left:0}.ki-book-open .path4:before{content:\"\\ea00\";position:absolute;left:0}.ki-book-square .path1:before{content:\"\\ea01\";opacity:.3}.ki-book-square .path2:before{content:\"\\ea02\";position:absolute;left:0}.ki-book-square .path3:before{content:\"\\ea03\";position:absolute;left:0}.ki-book .path1:before{content:\"\\ea04\";opacity:.3}.ki-book .path2:before{content:\"\\ea05\";position:absolute;left:0}.ki-book .path3:before{content:\"\\ea06\";position:absolute;left:0}.ki-book .path4:before{content:\"\\ea07\";position:absolute;left:0}.ki-bookmark-2 .path1:before{content:\"\\ea08\";opacity:.3}.ki-bookmark-2 .path2:before{content:\"\\ea09\";position:absolute;left:0}.ki-bookmark .path1:before{content:\"\\ea0a\";opacity:.3}.ki-bookmark .path2:before{content:\"\\ea0b\";position:absolute;left:0}.ki-bootstrap .path1:before{content:\"\\ea0c\";opacity:.3}.ki-bootstrap .path2:before{content:\"\\ea0d\";position:absolute;left:0}.ki-bootstrap .path3:before{content:\"\\ea0e\";position:absolute;left:0}.ki-briefcase .path1:before{content:\"\\ea0f\";opacity:.3}.ki-briefcase .path2:before{content:\"\\ea10\";position:absolute;left:0}.ki-brifecase-cros .path1:before{content:\"\\ea11\";opacity:.3}.ki-brifecase-cros .path2:before{content:\"\\ea12\";position:absolute;left:0}.ki-brifecase-cros .path3:before{content:\"\\ea13\";position:absolute;left:0}.ki-brifecase-tick .path1:before{content:\"\\ea14\";opacity:.3}.ki-brifecase-tick .path2:before{content:\"\\ea15\";position:absolute;left:0}.ki-brifecase-tick .path3:before{content:\"\\ea16\";position:absolute;left:0}.ki-brifecase-timer .path1:before{content:\"\\ea17\";opacity:.3}.ki-brifecase-timer .path2:before{content:\"\\ea18\";position:absolute;left:0}.ki-brifecase-timer .path3:before{content:\"\\ea19\";position:absolute;left:0}.ki-brush .path1:before{content:\"\\ea1a\";opacity:.3}.ki-brush .path2:before{content:\"\\ea1b\";position:absolute;left:0}.ki-bucket-square .path1:before{content:\"\\ea1c\";opacity:.3}.ki-bucket-square .path2:before{content:\"\\ea1d\";position:absolute;left:0}.ki-bucket-square .path3:before{content:\"\\ea1e\";position:absolute;left:0}.ki-bucket .path1:before{content:\"\\ea1f\";opacity:.3}.ki-bucket .path2:before{content:\"\\ea20\";position:absolute;left:0}.ki-bucket .path3:before{content:\"\\ea21\";position:absolute;left:0}.ki-bucket .path4:before{content:\"\\ea22\";position:absolute;left:0}.ki-burger-menu-1 .path1:before{content:\"\\ea23\";opacity:.3}.ki-burger-menu-1 .path2:before{content:\"\\ea24\";position:absolute;left:0}.ki-burger-menu-1 .path3:before{content:\"\\ea25\";position:absolute;left:0}.ki-burger-menu-1 .path4:before{content:\"\\ea26\";position:absolute;left:0}.ki-burger-menu-2 .path1:before{content:\"\\ea27\";opacity:.3}.ki-burger-menu-2 .path2:before{content:\"\\ea28\";position:absolute;left:0}.ki-burger-menu-2 .path3:before{content:\"\\ea29\";position:absolute;left:0}.ki-burger-menu-2 .path4:before{content:\"\\ea2a\";position:absolute;left:0}.ki-burger-menu-2 .path5:before{content:\"\\ea2b\";position:absolute;left:0}.ki-burger-menu-2 .path6:before{content:\"\\ea2c\";position:absolute;left:0}.ki-burger-menu-2 .path7:before{content:\"\\ea2d\";position:absolute;left:0}.ki-burger-menu-2 .path8:before{content:\"\\ea2e\";position:absolute;left:0}.ki-burger-menu-2 .path9:before{content:\"\\ea2f\";position:absolute;left:0}.ki-burger-menu-2 .path10:before{content:\"\\ea30\";position:absolute;left:0}.ki-burger-menu-3 .path1:before{content:\"\\ea31\";opacity:.3}.ki-burger-menu-3 .path2:before{content:\"\\ea32\";position:absolute;left:0}.ki-burger-menu-3 .path3:before{content:\"\\ea33\";position:absolute;left:0;opacity:.3}.ki-burger-menu-3 .path4:before{content:\"\\ea34\";position:absolute;left:0;opacity:.3}.ki-burger-menu-3 .path5:before{content:\"\\ea35\";position:absolute;left:0}.ki-burger-menu-3 .path6:before{content:\"\\ea36\";position:absolute;left:0;opacity:.3}.ki-burger-menu-3 .path7:before{content:\"\\ea37\";position:absolute;left:0;opacity:.3}.ki-burger-menu-3 .path8:before{content:\"\\ea38\";position:absolute;left:0}.ki-burger-menu-3 .path9:before{content:\"\\ea39\";position:absolute;left:0;opacity:.3}.ki-burger-menu-4:before{content:\"\\ea3a\"}.ki-burger-menu-5:before{content:\"\\ea3b\"}.ki-burger-menu-6:before{content:\"\\ea3c\"}.ki-burger-menu .path1:before{content:\"\\ea3d\";opacity:.3}.ki-burger-menu .path2:before{content:\"\\ea3e\";position:absolute;left:0}.ki-burger-menu .path3:before{content:\"\\ea3f\";position:absolute;left:0}.ki-burger-menu .path4:before{content:\"\\ea40\";position:absolute;left:0}.ki-bus .path1:before{content:\"\\ea41\";opacity:.3}.ki-bus .path2:before{content:\"\\ea42\";position:absolute;left:0}.ki-bus .path3:before{content:\"\\ea43\";position:absolute;left:0}.ki-bus .path4:before{content:\"\\ea44\";position:absolute;left:0}.ki-bus .path5:before{content:\"\\ea45\";position:absolute;left:0}.ki-calculator .path1:before{content:\"\\ea46\";opacity:.3}.ki-calculator .path2:before{content:\"\\ea47\";position:absolute;left:0}.ki-calculator .path3:before{content:\"\\ea48\";position:absolute;left:0}.ki-calculator .path4:before{content:\"\\ea49\";position:absolute;left:0}.ki-calculator .path5:before{content:\"\\ea4a\";position:absolute;left:0}.ki-calculator .path6:before{content:\"\\ea4b\";position:absolute;left:0}.ki-calendar-2 .path1:before{content:\"\\ea4c\";opacity:.3}.ki-calendar-2 .path2:before{content:\"\\ea4d\";position:absolute;left:0}.ki-calendar-2 .path3:before{content:\"\\ea4e\";position:absolute;left:0}.ki-calendar-2 .path4:before{content:\"\\ea4f\";position:absolute;left:0}.ki-calendar-2 .path5:before{content:\"\\ea50\";position:absolute;left:0}.ki-calendar-8 .path1:before{content:\"\\ea51\";opacity:.3}.ki-calendar-8 .path2:before{content:\"\\ea52\";position:absolute;left:0}.ki-calendar-8 .path3:before{content:\"\\ea53\";position:absolute;left:0}.ki-calendar-8 .path4:before{content:\"\\ea54\";position:absolute;left:0}.ki-calendar-8 .path5:before{content:\"\\ea55\";position:absolute;left:0}.ki-calendar-8 .path6:before{content:\"\\ea56\";position:absolute;left:0}.ki-calendar-add .path1:before{content:\"\\ea57\";opacity:.3}.ki-calendar-add .path2:before{content:\"\\ea58\";position:absolute;left:0}.ki-calendar-add .path3:before{content:\"\\ea59\";position:absolute;left:0}.ki-calendar-add .path4:before{content:\"\\ea5a\";position:absolute;left:0}.ki-calendar-add .path5:before{content:\"\\ea5b\";position:absolute;left:0}.ki-calendar-add .path6:before{content:\"\\ea5c\";position:absolute;left:0}.ki-calendar-edit .path1:before{content:\"\\ea5d\";opacity:.3}.ki-calendar-edit .path2:before{content:\"\\ea5e\";position:absolute;left:0}.ki-calendar-edit .path3:before{content:\"\\ea5f\";position:absolute;left:0}.ki-calendar-remove .path1:before{content:\"\\ea60\";opacity:.3}.ki-calendar-remove .path2:before{content:\"\\ea61\";position:absolute;left:0}.ki-calendar-remove .path3:before{content:\"\\ea62\";position:absolute;left:0}.ki-calendar-remove .path4:before{content:\"\\ea63\";position:absolute;left:0}.ki-calendar-remove .path5:before{content:\"\\ea64\";position:absolute;left:0}.ki-calendar-remove .path6:before{content:\"\\ea65\";position:absolute;left:0}.ki-calendar-search .path1:before{content:\"\\ea66\";opacity:.3}.ki-calendar-search .path2:before{content:\"\\ea67\";position:absolute;left:0}.ki-calendar-search .path3:before{content:\"\\ea68\";position:absolute;left:0}.ki-calendar-search .path4:before{content:\"\\ea69\";position:absolute;left:0}.ki-calendar-tick .path1:before{content:\"\\ea6a\";opacity:.3}.ki-calendar-tick .path2:before{content:\"\\ea6b\";position:absolute;left:0}.ki-calendar-tick .path3:before{content:\"\\ea6c\";position:absolute;left:0}.ki-calendar-tick .path4:before{content:\"\\ea6d\";position:absolute;left:0}.ki-calendar-tick .path5:before{content:\"\\ea6e\";position:absolute;left:0}.ki-calendar-tick .path6:before{content:\"\\ea6f\";position:absolute;left:0}.ki-calendar .path1:before{content:\"\\ea70\";opacity:.3}.ki-calendar .path2:before{content:\"\\ea71\";position:absolute;left:0}.ki-call .path1:before{content:\"\\ea72\";opacity:.3}.ki-call .path2:before{content:\"\\ea73\";position:absolute;left:0}.ki-call .path3:before{content:\"\\ea74\";position:absolute;left:0}.ki-call .path4:before{content:\"\\ea75\";position:absolute;left:0}.ki-call .path5:before{content:\"\\ea76\";position:absolute;left:0}.ki-call .path6:before{content:\"\\ea77\";position:absolute;left:0}.ki-call .path7:before{content:\"\\ea78\";position:absolute;left:0}.ki-call .path8:before{content:\"\\ea79\";position:absolute;left:0}.ki-capsule .path1:before{content:\"\\ea7a\";opacity:.3}.ki-capsule .path2:before{content:\"\\ea7b\";position:absolute;left:0}.ki-car-2 .path1:before{content:\"\\ea7c\"}.ki-car-2 .path2:before{content:\"\\ea7d\";position:absolute;left:0}.ki-car-2 .path3:before{content:\"\\ea7e\";position:absolute;left:0}.ki-car-2 .path4:before{content:\"\\ea7f\";position:absolute;left:0}.ki-car-2 .path5:before{content:\"\\ea80\";position:absolute;left:0;opacity:.3}.ki-car-2 .path6:before{content:\"\\ea81\";position:absolute;left:0}.ki-car-3 .path1:before{content:\"\\ea82\"}.ki-car-3 .path2:before{content:\"\\ea83\";position:absolute;left:0}.ki-car-3 .path3:before{content:\"\\ea84\";position:absolute;left:0;opacity:.3}.ki-car .path1:before{content:\"\\ea85\";opacity:.3}.ki-car .path2:before{content:\"\\ea86\";position:absolute;left:0}.ki-car .path3:before{content:\"\\ea87\";position:absolute;left:0}.ki-car .path4:before{content:\"\\ea88\";position:absolute;left:0}.ki-car .path5:before{content:\"\\ea89\";position:absolute;left:0}.ki-category .path1:before{content:\"\\ea8a\"}.ki-category .path2:before{content:\"\\ea8b\";position:absolute;left:0;opacity:.3}.ki-category .path3:before{content:\"\\ea8c\";position:absolute;left:0;opacity:.3}.ki-category .path4:before{content:\"\\ea8d\";position:absolute;left:0}.ki-cd .path1:before{content:\"\\ea8e\"}.ki-cd .path2:before{content:\"\\ea8f\";position:absolute;left:0;opacity:.3}.ki-celsius .path1:before{content:\"\\ea90\"}.ki-celsius .path2:before{content:\"\\ea91\";position:absolute;left:0;opacity:.3}.ki-chart-line-down-2 .path1:before{content:\"\\ea92\";opacity:.3}.ki-chart-line-down-2 .path2:before{content:\"\\ea93\";position:absolute;left:0}.ki-chart-line-down-2 .path3:before{content:\"\\ea94\";position:absolute;left:0}.ki-chart-line-down .path1:before{content:\"\\ea95\";opacity:.3}.ki-chart-line-down .path2:before{content:\"\\ea96\";position:absolute;left:0}.ki-chart-line-star .path1:before{content:\"\\ea97\"}.ki-chart-line-star .path2:before{content:\"\\ea98\";position:absolute;left:0}.ki-chart-line-star .path3:before{content:\"\\ea99\";position:absolute;left:0;opacity:.3}.ki-chart-line-up-2 .path1:before{content:\"\\ea9a\"}.ki-chart-line-up-2 .path2:before{content:\"\\ea9b\";position:absolute;left:0;opacity:.3}.ki-chart-line-up .path1:before{content:\"\\ea9c\"}.ki-chart-line-up .path2:before{content:\"\\ea9d\";position:absolute;left:0;opacity:.3}.ki-chart-line .path1:before{content:\"\\ea9e\";opacity:.3}.ki-chart-line .path2:before{content:\"\\ea9f\";position:absolute;left:0}.ki-chart-pie-3 .path1:before{content:\"\\eaa0\"}.ki-chart-pie-3 .path2:before{content:\"\\eaa1\";position:absolute;left:0;opacity:.3}.ki-chart-pie-3 .path3:before{content:\"\\eaa2\";position:absolute;left:0;opacity:.3}.ki-chart-pie-4 .path1:before{content:\"\\eaa3\"}.ki-chart-pie-4 .path2:before{content:\"\\eaa4\";position:absolute;left:0;opacity:.3}.ki-chart-pie-4 .path3:before{content:\"\\eaa5\";position:absolute;left:0;opacity:.3}.ki-chart-pie-simple .path1:before{content:\"\\eaa6\"}.ki-chart-pie-simple .path2:before{content:\"\\eaa7\";position:absolute;left:0;opacity:.3}.ki-chart-pie-too .path1:before{content:\"\\eaa8\";opacity:.3}.ki-chart-pie-too .path2:before{content:\"\\eaa9\";position:absolute;left:0}.ki-chart-simple-2 .path1:before{content:\"\\eaaa\";opacity:.3}.ki-chart-simple-2 .path2:before{content:\"\\eaab\";position:absolute;left:0;opacity:.3}.ki-chart-simple-2 .path3:before{content:\"\\eaac\";position:absolute;left:0}.ki-chart-simple-2 .path4:before{content:\"\\eaad\";position:absolute;left:0;opacity:.3}.ki-chart-simple-3 .path1:before{content:\"\\eaae\";opacity:.3}.ki-chart-simple-3 .path2:before{content:\"\\eaaf\";position:absolute;left:0;opacity:.3}.ki-chart-simple-3 .path3:before{content:\"\\eab0\";position:absolute;left:0}.ki-chart-simple-3 .path4:before{content:\"\\eab1\";position:absolute;left:0;opacity:.3}.ki-chart-simple .path1:before{content:\"\\eab2\";opacity:.3}.ki-chart-simple .path2:before{content:\"\\eab3\";position:absolute;left:0;opacity:.3}.ki-chart-simple .path3:before{content:\"\\eab4\";position:absolute;left:0}.ki-chart-simple .path4:before{content:\"\\eab5\";position:absolute;left:0;opacity:.3}.ki-chart .path1:before{content:\"\\eab6\"}.ki-chart .path2:before{content:\"\\eab7\";position:absolute;left:0;opacity:.3}.ki-check-circle .path1:before{content:\"\\eab8\";opacity:.3}.ki-check-circle .path2:before{content:\"\\eab9\";position:absolute;left:0}.ki-check-square .path1:before{content:\"\\eaba\";opacity:.3}.ki-check-square .path2:before{content:\"\\eabb\";position:absolute;left:0}.ki-check:before{content:\"\\eabc\"}.ki-cheque .path1:before{content:\"\\eabd\"}.ki-cheque .path2:before{content:\"\\eabe\";position:absolute;left:0}.ki-cheque .path3:before{content:\"\\eabf\";position:absolute;left:0}.ki-cheque .path4:before{content:\"\\eac0\";position:absolute;left:0}.ki-cheque .path5:before{content:\"\\eac1\";position:absolute;left:0}.ki-cheque .path6:before{content:\"\\eac2\";position:absolute;left:0;opacity:.3}.ki-cheque .path7:before{content:\"\\eac3\";position:absolute;left:0}.ki-chrome .path1:before{content:\"\\eac4\";opacity:.3}.ki-chrome .path2:before{content:\"\\eac5\";position:absolute;left:0}.ki-classmates .path1:before{content:\"\\eac6\"}.ki-classmates .path2:before{content:\"\\eac7\";position:absolute;left:0;opacity:.3}.ki-click .path1:before{content:\"\\eac8\"}.ki-click .path2:before{content:\"\\eac9\";position:absolute;left:0;opacity:.3}.ki-click .path3:before{content:\"\\eaca\";position:absolute;left:0;opacity:.3}.ki-click .path4:before{content:\"\\eacb\";position:absolute;left:0;opacity:.3}.ki-click .path5:before{content:\"\\eacc\";position:absolute;left:0;opacity:.3}.ki-clipboard .path1:before{content:\"\\eacd\";opacity:.3}.ki-clipboard .path2:before{content:\"\\eace\";position:absolute;left:0}.ki-clipboard .path3:before{content:\"\\eacf\";position:absolute;left:0}.ki-cloud-add .path1:before{content:\"\\ead0\"}.ki-cloud-add .path2:before{content:\"\\ead1\";position:absolute;left:0;opacity:.3}.ki-cloud-change .path1:before{content:\"\\ead2\"}.ki-cloud-change .path2:before{content:\"\\ead3\";position:absolute;left:0}.ki-cloud-change .path3:before{content:\"\\ead4\";position:absolute;left:0;opacity:.3}.ki-cloud-download .path1:before{content:\"\\ead5\";opacity:.3}.ki-cloud-download .path2:before{content:\"\\ead6\";position:absolute;left:0}.ki-cloud:before{content:\"\\ead7\"}.ki-code .path1:before{content:\"\\ead8\";opacity:.3}.ki-code .path2:before{content:\"\\ead9\";position:absolute;left:0}.ki-code .path3:before{content:\"\\eada\";position:absolute;left:0}.ki-code .path4:before{content:\"\\eadb\";position:absolute;left:0}.ki-coffee .path1:before{content:\"\\eadc\";opacity:.3}.ki-coffee .path2:before{content:\"\\eadd\";position:absolute;left:0}.ki-coffee .path3:before{content:\"\\eade\";position:absolute;left:0;opacity:.3}.ki-coffee .path4:before{content:\"\\eadf\";position:absolute;left:0}.ki-coffee .path5:before{content:\"\\eae0\";position:absolute;left:0}.ki-coffee .path6:before{content:\"\\eae1\";position:absolute;left:0}.ki-color-swatch .path1:before{content:\"\\eae2\";opacity:.3}.ki-color-swatch .path2:before{content:\"\\eae3\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path3:before{content:\"\\eae4\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path4:before{content:\"\\eae5\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path5:before{content:\"\\eae6\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path6:before{content:\"\\eae7\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path7:before{content:\"\\eae8\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path8:before{content:\"\\eae9\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path9:before{content:\"\\eaea\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path10:before{content:\"\\eaeb\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path11:before{content:\"\\eaec\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path12:before{content:\"\\eaed\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path13:before{content:\"\\eaee\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path14:before{content:\"\\eaef\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path15:before{content:\"\\eaf0\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path16:before{content:\"\\eaf1\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path17:before{content:\"\\eaf2\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path18:before{content:\"\\eaf3\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path19:before{content:\"\\eaf4\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path20:before{content:\"\\eaf5\";position:absolute;left:0;opacity:.3}.ki-color-swatch .path21:before{content:\"\\eaf6\";position:absolute;left:0}.ki-colors-square .path1:before{content:\"\\eaf7\";opacity:.3}.ki-colors-square .path2:before{content:\"\\eaf8\";position:absolute;left:0}.ki-colors-square .path3:before{content:\"\\eaf9\";position:absolute;left:0;opacity:.3}.ki-colors-square .path4:before{content:\"\\eafa\";position:absolute;left:0;opacity:.3}.ki-compass .path1:before{content:\"\\eafb\"}.ki-compass .path2:before{content:\"\\eafc\";position:absolute;left:0;opacity:.3}.ki-copy-success .path1:before{content:\"\\eafd\"}.ki-copy-success .path2:before{content:\"\\eafe\";position:absolute;left:0;opacity:.3}.ki-copy:before{content:\"\\eaff\"}.ki-courier-express .path1:before{content:\"\\eb00\"}.ki-courier-express .path2:before{content:\"\\eb01\";position:absolute;left:0}.ki-courier-express .path3:before{content:\"\\eb02\";position:absolute;left:0;opacity:.3}.ki-courier-express .path4:before{content:\"\\eb03\";position:absolute;left:0}.ki-courier-express .path5:before{content:\"\\eb04\";position:absolute;left:0}.ki-courier-express .path6:before{content:\"\\eb05\";position:absolute;left:0;opacity:.3}.ki-courier-express .path7:before{content:\"\\eb06\";position:absolute;left:0;opacity:.3}.ki-courier .path1:before{content:\"\\eb07\"}.ki-courier .path2:before{content:\"\\eb08\";position:absolute;left:0;opacity:.3}.ki-courier .path3:before{content:\"\\eb09\";position:absolute;left:0}.ki-credit-cart .path1:before{content:\"\\eb0a\";opacity:.3}.ki-credit-cart .path2:before{content:\"\\eb0b\";position:absolute;left:0}.ki-cross-circle .path1:before{content:\"\\eb0c\";opacity:.3}.ki-cross-circle .path2:before{content:\"\\eb0d\";position:absolute;left:0}.ki-cross-square .path1:before{content:\"\\eb0e\";opacity:.3}.ki-cross-square .path2:before{content:\"\\eb0f\";position:absolute;left:0}.ki-cross .path1:before{content:\"\\eb10\"}.ki-cross .path2:before{content:\"\\eb11\";position:absolute;left:0;opacity:.3}.ki-crown-2 .path1:before{content:\"\\eb12\";opacity:.3}.ki-crown-2 .path2:before{content:\"\\eb13\";position:absolute;left:0}.ki-crown-2 .path3:before{content:\"\\eb14\";position:absolute;left:0}.ki-crown .path1:before{content:\"\\eb15\";opacity:.3}.ki-crown .path2:before{content:\"\\eb16\";position:absolute;left:0}.ki-css .path1:before{content:\"\\eb17\";opacity:.3}.ki-css .path2:before{content:\"\\eb18\";position:absolute;left:0}.ki-cube-2 .path1:before{content:\"\\eb19\";opacity:.3}.ki-cube-2 .path2:before{content:\"\\eb1a\";position:absolute;left:0;opacity:.3}.ki-cube-2 .path3:before{content:\"\\eb1b\";position:absolute;left:0}.ki-cube-3 .path1:before{content:\"\\eb1c\";opacity:.3}.ki-cube-3 .path2:before{content:\"\\eb1d\";position:absolute;left:0}.ki-cup .path1:before{content:\"\\eb1e\";opacity:.3}.ki-cup .path2:before{content:\"\\eb1f\";position:absolute;left:0}.ki-dash .path1:before{content:\"\\eb20\";opacity:.3}.ki-dash .path2:before{content:\"\\eb21\";position:absolute;left:0}.ki-data .path1:before{content:\"\\eb22\"}.ki-data .path2:before{content:\"\\eb23\";position:absolute;left:0}.ki-data .path3:before{content:\"\\eb24\";position:absolute;left:0}.ki-data .path4:before{content:\"\\eb25\";position:absolute;left:0}.ki-data .path5:before{content:\"\\eb26\";position:absolute;left:0;opacity:.3}.ki-delete-files .path1:before{content:\"\\eb27\"}.ki-delete-files .path2:before{content:\"\\eb28\";position:absolute;left:0;opacity:.3}.ki-delete-folder .path1:before{content:\"\\eb29\";opacity:.3}.ki-delete-folder .path2:before{content:\"\\eb2a\";position:absolute;left:0}.ki-delivery-2 .path1:before{content:\"\\eb2b\";opacity:.3}.ki-delivery-2 .path2:before{content:\"\\eb2c\";position:absolute;left:0}.ki-delivery-2 .path3:before{content:\"\\eb2d\";position:absolute;left:0;opacity:.3}.ki-delivery-2 .path4:before{content:\"\\eb2e\";position:absolute;left:0}.ki-delivery-2 .path5:before{content:\"\\eb2f\";position:absolute;left:0}.ki-delivery-2 .path6:before{content:\"\\eb30\";position:absolute;left:0}.ki-delivery-2 .path7:before{content:\"\\eb31\";position:absolute;left:0;opacity:.3}.ki-delivery-2 .path8:before{content:\"\\eb32\";position:absolute;left:0}.ki-delivery-2 .path9:before{content:\"\\eb33\";position:absolute;left:0}.ki-delivery-3 .path1:before{content:\"\\eb34\";opacity:.3}.ki-delivery-3 .path2:before{content:\"\\eb35\";position:absolute;left:0}.ki-delivery-3 .path3:before{content:\"\\eb36\";position:absolute;left:0}.ki-delivery-24 .path1:before{content:\"\\eb37\"}.ki-delivery-24 .path2:before{content:\"\\eb38\";position:absolute;left:0;opacity:.3}.ki-delivery-24 .path3:before{content:\"\\eb39\";position:absolute;left:0}.ki-delivery-24 .path4:before{content:\"\\eb3a\";position:absolute;left:0}.ki-delivery-door .path1:before{content:\"\\eb3b\";opacity:.3}.ki-delivery-door .path2:before{content:\"\\eb3c\";position:absolute;left:0}.ki-delivery-door .path3:before{content:\"\\eb3d\";position:absolute;left:0}.ki-delivery-door .path4:before{content:\"\\eb3e\";position:absolute;left:0}.ki-delivery-geolocation .path1:before{content:\"\\eb3f\"}.ki-delivery-geolocation .path2:before{content:\"\\eb40\";position:absolute;left:0}.ki-delivery-geolocation .path3:before{content:\"\\eb41\";position:absolute;left:0}.ki-delivery-geolocation .path4:before{content:\"\\eb42\";position:absolute;left:0;opacity:.3}.ki-delivery-geolocation .path5:before{content:\"\\eb43\";position:absolute;left:0}.ki-delivery-time .path1:before{content:\"\\eb44\"}.ki-delivery-time .path2:before{content:\"\\eb45\";position:absolute;left:0}.ki-delivery-time .path3:before{content:\"\\eb46\";position:absolute;left:0}.ki-delivery-time .path4:before{content:\"\\eb47\";position:absolute;left:0}.ki-delivery-time .path5:before{content:\"\\eb48\";position:absolute;left:0;opacity:.3}.ki-delivery .path1:before{content:\"\\eb49\"}.ki-delivery .path2:before{content:\"\\eb4a\";position:absolute;left:0;opacity:.3}.ki-delivery .path3:before{content:\"\\eb4b\";position:absolute;left:0}.ki-delivery .path4:before{content:\"\\eb4c\";position:absolute;left:0}.ki-delivery .path5:before{content:\"\\eb4d\";position:absolute;left:0}.ki-design-2 .path1:before{content:\"\\eb4e\";opacity:.3}.ki-design-2 .path2:before{content:\"\\eb4f\";position:absolute;left:0}.ki-design-frame .path1:before{content:\"\\eb50\";opacity:.3}.ki-design-frame .path2:before{content:\"\\eb51\";position:absolute;left:0}.ki-design-mask .path1:before{content:\"\\eb52\";opacity:.3}.ki-design-mask .path2:before{content:\"\\eb53\";position:absolute;left:0}.ki-design .path1:before{content:\"\\eb54\";opacity:.3}.ki-design .path2:before{content:\"\\eb55\";position:absolute;left:0}.ki-devices-2 .path1:before{content:\"\\eb56\";opacity:.3}.ki-devices-2 .path2:before{content:\"\\eb57\";position:absolute;left:0}.ki-devices-2 .path3:before{content:\"\\eb58\";position:absolute;left:0}.ki-devices .path1:before{content:\"\\eb59\";opacity:.3}.ki-devices .path2:before{content:\"\\eb5a\";position:absolute;left:0}.ki-devices .path3:before{content:\"\\eb5b\";position:absolute;left:0}.ki-devices .path4:before{content:\"\\eb5c\";position:absolute;left:0}.ki-devices .path5:before{content:\"\\eb5d\";position:absolute;left:0}.ki-diamonds .path1:before{content:\"\\eb5e\"}.ki-diamonds .path2:before{content:\"\\eb5f\";position:absolute;left:0;opacity:.3}.ki-directbox-default .path1:before{content:\"\\eb60\"}.ki-directbox-default .path2:before{content:\"\\eb61\";position:absolute;left:0;opacity:.3}.ki-directbox-default .path3:before{content:\"\\eb62\";position:absolute;left:0}.ki-directbox-default .path4:before{content:\"\\eb63\";position:absolute;left:0}.ki-disconnect .path1:before{content:\"\\eb64\"}.ki-disconnect .path2:before{content:\"\\eb65\";position:absolute;left:0;opacity:.3}.ki-disconnect .path3:before{content:\"\\eb66\";position:absolute;left:0;opacity:.3}.ki-disconnect .path4:before{content:\"\\eb67\";position:absolute;left:0;opacity:.3}.ki-disconnect .path5:before{content:\"\\eb68\";position:absolute;left:0;opacity:.3}.ki-discount .path1:before{content:\"\\eb69\";opacity:.3}.ki-discount .path2:before{content:\"\\eb6a\";position:absolute;left:0}.ki-disk .path1:before{content:\"\\eb6b\";opacity:.3}.ki-disk .path2:before{content:\"\\eb6c\";position:absolute;left:0}.ki-dislike .path1:before{content:\"\\eb6d\"}.ki-dislike .path2:before{content:\"\\eb6e\";position:absolute;left:0;opacity:.3}.ki-dj:before{content:\"\\eb6f\"}.ki-document .path1:before{content:\"\\eb70\";opacity:.3}.ki-document .path2:before{content:\"\\eb71\";position:absolute;left:0}.ki-dollar .path1:before{content:\"\\eb72\";opacity:.3}.ki-dollar .path2:before{content:\"\\eb73\";position:absolute;left:0}.ki-dollar .path3:before{content:\"\\eb74\";position:absolute;left:0}.ki-dots-circle-vertical .path1:before{content:\"\\eb75\";opacity:.3}.ki-dots-circle-vertical .path2:before{content:\"\\eb76\";position:absolute;left:0}.ki-dots-circle-vertical .path3:before{content:\"\\eb77\";position:absolute;left:0}.ki-dots-circle-vertical .path4:before{content:\"\\eb78\";position:absolute;left:0}.ki-dots-circle .path1:before{content:\"\\eb79\";opacity:.3}.ki-dots-circle .path2:before{content:\"\\eb7a\";position:absolute;left:0}.ki-dots-circle .path3:before{content:\"\\eb7b\";position:absolute;left:0}.ki-dots-circle .path4:before{content:\"\\eb7c\";position:absolute;left:0}.ki-dots-horizontal .path1:before{content:\"\\eb7d\"}.ki-dots-horizontal .path2:before{content:\"\\eb7e\";position:absolute;left:0;opacity:.3}.ki-dots-horizontal .path3:before{content:\"\\eb7f\";position:absolute;left:0;opacity:.3}.ki-dots-square-vertical .path1:before{content:\"\\eb80\";opacity:.3}.ki-dots-square-vertical .path2:before{content:\"\\eb81\";position:absolute;left:0}.ki-dots-square-vertical .path3:before{content:\"\\eb82\";position:absolute;left:0}.ki-dots-square-vertical .path4:before{content:\"\\eb83\";position:absolute;left:0}.ki-dots-square .path1:before{content:\"\\eb84\";opacity:.3}.ki-dots-square .path2:before{content:\"\\eb85\";position:absolute;left:0}.ki-dots-square .path3:before{content:\"\\eb86\";position:absolute;left:0}.ki-dots-square .path4:before{content:\"\\eb87\";position:absolute;left:0}.ki-dots-vertical .path1:before{content:\"\\eb88\"}.ki-dots-vertical .path2:before{content:\"\\eb89\";position:absolute;left:0;opacity:.3}.ki-dots-vertical .path3:before{content:\"\\eb8a\";position:absolute;left:0;opacity:.3}.ki-double-check-circle .path1:before{content:\"\\eb8b\";opacity:.3}.ki-double-check-circle .path2:before{content:\"\\eb8c\";position:absolute;left:0;opacity:.3}.ki-double-check-circle .path3:before{content:\"\\eb8d\";position:absolute;left:0}.ki-double-check .path1:before{content:\"\\eb8e\"}.ki-double-check .path2:before{content:\"\\eb8f\";position:absolute;left:0;opacity:.3}.ki-double-down .path1:before{content:\"\\eb90\"}.ki-double-down .path2:before{content:\"\\eb91\";position:absolute;left:0}.ki-double-down .path3:before{content:\"\\eb92\";position:absolute;left:0;opacity:.3}.ki-double-left-arrow .path1:before{content:\"\\eb93\";opacity:.3}.ki-double-left-arrow .path2:before{content:\"\\eb94\";position:absolute;left:0}.ki-double-left .path1:before{content:\"\\eb95\"}.ki-double-left .path2:before{content:\"\\eb96\";position:absolute;left:0;opacity:.3}.ki-double-right-arrow .path1:before{content:\"\\eb97\";opacity:.3}.ki-double-right-arrow .path2:before{content:\"\\eb98\";position:absolute;left:0}.ki-double-right .path1:before{content:\"\\eb99\"}.ki-double-right .path2:before{content:\"\\eb9a\";position:absolute;left:0;opacity:.3}.ki-double-up .path1:before{content:\"\\eb9b\"}.ki-double-up .path2:before{content:\"\\eb9c\";position:absolute;left:0}.ki-double-up .path3:before{content:\"\\eb9d\";position:absolute;left:0;opacity:.3}.ki-down-square .path1:before{content:\"\\eb9e\";opacity:.3}.ki-down-square .path2:before{content:\"\\eb9f\";position:absolute;left:0}.ki-down:before{content:\"\\eba0\"}.ki-dribbble .path1:before{content:\"\\eba1\"}.ki-dribbble .path2:before{content:\"\\eba2\";position:absolute;left:0}.ki-dribbble .path3:before{content:\"\\eba3\";position:absolute;left:0;opacity:.3}.ki-dribbble .path4:before{content:\"\\eba4\";position:absolute;left:0;opacity:.3}.ki-dribbble .path5:before{content:\"\\eba5\";position:absolute;left:0}.ki-dribbble .path6:before{content:\"\\eba6\";position:absolute;left:0}.ki-drop .path1:before{content:\"\\eba7\"}.ki-drop .path2:before{content:\"\\eba8\";position:absolute;left:0;opacity:.3}.ki-dropbox .path1:before{content:\"\\eba9\";opacity:.4}.ki-dropbox .path2:before{content:\"\\ebaa\";position:absolute;left:0;opacity:.4}.ki-dropbox .path3:before{content:\"\\ebab\";position:absolute;left:0;opacity:.4}.ki-dropbox .path4:before{content:\"\\ebac\";position:absolute;left:0;opacity:.4}.ki-dropbox .path5:before{content:\"\\ebad\";position:absolute;left:0}.ki-educare .path1:before{content:\"\\ebae\";opacity:.3}.ki-educare .path2:before{content:\"\\ebaf\";position:absolute;left:0;opacity:.3}.ki-educare .path3:before{content:\"\\ebb0\";position:absolute;left:0}.ki-educare .path4:before{content:\"\\ebb1\";position:absolute;left:0}.ki-electricity .path1:before{content:\"\\ebb2\"}.ki-electricity .path2:before{content:\"\\ebb3\";position:absolute;left:0}.ki-electricity .path3:before{content:\"\\ebb4\";position:absolute;left:0}.ki-electricity .path4:before{content:\"\\ebb5\";position:absolute;left:0}.ki-electricity .path5:before{content:\"\\ebb6\";position:absolute;left:0;opacity:.3}.ki-electricity .path6:before{content:\"\\ebb7\";position:absolute;left:0}.ki-electricity .path7:before{content:\"\\ebb8\";position:absolute;left:0}.ki-electricity .path8:before{content:\"\\ebb9\";position:absolute;left:0}.ki-electricity .path9:before{content:\"\\ebba\";position:absolute;left:0}.ki-electricity .path10:before{content:\"\\ebbb\";position:absolute;left:0}.ki-electronic-clock .path1:before{content:\"\\ebbc\";opacity:.3}.ki-electronic-clock .path2:before{content:\"\\ebbd\";position:absolute;left:0}.ki-electronic-clock .path3:before{content:\"\\ebbe\";position:absolute;left:0}.ki-electronic-clock .path4:before{content:\"\\ebbf\";position:absolute;left:0}.ki-element-1 .path1:before{content:\"\\ebc0\"}.ki-element-1 .path2:before{content:\"\\ebc1\";position:absolute;left:0;opacity:.3}.ki-element-1 .path3:before{content:\"\\ebc2\";position:absolute;left:0;opacity:.3}.ki-element-1 .path4:before{content:\"\\ebc3\";position:absolute;left:0}.ki-element-2 .path1:before{content:\"\\ebc4\"}.ki-element-2 .path2:before{content:\"\\ebc5\";position:absolute;left:0;opacity:.3}.ki-element-3 .path1:before{content:\"\\ebc6\";opacity:.3}.ki-element-3 .path2:before{content:\"\\ebc7\";position:absolute;left:0}.ki-element-4 .path1:before{content:\"\\ebc8\"}.ki-element-4 .path2:before{content:\"\\ebc9\";position:absolute;left:0;opacity:.3}.ki-element-5 .path1:before{content:\"\\ebca\"}.ki-element-5 .path2:before{content:\"\\ebcb\";position:absolute;left:0;opacity:.3}.ki-element-6 .path1:before{content:\"\\ebcc\";opacity:.3}.ki-element-6 .path2:before{content:\"\\ebcd\";position:absolute;left:0}.ki-element-7 .path1:before{content:\"\\ebce\"}.ki-element-7 .path2:before{content:\"\\ebcf\";position:absolute;left:0;opacity:.3}.ki-element-8 .path1:before{content:\"\\ebd0\";opacity:.3}.ki-element-8 .path2:before{content:\"\\ebd1\";position:absolute;left:0}.ki-element-9 .path1:before{content:\"\\ebd2\";opacity:.3}.ki-element-9 .path2:before{content:\"\\ebd3\";position:absolute;left:0}.ki-element-10 .path1:before{content:\"\\ebd4\"}.ki-element-10 .path2:before{content:\"\\ebd5\";position:absolute;left:0;opacity:.3}.ki-element-10 .path3:before{content:\"\\ebd6\";position:absolute;left:0;opacity:.3}.ki-element-11 .path1:before{content:\"\\ebd7\"}.ki-element-11 .path2:before{content:\"\\ebd8\";position:absolute;left:0;opacity:.3}.ki-element-11 .path3:before{content:\"\\ebd9\";position:absolute;left:0}.ki-element-11 .path4:before{content:\"\\ebda\";position:absolute;left:0;opacity:.3}.ki-element-12 .path1:before{content:\"\\ebdb\";opacity:.3}.ki-element-12 .path2:before{content:\"\\ebdc\";position:absolute;left:0;opacity:.3}.ki-element-12 .path3:before{content:\"\\ebdd\";position:absolute;left:0}.ki-element-equal .path1:before{content:\"\\ebde\"}.ki-element-equal .path2:before{content:\"\\ebdf\";position:absolute;left:0;opacity:.3}.ki-element-equal .path3:before{content:\"\\ebe0\";position:absolute;left:0;opacity:.3}.ki-element-equal .path4:before{content:\"\\ebe1\";position:absolute;left:0}.ki-element-equal .path5:before{content:\"\\ebe2\";position:absolute;left:0}.ki-element-plus .path1:before{content:\"\\ebe3\"}.ki-element-plus .path2:before{content:\"\\ebe4\";position:absolute;left:0;opacity:.3}.ki-element-plus .path3:before{content:\"\\ebe5\";position:absolute;left:0;opacity:.3}.ki-element-plus .path4:before{content:\"\\ebe6\";position:absolute;left:0}.ki-element-plus .path5:before{content:\"\\ebe7\";position:absolute;left:0}.ki-emoji-happy .path1:before{content:\"\\ebe8\";opacity:.3}.ki-emoji-happy .path2:before{content:\"\\ebe9\";position:absolute;left:0}.ki-emoji-happy .path3:before{content:\"\\ebea\";position:absolute;left:0}.ki-emoji-happy .path4:before{content:\"\\ebeb\";position:absolute;left:0}.ki-enjin-coin .path1:before{content:\"\\ebec\"}.ki-enjin-coin .path2:before{content:\"\\ebed\";position:absolute;left:0;opacity:.3}.ki-entrance-left .path1:before{content:\"\\ebee\"}.ki-entrance-left .path2:before{content:\"\\ebef\";position:absolute;left:0;opacity:.3}.ki-entrance-right .path1:before{content:\"\\ebf0\";opacity:.3}.ki-entrance-right .path2:before{content:\"\\ebf1\";position:absolute;left:0}.ki-eraser .path1:before{content:\"\\ebf2\";opacity:.3}.ki-eraser .path2:before{content:\"\\ebf3\";position:absolute;left:0}.ki-eraser .path3:before{content:\"\\ebf4\";position:absolute;left:0;opacity:.3}.ki-euro .path1:before{content:\"\\ebf5\";opacity:.3}.ki-euro .path2:before{content:\"\\ebf6\";position:absolute;left:0}.ki-euro .path3:before{content:\"\\ebf7\";position:absolute;left:0}.ki-exit-down .path1:before{content:\"\\ebf8\";opacity:.3}.ki-exit-down .path2:before{content:\"\\ebf9\";position:absolute;left:0}.ki-exit-left .path1:before{content:\"\\ebfa\";opacity:.3}.ki-exit-left .path2:before{content:\"\\ebfb\";position:absolute;left:0}.ki-exit-right-corner .path1:before{content:\"\\ebfc\";opacity:.3}.ki-exit-right-corner .path2:before{content:\"\\ebfd\";position:absolute;left:0}.ki-exit-right .path1:before{content:\"\\ebfe\";opacity:.3}.ki-exit-right .path2:before{content:\"\\ebff\";position:absolute;left:0}.ki-exit-up .path1:before{content:\"\\ec00\";opacity:.3}.ki-exit-up .path2:before{content:\"\\ec01\";position:absolute;left:0}.ki-external-drive .path1:before{content:\"\\ec02\"}.ki-external-drive .path2:before{content:\"\\ec03\";position:absolute;left:0}.ki-external-drive .path3:before{content:\"\\ec04\";position:absolute;left:0}.ki-external-drive .path4:before{content:\"\\ec05\";position:absolute;left:0;opacity:.3}.ki-external-drive .path5:before{content:\"\\ec06\";position:absolute;left:0}.ki-eye-slash .path1:before{content:\"\\ec07\"}.ki-eye-slash .path2:before{content:\"\\ec08\";position:absolute;left:0}.ki-eye-slash .path3:before{content:\"\\ec09\";position:absolute;left:0;opacity:.3}.ki-eye-slash .path4:before{content:\"\\ec0a\";position:absolute;left:0;opacity:.3}.ki-eye .path1:before{content:\"\\ec0b\"}.ki-eye .path2:before{content:\"\\ec0c\";position:absolute;left:0;opacity:.3}.ki-eye .path3:before{content:\"\\ec0d\";position:absolute;left:0;opacity:.3}.ki-facebook .path1:before{content:\"\\ec0e\";opacity:.3}.ki-facebook .path2:before{content:\"\\ec0f\";position:absolute;left:0}.ki-faceid .path1:before{content:\"\\ec10\";opacity:.3}.ki-faceid .path2:before{content:\"\\ec11\";position:absolute;left:0}.ki-faceid .path3:before{content:\"\\ec12\";position:absolute;left:0}.ki-faceid .path4:before{content:\"\\ec13\";position:absolute;left:0}.ki-faceid .path5:before{content:\"\\ec14\";position:absolute;left:0}.ki-faceid .path6:before{content:\"\\ec15\";position:absolute;left:0}.ki-fasten .path1:before{content:\"\\ec16\";opacity:.3}.ki-fasten .path2:before{content:\"\\ec17\";position:absolute;left:0}.ki-fat-rows .path1:before{content:\"\\ec18\"}.ki-fat-rows .path2:before{content:\"\\ec19\";position:absolute;left:0;opacity:.3}.ki-feather .path1:before{content:\"\\ec1a\";opacity:.3}.ki-feather .path2:before{content:\"\\ec1b\";position:absolute;left:0}.ki-figma .path1:before{content:\"\\ec1c\";opacity:.4}.ki-figma .path2:before{content:\"\\ec1d\";position:absolute;left:0;opacity:.4}.ki-figma .path3:before{content:\"\\ec1e\";position:absolute;left:0}.ki-figma .path4:before{content:\"\\ec1f\";position:absolute;left:0}.ki-figma .path5:before{content:\"\\ec20\";position:absolute;left:0}.ki-file-added .path1:before{content:\"\\ec21\"}.ki-file-added .path2:before{content:\"\\ec22\";position:absolute;left:0;opacity:.3}.ki-file-deleted .path1:before{content:\"\\ec23\"}.ki-file-deleted .path2:before{content:\"\\ec24\";position:absolute;left:0;opacity:.3}.ki-file-down .path1:before{content:\"\\ec25\";opacity:.3}.ki-file-down .path2:before{content:\"\\ec26\";position:absolute;left:0}.ki-file-left .path1:before{content:\"\\ec27\";opacity:.3}.ki-file-left .path2:before{content:\"\\ec28\";position:absolute;left:0}.ki-file-right .path1:before{content:\"\\ec29\";opacity:.3}.ki-file-right .path2:before{content:\"\\ec2a\";position:absolute;left:0}.ki-file-sheet .path1:before{content:\"\\ec2b\"}.ki-file-sheet .path2:before{content:\"\\ec2c\";position:absolute;left:0;opacity:.3}.ki-file-up .path1:before{content:\"\\ec2d\";opacity:.3}.ki-file-up .path2:before{content:\"\\ec2e\";position:absolute;left:0}.ki-file .path1:before{content:\"\\ec2f\"}.ki-file .path2:before{content:\"\\ec30\";position:absolute;left:0;opacity:.3}.ki-files-tablet .path1:before{content:\"\\ec31\"}.ki-files-tablet .path2:before{content:\"\\ec32\";position:absolute;left:0;opacity:.3}.ki-filter-edit .path1:before{content:\"\\ec33\";opacity:.3}.ki-filter-edit .path2:before{content:\"\\ec34\";position:absolute;left:0}.ki-filter-search .path1:before{content:\"\\ec35\";opacity:.3}.ki-filter-search .path2:before{content:\"\\ec36\";position:absolute;left:0}.ki-filter-search .path3:before{content:\"\\ec37\";position:absolute;left:0}.ki-filter-square .path1:before{content:\"\\ec38\"}.ki-filter-square .path2:before{content:\"\\ec39\";position:absolute;left:0;opacity:.3}.ki-filter-tablet .path1:before{content:\"\\ec3a\";opacity:.3}.ki-filter-tablet .path2:before{content:\"\\ec3b\";position:absolute;left:0}.ki-filter-tick .path1:before{content:\"\\ec3c\";opacity:.3}.ki-filter-tick .path2:before{content:\"\\ec3d\";position:absolute;left:0}.ki-filter .path1:before{content:\"\\ec3e\"}.ki-filter .path2:before{content:\"\\ec3f\";position:absolute;left:0;opacity:.3}.ki-finance-calculator .path1:before{content:\"\\ec40\";opacity:.3}.ki-finance-calculator .path2:before{content:\"\\ec41\";position:absolute;left:0}.ki-finance-calculator .path3:before{content:\"\\ec42\";position:absolute;left:0}.ki-finance-calculator .path4:before{content:\"\\ec43\";position:absolute;left:0}.ki-finance-calculator .path5:before{content:\"\\ec44\";position:absolute;left:0}.ki-finance-calculator .path6:before{content:\"\\ec45\";position:absolute;left:0}.ki-finance-calculator .path7:before{content:\"\\ec46\";position:absolute;left:0}.ki-financial-schedule .path1:before{content:\"\\ec47\";opacity:.3}.ki-financial-schedule .path2:before{content:\"\\ec48\";position:absolute;left:0}.ki-financial-schedule .path3:before{content:\"\\ec49\";position:absolute;left:0}.ki-financial-schedule .path4:before{content:\"\\ec4a\";position:absolute;left:0;opacity:.3}.ki-fingerprint-scanning .path1:before{content:\"\\ec4b\"}.ki-fingerprint-scanning .path2:before{content:\"\\ec4c\";position:absolute;left:0;opacity:.3}.ki-fingerprint-scanning .path3:before{content:\"\\ec4d\";position:absolute;left:0;opacity:.3}.ki-fingerprint-scanning .path4:before{content:\"\\ec4e\";position:absolute;left:0;opacity:.3}.ki-fingerprint-scanning .path5:before{content:\"\\ec4f\";position:absolute;left:0;opacity:.3}.ki-flag .path1:before{content:\"\\ec50\";opacity:.3}.ki-flag .path2:before{content:\"\\ec51\";position:absolute;left:0}.ki-flash-circle .path1:before{content:\"\\ec52\";opacity:.3}.ki-flash-circle .path2:before{content:\"\\ec53\";position:absolute;left:0}.ki-flask .path1:before{content:\"\\ec54\";opacity:.3}.ki-flask .path2:before{content:\"\\ec55\";position:absolute;left:0}.ki-focus .path1:before{content:\"\\ec56\";opacity:.3}.ki-focus .path2:before{content:\"\\ec57\";position:absolute;left:0}.ki-folder-added .path1:before{content:\"\\ec58\";opacity:.3}.ki-folder-added .path2:before{content:\"\\ec59\";position:absolute;left:0}.ki-folder-down .path1:before{content:\"\\ec5a\";opacity:.3}.ki-folder-down .path2:before{content:\"\\ec5b\";position:absolute;left:0}.ki-folder-up .path1:before{content:\"\\ec5c\";opacity:.3}.ki-folder-up .path2:before{content:\"\\ec5d\";position:absolute;left:0}.ki-folder .path1:before{content:\"\\ec5e\";opacity:.3}.ki-folder .path2:before{content:\"\\ec5f\";position:absolute;left:0}.ki-frame .path1:before{content:\"\\ec60\"}.ki-frame .path2:before{content:\"\\ec61\";position:absolute;left:0}.ki-frame .path3:before{content:\"\\ec62\";position:absolute;left:0;opacity:.3}.ki-frame .path4:before{content:\"\\ec63\";position:absolute;left:0;opacity:.3}.ki-gear .path1:before{content:\"\\ec64\";opacity:.3}.ki-gear .path2:before{content:\"\\ec65\";position:absolute;left:0}.ki-general-mouse .path1:before{content:\"\\ec66\";opacity:.3}.ki-general-mouse .path2:before{content:\"\\ec67\";position:absolute;left:0}.ki-geolocation-home .path1:before{content:\"\\ec68\";opacity:.3}.ki-geolocation-home .path2:before{content:\"\\ec69\";position:absolute;left:0}.ki-geolocation .path1:before{content:\"\\ec6a\";opacity:.3}.ki-geolocation .path2:before{content:\"\\ec6b\";position:absolute;left:0}.ki-ghost .path1:before{content:\"\\ec6c\"}.ki-ghost .path2:before{content:\"\\ec6d\";position:absolute;left:0}.ki-ghost .path3:before{content:\"\\ec6e\";position:absolute;left:0;opacity:.3}.ki-gift .path1:before{content:\"\\ec6f\";opacity:.3}.ki-gift .path2:before{content:\"\\ec70\";position:absolute;left:0}.ki-gift .path3:before{content:\"\\ec71\";position:absolute;left:0;opacity:.3}.ki-gift .path4:before{content:\"\\ec72\";position:absolute;left:0;opacity:.3}.ki-github .path1:before{content:\"\\ec73\";opacity:.3}.ki-github .path2:before{content:\"\\ec74\";position:absolute;left:0}.ki-glass .path1:before{content:\"\\ec75\"}.ki-glass .path2:before{content:\"\\ec76\";position:absolute;left:0;opacity:.3}.ki-glass .path3:before{content:\"\\ec77\";position:absolute;left:0}.ki-google-play .path1:before{content:\"\\ec78\";opacity:.3}.ki-google-play .path2:before{content:\"\\ec79\";position:absolute;left:0}.ki-google .path1:before{content:\"\\ec7a\";opacity:.3}.ki-google .path2:before{content:\"\\ec7b\";position:absolute;left:0}.ki-graph-2 .path1:before{content:\"\\ec7c\";opacity:.3}.ki-graph-2 .path2:before{content:\"\\ec7d\";position:absolute;left:0}.ki-graph-2 .path3:before{content:\"\\ec7e\";position:absolute;left:0}.ki-graph-3 .path1:before{content:\"\\ec7f\";opacity:.3}.ki-graph-3 .path2:before{content:\"\\ec80\";position:absolute;left:0}.ki-graph-4 .path1:before{content:\"\\ec81\"}.ki-graph-4 .path2:before{content:\"\\ec82\";position:absolute;left:0;opacity:.3}.ki-graph-up .path1:before{content:\"\\ec83\";opacity:.3}.ki-graph-up .path2:before{content:\"\\ec84\";position:absolute;left:0}.ki-graph-up .path3:before{content:\"\\ec85\";position:absolute;left:0}.ki-graph-up .path4:before{content:\"\\ec86\";position:absolute;left:0}.ki-graph-up .path5:before{content:\"\\ec87\";position:absolute;left:0}.ki-graph-up .path6:before{content:\"\\ec88\";position:absolute;left:0}.ki-graph .path1:before{content:\"\\ec89\";opacity:.3}.ki-graph .path2:before{content:\"\\ec8a\";position:absolute;left:0}.ki-graph .path3:before{content:\"\\ec8b\";position:absolute;left:0}.ki-graph .path4:before{content:\"\\ec8c\";position:absolute;left:0}.ki-grid-2 .path1:before{content:\"\\ec8d\";opacity:.3}.ki-grid-2 .path2:before{content:\"\\ec8e\";position:absolute;left:0}.ki-grid-frame .path1:before{content:\"\\ec8f\"}.ki-grid-frame .path2:before{content:\"\\ec90\";position:absolute;left:0;opacity:.3}.ki-grid-frame .path3:before{content:\"\\ec91\";position:absolute;left:0}.ki-grid .path1:before{content:\"\\ec92\";opacity:.3}.ki-grid .path2:before{content:\"\\ec93\";position:absolute;left:0}.ki-handcart:before{content:\"\\ec94\"}.ki-happy-emoji .path1:before{content:\"\\ec95\";opacity:.3}.ki-happy-emoji .path2:before{content:\"\\ec96\";position:absolute;left:0}.ki-heart-circle .path1:before{content:\"\\ec97\";opacity:.3}.ki-heart-circle .path2:before{content:\"\\ec98\";position:absolute;left:0}.ki-heart .path1:before{content:\"\\ec99\";opacity:.3}.ki-heart .path2:before{content:\"\\ec9a\";position:absolute;left:0}.ki-home-1 .path1:before{content:\"\\ec9b\";opacity:.3}.ki-home-1 .path2:before{content:\"\\ec9c\";position:absolute;left:0}.ki-home-2 .path1:before{content:\"\\ec9d\";opacity:.3}.ki-home-2 .path2:before{content:\"\\ec9e\";position:absolute;left:0}.ki-home-3 .path1:before{content:\"\\ec9f\";opacity:.3}.ki-home-3 .path2:before{content:\"\\eca0\";position:absolute;left:0}.ki-home:before{content:\"\\eca1\"}.ki-html .path1:before{content:\"\\eca2\";opacity:.3}.ki-html .path2:before{content:\"\\eca3\";position:absolute;left:0}.ki-icon .path1:before{content:\"\\eca4\";opacity:.3}.ki-icon .path2:before{content:\"\\eca5\";position:absolute;left:0}.ki-icon .path3:before{content:\"\\eca6\";position:absolute;left:0}.ki-illustrator .path1:before{content:\"\\eca7\";opacity:.3}.ki-illustrator .path2:before{content:\"\\eca8\";position:absolute;left:0}.ki-illustrator .path3:before{content:\"\\eca9\";position:absolute;left:0}.ki-illustrator .path4:before{content:\"\\ecaa\";position:absolute;left:0}.ki-information-2 .path1:before{content:\"\\ecab\";opacity:.3}.ki-information-2 .path2:before{content:\"\\ecac\";position:absolute;left:0}.ki-information-2 .path3:before{content:\"\\ecad\";position:absolute;left:0}.ki-information-3 .path1:before{content:\"\\ecae\";opacity:.3}.ki-information-3 .path2:before{content:\"\\ecaf\";position:absolute;left:0}.ki-information-3 .path3:before{content:\"\\ecb0\";position:absolute;left:0}.ki-information-4 .path1:before{content:\"\\ecb1\";opacity:.3}.ki-information-4 .path2:before{content:\"\\ecb2\";position:absolute;left:0}.ki-information-4 .path3:before{content:\"\\ecb3\";position:absolute;left:0}.ki-information-5 .path1:before{content:\"\\ecb4\";opacity:.3}.ki-information-5 .path2:before{content:\"\\ecb5\";position:absolute;left:0}.ki-information-5 .path3:before{content:\"\\ecb6\";position:absolute;left:0}.ki-information .path1:before{content:\"\\ecb7\";opacity:.3}.ki-information .path2:before{content:\"\\ecb8\";position:absolute;left:0}.ki-information .path3:before{content:\"\\ecb9\";position:absolute;left:0}.ki-instagram .path1:before{content:\"\\ecba\";opacity:.3}.ki-instagram .path2:before{content:\"\\ecbb\";position:absolute;left:0}.ki-joystick .path1:before{content:\"\\ecbc\"}.ki-joystick .path2:before{content:\"\\ecbd\";position:absolute;left:0;opacity:.3}.ki-joystick .path3:before{content:\"\\ecbe\";position:absolute;left:0;opacity:.3}.ki-joystick .path4:before{content:\"\\ecbf\";position:absolute;left:0}.ki-js-2 .path1:before{content:\"\\ecc0\"}.ki-js-2 .path2:before{content:\"\\ecc1\";position:absolute;left:0;opacity:.3}.ki-js .path1:before{content:\"\\ecc2\"}.ki-js .path2:before{content:\"\\ecc3\";position:absolute;left:0;opacity:.3}.ki-kanban .path1:before{content:\"\\ecc4\"}.ki-kanban .path2:before{content:\"\\ecc5\";position:absolute;left:0;opacity:.3}.ki-key-square .path1:before{content:\"\\ecc6\";opacity:.3}.ki-key-square .path2:before{content:\"\\ecc7\";position:absolute;left:0}.ki-key .path1:before{content:\"\\ecc8\";opacity:.3}.ki-key .path2:before{content:\"\\ecc9\";position:absolute;left:0}.ki-keyboard .path1:before{content:\"\\ecca\"}.ki-keyboard .path2:before{content:\"\\eccb\";position:absolute;left:0;opacity:.3}.ki-laptop .path1:before{content:\"\\eccc\";opacity:.3}.ki-laptop .path2:before{content:\"\\eccd\";position:absolute;left:0}.ki-laravel .path1:before{content:\"\\ecce\";opacity:.3}.ki-laravel .path2:before{content:\"\\eccf\";position:absolute;left:0;opacity:.3}.ki-laravel .path3:before{content:\"\\ecd0\";position:absolute;left:0}.ki-laravel .path4:before{content:\"\\ecd1\";position:absolute;left:0;opacity:.3}.ki-laravel .path5:before{content:\"\\ecd2\";position:absolute;left:0}.ki-laravel .path6:before{content:\"\\ecd3\";position:absolute;left:0}.ki-laravel .path7:before{content:\"\\ecd4\";position:absolute;left:0;opacity:.3}.ki-left-square .path1:before{content:\"\\ecd5\";opacity:.3}.ki-left-square .path2:before{content:\"\\ecd6\";position:absolute;left:0}.ki-left:before{content:\"\\ecd7\"}.ki-like-2 .path1:before{content:\"\\ecd8\";opacity:.3}.ki-like-2 .path2:before{content:\"\\ecd9\";position:absolute;left:0}.ki-like-folder .path1:before{content:\"\\ecda\";opacity:.3}.ki-like-folder .path2:before{content:\"\\ecdb\";position:absolute;left:0}.ki-like-shapes .path1:before{content:\"\\ecdc\";opacity:.3}.ki-like-shapes .path2:before{content:\"\\ecdd\";position:absolute;left:0}.ki-like-tag .path1:before{content:\"\\ecde\";opacity:.3}.ki-like-tag .path2:before{content:\"\\ecdf\";position:absolute;left:0}.ki-like .path1:before{content:\"\\ece0\"}.ki-like .path2:before{content:\"\\ece1\";position:absolute;left:0;opacity:.3}.ki-loading .path1:before{content:\"\\ece2\"}.ki-loading .path2:before{content:\"\\ece3\";position:absolute;left:0;opacity:.3}.ki-lock-2 .path1:before{content:\"\\ece4\"}.ki-lock-2 .path2:before{content:\"\\ece5\";position:absolute;left:0}.ki-lock-2 .path3:before{content:\"\\ece6\";position:absolute;left:0}.ki-lock-2 .path4:before{content:\"\\ece7\";position:absolute;left:0;opacity:.3}.ki-lock-2 .path5:before{content:\"\\ece8\";position:absolute;left:0}.ki-lock-3 .path1:before{content:\"\\ece9\";opacity:.3}.ki-lock-3 .path2:before{content:\"\\ecea\";position:absolute;left:0}.ki-lock-3 .path3:before{content:\"\\eceb\";position:absolute;left:0}.ki-lock .path1:before{content:\"\\ecec\";opacity:.3}.ki-lock .path2:before{content:\"\\eced\";position:absolute;left:0}.ki-lock .path3:before{content:\"\\ecee\";position:absolute;left:0}.ki-logistic .path1:before{content:\"\\ecef\";opacity:.3}.ki-logistic .path2:before{content:\"\\ecf0\";position:absolute;left:0}.ki-logistic .path3:before{content:\"\\ecf1\";position:absolute;left:0}.ki-logistic .path4:before{content:\"\\ecf2\";position:absolute;left:0}.ki-logistic .path5:before{content:\"\\ecf3\";position:absolute;left:0}.ki-logistic .path6:before{content:\"\\ecf4\";position:absolute;left:0}.ki-logistic .path7:before{content:\"\\ecf5\";position:absolute;left:0}.ki-lots-shopping .path1:before{content:\"\\ecf6\";opacity:.3}.ki-lots-shopping .path2:before{content:\"\\ecf7\";position:absolute;left:0}.ki-lots-shopping .path3:before{content:\"\\ecf8\";position:absolute;left:0;opacity:.3}.ki-lots-shopping .path4:before{content:\"\\ecf9\";position:absolute;left:0}.ki-lots-shopping .path5:before{content:\"\\ecfa\";position:absolute;left:0;opacity:.3}.ki-lots-shopping .path6:before{content:\"\\ecfb\";position:absolute;left:0}.ki-lots-shopping .path7:before{content:\"\\ecfc\";position:absolute;left:0;opacity:.3}.ki-lots-shopping .path8:before{content:\"\\ecfd\";position:absolute;left:0}.ki-lovely .path1:before{content:\"\\ecfe\";opacity:.3}.ki-lovely .path2:before{content:\"\\ecff\";position:absolute;left:0}.ki-lts .path1:before{content:\"\\ed00\"}.ki-lts .path2:before{content:\"\\ed01\";position:absolute;left:0;opacity:.3}.ki-magnifier .path1:before{content:\"\\ed02\";opacity:.3}.ki-magnifier .path2:before{content:\"\\ed03\";position:absolute;left:0}.ki-map .path1:before{content:\"\\ed04\";opacity:.3}.ki-map .path2:before{content:\"\\ed05\";position:absolute;left:0}.ki-map .path3:before{content:\"\\ed06\";position:absolute;left:0}.ki-mask .path1:before{content:\"\\ed07\";opacity:.3}.ki-mask .path2:before{content:\"\\ed08\";position:absolute;left:0}.ki-mask .path3:before{content:\"\\ed09\";position:absolute;left:0}.ki-maximize .path1:before{content:\"\\ed0a\";opacity:.3}.ki-maximize .path2:before{content:\"\\ed0b\";position:absolute;left:0}.ki-maximize .path3:before{content:\"\\ed0c\";position:absolute;left:0}.ki-maximize .path4:before{content:\"\\ed0d\";position:absolute;left:0}.ki-maximize .path5:before{content:\"\\ed0e\";position:absolute;left:0}.ki-medal-star .path1:before{content:\"\\ed0f\"}.ki-medal-star .path2:before{content:\"\\ed10\";position:absolute;left:0;opacity:.3}.ki-medal-star .path3:before{content:\"\\ed11\";position:absolute;left:0}.ki-medal-star .path4:before{content:\"\\ed12\";position:absolute;left:0}.ki-menu .path1:before{content:\"\\ed13\";opacity:.3}.ki-menu .path2:before{content:\"\\ed14\";position:absolute;left:0}.ki-menu .path3:before{content:\"\\ed15\";position:absolute;left:0}.ki-menu .path4:before{content:\"\\ed16\";position:absolute;left:0}.ki-message-add .path1:before{content:\"\\ed17\";opacity:.3}.ki-message-add .path2:before{content:\"\\ed18\";position:absolute;left:0}.ki-message-add .path3:before{content:\"\\ed19\";position:absolute;left:0}.ki-message-edit .path1:before{content:\"\\ed1a\";opacity:.3}.ki-message-edit .path2:before{content:\"\\ed1b\";position:absolute;left:0}.ki-message-minus .path1:before{content:\"\\ed1c\";opacity:.3}.ki-message-minus .path2:before{content:\"\\ed1d\";position:absolute;left:0}.ki-message-notif .path1:before{content:\"\\ed1e\"}.ki-message-notif .path2:before{content:\"\\ed1f\";position:absolute;left:0;opacity:.3}.ki-message-notif .path3:before{content:\"\\ed20\";position:absolute;left:0}.ki-message-notif .path4:before{content:\"\\ed21\";position:absolute;left:0}.ki-message-notif .path5:before{content:\"\\ed22\";position:absolute;left:0}.ki-message-programming .path1:before{content:\"\\ed23\";opacity:.3}.ki-message-programming .path2:before{content:\"\\ed24\";position:absolute;left:0}.ki-message-programming .path3:before{content:\"\\ed25\";position:absolute;left:0}.ki-message-programming .path4:before{content:\"\\ed26\";position:absolute;left:0}.ki-message-question .path1:before{content:\"\\ed27\";opacity:.3}.ki-message-question .path2:before{content:\"\\ed28\";position:absolute;left:0}.ki-message-question .path3:before{content:\"\\ed29\";position:absolute;left:0}.ki-message-text-2 .path1:before{content:\"\\ed2a\";opacity:.3}.ki-message-text-2 .path2:before{content:\"\\ed2b\";position:absolute;left:0}.ki-message-text-2 .path3:before{content:\"\\ed2c\";position:absolute;left:0}.ki-message-text .path1:before{content:\"\\ed2d\";opacity:.3}.ki-message-text .path2:before{content:\"\\ed2e\";position:absolute;left:0}.ki-message-text .path3:before{content:\"\\ed2f\";position:absolute;left:0}.ki-messages .path1:before{content:\"\\ed30\"}.ki-messages .path2:before{content:\"\\ed31\";position:absolute;left:0;opacity:.3}.ki-messages .path3:before{content:\"\\ed32\";position:absolute;left:0}.ki-messages .path4:before{content:\"\\ed33\";position:absolute;left:0}.ki-messages .path5:before{content:\"\\ed34\";position:absolute;left:0}.ki-microsoft .path1:before{content:\"\\ed35\";opacity:.3}.ki-microsoft .path2:before{content:\"\\ed36\";position:absolute;left:0}.ki-microsoft .path3:before{content:\"\\ed37\";position:absolute;left:0}.ki-microsoft .path4:before{content:\"\\ed38\";position:absolute;left:0;opacity:.3}.ki-milk .path1:before{content:\"\\ed39\";opacity:.3}.ki-milk .path2:before{content:\"\\ed3a\";position:absolute;left:0}.ki-milk .path3:before{content:\"\\ed3b\";position:absolute;left:0}.ki-minus-circle .path1:before{content:\"\\ed3c\";opacity:.3}.ki-minus-circle .path2:before{content:\"\\ed3d\";position:absolute;left:0}.ki-minus-folder .path1:before{content:\"\\ed3e\";opacity:.3}.ki-minus-folder .path2:before{content:\"\\ed3f\";position:absolute;left:0}.ki-minus-square .path1:before{content:\"\\ed40\";opacity:.3}.ki-minus-square .path2:before{content:\"\\ed41\";position:absolute;left:0}.ki-minus:before{content:\"\\ed42\"}.ki-monitor-mobile .path1:before{content:\"\\ed43\";opacity:.3}.ki-monitor-mobile .path2:before{content:\"\\ed44\";position:absolute;left:0}.ki-moon .path1:before{content:\"\\ed45\"}.ki-moon .path2:before{content:\"\\ed46\";position:absolute;left:0;opacity:.3}.ki-more-2 .path1:before{content:\"\\ed47\";opacity:.3}.ki-more-2 .path2:before{content:\"\\ed48\";position:absolute;left:0}.ki-more-2 .path3:before{content:\"\\ed49\";position:absolute;left:0}.ki-more-2 .path4:before{content:\"\\ed4a\";position:absolute;left:0}.ki-mouse-circle .path1:before{content:\"\\ed4b\"}.ki-mouse-circle .path2:before{content:\"\\ed4c\";position:absolute;left:0;opacity:.3}.ki-mouse-square .path1:before{content:\"\\ed4d\";opacity:.3}.ki-mouse-square .path2:before{content:\"\\ed4e\";position:absolute;left:0}.ki-mouse .path1:before{content:\"\\ed4f\";opacity:.3}.ki-mouse .path2:before{content:\"\\ed50\";position:absolute;left:0}.ki-nexo .path1:before{content:\"\\ed51\";opacity:.3}.ki-nexo .path2:before{content:\"\\ed52\";position:absolute;left:0}.ki-night-day .path1:before{content:\"\\ed53\"}.ki-night-day .path2:before{content:\"\\ed54\";position:absolute;left:0;opacity:.3}.ki-night-day .path3:before{content:\"\\ed55\";position:absolute;left:0}.ki-night-day .path4:before{content:\"\\ed56\";position:absolute;left:0}.ki-night-day .path5:before{content:\"\\ed57\";position:absolute;left:0}.ki-night-day .path6:before{content:\"\\ed58\";position:absolute;left:0}.ki-night-day .path7:before{content:\"\\ed59\";position:absolute;left:0}.ki-night-day .path8:before{content:\"\\ed5a\";position:absolute;left:0}.ki-night-day .path9:before{content:\"\\ed5b\";position:absolute;left:0}.ki-night-day .path10:before{content:\"\\ed5c\";position:absolute;left:0}.ki-note-2 .path1:before{content:\"\\ed5d\";opacity:.3}.ki-note-2 .path2:before{content:\"\\ed5e\";position:absolute;left:0}.ki-note-2 .path3:before{content:\"\\ed5f\";position:absolute;left:0}.ki-note-2 .path4:before{content:\"\\ed60\";position:absolute;left:0}.ki-note .path1:before{content:\"\\ed61\";opacity:.3}.ki-note .path2:before{content:\"\\ed62\";position:absolute;left:0}.ki-notepad-bookmark .path1:before{content:\"\\ed63\"}.ki-notepad-bookmark .path2:before{content:\"\\ed64\";position:absolute;left:0}.ki-notepad-bookmark .path3:before{content:\"\\ed65\";position:absolute;left:0}.ki-notepad-bookmark .path4:before{content:\"\\ed66\";position:absolute;left:0}.ki-notepad-bookmark .path5:before{content:\"\\ed67\";position:absolute;left:0}.ki-notepad-bookmark .path6:before{content:\"\\ed68\";position:absolute;left:0;opacity:.3}.ki-notepad-edit .path1:before{content:\"\\ed69\";opacity:.3}.ki-notepad-edit .path2:before{content:\"\\ed6a\";position:absolute;left:0}.ki-notepad .path1:before{content:\"\\ed6b\"}.ki-notepad .path2:before{content:\"\\ed6c\";position:absolute;left:0}.ki-notepad .path3:before{content:\"\\ed6d\";position:absolute;left:0;opacity:.3}.ki-notepad .path4:before{content:\"\\ed6e\";position:absolute;left:0}.ki-notepad .path5:before{content:\"\\ed6f\";position:absolute;left:0}.ki-notification-2 .path1:before{content:\"\\ed70\"}.ki-notification-2 .path2:before{content:\"\\ed71\";position:absolute;left:0;opacity:.3}.ki-notification-bing .path1:before{content:\"\\ed72\"}.ki-notification-bing .path2:before{content:\"\\ed73\";position:absolute;left:0;opacity:.3}.ki-notification-bing .path3:before{content:\"\\ed74\";position:absolute;left:0}.ki-notification-circle .path1:before{content:\"\\ed75\"}.ki-notification-circle .path2:before{content:\"\\ed76\";position:absolute;left:0;opacity:.3}.ki-notification-favorite .path1:before{content:\"\\ed77\"}.ki-notification-favorite .path2:before{content:\"\\ed78\";position:absolute;left:0;opacity:.3}.ki-notification-favorite .path3:before{content:\"\\ed79\";position:absolute;left:0}.ki-notification-on .path1:before{content:\"\\ed7a\";opacity:.3}.ki-notification-on .path2:before{content:\"\\ed7b\";position:absolute;left:0}.ki-notification-on .path3:before{content:\"\\ed7c\";position:absolute;left:0}.ki-notification-on .path4:before{content:\"\\ed7d\";position:absolute;left:0}.ki-notification-on .path5:before{content:\"\\ed7e\";position:absolute;left:0}.ki-notification-status .path1:before{content:\"\\ed7f\"}.ki-notification-status .path2:before{content:\"\\ed80\";position:absolute;left:0;opacity:.3}.ki-notification-status .path3:before{content:\"\\ed81\";position:absolute;left:0}.ki-notification-status .path4:before{content:\"\\ed82\";position:absolute;left:0}.ki-notification .path1:before{content:\"\\ed83\";opacity:.3}.ki-notification .path2:before{content:\"\\ed84\";position:absolute;left:0}.ki-notification .path3:before{content:\"\\ed85\";position:absolute;left:0}.ki-ocean .path1:before{content:\"\\ed86\"}.ki-ocean .path2:before{content:\"\\ed87\";position:absolute;left:0}.ki-ocean .path3:before{content:\"\\ed88\";position:absolute;left:0}.ki-ocean .path4:before{content:\"\\ed89\";position:absolute;left:0}.ki-ocean .path5:before{content:\"\\ed8a\";position:absolute;left:0}.ki-ocean .path6:before{content:\"\\ed8b\";position:absolute;left:0}.ki-ocean .path7:before{content:\"\\ed8c\";position:absolute;left:0}.ki-ocean .path8:before{content:\"\\ed8d\";position:absolute;left:0}.ki-ocean .path9:before{content:\"\\ed8e\";position:absolute;left:0}.ki-ocean .path10:before{content:\"\\ed8f\";position:absolute;left:0;opacity:.3}.ki-ocean .path11:before{content:\"\\ed90\";position:absolute;left:0;opacity:.3}.ki-ocean .path12:before{content:\"\\ed91\";position:absolute;left:0;opacity:.3}.ki-ocean .path13:before{content:\"\\ed92\";position:absolute;left:0;opacity:.3}.ki-ocean .path14:before{content:\"\\ed93\";position:absolute;left:0;opacity:.3}.ki-ocean .path15:before{content:\"\\ed94\";position:absolute;left:0;opacity:.3}.ki-ocean .path16:before{content:\"\\ed95\";position:absolute;left:0;opacity:.3}.ki-ocean .path17:before{content:\"\\ed96\";position:absolute;left:0;opacity:.3}.ki-ocean .path18:before{content:\"\\ed97\";position:absolute;left:0;opacity:.3}.ki-ocean .path19:before{content:\"\\ed98\";position:absolute;left:0;opacity:.3}.ki-office-bag .path1:before{content:\"\\ed99\";opacity:.3}.ki-office-bag .path2:before{content:\"\\ed9a\";position:absolute;left:0}.ki-office-bag .path3:before{content:\"\\ed9b\";position:absolute;left:0}.ki-office-bag .path4:before{content:\"\\ed9c\";position:absolute;left:0}.ki-package .path1:before{content:\"\\ed9d\";opacity:.3}.ki-package .path2:before{content:\"\\ed9e\";position:absolute;left:0}.ki-package .path3:before{content:\"\\ed9f\";position:absolute;left:0}.ki-pails .path1:before{content:\"\\eda0\";opacity:.3}.ki-pails .path2:before{content:\"\\eda1\";position:absolute;left:0}.ki-pails .path3:before{content:\"\\eda2\";position:absolute;left:0}.ki-pails .path4:before{content:\"\\eda3\";position:absolute;left:0}.ki-pails .path5:before{content:\"\\eda4\";position:absolute;left:0}.ki-pails .path6:before{content:\"\\eda5\";position:absolute;left:0}.ki-pails .path7:before{content:\"\\eda6\";position:absolute;left:0}.ki-pails .path8:before{content:\"\\eda7\";position:absolute;left:0}.ki-pails .path9:before{content:\"\\eda8\";position:absolute;left:0}.ki-paintbucket .path1:before{content:\"\\eda9\";opacity:.3}.ki-paintbucket .path2:before{content:\"\\edaa\";position:absolute;left:0}.ki-paintbucket .path3:before{content:\"\\edab\";position:absolute;left:0}.ki-paper-clip:before{content:\"\\edac\"}.ki-parcel-tracking .path1:before{content:\"\\edad\"}.ki-parcel-tracking .path2:before{content:\"\\edae\";position:absolute;left:0}.ki-parcel-tracking .path3:before{content:\"\\edaf\";position:absolute;left:0;opacity:.3}.ki-parcel .path1:before{content:\"\\edb0\";opacity:.3}.ki-parcel .path2:before{content:\"\\edb1\";position:absolute;left:0;opacity:.3}.ki-parcel .path3:before{content:\"\\edb2\";position:absolute;left:0;opacity:.3}.ki-parcel .path4:before{content:\"\\edb3\";position:absolute;left:0}.ki-parcel .path5:before{content:\"\\edb4\";position:absolute;left:0}.ki-password-check .path1:before{content:\"\\edb5\"}.ki-password-check .path2:before{content:\"\\edb6\";position:absolute;left:0}.ki-password-check .path3:before{content:\"\\edb7\";position:absolute;left:0}.ki-password-check .path4:before{content:\"\\edb8\";position:absolute;left:0;opacity:.3}.ki-password-check .path5:before{content:\"\\edb9\";position:absolute;left:0;opacity:.3}.ki-paypal .path1:before{content:\"\\edba\"}.ki-paypal .path2:before{content:\"\\edbb\";position:absolute;left:0;opacity:.3}.ki-pencil .path1:before{content:\"\\edbc\";opacity:.3}.ki-pencil .path2:before{content:\"\\edbd\";position:absolute;left:0}.ki-people .path1:before{content:\"\\edbe\"}.ki-people .path2:before{content:\"\\edbf\";position:absolute;left:0}.ki-people .path3:before{content:\"\\edc0\";position:absolute;left:0;opacity:.3}.ki-people .path4:before{content:\"\\edc1\";position:absolute;left:0;opacity:.3}.ki-people .path5:before{content:\"\\edc2\";position:absolute;left:0;opacity:.3}.ki-percentage .path1:before{content:\"\\edc3\"}.ki-percentage .path2:before{content:\"\\edc4\";position:absolute;left:0;opacity:.3}.ki-phone .path1:before{content:\"\\edc5\";opacity:.3}.ki-phone .path2:before{content:\"\\edc6\";position:absolute;left:0}.ki-photoshop .path1:before{content:\"\\edc7\";opacity:.3}.ki-photoshop .path2:before{content:\"\\edc8\";position:absolute;left:0}.ki-picture .path1:before{content:\"\\edc9\";opacity:.3}.ki-picture .path2:before{content:\"\\edca\";position:absolute;left:0}.ki-pill:before{content:\"\\edcb\"}.ki-pin .path1:before{content:\"\\edcc\";opacity:.3}.ki-pin .path2:before{content:\"\\edcd\";position:absolute;left:0}.ki-plus-circle .path1:before{content:\"\\edce\";opacity:.3}.ki-plus-circle .path2:before{content:\"\\edcf\";position:absolute;left:0}.ki-plus-square .path1:before{content:\"\\edd0\";opacity:.3}.ki-plus-square .path2:before{content:\"\\edd1\";position:absolute;left:0;opacity:.3}.ki-plus-square .path3:before{content:\"\\edd2\";position:absolute;left:0}.ki-plus:before{content:\"\\edd3\"}.ki-pointers .path1:before{content:\"\\edd4\";opacity:.3}.ki-pointers .path2:before{content:\"\\edd5\";position:absolute;left:0}.ki-pointers .path3:before{content:\"\\edd6\";position:absolute;left:0}.ki-price-tag .path1:before{content:\"\\edd7\";opacity:.3}.ki-price-tag .path2:before{content:\"\\edd8\";position:absolute;left:0}.ki-price-tag .path3:before{content:\"\\edd9\";position:absolute;left:0}.ki-printer .path1:before{content:\"\\edda\";opacity:.3}.ki-printer .path2:before{content:\"\\eddb\";position:absolute;left:0}.ki-printer .path3:before{content:\"\\eddc\";position:absolute;left:0}.ki-printer .path4:before{content:\"\\eddd\";position:absolute;left:0}.ki-printer .path5:before{content:\"\\edde\";position:absolute;left:0}.ki-profile-circle .path1:before{content:\"\\eddf\";opacity:.3}.ki-profile-circle .path2:before{content:\"\\ede0\";position:absolute;left:0}.ki-profile-circle .path3:before{content:\"\\ede1\";position:absolute;left:0}.ki-profile-user .path1:before{content:\"\\ede2\";opacity:.3}.ki-profile-user .path2:before{content:\"\\ede3\";position:absolute;left:0}.ki-profile-user .path3:before{content:\"\\ede4\";position:absolute;left:0;opacity:.3}.ki-profile-user .path4:before{content:\"\\ede5\";position:absolute;left:0}.ki-pulse .path1:before{content:\"\\ede6\";opacity:.3}.ki-pulse .path2:before{content:\"\\ede7\";position:absolute;left:0}.ki-purchase .path1:before{content:\"\\ede8\";opacity:.3}.ki-purchase .path2:before{content:\"\\ede9\";position:absolute;left:0}.ki-python .path1:before{content:\"\\edea\";opacity:.3}.ki-python .path2:before{content:\"\\edeb\";position:absolute;left:0}.ki-question-2 .path1:before{content:\"\\edec\";opacity:.3}.ki-question-2 .path2:before{content:\"\\eded\";position:absolute;left:0}.ki-question-2 .path3:before{content:\"\\edee\";position:absolute;left:0}.ki-question .path1:before{content:\"\\edef\";opacity:.3}.ki-question .path2:before{content:\"\\edf0\";position:absolute;left:0}.ki-question .path3:before{content:\"\\edf1\";position:absolute;left:0}.ki-questionnaire-tablet .path1:before{content:\"\\edf2\";opacity:.3}.ki-questionnaire-tablet .path2:before{content:\"\\edf3\";position:absolute;left:0}.ki-ranking .path1:before{content:\"\\edf4\";opacity:.3}.ki-ranking .path2:before{content:\"\\edf5\";position:absolute;left:0}.ki-ranking .path3:before{content:\"\\edf6\";position:absolute;left:0;opacity:.3}.ki-ranking .path4:before{content:\"\\edf7\";position:absolute;left:0}.ki-react .path1:before{content:\"\\edf8\";opacity:.3}.ki-react .path2:before{content:\"\\edf9\";position:absolute;left:0}.ki-receipt-square .path1:before{content:\"\\edfa\";opacity:.3}.ki-receipt-square .path2:before{content:\"\\edfb\";position:absolute;left:0}.ki-rescue .path1:before{content:\"\\edfc\";opacity:.3}.ki-rescue .path2:before{content:\"\\edfd\";position:absolute;left:0}.ki-right-left .path1:before{content:\"\\edfe\"}.ki-right-left .path2:before{content:\"\\edff\";position:absolute;left:0}.ki-right-left .path3:before{content:\"\\ee00\";position:absolute;left:0;opacity:.3}.ki-right-square .path1:before{content:\"\\ee01\";opacity:.3}.ki-right-square .path2:before{content:\"\\ee02\";position:absolute;left:0}.ki-right:before{content:\"\\ee03\"}.ki-rocket .path1:before{content:\"\\ee04\";opacity:.3}.ki-rocket .path2:before{content:\"\\ee05\";position:absolute;left:0}.ki-route .path1:before{content:\"\\ee06\"}.ki-route .path2:before{content:\"\\ee07\";position:absolute;left:0;opacity:.3}.ki-route .path3:before{content:\"\\ee08\";position:absolute;left:0}.ki-route .path4:before{content:\"\\ee09\";position:absolute;left:0;opacity:.3}.ki-router .path1:before{content:\"\\ee0a\";opacity:.3}.ki-router .path2:before{content:\"\\ee0b\";position:absolute;left:0}.ki-row-horizontal .path1:before{content:\"\\ee0c\"}.ki-row-horizontal .path2:before{content:\"\\ee0d\";position:absolute;left:0;opacity:.3}.ki-row-vertical .path1:before{content:\"\\ee0e\"}.ki-row-vertical .path2:before{content:\"\\ee0f\";position:absolute;left:0;opacity:.3}.ki-safe-home .path1:before{content:\"\\ee10\";opacity:.3}.ki-safe-home .path2:before{content:\"\\ee11\";position:absolute;left:0}.ki-satellite .path1:before{content:\"\\ee12\";opacity:.3}.ki-satellite .path2:before{content:\"\\ee13\";position:absolute;left:0;opacity:.3}.ki-satellite .path3:before{content:\"\\ee14\";position:absolute;left:0}.ki-satellite .path4:before{content:\"\\ee15\";position:absolute;left:0}.ki-satellite .path5:before{content:\"\\ee16\";position:absolute;left:0}.ki-satellite .path6:before{content:\"\\ee17\";position:absolute;left:0}.ki-save-2 .path1:before{content:\"\\ee18\";opacity:.3}.ki-save-2 .path2:before{content:\"\\ee19\";position:absolute;left:0}.ki-save-deposit .path1:before{content:\"\\ee1a\"}.ki-save-deposit .path2:before{content:\"\\ee1b\";position:absolute;left:0}.ki-save-deposit .path3:before{content:\"\\ee1c\";position:absolute;left:0;opacity:.3}.ki-save-deposit .path4:before{content:\"\\ee1d\";position:absolute;left:0}.ki-scan-barcode .path1:before{content:\"\\ee1e\"}.ki-scan-barcode .path2:before{content:\"\\ee1f\";position:absolute;left:0;opacity:.3}.ki-scan-barcode .path3:before{content:\"\\ee20\";position:absolute;left:0;opacity:.3}.ki-scan-barcode .path4:before{content:\"\\ee21\";position:absolute;left:0}.ki-scan-barcode .path5:before{content:\"\\ee22\";position:absolute;left:0;opacity:.3}.ki-scan-barcode .path6:before{content:\"\\ee23\";position:absolute;left:0}.ki-scan-barcode .path7:before{content:\"\\ee24\";position:absolute;left:0}.ki-scan-barcode .path8:before{content:\"\\ee25\";position:absolute;left:0;opacity:.3}.ki-scooter-2:before{content:\"\\ee26\"}.ki-scooter .path1:before{content:\"\\ee27\"}.ki-scooter .path2:before{content:\"\\ee28\";position:absolute;left:0}.ki-scooter .path3:before{content:\"\\ee29\";position:absolute;left:0}.ki-scooter .path4:before{content:\"\\ee2a\";position:absolute;left:0;opacity:.3}.ki-scooter .path5:before{content:\"\\ee2b\";position:absolute;left:0}.ki-scooter .path6:before{content:\"\\ee2c\";position:absolute;left:0}.ki-scooter .path7:before{content:\"\\ee2d\";position:absolute;left:0}.ki-screen .path1:before{content:\"\\ee2e\";opacity:.3}.ki-screen .path2:before{content:\"\\ee2f\";position:absolute;left:0}.ki-screen .path3:before{content:\"\\ee30\";position:absolute;left:0}.ki-screen .path4:before{content:\"\\ee31\";position:absolute;left:0}.ki-scroll .path1:before{content:\"\\ee32\";opacity:.3}.ki-scroll .path2:before{content:\"\\ee33\";position:absolute;left:0}.ki-scroll .path3:before{content:\"\\ee34\";position:absolute;left:0}.ki-search-list .path1:before{content:\"\\ee35\";opacity:.3}.ki-search-list .path2:before{content:\"\\ee36\";position:absolute;left:0}.ki-search-list .path3:before{content:\"\\ee37\";position:absolute;left:0}.ki-security-check .path1:before{content:\"\\ee38\"}.ki-security-check .path2:before{content:\"\\ee39\";position:absolute;left:0;opacity:.3}.ki-security-check .path3:before{content:\"\\ee3a\";position:absolute;left:0;opacity:.3}.ki-security-check .path4:before{content:\"\\ee3b\";position:absolute;left:0}.ki-security-user .path1:before{content:\"\\ee3c\";opacity:.3}.ki-security-user .path2:before{content:\"\\ee3d\";position:absolute;left:0}.ki-send .path1:before{content:\"\\ee3e\"}.ki-send .path2:before{content:\"\\ee3f\";position:absolute;left:0;opacity:.3}.ki-setting-2 .path1:before{content:\"\\ee40\";opacity:.3}.ki-setting-2 .path2:before{content:\"\\ee41\";position:absolute;left:0}.ki-setting-3 .path1:before{content:\"\\ee42\";opacity:.3}.ki-setting-3 .path2:before{content:\"\\ee43\";position:absolute;left:0}.ki-setting-3 .path3:before{content:\"\\ee44\";position:absolute;left:0}.ki-setting-3 .path4:before{content:\"\\ee45\";position:absolute;left:0}.ki-setting-3 .path5:before{content:\"\\ee46\";position:absolute;left:0}.ki-setting-4:before{content:\"\\ee47\"}.ki-setting .path1:before{content:\"\\ee48\";opacity:.3}.ki-setting .path2:before{content:\"\\ee49\";position:absolute;left:0}.ki-share .path1:before{content:\"\\ee4a\";opacity:.3}.ki-share .path2:before{content:\"\\ee4b\";position:absolute;left:0;opacity:.3}.ki-share .path3:before{content:\"\\ee4c\";position:absolute;left:0;opacity:.3}.ki-share .path4:before{content:\"\\ee4d\";position:absolute;left:0}.ki-share .path5:before{content:\"\\ee4e\";position:absolute;left:0}.ki-share .path6:before{content:\"\\ee4f\";position:absolute;left:0}.ki-shield-cross .path1:before{content:\"\\ee50\";opacity:.3}.ki-shield-cross .path2:before{content:\"\\ee51\";position:absolute;left:0}.ki-shield-cross .path3:before{content:\"\\ee52\";position:absolute;left:0}.ki-shield-search .path1:before{content:\"\\ee53\";opacity:.3}.ki-shield-search .path2:before{content:\"\\ee54\";position:absolute;left:0}.ki-shield-search .path3:before{content:\"\\ee55\";position:absolute;left:0}.ki-shield-slash .path1:before{content:\"\\ee56\"}.ki-shield-slash .path2:before{content:\"\\ee57\";position:absolute;left:0;opacity:.3}.ki-shield-slash .path3:before{content:\"\\ee58\";position:absolute;left:0;opacity:.3}.ki-shield-tick .path1:before{content:\"\\ee59\";opacity:.3}.ki-shield-tick .path2:before{content:\"\\ee5a\";position:absolute;left:0}.ki-shield .path1:before{content:\"\\ee5b\";opacity:.3}.ki-shield .path2:before{content:\"\\ee5c\";position:absolute;left:0}.ki-ship .path1:before{content:\"\\ee5d\";opacity:.3}.ki-ship .path2:before{content:\"\\ee5e\";position:absolute;left:0}.ki-ship .path3:before{content:\"\\ee5f\";position:absolute;left:0}.ki-shop .path1:before{content:\"\\ee60\"}.ki-shop .path2:before{content:\"\\ee61\";position:absolute;left:0;opacity:.3}.ki-shop .path3:before{content:\"\\ee62\";position:absolute;left:0;opacity:.3}.ki-shop .path4:before{content:\"\\ee63\";position:absolute;left:0;opacity:.3}.ki-shop .path5:before{content:\"\\ee64\";position:absolute;left:0}.ki-simcard-2 .path1:before{content:\"\\ee65\";opacity:.3}.ki-simcard-2 .path2:before{content:\"\\ee66\";position:absolute;left:0}.ki-simcard .path1:before{content:\"\\ee67\";opacity:.3}.ki-simcard .path2:before{content:\"\\ee68\";position:absolute;left:0}.ki-simcard .path3:before{content:\"\\ee69\";position:absolute;left:0}.ki-simcard .path4:before{content:\"\\ee6a\";position:absolute;left:0}.ki-simcard .path5:before{content:\"\\ee6b\";position:absolute;left:0}.ki-size .path1:before{content:\"\\ee6c\";opacity:.3}.ki-size .path2:before{content:\"\\ee6d\";position:absolute;left:0}.ki-slack .path1:before{content:\"\\ee6e\"}.ki-slack .path2:before{content:\"\\ee6f\";position:absolute;left:0}.ki-slack .path3:before{content:\"\\ee70\";position:absolute;left:0}.ki-slack .path4:before{content:\"\\ee71\";position:absolute;left:0}.ki-slack .path5:before{content:\"\\ee72\";position:absolute;left:0;opacity:.3}.ki-slack .path6:before{content:\"\\ee73\";position:absolute;left:0;opacity:.3}.ki-slack .path7:before{content:\"\\ee74\";position:absolute;left:0;opacity:.3}.ki-slack .path8:before{content:\"\\ee75\";position:absolute;left:0;opacity:.3}.ki-slider-horizontal-2 .path1:before{content:\"\\ee76\"}.ki-slider-horizontal-2 .path2:before{content:\"\\ee77\";position:absolute;left:0}.ki-slider-horizontal-2 .path3:before{content:\"\\ee78\";position:absolute;left:0;opacity:.3}.ki-slider-horizontal .path1:before{content:\"\\ee79\";opacity:.3}.ki-slider-horizontal .path2:before{content:\"\\ee7a\";position:absolute;left:0}.ki-slider-horizontal .path3:before{content:\"\\ee7b\";position:absolute;left:0}.ki-slider-vertical-2 .path1:before{content:\"\\ee7c\";opacity:.3}.ki-slider-vertical-2 .path2:before{content:\"\\ee7d\";position:absolute;left:0}.ki-slider-vertical-2 .path3:before{content:\"\\ee7e\";position:absolute;left:0}.ki-slider-vertical .path1:before{content:\"\\ee7f\"}.ki-slider-vertical .path2:before{content:\"\\ee80\";position:absolute;left:0}.ki-slider-vertical .path3:before{content:\"\\ee81\";position:absolute;left:0;opacity:.3}.ki-slider .path1:before{content:\"\\ee82\";opacity:.3}.ki-slider .path2:before{content:\"\\ee83\";position:absolute;left:0}.ki-slider .path3:before{content:\"\\ee84\";position:absolute;left:0}.ki-slider .path4:before{content:\"\\ee85\";position:absolute;left:0}.ki-sms .path1:before{content:\"\\ee86\";opacity:.3}.ki-sms .path2:before{content:\"\\ee87\";position:absolute;left:0}.ki-snapchat .path1:before{content:\"\\ee88\"}.ki-snapchat .path2:before{content:\"\\ee89\";position:absolute;left:0;opacity:.3}.ki-social-media .path1:before{content:\"\\ee8a\"}.ki-social-media .path2:before{content:\"\\ee8b\";position:absolute;left:0;opacity:.3}.ki-soft-2 .path1:before{content:\"\\ee8c\";opacity:.3}.ki-soft-2 .path2:before{content:\"\\ee8d\";position:absolute;left:0}.ki-soft-3 .path1:before{content:\"\\ee8e\"}.ki-soft-3 .path2:before{content:\"\\ee8f\";position:absolute;left:0;opacity:.3}.ki-soft .path1:before{content:\"\\ee90\";opacity:.3}.ki-soft .path2:before{content:\"\\ee91\";position:absolute;left:0}.ki-soft .path3:before{content:\"\\ee92\";position:absolute;left:0}.ki-soft .path4:before{content:\"\\ee93\";position:absolute;left:0}.ki-soft .path5:before{content:\"\\ee94\";position:absolute;left:0}.ki-soft .path6:before{content:\"\\ee95\";position:absolute;left:0}.ki-some-files .path1:before{content:\"\\ee96\";opacity:.3}.ki-some-files .path2:before{content:\"\\ee97\";position:absolute;left:0}.ki-sort .path1:before{content:\"\\ee98\";opacity:.3}.ki-sort .path2:before{content:\"\\ee99\";position:absolute;left:0}.ki-sort .path3:before{content:\"\\ee9a\";position:absolute;left:0}.ki-sort .path4:before{content:\"\\ee9b\";position:absolute;left:0}.ki-speaker .path1:before{content:\"\\ee9c\";opacity:.3}.ki-speaker .path2:before{content:\"\\ee9d\";position:absolute;left:0}.ki-speaker .path3:before{content:\"\\ee9e\";position:absolute;left:0}.ki-spotify .path1:before{content:\"\\ee9f\"}.ki-spotify .path2:before{content:\"\\eea0\";position:absolute;left:0;opacity:.3}.ki-spring-framework:before{content:\"\\eea1\"}.ki-square-brackets .path1:before{content:\"\\eea2\";opacity:.3}.ki-square-brackets .path2:before{content:\"\\eea3\";position:absolute;left:0}.ki-square-brackets .path3:before{content:\"\\eea4\";position:absolute;left:0}.ki-square-brackets .path4:before{content:\"\\eea5\";position:absolute;left:0;opacity:.3}.ki-star:before{content:\"\\eea6\"}.ki-status .path1:before{content:\"\\eea7\";opacity:.3}.ki-status .path2:before{content:\"\\eea8\";position:absolute;left:0;opacity:.3}.ki-status .path3:before{content:\"\\eea9\";position:absolute;left:0}.ki-subtitle .path1:before{content:\"\\eeaa\";opacity:.3}.ki-subtitle .path2:before{content:\"\\eeab\";position:absolute;left:0}.ki-subtitle .path3:before{content:\"\\eeac\";position:absolute;left:0}.ki-subtitle .path4:before{content:\"\\eead\";position:absolute;left:0}.ki-subtitle .path5:before{content:\"\\eeae\";position:absolute;left:0}.ki-sun .path1:before{content:\"\\eeaf\"}.ki-sun .path2:before{content:\"\\eeb0\";position:absolute;left:0}.ki-sun .path3:before{content:\"\\eeb1\";position:absolute;left:0}.ki-sun .path4:before{content:\"\\eeb2\";position:absolute;left:0}.ki-sun .path5:before{content:\"\\eeb3\";position:absolute;left:0}.ki-sun .path6:before{content:\"\\eeb4\";position:absolute;left:0}.ki-sun .path7:before{content:\"\\eeb5\";position:absolute;left:0}.ki-sun .path8:before{content:\"\\eeb6\";position:absolute;left:0}.ki-sun .path9:before{content:\"\\eeb7\";position:absolute;left:0;opacity:.3}.ki-support-24 .path1:before{content:\"\\eeb8\";opacity:.3}.ki-support-24 .path2:before{content:\"\\eeb9\";position:absolute;left:0}.ki-support-24 .path3:before{content:\"\\eeba\";position:absolute;left:0}.ki-switch .path1:before{content:\"\\eebb\"}.ki-switch .path2:before{content:\"\\eebc\";position:absolute;left:0;opacity:.3}.ki-syringe .path1:before{content:\"\\eebd\";opacity:.3}.ki-syringe .path2:before{content:\"\\eebe\";position:absolute;left:0}.ki-syringe .path3:before{content:\"\\eebf\";position:absolute;left:0}.ki-tablet-book .path1:before{content:\"\\eec0\";opacity:.3}.ki-tablet-book .path2:before{content:\"\\eec1\";position:absolute;left:0}.ki-tablet-delete .path1:before{content:\"\\eec2\"}.ki-tablet-delete .path2:before{content:\"\\eec3\";position:absolute;left:0;opacity:.3}.ki-tablet-delete .path3:before{content:\"\\eec4\";position:absolute;left:0}.ki-tablet-down .path1:before{content:\"\\eec5\";opacity:.3}.ki-tablet-down .path2:before{content:\"\\eec6\";position:absolute;left:0}.ki-tablet-down .path3:before{content:\"\\eec7\";position:absolute;left:0}.ki-tablet-ok .path1:before{content:\"\\eec8\"}.ki-tablet-ok .path2:before{content:\"\\eec9\";position:absolute;left:0;opacity:.3}.ki-tablet-ok .path3:before{content:\"\\eeca\";position:absolute;left:0}.ki-tablet-text-down .path1:before{content:\"\\eecb\"}.ki-tablet-text-down .path2:before{content:\"\\eecc\";position:absolute;left:0}.ki-tablet-text-down .path3:before{content:\"\\eecd\";position:absolute;left:0}.ki-tablet-text-down .path4:before{content:\"\\eece\";position:absolute;left:0;opacity:.3}.ki-tablet-text-up .path1:before{content:\"\\eecf\"}.ki-tablet-text-up .path2:before{content:\"\\eed0\";position:absolute;left:0;opacity:.3}.ki-tablet-up .path1:before{content:\"\\eed1\";opacity:.3}.ki-tablet-up .path2:before{content:\"\\eed2\";position:absolute;left:0}.ki-tablet-up .path3:before{content:\"\\eed3\";position:absolute;left:0}.ki-tablet .path1:before{content:\"\\eed4\";opacity:.3}.ki-tablet .path2:before{content:\"\\eed5\";position:absolute;left:0}.ki-tablet .path3:before{content:\"\\eed6\";position:absolute;left:0}.ki-tag-cross .path1:before{content:\"\\eed7\";opacity:.3}.ki-tag-cross .path2:before{content:\"\\eed8\";position:absolute;left:0}.ki-tag-cross .path3:before{content:\"\\eed9\";position:absolute;left:0}.ki-tag .path1:before{content:\"\\eeda\";opacity:.3}.ki-tag .path2:before{content:\"\\eedb\";position:absolute;left:0}.ki-tag .path3:before{content:\"\\eedc\";position:absolute;left:0}.ki-teacher .path1:before{content:\"\\eedd\";opacity:.3}.ki-teacher .path2:before{content:\"\\eede\";position:absolute;left:0}.ki-tech-wifi .path1:before{content:\"\\eedf\";opacity:.3}.ki-tech-wifi .path2:before{content:\"\\eee0\";position:absolute;left:0}.ki-technology-2 .path1:before{content:\"\\eee1\";opacity:.3}.ki-technology-2 .path2:before{content:\"\\eee2\";position:absolute;left:0}.ki-technology-3 .path1:before{content:\"\\eee3\"}.ki-technology-3 .path2:before{content:\"\\eee4\";position:absolute;left:0;opacity:.3}.ki-technology-3 .path3:before{content:\"\\eee5\";position:absolute;left:0;opacity:.3}.ki-technology-3 .path4:before{content:\"\\eee6\";position:absolute;left:0}.ki-technology-4 .path1:before{content:\"\\eee7\"}.ki-technology-4 .path2:before{content:\"\\eee8\";position:absolute;left:0;opacity:.3}.ki-technology-4 .path3:before{content:\"\\eee9\";position:absolute;left:0}.ki-technology-4 .path4:before{content:\"\\eeea\";position:absolute;left:0}.ki-technology-4 .path5:before{content:\"\\eeeb\";position:absolute;left:0}.ki-technology-4 .path6:before{content:\"\\eeec\";position:absolute;left:0}.ki-technology-4 .path7:before{content:\"\\eeed\";position:absolute;left:0}.ki-technology .path1:before{content:\"\\eeee\"}.ki-technology .path2:before{content:\"\\eeef\";position:absolute;left:0;opacity:.3}.ki-technology .path3:before{content:\"\\eef0\";position:absolute;left:0;opacity:.3}.ki-technology .path4:before{content:\"\\eef1\";position:absolute;left:0;opacity:.3}.ki-technology .path5:before{content:\"\\eef2\";position:absolute;left:0;opacity:.3}.ki-technology .path6:before{content:\"\\eef3\";position:absolute;left:0;opacity:.3}.ki-technology .path7:before{content:\"\\eef4\";position:absolute;left:0;opacity:.3}.ki-technology .path8:before{content:\"\\eef5\";position:absolute;left:0;opacity:.3}.ki-technology .path9:before{content:\"\\eef6\";position:absolute;left:0;opacity:.3}.ki-telephone-geolocation .path1:before{content:\"\\eef7\"}.ki-telephone-geolocation .path2:before{content:\"\\eef8\";position:absolute;left:0;opacity:.3}.ki-telephone-geolocation .path3:before{content:\"\\eef9\";position:absolute;left:0}.ki-test-tubes .path1:before{content:\"\\eefa\";opacity:.3}.ki-test-tubes .path2:before{content:\"\\eefb\";position:absolute;left:0}.ki-text-align-center .path1:before{content:\"\\eefc\"}.ki-text-align-center .path2:before{content:\"\\eefd\";position:absolute;left:0}.ki-text-align-center .path3:before{content:\"\\eefe\";position:absolute;left:0;opacity:.3}.ki-text-align-center .path4:before{content:\"\\eeff\";position:absolute;left:0;opacity:.3}.ki-text-align-justify-center .path1:before{content:\"\\ef00\"}.ki-text-align-justify-center .path2:before{content:\"\\ef01\";position:absolute;left:0}.ki-text-align-justify-center .path3:before{content:\"\\ef02\";position:absolute;left:0;opacity:.3}.ki-text-align-justify-center .path4:before{content:\"\\ef03\";position:absolute;left:0;opacity:.3}.ki-text-align-left .path1:before{content:\"\\ef04\"}.ki-text-align-left .path2:before{content:\"\\ef05\";position:absolute;left:0}.ki-text-align-left .path3:before{content:\"\\ef06\";position:absolute;left:0;opacity:.3}.ki-text-align-left .path4:before{content:\"\\ef07\";position:absolute;left:0;opacity:.3}.ki-text-align-right .path1:before{content:\"\\ef08\"}.ki-text-align-right .path2:before{content:\"\\ef09\";position:absolute;left:0}.ki-text-align-right .path3:before{content:\"\\ef0a\";position:absolute;left:0;opacity:.3}.ki-text-align-right .path4:before{content:\"\\ef0b\";position:absolute;left:0;opacity:.3}.ki-text-bold .path1:before{content:\"\\ef0c\";opacity:.3}.ki-text-bold .path2:before{content:\"\\ef0d\";position:absolute;left:0}.ki-text-bold .path3:before{content:\"\\ef0e\";position:absolute;left:0}.ki-text-circle .path1:before{content:\"\\ef0f\"}.ki-text-circle .path2:before{content:\"\\ef10\";position:absolute;left:0}.ki-text-circle .path3:before{content:\"\\ef11\";position:absolute;left:0;opacity:.3}.ki-text-circle .path4:before{content:\"\\ef12\";position:absolute;left:0;opacity:.3}.ki-text-circle .path5:before{content:\"\\ef13\";position:absolute;left:0}.ki-text-circle .path6:before{content:\"\\ef14\";position:absolute;left:0}.ki-text-italic .path1:before{content:\"\\ef15\";opacity:.3}.ki-text-italic .path2:before{content:\"\\ef16\";position:absolute;left:0}.ki-text-italic .path3:before{content:\"\\ef17\";position:absolute;left:0}.ki-text-italic .path4:before{content:\"\\ef18\";position:absolute;left:0}.ki-text-number .path1:before{content:\"\\ef19\"}.ki-text-number .path2:before{content:\"\\ef1a\";position:absolute;left:0}.ki-text-number .path3:before{content:\"\\ef1b\";position:absolute;left:0;opacity:.3}.ki-text-number .path4:before{content:\"\\ef1c\";position:absolute;left:0;opacity:.3}.ki-text-number .path5:before{content:\"\\ef1d\";position:absolute;left:0}.ki-text-number .path6:before{content:\"\\ef1e\";position:absolute;left:0}.ki-text-strikethrough .path1:before{content:\"\\ef1f\";opacity:.3}.ki-text-strikethrough .path2:before{content:\"\\ef20\";position:absolute;left:0;opacity:.3}.ki-text-strikethrough .path3:before{content:\"\\ef21\";position:absolute;left:0}.ki-text-underline .path1:before{content:\"\\ef22\";opacity:.3}.ki-text-underline .path2:before{content:\"\\ef23\";position:absolute;left:0}.ki-text-underline .path3:before{content:\"\\ef24\";position:absolute;left:0}.ki-text:before{content:\"\\ef25\"}.ki-thermometer .path1:before{content:\"\\ef26\";opacity:.3}.ki-thermometer .path2:before{content:\"\\ef27\";position:absolute;left:0}.ki-theta .path1:before{content:\"\\ef28\"}.ki-theta .path2:before{content:\"\\ef29\";position:absolute;left:0;opacity:.3}.ki-tiktok .path1:before{content:\"\\ef2a\";opacity:.3}.ki-tiktok .path2:before{content:\"\\ef2b\";position:absolute;left:0}.ki-time .path1:before{content:\"\\ef2c\";opacity:.3}.ki-time .path2:before{content:\"\\ef2d\";position:absolute;left:0}.ki-timer .path1:before{content:\"\\ef2e\";opacity:.3}.ki-timer .path2:before{content:\"\\ef2f\";position:absolute;left:0}.ki-timer .path3:before{content:\"\\ef30\";position:absolute;left:0}.ki-to-left:before{content:\"\\ef31\"}.ki-to-right:before{content:\"\\ef32\"}.ki-toggle-off-circle .path1:before{content:\"\\ef33\";opacity:.3}.ki-toggle-off-circle .path2:before{content:\"\\ef34\";position:absolute;left:0}.ki-toggle-off .path1:before{content:\"\\ef35\";opacity:.3}.ki-toggle-off .path2:before{content:\"\\ef36\";position:absolute;left:0}.ki-toggle-on-circle .path1:before{content:\"\\ef37\";opacity:.3}.ki-toggle-on-circle .path2:before{content:\"\\ef38\";position:absolute;left:0}.ki-toggle-on .path1:before{content:\"\\ef39\";opacity:.3}.ki-toggle-on .path2:before{content:\"\\ef3a\";position:absolute;left:0}.ki-trailer .path1:before{content:\"\\ef3b\"}.ki-trailer .path2:before{content:\"\\ef3c\";position:absolute;left:0}.ki-trailer .path3:before{content:\"\\ef3d\";position:absolute;left:0}.ki-trailer .path4:before{content:\"\\ef3e\";position:absolute;left:0}.ki-trailer .path5:before{content:\"\\ef3f\";position:absolute;left:0;opacity:.3}.ki-trash-square .path1:before{content:\"\\ef40\";opacity:.3}.ki-trash-square .path2:before{content:\"\\ef41\";position:absolute;left:0}.ki-trash-square .path3:before{content:\"\\ef42\";position:absolute;left:0}.ki-trash-square .path4:before{content:\"\\ef43\";position:absolute;left:0}.ki-trash .path1:before{content:\"\\ef44\";opacity:.3}.ki-trash .path2:before{content:\"\\ef45\";position:absolute;left:0}.ki-trash .path3:before{content:\"\\ef46\";position:absolute;left:0}.ki-trash .path4:before{content:\"\\ef47\";position:absolute;left:0}.ki-trash .path5:before{content:\"\\ef48\";position:absolute;left:0}.ki-tree .path1:before{content:\"\\ef49\";opacity:.3}.ki-tree .path2:before{content:\"\\ef4a\";position:absolute;left:0}.ki-tree .path3:before{content:\"\\ef4b\";position:absolute;left:0}.ki-trello .path1:before{content:\"\\ef4c\";opacity:.3}.ki-trello .path2:before{content:\"\\ef4d\";position:absolute;left:0}.ki-trello .path3:before{content:\"\\ef4e\";position:absolute;left:0}.ki-triangle .path1:before{content:\"\\ef4f\"}.ki-triangle .path2:before{content:\"\\ef50\";position:absolute;left:0;opacity:.3}.ki-triangle .path3:before{content:\"\\ef51\";position:absolute;left:0;opacity:.3}.ki-truck .path1:before{content:\"\\ef52\"}.ki-truck .path2:before{content:\"\\ef53\";position:absolute;left:0}.ki-truck .path3:before{content:\"\\ef54\";position:absolute;left:0;opacity:.3}.ki-truck .path4:before{content:\"\\ef55\";position:absolute;left:0}.ki-truck .path5:before{content:\"\\ef56\";position:absolute;left:0;opacity:.3}.ki-ts .path1:before{content:\"\\ef57\";opacity:.3}.ki-ts .path2:before{content:\"\\ef58\";position:absolute;left:0}.ki-ts .path3:before{content:\"\\ef59\";position:absolute;left:0}.ki-twitch .path1:before{content:\"\\ef5a\";opacity:.3}.ki-twitch .path2:before{content:\"\\ef5b\";position:absolute;left:0}.ki-twitch .path3:before{content:\"\\ef5c\";position:absolute;left:0}.ki-twitter .path1:before{content:\"\\ef5d\";opacity:.3}.ki-twitter .path2:before{content:\"\\ef5e\";position:absolute;left:0}.ki-two-credit-cart .path1:before{content:\"\\ef5f\"}.ki-two-credit-cart .path2:before{content:\"\\ef60\";position:absolute;left:0;opacity:.3}.ki-two-credit-cart .path3:before{content:\"\\ef61\";position:absolute;left:0}.ki-two-credit-cart .path4:before{content:\"\\ef62\";position:absolute;left:0}.ki-two-credit-cart .path5:before{content:\"\\ef63\";position:absolute;left:0}.ki-underlining .path1:before{content:\"\\ef64\";opacity:.3}.ki-underlining .path2:before{content:\"\\ef65\";position:absolute;left:0}.ki-underlining .path3:before{content:\"\\ef66\";position:absolute;left:0;opacity:.3}.ki-up-down .path1:before{content:\"\\ef67\";opacity:.3}.ki-up-down .path2:before{content:\"\\ef68\";position:absolute;left:0}.ki-up-down .path3:before{content:\"\\ef69\";position:absolute;left:0}.ki-up-square .path1:before{content:\"\\ef6a\";opacity:.3}.ki-up-square .path2:before{content:\"\\ef6b\";position:absolute;left:0}.ki-up:before{content:\"\\ef6c\"}.ki-update-file .path1:before{content:\"\\ef6d\";opacity:.3}.ki-update-file .path2:before{content:\"\\ef6e\";position:absolute;left:0}.ki-update-file .path3:before{content:\"\\ef6f\";position:absolute;left:0}.ki-update-file .path4:before{content:\"\\ef70\";position:absolute;left:0}.ki-update-folder .path1:before{content:\"\\ef71\";opacity:.3}.ki-update-folder .path2:before{content:\"\\ef72\";position:absolute;left:0}.ki-user-edit .path1:before{content:\"\\ef73\"}.ki-user-edit .path2:before{content:\"\\ef74\";position:absolute;left:0;opacity:.3}.ki-user-edit .path3:before{content:\"\\ef75\";position:absolute;left:0}.ki-user-square .path1:before{content:\"\\ef76\";opacity:.3}.ki-user-square .path2:before{content:\"\\ef77\";position:absolute;left:0}.ki-user-square .path3:before{content:\"\\ef78\";position:absolute;left:0}.ki-user-tick .path1:before{content:\"\\ef79\"}.ki-user-tick .path2:before{content:\"\\ef7a\";position:absolute;left:0}.ki-user-tick .path3:before{content:\"\\ef7b\";position:absolute;left:0;opacity:.3}.ki-user .path1:before{content:\"\\ef7c\";opacity:.3}.ki-user .path2:before{content:\"\\ef7d\";position:absolute;left:0}.ki-verify .path1:before{content:\"\\ef7e\";opacity:.3}.ki-verify .path2:before{content:\"\\ef7f\";position:absolute;left:0}.ki-vibe .path1:before{content:\"\\ef80\"}.ki-vibe .path2:before{content:\"\\ef81\";position:absolute;left:0;opacity:.3}.ki-virus .path1:before{content:\"\\ef82\";opacity:.3}.ki-virus .path2:before{content:\"\\ef83\";position:absolute;left:0}.ki-virus .path3:before{content:\"\\ef84\";position:absolute;left:0}.ki-vue .path1:before{content:\"\\ef85\";opacity:.3}.ki-vue .path2:before{content:\"\\ef86\";position:absolute;left:0}.ki-vuesax .path1:before{content:\"\\ef87\"}.ki-vuesax .path2:before{content:\"\\ef88\";position:absolute;left:0;opacity:.4}.ki-vuesax .path3:before{content:\"\\ef89\";position:absolute;left:0}.ki-wallet .path1:before{content:\"\\ef8a\";opacity:.3}.ki-wallet .path2:before{content:\"\\ef8b\";position:absolute;left:0}.ki-wallet .path3:before{content:\"\\ef8c\";position:absolute;left:0}.ki-wallet .path4:before{content:\"\\ef8d\";position:absolute;left:0}.ki-wanchain .path1:before{content:\"\\ef8e\"}.ki-wanchain .path2:before{content:\"\\ef8f\";position:absolute;left:0;opacity:.3}.ki-watch .path1:before{content:\"\\ef90\";opacity:.3}.ki-watch .path2:before{content:\"\\ef91\";position:absolute;left:0}.ki-whatsapp .path1:before{content:\"\\ef92\";opacity:.4}.ki-whatsapp .path2:before{content:\"\\ef93\";position:absolute;left:0}.ki-wifi-home .path1:before{content:\"\\ef94\";opacity:.3}.ki-wifi-home .path2:before{content:\"\\ef95\";position:absolute;left:0}.ki-wifi-home .path3:before{content:\"\\ef96\";position:absolute;left:0}.ki-wifi-home .path4:before{content:\"\\ef97\";position:absolute;left:0}.ki-wifi-square .path1:before{content:\"\\ef98\";opacity:.3}.ki-wifi-square .path2:before{content:\"\\ef99\";position:absolute;left:0}.ki-wifi-square .path3:before{content:\"\\ef9a\";position:absolute;left:0}.ki-wifi-square .path4:before{content:\"\\ef9b\";position:absolute;left:0}.ki-wifi .path1:before{content:\"\\ef9c\"}.ki-wifi .path2:before{content:\"\\ef9d\";position:absolute;left:0;opacity:.3}.ki-wifi .path3:before{content:\"\\ef9e\";position:absolute;left:0;opacity:.3}.ki-wifi .path4:before{content:\"\\ef9f\";position:absolute;left:0;opacity:.3}.ki-wrench .path1:before{content:\"\\efa0\";opacity:.3}.ki-wrench .path2:before{content:\"\\efa1\";position:absolute;left:0}.ki-xaomi .path1:before{content:\"\\efa2\"}.ki-xaomi .path2:before{content:\"\\efa3\";position:absolute;left:0;opacity:.3}.ki-xd .path1:before{content:\"\\efa4\";opacity:.3}.ki-xd .path2:before{content:\"\\efa5\";position:absolute;left:0}.ki-xd .path3:before{content:\"\\efa6\";position:absolute;left:0}.ki-xmr .path1:before{content:\"\\efa7\"}.ki-xmr .path2:before{content:\"\\efa8\";position:absolute;left:0;opacity:.3}.ki-yii .path1:before{content:\"\\efa9\";opacity:.3}.ki-yii .path2:before{content:\"\\efaa\";position:absolute;left:0;opacity:.3}.ki-yii .path3:before{content:\"\\efab\";position:absolute;left:0}.ki-youtube .path1:before{content:\"\\efac\"}.ki-youtube .path2:before{content:\"\\efad\";position:absolute;left:0;opacity:.3}.ki-duotone i{font-style:normal}@font-face{font-family:keenicons-outline;src:url(fonts/keenicons/keenicons-outline.eot?fzo4bm);src:url(fonts/keenicons/keenicons-outline.eot?fzo4bm#iefix) format(\"embedded-opentype\"),url(fonts/keenicons/keenicons-outline.ttf?fzo4bm) format(\"truetype\"),url(fonts/keenicons/keenicons-outline.woff?fzo4bm) format(\"woff\"),url(fonts/keenicons/keenicons-outline.svg?fzo4bm#keenicons-outline) format(\"svg\");font-weight:400;font-style:normal;font-display:block}.ki-outline{font-family:keenicons-outline!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ki-abstract-1.ki-outline:before{content:\"\\e900\"}.ki-abstract-2.ki-outline:before{content:\"\\e901\"}.ki-abstract-3.ki-outline:before{content:\"\\e902\"}.ki-abstract-4.ki-outline:before{content:\"\\e903\"}.ki-abstract-5.ki-outline:before{content:\"\\e904\"}.ki-abstract-6.ki-outline:before{content:\"\\e905\"}.ki-abstract-7.ki-outline:before{content:\"\\e906\"}.ki-abstract-8.ki-outline:before{content:\"\\e907\"}.ki-abstract-9.ki-outline:before{content:\"\\e908\"}.ki-abstract-10.ki-outline:before{content:\"\\e909\"}.ki-abstract-11.ki-outline:before{content:\"\\e90a\"}.ki-abstract-12.ki-outline:before{content:\"\\e90b\"}.ki-abstract-13.ki-outline:before{content:\"\\e90c\"}.ki-abstract-14.ki-outline:before{content:\"\\e90d\"}.ki-abstract-15.ki-outline:before{content:\"\\e90e\"}.ki-abstract-16.ki-outline:before{content:\"\\e90f\"}.ki-abstract-17.ki-outline:before{content:\"\\e910\"}.ki-abstract-18.ki-outline:before{content:\"\\e911\"}.ki-abstract-19.ki-outline:before{content:\"\\e912\"}.ki-abstract-20.ki-outline:before{content:\"\\e913\"}.ki-abstract-21.ki-outline:before{content:\"\\e914\"}.ki-abstract-22.ki-outline:before{content:\"\\e915\"}.ki-abstract-23.ki-outline:before{content:\"\\e916\"}.ki-abstract-24.ki-outline:before{content:\"\\e917\"}.ki-abstract-25.ki-outline:before{content:\"\\e918\"}.ki-abstract-26.ki-outline:before{content:\"\\e919\"}.ki-abstract-27.ki-outline:before{content:\"\\e91a\"}.ki-abstract-28.ki-outline:before{content:\"\\e91b\"}.ki-abstract-29.ki-outline:before{content:\"\\e91c\"}.ki-abstract-30.ki-outline:before{content:\"\\e91d\"}.ki-abstract-31.ki-outline:before{content:\"\\e91e\"}.ki-abstract-32.ki-outline:before{content:\"\\e91f\"}.ki-abstract-33.ki-outline:before{content:\"\\e920\"}.ki-abstract-34.ki-outline:before{content:\"\\e921\"}.ki-abstract-35.ki-outline:before{content:\"\\e922\"}.ki-abstract-36.ki-outline:before{content:\"\\e923\"}.ki-abstract-37.ki-outline:before{content:\"\\e924\"}.ki-abstract-38.ki-outline:before{content:\"\\e925\"}.ki-abstract-39.ki-outline:before{content:\"\\e926\"}.ki-abstract-40.ki-outline:before{content:\"\\e927\"}.ki-abstract-41.ki-outline:before{content:\"\\e928\"}.ki-abstract-42.ki-outline:before{content:\"\\e929\"}.ki-abstract-43.ki-outline:before{content:\"\\e92a\"}.ki-abstract-44.ki-outline:before{content:\"\\e92b\"}.ki-abstract-45.ki-outline:before{content:\"\\e92c\"}.ki-abstract-46.ki-outline:before{content:\"\\e92d\"}.ki-abstract-47.ki-outline:before{content:\"\\e92e\"}.ki-abstract-48.ki-outline:before{content:\"\\e92f\"}.ki-abstract-49.ki-outline:before{content:\"\\e930\"}.ki-abstract.ki-outline:before{content:\"\\e931\"}.ki-add-files.ki-outline:before{content:\"\\e932\"}.ki-add-folder.ki-outline:before{content:\"\\e933\"}.ki-add-item.ki-outline:before{content:\"\\e934\"}.ki-add-notepad.ki-outline:before{content:\"\\e935\"}.ki-address-book.ki-outline:before{content:\"\\e936\"}.ki-airplane-square.ki-outline:before{content:\"\\e937\"}.ki-airplane.ki-outline:before{content:\"\\e938\"}.ki-airpod.ki-outline:before{content:\"\\e939\"}.ki-android.ki-outline:before{content:\"\\e93a\"}.ki-angular.ki-outline:before{content:\"\\e93b\"}.ki-apple.ki-outline:before{content:\"\\e93c\"}.ki-archive-tick.ki-outline:before{content:\"\\e93d\"}.ki-archive.ki-outline:before{content:\"\\e93e\"}.ki-arrow-circle-left.ki-outline:before{content:\"\\e93f\"}.ki-arrow-circle-right.ki-outline:before{content:\"\\e940\"}.ki-arrow-diagonal.ki-outline:before{content:\"\\e941\"}.ki-arrow-down-left.ki-outline:before{content:\"\\e942\"}.ki-arrow-down-refraction.ki-outline:before{content:\"\\e943\"}.ki-arrow-down-right.ki-outline:before{content:\"\\e944\"}.ki-arrow-down.ki-outline:before{content:\"\\e945\"}.ki-arrow-left.ki-outline:before{content:\"\\e946\"}.ki-arrow-mix.ki-outline:before{content:\"\\e947\"}.ki-arrow-right-left.ki-outline:before{content:\"\\e948\"}.ki-arrow-right.ki-outline:before{content:\"\\e949\"}.ki-arrow-two-diagonals.ki-outline:before{content:\"\\e94a\"}.ki-arrow-up-down.ki-outline:before{content:\"\\e94b\"}.ki-arrow-up-left.ki-outline:before{content:\"\\e94c\"}.ki-arrow-up-refraction.ki-outline:before{content:\"\\e94d\"}.ki-arrow-up-right.ki-outline:before{content:\"\\e94e\"}.ki-arrow-up.ki-outline:before{content:\"\\e94f\"}.ki-arrow-zigzag.ki-outline:before{content:\"\\e950\"}.ki-arrows-circle.ki-outline:before{content:\"\\e951\"}.ki-arrows-loop.ki-outline:before{content:\"\\e952\"}.ki-artificial-intelligence.ki-outline:before{content:\"\\e953\"}.ki-auto-brightness.ki-outline:before{content:\"\\e954\"}.ki-avalanche.ki-outline:before{content:\"\\e955\"}.ki-award.ki-outline:before{content:\"\\e956\"}.ki-badge.ki-outline:before{content:\"\\e957\"}.ki-bandage.ki-outline:before{content:\"\\e958\"}.ki-bank.ki-outline:before{content:\"\\e959\"}.ki-barcode.ki-outline:before{content:\"\\e95a\"}.ki-basket-ok.ki-outline:before{content:\"\\e95b\"}.ki-basket.ki-outline:before{content:\"\\e95c\"}.ki-behance.ki-outline:before{content:\"\\e95d\"}.ki-bill.ki-outline:before{content:\"\\e95e\"}.ki-binance-usd.ki-outline:before{content:\"\\e95f\"}.ki-binance.ki-outline:before{content:\"\\e960\"}.ki-bitcoin.ki-outline:before{content:\"\\e961\"}.ki-black-down.ki-outline:before{content:\"\\e962\"}.ki-black-left-line.ki-outline:before{content:\"\\e963\"}.ki-black-left.ki-outline:before{content:\"\\e964\"}.ki-black-right-line.ki-outline:before{content:\"\\e965\"}.ki-black-right.ki-outline:before{content:\"\\e966\"}.ki-black-up.ki-outline:before{content:\"\\e967\"}.ki-bluetooth.ki-outline:before{content:\"\\e968\"}.ki-book-open.ki-outline:before{content:\"\\e969\"}.ki-book-square.ki-outline:before{content:\"\\e96a\"}.ki-book.ki-outline:before{content:\"\\e96b\"}.ki-bookmark-2.ki-outline:before{content:\"\\e96c\"}.ki-bookmark.ki-outline:before{content:\"\\e96d\"}.ki-bootstrap.ki-outline:before{content:\"\\e96e\"}.ki-briefcase.ki-outline:before{content:\"\\e96f\"}.ki-brifecase-cros.ki-outline:before{content:\"\\e970\"}.ki-brifecase-tick.ki-outline:before{content:\"\\e971\"}.ki-brifecase-timer.ki-outline:before{content:\"\\e972\"}.ki-brush.ki-outline:before{content:\"\\e973\"}.ki-bucket-square.ki-outline:before{content:\"\\e974\"}.ki-bucket.ki-outline:before{content:\"\\e975\"}.ki-burger-menu-1.ki-outline:before{content:\"\\e976\"}.ki-burger-menu-2.ki-outline:before{content:\"\\e977\"}.ki-burger-menu-3.ki-outline:before{content:\"\\e978\"}.ki-burger-menu-4.ki-outline:before{content:\"\\e979\"}.ki-burger-menu-5.ki-outline:before{content:\"\\e97a\"}.ki-burger-menu-6.ki-outline:before{content:\"\\e97b\"}.ki-burger-menu.ki-outline:before{content:\"\\e97c\"}.ki-bus.ki-outline:before{content:\"\\e97d\"}.ki-calculator.ki-outline:before{content:\"\\e97e\"}.ki-calendar-2.ki-outline:before{content:\"\\e97f\"}.ki-calendar-8.ki-outline:before{content:\"\\e980\"}.ki-calendar-add.ki-outline:before{content:\"\\e981\"}.ki-calendar-edit.ki-outline:before{content:\"\\e982\"}.ki-calendar-remove.ki-outline:before{content:\"\\e983\"}.ki-calendar-search.ki-outline:before{content:\"\\e984\"}.ki-calendar-tick.ki-outline:before{content:\"\\e985\"}.ki-calendar.ki-outline:before{content:\"\\e986\"}.ki-call.ki-outline:before{content:\"\\e987\"}.ki-capsule.ki-outline:before{content:\"\\e988\"}.ki-car-2.ki-outline:before{content:\"\\e989\"}.ki-car-3.ki-outline:before{content:\"\\e98a\"}.ki-car.ki-outline:before{content:\"\\e98b\"}.ki-category.ki-outline:before{content:\"\\e98c\"}.ki-cd.ki-outline:before{content:\"\\e98d\"}.ki-celsius.ki-outline:before{content:\"\\e98e\"}.ki-chart-line-down-2.ki-outline:before{content:\"\\e98f\"}.ki-chart-line-down.ki-outline:before{content:\"\\e990\"}.ki-chart-line-star.ki-outline:before{content:\"\\e991\"}.ki-chart-line-up-2.ki-outline:before{content:\"\\e992\"}.ki-chart-line-up.ki-outline:before{content:\"\\e993\"}.ki-chart-line.ki-outline:before{content:\"\\e994\"}.ki-chart-pie-3.ki-outline:before{content:\"\\e995\"}.ki-chart-pie-4.ki-outline:before{content:\"\\e996\"}.ki-chart-pie-simple.ki-outline:before{content:\"\\e997\"}.ki-chart-pie-too.ki-outline:before{content:\"\\e998\"}.ki-chart-simple-2.ki-outline:before{content:\"\\e999\"}.ki-chart-simple-3.ki-outline:before{content:\"\\e99a\"}.ki-chart-simple.ki-outline:before{content:\"\\e99b\"}.ki-chart.ki-outline:before{content:\"\\e99c\"}.ki-check-circle.ki-outline:before{content:\"\\e99d\"}.ki-check-square.ki-outline:before{content:\"\\e99e\"}.ki-check.ki-outline:before{content:\"\\e99f\"}.ki-cheque.ki-outline:before{content:\"\\e9a0\"}.ki-chrome.ki-outline:before{content:\"\\e9a1\"}.ki-classmates.ki-outline:before{content:\"\\e9a2\"}.ki-click.ki-outline:before{content:\"\\e9a3\"}.ki-clipboard.ki-outline:before{content:\"\\e9a4\"}.ki-cloud-add.ki-outline:before{content:\"\\e9a5\"}.ki-cloud-change.ki-outline:before{content:\"\\e9a6\"}.ki-cloud-download.ki-outline:before{content:\"\\e9a7\"}.ki-cloud.ki-outline:before{content:\"\\e9a8\"}.ki-code.ki-outline:before{content:\"\\e9a9\"}.ki-coffee.ki-outline:before{content:\"\\e9aa\"}.ki-color-swatch.ki-outline:before{content:\"\\e9ab\"}.ki-colors-square.ki-outline:before{content:\"\\e9ac\"}.ki-compass.ki-outline:before{content:\"\\e9ad\"}.ki-copy-success.ki-outline:before{content:\"\\e9ae\"}.ki-copy.ki-outline:before{content:\"\\e9af\"}.ki-courier-express.ki-outline:before{content:\"\\e9b0\"}.ki-courier.ki-outline:before{content:\"\\e9b1\"}.ki-credit-cart.ki-outline:before{content:\"\\e9b2\"}.ki-cross-circle.ki-outline:before{content:\"\\e9b3\"}.ki-cross-square.ki-outline:before{content:\"\\e9b4\"}.ki-cross.ki-outline:before{content:\"\\e9b5\"}.ki-crown-2.ki-outline:before{content:\"\\e9b6\"}.ki-crown.ki-outline:before{content:\"\\e9b7\"}.ki-css.ki-outline:before{content:\"\\e9b8\"}.ki-cube-2.ki-outline:before{content:\"\\e9b9\"}.ki-cube-3.ki-outline:before{content:\"\\e9ba\"}.ki-cup.ki-outline:before{content:\"\\e9bb\"}.ki-dash.ki-outline:before{content:\"\\e9bc\"}.ki-data.ki-outline:before{content:\"\\e9bd\"}.ki-delete-files.ki-outline:before{content:\"\\e9be\"}.ki-delete-folder.ki-outline:before{content:\"\\e9bf\"}.ki-delivery-2.ki-outline:before{content:\"\\e9c0\"}.ki-delivery-3.ki-outline:before{content:\"\\e9c1\"}.ki-delivery-24.ki-outline:before{content:\"\\e9c2\"}.ki-delivery-door.ki-outline:before{content:\"\\e9c3\"}.ki-delivery-geolocation.ki-outline:before{content:\"\\e9c4\"}.ki-delivery-time.ki-outline:before{content:\"\\e9c5\"}.ki-delivery.ki-outline:before{content:\"\\e9c6\"}.ki-design-2.ki-outline:before{content:\"\\e9c7\"}.ki-design-frame.ki-outline:before{content:\"\\e9c8\"}.ki-design-mask.ki-outline:before{content:\"\\e9c9\"}.ki-design.ki-outline:before{content:\"\\e9ca\"}.ki-devices-2.ki-outline:before{content:\"\\e9cb\"}.ki-devices.ki-outline:before{content:\"\\e9cc\"}.ki-diamonds.ki-outline:before{content:\"\\e9cd\"}.ki-directbox-default.ki-outline:before{content:\"\\e9ce\"}.ki-disconnect.ki-outline:before{content:\"\\e9cf\"}.ki-discount.ki-outline:before{content:\"\\e9d0\"}.ki-disk.ki-outline:before{content:\"\\e9d1\"}.ki-dislike.ki-outline:before{content:\"\\e9d2\"}.ki-dj.ki-outline:before{content:\"\\e9d3\"}.ki-document.ki-outline:before{content:\"\\e9d4\"}.ki-dollar.ki-outline:before{content:\"\\e9d5\"}.ki-dots-circle-vertical.ki-outline:before{content:\"\\e9d6\"}.ki-dots-circle.ki-outline:before{content:\"\\e9d7\"}.ki-dots-horizontal.ki-outline:before{content:\"\\e9d8\"}.ki-dots-square-vertical.ki-outline:before{content:\"\\e9d9\"}.ki-dots-square.ki-outline:before{content:\"\\e9da\"}.ki-dots-vertical.ki-outline:before{content:\"\\e9db\"}.ki-double-check-circle.ki-outline:before{content:\"\\e9dc\"}.ki-double-check.ki-outline:before{content:\"\\e9dd\"}.ki-double-down.ki-outline:before{content:\"\\e9de\"}.ki-double-left-arrow.ki-outline:before{content:\"\\e9df\"}.ki-double-left.ki-outline:before{content:\"\\e9e0\"}.ki-double-right-arrow.ki-outline:before{content:\"\\e9e1\"}.ki-double-right.ki-outline:before{content:\"\\e9e2\"}.ki-double-up.ki-outline:before{content:\"\\e9e3\"}.ki-down-square.ki-outline:before{content:\"\\e9e4\"}.ki-down.ki-outline:before{content:\"\\e9e5\"}.ki-dribbble.ki-outline:before{content:\"\\e9e6\"}.ki-drop.ki-outline:before{content:\"\\e9e7\"}.ki-dropbox.ki-outline:before{content:\"\\e9e8\"}.ki-educare.ki-outline:before{content:\"\\e9e9\"}.ki-electricity.ki-outline:before{content:\"\\e9ea\"}.ki-electronic-clock.ki-outline:before{content:\"\\e9eb\"}.ki-element-1.ki-outline:before{content:\"\\e9ec\"}.ki-element-2.ki-outline:before{content:\"\\e9ed\"}.ki-element-3.ki-outline:before{content:\"\\e9ee\"}.ki-element-4.ki-outline:before{content:\"\\e9ef\"}.ki-element-5.ki-outline:before{content:\"\\e9f0\"}.ki-element-6.ki-outline:before{content:\"\\e9f1\"}.ki-element-7.ki-outline:before{content:\"\\e9f2\"}.ki-element-8.ki-outline:before{content:\"\\e9f3\"}.ki-element-9.ki-outline:before{content:\"\\e9f4\"}.ki-element-10.ki-outline:before{content:\"\\e9f5\"}.ki-element-11.ki-outline:before{content:\"\\e9f6\"}.ki-element-12.ki-outline:before{content:\"\\e9f7\"}.ki-element-equal.ki-outline:before{content:\"\\e9f8\"}.ki-element-plus.ki-outline:before{content:\"\\e9f9\"}.ki-emoji-happy.ki-outline:before{content:\"\\e9fa\"}.ki-enjin-coin.ki-outline:before{content:\"\\e9fb\"}.ki-entrance-left.ki-outline:before{content:\"\\e9fc\"}.ki-entrance-right.ki-outline:before{content:\"\\e9fd\"}.ki-eraser.ki-outline:before{content:\"\\e9fe\"}.ki-euro.ki-outline:before{content:\"\\e9ff\"}.ki-exit-down.ki-outline:before{content:\"\\ea00\"}.ki-exit-left.ki-outline:before{content:\"\\ea01\"}.ki-exit-right-corner.ki-outline:before{content:\"\\ea02\"}.ki-exit-right.ki-outline:before{content:\"\\ea03\"}.ki-exit-up.ki-outline:before{content:\"\\ea04\"}.ki-external-drive.ki-outline:before{content:\"\\ea05\"}.ki-eye-slash.ki-outline:before{content:\"\\ea06\"}.ki-eye.ki-outline:before{content:\"\\ea07\"}.ki-facebook.ki-outline:before{content:\"\\ea08\"}.ki-faceid.ki-outline:before{content:\"\\ea09\"}.ki-fasten.ki-outline:before{content:\"\\ea0a\"}.ki-fat-rows.ki-outline:before{content:\"\\ea0b\"}.ki-feather.ki-outline:before{content:\"\\ea0c\"}.ki-figma.ki-outline:before{content:\"\\ea0d\"}.ki-file-added.ki-outline:before{content:\"\\ea0e\"}.ki-file-deleted.ki-outline:before{content:\"\\ea0f\"}.ki-file-down.ki-outline:before{content:\"\\ea10\"}.ki-file-left.ki-outline:before{content:\"\\ea11\"}.ki-file-right.ki-outline:before{content:\"\\ea12\"}.ki-file-sheet.ki-outline:before{content:\"\\ea13\"}.ki-file-up.ki-outline:before{content:\"\\ea14\"}.ki-file.ki-outline:before{content:\"\\ea15\"}.ki-files-tablet.ki-outline:before{content:\"\\ea16\"}.ki-filter-edit.ki-outline:before{content:\"\\ea17\"}.ki-filter-search.ki-outline:before{content:\"\\ea18\"}.ki-filter-square.ki-outline:before{content:\"\\ea19\"}.ki-filter-tablet.ki-outline:before{content:\"\\ea1a\"}.ki-filter-tick.ki-outline:before{content:\"\\ea1b\"}.ki-filter.ki-outline:before{content:\"\\ea1c\"}.ki-finance-calculator.ki-outline:before{content:\"\\ea1d\"}.ki-financial-schedule.ki-outline:before{content:\"\\ea1e\"}.ki-fingerprint-scanning.ki-outline:before{content:\"\\ea1f\"}.ki-flag.ki-outline:before{content:\"\\ea20\"}.ki-flash-circle.ki-outline:before{content:\"\\ea21\"}.ki-flask.ki-outline:before{content:\"\\ea22\"}.ki-focus.ki-outline:before{content:\"\\ea23\"}.ki-folder-added.ki-outline:before{content:\"\\ea24\"}.ki-folder-down.ki-outline:before{content:\"\\ea25\"}.ki-folder-up.ki-outline:before{content:\"\\ea26\"}.ki-folder.ki-outline:before{content:\"\\ea27\"}.ki-frame.ki-outline:before{content:\"\\ea28\"}.ki-gear.ki-outline:before{content:\"\\ea29\"}.ki-general-mouse.ki-outline:before{content:\"\\ea2a\"}.ki-geolocation-home.ki-outline:before{content:\"\\ea2b\"}.ki-geolocation.ki-outline:before{content:\"\\ea2c\"}.ki-ghost.ki-outline:before{content:\"\\ea2d\"}.ki-gift.ki-outline:before{content:\"\\ea2e\"}.ki-github.ki-outline:before{content:\"\\ea2f\"}.ki-glass.ki-outline:before{content:\"\\ea30\"}.ki-google-play.ki-outline:before{content:\"\\ea31\"}.ki-google.ki-outline:before{content:\"\\ea32\"}.ki-graph-2.ki-outline:before{content:\"\\ea33\"}.ki-graph-3.ki-outline:before{content:\"\\ea34\"}.ki-graph-4.ki-outline:before{content:\"\\ea35\"}.ki-graph-up.ki-outline:before{content:\"\\ea36\"}.ki-graph.ki-outline:before{content:\"\\ea37\"}.ki-grid-2.ki-outline:before{content:\"\\ea38\"}.ki-grid-frame.ki-outline:before{content:\"\\ea39\"}.ki-grid.ki-outline:before{content:\"\\ea3a\"}.ki-handcart.ki-outline:before{content:\"\\ea3b\"}.ki-happy-emoji.ki-outline:before{content:\"\\ea3c\"}.ki-heart-circle.ki-outline:before{content:\"\\ea3d\"}.ki-heart.ki-outline:before{content:\"\\ea3e\"}.ki-home-1.ki-outline:before{content:\"\\ea3f\"}.ki-home-2.ki-outline:before{content:\"\\ea40\"}.ki-home-3.ki-outline:before{content:\"\\ea41\"}.ki-home.ki-outline:before{content:\"\\ea42\"}.ki-html.ki-outline:before{content:\"\\ea43\"}.ki-icon.ki-outline:before{content:\"\\ea44\"}.ki-illustrator.ki-outline:before{content:\"\\ea45\"}.ki-information-2.ki-outline:before{content:\"\\ea46\"}.ki-information-3.ki-outline:before{content:\"\\ea47\"}.ki-information-4.ki-outline:before{content:\"\\ea48\"}.ki-information-5.ki-outline:before{content:\"\\ea49\"}.ki-information.ki-outline:before{content:\"\\ea4a\"}.ki-instagram.ki-outline:before{content:\"\\ea4b\"}.ki-joystick.ki-outline:before{content:\"\\ea4c\"}.ki-js-2.ki-outline:before{content:\"\\ea4d\"}.ki-js.ki-outline:before{content:\"\\ea4e\"}.ki-kanban.ki-outline:before{content:\"\\ea4f\"}.ki-key-square.ki-outline:before{content:\"\\ea50\"}.ki-key.ki-outline:before{content:\"\\ea51\"}.ki-keyboard.ki-outline:before{content:\"\\ea52\"}.ki-laptop.ki-outline:before{content:\"\\ea53\"}.ki-laravel.ki-outline:before{content:\"\\ea54\"}.ki-left-square.ki-outline:before{content:\"\\ea55\"}.ki-left.ki-outline:before{content:\"\\ea56\"}.ki-like-2.ki-outline:before{content:\"\\ea57\"}.ki-like-folder.ki-outline:before{content:\"\\ea58\"}.ki-like-shapes.ki-outline:before{content:\"\\ea59\"}.ki-like-tag.ki-outline:before{content:\"\\ea5a\"}.ki-like.ki-outline:before{content:\"\\ea5b\"}.ki-loading.ki-outline:before{content:\"\\ea5c\"}.ki-lock-2.ki-outline:before{content:\"\\ea5d\"}.ki-lock-3.ki-outline:before{content:\"\\ea5e\"}.ki-lock.ki-outline:before{content:\"\\ea5f\"}.ki-logistic.ki-outline:before{content:\"\\ea60\"}.ki-lots-shopping.ki-outline:before{content:\"\\ea61\"}.ki-lovely.ki-outline:before{content:\"\\ea62\"}.ki-lts.ki-outline:before{content:\"\\ea63\"}.ki-magnifier.ki-outline:before{content:\"\\ea64\"}.ki-map.ki-outline:before{content:\"\\ea65\"}.ki-mask.ki-outline:before{content:\"\\ea66\"}.ki-maximize.ki-outline:before{content:\"\\ea67\"}.ki-medal-star.ki-outline:before{content:\"\\ea68\"}.ki-menu.ki-outline:before{content:\"\\ea69\"}.ki-message-add.ki-outline:before{content:\"\\ea6a\"}.ki-message-edit.ki-outline:before{content:\"\\ea6b\"}.ki-message-minus.ki-outline:before{content:\"\\ea6c\"}.ki-message-notif.ki-outline:before{content:\"\\ea6d\"}.ki-message-programming.ki-outline:before{content:\"\\ea6e\"}.ki-message-question.ki-outline:before{content:\"\\ea6f\"}.ki-message-text-2.ki-outline:before{content:\"\\ea70\"}.ki-message-text.ki-outline:before{content:\"\\ea71\"}.ki-messages.ki-outline:before{content:\"\\ea72\"}.ki-microsoft.ki-outline:before{content:\"\\ea73\"}.ki-milk.ki-outline:before{content:\"\\ea74\"}.ki-minus-circle.ki-outline:before{content:\"\\ea75\"}.ki-minus-folder.ki-outline:before{content:\"\\ea76\"}.ki-minus-square.ki-outline:before{content:\"\\ea77\"}.ki-minus.ki-outline:before{content:\"\\ea78\"}.ki-monitor-mobile.ki-outline:before{content:\"\\ea79\"}.ki-moon.ki-outline:before{content:\"\\ea7a\"}.ki-more-2.ki-outline:before{content:\"\\ea7b\"}.ki-mouse-circle.ki-outline:before{content:\"\\ea7c\"}.ki-mouse-square.ki-outline:before{content:\"\\ea7d\"}.ki-mouse.ki-outline:before{content:\"\\ea7e\"}.ki-nexo.ki-outline:before{content:\"\\ea7f\"}.ki-night-day.ki-outline:before{content:\"\\ea80\"}.ki-note-2.ki-outline:before{content:\"\\ea81\"}.ki-note.ki-outline:before{content:\"\\ea82\"}.ki-notepad-bookmark.ki-outline:before{content:\"\\ea83\"}.ki-notepad-edit.ki-outline:before{content:\"\\ea84\"}.ki-notepad.ki-outline:before{content:\"\\ea85\"}.ki-notification-2.ki-outline:before{content:\"\\ea86\"}.ki-notification-bing.ki-outline:before{content:\"\\ea87\"}.ki-notification-circle.ki-outline:before{content:\"\\ea88\"}.ki-notification-favorite.ki-outline:before{content:\"\\ea89\"}.ki-notification-on.ki-outline:before{content:\"\\ea8a\"}.ki-notification-status.ki-outline:before{content:\"\\ea8b\"}.ki-notification.ki-outline:before{content:\"\\ea8c\"}.ki-ocean.ki-outline:before{content:\"\\ea8d\"}.ki-office-bag.ki-outline:before{content:\"\\ea8e\"}.ki-package.ki-outline:before{content:\"\\ea8f\"}.ki-pails.ki-outline:before{content:\"\\ea90\"}.ki-paintbucket.ki-outline:before{content:\"\\ea91\"}.ki-paper-clip.ki-outline:before{content:\"\\ea92\"}.ki-parcel-tracking.ki-outline:before{content:\"\\ea93\"}.ki-parcel.ki-outline:before{content:\"\\ea94\"}.ki-password-check.ki-outline:before{content:\"\\ea95\"}.ki-paypal.ki-outline:before{content:\"\\ea96\"}.ki-pencil.ki-outline:before{content:\"\\ea97\"}.ki-people.ki-outline:before{content:\"\\ea98\"}.ki-percentage.ki-outline:before{content:\"\\ea99\"}.ki-phone.ki-outline:before{content:\"\\ea9a\"}.ki-photoshop.ki-outline:before{content:\"\\ea9b\"}.ki-picture.ki-outline:before{content:\"\\ea9c\"}.ki-pill.ki-outline:before{content:\"\\ea9d\"}.ki-pin.ki-outline:before{content:\"\\ea9e\"}.ki-plus-circle.ki-outline:before{content:\"\\ea9f\"}.ki-plus-square.ki-outline:before{content:\"\\eaa0\"}.ki-plus.ki-outline:before{content:\"\\eaa1\"}.ki-pointers.ki-outline:before{content:\"\\eaa2\"}.ki-price-tag.ki-outline:before{content:\"\\eaa3\"}.ki-printer.ki-outline:before{content:\"\\eaa4\"}.ki-profile-circle.ki-outline:before{content:\"\\eaa5\"}.ki-profile-user.ki-outline:before{content:\"\\eaa6\"}.ki-pulse.ki-outline:before{content:\"\\eaa7\"}.ki-purchase.ki-outline:before{content:\"\\eaa8\"}.ki-python.ki-outline:before{content:\"\\eaa9\"}.ki-question-2.ki-outline:before{content:\"\\eaaa\"}.ki-question.ki-outline:before{content:\"\\eaab\"}.ki-questionnaire-tablet.ki-outline:before{content:\"\\eaac\"}.ki-ranking.ki-outline:before{content:\"\\eaad\"}.ki-react.ki-outline:before{content:\"\\eaae\"}.ki-receipt-square.ki-outline:before{content:\"\\eaaf\"}.ki-rescue.ki-outline:before{content:\"\\eab0\"}.ki-right-left.ki-outline:before{content:\"\\eab1\"}.ki-right-square.ki-outline:before{content:\"\\eab2\"}.ki-right.ki-outline:before{content:\"\\eab3\"}.ki-rocket.ki-outline:before{content:\"\\eab4\"}.ki-route.ki-outline:before{content:\"\\eab5\"}.ki-router.ki-outline:before{content:\"\\eab6\"}.ki-row-horizontal.ki-outline:before{content:\"\\eab7\"}.ki-row-vertical.ki-outline:before{content:\"\\eab8\"}.ki-safe-home.ki-outline:before{content:\"\\eab9\"}.ki-satellite.ki-outline:before{content:\"\\eaba\"}.ki-save-2.ki-outline:before{content:\"\\eabb\"}.ki-save-deposit.ki-outline:before{content:\"\\eabc\"}.ki-scan-barcode.ki-outline:before{content:\"\\eabd\"}.ki-scooter-2.ki-outline:before{content:\"\\eabe\"}.ki-scooter.ki-outline:before{content:\"\\eabf\"}.ki-screen.ki-outline:before{content:\"\\eac0\"}.ki-scroll.ki-outline:before{content:\"\\eac1\"}.ki-search-list.ki-outline:before{content:\"\\eac2\"}.ki-security-check.ki-outline:before{content:\"\\eac3\"}.ki-security-user.ki-outline:before{content:\"\\eac4\"}.ki-send.ki-outline:before{content:\"\\eac5\"}.ki-setting-2.ki-outline:before{content:\"\\eac6\"}.ki-setting-3.ki-outline:before{content:\"\\eac7\"}.ki-setting-4.ki-outline:before{content:\"\\eac8\"}.ki-setting.ki-outline:before{content:\"\\eac9\"}.ki-share.ki-outline:before{content:\"\\eaca\"}.ki-shield-cross.ki-outline:before{content:\"\\eacb\"}.ki-shield-search.ki-outline:before{content:\"\\eacc\"}.ki-shield-slash.ki-outline:before{content:\"\\eacd\"}.ki-shield-tick.ki-outline:before{content:\"\\eace\"}.ki-shield.ki-outline:before{content:\"\\eacf\"}.ki-ship.ki-outline:before{content:\"\\ead0\"}.ki-shop.ki-outline:before{content:\"\\ead1\"}.ki-simcard-2.ki-outline:before{content:\"\\ead2\"}.ki-simcard.ki-outline:before{content:\"\\ead3\"}.ki-size.ki-outline:before{content:\"\\ead4\"}.ki-slack.ki-outline:before{content:\"\\ead5\"}.ki-slider-horizontal-2.ki-outline:before{content:\"\\ead6\"}.ki-slider-horizontal.ki-outline:before{content:\"\\ead7\"}.ki-slider-vertical-2.ki-outline:before{content:\"\\ead8\"}.ki-slider-vertical.ki-outline:before{content:\"\\ead9\"}.ki-slider.ki-outline:before{content:\"\\eada\"}.ki-sms.ki-outline:before{content:\"\\eadb\"}.ki-snapchat.ki-outline:before{content:\"\\eadc\"}.ki-social-media.ki-outline:before{content:\"\\eadd\"}.ki-soft-2.ki-outline:before{content:\"\\eade\"}.ki-soft-3.ki-outline:before{content:\"\\eadf\"}.ki-soft.ki-outline:before{content:\"\\eae0\"}.ki-some-files.ki-outline:before{content:\"\\eae1\"}.ki-sort.ki-outline:before{content:\"\\eae2\"}.ki-speaker.ki-outline:before{content:\"\\eae3\"}.ki-spotify.ki-outline:before{content:\"\\eae4\"}.ki-spring-framework.ki-outline:before{content:\"\\eae5\"}.ki-square-brackets.ki-outline:before{content:\"\\eae6\"}.ki-star.ki-outline:before{content:\"\\eae7\"}.ki-status.ki-outline:before{content:\"\\eae8\"}.ki-subtitle.ki-outline:before{content:\"\\eae9\"}.ki-sun.ki-outline:before{content:\"\\eaea\"}.ki-support-24.ki-outline:before{content:\"\\eaeb\"}.ki-switch.ki-outline:before{content:\"\\eaec\"}.ki-syringe.ki-outline:before{content:\"\\eaed\"}.ki-tablet-book.ki-outline:before{content:\"\\eaee\"}.ki-tablet-delete.ki-outline:before{content:\"\\eaef\"}.ki-tablet-down.ki-outline:before{content:\"\\eaf0\"}.ki-tablet-ok.ki-outline:before{content:\"\\eaf1\"}.ki-tablet-text-down.ki-outline:before{content:\"\\eaf2\"}.ki-tablet-text-up.ki-outline:before{content:\"\\eaf3\"}.ki-tablet-up.ki-outline:before{content:\"\\eaf4\"}.ki-tablet.ki-outline:before{content:\"\\eaf5\"}.ki-tag-cross.ki-outline:before{content:\"\\eaf6\"}.ki-tag.ki-outline:before{content:\"\\eaf7\"}.ki-teacher.ki-outline:before{content:\"\\eaf8\"}.ki-tech-wifi.ki-outline:before{content:\"\\eaf9\"}.ki-technology-2.ki-outline:before{content:\"\\eafa\"}.ki-technology-3.ki-outline:before{content:\"\\eafb\"}.ki-technology-4.ki-outline:before{content:\"\\eafc\"}.ki-technology.ki-outline:before{content:\"\\eafd\"}.ki-telephone-geolocation.ki-outline:before{content:\"\\eafe\"}.ki-test-tubes.ki-outline:before{content:\"\\eaff\"}.ki-text-align-center.ki-outline:before{content:\"\\eb00\"}.ki-text-align-justify-center.ki-outline:before{content:\"\\eb01\"}.ki-text-align-left.ki-outline:before{content:\"\\eb02\"}.ki-text-align-right.ki-outline:before{content:\"\\eb03\"}.ki-text-bold.ki-outline:before{content:\"\\eb04\"}.ki-text-circle.ki-outline:before{content:\"\\eb05\"}.ki-text-italic.ki-outline:before{content:\"\\eb06\"}.ki-text-number.ki-outline:before{content:\"\\eb07\"}.ki-text-strikethrough.ki-outline:before{content:\"\\eb08\"}.ki-text-underline.ki-outline:before{content:\"\\eb09\"}.ki-text.ki-outline:before{content:\"\\eb0a\"}.ki-thermometer.ki-outline:before{content:\"\\eb0b\"}.ki-theta.ki-outline:before{content:\"\\eb0c\"}.ki-tiktok.ki-outline:before{content:\"\\eb0d\"}.ki-time.ki-outline:before{content:\"\\eb0e\"}.ki-timer.ki-outline:before{content:\"\\eb0f\"}.ki-to-left.ki-outline:before{content:\"\\eb10\"}.ki-to-right.ki-outline:before{content:\"\\eb11\"}.ki-toggle-off-circle.ki-outline:before{content:\"\\eb12\"}.ki-toggle-off.ki-outline:before{content:\"\\eb13\"}.ki-toggle-on-circle.ki-outline:before{content:\"\\eb14\"}.ki-toggle-on.ki-outline:before{content:\"\\eb15\"}.ki-trailer.ki-outline:before{content:\"\\eb16\"}.ki-trash-square.ki-outline:before{content:\"\\eb17\"}.ki-trash.ki-outline:before{content:\"\\eb18\"}.ki-tree.ki-outline:before{content:\"\\eb19\"}.ki-trello.ki-outline:before{content:\"\\eb1a\"}.ki-triangle.ki-outline:before{content:\"\\eb1b\"}.ki-truck.ki-outline:before{content:\"\\eb1c\"}.ki-ts.ki-outline:before{content:\"\\eb1d\"}.ki-twitch.ki-outline:before{content:\"\\eb1e\"}.ki-twitter.ki-outline:before{content:\"\\eb1f\"}.ki-two-credit-cart.ki-outline:before{content:\"\\eb20\"}.ki-underlining.ki-outline:before{content:\"\\eb21\"}.ki-up-down.ki-outline:before{content:\"\\eb22\"}.ki-up-square.ki-outline:before{content:\"\\eb23\"}.ki-up.ki-outline:before{content:\"\\eb24\"}.ki-update-file.ki-outline:before{content:\"\\eb25\"}.ki-update-folder.ki-outline:before{content:\"\\eb26\"}.ki-user-edit.ki-outline:before{content:\"\\eb27\"}.ki-user-square.ki-outline:before{content:\"\\eb28\"}.ki-user-tick.ki-outline:before{content:\"\\eb29\"}.ki-user.ki-outline:before{content:\"\\eb2a\"}.ki-verify.ki-outline:before{content:\"\\eb2b\"}.ki-vibe.ki-outline:before{content:\"\\eb2c\"}.ki-virus.ki-outline:before{content:\"\\eb2d\"}.ki-vue.ki-outline:before{content:\"\\eb2e\"}.ki-vuesax.ki-outline:before{content:\"\\eb2f\"}.ki-wallet.ki-outline:before{content:\"\\eb30\"}.ki-wanchain.ki-outline:before{content:\"\\eb31\"}.ki-watch.ki-outline:before{content:\"\\eb32\"}.ki-whatsapp.ki-outline:before{content:\"\\eb33\"}.ki-wifi-home.ki-outline:before{content:\"\\eb34\"}.ki-wifi-square.ki-outline:before{content:\"\\eb35\"}.ki-wifi.ki-outline:before{content:\"\\eb36\"}.ki-wrench.ki-outline:before{content:\"\\eb37\"}.ki-xaomi.ki-outline:before{content:\"\\eb38\"}.ki-xd.ki-outline:before{content:\"\\eb39\"}.ki-xmr.ki-outline:before{content:\"\\eb3a\"}.ki-yii.ki-outline:before{content:\"\\eb3b\"}.ki-youtube.ki-outline:before{content:\"\\eb3c\"}@font-face{font-family:keenicons-solid;src:url(fonts/keenicons/keenicons-solid.eot?812fv7);src:url(fonts/keenicons/keenicons-solid.eot?812fv7#iefix) format(\"embedded-opentype\"),url(fonts/keenicons/keenicons-solid.ttf?812fv7) format(\"truetype\"),url(fonts/keenicons/keenicons-solid.woff?812fv7) format(\"woff\"),url(fonts/keenicons/keenicons-solid.svg?812fv7#keenicons-solid) format(\"svg\");font-weight:400;font-style:normal;font-display:block}.ki-solid{font-family:keenicons-solid!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ki-abstract-1.ki-solid:before{content:\"\\e900\"}.ki-abstract-2.ki-solid:before{content:\"\\e901\"}.ki-abstract-3.ki-solid:before{content:\"\\e902\"}.ki-abstract-4.ki-solid:before{content:\"\\e903\"}.ki-abstract-5.ki-solid:before{content:\"\\e904\"}.ki-abstract-6.ki-solid:before{content:\"\\e905\"}.ki-abstract-7.ki-solid:before{content:\"\\e906\"}.ki-abstract-8.ki-solid:before{content:\"\\e907\"}.ki-abstract-9.ki-solid:before{content:\"\\e908\"}.ki-abstract-10.ki-solid:before{content:\"\\e909\"}.ki-abstract-11.ki-solid:before{content:\"\\e90a\"}.ki-abstract-12.ki-solid:before{content:\"\\e90b\"}.ki-abstract-13.ki-solid:before{content:\"\\e90c\"}.ki-abstract-14.ki-solid:before{content:\"\\e90d\"}.ki-abstract-15.ki-solid:before{content:\"\\e90e\"}.ki-abstract-16.ki-solid:before{content:\"\\e90f\"}.ki-abstract-17.ki-solid:before{content:\"\\e910\"}.ki-abstract-18.ki-solid:before{content:\"\\e911\"}.ki-abstract-19.ki-solid:before{content:\"\\e912\"}.ki-abstract-20.ki-solid:before{content:\"\\e913\"}.ki-abstract-21.ki-solid:before{content:\"\\e914\"}.ki-abstract-22.ki-solid:before{content:\"\\e915\"}.ki-abstract-23.ki-solid:before{content:\"\\e916\"}.ki-abstract-24.ki-solid:before{content:\"\\e917\"}.ki-abstract-25.ki-solid:before{content:\"\\e918\"}.ki-abstract-26.ki-solid:before{content:\"\\e919\"}.ki-abstract-27.ki-solid:before{content:\"\\e91a\"}.ki-abstract-28.ki-solid:before{content:\"\\e91b\"}.ki-abstract-29.ki-solid:before{content:\"\\e91c\"}.ki-abstract-30.ki-solid:before{content:\"\\e91d\"}.ki-abstract-31.ki-solid:before{content:\"\\e91e\"}.ki-abstract-32.ki-solid:before{content:\"\\e91f\"}.ki-abstract-33.ki-solid:before{content:\"\\e920\"}.ki-abstract-34.ki-solid:before{content:\"\\e921\"}.ki-abstract-35.ki-solid:before{content:\"\\e922\"}.ki-abstract-36.ki-solid:before{content:\"\\e923\"}.ki-abstract-37.ki-solid:before{content:\"\\e924\"}.ki-abstract-38.ki-solid:before{content:\"\\e925\"}.ki-abstract-39.ki-solid:before{content:\"\\e926\"}.ki-abstract-40.ki-solid:before{content:\"\\e927\"}.ki-abstract-41.ki-solid:before{content:\"\\e928\"}.ki-abstract-42.ki-solid:before{content:\"\\e929\"}.ki-abstract-43.ki-solid:before{content:\"\\e92a\"}.ki-abstract-44.ki-solid:before{content:\"\\e92b\"}.ki-abstract-45.ki-solid:before{content:\"\\e92c\"}.ki-abstract-46.ki-solid:before{content:\"\\e92d\"}.ki-abstract-47.ki-solid:before{content:\"\\e92e\"}.ki-abstract-48.ki-solid:before{content:\"\\e92f\"}.ki-abstract-49.ki-solid:before{content:\"\\e930\"}.ki-abstract.ki-solid:before{content:\"\\e931\"}.ki-add-files.ki-solid:before{content:\"\\e932\"}.ki-add-folder.ki-solid:before{content:\"\\e933\"}.ki-add-item.ki-solid:before{content:\"\\e934\"}.ki-add-notepad.ki-solid:before{content:\"\\e935\"}.ki-address-book.ki-solid:before{content:\"\\e936\"}.ki-airplane-square.ki-solid:before{content:\"\\e937\"}.ki-airplane.ki-solid:before{content:\"\\e938\"}.ki-airpod.ki-solid:before{content:\"\\e939\"}.ki-android.ki-solid:before{content:\"\\e93a\"}.ki-angular.ki-solid:before{content:\"\\e93b\"}.ki-apple.ki-solid:before{content:\"\\e93c\"}.ki-archive-tick.ki-solid:before{content:\"\\e93d\"}.ki-archive.ki-solid:before{content:\"\\e93e\"}.ki-arrow-circle-left.ki-solid:before{content:\"\\e93f\"}.ki-arrow-circle-right.ki-solid:before{content:\"\\e940\"}.ki-arrow-diagonal.ki-solid:before{content:\"\\e941\"}.ki-arrow-down-left.ki-solid:before{content:\"\\e942\"}.ki-arrow-down-refraction.ki-solid:before{content:\"\\e943\"}.ki-arrow-down-right.ki-solid:before{content:\"\\e944\"}.ki-arrow-down.ki-solid:before{content:\"\\e945\"}.ki-arrow-left.ki-solid:before{content:\"\\e946\"}.ki-arrow-mix.ki-solid:before{content:\"\\e947\"}.ki-arrow-right-left.ki-solid:before{content:\"\\e948\"}.ki-arrow-right.ki-solid:before{content:\"\\e949\"}.ki-arrow-two-diagonals.ki-solid:before{content:\"\\e94a\"}.ki-arrow-up-down.ki-solid:before{content:\"\\e94b\"}.ki-arrow-up-left.ki-solid:before{content:\"\\e94c\"}.ki-arrow-up-refraction.ki-solid:before{content:\"\\e94d\"}.ki-arrow-up-right.ki-solid:before{content:\"\\e94e\"}.ki-arrow-up.ki-solid:before{content:\"\\e94f\"}.ki-arrow-zigzag.ki-solid:before{content:\"\\e950\"}.ki-arrows-circle.ki-solid:before{content:\"\\e951\"}.ki-arrows-loop.ki-solid:before{content:\"\\e952\"}.ki-artificial-intelligence.ki-solid:before{content:\"\\e953\"}.ki-auto-brightness.ki-solid:before{content:\"\\e954\"}.ki-avalanche.ki-solid:before{content:\"\\e955\"}.ki-award.ki-solid:before{content:\"\\e956\"}.ki-badge.ki-solid:before{content:\"\\e957\"}.ki-bandage.ki-solid:before{content:\"\\e958\"}.ki-bank.ki-solid:before{content:\"\\e959\"}.ki-barcode.ki-solid:before{content:\"\\e95a\"}.ki-basket-ok.ki-solid:before{content:\"\\e95b\"}.ki-basket.ki-solid:before{content:\"\\e95c\"}.ki-behance.ki-solid:before{content:\"\\e95d\"}.ki-bill.ki-solid:before{content:\"\\e95e\"}.ki-binance-usd.ki-solid:before{content:\"\\e95f\"}.ki-binance.ki-solid:before{content:\"\\e960\"}.ki-bitcoin.ki-solid:before{content:\"\\e961\"}.ki-black-down.ki-solid:before{content:\"\\e962\"}.ki-black-left-line.ki-solid:before{content:\"\\e963\"}.ki-black-left.ki-solid:before{content:\"\\e964\"}.ki-black-right-line.ki-solid:before{content:\"\\e965\"}.ki-black-right.ki-solid:before{content:\"\\e966\"}.ki-black-up.ki-solid:before{content:\"\\e967\"}.ki-bluetooth.ki-solid:before{content:\"\\e968\"}.ki-book-open.ki-solid:before{content:\"\\e969\"}.ki-book-square.ki-solid:before{content:\"\\e96a\"}.ki-book.ki-solid:before{content:\"\\e96b\"}.ki-bookmark-2.ki-solid:before{content:\"\\e96c\"}.ki-bookmark.ki-solid:before{content:\"\\e96d\"}.ki-bootstrap.ki-solid:before{content:\"\\e96e\"}.ki-briefcase.ki-solid:before{content:\"\\e96f\"}.ki-brifecase-cros.ki-solid:before{content:\"\\e970\"}.ki-brifecase-tick.ki-solid:before{content:\"\\e971\"}.ki-brifecase-timer.ki-solid:before{content:\"\\e972\"}.ki-brush.ki-solid:before{content:\"\\e973\"}.ki-bucket-square.ki-solid:before{content:\"\\e974\"}.ki-bucket.ki-solid:before{content:\"\\e975\"}.ki-burger-menu-1.ki-solid:before{content:\"\\e976\"}.ki-burger-menu-2.ki-solid:before{content:\"\\e977\"}.ki-burger-menu-3.ki-solid:before{content:\"\\e978\"}.ki-burger-menu-4.ki-solid:before{content:\"\\e979\"}.ki-burger-menu-5.ki-solid:before{content:\"\\e97a\"}.ki-burger-menu-6.ki-solid:before{content:\"\\e97b\"}.ki-burger-menu.ki-solid:before{content:\"\\e97c\"}.ki-bus.ki-solid:before{content:\"\\e97d\"}.ki-calculator.ki-solid:before{content:\"\\e97e\"}.ki-calendar-2.ki-solid:before{content:\"\\e97f\"}.ki-calendar-8.ki-solid:before{content:\"\\e980\"}.ki-calendar-add.ki-solid:before{content:\"\\e981\"}.ki-calendar-edit.ki-solid:before{content:\"\\e982\"}.ki-calendar-remove.ki-solid:before{content:\"\\e983\"}.ki-calendar-search.ki-solid:before{content:\"\\e984\"}.ki-calendar-tick.ki-solid:before{content:\"\\e985\"}.ki-calendar.ki-solid:before{content:\"\\e986\"}.ki-call.ki-solid:before{content:\"\\e987\"}.ki-capsule.ki-solid:before{content:\"\\e988\"}.ki-car-2.ki-solid:before{content:\"\\e989\"}.ki-car-3.ki-solid:before{content:\"\\e98a\"}.ki-car.ki-solid:before{content:\"\\e98b\"}.ki-category.ki-solid:before{content:\"\\e98c\"}.ki-cd.ki-solid:before{content:\"\\e98d\"}.ki-celsius.ki-solid:before{content:\"\\e98e\"}.ki-chart-line-down-2.ki-solid:before{content:\"\\e98f\"}.ki-chart-line-down.ki-solid:before{content:\"\\e990\"}.ki-chart-line-star.ki-solid:before{content:\"\\e991\"}.ki-chart-line-up-2.ki-solid:before{content:\"\\e992\"}.ki-chart-line-up.ki-solid:before{content:\"\\e993\"}.ki-chart-line.ki-solid:before{content:\"\\e994\"}.ki-chart-pie-3.ki-solid:before{content:\"\\e995\"}.ki-chart-pie-4.ki-solid:before{content:\"\\e996\"}.ki-chart-pie-simple.ki-solid:before{content:\"\\e997\"}.ki-chart-pie-too.ki-solid:before{content:\"\\e998\"}.ki-chart-simple-2.ki-solid:before{content:\"\\e999\"}.ki-chart-simple-3.ki-solid:before{content:\"\\e99a\"}.ki-chart-simple.ki-solid:before{content:\"\\e99b\"}.ki-chart.ki-solid:before{content:\"\\e99c\"}.ki-check-circle.ki-solid:before{content:\"\\e99d\"}.ki-check-square.ki-solid:before{content:\"\\e99e\"}.ki-check.ki-solid:before{content:\"\\e99f\"}.ki-cheque.ki-solid:before{content:\"\\e9a0\"}.ki-chrome.ki-solid:before{content:\"\\e9a1\"}.ki-classmates.ki-solid:before{content:\"\\e9a2\"}.ki-click.ki-solid:before{content:\"\\e9a3\"}.ki-clipboard.ki-solid:before{content:\"\\e9a4\"}.ki-cloud-add.ki-solid:before{content:\"\\e9a5\"}.ki-cloud-change.ki-solid:before{content:\"\\e9a6\"}.ki-cloud-download.ki-solid:before{content:\"\\e9a7\"}.ki-cloud.ki-solid:before{content:\"\\e9a8\"}.ki-code.ki-solid:before{content:\"\\e9a9\"}.ki-coffee.ki-solid:before{content:\"\\e9aa\"}.ki-color-swatch.ki-solid:before{content:\"\\e9ab\"}.ki-colors-square.ki-solid:before{content:\"\\e9ac\"}.ki-compass.ki-solid:before{content:\"\\e9ad\"}.ki-copy-success.ki-solid:before{content:\"\\e9ae\"}.ki-copy.ki-solid:before{content:\"\\e9af\"}.ki-courier-express.ki-solid:before{content:\"\\e9b0\"}.ki-courier.ki-solid:before{content:\"\\e9b1\"}.ki-credit-cart.ki-solid:before{content:\"\\e9b2\"}.ki-cross-circle.ki-solid:before{content:\"\\e9b3\"}.ki-cross-square.ki-solid:before{content:\"\\e9b4\"}.ki-cross.ki-solid:before{content:\"\\e9b5\"}.ki-crown-2.ki-solid:before{content:\"\\e9b6\"}.ki-crown.ki-solid:before{content:\"\\e9b7\"}.ki-css.ki-solid:before{content:\"\\e9b8\"}.ki-cube-2.ki-solid:before{content:\"\\e9b9\"}.ki-cube-3.ki-solid:before{content:\"\\e9ba\"}.ki-cup.ki-solid:before{content:\"\\e9bb\"}.ki-dash.ki-solid:before{content:\"\\e9bc\"}.ki-data.ki-solid:before{content:\"\\e9bd\"}.ki-delete-files.ki-solid:before{content:\"\\e9be\"}.ki-delete-folder.ki-solid:before{content:\"\\e9bf\"}.ki-delivery-2.ki-solid:before{content:\"\\e9c0\"}.ki-delivery-3.ki-solid:before{content:\"\\e9c1\"}.ki-delivery-24.ki-solid:before{content:\"\\e9c2\"}.ki-delivery-door.ki-solid:before{content:\"\\e9c3\"}.ki-delivery-geolocation.ki-solid:before{content:\"\\e9c4\"}.ki-delivery-time.ki-solid:before{content:\"\\e9c5\"}.ki-delivery.ki-solid:before{content:\"\\e9c6\"}.ki-design-2.ki-solid:before{content:\"\\e9c7\"}.ki-design-frame.ki-solid:before{content:\"\\e9c8\"}.ki-design-mask.ki-solid:before{content:\"\\e9c9\"}.ki-design.ki-solid:before{content:\"\\e9ca\"}.ki-devices-2.ki-solid:before{content:\"\\e9cb\"}.ki-devices.ki-solid:before{content:\"\\e9cc\"}.ki-diamonds.ki-solid:before{content:\"\\e9cd\"}.ki-directbox-default.ki-solid:before{content:\"\\e9ce\"}.ki-disconnect.ki-solid:before{content:\"\\e9cf\"}.ki-discount.ki-solid:before{content:\"\\e9d0\"}.ki-disk.ki-solid:before{content:\"\\e9d1\"}.ki-dislike.ki-solid:before{content:\"\\e9d2\"}.ki-dj.ki-solid:before{content:\"\\e9d3\"}.ki-document.ki-solid:before{content:\"\\e9d4\"}.ki-dollar.ki-solid:before{content:\"\\e9d5\"}.ki-dots-circle-vertical.ki-solid:before{content:\"\\e9d6\"}.ki-dots-circle.ki-solid:before{content:\"\\e9d7\"}.ki-dots-horizontal.ki-solid:before{content:\"\\e9d8\"}.ki-dots-square-vertical.ki-solid:before{content:\"\\e9d9\"}.ki-dots-square.ki-solid:before{content:\"\\e9da\"}.ki-dots-vertical.ki-solid:before{content:\"\\e9db\"}.ki-double-check-circle.ki-solid:before{content:\"\\e9dc\"}.ki-double-check.ki-solid:before{content:\"\\e9dd\"}.ki-double-down.ki-solid:before{content:\"\\e9de\"}.ki-double-left-arrow.ki-solid:before{content:\"\\e9df\"}.ki-double-left.ki-solid:before{content:\"\\e9e0\"}.ki-double-right-arrow.ki-solid:before{content:\"\\e9e1\"}.ki-double-right.ki-solid:before{content:\"\\e9e2\"}.ki-double-up.ki-solid:before{content:\"\\e9e3\"}.ki-down-square.ki-solid:before{content:\"\\e9e4\"}.ki-down.ki-solid:before{content:\"\\e9e5\"}.ki-dribbble.ki-solid:before{content:\"\\e9e6\"}.ki-drop.ki-solid:before{content:\"\\e9e7\"}.ki-dropbox.ki-solid:before{content:\"\\e9e8\"}.ki-educare.ki-solid:before{content:\"\\e9e9\"}.ki-electricity.ki-solid:before{content:\"\\e9ea\"}.ki-electronic-clock.ki-solid:before{content:\"\\e9eb\"}.ki-element-1.ki-solid:before{content:\"\\e9ec\"}.ki-element-2.ki-solid:before{content:\"\\e9ed\"}.ki-element-3.ki-solid:before{content:\"\\e9ee\"}.ki-element-4.ki-solid:before{content:\"\\e9ef\"}.ki-element-5.ki-solid:before{content:\"\\e9f0\"}.ki-element-6.ki-solid:before{content:\"\\e9f1\"}.ki-element-7.ki-solid:before{content:\"\\e9f2\"}.ki-element-8.ki-solid:before{content:\"\\e9f3\"}.ki-element-9.ki-solid:before{content:\"\\e9f4\"}.ki-element-10.ki-solid:before{content:\"\\e9f5\"}.ki-element-11.ki-solid:before{content:\"\\e9f6\"}.ki-element-12.ki-solid:before{content:\"\\e9f7\"}.ki-element-equal.ki-solid:before{content:\"\\e9f8\"}.ki-element-plus.ki-solid:before{content:\"\\e9f9\"}.ki-emoji-happy.ki-solid:before{content:\"\\e9fa\"}.ki-enjin-coin.ki-solid:before{content:\"\\e9fb\"}.ki-entrance-left.ki-solid:before{content:\"\\e9fc\"}.ki-entrance-right.ki-solid:before{content:\"\\e9fd\"}.ki-eraser.ki-solid:before{content:\"\\e9fe\"}.ki-euro.ki-solid:before{content:\"\\e9ff\"}.ki-exit-down.ki-solid:before{content:\"\\ea00\"}.ki-exit-left.ki-solid:before{content:\"\\ea01\"}.ki-exit-right-corner.ki-solid:before{content:\"\\ea02\"}.ki-exit-right.ki-solid:before{content:\"\\ea03\"}.ki-exit-up.ki-solid:before{content:\"\\ea04\"}.ki-external-drive.ki-solid:before{content:\"\\ea05\"}.ki-eye-slash.ki-solid:before{content:\"\\ea06\"}.ki-eye.ki-solid:before{content:\"\\ea07\"}.ki-facebook.ki-solid:before{content:\"\\ea08\"}.ki-faceid.ki-solid:before{content:\"\\ea09\"}.ki-fasten.ki-solid:before{content:\"\\ea0a\"}.ki-fat-rows.ki-solid:before{content:\"\\ea0b\"}.ki-feather.ki-solid:before{content:\"\\ea0c\"}.ki-figma.ki-solid:before{content:\"\\ea0d\"}.ki-file-added.ki-solid:before{content:\"\\ea0e\"}.ki-file-deleted.ki-solid:before{content:\"\\ea0f\"}.ki-file-down.ki-solid:before{content:\"\\ea10\"}.ki-file-left.ki-solid:before{content:\"\\ea11\"}.ki-file-right.ki-solid:before{content:\"\\ea12\"}.ki-file-sheet.ki-solid:before{content:\"\\ea13\"}.ki-file-up.ki-solid:before{content:\"\\ea14\"}.ki-file.ki-solid:before{content:\"\\ea15\"}.ki-files-tablet.ki-solid:before{content:\"\\ea16\"}.ki-filter-edit.ki-solid:before{content:\"\\ea17\"}.ki-filter-search.ki-solid:before{content:\"\\ea18\"}.ki-filter-square.ki-solid:before{content:\"\\ea19\"}.ki-filter-tablet.ki-solid:before{content:\"\\ea1a\"}.ki-filter-tick.ki-solid:before{content:\"\\ea1b\"}.ki-filter.ki-solid:before{content:\"\\ea1c\"}.ki-finance-calculator.ki-solid:before{content:\"\\ea1d\"}.ki-financial-schedule.ki-solid:before{content:\"\\ea1e\"}.ki-fingerprint-scanning.ki-solid:before{content:\"\\ea1f\"}.ki-flag.ki-solid:before{content:\"\\ea20\"}.ki-flash-circle.ki-solid:before{content:\"\\ea21\"}.ki-flask.ki-solid:before{content:\"\\ea22\"}.ki-focus.ki-solid:before{content:\"\\ea23\"}.ki-folder-added.ki-solid:before{content:\"\\ea24\"}.ki-folder-down.ki-solid:before{content:\"\\ea25\"}.ki-folder-up.ki-solid:before{content:\"\\ea26\"}.ki-folder.ki-solid:before{content:\"\\ea27\"}.ki-frame.ki-solid:before{content:\"\\ea28\"}.ki-gear.ki-solid:before{content:\"\\ea29\"}.ki-general-mouse.ki-solid:before{content:\"\\ea2a\"}.ki-geolocation-home.ki-solid:before{content:\"\\ea2b\"}.ki-geolocation.ki-solid:before{content:\"\\ea2c\"}.ki-ghost.ki-solid:before{content:\"\\ea2d\"}.ki-gift.ki-solid:before{content:\"\\ea2e\"}.ki-github.ki-solid:before{content:\"\\ea2f\"}.ki-glass.ki-solid:before{content:\"\\ea30\"}.ki-google-play.ki-solid:before{content:\"\\ea31\"}.ki-google.ki-solid:before{content:\"\\ea32\"}.ki-graph-2.ki-solid:before{content:\"\\ea33\"}.ki-graph-3.ki-solid:before{content:\"\\ea34\"}.ki-graph-4.ki-solid:before{content:\"\\ea35\"}.ki-graph-up.ki-solid:before{content:\"\\ea36\"}.ki-graph.ki-solid:before{content:\"\\ea37\"}.ki-grid-2.ki-solid:before{content:\"\\ea38\"}.ki-grid-frame.ki-solid:before{content:\"\\ea39\"}.ki-grid.ki-solid:before{content:\"\\ea3a\"}.ki-handcart.ki-solid:before{content:\"\\ea3b\"}.ki-happy-emoji.ki-solid:before{content:\"\\ea3c\"}.ki-heart-circle.ki-solid:before{content:\"\\ea3d\"}.ki-heart.ki-solid:before{content:\"\\ea3e\"}.ki-home-1.ki-solid:before{content:\"\\ea3f\"}.ki-home-2.ki-solid:before{content:\"\\ea40\"}.ki-home-3.ki-solid:before{content:\"\\ea41\"}.ki-home.ki-solid:before{content:\"\\ea42\"}.ki-html.ki-solid:before{content:\"\\ea43\"}.ki-icon.ki-solid:before{content:\"\\ea44\"}.ki-illustrator.ki-solid:before{content:\"\\ea45\"}.ki-information-2.ki-solid:before{content:\"\\ea46\"}.ki-information-3.ki-solid:before{content:\"\\ea47\"}.ki-information-4.ki-solid:before{content:\"\\ea48\"}.ki-information-5.ki-solid:before{content:\"\\ea49\"}.ki-information.ki-solid:before{content:\"\\ea4a\"}.ki-instagram.ki-solid:before{content:\"\\ea4b\"}.ki-joystick.ki-solid:before{content:\"\\ea4c\"}.ki-js-2.ki-solid:before{content:\"\\ea4d\"}.ki-js.ki-solid:before{content:\"\\ea4e\"}.ki-kanban.ki-solid:before{content:\"\\ea4f\"}.ki-key-square.ki-solid:before{content:\"\\ea50\"}.ki-key.ki-solid:before{content:\"\\ea51\"}.ki-keyboard.ki-solid:before{content:\"\\ea52\"}.ki-laptop.ki-solid:before{content:\"\\ea53\"}.ki-laravel.ki-solid:before{content:\"\\ea54\"}.ki-left-square.ki-solid:before{content:\"\\ea55\"}.ki-left.ki-solid:before{content:\"\\ea56\"}.ki-like-2.ki-solid:before{content:\"\\ea57\"}.ki-like-folder.ki-solid:before{content:\"\\ea58\"}.ki-like-shapes.ki-solid:before{content:\"\\ea59\"}.ki-like-tag.ki-solid:before{content:\"\\ea5a\"}.ki-like.ki-solid:before{content:\"\\ea5b\"}.ki-loading.ki-solid:before{content:\"\\ea5c\"}.ki-lock-2.ki-solid:before{content:\"\\ea5d\"}.ki-lock-3.ki-solid:before{content:\"\\ea5e\"}.ki-lock.ki-solid:before{content:\"\\ea5f\"}.ki-logistic.ki-solid:before{content:\"\\ea60\"}.ki-lots-shopping.ki-solid:before{content:\"\\ea61\"}.ki-lovely.ki-solid:before{content:\"\\ea62\"}.ki-lts.ki-solid:before{content:\"\\ea63\"}.ki-magnifier.ki-solid:before{content:\"\\ea64\"}.ki-map.ki-solid:before{content:\"\\ea65\"}.ki-mask.ki-solid:before{content:\"\\ea66\"}.ki-maximize.ki-solid:before{content:\"\\ea67\"}.ki-medal-star.ki-solid:before{content:\"\\ea68\"}.ki-menu.ki-solid:before{content:\"\\ea69\"}.ki-message-add.ki-solid:before{content:\"\\ea6a\"}.ki-message-edit.ki-solid:before{content:\"\\ea6b\"}.ki-message-minus.ki-solid:before{content:\"\\ea6c\"}.ki-message-notif.ki-solid:before{content:\"\\ea6d\"}.ki-message-programming.ki-solid:before{content:\"\\ea6e\"}.ki-message-question.ki-solid:before{content:\"\\ea6f\"}.ki-message-text-2.ki-solid:before{content:\"\\ea70\"}.ki-message-text.ki-solid:before{content:\"\\ea71\"}.ki-messages.ki-solid:before{content:\"\\ea72\"}.ki-microsoft.ki-solid:before{content:\"\\ea73\"}.ki-milk.ki-solid:before{content:\"\\ea74\"}.ki-minus-circle.ki-solid:before{content:\"\\ea75\"}.ki-minus-folder.ki-solid:before{content:\"\\ea76\"}.ki-minus-square.ki-solid:before{content:\"\\ea77\"}.ki-minus.ki-solid:before{content:\"\\ea78\"}.ki-monitor-mobile.ki-solid:before{content:\"\\ea79\"}.ki-moon.ki-solid:before{content:\"\\ea7a\"}.ki-more-2.ki-solid:before{content:\"\\ea7b\"}.ki-mouse-circle.ki-solid:before{content:\"\\ea7c\"}.ki-mouse-square.ki-solid:before{content:\"\\ea7d\"}.ki-mouse.ki-solid:before{content:\"\\ea7e\"}.ki-nexo.ki-solid:before{content:\"\\ea7f\"}.ki-night-day.ki-solid:before{content:\"\\ea80\"}.ki-note-2.ki-solid:before{content:\"\\ea81\"}.ki-note.ki-solid:before{content:\"\\ea82\"}.ki-notepad-bookmark.ki-solid:before{content:\"\\ea83\"}.ki-notepad-edit.ki-solid:before{content:\"\\ea84\"}.ki-notepad.ki-solid:before{content:\"\\ea85\"}.ki-notification-2.ki-solid:before{content:\"\\ea86\"}.ki-notification-bing.ki-solid:before{content:\"\\ea87\"}.ki-notification-circle.ki-solid:before{content:\"\\ea88\"}.ki-notification-favorite.ki-solid:before{content:\"\\ea89\"}.ki-notification-on.ki-solid:before{content:\"\\ea8a\"}.ki-notification-status.ki-solid:before{content:\"\\ea8b\"}.ki-notification.ki-solid:before{content:\"\\ea8c\"}.ki-ocean.ki-solid:before{content:\"\\ea8d\"}.ki-office-bag.ki-solid:before{content:\"\\ea8e\"}.ki-package.ki-solid:before{content:\"\\ea8f\"}.ki-pails.ki-solid:before{content:\"\\ea90\"}.ki-paintbucket.ki-solid:before{content:\"\\ea91\"}.ki-paper-clip.ki-solid:before{content:\"\\ea92\"}.ki-parcel-tracking.ki-solid:before{content:\"\\ea93\"}.ki-parcel.ki-solid:before{content:\"\\ea94\"}.ki-password-check.ki-solid:before{content:\"\\ea95\"}.ki-paypal.ki-solid:before{content:\"\\ea96\"}.ki-pencil.ki-solid:before{content:\"\\ea97\"}.ki-people.ki-solid:before{content:\"\\ea98\"}.ki-percentage.ki-solid:before{content:\"\\ea99\"}.ki-phone.ki-solid:before{content:\"\\ea9a\"}.ki-photoshop.ki-solid:before{content:\"\\ea9b\"}.ki-picture.ki-solid:before{content:\"\\ea9c\"}.ki-pill.ki-solid:before{content:\"\\ea9d\"}.ki-pin.ki-solid:before{content:\"\\ea9e\"}.ki-plus-circle.ki-solid:before{content:\"\\ea9f\"}.ki-plus-square.ki-solid:before{content:\"\\eaa0\"}.ki-plus.ki-solid:before{content:\"\\eaa1\"}.ki-pointers.ki-solid:before{content:\"\\eaa2\"}.ki-price-tag.ki-solid:before{content:\"\\eaa3\"}.ki-printer.ki-solid:before{content:\"\\eaa4\"}.ki-profile-circle.ki-solid:before{content:\"\\eaa5\"}.ki-profile-user.ki-solid:before{content:\"\\eaa6\"}.ki-pulse.ki-solid:before{content:\"\\eaa7\"}.ki-purchase.ki-solid:before{content:\"\\eaa8\"}.ki-python.ki-solid:before{content:\"\\eaa9\"}.ki-question-2.ki-solid:before{content:\"\\eaaa\"}.ki-question.ki-solid:before{content:\"\\eaab\"}.ki-questionnaire-tablet.ki-solid:before{content:\"\\eaac\"}.ki-ranking.ki-solid:before{content:\"\\eaad\"}.ki-react.ki-solid:before{content:\"\\eaae\"}.ki-receipt-square.ki-solid:before{content:\"\\eaaf\"}.ki-rescue.ki-solid:before{content:\"\\eab0\"}.ki-right-left.ki-solid:before{content:\"\\eab1\"}.ki-right-square.ki-solid:before{content:\"\\eab2\"}.ki-right.ki-solid:before{content:\"\\eab3\"}.ki-rocket.ki-solid:before{content:\"\\eab4\"}.ki-route.ki-solid:before{content:\"\\eab5\"}.ki-router.ki-solid:before{content:\"\\eab6\"}.ki-row-horizontal.ki-solid:before{content:\"\\eab7\"}.ki-row-vertical.ki-solid:before{content:\"\\eab8\"}.ki-safe-home.ki-solid:before{content:\"\\eab9\"}.ki-satellite.ki-solid:before{content:\"\\eaba\"}.ki-save-2.ki-solid:before{content:\"\\eabb\"}.ki-save-deposit.ki-solid:before{content:\"\\eabc\"}.ki-scan-barcode.ki-solid:before{content:\"\\eabd\"}.ki-scooter-2.ki-solid:before{content:\"\\eabe\"}.ki-scooter.ki-solid:before{content:\"\\eabf\"}.ki-screen.ki-solid:before{content:\"\\eac0\"}.ki-scroll.ki-solid:before{content:\"\\eac1\"}.ki-search-list.ki-solid:before{content:\"\\eac2\"}.ki-security-check.ki-solid:before{content:\"\\eac3\"}.ki-security-user.ki-solid:before{content:\"\\eac4\"}.ki-send.ki-solid:before{content:\"\\eac5\"}.ki-setting-2.ki-solid:before{content:\"\\eac6\"}.ki-setting-3.ki-solid:before{content:\"\\eac7\"}.ki-setting-4.ki-solid:before{content:\"\\eac8\"}.ki-setting.ki-solid:before{content:\"\\eac9\"}.ki-share.ki-solid:before{content:\"\\eaca\"}.ki-shield-cross.ki-solid:before{content:\"\\eacb\"}.ki-shield-search.ki-solid:before{content:\"\\eacc\"}.ki-shield-slash.ki-solid:before{content:\"\\eacd\"}.ki-shield-tick.ki-solid:before{content:\"\\eace\"}.ki-shield.ki-solid:before{content:\"\\eacf\"}.ki-ship.ki-solid:before{content:\"\\ead0\"}.ki-shop.ki-solid:before{content:\"\\ead1\"}.ki-simcard-2.ki-solid:before{content:\"\\ead2\"}.ki-simcard.ki-solid:before{content:\"\\ead3\"}.ki-size.ki-solid:before{content:\"\\ead4\"}.ki-slack.ki-solid:before{content:\"\\ead5\"}.ki-slider-horizontal-2.ki-solid:before{content:\"\\ead6\"}.ki-slider-horizontal.ki-solid:before{content:\"\\ead7\"}.ki-slider-vertical-2.ki-solid:before{content:\"\\ead8\"}.ki-slider-vertical.ki-solid:before{content:\"\\ead9\"}.ki-slider.ki-solid:before{content:\"\\eada\"}.ki-sms.ki-solid:before{content:\"\\eadb\"}.ki-snapchat.ki-solid:before{content:\"\\eadc\"}.ki-social-media.ki-solid:before{content:\"\\eadd\"}.ki-soft-2.ki-solid:before{content:\"\\eade\"}.ki-soft-3.ki-solid:before{content:\"\\eadf\"}.ki-soft.ki-solid:before{content:\"\\eae0\"}.ki-some-files.ki-solid:before{content:\"\\eae1\"}.ki-sort.ki-solid:before{content:\"\\eae2\"}.ki-speaker.ki-solid:before{content:\"\\eae3\"}.ki-spotify.ki-solid:before{content:\"\\eae4\"}.ki-spring-framework.ki-solid:before{content:\"\\eae5\"}.ki-square-brackets.ki-solid:before{content:\"\\eae6\"}.ki-star.ki-solid:before{content:\"\\eae7\"}.ki-status.ki-solid:before{content:\"\\eae8\"}.ki-subtitle.ki-solid:before{content:\"\\eae9\"}.ki-sun.ki-solid:before{content:\"\\eaea\"}.ki-support-24.ki-solid:before{content:\"\\eaeb\"}.ki-switch.ki-solid:before{content:\"\\eaec\"}.ki-syringe.ki-solid:before{content:\"\\eaed\"}.ki-tablet-book.ki-solid:before{content:\"\\eaee\"}.ki-tablet-delete.ki-solid:before{content:\"\\eaef\"}.ki-tablet-down.ki-solid:before{content:\"\\eaf0\"}.ki-tablet-ok.ki-solid:before{content:\"\\eaf1\"}.ki-tablet-text-down.ki-solid:before{content:\"\\eaf2\"}.ki-tablet-text-up.ki-solid:before{content:\"\\eaf3\"}.ki-tablet-up.ki-solid:before{content:\"\\eaf4\"}.ki-tablet.ki-solid:before{content:\"\\eaf5\"}.ki-tag-cross.ki-solid:before{content:\"\\eaf6\"}.ki-tag.ki-solid:before{content:\"\\eaf7\"}.ki-teacher.ki-solid:before{content:\"\\eaf8\"}.ki-tech-wifi.ki-solid:before{content:\"\\eaf9\"}.ki-technology-2.ki-solid:before{content:\"\\eafa\"}.ki-technology-3.ki-solid:before{content:\"\\eafb\"}.ki-technology-4.ki-solid:before{content:\"\\eafc\"}.ki-technology.ki-solid:before{content:\"\\eafd\"}.ki-telephone-geolocation.ki-solid:before{content:\"\\eafe\"}.ki-test-tubes.ki-solid:before{content:\"\\eaff\"}.ki-text-align-center.ki-solid:before{content:\"\\eb00\"}.ki-text-align-justify-center.ki-solid:before{content:\"\\eb01\"}.ki-text-align-left.ki-solid:before{content:\"\\eb02\"}.ki-text-align-right.ki-solid:before{content:\"\\eb03\"}.ki-text-bold.ki-solid:before{content:\"\\eb04\"}.ki-text-circle.ki-solid:before{content:\"\\eb05\"}.ki-text-italic.ki-solid:before{content:\"\\eb06\"}.ki-text-number.ki-solid:before{content:\"\\eb07\"}.ki-text-strikethrough.ki-solid:before{content:\"\\eb08\"}.ki-text-underline.ki-solid:before{content:\"\\eb09\"}.ki-text.ki-solid:before{content:\"\\eb0a\"}.ki-thermometer.ki-solid:before{content:\"\\eb0b\"}.ki-theta.ki-solid:before{content:\"\\eb0c\"}.ki-tiktok.ki-solid:before{content:\"\\eb0d\"}.ki-time.ki-solid:before{content:\"\\eb0e\"}.ki-timer.ki-solid:before{content:\"\\eb0f\"}.ki-to-left.ki-solid:before{content:\"\\eb10\"}.ki-to-right.ki-solid:before{content:\"\\eb11\"}.ki-toggle-off-circle.ki-solid:before{content:\"\\eb12\"}.ki-toggle-off.ki-solid:before{content:\"\\eb13\"}.ki-toggle-on-circle.ki-solid:before{content:\"\\eb14\"}.ki-toggle-on.ki-solid:before{content:\"\\eb15\"}.ki-trailer.ki-solid:before{content:\"\\eb16\"}.ki-trash-square.ki-solid:before{content:\"\\eb17\"}.ki-trash.ki-solid:before{content:\"\\eb18\"}.ki-tree.ki-solid:before{content:\"\\eb19\"}.ki-trello.ki-solid:before{content:\"\\eb1a\"}.ki-triangle.ki-solid:before{content:\"\\eb1b\"}.ki-truck.ki-solid:before{content:\"\\eb1c\"}.ki-ts.ki-solid:before{content:\"\\eb1d\"}.ki-twitch.ki-solid:before{content:\"\\eb1e\"}.ki-twitter.ki-solid:before{content:\"\\eb1f\"}.ki-two-credit-cart.ki-solid:before{content:\"\\eb20\"}.ki-underlining.ki-solid:before{content:\"\\eb21\"}.ki-up-down.ki-solid:before{content:\"\\eb22\"}.ki-up-square.ki-solid:before{content:\"\\eb23\"}.ki-up.ki-solid:before{content:\"\\eb24\"}.ki-update-file.ki-solid:before{content:\"\\eb25\"}.ki-update-folder.ki-solid:before{content:\"\\eb26\"}.ki-user-edit.ki-solid:before{content:\"\\eb27\"}.ki-user-square.ki-solid:before{content:\"\\eb28\"}.ki-user-tick.ki-solid:before{content:\"\\eb29\"}.ki-user.ki-solid:before{content:\"\\eb2a\"}.ki-verify.ki-solid:before{content:\"\\eb2b\"}.ki-vibe.ki-solid:before{content:\"\\eb2c\"}.ki-virus.ki-solid:before{content:\"\\eb2d\"}.ki-vue.ki-solid:before{content:\"\\eb2e\"}.ki-vuesax.ki-solid:before{content:\"\\eb2f\"}.ki-wallet.ki-solid:before{content:\"\\eb30\"}.ki-wanchain.ki-solid:before{content:\"\\eb31\"}.ki-watch.ki-solid:before{content:\"\\eb32\"}.ki-whatsapp.ki-solid:before{content:\"\\eb33\"}.ki-wifi-home.ki-solid:before{content:\"\\eb34\"}.ki-wifi-square.ki-solid:before{content:\"\\eb35\"}.ki-wifi.ki-solid:before{content:\"\\eb36\"}.ki-wrench.ki-solid:before{content:\"\\eb37\"}.ki-xaomi.ki-solid:before{content:\"\\eb38\"}.ki-xd.ki-solid:before{content:\"\\eb39\"}.ki-xmr.ki-solid:before{content:\"\\eb3a\"}.ki-yii.ki-solid:before{content:\"\\eb3b\"}.ki-youtube.ki-solid:before{content:\"\\eb3c\"}:root,[data-bs-theme=light]{--bs-prismjs-bg:#1e1e3f;--bs-prismjs-border:rgba(255, 255, 255, 0.1);--bs-prismjs-btn-bg:#2d2d5e;--bs-prismjs-btn-bg-hover:#2d2d5e;--bs-prismjs-btn-color:rgba(255, 255, 255, 0.75);--bs-prismjs-btn-color-hover:#0069E0;--bs-prismjs-scrollbar-color:#323268;--bs-prismjs-scrollbar-color-hover:#373773}[data-bs-theme=dark]{--bs-prismjs-bg:#151521;--bs-prismjs-border:rgba(255, 255, 255, 0.1);--bs-prismjs-btn-bg:#27273d;--bs-prismjs-btn-bg-hover:#27273d;--bs-prismjs-btn-color:rgba(255, 255, 255, 0.75);--bs-prismjs-btn-color-hover:#0069E0;--bs-prismjs-scrollbar-color:#2d2d46;--bs-prismjs-scrollbar-color-hover:#333350}.highlight{position:relative;background:var(--bs-prismjs-bg);border-radius:.475rem;padding:1.75rem 1.5rem 1.75rem 1.5rem}.highlight .nav{border-bottom:1px solid var(--bs-prismjs-border);padding-bottom:1rem;margin-bottom:1rem;margin-top:-.25rem}.highlight .nav .nav-item{margin-right:.75rem}.highlight .nav .nav-link{font-size:.9rem;font-weight:500;padding:.35rem 1rem;border-radius:.475rem;color:var(--bs-prismjs-btn-color);transition:all .2s ease-in-out;background-color:transparent}.highlight .nav .nav-link.active,.highlight .nav .nav-link:focus{transition:all .2s ease-in-out;background-color:var(--bs-prismjs-btn-bg-hover);color:var(--bs-prismjs-btn-color-hover)}.highlight .highlight-copy{display:none;position:absolute;right:1.75rem;top:1.5rem;font-size:.85rem;font-weight:500;padding:.35rem 1rem!important;transition:all .2s ease-in-out;background-color:var(--bs-prismjs-btn-bg);color:var(--bs-prismjs-btn-color)}.highlight .highlight-copy:focus,.highlight .highlight-copy:hover{transition:all .2s ease-in-out;background-color:var(--bs-prismjs-btn-bg-hover);color:var(--bs-prismjs-btn-color-hover)}.highlight:hover .highlight-copy{display:flex}.highlight .highlight-code pre{background-color:transparent;overflow:auto;padding:0;margin:0;scrollbar-color:var(--bs-prismjs-scrollbar-color) transparent}.highlight .highlight-code pre::-webkit-scrollbar-thumb{background-color:var(--bs-prismjs-scrollbar-color)}.highlight .highlight-code pre::-webkit-scrollbar-corner{background-color:transparent}.highlight .highlight-code pre:hover{scrollbar-color:var(--bs-prismjs-scrollbar-color-hover) transparent}.highlight .highlight-code pre:hover::-webkit-scrollbar-thumb{background-color:var(--bs-prismjs-scrollbar-color-hover)}.highlight .highlight-code pre:hover::-webkit-scrollbar-corner{background-color:transparent}.highlight .highlight-code pre code[class*=language-]{padding:0;margin:0;font-size:1rem!important}.fslightbox-slide-btn{border-radius:.475rem}.fslightbox-toolbar{border-bottom-left-radius:.475rem}.select2-container--bootstrap5 .select2-selection{box-shadow:none;height:auto;outline:0!important}.select2-container--bootstrap5.select2-container--focus:not(.select2-container--disabled) .form-select-solid,.select2-container--bootstrap5.select2-container--open:not(.select2-container--disabled) .form-select-solid{background-color:var(--bs-gray-200)}.select2-container--bootstrap5.select2-container--focus:not(.select2-container--disabled) .form-select:not(.form-select-solid):not(.form-select-transparent),.select2-container--bootstrap5.select2-container--open:not(.select2-container--disabled) .form-select:not(.form-select-solid):not(.form-select-transparent){border-color:var(--bs-gray-400)}.select2-container--bootstrap5.select2-container--disabled .form-select{background-color:var(--bs-gray-200);border-color:var(--bs-gray-300)}.select2-container--bootstrap5.select2-container--disabled .form-select .select2-selection__placeholder,.select2-container--bootstrap5.select2-container--disabled .form-select .select2-selection__rendered{color:var(--bs-gray-500)!important}.select2-container--bootstrap5.select2-container--disabled .form-select.form-select-transparent{background-color:transparent;border-color:transparent}.select2-container--bootstrap5 .select2-search.select2-search--inline{flex-grow:1}.select2-container--bootstrap5 .select2-search.select2-search--inline .select2-search__field{color:var(--bs-gray-700);font-weight:500;font-family:inherit!important;background-color:transparent;border:0;box-shadow:none;outline:0;line-height:1;margin:0;padding:0}.select2-container--bootstrap5 .select2-search.select2-search--inline .select2-search__field::placeholder{color:var(--bs-gray-500)}.select2-container--bootstrap5 .select2-search.select2-search--inline .select2-search__field::-moz-placeholder{color:var(--bs-gray-500);opacity:1}.select2-container--bootstrap5 .form-select-solid .select2-search.select2-search--inline .select2-search__field{color:var(--bs-gray-700);font-family:inherit!important}.select2-container--bootstrap5 .form-select-solid .select2-search.select2-search--inline .select2-search__field::placeholder{color:var(--bs-gray-500)}.select2-container--bootstrap5 .form-select-solid .select2-search.select2-search--inline .select2-search__field::-moz-placeholder{color:var(--bs-gray-500);opacity:1}.select2-container--bootstrap5 .select2-selection--single{display:flex;align-items:center}.select2-container--bootstrap5 .select2-selection--single .select2-selection__rendered{display:block;padding-left:0;padding-right:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bs-input-color)}.select2-container--bootstrap5 .select2-selection--single .select2-selection__placeholder{color:var(--bs-gray-500)}.select2-container--bootstrap5 .select2-selection--single.form-select-solid .select2-selection__rendered{color:var(--bs-gray-700)}.select2-container--bootstrap5 .select2-selection--single.form-select-solid .select2-selection__placeholder{color:var(--bs-gray-500)}.select2-container--bootstrap5 .select2-selection--single.form-select-transparent .select2-selection__rendered{color:var(--bs-gray-800)}.select2-container--bootstrap5 .select2-selection--single.form-select-transparent .select2-selection__placeholder{color:var(--bs-gray-800)}.select2-container--bootstrap5 .select2-selection--single.form-select-dark .select2-selection__rendered{color:var(--bs-gray-900)}.select2-container--bootstrap5 .select2-selection--multiple{display:flex;align-items:center}.select2-container--bootstrap5 .select2-selection--multiple .select2-search.select2-search--inline{display:inline-flex}.select2-container--bootstrap5 .select2-selection--multiple .select2-selection__rendered{display:inline;margin:0;padding:0}.select2-container--bootstrap5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice{display:inline-flex;align-items:center;position:relative;background-color:var(--bs-gray-300)}.select2-container--bootstrap5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-700%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-700%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");display:block;position:absolute;transform:translateY(-50%);opacity:.5;border:0;transition:color .2s ease;top:50%}.select2-container--bootstrap5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove span{display:none}.select2-container--bootstrap5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover{opacity:1;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-primary%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-primary%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");transition:color .2s ease}.select2-container--bootstrap5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__display{font-weight:500}.select2-container--bootstrap5 .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove{height:.6rem;width:.6rem}.select2-container--bootstrap5 .select2-selection--multiple.form-select-sm{min-height:calc(1.5em + 1.1rem + 2px);padding-top:.35rem;padding-bottom:.35rem}.select2-container--bootstrap5 .select2-selection--multiple.form-select-sm .select2-selection__choice{border-radius:.425rem;padding:.1rem .35rem;margin-right:.35rem;margin-top:.1rem;margin-bottom:.1rem}.select2-container--bootstrap5 .select2-selection--multiple.form-select-sm .select2-selection__choice .select2-selection__choice__display{margin-left:.95rem;font-size:.95rem}.select2-container--bootstrap5 .select2-selection--multiple.form-select-sm .select2-search__field{height:14px}.select2-container--bootstrap5 .select2-selection--multiple:not(.form-select-sm):not(.form-select-lg){min-height:calc(1.5em + 1.55rem + 2px);padding-top:.575rem;padding-bottom:.575rem}.select2-container--bootstrap5 .select2-selection--multiple:not(.form-select-sm):not(.form-select-lg) .select2-selection__choice{border-radius:.475rem;padding:.1rem .5rem;margin-right:.5rem;margin-top:.1rem;margin-bottom:.1rem}.select2-container--bootstrap5 .select2-selection--multiple:not(.form-select-sm):not(.form-select-lg) .select2-selection__choice .select2-selection__choice__display{margin-left:1.1rem;font-size:1.1rem}.select2-container--bootstrap5 .select2-selection--multiple:not(.form-select-sm):not(.form-select-lg) .select2-search__field{height:16px}.select2-container--bootstrap5 .select2-selection--multiple.form-select-lg{min-height:calc(1.5em + 2rem + 2px);padding-top:.7rem;padding-bottom:.7rem}.select2-container--bootstrap5 .select2-selection--multiple.form-select-lg .select2-selection__choice{border-radius:.625rem;padding:.15rem .65rem;margin-right:.65rem;margin-top:.15rem;margin-bottom:.15rem}.select2-container--bootstrap5 .select2-selection--multiple.form-select-lg .select2-selection__choice .select2-selection__choice__display{margin-left:1.25rem;font-size:1.15rem}.select2-container--bootstrap5 .select2-selection--multiple.form-select-lg .select2-search__field{height:18px}.select2-container--bootstrap5 .select2-dropdown{border:0;box-shadow:var(--bs-dropdown-box-shadow);border-radius:.475rem;padding:1rem 0;background-color:var(--bs-dropdown-bg)}.modal-open .select2-container--bootstrap5 .select2-dropdown{z-index:1056}.select2-container--bootstrap5 .select2-dropdown .select2-search{padding:.5rem 1.25rem;margin:0 0 .5rem 0}.select2-container--bootstrap5 .select2-dropdown .select2-search .select2-search__field{background-color:var(--bs-body-bg);padding:.55rem .75rem;color:var(--bs-gray-700);font-size:.95rem;border:1px solid var(--bs-gray-300);border-radius:.425rem;outline:0!important}.select2-container--bootstrap5 .select2-dropdown .select2-search .select2-search__field:active,.select2-container--bootstrap5 .select2-dropdown .select2-search .select2-search__field:focus{border:1px solid var(--bs-gray-400)}.select2-container--bootstrap5 .select2-dropdown .select2-results>.select2-results__options{max-height:250px;overflow-y:auto}.select2-container--bootstrap5 .select2-dropdown .select2-results__option{color:var(--bs-gray-700);transition:color .2s ease;padding:.75rem 1.25rem;margin:0 0}.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--highlighted{background-color:var(--bs-component-hover-bg);color:var(--bs-component-hover-color);transition:color .2s ease}.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--selected{background-color:var(--bs-component-hover-bg);color:var(--bs-component-hover-color);transition:color .2s ease;position:relative}.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--selected:after{top:50%;display:block;position:absolute;transform:translateY(-50%);height:.75rem;width:.75rem;content:\"\";mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-component-hover-color);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 11'%3e%3cpath fill='var%28--bs-component-hover-color%29' d='M4.89557 6.49823L2.79487 4.26513C2.26967 3.70683 1.38251 3.70683 0.857309 4.26513C0.375593 4.77721 0.375593 5.57574 0.857309 6.08781L4.74989 10.2257C5.14476 10.6455 5.81176 10.6455 6.20663 10.2257L13.1427 2.85252C13.6244 2.34044 13.6244 1.54191 13.1427 1.02984C12.6175 0.471537 11.7303 0.471536 11.2051 1.02984L6.06096 6.49823C5.74506 6.83403 5.21146 6.83403 4.89557 6.49823Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 11'%3e%3cpath fill='var%28--bs-component-hover-color%29' d='M4.89557 6.49823L2.79487 4.26513C2.26967 3.70683 1.38251 3.70683 0.857309 4.26513C0.375593 4.77721 0.375593 5.57574 0.857309 6.08781L4.74989 10.2257C5.14476 10.6455 5.81176 10.6455 6.20663 10.2257L13.1427 2.85252C13.6244 2.34044 13.6244 1.54191 13.1427 1.02984C12.6175 0.471537 11.7303 0.471536 11.2051 1.02984L6.06096 6.49823C5.74506 6.83403 5.21146 6.83403 4.89557 6.49823Z'/%3e%3c/svg%3e\");mask-position:center;-webkit-mask-position:center;right:1.25rem}.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--disabled{color:var(--bs-gray-400)}.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__message{color:var(--bs-gray-600)}.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--group{padding-left:0;padding-right:0}.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--group .select2-results__group{display:block;color:var(--bs-gray-800);font-weight:500;font-size:1.15rem;padding:0 1.25rem 0 1.25rem;margin:0 0 .25rem 0}.select2-container--bootstrap5 .select2-dropdown .select2-results__option.select2-results__option--group .select2-results__option{padding:.75rem 1.25rem;margin:0 0}.select2-container--bootstrap5 .select2-selection__clear{display:block;height:.7rem;width:.7rem;top:50%;right:3rem;position:absolute;transform:translateY(-50%);background-color:var(--bs-gray-700)!important;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-700);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-700%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-700%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.select2-container--bootstrap5 .select2-selection__clear span{display:none}.select2-container--bootstrap5 .select2-selection__clear:hover{background-color:var(--bs-primary)!important;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-primary%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-primary%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.form-floating .form-select{padding-top:1.85rem!important}.fv-plugins-message-container{--input-invalid-color:var(--bs-danger);margin-top:.3rem}.fv-plugins-message-container .fv-help-block{color:var(--bs-danger);font-size:1rem;font-weight:400}.fv-plugins-message-container.invalid-feedback,.fv-plugins-message-container.valid-feedback{display:block;font-weight:400}.daterangepicker{padding:0;margin:0;border:0;width:auto;background-color:var(--bs-body-bg);box-shadow:var(--bs-dropdown-box-shadow);font-family:Inter,Helvetica,sans-serif;z-index:1000;border-radius:.475rem}.daterangepicker:after,.daterangepicker:before{display:none}.daterangepicker td.off,.daterangepicker td.off.end-date,.daterangepicker td.off.in-range,.daterangepicker td.off.start-date{background-color:transparent}.modal-open .daterangepicker{z-index:1056}.daterangepicker .calendar-table{background-color:var(--bs-body-bg);border:0}.daterangepicker .ranges{border-radius:.475rem;background-color:var(--bs-body-bg);position:relative;overflow:hidden}.daterangepicker .ranges ul{padding:1rem 0;width:150px;overflow:auto;max-height:260px}.daterangepicker .ranges li{padding:.7rem 1.75rem;font-weight:500;font-size:1rem;color:var(--bs-gray-600);transition:color .2s ease}.daterangepicker .ranges li:hover{background-color:var(--bs-component-hover-bg);color:var(--bs-component-hover-color);transition:color .2s ease}.daterangepicker .ranges li.active{background-color:var(--bs-component-active-bg);color:var(--bs-component-active-color);transition:color .2s ease}.daterangepicker.show-calendar .ranges{border-radius:0;border-top-left-radius:.475rem;margin-top:0;height:297px}.daterangepicker.show-ranges.show-calendar .ranges{border-right:1px solid var(--bs-gray-200)}.daterangepicker.show-ranges .drp-calendar.left{border-left:0}.daterangepicker .drp-buttons{padding:1rem 1.75rem;border-top:1px solid var(--bs-gray-200)}.daterangepicker .drp-buttons .btn{font-size:1rem;font-weight:500;padding:.5rem 1rem}.daterangepicker .drp-buttons .cancelBtn{color:var(--bs-light-inverse);border-color:var(--bs-light);background-color:var(--bs-light)}.daterangepicker .drp-buttons .cancelBtn .svg-icon,.daterangepicker .drp-buttons .cancelBtn i{color:var(--bs-light-inverse)}.daterangepicker .drp-buttons .cancelBtn.dropdown-toggle:after{color:var(--bs-light-inverse)}.btn-check:active+.daterangepicker .drp-buttons .cancelBtn,.btn-check:checked+.daterangepicker .drp-buttons .cancelBtn,.daterangepicker .drp-buttons .cancelBtn.active,.daterangepicker .drp-buttons .cancelBtn.show,.daterangepicker .drp-buttons .cancelBtn:active:not(.btn-active),.daterangepicker .drp-buttons .cancelBtn:focus:not(.btn-active),.daterangepicker .drp-buttons .cancelBtn:hover:not(.btn-active),.show>.daterangepicker .drp-buttons .cancelBtn{color:var(--bs-light-inverse);border-color:var(--bs-light-active);background-color:var(--bs-light-active)!important}.btn-check:active+.daterangepicker .drp-buttons .cancelBtn .svg-icon,.btn-check:active+.daterangepicker .drp-buttons .cancelBtn i,.btn-check:checked+.daterangepicker .drp-buttons .cancelBtn .svg-icon,.btn-check:checked+.daterangepicker .drp-buttons .cancelBtn i,.daterangepicker .drp-buttons .cancelBtn.active .svg-icon,.daterangepicker .drp-buttons .cancelBtn.active i,.daterangepicker .drp-buttons .cancelBtn.show .svg-icon,.daterangepicker .drp-buttons .cancelBtn.show i,.daterangepicker .drp-buttons .cancelBtn:active:not(.btn-active) .svg-icon,.daterangepicker .drp-buttons .cancelBtn:active:not(.btn-active) i,.daterangepicker .drp-buttons .cancelBtn:focus:not(.btn-active) .svg-icon,.daterangepicker .drp-buttons .cancelBtn:focus:not(.btn-active) i,.daterangepicker .drp-buttons .cancelBtn:hover:not(.btn-active) .svg-icon,.daterangepicker .drp-buttons .cancelBtn:hover:not(.btn-active) i,.show>.daterangepicker .drp-buttons .cancelBtn .svg-icon,.show>.daterangepicker .drp-buttons .cancelBtn i{color:var(--bs-light-inverse)}.btn-check:active+.daterangepicker .drp-buttons .cancelBtn.dropdown-toggle:after,.btn-check:checked+.daterangepicker .drp-buttons .cancelBtn.dropdown-toggle:after,.daterangepicker .drp-buttons .cancelBtn.active.dropdown-toggle:after,.daterangepicker .drp-buttons .cancelBtn.show.dropdown-toggle:after,.daterangepicker .drp-buttons .cancelBtn:active:not(.btn-active).dropdown-toggle:after,.daterangepicker .drp-buttons .cancelBtn:focus:not(.btn-active).dropdown-toggle:after,.daterangepicker .drp-buttons .cancelBtn:hover:not(.btn-active).dropdown-toggle:after,.show>.daterangepicker .drp-buttons .cancelBtn.dropdown-toggle:after{color:var(--bs-light-inverse)}.daterangepicker .drp-selected{font-size:.9rem}.daterangepicker .drp-calendar.left,.daterangepicker .drp-calendar.right{padding:1rem 1rem}.daterangepicker .drp-calendar.left{border-left:0!important}.daterangepicker .drp-calendar td,.daterangepicker .drp-calendar th{font-size:1rem;font-weight:400;width:33px;height:33px}.daterangepicker .drp-calendar td.available:hover,.daterangepicker .drp-calendar th.available:hover{border-radius:.475rem;background-color:var(--bs-component-hover-bg);color:var(--bs-component-hover-color)}.daterangepicker .drp-calendar th{font-weight:500;color:var(--bs-gray-800)}.daterangepicker .drp-calendar th.month{font-weight:500;color:var(--bs-gray-800)}.daterangepicker .drp-calendar th.next span,.daterangepicker .drp-calendar th.prev span{border-width:0 1px 1px 0;border-color:var(--bs-gray-600)}.daterangepicker .drp-calendar th.next.available:hover span,.daterangepicker .drp-calendar th.prev.available:hover span{border-color:var(--bs-component-hover-color)}.daterangepicker .drp-calendar th.next span{margin-right:1px}.daterangepicker .drp-calendar th.prev span{margin-left:1px}.daterangepicker .drp-calendar td{color:var(--bs-gray-700)}.daterangepicker .drp-calendar td.available.off{color:var(--bs-gray-500)}.daterangepicker .drp-calendar td.active{background-color:var(--bs-component-active-bg)!important;color:var(--bs-component-active-color)!important;border-radius:.475rem}.daterangepicker .drp-calendar td.active.start-date{border-top-right-radius:0;border-bottom-right-radius:0}.daterangepicker .drp-calendar td.active.end-date{border-top-left-radius:0;border-bottom-left-radius:0}.daterangepicker .drp-calendar td.active.start-date.end-date{border-radius:.475rem}.daterangepicker .drp-calendar td.today,.daterangepicker .drp-calendar td.today.active{background:var(--bs-gray-200)!important;color:var(--bs-gray-700)!important;border-radius:.475rem}.daterangepicker .drp-calendar td.in-range.available:not(.active):not(.off):not(.today){background-color:var(--bs-component-hover-bg);color:var(--bs-component-hover-color)}.daterangepicker .drp-calendar td:hover{background-color:var(--bs-component-hover-bg);color:var(--bs-component-hover-color)}.daterangepicker select.ampmselect,.daterangepicker select.hourselect,.daterangepicker select.minuteselect,.daterangepicker select.monthselect,.daterangepicker select.yearselect{padding-top:.35rem;padding-bottom:.35rem;border-radius:.475rem;background-color:var(--bs-body-bg)!important;border-color:transparent;color:var(--bs-input-color);font-weight:500;outline:0!important}.daterangepicker select.ampmselect:focus,.daterangepicker select.hourselect:focus,.daterangepicker select.minuteselect:focus,.daterangepicker select.monthselect:focus,.daterangepicker select.yearselect:focus{background-color:var(--bs-gray-100)}@media (max-width:767.98px){.daterangepicker.show-calendar .ranges{float:none!important;height:auto!important}.daterangepicker.show-calendar .ranges ul{width:100%}.daterangepicker.show-calendar .drp-calendar{float:none!important;max-width:unset!important;display:flex;flex-direction:column;align-items:center}}.flatpickr-calendar{width:280px!important;font-family:inherit;border:0;border-radius:0;box-shadow:var(--bs-dropdown-box-shadow);background-color:var(--bs-body-bg);border-radius:.475rem}.flatpickr-calendar:after,.flatpickr-calendar:before{display:none}.flatpickr-calendar.hasWeeks{width:325px!important}.flatpickr-months{padding:0 1rem;padding-top:.5rem}.flatpickr-innerContainer{padding:.5rem 1rem}.dayContainer,.flatpickr-days{width:100%!important;min-width:100%!important;max-width:100%!important}.flatpickr-months .flatpickr-month{background:0 0;color:var(--bs-gray-600);fill:var(--bs-gray-600);height:46px}.flatpickr-months .flatpickr-next-month,.flatpickr-months .flatpickr-prev-month{display:flex;align-items:center;justify-content:center;border-radius:.475rem;top:1rem}.flatpickr-months .flatpickr-next-month svg,.flatpickr-months .flatpickr-prev-month svg{fill:var(--bs-gray-500);height:13px;width:13px}.flatpickr-months .flatpickr-next-month:hover,.flatpickr-months .flatpickr-prev-month:hover{background:var(--bs-gray-100)}.flatpickr-months .flatpickr-next-month:hover svg,.flatpickr-months .flatpickr-prev-month:hover svg{fill:var(--bs-gray-700)}.flatpickr-months .flatpickr-next-month.flatpickr-prev-month,.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month{left:1rem}.flatpickr-months .flatpickr-next-month.flatpickr-next-month,.flatpickr-months .flatpickr-prev-month.flatpickr-next-month{right:1rem}.flatpickr-current-month{font-weight:500;color:inherit}.flatpickr-current-month .numInputWrapper{border-radius:.475rem;width:65px}.flatpickr-current-month .numInputWrapper span.arrowUp{border-top-right-radius:.475rem}.flatpickr-current-month .numInputWrapper span.arrowDown{border-bottom-right-radius:.475rem}.flatpickr-current-month .flatpickr-monthDropdown-months{border:0!important;background-color:var(--bs-body-bg);font-size:1rem;color:var(--bs-gray-700);font-weight:500;padding:.5rem .75rem;margin-right:.5rem;outline:0!important;border-radius:.475rem;appearance:none;-moz-appearance:none;-webkit-appearance:none}.flatpickr-current-month .flatpickr-monthDropdown-months:hover{background:var(--bs-gray-100)}.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{font-size:1rem;color:var(--bs-gray-700);font-weight:500;background-color:transparent;outline:0;padding:0}.flatpickr-current-month span.cur-month{color:var(--bs-gray-700);font-size:1rem;font-weight:500}.flatpickr-current-month span.cur-month:hover{background:var(--bs-gray-100)}.flatpickr-current-month input.cur-year{color:var(--bs-gray-700);font-size:1.1rem!important;padding:.5rem .75rem;font-weight:500;outline:0!important}span.flatpickr-weekday{color:var(--bs-gray-800);font-size:1rem;font-weight:600}.flatpickr-time{border-bottom-right-radius:.475rem;border-bottom-left-radius:.475rem}.flatpickr-calendar.hasTime .flatpickr-time{height:height;line-height:height;max-height:height;border-top:1px solid var(--bs-gray-100)}.flatpickr-time .numInputWrapper{height:height}.flatpickr-time .flatpickr-am-pm{color:var(--bs-gray-700);font-size:1rem;font-weight:500}.flatpickr-time input.flatpickr-hour,.flatpickr-time input.flatpickr-minute{color:var(--bs-gray-700);font-size:1rem;font-weight:500}.flatpickr-time .flatpickr-am-pm:focus,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time input:hover{background:0 0}.numInputWrapper span{border-left:0!important;border-top:0!important;border-bottom:0!important;border-right:0!important}.numInputWrapper span:hover{background:0 0!important}.numInputWrapper span:after{top:50%!important;transform:translateY(-50%)}.numInputWrapper span.arrowUp:after{border-bottom-color:var(--bs-gray-500)!important}.numInputWrapper span.arrowUp:hover:after{border-bottom-color:var(--bs-gray-700)!important}.numInputWrapper span.arrowDown:after{border-top-color:var(--bs-gray-500)!important}.numInputWrapper span.arrowDown:hover:after{border-top-color:var(--bs-gray-700)!important}.numInputWrapper:hover{background:0 0}.flatpickr-day{font-size:1rem;border-radius:.475rem;box-shadow:none!important;height:36px;width:100%;max-width:100%!important;margin:0;line-height:36px;color:var(--bs-gray-600);margin-top:0!important}.flatpickr-day.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day.nextMonthDay:focus,.flatpickr-day.nextMonthDay:hover,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.today.inRange,.flatpickr-day:focus,.flatpickr-day:hover{cursor:pointer;outline:0;background:var(--bs-component-hover-bg);color:var(--bs-component-hover-color);border-color:transparent}.flatpickr-day.today{background:var(--bs-gray-100);color:var(--bs-gray-600);border-color:transparent}.flatpickr-day.today:focus,.flatpickr-day.today:hover{border-color:transparent;background:var(--bs-gray-200);color:var(--bs-gray-700)}.flatpickr-day.endRange,.flatpickr-day.endRange.inRange,.flatpickr-day.endRange.nextMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.endRange:focus,.flatpickr-day.endRange:hover,.flatpickr-day.selected,.flatpickr-day.selected.inRange,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.selected:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange,.flatpickr-day.startRange.inRange,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.startRange:focus,.flatpickr-day.startRange:hover{background:var(--bs-component-active-bg);color:var(--bs-component-active-color);border-color:transparent}.flatpickr-day.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day.nextMonthDay:focus,.flatpickr-day.nextMonthDay:hover,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.today.inRange,.flatpickr-day:focus,.flatpickr-day:hover{cursor:pointer;outline:0;background:var(--bs-component-hover-bg);color:var(--bs-component-hover-color);border-color:transparent}.flatpickr-day.today{border-color:transparent}.flatpickr-day.today:focus,.flatpickr-day.today:hover{border-color:transparent;background:var(--bs-gray-100);color:var(--bs-gray-600)}.flatpickr-day.endRange,.flatpickr-day.endRange.inRange,.flatpickr-day.endRange.nextMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.endRange:focus,.flatpickr-day.endRange:hover,.flatpickr-day.selected,.flatpickr-day.selected.inRange,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.selected:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange,.flatpickr-day.startRange.inRange,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.startRange:focus,.flatpickr-day.startRange:hover{background:var(--bs-component-active-bg);color:var(--bs-component-active-color);border-color:transparent}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.nextMonthDay,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.prevMonthDay{color:var(--bs-gray-500);background:0 0;border-color:transparent}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{cursor:not-allowed;color:var(--bs-gray-500)}.flatpickr-weekwrapper{margin-right:5px}.tagify{--tagify-dd-bg-color:var(--bs-body-bg);--tags-border-color:var(--bs-gray-300);--tags-hover-border-color:var(--bs-gray-300);--tags-focus-border-color:var(--bs-gray-400);--tag-bg:var(--bs-gray-200);--tag-hover:var(--bs-gray-200);--tag-text-color:var(--bs-gray-700);--tag-text-color--edit:var(--bs-gray-700);--tag-pad:0 0.5rem;--tag-inset-shadow-size:1rem;--tag-invalid-color:var(--bs-danger);--tag-invalid-bg:var(--bs-danger-light);--tag-remove-bg:var(--bs-gray-200);--tag-remove-btn-color:transparent;--tag-remove-btn-bg:transparent;--tag-remove-btn-bg--hover:transparent;--input-color:var(--bs-gray-700);--placeholder-color:var(--bs-gray-400);--placeholder-color-focus:var(--bs-gray-500);--loader-size:.8rem;--tagify-dd-item--hidden-duration:0.3s}.tagify .tagify__tag{background-color:var(--tag-bg);margin:0;line-height:1}.tagify .tagify__tag div{border-radius:inherit}.tagify .tagify__tag .tagify__tag-text{overflow:visible}.tagify .tagify__tag .tagify__tag__removeBtn{width:.6rem;height:.6rem;margin:0 .5rem 0 0;border-radius:0;content:\" \";mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-500);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-500%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-500%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.tagify .tagify__tag .tagify__tag__removeBtn:after{display:none}.tagify .tagify__tag .tagify__tag__removeBtn:hover{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-primary%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-primary%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.tagify .tagify__tag.tagify--notAllowed div .tagify__tag-text{color:var(--bs-danger);opacity:.5}.tagify .tagify__tag.tagify--notAllowed .tagify__tag__removeBtn{opacity:.5;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-danger);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-danger%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-danger%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.tagify .tagify__tag.tagify--notAllowed .tagify__tag__removeBtn:hover{background:0 0;opacity:.75}.tagify .tagify__input{margin:0}.tagify .tagify__input:before{font-size:inherit;line-height:inherit;font-weight:inherit}.tagify.form-control{display:flex;align-items:center;align-content:normal;gap:.25rem}.tagify.form-control-sm{border-radius:.425rem;min-height:calc(1.5em + 1.1rem + 2px);padding-top:.25rem;padding-bottom:.25rem}.tagify.form-control-sm .tagify__tag{padding:.3rem .3rem;border-radius:.425rem}.tagify.form-control-sm .tagify__tag .tagify__tag-text{font-size:.95rem}.tagify:not(.form-control-sm):not(.form-control-lg){border-radius:.475rem;min-height:calc(1.5em + 1.55rem + 2px);padding-top:.375rem;padding-bottom:.375rem}.tagify:not(.form-control-sm):not(.form-control-lg) .tagify__tag{padding:.4rem .4rem;border-radius:.475rem}.tagify:not(.form-control-sm):not(.form-control-lg) .tagify__tag .tagify__tag-text{font-size:1.1rem}.tagify.form-control-lg{border-radius:.625rem;min-height:calc(1.5em + 2rem + 2px);padding-top:.5rem;padding-bottom:.5rem}.tagify.form-control-lg .tagify__tag{padding:.5rem .5rem;border-radius:.625rem}.tagify.form-control-lg .tagify__tag .tagify__tag-text{font-size:1.15rem}.tagify__dropdown{box-shadow:var(--bs-dropdown-box-shadow);border:0!important;outline:0!important;padding:.75rem 0;z-index:1000;background-color:var(--bs-body-bg);border-radius:.475rem}.tagify__dropdown ._wrapper{max-height:none;border-radius:.475rem}.modal-open .tagify__dropdown{z-index:1056}.tagify__dropdown .tagify__dropdown__wrapper{background-color:var(--bs-body-bg);border:0!important;outline:0!important;box-shadow:none}.tagify__dropdown .tagify__dropdown__item{color:var(--bs-gray-700);border-radius:0;padding:.75rem 1.5rem;margin:0;box-shadow:none;font-weight:500}.tagify__dropdown .tagify__dropdown__item.tagify__dropdown__item--active,.tagify__dropdown .tagify__dropdown__item:hover{background-color:var(--bs-component-hover-bg);color:var(--bs-component-hover-color)}.tagify__dropdown.tagify__inline__suggestions{padding:.775rem 1rem}.tagify__dropdown.tagify__inline__suggestions .tagify__dropdown__item{display:inline-block;font-size:.95rem;padding:.35rem .5rem;margin:.25rem .5rem .25rem 0;background-color:var(--bs-gray-200);color:var(--bs-gray-700);border-radius:.475rem}.tagify__dropdown.tagify__inline__suggestions .tagify__dropdown__item.tagify__dropdown__item--active,.tagify__dropdown.tagify__inline__suggestions .tagify__dropdown__item:hover{background-color:var(--bs-component-hover-bg);color:var(--bs-component-hover-color)}.bootstrap-maxlength{z-index:1040!important}.modal-open .bootstrap-maxlength{z-index:1060!important}.bootstrap-maxlength.badge{display:inline-flex!important}.ck-target{display:none}.ck-toolbar{border-radius:.475rem!important}.ck-content{min-height:200px;border-radius:.475rem!important}.ck-content.ck-focused{border-color:var(--bs-primary)!important;box-shadow:none!important}.ck-editor .ck-toolbar{border-top-left-radius:.475rem!important;border-top-right-radius:.475rem!important;border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.ck-editor .ck-content{border-bottom-right-radius:.475rem!important;border-bottom-left-radius:.475rem!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.ck-body .ck-balloon-panel .ck-content{min-height:200px;border-color:transparent!important}.ck-body .ck-balloon-panel .ck-content.ck-focused{border-color:var(--bs-primary)!important}.ck-body .ck-balloon-panel .ck-toolbar,.ck-body .ck-balloon-panel.ck-toolbar-container{border-radius:.475rem!important}table.dataTable{width:100%!important;margin:0!important}table.dataTable th{border-bottom-color:var(--bs-table-border-color)}table.dataTable .dt-column-header{display:inline-flex!important}table.dataTable td.dt-type-date,table.dataTable td.dt-type-numeric,table.dataTable th.dt-type-date,table.dataTable th.dt-type-numeric{text-align:left}table.dataTable>thead>tr>td:not(.sorting_disabled),table.dataTable>thead>tr>th:not(.sorting_disabled){padding-right:0}table.dataTable>thead .dt-column-order{display:none}table.dataTable>thead .dt-orderable-asc,table.dataTable>thead .dt-orderable-desc{outline:0!important}table.dataTable>thead .dt-orderable-asc:after,table.dataTable>thead .dt-orderable-asc:before,table.dataTable>thead .dt-orderable-desc:after,table.dataTable>thead .dt-orderable-desc:before{display:none!important}table.dataTable>thead .dt-ordering-asc,table.dataTable>thead .dt-ordering-desc{vertical-align:middle}table.dataTable>thead .dt-ordering-asc:after,table.dataTable>thead .dt-ordering-asc:before,table.dataTable>thead .dt-ordering-desc:after,table.dataTable>thead .dt-ordering-desc:before{position:relative!important;opacity:1!important;display:inline-block!important;width:.65rem;height:.65rem;content:\" \"!important;bottom:auto;right:auto!important;left:auto;margin-left:.5rem}table.dataTable>thead .dt-ordering-asc:before,table.dataTable>thead .dt-ordering-desc:before{display:none!important}table.dataTable>thead .dt-ordering-asc:after{opacity:1;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M3.23571 2.72011L4.97917 4.46358C5.15176 4.63618 5.43158 4.63617 5.60417 4.46358C5.77676 4.29099 5.77676 4.01118 5.60417 3.83861L3.29463 1.52904C3.13192 1.36629 2.86809 1.36629 2.70538 1.52904L0.395812 3.83861C0.22325 4.01117 0.22325 4.29099 0.395812 4.46358C0.568437 4.63617 0.84825 4.63617 1.02081 4.46358L2.76429 2.72011C2.89446 2.58994 3.10554 2.58994 3.23571 2.72011Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M3.23571 2.72011L4.97917 4.46358C5.15176 4.63618 5.43158 4.63617 5.60417 4.46358C5.77676 4.29099 5.77676 4.01118 5.60417 3.83861L3.29463 1.52904C3.13192 1.36629 2.86809 1.36629 2.70538 1.52904L0.395812 3.83861C0.22325 4.01117 0.22325 4.29099 0.395812 4.46358C0.568437 4.63617 0.84825 4.63617 1.02081 4.46358L2.76429 2.72011C2.89446 2.58994 3.10554 2.58994 3.23571 2.72011Z'/%3e%3c/svg%3e\")}table.dataTable>thead .dt-ordering-desc:after{opacity:1;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-text-muted);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M2.76429 3.27989L1.02083 1.53642C0.848244 1.36382 0.568419 1.36383 0.395831 1.53642C0.223244 1.70901 0.223244 1.98882 0.395831 2.16139L2.70537 4.47096C2.86808 4.63371 3.13191 4.63371 3.29462 4.47096L5.60419 2.16139C5.77675 1.98883 5.77675 1.70901 5.60419 1.53642C5.43156 1.36383 5.15175 1.36383 4.97919 1.53642L3.23571 3.27989C3.10554 3.41006 2.89446 3.41006 2.76429 3.27989Z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6' fill='var%28--bs-text-muted%29'%3e%3cpath d='M2.76429 3.27989L1.02083 1.53642C0.848244 1.36382 0.568419 1.36383 0.395831 1.53642C0.223244 1.70901 0.223244 1.98882 0.395831 2.16139L2.70537 4.47096C2.86808 4.63371 3.13191 4.63371 3.29462 4.47096L5.60419 2.16139C5.77675 1.98883 5.77675 1.70901 5.60419 1.53642C5.43156 1.36383 5.15175 1.36383 4.97919 1.53642L3.23571 3.27989C3.10554 3.41006 2.89446 3.41006 2.76429 3.27989Z'/%3e%3c/svg%3e\")}.dt-container .table-responsive{position:relative}.dt-container .dt-processing{border-radius:.475rem;box-shadow:var(--bs-dropdown-box-shadow);background-color:var(--bs-body-bg);color:var(--bs-gray-700);font-weight:500;margin:0!important;width:auto;padding:1rem 2rem!important;transform:translateX(-50%) translateY(-50%)}.dt-container .dt-processing>div{display:none}.dataTable.collapsed .dtr-control:before{border:0!important;height:1.35rem;width:1.35rem;line-height:1.5;text-indent:-999px!important;margin-bottom:-.3375rem;margin-right:.675rem;display:inline-block;position:relative;font-size:1.05rem;border:0;box-shadow:none;mask-size:85%;-webkit-mask-size:85%;content:\" \"}:root .dataTable.collapsed .dtr-control:before,[data-bs-theme=light] .dataTable.collapsed .dtr-control:before{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#78829d;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3e%3crect opacity='0.3' x='2' y='2' width='20' height='20' rx='5' fill='%2378829D'/%3e%3crect x='10.8891' y='17.8033' width='12' height='2' rx='1' transform='rotate%28-90 10.8891 17.8033%29' fill='%2378829D'/%3e%3crect x='6.01041' y='10.9247' width='12' height='2' rx='1' fill='%2378829D'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3e%3crect opacity='0.3' x='2' y='2' width='20' height='20' rx='5' fill='%2378829D'/%3e%3crect x='10.8891' y='17.8033' width='12' height='2' rx='1' transform='rotate%28-90 10.8891 17.8033%29' fill='%2378829D'/%3e%3crect x='6.01041' y='10.9247' width='12' height='2' rx='1' fill='%2378829D'/%3e%3c/svg%3e\")}[data-bs-theme=dark] .dataTable.collapsed .dtr-control:before{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#808290;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3e%3crect opacity='0.3' x='2' y='2' width='20' height='20' rx='5' fill='%23808290'/%3e%3crect x='10.8891' y='17.8033' width='12' height='2' rx='1' transform='rotate%28-90 10.8891 17.8033%29' fill='%23808290'/%3e%3crect x='6.01041' y='10.9247' width='12' height='2' rx='1' fill='%23808290'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3e%3crect opacity='0.3' x='2' y='2' width='20' height='20' rx='5' fill='%23808290'/%3e%3crect x='10.8891' y='17.8033' width='12' height='2' rx='1' transform='rotate%28-90 10.8891 17.8033%29' fill='%23808290'/%3e%3crect x='6.01041' y='10.9247' width='12' height='2' rx='1' fill='%23808290'/%3e%3c/svg%3e\")}.dt-hasChild.dtr-expanded .dtr-control:before{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:#0069e0;-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3e%3crect opacity='0.3' x='2' y='2' width='20' height='20' rx='5' fill='%230069E0'/%3e%3crect x='6.0104' y='10.9247' width='12' height='2' rx='1' fill='%230069E0'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3e%3crect opacity='0.3' x='2' y='2' width='20' height='20' rx='5' fill='%230069E0'/%3e%3crect x='6.0104' y='10.9247' width='12' height='2' rx='1' fill='%230069E0'/%3e%3c/svg%3e\")}thead .dtr-control:before{display:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:100px;font-weight:500}table.dataTable>tbody>tr.child span.dtr-data{font-weight:400}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:inset 0 0 0 9999px var(--bs-table-striped-bg)}table.table.dataTable>tbody>tr.selected>*{box-shadow:inset 0 0 0 9999px var(--bs-primary);color:var(--bs-primary-inverse)}table.table.dataTable>tbody>tr.selected>* a:not([class=btn]){color:var(--bs-primary-light);font-weight:500;text-decoration:underline;text-decoration-style:dotted}table.table.dataTable>tbody>tr.selected>* a:not([class=btn]):hover{color:var(--bs-primary-inverse)}.dt-search>div,.dt-toolbar>div{display:flex;align-items:center}.dt-toolbar .dt-info{margin-left:.75rem;padding-top:0!important;font-size:1rem;font-weight:500;color:var(--bs-text-gray-700)}.dt-paging{padding:1rem 0;margin-left:.5rem}.dt-paging .pagination{margin:0}.dt-scroll-body{border-left:0!important;border-bottom:0!important}.dt-scroll-body .dt-orderable-none.dt-ordering-asc:after,.dt-scroll-body .dt-orderable-none.dt-ordering-desc:after{display:none!important}.dt-scroll-foot{border-top:1px solid var(--bs-border-color)}.dt-scroll>.dt-scroll-body>.table>thead{line-height:0}.dt-scroll>.dt-scroll-body>.table>thead .dt-orderable-asc:after,.dt-scroll>.dt-scroll-body>.table>thead .dt-orderable-asc:before,.dt-scroll>.dt-scroll-body>.table>thead .dt-orderable-desc:after,.dt-scroll>.dt-scroll-body>.table>thead .dt-orderable-desc:before{display:none!important}div.dtfc-left-top-blocker,div.dtfc-right-top-blocker{background-color:var(--bs-body-bg)}table.dataTable thead tr>.dtfc-fixed-left,table.dataTable thead tr>.dtfc-fixed-right{background-color:var(--bs-body-bg)}table.dataTable thead tr>.dtfc-fixed-left:after,table.dataTable thead tr>.dtfc-fixed-right:after{box-shadow:none!important}table.dataTable tbody tr>.dtfc-fixed-left,table.dataTable tbody tr>.dtfc-fixed-right{background-color:var(--bs-body-bg)}table.dataTable tbody tr>.dtfc-fixed-left:after,table.dataTable tbody tr>.dtfc-fixed-right:after{box-shadow:none!important}.dtfh-floatingparent{box-shadow:var(--bs-box-shadow-sm)}.dtfh-floatingparent .table{background-color:var(--bs-body-bg)!important}.dtfh-floatingparent,.dtfh-floatingparent .table,.dtfh-floatingparent .table th{border-top-left-radius:0!important;border-top-right-radius:0!important}.dtr-details{display:table!important}.dtr-details li{display:table-row!important}.dtr-details li .dtr-title{padding-right:.75rem;color:var(--bs-gray-900)}.dtr-details li .dtr-data{color:var(--bs-gray-700)}.dtr-details li .dtr-data,.dtr-details li .dtr-title{font-size:1rem;padding-top:.25rem;padding-bottom:.25rem;display:table-cell!important;border-bottom:1px solid var(--bs-border-color)}.dropzone{min-height:auto;padding:1.5rem 1.75rem;text-align:center;cursor:pointer;border:1px dashed var(--bs-primary);background-color:var(--bs-primary-light);border-radius:.475rem!important}.dropzone .dz-message{margin:0;display:flex;text-align:left}.dropzone .dz-preview{border-radius:.475rem!important;margin:.75rem}.dropzone .dz-preview .dz-image{border-radius:.475rem!important;z-index:1}.dropzone .dz-preview.dz-file-preview .dz-image{background:var(--bs-gray-200)}.dropzone .dz-error-mark,.dropzone .dz-success-mark{margin-left:-20px!important;margin-top:-20px!important}.dropzone .dz-error-mark svg,.dropzone .dz-success-mark svg{height:40px!important;width:40px!important}.dropzone .dz-remove{display:flex;justify-content:center;align-items:center;height:1.65rem;width:1.65rem;font-size:1rem;text-indent:-9999px;white-space:nowrap;position:absolute;z-index:2;background-color:var(--bs-body-bg)!important;box-shadow:var(--bs-box-shadow);border-radius:100%;top:-.825rem;right:-.825rem}.dropzone .dz-remove:after{position:absolute;top:0;left:0;bottom:0;right:0;display:block;content:\"\";mask-size:40%;-webkit-mask-size:40%;mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-gray-600);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-600%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-gray-600%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.dropzone .dz-remove:hover:after{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-primary);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-primary%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-primary%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.dropzone .dz-error-message{color:var(--bs-danger-inverse);background:var(--bs-danger)}.dropzone.dropzone-queue{border:0;padding:0;background-color:transparent;text-align:left}.dropzone.dropzone-queue .dz-message{display:none}.dropzone.dropzone-queue .dropzone-panel .dropzone-remove-all,.dropzone.dropzone-queue .dropzone-panel .dropzone-upload{display:none}.dropzone.dropzone-queue .dropzone-item{display:flex;align-items:center;margin-top:.75rem;border-radius:.475rem;padding:.5rem 1rem;background-color:var(--bs-gray-100)}.dropzone.dropzone-queue .dropzone-item .dropzone-file{flex-grow:1}.dropzone.dropzone-queue .dropzone-item .dropzone-file .dropzone-filename{font-size:.9rem;font-weight:500;color:var(--bs-gray-600);text-overflow:ellipsis;margin-right:.5rem}.dropzone.dropzone-queue .dropzone-item .dropzone-file .dropzone-filename:hover{color:var(--bs-primary)}.dropzone.dropzone-queue .dropzone-item .dropzone-file .dropzone-error{margin-top:.25rem;font-size:.9rem;font-weight:400;color:var(--bs-danger);text-overflow:ellipsis}.dropzone.dropzone-queue .dropzone-item .dropzone-progress{width:15%}.dropzone.dropzone-queue .dropzone-item .dropzone-progress .progress{height:5px;transition:all .2s ease-in-out}@media (prefers-reduced-motion:reduce){.dropzone.dropzone-queue .dropzone-item .dropzone-progress .progress{transition:none}}.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar{margin-left:1rem;display:flex;flex-wrap:nowrap}.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-cancel,.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-delete,.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-start{height:25px;width:25px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:color .2s ease}.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-cancel>i,.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-delete>i,.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-start>i{transition:color .2s ease;font-size:.8rem;color:var(--bs-gray-600)}.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-cancel:hover,.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-delete:hover,.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-start:hover{transition:color .2s ease}.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-cancel:hover>i,.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-delete:hover>i,.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-start:hover>i{color:var(--bs-primary)}.dropzone.dropzone-queue .dropzone-item .dropzone-toolbar .dropzone-start{transition:color .2s ease}.gmaps img{max-width:none}.gmaps.gmaps-static>div{background-repeat:no-repeat;background-position:50% 50%;display:block}.noUi-target{border:0;background:var(--bs-gray-100);box-shadow:none}.noUi-target.noUi-horizontal{height:15px}.noUi-target.noUi-horizontal .noUi-handle{width:24px;height:24px;top:-4.5px;border-radius:50%;outline:0}.noUi-target.noUi-horizontal .noUi-handle::before{display:none}.noUi-target.noUi-horizontal .noUi-handle::after{display:none}.noUi-target.noUi-vertical{height:150px;width:15px}.noUi-target.noUi-vertical .noUi-handle{width:24px;height:24px;right:-4.5px;border-radius:50%;outline:0}.noUi-target.noUi-vertical .noUi-handle::before{display:none}.noUi-target.noUi-vertical .noUi-handle::after{display:none}.noUi-target .noUi-connect{background:var(--bs-component-active-bg)}.noUi-target .noUi-handle{background-color:#fff;border:1px solid var(--bs-gray-200);box-shadow:var(--bs-box-shadow-sm)}.noUi-target.noUi-sm{height:6px}.noUi-target.noUi-sm .noUi-handle{width:20px;height:20px;top:-7px}.noUi-target.noUi-lg{height:18px}.noUi-target.noUi-lg .noUi-handle{width:30px;height:30px;top:-6px}.noUi-target.noUi-target-light .noUi-connects{background-color:var(--bs-light-light)}.noUi-target.noUi-target-light .noUi-connects .noUi-connect{background-color:var(--bs-light)}.noUi-target.noUi-target-light .noUi-handle{border:1px solid var(--bs-light);box-shadow:0 3px 6px -3px rgba(var(--bs-light),.7);background-color:var(--bs-light)}.noUi-target.noUi-target-primary .noUi-connects{background-color:var(--bs-primary-light)}.noUi-target.noUi-target-primary .noUi-connects .noUi-connect{background-color:var(--bs-primary)}.noUi-target.noUi-target-primary .noUi-handle{border:1px solid var(--bs-primary);box-shadow:0 3px 6px -3px rgba(var(--bs-primary),.7);background-color:var(--bs-primary)}.noUi-target.noUi-target-secondary .noUi-connects{background-color:var(--bs-secondary-light)}.noUi-target.noUi-target-secondary .noUi-connects .noUi-connect{background-color:var(--bs-secondary)}.noUi-target.noUi-target-secondary .noUi-handle{border:1px solid var(--bs-secondary);box-shadow:0 3px 6px -3px rgba(var(--bs-secondary),.7);background-color:var(--bs-secondary)}.noUi-target.noUi-target-success .noUi-connects{background-color:var(--bs-success-light)}.noUi-target.noUi-target-success .noUi-connects .noUi-connect{background-color:var(--bs-success)}.noUi-target.noUi-target-success .noUi-handle{border:1px solid var(--bs-success);box-shadow:0 3px 6px -3px rgba(var(--bs-success),.7);background-color:var(--bs-success)}.noUi-target.noUi-target-info .noUi-connects{background-color:var(--bs-info-light)}.noUi-target.noUi-target-info .noUi-connects .noUi-connect{background-color:var(--bs-info)}.noUi-target.noUi-target-info .noUi-handle{border:1px solid var(--bs-info);box-shadow:0 3px 6px -3px rgba(var(--bs-info),.7);background-color:var(--bs-info)}.noUi-target.noUi-target-warning .noUi-connects{background-color:var(--bs-warning-light)}.noUi-target.noUi-target-warning .noUi-connects .noUi-connect{background-color:var(--bs-warning)}.noUi-target.noUi-target-warning .noUi-handle{border:1px solid var(--bs-warning);box-shadow:0 3px 6px -3px rgba(var(--bs-warning),.7);background-color:var(--bs-warning)}.noUi-target.noUi-target-danger .noUi-connects{background-color:var(--bs-danger-light)}.noUi-target.noUi-target-danger .noUi-connects .noUi-connect{background-color:var(--bs-danger)}.noUi-target.noUi-target-danger .noUi-handle{border:1px solid var(--bs-danger);box-shadow:0 3px 6px -3px rgba(var(--bs-danger),.7);background-color:var(--bs-danger)}.noUi-target.noUi-target-dark .noUi-connects{background-color:var(--bs-dark-light)}.noUi-target.noUi-target-dark .noUi-connects .noUi-connect{background-color:var(--bs-dark)}.noUi-target.noUi-target-dark .noUi-handle{border:1px solid var(--bs-dark);box-shadow:0 3px 6px -3px rgba(var(--bs-dark),.7);background-color:var(--bs-dark)}.noUi-tooltip{box-shadow:var(--bs-tooltip-box-shadow);background:var(--bs-tooltip-bg);color:var(--bs-tooltip-color);font-size:1rem;border:0;padding:.5rem .75rem;border-radius:.475rem}.ql-toolbar{font-family:Inter,Helvetica,sans-serif}.ql-toolbar.ql-snow{border:1px solid var(--bs-border-color);border-top-left-radius:.475rem;border-top-right-radius:.475rem}.ql-toolbar.ql-snow .ql-picker .ql-fill,.ql-toolbar.ql-snow .ql-picker .ql-stroke,.ql-toolbar.ql-snow button .ql-fill,.ql-toolbar.ql-snow button .ql-stroke{stroke:var(--bs-gray-500)}.ql-toolbar.ql-snow .ql-picker .ql-fill,.ql-toolbar.ql-snow button .ql-fill{fill:var(--bs-gray-500)}.ql-toolbar.ql-snow .ql-picker.ql-active .ql-fill,.ql-toolbar.ql-snow .ql-picker.ql-active .ql-stroke,.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-fill,.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-stroke,.ql-toolbar.ql-snow .ql-picker:focus .ql-fill,.ql-toolbar.ql-snow .ql-picker:focus .ql-stroke,.ql-toolbar.ql-snow .ql-picker:hover .ql-fill,.ql-toolbar.ql-snow .ql-picker:hover .ql-stroke,.ql-toolbar.ql-snow button.ql-active .ql-fill,.ql-toolbar.ql-snow button.ql-active .ql-stroke,.ql-toolbar.ql-snow button.ql-expanded .ql-fill,.ql-toolbar.ql-snow button.ql-expanded .ql-stroke,.ql-toolbar.ql-snow button:focus .ql-fill,.ql-toolbar.ql-snow button:focus .ql-stroke,.ql-toolbar.ql-snow button:hover .ql-fill,.ql-toolbar.ql-snow button:hover .ql-stroke{stroke:var(--bs-primary)}.ql-toolbar.ql-snow .ql-picker.ql-active .ql-fill,.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-fill,.ql-toolbar.ql-snow .ql-picker:focus .ql-fill,.ql-toolbar.ql-snow .ql-picker:hover .ql-fill,.ql-toolbar.ql-snow button.ql-active .ql-fill,.ql-toolbar.ql-snow button.ql-expanded .ql-fill,.ql-toolbar.ql-snow button:focus .ql-fill,.ql-toolbar.ql-snow button:hover .ql-fill{fill:var(--bs-primary)}.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg{right:0}[dir=rtl] .ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg,[direction=rtl] .ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg{left:0;right:auto}.ql-editor{color:var(--bs-input-color);text-align:initial}.ql-editor.ql-blank:before{left:auto!important;right:auto!important;color:var(--bs-text-muted)!important;font-style:normal!important}.ql-container.ql-snow{background-color:var(--bs-input-bg);border:1px solid var(--bs-gray-200);border-top:0;border-bottom-right-radius:.475rem;border-bottom-left-radius:.475rem}.ql-snow .ql-picker .ql-picker-label{color:var(--bs-gray-500)}.ql-snow .ql-picker .ql-picker-label.ql-active,.ql-snow .ql-picker .ql-picker-label:hover{color:var(--bs-primary)}.ql-snow .ql-picker.ql-expanded{outline:0!important;border-color:transparent!important}.ql-snow .ql-picker.ql-expanded .ql-picker-label{border-color:transparent!important;color:var(--bs-primary);outline:0!important}.ql-snow .ql-picker.ql-expanded .ql-picker-label.ql-active,.ql-snow .ql-picker.ql-expanded .ql-picker-label:hover{color:var(--bs-primary)}.ql-snow .ql-picker.ql-expanded .ql-picker-options{border:0;padding:.5rem 1rem;box-shadow:var(--bs-dropdown-box-shadow);background-color:var(--bs-body-bg);border-radius:.475rem}.ql-snow .ql-picker.ql-expanded .ql-picker-options .ql-picker-item{color:var(--bs-gray-600);outline:0}.ql-snow .ql-picker.ql-expanded .ql-picker-options .ql-picker-item.ql-active,.ql-snow .ql-picker.ql-expanded .ql-picker-options .ql-picker-item.ql-selected,.ql-snow .ql-picker.ql-expanded .ql-picker-options .ql-picker-item:hover{color:var(--bs-primary)}.ql-snow .ql-tooltip{border:0;padding:.5rem 1rem;box-shadow:var(--bs-dropdown-box-shadow);border-radius:.475rem}.ql-snow .ql-tooltip input[type=text]{border:0;background-color:transparent;outline:0!important;box-shadow:none;border-radius:0;border:1px solid var(--bs-border-color);color:var(--bs-gray-700);outline:0!important;border-radius:.475rem}.ql-snow .ql-tooltip input[type=text]:active,.ql-snow .ql-tooltip input[type=text]:focus{border-color:var(--bs-input-focus-border-color)!important}.ql-snow .ql-tooltip .ql-preview{color:var(--bs-gray-600)}.ql-snow .ql-tooltip .ql-action{transition:color .3s ease;color:var(--bs-gray-600)}.ql-snow .ql-tooltip .ql-action:hover{transition:color .3s ease;color:var(--bs-primary)}.modal .ql-snow .ql-tooltip.ql-editing{left:20px!important}.ql-snow .ql-editor pre.ql-syntax{background-color:var(--bs-gray-900);color:var(--bs-text-muted);overflow:visible;border-radius:.475rem}.ql-quil.ql-quil-plain .ql-toolbar{padding:0;margin:0;border:0}.ql-quil.ql-quil-plain .ql-toolbar:after{display:none}.ql-quil.ql-quil-plain .ql-toolbar .ql-picker-label{padding-left:0}.ql-quil.ql-quil-plain .ql-container{border:0}.ql-quil.ql-quil-plain .ql-editor{border:0;padding:0}.recaptcha{padding:15px;border:1px solid var(--bs-gray-200);border-radius:.475rem}.recaptcha .recaptcha-img{margin-bottom:10px}.recaptcha .recaptcha_only_if_incorrect_sol{color:var(--bs-danger)}.recaptcha .input-group .btn i{padding-right:0}.recaptcha .input-group .form-control{border-top-left-radius:.475rem!important;border-bottom-left-radius:.475rem!important}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown):not(.modal-open),html.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown){overflow-y:initial!important}body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown):not(.modal-open):not(.sweetalert2-nopadding),html.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown):not(.sweetalert2-nopadding){padding-right:0!important}.swal2-popup{background-color:var(--bs-body-bg);padding:2rem;border-radius:.475rem}.swal2-popup .swal2-title{font-weight:500;font-size:1.3rem;color:var(--bs-dark)}.swal2-popup .swal2-content,.swal2-popup .swal2-html-container{font-weight:400;font-size:1.1rem;margin-top:1.5rem;color:var(--bs-gray-800)}.swal2-popup .btn{margin:15px 5px 0}.swal2-popup .swal2-styled:focus{box-shadow:none}.swal2-popup .swal2-actions{margin:1.5rem auto 1rem auto}.swal2-container{overflow-y:hidden!important}.swal2-container.swal2-shown{background-color:rgba(0,0,0,.2)}.swal2-container .swal2-html-container{max-height:200px;overflow:auto}body.swal2-height-auto{height:100%!important}.swal2-icon.swal2-warning{border-color:var(--bs-warning);color:var(--bs-warning)}.swal2-icon.swal2-error{border-color:var(--bs-danger);color:var(--bs-danger)}.swal2-icon.swal2-error [class^=swal2-x-mark-line]{background-color:rgba(var(--bs-danger-rgb),.75)}.swal2-icon.swal2-success{border-color:var(--bs-success);color:var(--bs-success)}.swal2-icon.swal2-success [class^=swal2-success-line]{background-color:var(--bs-success)}.swal2-icon.swal2-success .swal2-success-ring{border-color:rgba(var(--bs-success-rgb),.3)}.swal2-icon.swal2-info{border-color:var(--bs-info);color:var(--bs-info)}.swal2-icon.swal2-question{border-color:var(--bs-primary);color:var(--bs-primary)}.tox-target{display:none}.tox-tinymce{border-radius:.475rem!important}.toastr{background-position:calc(100% - 1.5rem) center!important;background-position:1.5rem center!important;box-shadow:var(--bs-dropdown-box-shadow)!important;border-radius:.475rem!important;border:0!important;background-color:var(--bs-gray-100);color:var(--bs-gray-700);padding:1.25rem 1.25rem 1.25rem 4.5rem!important}.toastr .toastr-close-button{outline:0!important;font-size:0;width:.85rem;height:.85rem}.toastr .toastr-title{font-size:1.15rem;font-weight:500}.toastr .toastr-title+.toastr-message{margin-top:.25rem}.toastr .toastr-message{font-size:1rem;font-weight:400}.toastr.toastr-success{background-color:var(--bs-success);color:var(--bs-success-inverse)}.toastr.toastr-success .toastr-close-button{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-success-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-success-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-success-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.toastr.toastr-info{background-color:var(--bs-info);color:var(--bs-info-inverse)}.toastr.toastr-info .toastr-close-button{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-info-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-info-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-info-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.toastr.toastr-warning{background-color:var(--bs-warning);color:var(--bs-warning-inverse)}.toastr.toastr-warning .toastr-close-button{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-warning-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-warning-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-warning-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.toastr.toastr-error{background-color:var(--bs-danger);color:var(--bs-danger-inverse)}.toastr.toastr-error .toastr-close-button{mask-repeat:no-repeat;mask-position:center;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center;background-color:var(--bs-danger-inverse);-webkit-mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-danger-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\");mask-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-danger-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\")}.toastr-top-center{top:12px}.toastr-bottom-center{bottom:12px}.draggable{transition:opacity .3s ease;outline:0!important}.draggable.draggable-mirror{opacity:.8;transition:opacity .3s ease;border:1px dashed var(--bs-gray-300)!important;border-radius:.475rem}.draggable.draggable--original{opacity:0!important}.draggable.draggable-source--is-dragging.draggable--over{opacity:0!important}.draggable .draggable-handle{cursor:move}.apexcharts-legend-text,.apexcharts-text,.apexcharts-title-text{font-family:Inter,Helvetica,sans-serif!important}.apexcharts-title-text{font-weight:400}.apexcharts-pie-label{font-weight:400;font-size:.95rem}.apexcharts-toolbar{text-align:left!important}.apexcharts-menu{background:var(--bs-body-bg)!important;border:0!important;padding:.5rem 0!important;box-shadow:var(--bs-dropdown-box-shadow);border-radius:.475rem!important;overflow:hidden;min-width:10rem!important}.apexcharts-menu .apexcharts-menu-item{padding:.65rem .85rem;transition:all .2s ease-in-out}.apexcharts-menu .apexcharts-menu-item:hover{background-color:var(--bs-component-hover-bg)!important}.apexcharts-tooltip.apexcharts-theme-light{border-radius:.475rem;box-shadow:var(--bs-dropdown-box-shadow);border:0!important;background:var(--bs-body-bg)!important;color:var(--bs-gray-800)}.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title{background:var(--bs-body-bg)!important;font-weight:500;color:var(--bs-gray-800);border-bottom:1px solid var(--bs-gray-100)!important}.apexcharts-tooltip .apexcharts-tooltip-title{padding:.5rem 1rem}.apexcharts-xaxistooltip.apexcharts-theme-light{border-radius:.475rem!important;box-shadow:var(--bs-dropdown-box-shadow)!important;border:0!important;background:var(--bs-dropdown-box-shadow)!important;color:var(--bs-gray-800)}.apexcharts-xaxistooltip.apexcharts-theme-light:before{border-bottom:0!important}.apexcharts-xaxistooltip.apexcharts-theme-light:after{border-bottom-color:var(--bs-dropdown-box-shadow)!important}.card-rounded-bottom .apexcharts-canvas svg{border-bottom-left-radius:.625rem;border-bottom-right-radius:.625rem}.rounded .apexcharts-canvas svg{border-radius:.475rem!important}.rounded-sm .apexcharts-canvas svg{border-radius:.425rem!important}.rounded-lg .apexcharts-canvas svg{border-radius:.625rem!important}.rounded-xl .apexcharts-canvas svg{border-radius:1rem!important}.leaflet-container .leaflet-bottom,.leaflet-container .leaflet-control,.leaflet-container .leaflet-pane,.leaflet-container .leaflet-top{z-index:1!important}.leaflet-container .leaflet-popup-content-wrapper{border-radius:.475rem!important;text-align:center;box-shadow:var(--bs-box-shadow)!important}.leaflet-container .leaflet-popup-content-wrapper .leaflet-popup-content{font-family:Inter,Helvetica,sans-serif;font-size:1rem}.tns{position:relative;overflow:hidden}.tns [data-tns=true]{display:none}.tns .tns-item{opacity:0;transition:all .3s ease}.tns .tns-controls{display:flex;justify-content:center;align-items:center}.tns .tns-controls button{outline:0;border:0;margin:0 .25rem;border-radius:.475rem;padding:.5rem .75rem;background-color:var(--bs-primary);color:var(--bs-primary-inverse)}.tns .tns-controls button:hover{background-color:var(--bs-primary-active)}.tns .tns-nav{display:flex;justify-content:center;align-items:center;padding-top:1.5rem;padding-bottom:1.5rem}.tns .tns-nav button{display:block;outline:0;width:1.25rem;height:.75rem;background-color:var(--bs-gray-200);margin:0 .25rem;border:0;border-radius:.35rem}.tns .tns-nav button.tns-nav-active{background-color:var(--bs-primary)}.tns.tns-initiazlied [data-tns=true]{display:flex}.tns.tns-initiazlied .tns-item{opacity:1;transition:all .3s ease}.tns.tns-default{position:relative}.tns.tns-default [data-controls=next],.tns.tns-default [data-controls=prev]{position:absolute;top:50%;transform:translateY(-50%)}.tns.tns-default [data-controls=prev]{left:0}.tns.tns-default [data-controls=next]{right:0}.tns.tns-default .tns-outer{margin:0 4rem}@media (max-width:767.98px){.tns.tns-default .tns-outer{margin:0 2rem}}.tns.tns-flush .tns-outer{margin:0}.tns.tns-circle-nav .tns-nav{display:flex;justify-content:center;align-items:center;padding-top:1.5rem;padding-bottom:1.5rem}.tns.tns-circle-nav .tns-nav button{display:block;outline:0;width:1.15rem;height:1.15rem;background-color:var(--bs-gray-200);margin:0 .55rem;border:0;border-radius:50%}.tns.tns-circle-nav .tns-nav button.tns-nav-active{background-color:var(--bs-gray-400)}.tns-hide-disabled-nav [disabled]{display:none!important}body{--fc-event-border-color:var(--bs-primary);--fc-event-bg-color:var(--bs-primary);--fc-event-text-color:var(--bs-primary-inverse)}.fc{--fc-border-color:var(--bs-gray-200);--fc-page-bg-color:#ffffff;--fc-small-font-size:0.95rem;--fc-highlight-color:var(--bs-light);--fc-bg-event-opacity:0.3;--fc-neutral-bg-color:var(--bs-light);--fc-today-bg-color:var(--bs-success-light);--fc-now-indicator-color:var(--bs-danger);--fc-list-event-hover-bg-color:var(--bs-light);--fc-button-text-color:var(--bs-gray-600);--fc-button-bg-color:var(--bs-gray-100);--fc-button-border-color:var(--bs-gray-100);--fc-button-hover-bg-color:var(--bs-gray-100);--fc-button-hover-border-color:var(--bs-gray-100);--fc-button-active-bg-color:var(--bs-gray-200);--fc-button-active-border-color:var(--bs-gray-200)}.fc table{font-size:1rem}.fc .fc-button{padding:.75rem 1.25rem;box-shadow:none!important;border:0!important;border-radius:.475rem;vertical-align:middle;font-weight:500;text-transform:capitalize}.fc .fc-button-primary{margin:0}.fc .fc-button-primary .fc-icon{font-size:1.35rem;margin-bottom:.15rem}.fc .fc-button-primary:not(:disabled):not(.fc-button-active):active,.fc .fc-button-primary:not(:disabled):not(.fc-button-active):focus,.fc .fc-button-primary:not(:disabled):not(.fc-button-active):hover{color:var(--bs-gray-900)}.fc .fc-button-primary:not(:disabled):not(.fc-button-active):active .fc-icon,.fc .fc-button-primary:not(:disabled):not(.fc-button-active):focus .fc-icon,.fc .fc-button-primary:not(:disabled):not(.fc-button-active):hover .fc-icon{color:var(--bs-gray-900)}.fc .fc-button-primary:not(:disabled).fc-button-active{color:var(--bs-gray-900)}.fc .fc-button-primary:not(:disabled).fc-button-active .fc-icon{color:var(--bs-gray-900)}.fc .fc-button-group .fc-button{margin:0!important}.fc .fc-toolbar-title{font-size:1.5rem;font-weight:600;color:var(--bs-gray-800)}.fc .fc-col-header-cell{padding:.75rem .5rem}.fc .fc-col-header-cell .fc-col-header-cell-cushion{font-size:1.1rem;font-weight:500;color:var(--bs-gray-800)}.fc .fc-scrollgrid{border-radius:.475rem}.fc .fc-scrollgrid thead>tr td:first-child{border-top-left-radius:.475rem}.fc .fc-scrollgrid thead>tr td:last-child{border-top-right-radius:.475rem}.fc .fc-scrollgrid tbody>tr:last-child td:first-child{border-bottom-left-radius:.475rem}.fc .fc-scrollgrid tbody>tr:last-child td:last-child{border-bottom-right-radius:.475rem}.fc .fc-daygrid-event{margin-top:3px}.fc .fc-daygrid-block-event .fc-event-time,.fc .fc-daygrid-block-event .fc-event-title,.fc .fc-daygrid-dot-event .fc-event-time,.fc .fc-daygrid-dot-event .fc-event-title{padding:.25rem .25rem}.fc .fc-daygrid-day-number{color:var(--bs-gray-800)}.fc .fc-daygrid-dot-event{background-color:var(--bs-light);color:var(--bs-gray-600)}.fc .fc-daygrid-dot-event .fc-event-title{font-weight:500}.fc .fc-daygrid-dot-event.fc-event-mirror,.fc .fc-daygrid-dot-event:hover{background-color:var(--bs-light);color:var(--bs-primary)}.fc .fc-daygrid-event-dot{margin-left:.5rem;margin-right:.1rem}.fc .fc-popover{border:0!important;background-color:var(--bs-body-bg);box-shadow:var(--bs-dropdown-box-shadow);border-radius:.475rem}.modal-open .fc .fc-popover{z-index:1054!important}.fc .fc-popover .fc-popover-header{border-top-left-radius:.475rem;border-top-right-radius:.475rem;padding:.65rem .75rem;background-color:var(--bs-tooltip-bg)}.fc .fc-popover .fc-popover-header .fc-popover-title{color:var(--bs-gray-800);font-size:1rem;font-weight:500}.fc .fc-popover .fc-popover-header .fc-popover-close{font-size:1rem;color:var(--bs-gray-600)}.fc .fc-popover .fc-popover-header .fc-popover-close:hover{color:var(--bs-primary)}.fc .fc-popover .fc-popover-body{padding:.5rem .75rem .75rem .75rem}.fc .fc-daygrid-more-link{font-weight:500}.fc .fc-timegrid-slot{height:2rem;font-size:.95rem}.fc .fc-list-day-cushion,.fc .fc-list-table td{padding:.85rem 1.15rem}.fc .fc-list-day-side-text,.fc .fc-list-day-text{font-size:1.1rem;color:var(--bs-gray-900);font-weight:600}.fc .fc-list,.fc .fc-list-table{border-radius:.475rem}.fc .fc-list{overflow:hidden;position:relative}.fc .fc-timegrid-axis{padding-left:0;padding-right:0}.fc .fc-timegrid-event .fc-event-main{padding:.25rem .25rem}.fc .fc-timegrid-now-indicator-arrow{margin-top:-1px}.fc-h-event{font-weight:400}@media (max-width:767.98px){.fc .fc-header-toolbar{flex-direction:column;align-items:flex-start}.fc .fc-header-toolbar .fc-toolbar-chunk:nth-child(1),.fc .fc-header-toolbar .fc-toolbar-chunk:nth-child(3){order:2}.fc .fc-header-toolbar .fc-toolbar-chunk:nth-child(2){order:1}.fc .fc-header-toolbar .fc-toolbar-chunk:nth-child(1),.fc .fc-header-toolbar .fc-toolbar-chunk:nth-child(2){margin-bottom:1rem}}.kanban-container{width:100%!important;display:flex;flex-wrap:wrap}.kanban-container .kanban-board{float:none;flex-shrink:0;margin-bottom:1.25rem;margin-right:1.25rem!important;background-color:var(--bs-gray-100);border-radius:.475rem}.kanban-container .kanban-board:last-child{margin-right:0!important}.kanban-container .kanban-board .kanban-board-header{border-top-left-radius:.475rem;border-top-right-radius:.475rem}.kanban-container .kanban-board .kanban-board-header .kanban-title-board{font-size:1.2rem;font-weight:500;color:var(--bs-gray-900)}.kanban-container .kanban-board .kanban-board-header.light{background-color:var(--bs-light);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light .kanban-title-board{color:var(--bs-light-inverse)}.kanban-container .kanban-board .kanban-board-header.light-light{color:rgba(var(--bs-light),.1);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light-light .kanban-title-board{color:var(--bs-light)}.kanban-container .kanban-board .kanban-board-header.primary{background-color:var(--bs-primary);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.primary .kanban-title-board{color:var(--bs-primary-inverse)}.kanban-container .kanban-board .kanban-board-header.light-primary{color:rgba(var(--bs-primary),.1);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light-primary .kanban-title-board{color:var(--bs-primary)}.kanban-container .kanban-board .kanban-board-header.secondary{background-color:var(--bs-secondary);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.secondary .kanban-title-board{color:var(--bs-secondary-inverse)}.kanban-container .kanban-board .kanban-board-header.light-secondary{color:rgba(var(--bs-secondary),.1);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light-secondary .kanban-title-board{color:var(--bs-secondary)}.kanban-container .kanban-board .kanban-board-header.success{background-color:var(--bs-success);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.success .kanban-title-board{color:var(--bs-success-inverse)}.kanban-container .kanban-board .kanban-board-header.light-success{color:rgba(var(--bs-success),.1);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light-success .kanban-title-board{color:var(--bs-success)}.kanban-container .kanban-board .kanban-board-header.info{background-color:var(--bs-info);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.info .kanban-title-board{color:var(--bs-info-inverse)}.kanban-container .kanban-board .kanban-board-header.light-info{color:rgba(var(--bs-info),.1);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light-info .kanban-title-board{color:var(--bs-info)}.kanban-container .kanban-board .kanban-board-header.warning{background-color:var(--bs-warning);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.warning .kanban-title-board{color:var(--bs-warning-inverse)}.kanban-container .kanban-board .kanban-board-header.light-warning{color:rgba(var(--bs-warning),.1);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light-warning .kanban-title-board{color:var(--bs-warning)}.kanban-container .kanban-board .kanban-board-header.danger{background-color:var(--bs-danger);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.danger .kanban-title-board{color:var(--bs-danger-inverse)}.kanban-container .kanban-board .kanban-board-header.light-danger{color:rgba(var(--bs-danger),.1);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light-danger .kanban-title-board{color:var(--bs-danger)}.kanban-container .kanban-board .kanban-board-header.dark{background-color:var(--bs-dark);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.dark .kanban-title-board{color:var(--bs-dark-inverse)}.kanban-container .kanban-board .kanban-board-header.light-dark{color:rgba(var(--bs-dark),.1);box-shadow:none}.kanban-container .kanban-board .kanban-board-header.light-dark .kanban-title-board{color:var(--bs-dark)}.kanban-container .kanban-board .kanban-drag .kanban-item{border-radius:.475rem;box-shadow:0 0 13px 0 rgba(0,0,0,.05);background:var(--bs-body-bg)}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light]{background-color:var(--bs-light);color:var(--bs-light-inverse);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light-light]{background-color:var(--bs-light-light);color:var(--bs-light);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=primary]{background-color:var(--bs-primary);color:var(--bs-primary-inverse);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light-primary]{background-color:var(--bs-primary-light);color:var(--bs-primary);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=secondary]{background-color:var(--bs-secondary);color:var(--bs-secondary-inverse);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light-secondary]{background-color:var(--bs-secondary-light);color:var(--bs-secondary);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=success]{background-color:var(--bs-success);color:var(--bs-success-inverse);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light-success]{background-color:var(--bs-success-light);color:var(--bs-success);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=info]{background-color:var(--bs-info);color:var(--bs-info-inverse);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light-info]{background-color:var(--bs-info-light);color:var(--bs-info);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=warning]{background-color:var(--bs-warning);color:var(--bs-warning-inverse);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light-warning]{background-color:var(--bs-warning-light);color:var(--bs-warning);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=danger]{background-color:var(--bs-danger);color:var(--bs-danger-inverse);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light-danger]{background-color:var(--bs-danger-light);color:var(--bs-danger);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=dark]{background-color:var(--bs-dark);color:var(--bs-dark-inverse);box-shadow:none}.kanban-container .kanban-board .kanban-drag .kanban-item[data-class=light-dark]{background-color:var(--bs-dark-light);color:var(--bs-dark);box-shadow:none}.kanban-fixed-height .kanban-container .kanban-board .kanban-drag{position:relative;overflow-y:auto}.jstree-default .jstree-anchor{color:var(--bs-gray-700);padding:0 8px 0 4px}.jstree-default .jstree-icon{color:var(--bs-gray-700);font-size:1.3rem}.jstree-default .jstree-icon.la{font-size:1.5rem}.jstree-default .jstree-icon.fa{font-size:1.2rem}.jstree-default .jstree-disabled{cursor:not-allowed;line-height:auto;height:auto;opacity:.7}.jstree-default .jstree-disabled .jstree-icon{color:var(--bs-gray-700)}.jstree-default .jstree-clicked{border:0;background:var(--bs-gray-100);box-shadow:none}.jstree-default .jstree-hovered{border:0;background-color:var(--bs-gray-100);box-shadow:none}.jstree-default .jstree-wholerow-clicked{background:var(--bs-gray-200);box-shadow:none}.jstree-default .jstree-wholerow-hovered,.jstree-default.jstree-wholerow .jstree-wholerow-hovered{border:0;background-color:var(--bs-gray-100);box-shadow:none}.jstree-open>.jstree-anchor>.fa-folder:before{margin-left:2px;content:\"\\f07c\"}.jstree-open>.jstree-anchor>.la-folder:before{margin-left:2px;content:\"\\f200\"}.jstree-default.jstree-rtl .jstree-node{background-position:100% 1px!important}.jstree-default.jstree-rtl .jstree-last{background:0 0;background-repeat:no-repeat}.jstree-rtl .jstree-anchor{padding:0 4px 0 8px}.vakata-context,.vakata-context ul{padding:.5rem 0;min-width:150px;font-size:1rem;font-family:var(--bs-font-sans-serif);background:var(--bs-body-bg);box-shadow:var(--bs-dropdown-box-shadow);border:0;border-radius:.475rem}.vakata-context li,.vakata-context ul li{padding:0;border:0}.vakata-context li a,.vakata-context ul li a{padding:0 1.2rem;border:0}.vakata-context li a i,.vakata-context ul li a i{display:none}.vakata-context li a .vakata-contextmenu-sep,.vakata-context ul li a .vakata-contextmenu-sep{display:none}.vakata-context li a ins,.vakata-context li a span,.vakata-context ul li a ins,.vakata-context ul li a span{display:none;border:0!important}.vakata-context .vakata-context-hover>a,.vakata-context li a:hover,.vakata-context ul .vakata-context-hover>a,.vakata-context ul li a:hover{margin:0;background-color:var(--bs-gray-100);color:var(--bs-primary);box-shadow:none}.vakata-context .vakata-context-hover>a .ins,.vakata-context .vakata-context-hover>a .span,.vakata-context li a:hover .ins,.vakata-context li a:hover .span,.vakata-context ul .vakata-context-hover>a .ins,.vakata-context ul .vakata-context-hover>a .span,.vakata-context ul li a:hover .ins,.vakata-context ul li a:hover .span{border:0!important}.vakata-context .vakata-context-separator a,.vakata-context-rtl .vakata-context-separator a{margin:0;border:0;height:2px;background-color:var(--bs-gray-200)}.jstree-rename-input{outline:0!important;padding:2px 6px!important;margin-right:-4px!important;background-color:var(--bs-gray-100)!important;border:1px solid var(--bs-gray-100)!important;border-radius:.475rem}.vis-timeline{border:1px solid var(--bs-border-color)!important;border-radius:.475rem!important}.vis-timeline .vis-labelset .vis-label{display:flex;align-items:center;padding-left:1rem;padding-right:1rem;border-bottom:none;font-size:1.25rem;font-weight:500;color:var(--bs-gray-900)}.vis-timeline .vis-foreground .vis-group{border-bottom:none}.vis-timeline .vis-item{position:absolute;color:var(--bs-gray-700);border-color:var(--bs-primary);border-width:1px;background-color:var(--bs-gray-100);border-radius:.475rem!important}.vis-timeline .vis-item.vis-selected{background-color:var(--bs-warning-light);color:var(--bs-gray-700);border-color:var(--bs-warning)}.vis-timeline .vis-item .vis-item-content{padding:.75rem 1rem;width:100%;transform:none!important}.vis-timeline .vis-time-axis{font-size:.95rem;text-transform:uppercase;font-weight:500}.vis-timeline .vis-time-axis .vis-text{color:var(--bs-gray-400)}.vis-timeline .vis-time-axis .vis-grid.vis-minor{border-left-color:var(--bs-border-dashed-color)!important}.vis-timeline .vis-time-axis .vis-grid.vis-vertical{border-left-style:dashed!important}.vis-timeline .vis-panel .vis-shadow{box-shadow:none!important}.vis-timeline .vis-panel.vis-bottom,.vis-timeline .vis-panel.vis-center,.vis-timeline .vis-panel.vis-left,.vis-timeline .vis-panel.vis-right,.vis-timeline .vis-panel.vis-top{border-color:var(--bs-border-color)!important}.vis-timeline .vis-current-time{background-color:var(--bs-success)}.vis-timeline-custom .vis-timeline{border:0!important}.vis-timeline-custom .vis-timeline .vis-label{padding-left:0!important}.vis-timeline-custom .vis-panel.vis-bottom,.vis-timeline-custom .vis-panel.vis-center,.vis-timeline-custom .vis-panel.vis-left,.vis-timeline-custom .vis-panel.vis-right,.vis-timeline-custom .vis-panel.vis-top{border:0!important}.vis-timeline-custom .vis-item{background-color:transparent;border:0!important;border-radius:0!important}.vis-timeline-custom .vis-item .vis-item-content{padding:0!important}.tempus-dominus-widget{padding:1rem .5rem .5rem .5rem;width:280px!important;box-shadow:var(--bs-dropdown-box-shadow)!important;background-color:var(--bs-body-bg)!important;border-radius:.475rem}.tempus-dominus-widget i:not(.ki-outline):not(.ki-solid):not(.ki-duotone){font-size:.9rem!important}.tempus-dominus-widget .picker-switch{font-size:1.05rem;font-weight:600}.tempus-dominus-widget .date-container-days{grid-auto-rows:36px}.tempus-dominus-widget .date-container-days .day{border-radius:.475rem!important}.tempus-dominus-widget .date-container-months{grid-auto-rows:36px}.tempus-dominus-widget .date-container-months .month{border-radius:.475rem!important}.tempus-dominus-widget .date-container-years{grid-auto-rows:36px}.tempus-dominus-widget .date-container-years .year{border-radius:.475rem!important}.tempus-dominus-widget .time-container .separator{border:0!important}.tempus-dominus-widget .time-container .time-container-clock div{border-radius:.475rem!important}.tempus-dominus-widget .toolbar div{border-radius:.475rem}.tempus-dominus-widget .toolbar div i:not(.ki-outline):not(.ki-solid):not(.ki-duotone){font-size:1.1rem!important}.tempus-dominus-widget.dark,.tempus-dominus-widget.light{color:var(--bs-gray-900)}.tempus-dominus-widget.dark [data-action].disabled,.tempus-dominus-widget.dark [data-action].disabled:hover,.tempus-dominus-widget.light [data-action].disabled,.tempus-dominus-widget.light [data-action].disabled:hover{color:var(--bs-gray-400)}.tempus-dominus-widget.dark .toolbar div:hover,.tempus-dominus-widget.light .toolbar div:hover{background:var(--bs-gray-200)}.tempus-dominus-widget.dark .date-container-days .dow,.tempus-dominus-widget.light .date-container-days .dow{color:var(--bs-gray-700)!important}.tempus-dominus-widget.dark .date-container-days .cw,.tempus-dominus-widget.light .date-container-days .cw{color:rgba(var(--bs-gray-900-rgb),.38)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight):hover{background:var(--bs-gray-200)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active{background-color:var(--bs-primary);color:var(--bs-body-bg);text-shadow:0 -1px 0 rgba(var(--bs-gray-900-rgb),.25)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.old{color:var(--bs-body-bg)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.today:before{border-bottom-color:var(--bs-body-bg)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).old{color:rgba(var(--bs-gray-900-rgb),.38)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled:hover{color:var(--bs-gray-400)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).today:before{border-bottom-color:var(--bs-primary);border-top-color:rgba(var(--bs-gray-900-rgb),.2)}.tempus-dominus-widget.dark button,.tempus-dominus-widget.light button{color:var(--bs-body-bg);background-color:var(--bs-primary);border-color:var(--bs-primary)}.ki-duotone,.ki-outline,.ki-solid{line-height:1;font-size:1rem;color:var(--bs-text-muted)}"
  },
  {
    "path": "static/assets/plugins/global/plugins.bundle.js",
    "content": "/*!\n * jQuery JavaScript Library v3.7.1\n * https://jquery.com/\n *\n * Copyright OpenJS Foundation and other contributors\n * Released under the MIT license\n * https://jquery.org/license\n *\n * Date: 2023-08-28T13:37Z\n */\n!function(e,t){\"use strict\";\"object\"==typeof module&&\"object\"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error(\"jQuery requires a window with a document\");return t(e)}:t(e)}(\"undefined\"!=typeof window?window:this,function(e,t){\"use strict\";var n=[],r=Object.getPrototypeOf,i=n.slice,a=n.flat?function(e){return n.flat.call(e)}:function(e){return n.concat.apply([],e)},o=n.push,s=n.indexOf,l={},u=l.toString,d=l.hasOwnProperty,c=d.toString,f=c.call(Object),p={},h=function(e){return\"function\"==typeof e&&\"number\"!=typeof e.nodeType&&\"function\"!=typeof e.item},m=function(e){return null!=e&&e===e.window},v=e.document,g={type:!0,src:!0,nonce:!0,noModule:!0};function _(e,t,n){var r,i,a=(n=n||v).createElement(\"script\");if(a.text=e,t)for(r in g)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&a.setAttribute(r,i);n.head.appendChild(a).parentNode.removeChild(a)}function y(e){return null==e?e+\"\":\"object\"==typeof e||\"function\"==typeof e?l[u.call(e)]||\"object\":typeof e}var b=\"3.7.1\",M=/HTML$/i,w=function(e,t){return new w.fn.init(e,t)};function k(e){var t=!!e&&\"length\"in e&&e.length,n=y(e);return!h(e)&&!m(e)&&(\"array\"===n||0===t||\"number\"==typeof t&&t>0&&t-1 in e)}function L(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}w.fn=w.prototype={jquery:b,constructor:w,length:0,toArray:function(){return i.call(this)},get:function(e){return null==e?i.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(w.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(w.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:o,sort:n.sort,splice:n.splice},w.extend=w.fn.extend=function(){var e,t,n,r,i,a,o=arguments[0]||{},s=1,l=arguments.length,u=!1;for(\"boolean\"==typeof o&&(u=o,o=arguments[s]||{},s++),\"object\"==typeof o||h(o)||(o={}),s===l&&(o=this,s--);s<l;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],\"__proto__\"!==t&&o!==r&&(u&&r&&(w.isPlainObject(r)||(i=Array.isArray(r)))?(n=o[t],a=i&&!Array.isArray(n)?[]:i||w.isPlainObject(n)?n:{},i=!1,o[t]=w.extend(u,a,r)):void 0!==r&&(o[t]=r));return o},w.extend({expando:\"jQuery\"+(b+Math.random()).replace(/\\D/g,\"\"),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||\"[object Object]\"!==u.call(e))&&(!(t=r(e))||\"function\"==typeof(n=d.call(t,\"constructor\")&&t.constructor)&&c.call(n)===f)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){_(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(k(e))for(n=e.length;r<n&&!1!==t.call(e[r],r,e[r]);r++);else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},text:function(e){var t,n=\"\",r=0,i=e.nodeType;if(!i)for(;t=e[r++];)n+=w.text(t);return 1===i||11===i?e.textContent:9===i?e.documentElement.textContent:3===i||4===i?e.nodeValue:n},makeArray:function(e,t){var n=t||[];return null!=e&&(k(Object(e))?w.merge(n,\"string\"==typeof e?[e]:e):o.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:s.call(t,e,n)},isXMLDoc:function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!M.test(t||n&&n.nodeName||\"HTML\")},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,a=e.length,o=!n;i<a;i++)!t(e[i],i)!==o&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,s=[];if(k(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&s.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&s.push(i);return a(s)},guid:1,support:p}),\"function\"==typeof Symbol&&(w.fn[Symbol.iterator]=n[Symbol.iterator]),w.each(\"Boolean Number String Function Array Date RegExp Object Error Symbol\".split(\" \"),function(e,t){l[\"[object \"+t+\"]\"]=t.toLowerCase()});var x=n.pop,T=n.sort,S=n.splice,D=\"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",E=new RegExp(\"^\"+D+\"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\"+D+\"+$\",\"g\");w.contains=function(e,t){var n=t&&t.parentNode;return e===n||!(!n||1!==n.nodeType||!(e.contains?e.contains(n):e.compareDocumentPosition&&16&e.compareDocumentPosition(n)))};var Y=/([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\x80-\\uFFFF\\w-]/g;function A(e,t){return t?\"\\0\"===e?\"�\":e.slice(0,-1)+\"\\\\\"+e.charCodeAt(e.length-1).toString(16)+\" \":\"\\\\\"+e}w.escapeSelector=function(e){return(e+\"\").replace(Y,A)};var O=v,C=o;!function(){var t,r,a,o,l,u,c,f,h,m,v=C,g=w.expando,_=0,y=0,b=ee(),M=ee(),k=ee(),Y=ee(),A=function(e,t){return e===t&&(l=!0),0},j=\"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",P=\"(?:\\\\\\\\[\\\\da-fA-F]{1,6}\"+D+\"?|\\\\\\\\[^\\\\r\\\\n\\\\f]|[\\\\w-]|[^\\0-\\\\x7f])+\",H=\"\\\\[\"+D+\"*(\"+P+\")(?:\"+D+\"*([*^$|!~]?=)\"+D+\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\"+P+\"))|)\"+D+\"*\\\\]\",I=\":(\"+P+\")(?:\\\\((('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\"+H+\")*)|.*)\\\\)|)\",F=new RegExp(D+\"+\",\"g\"),N=new RegExp(\"^\"+D+\"*,\"+D+\"*\"),R=new RegExp(\"^\"+D+\"*([>+~]|\"+D+\")\"+D+\"*\"),V=new RegExp(D+\"|>\"),$=new RegExp(I),z=new RegExp(\"^\"+P+\"$\"),W={ID:new RegExp(\"^#(\"+P+\")\"),CLASS:new RegExp(\"^\\\\.(\"+P+\")\"),TAG:new RegExp(\"^(\"+P+\"|[*])\"),ATTR:new RegExp(\"^\"+H),PSEUDO:new RegExp(\"^\"+I),CHILD:new RegExp(\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\"+D+\"*(even|odd|(([+-]|)(\\\\d*)n|)\"+D+\"*(?:([+-]|)\"+D+\"*(\\\\d+)|))\"+D+\"*\\\\)|)\",\"i\"),bool:new RegExp(\"^(?:\"+j+\")$\",\"i\"),needsContext:new RegExp(\"^\"+D+\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\"+D+\"*((?:-\\\\d)?\\\\d*)\"+D+\"*\\\\)|)(?=[^-]|$)\",\"i\")},U=/^(?:input|select|textarea|button)$/i,B=/^h\\d$/i,q=/^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,G=/[+~]/,J=new RegExp(\"\\\\\\\\[\\\\da-fA-F]{1,6}\"+D+\"?|\\\\\\\\([^\\\\r\\\\n\\\\f])\",\"g\"),Z=function(e,t){var n=\"0x\"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},K=function(){le()},X=fe(function(e){return!0===e.disabled&&L(e,\"fieldset\")},{dir:\"parentNode\",next:\"legend\"});try{v.apply(n=i.call(O.childNodes),O.childNodes),n[O.childNodes.length].nodeType}catch(e){v={apply:function(e,t){C.apply(e,i.call(t))},call:function(e){C.apply(e,i.call(arguments,1))}}}function Q(e,t,n,r){var i,a,o,s,l,d,c,m=t&&t.ownerDocument,_=t?t.nodeType:9;if(n=n||[],\"string\"!=typeof e||!e||1!==_&&9!==_&&11!==_)return n;if(!r&&(le(t),t=t||u,f)){if(11!==_&&(l=q.exec(e)))if(i=l[1]){if(9===_){if(!(o=t.getElementById(i)))return n;if(o.id===i)return v.call(n,o),n}else if(m&&(o=m.getElementById(i))&&Q.contains(t,o)&&o.id===i)return v.call(n,o),n}else{if(l[2])return v.apply(n,t.getElementsByTagName(e)),n;if((i=l[3])&&t.getElementsByClassName)return v.apply(n,t.getElementsByClassName(i)),n}if(!(Y[e+\" \"]||h&&h.test(e))){if(c=e,m=t,1===_&&(V.test(e)||R.test(e))){for((m=G.test(e)&&se(t.parentNode)||t)==t&&p.scope||((s=t.getAttribute(\"id\"))?s=w.escapeSelector(s):t.setAttribute(\"id\",s=g)),a=(d=de(e)).length;a--;)d[a]=(s?\"#\"+s:\":scope\")+\" \"+ce(d[a]);c=d.join(\",\")}try{return v.apply(n,m.querySelectorAll(c)),n}catch(t){Y(e,!0)}finally{s===g&&t.removeAttribute(\"id\")}}}return _e(e.replace(E,\"$1\"),t,n,r)}function ee(){var e=[];return function t(n,i){return e.push(n+\" \")>r.cacheLength&&delete t[e.shift()],t[n+\" \"]=i}}function te(e){return e[g]=!0,e}function ne(e){var t=u.createElement(\"fieldset\");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function re(e){return function(t){return L(t,\"input\")&&t.type===e}}function ie(e){return function(t){return(L(t,\"input\")||L(t,\"button\"))&&t.type===e}}function ae(e){return function(t){return\"form\"in t?t.parentNode&&!1===t.disabled?\"label\"in t?\"label\"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&X(t)===e:t.disabled===e:\"label\"in t&&t.disabled===e}}function oe(e){return te(function(t){return t=+t,te(function(n,r){for(var i,a=e([],n.length,t),o=a.length;o--;)n[i=a[o]]&&(n[i]=!(r[i]=n[i]))})})}function se(e){return e&&void 0!==e.getElementsByTagName&&e}function le(e){var t,n=e?e.ownerDocument||e:O;return n!=u&&9===n.nodeType&&n.documentElement?(c=(u=n).documentElement,f=!w.isXMLDoc(u),m=c.matches||c.webkitMatchesSelector||c.msMatchesSelector,c.msMatchesSelector&&O!=u&&(t=u.defaultView)&&t.top!==t&&t.addEventListener(\"unload\",K),p.getById=ne(function(e){return c.appendChild(e).id=w.expando,!u.getElementsByName||!u.getElementsByName(w.expando).length}),p.disconnectedMatch=ne(function(e){return m.call(e,\"*\")}),p.scope=ne(function(){return u.querySelectorAll(\":scope\")}),p.cssHas=ne(function(){try{return u.querySelector(\":has(*,:jqfake)\"),!1}catch(e){return!0}}),p.getById?(r.filter.ID=function(e){var t=e.replace(J,Z);return function(e){return e.getAttribute(\"id\")===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&f){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(J,Z);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode(\"id\");return n&&n.value===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&f){var n,r,i,a=t.getElementById(e);if(a){if((n=a.getAttributeNode(\"id\"))&&n.value===e)return[a];for(i=t.getElementsByName(e),r=0;a=i[r++];)if((n=a.getAttributeNode(\"id\"))&&n.value===e)return[a]}return[]}}),r.find.TAG=function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},r.find.CLASS=function(e,t){if(void 0!==t.getElementsByClassName&&f)return t.getElementsByClassName(e)},h=[],ne(function(e){var t;c.appendChild(e).innerHTML=\"<a id='\"+g+\"' href='' disabled='disabled'></a><select id='\"+g+\"-\\r\\\\' disabled='disabled'><option selected=''></option></select>\",e.querySelectorAll(\"[selected]\").length||h.push(\"\\\\[\"+D+\"*(?:value|\"+j+\")\"),e.querySelectorAll(\"[id~=\"+g+\"-]\").length||h.push(\"~=\"),e.querySelectorAll(\"a#\"+g+\"+*\").length||h.push(\".#.+[+~]\"),e.querySelectorAll(\":checked\").length||h.push(\":checked\"),(t=u.createElement(\"input\")).setAttribute(\"type\",\"hidden\"),e.appendChild(t).setAttribute(\"name\",\"D\"),c.appendChild(e).disabled=!0,2!==e.querySelectorAll(\":disabled\").length&&h.push(\":enabled\",\":disabled\"),(t=u.createElement(\"input\")).setAttribute(\"name\",\"\"),e.appendChild(t),e.querySelectorAll(\"[name='']\").length||h.push(\"\\\\[\"+D+\"*name\"+D+\"*=\"+D+\"*(?:''|\\\"\\\")\")}),p.cssHas||h.push(\":has\"),h=h.length&&new RegExp(h.join(\"|\")),A=function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e===u||e.ownerDocument==O&&Q.contains(O,e)?-1:t===u||t.ownerDocument==O&&Q.contains(O,t)?1:o?s.call(o,e)-s.call(o,t):0:4&n?-1:1)},u):u}for(t in Q.matches=function(e,t){return Q(e,null,null,t)},Q.matchesSelector=function(e,t){if(le(e),f&&!Y[t+\" \"]&&(!h||!h.test(t)))try{var n=m.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){Y(t,!0)}return Q(t,u,null,[e]).length>0},Q.contains=function(e,t){return(e.ownerDocument||e)!=u&&le(e),w.contains(e,t)},Q.attr=function(e,t){(e.ownerDocument||e)!=u&&le(e);var n=r.attrHandle[t.toLowerCase()],i=n&&d.call(r.attrHandle,t.toLowerCase())?n(e,t,!f):void 0;return void 0!==i?i:e.getAttribute(t)},Q.error=function(e){throw new Error(\"Syntax error, unrecognized expression: \"+e)},w.uniqueSort=function(e){var t,n=[],r=0,a=0;if(l=!p.sortStable,o=!p.sortStable&&i.call(e,0),T.call(e,A),l){for(;t=e[a++];)t===e[a]&&(r=n.push(a));for(;r--;)S.call(e,n[r],1)}return o=null,e},w.fn.uniqueSort=function(){return this.pushStack(w.uniqueSort(i.apply(this)))},r=w.expr={cacheLength:50,createPseudo:te,match:W,attrHandle:{},find:{},relative:{\">\":{dir:\"parentNode\",first:!0},\" \":{dir:\"parentNode\"},\"+\":{dir:\"previousSibling\",first:!0},\"~\":{dir:\"previousSibling\"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(J,Z),e[3]=(e[3]||e[4]||e[5]||\"\").replace(J,Z),\"~=\"===e[2]&&(e[3]=\" \"+e[3]+\" \"),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),\"nth\"===e[1].slice(0,3)?(e[3]||Q.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*(\"even\"===e[3]||\"odd\"===e[3])),e[5]=+(e[7]+e[8]||\"odd\"===e[3])):e[3]&&Q.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return W.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||\"\":n&&$.test(n)&&(t=de(n,!0))&&(t=n.indexOf(\")\",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(J,Z).toLowerCase();return\"*\"===e?function(){return!0}:function(e){return L(e,t)}},CLASS:function(e){var t=b[e+\" \"];return t||(t=new RegExp(\"(^|\"+D+\")\"+e+\"(\"+D+\"|$)\"))&&b(e,function(e){return t.test(\"string\"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute(\"class\")||\"\")})},ATTR:function(e,t,n){return function(r){var i=Q.attr(r,e);return null==i?\"!=\"===t:!t||(i+=\"\",\"=\"===t?i===n:\"!=\"===t?i!==n:\"^=\"===t?n&&0===i.indexOf(n):\"*=\"===t?n&&i.indexOf(n)>-1:\"$=\"===t?n&&i.slice(-n.length)===n:\"~=\"===t?(\" \"+i.replace(F,\" \")+\" \").indexOf(n)>-1:\"|=\"===t&&(i===n||i.slice(0,n.length+1)===n+\"-\"))}},CHILD:function(e,t,n,r,i){var a=\"nth\"!==e.slice(0,3),o=\"last\"!==e.slice(-4),s=\"of-type\"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,d,c,f,p,h=a!==o?\"nextSibling\":\"previousSibling\",m=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s,b=!1;if(m){if(a){for(;h;){for(c=t;c=c[h];)if(s?L(c,v):1===c.nodeType)return!1;p=h=\"only\"===e&&!p&&\"nextSibling\"}return!0}if(p=[o?m.firstChild:m.lastChild],o&&y){for(b=(f=(u=(d=m[g]||(m[g]={}))[e]||[])[0]===_&&u[1])&&u[2],c=f&&m.childNodes[f];c=++f&&c&&c[h]||(b=f=0)||p.pop();)if(1===c.nodeType&&++b&&c===t){d[e]=[_,f,b];break}}else if(y&&(b=f=(u=(d=t[g]||(t[g]={}))[e]||[])[0]===_&&u[1]),!1===b)for(;(c=++f&&c&&c[h]||(b=f=0)||p.pop())&&(!(s?L(c,v):1===c.nodeType)||!++b||(y&&((d=c[g]||(c[g]={}))[e]=[_,b]),c!==t)););return(b-=i)===r||b%r===0&&b/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||Q.error(\"unsupported pseudo: \"+e);return i[g]?i(t):i.length>1?(n=[e,e,\"\",t],r.setFilters.hasOwnProperty(e.toLowerCase())?te(function(e,n){for(var r,a=i(e,t),o=a.length;o--;)e[r=s.call(e,a[o])]=!(n[r]=a[o])}):function(e){return i(e,0,n)}):i}},pseudos:{not:te(function(e){var t=[],n=[],r=ge(e.replace(E,\"$1\"));return r[g]?te(function(e,t,n,i){for(var a,o=r(e,null,i,[]),s=e.length;s--;)(a=o[s])&&(e[s]=!(t[s]=a))}):function(e,i,a){return t[0]=e,r(t,null,a,n),t[0]=null,!n.pop()}}),has:te(function(e){return function(t){return Q(e,t).length>0}}),contains:te(function(e){return e=e.replace(J,Z),function(t){return(t.textContent||w.text(t)).indexOf(e)>-1}}),lang:te(function(e){return z.test(e||\"\")||Q.error(\"unsupported lang: \"+e),e=e.replace(J,Z).toLowerCase(),function(t){var n;do{if(n=f?t.lang:t.getAttribute(\"xml:lang\")||t.getAttribute(\"lang\"))return(n=n.toLowerCase())===e||0===n.indexOf(e+\"-\")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===c},focus:function(e){return e===function(){try{return u.activeElement}catch(e){}}()&&u.hasFocus()&&!!(e.type||e.href||~e.tabIndex)},enabled:ae(!1),disabled:ae(!0),checked:function(e){return L(e,\"input\")&&!!e.checked||L(e,\"option\")&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return B.test(e.nodeName)},input:function(e){return U.test(e.nodeName)},button:function(e){return L(e,\"input\")&&\"button\"===e.type||L(e,\"button\")},text:function(e){var t;return L(e,\"input\")&&\"text\"===e.type&&(null==(t=e.getAttribute(\"type\"))||\"text\"===t.toLowerCase())},first:oe(function(){return[0]}),last:oe(function(e,t){return[t-1]}),eq:oe(function(e,t,n){return[n<0?n+t:n]}),even:oe(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:oe(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:oe(function(e,t,n){var r;for(r=n<0?n+t:n>t?t:n;--r>=0;)e.push(r);return e}),gt:oe(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}},r.pseudos.nth=r.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=re(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=ie(t);function ue(){}function de(e,t){var n,i,a,o,s,l,u,d=M[e+\" \"];if(d)return t?0:d.slice(0);for(s=e,l=[],u=r.preFilter;s;){for(o in n&&!(i=N.exec(s))||(i&&(s=s.slice(i[0].length)||s),l.push(a=[])),n=!1,(i=R.exec(s))&&(n=i.shift(),a.push({value:n,type:i[0].replace(E,\" \")}),s=s.slice(n.length)),r.filter)!(i=W[o].exec(s))||u[o]&&!(i=u[o](i))||(n=i.shift(),a.push({value:n,type:o,matches:i}),s=s.slice(n.length));if(!n)break}return t?s.length:s?Q.error(e):M(e,l).slice(0)}function ce(e){for(var t=0,n=e.length,r=\"\";t<n;t++)r+=e[t].value;return r}function fe(e,t,n){var r=t.dir,i=t.next,a=i||r,o=n&&\"parentNode\"===a,s=y++;return t.first?function(t,n,i){for(;t=t[r];)if(1===t.nodeType||o)return e(t,n,i);return!1}:function(t,n,l){var u,d,c=[_,s];if(l){for(;t=t[r];)if((1===t.nodeType||o)&&e(t,n,l))return!0}else for(;t=t[r];)if(1===t.nodeType||o)if(d=t[g]||(t[g]={}),i&&L(t,i))t=t[r]||t;else{if((u=d[a])&&u[0]===_&&u[1]===s)return c[2]=u[2];if(d[a]=c,c[2]=e(t,n,l))return!0}return!1}}function pe(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function he(e,t,n,r,i){for(var a,o=[],s=0,l=e.length,u=null!=t;s<l;s++)(a=e[s])&&(n&&!n(a,r,i)||(o.push(a),u&&t.push(s)));return o}function me(e,t,n,r,i,a){return r&&!r[g]&&(r=me(r)),i&&!i[g]&&(i=me(i,a)),te(function(a,o,l,u){var d,c,f,p,h=[],m=[],g=o.length,_=a||function(e,t,n){for(var r=0,i=t.length;r<i;r++)Q(e,t[r],n);return n}(t||\"*\",l.nodeType?[l]:l,[]),y=!e||!a&&t?_:he(_,h,e,l,u);if(n?n(y,p=i||(a?e:g||r)?[]:o,l,u):p=y,r)for(d=he(p,m),r(d,[],l,u),c=d.length;c--;)(f=d[c])&&(p[m[c]]=!(y[m[c]]=f));if(a){if(i||e){if(i){for(d=[],c=p.length;c--;)(f=p[c])&&d.push(y[c]=f);i(null,p=[],d,u)}for(c=p.length;c--;)(f=p[c])&&(d=i?s.call(a,f):h[c])>-1&&(a[d]=!(o[d]=f))}}else p=he(p===o?p.splice(g,p.length):p),i?i(null,o,p,u):v.apply(o,p)})}function ve(e){for(var t,n,i,o=e.length,l=r.relative[e[0].type],u=l||r.relative[\" \"],d=l?1:0,c=fe(function(e){return e===t},u,!0),f=fe(function(e){return s.call(t,e)>-1},u,!0),p=[function(e,n,r){var i=!l&&(r||n!=a)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];d<o;d++)if(n=r.relative[e[d].type])p=[fe(pe(p),n)];else{if((n=r.filter[e[d].type].apply(null,e[d].matches))[g]){for(i=++d;i<o&&!r.relative[e[i].type];i++);return me(d>1&&pe(p),d>1&&ce(e.slice(0,d-1).concat({value:\" \"===e[d-2].type?\"*\":\"\"})).replace(E,\"$1\"),n,d<i&&ve(e.slice(d,i)),i<o&&ve(e=e.slice(i)),i<o&&ce(e))}p.push(n)}return pe(p)}function ge(e,t){var n,i=[],o=[],s=k[e+\" \"];if(!s){for(t||(t=de(e)),n=t.length;n--;)(s=ve(t[n]))[g]?i.push(s):o.push(s);s=k(e,function(e,t){var n=t.length>0,i=e.length>0,o=function(o,s,l,d,c){var p,h,m,g=0,y=\"0\",b=o&&[],M=[],k=a,L=o||i&&r.find.TAG(\"*\",c),T=_+=null==k?1:Math.random()||.1,S=L.length;for(c&&(a=s==u||s||c);y!==S&&null!=(p=L[y]);y++){if(i&&p){for(h=0,s||p.ownerDocument==u||(le(p),l=!f);m=e[h++];)if(m(p,s||u,l)){v.call(d,p);break}c&&(_=T)}n&&((p=!m&&p)&&g--,o&&b.push(p))}if(g+=y,n&&y!==g){for(h=0;m=t[h++];)m(b,M,s,l);if(o){if(g>0)for(;y--;)b[y]||M[y]||(M[y]=x.call(d));M=he(M)}v.apply(d,M),c&&!o&&M.length>0&&g+t.length>1&&w.uniqueSort(d)}return c&&(_=T,a=k),b};return n?te(o):o}(o,i)),s.selector=e}return s}function _e(e,t,n,i){var a,o,s,l,u,d=\"function\"==typeof e&&e,c=!i&&de(e=d.selector||e);if(n=n||[],1===c.length){if((o=c[0]=c[0].slice(0)).length>2&&\"ID\"===(s=o[0]).type&&9===t.nodeType&&f&&r.relative[o[1].type]){if(!(t=(r.find.ID(s.matches[0].replace(J,Z),t)||[])[0]))return n;d&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(a=W.needsContext.test(e)?0:o.length;a--&&(s=o[a],!r.relative[l=s.type]);)if((u=r.find[l])&&(i=u(s.matches[0].replace(J,Z),G.test(o[0].type)&&se(t.parentNode)||t))){if(o.splice(a,1),!(e=i.length&&ce(o)))return v.apply(n,i),n;break}}return(d||ge(e,c))(i,t,!f,n,!t||G.test(e)&&se(t.parentNode)||t),n}ue.prototype=r.filters=r.pseudos,r.setFilters=new ue,p.sortStable=g.split(\"\").sort(A).join(\"\")===g,le(),p.sortDetached=ne(function(e){return 1&e.compareDocumentPosition(u.createElement(\"fieldset\"))}),w.find=Q,w.expr[\":\"]=w.expr.pseudos,w.unique=w.uniqueSort,Q.compile=ge,Q.select=_e,Q.setDocument=le,Q.tokenize=de,Q.escape=w.escapeSelector,Q.getText=w.text,Q.isXML=w.isXMLDoc,Q.selectors=w.expr,Q.support=w.support,Q.uniqueSort=w.uniqueSort}();var j=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},P=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},H=w.expr.match.needsContext,I=/^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i;function F(e,t,n){return h(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):\"string\"!=typeof t?w.grep(e,function(e){return s.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=\":not(\"+e+\")\"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if(\"string\"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t<r;t++)if(w.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)w.find(e,i[t],n);return r>1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(F(this,e||[],!1))},not:function(e){return this.pushStack(F(this,e||[],!0))},is:function(e){return!!F(this,\"string\"==typeof e&&H.test(e)?w(e):e||[],!1).length}});var N,R=/^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/;(w.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||N,\"string\"==typeof e){if(!(r=\"<\"===e[0]&&\">\"===e[e.length-1]&&e.length>=3?[null,e,null]:R.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:v,!0)),I.test(r[1])&&w.isPlainObject(t))for(r in t)h(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=v.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):h(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,N=w(v);var V=/^(?:parents|prev(?:Until|All))/,$={children:!0,contents:!0,next:!0,prev:!0};function z(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(w.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,a=[],o=\"string\"!=typeof e&&w(e);if(!H.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(o?o.index(n)>-1:1===n.nodeType&&w.find.matchesSelector(n,e))){a.push(n);break}return this.pushStack(a.length>1?w.uniqueSort(a):a)},index:function(e){return e?\"string\"==typeof e?s.call(w(e),this[0]):s.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return j(e,\"parentNode\")},parentsUntil:function(e,t,n){return j(e,\"parentNode\",n)},next:function(e){return z(e,\"nextSibling\")},prev:function(e){return z(e,\"previousSibling\")},nextAll:function(e){return j(e,\"nextSibling\")},prevAll:function(e){return j(e,\"previousSibling\")},nextUntil:function(e,t,n){return j(e,\"nextSibling\",n)},prevUntil:function(e,t,n){return j(e,\"previousSibling\",n)},siblings:function(e){return P((e.parentNode||{}).firstChild,e)},children:function(e){return P(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(L(e,\"template\")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return\"Until\"!==e.slice(-5)&&(r=n),r&&\"string\"==typeof r&&(i=w.filter(r,i)),this.length>1&&($[e]||w.uniqueSort(i),V.test(e)&&i.reverse()),this.pushStack(i)}});var W=/[^\\x20\\t\\r\\n\\f]+/g;function U(e){return e}function B(e){throw e}function q(e,t,n,r){var i;try{e&&h(i=e.promise)?i.call(e).done(t).fail(n):e&&h(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.Callbacks=function(e){e=\"string\"==typeof e?function(e){var t={};return w.each(e.match(W)||[],function(e,n){t[n]=!0}),t}(e):w.extend({},e);var t,n,r,i,a=[],o=[],s=-1,l=function(){for(i=i||e.once,r=t=!0;o.length;s=-1)for(n=o.shift();++s<a.length;)!1===a[s].apply(n[0],n[1])&&e.stopOnFalse&&(s=a.length,n=!1);e.memory||(n=!1),t=!1,i&&(a=n?[]:\"\")},u={add:function(){return a&&(n&&!t&&(s=a.length-1,o.push(n)),function t(n){w.each(n,function(n,r){h(r)?e.unique&&u.has(r)||a.push(r):r&&r.length&&\"string\"!==y(r)&&t(r)})}(arguments),n&&!t&&l()),this},remove:function(){return w.each(arguments,function(e,t){for(var n;(n=w.inArray(t,a,n))>-1;)a.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,a)>-1:a.length>0},empty:function(){return a&&(a=[]),this},disable:function(){return i=o=[],a=n=\"\",this},disabled:function(){return!a},lock:function(){return i=o=[],n||t||(a=n=\"\"),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],o.push(n),t||l()),this},fire:function(){return u.fireWith(this,arguments),this},fired:function(){return!!r}};return u},w.extend({Deferred:function(t){var n=[[\"notify\",\"progress\",w.Callbacks(\"memory\"),w.Callbacks(\"memory\"),2],[\"resolve\",\"done\",w.Callbacks(\"once memory\"),w.Callbacks(\"once memory\"),0,\"resolved\"],[\"reject\",\"fail\",w.Callbacks(\"once memory\"),w.Callbacks(\"once memory\"),1,\"rejected\"]],r=\"pending\",i={state:function(){return r},always:function(){return a.done(arguments).fail(arguments),this},catch:function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=h(e[r[4]])&&e[r[4]];a[r[1]](function(){var e=i&&i.apply(this,arguments);e&&h(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+\"With\"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var a=0;function o(t,n,r,i){return function(){var s=this,l=arguments,u=function(){var e,u;if(!(t<a)){if((e=r.apply(s,l))===n.promise())throw new TypeError(\"Thenable self-resolution\");u=e&&(\"object\"==typeof e||\"function\"==typeof e)&&e.then,h(u)?i?u.call(e,o(a,n,U,i),o(a,n,B,i)):(a++,u.call(e,o(a,n,U,i),o(a,n,B,i),o(a,n,U,n.notifyWith))):(r!==U&&(s=void 0,l=[e]),(i||n.resolveWith)(s,l))}},d=i?u:function(){try{u()}catch(e){w.Deferred.exceptionHook&&w.Deferred.exceptionHook(e,d.error),t+1>=a&&(r!==B&&(s=void 0,l=[e]),n.rejectWith(s,l))}};t?d():(w.Deferred.getErrorHook?d.error=w.Deferred.getErrorHook():w.Deferred.getStackHook&&(d.error=w.Deferred.getStackHook()),e.setTimeout(d))}}return w.Deferred(function(e){n[0][3].add(o(0,e,h(i)?i:U,e.notifyWith)),n[1][3].add(o(0,e,h(t)?t:U)),n[2][3].add(o(0,e,h(r)?r:B))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},a={};return w.each(n,function(e,t){var o=t[2],s=t[5];i[t[1]]=o.add,s&&o.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),o.add(t[3].fire),a[t[0]]=function(){return a[t[0]+\"With\"](this===a?void 0:this,arguments),this},a[t[0]+\"With\"]=o.fireWith}),i.promise(a),t&&t.call(a,a),a},when:function(e){var t=arguments.length,n=t,r=Array(n),a=i.call(arguments),o=w.Deferred(),s=function(e){return function(n){r[e]=this,a[e]=arguments.length>1?i.call(arguments):n,--t||o.resolveWith(r,a)}};if(t<=1&&(q(e,o.done(s(n)).resolve,o.reject,!t),\"pending\"===o.state()||h(a[n]&&a[n].then)))return o.then();for(;n--;)q(a[n],s(n),o.reject);return o.promise()}});var G=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&G.test(t.name)&&e.console.warn(\"jQuery.Deferred exception: \"+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var J=w.Deferred();function Z(){v.removeEventListener(\"DOMContentLoaded\",Z),e.removeEventListener(\"load\",Z),w.ready()}w.fn.ready=function(e){return J.then(e).catch(function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||J.resolveWith(v,[w]))}}),w.ready.then=J.then,\"complete\"===v.readyState||\"loading\"!==v.readyState&&!v.documentElement.doScroll?e.setTimeout(w.ready):(v.addEventListener(\"DOMContentLoaded\",Z),e.addEventListener(\"load\",Z));var K=function(e,t,n,r,i,a,o){var s=0,l=e.length,u=null==n;if(\"object\"===y(n))for(s in i=!0,n)K(e,t,s,n[s],!0,a,o);else if(void 0!==r&&(i=!0,h(r)||(o=!0),u&&(o?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(w(e),n)})),t))for(;s<l;s++)t(e[s],n,o?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):a},X=/^-ms-/,Q=/-([a-z])/g;function ee(e,t){return t.toUpperCase()}function te(e){return e.replace(X,\"ms-\").replace(Q,ee)}var ne=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function re(){this.expando=w.expando+re.uid++}re.uid=1,re.prototype={cache:function(e){var t=e[this.expando];return t||(t={},ne(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if(\"string\"==typeof t)i[te(t)]=n;else for(r in t)i[te(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][te(t)]},access:function(e,t,n){return void 0===t||t&&\"string\"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(te):(t=te(t))in r?[t]:t.match(W)||[]).length;for(;n--;)delete r[t[n]]}(void 0===t||w.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!w.isEmptyObject(t)}};var ie=new re,ae=new re,oe=/^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,se=/[A-Z]/g;function le(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r=\"data-\"+t.replace(se,\"-$&\").toLowerCase(),\"string\"==typeof(n=e.getAttribute(r))){try{n=function(e){return\"true\"===e||\"false\"!==e&&(\"null\"===e?null:e===+e+\"\"?+e:oe.test(e)?JSON.parse(e):e)}(n)}catch(e){}ae.set(e,t,n)}else n=void 0;return n}w.extend({hasData:function(e){return ae.hasData(e)||ie.hasData(e)},data:function(e,t,n){return ae.access(e,t,n)},removeData:function(e,t){ae.remove(e,t)},_data:function(e,t,n){return ie.access(e,t,n)},_removeData:function(e,t){ie.remove(e,t)}}),w.fn.extend({data:function(e,t){var n,r,i,a=this[0],o=a&&a.attributes;if(void 0===e){if(this.length&&(i=ae.get(a),1===a.nodeType&&!ie.get(a,\"hasDataAttrs\"))){for(n=o.length;n--;)o[n]&&0===(r=o[n].name).indexOf(\"data-\")&&(r=te(r.slice(5)),le(a,r,i[r]));ie.set(a,\"hasDataAttrs\",!0)}return i}return\"object\"==typeof e?this.each(function(){ae.set(this,e)}):K(this,function(t){var n;if(a&&void 0===t)return void 0!==(n=ae.get(a,e))||void 0!==(n=le(a,e))?n:void 0;this.each(function(){ae.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){ae.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||\"fx\")+\"queue\",r=ie.get(e,t),n&&(!r||Array.isArray(n)?r=ie.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||\"fx\";var n=w.queue(e,t),r=n.length,i=n.shift(),a=w._queueHooks(e,t);\"inprogress\"===i&&(i=n.shift(),r--),i&&(\"fx\"===t&&n.unshift(\"inprogress\"),delete a.stop,i.call(e,function(){w.dequeue(e,t)},a)),!r&&a&&a.empty.fire()},_queueHooks:function(e,t){var n=t+\"queueHooks\";return ie.get(e,n)||ie.access(e,n,{empty:w.Callbacks(\"once memory\").add(function(){ie.remove(e,[t+\"queue\",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return\"string\"!=typeof e&&(t=e,e=\"fx\",n--),arguments.length<n?w.queue(this[0],e):void 0===t?this:this.each(function(){var n=w.queue(this,e,t);w._queueHooks(this,e),\"fx\"===e&&\"inprogress\"!==n[0]&&w.dequeue(this,e)})},dequeue:function(e){return this.each(function(){w.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||\"fx\",[])},promise:function(e,t){var n,r=1,i=w.Deferred(),a=this,o=this.length,s=function(){--r||i.resolveWith(a,[a])};for(\"string\"!=typeof e&&(t=e,e=void 0),e=e||\"fx\";o--;)(n=ie.get(a[o],e+\"queueHooks\"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var ue=/[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/.source,de=new RegExp(\"^(?:([+-])=|)(\"+ue+\")([a-z%]*)$\",\"i\"),ce=[\"Top\",\"Right\",\"Bottom\",\"Left\"],fe=v.documentElement,pe=function(e){return w.contains(e.ownerDocument,e)},he={composed:!0};fe.getRootNode&&(pe=function(e){return w.contains(e.ownerDocument,e)||e.getRootNode(he)===e.ownerDocument});var me=function(e,t){return\"none\"===(e=t||e).style.display||\"\"===e.style.display&&pe(e)&&\"none\"===w.css(e,\"display\")};function ve(e,t,n,r){var i,a,o=20,s=r?function(){return r.cur()}:function(){return w.css(e,t,\"\")},l=s(),u=n&&n[3]||(w.cssNumber[t]?\"\":\"px\"),d=e.nodeType&&(w.cssNumber[t]||\"px\"!==u&&+l)&&de.exec(w.css(e,t));if(d&&d[3]!==u){for(l/=2,u=u||d[3],d=+l||1;o--;)w.style(e,t,d+u),(1-a)*(1-(a=s()/l||.5))<=0&&(o=0),d/=a;d*=2,w.style(e,t,d+u),n=n||[]}return n&&(d=+d||+l||0,i=n[1]?d+(n[1]+1)*n[2]:+n[2],r&&(r.unit=u,r.start=d,r.end=i)),i}var ge={};function _e(e){var t,n=e.ownerDocument,r=e.nodeName,i=ge[r];return i||(t=n.body.appendChild(n.createElement(r)),i=w.css(t,\"display\"),t.parentNode.removeChild(t),\"none\"===i&&(i=\"block\"),ge[r]=i,i)}function ye(e,t){for(var n,r,i=[],a=0,o=e.length;a<o;a++)(r=e[a]).style&&(n=r.style.display,t?(\"none\"===n&&(i[a]=ie.get(r,\"display\")||null,i[a]||(r.style.display=\"\")),\"\"===r.style.display&&me(r)&&(i[a]=_e(r))):\"none\"!==n&&(i[a]=\"none\",ie.set(r,\"display\",n)));for(a=0;a<o;a++)null!=i[a]&&(e[a].style.display=i[a]);return e}w.fn.extend({show:function(){return ye(this,!0)},hide:function(){return ye(this)},toggle:function(e){return\"boolean\"==typeof e?e?this.show():this.hide():this.each(function(){me(this)?w(this).show():w(this).hide()})}});var be,Me,we=/^(?:checkbox|radio)$/i,ke=/<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)/i,Le=/^$|^module$|\\/(?:java|ecma)script/i;be=v.createDocumentFragment().appendChild(v.createElement(\"div\")),(Me=v.createElement(\"input\")).setAttribute(\"type\",\"radio\"),Me.setAttribute(\"checked\",\"checked\"),Me.setAttribute(\"name\",\"t\"),be.appendChild(Me),p.checkClone=be.cloneNode(!0).cloneNode(!0).lastChild.checked,be.innerHTML=\"<textarea>x</textarea>\",p.noCloneChecked=!!be.cloneNode(!0).lastChild.defaultValue,be.innerHTML=\"<option></option>\",p.option=!!be.lastChild;var xe={thead:[1,\"<table>\",\"</table>\"],col:[2,\"<table><colgroup>\",\"</colgroup></table>\"],tr:[2,\"<table><tbody>\",\"</tbody></table>\"],td:[3,\"<table><tbody><tr>\",\"</tr></tbody></table>\"],_default:[0,\"\",\"\"]};function Te(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||\"*\"):void 0!==e.querySelectorAll?e.querySelectorAll(t||\"*\"):[],void 0===t||t&&L(e,t)?w.merge([e],n):n}function Se(e,t){for(var n=0,r=e.length;n<r;n++)ie.set(e[n],\"globalEval\",!t||ie.get(t[n],\"globalEval\"))}xe.tbody=xe.tfoot=xe.colgroup=xe.caption=xe.thead,xe.th=xe.td,p.option||(xe.optgroup=xe.option=[1,\"<select multiple='multiple'>\",\"</select>\"]);var De=/<|&#?\\w+;/;function Ee(e,t,n,r,i){for(var a,o,s,l,u,d,c=t.createDocumentFragment(),f=[],p=0,h=e.length;p<h;p++)if((a=e[p])||0===a)if(\"object\"===y(a))w.merge(f,a.nodeType?[a]:a);else if(De.test(a)){for(o=o||c.appendChild(t.createElement(\"div\")),s=(ke.exec(a)||[\"\",\"\"])[1].toLowerCase(),l=xe[s]||xe._default,o.innerHTML=l[1]+w.htmlPrefilter(a)+l[2],d=l[0];d--;)o=o.lastChild;w.merge(f,o.childNodes),(o=c.firstChild).textContent=\"\"}else f.push(t.createTextNode(a));for(c.textContent=\"\",p=0;a=f[p++];)if(r&&w.inArray(a,r)>-1)i&&i.push(a);else if(u=pe(a),o=Te(c.appendChild(a),\"script\"),u&&Se(o),n)for(d=0;a=o[d++];)Le.test(a.type||\"\")&&n.push(a);return c}var Ye=/^([^.]*)(?:\\.(.+)|)/;function Ae(){return!0}function Oe(){return!1}function Ce(e,t,n,r,i,a){var o,s;if(\"object\"==typeof t){for(s in\"string\"!=typeof n&&(r=r||n,n=void 0),t)Ce(e,s,n,r,t[s],a);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&(\"string\"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Oe;else if(!i)return e;return 1===a&&(o=i,i=function(e){return w().off(e),o.apply(this,arguments)},i.guid=o.guid||(o.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}function je(e,t,n){n?(ie.set(e,t,!1),w.event.add(e,t,{namespace:!1,handler:function(e){var n,r=ie.get(this,t);if(1&e.isTrigger&&this[t]){if(r)(w.event.special[t]||{}).delegateType&&e.stopPropagation();else if(r=i.call(arguments),ie.set(this,t,r),this[t](),n=ie.get(this,t),ie.set(this,t,!1),r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n}else r&&(ie.set(this,t,w.event.trigger(r[0],r.slice(1),this)),e.stopPropagation(),e.isImmediatePropagationStopped=Ae)}})):void 0===ie.get(e,t)&&w.event.add(e,t,Ae)}w.event={global:{},add:function(e,t,n,r,i){var a,o,s,l,u,d,c,f,p,h,m,v=ie.get(e);if(ne(e))for(n.handler&&(n=(a=n).handler,i=a.selector),i&&w.find.matchesSelector(fe,i),n.guid||(n.guid=w.guid++),(l=v.events)||(l=v.events=Object.create(null)),(o=v.handle)||(o=v.handle=function(t){return void 0!==w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),u=(t=(t||\"\").match(W)||[\"\"]).length;u--;)p=m=(s=Ye.exec(t[u])||[])[1],h=(s[2]||\"\").split(\".\").sort(),p&&(c=w.event.special[p]||{},p=(i?c.delegateType:c.bindType)||p,c=w.event.special[p]||{},d=w.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(\".\")},a),(f=l[p])||((f=l[p]=[]).delegateCount=0,c.setup&&!1!==c.setup.call(e,r,h,o)||e.addEventListener&&e.addEventListener(p,o)),c.add&&(c.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),w.event.global[p]=!0)},remove:function(e,t,n,r,i){var a,o,s,l,u,d,c,f,p,h,m,v=ie.hasData(e)&&ie.get(e);if(v&&(l=v.events)){for(u=(t=(t||\"\").match(W)||[\"\"]).length;u--;)if(p=m=(s=Ye.exec(t[u])||[])[1],h=(s[2]||\"\").split(\".\").sort(),p){for(c=w.event.special[p]||{},f=l[p=(r?c.delegateType:c.bindType)||p]||[],s=s[2]&&new RegExp(\"(^|\\\\.)\"+h.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"),o=a=f.length;a--;)d=f[a],!i&&m!==d.origType||n&&n.guid!==d.guid||s&&!s.test(d.namespace)||r&&r!==d.selector&&(\"**\"!==r||!d.selector)||(f.splice(a,1),d.selector&&f.delegateCount--,c.remove&&c.remove.call(e,d));o&&!f.length&&(c.teardown&&!1!==c.teardown.call(e,h,v.handle)||w.removeEvent(e,p,v.handle),delete l[p])}else for(p in l)w.event.remove(e,p+t[u],n,r,!0);w.isEmptyObject(l)&&ie.remove(e,\"handle events\")}},dispatch:function(e){var t,n,r,i,a,o,s=new Array(arguments.length),l=w.event.fix(e),u=(ie.get(this,\"events\")||Object.create(null))[l.type]||[],d=w.event.special[l.type]||{};for(s[0]=l,t=1;t<arguments.length;t++)s[t]=arguments[t];if(l.delegateTarget=this,!d.preDispatch||!1!==d.preDispatch.call(this,l)){for(o=w.event.handlers.call(this,l,u),t=0;(i=o[t++])&&!l.isPropagationStopped();)for(l.currentTarget=i.elem,n=0;(a=i.handlers[n++])&&!l.isImmediatePropagationStopped();)l.rnamespace&&!1!==a.namespace&&!l.rnamespace.test(a.namespace)||(l.handleObj=a,l.data=a.data,void 0!==(r=((w.event.special[a.origType]||{}).handle||a.handler).apply(i.elem,s))&&!1===(l.result=r)&&(l.preventDefault(),l.stopPropagation()));return d.postDispatch&&d.postDispatch.call(this,l),l.result}},handlers:function(e,t){var n,r,i,a,o,s=[],l=t.delegateCount,u=e.target;if(l&&u.nodeType&&!(\"click\"===e.type&&e.button>=1))for(;u!==this;u=u.parentNode||this)if(1===u.nodeType&&(\"click\"!==e.type||!0!==u.disabled)){for(a=[],o={},n=0;n<l;n++)void 0===o[i=(r=t[n]).selector+\" \"]&&(o[i]=r.needsContext?w(i,this).index(u)>-1:w.find(i,this,null,[u]).length),o[i]&&a.push(r);a.length&&s.push({elem:u,handlers:a})}return u=this,l<t.length&&s.push({elem:u,handlers:t.slice(l)}),s},addProp:function(e,t){Object.defineProperty(w.Event.prototype,e,{enumerable:!0,configurable:!0,get:h(t)?function(){if(this.originalEvent)return t(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[e]},set:function(t){Object.defineProperty(this,e,{enumerable:!0,configurable:!0,writable:!0,value:t})}})},fix:function(e){return e[w.expando]?e:new w.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return we.test(t.type)&&t.click&&L(t,\"input\")&&je(t,\"click\",!0),!1},trigger:function(e){var t=this||e;return we.test(t.type)&&t.click&&L(t,\"input\")&&je(t,\"click\"),!0},_default:function(e){var t=e.target;return we.test(t.type)&&t.click&&L(t,\"input\")&&ie.get(t,\"click\")||L(t,\"a\")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},w.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},w.Event=function(e,t){if(!(this instanceof w.Event))return new w.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?Ae:Oe,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&w.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[w.expando]=!0},w.Event.prototype={constructor:w.Event,isDefaultPrevented:Oe,isPropagationStopped:Oe,isImmediatePropagationStopped:Oe,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=Ae,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=Ae,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=Ae,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},w.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,char:!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:!0},w.event.addProp),w.each({focus:\"focusin\",blur:\"focusout\"},function(e,t){function n(e){if(v.documentMode){var n=ie.get(this,\"handle\"),r=w.event.fix(e);r.type=\"focusin\"===e.type?\"focus\":\"blur\",r.isSimulated=!0,n(e),r.target===r.currentTarget&&n(r)}else w.event.simulate(t,e.target,w.event.fix(e))}w.event.special[e]={setup:function(){var r;if(je(this,e,!0),!v.documentMode)return!1;(r=ie.get(this,t))||this.addEventListener(t,n),ie.set(this,t,(r||0)+1)},trigger:function(){return je(this,e),!0},teardown:function(){var e;if(!v.documentMode)return!1;(e=ie.get(this,t)-1)?ie.set(this,t,e):(this.removeEventListener(t,n),ie.remove(this,t))},_default:function(t){return ie.get(t.target,e)},delegateType:t},w.event.special[t]={setup:function(){var r=this.ownerDocument||this.document||this,i=v.documentMode?this:r,a=ie.get(i,t);a||(v.documentMode?this.addEventListener(t,n):r.addEventListener(e,n,!0)),ie.set(i,t,(a||0)+1)},teardown:function(){var r=this.ownerDocument||this.document||this,i=v.documentMode?this:r,a=ie.get(i,t)-1;a?ie.set(i,t,a):(v.documentMode?this.removeEventListener(t,n):r.removeEventListener(e,n,!0),ie.remove(i,t))}}}),w.each({mouseenter:\"mouseover\",mouseleave:\"mouseout\",pointerenter:\"pointerover\",pointerleave:\"pointerout\"},function(e,t){w.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=e.relatedTarget,i=e.handleObj;return r&&(r===this||w.contains(this,r))||(e.type=i.origType,n=i.handler.apply(this,arguments),e.type=t),n}}}),w.fn.extend({on:function(e,t,n,r){return Ce(this,e,t,n,r)},one:function(e,t,n,r){return Ce(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,w(e.delegateTarget).off(r.namespace?r.origType+\".\"+r.namespace:r.origType,r.selector,r.handler),this;if(\"object\"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&\"function\"!=typeof t||(n=t,t=void 0),!1===n&&(n=Oe),this.each(function(){w.event.remove(this,e,n,t)})}});var Pe=/<script|<style|<link/i,He=/checked\\s*(?:[^=]|=\\s*.checked.)/i,Ie=/^\\s*<!\\[CDATA\\[|\\]\\]>\\s*$/g;function Fe(e,t){return L(e,\"table\")&&L(11!==t.nodeType?t:t.firstChild,\"tr\")&&w(e).children(\"tbody\")[0]||e}function Ne(e){return e.type=(null!==e.getAttribute(\"type\"))+\"/\"+e.type,e}function Re(e){return\"true/\"===(e.type||\"\").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute(\"type\"),e}function Ve(e,t){var n,r,i,a,o,s;if(1===t.nodeType){if(ie.hasData(e)&&(s=ie.get(e).events))for(i in ie.remove(t,\"handle events\"),s)for(n=0,r=s[i].length;n<r;n++)w.event.add(t,i,s[i][n]);ae.hasData(e)&&(a=ae.access(e),o=w.extend({},a),ae.set(t,o))}}function $e(e,t){var n=t.nodeName.toLowerCase();\"input\"===n&&we.test(e.type)?t.checked=e.checked:\"input\"!==n&&\"textarea\"!==n||(t.defaultValue=e.defaultValue)}function ze(e,t,n,r){t=a(t);var i,o,s,l,u,d,c=0,f=e.length,m=f-1,v=t[0],g=h(v);if(g||f>1&&\"string\"==typeof v&&!p.checkClone&&He.test(v))return e.each(function(i){var a=e.eq(i);g&&(t[0]=v.call(this,i,a.html())),ze(a,t,n,r)});if(f&&(o=(i=Ee(t,e[0].ownerDocument,!1,e,r)).firstChild,1===i.childNodes.length&&(i=o),o||r)){for(l=(s=w.map(Te(i,\"script\"),Ne)).length;c<f;c++)u=i,c!==m&&(u=w.clone(u,!0,!0),l&&w.merge(s,Te(u,\"script\"))),n.call(e[c],u,c);if(l)for(d=s[s.length-1].ownerDocument,w.map(s,Re),c=0;c<l;c++)u=s[c],Le.test(u.type||\"\")&&!ie.access(u,\"globalEval\")&&w.contains(d,u)&&(u.src&&\"module\"!==(u.type||\"\").toLowerCase()?w._evalUrl&&!u.noModule&&w._evalUrl(u.src,{nonce:u.nonce||u.getAttribute(\"nonce\")},d):_(u.textContent.replace(Ie,\"\"),u,d))}return e}function We(e,t,n){for(var r,i=t?w.filter(t,e):e,a=0;null!=(r=i[a]);a++)n||1!==r.nodeType||w.cleanData(Te(r)),r.parentNode&&(n&&pe(r)&&Se(Te(r,\"script\")),r.parentNode.removeChild(r));return e}w.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,a,o,s=e.cloneNode(!0),l=pe(e);if(!(p.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(o=Te(s),r=0,i=(a=Te(e)).length;r<i;r++)$e(a[r],o[r]);if(t)if(n)for(a=a||Te(e),o=o||Te(s),r=0,i=a.length;r<i;r++)Ve(a[r],o[r]);else Ve(e,s);return(o=Te(s,\"script\")).length>0&&Se(o,!l&&Te(e,\"script\")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,a=0;void 0!==(n=e[a]);a++)if(ne(n)){if(t=n[ie.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[ie.expando]=void 0}n[ae.expando]&&(n[ae.expando]=void 0)}}}),w.fn.extend({detach:function(e){return We(this,e,!0)},remove:function(e){return We(this,e)},text:function(e){return K(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return ze(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Fe(this,e).appendChild(e)})},prepend:function(){return ze(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Fe(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return ze(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return ze(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(Te(e,!1)),e.textContent=\"\");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return K(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if(\"string\"==typeof e&&!Pe.test(e)&&!xe[(ke.exec(e)||[\"\",\"\"])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(w.cleanData(Te(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=[];return ze(this,arguments,function(t){var n=this.parentNode;w.inArray(this,e)<0&&(w.cleanData(Te(this)),n&&n.replaceChild(t,this))},e)}}),w.each({appendTo:\"append\",prependTo:\"prepend\",insertBefore:\"before\",insertAfter:\"after\",replaceAll:\"replaceWith\"},function(e,t){w.fn[e]=function(e){for(var n,r=[],i=w(e),a=i.length-1,s=0;s<=a;s++)n=s===a?this:this.clone(!0),w(i[s])[t](n),o.apply(r,n.get());return this.pushStack(r)}});var Ue=new RegExp(\"^(\"+ue+\")(?!px)[a-z%]+$\",\"i\"),Be=/^--/,qe=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},Ge=function(e,t,n){var r,i,a={};for(i in t)a[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=a[i];return r},Je=new RegExp(ce.join(\"|\"),\"i\");function Ze(e,t,n){var r,i,a,o,s=Be.test(t),l=e.style;return(n=n||qe(e))&&(o=n.getPropertyValue(t)||n[t],s&&o&&(o=o.replace(E,\"$1\")||void 0),\"\"!==o||pe(e)||(o=w.style(e,t)),!p.pixelBoxStyles()&&Ue.test(o)&&Je.test(t)&&(r=l.width,i=l.minWidth,a=l.maxWidth,l.minWidth=l.maxWidth=l.width=o,o=n.width,l.width=r,l.minWidth=i,l.maxWidth=a)),void 0!==o?o+\"\":o}function Ke(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function t(){if(d){u.style.cssText=\"position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0\",d.style.cssText=\"position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%\",fe.appendChild(u).appendChild(d);var t=e.getComputedStyle(d);r=\"1%\"!==t.top,l=12===n(t.marginLeft),d.style.right=\"60%\",o=36===n(t.right),i=36===n(t.width),d.style.position=\"absolute\",a=12===n(d.offsetWidth/3),fe.removeChild(u),d=null}}function n(e){return Math.round(parseFloat(e))}var r,i,a,o,s,l,u=v.createElement(\"div\"),d=v.createElement(\"div\");d.style&&(d.style.backgroundClip=\"content-box\",d.cloneNode(!0).style.backgroundClip=\"\",p.clearCloneStyle=\"content-box\"===d.style.backgroundClip,w.extend(p,{boxSizingReliable:function(){return t(),i},pixelBoxStyles:function(){return t(),o},pixelPosition:function(){return t(),r},reliableMarginLeft:function(){return t(),l},scrollboxSize:function(){return t(),a},reliableTrDimensions:function(){var t,n,r,i;return null==s&&(t=v.createElement(\"table\"),n=v.createElement(\"tr\"),r=v.createElement(\"div\"),t.style.cssText=\"position:absolute;left:-11111px;border-collapse:separate\",n.style.cssText=\"box-sizing:content-box;border:1px solid\",n.style.height=\"1px\",r.style.height=\"9px\",r.style.display=\"block\",fe.appendChild(t).appendChild(n).appendChild(r),i=e.getComputedStyle(n),s=parseInt(i.height,10)+parseInt(i.borderTopWidth,10)+parseInt(i.borderBottomWidth,10)===n.offsetHeight,fe.removeChild(t)),s}}))}();var Xe=[\"Webkit\",\"Moz\",\"ms\"],Qe=v.createElement(\"div\").style,et={};function tt(e){var t=w.cssProps[e]||et[e];return t||(e in Qe?e:et[e]=function(e){for(var t=e[0].toUpperCase()+e.slice(1),n=Xe.length;n--;)if((e=Xe[n]+t)in Qe)return e}(e)||e)}var nt=/^(none|table(?!-c[ea]).+)/,rt={position:\"absolute\",visibility:\"hidden\",display:\"block\"},it={letterSpacing:\"0\",fontWeight:\"400\"};function at(e,t,n){var r=de.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||\"px\"):t}function ot(e,t,n,r,i,a){var o=\"width\"===t?1:0,s=0,l=0,u=0;if(n===(r?\"border\":\"content\"))return 0;for(;o<4;o+=2)\"margin\"===n&&(u+=w.css(e,n+ce[o],!0,i)),r?(\"content\"===n&&(l-=w.css(e,\"padding\"+ce[o],!0,i)),\"margin\"!==n&&(l-=w.css(e,\"border\"+ce[o]+\"Width\",!0,i))):(l+=w.css(e,\"padding\"+ce[o],!0,i),\"padding\"!==n?l+=w.css(e,\"border\"+ce[o]+\"Width\",!0,i):s+=w.css(e,\"border\"+ce[o]+\"Width\",!0,i));return!r&&a>=0&&(l+=Math.max(0,Math.ceil(e[\"offset\"+t[0].toUpperCase()+t.slice(1)]-a-l-s-.5))||0),l+u}function st(e,t,n){var r=qe(e),i=(!p.boxSizingReliable()||n)&&\"border-box\"===w.css(e,\"boxSizing\",!1,r),a=i,o=Ze(e,t,r),s=\"offset\"+t[0].toUpperCase()+t.slice(1);if(Ue.test(o)){if(!n)return o;o=\"auto\"}return(!p.boxSizingReliable()&&i||!p.reliableTrDimensions()&&L(e,\"tr\")||\"auto\"===o||!parseFloat(o)&&\"inline\"===w.css(e,\"display\",!1,r))&&e.getClientRects().length&&(i=\"border-box\"===w.css(e,\"boxSizing\",!1,r),(a=s in e)&&(o=e[s])),(o=parseFloat(o)||0)+ot(e,t,n||(i?\"border\":\"content\"),a,r,o)+\"px\"}function lt(e,t,n,r,i){return new lt.prototype.init(e,t,n,r,i)}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Ze(e,\"opacity\");return\"\"===n?\"1\":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,a,o,s=te(t),l=Be.test(t),u=e.style;if(l||(t=tt(s)),o=w.cssHooks[t]||w.cssHooks[s],void 0===n)return o&&\"get\"in o&&void 0!==(i=o.get(e,!1,r))?i:u[t];\"string\"===(a=typeof n)&&(i=de.exec(n))&&i[1]&&(n=ve(e,t,i),a=\"number\"),null!=n&&n==n&&(\"number\"!==a||l||(n+=i&&i[3]||(w.cssNumber[s]?\"\":\"px\")),p.clearCloneStyle||\"\"!==n||0!==t.indexOf(\"background\")||(u[t]=\"inherit\"),o&&\"set\"in o&&void 0===(n=o.set(e,n,r))||(l?u.setProperty(t,n):u[t]=n))}},css:function(e,t,n,r){var i,a,o,s=te(t);return Be.test(t)||(t=tt(s)),(o=w.cssHooks[t]||w.cssHooks[s])&&\"get\"in o&&(i=o.get(e,!0,n)),void 0===i&&(i=Ze(e,t,r)),\"normal\"===i&&t in it&&(i=it[t]),\"\"===n||n?(a=parseFloat(i),!0===n||isFinite(a)?a||0:i):i}}),w.each([\"height\",\"width\"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!nt.test(w.css(e,\"display\"))||e.getClientRects().length&&e.getBoundingClientRect().width?st(e,t,r):Ge(e,rt,function(){return st(e,t,r)})},set:function(e,n,r){var i,a=qe(e),o=!p.scrollboxSize()&&\"absolute\"===a.position,s=(o||r)&&\"border-box\"===w.css(e,\"boxSizing\",!1,a),l=r?ot(e,t,r,s,a):0;return s&&o&&(l-=Math.ceil(e[\"offset\"+t[0].toUpperCase()+t.slice(1)]-parseFloat(a[t])-ot(e,t,\"border\",!1,a)-.5)),l&&(i=de.exec(n))&&\"px\"!==(i[3]||\"px\")&&(e.style[t]=n,n=w.css(e,t)),at(0,n,l)}}}),w.cssHooks.marginLeft=Ke(p.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Ze(e,\"marginLeft\"))||e.getBoundingClientRect().left-Ge(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+\"px\"}),w.each({margin:\"\",padding:\"\",border:\"Width\"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},a=\"string\"==typeof n?n.split(\" \"):[n];r<4;r++)i[e+ce[r]+t]=a[r]||a[r-2]||a[0];return i}},\"margin\"!==e&&(w.cssHooks[e+t].set=at)}),w.fn.extend({css:function(e,t){return K(this,function(e,t,n){var r,i,a={},o=0;if(Array.isArray(t)){for(r=qe(e),i=t.length;o<i;o++)a[t[o]]=w.css(e,t[o],!1,r);return a}return void 0!==n?w.style(e,t,n):w.css(e,t)},e,t,arguments.length>1)}}),w.Tween=lt,lt.prototype={constructor:lt,init:function(e,t,n,r,i,a){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=a||(w.cssNumber[n]?\"\":\"px\")},cur:function(){var e=lt.propHooks[this.prop];return e&&e.get?e.get(this):lt.propHooks._default.get(this)},run:function(e){var t,n=lt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):lt.propHooks._default.set(this),this}},lt.prototype.init.prototype=lt.prototype,lt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,\"\"))&&\"auto\"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||!w.cssHooks[e.prop]&&null==e.elem.style[tt(e.prop)]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},lt.propHooks.scrollTop=lt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:\"swing\"},w.fx=lt.prototype.init,w.fx.step={};var ut,dt,ct=/^(?:toggle|show|hide)$/,ft=/queueHooks$/;function pt(){dt&&(!1===v.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(pt):e.setTimeout(pt,w.fx.interval),w.fx.tick())}function ht(){return e.setTimeout(function(){ut=void 0}),ut=Date.now()}function mt(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i[\"margin\"+(n=ce[r])]=i[\"padding\"+n]=e;return t&&(i.opacity=i.width=e),i}function vt(e,t,n){for(var r,i=(gt.tweeners[t]||[]).concat(gt.tweeners[\"*\"]),a=0,o=i.length;a<o;a++)if(r=i[a].call(n,t,e))return r}function gt(e,t,n){var r,i,a=0,o=gt.prefilters.length,s=w.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=ut||ht(),n=Math.max(0,u.startTime+u.duration-t),r=1-(n/u.duration||0),a=0,o=u.tweens.length;a<o;a++)u.tweens[a].run(r);return s.notifyWith(e,[u,r,n]),r<1&&o?n:(o||s.notifyWith(e,[u,1,0]),s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:w.extend({},t),opts:w.extend(!0,{specialEasing:{},easing:w.easing._default},n),originalProperties:t,originalOptions:n,startTime:ut||ht(),duration:n.duration,tweens:[],createTween:function(t,n){var r=w.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;n<r;n++)u.tweens[n].run(1);return t?(s.notifyWith(e,[u,1,0]),s.resolveWith(e,[u,t])):s.rejectWith(e,[u,t]),this}}),d=u.props;for(!function(e,t){var n,r,i,a,o;for(n in e)if(i=t[r=te(n)],a=e[n],Array.isArray(a)&&(i=a[1],a=e[n]=a[0]),n!==r&&(e[r]=a,delete e[n]),(o=w.cssHooks[r])&&\"expand\"in o)for(n in a=o.expand(a),delete e[r],a)n in e||(e[n]=a[n],t[n]=i);else t[r]=i}(d,u.opts.specialEasing);a<o;a++)if(r=gt.prefilters[a].call(u,e,d,u.opts))return h(r.stop)&&(w._queueHooks(u.elem,u.opts.queue).stop=r.stop.bind(r)),r;return w.map(d,vt,u),h(u.opts.start)&&u.opts.start.call(e,u),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always),w.fx.timer(w.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u}w.Animation=w.extend(gt,{tweeners:{\"*\":[function(e,t){var n=this.createTween(e,t);return ve(n.elem,e,de.exec(t),n),n}]},tweener:function(e,t){h(e)?(t=e,e=[\"*\"]):e=e.match(W);for(var n,r=0,i=e.length;r<i;r++)n=e[r],gt.tweeners[n]=gt.tweeners[n]||[],gt.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,a,o,s,l,u,d,c=\"width\"in t||\"height\"in t,f=this,p={},h=e.style,m=e.nodeType&&me(e),v=ie.get(e,\"fxshow\");for(r in n.queue||(null==(o=w._queueHooks(e,\"fx\")).unqueued&&(o.unqueued=0,s=o.empty.fire,o.empty.fire=function(){o.unqueued||s()}),o.unqueued++,f.always(function(){f.always(function(){o.unqueued--,w.queue(e,\"fx\").length||o.empty.fire()})})),t)if(i=t[r],ct.test(i)){if(delete t[r],a=a||\"toggle\"===i,i===(m?\"hide\":\"show\")){if(\"show\"!==i||!v||void 0===v[r])continue;m=!0}p[r]=v&&v[r]||w.style(e,r)}if((l=!w.isEmptyObject(t))||!w.isEmptyObject(p))for(r in c&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(u=v&&v.display)&&(u=ie.get(e,\"display\")),\"none\"===(d=w.css(e,\"display\"))&&(u?d=u:(ye([e],!0),u=e.style.display||u,d=w.css(e,\"display\"),ye([e]))),(\"inline\"===d||\"inline-block\"===d&&null!=u)&&\"none\"===w.css(e,\"float\")&&(l||(f.done(function(){h.display=u}),null==u&&(d=h.display,u=\"none\"===d?\"\":d)),h.display=\"inline-block\")),n.overflow&&(h.overflow=\"hidden\",f.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),l=!1,p)l||(v?\"hidden\"in v&&(m=v.hidden):v=ie.access(e,\"fxshow\",{display:u}),a&&(v.hidden=!m),m&&ye([e],!0),f.done(function(){for(r in m||ye([e]),ie.remove(e,\"fxshow\"),p)w.style(e,r,p[r])})),l=vt(m?v[r]:0,r,f),r in v||(v[r]=l.start,m&&(l.end=l.start,l.start=0))}],prefilter:function(e,t){t?gt.prefilters.unshift(e):gt.prefilters.push(e)}}),w.speed=function(e,t,n){var r=e&&\"object\"==typeof e?w.extend({},e):{complete:n||!n&&t||h(e)&&e,duration:e,easing:n&&t||t&&!h(t)&&t};return w.fx.off?r.duration=0:\"number\"!=typeof r.duration&&(r.duration in w.fx.speeds?r.duration=w.fx.speeds[r.duration]:r.duration=w.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue=\"fx\"),r.old=r.complete,r.complete=function(){h(r.old)&&r.old.call(this),r.queue&&w.dequeue(this,r.queue)},r},w.fn.extend({fadeTo:function(e,t,n,r){return this.filter(me).css(\"opacity\",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=w.isEmptyObject(e),a=w.speed(t,n,r),o=function(){var t=gt(this,w.extend({},e),a);(i||ie.get(this,\"finish\"))&&t.stop(!0)};return o.finish=o,i||!1===a.queue?this.each(o):this.queue(a.queue,o)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return\"string\"!=typeof e&&(n=t,t=e,e=void 0),t&&this.queue(e||\"fx\",[]),this.each(function(){var t=!0,i=null!=e&&e+\"queueHooks\",a=w.timers,o=ie.get(this);if(i)o[i]&&o[i].stop&&r(o[i]);else for(i in o)o[i]&&o[i].stop&&ft.test(i)&&r(o[i]);for(i=a.length;i--;)a[i].elem!==this||null!=e&&a[i].queue!==e||(a[i].anim.stop(n),t=!1,a.splice(i,1));!t&&n||w.dequeue(this,e)})},finish:function(e){return!1!==e&&(e=e||\"fx\"),this.each(function(){var t,n=ie.get(this),r=n[e+\"queue\"],i=n[e+\"queueHooks\"],a=w.timers,o=r?r.length:0;for(n.finish=!0,w.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=a.length;t--;)a[t].elem===this&&a[t].queue===e&&(a[t].anim.stop(!0),a.splice(t,1));for(t=0;t<o;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),w.each([\"toggle\",\"show\",\"hide\"],function(e,t){var n=w.fn[t];w.fn[t]=function(e,r,i){return null==e||\"boolean\"==typeof e?n.apply(this,arguments):this.animate(mt(t,!0),e,r,i)}}),w.each({slideDown:mt(\"show\"),slideUp:mt(\"hide\"),slideToggle:mt(\"toggle\"),fadeIn:{opacity:\"show\"},fadeOut:{opacity:\"hide\"},fadeToggle:{opacity:\"toggle\"}},function(e,t){w.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),w.timers=[],w.fx.tick=function(){var e,t=0,n=w.timers;for(ut=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||w.fx.stop(),ut=void 0},w.fx.timer=function(e){w.timers.push(e),w.fx.start()},w.fx.interval=13,w.fx.start=function(){dt||(dt=!0,pt())},w.fx.stop=function(){dt=null},w.fx.speeds={slow:600,fast:200,_default:400},w.fn.delay=function(t,n){return t=w.fx&&w.fx.speeds[t]||t,n=n||\"fx\",this.queue(n,function(n,r){var i=e.setTimeout(n,t);r.stop=function(){e.clearTimeout(i)}})},function(){var e=v.createElement(\"input\"),t=v.createElement(\"select\").appendChild(v.createElement(\"option\"));e.type=\"checkbox\",p.checkOn=\"\"!==e.value,p.optSelected=t.selected,(e=v.createElement(\"input\")).value=\"t\",e.type=\"radio\",p.radioValue=\"t\"===e.value}();var _t,yt=w.expr.attrHandle;w.fn.extend({attr:function(e,t){return K(this,w.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return void 0===e.getAttribute?w.prop(e,t,n):(1===a&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?_t:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&\"set\"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+\"\"),n):i&&\"get\"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!p.radioValue&&\"radio\"===t&&L(e,\"input\")){var n=e.value;return e.setAttribute(\"type\",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(W);if(i&&1===e.nodeType)for(;n=i[r++];)e.removeAttribute(n)}}),_t={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\\w+/g),function(e,t){var n=yt[t]||w.find.attr;yt[t]=function(e,t,r){var i,a,o=t.toLowerCase();return r||(a=yt[o],yt[o]=i,i=null!=n(e,t,r)?o:null,yt[o]=a),i}});var bt=/^(?:input|select|textarea|button)$/i,Mt=/^(?:a|area)$/i;function wt(e){return(e.match(W)||[]).join(\" \")}function kt(e){return e.getAttribute&&e.getAttribute(\"class\")||\"\"}function Lt(e){return Array.isArray(e)?e:\"string\"==typeof e&&e.match(W)||[]}w.fn.extend({prop:function(e,t){return K(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return 1===a&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&\"set\"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&\"get\"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,\"tabindex\");return t?parseInt(t,10):bt.test(e.nodeName)||Mt.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:\"htmlFor\",class:\"className\"}}),p.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each([\"tabIndex\",\"readOnly\",\"maxLength\",\"cellSpacing\",\"cellPadding\",\"rowSpan\",\"colSpan\",\"useMap\",\"frameBorder\",\"contentEditable\"],function(){w.propFix[this.toLowerCase()]=this}),w.fn.extend({addClass:function(e){var t,n,r,i,a,o;return h(e)?this.each(function(t){w(this).addClass(e.call(this,t,kt(this)))}):(t=Lt(e)).length?this.each(function(){if(r=kt(this),n=1===this.nodeType&&\" \"+wt(r)+\" \"){for(a=0;a<t.length;a++)i=t[a],n.indexOf(\" \"+i+\" \")<0&&(n+=i+\" \");o=wt(n),r!==o&&this.setAttribute(\"class\",o)}}):this},removeClass:function(e){var t,n,r,i,a,o;return h(e)?this.each(function(t){w(this).removeClass(e.call(this,t,kt(this)))}):arguments.length?(t=Lt(e)).length?this.each(function(){if(r=kt(this),n=1===this.nodeType&&\" \"+wt(r)+\" \"){for(a=0;a<t.length;a++)for(i=t[a];n.indexOf(\" \"+i+\" \")>-1;)n=n.replace(\" \"+i+\" \",\" \");o=wt(n),r!==o&&this.setAttribute(\"class\",o)}}):this:this.attr(\"class\",\"\")},toggleClass:function(e,t){var n,r,i,a,o=typeof e,s=\"string\"===o||Array.isArray(e);return h(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,kt(this),t),t)}):\"boolean\"==typeof t&&s?t?this.addClass(e):this.removeClass(e):(n=Lt(e),this.each(function(){if(s)for(a=w(this),i=0;i<n.length;i++)r=n[i],a.hasClass(r)?a.removeClass(r):a.addClass(r);else void 0!==e&&\"boolean\"!==o||((r=kt(this))&&ie.set(this,\"__className__\",r),this.setAttribute&&this.setAttribute(\"class\",r||!1===e?\"\":ie.get(this,\"__className__\")||\"\"))}))},hasClass:function(e){var t,n,r=0;for(t=\" \"+e+\" \";n=this[r++];)if(1===n.nodeType&&(\" \"+wt(kt(n))+\" \").indexOf(t)>-1)return!0;return!1}});var xt=/\\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];return arguments.length?(r=h(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i=\"\":\"number\"==typeof i?i+=\"\":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?\"\":e+\"\"})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&\"set\"in t&&void 0!==t.set(this,i,\"value\")||(this.value=i))})):i?(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&\"get\"in t&&void 0!==(n=t.get(i,\"value\"))?n:\"string\"==typeof(n=i.value)?n.replace(xt,\"\"):null==n?\"\":n:void 0}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,\"value\");return null!=t?t:wt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,a=e.selectedIndex,o=\"select-one\"===e.type,s=o?null:[],l=o?a+1:i.length;for(r=a<0?l:o?a:0;r<l;r++)if(((n=i[r]).selected||r===a)&&!n.disabled&&(!n.parentNode.disabled||!L(n.parentNode,\"optgroup\"))){if(t=w(n).val(),o)return t;s.push(t)}return s},set:function(e,t){for(var n,r,i=e.options,a=w.makeArray(t),o=i.length;o--;)((r=i[o]).selected=w.inArray(w.valHooks.option.get(r),a)>-1)&&(n=!0);return n||(e.selectedIndex=-1),a}}}}),w.each([\"radio\",\"checkbox\"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},p.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute(\"value\")?\"on\":e.value})});var Tt=e.location,St={guid:Date.now()},Dt=/\\?/;w.parseXML=function(t){var n,r;if(!t||\"string\"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,\"text/xml\")}catch(e){}return r=n&&n.getElementsByTagName(\"parsererror\")[0],n&&!r||w.error(\"Invalid XML: \"+(r?w.map(r.childNodes,function(e){return e.textContent}).join(\"\\n\"):t)),n};var Et=/^(?:focusinfocus|focusoutblur)$/,Yt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,r,i){var a,o,s,l,u,c,f,p,g=[r||v],_=d.call(t,\"type\")?t.type:t,y=d.call(t,\"namespace\")?t.namespace.split(\".\"):[];if(o=p=s=r=r||v,3!==r.nodeType&&8!==r.nodeType&&!Et.test(_+w.event.triggered)&&(_.indexOf(\".\")>-1&&(y=_.split(\".\"),_=y.shift(),y.sort()),u=_.indexOf(\":\")<0&&\"on\"+_,(t=t[w.expando]?t:new w.Event(_,\"object\"==typeof t&&t)).isTrigger=i?2:3,t.namespace=y.join(\".\"),t.rnamespace=t.namespace?new RegExp(\"(^|\\\\.)\"+y.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:w.makeArray(n,[t]),f=w.event.special[_]||{},i||!f.trigger||!1!==f.trigger.apply(r,n))){if(!i&&!f.noBubble&&!m(r)){for(l=f.delegateType||_,Et.test(l+_)||(o=o.parentNode);o;o=o.parentNode)g.push(o),s=o;s===(r.ownerDocument||v)&&g.push(s.defaultView||s.parentWindow||e)}for(a=0;(o=g[a++])&&!t.isPropagationStopped();)p=o,t.type=a>1?l:f.bindType||_,(c=(ie.get(o,\"events\")||Object.create(null))[t.type]&&ie.get(o,\"handle\"))&&c.apply(o,n),(c=u&&o[u])&&c.apply&&ne(o)&&(t.result=c.apply(o,n),!1===t.result&&t.preventDefault());return t.type=_,i||t.isDefaultPrevented()||f._default&&!1!==f._default.apply(g.pop(),n)||!ne(r)||u&&h(r[_])&&!m(r)&&((s=r[u])&&(r[u]=null),w.event.triggered=_,t.isPropagationStopped()&&p.addEventListener(_,Yt),r[_](),t.isPropagationStopped()&&p.removeEventListener(_,Yt),w.event.triggered=void 0,s&&(r[u]=s)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}});var At=/\\[\\]$/,Ot=/\\r?\\n/g,Ct=/^(?:submit|button|image|reset|file)$/i,jt=/^(?:input|select|textarea|keygen)/i;function Pt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||At.test(e)?r(e,i):Pt(e+\"[\"+(\"object\"==typeof i&&null!=i?t:\"\")+\"]\",i,n,r)});else if(n||\"object\"!==y(t))r(e,t);else for(i in t)Pt(e+\"[\"+i+\"]\",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=h(t)?t():t;r[r.length]=encodeURIComponent(e)+\"=\"+encodeURIComponent(null==n?\"\":n)};if(null==e)return\"\";if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)Pt(n,e[n],t,i);return r.join(\"&\")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,\"elements\");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(\":disabled\")&&jt.test(this.nodeName)&&!Ct.test(e)&&(this.checked||!we.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Ot,\"\\r\\n\")}}):{name:t.name,value:n.replace(Ot,\"\\r\\n\")}}).get()}});var Ht=/%20/g,It=/#.*$/,Ft=/([?&])_=[^&]*/,Nt=/^(.*?):[ \\t]*([^\\r\\n]*)$/gm,Rt=/^(?:GET|HEAD)$/,Vt=/^\\/\\//,$t={},zt={},Wt=\"*/\".concat(\"*\"),Ut=v.createElement(\"a\");function Bt(e){return function(t,n){\"string\"!=typeof t&&(n=t,t=\"*\");var r,i=0,a=t.toLowerCase().match(W)||[];if(h(n))for(;r=a[i++];)\"+\"===r[0]?(r=r.slice(1)||\"*\",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qt(e,t,n,r){var i={},a=e===zt;function o(s){var l;return i[s]=!0,w.each(e[s]||[],function(e,s){var u=s(t,n,r);return\"string\"!=typeof u||a||i[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),o(u),!1)}),l}return o(t.dataTypes[0])||!i[\"*\"]&&o(\"*\")}function Gt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}Ut.href=Tt.href,w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Tt.href,type:\"GET\",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Tt.protocol),global:!0,processData:!0,async:!0,contentType:\"application/x-www-form-urlencoded; charset=UTF-8\",accepts:{\"*\":Wt,text:\"text/plain\",html:\"text/html\",xml:\"application/xml, text/xml\",json:\"application/json, text/javascript\"},contents:{xml:/\\bxml\\b/,html:/\\bhtml/,json:/\\bjson\\b/},responseFields:{xml:\"responseXML\",text:\"responseText\",json:\"responseJSON\"},converters:{\"* text\":String,\"text html\":!0,\"text json\":JSON.parse,\"text xml\":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Gt(Gt(e,w.ajaxSettings),t):Gt(w.ajaxSettings,e)},ajaxPrefilter:Bt($t),ajaxTransport:Bt(zt),ajax:function(t,n){\"object\"==typeof t&&(n=t,t=void 0),n=n||{};var r,i,a,o,s,l,u,d,c,f,p=w.ajaxSetup({},n),h=p.context||p,m=p.context&&(h.nodeType||h.jquery)?w(h):w.event,g=w.Deferred(),_=w.Callbacks(\"once memory\"),y=p.statusCode||{},b={},M={},k=\"canceled\",L={readyState:0,getResponseHeader:function(e){var t;if(u){if(!o)for(o={};t=Nt.exec(a);)o[t[1].toLowerCase()+\" \"]=(o[t[1].toLowerCase()+\" \"]||[]).concat(t[2]);t=o[e.toLowerCase()+\" \"]}return null==t?null:t.join(\", \")},getAllResponseHeaders:function(){return u?a:null},setRequestHeader:function(e,t){return null==u&&(e=M[e.toLowerCase()]=M[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==u&&(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(u)L.always(e[L.status]);else for(t in e)y[t]=[y[t],e[t]];return this},abort:function(e){var t=e||k;return r&&r.abort(t),x(0,t),this}};if(g.promise(L),p.url=((t||p.url||Tt.href)+\"\").replace(Vt,Tt.protocol+\"//\"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=(p.dataType||\"*\").toLowerCase().match(W)||[\"\"],null==p.crossDomain){l=v.createElement(\"a\");try{l.href=p.url,l.href=l.href,p.crossDomain=Ut.protocol+\"//\"+Ut.host!=l.protocol+\"//\"+l.host}catch(e){p.crossDomain=!0}}if(p.data&&p.processData&&\"string\"!=typeof p.data&&(p.data=w.param(p.data,p.traditional)),qt($t,p,n,L),u)return L;for(c in(d=w.event&&p.global)&&0===w.active++&&w.event.trigger(\"ajaxStart\"),p.type=p.type.toUpperCase(),p.hasContent=!Rt.test(p.type),i=p.url.replace(It,\"\"),p.hasContent?p.data&&p.processData&&0===(p.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&(p.data=p.data.replace(Ht,\"+\")):(f=p.url.slice(i.length),p.data&&(p.processData||\"string\"==typeof p.data)&&(i+=(Dt.test(i)?\"&\":\"?\")+p.data,delete p.data),!1===p.cache&&(i=i.replace(Ft,\"$1\"),f=(Dt.test(i)?\"&\":\"?\")+\"_=\"+St.guid+++f),p.url=i+f),p.ifModified&&(w.lastModified[i]&&L.setRequestHeader(\"If-Modified-Since\",w.lastModified[i]),w.etag[i]&&L.setRequestHeader(\"If-None-Match\",w.etag[i])),(p.data&&p.hasContent&&!1!==p.contentType||n.contentType)&&L.setRequestHeader(\"Content-Type\",p.contentType),L.setRequestHeader(\"Accept\",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+(\"*\"!==p.dataTypes[0]?\", \"+Wt+\"; q=0.01\":\"\"):p.accepts[\"*\"]),p.headers)L.setRequestHeader(c,p.headers[c]);if(p.beforeSend&&(!1===p.beforeSend.call(h,L,p)||u))return L.abort();if(k=\"abort\",_.add(p.complete),L.done(p.success),L.fail(p.error),r=qt(zt,p,n,L)){if(L.readyState=1,d&&m.trigger(\"ajaxSend\",[L,p]),u)return L;p.async&&p.timeout>0&&(s=e.setTimeout(function(){L.abort(\"timeout\")},p.timeout));try{u=!1,r.send(b,x)}catch(e){if(u)throw e;x(-1,e)}}else x(-1,\"No Transport\");function x(t,n,o,l){var c,f,v,b,M,k=n;u||(u=!0,s&&e.clearTimeout(s),r=void 0,a=l||\"\",L.readyState=t>0?4:0,c=t>=200&&t<300||304===t,o&&(b=function(e,t,n){for(var r,i,a,o,s=e.contents,l=e.dataTypes;\"*\"===l[0];)l.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader(\"Content-Type\"));if(r)for(i in s)if(s[i]&&s[i].test(r)){l.unshift(i);break}if(l[0]in n)a=l[0];else{for(i in n){if(!l[0]||e.converters[i+\" \"+l[0]]){a=i;break}o||(o=i)}a=a||o}if(a)return a!==l[0]&&l.unshift(a),n[a]}(p,L,o)),!c&&w.inArray(\"script\",p.dataTypes)>-1&&w.inArray(\"json\",p.dataTypes)<0&&(p.converters[\"text script\"]=function(){}),b=function(e,t,n,r){var i,a,o,s,l,u={},d=e.dataTypes.slice();if(d[1])for(o in e.converters)u[o.toLowerCase()]=e.converters[o];for(a=d.shift();a;)if(e.responseFields[a]&&(n[e.responseFields[a]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=a,a=d.shift())if(\"*\"===a)a=l;else if(\"*\"!==l&&l!==a){if(!(o=u[l+\" \"+a]||u[\"* \"+a]))for(i in u)if((s=i.split(\" \"))[1]===a&&(o=u[l+\" \"+s[0]]||u[\"* \"+s[0]])){!0===o?o=u[i]:!0!==u[i]&&(a=s[0],d.unshift(s[1]));break}if(!0!==o)if(o&&e.throws)t=o(t);else try{t=o(t)}catch(e){return{state:\"parsererror\",error:o?e:\"No conversion from \"+l+\" to \"+a}}}return{state:\"success\",data:t}}(p,b,L,c),c?(p.ifModified&&((M=L.getResponseHeader(\"Last-Modified\"))&&(w.lastModified[i]=M),(M=L.getResponseHeader(\"etag\"))&&(w.etag[i]=M)),204===t||\"HEAD\"===p.type?k=\"nocontent\":304===t?k=\"notmodified\":(k=b.state,f=b.data,c=!(v=b.error))):(v=k,!t&&k||(k=\"error\",t<0&&(t=0))),L.status=t,L.statusText=(n||k)+\"\",c?g.resolveWith(h,[f,k,L]):g.rejectWith(h,[L,k,v]),L.statusCode(y),y=void 0,d&&m.trigger(c?\"ajaxSuccess\":\"ajaxError\",[L,p,c?f:v]),_.fireWith(h,[L,k]),d&&(m.trigger(\"ajaxComplete\",[L,p]),--w.active||w.event.trigger(\"ajaxStop\")))}return L},getJSON:function(e,t,n){return w.get(e,t,n,\"json\")},getScript:function(e,t){return w.get(e,void 0,t,\"script\")}}),w.each([\"get\",\"post\"],function(e,t){w[t]=function(e,n,r,i){return h(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w.ajaxPrefilter(function(e){var t;for(t in e.headers)\"content-type\"===t.toLowerCase()&&(e.contentType=e.headers[t]||\"\")}),w._evalUrl=function(e,t,n){return w.ajax({url:e,type:\"GET\",dataType:\"script\",cache:!0,async:!1,global:!1,converters:{\"text script\":function(){}},dataFilter:function(e){w.globalEval(e,t,n)}})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(h(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return h(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=h(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not(\"body\").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Jt={0:200,1223:204},Zt=w.ajaxSettings.xhr();p.cors=!!Zt&&\"withCredentials\"in Zt,p.ajax=Zt=!!Zt,w.ajaxTransport(function(t){var n,r;if(p.cors||Zt&&!t.crossDomain)return{send:function(i,a){var o,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(o in t.xhrFields)s[o]=t.xhrFields[o];for(o in t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i[\"X-Requested-With\"]||(i[\"X-Requested-With\"]=\"XMLHttpRequest\"),i)s.setRequestHeader(o,i[o]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,\"abort\"===e?s.abort():\"error\"===e?\"number\"!=typeof s.status?a(0,\"error\"):a(s.status,s.statusText):a(Jt[s.status]||s.status,s.statusText,\"text\"!==(s.responseType||\"text\")||\"string\"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n(\"error\"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n(\"abort\");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:\"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\"},contents:{script:/\\b(?:java|ecma)script\\b/},converters:{\"text script\":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter(\"script\",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type=\"GET\")}),w.ajaxTransport(\"script\",function(e){var t,n;if(e.crossDomain||e.scriptAttrs)return{send:function(r,i){t=w(\"<script>\").attr(e.scriptAttrs||{}).prop({charset:e.scriptCharset,src:e.url}).on(\"load error\",n=function(e){t.remove(),n=null,e&&i(\"error\"===e.type?404:200,e.type)}),v.head.appendChild(t[0])},abort:function(){n&&n()}}});var Kt,Xt=[],Qt=/(=)\\?(?=&|$)|\\?\\?/;w.ajaxSetup({jsonp:\"callback\",jsonpCallback:function(){var e=Xt.pop()||w.expando+\"_\"+St.guid++;return this[e]=!0,e}}),w.ajaxPrefilter(\"json jsonp\",function(t,n,r){var i,a,o,s=!1!==t.jsonp&&(Qt.test(t.url)?\"url\":\"string\"==typeof t.data&&0===(t.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&Qt.test(t.data)&&\"data\");if(s||\"jsonp\"===t.dataTypes[0])return i=t.jsonpCallback=h(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(Qt,\"$1\"+i):!1!==t.jsonp&&(t.url+=(Dt.test(t.url)?\"&\":\"?\")+t.jsonp+\"=\"+i),t.converters[\"script json\"]=function(){return o||w.error(i+\" was not called\"),o[0]},t.dataTypes[0]=\"json\",a=e[i],e[i]=function(){o=arguments},r.always(function(){void 0===a?w(e).removeProp(i):e[i]=a,t[i]&&(t.jsonpCallback=n.jsonpCallback,Xt.push(i)),o&&h(a)&&a(o[0]),o=a=void 0}),\"script\"}),p.createHTMLDocument=((Kt=v.implementation.createHTMLDocument(\"\").body).innerHTML=\"<form></form><form></form>\",2===Kt.childNodes.length),w.parseHTML=function(e,t,n){return\"string\"!=typeof e?[]:(\"boolean\"==typeof t&&(n=t,t=!1),t||(p.createHTMLDocument?((r=(t=v.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=v.location.href,t.head.appendChild(r)):t=v),a=!n&&[],(i=I.exec(e))?[t.createElement(i[1])]:(i=Ee([e],t,a),a&&a.length&&w(a).remove(),w.merge([],i.childNodes)));var r,i,a},w.fn.load=function(e,t,n){var r,i,a,o=this,s=e.indexOf(\" \");return s>-1&&(r=wt(e.slice(s)),e=e.slice(0,s)),h(t)?(n=t,t=void 0):t&&\"object\"==typeof t&&(i=\"POST\"),o.length>0&&w.ajax({url:e,type:i||\"GET\",dataType:\"html\",data:t}).done(function(e){a=arguments,o.html(r?w(\"<div>\").append(w.parseHTML(e)).find(r):e)}).always(n&&function(e,t){o.each(function(){n.apply(this,a||[e.responseText,t,e])})}),this},w.expr.pseudos.animated=function(e){return w.grep(w.timers,function(t){return e===t.elem}).length},w.offset={setOffset:function(e,t,n){var r,i,a,o,s,l,u=w.css(e,\"position\"),d=w(e),c={};\"static\"===u&&(e.style.position=\"relative\"),s=d.offset(),a=w.css(e,\"top\"),l=w.css(e,\"left\"),(\"absolute\"===u||\"fixed\"===u)&&(a+l).indexOf(\"auto\")>-1?(o=(r=d.position()).top,i=r.left):(o=parseFloat(a)||0,i=parseFloat(l)||0),h(t)&&(t=t.call(e,n,w.extend({},s))),null!=t.top&&(c.top=t.top-s.top+o),null!=t.left&&(c.left=t.left-s.left+i),\"using\"in t?t.using.call(e,c):d.css(c)}},w.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){w.offset.setOffset(this,e,t)});var t,n,r=this[0];return r?r.getClientRects().length?(t=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:t.top+n.pageYOffset,left:t.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if(\"fixed\"===w.css(r,\"position\"))t=r.getBoundingClientRect();else{for(t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&\"static\"===w.css(e,\"position\");)e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=w(e).offset()).top+=w.css(e,\"borderTopWidth\",!0),i.left+=w.css(e,\"borderLeftWidth\",!0))}return{top:t.top-i.top-w.css(r,\"marginTop\",!0),left:t.left-i.left-w.css(r,\"marginLeft\",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&\"static\"===w.css(e,\"position\");)e=e.offsetParent;return e||fe})}}),w.each({scrollLeft:\"pageXOffset\",scrollTop:\"pageYOffset\"},function(e,t){var n=\"pageYOffset\"===t;w.fn[e]=function(r){return K(this,function(e,r,i){var a;if(m(e)?a=e:9===e.nodeType&&(a=e.defaultView),void 0===i)return a?a[t]:e[r];a?a.scrollTo(n?a.pageXOffset:i,n?i:a.pageYOffset):e[r]=i},e,r,arguments.length)}}),w.each([\"top\",\"left\"],function(e,t){w.cssHooks[t]=Ke(p.pixelPosition,function(e,n){if(n)return n=Ze(e,t),Ue.test(n)?w(e).position()[t]+\"px\":n})}),w.each({Height:\"height\",Width:\"width\"},function(e,t){w.each({padding:\"inner\"+e,content:t,\"\":\"outer\"+e},function(n,r){w.fn[r]=function(i,a){var o=arguments.length&&(n||\"boolean\"!=typeof i),s=n||(!0===i||!0===a?\"margin\":\"border\");return K(this,function(t,n,i){var a;return m(t)?0===r.indexOf(\"outer\")?t[\"inner\"+e]:t.document.documentElement[\"client\"+e]:9===t.nodeType?(a=t.documentElement,Math.max(t.body[\"scroll\"+e],a[\"scroll\"+e],t.body[\"offset\"+e],a[\"offset\"+e],a[\"client\"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,o?i:void 0,o)}})}),w.each([\"ajaxStart\",\"ajaxStop\",\"ajaxComplete\",\"ajaxError\",\"ajaxSuccess\",\"ajaxSend\"],function(e,t){w.fn[t]=function(e){return this.on(t,e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)},hover:function(e,t){return this.on(\"mouseenter\",e).on(\"mouseleave\",t||e)}}),w.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}});var en=/^[\\s\\uFEFF\\xA0]+|([^\\s\\uFEFF\\xA0])[\\s\\uFEFF\\xA0]+$/g;w.proxy=function(e,t){var n,r,a;if(\"string\"==typeof t&&(n=e[t],t=e,e=n),h(e))return r=i.call(arguments,2),a=function(){return e.apply(t||this,r.concat(i.call(arguments)))},a.guid=e.guid=e.guid||w.guid++,a},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=L,w.isFunction=h,w.isWindow=m,w.camelCase=te,w.type=y,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return(\"number\"===t||\"string\"===t)&&!isNaN(e-parseFloat(e))},w.trim=function(e){return null==e?\"\":(e+\"\").replace(en,\"$1\")},\"function\"==typeof define&&define.amd&&define(\"jquery\",[],function(){return w});var tn=e.jQuery,nn=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=nn),t&&e.jQuery===w&&(e.jQuery=tn),w},void 0===t&&(e.jQuery=e.$=w),w}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],t):t((e=\"undefined\"!=typeof globalThis?globalThis:e||self).Popper={})}(this,function(e){\"use strict\";function t(e){if(null==e)return window;if(\"[object Window]\"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function n(e){return e instanceof t(e).Element||e instanceof Element}function r(e){return e instanceof t(e).HTMLElement||e instanceof HTMLElement}function i(e){return\"undefined\"!=typeof ShadowRoot&&(e instanceof t(e).ShadowRoot||e instanceof ShadowRoot)}var a=Math.max,o=Math.min,s=Math.round;function l(){var e=navigator.userAgentData;return null!=e&&e.brands&&Array.isArray(e.brands)?e.brands.map(function(e){return e.brand+\"/\"+e.version}).join(\" \"):navigator.userAgent}function u(){return!/^((?!chrome|android).)*safari/i.test(l())}function d(e,i,a){void 0===i&&(i=!1),void 0===a&&(a=!1);var o=e.getBoundingClientRect(),l=1,d=1;i&&r(e)&&(l=e.offsetWidth>0&&s(o.width)/e.offsetWidth||1,d=e.offsetHeight>0&&s(o.height)/e.offsetHeight||1);var c=(n(e)?t(e):window).visualViewport,f=!u()&&a,p=(o.left+(f&&c?c.offsetLeft:0))/l,h=(o.top+(f&&c?c.offsetTop:0))/d,m=o.width/l,v=o.height/d;return{width:m,height:v,top:h,right:p+m,bottom:h+v,left:p,x:p,y:h}}function c(e){var n=t(e);return{scrollLeft:n.pageXOffset,scrollTop:n.pageYOffset}}function f(e){return e?(e.nodeName||\"\").toLowerCase():null}function p(e){return((n(e)?e.ownerDocument:e.document)||window.document).documentElement}function h(e){return d(p(e)).left+c(e).scrollLeft}function m(e){return t(e).getComputedStyle(e)}function v(e){var t=m(e),n=t.overflow,r=t.overflowX,i=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+i+r)}function g(e,n,i){void 0===i&&(i=!1);var a,o,l=r(n),u=r(n)&&function(e){var t=e.getBoundingClientRect(),n=s(t.width)/e.offsetWidth||1,r=s(t.height)/e.offsetHeight||1;return 1!==n||1!==r}(n),m=p(n),g=d(e,u,i),_={scrollLeft:0,scrollTop:0},y={x:0,y:0};return(l||!l&&!i)&&((\"body\"!==f(n)||v(m))&&(_=(a=n)!==t(a)&&r(a)?{scrollLeft:(o=a).scrollLeft,scrollTop:o.scrollTop}:c(a)),r(n)?((y=d(n,!0)).x+=n.clientLeft,y.y+=n.clientTop):m&&(y.x=h(m))),{x:g.left+_.scrollLeft-y.x,y:g.top+_.scrollTop-y.y,width:g.width,height:g.height}}function _(e){var t=d(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function y(e){return\"html\"===f(e)?e:e.assignedSlot||e.parentNode||(i(e)?e.host:null)||p(e)}function b(e){return[\"html\",\"body\",\"#document\"].indexOf(f(e))>=0?e.ownerDocument.body:r(e)&&v(e)?e:b(y(e))}function M(e,n){var r;void 0===n&&(n=[]);var i=b(e),a=i===(null==(r=e.ownerDocument)?void 0:r.body),o=t(i),s=a?[o].concat(o.visualViewport||[],v(i)?i:[]):i,l=n.concat(s);return a?l:l.concat(M(y(s)))}function w(e){return[\"table\",\"td\",\"th\"].indexOf(f(e))>=0}function k(e){return r(e)&&\"fixed\"!==m(e).position?e.offsetParent:null}function L(e){for(var n=t(e),a=k(e);a&&w(a)&&\"static\"===m(a).position;)a=k(a);return a&&(\"html\"===f(a)||\"body\"===f(a)&&\"static\"===m(a).position)?n:a||function(e){var t=/firefox/i.test(l());if(/Trident/i.test(l())&&r(e)&&\"fixed\"===m(e).position)return null;var n=y(e);for(i(n)&&(n=n.host);r(n)&&[\"html\",\"body\"].indexOf(f(n))<0;){var a=m(n);if(\"none\"!==a.transform||\"none\"!==a.perspective||\"paint\"===a.contain||-1!==[\"transform\",\"perspective\"].indexOf(a.willChange)||t&&\"filter\"===a.willChange||t&&a.filter&&\"none\"!==a.filter)return n;n=n.parentNode}return null}(e)||n}var x=\"top\",T=\"bottom\",S=\"right\",D=\"left\",E=\"auto\",Y=[x,T,S,D],A=\"start\",O=\"end\",C=\"viewport\",j=\"popper\",P=Y.reduce(function(e,t){return e.concat([t+\"-\"+A,t+\"-\"+O])},[]),H=[].concat(Y,[E]).reduce(function(e,t){return e.concat([t,t+\"-\"+A,t+\"-\"+O])},[]),I=[\"beforeRead\",\"read\",\"afterRead\",\"beforeMain\",\"main\",\"afterMain\",\"beforeWrite\",\"write\",\"afterWrite\"];function F(e){var t=new Map,n=new Set,r=[];function i(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach(function(e){if(!n.has(e)){var r=t.get(e);r&&i(r)}}),r.push(e)}return e.forEach(function(e){t.set(e.name,e)}),e.forEach(function(e){n.has(e.name)||i(e)}),r}function N(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&i(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function R(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function V(e,r,i){return r===C?R(function(e,n){var r=t(e),i=p(e),a=r.visualViewport,o=i.clientWidth,s=i.clientHeight,l=0,d=0;if(a){o=a.width,s=a.height;var c=u();(c||!c&&\"fixed\"===n)&&(l=a.offsetLeft,d=a.offsetTop)}return{width:o,height:s,x:l+h(e),y:d}}(e,i)):n(r)?function(e,t){var n=d(e,!1,\"fixed\"===t);return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}(r,i):R(function(e){var t,n=p(e),r=c(e),i=null==(t=e.ownerDocument)?void 0:t.body,o=a(n.scrollWidth,n.clientWidth,i?i.scrollWidth:0,i?i.clientWidth:0),s=a(n.scrollHeight,n.clientHeight,i?i.scrollHeight:0,i?i.clientHeight:0),l=-r.scrollLeft+h(e),u=-r.scrollTop;return\"rtl\"===m(i||n).direction&&(l+=a(n.clientWidth,i?i.clientWidth:0)-o),{width:o,height:s,x:l,y:u}}(p(e)))}function $(e,t,i,s){var l=\"clippingParents\"===t?function(e){var t=M(y(e)),i=[\"absolute\",\"fixed\"].indexOf(m(e).position)>=0&&r(e)?L(e):e;return n(i)?t.filter(function(e){return n(e)&&N(e,i)&&\"body\"!==f(e)}):[]}(e):[].concat(t),u=[].concat(l,[i]),d=u[0],c=u.reduce(function(t,n){var r=V(e,n,s);return t.top=a(r.top,t.top),t.right=o(r.right,t.right),t.bottom=o(r.bottom,t.bottom),t.left=a(r.left,t.left),t},V(e,d,s));return c.width=c.right-c.left,c.height=c.bottom-c.top,c.x=c.left,c.y=c.top,c}function z(e){return e.split(\"-\")[0]}function W(e){return e.split(\"-\")[1]}function U(e){return[\"top\",\"bottom\"].indexOf(e)>=0?\"x\":\"y\"}function B(e){var t,n=e.reference,r=e.element,i=e.placement,a=i?z(i):null,o=i?W(i):null,s=n.x+n.width/2-r.width/2,l=n.y+n.height/2-r.height/2;switch(a){case x:t={x:s,y:n.y-r.height};break;case T:t={x:s,y:n.y+n.height};break;case S:t={x:n.x+n.width,y:l};break;case D:t={x:n.x-r.width,y:l};break;default:t={x:n.x,y:n.y}}var u=a?U(a):null;if(null!=u){var d=\"y\"===u?\"height\":\"width\";switch(o){case A:t[u]=t[u]-(n[d]/2-r[d]/2);break;case O:t[u]=t[u]+(n[d]/2-r[d]/2)}}return t}function q(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function G(e,t){return t.reduce(function(t,n){return t[n]=e,t},{})}function J(e,t){void 0===t&&(t={});var r=t,i=r.placement,a=void 0===i?e.placement:i,o=r.strategy,s=void 0===o?e.strategy:o,l=r.boundary,u=void 0===l?\"clippingParents\":l,c=r.rootBoundary,f=void 0===c?C:c,h=r.elementContext,m=void 0===h?j:h,v=r.altBoundary,g=void 0!==v&&v,_=r.padding,y=void 0===_?0:_,b=q(\"number\"!=typeof y?y:G(y,Y)),M=m===j?\"reference\":j,w=e.rects.popper,k=e.elements[g?M:m],L=$(n(k)?k:k.contextElement||p(e.elements.popper),u,f,s),D=d(e.elements.reference),E=B({reference:D,element:w,strategy:\"absolute\",placement:a}),A=R(Object.assign({},w,E)),O=m===j?A:D,P={top:L.top-O.top+b.top,bottom:O.bottom-L.bottom+b.bottom,left:L.left-O.left+b.left,right:O.right-L.right+b.right},H=e.modifiersData.offset;if(m===j&&H){var I=H[a];Object.keys(P).forEach(function(e){var t=[S,T].indexOf(e)>=0?1:-1,n=[x,T].indexOf(e)>=0?\"y\":\"x\";P[e]+=I[n]*t})}return P}var Z={placement:\"bottom\",modifiers:[],strategy:\"absolute\"};function K(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return!t.some(function(e){return!(e&&\"function\"==typeof e.getBoundingClientRect)})}function X(e){void 0===e&&(e={});var t=e,r=t.defaultModifiers,i=void 0===r?[]:r,a=t.defaultOptions,o=void 0===a?Z:a;return function(e,t,r){void 0===r&&(r=o);var a,s,l={placement:\"bottom\",orderedModifiers:[],options:Object.assign({},Z,o),modifiersData:{},elements:{reference:e,popper:t},attributes:{},styles:{}},u=[],d=!1,c={state:l,setOptions:function(r){var a=\"function\"==typeof r?r(l.options):r;f(),l.options=Object.assign({},o,l.options,a),l.scrollParents={reference:n(e)?M(e):e.contextElement?M(e.contextElement):[],popper:M(t)};var s,d,p=function(e){var t=F(e);return I.reduce(function(e,n){return e.concat(t.filter(function(e){return e.phase===n}))},[])}((s=[].concat(i,l.options.modifiers),d=s.reduce(function(e,t){var n=e[t.name];return e[t.name]=n?Object.assign({},n,t,{options:Object.assign({},n.options,t.options),data:Object.assign({},n.data,t.data)}):t,e},{}),Object.keys(d).map(function(e){return d[e]})));return l.orderedModifiers=p.filter(function(e){return e.enabled}),l.orderedModifiers.forEach(function(e){var t=e.name,n=e.options,r=void 0===n?{}:n,i=e.effect;if(\"function\"==typeof i){var a=i({state:l,name:t,instance:c,options:r}),o=function(){};u.push(a||o)}}),c.update()},forceUpdate:function(){if(!d){var e=l.elements,t=e.reference,n=e.popper;if(K(t,n)){l.rects={reference:g(t,L(n),\"fixed\"===l.options.strategy),popper:_(n)},l.reset=!1,l.placement=l.options.placement,l.orderedModifiers.forEach(function(e){return l.modifiersData[e.name]=Object.assign({},e.data)});for(var r=0;r<l.orderedModifiers.length;r++)if(!0!==l.reset){var i=l.orderedModifiers[r],a=i.fn,o=i.options,s=void 0===o?{}:o,u=i.name;\"function\"==typeof a&&(l=a({state:l,options:s,name:u,instance:c})||l)}else l.reset=!1,r=-1}}},update:(a=function(){return new Promise(function(e){c.forceUpdate(),e(l)})},function(){return s||(s=new Promise(function(e){Promise.resolve().then(function(){s=void 0,e(a())})})),s}),destroy:function(){f(),d=!0}};if(!K(e,t))return c;function f(){u.forEach(function(e){return e()}),u=[]}return c.setOptions(r).then(function(e){!d&&r.onFirstUpdate&&r.onFirstUpdate(e)}),c}}var Q={passive:!0};var ee={name:\"eventListeners\",enabled:!0,phase:\"write\",fn:function(){},effect:function(e){var n=e.state,r=e.instance,i=e.options,a=i.scroll,o=void 0===a||a,s=i.resize,l=void 0===s||s,u=t(n.elements.popper),d=[].concat(n.scrollParents.reference,n.scrollParents.popper);return o&&d.forEach(function(e){e.addEventListener(\"scroll\",r.update,Q)}),l&&u.addEventListener(\"resize\",r.update,Q),function(){o&&d.forEach(function(e){e.removeEventListener(\"scroll\",r.update,Q)}),l&&u.removeEventListener(\"resize\",r.update,Q)}},data:{}};var te={name:\"popperOffsets\",enabled:!0,phase:\"read\",fn:function(e){var t=e.state,n=e.name;t.modifiersData[n]=B({reference:t.rects.reference,element:t.rects.popper,strategy:\"absolute\",placement:t.placement})},data:{}},ne={top:\"auto\",right:\"auto\",bottom:\"auto\",left:\"auto\"};function re(e){var n,r=e.popper,i=e.popperRect,a=e.placement,o=e.variation,l=e.offsets,u=e.position,d=e.gpuAcceleration,c=e.adaptive,f=e.roundOffsets,h=e.isFixed,v=l.x,g=void 0===v?0:v,_=l.y,y=void 0===_?0:_,b=\"function\"==typeof f?f({x:g,y:y}):{x:g,y:y};g=b.x,y=b.y;var M=l.hasOwnProperty(\"x\"),w=l.hasOwnProperty(\"y\"),k=D,E=x,Y=window;if(c){var A=L(r),C=\"clientHeight\",j=\"clientWidth\";if(A===t(r)&&\"static\"!==m(A=p(r)).position&&\"absolute\"===u&&(C=\"scrollHeight\",j=\"scrollWidth\"),a===x||(a===D||a===S)&&o===O)E=T,y-=(h&&A===Y&&Y.visualViewport?Y.visualViewport.height:A[C])-i.height,y*=d?1:-1;if(a===D||(a===x||a===T)&&o===O)k=S,g-=(h&&A===Y&&Y.visualViewport?Y.visualViewport.width:A[j])-i.width,g*=d?1:-1}var P,H=Object.assign({position:u},c&&ne),I=!0===f?function(e,t){var n=e.x,r=e.y,i=t.devicePixelRatio||1;return{x:s(n*i)/i||0,y:s(r*i)/i||0}}({x:g,y:y},t(r)):{x:g,y:y};return g=I.x,y=I.y,d?Object.assign({},H,((P={})[E]=w?\"0\":\"\",P[k]=M?\"0\":\"\",P.transform=(Y.devicePixelRatio||1)<=1?\"translate(\"+g+\"px, \"+y+\"px)\":\"translate3d(\"+g+\"px, \"+y+\"px, 0)\",P)):Object.assign({},H,((n={})[E]=w?y+\"px\":\"\",n[k]=M?g+\"px\":\"\",n.transform=\"\",n))}var ie={name:\"computeStyles\",enabled:!0,phase:\"beforeWrite\",fn:function(e){var t=e.state,n=e.options,r=n.gpuAcceleration,i=void 0===r||r,a=n.adaptive,o=void 0===a||a,s=n.roundOffsets,l=void 0===s||s,u={placement:z(t.placement),variation:W(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:i,isFixed:\"fixed\"===t.options.strategy};null!=t.modifiersData.popperOffsets&&(t.styles.popper=Object.assign({},t.styles.popper,re(Object.assign({},u,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:o,roundOffsets:l})))),null!=t.modifiersData.arrow&&(t.styles.arrow=Object.assign({},t.styles.arrow,re(Object.assign({},u,{offsets:t.modifiersData.arrow,position:\"absolute\",adaptive:!1,roundOffsets:l})))),t.attributes.popper=Object.assign({},t.attributes.popper,{\"data-popper-placement\":t.placement})},data:{}};var ae={name:\"applyStyles\",enabled:!0,phase:\"write\",fn:function(e){var t=e.state;Object.keys(t.elements).forEach(function(e){var n=t.styles[e]||{},i=t.attributes[e]||{},a=t.elements[e];r(a)&&f(a)&&(Object.assign(a.style,n),Object.keys(i).forEach(function(e){var t=i[e];!1===t?a.removeAttribute(e):a.setAttribute(e,!0===t?\"\":t)}))})},effect:function(e){var t=e.state,n={popper:{position:t.options.strategy,left:\"0\",top:\"0\",margin:\"0\"},arrow:{position:\"absolute\"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(e){var i=t.elements[e],a=t.attributes[e]||{},o=Object.keys(t.styles.hasOwnProperty(e)?t.styles[e]:n[e]).reduce(function(e,t){return e[t]=\"\",e},{});r(i)&&f(i)&&(Object.assign(i.style,o),Object.keys(a).forEach(function(e){i.removeAttribute(e)}))})}},requires:[\"computeStyles\"]};var oe={name:\"offset\",enabled:!0,phase:\"main\",requires:[\"popperOffsets\"],fn:function(e){var t=e.state,n=e.options,r=e.name,i=n.offset,a=void 0===i?[0,0]:i,o=H.reduce(function(e,n){return e[n]=function(e,t,n){var r=z(e),i=[D,x].indexOf(r)>=0?-1:1,a=\"function\"==typeof n?n(Object.assign({},t,{placement:e})):n,o=a[0],s=a[1];return o=o||0,s=(s||0)*i,[D,S].indexOf(r)>=0?{x:s,y:o}:{x:o,y:s}}(n,t.rects,a),e},{}),s=o[t.placement],l=s.x,u=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=l,t.modifiersData.popperOffsets.y+=u),t.modifiersData[r]=o}},se={left:\"right\",right:\"left\",bottom:\"top\",top:\"bottom\"};function le(e){return e.replace(/left|right|bottom|top/g,function(e){return se[e]})}var ue={start:\"end\",end:\"start\"};function de(e){return e.replace(/start|end/g,function(e){return ue[e]})}function ce(e,t){void 0===t&&(t={});var n=t,r=n.placement,i=n.boundary,a=n.rootBoundary,o=n.padding,s=n.flipVariations,l=n.allowedAutoPlacements,u=void 0===l?H:l,d=W(r),c=d?s?P:P.filter(function(e){return W(e)===d}):Y,f=c.filter(function(e){return u.indexOf(e)>=0});0===f.length&&(f=c);var p=f.reduce(function(t,n){return t[n]=J(e,{placement:n,boundary:i,rootBoundary:a,padding:o})[z(n)],t},{});return Object.keys(p).sort(function(e,t){return p[e]-p[t]})}var fe={name:\"flip\",enabled:!0,phase:\"main\",fn:function(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var i=n.mainAxis,a=void 0===i||i,o=n.altAxis,s=void 0===o||o,l=n.fallbackPlacements,u=n.padding,d=n.boundary,c=n.rootBoundary,f=n.altBoundary,p=n.flipVariations,h=void 0===p||p,m=n.allowedAutoPlacements,v=t.options.placement,g=z(v),_=l||(g===v||!h?[le(v)]:function(e){if(z(e)===E)return[];var t=le(e);return[de(e),t,de(t)]}(v)),y=[v].concat(_).reduce(function(e,n){return e.concat(z(n)===E?ce(t,{placement:n,boundary:d,rootBoundary:c,padding:u,flipVariations:h,allowedAutoPlacements:m}):n)},[]),b=t.rects.reference,M=t.rects.popper,w=new Map,k=!0,L=y[0],Y=0;Y<y.length;Y++){var O=y[Y],C=z(O),j=W(O)===A,P=[x,T].indexOf(C)>=0,H=P?\"width\":\"height\",I=J(t,{placement:O,boundary:d,rootBoundary:c,altBoundary:f,padding:u}),F=P?j?S:D:j?T:x;b[H]>M[H]&&(F=le(F));var N=le(F),R=[];if(a&&R.push(I[C]<=0),s&&R.push(I[F]<=0,I[N]<=0),R.every(function(e){return e})){L=O,k=!1;break}w.set(O,R)}if(k)for(var V=function(e){var t=y.find(function(t){var n=w.get(t);if(n)return n.slice(0,e).every(function(e){return e})});if(t)return L=t,\"break\"},$=h?3:1;$>0;$--){if(\"break\"===V($))break}t.placement!==L&&(t.modifiersData[r]._skip=!0,t.placement=L,t.reset=!0)}},requiresIfExists:[\"offset\"],data:{_skip:!1}};function pe(e,t,n){return a(e,o(t,n))}var he={name:\"preventOverflow\",enabled:!0,phase:\"main\",fn:function(e){var t=e.state,n=e.options,r=e.name,i=n.mainAxis,s=void 0===i||i,l=n.altAxis,u=void 0!==l&&l,d=n.boundary,c=n.rootBoundary,f=n.altBoundary,p=n.padding,h=n.tether,m=void 0===h||h,v=n.tetherOffset,g=void 0===v?0:v,y=J(t,{boundary:d,rootBoundary:c,padding:p,altBoundary:f}),b=z(t.placement),M=W(t.placement),w=!M,k=U(b),E=\"x\"===k?\"y\":\"x\",Y=t.modifiersData.popperOffsets,O=t.rects.reference,C=t.rects.popper,j=\"function\"==typeof g?g(Object.assign({},t.rects,{placement:t.placement})):g,P=\"number\"==typeof j?{mainAxis:j,altAxis:j}:Object.assign({mainAxis:0,altAxis:0},j),H=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,I={x:0,y:0};if(Y){if(s){var F,N=\"y\"===k?x:D,R=\"y\"===k?T:S,V=\"y\"===k?\"height\":\"width\",$=Y[k],B=$+y[N],q=$-y[R],G=m?-C[V]/2:0,Z=M===A?O[V]:C[V],K=M===A?-C[V]:-O[V],X=t.elements.arrow,Q=m&&X?_(X):{width:0,height:0},ee=t.modifiersData[\"arrow#persistent\"]?t.modifiersData[\"arrow#persistent\"].padding:{top:0,right:0,bottom:0,left:0},te=ee[N],ne=ee[R],re=pe(0,O[V],Q[V]),ie=w?O[V]/2-G-re-te-P.mainAxis:Z-re-te-P.mainAxis,ae=w?-O[V]/2+G+re+ne+P.mainAxis:K+re+ne+P.mainAxis,oe=t.elements.arrow&&L(t.elements.arrow),se=oe?\"y\"===k?oe.clientTop||0:oe.clientLeft||0:0,le=null!=(F=null==H?void 0:H[k])?F:0,ue=$+ae-le,de=pe(m?o(B,$+ie-le-se):B,$,m?a(q,ue):q);Y[k]=de,I[k]=de-$}if(u){var ce,fe=\"x\"===k?x:D,he=\"x\"===k?T:S,me=Y[E],ve=\"y\"===E?\"height\":\"width\",ge=me+y[fe],_e=me-y[he],ye=-1!==[x,D].indexOf(b),be=null!=(ce=null==H?void 0:H[E])?ce:0,Me=ye?ge:me-O[ve]-C[ve]-be+P.altAxis,we=ye?me+O[ve]+C[ve]-be-P.altAxis:_e,ke=m&&ye?function(e,t,n){var r=pe(e,t,n);return r>n?n:r}(Me,me,we):pe(m?Me:ge,me,m?we:_e);Y[E]=ke,I[E]=ke-me}t.modifiersData[r]=I}},requiresIfExists:[\"offset\"]};var me={name:\"arrow\",enabled:!0,phase:\"main\",fn:function(e){var t,n=e.state,r=e.name,i=e.options,a=n.elements.arrow,o=n.modifiersData.popperOffsets,s=z(n.placement),l=U(s),u=[D,S].indexOf(s)>=0?\"height\":\"width\";if(a&&o){var d=function(e,t){return q(\"number\"!=typeof(e=\"function\"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:G(e,Y))}(i.padding,n),c=_(a),f=\"y\"===l?x:D,p=\"y\"===l?T:S,h=n.rects.reference[u]+n.rects.reference[l]-o[l]-n.rects.popper[u],m=o[l]-n.rects.reference[l],v=L(a),g=v?\"y\"===l?v.clientHeight||0:v.clientWidth||0:0,y=h/2-m/2,b=d[f],M=g-c[u]-d[p],w=g/2-c[u]/2+y,k=pe(b,w,M),E=l;n.modifiersData[r]=((t={})[E]=k,t.centerOffset=k-w,t)}},effect:function(e){var t=e.state,n=e.options.element,r=void 0===n?\"[data-popper-arrow]\":n;null!=r&&(\"string\"!=typeof r||(r=t.elements.popper.querySelector(r)))&&N(t.elements.popper,r)&&(t.elements.arrow=r)},requires:[\"popperOffsets\"],requiresIfExists:[\"preventOverflow\"]};function ve(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function ge(e){return[x,S,T,D].some(function(t){return e[t]>=0})}var _e={name:\"hide\",enabled:!0,phase:\"main\",requiresIfExists:[\"preventOverflow\"],fn:function(e){var t=e.state,n=e.name,r=t.rects.reference,i=t.rects.popper,a=t.modifiersData.preventOverflow,o=J(t,{elementContext:\"reference\"}),s=J(t,{altBoundary:!0}),l=ve(o,r),u=ve(s,i,a),d=ge(l),c=ge(u);t.modifiersData[n]={referenceClippingOffsets:l,popperEscapeOffsets:u,isReferenceHidden:d,hasPopperEscaped:c},t.attributes.popper=Object.assign({},t.attributes.popper,{\"data-popper-reference-hidden\":d,\"data-popper-escaped\":c})}},ye=X({defaultModifiers:[ee,te,ie,ae]}),be=[ee,te,ie,ae,oe,fe,he,me,_e],Me=X({defaultModifiers:be});e.applyStyles=ae,e.arrow=me,e.computeStyles=ie,e.createPopper=Me,e.createPopperLite=ye,e.defaultModifiers=be,e.detectOverflow=J,e.eventListeners=ee,e.flip=fe,e.hide=_e,e.offset=oe,e.popperGenerator=X,e.popperOffsets=te,e.preventOverflow=he,Object.defineProperty(e,\"__esModule\",{value:!0})}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t(require(\"@popperjs/core\")):\"function\"==typeof define&&define.amd?define([\"@popperjs/core\"],t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).bootstrap=t(e.Popper)}(this,function(e){\"use strict\";const t=function(e){const t=Object.create(null,{[Symbol.toStringTag]:{value:\"Module\"}});if(e)for(const n in e)if(\"default\"!==n){const r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:()=>e[n]})}return t.default=e,Object.freeze(t)}(e),n=new Map,r={set(e,t,r){n.has(e)||n.set(e,new Map);const i=n.get(e);i.has(t)||0===i.size?i.set(t,r):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(i.keys())[0]}.`)},get:(e,t)=>n.has(e)&&n.get(e).get(t)||null,remove(e,t){if(!n.has(e))return;const r=n.get(e);r.delete(t),0===r.size&&n.delete(e)}},i=\"transitionend\",a=e=>(e&&window.CSS&&window.CSS.escape&&(e=e.replace(/#([^\\s\"#']+)/g,(e,t)=>`#${CSS.escape(t)}`)),e),o=e=>null==e?`${e}`:Object.prototype.toString.call(e).match(/\\s([a-z]+)/i)[1].toLowerCase(),s=e=>{e.dispatchEvent(new Event(i))},l=e=>!(!e||\"object\"!=typeof e)&&(void 0!==e.jquery&&(e=e[0]),void 0!==e.nodeType),u=e=>l(e)?e.jquery?e[0]:e:\"string\"==typeof e&&e.length>0?document.querySelector(a(e)):null,d=e=>{if(!l(e)||0===e.getClientRects().length)return!1;const t=\"visible\"===getComputedStyle(e).getPropertyValue(\"visibility\"),n=e.closest(\"details:not([open])\");if(!n)return t;if(n!==e){const t=e.closest(\"summary\");if(t&&t.parentNode!==n)return!1;if(null===t)return!1}return t},c=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains(\"disabled\")||(void 0!==e.disabled?e.disabled:e.hasAttribute(\"disabled\")&&\"false\"!==e.getAttribute(\"disabled\")),f=e=>{if(!document.documentElement.attachShadow)return null;if(\"function\"==typeof e.getRootNode){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?f(e.parentNode):null},p=()=>{},h=e=>{e.offsetHeight},m=()=>window.jQuery&&!document.body.hasAttribute(\"data-bs-no-jquery\")?window.jQuery:null,v=[],g=()=>\"rtl\"===document.documentElement.dir,_=e=>{var t;t=()=>{const t=m();if(t){const n=e.NAME,r=t.fn[n];t.fn[n]=e.jQueryInterface,t.fn[n].Constructor=e,t.fn[n].noConflict=()=>(t.fn[n]=r,e.jQueryInterface)}},\"loading\"===document.readyState?(v.length||document.addEventListener(\"DOMContentLoaded\",()=>{for(const e of v)e()}),v.push(t)):t()},y=(e,t=[],n=e)=>\"function\"==typeof e?e.call(...t):n,b=(e,t,n=!0)=>{if(!n)return void y(e);const r=(e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const r=Number.parseFloat(t),i=Number.parseFloat(n);return r||i?(t=t.split(\",\")[0],n=n.split(\",\")[0],1e3*(Number.parseFloat(t)+Number.parseFloat(n))):0})(t)+5;let a=!1;const o=({target:n})=>{n===t&&(a=!0,t.removeEventListener(i,o),y(e))};t.addEventListener(i,o),setTimeout(()=>{a||s(t)},r)},M=(e,t,n,r)=>{const i=e.length;let a=e.indexOf(t);return-1===a?!n&&r?e[i-1]:e[0]:(a+=n?1:-1,r&&(a=(a+i)%i),e[Math.max(0,Math.min(a,i-1))])},w=/[^.]*(?=\\..*)\\.|.*/,k=/\\..*/,L=/::\\d+$/,x={};let T=1;const S={mouseenter:\"mouseover\",mouseleave:\"mouseout\"},D=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\"]);function E(e,t){return t&&`${t}::${T++}`||e.uidEvent||T++}function Y(e){const t=E(e);return e.uidEvent=t,x[t]=x[t]||{},x[t]}function A(e,t,n=null){return Object.values(e).find(e=>e.callable===t&&e.delegationSelector===n)}function O(e,t,n){const r=\"string\"==typeof t,i=r?n:t||n;let a=H(e);return D.has(a)||(a=e),[r,i,a]}function C(e,t,n,r,i){if(\"string\"!=typeof t||!e)return;let[a,o,s]=O(t,n,r);if(t in S){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};o=e(o)}const l=Y(e),u=l[s]||(l[s]={}),d=A(u,o,a?n:null);if(d)return void(d.oneOff=d.oneOff&&i);const c=E(o,t.replace(w,\"\")),f=a?function(e,t,n){return function r(i){const a=e.querySelectorAll(t);for(let{target:o}=i;o&&o!==this;o=o.parentNode)for(const s of a)if(s===o)return F(i,{delegateTarget:o}),r.oneOff&&I.off(e,i.type,t,n),n.apply(o,[i])}}(e,n,o):function(e,t){return function n(r){return F(r,{delegateTarget:e}),n.oneOff&&I.off(e,r.type,t),t.apply(e,[r])}}(e,o);f.delegationSelector=a?n:null,f.callable=o,f.oneOff=i,f.uidEvent=c,u[c]=f,e.addEventListener(s,f,a)}function j(e,t,n,r,i){const a=A(t[n],r,i);a&&(e.removeEventListener(n,a,Boolean(i)),delete t[n][a.uidEvent])}function P(e,t,n,r){const i=t[n]||{};for(const[a,o]of Object.entries(i))a.includes(r)&&j(e,t,n,o.callable,o.delegationSelector)}function H(e){return e=e.replace(k,\"\"),S[e]||e}const I={on(e,t,n,r){C(e,t,n,r,!1)},one(e,t,n,r){C(e,t,n,r,!0)},off(e,t,n,r){if(\"string\"!=typeof t||!e)return;const[i,a,o]=O(t,n,r),s=o!==t,l=Y(e),u=l[o]||{},d=t.startsWith(\".\");if(void 0===a){if(d)for(const n of Object.keys(l))P(e,l,n,t.slice(1));for(const[n,r]of Object.entries(u)){const i=n.replace(L,\"\");s&&!t.includes(i)||j(e,l,o,r.callable,r.delegationSelector)}}else{if(!Object.keys(u).length)return;j(e,l,o,a,i?n:null)}},trigger(e,t,n){if(\"string\"!=typeof t||!e)return null;const r=m();let i=null,a=!0,o=!0,s=!1;t!==H(t)&&r&&(i=r.Event(t,n),r(e).trigger(i),a=!i.isPropagationStopped(),o=!i.isImmediatePropagationStopped(),s=i.isDefaultPrevented());const l=F(new Event(t,{bubbles:a,cancelable:!0}),n);return s&&l.preventDefault(),o&&e.dispatchEvent(l),l.defaultPrevented&&i&&i.preventDefault(),l}};function F(e,t={}){for(const[n,r]of Object.entries(t))try{e[n]=r}catch(t){Object.defineProperty(e,n,{configurable:!0,get:()=>r})}return e}function N(e){if(\"true\"===e)return!0;if(\"false\"===e)return!1;if(e===Number(e).toString())return Number(e);if(\"\"===e||\"null\"===e)return null;if(\"string\"!=typeof e)return e;try{return JSON.parse(decodeURIComponent(e))}catch(t){return e}}function R(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}const V={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${R(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${R(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter(e=>e.startsWith(\"bs\")&&!e.startsWith(\"bsConfig\"));for(const r of n){let n=r.replace(/^bs/,\"\");n=n.charAt(0).toLowerCase()+n.slice(1),t[n]=N(e.dataset[r])}return t},getDataAttribute:(e,t)=>N(e.getAttribute(`data-bs-${R(t)}`))};class ${static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method \"NAME\", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=l(t)?V.getDataAttribute(t,\"config\"):{};return{...this.constructor.Default,...\"object\"==typeof n?n:{},...l(t)?V.getDataAttributes(t):{},...\"object\"==typeof e?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const[n,r]of Object.entries(t)){const t=e[n],i=l(t)?\"element\":o(t);if(!new RegExp(r).test(i))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${n}\" provided type \"${i}\" but expected type \"${r}\".`)}}}class z extends ${constructor(e,t){super(),(e=u(e))&&(this._element=e,this._config=this._getConfig(t),r.set(this._element,this.constructor.DATA_KEY,this))}dispose(){r.remove(this._element,this.constructor.DATA_KEY),I.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){b(e,t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return r.get(u(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,\"object\"==typeof t?t:null)}static get VERSION(){return\"5.3.8\"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const W=e=>{let t=e.getAttribute(\"data-bs-target\");if(!t||\"#\"===t){let n=e.getAttribute(\"href\");if(!n||!n.includes(\"#\")&&!n.startsWith(\".\"))return null;n.includes(\"#\")&&!n.startsWith(\"#\")&&(n=`#${n.split(\"#\")[1]}`),t=n&&\"#\"!==n?n.trim():null}return t?t.split(\",\").map(e=>a(e)).join(\",\"):null},U={find:(e,t=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(t,e)),findOne:(e,t=document.documentElement)=>Element.prototype.querySelector.call(t,e),children:(e,t)=>[].concat(...e.children).filter(e=>e.matches(t)),parents(e,t){const n=[];let r=e.parentNode.closest(t);for(;r;)n.push(r),r=r.parentNode.closest(t);return n},prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=[\"a\",\"button\",\"input\",\"textarea\",\"select\",\"details\",\"[tabindex]\",'[contenteditable=\"true\"]'].map(e=>`${e}:not([tabindex^=\"-\"])`).join(\",\");return this.find(t,e).filter(e=>!c(e)&&d(e))},getSelectorFromElement(e){const t=W(e);return t&&U.findOne(t)?t:null},getElementFromSelector(e){const t=W(e);return t?U.findOne(t):null},getMultipleElementsFromSelector(e){const t=W(e);return t?U.find(t):[]}},B=(e,t=\"hide\")=>{const n=`click.dismiss${e.EVENT_KEY}`,r=e.NAME;I.on(document,n,`[data-bs-dismiss=\"${r}\"]`,function(n){if([\"A\",\"AREA\"].includes(this.tagName)&&n.preventDefault(),c(this))return;const i=U.getElementFromSelector(this)||this.closest(`.${r}`);e.getOrCreateInstance(i)[t]()})},q=\".bs.alert\",G=`close${q}`,J=`closed${q}`;class Z extends z{static get NAME(){return\"alert\"}close(){if(I.trigger(this._element,G).defaultPrevented)return;this._element.classList.remove(\"show\");const e=this._element.classList.contains(\"fade\");this._queueCallback(()=>this._destroyElement(),this._element,e)}_destroyElement(){this._element.remove(),I.trigger(this._element,J),this.dispose()}static jQueryInterface(e){return this.each(function(){const t=Z.getOrCreateInstance(this);if(\"string\"==typeof e){if(void 0===t[e]||e.startsWith(\"_\")||\"constructor\"===e)throw new TypeError(`No method named \"${e}\"`);t[e](this)}})}}B(Z,\"close\"),_(Z);const K='[data-bs-toggle=\"button\"]';class X extends z{static get NAME(){return\"button\"}toggle(){this._element.setAttribute(\"aria-pressed\",this._element.classList.toggle(\"active\"))}static jQueryInterface(e){return this.each(function(){const t=X.getOrCreateInstance(this);\"toggle\"===e&&t[e]()})}}I.on(document,\"click.bs.button.data-api\",K,e=>{e.preventDefault();const t=e.target.closest(K);X.getOrCreateInstance(t).toggle()}),_(X);const Q=\".bs.swipe\",ee=`touchstart${Q}`,te=`touchmove${Q}`,ne=`touchend${Q}`,re=`pointerdown${Q}`,ie=`pointerup${Q}`,ae={endCallback:null,leftCallback:null,rightCallback:null},oe={endCallback:\"(function|null)\",leftCallback:\"(function|null)\",rightCallback:\"(function|null)\"};class se extends ${constructor(e,t){super(),this._element=e,e&&se.isSupported()&&(this._config=this._getConfig(t),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return ae}static get DefaultType(){return oe}static get NAME(){return\"swipe\"}dispose(){I.off(this._element,Q)}_start(e){this._supportPointerEvents?this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX):this._deltaX=e.touches[0].clientX}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX),this._handleSwipe(),y(this._config.endCallback)}_move(e){this._deltaX=e.touches&&e.touches.length>1?0:e.touches[0].clientX-this._deltaX}_handleSwipe(){const e=Math.abs(this._deltaX);if(e<=40)return;const t=e/this._deltaX;this._deltaX=0,t&&y(t>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(I.on(this._element,re,e=>this._start(e)),I.on(this._element,ie,e=>this._end(e)),this._element.classList.add(\"pointer-event\")):(I.on(this._element,ee,e=>this._start(e)),I.on(this._element,te,e=>this._move(e)),I.on(this._element,ne,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&(\"pen\"===e.pointerType||\"touch\"===e.pointerType)}static isSupported(){return\"ontouchstart\"in document.documentElement||navigator.maxTouchPoints>0}}const le=\".bs.carousel\",ue=\".data-api\",de=\"ArrowLeft\",ce=\"ArrowRight\",fe=\"next\",pe=\"prev\",he=\"left\",me=\"right\",ve=`slide${le}`,ge=`slid${le}`,_e=`keydown${le}`,ye=`mouseenter${le}`,be=`mouseleave${le}`,Me=`dragstart${le}`,we=`load${le}${ue}`,ke=`click${le}${ue}`,Le=\"carousel\",xe=\"active\",Te=\".active\",Se=\".carousel-item\",De=Te+Se,Ee={[de]:me,[ce]:he},Ye={interval:5e3,keyboard:!0,pause:\"hover\",ride:!1,touch:!0,wrap:!0},Ae={interval:\"(number|boolean)\",keyboard:\"boolean\",pause:\"(string|boolean)\",ride:\"(boolean|string)\",touch:\"boolean\",wrap:\"boolean\"};class Oe extends z{constructor(e,t){super(e,t),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=U.findOne(\".carousel-indicators\",this._element),this._addEventListeners(),this._config.ride===Le&&this.cycle()}static get Default(){return Ye}static get DefaultType(){return Ae}static get NAME(){return\"carousel\"}next(){this._slide(fe)}nextWhenVisible(){!document.hidden&&d(this._element)&&this.next()}prev(){this._slide(pe)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?I.one(this._element,ge,()=>this.cycle()):this.cycle())}to(e){const t=this._getItems();if(e>t.length-1||e<0)return;if(this._isSliding)return void I.one(this._element,ge,()=>this.to(e));const n=this._getItemIndex(this._getActive());if(n===e)return;const r=e>n?fe:pe;this._slide(r,t[e])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(e){return e.defaultInterval=e.interval,e}_addEventListeners(){this._config.keyboard&&I.on(this._element,_e,e=>this._keydown(e)),\"hover\"===this._config.pause&&(I.on(this._element,ye,()=>this.pause()),I.on(this._element,be,()=>this._maybeEnableCycle())),this._config.touch&&se.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const e of U.find(\".carousel-item img\",this._element))I.on(e,Me,e=>e.preventDefault());const e={leftCallback:()=>this._slide(this._directionToOrder(he)),rightCallback:()=>this._slide(this._directionToOrder(me)),endCallback:()=>{\"hover\"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),500+this._config.interval))}};this._swipeHelper=new se(this._element,e)}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=Ee[e.key];t&&(e.preventDefault(),this._slide(this._directionToOrder(t)))}_getItemIndex(e){return this._getItems().indexOf(e)}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const t=U.findOne(Te,this._indicatorsElement);t.classList.remove(xe),t.removeAttribute(\"aria-current\");const n=U.findOne(`[data-bs-slide-to=\"${e}\"]`,this._indicatorsElement);n&&(n.classList.add(xe),n.setAttribute(\"aria-current\",\"true\"))}_updateInterval(){const e=this._activeElement||this._getActive();if(!e)return;const t=Number.parseInt(e.getAttribute(\"data-bs-interval\"),10);this._config.interval=t||this._config.defaultInterval}_slide(e,t=null){if(this._isSliding)return;const n=this._getActive(),r=e===fe,i=t||M(this._getItems(),n,r,this._config.wrap);if(i===n)return;const a=this._getItemIndex(i),o=t=>I.trigger(this._element,t,{relatedTarget:i,direction:this._orderToDirection(e),from:this._getItemIndex(n),to:a});if(o(ve).defaultPrevented)return;if(!n||!i)return;const s=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(a),this._activeElement=i;const l=r?\"carousel-item-start\":\"carousel-item-end\",u=r?\"carousel-item-next\":\"carousel-item-prev\";i.classList.add(u),h(i),n.classList.add(l),i.classList.add(l),this._queueCallback(()=>{i.classList.remove(l,u),i.classList.add(xe),n.classList.remove(xe,u,l),this._isSliding=!1,o(ge)},n,this._isAnimated()),s&&this.cycle()}_isAnimated(){return this._element.classList.contains(\"slide\")}_getActive(){return U.findOne(De,this._element)}_getItems(){return U.find(Se,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(e){return g()?e===he?pe:fe:e===he?fe:pe}_orderToDirection(e){return g()?e===pe?he:me:e===pe?me:he}static jQueryInterface(e){return this.each(function(){const t=Oe.getOrCreateInstance(this,e);if(\"number\"!=typeof e){if(\"string\"==typeof e){if(void 0===t[e]||e.startsWith(\"_\")||\"constructor\"===e)throw new TypeError(`No method named \"${e}\"`);t[e]()}}else t.to(e)})}}I.on(document,ke,\"[data-bs-slide], [data-bs-slide-to]\",function(e){const t=U.getElementFromSelector(this);if(!t||!t.classList.contains(Le))return;e.preventDefault();const n=Oe.getOrCreateInstance(t),r=this.getAttribute(\"data-bs-slide-to\");return r?(n.to(r),void n._maybeEnableCycle()):\"next\"===V.getDataAttribute(this,\"slide\")?(n.next(),void n._maybeEnableCycle()):(n.prev(),void n._maybeEnableCycle())}),I.on(window,we,()=>{const e=U.find('[data-bs-ride=\"carousel\"]');for(const t of e)Oe.getOrCreateInstance(t)}),_(Oe);const Ce=\".bs.collapse\",je=`show${Ce}`,Pe=`shown${Ce}`,He=`hide${Ce}`,Ie=`hidden${Ce}`,Fe=`click${Ce}.data-api`,Ne=\"show\",Re=\"collapse\",Ve=\"collapsing\",$e=`:scope .${Re} .${Re}`,ze='[data-bs-toggle=\"collapse\"]',We={parent:null,toggle:!0},Ue={parent:\"(null|element)\",toggle:\"boolean\"};class Be extends z{constructor(e,t){super(e,t),this._isTransitioning=!1,this._triggerArray=[];const n=U.find(ze);for(const e of n){const t=U.getSelectorFromElement(e),n=U.find(t).filter(e=>e===this._element);null!==t&&n.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return We}static get DefaultType(){return Ue}static get NAME(){return\"collapse\"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let e=[];if(this._config.parent&&(e=this._getFirstLevelChildren(\".collapse.show, .collapse.collapsing\").filter(e=>e!==this._element).map(e=>Be.getOrCreateInstance(e,{toggle:!1}))),e.length&&e[0]._isTransitioning)return;if(I.trigger(this._element,je).defaultPrevented)return;for(const t of e)t.hide();const t=this._getDimension();this._element.classList.remove(Re),this._element.classList.add(Ve),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const n=`scroll${t[0].toUpperCase()+t.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(Ve),this._element.classList.add(Re,Ne),this._element.style[t]=\"\",I.trigger(this._element,Pe)},this._element,!0),this._element.style[t]=`${this._element[n]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(I.trigger(this._element,He).defaultPrevented)return;const e=this._getDimension();this._element.style[e]=`${this._element.getBoundingClientRect()[e]}px`,h(this._element),this._element.classList.add(Ve),this._element.classList.remove(Re,Ne);for(const e of this._triggerArray){const t=U.getElementFromSelector(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0,this._element.style[e]=\"\",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(Ve),this._element.classList.add(Re),I.trigger(this._element,Ie)},this._element,!0)}_isShown(e=this._element){return e.classList.contains(Ne)}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=u(e.parent),e}_getDimension(){return this._element.classList.contains(\"collapse-horizontal\")?\"width\":\"height\"}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(ze);for(const t of e){const e=U.getElementFromSelector(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}}_getFirstLevelChildren(e){const t=U.find($e,this._config.parent);return U.find(e,this._config.parent).filter(e=>!t.includes(e))}_addAriaAndCollapsedClass(e,t){if(e.length)for(const n of e)n.classList.toggle(\"collapsed\",!t),n.setAttribute(\"aria-expanded\",t)}static jQueryInterface(e){const t={};return\"string\"==typeof e&&/show|hide/.test(e)&&(t.toggle=!1),this.each(function(){const n=Be.getOrCreateInstance(this,t);if(\"string\"==typeof e){if(void 0===n[e])throw new TypeError(`No method named \"${e}\"`);n[e]()}})}}I.on(document,Fe,ze,function(e){(\"A\"===e.target.tagName||e.delegateTarget&&\"A\"===e.delegateTarget.tagName)&&e.preventDefault();for(const e of U.getMultipleElementsFromSelector(this))Be.getOrCreateInstance(e,{toggle:!1}).toggle()}),_(Be);const qe=\"dropdown\",Ge=\".bs.dropdown\",Je=\".data-api\",Ze=\"ArrowUp\",Ke=\"ArrowDown\",Xe=`hide${Ge}`,Qe=`hidden${Ge}`,et=`show${Ge}`,tt=`shown${Ge}`,nt=`click${Ge}${Je}`,rt=`keydown${Ge}${Je}`,it=`keyup${Ge}${Je}`,at=\"show\",ot='[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)',st=`${ot}.${at}`,lt=\".dropdown-menu\",ut=g()?\"top-end\":\"top-start\",dt=g()?\"top-start\":\"top-end\",ct=g()?\"bottom-end\":\"bottom-start\",ft=g()?\"bottom-start\":\"bottom-end\",pt=g()?\"left-start\":\"right-start\",ht=g()?\"right-start\":\"left-start\",mt={autoClose:!0,boundary:\"clippingParents\",display:\"dynamic\",offset:[0,2],popperConfig:null,reference:\"toggle\"},vt={autoClose:\"(boolean|string)\",boundary:\"(string|element)\",display:\"string\",offset:\"(array|string|function)\",popperConfig:\"(null|object|function)\",reference:\"(string|element|object)\"};class gt extends z{constructor(e,t){super(e,t),this._popper=null,this._parent=this._element.parentNode,this._menu=U.next(this._element,lt)[0]||U.prev(this._element,lt)[0]||U.findOne(lt,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return mt}static get DefaultType(){return vt}static get NAME(){return qe}toggle(){return this._isShown()?this.hide():this.show()}show(){if(c(this._element)||this._isShown())return;const e={relatedTarget:this._element};if(!I.trigger(this._element,et,e).defaultPrevented){if(this._createPopper(),\"ontouchstart\"in document.documentElement&&!this._parent.closest(\".navbar-nav\"))for(const e of[].concat(...document.body.children))I.on(e,\"mouseover\",p);this._element.focus(),this._element.setAttribute(\"aria-expanded\",!0),this._menu.classList.add(at),this._element.classList.add(at),I.trigger(this._element,tt,e)}}hide(){if(c(this._element)||!this._isShown())return;const e={relatedTarget:this._element};this._completeHide(e)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(e){if(!I.trigger(this._element,Xe,e).defaultPrevented){if(\"ontouchstart\"in document.documentElement)for(const e of[].concat(...document.body.children))I.off(e,\"mouseover\",p);this._popper&&this._popper.destroy(),this._menu.classList.remove(at),this._element.classList.remove(at),this._element.setAttribute(\"aria-expanded\",\"false\"),V.removeDataAttribute(this._menu,\"popper\"),I.trigger(this._element,Qe,e)}}_getConfig(e){if(\"object\"==typeof(e=super._getConfig(e)).reference&&!l(e.reference)&&\"function\"!=typeof e.reference.getBoundingClientRect)throw new TypeError(`${qe.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);return e}_createPopper(){if(void 0===t)throw new TypeError(\"Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)\");let e=this._element;\"parent\"===this._config.reference?e=this._parent:l(this._config.reference)?e=u(this._config.reference):\"object\"==typeof this._config.reference&&(e=this._config.reference);const n=this._getPopperConfig();this._popper=t.createPopper(e,this._menu,n)}_isShown(){return this._menu.classList.contains(at)}_getPlacement(){const e=this._parent;if(e.classList.contains(\"dropend\"))return pt;if(e.classList.contains(\"dropstart\"))return ht;if(e.classList.contains(\"dropup-center\"))return\"top\";if(e.classList.contains(\"dropdown-center\"))return\"bottom\";const t=\"end\"===getComputedStyle(this._menu).getPropertyValue(\"--bs-position\").trim();return e.classList.contains(\"dropup\")?t?dt:ut:t?ft:ct}_detectNavbar(){return null!==this._element.closest(\".navbar\")}_getOffset(){const{offset:e}=this._config;return\"string\"==typeof e?e.split(\",\").map(e=>Number.parseInt(e,10)):\"function\"==typeof e?t=>e(t,this._element):e}_getPopperConfig(){const e={placement:this._getPlacement(),modifiers:[{name:\"preventOverflow\",options:{boundary:this._config.boundary}},{name:\"offset\",options:{offset:this._getOffset()}}]};return(this._inNavbar||\"static\"===this._config.display)&&(V.setDataAttribute(this._menu,\"popper\",\"static\"),e.modifiers=[{name:\"applyStyles\",enabled:!1}]),{...e,...y(this._config.popperConfig,[void 0,e])}}_selectMenuItem({key:e,target:t}){const n=U.find(\".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)\",this._menu).filter(e=>d(e));n.length&&M(n,t,e===Ke,!n.includes(t)).focus()}static jQueryInterface(e){return this.each(function(){const t=gt.getOrCreateInstance(this,e);if(\"string\"==typeof e){if(void 0===t[e])throw new TypeError(`No method named \"${e}\"`);t[e]()}})}static clearMenus(e){if(2===e.button||\"keyup\"===e.type&&\"Tab\"!==e.key)return;const t=U.find(st);for(const n of t){const t=gt.getInstance(n);if(!t||!1===t._config.autoClose)continue;const r=e.composedPath(),i=r.includes(t._menu);if(r.includes(t._element)||\"inside\"===t._config.autoClose&&!i||\"outside\"===t._config.autoClose&&i)continue;if(t._menu.contains(e.target)&&(\"keyup\"===e.type&&\"Tab\"===e.key||/input|select|option|textarea|form/i.test(e.target.tagName)))continue;const a={relatedTarget:t._element};\"click\"===e.type&&(a.clickEvent=e),t._completeHide(a)}}static dataApiKeydownHandler(e){const t=/input|textarea/i.test(e.target.tagName),n=\"Escape\"===e.key,r=[Ze,Ke].includes(e.key);if(!r&&!n)return;if(t&&!n)return;e.preventDefault();const i=this.matches(ot)?this:U.prev(this,ot)[0]||U.next(this,ot)[0]||U.findOne(ot,e.delegateTarget.parentNode),a=gt.getOrCreateInstance(i);if(r)return e.stopPropagation(),a.show(),void a._selectMenuItem(e);a._isShown()&&(e.stopPropagation(),a.hide(),i.focus())}}I.on(document,rt,ot,gt.dataApiKeydownHandler),I.on(document,rt,lt,gt.dataApiKeydownHandler),I.on(document,nt,gt.clearMenus),I.on(document,it,gt.clearMenus),I.on(document,nt,ot,function(e){e.preventDefault(),gt.getOrCreateInstance(this).toggle()}),_(gt);const _t=\"backdrop\",yt=\"show\",bt=`mousedown.bs.${_t}`,Mt={className:\"modal-backdrop\",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:\"body\"},wt={className:\"string\",clickCallback:\"(function|null)\",isAnimated:\"boolean\",isVisible:\"boolean\",rootElement:\"(element|string)\"};class kt extends ${constructor(e){super(),this._config=this._getConfig(e),this._isAppended=!1,this._element=null}static get Default(){return Mt}static get DefaultType(){return wt}static get NAME(){return _t}show(e){if(!this._config.isVisible)return void y(e);this._append();const t=this._getElement();this._config.isAnimated&&h(t),t.classList.add(yt),this._emulateAnimation(()=>{y(e)})}hide(e){this._config.isVisible?(this._getElement().classList.remove(yt),this._emulateAnimation(()=>{this.dispose(),y(e)})):y(e)}dispose(){this._isAppended&&(I.off(this._element,bt),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const e=document.createElement(\"div\");e.className=this._config.className,this._config.isAnimated&&e.classList.add(\"fade\"),this._element=e}return this._element}_configAfterMerge(e){return e.rootElement=u(e.rootElement),e}_append(){if(this._isAppended)return;const e=this._getElement();this._config.rootElement.append(e),I.on(e,bt,()=>{y(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(e){b(e,this._getElement(),this._config.isAnimated)}}const Lt=\".bs.focustrap\",xt=`focusin${Lt}`,Tt=`keydown.tab${Lt}`,St=\"backward\",Dt={autofocus:!0,trapElement:null},Et={autofocus:\"boolean\",trapElement:\"element\"};class Yt extends ${constructor(e){super(),this._config=this._getConfig(e),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Dt}static get DefaultType(){return Et}static get NAME(){return\"focustrap\"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),I.off(document,Lt),I.on(document,xt,e=>this._handleFocusin(e)),I.on(document,Tt,e=>this._handleKeydown(e)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,I.off(document,Lt))}_handleFocusin(e){const{trapElement:t}=this._config;if(e.target===document||e.target===t||t.contains(e.target))return;const n=U.focusableChildren(t);0===n.length?t.focus():this._lastTabNavDirection===St?n[n.length-1].focus():n[0].focus()}_handleKeydown(e){\"Tab\"===e.key&&(this._lastTabNavDirection=e.shiftKey?St:\"forward\")}}const At=\".fixed-top, .fixed-bottom, .is-fixed, .sticky-top\",Ot=\".sticky-top\",Ct=\"padding-right\",jt=\"margin-right\";class Pt{constructor(){this._element=document.body}getWidth(){const e=document.documentElement.clientWidth;return Math.abs(window.innerWidth-e)}hide(){const e=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Ct,t=>t+e),this._setElementAttributes(At,Ct,t=>t+e),this._setElementAttributes(Ot,jt,t=>t-e)}reset(){this._resetElementAttributes(this._element,\"overflow\"),this._resetElementAttributes(this._element,Ct),this._resetElementAttributes(At,Ct),this._resetElementAttributes(Ot,jt)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,\"overflow\"),this._element.style.overflow=\"hidden\"}_setElementAttributes(e,t,n){const r=this.getWidth();this._applyManipulationCallback(e,e=>{if(e!==this._element&&window.innerWidth>e.clientWidth+r)return;this._saveInitialAttribute(e,t);const i=window.getComputedStyle(e).getPropertyValue(t);e.style.setProperty(t,`${n(Number.parseFloat(i))}px`)})}_saveInitialAttribute(e,t){const n=e.style.getPropertyValue(t);n&&V.setDataAttribute(e,t,n)}_resetElementAttributes(e,t){this._applyManipulationCallback(e,e=>{const n=V.getDataAttribute(e,t);null!==n?(V.removeDataAttribute(e,t),e.style.setProperty(t,n)):e.style.removeProperty(t)})}_applyManipulationCallback(e,t){if(l(e))t(e);else for(const n of U.find(e,this._element))t(n)}}const Ht=\".bs.modal\",It=`hide${Ht}`,Ft=`hidePrevented${Ht}`,Nt=`hidden${Ht}`,Rt=`show${Ht}`,Vt=`shown${Ht}`,$t=`resize${Ht}`,zt=`click.dismiss${Ht}`,Wt=`mousedown.dismiss${Ht}`,Ut=`keydown.dismiss${Ht}`,Bt=`click${Ht}.data-api`,qt=\"modal-open\",Gt=\"show\",Jt=\"modal-static\",Zt={backdrop:!0,focus:!0,keyboard:!0},Kt={backdrop:\"(boolean|string)\",focus:\"boolean\",keyboard:\"boolean\"};class Xt extends z{constructor(e,t){super(e,t),this._dialog=U.findOne(\".modal-dialog\",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new Pt,this._addEventListeners()}static get Default(){return Zt}static get DefaultType(){return Kt}static get NAME(){return\"modal\"}toggle(e){return this._isShown?this.hide():this.show(e)}show(e){this._isShown||this._isTransitioning||I.trigger(this._element,Rt,{relatedTarget:e}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(qt),this._adjustDialog(),this._backdrop.show(()=>this._showElement(e)))}hide(){this._isShown&&!this._isTransitioning&&(I.trigger(this._element,It).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Gt),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())))}dispose(){I.off(window,Ht),I.off(this._dialog,Ht),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new kt({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Yt({trapElement:this._element})}_showElement(e){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display=\"block\",this._element.removeAttribute(\"aria-hidden\"),this._element.setAttribute(\"aria-modal\",!0),this._element.setAttribute(\"role\",\"dialog\"),this._element.scrollTop=0;const t=U.findOne(\".modal-body\",this._dialog);t&&(t.scrollTop=0),h(this._element),this._element.classList.add(Gt),this._queueCallback(()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,I.trigger(this._element,Vt,{relatedTarget:e})},this._dialog,this._isAnimated())}_addEventListeners(){I.on(this._element,Ut,e=>{\"Escape\"===e.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())}),I.on(window,$t,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),I.on(this._element,Wt,e=>{I.one(this._element,zt,t=>{this._element===e.target&&this._element===t.target&&(\"static\"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())})})}_hideModal(){this._element.style.display=\"none\",this._element.setAttribute(\"aria-hidden\",!0),this._element.removeAttribute(\"aria-modal\"),this._element.removeAttribute(\"role\"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(qt),this._resetAdjustments(),this._scrollBar.reset(),I.trigger(this._element,Nt)})}_isAnimated(){return this._element.classList.contains(\"fade\")}_triggerBackdropTransition(){if(I.trigger(this._element,Ft).defaultPrevented)return;const e=this._element.scrollHeight>document.documentElement.clientHeight,t=this._element.style.overflowY;\"hidden\"===t||this._element.classList.contains(Jt)||(e||(this._element.style.overflowY=\"hidden\"),this._element.classList.add(Jt),this._queueCallback(()=>{this._element.classList.remove(Jt),this._queueCallback(()=>{this._element.style.overflowY=t},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const e=this._element.scrollHeight>document.documentElement.clientHeight,t=this._scrollBar.getWidth(),n=t>0;if(n&&!e){const e=g()?\"paddingLeft\":\"paddingRight\";this._element.style[e]=`${t}px`}if(!n&&e){const e=g()?\"paddingRight\":\"paddingLeft\";this._element.style[e]=`${t}px`}}_resetAdjustments(){this._element.style.paddingLeft=\"\",this._element.style.paddingRight=\"\"}static jQueryInterface(e,t){return this.each(function(){const n=Xt.getOrCreateInstance(this,e);if(\"string\"==typeof e){if(void 0===n[e])throw new TypeError(`No method named \"${e}\"`);n[e](t)}})}}I.on(document,Bt,'[data-bs-toggle=\"modal\"]',function(e){const t=U.getElementFromSelector(this);[\"A\",\"AREA\"].includes(this.tagName)&&e.preventDefault(),I.one(t,Rt,e=>{e.defaultPrevented||I.one(t,Nt,()=>{d(this)&&this.focus()})});const n=U.findOne(\".modal.show\");n&&Xt.getInstance(n).hide(),Xt.getOrCreateInstance(t).toggle(this)}),B(Xt),_(Xt);const Qt=\".bs.offcanvas\",en=\".data-api\",tn=`load${Qt}${en}`,nn=\"show\",rn=\"showing\",an=\"hiding\",on=\".offcanvas.show\",sn=`show${Qt}`,ln=`shown${Qt}`,un=`hide${Qt}`,dn=`hidePrevented${Qt}`,cn=`hidden${Qt}`,fn=`resize${Qt}`,pn=`click${Qt}${en}`,hn=`keydown.dismiss${Qt}`,mn={backdrop:!0,keyboard:!0,scroll:!1},vn={backdrop:\"(boolean|string)\",keyboard:\"boolean\",scroll:\"boolean\"};class gn extends z{constructor(e,t){super(e,t),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return mn}static get DefaultType(){return vn}static get NAME(){return\"offcanvas\"}toggle(e){return this._isShown?this.hide():this.show(e)}show(e){this._isShown||I.trigger(this._element,sn,{relatedTarget:e}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new Pt).hide(),this._element.setAttribute(\"aria-modal\",!0),this._element.setAttribute(\"role\",\"dialog\"),this._element.classList.add(rn),this._queueCallback(()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(nn),this._element.classList.remove(rn),I.trigger(this._element,ln,{relatedTarget:e})},this._element,!0))}hide(){this._isShown&&(I.trigger(this._element,un).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(an),this._backdrop.hide(),this._queueCallback(()=>{this._element.classList.remove(nn,an),this._element.removeAttribute(\"aria-modal\"),this._element.removeAttribute(\"role\"),this._config.scroll||(new Pt).reset(),I.trigger(this._element,cn)},this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const e=Boolean(this._config.backdrop);return new kt({className:\"offcanvas-backdrop\",isVisible:e,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:e?()=>{\"static\"!==this._config.backdrop?this.hide():I.trigger(this._element,dn)}:null})}_initializeFocusTrap(){return new Yt({trapElement:this._element})}_addEventListeners(){I.on(this._element,hn,e=>{\"Escape\"===e.key&&(this._config.keyboard?this.hide():I.trigger(this._element,dn))})}static jQueryInterface(e){return this.each(function(){const t=gn.getOrCreateInstance(this,e);if(\"string\"==typeof e){if(void 0===t[e]||e.startsWith(\"_\")||\"constructor\"===e)throw new TypeError(`No method named \"${e}\"`);t[e](this)}})}}I.on(document,pn,'[data-bs-toggle=\"offcanvas\"]',function(e){const t=U.getElementFromSelector(this);if([\"A\",\"AREA\"].includes(this.tagName)&&e.preventDefault(),c(this))return;I.one(t,cn,()=>{d(this)&&this.focus()});const n=U.findOne(on);n&&n!==t&&gn.getInstance(n).hide(),gn.getOrCreateInstance(t).toggle(this)}),I.on(window,tn,()=>{for(const e of U.find(on))gn.getOrCreateInstance(e).show()}),I.on(window,fn,()=>{for(const e of U.find(\"[aria-modal][class*=show][class*=offcanvas-]\"))\"fixed\"!==getComputedStyle(e).position&&gn.getOrCreateInstance(e).hide()}),B(gn),_(gn);const _n={\"*\":[\"class\",\"dir\",\"id\",\"lang\",\"role\",/^aria-[\\w-]*$/i],a:[\"target\",\"href\",\"title\",\"rel\"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:[\"src\",\"srcset\",\"alt\",\"title\",\"width\",\"height\"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},yn=new Set([\"background\",\"cite\",\"href\",\"itemtype\",\"longdesc\",\"poster\",\"src\",\"xlink:href\"]),bn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Mn=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!yn.has(n)||Boolean(bn.test(e.nodeValue)):t.filter(e=>e instanceof RegExp).some(e=>e.test(n))},wn={allowList:_n,content:{},extraClass:\"\",html:!1,sanitize:!0,sanitizeFn:null,template:\"<div></div>\"},kn={allowList:\"object\",content:\"object\",extraClass:\"(string|function)\",html:\"boolean\",sanitize:\"boolean\",sanitizeFn:\"(null|function)\",template:\"string\"},Ln={entry:\"(string|element|function|null)\",selector:\"(string|element)\"};class xn extends ${constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return wn}static get DefaultType(){return kn}static get NAME(){return\"TemplateFactory\"}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement(\"div\");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(\" \")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},Ln)}_setContent(e,t,n){const r=U.findOne(n,e);r&&((t=this._resolvePossibleFunction(t))?l(t)?this._putElementInTemplate(u(t),r):this._config.html?r.innerHTML=this._maybeSanitize(t):r.textContent=t:r.remove())}_maybeSanitize(e){return this._config.sanitize?function(e,t,n){if(!e.length)return e;if(n&&\"function\"==typeof n)return n(e);const r=(new window.DOMParser).parseFromString(e,\"text/html\"),i=[].concat(...r.body.querySelectorAll(\"*\"));for(const e of i){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const r=[].concat(...e.attributes),i=[].concat(t[\"*\"]||[],t[n]||[]);for(const t of r)Mn(t,i)||e.removeAttribute(t.nodeName)}return r.body.innerHTML}(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return y(e,[void 0,this])}_putElementInTemplate(e,t){if(this._config.html)return t.innerHTML=\"\",void t.append(e);t.textContent=e.textContent}}const Tn=new Set([\"sanitize\",\"allowList\",\"sanitizeFn\"]),Sn=\"fade\",Dn=\"show\",En=\".tooltip-inner\",Yn=\".modal\",An=\"hide.bs.modal\",On=\"hover\",Cn=\"focus\",jn=\"click\",Pn={AUTO:\"auto\",TOP:\"top\",RIGHT:g()?\"left\":\"right\",BOTTOM:\"bottom\",LEFT:g()?\"right\":\"left\"},Hn={allowList:_n,animation:!0,boundary:\"clippingParents\",container:!1,customClass:\"\",delay:0,fallbackPlacements:[\"top\",\"right\",\"bottom\",\"left\"],html:!1,offset:[0,6],placement:\"top\",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'<div class=\"tooltip\" role=\"tooltip\"><div class=\"tooltip-arrow\"></div><div class=\"tooltip-inner\"></div></div>',title:\"\",trigger:\"hover focus\"},In={allowList:\"object\",animation:\"boolean\",boundary:\"(string|element)\",container:\"(string|element|boolean)\",customClass:\"(string|function)\",delay:\"(number|object)\",fallbackPlacements:\"array\",html:\"boolean\",offset:\"(array|string|function)\",placement:\"(string|function)\",popperConfig:\"(null|object|function)\",sanitize:\"boolean\",sanitizeFn:\"(null|function)\",selector:\"(string|boolean)\",template:\"string\",title:\"(string|element|function)\",trigger:\"string\"};class Fn extends z{constructor(e,n){if(void 0===t)throw new TypeError(\"Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)\");super(e,n),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Hn}static get DefaultType(){return In}static get NAME(){return\"tooltip\"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),I.off(this._element.closest(Yn),An,this._hideModalHandler),this._element.getAttribute(\"data-bs-original-title\")&&this._element.setAttribute(\"title\",this._element.getAttribute(\"data-bs-original-title\")),this._disposePopper(),super.dispose()}show(){if(\"none\"===this._element.style.display)throw new Error(\"Please use show on visible elements\");if(!this._isWithContent()||!this._isEnabled)return;const e=I.trigger(this._element,this.constructor.eventName(\"show\")),t=(f(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(e.defaultPrevented||!t)return;this._disposePopper();const n=this._getTipElement();this._element.setAttribute(\"aria-describedby\",n.getAttribute(\"id\"));const{container:r}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(r.append(n),I.trigger(this._element,this.constructor.eventName(\"inserted\"))),this._popper=this._createPopper(n),n.classList.add(Dn),\"ontouchstart\"in document.documentElement)for(const e of[].concat(...document.body.children))I.on(e,\"mouseover\",p);this._queueCallback(()=>{I.trigger(this._element,this.constructor.eventName(\"shown\")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!I.trigger(this._element,this.constructor.eventName(\"hide\")).defaultPrevented){if(this._getTipElement().classList.remove(Dn),\"ontouchstart\"in document.documentElement)for(const e of[].concat(...document.body.children))I.off(e,\"mouseover\",p);this._activeTrigger[jn]=!1,this._activeTrigger[Cn]=!1,this._activeTrigger[On]=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute(\"aria-describedby\"),I.trigger(this._element,this.constructor.eventName(\"hidden\")))},this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();if(!t)return null;t.classList.remove(Sn,Dn),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=(e=>{do{e+=Math.floor(1e6*Math.random())}while(document.getElementById(e));return e})(this.constructor.NAME).toString();return t.setAttribute(\"id\",n),this._isAnimated()&&t.classList.add(Sn),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new xn({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[En]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute(\"data-bs-original-title\")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Sn)}_isShown(){return this.tip&&this.tip.classList.contains(Dn)}_createPopper(e){const n=y(this._config.placement,[this,e,this._element]),r=Pn[n.toUpperCase()];return t.createPopper(this._element,e,this._getPopperConfig(r))}_getOffset(){const{offset:e}=this._config;return\"string\"==typeof e?e.split(\",\").map(e=>Number.parseInt(e,10)):\"function\"==typeof e?t=>e(t,this._element):e}_resolvePossibleFunction(e){return y(e,[this._element,this._element])}_getPopperConfig(e){const t={placement:e,modifiers:[{name:\"flip\",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:\"offset\",options:{offset:this._getOffset()}},{name:\"preventOverflow\",options:{boundary:this._config.boundary}},{name:\"arrow\",options:{element:`.${this.constructor.NAME}-arrow`}},{name:\"preSetPlacement\",enabled:!0,phase:\"beforeMain\",fn:e=>{this._getTipElement().setAttribute(\"data-popper-placement\",e.state.placement)}}]};return{...t,...y(this._config.popperConfig,[void 0,t])}}_setListeners(){const e=this._config.trigger.split(\" \");for(const t of e)if(\"click\"===t)I.on(this._element,this.constructor.eventName(\"click\"),this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[jn]=!(t._isShown()&&t._activeTrigger[jn]),t.toggle()});else if(\"manual\"!==t){const e=t===On?this.constructor.eventName(\"mouseenter\"):this.constructor.eventName(\"focusin\"),n=t===On?this.constructor.eventName(\"mouseleave\"):this.constructor.eventName(\"focusout\");I.on(this._element,e,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[\"focusin\"===e.type?Cn:On]=!0,t._enter()}),I.on(this._element,n,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[\"focusout\"===e.type?Cn:On]=t._element.contains(e.relatedTarget),t._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},I.on(this._element.closest(Yn),An,this._hideModalHandler)}_fixTitle(){const e=this._element.getAttribute(\"title\");e&&(this._element.getAttribute(\"aria-label\")||this._element.textContent.trim()||this._element.setAttribute(\"aria-label\",e),this._element.setAttribute(\"data-bs-original-title\",e),this._element.removeAttribute(\"title\"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=V.getDataAttributes(this._element);for(const e of Object.keys(t))Tn.has(e)&&delete t[e];return e={...t,...\"object\"==typeof e&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=!1===e.container?document.body:u(e.container),\"number\"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),\"number\"==typeof e.title&&(e.title=e.title.toString()),\"number\"==typeof e.content&&(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const[t,n]of Object.entries(this._config))this.constructor.Default[t]!==n&&(e[t]=n);return e.selector=!1,e.trigger=\"manual\",e}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(e){return this.each(function(){const t=Fn.getOrCreateInstance(this,e);if(\"string\"==typeof e){if(void 0===t[e])throw new TypeError(`No method named \"${e}\"`);t[e]()}})}}_(Fn);const Nn=\".popover-header\",Rn=\".popover-body\",Vn={...Fn.Default,content:\"\",offset:[0,8],placement:\"right\",template:'<div class=\"popover\" role=\"tooltip\"><div class=\"popover-arrow\"></div><h3 class=\"popover-header\"></h3><div class=\"popover-body\"></div></div>',trigger:\"click\"},$n={...Fn.DefaultType,content:\"(null|string|element|function)\"};class zn extends Fn{static get Default(){return Vn}static get DefaultType(){return $n}static get NAME(){return\"popover\"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[Nn]:this._getTitle(),[Rn]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(e){return this.each(function(){const t=zn.getOrCreateInstance(this,e);if(\"string\"==typeof e){if(void 0===t[e])throw new TypeError(`No method named \"${e}\"`);t[e]()}})}}_(zn);const Wn=\".bs.scrollspy\",Un=`activate${Wn}`,Bn=`click${Wn}`,qn=`load${Wn}.data-api`,Gn=\"active\",Jn=\"[href]\",Zn=\".nav-link\",Kn=`${Zn}, .nav-item > ${Zn}, .list-group-item`,Xn={offset:null,rootMargin:\"0px 0px -25%\",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Qn={offset:\"(number|null)\",rootMargin:\"string\",smoothScroll:\"boolean\",target:\"element\",threshold:\"array\"};class er extends z{constructor(e,t){super(e,t),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement=\"visible\"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Xn}static get DefaultType(){return Qn}static get NAME(){return\"scrollspy\"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const e of this._observableSections.values())this._observer.observe(e)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(e){return e.target=u(e.target)||document.body,e.rootMargin=e.offset?`${e.offset}px 0px -30%`:e.rootMargin,\"string\"==typeof e.threshold&&(e.threshold=e.threshold.split(\",\").map(e=>Number.parseFloat(e))),e}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(I.off(this._config.target,Bn),I.on(this._config.target,Bn,Jn,e=>{const t=this._observableSections.get(e.target.hash);if(t){e.preventDefault();const n=this._rootElement||window,r=t.offsetTop-this._element.offsetTop;if(n.scrollTo)return void n.scrollTo({top:r,behavior:\"smooth\"});n.scrollTop=r}}))}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(e=>this._observerCallback(e),e)}_observerCallback(e){const t=e=>this._targetLinks.get(`#${e.target.id}`),n=e=>{this._previousScrollData.visibleEntryTop=e.target.offsetTop,this._process(t(e))},r=(this._rootElement||document.documentElement).scrollTop,i=r>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=r;for(const a of e){if(!a.isIntersecting){this._activeTarget=null,this._clearActiveClass(t(a));continue}const e=a.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(i&&e){if(n(a),!r)return}else i||e||n(a)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const e=U.find(Jn,this._config.target);for(const t of e){if(!t.hash||c(t))continue;const e=U.findOne(decodeURI(t.hash),this._element);d(e)&&(this._targetLinks.set(decodeURI(t.hash),t),this._observableSections.set(t.hash,e))}}_process(e){this._activeTarget!==e&&(this._clearActiveClass(this._config.target),this._activeTarget=e,e.classList.add(Gn),this._activateParents(e),I.trigger(this._element,Un,{relatedTarget:e}))}_activateParents(e){if(e.classList.contains(\"dropdown-item\"))U.findOne(\".dropdown-toggle\",e.closest(\".dropdown\")).classList.add(Gn);else for(const t of U.parents(e,\".nav, .list-group\"))for(const e of U.prev(t,Kn))e.classList.add(Gn)}_clearActiveClass(e){e.classList.remove(Gn);const t=U.find(`${Jn}.${Gn}`,e);for(const e of t)e.classList.remove(Gn)}static jQueryInterface(e){return this.each(function(){const t=er.getOrCreateInstance(this,e);if(\"string\"==typeof e){if(void 0===t[e]||e.startsWith(\"_\")||\"constructor\"===e)throw new TypeError(`No method named \"${e}\"`);t[e]()}})}}I.on(window,qn,()=>{for(const e of U.find('[data-bs-spy=\"scroll\"]'))er.getOrCreateInstance(e)}),_(er);const tr=\".bs.tab\",nr=`hide${tr}`,rr=`hidden${tr}`,ir=`show${tr}`,ar=`shown${tr}`,or=`click${tr}`,sr=`keydown${tr}`,lr=`load${tr}`,ur=\"ArrowLeft\",dr=\"ArrowRight\",cr=\"ArrowUp\",fr=\"ArrowDown\",pr=\"Home\",hr=\"End\",mr=\"active\",vr=\"fade\",gr=\"show\",_r=\".dropdown-toggle\",yr=`:not(${_r})`,br='[data-bs-toggle=\"tab\"], [data-bs-toggle=\"pill\"], [data-bs-toggle=\"list\"]',Mr=`.nav-link${yr}, .list-group-item${yr}, [role=\"tab\"]${yr}, ${br}`,wr=`.${mr}[data-bs-toggle=\"tab\"], .${mr}[data-bs-toggle=\"pill\"], .${mr}[data-bs-toggle=\"list\"]`;class kr extends z{constructor(e){super(e),this._parent=this._element.closest('.list-group, .nav, [role=\"tablist\"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),I.on(this._element,sr,e=>this._keydown(e)))}static get NAME(){return\"tab\"}show(){const e=this._element;if(this._elemIsActive(e))return;const t=this._getActiveElem(),n=t?I.trigger(t,nr,{relatedTarget:e}):null;I.trigger(e,ir,{relatedTarget:t}).defaultPrevented||n&&n.defaultPrevented||(this._deactivate(t,e),this._activate(e,t))}_activate(e,t){e&&(e.classList.add(mr),this._activate(U.getElementFromSelector(e)),this._queueCallback(()=>{\"tab\"===e.getAttribute(\"role\")?(e.removeAttribute(\"tabindex\"),e.setAttribute(\"aria-selected\",!0),this._toggleDropDown(e,!0),I.trigger(e,ar,{relatedTarget:t})):e.classList.add(gr)},e,e.classList.contains(vr)))}_deactivate(e,t){e&&(e.classList.remove(mr),e.blur(),this._deactivate(U.getElementFromSelector(e)),this._queueCallback(()=>{\"tab\"===e.getAttribute(\"role\")?(e.setAttribute(\"aria-selected\",!1),e.setAttribute(\"tabindex\",\"-1\"),this._toggleDropDown(e,!1),I.trigger(e,rr,{relatedTarget:t})):e.classList.remove(gr)},e,e.classList.contains(vr)))}_keydown(e){if(![ur,dr,cr,fr,pr,hr].includes(e.key))return;e.stopPropagation(),e.preventDefault();const t=this._getChildren().filter(e=>!c(e));let n;if([pr,hr].includes(e.key))n=t[e.key===pr?0:t.length-1];else{const r=[dr,fr].includes(e.key);n=M(t,e.target,r,!0)}n&&(n.focus({preventScroll:!0}),kr.getOrCreateInstance(n).show())}_getChildren(){return U.find(Mr,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,\"role\",\"tablist\");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute(\"aria-selected\",t),n!==e&&this._setAttributeIfNotExists(n,\"role\",\"presentation\"),t||e.setAttribute(\"tabindex\",\"-1\"),this._setAttributeIfNotExists(e,\"role\",\"tab\"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=U.getElementFromSelector(e);t&&(this._setAttributeIfNotExists(t,\"role\",\"tabpanel\"),e.id&&this._setAttributeIfNotExists(t,\"aria-labelledby\",`${e.id}`))}_toggleDropDown(e,t){const n=this._getOuterElement(e);if(!n.classList.contains(\"dropdown\"))return;const r=(e,r)=>{const i=U.findOne(e,n);i&&i.classList.toggle(r,t)};r(_r,mr),r(\".dropdown-menu\",gr),n.setAttribute(\"aria-expanded\",t)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains(mr)}_getInnerElement(e){return e.matches(Mr)?e:U.findOne(Mr,e)}_getOuterElement(e){return e.closest(\".nav-item, .list-group-item\")||e}static jQueryInterface(e){return this.each(function(){const t=kr.getOrCreateInstance(this);if(\"string\"==typeof e){if(void 0===t[e]||e.startsWith(\"_\")||\"constructor\"===e)throw new TypeError(`No method named \"${e}\"`);t[e]()}})}}I.on(document,or,br,function(e){[\"A\",\"AREA\"].includes(this.tagName)&&e.preventDefault(),c(this)||kr.getOrCreateInstance(this).show()}),I.on(window,lr,()=>{for(const e of U.find(wr))kr.getOrCreateInstance(e)}),_(kr);const Lr=\".bs.toast\",xr=`mouseover${Lr}`,Tr=`mouseout${Lr}`,Sr=`focusin${Lr}`,Dr=`focusout${Lr}`,Er=`hide${Lr}`,Yr=`hidden${Lr}`,Ar=`show${Lr}`,Or=`shown${Lr}`,Cr=\"hide\",jr=\"show\",Pr=\"showing\",Hr={animation:\"boolean\",autohide:\"boolean\",delay:\"number\"},Ir={animation:!0,autohide:!0,delay:5e3};class Fr extends z{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Ir}static get DefaultType(){return Hr}static get NAME(){return\"toast\"}show(){I.trigger(this._element,Ar).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add(\"fade\"),this._element.classList.remove(Cr),h(this._element),this._element.classList.add(jr,Pr),this._queueCallback(()=>{this._element.classList.remove(Pr),I.trigger(this._element,Or),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(I.trigger(this._element,Er).defaultPrevented||(this._element.classList.add(Pr),this._queueCallback(()=>{this._element.classList.add(Cr),this._element.classList.remove(Pr,jr),I.trigger(this._element,Yr)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(jr),super.dispose()}isShown(){return this._element.classList.contains(jr)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(e,t){switch(e.type){case\"mouseover\":case\"mouseout\":this._hasMouseInteraction=t;break;case\"focusin\":case\"focusout\":this._hasKeyboardInteraction=t}if(t)return void this._clearTimeout();const n=e.relatedTarget;this._element===n||this._element.contains(n)||this._maybeScheduleHide()}_setListeners(){I.on(this._element,xr,e=>this._onInteraction(e,!0)),I.on(this._element,Tr,e=>this._onInteraction(e,!1)),I.on(this._element,Sr,e=>this._onInteraction(e,!0)),I.on(this._element,Dr,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(e){return this.each(function(){const t=Fr.getOrCreateInstance(this,e);if(\"string\"==typeof e){if(void 0===t[e])throw new TypeError(`No method named \"${e}\"`);t[e](this)}})}}return B(Fr),_(Fr),{Alert:Z,Button:X,Carousel:Oe,Collapse:Be,Dropdown:gt,Modal:Xt,Offcanvas:gn,Popover:zn,ScrollSpy:er,Tab:kr,Toast:Fr,Tooltip:Fn}}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){\"use strict\";var e;function t(){return e.apply(null,arguments)}function n(e){return e instanceof Array||\"[object Array]\"===Object.prototype.toString.call(e)}function r(e){return null!=e&&\"[object Object]\"===Object.prototype.toString.call(e)}function i(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function a(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;for(var t in e)if(i(e,t))return;return 1}function o(e){return void 0===e}function s(e){return\"number\"==typeof e||\"[object Number]\"===Object.prototype.toString.call(e)}function l(e){return e instanceof Date||\"[object Date]\"===Object.prototype.toString.call(e)}function u(e,t){for(var n=[],r=e.length,i=0;i<r;++i)n.push(t(e[i],i));return n}function d(e,t){for(var n in t)i(t,n)&&(e[n]=t[n]);return i(t,\"toString\")&&(e.toString=t.toString),i(t,\"valueOf\")&&(e.valueOf=t.valueOf),e}function c(e,t,n,r){return Tt(e,t,n,r,!0).utc()}function f(e){return null==e._pf&&(e._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidEra:null,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],era:null,meridiem:null,rfc2822:!1,weekdayMismatch:!1}),e._pf}function p(e){var t,n,r=e._d&&!isNaN(e._d.getTime());return r&&(t=f(e),n=m.call(t.parsedDateParts,function(e){return null!=e}),r=t.overflow<0&&!t.empty&&!t.invalidEra&&!t.invalidMonth&&!t.invalidWeekday&&!t.weekdayMismatch&&!t.nullInput&&!t.invalidFormat&&!t.userInvalidated&&(!t.meridiem||t.meridiem&&n),e._strict)&&(r=r&&0===t.charsLeftOver&&0===t.unusedTokens.length&&void 0===t.bigHour),null!=Object.isFrozen&&Object.isFrozen(e)?r:(e._isValid=r,e._isValid)}function h(e){var t=c(NaN);return null!=e?d(f(t),e):f(t).userInvalidated=!0,t}var m=Array.prototype.some||function(e){for(var t=Object(this),n=t.length>>>0,r=0;r<n;r++)if(r in t&&e.call(this,t[r],r,t))return!0;return!1},v=t.momentProperties=[],g=!1;function _(e,t){var n,r,i,a=v.length;if(o(t._isAMomentObject)||(e._isAMomentObject=t._isAMomentObject),o(t._i)||(e._i=t._i),o(t._f)||(e._f=t._f),o(t._l)||(e._l=t._l),o(t._strict)||(e._strict=t._strict),o(t._tzm)||(e._tzm=t._tzm),o(t._isUTC)||(e._isUTC=t._isUTC),o(t._offset)||(e._offset=t._offset),o(t._pf)||(e._pf=f(t)),o(t._locale)||(e._locale=t._locale),0<a)for(n=0;n<a;n++)o(i=t[r=v[n]])||(e[r]=i);return e}function y(e){_(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===g&&(g=!0,t.updateOffset(this),g=!1)}function b(e){return e instanceof y||null!=e&&null!=e._isAMomentObject}function M(e){!1===t.suppressDeprecationWarnings&&\"undefined\"!=typeof console&&console.warn&&console.warn(\"Deprecation warning: \"+e)}function w(e,n){var r=!0;return d(function(){if(null!=t.deprecationHandler&&t.deprecationHandler(null,e),r){for(var a,o,s=[],l=arguments.length,u=0;u<l;u++){if(a=\"\",\"object\"==typeof arguments[u]){for(o in a+=\"\\n[\"+u+\"] \",arguments[0])i(arguments[0],o)&&(a+=o+\": \"+arguments[0][o]+\", \");a=a.slice(0,-2)}else a=arguments[u];s.push(a)}M(e+\"\\nArguments: \"+Array.prototype.slice.call(s).join(\"\")+\"\\n\"+(new Error).stack),r=!1}return n.apply(this,arguments)},n)}var k={};function L(e,n){null!=t.deprecationHandler&&t.deprecationHandler(e,n),k[e]||(M(n),k[e]=!0)}function x(e){return\"undefined\"!=typeof Function&&e instanceof Function||\"[object Function]\"===Object.prototype.toString.call(e)}function T(e,t){var n,a=d({},e);for(n in t)i(t,n)&&(r(e[n])&&r(t[n])?(a[n]={},d(a[n],e[n]),d(a[n],t[n])):null!=t[n]?a[n]=t[n]:delete a[n]);for(n in e)i(e,n)&&!i(t,n)&&r(e[n])&&(a[n]=d({},a[n]));return a}function S(e){null!=e&&this.set(e)}t.suppressDeprecationWarnings=!1,t.deprecationHandler=null;var D=Object.keys||function(e){var t,n=[];for(t in e)i(e,t)&&n.push(t);return n};function E(e,t,n){var r=\"\"+Math.abs(e);return(0<=e?n?\"+\":\"\":\"-\")+Math.pow(10,Math.max(0,t-r.length)).toString().substr(1)+r}var Y=/(\\[[^\\[]*\\])|(\\\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,A=/(\\[[^\\[]*\\])|(\\\\)?(LTS|LT|LL?L?L?|l{1,4})/g,O={},C={};function j(e,t,n,r){var i=\"string\"==typeof r?function(){return this[r]()}:r;e&&(C[e]=i),t&&(C[t[0]]=function(){return E(i.apply(this,arguments),t[1],t[2])}),n&&(C[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),e)})}function P(e,t){return e.isValid()?(t=H(t,e.localeData()),O[t]=O[t]||function(e){for(var t,n=e.match(Y),r=0,i=n.length;r<i;r++)C[n[r]]?n[r]=C[n[r]]:n[r]=(t=n[r]).match(/\\[[\\s\\S]/)?t.replace(/^\\[|\\]$/g,\"\"):t.replace(/\\\\/g,\"\");return function(t){for(var r=\"\",a=0;a<i;a++)r+=x(n[a])?n[a].call(t,e):n[a];return r}}(t),O[t](e)):e.localeData().invalidDate()}function H(e,t){var n=5;function r(e){return t.longDateFormat(e)||e}for(A.lastIndex=0;0<=n&&A.test(e);)e=e.replace(A,r),A.lastIndex=0,--n;return e}var I={D:\"date\",dates:\"date\",date:\"date\",d:\"day\",days:\"day\",day:\"day\",e:\"weekday\",weekdays:\"weekday\",weekday:\"weekday\",E:\"isoWeekday\",isoweekdays:\"isoWeekday\",isoweekday:\"isoWeekday\",DDD:\"dayOfYear\",dayofyears:\"dayOfYear\",dayofyear:\"dayOfYear\",h:\"hour\",hours:\"hour\",hour:\"hour\",ms:\"millisecond\",milliseconds:\"millisecond\",millisecond:\"millisecond\",m:\"minute\",minutes:\"minute\",minute:\"minute\",M:\"month\",months:\"month\",month:\"month\",Q:\"quarter\",quarters:\"quarter\",quarter:\"quarter\",s:\"second\",seconds:\"second\",second:\"second\",gg:\"weekYear\",weekyears:\"weekYear\",weekyear:\"weekYear\",GG:\"isoWeekYear\",isoweekyears:\"isoWeekYear\",isoweekyear:\"isoWeekYear\",w:\"week\",weeks:\"week\",week:\"week\",W:\"isoWeek\",isoweeks:\"isoWeek\",isoweek:\"isoWeek\",y:\"year\",years:\"year\",year:\"year\"};function F(e){return\"string\"==typeof e?I[e]||I[e.toLowerCase()]:void 0}function N(e){var t,n,r={};for(n in e)i(e,n)&&(t=F(n))&&(r[t]=e[n]);return r}var R={date:9,day:11,weekday:11,isoWeekday:11,dayOfYear:4,hour:13,millisecond:16,minute:14,month:8,quarter:7,second:15,weekYear:1,isoWeekYear:1,week:5,isoWeek:5,year:1},V=/\\d/,$=/\\d\\d/,z=/\\d{3}/,W=/\\d{4}/,U=/[+-]?\\d{6}/,B=/\\d\\d?/,q=/\\d\\d\\d\\d?/,G=/\\d\\d\\d\\d\\d\\d?/,J=/\\d{1,3}/,Z=/\\d{1,4}/,K=/[+-]?\\d{1,6}/,X=/\\d+/,Q=/[+-]?\\d+/,ee=/Z|[+-]\\d\\d:?\\d\\d/gi,te=/Z|[+-]\\d\\d(?::?\\d\\d)?/gi,ne=/[0-9]{0,256}['a-z\\u00A0-\\u05FF\\u0700-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFF07\\uFF10-\\uFFEF]{1,256}|[\\u0600-\\u06FF\\/]{1,256}(\\s*?[\\u0600-\\u06FF]{1,256}){1,2}/i,re=/^[1-9]\\d?/,ie=/^([1-9]\\d|\\d)/;function ae(e,t,n){de[e]=x(t)?t:function(e,r){return e&&n?n:t}}function oe(e,t){return i(de,e)?de[e](t._strict,t._locale):new RegExp(se(e.replace(\"\\\\\",\"\").replace(/\\\\(\\[)|\\\\(\\])|\\[([^\\]\\[]*)\\]|\\\\(.)/g,function(e,t,n,r,i){return t||n||r||i})))}function se(e){return e.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g,\"\\\\$&\")}function le(e){return e<0?Math.ceil(e)||0:Math.floor(e)}function ue(e){var t=0;return 0!=(e=+e)&&isFinite(e)?le(e):t}var de={},ce={};function fe(e,t){var n,r,i=t;for(\"string\"==typeof e&&(e=[e]),s(t)&&(i=function(e,n){n[t]=ue(e)}),r=e.length,n=0;n<r;n++)ce[e[n]]=i}function pe(e,t){fe(e,function(e,n,r,i){r._w=r._w||{},t(e,r._w,r,i)})}function he(e){return e%4==0&&e%100!=0||e%400==0}var me=0,ve=1,ge=2,_e=3,ye=4,be=5,Me=6,we=7,ke=8;function Le(e){return he(e)?366:365}j(\"Y\",0,0,function(){var e=this.year();return e<=9999?E(e,4):\"+\"+e}),j(0,[\"YY\",2],0,function(){return this.year()%100}),j(0,[\"YYYY\",4],0,\"year\"),j(0,[\"YYYYY\",5],0,\"year\"),j(0,[\"YYYYYY\",6,!0],0,\"year\"),ae(\"Y\",Q),ae(\"YY\",B,$),ae(\"YYYY\",Z,W),ae(\"YYYYY\",K,U),ae(\"YYYYYY\",K,U),fe([\"YYYYY\",\"YYYYYY\"],me),fe(\"YYYY\",function(e,n){n[me]=2===e.length?t.parseTwoDigitYear(e):ue(e)}),fe(\"YY\",function(e,n){n[me]=t.parseTwoDigitYear(e)}),fe(\"Y\",function(e,t){t[me]=parseInt(e,10)}),t.parseTwoDigitYear=function(e){return ue(e)+(68<ue(e)?1900:2e3)};var xe,Te=Se(\"FullYear\",!0);function Se(e,n){return function(r){return null!=r?(Ee(this,e,r),t.updateOffset(this,n),this):De(this,e)}}function De(e,t){if(!e.isValid())return NaN;var n=e._d,r=e._isUTC;switch(t){case\"Milliseconds\":return r?n.getUTCMilliseconds():n.getMilliseconds();case\"Seconds\":return r?n.getUTCSeconds():n.getSeconds();case\"Minutes\":return r?n.getUTCMinutes():n.getMinutes();case\"Hours\":return r?n.getUTCHours():n.getHours();case\"Date\":return r?n.getUTCDate():n.getDate();case\"Day\":return r?n.getUTCDay():n.getDay();case\"Month\":return r?n.getUTCMonth():n.getMonth();case\"FullYear\":return r?n.getUTCFullYear():n.getFullYear();default:return NaN}}function Ee(e,t,n){var r,i,a;if(e.isValid()&&!isNaN(n)){switch(r=e._d,i=e._isUTC,t){case\"Milliseconds\":return i?r.setUTCMilliseconds(n):r.setMilliseconds(n);case\"Seconds\":return i?r.setUTCSeconds(n):r.setSeconds(n);case\"Minutes\":return i?r.setUTCMinutes(n):r.setMinutes(n);case\"Hours\":return i?r.setUTCHours(n):r.setHours(n);case\"Date\":return i?r.setUTCDate(n):r.setDate(n);case\"FullYear\":break;default:return}t=n,a=e.month(),e=29!==(e=e.date())||1!==a||he(t)?e:28,i?r.setUTCFullYear(t,a,e):r.setFullYear(t,a,e)}}function Ye(e,t){var n;return isNaN(e)||isNaN(t)?NaN:(e+=(t-(n=(t%(n=12)+n)%n))/12,1==n?he(e)?29:28:31-n%7%2)}xe=Array.prototype.indexOf||function(e){for(var t=0;t<this.length;++t)if(this[t]===e)return t;return-1},j(\"M\",[\"MM\",2],\"Mo\",function(){return this.month()+1}),j(\"MMM\",0,0,function(e){return this.localeData().monthsShort(this,e)}),j(\"MMMM\",0,0,function(e){return this.localeData().months(this,e)}),ae(\"M\",B,re),ae(\"MM\",B,$),ae(\"MMM\",function(e,t){return t.monthsShortRegex(e)}),ae(\"MMMM\",function(e,t){return t.monthsRegex(e)}),fe([\"M\",\"MM\"],function(e,t){t[ve]=ue(e)-1}),fe([\"MMM\",\"MMMM\"],function(e,t,n,r){null!=(r=n._locale.monthsParse(e,r,n._strict))?t[ve]=r:f(n).invalidMonth=e});var Ae=\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),Oe=\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),Ce=/D[oD]?(\\[[^\\[\\]]*\\]|\\s)+MMMM?/,je=ne,Pe=ne;function He(e,t){if(e.isValid()){if(\"string\"==typeof t)if(/^\\d+$/.test(t))t=ue(t);else if(!s(t=e.localeData().monthsParse(t)))return;var n=(n=e.date())<29?n:Math.min(n,Ye(e.year(),t));e._isUTC?e._d.setUTCMonth(t,n):e._d.setMonth(t,n)}}function Ie(e){return null!=e?(He(this,e),t.updateOffset(this,!0),this):De(this,\"Month\")}function Fe(){function e(e,t){return t.length-e.length}for(var t,n,r=[],i=[],a=[],o=0;o<12;o++)n=c([2e3,o]),t=se(this.monthsShort(n,\"\")),n=se(this.months(n,\"\")),r.push(t),i.push(n),a.push(n),a.push(t);r.sort(e),i.sort(e),a.sort(e),this._monthsRegex=new RegExp(\"^(\"+a.join(\"|\")+\")\",\"i\"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp(\"^(\"+i.join(\"|\")+\")\",\"i\"),this._monthsShortStrictRegex=new RegExp(\"^(\"+r.join(\"|\")+\")\",\"i\")}function Ne(e,t,n,r,i,a,o){var s;return e<100&&0<=e?(s=new Date(e+400,t,n,r,i,a,o),isFinite(s.getFullYear())&&s.setFullYear(e)):s=new Date(e,t,n,r,i,a,o),s}function Re(e){var t;return e<100&&0<=e?((t=Array.prototype.slice.call(arguments))[0]=e+400,t=new Date(Date.UTC.apply(null,t)),isFinite(t.getUTCFullYear())&&t.setUTCFullYear(e)):t=new Date(Date.UTC.apply(null,arguments)),t}function Ve(e,t,n){return(n=7+t-n)-(7+Re(e,0,n).getUTCDay()-t)%7-1}function $e(e,t,n,r,i){var a;n=(t=1+7*(t-1)+(7+n-r)%7+Ve(e,r,i))<=0?Le(a=e-1)+t:t>Le(e)?(a=e+1,t-Le(e)):(a=e,t);return{year:a,dayOfYear:n}}function ze(e,t,n){var r,i,a=Ve(e.year(),t,n);return(a=Math.floor((e.dayOfYear()-a-1)/7)+1)<1?r=a+We(i=e.year()-1,t,n):a>We(e.year(),t,n)?(r=a-We(e.year(),t,n),i=e.year()+1):(i=e.year(),r=a),{week:r,year:i}}function We(e,t,n){var r=Ve(e,t,n);t=Ve(e+1,t,n);return(Le(e)-r+t)/7}function Ue(e,t){return e.slice(t,7).concat(e.slice(0,t))}j(\"w\",[\"ww\",2],\"wo\",\"week\"),j(\"W\",[\"WW\",2],\"Wo\",\"isoWeek\"),ae(\"w\",B,re),ae(\"ww\",B,$),ae(\"W\",B,re),ae(\"WW\",B,$),pe([\"w\",\"ww\",\"W\",\"WW\"],function(e,t,n,r){t[r.substr(0,1)]=ue(e)}),j(\"d\",0,\"do\",\"day\"),j(\"dd\",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),j(\"ddd\",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),j(\"dddd\",0,0,function(e){return this.localeData().weekdays(this,e)}),j(\"e\",0,0,\"weekday\"),j(\"E\",0,0,\"isoWeekday\"),ae(\"d\",B),ae(\"e\",B),ae(\"E\",B),ae(\"dd\",function(e,t){return t.weekdaysMinRegex(e)}),ae(\"ddd\",function(e,t){return t.weekdaysShortRegex(e)}),ae(\"dddd\",function(e,t){return t.weekdaysRegex(e)}),pe([\"dd\",\"ddd\",\"dddd\"],function(e,t,n,r){null!=(r=n._locale.weekdaysParse(e,r,n._strict))?t.d=r:f(n).invalidWeekday=e}),pe([\"d\",\"e\",\"E\"],function(e,t,n,r){t[r]=ue(e)});var Be=\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),qe=\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),Ge=\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),Je=ne,Ze=ne,Ke=ne;function Xe(){function e(e,t){return t.length-e.length}for(var t,n,r,i=[],a=[],o=[],s=[],l=0;l<7;l++)r=c([2e3,1]).day(l),t=se(this.weekdaysMin(r,\"\")),n=se(this.weekdaysShort(r,\"\")),r=se(this.weekdays(r,\"\")),i.push(t),a.push(n),o.push(r),s.push(t),s.push(n),s.push(r);i.sort(e),a.sort(e),o.sort(e),s.sort(e),this._weekdaysRegex=new RegExp(\"^(\"+s.join(\"|\")+\")\",\"i\"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp(\"^(\"+o.join(\"|\")+\")\",\"i\"),this._weekdaysShortStrictRegex=new RegExp(\"^(\"+a.join(\"|\")+\")\",\"i\"),this._weekdaysMinStrictRegex=new RegExp(\"^(\"+i.join(\"|\")+\")\",\"i\")}function Qe(){return this.hours()%12||12}function et(e,t){j(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function tt(e,t){return t._meridiemParse}j(\"H\",[\"HH\",2],0,\"hour\"),j(\"h\",[\"hh\",2],0,Qe),j(\"k\",[\"kk\",2],0,function(){return this.hours()||24}),j(\"hmm\",0,0,function(){return\"\"+Qe.apply(this)+E(this.minutes(),2)}),j(\"hmmss\",0,0,function(){return\"\"+Qe.apply(this)+E(this.minutes(),2)+E(this.seconds(),2)}),j(\"Hmm\",0,0,function(){return\"\"+this.hours()+E(this.minutes(),2)}),j(\"Hmmss\",0,0,function(){return\"\"+this.hours()+E(this.minutes(),2)+E(this.seconds(),2)}),et(\"a\",!0),et(\"A\",!1),ae(\"a\",tt),ae(\"A\",tt),ae(\"H\",B,ie),ae(\"h\",B,re),ae(\"k\",B,re),ae(\"HH\",B,$),ae(\"hh\",B,$),ae(\"kk\",B,$),ae(\"hmm\",q),ae(\"hmmss\",G),ae(\"Hmm\",q),ae(\"Hmmss\",G),fe([\"H\",\"HH\"],_e),fe([\"k\",\"kk\"],function(e,t,n){e=ue(e),t[_e]=24===e?0:e}),fe([\"a\",\"A\"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),fe([\"h\",\"hh\"],function(e,t,n){t[_e]=ue(e),f(n).bigHour=!0}),fe(\"hmm\",function(e,t,n){var r=e.length-2;t[_e]=ue(e.substr(0,r)),t[ye]=ue(e.substr(r)),f(n).bigHour=!0}),fe(\"hmmss\",function(e,t,n){var r=e.length-4,i=e.length-2;t[_e]=ue(e.substr(0,r)),t[ye]=ue(e.substr(r,2)),t[be]=ue(e.substr(i)),f(n).bigHour=!0}),fe(\"Hmm\",function(e,t,n){var r=e.length-2;t[_e]=ue(e.substr(0,r)),t[ye]=ue(e.substr(r))}),fe(\"Hmmss\",function(e,t,n){var r=e.length-4,i=e.length-2;t[_e]=ue(e.substr(0,r)),t[ye]=ue(e.substr(r,2)),t[be]=ue(e.substr(i))}),ne=Se(\"Hours\",!0);var nt,rt={calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},longDateFormat:{LTS:\"h:mm:ss A\",LT:\"h:mm A\",L:\"MM/DD/YYYY\",LL:\"MMMM D, YYYY\",LLL:\"MMMM D, YYYY h:mm A\",LLLL:\"dddd, MMMM D, YYYY h:mm A\"},invalidDate:\"Invalid date\",ordinal:\"%d\",dayOfMonthOrdinalParse:/\\d{1,2}/,relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",w:\"a week\",ww:\"%d weeks\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},months:Ae,monthsShort:Oe,week:{dow:0,doy:6},weekdays:Be,weekdaysMin:Ge,weekdaysShort:qe,meridiemParse:/[ap]\\.?m?\\.?/i},it={},at={};function ot(e){return e&&e.toLowerCase().replace(\"_\",\"-\")}function st(e){var t,n;if(void 0===it[e]&&\"undefined\"!=typeof module&&module&&module.exports&&(n=e)&&n.match(\"^[^/\\\\\\\\]*$\"))try{t=nt._abbr,require(\"./locale/\"+e),lt(t)}catch(t){it[e]=null}return it[e]}function lt(e,t){return e&&((t=o(t)?dt(e):ut(e,t))?nt=t:\"undefined\"!=typeof console&&console.warn&&console.warn(\"Locale \"+e+\" not found. Did you forget to load it?\")),nt._abbr}function ut(e,t){if(null===t)return delete it[e],null;var n,r=rt;if(t.abbr=e,null!=it[e])L(\"defineLocaleOverride\",\"use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info.\"),r=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])r=it[t.parentLocale]._config;else{if(null==(n=st(t.parentLocale)))return at[t.parentLocale]||(at[t.parentLocale]=[]),at[t.parentLocale].push({name:e,config:t}),null;r=n._config}return it[e]=new S(T(r,t)),at[e]&&at[e].forEach(function(e){ut(e.name,e.config)}),lt(e),it[e]}function dt(e){var t;if(!(e=e&&e._locale&&e._locale._abbr?e._locale._abbr:e))return nt;if(!n(e)){if(t=st(e))return t;e=[e]}return function(e){for(var t,n,r,i,a=0;a<e.length;){for(t=(i=ot(e[a]).split(\"-\")).length,n=(n=ot(e[a+1]))?n.split(\"-\"):null;0<t;){if(r=st(i.slice(0,t).join(\"-\")))return r;if(n&&n.length>=t&&function(e,t){for(var n=Math.min(e.length,t.length),r=0;r<n;r+=1)if(e[r]!==t[r])return r;return n}(i,n)>=t-1)break;t--}a++}return nt}(e)}function ct(e){var t=e._a;return t&&-2===f(e).overflow&&(t=t[ve]<0||11<t[ve]?ve:t[ge]<1||t[ge]>Ye(t[me],t[ve])?ge:t[_e]<0||24<t[_e]||24===t[_e]&&(0!==t[ye]||0!==t[be]||0!==t[Me])?_e:t[ye]<0||59<t[ye]?ye:t[be]<0||59<t[be]?be:t[Me]<0||999<t[Me]?Me:-1,f(e)._overflowDayOfYear&&(t<me||ge<t)&&(t=ge),f(e)._overflowWeeks&&-1===t&&(t=we),f(e)._overflowWeekday&&-1===t&&(t=ke),f(e).overflow=t),e}var ft=/^\\s*((?:[+-]\\d{6}|\\d{4})-(?:\\d\\d-\\d\\d|W\\d\\d-\\d|W\\d\\d|\\d\\d\\d|\\d\\d))(?:(T| )(\\d\\d(?::\\d\\d(?::\\d\\d(?:[.,]\\d+)?)?)?)([+-]\\d\\d(?::?\\d\\d)?|\\s*Z)?)?$/,pt=/^\\s*((?:[+-]\\d{6}|\\d{4})(?:\\d\\d\\d\\d|W\\d\\d\\d|W\\d\\d|\\d\\d\\d|\\d\\d|))(?:(T| )(\\d\\d(?:\\d\\d(?:\\d\\d(?:[.,]\\d+)?)?)?)([+-]\\d\\d(?::?\\d\\d)?|\\s*Z)?)?$/,ht=/Z|[+-]\\d\\d(?::?\\d\\d)?/,mt=[[\"YYYYYY-MM-DD\",/[+-]\\d{6}-\\d\\d-\\d\\d/],[\"YYYY-MM-DD\",/\\d{4}-\\d\\d-\\d\\d/],[\"GGGG-[W]WW-E\",/\\d{4}-W\\d\\d-\\d/],[\"GGGG-[W]WW\",/\\d{4}-W\\d\\d/,!1],[\"YYYY-DDD\",/\\d{4}-\\d{3}/],[\"YYYY-MM\",/\\d{4}-\\d\\d/,!1],[\"YYYYYYMMDD\",/[+-]\\d{10}/],[\"YYYYMMDD\",/\\d{8}/],[\"GGGG[W]WWE\",/\\d{4}W\\d{3}/],[\"GGGG[W]WW\",/\\d{4}W\\d{2}/,!1],[\"YYYYDDD\",/\\d{7}/],[\"YYYYMM\",/\\d{6}/,!1],[\"YYYY\",/\\d{4}/,!1]],vt=[[\"HH:mm:ss.SSSS\",/\\d\\d:\\d\\d:\\d\\d\\.\\d+/],[\"HH:mm:ss,SSSS\",/\\d\\d:\\d\\d:\\d\\d,\\d+/],[\"HH:mm:ss\",/\\d\\d:\\d\\d:\\d\\d/],[\"HH:mm\",/\\d\\d:\\d\\d/],[\"HHmmss.SSSS\",/\\d\\d\\d\\d\\d\\d\\.\\d+/],[\"HHmmss,SSSS\",/\\d\\d\\d\\d\\d\\d,\\d+/],[\"HHmmss\",/\\d\\d\\d\\d\\d\\d/],[\"HHmm\",/\\d\\d\\d\\d/],[\"HH\",/\\d\\d/]],gt=/^\\/?Date\\((-?\\d+)/i,_t=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\\s)?(\\d{1,2})\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s(\\d{2,4})\\s(\\d\\d):(\\d\\d)(?::(\\d\\d))?\\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\\d{4}))$/,yt={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function bt(e){var t,n,r,i,a,o,s=e._i,l=ft.exec(s)||pt.exec(s),u=(s=mt.length,vt.length);if(l){for(f(e).iso=!0,t=0,n=s;t<n;t++)if(mt[t][1].exec(l[1])){i=mt[t][0],r=!1!==mt[t][2];break}if(null==i)e._isValid=!1;else{if(l[3]){for(t=0,n=u;t<n;t++)if(vt[t][1].exec(l[3])){a=(l[2]||\" \")+vt[t][0];break}if(null==a)return void(e._isValid=!1)}if(r||null==a){if(l[4]){if(!ht.exec(l[4]))return void(e._isValid=!1);o=\"Z\"}e._f=i+(a||\"\")+(o||\"\"),Lt(e)}else e._isValid=!1}}else e._isValid=!1}function Mt(e){var t,n,r=_t.exec(e._i.replace(/\\([^()]*\\)|[\\n\\t]/g,\" \").replace(/(\\s\\s+)/g,\" \").replace(/^\\s\\s*/,\"\").replace(/\\s\\s*$/,\"\"));r?(t=function(e,t,n,r,i,a){return e=[function(e){return(e=parseInt(e,10))<=49?2e3+e:e<=999?1900+e:e}(e),Oe.indexOf(t),parseInt(n,10),parseInt(r,10),parseInt(i,10)],a&&e.push(parseInt(a,10)),e}(r[4],r[3],r[2],r[5],r[6],r[7]),function(e,t,n){if(!e||qe.indexOf(e)===new Date(t[0],t[1],t[2]).getDay())return 1;f(n).weekdayMismatch=!0,n._isValid=!1}(r[1],t,e)&&(e._a=t,e._tzm=(t=r[8],n=r[9],r=r[10],t?yt[t]:n?0:((t=parseInt(r,10))-(n=t%100))/100*60+n),e._d=Re.apply(null,e._a),e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),f(e).rfc2822=!0)):e._isValid=!1}function wt(e,t,n){return null!=e?e:null!=t?t:n}function kt(e){var n,r,i,a,o,s,l,u,d,c,p,h=[];if(!e._d){for(i=e,a=new Date(t.now()),r=i._useUTC?[a.getUTCFullYear(),a.getUTCMonth(),a.getUTCDate()]:[a.getFullYear(),a.getMonth(),a.getDate()],e._w&&null==e._a[ge]&&null==e._a[ve]&&(null!=(a=(i=e)._w).GG||null!=a.W||null!=a.E?(u=1,d=4,o=wt(a.GG,i._a[me],ze(St(),1,4).year),s=wt(a.W,1),((l=wt(a.E,1))<1||7<l)&&(c=!0)):(u=i._locale._week.dow,d=i._locale._week.doy,p=ze(St(),u,d),o=wt(a.gg,i._a[me],p.year),s=wt(a.w,p.week),null!=a.d?((l=a.d)<0||6<l)&&(c=!0):null!=a.e?(l=a.e+u,(a.e<0||6<a.e)&&(c=!0)):l=u),s<1||s>We(o,u,d)?f(i)._overflowWeeks=!0:null!=c?f(i)._overflowWeekday=!0:(p=$e(o,s,l,u,d),i._a[me]=p.year,i._dayOfYear=p.dayOfYear)),null!=e._dayOfYear&&(a=wt(e._a[me],r[me]),(e._dayOfYear>Le(a)||0===e._dayOfYear)&&(f(e)._overflowDayOfYear=!0),c=Re(a,0,e._dayOfYear),e._a[ve]=c.getUTCMonth(),e._a[ge]=c.getUTCDate()),n=0;n<3&&null==e._a[n];++n)e._a[n]=h[n]=r[n];for(;n<7;n++)e._a[n]=h[n]=null==e._a[n]?2===n?1:0:e._a[n];24===e._a[_e]&&0===e._a[ye]&&0===e._a[be]&&0===e._a[Me]&&(e._nextDay=!0,e._a[_e]=0),e._d=(e._useUTC?Re:Ne).apply(null,h),o=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[_e]=24),e._w&&void 0!==e._w.d&&e._w.d!==o&&(f(e).weekdayMismatch=!0)}}function Lt(e){if(e._f===t.ISO_8601)bt(e);else if(e._f===t.RFC_2822)Mt(e);else{e._a=[],f(e).empty=!0;for(var n,r,a,o,s,l=\"\"+e._i,u=l.length,d=0,c=H(e._f,e._locale).match(Y)||[],p=c.length,h=0;h<p;h++)r=c[h],(n=(l.match(oe(r,e))||[])[0])&&(0<(a=l.substr(0,l.indexOf(n))).length&&f(e).unusedInput.push(a),l=l.slice(l.indexOf(n)+n.length),d+=n.length),C[r]?(n?f(e).empty=!1:f(e).unusedTokens.push(r),a=r,s=e,null!=(o=n)&&i(ce,a)&&ce[a](o,s._a,s,a)):e._strict&&!n&&f(e).unusedTokens.push(r);f(e).charsLeftOver=u-d,0<l.length&&f(e).unusedInput.push(l),e._a[_e]<=12&&!0===f(e).bigHour&&0<e._a[_e]&&(f(e).bigHour=void 0),f(e).parsedDateParts=e._a.slice(0),f(e).meridiem=e._meridiem,e._a[_e]=function(e,t,n){return null==n?t:null!=e.meridiemHour?e.meridiemHour(t,n):null!=e.isPM?((e=e.isPM(n))&&t<12&&(t+=12),t=e||12!==t?t:0):t}(e._locale,e._a[_e],e._meridiem),null!==(u=f(e).era)&&(e._a[me]=e._locale.erasConvertYear(u,e._a[me])),kt(e),ct(e)}}function xt(e){var i,a,c,m=e._i,v=e._f;if(e._locale=e._locale||dt(e._l),null===m||void 0===v&&\"\"===m)return h({nullInput:!0});if(\"string\"==typeof m&&(e._i=m=e._locale.preparse(m)),b(m))return new y(ct(m));if(l(m))e._d=m;else if(n(v)){var g,M,w,k,L,x,T=e,S=!1,D=T._f.length;if(0===D)f(T).invalidFormat=!0,T._d=new Date(NaN);else{for(k=0;k<D;k++)L=0,x=!1,g=_({},T),null!=T._useUTC&&(g._useUTC=T._useUTC),g._f=T._f[k],Lt(g),p(g)&&(x=!0),L=(L+=f(g).charsLeftOver)+10*f(g).unusedTokens.length,f(g).score=L,S?L<w&&(w=L,M=g):(null==w||L<w||x)&&(w=L,M=g,x)&&(S=!0);d(T,M||g)}}else v?Lt(e):o(v=(m=e)._i)?m._d=new Date(t.now()):l(v)?m._d=new Date(v.valueOf()):\"string\"==typeof v?(a=m,null!==(i=gt.exec(a._i))?a._d=new Date(+i[1]):(bt(a),!1===a._isValid&&(delete a._isValid,Mt(a),!1===a._isValid)&&(delete a._isValid,a._strict?a._isValid=!1:t.createFromInputFallback(a)))):n(v)?(m._a=u(v.slice(0),function(e){return parseInt(e,10)}),kt(m)):r(v)?(i=m)._d||(c=void 0===(a=N(i._i)).day?a.date:a.day,i._a=u([a.year,a.month,c,a.hour,a.minute,a.second,a.millisecond],function(e){return e&&parseInt(e,10)}),kt(i)):s(v)?m._d=new Date(v):t.createFromInputFallback(m);return p(e)||(e._d=null),e}function Tt(e,t,i,o,s){var l={};return!0!==t&&!1!==t||(o=t,t=void 0),!0!==i&&!1!==i||(o=i,i=void 0),(r(e)&&a(e)||n(e)&&0===e.length)&&(e=void 0),l._isAMomentObject=!0,l._useUTC=l._isUTC=s,l._l=i,l._i=e,l._f=t,l._strict=o,(s=new y(ct(xt(s=l))))._nextDay&&(s.add(1,\"d\"),s._nextDay=void 0),s}function St(e,t,n,r){return Tt(e,t,n,r,!1)}function Dt(e,t){var r,i;if(!(t=1===t.length&&n(t[0])?t[0]:t).length)return St();for(r=t[0],i=1;i<t.length;++i)t[i].isValid()&&!t[i][e](r)||(r=t[i]);return r}t.createFromInputFallback=w(\"value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.\",function(e){e._d=new Date(e._i+(e._useUTC?\" UTC\":\"\"))}),t.ISO_8601=function(){},t.RFC_2822=function(){},q=w(\"moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/\",function(){var e=St.apply(null,arguments);return this.isValid()&&e.isValid()?e<this?this:e:h()}),G=w(\"moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/\",function(){var e=St.apply(null,arguments);return this.isValid()&&e.isValid()?this<e?this:e:h()});var Et=[\"year\",\"quarter\",\"month\",\"week\",\"day\",\"hour\",\"minute\",\"second\",\"millisecond\"];function Yt(e){var t=(e=N(e)).year||0,n=e.quarter||0,r=e.month||0,a=e.week||e.isoWeek||0,o=e.day||0,s=e.hour||0,l=e.minute||0,u=e.second||0,d=e.millisecond||0;this._isValid=function(e){var t,n,r=!1,a=Et.length;for(t in e)if(i(e,t)&&(-1===xe.call(Et,t)||null!=e[t]&&isNaN(e[t])))return!1;for(n=0;n<a;++n)if(e[Et[n]]){if(r)return!1;parseFloat(e[Et[n]])!==ue(e[Et[n]])&&(r=!0)}return!0}(e),this._milliseconds=+d+1e3*u+6e4*l+1e3*s*60*60,this._days=+o+7*a,this._months=+r+3*n+12*t,this._data={},this._locale=dt(),this._bubble()}function At(e){return e instanceof Yt}function Ot(e){return e<0?-1*Math.round(-1*e):Math.round(e)}function Ct(e,t){j(e,0,0,function(){var e=this.utcOffset(),n=\"+\";return e<0&&(e=-e,n=\"-\"),n+E(~~(e/60),2)+t+E(~~e%60,2)})}Ct(\"Z\",\":\"),Ct(\"ZZ\",\"\"),ae(\"Z\",te),ae(\"ZZ\",te),fe([\"Z\",\"ZZ\"],function(e,t,n){n._useUTC=!0,n._tzm=Pt(te,e)});var jt=/([\\+\\-]|\\d\\d)/gi;function Pt(e,t){return null===(t=(t||\"\").match(e))?null:0===(t=60*(e=((t[t.length-1]||[])+\"\").match(jt)||[\"-\",0,0])[1]+ue(e[2]))?0:\"+\"===e[0]?t:-t}function Ht(e,n){var r;return n._isUTC?(n=n.clone(),r=(b(e)||l(e)?e:St(e)).valueOf()-n.valueOf(),n._d.setTime(n._d.valueOf()+r),t.updateOffset(n,!1),n):St(e).local()}function It(e){return-Math.round(e._d.getTimezoneOffset())}function Ft(){return!!this.isValid()&&this._isUTC&&0===this._offset}t.updateOffset=function(){};var Nt=/^(-|\\+)?(?:(\\d*)[. ])?(\\d+):(\\d+)(?::(\\d+)(\\.\\d*)?)?$/,Rt=/^(-|\\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function Vt(e,t){var n,r=e;return At(e)?r={ms:e._milliseconds,d:e._days,M:e._months}:s(e)||!isNaN(+e)?(r={},t?r[t]=+e:r.milliseconds=+e):(t=Nt.exec(e))?(n=\"-\"===t[1]?-1:1,r={y:0,d:ue(t[ge])*n,h:ue(t[_e])*n,m:ue(t[ye])*n,s:ue(t[be])*n,ms:ue(Ot(1e3*t[Me]))*n}):(t=Rt.exec(e))?(n=\"-\"===t[1]?-1:1,r={y:$t(t[2],n),M:$t(t[3],n),w:$t(t[4],n),d:$t(t[5],n),h:$t(t[6],n),m:$t(t[7],n),s:$t(t[8],n)}):null==r?r={}:\"object\"==typeof r&&(\"from\"in r||\"to\"in r)&&(t=function(e,t){var n;return e.isValid()&&t.isValid()?(t=Ht(t,e),e.isBefore(t)?n=zt(e,t):((n=zt(t,e)).milliseconds=-n.milliseconds,n.months=-n.months),n):{milliseconds:0,months:0}}(St(r.from),St(r.to)),(r={}).ms=t.milliseconds,r.M=t.months),n=new Yt(r),At(e)&&i(e,\"_locale\")&&(n._locale=e._locale),At(e)&&i(e,\"_isValid\")&&(n._isValid=e._isValid),n}function $t(e,t){return e=e&&parseFloat(e.replace(\",\",\".\")),(isNaN(e)?0:e)*t}function zt(e,t){var n={};return n.months=t.month()-e.month()+12*(t.year()-e.year()),e.clone().add(n.months,\"M\").isAfter(t)&&--n.months,n.milliseconds=+t-+e.clone().add(n.months,\"M\"),n}function Wt(e,t){return function(n,r){var i;return null===r||isNaN(+r)||(L(t,\"moment().\"+t+\"(period, number) is deprecated. Please use moment().\"+t+\"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.\"),i=n,n=r,r=i),Ut(this,Vt(n,r),e),this}}function Ut(e,n,r,i){var a=n._milliseconds,o=Ot(n._days);n=Ot(n._months);e.isValid()&&(i=null==i||i,n&&He(e,De(e,\"Month\")+n*r),o&&Ee(e,\"Date\",De(e,\"Date\")+o*r),a&&e._d.setTime(e._d.valueOf()+a*r),i)&&t.updateOffset(e,o||n)}function Bt(e){return\"string\"==typeof e||e instanceof String}function qt(e,t){var n,r;return e.date()<t.date()?-qt(t,e):-((n=12*(t.year()-e.year())+(t.month()-e.month()))+(t-(r=e.clone().add(n,\"months\"))<0?(t-r)/(r-e.clone().add(n-1,\"months\")):(t-r)/(e.clone().add(1+n,\"months\")-r)))||0}function Gt(e){return void 0===e?this._locale._abbr:(null!=(e=dt(e))&&(this._locale=e),this)}function Jt(){return this._locale}Vt.fn=Yt.prototype,Vt.invalid=function(){return Vt(NaN)},Ae=Wt(1,\"add\"),Be=Wt(-1,\"subtract\"),t.defaultFormat=\"YYYY-MM-DDTHH:mm:ssZ\",t.defaultFormatUtc=\"YYYY-MM-DDTHH:mm:ss[Z]\",Ge=w(\"moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.\",function(e){return void 0===e?this.localeData():this.locale(e)});var Zt=126227808e5;function Kt(e,t){return(e%t+t)%t}function Xt(e,t,n){return e<100&&0<=e?new Date(e+400,t,n)-Zt:new Date(e,t,n).valueOf()}function Qt(e,t,n){return e<100&&0<=e?Date.UTC(e+400,t,n)-Zt:Date.UTC(e,t,n)}function en(e,t){return t.erasAbbrRegex(e)}function tn(){for(var e,t,n,r=[],i=[],a=[],o=[],s=this.eras(),l=0,u=s.length;l<u;++l)e=se(s[l].name),t=se(s[l].abbr),n=se(s[l].narrow),i.push(e),r.push(t),a.push(n),o.push(e),o.push(t),o.push(n);this._erasRegex=new RegExp(\"^(\"+o.join(\"|\")+\")\",\"i\"),this._erasNameRegex=new RegExp(\"^(\"+i.join(\"|\")+\")\",\"i\"),this._erasAbbrRegex=new RegExp(\"^(\"+r.join(\"|\")+\")\",\"i\"),this._erasNarrowRegex=new RegExp(\"^(\"+a.join(\"|\")+\")\",\"i\")}function nn(e,t){j(0,[e,e.length],0,t)}function rn(e,t,n,r,i){var a;return null==e?ze(this,r,i).year:(a=We(e,r,i),function(e,t,n,r,i){return t=Re((e=$e(e,t,n,r,i)).year,0,e.dayOfYear),this.year(t.getUTCFullYear()),this.month(t.getUTCMonth()),this.date(t.getUTCDate()),this}.call(this,e,t=a<t?a:t,n,r,i))}j(\"N\",0,0,\"eraAbbr\"),j(\"NN\",0,0,\"eraAbbr\"),j(\"NNN\",0,0,\"eraAbbr\"),j(\"NNNN\",0,0,\"eraName\"),j(\"NNNNN\",0,0,\"eraNarrow\"),j(\"y\",[\"y\",1],\"yo\",\"eraYear\"),j(\"y\",[\"yy\",2],0,\"eraYear\"),j(\"y\",[\"yyy\",3],0,\"eraYear\"),j(\"y\",[\"yyyy\",4],0,\"eraYear\"),ae(\"N\",en),ae(\"NN\",en),ae(\"NNN\",en),ae(\"NNNN\",function(e,t){return t.erasNameRegex(e)}),ae(\"NNNNN\",function(e,t){return t.erasNarrowRegex(e)}),fe([\"N\",\"NN\",\"NNN\",\"NNNN\",\"NNNNN\"],function(e,t,n,r){(r=n._locale.erasParse(e,r,n._strict))?f(n).era=r:f(n).invalidEra=e}),ae(\"y\",X),ae(\"yy\",X),ae(\"yyy\",X),ae(\"yyyy\",X),ae(\"yo\",function(e,t){return t._eraYearOrdinalRegex||X}),fe([\"y\",\"yy\",\"yyy\",\"yyyy\"],me),fe([\"yo\"],function(e,t,n,r){var i;n._locale._eraYearOrdinalRegex&&(i=e.match(n._locale._eraYearOrdinalRegex)),n._locale.eraYearOrdinalParse?t[me]=n._locale.eraYearOrdinalParse(e,i):t[me]=parseInt(e,10)}),j(0,[\"gg\",2],0,function(){return this.weekYear()%100}),j(0,[\"GG\",2],0,function(){return this.isoWeekYear()%100}),nn(\"gggg\",\"weekYear\"),nn(\"ggggg\",\"weekYear\"),nn(\"GGGG\",\"isoWeekYear\"),nn(\"GGGGG\",\"isoWeekYear\"),ae(\"G\",Q),ae(\"g\",Q),ae(\"GG\",B,$),ae(\"gg\",B,$),ae(\"GGGG\",Z,W),ae(\"gggg\",Z,W),ae(\"GGGGG\",K,U),ae(\"ggggg\",K,U),pe([\"gggg\",\"ggggg\",\"GGGG\",\"GGGGG\"],function(e,t,n,r){t[r.substr(0,2)]=ue(e)}),pe([\"gg\",\"GG\"],function(e,n,r,i){n[i]=t.parseTwoDigitYear(e)}),j(\"Q\",0,\"Qo\",\"quarter\"),ae(\"Q\",V),fe(\"Q\",function(e,t){t[ve]=3*(ue(e)-1)}),j(\"D\",[\"DD\",2],\"Do\",\"date\"),ae(\"D\",B,re),ae(\"DD\",B,$),ae(\"Do\",function(e,t){return e?t._dayOfMonthOrdinalParse||t._ordinalParse:t._dayOfMonthOrdinalParseLenient}),fe([\"D\",\"DD\"],ge),fe(\"Do\",function(e,t){t[ge]=ue(e.match(B)[0])}),Z=Se(\"Date\",!0),j(\"DDD\",[\"DDDD\",3],\"DDDo\",\"dayOfYear\"),ae(\"DDD\",J),ae(\"DDDD\",z),fe([\"DDD\",\"DDDD\"],function(e,t,n){n._dayOfYear=ue(e)}),j(\"m\",[\"mm\",2],0,\"minute\"),ae(\"m\",B,ie),ae(\"mm\",B,$),fe([\"m\",\"mm\"],ye);var an;W=Se(\"Minutes\",!1),j(\"s\",[\"ss\",2],0,\"second\"),ae(\"s\",B,ie),ae(\"ss\",B,$),fe([\"s\",\"ss\"],be),K=Se(\"Seconds\",!1);for(j(\"S\",0,0,function(){return~~(this.millisecond()/100)}),j(0,[\"SS\",2],0,function(){return~~(this.millisecond()/10)}),j(0,[\"SSS\",3],0,\"millisecond\"),j(0,[\"SSSS\",4],0,function(){return 10*this.millisecond()}),j(0,[\"SSSSS\",5],0,function(){return 100*this.millisecond()}),j(0,[\"SSSSSS\",6],0,function(){return 1e3*this.millisecond()}),j(0,[\"SSSSSSS\",7],0,function(){return 1e4*this.millisecond()}),j(0,[\"SSSSSSSS\",8],0,function(){return 1e5*this.millisecond()}),j(0,[\"SSSSSSSSS\",9],0,function(){return 1e6*this.millisecond()}),ae(\"S\",J,V),ae(\"SS\",J,$),ae(\"SSS\",J,z),an=\"SSSS\";an.length<=9;an+=\"S\")ae(an,X);function on(e,t){t[Me]=ue(1e3*(\"0.\"+e))}for(an=\"S\";an.length<=9;an+=\"S\")fe(an,on);function sn(e){return e}function ln(e,t,n,r){var i=dt();r=c().set(r,t);return i[n](r,e)}function un(e,t,n){if(s(e)&&(t=e,e=void 0),e=e||\"\",null!=t)return ln(e,t,n,\"month\");for(var r=[],i=0;i<12;i++)r[i]=ln(e,i,n,\"month\");return r}function dn(e,t,n,r){\"boolean\"==typeof e?s(t)&&(n=t,t=void 0):(t=e,e=!1,s(n=t)&&(n=t,t=void 0)),t=t||\"\";var i,a=dt(),o=e?a._week.dow:0,l=[];if(null!=n)return ln(t,(n+o)%7,r,\"day\");for(i=0;i<7;i++)l[i]=ln(t,(i+o)%7,r,\"day\");return l}U=Se(\"Milliseconds\",!1),j(\"z\",0,0,\"zoneAbbr\"),j(\"zz\",0,0,\"zoneName\"),(re=y.prototype).add=Ae,re.calendar=function(e,o){1===arguments.length&&(arguments[0]?function(e){return b(e)||l(e)||Bt(e)||s(e)||function(e){var t=n(e),r=!1;return t&&(r=0===e.filter(function(t){return!s(t)&&Bt(e)}).length),t&&r}(e)||function(e){var t,n=r(e)&&!a(e),o=!1,s=[\"years\",\"year\",\"y\",\"months\",\"month\",\"M\",\"days\",\"day\",\"d\",\"dates\",\"date\",\"D\",\"hours\",\"hour\",\"h\",\"minutes\",\"minute\",\"m\",\"seconds\",\"second\",\"s\",\"milliseconds\",\"millisecond\",\"ms\"],l=s.length;for(t=0;t<l;t+=1)o=o||i(e,s[t]);return n&&o}(e)||null==e}(arguments[0])?(e=arguments[0],o=void 0):function(e){for(var t=r(e)&&!a(e),n=!1,o=[\"sameDay\",\"nextDay\",\"lastDay\",\"nextWeek\",\"lastWeek\",\"sameElse\"],s=0;s<o.length;s+=1)n=n||i(e,o[s]);return t&&n}(arguments[0])&&(o=arguments[0],e=void 0):o=e=void 0);var u=Ht(e=e||St(),this).startOf(\"day\");u=t.calendarFormat(this,u)||\"sameElse\",o=o&&(x(o[u])?o[u].call(this,e):o[u]);return this.format(o||this.localeData().calendar(u,this,St(e)))},re.clone=function(){return new y(this)},re.diff=function(e,t,n){var r,i,a;if(!this.isValid())return NaN;if(!(r=Ht(e,this)).isValid())return NaN;switch(i=6e4*(r.utcOffset()-this.utcOffset()),t=F(t)){case\"year\":a=qt(this,r)/12;break;case\"month\":a=qt(this,r);break;case\"quarter\":a=qt(this,r)/3;break;case\"second\":a=(this-r)/1e3;break;case\"minute\":a=(this-r)/6e4;break;case\"hour\":a=(this-r)/36e5;break;case\"day\":a=(this-r-i)/864e5;break;case\"week\":a=(this-r-i)/6048e5;break;default:a=this-r}return n?a:le(a)},re.endOf=function(e){var n,r;if(void 0!==(e=F(e))&&\"millisecond\"!==e&&this.isValid()){switch(r=this._isUTC?Qt:Xt,e){case\"year\":n=r(this.year()+1,0,1)-1;break;case\"quarter\":n=r(this.year(),this.month()-this.month()%3+3,1)-1;break;case\"month\":n=r(this.year(),this.month()+1,1)-1;break;case\"week\":n=r(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case\"isoWeek\":n=r(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case\"day\":case\"date\":n=r(this.year(),this.month(),this.date()+1)-1;break;case\"hour\":n=this._d.valueOf(),n+=36e5-Kt(n+(this._isUTC?0:6e4*this.utcOffset()),36e5)-1;break;case\"minute\":n=this._d.valueOf(),n+=6e4-Kt(n,6e4)-1;break;case\"second\":n=this._d.valueOf(),n+=1e3-Kt(n,1e3)-1}this._d.setTime(n),t.updateOffset(this,!0)}return this},re.format=function(e){return e=P(this,e=e||(this.isUtc()?t.defaultFormatUtc:t.defaultFormat)),this.localeData().postformat(e)},re.from=function(e,t){return this.isValid()&&(b(e)&&e.isValid()||St(e).isValid())?Vt({to:this,from:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},re.fromNow=function(e){return this.from(St(),e)},re.to=function(e,t){return this.isValid()&&(b(e)&&e.isValid()||St(e).isValid())?Vt({from:this,to:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},re.toNow=function(e){return this.to(St(),e)},re.get=function(e){return x(this[e=F(e)])?this[e]():this},re.invalidAt=function(){return f(this).overflow},re.isAfter=function(e,t){return e=b(e)?e:St(e),!(!this.isValid()||!e.isValid())&&(\"millisecond\"===(t=F(t)||\"millisecond\")?this.valueOf()>e.valueOf():e.valueOf()<this.clone().startOf(t).valueOf())},re.isBefore=function(e,t){return e=b(e)?e:St(e),!(!this.isValid()||!e.isValid())&&(\"millisecond\"===(t=F(t)||\"millisecond\")?this.valueOf()<e.valueOf():this.clone().endOf(t).valueOf()<e.valueOf())},re.isBetween=function(e,t,n,r){return e=b(e)?e:St(e),t=b(t)?t:St(t),!!(this.isValid()&&e.isValid()&&t.isValid())&&(\"(\"===(r=r||\"()\")[0]?this.isAfter(e,n):!this.isBefore(e,n))&&(\")\"===r[1]?this.isBefore(t,n):!this.isAfter(t,n))},re.isSame=function(e,t){e=b(e)?e:St(e);return!(!this.isValid()||!e.isValid())&&(\"millisecond\"===(t=F(t)||\"millisecond\")?this.valueOf()===e.valueOf():(e=e.valueOf(),this.clone().startOf(t).valueOf()<=e&&e<=this.clone().endOf(t).valueOf()))},re.isSameOrAfter=function(e,t){return this.isSame(e,t)||this.isAfter(e,t)},re.isSameOrBefore=function(e,t){return this.isSame(e,t)||this.isBefore(e,t)},re.isValid=function(){return p(this)},re.lang=Ge,re.locale=Gt,re.localeData=Jt,re.max=G,re.min=q,re.parsingFlags=function(){return d({},f(this))},re.set=function(e,t){if(\"object\"==typeof e)for(var n=function(e){var t,n=[];for(t in e)i(e,t)&&n.push({unit:t,priority:R[t]});return n.sort(function(e,t){return e.priority-t.priority}),n}(e=N(e)),r=n.length,a=0;a<r;a++)this[n[a].unit](e[n[a].unit]);else if(x(this[e=F(e)]))return this[e](t);return this},re.startOf=function(e){var n,r;if(void 0!==(e=F(e))&&\"millisecond\"!==e&&this.isValid()){switch(r=this._isUTC?Qt:Xt,e){case\"year\":n=r(this.year(),0,1);break;case\"quarter\":n=r(this.year(),this.month()-this.month()%3,1);break;case\"month\":n=r(this.year(),this.month(),1);break;case\"week\":n=r(this.year(),this.month(),this.date()-this.weekday());break;case\"isoWeek\":n=r(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case\"day\":case\"date\":n=r(this.year(),this.month(),this.date());break;case\"hour\":n=this._d.valueOf(),n-=Kt(n+(this._isUTC?0:6e4*this.utcOffset()),36e5);break;case\"minute\":n=this._d.valueOf(),n-=Kt(n,6e4);break;case\"second\":n=this._d.valueOf(),n-=Kt(n,1e3)}this._d.setTime(n),t.updateOffset(this,!0)}return this},re.subtract=Be,re.toArray=function(){var e=this;return[e.year(),e.month(),e.date(),e.hour(),e.minute(),e.second(),e.millisecond()]},re.toObject=function(){var e=this;return{years:e.year(),months:e.month(),date:e.date(),hours:e.hours(),minutes:e.minutes(),seconds:e.seconds(),milliseconds:e.milliseconds()}},re.toDate=function(){return new Date(this.valueOf())},re.toISOString=function(e){var t;return this.isValid()?(t=(e=!0!==e)?this.clone().utc():this).year()<0||9999<t.year()?P(t,e?\"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]\":\"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ\"):x(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace(\"Z\",P(t,\"Z\")):P(t,e?\"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]\":\"YYYY-MM-DD[T]HH:mm:ss.SSSZ\"):null},re.inspect=function(){var e,t,n;return this.isValid()?(t=\"moment\",e=\"\",this.isLocal()||(t=0===this.utcOffset()?\"moment.utc\":\"moment.parseZone\",e=\"Z\"),t=\"[\"+t+'(\"]',n=0<=this.year()&&this.year()<=9999?\"YYYY\":\"YYYYYY\",this.format(t+n+\"-MM-DD[T]HH:mm:ss.SSS\"+e+'[\")]')):\"moment.invalid(/* \"+this._i+\" */)\"},\"undefined\"!=typeof Symbol&&null!=Symbol.for&&(re[Symbol.for(\"nodejs.util.inspect.custom\")]=function(){return\"Moment<\"+this.format()+\">\"}),re.toJSON=function(){return this.isValid()?this.toISOString():null},re.toString=function(){return this.clone().locale(\"en\").format(\"ddd MMM DD YYYY HH:mm:ss [GMT]ZZ\")},re.unix=function(){return Math.floor(this.valueOf()/1e3)},re.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},re.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},re.eraName=function(){for(var e,t=this.localeData().eras(),n=0,r=t.length;n<r;++n){if(e=this.clone().startOf(\"day\").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].name;if(t[n].until<=e&&e<=t[n].since)return t[n].name}return\"\"},re.eraNarrow=function(){for(var e,t=this.localeData().eras(),n=0,r=t.length;n<r;++n){if(e=this.clone().startOf(\"day\").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].narrow;if(t[n].until<=e&&e<=t[n].since)return t[n].narrow}return\"\"},re.eraAbbr=function(){for(var e,t=this.localeData().eras(),n=0,r=t.length;n<r;++n){if(e=this.clone().startOf(\"day\").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].abbr;if(t[n].until<=e&&e<=t[n].since)return t[n].abbr}return\"\"},re.eraYear=function(){for(var e,n,r=this.localeData().eras(),i=0,a=r.length;i<a;++i)if(e=r[i].since<=r[i].until?1:-1,n=this.clone().startOf(\"day\").valueOf(),r[i].since<=n&&n<=r[i].until||r[i].until<=n&&n<=r[i].since)return(this.year()-t(r[i].since).year())*e+r[i].offset;return this.year()},re.year=Te,re.isLeapYear=function(){return he(this.year())},re.weekYear=function(e){return rn.call(this,e,this.week(),this.weekday()+this.localeData()._week.dow,this.localeData()._week.dow,this.localeData()._week.doy)},re.isoWeekYear=function(e){return rn.call(this,e,this.isoWeek(),this.isoWeekday(),1,4)},re.quarter=re.quarters=function(e){return null==e?Math.ceil((this.month()+1)/3):this.month(3*(e-1)+this.month()%3)},re.month=Ie,re.daysInMonth=function(){return Ye(this.year(),this.month())},re.week=re.weeks=function(e){var t=this.localeData().week(this);return null==e?t:this.add(7*(e-t),\"d\")},re.isoWeek=re.isoWeeks=function(e){var t=ze(this,1,4).week;return null==e?t:this.add(7*(e-t),\"d\")},re.weeksInYear=function(){var e=this.localeData()._week;return We(this.year(),e.dow,e.doy)},re.weeksInWeekYear=function(){var e=this.localeData()._week;return We(this.weekYear(),e.dow,e.doy)},re.isoWeeksInYear=function(){return We(this.year(),1,4)},re.isoWeeksInISOWeekYear=function(){return We(this.isoWeekYear(),1,4)},re.date=Z,re.day=re.days=function(e){var t,n,r;return this.isValid()?(t=De(this,\"Day\"),null!=e?(n=e,r=this.localeData(),e=\"string\"!=typeof n?n:isNaN(n)?\"number\"==typeof(n=r.weekdaysParse(n))?n:null:parseInt(n,10),this.add(e-t,\"d\")):t):null!=e?this:NaN},re.weekday=function(e){var t;return this.isValid()?(t=(this.day()+7-this.localeData()._week.dow)%7,null==e?t:this.add(e-t,\"d\")):null!=e?this:NaN},re.isoWeekday=function(e){var t,n;return this.isValid()?null!=e?(t=e,n=this.localeData(),n=\"string\"==typeof t?n.weekdaysParse(t)%7||7:isNaN(t)?null:t,this.day(this.day()%7?n:n-7)):this.day()||7:null!=e?this:NaN},re.dayOfYear=function(e){var t=Math.round((this.clone().startOf(\"day\")-this.clone().startOf(\"year\"))/864e5)+1;return null==e?t:this.add(e-t,\"d\")},re.hour=re.hours=ne,re.minute=re.minutes=W,re.second=re.seconds=K,re.millisecond=re.milliseconds=U,re.utcOffset=function(e,n,r){var i,a=this._offset||0;if(!this.isValid())return null!=e?this:NaN;if(null==e)return this._isUTC?a:It(this);if(\"string\"==typeof e){if(null===(e=Pt(te,e)))return this}else Math.abs(e)<16&&!r&&(e*=60);return!this._isUTC&&n&&(i=It(this)),this._offset=e,this._isUTC=!0,null!=i&&this.add(i,\"m\"),a!==e&&(!n||this._changeInProgress?Ut(this,Vt(e-a,\"m\"),1,!1):this._changeInProgress||(this._changeInProgress=!0,t.updateOffset(this,!0),this._changeInProgress=null)),this},re.utc=function(e){return this.utcOffset(0,e)},re.local=function(e){return this._isUTC&&(this.utcOffset(0,e),this._isUTC=!1,e)&&this.subtract(It(this),\"m\"),this},re.parseZone=function(){var e;return null!=this._tzm?this.utcOffset(this._tzm,!1,!0):\"string\"==typeof this._i&&(null!=(e=Pt(ee,this._i))?this.utcOffset(e):this.utcOffset(0,!0)),this},re.hasAlignedHourOffset=function(e){return!!this.isValid()&&(e=e?St(e).utcOffset():0,(this.utcOffset()-e)%60==0)},re.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},re.isLocal=function(){return!!this.isValid()&&!this._isUTC},re.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},re.isUtc=Ft,re.isUTC=Ft,re.zoneAbbr=function(){return this._isUTC?\"UTC\":\"\"},re.zoneName=function(){return this._isUTC?\"Coordinated Universal Time\":\"\"},re.dates=w(\"dates accessor is deprecated. Use date instead.\",Z),re.months=w(\"months accessor is deprecated. Use month instead\",Ie),re.years=w(\"years accessor is deprecated. Use year instead\",Te),re.zone=w(\"moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/\",function(e,t){return null!=e?(this.utcOffset(e=\"string\"!=typeof e?-e:e,t),this):-this.utcOffset()}),re.isDSTShifted=w(\"isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information\",function(){var e,t;return o(this._isDSTShifted)&&(_(e={},this),(e=xt(e))._a?(t=(e._isUTC?c:St)(e._a),this._isDSTShifted=this.isValid()&&0<function(e,t){for(var n=Math.min(e.length,t.length),r=Math.abs(e.length-t.length),i=0,a=0;a<n;a++)ue(e[a])!==ue(t[a])&&i++;return i+r}(e._a,t.toArray())):this._isDSTShifted=!1),this._isDSTShifted}),(ie=S.prototype).calendar=function(e,t,n){return x(e=this._calendar[e]||this._calendar.sameElse)?e.call(t,n):e},ie.longDateFormat=function(e){var t=this._longDateFormat[e],n=this._longDateFormat[e.toUpperCase()];return t||!n?t:(this._longDateFormat[e]=n.match(Y).map(function(e){return\"MMMM\"===e||\"MM\"===e||\"DD\"===e||\"dddd\"===e?e.slice(1):e}).join(\"\"),this._longDateFormat[e])},ie.invalidDate=function(){return this._invalidDate},ie.ordinal=function(e){return this._ordinal.replace(\"%d\",e)},ie.preparse=sn,ie.postformat=sn,ie.relativeTime=function(e,t,n,r){var i=this._relativeTime[n];return x(i)?i(e,t,n,r):i.replace(/%d/i,e)},ie.pastFuture=function(e,t){return x(e=this._relativeTime[0<e?\"future\":\"past\"])?e(t):e.replace(/%s/i,t)},ie.set=function(e){var t,n;for(n in e)i(e,n)&&(x(t=e[n])?this[n]=t:this[\"_\"+n]=t);this._config=e,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+\"|\"+/\\d{1,2}/.source)},ie.eras=function(e,n){for(var r,i=this._eras||dt(\"en\")._eras,a=0,o=i.length;a<o;++a){if(\"string\"==typeof i[a].since)r=t(i[a].since).startOf(\"day\"),i[a].since=r.valueOf();switch(typeof i[a].until){case\"undefined\":i[a].until=1/0;break;case\"string\":r=t(i[a].until).startOf(\"day\").valueOf(),i[a].until=r.valueOf()}}return i},ie.erasParse=function(e,t,n){var r,i,a,o,s,l=this.eras();for(e=e.toUpperCase(),r=0,i=l.length;r<i;++r)if(a=l[r].name.toUpperCase(),o=l[r].abbr.toUpperCase(),s=l[r].narrow.toUpperCase(),n)switch(t){case\"N\":case\"NN\":case\"NNN\":if(o===e)return l[r];break;case\"NNNN\":if(a===e)return l[r];break;case\"NNNNN\":if(s===e)return l[r]}else if(0<=[a,o,s].indexOf(e))return l[r]},ie.erasConvertYear=function(e,n){var r=e.since<=e.until?1:-1;return void 0===n?t(e.since).year():t(e.since).year()+(n-e.offset)*r},ie.erasAbbrRegex=function(e){return i(this,\"_erasAbbrRegex\")||tn.call(this),e?this._erasAbbrRegex:this._erasRegex},ie.erasNameRegex=function(e){return i(this,\"_erasNameRegex\")||tn.call(this),e?this._erasNameRegex:this._erasRegex},ie.erasNarrowRegex=function(e){return i(this,\"_erasNarrowRegex\")||tn.call(this),e?this._erasNarrowRegex:this._erasRegex},ie.months=function(e,t){return e?(n(this._months)?this._months:this._months[(this._months.isFormat||Ce).test(t)?\"format\":\"standalone\"])[e.month()]:n(this._months)?this._months:this._months.standalone},ie.monthsShort=function(e,t){return e?(n(this._monthsShort)?this._monthsShort:this._monthsShort[Ce.test(t)?\"format\":\"standalone\"])[e.month()]:n(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},ie.monthsParse=function(e,t,n){var r,i;if(this._monthsParseExact)return function(e,t,n){var r,i,a;e=e.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],r=0;r<12;++r)a=c([2e3,r]),this._shortMonthsParse[r]=this.monthsShort(a,\"\").toLocaleLowerCase(),this._longMonthsParse[r]=this.months(a,\"\").toLocaleLowerCase();return n?\"MMM\"===t?-1!==(i=xe.call(this._shortMonthsParse,e))?i:null:-1!==(i=xe.call(this._longMonthsParse,e))?i:null:\"MMM\"===t?-1!==(i=xe.call(this._shortMonthsParse,e))||-1!==(i=xe.call(this._longMonthsParse,e))?i:null:-1!==(i=xe.call(this._longMonthsParse,e))||-1!==(i=xe.call(this._shortMonthsParse,e))?i:null}.call(this,e,t,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),r=0;r<12;r++){if(i=c([2e3,r]),n&&!this._longMonthsParse[r]&&(this._longMonthsParse[r]=new RegExp(\"^\"+this.months(i,\"\").replace(\".\",\"\")+\"$\",\"i\"),this._shortMonthsParse[r]=new RegExp(\"^\"+this.monthsShort(i,\"\").replace(\".\",\"\")+\"$\",\"i\")),n||this._monthsParse[r]||(i=\"^\"+this.months(i,\"\")+\"|^\"+this.monthsShort(i,\"\"),this._monthsParse[r]=new RegExp(i.replace(\".\",\"\"),\"i\")),n&&\"MMMM\"===t&&this._longMonthsParse[r].test(e))return r;if(n&&\"MMM\"===t&&this._shortMonthsParse[r].test(e))return r;if(!n&&this._monthsParse[r].test(e))return r}},ie.monthsRegex=function(e){return this._monthsParseExact?(i(this,\"_monthsRegex\")||Fe.call(this),e?this._monthsStrictRegex:this._monthsRegex):(i(this,\"_monthsRegex\")||(this._monthsRegex=Pe),this._monthsStrictRegex&&e?this._monthsStrictRegex:this._monthsRegex)},ie.monthsShortRegex=function(e){return this._monthsParseExact?(i(this,\"_monthsRegex\")||Fe.call(this),e?this._monthsShortStrictRegex:this._monthsShortRegex):(i(this,\"_monthsShortRegex\")||(this._monthsShortRegex=je),this._monthsShortStrictRegex&&e?this._monthsShortStrictRegex:this._monthsShortRegex)},ie.week=function(e){return ze(e,this._week.dow,this._week.doy).week},ie.firstDayOfYear=function(){return this._week.doy},ie.firstDayOfWeek=function(){return this._week.dow},ie.weekdays=function(e,t){return t=n(this._weekdays)?this._weekdays:this._weekdays[e&&!0!==e&&this._weekdays.isFormat.test(t)?\"format\":\"standalone\"],!0===e?Ue(t,this._week.dow):e?t[e.day()]:t},ie.weekdaysMin=function(e){return!0===e?Ue(this._weekdaysMin,this._week.dow):e?this._weekdaysMin[e.day()]:this._weekdaysMin},ie.weekdaysShort=function(e){return!0===e?Ue(this._weekdaysShort,this._week.dow):e?this._weekdaysShort[e.day()]:this._weekdaysShort},ie.weekdaysParse=function(e,t,n){var r,i;if(this._weekdaysParseExact)return function(e,t,n){var r,i,a;e=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],r=0;r<7;++r)a=c([2e3,1]).day(r),this._minWeekdaysParse[r]=this.weekdaysMin(a,\"\").toLocaleLowerCase(),this._shortWeekdaysParse[r]=this.weekdaysShort(a,\"\").toLocaleLowerCase(),this._weekdaysParse[r]=this.weekdays(a,\"\").toLocaleLowerCase();return n?\"dddd\"===t?-1!==(i=xe.call(this._weekdaysParse,e))?i:null:\"ddd\"===t?-1!==(i=xe.call(this._shortWeekdaysParse,e))?i:null:-1!==(i=xe.call(this._minWeekdaysParse,e))?i:null:\"dddd\"===t?-1!==(i=xe.call(this._weekdaysParse,e))||-1!==(i=xe.call(this._shortWeekdaysParse,e))||-1!==(i=xe.call(this._minWeekdaysParse,e))?i:null:\"ddd\"===t?-1!==(i=xe.call(this._shortWeekdaysParse,e))||-1!==(i=xe.call(this._weekdaysParse,e))||-1!==(i=xe.call(this._minWeekdaysParse,e))?i:null:-1!==(i=xe.call(this._minWeekdaysParse,e))||-1!==(i=xe.call(this._weekdaysParse,e))||-1!==(i=xe.call(this._shortWeekdaysParse,e))?i:null}.call(this,e,t,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),r=0;r<7;r++){if(i=c([2e3,1]).day(r),n&&!this._fullWeekdaysParse[r]&&(this._fullWeekdaysParse[r]=new RegExp(\"^\"+this.weekdays(i,\"\").replace(\".\",\"\\\\.?\")+\"$\",\"i\"),this._shortWeekdaysParse[r]=new RegExp(\"^\"+this.weekdaysShort(i,\"\").replace(\".\",\"\\\\.?\")+\"$\",\"i\"),this._minWeekdaysParse[r]=new RegExp(\"^\"+this.weekdaysMin(i,\"\").replace(\".\",\"\\\\.?\")+\"$\",\"i\")),this._weekdaysParse[r]||(i=\"^\"+this.weekdays(i,\"\")+\"|^\"+this.weekdaysShort(i,\"\")+\"|^\"+this.weekdaysMin(i,\"\"),this._weekdaysParse[r]=new RegExp(i.replace(\".\",\"\"),\"i\")),n&&\"dddd\"===t&&this._fullWeekdaysParse[r].test(e))return r;if(n&&\"ddd\"===t&&this._shortWeekdaysParse[r].test(e))return r;if(n&&\"dd\"===t&&this._minWeekdaysParse[r].test(e))return r;if(!n&&this._weekdaysParse[r].test(e))return r}},ie.weekdaysRegex=function(e){return this._weekdaysParseExact?(i(this,\"_weekdaysRegex\")||Xe.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):(i(this,\"_weekdaysRegex\")||(this._weekdaysRegex=Je),this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex)},ie.weekdaysShortRegex=function(e){return this._weekdaysParseExact?(i(this,\"_weekdaysRegex\")||Xe.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(i(this,\"_weekdaysShortRegex\")||(this._weekdaysShortRegex=Ze),this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},ie.weekdaysMinRegex=function(e){return this._weekdaysParseExact?(i(this,\"_weekdaysRegex\")||Xe.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(i(this,\"_weekdaysMinRegex\")||(this._weekdaysMinRegex=Ke),this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},ie.isPM=function(e){return\"p\"===(e+\"\").toLowerCase().charAt(0)},ie.meridiem=function(e,t,n){return 11<e?n?\"pm\":\"PM\":n?\"am\":\"AM\"},lt(\"en\",{eras:[{since:\"0001-01-01\",until:1/0,offset:1,name:\"Anno Domini\",narrow:\"AD\",abbr:\"AD\"},{since:\"0000-12-31\",until:-1/0,offset:1,name:\"Before Christ\",narrow:\"BC\",abbr:\"BC\"}],dayOfMonthOrdinalParse:/\\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10;return e+(1===ue(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")}}),t.lang=w(\"moment.lang is deprecated. Use moment.locale instead.\",lt),t.langData=w(\"moment.langData is deprecated. Use moment.localeData instead.\",dt);var cn=Math.abs;function fn(e,t,n,r){return t=Vt(t,n),e._milliseconds+=r*t._milliseconds,e._days+=r*t._days,e._months+=r*t._months,e._bubble()}function pn(e){return e<0?Math.floor(e):Math.ceil(e)}function hn(e){return 4800*e/146097}function mn(e){return 146097*e/4800}function vn(e){return function(){return this.as(e)}}function gn(e){return function(){return this.isValid()?this._data[e]:NaN}}V=vn(\"ms\"),$=vn(\"s\"),J=vn(\"m\"),z=vn(\"h\"),Ae=vn(\"d\"),G=vn(\"w\"),q=vn(\"M\"),Be=vn(\"Q\"),ne=vn(\"y\"),W=V;K=gn(\"milliseconds\"),U=gn(\"seconds\"),Z=gn(\"minutes\"),Te=gn(\"hours\"),ie=gn(\"days\");var _n=gn(\"months\"),yn=gn(\"years\"),bn=Math.round,Mn={ss:44,s:45,m:45,h:22,d:26,w:null,M:11};var wn=Math.abs;function kn(e){return(0<e)-(e<0)||+e}function Ln(){var e,t,n,r,i,a,o,s,l,u,d;return this.isValid()?(e=wn(this._milliseconds)/1e3,t=wn(this._days),n=wn(this._months),(s=this.asSeconds())?(r=le(e/60),i=le(r/60),e%=60,r%=60,a=le(n/12),n%=12,o=e?e.toFixed(3).replace(/\\.?0+$/,\"\"):\"\",l=kn(this._months)!==kn(s)?\"-\":\"\",u=kn(this._days)!==kn(s)?\"-\":\"\",d=kn(this._milliseconds)!==kn(s)?\"-\":\"\",(s<0?\"-\":\"\")+\"P\"+(a?l+a+\"Y\":\"\")+(n?l+n+\"M\":\"\")+(t?u+t+\"D\":\"\")+(i||r||e?\"T\":\"\")+(i?d+i+\"H\":\"\")+(r?d+r+\"M\":\"\")+(e?d+o+\"S\":\"\")):\"P0D\"):this.localeData().invalidDate()}function xn(e){return 0===e?0:1===e?1:2===e?2:3<=e%100&&e%100<=10?3:11<=e%100?4:5}function Tn(e){return function(t,n,r,i){var a=xn(t),o=An[e][xn(t)];return(o=2===a?o[n?0:1]:o).replace(/%d/i,t)}}function Sn(e){return 0===e?0:1===e?1:2===e?2:3<=e%100&&e%100<=10?3:11<=e%100?4:5}function Dn(e){return function(t,n,r,i){var a=Sn(t),o=Cn[e][Sn(t)];return(o=2===a?o[n?0:1]:o).replace(/%d/i,t)}}function En(e){return 0===e?0:1===e?1:2===e?2:3<=e%100&&e%100<=10?3:11<=e%100?4:5}function Yn(e){return function(t,n,r,i){var a=En(t),o=Rn[e][En(t)];return(o=2===a?o[n?0:1]:o).replace(/%d/i,t)}}var An=((lr=Yt.prototype).isValid=function(){return this._isValid},lr.abs=function(){var e=this._data;return this._milliseconds=cn(this._milliseconds),this._days=cn(this._days),this._months=cn(this._months),e.milliseconds=cn(e.milliseconds),e.seconds=cn(e.seconds),e.minutes=cn(e.minutes),e.hours=cn(e.hours),e.months=cn(e.months),e.years=cn(e.years),this},lr.add=function(e,t){return fn(this,e,t,1)},lr.subtract=function(e,t){return fn(this,e,t,-1)},lr.as=function(e){if(!this.isValid())return NaN;var t,n,r=this._milliseconds;if(\"month\"===(e=F(e))||\"quarter\"===e||\"year\"===e)switch(t=this._days+r/864e5,n=this._months+hn(t),e){case\"month\":return n;case\"quarter\":return n/3;case\"year\":return n/12}else switch(t=this._days+Math.round(mn(this._months)),e){case\"week\":return t/7+r/6048e5;case\"day\":return t+r/864e5;case\"hour\":return 24*t+r/36e5;case\"minute\":return 1440*t+r/6e4;case\"second\":return 86400*t+r/1e3;case\"millisecond\":return Math.floor(864e5*t)+r;default:throw new Error(\"Unknown unit \"+e)}},lr.asMilliseconds=V,lr.asSeconds=$,lr.asMinutes=J,lr.asHours=z,lr.asDays=Ae,lr.asWeeks=G,lr.asMonths=q,lr.asQuarters=Be,lr.asYears=ne,lr.valueOf=W,lr._bubble=function(){var e=this._milliseconds,t=this._days,n=this._months,r=this._data;return 0<=e&&0<=t&&0<=n||e<=0&&t<=0&&n<=0||(e+=864e5*pn(mn(n)+t),n=t=0),r.milliseconds=e%1e3,e=le(e/1e3),r.seconds=e%60,e=le(e/60),r.minutes=e%60,e=le(e/60),r.hours=e%24,t+=le(e/24),n+=e=le(hn(t)),t-=pn(mn(e)),e=le(n/12),n%=12,r.days=t,r.months=n,r.years=e,this},lr.clone=function(){return Vt(this)},lr.get=function(e){return e=F(e),this.isValid()?this[e+\"s\"]():NaN},lr.milliseconds=K,lr.seconds=U,lr.minutes=Z,lr.hours=Te,lr.days=ie,lr.weeks=function(){return le(this.days()/7)},lr.months=_n,lr.years=yn,lr.humanize=function(e,t){var n,r;return this.isValid()?(n=!1,r=Mn,\"object\"==typeof e&&(t=e,e=!1),\"boolean\"==typeof e&&(n=e),\"object\"==typeof t&&(r=Object.assign({},Mn,t),null!=t.s)&&null==t.ss&&(r.ss=t.s-1),t=function(e,t,n,r){var i=Vt(e).abs(),a=bn(i.as(\"s\")),o=bn(i.as(\"m\")),s=bn(i.as(\"h\")),l=bn(i.as(\"d\")),u=bn(i.as(\"M\")),d=bn(i.as(\"w\"));return i=bn(i.as(\"y\")),a=(a<=n.ss?[\"s\",a]:a<n.s&&[\"ss\",a])||(o<=1?[\"m\"]:o<n.m&&[\"mm\",o])||(s<=1?[\"h\"]:s<n.h&&[\"hh\",s])||(l<=1?[\"d\"]:l<n.d&&[\"dd\",l]),(a=(a=null!=n.w?a||(d<=1?[\"w\"]:d<n.w&&[\"ww\",d]):a)||(u<=1?[\"M\"]:u<n.M&&[\"MM\",u])||(i<=1?[\"y\"]:[\"yy\",i]))[2]=t,a[3]=0<+e,a[4]=r,function(e,t,n,r,i){return i.relativeTime(t||1,!!n,e,r)}.apply(null,a)}(this,!n,r,e=this.localeData()),n&&(t=e.pastFuture(+this,t)),e.postformat(t)):this.localeData().invalidDate()},lr.toISOString=Ln,lr.toString=Ln,lr.toJSON=Ln,lr.locale=Gt,lr.localeData=Jt,lr.toIsoString=w(\"toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)\",Ln),lr.lang=Ge,j(\"X\",0,0,\"unix\"),j(\"x\",0,0,\"valueOf\"),ae(\"x\",Q),ae(\"X\",/[+-]?\\d+(\\.\\d{1,3})?/),fe(\"X\",function(e,t,n){n._d=new Date(1e3*parseFloat(e))}),fe(\"x\",function(e,t,n){n._d=new Date(ue(e))}),t.version=\"2.30.1\",e=St,t.fn=re,t.min=function(){return Dt(\"isBefore\",[].slice.call(arguments,0))},t.max=function(){return Dt(\"isAfter\",[].slice.call(arguments,0))},t.now=function(){return Date.now?Date.now():+new Date},t.utc=c,t.unix=function(e){return St(1e3*e)},t.months=function(e,t){return un(e,t,\"months\")},t.isDate=l,t.locale=lt,t.invalid=h,t.duration=Vt,t.isMoment=b,t.weekdays=function(e,t,n){return dn(e,t,n,\"weekdays\")},t.parseZone=function(){return St.apply(null,arguments).parseZone()},t.localeData=dt,t.isDuration=At,t.monthsShort=function(e,t){return un(e,t,\"monthsShort\")},t.weekdaysMin=function(e,t,n){return dn(e,t,n,\"weekdaysMin\")},t.defineLocale=ut,t.updateLocale=function(e,t){var n,r;return null!=t?(r=rt,null!=it[e]&&null!=it[e].parentLocale?it[e].set(T(it[e]._config,t)):(t=T(r=null!=(n=st(e))?n._config:r,t),null==n&&(t.abbr=e),(r=new S(t)).parentLocale=it[e],it[e]=r),lt(e)):null!=it[e]&&(null!=it[e].parentLocale?(it[e]=it[e].parentLocale,e===lt()&&lt(e)):null!=it[e]&&delete it[e]),it[e]},t.locales=function(){return D(it)},t.weekdaysShort=function(e,t,n){return dn(e,t,n,\"weekdaysShort\")},t.normalizeUnits=F,t.relativeTimeRounding=function(e){return void 0===e?bn:\"function\"==typeof e&&(bn=e,!0)},t.relativeTimeThreshold=function(e,t){return void 0!==Mn[e]&&(void 0===t?Mn[e]:(Mn[e]=t,\"s\"===e&&(Mn.ss=t-1),!0))},t.calendarFormat=function(e,t){return(e=e.diff(t,\"days\",!0))<-6?\"sameElse\":e<-1?\"lastWeek\":e<0?\"lastDay\":e<1?\"sameDay\":e<2?\"nextDay\":e<7?\"nextWeek\":\"sameElse\"},t.prototype=re,t.HTML5_FMT={DATETIME_LOCAL:\"YYYY-MM-DDTHH:mm\",DATETIME_LOCAL_SECONDS:\"YYYY-MM-DDTHH:mm:ss\",DATETIME_LOCAL_MS:\"YYYY-MM-DDTHH:mm:ss.SSS\",DATE:\"YYYY-MM-DD\",TIME:\"HH:mm\",TIME_SECONDS:\"HH:mm:ss\",TIME_MS:\"HH:mm:ss.SSS\",WEEK:\"GGGG-[W]WW\",MONTH:\"YYYY-MM\"},t.defineLocale(\"af\",{months:\"Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember\".split(\"_\"),monthsShort:\"Jan_Feb_Mrt_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des\".split(\"_\"),weekdays:\"Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag\".split(\"_\"),weekdaysShort:\"Son_Maa_Din_Woe_Don_Vry_Sat\".split(\"_\"),weekdaysMin:\"So_Ma_Di_Wo_Do_Vr_Sa\".split(\"_\"),meridiemParse:/vm|nm/i,isPM:function(e){return/^nm$/i.test(e)},meridiem:function(e,t,n){return e<12?n?\"vm\":\"VM\":n?\"nm\":\"NM\"},longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Vandag om] LT\",nextDay:\"[Môre om] LT\",nextWeek:\"dddd [om] LT\",lastDay:\"[Gister om] LT\",lastWeek:\"[Laas] dddd [om] LT\",sameElse:\"L\"},relativeTime:{future:\"oor %s\",past:\"%s gelede\",s:\"'n paar sekondes\",ss:\"%d sekondes\",m:\"'n minuut\",mm:\"%d minute\",h:\"'n uur\",hh:\"%d ure\",d:\"'n dag\",dd:\"%d dae\",M:\"'n maand\",MM:\"%d maande\",y:\"'n jaar\",yy:\"%d jaar\"},dayOfMonthOrdinalParse:/\\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||20<=e?\"ste\":\"de\")},week:{dow:1,doy:4}}),{s:[\"أقل من ثانية\",\"ثانية واحدة\",[\"ثانيتان\",\"ثانيتين\"],\"%d ثوان\",\"%d ثانية\",\"%d ثانية\"],m:[\"أقل من دقيقة\",\"دقيقة واحدة\",[\"دقيقتان\",\"دقيقتين\"],\"%d دقائق\",\"%d دقيقة\",\"%d دقيقة\"],h:[\"أقل من ساعة\",\"ساعة واحدة\",[\"ساعتان\",\"ساعتين\"],\"%d ساعات\",\"%d ساعة\",\"%d ساعة\"],d:[\"أقل من يوم\",\"يوم واحد\",[\"يومان\",\"يومين\"],\"%d أيام\",\"%d يومًا\",\"%d يوم\"],M:[\"أقل من شهر\",\"شهر واحد\",[\"شهران\",\"شهرين\"],\"%d أشهر\",\"%d شهرا\",\"%d شهر\"],y:[\"أقل من عام\",\"عام واحد\",[\"عامان\",\"عامين\"],\"%d أعوام\",\"%d عامًا\",\"%d عام\"]}),On=(V=[\"جانفي\",\"فيفري\",\"مارس\",\"أفريل\",\"ماي\",\"جوان\",\"جويلية\",\"أوت\",\"سبتمبر\",\"أكتوبر\",\"نوفمبر\",\"ديسمبر\"],t.defineLocale(\"ar-dz\",{months:V,monthsShort:V,weekdays:\"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),weekdaysShort:\"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت\".split(\"_\"),weekdaysMin:\"ح_ن_ث_ر_خ_ج_س\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"D/‏M/‏YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},meridiemParse:/\\u0635|\\u0645/,isPM:function(e){return\"م\"===e},meridiem:function(e,t,n){return e<12?\"ص\":\"م\"},calendar:{sameDay:\"[اليوم عند الساعة] LT\",nextDay:\"[غدًا عند الساعة] LT\",nextWeek:\"dddd [عند الساعة] LT\",lastDay:\"[أمس عند الساعة] LT\",lastWeek:\"dddd [عند الساعة] LT\",sameElse:\"L\"},relativeTime:{future:\"بعد %s\",past:\"منذ %s\",s:Tn(\"s\"),ss:Tn(\"s\"),m:Tn(\"m\"),mm:Tn(\"m\"),h:Tn(\"h\"),hh:Tn(\"h\"),d:Tn(\"d\"),dd:Tn(\"d\"),M:Tn(\"M\"),MM:Tn(\"M\"),y:Tn(\"y\"),yy:Tn(\"y\")},postformat:function(e){return e.replace(/,/g,\"،\")},week:{dow:0,doy:4}}),t.defineLocale(\"ar-kw\",{months:\"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر\".split(\"_\"),monthsShort:\"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر\".split(\"_\"),weekdays:\"الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),weekdaysShort:\"احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت\".split(\"_\"),weekdaysMin:\"ح_ن_ث_ر_خ_ج_س\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[اليوم على الساعة] LT\",nextDay:\"[غدا على الساعة] LT\",nextWeek:\"dddd [على الساعة] LT\",lastDay:\"[أمس على الساعة] LT\",lastWeek:\"dddd [على الساعة] LT\",sameElse:\"L\"},relativeTime:{future:\"في %s\",past:\"منذ %s\",s:\"ثوان\",ss:\"%d ثانية\",m:\"دقيقة\",mm:\"%d دقائق\",h:\"ساعة\",hh:\"%d ساعات\",d:\"يوم\",dd:\"%d أيام\",M:\"شهر\",MM:\"%d أشهر\",y:\"سنة\",yy:\"%d سنوات\"},week:{dow:0,doy:12}}),{1:\"1\",2:\"2\",3:\"3\",4:\"4\",5:\"5\",6:\"6\",7:\"7\",8:\"8\",9:\"9\",0:\"0\"}),Cn={s:[\"أقل من ثانية\",\"ثانية واحدة\",[\"ثانيتان\",\"ثانيتين\"],\"%d ثوان\",\"%d ثانية\",\"%d ثانية\"],m:[\"أقل من دقيقة\",\"دقيقة واحدة\",[\"دقيقتان\",\"دقيقتين\"],\"%d دقائق\",\"%d دقيقة\",\"%d دقيقة\"],h:[\"أقل من ساعة\",\"ساعة واحدة\",[\"ساعتان\",\"ساعتين\"],\"%d ساعات\",\"%d ساعة\",\"%d ساعة\"],d:[\"أقل من يوم\",\"يوم واحد\",[\"يومان\",\"يومين\"],\"%d أيام\",\"%d يومًا\",\"%d يوم\"],M:[\"أقل من شهر\",\"شهر واحد\",[\"شهران\",\"شهرين\"],\"%d أشهر\",\"%d شهرا\",\"%d شهر\"],y:[\"أقل من عام\",\"عام واحد\",[\"عامان\",\"عامين\"],\"%d أعوام\",\"%d عامًا\",\"%d عام\"]},jn=($=[\"يناير\",\"فبراير\",\"مارس\",\"أبريل\",\"مايو\",\"يونيو\",\"يوليو\",\"أغسطس\",\"سبتمبر\",\"أكتوبر\",\"نوفمبر\",\"ديسمبر\"],t.defineLocale(\"ar-ly\",{months:$,monthsShort:$,weekdays:\"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),weekdaysShort:\"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت\".split(\"_\"),weekdaysMin:\"ح_ن_ث_ر_خ_ج_س\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"D/‏M/‏YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},meridiemParse:/\\u0635|\\u0645/,isPM:function(e){return\"م\"===e},meridiem:function(e,t,n){return e<12?\"ص\":\"م\"},calendar:{sameDay:\"[اليوم عند الساعة] LT\",nextDay:\"[غدًا عند الساعة] LT\",nextWeek:\"dddd [عند الساعة] LT\",lastDay:\"[أمس عند الساعة] LT\",lastWeek:\"dddd [عند الساعة] LT\",sameElse:\"L\"},relativeTime:{future:\"بعد %s\",past:\"منذ %s\",s:Dn(\"s\"),ss:Dn(\"s\"),m:Dn(\"m\"),mm:Dn(\"m\"),h:Dn(\"h\"),hh:Dn(\"h\"),d:Dn(\"d\"),dd:Dn(\"d\"),M:Dn(\"M\"),MM:Dn(\"M\"),y:Dn(\"y\"),yy:Dn(\"y\")},preparse:function(e){return e.replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/\\d/g,function(e){return On[e]}).replace(/,/g,\"،\")},week:{dow:6,doy:12}}),t.defineLocale(\"ar-ma\",{months:\"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر\".split(\"_\"),monthsShort:\"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر\".split(\"_\"),weekdays:\"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),weekdaysShort:\"احد_اثنين_ثلاثاء_اربعاء_خميس_جمعة_سبت\".split(\"_\"),weekdaysMin:\"ح_ن_ث_ر_خ_ج_س\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[اليوم على الساعة] LT\",nextDay:\"[غدا على الساعة] LT\",nextWeek:\"dddd [على الساعة] LT\",lastDay:\"[أمس على الساعة] LT\",lastWeek:\"dddd [على الساعة] LT\",sameElse:\"L\"},relativeTime:{future:\"في %s\",past:\"منذ %s\",s:\"ثوان\",ss:\"%d ثانية\",m:\"دقيقة\",mm:\"%d دقائق\",h:\"ساعة\",hh:\"%d ساعات\",d:\"يوم\",dd:\"%d أيام\",M:\"شهر\",MM:\"%d أشهر\",y:\"سنة\",yy:\"%d سنوات\"},week:{dow:1,doy:4}}),{1:\"١\",2:\"٢\",3:\"٣\",4:\"٤\",5:\"٥\",6:\"٦\",7:\"٧\",8:\"٨\",9:\"٩\",0:\"٠\"}),Pn={\"١\":\"1\",\"٢\":\"2\",\"٣\":\"3\",\"٤\":\"4\",\"٥\":\"5\",\"٦\":\"6\",\"٧\":\"7\",\"٨\":\"8\",\"٩\":\"9\",\"٠\":\"0\"},Hn=(t.defineLocale(\"ar-ps\",{months:\"كانون الثاني_شباط_آذار_نيسان_أيّار_حزيران_تمّوز_آب_أيلول_تشري الأوّل_تشرين الثاني_كانون الأوّل\".split(\"_\"),monthsShort:\"ك٢_شباط_آذار_نيسان_أيّار_حزيران_تمّوز_آب_أيلول_ت١_ت٢_ك١\".split(\"_\"),weekdays:\"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),weekdaysShort:\"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت\".split(\"_\"),weekdaysMin:\"ح_ن_ث_ر_خ_ج_س\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},meridiemParse:/\\u0635|\\u0645/,isPM:function(e){return\"م\"===e},meridiem:function(e,t,n){return e<12?\"ص\":\"م\"},calendar:{sameDay:\"[اليوم على الساعة] LT\",nextDay:\"[غدا على الساعة] LT\",nextWeek:\"dddd [على الساعة] LT\",lastDay:\"[أمس على الساعة] LT\",lastWeek:\"dddd [على الساعة] LT\",sameElse:\"L\"},relativeTime:{future:\"في %s\",past:\"منذ %s\",s:\"ثوان\",ss:\"%d ثانية\",m:\"دقيقة\",mm:\"%d دقائق\",h:\"ساعة\",hh:\"%d ساعات\",d:\"يوم\",dd:\"%d أيام\",M:\"شهر\",MM:\"%d أشهر\",y:\"سنة\",yy:\"%d سنوات\"},preparse:function(e){return e.replace(/[\\u0663\\u0664\\u0665\\u0666\\u0667\\u0668\\u0669\\u0660]/g,function(e){return Pn[e]}).split(\"\").reverse().join(\"\").replace(/[\\u0661\\u0662](?![\\u062a\\u0643])/g,function(e){return Pn[e]}).split(\"\").reverse().join(\"\").replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/\\d/g,function(e){return jn[e]}).replace(/,/g,\"،\")},week:{dow:0,doy:6}}),{1:\"١\",2:\"٢\",3:\"٣\",4:\"٤\",5:\"٥\",6:\"٦\",7:\"٧\",8:\"٨\",9:\"٩\",0:\"٠\"}),In={\"١\":\"1\",\"٢\":\"2\",\"٣\":\"3\",\"٤\":\"4\",\"٥\":\"5\",\"٦\":\"6\",\"٧\":\"7\",\"٨\":\"8\",\"٩\":\"9\",\"٠\":\"0\"},Fn=(t.defineLocale(\"ar-sa\",{months:\"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر\".split(\"_\"),monthsShort:\"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر\".split(\"_\"),weekdays:\"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),weekdaysShort:\"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت\".split(\"_\"),weekdaysMin:\"ح_ن_ث_ر_خ_ج_س\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},meridiemParse:/\\u0635|\\u0645/,isPM:function(e){return\"م\"===e},meridiem:function(e,t,n){return e<12?\"ص\":\"م\"},calendar:{sameDay:\"[اليوم على الساعة] LT\",nextDay:\"[غدا على الساعة] LT\",nextWeek:\"dddd [على الساعة] LT\",lastDay:\"[أمس على الساعة] LT\",lastWeek:\"dddd [على الساعة] LT\",sameElse:\"L\"},relativeTime:{future:\"في %s\",past:\"منذ %s\",s:\"ثوان\",ss:\"%d ثانية\",m:\"دقيقة\",mm:\"%d دقائق\",h:\"ساعة\",hh:\"%d ساعات\",d:\"يوم\",dd:\"%d أيام\",M:\"شهر\",MM:\"%d أشهر\",y:\"سنة\",yy:\"%d سنوات\"},preparse:function(e){return e.replace(/[\\u0661\\u0662\\u0663\\u0664\\u0665\\u0666\\u0667\\u0668\\u0669\\u0660]/g,function(e){return In[e]}).replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/\\d/g,function(e){return Hn[e]}).replace(/,/g,\"،\")},week:{dow:0,doy:6}}),t.defineLocale(\"ar-tn\",{months:\"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر\".split(\"_\"),monthsShort:\"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر\".split(\"_\"),weekdays:\"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),weekdaysShort:\"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت\".split(\"_\"),weekdaysMin:\"ح_ن_ث_ر_خ_ج_س\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[اليوم على الساعة] LT\",nextDay:\"[غدا على الساعة] LT\",nextWeek:\"dddd [على الساعة] LT\",lastDay:\"[أمس على الساعة] LT\",lastWeek:\"dddd [على الساعة] LT\",sameElse:\"L\"},relativeTime:{future:\"في %s\",past:\"منذ %s\",s:\"ثوان\",ss:\"%d ثانية\",m:\"دقيقة\",mm:\"%d دقائق\",h:\"ساعة\",hh:\"%d ساعات\",d:\"يوم\",dd:\"%d أيام\",M:\"شهر\",MM:\"%d أشهر\",y:\"سنة\",yy:\"%d سنوات\"},week:{dow:1,doy:4}}),{1:\"١\",2:\"٢\",3:\"٣\",4:\"٤\",5:\"٥\",6:\"٦\",7:\"٧\",8:\"٨\",9:\"٩\",0:\"٠\"}),Nn={\"١\":\"1\",\"٢\":\"2\",\"٣\":\"3\",\"٤\":\"4\",\"٥\":\"5\",\"٦\":\"6\",\"٧\":\"7\",\"٨\":\"8\",\"٩\":\"9\",\"٠\":\"0\"},Rn={s:[\"أقل من ثانية\",\"ثانية واحدة\",[\"ثانيتان\",\"ثانيتين\"],\"%d ثوان\",\"%d ثانية\",\"%d ثانية\"],m:[\"أقل من دقيقة\",\"دقيقة واحدة\",[\"دقيقتان\",\"دقيقتين\"],\"%d دقائق\",\"%d دقيقة\",\"%d دقيقة\"],h:[\"أقل من ساعة\",\"ساعة واحدة\",[\"ساعتان\",\"ساعتين\"],\"%d ساعات\",\"%d ساعة\",\"%d ساعة\"],d:[\"أقل من يوم\",\"يوم واحد\",[\"يومان\",\"يومين\"],\"%d أيام\",\"%d يومًا\",\"%d يوم\"],M:[\"أقل من شهر\",\"شهر واحد\",[\"شهران\",\"شهرين\"],\"%d أشهر\",\"%d شهرا\",\"%d شهر\"],y:[\"أقل من عام\",\"عام واحد\",[\"عامان\",\"عامين\"],\"%d أعوام\",\"%d عامًا\",\"%d عام\"]},Vn=(J=[\"يناير\",\"فبراير\",\"مارس\",\"أبريل\",\"مايو\",\"يونيو\",\"يوليو\",\"أغسطس\",\"سبتمبر\",\"أكتوبر\",\"نوفمبر\",\"ديسمبر\"],t.defineLocale(\"ar\",{months:J,monthsShort:J,weekdays:\"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),weekdaysShort:\"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت\".split(\"_\"),weekdaysMin:\"ح_ن_ث_ر_خ_ج_س\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"D/‏M/‏YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},meridiemParse:/\\u0635|\\u0645/,isPM:function(e){return\"م\"===e},meridiem:function(e,t,n){return e<12?\"ص\":\"م\"},calendar:{sameDay:\"[اليوم عند الساعة] LT\",nextDay:\"[غدًا عند الساعة] LT\",nextWeek:\"dddd [عند الساعة] LT\",lastDay:\"[أمس عند الساعة] LT\",lastWeek:\"dddd [عند الساعة] LT\",sameElse:\"L\"},relativeTime:{future:\"بعد %s\",past:\"منذ %s\",s:Yn(\"s\"),ss:Yn(\"s\"),m:Yn(\"m\"),mm:Yn(\"m\"),h:Yn(\"h\"),hh:Yn(\"h\"),d:Yn(\"d\"),dd:Yn(\"d\"),M:Yn(\"M\"),MM:Yn(\"M\"),y:Yn(\"y\"),yy:Yn(\"y\")},preparse:function(e){return e.replace(/[\\u0661\\u0662\\u0663\\u0664\\u0665\\u0666\\u0667\\u0668\\u0669\\u0660]/g,function(e){return Nn[e]}).replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/\\d/g,function(e){return Fn[e]}).replace(/,/g,\"،\")},week:{dow:6,doy:12}}),{1:\"-inci\",5:\"-inci\",8:\"-inci\",70:\"-inci\",80:\"-inci\",2:\"-nci\",7:\"-nci\",20:\"-nci\",50:\"-nci\",3:\"-üncü\",4:\"-üncü\",100:\"-üncü\",6:\"-ncı\",9:\"-uncu\",10:\"-uncu\",30:\"-uncu\",60:\"-ıncı\",90:\"-ıncı\"});function $n(e,t,n){return\"m\"===n?t?\"хвіліна\":\"хвіліну\":\"h\"===n?t?\"гадзіна\":\"гадзіну\":e+\" \"+(e=+e,t=(t={ss:t?\"секунда_секунды_секунд\":\"секунду_секунды_секунд\",mm:t?\"хвіліна_хвіліны_хвілін\":\"хвіліну_хвіліны_хвілін\",hh:t?\"гадзіна_гадзіны_гадзін\":\"гадзіну_гадзіны_гадзін\",dd:\"дзень_дні_дзён\",MM:\"месяц_месяцы_месяцаў\",yy:\"год_гады_гадоў\"}[n]).split(\"_\"),e%10==1&&e%100!=11?t[0]:2<=e%10&&e%10<=4&&(e%100<10||20<=e%100)?t[1]:t[2])}t.defineLocale(\"az\",{months:\"yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr\".split(\"_\"),monthsShort:\"yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek\".split(\"_\"),weekdays:\"Bazar_Bazar ertəsi_Çərşənbə axşamı_Çərşənbə_Cümə axşamı_Cümə_Şənbə\".split(\"_\"),weekdaysShort:\"Baz_BzE_ÇAx_Çər_CAx_Cüm_Şən\".split(\"_\"),weekdaysMin:\"Bz_BE_ÇA_Çə_CA_Cü_Şə\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[bugün saat] LT\",nextDay:\"[sabah saat] LT\",nextWeek:\"[gələn həftə] dddd [saat] LT\",lastDay:\"[dünən] LT\",lastWeek:\"[keçən həftə] dddd [saat] LT\",sameElse:\"L\"},relativeTime:{future:\"%s sonra\",past:\"%s əvvəl\",s:\"bir neçə saniyə\",ss:\"%d saniyə\",m:\"bir dəqiqə\",mm:\"%d dəqiqə\",h:\"bir saat\",hh:\"%d saat\",d:\"bir gün\",dd:\"%d gün\",M:\"bir ay\",MM:\"%d ay\",y:\"bir il\",yy:\"%d il\"},meridiemParse:/gec\\u0259|s\\u0259h\\u0259r|g\\xfcnd\\xfcz|ax\\u015fam/,isPM:function(e){return/^(g\\xfcnd\\xfcz|ax\\u015fam)$/.test(e)},meridiem:function(e,t,n){return e<4?\"gecə\":e<12?\"səhər\":e<17?\"gündüz\":\"axşam\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0131nc\\u0131|inci|nci|\\xfcnc\\xfc|nc\\u0131|uncu)/,ordinal:function(e){var t;return 0===e?e+\"-ıncı\":e+(Vn[t=e%10]||Vn[e%100-t]||Vn[100<=e?100:null])},week:{dow:1,doy:7}}),t.defineLocale(\"be\",{months:{format:\"студзеня_лютага_сакавіка_красавіка_траўня_чэрвеня_ліпеня_жніўня_верасня_кастрычніка_лістапада_снежня\".split(\"_\"),standalone:\"студзень_люты_сакавік_красавік_травень_чэрвень_ліпень_жнівень_верасень_кастрычнік_лістапад_снежань\".split(\"_\")},monthsShort:\"студ_лют_сак_крас_трав_чэрв_ліп_жнів_вер_каст_ліст_снеж\".split(\"_\"),weekdays:{format:\"нядзелю_панядзелак_аўторак_сераду_чацвер_пятніцу_суботу\".split(\"_\"),standalone:\"нядзеля_панядзелак_аўторак_серада_чацвер_пятніца_субота\".split(\"_\"),isFormat:/\\[ ?[\\u0423\\u0443\\u045e] ?(?:\\u043c\\u0456\\u043d\\u0443\\u043b\\u0443\\u044e|\\u043d\\u0430\\u0441\\u0442\\u0443\\u043f\\u043d\\u0443\\u044e)? ?\\] ?dddd/},weekdaysShort:\"нд_пн_ат_ср_чц_пт_сб\".split(\"_\"),weekdaysMin:\"нд_пн_ат_ср_чц_пт_сб\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY г.\",LLL:\"D MMMM YYYY г., HH:mm\",LLLL:\"dddd, D MMMM YYYY г., HH:mm\"},calendar:{sameDay:\"[Сёння ў] LT\",nextDay:\"[Заўтра ў] LT\",lastDay:\"[Учора ў] LT\",nextWeek:function(){return\"[У] dddd [ў] LT\"},lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return\"[У мінулую] dddd [ў] LT\";case 1:case 2:case 4:return\"[У мінулы] dddd [ў] LT\"}},sameElse:\"L\"},relativeTime:{future:\"праз %s\",past:\"%s таму\",s:\"некалькі секунд\",m:$n,mm:$n,h:$n,hh:$n,d:\"дзень\",dd:$n,M:\"месяц\",MM:$n,y:\"год\",yy:$n},meridiemParse:/\\u043d\\u043e\\u0447\\u044b|\\u0440\\u0430\\u043d\\u0456\\u0446\\u044b|\\u0434\\u043d\\u044f|\\u0432\\u0435\\u0447\\u0430\\u0440\\u0430/,isPM:function(e){return/^(\\u0434\\u043d\\u044f|\\u0432\\u0435\\u0447\\u0430\\u0440\\u0430)$/.test(e)},meridiem:function(e,t,n){return e<4?\"ночы\":e<12?\"раніцы\":e<17?\"дня\":\"вечара\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0456|\\u044b|\\u0433\\u0430)/,ordinal:function(e,t){switch(t){case\"M\":case\"d\":case\"DDD\":case\"w\":case\"W\":return e%10!=2&&e%10!=3||e%100==12||e%100==13?e+\"-ы\":e+\"-і\";case\"D\":return e+\"-га\";default:return e}},week:{dow:1,doy:7}}),t.defineLocale(\"bg\",{months:\"януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември\".split(\"_\"),monthsShort:\"яну_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек\".split(\"_\"),weekdays:\"неделя_понеделник_вторник_сряда_четвъртък_петък_събота\".split(\"_\"),weekdaysShort:\"нед_пон_вто_сря_чет_пет_съб\".split(\"_\"),weekdaysMin:\"нд_пн_вт_ср_чт_пт_сб\".split(\"_\"),longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"D.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY H:mm\",LLLL:\"dddd, D MMMM YYYY H:mm\"},calendar:{sameDay:\"[Днес в] LT\",nextDay:\"[Утре в] LT\",nextWeek:\"dddd [в] LT\",lastDay:\"[Вчера в] LT\",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return\"[Миналата] dddd [в] LT\";case 1:case 2:case 4:case 5:return\"[Миналия] dddd [в] LT\"}},sameElse:\"L\"},relativeTime:{future:\"след %s\",past:\"преди %s\",s:\"няколко секунди\",ss:\"%d секунди\",m:\"минута\",mm:\"%d минути\",h:\"час\",hh:\"%d часа\",d:\"ден\",dd:\"%d дена\",w:\"седмица\",ww:\"%d седмици\",M:\"месец\",MM:\"%d месеца\",y:\"година\",yy:\"%d години\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0435\\u0432|\\u0435\\u043d|\\u0442\\u0438|\\u0432\\u0438|\\u0440\\u0438|\\u043c\\u0438)/,ordinal:function(e){var t=e%10,n=e%100;return 0===e?e+\"-ев\":0==n?e+\"-ен\":10<n&&n<20?e+\"-ти\":1==t?e+\"-ви\":2==t?e+\"-ри\":7==t||8==t?e+\"-ми\":e+\"-ти\"},week:{dow:1,doy:7}}),t.defineLocale(\"bm\",{months:\"Zanwuyekalo_Fewuruyekalo_Marisikalo_Awirilikalo_Mɛkalo_Zuwɛnkalo_Zuluyekalo_Utikalo_Sɛtanburukalo_ɔkutɔburukalo_Nowanburukalo_Desanburukalo\".split(\"_\"),monthsShort:\"Zan_Few_Mar_Awi_Mɛ_Zuw_Zul_Uti_Sɛt_ɔku_Now_Des\".split(\"_\"),weekdays:\"Kari_Ntɛnɛn_Tarata_Araba_Alamisa_Juma_Sibiri\".split(\"_\"),weekdaysShort:\"Kar_Ntɛ_Tar_Ara_Ala_Jum_Sib\".split(\"_\"),weekdaysMin:\"Ka_Nt_Ta_Ar_Al_Ju_Si\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"MMMM [tile] D [san] YYYY\",LLL:\"MMMM [tile] D [san] YYYY [lɛrɛ] HH:mm\",LLLL:\"dddd MMMM [tile] D [san] YYYY [lɛrɛ] HH:mm\"},calendar:{sameDay:\"[Bi lɛrɛ] LT\",nextDay:\"[Sini lɛrɛ] LT\",nextWeek:\"dddd [don lɛrɛ] LT\",lastDay:\"[Kunu lɛrɛ] LT\",lastWeek:\"dddd [tɛmɛnen lɛrɛ] LT\",sameElse:\"L\"},relativeTime:{future:\"%s kɔnɔ\",past:\"a bɛ %s bɔ\",s:\"sanga dama dama\",ss:\"sekondi %d\",m:\"miniti kelen\",mm:\"miniti %d\",h:\"lɛrɛ kelen\",hh:\"lɛrɛ %d\",d:\"tile kelen\",dd:\"tile %d\",M:\"kalo kelen\",MM:\"kalo %d\",y:\"san kelen\",yy:\"san %d\"},week:{dow:1,doy:4}});var zn={1:\"১\",2:\"২\",3:\"৩\",4:\"৪\",5:\"৫\",6:\"৬\",7:\"৭\",8:\"৮\",9:\"৯\",0:\"০\"},Wn={\"১\":\"1\",\"২\":\"2\",\"৩\":\"3\",\"৪\":\"4\",\"৫\":\"5\",\"৬\":\"6\",\"৭\":\"7\",\"৮\":\"8\",\"৯\":\"9\",\"০\":\"0\"},Un=(t.defineLocale(\"bn-bd\",{months:\"জানুয়ারি_ফেব্রুয়ারি_মার্চ_এপ্রিল_মে_জুন_জুলাই_আগস্ট_সেপ্টেম্বর_অক্টোবর_নভেম্বর_ডিসেম্বর\".split(\"_\"),monthsShort:\"জানু_ফেব্রু_মার্চ_এপ্রিল_মে_জুন_জুলাই_আগস্ট_সেপ্ট_অক্টো_নভে_ডিসে\".split(\"_\"),weekdays:\"রবিবার_সোমবার_মঙ্গলবার_বুধবার_বৃহস্পতিবার_শুক্রবার_শনিবার\".split(\"_\"),weekdaysShort:\"রবি_সোম_মঙ্গল_বুধ_বৃহস্পতি_শুক্র_শনি\".split(\"_\"),weekdaysMin:\"রবি_সোম_মঙ্গল_বুধ_বৃহ_শুক্র_শনি\".split(\"_\"),longDateFormat:{LT:\"A h:mm সময়\",LTS:\"A h:mm:ss সময়\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm সময়\",LLLL:\"dddd, D MMMM YYYY, A h:mm সময়\"},calendar:{sameDay:\"[আজ] LT\",nextDay:\"[আগামীকাল] LT\",nextWeek:\"dddd, LT\",lastDay:\"[গতকাল] LT\",lastWeek:\"[গত] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s পরে\",past:\"%s আগে\",s:\"কয়েক সেকেন্ড\",ss:\"%d সেকেন্ড\",m:\"এক মিনিট\",mm:\"%d মিনিট\",h:\"এক ঘন্টা\",hh:\"%d ঘন্টা\",d:\"এক দিন\",dd:\"%d দিন\",M:\"এক মাস\",MM:\"%d মাস\",y:\"এক বছর\",yy:\"%d বছর\"},preparse:function(e){return e.replace(/[\\u09e7\\u09e8\\u09e9\\u09ea\\u09eb\\u09ec\\u09ed\\u09ee\\u09ef\\u09e6]/g,function(e){return Wn[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return zn[e]})},meridiemParse:/\\u09b0\\u09be\\u09a4|\\u09ad\\u09cb\\u09b0|\\u09b8\\u0995\\u09be\\u09b2|\\u09a6\\u09c1\\u09aa\\u09c1\\u09b0|\\u09ac\\u09bf\\u0995\\u09be\\u09b2|\\u09b8\\u09a8\\u09cd\\u09a7\\u09cd\\u09af\\u09be|\\u09b0\\u09be\\u09a4/,meridiemHour:function(e,t){return 12===e&&(e=0),\"রাত\"===t?e<4?e:e+12:\"ভোর\"===t||\"সকাল\"===t?e:\"দুপুর\"===t?3<=e?e:e+12:\"বিকাল\"===t||\"সন্ধ্যা\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"রাত\":e<6?\"ভোর\":e<12?\"সকাল\":e<15?\"দুপুর\":e<18?\"বিকাল\":e<20?\"সন্ধ্যা\":\"রাত\"},week:{dow:0,doy:6}}),{1:\"১\",2:\"২\",3:\"৩\",4:\"৪\",5:\"৫\",6:\"৬\",7:\"৭\",8:\"৮\",9:\"৯\",0:\"০\"}),Bn={\"১\":\"1\",\"২\":\"2\",\"৩\":\"3\",\"৪\":\"4\",\"৫\":\"5\",\"৬\":\"6\",\"৭\":\"7\",\"৮\":\"8\",\"৯\":\"9\",\"০\":\"0\"},qn=(t.defineLocale(\"bn\",{months:\"জানুয়ারি_ফেব্রুয়ারি_মার্চ_এপ্রিল_মে_জুন_জুলাই_আগস্ট_সেপ্টেম্বর_অক্টোবর_নভেম্বর_ডিসেম্বর\".split(\"_\"),monthsShort:\"জানু_ফেব্রু_মার্চ_এপ্রিল_মে_জুন_জুলাই_আগস্ট_সেপ্ট_অক্টো_নভে_ডিসে\".split(\"_\"),weekdays:\"রবিবার_সোমবার_মঙ্গলবার_বুধবার_বৃহস্পতিবার_শুক্রবার_শনিবার\".split(\"_\"),weekdaysShort:\"রবি_সোম_মঙ্গল_বুধ_বৃহস্পতি_শুক্র_শনি\".split(\"_\"),weekdaysMin:\"রবি_সোম_মঙ্গল_বুধ_বৃহ_শুক্র_শনি\".split(\"_\"),longDateFormat:{LT:\"A h:mm সময়\",LTS:\"A h:mm:ss সময়\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm সময়\",LLLL:\"dddd, D MMMM YYYY, A h:mm সময়\"},calendar:{sameDay:\"[আজ] LT\",nextDay:\"[আগামীকাল] LT\",nextWeek:\"dddd, LT\",lastDay:\"[গতকাল] LT\",lastWeek:\"[গত] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s পরে\",past:\"%s আগে\",s:\"কয়েক সেকেন্ড\",ss:\"%d সেকেন্ড\",m:\"এক মিনিট\",mm:\"%d মিনিট\",h:\"এক ঘন্টা\",hh:\"%d ঘন্টা\",d:\"এক দিন\",dd:\"%d দিন\",M:\"এক মাস\",MM:\"%d মাস\",y:\"এক বছর\",yy:\"%d বছর\"},preparse:function(e){return e.replace(/[\\u09e7\\u09e8\\u09e9\\u09ea\\u09eb\\u09ec\\u09ed\\u09ee\\u09ef\\u09e6]/g,function(e){return Bn[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return Un[e]})},meridiemParse:/\\u09b0\\u09be\\u09a4|\\u09b8\\u0995\\u09be\\u09b2|\\u09a6\\u09c1\\u09aa\\u09c1\\u09b0|\\u09ac\\u09bf\\u0995\\u09be\\u09b2|\\u09b0\\u09be\\u09a4/,meridiemHour:function(e,t){return 12===e&&(e=0),\"রাত\"===t&&4<=e||\"দুপুর\"===t&&e<5||\"বিকাল\"===t?e+12:e},meridiem:function(e,t,n){return e<4?\"রাত\":e<10?\"সকাল\":e<17?\"দুপুর\":e<20?\"বিকাল\":\"রাত\"},week:{dow:0,doy:6}}),{1:\"༡\",2:\"༢\",3:\"༣\",4:\"༤\",5:\"༥\",6:\"༦\",7:\"༧\",8:\"༨\",9:\"༩\",0:\"༠\"}),Gn={\"༡\":\"1\",\"༢\":\"2\",\"༣\":\"3\",\"༤\":\"4\",\"༥\":\"5\",\"༦\":\"6\",\"༧\":\"7\",\"༨\":\"8\",\"༩\":\"9\",\"༠\":\"0\"};function Jn(e,t,n){return e+\" \"+(n={mm:\"munutenn\",MM:\"miz\",dd:\"devezh\"}[n],2!==e?n:void 0!==(e={m:\"v\",b:\"v\",d:\"z\"})[n.charAt(0)]?e[n.charAt(0)]+n.substring(1):n)}function Zn(e,t,n){var r=e+\" \";switch(n){case\"ss\":return r+(1===e?\"sekunda\":2===e||3===e||4===e?\"sekunde\":\"sekundi\");case\"mm\":return r+(1===e||2!==e&&3!==e&&4!==e?\"minuta\":\"minute\");case\"h\":return\"jedan sat\";case\"hh\":return r+(1===e?\"sat\":2===e||3===e||4===e?\"sata\":\"sati\");case\"dd\":return r+(1===e?\"dan\":\"dana\");case\"MM\":return r+(1===e?\"mjesec\":2===e||3===e||4===e?\"mjeseca\":\"mjeseci\");case\"yy\":return r+(1===e||2!==e&&3!==e&&4!==e?\"godina\":\"godine\")}}function Kn(e){return 1<e&&e<5&&1!=~~(e/10)}function Xn(e,t,n,r){var i=e+\" \";switch(n){case\"s\":return t||r?\"pár sekund\":\"pár sekundami\";case\"ss\":return t||r?i+(Kn(e)?\"sekundy\":\"sekund\"):i+\"sekundami\";case\"m\":return t?\"minuta\":r?\"minutu\":\"minutou\";case\"mm\":return t||r?i+(Kn(e)?\"minuty\":\"minut\"):i+\"minutami\";case\"h\":return t?\"hodina\":r?\"hodinu\":\"hodinou\";case\"hh\":return t||r?i+(Kn(e)?\"hodiny\":\"hodin\"):i+\"hodinami\";case\"d\":return t||r?\"den\":\"dnem\";case\"dd\":return t||r?i+(Kn(e)?\"dny\":\"dní\"):i+\"dny\";case\"M\":return t||r?\"měsíc\":\"měsícem\";case\"MM\":return t||r?i+(Kn(e)?\"měsíce\":\"měsíců\"):i+\"měsíci\";case\"y\":return t||r?\"rok\":\"rokem\";case\"yy\":return t||r?i+(Kn(e)?\"roky\":\"let\"):i+\"lety\"}}function Qn(e,t,n,r){return e={m:[\"eine Minute\",\"einer Minute\"],h:[\"eine Stunde\",\"einer Stunde\"],d:[\"ein Tag\",\"einem Tag\"],dd:[e+\" Tage\",e+\" Tagen\"],w:[\"eine Woche\",\"einer Woche\"],M:[\"ein Monat\",\"einem Monat\"],MM:[e+\" Monate\",e+\" Monaten\"],y:[\"ein Jahr\",\"einem Jahr\"],yy:[e+\" Jahre\",e+\" Jahren\"]},t?e[n][0]:e[n][1]}function er(e,t,n,r){return e={m:[\"eine Minute\",\"einer Minute\"],h:[\"eine Stunde\",\"einer Stunde\"],d:[\"ein Tag\",\"einem Tag\"],dd:[e+\" Tage\",e+\" Tagen\"],w:[\"eine Woche\",\"einer Woche\"],M:[\"ein Monat\",\"einem Monat\"],MM:[e+\" Monate\",e+\" Monaten\"],y:[\"ein Jahr\",\"einem Jahr\"],yy:[e+\" Jahre\",e+\" Jahren\"]},t?e[n][0]:e[n][1]}function tr(e,t,n,r){return e={m:[\"eine Minute\",\"einer Minute\"],h:[\"eine Stunde\",\"einer Stunde\"],d:[\"ein Tag\",\"einem Tag\"],dd:[e+\" Tage\",e+\" Tagen\"],w:[\"eine Woche\",\"einer Woche\"],M:[\"ein Monat\",\"einem Monat\"],MM:[e+\" Monate\",e+\" Monaten\"],y:[\"ein Jahr\",\"einem Jahr\"],yy:[e+\" Jahre\",e+\" Jahren\"]},t?e[n][0]:e[n][1]}t.defineLocale(\"bo\",{months:\"ཟླ་བ་དང་པོ_ཟླ་བ་གཉིས་པ_ཟླ་བ་གསུམ་པ_ཟླ་བ་བཞི་པ_ཟླ་བ་ལྔ་པ_ཟླ་བ་དྲུག་པ_ཟླ་བ་བདུན་པ_ཟླ་བ་བརྒྱད་པ_ཟླ་བ་དགུ་པ_ཟླ་བ་བཅུ་པ_ཟླ་བ་བཅུ་གཅིག་པ_ཟླ་བ་བཅུ་གཉིས་པ\".split(\"_\"),monthsShort:\"ཟླ་1_ཟླ་2_ཟླ་3_ཟླ་4_ཟླ་5_ཟླ་6_ཟླ་7_ཟླ་8_ཟླ་9_ཟླ་10_ཟླ་11_ཟླ་12\".split(\"_\"),monthsShortRegex:/^(\\u0f5f\\u0fb3\\u0f0b\\d{1,2})/,monthsParseExact:!0,weekdays:\"གཟའ་ཉི་མ་_གཟའ་ཟླ་བ་_གཟའ་མིག་དམར་_གཟའ་ལྷག་པ་_གཟའ་ཕུར་བུ_གཟའ་པ་སངས་_གཟའ་སྤེན་པ་\".split(\"_\"),weekdaysShort:\"ཉི་མ་_ཟླ་བ་_མིག་དམར་_ལྷག་པ་_ཕུར་བུ_པ་སངས་_སྤེན་པ་\".split(\"_\"),weekdaysMin:\"ཉི_ཟླ_མིག_ལྷག_ཕུར_སངས_སྤེན\".split(\"_\"),longDateFormat:{LT:\"A h:mm\",LTS:\"A h:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm\",LLLL:\"dddd, D MMMM YYYY, A h:mm\"},calendar:{sameDay:\"[དི་རིང] LT\",nextDay:\"[སང་ཉིན] LT\",nextWeek:\"[བདུན་ཕྲག་རྗེས་མ], LT\",lastDay:\"[ཁ་སང] LT\",lastWeek:\"[བདུན་ཕྲག་མཐའ་མ] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s ལ་\",past:\"%s སྔན་ལ\",s:\"ལམ་སང\",ss:\"%d སྐར་ཆ།\",m:\"སྐར་མ་གཅིག\",mm:\"%d སྐར་མ\",h:\"ཆུ་ཚོད་གཅིག\",hh:\"%d ཆུ་ཚོད\",d:\"ཉིན་གཅིག\",dd:\"%d ཉིན་\",M:\"ཟླ་བ་གཅིག\",MM:\"%d ཟླ་བ\",y:\"ལོ་གཅིག\",yy:\"%d ལོ\"},preparse:function(e){return e.replace(/[\\u0f21\\u0f22\\u0f23\\u0f24\\u0f25\\u0f26\\u0f27\\u0f28\\u0f29\\u0f20]/g,function(e){return Gn[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return qn[e]})},meridiemParse:/\\u0f58\\u0f5a\\u0f53\\u0f0b\\u0f58\\u0f7c|\\u0f5e\\u0f7c\\u0f42\\u0f66\\u0f0b\\u0f40\\u0f66|\\u0f49\\u0f72\\u0f53\\u0f0b\\u0f42\\u0f74\\u0f44|\\u0f51\\u0f42\\u0f7c\\u0f44\\u0f0b\\u0f51\\u0f42|\\u0f58\\u0f5a\\u0f53\\u0f0b\\u0f58\\u0f7c/,meridiemHour:function(e,t){return 12===e&&(e=0),\"མཚན་མོ\"===t&&4<=e||\"ཉིན་གུང\"===t&&e<5||\"དགོང་དག\"===t?e+12:e},meridiem:function(e,t,n){return e<4?\"མཚན་མོ\":e<10?\"ཞོགས་ཀས\":e<17?\"ཉིན་གུང\":e<20?\"དགོང་དག\":\"མཚན་མོ\"},week:{dow:0,doy:6}}),z=[/^gen/i,/^c[\\u02bc\\']hwe/i,/^meu/i,/^ebr/i,/^mae/i,/^(mez|eve)/i,/^gou/i,/^eos/i,/^gwe/i,/^her/i,/^du/i,/^ker/i],Ae=/^(genver|c[\\u02bc\\']hwevrer|meurzh|ebrel|mae|mezheven|gouere|eost|gwengolo|here|du|kerzu|gen|c[\\u02bc\\']hwe|meu|ebr|mae|eve|gou|eos|gwe|her|du|ker)/i,G=[/^Su/i,/^Lu/i,/^Me([^r]|$)/i,/^Mer/i,/^Ya/i,/^Gw/i,/^Sa/i],t.defineLocale(\"br\",{months:\"Genver_Cʼhwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu\".split(\"_\"),monthsShort:\"Gen_Cʼhwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker\".split(\"_\"),weekdays:\"Sul_Lun_Meurzh_Mercʼher_Yaou_Gwener_Sadorn\".split(\"_\"),weekdaysShort:\"Sul_Lun_Meu_Mer_Yao_Gwe_Sad\".split(\"_\"),weekdaysMin:\"Su_Lu_Me_Mer_Ya_Gw_Sa\".split(\"_\"),weekdaysParse:G,fullWeekdaysParse:[/^sul/i,/^lun/i,/^meurzh/i,/^merc[\\u02bc\\']her/i,/^yaou/i,/^gwener/i,/^sadorn/i],shortWeekdaysParse:[/^Sul/i,/^Lun/i,/^Meu/i,/^Mer/i,/^Yao/i,/^Gwe/i,/^Sad/i],minWeekdaysParse:G,monthsRegex:Ae,monthsShortRegex:Ae,monthsStrictRegex:/^(genver|c[\\u02bc\\']hwevrer|meurzh|ebrel|mae|mezheven|gouere|eost|gwengolo|here|du|kerzu)/i,monthsShortStrictRegex:/^(gen|c[\\u02bc\\']hwe|meu|ebr|mae|eve|gou|eos|gwe|her|du|ker)/i,monthsParse:z,longMonthsParse:z,shortMonthsParse:z,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D [a viz] MMMM YYYY\",LLL:\"D [a viz] MMMM YYYY HH:mm\",LLLL:\"dddd, D [a viz] MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Hiziv da] LT\",nextDay:\"[Warcʼhoazh da] LT\",nextWeek:\"dddd [da] LT\",lastDay:\"[Decʼh da] LT\",lastWeek:\"dddd [paset da] LT\",sameElse:\"L\"},relativeTime:{future:\"a-benn %s\",past:\"%s ʼzo\",s:\"un nebeud segondennoù\",ss:\"%d eilenn\",m:\"ur vunutenn\",mm:Jn,h:\"un eur\",hh:\"%d eur\",d:\"un devezh\",dd:Jn,M:\"ur miz\",MM:Jn,y:\"ur bloaz\",yy:function(e){switch(function e(t){return 9<t?e(t%10):t}(e)){case 1:case 3:case 4:case 5:case 9:return e+\" bloaz\";default:return e+\" vloaz\"}}},dayOfMonthOrdinalParse:/\\d{1,2}(a\\xf1|vet)/,ordinal:function(e){return e+(1===e?\"añ\":\"vet\")},week:{dow:1,doy:4},meridiemParse:/a.m.|g.m./,isPM:function(e){return\"g.m.\"===e},meridiem:function(e,t,n){return e<12?\"a.m.\":\"g.m.\"}}),t.defineLocale(\"bs\",{months:\"januar_februar_mart_april_maj_juni_juli_august_septembar_oktobar_novembar_decembar\".split(\"_\"),monthsShort:\"jan._feb._mar._apr._maj._jun._jul._aug._sep._okt._nov._dec.\".split(\"_\"),monthsParseExact:!0,weekdays:\"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota\".split(\"_\"),weekdaysShort:\"ned._pon._uto._sri._čet._pet._sub.\".split(\"_\"),weekdaysMin:\"ne_po_ut_sr_če_pe_su\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY H:mm\",LLLL:\"dddd, D. MMMM YYYY H:mm\"},calendar:{sameDay:\"[danas u] LT\",nextDay:\"[sutra u] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[u] [nedjelju] [u] LT\";case 3:return\"[u] [srijedu] [u] LT\";case 6:return\"[u] [subotu] [u] LT\";case 1:case 2:case 4:case 5:return\"[u] dddd [u] LT\"}},lastDay:\"[jučer u] LT\",lastWeek:function(){switch(this.day()){case 0:case 3:return\"[prošlu] dddd [u] LT\";case 6:return\"[prošle] [subote] [u] LT\";case 1:case 2:case 4:case 5:return\"[prošli] dddd [u] LT\"}},sameElse:\"L\"},relativeTime:{future:\"za %s\",past:\"prije %s\",s:\"par sekundi\",ss:Zn,m:function(e,t,n,r){if(\"m\"===n)return t?\"jedna minuta\":r?\"jednu minutu\":\"jedne minute\"},mm:Zn,h:Zn,hh:Zn,d:\"dan\",dd:Zn,M:\"mjesec\",MM:Zn,y:\"godinu\",yy:Zn},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:7}}),t.defineLocale(\"ca\",{months:{standalone:\"gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre\".split(\"_\"),format:\"de gener_de febrer_de març_d'abril_de maig_de juny_de juliol_d'agost_de setembre_d'octubre_de novembre_de desembre\".split(\"_\"),isFormat:/D[oD]?(\\s)+MMMM/},monthsShort:\"gen._febr._març_abr._maig_juny_jul._ag._set._oct._nov._des.\".split(\"_\"),monthsParseExact:!0,weekdays:\"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte\".split(\"_\"),weekdaysShort:\"dg._dl._dt._dc._dj._dv._ds.\".split(\"_\"),weekdaysMin:\"dg_dl_dt_dc_dj_dv_ds\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM [de] YYYY\",ll:\"D MMM YYYY\",LLL:\"D MMMM [de] YYYY [a les] H:mm\",lll:\"D MMM YYYY, H:mm\",LLLL:\"dddd D MMMM [de] YYYY [a les] H:mm\",llll:\"ddd D MMM YYYY, H:mm\"},calendar:{sameDay:function(){return\"[avui a \"+(1!==this.hours()?\"les\":\"la\")+\"] LT\"},nextDay:function(){return\"[demà a \"+(1!==this.hours()?\"les\":\"la\")+\"] LT\"},nextWeek:function(){return\"dddd [a \"+(1!==this.hours()?\"les\":\"la\")+\"] LT\"},lastDay:function(){return\"[ahir a \"+(1!==this.hours()?\"les\":\"la\")+\"] LT\"},lastWeek:function(){return\"[el] dddd [passat a \"+(1!==this.hours()?\"les\":\"la\")+\"] LT\"},sameElse:\"L\"},relativeTime:{future:\"d'aquí %s\",past:\"fa %s\",s:\"uns segons\",ss:\"%d segons\",m:\"un minut\",mm:\"%d minuts\",h:\"una hora\",hh:\"%d hores\",d:\"un dia\",dd:\"%d dies\",M:\"un mes\",MM:\"%d mesos\",y:\"un any\",yy:\"%d anys\"},dayOfMonthOrdinalParse:/\\d{1,2}(r|n|t|\\xe8|a)/,ordinal:function(e,t){return e+(\"w\"!==t&&\"W\"!==t?1===e?\"r\":2===e?\"n\":3===e?\"r\":4===e?\"t\":\"è\":\"a\")},week:{dow:1,doy:4}}),q={standalone:\"leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec\".split(\"_\"),format:\"ledna_února_března_dubna_května_června_července_srpna_září_října_listopadu_prosince\".split(\"_\"),isFormat:/DD?[o.]?(\\[[^\\[\\]]*\\]|\\s)+MMMM/},Be=\"led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro\".split(\"_\"),ne=[/^led/i,/^\\xfano/i,/^b\\u0159e/i,/^dub/i,/^kv\\u011b/i,/^(\\u010dvn|\\u010derven$|\\u010dervna)/i,/^(\\u010dvc|\\u010dervenec|\\u010dervence)/i,/^srp/i,/^z\\xe1\\u0159/i,/^\\u0159\\xedj/i,/^lis/i,/^pro/i],W=/^(leden|\\xfanor|b\\u0159ezen|duben|kv\\u011bten|\\u010dervenec|\\u010dervence|\\u010derven|\\u010dervna|srpen|z\\xe1\\u0159\\xed|\\u0159\\xedjen|listopad|prosinec|led|\\xfano|b\\u0159e|dub|kv\\u011b|\\u010dvn|\\u010dvc|srp|z\\xe1\\u0159|\\u0159\\xedj|lis|pro)/i,t.defineLocale(\"cs\",{months:q,monthsShort:Be,monthsRegex:W,monthsShortRegex:W,monthsStrictRegex:/^(leden|ledna|\\xfanora|\\xfanor|b\\u0159ezen|b\\u0159ezna|duben|dubna|kv\\u011bten|kv\\u011btna|\\u010dervenec|\\u010dervence|\\u010derven|\\u010dervna|srpen|srpna|z\\xe1\\u0159\\xed|\\u0159\\xedjen|\\u0159\\xedjna|listopadu|listopad|prosinec|prosince)/i,monthsShortStrictRegex:/^(led|\\xfano|b\\u0159e|dub|kv\\u011b|\\u010dvn|\\u010dvc|srp|z\\xe1\\u0159|\\u0159\\xedj|lis|pro)/i,monthsParse:ne,longMonthsParse:ne,shortMonthsParse:ne,weekdays:\"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota\".split(\"_\"),weekdaysShort:\"ne_po_út_st_čt_pá_so\".split(\"_\"),weekdaysMin:\"ne_po_út_st_čt_pá_so\".split(\"_\"),longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY H:mm\",LLLL:\"dddd D. MMMM YYYY H:mm\",l:\"D. M. YYYY\"},calendar:{sameDay:\"[dnes v] LT\",nextDay:\"[zítra v] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[v neděli v] LT\";case 1:case 2:return\"[v] dddd [v] LT\";case 3:return\"[ve středu v] LT\";case 4:return\"[ve čtvrtek v] LT\";case 5:return\"[v pátek v] LT\";case 6:return\"[v sobotu v] LT\"}},lastDay:\"[včera v] LT\",lastWeek:function(){switch(this.day()){case 0:return\"[minulou neděli v] LT\";case 1:case 2:return\"[minulé] dddd [v] LT\";case 3:return\"[minulou středu v] LT\";case 4:case 5:return\"[minulý] dddd [v] LT\";case 6:return\"[minulou sobotu v] LT\"}},sameElse:\"L\"},relativeTime:{future:\"za %s\",past:\"před %s\",s:Xn,ss:Xn,m:Xn,mm:Xn,h:Xn,hh:Xn,d:Xn,dd:Xn,M:Xn,MM:Xn,y:Xn,yy:Xn},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"cv\",{months:\"кӑрлач_нарӑс_пуш_ака_май_ҫӗртме_утӑ_ҫурла_авӑн_юпа_чӳк_раштав\".split(\"_\"),monthsShort:\"кӑр_нар_пуш_ака_май_ҫӗр_утӑ_ҫур_авн_юпа_чӳк_раш\".split(\"_\"),weekdays:\"вырсарникун_тунтикун_ытларикун_юнкун_кӗҫнерникун_эрнекун_шӑматкун\".split(\"_\"),weekdaysShort:\"выр_тун_ытл_юн_кӗҫ_эрн_шӑм\".split(\"_\"),weekdaysMin:\"вр_тн_ыт_юн_кҫ_эр_шм\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD-MM-YYYY\",LL:\"YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ]\",LLL:\"YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ], HH:mm\",LLLL:\"dddd, YYYY [ҫулхи] MMMM [уйӑхӗн] D[-мӗшӗ], HH:mm\"},calendar:{sameDay:\"[Паян] LT [сехетре]\",nextDay:\"[Ыран] LT [сехетре]\",lastDay:\"[Ӗнер] LT [сехетре]\",nextWeek:\"[Ҫитес] dddd LT [сехетре]\",lastWeek:\"[Иртнӗ] dddd LT [сехетре]\",sameElse:\"L\"},relativeTime:{future:function(e){return e+(/\\u0441\\u0435\\u0445\\u0435\\u0442$/i.exec(e)?\"рен\":/\\u04ab\\u0443\\u043b$/i.exec(e)?\"тан\":\"ран\")},past:\"%s каялла\",s:\"пӗр-ик ҫеккунт\",ss:\"%d ҫеккунт\",m:\"пӗр минут\",mm:\"%d минут\",h:\"пӗр сехет\",hh:\"%d сехет\",d:\"пӗр кун\",dd:\"%d кун\",M:\"пӗр уйӑх\",MM:\"%d уйӑх\",y:\"пӗр ҫул\",yy:\"%d ҫул\"},dayOfMonthOrdinalParse:/\\d{1,2}-\\u043c\\u04d7\\u0448/,ordinal:\"%d-мӗш\",week:{dow:1,doy:7}}),t.defineLocale(\"cy\",{months:\"Ionawr_Chwefror_Mawrth_Ebrill_Mai_Mehefin_Gorffennaf_Awst_Medi_Hydref_Tachwedd_Rhagfyr\".split(\"_\"),monthsShort:\"Ion_Chwe_Maw_Ebr_Mai_Meh_Gor_Aws_Med_Hyd_Tach_Rhag\".split(\"_\"),weekdays:\"Dydd Sul_Dydd Llun_Dydd Mawrth_Dydd Mercher_Dydd Iau_Dydd Gwener_Dydd Sadwrn\".split(\"_\"),weekdaysShort:\"Sul_Llun_Maw_Mer_Iau_Gwe_Sad\".split(\"_\"),weekdaysMin:\"Su_Ll_Ma_Me_Ia_Gw_Sa\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Heddiw am] LT\",nextDay:\"[Yfory am] LT\",nextWeek:\"dddd [am] LT\",lastDay:\"[Ddoe am] LT\",lastWeek:\"dddd [diwethaf am] LT\",sameElse:\"L\"},relativeTime:{future:\"mewn %s\",past:\"%s yn ôl\",s:\"ychydig eiliadau\",ss:\"%d eiliad\",m:\"munud\",mm:\"%d munud\",h:\"awr\",hh:\"%d awr\",d:\"diwrnod\",dd:\"%d diwrnod\",M:\"mis\",MM:\"%d mis\",y:\"blwyddyn\",yy:\"%d flynedd\"},dayOfMonthOrdinalParse:/\\d{1,2}(fed|ain|af|il|ydd|ed|eg)/,ordinal:function(e){var t=\"\";return 20<e?t=40===e||50===e||60===e||80===e||100===e?\"fed\":\"ain\":0<e&&(t=[\"\",\"af\",\"il\",\"ydd\",\"ydd\",\"ed\",\"ed\",\"ed\",\"fed\",\"fed\",\"fed\",\"eg\",\"fed\",\"eg\",\"eg\",\"fed\",\"eg\",\"eg\",\"fed\",\"eg\",\"fed\"][e]),e+t},week:{dow:1,doy:4}}),t.defineLocale(\"da\",{months:\"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december\".split(\"_\"),monthsShort:\"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec\".split(\"_\"),weekdays:\"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag\".split(\"_\"),weekdaysShort:\"søn_man_tir_ons_tor_fre_lør\".split(\"_\"),weekdaysMin:\"sø_ma_ti_on_to_fr_lø\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY HH:mm\",LLLL:\"dddd [d.] D. MMMM YYYY [kl.] HH:mm\"},calendar:{sameDay:\"[i dag kl.] LT\",nextDay:\"[i morgen kl.] LT\",nextWeek:\"på dddd [kl.] LT\",lastDay:\"[i går kl.] LT\",lastWeek:\"[i] dddd[s kl.] LT\",sameElse:\"L\"},relativeTime:{future:\"om %s\",past:\"%s siden\",s:\"få sekunder\",ss:\"%d sekunder\",m:\"et minut\",mm:\"%d minutter\",h:\"en time\",hh:\"%d timer\",d:\"en dag\",dd:\"%d dage\",M:\"en måned\",MM:\"%d måneder\",y:\"et år\",yy:\"%d år\"},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"de-at\",{months:\"Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember\".split(\"_\"),monthsShort:\"Jän._Feb._März_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.\".split(\"_\"),monthsParseExact:!0,weekdays:\"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag\".split(\"_\"),weekdaysShort:\"So._Mo._Di._Mi._Do._Fr._Sa.\".split(\"_\"),weekdaysMin:\"So_Mo_Di_Mi_Do_Fr_Sa\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY HH:mm\",LLLL:\"dddd, D. MMMM YYYY HH:mm\"},calendar:{sameDay:\"[heute um] LT [Uhr]\",sameElse:\"L\",nextDay:\"[morgen um] LT [Uhr]\",nextWeek:\"dddd [um] LT [Uhr]\",lastDay:\"[gestern um] LT [Uhr]\",lastWeek:\"[letzten] dddd [um] LT [Uhr]\"},relativeTime:{future:\"in %s\",past:\"vor %s\",s:\"ein paar Sekunden\",ss:\"%d Sekunden\",m:Qn,mm:\"%d Minuten\",h:Qn,hh:\"%d Stunden\",d:Qn,dd:Qn,w:Qn,ww:\"%d Wochen\",M:Qn,MM:Qn,y:Qn,yy:Qn},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"de-ch\",{months:\"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember\".split(\"_\"),monthsShort:\"Jan._Feb._März_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.\".split(\"_\"),monthsParseExact:!0,weekdays:\"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag\".split(\"_\"),weekdaysShort:\"So_Mo_Di_Mi_Do_Fr_Sa\".split(\"_\"),weekdaysMin:\"So_Mo_Di_Mi_Do_Fr_Sa\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY HH:mm\",LLLL:\"dddd, D. MMMM YYYY HH:mm\"},calendar:{sameDay:\"[heute um] LT [Uhr]\",sameElse:\"L\",nextDay:\"[morgen um] LT [Uhr]\",nextWeek:\"dddd [um] LT [Uhr]\",lastDay:\"[gestern um] LT [Uhr]\",lastWeek:\"[letzten] dddd [um] LT [Uhr]\"},relativeTime:{future:\"in %s\",past:\"vor %s\",s:\"ein paar Sekunden\",ss:\"%d Sekunden\",m:er,mm:\"%d Minuten\",h:er,hh:\"%d Stunden\",d:er,dd:er,w:er,ww:\"%d Wochen\",M:er,MM:er,y:er,yy:er},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"de\",{months:\"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember\".split(\"_\"),monthsShort:\"Jan._Feb._März_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.\".split(\"_\"),monthsParseExact:!0,weekdays:\"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag\".split(\"_\"),weekdaysShort:\"So._Mo._Di._Mi._Do._Fr._Sa.\".split(\"_\"),weekdaysMin:\"So_Mo_Di_Mi_Do_Fr_Sa\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY HH:mm\",LLLL:\"dddd, D. MMMM YYYY HH:mm\"},calendar:{sameDay:\"[heute um] LT [Uhr]\",sameElse:\"L\",nextDay:\"[morgen um] LT [Uhr]\",nextWeek:\"dddd [um] LT [Uhr]\",lastDay:\"[gestern um] LT [Uhr]\",lastWeek:\"[letzten] dddd [um] LT [Uhr]\"},relativeTime:{future:\"in %s\",past:\"vor %s\",s:\"ein paar Sekunden\",ss:\"%d Sekunden\",m:tr,mm:\"%d Minuten\",h:tr,hh:\"%d Stunden\",d:tr,dd:tr,w:tr,ww:\"%d Wochen\",M:tr,MM:tr,y:tr,yy:tr},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),K=[\"ޖެނުއަރީ\",\"ފެބްރުއަރީ\",\"މާރިޗު\",\"އޭޕްރީލު\",\"މޭ\",\"ޖޫން\",\"ޖުލައި\",\"އޯގަސްޓު\",\"ސެޕްޓެމްބަރު\",\"އޮކްޓޯބަރު\",\"ނޮވެމްބަރު\",\"ޑިސެމްބަރު\"],U=[\"އާދިއްތަ\",\"ހޯމަ\",\"އަންގާރަ\",\"ބުދަ\",\"ބުރާސްފަތި\",\"ހުކުރު\",\"ހޮނިހިރު\"],t.defineLocale(\"dv\",{months:K,monthsShort:K,weekdays:U,weekdaysShort:U,weekdaysMin:\"އާދި_ހޯމަ_އަން_ބުދަ_ބުރާ_ހުކު_ހޮނި\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"D/M/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},meridiemParse:/\\u0789\\u0786|\\u0789\\u078a/,isPM:function(e){return\"މފ\"===e},meridiem:function(e,t,n){return e<12?\"މކ\":\"މފ\"},calendar:{sameDay:\"[މިއަދު] LT\",nextDay:\"[މާދަމާ] LT\",nextWeek:\"dddd LT\",lastDay:\"[އިއްޔެ] LT\",lastWeek:\"[ފާއިތުވި] dddd LT\",sameElse:\"L\"},relativeTime:{future:\"ތެރޭގައި %s\",past:\"ކުރިން %s\",s:\"ސިކުންތުކޮޅެއް\",ss:\"d% ސިކުންތު\",m:\"މިނިޓެއް\",mm:\"މިނިޓު %d\",h:\"ގަޑިއިރެއް\",hh:\"ގަޑިއިރު %d\",d:\"ދުވަހެއް\",dd:\"ދުވަސް %d\",M:\"މަހެއް\",MM:\"މަސް %d\",y:\"އަހަރެއް\",yy:\"އަހަރު %d\"},preparse:function(e){return e.replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/,/g,\"،\")},week:{dow:7,doy:12}}),t.defineLocale(\"el\",{monthsNominativeEl:\"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος\".split(\"_\"),monthsGenitiveEl:\"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου\".split(\"_\"),months:function(e,t){return e?(\"string\"==typeof t&&/D/.test(t.substring(0,t.indexOf(\"MMMM\")))?this._monthsGenitiveEl:this._monthsNominativeEl)[e.month()]:this._monthsNominativeEl},monthsShort:\"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ\".split(\"_\"),weekdays:\"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο\".split(\"_\"),weekdaysShort:\"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ\".split(\"_\"),weekdaysMin:\"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα\".split(\"_\"),meridiem:function(e,t,n){return 11<e?n?\"μμ\":\"ΜΜ\":n?\"πμ\":\"ΠΜ\"},isPM:function(e){return\"μ\"===(e+\"\").toLowerCase()[0]},meridiemParse:/[\\u03a0\\u039c]\\.?\\u039c?\\.?/i,longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY h:mm A\",LLLL:\"dddd, D MMMM YYYY h:mm A\"},calendarEl:{sameDay:\"[Σήμερα {}] LT\",nextDay:\"[Αύριο {}] LT\",nextWeek:\"dddd [{}] LT\",lastDay:\"[Χθες {}] LT\",lastWeek:function(){return 6===this.day()?\"[το προηγούμενο] dddd [{}] LT\":\"[την προηγούμενη] dddd [{}] LT\"},sameElse:\"L\"},calendar:function(e,t){e=this._calendarEl[e];var n,r=t&&t.hours();return n=e,(e=\"undefined\"!=typeof Function&&n instanceof Function||\"[object Function]\"===Object.prototype.toString.call(n)?e.apply(t):e).replace(\"{}\",r%12==1?\"στη\":\"στις\")},relativeTime:{future:\"σε %s\",past:\"%s πριν\",s:\"λίγα δευτερόλεπτα\",ss:\"%d δευτερόλεπτα\",m:\"ένα λεπτό\",mm:\"%d λεπτά\",h:\"μία ώρα\",hh:\"%d ώρες\",d:\"μία μέρα\",dd:\"%d μέρες\",M:\"ένας μήνας\",MM:\"%d μήνες\",y:\"ένας χρόνος\",yy:\"%d χρόνια\"},dayOfMonthOrdinalParse:/\\d{1,2}\\u03b7/,ordinal:\"%dη\",week:{dow:1,doy:4}}),t.defineLocale(\"en-au\",{months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),weekdaysShort:\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),weekdaysMin:\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY h:mm A\",LLLL:\"dddd, D MMMM YYYY h:mm A\"},calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")},week:{dow:0,doy:4}}),t.defineLocale(\"en-ca\",{months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),weekdaysShort:\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),weekdaysMin:\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"YYYY-MM-DD\",LL:\"MMMM D, YYYY\",LLL:\"MMMM D, YYYY h:mm A\",LLLL:\"dddd, MMMM D, YYYY h:mm A\"},calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")}}),t.defineLocale(\"en-gb\",{months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),weekdaysShort:\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),weekdaysMin:\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")},week:{dow:1,doy:4}}),t.defineLocale(\"en-ie\",{months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),weekdaysShort:\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),weekdaysMin:\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")},week:{dow:1,doy:4}}),t.defineLocale(\"en-il\",{months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),weekdaysShort:\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),weekdaysMin:\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")}}),t.defineLocale(\"en-in\",{months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),weekdaysShort:\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),weekdaysMin:\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY h:mm A\",LLLL:\"dddd, D MMMM YYYY h:mm A\"},calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")},week:{dow:0,doy:6}}),t.defineLocale(\"en-nz\",{months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),weekdaysShort:\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),weekdaysMin:\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY h:mm A\",LLLL:\"dddd, D MMMM YYYY h:mm A\"},calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")},week:{dow:1,doy:4}}),t.defineLocale(\"en-sg\",{months:\"January_February_March_April_May_June_July_August_September_October_November_December\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec\".split(\"_\"),weekdays:\"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday\".split(\"_\"),weekdaysShort:\"Sun_Mon_Tue_Wed_Thu_Fri_Sat\".split(\"_\"),weekdaysMin:\"Su_Mo_Tu_We_Th_Fr_Sa\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Today at] LT\",nextDay:\"[Tomorrow at] LT\",nextWeek:\"dddd [at] LT\",lastDay:\"[Yesterday at] LT\",lastWeek:\"[Last] dddd [at] LT\",sameElse:\"L\"},relativeTime:{future:\"in %s\",past:\"%s ago\",s:\"a few seconds\",ss:\"%d seconds\",m:\"a minute\",mm:\"%d minutes\",h:\"an hour\",hh:\"%d hours\",d:\"a day\",dd:\"%d days\",M:\"a month\",MM:\"%d months\",y:\"a year\",yy:\"%d years\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")},week:{dow:1,doy:4}}),t.defineLocale(\"eo\",{months:\"januaro_februaro_marto_aprilo_majo_junio_julio_aŭgusto_septembro_oktobro_novembro_decembro\".split(\"_\"),monthsShort:\"jan_feb_mart_apr_maj_jun_jul_aŭg_sept_okt_nov_dec\".split(\"_\"),weekdays:\"dimanĉo_lundo_mardo_merkredo_ĵaŭdo_vendredo_sabato\".split(\"_\"),weekdaysShort:\"dim_lun_mard_merk_ĵaŭ_ven_sab\".split(\"_\"),weekdaysMin:\"di_lu_ma_me_ĵa_ve_sa\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY-MM-DD\",LL:\"[la] D[-an de] MMMM, YYYY\",LLL:\"[la] D[-an de] MMMM, YYYY HH:mm\",LLLL:\"dddd[n], [la] D[-an de] MMMM, YYYY HH:mm\",llll:\"ddd, [la] D[-an de] MMM, YYYY HH:mm\"},meridiemParse:/[ap]\\.t\\.m/i,isPM:function(e){return\"p\"===e.charAt(0).toLowerCase()},meridiem:function(e,t,n){return 11<e?n?\"p.t.m.\":\"P.T.M.\":n?\"a.t.m.\":\"A.T.M.\"},calendar:{sameDay:\"[Hodiaŭ je] LT\",nextDay:\"[Morgaŭ je] LT\",nextWeek:\"dddd[n je] LT\",lastDay:\"[Hieraŭ je] LT\",lastWeek:\"[pasintan] dddd[n je] LT\",sameElse:\"L\"},relativeTime:{future:\"post %s\",past:\"antaŭ %s\",s:\"kelkaj sekundoj\",ss:\"%d sekundoj\",m:\"unu minuto\",mm:\"%d minutoj\",h:\"unu horo\",hh:\"%d horoj\",d:\"unu tago\",dd:\"%d tagoj\",M:\"unu monato\",MM:\"%d monatoj\",y:\"unu jaro\",yy:\"%d jaroj\"},dayOfMonthOrdinalParse:/\\d{1,2}a/,ordinal:\"%da\",week:{dow:1,doy:7}});var nr=\"ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.\".split(\"_\"),rr=\"ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic\".split(\"_\"),ir=(Z=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],Te=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\\.?|feb\\.?|mar\\.?|abr\\.?|may\\.?|jun\\.?|jul\\.?|ago\\.?|sep\\.?|oct\\.?|nov\\.?|dic\\.?)/i,t.defineLocale(\"es-do\",{months:\"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre\".split(\"_\"),monthsShort:function(e,t){return e?(/-MMM-/.test(t)?rr:nr)[e.month()]:nr},monthsRegex:Te,monthsShortRegex:Te,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\\.?|feb\\.?|mar\\.?|abr\\.?|may\\.?|jun\\.?|jul\\.?|ago\\.?|sep\\.?|oct\\.?|nov\\.?|dic\\.?)/i,monthsParse:Z,longMonthsParse:Z,shortMonthsParse:Z,weekdays:\"domingo_lunes_martes_miércoles_jueves_viernes_sábado\".split(\"_\"),weekdaysShort:\"dom._lun._mar._mié._jue._vie._sáb.\".split(\"_\"),weekdaysMin:\"do_lu_ma_mi_ju_vi_sá\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"DD/MM/YYYY\",LL:\"D [de] MMMM [de] YYYY\",LLL:\"D [de] MMMM [de] YYYY h:mm A\",LLLL:\"dddd, D [de] MMMM [de] YYYY h:mm A\"},calendar:{sameDay:function(){return\"[hoy a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},nextDay:function(){return\"[mañana a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},nextWeek:function(){return\"dddd [a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},lastDay:function(){return\"[ayer a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},lastWeek:function(){return\"[el] dddd [pasado a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},sameElse:\"L\"},relativeTime:{future:\"en %s\",past:\"hace %s\",s:\"unos segundos\",ss:\"%d segundos\",m:\"un minuto\",mm:\"%d minutos\",h:\"una hora\",hh:\"%d horas\",d:\"un día\",dd:\"%d días\",w:\"una semana\",ww:\"%d semanas\",M:\"un mes\",MM:\"%d meses\",y:\"un año\",yy:\"%d años\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:1,doy:4}}),\"ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.\".split(\"_\")),ar=\"ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic\".split(\"_\"),or=(ie=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],_n=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\\.?|feb\\.?|mar\\.?|abr\\.?|may\\.?|jun\\.?|jul\\.?|ago\\.?|sep\\.?|oct\\.?|nov\\.?|dic\\.?)/i,t.defineLocale(\"es-mx\",{months:\"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre\".split(\"_\"),monthsShort:function(e,t){return e?(/-MMM-/.test(t)?ar:ir)[e.month()]:ir},monthsRegex:_n,monthsShortRegex:_n,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\\.?|feb\\.?|mar\\.?|abr\\.?|may\\.?|jun\\.?|jul\\.?|ago\\.?|sep\\.?|oct\\.?|nov\\.?|dic\\.?)/i,monthsParse:ie,longMonthsParse:ie,shortMonthsParse:ie,weekdays:\"domingo_lunes_martes_miércoles_jueves_viernes_sábado\".split(\"_\"),weekdaysShort:\"dom._lun._mar._mié._jue._vie._sáb.\".split(\"_\"),weekdaysMin:\"do_lu_ma_mi_ju_vi_sá\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D [de] MMMM [de] YYYY\",LLL:\"D [de] MMMM [de] YYYY H:mm\",LLLL:\"dddd, D [de] MMMM [de] YYYY H:mm\"},calendar:{sameDay:function(){return\"[hoy a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},nextDay:function(){return\"[mañana a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},nextWeek:function(){return\"dddd [a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},lastDay:function(){return\"[ayer a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},lastWeek:function(){return\"[el] dddd [pasado a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},sameElse:\"L\"},relativeTime:{future:\"en %s\",past:\"hace %s\",s:\"unos segundos\",ss:\"%d segundos\",m:\"un minuto\",mm:\"%d minutos\",h:\"una hora\",hh:\"%d horas\",d:\"un día\",dd:\"%d días\",w:\"una semana\",ww:\"%d semanas\",M:\"un mes\",MM:\"%d meses\",y:\"un año\",yy:\"%d años\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:0,doy:4},invalidDate:\"Fecha inválida\"}),\"ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.\".split(\"_\")),sr=\"ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic\".split(\"_\"),lr=(yn=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\\.?|feb\\.?|mar\\.?|abr\\.?|may\\.?|jun\\.?|jul\\.?|ago\\.?|sep\\.?|oct\\.?|nov\\.?|dic\\.?)/i),ur=(t.defineLocale(\"es-us\",{months:\"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre\".split(\"_\"),monthsShort:function(e,t){return e?(/-MMM-/.test(t)?sr:or)[e.month()]:or},monthsRegex:lr,monthsShortRegex:lr,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\\.?|feb\\.?|mar\\.?|abr\\.?|may\\.?|jun\\.?|jul\\.?|ago\\.?|sep\\.?|oct\\.?|nov\\.?|dic\\.?)/i,monthsParse:yn,longMonthsParse:yn,shortMonthsParse:yn,weekdays:\"domingo_lunes_martes_miércoles_jueves_viernes_sábado\".split(\"_\"),weekdaysShort:\"dom._lun._mar._mié._jue._vie._sáb.\".split(\"_\"),weekdaysMin:\"do_lu_ma_mi_ju_vi_sá\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"MM/DD/YYYY\",LL:\"D [de] MMMM [de] YYYY\",LLL:\"D [de] MMMM [de] YYYY h:mm A\",LLLL:\"dddd, D [de] MMMM [de] YYYY h:mm A\"},calendar:{sameDay:function(){return\"[hoy a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},nextDay:function(){return\"[mañana a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},nextWeek:function(){return\"dddd [a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},lastDay:function(){return\"[ayer a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},lastWeek:function(){return\"[el] dddd [pasado a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},sameElse:\"L\"},relativeTime:{future:\"en %s\",past:\"hace %s\",s:\"unos segundos\",ss:\"%d segundos\",m:\"un minuto\",mm:\"%d minutos\",h:\"una hora\",hh:\"%d horas\",d:\"un día\",dd:\"%d días\",w:\"una semana\",ww:\"%d semanas\",M:\"un mes\",MM:\"%d meses\",y:\"un año\",yy:\"%d años\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:0,doy:6}}),\"ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.\".split(\"_\")),dr=\"ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic\".split(\"_\");Ge=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],Q=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\\.?|feb\\.?|mar\\.?|abr\\.?|may\\.?|jun\\.?|jul\\.?|ago\\.?|sep\\.?|oct\\.?|nov\\.?|dic\\.?)/i;function cr(e,t,n,r){return e={s:[\"mõne sekundi\",\"mõni sekund\",\"paar sekundit\"],ss:[e+\"sekundi\",e+\"sekundit\"],m:[\"ühe minuti\",\"üks minut\"],mm:[e+\" minuti\",e+\" minutit\"],h:[\"ühe tunni\",\"tund aega\",\"üks tund\"],hh:[e+\" tunni\",e+\" tundi\"],d:[\"ühe päeva\",\"üks päev\"],M:[\"kuu aja\",\"kuu aega\",\"üks kuu\"],MM:[e+\" kuu\",e+\" kuud\"],y:[\"ühe aasta\",\"aasta\",\"üks aasta\"],yy:[e+\" aasta\",e+\" aastat\"]},t?e[n][2]||e[n][1]:r?e[n][0]:e[n][1]}t.defineLocale(\"es\",{months:\"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre\".split(\"_\"),monthsShort:function(e,t){return e?(/-MMM-/.test(t)?dr:ur)[e.month()]:ur},monthsRegex:Q,monthsShortRegex:Q,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\\.?|feb\\.?|mar\\.?|abr\\.?|may\\.?|jun\\.?|jul\\.?|ago\\.?|sep\\.?|oct\\.?|nov\\.?|dic\\.?)/i,monthsParse:Ge,longMonthsParse:Ge,shortMonthsParse:Ge,weekdays:\"domingo_lunes_martes_miércoles_jueves_viernes_sábado\".split(\"_\"),weekdaysShort:\"dom._lun._mar._mié._jue._vie._sáb.\".split(\"_\"),weekdaysMin:\"do_lu_ma_mi_ju_vi_sá\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D [de] MMMM [de] YYYY\",LLL:\"D [de] MMMM [de] YYYY H:mm\",LLLL:\"dddd, D [de] MMMM [de] YYYY H:mm\"},calendar:{sameDay:function(){return\"[hoy a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},nextDay:function(){return\"[mañana a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},nextWeek:function(){return\"dddd [a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},lastDay:function(){return\"[ayer a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},lastWeek:function(){return\"[el] dddd [pasado a la\"+(1!==this.hours()?\"s\":\"\")+\"] LT\"},sameElse:\"L\"},relativeTime:{future:\"en %s\",past:\"hace %s\",s:\"unos segundos\",ss:\"%d segundos\",m:\"un minuto\",mm:\"%d minutos\",h:\"una hora\",hh:\"%d horas\",d:\"un día\",dd:\"%d días\",w:\"una semana\",ww:\"%d semanas\",M:\"un mes\",MM:\"%d meses\",y:\"un año\",yy:\"%d años\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:1,doy:4},invalidDate:\"Fecha inválida\"}),t.defineLocale(\"et\",{months:\"jaanuar_veebruar_märts_aprill_mai_juuni_juuli_august_september_oktoober_november_detsember\".split(\"_\"),monthsShort:\"jaan_veebr_märts_apr_mai_juuni_juuli_aug_sept_okt_nov_dets\".split(\"_\"),weekdays:\"pühapäev_esmaspäev_teisipäev_kolmapäev_neljapäev_reede_laupäev\".split(\"_\"),weekdaysShort:\"P_E_T_K_N_R_L\".split(\"_\"),weekdaysMin:\"P_E_T_K_N_R_L\".split(\"_\"),longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY H:mm\",LLLL:\"dddd, D. MMMM YYYY H:mm\"},calendar:{sameDay:\"[Täna,] LT\",nextDay:\"[Homme,] LT\",nextWeek:\"[Järgmine] dddd LT\",lastDay:\"[Eile,] LT\",lastWeek:\"[Eelmine] dddd LT\",sameElse:\"L\"},relativeTime:{future:\"%s pärast\",past:\"%s tagasi\",s:cr,ss:cr,m:cr,mm:cr,h:cr,hh:cr,d:cr,dd:\"%d päeva\",M:cr,MM:cr,y:cr,yy:cr},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"eu\",{months:\"urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua\".split(\"_\"),monthsShort:\"urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.\".split(\"_\"),monthsParseExact:!0,weekdays:\"igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata\".split(\"_\"),weekdaysShort:\"ig._al._ar._az._og._ol._lr.\".split(\"_\"),weekdaysMin:\"ig_al_ar_az_og_ol_lr\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY-MM-DD\",LL:\"YYYY[ko] MMMM[ren] D[a]\",LLL:\"YYYY[ko] MMMM[ren] D[a] HH:mm\",LLLL:\"dddd, YYYY[ko] MMMM[ren] D[a] HH:mm\",l:\"YYYY-M-D\",ll:\"YYYY[ko] MMM D[a]\",lll:\"YYYY[ko] MMM D[a] HH:mm\",llll:\"ddd, YYYY[ko] MMM D[a] HH:mm\"},calendar:{sameDay:\"[gaur] LT[etan]\",nextDay:\"[bihar] LT[etan]\",nextWeek:\"dddd LT[etan]\",lastDay:\"[atzo] LT[etan]\",lastWeek:\"[aurreko] dddd LT[etan]\",sameElse:\"L\"},relativeTime:{future:\"%s barru\",past:\"duela %s\",s:\"segundo batzuk\",ss:\"%d segundo\",m:\"minutu bat\",mm:\"%d minutu\",h:\"ordu bat\",hh:\"%d ordu\",d:\"egun bat\",dd:\"%d egun\",M:\"hilabete bat\",MM:\"%d hilabete\",y:\"urte bat\",yy:\"%d urte\"},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:7}});var fr={1:\"۱\",2:\"۲\",3:\"۳\",4:\"۴\",5:\"۵\",6:\"۶\",7:\"۷\",8:\"۸\",9:\"۹\",0:\"۰\"},pr={\"۱\":\"1\",\"۲\":\"2\",\"۳\":\"3\",\"۴\":\"4\",\"۵\":\"5\",\"۶\":\"6\",\"۷\":\"7\",\"۸\":\"8\",\"۹\":\"9\",\"۰\":\"0\"},hr=(t.defineLocale(\"fa\",{months:\"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر\".split(\"_\"),monthsShort:\"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر\".split(\"_\"),weekdays:\"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه\".split(\"_\"),weekdaysShort:\"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه\".split(\"_\"),weekdaysMin:\"ی_د_س_چ_پ_ج_ش\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},meridiemParse:/\\u0642\\u0628\\u0644 \\u0627\\u0632 \\u0638\\u0647\\u0631|\\u0628\\u0639\\u062f \\u0627\\u0632 \\u0638\\u0647\\u0631/,isPM:function(e){return/\\u0628\\u0639\\u062f \\u0627\\u0632 \\u0638\\u0647\\u0631/.test(e)},meridiem:function(e,t,n){return e<12?\"قبل از ظهر\":\"بعد از ظهر\"},calendar:{sameDay:\"[امروز ساعت] LT\",nextDay:\"[فردا ساعت] LT\",nextWeek:\"dddd [ساعت] LT\",lastDay:\"[دیروز ساعت] LT\",lastWeek:\"dddd [پیش] [ساعت] LT\",sameElse:\"L\"},relativeTime:{future:\"در %s\",past:\"%s پیش\",s:\"چند ثانیه\",ss:\"%d ثانیه\",m:\"یک دقیقه\",mm:\"%d دقیقه\",h:\"یک ساعت\",hh:\"%d ساعت\",d:\"یک روز\",dd:\"%d روز\",M:\"یک ماه\",MM:\"%d ماه\",y:\"یک سال\",yy:\"%d سال\"},preparse:function(e){return e.replace(/[\\u06f0-\\u06f9]/g,function(e){return pr[e]}).replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/\\d/g,function(e){return fr[e]}).replace(/,/g,\"،\")},dayOfMonthOrdinalParse:/\\d{1,2}\\u0645/,ordinal:\"%dم\",week:{dow:6,doy:12}}),\"nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän\".split(\" \")),mr=[\"nolla\",\"yhden\",\"kahden\",\"kolmen\",\"neljän\",\"viiden\",\"kuuden\",hr[7],hr[8],hr[9]];function vr(e,t,n,r){var i=\"\";switch(n){case\"s\":return r?\"muutaman sekunnin\":\"muutama sekunti\";case\"ss\":i=r?\"sekunnin\":\"sekuntia\";break;case\"m\":return r?\"minuutin\":\"minuutti\";case\"mm\":i=r?\"minuutin\":\"minuuttia\";break;case\"h\":return r?\"tunnin\":\"tunti\";case\"hh\":i=r?\"tunnin\":\"tuntia\";break;case\"d\":return r?\"päivän\":\"päivä\";case\"dd\":i=r?\"päivän\":\"päivää\";break;case\"M\":return r?\"kuukauden\":\"kuukausi\";case\"MM\":i=r?\"kuukauden\":\"kuukautta\";break;case\"y\":return r?\"vuoden\":\"vuosi\";case\"yy\":i=r?\"vuoden\":\"vuotta\"}return n=r,(e<10?(n?mr:hr)[e]:e)+\" \"+i}t.defineLocale(\"fi\",{months:\"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu\".split(\"_\"),monthsShort:\"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu\".split(\"_\"),weekdays:\"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai\".split(\"_\"),weekdaysShort:\"su_ma_ti_ke_to_pe_la\".split(\"_\"),weekdaysMin:\"su_ma_ti_ke_to_pe_la\".split(\"_\"),longDateFormat:{LT:\"HH.mm\",LTS:\"HH.mm.ss\",L:\"DD.MM.YYYY\",LL:\"Do MMMM[ta] YYYY\",LLL:\"Do MMMM[ta] YYYY, [klo] HH.mm\",LLLL:\"dddd, Do MMMM[ta] YYYY, [klo] HH.mm\",l:\"D.M.YYYY\",ll:\"Do MMM YYYY\",lll:\"Do MMM YYYY, [klo] HH.mm\",llll:\"ddd, Do MMM YYYY, [klo] HH.mm\"},calendar:{sameDay:\"[tänään] [klo] LT\",nextDay:\"[huomenna] [klo] LT\",nextWeek:\"dddd [klo] LT\",lastDay:\"[eilen] [klo] LT\",lastWeek:\"[viime] dddd[na] [klo] LT\",sameElse:\"L\"},relativeTime:{future:\"%s päästä\",past:\"%s sitten\",s:vr,ss:vr,m:vr,mm:vr,h:vr,hh:vr,d:vr,dd:vr,M:vr,MM:vr,y:vr,yy:vr},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"fil\",{months:\"Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre\".split(\"_\"),monthsShort:\"Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis\".split(\"_\"),weekdays:\"Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado\".split(\"_\"),weekdaysShort:\"Lin_Lun_Mar_Miy_Huw_Biy_Sab\".split(\"_\"),weekdaysMin:\"Li_Lu_Ma_Mi_Hu_Bi_Sab\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"MM/D/YYYY\",LL:\"MMMM D, YYYY\",LLL:\"MMMM D, YYYY HH:mm\",LLLL:\"dddd, MMMM DD, YYYY HH:mm\"},calendar:{sameDay:\"LT [ngayong araw]\",nextDay:\"[Bukas ng] LT\",nextWeek:\"LT [sa susunod na] dddd\",lastDay:\"LT [kahapon]\",lastWeek:\"LT [noong nakaraang] dddd\",sameElse:\"L\"},relativeTime:{future:\"sa loob ng %s\",past:\"%s ang nakalipas\",s:\"ilang segundo\",ss:\"%d segundo\",m:\"isang minuto\",mm:\"%d minuto\",h:\"isang oras\",hh:\"%d oras\",d:\"isang araw\",dd:\"%d araw\",M:\"isang buwan\",MM:\"%d buwan\",y:\"isang taon\",yy:\"%d taon\"},dayOfMonthOrdinalParse:/\\d{1,2}/,ordinal:function(e){return e},week:{dow:1,doy:4}}),t.defineLocale(\"fo\",{months:\"januar_februar_mars_apríl_mai_juni_juli_august_september_oktober_november_desember\".split(\"_\"),monthsShort:\"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des\".split(\"_\"),weekdays:\"sunnudagur_mánadagur_týsdagur_mikudagur_hósdagur_fríggjadagur_leygardagur\".split(\"_\"),weekdaysShort:\"sun_mán_týs_mik_hós_frí_ley\".split(\"_\"),weekdaysMin:\"su_má_tý_mi_hó_fr_le\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D. MMMM, YYYY HH:mm\"},calendar:{sameDay:\"[Í dag kl.] LT\",nextDay:\"[Í morgin kl.] LT\",nextWeek:\"dddd [kl.] LT\",lastDay:\"[Í gjár kl.] LT\",lastWeek:\"[síðstu] dddd [kl] LT\",sameElse:\"L\"},relativeTime:{future:\"um %s\",past:\"%s síðani\",s:\"fá sekund\",ss:\"%d sekundir\",m:\"ein minuttur\",mm:\"%d minuttir\",h:\"ein tími\",hh:\"%d tímar\",d:\"ein dagur\",dd:\"%d dagar\",M:\"ein mánaður\",MM:\"%d mánaðir\",y:\"eitt ár\",yy:\"%d ár\"},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"fr-ca\",{months:\"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre\".split(\"_\"),monthsShort:\"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.\".split(\"_\"),monthsParseExact:!0,weekdays:\"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi\".split(\"_\"),weekdaysShort:\"dim._lun._mar._mer._jeu._ven._sam.\".split(\"_\"),weekdaysMin:\"di_lu_ma_me_je_ve_sa\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY-MM-DD\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Aujourd’hui à] LT\",nextDay:\"[Demain à] LT\",nextWeek:\"dddd [à] LT\",lastDay:\"[Hier à] LT\",lastWeek:\"dddd [dernier à] LT\",sameElse:\"L\"},relativeTime:{future:\"dans %s\",past:\"il y a %s\",s:\"quelques secondes\",ss:\"%d secondes\",m:\"une minute\",mm:\"%d minutes\",h:\"une heure\",hh:\"%d heures\",d:\"un jour\",dd:\"%d jours\",M:\"un mois\",MM:\"%d mois\",y:\"un an\",yy:\"%d ans\"},dayOfMonthOrdinalParse:/\\d{1,2}(er|e)/,ordinal:function(e,t){switch(t){default:case\"M\":case\"Q\":case\"D\":case\"DDD\":case\"d\":return e+(1===e?\"er\":\"e\");case\"w\":case\"W\":return e+(1===e?\"re\":\"e\")}}}),t.defineLocale(\"fr-ch\",{months:\"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre\".split(\"_\"),monthsShort:\"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.\".split(\"_\"),monthsParseExact:!0,weekdays:\"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi\".split(\"_\"),weekdaysShort:\"dim._lun._mar._mer._jeu._ven._sam.\".split(\"_\"),weekdaysMin:\"di_lu_ma_me_je_ve_sa\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Aujourd’hui à] LT\",nextDay:\"[Demain à] LT\",nextWeek:\"dddd [à] LT\",lastDay:\"[Hier à] LT\",lastWeek:\"dddd [dernier à] LT\",sameElse:\"L\"},relativeTime:{future:\"dans %s\",past:\"il y a %s\",s:\"quelques secondes\",ss:\"%d secondes\",m:\"une minute\",mm:\"%d minutes\",h:\"une heure\",hh:\"%d heures\",d:\"un jour\",dd:\"%d jours\",M:\"un mois\",MM:\"%d mois\",y:\"un an\",yy:\"%d ans\"},dayOfMonthOrdinalParse:/\\d{1,2}(er|e)/,ordinal:function(e,t){switch(t){default:case\"M\":case\"Q\":case\"D\":case\"DDD\":case\"d\":return e+(1===e?\"er\":\"e\");case\"w\":case\"W\":return e+(1===e?\"re\":\"e\")}},week:{dow:1,doy:4}});re=/(janv\\.?|f\\xe9vr\\.?|mars|avr\\.?|mai|juin|juil\\.?|ao\\xfbt|sept\\.?|oct\\.?|nov\\.?|d\\xe9c\\.?|janvier|f\\xe9vrier|mars|avril|mai|juin|juillet|ao\\xfbt|septembre|octobre|novembre|d\\xe9cembre)/i,V=[/^janv/i,/^f\\xe9vr/i,/^mars/i,/^avr/i,/^mai/i,/^juin/i,/^juil/i,/^ao\\xfbt/i,/^sept/i,/^oct/i,/^nov/i,/^d\\xe9c/i];var gr=(t.defineLocale(\"fr\",{months:\"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre\".split(\"_\"),monthsShort:\"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.\".split(\"_\"),monthsRegex:re,monthsShortRegex:re,monthsStrictRegex:/^(janvier|f\\xe9vrier|mars|avril|mai|juin|juillet|ao\\xfbt|septembre|octobre|novembre|d\\xe9cembre)/i,monthsShortStrictRegex:/(janv\\.?|f\\xe9vr\\.?|mars|avr\\.?|mai|juin|juil\\.?|ao\\xfbt|sept\\.?|oct\\.?|nov\\.?|d\\xe9c\\.?)/i,monthsParse:V,longMonthsParse:V,shortMonthsParse:V,weekdays:\"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi\".split(\"_\"),weekdaysShort:\"dim._lun._mar._mer._jeu._ven._sam.\".split(\"_\"),weekdaysMin:\"di_lu_ma_me_je_ve_sa\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Aujourd’hui à] LT\",nextDay:\"[Demain à] LT\",nextWeek:\"dddd [à] LT\",lastDay:\"[Hier à] LT\",lastWeek:\"dddd [dernier à] LT\",sameElse:\"L\"},relativeTime:{future:\"dans %s\",past:\"il y a %s\",s:\"quelques secondes\",ss:\"%d secondes\",m:\"une minute\",mm:\"%d minutes\",h:\"une heure\",hh:\"%d heures\",d:\"un jour\",dd:\"%d jours\",w:\"une semaine\",ww:\"%d semaines\",M:\"un mois\",MM:\"%d mois\",y:\"un an\",yy:\"%d ans\"},dayOfMonthOrdinalParse:/\\d{1,2}(er|)/,ordinal:function(e,t){switch(t){case\"D\":return e+(1===e?\"er\":\"\");default:case\"M\":case\"Q\":case\"DDD\":case\"d\":return e+(1===e?\"er\":\"e\");case\"w\":case\"W\":return e+(1===e?\"re\":\"e\")}},week:{dow:1,doy:4}}),\"jan._feb._mrt._apr._mai_jun._jul._aug._sep._okt._nov._des.\".split(\"_\")),_r=\"jan_feb_mrt_apr_mai_jun_jul_aug_sep_okt_nov_des\".split(\"_\");function yr(e,t,n,r){return e={s:[\"थोडया सॅकंडांनी\",\"थोडे सॅकंड\"],ss:[e+\" सॅकंडांनी\",e+\" सॅकंड\"],m:[\"एका मिणटान\",\"एक मिनूट\"],mm:[e+\" मिणटांनी\",e+\" मिणटां\"],h:[\"एका वरान\",\"एक वर\"],hh:[e+\" वरांनी\",e+\" वरां\"],d:[\"एका दिसान\",\"एक दीस\"],dd:[e+\" दिसांनी\",e+\" दीस\"],M:[\"एका म्हयन्यान\",\"एक म्हयनो\"],MM:[e+\" म्हयन्यानी\",e+\" म्हयने\"],y:[\"एका वर्सान\",\"एक वर्स\"],yy:[e+\" वर्सांनी\",e+\" वर्सां\"]},r?e[n][0]:e[n][1]}function br(e,t,n,r){return e={s:[\"thoddea sekondamni\",\"thodde sekond\"],ss:[e+\" sekondamni\",e+\" sekond\"],m:[\"eka mintan\",\"ek minut\"],mm:[e+\" mintamni\",e+\" mintam\"],h:[\"eka voran\",\"ek vor\"],hh:[e+\" voramni\",e+\" voram\"],d:[\"eka disan\",\"ek dis\"],dd:[e+\" disamni\",e+\" dis\"],M:[\"eka mhoinean\",\"ek mhoino\"],MM:[e+\" mhoineamni\",e+\" mhoine\"],y:[\"eka vorsan\",\"ek voros\"],yy:[e+\" vorsamni\",e+\" vorsam\"]},r?e[n][0]:e[n][1]}t.defineLocale(\"fy\",{months:\"jannewaris_febrewaris_maart_april_maaie_juny_july_augustus_septimber_oktober_novimber_desimber\".split(\"_\"),monthsShort:function(e,t){return e?(/-MMM-/.test(t)?_r:gr)[e.month()]:gr},monthsParseExact:!0,weekdays:\"snein_moandei_tiisdei_woansdei_tongersdei_freed_sneon\".split(\"_\"),weekdaysShort:\"si._mo._ti._wo._to._fr._so.\".split(\"_\"),weekdaysMin:\"Si_Mo_Ti_Wo_To_Fr_So\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD-MM-YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[hjoed om] LT\",nextDay:\"[moarn om] LT\",nextWeek:\"dddd [om] LT\",lastDay:\"[juster om] LT\",lastWeek:\"[ôfrûne] dddd [om] LT\",sameElse:\"L\"},relativeTime:{future:\"oer %s\",past:\"%s lyn\",s:\"in pear sekonden\",ss:\"%d sekonden\",m:\"ien minút\",mm:\"%d minuten\",h:\"ien oere\",hh:\"%d oeren\",d:\"ien dei\",dd:\"%d dagen\",M:\"ien moanne\",MM:\"%d moannen\",y:\"ien jier\",yy:\"%d jierren\"},dayOfMonthOrdinalParse:/\\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||20<=e?\"ste\":\"de\")},week:{dow:1,doy:4}}),t.defineLocale(\"ga\",{months:[\"Eanáir\",\"Feabhra\",\"Márta\",\"Aibreán\",\"Bealtaine\",\"Meitheamh\",\"Iúil\",\"Lúnasa\",\"Meán Fómhair\",\"Deireadh Fómhair\",\"Samhain\",\"Nollaig\"],monthsShort:[\"Ean\",\"Feabh\",\"Márt\",\"Aib\",\"Beal\",\"Meith\",\"Iúil\",\"Lún\",\"M.F.\",\"D.F.\",\"Samh\",\"Noll\"],monthsParseExact:!0,weekdays:[\"Dé Domhnaigh\",\"Dé Luain\",\"Dé Máirt\",\"Dé Céadaoin\",\"Déardaoin\",\"Dé hAoine\",\"Dé Sathairn\"],weekdaysShort:[\"Domh\",\"Luan\",\"Máirt\",\"Céad\",\"Déar\",\"Aoine\",\"Sath\"],weekdaysMin:[\"Do\",\"Lu\",\"Má\",\"Cé\",\"Dé\",\"A\",\"Sa\"],longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Inniu ag] LT\",nextDay:\"[Amárach ag] LT\",nextWeek:\"dddd [ag] LT\",lastDay:\"[Inné ag] LT\",lastWeek:\"dddd [seo caite] [ag] LT\",sameElse:\"L\"},relativeTime:{future:\"i %s\",past:\"%s ó shin\",s:\"cúpla soicind\",ss:\"%d soicind\",m:\"nóiméad\",mm:\"%d nóiméad\",h:\"uair an chloig\",hh:\"%d uair an chloig\",d:\"lá\",dd:\"%d lá\",M:\"mí\",MM:\"%d míonna\",y:\"bliain\",yy:\"%d bliain\"},dayOfMonthOrdinalParse:/\\d{1,2}(d|na|mh)/,ordinal:function(e){return e+(1===e?\"d\":e%10==2?\"na\":\"mh\")},week:{dow:1,doy:4}}),t.defineLocale(\"gd\",{months:[\"Am Faoilleach\",\"An Gearran\",\"Am Màrt\",\"An Giblean\",\"An Cèitean\",\"An t-Ògmhios\",\"An t-Iuchar\",\"An Lùnastal\",\"An t-Sultain\",\"An Dàmhair\",\"An t-Samhain\",\"An Dùbhlachd\"],monthsShort:[\"Faoi\",\"Gear\",\"Màrt\",\"Gibl\",\"Cèit\",\"Ògmh\",\"Iuch\",\"Lùn\",\"Sult\",\"Dàmh\",\"Samh\",\"Dùbh\"],monthsParseExact:!0,weekdays:[\"Didòmhnaich\",\"Diluain\",\"Dimàirt\",\"Diciadain\",\"Diardaoin\",\"Dihaoine\",\"Disathairne\"],weekdaysShort:[\"Did\",\"Dil\",\"Dim\",\"Dic\",\"Dia\",\"Dih\",\"Dis\"],weekdaysMin:[\"Dò\",\"Lu\",\"Mà\",\"Ci\",\"Ar\",\"Ha\",\"Sa\"],longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[An-diugh aig] LT\",nextDay:\"[A-màireach aig] LT\",nextWeek:\"dddd [aig] LT\",lastDay:\"[An-dè aig] LT\",lastWeek:\"dddd [seo chaidh] [aig] LT\",sameElse:\"L\"},relativeTime:{future:\"ann an %s\",past:\"bho chionn %s\",s:\"beagan diogan\",ss:\"%d diogan\",m:\"mionaid\",mm:\"%d mionaidean\",h:\"uair\",hh:\"%d uairean\",d:\"latha\",dd:\"%d latha\",M:\"mìos\",MM:\"%d mìosan\",y:\"bliadhna\",yy:\"%d bliadhna\"},dayOfMonthOrdinalParse:/\\d{1,2}(d|na|mh)/,ordinal:function(e){return e+(1===e?\"d\":e%10==2?\"na\":\"mh\")},week:{dow:1,doy:4}}),t.defineLocale(\"gl\",{months:\"xaneiro_febreiro_marzo_abril_maio_xuño_xullo_agosto_setembro_outubro_novembro_decembro\".split(\"_\"),monthsShort:\"xan._feb._mar._abr._mai._xuñ._xul._ago._set._out._nov._dec.\".split(\"_\"),monthsParseExact:!0,weekdays:\"domingo_luns_martes_mércores_xoves_venres_sábado\".split(\"_\"),weekdaysShort:\"dom._lun._mar._mér._xov._ven._sáb.\".split(\"_\"),weekdaysMin:\"do_lu_ma_mé_xo_ve_sá\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D [de] MMMM [de] YYYY\",LLL:\"D [de] MMMM [de] YYYY H:mm\",LLLL:\"dddd, D [de] MMMM [de] YYYY H:mm\"},calendar:{sameDay:function(){return\"[hoxe \"+(1!==this.hours()?\"ás\":\"á\")+\"] LT\"},nextDay:function(){return\"[mañá \"+(1!==this.hours()?\"ás\":\"á\")+\"] LT\"},nextWeek:function(){return\"dddd [\"+(1!==this.hours()?\"ás\":\"a\")+\"] LT\"},lastDay:function(){return\"[onte \"+(1!==this.hours()?\"á\":\"a\")+\"] LT\"},lastWeek:function(){return\"[o] dddd [pasado \"+(1!==this.hours()?\"ás\":\"a\")+\"] LT\"},sameElse:\"L\"},relativeTime:{future:function(e){return 0===e.indexOf(\"un\")?\"n\"+e:\"en \"+e},past:\"hai %s\",s:\"uns segundos\",ss:\"%d segundos\",m:\"un minuto\",mm:\"%d minutos\",h:\"unha hora\",hh:\"%d horas\",d:\"un día\",dd:\"%d días\",M:\"un mes\",MM:\"%d meses\",y:\"un ano\",yy:\"%d anos\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:1,doy:4}}),t.defineLocale(\"gom-deva\",{months:{standalone:\"जानेवारी_फेब्रुवारी_मार्च_एप्रील_मे_जून_जुलय_ऑगस्ट_सप्टेंबर_ऑक्टोबर_नोव्हेंबर_डिसेंबर\".split(\"_\"),format:\"जानेवारीच्या_फेब्रुवारीच्या_मार्चाच्या_एप्रीलाच्या_मेयाच्या_जूनाच्या_जुलयाच्या_ऑगस्टाच्या_सप्टेंबराच्या_ऑक्टोबराच्या_नोव्हेंबराच्या_डिसेंबराच्या\".split(\"_\"),isFormat:/MMMM(\\s)+D[oD]?/},monthsShort:\"जाने._फेब्रु._मार्च_एप्री._मे_जून_जुल._ऑग._सप्टें._ऑक्टो._नोव्हें._डिसें.\".split(\"_\"),monthsParseExact:!0,weekdays:\"आयतार_सोमार_मंगळार_बुधवार_बिरेस्तार_सुक्रार_शेनवार\".split(\"_\"),weekdaysShort:\"आयत._सोम._मंगळ._बुध._ब्रेस्त._सुक्र._शेन.\".split(\"_\"),weekdaysMin:\"आ_सो_मं_बु_ब्रे_सु_शे\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"A h:mm [वाजतां]\",LTS:\"A h:mm:ss [वाजतां]\",L:\"DD-MM-YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY A h:mm [वाजतां]\",LLLL:\"dddd, MMMM Do, YYYY, A h:mm [वाजतां]\",llll:\"ddd, D MMM YYYY, A h:mm [वाजतां]\"},calendar:{sameDay:\"[आयज] LT\",nextDay:\"[फाल्यां] LT\",nextWeek:\"[फुडलो] dddd[,] LT\",lastDay:\"[काल] LT\",lastWeek:\"[फाटलो] dddd[,] LT\",sameElse:\"L\"},relativeTime:{future:\"%s\",past:\"%s आदीं\",s:yr,ss:yr,m:yr,mm:yr,h:yr,hh:yr,d:yr,dd:yr,M:yr,MM:yr,y:yr,yy:yr},dayOfMonthOrdinalParse:/\\d{1,2}(\\u0935\\u0947\\u0930)/,ordinal:function(e,t){return\"D\"===t?e+\"वेर\":e},week:{dow:0,doy:3},meridiemParse:/\\u0930\\u093e\\u0924\\u0940|\\u0938\\u0915\\u093e\\u0933\\u0940\\u0902|\\u0926\\u0928\\u092a\\u093e\\u0930\\u093e\\u0902|\\u0938\\u093e\\u0902\\u091c\\u0947/,meridiemHour:function(e,t){return 12===e&&(e=0),\"राती\"===t?e<4?e:e+12:\"सकाळीं\"===t?e:\"दनपारां\"===t?12<e?e:e+12:\"सांजे\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"राती\":e<12?\"सकाळीं\":e<16?\"दनपारां\":e<20?\"सांजे\":\"राती\"}}),t.defineLocale(\"gom-latn\",{months:{standalone:\"Janer_Febrer_Mars_Abril_Mai_Jun_Julai_Agost_Setembr_Otubr_Novembr_Dezembr\".split(\"_\"),format:\"Janerachea_Febrerachea_Marsachea_Abrilachea_Maiachea_Junachea_Julaiachea_Agostachea_Setembrachea_Otubrachea_Novembrachea_Dezembrachea\".split(\"_\"),isFormat:/MMMM(\\s)+D[oD]?/},monthsShort:\"Jan._Feb._Mars_Abr._Mai_Jun_Jul._Ago._Set._Otu._Nov._Dez.\".split(\"_\"),monthsParseExact:!0,weekdays:\"Aitar_Somar_Mongllar_Budhvar_Birestar_Sukrar_Son'var\".split(\"_\"),weekdaysShort:\"Ait._Som._Mon._Bud._Bre._Suk._Son.\".split(\"_\"),weekdaysMin:\"Ai_Sm_Mo_Bu_Br_Su_Sn\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"A h:mm [vazta]\",LTS:\"A h:mm:ss [vazta]\",L:\"DD-MM-YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY A h:mm [vazta]\",LLLL:\"dddd, MMMM Do, YYYY, A h:mm [vazta]\",llll:\"ddd, D MMM YYYY, A h:mm [vazta]\"},calendar:{sameDay:\"[Aiz] LT\",nextDay:\"[Faleam] LT\",nextWeek:\"[Fuddlo] dddd[,] LT\",lastDay:\"[Kal] LT\",lastWeek:\"[Fattlo] dddd[,] LT\",sameElse:\"L\"},relativeTime:{future:\"%s\",past:\"%s adim\",s:br,ss:br,m:br,mm:br,h:br,hh:br,d:br,dd:br,M:br,MM:br,y:br,yy:br},dayOfMonthOrdinalParse:/\\d{1,2}(er)/,ordinal:function(e,t){return\"D\"===t?e+\"er\":e},week:{dow:0,doy:3},meridiemParse:/rati|sokallim|donparam|sanje/,meridiemHour:function(e,t){return 12===e&&(e=0),\"rati\"===t?e<4?e:e+12:\"sokallim\"===t?e:\"donparam\"===t?12<e?e:e+12:\"sanje\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"rati\":e<12?\"sokallim\":e<16?\"donparam\":e<20?\"sanje\":\"rati\"}});var Mr={1:\"૧\",2:\"૨\",3:\"૩\",4:\"૪\",5:\"૫\",6:\"૬\",7:\"૭\",8:\"૮\",9:\"૯\",0:\"૦\"},wr={\"૧\":\"1\",\"૨\":\"2\",\"૩\":\"3\",\"૪\":\"4\",\"૫\":\"5\",\"૬\":\"6\",\"૭\":\"7\",\"૮\":\"8\",\"૯\":\"9\",\"૦\":\"0\"},kr=(t.defineLocale(\"gu\",{months:\"જાન્યુઆરી_ફેબ્રુઆરી_માર્ચ_એપ્રિલ_મે_જૂન_જુલાઈ_ઑગસ્ટ_સપ્ટેમ્બર_ઑક્ટ્બર_નવેમ્બર_ડિસેમ્બર\".split(\"_\"),monthsShort:\"જાન્યુ._ફેબ્રુ._માર્ચ_એપ્રિ._મે_જૂન_જુલા._ઑગ._સપ્ટે._ઑક્ટ્._નવે._ડિસે.\".split(\"_\"),monthsParseExact:!0,weekdays:\"રવિવાર_સોમવાર_મંગળવાર_બુધ્વાર_ગુરુવાર_શુક્રવાર_શનિવાર\".split(\"_\"),weekdaysShort:\"રવિ_સોમ_મંગળ_બુધ્_ગુરુ_શુક્ર_શનિ\".split(\"_\"),weekdaysMin:\"ર_સો_મં_બુ_ગુ_શુ_શ\".split(\"_\"),longDateFormat:{LT:\"A h:mm વાગ્યે\",LTS:\"A h:mm:ss વાગ્યે\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm વાગ્યે\",LLLL:\"dddd, D MMMM YYYY, A h:mm વાગ્યે\"},calendar:{sameDay:\"[આજ] LT\",nextDay:\"[કાલે] LT\",nextWeek:\"dddd, LT\",lastDay:\"[ગઇકાલે] LT\",lastWeek:\"[પાછલા] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s મા\",past:\"%s પહેલા\",s:\"અમુક પળો\",ss:\"%d સેકંડ\",m:\"એક મિનિટ\",mm:\"%d મિનિટ\",h:\"એક કલાક\",hh:\"%d કલાક\",d:\"એક દિવસ\",dd:\"%d દિવસ\",M:\"એક મહિનો\",MM:\"%d મહિનો\",y:\"એક વર્ષ\",yy:\"%d વર્ષ\"},preparse:function(e){return e.replace(/[\\u0ae7\\u0ae8\\u0ae9\\u0aea\\u0aeb\\u0aec\\u0aed\\u0aee\\u0aef\\u0ae6]/g,function(e){return wr[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return Mr[e]})},meridiemParse:/\\u0ab0\\u0abe\\u0aa4|\\u0aac\\u0aaa\\u0acb\\u0ab0|\\u0ab8\\u0ab5\\u0abe\\u0ab0|\\u0ab8\\u0abe\\u0a82\\u0a9c/,meridiemHour:function(e,t){return 12===e&&(e=0),\"રાત\"===t?e<4?e:e+12:\"સવાર\"===t?e:\"બપોર\"===t?10<=e?e:e+12:\"સાંજ\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"રાત\":e<10?\"સવાર\":e<17?\"બપોર\":e<20?\"સાંજ\":\"રાત\"},week:{dow:0,doy:6}}),t.defineLocale(\"he\",{months:\"ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר\".split(\"_\"),monthsShort:\"ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳\".split(\"_\"),weekdays:\"ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת\".split(\"_\"),weekdaysShort:\"א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳\".split(\"_\"),weekdaysMin:\"א_ב_ג_ד_ה_ו_ש\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D [ב]MMMM YYYY\",LLL:\"D [ב]MMMM YYYY HH:mm\",LLLL:\"dddd, D [ב]MMMM YYYY HH:mm\",l:\"D/M/YYYY\",ll:\"D MMM YYYY\",lll:\"D MMM YYYY HH:mm\",llll:\"ddd, D MMM YYYY HH:mm\"},calendar:{sameDay:\"[היום ב־]LT\",nextDay:\"[מחר ב־]LT\",nextWeek:\"dddd [בשעה] LT\",lastDay:\"[אתמול ב־]LT\",lastWeek:\"[ביום] dddd [האחרון בשעה] LT\",sameElse:\"L\"},relativeTime:{future:\"בעוד %s\",past:\"לפני %s\",s:\"מספר שניות\",ss:\"%d שניות\",m:\"דקה\",mm:\"%d דקות\",h:\"שעה\",hh:function(e){return 2===e?\"שעתיים\":e+\" שעות\"},d:\"יום\",dd:function(e){return 2===e?\"יומיים\":e+\" ימים\"},M:\"חודש\",MM:function(e){return 2===e?\"חודשיים\":e+\" חודשים\"},y:\"שנה\",yy:function(e){return 2===e?\"שנתיים\":e%10==0&&10!==e?e+\" שנה\":e+\" שנים\"}},meridiemParse:/\\u05d0\\u05d7\\u05d4\"\\u05e6|\\u05dc\\u05e4\\u05e0\\u05d4\"\\u05e6|\\u05d0\\u05d7\\u05e8\\u05d9 \\u05d4\\u05e6\\u05d4\\u05e8\\u05d9\\u05d9\\u05dd|\\u05dc\\u05e4\\u05e0\\u05d9 \\u05d4\\u05e6\\u05d4\\u05e8\\u05d9\\u05d9\\u05dd|\\u05dc\\u05e4\\u05e0\\u05d5\\u05ea \\u05d1\\u05d5\\u05e7\\u05e8|\\u05d1\\u05d1\\u05d5\\u05e7\\u05e8|\\u05d1\\u05e2\\u05e8\\u05d1/i,isPM:function(e){return/^(\\u05d0\\u05d7\\u05d4\"\\u05e6|\\u05d0\\u05d7\\u05e8\\u05d9 \\u05d4\\u05e6\\u05d4\\u05e8\\u05d9\\u05d9\\u05dd|\\u05d1\\u05e2\\u05e8\\u05d1)$/.test(e)},meridiem:function(e,t,n){return e<5?\"לפנות בוקר\":e<10?\"בבוקר\":e<12?n?'לפנה\"צ':\"לפני הצהריים\":e<18?n?'אחה\"צ':\"אחרי הצהריים\":\"בערב\"}}),{1:\"१\",2:\"२\",3:\"३\",4:\"४\",5:\"५\",6:\"६\",7:\"७\",8:\"८\",9:\"९\",0:\"०\"}),Lr={\"१\":\"1\",\"२\":\"2\",\"३\":\"3\",\"४\":\"4\",\"५\":\"5\",\"६\":\"6\",\"७\":\"7\",\"८\":\"8\",\"९\":\"9\",\"०\":\"0\"};$=[/^\\u091c\\u0928/i,/^\\u092b\\u093c\\u0930|\\u092b\\u0930/i,/^\\u092e\\u093e\\u0930\\u094d\\u091a/i,/^\\u0905\\u092a\\u094d\\u0930\\u0948/i,/^\\u092e\\u0908/i,/^\\u091c\\u0942\\u0928/i,/^\\u091c\\u0941\\u0932/i,/^\\u0905\\u0917/i,/^\\u0938\\u093f\\u0924\\u0902|\\u0938\\u093f\\u0924/i,/^\\u0905\\u0915\\u094d\\u091f\\u0942/i,/^\\u0928\\u0935|\\u0928\\u0935\\u0902/i,/^\\u0926\\u093f\\u0938\\u0902|\\u0926\\u093f\\u0938/i];function xr(e,t,n){var r=e+\" \";switch(n){case\"ss\":return r+(1===e?\"sekunda\":2===e||3===e||4===e?\"sekunde\":\"sekundi\");case\"m\":return t?\"jedna minuta\":\"jedne minute\";case\"mm\":return r+(1===e||2!==e&&3!==e&&4!==e?\"minuta\":\"minute\");case\"h\":return t?\"jedan sat\":\"jednog sata\";case\"hh\":return r+(1===e?\"sat\":2===e||3===e||4===e?\"sata\":\"sati\");case\"dd\":return r+(1===e?\"dan\":\"dana\");case\"MM\":return r+(1===e?\"mjesec\":2===e||3===e||4===e?\"mjeseca\":\"mjeseci\");case\"yy\":return r+(1===e||2!==e&&3!==e&&4!==e?\"godina\":\"godine\")}}t.defineLocale(\"hi\",{months:{format:\"जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर\".split(\"_\"),standalone:\"जनवरी_फरवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितंबर_अक्टूबर_नवंबर_दिसंबर\".split(\"_\")},monthsShort:\"जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.\".split(\"_\"),weekdays:\"रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार\".split(\"_\"),weekdaysShort:\"रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि\".split(\"_\"),weekdaysMin:\"र_सो_मं_बु_गु_शु_श\".split(\"_\"),longDateFormat:{LT:\"A h:mm बजे\",LTS:\"A h:mm:ss बजे\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm बजे\",LLLL:\"dddd, D MMMM YYYY, A h:mm बजे\"},monthsParse:$,longMonthsParse:$,shortMonthsParse:[/^\\u091c\\u0928/i,/^\\u092b\\u093c\\u0930/i,/^\\u092e\\u093e\\u0930\\u094d\\u091a/i,/^\\u0905\\u092a\\u094d\\u0930\\u0948/i,/^\\u092e\\u0908/i,/^\\u091c\\u0942\\u0928/i,/^\\u091c\\u0941\\u0932/i,/^\\u0905\\u0917/i,/^\\u0938\\u093f\\u0924/i,/^\\u0905\\u0915\\u094d\\u091f\\u0942/i,/^\\u0928\\u0935/i,/^\\u0926\\u093f\\u0938/i],monthsRegex:/^(\\u091c\\u0928\\u0935\\u0930\\u0940|\\u091c\\u0928\\.?|\\u092b\\u093c\\u0930\\u0935\\u0930\\u0940|\\u092b\\u0930\\u0935\\u0930\\u0940|\\u092b\\u093c\\u0930\\.?|\\u092e\\u093e\\u0930\\u094d\\u091a?|\\u0905\\u092a\\u094d\\u0930\\u0948\\u0932|\\u0905\\u092a\\u094d\\u0930\\u0948\\.?|\\u092e\\u0908?|\\u091c\\u0942\\u0928?|\\u091c\\u0941\\u0932\\u093e\\u0908|\\u091c\\u0941\\u0932\\.?|\\u0905\\u0917\\u0938\\u094d\\u0924|\\u0905\\u0917\\.?|\\u0938\\u093f\\u0924\\u092e\\u094d\\u092c\\u0930|\\u0938\\u093f\\u0924\\u0902\\u092c\\u0930|\\u0938\\u093f\\u0924\\.?|\\u0905\\u0915\\u094d\\u091f\\u0942\\u092c\\u0930|\\u0905\\u0915\\u094d\\u091f\\u0942\\.?|\\u0928\\u0935\\u092e\\u094d\\u092c\\u0930|\\u0928\\u0935\\u0902\\u092c\\u0930|\\u0928\\u0935\\.?|\\u0926\\u093f\\u0938\\u092e\\u094d\\u092c\\u0930|\\u0926\\u093f\\u0938\\u0902\\u092c\\u0930|\\u0926\\u093f\\u0938\\.?)/i,monthsShortRegex:/^(\\u091c\\u0928\\u0935\\u0930\\u0940|\\u091c\\u0928\\.?|\\u092b\\u093c\\u0930\\u0935\\u0930\\u0940|\\u092b\\u0930\\u0935\\u0930\\u0940|\\u092b\\u093c\\u0930\\.?|\\u092e\\u093e\\u0930\\u094d\\u091a?|\\u0905\\u092a\\u094d\\u0930\\u0948\\u0932|\\u0905\\u092a\\u094d\\u0930\\u0948\\.?|\\u092e\\u0908?|\\u091c\\u0942\\u0928?|\\u091c\\u0941\\u0932\\u093e\\u0908|\\u091c\\u0941\\u0932\\.?|\\u0905\\u0917\\u0938\\u094d\\u0924|\\u0905\\u0917\\.?|\\u0938\\u093f\\u0924\\u092e\\u094d\\u092c\\u0930|\\u0938\\u093f\\u0924\\u0902\\u092c\\u0930|\\u0938\\u093f\\u0924\\.?|\\u0905\\u0915\\u094d\\u091f\\u0942\\u092c\\u0930|\\u0905\\u0915\\u094d\\u091f\\u0942\\.?|\\u0928\\u0935\\u092e\\u094d\\u092c\\u0930|\\u0928\\u0935\\u0902\\u092c\\u0930|\\u0928\\u0935\\.?|\\u0926\\u093f\\u0938\\u092e\\u094d\\u092c\\u0930|\\u0926\\u093f\\u0938\\u0902\\u092c\\u0930|\\u0926\\u093f\\u0938\\.?)/i,monthsStrictRegex:/^(\\u091c\\u0928\\u0935\\u0930\\u0940?|\\u092b\\u093c\\u0930\\u0935\\u0930\\u0940|\\u092b\\u0930\\u0935\\u0930\\u0940?|\\u092e\\u093e\\u0930\\u094d\\u091a?|\\u0905\\u092a\\u094d\\u0930\\u0948\\u0932?|\\u092e\\u0908?|\\u091c\\u0942\\u0928?|\\u091c\\u0941\\u0932\\u093e\\u0908?|\\u0905\\u0917\\u0938\\u094d\\u0924?|\\u0938\\u093f\\u0924\\u092e\\u094d\\u092c\\u0930|\\u0938\\u093f\\u0924\\u0902\\u092c\\u0930|\\u0938\\u093f\\u0924?\\.?|\\u0905\\u0915\\u094d\\u091f\\u0942\\u092c\\u0930|\\u0905\\u0915\\u094d\\u091f\\u0942\\.?|\\u0928\\u0935\\u092e\\u094d\\u092c\\u0930|\\u0928\\u0935\\u0902\\u092c\\u0930?|\\u0926\\u093f\\u0938\\u092e\\u094d\\u092c\\u0930|\\u0926\\u093f\\u0938\\u0902\\u092c\\u0930?)/i,monthsShortStrictRegex:/^(\\u091c\\u0928\\.?|\\u092b\\u093c\\u0930\\.?|\\u092e\\u093e\\u0930\\u094d\\u091a?|\\u0905\\u092a\\u094d\\u0930\\u0948\\.?|\\u092e\\u0908?|\\u091c\\u0942\\u0928?|\\u091c\\u0941\\u0932\\.?|\\u0905\\u0917\\.?|\\u0938\\u093f\\u0924\\.?|\\u0905\\u0915\\u094d\\u091f\\u0942\\.?|\\u0928\\u0935\\.?|\\u0926\\u093f\\u0938\\.?)/i,calendar:{sameDay:\"[आज] LT\",nextDay:\"[कल] LT\",nextWeek:\"dddd, LT\",lastDay:\"[कल] LT\",lastWeek:\"[पिछले] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s में\",past:\"%s पहले\",s:\"कुछ ही क्षण\",ss:\"%d सेकंड\",m:\"एक मिनट\",mm:\"%d मिनट\",h:\"एक घंटा\",hh:\"%d घंटे\",d:\"एक दिन\",dd:\"%d दिन\",M:\"एक महीने\",MM:\"%d महीने\",y:\"एक वर्ष\",yy:\"%d वर्ष\"},preparse:function(e){return e.replace(/[\\u0967\\u0968\\u0969\\u096a\\u096b\\u096c\\u096d\\u096e\\u096f\\u0966]/g,function(e){return Lr[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return kr[e]})},meridiemParse:/\\u0930\\u093e\\u0924|\\u0938\\u0941\\u092c\\u0939|\\u0926\\u094b\\u092a\\u0939\\u0930|\\u0936\\u093e\\u092e/,meridiemHour:function(e,t){return 12===e&&(e=0),\"रात\"===t?e<4?e:e+12:\"सुबह\"===t?e:\"दोपहर\"===t?10<=e?e:e+12:\"शाम\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"रात\":e<10?\"सुबह\":e<17?\"दोपहर\":e<20?\"शाम\":\"रात\"},week:{dow:0,doy:6}}),t.defineLocale(\"hr\",{months:{format:\"siječnja_veljače_ožujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca\".split(\"_\"),standalone:\"siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac\".split(\"_\")},monthsShort:\"sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.\".split(\"_\"),monthsParseExact:!0,weekdays:\"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota\".split(\"_\"),weekdaysShort:\"ned._pon._uto._sri._čet._pet._sub.\".split(\"_\"),weekdaysMin:\"ne_po_ut_sr_če_pe_su\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"Do MMMM YYYY\",LLL:\"Do MMMM YYYY H:mm\",LLLL:\"dddd, Do MMMM YYYY H:mm\"},calendar:{sameDay:\"[danas u] LT\",nextDay:\"[sutra u] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[u] [nedjelju] [u] LT\";case 3:return\"[u] [srijedu] [u] LT\";case 6:return\"[u] [subotu] [u] LT\";case 1:case 2:case 4:case 5:return\"[u] dddd [u] LT\"}},lastDay:\"[jučer u] LT\",lastWeek:function(){switch(this.day()){case 0:return\"[prošlu] [nedjelju] [u] LT\";case 3:return\"[prošlu] [srijedu] [u] LT\";case 6:return\"[prošle] [subote] [u] LT\";case 1:case 2:case 4:case 5:return\"[prošli] dddd [u] LT\"}},sameElse:\"L\"},relativeTime:{future:\"za %s\",past:\"prije %s\",s:\"par sekundi\",ss:xr,m:xr,mm:xr,h:xr,hh:xr,d:\"dan\",dd:xr,M:\"mjesec\",MM:xr,y:\"godinu\",yy:xr},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:7}});var Tr=\"vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton\".split(\" \");function Sr(e,t,n,r){var i=e;switch(n){case\"s\":return r||t?\"néhány másodperc\":\"néhány másodperce\";case\"ss\":return i+(r||t)?\" másodperc\":\" másodperce\";case\"m\":return\"egy\"+(r||t?\" perc\":\" perce\");case\"mm\":return i+(r||t?\" perc\":\" perce\");case\"h\":return\"egy\"+(r||t?\" óra\":\" órája\");case\"hh\":return i+(r||t?\" óra\":\" órája\");case\"d\":return\"egy\"+(r||t?\" nap\":\" napja\");case\"dd\":return i+(r||t?\" nap\":\" napja\");case\"M\":return\"egy\"+(r||t?\" hónap\":\" hónapja\");case\"MM\":return i+(r||t?\" hónap\":\" hónapja\");case\"y\":return\"egy\"+(r||t?\" év\":\" éve\");case\"yy\":return i+(r||t?\" év\":\" éve\")}return\"\"}function Dr(e){return(e?\"\":\"[múlt] \")+\"[\"+Tr[this.day()]+\"] LT[-kor]\"}function Er(e){return e%100==11||e%10!=1}function Yr(e,t,n,r){var i=e+\" \";switch(n){case\"s\":return t||r?\"nokkrar sekúndur\":\"nokkrum sekúndum\";case\"ss\":return Er(e)?i+(t||r?\"sekúndur\":\"sekúndum\"):i+\"sekúnda\";case\"m\":return t?\"mínúta\":\"mínútu\";case\"mm\":return Er(e)?i+(t||r?\"mínútur\":\"mínútum\"):t?i+\"mínúta\":i+\"mínútu\";case\"hh\":return Er(e)?i+(t||r?\"klukkustundir\":\"klukkustundum\"):i+\"klukkustund\";case\"d\":return t?\"dagur\":r?\"dag\":\"degi\";case\"dd\":return Er(e)?t?i+\"dagar\":i+(r?\"daga\":\"dögum\"):t?i+\"dagur\":i+(r?\"dag\":\"degi\");case\"M\":return t?\"mánuður\":r?\"mánuð\":\"mánuði\";case\"MM\":return Er(e)?t?i+\"mánuðir\":i+(r?\"mánuði\":\"mánuðum\"):t?i+\"mánuður\":i+(r?\"mánuð\":\"mánuði\");case\"y\":return t||r?\"ár\":\"ári\";case\"yy\":return Er(e)?i+(t||r?\"ár\":\"árum\"):i+(t||r?\"ár\":\"ári\")}}t.defineLocale(\"hu\",{months:\"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december\".split(\"_\"),monthsShort:\"jan._feb._márc._ápr._máj._jún._júl._aug._szept._okt._nov._dec.\".split(\"_\"),monthsParseExact:!0,weekdays:\"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat\".split(\"_\"),weekdaysShort:\"vas_hét_kedd_sze_csüt_pén_szo\".split(\"_\"),weekdaysMin:\"v_h_k_sze_cs_p_szo\".split(\"_\"),longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"YYYY.MM.DD.\",LL:\"YYYY. MMMM D.\",LLL:\"YYYY. MMMM D. H:mm\",LLLL:\"YYYY. MMMM D., dddd H:mm\"},meridiemParse:/de|du/i,isPM:function(e){return\"u\"===e.charAt(1).toLowerCase()},meridiem:function(e,t,n){return e<12?!0===n?\"de\":\"DE\":!0===n?\"du\":\"DU\"},calendar:{sameDay:\"[ma] LT[-kor]\",nextDay:\"[holnap] LT[-kor]\",nextWeek:function(){return Dr.call(this,!0)},lastDay:\"[tegnap] LT[-kor]\",lastWeek:function(){return Dr.call(this,!1)},sameElse:\"L\"},relativeTime:{future:\"%s múlva\",past:\"%s\",s:Sr,ss:Sr,m:Sr,mm:Sr,h:Sr,hh:Sr,d:Sr,dd:Sr,M:Sr,MM:Sr,y:Sr,yy:Sr},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"hy-am\",{months:{format:\"հունվարի_փետրվարի_մարտի_ապրիլի_մայիսի_հունիսի_հուլիսի_օգոստոսի_սեպտեմբերի_հոկտեմբերի_նոյեմբերի_դեկտեմբերի\".split(\"_\"),standalone:\"հունվար_փետրվար_մարտ_ապրիլ_մայիս_հունիս_հուլիս_օգոստոս_սեպտեմբեր_հոկտեմբեր_նոյեմբեր_դեկտեմբեր\".split(\"_\")},monthsShort:\"հնվ_փտր_մրտ_ապր_մյս_հնս_հլս_օգս_սպտ_հկտ_նմբ_դկտ\".split(\"_\"),weekdays:\"կիրակի_երկուշաբթի_երեքշաբթի_չորեքշաբթի_հինգշաբթի_ուրբաթ_շաբաթ\".split(\"_\"),weekdaysShort:\"կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ\".split(\"_\"),weekdaysMin:\"կրկ_երկ_երք_չրք_հնգ_ուրբ_շբթ\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY թ.\",LLL:\"D MMMM YYYY թ., HH:mm\",LLLL:\"dddd, D MMMM YYYY թ., HH:mm\"},calendar:{sameDay:\"[այսօր] LT\",nextDay:\"[վաղը] LT\",lastDay:\"[երեկ] LT\",nextWeek:function(){return\"dddd [օրը ժամը] LT\"},lastWeek:function(){return\"[անցած] dddd [օրը ժամը] LT\"},sameElse:\"L\"},relativeTime:{future:\"%s հետո\",past:\"%s առաջ\",s:\"մի քանի վայրկյան\",ss:\"%d վայրկյան\",m:\"րոպե\",mm:\"%d րոպե\",h:\"ժամ\",hh:\"%d ժամ\",d:\"օր\",dd:\"%d օր\",M:\"ամիս\",MM:\"%d ամիս\",y:\"տարի\",yy:\"%d տարի\"},meridiemParse:/\\u0563\\u056b\\u0577\\u0565\\u0580\\u057e\\u0561|\\u0561\\u057c\\u0561\\u057e\\u0578\\u057f\\u057e\\u0561|\\u0581\\u0565\\u0580\\u0565\\u056f\\u057e\\u0561|\\u0565\\u0580\\u0565\\u056f\\u0578\\u0575\\u0561\\u0576/,isPM:function(e){return/^(\\u0581\\u0565\\u0580\\u0565\\u056f\\u057e\\u0561|\\u0565\\u0580\\u0565\\u056f\\u0578\\u0575\\u0561\\u0576)$/.test(e)},meridiem:function(e){return e<4?\"գիշերվա\":e<12?\"առավոտվա\":e<17?\"ցերեկվա\":\"երեկոյան\"},dayOfMonthOrdinalParse:/\\d{1,2}|\\d{1,2}-(\\u056b\\u0576|\\u0580\\u0564)/,ordinal:function(e,t){switch(t){case\"DDD\":case\"w\":case\"W\":case\"DDDo\":return 1===e?e+\"-ին\":e+\"-րդ\";default:return e}},week:{dow:1,doy:7}}),t.defineLocale(\"id\",{months:\"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Agt_Sep_Okt_Nov_Des\".split(\"_\"),weekdays:\"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu\".split(\"_\"),weekdaysShort:\"Min_Sen_Sel_Rab_Kam_Jum_Sab\".split(\"_\"),weekdaysMin:\"Mg_Sn_Sl_Rb_Km_Jm_Sb\".split(\"_\"),longDateFormat:{LT:\"HH.mm\",LTS:\"HH.mm.ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY [pukul] HH.mm\",LLLL:\"dddd, D MMMM YYYY [pukul] HH.mm\"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(e,t){return 12===e&&(e=0),\"pagi\"===t?e:\"siang\"===t?11<=e?e:e+12:\"sore\"===t||\"malam\"===t?e+12:void 0},meridiem:function(e,t,n){return e<11?\"pagi\":e<15?\"siang\":e<19?\"sore\":\"malam\"},calendar:{sameDay:\"[Hari ini pukul] LT\",nextDay:\"[Besok pukul] LT\",nextWeek:\"dddd [pukul] LT\",lastDay:\"[Kemarin pukul] LT\",lastWeek:\"dddd [lalu pukul] LT\",sameElse:\"L\"},relativeTime:{future:\"dalam %s\",past:\"%s yang lalu\",s:\"beberapa detik\",ss:\"%d detik\",m:\"semenit\",mm:\"%d menit\",h:\"sejam\",hh:\"%d jam\",d:\"sehari\",dd:\"%d hari\",M:\"sebulan\",MM:\"%d bulan\",y:\"setahun\",yy:\"%d tahun\"},week:{dow:0,doy:6}}),t.defineLocale(\"is\",{months:\"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember\".split(\"_\"),monthsShort:\"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des\".split(\"_\"),weekdays:\"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur\".split(\"_\"),weekdaysShort:\"sun_mán_þri_mið_fim_fös_lau\".split(\"_\"),weekdaysMin:\"Su_Má_Þr_Mi_Fi_Fö_La\".split(\"_\"),longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY [kl.] H:mm\",LLLL:\"dddd, D. MMMM YYYY [kl.] H:mm\"},calendar:{sameDay:\"[í dag kl.] LT\",nextDay:\"[á morgun kl.] LT\",nextWeek:\"dddd [kl.] LT\",lastDay:\"[í gær kl.] LT\",lastWeek:\"[síðasta] dddd [kl.] LT\",sameElse:\"L\"},relativeTime:{future:\"eftir %s\",past:\"fyrir %s síðan\",s:Yr,ss:Yr,m:Yr,mm:Yr,h:\"klukkustund\",hh:Yr,d:Yr,dd:Yr,M:Yr,MM:Yr,y:Yr,yy:Yr},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"it-ch\",{months:\"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre\".split(\"_\"),monthsShort:\"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic\".split(\"_\"),weekdays:\"domenica_lunedì_martedì_mercoledì_giovedì_venerdì_sabato\".split(\"_\"),weekdaysShort:\"dom_lun_mar_mer_gio_ven_sab\".split(\"_\"),weekdaysMin:\"do_lu_ma_me_gi_ve_sa\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Oggi alle] LT\",nextDay:\"[Domani alle] LT\",nextWeek:\"dddd [alle] LT\",lastDay:\"[Ieri alle] LT\",lastWeek:function(){return 0===this.day()?\"[la scorsa] dddd [alle] LT\":\"[lo scorso] dddd [alle] LT\"},sameElse:\"L\"},relativeTime:{future:function(e){return(/^[0-9].+$/.test(e)?\"tra\":\"in\")+\" \"+e},past:\"%s fa\",s:\"alcuni secondi\",ss:\"%d secondi\",m:\"un minuto\",mm:\"%d minuti\",h:\"un'ora\",hh:\"%d ore\",d:\"un giorno\",dd:\"%d giorni\",M:\"un mese\",MM:\"%d mesi\",y:\"un anno\",yy:\"%d anni\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:1,doy:4}}),t.defineLocale(\"it\",{months:\"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre\".split(\"_\"),monthsShort:\"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic\".split(\"_\"),weekdays:\"domenica_lunedì_martedì_mercoledì_giovedì_venerdì_sabato\".split(\"_\"),weekdaysShort:\"dom_lun_mar_mer_gio_ven_sab\".split(\"_\"),weekdaysMin:\"do_lu_ma_me_gi_ve_sa\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:function(){return\"[Oggi a\"+(1<this.hours()?\"lle \":0===this.hours()?\" \":\"ll'\")+\"]LT\"},nextDay:function(){return\"[Domani a\"+(1<this.hours()?\"lle \":0===this.hours()?\" \":\"ll'\")+\"]LT\"},nextWeek:function(){return\"dddd [a\"+(1<this.hours()?\"lle \":0===this.hours()?\" \":\"ll'\")+\"]LT\"},lastDay:function(){return\"[Ieri a\"+(1<this.hours()?\"lle \":0===this.hours()?\" \":\"ll'\")+\"]LT\"},lastWeek:function(){return 0===this.day()?\"[La scorsa] dddd [a\"+(1<this.hours()?\"lle \":0===this.hours()?\" \":\"ll'\")+\"]LT\":\"[Lo scorso] dddd [a\"+(1<this.hours()?\"lle \":0===this.hours()?\" \":\"ll'\")+\"]LT\"},sameElse:\"L\"},relativeTime:{future:\"tra %s\",past:\"%s fa\",s:\"alcuni secondi\",ss:\"%d secondi\",m:\"un minuto\",mm:\"%d minuti\",h:\"un'ora\",hh:\"%d ore\",d:\"un giorno\",dd:\"%d giorni\",w:\"una settimana\",ww:\"%d settimane\",M:\"un mese\",MM:\"%d mesi\",y:\"un anno\",yy:\"%d anni\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:1,doy:4}}),t.defineLocale(\"ja\",{eras:[{since:\"2019-05-01\",offset:1,name:\"令和\",narrow:\"㋿\",abbr:\"R\"},{since:\"1989-01-08\",until:\"2019-04-30\",offset:1,name:\"平成\",narrow:\"㍻\",abbr:\"H\"},{since:\"1926-12-25\",until:\"1989-01-07\",offset:1,name:\"昭和\",narrow:\"㍼\",abbr:\"S\"},{since:\"1912-07-30\",until:\"1926-12-24\",offset:1,name:\"大正\",narrow:\"㍽\",abbr:\"T\"},{since:\"1873-01-01\",until:\"1912-07-29\",offset:6,name:\"明治\",narrow:\"㍾\",abbr:\"M\"},{since:\"0001-01-01\",until:\"1873-12-31\",offset:1,name:\"西暦\",narrow:\"AD\",abbr:\"AD\"},{since:\"0000-12-31\",until:-1/0,offset:1,name:\"紀元前\",narrow:\"BC\",abbr:\"BC\"}],eraYearOrdinalRegex:/(\\u5143|\\d+)\\u5e74/,eraYearOrdinalParse:function(e,t){return\"元\"===t[1]?1:parseInt(t[1]||e,10)},months:\"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月\".split(\"_\"),monthsShort:\"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月\".split(\"_\"),weekdays:\"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日\".split(\"_\"),weekdaysShort:\"日_月_火_水_木_金_土\".split(\"_\"),weekdaysMin:\"日_月_火_水_木_金_土\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY/MM/DD\",LL:\"YYYY年M月D日\",LLL:\"YYYY年M月D日 HH:mm\",LLLL:\"YYYY年M月D日 dddd HH:mm\",l:\"YYYY/MM/DD\",ll:\"YYYY年M月D日\",lll:\"YYYY年M月D日 HH:mm\",llll:\"YYYY年M月D日(ddd) HH:mm\"},meridiemParse:/\\u5348\\u524d|\\u5348\\u5f8c/i,isPM:function(e){return\"午後\"===e},meridiem:function(e,t,n){return e<12?\"午前\":\"午後\"},calendar:{sameDay:\"[今日] LT\",nextDay:\"[明日] LT\",nextWeek:function(e){return e.week()!==this.week()?\"[来週]dddd LT\":\"dddd LT\"},lastDay:\"[昨日] LT\",lastWeek:function(e){return this.week()!==e.week()?\"[先週]dddd LT\":\"dddd LT\"},sameElse:\"L\"},dayOfMonthOrdinalParse:/\\d{1,2}\\u65e5/,ordinal:function(e,t){switch(t){case\"y\":return 1===e?\"元年\":e+\"年\";case\"d\":case\"D\":case\"DDD\":return e+\"日\";default:return e}},relativeTime:{future:\"%s後\",past:\"%s前\",s:\"数秒\",ss:\"%d秒\",m:\"1分\",mm:\"%d分\",h:\"1時間\",hh:\"%d時間\",d:\"1日\",dd:\"%d日\",M:\"1ヶ月\",MM:\"%dヶ月\",y:\"1年\",yy:\"%d年\"}}),t.defineLocale(\"jv\",{months:\"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_Nopember_Desember\".split(\"_\"),monthsShort:\"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nop_Des\".split(\"_\"),weekdays:\"Minggu_Senen_Seloso_Rebu_Kemis_Jemuwah_Septu\".split(\"_\"),weekdaysShort:\"Min_Sen_Sel_Reb_Kem_Jem_Sep\".split(\"_\"),weekdaysMin:\"Mg_Sn_Sl_Rb_Km_Jm_Sp\".split(\"_\"),longDateFormat:{LT:\"HH.mm\",LTS:\"HH.mm.ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY [pukul] HH.mm\",LLLL:\"dddd, D MMMM YYYY [pukul] HH.mm\"},meridiemParse:/enjing|siyang|sonten|ndalu/,meridiemHour:function(e,t){return 12===e&&(e=0),\"enjing\"===t?e:\"siyang\"===t?11<=e?e:e+12:\"sonten\"===t||\"ndalu\"===t?e+12:void 0},meridiem:function(e,t,n){return e<11?\"enjing\":e<15?\"siyang\":e<19?\"sonten\":\"ndalu\"},calendar:{sameDay:\"[Dinten puniko pukul] LT\",nextDay:\"[Mbenjang pukul] LT\",nextWeek:\"dddd [pukul] LT\",lastDay:\"[Kala wingi pukul] LT\",lastWeek:\"dddd [kepengker pukul] LT\",sameElse:\"L\"},relativeTime:{future:\"wonten ing %s\",past:\"%s ingkang kepengker\",s:\"sawetawis detik\",ss:\"%d detik\",m:\"setunggal menit\",mm:\"%d menit\",h:\"setunggal jam\",hh:\"%d jam\",d:\"sedinten\",dd:\"%d dinten\",M:\"sewulan\",MM:\"%d wulan\",y:\"setaun\",yy:\"%d taun\"},week:{dow:1,doy:7}}),t.defineLocale(\"ka\",{months:\"იანვარი_თებერვალი_მარტი_აპრილი_მაისი_ივნისი_ივლისი_აგვისტო_სექტემბერი_ოქტომბერი_ნოემბერი_დეკემბერი\".split(\"_\"),monthsShort:\"იან_თებ_მარ_აპრ_მაი_ივნ_ივლ_აგვ_სექ_ოქტ_ნოე_დეკ\".split(\"_\"),weekdays:{standalone:\"კვირა_ორშაბათი_სამშაბათი_ოთხშაბათი_ხუთშაბათი_პარასკევი_შაბათი\".split(\"_\"),format:\"კვირას_ორშაბათს_სამშაბათს_ოთხშაბათს_ხუთშაბათს_პარასკევს_შაბათს\".split(\"_\"),isFormat:/(\\u10ec\\u10d8\\u10dc\\u10d0|\\u10e8\\u10d4\\u10db\\u10d3\\u10d4\\u10d2)/},weekdaysShort:\"კვი_ორშ_სამ_ოთხ_ხუთ_პარ_შაბ\".split(\"_\"),weekdaysMin:\"კვ_ორ_სა_ოთ_ხუ_პა_შა\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[დღეს] LT[-ზე]\",nextDay:\"[ხვალ] LT[-ზე]\",lastDay:\"[გუშინ] LT[-ზე]\",nextWeek:\"[შემდეგ] dddd LT[-ზე]\",lastWeek:\"[წინა] dddd LT-ზე\",sameElse:\"L\"},relativeTime:{future:function(e){return e.replace(/(\\u10ec\\u10d0\\u10db|\\u10ec\\u10e3\\u10d7|\\u10e1\\u10d0\\u10d0\\u10d7|\\u10ec\\u10d4\\u10da|\\u10d3\\u10e6|\\u10d7\\u10d5)(\\u10d8|\\u10d4)/,function(e,t,n){return\"ი\"===n?t+\"ში\":t+n+\"ში\"})},past:function(e){return/(\\u10ec\\u10d0\\u10db\\u10d8|\\u10ec\\u10e3\\u10d7\\u10d8|\\u10e1\\u10d0\\u10d0\\u10d7\\u10d8|\\u10d3\\u10e6\\u10d4|\\u10d7\\u10d5\\u10d4)/.test(e)?e.replace(/(\\u10d8|\\u10d4)$/,\"ის წინ\"):/\\u10ec\\u10d4\\u10da\\u10d8/.test(e)?e.replace(/\\u10ec\\u10d4\\u10da\\u10d8$/,\"წლის წინ\"):e},s:\"რამდენიმე წამი\",ss:\"%d წამი\",m:\"წუთი\",mm:\"%d წუთი\",h:\"საათი\",hh:\"%d საათი\",d:\"დღე\",dd:\"%d დღე\",M:\"თვე\",MM:\"%d თვე\",y:\"წელი\",yy:\"%d წელი\"},dayOfMonthOrdinalParse:/0|1-\\u10da\\u10d8|\\u10db\\u10d4-\\d{1,2}|\\d{1,2}-\\u10d4/,ordinal:function(e){return 0===e?e:1===e?e+\"-ლი\":e<20||e<=100&&e%20==0||e%100==0?\"მე-\"+e:e+\"-ე\"},week:{dow:1,doy:7}});var Ar={0:\"-ші\",1:\"-ші\",2:\"-ші\",3:\"-ші\",4:\"-ші\",5:\"-ші\",6:\"-шы\",7:\"-ші\",8:\"-ші\",9:\"-шы\",10:\"-шы\",20:\"-шы\",30:\"-шы\",40:\"-шы\",50:\"-ші\",60:\"-шы\",70:\"-ші\",80:\"-ші\",90:\"-шы\",100:\"-ші\"},Or=(t.defineLocale(\"kk\",{months:\"қаңтар_ақпан_наурыз_сәуір_мамыр_маусым_шілде_тамыз_қыркүйек_қазан_қараша_желтоқсан\".split(\"_\"),monthsShort:\"қаң_ақп_нау_сәу_мам_мау_шіл_там_қыр_қаз_қар_жел\".split(\"_\"),weekdays:\"жексенбі_дүйсенбі_сейсенбі_сәрсенбі_бейсенбі_жұма_сенбі\".split(\"_\"),weekdaysShort:\"жек_дүй_сей_сәр_бей_жұм_сен\".split(\"_\"),weekdaysMin:\"жк_дй_сй_ср_бй_жм_сн\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Бүгін сағат] LT\",nextDay:\"[Ертең сағат] LT\",nextWeek:\"dddd [сағат] LT\",lastDay:\"[Кеше сағат] LT\",lastWeek:\"[Өткен аптаның] dddd [сағат] LT\",sameElse:\"L\"},relativeTime:{future:\"%s ішінде\",past:\"%s бұрын\",s:\"бірнеше секунд\",ss:\"%d секунд\",m:\"бір минут\",mm:\"%d минут\",h:\"бір сағат\",hh:\"%d сағат\",d:\"бір күн\",dd:\"%d күн\",M:\"бір ай\",MM:\"%d ай\",y:\"бір жыл\",yy:\"%d жыл\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0448\\u0456|\\u0448\\u044b)/,ordinal:function(e){return e+(Ar[e]||Ar[e%10]||Ar[100<=e?100:null])},week:{dow:1,doy:7}}),{1:\"១\",2:\"២\",3:\"៣\",4:\"៤\",5:\"៥\",6:\"៦\",7:\"៧\",8:\"៨\",9:\"៩\",0:\"០\"}),Cr={\"១\":\"1\",\"២\":\"2\",\"៣\":\"3\",\"៤\":\"4\",\"៥\":\"5\",\"៦\":\"6\",\"៧\":\"7\",\"៨\":\"8\",\"៩\":\"9\",\"០\":\"0\"},jr=(t.defineLocale(\"km\",{months:\"មករា_កុម្ភៈ_មីនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ\".split(\"_\"),monthsShort:\"មករា_កុម្ភៈ_មីនា_មេសា_ឧសភា_មិថុនា_កក្កដា_សីហា_កញ្ញា_តុលា_វិច្ឆិកា_ធ្នូ\".split(\"_\"),weekdays:\"អាទិត្យ_ច័ន្ទ_អង្គារ_ពុធ_ព្រហស្បតិ៍_សុក្រ_សៅរ៍\".split(\"_\"),weekdaysShort:\"អា_ច_អ_ព_ព្រ_សុ_ស\".split(\"_\"),weekdaysMin:\"អា_ច_អ_ព_ព្រ_សុ_ស\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},meridiemParse:/\\u1796\\u17d2\\u179a\\u17b9\\u1780|\\u179b\\u17d2\\u1784\\u17b6\\u1785/,isPM:function(e){return\"ល្ងាច\"===e},meridiem:function(e,t,n){return e<12?\"ព្រឹក\":\"ល្ងាច\"},calendar:{sameDay:\"[ថ្ងៃនេះ ម៉ោង] LT\",nextDay:\"[ស្អែក ម៉ោង] LT\",nextWeek:\"dddd [ម៉ោង] LT\",lastDay:\"[ម្សិលមិញ ម៉ោង] LT\",lastWeek:\"dddd [សប្តាហ៍មុន] [ម៉ោង] LT\",sameElse:\"L\"},relativeTime:{future:\"%sទៀត\",past:\"%sមុន\",s:\"ប៉ុន្មានវិនាទី\",ss:\"%d វិនាទី\",m:\"មួយនាទី\",mm:\"%d នាទី\",h:\"មួយម៉ោង\",hh:\"%d ម៉ោង\",d:\"មួយថ្ងៃ\",dd:\"%d ថ្ងៃ\",M:\"មួយខែ\",MM:\"%d ខែ\",y:\"មួយឆ្នាំ\",yy:\"%d ឆ្នាំ\"},dayOfMonthOrdinalParse:/\\u1791\\u17b8\\d{1,2}/,ordinal:\"ទី%d\",preparse:function(e){return e.replace(/[\\u17e1\\u17e2\\u17e3\\u17e4\\u17e5\\u17e6\\u17e7\\u17e8\\u17e9\\u17e0]/g,function(e){return Cr[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return Or[e]})},week:{dow:1,doy:4}}),{1:\"೧\",2:\"೨\",3:\"೩\",4:\"೪\",5:\"೫\",6:\"೬\",7:\"೭\",8:\"೮\",9:\"೯\",0:\"೦\"}),Pr={\"೧\":\"1\",\"೨\":\"2\",\"೩\":\"3\",\"೪\":\"4\",\"೫\":\"5\",\"೬\":\"6\",\"೭\":\"7\",\"೮\":\"8\",\"೯\":\"9\",\"೦\":\"0\"};function Hr(e,t,n,r){return e={s:[\"çend sanîye\",\"çend sanîyeyan\"],ss:[e+\" sanîye\",e+\" sanîyeyan\"],m:[\"deqîqeyek\",\"deqîqeyekê\"],mm:[e+\" deqîqe\",e+\" deqîqeyan\"],h:[\"saetek\",\"saetekê\"],hh:[e+\" saet\",e+\" saetan\"],d:[\"rojek\",\"rojekê\"],dd:[e+\" roj\",e+\" rojan\"],w:[\"hefteyek\",\"hefteyekê\"],ww:[e+\" hefte\",e+\" hefteyan\"],M:[\"mehek\",\"mehekê\"],MM:[e+\" meh\",e+\" mehan\"],y:[\"salek\",\"salekê\"],yy:[e+\" sal\",e+\" salan\"]},t?e[n][0]:e[n][1]}t.defineLocale(\"kn\",{months:\"ಜನವರಿ_ಫೆಬ್ರವರಿ_ಮಾರ್ಚ್_ಏಪ್ರಿಲ್_ಮೇ_ಜೂನ್_ಜುಲೈ_ಆಗಸ್ಟ್_ಸೆಪ್ಟೆಂಬರ್_ಅಕ್ಟೋಬರ್_ನವೆಂಬರ್_ಡಿಸೆಂಬರ್\".split(\"_\"),monthsShort:\"ಜನ_ಫೆಬ್ರ_ಮಾರ್ಚ್_ಏಪ್ರಿಲ್_ಮೇ_ಜೂನ್_ಜುಲೈ_ಆಗಸ್ಟ್_ಸೆಪ್ಟೆಂ_ಅಕ್ಟೋ_ನವೆಂ_ಡಿಸೆಂ\".split(\"_\"),monthsParseExact:!0,weekdays:\"ಭಾನುವಾರ_ಸೋಮವಾರ_ಮಂಗಳವಾರ_ಬುಧವಾರ_ಗುರುವಾರ_ಶುಕ್ರವಾರ_ಶನಿವಾರ\".split(\"_\"),weekdaysShort:\"ಭಾನು_ಸೋಮ_ಮಂಗಳ_ಬುಧ_ಗುರು_ಶುಕ್ರ_ಶನಿ\".split(\"_\"),weekdaysMin:\"ಭಾ_ಸೋ_ಮಂ_ಬು_ಗು_ಶು_ಶ\".split(\"_\"),longDateFormat:{LT:\"A h:mm\",LTS:\"A h:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm\",LLLL:\"dddd, D MMMM YYYY, A h:mm\"},calendar:{sameDay:\"[ಇಂದು] LT\",nextDay:\"[ನಾಳೆ] LT\",nextWeek:\"dddd, LT\",lastDay:\"[ನಿನ್ನೆ] LT\",lastWeek:\"[ಕೊನೆಯ] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s ನಂತರ\",past:\"%s ಹಿಂದೆ\",s:\"ಕೆಲವು ಕ್ಷಣಗಳು\",ss:\"%d ಸೆಕೆಂಡುಗಳು\",m:\"ಒಂದು ನಿಮಿಷ\",mm:\"%d ನಿಮಿಷ\",h:\"ಒಂದು ಗಂಟೆ\",hh:\"%d ಗಂಟೆ\",d:\"ಒಂದು ದಿನ\",dd:\"%d ದಿನ\",M:\"ಒಂದು ತಿಂಗಳು\",MM:\"%d ತಿಂಗಳು\",y:\"ಒಂದು ವರ್ಷ\",yy:\"%d ವರ್ಷ\"},preparse:function(e){return e.replace(/[\\u0ce7\\u0ce8\\u0ce9\\u0cea\\u0ceb\\u0cec\\u0ced\\u0cee\\u0cef\\u0ce6]/g,function(e){return Pr[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return jr[e]})},meridiemParse:/\\u0cb0\\u0cbe\\u0ca4\\u0ccd\\u0cb0\\u0cbf|\\u0cac\\u0cc6\\u0cb3\\u0cbf\\u0c97\\u0ccd\\u0c97\\u0cc6|\\u0cae\\u0ca7\\u0ccd\\u0caf\\u0cbe\\u0cb9\\u0ccd\\u0ca8|\\u0cb8\\u0c82\\u0c9c\\u0cc6/,meridiemHour:function(e,t){return 12===e&&(e=0),\"ರಾತ್ರಿ\"===t?e<4?e:e+12:\"ಬೆಳಿಗ್ಗೆ\"===t?e:\"ಮಧ್ಯಾಹ್ನ\"===t?10<=e?e:e+12:\"ಸಂಜೆ\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"ರಾತ್ರಿ\":e<10?\"ಬೆಳಿಗ್ಗೆ\":e<17?\"ಮಧ್ಯಾಹ್ನ\":e<20?\"ಸಂಜೆ\":\"ರಾತ್ರಿ\"},dayOfMonthOrdinalParse:/\\d{1,2}(\\u0ca8\\u0cc6\\u0cd5)/,ordinal:function(e){return e+\"ನೇ\"},week:{dow:0,doy:6}}),t.defineLocale(\"ko\",{months:\"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월\".split(\"_\"),monthsShort:\"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월\".split(\"_\"),weekdays:\"일요일_월요일_화요일_수요일_목요일_금요일_토요일\".split(\"_\"),weekdaysShort:\"일_월_화_수_목_금_토\".split(\"_\"),weekdaysMin:\"일_월_화_수_목_금_토\".split(\"_\"),longDateFormat:{LT:\"A h:mm\",LTS:\"A h:mm:ss\",L:\"YYYY.MM.DD.\",LL:\"YYYY년 MMMM D일\",LLL:\"YYYY년 MMMM D일 A h:mm\",LLLL:\"YYYY년 MMMM D일 dddd A h:mm\",l:\"YYYY.MM.DD.\",ll:\"YYYY년 MMMM D일\",lll:\"YYYY년 MMMM D일 A h:mm\",llll:\"YYYY년 MMMM D일 dddd A h:mm\"},calendar:{sameDay:\"오늘 LT\",nextDay:\"내일 LT\",nextWeek:\"dddd LT\",lastDay:\"어제 LT\",lastWeek:\"지난주 dddd LT\",sameElse:\"L\"},relativeTime:{future:\"%s 후\",past:\"%s 전\",s:\"몇 초\",ss:\"%d초\",m:\"1분\",mm:\"%d분\",h:\"한 시간\",hh:\"%d시간\",d:\"하루\",dd:\"%d일\",M:\"한 달\",MM:\"%d달\",y:\"일 년\",yy:\"%d년\"},dayOfMonthOrdinalParse:/\\d{1,2}(\\uc77c|\\uc6d4|\\uc8fc)/,ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"DDD\":return e+\"일\";case\"M\":return e+\"월\";case\"w\":case\"W\":return e+\"주\";default:return e}},meridiemParse:/\\uc624\\uc804|\\uc624\\ud6c4/,isPM:function(e){return\"오후\"===e},meridiem:function(e,t,n){return e<12?\"오전\":\"오후\"}}),t.defineLocale(\"ku-kmr\",{months:\"Rêbendan_Sibat_Adar_Nîsan_Gulan_Hezîran_Tîrmeh_Tebax_Îlon_Cotmeh_Mijdar_Berfanbar\".split(\"_\"),monthsShort:\"Rêb_Sib_Ada_Nîs_Gul_Hez_Tîr_Teb_Îlo_Cot_Mij_Ber\".split(\"_\"),monthsParseExact:!0,weekdays:\"Yekşem_Duşem_Sêşem_Çarşem_Pêncşem_În_Şemî\".split(\"_\"),weekdaysShort:\"Yek_Du_Sê_Çar_Pên_În_Şem\".split(\"_\"),weekdaysMin:\"Ye_Du_Sê_Ça_Pê_În_Şe\".split(\"_\"),meridiem:function(e,t,n){return e<12?n?\"bn\":\"BN\":n?\"pn\":\"PN\"},meridiemParse:/bn|BN|pn|PN/,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"Do MMMM[a] YYYY[an]\",LLL:\"Do MMMM[a] YYYY[an] HH:mm\",LLLL:\"dddd, Do MMMM[a] YYYY[an] HH:mm\",ll:\"Do MMM[.] YYYY[an]\",lll:\"Do MMM[.] YYYY[an] HH:mm\",llll:\"ddd[.], Do MMM[.] YYYY[an] HH:mm\"},calendar:{sameDay:\"[Îro di saet] LT [de]\",nextDay:\"[Sibê di saet] LT [de]\",nextWeek:\"dddd [di saet] LT [de]\",lastDay:\"[Duh di saet] LT [de]\",lastWeek:\"dddd[a borî di saet] LT [de]\",sameElse:\"L\"},relativeTime:{future:\"di %s de\",past:\"berî %s\",s:Hr,ss:Hr,m:Hr,mm:Hr,h:Hr,hh:Hr,d:Hr,dd:Hr,w:Hr,ww:Hr,M:Hr,MM:Hr,y:Hr,yy:Hr},dayOfMonthOrdinalParse:/\\d{1,2}(?:y\\xea|\\xea|\\.)/,ordinal:function(e,t){return(t=t.toLowerCase()).includes(\"w\")||t.includes(\"m\")?e+\".\":e+(e=(t=\"\"+(t=e)).substring(t.length-1),12==(t=1<t.length?t.substring(t.length-2):\"\")||13==t||\"2\"!=e&&\"3\"!=e&&\"50\"!=t&&\"70\"!=e&&\"80\"!=e?\"ê\":\"yê\")},week:{dow:1,doy:4}});var Ir={1:\"١\",2:\"٢\",3:\"٣\",4:\"٤\",5:\"٥\",6:\"٦\",7:\"٧\",8:\"٨\",9:\"٩\",0:\"٠\"},Fr={\"١\":\"1\",\"٢\":\"2\",\"٣\":\"3\",\"٤\":\"4\",\"٥\":\"5\",\"٦\":\"6\",\"٧\":\"7\",\"٨\":\"8\",\"٩\":\"9\",\"٠\":\"0\"},Nr=(J=[\"کانونی دووەم\",\"شوبات\",\"ئازار\",\"نیسان\",\"ئایار\",\"حوزەیران\",\"تەمموز\",\"ئاب\",\"ئەیلوول\",\"تشرینی یەكەم\",\"تشرینی دووەم\",\"كانونی یەکەم\"],t.defineLocale(\"ku\",{months:J,monthsShort:J,weekdays:\"یه‌كشه‌ممه‌_دووشه‌ممه‌_سێشه‌ممه‌_چوارشه‌ممه‌_پێنجشه‌ممه‌_هه‌ینی_شه‌ممه‌\".split(\"_\"),weekdaysShort:\"یه‌كشه‌م_دووشه‌م_سێشه‌م_چوارشه‌م_پێنجشه‌م_هه‌ینی_شه‌ممه‌\".split(\"_\"),weekdaysMin:\"ی_د_س_چ_پ_ه_ش\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},meridiemParse:/\\u0626\\u06ce\\u0648\\u0627\\u0631\\u0647\\u200c|\\u0628\\u0647\\u200c\\u06cc\\u0627\\u0646\\u06cc/,isPM:function(e){return/\\u0626\\u06ce\\u0648\\u0627\\u0631\\u0647\\u200c/.test(e)},meridiem:function(e,t,n){return e<12?\"به‌یانی\":\"ئێواره‌\"},calendar:{sameDay:\"[ئه‌مرۆ كاتژمێر] LT\",nextDay:\"[به‌یانی كاتژمێر] LT\",nextWeek:\"dddd [كاتژمێر] LT\",lastDay:\"[دوێنێ كاتژمێر] LT\",lastWeek:\"dddd [كاتژمێر] LT\",sameElse:\"L\"},relativeTime:{future:\"له‌ %s\",past:\"%s\",s:\"چه‌ند چركه‌یه‌ك\",ss:\"چركه‌ %d\",m:\"یه‌ك خوله‌ك\",mm:\"%d خوله‌ك\",h:\"یه‌ك كاتژمێر\",hh:\"%d كاتژمێر\",d:\"یه‌ك ڕۆژ\",dd:\"%d ڕۆژ\",M:\"یه‌ك مانگ\",MM:\"%d مانگ\",y:\"یه‌ك ساڵ\",yy:\"%d ساڵ\"},preparse:function(e){return e.replace(/[\\u0661\\u0662\\u0663\\u0664\\u0665\\u0666\\u0667\\u0668\\u0669\\u0660]/g,function(e){return Fr[e]}).replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/\\d/g,function(e){return Ir[e]}).replace(/,/g,\"،\")},week:{dow:6,doy:12}}),{0:\"-чү\",1:\"-чи\",2:\"-чи\",3:\"-чү\",4:\"-чү\",5:\"-чи\",6:\"-чы\",7:\"-чи\",8:\"-чи\",9:\"-чу\",10:\"-чу\",20:\"-чы\",30:\"-чу\",40:\"-чы\",50:\"-чү\",60:\"-чы\",70:\"-чи\",80:\"-чи\",90:\"-чу\",100:\"-чү\"});function Rr(e,t,n,r){var i={m:[\"eng Minutt\",\"enger Minutt\"],h:[\"eng Stonn\",\"enger Stonn\"],d:[\"een Dag\",\"engem Dag\"],M:[\"ee Mount\",\"engem Mount\"],y:[\"ee Joer\",\"engem Joer\"]};return t?i[n][0]:i[n][1]}function Vr(e){if(e=parseInt(e,10),isNaN(e))return!1;if(e<0)return!0;if(e<10)return 4<=e&&e<=7;var t;if(e<100)return Vr(0==(t=e%10)?e/10:t);if(e<1e4){for(;10<=e;)e/=10;return Vr(e)}return Vr(e/=1e3)}t.defineLocale(\"ky\",{months:\"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь\".split(\"_\"),monthsShort:\"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек\".split(\"_\"),weekdays:\"Жекшемби_Дүйшөмбү_Шейшемби_Шаршемби_Бейшемби_Жума_Ишемби\".split(\"_\"),weekdaysShort:\"Жек_Дүй_Шей_Шар_Бей_Жум_Ише\".split(\"_\"),weekdaysMin:\"Жк_Дй_Шй_Шр_Бй_Жм_Иш\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Бүгүн саат] LT\",nextDay:\"[Эртең саат] LT\",nextWeek:\"dddd [саат] LT\",lastDay:\"[Кечээ саат] LT\",lastWeek:\"[Өткөн аптанын] dddd [күнү] [саат] LT\",sameElse:\"L\"},relativeTime:{future:\"%s ичинде\",past:\"%s мурун\",s:\"бирнече секунд\",ss:\"%d секунд\",m:\"бир мүнөт\",mm:\"%d мүнөт\",h:\"бир саат\",hh:\"%d саат\",d:\"бир күн\",dd:\"%d күн\",M:\"бир ай\",MM:\"%d ай\",y:\"бир жыл\",yy:\"%d жыл\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0447\\u0438|\\u0447\\u044b|\\u0447\\u04af|\\u0447\\u0443)/,ordinal:function(e){return e+(Nr[e]||Nr[e%10]||Nr[100<=e?100:null])},week:{dow:1,doy:7}}),t.defineLocale(\"lb\",{months:\"Januar_Februar_Mäerz_Abrëll_Mee_Juni_Juli_August_September_Oktober_November_Dezember\".split(\"_\"),monthsShort:\"Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.\".split(\"_\"),monthsParseExact:!0,weekdays:\"Sonndeg_Méindeg_Dënschdeg_Mëttwoch_Donneschdeg_Freideg_Samschdeg\".split(\"_\"),weekdaysShort:\"So._Mé._Dë._Më._Do._Fr._Sa.\".split(\"_\"),weekdaysMin:\"So_Mé_Dë_Më_Do_Fr_Sa\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm [Auer]\",LTS:\"H:mm:ss [Auer]\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY H:mm [Auer]\",LLLL:\"dddd, D. MMMM YYYY H:mm [Auer]\"},calendar:{sameDay:\"[Haut um] LT\",sameElse:\"L\",nextDay:\"[Muer um] LT\",nextWeek:\"dddd [um] LT\",lastDay:\"[Gëschter um] LT\",lastWeek:function(){switch(this.day()){case 2:case 4:return\"[Leschten] dddd [um] LT\";default:return\"[Leschte] dddd [um] LT\"}}},relativeTime:{future:function(e){return Vr(e.substr(0,e.indexOf(\" \")))?\"a \"+e:\"an \"+e},past:function(e){return Vr(e.substr(0,e.indexOf(\" \")))?\"viru \"+e:\"virun \"+e},s:\"e puer Sekonnen\",ss:\"%d Sekonnen\",m:Rr,mm:\"%d Minutten\",h:Rr,hh:\"%d Stonnen\",d:Rr,dd:\"%d Deeg\",M:Rr,MM:\"%d Méint\",y:Rr,yy:\"%d Joer\"},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"lo\",{months:\"ມັງກອນ_ກຸມພາ_ມີນາ_ເມສາ_ພຶດສະພາ_ມິຖຸນາ_ກໍລະກົດ_ສິງຫາ_ກັນຍາ_ຕຸລາ_ພະຈິກ_ທັນວາ\".split(\"_\"),monthsShort:\"ມັງກອນ_ກຸມພາ_ມີນາ_ເມສາ_ພຶດສະພາ_ມິຖຸນາ_ກໍລະກົດ_ສິງຫາ_ກັນຍາ_ຕຸລາ_ພະຈິກ_ທັນວາ\".split(\"_\"),weekdays:\"ອາທິດ_ຈັນ_ອັງຄານ_ພຸດ_ພະຫັດ_ສຸກ_ເສົາ\".split(\"_\"),weekdaysShort:\"ທິດ_ຈັນ_ອັງຄານ_ພຸດ_ພະຫັດ_ສຸກ_ເສົາ\".split(\"_\"),weekdaysMin:\"ທ_ຈ_ອຄ_ພ_ພຫ_ສກ_ສ\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"ວັນdddd D MMMM YYYY HH:mm\"},meridiemParse:/\\u0e95\\u0ead\\u0e99\\u0ec0\\u0e8a\\u0ebb\\u0ec9\\u0eb2|\\u0e95\\u0ead\\u0e99\\u0ec1\\u0ea5\\u0e87/,isPM:function(e){return\"ຕອນແລງ\"===e},meridiem:function(e,t,n){return e<12?\"ຕອນເຊົ້າ\":\"ຕອນແລງ\"},calendar:{sameDay:\"[ມື້ນີ້ເວລາ] LT\",nextDay:\"[ມື້ອື່ນເວລາ] LT\",nextWeek:\"[ວັນ]dddd[ໜ້າເວລາ] LT\",lastDay:\"[ມື້ວານນີ້ເວລາ] LT\",lastWeek:\"[ວັນ]dddd[ແລ້ວນີ້ເວລາ] LT\",sameElse:\"L\"},relativeTime:{future:\"ອີກ %s\",past:\"%sຜ່ານມາ\",s:\"ບໍ່ເທົ່າໃດວິນາທີ\",ss:\"%d ວິນາທີ\",m:\"1 ນາທີ\",mm:\"%d ນາທີ\",h:\"1 ຊົ່ວໂມງ\",hh:\"%d ຊົ່ວໂມງ\",d:\"1 ມື້\",dd:\"%d ມື້\",M:\"1 ເດືອນ\",MM:\"%d ເດືອນ\",y:\"1 ປີ\",yy:\"%d ປີ\"},dayOfMonthOrdinalParse:/(\\u0e97\\u0eb5\\u0ec8)\\d{1,2}/,ordinal:function(e){return\"ທີ່\"+e}});var $r={ss:\"sekundė_sekundžių_sekundes\",m:\"minutė_minutės_minutę\",mm:\"minutės_minučių_minutes\",h:\"valanda_valandos_valandą\",hh:\"valandos_valandų_valandas\",d:\"diena_dienos_dieną\",dd:\"dienos_dienų_dienas\",M:\"mėnuo_mėnesio_mėnesį\",MM:\"mėnesiai_mėnesių_mėnesius\",y:\"metai_metų_metus\",yy:\"metai_metų_metus\"};function zr(e,t,n,r){return t?Ur(n)[0]:r?Ur(n)[1]:Ur(n)[2]}function Wr(e){return e%10==0||10<e&&e<20}function Ur(e){return $r[e].split(\"_\")}function Br(e,t,n,r){var i=e+\" \";return 1===e?i+zr(0,t,n[0],r):t?i+(Wr(e)?Ur(n)[1]:Ur(n)[0]):r?i+Ur(n)[1]:i+(Wr(e)?Ur(n)[1]:Ur(n)[2])}t.defineLocale(\"lt\",{months:{format:\"sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio\".split(\"_\"),standalone:\"sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis\".split(\"_\"),isFormat:/D[oD]?(\\[[^\\[\\]]*\\]|\\s)+MMMM?|MMMM?(\\[[^\\[\\]]*\\]|\\s)+D[oD]?/},monthsShort:\"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd\".split(\"_\"),weekdays:{format:\"sekmadienį_pirmadienį_antradienį_trečiadienį_ketvirtadienį_penktadienį_šeštadienį\".split(\"_\"),standalone:\"sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis\".split(\"_\"),isFormat:/dddd HH:mm/},weekdaysShort:\"Sek_Pir_Ant_Tre_Ket_Pen_Šeš\".split(\"_\"),weekdaysMin:\"S_P_A_T_K_Pn_Š\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY-MM-DD\",LL:\"YYYY [m.] MMMM D [d.]\",LLL:\"YYYY [m.] MMMM D [d.], HH:mm [val.]\",LLLL:\"YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]\",l:\"YYYY-MM-DD\",ll:\"YYYY [m.] MMMM D [d.]\",lll:\"YYYY [m.] MMMM D [d.], HH:mm [val.]\",llll:\"YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]\"},calendar:{sameDay:\"[Šiandien] LT\",nextDay:\"[Rytoj] LT\",nextWeek:\"dddd LT\",lastDay:\"[Vakar] LT\",lastWeek:\"[Praėjusį] dddd LT\",sameElse:\"L\"},relativeTime:{future:\"po %s\",past:\"prieš %s\",s:function(e,t,n,r){return t?\"kelios sekundės\":r?\"kelių sekundžių\":\"kelias sekundes\"},ss:Br,m:zr,mm:Br,h:zr,hh:Br,d:zr,dd:Br,M:zr,MM:Br,y:zr,yy:Br},dayOfMonthOrdinalParse:/\\d{1,2}-oji/,ordinal:function(e){return e+\"-oji\"},week:{dow:1,doy:4}});var qr={ss:\"sekundes_sekundēm_sekunde_sekundes\".split(\"_\"),m:\"minūtes_minūtēm_minūte_minūtes\".split(\"_\"),mm:\"minūtes_minūtēm_minūte_minūtes\".split(\"_\"),h:\"stundas_stundām_stunda_stundas\".split(\"_\"),hh:\"stundas_stundām_stunda_stundas\".split(\"_\"),d:\"dienas_dienām_diena_dienas\".split(\"_\"),dd:\"dienas_dienām_diena_dienas\".split(\"_\"),M:\"mēneša_mēnešiem_mēnesis_mēneši\".split(\"_\"),MM:\"mēneša_mēnešiem_mēnesis_mēneši\".split(\"_\"),y:\"gada_gadiem_gads_gadi\".split(\"_\"),yy:\"gada_gadiem_gads_gadi\".split(\"_\")};function Gr(e,t,n){return n?t%10==1&&t%100!=11?e[2]:e[3]:t%10==1&&t%100!=11?e[0]:e[1]}function Jr(e,t,n){return e+\" \"+Gr(qr[n],e,t)}function Zr(e,t,n){return Gr(qr[n],e,t)}t.defineLocale(\"lv\",{months:\"janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris\".split(\"_\"),monthsShort:\"jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec\".split(\"_\"),weekdays:\"svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena\".split(\"_\"),weekdaysShort:\"Sv_P_O_T_C_Pk_S\".split(\"_\"),weekdaysMin:\"Sv_P_O_T_C_Pk_S\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY.\",LL:\"YYYY. [gada] D. MMMM\",LLL:\"YYYY. [gada] D. MMMM, HH:mm\",LLLL:\"YYYY. [gada] D. MMMM, dddd, HH:mm\"},calendar:{sameDay:\"[Šodien pulksten] LT\",nextDay:\"[Rīt pulksten] LT\",nextWeek:\"dddd [pulksten] LT\",lastDay:\"[Vakar pulksten] LT\",lastWeek:\"[Pagājušā] dddd [pulksten] LT\",sameElse:\"L\"},relativeTime:{future:\"pēc %s\",past:\"pirms %s\",s:function(e,t){return t?\"dažas sekundes\":\"dažām sekundēm\"},ss:Jr,m:Zr,mm:Jr,h:Zr,hh:Jr,d:Zr,dd:Jr,M:Zr,MM:Jr,y:Zr,yy:Jr},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}});var Kr={words:{ss:[\"sekund\",\"sekunda\",\"sekundi\"],m:[\"jedan minut\",\"jednog minuta\"],mm:[\"minut\",\"minuta\",\"minuta\"],h:[\"jedan sat\",\"jednog sata\"],hh:[\"sat\",\"sata\",\"sati\"],dd:[\"dan\",\"dana\",\"dana\"],MM:[\"mjesec\",\"mjeseca\",\"mjeseci\"],yy:[\"godina\",\"godine\",\"godina\"]},correctGrammaticalCase:function(e,t){return 1===e?t[0]:2<=e&&e<=4?t[1]:t[2]},translate:function(e,t,n){var r=Kr.words[n];return 1===n.length?t?r[0]:r[1]:e+\" \"+Kr.correctGrammaticalCase(e,r)}};function Xr(e,t,n,r){switch(n){case\"s\":return t?\"хэдхэн секунд\":\"хэдхэн секундын\";case\"ss\":return e+(t?\" секунд\":\" секундын\");case\"m\":case\"mm\":return e+(t?\" минут\":\" минутын\");case\"h\":case\"hh\":return e+(t?\" цаг\":\" цагийн\");case\"d\":case\"dd\":return e+(t?\" өдөр\":\" өдрийн\");case\"M\":case\"MM\":return e+(t?\" сар\":\" сарын\");case\"y\":case\"yy\":return e+(t?\" жил\":\" жилийн\");default:return e}}t.defineLocale(\"me\",{months:\"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar\".split(\"_\"),monthsShort:\"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.\".split(\"_\"),monthsParseExact:!0,weekdays:\"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota\".split(\"_\"),weekdaysShort:\"ned._pon._uto._sri._čet._pet._sub.\".split(\"_\"),weekdaysMin:\"ne_po_ut_sr_če_pe_su\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY H:mm\",LLLL:\"dddd, D. MMMM YYYY H:mm\"},calendar:{sameDay:\"[danas u] LT\",nextDay:\"[sjutra u] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[u] [nedjelju] [u] LT\";case 3:return\"[u] [srijedu] [u] LT\";case 6:return\"[u] [subotu] [u] LT\";case 1:case 2:case 4:case 5:return\"[u] dddd [u] LT\"}},lastDay:\"[juče u] LT\",lastWeek:function(){return[\"[prošle] [nedjelje] [u] LT\",\"[prošlog] [ponedjeljka] [u] LT\",\"[prošlog] [utorka] [u] LT\",\"[prošle] [srijede] [u] LT\",\"[prošlog] [četvrtka] [u] LT\",\"[prošlog] [petka] [u] LT\",\"[prošle] [subote] [u] LT\"][this.day()]},sameElse:\"L\"},relativeTime:{future:\"za %s\",past:\"prije %s\",s:\"nekoliko sekundi\",ss:Kr.translate,m:Kr.translate,mm:Kr.translate,h:Kr.translate,hh:Kr.translate,d:\"dan\",dd:Kr.translate,M:\"mjesec\",MM:Kr.translate,y:\"godinu\",yy:Kr.translate},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:7}}),t.defineLocale(\"mi\",{months:\"Kohi-tāte_Hui-tanguru_Poutū-te-rangi_Paenga-whāwhā_Haratua_Pipiri_Hōngoingoi_Here-turi-kōkā_Mahuru_Whiringa-ā-nuku_Whiringa-ā-rangi_Hakihea\".split(\"_\"),monthsShort:\"Kohi_Hui_Pou_Pae_Hara_Pipi_Hōngoi_Here_Mahu_Whi-nu_Whi-ra_Haki\".split(\"_\"),monthsRegex:/(?:['a-z\\u0101\\u014D\\u016B]+\\-?){1,3}/i,monthsStrictRegex:/(?:['a-z\\u0101\\u014D\\u016B]+\\-?){1,3}/i,monthsShortRegex:/(?:['a-z\\u0101\\u014D\\u016B]+\\-?){1,3}/i,monthsShortStrictRegex:/(?:['a-z\\u0101\\u014D\\u016B]+\\-?){1,2}/i,weekdays:\"Rātapu_Mane_Tūrei_Wenerei_Tāite_Paraire_Hātarei\".split(\"_\"),weekdaysShort:\"Ta_Ma_Tū_We_Tāi_Pa_Hā\".split(\"_\"),weekdaysMin:\"Ta_Ma_Tū_We_Tāi_Pa_Hā\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY [i] HH:mm\",LLLL:\"dddd, D MMMM YYYY [i] HH:mm\"},calendar:{sameDay:\"[i teie mahana, i] LT\",nextDay:\"[apopo i] LT\",nextWeek:\"dddd [i] LT\",lastDay:\"[inanahi i] LT\",lastWeek:\"dddd [whakamutunga i] LT\",sameElse:\"L\"},relativeTime:{future:\"i roto i %s\",past:\"%s i mua\",s:\"te hēkona ruarua\",ss:\"%d hēkona\",m:\"he meneti\",mm:\"%d meneti\",h:\"te haora\",hh:\"%d haora\",d:\"he ra\",dd:\"%d ra\",M:\"he marama\",MM:\"%d marama\",y:\"he tau\",yy:\"%d tau\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:1,doy:4}}),t.defineLocale(\"mk\",{months:\"јануари_февруари_март_април_мај_јуни_јули_август_септември_октомври_ноември_декември\".split(\"_\"),monthsShort:\"јан_фев_мар_апр_мај_јун_јул_авг_сеп_окт_ное_дек\".split(\"_\"),weekdays:\"недела_понеделник_вторник_среда_четврток_петок_сабота\".split(\"_\"),weekdaysShort:\"нед_пон_вто_сре_чет_пет_саб\".split(\"_\"),weekdaysMin:\"нe_пo_вт_ср_че_пе_сa\".split(\"_\"),longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"D.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY H:mm\",LLLL:\"dddd, D MMMM YYYY H:mm\"},calendar:{sameDay:\"[Денес во] LT\",nextDay:\"[Утре во] LT\",nextWeek:\"[Во] dddd [во] LT\",lastDay:\"[Вчера во] LT\",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return\"[Изминатата] dddd [во] LT\";case 1:case 2:case 4:case 5:return\"[Изминатиот] dddd [во] LT\"}},sameElse:\"L\"},relativeTime:{future:\"за %s\",past:\"пред %s\",s:\"неколку секунди\",ss:\"%d секунди\",m:\"една минута\",mm:\"%d минути\",h:\"еден час\",hh:\"%d часа\",d:\"еден ден\",dd:\"%d дена\",M:\"еден месец\",MM:\"%d месеци\",y:\"една година\",yy:\"%d години\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0435\\u0432|\\u0435\\u043d|\\u0442\\u0438|\\u0432\\u0438|\\u0440\\u0438|\\u043c\\u0438)/,ordinal:function(e){var t=e%10,n=e%100;return 0===e?e+\"-ев\":0==n?e+\"-ен\":10<n&&n<20?e+\"-ти\":1==t?e+\"-ви\":2==t?e+\"-ри\":7==t||8==t?e+\"-ми\":e+\"-ти\"},week:{dow:1,doy:7}}),t.defineLocale(\"ml\",{months:\"ജനുവരി_ഫെബ്രുവരി_മാർച്ച്_ഏപ്രിൽ_മേയ്_ജൂൺ_ജൂലൈ_ഓഗസ്റ്റ്_സെപ്റ്റംബർ_ഒക്ടോബർ_നവംബർ_ഡിസംബർ\".split(\"_\"),monthsShort:\"ജനു._ഫെബ്രു._മാർ._ഏപ്രി._മേയ്_ജൂൺ_ജൂലൈ._ഓഗ._സെപ്റ്റ._ഒക്ടോ._നവം._ഡിസം.\".split(\"_\"),monthsParseExact:!0,weekdays:\"ഞായറാഴ്ച_തിങ്കളാഴ്ച_ചൊവ്വാഴ്ച_ബുധനാഴ്ച_വ്യാഴാഴ്ച_വെള്ളിയാഴ്ച_ശനിയാഴ്ച\".split(\"_\"),weekdaysShort:\"ഞായർ_തിങ്കൾ_ചൊവ്വ_ബുധൻ_വ്യാഴം_വെള്ളി_ശനി\".split(\"_\"),weekdaysMin:\"ഞാ_തി_ചൊ_ബു_വ്യാ_വെ_ശ\".split(\"_\"),longDateFormat:{LT:\"A h:mm -നു\",LTS:\"A h:mm:ss -നു\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm -നു\",LLLL:\"dddd, D MMMM YYYY, A h:mm -നു\"},calendar:{sameDay:\"[ഇന്ന്] LT\",nextDay:\"[നാളെ] LT\",nextWeek:\"dddd, LT\",lastDay:\"[ഇന്നലെ] LT\",lastWeek:\"[കഴിഞ്ഞ] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s കഴിഞ്ഞ്\",past:\"%s മുൻപ്\",s:\"അൽപ നിമിഷങ്ങൾ\",ss:\"%d സെക്കൻഡ്\",m:\"ഒരു മിനിറ്റ്\",mm:\"%d മിനിറ്റ്\",h:\"ഒരു മണിക്കൂർ\",hh:\"%d മണിക്കൂർ\",d:\"ഒരു ദിവസം\",dd:\"%d ദിവസം\",M:\"ഒരു മാസം\",MM:\"%d മാസം\",y:\"ഒരു വർഷം\",yy:\"%d വർഷം\"},meridiemParse:/\\u0d30\\u0d3e\\u0d24\\u0d4d\\u0d30\\u0d3f|\\u0d30\\u0d3e\\u0d35\\u0d3f\\u0d32\\u0d46|\\u0d09\\u0d1a\\u0d4d\\u0d1a \\u0d15\\u0d34\\u0d3f\\u0d1e\\u0d4d\\u0d1e\\u0d4d|\\u0d35\\u0d48\\u0d15\\u0d41\\u0d28\\u0d4d\\u0d28\\u0d47\\u0d30\\u0d02|\\u0d30\\u0d3e\\u0d24\\u0d4d\\u0d30\\u0d3f/i,meridiemHour:function(e,t){return 12===e&&(e=0),\"രാത്രി\"===t&&4<=e||\"ഉച്ച കഴിഞ്ഞ്\"===t||\"വൈകുന്നേരം\"===t?e+12:e},meridiem:function(e,t,n){return e<4?\"രാത്രി\":e<12?\"രാവിലെ\":e<17?\"ഉച്ച കഴിഞ്ഞ്\":e<20?\"വൈകുന്നേരം\":\"രാത്രി\"}}),t.defineLocale(\"mn\",{months:\"Нэгдүгээр сар_Хоёрдугаар сар_Гуравдугаар сар_Дөрөвдүгээр сар_Тавдугаар сар_Зургадугаар сар_Долдугаар сар_Наймдугаар сар_Есдүгээр сар_Аравдугаар сар_Арван нэгдүгээр сар_Арван хоёрдугаар сар\".split(\"_\"),monthsShort:\"1 сар_2 сар_3 сар_4 сар_5 сар_6 сар_7 сар_8 сар_9 сар_10 сар_11 сар_12 сар\".split(\"_\"),monthsParseExact:!0,weekdays:\"Ням_Даваа_Мягмар_Лхагва_Пүрэв_Баасан_Бямба\".split(\"_\"),weekdaysShort:\"Ням_Дав_Мяг_Лха_Пүр_Баа_Бям\".split(\"_\"),weekdaysMin:\"Ня_Да_Мя_Лх_Пү_Ба_Бя\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY-MM-DD\",LL:\"YYYY оны MMMMын D\",LLL:\"YYYY оны MMMMын D HH:mm\",LLLL:\"dddd, YYYY оны MMMMын D HH:mm\"},meridiemParse:/\\u04ae\\u04e8|\\u04ae\\u0425/i,isPM:function(e){return\"ҮХ\"===e},meridiem:function(e,t,n){return e<12?\"ҮӨ\":\"ҮХ\"},calendar:{sameDay:\"[Өнөөдөр] LT\",nextDay:\"[Маргааш] LT\",nextWeek:\"[Ирэх] dddd LT\",lastDay:\"[Өчигдөр] LT\",lastWeek:\"[Өнгөрсөн] dddd LT\",sameElse:\"L\"},relativeTime:{future:\"%s дараа\",past:\"%s өмнө\",s:Xr,ss:Xr,m:Xr,mm:Xr,h:Xr,hh:Xr,d:Xr,dd:Xr,M:Xr,MM:Xr,y:Xr,yy:Xr},dayOfMonthOrdinalParse:/\\d{1,2} \\u04e9\\u0434\\u04e9\\u0440/,ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"DDD\":return e+\" өдөр\";default:return e}}});var Qr={1:\"१\",2:\"२\",3:\"३\",4:\"४\",5:\"५\",6:\"६\",7:\"७\",8:\"८\",9:\"९\",0:\"०\"},ei={\"१\":\"1\",\"२\":\"2\",\"३\":\"3\",\"४\":\"4\",\"५\":\"5\",\"६\":\"6\",\"७\":\"7\",\"८\":\"8\",\"९\":\"9\",\"०\":\"0\"};function ti(e,t,n,r){var i=\"\";if(t)switch(n){case\"s\":i=\"काही सेकंद\";break;case\"ss\":i=\"%d सेकंद\";break;case\"m\":i=\"एक मिनिट\";break;case\"mm\":i=\"%d मिनिटे\";break;case\"h\":i=\"एक तास\";break;case\"hh\":i=\"%d तास\";break;case\"d\":i=\"एक दिवस\";break;case\"dd\":i=\"%d दिवस\";break;case\"M\":i=\"एक महिना\";break;case\"MM\":i=\"%d महिने\";break;case\"y\":i=\"एक वर्ष\";break;case\"yy\":i=\"%d वर्षे\"}else switch(n){case\"s\":i=\"काही सेकंदां\";break;case\"ss\":i=\"%d सेकंदां\";break;case\"m\":i=\"एका मिनिटा\";break;case\"mm\":i=\"%d मिनिटां\";break;case\"h\":i=\"एका तासा\";break;case\"hh\":i=\"%d तासां\";break;case\"d\":i=\"एका दिवसा\";break;case\"dd\":i=\"%d दिवसां\";break;case\"M\":i=\"एका महिन्या\";break;case\"MM\":i=\"%d महिन्यां\";break;case\"y\":i=\"एका वर्षा\";break;case\"yy\":i=\"%d वर्षां\"}return i.replace(/%d/i,e)}t.defineLocale(\"mr\",{months:\"जानेवारी_फेब्रुवारी_मार्च_एप्रिल_मे_जून_जुलै_ऑगस्ट_सप्टेंबर_ऑक्टोबर_नोव्हेंबर_डिसेंबर\".split(\"_\"),monthsShort:\"जाने._फेब्रु._मार्च._एप्रि._मे._जून._जुलै._ऑग._सप्टें._ऑक्टो._नोव्हें._डिसें.\".split(\"_\"),monthsParseExact:!0,weekdays:\"रविवार_सोमवार_मंगळवार_बुधवार_गुरूवार_शुक्रवार_शनिवार\".split(\"_\"),weekdaysShort:\"रवि_सोम_मंगळ_बुध_गुरू_शुक्र_शनि\".split(\"_\"),weekdaysMin:\"र_सो_मं_बु_गु_शु_श\".split(\"_\"),longDateFormat:{LT:\"A h:mm वाजता\",LTS:\"A h:mm:ss वाजता\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm वाजता\",LLLL:\"dddd, D MMMM YYYY, A h:mm वाजता\"},calendar:{sameDay:\"[आज] LT\",nextDay:\"[उद्या] LT\",nextWeek:\"dddd, LT\",lastDay:\"[काल] LT\",lastWeek:\"[मागील] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%sमध्ये\",past:\"%sपूर्वी\",s:ti,ss:ti,m:ti,mm:ti,h:ti,hh:ti,d:ti,dd:ti,M:ti,MM:ti,y:ti,yy:ti},preparse:function(e){return e.replace(/[\\u0967\\u0968\\u0969\\u096a\\u096b\\u096c\\u096d\\u096e\\u096f\\u0966]/g,function(e){return ei[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return Qr[e]})},meridiemParse:/\\u092a\\u0939\\u093e\\u091f\\u0947|\\u0938\\u0915\\u093e\\u0933\\u0940|\\u0926\\u0941\\u092a\\u093e\\u0930\\u0940|\\u0938\\u093e\\u092f\\u0902\\u0915\\u093e\\u0933\\u0940|\\u0930\\u093e\\u0924\\u094d\\u0930\\u0940/,meridiemHour:function(e,t){return 12===e&&(e=0),\"पहाटे\"===t||\"सकाळी\"===t?e:\"दुपारी\"===t||\"सायंकाळी\"===t||\"रात्री\"===t?12<=e?e:e+12:void 0},meridiem:function(e,t,n){return 0<=e&&e<6?\"पहाटे\":e<12?\"सकाळी\":e<17?\"दुपारी\":e<20?\"सायंकाळी\":\"रात्री\"},week:{dow:0,doy:6}}),t.defineLocale(\"ms-my\",{months:\"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember\".split(\"_\"),monthsShort:\"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis\".split(\"_\"),weekdays:\"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu\".split(\"_\"),weekdaysShort:\"Ahd_Isn_Sel_Rab_Kha_Jum_Sab\".split(\"_\"),weekdaysMin:\"Ah_Is_Sl_Rb_Km_Jm_Sb\".split(\"_\"),longDateFormat:{LT:\"HH.mm\",LTS:\"HH.mm.ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY [pukul] HH.mm\",LLLL:\"dddd, D MMMM YYYY [pukul] HH.mm\"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(e,t){return 12===e&&(e=0),\"pagi\"===t?e:\"tengahari\"===t?11<=e?e:e+12:\"petang\"===t||\"malam\"===t?e+12:void 0},meridiem:function(e,t,n){return e<11?\"pagi\":e<15?\"tengahari\":e<19?\"petang\":\"malam\"},calendar:{sameDay:\"[Hari ini pukul] LT\",nextDay:\"[Esok pukul] LT\",nextWeek:\"dddd [pukul] LT\",lastDay:\"[Kelmarin pukul] LT\",lastWeek:\"dddd [lepas pukul] LT\",sameElse:\"L\"},relativeTime:{future:\"dalam %s\",past:\"%s yang lepas\",s:\"beberapa saat\",ss:\"%d saat\",m:\"seminit\",mm:\"%d minit\",h:\"sejam\",hh:\"%d jam\",d:\"sehari\",dd:\"%d hari\",M:\"sebulan\",MM:\"%d bulan\",y:\"setahun\",yy:\"%d tahun\"},week:{dow:1,doy:7}}),t.defineLocale(\"ms\",{months:\"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember\".split(\"_\"),monthsShort:\"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis\".split(\"_\"),weekdays:\"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu\".split(\"_\"),weekdaysShort:\"Ahd_Isn_Sel_Rab_Kha_Jum_Sab\".split(\"_\"),weekdaysMin:\"Ah_Is_Sl_Rb_Km_Jm_Sb\".split(\"_\"),longDateFormat:{LT:\"HH.mm\",LTS:\"HH.mm.ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY [pukul] HH.mm\",LLLL:\"dddd, D MMMM YYYY [pukul] HH.mm\"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(e,t){return 12===e&&(e=0),\"pagi\"===t?e:\"tengahari\"===t?11<=e?e:e+12:\"petang\"===t||\"malam\"===t?e+12:void 0},meridiem:function(e,t,n){return e<11?\"pagi\":e<15?\"tengahari\":e<19?\"petang\":\"malam\"},calendar:{sameDay:\"[Hari ini pukul] LT\",nextDay:\"[Esok pukul] LT\",nextWeek:\"dddd [pukul] LT\",lastDay:\"[Kelmarin pukul] LT\",lastWeek:\"dddd [lepas pukul] LT\",sameElse:\"L\"},relativeTime:{future:\"dalam %s\",past:\"%s yang lepas\",s:\"beberapa saat\",ss:\"%d saat\",m:\"seminit\",mm:\"%d minit\",h:\"sejam\",hh:\"%d jam\",d:\"sehari\",dd:\"%d hari\",M:\"sebulan\",MM:\"%d bulan\",y:\"setahun\",yy:\"%d tahun\"},week:{dow:1,doy:7}}),t.defineLocale(\"mt\",{months:\"Jannar_Frar_Marzu_April_Mejju_Ġunju_Lulju_Awwissu_Settembru_Ottubru_Novembru_Diċembru\".split(\"_\"),monthsShort:\"Jan_Fra_Mar_Apr_Mej_Ġun_Lul_Aww_Set_Ott_Nov_Diċ\".split(\"_\"),weekdays:\"Il-Ħadd_It-Tnejn_It-Tlieta_L-Erbgħa_Il-Ħamis_Il-Ġimgħa_Is-Sibt\".split(\"_\"),weekdaysShort:\"Ħad_Tne_Tli_Erb_Ħam_Ġim_Sib\".split(\"_\"),weekdaysMin:\"Ħa_Tn_Tl_Er_Ħa_Ġi_Si\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Illum fil-]LT\",nextDay:\"[Għada fil-]LT\",nextWeek:\"dddd [fil-]LT\",lastDay:\"[Il-bieraħ fil-]LT\",lastWeek:\"dddd [li għadda] [fil-]LT\",sameElse:\"L\"},relativeTime:{future:\"f’ %s\",past:\"%s ilu\",s:\"ftit sekondi\",ss:\"%d sekondi\",m:\"minuta\",mm:\"%d minuti\",h:\"siegħa\",hh:\"%d siegħat\",d:\"ġurnata\",dd:\"%d ġranet\",M:\"xahar\",MM:\"%d xhur\",y:\"sena\",yy:\"%d sni\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:1,doy:4}});var ni={1:\"၁\",2:\"၂\",3:\"၃\",4:\"၄\",5:\"၅\",6:\"၆\",7:\"၇\",8:\"၈\",9:\"၉\",0:\"၀\"},ri={\"၁\":\"1\",\"၂\":\"2\",\"၃\":\"3\",\"၄\":\"4\",\"၅\":\"5\",\"၆\":\"6\",\"၇\":\"7\",\"၈\":\"8\",\"၉\":\"9\",\"၀\":\"0\"},ii=(t.defineLocale(\"my\",{months:\"ဇန်နဝါရီ_ဖေဖော်ဝါရီ_မတ်_ဧပြီ_မေ_ဇွန်_ဇူလိုင်_သြဂုတ်_စက်တင်ဘာ_အောက်တိုဘာ_နိုဝင်ဘာ_ဒီဇင်ဘာ\".split(\"_\"),monthsShort:\"ဇန်_ဖေ_မတ်_ပြီ_မေ_ဇွန်_လိုင်_သြ_စက်_အောက်_နို_ဒီ\".split(\"_\"),weekdays:\"တနင်္ဂနွေ_တနင်္လာ_အင်္ဂါ_ဗုဒ္ဓဟူး_ကြာသပတေး_သောကြာ_စနေ\".split(\"_\"),weekdaysShort:\"နွေ_လာ_ဂါ_ဟူး_ကြာ_သော_နေ\".split(\"_\"),weekdaysMin:\"နွေ_လာ_ဂါ_ဟူး_ကြာ_သော_နေ\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[ယနေ.] LT [မှာ]\",nextDay:\"[မနက်ဖြန်] LT [မှာ]\",nextWeek:\"dddd LT [မှာ]\",lastDay:\"[မနေ.က] LT [မှာ]\",lastWeek:\"[ပြီးခဲ့သော] dddd LT [မှာ]\",sameElse:\"L\"},relativeTime:{future:\"လာမည့် %s မှာ\",past:\"လွန်ခဲ့သော %s က\",s:\"စက္ကန်.အနည်းငယ်\",ss:\"%d စက္ကန့်\",m:\"တစ်မိနစ်\",mm:\"%d မိနစ်\",h:\"တစ်နာရီ\",hh:\"%d နာရီ\",d:\"တစ်ရက်\",dd:\"%d ရက်\",M:\"တစ်လ\",MM:\"%d လ\",y:\"တစ်နှစ်\",yy:\"%d နှစ်\"},preparse:function(e){return e.replace(/[\\u1041\\u1042\\u1043\\u1044\\u1045\\u1046\\u1047\\u1048\\u1049\\u1040]/g,function(e){return ri[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return ni[e]})},week:{dow:1,doy:4}}),t.defineLocale(\"nb\",{months:\"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember\".split(\"_\"),monthsShort:\"jan._feb._mars_apr._mai_juni_juli_aug._sep._okt._nov._des.\".split(\"_\"),monthsParseExact:!0,weekdays:\"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag\".split(\"_\"),weekdaysShort:\"sø._ma._ti._on._to._fr._lø.\".split(\"_\"),weekdaysMin:\"sø_ma_ti_on_to_fr_lø\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY [kl.] HH:mm\",LLLL:\"dddd D. MMMM YYYY [kl.] HH:mm\"},calendar:{sameDay:\"[i dag kl.] LT\",nextDay:\"[i morgen kl.] LT\",nextWeek:\"dddd [kl.] LT\",lastDay:\"[i går kl.] LT\",lastWeek:\"[forrige] dddd [kl.] LT\",sameElse:\"L\"},relativeTime:{future:\"om %s\",past:\"%s siden\",s:\"noen sekunder\",ss:\"%d sekunder\",m:\"ett minutt\",mm:\"%d minutter\",h:\"én time\",hh:\"%d timer\",d:\"én dag\",dd:\"%d dager\",w:\"én uke\",ww:\"%d uker\",M:\"én måned\",MM:\"%d måneder\",y:\"ett år\",yy:\"%d år\"},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),{1:\"१\",2:\"२\",3:\"३\",4:\"४\",5:\"५\",6:\"६\",7:\"७\",8:\"८\",9:\"९\",0:\"०\"}),ai={\"१\":\"1\",\"२\":\"2\",\"३\":\"3\",\"४\":\"4\",\"५\":\"5\",\"६\":\"6\",\"७\":\"7\",\"८\":\"8\",\"९\":\"9\",\"०\":\"0\"},oi=(t.defineLocale(\"ne\",{months:\"जनवरी_फेब्रुवरी_मार्च_अप्रिल_मई_जुन_जुलाई_अगष्ट_सेप्टेम्बर_अक्टोबर_नोभेम्बर_डिसेम्बर\".split(\"_\"),monthsShort:\"जन._फेब्रु._मार्च_अप्रि._मई_जुन_जुलाई._अग._सेप्ट._अक्टो._नोभे._डिसे.\".split(\"_\"),monthsParseExact:!0,weekdays:\"आइतबार_सोमबार_मङ्गलबार_बुधबार_बिहिबार_शुक्रबार_शनिबार\".split(\"_\"),weekdaysShort:\"आइत._सोम._मङ्गल._बुध._बिहि._शुक्र._शनि.\".split(\"_\"),weekdaysMin:\"आ._सो._मं._बु._बि._शु._श.\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"Aको h:mm बजे\",LTS:\"Aको h:mm:ss बजे\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, Aको h:mm बजे\",LLLL:\"dddd, D MMMM YYYY, Aको h:mm बजे\"},preparse:function(e){return e.replace(/[\\u0967\\u0968\\u0969\\u096a\\u096b\\u096c\\u096d\\u096e\\u096f\\u0966]/g,function(e){return ai[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return ii[e]})},meridiemParse:/\\u0930\\u093e\\u0924\\u093f|\\u092c\\u093f\\u0939\\u093e\\u0928|\\u0926\\u093f\\u0909\\u0901\\u0938\\u094b|\\u0938\\u093e\\u0901\\u091d/,meridiemHour:function(e,t){return 12===e&&(e=0),\"राति\"===t?e<4?e:e+12:\"बिहान\"===t?e:\"दिउँसो\"===t?10<=e?e:e+12:\"साँझ\"===t?e+12:void 0},meridiem:function(e,t,n){return e<3?\"राति\":e<12?\"बिहान\":e<16?\"दिउँसो\":e<20?\"साँझ\":\"राति\"},calendar:{sameDay:\"[आज] LT\",nextDay:\"[भोलि] LT\",nextWeek:\"[आउँदो] dddd[,] LT\",lastDay:\"[हिजो] LT\",lastWeek:\"[गएको] dddd[,] LT\",sameElse:\"L\"},relativeTime:{future:\"%sमा\",past:\"%s अगाडि\",s:\"केही क्षण\",ss:\"%d सेकेण्ड\",m:\"एक मिनेट\",mm:\"%d मिनेट\",h:\"एक घण्टा\",hh:\"%d घण्टा\",d:\"एक दिन\",dd:\"%d दिन\",M:\"एक महिना\",MM:\"%d महिना\",y:\"एक बर्ष\",yy:\"%d बर्ष\"},week:{dow:0,doy:6}}),\"jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.\".split(\"_\")),si=\"jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec\".split(\"_\"),li=(G=[/^jan/i,/^feb/i,/^(maart|mrt\\.?)$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],Ae=/^(januari|februari|maart|april|mei|ju[nl]i|augustus|september|oktober|november|december|jan\\.?|feb\\.?|mrt\\.?|apr\\.?|ju[nl]\\.?|aug\\.?|sep\\.?|okt\\.?|nov\\.?|dec\\.?)/i,t.defineLocale(\"nl-be\",{months:\"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december\".split(\"_\"),monthsShort:function(e,t){return e?(/-MMM-/.test(t)?si:oi)[e.month()]:oi},monthsRegex:Ae,monthsShortRegex:Ae,monthsStrictRegex:/^(januari|februari|maart|april|mei|ju[nl]i|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\\.?|feb\\.?|mrt\\.?|apr\\.?|mei|ju[nl]\\.?|aug\\.?|sep\\.?|okt\\.?|nov\\.?|dec\\.?)/i,monthsParse:G,longMonthsParse:G,shortMonthsParse:G,weekdays:\"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag\".split(\"_\"),weekdaysShort:\"zo._ma._di._wo._do._vr._za.\".split(\"_\"),weekdaysMin:\"zo_ma_di_wo_do_vr_za\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[vandaag om] LT\",nextDay:\"[morgen om] LT\",nextWeek:\"dddd [om] LT\",lastDay:\"[gisteren om] LT\",lastWeek:\"[afgelopen] dddd [om] LT\",sameElse:\"L\"},relativeTime:{future:\"over %s\",past:\"%s geleden\",s:\"een paar seconden\",ss:\"%d seconden\",m:\"één minuut\",mm:\"%d minuten\",h:\"één uur\",hh:\"%d uur\",d:\"één dag\",dd:\"%d dagen\",M:\"één maand\",MM:\"%d maanden\",y:\"één jaar\",yy:\"%d jaar\"},dayOfMonthOrdinalParse:/\\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||20<=e?\"ste\":\"de\")},week:{dow:1,doy:4}}),\"jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.\".split(\"_\")),ui=\"jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec\".split(\"_\"),di=(z=[/^jan/i,/^feb/i,/^(maart|mrt\\.?)$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],q=/^(januari|februari|maart|april|mei|ju[nl]i|augustus|september|oktober|november|december|jan\\.?|feb\\.?|mrt\\.?|apr\\.?|ju[nl]\\.?|aug\\.?|sep\\.?|okt\\.?|nov\\.?|dec\\.?)/i,t.defineLocale(\"nl\",{months:\"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december\".split(\"_\"),monthsShort:function(e,t){return e?(/-MMM-/.test(t)?ui:li)[e.month()]:li},monthsRegex:q,monthsShortRegex:q,monthsStrictRegex:/^(januari|februari|maart|april|mei|ju[nl]i|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\\.?|feb\\.?|mrt\\.?|apr\\.?|mei|ju[nl]\\.?|aug\\.?|sep\\.?|okt\\.?|nov\\.?|dec\\.?)/i,monthsParse:z,longMonthsParse:z,shortMonthsParse:z,weekdays:\"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag\".split(\"_\"),weekdaysShort:\"zo._ma._di._wo._do._vr._za.\".split(\"_\"),weekdaysMin:\"zo_ma_di_wo_do_vr_za\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD-MM-YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[vandaag om] LT\",nextDay:\"[morgen om] LT\",nextWeek:\"dddd [om] LT\",lastDay:\"[gisteren om] LT\",lastWeek:\"[afgelopen] dddd [om] LT\",sameElse:\"L\"},relativeTime:{future:\"over %s\",past:\"%s geleden\",s:\"een paar seconden\",ss:\"%d seconden\",m:\"één minuut\",mm:\"%d minuten\",h:\"één uur\",hh:\"%d uur\",d:\"één dag\",dd:\"%d dagen\",w:\"één week\",ww:\"%d weken\",M:\"één maand\",MM:\"%d maanden\",y:\"één jaar\",yy:\"%d jaar\"},dayOfMonthOrdinalParse:/\\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||20<=e?\"ste\":\"de\")},week:{dow:1,doy:4}}),t.defineLocale(\"nn\",{months:\"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember\".split(\"_\"),monthsShort:\"jan._feb._mars_apr._mai_juni_juli_aug._sep._okt._nov._des.\".split(\"_\"),monthsParseExact:!0,weekdays:\"sundag_måndag_tysdag_onsdag_torsdag_fredag_laurdag\".split(\"_\"),weekdaysShort:\"su._må._ty._on._to._fr._lau.\".split(\"_\"),weekdaysMin:\"su_må_ty_on_to_fr_la\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY [kl.] H:mm\",LLLL:\"dddd D. MMMM YYYY [kl.] HH:mm\"},calendar:{sameDay:\"[I dag klokka] LT\",nextDay:\"[I morgon klokka] LT\",nextWeek:\"dddd [klokka] LT\",lastDay:\"[I går klokka] LT\",lastWeek:\"[Føregåande] dddd [klokka] LT\",sameElse:\"L\"},relativeTime:{future:\"om %s\",past:\"%s sidan\",s:\"nokre sekund\",ss:\"%d sekund\",m:\"eit minutt\",mm:\"%d minutt\",h:\"ein time\",hh:\"%d timar\",d:\"ein dag\",dd:\"%d dagar\",w:\"ei veke\",ww:\"%d veker\",M:\"ein månad\",MM:\"%d månader\",y:\"eit år\",yy:\"%d år\"},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"oc-lnc\",{months:{standalone:\"genièr_febrièr_març_abril_mai_junh_julhet_agost_setembre_octòbre_novembre_decembre\".split(\"_\"),format:\"de genièr_de febrièr_de març_d'abril_de mai_de junh_de julhet_d'agost_de setembre_d'octòbre_de novembre_de decembre\".split(\"_\"),isFormat:/D[oD]?(\\s)+MMMM/},monthsShort:\"gen._febr._març_abr._mai_junh_julh._ago._set._oct._nov._dec.\".split(\"_\"),monthsParseExact:!0,weekdays:\"dimenge_diluns_dimars_dimècres_dijòus_divendres_dissabte\".split(\"_\"),weekdaysShort:\"dg._dl._dm._dc._dj._dv._ds.\".split(\"_\"),weekdaysMin:\"dg_dl_dm_dc_dj_dv_ds\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM [de] YYYY\",ll:\"D MMM YYYY\",LLL:\"D MMMM [de] YYYY [a] H:mm\",lll:\"D MMM YYYY, H:mm\",LLLL:\"dddd D MMMM [de] YYYY [a] H:mm\",llll:\"ddd D MMM YYYY, H:mm\"},calendar:{sameDay:\"[uèi a] LT\",nextDay:\"[deman a] LT\",nextWeek:\"dddd [a] LT\",lastDay:\"[ièr a] LT\",lastWeek:\"dddd [passat a] LT\",sameElse:\"L\"},relativeTime:{future:\"d'aquí %s\",past:\"fa %s\",s:\"unas segondas\",ss:\"%d segondas\",m:\"una minuta\",mm:\"%d minutas\",h:\"una ora\",hh:\"%d oras\",d:\"un jorn\",dd:\"%d jorns\",M:\"un mes\",MM:\"%d meses\",y:\"un an\",yy:\"%d ans\"},dayOfMonthOrdinalParse:/\\d{1,2}(r|n|t|\\xe8|a)/,ordinal:function(e,t){return e+(\"w\"!==t&&\"W\"!==t?1===e?\"r\":2===e?\"n\":3===e?\"r\":4===e?\"t\":\"è\":\"a\")},week:{dow:1,doy:4}}),{1:\"੧\",2:\"੨\",3:\"੩\",4:\"੪\",5:\"੫\",6:\"੬\",7:\"੭\",8:\"੮\",9:\"੯\",0:\"੦\"}),ci={\"੧\":\"1\",\"੨\":\"2\",\"੩\":\"3\",\"੪\":\"4\",\"੫\":\"5\",\"੬\":\"6\",\"੭\":\"7\",\"੮\":\"8\",\"੯\":\"9\",\"੦\":\"0\"},fi=(t.defineLocale(\"pa-in\",{months:\"ਜਨਵਰੀ_ਫ਼ਰਵਰੀ_ਮਾਰਚ_ਅਪ੍ਰੈਲ_ਮਈ_ਜੂਨ_ਜੁਲਾਈ_ਅਗਸਤ_ਸਤੰਬਰ_ਅਕਤੂਬਰ_ਨਵੰਬਰ_ਦਸੰਬਰ\".split(\"_\"),monthsShort:\"ਜਨਵਰੀ_ਫ਼ਰਵਰੀ_ਮਾਰਚ_ਅਪ੍ਰੈਲ_ਮਈ_ਜੂਨ_ਜੁਲਾਈ_ਅਗਸਤ_ਸਤੰਬਰ_ਅਕਤੂਬਰ_ਨਵੰਬਰ_ਦਸੰਬਰ\".split(\"_\"),weekdays:\"ਐਤਵਾਰ_ਸੋਮਵਾਰ_ਮੰਗਲਵਾਰ_ਬੁਧਵਾਰ_ਵੀਰਵਾਰ_ਸ਼ੁੱਕਰਵਾਰ_ਸ਼ਨੀਚਰਵਾਰ\".split(\"_\"),weekdaysShort:\"ਐਤ_ਸੋਮ_ਮੰਗਲ_ਬੁਧ_ਵੀਰ_ਸ਼ੁਕਰ_ਸ਼ਨੀ\".split(\"_\"),weekdaysMin:\"ਐਤ_ਸੋਮ_ਮੰਗਲ_ਬੁਧ_ਵੀਰ_ਸ਼ੁਕਰ_ਸ਼ਨੀ\".split(\"_\"),longDateFormat:{LT:\"A h:mm ਵਜੇ\",LTS:\"A h:mm:ss ਵਜੇ\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm ਵਜੇ\",LLLL:\"dddd, D MMMM YYYY, A h:mm ਵਜੇ\"},calendar:{sameDay:\"[ਅਜ] LT\",nextDay:\"[ਕਲ] LT\",nextWeek:\"[ਅਗਲਾ] dddd, LT\",lastDay:\"[ਕਲ] LT\",lastWeek:\"[ਪਿਛਲੇ] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s ਵਿੱਚ\",past:\"%s ਪਿਛਲੇ\",s:\"ਕੁਝ ਸਕਿੰਟ\",ss:\"%d ਸਕਿੰਟ\",m:\"ਇਕ ਮਿੰਟ\",mm:\"%d ਮਿੰਟ\",h:\"ਇੱਕ ਘੰਟਾ\",hh:\"%d ਘੰਟੇ\",d:\"ਇੱਕ ਦਿਨ\",dd:\"%d ਦਿਨ\",M:\"ਇੱਕ ਮਹੀਨਾ\",MM:\"%d ਮਹੀਨੇ\",y:\"ਇੱਕ ਸਾਲ\",yy:\"%d ਸਾਲ\"},preparse:function(e){return e.replace(/[\\u0a67\\u0a68\\u0a69\\u0a6a\\u0a6b\\u0a6c\\u0a6d\\u0a6e\\u0a6f\\u0a66]/g,function(e){return ci[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return di[e]})},meridiemParse:/\\u0a30\\u0a3e\\u0a24|\\u0a38\\u0a35\\u0a47\\u0a30|\\u0a26\\u0a41\\u0a2a\\u0a39\\u0a3f\\u0a30|\\u0a38\\u0a3c\\u0a3e\\u0a2e/,meridiemHour:function(e,t){return 12===e&&(e=0),\"ਰਾਤ\"===t?e<4?e:e+12:\"ਸਵੇਰ\"===t?e:\"ਦੁਪਹਿਰ\"===t?10<=e?e:e+12:\"ਸ਼ਾਮ\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"ਰਾਤ\":e<10?\"ਸਵੇਰ\":e<17?\"ਦੁਪਹਿਰ\":e<20?\"ਸ਼ਾਮ\":\"ਰਾਤ\"},week:{dow:0,doy:6}}),\"styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień\".split(\"_\")),pi=\"stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia\".split(\"_\");Be=[/^sty/i,/^lut/i,/^mar/i,/^kwi/i,/^maj/i,/^cze/i,/^lip/i,/^sie/i,/^wrz/i,/^pa\\u017a/i,/^lis/i,/^gru/i];function hi(e){return e%10<5&&1<e%10&&~~(e/10)%10!=1}function mi(e,t,n){var r=e+\" \";switch(n){case\"ss\":return r+(hi(e)?\"sekundy\":\"sekund\");case\"m\":return t?\"minuta\":\"minutę\";case\"mm\":return r+(hi(e)?\"minuty\":\"minut\");case\"h\":return t?\"godzina\":\"godzinę\";case\"hh\":return r+(hi(e)?\"godziny\":\"godzin\");case\"ww\":return r+(hi(e)?\"tygodnie\":\"tygodni\");case\"MM\":return r+(hi(e)?\"miesiące\":\"miesięcy\");case\"yy\":return r+(hi(e)?\"lata\":\"lat\")}}function vi(e,t,n){return e+(20<=e%100||100<=e&&e%100==0?\" de \":\" \")+{ss:\"secunde\",mm:\"minute\",hh:\"ore\",dd:\"zile\",ww:\"săptămâni\",MM:\"luni\",yy:\"ani\"}[n]}function gi(e,t,n){return\"m\"===n?t?\"минута\":\"минуту\":e+\" \"+(e=+e,t=(t={ss:t?\"секунда_секунды_секунд\":\"секунду_секунды_секунд\",mm:t?\"минута_минуты_минут\":\"минуту_минуты_минут\",hh:\"час_часа_часов\",dd:\"день_дня_дней\",ww:\"неделя_недели_недель\",MM:\"месяц_месяца_месяцев\",yy:\"год_года_лет\"}[n]).split(\"_\"),e%10==1&&e%100!=11?t[0]:2<=e%10&&e%10<=4&&(e%100<10||20<=e%100)?t[1]:t[2])}function _i(e){return 1<e&&e<5}function yi(e,t,n,r){var i=e+\" \";switch(n){case\"s\":return t||r?\"pár sekúnd\":\"pár sekundami\";case\"ss\":return t||r?i+(_i(e)?\"sekundy\":\"sekúnd\"):i+\"sekundami\";case\"m\":return t?\"minúta\":r?\"minútu\":\"minútou\";case\"mm\":return t||r?i+(_i(e)?\"minúty\":\"minút\"):i+\"minútami\";case\"h\":return t?\"hodina\":r?\"hodinu\":\"hodinou\";case\"hh\":return t||r?i+(_i(e)?\"hodiny\":\"hodín\"):i+\"hodinami\";case\"d\":return t||r?\"deň\":\"dňom\";case\"dd\":return t||r?i+(_i(e)?\"dni\":\"dní\"):i+\"dňami\";case\"M\":return t||r?\"mesiac\":\"mesiacom\";case\"MM\":return t||r?i+(_i(e)?\"mesiace\":\"mesiacov\"):i+\"mesiacmi\";case\"y\":return t||r?\"rok\":\"rokom\";case\"yy\":return t||r?i+(_i(e)?\"roky\":\"rokov\"):i+\"rokmi\"}}function bi(e,t,n,r){var i=e+\" \";switch(n){case\"s\":return t||r?\"nekaj sekund\":\"nekaj sekundami\";case\"ss\":return i+(1===e?t?\"sekundo\":\"sekundi\":2===e?t||r?\"sekundi\":\"sekundah\":e<5?t||r?\"sekunde\":\"sekundah\":\"sekund\");case\"m\":return t?\"ena minuta\":\"eno minuto\";case\"mm\":return i+(1===e?t?\"minuta\":\"minuto\":2===e?t||r?\"minuti\":\"minutama\":e<5?t||r?\"minute\":\"minutami\":t||r?\"minut\":\"minutami\");case\"h\":return t?\"ena ura\":\"eno uro\";case\"hh\":return i+(1===e?t?\"ura\":\"uro\":2===e?t||r?\"uri\":\"urama\":e<5?t||r?\"ure\":\"urami\":t||r?\"ur\":\"urami\");case\"d\":return t||r?\"en dan\":\"enim dnem\";case\"dd\":return i+(1===e?t||r?\"dan\":\"dnem\":2===e?t||r?\"dni\":\"dnevoma\":t||r?\"dni\":\"dnevi\");case\"M\":return t||r?\"en mesec\":\"enim mesecem\";case\"MM\":return i+(1===e?t||r?\"mesec\":\"mesecem\":2===e?t||r?\"meseca\":\"mesecema\":e<5?t||r?\"mesece\":\"meseci\":t||r?\"mesecev\":\"meseci\");case\"y\":return t||r?\"eno leto\":\"enim letom\";case\"yy\":return i+(1===e?t||r?\"leto\":\"letom\":2===e?t||r?\"leti\":\"letoma\":e<5?t||r?\"leta\":\"leti\":t||r?\"let\":\"leti\")}}t.defineLocale(\"pl\",{months:function(e,t){return e?(/D MMMM/.test(t)?pi:fi)[e.month()]:fi},monthsShort:\"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru\".split(\"_\"),monthsParse:Be,longMonthsParse:Be,shortMonthsParse:Be,weekdays:\"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota\".split(\"_\"),weekdaysShort:\"ndz_pon_wt_śr_czw_pt_sob\".split(\"_\"),weekdaysMin:\"Nd_Pn_Wt_Śr_Cz_Pt_So\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Dziś o] LT\",nextDay:\"[Jutro o] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[W niedzielę o] LT\";case 2:return\"[We wtorek o] LT\";case 3:return\"[W środę o] LT\";case 6:return\"[W sobotę o] LT\";default:return\"[W] dddd [o] LT\"}},lastDay:\"[Wczoraj o] LT\",lastWeek:function(){switch(this.day()){case 0:return\"[W zeszłą niedzielę o] LT\";case 3:return\"[W zeszłą środę o] LT\";case 6:return\"[W zeszłą sobotę o] LT\";default:return\"[W zeszły] dddd [o] LT\"}},sameElse:\"L\"},relativeTime:{future:\"za %s\",past:\"%s temu\",s:\"kilka sekund\",ss:mi,m:mi,mm:mi,h:mi,hh:mi,d:\"1 dzień\",dd:\"%d dni\",w:\"tydzień\",ww:mi,M:\"miesiąc\",MM:mi,y:\"rok\",yy:mi},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"pt-br\",{months:\"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro\".split(\"_\"),monthsShort:\"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez\".split(\"_\"),weekdays:\"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado\".split(\"_\"),weekdaysShort:\"dom_seg_ter_qua_qui_sex_sáb\".split(\"_\"),weekdaysMin:\"do_2ª_3ª_4ª_5ª_6ª_sá\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D [de] MMMM [de] YYYY\",LLL:\"D [de] MMMM [de] YYYY [às] HH:mm\",LLLL:\"dddd, D [de] MMMM [de] YYYY [às] HH:mm\"},calendar:{sameDay:\"[Hoje às] LT\",nextDay:\"[Amanhã às] LT\",nextWeek:\"dddd [às] LT\",lastDay:\"[Ontem às] LT\",lastWeek:function(){return 0===this.day()||6===this.day()?\"[Último] dddd [às] LT\":\"[Última] dddd [às] LT\"},sameElse:\"L\"},relativeTime:{future:\"em %s\",past:\"há %s\",s:\"poucos segundos\",ss:\"%d segundos\",m:\"um minuto\",mm:\"%d minutos\",h:\"uma hora\",hh:\"%d horas\",d:\"um dia\",dd:\"%d dias\",M:\"um mês\",MM:\"%d meses\",y:\"um ano\",yy:\"%d anos\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",invalidDate:\"Data inválida\"}),t.defineLocale(\"pt\",{months:\"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro\".split(\"_\"),monthsShort:\"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez\".split(\"_\"),weekdays:\"Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado\".split(\"_\"),weekdaysShort:\"Dom_Seg_Ter_Qua_Qui_Sex_Sáb\".split(\"_\"),weekdaysMin:\"Do_2ª_3ª_4ª_5ª_6ª_Sá\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D [de] MMMM [de] YYYY\",LLL:\"D [de] MMMM [de] YYYY HH:mm\",LLLL:\"dddd, D [de] MMMM [de] YYYY HH:mm\"},calendar:{sameDay:\"[Hoje às] LT\",nextDay:\"[Amanhã às] LT\",nextWeek:\"dddd [às] LT\",lastDay:\"[Ontem às] LT\",lastWeek:function(){return 0===this.day()||6===this.day()?\"[Último] dddd [às] LT\":\"[Última] dddd [às] LT\"},sameElse:\"L\"},relativeTime:{future:\"em %s\",past:\"há %s\",s:\"segundos\",ss:\"%d segundos\",m:\"um minuto\",mm:\"%d minutos\",h:\"uma hora\",hh:\"%d horas\",d:\"um dia\",dd:\"%d dias\",w:\"uma semana\",ww:\"%d semanas\",M:\"um mês\",MM:\"%d meses\",y:\"um ano\",yy:\"%d anos\"},dayOfMonthOrdinalParse:/\\d{1,2}\\xba/,ordinal:\"%dº\",week:{dow:1,doy:4}}),t.defineLocale(\"ro\",{months:\"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie\".split(\"_\"),monthsShort:\"ian._feb._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.\".split(\"_\"),monthsParseExact:!0,weekdays:\"duminică_luni_marți_miercuri_joi_vineri_sâmbătă\".split(\"_\"),weekdaysShort:\"Dum_Lun_Mar_Mie_Joi_Vin_Sâm\".split(\"_\"),weekdaysMin:\"Du_Lu_Ma_Mi_Jo_Vi_Sâ\".split(\"_\"),longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY H:mm\",LLLL:\"dddd, D MMMM YYYY H:mm\"},calendar:{sameDay:\"[azi la] LT\",nextDay:\"[mâine la] LT\",nextWeek:\"dddd [la] LT\",lastDay:\"[ieri la] LT\",lastWeek:\"[fosta] dddd [la] LT\",sameElse:\"L\"},relativeTime:{future:\"peste %s\",past:\"%s în urmă\",s:\"câteva secunde\",ss:vi,m:\"un minut\",mm:vi,h:\"o oră\",hh:vi,d:\"o zi\",dd:vi,w:\"o săptămână\",ww:vi,M:\"o lună\",MM:vi,y:\"un an\",yy:vi},week:{dow:1,doy:7}}),W=[/^\\u044f\\u043d\\u0432/i,/^\\u0444\\u0435\\u0432/i,/^\\u043c\\u0430\\u0440/i,/^\\u0430\\u043f\\u0440/i,/^\\u043c\\u0430[\\u0439\\u044f]/i,/^\\u0438\\u044e\\u043d/i,/^\\u0438\\u044e\\u043b/i,/^\\u0430\\u0432\\u0433/i,/^\\u0441\\u0435\\u043d/i,/^\\u043e\\u043a\\u0442/i,/^\\u043d\\u043e\\u044f/i,/^\\u0434\\u0435\\u043a/i],t.defineLocale(\"ru\",{months:{format:\"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря\".split(\"_\"),standalone:\"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь\".split(\"_\")},monthsShort:{format:\"янв._февр._мар._апр._мая_июня_июля_авг._сент._окт._нояб._дек.\".split(\"_\"),standalone:\"янв._февр._март_апр._май_июнь_июль_авг._сент._окт._нояб._дек.\".split(\"_\")},weekdays:{standalone:\"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота\".split(\"_\"),format:\"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу\".split(\"_\"),isFormat:/\\[ ?[\\u0412\\u0432] ?(?:\\u043f\\u0440\\u043e\\u0448\\u043b\\u0443\\u044e|\\u0441\\u043b\\u0435\\u0434\\u0443\\u044e\\u0449\\u0443\\u044e|\\u044d\\u0442\\u0443)? ?] ?dddd/},weekdaysShort:\"вс_пн_вт_ср_чт_пт_сб\".split(\"_\"),weekdaysMin:\"вс_пн_вт_ср_чт_пт_сб\".split(\"_\"),monthsParse:W,longMonthsParse:W,shortMonthsParse:W,monthsRegex:/^(\\u044f\\u043d\\u0432\\u0430\\u0440[\\u044c\\u044f]|\\u044f\\u043d\\u0432\\.?|\\u0444\\u0435\\u0432\\u0440\\u0430\\u043b[\\u044c\\u044f]|\\u0444\\u0435\\u0432\\u0440?\\.?|\\u043c\\u0430\\u0440\\u0442\\u0430?|\\u043c\\u0430\\u0440\\.?|\\u0430\\u043f\\u0440\\u0435\\u043b[\\u044c\\u044f]|\\u0430\\u043f\\u0440\\.?|\\u043c\\u0430[\\u0439\\u044f]|\\u0438\\u044e\\u043d[\\u044c\\u044f]|\\u0438\\u044e\\u043d\\.?|\\u0438\\u044e\\u043b[\\u044c\\u044f]|\\u0438\\u044e\\u043b\\.?|\\u0430\\u0432\\u0433\\u0443\\u0441\\u0442\\u0430?|\\u0430\\u0432\\u0433\\.?|\\u0441\\u0435\\u043d\\u0442\\u044f\\u0431\\u0440[\\u044c\\u044f]|\\u0441\\u0435\\u043d\\u0442?\\.?|\\u043e\\u043a\\u0442\\u044f\\u0431\\u0440[\\u044c\\u044f]|\\u043e\\u043a\\u0442\\.?|\\u043d\\u043e\\u044f\\u0431\\u0440[\\u044c\\u044f]|\\u043d\\u043e\\u044f\\u0431?\\.?|\\u0434\\u0435\\u043a\\u0430\\u0431\\u0440[\\u044c\\u044f]|\\u0434\\u0435\\u043a\\.?)/i,monthsShortRegex:/^(\\u044f\\u043d\\u0432\\u0430\\u0440[\\u044c\\u044f]|\\u044f\\u043d\\u0432\\.?|\\u0444\\u0435\\u0432\\u0440\\u0430\\u043b[\\u044c\\u044f]|\\u0444\\u0435\\u0432\\u0440?\\.?|\\u043c\\u0430\\u0440\\u0442\\u0430?|\\u043c\\u0430\\u0440\\.?|\\u0430\\u043f\\u0440\\u0435\\u043b[\\u044c\\u044f]|\\u0430\\u043f\\u0440\\.?|\\u043c\\u0430[\\u0439\\u044f]|\\u0438\\u044e\\u043d[\\u044c\\u044f]|\\u0438\\u044e\\u043d\\.?|\\u0438\\u044e\\u043b[\\u044c\\u044f]|\\u0438\\u044e\\u043b\\.?|\\u0430\\u0432\\u0433\\u0443\\u0441\\u0442\\u0430?|\\u0430\\u0432\\u0433\\.?|\\u0441\\u0435\\u043d\\u0442\\u044f\\u0431\\u0440[\\u044c\\u044f]|\\u0441\\u0435\\u043d\\u0442?\\.?|\\u043e\\u043a\\u0442\\u044f\\u0431\\u0440[\\u044c\\u044f]|\\u043e\\u043a\\u0442\\.?|\\u043d\\u043e\\u044f\\u0431\\u0440[\\u044c\\u044f]|\\u043d\\u043e\\u044f\\u0431?\\.?|\\u0434\\u0435\\u043a\\u0430\\u0431\\u0440[\\u044c\\u044f]|\\u0434\\u0435\\u043a\\.?)/i,monthsStrictRegex:/^(\\u044f\\u043d\\u0432\\u0430\\u0440[\\u044f\\u044c]|\\u0444\\u0435\\u0432\\u0440\\u0430\\u043b[\\u044f\\u044c]|\\u043c\\u0430\\u0440\\u0442\\u0430?|\\u0430\\u043f\\u0440\\u0435\\u043b[\\u044f\\u044c]|\\u043c\\u0430[\\u044f\\u0439]|\\u0438\\u044e\\u043d[\\u044f\\u044c]|\\u0438\\u044e\\u043b[\\u044f\\u044c]|\\u0430\\u0432\\u0433\\u0443\\u0441\\u0442\\u0430?|\\u0441\\u0435\\u043d\\u0442\\u044f\\u0431\\u0440[\\u044f\\u044c]|\\u043e\\u043a\\u0442\\u044f\\u0431\\u0440[\\u044f\\u044c]|\\u043d\\u043e\\u044f\\u0431\\u0440[\\u044f\\u044c]|\\u0434\\u0435\\u043a\\u0430\\u0431\\u0440[\\u044f\\u044c])/i,monthsShortStrictRegex:/^(\\u044f\\u043d\\u0432\\.|\\u0444\\u0435\\u0432\\u0440?\\.|\\u043c\\u0430\\u0440[\\u0442.]|\\u0430\\u043f\\u0440\\.|\\u043c\\u0430[\\u044f\\u0439]|\\u0438\\u044e\\u043d[\\u044c\\u044f.]|\\u0438\\u044e\\u043b[\\u044c\\u044f.]|\\u0430\\u0432\\u0433\\.|\\u0441\\u0435\\u043d\\u0442?\\.|\\u043e\\u043a\\u0442\\.|\\u043d\\u043e\\u044f\\u0431?\\.|\\u0434\\u0435\\u043a\\.)/i,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY г.\",LLL:\"D MMMM YYYY г., H:mm\",LLLL:\"dddd, D MMMM YYYY г., H:mm\"},calendar:{sameDay:\"[Сегодня, в] LT\",nextDay:\"[Завтра, в] LT\",lastDay:\"[Вчера, в] LT\",nextWeek:function(e){if(e.week()===this.week())return 2===this.day()?\"[Во] dddd, [в] LT\":\"[В] dddd, [в] LT\";switch(this.day()){case 0:return\"[В следующее] dddd, [в] LT\";case 1:case 2:case 4:return\"[В следующий] dddd, [в] LT\";case 3:case 5:case 6:return\"[В следующую] dddd, [в] LT\"}},lastWeek:function(e){if(e.week()===this.week())return 2===this.day()?\"[Во] dddd, [в] LT\":\"[В] dddd, [в] LT\";switch(this.day()){case 0:return\"[В прошлое] dddd, [в] LT\";case 1:case 2:case 4:return\"[В прошлый] dddd, [в] LT\";case 3:case 5:case 6:return\"[В прошлую] dddd, [в] LT\"}},sameElse:\"L\"},relativeTime:{future:\"через %s\",past:\"%s назад\",s:\"несколько секунд\",ss:gi,m:gi,mm:gi,h:\"час\",hh:gi,d:\"день\",dd:gi,w:\"неделя\",ww:gi,M:\"месяц\",MM:gi,y:\"год\",yy:gi},meridiemParse:/\\u043d\\u043e\\u0447\\u0438|\\u0443\\u0442\\u0440\\u0430|\\u0434\\u043d\\u044f|\\u0432\\u0435\\u0447\\u0435\\u0440\\u0430/i,isPM:function(e){return/^(\\u0434\\u043d\\u044f|\\u0432\\u0435\\u0447\\u0435\\u0440\\u0430)$/.test(e)},meridiem:function(e,t,n){return e<4?\"ночи\":e<12?\"утра\":e<17?\"дня\":\"вечера\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0439|\\u0433\\u043e|\\u044f)/,ordinal:function(e,t){switch(t){case\"M\":case\"d\":case\"DDD\":return e+\"-й\";case\"D\":return e+\"-го\";case\"w\":case\"W\":return e+\"-я\";default:return e}},week:{dow:1,doy:4}}),ne=[\"جنوري\",\"فيبروري\",\"مارچ\",\"اپريل\",\"مئي\",\"جون\",\"جولاءِ\",\"آگسٽ\",\"سيپٽمبر\",\"آڪٽوبر\",\"نومبر\",\"ڊسمبر\"],K=[\"آچر\",\"سومر\",\"اڱارو\",\"اربع\",\"خميس\",\"جمع\",\"ڇنڇر\"],t.defineLocale(\"sd\",{months:ne,monthsShort:ne,weekdays:K,weekdaysShort:K,weekdaysMin:K,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd، D MMMM YYYY HH:mm\"},meridiemParse:/\\u0635\\u0628\\u062d|\\u0634\\u0627\\u0645/,isPM:function(e){return\"شام\"===e},meridiem:function(e,t,n){return e<12?\"صبح\":\"شام\"},calendar:{sameDay:\"[اڄ] LT\",nextDay:\"[سڀاڻي] LT\",nextWeek:\"dddd [اڳين هفتي تي] LT\",lastDay:\"[ڪالهه] LT\",lastWeek:\"[گزريل هفتي] dddd [تي] LT\",sameElse:\"L\"},relativeTime:{future:\"%s پوء\",past:\"%s اڳ\",s:\"چند سيڪنڊ\",ss:\"%d سيڪنڊ\",m:\"هڪ منٽ\",mm:\"%d منٽ\",h:\"هڪ ڪلاڪ\",hh:\"%d ڪلاڪ\",d:\"هڪ ڏينهن\",dd:\"%d ڏينهن\",M:\"هڪ مهينو\",MM:\"%d مهينا\",y:\"هڪ سال\",yy:\"%d سال\"},preparse:function(e){return e.replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/,/g,\"،\")},week:{dow:1,doy:4}}),t.defineLocale(\"se\",{months:\"ođđajagemánnu_guovvamánnu_njukčamánnu_cuoŋománnu_miessemánnu_geassemánnu_suoidnemánnu_borgemánnu_čakčamánnu_golggotmánnu_skábmamánnu_juovlamánnu\".split(\"_\"),monthsShort:\"ođđj_guov_njuk_cuo_mies_geas_suoi_borg_čakč_golg_skáb_juov\".split(\"_\"),weekdays:\"sotnabeaivi_vuossárga_maŋŋebárga_gaskavahkku_duorastat_bearjadat_lávvardat\".split(\"_\"),weekdaysShort:\"sotn_vuos_maŋ_gask_duor_bear_láv\".split(\"_\"),weekdaysMin:\"s_v_m_g_d_b_L\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"MMMM D. [b.] YYYY\",LLL:\"MMMM D. [b.] YYYY [ti.] HH:mm\",LLLL:\"dddd, MMMM D. [b.] YYYY [ti.] HH:mm\"},calendar:{sameDay:\"[otne ti] LT\",nextDay:\"[ihttin ti] LT\",nextWeek:\"dddd [ti] LT\",lastDay:\"[ikte ti] LT\",lastWeek:\"[ovddit] dddd [ti] LT\",sameElse:\"L\"},relativeTime:{future:\"%s geažes\",past:\"maŋit %s\",s:\"moadde sekunddat\",ss:\"%d sekunddat\",m:\"okta minuhta\",mm:\"%d minuhtat\",h:\"okta diimmu\",hh:\"%d diimmut\",d:\"okta beaivi\",dd:\"%d beaivvit\",M:\"okta mánnu\",MM:\"%d mánut\",y:\"okta jahki\",yy:\"%d jagit\"},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"si\",{months:\"ජනවාරි_පෙබරවාරි_මාර්තු_අප්‍රේල්_මැයි_ජූනි_ජූලි_අගෝස්තු_සැප්තැම්බර්_ඔක්තෝබර්_නොවැම්බර්_දෙසැම්බර්\".split(\"_\"),monthsShort:\"ජන_පෙබ_මාර්_අප්_මැයි_ජූනි_ජූලි_අගෝ_සැප්_ඔක්_නොවැ_දෙසැ\".split(\"_\"),weekdays:\"ඉරිදා_සඳුදා_අඟහරුවාදා_බදාදා_බ්‍රහස්පතින්දා_සිකුරාදා_සෙනසුරාදා\".split(\"_\"),weekdaysShort:\"ඉරි_සඳු_අඟ_බදා_බ්‍රහ_සිකු_සෙන\".split(\"_\"),weekdaysMin:\"ඉ_ස_අ_බ_බ්‍ර_සි_සෙ\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"a h:mm\",LTS:\"a h:mm:ss\",L:\"YYYY/MM/DD\",LL:\"YYYY MMMM D\",LLL:\"YYYY MMMM D, a h:mm\",LLLL:\"YYYY MMMM D [වැනි] dddd, a h:mm:ss\"},calendar:{sameDay:\"[අද] LT[ට]\",nextDay:\"[හෙට] LT[ට]\",nextWeek:\"dddd LT[ට]\",lastDay:\"[ඊයේ] LT[ට]\",lastWeek:\"[පසුගිය] dddd LT[ට]\",sameElse:\"L\"},relativeTime:{future:\"%sකින්\",past:\"%sකට පෙර\",s:\"තත්පර කිහිපය\",ss:\"තත්පර %d\",m:\"මිනිත්තුව\",mm:\"මිනිත්තු %d\",h:\"පැය\",hh:\"පැය %d\",d:\"දිනය\",dd:\"දින %d\",M:\"මාසය\",MM:\"මාස %d\",y:\"වසර\",yy:\"වසර %d\"},dayOfMonthOrdinalParse:/\\d{1,2} \\u0dc0\\u0dd0\\u0db1\\u0dd2/,ordinal:function(e){return e+\" වැනි\"},meridiemParse:/\\u0db4\\u0dd9\\u0dbb \\u0dc0\\u0dbb\\u0dd4|\\u0db4\\u0dc3\\u0dca \\u0dc0\\u0dbb\\u0dd4|\\u0db4\\u0dd9.\\u0dc0|\\u0db4.\\u0dc0./,isPM:function(e){return\"ප.ව.\"===e||\"පස් වරු\"===e},meridiem:function(e,t,n){return 11<e?n?\"ප.ව.\":\"පස් වරු\":n?\"පෙ.ව.\":\"පෙර වරු\"}}),U=\"január_február_marec_apríl_máj_jún_júl_august_september_október_november_december\".split(\"_\"),Te=\"jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec\".split(\"_\"),t.defineLocale(\"sk\",{months:U,monthsShort:Te,weekdays:\"nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota\".split(\"_\"),weekdaysShort:\"ne_po_ut_st_št_pi_so\".split(\"_\"),weekdaysMin:\"ne_po_ut_st_št_pi_so\".split(\"_\"),longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY H:mm\",LLLL:\"dddd D. MMMM YYYY H:mm\"},calendar:{sameDay:\"[dnes o] LT\",nextDay:\"[zajtra o] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[v nedeľu o] LT\";case 1:case 2:return\"[v] dddd [o] LT\";case 3:return\"[v stredu o] LT\";case 4:return\"[vo štvrtok o] LT\";case 5:return\"[v piatok o] LT\";case 6:return\"[v sobotu o] LT\"}},lastDay:\"[včera o] LT\",lastWeek:function(){switch(this.day()){case 0:return\"[minulú nedeľu o] LT\";case 1:case 2:case 4:case 5:return\"[minulý] dddd [o] LT\";case 3:return\"[minulú stredu o] LT\";case 6:return\"[minulú sobotu o] LT\"}},sameElse:\"L\"},relativeTime:{future:\"za %s\",past:\"pred %s\",s:yi,ss:yi,m:yi,mm:yi,h:yi,hh:yi,d:yi,dd:yi,M:yi,MM:yi,y:yi,yy:yi},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"sl\",{months:\"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december\".split(\"_\"),monthsShort:\"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.\".split(\"_\"),monthsParseExact:!0,weekdays:\"nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota\".split(\"_\"),weekdaysShort:\"ned._pon._tor._sre._čet._pet._sob.\".split(\"_\"),weekdaysMin:\"ne_po_to_sr_če_pe_so\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD. MM. YYYY\",LL:\"D. MMMM YYYY\",LLL:\"D. MMMM YYYY H:mm\",LLLL:\"dddd, D. MMMM YYYY H:mm\"},calendar:{sameDay:\"[danes ob] LT\",nextDay:\"[jutri ob] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[v] [nedeljo] [ob] LT\";case 3:return\"[v] [sredo] [ob] LT\";case 6:return\"[v] [soboto] [ob] LT\";case 1:case 2:case 4:case 5:return\"[v] dddd [ob] LT\"}},lastDay:\"[včeraj ob] LT\",lastWeek:function(){switch(this.day()){case 0:return\"[prejšnjo] [nedeljo] [ob] LT\";case 3:return\"[prejšnjo] [sredo] [ob] LT\";case 6:return\"[prejšnjo] [soboto] [ob] LT\";case 1:case 2:case 4:case 5:return\"[prejšnji] dddd [ob] LT\"}},sameElse:\"L\"},relativeTime:{future:\"čez %s\",past:\"pred %s\",s:bi,ss:bi,m:bi,mm:bi,h:bi,hh:bi,d:bi,dd:bi,M:bi,MM:bi,y:bi,yy:bi},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:7}}),t.defineLocale(\"sq\",{months:\"Janar_Shkurt_Mars_Prill_Maj_Qershor_Korrik_Gusht_Shtator_Tetor_Nëntor_Dhjetor\".split(\"_\"),monthsShort:\"Jan_Shk_Mar_Pri_Maj_Qer_Kor_Gus_Sht_Tet_Nën_Dhj\".split(\"_\"),weekdays:\"E Diel_E Hënë_E Martë_E Mërkurë_E Enjte_E Premte_E Shtunë\".split(\"_\"),weekdaysShort:\"Die_Hën_Mar_Mër_Enj_Pre_Sht\".split(\"_\"),weekdaysMin:\"D_H_Ma_Më_E_P_Sh\".split(\"_\"),weekdaysParseExact:!0,meridiemParse:/PD|MD/,isPM:function(e){return\"M\"===e.charAt(0)},meridiem:function(e,t,n){return e<12?\"PD\":\"MD\"},longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Sot në] LT\",nextDay:\"[Nesër në] LT\",nextWeek:\"dddd [në] LT\",lastDay:\"[Dje në] LT\",lastWeek:\"dddd [e kaluar në] LT\",sameElse:\"L\"},relativeTime:{future:\"në %s\",past:\"%s më parë\",s:\"disa sekonda\",ss:\"%d sekonda\",m:\"një minutë\",mm:\"%d minuta\",h:\"një orë\",hh:\"%d orë\",d:\"një ditë\",dd:\"%d ditë\",M:\"një muaj\",MM:\"%d muaj\",y:\"një vit\",yy:\"%d vite\"},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}});var Mi={words:{ss:[\"секунда\",\"секунде\",\"секунди\"],m:[\"један минут\",\"једног минута\"],mm:[\"минут\",\"минута\",\"минута\"],h:[\"један сат\",\"једног сата\"],hh:[\"сат\",\"сата\",\"сати\"],d:[\"један дан\",\"једног дана\"],dd:[\"дан\",\"дана\",\"дана\"],M:[\"један месец\",\"једног месеца\"],MM:[\"месец\",\"месеца\",\"месеци\"],y:[\"једну годину\",\"једне године\"],yy:[\"годину\",\"године\",\"година\"]},correctGrammaticalCase:function(e,t){return 1<=e%10&&e%10<=4&&(e%100<10||20<=e%100)?e%10==1?t[0]:t[1]:t[2]},translate:function(e,t,n,r){var i=Mi.words[n];return 1===n.length?\"y\"===n&&t?\"једна година\":r||t?i[0]:i[1]:(r=Mi.correctGrammaticalCase(e,i),\"yy\"===n&&t&&\"годину\"===r?e+\" година\":e+\" \"+r)}},wi=(t.defineLocale(\"sr-cyrl\",{months:\"јануар_фебруар_март_април_мај_јун_јул_август_септембар_октобар_новембар_децембар\".split(\"_\"),monthsShort:\"јан._феб._мар._апр._мај_јун_јул_авг._сеп._окт._нов._дец.\".split(\"_\"),monthsParseExact:!0,weekdays:\"недеља_понедељак_уторак_среда_четвртак_петак_субота\".split(\"_\"),weekdaysShort:\"нед._пон._уто._сре._чет._пет._суб.\".split(\"_\"),weekdaysMin:\"не_по_ут_ср_че_пе_су\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"D. M. YYYY.\",LL:\"D. MMMM YYYY.\",LLL:\"D. MMMM YYYY. H:mm\",LLLL:\"dddd, D. MMMM YYYY. H:mm\"},calendar:{sameDay:\"[данас у] LT\",nextDay:\"[сутра у] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[у] [недељу] [у] LT\";case 3:return\"[у] [среду] [у] LT\";case 6:return\"[у] [суботу] [у] LT\";case 1:case 2:case 4:case 5:return\"[у] dddd [у] LT\"}},lastDay:\"[јуче у] LT\",lastWeek:function(){return[\"[прошле] [недеље] [у] LT\",\"[прошлог] [понедељка] [у] LT\",\"[прошлог] [уторка] [у] LT\",\"[прошле] [среде] [у] LT\",\"[прошлог] [четвртка] [у] LT\",\"[прошлог] [петка] [у] LT\",\"[прошле] [суботе] [у] LT\"][this.day()]},sameElse:\"L\"},relativeTime:{future:\"за %s\",past:\"пре %s\",s:\"неколико секунди\",ss:Mi.translate,m:Mi.translate,mm:Mi.translate,h:Mi.translate,hh:Mi.translate,d:Mi.translate,dd:Mi.translate,M:Mi.translate,MM:Mi.translate,y:Mi.translate,yy:Mi.translate},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:7}}),{words:{ss:[\"sekunda\",\"sekunde\",\"sekundi\"],m:[\"jedan minut\",\"jednog minuta\"],mm:[\"minut\",\"minuta\",\"minuta\"],h:[\"jedan sat\",\"jednog sata\"],hh:[\"sat\",\"sata\",\"sati\"],d:[\"jedan dan\",\"jednog dana\"],dd:[\"dan\",\"dana\",\"dana\"],M:[\"jedan mesec\",\"jednog meseca\"],MM:[\"mesec\",\"meseca\",\"meseci\"],y:[\"jednu godinu\",\"jedne godine\"],yy:[\"godinu\",\"godine\",\"godina\"]},correctGrammaticalCase:function(e,t){return 1<=e%10&&e%10<=4&&(e%100<10||20<=e%100)?e%10==1?t[0]:t[1]:t[2]},translate:function(e,t,n,r){var i=wi.words[n];return 1===n.length?\"y\"===n&&t?\"jedna godina\":r||t?i[0]:i[1]:(r=wi.correctGrammaticalCase(e,i),\"yy\"===n&&t&&\"godinu\"===r?e+\" godina\":e+\" \"+r)}}),ki=(t.defineLocale(\"sr\",{months:\"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar\".split(\"_\"),monthsShort:\"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.\".split(\"_\"),monthsParseExact:!0,weekdays:\"nedelja_ponedeljak_utorak_sreda_četvrtak_petak_subota\".split(\"_\"),weekdaysShort:\"ned._pon._uto._sre._čet._pet._sub.\".split(\"_\"),weekdaysMin:\"ne_po_ut_sr_če_pe_su\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"D. M. YYYY.\",LL:\"D. MMMM YYYY.\",LLL:\"D. MMMM YYYY. H:mm\",LLLL:\"dddd, D. MMMM YYYY. H:mm\"},calendar:{sameDay:\"[danas u] LT\",nextDay:\"[sutra u] LT\",nextWeek:function(){switch(this.day()){case 0:return\"[u] [nedelju] [u] LT\";case 3:return\"[u] [sredu] [u] LT\";case 6:return\"[u] [subotu] [u] LT\";case 1:case 2:case 4:case 5:return\"[u] dddd [u] LT\"}},lastDay:\"[juče u] LT\",lastWeek:function(){return[\"[prošle] [nedelje] [u] LT\",\"[prošlog] [ponedeljka] [u] LT\",\"[prošlog] [utorka] [u] LT\",\"[prošle] [srede] [u] LT\",\"[prošlog] [četvrtka] [u] LT\",\"[prošlog] [petka] [u] LT\",\"[prošle] [subote] [u] LT\"][this.day()]},sameElse:\"L\"},relativeTime:{future:\"za %s\",past:\"pre %s\",s:\"nekoliko sekundi\",ss:wi.translate,m:wi.translate,mm:wi.translate,h:wi.translate,hh:wi.translate,d:wi.translate,dd:wi.translate,M:wi.translate,MM:wi.translate,y:wi.translate,yy:wi.translate},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:7}}),t.defineLocale(\"ss\",{months:\"Bhimbidvwane_Indlovana_Indlov'lenkhulu_Mabasa_Inkhwekhweti_Inhlaba_Kholwane_Ingci_Inyoni_Imphala_Lweti_Ingongoni\".split(\"_\"),monthsShort:\"Bhi_Ina_Inu_Mab_Ink_Inh_Kho_Igc_Iny_Imp_Lwe_Igo\".split(\"_\"),weekdays:\"Lisontfo_Umsombuluko_Lesibili_Lesitsatfu_Lesine_Lesihlanu_Umgcibelo\".split(\"_\"),weekdaysShort:\"Lis_Umb_Lsb_Les_Lsi_Lsh_Umg\".split(\"_\"),weekdaysMin:\"Li_Us_Lb_Lt_Ls_Lh_Ug\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY h:mm A\",LLLL:\"dddd, D MMMM YYYY h:mm A\"},calendar:{sameDay:\"[Namuhla nga] LT\",nextDay:\"[Kusasa nga] LT\",nextWeek:\"dddd [nga] LT\",lastDay:\"[Itolo nga] LT\",lastWeek:\"dddd [leliphelile] [nga] LT\",sameElse:\"L\"},relativeTime:{future:\"nga %s\",past:\"wenteka nga %s\",s:\"emizuzwana lomcane\",ss:\"%d mzuzwana\",m:\"umzuzu\",mm:\"%d emizuzu\",h:\"lihora\",hh:\"%d emahora\",d:\"lilanga\",dd:\"%d emalanga\",M:\"inyanga\",MM:\"%d tinyanga\",y:\"umnyaka\",yy:\"%d iminyaka\"},meridiemParse:/ekuseni|emini|entsambama|ebusuku/,meridiem:function(e,t,n){return e<11?\"ekuseni\":e<15?\"emini\":e<19?\"entsambama\":\"ebusuku\"},meridiemHour:function(e,t){return 12===e&&(e=0),\"ekuseni\"===t?e:\"emini\"===t?11<=e?e:e+12:\"entsambama\"===t||\"ebusuku\"===t?0===e?0:e+12:void 0},dayOfMonthOrdinalParse:/\\d{1,2}/,ordinal:\"%d\",week:{dow:1,doy:4}}),t.defineLocale(\"sv\",{months:\"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december\".split(\"_\"),monthsShort:\"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec\".split(\"_\"),weekdays:\"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag\".split(\"_\"),weekdaysShort:\"sön_mån_tis_ons_tor_fre_lör\".split(\"_\"),weekdaysMin:\"sö_må_ti_on_to_fr_lö\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY-MM-DD\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY [kl.] HH:mm\",LLLL:\"dddd D MMMM YYYY [kl.] HH:mm\",lll:\"D MMM YYYY HH:mm\",llll:\"ddd D MMM YYYY HH:mm\"},calendar:{sameDay:\"[Idag] LT\",nextDay:\"[Imorgon] LT\",lastDay:\"[Igår] LT\",nextWeek:\"[På] dddd LT\",lastWeek:\"[I] dddd[s] LT\",sameElse:\"L\"},relativeTime:{future:\"om %s\",past:\"för %s sedan\",s:\"några sekunder\",ss:\"%d sekunder\",m:\"en minut\",mm:\"%d minuter\",h:\"en timme\",hh:\"%d timmar\",d:\"en dag\",dd:\"%d dagar\",M:\"en månad\",MM:\"%d månader\",y:\"ett år\",yy:\"%d år\"},dayOfMonthOrdinalParse:/\\d{1,2}(\\:e|\\:a)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)||1!=t&&2!=t?\":e\":\":a\")},week:{dow:1,doy:4}}),t.defineLocale(\"sw\",{months:\"Januari_Februari_Machi_Aprili_Mei_Juni_Julai_Agosti_Septemba_Oktoba_Novemba_Desemba\".split(\"_\"),monthsShort:\"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ago_Sep_Okt_Nov_Des\".split(\"_\"),weekdays:\"Jumapili_Jumatatu_Jumanne_Jumatano_Alhamisi_Ijumaa_Jumamosi\".split(\"_\"),weekdaysShort:\"Jpl_Jtat_Jnne_Jtan_Alh_Ijm_Jmos\".split(\"_\"),weekdaysMin:\"J2_J3_J4_J5_Al_Ij_J1\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"hh:mm A\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[leo saa] LT\",nextDay:\"[kesho saa] LT\",nextWeek:\"[wiki ijayo] dddd [saat] LT\",lastDay:\"[jana] LT\",lastWeek:\"[wiki iliyopita] dddd [saat] LT\",sameElse:\"L\"},relativeTime:{future:\"%s baadaye\",past:\"tokea %s\",s:\"hivi punde\",ss:\"sekunde %d\",m:\"dakika moja\",mm:\"dakika %d\",h:\"saa limoja\",hh:\"masaa %d\",d:\"siku moja\",dd:\"siku %d\",M:\"mwezi mmoja\",MM:\"miezi %d\",y:\"mwaka mmoja\",yy:\"miaka %d\"},week:{dow:1,doy:7}}),{1:\"௧\",2:\"௨\",3:\"௩\",4:\"௪\",5:\"௫\",6:\"௬\",7:\"௭\",8:\"௮\",9:\"௯\",0:\"௦\"}),Li={\"௧\":\"1\",\"௨\":\"2\",\"௩\":\"3\",\"௪\":\"4\",\"௫\":\"5\",\"௬\":\"6\",\"௭\":\"7\",\"௮\":\"8\",\"௯\":\"9\",\"௦\":\"0\"},xi=(t.defineLocale(\"ta\",{months:\"ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்\".split(\"_\"),monthsShort:\"ஜனவரி_பிப்ரவரி_மார்ச்_ஏப்ரல்_மே_ஜூன்_ஜூலை_ஆகஸ்ட்_செப்டெம்பர்_அக்டோபர்_நவம்பர்_டிசம்பர்\".split(\"_\"),weekdays:\"ஞாயிற்றுக்கிழமை_திங்கட்கிழமை_செவ்வாய்கிழமை_புதன்கிழமை_வியாழக்கிழமை_வெள்ளிக்கிழமை_சனிக்கிழமை\".split(\"_\"),weekdaysShort:\"ஞாயிறு_திங்கள்_செவ்வாய்_புதன்_வியாழன்_வெள்ளி_சனி\".split(\"_\"),weekdaysMin:\"ஞா_தி_செ_பு_வி_வெ_ச\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, HH:mm\",LLLL:\"dddd, D MMMM YYYY, HH:mm\"},calendar:{sameDay:\"[இன்று] LT\",nextDay:\"[நாளை] LT\",nextWeek:\"dddd, LT\",lastDay:\"[நேற்று] LT\",lastWeek:\"[கடந்த வாரம்] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s இல்\",past:\"%s முன்\",s:\"ஒரு சில விநாடிகள்\",ss:\"%d விநாடிகள்\",m:\"ஒரு நிமிடம்\",mm:\"%d நிமிடங்கள்\",h:\"ஒரு மணி நேரம்\",hh:\"%d மணி நேரம்\",d:\"ஒரு நாள்\",dd:\"%d நாட்கள்\",M:\"ஒரு மாதம்\",MM:\"%d மாதங்கள்\",y:\"ஒரு வருடம்\",yy:\"%d ஆண்டுகள்\"},dayOfMonthOrdinalParse:/\\d{1,2}\\u0bb5\\u0ba4\\u0bc1/,ordinal:function(e){return e+\"வது\"},preparse:function(e){return e.replace(/[\\u0be7\\u0be8\\u0be9\\u0bea\\u0beb\\u0bec\\u0bed\\u0bee\\u0bef\\u0be6]/g,function(e){return Li[e]})},postformat:function(e){return e.replace(/\\d/g,function(e){return ki[e]})},meridiemParse:/\\u0baf\\u0bbe\\u0bae\\u0bae\\u0bcd|\\u0bb5\\u0bc8\\u0b95\\u0bb1\\u0bc8|\\u0b95\\u0bbe\\u0bb2\\u0bc8|\\u0ba8\\u0ba3\\u0bcd\\u0baa\\u0b95\\u0bb2\\u0bcd|\\u0b8e\\u0bb1\\u0bcd\\u0baa\\u0bbe\\u0b9f\\u0bc1|\\u0bae\\u0bbe\\u0bb2\\u0bc8/,meridiem:function(e,t,n){return e<2?\" யாமம்\":e<6?\" வைகறை\":e<10?\" காலை\":e<14?\" நண்பகல்\":e<18?\" எற்பாடு\":e<22?\" மாலை\":\" யாமம்\"},meridiemHour:function(e,t){return 12===e&&(e=0),\"யாமம்\"===t?e<2?e:e+12:\"வைகறை\"===t||\"காலை\"===t||\"நண்பகல்\"===t&&10<=e?e:e+12},week:{dow:0,doy:6}}),t.defineLocale(\"te\",{months:\"జనవరి_ఫిబ్రవరి_మార్చి_ఏప్రిల్_మే_జూన్_జులై_ఆగస్టు_సెప్టెంబర్_అక్టోబర్_నవంబర్_డిసెంబర్\".split(\"_\"),monthsShort:\"జన._ఫిబ్ర._మార్చి_ఏప్రి._మే_జూన్_జులై_ఆగ._సెప్._అక్టో._నవ._డిసె.\".split(\"_\"),monthsParseExact:!0,weekdays:\"ఆదివారం_సోమవారం_మంగళవారం_బుధవారం_గురువారం_శుక్రవారం_శనివారం\".split(\"_\"),weekdaysShort:\"ఆది_సోమ_మంగళ_బుధ_గురు_శుక్ర_శని\".split(\"_\"),weekdaysMin:\"ఆ_సో_మం_బు_గు_శు_శ\".split(\"_\"),longDateFormat:{LT:\"A h:mm\",LTS:\"A h:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY, A h:mm\",LLLL:\"dddd, D MMMM YYYY, A h:mm\"},calendar:{sameDay:\"[నేడు] LT\",nextDay:\"[రేపు] LT\",nextWeek:\"dddd, LT\",lastDay:\"[నిన్న] LT\",lastWeek:\"[గత] dddd, LT\",sameElse:\"L\"},relativeTime:{future:\"%s లో\",past:\"%s క్రితం\",s:\"కొన్ని క్షణాలు\",ss:\"%d సెకన్లు\",m:\"ఒక నిమిషం\",mm:\"%d నిమిషాలు\",h:\"ఒక గంట\",hh:\"%d గంటలు\",d:\"ఒక రోజు\",dd:\"%d రోజులు\",M:\"ఒక నెల\",MM:\"%d నెలలు\",y:\"ఒక సంవత్సరం\",yy:\"%d సంవత్సరాలు\"},dayOfMonthOrdinalParse:/\\d{1,2}\\u0c35/,ordinal:\"%dవ\",meridiemParse:/\\u0c30\\u0c3e\\u0c24\\u0c4d\\u0c30\\u0c3f|\\u0c09\\u0c26\\u0c2f\\u0c02|\\u0c2e\\u0c27\\u0c4d\\u0c2f\\u0c3e\\u0c39\\u0c4d\\u0c28\\u0c02|\\u0c38\\u0c3e\\u0c2f\\u0c02\\u0c24\\u0c4d\\u0c30\\u0c02/,meridiemHour:function(e,t){return 12===e&&(e=0),\"రాత్రి\"===t?e<4?e:e+12:\"ఉదయం\"===t?e:\"మధ్యాహ్నం\"===t?10<=e?e:e+12:\"సాయంత్రం\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"రాత్రి\":e<10?\"ఉదయం\":e<17?\"మధ్యాహ్నం\":e<20?\"సాయంత్రం\":\"రాత్రి\"},week:{dow:0,doy:6}}),t.defineLocale(\"tet\",{months:\"Janeiru_Fevereiru_Marsu_Abril_Maiu_Juñu_Jullu_Agustu_Setembru_Outubru_Novembru_Dezembru\".split(\"_\"),monthsShort:\"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez\".split(\"_\"),weekdays:\"Domingu_Segunda_Tersa_Kuarta_Kinta_Sesta_Sabadu\".split(\"_\"),weekdaysShort:\"Dom_Seg_Ters_Kua_Kint_Sest_Sab\".split(\"_\"),weekdaysMin:\"Do_Seg_Te_Ku_Ki_Ses_Sa\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Ohin iha] LT\",nextDay:\"[Aban iha] LT\",nextWeek:\"dddd [iha] LT\",lastDay:\"[Horiseik iha] LT\",lastWeek:\"dddd [semana kotuk] [iha] LT\",sameElse:\"L\"},relativeTime:{future:\"iha %s\",past:\"%s liuba\",s:\"segundu balun\",ss:\"segundu %d\",m:\"minutu ida\",mm:\"minutu %d\",h:\"oras ida\",hh:\"oras %d\",d:\"loron ida\",dd:\"loron %d\",M:\"fulan ida\",MM:\"fulan %d\",y:\"tinan ida\",yy:\"tinan %d\"},dayOfMonthOrdinalParse:/\\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")},week:{dow:1,doy:4}}),{0:\"-ум\",1:\"-ум\",2:\"-юм\",3:\"-юм\",4:\"-ум\",5:\"-ум\",6:\"-ум\",7:\"-ум\",8:\"-ум\",9:\"-ум\",10:\"-ум\",12:\"-ум\",13:\"-ум\",20:\"-ум\",30:\"-юм\",40:\"-ум\",50:\"-ум\",60:\"-ум\",70:\"-ум\",80:\"-ум\",90:\"-ум\",100:\"-ум\"}),Ti=(t.defineLocale(\"tg\",{months:{format:\"январи_феврали_марти_апрели_майи_июни_июли_августи_сентябри_октябри_ноябри_декабри\".split(\"_\"),standalone:\"январ_феврал_март_апрел_май_июн_июл_август_сентябр_октябр_ноябр_декабр\".split(\"_\")},monthsShort:\"янв_фев_мар_апр_май_июн_июл_авг_сен_окт_ноя_дек\".split(\"_\"),weekdays:\"якшанбе_душанбе_сешанбе_чоршанбе_панҷшанбе_ҷумъа_шанбе\".split(\"_\"),weekdaysShort:\"яшб_дшб_сшб_чшб_пшб_ҷум_шнб\".split(\"_\"),weekdaysMin:\"яш_дш_сш_чш_пш_ҷм_шб\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[Имрӯз соати] LT\",nextDay:\"[Фардо соати] LT\",lastDay:\"[Дирӯз соати] LT\",nextWeek:\"dddd[и] [ҳафтаи оянда соати] LT\",lastWeek:\"dddd[и] [ҳафтаи гузашта соати] LT\",sameElse:\"L\"},relativeTime:{future:\"баъди %s\",past:\"%s пеш\",s:\"якчанд сония\",m:\"як дақиқа\",mm:\"%d дақиқа\",h:\"як соат\",hh:\"%d соат\",d:\"як рӯз\",dd:\"%d рӯз\",M:\"як моҳ\",MM:\"%d моҳ\",y:\"як сол\",yy:\"%d сол\"},meridiemParse:/\\u0448\\u0430\\u0431|\\u0441\\u0443\\u0431\\u04b3|\\u0440\\u04ef\\u0437|\\u0431\\u0435\\u0433\\u043e\\u04b3/,meridiemHour:function(e,t){return 12===e&&(e=0),\"шаб\"===t?e<4?e:e+12:\"субҳ\"===t?e:\"рӯз\"===t?11<=e?e:e+12:\"бегоҳ\"===t?e+12:void 0},meridiem:function(e,t,n){return e<4?\"шаб\":e<11?\"субҳ\":e<16?\"рӯз\":e<19?\"бегоҳ\":\"шаб\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0443\\u043c|\\u044e\\u043c)/,ordinal:function(e){return e+(xi[e]||xi[e%10]||xi[100<=e?100:null])},week:{dow:1,doy:7}}),t.defineLocale(\"th\",{months:\"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม\".split(\"_\"),monthsShort:\"ม.ค._ก.พ._มี.ค._เม.ย._พ.ค._มิ.ย._ก.ค._ส.ค._ก.ย._ต.ค._พ.ย._ธ.ค.\".split(\"_\"),monthsParseExact:!0,weekdays:\"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์\".split(\"_\"),weekdaysShort:\"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์\".split(\"_\"),weekdaysMin:\"อา._จ._อ._พ._พฤ._ศ._ส.\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"H:mm\",LTS:\"H:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY เวลา H:mm\",LLLL:\"วันddddที่ D MMMM YYYY เวลา H:mm\"},meridiemParse:/\\u0e01\\u0e48\\u0e2d\\u0e19\\u0e40\\u0e17\\u0e35\\u0e48\\u0e22\\u0e07|\\u0e2b\\u0e25\\u0e31\\u0e07\\u0e40\\u0e17\\u0e35\\u0e48\\u0e22\\u0e07/,isPM:function(e){return\"หลังเที่ยง\"===e},meridiem:function(e,t,n){return e<12?\"ก่อนเที่ยง\":\"หลังเที่ยง\"},calendar:{sameDay:\"[วันนี้ เวลา] LT\",nextDay:\"[พรุ่งนี้ เวลา] LT\",nextWeek:\"dddd[หน้า เวลา] LT\",lastDay:\"[เมื่อวานนี้ เวลา] LT\",lastWeek:\"[วัน]dddd[ที่แล้ว เวลา] LT\",sameElse:\"L\"},relativeTime:{future:\"อีก %s\",past:\"%sที่แล้ว\",s:\"ไม่กี่วินาที\",ss:\"%d วินาที\",m:\"1 นาที\",mm:\"%d นาที\",h:\"1 ชั่วโมง\",hh:\"%d ชั่วโมง\",d:\"1 วัน\",dd:\"%d วัน\",w:\"1 สัปดาห์\",ww:\"%d สัปดาห์\",M:\"1 เดือน\",MM:\"%d เดือน\",y:\"1 ปี\",yy:\"%d ปี\"}}),{1:\"'inji\",5:\"'inji\",8:\"'inji\",70:\"'inji\",80:\"'inji\",2:\"'nji\",7:\"'nji\",20:\"'nji\",50:\"'nji\",3:\"'ünji\",4:\"'ünji\",100:\"'ünji\",6:\"'njy\",9:\"'unjy\",10:\"'unjy\",30:\"'unjy\",60:\"'ynjy\",90:\"'ynjy\"}),Si=(t.defineLocale(\"tk\",{months:\"Ýanwar_Fewral_Mart_Aprel_Maý_Iýun_Iýul_Awgust_Sentýabr_Oktýabr_Noýabr_Dekabr\".split(\"_\"),monthsShort:\"Ýan_Few_Mar_Apr_Maý_Iýn_Iýl_Awg_Sen_Okt_Noý_Dek\".split(\"_\"),weekdays:\"Ýekşenbe_Duşenbe_Sişenbe_Çarşenbe_Penşenbe_Anna_Şenbe\".split(\"_\"),weekdaysShort:\"Ýek_Duş_Siş_Çar_Pen_Ann_Şen\".split(\"_\"),weekdaysMin:\"Ýk_Dş_Sş_Çr_Pn_An_Şn\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[bugün sagat] LT\",nextDay:\"[ertir sagat] LT\",nextWeek:\"[indiki] dddd [sagat] LT\",lastDay:\"[düýn] LT\",lastWeek:\"[geçen] dddd [sagat] LT\",sameElse:\"L\"},relativeTime:{future:\"%s soň\",past:\"%s öň\",s:\"birnäçe sekunt\",m:\"bir minut\",mm:\"%d minut\",h:\"bir sagat\",hh:\"%d sagat\",d:\"bir gün\",dd:\"%d gün\",M:\"bir aý\",MM:\"%d aý\",y:\"bir ýyl\",yy:\"%d ýyl\"},ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"Do\":case\"DD\":return e;default:var n;return 0===e?e+\"'unjy\":e+(Ti[n=e%10]||Ti[e%100-n]||Ti[100<=e?100:null])}},week:{dow:1,doy:7}}),t.defineLocale(\"tl-ph\",{months:\"Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre\".split(\"_\"),monthsShort:\"Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis\".split(\"_\"),weekdays:\"Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado\".split(\"_\"),weekdaysShort:\"Lin_Lun_Mar_Miy_Huw_Biy_Sab\".split(\"_\"),weekdaysMin:\"Li_Lu_Ma_Mi_Hu_Bi_Sab\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"MM/D/YYYY\",LL:\"MMMM D, YYYY\",LLL:\"MMMM D, YYYY HH:mm\",LLLL:\"dddd, MMMM DD, YYYY HH:mm\"},calendar:{sameDay:\"LT [ngayong araw]\",nextDay:\"[Bukas ng] LT\",nextWeek:\"LT [sa susunod na] dddd\",lastDay:\"LT [kahapon]\",lastWeek:\"LT [noong nakaraang] dddd\",sameElse:\"L\"},relativeTime:{future:\"sa loob ng %s\",past:\"%s ang nakalipas\",s:\"ilang segundo\",ss:\"%d segundo\",m:\"isang minuto\",mm:\"%d minuto\",h:\"isang oras\",hh:\"%d oras\",d:\"isang araw\",dd:\"%d araw\",M:\"isang buwan\",MM:\"%d buwan\",y:\"isang taon\",yy:\"%d taon\"},dayOfMonthOrdinalParse:/\\d{1,2}/,ordinal:function(e){return e},week:{dow:1,doy:4}}),\"pagh_wa’_cha’_wej_loS_vagh_jav_Soch_chorgh_Hut\".split(\"_\"));function Di(e,t,n,r){var i=function(e){var t=Math.floor(e%1e3/100),n=Math.floor(e%100/10),r=(e=e%10,\"\");return 0<t&&(r+=Si[t]+\"vatlh\"),0<n&&(r+=(\"\"!==r?\" \":\"\")+Si[n]+\"maH\"),0<e&&(r+=(\"\"!==r?\" \":\"\")+Si[e]),\"\"===r?\"pagh\":r}(e);switch(n){case\"ss\":return i+\" lup\";case\"mm\":return i+\" tup\";case\"hh\":return i+\" rep\";case\"dd\":return i+\" jaj\";case\"MM\":return i+\" jar\";case\"yy\":return i+\" DIS\"}}t.defineLocale(\"tlh\",{months:\"tera’ jar wa’_tera’ jar cha’_tera’ jar wej_tera’ jar loS_tera’ jar vagh_tera’ jar jav_tera’ jar Soch_tera’ jar chorgh_tera’ jar Hut_tera’ jar wa’maH_tera’ jar wa’maH wa’_tera’ jar wa’maH cha’\".split(\"_\"),monthsShort:\"jar wa’_jar cha’_jar wej_jar loS_jar vagh_jar jav_jar Soch_jar chorgh_jar Hut_jar wa’maH_jar wa’maH wa’_jar wa’maH cha’\".split(\"_\"),monthsParseExact:!0,weekdays:\"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj\".split(\"_\"),weekdaysShort:\"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj\".split(\"_\"),weekdaysMin:\"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[DaHjaj] LT\",nextDay:\"[wa’leS] LT\",nextWeek:\"LLL\",lastDay:\"[wa’Hu’] LT\",lastWeek:\"LLL\",sameElse:\"L\"},relativeTime:{future:function(e){var t=e;return-1!==e.indexOf(\"jaj\")?t.slice(0,-3)+\"leS\":-1!==e.indexOf(\"jar\")?t.slice(0,-3)+\"waQ\":-1!==e.indexOf(\"DIS\")?t.slice(0,-3)+\"nem\":t+\" pIq\"},past:function(e){var t=e;return-1!==e.indexOf(\"jaj\")?t.slice(0,-3)+\"Hu’\":-1!==e.indexOf(\"jar\")?t.slice(0,-3)+\"wen\":-1!==e.indexOf(\"DIS\")?t.slice(0,-3)+\"ben\":t+\" ret\"},s:\"puS lup\",ss:Di,m:\"wa’ tup\",mm:Di,h:\"wa’ rep\",hh:Di,d:\"wa’ jaj\",dd:Di,M:\"wa’ jar\",MM:Di,y:\"wa’ DIS\",yy:Di},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}});var Ei={1:\"'inci\",5:\"'inci\",8:\"'inci\",70:\"'inci\",80:\"'inci\",2:\"'nci\",7:\"'nci\",20:\"'nci\",50:\"'nci\",3:\"'üncü\",4:\"'üncü\",100:\"'üncü\",6:\"'ncı\",9:\"'uncu\",10:\"'uncu\",30:\"'uncu\",60:\"'ıncı\",90:\"'ıncı\"};function Yi(e,t,n,r){return e={s:[\"viensas secunds\",\"'iensas secunds\"],ss:[e+\" secunds\",e+\" secunds\"],m:[\"'n míut\",\"'iens míut\"],mm:[e+\" míuts\",e+\" míuts\"],h:[\"'n þora\",\"'iensa þora\"],hh:[e+\" þoras\",e+\" þoras\"],d:[\"'n ziua\",\"'iensa ziua\"],dd:[e+\" ziuas\",e+\" ziuas\"],M:[\"'n mes\",\"'iens mes\"],MM:[e+\" mesen\",e+\" mesen\"],y:[\"'n ar\",\"'iens ar\"],yy:[e+\" ars\",e+\" ars\"]},r||t?e[n][0]:e[n][1]}function Ai(e,t,n){return\"m\"===n?t?\"хвилина\":\"хвилину\":\"h\"===n?t?\"година\":\"годину\":e+\" \"+(e=+e,t=(t={ss:t?\"секунда_секунди_секунд\":\"секунду_секунди_секунд\",mm:t?\"хвилина_хвилини_хвилин\":\"хвилину_хвилини_хвилин\",hh:t?\"година_години_годин\":\"годину_години_годин\",dd:\"день_дні_днів\",MM:\"місяць_місяці_місяців\",yy:\"рік_роки_років\"}[n]).split(\"_\"),e%10==1&&e%100!=11?t[0]:2<=e%10&&e%10<=4&&(e%100<10||20<=e%100)?t[1]:t[2])}function Oi(e){return function(){return e+\"о\"+(11===this.hours()?\"б\":\"\")+\"] LT\"}}return t.defineLocale(\"tr\",{months:\"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık\".split(\"_\"),monthsShort:\"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara\".split(\"_\"),weekdays:\"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi\".split(\"_\"),weekdaysShort:\"Paz_Pzt_Sal_Çar_Per_Cum_Cmt\".split(\"_\"),weekdaysMin:\"Pz_Pt_Sa_Ça_Pe_Cu_Ct\".split(\"_\"),meridiem:function(e,t,n){return e<12?n?\"öö\":\"ÖÖ\":n?\"ös\":\"ÖS\"},meridiemParse:/\\xf6\\xf6|\\xd6\\xd6|\\xf6s|\\xd6S/,isPM:function(e){return\"ös\"===e||\"ÖS\"===e},longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[bugün saat] LT\",nextDay:\"[yarın saat] LT\",nextWeek:\"[gelecek] dddd [saat] LT\",lastDay:\"[dün] LT\",lastWeek:\"[geçen] dddd [saat] LT\",sameElse:\"L\"},relativeTime:{future:\"%s sonra\",past:\"%s önce\",s:\"birkaç saniye\",ss:\"%d saniye\",m:\"bir dakika\",mm:\"%d dakika\",h:\"bir saat\",hh:\"%d saat\",d:\"bir gün\",dd:\"%d gün\",w:\"bir hafta\",ww:\"%d hafta\",M:\"bir ay\",MM:\"%d ay\",y:\"bir yıl\",yy:\"%d yıl\"},ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"Do\":case\"DD\":return e;default:var n;return 0===e?e+\"'ıncı\":e+(Ei[n=e%10]||Ei[e%100-n]||Ei[100<=e?100:null])}},week:{dow:1,doy:7}}),t.defineLocale(\"tzl\",{months:\"Januar_Fevraglh_Març_Avrïu_Mai_Gün_Julia_Guscht_Setemvar_Listopäts_Noemvar_Zecemvar\".split(\"_\"),monthsShort:\"Jan_Fev_Mar_Avr_Mai_Gün_Jul_Gus_Set_Lis_Noe_Zec\".split(\"_\"),weekdays:\"Súladi_Lúneçi_Maitzi_Márcuri_Xhúadi_Viénerçi_Sáturi\".split(\"_\"),weekdaysShort:\"Súl_Lún_Mai_Már_Xhú_Vié_Sát\".split(\"_\"),weekdaysMin:\"Sú_Lú_Ma_Má_Xh_Vi_Sá\".split(\"_\"),longDateFormat:{LT:\"HH.mm\",LTS:\"HH.mm.ss\",L:\"DD.MM.YYYY\",LL:\"D. MMMM [dallas] YYYY\",LLL:\"D. MMMM [dallas] YYYY HH.mm\",LLLL:\"dddd, [li] D. MMMM [dallas] YYYY HH.mm\"},meridiemParse:/d\\'o|d\\'a/i,isPM:function(e){return\"d'o\"===e.toLowerCase()},meridiem:function(e,t,n){return 11<e?n?\"d'o\":\"D'O\":n?\"d'a\":\"D'A\"},calendar:{sameDay:\"[oxhi à] LT\",nextDay:\"[demà à] LT\",nextWeek:\"dddd [à] LT\",lastDay:\"[ieiri à] LT\",lastWeek:\"[sür el] dddd [lasteu à] LT\",sameElse:\"L\"},relativeTime:{future:\"osprei %s\",past:\"ja%s\",s:Yi,ss:Yi,m:Yi,mm:Yi,h:Yi,hh:Yi,d:Yi,dd:Yi,M:Yi,MM:Yi,y:Yi,yy:Yi},dayOfMonthOrdinalParse:/\\d{1,2}\\./,ordinal:\"%d.\",week:{dow:1,doy:4}}),t.defineLocale(\"tzm-latn\",{months:\"innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir\".split(\"_\"),monthsShort:\"innayr_brˤayrˤ_marˤsˤ_ibrir_mayyw_ywnyw_ywlywz_ɣwšt_šwtanbir_ktˤwbrˤ_nwwanbir_dwjnbir\".split(\"_\"),weekdays:\"asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas\".split(\"_\"),weekdaysShort:\"asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas\".split(\"_\"),weekdaysMin:\"asamas_aynas_asinas_akras_akwas_asimwas_asiḍyas\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[asdkh g] LT\",nextDay:\"[aska g] LT\",nextWeek:\"dddd [g] LT\",lastDay:\"[assant g] LT\",lastWeek:\"dddd [g] LT\",sameElse:\"L\"},relativeTime:{future:\"dadkh s yan %s\",past:\"yan %s\",s:\"imik\",ss:\"%d imik\",m:\"minuḍ\",mm:\"%d minuḍ\",h:\"saɛa\",hh:\"%d tassaɛin\",d:\"ass\",dd:\"%d ossan\",M:\"ayowr\",MM:\"%d iyyirn\",y:\"asgas\",yy:\"%d isgasn\"},week:{dow:6,doy:12}}),t.defineLocale(\"tzm\",{months:\"ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ\".split(\"_\"),monthsShort:\"ⵉⵏⵏⴰⵢⵔ_ⴱⵕⴰⵢⵕ_ⵎⴰⵕⵚ_ⵉⴱⵔⵉⵔ_ⵎⴰⵢⵢⵓ_ⵢⵓⵏⵢⵓ_ⵢⵓⵍⵢⵓⵣ_ⵖⵓⵛⵜ_ⵛⵓⵜⴰⵏⴱⵉⵔ_ⴽⵟⵓⴱⵕ_ⵏⵓⵡⴰⵏⴱⵉⵔ_ⴷⵓⵊⵏⴱⵉⵔ\".split(\"_\"),weekdays:\"ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ\".split(\"_\"),weekdaysShort:\"ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ\".split(\"_\"),weekdaysMin:\"ⴰⵙⴰⵎⴰⵙ_ⴰⵢⵏⴰⵙ_ⴰⵙⵉⵏⴰⵙ_ⴰⴽⵔⴰⵙ_ⴰⴽⵡⴰⵙ_ⴰⵙⵉⵎⵡⴰⵙ_ⴰⵙⵉⴹⵢⴰⵙ\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[ⴰⵙⴷⵅ ⴴ] LT\",nextDay:\"[ⴰⵙⴽⴰ ⴴ] LT\",nextWeek:\"dddd [ⴴ] LT\",lastDay:\"[ⴰⵚⴰⵏⵜ ⴴ] LT\",lastWeek:\"dddd [ⴴ] LT\",sameElse:\"L\"},relativeTime:{future:\"ⴷⴰⴷⵅ ⵙ ⵢⴰⵏ %s\",past:\"ⵢⴰⵏ %s\",s:\"ⵉⵎⵉⴽ\",ss:\"%d ⵉⵎⵉⴽ\",m:\"ⵎⵉⵏⵓⴺ\",mm:\"%d ⵎⵉⵏⵓⴺ\",h:\"ⵙⴰⵄⴰ\",hh:\"%d ⵜⴰⵙⵙⴰⵄⵉⵏ\",d:\"ⴰⵙⵙ\",dd:\"%d oⵙⵙⴰⵏ\",M:\"ⴰⵢoⵓⵔ\",MM:\"%d ⵉⵢⵢⵉⵔⵏ\",y:\"ⴰⵙⴳⴰⵙ\",yy:\"%d ⵉⵙⴳⴰⵙⵏ\"},week:{dow:6,doy:12}}),t.defineLocale(\"ug-cn\",{months:\"يانۋار_فېۋرال_مارت_ئاپرېل_ماي_ئىيۇن_ئىيۇل_ئاۋغۇست_سېنتەبىر_ئۆكتەبىر_نويابىر_دېكابىر\".split(\"_\"),monthsShort:\"يانۋار_فېۋرال_مارت_ئاپرېل_ماي_ئىيۇن_ئىيۇل_ئاۋغۇست_سېنتەبىر_ئۆكتەبىر_نويابىر_دېكابىر\".split(\"_\"),weekdays:\"يەكشەنبە_دۈشەنبە_سەيشەنبە_چارشەنبە_پەيشەنبە_جۈمە_شەنبە\".split(\"_\"),weekdaysShort:\"يە_دۈ_سە_چا_پە_جۈ_شە\".split(\"_\"),weekdaysMin:\"يە_دۈ_سە_چا_پە_جۈ_شە\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY-MM-DD\",LL:\"YYYY-يىلىM-ئاينىڭD-كۈنى\",LLL:\"YYYY-يىلىM-ئاينىڭD-كۈنى، HH:mm\",LLLL:\"dddd، YYYY-يىلىM-ئاينىڭD-كۈنى، HH:mm\"},meridiemParse:/\\u064a\\u06d0\\u0631\\u0649\\u0645 \\u0643\\u06d0\\u0686\\u06d5|\\u0633\\u06d5\\u06be\\u06d5\\u0631|\\u0686\\u06c8\\u0634\\u062a\\u0649\\u0646 \\u0628\\u06c7\\u0631\\u06c7\\u0646|\\u0686\\u06c8\\u0634|\\u0686\\u06c8\\u0634\\u062a\\u0649\\u0646 \\u0643\\u06d0\\u064a\\u0649\\u0646|\\u0643\\u06d5\\u0686/,meridiemHour:function(e,t){return 12===e&&(e=0),\"يېرىم كېچە\"===t||\"سەھەر\"===t||\"چۈشتىن بۇرۇن\"===t||\"چۈشتىن كېيىن\"!==t&&\"كەچ\"!==t&&11<=e?e:e+12},meridiem:function(e,t,n){return(e=100*e+t)<600?\"يېرىم كېچە\":e<900?\"سەھەر\":e<1130?\"چۈشتىن بۇرۇن\":e<1230?\"چۈش\":e<1800?\"چۈشتىن كېيىن\":\"كەچ\"},calendar:{sameDay:\"[بۈگۈن سائەت] LT\",nextDay:\"[ئەتە سائەت] LT\",nextWeek:\"[كېلەركى] dddd [سائەت] LT\",lastDay:\"[تۆنۈگۈن] LT\",lastWeek:\"[ئالدىنقى] dddd [سائەت] LT\",sameElse:\"L\"},relativeTime:{future:\"%s كېيىن\",past:\"%s بۇرۇن\",s:\"نەچچە سېكونت\",ss:\"%d سېكونت\",m:\"بىر مىنۇت\",mm:\"%d مىنۇت\",h:\"بىر سائەت\",hh:\"%d سائەت\",d:\"بىر كۈن\",dd:\"%d كۈن\",M:\"بىر ئاي\",MM:\"%d ئاي\",y:\"بىر يىل\",yy:\"%d يىل\"},dayOfMonthOrdinalParse:/\\d{1,2}(-\\u0643\\u06c8\\u0646\\u0649|-\\u0626\\u0627\\u064a|-\\u06be\\u06d5\\u067e\\u062a\\u06d5)/,ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"DDD\":return e+\"-كۈنى\";case\"w\":case\"W\":return e+\"-ھەپتە\";default:return e}},preparse:function(e){return e.replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/,/g,\"،\")},week:{dow:1,doy:7}}),t.defineLocale(\"uk\",{months:{format:\"січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня\".split(\"_\"),standalone:\"січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень\".split(\"_\")},monthsShort:\"січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд\".split(\"_\"),weekdays:function(e,t){var n={nominative:\"неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота\".split(\"_\"),accusative:\"неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу\".split(\"_\"),genitive:\"неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи\".split(\"_\")};return!0===e?n.nominative.slice(1,7).concat(n.nominative.slice(0,1)):e?n[/(\\[[\\u0412\\u0432\\u0423\\u0443]\\]) ?dddd/.test(t)?\"accusative\":/\\[?(?:\\u043c\\u0438\\u043d\\u0443\\u043b\\u043e\\u0457|\\u043d\\u0430\\u0441\\u0442\\u0443\\u043f\\u043d\\u043e\\u0457)? ?\\] ?dddd/.test(t)?\"genitive\":\"nominative\"][e.day()]:n.nominative},weekdaysShort:\"нд_пн_вт_ср_чт_пт_сб\".split(\"_\"),weekdaysMin:\"нд_пн_вт_ср_чт_пт_сб\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD.MM.YYYY\",LL:\"D MMMM YYYY р.\",LLL:\"D MMMM YYYY р., HH:mm\",LLLL:\"dddd, D MMMM YYYY р., HH:mm\"},calendar:{sameDay:Oi(\"[Сьогодні \"),nextDay:Oi(\"[Завтра \"),lastDay:Oi(\"[Вчора \"),nextWeek:Oi(\"[У] dddd [\"),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return Oi(\"[Минулої] dddd [\").call(this);case 1:case 2:case 4:return Oi(\"[Минулого] dddd [\").call(this)}},sameElse:\"L\"},relativeTime:{future:\"за %s\",past:\"%s тому\",s:\"декілька секунд\",ss:Ai,m:Ai,mm:Ai,h:\"годину\",hh:Ai,d:\"день\",dd:Ai,M:\"місяць\",MM:Ai,y:\"рік\",yy:Ai},meridiemParse:/\\u043d\\u043e\\u0447\\u0456|\\u0440\\u0430\\u043d\\u043a\\u0443|\\u0434\\u043d\\u044f|\\u0432\\u0435\\u0447\\u043e\\u0440\\u0430/,isPM:function(e){return/^(\\u0434\\u043d\\u044f|\\u0432\\u0435\\u0447\\u043e\\u0440\\u0430)$/.test(e)},meridiem:function(e,t,n){return e<4?\"ночі\":e<12?\"ранку\":e<17?\"дня\":\"вечора\"},dayOfMonthOrdinalParse:/\\d{1,2}-(\\u0439|\\u0433\\u043e)/,ordinal:function(e,t){switch(t){case\"M\":case\"d\":case\"DDD\":case\"w\":case\"W\":return e+\"-й\";case\"D\":return e+\"-го\";default:return e}},week:{dow:1,doy:7}}),Z=[\"جنوری\",\"فروری\",\"مارچ\",\"اپریل\",\"مئی\",\"جون\",\"جولائی\",\"اگست\",\"ستمبر\",\"اکتوبر\",\"نومبر\",\"دسمبر\"],_n=[\"اتوار\",\"پیر\",\"منگل\",\"بدھ\",\"جمعرات\",\"جمعہ\",\"ہفتہ\"],t.defineLocale(\"ur\",{months:Z,monthsShort:Z,weekdays:_n,weekdaysShort:_n,weekdaysMin:_n,longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd، D MMMM YYYY HH:mm\"},meridiemParse:/\\u0635\\u0628\\u062d|\\u0634\\u0627\\u0645/,isPM:function(e){return\"شام\"===e},meridiem:function(e,t,n){return e<12?\"صبح\":\"شام\"},calendar:{sameDay:\"[آج بوقت] LT\",nextDay:\"[کل بوقت] LT\",nextWeek:\"dddd [بوقت] LT\",lastDay:\"[گذشتہ روز بوقت] LT\",lastWeek:\"[گذشتہ] dddd [بوقت] LT\",sameElse:\"L\"},relativeTime:{future:\"%s بعد\",past:\"%s قبل\",s:\"چند سیکنڈ\",ss:\"%d سیکنڈ\",m:\"ایک منٹ\",mm:\"%d منٹ\",h:\"ایک گھنٹہ\",hh:\"%d گھنٹے\",d:\"ایک دن\",dd:\"%d دن\",M:\"ایک ماہ\",MM:\"%d ماہ\",y:\"ایک سال\",yy:\"%d سال\"},preparse:function(e){return e.replace(/\\u060c/g,\",\")},postformat:function(e){return e.replace(/,/g,\"،\")},week:{dow:1,doy:4}}),t.defineLocale(\"uz-latn\",{months:\"Yanvar_Fevral_Mart_Aprel_May_Iyun_Iyul_Avgust_Sentabr_Oktabr_Noyabr_Dekabr\".split(\"_\"),monthsShort:\"Yan_Fev_Mar_Apr_May_Iyun_Iyul_Avg_Sen_Okt_Noy_Dek\".split(\"_\"),weekdays:\"Yakshanba_Dushanba_Seshanba_Chorshanba_Payshanba_Juma_Shanba\".split(\"_\"),weekdaysShort:\"Yak_Dush_Sesh_Chor_Pay_Jum_Shan\".split(\"_\"),weekdaysMin:\"Ya_Du_Se_Cho_Pa_Ju_Sha\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"D MMMM YYYY, dddd HH:mm\"},calendar:{sameDay:\"[Bugun soat] LT [da]\",nextDay:\"[Ertaga] LT [da]\",nextWeek:\"dddd [kuni soat] LT [da]\",lastDay:\"[Kecha soat] LT [da]\",lastWeek:\"[O'tgan] dddd [kuni soat] LT [da]\",sameElse:\"L\"},relativeTime:{future:\"Yaqin %s ichida\",past:\"Bir necha %s oldin\",s:\"soniya\",ss:\"%d soniya\",m:\"bir daqiqa\",mm:\"%d daqiqa\",h:\"bir soat\",hh:\"%d soat\",d:\"bir kun\",dd:\"%d kun\",M:\"bir oy\",MM:\"%d oy\",y:\"bir yil\",yy:\"%d yil\"},week:{dow:1,doy:7}}),t.defineLocale(\"uz\",{months:\"январ_феврал_март_апрел_май_июн_июл_август_сентябр_октябр_ноябр_декабр\".split(\"_\"),monthsShort:\"янв_фев_мар_апр_май_июн_июл_авг_сен_окт_ноя_дек\".split(\"_\"),weekdays:\"Якшанба_Душанба_Сешанба_Чоршанба_Пайшанба_Жума_Шанба\".split(\"_\"),weekdaysShort:\"Якш_Душ_Сеш_Чор_Пай_Жум_Шан\".split(\"_\"),weekdaysMin:\"Як_Ду_Се_Чо_Па_Жу_Ша\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"D MMMM YYYY, dddd HH:mm\"},calendar:{sameDay:\"[Бугун соат] LT [да]\",nextDay:\"[Эртага] LT [да]\",nextWeek:\"dddd [куни соат] LT [да]\",lastDay:\"[Кеча соат] LT [да]\",lastWeek:\"[Утган] dddd [куни соат] LT [да]\",sameElse:\"L\"},relativeTime:{future:\"Якин %s ичида\",past:\"Бир неча %s олдин\",s:\"фурсат\",ss:\"%d фурсат\",m:\"бир дакика\",mm:\"%d дакика\",h:\"бир соат\",hh:\"%d соат\",d:\"бир кун\",dd:\"%d кун\",M:\"бир ой\",MM:\"%d ой\",y:\"бир йил\",yy:\"%d йил\"},week:{dow:1,doy:7}}),t.defineLocale(\"vi\",{months:\"tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12\".split(\"_\"),monthsShort:\"Thg 01_Thg 02_Thg 03_Thg 04_Thg 05_Thg 06_Thg 07_Thg 08_Thg 09_Thg 10_Thg 11_Thg 12\".split(\"_\"),monthsParseExact:!0,weekdays:\"chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy\".split(\"_\"),weekdaysShort:\"CN_T2_T3_T4_T5_T6_T7\".split(\"_\"),weekdaysMin:\"CN_T2_T3_T4_T5_T6_T7\".split(\"_\"),weekdaysParseExact:!0,meridiemParse:/sa|ch/i,isPM:function(e){return/^ch$/i.test(e)},meridiem:function(e,t,n){return e<12?n?\"sa\":\"SA\":n?\"ch\":\"CH\"},longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM [năm] YYYY\",LLL:\"D MMMM [năm] YYYY HH:mm\",LLLL:\"dddd, D MMMM [năm] YYYY HH:mm\",l:\"DD/M/YYYY\",ll:\"D MMM YYYY\",lll:\"D MMM YYYY HH:mm\",llll:\"ddd, D MMM YYYY HH:mm\"},calendar:{sameDay:\"[Hôm nay lúc] LT\",nextDay:\"[Ngày mai lúc] LT\",nextWeek:\"dddd [tuần tới lúc] LT\",lastDay:\"[Hôm qua lúc] LT\",lastWeek:\"dddd [tuần trước lúc] LT\",sameElse:\"L\"},relativeTime:{future:\"%s tới\",past:\"%s trước\",s:\"vài giây\",ss:\"%d giây\",m:\"một phút\",mm:\"%d phút\",h:\"một giờ\",hh:\"%d giờ\",d:\"một ngày\",dd:\"%d ngày\",w:\"một tuần\",ww:\"%d tuần\",M:\"một tháng\",MM:\"%d tháng\",y:\"một năm\",yy:\"%d năm\"},dayOfMonthOrdinalParse:/\\d{1,2}/,ordinal:function(e){return e},week:{dow:1,doy:4}}),t.defineLocale(\"x-pseudo\",{months:\"J~áñúá~rý_F~ébrú~árý_~Márc~h_Áp~ríl_~Máý_~Júñé~_Júl~ý_Áú~gúst~_Sép~témb~ér_Ó~ctób~ér_Ñ~óvém~bér_~Décé~mbér\".split(\"_\"),monthsShort:\"J~áñ_~Féb_~Már_~Ápr_~Máý_~Júñ_~Júl_~Áúg_~Sép_~Óct_~Ñóv_~Déc\".split(\"_\"),monthsParseExact:!0,weekdays:\"S~úñdá~ý_Mó~ñdáý~_Túé~sdáý~_Wéd~ñésd~áý_T~húrs~dáý_~Fríd~áý_S~átúr~dáý\".split(\"_\"),weekdaysShort:\"S~úñ_~Móñ_~Túé_~Wéd_~Thú_~Frí_~Sát\".split(\"_\"),weekdaysMin:\"S~ú_Mó~_Tú_~Wé_T~h_Fr~_Sá\".split(\"_\"),weekdaysParseExact:!0,longDateFormat:{LT:\"HH:mm\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},calendar:{sameDay:\"[T~ódá~ý át] LT\",nextDay:\"[T~ómó~rró~w át] LT\",nextWeek:\"dddd [át] LT\",lastDay:\"[Ý~ést~érdá~ý át] LT\",lastWeek:\"[L~ást] dddd [át] LT\",sameElse:\"L\"},relativeTime:{future:\"í~ñ %s\",past:\"%s á~gó\",s:\"á ~féw ~sécó~ñds\",ss:\"%d s~écóñ~ds\",m:\"á ~míñ~úté\",mm:\"%d m~íñú~tés\",h:\"á~ñ hó~úr\",hh:\"%d h~óúrs\",d:\"á ~dáý\",dd:\"%d d~áýs\",M:\"á ~móñ~th\",MM:\"%d m~óñt~hs\",y:\"á ~ýéár\",yy:\"%d ý~éárs\"},dayOfMonthOrdinalParse:/\\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10;return e+(1==~~(e%100/10)?\"th\":1==t?\"st\":2==t?\"nd\":3==t?\"rd\":\"th\")},week:{dow:1,doy:4}}),t.defineLocale(\"yo\",{months:\"Sẹ́rẹ́_Èrèlè_Ẹrẹ̀nà_Ìgbé_Èbibi_Òkùdu_Agẹmo_Ògún_Owewe_Ọ̀wàrà_Bélú_Ọ̀pẹ̀̀\".split(\"_\"),monthsShort:\"Sẹ́r_Èrl_Ẹrn_Ìgb_Èbi_Òkù_Agẹ_Ògú_Owe_Ọ̀wà_Bél_Ọ̀pẹ̀̀\".split(\"_\"),weekdays:\"Àìkú_Ajé_Ìsẹ́gun_Ọjọ́rú_Ọjọ́bọ_Ẹtì_Àbámẹ́ta\".split(\"_\"),weekdaysShort:\"Àìk_Ajé_Ìsẹ́_Ọjr_Ọjb_Ẹtì_Àbá\".split(\"_\"),weekdaysMin:\"Àì_Aj_Ìs_Ọr_Ọb_Ẹt_Àb\".split(\"_\"),longDateFormat:{LT:\"h:mm A\",LTS:\"h:mm:ss A\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY h:mm A\",LLLL:\"dddd, D MMMM YYYY h:mm A\"},calendar:{sameDay:\"[Ònì ni] LT\",nextDay:\"[Ọ̀la ni] LT\",nextWeek:\"dddd [Ọsẹ̀ tón'bọ] [ni] LT\",lastDay:\"[Àna ni] LT\",lastWeek:\"dddd [Ọsẹ̀ tólọ́] [ni] LT\",sameElse:\"L\"},relativeTime:{future:\"ní %s\",past:\"%s kọjá\",s:\"ìsẹjú aayá die\",ss:\"aayá %d\",m:\"ìsẹjú kan\",mm:\"ìsẹjú %d\",h:\"wákati kan\",hh:\"wákati %d\",d:\"ọjọ́ kan\",dd:\"ọjọ́ %d\",M:\"osù kan\",MM:\"osù %d\",y:\"ọdún kan\",yy:\"ọdún %d\"},dayOfMonthOrdinalParse:/\\u1ecdj\\u1ecd\\u0301\\s\\d{1,2}/,ordinal:\"ọjọ́ %d\",week:{dow:1,doy:4}}),t.defineLocale(\"zh-cn\",{months:\"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月\".split(\"_\"),monthsShort:\"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月\".split(\"_\"),weekdays:\"星期日_星期一_星期二_星期三_星期四_星期五_星期六\".split(\"_\"),weekdaysShort:\"周日_周一_周二_周三_周四_周五_周六\".split(\"_\"),weekdaysMin:\"日_一_二_三_四_五_六\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY/MM/DD\",LL:\"YYYY年M月D日\",LLL:\"YYYY年M月D日Ah点mm分\",LLLL:\"YYYY年M月D日ddddAh点mm分\",l:\"YYYY/M/D\",ll:\"YYYY年M月D日\",lll:\"YYYY年M月D日 HH:mm\",llll:\"YYYY年M月D日dddd HH:mm\"},meridiemParse:/\\u51cc\\u6668|\\u65e9\\u4e0a|\\u4e0a\\u5348|\\u4e2d\\u5348|\\u4e0b\\u5348|\\u665a\\u4e0a/,meridiemHour:function(e,t){return 12===e&&(e=0),\"凌晨\"===t||\"早上\"===t||\"上午\"===t||\"下午\"!==t&&\"晚上\"!==t&&11<=e?e:e+12},meridiem:function(e,t,n){return(e=100*e+t)<600?\"凌晨\":e<900?\"早上\":e<1130?\"上午\":e<1230?\"中午\":e<1800?\"下午\":\"晚上\"},calendar:{sameDay:\"[今天]LT\",nextDay:\"[明天]LT\",nextWeek:function(e){return e.week()!==this.week()?\"[下]dddLT\":\"[本]dddLT\"},lastDay:\"[昨天]LT\",lastWeek:function(e){return this.week()!==e.week()?\"[上]dddLT\":\"[本]dddLT\"},sameElse:\"L\"},dayOfMonthOrdinalParse:/\\d{1,2}(\\u65e5|\\u6708|\\u5468)/,ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"DDD\":return e+\"日\";case\"M\":return e+\"月\";case\"w\":case\"W\":return e+\"周\";default:return e}},relativeTime:{future:\"%s后\",past:\"%s前\",s:\"几秒\",ss:\"%d 秒\",m:\"1 分钟\",mm:\"%d 分钟\",h:\"1 小时\",hh:\"%d 小时\",d:\"1 天\",dd:\"%d 天\",w:\"1 周\",ww:\"%d 周\",M:\"1 个月\",MM:\"%d 个月\",y:\"1 年\",yy:\"%d 年\"},week:{dow:1,doy:4}}),t.defineLocale(\"zh-hk\",{months:\"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月\".split(\"_\"),monthsShort:\"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月\".split(\"_\"),weekdays:\"星期日_星期一_星期二_星期三_星期四_星期五_星期六\".split(\"_\"),weekdaysShort:\"週日_週一_週二_週三_週四_週五_週六\".split(\"_\"),weekdaysMin:\"日_一_二_三_四_五_六\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY/MM/DD\",LL:\"YYYY年M月D日\",LLL:\"YYYY年M月D日 HH:mm\",LLLL:\"YYYY年M月D日dddd HH:mm\",l:\"YYYY/M/D\",ll:\"YYYY年M月D日\",lll:\"YYYY年M月D日 HH:mm\",llll:\"YYYY年M月D日dddd HH:mm\"},meridiemParse:/\\u51cc\\u6668|\\u65e9\\u4e0a|\\u4e0a\\u5348|\\u4e2d\\u5348|\\u4e0b\\u5348|\\u665a\\u4e0a/,meridiemHour:function(e,t){return 12===e&&(e=0),\"凌晨\"===t||\"早上\"===t||\"上午\"===t?e:\"中午\"===t?11<=e?e:e+12:\"下午\"===t||\"晚上\"===t?e+12:void 0},meridiem:function(e,t,n){return(e=100*e+t)<600?\"凌晨\":e<900?\"早上\":e<1200?\"上午\":1200===e?\"中午\":e<1800?\"下午\":\"晚上\"},calendar:{sameDay:\"[今天]LT\",nextDay:\"[明天]LT\",nextWeek:\"[下]ddddLT\",lastDay:\"[昨天]LT\",lastWeek:\"[上]ddddLT\",sameElse:\"L\"},dayOfMonthOrdinalParse:/\\d{1,2}(\\u65e5|\\u6708|\\u9031)/,ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"DDD\":return e+\"日\";case\"M\":return e+\"月\";case\"w\":case\"W\":return e+\"週\";default:return e}},relativeTime:{future:\"%s後\",past:\"%s前\",s:\"幾秒\",ss:\"%d 秒\",m:\"1 分鐘\",mm:\"%d 分鐘\",h:\"1 小時\",hh:\"%d 小時\",d:\"1 天\",dd:\"%d 天\",M:\"1 個月\",MM:\"%d 個月\",y:\"1 年\",yy:\"%d 年\"}}),t.defineLocale(\"zh-mo\",{months:\"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月\".split(\"_\"),monthsShort:\"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月\".split(\"_\"),weekdays:\"星期日_星期一_星期二_星期三_星期四_星期五_星期六\".split(\"_\"),weekdaysShort:\"週日_週一_週二_週三_週四_週五_週六\".split(\"_\"),weekdaysMin:\"日_一_二_三_四_五_六\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"YYYY年M月D日\",LLL:\"YYYY年M月D日 HH:mm\",LLLL:\"YYYY年M月D日dddd HH:mm\",l:\"D/M/YYYY\",ll:\"YYYY年M月D日\",lll:\"YYYY年M月D日 HH:mm\",llll:\"YYYY年M月D日dddd HH:mm\"},meridiemParse:/\\u51cc\\u6668|\\u65e9\\u4e0a|\\u4e0a\\u5348|\\u4e2d\\u5348|\\u4e0b\\u5348|\\u665a\\u4e0a/,meridiemHour:function(e,t){return 12===e&&(e=0),\"凌晨\"===t||\"早上\"===t||\"上午\"===t?e:\"中午\"===t?11<=e?e:e+12:\"下午\"===t||\"晚上\"===t?e+12:void 0},meridiem:function(e,t,n){return(e=100*e+t)<600?\"凌晨\":e<900?\"早上\":e<1130?\"上午\":e<1230?\"中午\":e<1800?\"下午\":\"晚上\"},calendar:{sameDay:\"[今天] LT\",nextDay:\"[明天] LT\",nextWeek:\"[下]dddd LT\",lastDay:\"[昨天] LT\",lastWeek:\"[上]dddd LT\",sameElse:\"L\"},dayOfMonthOrdinalParse:/\\d{1,2}(\\u65e5|\\u6708|\\u9031)/,ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"DDD\":return e+\"日\";case\"M\":return e+\"月\";case\"w\":case\"W\":return e+\"週\";default:return e}},relativeTime:{future:\"%s內\",past:\"%s前\",s:\"幾秒\",ss:\"%d 秒\",m:\"1 分鐘\",mm:\"%d 分鐘\",h:\"1 小時\",hh:\"%d 小時\",d:\"1 天\",dd:\"%d 天\",M:\"1 個月\",MM:\"%d 個月\",y:\"1 年\",yy:\"%d 年\"}}),t.defineLocale(\"zh-tw\",{months:\"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月\".split(\"_\"),monthsShort:\"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月\".split(\"_\"),weekdays:\"星期日_星期一_星期二_星期三_星期四_星期五_星期六\".split(\"_\"),weekdaysShort:\"週日_週一_週二_週三_週四_週五_週六\".split(\"_\"),weekdaysMin:\"日_一_二_三_四_五_六\".split(\"_\"),longDateFormat:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"YYYY/MM/DD\",LL:\"YYYY年M月D日\",LLL:\"YYYY年M月D日 HH:mm\",LLLL:\"YYYY年M月D日dddd HH:mm\",l:\"YYYY/M/D\",ll:\"YYYY年M月D日\",lll:\"YYYY年M月D日 HH:mm\",llll:\"YYYY年M月D日dddd HH:mm\"},meridiemParse:/\\u51cc\\u6668|\\u65e9\\u4e0a|\\u4e0a\\u5348|\\u4e2d\\u5348|\\u4e0b\\u5348|\\u665a\\u4e0a/,meridiemHour:function(e,t){return 12===e&&(e=0),\"凌晨\"===t||\"早上\"===t||\"上午\"===t?e:\"中午\"===t?11<=e?e:e+12:\"下午\"===t||\"晚上\"===t?e+12:void 0},meridiem:function(e,t,n){return(e=100*e+t)<600?\"凌晨\":e<900?\"早上\":e<1130?\"上午\":e<1230?\"中午\":e<1800?\"下午\":\"晚上\"},calendar:{sameDay:\"[今天] LT\",nextDay:\"[明天] LT\",nextWeek:\"[下]dddd LT\",lastDay:\"[昨天] LT\",lastWeek:\"[上]dddd LT\",sameElse:\"L\"},dayOfMonthOrdinalParse:/\\d{1,2}(\\u65e5|\\u6708|\\u9031)/,ordinal:function(e,t){switch(t){case\"d\":case\"D\":case\"DDD\":return e+\"日\";case\"M\":return e+\"月\";case\"w\":case\"W\":return e+\"週\";default:return e}},relativeTime:{future:\"%s後\",past:\"%s前\",s:\"幾秒\",ss:\"%d 秒\",m:\"1 分鐘\",mm:\"%d 分鐘\",h:\"1 小時\",hh:\"%d 小時\",d:\"1 天\",dd:\"%d 天\",M:\"1 個月\",MM:\"%d 個月\",y:\"1 年\",yy:\"%d 年\"}}),t.locale(\"en\"),t}),function(e){\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?module.exports=e():window.wNumb=e()}(function(){\"use strict\";var e=[\"decimals\",\"thousand\",\"mark\",\"prefix\",\"suffix\",\"encoder\",\"decoder\",\"negativeBefore\",\"negative\",\"edit\",\"undo\"];function t(e){return e.split(\"\").reverse().join(\"\")}function n(e,t){return e.substring(0,t.length)===t}function r(e,t,n){if((e[t]||e[n])&&e[t]===e[n])throw new Error(t)}function i(e){return\"number\"==typeof e&&isFinite(e)}function a(e,n,r,a,o,s,l,u,d,c,f,p){var h,m,v,g,_,y=p,b=\"\",M=\"\";return s&&(p=s(p)),!!i(p)&&(!1!==e&&0===parseFloat(p.toFixed(e))&&(p=0),p<0&&(h=!0,p=Math.abs(p)),!1!==e&&(_=e,g=(g=p).toString().split(\"e\"),p=(+((g=(g=Math.round(+(g[0]+\"e\"+(g[1]?+g[1]+_:_)))).toString().split(\"e\"))[0]+\"e\"+(g[1]?+g[1]-_:-_))).toFixed(_)),-1!==(p=p.toString()).indexOf(\".\")?(v=(m=p.split(\".\"))[0],r&&(b=r+m[1])):v=p,n&&(v=t(v).match(/.{1,3}/g),v=t(v.join(t(n)))),h&&u&&(M+=u),a&&(M+=a),h&&d&&(M+=d),M+=v,M+=b,o&&(M+=o),c&&(M=c(M,y)),M)}function o(e,t,r,a,o,s,l,u,d,c,f,p){var h,m=\"\";return f&&(p=f(p)),!(!p||\"string\"!=typeof p)&&(u&&n(p,u)&&(p=p.replace(u,\"\"),h=!0),a&&n(p,a)&&(p=p.replace(a,\"\")),d&&n(p,d)&&(p=p.replace(d,\"\"),h=!0),o&&function(e,t){return e.slice(-1*t.length)===t}(p,o)&&(p=p.slice(0,-1*o.length)),t&&(p=p.split(t).join(\"\")),r&&(p=p.replace(r,\".\")),h&&(m+=\"-\"),\"\"!==(m=(m+=p).replace(/[^0-9\\.\\-.]/g,\"\"))&&(m=Number(m),l&&(m=l(m)),!!i(m)&&m))}function s(t,n,r){var i,a=[];for(i=0;i<e.length;i+=1)a.push(t[e[i]]);return a.push(r),n.apply(\"\",a)}return function t(n){if(!(this instanceof t))return new t(n);\"object\"==typeof n&&(n=function(t){var n,i,a,o={};for(void 0===t.suffix&&(t.suffix=t.postfix),n=0;n<e.length;n+=1)if(void 0===(a=t[i=e[n]]))\"negative\"!==i||o.negativeBefore?\"mark\"===i&&\".\"!==o.thousand?o[i]=\".\":o[i]=!1:o[i]=\"-\";else if(\"decimals\"===i){if(!(a>=0&&a<8))throw new Error(i);o[i]=a}else if(\"encoder\"===i||\"decoder\"===i||\"edit\"===i||\"undo\"===i){if(\"function\"!=typeof a)throw new Error(i);o[i]=a}else{if(\"string\"!=typeof a)throw new Error(i);o[i]=a}return r(o,\"mark\",\"thousand\"),r(o,\"prefix\",\"negative\"),r(o,\"prefix\",\"negativeBefore\"),o}(n),this.to=function(e){return s(n,a,e)},this.from=function(e){return s(n,o,e)})}}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).axios=t()}(this,function(){\"use strict\";function e(e,t){this.v=e,this.k=t}function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function n(t){var n={},r=!1;function i(n,i){return r=!0,i=new Promise(function(e){e(t[n](i))}),{done:!1,value:new e(i,1)}}return n[\"undefined\"!=typeof Symbol&&Symbol.iterator||\"@@iterator\"]=function(){return this},n.next=function(e){return r?(r=!1,e):i(\"next\",e)},\"function\"==typeof t.throw&&(n.throw=function(e){if(r)throw r=!1,e;return i(\"throw\",e)}),\"function\"==typeof t.return&&(n.return=function(e){return r?(r=!1,e):i(\"return\",e)}),n}function r(e){var t,n,r,a=2;for(\"undefined\"!=typeof Symbol&&(n=Symbol.asyncIterator,r=Symbol.iterator);a--;){if(n&&null!=(t=e[n]))return t.call(e);if(r&&null!=(t=e[r]))return new i(t.call(e));n=\"@@asyncIterator\",r=\"@@iterator\"}throw new TypeError(\"Object is not async iterable\")}function i(e){function t(e){if(Object(e)!==e)return Promise.reject(new TypeError(e+\" is not an object.\"));var t=e.done;return Promise.resolve(e.value).then(function(e){return{value:e,done:t}})}return i=function(e){this.s=e,this.n=e.next},i.prototype={s:null,n:null,next:function(){return t(this.n.apply(this.s,arguments))},return:function(e){var n=this.s.return;return void 0===n?Promise.resolve({value:e,done:!0}):t(n.apply(this.s,arguments))},throw:function(e){var n=this.s.return;return void 0===n?Promise.reject(e):t(n.apply(this.s,arguments))}},new i(e)}function a(e,t,n,r,i,a,o){try{var s=e[a](o),l=s.value}catch(e){return void n(e)}s.done?t(l):Promise.resolve(l).then(r,i)}function o(e){return function(){var t=this,n=arguments;return new Promise(function(r,i){var o=e.apply(t,n);function s(e){a(o,r,i,s,l,\"next\",e)}function l(e){a(o,r,i,s,l,\"throw\",e)}s(void 0)})}}function s(t){return new e(t,0)}function l(e,t,n){return t=p(t),function(e,t){if(t&&(\"object\"==typeof t||\"function\"==typeof t))return t;if(void 0!==t)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(e){if(void 0===e)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return e}(e)}(e,m()?Reflect.construct(t,n||[],p(e).constructor):t.apply(e,n))}function u(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}function d(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\"value\"in r&&(r.writable=!0),Object.defineProperty(e,L(r.key),r)}}function c(e,t,n){return t&&d(e.prototype,t),n&&d(e,n),Object.defineProperty(e,\"prototype\",{writable:!1}),e}function f(e,t,n){return(t=L(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function p(e){return p=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},p(e)}function h(e,t){if(\"function\"!=typeof t&&null!==t)throw new TypeError(\"Super expression must either be null or a function\");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,\"prototype\",{writable:!1}),t&&M(e,t)}function m(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(e){}return(m=function(){return!!e})()}function v(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function g(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?v(Object(n),!0).forEach(function(t){f(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):v(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function _(){\n/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */\nvar e,t,n=\"function\"==typeof Symbol?Symbol:{},r=n.iterator||\"@@iterator\",i=n.toStringTag||\"@@toStringTag\";function a(n,r,i,a){var l=r&&r.prototype instanceof s?r:s,u=Object.create(l.prototype);return y(u,\"_invoke\",function(n,r,i){var a,s,l,u=0,d=i||[],c=!1,f={p:0,n:0,v:e,a:p,f:p.bind(e,4),d:function(t,n){return a=t,s=0,l=e,f.n=n,o}};function p(n,r){for(s=n,l=r,t=0;!c&&u&&!i&&t<d.length;t++){var i,a=d[t],p=f.p,h=a[2];n>3?(i=h===r)&&(l=a[(s=a[4])?5:(s=3,3)],a[4]=a[5]=e):a[0]<=p&&((i=n<2&&p<a[1])?(s=0,f.v=r,f.n=a[1]):p<h&&(i=n<3||a[0]>r||r>h)&&(a[4]=n,a[5]=r,f.n=h,s=0))}if(i||n>1)return o;throw c=!0,r}return function(i,d,h){if(u>1)throw TypeError(\"Generator is already running\");for(c&&1===d&&p(d,h),s=d,l=h;(t=s<2?e:l)||!c;){a||(s?s<3?(s>1&&(f.n=-1),p(s,l)):f.n=l:f.v=l);try{if(u=2,a){if(s||(i=\"next\"),t=a[i]){if(!(t=t.call(a,l)))throw TypeError(\"iterator result is not an object\");if(!t.done)return t;l=t.value,s<2&&(s=0)}else 1===s&&(t=a.return)&&t.call(a),s<2&&(l=TypeError(\"The iterator does not provide a '\"+i+\"' method\"),s=1);a=e}else if((t=(c=f.n<0)?l:n.call(r,f))!==o)break}catch(t){a=e,s=1,l=t}finally{u=1}}return{value:t,done:c}}}(n,i,a),!0),u}var o={};function s(){}function l(){}function u(){}t=Object.getPrototypeOf;var d=[][r]?t(t([][r]())):(y(t={},r,function(){return this}),t),c=u.prototype=s.prototype=Object.create(d);function f(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,u):(e.__proto__=u,y(e,i,\"GeneratorFunction\")),e.prototype=Object.create(c),e}return l.prototype=u,y(c,\"constructor\",u),y(u,\"constructor\",l),l.displayName=\"GeneratorFunction\",y(u,i,\"GeneratorFunction\"),y(c),y(c,i,\"Generator\"),y(c,r,function(){return this}),y(c,\"toString\",function(){return\"[object Generator]\"}),(_=function(){return{w:a,m:f}})()}function y(e,t,n,r){var i=Object.defineProperty;try{i({},\"\",{})}catch(e){i=0}y=function(e,t,n,r){function a(t,n){y(e,t,function(e){return this._invoke(t,n,e)})}t?i?i(e,t,{value:n,enumerable:!r,configurable:!r,writable:!r}):e[t]=n:(a(\"next\",0),a(\"throw\",1),a(\"return\",2))},y(e,t,n,r)}function b(e){if(null!=e){var t=e[\"function\"==typeof Symbol&&Symbol.iterator||\"@@iterator\"],n=0;if(t)return t.call(e);if(\"function\"==typeof e.next)return e;if(!isNaN(e.length))return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}}}throw new TypeError(typeof e+\" is not iterable\")}function M(e,t){return M=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},M(e,t)}function w(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:\"undefined\"!=typeof Symbol&&e[Symbol.iterator]||e[\"@@iterator\"];if(null!=n){var r,i,a,o,s=[],l=!0,u=!1;try{if(a=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=a.call(n)).done)&&(s.push(r.value),s.length!==t);l=!0);}catch(e){u=!0,i=e}finally{try{if(!l&&null!=n.return&&(o=n.return(),Object(o)!==o))return}finally{if(u)throw i}}return s}}(e,t)||T(e,t)||function(){throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}function k(e){return function(e){if(Array.isArray(e))return t(e)}(e)||function(e){if(\"undefined\"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e[\"@@iterator\"])return Array.from(e)}(e)||T(e)||function(){throw new TypeError(\"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}function L(e){var t=function(e){if(\"object\"!=typeof e||!e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var n=t.call(e,\"string\");if(\"object\"!=typeof n)return n;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return String(e)}(e);return\"symbol\"==typeof t?t:t+\"\"}function x(e){return x=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},x(e)}function T(e,n){if(e){if(\"string\"==typeof e)return t(e,n);var r={}.toString.call(e).slice(8,-1);return\"Object\"===r&&e.constructor&&(r=e.constructor.name),\"Map\"===r||\"Set\"===r?Array.from(e):\"Arguments\"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?t(e,n):void 0}}function S(e){return function(){return new D(e.apply(this,arguments))}}function D(t){var n,r;function i(n,r){try{var o=t[n](r),s=o.value,l=s instanceof e;Promise.resolve(l?s.v:s).then(function(e){if(l){var r=\"return\"===n?\"return\":\"next\";if(!s.k||e.done)return i(r,e);e=t[r](e).value}a(o.done?\"return\":\"normal\",e)},function(e){i(\"throw\",e)})}catch(e){a(\"throw\",e)}}function a(e,t){switch(e){case\"return\":n.resolve({value:t,done:!0});break;case\"throw\":n.reject(t);break;default:n.resolve({value:t,done:!1})}(n=n.next)?i(n.key,n.arg):r=null}this._invoke=function(e,t){return new Promise(function(a,o){var s={key:e,arg:t,resolve:a,reject:o,next:null};r?r=r.next=s:(n=r=s,i(e,t))})},\"function\"!=typeof t.return&&(this.return=void 0)}function E(e){var t=\"function\"==typeof Map?new Map:void 0;return E=function(e){if(null===e||!function(e){try{return-1!==Function.toString.call(e).indexOf(\"[native code]\")}catch(t){return\"function\"==typeof e}}(e))return e;if(\"function\"!=typeof e)throw new TypeError(\"Super expression must either be null or a function\");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return function(e,t,n){if(m())return Reflect.construct.apply(null,arguments);var r=[null];r.push.apply(r,t);var i=new(e.bind.apply(e,r));return n&&M(i,n.prototype),i}(e,arguments,p(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),M(n,e)},E(e)}function Y(e,t){return function(){return e.apply(t,arguments)}}D.prototype[\"function\"==typeof Symbol&&Symbol.asyncIterator||\"@@asyncIterator\"]=function(){return this},D.prototype.next=function(e){return this._invoke(\"next\",e)},D.prototype.throw=function(e){return this._invoke(\"throw\",e)},D.prototype.return=function(e){return this._invoke(\"return\",e)};var A,O=Object.prototype.toString,C=Object.getPrototypeOf,j=Symbol.iterator,P=Symbol.toStringTag,H=(A=Object.create(null),function(e){var t=O.call(e);return A[t]||(A[t]=t.slice(8,-1).toLowerCase())}),I=function(e){return e=e.toLowerCase(),function(t){return H(t)===e}},F=function(e){return function(t){return x(t)===e}},N=Array.isArray,R=F(\"undefined\");function V(e){return null!==e&&!R(e)&&null!==e.constructor&&!R(e.constructor)&&W(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}var $=I(\"ArrayBuffer\"),z=F(\"string\"),W=F(\"function\"),U=F(\"number\"),B=function(e){return null!==e&&\"object\"===x(e)},q=function(e){if(\"object\"!==H(e))return!1;var t=C(e);return!(null!==t&&t!==Object.prototype&&null!==Object.getPrototypeOf(t)||P in e||j in e)},G=I(\"Date\"),J=I(\"File\"),Z=I(\"Blob\"),K=I(\"FileList\"),X=I(\"URLSearchParams\"),Q=w([\"ReadableStream\",\"Request\",\"Response\",\"Headers\"].map(I),4),ee=Q[0],te=Q[1],ne=Q[2],re=Q[3];function ie(e,t){var n,r,i=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).allOwnKeys,a=void 0!==i&&i;if(null!=e)if(\"object\"!==x(e)&&(e=[e]),N(e))for(n=0,r=e.length;n<r;n++)t.call(null,e[n],n,e);else{if(V(e))return;var o,s=a?Object.getOwnPropertyNames(e):Object.keys(e),l=s.length;for(n=0;n<l;n++)o=s[n],t.call(null,e[o],o,e)}}function ae(e,t){if(V(e))return null;t=t.toLowerCase();for(var n,r=Object.keys(e),i=r.length;i-- >0;)if(t===(n=r[i]).toLowerCase())return n;return null}var oe,se,le,ue,de,ce=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof self?self:\"undefined\"!=typeof window?window:global,fe=function(e){return!R(e)&&e!==ce},pe=(oe=\"undefined\"!=typeof Uint8Array&&C(Uint8Array),function(e){return oe&&e instanceof oe}),he=I(\"HTMLFormElement\"),me=function(){var e=Object.prototype.hasOwnProperty;return function(t,n){return e.call(t,n)}}(),ve=I(\"RegExp\"),ge=function(e,t){var n=Object.getOwnPropertyDescriptors(e),r={};ie(n,function(n,i){var a;!1!==(a=t(n,i,e))&&(r[i]=a||n)}),Object.defineProperties(e,r)},_e=I(\"AsyncFunction\"),ye=(se=\"function\"==typeof setImmediate,le=W(ce.postMessage),se?setImmediate:le?(ue=\"axios@\".concat(Math.random()),de=[],ce.addEventListener(\"message\",function(e){var t=e.source,n=e.data;t===ce&&n===ue&&de.length&&de.shift()()},!1),function(e){de.push(e),ce.postMessage(ue,\"*\")}):function(e){return setTimeout(e)}),be=\"undefined\"!=typeof queueMicrotask?queueMicrotask.bind(ce):\"undefined\"!=typeof process&&process.nextTick||ye,Me={isArray:N,isArrayBuffer:$,isBuffer:V,isFormData:function(e){var t;return e&&(\"function\"==typeof FormData&&e instanceof FormData||W(e.append)&&(\"formdata\"===(t=H(e))||\"object\"===t&&W(e.toString)&&\"[object FormData]\"===e.toString()))},isArrayBufferView:function(e){return\"undefined\"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&$(e.buffer)},isString:z,isNumber:U,isBoolean:function(e){return!0===e||!1===e},isObject:B,isPlainObject:q,isEmptyObject:function(e){if(!B(e)||V(e))return!1;try{return 0===Object.keys(e).length&&Object.getPrototypeOf(e)===Object.prototype}catch(e){return!1}},isReadableStream:ee,isRequest:te,isResponse:ne,isHeaders:re,isUndefined:R,isDate:G,isFile:J,isBlob:Z,isRegExp:ve,isFunction:W,isStream:function(e){return B(e)&&W(e.pipe)},isURLSearchParams:X,isTypedArray:pe,isFileList:K,forEach:ie,merge:function e(){for(var t=fe(this)&&this||{},n=t.caseless,r=t.skipUndefined,i={},a=function(t,a){if(\"__proto__\"!==a&&\"constructor\"!==a&&\"prototype\"!==a){var o=n&&ae(i,a)||a;q(i[o])&&q(t)?i[o]=e(i[o],t):q(t)?i[o]=e({},t):N(t)?i[o]=t.slice():r&&R(t)||(i[o]=t)}},o=0,s=arguments.length;o<s;o++)arguments[o]&&ie(arguments[o],a);return i},extend:function(e,t,n){return ie(t,function(t,r){n&&W(t)?Object.defineProperty(e,r,{value:Y(t,n),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(e,r,{value:t,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:(arguments.length>3&&void 0!==arguments[3]?arguments[3]:{}).allOwnKeys}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g,\"\")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,n,r){e.prototype=Object.create(t.prototype,r),Object.defineProperty(e.prototype,\"constructor\",{value:e,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(e,\"super\",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:function(e,t,n,r){var i,a,o,s={};if(t=t||{},null==e)return t;do{for(a=(i=Object.getOwnPropertyNames(e)).length;a-- >0;)o=i[a],r&&!r(o,e,t)||s[o]||(t[o]=e[o],s[o]=!0);e=!1!==n&&C(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:H,kindOfTest:I,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;if(N(e))return e;var t=e.length;if(!U(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},forEachEntry:function(e,t){for(var n,r=(e&&e[j]).call(e);(n=r.next())&&!n.done;){var i=n.value;t.call(e,i[0],i[1])}},matchAll:function(e,t){for(var n,r=[];null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:he,hasOwnProperty:me,hasOwnProp:me,reduceDescriptors:ge,freezeMethods:function(e){ge(e,function(t,n){if(W(e)&&-1!==[\"arguments\",\"caller\",\"callee\"].indexOf(n))return!1;var r=e[n];W(r)&&(t.enumerable=!1,\"writable\"in t?t.writable=!1:t.set||(t.set=function(){throw Error(\"Can not rewrite read-only method '\"+n+\"'\")}))})},toObjectSet:function(e,t){var n={},r=function(e){e.forEach(function(e){n[e]=!0})};return N(e)?r(e):r(String(e).split(t)),n},toCamelCase:function(e){return e.toLowerCase().replace(/[-_\\s]([a-z\\d])(\\w*)/g,function(e,t,n){return t.toUpperCase()+n})},noop:function(){},toFiniteNumber:function(e,t){return null!=e&&Number.isFinite(e=+e)?e:t},findKey:ae,global:ce,isContextDefined:fe,isSpecCompliantForm:function(e){return!!(e&&W(e.append)&&\"FormData\"===e[P]&&e[j])},toJSONObject:function(e){var t=new Array(10),n=function(e,r){if(B(e)){if(t.indexOf(e)>=0)return;if(V(e))return e;if(!(\"toJSON\"in e)){t[r]=e;var i=N(e)?[]:{};return ie(e,function(e,t){var a=n(e,r+1);!R(a)&&(i[t]=a)}),t[r]=void 0,i}}return e};return n(e,0)},isAsyncFn:_e,isThenable:function(e){return e&&(B(e)||W(e))&&W(e.then)&&W(e.catch)},setImmediate:ye,asap:be,isIterable:function(e){return null!=e&&W(e[j])}},we=function(e){function t(e,n,r,i,a){var o;return u(this,t),(o=l(this,t,[e])).name=\"AxiosError\",o.isAxiosError=!0,n&&(o.code=n),r&&(o.config=r),i&&(o.request=i),a&&(o.response=a,o.status=a.status),o}return h(t,e),c(t,[{key:\"toJSON\",value:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:Me.toJSONObject(this.config),code:this.code,status:this.status}}}],[{key:\"from\",value:function(e,n,r,i,a,o){var s=new t(e.message,n||e.code,r,i,a);return s.cause=e,s.name=e.name,o&&Object.assign(s,o),s}}])}(E(Error));we.ERR_BAD_OPTION_VALUE=\"ERR_BAD_OPTION_VALUE\",we.ERR_BAD_OPTION=\"ERR_BAD_OPTION\",we.ECONNABORTED=\"ECONNABORTED\",we.ETIMEDOUT=\"ETIMEDOUT\",we.ERR_NETWORK=\"ERR_NETWORK\",we.ERR_FR_TOO_MANY_REDIRECTS=\"ERR_FR_TOO_MANY_REDIRECTS\",we.ERR_DEPRECATED=\"ERR_DEPRECATED\",we.ERR_BAD_RESPONSE=\"ERR_BAD_RESPONSE\",we.ERR_BAD_REQUEST=\"ERR_BAD_REQUEST\",we.ERR_CANCELED=\"ERR_CANCELED\",we.ERR_NOT_SUPPORT=\"ERR_NOT_SUPPORT\",we.ERR_INVALID_URL=\"ERR_INVALID_URL\";var ke=we;function Le(e){return Me.isPlainObject(e)||Me.isArray(e)}function xe(e){return Me.endsWith(e,\"[]\")?e.slice(0,-2):e}function Te(e,t,n){return e?e.concat(t).map(function(e,t){return e=xe(e),!n&&t?\"[\"+e+\"]\":e}).join(n?\".\":\"\"):t}var Se=Me.toFlatObject(Me,{},null,function(e){return/^is[A-Z]/.test(e)});function De(e,t,n){if(!Me.isObject(e))throw new TypeError(\"target must be an object\");t=t||new FormData;var r=(n=Me.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(e,t){return!Me.isUndefined(t[e])})).metaTokens,i=n.visitor||u,a=n.dots,o=n.indexes,s=(n.Blob||\"undefined\"!=typeof Blob&&Blob)&&Me.isSpecCompliantForm(t);if(!Me.isFunction(i))throw new TypeError(\"visitor must be a function\");function l(e){if(null===e)return\"\";if(Me.isDate(e))return e.toISOString();if(Me.isBoolean(e))return e.toString();if(!s&&Me.isBlob(e))throw new ke(\"Blob is not supported. Use a Buffer instead.\");return Me.isArrayBuffer(e)||Me.isTypedArray(e)?s&&\"function\"==typeof Blob?new Blob([e]):Buffer.from(e):e}function u(e,n,i){var s=e;if(e&&!i&&\"object\"===x(e))if(Me.endsWith(n,\"{}\"))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(Me.isArray(e)&&function(e){return Me.isArray(e)&&!e.some(Le)}(e)||(Me.isFileList(e)||Me.endsWith(n,\"[]\"))&&(s=Me.toArray(e)))return n=xe(n),s.forEach(function(e,r){!Me.isUndefined(e)&&null!==e&&t.append(!0===o?Te([n],r,a):null===o?n:n+\"[]\",l(e))}),!1;return!!Le(e)||(t.append(Te(i,n,a),l(e)),!1)}var d=[],c=Object.assign(Se,{defaultVisitor:u,convertValue:l,isVisitable:Le});if(!Me.isObject(e))throw new TypeError(\"data must be an object\");return function e(n,r){if(!Me.isUndefined(n)){if(-1!==d.indexOf(n))throw Error(\"Circular reference detected in \"+r.join(\".\"));d.push(n),Me.forEach(n,function(n,a){!0===(!(Me.isUndefined(n)||null===n)&&i.call(t,n,Me.isString(a)?a.trim():a,r,c))&&e(n,r?r.concat(a):[a])}),d.pop()}}(e),t}function Ee(e){var t={\"!\":\"%21\",\"'\":\"%27\",\"(\":\"%28\",\")\":\"%29\",\"~\":\"%7E\",\"%20\":\"+\",\"%00\":\"\\0\"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(e){return t[e]})}function Ye(e,t){this._pairs=[],e&&De(e,this,t)}var Ae=Ye.prototype;function Oe(e){return encodeURIComponent(e).replace(/%3A/gi,\":\").replace(/%24/g,\"$\").replace(/%2C/gi,\",\").replace(/%20/g,\"+\")}function Ce(e,t,n){if(!t)return e;var r,i=n&&n.encode||Oe,a=Me.isFunction(n)?{serialize:n}:n,o=a&&a.serialize;if(r=o?o(t,a):Me.isURLSearchParams(t)?t.toString():new Ye(t,a).toString(i)){var s=e.indexOf(\"#\");-1!==s&&(e=e.slice(0,s)),e+=(-1===e.indexOf(\"?\")?\"?\":\"&\")+r}return e}Ae.append=function(e,t){this._pairs.push([e,t])},Ae.toString=function(e){var t=e?function(t){return e.call(this,t,Ee)}:Ee;return this._pairs.map(function(e){return t(e[0])+\"=\"+t(e[1])},\"\").join(\"&\")};var je=c(function e(){u(this,e),this.handlers=[]},[{key:\"use\",value:function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}},{key:\"eject\",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:\"clear\",value:function(){this.handlers&&(this.handlers=[])}},{key:\"forEach\",value:function(e){Me.forEach(this.handlers,function(t){null!==t&&e(t)})}}]),Pe={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1,legacyInterceptorReqResOrdering:!0},He={isBrowser:!0,classes:{URLSearchParams:\"undefined\"!=typeof URLSearchParams?URLSearchParams:Ye,FormData:\"undefined\"!=typeof FormData?FormData:null,Blob:\"undefined\"!=typeof Blob?Blob:null},protocols:[\"http\",\"https\",\"file\",\"blob\",\"url\",\"data\"]},Ie=\"undefined\"!=typeof window&&\"undefined\"!=typeof document,Fe=\"object\"===(\"undefined\"==typeof navigator?\"undefined\":x(navigator))&&navigator||void 0,Ne=Ie&&(!Fe||[\"ReactNative\",\"NativeScript\",\"NS\"].indexOf(Fe.product)<0),Re=\"undefined\"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&\"function\"==typeof self.importScripts,Ve=Ie&&window.location.href||\"http://localhost\",$e=g(g({},Object.freeze({__proto__:null,hasBrowserEnv:Ie,hasStandardBrowserWebWorkerEnv:Re,hasStandardBrowserEnv:Ne,navigator:Fe,origin:Ve})),He);function ze(e){function t(e,n,r,i){var a=e[i++];if(\"__proto__\"===a)return!0;var o=Number.isFinite(+a),s=i>=e.length;return a=!a&&Me.isArray(r)?r.length:a,s?(Me.hasOwnProp(r,a)?r[a]=[r[a],n]:r[a]=n,!o):(r[a]&&Me.isObject(r[a])||(r[a]=[]),t(e,n,r[a],i)&&Me.isArray(r[a])&&(r[a]=function(e){var t,n,r={},i=Object.keys(e),a=i.length;for(t=0;t<a;t++)r[n=i[t]]=e[n];return r}(r[a])),!o)}if(Me.isFormData(e)&&Me.isFunction(e.entries)){var n={};return Me.forEachEntry(e,function(e,r){t(function(e){return Me.matchAll(/\\w+|\\[(\\w*)]/g,e).map(function(e){return\"[]\"===e[0]?\"\":e[1]||e[0]})}(e),r,n,0)}),n}return null}var We={transitional:Pe,adapter:[\"xhr\",\"http\",\"fetch\"],transformRequest:[function(e,t){var n,r=t.getContentType()||\"\",i=r.indexOf(\"application/json\")>-1,a=Me.isObject(e);if(a&&Me.isHTMLForm(e)&&(e=new FormData(e)),Me.isFormData(e))return i?JSON.stringify(ze(e)):e;if(Me.isArrayBuffer(e)||Me.isBuffer(e)||Me.isStream(e)||Me.isFile(e)||Me.isBlob(e)||Me.isReadableStream(e))return e;if(Me.isArrayBufferView(e))return e.buffer;if(Me.isURLSearchParams(e))return t.setContentType(\"application/x-www-form-urlencoded;charset=utf-8\",!1),e.toString();if(a){if(r.indexOf(\"application/x-www-form-urlencoded\")>-1)return function(e,t){return De(e,new $e.classes.URLSearchParams,g({visitor:function(e,t,n,r){return $e.isNode&&Me.isBuffer(e)?(this.append(t,e.toString(\"base64\")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((n=Me.isFileList(e))||r.indexOf(\"multipart/form-data\")>-1){var o=this.env&&this.env.FormData;return De(n?{\"files[]\":e}:e,o&&new o,this.formSerializer)}}return a||i?(t.setContentType(\"application/json\",!1),function(e){if(Me.isString(e))try{return(0,JSON.parse)(e),Me.trim(e)}catch(e){if(\"SyntaxError\"!==e.name)throw e}return(0,JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||We.transitional,n=t&&t.forcedJSONParsing,r=\"json\"===this.responseType;if(Me.isResponse(e)||Me.isReadableStream(e))return e;if(e&&Me.isString(e)&&(n&&!this.responseType||r)){var i=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e,this.parseReviver)}catch(e){if(i){if(\"SyntaxError\"===e.name)throw ke.from(e,ke.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:\"XSRF-TOKEN\",xsrfHeaderName:\"X-XSRF-TOKEN\",maxContentLength:-1,maxBodyLength:-1,env:{FormData:$e.classes.FormData,Blob:$e.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:\"application/json, text/plain, */*\",\"Content-Type\":void 0}}};Me.forEach([\"delete\",\"get\",\"head\",\"post\",\"put\",\"patch\"],function(e){We.headers[e]={}});var Ue=We,Be=Me.toObjectSet([\"age\",\"authorization\",\"content-length\",\"content-type\",\"etag\",\"expires\",\"from\",\"host\",\"if-modified-since\",\"if-unmodified-since\",\"last-modified\",\"location\",\"max-forwards\",\"proxy-authorization\",\"referer\",\"retry-after\",\"user-agent\"]),qe=Symbol(\"internals\");function Ge(e){return e&&String(e).trim().toLowerCase()}function Je(e){return!1===e||null==e?e:Me.isArray(e)?e.map(Je):String(e)}function Ze(e,t,n,r,i){return Me.isFunction(r)?r.call(this,t,n):(i&&(t=n),Me.isString(t)?Me.isString(r)?-1!==t.indexOf(r):Me.isRegExp(r)?r.test(t):void 0:void 0)}var Ke=c(function e(t){u(this,e),t&&this.set(t)},[{key:\"set\",value:function(e,t,n){var r=this;function i(e,t,n){var i=Ge(t);if(!i)throw new Error(\"header name must be a non-empty string\");var a=Me.findKey(r,i);(!a||void 0===r[a]||!0===n||void 0===n&&!1!==r[a])&&(r[a||t]=Je(e))}var a=function(e,t){return Me.forEach(e,function(e,n){return i(e,n,t)})};if(Me.isPlainObject(e)||e instanceof this.constructor)a(e,t);else if(Me.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim()))a(function(e){var t,n,r,i={};return e&&e.split(\"\\n\").forEach(function(e){r=e.indexOf(\":\"),t=e.substring(0,r).trim().toLowerCase(),n=e.substring(r+1).trim(),!t||i[t]&&Be[t]||(\"set-cookie\"===t?i[t]?i[t].push(n):i[t]=[n]:i[t]=i[t]?i[t]+\", \"+n:n)}),i}(e),t);else if(Me.isObject(e)&&Me.isIterable(e)){var o,s,l,u={},d=function(e){var t=\"undefined\"!=typeof Symbol&&e[Symbol.iterator]||e[\"@@iterator\"];if(!t){if(Array.isArray(e)||(t=T(e))){t&&(e=t);var n=0,r=function(){};return{s:r,n:function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:r}}throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}var i,a=!0,o=!1;return{s:function(){t=t.call(e)},n:function(){var e=t.next();return a=e.done,e},e:function(e){o=!0,i=e},f:function(){try{a||null==t.return||t.return()}finally{if(o)throw i}}}}(e);try{for(d.s();!(l=d.n()).done;){var c=l.value;if(!Me.isArray(c))throw TypeError(\"Object iterator must return a key-value pair\");u[s=c[0]]=(o=u[s])?Me.isArray(o)?[].concat(k(o),[c[1]]):[o,c[1]]:c[1]}}catch(e){d.e(e)}finally{d.f()}a(u,t)}else null!=e&&i(t,e,n);return this}},{key:\"get\",value:function(e,t){if(e=Ge(e)){var n=Me.findKey(this,e);if(n){var r=this[n];if(!t)return r;if(!0===t)return function(e){for(var t,n=Object.create(null),r=/([^\\s,;=]+)\\s*(?:=\\s*([^,;]+))?/g;t=r.exec(e);)n[t[1]]=t[2];return n}(r);if(Me.isFunction(t))return t.call(this,r,n);if(Me.isRegExp(t))return t.exec(r);throw new TypeError(\"parser must be boolean|regexp|function\")}}}},{key:\"has\",value:function(e,t){if(e=Ge(e)){var n=Me.findKey(this,e);return!(!n||void 0===this[n]||t&&!Ze(0,this[n],n,t))}return!1}},{key:\"delete\",value:function(e,t){var n=this,r=!1;function i(e){if(e=Ge(e)){var i=Me.findKey(n,e);!i||t&&!Ze(0,n[i],i,t)||(delete n[i],r=!0)}}return Me.isArray(e)?e.forEach(i):i(e),r}},{key:\"clear\",value:function(e){for(var t=Object.keys(this),n=t.length,r=!1;n--;){var i=t[n];e&&!Ze(0,this[i],i,e,!0)||(delete this[i],r=!0)}return r}},{key:\"normalize\",value:function(e){var t=this,n={};return Me.forEach(this,function(r,i){var a=Me.findKey(n,i);if(a)return t[a]=Je(r),void delete t[i];var o=e?function(e){return e.trim().toLowerCase().replace(/([a-z\\d])(\\w*)/g,function(e,t,n){return t.toUpperCase()+n})}(i):String(i).trim();o!==i&&delete t[i],t[o]=Je(r),n[o]=!0}),this}},{key:\"concat\",value:function(){for(var e,t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return(e=this.constructor).concat.apply(e,[this].concat(n))}},{key:\"toJSON\",value:function(e){var t=Object.create(null);return Me.forEach(this,function(n,r){null!=n&&!1!==n&&(t[r]=e&&Me.isArray(n)?n.join(\", \"):n)}),t}},{key:Symbol.iterator,value:function(){return Object.entries(this.toJSON())[Symbol.iterator]()}},{key:\"toString\",value:function(){return Object.entries(this.toJSON()).map(function(e){var t=w(e,2);return t[0]+\": \"+t[1]}).join(\"\\n\")}},{key:\"getSetCookie\",value:function(){return this.get(\"set-cookie\")||[]}},{key:Symbol.toStringTag,get:function(){return\"AxiosHeaders\"}}],[{key:\"from\",value:function(e){return e instanceof this?e:new this(e)}},{key:\"concat\",value:function(e){for(var t=new this(e),n=arguments.length,r=new Array(n>1?n-1:0),i=1;i<n;i++)r[i-1]=arguments[i];return r.forEach(function(e){return t.set(e)}),t}},{key:\"accessor\",value:function(e){var t=(this[qe]=this[qe]={accessors:{}}).accessors,n=this.prototype;function r(e){var r=Ge(e);t[r]||(function(e,t){var n=Me.toCamelCase(\" \"+t);[\"get\",\"set\",\"has\"].forEach(function(r){Object.defineProperty(e,r+n,{value:function(e,n,i){return this[r].call(this,t,e,n,i)},configurable:!0})})}(n,e),t[r]=!0)}return Me.isArray(e)?e.forEach(r):r(e),this}}]);Ke.accessor([\"Content-Type\",\"Content-Length\",\"Accept\",\"Accept-Encoding\",\"User-Agent\",\"Authorization\"]),Me.reduceDescriptors(Ke.prototype,function(e,t){var n=e.value,r=t[0].toUpperCase()+t.slice(1);return{get:function(){return n},set:function(e){this[r]=e}}}),Me.freezeMethods(Ke);var Xe=Ke;function Qe(e,t){var n=this||Ue,r=t||n,i=Xe.from(r.headers),a=r.data;return Me.forEach(e,function(e){a=e.call(n,a,i.normalize(),t?t.status:void 0)}),i.normalize(),a}function et(e){return!(!e||!e.__CANCEL__)}var tt=function(e){function t(e,n,r){var i;return u(this,t),(i=l(this,t,[null==e?\"canceled\":e,ke.ERR_CANCELED,n,r])).name=\"CanceledError\",i.__CANCEL__=!0,i}return h(t,e),c(t)}(ke);function nt(e,t,n){var r=n.config.validateStatus;n.status&&r&&!r(n.status)?t(new ke(\"Request failed with status code \"+n.status,[ke.ERR_BAD_REQUEST,ke.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n)):e(n)}var rt=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:3,r=0,i=function(e,t){e=e||10;var n,r=new Array(e),i=new Array(e),a=0,o=0;return t=void 0!==t?t:1e3,function(s){var l=Date.now(),u=i[o];n||(n=l),r[a]=s,i[a]=l;for(var d=o,c=0;d!==a;)c+=r[d++],d%=e;if((a=(a+1)%e)===o&&(o=(o+1)%e),!(l-n<t)){var f=u&&l-u;return f?Math.round(1e3*c/f):void 0}}}(50,250);return function(e,t){var n,r,i=0,a=1e3/t,o=function(t){var a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Date.now();i=a,n=null,r&&(clearTimeout(r),r=null),e.apply(void 0,k(t))};return[function(){for(var e=Date.now(),t=e-i,s=arguments.length,l=new Array(s),u=0;u<s;u++)l[u]=arguments[u];t>=a?o(l,e):(n=l,r||(r=setTimeout(function(){r=null,o(n)},a-t)))},function(){return n&&o(n)}]}(function(n){var a=n.loaded,o=n.lengthComputable?n.total:void 0,s=a-r,l=i(s);r=a;var u=f({loaded:a,total:o,progress:o?a/o:void 0,bytes:s,rate:l||void 0,estimated:l&&o&&a<=o?(o-a)/l:void 0,event:n,lengthComputable:null!=o},t?\"download\":\"upload\",!0);e(u)},n)},it=function(e,t){var n=null!=e;return[function(r){return t[0]({lengthComputable:n,total:e,loaded:r})},t[1]]},at=function(e){return function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return Me.asap(function(){return e.apply(void 0,n)})}},ot=$e.hasStandardBrowserEnv?function(e,t){return function(n){return n=new URL(n,$e.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)}}(new URL($e.origin),$e.navigator&&/(msie|trident)/i.test($e.navigator.userAgent)):function(){return!0},st=$e.hasStandardBrowserEnv?{write:function(e,t,n,r,i,a,o){if(\"undefined\"!=typeof document){var s=[\"\".concat(e,\"=\").concat(encodeURIComponent(t))];Me.isNumber(n)&&s.push(\"expires=\".concat(new Date(n).toUTCString())),Me.isString(r)&&s.push(\"path=\".concat(r)),Me.isString(i)&&s.push(\"domain=\".concat(i)),!0===a&&s.push(\"secure\"),Me.isString(o)&&s.push(\"SameSite=\".concat(o)),document.cookie=s.join(\"; \")}},read:function(e){if(\"undefined\"==typeof document)return null;var t=document.cookie.match(new RegExp(\"(?:^|; )\"+e+\"=([^;]*)\"));return t?decodeURIComponent(t[1]):null},remove:function(e){this.write(e,\"\",Date.now()-864e5,\"/\")}}:{write:function(){},read:function(){return null},remove:function(){}};function lt(e,t,n){var r,i=!(\"string\"==typeof(r=t)&&/^([a-z][a-z\\d+\\-.]*:)?\\/\\//i.test(r));return e&&(i||0==n)?function(e,t){return t?e.replace(/\\/?\\/$/,\"\")+\"/\"+t.replace(/^\\/+/,\"\"):e}(e,t):t}var ut=function(e){return e instanceof Xe?g({},e):e};function dt(e,t){t=t||{};var n={};function r(e,t,n,r){return Me.isPlainObject(e)&&Me.isPlainObject(t)?Me.merge.call({caseless:r},e,t):Me.isPlainObject(t)?Me.merge({},t):Me.isArray(t)?t.slice():t}function i(e,t,n,i){return Me.isUndefined(t)?Me.isUndefined(e)?void 0:r(void 0,e,0,i):r(e,t,0,i)}function a(e,t){if(!Me.isUndefined(t))return r(void 0,t)}function o(e,t){return Me.isUndefined(t)?Me.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function s(n,i,a){return a in t?r(n,i):a in e?r(void 0,n):void 0}var l={url:a,method:a,data:a,baseURL:o,transformRequest:o,transformResponse:o,paramsSerializer:o,timeout:o,timeoutMessage:o,withCredentials:o,withXSRFToken:o,adapter:o,responseType:o,xsrfCookieName:o,xsrfHeaderName:o,onUploadProgress:o,onDownloadProgress:o,decompress:o,maxContentLength:o,maxBodyLength:o,beforeRedirect:o,transport:o,httpAgent:o,httpsAgent:o,cancelToken:o,socketPath:o,responseEncoding:o,validateStatus:s,headers:function(e,t,n){return i(ut(e),ut(t),0,!0)}};return Me.forEach(Object.keys(g(g({},e),t)),function(r){if(\"__proto__\"!==r&&\"constructor\"!==r&&\"prototype\"!==r){var a=Me.hasOwnProp(l,r)?l[r]:i,o=a(e[r],t[r],r);Me.isUndefined(o)&&a!==s||(n[r]=o)}}),n}var ct,ft=function(e){var t=dt({},e),n=t.data,r=t.withXSRFToken,i=t.xsrfHeaderName,a=t.xsrfCookieName,o=t.headers,s=t.auth;if(t.headers=o=Xe.from(o),t.url=Ce(lt(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),s&&o.set(\"Authorization\",\"Basic \"+btoa((s.username||\"\")+\":\"+(s.password?unescape(encodeURIComponent(s.password)):\"\"))),Me.isFormData(n))if($e.hasStandardBrowserEnv||$e.hasStandardBrowserWebWorkerEnv)o.setContentType(void 0);else if(Me.isFunction(n.getHeaders)){var l=n.getHeaders(),u=[\"content-type\",\"content-length\"];Object.entries(l).forEach(function(e){var t=w(e,2),n=t[0],r=t[1];u.includes(n.toLowerCase())&&o.set(n,r)})}if($e.hasStandardBrowserEnv&&(r&&Me.isFunction(r)&&(r=r(t)),r||!1!==r&&ot(t.url))){var d=i&&a&&st.read(a);d&&o.set(i,d)}return t},pt=\"undefined\"!=typeof XMLHttpRequest&&function(e){return new Promise(function(t,n){var r,i,a,o,s,l=ft(e),u=l.data,d=Xe.from(l.headers).normalize(),c=l.responseType,f=l.onUploadProgress,p=l.onDownloadProgress;function h(){o&&o(),s&&s(),l.cancelToken&&l.cancelToken.unsubscribe(r),l.signal&&l.signal.removeEventListener(\"abort\",r)}var m=new XMLHttpRequest;function v(){if(m){var r=Xe.from(\"getAllResponseHeaders\"in m&&m.getAllResponseHeaders());nt(function(e){t(e),h()},function(e){n(e),h()},{data:c&&\"text\"!==c&&\"json\"!==c?m.response:m.responseText,status:m.status,statusText:m.statusText,headers:r,config:e,request:m}),m=null}}if(m.open(l.method.toUpperCase(),l.url,!0),m.timeout=l.timeout,\"onloadend\"in m?m.onloadend=v:m.onreadystatechange=function(){m&&4===m.readyState&&(0!==m.status||m.responseURL&&0===m.responseURL.indexOf(\"file:\"))&&setTimeout(v)},m.onabort=function(){m&&(n(new ke(\"Request aborted\",ke.ECONNABORTED,e,m)),m=null)},m.onerror=function(t){var r=t&&t.message?t.message:\"Network Error\",i=new ke(r,ke.ERR_NETWORK,e,m);i.event=t||null,n(i),m=null},m.ontimeout=function(){var t=l.timeout?\"timeout of \"+l.timeout+\"ms exceeded\":\"timeout exceeded\",r=l.transitional||Pe;l.timeoutErrorMessage&&(t=l.timeoutErrorMessage),n(new ke(t,r.clarifyTimeoutError?ke.ETIMEDOUT:ke.ECONNABORTED,e,m)),m=null},void 0===u&&d.setContentType(null),\"setRequestHeader\"in m&&Me.forEach(d.toJSON(),function(e,t){m.setRequestHeader(t,e)}),Me.isUndefined(l.withCredentials)||(m.withCredentials=!!l.withCredentials),c&&\"json\"!==c&&(m.responseType=l.responseType),p){var g=w(rt(p,!0),2);a=g[0],s=g[1],m.addEventListener(\"progress\",a)}if(f&&m.upload){var _=w(rt(f),2);i=_[0],o=_[1],m.upload.addEventListener(\"progress\",i),m.upload.addEventListener(\"loadend\",o)}(l.cancelToken||l.signal)&&(r=function(t){m&&(n(!t||t.type?new tt(null,e,m):t),m.abort(),m=null)},l.cancelToken&&l.cancelToken.subscribe(r),l.signal&&(l.signal.aborted?r():l.signal.addEventListener(\"abort\",r)));var y,b,M=(y=l.url,(b=/^([-+\\w]{1,25})(:?\\/\\/|:)/.exec(y))&&b[1]||\"\");M&&-1===$e.protocols.indexOf(M)?n(new ke(\"Unsupported protocol \"+M+\":\",ke.ERR_BAD_REQUEST,e)):m.send(u||null)})},ht=function(e,t){var n=(e=e?e.filter(Boolean):[]).length;if(t||n){var r,i=new AbortController,a=function(e){if(!r){r=!0,s();var t=e instanceof Error?e:this.reason;i.abort(t instanceof ke?t:new tt(t instanceof Error?t.message:t))}},o=t&&setTimeout(function(){o=null,a(new ke(\"timeout of \".concat(t,\"ms exceeded\"),ke.ETIMEDOUT))},t),s=function(){e&&(o&&clearTimeout(o),o=null,e.forEach(function(e){e.unsubscribe?e.unsubscribe(a):e.removeEventListener(\"abort\",a)}),e=null)};e.forEach(function(e){return e.addEventListener(\"abort\",a)});var l=i.signal;return l.unsubscribe=function(){return Me.asap(s)},l}},mt=_().m(function e(t,n){var r,i,a;return _().w(function(e){for(;;)switch(e.n){case 0:if(r=t.byteLength,n&&!(r<n)){e.n=2;break}return e.n=1,t;case 1:return e.a(2);case 2:i=0;case 3:if(!(i<r)){e.n=5;break}return a=i+n,e.n=4,t.slice(i,a);case 4:i=a,e.n=3;break;case 5:return e.a(2)}},e)}),vt=function(){var e=S(_().m(function e(t,i){var a,o,l,u,d,c,f;return _().w(function(e){for(;;)switch(e.p=e.n){case 0:a=!1,o=!1,e.p=1,u=r(gt(t));case 2:return e.n=3,s(u.next());case 3:if(!(a=!(d=e.v).done)){e.n=5;break}return c=d.value,e.d(b(n(r(mt(c,i)))),4);case 4:a=!1,e.n=2;break;case 5:e.n=7;break;case 6:e.p=6,f=e.v,o=!0,l=f;case 7:if(e.p=7,e.p=8,!a||null==u.return){e.n=9;break}return e.n=9,s(u.return());case 9:if(e.p=9,!o){e.n=10;break}throw l;case 10:return e.f(9);case 11:return e.f(7);case 12:return e.a(2)}},e,null,[[8,,9,11],[1,6,7,12]])}));return function(t,n){return e.apply(this,arguments)}}(),gt=function(){var e=S(_().m(function e(t){var i,a,o,l;return _().w(function(e){for(;;)switch(e.p=e.n){case 0:if(!t[Symbol.asyncIterator]){e.n=2;break}return e.d(b(n(r(t))),1);case 1:return e.a(2);case 2:i=t.getReader(),e.p=3;case 4:return e.n=5,s(i.read());case 5:if(a=e.v,o=a.done,l=a.value,!o){e.n=6;break}return e.a(3,8);case 6:return e.n=7,l;case 7:e.n=4;break;case 8:return e.p=8,e.n=9,s(i.cancel());case 9:return e.f(8);case 10:return e.a(2)}},e,null,[[3,,8,10]])}));return function(t){return e.apply(this,arguments)}}(),_t=function(e,t,n,r){var i,a=vt(e,t),s=0,l=function(e){i||(i=!0,r&&r(e))};return new ReadableStream({pull:function(e){return o(_().m(function t(){var r,i,o,u,d,c;return _().w(function(t){for(;;)switch(t.p=t.n){case 0:return t.p=0,t.n=1,a.next();case 1:if(r=t.v,i=r.done,o=r.value,!i){t.n=2;break}return l(),e.close(),t.a(2);case 2:u=o.byteLength,n&&(d=s+=u,n(d)),e.enqueue(new Uint8Array(o)),t.n=4;break;case 3:throw t.p=3,c=t.v,l(c),c;case 4:return t.a(2)}},t,null,[[0,3]])}))()},cancel:function(e){return l(e),a.return()}},{highWaterMark:2})},yt=Me.isFunction,bt={Request:(ct=Me.global).Request,Response:ct.Response},Mt=Me.global,wt=Mt.ReadableStream,kt=Mt.TextEncoder,Lt=function(e){try{for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r<t;r++)n[r-1]=arguments[r];return!!e.apply(void 0,n)}catch(e){return!1}},xt=function(e){var t=e=Me.merge.call({skipUndefined:!0},bt,e),n=t.fetch,r=t.Request,i=t.Response,a=n?yt(n):\"function\"==typeof fetch,s=yt(r),l=yt(i);if(!a)return!1;var u,d=a&&yt(wt),c=a&&(\"function\"==typeof kt?(u=new kt,function(e){return u.encode(e)}):function(){var e=o(_().m(function e(t){var n,i;return _().w(function(e){for(;;)switch(e.n){case 0:return n=Uint8Array,e.n=1,new r(t).arrayBuffer();case 1:return i=e.v,e.a(2,new n(i))}},e)}));return function(t){return e.apply(this,arguments)}}()),f=s&&d&&Lt(function(){var e=!1,t=new r($e.origin,{body:new wt,method:\"POST\",get duplex(){return e=!0,\"half\"}}).headers.has(\"Content-Type\");return e&&!t}),p=l&&d&&Lt(function(){return Me.isReadableStream(new i(\"\").body)}),h={stream:p&&function(e){return e.body}};a&&[\"text\",\"arrayBuffer\",\"blob\",\"formData\",\"stream\"].forEach(function(e){!h[e]&&(h[e]=function(t,n){var r=t&&t[e];if(r)return r.call(t);throw new ke(\"Response type '\".concat(e,\"' is not supported\"),ke.ERR_NOT_SUPPORT,n)})});var m=function(){var e=o(_().m(function e(t){var n;return _().w(function(e){for(;;)switch(e.n){case 0:if(null!=t){e.n=1;break}return e.a(2,0);case 1:if(!Me.isBlob(t)){e.n=2;break}return e.a(2,t.size);case 2:if(!Me.isSpecCompliantForm(t)){e.n=4;break}return n=new r($e.origin,{method:\"POST\",body:t}),e.n=3,n.arrayBuffer();case 3:case 6:return e.a(2,e.v.byteLength);case 4:if(!Me.isArrayBufferView(t)&&!Me.isArrayBuffer(t)){e.n=5;break}return e.a(2,t.byteLength);case 5:if(Me.isURLSearchParams(t)&&(t+=\"\"),!Me.isString(t)){e.n=7;break}return e.n=6,c(t);case 7:return e.a(2)}},e)}));return function(t){return e.apply(this,arguments)}}(),v=function(){var e=o(_().m(function e(t,n){var r;return _().w(function(e){for(;;)if(0===e.n)return r=Me.toFiniteNumber(t.getContentLength()),e.a(2,null==r?m(n):r)},e)}));return function(t,n){return e.apply(this,arguments)}}();return function(){var e=o(_().m(function e(t){var a,o,l,u,d,c,m,y,b,M,k,L,x,T,S,D,E,Y,A,O,C,j,P,H,I,F,N,R,V,$,z,W,U,B,q,G,J,Z,K;return _().w(function(e){for(;;)switch(e.p=e.n){case 0:if(a=ft(t),o=a.url,l=a.method,u=a.data,d=a.signal,c=a.cancelToken,m=a.timeout,y=a.onDownloadProgress,b=a.onUploadProgress,M=a.responseType,k=a.headers,L=a.withCredentials,x=void 0===L?\"same-origin\":L,T=a.fetchOptions,S=n||fetch,M=M?(M+\"\").toLowerCase():\"text\",D=ht([d,c&&c.toAbortSignal()],m),E=null,Y=D&&D.unsubscribe&&function(){D.unsubscribe()},e.p=1,!(J=b&&f&&\"get\"!==l&&\"head\"!==l)){e.n=3;break}return e.n=2,v(k,u);case 2:Z=A=e.v,J=0!==Z;case 3:if(!J){e.n=4;break}O=new r(o,{method:\"POST\",body:u,duplex:\"half\"}),Me.isFormData(u)&&(C=O.headers.get(\"content-type\"))&&k.setContentType(C),O.body&&(j=it(A,rt(at(b))),P=w(j,2),H=P[0],I=P[1],u=_t(O.body,65536,H,I));case 4:return Me.isString(x)||(x=x?\"include\":\"omit\"),F=s&&\"credentials\"in r.prototype,N=g(g({},T),{},{signal:D,method:l.toUpperCase(),headers:k.normalize().toJSON(),body:u,duplex:\"half\",credentials:F?x:void 0}),E=s&&new r(o,N),e.n=5,s?S(E,T):S(o,N);case 5:return R=e.v,V=p&&(\"stream\"===M||\"response\"===M),p&&(y||V&&Y)&&($={},[\"status\",\"statusText\",\"headers\"].forEach(function(e){$[e]=R[e]}),z=Me.toFiniteNumber(R.headers.get(\"content-length\")),W=y&&it(z,rt(at(y),!0))||[],U=w(W,2),B=U[0],q=U[1],R=new i(_t(R.body,65536,B,function(){q&&q(),Y&&Y()}),$)),M=M||\"text\",e.n=6,h[Me.findKey(h,M)||\"text\"](R,t);case 6:return G=e.v,!V&&Y&&Y(),e.n=7,new Promise(function(e,n){nt(e,n,{data:G,headers:Xe.from(R.headers),status:R.status,statusText:R.statusText,config:t,request:E})});case 7:return e.a(2,e.v);case 8:if(e.p=8,K=e.v,Y&&Y(),!K||\"TypeError\"!==K.name||!/Load failed|fetch/i.test(K.message)){e.n=9;break}throw Object.assign(new ke(\"Network Error\",ke.ERR_NETWORK,t,E,K&&K.response),{cause:K.cause||K});case 9:throw ke.from(K,K&&K.code,t,E,K&&K.response);case 10:return e.a(2)}},e,null,[[1,8]])}));return function(t){return e.apply(this,arguments)}}()},Tt=new Map,St=function(e){for(var t,n,r=e&&e.env||{},i=r.fetch,a=[r.Request,r.Response,i],o=a.length,s=Tt;o--;)t=a[o],void 0===(n=s.get(t))&&s.set(t,n=o?new Map:xt(r)),s=n;return n};St();var Dt={http:null,xhr:pt,fetch:{get:St}};Me.forEach(Dt,function(e,t){if(e){try{Object.defineProperty(e,\"name\",{value:t})}catch(e){}Object.defineProperty(e,\"adapterName\",{value:t})}});var Et=function(e){return\"- \".concat(e)},Yt=function(e){return Me.isFunction(e)||null===e||!1===e},At=function(e,t){for(var n,r,i=(e=Me.isArray(e)?e:[e]).length,a={},o=0;o<i;o++){var s=void 0;if(r=n=e[o],!Yt(n)&&void 0===(r=Dt[(s=String(n)).toLowerCase()]))throw new ke(\"Unknown adapter '\".concat(s,\"'\"));if(r&&(Me.isFunction(r)||(r=r.get(t))))break;a[s||\"#\"+o]=r}if(!r){var l=Object.entries(a).map(function(e){var t=w(e,2),n=t[0],r=t[1];return\"adapter \".concat(n,\" \")+(!1===r?\"is not supported by the environment\":\"is not available in the build\")}),u=i?l.length>1?\"since :\\n\"+l.map(Et).join(\"\\n\"):\" \"+Et(l[0]):\"as no adapter specified\";throw new ke(\"There is no suitable adapter to dispatch the request \"+u,\"ERR_NOT_SUPPORT\")}return r};function Ot(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new tt(null,e)}function Ct(e){return Ot(e),e.headers=Xe.from(e.headers),e.data=Qe.call(e,e.transformRequest),-1!==[\"post\",\"put\",\"patch\"].indexOf(e.method)&&e.headers.setContentType(\"application/x-www-form-urlencoded\",!1),At(e.adapter||Ue.adapter,e)(e).then(function(t){return Ot(e),t.data=Qe.call(e,e.transformResponse,t),t.headers=Xe.from(t.headers),t},function(t){return et(t)||(Ot(e),t&&t.response&&(t.response.data=Qe.call(e,e.transformResponse,t.response),t.response.headers=Xe.from(t.response.headers))),Promise.reject(t)})}var jt=\"1.13.5\",Pt={};[\"object\",\"boolean\",\"number\",\"function\",\"string\",\"symbol\"].forEach(function(e,t){Pt[e]=function(n){return x(n)===e||\"a\"+(t<1?\"n \":\" \")+e}});var Ht={};Pt.transitional=function(e,t,n){function r(e,t){return\"[Axios v\"+jt+\"] Transitional option '\"+e+\"'\"+t+(n?\". \"+n:\"\")}return function(n,i,a){if(!1===e)throw new ke(r(i,\" has been removed\"+(t?\" in \"+t:\"\")),ke.ERR_DEPRECATED);return t&&!Ht[i]&&(Ht[i]=!0,console.warn(r(i,\" has been deprecated since v\"+t+\" and will be removed in the near future\"))),!e||e(n,i,a)}},Pt.spelling=function(e){return function(t,n){return console.warn(\"\".concat(n,\" is likely a misspelling of \").concat(e)),!0}};var It={assertOptions:function(e,t,n){if(\"object\"!==x(e))throw new ke(\"options must be an object\",ke.ERR_BAD_OPTION_VALUE);for(var r=Object.keys(e),i=r.length;i-- >0;){var a=r[i],o=t[a];if(o){var s=e[a],l=void 0===s||o(s,a,e);if(!0!==l)throw new ke(\"option \"+a+\" must be \"+l,ke.ERR_BAD_OPTION_VALUE)}else if(!0!==n)throw new ke(\"Unknown option \"+a,ke.ERR_BAD_OPTION)}},validators:Pt},Ft=It.validators,Nt=function(){return c(function e(t){u(this,e),this.defaults=t||{},this.interceptors={request:new je,response:new je}},[{key:\"request\",value:(e=o(_().m(function e(t,n){var r,i,a;return _().w(function(e){for(;;)switch(e.p=e.n){case 0:return e.p=0,e.n=1,this._request(t,n);case 1:return e.a(2,e.v);case 2:if(e.p=2,(a=e.v)instanceof Error){r={},Error.captureStackTrace?Error.captureStackTrace(r):r=new Error,i=r.stack?r.stack.replace(/^.+\\n/,\"\"):\"\";try{a.stack?i&&!String(a.stack).endsWith(i.replace(/^.+\\n.+\\n/,\"\"))&&(a.stack+=\"\\n\"+i):a.stack=i}catch(e){}}throw a;case 3:return e.a(2)}},e,this,[[0,2]])})),function(t,n){return e.apply(this,arguments)})},{key:\"_request\",value:function(e,t){\"string\"==typeof e?(t=t||{}).url=e:t=e||{};var n=t=dt(this.defaults,t),r=n.transitional,i=n.paramsSerializer,a=n.headers;void 0!==r&&It.assertOptions(r,{silentJSONParsing:Ft.transitional(Ft.boolean),forcedJSONParsing:Ft.transitional(Ft.boolean),clarifyTimeoutError:Ft.transitional(Ft.boolean),legacyInterceptorReqResOrdering:Ft.transitional(Ft.boolean)},!1),null!=i&&(Me.isFunction(i)?t.paramsSerializer={serialize:i}:It.assertOptions(i,{encode:Ft.function,serialize:Ft.function},!0)),void 0!==t.allowAbsoluteUrls||(void 0!==this.defaults.allowAbsoluteUrls?t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:t.allowAbsoluteUrls=!0),It.assertOptions(t,{baseUrl:Ft.spelling(\"baseURL\"),withXsrfToken:Ft.spelling(\"withXSRFToken\")},!0),t.method=(t.method||this.defaults.method||\"get\").toLowerCase();var o=a&&Me.merge(a.common,a[t.method]);a&&Me.forEach([\"delete\",\"get\",\"head\",\"post\",\"put\",\"patch\",\"common\"],function(e){delete a[e]}),t.headers=Xe.concat(o,a);var s=[],l=!0;this.interceptors.request.forEach(function(e){if(\"function\"!=typeof e.runWhen||!1!==e.runWhen(t)){l=l&&e.synchronous;var n=t.transitional||Pe;n&&n.legacyInterceptorReqResOrdering?s.unshift(e.fulfilled,e.rejected):s.push(e.fulfilled,e.rejected)}});var u,d=[];this.interceptors.response.forEach(function(e){d.push(e.fulfilled,e.rejected)});var c,f=0;if(!l){var p=[Ct.bind(this),void 0];for(p.unshift.apply(p,s),p.push.apply(p,d),c=p.length,u=Promise.resolve(t);f<c;)u=u.then(p[f++],p[f++]);return u}c=s.length;for(var h=t;f<c;){var m=s[f++],v=s[f++];try{h=m(h)}catch(e){v.call(this,e);break}}try{u=Ct.call(this,h)}catch(e){return Promise.reject(e)}for(f=0,c=d.length;f<c;)u=u.then(d[f++],d[f++]);return u}},{key:\"getUri\",value:function(e){return Ce(lt((e=dt(this.defaults,e)).baseURL,e.url,e.allowAbsoluteUrls),e.params,e.paramsSerializer)}}]);var e}();Me.forEach([\"delete\",\"get\",\"head\",\"options\"],function(e){Nt.prototype[e]=function(t,n){return this.request(dt(n||{},{method:e,url:t,data:(n||{}).data}))}}),Me.forEach([\"post\",\"put\",\"patch\"],function(e){function t(t){return function(n,r,i){return this.request(dt(i||{},{method:e,headers:t?{\"Content-Type\":\"multipart/form-data\"}:{},url:n,data:r}))}}Nt.prototype[e]=t(),Nt.prototype[e+\"Form\"]=t(!0)});var Rt=Nt,Vt=function(){function e(t){if(u(this,e),\"function\"!=typeof t)throw new TypeError(\"executor must be a function.\");var n;this.promise=new Promise(function(e){n=e});var r=this;this.promise.then(function(e){if(r._listeners){for(var t=r._listeners.length;t-- >0;)r._listeners[t](e);r._listeners=null}}),this.promise.then=function(e){var t,n=new Promise(function(e){r.subscribe(e),t=e}).then(e);return n.cancel=function(){r.unsubscribe(t)},n},t(function(e,t,i){r.reason||(r.reason=new tt(e,t,i),n(r.reason))})}return c(e,[{key:\"throwIfRequested\",value:function(){if(this.reason)throw this.reason}},{key:\"subscribe\",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:\"unsubscribe\",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}},{key:\"toAbortSignal\",value:function(){var e=this,t=new AbortController,n=function(e){t.abort(e)};return this.subscribe(n),t.signal.unsubscribe=function(){return e.unsubscribe(n)},t.signal}}],[{key:\"source\",value:function(){var t;return{token:new e(function(e){t=e}),cancel:t}}}])}(),$t=Vt,zt={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(zt).forEach(function(e){var t=w(e,2),n=t[0],r=t[1];zt[r]=n});var Wt=zt,Ut=function e(t){var n=new Rt(t),r=Y(Rt.prototype.request,n);return Me.extend(r,Rt.prototype,n,{allOwnKeys:!0}),Me.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(dt(t,n))},r}(Ue);return Ut.Axios=Rt,Ut.CanceledError=tt,Ut.CancelToken=$t,Ut.isCancel=et,Ut.VERSION=jt,Ut.toFormData=De,Ut.AxiosError=ke,Ut.Cancel=Ut.CanceledError,Ut.all=function(e){return Promise.all(e)},Ut.spread=function(e){return function(t){return e.apply(null,t)}},Ut.isAxiosError=function(e){return Me.isObject(e)&&!0===e.isAxiosError},Ut.mergeConfig=dt,Ut.AxiosHeaders=Xe,Ut.formToJSON=function(e){return ze(Me.isHTMLForm(e)?new FormData(e):e)},Ut.getAdapter=At,Ut.HttpStatusCode=Wt,Ut.default=Ut,Ut}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(t):e.lozad=t()}(this,function(){\"use strict\";var e=\"undefined\"!=typeof document&&document.documentMode,t={rootMargin:\"0px\",threshold:0,load:function(t){if(\"picture\"===t.nodeName.toLowerCase()){var n=t.querySelector(\"img\"),r=!1;null===n&&(n=document.createElement(\"img\"),r=!0),e&&t.getAttribute(\"data-iesrc\")&&(n.src=t.getAttribute(\"data-iesrc\")),t.getAttribute(\"data-alt\")&&(n.alt=t.getAttribute(\"data-alt\")),r&&t.append(n)}if(\"video\"===t.nodeName.toLowerCase()&&!t.getAttribute(\"data-src\")&&t.children){for(var i=t.children,a=void 0,o=0;o<=i.length-1;o++)(a=i[o].getAttribute(\"data-src\"))&&(i[o].src=a);t.load()}t.getAttribute(\"data-poster\")&&(t.poster=t.getAttribute(\"data-poster\")),t.getAttribute(\"data-src\")&&(t.src=t.getAttribute(\"data-src\")),t.getAttribute(\"data-srcset\")&&t.setAttribute(\"srcset\",t.getAttribute(\"data-srcset\"));var s=\",\";if(t.getAttribute(\"data-background-delimiter\")&&(s=t.getAttribute(\"data-background-delimiter\")),t.getAttribute(\"data-background-image\"))t.style.backgroundImage=\"url('\"+t.getAttribute(\"data-background-image\").split(s).join(\"'),url('\")+\"')\";else if(t.getAttribute(\"data-background-image-set\")){var l=t.getAttribute(\"data-background-image-set\").split(s),u=l[0].substr(0,l[0].indexOf(\" \"))||l[0];u=-1===u.indexOf(\"url(\")?\"url(\"+u+\")\":u,1===l.length?t.style.backgroundImage=u:t.setAttribute(\"style\",(t.getAttribute(\"style\")||\"\")+\"background-image: \"+u+\"; background-image: -webkit-image-set(\"+l+\"); background-image: image-set(\"+l+\")\")}t.getAttribute(\"data-toggle-class\")&&t.classList.toggle(t.getAttribute(\"data-toggle-class\"))},loaded:function(){}};function n(e){e.setAttribute(\"data-loaded\",!0)}var r=function(e){return\"true\"===e.getAttribute(\"data-loaded\")},i=function(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:document;return e instanceof Element?[e]:e instanceof NodeList?e:t.querySelectorAll(e)};return function(){var e,a,o=0<arguments.length&&void 0!==arguments[0]?arguments[0]:\".lozad\",s=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},l=Object.assign({},t,s),u=l.root,d=l.rootMargin,c=l.threshold,f=l.load,p=l.loaded,h=void 0;\"undefined\"!=typeof window&&window.IntersectionObserver&&(h=new IntersectionObserver((e=f,a=p,function(t,i){t.forEach(function(t){(0<t.intersectionRatio||t.isIntersecting)&&(i.unobserve(t.target),r(t.target)||(e(t.target),n(t.target),a(t.target)))})}),{root:u,rootMargin:d,threshold:c}));for(var m,v=i(o,u),g=0;g<v.length;g++)(m=v[g]).getAttribute(\"data-placeholder-background\")&&(m.style.background=m.getAttribute(\"data-placeholder-background\"));return{observe:function(){for(var e=i(o,u),t=0;t<e.length;t++)r(e[t])||(h?h.observe(e[t]):(f(e[t]),n(e[t]),p(e[t])))},triggerLoad:function(e){r(e)||(f(e),n(e),p(e))},observer:h}}}),function(e){\"function\"==typeof define&&define.amd?define([\"jquery\"],e):\"object\"==typeof module&&module.exports?module.exports=function(t,n){return void 0===n&&(n=\"undefined\"!=typeof window?require(\"jquery\"):require(\"jquery\")(t)),e(n),n}:e(jQuery)}(function(e){var t=function(){if(e&&e.fn&&e.fn.select2&&e.fn.select2.amd)var t=e.fn.select2.amd;var n;return function(){\n/**\n * @license almond 0.3.3 Copyright jQuery Foundation and other contributors.\n * Released under MIT license, http://github.com/requirejs/almond/LICENSE\n */\nvar e,n,r;t&&t.requirejs||(t?n=t:t={},function(t){var i,a,o,s,l={},u={},d={},c={},f=Object.prototype.hasOwnProperty,p=[].slice,h=/\\.js$/;function m(e,t){return f.call(e,t)}function v(e,t){var n,r,i,a,o,s,l,u,c,f,p,m=t&&t.split(\"/\"),v=d.map,g=v&&v[\"*\"]||{};if(e){for(o=(e=e.split(\"/\")).length-1,d.nodeIdCompat&&h.test(e[o])&&(e[o]=e[o].replace(h,\"\")),\".\"===e[0].charAt(0)&&m&&(e=m.slice(0,m.length-1).concat(e)),c=0;c<e.length;c++)if(\".\"===(p=e[c]))e.splice(c,1),c-=1;else if(\"..\"===p){if(0===c||1===c&&\"..\"===e[2]||\"..\"===e[c-1])continue;c>0&&(e.splice(c-1,2),c-=2)}e=e.join(\"/\")}if((m||g)&&v){for(c=(n=e.split(\"/\")).length;c>0;c-=1){if(r=n.slice(0,c).join(\"/\"),m)for(f=m.length;f>0;f-=1)if((i=v[m.slice(0,f).join(\"/\")])&&(i=i[r])){a=i,s=c;break}if(a)break;!l&&g&&g[r]&&(l=g[r],u=c)}!a&&l&&(a=l,s=u),a&&(n.splice(0,s,a),e=n.join(\"/\"))}return e}function g(e,n){return function(){var r=p.call(arguments,0);return\"string\"!=typeof r[0]&&1===r.length&&r.push(null),a.apply(t,r.concat([e,n]))}}function _(e){return function(t){l[e]=t}}function y(e){if(m(u,e)){var n=u[e];delete u[e],c[e]=!0,i.apply(t,n)}if(!m(l,e)&&!m(c,e))throw new Error(\"No \"+e);return l[e]}function b(e){var t,n=e?e.indexOf(\"!\"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function M(e){return e?b(e):[]}function w(e){return function(){return d&&d.config&&d.config[e]||{}}}o=function(e,t){var n,r,i=b(e),a=i[0],o=t[1];return e=i[1],a&&(n=y(a=v(a,o))),a?e=n&&n.normalize?n.normalize(e,(r=o,function(e){return v(e,r)})):v(e,o):(a=(i=b(e=v(e,o)))[0],e=i[1],a&&(n=y(a))),{f:a?a+\"!\"+e:e,n:e,pr:a,p:n}},s={require:function(e){return g(e)},exports:function(e){var t=l[e];return void 0!==t?t:l[e]={}},module:function(e){return{id:e,uri:\"\",exports:l[e],config:w(e)}}},i=function(e,n,r,i){var a,d,f,p,h,v,b,w=[],k=typeof r;if(v=M(i=i||e),\"undefined\"===k||\"function\"===k){for(n=!n.length&&r.length?[\"require\",\"exports\",\"module\"]:n,h=0;h<n.length;h+=1)if(\"require\"===(d=(p=o(n[h],v)).f))w[h]=s.require(e);else if(\"exports\"===d)w[h]=s.exports(e),b=!0;else if(\"module\"===d)a=w[h]=s.module(e);else if(m(l,d)||m(u,d)||m(c,d))w[h]=y(d);else{if(!p.p)throw new Error(e+\" missing \"+d);p.p.load(p.n,g(i,!0),_(d),{}),w[h]=l[d]}f=r?r.apply(l[e],w):void 0,e&&(a&&a.exports!==t&&a.exports!==l[e]?l[e]=a.exports:f===t&&b||(l[e]=f))}else e&&(l[e]=r)},e=n=a=function(e,n,r,l,u){if(\"string\"==typeof e)return s[e]?s[e](n):y(o(e,M(n)).f);if(!e.splice){if((d=e).deps&&a(d.deps,d.callback),!n)return;n.splice?(e=n,n=r,r=null):e=t}return n=n||function(){},\"function\"==typeof r&&(r=l,l=u),l?i(t,e,n,r):setTimeout(function(){i(t,e,n,r)},4),a},a.config=function(e){return a(e)},e._defined=l,(r=function(e,t,n){if(\"string\"!=typeof e)throw new Error(\"See almond README: incorrect module build, no module name\");t.splice||(n=t,t=[]),m(l,e)||m(u,e)||(u[e]=[e,t,n])}).amd={jQuery:!0}}(),t.requirejs=e,t.require=n,t.define=r)}(),t.define(\"almond\",function(){}),t.define(\"jquery\",[],function(){var t=e||$;return null==t&&console&&console.error&&console.error(\"Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page.\"),t}),t.define(\"select2/utils\",[\"jquery\"],function(e){var t={};function n(e){var t=e.prototype,n=[];for(var r in t){\"function\"==typeof t[r]&&(\"constructor\"!==r&&n.push(r))}return n}t.Extend=function(e,t){var n={}.hasOwnProperty;function r(){this.constructor=e}for(var i in t)n.call(t,i)&&(e[i]=t[i]);return r.prototype=t.prototype,e.prototype=new r,e.__super__=t.prototype,e},t.Decorate=function(e,t){var r=n(t),i=n(e);function a(){var n=Array.prototype.unshift,r=t.prototype.constructor.length,i=e.prototype.constructor;r>0&&(n.call(arguments,e.prototype.constructor),i=t.prototype.constructor),i.apply(this,arguments)}t.displayName=e.displayName,a.prototype=new function(){this.constructor=a};for(var o=0;o<i.length;o++){var s=i[o];a.prototype[s]=e.prototype[s]}for(var l=function(e){var n=function(){};e in a.prototype&&(n=a.prototype[e]);var r=t.prototype[e];return function(){return Array.prototype.unshift.call(arguments,n),r.apply(this,arguments)}},u=0;u<r.length;u++){var d=r[u];a.prototype[d]=l(d)}return a};var r=function(){this.listeners={}};r.prototype.on=function(e,t){this.listeners=this.listeners||{},e in this.listeners?this.listeners[e].push(t):this.listeners[e]=[t]},r.prototype.trigger=function(e){var t=Array.prototype.slice,n=t.call(arguments,1);this.listeners=this.listeners||{},null==n&&(n=[]),0===n.length&&n.push({}),n[0]._type=e,e in this.listeners&&this.invoke(this.listeners[e],t.call(arguments,1)),\"*\"in this.listeners&&this.invoke(this.listeners[\"*\"],arguments)},r.prototype.invoke=function(e,t){for(var n=0,r=e.length;n<r;n++)e[n].apply(this,t)},t.Observable=r,t.generateChars=function(e){for(var t=\"\",n=0;n<e;n++){t+=Math.floor(36*Math.random()).toString(36)}return t},t.bind=function(e,t){return function(){e.apply(t,arguments)}},t._convertData=function(e){for(var t in e){var n=t.split(\"-\"),r=e;if(1!==n.length){for(var i=0;i<n.length;i++){var a=n[i];(a=a.substring(0,1).toLowerCase()+a.substring(1))in r||(r[a]={}),i==n.length-1&&(r[a]=e[t]),r=r[a]}delete e[t]}}return e},t.hasScroll=function(t,n){var r=e(n),i=n.style.overflowX,a=n.style.overflowY;return(i!==a||\"hidden\"!==a&&\"visible\"!==a)&&(\"scroll\"===i||\"scroll\"===a||(r.innerHeight()<n.scrollHeight||r.innerWidth()<n.scrollWidth))},t.escapeMarkup=function(e){var t={\"\\\\\":\"&#92;\",\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\",\"/\":\"&#47;\"};return\"string\"!=typeof e?e:String(e).replace(/[&<>\"'\\/\\\\]/g,function(e){return t[e]})},t.__cache={};var i=0;return t.GetUniqueElementId=function(e){var n=e.getAttribute(\"data-select2-id\");return null!=n||(n=e.id?\"select2-data-\"+e.id:\"select2-data-\"+(++i).toString()+\"-\"+t.generateChars(4),e.setAttribute(\"data-select2-id\",n)),n},t.StoreData=function(e,n,r){var i=t.GetUniqueElementId(e);t.__cache[i]||(t.__cache[i]={}),t.__cache[i][n]=r},t.GetData=function(n,r){var i=t.GetUniqueElementId(n);return r?t.__cache[i]&&null!=t.__cache[i][r]?t.__cache[i][r]:e(n).data(r):t.__cache[i]},t.RemoveData=function(e){var n=t.GetUniqueElementId(e);null!=t.__cache[n]&&delete t.__cache[n],e.removeAttribute(\"data-select2-id\")},t.copyNonInternalCssClasses=function(e,t){var n=e.getAttribute(\"class\").trim().split(/\\s+/);n=n.filter(function(e){return 0===e.indexOf(\"select2-\")});var r=t.getAttribute(\"class\").trim().split(/\\s+/);r=r.filter(function(e){return 0!==e.indexOf(\"select2-\")});var i=n.concat(r);e.setAttribute(\"class\",i.join(\" \"))},t}),t.define(\"select2/results\",[\"jquery\",\"./utils\"],function(e,t){function n(e,t,r){this.$element=e,this.data=r,this.options=t,n.__super__.constructor.call(this)}return t.Extend(n,t.Observable),n.prototype.render=function(){var t=e('<ul class=\"select2-results__options\" role=\"listbox\"></ul>');return this.options.get(\"multiple\")&&t.attr(\"aria-multiselectable\",\"true\"),this.$results=t,t},n.prototype.clear=function(){this.$results.empty()},n.prototype.displayMessage=function(t){var n=this.options.get(\"escapeMarkup\");this.clear(),this.hideLoading();var r=e('<li role=\"alert\" aria-live=\"assertive\" class=\"select2-results__option\"></li>'),i=this.options.get(\"translations\").get(t.message);r.append(n(i(t.args))),r[0].className+=\" select2-results__message\",this.$results.append(r)},n.prototype.hideMessages=function(){this.$results.find(\".select2-results__message\").remove()},n.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n<e.results.length;n++){var r=e.results[n],i=this.option(r);t.push(i)}this.$results.append(t)}else 0===this.$results.children().length&&this.trigger(\"results:message\",{message:\"noResults\"})},n.prototype.position=function(e,t){t.find(\".select2-results\").append(e)},n.prototype.sort=function(e){return this.options.get(\"sorter\")(e)},n.prototype.highlightFirstItem=function(){var e=this.$results.find(\".select2-results__option--selectable\"),t=e.filter(\".select2-results__option--selected\");t.length>0?t.first().trigger(\"mouseenter\"):e.first().trigger(\"mouseenter\"),this.ensureHighlightVisible()},n.prototype.setClasses=function(){var n=this;this.data.current(function(r){var i=r.map(function(e){return e.id.toString()});n.$results.find(\".select2-results__option--selectable\").each(function(){var n=e(this),r=t.GetData(this,\"data\"),a=\"\"+r.id;null!=r.element&&r.element.selected||null==r.element&&i.indexOf(a)>-1?(this.classList.add(\"select2-results__option--selected\"),n.attr(\"aria-selected\",\"true\")):(this.classList.remove(\"select2-results__option--selected\"),n.attr(\"aria-selected\",\"false\"))})})},n.prototype.showLoading=function(e){this.hideLoading();var t={disabled:!0,loading:!0,text:this.options.get(\"translations\").get(\"searching\")(e)},n=this.option(t);n.className+=\" loading-results\",this.$results.prepend(n)},n.prototype.hideLoading=function(){this.$results.find(\".loading-results\").remove()},n.prototype.option=function(n){var r=document.createElement(\"li\");r.classList.add(\"select2-results__option\"),r.classList.add(\"select2-results__option--selectable\");var i={role:\"option\"},a=window.Element.prototype.matches||window.Element.prototype.msMatchesSelector||window.Element.prototype.webkitMatchesSelector;for(var o in(null!=n.element&&a.call(n.element,\":disabled\")||null==n.element&&n.disabled)&&(i[\"aria-disabled\"]=\"true\",r.classList.remove(\"select2-results__option--selectable\"),r.classList.add(\"select2-results__option--disabled\")),null==n.id&&r.classList.remove(\"select2-results__option--selectable\"),null!=n._resultId&&(r.id=n._resultId),n.title&&(r.title=n.title),n.children&&(i.role=\"group\",i[\"aria-label\"]=n.text,r.classList.remove(\"select2-results__option--selectable\"),r.classList.add(\"select2-results__option--group\")),i){var s=i[o];r.setAttribute(o,s)}if(n.children){var l=e(r),u=document.createElement(\"strong\");u.className=\"select2-results__group\",this.template(n,u);for(var d=[],c=0;c<n.children.length;c++){var f=n.children[c],p=this.option(f);d.push(p)}var h=e(\"<ul></ul>\",{class:\"select2-results__options select2-results__options--nested\",role:\"none\"});h.append(d),l.append(u),l.append(h)}else this.template(n,r);return t.StoreData(r,\"data\",n),r},n.prototype.bind=function(n,r){var i=this,a=n.id+\"-results\";this.$results.attr(\"id\",a),n.on(\"results:all\",function(e){i.clear(),i.append(e.data),n.isOpen()&&(i.setClasses(),i.highlightFirstItem())}),n.on(\"results:append\",function(e){i.append(e.data),n.isOpen()&&i.setClasses()}),n.on(\"query\",function(e){i.hideMessages(),i.showLoading(e)}),n.on(\"select\",function(){n.isOpen()&&(i.setClasses(),i.options.get(\"scrollAfterSelect\")&&i.highlightFirstItem())}),n.on(\"unselect\",function(){n.isOpen()&&(i.setClasses(),i.options.get(\"scrollAfterSelect\")&&i.highlightFirstItem())}),n.on(\"open\",function(){i.$results.attr(\"aria-expanded\",\"true\"),i.$results.attr(\"aria-hidden\",\"false\"),i.setClasses(),i.ensureHighlightVisible()}),n.on(\"close\",function(){i.$results.attr(\"aria-expanded\",\"false\"),i.$results.attr(\"aria-hidden\",\"true\"),i.$results.removeAttr(\"aria-activedescendant\")}),n.on(\"results:toggle\",function(){var e=i.getHighlightedResults();0!==e.length&&e.trigger(\"mouseup\")}),n.on(\"results:select\",function(){var e=i.getHighlightedResults();if(0!==e.length){var n=t.GetData(e[0],\"data\");e.hasClass(\"select2-results__option--selected\")?i.trigger(\"close\",{}):i.trigger(\"select\",{data:n})}}),n.on(\"results:previous\",function(){var e=i.getHighlightedResults(),t=i.$results.find(\".select2-results__option--selectable\"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var a=t.eq(r);a.trigger(\"mouseenter\");var o=i.$results.offset().top,s=a.offset().top,l=i.$results.scrollTop()+(s-o);0===r?i.$results.scrollTop(0):s-o<0&&i.$results.scrollTop(l)}}),n.on(\"results:next\",function(){var e=i.getHighlightedResults(),t=i.$results.find(\".select2-results__option--selectable\"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger(\"mouseenter\");var a=i.$results.offset().top+i.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=i.$results.scrollTop()+o-a;0===n?i.$results.scrollTop(0):o>a&&i.$results.scrollTop(s)}}),n.on(\"results:focus\",function(e){e.element[0].classList.add(\"select2-results__option--highlighted\"),e.element[0].setAttribute(\"aria-selected\",\"true\")}),n.on(\"results:message\",function(e){i.displayMessage(e)}),e.fn.mousewheel&&this.$results.on(\"mousewheel\",function(e){var t=i.$results.scrollTop(),n=i.$results.get(0).scrollHeight-t+e.deltaY,r=e.deltaY>0&&t-e.deltaY<=0,a=e.deltaY<0&&n<=i.$results.height();r?(i.$results.scrollTop(0),e.preventDefault(),e.stopPropagation()):a&&(i.$results.scrollTop(i.$results.get(0).scrollHeight-i.$results.height()),e.preventDefault(),e.stopPropagation())}),this.$results.on(\"mouseup\",\".select2-results__option--selectable\",function(n){var r=e(this),a=t.GetData(this,\"data\");r.hasClass(\"select2-results__option--selected\")?i.options.get(\"multiple\")?i.trigger(\"unselect\",{originalEvent:n,data:a}):i.trigger(\"close\",{}):i.trigger(\"select\",{originalEvent:n,data:a})}),this.$results.on(\"mouseenter\",\".select2-results__option--selectable\",function(n){var r=t.GetData(this,\"data\");i.getHighlightedResults().removeClass(\"select2-results__option--highlighted\").attr(\"aria-selected\",\"false\"),i.trigger(\"results:focus\",{data:r,element:e(this)})})},n.prototype.getHighlightedResults=function(){return this.$results.find(\".select2-results__option--highlighted\")},n.prototype.destroy=function(){this.$results.remove()},n.prototype.ensureHighlightVisible=function(){var e=this.getHighlightedResults();if(0!==e.length){var t=this.$results.find(\".select2-results__option--selectable\").index(e),n=this.$results.offset().top,r=e.offset().top,i=this.$results.scrollTop()+(r-n),a=r-n;i-=2*e.outerHeight(!1),t<=2?this.$results.scrollTop(0):(a>this.$results.outerHeight()||a<0)&&this.$results.scrollTop(i)}},n.prototype.template=function(t,n){var r=this.options.get(\"templateResult\"),i=this.options.get(\"escapeMarkup\"),a=r(t,n);null==a?n.style.display=\"none\":\"string\"==typeof a?n.innerHTML=i(a):e(n).append(a)},n}),t.define(\"select2/keys\",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),t.define(\"select2/selection/base\",[\"jquery\",\"../utils\",\"../keys\"],function(e,t,n){function r(e,t){this.$element=e,this.options=t,r.__super__.constructor.call(this)}return t.Extend(r,t.Observable),r.prototype.render=function(){var n=e('<span class=\"select2-selection\" role=\"combobox\"  aria-haspopup=\"true\" aria-expanded=\"false\"></span>');return this._tabindex=0,null!=t.GetData(this.$element[0],\"old-tabindex\")?this._tabindex=t.GetData(this.$element[0],\"old-tabindex\"):null!=this.$element.attr(\"tabindex\")&&(this._tabindex=this.$element.attr(\"tabindex\")),n.attr(\"title\",this.$element.attr(\"title\")),n.attr(\"tabindex\",this._tabindex),n.attr(\"aria-disabled\",\"false\"),this.$selection=n,n},r.prototype.bind=function(e,t){var r=this,i=e.id+\"-results\";this.container=e,this.$selection.on(\"focus\",function(e){r.trigger(\"focus\",e)}),this.$selection.on(\"blur\",function(e){r._handleBlur(e)}),this.$selection.on(\"keydown\",function(e){r.trigger(\"keypress\",e),e.which===n.SPACE&&e.preventDefault()}),e.on(\"results:focus\",function(e){r.$selection.attr(\"aria-activedescendant\",e.data._resultId)}),e.on(\"selection:update\",function(e){r.update(e.data)}),e.on(\"open\",function(){r.$selection.attr(\"aria-expanded\",\"true\"),r.$selection.attr(\"aria-owns\",i),r._attachCloseHandler(e)}),e.on(\"close\",function(){r.$selection.attr(\"aria-expanded\",\"false\"),r.$selection.removeAttr(\"aria-activedescendant\"),r.$selection.removeAttr(\"aria-owns\"),r.$selection.trigger(\"focus\"),r._detachCloseHandler(e)}),e.on(\"enable\",function(){r.$selection.attr(\"tabindex\",r._tabindex),r.$selection.attr(\"aria-disabled\",\"false\")}),e.on(\"disable\",function(){r.$selection.attr(\"tabindex\",\"-1\"),r.$selection.attr(\"aria-disabled\",\"true\")})},r.prototype._handleBlur=function(t){var n=this;window.setTimeout(function(){document.activeElement==n.$selection[0]||e.contains(n.$selection[0],document.activeElement)||n.trigger(\"blur\",t)},1)},r.prototype._attachCloseHandler=function(n){e(document.body).on(\"mousedown.select2.\"+n.id,function(n){var r=e(n.target).closest(\".select2\");e(\".select2.select2-container--open\").each(function(){this!=r[0]&&t.GetData(this,\"element\").select2(\"close\")})})},r.prototype._detachCloseHandler=function(t){e(document.body).off(\"mousedown.select2.\"+t.id)},r.prototype.position=function(e,t){t.find(\".selection\").append(e)},r.prototype.destroy=function(){this._detachCloseHandler(this.container)},r.prototype.update=function(e){throw new Error(\"The `update` method must be defined in child classes.\")},r.prototype.isEnabled=function(){return!this.isDisabled()},r.prototype.isDisabled=function(){return this.options.get(\"disabled\")},r}),t.define(\"select2/selection/single\",[\"jquery\",\"./base\",\"../utils\",\"../keys\"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e[0].classList.add(\"select2-selection--single\"),e.html('<span class=\"select2-selection__rendered\"></span><span class=\"select2-selection__arrow\" role=\"presentation\"><b role=\"presentation\"></b></span>'),e},i.prototype.bind=function(e,t){var n=this;i.__super__.bind.apply(this,arguments);var r=e.id+\"-container\";this.$selection.find(\".select2-selection__rendered\").attr(\"id\",r).attr(\"role\",\"textbox\").attr(\"aria-readonly\",\"true\"),this.$selection.attr(\"aria-labelledby\",r),this.$selection.attr(\"aria-controls\",r),this.$selection.on(\"mousedown\",function(e){1===e.which&&n.trigger(\"toggle\",{originalEvent:e})}),this.$selection.on(\"focus\",function(e){}),this.$selection.on(\"blur\",function(e){}),e.on(\"focus\",function(t){e.isOpen()||n.$selection.trigger(\"focus\")})},i.prototype.clear=function(){var e=this.$selection.find(\".select2-selection__rendered\");e.empty(),e.removeAttr(\"title\")},i.prototype.display=function(e,t){var n=this.options.get(\"templateSelection\");return this.options.get(\"escapeMarkup\")(n(e,t))},i.prototype.selectionContainer=function(){return e(\"<span></span>\")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(\".select2-selection__rendered\"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr(\"title\",i):n.removeAttr(\"title\")}else this.clear()},i}),t.define(\"select2/selection/multiple\",[\"jquery\",\"./base\",\"../utils\"],function(e,t,n){function r(e,t){r.__super__.constructor.apply(this,arguments)}return n.Extend(r,t),r.prototype.render=function(){var e=r.__super__.render.call(this);return e[0].classList.add(\"select2-selection--multiple\"),e.html('<ul class=\"select2-selection__rendered\"></ul>'),e},r.prototype.bind=function(t,i){var a=this;r.__super__.bind.apply(this,arguments);var o=t.id+\"-container\";this.$selection.find(\".select2-selection__rendered\").attr(\"id\",o),this.$selection.on(\"click\",function(e){a.trigger(\"toggle\",{originalEvent:e})}),this.$selection.on(\"click\",\".select2-selection__choice__remove\",function(t){if(!a.isDisabled()){var r=e(this).parent(),i=n.GetData(r[0],\"data\");a.trigger(\"unselect\",{originalEvent:t,data:i})}}),this.$selection.on(\"keydown\",\".select2-selection__choice__remove\",function(e){a.isDisabled()||e.stopPropagation()})},r.prototype.clear=function(){var e=this.$selection.find(\".select2-selection__rendered\");e.empty(),e.removeAttr(\"title\")},r.prototype.display=function(e,t){var n=this.options.get(\"templateSelection\");return this.options.get(\"escapeMarkup\")(n(e,t))},r.prototype.selectionContainer=function(){return e('<li class=\"select2-selection__choice\"><button type=\"button\" class=\"select2-selection__choice__remove\" tabindex=\"-1\"><span aria-hidden=\"true\">&times;</span></button><span class=\"select2-selection__choice__display\"></span></li>')},r.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],r=this.$selection.find(\".select2-selection__rendered\").attr(\"id\")+\"-choice-\",i=0;i<e.length;i++){var a=e[i],o=this.selectionContainer(),s=this.display(a,o),l=r+n.generateChars(4)+\"-\";a.id?l+=a.id:l+=n.generateChars(4),o.find(\".select2-selection__choice__display\").append(s).attr(\"id\",l);var u=a.title||a.text;u&&o.attr(\"title\",u);var d=this.options.get(\"translations\").get(\"removeItem\"),c=o.find(\".select2-selection__choice__remove\");c.attr(\"title\",d()),c.attr(\"aria-label\",d()),c.attr(\"aria-describedby\",l),n.StoreData(o[0],\"data\",a),t.push(o)}this.$selection.find(\".select2-selection__rendered\").append(t)}},r}),t.define(\"select2/selection/placeholder\",[],function(){function e(e,t,n){this.placeholder=this.normalizePlaceholder(n.get(\"placeholder\")),e.call(this,t,n)}return e.prototype.normalizePlaceholder=function(e,t){return\"string\"==typeof t&&(t={id:\"\",text:t}),t},e.prototype.createPlaceholder=function(e,t){var n=this.selectionContainer();n.html(this.display(t)),n[0].classList.add(\"select2-selection__placeholder\"),n[0].classList.remove(\"select2-selection__choice\");var r=t.title||t.text||n.text();return this.$selection.find(\".select2-selection__rendered\").attr(\"title\",r),n},e.prototype.update=function(e,t){var n=1==t.length&&t[0].id!=this.placeholder.id;if(t.length>1||n)return e.call(this,t);this.clear();var r=this.createPlaceholder(this.placeholder);this.$selection.find(\".select2-selection__rendered\").append(r)},e}),t.define(\"select2/selection/allowClear\",[\"jquery\",\"../keys\",\"../utils\"],function(e,t,n){function r(){}return r.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),null==this.placeholder&&this.options.get(\"debug\")&&window.console&&console.error&&console.error(\"Select2: The `allowClear` option should be used in combination with the `placeholder` option.\"),this.$selection.on(\"mousedown\",\".select2-selection__clear\",function(e){r._handleClear(e)}),t.on(\"keypress\",function(e){r._handleKeyboardClear(e,t)})},r.prototype._handleClear=function(e,t){if(!this.isDisabled()){var r=this.$selection.find(\".select2-selection__clear\");if(0!==r.length){t.stopPropagation();var i=n.GetData(r[0],\"data\"),a=this.$element.val();this.$element.val(this.placeholder.id);var o={data:i};if(this.trigger(\"clear\",o),o.prevented)this.$element.val(a);else{for(var s=0;s<i.length;s++)if(o={data:i[s]},this.trigger(\"unselect\",o),o.prevented)return void this.$element.val(a);this.$element.trigger(\"input\").trigger(\"change\"),this.trigger(\"toggle\",{})}}}},r.prototype._handleKeyboardClear=function(e,n,r){r.isOpen()||n.which!=t.DELETE&&n.which!=t.BACKSPACE||this._handleClear(n)},r.prototype.update=function(t,r){if(t.call(this,r),this.$selection.find(\".select2-selection__clear\").remove(),this.$selection[0].classList.remove(\"select2-selection--clearable\"),!(this.$selection.find(\".select2-selection__placeholder\").length>0||0===r.length)){var i=this.$selection.find(\".select2-selection__rendered\").attr(\"id\"),a=this.options.get(\"translations\").get(\"removeAllItems\"),o=e('<button type=\"button\" class=\"select2-selection__clear\" tabindex=\"-1\"><span aria-hidden=\"true\">&times;</span></button>');o.attr(\"title\",a()),o.attr(\"aria-label\",a()),o.attr(\"aria-describedby\",i),n.StoreData(o[0],\"data\",r),this.$selection.prepend(o),this.$selection[0].classList.add(\"select2-selection--clearable\")}},r}),t.define(\"select2/selection/search\",[\"jquery\",\"../utils\",\"../keys\"],function(e,t,n){function r(e,t,n){e.call(this,t,n)}return r.prototype.render=function(t){var n=this.options.get(\"translations\").get(\"search\"),r=e('<span class=\"select2-search select2-search--inline\"><textarea class=\"select2-search__field\" type=\"search\" tabindex=\"-1\" autocorrect=\"off\" autocapitalize=\"none\" spellcheck=\"false\" role=\"searchbox\" aria-autocomplete=\"list\" ></textarea></span>');this.$searchContainer=r,this.$search=r.find(\"textarea\"),this.$search.prop(\"autocomplete\",this.options.get(\"autocomplete\")),this.$search.attr(\"aria-label\",n());var i=t.call(this);return this._transferTabIndex(),i.append(this.$searchContainer),i},r.prototype.bind=function(e,r,i){var a=this,o=r.id+\"-results\",s=r.id+\"-container\";e.call(this,r,i),a.$search.attr(\"aria-describedby\",s),r.on(\"open\",function(){a.$search.attr(\"aria-controls\",o),a.$search.trigger(\"focus\")}),r.on(\"close\",function(){a.$search.val(\"\"),a.resizeSearch(),a.$search.removeAttr(\"aria-controls\"),a.$search.removeAttr(\"aria-activedescendant\"),a.$search.trigger(\"focus\")}),r.on(\"enable\",function(){a.$search.prop(\"disabled\",!1),a._transferTabIndex()}),r.on(\"disable\",function(){a.$search.prop(\"disabled\",!0)}),r.on(\"focus\",function(e){a.$search.trigger(\"focus\")}),r.on(\"results:focus\",function(e){e.data._resultId?a.$search.attr(\"aria-activedescendant\",e.data._resultId):a.$search.removeAttr(\"aria-activedescendant\")}),this.$selection.on(\"focusin\",\".select2-search--inline\",function(e){a.trigger(\"focus\",e)}),this.$selection.on(\"focusout\",\".select2-search--inline\",function(e){a._handleBlur(e)}),this.$selection.on(\"keydown\",\".select2-search--inline\",function(e){if(e.stopPropagation(),a.trigger(\"keypress\",e),a._keyUpPrevented=e.isDefaultPrevented(),e.which===n.BACKSPACE&&\"\"===a.$search.val()){var r=a.$selection.find(\".select2-selection__choice\").last();if(r.length>0){var i=t.GetData(r[0],\"data\");a.searchRemoveChoice(i),e.preventDefault()}}}),this.$selection.on(\"click\",\".select2-search--inline\",function(e){a.$search.val()&&e.stopPropagation()});var l=document.documentMode,u=l&&l<=11;this.$selection.on(\"input.searchcheck\",\".select2-search--inline\",function(e){u?a.$selection.off(\"input.search input.searchcheck\"):a.$selection.off(\"keyup.search\")}),this.$selection.on(\"keyup.search input.search\",\".select2-search--inline\",function(e){if(u&&\"input\"===e.type)a.$selection.off(\"input.search input.searchcheck\");else{var t=e.which;t!=n.SHIFT&&t!=n.CTRL&&t!=n.ALT&&t!=n.TAB&&a.handleSearch(e)}})},r.prototype._transferTabIndex=function(e){this.$search.attr(\"tabindex\",this.$selection.attr(\"tabindex\")),this.$selection.attr(\"tabindex\",\"-1\")},r.prototype.createPlaceholder=function(e,t){this.$search.attr(\"placeholder\",t.text)},r.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr(\"placeholder\",\"\"),e.call(this,t),this.resizeSearch(),n&&this.$search.trigger(\"focus\")},r.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var e=this.$search.val();this.trigger(\"query\",{term:e})}this._keyUpPrevented=!1},r.prototype.searchRemoveChoice=function(e,t){this.trigger(\"unselect\",{data:t}),this.$search.val(t.text),this.handleSearch()},r.prototype.resizeSearch=function(){this.$search.css(\"width\",\"25px\");var e=\"100%\";\"\"===this.$search.attr(\"placeholder\")&&(e=.75*(this.$search.val().length+1)+\"em\");this.$search.css(\"width\",e)},r}),t.define(\"select2/selection/selectionCss\",[\"../utils\"],function(e){function t(){}return t.prototype.render=function(t){var n=t.call(this),r=this.options.get(\"selectionCssClass\")||\"\";return-1!==r.indexOf(\":all:\")&&(r=r.replace(\":all:\",\"\"),e.copyNonInternalCssClasses(n[0],this.$element[0])),n.addClass(r),n},t}),t.define(\"select2/selection/eventRelay\",[\"jquery\"],function(e){function t(){}return t.prototype.bind=function(t,n,r){var i=this,a=[\"open\",\"opening\",\"close\",\"closing\",\"select\",\"selecting\",\"unselect\",\"unselecting\",\"clear\",\"clearing\"],o=[\"opening\",\"closing\",\"selecting\",\"unselecting\",\"clearing\"];t.call(this,n,r),n.on(\"*\",function(t,n){if(-1!==a.indexOf(t)){n=n||{};var r=e.Event(\"select2:\"+t,{params:n});i.$element.trigger(r),-1!==o.indexOf(t)&&(n.prevented=r.isDefaultPrevented())}})},t}),t.define(\"select2/translation\",[\"jquery\",\"require\"],function(e,t){function n(e){this.dict=e||{}}return n.prototype.all=function(){return this.dict},n.prototype.get=function(e){return this.dict[e]},n.prototype.extend=function(t){this.dict=e.extend({},t.all(),this.dict)},n._cache={},n.loadPath=function(e){if(!(e in n._cache)){var r=t(e);n._cache[e]=r}return new n(n._cache[e])},n}),t.define(\"select2/diacritics\",[],function(){return{\"Ⓐ\":\"A\",\"Ａ\":\"A\",\"À\":\"A\",\"Á\":\"A\",\"Â\":\"A\",\"Ầ\":\"A\",\"Ấ\":\"A\",\"Ẫ\":\"A\",\"Ẩ\":\"A\",\"Ã\":\"A\",\"Ā\":\"A\",\"Ă\":\"A\",\"Ằ\":\"A\",\"Ắ\":\"A\",\"Ẵ\":\"A\",\"Ẳ\":\"A\",\"Ȧ\":\"A\",\"Ǡ\":\"A\",\"Ä\":\"A\",\"Ǟ\":\"A\",\"Ả\":\"A\",\"Å\":\"A\",\"Ǻ\":\"A\",\"Ǎ\":\"A\",\"Ȁ\":\"A\",\"Ȃ\":\"A\",\"Ạ\":\"A\",\"Ậ\":\"A\",\"Ặ\":\"A\",\"Ḁ\":\"A\",\"Ą\":\"A\",\"Ⱥ\":\"A\",\"Ɐ\":\"A\",\"Ꜳ\":\"AA\",\"Æ\":\"AE\",\"Ǽ\":\"AE\",\"Ǣ\":\"AE\",\"Ꜵ\":\"AO\",\"Ꜷ\":\"AU\",\"Ꜹ\":\"AV\",\"Ꜻ\":\"AV\",\"Ꜽ\":\"AY\",\"Ⓑ\":\"B\",\"Ｂ\":\"B\",\"Ḃ\":\"B\",\"Ḅ\":\"B\",\"Ḇ\":\"B\",\"Ƀ\":\"B\",\"Ƃ\":\"B\",\"Ɓ\":\"B\",\"Ⓒ\":\"C\",\"Ｃ\":\"C\",\"Ć\":\"C\",\"Ĉ\":\"C\",\"Ċ\":\"C\",\"Č\":\"C\",\"Ç\":\"C\",\"Ḉ\":\"C\",\"Ƈ\":\"C\",\"Ȼ\":\"C\",\"Ꜿ\":\"C\",\"Ⓓ\":\"D\",\"Ｄ\":\"D\",\"Ḋ\":\"D\",\"Ď\":\"D\",\"Ḍ\":\"D\",\"Ḑ\":\"D\",\"Ḓ\":\"D\",\"Ḏ\":\"D\",\"Đ\":\"D\",\"Ƌ\":\"D\",\"Ɗ\":\"D\",\"Ɖ\":\"D\",\"Ꝺ\":\"D\",\"Ǳ\":\"DZ\",\"Ǆ\":\"DZ\",\"ǲ\":\"Dz\",\"ǅ\":\"Dz\",\"Ⓔ\":\"E\",\"Ｅ\":\"E\",\"È\":\"E\",\"É\":\"E\",\"Ê\":\"E\",\"Ề\":\"E\",\"Ế\":\"E\",\"Ễ\":\"E\",\"Ể\":\"E\",\"Ẽ\":\"E\",\"Ē\":\"E\",\"Ḕ\":\"E\",\"Ḗ\":\"E\",\"Ĕ\":\"E\",\"Ė\":\"E\",\"Ë\":\"E\",\"Ẻ\":\"E\",\"Ě\":\"E\",\"Ȅ\":\"E\",\"Ȇ\":\"E\",\"Ẹ\":\"E\",\"Ệ\":\"E\",\"Ȩ\":\"E\",\"Ḝ\":\"E\",\"Ę\":\"E\",\"Ḙ\":\"E\",\"Ḛ\":\"E\",\"Ɛ\":\"E\",\"Ǝ\":\"E\",\"Ⓕ\":\"F\",\"Ｆ\":\"F\",\"Ḟ\":\"F\",\"Ƒ\":\"F\",\"Ꝼ\":\"F\",\"Ⓖ\":\"G\",\"Ｇ\":\"G\",\"Ǵ\":\"G\",\"Ĝ\":\"G\",\"Ḡ\":\"G\",\"Ğ\":\"G\",\"Ġ\":\"G\",\"Ǧ\":\"G\",\"Ģ\":\"G\",\"Ǥ\":\"G\",\"Ɠ\":\"G\",\"Ꞡ\":\"G\",\"Ᵹ\":\"G\",\"Ꝿ\":\"G\",\"Ⓗ\":\"H\",\"Ｈ\":\"H\",\"Ĥ\":\"H\",\"Ḣ\":\"H\",\"Ḧ\":\"H\",\"Ȟ\":\"H\",\"Ḥ\":\"H\",\"Ḩ\":\"H\",\"Ḫ\":\"H\",\"Ħ\":\"H\",\"Ⱨ\":\"H\",\"Ⱶ\":\"H\",\"Ɥ\":\"H\",\"Ⓘ\":\"I\",\"Ｉ\":\"I\",\"Ì\":\"I\",\"Í\":\"I\",\"Î\":\"I\",\"Ĩ\":\"I\",\"Ī\":\"I\",\"Ĭ\":\"I\",\"İ\":\"I\",\"Ï\":\"I\",\"Ḯ\":\"I\",\"Ỉ\":\"I\",\"Ǐ\":\"I\",\"Ȉ\":\"I\",\"Ȋ\":\"I\",\"Ị\":\"I\",\"Į\":\"I\",\"Ḭ\":\"I\",\"Ɨ\":\"I\",\"Ⓙ\":\"J\",\"Ｊ\":\"J\",\"Ĵ\":\"J\",\"Ɉ\":\"J\",\"Ⓚ\":\"K\",\"Ｋ\":\"K\",\"Ḱ\":\"K\",\"Ǩ\":\"K\",\"Ḳ\":\"K\",\"Ķ\":\"K\",\"Ḵ\":\"K\",\"Ƙ\":\"K\",\"Ⱪ\":\"K\",\"Ꝁ\":\"K\",\"Ꝃ\":\"K\",\"Ꝅ\":\"K\",\"Ꞣ\":\"K\",\"Ⓛ\":\"L\",\"Ｌ\":\"L\",\"Ŀ\":\"L\",\"Ĺ\":\"L\",\"Ľ\":\"L\",\"Ḷ\":\"L\",\"Ḹ\":\"L\",\"Ļ\":\"L\",\"Ḽ\":\"L\",\"Ḻ\":\"L\",\"Ł\":\"L\",\"Ƚ\":\"L\",\"Ɫ\":\"L\",\"Ⱡ\":\"L\",\"Ꝉ\":\"L\",\"Ꝇ\":\"L\",\"Ꞁ\":\"L\",\"Ǉ\":\"LJ\",\"ǈ\":\"Lj\",\"Ⓜ\":\"M\",\"Ｍ\":\"M\",\"Ḿ\":\"M\",\"Ṁ\":\"M\",\"Ṃ\":\"M\",\"Ɱ\":\"M\",\"Ɯ\":\"M\",\"Ⓝ\":\"N\",\"Ｎ\":\"N\",\"Ǹ\":\"N\",\"Ń\":\"N\",\"Ñ\":\"N\",\"Ṅ\":\"N\",\"Ň\":\"N\",\"Ṇ\":\"N\",\"Ņ\":\"N\",\"Ṋ\":\"N\",\"Ṉ\":\"N\",\"Ƞ\":\"N\",\"Ɲ\":\"N\",\"Ꞑ\":\"N\",\"Ꞥ\":\"N\",\"Ǌ\":\"NJ\",\"ǋ\":\"Nj\",\"Ⓞ\":\"O\",\"Ｏ\":\"O\",\"Ò\":\"O\",\"Ó\":\"O\",\"Ô\":\"O\",\"Ồ\":\"O\",\"Ố\":\"O\",\"Ỗ\":\"O\",\"Ổ\":\"O\",\"Õ\":\"O\",\"Ṍ\":\"O\",\"Ȭ\":\"O\",\"Ṏ\":\"O\",\"Ō\":\"O\",\"Ṑ\":\"O\",\"Ṓ\":\"O\",\"Ŏ\":\"O\",\"Ȯ\":\"O\",\"Ȱ\":\"O\",\"Ö\":\"O\",\"Ȫ\":\"O\",\"Ỏ\":\"O\",\"Ő\":\"O\",\"Ǒ\":\"O\",\"Ȍ\":\"O\",\"Ȏ\":\"O\",\"Ơ\":\"O\",\"Ờ\":\"O\",\"Ớ\":\"O\",\"Ỡ\":\"O\",\"Ở\":\"O\",\"Ợ\":\"O\",\"Ọ\":\"O\",\"Ộ\":\"O\",\"Ǫ\":\"O\",\"Ǭ\":\"O\",\"Ø\":\"O\",\"Ǿ\":\"O\",\"Ɔ\":\"O\",\"Ɵ\":\"O\",\"Ꝋ\":\"O\",\"Ꝍ\":\"O\",\"Œ\":\"OE\",\"Ƣ\":\"OI\",\"Ꝏ\":\"OO\",\"Ȣ\":\"OU\",\"Ⓟ\":\"P\",\"Ｐ\":\"P\",\"Ṕ\":\"P\",\"Ṗ\":\"P\",\"Ƥ\":\"P\",\"Ᵽ\":\"P\",\"Ꝑ\":\"P\",\"Ꝓ\":\"P\",\"Ꝕ\":\"P\",\"Ⓠ\":\"Q\",\"Ｑ\":\"Q\",\"Ꝗ\":\"Q\",\"Ꝙ\":\"Q\",\"Ɋ\":\"Q\",\"Ⓡ\":\"R\",\"Ｒ\":\"R\",\"Ŕ\":\"R\",\"Ṙ\":\"R\",\"Ř\":\"R\",\"Ȑ\":\"R\",\"Ȓ\":\"R\",\"Ṛ\":\"R\",\"Ṝ\":\"R\",\"Ŗ\":\"R\",\"Ṟ\":\"R\",\"Ɍ\":\"R\",\"Ɽ\":\"R\",\"Ꝛ\":\"R\",\"Ꞧ\":\"R\",\"Ꞃ\":\"R\",\"Ⓢ\":\"S\",\"Ｓ\":\"S\",\"ẞ\":\"S\",\"Ś\":\"S\",\"Ṥ\":\"S\",\"Ŝ\":\"S\",\"Ṡ\":\"S\",\"Š\":\"S\",\"Ṧ\":\"S\",\"Ṣ\":\"S\",\"Ṩ\":\"S\",\"Ș\":\"S\",\"Ş\":\"S\",\"Ȿ\":\"S\",\"Ꞩ\":\"S\",\"Ꞅ\":\"S\",\"Ⓣ\":\"T\",\"Ｔ\":\"T\",\"Ṫ\":\"T\",\"Ť\":\"T\",\"Ṭ\":\"T\",\"Ț\":\"T\",\"Ţ\":\"T\",\"Ṱ\":\"T\",\"Ṯ\":\"T\",\"Ŧ\":\"T\",\"Ƭ\":\"T\",\"Ʈ\":\"T\",\"Ⱦ\":\"T\",\"Ꞇ\":\"T\",\"Ꜩ\":\"TZ\",\"Ⓤ\":\"U\",\"Ｕ\":\"U\",\"Ù\":\"U\",\"Ú\":\"U\",\"Û\":\"U\",\"Ũ\":\"U\",\"Ṹ\":\"U\",\"Ū\":\"U\",\"Ṻ\":\"U\",\"Ŭ\":\"U\",\"Ü\":\"U\",\"Ǜ\":\"U\",\"Ǘ\":\"U\",\"Ǖ\":\"U\",\"Ǚ\":\"U\",\"Ủ\":\"U\",\"Ů\":\"U\",\"Ű\":\"U\",\"Ǔ\":\"U\",\"Ȕ\":\"U\",\"Ȗ\":\"U\",\"Ư\":\"U\",\"Ừ\":\"U\",\"Ứ\":\"U\",\"Ữ\":\"U\",\"Ử\":\"U\",\"Ự\":\"U\",\"Ụ\":\"U\",\"Ṳ\":\"U\",\"Ų\":\"U\",\"Ṷ\":\"U\",\"Ṵ\":\"U\",\"Ʉ\":\"U\",\"Ⓥ\":\"V\",\"Ｖ\":\"V\",\"Ṽ\":\"V\",\"Ṿ\":\"V\",\"Ʋ\":\"V\",\"Ꝟ\":\"V\",\"Ʌ\":\"V\",\"Ꝡ\":\"VY\",\"Ⓦ\":\"W\",\"Ｗ\":\"W\",\"Ẁ\":\"W\",\"Ẃ\":\"W\",\"Ŵ\":\"W\",\"Ẇ\":\"W\",\"Ẅ\":\"W\",\"Ẉ\":\"W\",\"Ⱳ\":\"W\",\"Ⓧ\":\"X\",\"Ｘ\":\"X\",\"Ẋ\":\"X\",\"Ẍ\":\"X\",\"Ⓨ\":\"Y\",\"Ｙ\":\"Y\",\"Ỳ\":\"Y\",\"Ý\":\"Y\",\"Ŷ\":\"Y\",\"Ỹ\":\"Y\",\"Ȳ\":\"Y\",\"Ẏ\":\"Y\",\"Ÿ\":\"Y\",\"Ỷ\":\"Y\",\"Ỵ\":\"Y\",\"Ƴ\":\"Y\",\"Ɏ\":\"Y\",\"Ỿ\":\"Y\",\"Ⓩ\":\"Z\",\"Ｚ\":\"Z\",\"Ź\":\"Z\",\"Ẑ\":\"Z\",\"Ż\":\"Z\",\"Ž\":\"Z\",\"Ẓ\":\"Z\",\"Ẕ\":\"Z\",\"Ƶ\":\"Z\",\"Ȥ\":\"Z\",\"Ɀ\":\"Z\",\"Ⱬ\":\"Z\",\"Ꝣ\":\"Z\",\"ⓐ\":\"a\",\"ａ\":\"a\",\"ẚ\":\"a\",\"à\":\"a\",\"á\":\"a\",\"â\":\"a\",\"ầ\":\"a\",\"ấ\":\"a\",\"ẫ\":\"a\",\"ẩ\":\"a\",\"ã\":\"a\",\"ā\":\"a\",\"ă\":\"a\",\"ằ\":\"a\",\"ắ\":\"a\",\"ẵ\":\"a\",\"ẳ\":\"a\",\"ȧ\":\"a\",\"ǡ\":\"a\",\"ä\":\"a\",\"ǟ\":\"a\",\"ả\":\"a\",\"å\":\"a\",\"ǻ\":\"a\",\"ǎ\":\"a\",\"ȁ\":\"a\",\"ȃ\":\"a\",\"ạ\":\"a\",\"ậ\":\"a\",\"ặ\":\"a\",\"ḁ\":\"a\",\"ą\":\"a\",\"ⱥ\":\"a\",\"ɐ\":\"a\",\"ꜳ\":\"aa\",\"æ\":\"ae\",\"ǽ\":\"ae\",\"ǣ\":\"ae\",\"ꜵ\":\"ao\",\"ꜷ\":\"au\",\"ꜹ\":\"av\",\"ꜻ\":\"av\",\"ꜽ\":\"ay\",\"ⓑ\":\"b\",\"ｂ\":\"b\",\"ḃ\":\"b\",\"ḅ\":\"b\",\"ḇ\":\"b\",\"ƀ\":\"b\",\"ƃ\":\"b\",\"ɓ\":\"b\",\"ⓒ\":\"c\",\"ｃ\":\"c\",\"ć\":\"c\",\"ĉ\":\"c\",\"ċ\":\"c\",\"č\":\"c\",\"ç\":\"c\",\"ḉ\":\"c\",\"ƈ\":\"c\",\"ȼ\":\"c\",\"ꜿ\":\"c\",\"ↄ\":\"c\",\"ⓓ\":\"d\",\"ｄ\":\"d\",\"ḋ\":\"d\",\"ď\":\"d\",\"ḍ\":\"d\",\"ḑ\":\"d\",\"ḓ\":\"d\",\"ḏ\":\"d\",\"đ\":\"d\",\"ƌ\":\"d\",\"ɖ\":\"d\",\"ɗ\":\"d\",\"ꝺ\":\"d\",\"ǳ\":\"dz\",\"ǆ\":\"dz\",\"ⓔ\":\"e\",\"ｅ\":\"e\",\"è\":\"e\",\"é\":\"e\",\"ê\":\"e\",\"ề\":\"e\",\"ế\":\"e\",\"ễ\":\"e\",\"ể\":\"e\",\"ẽ\":\"e\",\"ē\":\"e\",\"ḕ\":\"e\",\"ḗ\":\"e\",\"ĕ\":\"e\",\"ė\":\"e\",\"ë\":\"e\",\"ẻ\":\"e\",\"ě\":\"e\",\"ȅ\":\"e\",\"ȇ\":\"e\",\"ẹ\":\"e\",\"ệ\":\"e\",\"ȩ\":\"e\",\"ḝ\":\"e\",\"ę\":\"e\",\"ḙ\":\"e\",\"ḛ\":\"e\",\"ɇ\":\"e\",\"ɛ\":\"e\",\"ǝ\":\"e\",\"ⓕ\":\"f\",\"ｆ\":\"f\",\"ḟ\":\"f\",\"ƒ\":\"f\",\"ꝼ\":\"f\",\"ⓖ\":\"g\",\"ｇ\":\"g\",\"ǵ\":\"g\",\"ĝ\":\"g\",\"ḡ\":\"g\",\"ğ\":\"g\",\"ġ\":\"g\",\"ǧ\":\"g\",\"ģ\":\"g\",\"ǥ\":\"g\",\"ɠ\":\"g\",\"ꞡ\":\"g\",\"ᵹ\":\"g\",\"ꝿ\":\"g\",\"ⓗ\":\"h\",\"ｈ\":\"h\",\"ĥ\":\"h\",\"ḣ\":\"h\",\"ḧ\":\"h\",\"ȟ\":\"h\",\"ḥ\":\"h\",\"ḩ\":\"h\",\"ḫ\":\"h\",\"ẖ\":\"h\",\"ħ\":\"h\",\"ⱨ\":\"h\",\"ⱶ\":\"h\",\"ɥ\":\"h\",\"ƕ\":\"hv\",\"ⓘ\":\"i\",\"ｉ\":\"i\",\"ì\":\"i\",\"í\":\"i\",\"î\":\"i\",\"ĩ\":\"i\",\"ī\":\"i\",\"ĭ\":\"i\",\"ï\":\"i\",\"ḯ\":\"i\",\"ỉ\":\"i\",\"ǐ\":\"i\",\"ȉ\":\"i\",\"ȋ\":\"i\",\"ị\":\"i\",\"į\":\"i\",\"ḭ\":\"i\",\"ɨ\":\"i\",\"ı\":\"i\",\"ⓙ\":\"j\",\"ｊ\":\"j\",\"ĵ\":\"j\",\"ǰ\":\"j\",\"ɉ\":\"j\",\"ⓚ\":\"k\",\"ｋ\":\"k\",\"ḱ\":\"k\",\"ǩ\":\"k\",\"ḳ\":\"k\",\"ķ\":\"k\",\"ḵ\":\"k\",\"ƙ\":\"k\",\"ⱪ\":\"k\",\"ꝁ\":\"k\",\"ꝃ\":\"k\",\"ꝅ\":\"k\",\"ꞣ\":\"k\",\"ⓛ\":\"l\",\"ｌ\":\"l\",\"ŀ\":\"l\",\"ĺ\":\"l\",\"ľ\":\"l\",\"ḷ\":\"l\",\"ḹ\":\"l\",\"ļ\":\"l\",\"ḽ\":\"l\",\"ḻ\":\"l\",\"ſ\":\"l\",\"ł\":\"l\",\"ƚ\":\"l\",\"ɫ\":\"l\",\"ⱡ\":\"l\",\"ꝉ\":\"l\",\"ꞁ\":\"l\",\"ꝇ\":\"l\",\"ǉ\":\"lj\",\"ⓜ\":\"m\",\"ｍ\":\"m\",\"ḿ\":\"m\",\"ṁ\":\"m\",\"ṃ\":\"m\",\"ɱ\":\"m\",\"ɯ\":\"m\",\"ⓝ\":\"n\",\"ｎ\":\"n\",\"ǹ\":\"n\",\"ń\":\"n\",\"ñ\":\"n\",\"ṅ\":\"n\",\"ň\":\"n\",\"ṇ\":\"n\",\"ņ\":\"n\",\"ṋ\":\"n\",\"ṉ\":\"n\",\"ƞ\":\"n\",\"ɲ\":\"n\",\"ŉ\":\"n\",\"ꞑ\":\"n\",\"ꞥ\":\"n\",\"ǌ\":\"nj\",\"ⓞ\":\"o\",\"ｏ\":\"o\",\"ò\":\"o\",\"ó\":\"o\",\"ô\":\"o\",\"ồ\":\"o\",\"ố\":\"o\",\"ỗ\":\"o\",\"ổ\":\"o\",\"õ\":\"o\",\"ṍ\":\"o\",\"ȭ\":\"o\",\"ṏ\":\"o\",\"ō\":\"o\",\"ṑ\":\"o\",\"ṓ\":\"o\",\"ŏ\":\"o\",\"ȯ\":\"o\",\"ȱ\":\"o\",\"ö\":\"o\",\"ȫ\":\"o\",\"ỏ\":\"o\",\"ő\":\"o\",\"ǒ\":\"o\",\"ȍ\":\"o\",\"ȏ\":\"o\",\"ơ\":\"o\",\"ờ\":\"o\",\"ớ\":\"o\",\"ỡ\":\"o\",\"ở\":\"o\",\"ợ\":\"o\",\"ọ\":\"o\",\"ộ\":\"o\",\"ǫ\":\"o\",\"ǭ\":\"o\",\"ø\":\"o\",\"ǿ\":\"o\",\"ɔ\":\"o\",\"ꝋ\":\"o\",\"ꝍ\":\"o\",\"ɵ\":\"o\",\"œ\":\"oe\",\"ƣ\":\"oi\",\"ȣ\":\"ou\",\"ꝏ\":\"oo\",\"ⓟ\":\"p\",\"ｐ\":\"p\",\"ṕ\":\"p\",\"ṗ\":\"p\",\"ƥ\":\"p\",\"ᵽ\":\"p\",\"ꝑ\":\"p\",\"ꝓ\":\"p\",\"ꝕ\":\"p\",\"ⓠ\":\"q\",\"ｑ\":\"q\",\"ɋ\":\"q\",\"ꝗ\":\"q\",\"ꝙ\":\"q\",\"ⓡ\":\"r\",\"ｒ\":\"r\",\"ŕ\":\"r\",\"ṙ\":\"r\",\"ř\":\"r\",\"ȑ\":\"r\",\"ȓ\":\"r\",\"ṛ\":\"r\",\"ṝ\":\"r\",\"ŗ\":\"r\",\"ṟ\":\"r\",\"ɍ\":\"r\",\"ɽ\":\"r\",\"ꝛ\":\"r\",\"ꞧ\":\"r\",\"ꞃ\":\"r\",\"ⓢ\":\"s\",\"ｓ\":\"s\",\"ß\":\"s\",\"ś\":\"s\",\"ṥ\":\"s\",\"ŝ\":\"s\",\"ṡ\":\"s\",\"š\":\"s\",\"ṧ\":\"s\",\"ṣ\":\"s\",\"ṩ\":\"s\",\"ș\":\"s\",\"ş\":\"s\",\"ȿ\":\"s\",\"ꞩ\":\"s\",\"ꞅ\":\"s\",\"ẛ\":\"s\",\"ⓣ\":\"t\",\"ｔ\":\"t\",\"ṫ\":\"t\",\"ẗ\":\"t\",\"ť\":\"t\",\"ṭ\":\"t\",\"ț\":\"t\",\"ţ\":\"t\",\"ṱ\":\"t\",\"ṯ\":\"t\",\"ŧ\":\"t\",\"ƭ\":\"t\",\"ʈ\":\"t\",\"ⱦ\":\"t\",\"ꞇ\":\"t\",\"ꜩ\":\"tz\",\"ⓤ\":\"u\",\"ｕ\":\"u\",\"ù\":\"u\",\"ú\":\"u\",\"û\":\"u\",\"ũ\":\"u\",\"ṹ\":\"u\",\"ū\":\"u\",\"ṻ\":\"u\",\"ŭ\":\"u\",\"ü\":\"u\",\"ǜ\":\"u\",\"ǘ\":\"u\",\"ǖ\":\"u\",\"ǚ\":\"u\",\"ủ\":\"u\",\"ů\":\"u\",\"ű\":\"u\",\"ǔ\":\"u\",\"ȕ\":\"u\",\"ȗ\":\"u\",\"ư\":\"u\",\"ừ\":\"u\",\"ứ\":\"u\",\"ữ\":\"u\",\"ử\":\"u\",\"ự\":\"u\",\"ụ\":\"u\",\"ṳ\":\"u\",\"ų\":\"u\",\"ṷ\":\"u\",\"ṵ\":\"u\",\"ʉ\":\"u\",\"ⓥ\":\"v\",\"ｖ\":\"v\",\"ṽ\":\"v\",\"ṿ\":\"v\",\"ʋ\":\"v\",\"ꝟ\":\"v\",\"ʌ\":\"v\",\"ꝡ\":\"vy\",\"ⓦ\":\"w\",\"ｗ\":\"w\",\"ẁ\":\"w\",\"ẃ\":\"w\",\"ŵ\":\"w\",\"ẇ\":\"w\",\"ẅ\":\"w\",\"ẘ\":\"w\",\"ẉ\":\"w\",\"ⱳ\":\"w\",\"ⓧ\":\"x\",\"ｘ\":\"x\",\"ẋ\":\"x\",\"ẍ\":\"x\",\"ⓨ\":\"y\",\"ｙ\":\"y\",\"ỳ\":\"y\",\"ý\":\"y\",\"ŷ\":\"y\",\"ỹ\":\"y\",\"ȳ\":\"y\",\"ẏ\":\"y\",\"ÿ\":\"y\",\"ỷ\":\"y\",\"ẙ\":\"y\",\"ỵ\":\"y\",\"ƴ\":\"y\",\"ɏ\":\"y\",\"ỿ\":\"y\",\"ⓩ\":\"z\",\"ｚ\":\"z\",\"ź\":\"z\",\"ẑ\":\"z\",\"ż\":\"z\",\"ž\":\"z\",\"ẓ\":\"z\",\"ẕ\":\"z\",\"ƶ\":\"z\",\"ȥ\":\"z\",\"ɀ\":\"z\",\"ⱬ\":\"z\",\"ꝣ\":\"z\",\"Ά\":\"Α\",\"Έ\":\"Ε\",\"Ή\":\"Η\",\"Ί\":\"Ι\",\"Ϊ\":\"Ι\",\"Ό\":\"Ο\",\"Ύ\":\"Υ\",\"Ϋ\":\"Υ\",\"Ώ\":\"Ω\",\"ά\":\"α\",\"έ\":\"ε\",\"ή\":\"η\",\"ί\":\"ι\",\"ϊ\":\"ι\",\"ΐ\":\"ι\",\"ό\":\"ο\",\"ύ\":\"υ\",\"ϋ\":\"υ\",\"ΰ\":\"υ\",\"ώ\":\"ω\",\"ς\":\"σ\",\"’\":\"'\"}}),t.define(\"select2/data/base\",[\"../utils\"],function(e){function t(e,n){t.__super__.constructor.call(this)}return e.Extend(t,e.Observable),t.prototype.current=function(e){throw new Error(\"The `current` method must be defined in child classes.\")},t.prototype.query=function(e,t){throw new Error(\"The `query` method must be defined in child classes.\")},t.prototype.bind=function(e,t){},t.prototype.destroy=function(){},t.prototype.generateResultId=function(t,n){var r=t.id+\"-result-\";return r+=e.generateChars(4),null!=n.id?r+=\"-\"+n.id.toString():r+=\"-\"+e.generateChars(4),r},t}),t.define(\"select2/data/select\",[\"./base\",\"../utils\",\"jquery\"],function(e,t,n){function r(e,t){this.$element=e,this.options=t,r.__super__.constructor.call(this)}return t.Extend(r,e),r.prototype.current=function(e){var t=this;e(Array.prototype.map.call(this.$element[0].querySelectorAll(\":checked\"),function(e){return t.item(n(e))}))},r.prototype.select=function(e){var t=this;if(e.selected=!0,null!=e.element&&\"option\"===e.element.tagName.toLowerCase())return e.element.selected=!0,void this.$element.trigger(\"input\").trigger(\"change\");if(this.$element.prop(\"multiple\"))this.current(function(n){var r=[];(e=[e]).push.apply(e,n);for(var i=0;i<e.length;i++){var a=e[i].id;-1===r.indexOf(a)&&r.push(a)}t.$element.val(r),t.$element.trigger(\"input\").trigger(\"change\")});else{var n=e.id;this.$element.val(n),this.$element.trigger(\"input\").trigger(\"change\")}},r.prototype.unselect=function(e){var t=this;if(this.$element.prop(\"multiple\")){if(e.selected=!1,null!=e.element&&\"option\"===e.element.tagName.toLowerCase())return e.element.selected=!1,void this.$element.trigger(\"input\").trigger(\"change\");this.current(function(n){for(var r=[],i=0;i<n.length;i++){var a=n[i].id;a!==e.id&&-1===r.indexOf(a)&&r.push(a)}t.$element.val(r),t.$element.trigger(\"input\").trigger(\"change\")})}},r.prototype.bind=function(e,t){var n=this;this.container=e,e.on(\"select\",function(e){n.select(e.data)}),e.on(\"unselect\",function(e){n.unselect(e.data)})},r.prototype.destroy=function(){this.$element.find(\"*\").each(function(){t.RemoveData(this)})},r.prototype.query=function(e,t){var r=[],i=this;this.$element.children().each(function(){if(\"option\"===this.tagName.toLowerCase()||\"optgroup\"===this.tagName.toLowerCase()){var t=n(this),a=i.item(t),o=i.matches(e,a);null!==o&&r.push(o)}}),t({results:r})},r.prototype.addOptions=function(e){this.$element.append(e)},r.prototype.option=function(e){var r;e.children?(r=document.createElement(\"optgroup\")).label=e.text:void 0!==(r=document.createElement(\"option\")).textContent?r.textContent=e.text:r.innerText=e.text,void 0!==e.id&&(r.value=e.id),e.disabled&&(r.disabled=!0),e.selected&&(r.selected=!0),e.title&&(r.title=e.title);var i=this._normalizeItem(e);return i.element=r,t.StoreData(r,\"data\",i),n(r)},r.prototype.item=function(e){var r={};if(null!=(r=t.GetData(e[0],\"data\")))return r;var i=e[0];if(\"option\"===i.tagName.toLowerCase())r={id:e.val(),text:e.text(),disabled:e.prop(\"disabled\"),selected:e.prop(\"selected\"),title:e.prop(\"title\")};else if(\"optgroup\"===i.tagName.toLowerCase()){r={text:e.prop(\"label\"),children:[],title:e.prop(\"title\")};for(var a=e.children(\"option\"),o=[],s=0;s<a.length;s++){var l=n(a[s]),u=this.item(l);o.push(u)}r.children=o}return(r=this._normalizeItem(r)).element=e[0],t.StoreData(e[0],\"data\",r),r},r.prototype._normalizeItem=function(e){e!==Object(e)&&(e={id:e,text:e});return null!=(e=n.extend({},{text:\"\"},e)).id&&(e.id=e.id.toString()),null!=e.text&&(e.text=e.text.toString()),null==e._resultId&&e.id&&null!=this.container&&(e._resultId=this.generateResultId(this.container,e)),n.extend({},{selected:!1,disabled:!1},e)},r.prototype.matches=function(e,t){return this.options.get(\"matcher\")(e,t)},r}),t.define(\"select2/data/array\",[\"./select\",\"../utils\",\"jquery\"],function(e,t,n){function r(e,t){this._dataToConvert=t.get(\"data\")||[],r.__super__.constructor.call(this,e,t)}return t.Extend(r,e),r.prototype.bind=function(e,t){r.__super__.bind.call(this,e,t),this.addOptions(this.convertToOptions(this._dataToConvert))},r.prototype.select=function(e){var t=this.$element.find(\"option\").filter(function(t,n){return n.value==e.id.toString()});0===t.length&&(t=this.option(e),this.addOptions(t)),r.__super__.select.call(this,e)},r.prototype.convertToOptions=function(e){var t=this,r=this.$element.find(\"option\"),i=r.map(function(){return t.item(n(this)).id}).get(),a=[];function o(e){return function(){return n(this).val()==e.id}}for(var s=0;s<e.length;s++){var l=this._normalizeItem(e[s]);if(i.indexOf(l.id)>=0){var u=r.filter(o(l)),d=this.item(u),c=n.extend(!0,{},l,d),f=this.option(c);u.replaceWith(f)}else{var p=this.option(l);if(l.children){var h=this.convertToOptions(l.children);p.append(h)}a.push(p)}}return a},r}),t.define(\"select2/data/ajax\",[\"./array\",\"../utils\",\"jquery\"],function(e,t,n){function r(e,t){this.ajaxOptions=this._applyDefaults(t.get(\"ajax\")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),r.__super__.constructor.call(this,e,t)}return t.Extend(r,e),r.prototype._applyDefaults=function(e){var t={data:function(e){return n.extend({},e,{q:e.term})},transport:function(e,t,r){var i=n.ajax(e);return i.then(t),i.fail(r),i}};return n.extend({},t,e,!0)},r.prototype.processResults=function(e){return e},r.prototype.query=function(e,t){var r=this;null!=this._request&&(\"function\"==typeof this._request.abort&&this._request.abort(),this._request=null);var i=n.extend({type:\"GET\"},this.ajaxOptions);function a(){var n=i.transport(i,function(n){var i=r.processResults(n,e);r.options.get(\"debug\")&&window.console&&console.error&&(i&&i.results&&Array.isArray(i.results)||console.error(\"Select2: The AJAX results did not return an array in the `results` key of the response.\")),t(i)},function(){(!(\"status\"in n)||0!==n.status&&\"0\"!==n.status)&&r.trigger(\"results:message\",{message:\"errorLoading\"})});r._request=n}\"function\"==typeof i.url&&(i.url=i.url.call(this.$element,e)),\"function\"==typeof i.data&&(i.data=i.data.call(this.$element,e)),this.ajaxOptions.delay&&null!=e.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(a,this.ajaxOptions.delay)):a()},r}),t.define(\"select2/data/tags\",[\"jquery\"],function(e){function t(e,t,n){var r=n.get(\"tags\"),i=n.get(\"createTag\");void 0!==i&&(this.createTag=i);var a=n.get(\"insertTag\");if(void 0!==a&&(this.insertTag=a),e.call(this,t,n),Array.isArray(r))for(var o=0;o<r.length;o++){var s=r[o],l=this._normalizeItem(s),u=this.option(l);this.$element.append(u)}}return t.prototype.query=function(e,t,n){var r=this;this._removeOldTags(),null!=t.term&&null==t.page?e.call(this,t,function e(i,a){for(var o=i.results,s=0;s<o.length;s++){var l=o[s],u=null!=l.children&&!e({results:l.children},!0);if((l.text||\"\").toUpperCase()===(t.term||\"\").toUpperCase()||u)return!a&&(i.data=o,void n(i))}if(a)return!0;var d=r.createTag(t);if(null!=d){var c=r.option(d);c.attr(\"data-select2-tag\",\"true\"),r.addOptions([c]),r.insertTag(o,d)}i.results=o,n(i)}):e.call(this,t,n)},t.prototype.createTag=function(e,t){if(null==t.term)return null;var n=t.term.trim();return\"\"===n?null:{id:n,text:n}},t.prototype.insertTag=function(e,t,n){t.unshift(n)},t.prototype._removeOldTags=function(t){this.$element.find(\"option[data-select2-tag]\").each(function(){this.selected||e(this).remove()})},t}),t.define(\"select2/data/tokenizer\",[\"jquery\"],function(e){function t(e,t,n){var r=n.get(\"tokenizer\");void 0!==r&&(this.tokenizer=r),e.call(this,t,n)}return t.prototype.bind=function(e,t,n){e.call(this,t,n),this.$search=t.dropdown.$search||t.selection.$search||n.find(\".select2-search__field\")},t.prototype.query=function(t,n,r){var i=this;n.term=n.term||\"\";var a=this.tokenizer(n,this.options,function(t){var n=i._normalizeItem(t);if(!i.$element.find(\"option\").filter(function(){return e(this).val()===n.id}).length){var r=i.option(n);r.attr(\"data-select2-tag\",!0),i._removeOldTags(),i.addOptions([r])}!function(e){i.trigger(\"select\",{data:e})}(n)});a.term!==n.term&&(this.$search.length&&(this.$search.val(a.term),this.$search.trigger(\"focus\")),n.term=a.term),t.call(this,n,r)},t.prototype.tokenizer=function(t,n,r,i){for(var a=r.get(\"tokenSeparators\")||[],o=n.term,s=0,l=this.createTag||function(e){return{id:e.term,text:e.term}};s<o.length;){var u=o[s];if(-1!==a.indexOf(u)){var d=o.substr(0,s),c=l(e.extend({},n,{term:d}));null!=c?(i(c),o=o.substr(s+1)||\"\",s=0):s++}else s++}return{term:o}},t}),t.define(\"select2/data/minimumInputLength\",[],function(){function e(e,t,n){this.minimumInputLength=n.get(\"minimumInputLength\"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||\"\",t.term.length<this.minimumInputLength?this.trigger(\"results:message\",{message:\"inputTooShort\",args:{minimum:this.minimumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),t.define(\"select2/data/maximumInputLength\",[],function(){function e(e,t,n){this.maximumInputLength=n.get(\"maximumInputLength\"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||\"\",this.maximumInputLength>0&&t.term.length>this.maximumInputLength?this.trigger(\"results:message\",{message:\"inputTooLong\",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),t.define(\"select2/data/maximumSelectionLength\",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get(\"maximumSelectionLength\"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on(\"select\",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,t){var n=this;this.current(function(e){var r=null!=e?e.length:0;n.maximumSelectionLength>0&&r>=n.maximumSelectionLength?n.trigger(\"results:message\",{message:\"maximumSelected\",args:{maximum:n.maximumSelectionLength}}):t&&t()})},e}),t.define(\"select2/dropdown\",[\"jquery\",\"./utils\"],function(e,t){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return t.Extend(n,t.Observable),n.prototype.render=function(){var t=e('<span class=\"select2-dropdown\"><span class=\"select2-results\"></span></span>');return t.attr(\"dir\",this.options.get(\"dir\")),this.$dropdown=t,t},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),t.define(\"select2/dropdown/search\",[\"jquery\"],function(e){function t(){}return t.prototype.render=function(t){var n=t.call(this),r=this.options.get(\"translations\").get(\"search\"),i=e('<span class=\"select2-search select2-search--dropdown\"><input class=\"select2-search__field\" type=\"search\" tabindex=\"-1\" autocorrect=\"off\" autocapitalize=\"none\" spellcheck=\"false\" role=\"searchbox\" aria-autocomplete=\"list\" /></span>');return this.$searchContainer=i,this.$search=i.find(\"input\"),this.$search.prop(\"autocomplete\",this.options.get(\"autocomplete\")),this.$search.attr(\"aria-label\",r()),n.prepend(i),n},t.prototype.bind=function(t,n,r){var i=this,a=n.id+\"-results\";t.call(this,n,r),this.$search.on(\"keydown\",function(e){i.trigger(\"keypress\",e),i._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on(\"input\",function(t){e(this).off(\"keyup\")}),this.$search.on(\"keyup input\",function(e){i.handleSearch(e)}),n.on(\"open\",function(){i.$search.attr(\"tabindex\",0),i.$search.attr(\"aria-controls\",a),i.$search.trigger(\"focus\"),window.setTimeout(function(){i.$search.trigger(\"focus\")},0)}),n.on(\"close\",function(){i.$search.attr(\"tabindex\",-1),i.$search.removeAttr(\"aria-controls\"),i.$search.removeAttr(\"aria-activedescendant\"),i.$search.val(\"\"),i.$search.trigger(\"blur\")}),n.on(\"focus\",function(){n.isOpen()||i.$search.trigger(\"focus\")}),n.on(\"results:all\",function(e){null!=e.query.term&&\"\"!==e.query.term||(i.showSearch(e)?i.$searchContainer[0].classList.remove(\"select2-search--hide\"):i.$searchContainer[0].classList.add(\"select2-search--hide\"))}),n.on(\"results:focus\",function(e){e.data._resultId?i.$search.attr(\"aria-activedescendant\",e.data._resultId):i.$search.removeAttr(\"aria-activedescendant\")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger(\"query\",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),t.define(\"select2/dropdown/hidePlaceholder\",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get(\"placeholder\")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return\"string\"==typeof t&&(t={id:\"\",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;r>=0;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),t.define(\"select2/dropdown/infiniteScroll\",[\"jquery\"],function(e){function t(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return t.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},t.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on(\"query\",function(e){r.lastParams=e,r.loading=!0}),t.on(\"query:append\",function(e){r.lastParams=e,r.loading=!0}),this.$results.on(\"scroll\",this.loadMoreIfNeeded.bind(this))},t.prototype.loadMoreIfNeeded=function(){var t=e.contains(document.documentElement,this.$loadingMore[0]);!this.loading&&t&&(this.$results.offset().top+this.$results.outerHeight(!1)+50>=this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)&&this.loadMore())},t.prototype.loadMore=function(){this.loading=!0;var t=e.extend({},{page:1},this.lastParams);t.page++,this.trigger(\"query:append\",t)},t.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},t.prototype.createLoadingMore=function(){var t=e('<li class=\"select2-results__option select2-results__option--load-more\"role=\"option\" aria-disabled=\"true\"></li>'),n=this.options.get(\"translations\").get(\"loadingMore\");return t.html(n(this.lastParams)),t},t}),t.define(\"select2/dropdown/attachBody\",[\"jquery\",\"../utils\"],function(e,t){function n(t,n,r){this.$dropdownParent=e(r.get(\"dropdownParent\")||document.body),t.call(this,n,r)}return n.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on(\"open\",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on(\"close\",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on(\"mousedown\",function(e){e.stopPropagation()})},n.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},n.prototype.position=function(e,t,n){t.attr(\"class\",n.attr(\"class\")),t[0].classList.remove(\"select2\"),t[0].classList.add(\"select2-container--open\"),t.css({position:\"absolute\",top:-999999}),this.$container=n},n.prototype.render=function(t){var n=e(\"<span></span>\"),r=t.call(this);return n.append(r),this.$dropdownContainer=n,n},n.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},n.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on(\"results:all\",function(){n._positionDropdown(),n._resizeDropdown()}),t.on(\"results:append\",function(){n._positionDropdown(),n._resizeDropdown()}),t.on(\"results:message\",function(){n._positionDropdown(),n._resizeDropdown()}),t.on(\"select\",function(){n._positionDropdown(),n._resizeDropdown()}),t.on(\"unselect\",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},n.prototype._attachPositioningHandler=function(n,r){var i=this,a=\"scroll.select2.\"+r.id,o=\"resize.select2.\"+r.id,s=\"orientationchange.select2.\"+r.id,l=this.$container.parents().filter(t.hasScroll);l.each(function(){t.StoreData(this,\"select2-scroll-position\",{x:e(this).scrollLeft(),y:e(this).scrollTop()})}),l.on(a,function(n){var r=t.GetData(this,\"select2-scroll-position\");e(this).scrollTop(r.y)}),e(window).on(a+\" \"+o+\" \"+s,function(e){i._positionDropdown(),i._resizeDropdown()})},n.prototype._detachPositioningHandler=function(n,r){var i=\"scroll.select2.\"+r.id,a=\"resize.select2.\"+r.id,o=\"orientationchange.select2.\"+r.id;this.$container.parents().filter(t.hasScroll).off(i),e(window).off(i+\" \"+a+\" \"+o)},n.prototype._positionDropdown=function(){var t=e(window),n=this.$dropdown[0].classList.contains(\"select2-dropdown--above\"),r=this.$dropdown[0].classList.contains(\"select2-dropdown--below\"),i=null,a=this.$container.offset();a.bottom=a.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=a.top,o.bottom=a.top+o.height;var s=this.$dropdown.outerHeight(!1),l=t.scrollTop(),u=t.scrollTop()+t.height(),d=l<a.top-s,c=u>a.bottom+s,f={left:a.left,top:o.bottom},p=this.$dropdownParent;\"static\"===p.css(\"position\")&&(p=p.offsetParent());var h={top:0,left:0};(e.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),f.top-=h.top,f.left-=h.left,n||r||(i=\"below\"),c||!d||n?!d&&c&&n&&(i=\"below\"):i=\"above\",(\"above\"==i||n&&\"below\"!==i)&&(f.top=o.top-h.top-s),null!=i&&(this.$dropdown[0].classList.remove(\"select2-dropdown--below\"),this.$dropdown[0].classList.remove(\"select2-dropdown--above\"),this.$dropdown[0].classList.add(\"select2-dropdown--\"+i),this.$container[0].classList.remove(\"select2-container--below\"),this.$container[0].classList.remove(\"select2-container--above\"),this.$container[0].classList.add(\"select2-container--\"+i)),this.$dropdownContainer.css(f)},n.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+\"px\"};this.options.get(\"dropdownAutoWidth\")&&(e.minWidth=e.width,e.position=\"relative\",e.width=\"auto\"),this.$dropdown.css(e)},n.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},n}),t.define(\"select2/dropdown/minimumResultsForSearch\",[],function(){function e(t){for(var n=0,r=0;r<t.length;r++){var i=t[r];i.children?n+=e(i.children):n++}return n}function t(e,t,n,r){this.minimumResultsForSearch=n.get(\"minimumResultsForSearch\"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return t.prototype.showSearch=function(t,n){return!(e(n.data.results)<this.minimumResultsForSearch)&&t.call(this,n)},t}),t.define(\"select2/dropdown/selectOnClose\",[\"../utils\"],function(e){function t(){}return t.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on(\"close\",function(e){r._handleSelectOnClose(e)})},t.prototype._handleSelectOnClose=function(t,n){if(n&&null!=n.originalSelect2Event){var r=n.originalSelect2Event;if(\"select\"===r._type||\"unselect\"===r._type)return}var i=this.getHighlightedResults();if(!(i.length<1)){var a=e.GetData(i[0],\"data\");null!=a.element&&a.element.selected||null==a.element&&a.selected||this.trigger(\"select\",{data:a})}},t}),t.define(\"select2/dropdown/closeOnSelect\",[],function(){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on(\"select\",function(e){r._selectTriggered(e)}),t.on(\"unselect\",function(e){r._selectTriggered(e)})},e.prototype._selectTriggered=function(e,t){var n=t.originalEvent;n&&(n.ctrlKey||n.metaKey)||this.trigger(\"close\",{originalEvent:n,originalSelect2Event:t})},e}),t.define(\"select2/dropdown/dropdownCss\",[\"../utils\"],function(e){function t(){}return t.prototype.render=function(t){var n=t.call(this),r=this.options.get(\"dropdownCssClass\")||\"\";return-1!==r.indexOf(\":all:\")&&(r=r.replace(\":all:\",\"\"),e.copyNonInternalCssClasses(n[0],this.$element[0])),n.addClass(r),n},t}),t.define(\"select2/dropdown/tagsSearchHighlight\",[\"../utils\"],function(e){function t(){}return t.prototype.highlightFirstItem=function(t){var n=this.$results.find(\".select2-results__option--selectable:not(.select2-results__option--selected)\");if(n.length>0){var r=n.first(),i=e.GetData(r[0],\"data\").element;if(i&&i.getAttribute&&\"true\"===i.getAttribute(\"data-select2-tag\"))return void r.trigger(\"mouseenter\")}t.call(this)},t}),t.define(\"select2/i18n/en\",[],function(){return{errorLoading:function(){return\"The results could not be loaded.\"},inputTooLong:function(e){var t=e.input.length-e.maximum,n=\"Please delete \"+t+\" character\";return 1!=t&&(n+=\"s\"),n},inputTooShort:function(e){return\"Please enter \"+(e.minimum-e.input.length)+\" or more characters\"},loadingMore:function(){return\"Loading more results…\"},maximumSelected:function(e){var t=\"You can only select \"+e.maximum+\" item\";return 1!=e.maximum&&(t+=\"s\"),t},noResults:function(){return\"No results found\"},searching:function(){return\"Searching…\"},removeAllItems:function(){return\"Remove all items\"},removeItem:function(){return\"Remove item\"},search:function(){return\"Search\"}}}),t.define(\"select2/defaults\",[\"jquery\",\"./results\",\"./selection/single\",\"./selection/multiple\",\"./selection/placeholder\",\"./selection/allowClear\",\"./selection/search\",\"./selection/selectionCss\",\"./selection/eventRelay\",\"./utils\",\"./translation\",\"./diacritics\",\"./data/select\",\"./data/array\",\"./data/ajax\",\"./data/tags\",\"./data/tokenizer\",\"./data/minimumInputLength\",\"./data/maximumInputLength\",\"./data/maximumSelectionLength\",\"./dropdown\",\"./dropdown/search\",\"./dropdown/hidePlaceholder\",\"./dropdown/infiniteScroll\",\"./dropdown/attachBody\",\"./dropdown/minimumResultsForSearch\",\"./dropdown/selectOnClose\",\"./dropdown/closeOnSelect\",\"./dropdown/dropdownCss\",\"./dropdown/tagsSearchHighlight\",\"./i18n/en\"],function(e,t,n,r,i,a,o,s,l,u,d,c,f,p,h,m,v,g,_,y,b,M,w,k,L,x,T,S,D,E,Y){function A(){this.reset()}return A.prototype.apply=function(d){if(null==(d=e.extend(!0,{},this.defaults,d)).dataAdapter&&(null!=d.ajax?d.dataAdapter=h:null!=d.data?d.dataAdapter=p:d.dataAdapter=f,d.minimumInputLength>0&&(d.dataAdapter=u.Decorate(d.dataAdapter,g)),d.maximumInputLength>0&&(d.dataAdapter=u.Decorate(d.dataAdapter,_)),d.maximumSelectionLength>0&&(d.dataAdapter=u.Decorate(d.dataAdapter,y)),d.tags&&(d.dataAdapter=u.Decorate(d.dataAdapter,m)),null==d.tokenSeparators&&null==d.tokenizer||(d.dataAdapter=u.Decorate(d.dataAdapter,v))),null==d.resultsAdapter&&(d.resultsAdapter=t,null!=d.ajax&&(d.resultsAdapter=u.Decorate(d.resultsAdapter,k)),null!=d.placeholder&&(d.resultsAdapter=u.Decorate(d.resultsAdapter,w)),d.selectOnClose&&(d.resultsAdapter=u.Decorate(d.resultsAdapter,T)),d.tags&&(d.resultsAdapter=u.Decorate(d.resultsAdapter,E))),null==d.dropdownAdapter){if(d.multiple)d.dropdownAdapter=b;else{var c=u.Decorate(b,M);d.dropdownAdapter=c}0!==d.minimumResultsForSearch&&(d.dropdownAdapter=u.Decorate(d.dropdownAdapter,x)),d.closeOnSelect&&(d.dropdownAdapter=u.Decorate(d.dropdownAdapter,S)),null!=d.dropdownCssClass&&(d.dropdownAdapter=u.Decorate(d.dropdownAdapter,D)),d.dropdownAdapter=u.Decorate(d.dropdownAdapter,L)}null==d.selectionAdapter&&(d.multiple?d.selectionAdapter=r:d.selectionAdapter=n,null!=d.placeholder&&(d.selectionAdapter=u.Decorate(d.selectionAdapter,i)),d.allowClear&&(d.selectionAdapter=u.Decorate(d.selectionAdapter,a)),d.multiple&&(d.selectionAdapter=u.Decorate(d.selectionAdapter,o)),null!=d.selectionCssClass&&(d.selectionAdapter=u.Decorate(d.selectionAdapter,s)),d.selectionAdapter=u.Decorate(d.selectionAdapter,l)),d.language=this._resolveLanguage(d.language),d.language.push(\"en\");for(var Y=[],A=0;A<d.language.length;A++){var O=d.language[A];-1===Y.indexOf(O)&&Y.push(O)}return d.language=Y,d.translations=this._processTranslations(d.language,d.debug),d},A.prototype.reset=function(){function t(e){return e.replace(/[^\\u0000-\\u007E]/g,function(e){return c[e]||e})}this.defaults={amdLanguageBase:\"./i18n/\",autocomplete:\"off\",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:u.escapeMarkup,language:{},matcher:function n(r,i){if(null==r.term||\"\"===r.term.trim())return i;if(i.children&&i.children.length>0){for(var a=e.extend(!0,{},i),o=i.children.length-1;o>=0;o--){null==n(r,i.children[o])&&a.children.splice(o,1)}return a.children.length>0?a:n(r,a)}var s=t(i.text).toUpperCase(),l=t(r.term).toUpperCase();return s.indexOf(l)>-1?i:null},minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,scrollAfterSelect:!1,sorter:function(e){return e},templateResult:function(e){return e.text},templateSelection:function(e){return e.text},theme:\"default\",width:\"resolve\"}},A.prototype.applyFromElement=function(e,t){var n=e.language,r=this.defaults.language,i=t.prop(\"lang\"),a=t.closest(\"[lang]\").prop(\"lang\"),o=Array.prototype.concat.call(this._resolveLanguage(i),this._resolveLanguage(n),this._resolveLanguage(r),this._resolveLanguage(a));return e.language=o,e},A.prototype._resolveLanguage=function(t){if(!t)return[];if(e.isEmptyObject(t))return[];if(e.isPlainObject(t))return[t];var n;n=Array.isArray(t)?t:[t];for(var r=[],i=0;i<n.length;i++)if(r.push(n[i]),\"string\"==typeof n[i]&&n[i].indexOf(\"-\")>0){var a=n[i].split(\"-\")[0];r.push(a)}return r},A.prototype._processTranslations=function(t,n){for(var r=new d,i=0;i<t.length;i++){var a=new d,o=t[i];if(\"string\"==typeof o)try{a=d.loadPath(o)}catch(e){try{o=this.defaults.amdLanguageBase+o,a=d.loadPath(o)}catch(e){n&&window.console&&console.warn&&console.warn('Select2: The language file for \"'+o+'\" could not be automatically loaded. A fallback will be used instead.')}}else a=e.isPlainObject(o)?new d(o):o;r.extend(a)}return r},A.prototype.set=function(t,n){var r={};r[e.camelCase(t)]=n;var i=u._convertData(r);e.extend(!0,this.defaults,i)},new A}),t.define(\"select2/options\",[\"jquery\",\"./defaults\",\"./utils\"],function(e,t,n){function r(e,n){this.options=e,null!=n&&this.fromElement(n),null!=n&&(this.options=t.applyFromElement(this.options,n)),this.options=t.apply(this.options)}return r.prototype.fromElement=function(t){var r=[\"select2\"];null==this.options.multiple&&(this.options.multiple=t.prop(\"multiple\")),null==this.options.disabled&&(this.options.disabled=t.prop(\"disabled\")),null==this.options.autocomplete&&t.prop(\"autocomplete\")&&(this.options.autocomplete=t.prop(\"autocomplete\")),null==this.options.dir&&(t.prop(\"dir\")?this.options.dir=t.prop(\"dir\"):t.closest(\"[dir]\").prop(\"dir\")?this.options.dir=t.closest(\"[dir]\").prop(\"dir\"):this.options.dir=\"ltr\"),t.prop(\"disabled\",this.options.disabled),t.prop(\"multiple\",this.options.multiple),n.GetData(t[0],\"select2Tags\")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags=\"true\"` attributes and will be removed in future versions of Select2.'),n.StoreData(t[0],\"data\",n.GetData(t[0],\"select2Tags\")),n.StoreData(t[0],\"tags\",!0)),n.GetData(t[0],\"ajaxUrl\")&&(this.options.debug&&window.console&&console.warn&&console.warn(\"Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2.\"),t.attr(\"ajax--url\",n.GetData(t[0],\"ajaxUrl\")),n.StoreData(t[0],\"ajax-Url\",n.GetData(t[0],\"ajaxUrl\")));var i={};function a(e,t){return t.toUpperCase()}for(var o=0;o<t[0].attributes.length;o++){var s=t[0].attributes[o].name,l=\"data-\";if(s.substr(0,5)==l){var u=s.substring(5),d=n.GetData(t[0],u);i[u.replace(/-([a-z])/g,a)]=d}}e.fn.jquery&&\"1.\"==e.fn.jquery.substr(0,2)&&t[0].dataset&&(i=e.extend(!0,{},t[0].dataset,i));var c=e.extend(!0,{},n.GetData(t[0]),i);for(var f in c=n._convertData(c))r.indexOf(f)>-1||(e.isPlainObject(this.options[f])?e.extend(this.options[f],c[f]):this.options[f]=c[f]);return this},r.prototype.get=function(e){return this.options[e]},r.prototype.set=function(e,t){this.options[e]=t},r}),t.define(\"select2/core\",[\"jquery\",\"./options\",\"./utils\",\"./keys\"],function(e,t,n,r){var i=function(e,r){null!=n.GetData(e[0],\"select2\")&&n.GetData(e[0],\"select2\").destroy(),this.$element=e,this.id=this._generateId(e),r=r||{},this.options=new t(r,e),i.__super__.constructor.call(this);var a=e.attr(\"tabindex\")||0;n.StoreData(e[0],\"old-tabindex\",a),e.attr(\"tabindex\",\"-1\");var o=this.options.get(\"dataAdapter\");this.dataAdapter=new o(e,this.options);var s=this.render();this._placeContainer(s);var l=this.options.get(\"selectionAdapter\");this.selection=new l(e,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,s);var u=this.options.get(\"dropdownAdapter\");this.dropdown=new u(e,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,s);var d=this.options.get(\"resultsAdapter\");this.results=new d(e,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var c=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(e){c.trigger(\"selection:update\",{data:e})}),e[0].classList.add(\"select2-hidden-accessible\"),e.attr(\"aria-hidden\",\"true\"),this._syncAttributes(),n.StoreData(e[0],\"select2\",this),e.data(\"select2\",this)};return n.Extend(i,n.Observable),i.prototype._generateId=function(e){return\"select2-\"+(null!=e.attr(\"id\")?e.attr(\"id\"):null!=e.attr(\"name\")?e.attr(\"name\")+\"-\"+n.generateChars(2):n.generateChars(4)).replace(/(:|\\.|\\[|\\]|,)/g,\"\")},i.prototype._placeContainer=function(e){e.insertAfter(this.$element);var t=this._resolveWidth(this.$element,this.options.get(\"width\"));null!=t&&e.css(\"width\",t)},i.prototype._resolveWidth=function(e,t){var n=/^width:(([-+]?([0-9]*\\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if(\"resolve\"==t){var r=this._resolveWidth(e,\"style\");return null!=r?r:this._resolveWidth(e,\"element\")}if(\"element\"==t){var i=e.outerWidth(!1);return i<=0?\"auto\":i+\"px\"}if(\"style\"==t){var a=e.attr(\"style\");if(\"string\"!=typeof a)return null;for(var o=a.split(\";\"),s=0,l=o.length;s<l;s+=1){var u=o[s].replace(/\\s/g,\"\").match(n);if(null!==u&&u.length>=1)return u[1]}return null}return\"computedstyle\"==t?window.getComputedStyle(e[0]).width:t},i.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},i.prototype._registerDomEvents=function(){var e=this;this.$element.on(\"change.select2\",function(){e.dataAdapter.current(function(t){e.trigger(\"selection:update\",{data:t})})}),this.$element.on(\"focus.select2\",function(t){e.trigger(\"focus\",t)}),this._syncA=n.bind(this._syncAttributes,this),this._syncS=n.bind(this._syncSubtree,this),this._observer=new window.MutationObserver(function(t){e._syncA(),e._syncS(t)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})},i.prototype._registerDataEvents=function(){var e=this;this.dataAdapter.on(\"*\",function(t,n){e.trigger(t,n)})},i.prototype._registerSelectionEvents=function(){var e=this,t=[\"toggle\",\"focus\"];this.selection.on(\"toggle\",function(){e.toggleDropdown()}),this.selection.on(\"focus\",function(t){e.focus(t)}),this.selection.on(\"*\",function(n,r){-1===t.indexOf(n)&&e.trigger(n,r)})},i.prototype._registerDropdownEvents=function(){var e=this;this.dropdown.on(\"*\",function(t,n){e.trigger(t,n)})},i.prototype._registerResultsEvents=function(){var e=this;this.results.on(\"*\",function(t,n){e.trigger(t,n)})},i.prototype._registerEvents=function(){var e=this;this.on(\"open\",function(){e.$container[0].classList.add(\"select2-container--open\")}),this.on(\"close\",function(){e.$container[0].classList.remove(\"select2-container--open\")}),this.on(\"enable\",function(){e.$container[0].classList.remove(\"select2-container--disabled\")}),this.on(\"disable\",function(){e.$container[0].classList.add(\"select2-container--disabled\")}),this.on(\"blur\",function(){e.$container[0].classList.remove(\"select2-container--focus\")}),this.on(\"query\",function(t){e.isOpen()||e.trigger(\"open\",{}),this.dataAdapter.query(t,function(n){e.trigger(\"results:all\",{data:n,query:t})})}),this.on(\"query:append\",function(t){this.dataAdapter.query(t,function(n){e.trigger(\"results:append\",{data:n,query:t})})}),this.on(\"keypress\",function(t){var n=t.which;e.isOpen()?n===r.ESC||n===r.UP&&t.altKey?(e.close(t),t.preventDefault()):n===r.ENTER||n===r.TAB?(e.trigger(\"results:select\",{}),t.preventDefault()):n===r.SPACE&&t.ctrlKey?(e.trigger(\"results:toggle\",{}),t.preventDefault()):n===r.UP?(e.trigger(\"results:previous\",{}),t.preventDefault()):n===r.DOWN&&(e.trigger(\"results:next\",{}),t.preventDefault()):(n===r.ENTER||n===r.SPACE||n===r.DOWN&&t.altKey)&&(e.open(),t.preventDefault())})},i.prototype._syncAttributes=function(){this.options.set(\"disabled\",this.$element.prop(\"disabled\")),this.isDisabled()?(this.isOpen()&&this.close(),this.trigger(\"disable\",{})):this.trigger(\"enable\",{})},i.prototype._isChangeMutation=function(e){var t=this;if(e.addedNodes&&e.addedNodes.length>0)for(var n=0;n<e.addedNodes.length;n++){if(e.addedNodes[n].selected)return!0}else{if(e.removedNodes&&e.removedNodes.length>0)return!0;if(Array.isArray(e))return e.some(function(e){return t._isChangeMutation(e)})}return!1},i.prototype._syncSubtree=function(e){var t=this._isChangeMutation(e),n=this;t&&this.dataAdapter.current(function(e){n.trigger(\"selection:update\",{data:e})})},i.prototype.trigger=function(e,t){var n=i.__super__.trigger,r={open:\"opening\",close:\"closing\",select:\"selecting\",unselect:\"unselecting\",clear:\"clearing\"};if(void 0===t&&(t={}),e in r){var a=r[e],o={prevented:!1,name:e,args:t};if(n.call(this,a,o),o.prevented)return void(t.prevented=!0)}n.call(this,e,t)},i.prototype.toggleDropdown=function(){this.isDisabled()||(this.isOpen()?this.close():this.open())},i.prototype.open=function(){this.isOpen()||this.isDisabled()||this.trigger(\"query\",{})},i.prototype.close=function(e){this.isOpen()&&this.trigger(\"close\",{originalEvent:e})},i.prototype.isEnabled=function(){return!this.isDisabled()},i.prototype.isDisabled=function(){return this.options.get(\"disabled\")},i.prototype.isOpen=function(){return this.$container[0].classList.contains(\"select2-container--open\")},i.prototype.hasFocus=function(){return this.$container[0].classList.contains(\"select2-container--focus\")},i.prototype.focus=function(e){this.hasFocus()||(this.$container[0].classList.add(\"select2-container--focus\"),this.trigger(\"focus\",{}))},i.prototype.enable=function(e){this.options.get(\"debug\")&&window.console&&console.warn&&console.warn('Select2: The `select2(\"enable\")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop(\"disabled\") instead.'),null!=e&&0!==e.length||(e=[!0]);var t=!e[0];this.$element.prop(\"disabled\",t)},i.prototype.data=function(){this.options.get(\"debug\")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2(\"data\")`. You should consider setting the value instead using `$element.val()`.');var e=[];return this.dataAdapter.current(function(t){e=t}),e},i.prototype.val=function(e){if(this.options.get(\"debug\")&&window.console&&console.warn&&console.warn('Select2: The `select2(\"val\")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==e||0===e.length)return this.$element.val();var t=e[0];Array.isArray(t)&&(t=t.map(function(e){return e.toString()})),this.$element.val(t).trigger(\"input\").trigger(\"change\")},i.prototype.destroy=function(){n.RemoveData(this.$container[0]),this.$container.remove(),this._observer.disconnect(),this._observer=null,this._syncA=null,this._syncS=null,this.$element.off(\".select2\"),this.$element.attr(\"tabindex\",n.GetData(this.$element[0],\"old-tabindex\")),this.$element[0].classList.remove(\"select2-hidden-accessible\"),this.$element.attr(\"aria-hidden\",\"false\"),n.RemoveData(this.$element[0]),this.$element.removeData(\"select2\"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},i.prototype.render=function(){var t=e('<span class=\"select2 select2-container\"><span class=\"selection\"></span><span class=\"dropdown-wrapper\" aria-hidden=\"true\"></span></span>');return t.attr(\"dir\",this.options.get(\"dir\")),this.$container=t,this.$container[0].classList.add(\"select2-container--\"+this.options.get(\"theme\")),n.StoreData(t[0],\"element\",this.$element),t},i}),t.define(\"select2/dropdown/attachContainer\",[],function(){function e(e,t,n){e.call(this,t,n)}return e.prototype.position=function(e,t,n){n.find(\".dropdown-wrapper\").append(t),t[0].classList.add(\"select2-dropdown--below\"),n[0].classList.add(\"select2-container--below\")},e}),t.define(\"select2/dropdown/stopPropagation\",[],function(){function e(){}return e.prototype.bind=function(e,t,n){e.call(this,t,n);this.$dropdown.on([\"blur\",\"change\",\"click\",\"dblclick\",\"focus\",\"focusin\",\"focusout\",\"input\",\"keydown\",\"keyup\",\"keypress\",\"mousedown\",\"mouseenter\",\"mouseleave\",\"mousemove\",\"mouseover\",\"mouseup\",\"search\",\"touchend\",\"touchstart\"].join(\" \"),function(e){e.stopPropagation()})},e}),t.define(\"select2/selection/stopPropagation\",[],function(){function e(){}return e.prototype.bind=function(e,t,n){e.call(this,t,n);this.$selection.on([\"blur\",\"change\",\"click\",\"dblclick\",\"focus\",\"focusin\",\"focusout\",\"input\",\"keydown\",\"keyup\",\"keypress\",\"mousedown\",\"mouseenter\",\"mouseleave\",\"mousemove\",\"mouseover\",\"mouseup\",\"search\",\"touchend\",\"touchstart\"].join(\" \"),function(e){e.stopPropagation()})},e}),\n/*!\n * jQuery Mousewheel 3.1.13\n *\n * Copyright jQuery Foundation and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n */\nn=function(e){var t,n,r=[\"wheel\",\"mousewheel\",\"DOMMouseScroll\",\"MozMousePixelScroll\"],i=\"onwheel\"in document||document.documentMode>=9?[\"wheel\"]:[\"mousewheel\",\"DomMouseScroll\",\"MozMousePixelScroll\"],a=Array.prototype.slice;if(e.event.fixHooks)for(var o=r.length;o;)e.event.fixHooks[r[--o]]=e.event.mouseHooks;var s=e.event.special.mousewheel={version:\"3.1.12\",setup:function(){if(this.addEventListener)for(var t=i.length;t;)this.addEventListener(i[--t],l,!1);else this.onmousewheel=l;e.data(this,\"mousewheel-line-height\",s.getLineHeight(this)),e.data(this,\"mousewheel-page-height\",s.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var t=i.length;t;)this.removeEventListener(i[--t],l,!1);else this.onmousewheel=null;e.removeData(this,\"mousewheel-line-height\"),e.removeData(this,\"mousewheel-page-height\")},getLineHeight:function(t){var n=e(t),r=n[\"offsetParent\"in e.fn?\"offsetParent\":\"parent\"]();return r.length||(r=e(\"body\")),parseInt(r.css(\"fontSize\"),10)||parseInt(n.css(\"fontSize\"),10)||16},getPageHeight:function(t){return e(t).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function l(r){var i,o=r||window.event,l=a.call(arguments,1),c=0,f=0,p=0,h=0,m=0;if((r=e.event.fix(o)).type=\"mousewheel\",\"detail\"in o&&(p=-1*o.detail),\"wheelDelta\"in o&&(p=o.wheelDelta),\"wheelDeltaY\"in o&&(p=o.wheelDeltaY),\"wheelDeltaX\"in o&&(f=-1*o.wheelDeltaX),\"axis\"in o&&o.axis===o.HORIZONTAL_AXIS&&(f=-1*p,p=0),c=0===p?f:p,\"deltaY\"in o&&(c=p=-1*o.deltaY),\"deltaX\"in o&&(f=o.deltaX,0===p&&(c=-1*f)),0!==p||0!==f){if(1===o.deltaMode){var v=e.data(this,\"mousewheel-line-height\");c*=v,p*=v,f*=v}else if(2===o.deltaMode){var g=e.data(this,\"mousewheel-page-height\");c*=g,p*=g,f*=g}if(i=Math.max(Math.abs(p),Math.abs(f)),(!n||i<n)&&(n=i,d(o,i)&&(n/=40)),d(o,i)&&(c/=40,f/=40,p/=40),c=Math[c>=1?\"floor\":\"ceil\"](c/n),f=Math[f>=1?\"floor\":\"ceil\"](f/n),p=Math[p>=1?\"floor\":\"ceil\"](p/n),s.settings.normalizeOffset&&this.getBoundingClientRect){var _=this.getBoundingClientRect();h=r.clientX-_.left,m=r.clientY-_.top}return r.deltaX=f,r.deltaY=p,r.deltaFactor=n,r.offsetX=h,r.offsetY=m,r.deltaMode=0,l.unshift(r,c,f,p),t&&clearTimeout(t),t=setTimeout(u,200),(e.event.dispatch||e.event.handle).apply(this,l)}}function u(){n=null}function d(e,t){return s.settings.adjustOldDeltas&&\"mousewheel\"===e.type&&t%120==0}e.fn.extend({mousewheel:function(e){return e?this.bind(\"mousewheel\",e):this.trigger(\"mousewheel\")},unmousewheel:function(e){return this.unbind(\"mousewheel\",e)}})},\"function\"==typeof t.define&&t.define.amd?t.define(\"jquery-mousewheel\",[\"jquery\"],n):\"object\"==typeof exports?module.exports=n:n(e),t.define(\"jquery.select2\",[\"jquery\",\"jquery-mousewheel\",\"./select2/core\",\"./select2/defaults\",\"./select2/utils\"],function(e,t,n,r,i){if(null==e.fn.select2){var a=[\"open\",\"close\",\"destroy\"];e.fn.select2=function(t){if(\"object\"==typeof(t=t||{}))return this.each(function(){var r=e.extend(!0,{},t);new n(e(this),r)}),this;if(\"string\"==typeof t){var r,o=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=i.GetData(this,\"select2\");null==e&&window.console&&console.error&&console.error(\"The select2('\"+t+\"') method was called on an element that is not using Select2.\"),r=e[t].apply(e,o)}),a.indexOf(t)>-1?this:r}throw new Error(\"Invalid arguments for Select2: \"+t)}}return null==e.fn.select2.defaults&&(e.fn.select2.defaults=r),n}),{define:t.define,require:t.require}}(),n=t.require(\"jquery.select2\");return e.fn.select2.amd=t,n}),$.fn.select2.defaults.set(\"theme\",\"bootstrap5\"),$.fn.select2.defaults.set(\"width\",\"100%\"),$.fn.select2.defaults.set(\"selectionCssClass\",\":all:\"),\n/* flatpickr v4.6.13, @license MIT */\nfunction(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).flatpickr=t()}(this,function(){\"use strict\";\n/*! *****************************************************************************\n    Copyright (c) Microsoft Corporation.\n\n    Permission to use, copy, modify, and/or distribute this software for any\n    purpose with or without fee is hereby granted.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\n    REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n    INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\n    LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\n    OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\n    PERFORMANCE OF THIS SOFTWARE.\n    ***************************************************************************** */var e=function(){return e=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var i in t=arguments[n])Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i]);return e},e.apply(this,arguments)};function t(){for(var e=0,t=0,n=arguments.length;t<n;t++)e+=arguments[t].length;var r=Array(e),i=0;for(t=0;t<n;t++)for(var a=arguments[t],o=0,s=a.length;o<s;o++,i++)r[i]=a[o];return r}var n=[\"onChange\",\"onClose\",\"onDayCreate\",\"onDestroy\",\"onKeyDown\",\"onMonthChange\",\"onOpen\",\"onParseConfig\",\"onReady\",\"onValueUpdate\",\"onYearChange\",\"onPreCalendarPosition\"],r={_disable:[],allowInput:!1,allowInvalidPreload:!1,altFormat:\"F j, Y\",altInput:!1,altInputClass:\"form-control input\",animate:\"object\"==typeof window&&-1===window.navigator.userAgent.indexOf(\"MSIE\"),ariaDateFormat:\"F j, Y\",autoFillDefaultTime:!0,clickOpens:!0,closeOnSelect:!0,conjunction:\", \",dateFormat:\"Y-m-d\",defaultHour:12,defaultMinute:0,defaultSeconds:0,disable:[],disableMobile:!1,enableSeconds:!1,enableTime:!1,errorHandler:function(e){return\"undefined\"!=typeof console&&console.warn(e)},getWeek:function(e){var t=new Date(e.getTime());t.setHours(0,0,0,0),t.setDate(t.getDate()+3-(t.getDay()+6)%7);var n=new Date(t.getFullYear(),0,4);return 1+Math.round(((t.getTime()-n.getTime())/864e5-3+(n.getDay()+6)%7)/7)},hourIncrement:1,ignoredFocusElements:[],inline:!1,locale:\"default\",minuteIncrement:5,mode:\"single\",monthSelectorType:\"dropdown\",nextArrow:\"<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 17 17'><g></g><path d='M13.207 8.472l-7.854 7.854-0.707-0.707 7.146-7.146-7.146-7.148 0.707-0.707 7.854 7.854z' /></svg>\",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:\"auto\",positionElement:void 0,prevArrow:\"<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 17 17'><g></g><path d='M5.207 8.471l7.146 7.147-0.707 0.707-7.853-7.854 7.854-7.853 0.707 0.707-7.147 7.146z' /></svg>\",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1},i={weekdays:{shorthand:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],longhand:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"]},months:{shorthand:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],longhand:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"]},daysInMonth:[31,28,31,30,31,30,31,31,30,31,30,31],firstDayOfWeek:0,ordinal:function(e){var t=e%100;if(t>3&&t<21)return\"th\";switch(t%10){case 1:return\"st\";case 2:return\"nd\";case 3:return\"rd\";default:return\"th\"}},rangeSeparator:\" to \",weekAbbreviation:\"Wk\",scrollTitle:\"Scroll to increment\",toggleTitle:\"Click to toggle\",amPM:[\"AM\",\"PM\"],yearAriaLabel:\"Year\",monthAriaLabel:\"Month\",hourAriaLabel:\"Hour\",minuteAriaLabel:\"Minute\",time_24hr:!1},a=function(e,t){return void 0===t&&(t=2),(\"000\"+e).slice(-1*t)},o=function(e){return!0===e?1:0};function s(e,t){var n;return function(){var r=this,i=arguments;clearTimeout(n),n=setTimeout(function(){return e.apply(r,i)},t)}}var l=function(e){return e instanceof Array?e:[e]};function u(e,t,n){if(!0===n)return e.classList.add(t);e.classList.remove(t)}function d(e,t,n){var r=window.document.createElement(e);return t=t||\"\",n=n||\"\",r.className=t,void 0!==n&&(r.textContent=n),r}function c(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function f(e,t){return t(e)?e:e.parentNode?f(e.parentNode,t):void 0}function p(e,t){var n=d(\"div\",\"numInputWrapper\"),r=d(\"input\",\"numInput \"+e),i=d(\"span\",\"arrowUp\"),a=d(\"span\",\"arrowDown\");if(-1===navigator.userAgent.indexOf(\"MSIE 9.0\")?r.type=\"number\":(r.type=\"text\",r.pattern=\"\\\\d*\"),void 0!==t)for(var o in t)r.setAttribute(o,t[o]);return n.appendChild(r),n.appendChild(i),n.appendChild(a),n}function h(e){try{return\"function\"==typeof e.composedPath?e.composedPath()[0]:e.target}catch(t){return e.target}}var m=function(){},v=function(e,t,n){return n.months[t?\"shorthand\":\"longhand\"][e]},g={D:m,F:function(e,t,n){e.setMonth(n.months.longhand.indexOf(t))},G:function(e,t){e.setHours((e.getHours()>=12?12:0)+parseFloat(t))},H:function(e,t){e.setHours(parseFloat(t))},J:function(e,t){e.setDate(parseFloat(t))},K:function(e,t,n){e.setHours(e.getHours()%12+12*o(new RegExp(n.amPM[1],\"i\").test(t)))},M:function(e,t,n){e.setMonth(n.months.shorthand.indexOf(t))},S:function(e,t){e.setSeconds(parseFloat(t))},U:function(e,t){return new Date(1e3*parseFloat(t))},W:function(e,t,n){var r=parseInt(t),i=new Date(e.getFullYear(),0,2+7*(r-1),0,0,0,0);return i.setDate(i.getDate()-i.getDay()+n.firstDayOfWeek),i},Y:function(e,t){e.setFullYear(parseFloat(t))},Z:function(e,t){return new Date(t)},d:function(e,t){e.setDate(parseFloat(t))},h:function(e,t){e.setHours((e.getHours()>=12?12:0)+parseFloat(t))},i:function(e,t){e.setMinutes(parseFloat(t))},j:function(e,t){e.setDate(parseFloat(t))},l:m,m:function(e,t){e.setMonth(parseFloat(t)-1)},n:function(e,t){e.setMonth(parseFloat(t)-1)},s:function(e,t){e.setSeconds(parseFloat(t))},u:function(e,t){return new Date(parseFloat(t))},w:m,y:function(e,t){e.setFullYear(2e3+parseFloat(t))}},_={D:\"\",F:\"\",G:\"(\\\\d\\\\d|\\\\d)\",H:\"(\\\\d\\\\d|\\\\d)\",J:\"(\\\\d\\\\d|\\\\d)\\\\w+\",K:\"\",M:\"\",S:\"(\\\\d\\\\d|\\\\d)\",U:\"(.+)\",W:\"(\\\\d\\\\d|\\\\d)\",Y:\"(\\\\d{4})\",Z:\"(.+)\",d:\"(\\\\d\\\\d|\\\\d)\",h:\"(\\\\d\\\\d|\\\\d)\",i:\"(\\\\d\\\\d|\\\\d)\",j:\"(\\\\d\\\\d|\\\\d)\",l:\"\",m:\"(\\\\d\\\\d|\\\\d)\",n:\"(\\\\d\\\\d|\\\\d)\",s:\"(\\\\d\\\\d|\\\\d)\",u:\"(.+)\",w:\"(\\\\d\\\\d|\\\\d)\",y:\"(\\\\d{2})\"},y={Z:function(e){return e.toISOString()},D:function(e,t,n){return t.weekdays.shorthand[y.w(e,t,n)]},F:function(e,t,n){return v(y.n(e,t,n)-1,!1,t)},G:function(e,t,n){return a(y.h(e,t,n))},H:function(e){return a(e.getHours())},J:function(e,t){return void 0!==t.ordinal?e.getDate()+t.ordinal(e.getDate()):e.getDate()},K:function(e,t){return t.amPM[o(e.getHours()>11)]},M:function(e,t){return v(e.getMonth(),!0,t)},S:function(e){return a(e.getSeconds())},U:function(e){return e.getTime()/1e3},W:function(e,t,n){return n.getWeek(e)},Y:function(e){return a(e.getFullYear(),4)},d:function(e){return a(e.getDate())},h:function(e){return e.getHours()%12?e.getHours()%12:12},i:function(e){return a(e.getMinutes())},j:function(e){return e.getDate()},l:function(e,t){return t.weekdays.longhand[e.getDay()]},m:function(e){return a(e.getMonth()+1)},n:function(e){return e.getMonth()+1},s:function(e){return e.getSeconds()},u:function(e){return e.getTime()},w:function(e){return e.getDay()},y:function(e){return String(e.getFullYear()).substring(2)}},b=function(e){var t=e.config,n=void 0===t?r:t,a=e.l10n,o=void 0===a?i:a,s=e.isMobile,l=void 0!==s&&s;return function(e,t,r){var i=r||o;return void 0===n.formatDate||l?t.split(\"\").map(function(t,r,a){return y[t]&&\"\\\\\"!==a[r-1]?y[t](e,i,n):\"\\\\\"!==t?t:\"\"}).join(\"\"):n.formatDate(e,t,i)}},M=function(e){var t=e.config,n=void 0===t?r:t,a=e.l10n,o=void 0===a?i:a;return function(e,t,i,a){if(0===e||e){var s,l=a||o,u=e;if(e instanceof Date)s=new Date(e.getTime());else if(\"string\"!=typeof e&&void 0!==e.toFixed)s=new Date(e);else if(\"string\"==typeof e){var d=t||(n||r).dateFormat,c=String(e).trim();if(\"today\"===c)s=new Date,i=!0;else if(n&&n.parseDate)s=n.parseDate(e,d);else if(/Z$/.test(c)||/GMT$/.test(c))s=new Date(e);else{for(var f=void 0,p=[],h=0,m=0,v=\"\";h<d.length;h++){var y=d[h],b=\"\\\\\"===y,M=\"\\\\\"===d[h-1]||b;if(_[y]&&!M){v+=_[y];var w=new RegExp(v).exec(e);w&&(f=!0)&&p[\"Y\"!==y?\"push\":\"unshift\"]({fn:g[y],val:w[++m]})}else b||(v+=\".\")}s=n&&n.noCalendar?new Date((new Date).setHours(0,0,0,0)):new Date((new Date).getFullYear(),0,1,0,0,0,0),p.forEach(function(e){var t=e.fn,n=e.val;return s=t(s,n,l)||s}),s=f?s:void 0}}if(s instanceof Date&&!isNaN(s.getTime()))return!0===i&&s.setHours(0,0,0,0),s;n.errorHandler(new Error(\"Invalid date provided: \"+u))}}};function w(e,t,n){return void 0===n&&(n=!0),!1!==n?new Date(e.getTime()).setHours(0,0,0,0)-new Date(t.getTime()).setHours(0,0,0,0):e.getTime()-t.getTime()}var k=function(e,t,n){return 3600*e+60*t+n},L=864e5;function x(e){var t=e.defaultHour,n=e.defaultMinute,r=e.defaultSeconds;if(void 0!==e.minDate){var i=e.minDate.getHours(),a=e.minDate.getMinutes(),o=e.minDate.getSeconds();t<i&&(t=i),t===i&&n<a&&(n=a),t===i&&n===a&&r<o&&(r=e.minDate.getSeconds())}if(void 0!==e.maxDate){var s=e.maxDate.getHours(),l=e.maxDate.getMinutes();(t=Math.min(t,s))===s&&(n=Math.min(l,n)),t===s&&n===l&&(r=e.maxDate.getSeconds())}return{hours:t,minutes:n,seconds:r}}\"function\"!=typeof Object.assign&&(Object.assign=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];if(!e)throw TypeError(\"Cannot convert undefined or null to object\");for(var r=function(t){t&&Object.keys(t).forEach(function(n){return e[n]=t[n]})},i=0,a=t;i<a.length;i++){r(a[i])}return e});function T(m,g){var y={config:e(e({},r),D.defaultConfig),l10n:i};function T(){var e;return(null===(e=y.calendarContainer)||void 0===e?void 0:e.getRootNode()).activeElement||document.activeElement}function S(e){return e.bind(y)}function E(){var e=y.config;!1===e.weekNumbers&&1===e.showMonths||!0!==e.noCalendar&&window.requestAnimationFrame(function(){if(void 0!==y.calendarContainer&&(y.calendarContainer.style.visibility=\"hidden\",y.calendarContainer.style.display=\"block\"),void 0!==y.daysContainer){var t=(y.days.offsetWidth+1)*e.showMonths;y.daysContainer.style.width=t+\"px\",y.calendarContainer.style.width=t+(void 0!==y.weekWrapper?y.weekWrapper.offsetWidth:0)+\"px\",y.calendarContainer.style.removeProperty(\"visibility\"),y.calendarContainer.style.removeProperty(\"display\")}})}function Y(e){if(0===y.selectedDates.length){var t=void 0===y.config.minDate||w(new Date,y.config.minDate)>=0?new Date:new Date(y.config.minDate.getTime()),n=x(y.config);t.setHours(n.hours,n.minutes,n.seconds,t.getMilliseconds()),y.selectedDates=[t],y.latestSelectedDateObj=t}void 0!==e&&\"blur\"!==e.type&&function(e){e.preventDefault();var t=\"keydown\"===e.type,n=h(e),r=n;void 0!==y.amPM&&n===y.amPM&&(y.amPM.textContent=y.l10n.amPM[o(y.amPM.textContent===y.l10n.amPM[0])]);var i=parseFloat(r.getAttribute(\"min\")),s=parseFloat(r.getAttribute(\"max\")),l=parseFloat(r.getAttribute(\"step\")),u=parseInt(r.value,10),d=e.delta||(t?38===e.which?1:-1:0),c=u+l*d;if(void 0!==r.value&&2===r.value.length){var f=r===y.hourElement,p=r===y.minuteElement;c<i?(c=s+c+o(!f)+(o(f)&&o(!y.amPM)),p&&N(void 0,-1,y.hourElement)):c>s&&(c=r===y.hourElement?c-s-o(!y.amPM):i,p&&N(void 0,1,y.hourElement)),y.amPM&&f&&(1===l?c+u===23:Math.abs(c-u)>l)&&(y.amPM.textContent=y.l10n.amPM[o(y.amPM.textContent===y.l10n.amPM[0])]),r.value=a(c)}}(e);var r=y._input.value;A(),ke(),y._input.value!==r&&y._debouncedChange()}function A(){if(void 0!==y.hourElement&&void 0!==y.minuteElement){var e,t,n=(parseInt(y.hourElement.value.slice(-2),10)||0)%24,r=(parseInt(y.minuteElement.value,10)||0)%60,i=void 0!==y.secondElement?(parseInt(y.secondElement.value,10)||0)%60:0;void 0!==y.amPM&&(e=n,t=y.amPM.textContent,n=e%12+12*o(t===y.l10n.amPM[1]));var a=void 0!==y.config.minTime||y.config.minDate&&y.minDateHasTime&&y.latestSelectedDateObj&&0===w(y.latestSelectedDateObj,y.config.minDate,!0),s=void 0!==y.config.maxTime||y.config.maxDate&&y.maxDateHasTime&&y.latestSelectedDateObj&&0===w(y.latestSelectedDateObj,y.config.maxDate,!0);if(void 0!==y.config.maxTime&&void 0!==y.config.minTime&&y.config.minTime>y.config.maxTime){var l=k(y.config.minTime.getHours(),y.config.minTime.getMinutes(),y.config.minTime.getSeconds()),u=k(y.config.maxTime.getHours(),y.config.maxTime.getMinutes(),y.config.maxTime.getSeconds()),d=k(n,r,i);if(d>u&&d<l){var c=function(e){var t=Math.floor(e/3600),n=(e-3600*t)/60;return[t,n,e-3600*t-60*n]}(l);n=c[0],r=c[1],i=c[2]}}else{if(s){var f=void 0!==y.config.maxTime?y.config.maxTime:y.config.maxDate;(n=Math.min(n,f.getHours()))===f.getHours()&&(r=Math.min(r,f.getMinutes())),r===f.getMinutes()&&(i=Math.min(i,f.getSeconds()))}if(a){var p=void 0!==y.config.minTime?y.config.minTime:y.config.minDate;(n=Math.max(n,p.getHours()))===p.getHours()&&r<p.getMinutes()&&(r=p.getMinutes()),r===p.getMinutes()&&(i=Math.max(i,p.getSeconds()))}}C(n,r,i)}}function O(e){var t=e||y.latestSelectedDateObj;t&&t instanceof Date&&C(t.getHours(),t.getMinutes(),t.getSeconds())}function C(e,t,n){void 0!==y.latestSelectedDateObj&&y.latestSelectedDateObj.setHours(e%24,t,n||0,0),y.hourElement&&y.minuteElement&&!y.isMobile&&(y.hourElement.value=a(y.config.time_24hr?e:(12+e)%12+12*o(e%12==0)),y.minuteElement.value=a(t),void 0!==y.amPM&&(y.amPM.textContent=y.l10n.amPM[o(e>=12)]),void 0!==y.secondElement&&(y.secondElement.value=a(n)))}function j(e){var t=h(e),n=parseInt(t.value)+(e.delta||0);(n/1e3>1||\"Enter\"===e.key&&!/[^\\d]/.test(n.toString()))&&ee(n)}function P(e,t,n,r){return t instanceof Array?t.forEach(function(t){return P(e,t,n,r)}):e instanceof Array?e.forEach(function(e){return P(e,t,n,r)}):(e.addEventListener(t,n,r),void y._handlers.push({remove:function(){return e.removeEventListener(t,n,r)}}))}function H(){_e(\"onChange\")}function I(e,t){var n=void 0!==e?y.parseDate(e):y.latestSelectedDateObj||(y.config.minDate&&y.config.minDate>y.now?y.config.minDate:y.config.maxDate&&y.config.maxDate<y.now?y.config.maxDate:y.now),r=y.currentYear,i=y.currentMonth;try{void 0!==n&&(y.currentYear=n.getFullYear(),y.currentMonth=n.getMonth())}catch(e){e.message=\"Invalid date supplied: \"+n,y.config.errorHandler(e)}t&&y.currentYear!==r&&(_e(\"onYearChange\"),B()),!t||y.currentYear===r&&y.currentMonth===i||_e(\"onMonthChange\"),y.redraw()}function F(e){var t=h(e);~t.className.indexOf(\"arrow\")&&N(e,t.classList.contains(\"arrowUp\")?1:-1)}function N(e,t,n){var r=e&&h(e),i=n||r&&r.parentNode&&r.parentNode.firstChild,a=ye(\"increment\");a.delta=t,i&&i.dispatchEvent(a)}function R(e,t,n,r){var i=te(t,!0),a=d(\"span\",e,t.getDate().toString());return a.dateObj=t,a.$i=r,a.setAttribute(\"aria-label\",y.formatDate(t,y.config.ariaDateFormat)),-1===e.indexOf(\"hidden\")&&0===w(t,y.now)&&(y.todayDateElem=a,a.classList.add(\"today\"),a.setAttribute(\"aria-current\",\"date\")),i?(a.tabIndex=-1,be(t)&&(a.classList.add(\"selected\"),y.selectedDateElem=a,\"range\"===y.config.mode&&(u(a,\"startRange\",y.selectedDates[0]&&0===w(t,y.selectedDates[0],!0)),u(a,\"endRange\",y.selectedDates[1]&&0===w(t,y.selectedDates[1],!0)),\"nextMonthDay\"===e&&a.classList.add(\"inRange\")))):a.classList.add(\"flatpickr-disabled\"),\"range\"===y.config.mode&&function(e){return!(\"range\"!==y.config.mode||y.selectedDates.length<2)&&(w(e,y.selectedDates[0])>=0&&w(e,y.selectedDates[1])<=0)}(t)&&!be(t)&&a.classList.add(\"inRange\"),y.weekNumbers&&1===y.config.showMonths&&\"prevMonthDay\"!==e&&r%7==6&&y.weekNumbers.insertAdjacentHTML(\"beforeend\",\"<span class='flatpickr-day'>\"+y.config.getWeek(t)+\"</span>\"),_e(\"onDayCreate\",a),a}function V(e){e.focus(),\"range\"===y.config.mode&&ae(e)}function $(e){for(var t=e>0?0:y.config.showMonths-1,n=e>0?y.config.showMonths:-1,r=t;r!=n;r+=e)for(var i=y.daysContainer.children[r],a=e>0?0:i.children.length-1,o=e>0?i.children.length:-1,s=a;s!=o;s+=e){var l=i.children[s];if(-1===l.className.indexOf(\"hidden\")&&te(l.dateObj))return l}}function z(e,t){var n=T(),r=ne(n||document.body),i=void 0!==e?e:r?n:void 0!==y.selectedDateElem&&ne(y.selectedDateElem)?y.selectedDateElem:void 0!==y.todayDateElem&&ne(y.todayDateElem)?y.todayDateElem:$(t>0?1:-1);void 0===i?y._input.focus():r?function(e,t){for(var n=-1===e.className.indexOf(\"Month\")?e.dateObj.getMonth():y.currentMonth,r=t>0?y.config.showMonths:-1,i=t>0?1:-1,a=n-y.currentMonth;a!=r;a+=i)for(var o=y.daysContainer.children[a],s=n-y.currentMonth===a?e.$i+t:t<0?o.children.length-1:0,l=o.children.length,u=s;u>=0&&u<l&&u!=(t>0?l:-1);u+=i){var d=o.children[u];if(-1===d.className.indexOf(\"hidden\")&&te(d.dateObj)&&Math.abs(e.$i-u)>=Math.abs(t))return V(d)}y.changeMonth(i),z($(i),0)}(i,t):V(i)}function W(e,t){for(var n=(new Date(e,t,1).getDay()-y.l10n.firstDayOfWeek+7)%7,r=y.utils.getDaysInMonth((t-1+12)%12,e),i=y.utils.getDaysInMonth(t,e),a=window.document.createDocumentFragment(),o=y.config.showMonths>1,s=o?\"prevMonthDay hidden\":\"prevMonthDay\",l=o?\"nextMonthDay hidden\":\"nextMonthDay\",u=r+1-n,c=0;u<=r;u++,c++)a.appendChild(R(\"flatpickr-day \"+s,new Date(e,t-1,u),0,c));for(u=1;u<=i;u++,c++)a.appendChild(R(\"flatpickr-day\",new Date(e,t,u),0,c));for(var f=i+1;f<=42-n&&(1===y.config.showMonths||c%7!=0);f++,c++)a.appendChild(R(\"flatpickr-day \"+l,new Date(e,t+1,f%i),0,c));var p=d(\"div\",\"dayContainer\");return p.appendChild(a),p}function U(){if(void 0!==y.daysContainer){c(y.daysContainer),y.weekNumbers&&c(y.weekNumbers);for(var e=document.createDocumentFragment(),t=0;t<y.config.showMonths;t++){var n=new Date(y.currentYear,y.currentMonth,1);n.setMonth(y.currentMonth+t),e.appendChild(W(n.getFullYear(),n.getMonth()))}y.daysContainer.appendChild(e),y.days=y.daysContainer.firstChild,\"range\"===y.config.mode&&1===y.selectedDates.length&&ae()}}function B(){if(!(y.config.showMonths>1||\"dropdown\"!==y.config.monthSelectorType)){var e=function(e){return!(void 0!==y.config.minDate&&y.currentYear===y.config.minDate.getFullYear()&&e<y.config.minDate.getMonth())&&!(void 0!==y.config.maxDate&&y.currentYear===y.config.maxDate.getFullYear()&&e>y.config.maxDate.getMonth())};y.monthsDropdownContainer.tabIndex=-1,y.monthsDropdownContainer.innerHTML=\"\";for(var t=0;t<12;t++)if(e(t)){var n=d(\"option\",\"flatpickr-monthDropdown-month\");n.value=new Date(y.currentYear,t).getMonth().toString(),n.textContent=v(t,y.config.shorthandCurrentMonth,y.l10n),n.tabIndex=-1,y.currentMonth===t&&(n.selected=!0),y.monthsDropdownContainer.appendChild(n)}}}function q(){var e,t=d(\"div\",\"flatpickr-month\"),n=window.document.createDocumentFragment();y.config.showMonths>1||\"static\"===y.config.monthSelectorType?e=d(\"span\",\"cur-month\"):(y.monthsDropdownContainer=d(\"select\",\"flatpickr-monthDropdown-months\"),y.monthsDropdownContainer.setAttribute(\"aria-label\",y.l10n.monthAriaLabel),P(y.monthsDropdownContainer,\"change\",function(e){var t=h(e),n=parseInt(t.value,10);y.changeMonth(n-y.currentMonth),_e(\"onMonthChange\")}),B(),e=y.monthsDropdownContainer);var r=p(\"cur-year\",{tabindex:\"-1\"}),i=r.getElementsByTagName(\"input\")[0];i.setAttribute(\"aria-label\",y.l10n.yearAriaLabel),y.config.minDate&&i.setAttribute(\"min\",y.config.minDate.getFullYear().toString()),y.config.maxDate&&(i.setAttribute(\"max\",y.config.maxDate.getFullYear().toString()),i.disabled=!!y.config.minDate&&y.config.minDate.getFullYear()===y.config.maxDate.getFullYear());var a=d(\"div\",\"flatpickr-current-month\");return a.appendChild(e),a.appendChild(r),n.appendChild(a),t.appendChild(n),{container:t,yearElement:i,monthElement:e}}function G(){c(y.monthNav),y.monthNav.appendChild(y.prevMonthNav),y.config.showMonths&&(y.yearElements=[],y.monthElements=[]);for(var e=y.config.showMonths;e--;){var t=q();y.yearElements.push(t.yearElement),y.monthElements.push(t.monthElement),y.monthNav.appendChild(t.container)}y.monthNav.appendChild(y.nextMonthNav)}function J(){y.weekdayContainer?c(y.weekdayContainer):y.weekdayContainer=d(\"div\",\"flatpickr-weekdays\");for(var e=y.config.showMonths;e--;){var t=d(\"div\",\"flatpickr-weekdaycontainer\");y.weekdayContainer.appendChild(t)}return Z(),y.weekdayContainer}function Z(){if(y.weekdayContainer){var e=y.l10n.firstDayOfWeek,n=t(y.l10n.weekdays.shorthand);e>0&&e<n.length&&(n=t(n.splice(e,n.length),n.splice(0,e)));for(var r=y.config.showMonths;r--;)y.weekdayContainer.children[r].innerHTML=\"\\n      <span class='flatpickr-weekday'>\\n        \"+n.join(\"</span><span class='flatpickr-weekday'>\")+\"\\n      </span>\\n      \"}}function K(e,t){void 0===t&&(t=!0);var n=t?e:e-y.currentMonth;n<0&&!0===y._hidePrevMonthArrow||n>0&&!0===y._hideNextMonthArrow||(y.currentMonth+=n,(y.currentMonth<0||y.currentMonth>11)&&(y.currentYear+=y.currentMonth>11?1:-1,y.currentMonth=(y.currentMonth+12)%12,_e(\"onYearChange\"),B()),U(),_e(\"onMonthChange\"),Me())}function X(e){return y.calendarContainer.contains(e)}function Q(e){if(y.isOpen&&!y.config.inline){var t=h(e),n=X(t),r=!(t===y.input||t===y.altInput||y.element.contains(t)||e.path&&e.path.indexOf&&(~e.path.indexOf(y.input)||~e.path.indexOf(y.altInput)))&&!n&&!X(e.relatedTarget),i=!y.config.ignoredFocusElements.some(function(e){return e.contains(t)});r&&i&&(y.config.allowInput&&y.setDate(y._input.value,!1,y.config.altInput?y.config.altFormat:y.config.dateFormat),void 0!==y.timeContainer&&void 0!==y.minuteElement&&void 0!==y.hourElement&&\"\"!==y.input.value&&void 0!==y.input.value&&Y(),y.close(),y.config&&\"range\"===y.config.mode&&1===y.selectedDates.length&&y.clear(!1))}}function ee(e){if(!(!e||y.config.minDate&&e<y.config.minDate.getFullYear()||y.config.maxDate&&e>y.config.maxDate.getFullYear())){var t=e,n=y.currentYear!==t;y.currentYear=t||y.currentYear,y.config.maxDate&&y.currentYear===y.config.maxDate.getFullYear()?y.currentMonth=Math.min(y.config.maxDate.getMonth(),y.currentMonth):y.config.minDate&&y.currentYear===y.config.minDate.getFullYear()&&(y.currentMonth=Math.max(y.config.minDate.getMonth(),y.currentMonth)),n&&(y.redraw(),_e(\"onYearChange\"),B())}}function te(e,t){var n;void 0===t&&(t=!0);var r=y.parseDate(e,void 0,t);if(y.config.minDate&&r&&w(r,y.config.minDate,void 0!==t?t:!y.minDateHasTime)<0||y.config.maxDate&&r&&w(r,y.config.maxDate,void 0!==t?t:!y.maxDateHasTime)>0)return!1;if(!y.config.enable&&0===y.config.disable.length)return!0;if(void 0===r)return!1;for(var i=!!y.config.enable,a=null!==(n=y.config.enable)&&void 0!==n?n:y.config.disable,o=0,s=void 0;o<a.length;o++){if(\"function\"==typeof(s=a[o])&&s(r))return i;if(s instanceof Date&&void 0!==r&&s.getTime()===r.getTime())return i;if(\"string\"==typeof s){var l=y.parseDate(s,void 0,!0);return l&&l.getTime()===r.getTime()?i:!i}if(\"object\"==typeof s&&void 0!==r&&s.from&&s.to&&r.getTime()>=s.from.getTime()&&r.getTime()<=s.to.getTime())return i}return!i}function ne(e){return void 0!==y.daysContainer&&(-1===e.className.indexOf(\"hidden\")&&-1===e.className.indexOf(\"flatpickr-disabled\")&&y.daysContainer.contains(e))}function re(e){var t=e.target===y._input,n=y._input.value.trimEnd()!==we();!t||!n||e.relatedTarget&&X(e.relatedTarget)||y.setDate(y._input.value,!0,e.target===y.altInput?y.config.altFormat:y.config.dateFormat)}function ie(e){var t=h(e),n=y.config.wrap?m.contains(t):t===y._input,r=y.config.allowInput,i=y.isOpen&&(!r||!n),a=y.config.inline&&n&&!r;if(13===e.keyCode&&n){if(r)return y.setDate(y._input.value,!0,t===y.altInput?y.config.altFormat:y.config.dateFormat),y.close(),t.blur();y.open()}else if(X(t)||i||a){var o=!!y.timeContainer&&y.timeContainer.contains(t);switch(e.keyCode){case 13:o?(e.preventDefault(),Y(),fe()):pe(e);break;case 27:e.preventDefault(),fe();break;case 8:case 46:n&&!y.config.allowInput&&(e.preventDefault(),y.clear());break;case 37:case 39:if(o||n)y.hourElement&&y.hourElement.focus();else{e.preventDefault();var s=T();if(void 0!==y.daysContainer&&(!1===r||s&&ne(s))){var l=39===e.keyCode?1:-1;e.ctrlKey?(e.stopPropagation(),K(l),z($(1),0)):z(void 0,l)}}break;case 38:case 40:e.preventDefault();var u=40===e.keyCode?1:-1;y.daysContainer&&void 0!==t.$i||t===y.input||t===y.altInput?e.ctrlKey?(e.stopPropagation(),ee(y.currentYear-u),z($(1),0)):o||z(void 0,7*u):t===y.currentYearElement?ee(y.currentYear-u):y.config.enableTime&&(!o&&y.hourElement&&y.hourElement.focus(),Y(e),y._debouncedChange());break;case 9:if(o){var d=[y.hourElement,y.minuteElement,y.secondElement,y.amPM].concat(y.pluginElements).filter(function(e){return e}),c=d.indexOf(t);if(-1!==c){var f=d[c+(e.shiftKey?-1:1)];e.preventDefault(),(f||y._input).focus()}}else!y.config.noCalendar&&y.daysContainer&&y.daysContainer.contains(t)&&e.shiftKey&&(e.preventDefault(),y._input.focus())}}if(void 0!==y.amPM&&t===y.amPM)switch(e.key){case y.l10n.amPM[0].charAt(0):case y.l10n.amPM[0].charAt(0).toLowerCase():y.amPM.textContent=y.l10n.amPM[0],A(),ke();break;case y.l10n.amPM[1].charAt(0):case y.l10n.amPM[1].charAt(0).toLowerCase():y.amPM.textContent=y.l10n.amPM[1],A(),ke()}(n||X(t))&&_e(\"onKeyDown\",e)}function ae(e,t){if(void 0===t&&(t=\"flatpickr-day\"),1===y.selectedDates.length&&(!e||e.classList.contains(t)&&!e.classList.contains(\"flatpickr-disabled\"))){for(var n=e?e.dateObj.getTime():y.days.firstElementChild.dateObj.getTime(),r=y.parseDate(y.selectedDates[0],void 0,!0).getTime(),i=Math.min(n,y.selectedDates[0].getTime()),a=Math.max(n,y.selectedDates[0].getTime()),o=!1,s=0,l=0,u=i;u<a;u+=L)te(new Date(u),!0)||(o=o||u>i&&u<a,u<r&&(!s||u>s)?s=u:u>r&&(!l||u<l)&&(l=u));Array.from(y.rContainer.querySelectorAll(\"*:nth-child(-n+\"+y.config.showMonths+\") > .\"+t)).forEach(function(t){var i,a,u,d=t.dateObj.getTime(),c=s>0&&d<s||l>0&&d>l;if(c)return t.classList.add(\"notAllowed\"),void[\"inRange\",\"startRange\",\"endRange\"].forEach(function(e){t.classList.remove(e)});o&&!c||([\"startRange\",\"inRange\",\"endRange\",\"notAllowed\"].forEach(function(e){t.classList.remove(e)}),void 0!==e&&(e.classList.add(n<=y.selectedDates[0].getTime()?\"startRange\":\"endRange\"),r<n&&d===r?t.classList.add(\"startRange\"):r>n&&d===r&&t.classList.add(\"endRange\"),d>=s&&(0===l||d<=l)&&(a=r,u=n,(i=d)>Math.min(a,u)&&i<Math.max(a,u))&&t.classList.add(\"inRange\")))})}}function oe(){!y.isOpen||y.config.static||y.config.inline||de()}function se(e){return function(t){var n=y.config[\"_\"+e+\"Date\"]=y.parseDate(t,y.config.dateFormat),r=y.config[\"_\"+(\"min\"===e?\"max\":\"min\")+\"Date\"];void 0!==n&&(y[\"min\"===e?\"minDateHasTime\":\"maxDateHasTime\"]=n.getHours()>0||n.getMinutes()>0||n.getSeconds()>0),y.selectedDates&&(y.selectedDates=y.selectedDates.filter(function(e){return te(e)}),y.selectedDates.length||\"min\"!==e||O(n),ke()),y.daysContainer&&(ce(),void 0!==n?y.currentYearElement[e]=n.getFullYear().toString():y.currentYearElement.removeAttribute(e),y.currentYearElement.disabled=!!r&&void 0!==n&&r.getFullYear()===n.getFullYear())}}function le(){return y.config.wrap?m.querySelector(\"[data-input]\"):m}function ue(){\"object\"!=typeof y.config.locale&&void 0===D.l10ns[y.config.locale]&&y.config.errorHandler(new Error(\"flatpickr: invalid locale \"+y.config.locale)),y.l10n=e(e({},D.l10ns.default),\"object\"==typeof y.config.locale?y.config.locale:\"default\"!==y.config.locale?D.l10ns[y.config.locale]:void 0),_.D=\"(\"+y.l10n.weekdays.shorthand.join(\"|\")+\")\",_.l=\"(\"+y.l10n.weekdays.longhand.join(\"|\")+\")\",_.M=\"(\"+y.l10n.months.shorthand.join(\"|\")+\")\",_.F=\"(\"+y.l10n.months.longhand.join(\"|\")+\")\",_.K=\"(\"+y.l10n.amPM[0]+\"|\"+y.l10n.amPM[1]+\"|\"+y.l10n.amPM[0].toLowerCase()+\"|\"+y.l10n.amPM[1].toLowerCase()+\")\",void 0===e(e({},g),JSON.parse(JSON.stringify(m.dataset||{}))).time_24hr&&void 0===D.defaultConfig.time_24hr&&(y.config.time_24hr=y.l10n.time_24hr),y.formatDate=b(y),y.parseDate=M({config:y.config,l10n:y.l10n})}function de(e){if(\"function\"!=typeof y.config.position){if(void 0!==y.calendarContainer){_e(\"onPreCalendarPosition\");var t=e||y._positionElement,n=Array.prototype.reduce.call(y.calendarContainer.children,function(e,t){return e+t.offsetHeight},0),r=y.calendarContainer.offsetWidth,i=y.config.position.split(\" \"),a=i[0],o=i.length>1?i[1]:null,s=t.getBoundingClientRect(),l=window.innerHeight-s.bottom,d=\"above\"===a||\"below\"!==a&&l<n&&s.top>n,c=window.pageYOffset+s.top+(d?-n-2:t.offsetHeight+2);if(u(y.calendarContainer,\"arrowTop\",!d),u(y.calendarContainer,\"arrowBottom\",d),!y.config.inline){var f=window.pageXOffset+s.left,p=!1,h=!1;\"center\"===o?(f-=(r-s.width)/2,p=!0):\"right\"===o&&(f-=r-s.width,h=!0),u(y.calendarContainer,\"arrowLeft\",!p&&!h),u(y.calendarContainer,\"arrowCenter\",p),u(y.calendarContainer,\"arrowRight\",h);var m=window.document.body.offsetWidth-(window.pageXOffset+s.right),v=f+r>window.document.body.offsetWidth,g=m+r>window.document.body.offsetWidth;if(u(y.calendarContainer,\"rightMost\",v),!y.config.static)if(y.calendarContainer.style.top=c+\"px\",v)if(g){var _=function(){for(var e=null,t=0;t<document.styleSheets.length;t++){var n=document.styleSheets[t];if(n.cssRules){try{n.cssRules}catch(e){continue}e=n;break}}return null!=e?e:(r=document.createElement(\"style\"),document.head.appendChild(r),r.sheet);var r}();if(void 0===_)return;var b=window.document.body.offsetWidth,M=Math.max(0,b/2-r/2),w=_.cssRules.length,k=\"{left:\"+s.left+\"px;right:auto;}\";u(y.calendarContainer,\"rightMost\",!1),u(y.calendarContainer,\"centerMost\",!0),_.insertRule(\".flatpickr-calendar.centerMost:before,.flatpickr-calendar.centerMost:after\"+k,w),y.calendarContainer.style.left=M+\"px\",y.calendarContainer.style.right=\"auto\"}else y.calendarContainer.style.left=\"auto\",y.calendarContainer.style.right=m+\"px\";else y.calendarContainer.style.left=f+\"px\",y.calendarContainer.style.right=\"auto\"}}}else y.config.position(y,e)}function ce(){y.config.noCalendar||y.isMobile||(B(),Me(),U())}function fe(){y._input.focus(),-1!==window.navigator.userAgent.indexOf(\"MSIE\")||void 0!==navigator.msMaxTouchPoints?setTimeout(y.close,0):y.close()}function pe(e){e.preventDefault(),e.stopPropagation();var t=f(h(e),function(e){return e.classList&&e.classList.contains(\"flatpickr-day\")&&!e.classList.contains(\"flatpickr-disabled\")&&!e.classList.contains(\"notAllowed\")});if(void 0!==t){var n=t,r=y.latestSelectedDateObj=new Date(n.dateObj.getTime()),i=(r.getMonth()<y.currentMonth||r.getMonth()>y.currentMonth+y.config.showMonths-1)&&\"range\"!==y.config.mode;if(y.selectedDateElem=n,\"single\"===y.config.mode)y.selectedDates=[r];else if(\"multiple\"===y.config.mode){var a=be(r);a?y.selectedDates.splice(parseInt(a),1):y.selectedDates.push(r)}else\"range\"===y.config.mode&&(2===y.selectedDates.length&&y.clear(!1,!1),y.latestSelectedDateObj=r,y.selectedDates.push(r),0!==w(r,y.selectedDates[0],!0)&&y.selectedDates.sort(function(e,t){return e.getTime()-t.getTime()}));if(A(),i){var o=y.currentYear!==r.getFullYear();y.currentYear=r.getFullYear(),y.currentMonth=r.getMonth(),o&&(_e(\"onYearChange\"),B()),_e(\"onMonthChange\")}if(Me(),U(),ke(),i||\"range\"===y.config.mode||1!==y.config.showMonths?void 0!==y.selectedDateElem&&void 0===y.hourElement&&y.selectedDateElem&&y.selectedDateElem.focus():V(n),void 0!==y.hourElement&&void 0!==y.hourElement&&y.hourElement.focus(),y.config.closeOnSelect){var s=\"single\"===y.config.mode&&!y.config.enableTime,l=\"range\"===y.config.mode&&2===y.selectedDates.length&&!y.config.enableTime;(s||l)&&fe()}H()}}y.parseDate=M({config:y.config,l10n:y.l10n}),y._handlers=[],y.pluginElements=[],y.loadedPlugins=[],y._bind=P,y._setHoursFromDate=O,y._positionCalendar=de,y.changeMonth=K,y.changeYear=ee,y.clear=function(e,t){void 0===e&&(e=!0);void 0===t&&(t=!0);y.input.value=\"\",void 0!==y.altInput&&(y.altInput.value=\"\");void 0!==y.mobileInput&&(y.mobileInput.value=\"\");y.selectedDates=[],y.latestSelectedDateObj=void 0,!0===t&&(y.currentYear=y._initialDate.getFullYear(),y.currentMonth=y._initialDate.getMonth());if(!0===y.config.enableTime){var n=x(y.config);C(n.hours,n.minutes,n.seconds)}y.redraw(),e&&_e(\"onChange\")},y.close=function(){y.isOpen=!1,y.isMobile||(void 0!==y.calendarContainer&&y.calendarContainer.classList.remove(\"open\"),void 0!==y._input&&y._input.classList.remove(\"active\"));_e(\"onClose\")},y.onMouseOver=ae,y._createElement=d,y.createDay=R,y.destroy=function(){void 0!==y.config&&_e(\"onDestroy\");for(var e=y._handlers.length;e--;)y._handlers[e].remove();if(y._handlers=[],y.mobileInput)y.mobileInput.parentNode&&y.mobileInput.parentNode.removeChild(y.mobileInput),y.mobileInput=void 0;else if(y.calendarContainer&&y.calendarContainer.parentNode)if(y.config.static&&y.calendarContainer.parentNode){var t=y.calendarContainer.parentNode;if(t.lastChild&&t.removeChild(t.lastChild),t.parentNode){for(;t.firstChild;)t.parentNode.insertBefore(t.firstChild,t);t.parentNode.removeChild(t)}}else y.calendarContainer.parentNode.removeChild(y.calendarContainer);y.altInput&&(y.input.type=\"text\",y.altInput.parentNode&&y.altInput.parentNode.removeChild(y.altInput),delete y.altInput);y.input&&(y.input.type=y.input._type,y.input.classList.remove(\"flatpickr-input\"),y.input.removeAttribute(\"readonly\"));[\"_showTimeInput\",\"latestSelectedDateObj\",\"_hideNextMonthArrow\",\"_hidePrevMonthArrow\",\"__hideNextMonthArrow\",\"__hidePrevMonthArrow\",\"isMobile\",\"isOpen\",\"selectedDateElem\",\"minDateHasTime\",\"maxDateHasTime\",\"days\",\"daysContainer\",\"_input\",\"_positionElement\",\"innerContainer\",\"rContainer\",\"monthNav\",\"todayDateElem\",\"calendarContainer\",\"weekdayContainer\",\"prevMonthNav\",\"nextMonthNav\",\"monthsDropdownContainer\",\"currentMonthElement\",\"currentYearElement\",\"navigationCurrentMonth\",\"selectedDateElem\",\"config\"].forEach(function(e){try{delete y[e]}catch(e){}})},y.isEnabled=te,y.jumpToDate=I,y.updateValue=ke,y.open=function(e,t){void 0===t&&(t=y._positionElement);if(!0===y.isMobile){if(e){e.preventDefault();var n=h(e);n&&n.blur()}return void 0!==y.mobileInput&&(y.mobileInput.focus(),y.mobileInput.click()),void _e(\"onOpen\")}if(y._input.disabled||y.config.inline)return;var r=y.isOpen;y.isOpen=!0,r||(y.calendarContainer.classList.add(\"open\"),y._input.classList.add(\"active\"),_e(\"onOpen\"),de(t));!0===y.config.enableTime&&!0===y.config.noCalendar&&(!1!==y.config.allowInput||void 0!==e&&y.timeContainer.contains(e.relatedTarget)||setTimeout(function(){return y.hourElement.select()},50))},y.redraw=ce,y.set=function(e,t){if(null!==e&&\"object\"==typeof e)for(var r in Object.assign(y.config,e),e)void 0!==he[r]&&he[r].forEach(function(e){return e()});else y.config[e]=t,void 0!==he[e]?he[e].forEach(function(e){return e()}):n.indexOf(e)>-1&&(y.config[e]=l(t));y.redraw(),ke(!0)},y.setDate=function(e,t,n){void 0===t&&(t=!1);void 0===n&&(n=y.config.dateFormat);if(0!==e&&!e||e instanceof Array&&0===e.length)return y.clear(t);me(e,n),y.latestSelectedDateObj=y.selectedDates[y.selectedDates.length-1],y.redraw(),I(void 0,t),O(),0===y.selectedDates.length&&y.clear(!1);ke(t),t&&_e(\"onChange\")},y.toggle=function(e){if(!0===y.isOpen)return y.close();y.open(e)};var he={locale:[ue,Z],showMonths:[G,E,J],minDate:[I],maxDate:[I],positionElement:[ge],clickOpens:[function(){!0===y.config.clickOpens?(P(y._input,\"focus\",y.open),P(y._input,\"click\",y.open)):(y._input.removeEventListener(\"focus\",y.open),y._input.removeEventListener(\"click\",y.open))}]};function me(e,t){var n=[];if(e instanceof Array)n=e.map(function(e){return y.parseDate(e,t)});else if(e instanceof Date||\"number\"==typeof e)n=[y.parseDate(e,t)];else if(\"string\"==typeof e)switch(y.config.mode){case\"single\":case\"time\":n=[y.parseDate(e,t)];break;case\"multiple\":n=e.split(y.config.conjunction).map(function(e){return y.parseDate(e,t)});break;case\"range\":n=e.split(y.l10n.rangeSeparator).map(function(e){return y.parseDate(e,t)})}else y.config.errorHandler(new Error(\"Invalid date supplied: \"+JSON.stringify(e)));y.selectedDates=y.config.allowInvalidPreload?n:n.filter(function(e){return e instanceof Date&&te(e,!1)}),\"range\"===y.config.mode&&y.selectedDates.sort(function(e,t){return e.getTime()-t.getTime()})}function ve(e){return e.slice().map(function(e){return\"string\"==typeof e||\"number\"==typeof e||e instanceof Date?y.parseDate(e,void 0,!0):e&&\"object\"==typeof e&&e.from&&e.to?{from:y.parseDate(e.from,void 0),to:y.parseDate(e.to,void 0)}:e}).filter(function(e){return e})}function ge(){y._positionElement=y.config.positionElement||y._input}function _e(e,t){if(void 0!==y.config){var n=y.config[e];if(void 0!==n&&n.length>0)for(var r=0;n[r]&&r<n.length;r++)n[r](y.selectedDates,y.input.value,y,t);\"onChange\"===e&&(y.input.dispatchEvent(ye(\"change\")),y.input.dispatchEvent(ye(\"input\")))}}function ye(e){var t=document.createEvent(\"Event\");return t.initEvent(e,!0,!0),t}function be(e){for(var t=0;t<y.selectedDates.length;t++){var n=y.selectedDates[t];if(n instanceof Date&&0===w(n,e))return\"\"+t}return!1}function Me(){y.config.noCalendar||y.isMobile||!y.monthNav||(y.yearElements.forEach(function(e,t){var n=new Date(y.currentYear,y.currentMonth,1);n.setMonth(y.currentMonth+t),y.config.showMonths>1||\"static\"===y.config.monthSelectorType?y.monthElements[t].textContent=v(n.getMonth(),y.config.shorthandCurrentMonth,y.l10n)+\" \":y.monthsDropdownContainer.value=n.getMonth().toString(),e.value=n.getFullYear().toString()}),y._hidePrevMonthArrow=void 0!==y.config.minDate&&(y.currentYear===y.config.minDate.getFullYear()?y.currentMonth<=y.config.minDate.getMonth():y.currentYear<y.config.minDate.getFullYear()),y._hideNextMonthArrow=void 0!==y.config.maxDate&&(y.currentYear===y.config.maxDate.getFullYear()?y.currentMonth+1>y.config.maxDate.getMonth():y.currentYear>y.config.maxDate.getFullYear()))}function we(e){var t=e||(y.config.altInput?y.config.altFormat:y.config.dateFormat);return y.selectedDates.map(function(e){return y.formatDate(e,t)}).filter(function(e,t,n){return\"range\"!==y.config.mode||y.config.enableTime||n.indexOf(e)===t}).join(\"range\"!==y.config.mode?y.config.conjunction:y.l10n.rangeSeparator)}function ke(e){void 0===e&&(e=!0),void 0!==y.mobileInput&&y.mobileFormatStr&&(y.mobileInput.value=void 0!==y.latestSelectedDateObj?y.formatDate(y.latestSelectedDateObj,y.mobileFormatStr):\"\"),y.input.value=we(y.config.dateFormat),void 0!==y.altInput&&(y.altInput.value=we(y.config.altFormat)),!1!==e&&_e(\"onValueUpdate\")}function Le(e){var t=h(e),n=y.prevMonthNav.contains(t),r=y.nextMonthNav.contains(t);n||r?K(n?-1:1):y.yearElements.indexOf(t)>=0?t.select():t.classList.contains(\"arrowUp\")?y.changeYear(y.currentYear+1):t.classList.contains(\"arrowDown\")&&y.changeYear(y.currentYear-1)}return function(){y.element=y.input=m,y.isOpen=!1,function(){var t=[\"wrap\",\"weekNumbers\",\"allowInput\",\"allowInvalidPreload\",\"clickOpens\",\"time_24hr\",\"enableTime\",\"noCalendar\",\"altInput\",\"shorthandCurrentMonth\",\"inline\",\"static\",\"enableSeconds\",\"disableMobile\"],i=e(e({},JSON.parse(JSON.stringify(m.dataset||{}))),g),a={};y.config.parseDate=i.parseDate,y.config.formatDate=i.formatDate,Object.defineProperty(y.config,\"enable\",{get:function(){return y.config._enable},set:function(e){y.config._enable=ve(e)}}),Object.defineProperty(y.config,\"disable\",{get:function(){return y.config._disable},set:function(e){y.config._disable=ve(e)}});var o=\"time\"===i.mode;if(!i.dateFormat&&(i.enableTime||o)){var s=D.defaultConfig.dateFormat||r.dateFormat;a.dateFormat=i.noCalendar||o?\"H:i\"+(i.enableSeconds?\":S\":\"\"):s+\" H:i\"+(i.enableSeconds?\":S\":\"\")}if(i.altInput&&(i.enableTime||o)&&!i.altFormat){var u=D.defaultConfig.altFormat||r.altFormat;a.altFormat=i.noCalendar||o?\"h:i\"+(i.enableSeconds?\":S K\":\" K\"):u+\" h:i\"+(i.enableSeconds?\":S\":\"\")+\" K\"}Object.defineProperty(y.config,\"minDate\",{get:function(){return y.config._minDate},set:se(\"min\")}),Object.defineProperty(y.config,\"maxDate\",{get:function(){return y.config._maxDate},set:se(\"max\")});var d=function(e){return function(t){y.config[\"min\"===e?\"_minTime\":\"_maxTime\"]=y.parseDate(t,\"H:i:S\")}};Object.defineProperty(y.config,\"minTime\",{get:function(){return y.config._minTime},set:d(\"min\")}),Object.defineProperty(y.config,\"maxTime\",{get:function(){return y.config._maxTime},set:d(\"max\")}),\"time\"===i.mode&&(y.config.noCalendar=!0,y.config.enableTime=!0);Object.assign(y.config,a,i);for(var c=0;c<t.length;c++)y.config[t[c]]=!0===y.config[t[c]]||\"true\"===y.config[t[c]];n.filter(function(e){return void 0!==y.config[e]}).forEach(function(e){y.config[e]=l(y.config[e]||[]).map(S)}),y.isMobile=!y.config.disableMobile&&!y.config.inline&&\"single\"===y.config.mode&&!y.config.disable.length&&!y.config.enable&&!y.config.weekNumbers&&/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);for(c=0;c<y.config.plugins.length;c++){var f=y.config.plugins[c](y)||{};for(var p in f)n.indexOf(p)>-1?y.config[p]=l(f[p]).map(S).concat(y.config[p]):void 0===i[p]&&(y.config[p]=f[p])}i.altInputClass||(y.config.altInputClass=le().className+\" \"+y.config.altInputClass);_e(\"onParseConfig\")}(),ue(),function(){if(y.input=le(),!y.input)return void y.config.errorHandler(new Error(\"Invalid input element specified\"));y.input._type=y.input.type,y.input.type=\"text\",y.input.classList.add(\"flatpickr-input\"),y._input=y.input,y.config.altInput&&(y.altInput=d(y.input.nodeName,y.config.altInputClass),y._input=y.altInput,y.altInput.placeholder=y.input.placeholder,y.altInput.disabled=y.input.disabled,y.altInput.required=y.input.required,y.altInput.tabIndex=y.input.tabIndex,y.altInput.type=\"text\",y.input.setAttribute(\"type\",\"hidden\"),!y.config.static&&y.input.parentNode&&y.input.parentNode.insertBefore(y.altInput,y.input.nextSibling));y.config.allowInput||y._input.setAttribute(\"readonly\",\"readonly\");ge()}(),function(){y.selectedDates=[],y.now=y.parseDate(y.config.now)||new Date;var e=y.config.defaultDate||(\"INPUT\"!==y.input.nodeName&&\"TEXTAREA\"!==y.input.nodeName||!y.input.placeholder||y.input.value!==y.input.placeholder?y.input.value:null);e&&me(e,y.config.dateFormat);y._initialDate=y.selectedDates.length>0?y.selectedDates[0]:y.config.minDate&&y.config.minDate.getTime()>y.now.getTime()?y.config.minDate:y.config.maxDate&&y.config.maxDate.getTime()<y.now.getTime()?y.config.maxDate:y.now,y.currentYear=y._initialDate.getFullYear(),y.currentMonth=y._initialDate.getMonth(),y.selectedDates.length>0&&(y.latestSelectedDateObj=y.selectedDates[0]);void 0!==y.config.minTime&&(y.config.minTime=y.parseDate(y.config.minTime,\"H:i\"));void 0!==y.config.maxTime&&(y.config.maxTime=y.parseDate(y.config.maxTime,\"H:i\"));y.minDateHasTime=!!y.config.minDate&&(y.config.minDate.getHours()>0||y.config.minDate.getMinutes()>0||y.config.minDate.getSeconds()>0),y.maxDateHasTime=!!y.config.maxDate&&(y.config.maxDate.getHours()>0||y.config.maxDate.getMinutes()>0||y.config.maxDate.getSeconds()>0)}(),y.utils={getDaysInMonth:function(e,t){return void 0===e&&(e=y.currentMonth),void 0===t&&(t=y.currentYear),1===e&&(t%4==0&&t%100!=0||t%400==0)?29:y.l10n.daysInMonth[e]}},y.isMobile||function(){var e=window.document.createDocumentFragment();if(y.calendarContainer=d(\"div\",\"flatpickr-calendar\"),y.calendarContainer.tabIndex=-1,!y.config.noCalendar){if(e.appendChild((y.monthNav=d(\"div\",\"flatpickr-months\"),y.yearElements=[],y.monthElements=[],y.prevMonthNav=d(\"span\",\"flatpickr-prev-month\"),y.prevMonthNav.innerHTML=y.config.prevArrow,y.nextMonthNav=d(\"span\",\"flatpickr-next-month\"),y.nextMonthNav.innerHTML=y.config.nextArrow,G(),Object.defineProperty(y,\"_hidePrevMonthArrow\",{get:function(){return y.__hidePrevMonthArrow},set:function(e){y.__hidePrevMonthArrow!==e&&(u(y.prevMonthNav,\"flatpickr-disabled\",e),y.__hidePrevMonthArrow=e)}}),Object.defineProperty(y,\"_hideNextMonthArrow\",{get:function(){return y.__hideNextMonthArrow},set:function(e){y.__hideNextMonthArrow!==e&&(u(y.nextMonthNav,\"flatpickr-disabled\",e),y.__hideNextMonthArrow=e)}}),y.currentYearElement=y.yearElements[0],Me(),y.monthNav)),y.innerContainer=d(\"div\",\"flatpickr-innerContainer\"),y.config.weekNumbers){var t=function(){y.calendarContainer.classList.add(\"hasWeeks\");var e=d(\"div\",\"flatpickr-weekwrapper\");e.appendChild(d(\"span\",\"flatpickr-weekday\",y.l10n.weekAbbreviation));var t=d(\"div\",\"flatpickr-weeks\");return e.appendChild(t),{weekWrapper:e,weekNumbers:t}}(),n=t.weekWrapper,r=t.weekNumbers;y.innerContainer.appendChild(n),y.weekNumbers=r,y.weekWrapper=n}y.rContainer=d(\"div\",\"flatpickr-rContainer\"),y.rContainer.appendChild(J()),y.daysContainer||(y.daysContainer=d(\"div\",\"flatpickr-days\"),y.daysContainer.tabIndex=-1),U(),y.rContainer.appendChild(y.daysContainer),y.innerContainer.appendChild(y.rContainer),e.appendChild(y.innerContainer)}y.config.enableTime&&e.appendChild(function(){y.calendarContainer.classList.add(\"hasTime\"),y.config.noCalendar&&y.calendarContainer.classList.add(\"noCalendar\");var e=x(y.config);y.timeContainer=d(\"div\",\"flatpickr-time\"),y.timeContainer.tabIndex=-1;var t=d(\"span\",\"flatpickr-time-separator\",\":\"),n=p(\"flatpickr-hour\",{\"aria-label\":y.l10n.hourAriaLabel});y.hourElement=n.getElementsByTagName(\"input\")[0];var r=p(\"flatpickr-minute\",{\"aria-label\":y.l10n.minuteAriaLabel});y.minuteElement=r.getElementsByTagName(\"input\")[0],y.hourElement.tabIndex=y.minuteElement.tabIndex=-1,y.hourElement.value=a(y.latestSelectedDateObj?y.latestSelectedDateObj.getHours():y.config.time_24hr?e.hours:function(e){switch(e%24){case 0:case 12:return 12;default:return e%12}}(e.hours)),y.minuteElement.value=a(y.latestSelectedDateObj?y.latestSelectedDateObj.getMinutes():e.minutes),y.hourElement.setAttribute(\"step\",y.config.hourIncrement.toString()),y.minuteElement.setAttribute(\"step\",y.config.minuteIncrement.toString()),y.hourElement.setAttribute(\"min\",y.config.time_24hr?\"0\":\"1\"),y.hourElement.setAttribute(\"max\",y.config.time_24hr?\"23\":\"12\"),y.hourElement.setAttribute(\"maxlength\",\"2\"),y.minuteElement.setAttribute(\"min\",\"0\"),y.minuteElement.setAttribute(\"max\",\"59\"),y.minuteElement.setAttribute(\"maxlength\",\"2\"),y.timeContainer.appendChild(n),y.timeContainer.appendChild(t),y.timeContainer.appendChild(r),y.config.time_24hr&&y.timeContainer.classList.add(\"time24hr\");if(y.config.enableSeconds){y.timeContainer.classList.add(\"hasSeconds\");var i=p(\"flatpickr-second\");y.secondElement=i.getElementsByTagName(\"input\")[0],y.secondElement.value=a(y.latestSelectedDateObj?y.latestSelectedDateObj.getSeconds():e.seconds),y.secondElement.setAttribute(\"step\",y.minuteElement.getAttribute(\"step\")),y.secondElement.setAttribute(\"min\",\"0\"),y.secondElement.setAttribute(\"max\",\"59\"),y.secondElement.setAttribute(\"maxlength\",\"2\"),y.timeContainer.appendChild(d(\"span\",\"flatpickr-time-separator\",\":\")),y.timeContainer.appendChild(i)}y.config.time_24hr||(y.amPM=d(\"span\",\"flatpickr-am-pm\",y.l10n.amPM[o((y.latestSelectedDateObj?y.hourElement.value:y.config.defaultHour)>11)]),y.amPM.title=y.l10n.toggleTitle,y.amPM.tabIndex=-1,y.timeContainer.appendChild(y.amPM));return y.timeContainer}());u(y.calendarContainer,\"rangeMode\",\"range\"===y.config.mode),u(y.calendarContainer,\"animate\",!0===y.config.animate),u(y.calendarContainer,\"multiMonth\",y.config.showMonths>1),y.calendarContainer.appendChild(e);var i=void 0!==y.config.appendTo&&void 0!==y.config.appendTo.nodeType;if((y.config.inline||y.config.static)&&(y.calendarContainer.classList.add(y.config.inline?\"inline\":\"static\"),y.config.inline&&(!i&&y.element.parentNode?y.element.parentNode.insertBefore(y.calendarContainer,y._input.nextSibling):void 0!==y.config.appendTo&&y.config.appendTo.appendChild(y.calendarContainer)),y.config.static)){var s=d(\"div\",\"flatpickr-wrapper\");y.element.parentNode&&y.element.parentNode.insertBefore(s,y.element),s.appendChild(y.element),y.altInput&&s.appendChild(y.altInput),s.appendChild(y.calendarContainer)}y.config.static||y.config.inline||(void 0!==y.config.appendTo?y.config.appendTo:window.document.body).appendChild(y.calendarContainer)}(),function(){y.config.wrap&&[\"open\",\"close\",\"toggle\",\"clear\"].forEach(function(e){Array.prototype.forEach.call(y.element.querySelectorAll(\"[data-\"+e+\"]\"),function(t){return P(t,\"click\",y[e])})});if(y.isMobile)return void function(){var e=y.config.enableTime?y.config.noCalendar?\"time\":\"datetime-local\":\"date\";y.mobileInput=d(\"input\",y.input.className+\" flatpickr-mobile\"),y.mobileInput.tabIndex=1,y.mobileInput.type=e,y.mobileInput.disabled=y.input.disabled,y.mobileInput.required=y.input.required,y.mobileInput.placeholder=y.input.placeholder,y.mobileFormatStr=\"datetime-local\"===e?\"Y-m-d\\\\TH:i:S\":\"date\"===e?\"Y-m-d\":\"H:i:S\",y.selectedDates.length>0&&(y.mobileInput.defaultValue=y.mobileInput.value=y.formatDate(y.selectedDates[0],y.mobileFormatStr));y.config.minDate&&(y.mobileInput.min=y.formatDate(y.config.minDate,\"Y-m-d\"));y.config.maxDate&&(y.mobileInput.max=y.formatDate(y.config.maxDate,\"Y-m-d\"));y.input.getAttribute(\"step\")&&(y.mobileInput.step=String(y.input.getAttribute(\"step\")));y.input.type=\"hidden\",void 0!==y.altInput&&(y.altInput.type=\"hidden\");try{y.input.parentNode&&y.input.parentNode.insertBefore(y.mobileInput,y.input.nextSibling)}catch(e){}P(y.mobileInput,\"change\",function(e){y.setDate(h(e).value,!1,y.mobileFormatStr),_e(\"onChange\"),_e(\"onClose\")})}();var e=s(oe,50);y._debouncedChange=s(H,300),y.daysContainer&&!/iPhone|iPad|iPod/i.test(navigator.userAgent)&&P(y.daysContainer,\"mouseover\",function(e){\"range\"===y.config.mode&&ae(h(e))});P(y._input,\"keydown\",ie),void 0!==y.calendarContainer&&P(y.calendarContainer,\"keydown\",ie);y.config.inline||y.config.static||P(window,\"resize\",e);void 0!==window.ontouchstart?P(window.document,\"touchstart\",Q):P(window.document,\"mousedown\",Q);P(window.document,\"focus\",Q,{capture:!0}),!0===y.config.clickOpens&&(P(y._input,\"focus\",y.open),P(y._input,\"click\",y.open));void 0!==y.daysContainer&&(P(y.monthNav,\"click\",Le),P(y.monthNav,[\"keyup\",\"increment\"],j),P(y.daysContainer,\"click\",pe));if(void 0!==y.timeContainer&&void 0!==y.minuteElement&&void 0!==y.hourElement){var t=function(e){return h(e).select()};P(y.timeContainer,[\"increment\"],Y),P(y.timeContainer,\"blur\",Y,{capture:!0}),P(y.timeContainer,\"click\",F),P([y.hourElement,y.minuteElement],[\"focus\",\"click\"],t),void 0!==y.secondElement&&P(y.secondElement,\"focus\",function(){return y.secondElement&&y.secondElement.select()}),void 0!==y.amPM&&P(y.amPM,\"click\",function(e){Y(e)})}y.config.allowInput&&P(y._input,\"blur\",re)}(),(y.selectedDates.length||y.config.noCalendar)&&(y.config.enableTime&&O(y.config.noCalendar?y.latestSelectedDateObj:void 0),ke(!1)),E();var t=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);!y.isMobile&&t&&de(),_e(\"onReady\")}(),y}function S(e,t){for(var n=Array.prototype.slice.call(e).filter(function(e){return e instanceof HTMLElement}),r=[],i=0;i<n.length;i++){var a=n[i];try{if(null!==a.getAttribute(\"data-fp-omit\"))continue;void 0!==a._flatpickr&&(a._flatpickr.destroy(),a._flatpickr=void 0),a._flatpickr=T(a,t||{}),r.push(a._flatpickr)}catch(e){console.error(e)}}return 1===r.length?r[0]:r}\"undefined\"!=typeof HTMLElement&&\"undefined\"!=typeof HTMLCollection&&\"undefined\"!=typeof NodeList&&(HTMLCollection.prototype.flatpickr=NodeList.prototype.flatpickr=function(e){return S(this,e)},HTMLElement.prototype.flatpickr=function(e){return S([this],e)});var D=function(e,t){return\"string\"==typeof e?S(window.document.querySelectorAll(e),t):e instanceof Node?S([e],t):S(e,t)};return D.defaultConfig={},D.l10ns={en:e({},i),default:e({},i)},D.localize=function(t){D.l10ns.default=e(e({},D.l10ns.default),t)},D.setDefaults=function(t){D.defaultConfig=e(e({},D.defaultConfig),t)},D.parseDate=M({}),D.formatDate=b({}),D.compareDates=w,\"undefined\"!=typeof jQuery&&void 0!==jQuery.fn&&(jQuery.fn.flatpickr=function(e){return S(this,e)}),Date.prototype.fp_incr=function(e){return new Date(this.getFullYear(),this.getMonth(),this.getDate()+(\"string\"==typeof e?parseInt(e,10):e))},\"undefined\"!=typeof window&&(window.flatpickr=D),D}),\n/*!\n * https://github.com/paulmillr/es6-shim\n * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com)\n *   and contributors,  MIT License\n * es6-shim: v0.35.4\n * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE\n * Details and documentation:\n * https://github.com/paulmillr/es6-shim/\n */\nfunction(e,t){\"function\"==typeof define&&define.amd?define(t):\"object\"==typeof exports?module.exports=t():e.returnExports=t()}(this,function(){\"use strict\";var e,t,n=Function.call.bind(Function.apply),r=Function.call.bind(Function.call),i=Array.isArray,a=Object.keys,o=function(e){try{return e(),!1}catch(e){return!0}},s=function(e){try{return e()}catch(e){return!1}},l=(e=o,function(){return!n(e,this,arguments)}),u=!!Object.defineProperty&&!o(function(){return Object.defineProperty({},\"x\",{get:function(){}})}),d=\"foo\"===function(){}.name,c=Function.call.bind(Array.prototype.forEach),f=Function.call.bind(Array.prototype.reduce),p=Function.call.bind(Array.prototype.filter),h=Function.call.bind(Array.prototype.some),m=function(e,t,n,r){!r&&t in e||(u?Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:!0,value:n}):e[t]=n)},v=function(e,t,n){c(a(t),function(r){var i=t[r];m(e,r,i,!!n)})},g=Function.call.bind(Object.prototype.toString),_=\"function\"==typeof/abc/?function(e){return\"function\"==typeof e&&\"[object Function]\"===g(e)}:function(e){return\"function\"==typeof e},y=function(e,t,n){if(!u)throw new TypeError(\"getters require true ES5 support\");Object.defineProperty(e,t,{configurable:!0,enumerable:!1,get:n})},b=function(e,t,n){if(!u)throw new TypeError(\"getters require true ES5 support\");var r=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(n,t,{configurable:r.configurable,enumerable:r.enumerable,get:function(){return e[t]},set:function(n){e[t]=n}})},M=function(e,t,n){if(u){var r=Object.getOwnPropertyDescriptor(e,t);r.value=n,Object.defineProperty(e,t,r)}else e[t]=n},w=function(e,t,n){u?Object.defineProperty(e,t,n):\"value\"in n&&(e[t]=n.value)},k=function(e,t){t&&_(t.toString)&&m(e,\"toString\",t.toString.bind(t),!0)},L=Object.create||function(e,t){var n=function(){};n.prototype=e;var r=new n;return void 0!==t&&a(t).forEach(function(e){w(r,e,t[e])}),r},x=function(e,t){return!!Object.setPrototypeOf&&s(function(){var n=function t(n){var r=new e(n);return Object.setPrototypeOf(r,t.prototype),r};return Object.setPrototypeOf(n,e),n.prototype=L(e.prototype,{constructor:{value:n}}),t(n)})},T=function(){if(\"undefined\"!=typeof self)return self;if(\"undefined\"!=typeof window)return window;if(\"undefined\"!=typeof global)return global;throw new Error(\"unable to locate global object\")}(),S=T.isFinite,D=Function.call.bind(String.prototype.indexOf),E=Function.apply.bind(Array.prototype.indexOf),Y=Function.call.bind(Array.prototype.concat),A=Function.call.bind(String.prototype.slice),O=Function.call.bind(Array.prototype.push),C=Function.apply.bind(Array.prototype.push),j=Function.call.bind(Array.prototype.join),P=Function.call.bind(Array.prototype.shift),H=Math.max,I=Math.min,F=Math.floor,N=Math.abs,R=Math.exp,V=Math.log,$=Math.sqrt,z=Function.call.bind(Object.prototype.hasOwnProperty),W=function(){},U=T.Map,B=U&&U.prototype.delete,q=U&&U.prototype.get,G=U&&U.prototype.has,J=U&&U.prototype.set,Z=T.Symbol||{},K=Z.species||\"@@species\",X=Number.isNaN||function(e){return e!=e},Q=Number.isFinite||function(e){return\"number\"==typeof e&&S(e)},ee=_(Math.sign)?Math.sign:function(e){var t=Number(e);return 0===t||X(t)?t:t<0?-1:1},te=function(e){var t=Number(e);return t<-1||X(t)?NaN:0===t||t===1/0?t:-1===t?-1/0:1+t-1==0?t:t*(V(1+t)/(1+t-1))},ne=function(e){return\"[object Arguments]\"===g(e)},re=ne(arguments)?ne:function(e){return null!==e&&\"object\"==typeof e&&\"number\"==typeof e.length&&e.length>=0&&\"[object Array]\"!==g(e)&&\"[object Function]\"===g(e.callee)},ie=function(e){return null===e||\"function\"!=typeof e&&\"object\"!=typeof e},ae=function(e){return\"[object String]\"===g(e)},oe=function(e){return\"[object RegExp]\"===g(e)},se=function(e){return\"function\"==typeof T.Symbol&&\"symbol\"==typeof e},le=function(e,t,n){var r=e[t];m(e,t,n,!0),k(e[t],r)},ue=\"function\"==typeof Z&&\"function\"==typeof Z.for&&se(Z()),de=se(Z.iterator)?Z.iterator:\"_es6-shim iterator_\";T.Set&&\"function\"==typeof(new T.Set)[\"@@iterator\"]&&(de=\"@@iterator\"),T.Reflect||m(T,\"Reflect\",{},!0);var ce,fe=T.Reflect,pe=String,he=\"undefined\"!=typeof document&&document?document.all:null,me=null==he?function(e){return null==e}:function(e){return null==e&&e!==he},ve={Call:function(e,t){var r=arguments.length>2?arguments[2]:[];if(!ve.IsCallable(e))throw new TypeError(e+\" is not a function\");return n(e,t,r)},RequireObjectCoercible:function(e,t){if(me(e))throw new TypeError(t||\"Cannot call method on \"+e);return e},TypeIsObject:function(e){return null!=e&&!0!==e&&!1!==e&&(\"function\"==typeof e||\"object\"==typeof e||e===he)},ToObject:function(e,t){return Object(ve.RequireObjectCoercible(e,t))},IsCallable:_,IsConstructor:function(e){return ve.IsCallable(e)},ToInt32:function(e){return ve.ToNumber(e)|0},ToUint32:function(e){return ve.ToNumber(e)>>>0},ToNumber:function(e){if(ue&&\"[object Symbol]\"===g(e))throw new TypeError(\"Cannot convert a Symbol value to a number\");return+e},ToInteger:function(e){var t=ve.ToNumber(e);return X(t)?0:0!==t&&Q(t)?(t>0?1:-1)*F(N(t)):t},ToLength:function(e){var t=ve.ToInteger(e);return t<=0?0:t>Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:t},SameValue:function(e,t){return e===t?0!==e||1/e==1/t:X(e)&&X(t)},SameValueZero:function(e,t){return e===t||X(e)&&X(t)},GetIterator:function(e){if(re(e))return new t(e,\"value\");var n=ve.GetMethod(e,de);if(!ve.IsCallable(n))throw new TypeError(\"value is not an iterable\");var r=ve.Call(n,e);if(!ve.TypeIsObject(r))throw new TypeError(\"bad iterator\");return r},GetMethod:function(e,t){var n=ve.ToObject(e)[t];if(!me(n)){if(!ve.IsCallable(n))throw new TypeError(\"Method not callable: \"+t);return n}},IteratorComplete:function(e){return!!e.done},IteratorClose:function(e,t){var n=ve.GetMethod(e,\"return\");if(void 0!==n){var r,i;try{r=ve.Call(n,e)}catch(e){i=e}if(!t){if(i)throw i;if(!ve.TypeIsObject(r))throw new TypeError(\"Iterator's return method returned a non-object.\")}}},IteratorNext:function(e){var t=arguments.length>1?e.next(arguments[1]):e.next();if(!ve.TypeIsObject(t))throw new TypeError(\"bad iterator\");return t},IteratorStep:function(e){var t=ve.IteratorNext(e);return!ve.IteratorComplete(t)&&t},Construct:function(e,t,n,r){var i=void 0===n?e:n;if(!r&&fe.construct)return fe.construct(e,t,i);var a=i.prototype;ve.TypeIsObject(a)||(a=Object.prototype);var o=L(a),s=ve.Call(e,o,t);return ve.TypeIsObject(s)?s:o},SpeciesConstructor:function(e,t){var n=e.constructor;if(void 0===n)return t;if(!ve.TypeIsObject(n))throw new TypeError(\"Bad constructor\");var r=n[K];if(me(r))return t;if(!ve.IsConstructor(r))throw new TypeError(\"Bad @@species\");return r},CreateHTML:function(e,t,n,r){var i=ve.ToString(e),a=\"<\"+t;\"\"!==n&&(a+=\" \"+n+'=\"'+ve.ToString(r).replace(/\"/g,\"&quot;\")+'\"');return a+\">\"+i+\"</\"+t+\">\"},IsRegExp:function(e){if(!ve.TypeIsObject(e))return!1;var t=e[Z.match];return void 0!==t?!!t:oe(e)},ToString:function(e){if(ue&&\"[object Symbol]\"===g(e))throw new TypeError(\"Cannot convert a Symbol value to a number\");return pe(e)}};if(u&&ue){var ge=function(e){if(se(Z[e]))return Z[e];var t=Z.for(\"Symbol.\"+e);return Object.defineProperty(Z,e,{configurable:!1,enumerable:!1,writable:!1,value:t}),t};if(!se(Z.search)){var _e=ge(\"search\"),ye=String.prototype.search;m(RegExp.prototype,_e,function(e){return ve.Call(ye,e,[this])});le(String.prototype,\"search\",function(e){var t=ve.RequireObjectCoercible(this);if(!me(e)){var n=ve.GetMethod(e,_e);if(void 0!==n)return ve.Call(n,e,[t])}return ve.Call(ye,t,[ve.ToString(e)])})}if(!se(Z.replace)){var be=ge(\"replace\"),Me=String.prototype.replace;m(RegExp.prototype,be,function(e,t){return ve.Call(Me,e,[this,t])});le(String.prototype,\"replace\",function(e,t){var n=ve.RequireObjectCoercible(this);if(!me(e)){var r=ve.GetMethod(e,be);if(void 0!==r)return ve.Call(r,e,[n,t])}return ve.Call(Me,n,[ve.ToString(e),t])})}if(!se(Z.split)){var we=ge(\"split\"),ke=String.prototype.split;m(RegExp.prototype,we,function(e,t){return ve.Call(ke,e,[this,t])});le(String.prototype,\"split\",function(e,t){var n=ve.RequireObjectCoercible(this);if(!me(e)){var r=ve.GetMethod(e,we);if(void 0!==r)return ve.Call(r,e,[n,t])}return ve.Call(ke,n,[ve.ToString(e),t])})}var Le=se(Z.match),xe=Le&&((ce={})[Z.match]=function(){return 42},42!==\"a\".match(ce));if(!Le||xe){var Te=ge(\"match\"),Se=String.prototype.match;m(RegExp.prototype,Te,function(e){return ve.Call(Se,e,[this])});le(String.prototype,\"match\",function(e){var t=ve.RequireObjectCoercible(this);if(!me(e)){var n=ve.GetMethod(e,Te);if(void 0!==n)return ve.Call(n,e,[t])}return ve.Call(Se,t,[ve.ToString(e)])})}}var De=function(e,t,n){k(t,e),Object.setPrototypeOf&&Object.setPrototypeOf(e,t),u?c(Object.getOwnPropertyNames(e),function(r){r in W||n[r]||b(e,r,t)}):c(Object.keys(e),function(r){r in W||n[r]||(t[r]=e[r])}),t.prototype=e.prototype,M(e.prototype,\"constructor\",t)},Ee=function(){return this},Ye=function(e){u&&!z(e,K)&&y(e,K,Ee)},Ae=function(e,t){var n=t||function(){return this};m(e,de,n),!e[de]&&se(de)&&(e[de]=n)},Oe=function(e,t,n){if(function(e,t,n){u?Object.defineProperty(e,t,{configurable:!0,enumerable:!0,writable:!0,value:n}):e[t]=n}(e,t,n),!ve.SameValue(e[t],n))throw new TypeError(\"property is nonconfigurable\")},Ce=function(e,t,n,r){if(!ve.TypeIsObject(e))throw new TypeError(\"Constructor requires `new`: \"+t.name);var i=t.prototype;ve.TypeIsObject(i)||(i=n);var a=L(i);for(var o in r)if(z(r,o)){var s=r[o];m(a,o,s,!0)}return a};if(String.fromCodePoint&&1!==String.fromCodePoint.length){var je=String.fromCodePoint;le(String,\"fromCodePoint\",function(e){return ve.Call(je,this,arguments)})}var Pe={fromCodePoint:function(e){for(var t,n=[],r=0,i=arguments.length;r<i;r++){if(t=Number(arguments[r]),!ve.SameValue(t,ve.ToInteger(t))||t<0||t>1114111)throw new RangeError(\"Invalid code point \"+t);t<65536?O(n,String.fromCharCode(t)):(t-=65536,O(n,String.fromCharCode(55296+(t>>10))),O(n,String.fromCharCode(t%1024+56320)))}return j(n,\"\")},raw:function(e){var t=ve.ToObject(e,\"bad template\"),n=ve.ToObject(t.raw,\"bad raw value\"),r=n.length,i=ve.ToLength(r);if(i<=0)return\"\";for(var a,o,s,l,u=[],d=0;d<i&&(a=ve.ToString(d),s=ve.ToString(n[a]),O(u,s),!(d+1>=i));)o=d+1<arguments.length?arguments[d+1]:\"\",l=ve.ToString(o),O(u,l),d+=1;return j(u,\"\")}};String.raw&&\"xy\"!==String.raw({raw:{0:\"x\",1:\"y\",length:2}})&&le(String,\"raw\",Pe.raw),v(String,Pe);var He=function e(t,n){if(n<1)return\"\";if(n%2)return e(t,n-1)+t;var r=e(t,n/2);return r+r},Ie=1/0,Fe={repeat:function(e){var t=ve.ToString(ve.RequireObjectCoercible(this)),n=ve.ToInteger(e);if(n<0||n>=Ie)throw new RangeError(\"repeat count must be less than infinity and not overflow maximum string size\");return He(t,n)},startsWith:function(e){var t=ve.ToString(ve.RequireObjectCoercible(this));if(ve.IsRegExp(e))throw new TypeError('Cannot call method \"startsWith\" with a regex');var n,r=ve.ToString(e);arguments.length>1&&(n=arguments[1]);var i=H(ve.ToInteger(n),0);return A(t,i,i+r.length)===r},endsWith:function(e){var t=ve.ToString(ve.RequireObjectCoercible(this));if(ve.IsRegExp(e))throw new TypeError('Cannot call method \"endsWith\" with a regex');var n,r=ve.ToString(e),i=t.length;arguments.length>1&&(n=arguments[1]);var a=void 0===n?i:ve.ToInteger(n),o=I(H(a,0),i);return A(t,o-r.length,o)===r},includes:function(e){if(ve.IsRegExp(e))throw new TypeError('\"includes\" does not accept a RegExp');var t,n=ve.ToString(e);return arguments.length>1&&(t=arguments[1]),-1!==D(this,n,t)},codePointAt:function(e){var t=ve.ToString(ve.RequireObjectCoercible(this)),n=ve.ToInteger(e),r=t.length;if(n>=0&&n<r){var i=t.charCodeAt(n);if(i<55296||i>56319||n+1===r)return i;var a=t.charCodeAt(n+1);return a<56320||a>57343?i:1024*(i-55296)+(a-56320)+65536}}};if(String.prototype.includes&&!1!==\"a\".includes(\"a\",1/0)&&le(String.prototype,\"includes\",Fe.includes),String.prototype.startsWith&&String.prototype.endsWith){var Ne=o(function(){return\"/a/\".startsWith(/a/)}),Re=s(function(){return!1===\"abc\".startsWith(\"a\",1/0)});Ne&&Re||(le(String.prototype,\"startsWith\",Fe.startsWith),le(String.prototype,\"endsWith\",Fe.endsWith))}ue&&(s(function(){var e=/a/;return e[Z.match]=!1,\"/a/\".startsWith(e)})||le(String.prototype,\"startsWith\",Fe.startsWith),s(function(){var e=/a/;return e[Z.match]=!1,\"/a/\".endsWith(e)})||le(String.prototype,\"endsWith\",Fe.endsWith),s(function(){var e=/a/;return e[Z.match]=!1,\"/a/\".includes(e)})||le(String.prototype,\"includes\",Fe.includes));v(String.prototype,Fe);var Ve=[\"\\t\\n\\v\\f\\r   ᠎    \",\"         　\\u2028\",\"\\u2029\\ufeff\"].join(\"\"),$e=new RegExp(\"(^[\"+Ve+\"]+)|([\"+Ve+\"]+$)\",\"g\"),ze=function(){return ve.ToString(ve.RequireObjectCoercible(this)).replace($e,\"\")},We=[\"\",\"​\",\"￾\"].join(\"\"),Ue=new RegExp(\"[\"+We+\"]\",\"g\"),Be=/^[-+]0x[0-9a-f]+$/i,qe=We.trim().length!==We.length;m(String.prototype,\"trim\",ze,qe);var Ge=function(e){return{value:e,done:0===arguments.length}},Je=function(e){ve.RequireObjectCoercible(e),m(this,\"_s\",ve.ToString(e)),m(this,\"_i\",0)};Je.prototype.next=function(){var e=this._s,t=this._i;if(void 0===e||t>=e.length)return this._s=void 0,Ge();var n,r,i=e.charCodeAt(t);return r=i<55296||i>56319||t+1===e.length||(n=e.charCodeAt(t+1))<56320||n>57343?1:2,this._i=t+r,Ge(e.substr(t,r))},Ae(Je.prototype),Ae(String.prototype,function(){return new Je(this)});var Ze={from:function(e){var t,n,i,a,o,s,l=this;if(arguments.length>1&&(t=arguments[1]),void 0===t)n=!1;else{if(!ve.IsCallable(t))throw new TypeError(\"Array.from: when provided, the second argument must be a function\");arguments.length>2&&(i=arguments[2]),n=!0}if(void 0!==(re(e)||ve.GetMethod(e,de))){o=ve.IsConstructor(l)?Object(new l):[];var u,d,c=ve.GetIterator(e);for(s=0;!1!==(u=ve.IteratorStep(c));){d=u.value;try{n&&(d=void 0===i?t(d,s):r(t,i,d,s)),o[s]=d}catch(e){throw ve.IteratorClose(c,!0),e}s+=1}a=s}else{var f,p=ve.ToObject(e);for(a=ve.ToLength(p.length),o=ve.IsConstructor(l)?Object(new l(a)):new Array(a),s=0;s<a;++s)f=p[s],n&&(f=void 0===i?t(f,s):r(t,i,f,s)),Oe(o,s,f)}return o.length=a,o},of:function(){for(var e=arguments.length,t=this,n=i(t)||!ve.IsCallable(t)?new Array(e):ve.Construct(t,[e]),r=0;r<e;++r)Oe(n,r,arguments[r]);return n.length=e,n}};v(Array,Ze),Ye(Array),v((t=function(e,t){m(this,\"i\",0),m(this,\"array\",e),m(this,\"kind\",t)}).prototype,{next:function(){var e=this.i,n=this.array;if(!(this instanceof t))throw new TypeError(\"Not an ArrayIterator\");if(void 0!==n&&e<ve.ToLength(n.length)){var r,i=this.kind;return\"key\"===i?r=e:\"value\"===i?r=n[e]:\"entry\"===i&&(r=[e,n[e]]),this.i=e+1,Ge(r)}return this.array=void 0,Ge()}}),Ae(t.prototype),Array.of===Ze.of||function(){var e=function(e){this.length=e};e.prototype=[];var t=Array.of.apply(e,[1,2]);return t instanceof e&&2===t.length}()||le(Array,\"of\",Ze.of);var Ke={copyWithin:function(e,t){var n,r=ve.ToObject(this),i=ve.ToLength(r.length),a=ve.ToInteger(e),o=ve.ToInteger(t),s=a<0?H(i+a,0):I(a,i),l=o<0?H(i+o,0):I(o,i);arguments.length>2&&(n=arguments[2]);var u=void 0===n?i:ve.ToInteger(n),d=u<0?H(i+u,0):I(u,i),c=I(d-l,i-s),f=1;for(l<s&&s<l+c&&(f=-1,l+=c-1,s+=c-1);c>0;)l in r?r[s]=r[l]:delete r[s],l+=f,s+=f,c-=1;return r},fill:function(e){var t,n;arguments.length>1&&(t=arguments[1]),arguments.length>2&&(n=arguments[2]);var r=ve.ToObject(this),i=ve.ToLength(r.length);t=ve.ToInteger(void 0===t?0:t);for(var a=(n=ve.ToInteger(void 0===n?i:n))<0?i+n:n,o=t<0?H(i+t,0):I(t,i);o<i&&o<a;++o)r[o]=e;return r},find:function(e){var t=ve.ToObject(this),n=ve.ToLength(t.length);if(!ve.IsCallable(e))throw new TypeError(\"Array#find: predicate must be a function\");for(var i,a=arguments.length>1?arguments[1]:null,o=0;o<n;o++)if(i=t[o],a){if(r(e,a,i,o,t))return i}else if(e(i,o,t))return i},findIndex:function(e){var t=ve.ToObject(this),n=ve.ToLength(t.length);if(!ve.IsCallable(e))throw new TypeError(\"Array#findIndex: predicate must be a function\");for(var i=arguments.length>1?arguments[1]:null,a=0;a<n;a++)if(i){if(r(e,i,t[a],a,t))return a}else if(e(t[a],a,t))return a;return-1},keys:function(){return new t(this,\"key\")},values:function(){return new t(this,\"value\")},entries:function(){return new t(this,\"entry\")}};if(Array.prototype.keys&&!ve.IsCallable([1].keys().next)&&delete Array.prototype.keys,Array.prototype.entries&&!ve.IsCallable([1].entries().next)&&delete Array.prototype.entries,Array.prototype.keys&&Array.prototype.entries&&!Array.prototype.values&&Array.prototype[de]&&(v(Array.prototype,{values:Array.prototype[de]}),se(Z.unscopables)&&(Array.prototype[Z.unscopables].values=!0)),d&&Array.prototype.values&&\"values\"!==Array.prototype.values.name){var Xe=Array.prototype.values;le(Array.prototype,\"values\",function(){return ve.Call(Xe,this,arguments)}),m(Array.prototype,de,Array.prototype.values,!0)}if(v(Array.prototype,Ke),1/[!0].indexOf(!0,-0)<0&&m(Array.prototype,\"indexOf\",function(e){var t=E(this,arguments);return 0===t&&1/t<0?0:t},!0),Ae(Array.prototype,function(){return this.values()}),Object.getPrototypeOf){var Qe=Object.getPrototypeOf([].values());Qe&&Ae(Qe)}var et,tt=s(function(){return 0===Array.from({length:-1}).length}),nt=1===(et=Array.from([0].entries())).length&&i(et[0])&&0===et[0][0]&&0===et[0][1];if(tt&&nt||le(Array,\"from\",Ze.from),!s(function(){return Array.from([0],void 0)})){var rt=Array.from;le(Array,\"from\",function(e){return arguments.length>1&&void 0!==arguments[1]?ve.Call(rt,this,arguments):r(rt,this,e)})}var it=-(Math.pow(2,32)-1),at=function(e,t){var n={length:it};return n[t?(n.length>>>0)-1:0]=!0,s(function(){return r(e,n,function(){throw new RangeError(\"should not reach here\")},[]),!0})};if(!at(Array.prototype.forEach)){var ot=Array.prototype.forEach;le(Array.prototype,\"forEach\",function(e){return ve.Call(ot,this.length>=0?this:[],arguments)})}if(!at(Array.prototype.map)){var st=Array.prototype.map;le(Array.prototype,\"map\",function(e){return ve.Call(st,this.length>=0?this:[],arguments)})}if(!at(Array.prototype.filter)){var lt=Array.prototype.filter;le(Array.prototype,\"filter\",function(e){return ve.Call(lt,this.length>=0?this:[],arguments)})}if(!at(Array.prototype.some)){var ut=Array.prototype.some;le(Array.prototype,\"some\",function(e){return ve.Call(ut,this.length>=0?this:[],arguments)})}if(!at(Array.prototype.every)){var dt=Array.prototype.every;le(Array.prototype,\"every\",function(e){return ve.Call(dt,this.length>=0?this:[],arguments)})}if(!at(Array.prototype.reduce)){var ct=Array.prototype.reduce;le(Array.prototype,\"reduce\",function(e){return ve.Call(ct,this.length>=0?this:[],arguments)})}if(!at(Array.prototype.reduceRight,!0)){var ft=Array.prototype.reduceRight;le(Array.prototype,\"reduceRight\",function(e){return ve.Call(ft,this.length>=0?this:[],arguments)})}var pt=8!==Number(\"0o10\"),ht=2!==Number(\"0b10\"),mt=h(We,function(e){return 0===Number(e+0+e)});if(pt||ht||mt){var vt=Number,gt=/^0b[01]+$/i,_t=/^0o[0-7]+$/i,yt=gt.test.bind(gt),bt=_t.test.bind(_t),Mt=Ue.test.bind(Ue),wt=Be.test.bind(Be),kt=function(){var e=function(t){var n;\"string\"==typeof(n=arguments.length>0?ie(t)?t:function(e){var t;if(\"function\"==typeof e.valueOf&&(t=e.valueOf(),ie(t)))return t;if(\"function\"==typeof e.toString&&(t=e.toString(),ie(t)))return t;throw new TypeError(\"No default value\")}(t):0)&&(n=ve.Call(ze,n),yt(n)?n=parseInt(A(n,2),2):bt(n)?n=parseInt(A(n,2),8):(Mt(n)||wt(n))&&(n=NaN));var r=this,i=s(function(){return vt.prototype.valueOf.call(r),!0});return r instanceof e&&!i?new vt(n):vt(n)};return e}();De(vt,kt,{}),v(kt,{NaN:vt.NaN,MAX_VALUE:vt.MAX_VALUE,MIN_VALUE:vt.MIN_VALUE,NEGATIVE_INFINITY:vt.NEGATIVE_INFINITY,POSITIVE_INFINITY:vt.POSITIVE_INFINITY}),Number=kt,M(T,\"Number\",kt)}var Lt=Math.pow(2,53)-1;v(Number,{MAX_SAFE_INTEGER:Lt,MIN_SAFE_INTEGER:-Lt,EPSILON:2220446049250313e-31,parseInt:T.parseInt,parseFloat:T.parseFloat,isFinite:Q,isInteger:function(e){return Q(e)&&ve.ToInteger(e)===e},isSafeInteger:function(e){return Number.isInteger(e)&&N(e)<=Number.MAX_SAFE_INTEGER},isNaN:X}),m(Number,\"parseInt\",T.parseInt,Number.parseInt!==T.parseInt),1===[,1].find(function(){return!0})&&le(Array.prototype,\"find\",Ke.find),0!==[,1].findIndex(function(){return!0})&&le(Array.prototype,\"findIndex\",Ke.findIndex);var xt,Tt,St,Dt=Function.bind.call(Function.bind,Object.prototype.propertyIsEnumerable),Et=function(e,t){u&&Dt(e,t)&&Object.defineProperty(e,t,{enumerable:!1})},Yt=function(){for(var e=Number(this),t=arguments.length,n=t-e,r=new Array(n<0?0:n),i=e;i<t;++i)r[i-e]=arguments[i];return r},At=function(e){return function(t,n){return t[n]=e[n],t}},Ot=function(e,t){var n,r=a(Object(t));return ve.IsCallable(Object.getOwnPropertySymbols)&&(n=p(Object.getOwnPropertySymbols(Object(t)),Dt(t))),f(Y(r,n||[]),At(t),e)},Ct={assign:function(e,t){var n=ve.ToObject(e,\"Cannot convert undefined or null to object\");return f(ve.Call(Yt,1,arguments),Ot,n)},is:function(e,t){return ve.SameValue(e,t)}};if(Object.assign&&Object.preventExtensions&&function(){var e=Object.preventExtensions({1:2});try{Object.assign(e,\"xy\")}catch(t){return\"y\"===e[1]}}()&&le(Object,\"assign\",Ct.assign),v(Object,Ct),u){var jt={setPrototypeOf:function(e){var t,n=function(e,n){return function(e,t){if(!ve.TypeIsObject(e))throw new TypeError(\"cannot set prototype on a non-object\");if(null!==t&&!ve.TypeIsObject(t))throw new TypeError(\"can only set prototype to an object or null\"+t)}(e,n),r(t,e,n),e};try{t=e.getOwnPropertyDescriptor(e.prototype,\"__proto__\").set,r(t,{},null)}catch(r){if(e.prototype!=={}.__proto__)return;t=function(e){this.__proto__=e},n.polyfill=n(n({},null),e.prototype)instanceof e}return n}(Object)};v(Object,jt)}if(Object.setPrototypeOf&&Object.getPrototypeOf&&null!==Object.getPrototypeOf(Object.setPrototypeOf({},null))&&null===Object.getPrototypeOf(Object.create(null))&&(xt=Object.create(null),Tt=Object.getPrototypeOf,St=Object.setPrototypeOf,Object.getPrototypeOf=function(e){var t=Tt(e);return t===xt?null:t},Object.setPrototypeOf=function(e,t){return St(e,null===t?xt:t)},Object.setPrototypeOf.polyfill=!1),!!o(function(){return Object.keys(\"foo\")})){var Pt=Object.keys;le(Object,\"keys\",function(e){return Pt(ve.ToObject(e))}),a=Object.keys}if(o(function(){return Object.keys(/a/g)})){var Ht=Object.keys;le(Object,\"keys\",function(e){if(oe(e)){var t=[];for(var n in e)z(e,n)&&O(t,n);return t}return Ht(e)}),a=Object.keys}if(Object.getOwnPropertyNames&&!!o(function(){return Object.getOwnPropertyNames(\"foo\")})){var It=\"object\"==typeof window?Object.getOwnPropertyNames(window):[],Ft=Object.getOwnPropertyNames;le(Object,\"getOwnPropertyNames\",function(e){var t=ve.ToObject(e);if(\"[object Window]\"===g(t))try{return Ft(t)}catch(e){return Y([],It)}return Ft(t)})}if(Object.getOwnPropertyDescriptor&&!!o(function(){return Object.getOwnPropertyDescriptor(\"foo\",\"bar\")})){var Nt=Object.getOwnPropertyDescriptor;le(Object,\"getOwnPropertyDescriptor\",function(e,t){return Nt(ve.ToObject(e),t)})}if(Object.seal&&!!o(function(){return Object.seal(\"foo\")})){var Rt=Object.seal;le(Object,\"seal\",function(e){return ve.TypeIsObject(e)?Rt(e):e})}if(Object.isSealed&&!!o(function(){return Object.isSealed(\"foo\")})){var Vt=Object.isSealed;le(Object,\"isSealed\",function(e){return!ve.TypeIsObject(e)||Vt(e)})}if(Object.freeze&&!!o(function(){return Object.freeze(\"foo\")})){var $t=Object.freeze;le(Object,\"freeze\",function(e){return ve.TypeIsObject(e)?$t(e):e})}if(Object.isFrozen&&!!o(function(){return Object.isFrozen(\"foo\")})){var zt=Object.isFrozen;le(Object,\"isFrozen\",function(e){return!ve.TypeIsObject(e)||zt(e)})}if(Object.preventExtensions&&!!o(function(){return Object.preventExtensions(\"foo\")})){var Wt=Object.preventExtensions;le(Object,\"preventExtensions\",function(e){return ve.TypeIsObject(e)?Wt(e):e})}if(Object.isExtensible&&!!o(function(){return Object.isExtensible(\"foo\")})){var Ut=Object.isExtensible;le(Object,\"isExtensible\",function(e){return!!ve.TypeIsObject(e)&&Ut(e)})}if(Object.getPrototypeOf&&!!o(function(){return Object.getPrototypeOf(\"foo\")})){var Bt=Object.getPrototypeOf;le(Object,\"getPrototypeOf\",function(e){return Bt(ve.ToObject(e))})}var qt,Gt=u&&((qt=Object.getOwnPropertyDescriptor(RegExp.prototype,\"flags\"))&&ve.IsCallable(qt.get));if(u&&!Gt){y(RegExp.prototype,\"flags\",function(){if(!ve.TypeIsObject(this))throw new TypeError(\"Method called on incompatible type: must be an object.\");var e=\"\";return this.global&&(e+=\"g\"),this.ignoreCase&&(e+=\"i\"),this.multiline&&(e+=\"m\"),this.unicode&&(e+=\"u\"),this.sticky&&(e+=\"y\"),e})}var Jt,Zt=u&&s(function(){return\"/a/i\"===String(new RegExp(/a/g,\"i\"))}),Kt=ue&&u&&((Jt=/./)[Z.match]=!1,RegExp(Jt)===Jt),Xt=s(function(){return\"/abc/\"===RegExp.prototype.toString.call({source:\"abc\"})}),Qt=Xt&&s(function(){return\"/a/b\"===RegExp.prototype.toString.call({source:\"a\",flags:\"b\"})});if(!Xt||!Qt){var en=RegExp.prototype.toString;m(RegExp.prototype,\"toString\",function(){var e=ve.RequireObjectCoercible(this);return oe(e)?r(en,e):\"/\"+pe(e.source)+\"/\"+pe(e.flags)},!0),k(RegExp.prototype.toString,en),RegExp.prototype.toString.prototype=void 0}if(u&&(!Zt||Kt)){var tn=Object.getOwnPropertyDescriptor(RegExp.prototype,\"flags\").get,nn=Object.getOwnPropertyDescriptor(RegExp.prototype,\"source\")||{},rn=ve.IsCallable(nn.get)?nn.get:function(){return this.source},an=RegExp,on=function e(t,n){var r=ve.IsRegExp(t);return this instanceof e||!r||void 0!==n||t.constructor!==e?oe(t)?new e(ve.Call(rn,t),void 0===n?ve.Call(tn,t):n):(r&&(t.source,void 0===n&&t.flags),new an(t,n)):t};De(an,on,{$input:!0}),RegExp=on,M(T,\"RegExp\",on)}if(u){var sn={input:\"$_\",lastMatch:\"$&\",lastParen:\"$+\",leftContext:\"$`\",rightContext:\"$'\"};c(a(sn),function(e){e in RegExp&&!(sn[e]in RegExp)&&y(RegExp,sn[e],function(){return RegExp[e]})})}Ye(RegExp);var ln=1/Number.EPSILON,un=Math.pow(2,-23),dn=Math.pow(2,127)*(2-un),cn=Math.pow(2,-126),fn=Math.E,pn=Math.LOG2E,hn=Math.LOG10E,mn=Number.prototype.clz;delete Number.prototype.clz;var vn={acosh:function(e){var t=Number(e);if(X(t)||e<1)return NaN;if(1===t)return 0;if(t===1/0)return t;var n=1/(t*t);if(t<2)return te(t-1+$(1-n)*t);var r=t/2;return te(r+$(1-n)*r-1)+1/pn},asinh:function(e){var t=Number(e);if(0===t||!S(t))return t;var n=N(t),r=n*n,i=ee(t);return n<1?i*te(n+r/($(r+1)+1)):i*(te(n/2+$(1+1/r)*n/2-1)+1/pn)},atanh:function(e){var t=Number(e);if(0===t)return t;if(-1===t)return-1/0;if(1===t)return 1/0;if(X(t)||t<-1||t>1)return NaN;var n=N(t);return ee(t)*te(2*n/(1-n))/2},cbrt:function(e){var t=Number(e);if(0===t)return t;var n,r=t<0;return r&&(t=-t),n=t===1/0?1/0:(t/((n=R(V(t)/3))*n)+2*n)/3,r?-n:n},clz32:function(e){var t=Number(e),n=ve.ToUint32(t);return 0===n?32:mn?ve.Call(mn,n):31-F(V(n+.5)*pn)},cosh:function(e){var t=Number(e);if(0===t)return 1;if(X(t))return NaN;if(!S(t))return 1/0;var n=R(N(t)-1);return(n+1/(n*fn*fn))*(fn/2)},expm1:function(e){var t=Number(e);if(t===-1/0)return-1;if(!S(t)||0===t)return t;if(N(t)>.5)return R(t)-1;for(var n=t,r=0,i=1;r+n!==r;)r+=n,n*=t/(i+=1);return r},hypot:function(e,t){for(var n=0,r=0,i=0;i<arguments.length;++i){var a=N(Number(arguments[i]));r<a?(n*=r/a*(r/a),n+=1,r=a):n+=a>0?a/r*(a/r):a}return r===1/0?1/0:r*$(n)},log2:function(e){return V(e)*pn},log10:function(e){return V(e)*hn},log1p:te,sign:ee,sinh:function(e){var t=Number(e);if(!S(t)||0===t)return t;var n=N(t);if(n<1){var r=Math.expm1(n);return ee(t)*r*(1+1/(r+1))/2}var i=R(n-1);return ee(t)*(i-1/(i*fn*fn))*(fn/2)},tanh:function(e){var t=Number(e);return X(t)||0===t?t:t>=20?1:t<=-20?-1:(Math.expm1(t)-Math.expm1(-t))/(R(t)+R(-t))},trunc:function(e){var t=Number(e);return t<0?-F(-t):F(t)},imul:function(e,t){var n=ve.ToUint32(e),r=ve.ToUint32(t),i=65535&n,a=65535&r;return i*a+((n>>>16&65535)*a+i*(r>>>16&65535)<<16>>>0)|0},fround:function(e){var t=Number(e);if(0===t||t===1/0||t===-1/0||X(t))return t;var n=ee(t),r=N(t);if(r<cn)return n*(r/cn/un+ln-ln)*cn*un;var i=(1+un/Number.EPSILON)*r,a=i-(i-r);return a>dn||X(a)?n*(1/0):n*a}},gn=function(e,t,n){return N(1-e/t)/Number.EPSILON<(n||8)};v(Math,vn),m(Math,\"sinh\",vn.sinh,Math.sinh(710)===1/0),m(Math,\"cosh\",vn.cosh,Math.cosh(710)===1/0),m(Math,\"log1p\",vn.log1p,-1e-17!==Math.log1p(-1e-17)),m(Math,\"asinh\",vn.asinh,Math.asinh(-1e7)!==-Math.asinh(1e7)),m(Math,\"asinh\",vn.asinh,Math.asinh(1e300)===1/0),m(Math,\"atanh\",vn.atanh,0===Math.atanh(1e-300)),m(Math,\"tanh\",vn.tanh,-2e-17!==Math.tanh(-2e-17)),m(Math,\"acosh\",vn.acosh,Math.acosh(Number.MAX_VALUE)===1/0),m(Math,\"acosh\",vn.acosh,!gn(Math.acosh(1+Number.EPSILON),Math.sqrt(2*Number.EPSILON))),m(Math,\"cbrt\",vn.cbrt,!gn(Math.cbrt(1e-300),1e-100)),m(Math,\"sinh\",vn.sinh,-2e-17!==Math.sinh(-2e-17));var _n=Math.expm1(10);m(Math,\"expm1\",vn.expm1,_n>22025.465794806718||_n<22025.465794806718),m(Math,\"hypot\",vn.hypot,Math.hypot(1/0,NaN)!==1/0);var yn=Math.round,bn=0===Math.round(.5-Number.EPSILON/4)&&1===Math.round(Number.EPSILON/3.99-.5),Mn=[ln+1,2*ln-1].every(function(e){return Math.round(e)===e});m(Math,\"round\",function(e){var t=F(e);return e-t<.5?t:-1===t?-0:t+1},!bn||!Mn),k(Math.round,yn);var wn=Math.imul;-5!==Math.imul(4294967295,5)&&(Math.imul=vn.imul,k(Math.imul,wn)),2!==Math.imul.length&&le(Math,\"imul\",function(e,t){return ve.Call(wn,Math,arguments)});var kn,Ln,xn=function(){var e=T.setTimeout;if(\"function\"==typeof e||\"object\"==typeof e){ve.IsPromise=function(e){return!!ve.TypeIsObject(e)&&void 0!==e._promise};var t,n=function(e){if(!ve.IsConstructor(e))throw new TypeError(\"Bad promise constructor\");var t=this;if(t.resolve=void 0,t.reject=void 0,t.promise=new e(function(e,n){if(void 0!==t.resolve||void 0!==t.reject)throw new TypeError(\"Bad Promise implementation!\");t.resolve=e,t.reject=n}),!ve.IsCallable(t.resolve)||!ve.IsCallable(t.reject))throw new TypeError(\"Bad promise constructor\")};\"undefined\"!=typeof window&&ve.IsCallable(window.postMessage)&&(t=function(){var e=[],t=\"zero-timeout-message\";return window.addEventListener(\"message\",function(n){if(n.source===window&&n.data===t){if(n.stopPropagation(),0===e.length)return;P(e)()}},!0),function(n){O(e,n),window.postMessage(t,\"*\")}});var i,a,o,s,l=ve.IsCallable(T.setImmediate)?T.setImmediate:\"object\"==typeof process&&process.nextTick?process.nextTick:(i=T.Promise,(a=i&&i.resolve&&i.resolve())&&function(e){return a.then(e)}||(ve.IsCallable(t)?t():function(t){e(t,0)})),u=function(e){return e},d=function(e){throw e},c={},f=function(e,t,n){l(function(){p(e,t,n)})},p=function(e,t,n){var r,i;if(t===c)return e(n);try{r=e(n),i=t.resolve}catch(e){r=e,i=t.reject}i(r)},h=function(e,t){var n=e._promise,r=n.reactionLength;if(r>0&&(f(n.fulfillReactionHandler0,n.reactionCapability0,t),n.fulfillReactionHandler0=void 0,n.rejectReactions0=void 0,n.reactionCapability0=void 0,r>1))for(var i=1,a=0;i<r;i++,a+=3)f(n[a+0],n[a+2],t),e[a+0]=void 0,e[a+1]=void 0,e[a+2]=void 0;n.result=t,n.state=1,n.reactionLength=0},m=function(e,t){var n=e._promise,r=n.reactionLength;if(r>0&&(f(n.rejectReactionHandler0,n.reactionCapability0,t),n.fulfillReactionHandler0=void 0,n.rejectReactions0=void 0,n.reactionCapability0=void 0,r>1))for(var i=1,a=0;i<r;i++,a+=3)f(n[a+1],n[a+2],t),e[a+0]=void 0,e[a+1]=void 0,e[a+2]=void 0;n.result=t,n.state=2,n.reactionLength=0},g=function(e){var t=!1;return{resolve:function(n){var r;if(!t){if(t=!0,n===e)return m(e,new TypeError(\"Self resolution\"));if(!ve.TypeIsObject(n))return h(e,n);try{r=n.then}catch(t){return m(e,t)}if(!ve.IsCallable(r))return h(e,n);l(function(){y(e,n,r)})}},reject:function(n){if(!t)return t=!0,m(e,n)}}},_=function(e,t,n,i){e===s?r(e,t,n,i,c):r(e,t,n,i)},y=function(e,t,n){var r=g(e),i=r.resolve,a=r.reject;try{_(n,t,i,a)}catch(e){a(e)}},b=function(){var e=function(t){if(!(this instanceof e))throw new TypeError('Constructor Promise requires \"new\"');if(this&&this._promise)throw new TypeError(\"Bad construction\");if(!ve.IsCallable(t))throw new TypeError(\"not a valid resolver\");var n=Ce(this,e,o,{_promise:{result:void 0,state:0,reactionLength:0,fulfillReactionHandler0:void 0,rejectReactionHandler0:void 0,reactionCapability0:void 0}}),r=g(n),i=r.reject;try{t(r.resolve,i)}catch(e){i(e)}return n};return e}();o=b.prototype;var M=function(e,t,n,r){var i=!1;return function(a){i||(i=!0,t[e]=a,0===--r.count&&(0,n.resolve)(t))}};return v(b,{all:function(e){var t=this;if(!ve.TypeIsObject(t))throw new TypeError(\"Promise is not object\");var r,i,a=new n(t);try{return function(e,t,n){for(var r,i,a=e.iterator,o=[],s={count:1},l=0;;){try{if(!1===(r=ve.IteratorStep(a))){e.done=!0;break}i=r.value}catch(t){throw e.done=!0,t}o[l]=void 0;var u=t.resolve(i),d=M(l,o,n,s);s.count+=1,_(u.then,u,d,n.reject),l+=1}return 0===--s.count&&(0,n.resolve)(o),n.promise}(i={iterator:r=ve.GetIterator(e),done:!1},t,a)}catch(e){var o=e;if(i&&!i.done)try{ve.IteratorClose(r,!0)}catch(e){o=e}return(0,a.reject)(o),a.promise}},race:function(e){var t=this;if(!ve.TypeIsObject(t))throw new TypeError(\"Promise is not object\");var r,i,a=new n(t);try{return function(e,t,n){for(var r,i,a,o=e.iterator;;){try{if(!1===(r=ve.IteratorStep(o))){e.done=!0;break}i=r.value}catch(t){throw e.done=!0,t}a=t.resolve(i),_(a.then,a,n.resolve,n.reject)}return n.promise}(i={iterator:r=ve.GetIterator(e),done:!1},t,a)}catch(e){var o=e;if(i&&!i.done)try{ve.IteratorClose(r,!0)}catch(e){o=e}return(0,a.reject)(o),a.promise}},reject:function(e){if(!ve.TypeIsObject(this))throw new TypeError(\"Bad promise constructor\");var t=new n(this);return(0,t.reject)(e),t.promise},resolve:function(e){var t=this;if(!ve.TypeIsObject(t))throw new TypeError(\"Bad promise constructor\");if(ve.IsPromise(e)&&e.constructor===t)return e;var r=new n(t);return(0,r.resolve)(e),r.promise}}),v(o,{catch:function(e){return this.then(null,e)},then:function(e,t){var r=this;if(!ve.IsPromise(r))throw new TypeError(\"not a promise\");var i,a=ve.SpeciesConstructor(r,b);i=arguments.length>2&&arguments[2]===c&&a===b?c:new n(a);var o,s=ve.IsCallable(e)?e:u,l=ve.IsCallable(t)?t:d,p=r._promise;if(0===p.state){if(0===p.reactionLength)p.fulfillReactionHandler0=s,p.rejectReactionHandler0=l,p.reactionCapability0=i;else{var h=3*(p.reactionLength-1);p[h+0]=s,p[h+1]=l,p[h+2]=i}p.reactionLength+=1}else if(1===p.state)o=p.result,f(s,i,o);else{if(2!==p.state)throw new TypeError(\"unexpected Promise state\");o=p.result,f(l,i,o)}return i.promise}}),c=new n(b),s=o.then,b}}();if(T.Promise&&(delete T.Promise.accept,delete T.Promise.defer,delete T.Promise.prototype.chain),\"function\"==typeof xn){v(T,{Promise:xn});var Tn=x(T.Promise,function(e){return e.resolve(42).then(function(){})instanceof e}),Sn=!o(function(){return T.Promise.reject(42).then(null,5).then(null,W)}),Dn=o(function(){return T.Promise.call(3,W)}),En=function(e){var t=e.resolve(5);t.constructor={};var n=e.resolve(t);try{n.then(null,W).then(null,W)}catch(e){return!0}return t===n}(T.Promise),Yn=u&&(kn=0,Ln=Object.defineProperty({},\"then\",{get:function(){kn+=1}}),Promise.resolve(Ln),1===kn),An=function e(t){var n=new Promise(t);t(3,function(){}),this.then=n.then,this.constructor=e};An.prototype=Promise.prototype,An.all=Promise.all;var On=s(function(){return!!An.all([1,2])});if(Tn&&Sn&&Dn&&!En&&Yn&&!On||(Promise=xn,le(T,\"Promise\",xn)),1!==Promise.all.length){var Cn=Promise.all;le(Promise,\"all\",function(e){return ve.Call(Cn,this,arguments)})}if(1!==Promise.race.length){var jn=Promise.race;le(Promise,\"race\",function(e){return ve.Call(jn,this,arguments)})}if(1!==Promise.resolve.length){var Pn=Promise.resolve;le(Promise,\"resolve\",function(e){return ve.Call(Pn,this,arguments)})}if(1!==Promise.reject.length){var Hn=Promise.reject;le(Promise,\"reject\",function(e){return ve.Call(Hn,this,arguments)})}Et(Promise,\"all\"),Et(Promise,\"race\"),Et(Promise,\"resolve\"),Et(Promise,\"reject\"),Ye(Promise)}var In,Fn,Nn=function(e){var t=a(f(e,function(e,t){return e[t]=!0,e},{}));return e.join(\":\")===t.join(\":\")},Rn=Nn([\"z\",\"a\",\"bb\"]),Vn=Nn([\"z\",1,\"a\",\"3\",2]);if(u){var $n=function(e,t){return t||Rn?me(e)?\"^\"+ve.ToString(e):\"string\"==typeof e?\"$\"+e:\"number\"==typeof e?Vn?e:\"n\"+e:\"boolean\"==typeof e?\"b\"+e:null:null},zn=function(){return Object.create?Object.create(null):{}},Wn=function(e,t,n){if(i(n)||ae(n))c(n,function(e){if(!ve.TypeIsObject(e))throw new TypeError(\"Iterator value \"+e+\" is not an entry object\");t.set(e[0],e[1])});else if(n instanceof e)r(e.prototype.forEach,n,function(e,n){t.set(n,e)});else{var a,o;if(!me(n)){if(o=t.set,!ve.IsCallable(o))throw new TypeError(\"bad map\");a=ve.GetIterator(n)}if(void 0!==a)for(;;){var s=ve.IteratorStep(a);if(!1===s)break;var l=s.value;try{if(!ve.TypeIsObject(l))throw new TypeError(\"Iterator value \"+l+\" is not an entry object\");r(o,t,l[0],l[1])}catch(e){throw ve.IteratorClose(a,!0),e}}}},Un=function(e,t,n){if(i(n)||ae(n))c(n,function(e){t.add(e)});else if(n instanceof e)r(e.prototype.forEach,n,function(e){t.add(e)});else{var a,o;if(!me(n)){if(o=t.add,!ve.IsCallable(o))throw new TypeError(\"bad set\");a=ve.GetIterator(n)}if(void 0!==a)for(;;){var s=ve.IteratorStep(a);if(!1===s)break;var l=s.value;try{r(o,t,l)}catch(e){throw ve.IteratorClose(a,!0),e}}}},Bn={Map:function(){var e={},t=function(e,t){this.key=e,this.value=t,this.next=null,this.prev=null};t.prototype.isRemoved=function(){return this.key===e};var n,i=function(e,t){if(!ve.TypeIsObject(e)||!function(e){return!!e._es6map}(e))throw new TypeError(\"Method Map.prototype.\"+t+\" called on incompatible receiver \"+ve.ToString(e))},a=function(e,t){i(e,\"[[MapIterator]]\"),m(this,\"head\",e._head),m(this,\"i\",this.head),m(this,\"kind\",t)};Ae(a.prototype={isMapIterator:!0,next:function(){if(!this.isMapIterator)throw new TypeError(\"Not a MapIterator\");var e,t=this.i,n=this.kind,r=this.head;if(void 0===this.i)return Ge();for(;t.isRemoved()&&t!==r;)t=t.prev;for(;t.next!==r;)if(!(t=t.next).isRemoved())return e=\"key\"===n?t.key:\"value\"===n?t.value:[t.key,t.value],this.i=t,Ge(e);return this.i=void 0,Ge()}});var o=function e(){if(!(this instanceof e))throw new TypeError('Constructor Map requires \"new\"');if(this&&this._es6map)throw new TypeError(\"Bad construction\");var r=Ce(this,e,n,{_es6map:!0,_head:null,_map:U?new U:null,_size:0,_storage:zn()}),i=new t(null,null);return i.next=i.prev=i,r._head=i,arguments.length>0&&Wn(e,r,arguments[0]),r};return y(n=o.prototype,\"size\",function(){if(void 0===this._size)throw new TypeError(\"size method called on incompatible Map\");return this._size}),v(n,{get:function(e){var t;i(this,\"get\");var n=$n(e,!0);if(null!==n)return(t=this._storage[n])?t.value:void 0;if(this._map)return(t=q.call(this._map,e))?t.value:void 0;for(var r=this._head,a=r;(a=a.next)!==r;)if(ve.SameValueZero(a.key,e))return a.value},has:function(e){i(this,\"has\");var t=$n(e,!0);if(null!==t)return void 0!==this._storage[t];if(this._map)return G.call(this._map,e);for(var n=this._head,r=n;(r=r.next)!==n;)if(ve.SameValueZero(r.key,e))return!0;return!1},set:function(e,n){i(this,\"set\");var r,a=this._head,o=a,s=$n(e,!0);if(null!==s){if(void 0!==this._storage[s])return this._storage[s].value=n,this;r=this._storage[s]=new t(e,n),o=a.prev}else this._map&&(G.call(this._map,e)?q.call(this._map,e).value=n:(r=new t(e,n),J.call(this._map,e,r),o=a.prev));for(;(o=o.next)!==a;)if(ve.SameValueZero(o.key,e))return o.value=n,this;return r=r||new t(e,n),ve.SameValue(-0,e)&&(r.key=0),r.next=this._head,r.prev=this._head.prev,r.prev.next=r,r.next.prev=r,this._size+=1,this},delete:function(t){i(this,\"delete\");var n=this._head,r=n,a=$n(t,!0);if(null!==a){if(void 0===this._storage[a])return!1;r=this._storage[a].prev,delete this._storage[a]}else if(this._map){if(!G.call(this._map,t))return!1;r=q.call(this._map,t).prev,B.call(this._map,t)}for(;(r=r.next)!==n;)if(ve.SameValueZero(r.key,t))return r.key=e,r.value=e,r.prev.next=r.next,r.next.prev=r.prev,this._size-=1,!0;return!1},clear:function(){i(this,\"clear\"),this._map=U?new U:null,this._size=0,this._storage=zn();for(var t=this._head,n=t,r=n.next;(n=r)!==t;)n.key=e,n.value=e,r=n.next,n.next=n.prev=t;t.next=t.prev=t},keys:function(){return i(this,\"keys\"),new a(this,\"key\")},values:function(){return i(this,\"values\"),new a(this,\"value\")},entries:function(){return i(this,\"entries\"),new a(this,\"key+value\")},forEach:function(e){i(this,\"forEach\");for(var t=arguments.length>1?arguments[1]:null,n=this.entries(),a=n.next();!a.done;a=n.next())t?r(e,t,a.value[1],a.value[0],this):e(a.value[1],a.value[0],this)}}),Ae(n,n.entries),o}(),Set:function(){var e,t=function(e,t){if(!ve.TypeIsObject(e)||!function(e){return e._es6set&&void 0!==e._storage}(e))throw new TypeError(\"Set.prototype.\"+t+\" called on incompatible receiver \"+ve.ToString(e))},n=function t(){if(!(this instanceof t))throw new TypeError('Constructor Set requires \"new\"');if(this&&this._es6set)throw new TypeError(\"Bad construction\");var n=Ce(this,t,e,{_es6set:!0,\"[[SetData]]\":null,_storage:zn()});if(!n._es6set)throw new TypeError(\"bad set\");return arguments.length>0&&Un(t,n,arguments[0]),n};e=n.prototype;var i=function(e){if(!e[\"[[SetData]]\"]){var t=new Bn.Map;e[\"[[SetData]]\"]=t,c(a(e._storage),function(e){var n=function(e){var t=e;if(\"^null\"===t)return null;if(\"^undefined\"!==t){var n=t.charAt(0);return\"$\"===n?A(t,1):\"n\"===n?+A(t,1):\"b\"===n?\"btrue\"===t:+t}}(e);t.set(n,n)}),e[\"[[SetData]]\"]=t}e._storage=null};y(n.prototype,\"size\",function(){return t(this,\"size\"),this._storage?a(this._storage).length:(i(this),this[\"[[SetData]]\"].size)}),v(n.prototype,{has:function(e){var n;return t(this,\"has\"),this._storage&&null!==(n=$n(e))?!!this._storage[n]:(i(this),this[\"[[SetData]]\"].has(e))},add:function(e){var n;return t(this,\"add\"),this._storage&&null!==(n=$n(e))?(this._storage[n]=!0,this):(i(this),this[\"[[SetData]]\"].set(e,e),this)},delete:function(e){var n;if(t(this,\"delete\"),this._storage&&null!==(n=$n(e))){var r=z(this._storage,n);return delete this._storage[n]&&r}return i(this),this[\"[[SetData]]\"].delete(e)},clear:function(){t(this,\"clear\"),this._storage&&(this._storage=zn()),this[\"[[SetData]]\"]&&this[\"[[SetData]]\"].clear()},values:function(){return t(this,\"values\"),i(this),new o(this[\"[[SetData]]\"].values())},entries:function(){return t(this,\"entries\"),i(this),new o(this[\"[[SetData]]\"].entries())},forEach:function(e){t(this,\"forEach\");var n=arguments.length>1?arguments[1]:null,a=this;i(a),this[\"[[SetData]]\"].forEach(function(t,i){n?r(e,n,i,i,a):e(i,i,a)})}}),m(n.prototype,\"keys\",n.prototype.values,!0),Ae(n.prototype,n.prototype.values);var o=function(e){m(this,\"it\",e)};return o.prototype={isSetIterator:!0,next:function(){if(!this.isSetIterator)throw new TypeError(\"Not a SetIterator\");return this.it.next()}},Ae(o.prototype),n}()};if(T.Set&&!Set.prototype.delete&&Set.prototype.remove&&Set.prototype.items&&Set.prototype.map&&Array.isArray((new Set).keys)&&(T.Set=Bn.Set),T.Map||T.Set){s(function(){return 2===new Map([[1,2]]).get(1)})||(T.Map=function e(){if(!(this instanceof e))throw new TypeError('Constructor Map requires \"new\"');var t=new U;return arguments.length>0&&Wn(e,t,arguments[0]),delete t.constructor,Object.setPrototypeOf(t,T.Map.prototype),t},T.Map.prototype=L(U.prototype),m(T.Map.prototype,\"constructor\",T.Map,!0),k(T.Map,U));var qn=new Map,Gn=((Fn=new Map([[1,0],[2,0],[3,0],[4,0]])).set(-0,Fn),Fn.get(0)===Fn&&Fn.get(-0)===Fn&&Fn.has(0)&&Fn.has(-0)),Jn=qn.set(1,2)===qn;Gn&&Jn||le(Map.prototype,\"set\",function(e,t){return r(J,this,0===e?0:e,t),this}),Gn||(v(Map.prototype,{get:function(e){return r(q,this,0===e?0:e)},has:function(e){return r(G,this,0===e?0:e)}},!0),k(Map.prototype.get,q),k(Map.prototype.has,G));var Zn=new Set,Kn=Set.prototype.delete&&Set.prototype.add&&Set.prototype.has&&((In=Zn).delete(0),In.add(-0),!In.has(0)),Xn=Zn.add(1)===Zn;if(!Kn||!Xn){var Qn=Set.prototype.add;Set.prototype.add=function(e){return r(Qn,this,0===e?0:e),this},k(Set.prototype.add,Qn)}if(!Kn){var er=Set.prototype.has;Set.prototype.has=function(e){return r(er,this,0===e?0:e)},k(Set.prototype.has,er);var tr=Set.prototype.delete;Set.prototype.delete=function(e){return r(tr,this,0===e?0:e)},k(Set.prototype.delete,tr)}var nr=x(T.Map,function(e){var t=new e([]);return t.set(42,42),t instanceof e}),rr=Object.setPrototypeOf&&!nr,ir=function(){try{return!(T.Map()instanceof T.Map)}catch(e){return e instanceof TypeError}}();0===T.Map.length&&!rr&&ir||(T.Map=function e(){if(!(this instanceof e))throw new TypeError('Constructor Map requires \"new\"');var t=new U;return arguments.length>0&&Wn(e,t,arguments[0]),delete t.constructor,Object.setPrototypeOf(t,e.prototype),t},T.Map.prototype=U.prototype,m(T.Map.prototype,\"constructor\",T.Map,!0),k(T.Map,U));var ar=x(T.Set,function(e){var t=new e([]);return t.add(42,42),t instanceof e}),or=Object.setPrototypeOf&&!ar,sr=function(){try{return!(T.Set()instanceof T.Set)}catch(e){return e instanceof TypeError}}();if(0!==T.Set.length||or||!sr){var lr=T.Set;T.Set=function e(){if(!(this instanceof e))throw new TypeError('Constructor Set requires \"new\"');var t=new lr;return arguments.length>0&&Un(e,t,arguments[0]),delete t.constructor,Object.setPrototypeOf(t,e.prototype),t},T.Set.prototype=lr.prototype,m(T.Set.prototype,\"constructor\",T.Set,!0),k(T.Set,lr)}var ur=new T.Map,dr=!s(function(){return ur.keys().next().done});if((\"function\"!=typeof T.Map.prototype.clear||0!==(new T.Set).size||0!==ur.size||\"function\"!=typeof T.Map.prototype.keys||\"function\"!=typeof T.Set.prototype.keys||\"function\"!=typeof T.Map.prototype.forEach||\"function\"!=typeof T.Set.prototype.forEach||l(T.Map)||l(T.Set)||\"function\"!=typeof ur.keys().next||dr||!nr)&&v(T,{Map:Bn.Map,Set:Bn.Set},!0),T.Set.prototype.keys!==T.Set.prototype.values&&m(T.Set.prototype,\"keys\",T.Set.prototype.values,!0),Ae(Object.getPrototypeOf((new T.Map).keys())),Ae(Object.getPrototypeOf((new T.Set).keys())),d&&\"has\"!==T.Set.prototype.has.name){var cr=T.Set.prototype.has;le(T.Set.prototype,\"has\",function(e){return r(cr,this,e)})}}v(T,Bn),Ye(T.Map),Ye(T.Set)}var fr=function(e){if(!ve.TypeIsObject(e))throw new TypeError(\"target must be an object\")},pr={apply:function(){return ve.Call(ve.Call,null,arguments)},construct:function(e,t){if(!ve.IsConstructor(e))throw new TypeError(\"First argument must be a constructor.\");var n=arguments.length>2?arguments[2]:e;if(!ve.IsConstructor(n))throw new TypeError(\"new.target must be a constructor.\");return ve.Construct(e,t,n,\"internal\")},deleteProperty:function(e,t){if(fr(e),u){var n=Object.getOwnPropertyDescriptor(e,t);if(n&&!n.configurable)return!1}return delete e[t]},has:function(e,t){return fr(e),t in e}};Object.getOwnPropertyNames&&Object.assign(pr,{ownKeys:function(e){fr(e);var t=Object.getOwnPropertyNames(e);return ve.IsCallable(Object.getOwnPropertySymbols)&&C(t,Object.getOwnPropertySymbols(e)),t}});var hr=function(e){return!o(e)};if(Object.preventExtensions&&Object.assign(pr,{isExtensible:function(e){return fr(e),Object.isExtensible(e)},preventExtensions:function(e){return fr(e),hr(function(){return Object.preventExtensions(e)})}}),u){var mr=function(e,t,n){var r=Object.getOwnPropertyDescriptor(e,t);if(!r){var i=Object.getPrototypeOf(e);if(null===i)return;return mr(i,t,n)}return\"value\"in r?r.value:r.get?ve.Call(r.get,n):void 0},vr=function(e,t,n,i){var a=Object.getOwnPropertyDescriptor(e,t);if(!a){var o=Object.getPrototypeOf(e);if(null!==o)return vr(o,t,n,i);a={value:void 0,writable:!0,enumerable:!0,configurable:!0}}return\"value\"in a?!!a.writable&&(!!ve.TypeIsObject(i)&&(Object.getOwnPropertyDescriptor(i,t)?fe.defineProperty(i,t,{value:n}):fe.defineProperty(i,t,{value:n,writable:!0,enumerable:!0,configurable:!0}))):!!a.set&&(r(a.set,i,n),!0)};Object.assign(pr,{defineProperty:function(e,t,n){return fr(e),hr(function(){return Object.defineProperty(e,t,n)})},getOwnPropertyDescriptor:function(e,t){return fr(e),Object.getOwnPropertyDescriptor(e,t)},get:function(e,t){return fr(e),mr(e,t,arguments.length>2?arguments[2]:e)},set:function(e,t,n){return fr(e),vr(e,t,n,arguments.length>3?arguments[3]:e)}})}if(Object.getPrototypeOf){var gr=Object.getPrototypeOf;pr.getPrototypeOf=function(e){return fr(e),gr(e)}}if(Object.setPrototypeOf&&pr.getPrototypeOf){Object.assign(pr,{setPrototypeOf:function(e,t){if(fr(e),null!==t&&!ve.TypeIsObject(t))throw new TypeError(\"proto must be an object or null\");return t===fe.getPrototypeOf(e)||!(fe.isExtensible&&!fe.isExtensible(e))&&(!function(e,t){for(var n=t;n;){if(e===n)return!0;n=pr.getPrototypeOf(n)}return!1}(e,t)&&(Object.setPrototypeOf(e,t),!0))}})}Object.keys(pr).forEach(function(e){!function(e,t){ve.IsCallable(T.Reflect[e])?s(function(){return T.Reflect[e](1),T.Reflect[e](NaN),T.Reflect[e](!0),!0})&&le(T.Reflect,e,t):m(T.Reflect,e,t)}(e,pr[e])});var _r=T.Reflect.getPrototypeOf;if(d&&_r&&\"getPrototypeOf\"!==_r.name&&le(T.Reflect,\"getPrototypeOf\",function(e){return r(_r,T.Reflect,e)}),T.Reflect.setPrototypeOf&&s(function(){return T.Reflect.setPrototypeOf(1,{}),!0})&&le(T.Reflect,\"setPrototypeOf\",pr.setPrototypeOf),T.Reflect.defineProperty&&(s(function(){var e=!T.Reflect.defineProperty(1,\"test\",{value:1}),t=\"function\"!=typeof Object.preventExtensions||!T.Reflect.defineProperty(Object.preventExtensions({}),\"test\",{});return e&&t})||le(T.Reflect,\"defineProperty\",pr.defineProperty)),T.Reflect.construct&&(s(function(){var e=function(){};return T.Reflect.construct(function(){},[],e)instanceof e})||le(T.Reflect,\"construct\",pr.construct)),\"Invalid Date\"!==String(new Date(NaN))){var yr=Date.prototype.toString;le(Date.prototype,\"toString\",function(){var e=+this;return e!=e?\"Invalid Date\":ve.Call(yr,this)})}var br={anchor:function(e){return ve.CreateHTML(this,\"a\",\"name\",e)},big:function(){return ve.CreateHTML(this,\"big\",\"\",\"\")},blink:function(){return ve.CreateHTML(this,\"blink\",\"\",\"\")},bold:function(){return ve.CreateHTML(this,\"b\",\"\",\"\")},fixed:function(){return ve.CreateHTML(this,\"tt\",\"\",\"\")},fontcolor:function(e){return ve.CreateHTML(this,\"font\",\"color\",e)},fontsize:function(e){return ve.CreateHTML(this,\"font\",\"size\",e)},italics:function(){return ve.CreateHTML(this,\"i\",\"\",\"\")},link:function(e){return ve.CreateHTML(this,\"a\",\"href\",e)},small:function(){return ve.CreateHTML(this,\"small\",\"\",\"\")},strike:function(){return ve.CreateHTML(this,\"strike\",\"\",\"\")},sub:function(){return ve.CreateHTML(this,\"sub\",\"\",\"\")},sup:function(){return ve.CreateHTML(this,\"sup\",\"\",\"\")}};c(Object.keys(br),function(e){var t=String.prototype[e],n=!1;if(ve.IsCallable(t)){var i=r(t,\"\",' \" '),a=Y([],i.match(/\"/g)).length;n=i!==i.toLowerCase()||a>2}else n=!0;n&&le(String.prototype,e,br[e])});var Mr=function(){if(!ue)return!1;var e=\"object\"==typeof JSON&&\"function\"==typeof JSON.stringify?JSON.stringify:null;if(!e)return!1;if(void 0!==e(Z()))return!0;if(\"[null]\"!==e([Z()]))return!0;var t={a:Z()};return t[Z()]=!0,\"{}\"!==e(t)}(),wr=s(function(){return!ue||\"{}\"===JSON.stringify(Object(Z()))&&\"[{}]\"===JSON.stringify([Object(Z())])});if(Mr||!wr){var kr=JSON.stringify;le(JSON,\"stringify\",function(e){if(\"symbol\"!=typeof e){var t;arguments.length>1&&(t=arguments[1]);var n=[e];if(i(t))n.push(t);else{var a=ve.IsCallable(t)?t:null;n.push(function(e,t){var n=a?r(a,this,e,t):t;if(\"symbol\"!=typeof n)return se(n)?At({})(n):n})}return arguments.length>2&&n.push(arguments[2]),kr.apply(this,n)}})}return T}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],t):t((e=\"undefined\"!=typeof globalThis?globalThis:e||self).FormValidation={})}(this,function(e){\"use strict\";var t,n={exports:{}},r={};n.exports=function(){if(t)return r;t=1;var e={luhn:function(e){for(var t=e.length,n=[[0,1,2,3,4,5,6,7,8,9],[0,2,4,6,8,1,3,5,7,9]],r=0,i=0;t--;)i+=n[r][parseInt(e.charAt(t),10)],r=1-r;return i%10==0&&i>0},mod11And10:function(e){for(var t=e.length,n=5,r=0;r<t;r++)n=(2*(n||10)%11+parseInt(e.charAt(r),10))%10;return 1===n},mod37And36:function(e,t){void 0===t&&(t=\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\");for(var n=e.length,r=t.length,i=Math.floor(r/2),a=0;a<n;a++)i=(2*(i||r)%(r+1)+t.indexOf(e.charAt(a)))%r;return 1===i},mod97And10:function(e){for(var t=function(e){return e.split(\"\").map(function(e){var t=e.charCodeAt(0);return t>=65&&t<=90?t-55:e}).join(\"\").split(\"\").map(function(e){return parseInt(e,10)})}(e),n=0,r=t.length,i=0;i<r-1;++i)n=10*(n+t[i])%97;return(n+=t[r-1])%97==1},verhoeff:function(e){for(var t=[[0,1,2,3,4,5,6,7,8,9],[1,2,3,4,0,6,7,8,9,5],[2,3,4,0,1,7,8,9,5,6],[3,4,0,1,2,8,9,5,6,7],[4,0,1,2,3,9,5,6,7,8],[5,9,8,7,6,0,4,3,2,1],[6,5,9,8,7,1,0,4,3,2],[7,6,5,9,8,2,1,0,4,3],[8,7,6,5,9,3,2,1,0,4],[9,8,7,6,5,4,3,2,1,0]],n=[[0,1,2,3,4,5,6,7,8,9],[1,5,7,6,2,8,3,0,9,4],[5,8,0,3,7,9,6,1,4,2],[8,9,1,6,0,4,3,5,2,7],[9,4,5,3,1,2,6,8,7,0],[4,2,8,6,5,7,3,9,0,1],[2,7,9,3,8,0,6,4,1,5],[7,0,4,6,9,1,3,2,5,8]],r=e.reverse(),i=0,a=0;a<r.length;a++)i=t[i][n[a%8][r[a]]];return 0===i}},n=function(){function e(e,t){this.fields={},this.elements={},this.ee={fns:{},clear:function(){this.fns={}},emit:function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];(this.fns[e]||[]).map(function(e){return e.apply(e,t)})},off:function(e,t){if(this.fns[e]){var n=this.fns[e].indexOf(t);n>=0&&this.fns[e].splice(n,1)}},on:function(e,t){(this.fns[e]=this.fns[e]||[]).push(t)}},this.filter={filters:{},add:function(e,t){(this.filters[e]=this.filters[e]||[]).push(t)},clear:function(){this.filters={}},execute:function(e,t,n){if(!this.filters[e]||!this.filters[e].length)return t;for(var r=t,i=this.filters[e],a=i.length,o=0;o<a;o++)r=i[o].apply(r,n);return r},remove:function(e,t){this.filters[e]&&(this.filters[e]=this.filters[e].filter(function(e){return e!==t}))}},this.plugins={},this.results=new Map,this.validators={},this.form=e,this.fields=t}return e.prototype.on=function(e,t){return this.ee.on(e,t),this},e.prototype.off=function(e,t){return this.ee.off(e,t),this},e.prototype.emit=function(e){for(var t,n=[],r=1;r<arguments.length;r++)n[r-1]=arguments[r];return(t=this.ee).emit.apply(t,function(e,t,n){if(n||2===arguments.length)for(var r,i=0,a=t.length;i<a;i++)!r&&i in t||(r||(r=Array.prototype.slice.call(t,0,i)),r[i]=t[i]);return e.concat(r||Array.prototype.slice.call(t))}([e],n,!1)),this},e.prototype.registerPlugin=function(e,t){if(this.plugins[e])throw new Error(\"The plguin \".concat(e,\" is registered\"));return t.setCore(this),t.install(),this.plugins[e]=t,this},e.prototype.deregisterPlugin=function(e){var t=this.plugins[e];return t&&t.uninstall(),delete this.plugins[e],this},e.prototype.enablePlugin=function(e){var t=this.plugins[e];return t&&t.enable(),this},e.prototype.disablePlugin=function(e){var t=this.plugins[e];return t&&t.disable(),this},e.prototype.isPluginEnabled=function(e){var t=this.plugins[e];return!!t&&t.isPluginEnabled()},e.prototype.registerValidator=function(e,t){if(this.validators[e])throw new Error(\"The validator \".concat(e,\" is registered\"));return this.validators[e]=t,this},e.prototype.registerFilter=function(e,t){return this.filter.add(e,t),this},e.prototype.deregisterFilter=function(e,t){return this.filter.remove(e,t),this},e.prototype.executeFilter=function(e,t,n){return this.filter.execute(e,t,n)},e.prototype.addField=function(e,t){var n=Object.assign({},{selector:\"\",validators:{}},t);return this.fields[e]=this.fields[e]?{selector:n.selector||this.fields[e].selector,validators:Object.assign({},this.fields[e].validators,n.validators)}:n,this.elements[e]=this.queryElements(e),this.emit(\"core.field.added\",{elements:this.elements[e],field:e,options:this.fields[e]}),this},e.prototype.removeField=function(e){if(!this.fields[e])throw new Error(\"The field \".concat(e,\" validators are not defined. Please ensure the field is added first\"));var t=this.elements[e],n=this.fields[e];return delete this.elements[e],delete this.fields[e],this.emit(\"core.field.removed\",{elements:t,field:e,options:n}),this},e.prototype.validate=function(){var e=this;return this.emit(\"core.form.validating\",{formValidation:this}),this.filter.execute(\"validate-pre\",Promise.resolve(),[]).then(function(){return Promise.all(Object.keys(e.fields).map(function(t){return e.validateField(t)})).then(function(t){switch(!0){case-1!==t.indexOf(\"Invalid\"):return e.emit(\"core.form.invalid\",{formValidation:e}),Promise.resolve(\"Invalid\");case-1!==t.indexOf(\"NotValidated\"):return e.emit(\"core.form.notvalidated\",{formValidation:e}),Promise.resolve(\"NotValidated\");default:return e.emit(\"core.form.valid\",{formValidation:e}),Promise.resolve(\"Valid\")}})})},e.prototype.validateField=function(e){var t=this,n=this.results.get(e);if(\"Valid\"===n||\"Invalid\"===n)return Promise.resolve(n);this.emit(\"core.field.validating\",e);var r=this.elements[e];if(0===r.length)return this.emit(\"core.field.valid\",e),Promise.resolve(\"Valid\");var i=r[0].getAttribute(\"type\");return\"radio\"===i||\"checkbox\"===i||1===r.length?this.validateElement(e,r[0]):Promise.all(r.map(function(n){return t.validateElement(e,n)})).then(function(n){switch(!0){case-1!==n.indexOf(\"Invalid\"):return t.emit(\"core.field.invalid\",e),t.results.set(e,\"Invalid\"),Promise.resolve(\"Invalid\");case-1!==n.indexOf(\"NotValidated\"):return t.emit(\"core.field.notvalidated\",e),t.results.delete(e),Promise.resolve(\"NotValidated\");default:return t.emit(\"core.field.valid\",e),t.results.set(e,\"Valid\"),Promise.resolve(\"Valid\")}})},e.prototype.validateElement=function(e,t){var n=this;this.results.delete(e);var r=this.elements[e];if(this.filter.execute(\"element-ignored\",!1,[e,t,r]))return this.emit(\"core.element.ignored\",{element:t,elements:r,field:e}),Promise.resolve(\"Ignored\");var i=this.fields[e].validators;this.emit(\"core.element.validating\",{element:t,elements:r,field:e});var a=Object.keys(i).map(function(r){return function(){return n.executeValidator(e,t,r,i[r])}});return this.waterfall(a).then(function(i){var a=-1===i.indexOf(\"Invalid\");n.emit(\"core.element.validated\",{element:t,elements:r,field:e,valid:a});var o=t.getAttribute(\"type\");return\"radio\"!==o&&\"checkbox\"!==o&&1!==r.length||n.emit(a?\"core.field.valid\":\"core.field.invalid\",e),Promise.resolve(a?\"Valid\":\"Invalid\")}).catch(function(i){return n.emit(\"core.element.notvalidated\",{element:t,elements:r,field:e}),Promise.resolve(i)})},e.prototype.executeValidator=function(e,t,n,r){var i=this,a=this.elements[e],o=this.filter.execute(\"validator-name\",n,[n,e]);if(r.message=this.filter.execute(\"validator-message\",r.message,[this.locale,e,o]),!this.validators[o]||!1===r.enabled)return this.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:this.normalizeResult(e,o,{valid:!0}),validator:o}),Promise.resolve(\"Valid\");var s=this.validators[o],l=this.getElementValue(e,t,o);if(!this.filter.execute(\"field-should-validate\",!0,[e,t,l,n]))return this.emit(\"core.validator.notvalidated\",{element:t,elements:a,field:e,validator:n}),Promise.resolve(\"NotValidated\");this.emit(\"core.validator.validating\",{element:t,elements:a,field:e,validator:n});var u=s().validate({element:t,elements:a,field:e,l10n:this.localization,options:r,value:l});if(\"function\"==typeof u.then)return u.then(function(r){var o=i.normalizeResult(e,n,r);return i.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:o,validator:n}),o.valid?\"Valid\":\"Invalid\"});var d=this.normalizeResult(e,n,u);return this.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:d,validator:n}),Promise.resolve(d.valid?\"Valid\":\"Invalid\")},e.prototype.getElementValue=function(e,t,n){var r=function(e,t,n,r){var i=(n.getAttribute(\"type\")||\"\").toLowerCase(),a=n.tagName.toLowerCase();if(\"textarea\"===a)return n.value;if(\"select\"===a){var o=n,s=o.selectedIndex;return s>=0?o.options.item(s).value:\"\"}if(\"input\"===a){if(\"radio\"===i||\"checkbox\"===i){var l=r.filter(function(e){return e.checked}).length;return 0===l?\"\":l+\"\"}return n.value}return\"\"}(this.form,0,t,this.elements[e]);return this.filter.execute(\"field-value\",r,[r,e,t,n])},e.prototype.getElements=function(e){return this.elements[e]},e.prototype.getFields=function(){return this.fields},e.prototype.getFormElement=function(){return this.form},e.prototype.getLocale=function(){return this.locale},e.prototype.getPlugin=function(e){return this.plugins[e]},e.prototype.updateFieldStatus=function(e,t,n){var r=this,i=this.elements[e],a=i[0].getAttribute(\"type\");if((\"radio\"===a||\"checkbox\"===a?[i[0]]:i).forEach(function(i){return r.updateElementStatus(e,i,t,n)}),n)\"Invalid\"===t&&(this.emit(\"core.field.invalid\",e),this.results.set(e,\"Invalid\"));else switch(t){case\"NotValidated\":this.emit(\"core.field.notvalidated\",e),this.results.delete(e);break;case\"Validating\":this.emit(\"core.field.validating\",e),this.results.delete(e);break;case\"Valid\":this.emit(\"core.field.valid\",e),this.results.set(e,\"Valid\");break;case\"Invalid\":this.emit(\"core.field.invalid\",e),this.results.set(e,\"Invalid\")}return this},e.prototype.updateElementStatus=function(e,t,n,r){var i=this,a=this.elements[e],o=this.fields[e].validators,s=r?[r]:Object.keys(o);switch(n){case\"NotValidated\":s.forEach(function(n){return i.emit(\"core.validator.notvalidated\",{element:t,elements:a,field:e,validator:n})}),this.emit(\"core.element.notvalidated\",{element:t,elements:a,field:e});break;case\"Validating\":s.forEach(function(n){return i.emit(\"core.validator.validating\",{element:t,elements:a,field:e,validator:n})}),this.emit(\"core.element.validating\",{element:t,elements:a,field:e});break;case\"Valid\":s.forEach(function(n){return i.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:{message:o[n].message,valid:!0},validator:n})}),this.emit(\"core.element.validated\",{element:t,elements:a,field:e,valid:!0});break;case\"Invalid\":s.forEach(function(n){return i.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:{message:o[n].message,valid:!1},validator:n})}),this.emit(\"core.element.validated\",{element:t,elements:a,field:e,valid:!1})}return this},e.prototype.resetForm=function(e){var t=this;return Object.keys(this.fields).forEach(function(n){return t.resetField(n,e)}),this.emit(\"core.form.reset\",{formValidation:this,reset:e}),this},e.prototype.resetField=function(e,t){if(t){var n=this.elements[e],r=n[0].getAttribute(\"type\");n.forEach(function(e){\"radio\"===r||\"checkbox\"===r?(e.removeAttribute(\"selected\"),e.removeAttribute(\"checked\"),e.checked=!1):(e.setAttribute(\"value\",\"\"),(e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement)&&(e.value=\"\"))})}return this.updateFieldStatus(e,\"NotValidated\"),this.emit(\"core.field.reset\",{field:e,reset:t}),this},e.prototype.revalidateField=function(e){return this.fields[e]?(this.updateFieldStatus(e,\"NotValidated\"),this.validateField(e)):Promise.resolve(\"Ignored\")},e.prototype.disableValidator=function(e,t){if(!this.fields[e])return this;var n=this.elements[e];return this.toggleValidator(!1,e,t),this.emit(\"core.validator.disabled\",{elements:n,field:e,formValidation:this,validator:t}),this},e.prototype.enableValidator=function(e,t){if(!this.fields[e])return this;var n=this.elements[e];return this.toggleValidator(!0,e,t),this.emit(\"core.validator.enabled\",{elements:n,field:e,formValidation:this,validator:t}),this},e.prototype.updateValidatorOption=function(e,t,n,r){return this.fields[e]&&this.fields[e].validators&&this.fields[e].validators[t]&&(this.fields[e].validators[t][n]=r),this},e.prototype.setFieldOptions=function(e,t){return this.fields[e]=t,this},e.prototype.destroy=function(){var e=this;return Object.keys(this.plugins).forEach(function(t){return e.plugins[t].uninstall()}),this.ee.clear(),this.filter.clear(),this.results.clear(),this.plugins={},this},e.prototype.setLocale=function(e,t){return this.locale=e,this.localization=t,this},e.prototype.waterfall=function(e){return e.reduce(function(e,t){return e.then(function(e){return t().then(function(t){return e.push(t),e})})},Promise.resolve([]))},e.prototype.queryElements=function(e){var t=this.fields[e].selector?\"#\"===this.fields[e].selector.charAt(0)?'[id=\"'.concat(this.fields[e].selector.substring(1),'\"]'):this.fields[e].selector:'[name=\"'.concat(e.replace(/\"/g,'\\\\\"'),'\"]');return[].slice.call(this.form.querySelectorAll(t))},e.prototype.normalizeResult=function(e,t,n){var r=this.fields[e].validators[t];return Object.assign({},n,{message:n.message||(r?r.message:\"\")||(this.localization&&this.localization[t]&&this.localization[t].default?this.localization[t].default:\"\")||\"The field \".concat(e,\" is not valid\")})},e.prototype.toggleValidator=function(e,t,n){var r=this,i=this.fields[t].validators;return n&&i&&i[n]?this.fields[t].validators[n].enabled=e:n||Object.keys(i).forEach(function(n){return r.fields[t].validators[n].enabled=e}),this.updateFieldStatus(t,\"NotValidated\",n)},e}(),i=function(){function e(e){this.opts=e,this.isEnabled=!0}return e.prototype.setCore=function(e){return this.core=e,this},e.prototype.enable=function(){return this.isEnabled=!0,this.onEnabled(),this},e.prototype.disable=function(){return this.isEnabled=!1,this.onDisabled(),this},e.prototype.isPluginEnabled=function(){return this.isEnabled},e.prototype.onEnabled=function(){},e.prototype.onDisabled=function(){},e.prototype.install=function(){},e.prototype.uninstall=function(){},e}(),a=function(e,t){var n=e.matches||e.webkitMatchesSelector||e.mozMatchesSelector||e.msMatchesSelector;return n?n.call(e,t):[].slice.call(e.parentElement.querySelectorAll(t)).indexOf(e)>=0},o={call:function(e,t){if(\"function\"==typeof e)return e.apply(this,t);if(\"string\"==typeof e){var n=e;\"()\"===n.substring(n.length-2)&&(n=n.substring(0,n.length-2));for(var r=n.split(\".\"),i=r.pop(),a=window,o=0,s=r;o<s.length;o++)a=a[s[o]];return void 0===a[i]?null:a[i].apply(this,t)}},classSet:function(e,t){var n=[],r=[];Object.keys(t).forEach(function(e){e&&(t[e]?n.push(e):r.push(e))}),r.forEach(function(t){return function(e,t){t.split(\" \").forEach(function(t){e.classList?e.classList.remove(t):e.className=e.className.replace(t,\"\")})}(e,t)}),n.forEach(function(t){return function(e,t){t.split(\" \").forEach(function(t){e.classList?e.classList.add(t):\" \".concat(e.className,\" \").indexOf(\" \".concat(t,\" \"))&&(e.className+=\" \".concat(t))})}(e,t)})},closest:function(e,t){for(var n=e;n&&!a(n,t);)n=n.parentElement;return n},fetch:function(e,t){return new Promise(function(n,r){var i,a=Object.assign({},{crossDomain:!1,headers:{},method:\"GET\",params:{}},t),o=Object.keys(a.params).map(function(e){return\"\".concat(encodeURIComponent(e),\"=\").concat(encodeURIComponent(a.params[e]))}).join(\"&\"),s=e.indexOf(\"?\")>-1,l=\"GET\"===a.method?\"\".concat(e).concat(s?\"&\":\"?\").concat(o):e;if(a.crossDomain){var u=document.createElement(\"script\"),d=\"___FormValidationFetch_\".concat(Array(12).fill(\"\").map(function(e){return Math.random().toString(36).charAt(2)}).join(\"\"),\"___\");window[d]=function(e){delete window[d],n(e)},u.src=\"\".concat(l).concat(s?\"&\":\"?\",\"callback=\").concat(d),u.async=!0,u.addEventListener(\"load\",function(){u.parentNode.removeChild(u)}),u.addEventListener(\"error\",function(){return r}),document.head.appendChild(u)}else{var c=new XMLHttpRequest;c.open(a.method,l),c.setRequestHeader(\"X-Requested-With\",\"XMLHttpRequest\"),\"POST\"===a.method&&c.setRequestHeader(\"Content-Type\",\"application/x-www-form-urlencoded\"),Object.keys(a.headers).forEach(function(e){return c.setRequestHeader(e,a.headers[e])}),c.addEventListener(\"load\",function(){n(JSON.parse(this.responseText))}),c.addEventListener(\"error\",function(){return r}),c.send((i=a.params,Object.keys(i).map(function(e){return\"\".concat(encodeURIComponent(e),\"=\").concat(encodeURIComponent(i[e]))}).join(\"&\")))}})},format:function(e,t){var n=Array.isArray(t)?t:[t],r=e;return n.forEach(function(e){r=r.replace(\"%s\",e)}),r},hasClass:function(e,t){return e.classList?e.classList.contains(t):new RegExp(\"(^| )\".concat(t,\"( |$)\"),\"gi\").test(e.className)},isValidDate:function(e,t,n,r){if(isNaN(e)||isNaN(t)||isNaN(n))return!1;if(e<1e3||e>9999||t<=0||t>12)return!1;if(n<=0||n>[31,e%400==0||e%100!=0&&e%4==0?29:28,31,30,31,30,31,31,30,31,30,31][t-1])return!1;if(!0===r){var i=new Date,a=i.getFullYear(),o=i.getMonth(),s=i.getDate();return e<a||e===a&&t-1<o||e===a&&t-1===o&&n<s}return!0},removeUndefined:function(e){return e?Object.entries(e).reduce(function(e,t){var n=t[0],r=t[1];return void 0===r||(e[n]=r),e},{}):{}}};return r.Plugin=i,r.algorithms=e,r.formValidation=function(e,t){var r=Object.assign({},{fields:{},locale:\"en_US\",plugins:{},init:function(e){}},t),i=new n(e,r.fields);return i.setLocale(r.locale,r.localization),Object.keys(r.plugins).forEach(function(e){return i.registerPlugin(e,r.plugins[e])}),r.init(i),Object.keys(r.fields).forEach(function(e){return i.addField(e,r.fields[e])}),i},r.utils=o,r}();var i,a=n.exports,o={exports:{}},s={};o.exports=function(){if(i)return s;i=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.opts=e||{},n.validatorNameFilter=n.getValidatorName.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.registerFilter(\"validator-name\",this.validatorNameFilter)},n.prototype.uninstall=function(){this.core.deregisterFilter(\"validator-name\",this.validatorNameFilter)},n.prototype.getValidatorName=function(e,t){return this.isEnabled&&this.opts[e]||e},n}(a.Plugin);return s.Alias=t,s}();var l,u=o.exports,d={exports:{}},c={};d.exports=function(){if(l)return c;l=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(){var e=t.call(this,{})||this;return e.elementValidatedHandler=e.onElementValidated.bind(e),e.fieldValidHandler=e.onFieldValid.bind(e),e.fieldInvalidHandler=e.onFieldInvalid.bind(e),e.messageDisplayedHandler=e.onMessageDisplayed.bind(e),e}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.on(\"core.field.valid\",this.fieldValidHandler).on(\"core.field.invalid\",this.fieldInvalidHandler).on(\"core.element.validated\",this.elementValidatedHandler).on(\"plugins.message.displayed\",this.messageDisplayedHandler)},n.prototype.uninstall=function(){this.core.off(\"core.field.valid\",this.fieldValidHandler).off(\"core.field.invalid\",this.fieldInvalidHandler).off(\"core.element.validated\",this.elementValidatedHandler).off(\"plugins.message.displayed\",this.messageDisplayedHandler)},n.prototype.onElementValidated=function(e){e.valid&&(e.element.setAttribute(\"aria-invalid\",\"false\"),e.element.removeAttribute(\"aria-describedby\"))},n.prototype.onFieldValid=function(e){var t=this.core.getElements(e);t&&t.forEach(function(e){e.setAttribute(\"aria-invalid\",\"false\"),e.removeAttribute(\"aria-describedby\")})},n.prototype.onFieldInvalid=function(e){var t=this.core.getElements(e);t&&t.forEach(function(e){return e.setAttribute(\"aria-invalid\",\"true\")})},n.prototype.onMessageDisplayed=function(e){e.messageElement.setAttribute(\"role\",\"alert\"),e.messageElement.setAttribute(\"aria-hidden\",\"false\");var t=this.core.getElements(e.field),n=t.indexOf(e.element),r=\"js-fv-\".concat(e.field,\"-\").concat(n,\"-\").concat(Date.now(),\"-message\");e.messageElement.setAttribute(\"id\",r),e.element.setAttribute(\"aria-describedby\",r);var i=e.element.getAttribute(\"type\");\"radio\"!==i&&\"checkbox\"!==i||t.forEach(function(e){return e.setAttribute(\"aria-describedby\",r)})},n}(a.Plugin);return c.Aria=t,c}();var f,p=d.exports,h={exports:{}},m={};h.exports=function(){if(f)return m;f=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.addedFields=new Map,n.opts=Object.assign({},{html5Input:!1,pluginPrefix:\"data-fvp-\",prefix:\"data-fv-\"},e),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){var e=this;this.parsePlugins();var t=this.parseOptions();Object.keys(t).forEach(function(n){e.addedFields.has(n)||e.addedFields.set(n,!0),e.core.addField(n,t[n])}),this.core.on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.uninstall=function(){this.addedFields.clear(),this.core.off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.onFieldAdded=function(e){var t=this,n=e.elements;n&&0!==n.length&&!this.addedFields.has(e.field)&&(this.addedFields.set(e.field,!0),n.forEach(function(n){var r=t.parseElement(n);if(!t.isEmptyOption(r)){var i={selector:e.options.selector,validators:Object.assign({},e.options.validators||{},r.validators)};t.core.setFieldOptions(e.field,i)}}))},n.prototype.onFieldRemoved=function(e){e.field&&this.addedFields.has(e.field)&&this.addedFields.delete(e.field)},n.prototype.parseOptions=function(){var e=this,t=this.opts.prefix,n={},r=this.core.getFields(),i=this.core.getFormElement();return[].slice.call(i.querySelectorAll(\"[name], [\".concat(t,\"field]\"))).forEach(function(r){var i=e.parseElement(r);if(!e.isEmptyOption(i)){var a=r.getAttribute(\"name\")||r.getAttribute(\"\".concat(t,\"field\"));n[a]=Object.assign({},n[a],i)}}),Object.keys(n).forEach(function(e){Object.keys(n[e].validators).forEach(function(t){n[e].validators[t].enabled=n[e].validators[t].enabled||!1,r[e]&&r[e].validators&&r[e].validators[t]&&Object.assign(n[e].validators[t],r[e].validators[t])})}),Object.assign({},r,n)},n.prototype.createPluginInstance=function(e,t){for(var n=e.split(\".\"),r=window||this,i=0,a=n.length;i<a;i++)r=r[n[i]];if(\"function\"!=typeof r)throw new Error(\"the plugin \".concat(e,\" doesn't exist\"));return new r(t)},n.prototype.parsePlugins=function(){for(var e,t=this,n=this.core.getFormElement(),r=new RegExp(\"^\".concat(this.opts.pluginPrefix,\"([a-z0-9-]+)(___)*([a-z0-9-]+)*$\")),i=n.attributes.length,a={},o=0;o<i;o++){var s=n.attributes[o].name,l=n.attributes[o].value,u=r.exec(s);if(u&&4===u.length){var d=this.toCamelCase(u[1]);a[d]=Object.assign({},u[3]?((e={})[this.toCamelCase(u[3])]=l,e):{enabled:\"\"===l||\"true\"===l},a[d])}}Object.keys(a).forEach(function(e){var n=a[e],r=n.enabled,i=n.class;if(r&&i){delete n.enabled,delete n.clazz;var o=t.createPluginInstance(i,n);t.core.registerPlugin(e,o)}})},n.prototype.isEmptyOption=function(e){var t=e.validators;return 0===Object.keys(t).length&&t.constructor===Object},n.prototype.parseElement=function(e){for(var t=new RegExp(\"^\".concat(this.opts.prefix,\"([a-z0-9-]+)(___)*([a-z0-9-]+)*$\")),n=e.attributes.length,r={},i=e.getAttribute(\"type\"),a=0;a<n;a++){var o=e.attributes[a].name,s=e.attributes[a].value;if(this.opts.html5Input)switch(!0){case\"minlength\"===o:r.stringLength=Object.assign({},{enabled:!0,min:parseInt(s,10)},r.stringLength);break;case\"maxlength\"===o:r.stringLength=Object.assign({},{enabled:!0,max:parseInt(s,10)},r.stringLength);break;case\"pattern\"===o:r.regexp=Object.assign({},{enabled:!0,regexp:s},r.regexp);break;case\"required\"===o:r.notEmpty=Object.assign({},{enabled:!0},r.notEmpty);break;case\"type\"===o&&\"color\"===s:r.color=Object.assign({},{enabled:!0,type:\"hex\"},r.color);break;case\"type\"===o&&\"email\"===s:r.emailAddress=Object.assign({},{enabled:!0},r.emailAddress);break;case\"type\"===o&&\"url\"===s:r.uri=Object.assign({},{enabled:!0},r.uri);break;case\"type\"===o&&\"range\"===s:r.between=Object.assign({},{enabled:!0,max:parseFloat(e.getAttribute(\"max\")),min:parseFloat(e.getAttribute(\"min\"))},r.between);break;case\"min\"===o&&\"date\"!==i&&\"range\"!==i:r.greaterThan=Object.assign({},{enabled:!0,min:parseFloat(s)},r.greaterThan);break;case\"max\"===o&&\"date\"!==i&&\"range\"!==i:r.lessThan=Object.assign({},{enabled:!0,max:parseFloat(s)},r.lessThan)}var l=t.exec(o);if(l&&4===l.length){var u=this.toCamelCase(l[1]);r[u]||(r[u]={}),l[3]?r[u][this.toCamelCase(l[3])]=this.normalizeValue(s):!0===r[u].enabled&&!1===r[u].enabled||(r[u].enabled=\"\"===s||\"true\"===s)}}return{validators:r}},n.prototype.normalizeValue=function(e){return\"true\"===e||\"\"===e||\"false\"!==e&&e},n.prototype.toUpperCase=function(e){return e.charAt(1).toUpperCase()},n.prototype.toCamelCase=function(e){return e.replace(/-./g,this.toUpperCase)},n}(a.Plugin);return m.Declarative=t,m}();var v,g=h.exports,_={exports:{}},y={};_.exports=function(){if(v)return y;v=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(){var e=t.call(this,{})||this;return e.onValidHandler=e.onFormValid.bind(e),e}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){if(this.core.getFormElement().querySelectorAll('[type=\"submit\"][name=\"submit\"]').length)throw new Error(\"Do not use `submit` for the name attribute of submit button\");this.core.on(\"core.form.valid\",this.onValidHandler)},n.prototype.uninstall=function(){this.core.off(\"core.form.valid\",this.onValidHandler)},n.prototype.onFormValid=function(){var e=this.core.getFormElement();this.isEnabled&&e instanceof HTMLFormElement&&e.submit()},n}(a.Plugin);return y.DefaultSubmit=t,y}();var b,M=_.exports,w={exports:{}},k={};w.exports=function(){if(b)return k;b=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.opts=e||{},n.triggerExecutedHandler=n.onTriggerExecuted.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.on(\"plugins.trigger.executed\",this.triggerExecutedHandler)},n.prototype.uninstall=function(){this.core.off(\"plugins.trigger.executed\",this.triggerExecutedHandler)},n.prototype.onTriggerExecuted=function(e){if(this.isEnabled&&this.opts[e.field])for(var t=0,n=this.opts[e.field].split(\" \");t<n.length;t++){var r=n[t].trim();this.opts[r]&&this.core.revalidateField(r)}},n}(a.Plugin);return k.Dependency=t,k}();var L,x=w.exports,T={exports:{}},S={};T.exports=function(){if(L)return S;L=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.removeUndefined,r=function(e){function r(t){var i=e.call(this,t)||this;return i.opts=Object.assign({},{excluded:r.defaultIgnore},n(t)),i.ignoreValidationFilter=i.ignoreValidation.bind(i),i}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.defaultIgnore=function(e,t,n){var r=!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length),i=t.getAttribute(\"disabled\");return\"\"===i||\"disabled\"===i||\"hidden\"===t.getAttribute(\"type\")||!r},r.prototype.install=function(){this.core.registerFilter(\"element-ignored\",this.ignoreValidationFilter)},r.prototype.uninstall=function(){this.core.deregisterFilter(\"element-ignored\",this.ignoreValidationFilter)},r.prototype.ignoreValidation=function(e,t,n){return!!this.isEnabled&&this.opts.excluded.apply(this,[e,t,n])},r}(e.Plugin);return S.Excluded=r,S}();var D,E=T.exports,Y={exports:{}},A={};Y.exports=function(){if(D)return A;D=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.statuses=new Map,n.opts=Object.assign({},{onStatusChanged:function(){}},e),n.elementValidatingHandler=n.onElementValidating.bind(n),n.elementValidatedHandler=n.onElementValidated.bind(n),n.elementNotValidatedHandler=n.onElementNotValidated.bind(n),n.elementIgnoredHandler=n.onElementIgnored.bind(n),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.on(\"core.element.validating\",this.elementValidatingHandler).on(\"core.element.validated\",this.elementValidatedHandler).on(\"core.element.notvalidated\",this.elementNotValidatedHandler).on(\"core.element.ignored\",this.elementIgnoredHandler).on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.uninstall=function(){this.statuses.clear(),this.core.off(\"core.element.validating\",this.elementValidatingHandler).off(\"core.element.validated\",this.elementValidatedHandler).off(\"core.element.notvalidated\",this.elementNotValidatedHandler).off(\"core.element.ignored\",this.elementIgnoredHandler).off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.areFieldsValid=function(){return Array.from(this.statuses.values()).every(function(e){return\"Valid\"===e||\"NotValidated\"===e||\"Ignored\"===e})},n.prototype.getStatuses=function(){return this.isEnabled?this.statuses:new Map},n.prototype.onFieldAdded=function(e){this.statuses.set(e.field,\"NotValidated\")},n.prototype.onFieldRemoved=function(e){this.statuses.has(e.field)&&this.statuses.delete(e.field),this.handleStatusChanged(this.areFieldsValid())},n.prototype.onElementValidating=function(e){this.statuses.set(e.field,\"Validating\"),this.handleStatusChanged(!1)},n.prototype.onElementValidated=function(e){this.statuses.set(e.field,e.valid?\"Valid\":\"Invalid\"),e.valid?this.handleStatusChanged(this.areFieldsValid()):this.handleStatusChanged(!1)},n.prototype.onElementNotValidated=function(e){this.statuses.set(e.field,\"NotValidated\"),this.handleStatusChanged(!1)},n.prototype.onElementIgnored=function(e){this.statuses.set(e.field,\"Ignored\"),this.handleStatusChanged(this.areFieldsValid())},n.prototype.handleStatusChanged=function(e){this.isEnabled&&this.opts.onStatusChanged(e)},n}(a.Plugin);return A.FieldStatus=t,A}();var O,C=Y.exports,j={exports:{}},P={},H={exports:{}},I={};H.exports=function(){if(O)return I;O=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.classSet,r=function(e){function r(t){var n=e.call(this,t)||this;return n.useDefaultContainer=!1,n.messages=new Map,n.defaultContainer=document.createElement(\"div\"),n.useDefaultContainer=!t||!t.container,n.opts=Object.assign({},{container:function(e,t){return n.defaultContainer}},t),n.elementIgnoredHandler=n.onElementIgnored.bind(n),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n.validatorValidatedHandler=n.onValidatorValidated.bind(n),n.validatorNotValidatedHandler=n.onValidatorNotValidated.bind(n),n}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.getClosestContainer=function(e,t,n){for(var r=e;r&&r!==t&&(r=r.parentElement,!n.test(r.className)););return r},r.prototype.install=function(){this.useDefaultContainer&&this.core.getFormElement().appendChild(this.defaultContainer),this.core.on(\"core.element.ignored\",this.elementIgnoredHandler).on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler).on(\"core.validator.validated\",this.validatorValidatedHandler).on(\"core.validator.notvalidated\",this.validatorNotValidatedHandler)},r.prototype.uninstall=function(){this.useDefaultContainer&&this.core.getFormElement().removeChild(this.defaultContainer),this.messages.forEach(function(e){return e.parentNode.removeChild(e)}),this.messages.clear(),this.core.off(\"core.element.ignored\",this.elementIgnoredHandler).off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler).off(\"core.validator.validated\",this.validatorValidatedHandler).off(\"core.validator.notvalidated\",this.validatorNotValidatedHandler)},r.prototype.onEnabled=function(){this.messages.forEach(function(e,t,r){n(t,{\"fv-plugins-message-container--enabled\":!0,\"fv-plugins-message-container--disabled\":!1})})},r.prototype.onDisabled=function(){this.messages.forEach(function(e,t,r){n(t,{\"fv-plugins-message-container--enabled\":!1,\"fv-plugins-message-container--disabled\":!0})})},r.prototype.onFieldAdded=function(e){var t=this,n=e.elements;n&&(n.forEach(function(e){var n=t.messages.get(e);n&&(n.parentNode.removeChild(n),t.messages.delete(e))}),this.prepareFieldContainer(e.field,n))},r.prototype.onFieldRemoved=function(e){var t=this;if(e.elements.length&&e.field){var n=e.elements[0].getAttribute(\"type\");(\"radio\"===n||\"checkbox\"===n?[e.elements[0]]:e.elements).forEach(function(e){if(t.messages.has(e)){var n=t.messages.get(e);n.parentNode.removeChild(n),t.messages.delete(e)}})}},r.prototype.prepareFieldContainer=function(e,t){var n=this;if(t.length){var r=t[0].getAttribute(\"type\");\"radio\"===r||\"checkbox\"===r?this.prepareElementContainer(e,t[0],t):t.forEach(function(r){return n.prepareElementContainer(e,r,t)})}},r.prototype.prepareElementContainer=function(e,t,r){var i;if(\"string\"==typeof this.opts.container){var a=\"#\"===this.opts.container.charAt(0)?'[id=\"'.concat(this.opts.container.substring(1),'\"]'):this.opts.container;i=this.core.getFormElement().querySelector(a)}else i=this.opts.container(e,t);var o=document.createElement(\"div\");i.appendChild(o),n(o,{\"fv-plugins-message-container\":!0,\"fv-plugins-message-container--enabled\":this.isEnabled,\"fv-plugins-message-container--disabled\":!this.isEnabled}),this.core.emit(\"plugins.message.placed\",{element:t,elements:r,field:e,messageElement:o}),this.messages.set(t,o)},r.prototype.getMessage=function(e){return\"string\"==typeof e.message?e.message:e.message[this.core.getLocale()]},r.prototype.onValidatorValidated=function(e){var t,r=e.elements,i=e.element.getAttribute(\"type\"),a=(\"radio\"===i||\"checkbox\"===i)&&r.length>0?r[0]:e.element;if(this.messages.has(a)){var o=this.messages.get(a),s=o.querySelector('[data-field=\"'.concat(e.field.replace(/\"/g,'\\\\\"'),'\"][data-validator=\"').concat(e.validator.replace(/\"/g,'\\\\\"'),'\"]'));if(s||e.result.valid)s&&!e.result.valid?(s.innerHTML=this.getMessage(e.result),this.core.emit(\"plugins.message.displayed\",{element:e.element,field:e.field,message:e.result.message,messageElement:s,meta:e.result.meta,validator:e.validator})):s&&e.result.valid&&o.removeChild(s);else{var l=document.createElement(\"div\");l.innerHTML=this.getMessage(e.result),l.setAttribute(\"data-field\",e.field),l.setAttribute(\"data-validator\",e.validator),this.opts.clazz&&n(l,((t={})[this.opts.clazz]=!0,t)),o.appendChild(l),this.core.emit(\"plugins.message.displayed\",{element:e.element,field:e.field,message:e.result.message,messageElement:l,meta:e.result.meta,validator:e.validator})}}},r.prototype.onValidatorNotValidated=function(e){var t=e.elements,n=e.element.getAttribute(\"type\"),r=\"radio\"===n||\"checkbox\"===n?t[0]:e.element;if(this.messages.has(r)){var i=this.messages.get(r),a=i.querySelector('[data-field=\"'.concat(e.field.replace(/\"/g,'\\\\\"'),'\"][data-validator=\"').concat(e.validator.replace(/\"/g,'\\\\\"'),'\"]'));a&&i.removeChild(a)}},r.prototype.onElementIgnored=function(e){var t=e.elements,n=e.element.getAttribute(\"type\"),r=\"radio\"===n||\"checkbox\"===n?t[0]:e.element;if(this.messages.has(r)){var i=this.messages.get(r);[].slice.call(i.querySelectorAll('[data-field=\"'.concat(e.field.replace(/\"/g,'\\\\\"'),'\"]'))).forEach(function(e){i.removeChild(e)})}},r}(e.Plugin);return I.Message=r,I}();var F,N=H.exports;\n/** \n     * FormValidation (https://formvalidation.io)\n     * The best validation library for JavaScript\n     * (c) 2013 - 2023 Nguyen Huu Phuoc <me@phuoc.ng>\n     *\n     * @license https://formvalidation.io/license\n     * @package @form-validation/plugin-framework\n     * @version 2.4.0\n     */j.exports=function(){if(F)return P;F=1;var e=a,t=N,n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},n(e,t)},r=e.utils.classSet,i=e.utils.closest,o=function(e){function a(t){var n=e.call(this,t)||this;return n.results=new Map,n.containers=new Map,n.opts=Object.assign({},{defaultMessageContainer:!0,eleInvalidClass:\"\",eleValidClass:\"\",rowClasses:\"\",rowValidatingClass:\"\"},t),n.elementIgnoredHandler=n.onElementIgnored.bind(n),n.elementValidatingHandler=n.onElementValidating.bind(n),n.elementValidatedHandler=n.onElementValidated.bind(n),n.elementNotValidatedHandler=n.onElementNotValidated.bind(n),n.iconPlacedHandler=n.onIconPlaced.bind(n),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n.messagePlacedHandler=n.onMessagePlaced.bind(n),n}return function(e,t){if(\"function\"!=typeof t&&null!==t)throw new TypeError(\"Class extends value \"+String(t)+\" is not a constructor or null\");function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}(a,e),a.prototype.install=function(){var e,n=this;r(this.core.getFormElement(),((e={})[this.opts.formClass]=!0,e[\"fv-plugins-framework\"]=!0,e)),this.core.on(\"core.element.ignored\",this.elementIgnoredHandler).on(\"core.element.validating\",this.elementValidatingHandler).on(\"core.element.validated\",this.elementValidatedHandler).on(\"core.element.notvalidated\",this.elementNotValidatedHandler).on(\"plugins.icon.placed\",this.iconPlacedHandler).on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler),this.opts.defaultMessageContainer&&(this.core.registerPlugin(a.MESSAGE_PLUGIN,new t.Message({clazz:this.opts.messageClass,container:function(e,r){var a=\"string\"==typeof n.opts.rowSelector?n.opts.rowSelector:n.opts.rowSelector(e,r),o=i(r,a);return t.Message.getClosestContainer(r,o,n.opts.rowPattern)}})),this.core.on(\"plugins.message.placed\",this.messagePlacedHandler))},a.prototype.uninstall=function(){var e;this.results.clear(),this.containers.clear(),r(this.core.getFormElement(),((e={})[this.opts.formClass]=!1,e[\"fv-plugins-framework\"]=!1,e)),this.core.off(\"core.element.ignored\",this.elementIgnoredHandler).off(\"core.element.validating\",this.elementValidatingHandler).off(\"core.element.validated\",this.elementValidatedHandler).off(\"core.element.notvalidated\",this.elementNotValidatedHandler).off(\"plugins.icon.placed\",this.iconPlacedHandler).off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler),this.opts.defaultMessageContainer&&(this.core.deregisterPlugin(a.MESSAGE_PLUGIN),this.core.off(\"plugins.message.placed\",this.messagePlacedHandler))},a.prototype.onEnabled=function(){var e;r(this.core.getFormElement(),((e={})[this.opts.formClass]=!0,e)),this.opts.defaultMessageContainer&&this.core.enablePlugin(a.MESSAGE_PLUGIN)},a.prototype.onDisabled=function(){var e;r(this.core.getFormElement(),((e={})[this.opts.formClass]=!1,e)),this.opts.defaultMessageContainer&&this.core.disablePlugin(a.MESSAGE_PLUGIN)},a.prototype.onIconPlaced=function(e){},a.prototype.onMessagePlaced=function(e){},a.prototype.onFieldAdded=function(e){var t=this,n=e.elements;n&&(n.forEach(function(e){var n,i=t.containers.get(e);i&&(r(i,((n={})[t.opts.rowInvalidClass]=!1,n[t.opts.rowValidatingClass]=!1,n[t.opts.rowValidClass]=!1,n[\"fv-plugins-icon-container\"]=!1,n)),t.containers.delete(e))}),this.prepareFieldContainer(e.field,n))},a.prototype.onFieldRemoved=function(e){var t=this;e.elements.forEach(function(e){var n,i=t.containers.get(e);i&&r(i,((n={})[t.opts.rowInvalidClass]=!1,n[t.opts.rowValidatingClass]=!1,n[t.opts.rowValidClass]=!1,n))})},a.prototype.prepareFieldContainer=function(e,t){var n=this;if(t.length){var r=t[0].getAttribute(\"type\");\"radio\"===r||\"checkbox\"===r?this.prepareElementContainer(e,t[0]):t.forEach(function(t){return n.prepareElementContainer(e,t)})}},a.prototype.prepareElementContainer=function(e,t){var n,a=\"string\"==typeof this.opts.rowSelector?this.opts.rowSelector:this.opts.rowSelector(e,t),o=i(t,a);o!==t&&(r(o,((n={})[this.opts.rowClasses]=!0,n[\"fv-plugins-icon-container\"]=!0,n)),this.containers.set(t,o))},a.prototype.onElementValidating=function(e){this.removeClasses(e.element,e.elements)},a.prototype.onElementNotValidated=function(e){this.removeClasses(e.element,e.elements)},a.prototype.onElementIgnored=function(e){this.removeClasses(e.element,e.elements)},a.prototype.removeClasses=function(e,t){var n,i=this,a=e.getAttribute(\"type\"),o=\"radio\"===a||\"checkbox\"===a?t[0]:e;t.forEach(function(e){var t;r(e,((t={})[i.opts.eleValidClass]=!1,t[i.opts.eleInvalidClass]=!1,t))});var s=this.containers.get(o);s&&r(s,((n={})[this.opts.rowInvalidClass]=!1,n[this.opts.rowValidatingClass]=!1,n[this.opts.rowValidClass]=!1,n))},a.prototype.onElementValidated=function(e){var t,n,i=this,a=e.elements,o=e.element.getAttribute(\"type\"),s=\"radio\"===o||\"checkbox\"===o?a[0]:e.element;a.forEach(function(t){var n;r(t,((n={})[i.opts.eleValidClass]=e.valid,n[i.opts.eleInvalidClass]=!e.valid,n))});var l=this.containers.get(s);if(l)if(e.valid){this.results.delete(s);var u=!0;this.containers.forEach(function(e,t){e===l&&!1===i.results.get(t)&&(u=!1)}),u&&r(l,((n={})[this.opts.rowInvalidClass]=!1,n[this.opts.rowValidatingClass]=!1,n[this.opts.rowValidClass]=!0,n))}else this.results.set(s,!1),r(l,((t={})[this.opts.rowInvalidClass]=!0,t[this.opts.rowValidatingClass]=!1,t[this.opts.rowValidClass]=!1,t))},a.MESSAGE_PLUGIN=\"___frameworkMessage\",a}(e.Plugin);return P.Framework=o,P}();var R,V=j.exports,$={exports:{}},z={};$.exports=function(){if(R)return z;R=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.classSet,r=function(e){function r(t){var n=e.call(this,t)||this;return n.icons=new Map,n.opts=Object.assign({},{invalid:\"fv-plugins-icon--invalid\",onPlaced:function(){},onSet:function(){},valid:\"fv-plugins-icon--valid\",validating:\"fv-plugins-icon--validating\"},t),n.elementValidatingHandler=n.onElementValidating.bind(n),n.elementValidatedHandler=n.onElementValidated.bind(n),n.elementNotValidatedHandler=n.onElementNotValidated.bind(n),n.elementIgnoredHandler=n.onElementIgnored.bind(n),n.fieldAddedHandler=n.onFieldAdded.bind(n),n}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.prototype.install=function(){this.core.on(\"core.element.validating\",this.elementValidatingHandler).on(\"core.element.validated\",this.elementValidatedHandler).on(\"core.element.notvalidated\",this.elementNotValidatedHandler).on(\"core.element.ignored\",this.elementIgnoredHandler).on(\"core.field.added\",this.fieldAddedHandler)},r.prototype.uninstall=function(){this.icons.forEach(function(e){return e.parentNode.removeChild(e)}),this.icons.clear(),this.core.off(\"core.element.validating\",this.elementValidatingHandler).off(\"core.element.validated\",this.elementValidatedHandler).off(\"core.element.notvalidated\",this.elementNotValidatedHandler).off(\"core.element.ignored\",this.elementIgnoredHandler).off(\"core.field.added\",this.fieldAddedHandler)},r.prototype.onEnabled=function(){this.icons.forEach(function(e,t,r){n(t,{\"fv-plugins-icon--enabled\":!0,\"fv-plugins-icon--disabled\":!1})})},r.prototype.onDisabled=function(){this.icons.forEach(function(e,t,r){n(t,{\"fv-plugins-icon--enabled\":!1,\"fv-plugins-icon--disabled\":!0})})},r.prototype.onFieldAdded=function(e){var t=this,n=e.elements;n&&(n.forEach(function(e){var n=t.icons.get(e);n&&(n.parentNode.removeChild(n),t.icons.delete(e))}),this.prepareFieldIcon(e.field,n))},r.prototype.prepareFieldIcon=function(e,t){var n=this;if(t.length){var r=t[0].getAttribute(\"type\");\"radio\"===r||\"checkbox\"===r?this.prepareElementIcon(e,t[0]):t.forEach(function(t){return n.prepareElementIcon(e,t)})}},r.prototype.prepareElementIcon=function(e,t){var r=document.createElement(\"i\");r.setAttribute(\"data-field\",e),t.parentNode.insertBefore(r,t.nextSibling),n(r,{\"fv-plugins-icon\":!0,\"fv-plugins-icon--enabled\":this.isEnabled,\"fv-plugins-icon--disabled\":!this.isEnabled});var i={classes:{invalid:this.opts.invalid,valid:this.opts.valid,validating:this.opts.validating},element:t,field:e,iconElement:r};this.core.emit(\"plugins.icon.placed\",i),this.opts.onPlaced(i),this.icons.set(t,r)},r.prototype.onElementValidating=function(e){var t,n=this.setClasses(e.field,e.element,e.elements,((t={})[this.opts.invalid]=!1,t[this.opts.valid]=!1,t[this.opts.validating]=!0,t)),r={element:e.element,field:e.field,iconElement:n,status:\"Validating\"};this.core.emit(\"plugins.icon.set\",r),this.opts.onSet(r)},r.prototype.onElementValidated=function(e){var t,n=this.setClasses(e.field,e.element,e.elements,((t={})[this.opts.invalid]=!e.valid,t[this.opts.valid]=e.valid,t[this.opts.validating]=!1,t)),r={element:e.element,field:e.field,iconElement:n,status:e.valid?\"Valid\":\"Invalid\"};this.core.emit(\"plugins.icon.set\",r),this.opts.onSet(r)},r.prototype.onElementNotValidated=function(e){var t,n=this.setClasses(e.field,e.element,e.elements,((t={})[this.opts.invalid]=!1,t[this.opts.valid]=!1,t[this.opts.validating]=!1,t)),r={element:e.element,field:e.field,iconElement:n,status:\"NotValidated\"};this.core.emit(\"plugins.icon.set\",r),this.opts.onSet(r)},r.prototype.onElementIgnored=function(e){var t,n=this.setClasses(e.field,e.element,e.elements,((t={})[this.opts.invalid]=!1,t[this.opts.valid]=!1,t[this.opts.validating]=!1,t)),r={element:e.element,field:e.field,iconElement:n,status:\"Ignored\"};this.core.emit(\"plugins.icon.set\",r),this.opts.onSet(r)},r.prototype.setClasses=function(e,t,r,i){var a=t.getAttribute(\"type\"),o=\"radio\"===a||\"checkbox\"===a?r[0]:t;if(this.icons.has(o)){var s=this.icons.get(o);return n(s,i),s}return null},r}(e.Plugin);return z.Icon=r,z}();var W,U=$.exports,B={exports:{}},q={};B.exports=function(){if(W)return q;W=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.removeUndefined,r=function(e){function r(t){var r=e.call(this,t)||this;return r.invalidFields=new Map,r.opts=Object.assign({},{enabled:!0},n(t)),r.validatorHandler=r.onValidatorValidated.bind(r),r.shouldValidateFilter=r.shouldValidate.bind(r),r.fieldAddedHandler=r.onFieldAdded.bind(r),r.elementNotValidatedHandler=r.onElementNotValidated.bind(r),r.elementValidatingHandler=r.onElementValidating.bind(r),r}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.prototype.install=function(){this.core.on(\"core.validator.validated\",this.validatorHandler).on(\"core.field.added\",this.fieldAddedHandler).on(\"core.element.notvalidated\",this.elementNotValidatedHandler).on(\"core.element.validating\",this.elementValidatingHandler).registerFilter(\"field-should-validate\",this.shouldValidateFilter)},r.prototype.uninstall=function(){this.invalidFields.clear(),this.core.off(\"core.validator.validated\",this.validatorHandler).off(\"core.field.added\",this.fieldAddedHandler).off(\"core.element.notvalidated\",this.elementNotValidatedHandler).off(\"core.element.validating\",this.elementValidatingHandler).deregisterFilter(\"field-should-validate\",this.shouldValidateFilter)},r.prototype.shouldValidate=function(e,t,n,r){return!this.isEnabled||!((!0===this.opts.enabled||!0===this.opts.enabled[e])&&this.invalidFields.has(t)&&this.invalidFields.get(t).length&&-1===this.invalidFields.get(t).indexOf(r))},r.prototype.onValidatorValidated=function(e){var t=this.invalidFields.has(e.element)?this.invalidFields.get(e.element):[],n=t.indexOf(e.validator);e.result.valid&&n>=0?t.splice(n,1):e.result.valid||-1!==n||t.push(e.validator),this.invalidFields.set(e.element,t)},r.prototype.onFieldAdded=function(e){e.elements&&this.clearInvalidFields(e.elements)},r.prototype.onElementNotValidated=function(e){this.clearInvalidFields(e.elements)},r.prototype.onElementValidating=function(e){this.clearInvalidFields(e.elements)},r.prototype.clearInvalidFields=function(e){var t=this;e.forEach(function(e){return t.invalidFields.delete(e)})},r}(e.Plugin);return q.Sequence=r,q}();var G,J=B.exports,Z={exports:{}},K={};Z.exports=function(){if(G)return K;G=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.isFormValid=!1,n.isButtonClicked=!1,n.opts=Object.assign({},{aspNetButton:!1,buttons:function(e){return[].slice.call(e.querySelectorAll('[type=\"submit\"]:not([formnovalidate])'))},liveMode:!0},e),n.submitHandler=n.handleSubmitEvent.bind(n),n.buttonClickHandler=n.handleClickEvent.bind(n),n.ignoreValidationFilter=n.ignoreValidation.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){var e=this;if(this.core.getFormElement()instanceof HTMLFormElement){var t=this.core.getFormElement();this.submitButtons=this.opts.buttons(t),t.setAttribute(\"novalidate\",\"novalidate\"),t.addEventListener(\"submit\",this.submitHandler),this.hiddenClickedEle=document.createElement(\"input\"),this.hiddenClickedEle.setAttribute(\"type\",\"hidden\"),t.appendChild(this.hiddenClickedEle),this.submitButtons.forEach(function(t){t.addEventListener(\"click\",e.buttonClickHandler)}),this.core.registerFilter(\"element-ignored\",this.ignoreValidationFilter)}},n.prototype.uninstall=function(){var e=this,t=this.core.getFormElement();t instanceof HTMLFormElement&&t.removeEventListener(\"submit\",this.submitHandler),this.submitButtons.forEach(function(t){t.removeEventListener(\"click\",e.buttonClickHandler)}),this.hiddenClickedEle.parentElement.removeChild(this.hiddenClickedEle),this.core.deregisterFilter(\"element-ignored\",this.ignoreValidationFilter)},n.prototype.handleSubmitEvent=function(e){this.validateForm(e)},n.prototype.handleClickEvent=function(e){var t=e.currentTarget;if(this.isButtonClicked=!0,t instanceof HTMLElement)if(this.opts.aspNetButton&&!0===this.isFormValid);else{this.core.getFormElement().removeEventListener(\"submit\",this.submitHandler),this.clickedButton=e.target;var n=this.clickedButton.getAttribute(\"name\"),r=this.clickedButton.getAttribute(\"value\");n&&r&&(this.hiddenClickedEle.setAttribute(\"name\",n),this.hiddenClickedEle.setAttribute(\"value\",r)),this.validateForm(e)}},n.prototype.validateForm=function(e){var t=this;this.isEnabled&&(e.preventDefault(),this.core.validate().then(function(e){\"Valid\"===e&&t.opts.aspNetButton&&!t.isFormValid&&t.clickedButton&&(t.isFormValid=!0,t.clickedButton.removeEventListener(\"click\",t.buttonClickHandler),t.clickedButton.click())}))},n.prototype.ignoreValidation=function(e,t,n){return!!this.isEnabled&&!this.opts.liveMode&&!this.isButtonClicked},n}(a.Plugin);return K.SubmitButton=t,K}();var X,Q=Z.exports,ee={exports:{}},te={};ee.exports=function(){if(X)return te;X=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.classSet,r=function(e){function r(t){var n=e.call(this,t)||this;return n.messages=new Map,n.opts=Object.assign({},{placement:\"top\",trigger:\"click\"},t),n.iconPlacedHandler=n.onIconPlaced.bind(n),n.validatorValidatedHandler=n.onValidatorValidated.bind(n),n.elementValidatedHandler=n.onElementValidated.bind(n),n.documentClickHandler=n.onDocumentClicked.bind(n),n}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.prototype.install=function(){var e;this.tip=document.createElement(\"div\"),n(this.tip,((e={\"fv-plugins-tooltip\":!0})[\"fv-plugins-tooltip--\".concat(this.opts.placement)]=!0,e)),document.body.appendChild(this.tip),this.core.on(\"plugins.icon.placed\",this.iconPlacedHandler).on(\"core.validator.validated\",this.validatorValidatedHandler).on(\"core.element.validated\",this.elementValidatedHandler),\"click\"===this.opts.trigger&&document.addEventListener(\"click\",this.documentClickHandler)},r.prototype.uninstall=function(){this.messages.clear(),document.body.removeChild(this.tip),this.core.off(\"plugins.icon.placed\",this.iconPlacedHandler).off(\"core.validator.validated\",this.validatorValidatedHandler).off(\"core.element.validated\",this.elementValidatedHandler),\"click\"===this.opts.trigger&&document.removeEventListener(\"click\",this.documentClickHandler)},r.prototype.onIconPlaced=function(e){var t=this;n(e.iconElement,{\"fv-plugins-tooltip-icon\":!0}),\"hover\"===this.opts.trigger?(e.iconElement.addEventListener(\"mouseenter\",function(n){return t.show(e.element,n)}),e.iconElement.addEventListener(\"mouseleave\",function(e){return t.hide()})):e.iconElement.addEventListener(\"click\",function(n){return t.show(e.element,n)})},r.prototype.onValidatorValidated=function(e){if(!e.result.valid){var t=e.elements,n=e.element.getAttribute(\"type\"),r=\"radio\"===n||\"checkbox\"===n?t[0]:e.element,i=\"string\"==typeof e.result.message?e.result.message:e.result.message[this.core.getLocale()];this.messages.set(r,i)}},r.prototype.onElementValidated=function(e){if(e.valid){var t=e.elements,n=e.element.getAttribute(\"type\"),r=\"radio\"===n||\"checkbox\"===n?t[0]:e.element;this.messages.delete(r)}},r.prototype.onDocumentClicked=function(e){this.hide()},r.prototype.show=function(e,t){if(this.isEnabled&&(t.preventDefault(),t.stopPropagation(),this.messages.has(e))){n(this.tip,{\"fv-plugins-tooltip--hide\":!1}),this.tip.innerHTML='<div class=\"fv-plugins-tooltip__content\">'.concat(this.messages.get(e),\"</div>\");var r=t.target.getBoundingClientRect(),i=this.tip.getBoundingClientRect(),a=i.height,o=i.width,s=0,l=0;switch(this.opts.placement){case\"bottom\":s=r.top+r.height,l=r.left+r.width/2-o/2;break;case\"bottom-left\":s=r.top+r.height,l=r.left;break;case\"bottom-right\":s=r.top+r.height,l=r.left+r.width-o;break;case\"left\":s=r.top+r.height/2-a/2,l=r.left-o;break;case\"right\":s=r.top+r.height/2-a/2,l=r.left+r.width;break;case\"top-left\":s=r.top-a,l=r.left;break;case\"top-right\":s=r.top-a,l=r.left+r.width-o;break;default:s=r.top-a,l=r.left+r.width/2-o/2}s+=window.scrollY||document.documentElement.scrollTop||document.body.scrollTop||0,l+=window.scrollX||document.documentElement.scrollLeft||document.body.scrollLeft||0,this.tip.setAttribute(\"style\",\"top: \".concat(s,\"px; left: \").concat(l,\"px\"))}},r.prototype.hide=function(){this.isEnabled&&n(this.tip,{\"fv-plugins-tooltip--hide\":!0})},r}(e.Plugin);return te.Tooltip=r,te}();var ne,re=ee.exports,ie={exports:{}},ae={};ie.exports=function(){if(ne)return ae;ne=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;n.handlers=[],n.timers=new Map;var r=document.createElement(\"div\");return n.defaultEvent=\"oninput\"in r?\"input\":\"keyup\",n.opts=Object.assign({},{delay:0,event:n.defaultEvent,threshold:0},e),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.uninstall=function(){this.handlers.forEach(function(e){return e.element.removeEventListener(e.event,e.handler)}),this.handlers=[],this.timers.forEach(function(e){return window.clearTimeout(e)}),this.timers.clear(),this.core.off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.prepareHandler=function(e,t){var n=this;t.forEach(function(t){var r=[];if(n.opts.event&&!1===n.opts.event[e])r=[];else if(n.opts.event&&n.opts.event[e]&&\"function\"!=typeof n.opts.event[e])r=n.opts.event[e].split(\" \");else if(\"string\"==typeof n.opts.event&&n.opts.event!==n.defaultEvent)r=n.opts.event.split(\" \");else{var i=t.getAttribute(\"type\"),a=t.tagName.toLowerCase();r=[\"radio\"===i||\"checkbox\"===i||\"file\"===i||\"select\"===a?\"change\":n.ieVersion>=10&&t.getAttribute(\"placeholder\")?\"keyup\":n.defaultEvent]}r.forEach(function(r){var i=function(r){return n.handleEvent(r,e,t)};n.handlers.push({element:t,event:r,field:e,handler:i}),t.addEventListener(r,i)})})},n.prototype.handleEvent=function(e,t,n){var r=this;if(this.isEnabled&&this.exceedThreshold(t,n)&&this.core.executeFilter(\"plugins-trigger-should-validate\",!0,[t,n])){var i=function(){return r.core.validateElement(t,n).then(function(i){r.core.emit(\"plugins.trigger.executed\",{element:n,event:e,field:t})})},a=this.opts.delay[t]||this.opts.delay;if(0===a)i();else{var o=this.timers.get(n);o&&window.clearTimeout(o),this.timers.set(n,window.setTimeout(i,1e3*a))}}},n.prototype.onFieldAdded=function(e){this.handlers.filter(function(t){return t.field===e.field}).forEach(function(e){return e.element.removeEventListener(e.event,e.handler)}),this.prepareHandler(e.field,e.elements)},n.prototype.onFieldRemoved=function(e){this.handlers.filter(function(t){return t.field===e.field&&e.elements.indexOf(t.element)>=0}).forEach(function(e){return e.element.removeEventListener(e.event,e.handler)})},n.prototype.exceedThreshold=function(e,t){var n=0!==this.opts.threshold[e]&&0!==this.opts.threshold&&(this.opts.threshold[e]||this.opts.threshold);if(!n)return!0;var r=t.getAttribute(\"type\");return-1!==[\"button\",\"checkbox\",\"file\",\"hidden\",\"image\",\"radio\",\"reset\",\"submit\"].indexOf(r)||this.core.getElementValue(e,t).length>=n},n}(a.Plugin);return ae.Trigger=t,ae}();var oe,se=ie.exports,le={exports:{}},ue={};le.exports=function(){if(oe)return ue;oe=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return ue.between=function(){var e=function(e){return parseFloat(\"\".concat(e).replace(\",\",\".\"))};return{validate:function(r){var i=r.value;if(\"\"===i)return{valid:!0};var a=Object.assign({},{inclusive:!0,message:\"\"},n(r.options)),o=e(a.min),s=e(a.max);return a.inclusive?{message:t(r.l10n?a.message||r.l10n.between.default:a.message,[\"\".concat(o),\"\".concat(s)]),valid:parseFloat(i)>=o&&parseFloat(i)<=s}:{message:t(r.l10n?a.message||r.l10n.between.notInclusive:a.message,[\"\".concat(o),\"\".concat(s)]),valid:parseFloat(i)>o&&parseFloat(i)<s}}}},ue}();var de,ce=le.exports,fe={exports:{}},pe={};fe.exports=(de||(de=1,pe.blank=function(){return{validate:function(e){return{valid:!0}}}}),pe);var he,me=fe.exports,ve={exports:{}},ge={};ve.exports=function(){if(he)return ge;he=1;var e=a.utils.call;return ge.callback=function(){return{validate:function(t){var n=e(t.options.callback,[t]);return\"boolean\"==typeof n?{valid:n}:n}}},ge}();var _e,ye=ve.exports,be={exports:{}},Me={};be.exports=function(){if(_e)return Me;_e=1;var e=a.utils.format;return Me.choice=function(){return{validate:function(t){var n=\"select\"===t.element.tagName.toLowerCase()?t.element.querySelectorAll(\"option:checked\").length:t.elements.filter(function(e){return e.checked}).length,r=t.options.min?\"\".concat(t.options.min):\"\",i=t.options.max?\"\".concat(t.options.max):\"\",a=t.l10n?t.options.message||t.l10n.choice.default:t.options.message,o=!(r&&n<parseInt(r,10)||i&&n>parseInt(i,10));switch(!0){case!!r&&!!i:a=e(t.l10n?t.l10n.choice.between:t.options.message,[r,i]);break;case!!r:a=e(t.l10n?t.l10n.choice.more:t.options.message,r);break;case!!i:a=e(t.l10n?t.l10n.choice.less:t.options.message,i)}return{message:a,valid:o}}}},Me}();var we,ke=be.exports,Le={exports:{}},xe={};Le.exports=function(){if(we)return xe;we=1;var e=a.algorithms.luhn,t={AMERICAN_EXPRESS:{length:[15],prefix:[\"34\",\"37\"]},DANKORT:{length:[16],prefix:[\"5019\"]},DINERS_CLUB:{length:[14],prefix:[\"300\",\"301\",\"302\",\"303\",\"304\",\"305\",\"36\"]},DINERS_CLUB_US:{length:[16],prefix:[\"54\",\"55\"]},DISCOVER:{length:[16],prefix:[\"6011\",\"622126\",\"622127\",\"622128\",\"622129\",\"62213\",\"62214\",\"62215\",\"62216\",\"62217\",\"62218\",\"62219\",\"6222\",\"6223\",\"6224\",\"6225\",\"6226\",\"6227\",\"6228\",\"62290\",\"62291\",\"622920\",\"622921\",\"622922\",\"622923\",\"622924\",\"622925\",\"644\",\"645\",\"646\",\"647\",\"648\",\"649\",\"65\"]},ELO:{length:[16],prefix:[\"4011\",\"4312\",\"4389\",\"4514\",\"4573\",\"4576\",\"5041\",\"5066\",\"5067\",\"509\",\"6277\",\"6362\",\"6363\",\"650\",\"6516\",\"6550\"]},FORBRUGSFORENINGEN:{length:[16],prefix:[\"600722\"]},JCB:{length:[16],prefix:[\"3528\",\"3529\",\"353\",\"354\",\"355\",\"356\",\"357\",\"358\"]},LASER:{length:[16,17,18,19],prefix:[\"6304\",\"6706\",\"6771\",\"6709\"]},MAESTRO:{length:[12,13,14,15,16,17,18,19],prefix:[\"5018\",\"5020\",\"5038\",\"5868\",\"6304\",\"6759\",\"6761\",\"6762\",\"6763\",\"6764\",\"6765\",\"6766\"]},MASTERCARD:{length:[16],prefix:[\"51\",\"52\",\"53\",\"54\",\"55\"]},SOLO:{length:[16,18,19],prefix:[\"6334\",\"6767\"]},UNIONPAY:{length:[16,17,18,19],prefix:[\"622126\",\"622127\",\"622128\",\"622129\",\"62213\",\"62214\",\"62215\",\"62216\",\"62217\",\"62218\",\"62219\",\"6222\",\"6223\",\"6224\",\"6225\",\"6226\",\"6227\",\"6228\",\"62290\",\"62291\",\"622920\",\"622921\",\"622922\",\"622923\",\"622924\",\"622925\"]},VISA:{length:[16],prefix:[\"4\"]},VISA_ELECTRON:{length:[16],prefix:[\"4026\",\"417500\",\"4405\",\"4508\",\"4844\",\"4913\",\"4917\"]}};return xe.CREDIT_CARD_TYPES=t,xe.creditCard=function(){return{validate:function(n){if(\"\"===n.value)return{meta:{type:null},valid:!0};if(/[^0-9-\\s]+/.test(n.value))return{meta:{type:null},valid:!1};var r=n.value.replace(/\\D/g,\"\");if(!e(r))return{meta:{type:null},valid:!1};for(var i=0,a=Object.keys(t);i<a.length;i++){var o=a[i];for(var s in t[o].prefix)if(n.value.substr(0,t[o].prefix[s].length)===t[o].prefix[s]&&-1!==t[o].length.indexOf(r.length))return{meta:{type:o},valid:!0}}return{meta:{type:null},valid:!1}}}},xe}();var Te,Se=Le.exports,De={exports:{}},Ee={};De.exports=function(){if(Te)return Ee;Te=1;var e=a,t=e.utils.format,n=e.utils.isValidDate,r=e.utils.removeUndefined,i=function(e,t,n){var r=t.indexOf(\"YYYY\"),i=t.indexOf(\"MM\"),a=t.indexOf(\"DD\");if(-1===r||-1===i||-1===a)return null;var o=e.split(\" \"),s=o[0].split(n);if(s.length<3)return null;var l=new Date(parseInt(s[r],10),parseInt(s[i],10)-1,parseInt(s[a],10)),u=o.length>2?o[2]:null;if(o.length>1){var d=o[1].split(\":\"),c=d.length>0?parseInt(d[0],10):0;l.setHours(u&&\"PM\"===u.toUpperCase()&&c<12?c+12:c),l.setMinutes(d.length>1?parseInt(d[1],10):0),l.setSeconds(d.length>2?parseInt(d[2],10):0)}return l},o=function(e,t){var n=t.replace(/Y/g,\"y\").replace(/M/g,\"m\").replace(/D/g,\"d\").replace(/:m/g,\":M\").replace(/:mm/g,\":MM\").replace(/:S/,\":s\").replace(/:SS/,\":ss\"),r=e.getDate(),i=r<10?\"0\".concat(r):r,a=e.getMonth()+1,o=a<10?\"0\".concat(a):a,s=\"\".concat(e.getFullYear()).substr(2),l=e.getFullYear(),u=e.getHours()%12||12,d=u<10?\"0\".concat(u):u,c=e.getHours(),f=c<10?\"0\".concat(c):c,p=e.getMinutes(),h=p<10?\"0\".concat(p):p,m=e.getSeconds(),v=m<10?\"0\".concat(m):m,g={H:\"\".concat(c),HH:\"\".concat(f),M:\"\".concat(p),MM:\"\".concat(h),d:\"\".concat(r),dd:\"\".concat(i),h:\"\".concat(u),hh:\"\".concat(d),m:\"\".concat(a),mm:\"\".concat(o),s:\"\".concat(m),ss:\"\".concat(v),yy:\"\".concat(s),yyyy:\"\".concat(l)};return n.replace(/d{1,4}|m{1,4}|yy(?:yy)?|([HhMs])\\1?|\"[^\"]*\"|'[^']*'/g,function(e){return g[e]?g[e]:e.slice(1,e.length-1)})};return Ee.date=function(){return{validate:function(e){if(\"\"===e.value)return{meta:{date:null},valid:!0};var a=Object.assign({},{format:e.element&&\"date\"===e.element.getAttribute(\"type\")?\"YYYY-MM-DD\":\"MM/DD/YYYY\",message:\"\"},r(e.options)),s=e.l10n?e.l10n.date.default:a.message,l={message:\"\".concat(s),meta:{date:null},valid:!1},u=a.format.split(\" \"),d=u.length>1?u[1]:null,c=u.length>2?u[2]:null,f=e.value.split(\" \"),p=f[0],h=f.length>1?f[1]:null,m=f.length>2?f[2]:null;if(u.length!==f.length)return l;var v=a.separator||(-1!==p.indexOf(\"/\")?\"/\":-1!==p.indexOf(\"-\")?\"-\":-1!==p.indexOf(\".\")?\".\":\"/\");if(null===v||-1===p.indexOf(v))return l;var g=p.split(v),_=u[0].split(v);if(g.length!==_.length)return l;var y=g[_.indexOf(\"YYYY\")],b=g[_.indexOf(\"MM\")],M=g[_.indexOf(\"DD\")];if(!/^\\d+$/.test(y)||!/^\\d+$/.test(b)||!/^\\d+$/.test(M)||y.length>4||b.length>2||M.length>2)return l;var w=parseInt(y,10),k=parseInt(b,10),L=parseInt(M,10);if(!n(w,k,L))return l;var x=new Date(w,k-1,L);if(d){var T=h.split(\":\");if(d.split(\":\").length!==T.length)return l;var S=T.length>0?T[0].length<=2&&/^\\d+$/.test(T[0])?parseInt(T[0],10):-1:0,D=T.length>1?T[1].length<=2&&/^\\d+$/.test(T[1])?parseInt(T[1],10):-1:0,E=T.length>2?T[2].length<=2&&/^\\d+$/.test(T[2])?parseInt(T[2],10):-1:0;if(-1===S||-1===D||-1===E)return l;if(E<0||E>60)return l;if(S<0||S>=24||c&&S>12)return l;if(D<0||D>59)return l;x.setHours(m&&\"PM\"===m.toUpperCase()&&S<12?S+12:S),x.setMinutes(D),x.setSeconds(E)}var Y=\"function\"==typeof a.min?a.min():a.min,A=Y instanceof Date?Y:Y?i(Y,_,v):x,O=\"function\"==typeof a.max?a.max():a.max,C=O instanceof Date?O:O?i(O,_,v):x,j=Y instanceof Date?o(A,a.format):Y,P=O instanceof Date?o(C,a.format):O;switch(!0){case!!j&&!P:return{message:t(e.l10n?e.l10n.date.min:s,j),meta:{date:x},valid:x.getTime()>=A.getTime()};case!!P&&!j:return{message:t(e.l10n?e.l10n.date.max:s,P),meta:{date:x},valid:x.getTime()<=C.getTime()};case!!P&&!!j:return{message:t(e.l10n?e.l10n.date.range:s,[j,P]),meta:{date:x},valid:x.getTime()<=C.getTime()&&x.getTime()>=A.getTime()};default:return{message:\"\".concat(s),meta:{date:x},valid:!0}}}}},Ee}();var Ye,Ae=De.exports,Oe={exports:{}},Ce={};Oe.exports=(Ye||(Ye=1,Ce.different=function(){return{validate:function(e){var t=\"function\"==typeof e.options.compare?e.options.compare.call(this):e.options.compare;return{valid:\"\"===t||e.value!==t}}}}),Ce);var je,Pe=Oe.exports,He={exports:{}},Ie={};He.exports=(je||(je=1,Ie.digits=function(){return{validate:function(e){return{valid:\"\"===e.value||/^\\d+$/.test(e.value)}}}}),Ie);var Fe,Ne=He.exports,Re={exports:{}},Ve={};Re.exports=function(){if(Fe)return Ve;Fe=1;var e=a.utils.removeUndefined,t=/^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,n=/^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;return Ve.emailAddress=function(){return{validate:function(r){if(\"\"===r.value)return{valid:!0};var i=Object.assign({},{multiple:!1,requireGlobalDomain:!1,separator:/[,;]/},e(r.options)),a=i.requireGlobalDomain?n:t;if(!0===i.multiple||\"true\"===\"\".concat(i.multiple)){for(var o=i.separator||/[,;]/,s=function(e,t){for(var n=e.split(/\"/),r=n.length,i=[],a=\"\",o=0;o<r;o++)if(o%2==0){var s=n[o].split(t),l=s.length;if(1===l)a+=s[0];else{i.push(a+s[0]);for(var u=1;u<l-1;u++)i.push(s[u]);a=s[l-1]}}else a+='\"'+n[o],o<r-1&&(a+='\"');return i.push(a),i}(r.value,o),l=s.length,u=0;u<l;u++)if(!a.test(s[u]))return{valid:!1};return{valid:!0}}return{valid:a.test(r.value)}}}},Ve}();var $e,ze=Re.exports,We={exports:{}},Ue={};We.exports=function(){if($e)return Ue;$e=1;var e=function(e){return-1===e.indexOf(\".\")?e:e.split(\".\").slice(0,-1).join(\".\")};return Ue.file=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n,r,i=t.options.extension?t.options.extension.toLowerCase().split(\",\").map(function(e){return e.trim()}):[],a=t.options.type?t.options.type.toLowerCase().split(\",\").map(function(e){return e.trim()}):[];if(window.File&&window.FileList&&window.FileReader){var o=t.element.files,s=o.length,l=0;if(t.options.maxFiles&&s>parseInt(\"\".concat(t.options.maxFiles),10))return{meta:{error:\"INVALID_MAX_FILES\"},valid:!1};if(t.options.minFiles&&s<parseInt(\"\".concat(t.options.minFiles),10))return{meta:{error:\"INVALID_MIN_FILES\"},valid:!1};for(var u={},d=0;d<s;d++){if(l+=o[d].size,u={ext:n=o[d].name.substr(o[d].name.lastIndexOf(\".\")+1),file:o[d],size:o[d].size,type:o[d].type},t.options.minSize&&o[d].size<parseInt(\"\".concat(t.options.minSize),10))return{meta:Object.assign({},{error:\"INVALID_MIN_SIZE\"},u),valid:!1};if(t.options.maxSize&&o[d].size>parseInt(\"\".concat(t.options.maxSize),10))return{meta:Object.assign({},{error:\"INVALID_MAX_SIZE\"},u),valid:!1};if(i.length>0&&-1===i.indexOf(n.toLowerCase()))return{meta:Object.assign({},{error:\"INVALID_EXTENSION\"},u),valid:!1};if(a.length>0&&o[d].type&&-1===a.indexOf(o[d].type.toLowerCase()))return{meta:Object.assign({},{error:\"INVALID_TYPE\"},u),valid:!1};if(t.options.validateFileName&&!t.options.validateFileName(e(o[d].name)))return{meta:Object.assign({},{error:\"INVALID_NAME\"},u),valid:!1}}if(t.options.maxTotalSize&&l>parseInt(\"\".concat(t.options.maxTotalSize),10))return{meta:Object.assign({},{error:\"INVALID_MAX_TOTAL_SIZE\",totalSize:l},u),valid:!1};if(t.options.minTotalSize&&l<parseInt(\"\".concat(t.options.minTotalSize),10))return{meta:Object.assign({},{error:\"INVALID_MIN_TOTAL_SIZE\",totalSize:l},u),valid:!1}}else{if(n=t.value.substr(t.value.lastIndexOf(\".\")+1),i.length>0&&-1===i.indexOf(n.toLowerCase()))return{meta:{error:\"INVALID_EXTENSION\",ext:n},valid:!1};if(r=e(t.value),t.options.validateFileName&&!t.options.validateFileName(r))return{meta:{error:\"INVALID_NAME\",name:r},valid:!1}}return{valid:!0}}}},Ue}();var Be,qe=We.exports,Ge={exports:{}},Je={};Ge.exports=function(){if(Be)return Je;Be=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return Je.greaterThan=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var r=Object.assign({},{inclusive:!0,message:\"\"},n(e.options)),i=parseFloat(\"\".concat(r.min).replace(\",\",\".\"));return r.inclusive?{message:t(e.l10n?r.message||e.l10n.greaterThan.default:r.message,\"\".concat(i)),valid:parseFloat(e.value)>=i}:{message:t(e.l10n?r.message||e.l10n.greaterThan.notInclusive:r.message,\"\".concat(i)),valid:parseFloat(e.value)>i}}}},Je}();var Ze,Ke=Ge.exports,Xe={exports:{}},Qe={};Xe.exports=(Ze||(Ze=1,Qe.identical=function(){return{validate:function(e){var t=\"function\"==typeof e.options.compare?e.options.compare.call(this):e.options.compare;return{valid:\"\"===t||e.value===t}}}}),Qe);var et,tt=Xe.exports,nt={exports:{}},rt={};nt.exports=function(){if(et)return rt;et=1;var e=a.utils.removeUndefined;return rt.integer=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=Object.assign({},{decimalSeparator:\".\",thousandsSeparator:\"\"},e(t.options)),r=\".\"===n.decimalSeparator?\"\\\\.\":n.decimalSeparator,i=\".\"===n.thousandsSeparator?\"\\\\.\":n.thousandsSeparator,a=new RegExp(\"^-?[0-9]{1,3}(\".concat(i,\"[0-9]{3})*(\").concat(r,\"[0-9]+)?$\")),o=new RegExp(i,\"g\"),s=\"\".concat(t.value);if(!a.test(s))return{valid:!1};i&&(s=s.replace(o,\"\")),r&&(s=s.replace(r,\".\"));var l=parseFloat(s);return{valid:!isNaN(l)&&isFinite(l)&&Math.floor(l)===l}}}},rt}();var it,at=nt.exports,ot={exports:{}},st={};ot.exports=function(){if(it)return st;it=1;var e=a.utils.removeUndefined;return st.ip=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=Object.assign({},{ipv4:!0,ipv6:!0},e(t.options)),r=/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\/([0-9]|[1-2][0-9]|3[0-2]))?$/,i=/^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*(\\/(\\d|\\d\\d|1[0-1]\\d|12[0-8]))?$/;switch(!0){case n.ipv4&&!n.ipv6:return{message:t.l10n?n.message||t.l10n.ip.ipv4:n.message,valid:r.test(t.value)};case!n.ipv4&&n.ipv6:return{message:t.l10n?n.message||t.l10n.ip.ipv6:n.message,valid:i.test(t.value)};case n.ipv4&&n.ipv6:default:return{message:t.l10n?n.message||t.l10n.ip.default:n.message,valid:r.test(t.value)||i.test(t.value)}}}}},st}();var lt,ut=ot.exports,dt={exports:{}},ct={};dt.exports=function(){if(lt)return ct;lt=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return ct.lessThan=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var r=Object.assign({},{inclusive:!0,message:\"\"},n(e.options)),i=parseFloat(\"\".concat(r.max).replace(\",\",\".\"));return r.inclusive?{message:t(e.l10n?r.message||e.l10n.lessThan.default:r.message,\"\".concat(i)),valid:parseFloat(e.value)<=i}:{message:t(e.l10n?r.message||e.l10n.lessThan.notInclusive:r.message,\"\".concat(i)),valid:parseFloat(e.value)<i}}}},ct}();var ft,pt=dt.exports,ht={exports:{}},mt={};ht.exports=(ft||(ft=1,mt.notEmpty=function(){return{validate:function(e){var t=!!e.options&&!!e.options.trim,n=e.value;return{valid:!t&&\"\"!==n||t&&\"\"!==n&&\"\"!==n.trim()}}}}),mt);var vt,gt=ht.exports,_t={exports:{}},yt={};_t.exports=function(){if(vt)return yt;vt=1;var e=a.utils.removeUndefined;return yt.numeric=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=Object.assign({},{decimalSeparator:\".\",thousandsSeparator:\"\"},e(t.options)),r=\"\".concat(t.value);r.substr(0,1)===n.decimalSeparator?r=\"0\".concat(n.decimalSeparator).concat(r.substr(1)):r.substr(0,2)===\"-\".concat(n.decimalSeparator)&&(r=\"-0\".concat(n.decimalSeparator).concat(r.substr(2)));var i=\".\"===n.decimalSeparator?\"\\\\.\":n.decimalSeparator,a=\".\"===n.thousandsSeparator?\"\\\\.\":n.thousandsSeparator,o=new RegExp(\"^-?[0-9]{1,3}(\".concat(a,\"[0-9]{3})*(\").concat(i,\"[0-9]+)?$\")),s=new RegExp(a,\"g\");if(!o.test(r))return{valid:!1};a&&(r=r.replace(s,\"\")),i&&(r=r.replace(i,\".\"));var l=parseFloat(r);return{valid:!isNaN(l)&&isFinite(l)}}}},yt}();var bt,Mt=_t.exports,wt={exports:{}},kt={};wt.exports=function(){if(bt)return kt;bt=1;var e=a.utils.call;return kt.promise=function(){return{validate:function(t){return e(t.options.promise,[t])}}},kt}();var Lt,xt=wt.exports,Tt={exports:{}},St={};Tt.exports=(Lt||(Lt=1,St.regexp=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var t=e.options.regexp;if(t instanceof RegExp)return{valid:t.test(e.value)};var n=t.toString();return{valid:(e.options.flags?new RegExp(n,e.options.flags):new RegExp(n)).test(e.value)}}}}),St);var Dt,Et=Tt.exports,Yt={exports:{}},At={};Yt.exports=function(){if(Dt)return At;Dt=1;var e=a,t=e.utils.fetch,n=e.utils.removeUndefined;return At.remote=function(){var e={crossDomain:!1,data:{},headers:{},method:\"GET\",validKey:\"valid\"};return{validate:function(r){if(\"\"===r.value)return Promise.resolve({valid:!0});var i=Object.assign({},e,n(r.options)),a=i.data;\"function\"==typeof i.data&&(a=i.data.call(this,r)),\"string\"==typeof a&&(a=JSON.parse(a)),a[i.name||r.field]=r.value;var o=\"function\"==typeof i.url?i.url.call(this,r):i.url;return t(o,{crossDomain:i.crossDomain,headers:i.headers,method:i.method,params:a}).then(function(e){return Promise.resolve({message:e.message,meta:e,valid:\"true\"===\"\".concat(e[i.validKey])})}).catch(function(e){return Promise.reject({valid:!1})})}}},At}();var Ot,Ct=Yt.exports,jt={exports:{}},Pt={};jt.exports=function(){if(Ot)return Pt;Ot=1;var e=a.utils.removeUndefined;return Pt.stringCase=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=Object.assign({},{case:\"lower\"},e(t.options)),r=(n.case||\"lower\").toLowerCase();return{message:n.message||(t.l10n?\"upper\"===r?t.l10n.stringCase.upper:t.l10n.stringCase.default:n.message),valid:\"upper\"===r?t.value===t.value.toUpperCase():t.value===t.value.toLowerCase()}}}},Pt}();var Ht,It=jt.exports,Ft={exports:{}},Nt={};Ft.exports=function(){if(Ht)return Nt;Ht=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return Nt.stringLength=function(){return{validate:function(e){var r=Object.assign({},{message:\"\",trim:!1,utf8Bytes:!1},n(e.options)),i=!0===r.trim||\"true\"===\"\".concat(r.trim)?e.value.trim():e.value;if(\"\"===i)return{valid:!0};var a=r.min?\"\".concat(r.min):\"\",o=r.max?\"\".concat(r.max):\"\",s=r.utf8Bytes?function(e){for(var t=e.length,n=e.length-1;n>=0;n--){var r=e.charCodeAt(n);r>127&&r<=2047?t++:r>2047&&r<=65535&&(t+=2),r>=56320&&r<=57343&&n--}return t}(i):i.length,l=!0,u=e.l10n?r.message||e.l10n.stringLength.default:r.message;switch((a&&s<parseInt(a,10)||o&&s>parseInt(o,10))&&(l=!1),!0){case!!a&&!!o:u=t(e.l10n?r.message||e.l10n.stringLength.between:r.message,[a,o]);break;case!!a:u=t(e.l10n?r.message||e.l10n.stringLength.more:r.message,\"\".concat(parseInt(a,10)));break;case!!o:u=t(e.l10n?r.message||e.l10n.stringLength.less:r.message,\"\".concat(parseInt(o,10)))}return{message:u,valid:l}}}},Nt}();var Rt,Vt=Ft.exports,$t={exports:{}},zt={};$t.exports=function(){if(Rt)return zt;Rt=1;var e=a.utils.removeUndefined;return zt.uri=function(){var t={allowEmptyProtocol:!1,allowLocal:!1,protocol:\"http, https, ftp\"};return{validate:function(n){if(\"\"===n.value)return{valid:!0};var r=Object.assign({},t,e(n.options)),i=!0===r.allowLocal||\"true\"===\"\".concat(r.allowLocal),a=!0===r.allowEmptyProtocol||\"true\"===\"\".concat(r.allowEmptyProtocol),o=r.protocol.split(\",\").join(\"|\").replace(/\\s/g,\"\");return{valid:new RegExp(\"^(?:(?:\"+o+\")://)\"+(a?\"?\":\"\")+\"(?:\\\\S+(?::\\\\S*)?@)?(?:\"+(i?\"\":\"(?!(?:10|127)(?:\\\\.\\\\d{1,3}){3})(?!(?:169\\\\.254|192\\\\.168)(?:\\\\.\\\\d{1,3}){2})(?!172\\\\.(?:1[6-9]|2\\\\d|3[0-1])(?:\\\\.\\\\d{1,3}){2})\")+\"(?:[1-9]\\\\d?|1\\\\d\\\\d|2[01]\\\\d|22[0-3])(?:\\\\.(?:1?\\\\d{1,2}|2[0-4]\\\\d|25[0-5])){2}(?:\\\\.(?:[1-9]\\\\d?|1\\\\d\\\\d|2[0-4]\\\\d|25[0-4]))|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-?)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-?)*[a-z\\\\u00a1-\\\\uffff0-9])*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,}))\"+(i?\"?\":\"\")+\")(?::\\\\d{2,5})?(?:/[^\\\\s]*)?$\",\"i\").test(n.value)}}}},zt}();var Wt=$t.exports,Ut={Alias:u.Alias,Aria:p.Aria,Declarative:g.Declarative,DefaultSubmit:M.DefaultSubmit,Dependency:x.Dependency,Excluded:E.Excluded,FieldStatus:C.FieldStatus,Framework:V.Framework,Icon:U.Icon,Message:N.Message,Sequence:J.Sequence,SubmitButton:Q.SubmitButton,Tooltip:re.Tooltip,Trigger:se.Trigger},Bt={between:ce.between,blank:me.blank,callback:ye.callback,choice:ke.choice,creditCard:Se.creditCard,date:Ae.date,different:Pe.different,digits:Ne.digits,emailAddress:ze.emailAddress,file:qe.file,greaterThan:Ke.greaterThan,identical:tt.identical,integer:at.integer,ip:ut.ip,lessThan:pt.lessThan,notEmpty:gt.notEmpty,numeric:Mt.numeric,promise:xt.promise,regexp:Et.regexp,remote:Ct.remote,stringCase:It.stringCase,stringLength:Vt.stringLength,uri:Wt.uri};e.Plugin=a.Plugin,e.algorithms=a.algorithms,e.formValidation=function(e,t){var n=a.formValidation(e,t);return Object.keys(Bt).forEach(function(e){return n.registerValidator(e,Bt[e])}),n},e.plugins=Ut,e.utils=a.utils,e.validators=Bt}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],t):t((e=\"undefined\"!=typeof globalThis?globalThis:e||self).FormValidation={})}(this,function(e){\"use strict\";var t,n={exports:{}},r={};n.exports=function(){if(t)return r;t=1;var e={luhn:function(e){for(var t=e.length,n=[[0,1,2,3,4,5,6,7,8,9],[0,2,4,6,8,1,3,5,7,9]],r=0,i=0;t--;)i+=n[r][parseInt(e.charAt(t),10)],r=1-r;return i%10==0&&i>0},mod11And10:function(e){for(var t=e.length,n=5,r=0;r<t;r++)n=(2*(n||10)%11+parseInt(e.charAt(r),10))%10;return 1===n},mod37And36:function(e,t){void 0===t&&(t=\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\");for(var n=e.length,r=t.length,i=Math.floor(r/2),a=0;a<n;a++)i=(2*(i||r)%(r+1)+t.indexOf(e.charAt(a)))%r;return 1===i},mod97And10:function(e){for(var t=function(e){return e.split(\"\").map(function(e){var t=e.charCodeAt(0);return t>=65&&t<=90?t-55:e}).join(\"\").split(\"\").map(function(e){return parseInt(e,10)})}(e),n=0,r=t.length,i=0;i<r-1;++i)n=10*(n+t[i])%97;return(n+=t[r-1])%97==1},verhoeff:function(e){for(var t=[[0,1,2,3,4,5,6,7,8,9],[1,2,3,4,0,6,7,8,9,5],[2,3,4,0,1,7,8,9,5,6],[3,4,0,1,2,8,9,5,6,7],[4,0,1,2,3,9,5,6,7,8],[5,9,8,7,6,0,4,3,2,1],[6,5,9,8,7,1,0,4,3,2],[7,6,5,9,8,2,1,0,4,3],[8,7,6,5,9,3,2,1,0,4],[9,8,7,6,5,4,3,2,1,0]],n=[[0,1,2,3,4,5,6,7,8,9],[1,5,7,6,2,8,3,0,9,4],[5,8,0,3,7,9,6,1,4,2],[8,9,1,6,0,4,3,5,2,7],[9,4,5,3,1,2,6,8,7,0],[4,2,8,6,5,7,3,9,0,1],[2,7,9,3,8,0,6,4,1,5],[7,0,4,6,9,1,3,2,5,8]],r=e.reverse(),i=0,a=0;a<r.length;a++)i=t[i][n[a%8][r[a]]];return 0===i}},n=function(){function e(e,t){this.fields={},this.elements={},this.ee={fns:{},clear:function(){this.fns={}},emit:function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];(this.fns[e]||[]).map(function(e){return e.apply(e,t)})},off:function(e,t){if(this.fns[e]){var n=this.fns[e].indexOf(t);n>=0&&this.fns[e].splice(n,1)}},on:function(e,t){(this.fns[e]=this.fns[e]||[]).push(t)}},this.filter={filters:{},add:function(e,t){(this.filters[e]=this.filters[e]||[]).push(t)},clear:function(){this.filters={}},execute:function(e,t,n){if(!this.filters[e]||!this.filters[e].length)return t;for(var r=t,i=this.filters[e],a=i.length,o=0;o<a;o++)r=i[o].apply(r,n);return r},remove:function(e,t){this.filters[e]&&(this.filters[e]=this.filters[e].filter(function(e){return e!==t}))}},this.plugins={},this.results=new Map,this.validators={},this.form=e,this.fields=t}return e.prototype.on=function(e,t){return this.ee.on(e,t),this},e.prototype.off=function(e,t){return this.ee.off(e,t),this},e.prototype.emit=function(e){for(var t,n=[],r=1;r<arguments.length;r++)n[r-1]=arguments[r];return(t=this.ee).emit.apply(t,function(e,t,n){if(n||2===arguments.length)for(var r,i=0,a=t.length;i<a;i++)!r&&i in t||(r||(r=Array.prototype.slice.call(t,0,i)),r[i]=t[i]);return e.concat(r||Array.prototype.slice.call(t))}([e],n,!1)),this},e.prototype.registerPlugin=function(e,t){if(this.plugins[e])throw new Error(\"The plguin \".concat(e,\" is registered\"));return t.setCore(this),t.install(),this.plugins[e]=t,this},e.prototype.deregisterPlugin=function(e){var t=this.plugins[e];return t&&t.uninstall(),delete this.plugins[e],this},e.prototype.enablePlugin=function(e){var t=this.plugins[e];return t&&t.enable(),this},e.prototype.disablePlugin=function(e){var t=this.plugins[e];return t&&t.disable(),this},e.prototype.isPluginEnabled=function(e){var t=this.plugins[e];return!!t&&t.isPluginEnabled()},e.prototype.registerValidator=function(e,t){if(this.validators[e])throw new Error(\"The validator \".concat(e,\" is registered\"));return this.validators[e]=t,this},e.prototype.registerFilter=function(e,t){return this.filter.add(e,t),this},e.prototype.deregisterFilter=function(e,t){return this.filter.remove(e,t),this},e.prototype.executeFilter=function(e,t,n){return this.filter.execute(e,t,n)},e.prototype.addField=function(e,t){var n=Object.assign({},{selector:\"\",validators:{}},t);return this.fields[e]=this.fields[e]?{selector:n.selector||this.fields[e].selector,validators:Object.assign({},this.fields[e].validators,n.validators)}:n,this.elements[e]=this.queryElements(e),this.emit(\"core.field.added\",{elements:this.elements[e],field:e,options:this.fields[e]}),this},e.prototype.removeField=function(e){if(!this.fields[e])throw new Error(\"The field \".concat(e,\" validators are not defined. Please ensure the field is added first\"));var t=this.elements[e],n=this.fields[e];return delete this.elements[e],delete this.fields[e],this.emit(\"core.field.removed\",{elements:t,field:e,options:n}),this},e.prototype.validate=function(){var e=this;return this.emit(\"core.form.validating\",{formValidation:this}),this.filter.execute(\"validate-pre\",Promise.resolve(),[]).then(function(){return Promise.all(Object.keys(e.fields).map(function(t){return e.validateField(t)})).then(function(t){switch(!0){case-1!==t.indexOf(\"Invalid\"):return e.emit(\"core.form.invalid\",{formValidation:e}),Promise.resolve(\"Invalid\");case-1!==t.indexOf(\"NotValidated\"):return e.emit(\"core.form.notvalidated\",{formValidation:e}),Promise.resolve(\"NotValidated\");default:return e.emit(\"core.form.valid\",{formValidation:e}),Promise.resolve(\"Valid\")}})})},e.prototype.validateField=function(e){var t=this,n=this.results.get(e);if(\"Valid\"===n||\"Invalid\"===n)return Promise.resolve(n);this.emit(\"core.field.validating\",e);var r=this.elements[e];if(0===r.length)return this.emit(\"core.field.valid\",e),Promise.resolve(\"Valid\");var i=r[0].getAttribute(\"type\");return\"radio\"===i||\"checkbox\"===i||1===r.length?this.validateElement(e,r[0]):Promise.all(r.map(function(n){return t.validateElement(e,n)})).then(function(n){switch(!0){case-1!==n.indexOf(\"Invalid\"):return t.emit(\"core.field.invalid\",e),t.results.set(e,\"Invalid\"),Promise.resolve(\"Invalid\");case-1!==n.indexOf(\"NotValidated\"):return t.emit(\"core.field.notvalidated\",e),t.results.delete(e),Promise.resolve(\"NotValidated\");default:return t.emit(\"core.field.valid\",e),t.results.set(e,\"Valid\"),Promise.resolve(\"Valid\")}})},e.prototype.validateElement=function(e,t){var n=this;this.results.delete(e);var r=this.elements[e];if(this.filter.execute(\"element-ignored\",!1,[e,t,r]))return this.emit(\"core.element.ignored\",{element:t,elements:r,field:e}),Promise.resolve(\"Ignored\");var i=this.fields[e].validators;this.emit(\"core.element.validating\",{element:t,elements:r,field:e});var a=Object.keys(i).map(function(r){return function(){return n.executeValidator(e,t,r,i[r])}});return this.waterfall(a).then(function(i){var a=-1===i.indexOf(\"Invalid\");n.emit(\"core.element.validated\",{element:t,elements:r,field:e,valid:a});var o=t.getAttribute(\"type\");return\"radio\"!==o&&\"checkbox\"!==o&&1!==r.length||n.emit(a?\"core.field.valid\":\"core.field.invalid\",e),Promise.resolve(a?\"Valid\":\"Invalid\")}).catch(function(i){return n.emit(\"core.element.notvalidated\",{element:t,elements:r,field:e}),Promise.resolve(i)})},e.prototype.executeValidator=function(e,t,n,r){var i=this,a=this.elements[e],o=this.filter.execute(\"validator-name\",n,[n,e]);if(r.message=this.filter.execute(\"validator-message\",r.message,[this.locale,e,o]),!this.validators[o]||!1===r.enabled)return this.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:this.normalizeResult(e,o,{valid:!0}),validator:o}),Promise.resolve(\"Valid\");var s=this.validators[o],l=this.getElementValue(e,t,o);if(!this.filter.execute(\"field-should-validate\",!0,[e,t,l,n]))return this.emit(\"core.validator.notvalidated\",{element:t,elements:a,field:e,validator:n}),Promise.resolve(\"NotValidated\");this.emit(\"core.validator.validating\",{element:t,elements:a,field:e,validator:n});var u=s().validate({element:t,elements:a,field:e,l10n:this.localization,options:r,value:l});if(\"function\"==typeof u.then)return u.then(function(r){var o=i.normalizeResult(e,n,r);return i.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:o,validator:n}),o.valid?\"Valid\":\"Invalid\"});var d=this.normalizeResult(e,n,u);return this.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:d,validator:n}),Promise.resolve(d.valid?\"Valid\":\"Invalid\")},e.prototype.getElementValue=function(e,t,n){var r=function(e,t,n,r){var i=(n.getAttribute(\"type\")||\"\").toLowerCase(),a=n.tagName.toLowerCase();if(\"textarea\"===a)return n.value;if(\"select\"===a){var o=n,s=o.selectedIndex;return s>=0?o.options.item(s).value:\"\"}if(\"input\"===a){if(\"radio\"===i||\"checkbox\"===i){var l=r.filter(function(e){return e.checked}).length;return 0===l?\"\":l+\"\"}return n.value}return\"\"}(this.form,0,t,this.elements[e]);return this.filter.execute(\"field-value\",r,[r,e,t,n])},e.prototype.getElements=function(e){return this.elements[e]},e.prototype.getFields=function(){return this.fields},e.prototype.getFormElement=function(){return this.form},e.prototype.getLocale=function(){return this.locale},e.prototype.getPlugin=function(e){return this.plugins[e]},e.prototype.updateFieldStatus=function(e,t,n){var r=this,i=this.elements[e],a=i[0].getAttribute(\"type\");if((\"radio\"===a||\"checkbox\"===a?[i[0]]:i).forEach(function(i){return r.updateElementStatus(e,i,t,n)}),n)\"Invalid\"===t&&(this.emit(\"core.field.invalid\",e),this.results.set(e,\"Invalid\"));else switch(t){case\"NotValidated\":this.emit(\"core.field.notvalidated\",e),this.results.delete(e);break;case\"Validating\":this.emit(\"core.field.validating\",e),this.results.delete(e);break;case\"Valid\":this.emit(\"core.field.valid\",e),this.results.set(e,\"Valid\");break;case\"Invalid\":this.emit(\"core.field.invalid\",e),this.results.set(e,\"Invalid\")}return this},e.prototype.updateElementStatus=function(e,t,n,r){var i=this,a=this.elements[e],o=this.fields[e].validators,s=r?[r]:Object.keys(o);switch(n){case\"NotValidated\":s.forEach(function(n){return i.emit(\"core.validator.notvalidated\",{element:t,elements:a,field:e,validator:n})}),this.emit(\"core.element.notvalidated\",{element:t,elements:a,field:e});break;case\"Validating\":s.forEach(function(n){return i.emit(\"core.validator.validating\",{element:t,elements:a,field:e,validator:n})}),this.emit(\"core.element.validating\",{element:t,elements:a,field:e});break;case\"Valid\":s.forEach(function(n){return i.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:{message:o[n].message,valid:!0},validator:n})}),this.emit(\"core.element.validated\",{element:t,elements:a,field:e,valid:!0});break;case\"Invalid\":s.forEach(function(n){return i.emit(\"core.validator.validated\",{element:t,elements:a,field:e,result:{message:o[n].message,valid:!1},validator:n})}),this.emit(\"core.element.validated\",{element:t,elements:a,field:e,valid:!1})}return this},e.prototype.resetForm=function(e){var t=this;return Object.keys(this.fields).forEach(function(n){return t.resetField(n,e)}),this.emit(\"core.form.reset\",{formValidation:this,reset:e}),this},e.prototype.resetField=function(e,t){if(t){var n=this.elements[e],r=n[0].getAttribute(\"type\");n.forEach(function(e){\"radio\"===r||\"checkbox\"===r?(e.removeAttribute(\"selected\"),e.removeAttribute(\"checked\"),e.checked=!1):(e.setAttribute(\"value\",\"\"),(e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement)&&(e.value=\"\"))})}return this.updateFieldStatus(e,\"NotValidated\"),this.emit(\"core.field.reset\",{field:e,reset:t}),this},e.prototype.revalidateField=function(e){return this.fields[e]?(this.updateFieldStatus(e,\"NotValidated\"),this.validateField(e)):Promise.resolve(\"Ignored\")},e.prototype.disableValidator=function(e,t){if(!this.fields[e])return this;var n=this.elements[e];return this.toggleValidator(!1,e,t),this.emit(\"core.validator.disabled\",{elements:n,field:e,formValidation:this,validator:t}),this},e.prototype.enableValidator=function(e,t){if(!this.fields[e])return this;var n=this.elements[e];return this.toggleValidator(!0,e,t),this.emit(\"core.validator.enabled\",{elements:n,field:e,formValidation:this,validator:t}),this},e.prototype.updateValidatorOption=function(e,t,n,r){return this.fields[e]&&this.fields[e].validators&&this.fields[e].validators[t]&&(this.fields[e].validators[t][n]=r),this},e.prototype.setFieldOptions=function(e,t){return this.fields[e]=t,this},e.prototype.destroy=function(){var e=this;return Object.keys(this.plugins).forEach(function(t){return e.plugins[t].uninstall()}),this.ee.clear(),this.filter.clear(),this.results.clear(),this.plugins={},this},e.prototype.setLocale=function(e,t){return this.locale=e,this.localization=t,this},e.prototype.waterfall=function(e){return e.reduce(function(e,t){return e.then(function(e){return t().then(function(t){return e.push(t),e})})},Promise.resolve([]))},e.prototype.queryElements=function(e){var t=this.fields[e].selector?\"#\"===this.fields[e].selector.charAt(0)?'[id=\"'.concat(this.fields[e].selector.substring(1),'\"]'):this.fields[e].selector:'[name=\"'.concat(e.replace(/\"/g,'\\\\\"'),'\"]');return[].slice.call(this.form.querySelectorAll(t))},e.prototype.normalizeResult=function(e,t,n){var r=this.fields[e].validators[t];return Object.assign({},n,{message:n.message||(r?r.message:\"\")||(this.localization&&this.localization[t]&&this.localization[t].default?this.localization[t].default:\"\")||\"The field \".concat(e,\" is not valid\")})},e.prototype.toggleValidator=function(e,t,n){var r=this,i=this.fields[t].validators;return n&&i&&i[n]?this.fields[t].validators[n].enabled=e:n||Object.keys(i).forEach(function(n){return r.fields[t].validators[n].enabled=e}),this.updateFieldStatus(t,\"NotValidated\",n)},e}(),i=function(){function e(e){this.opts=e,this.isEnabled=!0}return e.prototype.setCore=function(e){return this.core=e,this},e.prototype.enable=function(){return this.isEnabled=!0,this.onEnabled(),this},e.prototype.disable=function(){return this.isEnabled=!1,this.onDisabled(),this},e.prototype.isPluginEnabled=function(){return this.isEnabled},e.prototype.onEnabled=function(){},e.prototype.onDisabled=function(){},e.prototype.install=function(){},e.prototype.uninstall=function(){},e}(),a=function(e,t){var n=e.matches||e.webkitMatchesSelector||e.mozMatchesSelector||e.msMatchesSelector;return n?n.call(e,t):[].slice.call(e.parentElement.querySelectorAll(t)).indexOf(e)>=0},o={call:function(e,t){if(\"function\"==typeof e)return e.apply(this,t);if(\"string\"==typeof e){var n=e;\"()\"===n.substring(n.length-2)&&(n=n.substring(0,n.length-2));for(var r=n.split(\".\"),i=r.pop(),a=window,o=0,s=r;o<s.length;o++)a=a[s[o]];return void 0===a[i]?null:a[i].apply(this,t)}},classSet:function(e,t){var n=[],r=[];Object.keys(t).forEach(function(e){e&&(t[e]?n.push(e):r.push(e))}),r.forEach(function(t){return function(e,t){t.split(\" \").forEach(function(t){e.classList?e.classList.remove(t):e.className=e.className.replace(t,\"\")})}(e,t)}),n.forEach(function(t){return function(e,t){t.split(\" \").forEach(function(t){e.classList?e.classList.add(t):\" \".concat(e.className,\" \").indexOf(\" \".concat(t,\" \"))&&(e.className+=\" \".concat(t))})}(e,t)})},closest:function(e,t){for(var n=e;n&&!a(n,t);)n=n.parentElement;return n},fetch:function(e,t){return new Promise(function(n,r){var i,a=Object.assign({},{crossDomain:!1,headers:{},method:\"GET\",params:{}},t),o=Object.keys(a.params).map(function(e){return\"\".concat(encodeURIComponent(e),\"=\").concat(encodeURIComponent(a.params[e]))}).join(\"&\"),s=e.indexOf(\"?\")>-1,l=\"GET\"===a.method?\"\".concat(e).concat(s?\"&\":\"?\").concat(o):e;if(a.crossDomain){var u=document.createElement(\"script\"),d=\"___FormValidationFetch_\".concat(Array(12).fill(\"\").map(function(e){return Math.random().toString(36).charAt(2)}).join(\"\"),\"___\");window[d]=function(e){delete window[d],n(e)},u.src=\"\".concat(l).concat(s?\"&\":\"?\",\"callback=\").concat(d),u.async=!0,u.addEventListener(\"load\",function(){u.parentNode.removeChild(u)}),u.addEventListener(\"error\",function(){return r}),document.head.appendChild(u)}else{var c=new XMLHttpRequest;c.open(a.method,l),c.setRequestHeader(\"X-Requested-With\",\"XMLHttpRequest\"),\"POST\"===a.method&&c.setRequestHeader(\"Content-Type\",\"application/x-www-form-urlencoded\"),Object.keys(a.headers).forEach(function(e){return c.setRequestHeader(e,a.headers[e])}),c.addEventListener(\"load\",function(){n(JSON.parse(this.responseText))}),c.addEventListener(\"error\",function(){return r}),c.send((i=a.params,Object.keys(i).map(function(e){return\"\".concat(encodeURIComponent(e),\"=\").concat(encodeURIComponent(i[e]))}).join(\"&\")))}})},format:function(e,t){var n=Array.isArray(t)?t:[t],r=e;return n.forEach(function(e){r=r.replace(\"%s\",e)}),r},hasClass:function(e,t){return e.classList?e.classList.contains(t):new RegExp(\"(^| )\".concat(t,\"( |$)\"),\"gi\").test(e.className)},isValidDate:function(e,t,n,r){if(isNaN(e)||isNaN(t)||isNaN(n))return!1;if(e<1e3||e>9999||t<=0||t>12)return!1;if(n<=0||n>[31,e%400==0||e%100!=0&&e%4==0?29:28,31,30,31,30,31,31,30,31,30,31][t-1])return!1;if(!0===r){var i=new Date,a=i.getFullYear(),o=i.getMonth(),s=i.getDate();return e<a||e===a&&t-1<o||e===a&&t-1===o&&n<s}return!0},removeUndefined:function(e){return e?Object.entries(e).reduce(function(e,t){var n=t[0],r=t[1];return void 0===r||(e[n]=r),e},{}):{}}};return r.Plugin=i,r.algorithms=e,r.formValidation=function(e,t){var r=Object.assign({},{fields:{},locale:\"en_US\",plugins:{},init:function(e){}},t),i=new n(e,r.fields);return i.setLocale(r.locale,r.localization),Object.keys(r.plugins).forEach(function(e){return i.registerPlugin(e,r.plugins[e])}),r.init(i),Object.keys(r.fields).forEach(function(e){return i.addField(e,r.fields[e])}),i},r.utils=o,r}();var i,a=n.exports,o={exports:{}},s={};o.exports=function(){if(i)return s;i=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.opts=e||{},n.validatorNameFilter=n.getValidatorName.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.registerFilter(\"validator-name\",this.validatorNameFilter)},n.prototype.uninstall=function(){this.core.deregisterFilter(\"validator-name\",this.validatorNameFilter)},n.prototype.getValidatorName=function(e,t){return this.isEnabled&&this.opts[e]||e},n}(a.Plugin);return s.Alias=t,s}();var l,u=o.exports,d={exports:{}},c={};d.exports=function(){if(l)return c;l=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(){var e=t.call(this,{})||this;return e.elementValidatedHandler=e.onElementValidated.bind(e),e.fieldValidHandler=e.onFieldValid.bind(e),e.fieldInvalidHandler=e.onFieldInvalid.bind(e),e.messageDisplayedHandler=e.onMessageDisplayed.bind(e),e}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.on(\"core.field.valid\",this.fieldValidHandler).on(\"core.field.invalid\",this.fieldInvalidHandler).on(\"core.element.validated\",this.elementValidatedHandler).on(\"plugins.message.displayed\",this.messageDisplayedHandler)},n.prototype.uninstall=function(){this.core.off(\"core.field.valid\",this.fieldValidHandler).off(\"core.field.invalid\",this.fieldInvalidHandler).off(\"core.element.validated\",this.elementValidatedHandler).off(\"plugins.message.displayed\",this.messageDisplayedHandler)},n.prototype.onElementValidated=function(e){e.valid&&(e.element.setAttribute(\"aria-invalid\",\"false\"),e.element.removeAttribute(\"aria-describedby\"))},n.prototype.onFieldValid=function(e){var t=this.core.getElements(e);t&&t.forEach(function(e){e.setAttribute(\"aria-invalid\",\"false\"),e.removeAttribute(\"aria-describedby\")})},n.prototype.onFieldInvalid=function(e){var t=this.core.getElements(e);t&&t.forEach(function(e){return e.setAttribute(\"aria-invalid\",\"true\")})},n.prototype.onMessageDisplayed=function(e){e.messageElement.setAttribute(\"role\",\"alert\"),e.messageElement.setAttribute(\"aria-hidden\",\"false\");var t=this.core.getElements(e.field),n=t.indexOf(e.element),r=\"js-fv-\".concat(e.field,\"-\").concat(n,\"-\").concat(Date.now(),\"-message\");e.messageElement.setAttribute(\"id\",r),e.element.setAttribute(\"aria-describedby\",r);var i=e.element.getAttribute(\"type\");\"radio\"!==i&&\"checkbox\"!==i||t.forEach(function(e){return e.setAttribute(\"aria-describedby\",r)})},n}(a.Plugin);return c.Aria=t,c}();var f,p=d.exports,h={exports:{}},m={};h.exports=function(){if(f)return m;f=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.addedFields=new Map,n.opts=Object.assign({},{html5Input:!1,pluginPrefix:\"data-fvp-\",prefix:\"data-fv-\"},e),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){var e=this;this.parsePlugins();var t=this.parseOptions();Object.keys(t).forEach(function(n){e.addedFields.has(n)||e.addedFields.set(n,!0),e.core.addField(n,t[n])}),this.core.on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.uninstall=function(){this.addedFields.clear(),this.core.off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.onFieldAdded=function(e){var t=this,n=e.elements;n&&0!==n.length&&!this.addedFields.has(e.field)&&(this.addedFields.set(e.field,!0),n.forEach(function(n){var r=t.parseElement(n);if(!t.isEmptyOption(r)){var i={selector:e.options.selector,validators:Object.assign({},e.options.validators||{},r.validators)};t.core.setFieldOptions(e.field,i)}}))},n.prototype.onFieldRemoved=function(e){e.field&&this.addedFields.has(e.field)&&this.addedFields.delete(e.field)},n.prototype.parseOptions=function(){var e=this,t=this.opts.prefix,n={},r=this.core.getFields(),i=this.core.getFormElement();return[].slice.call(i.querySelectorAll(\"[name], [\".concat(t,\"field]\"))).forEach(function(r){var i=e.parseElement(r);if(!e.isEmptyOption(i)){var a=r.getAttribute(\"name\")||r.getAttribute(\"\".concat(t,\"field\"));n[a]=Object.assign({},n[a],i)}}),Object.keys(n).forEach(function(e){Object.keys(n[e].validators).forEach(function(t){n[e].validators[t].enabled=n[e].validators[t].enabled||!1,r[e]&&r[e].validators&&r[e].validators[t]&&Object.assign(n[e].validators[t],r[e].validators[t])})}),Object.assign({},r,n)},n.prototype.createPluginInstance=function(e,t){for(var n=e.split(\".\"),r=window||this,i=0,a=n.length;i<a;i++)r=r[n[i]];if(\"function\"!=typeof r)throw new Error(\"the plugin \".concat(e,\" doesn't exist\"));return new r(t)},n.prototype.parsePlugins=function(){for(var e,t=this,n=this.core.getFormElement(),r=new RegExp(\"^\".concat(this.opts.pluginPrefix,\"([a-z0-9-]+)(___)*([a-z0-9-]+)*$\")),i=n.attributes.length,a={},o=0;o<i;o++){var s=n.attributes[o].name,l=n.attributes[o].value,u=r.exec(s);if(u&&4===u.length){var d=this.toCamelCase(u[1]);a[d]=Object.assign({},u[3]?((e={})[this.toCamelCase(u[3])]=l,e):{enabled:\"\"===l||\"true\"===l},a[d])}}Object.keys(a).forEach(function(e){var n=a[e],r=n.enabled,i=n.class;if(r&&i){delete n.enabled,delete n.clazz;var o=t.createPluginInstance(i,n);t.core.registerPlugin(e,o)}})},n.prototype.isEmptyOption=function(e){var t=e.validators;return 0===Object.keys(t).length&&t.constructor===Object},n.prototype.parseElement=function(e){for(var t=new RegExp(\"^\".concat(this.opts.prefix,\"([a-z0-9-]+)(___)*([a-z0-9-]+)*$\")),n=e.attributes.length,r={},i=e.getAttribute(\"type\"),a=0;a<n;a++){var o=e.attributes[a].name,s=e.attributes[a].value;if(this.opts.html5Input)switch(!0){case\"minlength\"===o:r.stringLength=Object.assign({},{enabled:!0,min:parseInt(s,10)},r.stringLength);break;case\"maxlength\"===o:r.stringLength=Object.assign({},{enabled:!0,max:parseInt(s,10)},r.stringLength);break;case\"pattern\"===o:r.regexp=Object.assign({},{enabled:!0,regexp:s},r.regexp);break;case\"required\"===o:r.notEmpty=Object.assign({},{enabled:!0},r.notEmpty);break;case\"type\"===o&&\"color\"===s:r.color=Object.assign({},{enabled:!0,type:\"hex\"},r.color);break;case\"type\"===o&&\"email\"===s:r.emailAddress=Object.assign({},{enabled:!0},r.emailAddress);break;case\"type\"===o&&\"url\"===s:r.uri=Object.assign({},{enabled:!0},r.uri);break;case\"type\"===o&&\"range\"===s:r.between=Object.assign({},{enabled:!0,max:parseFloat(e.getAttribute(\"max\")),min:parseFloat(e.getAttribute(\"min\"))},r.between);break;case\"min\"===o&&\"date\"!==i&&\"range\"!==i:r.greaterThan=Object.assign({},{enabled:!0,min:parseFloat(s)},r.greaterThan);break;case\"max\"===o&&\"date\"!==i&&\"range\"!==i:r.lessThan=Object.assign({},{enabled:!0,max:parseFloat(s)},r.lessThan)}var l=t.exec(o);if(l&&4===l.length){var u=this.toCamelCase(l[1]);r[u]||(r[u]={}),l[3]?r[u][this.toCamelCase(l[3])]=this.normalizeValue(s):!0===r[u].enabled&&!1===r[u].enabled||(r[u].enabled=\"\"===s||\"true\"===s)}}return{validators:r}},n.prototype.normalizeValue=function(e){return\"true\"===e||\"\"===e||\"false\"!==e&&e},n.prototype.toUpperCase=function(e){return e.charAt(1).toUpperCase()},n.prototype.toCamelCase=function(e){return e.replace(/-./g,this.toUpperCase)},n}(a.Plugin);return m.Declarative=t,m}();var v,g=h.exports,_={exports:{}},y={};_.exports=function(){if(v)return y;v=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(){var e=t.call(this,{})||this;return e.onValidHandler=e.onFormValid.bind(e),e}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){if(this.core.getFormElement().querySelectorAll('[type=\"submit\"][name=\"submit\"]').length)throw new Error(\"Do not use `submit` for the name attribute of submit button\");this.core.on(\"core.form.valid\",this.onValidHandler)},n.prototype.uninstall=function(){this.core.off(\"core.form.valid\",this.onValidHandler)},n.prototype.onFormValid=function(){var e=this.core.getFormElement();this.isEnabled&&e instanceof HTMLFormElement&&e.submit()},n}(a.Plugin);return y.DefaultSubmit=t,y}();var b,M=_.exports,w={exports:{}},k={};w.exports=function(){if(b)return k;b=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.opts=e||{},n.triggerExecutedHandler=n.onTriggerExecuted.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.on(\"plugins.trigger.executed\",this.triggerExecutedHandler)},n.prototype.uninstall=function(){this.core.off(\"plugins.trigger.executed\",this.triggerExecutedHandler)},n.prototype.onTriggerExecuted=function(e){if(this.isEnabled&&this.opts[e.field])for(var t=0,n=this.opts[e.field].split(\" \");t<n.length;t++){var r=n[t].trim();this.opts[r]&&this.core.revalidateField(r)}},n}(a.Plugin);return k.Dependency=t,k}();var L,x=w.exports,T={exports:{}},S={};T.exports=function(){if(L)return S;L=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.removeUndefined,r=function(e){function r(t){var i=e.call(this,t)||this;return i.opts=Object.assign({},{excluded:r.defaultIgnore},n(t)),i.ignoreValidationFilter=i.ignoreValidation.bind(i),i}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.defaultIgnore=function(e,t,n){var r=!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length),i=t.getAttribute(\"disabled\");return\"\"===i||\"disabled\"===i||\"hidden\"===t.getAttribute(\"type\")||!r},r.prototype.install=function(){this.core.registerFilter(\"element-ignored\",this.ignoreValidationFilter)},r.prototype.uninstall=function(){this.core.deregisterFilter(\"element-ignored\",this.ignoreValidationFilter)},r.prototype.ignoreValidation=function(e,t,n){return!!this.isEnabled&&this.opts.excluded.apply(this,[e,t,n])},r}(e.Plugin);return S.Excluded=r,S}();var D,E=T.exports,Y={exports:{}},A={};Y.exports=function(){if(D)return A;D=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.statuses=new Map,n.opts=Object.assign({},{onStatusChanged:function(){}},e),n.elementValidatingHandler=n.onElementValidating.bind(n),n.elementValidatedHandler=n.onElementValidated.bind(n),n.elementNotValidatedHandler=n.onElementNotValidated.bind(n),n.elementIgnoredHandler=n.onElementIgnored.bind(n),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.on(\"core.element.validating\",this.elementValidatingHandler).on(\"core.element.validated\",this.elementValidatedHandler).on(\"core.element.notvalidated\",this.elementNotValidatedHandler).on(\"core.element.ignored\",this.elementIgnoredHandler).on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.uninstall=function(){this.statuses.clear(),this.core.off(\"core.element.validating\",this.elementValidatingHandler).off(\"core.element.validated\",this.elementValidatedHandler).off(\"core.element.notvalidated\",this.elementNotValidatedHandler).off(\"core.element.ignored\",this.elementIgnoredHandler).off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.areFieldsValid=function(){return Array.from(this.statuses.values()).every(function(e){return\"Valid\"===e||\"NotValidated\"===e||\"Ignored\"===e})},n.prototype.getStatuses=function(){return this.isEnabled?this.statuses:new Map},n.prototype.onFieldAdded=function(e){this.statuses.set(e.field,\"NotValidated\")},n.prototype.onFieldRemoved=function(e){this.statuses.has(e.field)&&this.statuses.delete(e.field),this.handleStatusChanged(this.areFieldsValid())},n.prototype.onElementValidating=function(e){this.statuses.set(e.field,\"Validating\"),this.handleStatusChanged(!1)},n.prototype.onElementValidated=function(e){this.statuses.set(e.field,e.valid?\"Valid\":\"Invalid\"),e.valid?this.handleStatusChanged(this.areFieldsValid()):this.handleStatusChanged(!1)},n.prototype.onElementNotValidated=function(e){this.statuses.set(e.field,\"NotValidated\"),this.handleStatusChanged(!1)},n.prototype.onElementIgnored=function(e){this.statuses.set(e.field,\"Ignored\"),this.handleStatusChanged(this.areFieldsValid())},n.prototype.handleStatusChanged=function(e){this.isEnabled&&this.opts.onStatusChanged(e)},n}(a.Plugin);return A.FieldStatus=t,A}();var O,C=Y.exports,j={exports:{}},P={},H={exports:{}},I={};H.exports=function(){if(O)return I;O=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.classSet,r=function(e){function r(t){var n=e.call(this,t)||this;return n.useDefaultContainer=!1,n.messages=new Map,n.defaultContainer=document.createElement(\"div\"),n.useDefaultContainer=!t||!t.container,n.opts=Object.assign({},{container:function(e,t){return n.defaultContainer}},t),n.elementIgnoredHandler=n.onElementIgnored.bind(n),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n.validatorValidatedHandler=n.onValidatorValidated.bind(n),n.validatorNotValidatedHandler=n.onValidatorNotValidated.bind(n),n}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.getClosestContainer=function(e,t,n){for(var r=e;r&&r!==t&&(r=r.parentElement,!n.test(r.className)););return r},r.prototype.install=function(){this.useDefaultContainer&&this.core.getFormElement().appendChild(this.defaultContainer),this.core.on(\"core.element.ignored\",this.elementIgnoredHandler).on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler).on(\"core.validator.validated\",this.validatorValidatedHandler).on(\"core.validator.notvalidated\",this.validatorNotValidatedHandler)},r.prototype.uninstall=function(){this.useDefaultContainer&&this.core.getFormElement().removeChild(this.defaultContainer),this.messages.forEach(function(e){return e.parentNode.removeChild(e)}),this.messages.clear(),this.core.off(\"core.element.ignored\",this.elementIgnoredHandler).off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler).off(\"core.validator.validated\",this.validatorValidatedHandler).off(\"core.validator.notvalidated\",this.validatorNotValidatedHandler)},r.prototype.onEnabled=function(){this.messages.forEach(function(e,t,r){n(t,{\"fv-plugins-message-container--enabled\":!0,\"fv-plugins-message-container--disabled\":!1})})},r.prototype.onDisabled=function(){this.messages.forEach(function(e,t,r){n(t,{\"fv-plugins-message-container--enabled\":!1,\"fv-plugins-message-container--disabled\":!0})})},r.prototype.onFieldAdded=function(e){var t=this,n=e.elements;n&&(n.forEach(function(e){var n=t.messages.get(e);n&&(n.parentNode.removeChild(n),t.messages.delete(e))}),this.prepareFieldContainer(e.field,n))},r.prototype.onFieldRemoved=function(e){var t=this;if(e.elements.length&&e.field){var n=e.elements[0].getAttribute(\"type\");(\"radio\"===n||\"checkbox\"===n?[e.elements[0]]:e.elements).forEach(function(e){if(t.messages.has(e)){var n=t.messages.get(e);n.parentNode.removeChild(n),t.messages.delete(e)}})}},r.prototype.prepareFieldContainer=function(e,t){var n=this;if(t.length){var r=t[0].getAttribute(\"type\");\"radio\"===r||\"checkbox\"===r?this.prepareElementContainer(e,t[0],t):t.forEach(function(r){return n.prepareElementContainer(e,r,t)})}},r.prototype.prepareElementContainer=function(e,t,r){var i;if(\"string\"==typeof this.opts.container){var a=\"#\"===this.opts.container.charAt(0)?'[id=\"'.concat(this.opts.container.substring(1),'\"]'):this.opts.container;i=this.core.getFormElement().querySelector(a)}else i=this.opts.container(e,t);var o=document.createElement(\"div\");i.appendChild(o),n(o,{\"fv-plugins-message-container\":!0,\"fv-plugins-message-container--enabled\":this.isEnabled,\"fv-plugins-message-container--disabled\":!this.isEnabled}),this.core.emit(\"plugins.message.placed\",{element:t,elements:r,field:e,messageElement:o}),this.messages.set(t,o)},r.prototype.getMessage=function(e){return\"string\"==typeof e.message?e.message:e.message[this.core.getLocale()]},r.prototype.onValidatorValidated=function(e){var t,r=e.elements,i=e.element.getAttribute(\"type\"),a=(\"radio\"===i||\"checkbox\"===i)&&r.length>0?r[0]:e.element;if(this.messages.has(a)){var o=this.messages.get(a),s=o.querySelector('[data-field=\"'.concat(e.field.replace(/\"/g,'\\\\\"'),'\"][data-validator=\"').concat(e.validator.replace(/\"/g,'\\\\\"'),'\"]'));if(s||e.result.valid)s&&!e.result.valid?(s.innerHTML=this.getMessage(e.result),this.core.emit(\"plugins.message.displayed\",{element:e.element,field:e.field,message:e.result.message,messageElement:s,meta:e.result.meta,validator:e.validator})):s&&e.result.valid&&o.removeChild(s);else{var l=document.createElement(\"div\");l.innerHTML=this.getMessage(e.result),l.setAttribute(\"data-field\",e.field),l.setAttribute(\"data-validator\",e.validator),this.opts.clazz&&n(l,((t={})[this.opts.clazz]=!0,t)),o.appendChild(l),this.core.emit(\"plugins.message.displayed\",{element:e.element,field:e.field,message:e.result.message,messageElement:l,meta:e.result.meta,validator:e.validator})}}},r.prototype.onValidatorNotValidated=function(e){var t=e.elements,n=e.element.getAttribute(\"type\"),r=\"radio\"===n||\"checkbox\"===n?t[0]:e.element;if(this.messages.has(r)){var i=this.messages.get(r),a=i.querySelector('[data-field=\"'.concat(e.field.replace(/\"/g,'\\\\\"'),'\"][data-validator=\"').concat(e.validator.replace(/\"/g,'\\\\\"'),'\"]'));a&&i.removeChild(a)}},r.prototype.onElementIgnored=function(e){var t=e.elements,n=e.element.getAttribute(\"type\"),r=\"radio\"===n||\"checkbox\"===n?t[0]:e.element;if(this.messages.has(r)){var i=this.messages.get(r);[].slice.call(i.querySelectorAll('[data-field=\"'.concat(e.field.replace(/\"/g,'\\\\\"'),'\"]'))).forEach(function(e){i.removeChild(e)})}},r}(e.Plugin);return I.Message=r,I}();var F,N=H.exports;\n/** \n     * FormValidation (https://formvalidation.io)\n     * The best validation library for JavaScript\n     * (c) 2013 - 2023 Nguyen Huu Phuoc <me@phuoc.ng>\n     *\n     * @license https://formvalidation.io/license\n     * @package @form-validation/plugin-framework\n     * @version 2.4.0\n     */j.exports=function(){if(F)return P;F=1;var e=a,t=N,n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},n(e,t)},r=e.utils.classSet,i=e.utils.closest,o=function(e){function a(t){var n=e.call(this,t)||this;return n.results=new Map,n.containers=new Map,n.opts=Object.assign({},{defaultMessageContainer:!0,eleInvalidClass:\"\",eleValidClass:\"\",rowClasses:\"\",rowValidatingClass:\"\"},t),n.elementIgnoredHandler=n.onElementIgnored.bind(n),n.elementValidatingHandler=n.onElementValidating.bind(n),n.elementValidatedHandler=n.onElementValidated.bind(n),n.elementNotValidatedHandler=n.onElementNotValidated.bind(n),n.iconPlacedHandler=n.onIconPlaced.bind(n),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n.messagePlacedHandler=n.onMessagePlaced.bind(n),n}return function(e,t){if(\"function\"!=typeof t&&null!==t)throw new TypeError(\"Class extends value \"+String(t)+\" is not a constructor or null\");function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}(a,e),a.prototype.install=function(){var e,n=this;r(this.core.getFormElement(),((e={})[this.opts.formClass]=!0,e[\"fv-plugins-framework\"]=!0,e)),this.core.on(\"core.element.ignored\",this.elementIgnoredHandler).on(\"core.element.validating\",this.elementValidatingHandler).on(\"core.element.validated\",this.elementValidatedHandler).on(\"core.element.notvalidated\",this.elementNotValidatedHandler).on(\"plugins.icon.placed\",this.iconPlacedHandler).on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler),this.opts.defaultMessageContainer&&(this.core.registerPlugin(a.MESSAGE_PLUGIN,new t.Message({clazz:this.opts.messageClass,container:function(e,r){var a=\"string\"==typeof n.opts.rowSelector?n.opts.rowSelector:n.opts.rowSelector(e,r),o=i(r,a);return t.Message.getClosestContainer(r,o,n.opts.rowPattern)}})),this.core.on(\"plugins.message.placed\",this.messagePlacedHandler))},a.prototype.uninstall=function(){var e;this.results.clear(),this.containers.clear(),r(this.core.getFormElement(),((e={})[this.opts.formClass]=!1,e[\"fv-plugins-framework\"]=!1,e)),this.core.off(\"core.element.ignored\",this.elementIgnoredHandler).off(\"core.element.validating\",this.elementValidatingHandler).off(\"core.element.validated\",this.elementValidatedHandler).off(\"core.element.notvalidated\",this.elementNotValidatedHandler).off(\"plugins.icon.placed\",this.iconPlacedHandler).off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler),this.opts.defaultMessageContainer&&(this.core.deregisterPlugin(a.MESSAGE_PLUGIN),this.core.off(\"plugins.message.placed\",this.messagePlacedHandler))},a.prototype.onEnabled=function(){var e;r(this.core.getFormElement(),((e={})[this.opts.formClass]=!0,e)),this.opts.defaultMessageContainer&&this.core.enablePlugin(a.MESSAGE_PLUGIN)},a.prototype.onDisabled=function(){var e;r(this.core.getFormElement(),((e={})[this.opts.formClass]=!1,e)),this.opts.defaultMessageContainer&&this.core.disablePlugin(a.MESSAGE_PLUGIN)},a.prototype.onIconPlaced=function(e){},a.prototype.onMessagePlaced=function(e){},a.prototype.onFieldAdded=function(e){var t=this,n=e.elements;n&&(n.forEach(function(e){var n,i=t.containers.get(e);i&&(r(i,((n={})[t.opts.rowInvalidClass]=!1,n[t.opts.rowValidatingClass]=!1,n[t.opts.rowValidClass]=!1,n[\"fv-plugins-icon-container\"]=!1,n)),t.containers.delete(e))}),this.prepareFieldContainer(e.field,n))},a.prototype.onFieldRemoved=function(e){var t=this;e.elements.forEach(function(e){var n,i=t.containers.get(e);i&&r(i,((n={})[t.opts.rowInvalidClass]=!1,n[t.opts.rowValidatingClass]=!1,n[t.opts.rowValidClass]=!1,n))})},a.prototype.prepareFieldContainer=function(e,t){var n=this;if(t.length){var r=t[0].getAttribute(\"type\");\"radio\"===r||\"checkbox\"===r?this.prepareElementContainer(e,t[0]):t.forEach(function(t){return n.prepareElementContainer(e,t)})}},a.prototype.prepareElementContainer=function(e,t){var n,a=\"string\"==typeof this.opts.rowSelector?this.opts.rowSelector:this.opts.rowSelector(e,t),o=i(t,a);o!==t&&(r(o,((n={})[this.opts.rowClasses]=!0,n[\"fv-plugins-icon-container\"]=!0,n)),this.containers.set(t,o))},a.prototype.onElementValidating=function(e){this.removeClasses(e.element,e.elements)},a.prototype.onElementNotValidated=function(e){this.removeClasses(e.element,e.elements)},a.prototype.onElementIgnored=function(e){this.removeClasses(e.element,e.elements)},a.prototype.removeClasses=function(e,t){var n,i=this,a=e.getAttribute(\"type\"),o=\"radio\"===a||\"checkbox\"===a?t[0]:e;t.forEach(function(e){var t;r(e,((t={})[i.opts.eleValidClass]=!1,t[i.opts.eleInvalidClass]=!1,t))});var s=this.containers.get(o);s&&r(s,((n={})[this.opts.rowInvalidClass]=!1,n[this.opts.rowValidatingClass]=!1,n[this.opts.rowValidClass]=!1,n))},a.prototype.onElementValidated=function(e){var t,n,i=this,a=e.elements,o=e.element.getAttribute(\"type\"),s=\"radio\"===o||\"checkbox\"===o?a[0]:e.element;a.forEach(function(t){var n;r(t,((n={})[i.opts.eleValidClass]=e.valid,n[i.opts.eleInvalidClass]=!e.valid,n))});var l=this.containers.get(s);if(l)if(e.valid){this.results.delete(s);var u=!0;this.containers.forEach(function(e,t){e===l&&!1===i.results.get(t)&&(u=!1)}),u&&r(l,((n={})[this.opts.rowInvalidClass]=!1,n[this.opts.rowValidatingClass]=!1,n[this.opts.rowValidClass]=!0,n))}else this.results.set(s,!1),r(l,((t={})[this.opts.rowInvalidClass]=!0,t[this.opts.rowValidatingClass]=!1,t[this.opts.rowValidClass]=!1,t))},a.MESSAGE_PLUGIN=\"___frameworkMessage\",a}(e.Plugin);return P.Framework=o,P}();var R,V=j.exports,$={exports:{}},z={};$.exports=function(){if(R)return z;R=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.classSet,r=function(e){function r(t){var n=e.call(this,t)||this;return n.icons=new Map,n.opts=Object.assign({},{invalid:\"fv-plugins-icon--invalid\",onPlaced:function(){},onSet:function(){},valid:\"fv-plugins-icon--valid\",validating:\"fv-plugins-icon--validating\"},t),n.elementValidatingHandler=n.onElementValidating.bind(n),n.elementValidatedHandler=n.onElementValidated.bind(n),n.elementNotValidatedHandler=n.onElementNotValidated.bind(n),n.elementIgnoredHandler=n.onElementIgnored.bind(n),n.fieldAddedHandler=n.onFieldAdded.bind(n),n}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.prototype.install=function(){this.core.on(\"core.element.validating\",this.elementValidatingHandler).on(\"core.element.validated\",this.elementValidatedHandler).on(\"core.element.notvalidated\",this.elementNotValidatedHandler).on(\"core.element.ignored\",this.elementIgnoredHandler).on(\"core.field.added\",this.fieldAddedHandler)},r.prototype.uninstall=function(){this.icons.forEach(function(e){return e.parentNode.removeChild(e)}),this.icons.clear(),this.core.off(\"core.element.validating\",this.elementValidatingHandler).off(\"core.element.validated\",this.elementValidatedHandler).off(\"core.element.notvalidated\",this.elementNotValidatedHandler).off(\"core.element.ignored\",this.elementIgnoredHandler).off(\"core.field.added\",this.fieldAddedHandler)},r.prototype.onEnabled=function(){this.icons.forEach(function(e,t,r){n(t,{\"fv-plugins-icon--enabled\":!0,\"fv-plugins-icon--disabled\":!1})})},r.prototype.onDisabled=function(){this.icons.forEach(function(e,t,r){n(t,{\"fv-plugins-icon--enabled\":!1,\"fv-plugins-icon--disabled\":!0})})},r.prototype.onFieldAdded=function(e){var t=this,n=e.elements;n&&(n.forEach(function(e){var n=t.icons.get(e);n&&(n.parentNode.removeChild(n),t.icons.delete(e))}),this.prepareFieldIcon(e.field,n))},r.prototype.prepareFieldIcon=function(e,t){var n=this;if(t.length){var r=t[0].getAttribute(\"type\");\"radio\"===r||\"checkbox\"===r?this.prepareElementIcon(e,t[0]):t.forEach(function(t){return n.prepareElementIcon(e,t)})}},r.prototype.prepareElementIcon=function(e,t){var r=document.createElement(\"i\");r.setAttribute(\"data-field\",e),t.parentNode.insertBefore(r,t.nextSibling),n(r,{\"fv-plugins-icon\":!0,\"fv-plugins-icon--enabled\":this.isEnabled,\"fv-plugins-icon--disabled\":!this.isEnabled});var i={classes:{invalid:this.opts.invalid,valid:this.opts.valid,validating:this.opts.validating},element:t,field:e,iconElement:r};this.core.emit(\"plugins.icon.placed\",i),this.opts.onPlaced(i),this.icons.set(t,r)},r.prototype.onElementValidating=function(e){var t,n=this.setClasses(e.field,e.element,e.elements,((t={})[this.opts.invalid]=!1,t[this.opts.valid]=!1,t[this.opts.validating]=!0,t)),r={element:e.element,field:e.field,iconElement:n,status:\"Validating\"};this.core.emit(\"plugins.icon.set\",r),this.opts.onSet(r)},r.prototype.onElementValidated=function(e){var t,n=this.setClasses(e.field,e.element,e.elements,((t={})[this.opts.invalid]=!e.valid,t[this.opts.valid]=e.valid,t[this.opts.validating]=!1,t)),r={element:e.element,field:e.field,iconElement:n,status:e.valid?\"Valid\":\"Invalid\"};this.core.emit(\"plugins.icon.set\",r),this.opts.onSet(r)},r.prototype.onElementNotValidated=function(e){var t,n=this.setClasses(e.field,e.element,e.elements,((t={})[this.opts.invalid]=!1,t[this.opts.valid]=!1,t[this.opts.validating]=!1,t)),r={element:e.element,field:e.field,iconElement:n,status:\"NotValidated\"};this.core.emit(\"plugins.icon.set\",r),this.opts.onSet(r)},r.prototype.onElementIgnored=function(e){var t,n=this.setClasses(e.field,e.element,e.elements,((t={})[this.opts.invalid]=!1,t[this.opts.valid]=!1,t[this.opts.validating]=!1,t)),r={element:e.element,field:e.field,iconElement:n,status:\"Ignored\"};this.core.emit(\"plugins.icon.set\",r),this.opts.onSet(r)},r.prototype.setClasses=function(e,t,r,i){var a=t.getAttribute(\"type\"),o=\"radio\"===a||\"checkbox\"===a?r[0]:t;if(this.icons.has(o)){var s=this.icons.get(o);return n(s,i),s}return null},r}(e.Plugin);return z.Icon=r,z}();var W,U=$.exports,B={exports:{}},q={};B.exports=function(){if(W)return q;W=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.removeUndefined,r=function(e){function r(t){var r=e.call(this,t)||this;return r.invalidFields=new Map,r.opts=Object.assign({},{enabled:!0},n(t)),r.validatorHandler=r.onValidatorValidated.bind(r),r.shouldValidateFilter=r.shouldValidate.bind(r),r.fieldAddedHandler=r.onFieldAdded.bind(r),r.elementNotValidatedHandler=r.onElementNotValidated.bind(r),r.elementValidatingHandler=r.onElementValidating.bind(r),r}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.prototype.install=function(){this.core.on(\"core.validator.validated\",this.validatorHandler).on(\"core.field.added\",this.fieldAddedHandler).on(\"core.element.notvalidated\",this.elementNotValidatedHandler).on(\"core.element.validating\",this.elementValidatingHandler).registerFilter(\"field-should-validate\",this.shouldValidateFilter)},r.prototype.uninstall=function(){this.invalidFields.clear(),this.core.off(\"core.validator.validated\",this.validatorHandler).off(\"core.field.added\",this.fieldAddedHandler).off(\"core.element.notvalidated\",this.elementNotValidatedHandler).off(\"core.element.validating\",this.elementValidatingHandler).deregisterFilter(\"field-should-validate\",this.shouldValidateFilter)},r.prototype.shouldValidate=function(e,t,n,r){return!this.isEnabled||!((!0===this.opts.enabled||!0===this.opts.enabled[e])&&this.invalidFields.has(t)&&this.invalidFields.get(t).length&&-1===this.invalidFields.get(t).indexOf(r))},r.prototype.onValidatorValidated=function(e){var t=this.invalidFields.has(e.element)?this.invalidFields.get(e.element):[],n=t.indexOf(e.validator);e.result.valid&&n>=0?t.splice(n,1):e.result.valid||-1!==n||t.push(e.validator),this.invalidFields.set(e.element,t)},r.prototype.onFieldAdded=function(e){e.elements&&this.clearInvalidFields(e.elements)},r.prototype.onElementNotValidated=function(e){this.clearInvalidFields(e.elements)},r.prototype.onElementValidating=function(e){this.clearInvalidFields(e.elements)},r.prototype.clearInvalidFields=function(e){var t=this;e.forEach(function(e){return t.invalidFields.delete(e)})},r}(e.Plugin);return q.Sequence=r,q}();var G,J=B.exports,Z={exports:{}},K={};Z.exports=function(){if(G)return K;G=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;return n.isFormValid=!1,n.isButtonClicked=!1,n.opts=Object.assign({},{aspNetButton:!1,buttons:function(e){return[].slice.call(e.querySelectorAll('[type=\"submit\"]:not([formnovalidate])'))},liveMode:!0},e),n.submitHandler=n.handleSubmitEvent.bind(n),n.buttonClickHandler=n.handleClickEvent.bind(n),n.ignoreValidationFilter=n.ignoreValidation.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){var e=this;if(this.core.getFormElement()instanceof HTMLFormElement){var t=this.core.getFormElement();this.submitButtons=this.opts.buttons(t),t.setAttribute(\"novalidate\",\"novalidate\"),t.addEventListener(\"submit\",this.submitHandler),this.hiddenClickedEle=document.createElement(\"input\"),this.hiddenClickedEle.setAttribute(\"type\",\"hidden\"),t.appendChild(this.hiddenClickedEle),this.submitButtons.forEach(function(t){t.addEventListener(\"click\",e.buttonClickHandler)}),this.core.registerFilter(\"element-ignored\",this.ignoreValidationFilter)}},n.prototype.uninstall=function(){var e=this,t=this.core.getFormElement();t instanceof HTMLFormElement&&t.removeEventListener(\"submit\",this.submitHandler),this.submitButtons.forEach(function(t){t.removeEventListener(\"click\",e.buttonClickHandler)}),this.hiddenClickedEle.parentElement.removeChild(this.hiddenClickedEle),this.core.deregisterFilter(\"element-ignored\",this.ignoreValidationFilter)},n.prototype.handleSubmitEvent=function(e){this.validateForm(e)},n.prototype.handleClickEvent=function(e){var t=e.currentTarget;if(this.isButtonClicked=!0,t instanceof HTMLElement)if(this.opts.aspNetButton&&!0===this.isFormValid);else{this.core.getFormElement().removeEventListener(\"submit\",this.submitHandler),this.clickedButton=e.target;var n=this.clickedButton.getAttribute(\"name\"),r=this.clickedButton.getAttribute(\"value\");n&&r&&(this.hiddenClickedEle.setAttribute(\"name\",n),this.hiddenClickedEle.setAttribute(\"value\",r)),this.validateForm(e)}},n.prototype.validateForm=function(e){var t=this;this.isEnabled&&(e.preventDefault(),this.core.validate().then(function(e){\"Valid\"===e&&t.opts.aspNetButton&&!t.isFormValid&&t.clickedButton&&(t.isFormValid=!0,t.clickedButton.removeEventListener(\"click\",t.buttonClickHandler),t.clickedButton.click())}))},n.prototype.ignoreValidation=function(e,t,n){return!!this.isEnabled&&!this.opts.liveMode&&!this.isButtonClicked},n}(a.Plugin);return K.SubmitButton=t,K}();var X,Q=Z.exports,ee={exports:{}},te={};ee.exports=function(){if(X)return te;X=1;var e=a,t=function(e,n){return t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},t(e,n)},n=e.utils.classSet,r=function(e){function r(t){var n=e.call(this,t)||this;return n.messages=new Map,n.opts=Object.assign({},{placement:\"top\",trigger:\"click\"},t),n.iconPlacedHandler=n.onIconPlaced.bind(n),n.validatorValidatedHandler=n.onValidatorValidated.bind(n),n.elementValidatedHandler=n.onElementValidated.bind(n),n.documentClickHandler=n.onDocumentClicked.bind(n),n}return function(e,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(r,e),r.prototype.install=function(){var e;this.tip=document.createElement(\"div\"),n(this.tip,((e={\"fv-plugins-tooltip\":!0})[\"fv-plugins-tooltip--\".concat(this.opts.placement)]=!0,e)),document.body.appendChild(this.tip),this.core.on(\"plugins.icon.placed\",this.iconPlacedHandler).on(\"core.validator.validated\",this.validatorValidatedHandler).on(\"core.element.validated\",this.elementValidatedHandler),\"click\"===this.opts.trigger&&document.addEventListener(\"click\",this.documentClickHandler)},r.prototype.uninstall=function(){this.messages.clear(),document.body.removeChild(this.tip),this.core.off(\"plugins.icon.placed\",this.iconPlacedHandler).off(\"core.validator.validated\",this.validatorValidatedHandler).off(\"core.element.validated\",this.elementValidatedHandler),\"click\"===this.opts.trigger&&document.removeEventListener(\"click\",this.documentClickHandler)},r.prototype.onIconPlaced=function(e){var t=this;n(e.iconElement,{\"fv-plugins-tooltip-icon\":!0}),\"hover\"===this.opts.trigger?(e.iconElement.addEventListener(\"mouseenter\",function(n){return t.show(e.element,n)}),e.iconElement.addEventListener(\"mouseleave\",function(e){return t.hide()})):e.iconElement.addEventListener(\"click\",function(n){return t.show(e.element,n)})},r.prototype.onValidatorValidated=function(e){if(!e.result.valid){var t=e.elements,n=e.element.getAttribute(\"type\"),r=\"radio\"===n||\"checkbox\"===n?t[0]:e.element,i=\"string\"==typeof e.result.message?e.result.message:e.result.message[this.core.getLocale()];this.messages.set(r,i)}},r.prototype.onElementValidated=function(e){if(e.valid){var t=e.elements,n=e.element.getAttribute(\"type\"),r=\"radio\"===n||\"checkbox\"===n?t[0]:e.element;this.messages.delete(r)}},r.prototype.onDocumentClicked=function(e){this.hide()},r.prototype.show=function(e,t){if(this.isEnabled&&(t.preventDefault(),t.stopPropagation(),this.messages.has(e))){n(this.tip,{\"fv-plugins-tooltip--hide\":!1}),this.tip.innerHTML='<div class=\"fv-plugins-tooltip__content\">'.concat(this.messages.get(e),\"</div>\");var r=t.target.getBoundingClientRect(),i=this.tip.getBoundingClientRect(),a=i.height,o=i.width,s=0,l=0;switch(this.opts.placement){case\"bottom\":s=r.top+r.height,l=r.left+r.width/2-o/2;break;case\"bottom-left\":s=r.top+r.height,l=r.left;break;case\"bottom-right\":s=r.top+r.height,l=r.left+r.width-o;break;case\"left\":s=r.top+r.height/2-a/2,l=r.left-o;break;case\"right\":s=r.top+r.height/2-a/2,l=r.left+r.width;break;case\"top-left\":s=r.top-a,l=r.left;break;case\"top-right\":s=r.top-a,l=r.left+r.width-o;break;default:s=r.top-a,l=r.left+r.width/2-o/2}s+=window.scrollY||document.documentElement.scrollTop||document.body.scrollTop||0,l+=window.scrollX||document.documentElement.scrollLeft||document.body.scrollLeft||0,this.tip.setAttribute(\"style\",\"top: \".concat(s,\"px; left: \").concat(l,\"px\"))}},r.prototype.hide=function(){this.isEnabled&&n(this.tip,{\"fv-plugins-tooltip--hide\":!0})},r}(e.Plugin);return te.Tooltip=r,te}();var ne,re=ee.exports,ie={exports:{}},ae={};ie.exports=function(){if(ne)return ae;ne=1;var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)},t=function(t){function n(e){var n=t.call(this,e)||this;n.handlers=[],n.timers=new Map;var r=document.createElement(\"div\");return n.defaultEvent=\"oninput\"in r?\"input\":\"keyup\",n.opts=Object.assign({},{delay:0,event:n.defaultEvent,threshold:0},e),n.fieldAddedHandler=n.onFieldAdded.bind(n),n.fieldRemovedHandler=n.onFieldRemoved.bind(n),n}return function(t,n){if(\"function\"!=typeof n&&null!==n)throw new TypeError(\"Class extends value \"+String(n)+\" is not a constructor or null\");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}(n,t),n.prototype.install=function(){this.core.on(\"core.field.added\",this.fieldAddedHandler).on(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.uninstall=function(){this.handlers.forEach(function(e){return e.element.removeEventListener(e.event,e.handler)}),this.handlers=[],this.timers.forEach(function(e){return window.clearTimeout(e)}),this.timers.clear(),this.core.off(\"core.field.added\",this.fieldAddedHandler).off(\"core.field.removed\",this.fieldRemovedHandler)},n.prototype.prepareHandler=function(e,t){var n=this;t.forEach(function(t){var r=[];if(n.opts.event&&!1===n.opts.event[e])r=[];else if(n.opts.event&&n.opts.event[e]&&\"function\"!=typeof n.opts.event[e])r=n.opts.event[e].split(\" \");else if(\"string\"==typeof n.opts.event&&n.opts.event!==n.defaultEvent)r=n.opts.event.split(\" \");else{var i=t.getAttribute(\"type\"),a=t.tagName.toLowerCase();r=[\"radio\"===i||\"checkbox\"===i||\"file\"===i||\"select\"===a?\"change\":n.ieVersion>=10&&t.getAttribute(\"placeholder\")?\"keyup\":n.defaultEvent]}r.forEach(function(r){var i=function(r){return n.handleEvent(r,e,t)};n.handlers.push({element:t,event:r,field:e,handler:i}),t.addEventListener(r,i)})})},n.prototype.handleEvent=function(e,t,n){var r=this;if(this.isEnabled&&this.exceedThreshold(t,n)&&this.core.executeFilter(\"plugins-trigger-should-validate\",!0,[t,n])){var i=function(){return r.core.validateElement(t,n).then(function(i){r.core.emit(\"plugins.trigger.executed\",{element:n,event:e,field:t})})},a=this.opts.delay[t]||this.opts.delay;if(0===a)i();else{var o=this.timers.get(n);o&&window.clearTimeout(o),this.timers.set(n,window.setTimeout(i,1e3*a))}}},n.prototype.onFieldAdded=function(e){this.handlers.filter(function(t){return t.field===e.field}).forEach(function(e){return e.element.removeEventListener(e.event,e.handler)}),this.prepareHandler(e.field,e.elements)},n.prototype.onFieldRemoved=function(e){this.handlers.filter(function(t){return t.field===e.field&&e.elements.indexOf(t.element)>=0}).forEach(function(e){return e.element.removeEventListener(e.event,e.handler)})},n.prototype.exceedThreshold=function(e,t){var n=0!==this.opts.threshold[e]&&0!==this.opts.threshold&&(this.opts.threshold[e]||this.opts.threshold);if(!n)return!0;var r=t.getAttribute(\"type\");return-1!==[\"button\",\"checkbox\",\"file\",\"hidden\",\"image\",\"radio\",\"reset\",\"submit\"].indexOf(r)||this.core.getElementValue(e,t).length>=n},n}(a.Plugin);return ae.Trigger=t,ae}();var oe,se=ie.exports,le={exports:{}},ue={};le.exports=function(){if(oe)return ue;oe=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return ue.between=function(){var e=function(e){return parseFloat(\"\".concat(e).replace(\",\",\".\"))};return{validate:function(r){var i=r.value;if(\"\"===i)return{valid:!0};var a=Object.assign({},{inclusive:!0,message:\"\"},n(r.options)),o=e(a.min),s=e(a.max);return a.inclusive?{message:t(r.l10n?a.message||r.l10n.between.default:a.message,[\"\".concat(o),\"\".concat(s)]),valid:parseFloat(i)>=o&&parseFloat(i)<=s}:{message:t(r.l10n?a.message||r.l10n.between.notInclusive:a.message,[\"\".concat(o),\"\".concat(s)]),valid:parseFloat(i)>o&&parseFloat(i)<s}}}},ue}();var de,ce=le.exports,fe={exports:{}},pe={};fe.exports=(de||(de=1,pe.blank=function(){return{validate:function(e){return{valid:!0}}}}),pe);var he,me=fe.exports,ve={exports:{}},ge={};ve.exports=function(){if(he)return ge;he=1;var e=a.utils.call;return ge.callback=function(){return{validate:function(t){var n=e(t.options.callback,[t]);return\"boolean\"==typeof n?{valid:n}:n}}},ge}();var _e,ye=ve.exports,be={exports:{}},Me={};be.exports=function(){if(_e)return Me;_e=1;var e=a.utils.format;return Me.choice=function(){return{validate:function(t){var n=\"select\"===t.element.tagName.toLowerCase()?t.element.querySelectorAll(\"option:checked\").length:t.elements.filter(function(e){return e.checked}).length,r=t.options.min?\"\".concat(t.options.min):\"\",i=t.options.max?\"\".concat(t.options.max):\"\",a=t.l10n?t.options.message||t.l10n.choice.default:t.options.message,o=!(r&&n<parseInt(r,10)||i&&n>parseInt(i,10));switch(!0){case!!r&&!!i:a=e(t.l10n?t.l10n.choice.between:t.options.message,[r,i]);break;case!!r:a=e(t.l10n?t.l10n.choice.more:t.options.message,r);break;case!!i:a=e(t.l10n?t.l10n.choice.less:t.options.message,i)}return{message:a,valid:o}}}},Me}();var we,ke=be.exports,Le={exports:{}},xe={};Le.exports=function(){if(we)return xe;we=1;var e=a.algorithms.luhn,t={AMERICAN_EXPRESS:{length:[15],prefix:[\"34\",\"37\"]},DANKORT:{length:[16],prefix:[\"5019\"]},DINERS_CLUB:{length:[14],prefix:[\"300\",\"301\",\"302\",\"303\",\"304\",\"305\",\"36\"]},DINERS_CLUB_US:{length:[16],prefix:[\"54\",\"55\"]},DISCOVER:{length:[16],prefix:[\"6011\",\"622126\",\"622127\",\"622128\",\"622129\",\"62213\",\"62214\",\"62215\",\"62216\",\"62217\",\"62218\",\"62219\",\"6222\",\"6223\",\"6224\",\"6225\",\"6226\",\"6227\",\"6228\",\"62290\",\"62291\",\"622920\",\"622921\",\"622922\",\"622923\",\"622924\",\"622925\",\"644\",\"645\",\"646\",\"647\",\"648\",\"649\",\"65\"]},ELO:{length:[16],prefix:[\"4011\",\"4312\",\"4389\",\"4514\",\"4573\",\"4576\",\"5041\",\"5066\",\"5067\",\"509\",\"6277\",\"6362\",\"6363\",\"650\",\"6516\",\"6550\"]},FORBRUGSFORENINGEN:{length:[16],prefix:[\"600722\"]},JCB:{length:[16],prefix:[\"3528\",\"3529\",\"353\",\"354\",\"355\",\"356\",\"357\",\"358\"]},LASER:{length:[16,17,18,19],prefix:[\"6304\",\"6706\",\"6771\",\"6709\"]},MAESTRO:{length:[12,13,14,15,16,17,18,19],prefix:[\"5018\",\"5020\",\"5038\",\"5868\",\"6304\",\"6759\",\"6761\",\"6762\",\"6763\",\"6764\",\"6765\",\"6766\"]},MASTERCARD:{length:[16],prefix:[\"51\",\"52\",\"53\",\"54\",\"55\"]},SOLO:{length:[16,18,19],prefix:[\"6334\",\"6767\"]},UNIONPAY:{length:[16,17,18,19],prefix:[\"622126\",\"622127\",\"622128\",\"622129\",\"62213\",\"62214\",\"62215\",\"62216\",\"62217\",\"62218\",\"62219\",\"6222\",\"6223\",\"6224\",\"6225\",\"6226\",\"6227\",\"6228\",\"62290\",\"62291\",\"622920\",\"622921\",\"622922\",\"622923\",\"622924\",\"622925\"]},VISA:{length:[16],prefix:[\"4\"]},VISA_ELECTRON:{length:[16],prefix:[\"4026\",\"417500\",\"4405\",\"4508\",\"4844\",\"4913\",\"4917\"]}};return xe.CREDIT_CARD_TYPES=t,xe.creditCard=function(){return{validate:function(n){if(\"\"===n.value)return{meta:{type:null},valid:!0};if(/[^0-9-\\s]+/.test(n.value))return{meta:{type:null},valid:!1};var r=n.value.replace(/\\D/g,\"\");if(!e(r))return{meta:{type:null},valid:!1};for(var i=0,a=Object.keys(t);i<a.length;i++){var o=a[i];for(var s in t[o].prefix)if(n.value.substr(0,t[o].prefix[s].length)===t[o].prefix[s]&&-1!==t[o].length.indexOf(r.length))return{meta:{type:o},valid:!0}}return{meta:{type:null},valid:!1}}}},xe}();var Te,Se=Le.exports,De={exports:{}},Ee={};De.exports=function(){if(Te)return Ee;Te=1;var e=a,t=e.utils.format,n=e.utils.isValidDate,r=e.utils.removeUndefined,i=function(e,t,n){var r=t.indexOf(\"YYYY\"),i=t.indexOf(\"MM\"),a=t.indexOf(\"DD\");if(-1===r||-1===i||-1===a)return null;var o=e.split(\" \"),s=o[0].split(n);if(s.length<3)return null;var l=new Date(parseInt(s[r],10),parseInt(s[i],10)-1,parseInt(s[a],10)),u=o.length>2?o[2]:null;if(o.length>1){var d=o[1].split(\":\"),c=d.length>0?parseInt(d[0],10):0;l.setHours(u&&\"PM\"===u.toUpperCase()&&c<12?c+12:c),l.setMinutes(d.length>1?parseInt(d[1],10):0),l.setSeconds(d.length>2?parseInt(d[2],10):0)}return l},o=function(e,t){var n=t.replace(/Y/g,\"y\").replace(/M/g,\"m\").replace(/D/g,\"d\").replace(/:m/g,\":M\").replace(/:mm/g,\":MM\").replace(/:S/,\":s\").replace(/:SS/,\":ss\"),r=e.getDate(),i=r<10?\"0\".concat(r):r,a=e.getMonth()+1,o=a<10?\"0\".concat(a):a,s=\"\".concat(e.getFullYear()).substr(2),l=e.getFullYear(),u=e.getHours()%12||12,d=u<10?\"0\".concat(u):u,c=e.getHours(),f=c<10?\"0\".concat(c):c,p=e.getMinutes(),h=p<10?\"0\".concat(p):p,m=e.getSeconds(),v=m<10?\"0\".concat(m):m,g={H:\"\".concat(c),HH:\"\".concat(f),M:\"\".concat(p),MM:\"\".concat(h),d:\"\".concat(r),dd:\"\".concat(i),h:\"\".concat(u),hh:\"\".concat(d),m:\"\".concat(a),mm:\"\".concat(o),s:\"\".concat(m),ss:\"\".concat(v),yy:\"\".concat(s),yyyy:\"\".concat(l)};return n.replace(/d{1,4}|m{1,4}|yy(?:yy)?|([HhMs])\\1?|\"[^\"]*\"|'[^']*'/g,function(e){return g[e]?g[e]:e.slice(1,e.length-1)})};return Ee.date=function(){return{validate:function(e){if(\"\"===e.value)return{meta:{date:null},valid:!0};var a=Object.assign({},{format:e.element&&\"date\"===e.element.getAttribute(\"type\")?\"YYYY-MM-DD\":\"MM/DD/YYYY\",message:\"\"},r(e.options)),s=e.l10n?e.l10n.date.default:a.message,l={message:\"\".concat(s),meta:{date:null},valid:!1},u=a.format.split(\" \"),d=u.length>1?u[1]:null,c=u.length>2?u[2]:null,f=e.value.split(\" \"),p=f[0],h=f.length>1?f[1]:null,m=f.length>2?f[2]:null;if(u.length!==f.length)return l;var v=a.separator||(-1!==p.indexOf(\"/\")?\"/\":-1!==p.indexOf(\"-\")?\"-\":-1!==p.indexOf(\".\")?\".\":\"/\");if(null===v||-1===p.indexOf(v))return l;var g=p.split(v),_=u[0].split(v);if(g.length!==_.length)return l;var y=g[_.indexOf(\"YYYY\")],b=g[_.indexOf(\"MM\")],M=g[_.indexOf(\"DD\")];if(!/^\\d+$/.test(y)||!/^\\d+$/.test(b)||!/^\\d+$/.test(M)||y.length>4||b.length>2||M.length>2)return l;var w=parseInt(y,10),k=parseInt(b,10),L=parseInt(M,10);if(!n(w,k,L))return l;var x=new Date(w,k-1,L);if(d){var T=h.split(\":\");if(d.split(\":\").length!==T.length)return l;var S=T.length>0?T[0].length<=2&&/^\\d+$/.test(T[0])?parseInt(T[0],10):-1:0,D=T.length>1?T[1].length<=2&&/^\\d+$/.test(T[1])?parseInt(T[1],10):-1:0,E=T.length>2?T[2].length<=2&&/^\\d+$/.test(T[2])?parseInt(T[2],10):-1:0;if(-1===S||-1===D||-1===E)return l;if(E<0||E>60)return l;if(S<0||S>=24||c&&S>12)return l;if(D<0||D>59)return l;x.setHours(m&&\"PM\"===m.toUpperCase()&&S<12?S+12:S),x.setMinutes(D),x.setSeconds(E)}var Y=\"function\"==typeof a.min?a.min():a.min,A=Y instanceof Date?Y:Y?i(Y,_,v):x,O=\"function\"==typeof a.max?a.max():a.max,C=O instanceof Date?O:O?i(O,_,v):x,j=Y instanceof Date?o(A,a.format):Y,P=O instanceof Date?o(C,a.format):O;switch(!0){case!!j&&!P:return{message:t(e.l10n?e.l10n.date.min:s,j),meta:{date:x},valid:x.getTime()>=A.getTime()};case!!P&&!j:return{message:t(e.l10n?e.l10n.date.max:s,P),meta:{date:x},valid:x.getTime()<=C.getTime()};case!!P&&!!j:return{message:t(e.l10n?e.l10n.date.range:s,[j,P]),meta:{date:x},valid:x.getTime()<=C.getTime()&&x.getTime()>=A.getTime()};default:return{message:\"\".concat(s),meta:{date:x},valid:!0}}}}},Ee}();var Ye,Ae=De.exports,Oe={exports:{}},Ce={};Oe.exports=(Ye||(Ye=1,Ce.different=function(){return{validate:function(e){var t=\"function\"==typeof e.options.compare?e.options.compare.call(this):e.options.compare;return{valid:\"\"===t||e.value!==t}}}}),Ce);var je,Pe=Oe.exports,He={exports:{}},Ie={};He.exports=(je||(je=1,Ie.digits=function(){return{validate:function(e){return{valid:\"\"===e.value||/^\\d+$/.test(e.value)}}}}),Ie);var Fe,Ne=He.exports,Re={exports:{}},Ve={};Re.exports=function(){if(Fe)return Ve;Fe=1;var e=a.utils.removeUndefined,t=/^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,n=/^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;return Ve.emailAddress=function(){return{validate:function(r){if(\"\"===r.value)return{valid:!0};var i=Object.assign({},{multiple:!1,requireGlobalDomain:!1,separator:/[,;]/},e(r.options)),a=i.requireGlobalDomain?n:t;if(!0===i.multiple||\"true\"===\"\".concat(i.multiple)){for(var o=i.separator||/[,;]/,s=function(e,t){for(var n=e.split(/\"/),r=n.length,i=[],a=\"\",o=0;o<r;o++)if(o%2==0){var s=n[o].split(t),l=s.length;if(1===l)a+=s[0];else{i.push(a+s[0]);for(var u=1;u<l-1;u++)i.push(s[u]);a=s[l-1]}}else a+='\"'+n[o],o<r-1&&(a+='\"');return i.push(a),i}(r.value,o),l=s.length,u=0;u<l;u++)if(!a.test(s[u]))return{valid:!1};return{valid:!0}}return{valid:a.test(r.value)}}}},Ve}();var $e,ze=Re.exports,We={exports:{}},Ue={};We.exports=function(){if($e)return Ue;$e=1;var e=function(e){return-1===e.indexOf(\".\")?e:e.split(\".\").slice(0,-1).join(\".\")};return Ue.file=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n,r,i=t.options.extension?t.options.extension.toLowerCase().split(\",\").map(function(e){return e.trim()}):[],a=t.options.type?t.options.type.toLowerCase().split(\",\").map(function(e){return e.trim()}):[];if(window.File&&window.FileList&&window.FileReader){var o=t.element.files,s=o.length,l=0;if(t.options.maxFiles&&s>parseInt(\"\".concat(t.options.maxFiles),10))return{meta:{error:\"INVALID_MAX_FILES\"},valid:!1};if(t.options.minFiles&&s<parseInt(\"\".concat(t.options.minFiles),10))return{meta:{error:\"INVALID_MIN_FILES\"},valid:!1};for(var u={},d=0;d<s;d++){if(l+=o[d].size,u={ext:n=o[d].name.substr(o[d].name.lastIndexOf(\".\")+1),file:o[d],size:o[d].size,type:o[d].type},t.options.minSize&&o[d].size<parseInt(\"\".concat(t.options.minSize),10))return{meta:Object.assign({},{error:\"INVALID_MIN_SIZE\"},u),valid:!1};if(t.options.maxSize&&o[d].size>parseInt(\"\".concat(t.options.maxSize),10))return{meta:Object.assign({},{error:\"INVALID_MAX_SIZE\"},u),valid:!1};if(i.length>0&&-1===i.indexOf(n.toLowerCase()))return{meta:Object.assign({},{error:\"INVALID_EXTENSION\"},u),valid:!1};if(a.length>0&&o[d].type&&-1===a.indexOf(o[d].type.toLowerCase()))return{meta:Object.assign({},{error:\"INVALID_TYPE\"},u),valid:!1};if(t.options.validateFileName&&!t.options.validateFileName(e(o[d].name)))return{meta:Object.assign({},{error:\"INVALID_NAME\"},u),valid:!1}}if(t.options.maxTotalSize&&l>parseInt(\"\".concat(t.options.maxTotalSize),10))return{meta:Object.assign({},{error:\"INVALID_MAX_TOTAL_SIZE\",totalSize:l},u),valid:!1};if(t.options.minTotalSize&&l<parseInt(\"\".concat(t.options.minTotalSize),10))return{meta:Object.assign({},{error:\"INVALID_MIN_TOTAL_SIZE\",totalSize:l},u),valid:!1}}else{if(n=t.value.substr(t.value.lastIndexOf(\".\")+1),i.length>0&&-1===i.indexOf(n.toLowerCase()))return{meta:{error:\"INVALID_EXTENSION\",ext:n},valid:!1};if(r=e(t.value),t.options.validateFileName&&!t.options.validateFileName(r))return{meta:{error:\"INVALID_NAME\",name:r},valid:!1}}return{valid:!0}}}},Ue}();var Be,qe=We.exports,Ge={exports:{}},Je={};Ge.exports=function(){if(Be)return Je;Be=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return Je.greaterThan=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var r=Object.assign({},{inclusive:!0,message:\"\"},n(e.options)),i=parseFloat(\"\".concat(r.min).replace(\",\",\".\"));return r.inclusive?{message:t(e.l10n?r.message||e.l10n.greaterThan.default:r.message,\"\".concat(i)),valid:parseFloat(e.value)>=i}:{message:t(e.l10n?r.message||e.l10n.greaterThan.notInclusive:r.message,\"\".concat(i)),valid:parseFloat(e.value)>i}}}},Je}();var Ze,Ke=Ge.exports,Xe={exports:{}},Qe={};Xe.exports=(Ze||(Ze=1,Qe.identical=function(){return{validate:function(e){var t=\"function\"==typeof e.options.compare?e.options.compare.call(this):e.options.compare;return{valid:\"\"===t||e.value===t}}}}),Qe);var et,tt=Xe.exports,nt={exports:{}},rt={};nt.exports=function(){if(et)return rt;et=1;var e=a.utils.removeUndefined;return rt.integer=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=Object.assign({},{decimalSeparator:\".\",thousandsSeparator:\"\"},e(t.options)),r=\".\"===n.decimalSeparator?\"\\\\.\":n.decimalSeparator,i=\".\"===n.thousandsSeparator?\"\\\\.\":n.thousandsSeparator,a=new RegExp(\"^-?[0-9]{1,3}(\".concat(i,\"[0-9]{3})*(\").concat(r,\"[0-9]+)?$\")),o=new RegExp(i,\"g\"),s=\"\".concat(t.value);if(!a.test(s))return{valid:!1};i&&(s=s.replace(o,\"\")),r&&(s=s.replace(r,\".\"));var l=parseFloat(s);return{valid:!isNaN(l)&&isFinite(l)&&Math.floor(l)===l}}}},rt}();var it,at=nt.exports,ot={exports:{}},st={};ot.exports=function(){if(it)return st;it=1;var e=a.utils.removeUndefined;return st.ip=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=Object.assign({},{ipv4:!0,ipv6:!0},e(t.options)),r=/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\/([0-9]|[1-2][0-9]|3[0-2]))?$/,i=/^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*(\\/(\\d|\\d\\d|1[0-1]\\d|12[0-8]))?$/;switch(!0){case n.ipv4&&!n.ipv6:return{message:t.l10n?n.message||t.l10n.ip.ipv4:n.message,valid:r.test(t.value)};case!n.ipv4&&n.ipv6:return{message:t.l10n?n.message||t.l10n.ip.ipv6:n.message,valid:i.test(t.value)};case n.ipv4&&n.ipv6:default:return{message:t.l10n?n.message||t.l10n.ip.default:n.message,valid:r.test(t.value)||i.test(t.value)}}}}},st}();var lt,ut=ot.exports,dt={exports:{}},ct={};dt.exports=function(){if(lt)return ct;lt=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return ct.lessThan=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var r=Object.assign({},{inclusive:!0,message:\"\"},n(e.options)),i=parseFloat(\"\".concat(r.max).replace(\",\",\".\"));return r.inclusive?{message:t(e.l10n?r.message||e.l10n.lessThan.default:r.message,\"\".concat(i)),valid:parseFloat(e.value)<=i}:{message:t(e.l10n?r.message||e.l10n.lessThan.notInclusive:r.message,\"\".concat(i)),valid:parseFloat(e.value)<i}}}},ct}();var ft,pt=dt.exports,ht={exports:{}},mt={};ht.exports=(ft||(ft=1,mt.notEmpty=function(){return{validate:function(e){var t=!!e.options&&!!e.options.trim,n=e.value;return{valid:!t&&\"\"!==n||t&&\"\"!==n&&\"\"!==n.trim()}}}}),mt);var vt,gt=ht.exports,_t={exports:{}},yt={};_t.exports=function(){if(vt)return yt;vt=1;var e=a.utils.removeUndefined;return yt.numeric=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=Object.assign({},{decimalSeparator:\".\",thousandsSeparator:\"\"},e(t.options)),r=\"\".concat(t.value);r.substr(0,1)===n.decimalSeparator?r=\"0\".concat(n.decimalSeparator).concat(r.substr(1)):r.substr(0,2)===\"-\".concat(n.decimalSeparator)&&(r=\"-0\".concat(n.decimalSeparator).concat(r.substr(2)));var i=\".\"===n.decimalSeparator?\"\\\\.\":n.decimalSeparator,a=\".\"===n.thousandsSeparator?\"\\\\.\":n.thousandsSeparator,o=new RegExp(\"^-?[0-9]{1,3}(\".concat(a,\"[0-9]{3})*(\").concat(i,\"[0-9]+)?$\")),s=new RegExp(a,\"g\");if(!o.test(r))return{valid:!1};a&&(r=r.replace(s,\"\")),i&&(r=r.replace(i,\".\"));var l=parseFloat(r);return{valid:!isNaN(l)&&isFinite(l)}}}},yt}();var bt,Mt=_t.exports,wt={exports:{}},kt={};wt.exports=function(){if(bt)return kt;bt=1;var e=a.utils.call;return kt.promise=function(){return{validate:function(t){return e(t.options.promise,[t])}}},kt}();var Lt,xt=wt.exports,Tt={exports:{}},St={};Tt.exports=(Lt||(Lt=1,St.regexp=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var t=e.options.regexp;if(t instanceof RegExp)return{valid:t.test(e.value)};var n=t.toString();return{valid:(e.options.flags?new RegExp(n,e.options.flags):new RegExp(n)).test(e.value)}}}}),St);var Dt,Et=Tt.exports,Yt={exports:{}},At={};Yt.exports=function(){if(Dt)return At;Dt=1;var e=a,t=e.utils.fetch,n=e.utils.removeUndefined;return At.remote=function(){var e={crossDomain:!1,data:{},headers:{},method:\"GET\",validKey:\"valid\"};return{validate:function(r){if(\"\"===r.value)return Promise.resolve({valid:!0});var i=Object.assign({},e,n(r.options)),a=i.data;\"function\"==typeof i.data&&(a=i.data.call(this,r)),\"string\"==typeof a&&(a=JSON.parse(a)),a[i.name||r.field]=r.value;var o=\"function\"==typeof i.url?i.url.call(this,r):i.url;return t(o,{crossDomain:i.crossDomain,headers:i.headers,method:i.method,params:a}).then(function(e){return Promise.resolve({message:e.message,meta:e,valid:\"true\"===\"\".concat(e[i.validKey])})}).catch(function(e){return Promise.reject({valid:!1})})}}},At}();var Ot,Ct=Yt.exports,jt={exports:{}},Pt={};jt.exports=function(){if(Ot)return Pt;Ot=1;var e=a.utils.removeUndefined;return Pt.stringCase=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=Object.assign({},{case:\"lower\"},e(t.options)),r=(n.case||\"lower\").toLowerCase();return{message:n.message||(t.l10n?\"upper\"===r?t.l10n.stringCase.upper:t.l10n.stringCase.default:n.message),valid:\"upper\"===r?t.value===t.value.toUpperCase():t.value===t.value.toLowerCase()}}}},Pt}();var Ht,It=jt.exports,Ft={exports:{}},Nt={};Ft.exports=function(){if(Ht)return Nt;Ht=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return Nt.stringLength=function(){return{validate:function(e){var r=Object.assign({},{message:\"\",trim:!1,utf8Bytes:!1},n(e.options)),i=!0===r.trim||\"true\"===\"\".concat(r.trim)?e.value.trim():e.value;if(\"\"===i)return{valid:!0};var a=r.min?\"\".concat(r.min):\"\",o=r.max?\"\".concat(r.max):\"\",s=r.utf8Bytes?function(e){for(var t=e.length,n=e.length-1;n>=0;n--){var r=e.charCodeAt(n);r>127&&r<=2047?t++:r>2047&&r<=65535&&(t+=2),r>=56320&&r<=57343&&n--}return t}(i):i.length,l=!0,u=e.l10n?r.message||e.l10n.stringLength.default:r.message;switch((a&&s<parseInt(a,10)||o&&s>parseInt(o,10))&&(l=!1),!0){case!!a&&!!o:u=t(e.l10n?r.message||e.l10n.stringLength.between:r.message,[a,o]);break;case!!a:u=t(e.l10n?r.message||e.l10n.stringLength.more:r.message,\"\".concat(parseInt(a,10)));break;case!!o:u=t(e.l10n?r.message||e.l10n.stringLength.less:r.message,\"\".concat(parseInt(o,10)))}return{message:u,valid:l}}}},Nt}();var Rt,Vt=Ft.exports,$t={exports:{}},zt={};$t.exports=function(){if(Rt)return zt;Rt=1;var e=a.utils.removeUndefined;return zt.uri=function(){var t={allowEmptyProtocol:!1,allowLocal:!1,protocol:\"http, https, ftp\"};return{validate:function(n){if(\"\"===n.value)return{valid:!0};var r=Object.assign({},t,e(n.options)),i=!0===r.allowLocal||\"true\"===\"\".concat(r.allowLocal),a=!0===r.allowEmptyProtocol||\"true\"===\"\".concat(r.allowEmptyProtocol),o=r.protocol.split(\",\").join(\"|\").replace(/\\s/g,\"\");return{valid:new RegExp(\"^(?:(?:\"+o+\")://)\"+(a?\"?\":\"\")+\"(?:\\\\S+(?::\\\\S*)?@)?(?:\"+(i?\"\":\"(?!(?:10|127)(?:\\\\.\\\\d{1,3}){3})(?!(?:169\\\\.254|192\\\\.168)(?:\\\\.\\\\d{1,3}){2})(?!172\\\\.(?:1[6-9]|2\\\\d|3[0-1])(?:\\\\.\\\\d{1,3}){2})\")+\"(?:[1-9]\\\\d?|1\\\\d\\\\d|2[01]\\\\d|22[0-3])(?:\\\\.(?:1?\\\\d{1,2}|2[0-4]\\\\d|25[0-5])){2}(?:\\\\.(?:[1-9]\\\\d?|1\\\\d\\\\d|2[0-4]\\\\d|25[0-4]))|(?:(?:[a-z\\\\u00a1-\\\\uffff0-9]-?)*[a-z\\\\u00a1-\\\\uffff0-9]+)(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-?)*[a-z\\\\u00a1-\\\\uffff0-9])*(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,}))\"+(i?\"?\":\"\")+\")(?::\\\\d{2,5})?(?:/[^\\\\s]*)?$\",\"i\").test(n.value)}}}},zt}();var Wt,Ut=$t.exports,Bt={exports:{}},qt={};Bt.exports=(Wt||(Wt=1,qt.base64=function(){return{validate:function(e){return{valid:\"\"===e.value||/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/.test(e.value)}}}}),qt);var Gt,Jt=Bt.exports,Zt={exports:{}},Kt={};Zt.exports=(Gt||(Gt=1,Kt.bic=function(){return{validate:function(e){return{valid:\"\"===e.value||/^[a-zA-Z]{6}[a-zA-Z0-9]{2}([a-zA-Z0-9]{3})?$/.test(e.value)}}}}),Kt);var Xt,Qt=Zt.exports,en={exports:{}},tn={};en.exports=(Xt||(Xt=1,tn.color=function(){var e=[\"hex\",\"rgb\",\"rgba\",\"hsl\",\"hsla\",\"keyword\"],t=[\"aliceblue\",\"antiquewhite\",\"aqua\",\"aquamarine\",\"azure\",\"beige\",\"bisque\",\"black\",\"blanchedalmond\",\"blue\",\"blueviolet\",\"brown\",\"burlywood\",\"cadetblue\",\"chartreuse\",\"chocolate\",\"coral\",\"cornflowerblue\",\"cornsilk\",\"crimson\",\"cyan\",\"darkblue\",\"darkcyan\",\"darkgoldenrod\",\"darkgray\",\"darkgreen\",\"darkgrey\",\"darkkhaki\",\"darkmagenta\",\"darkolivegreen\",\"darkorange\",\"darkorchid\",\"darkred\",\"darksalmon\",\"darkseagreen\",\"darkslateblue\",\"darkslategray\",\"darkslategrey\",\"darkturquoise\",\"darkviolet\",\"deeppink\",\"deepskyblue\",\"dimgray\",\"dimgrey\",\"dodgerblue\",\"firebrick\",\"floralwhite\",\"forestgreen\",\"fuchsia\",\"gainsboro\",\"ghostwhite\",\"gold\",\"goldenrod\",\"gray\",\"green\",\"greenyellow\",\"grey\",\"honeydew\",\"hotpink\",\"indianred\",\"indigo\",\"ivory\",\"khaki\",\"lavender\",\"lavenderblush\",\"lawngreen\",\"lemonchiffon\",\"lightblue\",\"lightcoral\",\"lightcyan\",\"lightgoldenrodyellow\",\"lightgray\",\"lightgreen\",\"lightgrey\",\"lightpink\",\"lightsalmon\",\"lightseagreen\",\"lightskyblue\",\"lightslategray\",\"lightslategrey\",\"lightsteelblue\",\"lightyellow\",\"lime\",\"limegreen\",\"linen\",\"magenta\",\"maroon\",\"mediumaquamarine\",\"mediumblue\",\"mediumorchid\",\"mediumpurple\",\"mediumseagreen\",\"mediumslateblue\",\"mediumspringgreen\",\"mediumturquoise\",\"mediumvioletred\",\"midnightblue\",\"mintcream\",\"mistyrose\",\"moccasin\",\"navajowhite\",\"navy\",\"oldlace\",\"olive\",\"olivedrab\",\"orange\",\"orangered\",\"orchid\",\"palegoldenrod\",\"palegreen\",\"paleturquoise\",\"palevioletred\",\"papayawhip\",\"peachpuff\",\"peru\",\"pink\",\"plum\",\"powderblue\",\"purple\",\"red\",\"rosybrown\",\"royalblue\",\"saddlebrown\",\"salmon\",\"sandybrown\",\"seagreen\",\"seashell\",\"sienna\",\"silver\",\"skyblue\",\"slateblue\",\"slategray\",\"slategrey\",\"snow\",\"springgreen\",\"steelblue\",\"tan\",\"teal\",\"thistle\",\"tomato\",\"transparent\",\"turquoise\",\"violet\",\"wheat\",\"white\",\"whitesmoke\",\"yellow\",\"yellowgreen\"],n=function(e){return/^hsl\\((\\s*(-?\\d+)\\s*,)(\\s*(\\b(0?\\d{1,2}|100)\\b%)\\s*,)(\\s*(\\b(0?\\d{1,2}|100)\\b%)\\s*)\\)$/.test(e)},r=function(e){return/^hsla\\((\\s*(-?\\d+)\\s*,)(\\s*(\\b(0?\\d{1,2}|100)\\b%)\\s*,){2}(\\s*(0?(\\.\\d+)?|1(\\.0+)?)\\s*)\\)$/.test(e)},i=function(e){return t.indexOf(e)>=0},a=function(e){return/^rgb\\((\\s*(\\b([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\b)\\s*,){2}(\\s*(\\b([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\b)\\s*)\\)$/.test(e)||/^rgb\\((\\s*(\\b(0?\\d{1,2}|100)\\b%)\\s*,){2}(\\s*(\\b(0?\\d{1,2}|100)\\b%)\\s*)\\)$/.test(e)},o=function(e){return/^rgba\\((\\s*(\\b([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\b)\\s*,){3}(\\s*(0?(\\.\\d+)?|1(\\.0+)?)\\s*)\\)$/.test(e)||/^rgba\\((\\s*(\\b(0?\\d{1,2}|100)\\b%)\\s*,){3}(\\s*(0?(\\.\\d+)?|1(\\.0+)?)\\s*)\\)$/.test(e)};return{validate:function(t){if(\"\"===t.value)return{valid:!0};for(var s,l=0,u=\"string\"==typeof t.options.type?t.options.type.toString().replace(/s/g,\"\").split(\",\"):t.options.type||e;l<u.length;l++){var d=u[l].toLowerCase();if(-1!==e.indexOf(d)){var c=!0;switch(d){case\"hex\":s=t.value,c=/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(s);break;case\"hsl\":c=n(t.value);break;case\"hsla\":c=r(t.value);break;case\"keyword\":c=i(t.value);break;case\"rgb\":c=a(t.value);break;case\"rgba\":c=o(t.value)}if(c)return{valid:!0}}}return{valid:!1}}}}),tn);var nn,rn=en.exports,an={exports:{}},on={};an.exports=(nn||(nn=1,on.cusip=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var t=e.value.toUpperCase();if(!/^[0123456789ABCDEFGHJKLMNPQRSTUVWXYZ*@#]{9}$/.test(t))return{valid:!1};var n=t.split(\"\"),r=n.pop(),i=n.map(function(e){var t=e.charCodeAt(0);switch(!0){case\"*\"===e:return 36;case\"@\"===e:return 37;case\"#\"===e:return 38;case t>=\"A\".charCodeAt(0)&&t<=\"Z\".charCodeAt(0):return t-\"A\".charCodeAt(0)+10;default:return parseInt(e,10)}}).map(function(e,t){var n=t%2==0?e:2*e;return Math.floor(n/10)+n%10}).reduce(function(e,t){return e+t},0);return{valid:r===\"\".concat((10-i%10)%10)}}}}),on);var sn,ln=an.exports,un={exports:{}},dn={};un.exports=(sn||(sn=1,dn.ean=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};if(!/^(\\d{8}|\\d{12}|\\d{13}|\\d{14})$/.test(e.value))return{valid:!1};for(var t=e.value.length,n=0,r=8===t?[3,1]:[1,3],i=0;i<t-1;i++)n+=parseInt(e.value.charAt(i),10)*r[i%2];return{valid:\"\".concat(n=(10-n%10)%10)===e.value.charAt(t-1)}}}}),dn);var cn,fn=un.exports,pn={exports:{}},hn={};pn.exports=(cn||(cn=1,hn.ein=function(){var e={ANDOVER:[\"10\",\"12\"],ATLANTA:[\"60\",\"67\"],AUSTIN:[\"50\",\"53\"],BROOKHAVEN:[\"01\",\"02\",\"03\",\"04\",\"05\",\"06\",\"11\",\"13\",\"14\",\"16\",\"21\",\"22\",\"23\",\"25\",\"34\",\"51\",\"52\",\"54\",\"55\",\"56\",\"57\",\"58\",\"59\",\"65\"],CINCINNATI:[\"30\",\"32\",\"35\",\"36\",\"37\",\"38\",\"61\"],FRESNO:[\"15\",\"24\"],INTERNET:[\"20\",\"26\",\"27\",\"45\",\"46\",\"47\"],KANSAS_CITY:[\"40\",\"44\"],MEMPHIS:[\"94\",\"95\"],OGDEN:[\"80\",\"90\"],PHILADELPHIA:[\"33\",\"39\",\"41\",\"42\",\"43\",\"48\",\"62\",\"63\",\"64\",\"66\",\"68\",\"71\",\"72\",\"73\",\"74\",\"75\",\"76\",\"77\",\"81\",\"82\",\"83\",\"84\",\"85\",\"86\",\"87\",\"88\",\"91\",\"92\",\"93\",\"98\",\"99\"],SMALL_BUSINESS_ADMINISTRATION:[\"31\"]};return{validate:function(t){if(\"\"===t.value)return{meta:null,valid:!0};if(!/^[0-9]{2}-?[0-9]{7}$/.test(t.value))return{meta:null,valid:!1};var n=\"\".concat(t.value.substr(0,2));for(var r in e)if(-1!==e[r].indexOf(n))return{meta:{campus:r},valid:!0};return{meta:null,valid:!1}}}}),hn);var mn,vn=pn.exports,gn={exports:{}},_n={};gn.exports=function(){if(mn)return _n;mn=1;var e=a.algorithms.mod37And36;return _n.grid=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=t.value.toUpperCase();return/^[GRID:]*([0-9A-Z]{2})[-\\s]*([0-9A-Z]{5})[-\\s]*([0-9A-Z]{10})[-\\s]*([0-9A-Z]{1})$/g.test(n)?(\"GRID:\"===(n=n.replace(/\\s/g,\"\").replace(/-/g,\"\")).substr(0,5)&&(n=n.substr(5)),{valid:e(n)}):{valid:!1}}}},_n}();var yn,bn=gn.exports,Mn={exports:{}},wn={};Mn.exports=(yn||(yn=1,wn.hex=function(){return{validate:function(e){return{valid:\"\"===e.value||/^[0-9a-fA-F]+$/.test(e.value)}}}}),wn);var kn,Ln=Mn.exports,xn={exports:{}},Tn={};xn.exports=function(){if(kn)return Tn;kn=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return Tn.iban=function(){var e={AD:\"AD[0-9]{2}[0-9]{4}[0-9]{4}[A-Z0-9]{12}\",AE:\"AE[0-9]{2}[0-9]{3}[0-9]{16}\",AL:\"AL[0-9]{2}[0-9]{8}[A-Z0-9]{16}\",AO:\"AO[0-9]{2}[0-9]{21}\",AT:\"AT[0-9]{2}[0-9]{5}[0-9]{11}\",AZ:\"AZ[0-9]{2}[A-Z]{4}[A-Z0-9]{20}\",BA:\"BA[0-9]{2}[0-9]{3}[0-9]{3}[0-9]{8}[0-9]{2}\",BE:\"BE[0-9]{2}[0-9]{3}[0-9]{7}[0-9]{2}\",BF:\"BF[0-9]{2}[0-9]{23}\",BG:\"BG[0-9]{2}[A-Z]{4}[0-9]{4}[0-9]{2}[A-Z0-9]{8}\",BH:\"BH[0-9]{2}[A-Z]{4}[A-Z0-9]{14}\",BI:\"BI[0-9]{2}[0-9]{12}\",BJ:\"BJ[0-9]{2}[A-Z]{1}[0-9]{23}\",BR:\"BR[0-9]{2}[0-9]{8}[0-9]{5}[0-9]{10}[A-Z][A-Z0-9]\",CH:\"CH[0-9]{2}[0-9]{5}[A-Z0-9]{12}\",CI:\"CI[0-9]{2}[A-Z]{1}[0-9]{23}\",CM:\"CM[0-9]{2}[0-9]{23}\",CR:\"CR[0-9]{2}[0-9][0-9]{3}[0-9]{14}\",CV:\"CV[0-9]{2}[0-9]{21}\",CY:\"CY[0-9]{2}[0-9]{3}[0-9]{5}[A-Z0-9]{16}\",CZ:\"CZ[0-9]{2}[0-9]{20}\",DE:\"DE[0-9]{2}[0-9]{8}[0-9]{10}\",DK:\"DK[0-9]{2}[0-9]{14}\",DO:\"DO[0-9]{2}[A-Z0-9]{4}[0-9]{20}\",DZ:\"DZ[0-9]{2}[0-9]{20}\",EE:\"EE[0-9]{2}[0-9]{2}[0-9]{2}[0-9]{11}[0-9]{1}\",ES:\"ES[0-9]{2}[0-9]{4}[0-9]{4}[0-9]{1}[0-9]{1}[0-9]{10}\",FI:\"FI[0-9]{2}[0-9]{6}[0-9]{7}[0-9]{1}\",FO:\"FO[0-9]{2}[0-9]{4}[0-9]{9}[0-9]{1}\",FR:\"FR[0-9]{2}[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}\",GB:\"GB[0-9]{2}[A-Z]{4}[0-9]{6}[0-9]{8}\",GE:\"GE[0-9]{2}[A-Z]{2}[0-9]{16}\",GI:\"GI[0-9]{2}[A-Z]{4}[A-Z0-9]{15}\",GL:\"GL[0-9]{2}[0-9]{4}[0-9]{9}[0-9]{1}\",GR:\"GR[0-9]{2}[0-9]{3}[0-9]{4}[A-Z0-9]{16}\",GT:\"GT[0-9]{2}[A-Z0-9]{4}[A-Z0-9]{20}\",HR:\"HR[0-9]{2}[0-9]{7}[0-9]{10}\",HU:\"HU[0-9]{2}[0-9]{3}[0-9]{4}[0-9]{1}[0-9]{15}[0-9]{1}\",IE:\"IE[0-9]{2}[A-Z]{4}[0-9]{6}[0-9]{8}\",IL:\"IL[0-9]{2}[0-9]{3}[0-9]{3}[0-9]{13}\",IR:\"IR[0-9]{2}[0-9]{22}\",IS:\"IS[0-9]{2}[0-9]{4}[0-9]{2}[0-9]{6}[0-9]{10}\",IT:\"IT[0-9]{2}[A-Z]{1}[0-9]{5}[0-9]{5}[A-Z0-9]{12}\",JO:\"JO[0-9]{2}[A-Z]{4}[0-9]{4}[0]{8}[A-Z0-9]{10}\",KW:\"KW[0-9]{2}[A-Z]{4}[0-9]{22}\",KZ:\"KZ[0-9]{2}[0-9]{3}[A-Z0-9]{13}\",LB:\"LB[0-9]{2}[0-9]{4}[A-Z0-9]{20}\",LI:\"LI[0-9]{2}[0-9]{5}[A-Z0-9]{12}\",LT:\"LT[0-9]{2}[0-9]{5}[0-9]{11}\",LU:\"LU[0-9]{2}[0-9]{3}[A-Z0-9]{13}\",LV:\"LV[0-9]{2}[A-Z]{4}[A-Z0-9]{13}\",MC:\"MC[0-9]{2}[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}\",MD:\"MD[0-9]{2}[A-Z0-9]{20}\",ME:\"ME[0-9]{2}[0-9]{3}[0-9]{13}[0-9]{2}\",MG:\"MG[0-9]{2}[0-9]{23}\",MK:\"MK[0-9]{2}[0-9]{3}[A-Z0-9]{10}[0-9]{2}\",ML:\"ML[0-9]{2}[A-Z]{1}[0-9]{23}\",MR:\"MR13[0-9]{5}[0-9]{5}[0-9]{11}[0-9]{2}\",MT:\"MT[0-9]{2}[A-Z]{4}[0-9]{5}[A-Z0-9]{18}\",MU:\"MU[0-9]{2}[A-Z]{4}[0-9]{2}[0-9]{2}[0-9]{12}[0-9]{3}[A-Z]{3}\",MZ:\"MZ[0-9]{2}[0-9]{21}\",NL:\"NL[0-9]{2}[A-Z]{4}[0-9]{10}\",NO:\"NO[0-9]{2}[0-9]{4}[0-9]{6}[0-9]{1}\",PK:\"PK[0-9]{2}[A-Z]{4}[A-Z0-9]{16}\",PL:\"PL[0-9]{2}[0-9]{8}[0-9]{16}\",PS:\"PS[0-9]{2}[A-Z]{4}[A-Z0-9]{21}\",PT:\"PT[0-9]{2}[0-9]{4}[0-9]{4}[0-9]{11}[0-9]{2}\",QA:\"QA[0-9]{2}[A-Z]{4}[A-Z0-9]{21}\",RO:\"RO[0-9]{2}[A-Z]{4}[A-Z0-9]{16}\",RS:\"RS[0-9]{2}[0-9]{3}[0-9]{13}[0-9]{2}\",SA:\"SA[0-9]{2}[0-9]{2}[A-Z0-9]{18}\",SE:\"SE[0-9]{2}[0-9]{3}[0-9]{16}[0-9]{1}\",SI:\"SI[0-9]{2}[0-9]{5}[0-9]{8}[0-9]{2}\",SK:\"SK[0-9]{2}[0-9]{4}[0-9]{6}[0-9]{10}\",SM:\"SM[0-9]{2}[A-Z]{1}[0-9]{5}[0-9]{5}[A-Z0-9]{12}\",SN:\"SN[0-9]{2}[A-Z]{1}[0-9]{23}\",TL:\"TL38[0-9]{3}[0-9]{14}[0-9]{2}\",TN:\"TN59[0-9]{2}[0-9]{3}[0-9]{13}[0-9]{2}\",TR:\"TR[0-9]{2}[0-9]{5}[A-Z0-9]{1}[A-Z0-9]{16}\",VG:\"VG[0-9]{2}[A-Z]{4}[0-9]{16}\",XK:\"XK[0-9]{2}[0-9]{4}[0-9]{10}[0-9]{2}\"},r=[\"AT\",\"BE\",\"BG\",\"CH\",\"CY\",\"CZ\",\"DE\",\"DK\",\"EE\",\"ES\",\"FI\",\"FR\",\"GB\",\"GI\",\"GR\",\"HR\",\"HU\",\"IE\",\"IS\",\"IT\",\"LI\",\"LT\",\"LU\",\"LV\",\"MC\",\"MT\",\"NL\",\"NO\",\"PL\",\"PT\",\"RO\",\"SE\",\"SI\",\"SK\",\"SM\"];return{validate:function(i){if(\"\"===i.value)return{valid:!0};var a=Object.assign({},{message:\"\"},n(i.options)),o=i.value.replace(/[^a-zA-Z0-9]/g,\"\").toUpperCase(),s=a.country||o.substr(0,2);if(!e[s])return{message:a.message,valid:!1};if(void 0!==a.sepa){var l=-1!==r.indexOf(s);if((\"true\"===a.sepa||!0===a.sepa)&&!l||(\"false\"===a.sepa||!1===a.sepa)&&l)return{message:a.message,valid:!1}}var u=t(i.l10n?a.message||i.l10n.iban.country:a.message,i.l10n?i.l10n.iban.countries[s]:s);if(!new RegExp(\"^\".concat(e[s],\"$\")).test(i.value))return{message:u,valid:!1};o=(o=\"\".concat(o.substr(4)).concat(o.substr(0,4))).split(\"\").map(function(e){var t=e.charCodeAt(0);return t>=\"A\".charCodeAt(0)&&t<=\"Z\".charCodeAt(0)?t-\"A\".charCodeAt(0)+10:e}).join(\"\");for(var d=parseInt(o.substr(0,1),10),c=o.length,f=1;f<c;++f)d=(10*d+parseInt(o.substr(f,1),10))%97;return{message:u,valid:1===d}}}},Tn}();var Sn,Dn=xn.exports,En={exports:{}},Yn={};En.exports=function(){if(Sn)return Yn;Sn=1;var e=a;function t(e,t){if(!/^\\d{13}$/.test(e))return!1;var n=parseInt(e.substr(0,2),10),r=parseInt(e.substr(2,2),10),i=parseInt(e.substr(7,2),10),a=parseInt(e.substr(12,1),10);if(n>31||r>12)return!1;for(var o=0,s=0;s<6;s++)o+=(7-s)*(parseInt(e.charAt(s),10)+parseInt(e.charAt(s+6),10));if(10!=(o=11-o%11)&&11!==o||(o=0),o!==a)return!1;switch(t.toUpperCase()){case\"BA\":return 10<=i&&i<=19;case\"MK\":return 41<=i&&i<=49;case\"ME\":return 20<=i&&i<=29;case\"RS\":return 70<=i&&i<=99;case\"SI\":return 50<=i&&i<=59;default:return!0}}var n=e.utils.isValidDate,r=e.utils.isValidDate,i=e.utils.isValidDate;function o(e){if(!/^\\d{9,10}$/.test(e))return{meta:{},valid:!1};var t=1900+parseInt(e.substr(0,2),10),n=parseInt(e.substr(2,2),10)%50%20,r=parseInt(e.substr(4,2),10);if(9===e.length){if(t>=1980&&(t-=100),t>1953)return{meta:{},valid:!1}}else t<1954&&(t+=100);if(!i(t,n,r))return{meta:{},valid:!1};if(10===e.length){var a=parseInt(e.substr(0,9),10)%11;return t<1985&&(a%=10),{meta:{},valid:\"\".concat(a)===e.substr(9,1)}}return{meta:{},valid:!0}}var s=e.utils.isValidDate,l=e.utils.isValidDate,u=e.algorithms.mod11And10,d=e.algorithms.verhoeff,c=e.algorithms.luhn,f=e.utils.isValidDate,p=e.utils.isValidDate,h=e.utils.isValidDate;function m(e){if(!/^[0-9]{11}$/.test(e))return{meta:{},valid:!1};var t=parseInt(e.charAt(0),10),n=parseInt(e.substr(1,2),10),r=parseInt(e.substr(3,2),10),i=parseInt(e.substr(5,2),10);if(!h(n=100*(t%2==0?17+t/2:17+(t+1)/2)+n,r,i,!0))return{meta:{},valid:!1};var a,o=[1,2,3,4,5,6,7,8,9,1],s=0;for(a=0;a<10;a++)s+=parseInt(e.charAt(a),10)*o[a];if(10!=(s%=11))return{meta:{},valid:\"\".concat(s)===e.charAt(10)};for(s=0,o=[3,4,5,6,7,8,9,1,2,3],a=0;a<10;a++)s+=parseInt(e.charAt(a),10)*o[a];return 10==(s%=11)&&(s=0),{meta:{},valid:\"\".concat(s)===e.charAt(10)}}var v=e.utils.isValidDate,g=e.utils.isValidDate,_=e.utils.isValidDate,y=e.utils.isValidDate,b=e.algorithms.luhn,M=e.utils.isValidDate,w=e.algorithms.luhn,k=e.utils.isValidDate,L=e.utils.format,x=e.utils.removeUndefined;return Yn.id=function(){var e=[\"AR\",\"BA\",\"BG\",\"BR\",\"CH\",\"CL\",\"CN\",\"CO\",\"CZ\",\"DK\",\"EE\",\"ES\",\"FI\",\"FR\",\"HK\",\"HR\",\"ID\",\"IE\",\"IL\",\"IS\",\"KR\",\"LT\",\"LV\",\"ME\",\"MK\",\"MX\",\"MY\",\"NL\",\"NO\",\"PE\",\"PL\",\"RO\",\"RS\",\"SE\",\"SI\",\"SK\",\"SM\",\"TH\",\"TR\",\"TW\",\"UY\",\"ZA\"];return{validate:function(i){if(\"\"===i.value)return{valid:!0};var a=Object.assign({},{message:\"\"},x(i.options)),h=i.value.substr(0,2);if(h=\"function\"==typeof a.country?a.country.call(this):a.country,-1===e.indexOf(h))return{valid:!0};var T,S={meta:{},valid:!0};switch(h.toLowerCase()){case\"ar\":T=i.value.replace(/\\./g,\"\"),S={meta:{},valid:/^\\d{7,8}$/.test(T)};break;case\"ba\":S=function(e){return{meta:{},valid:t(e,\"BA\")}}(i.value);break;case\"bg\":S=function(e){if(!/^\\d{10}$/.test(e)&&!/^\\d{6}\\s\\d{3}\\s\\d{1}$/.test(e))return{meta:{},valid:!1};var t=e.replace(/\\s/g,\"\"),r=parseInt(t.substr(0,2),10)+1900,i=parseInt(t.substr(2,2),10),a=parseInt(t.substr(4,2),10);if(i>40?(r+=100,i-=40):i>20&&(r-=100,i-=20),!n(r,i,a))return{meta:{},valid:!1};for(var o=0,s=[2,4,8,5,10,9,7,3,6],l=0;l<9;l++)o+=parseInt(t.charAt(l),10)*s[l];return{meta:{},valid:\"\".concat(o=o%11%10)===t.substr(9,1)}}(i.value);break;case\"br\":S=function(e){var t=e.replace(/\\D/g,\"\");if(!/^\\d{11}$/.test(t)||/^1{11}|2{11}|3{11}|4{11}|5{11}|6{11}|7{11}|8{11}|9{11}|0{11}$/.test(t))return{meta:{},valid:!1};var n,r=0;for(n=0;n<9;n++)r+=(10-n)*parseInt(t.charAt(n),10);if(10!=(r=11-r%11)&&11!==r||(r=0),\"\".concat(r)!==t.charAt(9))return{meta:{},valid:!1};var i=0;for(n=0;n<10;n++)i+=(11-n)*parseInt(t.charAt(n),10);return 10!=(i=11-i%11)&&11!==i||(i=0),{meta:{},valid:\"\".concat(i)===t.charAt(10)}}(i.value);break;case\"ch\":S=function(e){if(!/^756[.]{0,1}[0-9]{4}[.]{0,1}[0-9]{4}[.]{0,1}[0-9]{2}$/.test(e))return{meta:{},valid:!1};for(var t=e.replace(/\\D/g,\"\").substr(3),n=t.length,r=8===n?[3,1]:[1,3],i=0,a=0;a<n-1;a++)i+=parseInt(t.charAt(a),10)*r[a%2];return{meta:{},valid:\"\".concat(i=10-i%10)===t.charAt(n-1)}}(i.value);break;case\"cl\":S=function(e){if(!/^\\d{7,8}[-]{0,1}[0-9K]$/i.test(e))return{meta:{},valid:!1};for(var t=e.replace(/-/g,\"\");t.length<9;)t=\"0\".concat(t);for(var n=[3,2,7,6,5,4,3,2],r=0,i=0;i<8;i++)r+=parseInt(t.charAt(i),10)*n[i];var a=\"\".concat(r=11-r%11);return 11===r?a=\"0\":10===r&&(a=\"K\"),{meta:{},valid:a===t.charAt(8).toUpperCase()}}(i.value);break;case\"cn\":S=function(e){var t=e.trim();if(!/^\\d{15}$/.test(t)&&!/^\\d{17}[\\dXx]{1}$/.test(t))return{meta:{},valid:!1};var n={11:{0:[0],1:[[0,9],[11,17]],2:[0,28,29]},12:{0:[0],1:[[0,16]],2:[0,21,23,25]},13:{0:[0],1:[[0,5],7,8,21,[23,33],[81,85]],2:[[0,5],[7,9],[23,25],27,29,30,81,83],3:[[0,4],[21,24]],4:[[0,4],6,21,[23,35],81],5:[[0,3],[21,35],81,82],6:[[0,4],[21,38],[81,84]],7:[[0,3],5,6,[21,33]],8:[[0,4],[21,28]],9:[[0,3],[21,30],[81,84]],10:[[0,3],[22,26],28,81,82],11:[[0,2],[21,28],81,82]},14:{0:[0],1:[0,1,[5,10],[21,23],81],2:[[0,3],11,12,[21,27]],3:[[0,3],11,21,22],4:[[0,2],11,21,[23,31],81],5:[[0,2],21,22,24,25,81],6:[[0,3],[21,24]],7:[[0,2],[21,29],81],8:[[0,2],[21,30],81,82],9:[[0,2],[21,32],81],10:[[0,2],[21,34],81,82],11:[[0,2],[21,30],81,82],23:[[0,3],22,23,[25,30],32,33]},15:{0:[0],1:[[0,5],[21,25]],2:[[0,7],[21,23]],3:[[0,4]],4:[[0,4],[21,26],[28,30]],5:[[0,2],[21,26],81],6:[[0,2],[21,27]],7:[[0,3],[21,27],[81,85]],8:[[0,2],[21,26]],9:[[0,2],[21,29],81],22:[[0,2],[21,24]],25:[[0,2],[22,31]],26:[[0,2],[24,27],[29,32],34],28:[0,1,[22,27]],29:[0,[21,23]]},21:{0:[0],1:[[0,6],[11,14],[22,24],81],2:[[0,4],[11,13],24,[81,83]],3:[[0,4],11,21,23,81],4:[[0,4],11,[21,23]],5:[[0,5],21,22],6:[[0,4],24,81,82],7:[[0,3],11,26,27,81,82],8:[[0,4],11,81,82],9:[[0,5],11,21,22],10:[[0,5],11,21,81],11:[[0,3],21,22],12:[[0,2],4,21,23,24,81,82],13:[[0,3],21,22,24,81,82],14:[[0,4],21,22,81]},22:{0:[0],1:[[0,6],12,22,[81,83]],2:[[0,4],11,21,[81,84]],3:[[0,3],22,23,81,82],4:[[0,3],21,22],5:[[0,3],21,23,24,81,82],6:[[0,2],4,5,[21,23],25,81],7:[[0,2],[21,24],81],8:[[0,2],21,22,81,82],24:[[0,6],24,26]},23:{0:[0],1:[[0,12],21,[23,29],[81,84]],2:[[0,8],21,[23,25],27,[29,31],81],3:[[0,7],21,81,82],4:[[0,7],21,22],5:[[0,3],5,6,[21,24]],6:[[0,6],[21,24]],7:[[0,16],22,81],8:[[0,5],11,22,26,28,33,81,82],9:[[0,4],21],10:[[0,5],24,25,81,[83,85]],11:[[0,2],21,23,24,81,82],12:[[0,2],[21,26],[81,83]],27:[[0,4],[21,23]]},31:{0:[0],1:[0,1,[3,10],[12,20]],2:[0,30]},32:{0:[0],1:[[0,7],11,[13,18],24,25],2:[[0,6],11,81,82],3:[[0,5],11,12,[21,24],81,82],4:[[0,2],4,5,11,12,81,82],5:[[0,9],[81,85]],6:[[0,2],11,12,21,23,[81,84]],7:[0,1,3,5,6,[21,24]],8:[[0,4],11,26,[29,31]],9:[[0,3],[21,25],28,81,82],10:[[0,3],11,12,23,81,84,88],11:[[0,2],11,12,[81,83]],12:[[0,4],[81,84]],13:[[0,2],11,[21,24]]},33:{0:[0],1:[[0,6],[8,10],22,27,82,83,85],2:[0,1,[3,6],11,12,25,26,[81,83]],3:[[0,4],22,24,[26,29],81,82],4:[[0,2],11,21,24,[81,83]],5:[[0,3],[21,23]],6:[[0,2],21,24,[81,83]],7:[[0,3],23,26,27,[81,84]],8:[[0,3],22,24,25,81],9:[[0,3],21,22],10:[[0,4],[21,24],81,82],11:[[0,2],[21,27],81]},34:{0:[0],1:[[0,4],11,[21,24],81],2:[[0,4],7,8,[21,23],25],3:[[0,4],11,[21,23]],4:[[0,6],21],5:[[0,4],6,[21,23]],6:[[0,4],21],7:[[0,3],11,21],8:[[0,3],11,[22,28],81],10:[[0,4],[21,24]],11:[[0,3],22,[24,26],81,82],12:[[0,4],21,22,25,26,82],13:[[0,2],[21,24]],14:[[0,2],[21,24]],15:[[0,3],[21,25]],16:[[0,2],[21,23]],17:[[0,2],[21,23]],18:[[0,2],[21,25],81]},35:{0:[0],1:[[0,5],11,[21,25],28,81,82],2:[[0,6],[11,13]],3:[[0,5],22],4:[[0,3],21,[23,30],81],5:[[0,5],21,[24,27],[81,83]],6:[[0,3],[22,29],81],7:[[0,2],[21,25],[81,84]],8:[[0,2],[21,25],81],9:[[0,2],[21,26],81,82]},36:{0:[0],1:[[0,5],11,[21,24]],2:[[0,3],22,81],3:[[0,2],13,[21,23]],4:[[0,3],21,[23,30],81,82],5:[[0,2],21],6:[[0,2],22,81],7:[[0,2],[21,35],81,82],8:[[0,3],[21,30],81],9:[[0,2],[21,26],[81,83]],10:[[0,2],[21,30]],11:[[0,2],[21,30],81]},37:{0:[0],1:[[0,5],12,13,[24,26],81],2:[[0,3],5,[11,14],[81,85]],3:[[0,6],[21,23]],4:[[0,6],81],5:[[0,3],[21,23]],6:[[0,2],[11,13],34,[81,87]],7:[[0,5],24,25,[81,86]],8:[[0,2],11,[26,32],[81,83]],9:[[0,3],11,21,23,82,83],10:[[0,2],[81,83]],11:[[0,3],21,22],12:[[0,3]],13:[[0,2],11,12,[21,29]],14:[[0,2],[21,28],81,82],15:[[0,2],[21,26],81],16:[[0,2],[21,26]],17:[[0,2],[21,28]]},41:{0:[0],1:[[0,6],8,22,[81,85]],2:[[0,5],11,[21,25]],3:[[0,7],11,[22,29],81],4:[[0,4],11,[21,23],25,81,82],5:[[0,3],5,6,22,23,26,27,81],6:[[0,3],11,21,22],7:[[0,4],11,21,[24,28],81,82],8:[[0,4],11,[21,23],25,[81,83]],9:[[0,2],22,23,[26,28]],10:[[0,2],[23,25],81,82],11:[[0,4],[21,23]],12:[[0,2],21,22,24,81,82],13:[[0,3],[21,30],81],14:[[0,3],[21,26],81],15:[[0,3],[21,28]],16:[[0,2],[21,28],81],17:[[0,2],[21,29]],90:[0,1]},42:{0:[0],1:[[0,7],[11,17]],2:[[0,5],22,81],3:[[0,3],[21,25],81],5:[[0,6],[25,29],[81,83]],6:[[0,2],6,7,[24,26],[82,84]],7:[[0,4]],8:[[0,2],4,21,22,81],9:[[0,2],[21,23],81,82,84],10:[[0,3],[22,24],81,83,87],11:[[0,2],[21,27],81,82],12:[[0,2],[21,24],81],13:[[0,3],21,81],28:[[0,2],22,23,[25,28]],90:[0,[4,6],21]},43:{0:[0],1:[[0,5],11,12,21,22,24,81],2:[[0,4],11,21,[23,25],81],3:[[0,2],4,21,81,82],4:[0,1,[5,8],12,[21,24],26,81,82],5:[[0,3],11,[21,25],[27,29],81],6:[[0,3],11,21,23,24,26,81,82],7:[[0,3],[21,26],81],8:[[0,2],11,21,22],9:[[0,3],[21,23],81],10:[[0,3],[21,28],81],11:[[0,3],[21,29]],12:[[0,2],[21,30],81],13:[[0,2],21,22,81,82],31:[0,1,[22,27],30]},44:{0:[0],1:[[0,7],[11,16],83,84],2:[[0,5],21,22,24,29,32,33,81,82],3:[0,1,[3,8]],4:[[0,4]],5:[0,1,[6,15],23,82,83],6:[0,1,[4,8]],7:[0,1,[3,5],81,[83,85]],8:[[0,4],11,23,25,[81,83]],9:[[0,3],23,[81,83]],12:[[0,3],[23,26],83,84],13:[[0,3],[22,24],81],14:[[0,2],[21,24],26,27,81],15:[[0,2],21,23,81],16:[[0,2],[21,25]],17:[[0,2],21,23,81],18:[[0,3],21,23,[25,27],81,82],19:[0],20:[0],51:[[0,3],21,22],52:[[0,3],21,22,24,81],53:[[0,2],[21,23],81]},45:{0:[0],1:[[0,9],[21,27]],2:[[0,5],[21,26]],3:[[0,5],11,12,[21,32]],4:[0,1,[3,6],11,[21,23],81],5:[[0,3],12,21],6:[[0,3],21,81],7:[[0,3],21,22],8:[[0,4],21,81],9:[[0,3],[21,24],81],10:[[0,2],[21,31]],11:[[0,2],[21,23]],12:[[0,2],[21,29],81],13:[[0,2],[21,24],81],14:[[0,2],[21,25],81]},46:{0:[0],1:[0,1,[5,8]],2:[0,1],3:[0,[21,23]],90:[[0,3],[5,7],[21,39]]},50:{0:[0],1:[[0,19]],2:[0,[22,38],[40,43]],3:[0,[81,84]]},51:{0:[0],1:[0,1,[4,8],[12,15],[21,24],29,31,32,[81,84]],3:[[0,4],11,21,22],4:[[0,3],11,21,22],5:[[0,4],21,22,24,25],6:[0,1,3,23,26,[81,83]],7:[0,1,3,4,[22,27],81],8:[[0,2],11,12,[21,24]],9:[[0,4],[21,23]],10:[[0,2],11,24,25,28],11:[[0,2],[11,13],23,24,26,29,32,33,81],13:[[0,4],[21,25],81],14:[[0,2],[21,25]],15:[[0,3],[21,29]],16:[[0,3],[21,23],81],17:[[0,3],[21,25],81],18:[[0,3],[21,27]],19:[[0,3],[21,23]],20:[[0,2],21,22,81],32:[0,[21,33]],33:[0,[21,38]],34:[0,1,[22,37]]},52:{0:[0],1:[[0,3],[11,15],[21,23],81],2:[0,1,3,21,22],3:[[0,3],[21,30],81,82],4:[[0,2],[21,25]],5:[[0,2],[21,27]],6:[[0,3],[21,28]],22:[0,1,[22,30]],23:[0,1,[22,28]],24:[0,1,[22,28]],26:[0,1,[22,36]],27:[[0,2],22,23,[25,32]]},53:{0:[0],1:[[0,3],[11,14],21,22,[24,29],81],3:[[0,2],[21,26],28,81],4:[[0,2],[21,28]],5:[[0,2],[21,24]],6:[[0,2],[21,30]],7:[[0,2],[21,24]],8:[[0,2],[21,29]],9:[[0,2],[21,27]],23:[0,1,[22,29],31],25:[[0,4],[22,32]],26:[0,1,[21,28]],27:[0,1,[22,30]],28:[0,1,22,23],29:[0,1,[22,32]],31:[0,2,3,[22,24]],34:[0,[21,23]],33:[0,21,[23,25]],35:[0,[21,28]]},54:{0:[0],1:[[0,2],[21,27]],21:[0,[21,29],32,33],22:[0,[21,29],[31,33]],23:[0,1,[22,38]],24:[0,[21,31]],25:[0,[21,27]],26:[0,[21,27]]},61:{0:[0],1:[[0,4],[11,16],22,[24,26]],2:[[0,4],22],3:[[0,4],[21,24],[26,31]],4:[[0,4],[22,31],81],5:[[0,2],[21,28],81,82],6:[[0,2],[21,32]],7:[[0,2],[21,30]],8:[[0,2],[21,31]],9:[[0,2],[21,29]],10:[[0,2],[21,26]]},62:{0:[0],1:[[0,5],11,[21,23]],2:[0,1],3:[[0,2],21],4:[[0,3],[21,23]],5:[[0,3],[21,25]],6:[[0,2],[21,23]],7:[[0,2],[21,25]],8:[[0,2],[21,26]],9:[[0,2],[21,24],81,82],10:[[0,2],[21,27]],11:[[0,2],[21,26]],12:[[0,2],[21,28]],24:[0,21,[24,29]],26:[0,21,[23,30]],29:[0,1,[21,27]],30:[0,1,[21,27]]},63:{0:[0],1:[[0,5],[21,23]],2:[0,2,[21,25]],21:[0,[21,23],[26,28]],22:[0,[21,24]],23:[0,[21,24]],25:[0,[21,25]],26:[0,[21,26]],27:[0,1,[21,26]],28:[[0,2],[21,23]]},64:{0:[0],1:[0,1,[4,6],21,22,81],2:[[0,3],5,[21,23]],3:[[0,3],[21,24],81],4:[[0,2],[21,25]],5:[[0,2],21,22]},65:{0:[0],1:[[0,9],21],2:[[0,5]],21:[0,1,22,23],22:[0,1,22,23],23:[[0,3],[23,25],27,28],28:[0,1,[22,29]],29:[0,1,[22,29]],30:[0,1,[22,24]],31:[0,1,[21,31]],32:[0,1,[21,27]],40:[0,2,3,[21,28]],42:[[0,2],21,[23,26]],43:[0,1,[21,26]],90:[[0,4]],27:[[0,2],22,23]},71:{0:[0]},81:{0:[0]},82:{0:[0]}},i=parseInt(t.substr(0,2),10),a=parseInt(t.substr(2,2),10),o=parseInt(t.substr(4,2),10);if(!n[i]||!n[i][a])return{meta:{},valid:!1};var s,l,u=!1,d=n[i][a];for(s=0;s<d.length;s++)if(Array.isArray(d[s])&&d[s][0]<=o&&o<=d[s][1]||!Array.isArray(d[s])&&o===d[s]){u=!0;break}if(!u)return{meta:{},valid:!1};l=18===t.length?t.substr(6,8):\"19\".concat(t.substr(6,6));var c=parseInt(l.substr(0,4),10),f=parseInt(l.substr(4,2),10),p=parseInt(l.substr(6,2),10);if(!r(c,f,p))return{meta:{},valid:!1};if(18===t.length){var h=[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2],m=0;for(s=0;s<17;s++)m+=parseInt(t.charAt(s),10)*h[s];return m=(12-m%11)%11,{meta:{},valid:(\"X\"!==t.charAt(17).toUpperCase()?parseInt(t.charAt(17),10):10)===m}}return{meta:{},valid:!0}}(i.value);break;case\"co\":S=function(e){var t=e.replace(/\\./g,\"\").replace(\"-\",\"\");if(!/^\\d{8,16}$/.test(t))return{meta:{},valid:!1};for(var n=t.length,r=[3,7,13,17,19,23,29,37,41,43,47,53,59,67,71],i=0,a=n-2;a>=0;a--)i+=parseInt(t.charAt(a),10)*r[a];return(i%=11)>=2&&(i=11-i),{meta:{},valid:\"\".concat(i)===t.substr(n-1)}}(i.value);break;case\"cz\":case\"sk\":S=o(i.value);break;case\"dk\":S=function(e){if(!/^[0-9]{6}[-]{0,1}[0-9]{4}$/.test(e))return{meta:{},valid:!1};var t=e.replace(/-/g,\"\"),n=parseInt(t.substr(0,2),10),r=parseInt(t.substr(2,2),10),i=parseInt(t.substr(4,2),10);switch(!0){case-1!==\"5678\".indexOf(t.charAt(6))&&i>=58:i+=1800;break;case-1!==\"0123\".indexOf(t.charAt(6)):case-1!==\"49\".indexOf(t.charAt(6))&&i>=37:i+=1900;break;default:i+=2e3}return{meta:{},valid:s(i,r,n)}}(i.value);break;case\"ee\":case\"lt\":S=m(i.value);break;case\"es\":S=function(e){var t=/^[0-9]{8}[-]{0,1}[A-HJ-NP-TV-Z]$/.test(e),n=/^[XYZ][-]{0,1}[0-9]{7}[-]{0,1}[A-HJ-NP-TV-Z]$/.test(e),r=/^[A-HNPQS][-]{0,1}[0-9]{7}[-]{0,1}[0-9A-J]$/.test(e);if(!t&&!n&&!r)return{meta:{},valid:!1};var i,a,o=e.replace(/-/g,\"\");if(t||n){a=\"DNI\";var s=\"XYZ\".indexOf(o.charAt(0));return-1!==s&&(o=s+o.substr(1)+\"\",a=\"NIE\"),{meta:{type:a},valid:(i=\"TRWAGMYFPDXBNJZSQVHLCKE\"[(i=parseInt(o.substr(0,8),10))%23])===o.substr(8,1)}}i=o.substr(1,7),a=\"CIF\";for(var l=o[0],u=o.substr(-1),d=0,c=0;c<i.length;c++)if(c%2!=0)d+=parseInt(i[c],10);else{var f=\"\"+2*parseInt(i[c],10);d+=parseInt(f[0],10),2===f.length&&(d+=parseInt(f[1],10))}var p=d-10*Math.floor(d/10);return 0!==p&&(p=10-p),{meta:{type:a},valid:-1!==\"KQS\".indexOf(l)?u===\"JABCDEFGHI\"[p]:-1!==\"ABEH\".indexOf(l)?u===\"\"+p:u===\"\"+p||u===\"JABCDEFGHI\"[p]}}(i.value);break;case\"fi\":S=function(e){if(!/^[0-9]{6}[-+A][0-9]{3}[0-9ABCDEFHJKLMNPRSTUVWXY]$/.test(e))return{meta:{},valid:!1};var t=parseInt(e.substr(0,2),10),n=parseInt(e.substr(2,2),10),r=parseInt(e.substr(4,2),10);if(r={\"+\":1800,\"-\":1900,A:2e3}[e.charAt(6)]+r,!l(r,n,t))return{meta:{},valid:!1};if(parseInt(e.substr(7,3),10)<2)return{meta:{},valid:!1};var i=parseInt(e.substr(0,6)+e.substr(7,3)+\"\",10);return{meta:{},valid:\"0123456789ABCDEFHJKLMNPRSTUVWXY\".charAt(i%31)===e.charAt(10)}}(i.value);break;case\"fr\":S=function(e){var t=e.toUpperCase();if(!/^(1|2)\\d{2}\\d{2}(\\d{2}|\\d[A-Z]|\\d{3})\\d{2,3}\\d{3}\\d{2}$/.test(t))return{meta:{},valid:!1};var n=t.substr(5,2);switch(!0){case/^\\d{2}$/.test(n):t=e;break;case\"2A\"===n:t=\"\".concat(e.substr(0,5),\"19\").concat(e.substr(7));break;case\"2B\"===n:t=\"\".concat(e.substr(0,5),\"18\").concat(e.substr(7));break;default:return{meta:{},valid:!1}}var r=97-parseInt(t.substr(0,13),10)%97;return{meta:{},valid:(r<10?\"0\".concat(r):\"\".concat(r))===t.substr(13)}}(i.value);break;case\"hk\":S=function(e){var t=e.toUpperCase();if(!/^[A-MP-Z]{1,2}[0-9]{6}[0-9A]$/.test(t))return{meta:{},valid:!1};var n=\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",r=t.charAt(0),i=t.charAt(1),a=0,o=t;/^[A-Z]$/.test(i)?(a+=9*(10+n.indexOf(r)),a+=8*(10+n.indexOf(i)),o=t.substr(2)):(a+=324,a+=8*(10+n.indexOf(r)),o=t.substr(1));for(var s=o.length,l=0;l<s-1;l++)a+=(7-l)*parseInt(o.charAt(l),10);var u=a%11;return{meta:{},valid:(0===u?\"0\":11-u==10?\"A\":\"\".concat(11-u))===o.charAt(s-1)}}(i.value);break;case\"hr\":S=function(e){return{meta:{},valid:/^[0-9]{11}$/.test(e)&&u(e)}}(i.value);break;case\"id\":S=function(e){if(!/^[2-9]\\d{11}$/.test(e))return{meta:{},valid:!1};var t=e.split(\"\").map(function(e){return parseInt(e,10)});return{meta:{},valid:d(t)}}(i.value);break;case\"ie\":S=function(e){if(!/^\\d{7}[A-W][AHWTX]?$/.test(e))return{meta:{},valid:!1};var t=function(e){for(var t=e;t.length<7;)t=\"0\".concat(t);for(var n=\"WABCDEFGHIJKLMNOPQRSTUV\",r=0,i=0;i<7;i++)r+=parseInt(t.charAt(i),10)*(8-i);return r+=9*n.indexOf(t.substr(7)),n[r%23]};return{meta:{},valid:9!==e.length||\"A\"!==e.charAt(8)&&\"H\"!==e.charAt(8)?e.charAt(7)===t(e.substr(0,7)):e.charAt(7)===t(e.substr(0,7)+e.substr(8)+\"\")}}(i.value);break;case\"il\":S=function(e){return/^\\d{1,9}$/.test(e)?{meta:{},valid:c(e)}:{meta:{},valid:!1}}(i.value);break;case\"is\":S=function(e){if(!/^[0-9]{6}[-]{0,1}[0-9]{4}$/.test(e))return{meta:{},valid:!1};var t=e.replace(/-/g,\"\"),n=parseInt(t.substr(0,2),10),r=parseInt(t.substr(2,2),10),i=parseInt(t.substr(4,2),10),a=parseInt(t.charAt(9),10);if(!f(i=9===a?1900+i:100*(20+a)+i,r,n,!0))return{meta:{},valid:!1};for(var o=[3,2,7,6,5,4,3,2],s=0,l=0;l<8;l++)s+=parseInt(t.charAt(l),10)*o[l];return{meta:{},valid:\"\".concat(s=11-s%11)===t.charAt(8)}}(i.value);break;case\"kr\":S=function(e){var t=e.replace(\"-\",\"\");if(!/^\\d{13}$/.test(t))return{meta:{},valid:!1};var n=t.charAt(6),r=parseInt(t.substr(0,2),10),i=parseInt(t.substr(2,2),10),a=parseInt(t.substr(4,2),10);switch(n){case\"1\":case\"2\":case\"5\":case\"6\":r+=1900;break;case\"3\":case\"4\":case\"7\":case\"8\":r+=2e3;break;default:r+=1800}if(!p(r,i,a))return{meta:{},valid:!1};for(var o=[2,3,4,5,6,7,8,9,2,3,4,5],s=t.length,l=0,u=0;u<s-1;u++)l+=o[u]*parseInt(t.charAt(u),10);return{meta:{},valid:\"\".concat((11-l%11)%10)===t.charAt(s-1)}}(i.value);break;case\"lv\":S=function(e){if(!/^[0-9]{6}[-]{0,1}[0-9]{5}$/.test(e))return{meta:{},valid:!1};var t=e.replace(/\\D/g,\"\"),n=parseInt(t.substr(0,2),10),r=parseInt(t.substr(2,2),10),i=parseInt(t.substr(4,2),10);if(i=i+1800+100*parseInt(t.charAt(6),10),!v(i,r,n,!0))return{meta:{},valid:!1};for(var a=0,o=[10,5,8,4,2,1,6,3,7,9],s=0;s<10;s++)a+=parseInt(t.charAt(s),10)*o[s];return{meta:{},valid:\"\".concat(a=(a+1)%11%10)===t.charAt(10)}}(i.value);break;case\"me\":S=function(e){return{meta:{},valid:t(e,\"ME\")}}(i.value);break;case\"mk\":S=function(e){return{meta:{},valid:t(e,\"MK\")}}(i.value);break;case\"mx\":S=function(e){var t=e.toUpperCase();if(!/^[A-Z]{4}\\d{6}[A-Z]{6}[0-9A-Z]\\d$/.test(t))return{meta:{},valid:!1};var n=t.substr(0,4);if([\"BACA\",\"BAKA\",\"BUEI\",\"BUEY\",\"CACA\",\"CACO\",\"CAGA\",\"CAGO\",\"CAKA\",\"CAKO\",\"COGE\",\"COGI\",\"COJA\",\"COJE\",\"COJI\",\"COJO\",\"COLA\",\"CULO\",\"FALO\",\"FETO\",\"GETA\",\"GUEI\",\"GUEY\",\"JETA\",\"JOTO\",\"KACA\",\"KACO\",\"KAGA\",\"KAGO\",\"KAKA\",\"KAKO\",\"KOGE\",\"KOGI\",\"KOJA\",\"KOJE\",\"KOJI\",\"KOJO\",\"KOLA\",\"KULO\",\"LILO\",\"LOCA\",\"LOCO\",\"LOKA\",\"LOKO\",\"MAME\",\"MAMO\",\"MEAR\",\"MEAS\",\"MEON\",\"MIAR\",\"MION\",\"MOCO\",\"MOKO\",\"MULA\",\"MULO\",\"NACA\",\"NACO\",\"PEDA\",\"PEDO\",\"PENE\",\"PIPI\",\"PITO\",\"POPO\",\"PUTA\",\"PUTO\",\"QULO\",\"RATA\",\"ROBA\",\"ROBE\",\"ROBO\",\"RUIN\",\"SENO\",\"TETA\",\"VACA\",\"VAGA\",\"VAGO\",\"VAKA\",\"VUEI\",\"VUEY\",\"WUEI\",\"WUEY\"].indexOf(n)>=0)return{meta:{},valid:!1};var r=parseInt(t.substr(4,2),10),i=parseInt(t.substr(6,2),10),a=parseInt(t.substr(6,2),10);if(/^[0-9]$/.test(t.charAt(16))?r+=1900:r+=2e3,!g(r,i,a))return{meta:{},valid:!1};var o=t.charAt(10);if(\"H\"!==o&&\"M\"!==o)return{meta:{},valid:!1};var s=t.substr(11,2);if(-1===[\"AS\",\"BC\",\"BS\",\"CC\",\"CH\",\"CL\",\"CM\",\"CS\",\"DF\",\"DG\",\"GR\",\"GT\",\"HG\",\"JC\",\"MC\",\"MN\",\"MS\",\"NE\",\"NL\",\"NT\",\"OC\",\"PL\",\"QR\",\"QT\",\"SL\",\"SP\",\"SR\",\"TC\",\"TL\",\"TS\",\"VZ\",\"YN\",\"ZS\"].indexOf(s))return{meta:{},valid:!1};for(var l=0,u=t.length,d=0;d<u-1;d++)l+=(18-d)*\"0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ\".indexOf(t.charAt(d));return{meta:{},valid:\"\".concat(l=(10-l%10)%10)===t.charAt(u-1)}}(i.value);break;case\"my\":S=function(e){if(!/^\\d{12}$/.test(e))return{meta:{},valid:!1};var t=parseInt(e.substr(0,2),10),n=parseInt(e.substr(2,2),10),r=parseInt(e.substr(4,2),10);if(!_(t+1900,n,r)&&!_(t+2e3,n,r))return{meta:{},valid:!1};var i=e.substr(6,2);return{meta:{},valid:-1===[\"17\",\"18\",\"19\",\"20\",\"69\",\"70\",\"73\",\"80\",\"81\",\"94\",\"95\",\"96\",\"97\"].indexOf(i)}}(i.value);break;case\"nl\":S=function(e){if(e.length<8)return{meta:{},valid:!1};var t=e;if(8===t.length&&(t=\"0\".concat(t)),!/^[0-9]{4}[.]{0,1}[0-9]{2}[.]{0,1}[0-9]{3}$/.test(t))return{meta:{},valid:!1};if(t=t.replace(/\\./g,\"\"),0===parseInt(t,10))return{meta:{},valid:!1};for(var n=0,r=t.length,i=0;i<r-1;i++)n+=(9-i)*parseInt(t.charAt(i),10);return 10==(n%=11)&&(n=0),{meta:{},valid:\"\".concat(n)===t.charAt(r-1)}}(i.value);break;case\"no\":S=function(e){return/^\\d{11}$/.test(e)?{meta:{},valid:\"\".concat(function(e){for(var t=[3,7,6,1,8,9,4,5,2],n=0,r=0;r<9;r++)n+=t[r]*parseInt(e.charAt(r),10);return 11-n%11}(e))===e.substr(-2,1)&&\"\".concat(function(e){for(var t=[5,4,3,2,7,6,5,4,3,2],n=0,r=0;r<10;r++)n+=t[r]*parseInt(e.charAt(r),10);return 11-n%11}(e))===e.substr(-1)}:{meta:{},valid:!1}}(i.value);break;case\"pe\":S=function(e){if(!/^\\d{8}[0-9A-Z]*$/.test(e))return{meta:{},valid:!1};if(8===e.length)return{meta:{},valid:!0};for(var t=[3,2,7,6,5,4,3,2],n=0,r=0;r<8;r++)n+=t[r]*parseInt(e.charAt(r),10);var i=n%11,a=[6,5,4,3,2,1,1,0,9,8,7][i],o=\"KJIHGFEDCBA\".charAt(i);return{meta:{},valid:e.charAt(8)===\"\".concat(a)||e.charAt(8)===o}}(i.value);break;case\"pl\":S=function(e){if(!/^[0-9]{11}$/.test(e))return{meta:{},valid:!1};for(var t=0,n=e.length,r=[1,3,7,9,1,3,7,9,1,3,7],i=0;i<n-1;i++)t+=r[i]*parseInt(e.charAt(i),10);return 0==(t%=10)&&(t=10),{meta:{},valid:\"\".concat(t=10-t)===e.charAt(n-1)}}(i.value);break;case\"ro\":S=function(e){if(!/^[0-9]{13}$/.test(e))return{meta:{},valid:!1};var t=parseInt(e.charAt(0),10);if(0===t||7===t||8===t)return{meta:{},valid:!1};var n=parseInt(e.substr(1,2),10),r=parseInt(e.substr(3,2),10),i=parseInt(e.substr(5,2),10);if(i>31&&r>12)return{meta:{},valid:!1};if(9!==t&&!y(n={1:1900,2:1900,3:1800,4:1800,5:2e3,6:2e3}[t+\"\"]+n,r,i))return{meta:{},valid:!1};for(var a=0,o=[2,7,9,1,4,6,3,5,8,2,7,9],s=e.length,l=0;l<s-1;l++)a+=parseInt(e.charAt(l),10)*o[l];return 10==(a%=11)&&(a=1),{meta:{},valid:\"\".concat(a)===e.charAt(s-1)}}(i.value);break;case\"rs\":S=function(e){return{meta:{},valid:t(e,\"RS\")}}(i.value);break;case\"se\":S=function(e){if(!/^[0-9]{10}$/.test(e)&&!/^[0-9]{6}[-|+][0-9]{4}$/.test(e))return{meta:{},valid:!1};var t=e.replace(/[^0-9]/g,\"\"),n=parseInt(t.substr(0,2),10)+1900,r=parseInt(t.substr(2,2),10),i=parseInt(t.substr(4,2),10);return M(n,r,i)?{meta:{},valid:b(t)}:{meta:{},valid:!1}}(i.value);break;case\"si\":S=function(e){return{meta:{},valid:t(e,\"SI\")}}(i.value);break;case\"sm\":S=function(e){return{meta:{},valid:/^\\d{5}$/.test(e)}}(i.value);break;case\"th\":S=function(e){if(13!==e.length)return{meta:{},valid:!1};for(var t=0,n=0;n<12;n++)t+=parseInt(e.charAt(n),10)*(13-n);return{meta:{},valid:(11-t%11)%10===parseInt(e.charAt(12),10)}}(i.value);break;case\"tr\":S=function(e){if(11!==e.length)return{meta:{},valid:!1};for(var t=0,n=0;n<10;n++)t+=parseInt(e.charAt(n),10);return{meta:{},valid:t%10===parseInt(e.charAt(10),10)}}(i.value);break;case\"tw\":S=function(e){var t=e.toUpperCase();if(!/^[A-Z][12][0-9]{8}$/.test(t))return{meta:{},valid:!1};for(var n=t.length,r=\"ABCDEFGHJKLMNPQRSTUVXYWZIO\".indexOf(t.charAt(0))+10,i=Math.floor(r/10)+r%10*(n-1),a=0,o=1;o<n-1;o++)a+=parseInt(t.charAt(o),10)*(n-1-o);return{meta:{},valid:(i+a+parseInt(t.charAt(n-1),10))%10==0}}(i.value);break;case\"uy\":S=function(e){if(!/^\\d{8}$/.test(e))return{meta:{},valid:!1};for(var t=[2,9,8,7,6,3,4],n=0,r=0;r<7;r++)n+=parseInt(e.charAt(r),10)*t[r];return(n%=10)>0&&(n=10-n),{meta:{},valid:\"\".concat(n)===e.charAt(7)}}(i.value);break;case\"za\":S=function(e){if(!/^[0-9]{10}[0|1][8|9][0-9]$/.test(e))return{meta:{},valid:!1};var t=parseInt(e.substr(0,2),10),n=(new Date).getFullYear()%100,r=parseInt(e.substr(2,2),10),i=parseInt(e.substr(4,2),10);return k(t=t>=n?t+1900:t+2e3,r,i)?{meta:{},valid:w(e)}:{meta:{},valid:!1}}(i.value)}var D=L(i.l10n&&i.l10n.id?a.message||i.l10n.id.country:a.message,i.l10n&&i.l10n.id&&i.l10n.id.countries?i.l10n.id.countries[h.toUpperCase()]:h.toUpperCase());return Object.assign({},{message:D},S)}}},Yn}();var An,On=En.exports,Cn={exports:{}},jn={};Cn.exports=function(){if(An)return jn;An=1;var e=a.algorithms.luhn;return jn.imei=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};switch(!0){case/^\\d{15}$/.test(t.value):case/^\\d{2}-\\d{6}-\\d{6}-\\d{1}$/.test(t.value):case/^\\d{2}\\s\\d{6}\\s\\d{6}\\s\\d{1}$/.test(t.value):return{valid:e(t.value.replace(/[^0-9]/g,\"\"))};case/^\\d{14}$/.test(t.value):case/^\\d{16}$/.test(t.value):case/^\\d{2}-\\d{6}-\\d{6}(|-\\d{2})$/.test(t.value):case/^\\d{2}\\s\\d{6}\\s\\d{6}(|\\s\\d{2})$/.test(t.value):return{valid:!0};default:return{valid:!1}}}}},jn}();var Pn,Hn=Cn.exports,In={exports:{}},Fn={};In.exports=(Pn||(Pn=1,Fn.imo=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};if(!/^IMO \\d{7}$/i.test(e.value))return{valid:!1};for(var t=e.value.replace(/^.*(\\d{7})$/,\"$1\"),n=0,r=6;r>=1;r--)n+=parseInt(t.slice(6-r,-r),10)*(r+1);return{valid:n%10===parseInt(t.charAt(6),10)}}}}),Fn);var Nn,Rn=In.exports,Vn={exports:{}},$n={};Vn.exports=(Nn||(Nn=1,$n.isbn=function(){return{validate:function(e){if(\"\"===e.value)return{meta:{type:null},valid:!0};var t;switch(!0){case/^\\d{9}[\\dX]$/.test(e.value):case 13===e.value.length&&/^(\\d+)-(\\d+)-(\\d+)-([\\dX])$/.test(e.value):case 13===e.value.length&&/^(\\d+)\\s(\\d+)\\s(\\d+)\\s([\\dX])$/.test(e.value):t=\"ISBN10\";break;case/^(978|979)\\d{9}[\\dX]$/.test(e.value):case 17===e.value.length&&/^(978|979)-(\\d+)-(\\d+)-(\\d+)-([\\dX])$/.test(e.value):case 17===e.value.length&&/^(978|979)\\s(\\d+)\\s(\\d+)\\s(\\d+)\\s([\\dX])$/.test(e.value):t=\"ISBN13\";break;default:return{meta:{type:null},valid:!1}}var n,r,i=e.value.replace(/[^0-9X]/gi,\"\").split(\"\"),a=i.length,o=0;switch(t){case\"ISBN10\":for(o=0,n=0;n<a-1;n++)o+=parseInt(i[n],10)*(10-n);return 11==(r=11-o%11)?r=0:10===r&&(r=\"X\"),{meta:{type:t},valid:\"\".concat(r)===i[a-1]};case\"ISBN13\":for(o=0,n=0;n<a-1;n++)o+=n%2==0?parseInt(i[n],10):3*parseInt(i[n],10);return 10==(r=10-o%10)&&(r=\"0\"),{meta:{type:t},valid:\"\".concat(r)===i[a-1]}}}}}),$n);var zn,Wn=Vn.exports,Un={exports:{}},Bn={};Un.exports=(zn||(zn=1,Bn.isin=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var t=e.value.toUpperCase();if(!new RegExp(\"^(AF|AX|AL|DZ|AS|AD|AO|AI|AQ|AG|AR|AM|AW|AU|AT|AZ|BS|BH|BD|BB|BY|BE|BZ|BJ|BM|BT|BO|BQ|BA|BW|BV|BR|IO|BN|BG|BF|BI|KH|CM|CA|CV|KY|CF|TD|CL|CN|CX|CC|CO|KM|CG|CD|CK|CR|CI|HR|CU|CW|CY|CZ|DK|DJ|DM|DO|EC|EG|SV|GQ|ER|EE|ET|FK|FO|FJ|FI|FR|GF|PF|TF|GA|GM|GE|DE|GH|GI|GR|GL|GD|GP|GU|GT|GG|GN|GW|GY|HT|HM|VA|HN|HK|HU|IS|IN|ID|IR|IQ|IE|IM|IL|IT|JM|JP|JE|JO|KZ|KE|KI|KP|KR|KW|KG|LA|LV|LB|LS|LR|LY|LI|LT|LU|MO|MK|MG|MW|MY|MV|ML|MT|MH|MQ|MR|MU|YT|MX|FM|MD|MC|MN|ME|MS|MA|MZ|MM|NA|NR|NP|NL|NC|NZ|NI|NE|NG|NU|NF|MP|NO|OM|PK|PW|PS|PA|PG|PY|PE|PH|PN|PL|PT|PR|QA|RE|RO|RU|RW|BL|SH|KN|LC|MF|PM|VC|WS|SM|ST|SA|SN|RS|SC|SL|SG|SX|SK|SI|SB|SO|ZA|GS|SS|ES|LK|SD|SR|SJ|SZ|SE|CH|SY|TW|TJ|TZ|TH|TL|TG|TK|TO|TT|TN|TR|TM|TC|TV|UG|UA|AE|GB|US|UM|UY|UZ|VU|VE|VN|VG|VI|WF|EH|YE|ZM|ZW)[0-9A-Z]{10}$\").test(e.value))return{valid:!1};var n,r=t.length,i=\"\";for(n=0;n<r-1;n++){var a=t.charCodeAt(n);i+=a>57?(a-55).toString():t.charAt(n)}var o=\"\",s=i.length,l=s%2!=0?0:1;for(n=0;n<s;n++)o+=parseInt(i[n],10)*(n%2===l?2:1)+\"\";var u=0;for(n=0;n<o.length;n++)u+=parseInt(o.charAt(n),10);return{valid:\"\".concat(u=(10-u%10)%10)===t.charAt(r-1)}}}}),Bn);var qn,Gn=Un.exports,Jn={exports:{}},Zn={};Jn.exports=(qn||(qn=1,Zn.ismn=function(){return{validate:function(e){if(\"\"===e.value)return{meta:null,valid:!0};var t;switch(!0){case/^M\\d{9}$/.test(e.value):case/^M-\\d{4}-\\d{4}-\\d{1}$/.test(e.value):case/^M\\s\\d{4}\\s\\d{4}\\s\\d{1}$/.test(e.value):t=\"ISMN10\";break;case/^9790\\d{9}$/.test(e.value):case/^979-0-\\d{4}-\\d{4}-\\d{1}$/.test(e.value):case/^979\\s0\\s\\d{4}\\s\\d{4}\\s\\d{1}$/.test(e.value):t=\"ISMN13\";break;default:return{meta:null,valid:!1}}var n=e.value;\"ISMN10\"===t&&(n=\"9790\".concat(n.substr(1)));for(var r=0,i=(n=n.replace(/[^0-9]/gi,\"\")).length,a=[1,3],o=0;o<i-1;o++)r+=parseInt(n.charAt(o),10)*a[o%2];return{meta:{type:t},valid:\"\".concat(r=(10-r%10)%10)===n.charAt(i-1)}}}}),Zn);var Kn,Xn=Jn.exports,Qn={exports:{}},er={};Qn.exports=(Kn||(Kn=1,er.issn=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};if(!/^\\d{4}-\\d{3}[\\dX]$/.test(e.value))return{valid:!1};var t=e.value.replace(/[^0-9X]/gi,\"\").split(\"\"),n=t.length,r=0;\"X\"===t[7]&&(t[7]=\"10\");for(var i=0;i<n;i++)r+=parseInt(t[i],10)*(8-i);return{valid:r%11==0}}}}),er);var tr,nr=Qn.exports,rr={exports:{}},ir={};rr.exports=(tr||(tr=1,ir.mac=function(){return{validate:function(e){return{valid:\"\"===e.value||/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(e.value)||/^([0-9A-Fa-f]{4}\\.){2}([0-9A-Fa-f]{4})$/.test(e.value)}}}}),ir);var ar,or=rr.exports,sr={exports:{}},lr={};sr.exports=function(){if(ar)return lr;ar=1;var e=a.algorithms.luhn;return lr.meid=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=t.value;if(/^[0-9A-F]{15}$/i.test(n)||/^[0-9A-F]{2}[- ][0-9A-F]{6}[- ][0-9A-F]{6}[- ][0-9A-F]$/i.test(n)||/^\\d{19}$/.test(n)||/^\\d{5}[- ]\\d{5}[- ]\\d{4}[- ]\\d{4}[- ]\\d$/.test(n)){var r=n.charAt(n.length-1).toUpperCase();if((n=n.replace(/[- ]/g,\"\")).match(/^\\d*$/i))return{valid:e(n)};n=n.slice(0,-1);var i=\"\",a=void 0;for(a=1;a<=13;a+=2)i+=(2*parseInt(n.charAt(a),16)).toString(16);var o=0;for(a=0;a<i.length;a++)o+=parseInt(i.charAt(a),16);return{valid:o%10==0?\"0\"===r:r===(2*(10*Math.floor((o+10)/10)-o)).toString(16).toUpperCase()}}return/^[0-9A-F]{14}$/i.test(n)||/^[0-9A-F]{2}[- ][0-9A-F]{6}[- ][0-9A-F]{6}$/i.test(n)||/^\\d{18}$/.test(n)||/^\\d{5}[- ]\\d{5}[- ]\\d{4}[- ]\\d{4}$/.test(n)?{valid:!0}:{valid:!1}}}},lr}();var ur,dr=sr.exports,cr={exports:{}},fr={};cr.exports=function(){if(ur)return fr;ur=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return fr.phone=function(){var e=[\"AE\",\"BG\",\"BR\",\"CN\",\"CZ\",\"DE\",\"DK\",\"ES\",\"FR\",\"GB\",\"IN\",\"MA\",\"NL\",\"PK\",\"RO\",\"RU\",\"SK\",\"TH\",\"US\",\"VE\"];return{validate:function(r){if(\"\"===r.value)return{valid:!0};var i=Object.assign({},{message:\"\"},n(r.options)),a=r.value.trim(),o=a.substr(0,2);if(!(o=\"function\"==typeof i.country?i.country.call(this):i.country)||-1===e.indexOf(o.toUpperCase()))return{valid:!0};var s=!0;switch(o.toUpperCase()){case\"AE\":s=/^(((\\+|00)?971[\\s.-]?(\\(0\\)[\\s.-]?)?|0)(\\(5(0|2|5|6)\\)|5(0|2|5|6)|2|3|4|6|7|9)|60)([\\s.-]?[0-9]){7}$/.test(a);break;case\"BG\":s=/^(0|359|00)(((700|900)[0-9]{5}|((800)[0-9]{5}|(800)[0-9]{4}))|(87|88|89)([0-9]{7})|((2[0-9]{7})|(([3-9][0-9])(([0-9]{6})|([0-9]{5})))))$/.test(a.replace(/\\+|\\s|-|\\/|\\(|\\)/gi,\"\"));break;case\"BR\":s=/^(([\\d]{4}[-.\\s]{1}[\\d]{2,3}[-.\\s]{1}[\\d]{2}[-.\\s]{1}[\\d]{2})|([\\d]{4}[-.\\s]{1}[\\d]{3}[-.\\s]{1}[\\d]{4})|((\\(?\\+?[0-9]{2}\\)?\\s?)?(\\(?\\d{2}\\)?\\s?)?\\d{4,5}[-.\\s]?\\d{4}))$/.test(a);break;case\"CN\":s=/^((00|\\+)?(86(?:-| )))?((\\d{11})|(\\d{3}[- ]{1}\\d{4}[- ]{1}\\d{4})|((\\d{2,4}[- ]){1}(\\d{7,8}|(\\d{3,4}[- ]{1}\\d{4}))([- ]{1}\\d{1,4})?))$/.test(a);break;case\"CZ\":s=/^(((00)([- ]?)|\\+)(420)([- ]?))?((\\d{3})([- ]?)){2}(\\d{3})$/.test(a);break;case\"DE\":s=/^(((((((00|\\+)49[ \\-/]?)|0)[1-9][0-9]{1,4})[ \\-/]?)|((((00|\\+)49\\()|\\(0)[1-9][0-9]{1,4}\\)[ \\-/]?))[0-9]{1,7}([ \\-/]?[0-9]{1,5})?)$/.test(a);break;case\"DK\":s=/^(\\+45|0045|\\(45\\))?\\s?[2-9](\\s?\\d){7}$/.test(a);break;case\"ES\":s=/^(?:(?:(?:\\+|00)34\\D?))?(?:5|6|7|8|9)(?:\\d\\D?){8}$/.test(a);break;case\"FR\":s=/^(?:(?:(?:\\+|00)33[ ]?(?:\\(0\\)[ ]?)?)|0){1}[1-9]{1}([ .-]?)(?:\\d{2}\\1?){3}\\d{2}$/.test(a);break;case\"GB\":s=/^\\(?(?:(?:0(?:0|11)\\)?[\\s-]?\\(?|\\+)44\\)?[\\s-]?\\(?(?:0\\)?[\\s-]?\\(?)?|0)(?:\\d{2}\\)?[\\s-]?\\d{4}[\\s-]?\\d{4}|\\d{3}\\)?[\\s-]?\\d{3}[\\s-]?\\d{3,4}|\\d{4}\\)?[\\s-]?(?:\\d{5}|\\d{3}[\\s-]?\\d{3})|\\d{5}\\)?[\\s-]?\\d{4,5}|8(?:00[\\s-]?11[\\s-]?11|45[\\s-]?46[\\s-]?4\\d))(?:(?:[\\s-]?(?:x|ext\\.?\\s?|#)\\d+)?)$/.test(a);break;case\"IN\":s=/((\\+?)((0[ -]+)*|(91 )*)(\\d{12}|\\d{10}))|\\d{5}([- ]*)\\d{6}/.test(a);break;case\"MA\":s=/^(?:(?:(?:\\+|00)212[\\s]?(?:[\\s]?\\(0\\)[\\s]?)?)|0){1}(?:5[\\s.-]?[2-3]|6[\\s.-]?[13-9]){1}[0-9]{1}(?:[\\s.-]?\\d{2}){3}$/.test(a);break;case\"NL\":s=/^((\\+|00(\\s|\\s?-\\s?)?)31(\\s|\\s?-\\s?)?(\\(0\\)[-\\s]?)?|0)[1-9]((\\s|\\s?-\\s?)?[0-9])((\\s|\\s?-\\s?)?[0-9])((\\s|\\s?-\\s?)?[0-9])\\s?[0-9]\\s?[0-9]\\s?[0-9]\\s?[0-9]\\s?[0-9]$/gm.test(a);break;case\"PK\":s=/^0?3[0-9]{2}[0-9]{7}$/.test(a);break;case\"RO\":s=/^(\\+4|)?(07[0-8]{1}[0-9]{1}|02[0-9]{2}|03[0-9]{2}){1}?(\\s|\\.|-)?([0-9]{3}(\\s|\\.|-|)){2}$/g.test(a);break;case\"RU\":s=/^((8|\\+7|007)[-./ ]?)?([(/.]?\\d{3}[)/.]?[-./ ]?)?[\\d\\-./ ]{7,10}$/g.test(a);break;case\"SK\":s=/^(((00)([- ]?)|\\+)(421)([- ]?))?((\\d{3})([- ]?)){2}(\\d{3})$/.test(a);break;case\"TH\":s=/^0\\(?([6|8-9]{2})*-([0-9]{3})*-([0-9]{4})$/.test(a);break;case\"VE\":s=/^0(?:2(?:12|4[0-9]|5[1-9]|6[0-9]|7[0-8]|8[1-35-8]|9[1-5]|3[45789])|4(?:1[246]|2[46]))\\d{7}$/.test(a);break;default:s=/^(?:(1-?)|(\\+1 ?))?\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}$/.test(a)}return{message:t(r.l10n&&r.l10n.phone?i.message||r.l10n.phone.country:i.message,r.l10n&&r.l10n.phone&&r.l10n.phone.countries?r.l10n.phone.countries[o]:o),valid:s}}}},fr}();var pr,hr=cr.exports,mr={exports:{}},vr={};mr.exports=(pr||(pr=1,vr.rtn=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};if(!/^\\d{9}$/.test(e.value))return{valid:!1};for(var t=0,n=0;n<e.value.length;n+=3)t+=3*parseInt(e.value.charAt(n),10)+7*parseInt(e.value.charAt(n+1),10)+parseInt(e.value.charAt(n+2),10);return{valid:0!==t&&t%10==0}}}}),vr);var gr,_r=mr.exports,yr={exports:{}},br={};yr.exports=(gr||(gr=1,br.sedol=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var t=e.value.toUpperCase();if(!/^[0-9A-Z]{7}$/.test(t))return{valid:!1};for(var n=[1,3,1,7,3,9,1],r=t.length,i=0,a=0;a<r-1;a++)i+=n[a]*parseInt(t.charAt(a),36);return{valid:\"\".concat(i=(10-i%10)%10)===t.charAt(r-1)}}}}),br);var Mr,wr=yr.exports,kr={exports:{}},Lr={};kr.exports=function(){if(Mr)return Lr;Mr=1;var e=a.algorithms.luhn;return Lr.siren=function(){return{validate:function(t){return{valid:\"\"===t.value||/^\\d{9}$/.test(t.value)&&e(t.value)}}}},Lr}();var xr,Tr=kr.exports,Sr={exports:{}},Dr={};Sr.exports=(xr||(xr=1,Dr.siret=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};for(var t,n=e.value.length,r=0,i=0;i<n;i++)t=parseInt(e.value.charAt(i),10),i%2==0&&(t*=2)>9&&(t-=9),r+=t;return{valid:r%10==0}}}}),Dr);var Er,Yr=Sr.exports,Ar={exports:{}},Or={};Ar.exports=function(){if(Er)return Or;Er=1;var e=a.utils.format;return Or.step=function(){return{validate:function(t){if(\"\"===t.value)return{valid:!0};var n=parseFloat(t.value);if(isNaN(n)||!isFinite(n))return{valid:!1};var r=Object.assign({},{baseValue:0,message:\"\",step:1},t.options),i=function(e,t){if(0===t)return 1;var n=\"\".concat(e).split(\".\"),r=\"\".concat(t).split(\".\"),i=(1===n.length?0:n[1].length)+(1===r.length?0:r[1].length);return function(e,t){var n,r=Math.pow(10,t),i=e*r;switch(!0){case 0===i:n=0;break;case i>0:n=1;break;case i<0:n=-1}return i%1==.5*n?(Math.floor(i)+(n>0?1:0))/r:Math.round(i)/r}(e-t*Math.floor(e/t),i)}(n-r.baseValue,r.step);return{message:e(t.l10n?r.message||t.l10n.step.default:r.message,\"\".concat(r.step)),valid:0===i||i===r.step}}}},Or}();var Cr,jr=Ar.exports,Pr={exports:{}},Hr={};Pr.exports=function(){if(Cr)return Hr;Cr=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return Hr.uuid=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};var r=Object.assign({},{message:\"\"},n(e.options)),i={3:/^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i,4:/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,5:/^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,all:/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i},a=r.version?\"\".concat(r.version):\"all\";return{message:r.version?t(e.l10n?r.message||e.l10n.uuid.version:r.message,r.version):e.l10n?e.l10n.uuid.default:r.message,valid:null===i[a]||i[a].test(e.value)}}}},Hr}();var Ir,Fr=Pr.exports,Nr={exports:{}},Rr={};Nr.exports=function(){if(Ir)return Rr;Ir=1;var e=a,t=e.utils.isValidDate,n=e.utils.isValidDate,r=e.algorithms.mod11And10,i=e.algorithms.luhn,o=e.algorithms.mod11And10,s=e.algorithms.luhn,l=e.utils.isValidDate,u=e.algorithms.mod97And10;function d(e){if(e.length<8)return{meta:{},valid:!1};var t=e;if(8===t.length&&(t=\"0\".concat(t)),!/^[0-9]{4}[.]{0,1}[0-9]{2}[.]{0,1}[0-9]{3}$/.test(t))return{meta:{},valid:!1};if(t=t.replace(/\\./g,\"\"),0===parseInt(t,10))return{meta:{},valid:!1};for(var n=0,r=t.length,i=0;i<r-1;i++)n+=(9-i)*parseInt(t.charAt(i),10);return 10==(n%=11)&&(n=0),{meta:{},valid:\"\".concat(n)===t.charAt(r-1)}}var c=e.algorithms.luhn,f=e.utils.format,p=e.utils.removeUndefined;return Rr.vat=function(){var e=[\"AR\",\"AT\",\"BE\",\"BG\",\"BR\",\"CH\",\"CY\",\"CZ\",\"DE\",\"DK\",\"EE\",\"EL\",\"ES\",\"FI\",\"FR\",\"GB\",\"GR\",\"HR\",\"HU\",\"IE\",\"IS\",\"IT\",\"LT\",\"LU\",\"LV\",\"MT\",\"NL\",\"NO\",\"PL\",\"PT\",\"RO\",\"RU\",\"RS\",\"SE\",\"SK\",\"SI\",\"VE\",\"ZA\"];return{validate:function(a){var h=a.value;if(\"\"===h)return{valid:!0};var m=Object.assign({},{message:\"\"},p(a.options)),v=h.substr(0,2);if(v=\"function\"==typeof m.country?m.country.call(this):m.country,-1===e.indexOf(v))return{valid:!0};var g={meta:{},valid:!0};switch(v.toLowerCase()){case\"ar\":g=function(e){var t=e.replace(\"-\",\"\");if(/^AR[0-9]{11}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{11}$/.test(t))return{meta:{},valid:!1};for(var n=[5,4,3,2,7,6,5,4,3,2],r=0,i=0;i<10;i++)r+=parseInt(t.charAt(i),10)*n[i];return 11==(r=11-r%11)&&(r=0),{meta:{},valid:\"\".concat(r)===t.substr(10)}}(h);break;case\"at\":g=function(e){var t=e;if(/^ATU[0-9]{8}$/.test(t)&&(t=t.substr(2)),!/^U[0-9]{8}$/.test(t))return{meta:{},valid:!1};t=t.substr(1);for(var n=[1,2,1,2,1,2,1],r=0,i=0,a=0;a<7;a++)(i=parseInt(t.charAt(a),10)*n[a])>9&&(i=Math.floor(i/10)+i%10),r+=i;return 10==(r=10-(r+4)%10)&&(r=0),{meta:{},valid:\"\".concat(r)===t.substr(7,1)}}(h);break;case\"be\":g=function(e){var t=e;return/^BE[0]?[0-9]{9}$/.test(t)&&(t=t.substr(2)),/^[0]?[0-9]{9}$/.test(t)?(9===t.length&&(t=\"0\".concat(t)),\"0\"===t.substr(1,1)?{meta:{},valid:!1}:{meta:{},valid:(parseInt(t.substr(0,8),10)+parseInt(t.substr(8,2),10))%97==0}):{meta:{},valid:!1}}(h);break;case\"bg\":g=function(e){var n=e;if(/^BG[0-9]{9,10}$/.test(n)&&(n=n.substr(2)),!/^[0-9]{9,10}$/.test(n))return{meta:{},valid:!1};var r=0,i=0;if(9===n.length){for(i=0;i<8;i++)r+=parseInt(n.charAt(i),10)*(i+1);if(10==(r%=11)){for(r=0,i=0;i<8;i++)r+=parseInt(n.charAt(i),10)*(i+3);r%=11}return{meta:{},valid:\"\".concat(r%=10)===n.substr(8)}}return{meta:{},valid:function(e){var n=parseInt(e.substr(0,2),10)+1900,r=parseInt(e.substr(2,2),10),i=parseInt(e.substr(4,2),10);if(r>40?(n+=100,r-=40):r>20&&(n-=100,r-=20),!t(n,r,i))return!1;for(var a=[2,4,8,5,10,9,7,3,6],o=0,s=0;s<9;s++)o+=parseInt(e.charAt(s),10)*a[s];return\"\".concat(o=o%11%10)===e.substr(9,1)}(n)||function(e){for(var t=[21,19,17,13,11,9,7,3,1],n=0,r=0;r<9;r++)n+=parseInt(e.charAt(r),10)*t[r];return\"\".concat(n%=10)===e.substr(9,1)}(n)||function(e){for(var t=[4,3,2,7,6,5,4,3,2],n=0,r=0;r<9;r++)n+=parseInt(e.charAt(r),10)*t[r];return 10!=(n=11-n%11)&&(11===n&&(n=0),\"\".concat(n)===e.substr(9,1))}(n)}}(h);break;case\"br\":g=function(e){if(\"\"===e)return{meta:{},valid:!0};var t=e.replace(/[^\\d]+/g,\"\");if(\"\"===t||14!==t.length)return{meta:{},valid:!1};if(\"00000000000000\"===t||\"11111111111111\"===t||\"22222222222222\"===t||\"33333333333333\"===t||\"44444444444444\"===t||\"55555555555555\"===t||\"66666666666666\"===t||\"77777777777777\"===t||\"88888888888888\"===t||\"99999999999999\"===t)return{meta:{},valid:!1};var n,r=t.length-2,i=t.substring(0,r),a=t.substring(r),o=0,s=r-7;for(n=r;n>=1;n--)o+=parseInt(i.charAt(r-n),10)*s--,s<2&&(s=9);var l=o%11<2?0:11-o%11;if(l!==parseInt(a.charAt(0),10))return{meta:{},valid:!1};for(r+=1,i=t.substring(0,r),o=0,s=r-7,n=r;n>=1;n--)o+=parseInt(i.charAt(r-n),10)*s--,s<2&&(s=9);return{meta:{},valid:(l=o%11<2?0:11-o%11)===parseInt(a.charAt(1),10)}}(h);break;case\"ch\":g=function(e){var t=e;if(/^CHE[0-9]{9}(MWST|TVA|IVA|TPV)?$/.test(t)&&(t=t.substr(2)),!/^E[0-9]{9}(MWST|TVA|IVA|TPV)?$/.test(t))return{meta:{},valid:!1};t=t.substr(1);for(var n=[5,4,3,2,7,6,5,4],r=0,i=0;i<8;i++)r+=parseInt(t.charAt(i),10)*n[i];return 10==(r=11-r%11)?{meta:{},valid:!1}:(11===r&&(r=0),{meta:{},valid:\"\".concat(r)===t.substr(8,1)})}(h);break;case\"cy\":g=function(e){var t=e;if(/^CY[0-5|9][0-9]{7}[A-Z]$/.test(t)&&(t=t.substr(2)),!/^[0-5|9][0-9]{7}[A-Z]$/.test(t))return{meta:{},valid:!1};if(\"12\"===t.substr(0,2))return{meta:{},valid:!1};for(var n=0,r={0:1,1:0,2:5,3:7,4:9,5:13,6:15,7:17,8:19,9:21},i=0;i<8;i++){var a=parseInt(t.charAt(i),10);i%2==0&&(a=r[\"\".concat(a)]),n+=a}return{meta:{},valid:\"\".concat(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"[n%26])===t.substr(8,1)}}(h);break;case\"cz\":g=function(e){var t=e;if(/^CZ[0-9]{8,10}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{8,10}$/.test(t))return{meta:{},valid:!1};var r=0,i=0;if(8===t.length){if(\"9\"===\"\".concat(t.charAt(0)))return{meta:{},valid:!1};for(r=0,i=0;i<7;i++)r+=parseInt(t.charAt(i),10)*(8-i);return 10==(r=11-r%11)&&(r=0),11===r&&(r=1),{meta:{},valid:\"\".concat(r)===t.substr(7,1)}}if(9===t.length&&\"6\"===\"\".concat(t.charAt(0))){for(r=0,i=0;i<7;i++)r+=parseInt(t.charAt(i+1),10)*(8-i);return 10==(r=11-r%11)&&(r=0),11===r&&(r=1),{meta:{},valid:\"\".concat(r=[8,7,6,5,4,3,2,1,0,9,10][r-1])===t.substr(8,1)}}if(9===t.length||10===t.length){var a=1900+parseInt(t.substr(0,2),10),o=parseInt(t.substr(2,2),10)%50%20,s=parseInt(t.substr(4,2),10);if(9===t.length){if(a>=1980&&(a-=100),a>1953)return{meta:{},valid:!1}}else a<1954&&(a+=100);if(!n(a,o,s))return{meta:{},valid:!1};if(10===t.length){var l=parseInt(t.substr(0,9),10)%11;return a<1985&&(l%=10),{meta:{},valid:\"\".concat(l)===t.substr(9,1)}}return{meta:{},valid:!0}}return{meta:{},valid:!1}}(h);break;case\"de\":g=function(e){var t=e;return/^DE[0-9]{9}$/.test(t)&&(t=t.substr(2)),/^[1-9][0-9]{8}$/.test(t)?{meta:{},valid:r(t)}:{meta:{},valid:!1}}(h);break;case\"dk\":g=function(e){var t=e;if(/^DK[0-9]{8}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{8}$/.test(t))return{meta:{},valid:!1};for(var n=0,r=[2,7,6,5,4,3,2,1],i=0;i<8;i++)n+=parseInt(t.charAt(i),10)*r[i];return{meta:{},valid:n%11==0}}(h);break;case\"ee\":g=function(e){var t=e;if(/^EE[0-9]{9}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{9}$/.test(t))return{meta:{},valid:!1};for(var n=0,r=[3,7,1,3,7,1,3,7,1],i=0;i<9;i++)n+=parseInt(t.charAt(i),10)*r[i];return{meta:{},valid:n%10==0}}(h);break;case\"el\":case\"gr\":g=function(e){var t=e;if(/^(GR|EL)[0-9]{9}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{9}$/.test(t))return{meta:{},valid:!1};8===t.length&&(t=\"0\".concat(t));for(var n=[256,128,64,32,16,8,4,2],r=0,i=0;i<8;i++)r+=parseInt(t.charAt(i),10)*n[i];return{meta:{},valid:\"\".concat(r=r%11%10)===t.substr(8,1)}}(h);break;case\"es\":g=function(e){var t=e;if(/^ES[0-9A-Z][0-9]{7}[0-9A-Z]$/.test(t)&&(t=t.substr(2)),!/^[0-9A-Z][0-9]{7}[0-9A-Z]$/.test(t))return{meta:{},valid:!1};var n,r,i=t.charAt(0);return/^[0-9]$/.test(i)?{meta:{type:\"DNI\"},valid:(n=t,r=parseInt(n.substr(0,8),10),\"\".concat(\"TRWAGMYFPDXBNJZSQVHLCKE\"[r%23])===n.substr(8,1))}:/^[XYZ]$/.test(i)?{meta:{type:\"NIE\"},valid:function(e){var t=[\"XYZ\".indexOf(e.charAt(0)),e.substr(1)].join(\"\"),n=\"TRWAGMYFPDXBNJZSQVHLCKE\"[parseInt(t,10)%23];return\"\".concat(n)===e.substr(8,1)}(t)}:{meta:{type:\"CIF\"},valid:function(e){var t,n=e.charAt(0);if(-1!==\"KLM\".indexOf(n))return t=parseInt(e.substr(1,8),10),\"\".concat(t=\"TRWAGMYFPDXBNJZSQVHLCKE\"[t%23])===e.substr(8,1);if(-1!==\"ABCDEFGHJNPQRSUVW\".indexOf(n)){for(var r=[2,1,2,1,2,1,2],i=0,a=0,o=0;o<7;o++)(a=parseInt(e.charAt(o+1),10)*r[o])>9&&(a=Math.floor(a/10)+a%10),i+=a;return 10==(i=10-i%10)&&(i=0),\"\".concat(i)===e.substr(8,1)||\"JABCDEFGHI\"[i]===e.substr(8,1)}return!1}(t)}}(h);break;case\"fi\":g=function(e){var t=e;if(/^FI[0-9]{8}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{8}$/.test(t))return{meta:{},valid:!1};for(var n=[7,9,10,5,8,4,2,1],r=0,i=0;i<8;i++)r+=parseInt(t.charAt(i),10)*n[i];return{meta:{},valid:r%11==0}}(h);break;case\"fr\":g=function(e){var t=e;if(/^FR[0-9A-Z]{2}[0-9]{9}$/.test(t)&&(t=t.substr(2)),!/^[0-9A-Z]{2}[0-9]{9}$/.test(t))return{meta:{},valid:!1};if(\"000\"!==t.substr(2,4))return{meta:{},valid:i(t.substr(2))};if(/^[0-9]{2}$/.test(t.substr(0,2)))return{meta:{},valid:t.substr(0,2)===\"\".concat(parseInt(t.substr(2)+\"12\",10)%97)};var n,r=\"0123456789ABCDEFGHJKLMNPQRSTUVWXYZ\";return n=/^[0-9]$/.test(t.charAt(0))?24*r.indexOf(t.charAt(0))+r.indexOf(t.charAt(1))-10:34*r.indexOf(t.charAt(0))+r.indexOf(t.charAt(1))-100,{meta:{},valid:(parseInt(t.substr(2),10)+1+Math.floor(n/11))%11==n%11}}(h);break;case\"gb\":g=function(e){var t=e;if((/^GB[0-9]{9}$/.test(t)||/^GB[0-9]{12}$/.test(t)||/^GBGD[0-9]{3}$/.test(t)||/^GBHA[0-9]{3}$/.test(t)||/^GB(GD|HA)8888[0-9]{5}$/.test(t))&&(t=t.substr(2)),!(/^[0-9]{9}$/.test(t)||/^[0-9]{12}$/.test(t)||/^GD[0-9]{3}$/.test(t)||/^HA[0-9]{3}$/.test(t)||/^(GD|HA)8888[0-9]{5}$/.test(t)))return{meta:{},valid:!1};var n=t.length;if(5===n){var r=t.substr(0,2),i=parseInt(t.substr(2),10);return{meta:{},valid:\"GD\"===r&&i<500||\"HA\"===r&&i>=500}}if(11===n&&(\"GD8888\"===t.substr(0,6)||\"HA8888\"===t.substr(0,6)))return\"GD\"===t.substr(0,2)&&parseInt(t.substr(6,3),10)>=500||\"HA\"===t.substr(0,2)&&parseInt(t.substr(6,3),10)<500?{meta:{},valid:!1}:{meta:{},valid:parseInt(t.substr(6,3),10)%97===parseInt(t.substr(9,2),10)};if(9===n||12===n){for(var a=[8,7,6,5,4,3,2,10,1],o=0,s=0;s<9;s++)o+=parseInt(t.charAt(s),10)*a[s];return o%=97,{meta:{},valid:parseInt(t.substr(0,3),10)>=100?0===o||42===o||55===o:0===o}}return{meta:{},valid:!0}}(h);break;case\"hr\":g=function(e){var t=e;return/^HR[0-9]{11}$/.test(t)&&(t=t.substr(2)),/^[0-9]{11}$/.test(t)?{meta:{},valid:o(t)}:{meta:{},valid:!1}}(h);break;case\"hu\":g=function(e){var t=e;if(/^HU[0-9]{8}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{8}$/.test(t))return{meta:{},valid:!1};for(var n=[9,7,3,1,9,7,3,1],r=0,i=0;i<8;i++)r+=parseInt(t.charAt(i),10)*n[i];return{meta:{},valid:r%10==0}}(h);break;case\"ie\":g=function(e){var t=e;if(/^IE[0-9][0-9A-Z*+][0-9]{5}[A-Z]{1,2}$/.test(t)&&(t=t.substr(2)),!/^[0-9][0-9A-Z*+][0-9]{5}[A-Z]{1,2}$/.test(t))return{meta:{},valid:!1};var n=function(e){for(var t=e;t.length<7;)t=\"0\".concat(t);for(var n=\"WABCDEFGHIJKLMNOPQRSTUV\",r=0,i=0;i<7;i++)r+=parseInt(t.charAt(i),10)*(8-i);return r+=9*n.indexOf(t.substr(7)),n[r%23]};return/^[0-9]+$/.test(t.substr(0,7))?{meta:{},valid:t.charAt(7)===n(\"\".concat(t.substr(0,7)).concat(t.substr(8)))}:-1!==\"ABCDEFGHIJKLMNOPQRSTUVWXYZ+*\".indexOf(t.charAt(1))?{meta:{},valid:t.charAt(7)===n(\"\".concat(t.substr(2,5)).concat(t.substr(0,1)))}:{meta:{},valid:!0}}(h);break;case\"is\":g=function(e){var t=e;return/^IS[0-9]{5,6}$/.test(t)&&(t=t.substr(2)),{meta:{},valid:/^[0-9]{5,6}$/.test(t)}}(h);break;case\"it\":g=function(e){var t=e;if(/^IT[0-9]{11}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{11}$/.test(t))return{meta:{},valid:!1};if(0===parseInt(t.substr(0,7),10))return{meta:{},valid:!1};var n=parseInt(t.substr(7,3),10);return n<1||n>201&&999!==n&&888!==n?{meta:{},valid:!1}:{meta:{},valid:s(t)}}(h);break;case\"lt\":g=function(e){var t=e;if(/^LT([0-9]{7}1[0-9]|[0-9]{10}1[0-9])$/.test(t)&&(t=t.substr(2)),!/^([0-9]{7}1[0-9]|[0-9]{10}1[0-9])$/.test(t))return{meta:{},valid:!1};var n,r=t.length,i=0;for(n=0;n<r-1;n++)i+=parseInt(t.charAt(n),10)*(1+n%9);var a=i%11;if(10===a)for(i=0,n=0;n<r-1;n++)i+=parseInt(t.charAt(n),10)*(1+(n+2)%9);return{meta:{},valid:\"\".concat(a=a%11%10)===t.charAt(r-1)}}(h);break;case\"lu\":g=function(e){var t=e;return/^LU[0-9]{8}$/.test(t)&&(t=t.substring(2)),/^[0-9]{8}$/.test(t)?{meta:{},valid:parseInt(t.substring(0,6),10)%89===parseInt(t.substring(6,8),10)}:{meta:{},valid:!1}}(h);break;case\"lv\":g=function(e){var t=e;if(/^LV[0-9]{11}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{11}$/.test(t))return{meta:{},valid:!1};var n,r=parseInt(t.charAt(0),10),i=t.length,a=0,o=[];if(r>3){for(a=0,o=[9,1,4,8,3,10,2,5,7,6,1],n=0;n<i;n++)a+=parseInt(t.charAt(n),10)*o[n];return{meta:{},valid:3==(a%=11)}}var s=parseInt(t.substr(0,2),10),u=parseInt(t.substr(2,2),10),d=parseInt(t.substr(4,2),10);if(d=d+1800+100*parseInt(t.charAt(6),10),!l(d,u,s))return{meta:{},valid:!1};for(a=0,o=[10,5,8,4,2,1,6,3,7,9],n=0;n<i-1;n++)a+=parseInt(t.charAt(n),10)*o[n];return{meta:{},valid:\"\".concat(a=(a+1)%11%10)===t.charAt(i-1)}}(h);break;case\"mt\":g=function(e){var t=e;if(/^MT[0-9]{8}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{8}$/.test(t))return{meta:{},valid:!1};for(var n=[3,4,6,7,8,9,10,1],r=0,i=0;i<8;i++)r+=parseInt(t.charAt(i),10)*n[i];return{meta:{},valid:r%37==0}}(h);break;case\"nl\":g=function(e){var t=e;return/^NL[0-9]{9}B[0-9]{2}$/.test(t)&&(t=t.substr(2)),/^[0-9]{9}B[0-9]{2}$/.test(t)?{meta:{},valid:d(t.substr(0,9)).valid||u(\"NL\".concat(t))}:{meta:{},valid:!1}}(h);break;case\"no\":g=function(e){var t=e;if(/^NO[0-9]{9}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{9}$/.test(t))return{meta:{},valid:!1};for(var n=[3,2,7,6,5,4,3,2],r=0,i=0;i<8;i++)r+=parseInt(t.charAt(i),10)*n[i];return 11==(r=11-r%11)&&(r=0),{meta:{},valid:\"\".concat(r)===t.substr(8,1)}}(h);break;case\"pl\":g=function(e){var t=e;if(/^PL[0-9]{10}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{10}$/.test(t))return{meta:{},valid:!1};for(var n=[6,5,7,2,3,4,5,6,7,-1],r=0,i=0;i<10;i++)r+=parseInt(t.charAt(i),10)*n[i];return{meta:{},valid:r%11==0}}(h);break;case\"pt\":g=function(e){var t=e;if(/^PT[0-9]{9}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{9}$/.test(t))return{meta:{},valid:!1};for(var n=[9,8,7,6,5,4,3,2],r=0,i=0;i<8;i++)r+=parseInt(t.charAt(i),10)*n[i];return(r=11-r%11)>9&&(r=0),{meta:{},valid:\"\".concat(r)===t.substr(8,1)}}(h);break;case\"ro\":g=function(e){var t=e;if(/^RO[1-9][0-9]{1,9}$/.test(t)&&(t=t.substr(2)),!/^[1-9][0-9]{1,9}$/.test(t))return{meta:{},valid:!1};for(var n=t.length,r=[7,5,3,2,1,7,5,3,2].slice(10-n),i=0,a=0;a<n-1;a++)i+=parseInt(t.charAt(a),10)*r[a];return{meta:{},valid:\"\".concat(i=10*i%11%10)===t.substr(n-1,1)}}(h);break;case\"rs\":g=function(e){var t=e;if(/^RS[0-9]{9}$/.test(t)&&(t=t.substr(2)),!/^[0-9]{9}$/.test(t))return{meta:{},valid:!1};for(var n=10,r=0,i=0;i<8;i++)0==(r=(parseInt(t.charAt(i),10)+n)%10)&&(r=10),n=2*r%11;return{meta:{},valid:(n+parseInt(t.substr(8,1),10))%10==1}}(h);break;case\"ru\":g=function(e){var t=e;if(/^RU([0-9]{10}|[0-9]{12})$/.test(t)&&(t=t.substr(2)),!/^([0-9]{10}|[0-9]{12})$/.test(t))return{meta:{},valid:!1};var n=0;if(10===t.length){var r=[2,4,10,3,5,9,4,6,8,0],i=0;for(n=0;n<10;n++)i+=parseInt(t.charAt(n),10)*r[n];return(i%=11)>9&&(i%=10),{meta:{},valid:\"\".concat(i)===t.substr(9,1)}}if(12===t.length){var a=[7,2,4,10,3,5,9,4,6,8,0],o=[3,7,2,4,10,3,5,9,4,6,8,0],s=0,l=0;for(n=0;n<11;n++)s+=parseInt(t.charAt(n),10)*a[n],l+=parseInt(t.charAt(n),10)*o[n];return(s%=11)>9&&(s%=10),(l%=11)>9&&(l%=10),{meta:{},valid:\"\".concat(s)===t.substr(10,1)&&\"\".concat(l)===t.substr(11,1)}}return{meta:{},valid:!0}}(h);break;case\"se\":g=function(e){var t=e;return/^SE[0-9]{10}01$/.test(t)&&(t=t.substr(2)),/^[0-9]{10}01$/.test(t)?(t=t.substr(0,10),{meta:{},valid:c(t)}):{meta:{},valid:!1}}(h);break;case\"si\":g=function(e){var t=e.match(/^(SI)?([1-9][0-9]{7})$/);if(!t)return{meta:{},valid:!1};for(var n=t[1]?e.substr(2):e,r=[8,7,6,5,4,3,2],i=0,a=0;a<7;a++)i+=parseInt(n.charAt(a),10)*r[a];return 10==(i=11-i%11)&&(i=0),{meta:{},valid:\"\".concat(i)===n.substr(7,1)}}(h);break;case\"sk\":g=function(e){var t=e;return/^SK[1-9][0-9][(2-4)|(6-9)][0-9]{7}$/.test(t)&&(t=t.substr(2)),/^[1-9][0-9][(2-4)|(6-9)][0-9]{7}$/.test(t)?{meta:{},valid:parseInt(t,10)%11==0}:{meta:{},valid:!1}}(h);break;case\"ve\":g=function(e){var t=e;if(/^VE[VEJPG][0-9]{9}$/.test(t)&&(t=t.substr(2)),!/^[VEJPG][0-9]{9}$/.test(t))return{meta:{},valid:!1};for(var n=[3,2,7,6,5,4,3,2],r={E:8,G:20,J:12,P:16,V:4}[t.charAt(0)],i=0;i<8;i++)r+=parseInt(t.charAt(i+1),10)*n[i];return 11!=(r=11-r%11)&&10!==r||(r=0),{meta:{},valid:\"\".concat(r)===t.substr(9,1)}}(h);break;case\"za\":g=function(e){var t=e;return/^ZA4[0-9]{9}$/.test(t)&&(t=t.substr(2)),{meta:{},valid:/^4[0-9]{9}$/.test(t)}}(h)}var _=f(a.l10n&&a.l10n.vat?m.message||a.l10n.vat.country:m.message,a.l10n&&a.l10n.vat&&a.l10n.vat.countries?a.l10n.vat.countries[v.toUpperCase()]:v.toUpperCase());return Object.assign({},{message:_},g)}}},Rr}();var Vr,$r=Nr.exports,zr={exports:{}},Wr={};zr.exports=(Vr||(Vr=1,Wr.vin=function(){return{validate:function(e){if(\"\"===e.value)return{valid:!0};if(!/^[a-hj-npr-z0-9]{8}[0-9xX][a-hj-npr-z0-9]{8}$/i.test(e.value))return{valid:!1};for(var t=e.value.toUpperCase(),n={A:1,B:2,C:3,D:4,E:5,F:6,G:7,H:8,J:1,K:2,L:3,M:4,N:5,P:7,R:9,S:2,T:3,U:4,V:5,W:6,X:7,Y:8,Z:9,0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9},r=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],i=t.length,a=0,o=0;o<i;o++)a+=n[\"\".concat(t.charAt(o))]*r[o];var s=\"\".concat(a%11);return\"10\"===s&&(s=\"X\"),{valid:s===t.charAt(8)}}}}),Wr);var Ur,Br=zr.exports,qr={exports:{}},Gr={};qr.exports=function(){if(Ur)return Gr;Ur=1;var e=a,t=e.utils.format,n=e.utils.removeUndefined;return Gr.zipCode=function(){var e=[\"AT\",\"BG\",\"BR\",\"CA\",\"CH\",\"CZ\",\"DE\",\"DK\",\"ES\",\"FR\",\"GB\",\"IE\",\"IN\",\"IT\",\"MA\",\"NL\",\"PL\",\"PT\",\"RO\",\"RU\",\"SE\",\"SG\",\"SK\",\"US\"];return{validate:function(r){var i=Object.assign({},{message:\"\"},n(r.options));if(\"\"===r.value||!i.country)return{valid:!0};var a=r.value.substr(0,2);if(!(a=\"function\"==typeof i.country?i.country.call(this):i.country)||-1===e.indexOf(a.toUpperCase()))return{valid:!0};var o=!1;switch(a=a.toUpperCase()){case\"AT\":case\"CH\":o=/^([1-9]{1})(\\d{3})$/.test(r.value);break;case\"BG\":o=/^([1-9]{1}[0-9]{3})$/.test(r.value);break;case\"BR\":o=/^(\\d{2})([.]?)(\\d{3})([-]?)(\\d{3})$/.test(r.value);break;case\"CA\":o=/^(?:A|B|C|E|G|H|J|K|L|M|N|P|R|S|T|V|X|Y){1}[0-9]{1}(?:A|B|C|E|G|H|J|K|L|M|N|P|R|S|T|V|W|X|Y|Z){1}\\s?[0-9]{1}(?:A|B|C|E|G|H|J|K|L|M|N|P|R|S|T|V|W|X|Y|Z){1}[0-9]{1}$/i.test(r.value);break;case\"CZ\":case\"SK\":o=/^(\\d{3})([ ]?)(\\d{2})$/.test(r.value);break;case\"DE\":o=/^(?!01000|99999)(0[1-9]\\d{3}|[1-9]\\d{4})$/.test(r.value);break;case\"DK\":o=/^(DK(-|\\s)?)?\\d{4}$/i.test(r.value);break;case\"ES\":o=/^(?:0[1-9]|[1-4][0-9]|5[0-2])\\d{3}$/.test(r.value);break;case\"FR\":o=/^[0-9]{5}$/i.test(r.value);break;case\"GB\":o=function(e){for(var t=\"[ABCDEFGHIJKLMNOPRSTUWYZ]\",n=\"[ABCDEFGHKLMNOPQRSTUVWXY]\",r=\"[ABDEFGHJLNPQRSTUWXYZ]\",i=0,a=[new RegExp(\"^(\".concat(t,\"{1}\").concat(n,\"?[0-9]{1,2})(\\\\s*)([0-9]{1}\").concat(r,\"{2})$\"),\"i\"),new RegExp(\"^(\".concat(t,\"{1}[0-9]{1}\").concat(\"[ABCDEFGHJKPMNRSTUVWXY]\",\"{1})(\\\\s*)([0-9]{1}\").concat(r,\"{2})$\"),\"i\"),new RegExp(\"^(\".concat(t,\"{1}\").concat(n,\"{1}?[0-9]{1}\").concat(\"[ABEHMNPRVWXY]\",\"{1})(\\\\s*)([0-9]{1}\").concat(r,\"{2})$\"),\"i\"),new RegExp(\"^(BF1)(\\\\s*)([0-6]{1}[ABDEFGHJLNPQRST]{1}[ABDEFGHJLNPQRSTUWZYZ]{1})$\",\"i\"),/^(GIR)(\\s*)(0AA)$/i,/^(BFPO)(\\s*)([0-9]{1,4})$/i,/^(BFPO)(\\s*)(c\\/o\\s*[0-9]{1,3})$/i,/^([A-Z]{4})(\\s*)(1ZZ)$/i,/^(AI-2640)$/i];i<a.length;i++)if(a[i].test(e))return!0;return!1}(r.value);break;case\"IN\":o=/^\\d{3}\\s?\\d{3}$/.test(r.value);break;case\"IE\":o=/^(D6W|[ACDEFHKNPRTVWXY]\\d{2})\\s[0-9ACDEFHKNPRTVWXY]{4}$/.test(r.value);break;case\"IT\":o=/^(I-|IT-)?\\d{5}$/i.test(r.value);break;case\"MA\":o=/^[1-9][0-9]{4}$/i.test(r.value);break;case\"NL\":o=/^[1-9][0-9]{3} ?(?!sa|sd|ss)[a-z]{2}$/i.test(r.value);break;case\"PL\":o=/^[0-9]{2}-[0-9]{3}$/.test(r.value);break;case\"PT\":o=/^[1-9]\\d{3}-\\d{3}$/.test(r.value);break;case\"RO\":o=/^(0[1-8]{1}|[1-9]{1}[0-5]{1})?[0-9]{4}$/i.test(r.value);break;case\"RU\":o=/^[0-9]{6}$/i.test(r.value);break;case\"SE\":o=/^(S-)?\\d{3}\\s?\\d{2}$/i.test(r.value);break;case\"SG\":o=/^([0][1-9]|[1-6][0-9]|[7]([0-3]|[5-9])|[8][0-2])(\\d{4})$/i.test(r.value);break;default:o=/^\\d{4,5}([-]?\\d{4})?$/.test(r.value)}return{message:t(r.l10n&&r.l10n.zipCode?i.message||r.l10n.zipCode.country:i.message,r.l10n&&r.l10n.zipCode&&r.l10n.zipCode.countries?r.l10n.zipCode.countries[a]:a),valid:o}}}},Gr}();var Jr=qr.exports,Zr={Alias:u.Alias,Aria:p.Aria,Declarative:g.Declarative,DefaultSubmit:M.DefaultSubmit,Dependency:x.Dependency,Excluded:E.Excluded,FieldStatus:C.FieldStatus,Framework:V.Framework,Icon:U.Icon,Message:N.Message,Sequence:J.Sequence,SubmitButton:Q.SubmitButton,Tooltip:re.Tooltip,Trigger:se.Trigger},Kr={between:ce.between,blank:me.blank,callback:ye.callback,choice:ke.choice,creditCard:Se.creditCard,date:Ae.date,different:Pe.different,digits:Ne.digits,emailAddress:ze.emailAddress,file:qe.file,greaterThan:Ke.greaterThan,identical:tt.identical,integer:at.integer,ip:ut.ip,lessThan:pt.lessThan,notEmpty:gt.notEmpty,numeric:Mt.numeric,promise:xt.promise,regexp:Et.regexp,remote:Ct.remote,stringCase:It.stringCase,stringLength:Vt.stringLength,uri:Ut.uri,base64:Jt.base64,bic:Qt.bic,color:rn.color,cusip:ln.cusip,ean:fn.ean,ein:vn.ein,grid:bn.grid,hex:Ln.hex,iban:Dn.iban,id:On.id,imei:Hn.imei,imo:Rn.imo,isbn:Wn.isbn,isin:Gn.isin,ismn:Xn.ismn,issn:nr.issn,mac:or.mac,meid:dr.meid,phone:hr.phone,rtn:_r.rtn,sedol:wr.sedol,siren:Tr.siren,siret:Yr.siret,step:jr.step,uuid:Fr.uuid,vat:$r.vat,vin:Br.vin,zipCode:Jr.zipCode};e.Plugin=a.Plugin,e.algorithms=a.algorithms,e.formValidation=function(e,t){var n=a.formValidation(e,t);return Object.keys(Kr).forEach(function(e){return n.registerValidator(e,Kr[e])}),n},e.plugins=Zr,e.utils=a.utils,e.validators=Kr}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t(require(\"@form-validation/core\"),require(\"@form-validation/plugin-framework\")):\"function\"==typeof define&&define.amd?define([\"@form-validation/core\",\"@form-validation/plugin-framework\"],t):((e=\"undefined\"!=typeof globalThis?globalThis:e||self).FormValidation=e.FormValidation||{},e.FormValidation.plugins=e.FormValidation.plugins||{},e.FormValidation.plugins.Bootstrap5=t(e.FormValidation,e.FormValidation.plugins))}(this,function(e,t){\"use strict\";var n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},n(e,t)},r=e.utils.classSet,i=e.utils.hasClass;return function(e){function t(t){var n=e.call(this,Object.assign({},{eleInvalidClass:\"is-invalid\",eleValidClass:\"is-valid\",formClass:\"fv-plugins-bootstrap5\",rowInvalidClass:\"fv-plugins-bootstrap5-row-invalid\",rowPattern:/^(.*)(col|offset)(-(sm|md|lg|xl))*-[0-9]+(.*)$/,rowSelector:\".row\",rowValidClass:\"fv-plugins-bootstrap5-row-valid\"},t))||this;return n.eleValidatedHandler=n.handleElementValidated.bind(n),n}return function(e,t){if(\"function\"!=typeof t&&null!==t)throw new TypeError(\"Class extends value \"+String(t)+\" is not a constructor or null\");function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}(t,e),t.prototype.install=function(){e.prototype.install.call(this),this.core.on(\"core.element.validated\",this.eleValidatedHandler)},t.prototype.uninstall=function(){e.prototype.uninstall.call(this),this.core.off(\"core.element.validated\",this.eleValidatedHandler)},t.prototype.handleElementValidated=function(e){var t=e.element.getAttribute(\"type\");if((\"checkbox\"===t||\"radio\"===t)&&e.elements.length>1&&i(e.element,\"form-check-input\")){var n=e.element.parentElement;i(n,\"form-check\")&&i(n,\"form-check-inline\")&&r(n,{\"is-invalid\":!e.valid,\"is-valid\":e.valid})}},t.prototype.onIconPlaced=function(e){r(e.element,{\"fv-plugins-icon-input\":!0});var t=e.element.parentElement;i(t,\"input-group\")&&(t.parentElement.insertBefore(e.iconElement,t.nextSibling),e.element.nextElementSibling&&i(e.element.nextElementSibling,\"input-group-text\")&&r(e.iconElement,{\"fv-plugins-icon-input-group\":!0}));var n=e.element.getAttribute(\"type\");if(\"checkbox\"===n||\"radio\"===n){var a=t.parentElement;i(t,\"form-check\")?(r(e.iconElement,{\"fv-plugins-icon-check\":!0}),t.parentElement.insertBefore(e.iconElement,t.nextSibling)):i(t.parentElement,\"form-check\")&&(r(e.iconElement,{\"fv-plugins-icon-check\":!0}),a.parentElement.insertBefore(e.iconElement,a.nextSibling))}},t.prototype.onMessagePlaced=function(e){e.messageElement.classList.add(\"invalid-feedback\");var t=e.element.parentElement;if(i(t,\"input-group\"))return t.appendChild(e.messageElement),void r(t,{\"has-validation\":!0});var n=e.element.getAttribute(\"type\");\"checkbox\"!==n&&\"radio\"!==n||!i(e.element,\"form-check-input\")||i(t,\"form-check\")||i(t,\"form-check-inline\")||e.elements[e.elements.length-1].parentElement.appendChild(e.messageElement)},t}(t.Framework)}),function(e,t){if(\"object\"==typeof exports&&\"object\"==typeof module)module.exports=t();else if(\"function\"==typeof define&&define.amd)define([],t);else{var n=t();for(var r in n)(\"object\"==typeof exports?exports:e)[r]=n[r]}}(\"undefined\"!=typeof self?self:this,function(){return function(){\"use strict\";var e={3976:function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=void 0,t.default={_maxTestPos:500,placeholder:\"_\",optionalmarker:[\"[\",\"]\"],quantifiermarker:[\"{\",\"}\"],groupmarker:[\"(\",\")\"],alternatormarker:\"|\",escapeChar:\"\\\\\",mask:null,regex:null,oncomplete:function(){},onincomplete:function(){},oncleared:function(){},repeat:0,greedy:!1,autoUnmask:!1,removeMaskOnSubmit:!1,clearMaskOnLostFocus:!0,insertMode:!0,insertModeVisual:!0,clearIncomplete:!1,alias:null,onKeyDown:function(){},onBeforeMask:null,onBeforePaste:function(e,t){return\"function\"==typeof t.onBeforeMask?t.onBeforeMask.call(this,e,t):e},onBeforeWrite:null,onUnMask:null,showMaskOnFocus:!0,showMaskOnHover:!0,onKeyValidation:function(){},skipOptionalPartCharacter:\" \",numericInput:!1,rightAlign:!1,undoOnEscape:!0,radixPoint:\"\",_radixDance:!1,groupSeparator:\"\",keepStatic:null,positionCaretOnTab:!0,tabThrough:!1,supportsInputType:[\"text\",\"tel\",\"url\",\"password\",\"search\"],isComplete:null,preValidation:null,postValidation:null,staticDefinitionSymbol:void 0,jitMasking:!1,nullable:!0,inputEventOnly:!1,noValuePatching:!1,positionCaretOnClick:\"lvp\",casing:null,inputmode:\"text\",importDataAttributes:!0,shiftPositions:!0,usePrototypeDefinitions:!0,validationEventTimeOut:3e3,substitutes:{}}},7392:function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=void 0,t.default={9:{validator:\"[0-9０-９]\",definitionSymbol:\"*\"},a:{validator:\"[A-Za-zА-яЁёÀ-ÿµ]\",definitionSymbol:\"*\"},\"*\":{validator:\"[0-9０-９A-Za-zА-яЁёÀ-ÿµ]\"}}},253:function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function(e,t,n){if(void 0===n)return e.__data?e.__data[t]:null;e.__data=e.__data||{},e.__data[t]=n}},3776:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.Event=void 0,t.off=function(e,t){var n,r;return d(this[0])&&e&&(n=this[0].eventRegistry,r=this[0],e.split(\" \").forEach(function(e){var i=o(e.split(\".\"),2);(function(e,r){var i,a,o=[];if(e.length>0)if(void 0===t)for(i=0,a=n[e][r].length;i<a;i++)o.push({ev:e,namespace:r&&r.length>0?r:\"global\",handler:n[e][r][i]});else o.push({ev:e,namespace:r&&r.length>0?r:\"global\",handler:t});else if(r.length>0)for(var s in n)for(var l in n[s])if(l===r)if(void 0===t)for(i=0,a=n[s][l].length;i<a;i++)o.push({ev:s,namespace:l,handler:n[s][l][i]});else o.push({ev:s,namespace:l,handler:t});return o})(i[0],i[1]).forEach(function(e){var t=e.ev,i=e.handler;!function(e,t,i){if(e in n==1)if(r.removeEventListener?r.removeEventListener(e,i,!1):r.detachEvent&&r.detachEvent(\"on\".concat(e),i),\"global\"===t)for(var a in n[e])n[e][a].splice(n[e][a].indexOf(i),1);else n[e][t].splice(n[e][t].indexOf(i),1)}(t,e.namespace,i)})})),this},t.on=function(e,t){if(d(this[0])){var n=this[0].eventRegistry,r=this[0];e.split(\" \").forEach(function(e){var i=o(e.split(\".\"),2),a=i[0],s=i[1];!function(e,i){r.addEventListener?r.addEventListener(e,t,!1):r.attachEvent&&r.attachEvent(\"on\".concat(e),t),n[e]=n[e]||{},n[e][i]=n[e][i]||[],n[e][i].push(t)}(a,void 0===s?\"global\":s)})}return this},t.trigger=function(e){var t=arguments;if(d(this[0]))for(var n=this[0].eventRegistry,r=this[0],o=\"string\"==typeof e?e.split(\" \"):[e.type],s=0;s<o.length;s++){var l=o[s].split(\".\"),c=l[0],f=l[1]||\"global\";if(void 0!==u&&\"global\"===f){var p,h={bubbles:!0,cancelable:!0,composed:!0,detail:arguments[1]};if(u.createEvent){try{\"input\"===c?(h.inputType=\"insertText\",p=new InputEvent(c,h)):p=new CustomEvent(c,h)}catch(e){(p=u.createEvent(\"CustomEvent\")).initCustomEvent(c,h.bubbles,h.cancelable,h.detail)}e.type&&(0,i.default)(p,e),r.dispatchEvent(p)}else(p=u.createEventObject()).eventType=c,p.detail=arguments[1],e.type&&(0,i.default)(p,e),r.fireEvent(\"on\"+p.eventType,p)}else if(void 0!==n[c]){arguments[0]=arguments[0].type?arguments[0]:a.default.Event(arguments[0]),arguments[0].detail=arguments.slice(1);var m=n[c];(\"global\"===f?Object.values(m).flat():m[f]).forEach(function(e){return e.apply(r,t)})}}return this};var r=l(n(9380)),i=l(n(600)),a=l(n(4963));function o(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:\"undefined\"!=typeof Symbol&&e[Symbol.iterator]||e[\"@@iterator\"];if(null!=n){var r,i,a,o,s=[],l=!0,u=!1;try{if(a=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=a.call(n)).done)&&(s.push(r.value),s.length!==t);l=!0);}catch(e){u=!0,i=e}finally{try{if(!l&&null!=n.return&&(o=n.return(),Object(o)!==o))return}finally{if(u)throw i}}return s}}(e,t)||function(e,t){if(e){if(\"string\"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return\"Object\"===n&&e.constructor&&(n=e.constructor.name),\"Map\"===n||\"Set\"===n?Array.from(e):\"Arguments\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?s(e,t):void 0}}(e,t)||function(){throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function l(e){return e&&e.__esModule?e:{default:e}}var u=r.default.document;function d(e){return e instanceof Element}var c=t.Event=void 0;\"function\"==typeof r.default.CustomEvent?t.Event=c=r.default.CustomEvent:r.default.Event&&u&&u.createEvent?(t.Event=c=function(e,t){t=t||{bubbles:!1,cancelable:!1,composed:!0,detail:void 0};var n=u.createEvent(\"CustomEvent\");return n.initCustomEvent(e,t.bubbles,t.cancelable,t.detail),n},c.prototype=r.default.Event.prototype):\"undefined\"!=typeof Event&&(t.Event=c=Event)},600:function(e,t){function n(e){return n=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},n(e)}Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function e(){var t,r,i,a,o,s,l=arguments[0]||{},u=1,d=arguments.length,c=!1;for(\"boolean\"==typeof l&&(c=l,l=arguments[u]||{},u++),\"object\"!==n(l)&&\"function\"!=typeof l&&(l={});u<d;u++)if(null!=(t=arguments[u]))for(r in t)i=l[r],l!==(a=t[r])&&(c&&a&&(\"[object Object]\"===Object.prototype.toString.call(a)||(o=Array.isArray(a)))?(o?(o=!1,s=i&&Array.isArray(i)?i:[]):s=i&&\"[object Object]\"===Object.prototype.toString.call(i)?i:{},l[r]=e(c,s,a)):void 0!==a&&(l[r]=a));return l}},4963:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=void 0;var r=s(n(9380)),i=s(n(253)),a=n(3776),o=s(n(600));function s(e){return e&&e.__esModule?e:{default:e}}var l=r.default.document;function u(e){return e instanceof u?e:this instanceof u?void(null!=e&&e!==r.default&&(this[0]=e.nodeName?e:void 0!==e[0]&&e[0].nodeName?e[0]:l.querySelector(e),void 0!==this[0]&&null!==this[0]&&(this[0].eventRegistry=this[0].eventRegistry||{}))):new u(e)}u.prototype={on:a.on,off:a.off,trigger:a.trigger},u.extend=o.default,u.data=i.default,u.Event=a.Event,t.default=u},9845:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.mobile=t.iphone=t.ie=void 0;var r,i=(r=n(9380))&&r.__esModule?r:{default:r},a=i.default.navigator&&i.default.navigator.userAgent||\"\";t.ie=a.indexOf(\"MSIE \")>0||a.indexOf(\"Trident/\")>0,t.mobile=i.default.navigator&&i.default.navigator.userAgentData&&i.default.navigator.userAgentData.mobile||i.default.navigator&&i.default.navigator.maxTouchPoints||\"ontouchstart\"in i.default,t.iphone=/iphone/i.test(a)},7184:function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function(e){return e.replace(n,\"\\\\$1\")};var n=new RegExp(\"(\\\\\"+[\"/\",\".\",\"*\",\"+\",\"?\",\"|\",\"(\",\")\",\"[\",\"]\",\"{\",\"}\",\"\\\\\",\"$\",\"^\"].join(\"|\\\\\")+\")\",\"gim\")},6030:function(e,t,n){function r(e){return r=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},r(e)}Object.defineProperty(t,\"__esModule\",{value:!0}),t.EventHandlers=void 0;var i,a=n(9845),o=(i=n(9380))&&i.__esModule?i:{default:i},s=n(7760),l=n(2839),u=n(8711),d=n(7215),c=n(4713);function f(){\n/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */f=function(){return t};var e,t={},n=Object.prototype,i=n.hasOwnProperty,a=Object.defineProperty||function(e,t,n){e[t]=n.value},o=\"function\"==typeof Symbol?Symbol:{},s=o.iterator||\"@@iterator\",l=o.asyncIterator||\"@@asyncIterator\",u=o.toStringTag||\"@@toStringTag\";function d(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{d({},\"\")}catch(e){d=function(e,t,n){return e[t]=n}}function c(e,t,n,r){var i=t&&t.prototype instanceof y?t:y,o=Object.create(i.prototype),s=new O(r||[]);return a(o,\"_invoke\",{value:D(e,n,s)}),o}function p(e,t,n){try{return{type:\"normal\",arg:e.call(t,n)}}catch(e){return{type:\"throw\",arg:e}}}t.wrap=c;var h=\"suspendedStart\",m=\"suspendedYield\",v=\"executing\",g=\"completed\",_={};function y(){}function b(){}function M(){}var w={};d(w,s,function(){return this});var k=Object.getPrototypeOf,L=k&&k(k(C([])));L&&L!==n&&i.call(L,s)&&(w=L);var x=M.prototype=y.prototype=Object.create(w);function T(e){[\"next\",\"throw\",\"return\"].forEach(function(t){d(e,t,function(e){return this._invoke(t,e)})})}function S(e,t){function n(a,o,s,l){var u=p(e[a],e,o);if(\"throw\"!==u.type){var d=u.arg,c=d.value;return c&&\"object\"==r(c)&&i.call(c,\"__await\")?t.resolve(c.__await).then(function(e){n(\"next\",e,s,l)},function(e){n(\"throw\",e,s,l)}):t.resolve(c).then(function(e){d.value=e,s(d)},function(e){return n(\"throw\",e,s,l)})}l(u.arg)}var o;a(this,\"_invoke\",{value:function(e,r){function i(){return new t(function(t,i){n(e,r,t,i)})}return o=o?o.then(i,i):i()}})}function D(t,n,r){var i=h;return function(a,o){if(i===v)throw new Error(\"Generator is already running\");if(i===g){if(\"throw\"===a)throw o;return{value:e,done:!0}}for(r.method=a,r.arg=o;;){var s=r.delegate;if(s){var l=E(s,r);if(l){if(l===_)continue;return l}}if(\"next\"===r.method)r.sent=r._sent=r.arg;else if(\"throw\"===r.method){if(i===h)throw i=g,r.arg;r.dispatchException(r.arg)}else\"return\"===r.method&&r.abrupt(\"return\",r.arg);i=v;var u=p(t,n,r);if(\"normal\"===u.type){if(i=r.done?g:m,u.arg===_)continue;return{value:u.arg,done:r.done}}\"throw\"===u.type&&(i=g,r.method=\"throw\",r.arg=u.arg)}}}function E(t,n){var r=n.method,i=t.iterator[r];if(i===e)return n.delegate=null,\"throw\"===r&&t.iterator.return&&(n.method=\"return\",n.arg=e,E(t,n),\"throw\"===n.method)||\"return\"!==r&&(n.method=\"throw\",n.arg=new TypeError(\"The iterator does not provide a '\"+r+\"' method\")),_;var a=p(i,t.iterator,n.arg);if(\"throw\"===a.type)return n.method=\"throw\",n.arg=a.arg,n.delegate=null,_;var o=a.arg;return o?o.done?(n[t.resultName]=o.value,n.next=t.nextLoc,\"return\"!==n.method&&(n.method=\"next\",n.arg=e),n.delegate=null,_):o:(n.method=\"throw\",n.arg=new TypeError(\"iterator result is not an object\"),n.delegate=null,_)}function Y(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function A(e){var t=e.completion||{};t.type=\"normal\",delete t.arg,e.completion=t}function O(e){this.tryEntries=[{tryLoc:\"root\"}],e.forEach(Y,this),this.reset(!0)}function C(t){if(t||\"\"===t){var n=t[s];if(n)return n.call(t);if(\"function\"==typeof t.next)return t;if(!isNaN(t.length)){var a=-1,o=function n(){for(;++a<t.length;)if(i.call(t,a))return n.value=t[a],n.done=!1,n;return n.value=e,n.done=!0,n};return o.next=o}}throw new TypeError(r(t)+\" is not iterable\")}return b.prototype=M,a(x,\"constructor\",{value:M,configurable:!0}),a(M,\"constructor\",{value:b,configurable:!0}),b.displayName=d(M,u,\"GeneratorFunction\"),t.isGeneratorFunction=function(e){var t=\"function\"==typeof e&&e.constructor;return!!t&&(t===b||\"GeneratorFunction\"===(t.displayName||t.name))},t.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,M):(e.__proto__=M,d(e,u,\"GeneratorFunction\")),e.prototype=Object.create(x),e},t.awrap=function(e){return{__await:e}},T(S.prototype),d(S.prototype,l,function(){return this}),t.AsyncIterator=S,t.async=function(e,n,r,i,a){void 0===a&&(a=Promise);var o=new S(c(e,n,r,i),a);return t.isGeneratorFunction(n)?o:o.next().then(function(e){return e.done?e.value:o.next()})},T(x),d(x,u,\"Generator\"),d(x,s,function(){return this}),d(x,\"toString\",function(){return\"[object Generator]\"}),t.keys=function(e){var t=Object(e),n=[];for(var r in t)n.push(r);return n.reverse(),function e(){for(;n.length;){var r=n.pop();if(r in t)return e.value=r,e.done=!1,e}return e.done=!0,e}},t.values=C,O.prototype={constructor:O,reset:function(t){if(this.prev=0,this.next=0,this.sent=this._sent=e,this.done=!1,this.delegate=null,this.method=\"next\",this.arg=e,this.tryEntries.forEach(A),!t)for(var n in this)\"t\"===n.charAt(0)&&i.call(this,n)&&!isNaN(+n.slice(1))&&(this[n]=e)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if(\"throw\"===e.type)throw e.arg;return this.rval},dispatchException:function(t){if(this.done)throw t;var n=this;function r(r,i){return s.type=\"throw\",s.arg=t,n.next=r,i&&(n.method=\"next\",n.arg=e),!!i}for(var a=this.tryEntries.length-1;a>=0;--a){var o=this.tryEntries[a],s=o.completion;if(\"root\"===o.tryLoc)return r(\"end\");if(o.tryLoc<=this.prev){var l=i.call(o,\"catchLoc\"),u=i.call(o,\"finallyLoc\");if(l&&u){if(this.prev<o.catchLoc)return r(o.catchLoc,!0);if(this.prev<o.finallyLoc)return r(o.finallyLoc)}else if(l){if(this.prev<o.catchLoc)return r(o.catchLoc,!0)}else{if(!u)throw new Error(\"try statement without catch or finally\");if(this.prev<o.finallyLoc)return r(o.finallyLoc)}}}},abrupt:function(e,t){for(var n=this.tryEntries.length-1;n>=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&i.call(r,\"finallyLoc\")&&this.prev<r.finallyLoc){var a=r;break}}a&&(\"break\"===e||\"continue\"===e)&&a.tryLoc<=t&&t<=a.finallyLoc&&(a=null);var o=a?a.completion:{};return o.type=e,o.arg=t,a?(this.method=\"next\",this.next=a.finallyLoc,_):this.complete(o)},complete:function(e,t){if(\"throw\"===e.type)throw e.arg;return\"break\"===e.type||\"continue\"===e.type?this.next=e.arg:\"return\"===e.type?(this.rval=this.arg=e.arg,this.method=\"return\",this.next=\"end\"):\"normal\"===e.type&&t&&(this.next=t),_},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),A(n),_}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if(\"throw\"===r.type){var i=r.arg;A(n)}return i}}throw new Error(\"illegal catch attempt\")},delegateYield:function(t,n,r){return this.delegate={iterator:C(t),resultName:n,nextLoc:r},\"next\"===this.method&&(this.arg=e),_}},t}function p(e,t){var n=\"undefined\"!=typeof Symbol&&e[Symbol.iterator]||e[\"@@iterator\"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(e){if(\"string\"==typeof e)return h(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return\"Object\"===n&&e.constructor&&(n=e.constructor.name),\"Map\"===n||\"Set\"===n?Array.from(e):\"Arguments\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?h(e,t):void 0}}(e))||t&&e&&\"number\"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}var a,o=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function h(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function m(e,t,n,r,i,a,o){try{var s=e[a](o),l=s.value}catch(e){return void n(e)}s.done?t(l):Promise.resolve(l).then(r,i)}var v,g,_=t.EventHandlers={keyEvent:function(e,t,n,r,i){var o=this.inputmask,f=o.opts,p=o.dependencyLib,h=o.maskset,m=this,v=p(m),g=e.key,y=u.caret.call(o,m),b=f.onKeyDown.call(this,e,u.getBuffer.call(o),y,f);if(void 0!==b)return b;if(g===l.keys.Backspace||g===l.keys.Delete||a.iphone&&g===l.keys.BACKSPACE_SAFARI||e.ctrlKey&&g===l.keys.x&&!(\"oncut\"in m))e.preventDefault(),d.handleRemove.call(o,m,g,y),(0,s.writeBuffer)(m,u.getBuffer.call(o,!0),h.p,e,m.inputmask._valueGet()!==u.getBuffer.call(o).join(\"\"));else if(g===l.keys.End||g===l.keys.PageDown){e.preventDefault();var M=u.seekNext.call(o,u.getLastValidPosition.call(o));u.caret.call(o,m,e.shiftKey?y.begin:M,M,!0)}else g===l.keys.Home&&!e.shiftKey||g===l.keys.PageUp?(e.preventDefault(),u.caret.call(o,m,0,e.shiftKey?y.begin:0,!0)):f.undoOnEscape&&g===l.keys.Escape&&!0!==e.altKey?((0,s.checkVal)(m,!0,!1,o.undoValue.split(\"\")),v.trigger(\"click\")):g!==l.keys.Insert||e.shiftKey||e.ctrlKey||void 0!==o.userOptions.insertMode?!0===f.tabThrough&&g===l.keys.Tab?!0===e.shiftKey?(y.end=u.seekPrevious.call(o,y.end,!0),!0===c.getTest.call(o,y.end-1).match.static&&y.end--,y.begin=u.seekPrevious.call(o,y.end,!0),y.begin>=0&&y.end>0&&(e.preventDefault(),u.caret.call(o,m,y.begin,y.end))):(y.begin=u.seekNext.call(o,y.begin,!0),y.end=u.seekNext.call(o,y.begin,!0),y.end<h.maskLength&&y.end--,y.begin<=h.maskLength&&(e.preventDefault(),u.caret.call(o,m,y.begin,y.end))):e.shiftKey||(f.insertModeVisual&&!1===f.insertMode?g===l.keys.ArrowRight?setTimeout(function(){var e=u.caret.call(o,m);u.caret.call(o,m,e.begin)},0):g===l.keys.ArrowLeft&&setTimeout(function(){var e=u.translatePosition.call(o,m.inputmask.caretPos.begin);u.translatePosition.call(o,m.inputmask.caretPos.end),o.isRTL?u.caret.call(o,m,e+(e===h.maskLength?0:1)):u.caret.call(o,m,e-(0===e?0:1))},0):void 0===o.keyEventHook||o.keyEventHook(e)):d.isSelection.call(o,y)?f.insertMode=!f.insertMode:(f.insertMode=!f.insertMode,u.caret.call(o,m,y.begin,y.begin));return o.isComposing=g==l.keys.Process||g==l.keys.Unidentified,o.ignorable=g.length>1&&!(\"textarea\"===m.tagName.toLowerCase()&&g==l.keys.Enter),_.keypressEvent.call(this,e,t,n,r,i)},keypressEvent:function(e,t,n,r,i){var a=this.inputmask||this,o=a.opts,c=a.dependencyLib,f=a.maskset,p=a.el,h=c(p),m=e.key;if(!0===t||e.ctrlKey&&e.altKey&&!a.ignorable||!(e.ctrlKey||e.metaKey||a.ignorable)){if(m){var v,g=t?{begin:i,end:i}:u.caret.call(a,p);t||(m=o.substitutes[m]||m),f.writeOutBuffer=!0;var _=d.isValid.call(a,g,m,r,void 0,void 0,void 0,t);if(!1!==_&&(u.resetMaskSet.call(a,!0),v=void 0!==_.caret?_.caret:u.seekNext.call(a,_.pos.begin?_.pos.begin:_.pos),f.p=v),v=o.numericInput&&void 0===_.caret?u.seekPrevious.call(a,v):v,!1!==n&&(setTimeout(function(){o.onKeyValidation.call(p,m,_)},0),f.writeOutBuffer&&!1!==_)){var y=u.getBuffer.call(a);(0,s.writeBuffer)(p,y,v,e,!0!==t)}if(e.preventDefault(),t)return!1!==_&&(_.forwardPosition=v),_}}else m===l.keys.Enter&&a.undoValue!==a._valueGet(!0)&&(a.undoValue=a._valueGet(!0),setTimeout(function(){h.trigger(\"change\")},0))},pasteEvent:(v=f().mark(function e(t){var n,r,i,a,l,d;return f().wrap(function(e){for(;;)switch(e.prev=e.next){case 0:n=function(e,n,r,i,o){var l=u.caret.call(e,n,void 0,void 0,!0),d=r.substr(0,l.begin),c=r.substr(l.end,r.length);if(d==(e.isRTL?u.getBufferTemplate.call(e).slice().reverse():u.getBufferTemplate.call(e)).slice(0,l.begin).join(\"\")&&(d=\"\"),c==(e.isRTL?u.getBufferTemplate.call(e).slice().reverse():u.getBufferTemplate.call(e)).slice(l.end).join(\"\")&&(c=\"\"),i=d+i+c,e.isRTL&&!0!==a.numericInput){i=i.split(\"\");var f,h=p(u.getBufferTemplate.call(e));try{for(h.s();!(f=h.n()).done;){var m=f.value;i[0]===m&&i.shift()}}catch(e){h.e(e)}finally{h.f()}i=i.reverse().join(\"\")}var v=i;if(\"function\"==typeof o){if(!1===(v=o.call(e,v,a)))return!1;v||(v=r)}(0,s.checkVal)(n,!0,!1,v.toString().split(\"\"),t)},r=this,i=this.inputmask,a=i.opts,l=i._valueGet(!0),i.skipInputEvent=!0,t.clipboardData&&t.clipboardData.getData?d=t.clipboardData.getData(\"text/plain\"):o.default.clipboardData&&o.default.clipboardData.getData&&(d=o.default.clipboardData.getData(\"Text\")),n(i,r,l,d,a.onBeforePaste),t.preventDefault();case 7:case\"end\":return e.stop()}},e,this)}),g=function(){var e=this,t=arguments;return new Promise(function(n,r){var i=v.apply(e,t);function a(e){m(i,n,r,a,o,\"next\",e)}function o(e){m(i,n,r,a,o,\"throw\",e)}a(void 0)})},function(e){return g.apply(this,arguments)}),inputFallBackEvent:function(e){var t,n=this.inputmask,r=n.opts,i=n.dependencyLib,o=this,d=o.inputmask._valueGet(!0),f=(n.isRTL?u.getBuffer.call(n).slice().reverse():u.getBuffer.call(n)).join(\"\"),p=u.caret.call(n,o,void 0,void 0,!0);if(f!==d){if(t=function(e,t,i){for(var a,o,s,l=e.substr(0,i.begin).split(\"\"),d=e.substr(i.begin).split(\"\"),f=t.substr(0,i.begin).split(\"\"),p=t.substr(i.begin).split(\"\"),h=l.length>=f.length?l.length:f.length,m=d.length>=p.length?d.length:p.length,v=\"\",g=[],_=\"~\";l.length<h;)l.push(_);for(;f.length<h;)f.push(_);for(;d.length<m;)d.unshift(_);for(;p.length<m;)p.unshift(_);var y=l.concat(d),b=f.concat(p);for(o=0,a=y.length;o<a;o++)switch(s=c.getPlaceholder.call(n,u.translatePosition.call(n,o)),v){case\"insertText\":b[o-1]===y[o]&&i.begin==y.length-1&&g.push(y[o]),o=a;break;case\"insertReplacementText\":case\"deleteContentBackward\":y[o]===_?i.end++:o=a;break;default:y[o]!==b[o]&&(y[o+1]!==_&&y[o+1]!==s&&void 0!==y[o+1]||(b[o]!==s||b[o+1]!==_)&&b[o]!==_?b[o+1]===_&&b[o]===y[o+1]?(v=\"insertText\",g.push(y[o]),i.begin--,i.end--):y[o]!==s&&y[o]!==_&&(y[o+1]===_||b[o]!==y[o]&&b[o+1]===y[o+1])?(v=\"insertReplacementText\",g.push(y[o]),i.begin--):y[o]===_?(v=\"deleteContentBackward\",(u.isMask.call(n,u.translatePosition.call(n,o),!0)||b[o]===r.radixPoint)&&i.end++):o=a:(v=\"insertText\",g.push(y[o]),i.begin--,i.end--))}return{action:v,data:g,caret:i}}(d,f,p),(o.inputmask.shadowRoot||o.ownerDocument).activeElement!==o&&o.focus(),(0,s.writeBuffer)(o,u.getBuffer.call(n)),u.caret.call(n,o,p.begin,p.end,!0),!a.mobile&&n.skipNextInsert&&\"insertText\"===e.inputType&&\"insertText\"===t.action&&n.isComposing)return!1;switch(\"insertCompositionText\"===e.inputType&&\"insertText\"===t.action&&n.isComposing?n.skipNextInsert=!0:n.skipNextInsert=!1,t.action){case\"insertText\":case\"insertReplacementText\":t.data.forEach(function(e,t){var r=new i.Event(\"keypress\");r.key=e,n.ignorable=!1,_.keypressEvent.call(o,r)}),setTimeout(function(){n.$el.trigger(\"keyup\")},0);break;case\"deleteContentBackward\":var h=new i.Event(\"keydown\");h.key=l.keys.Backspace,_.keyEvent.call(o,h);break;default:(0,s.applyInputValue)(o,d),u.caret.call(n,o,p.begin,p.end,!0)}e.preventDefault()}},setValueEvent:function(e){var t=this.inputmask,n=t.dependencyLib,r=this,i=e&&e.detail?e.detail[0]:arguments[1];void 0===i&&(i=r.inputmask._valueGet(!0)),(0,s.applyInputValue)(r,i,new n.Event(\"input\")),(e.detail&&void 0!==e.detail[1]||void 0!==arguments[2])&&u.caret.call(t,r,e.detail?e.detail[1]:arguments[2])},focusEvent:function(e){var t=this.inputmask,n=t.opts,r=t&&t._valueGet();n.showMaskOnFocus&&r!==u.getBuffer.call(t).join(\"\")&&(0,s.writeBuffer)(this,u.getBuffer.call(t),u.seekNext.call(t,u.getLastValidPosition.call(t))),!0!==n.positionCaretOnTab||!1!==t.mouseEnter||d.isComplete.call(t,u.getBuffer.call(t))&&-1!==u.getLastValidPosition.call(t)||_.clickEvent.apply(this,[e,!0]),t.undoValue=t&&t._valueGet(!0)},invalidEvent:function(e){this.inputmask.validationEvent=!0},mouseleaveEvent:function(){var e=this.inputmask,t=e.opts,n=this;e.mouseEnter=!1,t.clearMaskOnLostFocus&&(n.inputmask.shadowRoot||n.ownerDocument).activeElement!==n&&(0,s.HandleNativePlaceholder)(n,e.originalPlaceholder)},clickEvent:function(e,t){var n=this.inputmask;n.clicked++;var r=this;if((r.inputmask.shadowRoot||r.ownerDocument).activeElement===r){var i=u.determineNewCaretPosition.call(n,u.caret.call(n,r),t);void 0!==i&&u.caret.call(n,r,i)}},cutEvent:function(e){var t=this.inputmask,n=t.maskset,r=this,i=u.caret.call(t,r),a=t.isRTL?u.getBuffer.call(t).slice(i.end,i.begin):u.getBuffer.call(t).slice(i.begin,i.end),c=t.isRTL?a.reverse().join(\"\"):a.join(\"\");o.default.navigator&&o.default.navigator.clipboard?o.default.navigator.clipboard.writeText(c):o.default.clipboardData&&o.default.clipboardData.getData&&o.default.clipboardData.setData(\"Text\",c),d.handleRemove.call(t,r,l.keys.Delete,i),(0,s.writeBuffer)(r,u.getBuffer.call(t),n.p,e,t.undoValue!==t._valueGet(!0))},blurEvent:function(e){var t=this.inputmask,n=t.opts,r=t.dependencyLib;t.clicked=0;var i=r(this),a=this;if(a.inputmask){(0,s.HandleNativePlaceholder)(a,t.originalPlaceholder);var o=a.inputmask._valueGet(),l=u.getBuffer.call(t).slice();\"\"!==o&&(n.clearMaskOnLostFocus&&(-1===u.getLastValidPosition.call(t)&&o===u.getBufferTemplate.call(t).join(\"\")?l=[]:s.clearOptionalTail.call(t,l)),!1===d.isComplete.call(t,l)&&(setTimeout(function(){i.trigger(\"incomplete\")},0),n.clearIncomplete&&(u.resetMaskSet.call(t,!1),l=n.clearMaskOnLostFocus?[]:u.getBufferTemplate.call(t).slice())),(0,s.writeBuffer)(a,l,void 0,e)),o=t._valueGet(!0),t.undoValue!==o&&(\"\"!=o||t.undoValue!=u.getBufferTemplate.call(t).join(\"\")||t.undoValue==u.getBufferTemplate.call(t).join(\"\")&&t.maskset.validPositions.length>0)&&(t.undoValue=o,i.trigger(\"change\"))}},mouseenterEvent:function(){var e=this.inputmask,t=e.opts.showMaskOnHover,n=this;if(e.mouseEnter=!0,(n.inputmask.shadowRoot||n.ownerDocument).activeElement!==n){var r=(e.isRTL?u.getBufferTemplate.call(e).slice().reverse():u.getBufferTemplate.call(e)).join(\"\");t&&(0,s.HandleNativePlaceholder)(n,r)}},submitEvent:function(){var e=this.inputmask,t=e.opts;e.undoValue!==e._valueGet(!0)&&e.$el.trigger(\"change\"),-1===u.getLastValidPosition.call(e)&&e._valueGet&&e._valueGet()===u.getBufferTemplate.call(e).join(\"\")&&e._valueSet(\"\"),t.clearIncomplete&&!1===d.isComplete.call(e,u.getBuffer.call(e))&&e._valueSet(\"\"),t.removeMaskOnSubmit&&(e._valueSet(e.unmaskedvalue(),!0),setTimeout(function(){(0,s.writeBuffer)(e.el,u.getBuffer.call(e))},0))},resetEvent:function(){var e=this.inputmask;e.refreshValue=!0,setTimeout(function(){(0,s.applyInputValue)(e.el,e._valueGet(!0))},0)}}},9716:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.EventRuler=void 0;var r,i=n(7760),a=(r=n(2394))&&r.__esModule?r:{default:r},o=n(2839),s=n(8711);t.EventRuler={on:function(e,t,n){var r=e.inputmask.dependencyLib,l=function(t){t.originalEvent&&(t=t.originalEvent||t,arguments[0]=t);var l,u=this,d=u.inputmask,c=d?d.opts:void 0;if(void 0===d&&\"FORM\"!==this.nodeName){var f=r.data(u,\"_inputmask_opts\");r(u).off(),f&&new a.default(f).mask(u)}else{if([\"submit\",\"reset\",\"setvalue\"].includes(t.type)||\"FORM\"===this.nodeName||!(u.disabled||u.readOnly&&!(\"keydown\"===t.type&&t.ctrlKey&&t.key===o.keys.c||!1===c.tabThrough&&t.key===o.keys.Tab))){switch(t.type){case\"input\":if(!0===d.skipInputEvent)return d.skipInputEvent=!1,t.preventDefault();break;case\"click\":case\"focus\":return d.validationEvent?(d.validationEvent=!1,e.blur(),(0,i.HandleNativePlaceholder)(e,(d.isRTL?s.getBufferTemplate.call(d).slice().reverse():s.getBufferTemplate.call(d)).join(\"\")),setTimeout(function(){e.focus()},c.validationEventTimeOut),!1):(l=arguments,void setTimeout(function(){e.inputmask&&n.apply(u,l)},0))}var p=n.apply(u,arguments);return!1===p&&(t.preventDefault(),t.stopPropagation()),p}t.preventDefault()}};[\"submit\",\"reset\"].includes(t)?(l=l.bind(e),null!==e.form&&r(e.form).on(t,l)):r(e).on(t,l),e.inputmask.events[t]=e.inputmask.events[t]||[],e.inputmask.events[t].push(l)},off:function(e,t){if(e.inputmask&&e.inputmask.events){var n=e.inputmask.dependencyLib,r=e.inputmask.events;for(var i in t&&((r=[])[t]=e.inputmask.events[t]),r){for(var a=r[i];a.length>0;){var o=a.pop();[\"submit\",\"reset\"].includes(i)?null!==e.form&&n(e.form).off(i,o):n(e).off(i,o)}delete e.inputmask.events[i]}}}}},219:function(e,t,n){var r=c(n(7184)),i=c(n(2394)),a=n(2839),o=n(8711),s=n(4713);function l(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function u(e){return u=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},u(e)}function d(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\"value\"in r&&(r.writable=!0),Object.defineProperty(e,(void 0,i=function(e){if(\"object\"!==u(e)||null===e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var n=t.call(e,\"string\");if(\"object\"!==u(n))return n;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return String(e)}(r.key),\"symbol\"===u(i)?i:String(i)),r)}var i}function c(e){return e&&e.__esModule?e:{default:e}}n(1313);var f=i.default.dependencyLib,p=function(){function e(t,n,r,i){!function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}(this,e),this.mask=t,this.format=n,this.opts=r,this.inputmask=i,this._date=new Date(1,0,1),this.initDateObject(t,this.opts,this.inputmask)}var t,n;return t=e,(n=[{key:\"date\",get:function(){return void 0===this._date&&(this._date=new Date(1,0,1),this.initDateObject(void 0,this.opts,this.inputmask)),this._date}},{key:\"initDateObject\",value:function(e,t,n){var r;for(w(t).lastIndex=0;r=w(t).exec(this.format);){var i=/\\d+$/.exec(r[0]),a=i?r[0][0]+\"x\":r[0],o=void 0;if(void 0!==e){if(i){var l=w(t).lastIndex,u=D.call(n,r.index,t,n&&n.maskset);w(t).lastIndex=l,o=e.slice(0,e.indexOf(u.nextMatch[0]))}else{for(var d=r[0][0],c=r.index;n&&(t.placeholder[s.getTest.call(n,c).match.placeholder]||s.getTest.call(n,c).match.placeholder)===d;)c++;var f=c-r.index;o=e.slice(0,f||g[a]&&g[a][4]||a.length)}e=e.slice(o.length)}Object.prototype.hasOwnProperty.call(g,a)&&this.setValue(this,o,a,g[a][2],g[a][1])}}},{key:\"setValue\",value:function(e,t,n,r,i){if(void 0!==t)switch(r){case\"ampm\":e[r]=t,e[\"raw\"+r]=t.replace(/\\s/g,\"_\");break;case\"month\":if(\"mmm\"===n||\"mmmm\"===n){e[r]=x(\"mmm\"===n?m.monthNames.slice(0,12).findIndex(function(e){return t.toLowerCase()===e.toLowerCase()})+1:m.monthNames.slice(12,24).findIndex(function(e){return t.toLowerCase()===e.toLowerCase()})+1,2),e[r]=\"00\"===e[r]?\"\":e[r].toString(),e[\"raw\"+r]=e[r];break}default:e[r]=t.replace(/[^0-9]/g,\"0\"),e[\"raw\"+r]=t.replace(/\\s/g,\"_\")}if(void 0!==i){var a=e[r];(\"day\"===r&&29===parseInt(a)||\"month\"===r&&2===parseInt(a))&&(29!==parseInt(e.day)||2!==parseInt(e.month)||\"\"!==e.year&&void 0!==e.year||e._date.setFullYear(2012,1,29)),\"day\"===r&&(v=!0,0===parseInt(a)&&(a=1)),\"month\"===r&&(v=!0),\"year\"===r&&(v=!0,a.length<g[n][4]&&(a=x(a,g[n][4],!0))),(\"\"!==a&&!isNaN(a)||\"ampm\"===r)&&i.call(e._date,a)}}},{key:\"reset\",value:function(){this._date=new Date(1,0,1)}},{key:\"reInit\",value:function(){this._date=void 0,this.date}}])&&d(t.prototype,n),Object.defineProperty(t,\"prototype\",{writable:!1}),e}(),h=(new Date).getFullYear(),m=i.default.prototype.i18n,v=!1,g={d:[\"[1-9]|[12][0-9]|3[01]\",Date.prototype.setDate,\"day\",Date.prototype.getDate],dd:[\"0[1-9]|[12][0-9]|3[01]\",Date.prototype.setDate,\"day\",function(){return x(Date.prototype.getDate.call(this),2)}],ddd:[\"\"],dddd:[\"\"],m:[\"[1-9]|1[012]\",function(e){var t=e?parseInt(e):0;return t>0&&t--,Date.prototype.setMonth.call(this,t)},\"month\",function(){return Date.prototype.getMonth.call(this)+1}],mm:[\"0[1-9]|1[012]\",function(e){var t=e?parseInt(e):0;return t>0&&t--,Date.prototype.setMonth.call(this,t)},\"month\",function(){return x(Date.prototype.getMonth.call(this)+1,2)}],mmm:[m.monthNames.slice(0,12).join(\"|\"),function(e){var t=m.monthNames.slice(0,12).findIndex(function(t){return e.toLowerCase()===t.toLowerCase()});return-1!==t&&Date.prototype.setMonth.call(this,t)},\"month\",function(){return m.monthNames.slice(0,12)[Date.prototype.getMonth.call(this)]}],mmmm:[m.monthNames.slice(12,24).join(\"|\"),function(e){var t=m.monthNames.slice(12,24).findIndex(function(t){return e.toLowerCase()===t.toLowerCase()});return-1!==t&&Date.prototype.setMonth.call(this,t)},\"month\",function(){return m.monthNames.slice(12,24)[Date.prototype.getMonth.call(this)]}],yy:[\"[0-9]{2}\",function(e){var t=(new Date).getFullYear().toString().slice(0,2);Date.prototype.setFullYear.call(this,\"\".concat(t).concat(e))},\"year\",function(){return x(Date.prototype.getFullYear.call(this),2)},2],yyyy:[\"[0-9]{4}\",Date.prototype.setFullYear,\"year\",function(){return x(Date.prototype.getFullYear.call(this),4)},4],h:[\"[1-9]|1[0-2]\",Date.prototype.setHours,\"hours\",Date.prototype.getHours],hh:[\"0[1-9]|1[0-2]\",Date.prototype.setHours,\"hours\",function(){return x(Date.prototype.getHours.call(this),2)}],hx:[function(e){return\"[0-9]{\".concat(e,\"}\")},Date.prototype.setHours,\"hours\",function(e){return Date.prototype.getHours}],H:[\"1?[0-9]|2[0-3]\",Date.prototype.setHours,\"hours\",Date.prototype.getHours],HH:[\"0[0-9]|1[0-9]|2[0-3]\",Date.prototype.setHours,\"hours\",function(){return x(Date.prototype.getHours.call(this),2)}],Hx:[function(e){return\"[0-9]{\".concat(e,\"}\")},Date.prototype.setHours,\"hours\",function(e){return function(){return x(Date.prototype.getHours.call(this),e)}}],M:[\"[1-5]?[0-9]\",Date.prototype.setMinutes,\"minutes\",Date.prototype.getMinutes],MM:[\"0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]\",Date.prototype.setMinutes,\"minutes\",function(){return x(Date.prototype.getMinutes.call(this),2)}],s:[\"[1-5]?[0-9]\",Date.prototype.setSeconds,\"seconds\",Date.prototype.getSeconds],ss:[\"0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]\",Date.prototype.setSeconds,\"seconds\",function(){return x(Date.prototype.getSeconds.call(this),2)}],l:[\"[0-9]{3}\",Date.prototype.setMilliseconds,\"milliseconds\",function(){return x(Date.prototype.getMilliseconds.call(this),3)},3],L:[\"[0-9]{2}\",Date.prototype.setMilliseconds,\"milliseconds\",function(){return x(Date.prototype.getMilliseconds.call(this),2)},2],t:[\"[ap]\",y,\"ampm\",b,1],tt:[\"[ap]m\",y,\"ampm\",b,2],T:[\"[AP]\",y,\"ampm\",b,1],TT:[\"[AP]M\",y,\"ampm\",b,2],Z:[\".*\",void 0,\"Z\",function(){var e=this.toString().match(/\\((.+)\\)/)[1];return e.includes(\" \")&&(e=(e=e.replace(\"-\",\" \").toUpperCase()).split(\" \").map(function(e){return function(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:\"undefined\"!=typeof Symbol&&e[Symbol.iterator]||e[\"@@iterator\"];if(null!=n){var r,i,a,o,s=[],l=!0,u=!1;try{if(a=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=a.call(n)).done)&&(s.push(r.value),s.length!==t);l=!0);}catch(e){u=!0,i=e}finally{try{if(!l&&null!=n.return&&(o=n.return(),Object(o)!==o))return}finally{if(u)throw i}}return s}}(e,t)||function(e,t){if(e){if(\"string\"==typeof e)return l(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return\"Object\"===n&&e.constructor&&(n=e.constructor.name),\"Map\"===n||\"Set\"===n?Array.from(e):\"Arguments\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(e,t):void 0}}(e,t)||function(){throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}(e,1)[0]}).join(\"\")),e}],o:[\"\"],S:[\"\"]},_={isoDate:\"yyyy-mm-dd\",isoTime:\"HH:MM:ss\",isoDateTime:\"yyyy-mm-dd'T'HH:MM:ss\",isoUtcDateTime:\"UTC:yyyy-mm-dd'T'HH:MM:ss'Z'\"};function y(e){var t=this.getHours();e.toLowerCase().includes(\"p\")?this.setHours(t+12):e.toLowerCase().includes(\"a\")&&t>=12&&this.setHours(t-12)}function b(){var e=this.getHours();return(e=e||12)>=12?\"PM\":\"AM\"}function M(e){var t=/\\d+$/.exec(e[0]);if(t&&void 0!==t[0]){var n=g[e[0][0]+\"x\"].slice(\"\");return n[0]=n[0](t[0]),n[3]=n[3](t[0]),n}if(g[e[0]])return g[e[0]]}function w(e){if(!e.tokenizer){var t=[],n=[];for(var r in g)if(/\\.*x$/.test(r)){var i=r[0]+\"\\\\d+\";-1===n.indexOf(i)&&n.push(i)}else-1===t.indexOf(r[0])&&t.push(r[0]);e.tokenizer=\"(\"+(n.length>0?n.join(\"|\")+\"|\":\"\")+t.join(\"+|\")+\")+?|.\",e.tokenizer=new RegExp(e.tokenizer,\"g\")}return e.tokenizer}function k(e,t,n){if(!v)return!0;if(void 0===e.rawday||!isFinite(e.rawday)&&new Date(e.date.getFullYear(),isFinite(e.rawmonth)?e.month:e.date.getMonth()+1,0).getDate()>=e.day||\"29\"==e.day&&(!isFinite(e.rawyear)||void 0===e.rawyear||\"\"===e.rawyear)||new Date(e.date.getFullYear(),isFinite(e.rawmonth)?e.month:e.date.getMonth()+1,0).getDate()>=e.day)return t;if(\"29\"==e.day){var r=D.call(this,t.pos,n,this.maskset);if(r.targetMatch&&\"yyyy\"===r.targetMatch[0]&&t.pos-r.targetMatchIndex==2)return t.remove=t.pos+1,t}else if(2==e.date.getMonth()&&\"30\"==e.day&&void 0!==t.c)return e.day=\"03\",e.date.setDate(3),e.date.setMonth(1),t.insert=[{pos:t.pos,c:\"0\"},{pos:t.pos+1,c:t.c}],t.caret=o.seekNext.call(this,t.pos+1),t;return!1}function L(e,t,n,i){var a,o,s=\"\",l=0,u={};for(w(n).lastIndex=0;a=w(n).exec(e);){if(void 0===t)if(o=M(a))s+=\"(\"+o[0]+\")\",n.placeholder&&\"\"!==n.placeholder?(u[l]=n.placeholder[a.index%n.placeholder.length],u[n.placeholder[a.index%n.placeholder.length]]=a[0].charAt(0)):u[l]=a[0].charAt(0);else switch(a[0]){case\"[\":s+=\"(\";break;case\"]\":s+=\")?\";break;default:s+=(0,r.default)(a[0]),u[l]=a[0].charAt(0)}else(o=M(a))?!0!==i&&o[3]?s+=o[3].call(t.date):o[2]?s+=t[\"raw\"+o[2]]:s+=a[0]:s+=a[0];l++}return void 0===t&&(n.placeholder=u),s}function x(e,t,n){for(e=String(e),t=t||2;e.length<t;)e=n?e+\"0\":\"0\"+e;return e}function T(e,t,n){return\"string\"==typeof e?new p(e,t,n,this):e&&\"object\"===u(e)&&Object.prototype.hasOwnProperty.call(e,\"date\")?e:void 0}function S(e,t){return L(t.inputFormat,{date:e},t)}function D(e,t,n){var r,i,a=this,o=n&&n.tests[e]?t.placeholder[n.tests[e][0].match.placeholder]||n.tests[e][0].match.placeholder:\"\",l=0,u=0;for(w(t).lastIndex=0;i=w(t).exec(t.inputFormat);){var d=/\\d+$/.exec(i[0]);if(d)u=parseInt(d[0]);else{for(var c=i[0][0],f=l;a&&(t.placeholder[s.getTest.call(a,f).match.placeholder]||s.getTest.call(a,f).match.placeholder)===c;)f++;0===(u=f-l)&&(u=i[0].length)}if(l+=u,-1!=i[0].indexOf(o)||l>=e+1){r=i,i=w(t).exec(t.inputFormat);break}}return{targetMatchIndex:l-u,nextMatch:i,targetMatch:r}}i.default.extendAliases({datetime:{mask:function(e){return e.numericInput=!1,g.S=m.ordinalSuffix.join(\"|\"),e.inputFormat=_[e.inputFormat]||e.inputFormat,e.displayFormat=_[e.displayFormat]||e.displayFormat||e.inputFormat,e.outputFormat=_[e.outputFormat]||e.outputFormat||e.inputFormat,e.regex=L(e.inputFormat,void 0,e),e.min=T(e.min,e.inputFormat,e),e.max=T(e.max,e.inputFormat,e),null},placeholder:\"\",inputFormat:\"isoDateTime\",displayFormat:null,outputFormat:null,min:null,max:null,skipOptionalPartCharacter:\"\",preValidation:function(e,t,n,r,i,a,o,s){if(s)return!0;if(isNaN(n)&&e[t]!==n){var l=D.call(this,t,i,a);if(l.nextMatch&&l.nextMatch[0]===n&&l.targetMatch[0].length>1){var u=M(l.targetMatch)[0];if(new RegExp(u).test(\"0\"+e[t-1]))return e[t]=e[t-1],e[t-1]=\"0\",{fuzzy:!0,buffer:e,refreshFromBuffer:{start:t-1,end:t+1},pos:t+1}}}return!0},postValidation:function(e,t,n,r,i,a,o,l){var u,d,c=this;if(o)return!0;if(!1===r&&(((u=D.call(c,t+1,i,a)).targetMatch&&u.targetMatchIndex===t&&u.targetMatch[0].length>1&&void 0!==g[u.targetMatch[0]]||(u=D.call(c,t+2,i,a)).targetMatch&&u.targetMatchIndex===t+1&&u.targetMatch[0].length>1&&void 0!==g[u.targetMatch[0]])&&(d=M(u.targetMatch)[0]),void 0!==d&&(void 0!==a.validPositions[t+1]&&new RegExp(d).test(n+\"0\")?(e[t]=n,e[t+1]=\"0\",r={pos:t+2,caret:t}):new RegExp(d).test(\"0\"+n)&&(e[t]=\"0\",e[t+1]=n,r={pos:t+2})),!1===r))return r;if(r.fuzzy&&(e=r.buffer,t=r.pos),(u=D.call(c,t,i,a)).targetMatch&&u.targetMatch[0]&&void 0!==g[u.targetMatch[0]]){var f=M(u.targetMatch);d=f[0];var p=e.slice(u.targetMatchIndex,u.targetMatchIndex+u.targetMatch[0].length);if(!1===new RegExp(d).test(p.join(\"\"))&&2===u.targetMatch[0].length&&a.validPositions[u.targetMatchIndex]&&a.validPositions[u.targetMatchIndex+1]&&(a.validPositions[u.targetMatchIndex+1].input=\"0\"),\"year\"==f[2])for(var m=s.getMaskTemplate.call(c,!1,1,void 0,!0),v=t+1;v<e.length;v++)e[v]=m[v],a.validPositions.splice(t+1,1)}var _=r,y=T.call(c,e.join(\"\"),i.inputFormat,i);return _&&!isNaN(y.date.getTime())&&(i.prefillYear&&(_=function(e,t,n){if(e.year!==e.rawyear){var r=h.toString(),i=e.rawyear.replace(/[^0-9]/g,\"\"),a=r.slice(0,i.length),o=r.slice(i.length);if(2===i.length&&i===a){var s=new Date(h,e.month-1,e.day);e.day==s.getDate()&&(!n.max||n.max.date.getTime()>=s.getTime())&&(e.date.setFullYear(h),e.year=r,t.insert=[{pos:t.pos+1,c:o[0]},{pos:t.pos+2,c:o[1]}])}}return t}(y,_,i)),_=function(e,t,n,r){if(!t)return t;if(t&&n.min&&!isNaN(n.min.date.getTime())){var i;for(e.reset(),w(n).lastIndex=0;i=w(n).exec(n.inputFormat);){var a;if((a=M(i))&&a[3]){for(var o=a[1],s=e[a[2]],l=n.min[a[2]],u=n.max?n.max[a[2]]:l+1,d=[],c=!1,f=0;f<l.length;f++)void 0!==r.validPositions[f+i.index]||c?(d[f]=s[f],c=c||s[f]>l[f]):(f+i.index==0&&s[f]<l[f]?(d[f]=s[f],c=!0):d[f]=l[f],\"year\"===a[2]&&s.length-1==f&&l!=u&&(d=(parseInt(d.join(\"\"))+1).toString().split(\"\")),\"ampm\"===a[2]&&l!=u&&n.min.date.getTime()>e.date.getTime()&&(d[f]=u[f]));o.call(e._date,d.join(\"\"))}}t=n.min.date.getTime()<=e.date.getTime(),e.reInit()}return t&&n.max&&(isNaN(n.max.date.getTime())||(t=n.max.date.getTime()>=e.date.getTime())),t}(y,_=k.call(c,y,_,i),i,a)),void 0!==t&&_&&r.pos!==t?{buffer:L(i.inputFormat,y,i).split(\"\"),refreshFromBuffer:{start:t,end:r.pos},pos:r.caret||r.pos}:_},onKeyDown:function(e,t,n,r){e.ctrlKey&&e.key===a.keys.ArrowRight&&(this.inputmask._valueSet(S(new Date,r)),f(this).trigger(\"setvalue\"))},onUnMask:function(e,t,n){return t?L(n.outputFormat,T.call(this,e,n.inputFormat,n),n,!0):t},casing:function(e,t,n,r){if(0==t.nativeDef.indexOf(\"[ap]\"))return e.toLowerCase();if(0==t.nativeDef.indexOf(\"[AP]\"))return e.toUpperCase();var i=s.getTest.call(this,[n-1]);return 0==i.match.def.indexOf(\"[AP]\")||0===n||i&&i.input===String.fromCharCode(a.keyCode.Space)||i&&i.match.def===String.fromCharCode(a.keyCode.Space)?e.toUpperCase():e.toLowerCase()},onBeforeMask:function(e,t){return\"[object Date]\"===Object.prototype.toString.call(e)&&(e=S(e,t)),e},insertMode:!1,insertModeVisual:!1,shiftPositions:!1,keepStatic:!1,inputmode:\"numeric\",prefillYear:!0}})},1313:function(e,t,n){var r,i=(r=n(2394))&&r.__esModule?r:{default:r};i.default.dependencyLib.extend(!0,i.default.prototype.i18n,{dayNames:[\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\",\"Sun\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\",\"Sunday\"],monthNames:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\",\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],ordinalSuffix:[\"st\",\"nd\",\"rd\",\"th\"]})},3851:function(e,t,n){var r,i=(r=n(2394))&&r.__esModule?r:{default:r},a=n(8711),o=n(4713);function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}i.default.extendDefinitions({A:{validator:\"[A-Za-zА-яЁёÀ-ÿµ]\",casing:\"upper\"},\"&\":{validator:\"[0-9A-Za-zА-яЁёÀ-ÿµ]\",casing:\"upper\"},\"#\":{validator:\"[0-9A-Fa-f]\",casing:\"upper\"}});var l=/25[0-5]|2[0-4][0-9]|[01][0-9][0-9]/;function u(e,t,n,r,i){if(n-1>-1&&\".\"!==t.buffer[n-1]?(e=t.buffer[n-1]+e,e=n-2>-1&&\".\"!==t.buffer[n-2]?t.buffer[n-2]+e:\"0\"+e):e=\"00\"+e,i.greedy&&parseInt(e)>255&&l.test(\"00\"+e.charAt(2))){var a=[].concat(function(e){return function(e){if(Array.isArray(e))return s(e)}(e)||function(e){if(\"undefined\"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e[\"@@iterator\"])return Array.from(e)}(e)||function(e,t){if(e){if(\"string\"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return\"Object\"===n&&e.constructor&&(n=e.constructor.name),\"Map\"===n||\"Set\"===n?Array.from(e):\"Arguments\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?s(e,t):void 0}}(e)||function(){throw new TypeError(\"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}(t.buffer.slice(0,n)),[\".\",e.charAt(2)]);if(a.join(\"\").match(/\\./g).length<4)return{refreshFromBuffer:!0,buffer:a,caret:n+2}}return l.test(e)}i.default.extendAliases({cssunit:{regex:\"[+-]?[0-9]+\\\\.?([0-9]+)?(px|em|rem|ex|%|in|cm|mm|pt|pc)\"},url:{regex:\"(https?|ftp)://.*\",autoUnmask:!1,keepStatic:!1,tabThrough:!0},ip:{mask:\"i{1,3}.j{1,3}.k{1,3}.l{1,3}\",definitions:{i:{validator:u},j:{validator:u},k:{validator:u},l:{validator:u}},onUnMask:function(e,t,n){return e},inputmode:\"decimal\",substitutes:{\",\":\".\"}},email:{mask:function(e){var t=e.separator,n=e.quantifier,r=\"*{1,64}[.*{1,64}][.*{1,64}][.*{1,63}]@-{1,63}.-{1,63}[.-{1,63}][.-{1,63}]\",i=r;if(t)for(var a=0;a<n;a++)i+=\"[\".concat(t).concat(r,\"]\");return i},greedy:!1,casing:\"lower\",separator:null,quantifier:5,skipOptionalPartCharacter:\"\",onBeforePaste:function(e,t){return(e=e.toLowerCase()).replace(\"mailto:\",\"\")},definitions:{\"*\":{validator:\"[0-9１-９A-Za-zА-яЁёÀ-ÿµ!#$%&'*+/=?^_`{|}~-]\"},\"-\":{validator:\"[0-9A-Za-z-]\"}},onUnMask:function(e,t,n){return e},inputmode:\"email\"},mac:{mask:\"##:##:##:##:##:##\"},vin:{mask:\"V{13}9{4}\",definitions:{V:{validator:\"[A-HJ-NPR-Za-hj-npr-z\\\\d]\",casing:\"upper\"}},clearIncomplete:!0,autoUnmask:!0},ssn:{mask:\"999-99-9999\",postValidation:function(e,t,n,r,i,s,l){var u=o.getMaskTemplate.call(this,!0,a.getLastValidPosition.call(this),!0,!0);return/^(?!219-09-9999|078-05-1120)(?!666|000|9.{2}).{3}-(?!00).{2}-(?!0{4}).{4}$/.test(u.join(\"\"))}}})},207:function(e,t,n){var r=s(n(7184)),i=s(n(2394)),a=n(2839),o=n(8711);function s(e){return e&&e.__esModule?e:{default:e}}var l=i.default.dependencyLib;function u(e,t){for(var n=\"\",r=0;r<e.length;r++)i.default.prototype.definitions[e.charAt(r)]||t.definitions[e.charAt(r)]||t.optionalmarker[0]===e.charAt(r)||t.optionalmarker[1]===e.charAt(r)||t.quantifiermarker[0]===e.charAt(r)||t.quantifiermarker[1]===e.charAt(r)||t.groupmarker[0]===e.charAt(r)||t.groupmarker[1]===e.charAt(r)||t.alternatormarker===e.charAt(r)?n+=\"\\\\\"+e.charAt(r):n+=e.charAt(r);return n}function d(e,t,n,r){if(e.length>0&&t>0&&(!n.digitsOptional||r)){var i=e.indexOf(n.radixPoint),a=!1;n.negationSymbol.back===e[e.length-1]&&(a=!0,e.length--),-1===i&&(e.push(n.radixPoint),i=e.length-1);for(var o=1;o<=t;o++)isFinite(e[i+o])||(e[i+o]=\"0\")}return a&&e.push(n.negationSymbol.back),e}function c(e,t){var n=0;for(var r in\"+\"===e&&(n=o.seekNext.call(this,t.validPositions.length-1)),t.tests)if((r=parseInt(r))>=n)for(var i=0,a=t.tests[r].length;i<a;i++)if((void 0===t.validPositions[r]||\"-\"===e)&&t.tests[r][i].match.def===e)return r+(void 0!==t.validPositions[r]&&\"-\"!==e?1:0);return n}function f(e,t){for(var n=-1,r=0,i=t.validPositions.length;r<i;r++){var a=t.validPositions[r];if(a&&a.match.def===e){n=r;break}}return n}function p(e,t,n,r,i){var a=t.buffer?t.buffer.indexOf(i.radixPoint):-1,o=(-1!==a||r&&i.jitMasking)&&new RegExp(i.definitions[9].validator).test(e);return!r&&i._radixDance&&-1!==a&&o&&null==t.validPositions[a]?{insert:{pos:a===n?a+1:a,c:i.radixPoint},pos:n}:o}i.default.extendAliases({numeric:{mask:function(e){e.repeat=0,e.groupSeparator===e.radixPoint&&e.digits&&\"0\"!==e.digits&&(\".\"===e.radixPoint?e.groupSeparator=\",\":\",\"===e.radixPoint?e.groupSeparator=\".\":e.groupSeparator=\"\"),\" \"===e.groupSeparator&&(e.skipOptionalPartCharacter=void 0),e.placeholder.length>1&&(e.placeholder=e.placeholder.charAt(0)),\"radixFocus\"===e.positionCaretOnClick&&\"\"===e.placeholder&&(e.positionCaretOnClick=\"lvp\");var t=\"0\",n=e.radixPoint;!0===e.numericInput&&void 0===e.__financeInput?(t=\"1\",e.positionCaretOnClick=\"radixFocus\"===e.positionCaretOnClick?\"lvp\":e.positionCaretOnClick,e.digitsOptional=!1,isNaN(e.digits)&&(e.digits=2),e._radixDance=!1,n=\",\"===e.radixPoint?\"?\":\"!\",\"\"!==e.radixPoint&&void 0===e.definitions[n]&&(e.definitions[n]={},e.definitions[n].validator=\"[\"+e.radixPoint+\"]\",e.definitions[n].placeholder=e.radixPoint,e.definitions[n].static=!0,e.definitions[n].generated=!0)):(e.__financeInput=!1,e.numericInput=!0);var i,a=\"[+]\";if(a+=u(e.prefix,e),\"\"!==e.groupSeparator?(void 0===e.definitions[e.groupSeparator]&&(e.definitions[e.groupSeparator]={},e.definitions[e.groupSeparator].validator=\"[\"+e.groupSeparator+\"]\",e.definitions[e.groupSeparator].placeholder=e.groupSeparator,e.definitions[e.groupSeparator].static=!0,e.definitions[e.groupSeparator].generated=!0),a+=e._mask(e)):a+=\"9{+}\",void 0!==e.digits&&0!==e.digits){var o=e.digits.toString().split(\",\");isFinite(o[0])&&o[1]&&isFinite(o[1])?a+=n+t+\"{\"+e.digits+\"}\":(isNaN(e.digits)||parseInt(e.digits)>0)&&(e.digitsOptional||e.jitMasking?(i=a+n+t+\"{0,\"+e.digits+\"}\",e.keepStatic=!0):a+=n+t+\"{\"+e.digits+\"}\")}else e.inputmode=\"numeric\";return a+=u(e.suffix,e),a+=\"[-]\",i&&(a=[i+u(e.suffix,e)+\"[-]\",a]),e.greedy=!1,function(e){void 0===e.parseMinMaxOptions&&(null!==e.min&&(e.min=e.min.toString().replace(new RegExp((0,r.default)(e.groupSeparator),\"g\"),\"\"),\",\"===e.radixPoint&&(e.min=e.min.replace(e.radixPoint,\".\")),e.min=isFinite(e.min)?parseFloat(e.min):NaN,isNaN(e.min)&&(e.min=Number.MIN_VALUE)),null!==e.max&&(e.max=e.max.toString().replace(new RegExp((0,r.default)(e.groupSeparator),\"g\"),\"\"),\",\"===e.radixPoint&&(e.max=e.max.replace(e.radixPoint,\".\")),e.max=isFinite(e.max)?parseFloat(e.max):NaN,isNaN(e.max)&&(e.max=Number.MAX_VALUE)),e.parseMinMaxOptions=\"done\")}(e),\"\"!==e.radixPoint&&e.substituteRadixPoint&&(e.substitutes[\".\"==e.radixPoint?\",\":\".\"]=e.radixPoint),a},_mask:function(e){return\"(\"+e.groupSeparator+\"999){+|1}\"},digits:\"*\",digitsOptional:!0,enforceDigitsOnBlur:!1,radixPoint:\".\",positionCaretOnClick:\"radixFocus\",_radixDance:!0,groupSeparator:\"\",allowMinus:!0,negationSymbol:{front:\"-\",back:\"\"},prefix:\"\",suffix:\"\",min:null,max:null,SetMaxOnOverflow:!1,step:1,inputType:\"text\",unmaskAsNumber:!1,roundingFN:Math.round,inputmode:\"decimal\",shortcuts:{k:\"1000\",m:\"1000000\"},placeholder:\"0\",greedy:!1,rightAlign:!0,insertMode:!0,autoUnmask:!1,skipOptionalPartCharacter:\"\",usePrototypeDefinitions:!1,stripLeadingZeroes:!0,substituteRadixPoint:!0,definitions:{0:{validator:p},1:{validator:p,definitionSymbol:\"9\"},9:{validator:\"[0-9０-９٠-٩۰-۹]\",definitionSymbol:\"*\"},\"+\":{validator:function(e,t,n,r,i){return i.allowMinus&&(\"-\"===e||e===i.negationSymbol.front)}},\"-\":{validator:function(e,t,n,r,i){return i.allowMinus&&e===i.negationSymbol.back}}},preValidation:function(e,t,n,r,i,a,o,s){var l=this;if(!1!==i.__financeInput&&n===i.radixPoint)return!1;var u=e.indexOf(i.radixPoint),d=t;if(t=function(e,t,n,r,i){return i._radixDance&&i.numericInput&&t!==i.negationSymbol.back&&e<=n&&(n>0||t==i.radixPoint)&&(void 0===r.validPositions[e-1]||r.validPositions[e-1].input!==i.negationSymbol.back)&&(e-=1),e}(t,n,u,a,i),\"-\"===n||n===i.negationSymbol.front){if(!0!==i.allowMinus)return!1;var p=!1,h=f(\"+\",a),m=f(\"-\",a);return-1!==h&&(p=[h],-1!==m&&p.push(m)),!1!==p?{remove:p,caret:d-i.negationSymbol.back.length}:{insert:[{pos:c.call(l,\"+\",a),c:i.negationSymbol.front,fromIsValid:!0},{pos:c.call(l,\"-\",a),c:i.negationSymbol.back,fromIsValid:void 0}],caret:d+i.negationSymbol.back.length}}if(n===i.groupSeparator)return{caret:d};if(s)return!0;if(-1!==u&&!0===i._radixDance&&!1===r&&n===i.radixPoint&&void 0!==i.digits&&(isNaN(i.digits)||parseInt(i.digits)>0)&&u!==t){var v=c.call(l,i.radixPoint,a);return a.validPositions[v]&&(a.validPositions[v].generatedInput=a.validPositions[v].generated||!1),{caret:i._radixDance&&t===u-1?u+1:u}}if(!1===i.__financeInput)if(r){if(i.digitsOptional)return{rewritePosition:o.end};if(!i.digitsOptional){if(o.begin>u&&o.end<=u)return n===i.radixPoint?{insert:{pos:u+1,c:\"0\",fromIsValid:!0},rewritePosition:u}:{rewritePosition:u+1};if(o.begin<u)return{rewritePosition:o.begin-1}}}else if(!i.showMaskOnHover&&!i.showMaskOnFocus&&!i.digitsOptional&&i.digits>0&&\"\"===this.__valueGet.call(this.el))return{rewritePosition:u};return{rewritePosition:t}},postValidation:function(e,t,n,r,i,a,o){if(!1===r)return r;if(o)return!0;if(null!==i.min||null!==i.max){var s=i.onUnMask(e.slice().reverse().join(\"\"),void 0,l.extend({},i,{unmaskAsNumber:!0}));if(null!==i.min&&s<i.min&&(s.toString().length>i.min.toString().length||s<0))return!1;if(null!==i.max&&s>i.max)return!!i.SetMaxOnOverflow&&{refreshFromBuffer:!0,buffer:d(i.max.toString().replace(\".\",i.radixPoint).split(\"\"),i.digits,i).reverse()}}return r},onUnMask:function(e,t,n){if(\"\"===t&&!0===n.nullable)return t;var i=e.replace(n.prefix,\"\");return i=(i=i.replace(n.suffix,\"\")).replace(new RegExp((0,r.default)(n.groupSeparator),\"g\"),\"\"),\"\"!==n.placeholder.charAt(0)&&(i=i.replace(new RegExp(n.placeholder.charAt(0),\"g\"),\"0\")),n.unmaskAsNumber?(\"\"!==n.radixPoint&&-1!==i.indexOf(n.radixPoint)&&(i=i.replace(r.default.call(this,n.radixPoint),\".\")),i=(i=i.replace(new RegExp(\"^\"+(0,r.default)(n.negationSymbol.front)),\"-\")).replace(new RegExp((0,r.default)(n.negationSymbol.back)+\"$\"),\"\"),Number(i)):i},isComplete:function(e,t){var n=(t.numericInput?e.slice().reverse():e).join(\"\");return n=(n=(n=(n=(n=n.replace(new RegExp(\"^\"+(0,r.default)(t.negationSymbol.front)),\"-\")).replace(new RegExp((0,r.default)(t.negationSymbol.back)+\"$\"),\"\")).replace(t.prefix,\"\")).replace(t.suffix,\"\")).replace(new RegExp((0,r.default)(t.groupSeparator)+\"([0-9]{3})\",\"g\"),\"$1\"),\",\"===t.radixPoint&&(n=n.replace((0,r.default)(t.radixPoint),\".\")),isFinite(n)},onBeforeMask:function(e,t){var n;e=null!==(n=e)&&void 0!==n?n:\"\";var i=t.radixPoint||\",\";isFinite(t.digits)&&(t.digits=parseInt(t.digits)),\"number\"!=typeof e&&\"number\"!==t.inputType||\"\"===i||(e=e.toString().replace(\".\",i));var a=\"-\"===e.charAt(0)||e.charAt(0)===t.negationSymbol.front,o=e.split(i),s=o[0].replace(/[^\\-0-9]/g,\"\"),l=o.length>1?o[1].replace(/[^0-9]/g,\"\"):\"\",u=o.length>1;e=s+(\"\"!==l?i+l:l);var c=0;if(\"\"!==i&&(c=t.digitsOptional?t.digits<l.length?t.digits:l.length:t.digits,\"\"!==l||!t.digitsOptional)){var f=Math.pow(10,c||1);e=e.replace((0,r.default)(i),\".\"),isNaN(parseFloat(e))||(e=(t.roundingFN(parseFloat(e)*f)/f).toFixed(c)),e=e.toString().replace(\".\",i)}if(0===t.digits&&-1!==e.indexOf(i)&&(e=e.substring(0,e.indexOf(i))),null!==t.min||null!==t.max){var p=e.toString().replace(i,\".\");null!==t.min&&p<t.min?e=t.min.toString().replace(\".\",i):null!==t.max&&p>t.max&&(e=t.max.toString().replace(\".\",i))}return a&&\"-\"!==e.charAt(0)&&(e=\"-\"+e),d(e.toString().split(\"\"),c,t,u).join(\"\")},onBeforeWrite:function(e,t,n,i){function a(e,t){if(!1!==i.__financeInput||t){var n=e.indexOf(i.radixPoint);-1!==n&&e.splice(n,1)}if(\"\"!==i.groupSeparator)for(;-1!==(n=e.indexOf(i.groupSeparator));)e.splice(n,1);return e}var o,s;if(i.stripLeadingZeroes&&(s=function(e,t){var n=new RegExp(\"(^\"+(\"\"!==t.negationSymbol.front?(0,r.default)(t.negationSymbol.front)+\"?\":\"\")+(0,r.default)(t.prefix)+\")(.*)(\"+(0,r.default)(t.suffix)+(\"\"!=t.negationSymbol.back?(0,r.default)(t.negationSymbol.back)+\"?\":\"\")+\"$)\").exec(e.slice().reverse().join(\"\")),i=n?n[2]:\"\",a=!1;return i&&(i=i.split(t.radixPoint.charAt(0))[0],a=new RegExp(\"^[0\"+t.groupSeparator+\"]*\").exec(i)),!(!a||!(a[0].length>1||a[0].length>0&&a[0].length<i.length))&&a}(t,i)))for(var u=t.join(\"\").lastIndexOf(s[0].split(\"\").reverse().join(\"\"))-(s[0]==s.input?0:1),c=s[0]==s.input?1:0,f=s[0].length-c;f>0;f--)this.maskset.validPositions.splice(u+f,1),delete t[u+f];if(e)switch(e.type){case\"blur\":case\"checkval\":if(null!==i.min){var p=i.onUnMask(t.slice().reverse().join(\"\"),void 0,l.extend({},i,{unmaskAsNumber:!0}));if(null!==i.min&&p<i.min)return{refreshFromBuffer:!0,buffer:d(i.min.toString().replace(\".\",i.radixPoint).split(\"\"),i.digits,i).reverse()}}if(t[t.length-1]===i.negationSymbol.front){var h=new RegExp(\"(^\"+(\"\"!=i.negationSymbol.front?(0,r.default)(i.negationSymbol.front)+\"?\":\"\")+(0,r.default)(i.prefix)+\")(.*)(\"+(0,r.default)(i.suffix)+(\"\"!=i.negationSymbol.back?(0,r.default)(i.negationSymbol.back)+\"?\":\"\")+\"$)\").exec(a(t.slice(),!0).reverse().join(\"\"));0==(h?h[2]:\"\")&&(o={refreshFromBuffer:!0,buffer:[0]})}else\"\"!==i.radixPoint&&t.indexOf(i.radixPoint)===i.suffix.length&&(o&&o.buffer?o.buffer.splice(0,1+i.suffix.length):(t.splice(0,1+i.suffix.length),o={refreshFromBuffer:!0,buffer:a(t)}));if(i.enforceDigitsOnBlur){var m=(o=o||{})&&o.buffer||t.slice().reverse();o.refreshFromBuffer=!0,o.buffer=d(m,i.digits,i,!0).reverse()}}return o},onKeyDown:function(e,t,n,r){var i,o=l(this);if(3!=e.location){var s,u=e.key;if((s=r.shortcuts&&r.shortcuts[u])&&s.length>1)return this.inputmask.__valueSet.call(this,parseFloat(this.inputmask.unmaskedvalue())*parseInt(s)),o.trigger(\"setvalue\"),!1}if(e.ctrlKey)switch(e.key){case a.keys.ArrowUp:return this.inputmask.__valueSet.call(this,parseFloat(this.inputmask.unmaskedvalue())+parseInt(r.step)),o.trigger(\"setvalue\"),!1;case a.keys.ArrowDown:return this.inputmask.__valueSet.call(this,parseFloat(this.inputmask.unmaskedvalue())-parseInt(r.step)),o.trigger(\"setvalue\"),!1}if(!e.shiftKey&&(e.key===a.keys.Delete||e.key===a.keys.Backspace||e.key===a.keys.BACKSPACE_SAFARI)&&n.begin!==t.length){if(t[e.key===a.keys.Delete?n.begin-1:n.end]===r.negationSymbol.front)return i=t.slice().reverse(),\"\"!==r.negationSymbol.front&&i.shift(),\"\"!==r.negationSymbol.back&&i.pop(),o.trigger(\"setvalue\",[i.join(\"\"),n.begin]),!1;if(!0===r._radixDance){var c,f=t.indexOf(r.radixPoint);if(r.digitsOptional){if(0===f)return(i=t.slice().reverse()).pop(),o.trigger(\"setvalue\",[i.join(\"\"),n.begin>=i.length?i.length:n.begin]),!1}else if(-1!==f&&(n.begin<f||n.end<f||e.key===a.keys.Delete&&(n.begin===f||n.begin-1===f)))return n.begin===n.end&&(e.key===a.keys.Backspace||e.key===a.keys.BACKSPACE_SAFARI?n.begin++:e.key===a.keys.Delete&&n.begin-1===f&&(c=l.extend({},n),n.begin--,n.end--)),(i=t.slice().reverse()).splice(i.length-n.begin,n.begin-n.end+1),i=d(i,r.digits,r).join(\"\"),c&&(n=c),o.trigger(\"setvalue\",[i,n.begin>=i.length?f+1:n.begin]),!1}}}},currency:{prefix:\"\",groupSeparator:\",\",alias:\"numeric\",digits:2,digitsOptional:!1},decimal:{alias:\"numeric\"},integer:{alias:\"numeric\",inputmode:\"numeric\",digits:0},percentage:{alias:\"numeric\",min:0,max:100,suffix:\" %\",digits:0,allowMinus:!1},indianns:{alias:\"numeric\",_mask:function(e){return\"(\"+e.groupSeparator+\"99){*|1}(\"+e.groupSeparator+\"999){1|1}\"},groupSeparator:\",\",radixPoint:\".\",placeholder:\"0\",digits:2,digitsOptional:!1}})},9380:function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=void 0;var n=!(\"undefined\"==typeof window||!window.document||!window.document.createElement);t.default=n?window:{}},7760:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.HandleNativePlaceholder=function(e,t){var n=e?e.inputmask:this;if(r.ie){if(e.inputmask._valueGet()!==t&&(e.placeholder!==t||\"\"===e.placeholder)){var i=o.getBuffer.call(n).slice(),a=e.inputmask._valueGet();if(a!==t){var s=o.getLastValidPosition.call(n);-1===s&&a===o.getBufferTemplate.call(n).join(\"\")?i=[]:-1!==s&&d.call(n,i),f(e,i)}}}else e.placeholder!==t&&(e.placeholder=t,\"\"===e.placeholder&&e.removeAttribute(\"placeholder\"))},t.applyInputValue=u,t.checkVal=c,t.clearOptionalTail=d,t.unmaskedvalue=function(e){var t=e?e.inputmask:this,n=t.opts,r=t.maskset;if(e){if(void 0===e.inputmask)return e.value;e.inputmask&&e.inputmask.refreshValue&&u(e,e.inputmask._valueGet(!0))}for(var i=[],a=r.validPositions,s=0,l=a.length;s<l;s++)a[s]&&a[s].match&&(1!=a[s].match.static||Array.isArray(r.metadata)&&!0!==a[s].generatedInput)&&i.push(a[s].input);var d=0===i.length?\"\":(t.isRTL?i.reverse():i).join(\"\");if(\"function\"==typeof n.onUnMask){var c=(t.isRTL?o.getBuffer.call(t).slice().reverse():o.getBuffer.call(t)).join(\"\");d=n.onUnMask.call(t,c,d,n)}return d},t.writeBuffer=f;var r=n(9845),i=n(6030),a=n(2839),o=n(8711),s=n(7215),l=n(4713);function u(e,t,n){var r=e?e.inputmask:this,i=r.opts;e.inputmask.refreshValue=!1,\"function\"==typeof i.onBeforeMask&&(t=i.onBeforeMask.call(r,t,i)||t),c(e,!0,!1,t=(t||\"\").toString().split(\"\"),n),r.undoValue=r._valueGet(!0),(i.clearMaskOnLostFocus||i.clearIncomplete)&&e.inputmask._valueGet()===o.getBufferTemplate.call(r).join(\"\")&&-1===o.getLastValidPosition.call(r)&&e.inputmask._valueSet(\"\")}function d(e){e.length=0;for(var t,n=l.getMaskTemplate.call(this,!0,0,!0,void 0,!0);void 0!==(t=n.shift());)e.push(t);return e}function c(e,t,n,r,a){var u,d=e?e.inputmask:this,c=d.maskset,p=d.opts,h=d.dependencyLib,m=r.slice(),v=\"\",g=-1,_=p.skipOptionalPartCharacter;p.skipOptionalPartCharacter=\"\",o.resetMaskSet.call(d,!1),d.clicked=0,g=p.radixPoint?o.determineNewCaretPosition.call(d,{begin:0,end:0},!1,!1===p.__financeInput?\"radixFocus\":void 0).begin:0,c.p=g,d.caretPos={begin:g};var y=[],b=d.caretPos;if(m.forEach(function(e,t){if(void 0!==e){var r=new h.Event(\"_checkval\");r.key=e,v+=e;var a=o.getLastValidPosition.call(d,void 0,!0);!function(e,t){for(var n=l.getMaskTemplate.call(d,!0,0).slice(e,o.seekNext.call(d,e,!1,!1)).join(\"\").replace(/'/g,\"\"),r=n.indexOf(t);r>0&&\" \"===n[r-1];)r--;var i=0===r&&!o.isMask.call(d,e)&&(l.getTest.call(d,e).match.nativeDef===t.charAt(0)||!0===l.getTest.call(d,e).match.static&&l.getTest.call(d,e).match.nativeDef===\"'\"+t.charAt(0)||\" \"===l.getTest.call(d,e).match.nativeDef&&(l.getTest.call(d,e+1).match.nativeDef===t.charAt(0)||!0===l.getTest.call(d,e+1).match.static&&l.getTest.call(d,e+1).match.nativeDef===\"'\"+t.charAt(0)));if(!i&&r>0&&!o.isMask.call(d,e,!1,!0)){var a=o.seekNext.call(d,e);d.caretPos.begin<a&&(d.caretPos={begin:a})}return i}(g,v)?(u=i.EventHandlers.keypressEvent.call(d,r,!0,!1,n,d.caretPos.begin))&&(g=d.caretPos.begin+1,v=\"\"):u=i.EventHandlers.keypressEvent.call(d,r,!0,!1,n,a+1),u?(void 0!==u.pos&&c.validPositions[u.pos]&&!0===c.validPositions[u.pos].match.static&&void 0===c.validPositions[u.pos].alternation&&(y.push(u.pos),d.isRTL||(u.forwardPosition=u.pos+1)),f.call(d,void 0,o.getBuffer.call(d),u.forwardPosition,r,!1),d.caretPos={begin:u.forwardPosition,end:u.forwardPosition},b=d.caretPos):void 0===c.validPositions[t]&&m[t]===l.getPlaceholder.call(d,t)&&o.isMask.call(d,t,!0)?d.caretPos.begin++:d.caretPos=b}}),y.length>0){var M,w,k=o.seekNext.call(d,-1,void 0,!1);if(!s.isComplete.call(d,o.getBuffer.call(d))&&y.length<=k||s.isComplete.call(d,o.getBuffer.call(d))&&y.length>0&&y.length!==k&&0===y[0])for(var L=k;void 0!==(M=y.shift());)if(M<L){var x=new h.Event(\"_checkval\");if((w=c.validPositions[M]).generatedInput=!0,x.key=w.input,(u=i.EventHandlers.keypressEvent.call(d,x,!0,!1,n,L))&&void 0!==u.pos&&u.pos!==M&&c.validPositions[u.pos]&&!0===c.validPositions[u.pos].match.static)y.push(u.pos);else if(!u)break;L++}}t&&f.call(d,e,o.getBuffer.call(d),u?u.forwardPosition:d.caretPos.begin,a||new h.Event(\"checkval\"),a&&(\"input\"===a.type&&d.undoValue!==o.getBuffer.call(d).join(\"\")||\"paste\"===a.type)),p.skipOptionalPartCharacter=_}function f(e,t,n,r,i){var l=e?e.inputmask:this,u=l.opts,d=l.dependencyLib;if(r&&\"function\"==typeof u.onBeforeWrite){var c=u.onBeforeWrite.call(l,r,t,n,u);if(c){if(c.refreshFromBuffer){var f=c.refreshFromBuffer;s.refreshFromBuffer.call(l,!0===f?f:f.start,f.end,c.buffer||t),t=o.getBuffer.call(l,!0)}void 0!==n&&(n=void 0!==c.caret?c.caret:n)}}if(void 0!==e&&(e.inputmask._valueSet(t.join(\"\")),void 0===n||void 0!==r&&\"blur\"===r.type||o.caret.call(l,e,n,void 0,void 0,void 0!==r&&\"keydown\"===r.type&&(r.key===a.keys.Delete||r.key===a.keys.Backspace)),void 0===e.inputmask.writeBufferHook||e.inputmask.writeBufferHook(n),!0===i)){var p=d(e),h=e.inputmask._valueGet();e.inputmask.skipInputEvent=!0,p.trigger(\"input\"),setTimeout(function(){h===o.getBufferTemplate.call(l).join(\"\")?p.trigger(\"cleared\"):!0===s.isComplete.call(l,t)&&p.trigger(\"complete\")},0)}}},2394:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=void 0;var r=m(n(3976)),i=m(n(7392)),a=m(n(4963)),o=n(9716),s=m(n(9380)),l=n(7760),u=n(157),d=n(2391),c=n(8711),f=n(7215),p=n(4713);function h(e){return h=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},h(e)}function m(e){return e&&e.__esModule?e:{default:e}}var v=s.default.document,g=\"_inputmask_opts\";function _(e,t,n){if(!(this instanceof _))return new _(e,t,n);this.dependencyLib=a.default,this.el=void 0,this.events={},this.maskset=void 0,!0!==n&&(\"[object Object]\"===Object.prototype.toString.call(e)?t=e:(t=t||{},e&&(t.alias=e)),this.opts=a.default.extend(!0,{},this.defaults,t),this.noMasksCache=t&&void 0!==t.definitions,this.userOptions=t||{},y(this.opts.alias,t,this.opts)),this.refreshValue=!1,this.undoValue=void 0,this.$el=void 0,this.skipInputEvent=!1,this.validationEvent=!1,this.ignorable=!1,this.maxLength,this.mouseEnter=!1,this.clicked=0,this.originalPlaceholder=void 0,this.isComposing=!1,this.hasAlternator=!1}function y(e,t,n){var r=_.prototype.aliases[e];return r?(r.alias&&y(r.alias,void 0,n),a.default.extend(!0,n,r),a.default.extend(!0,n,t),!0):(null===n.mask&&(n.mask=e),!1)}_.prototype={dataAttribute:\"data-inputmask\",defaults:r.default,definitions:i.default,aliases:{},masksCache:{},i18n:{},get isRTL(){return this.opts.isRTL||this.opts.numericInput},mask:function(e){var t=this;return\"string\"==typeof e&&(e=v.getElementById(e)||v.querySelectorAll(e)),(e=e.nodeName?[e]:Array.isArray(e)?e:[].slice.call(e)).forEach(function(e,n){var r=a.default.extend(!0,{},t.opts);if(function(e,t,n,r){function i(t,i){var a=\"\"===r?t:r+\"-\"+t;null!==(i=void 0!==i?i:e.getAttribute(a))&&(\"string\"==typeof i&&(0===t.indexOf(\"on\")?i=s.default[i]:\"false\"===i?i=!1:\"true\"===i&&(i=!0)),n[t]=i)}if(!0===t.importDataAttributes){var o,l,u,d,c=e.getAttribute(r);if(c&&\"\"!==c&&(c=c.replace(/'/g,'\"'),l=JSON.parse(\"{\"+c+\"}\")),l)for(d in u=void 0,l)if(\"alias\"===d.toLowerCase()){u=l[d];break}for(o in i(\"alias\",u),n.alias&&y(n.alias,n,t),t){if(l)for(d in u=void 0,l)if(d.toLowerCase()===o.toLowerCase()){u=l[d];break}i(o,u)}}return a.default.extend(!0,t,n),(\"rtl\"===e.dir||t.rightAlign)&&(e.style.textAlign=\"right\"),(\"rtl\"===e.dir||t.numericInput)&&(e.dir=\"ltr\",e.removeAttribute(\"dir\"),t.isRTL=!0),Object.keys(n).length}(e,r,a.default.extend(!0,{},t.userOptions),t.dataAttribute)){var i=(0,d.generateMaskSet)(r,t.noMasksCache);void 0!==i&&(void 0!==e.inputmask&&(e.inputmask.opts.autoUnmask=!0,e.inputmask.remove()),e.inputmask=new _(void 0,void 0,!0),e.inputmask.opts=r,e.inputmask.noMasksCache=t.noMasksCache,e.inputmask.userOptions=a.default.extend(!0,{},t.userOptions),e.inputmask.el=e,e.inputmask.$el=(0,a.default)(e),e.inputmask.maskset=i,a.default.data(e,g,t.userOptions),u.mask.call(e.inputmask))}}),e&&e[0]&&e[0].inputmask||this},option:function(e,t){return\"string\"==typeof e?this.opts[e]:\"object\"===h(e)?(a.default.extend(this.userOptions,e),this.el&&!0!==t&&this.mask(this.el),this):void 0},unmaskedvalue:function(e){if(this.maskset=this.maskset||(0,d.generateMaskSet)(this.opts,this.noMasksCache),void 0===this.el||void 0!==e){var t=(\"function\"==typeof this.opts.onBeforeMask&&this.opts.onBeforeMask.call(this,e,this.opts)||e).split(\"\");l.checkVal.call(this,void 0,!1,!1,t),\"function\"==typeof this.opts.onBeforeWrite&&this.opts.onBeforeWrite.call(this,void 0,c.getBuffer.call(this),0,this.opts)}return l.unmaskedvalue.call(this,this.el)},remove:function(){if(this.el){a.default.data(this.el,g,null);var e=this.opts.autoUnmask?(0,l.unmaskedvalue)(this.el):this._valueGet(this.opts.autoUnmask);e!==c.getBufferTemplate.call(this).join(\"\")?this._valueSet(e,this.opts.autoUnmask):this._valueSet(\"\"),o.EventRuler.off(this.el),Object.getOwnPropertyDescriptor&&Object.getPrototypeOf?Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this.el),\"value\")&&this.__valueGet&&Object.defineProperty(this.el,\"value\",{get:this.__valueGet,set:this.__valueSet,configurable:!0}):v.__lookupGetter__&&this.el.__lookupGetter__(\"value\")&&this.__valueGet&&(this.el.__defineGetter__(\"value\",this.__valueGet),this.el.__defineSetter__(\"value\",this.__valueSet)),this.el.inputmask=void 0}return this.el},getemptymask:function(){return this.maskset=this.maskset||(0,d.generateMaskSet)(this.opts,this.noMasksCache),(this.isRTL?c.getBufferTemplate.call(this).reverse():c.getBufferTemplate.call(this)).join(\"\")},hasMaskedValue:function(){return!this.opts.autoUnmask},isComplete:function(){return this.maskset=this.maskset||(0,d.generateMaskSet)(this.opts,this.noMasksCache),f.isComplete.call(this,c.getBuffer.call(this))},getmetadata:function(){if(this.maskset=this.maskset||(0,d.generateMaskSet)(this.opts,this.noMasksCache),Array.isArray(this.maskset.metadata)){var e=p.getMaskTemplate.call(this,!0,0,!1).join(\"\");return this.maskset.metadata.forEach(function(t){return t.mask!==e||(e=t,!1)}),e}return this.maskset.metadata},isValid:function(e){if(this.maskset=this.maskset||(0,d.generateMaskSet)(this.opts,this.noMasksCache),e){var t=(\"function\"==typeof this.opts.onBeforeMask&&this.opts.onBeforeMask.call(this,e,this.opts)||e).split(\"\");l.checkVal.call(this,void 0,!0,!1,t)}else e=this.isRTL?c.getBuffer.call(this).slice().reverse().join(\"\"):c.getBuffer.call(this).join(\"\");for(var n=c.getBuffer.call(this),r=c.determineLastRequiredPosition.call(this),i=n.length-1;i>r&&!c.isMask.call(this,i);i--);return n.splice(r,i+1-r),f.isComplete.call(this,n)&&e===(this.isRTL?c.getBuffer.call(this).slice().reverse().join(\"\"):c.getBuffer.call(this).join(\"\"))},format:function(e,t){this.maskset=this.maskset||(0,d.generateMaskSet)(this.opts,this.noMasksCache);var n=(\"function\"==typeof this.opts.onBeforeMask&&this.opts.onBeforeMask.call(this,e,this.opts)||e).split(\"\");l.checkVal.call(this,void 0,!0,!1,n);var r=this.isRTL?c.getBuffer.call(this).slice().reverse().join(\"\"):c.getBuffer.call(this).join(\"\");return t?{value:r,metadata:this.getmetadata()}:r},setValue:function(e){this.el&&(0,a.default)(this.el).trigger(\"setvalue\",[e])},analyseMask:d.analyseMask},_.extendDefaults=function(e){a.default.extend(!0,_.prototype.defaults,e)},_.extendDefinitions=function(e){a.default.extend(!0,_.prototype.definitions,e)},_.extendAliases=function(e){a.default.extend(!0,_.prototype.aliases,e)},_.format=function(e,t,n){return _(t).format(e,n)},_.unmask=function(e,t){return _(t).unmaskedvalue(e)},_.isValid=function(e,t){return _(t).isValid(e)},_.remove=function(e){\"string\"==typeof e&&(e=v.getElementById(e)||v.querySelectorAll(e)),(e=e.nodeName?[e]:e).forEach(function(e){e.inputmask&&e.inputmask.remove()})},_.setValue=function(e,t){\"string\"==typeof e&&(e=v.getElementById(e)||v.querySelectorAll(e)),(e=e.nodeName?[e]:e).forEach(function(e){e.inputmask?e.inputmask.setValue(t):(0,a.default)(e).trigger(\"setvalue\",[t])})},_.dependencyLib=a.default,s.default.Inputmask=_,t.default=_},5296:function(e,t,n){function r(e){return r=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},r(e)}var i=p(n(9380)),a=p(n(2394));function o(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,\"value\"in i&&(i.writable=!0),Object.defineProperty(e,(void 0,a=function(e){if(\"object\"!==r(e)||null===e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var n=t.call(e,\"string\");if(\"object\"!==r(n))return n;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return String(e)}(i.key),\"symbol\"===r(a)?a:String(a)),i)}var a}function s(e){var t=d();return function(){var n,i=f(e);if(t){var a=f(this).constructor;n=Reflect.construct(i,arguments,a)}else n=i.apply(this,arguments);return function(e,t){if(t&&(\"object\"===r(t)||\"function\"==typeof t))return t;if(void 0!==t)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(e){if(void 0===e)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return e}(e)}(this,n)}}function l(e){var t=\"function\"==typeof Map?new Map:void 0;return l=function(e){if(null===e||!function(e){try{return-1!==Function.toString.call(e).indexOf(\"[native code]\")}catch(t){return\"function\"==typeof e}}(e))return e;if(\"function\"!=typeof e)throw new TypeError(\"Super expression must either be null or a function\");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return u(e,arguments,f(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),c(n,e)},l(e)}function u(e,t,n){return u=d()?Reflect.construct.bind():function(e,t,n){var r=[null];r.push.apply(r,t);var i=new(Function.bind.apply(e,r));return n&&c(i,n.prototype),i},u.apply(null,arguments)}function d(){if(\"undefined\"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if(\"function\"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch(e){return!1}}function c(e,t){return c=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},c(e,t)}function f(e){return f=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},f(e)}function p(e){return e&&e.__esModule?e:{default:e}}var h=i.default.document;if(h&&h.head&&h.head.attachShadow&&i.default.customElements&&void 0===i.default.customElements.get(\"input-mask\")){var m=function(e){!function(e,t){if(\"function\"!=typeof t&&null!==t)throw new TypeError(\"Super expression must either be null or a function\");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,\"prototype\",{writable:!1}),t&&c(e,t)}(i,e);var t,n,r=s(i);function i(){var e;!function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}(this,i);var t=(e=r.call(this)).getAttributeNames(),n=e.attachShadow({mode:\"closed\"});for(var o in e.input=h.createElement(\"input\"),e.input.type=\"text\",n.appendChild(e.input),t)Object.prototype.hasOwnProperty.call(t,o)&&e.input.setAttribute(t[o],e.getAttribute(t[o]));var s=new a.default;return s.dataAttribute=\"\",s.mask(e.input),e.input.inputmask.shadowRoot=n,e}return t=i,(n=[{key:\"attributeChangedCallback\",value:function(e,t,n){this.input.setAttribute(e,n)}},{key:\"value\",get:function(){return this.input.value},set:function(e){this.input.value=e}}])&&o(t.prototype,n),Object.defineProperty(t,\"prototype\",{writable:!1}),i}(l(HTMLElement));i.default.customElements.define(\"input-mask\",m)}},2839:function(e,t){function n(e){return n=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},n(e)}function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:\"undefined\"!=typeof Symbol&&e[Symbol.iterator]||e[\"@@iterator\"];if(null!=n){var r,i,a,o,s=[],l=!0,u=!1;try{if(a=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=a.call(n)).done)&&(s.push(r.value),s.length!==t);l=!0);}catch(e){u=!0,i=e}finally{try{if(!l&&null!=n.return&&(o=n.return(),Object(o)!==o))return}finally{if(u)throw i}}return s}}(e,t)||function(e,t){if(e){if(\"string\"==typeof e)return i(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return\"Object\"===n&&e.constructor&&(n=e.constructor.name),\"Map\"===n||\"Set\"===n?Array.from(e):\"Arguments\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?i(e,t):void 0}}(e,t)||function(){throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function a(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function o(e,t,r){return(t=function(e){var t=function(e){if(\"object\"!==n(e)||null===e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var r=t.call(e,\"string\");if(\"object\"!==n(r))return r;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return String(e)}(e);return\"symbol\"===n(t)?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}Object.defineProperty(t,\"__esModule\",{value:!0}),t.keys=t.keyCode=void 0,t.toKey=function(e,t){return l[e]||(t?String.fromCharCode(e):String.fromCharCode(e).toLowerCase())},t.toKeyCode=function(e){return s[e]};var s=t.keyCode=function(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?a(Object(n),!0).forEach(function(t){o(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):a(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}({c:67,x:88,z:90,BACKSPACE_SAFARI:127,Enter:13,Meta_LEFT:91,Meta_RIGHT:92,Space:32},{Alt:18,AltGraph:18,ArrowDown:40,ArrowLeft:37,ArrowRight:39,ArrowUp:38,Backspace:8,CapsLock:20,Control:17,ContextMenu:93,Dead:221,Delete:46,End:35,Escape:27,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,Home:36,Insert:45,NumLock:144,PageDown:34,PageUp:33,Pause:19,PrintScreen:44,Process:229,Shift:16,ScrollLock:145,Tab:9,Unidentified:229}),l=Object.entries(s).reduce(function(e,t){var n=r(t,2),i=n[0],a=n[1];return e[a]=void 0===e[a]?i:e[a],e},{});t.keys=Object.entries(s).reduce(function(e,t){var n=r(t,2),i=n[0];return n[1],e[i]=\"Space\"===i?\" \":i,e},{})},2391:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.analyseMask=function(e,t,n){var r,i,l,u,d,c,f=/(?:[?*+]|\\{[0-9+*]+(?:,[0-9+*]*)?(?:\\|[0-9+*]*)?\\})|[^.?*+^${[]()|\\\\]+|./g,p=/\\[\\^?]?(?:[^\\\\\\]]+|\\\\[\\S\\s]?)*]?|\\\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\\S\\s]?)|\\((?:\\?[:=!]?)?|(?:[?*+]|\\{[0-9]+(?:,[0-9]*)?\\})\\??|[^.?*+^${[()|\\\\]+|./g,h=!1,m=new o.default,v=[],g=[],_=!1;function y(e,r,i){i=void 0!==i?i:e.matches.length;var o=e.matches[i-1];if(t){if(0===r.indexOf(\"[\")||h&&/\\\\d|\\\\s|\\\\w|\\\\p/i.test(r)||\".\"===r){var l=n.casing?\"i\":\"\";/\\\\p\\{.*}/i.test(r)&&(l+=\"u\"),e.matches.splice(i++,0,{fn:new RegExp(r,l),static:!1,optionality:!1,newBlockMarker:void 0===o?\"master\":o.def!==r,casing:null,def:r,placeholder:\"object\"===s(n.placeholder)?n.placeholder[m.matches.length]:void 0,nativeDef:r})}else h&&(r=r[r.length-1]),r.split(\"\").forEach(function(t,r){o=e.matches[i-1],e.matches.splice(i++,0,{fn:/[a-z]/i.test(n.staticDefinitionSymbol||t)?new RegExp(\"[\"+(n.staticDefinitionSymbol||t)+\"]\",n.casing?\"i\":\"\"):null,static:!0,optionality:!1,newBlockMarker:void 0===o?\"master\":o.def!==t&&!0!==o.static,casing:null,def:n.staticDefinitionSymbol||t,placeholder:void 0!==n.staticDefinitionSymbol?t:\"object\"===s(n.placeholder)?n.placeholder[m.matches.length]:void 0,nativeDef:(h?\"'\":\"\")+t})});h=!1}else{var u=n.definitions&&n.definitions[r]||n.usePrototypeDefinitions&&a.default.prototype.definitions[r];u&&!h?e.matches.splice(i++,0,{fn:u.validator?\"string\"==typeof u.validator?new RegExp(u.validator,n.casing?\"i\":\"\"):new function(){this.test=u.validator}:/./,static:u.static||!1,optionality:u.optional||!1,defOptionality:u.optional||!1,newBlockMarker:void 0===o||u.optional?\"master\":o.def!==(u.definitionSymbol||r),casing:u.casing,def:u.definitionSymbol||r,placeholder:u.placeholder,nativeDef:r,generated:u.generated}):(e.matches.splice(i++,0,{fn:/[a-z]/i.test(n.staticDefinitionSymbol||r)?new RegExp(\"[\"+(n.staticDefinitionSymbol||r)+\"]\",n.casing?\"i\":\"\"):null,static:!0,optionality:!1,newBlockMarker:void 0===o?\"master\":o.def!==r&&!0!==o.static,casing:null,def:n.staticDefinitionSymbol||r,placeholder:void 0!==n.staticDefinitionSymbol?r:void 0,nativeDef:(h?\"'\":\"\")+r}),h=!1)}}function b(){if(v.length>0){if(y(u=v[v.length-1],i),u.isAlternator){d=v.pop();for(var e=0;e<d.matches.length;e++)d.matches[e].isGroup&&(d.matches[e].isGroup=!1);v.length>0?(u=v[v.length-1]).matches.push(d):m.matches.push(d)}}else y(m,i)}function M(e){var t=new o.default(!0);return t.openGroup=!1,t.matches=e,t}function w(){if((l=v.pop()).openGroup=!1,void 0!==l)if(v.length>0){if((u=v[v.length-1]).matches.push(l),u.isAlternator){d=v.pop();for(var e=0;e<d.matches.length;e++)d.matches[e].isGroup=!1,d.matches[e].alternatorGroup=!1;v.length>0?(u=v[v.length-1]).matches.push(d):m.matches.push(d)}}else m.matches.push(l);else b()}function k(e){var t=e.pop();return t.isQuantifier&&(t=M([e.pop(),t])),t}for(t&&(n.optionalmarker[0]=void 0,n.optionalmarker[1]=void 0);r=t?p.exec(e):f.exec(e);){if(i=r[0],t){switch(i.charAt(0)){case\"?\":i=\"{0,1}\";break;case\"+\":case\"*\":i=\"{\"+i+\"}\";break;case\"|\":if(0===v.length){var L=M(m.matches);L.openGroup=!0,v.push(L),m.matches=[],_=!0}}switch(i){case\"\\\\d\":i=\"[0-9]\";break;case\"\\\\p\":i+=p.exec(e)[0],i+=p.exec(e)[0]}}if(h)b();else switch(i.charAt(0)){case\"$\":case\"^\":t||b();break;case n.escapeChar:h=!0,t&&b();break;case n.optionalmarker[1]:case n.groupmarker[1]:w();break;case n.optionalmarker[0]:v.push(new o.default(!1,!0));break;case n.groupmarker[0]:v.push(new o.default(!0));break;case n.quantifiermarker[0]:var x=new o.default(!1,!1,!0),T=(i=i.replace(/[{}?]/g,\"\")).split(\"|\"),S=T[0].split(\",\"),D=isNaN(S[0])?S[0]:parseInt(S[0]),E=1===S.length?D:isNaN(S[1])?S[1]:parseInt(S[1]),Y=isNaN(T[1])?T[1]:parseInt(T[1]);\"*\"!==D&&\"+\"!==D||(D=\"*\"===E?0:1),x.quantifier={min:D,max:E,jit:Y};var A=v.length>0?v[v.length-1].matches:m.matches;(r=A.pop()).isGroup||(r=M([r])),A.push(r),A.push(x);break;case n.alternatormarker:if(v.length>0){var O=(u=v[v.length-1]).matches[u.matches.length-1];c=u.openGroup&&(void 0===O.matches||!1===O.isGroup&&!1===O.isAlternator)?v.pop():k(u.matches)}else c=k(m.matches);if(c.isAlternator)v.push(c);else if(c.alternatorGroup?(d=v.pop(),c.alternatorGroup=!1):d=new o.default(!1,!1,!1,!0),d.matches.push(c),v.push(d),c.openGroup){c.openGroup=!1;var C=new o.default(!0);C.alternatorGroup=!0,v.push(C)}break;default:b()}}for(_&&w();v.length>0;)l=v.pop(),m.matches.push(l);return m.matches.length>0&&(function e(r){r&&r.matches&&r.matches.forEach(function(i,a){var o=r.matches[a+1];(void 0===o||void 0===o.matches||!1===o.isQuantifier)&&i&&i.isGroup&&(i.isGroup=!1,t||(y(i,n.groupmarker[0],0),!0!==i.openGroup&&y(i,n.groupmarker[1]))),e(i)})}(m),g.push(m)),(n.numericInput||n.isRTL)&&function e(t){for(var r in t.matches=t.matches.reverse(),t.matches)if(Object.prototype.hasOwnProperty.call(t.matches,r)){var i=parseInt(r);if(t.matches[r].isQuantifier&&t.matches[i+1]&&t.matches[i+1].isGroup){var a=t.matches[r];t.matches.splice(r,1),t.matches.splice(i+1,0,a)}void 0!==t.matches[r].matches?t.matches[r]=e(t.matches[r]):t.matches[r]=((o=t.matches[r])===n.optionalmarker[0]?o=n.optionalmarker[1]:o===n.optionalmarker[1]?o=n.optionalmarker[0]:o===n.groupmarker[0]?o=n.groupmarker[1]:o===n.groupmarker[1]&&(o=n.groupmarker[0]),o)}var o;return t}(g[0]),g},t.generateMaskSet=function(e,t){var n;function o(e,t){var n=t.repeat,r=t.groupmarker,a=t.quantifiermarker,o=t.keepStatic;if(n>0||\"*\"===n||\"+\"===n){var s=\"*\"===n?0:\"+\"===n?1:n;if(s!=n)e=r[0]+e+r[1]+a[0]+s+\",\"+n+a[1];else for(var u=e,d=1;d<s;d++)e+=u}if(!0===o){var c=e.match(new RegExp(\"(.)\\\\[([^\\\\]]*)\\\\]\",\"g\"));c&&c.forEach(function(t,n){var r=function(e){return function(e){if(Array.isArray(e))return e}(e)||function(e){var t=null==e?null:\"undefined\"!=typeof Symbol&&e[Symbol.iterator]||e[\"@@iterator\"];if(null!=t){var n,r,i,a,o=[],s=!0,l=!1;try{for(i=(t=t.call(e)).next,!2;!(s=(n=i.call(t)).done)&&(o.push(n.value),2!==o.length);s=!0);}catch(e){l=!0,r=e}finally{try{if(!s&&null!=t.return&&(a=t.return(),Object(a)!==a))return}finally{if(l)throw r}}return o}}(e)||function(e){if(e){if(\"string\"==typeof e)return l(e,2);var t=Object.prototype.toString.call(e).slice(8,-1);return\"Object\"===t&&e.constructor&&(t=e.constructor.name),\"Map\"===t||\"Set\"===t?Array.from(e):\"Arguments\"===t||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)?l(e,2):void 0}}(e)||function(){throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}(t.split(\"[\")),a=r[0],o=r[1];o=o.replace(\"]\",\"\"),e=e.replace(new RegExp(\"\".concat((0,i.default)(a),\"\\\\[\").concat((0,i.default)(o),\"\\\\]\")),a.charAt(0)===o.charAt(0)?\"(\".concat(a,\"|\").concat(a).concat(o,\")\"):\"\".concat(a,\"[\").concat(o,\"]\"))})}return e}function u(e,n,i){var l,u,d=!1;return null!==e&&\"\"!==e||((d=null!==i.regex)?e=(e=i.regex).replace(/^(\\^)(.*)(\\$)$/,\"$2\"):(d=!0,e=\".*\")),1===e.length&&!1===i.greedy&&0!==i.repeat&&(i.placeholder=\"\"),e=o(e,i),u=d?\"regex_\"+i.regex:i.numericInput?e.split(\"\").reverse().join(\"\"):e,null!==i.keepStatic&&(u=\"ks_\"+i.keepStatic+u),\"object\"===s(i.placeholder)&&(u=\"ph_\"+JSON.stringify(i.placeholder)+u),void 0===a.default.prototype.masksCache[u]||!0===t?(l={mask:e,maskToken:a.default.prototype.analyseMask(e,d,i),validPositions:[],_buffer:void 0,buffer:void 0,tests:{},excludes:{},metadata:n,maskLength:void 0,jitOffset:{}},!0!==t&&(a.default.prototype.masksCache[u]=l,l=r.default.extend(!0,{},a.default.prototype.masksCache[u]))):l=r.default.extend(!0,{},a.default.prototype.masksCache[u]),l}if(\"function\"==typeof e.mask&&(e.mask=e.mask(e)),Array.isArray(e.mask)){if(e.mask.length>1){null===e.keepStatic&&(e.keepStatic=!0);var d=e.groupmarker[0];return(e.isRTL?e.mask.reverse():e.mask).forEach(function(t){d.length>1&&(d+=e.alternatormarker),void 0!==t.mask&&\"function\"!=typeof t.mask?d+=t.mask:d+=t}),u(d+=e.groupmarker[1],e.mask,e)}e.mask=e.mask.pop()}return n=e.mask&&void 0!==e.mask.mask&&\"function\"!=typeof e.mask.mask?u(e.mask.mask,e.mask,e):u(e.mask,e.mask,e),null===e.keepStatic&&(e.keepStatic=!1),n};var r=u(n(4963)),i=u(n(7184)),a=u(n(2394)),o=u(n(9695));function s(e){return s=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},s(e)}function l(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function u(e){return e&&e.__esModule?e:{default:e}}},157:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.mask=function(){var e=this,t=this.opts,n=this.el,u=this.dependencyLib;a.EventRuler.off(n);var d=function(t,n){var r=t.getAttribute(\"type\"),i=\"input\"===t.tagName.toLowerCase()&&n.supportsInputType.includes(r)||t.isContentEditable||\"textarea\"===t.tagName.toLowerCase();if(!i)if(\"input\"===t.tagName.toLowerCase()){var l=document.createElement(\"input\");l.setAttribute(\"type\",r),i=\"text\"===l.type,l=null}else i=\"partial\";return!1!==i?function(t){var r,i;function l(){return this.inputmask?this.inputmask.opts.autoUnmask?this.inputmask.unmaskedvalue():-1!==s.getLastValidPosition.call(e)||!0!==n.nullable?(this.inputmask.shadowRoot||this.ownerDocument).activeElement===this&&n.clearMaskOnLostFocus?(e.isRTL?o.clearOptionalTail.call(e,s.getBuffer.call(e).slice()).reverse():o.clearOptionalTail.call(e,s.getBuffer.call(e).slice())).join(\"\"):r.call(this):\"\":r.call(this)}function d(e){i.call(this,e),this.inputmask&&(0,o.applyInputValue)(this,e)}if(!t.inputmask.__valueGet){if(!0!==n.noValuePatching){if(Object.getOwnPropertyDescriptor){var c=Object.getPrototypeOf?Object.getOwnPropertyDescriptor(Object.getPrototypeOf(t),\"value\"):void 0;c&&c.get&&c.set?(r=c.get,i=c.set,Object.defineProperty(t,\"value\",{get:l,set:d,configurable:!0})):\"input\"!==t.tagName.toLowerCase()&&(r=function(){return this.textContent},i=function(e){this.textContent=e},Object.defineProperty(t,\"value\",{get:l,set:d,configurable:!0}))}else document.__lookupGetter__&&t.__lookupGetter__(\"value\")&&(r=t.__lookupGetter__(\"value\"),i=t.__lookupSetter__(\"value\"),t.__defineGetter__(\"value\",l),t.__defineSetter__(\"value\",d));t.inputmask.__valueGet=r,t.inputmask.__valueSet=i}t.inputmask._valueGet=function(t){return e.isRTL&&!0!==t?r.call(this.el).split(\"\").reverse().join(\"\"):r.call(this.el)},t.inputmask._valueSet=function(t,n){i.call(this.el,null==t?\"\":!0!==n&&e.isRTL?t.split(\"\").reverse().join(\"\"):t)},void 0===r&&(r=function(){return this.value},i=function(e){this.value=e},function(t){if(u.valHooks&&(void 0===u.valHooks[t]||!0!==u.valHooks[t].inputmaskpatch)){var r=u.valHooks[t]&&u.valHooks[t].get?u.valHooks[t].get:function(e){return e.value},i=u.valHooks[t]&&u.valHooks[t].set?u.valHooks[t].set:function(e,t){return e.value=t,e};u.valHooks[t]={get:function(t){if(t.inputmask){if(t.inputmask.opts.autoUnmask)return t.inputmask.unmaskedvalue();var i=r(t);return-1!==s.getLastValidPosition.call(e,void 0,void 0,t.inputmask.maskset.validPositions)||!0!==n.nullable?i:\"\"}return r(t)},set:function(e,t){var n=i(e,t);return e.inputmask&&(0,o.applyInputValue)(e,t),n},inputmaskpatch:!0}}}(t.type),function(e){a.EventRuler.on(e,\"mouseenter\",function(){var e=this,t=e.inputmask._valueGet(!0);t!=(e.inputmask.isRTL?s.getBuffer.call(e.inputmask).slice().reverse():s.getBuffer.call(e.inputmask)).join(\"\")&&(0,o.applyInputValue)(e,t)})}(t))}}(t):t.inputmask=void 0,i}(n,t);if(!1!==d){e.originalPlaceholder=n.placeholder,e.maxLength=void 0!==n?n.maxLength:void 0,-1===e.maxLength&&(e.maxLength=void 0),\"inputMode\"in n&&null===n.getAttribute(\"inputmode\")&&(n.inputMode=t.inputmode,n.setAttribute(\"inputmode\",t.inputmode)),!0===d&&(t.showMaskOnFocus=t.showMaskOnFocus&&-1===[\"cc-number\",\"cc-exp\"].indexOf(n.autocomplete),r.iphone&&(t.insertModeVisual=!1,n.setAttribute(\"autocorrect\",\"off\")),a.EventRuler.on(n,\"submit\",i.EventHandlers.submitEvent),a.EventRuler.on(n,\"reset\",i.EventHandlers.resetEvent),a.EventRuler.on(n,\"blur\",i.EventHandlers.blurEvent),a.EventRuler.on(n,\"focus\",i.EventHandlers.focusEvent),a.EventRuler.on(n,\"invalid\",i.EventHandlers.invalidEvent),a.EventRuler.on(n,\"click\",i.EventHandlers.clickEvent),a.EventRuler.on(n,\"mouseleave\",i.EventHandlers.mouseleaveEvent),a.EventRuler.on(n,\"mouseenter\",i.EventHandlers.mouseenterEvent),a.EventRuler.on(n,\"paste\",i.EventHandlers.pasteEvent),a.EventRuler.on(n,\"cut\",i.EventHandlers.cutEvent),a.EventRuler.on(n,\"complete\",t.oncomplete),a.EventRuler.on(n,\"incomplete\",t.onincomplete),a.EventRuler.on(n,\"cleared\",t.oncleared),!0!==t.inputEventOnly&&a.EventRuler.on(n,\"keydown\",i.EventHandlers.keyEvent),(r.mobile||t.inputEventOnly)&&n.removeAttribute(\"maxLength\"),a.EventRuler.on(n,\"input\",i.EventHandlers.inputFallBackEvent)),a.EventRuler.on(n,\"setvalue\",i.EventHandlers.setValueEvent),void 0===e.applyMaskHook||e.applyMaskHook(),s.getBufferTemplate.call(e).join(\"\"),e.undoValue=e._valueGet(!0);var c=(n.inputmask.shadowRoot||n.ownerDocument).activeElement;if(\"\"!==n.inputmask._valueGet(!0)||!1===t.clearMaskOnLostFocus||c===n){(0,o.applyInputValue)(n,n.inputmask._valueGet(!0),t);var f=s.getBuffer.call(e).slice();!1===l.isComplete.call(e,f)&&t.clearIncomplete&&s.resetMaskSet.call(e,!1),t.clearMaskOnLostFocus&&c!==n&&(-1===s.getLastValidPosition.call(e)?f=[]:o.clearOptionalTail.call(e,f)),(!1===t.clearMaskOnLostFocus||t.showMaskOnFocus&&c===n||\"\"!==n.inputmask._valueGet(!0))&&(0,o.writeBuffer)(n,f),c===n&&s.caret.call(e,n,s.seekNext.call(e,s.getLastValidPosition.call(e)))}}};var r=n(9845),i=n(6030),a=n(9716),o=n(7760),s=n(8711),l=n(7215)},9695:function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function(e,t,n,r){this.matches=[],this.openGroup=e||!1,this.alternatorGroup=!1,this.isGroup=e||!1,this.isOptional=t||!1,this.isQuantifier=n||!1,this.isAlternator=r||!1,this.quantifier={min:1,max:1}}},3194:function(){Array.prototype.includes||Object.defineProperty(Array.prototype,\"includes\",{value:function(e,t){if(null==this)throw new TypeError('\"this\" is null or not defined');var n=Object(this),r=n.length>>>0;if(0===r)return!1;for(var i=0|t,a=Math.max(i>=0?i:r-Math.abs(i),0);a<r;){if(n[a]===e)return!0;a++}return!1}})},9302:function(){var e=Function.bind.call(Function.call,Array.prototype.reduce),t=Function.bind.call(Function.call,Object.prototype.propertyIsEnumerable),n=Function.bind.call(Function.call,Array.prototype.concat),r=Object.keys;Object.entries||(Object.entries=function(i){return e(r(i),function(e,r){return n(e,\"string\"==typeof r&&t(i,r)?[[r,i[r]]]:[])},[])})},7149:function(){function e(t){return(e=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e})(t)}\"function\"!=typeof Object.getPrototypeOf&&(Object.getPrototypeOf=\"object\"===e(\"test\".__proto__)?function(e){return e.__proto__}:function(e){return e.constructor.prototype})},4013:function(){String.prototype.includes||(String.prototype.includes=function(e,t){return\"number\"!=typeof t&&(t=0),!(t+e.length>this.length)&&-1!==this.indexOf(e,t)})},8711:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.caret=function(e,t,n,r,a){var o,s=this,l=this.opts;if(void 0===t)return\"selectionStart\"in e&&\"selectionEnd\"in e?(t=e.selectionStart,n=e.selectionEnd):i.default.getSelection?(o=i.default.getSelection().getRangeAt(0)).commonAncestorContainer.parentNode!==e&&o.commonAncestorContainer!==e||(t=o.startOffset,n=o.endOffset):document.selection&&document.selection.createRange&&(n=(t=0-(o=document.selection.createRange()).duplicate().moveStart(\"character\",-e.inputmask._valueGet().length))+o.text.length),{begin:r?t:c.call(s,t),end:r?n:c.call(s,n)};if(Array.isArray(t)&&(n=s.isRTL?t[0]:t[1],t=s.isRTL?t[1]:t[0]),void 0!==t.begin&&(n=s.isRTL?t.begin:t.end,t=s.isRTL?t.end:t.begin),\"number\"==typeof t){t=r?t:c.call(s,t),n=\"number\"==typeof(n=r?n:c.call(s,n))?n:t;var u=parseInt(((e.ownerDocument.defaultView||i.default).getComputedStyle?(e.ownerDocument.defaultView||i.default).getComputedStyle(e,null):e.currentStyle).fontSize)*n;if(e.scrollLeft=u>e.scrollWidth?u:0,e.inputmask.caretPos={begin:t,end:n},l.insertModeVisual&&!1===l.insertMode&&t===n&&(a||n++),e===(e.inputmask.shadowRoot||e.ownerDocument).activeElement){if(\"setSelectionRange\"in e)e.setSelectionRange(t,n);else if(i.default.getSelection){if(o=document.createRange(),void 0===e.firstChild||null===e.firstChild){var d=document.createTextNode(\"\");e.appendChild(d)}o.setStart(e.firstChild,t<e.inputmask._valueGet().length?t:e.inputmask._valueGet().length),o.setEnd(e.firstChild,n<e.inputmask._valueGet().length?n:e.inputmask._valueGet().length),o.collapse(!0);var f=i.default.getSelection();f.removeAllRanges(),f.addRange(o)}else e.createTextRange&&((o=e.createTextRange()).collapse(!0),o.moveEnd(\"character\",n),o.moveStart(\"character\",t),o.select());void 0===e.inputmask.caretHook||e.inputmask.caretHook.call(s,{begin:t,end:n})}}},t.determineLastRequiredPosition=function(e){var t,n,r=this,i=r.maskset,s=r.dependencyLib,u=l.call(r),d={},c=i.validPositions[u],f=o.getMaskTemplate.call(r,!0,l.call(r),!0,!0),p=f.length,h=void 0!==c?c.locator.slice():void 0;for(t=u+1;t<f.length;t++)h=(n=o.getTestTemplate.call(r,t,h,t-1)).locator.slice(),d[t]=s.extend(!0,{},n);var m=c&&void 0!==c.alternation?c.locator[c.alternation]:void 0;for(t=p-1;t>u&&((n=d[t]).match.optionality||n.match.optionalQuantifier&&n.match.newBlockMarker||m&&(m!==d[t].locator[c.alternation]&&!0!==n.match.static||!0===n.match.static&&n.locator[c.alternation]&&a.checkAlternationMatch.call(r,n.locator[c.alternation].toString().split(\",\"),m.toString().split(\",\"))&&\"\"!==o.getTests.call(r,t)[0].def))&&f[t]===o.getPlaceholder.call(r,t,n.match);t--)p--;return e?{l:p,def:d[p]?d[p].match:void 0}:p},t.determineNewCaretPosition=function(e,t,n){var r,i,a,c=this,f=c.maskset,p=c.opts;if(t&&(c.isRTL?e.end=e.begin:e.begin=e.end),e.begin===e.end){switch(n=n||p.positionCaretOnClick){case\"none\":break;case\"select\":e={begin:0,end:s.call(c).length};break;case\"ignore\":e.end=e.begin=d.call(c,l.call(c));break;case\"radixFocus\":if(c.clicked>1&&0===f.validPositions.length)break;if(function(e){if(\"\"!==p.radixPoint&&0!==p.digits){var t=f.validPositions;if(void 0===t[e]||void 0===t[e].input){if(e<d.call(c,-1))return!0;var n=s.call(c).indexOf(p.radixPoint);if(-1!==n){for(var r=0,i=t.length;r<i;r++)if(t[r]&&n<r&&t[r].input!==o.getPlaceholder.call(c,r))return!1;return!0}}}return!1}(e.begin)){var h=s.call(c).join(\"\").indexOf(p.radixPoint);e.end=e.begin=p.numericInput?d.call(c,h):h;break}default:if(r=e.begin,i=l.call(c,r,!0),r<=(a=d.call(c,-1!==i||u.call(c,0)?i:-1)))e.end=e.begin=u.call(c,r,!1,!0)?r:d.call(c,r);else{var m=f.validPositions[i],v=o.getTestTemplate.call(c,a,m?m.match.locator:void 0,m),g=o.getPlaceholder.call(c,a,v.match);if(\"\"!==g&&s.call(c)[a]!==g&&!0!==v.match.optionalQuantifier&&!0!==v.match.newBlockMarker||!u.call(c,a,p.keepStatic,!0)&&v.match.def===g){var _=d.call(c,a);(r>=_||r===a)&&(a=_)}e.end=e.begin=a}}return e}},t.getBuffer=s,t.getBufferTemplate=function(){var e=this.maskset;return void 0===e._buffer&&(e._buffer=o.getMaskTemplate.call(this,!1,1),void 0===e.buffer&&(e.buffer=e._buffer.slice())),e._buffer},t.getLastValidPosition=l,t.isMask=u,t.resetMaskSet=function(e){var t=this.maskset;t.buffer=void 0,!0!==e&&(t.validPositions=[],t.p=0),!1===e&&(t.tests={},t.jitOffset={})},t.seekNext=d,t.seekPrevious=function(e,t){var n=this,r=e-1;if(e<=0)return 0;for(;r>0&&(!0===t&&(!0!==o.getTest.call(n,r).match.newBlockMarker||!u.call(n,r,void 0,!0))||!0!==t&&!u.call(n,r,void 0,!0));)r--;return r},t.translatePosition=c;var r,i=(r=n(9380))&&r.__esModule?r:{default:r},a=n(7215),o=n(4713);function s(e){var t=this,n=t.maskset;return void 0!==n.buffer&&!0!==e||(n.buffer=o.getMaskTemplate.call(t,!0,l.call(t),!0),void 0===n._buffer&&(n._buffer=n.buffer.slice())),n.buffer}function l(e,t,n){var r=this.maskset,i=-1,a=-1,o=n||r.validPositions;void 0===e&&(e=-1);for(var s=0,l=o.length;s<l;s++)o[s]&&(t||!0!==o[s].generatedInput)&&(s<=e&&(i=s),s>=e&&(a=s));return-1===i||i===e?a:-1===a||e-i<a-e?i:a}function u(e,t,n){var r=this,i=this.maskset,a=o.getTestTemplate.call(r,e).match;if(\"\"===a.def&&(a=o.getTest.call(r,e).match),!0!==a.static)return a.fn;if(!0===n&&void 0!==i.validPositions[e]&&!0!==i.validPositions[e].generatedInput)return!0;if(!0!==t&&e>-1){if(n){var s=o.getTests.call(r,e);return s.length>1+(\"\"===s[s.length-1].match.def?1:0)}var l=o.determineTestTemplate.call(r,e,o.getTests.call(r,e)),u=o.getPlaceholder.call(r,e,l.match);return l.match.def!==u}return!1}function d(e,t,n){var r=this;void 0===n&&(n=!0);for(var i=e+1;\"\"!==o.getTest.call(r,i).match.def&&(!0===t&&(!0!==o.getTest.call(r,i).match.newBlockMarker||!u.call(r,i,void 0,!0))||!0!==t&&!u.call(r,i,void 0,n));)i++;return i}function c(e){var t=this.opts,n=this.el;return!this.isRTL||\"number\"!=typeof e||t.greedy&&\"\"===t.placeholder||!n||(e=this._valueGet().length-e)<0&&(e=0),e}},4713:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.determineTestTemplate=c,t.getDecisionTaker=l,t.getMaskTemplate=function(e,t,n,r,i){var a=this,o=this.opts,s=this.maskset,l=o.greedy;i&&o.greedy&&(o.greedy=!1,a.maskset.tests={}),t=t||0;var f,p,m,v,g=[],_=0;do{if(!0===e&&s.validPositions[_])p=(m=i&&s.validPositions[_].match.optionality&&void 0===s.validPositions[_+1]&&(!0===s.validPositions[_].generatedInput||s.validPositions[_].input==o.skipOptionalPartCharacter&&_>0)?c.call(a,_,h.call(a,_,f,_-1)):s.validPositions[_]).match,f=m.locator.slice(),g.push(!0===n?m.input:!1===n?p.nativeDef:u.call(a,_,p));else{p=(m=d.call(a,_,f,_-1)).match,f=m.locator.slice();var y=!0!==r&&(!1!==o.jitMasking?o.jitMasking:p.jit);(v=(v||s.validPositions[_-1])&&p.static&&p.def!==o.groupSeparator&&null===p.fn)||!1===y||void 0===y||\"number\"==typeof y&&isFinite(y)&&y>_?g.push(!1===n?p.nativeDef:u.call(a,g.length,p)):v=!1}_++}while(!0!==p.static||\"\"!==p.def||t>_);return\"\"===g[g.length-1]&&g.pop(),!1===n&&void 0!==s.maskLength||(s.maskLength=_-1),o.greedy=l,g},t.getPlaceholder=u,t.getTest=f,t.getTestTemplate=d,t.getTests=h,t.isSubsetOf=p;var r,i=(r=n(2394))&&r.__esModule?r:{default:r},a=n(8711);function o(e){return o=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},o(e)}function s(e,t){var n=(null!=e.alternation?e.mloc[l(e)]:e.locator).join(\"\");if(\"\"!==n)for(n=n.split(\":\")[0];n.length<t;)n+=\"0\";return n}function l(e){var t=e.locator[e.alternation];return\"string\"==typeof t&&t.length>0&&(t=t.split(\",\")[0]),void 0!==t?t.toString():\"\"}function u(e,t,n){var r=this,i=this.opts,s=this.maskset;if(void 0!==(t=t||f.call(r,e).match).placeholder||!0===n){if(\"\"!==t.placeholder&&!0===t.static&&!0!==t.generated){var l=a.getLastValidPosition.call(r,e),u=a.seekNext.call(r,l);return(n?e<=u:e<u)?i.staticDefinitionSymbol&&t.static?t.nativeDef:t.def:\"function\"==typeof t.placeholder?t.placeholder(i):t.placeholder}return\"function\"==typeof t.placeholder?t.placeholder(i):t.placeholder}if(!0===t.static){if(e>-1&&void 0===s.validPositions[e]){var d,c=h.call(r,e),p=[];if(\"string\"==typeof i.placeholder&&c.length>1+(\"\"===c[c.length-1].match.def?1:0))for(var m=0;m<c.length;m++)if(\"\"!==c[m].match.def&&!0!==c[m].match.optionality&&!0!==c[m].match.optionalQuantifier&&(!0===c[m].match.static||void 0===d||!1!==c[m].match.fn.test(d.match.def,s,e,!0,i))&&(p.push(c[m]),!0===c[m].match.static&&(d=c[m]),p.length>1&&/[0-9a-bA-Z]/.test(p[0].match.def)))return i.placeholder.charAt(e%i.placeholder.length)}return t.def}return\"object\"===o(i.placeholder)?t.def:i.placeholder.charAt(e%i.placeholder.length)}function d(e,t,n){return this.maskset.validPositions[e]||c.call(this,e,h.call(this,e,t?t.slice():t,n))}function c(e,t){var n=this.opts,r=0,i=function(e,t){var n=0,r=!1;return t.forEach(function(e){e.match.optionality&&(0!==n&&n!==e.match.optionality&&(r=!0),(0===n||n>e.match.optionality)&&(n=e.match.optionality))}),n&&(0==e||1==t.length?n=0:r||(n=0)),n}(e,t);e=e>0?e-1:0;var a,o,l,u=s(f.call(this,e));n.greedy&&t.length>1&&\"\"===t[t.length-1].match.def&&(r=1);for(var d=0;d<t.length-r;d++){var c=t[d];a=s(c,u.length);var p=Math.abs(a-u);(!0!==c.unMatchedAlternationStopped||t.filter(function(e){return!0!==e.unMatchedAlternationStopped}).length<=1)&&(void 0===o||\"\"!==a&&p<o||l&&!n.greedy&&l.match.optionality&&l.match.optionality-i>0&&\"master\"===l.match.newBlockMarker&&(!c.match.optionality||c.match.optionality-i<1||!c.match.newBlockMarker)||l&&!n.greedy&&l.match.optionalQuantifier&&!c.match.optionalQuantifier)&&(o=p,l=c)}return l}function f(e,t){var n=this.maskset;return n.validPositions[e]?n.validPositions[e]:(t||h.call(this,e))[0]}function p(e,t,n){function r(e){for(var t,n=[],r=-1,i=0,a=e.length;i<a;i++)if(\"-\"===e.charAt(i))for(t=e.charCodeAt(i+1);++r<t;)n.push(String.fromCharCode(r));else r=e.charCodeAt(i),n.push(e.charAt(i));return n.join(\"\")}return e.match.def===t.match.nativeDef||!(!(n.regex||e.match.fn instanceof RegExp&&t.match.fn instanceof RegExp)||!0===e.match.static||!0===t.match.static)&&(\".\"===t.match.fn.source||-1!==r(t.match.fn.source.replace(/[[\\]/]/g,\"\")).indexOf(r(e.match.fn.source.replace(/[[\\]/]/g,\"\"))))}function h(e,t,n){var r,a,o=this,s=this.dependencyLib,l=this.maskset,u=this.opts,d=this.el,f=l.maskToken,h=t?n:0,m=t?t.slice():[0],v=[],g=!1,_=t?t.join(\"\"):\"\",y=!1;function b(t,n,a,s){function c(a,s,f){function m(e,t){var n=0===t.matches.indexOf(e);return n||t.matches.every(function(r,i){return!0===r.isQuantifier?n=m(e,t.matches[i-1]):Object.prototype.hasOwnProperty.call(r,\"matches\")&&(n=m(e,r)),!n}),n}function w(e,t,n){var r,i;if((l.tests[e]||l.validPositions[e])&&(l.validPositions[e]?[l.validPositions[e]]:l.tests[e]).every(function(e,a){if(e.mloc[t])return r=e,!1;var o=void 0!==n?n:e.alternation,s=void 0!==e.locator[o]?e.locator[o].toString().indexOf(t):-1;return(void 0===i||s<i)&&-1!==s&&(r=e,i=s),!0}),r){var a=r.locator[r.alternation],o=r.mloc[t]||r.mloc[a]||r.locator;return-1!==o[o.length-1].toString().indexOf(\":\")&&o.pop(),o.slice((void 0!==n?n:r.alternation)+1)}return void 0!==n?w(e,t):void 0}function k(t,n){return!0===t.match.static&&!0!==n.match.static&&n.match.fn.test(t.match.def,l,e,!1,u,!1)}function L(e,t){var n=e.alternation,r=void 0===t||n<=t.alternation&&-1===e.locator[n].toString().indexOf(t.locator[n]);if(!r&&n>t.alternation)for(var i=0;i<n;i++)if(e.locator[i]!==t.locator[i]){n=i,r=!0;break}return!!r&&function(n){e.mloc=e.mloc||{};var r=e.locator[n];if(void 0!==r){if(\"string\"==typeof r&&(r=r.split(\",\")[0]),void 0===e.mloc[r]&&(e.mloc[r]=e.locator.slice(),e.mloc[r].push(\":\".concat(e.alternation))),void 0!==t){for(var i in t.mloc)\"string\"==typeof i&&(i=parseInt(i.split(\",\")[0])),e.mloc[i+0]=t.mloc[i];e.locator[n]=Object.keys(e.mloc).join(\",\")}return e.alternation>n&&(e.alternation=n),!0}return e.alternation=void 0,!1}(n)}function x(e,t){if(e.locator.length!==t.locator.length)return!1;for(var n=e.alternation+1;n<e.locator.length;n++)if(e.locator[n]!==t.locator[n])return!1;return!0}if(h>e+u._maxTestPos)throw new Error(\"Inputmask: There is probably an error in your mask definition or in the code. Create an issue on github with an example of the mask you are using. \".concat(l.mask));if(h===e&&void 0===a.matches){if(v.push({match:a,locator:s.reverse(),cd:_,mloc:{}}),!a.optionality||void 0!==f||!(u.definitions&&u.definitions[a.nativeDef]&&u.definitions[a.nativeDef].optional||i.default.prototype.definitions[a.nativeDef]&&i.default.prototype.definitions[a.nativeDef].optional))return!0;g=!0,h=e}else if(void 0!==a.matches){if(a.isGroup&&f!==a)return function(){if(a=c(t.matches[t.matches.indexOf(a)+1],s,f))return!0}();if(a.isOptional)return function(){var t=a,i=v.length;if(a=b(a,n,s,f),v.length>0){if(v.forEach(function(e,t){t>=i&&(e.match.optionality=e.match.optionality?e.match.optionality+1:1)}),r=v[v.length-1].match,void 0!==f||!m(r,t))return a;g=!0,h=e}}();if(a.isAlternator)return function(){function r(e){for(var t,n=e.matches[0].matches?e.matches[0].matches.length:1,r=0;r<e.matches.length&&n===(t=e.matches[r].matches?e.matches[r].matches.length:1);r++);return n!==t}o.hasAlternator=!0;var i,m=a,_=[],b=v.slice(),M=s.length,T=n.length>0?n.shift():-1;if(-1===T||\"string\"==typeof T){var S,D=h,E=n.slice(),Y=[];if(\"string\"==typeof T)Y=T.split(\",\");else for(S=0;S<m.matches.length;S++)Y.push(S.toString());if(void 0!==l.excludes[e]){for(var A=Y.slice(),O=0,C=l.excludes[e].length;O<C;O++){var j=l.excludes[e][O].toString().split(\":\");s.length==j[1]&&Y.splice(Y.indexOf(j[0]),1)}0===Y.length&&(delete l.excludes[e],Y=A)}(!0===u.keepStatic||isFinite(parseInt(u.keepStatic))&&D>=u.keepStatic)&&(Y=Y.slice(0,1));for(var P=0;P<Y.length;P++){S=parseInt(Y[P]),v=[],n=\"string\"==typeof T&&w(h,S,M)||E.slice();var H=m.matches[S];if(H&&c(H,[S].concat(s),f))a=!0;else if(0===P&&(y=r(m)),H&&H.matches&&H.matches.length>m.matches[0].matches.length)break;i=v.slice(),h=D,v=[];for(var I=0;I<i.length;I++){var F=i[I],N=!1;F.alternation=F.alternation||M,L(F);for(var R=0;R<_.length;R++){var V=_[R];if(\"string\"!=typeof T||void 0!==F.alternation&&Y.includes(F.locator[F.alternation].toString())){if(F.match.nativeDef===V.match.nativeDef){N=!0,L(V,F);break}if(p(F,V,u)){L(F,V)&&(N=!0,_.splice(_.indexOf(V),0,F));break}if(p(V,F,u)){L(V,F);break}if(k(F,V)){x(F,V)||void 0!==d.inputmask.userOptions.keepStatic?L(F,V)&&(N=!0,_.splice(_.indexOf(V),0,F)):u.keepStatic=!0;break}if(k(V,F)){L(V,F);break}}}N||_.push(F)}}v=b.concat(_),h=e,g=v.length>0&&y,a=_.length>0&&!y,y&&g&&!a&&v.forEach(function(e,t){e.unMatchedAlternationStopped=!0}),n=E.slice()}else a=c(m.matches[T]||t.matches[T],[T].concat(s),f);if(a)return!0}();if(a.isQuantifier&&f!==t.matches[t.matches.indexOf(a)-1])return function(){for(var i=a,o=!1,d=n.length>0?n.shift():0;d<(isNaN(i.quantifier.max)?d+1:i.quantifier.max)&&h<=e;d++){var f=t.matches[t.matches.indexOf(i)-1];if(a=c(f,[d].concat(s),f)){if(v.forEach(function(t,n){(r=M(f,t.match)?t.match:v[v.length-1].match).optionalQuantifier=d>=i.quantifier.min,r.jit=(d+1)*(f.matches.indexOf(r)+1)>i.quantifier.jit,r.optionalQuantifier&&m(r,f)&&(g=!0,h=e,u.greedy&&null==l.validPositions[e-1]&&d>i.quantifier.min&&-1!=[\"*\",\"+\"].indexOf(i.quantifier.max)&&(v.pop(),_=void 0),o=!0,a=!1),!o&&r.jit&&(l.jitOffset[e]=f.matches.length-f.matches.indexOf(r))}),o)break;return!0}}}();if(a=b(a,n,s,f))return!0}else h++}for(var f=n.length>0?n.shift():0;f<t.matches.length;f++)if(!0!==t.matches[f].isQuantifier){var m=c(t.matches[f],[f].concat(a),s);if(m&&h===e)return m;if(h>e)break}}function M(e,t){var n=-1!=e.matches.indexOf(t);return n||e.matches.forEach(function(e,r){void 0===e.matches||n||(n=M(e,t))}),n}if(e>-1){if(void 0===t){for(var w,k=e-1;void 0===(w=l.validPositions[k]||l.tests[k])&&k>-1;)k--;void 0!==w&&k>-1&&(m=function(e,t){var n,r=[];return Array.isArray(t)||(t=[t]),t.length>0&&(void 0===t[0].alternation||!0===u.keepStatic?0===(r=c.call(o,e,t.slice()).locator.slice()).length&&(r=t[0].locator.slice()):t.forEach(function(e){\"\"!==e.def&&(0===r.length?(n=e.alternation,r=e.locator.slice()):e.locator[n]&&-1===r[n].toString().indexOf(e.locator[n])&&(r[n]+=\",\"+e.locator[n]))})),r}(k,w),_=m.join(\"\"),h=k)}if(l.tests[e]&&l.tests[e][0].cd===_)return l.tests[e];for(var L=m.shift();L<f.length&&!(b(f[L],m,[L])&&h===e||h>e);L++);}return(0===v.length||g)&&v.push({match:{fn:null,static:!0,optionality:!1,casing:null,def:\"\",placeholder:\"\"},locator:y&&0===v.filter(function(e){return!0!==e.unMatchedAlternationStopped}).length?[0]:[],mloc:{},cd:_}),void 0!==t&&l.tests[e]?a=s.extend(!0,[],v):(l.tests[e]=s.extend(!0,[],v),a=l.tests[e]),v.forEach(function(e){e.match.optionality=e.match.defOptionality||!1}),a}},7215:function(e,t,n){Object.defineProperty(t,\"__esModule\",{value:!0}),t.alternate=s,t.checkAlternationMatch=function(e,t,n){for(var r,i=this.opts.greedy?t:t.slice(0,1),a=!1,o=void 0!==n?n.split(\",\"):[],s=0;s<o.length;s++)-1!==(r=e.indexOf(o[s]))&&e.splice(r,1);for(var l=0;l<e.length;l++)if(i.includes(e[l])){a=!0;break}return a},t.handleRemove=function(e,t,n,r,l){var u=this,d=this.maskset,c=this.opts;if((c.numericInput||u.isRTL)&&(t===i.keys.Backspace?t=i.keys.Delete:t===i.keys.Delete&&(t=i.keys.Backspace),u.isRTL)){var f=n.end;n.end=n.begin,n.begin=f}var p,h=a.getLastValidPosition.call(u,void 0,!0);n.end>=a.getBuffer.call(u).length&&h>=n.end&&(n.end=h+1),t===i.keys.Backspace?n.end-n.begin<1&&(n.begin=a.seekPrevious.call(u,n.begin)):t===i.keys.Delete&&n.begin===n.end&&(n.end=a.isMask.call(u,n.end,!0,!0)?n.end+1:a.seekNext.call(u,n.end)+1),!1!==(p=m.call(u,n))&&((!0!==r&&!1!==c.keepStatic||null!==c.regex&&-1!==o.getTest.call(u,n.begin).match.def.indexOf(\"|\"))&&s.call(u,!0),!0!==r&&(d.p=t===i.keys.Delete?n.begin+p:n.begin,d.p=a.determineNewCaretPosition.call(u,{begin:d.p,end:d.p},!1,!1===c.insertMode&&t===i.keys.Backspace?\"none\":void 0).begin))},t.isComplete=u,t.isSelection=d,t.isValid=c,t.refreshFromBuffer=p,t.revalidateMask=m;var r=n(6030),i=n(2839),a=n(8711),o=n(4713);function s(e,t,n,r,i,l){var u=this,d=this.dependencyLib,f=this.opts,p=u.maskset;if(!u.hasAlternator)return!1;var h,m,v,g,_,y,b,M,w,k,L,x=d.extend(!0,[],p.validPositions),T=d.extend(!0,{},p.tests),S=!1,D=!1,E=void 0!==i?i:a.getLastValidPosition.call(u);if(l&&(k=l.begin,L=l.end,l.begin>l.end&&(k=l.end,L=l.begin)),-1===E&&void 0===i)h=0,m=(g=o.getTest.call(u,h)).alternation;else for(;E>=0;E--)if((v=p.validPositions[E])&&void 0!==v.alternation){if(E<=(e||0)&&g&&g.locator[v.alternation]!==v.locator[v.alternation])break;h=E,m=p.validPositions[h].alternation,g=v}if(void 0!==m){b=parseInt(h),p.excludes[b]=p.excludes[b]||[],!0!==e&&p.excludes[b].push((0,o.getDecisionTaker)(g)+\":\"+g.alternation);var Y=[],A=-1;for(_=b;b<a.getLastValidPosition.call(u,void 0,!0)+1;_++)-1===A&&e<=_&&void 0!==t&&(Y.push(t),A=Y.length-1),(y=p.validPositions[b])&&!0!==y.generatedInput&&(void 0===l||_<k||_>=L)&&Y.push(y.input),p.validPositions.splice(b,1);for(-1===A&&void 0!==t&&(Y.push(t),A=Y.length-1);void 0!==p.excludes[b]&&p.excludes[b].length<10;){for(p.tests={},a.resetMaskSet.call(u,!0),S=!0,_=0;_<Y.length&&(M=S.caret||0==f.insertMode&&null!=M?a.seekNext.call(u,M):a.getLastValidPosition.call(u,void 0,!0)+1,w=Y[_],S=c.call(u,M,w,!1,r,!0));_++)_===A&&(D=S),1==e&&S&&(D={caretPos:_});if(S)break;if(a.resetMaskSet.call(u),g=o.getTest.call(u,b),p.validPositions=d.extend(!0,[],x),p.tests=d.extend(!0,{},T),!p.excludes[b]){D=s.call(u,e,t,n,r,b-1,l);break}if(null!=g.alternation){var O=(0,o.getDecisionTaker)(g);if(-1!==p.excludes[b].indexOf(O+\":\"+g.alternation)){D=s.call(u,e,t,n,r,b-1,l);break}for(p.excludes[b].push(O+\":\"+g.alternation),_=b;_<a.getLastValidPosition.call(u,void 0,!0)+1;_++)p.validPositions.splice(b)}else delete p.excludes[b]}}return D&&!1===f.keepStatic||delete p.excludes[b],D}function l(e,t,n){var r=this.opts,a=this.maskset;switch(r.casing||t.casing){case\"upper\":e=e.toUpperCase();break;case\"lower\":e=e.toLowerCase();break;case\"title\":var o=a.validPositions[n-1];e=0===n||o&&o.input===String.fromCharCode(i.keyCode.Space)?e.toUpperCase():e.toLowerCase();break;default:if(\"function\"==typeof r.casing){var s=Array.prototype.slice.call(arguments);s.push(a.validPositions),e=r.casing.apply(this,s)}}return e}function u(e){var t=this,n=this.opts,r=this.maskset;if(\"function\"==typeof n.isComplete)return n.isComplete(e,n);if(\"*\"!==n.repeat){var i=!1,s=a.determineLastRequiredPosition.call(t,!0),l=s.l;if(void 0===s.def||s.def.newBlockMarker||s.def.optionality||s.def.optionalQuantifier){i=!0;for(var u=0;u<=l;u++){var d=o.getTestTemplate.call(t,u).match;if(!0!==d.static&&void 0===r.validPositions[u]&&(!1===d.optionality||void 0===d.optionality||d.optionality&&0==d.newBlockMarker)&&(!1===d.optionalQuantifier||void 0===d.optionalQuantifier)||!0===d.static&&\"\"!=d.def&&e[u]!==o.getPlaceholder.call(t,u,d)){i=!1;break}}}return i}}function d(e){var t=this.opts.insertMode?0:1;return this.isRTL?e.begin-e.end>t:e.end-e.begin>t}function c(e,t,n,r,i,f,v){var g=this,_=this.dependencyLib,y=this.opts,b=g.maskset;n=!0===n;var M=e;function w(e){if(void 0!==e){if(void 0!==e.remove&&(Array.isArray(e.remove)||(e.remove=[e.remove]),e.remove.sort(function(e,t){return g.isRTL?e.pos-t.pos:t.pos-e.pos}).forEach(function(e){m.call(g,{begin:e,end:e+1})}),e.remove=void 0),void 0!==e.insert&&(Array.isArray(e.insert)||(e.insert=[e.insert]),e.insert.sort(function(e,t){return g.isRTL?t.pos-e.pos:e.pos-t.pos}).forEach(function(e){\"\"!==e.c&&c.call(g,e.pos,e.c,void 0===e.strict||e.strict,void 0!==e.fromIsValid?e.fromIsValid:r)}),e.insert=void 0),e.refreshFromBuffer&&e.buffer){var t=e.refreshFromBuffer;p.call(g,!0===t?t:t.start,t.end,e.buffer),e.refreshFromBuffer=void 0}void 0!==e.rewritePosition&&(M=e.rewritePosition,e=!0)}return e}function k(t,n,i){var s=!1;return o.getTests.call(g,t).every(function(u,c){var f=u.match;if(a.getBuffer.call(g,!0),!1!==(s=(!f.jit||void 0!==b.validPositions[a.seekPrevious.call(g,t)])&&(null!=f.fn?f.fn.test(n,b,t,i,y,d.call(g,e)):(n===f.def||n===y.skipOptionalPartCharacter)&&\"\"!==f.def&&{c:o.getPlaceholder.call(g,t,f,!0)||f.def,pos:t}))){var p=void 0!==s.c?s.c:n,h=t;return p=p===y.skipOptionalPartCharacter&&!0===f.static?o.getPlaceholder.call(g,t,f,!0)||f.def:p,!0!==(s=w(s))&&void 0!==s.pos&&s.pos!==t&&(h=s.pos),!0!==s&&void 0===s.pos&&void 0===s.c||!1===m.call(g,e,_.extend({},u,{input:l.call(g,p,f,h)}),r,h)&&(s=!1),!1}return!0}),s}void 0!==e.begin&&(M=g.isRTL?e.end:e.begin);var L=!0,x=_.extend(!0,[],b.validPositions);if(!1===y.keepStatic&&void 0!==b.excludes[M]&&!0!==i&&!0!==r)for(var T=M;T<(g.isRTL?e.begin:e.end);T++)void 0!==b.excludes[T]&&(b.excludes[T]=void 0,delete b.tests[T]);if(\"function\"==typeof y.preValidation&&!0!==r&&!0!==f&&(L=w(L=y.preValidation.call(g,a.getBuffer.call(g),M,t,d.call(g,e),y,b,e,n||i))),!0===L){if(L=k(M,t,n),(!n||!0===r)&&!1===L&&!0!==f){var S=b.validPositions[M];if(!S||!0!==S.match.static||S.match.def!==t&&t!==y.skipOptionalPartCharacter){if(y.insertMode||void 0===b.validPositions[a.seekNext.call(g,M)]||e.end>M){var D=!1;if(b.jitOffset[M]&&void 0===b.validPositions[a.seekNext.call(g,M)]&&!1!==(L=c.call(g,M+b.jitOffset[M],t,!0,!0))&&(!0!==i&&(L.caret=M),D=!0),e.end>M&&(b.validPositions[M]=void 0),!D&&!a.isMask.call(g,M,y.keepStatic&&0===M))for(var E=M+1,Y=a.seekNext.call(g,M,!1,0!==M);E<=Y;E++)if(!1!==(L=k(E,t,n))){L=h.call(g,M,void 0!==L.pos?L.pos:E)||L,M=E;break}}}else L={caret:a.seekNext.call(g,M)}}g.hasAlternator&&!0!==i&&!n&&(i=!0,!1===L&&y.keepStatic&&(u.call(g,a.getBuffer.call(g))||0===M)?L=s.call(g,M,t,n,r,void 0,e):(d.call(g,e)&&b.tests[M]&&b.tests[M].length>1&&y.keepStatic||1==L&&!0!==y.numericInput&&b.tests[M]&&b.tests[M].length>1&&a.getLastValidPosition.call(g,void 0,!0)>M)&&(L=s.call(g,!0))),!0===L&&(L={pos:M})}if(\"function\"==typeof y.postValidation&&!0!==r&&!0!==f){var A=y.postValidation.call(g,a.getBuffer.call(g,!0),void 0!==e.begin?g.isRTL?e.end:e.begin:e,t,L,y,b,n,v);void 0!==A&&(L=!0===A?L:A)}L&&void 0===L.pos&&(L.pos=M),!1===L||!0===f?(a.resetMaskSet.call(g,!0),b.validPositions=_.extend(!0,[],x)):h.call(g,void 0,M,!0);var O=w(L);return void 0!==g.maxLength&&a.getBuffer.call(g).length>g.maxLength&&!r&&(a.resetMaskSet.call(g,!0),b.validPositions=_.extend(!0,[],x),O=!1),O}function f(e,t,n){for(var r=this.maskset,i=!1,a=o.getTests.call(this,e),s=0;s<a.length;s++){if(a[s].match&&(a[s].match.nativeDef===t.match[n.shiftPositions?\"def\":\"nativeDef\"]&&(!n.shiftPositions||!t.match.static)||a[s].match.nativeDef===t.match.nativeDef||n.regex&&!a[s].match.static&&a[s].match.fn.test(t.input,r,e,!1,n))){i=!0;break}if(a[s].match&&a[s].match.def===t.match.nativeDef){i=void 0;break}}return!1===i&&void 0!==r.jitOffset[e]&&(i=f.call(this,e+r.jitOffset[e],t,n)),i}function p(e,t,n){var i,o,s=this,l=this.maskset,u=this.opts,d=this.dependencyLib,c=u.skipOptionalPartCharacter,f=s.isRTL?n.slice().reverse():n;if(u.skipOptionalPartCharacter=\"\",!0===e)a.resetMaskSet.call(s,!1),e=0,t=n.length,o=a.determineNewCaretPosition.call(s,{begin:0,end:0},!1).begin;else{for(i=e;i<t;i++)l.validPositions.splice(e,0);o=e}var p=new d.Event(\"keypress\");for(i=e;i<t;i++){p.key=f[i].toString(),s.ignorable=!1;var h=r.EventHandlers.keypressEvent.call(s,p,!0,!1,!1,o);!1!==h&&void 0!==h&&(o=h.forwardPosition)}u.skipOptionalPartCharacter=c}function h(e,t,n){var r=this,i=this.maskset,s=this.dependencyLib;if(void 0===e)for(e=t-1;e>0&&!i.validPositions[e];e--);for(var l=e;l<t;l++)if(void 0===i.validPositions[l]&&!a.isMask.call(r,l,!1)&&(0==l?o.getTest.call(r,l):i.validPositions[l-1])){var u=o.getTests.call(r,l).slice();\"\"===u[u.length-1].match.def&&u.pop();var d,f=o.determineTestTemplate.call(r,l,u);if(f&&(!0!==f.match.jit||\"master\"===f.match.newBlockMarker&&(d=i.validPositions[l+1])&&!0===d.match.optionalQuantifier)&&((f=s.extend({},f,{input:o.getPlaceholder.call(r,l,f.match,!0)||f.match.def})).generatedInput=!0,m.call(r,l,f,!0),!0!==n)){var p=i.validPositions[t].input;return i.validPositions[t]=void 0,c.call(r,t,p,!0,!0)}}}function m(e,t,n,r){var i=this,s=this.maskset,l=this.opts,u=this.dependencyLib;function p(e,t,n){var r=t[e];if(void 0!==r&&!0===r.match.static&&!0!==r.match.optionality&&(void 0===t[0]||void 0===t[0].alternation)){var i=n.begin<=e-1?t[e-1]&&!0===t[e-1].match.static&&t[e-1]:t[e-1],a=n.end>e+1?t[e+1]&&!0===t[e+1].match.static&&t[e+1]:t[e+1];return i&&a}return!1}var h=0,m=void 0!==e.begin?e.begin:e,v=void 0!==e.end?e.end:e,g=!0;if(e.begin>e.end&&(m=e.end,v=e.begin),r=void 0!==r?r:m,void 0===n&&(m!==v||l.insertMode&&void 0!==s.validPositions[r]||void 0===t||t.match.optionalQuantifier||t.match.optionality)){var _,y=u.extend(!0,[],s.validPositions),b=a.getLastValidPosition.call(i,void 0,!0);s.p=m;var M=d.call(i,e)?m:r;for(_=b;_>=M;_--)s.validPositions.splice(_,1),void 0===t&&delete s.tests[_+1];var w,k,L=r,x=L;for(t&&(s.validPositions[r]=u.extend(!0,{},t),x++,L++),null==y[v]&&s.jitOffset[v]&&(v+=s.jitOffset[v]+1),_=t?v:v-1;_<=b;_++){if(void 0!==(w=y[_])&&!0!==w.generatedInput&&(_>=v||_>=m&&p(_,y,{begin:m,end:v}))){for(;\"\"!==o.getTest.call(i,x).match.def;){if(!1!==(k=f.call(i,x,w,l))||\"+\"===w.match.def){\"+\"===w.match.def&&a.getBuffer.call(i,!0);var T=c.call(i,x,w.input,\"+\"!==w.match.def,!0);if(g=!1!==T,L=(T.pos||x)+1,!g&&k)break}else g=!1;if(g){void 0===t&&w.match.static&&_===e.begin&&h++;break}if(!g&&a.getBuffer.call(i),x>s.maskLength)break;x++}\"\"==o.getTest.call(i,x).match.def&&(g=!1),x=L}if(!g)break}if(!g)return s.validPositions=u.extend(!0,[],y),a.resetMaskSet.call(i,!0),!1}else t&&o.getTest.call(i,r).match.cd===t.match.cd&&(s.validPositions[r]=u.extend(!0,{},t));return a.resetMaskSet.call(i,!0),h}}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var a=t[r]={exports:{}};return e[r](a,a.exports,n),a.exports}var r={};return function(){var e=r;Object.defineProperty(e,\"__esModule\",{value:!0}),e.default=void 0,n(7149),n(3194),n(9302),n(4013),n(3851),n(219),n(207),n(5296);var t,i=(t=n(2394))&&t.__esModule?t:{default:t};e.default=i.default}(),r}()}),function(e,t,n){e(n.document).ajaxComplete(function(n,r,i){-1!==e.inArray(\"html\",i.dataTypes)&&e(\".inputmask, [data-inputmask], [data-inputmask-mask], [data-inputmask-alias], [data-inputmask-regex]\").each(function(e,n){void 0===n.inputmask&&t().mask(n)})}).ready(function(){e(\".inputmask, [data-inputmask], [data-inputmask-mask], [data-inputmask-alias],[data-inputmask-regex]\").each(function(e,n){void 0===n.inputmask&&t().mask(n)})})}(jQuery,window.Inputmask,window),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],t):t((e=\"undefined\"!=typeof globalThis?globalThis:e||self).noUiSlider={})}(this,function(e){\"use strict\";var t,n;function r(e){return\"object\"==typeof e&&\"function\"==typeof e.to}function i(e){e.parentElement.removeChild(e)}function a(e){return null!=e}function o(e){e.preventDefault()}function s(e){return\"number\"==typeof e&&!isNaN(e)&&isFinite(e)}function l(e,t,n){n>0&&(f(e,t),setTimeout(function(){p(e,t)},n))}function u(e){return Math.max(Math.min(e,100),0)}function d(e){return Array.isArray(e)?e:[e]}function c(e){var t=(e=String(e)).split(\".\");return t.length>1?t[1].length:0}function f(e,t){e.classList&&!/\\s/.test(t)?e.classList.add(t):e.className+=\" \"+t}function p(e,t){e.classList&&!/\\s/.test(t)?e.classList.remove(t):e.className=e.className.replace(new RegExp(\"(^|\\\\b)\"+t.split(\" \").join(\"|\")+\"(\\\\b|$)\",\"gi\"),\" \")}function h(e){var t=void 0!==window.pageXOffset,n=\"CSS1Compat\"===(e.compatMode||\"\");return{x:t?window.pageXOffset:n?e.documentElement.scrollLeft:e.body.scrollLeft,y:t?window.pageYOffset:n?e.documentElement.scrollTop:e.body.scrollTop}}function m(e,t){return 100/(t-e)}function v(e,t,n){return 100*t/(e[n+1]-e[n])}function g(e,t){for(var n=1;e>=t[n];)n+=1;return n}function _(e,t,n){if(n>=e.slice(-1)[0])return 100;var r=g(n,e),i=e[r-1],a=e[r],o=t[r-1],s=t[r];return o+function(e,t){return v(e,e[0]<0?t+Math.abs(e[0]):t-e[0],0)}([i,a],n)/m(o,s)}function y(e,t,n,r){if(100===r)return r;var i=g(r,e),a=e[i-1],o=e[i];return n?r-a>(o-a)/2?o:a:t[i-1]?e[i-1]+function(e,t){return Math.round(e/t)*t}(r-e[i-1],t[i-1]):r}e.PipsMode=void 0,(t=e.PipsMode||(e.PipsMode={})).Range=\"range\",t.Steps=\"steps\",t.Positions=\"positions\",t.Count=\"count\",t.Values=\"values\",e.PipsType=void 0,(n=e.PipsType||(e.PipsType={}))[n.None=-1]=\"None\",n[n.NoValue=0]=\"NoValue\",n[n.LargeValue=1]=\"LargeValue\",n[n.SmallValue=2]=\"SmallValue\";var b=function(){function e(e,t,n){var r;this.xPct=[],this.xVal=[],this.xSteps=[],this.xNumSteps=[],this.xHighestCompleteStep=[],this.xSteps=[n||!1],this.xNumSteps=[!1],this.snap=t;var i=[];for(Object.keys(e).forEach(function(t){i.push([d(e[t]),t])}),i.sort(function(e,t){return e[0][0]-t[0][0]}),r=0;r<i.length;r++)this.handleEntryPoint(i[r][1],i[r][0]);for(this.xNumSteps=this.xSteps.slice(0),r=0;r<this.xNumSteps.length;r++)this.handleStepPoint(r,this.xNumSteps[r])}return e.prototype.getDistance=function(e){for(var t=[],n=0;n<this.xNumSteps.length-1;n++)t[n]=v(this.xVal,e,n);return t},e.prototype.getAbsoluteDistance=function(e,t,n){var r,i=0;if(e<this.xPct[this.xPct.length-1])for(;e>this.xPct[i+1];)i++;else e===this.xPct[this.xPct.length-1]&&(i=this.xPct.length-2);n||e!==this.xPct[i+1]||i++,null===t&&(t=[]);var a=1,o=t[i],s=0,l=0,u=0,d=0;for(r=n?(e-this.xPct[i])/(this.xPct[i+1]-this.xPct[i]):(this.xPct[i+1]-e)/(this.xPct[i+1]-this.xPct[i]);o>0;)s=this.xPct[i+1+d]-this.xPct[i+d],t[i+d]*a+100-100*r>100?(l=s*r,a=(o-100*r)/t[i+d],r=1):(l=t[i+d]*s/100*a,a=0),n?(u-=l,this.xPct.length+d>=1&&d--):(u+=l,this.xPct.length-d>=1&&d++),o=t[i+d]*a;return e+u},e.prototype.toStepping=function(e){return e=_(this.xVal,this.xPct,e)},e.prototype.fromStepping=function(e){return function(e,t,n){if(n>=100)return e.slice(-1)[0];var r=g(n,t),i=e[r-1],a=e[r],o=t[r-1];return function(e,t){return t*(e[1]-e[0])/100+e[0]}([i,a],(n-o)*m(o,t[r]))}(this.xVal,this.xPct,e)},e.prototype.getStep=function(e){return e=y(this.xPct,this.xSteps,this.snap,e)},e.prototype.getDefaultStep=function(e,t,n){var r=g(e,this.xPct);return(100===e||t&&e===this.xPct[r-1])&&(r=Math.max(r-1,1)),(this.xVal[r]-this.xVal[r-1])/n},e.prototype.getNearbySteps=function(e){var t=g(e,this.xPct);return{stepBefore:{startValue:this.xVal[t-2],step:this.xNumSteps[t-2],highestStep:this.xHighestCompleteStep[t-2]},thisStep:{startValue:this.xVal[t-1],step:this.xNumSteps[t-1],highestStep:this.xHighestCompleteStep[t-1]},stepAfter:{startValue:this.xVal[t],step:this.xNumSteps[t],highestStep:this.xHighestCompleteStep[t]}}},e.prototype.countStepDecimals=function(){var e=this.xNumSteps.map(c);return Math.max.apply(null,e)},e.prototype.hasNoSize=function(){return this.xVal[0]===this.xVal[this.xVal.length-1]},e.prototype.convert=function(e){return this.getStep(this.toStepping(e))},e.prototype.handleEntryPoint=function(e,t){var n;if(!s(n=\"min\"===e?0:\"max\"===e?100:parseFloat(e))||!s(t[0]))throw new Error(\"noUiSlider: 'range' value isn't numeric.\");this.xPct.push(n),this.xVal.push(t[0]);var r=Number(t[1]);n?this.xSteps.push(!isNaN(r)&&r):isNaN(r)||(this.xSteps[0]=r),this.xHighestCompleteStep.push(0)},e.prototype.handleStepPoint=function(e,t){if(t)if(this.xVal[e]!==this.xVal[e+1]){this.xSteps[e]=v([this.xVal[e],this.xVal[e+1]],t,0)/m(this.xPct[e],this.xPct[e+1]);var n=(this.xVal[e+1]-this.xVal[e])/this.xNumSteps[e],r=Math.ceil(Number(n.toFixed(3))-1),i=this.xVal[e]+this.xNumSteps[e]*r;this.xHighestCompleteStep[e]=i}else this.xSteps[e]=this.xHighestCompleteStep[e]=this.xVal[e]},e}(),M={to:function(e){return void 0===e?\"\":e.toFixed(2)},from:Number},w={target:\"target\",base:\"base\",origin:\"origin\",handle:\"handle\",handleLower:\"handle-lower\",handleUpper:\"handle-upper\",touchArea:\"touch-area\",horizontal:\"horizontal\",vertical:\"vertical\",background:\"background\",connect:\"connect\",connects:\"connects\",ltr:\"ltr\",rtl:\"rtl\",textDirectionLtr:\"txt-dir-ltr\",textDirectionRtl:\"txt-dir-rtl\",draggable:\"draggable\",drag:\"state-drag\",tap:\"state-tap\",active:\"active\",tooltip:\"tooltip\",pips:\"pips\",pipsHorizontal:\"pips-horizontal\",pipsVertical:\"pips-vertical\",marker:\"marker\",markerHorizontal:\"marker-horizontal\",markerVertical:\"marker-vertical\",markerNormal:\"marker-normal\",markerLarge:\"marker-large\",markerSub:\"marker-sub\",value:\"value\",valueHorizontal:\"value-horizontal\",valueVertical:\"value-vertical\",valueNormal:\"value-normal\",valueLarge:\"value-large\",valueSub:\"value-sub\"},k=\".__tooltips\",L=\".__aria\";function x(e,t){if(!s(t))throw new Error(\"noUiSlider: 'step' is not numeric.\");e.singleStep=t}function T(e,t){if(!s(t))throw new Error(\"noUiSlider: 'keyboardPageMultiplier' is not numeric.\");e.keyboardPageMultiplier=t}function S(e,t){if(!s(t))throw new Error(\"noUiSlider: 'keyboardMultiplier' is not numeric.\");e.keyboardMultiplier=t}function D(e,t){if(!s(t))throw new Error(\"noUiSlider: 'keyboardDefaultStep' is not numeric.\");e.keyboardDefaultStep=t}function E(e,t){if(\"object\"!=typeof t||Array.isArray(t))throw new Error(\"noUiSlider: 'range' is not an object.\");if(void 0===t.min||void 0===t.max)throw new Error(\"noUiSlider: Missing 'min' or 'max' in 'range'.\");e.spectrum=new b(t,e.snap||!1,e.singleStep)}function Y(e,t){if(t=d(t),!Array.isArray(t)||!t.length)throw new Error(\"noUiSlider: 'start' option is incorrect.\");e.handles=t.length,e.start=t}function A(e,t){if(\"boolean\"!=typeof t)throw new Error(\"noUiSlider: 'snap' option must be a boolean.\");e.snap=t}function O(e,t){if(\"boolean\"!=typeof t)throw new Error(\"noUiSlider: 'animate' option must be a boolean.\");e.animate=t}function C(e,t){if(\"number\"!=typeof t)throw new Error(\"noUiSlider: 'animationDuration' option must be a number.\");e.animationDuration=t}function j(e,t){var n,r=[!1];if(\"lower\"===t?t=[!0,!1]:\"upper\"===t&&(t=[!1,!0]),!0===t||!1===t){for(n=1;n<e.handles;n++)r.push(t);r.push(!1)}else{if(!Array.isArray(t)||!t.length||t.length!==e.handles+1)throw new Error(\"noUiSlider: 'connect' option doesn't match handle count.\");r=t}e.connect=r}function P(e,t){switch(t){case\"horizontal\":e.ort=0;break;case\"vertical\":e.ort=1;break;default:throw new Error(\"noUiSlider: 'orientation' option is invalid.\")}}function H(e,t){if(!s(t))throw new Error(\"noUiSlider: 'margin' option must be numeric.\");0!==t&&(e.margin=e.spectrum.getDistance(t))}function I(e,t){if(!s(t))throw new Error(\"noUiSlider: 'limit' option must be numeric.\");if(e.limit=e.spectrum.getDistance(t),!e.limit||e.handles<2)throw new Error(\"noUiSlider: 'limit' option is only supported on linear sliders with 2 or more handles.\")}function F(e,t){var n;if(!s(t)&&!Array.isArray(t))throw new Error(\"noUiSlider: 'padding' option must be numeric or array of exactly 2 numbers.\");if(Array.isArray(t)&&2!==t.length&&!s(t[0])&&!s(t[1]))throw new Error(\"noUiSlider: 'padding' option must be numeric or array of exactly 2 numbers.\");if(0!==t){for(Array.isArray(t)||(t=[t,t]),e.padding=[e.spectrum.getDistance(t[0]),e.spectrum.getDistance(t[1])],n=0;n<e.spectrum.xNumSteps.length-1;n++)if(e.padding[0][n]<0||e.padding[1][n]<0)throw new Error(\"noUiSlider: 'padding' option must be a positive number(s).\");var r=t[0]+t[1],i=e.spectrum.xVal[0];if(r/(e.spectrum.xVal[e.spectrum.xVal.length-1]-i)>1)throw new Error(\"noUiSlider: 'padding' option must not exceed 100% of the range.\")}}function N(e,t){switch(t){case\"ltr\":e.dir=0;break;case\"rtl\":e.dir=1;break;default:throw new Error(\"noUiSlider: 'direction' option was not recognized.\")}}function R(e,t){if(\"string\"!=typeof t)throw new Error(\"noUiSlider: 'behaviour' must be a string containing options.\");var n=t.indexOf(\"tap\")>=0,r=t.indexOf(\"drag\")>=0,i=t.indexOf(\"fixed\")>=0,a=t.indexOf(\"snap\")>=0,o=t.indexOf(\"hover\")>=0,s=t.indexOf(\"unconstrained\")>=0,l=t.indexOf(\"invert-connects\")>=0,u=t.indexOf(\"drag-all\")>=0,d=t.indexOf(\"smooth-steps\")>=0;if(i){if(2!==e.handles)throw new Error(\"noUiSlider: 'fixed' behaviour must be used with 2 handles\");H(e,e.start[1]-e.start[0])}if(l&&2!==e.handles)throw new Error(\"noUiSlider: 'invert-connects' behaviour must be used with 2 handles\");if(s&&(e.margin||e.limit))throw new Error(\"noUiSlider: 'unconstrained' behaviour cannot be used with margin or limit\");e.events={tap:n||a,drag:r,dragAll:u,smoothSteps:d,fixed:i,snap:a,hover:o,unconstrained:s,invertConnects:l}}function V(e,t){if(!1!==t)if(!0===t||r(t)){e.tooltips=[];for(var n=0;n<e.handles;n++)e.tooltips.push(t)}else{if((t=d(t)).length!==e.handles)throw new Error(\"noUiSlider: must pass a formatter for all handles.\");t.forEach(function(e){if(\"boolean\"!=typeof e&&!r(e))throw new Error(\"noUiSlider: 'tooltips' must be passed a formatter or 'false'.\")}),e.tooltips=t}}function $(e,t){if(t.length!==e.handles)throw new Error(\"noUiSlider: must pass a attributes for all handles.\");e.handleAttributes=t}function z(e,t){if(!r(t))throw new Error(\"noUiSlider: 'ariaFormat' requires 'to' method.\");e.ariaFormat=t}function W(e,t){if(!function(e){return r(e)&&\"function\"==typeof e.from}(t))throw new Error(\"noUiSlider: 'format' requires 'to' and 'from' methods.\");e.format=t}function U(e,t){if(\"boolean\"!=typeof t)throw new Error(\"noUiSlider: 'keyboardSupport' option must be a boolean.\");e.keyboardSupport=t}function B(e,t){e.documentElement=t}function q(e,t){if(\"string\"!=typeof t&&!1!==t)throw new Error(\"noUiSlider: 'cssPrefix' must be a string or `false`.\");e.cssPrefix=t}function G(e,t){if(\"object\"!=typeof t)throw new Error(\"noUiSlider: 'cssClasses' must be an object.\");\"string\"==typeof e.cssPrefix?(e.cssClasses={},Object.keys(t).forEach(function(n){e.cssClasses[n]=e.cssPrefix+t[n]})):e.cssClasses=t}function J(e){var t={margin:null,limit:null,padding:null,animate:!0,animationDuration:300,ariaFormat:M,format:M},n={step:{r:!1,t:x},keyboardPageMultiplier:{r:!1,t:T},keyboardMultiplier:{r:!1,t:S},keyboardDefaultStep:{r:!1,t:D},start:{r:!0,t:Y},connect:{r:!0,t:j},direction:{r:!0,t:N},snap:{r:!1,t:A},animate:{r:!1,t:O},animationDuration:{r:!1,t:C},range:{r:!0,t:E},orientation:{r:!1,t:P},margin:{r:!1,t:H},limit:{r:!1,t:I},padding:{r:!1,t:F},behaviour:{r:!0,t:R},ariaFormat:{r:!1,t:z},format:{r:!1,t:W},tooltips:{r:!1,t:V},keyboardSupport:{r:!0,t:U},documentElement:{r:!1,t:B},cssPrefix:{r:!0,t:q},cssClasses:{r:!0,t:G},handleAttributes:{r:!1,t:$}},r={connect:!1,direction:\"ltr\",behaviour:\"tap\",orientation:\"horizontal\",keyboardSupport:!0,cssPrefix:\"noUi-\",cssClasses:w,keyboardPageMultiplier:5,keyboardMultiplier:1,keyboardDefaultStep:10};e.format&&!e.ariaFormat&&(e.ariaFormat=e.format),Object.keys(n).forEach(function(i){if(a(e[i])||void 0!==r[i])n[i].t(t,a(e[i])?e[i]:r[i]);else if(n[i].r)throw new Error(\"noUiSlider: '\"+i+\"' is required.\")}),t.pips=e.pips;var i=document.createElement(\"div\"),o=void 0!==i.style.msTransform,s=void 0!==i.style.transform;t.transformRule=s?\"transform\":o?\"msTransform\":\"webkitTransform\";return t.style=[[\"left\",\"top\"],[\"right\",\"bottom\"]][t.dir][t.ort],t}function Z(t,n,r){var s,c,m,v,g,_,y,b=window.navigator.pointerEnabled?{start:\"pointerdown\",move:\"pointermove\",end:\"pointerup\"}:window.navigator.msPointerEnabled?{start:\"MSPointerDown\",move:\"MSPointerMove\",end:\"MSPointerUp\"}:{start:\"mousedown touchstart\",move:\"mousemove touchmove\",end:\"mouseup touchend\"},M=window.CSS&&CSS.supports&&CSS.supports(\"touch-action\",\"none\")&&function(){var e=!1;try{var t=Object.defineProperty({},\"passive\",{get:function(){e=!0}});window.addEventListener(\"test\",null,t)}catch(e){}return e}(),w=t,x=n.spectrum,T=[],S=[],D=[],E=0,Y={},A=!1,O=t.ownerDocument,C=n.documentElement||O.documentElement,P=O.body,H=\"rtl\"===O.dir||1===n.ort?0:100;function I(e,t){var n=O.createElement(\"div\");return t&&f(n,t),e.appendChild(n),n}function F(e,t){var r=I(e,n.cssClasses.origin),i=I(r,n.cssClasses.handle);if(I(i,n.cssClasses.touchArea),i.setAttribute(\"data-handle\",String(t)),n.keyboardSupport&&(i.setAttribute(\"tabindex\",\"0\"),i.addEventListener(\"keydown\",function(e){return function(e,t){if(V()||$(t))return!1;var r=[\"Left\",\"Right\"],i=[\"Down\",\"Up\"],a=[\"PageDown\",\"PageUp\"],o=[\"Home\",\"End\"];n.dir&&!n.ort?r.reverse():n.ort&&!n.dir&&(i.reverse(),a.reverse());var s,l=e.key.replace(\"Arrow\",\"\"),u=l===a[0],d=l===a[1],c=l===i[0]||l===r[0]||u,f=l===i[1]||l===r[1]||d,p=l===o[0],h=l===o[1];if(!(c||f||p||h))return!0;if(e.preventDefault(),f||c){var m=c?0:1,v=Me(t)[m];if(null===v)return!1;!1===v&&(v=x.getDefaultStep(S[t],c,n.keyboardDefaultStep)),v*=d||u?n.keyboardPageMultiplier:n.keyboardMultiplier,v=Math.max(v,1e-7),v*=c?-1:1,s=T[t]+v}else s=h?n.spectrum.xVal[n.spectrum.xVal.length-1]:n.spectrum.xVal[0];return ve(t,x.toStepping(s),!0,!0),ue(\"slide\",t),ue(\"update\",t),ue(\"change\",t),ue(\"set\",t),!1}(e,t)})),void 0!==n.handleAttributes){var a=n.handleAttributes[t];Object.keys(a).forEach(function(e){i.setAttribute(e,a[e])})}return i.setAttribute(\"role\",\"slider\"),i.setAttribute(\"aria-orientation\",n.ort?\"vertical\":\"horizontal\"),0===t?f(i,n.cssClasses.handleLower):t===n.handles-1&&f(i,n.cssClasses.handleUpper),r.handle=i,r}function N(e,t){return!!t&&I(e,n.cssClasses.connect)}function R(e,t){return!(!n.tooltips||!n.tooltips[t])&&I(e.firstChild,n.cssClasses.tooltip)}function V(){return w.hasAttribute(\"disabled\")}function $(e){return m[e].hasAttribute(\"disabled\")}function z(){_&&(le(\"update\"+k),_.forEach(function(e){e&&i(e)}),_=null)}function W(){z(),_=m.map(R),se(\"update\"+k,function(e,t,r){if(_&&n.tooltips&&!1!==_[t]){var i=e[t];!0!==n.tooltips[t]&&(i=n.tooltips[t].to(r[t])),_[t].innerHTML=i}})}function U(e,t){return e.map(function(e){return x.fromStepping(t?x.getStep(e):e)})}function B(t){function n(e,t){return Number((e+t).toFixed(7))}var r,i=function(t){if(t.mode===e.PipsMode.Range||t.mode===e.PipsMode.Steps)return x.xVal;if(t.mode===e.PipsMode.Count){if(t.values<2)throw new Error(\"noUiSlider: 'values' (>= 2) required for mode 'count'.\");for(var n=t.values-1,r=100/n,i=[];n--;)i[n]=n*r;return i.push(100),U(i,t.stepped)}return t.mode===e.PipsMode.Positions?U(t.values,t.stepped):t.mode===e.PipsMode.Values?t.stepped?t.values.map(function(e){return x.fromStepping(x.getStep(x.toStepping(e)))}):t.values:[]}(t),a={},o=x.xVal[0],s=x.xVal[x.xVal.length-1],l=!1,u=!1,d=0;return r=i.slice().sort(function(e,t){return e-t}),(i=r.filter(function(e){return!this[e]&&(this[e]=!0)},{}))[0]!==o&&(i.unshift(o),l=!0),i[i.length-1]!==s&&(i.push(s),u=!0),i.forEach(function(r,o){var s,c,f,p,h,m,v,g,_,y,b=r,M=i[o+1],w=t.mode===e.PipsMode.Steps;for(w&&(s=x.xNumSteps[o]),s||(s=M-b),void 0===M&&(M=b),s=Math.max(s,1e-7),c=b;c<=M;c=n(c,s)){for(g=(h=(p=x.toStepping(c))-d)/(t.density||1),y=h/(_=Math.round(g)),f=1;f<=_;f+=1)a[(m=d+f*y).toFixed(5)]=[x.fromStepping(m),0];v=i.indexOf(c)>-1?e.PipsType.LargeValue:w?e.PipsType.SmallValue:e.PipsType.NoValue,!o&&l&&c!==M&&(v=0),c===M&&u||(a[p.toFixed(5)]=[c,v]),d=p}}),a}function q(t,r,i){var a,o,s=O.createElement(\"div\"),l=((a={})[e.PipsType.None]=\"\",a[e.PipsType.NoValue]=n.cssClasses.valueNormal,a[e.PipsType.LargeValue]=n.cssClasses.valueLarge,a[e.PipsType.SmallValue]=n.cssClasses.valueSub,a),u=((o={})[e.PipsType.None]=\"\",o[e.PipsType.NoValue]=n.cssClasses.markerNormal,o[e.PipsType.LargeValue]=n.cssClasses.markerLarge,o[e.PipsType.SmallValue]=n.cssClasses.markerSub,o),d=[n.cssClasses.valueHorizontal,n.cssClasses.valueVertical],c=[n.cssClasses.markerHorizontal,n.cssClasses.markerVertical];function p(e,t){var r=t===n.cssClasses.value,i=r?l:u;return t+\" \"+(r?d:c)[n.ort]+\" \"+i[e]}return f(s,n.cssClasses.pips),f(s,0===n.ort?n.cssClasses.pipsHorizontal:n.cssClasses.pipsVertical),Object.keys(t).forEach(function(a){!function(t,a,o){if((o=r?r(a,o):o)!==e.PipsType.None){var l=I(s,!1);l.className=p(o,n.cssClasses.marker),l.style[n.style]=t+\"%\",o>e.PipsType.NoValue&&((l=I(s,!1)).className=p(o,n.cssClasses.value),l.setAttribute(\"data-value\",String(a)),l.style[n.style]=t+\"%\",l.innerHTML=String(i.to(a)))}}(a,t[a][0],t[a][1])}),s}function G(){g&&(i(g),g=null)}function Z(e){G();var t=B(e),n=e.filter,r=e.format||{to:function(e){return String(Math.round(e))}};return g=w.appendChild(q(t,n,r))}function K(){var e=s.getBoundingClientRect(),t=\"offset\"+[\"Width\",\"Height\"][n.ort];return 0===n.ort?e.width||s[t]:e.height||s[t]}function X(e,t,r,i){var a=function(a){var o,s,l=function(e,t,n){var r=0===e.type.indexOf(\"touch\"),i=0===e.type.indexOf(\"mouse\"),a=0===e.type.indexOf(\"pointer\"),o=0,s=0;0===e.type.indexOf(\"MSPointer\")&&(a=!0);if(\"mousedown\"===e.type&&!e.buttons&&!e.touches)return!1;if(r){var l=function(t){var r=t.target;return r===n||n.contains(r)||e.composed&&e.composedPath().shift()===n};if(\"touchstart\"===e.type){var u=Array.prototype.filter.call(e.touches,l);if(u.length>1)return!1;o=u[0].pageX,s=u[0].pageY}else{var d=Array.prototype.find.call(e.changedTouches,l);if(!d)return!1;o=d.pageX,s=d.pageY}}t=t||h(O),(i||a)&&(o=e.clientX+t.x,s=e.clientY+t.y);return e.pageOffset=t,e.points=[o,s],e.cursor=i||a,e}(a,i.pageOffset,i.target||t);return!!l&&(!(V()&&!i.doNotReject)&&(o=w,s=n.cssClasses.tap,!((o.classList?o.classList.contains(s):new RegExp(\"\\\\b\"+s+\"\\\\b\").test(o.className))&&!i.doNotReject)&&(!(e===b.start&&void 0!==l.buttons&&l.buttons>1)&&((!i.hover||!l.buttons)&&(M||l.preventDefault(),l.calcPoint=l.points[n.ort],void r(l,i))))))},o=[];return e.split(\" \").forEach(function(e){t.addEventListener(e,a,!!M&&{passive:!0}),o.push([e,a])}),o}function Q(e){var t,r,i,a,o,l,d=100*(e-(t=s,r=n.ort,i=t.getBoundingClientRect(),a=t.ownerDocument,o=a.documentElement,l=h(a),/webkit.*Chrome.*Mobile/i.test(navigator.userAgent)&&(l.x=0),r?i.top+l.y-o.clientTop:i.left+l.x-o.clientLeft))/K();return d=u(d),n.dir?100-d:d}function ee(e,t){\"mouseout\"===e.type&&\"HTML\"===e.target.nodeName&&null===e.relatedTarget&&ne(e,t)}function te(e,t){if(-1===navigator.appVersion.indexOf(\"MSIE 9\")&&0===e.buttons&&0!==t.buttonsProperty)return ne(e,t);var r=(n.dir?-1:1)*(e.calcPoint-t.startCalcPoint);fe(r>0,100*r/t.baseSize,t.locations,t.handleNumbers,t.connect)}function ne(e,t){t.handle&&(p(t.handle,n.cssClasses.active),E-=1),t.listeners.forEach(function(e){C.removeEventListener(e[0],e[1])}),0===E&&(p(w,n.cssClasses.drag),me(),e.cursor&&(P.style.cursor=\"\",P.removeEventListener(\"selectstart\",o))),n.events.smoothSteps&&(t.handleNumbers.forEach(function(e){ve(e,S[e],!0,!0,!1,!1)}),t.handleNumbers.forEach(function(e){ue(\"update\",e)})),t.handleNumbers.forEach(function(e){ue(\"change\",e),ue(\"set\",e),ue(\"end\",e)})}function re(e,t){if(!t.handleNumbers.some($)){var r;if(1===t.handleNumbers.length)r=m[t.handleNumbers[0]].children[0],E+=1,f(r,n.cssClasses.active);e.stopPropagation();var i=[],a=X(b.move,C,te,{target:e.target,handle:r,connect:t.connect,listeners:i,startCalcPoint:e.calcPoint,baseSize:K(),pageOffset:e.pageOffset,handleNumbers:t.handleNumbers,buttonsProperty:e.buttons,locations:S.slice()}),s=X(b.end,C,ne,{target:e.target,handle:r,listeners:i,doNotReject:!0,handleNumbers:t.handleNumbers}),l=X(\"mouseout\",C,ee,{target:e.target,handle:r,listeners:i,doNotReject:!0,handleNumbers:t.handleNumbers});i.push.apply(i,a.concat(s,l)),e.cursor&&(P.style.cursor=getComputedStyle(e.target).cursor,m.length>1&&f(w,n.cssClasses.drag),P.addEventListener(\"selectstart\",o,!1)),t.handleNumbers.forEach(function(e){ue(\"start\",e)})}}function ie(e){e.stopPropagation();var t=Q(e.calcPoint),r=function(e){var t=100,n=!1;return m.forEach(function(r,i){if(!$(i)){var a=S[i],o=Math.abs(a-e);(o<t||o<=t&&e>a||100===o&&100===t)&&(n=i,t=o)}}),n}(t);!1!==r&&(n.events.snap||l(w,n.cssClasses.tap,n.animationDuration),ve(r,t,!0,!0),me(),ue(\"slide\",r,!0),ue(\"update\",r,!0),n.events.snap?re(e,{handleNumbers:[r]}):(ue(\"change\",r,!0),ue(\"set\",r,!0)))}function ae(e){var t=Q(e.calcPoint),n=x.getStep(t),r=x.fromStepping(n);Object.keys(Y).forEach(function(e){\"hover\"===e.split(\".\")[0]&&Y[e].forEach(function(e){e.call(ke,r)})})}function oe(e){e.fixed||m.forEach(function(e,t){X(b.start,e.children[0],re,{handleNumbers:[t]})}),e.tap&&X(b.start,s,ie,{}),e.hover&&X(b.move,s,ae,{hover:!0}),e.drag&&v.forEach(function(t,r){if(!1!==t&&0!==r&&r!==v.length-1){var i=m[r-1],a=m[r],o=[t],s=[i,a],l=[r-1,r];f(t,n.cssClasses.draggable),e.fixed&&(o.push(i.children[0]),o.push(a.children[0])),e.dragAll&&(s=m,l=D),o.forEach(function(e){X(b.start,e,re,{handles:s,handleNumbers:l,connect:t})})}})}function se(e,t){Y[e]=Y[e]||[],Y[e].push(t),\"update\"===e.split(\".\")[0]&&m.forEach(function(e,t){ue(\"update\",t)})}function le(e){var t=e&&e.split(\".\")[0],n=t?e.substring(t.length):e;Object.keys(Y).forEach(function(e){var r=e.split(\".\")[0],i=e.substring(r.length);t&&t!==r||n&&n!==i||function(e){return e===L||e===k}(i)&&n!==i||delete Y[e]})}function ue(e,t,r){Object.keys(Y).forEach(function(i){var a=i.split(\".\")[0];e===a&&Y[i].forEach(function(e){e.call(ke,T.map(n.format.to),t,T.slice(),r||!1,S.slice(),ke)})})}function de(e,t,r,i,a,o,s){var l;return m.length>1&&!n.events.unconstrained&&(i&&t>0&&(l=x.getAbsoluteDistance(e[t-1],n.margin,!1),r=Math.max(r,l)),a&&t<m.length-1&&(l=x.getAbsoluteDistance(e[t+1],n.margin,!0),r=Math.min(r,l))),m.length>1&&n.limit&&(i&&t>0&&(l=x.getAbsoluteDistance(e[t-1],n.limit,!1),r=Math.min(r,l)),a&&t<m.length-1&&(l=x.getAbsoluteDistance(e[t+1],n.limit,!0),r=Math.max(r,l))),n.padding&&(0===t&&(l=x.getAbsoluteDistance(0,n.padding[0],!1),r=Math.max(r,l)),t===m.length-1&&(l=x.getAbsoluteDistance(100,n.padding[1],!0),r=Math.min(r,l))),s||(r=x.getStep(r)),!((r=u(r))===e[t]&&!o)&&r}function ce(e,t){var r=n.ort;return(r?t:e)+\", \"+(r?e:t)}function fe(e,t,r,i,a){var o=r.slice(),s=i[0],l=n.events.smoothSteps,u=[!e,e],d=[e,!e];i=i.slice(),e&&i.reverse(),i.length>1?i.forEach(function(e,n){var r=de(o,e,o[e]+t,u[n],d[n],!1,l);!1===r?t=0:(t=r-o[e],o[e]=r)}):u=d=[!0];var c=!1;i.forEach(function(e,n){c=ve(e,r[e]+t,u[n],d[n],!1,l)||c}),c&&(i.forEach(function(e){ue(\"update\",e),ue(\"slide\",e)}),null!=a&&ue(\"drag\",s))}function pe(e,t){return n.dir?100-e-t:e}function he(e,t){S[e]=t,T[e]=x.fromStepping(t);var r=\"translate(\"+ce(pe(t,0)-H+\"%\",\"0\")+\")\";if(m[e].style[n.transformRule]=r,n.events.invertConnects&&S.length>1){var i=S.every(function(e,t,n){return 0===t||e>=n[t-1]});if(A!==!i)return A=!A,j(n,n.connect.map(function(e){return!e})),void we()}ge(e),ge(e+1),A&&(ge(e-1),ge(e+2))}function me(){D.forEach(function(e){var t=S[e]>50?-1:1,n=3+(m.length+t*e);m[e].style.zIndex=String(n)})}function ve(e,t,n,r,i,a){return i||(t=de(S,e,t,n,r,!1,a)),!1!==t&&(he(e,t),!0)}function ge(e){if(v[e]){var t=S.slice();A&&t.sort(function(e,t){return e-t});var r=0,i=100;0!==e&&(r=t[e-1]),e!==v.length-1&&(i=t[e]);var a=i-r,o=\"translate(\"+ce(pe(r,a)+\"%\",\"0\")+\")\",s=\"scale(\"+ce(a/100,\"1\")+\")\";v[e].style[n.transformRule]=o+\" \"+s}}function _e(e,t){return null===e||!1===e||void 0===e?S[t]:(\"number\"==typeof e&&(e=String(e)),!1!==(e=n.format.from(e))&&(e=x.toStepping(e)),!1===e||isNaN(e)?S[t]:e)}function ye(e,t,r){var i=d(e),a=void 0===S[0];t=void 0===t||t,n.animate&&!a&&l(w,n.cssClasses.tap,n.animationDuration),D.forEach(function(e){ve(e,_e(i[e],e),!0,!1,r)});var o=1===D.length?0:1;if(a&&x.hasNoSize()&&(r=!0,S[0]=0,D.length>1)){var s=100/(D.length-1);D.forEach(function(e){S[e]=e*s})}for(;o<D.length;++o)D.forEach(function(e){ve(e,S[e],!0,!0,r)});me(),D.forEach(function(e){ue(\"update\",e),null!==i[e]&&t&&ue(\"set\",e)})}function be(e){if(void 0===e&&(e=!1),e)return 1===T.length?T[0]:T.slice(0);var t=T.map(n.format.to);return 1===t.length?t[0]:t}function Me(e){var t=S[e],r=x.getNearbySteps(t),i=T[e],a=r.thisStep.step,o=null;if(n.snap)return[i-r.stepBefore.startValue||null,r.stepAfter.startValue-i||null];!1!==a&&i+a>r.stepAfter.startValue&&(a=r.stepAfter.startValue-i),o=i>r.thisStep.startValue?r.thisStep.step:!1!==r.stepBefore.step&&i-r.stepBefore.highestStep,100===t?a=null:0===t&&(o=null);var s=x.countStepDecimals();return null!==a&&!1!==a&&(a=Number(a.toFixed(s))),null!==o&&!1!==o&&(o=Number(o.toFixed(s))),[o,a]}function we(){for(;c.firstChild;)c.removeChild(c.firstChild);for(var e=0;e<=n.handles;e++)v[e]=N(c,n.connect[e]),ge(e);oe({drag:n.events.drag,fixed:!0})}f(y=w,n.cssClasses.target),0===n.dir?f(y,n.cssClasses.ltr):f(y,n.cssClasses.rtl),0===n.ort?f(y,n.cssClasses.horizontal):f(y,n.cssClasses.vertical),f(y,\"rtl\"===getComputedStyle(y).direction?n.cssClasses.textDirectionRtl:n.cssClasses.textDirectionLtr),s=I(y,n.cssClasses.base),function(e,t){c=I(t,n.cssClasses.connects),m=[],(v=[]).push(N(c,e[0]));for(var r=0;r<n.handles;r++)m.push(F(t,r)),D[r]=r,v.push(N(c,e[r+1]))}(n.connect,s),oe(n.events),ye(n.start),n.pips&&Z(n.pips),n.tooltips&&W(),le(\"update\"+L),se(\"update\"+L,function(e,t,r,i,a){D.forEach(function(e){var t=m[e],i=de(S,e,0,!0,!0,!0),o=de(S,e,100,!0,!0,!0),s=a[e],l=String(n.ariaFormat.to(r[e]));i=x.fromStepping(i).toFixed(1),o=x.fromStepping(o).toFixed(1),s=x.fromStepping(s).toFixed(1),t.children[0].setAttribute(\"aria-valuemin\",i),t.children[0].setAttribute(\"aria-valuemax\",o),t.children[0].setAttribute(\"aria-valuenow\",s),t.children[0].setAttribute(\"aria-valuetext\",l)})});var ke={destroy:function(){for(le(L),le(k),Object.keys(n.cssClasses).forEach(function(e){p(w,n.cssClasses[e])});w.firstChild;)w.removeChild(w.firstChild);delete w.noUiSlider},steps:function(){return D.map(Me)},on:se,off:le,get:be,set:ye,setHandle:function(e,t,n,r){if(!((e=Number(e))>=0&&e<D.length))throw new Error(\"noUiSlider: invalid handle number, got: \"+e);ve(e,_e(t,e),!0,!0,r),ue(\"update\",e),n&&ue(\"set\",e)},reset:function(e){ye(n.start,e)},disable:function(e){null!=e?(m[e].setAttribute(\"disabled\",\"\"),m[e].handle.removeAttribute(\"tabindex\")):(w.setAttribute(\"disabled\",\"\"),m.forEach(function(e){e.handle.removeAttribute(\"tabindex\")}))},enable:function(e){null!=e?(m[e].removeAttribute(\"disabled\"),m[e].handle.setAttribute(\"tabindex\",\"0\")):(w.removeAttribute(\"disabled\"),m.forEach(function(e){e.removeAttribute(\"disabled\"),e.handle.setAttribute(\"tabindex\",\"0\")}))},__moveHandles:function(e,t,n){fe(e,t,S,n)},options:r,updateOptions:function(e,t){var i=be(),o=[\"margin\",\"limit\",\"padding\",\"range\",\"animate\",\"snap\",\"step\",\"format\",\"pips\",\"tooltips\",\"connect\"];o.forEach(function(t){void 0!==e[t]&&(r[t]=e[t])});var s=J(r);o.forEach(function(t){void 0!==e[t]&&(n[t]=s[t])}),x=s.spectrum,n.margin=s.margin,n.limit=s.limit,n.padding=s.padding,n.pips?Z(n.pips):G(),n.tooltips?W():z(),S=[],ye(a(e.start)?e.start:i,t),e.connect&&we()},target:w,removePips:G,removeTooltips:z,getPositions:function(){return S.slice()},getTooltips:function(){return _},getOrigins:function(){return m},pips:Z};return ke}function K(e,t){if(!e||!e.nodeName)throw new Error(\"noUiSlider: create requires a single element, got: \"+e);if(e.noUiSlider)throw new Error(\"noUiSlider: Slider was already initialized.\");var n=Z(e,J(t),t);return e.noUiSlider=n,n}var X={__spectrum:b,cssClasses:w,create:K};e.create=K,e.cssClasses=w,e.default=X,Object.defineProperty(e,\"__esModule\",{value:!0})}),function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(t):(e=e||self).autosize=t()}(this,function(){var e=new Map;function t(t){if(t&&t.nodeName&&\"TEXTAREA\"===t.nodeName&&!e.has(t)){var n,r=null,i=window.getComputedStyle(t),a=(n=t.value,function(){s({testForHeightReduction:\"\"===n||!t.value.startsWith(n),restoreTextAlign:null}),n=t.value}),o=function(n){t.removeEventListener(\"autosize:destroy\",o),t.removeEventListener(\"autosize:update\",l),t.removeEventListener(\"input\",a),window.removeEventListener(\"resize\",l),Object.keys(n).forEach(function(e){return t.style[e]=n[e]}),e.delete(t)}.bind(t,{height:t.style.height,resize:t.style.resize,textAlign:t.style.textAlign,overflowY:t.style.overflowY,overflowX:t.style.overflowX,wordWrap:t.style.wordWrap});t.addEventListener(\"autosize:destroy\",o),t.addEventListener(\"autosize:update\",l),t.addEventListener(\"input\",a),window.addEventListener(\"resize\",l),t.style.overflowX=\"hidden\",t.style.wordWrap=\"break-word\",e.set(t,{destroy:o,update:l}),l()}function s(e){var n,a,o=e.restoreTextAlign,l=void 0===o?null:o,u=e.testForHeightReduction,d=void 0===u||u,c=i.overflowY;if(0!==t.scrollHeight&&(\"vertical\"===i.resize?t.style.resize=\"none\":\"both\"===i.resize&&(t.style.resize=\"horizontal\"),d&&(n=function(e){for(var t=[];e&&e.parentNode&&e.parentNode instanceof Element;)e.parentNode.scrollTop&&t.push([e.parentNode,e.parentNode.scrollTop]),e=e.parentNode;return function(){return t.forEach(function(e){var t=e[0],n=e[1];t.style.scrollBehavior=\"auto\",t.scrollTop=n,t.style.scrollBehavior=null})}}(t),t.style.height=\"\"),a=\"content-box\"===i.boxSizing?t.scrollHeight-(parseFloat(i.paddingTop)+parseFloat(i.paddingBottom)):t.scrollHeight+parseFloat(i.borderTopWidth)+parseFloat(i.borderBottomWidth),\"none\"!==i.maxHeight&&a>parseFloat(i.maxHeight)?(\"hidden\"===i.overflowY&&(t.style.overflow=\"scroll\"),a=parseFloat(i.maxHeight)):\"hidden\"!==i.overflowY&&(t.style.overflow=\"hidden\"),t.style.height=a+\"px\",l&&(t.style.textAlign=l),n&&n(),r!==a&&(t.dispatchEvent(new Event(\"autosize:resized\",{bubbles:!0})),r=a),c!==i.overflow&&!l)){var f=i.textAlign;\"hidden\"===i.overflow&&(t.style.textAlign=\"start\"===f?\"end\":\"start\"),s({restoreTextAlign:f,testForHeightReduction:!0})}}function l(){s({testForHeightReduction:!0,restoreTextAlign:null})}}function n(t){var n=e.get(t);n&&n.destroy()}function r(t){var n=e.get(t);n&&n.update()}var i=null;return\"undefined\"==typeof window?((i=function(e){return e}).destroy=function(e){return e},i.update=function(e){return e}):((i=function(e,n){return e&&Array.prototype.forEach.call(e.length?e:[e],function(e){return t(e)}),e}).destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],n),e},i.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],r),e}),i}),function(e,t){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define([],t):\"object\"==typeof exports?exports.ClipboardJS=t():e.ClipboardJS=t()}(this,function(){return t={686:function(e,t,n){\"use strict\";n.d(t,{default:function(){return v}});t=n(279);var r=n.n(t),i=(t=n(370),n.n(t)),a=(t=n(817),n.n(t));function o(e){try{return document.execCommand(e)}catch(e){return}}var s=function(e){return e=a()(e),o(\"cut\"),e};function l(e,t){var n,r;n=e,r=\"rtl\"===document.documentElement.getAttribute(\"dir\"),(e=document.createElement(\"textarea\")).style.fontSize=\"12pt\",e.style.border=\"0\",e.style.padding=\"0\",e.style.margin=\"0\",e.style.position=\"absolute\",e.style[r?\"right\":\"left\"]=\"-9999px\",r=window.pageYOffset||document.documentElement.scrollTop,e.style.top=\"\".concat(r,\"px\"),e.setAttribute(\"readonly\",\"\"),e.value=n;return t.container.appendChild(e),t=a()(e),o(\"copy\"),e.remove(),t}var u=function(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{container:document.body},n=\"\";return\"string\"==typeof e?n=l(e,t):e instanceof HTMLInputElement&&![\"text\",\"search\",\"url\",\"tel\",\"password\"].includes(null==e?void 0:e.type)?n=l(e.value,t):(n=a()(e),o(\"copy\")),n};function d(e){return(d=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e})(e)}function c(e){return(c=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e})(e)}function f(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\"value\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function p(e,t){return(p=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function h(e){return(h=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function m(e,t){if(e=\"data-clipboard-\".concat(e),t.hasAttribute(e))return t.getAttribute(e)}var v=function(){!function(e,t){if(\"function\"!=typeof t&&null!==t)throw new TypeError(\"Super expression must either be null or a function\");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&p(e,t)}(o,r());var e,t,n,a=function(e){var t=function(){if(\"undefined\"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if(\"function\"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}();return function(){var n,r=h(e);return n=t?(n=h(this).constructor,Reflect.construct(r,arguments,n)):r.apply(this,arguments),r=this,!n||\"object\"!==c(n)&&\"function\"!=typeof n?function(e){if(void 0!==e)return e;throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\")}(r):n}}(o);function o(e,t){var n;return function(e){if(!(e instanceof o))throw new TypeError(\"Cannot call a class as a function\")}(this),(n=a.call(this)).resolveOptions(t),n.listenClick(e),n}return e=o,n=[{key:\"copy\",value:function(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{container:document.body};return u(e,t)}},{key:\"cut\",value:function(e){return s(e)}},{key:\"isSupported\",value:function(){var e=\"string\"==typeof(e=0<arguments.length&&void 0!==arguments[0]?arguments[0]:[\"copy\",\"cut\"])?[e]:e,t=!!document.queryCommandSupported;return e.forEach(function(e){t=t&&!!document.queryCommandSupported(e)}),t}}],(t=[{key:\"resolveOptions\",value:function(){var e=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{};this.action=\"function\"==typeof e.action?e.action:this.defaultAction,this.target=\"function\"==typeof e.target?e.target:this.defaultTarget,this.text=\"function\"==typeof e.text?e.text:this.defaultText,this.container=\"object\"===c(e.container)?e.container:document.body}},{key:\"listenClick\",value:function(e){var t=this;this.listener=i()(e,\"click\",function(e){return t.onClick(e)})}},{key:\"onClick\",value:function(e){var t=e.delegateTarget||e.currentTarget,n=this.action(t)||\"copy\";e=function(){var e=void 0===(n=(r=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{}).action)?\"copy\":n,t=r.container,n=r.target,r=r.text;if(\"copy\"!==e&&\"cut\"!==e)throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');if(void 0!==n){if(!n||\"object\"!==d(n)||1!==n.nodeType)throw new Error('Invalid \"target\" value, use a valid Element');if(\"copy\"===e&&n.hasAttribute(\"disabled\"))throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');if(\"cut\"===e&&(n.hasAttribute(\"readonly\")||n.hasAttribute(\"disabled\")))throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes')}return r?u(r,{container:t}):n?\"cut\"===e?s(n):u(n,{container:t}):void 0}({action:n,container:this.container,target:this.target(t),text:this.text(t)});this.emit(e?\"success\":\"error\",{action:n,text:e,trigger:t,clearSelection:function(){t&&t.focus(),window.getSelection().removeAllRanges()}})}},{key:\"defaultAction\",value:function(e){return m(\"action\",e)}},{key:\"defaultTarget\",value:function(e){if(e=m(\"target\",e))return document.querySelector(e)}},{key:\"defaultText\",value:function(e){return m(\"text\",e)}},{key:\"destroy\",value:function(){this.listener.destroy()}}])&&f(e.prototype,t),n&&f(e,n),o}()},828:function(e){var t;\"undefined\"==typeof Element||Element.prototype.matches||((t=Element.prototype).matches=t.matchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector||t.webkitMatchesSelector),e.exports=function(e,t){for(;e&&9!==e.nodeType;){if(\"function\"==typeof e.matches&&e.matches(t))return e;e=e.parentNode}}},438:function(e,t,n){var r=n(828);function i(e,t,n,i,a){var o=function(e,t,n,i){return function(n){n.delegateTarget=r(n.target,t),n.delegateTarget&&i.call(e,n)}}.apply(this,arguments);return e.addEventListener(n,o,a),{destroy:function(){e.removeEventListener(n,o,a)}}}e.exports=function(e,t,n,r,a){return\"function\"==typeof e.addEventListener?i.apply(null,arguments):\"function\"==typeof n?i.bind(null,document).apply(null,arguments):(\"string\"==typeof e&&(e=document.querySelectorAll(e)),Array.prototype.map.call(e,function(e){return i(e,t,n,r,a)}))}},879:function(e,t){t.node=function(e){return void 0!==e&&e instanceof HTMLElement&&1===e.nodeType},t.nodeList=function(e){var n=Object.prototype.toString.call(e);return void 0!==e&&(\"[object NodeList]\"===n||\"[object HTMLCollection]\"===n)&&\"length\"in e&&(0===e.length||t.node(e[0]))},t.string=function(e){return\"string\"==typeof e||e instanceof String},t.fn=function(e){return\"[object Function]\"===Object.prototype.toString.call(e)}},370:function(e,t,n){var r=n(879),i=n(438);e.exports=function(e,t,n){if(!e&&!t&&!n)throw new Error(\"Missing required arguments\");if(!r.string(t))throw new TypeError(\"Second argument must be a String\");if(!r.fn(n))throw new TypeError(\"Third argument must be a Function\");if(r.node(e))return u=t,d=n,(l=e).addEventListener(u,d),{destroy:function(){l.removeEventListener(u,d)}};if(r.nodeList(e))return a=e,o=t,s=n,Array.prototype.forEach.call(a,function(e){e.addEventListener(o,s)}),{destroy:function(){Array.prototype.forEach.call(a,function(e){e.removeEventListener(o,s)})}};if(r.string(e))return i(document.body,e,t,n);throw new TypeError(\"First argument must be a String, HTMLElement, HTMLCollection, or NodeList\");var a,o,s,l,u,d}},817:function(e){e.exports=function(e){var t,n=\"SELECT\"===e.nodeName?(e.focus(),e.value):\"INPUT\"===e.nodeName||\"TEXTAREA\"===e.nodeName?((t=e.hasAttribute(\"readonly\"))||e.setAttribute(\"readonly\",\"\"),e.select(),e.setSelectionRange(0,e.value.length),t||e.removeAttribute(\"readonly\"),e.value):(e.hasAttribute(\"contenteditable\")&&e.focus(),n=window.getSelection(),(t=document.createRange()).selectNodeContents(e),n.removeAllRanges(),n.addRange(t),n.toString());return n}},279:function(e){function t(){}t.prototype={on:function(e,t,n){var r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this},once:function(e,t,n){var r=this;function i(){r.off(e,i),t.apply(n,arguments)}return i._=t,this.on(e,i,n)},emit:function(e){for(var t=[].slice.call(arguments,1),n=((this.e||(this.e={}))[e]||[]).slice(),r=0,i=n.length;r<i;r++)n[r].fn.apply(n[r].ctx,t);return this},off:function(e,t){var n=this.e||(this.e={}),r=n[e],i=[];if(r&&t)for(var a=0,o=r.length;a<o;a++)r[a].fn!==t&&r[a].fn._!==t&&i.push(r[a]);return i.length?n[e]=i:delete n[e],this}},e.exports=t,e.exports.TinyEmitter=t}},n={},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,{a:n}),n},e.d=function(t,n){for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},e.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},e(686).default;function e(r){if(n[r])return n[r].exports;var i=n[r]={exports:{}};return t[r](i,i.exports,e),i.exports}var t,n}),\n/*!\n * smooth-scroll v16.1.3\n * Animate scrolling to anchor links\n * (c) 2020 Chris Ferdinandi\n * MIT License\n * http://github.com/cferdinandi/smooth-scroll\n */\nfunction(e,t){\"function\"==typeof define&&define.amd?define([],function(){return t(e)}):\"object\"==typeof exports?module.exports=t(e):e.SmoothScroll=t(e)}(\"undefined\"!=typeof global?global:\"undefined\"!=typeof window?window:this,function(e){\"use strict\";var t={ignore:\"[data-scroll-ignore]\",header:null,topOnEmptyHash:!0,speed:500,speedAsDuration:!1,durationMax:null,durationMin:null,clip:!0,offset:0,easing:\"easeInOutCubic\",customEasing:null,updateURL:!0,popstate:!0,emitEvents:!0},n=function(){var e={};return Array.prototype.forEach.call(arguments,function(t){for(var n in t){if(!t.hasOwnProperty(n))return;e[n]=t[n]}}),e},r=function(e){\"#\"===e.charAt(0)&&(e=e.substr(1));for(var t,n=String(e),r=n.length,i=-1,a=\"\",o=n.charCodeAt(0);++i<r;){if(0===(t=n.charCodeAt(i)))throw new InvalidCharacterError(\"Invalid character: the input contains U+0000.\");t>=1&&t<=31||127==t||0===i&&t>=48&&t<=57||1===i&&t>=48&&t<=57&&45===o?a+=\"\\\\\"+t.toString(16)+\" \":a+=t>=128||45===t||95===t||t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122?n.charAt(i):\"\\\\\"+n.charAt(i)}return\"#\"+a},i=function(){return Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},a=function(t){return t?(n=t,parseInt(e.getComputedStyle(n).height,10)+t.offsetTop):0;var n},o=function(t,n,r){0===t&&document.body.focus(),r||(t.focus(),document.activeElement!==t&&(t.setAttribute(\"tabindex\",\"-1\"),t.focus(),t.style.outline=\"none\"),e.scrollTo(0,n))},s=function(t,n,r,i){if(n.emitEvents&&\"function\"==typeof e.CustomEvent){var a=new CustomEvent(t,{bubbles:!0,detail:{anchor:r,toggle:i}});document.dispatchEvent(a)}};return function(l,u){var d,c,f,p,h={};h.cancelScroll=function(e){cancelAnimationFrame(p),p=null,e||s(\"scrollCancel\",d)},h.animateScroll=function(r,l,u){h.cancelScroll();var c=n(d||t,u||{}),m=\"[object Number]\"===Object.prototype.toString.call(r),v=m||!r.tagName?null:r;if(m||v){var g=e.pageYOffset;c.header&&!f&&(f=document.querySelector(c.header));var _,y,b,M=a(f),w=m?r:function(t,n,r,a){var o=0;if(t.offsetParent)do{o+=t.offsetTop,t=t.offsetParent}while(t);return o=Math.max(o-n-r,0),a&&(o=Math.min(o,i()-e.innerHeight)),o}(v,M,parseInt(\"function\"==typeof c.offset?c.offset(r,l):c.offset,10),c.clip),k=w-g,L=i(),x=0,T=function(e,t){var n=t.speedAsDuration?t.speed:Math.abs(e/1e3*t.speed);return t.durationMax&&n>t.durationMax?t.durationMax:t.durationMin&&n<t.durationMin?t.durationMin:parseInt(n,10)}(k,c),S=function(t){_||(_=t),x+=t-_,b=g+k*function(e,t){var n;return\"easeInQuad\"===e.easing&&(n=t*t),\"easeOutQuad\"===e.easing&&(n=t*(2-t)),\"easeInOutQuad\"===e.easing&&(n=t<.5?2*t*t:(4-2*t)*t-1),\"easeInCubic\"===e.easing&&(n=t*t*t),\"easeOutCubic\"===e.easing&&(n=--t*t*t+1),\"easeInOutCubic\"===e.easing&&(n=t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1),\"easeInQuart\"===e.easing&&(n=t*t*t*t),\"easeOutQuart\"===e.easing&&(n=1- --t*t*t*t),\"easeInOutQuart\"===e.easing&&(n=t<.5?8*t*t*t*t:1-8*--t*t*t*t),\"easeInQuint\"===e.easing&&(n=t*t*t*t*t),\"easeOutQuint\"===e.easing&&(n=1+--t*t*t*t*t),\"easeInOutQuint\"===e.easing&&(n=t<.5?16*t*t*t*t*t:1+16*--t*t*t*t*t),e.customEasing&&(n=e.customEasing(t)),n||t}(c,y=(y=0===T?0:x/T)>1?1:y),e.scrollTo(0,Math.floor(b)),function(t,n){var i=e.pageYOffset;if(t==n||i==n||(g<n&&e.innerHeight+i)>=L)return h.cancelScroll(!0),o(r,n,m),s(\"scrollStop\",c,r,l),_=null,p=null,!0}(b,w)||(p=e.requestAnimationFrame(S),_=t)};0===e.pageYOffset&&e.scrollTo(0,0),function(e,t,n){t||history.pushState&&n.updateURL&&history.pushState({smoothScroll:JSON.stringify(n),anchor:e.id},document.title,e===document.documentElement?\"#top\":\"#\"+e.id)}(r,m,c),\"matchMedia\"in e&&e.matchMedia(\"(prefers-reduced-motion)\").matches?o(r,Math.floor(w),!1):(s(\"scrollStart\",c,r,l),h.cancelScroll(!0),e.requestAnimationFrame(S))}};var m=function(t){if(!t.defaultPrevented&&!(0!==t.button||t.metaKey||t.ctrlKey||t.shiftKey)&&\"closest\"in t.target&&(c=t.target.closest(l))&&\"a\"===c.tagName.toLowerCase()&&!t.target.closest(d.ignore)&&c.hostname===e.location.hostname&&c.pathname===e.location.pathname&&/#/.test(c.href)){var n,i;try{n=r(decodeURIComponent(c.hash))}catch(e){n=r(c.hash)}if(\"#\"===n){if(!d.topOnEmptyHash)return;i=document.documentElement}else i=document.querySelector(n);(i=i||\"#top\"!==n?i:document.documentElement)&&(t.preventDefault(),function(t){if(history.replaceState&&t.updateURL&&!history.state){var n=e.location.hash;n=n||\"\",history.replaceState({smoothScroll:JSON.stringify(t),anchor:n||e.pageYOffset},document.title,n||e.location.href)}}(d),h.animateScroll(i,c))}},v=function(e){if(null!==history.state&&history.state.smoothScroll&&history.state.smoothScroll===JSON.stringify(d)){var t=history.state.anchor;\"string\"==typeof t&&t&&!(t=document.querySelector(r(history.state.anchor)))||h.animateScroll(t,null,{updateURL:!1})}};h.destroy=function(){d&&(document.removeEventListener(\"click\",m,!1),e.removeEventListener(\"popstate\",v,!1),h.cancelScroll(),d=null,c=null,f=null,p=null)};return function(){if(!(\"querySelector\"in document&&\"addEventListener\"in e&&\"requestAnimationFrame\"in e&&\"closest\"in e.Element.prototype))throw\"Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.\";h.destroy(),d=n(t,u||{}),f=d.header?document.querySelector(d.header):null,document.addEventListener(\"click\",m,!1),d.updateURL&&d.popstate&&e.addEventListener(\"popstate\",v,!1)}(),h}}),function(e,t){if(\"object\"==typeof exports&&\"object\"==typeof module)module.exports=t();else if(\"function\"==typeof define&&define.amd)define([],t);else{var n=t();for(var r in n)(\"object\"==typeof exports?exports:e)[r]=n[r]}}(self,function(){return function(){var e={3099:function(e){e.exports=function(e){if(\"function\"!=typeof e)throw TypeError(String(e)+\" is not a function\");return e}},6077:function(e,t,n){var r=n(111);e.exports=function(e){if(!r(e)&&null!==e)throw TypeError(\"Can't set \"+String(e)+\" as a prototype\");return e}},1223:function(e,t,n){var r=n(5112),i=n(30),a=n(3070),o=r(\"unscopables\"),s=Array.prototype;null==s[o]&&a.f(s,o,{configurable:!0,value:i(null)}),e.exports=function(e){s[o][e]=!0}},1530:function(e,t,n){\"use strict\";var r=n(8710).charAt;e.exports=function(e,t,n){return t+(n?r(e,t).length:1)}},5787:function(e){e.exports=function(e,t,n){if(!(e instanceof t))throw TypeError(\"Incorrect \"+(n?n+\" \":\"\")+\"invocation\");return e}},9670:function(e,t,n){var r=n(111);e.exports=function(e){if(!r(e))throw TypeError(String(e)+\" is not an object\");return e}},4019:function(e){e.exports=\"undefined\"!=typeof ArrayBuffer&&\"undefined\"!=typeof DataView},260:function(e,t,n){\"use strict\";var r,i=n(4019),a=n(9781),o=n(7854),s=n(111),l=n(6656),u=n(648),d=n(8880),c=n(1320),f=n(3070).f,p=n(9518),h=n(7674),m=n(5112),v=n(9711),g=o.Int8Array,_=g&&g.prototype,y=o.Uint8ClampedArray,b=y&&y.prototype,M=g&&p(g),w=_&&p(_),k=Object.prototype,L=k.isPrototypeOf,x=m(\"toStringTag\"),T=v(\"TYPED_ARRAY_TAG\"),S=i&&!!h&&\"Opera\"!==u(o.opera),D=!1,E={Int8Array:1,Uint8Array:1,Uint8ClampedArray:1,Int16Array:2,Uint16Array:2,Int32Array:4,Uint32Array:4,Float32Array:4,Float64Array:8},Y={BigInt64Array:8,BigUint64Array:8},A=function(e){if(!s(e))return!1;var t=u(e);return l(E,t)||l(Y,t)};for(r in E)o[r]||(S=!1);if((!S||\"function\"!=typeof M||M===Function.prototype)&&(M=function(){throw TypeError(\"Incorrect invocation\")},S))for(r in E)o[r]&&h(o[r],M);if((!S||!w||w===k)&&(w=M.prototype,S))for(r in E)o[r]&&h(o[r].prototype,w);if(S&&p(b)!==w&&h(b,w),a&&!l(w,x))for(r in D=!0,f(w,x,{get:function(){return s(this)?this[T]:void 0}}),E)o[r]&&d(o[r],T,r);e.exports={NATIVE_ARRAY_BUFFER_VIEWS:S,TYPED_ARRAY_TAG:D&&T,aTypedArray:function(e){if(A(e))return e;throw TypeError(\"Target is not a typed array\")},aTypedArrayConstructor:function(e){if(h){if(L.call(M,e))return e}else for(var t in E)if(l(E,r)){var n=o[t];if(n&&(e===n||L.call(n,e)))return e}throw TypeError(\"Target is not a typed array constructor\")},exportTypedArrayMethod:function(e,t,n){if(a){if(n)for(var r in E){var i=o[r];i&&l(i.prototype,e)&&delete i.prototype[e]}w[e]&&!n||c(w,e,n?t:S&&_[e]||t)}},exportTypedArrayStaticMethod:function(e,t,n){var r,i;if(a){if(h){if(n)for(r in E)(i=o[r])&&l(i,e)&&delete i[e];if(M[e]&&!n)return;try{return c(M,e,n?t:S&&g[e]||t)}catch(e){}}for(r in E)!(i=o[r])||i[e]&&!n||c(i,e,t)}},isView:function(e){if(!s(e))return!1;var t=u(e);return\"DataView\"===t||l(E,t)||l(Y,t)},isTypedArray:A,TypedArray:M,TypedArrayPrototype:w}},3331:function(e,t,n){\"use strict\";var r=n(7854),i=n(9781),a=n(4019),o=n(8880),s=n(2248),l=n(7293),u=n(5787),d=n(9958),c=n(7466),f=n(7067),p=n(1179),h=n(9518),m=n(7674),v=n(8006).f,g=n(3070).f,_=n(1285),y=n(8003),b=n(9909),M=b.get,w=b.set,k=\"ArrayBuffer\",L=\"DataView\",x=\"prototype\",T=\"Wrong index\",S=r[k],D=S,E=r[L],Y=E&&E[x],A=Object.prototype,O=r.RangeError,C=p.pack,j=p.unpack,P=function(e){return[255&e]},H=function(e){return[255&e,e>>8&255]},I=function(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]},F=function(e){return e[3]<<24|e[2]<<16|e[1]<<8|e[0]},N=function(e){return C(e,23,4)},R=function(e){return C(e,52,8)},V=function(e,t){g(e[x],t,{get:function(){return M(this)[t]}})},$=function(e,t,n,r){var i=f(n),a=M(e);if(i+t>a.byteLength)throw O(T);var o=M(a.buffer).bytes,s=i+a.byteOffset,l=o.slice(s,s+t);return r?l:l.reverse()},z=function(e,t,n,r,i,a){var o=f(n),s=M(e);if(o+t>s.byteLength)throw O(T);for(var l=M(s.buffer).bytes,u=o+s.byteOffset,d=r(+i),c=0;c<t;c++)l[u+c]=d[a?c:t-c-1]};if(a){if(!l(function(){S(1)})||!l(function(){new S(-1)})||l(function(){return new S,new S(1.5),new S(NaN),S.name!=k})){for(var W,U=(D=function(e){return u(this,D),new S(f(e))})[x]=S[x],B=v(S),q=0;B.length>q;)(W=B[q++])in D||o(D,W,S[W]);U.constructor=D}m&&h(Y)!==A&&m(Y,A);var G=new E(new D(2)),J=Y.setInt8;G.setInt8(0,2147483648),G.setInt8(1,2147483649),!G.getInt8(0)&&G.getInt8(1)||s(Y,{setInt8:function(e,t){J.call(this,e,t<<24>>24)},setUint8:function(e,t){J.call(this,e,t<<24>>24)}},{unsafe:!0})}else D=function(e){u(this,D,k);var t=f(e);w(this,{bytes:_.call(new Array(t),0),byteLength:t}),i||(this.byteLength=t)},E=function(e,t,n){u(this,E,L),u(e,D,L);var r=M(e).byteLength,a=d(t);if(a<0||a>r)throw O(\"Wrong offset\");if(a+(n=void 0===n?r-a:c(n))>r)throw O(\"Wrong length\");w(this,{buffer:e,byteLength:n,byteOffset:a}),i||(this.buffer=e,this.byteLength=n,this.byteOffset=a)},i&&(V(D,\"byteLength\"),V(E,\"buffer\"),V(E,\"byteLength\"),V(E,\"byteOffset\")),s(E[x],{getInt8:function(e){return $(this,1,e)[0]<<24>>24},getUint8:function(e){return $(this,1,e)[0]},getInt16:function(e){var t=$(this,2,e,arguments.length>1?arguments[1]:void 0);return(t[1]<<8|t[0])<<16>>16},getUint16:function(e){var t=$(this,2,e,arguments.length>1?arguments[1]:void 0);return t[1]<<8|t[0]},getInt32:function(e){return F($(this,4,e,arguments.length>1?arguments[1]:void 0))},getUint32:function(e){return F($(this,4,e,arguments.length>1?arguments[1]:void 0))>>>0},getFloat32:function(e){return j($(this,4,e,arguments.length>1?arguments[1]:void 0),23)},getFloat64:function(e){return j($(this,8,e,arguments.length>1?arguments[1]:void 0),52)},setInt8:function(e,t){z(this,1,e,P,t)},setUint8:function(e,t){z(this,1,e,P,t)},setInt16:function(e,t){z(this,2,e,H,t,arguments.length>2?arguments[2]:void 0)},setUint16:function(e,t){z(this,2,e,H,t,arguments.length>2?arguments[2]:void 0)},setInt32:function(e,t){z(this,4,e,I,t,arguments.length>2?arguments[2]:void 0)},setUint32:function(e,t){z(this,4,e,I,t,arguments.length>2?arguments[2]:void 0)},setFloat32:function(e,t){z(this,4,e,N,t,arguments.length>2?arguments[2]:void 0)},setFloat64:function(e,t){z(this,8,e,R,t,arguments.length>2?arguments[2]:void 0)}});y(D,k),y(E,L),e.exports={ArrayBuffer:D,DataView:E}},1048:function(e,t,n){\"use strict\";var r=n(7908),i=n(1400),a=n(7466),o=Math.min;e.exports=[].copyWithin||function(e,t){var n=r(this),s=a(n.length),l=i(e,s),u=i(t,s),d=arguments.length>2?arguments[2]:void 0,c=o((void 0===d?s:i(d,s))-u,s-l),f=1;for(u<l&&l<u+c&&(f=-1,u+=c-1,l+=c-1);c-- >0;)u in n?n[l]=n[u]:delete n[l],l+=f,u+=f;return n}},1285:function(e,t,n){\"use strict\";var r=n(7908),i=n(1400),a=n(7466);e.exports=function(e){for(var t=r(this),n=a(t.length),o=arguments.length,s=i(o>1?arguments[1]:void 0,n),l=o>2?arguments[2]:void 0,u=void 0===l?n:i(l,n);u>s;)t[s++]=e;return t}},8533:function(e,t,n){\"use strict\";var r=n(2092).forEach,i=n(9341)(\"forEach\");e.exports=i?[].forEach:function(e){return r(this,e,arguments.length>1?arguments[1]:void 0)}},8457:function(e,t,n){\"use strict\";var r=n(9974),i=n(7908),a=n(3411),o=n(7659),s=n(7466),l=n(6135),u=n(1246);e.exports=function(e){var t,n,d,c,f,p,h=i(e),m=\"function\"==typeof this?this:Array,v=arguments.length,g=v>1?arguments[1]:void 0,_=void 0!==g,y=u(h),b=0;if(_&&(g=r(g,v>2?arguments[2]:void 0,2)),null==y||m==Array&&o(y))for(n=new m(t=s(h.length));t>b;b++)p=_?g(h[b],b):h[b],l(n,b,p);else for(f=(c=y.call(h)).next,n=new m;!(d=f.call(c)).done;b++)p=_?a(c,g,[d.value,b],!0):d.value,l(n,b,p);return n.length=b,n}},1318:function(e,t,n){var r=n(5656),i=n(7466),a=n(1400),o=function(e){return function(t,n,o){var s,l=r(t),u=i(l.length),d=a(o,u);if(e&&n!=n){for(;u>d;)if((s=l[d++])!=s)return!0}else for(;u>d;d++)if((e||d in l)&&l[d]===n)return e||d||0;return!e&&-1}};e.exports={includes:o(!0),indexOf:o(!1)}},2092:function(e,t,n){var r=n(9974),i=n(8361),a=n(7908),o=n(7466),s=n(5417),l=[].push,u=function(e){var t=1==e,n=2==e,u=3==e,d=4==e,c=6==e,f=7==e,p=5==e||c;return function(h,m,v,g){for(var _,y,b=a(h),M=i(b),w=r(m,v,3),k=o(M.length),L=0,x=g||s,T=t?x(h,k):n||f?x(h,0):void 0;k>L;L++)if((p||L in M)&&(y=w(_=M[L],L,b),e))if(t)T[L]=y;else if(y)switch(e){case 3:return!0;case 5:return _;case 6:return L;case 2:l.call(T,_)}else switch(e){case 4:return!1;case 7:l.call(T,_)}return c?-1:u||d?d:T}};e.exports={forEach:u(0),map:u(1),filter:u(2),some:u(3),every:u(4),find:u(5),findIndex:u(6),filterOut:u(7)}},6583:function(e,t,n){\"use strict\";var r=n(5656),i=n(9958),a=n(7466),o=n(9341),s=Math.min,l=[].lastIndexOf,u=!!l&&1/[1].lastIndexOf(1,-0)<0,d=o(\"lastIndexOf\"),c=u||!d;e.exports=c?function(e){if(u)return l.apply(this,arguments)||0;var t=r(this),n=a(t.length),o=n-1;for(arguments.length>1&&(o=s(o,i(arguments[1]))),o<0&&(o=n+o);o>=0;o--)if(o in t&&t[o]===e)return o||0;return-1}:l},1194:function(e,t,n){var r=n(7293),i=n(5112),a=n(7392),o=i(\"species\");e.exports=function(e){return a>=51||!r(function(){var t=[];return(t.constructor={})[o]=function(){return{foo:1}},1!==t[e](Boolean).foo})}},9341:function(e,t,n){\"use strict\";var r=n(7293);e.exports=function(e,t){var n=[][e];return!!n&&r(function(){n.call(null,t||function(){throw 1},1)})}},3671:function(e,t,n){var r=n(3099),i=n(7908),a=n(8361),o=n(7466),s=function(e){return function(t,n,s,l){r(n);var u=i(t),d=a(u),c=o(u.length),f=e?c-1:0,p=e?-1:1;if(s<2)for(;;){if(f in d){l=d[f],f+=p;break}if(f+=p,e?f<0:c<=f)throw TypeError(\"Reduce of empty array with no initial value\")}for(;e?f>=0:c>f;f+=p)f in d&&(l=n(l,d[f],f,u));return l}};e.exports={left:s(!1),right:s(!0)}},5417:function(e,t,n){var r=n(111),i=n(3157),a=n(5112)(\"species\");e.exports=function(e,t){var n;return i(e)&&(\"function\"!=typeof(n=e.constructor)||n!==Array&&!i(n.prototype)?r(n)&&null===(n=n[a])&&(n=void 0):n=void 0),new(void 0===n?Array:n)(0===t?0:t)}},3411:function(e,t,n){var r=n(9670),i=n(9212);e.exports=function(e,t,n,a){try{return a?t(r(n)[0],n[1]):t(n)}catch(t){throw i(e),t}}},7072:function(e,t,n){var r=n(5112)(\"iterator\"),i=!1;try{var a=0,o={next:function(){return{done:!!a++}},return:function(){i=!0}};o[r]=function(){return this},Array.from(o,function(){throw 2})}catch(e){}e.exports=function(e,t){if(!t&&!i)return!1;var n=!1;try{var a={};a[r]=function(){return{next:function(){return{done:n=!0}}}},e(a)}catch(e){}return n}},4326:function(e){var t={}.toString;e.exports=function(e){return t.call(e).slice(8,-1)}},648:function(e,t,n){var r=n(1694),i=n(4326),a=n(5112)(\"toStringTag\"),o=\"Arguments\"==i(function(){return arguments}());e.exports=r?i:function(e){var t,n,r;return void 0===e?\"Undefined\":null===e?\"Null\":\"string\"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),a))?n:o?i(t):\"Object\"==(r=i(t))&&\"function\"==typeof t.callee?\"Arguments\":r}},9920:function(e,t,n){var r=n(6656),i=n(3887),a=n(1236),o=n(3070);e.exports=function(e,t){for(var n=i(t),s=o.f,l=a.f,u=0;u<n.length;u++){var d=n[u];r(e,d)||s(e,d,l(t,d))}}},8544:function(e,t,n){var r=n(7293);e.exports=!r(function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype})},4994:function(e,t,n){\"use strict\";var r=n(3383).IteratorPrototype,i=n(30),a=n(9114),o=n(8003),s=n(7497),l=function(){return this};e.exports=function(e,t,n){var u=t+\" Iterator\";return e.prototype=i(r,{next:a(1,n)}),o(e,u,!1,!0),s[u]=l,e}},8880:function(e,t,n){var r=n(9781),i=n(3070),a=n(9114);e.exports=r?function(e,t,n){return i.f(e,t,a(1,n))}:function(e,t,n){return e[t]=n,e}},9114:function(e){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},6135:function(e,t,n){\"use strict\";var r=n(7593),i=n(3070),a=n(9114);e.exports=function(e,t,n){var o=r(t);o in e?i.f(e,o,a(0,n)):e[o]=n}},654:function(e,t,n){\"use strict\";var r=n(2109),i=n(4994),a=n(9518),o=n(7674),s=n(8003),l=n(8880),u=n(1320),d=n(5112),c=n(1913),f=n(7497),p=n(3383),h=p.IteratorPrototype,m=p.BUGGY_SAFARI_ITERATORS,v=d(\"iterator\"),g=\"keys\",_=\"values\",y=\"entries\",b=function(){return this};e.exports=function(e,t,n,d,p,M,w){i(n,t,d);var k,L,x,T=function(e){if(e===p&&A)return A;if(!m&&e in E)return E[e];switch(e){case g:case _:case y:return function(){return new n(this,e)}}return function(){return new n(this)}},S=t+\" Iterator\",D=!1,E=e.prototype,Y=E[v]||E[\"@@iterator\"]||p&&E[p],A=!m&&Y||T(p),O=\"Array\"==t&&E.entries||Y;if(O&&(k=a(O.call(new e)),h!==Object.prototype&&k.next&&(c||a(k)===h||(o?o(k,h):\"function\"!=typeof k[v]&&l(k,v,b)),s(k,S,!0,!0),c&&(f[S]=b))),p==_&&Y&&Y.name!==_&&(D=!0,A=function(){return Y.call(this)}),c&&!w||E[v]===A||l(E,v,A),f[t]=A,p)if(L={values:T(_),keys:M?A:T(g),entries:T(y)},w)for(x in L)(m||D||!(x in E))&&u(E,x,L[x]);else r({target:t,proto:!0,forced:m||D},L);return L}},9781:function(e,t,n){var r=n(7293);e.exports=!r(function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})},317:function(e,t,n){var r=n(7854),i=n(111),a=r.document,o=i(a)&&i(a.createElement);e.exports=function(e){return o?a.createElement(e):{}}},8324:function(e){e.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},8113:function(e,t,n){var r=n(5005);e.exports=r(\"navigator\",\"userAgent\")||\"\"},7392:function(e,t,n){var r,i,a=n(7854),o=n(8113),s=a.process,l=s&&s.versions,u=l&&l.v8;u?i=(r=u.split(\".\"))[0]+r[1]:o&&(!(r=o.match(/Edge\\/(\\d+)/))||r[1]>=74)&&(r=o.match(/Chrome\\/(\\d+)/))&&(i=r[1]),e.exports=i&&+i},748:function(e){e.exports=[\"constructor\",\"hasOwnProperty\",\"isPrototypeOf\",\"propertyIsEnumerable\",\"toLocaleString\",\"toString\",\"valueOf\"]},2109:function(e,t,n){var r=n(7854),i=n(1236).f,a=n(8880),o=n(1320),s=n(3505),l=n(9920),u=n(4705);e.exports=function(e,t){var n,d,c,f,p,h=e.target,m=e.global,v=e.stat;if(n=m?r:v?r[h]||s(h,{}):(r[h]||{}).prototype)for(d in t){if(f=t[d],c=e.noTargetGet?(p=i(n,d))&&p.value:n[d],!u(m?d:h+(v?\".\":\"#\")+d,e.forced)&&void 0!==c){if(typeof f==typeof c)continue;l(f,c)}(e.sham||c&&c.sham)&&a(f,\"sham\",!0),o(n,d,f,e)}}},7293:function(e){e.exports=function(e){try{return!!e()}catch(e){return!0}}},7007:function(e,t,n){\"use strict\";n(4916);var r=n(1320),i=n(7293),a=n(5112),o=n(2261),s=n(8880),l=a(\"species\"),u=!i(function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:\"7\"},e},\"7\"!==\"\".replace(e,\"$<a>\")}),d=\"$0\"===\"a\".replace(/./,\"$0\"),c=a(\"replace\"),f=!!/./[c]&&\"\"===/./[c](\"a\",\"$0\"),p=!i(function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n=\"ab\".split(e);return 2!==n.length||\"a\"!==n[0]||\"b\"!==n[1]});e.exports=function(e,t,n,c){var h=a(e),m=!i(function(){var t={};return t[h]=function(){return 7},7!=\"\"[e](t)}),v=m&&!i(function(){var t=!1,n=/a/;return\"split\"===e&&((n={}).constructor={},n.constructor[l]=function(){return n},n.flags=\"\",n[h]=/./[h]),n.exec=function(){return t=!0,null},n[h](\"\"),!t});if(!m||!v||\"replace\"===e&&(!u||!d||f)||\"split\"===e&&!p){var g=/./[h],_=n(h,\"\"[e],function(e,t,n,r,i){return t.exec===o?m&&!i?{done:!0,value:g.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}},{REPLACE_KEEPS_$0:d,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:f}),y=_[0],b=_[1];r(String.prototype,e,y),r(RegExp.prototype,h,2==t?function(e,t){return b.call(e,this,t)}:function(e){return b.call(e,this)})}c&&s(RegExp.prototype[h],\"sham\",!0)}},9974:function(e,t,n){var r=n(3099);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},5005:function(e,t,n){var r=n(857),i=n(7854),a=function(e){return\"function\"==typeof e?e:void 0};e.exports=function(e,t){return arguments.length<2?a(r[e])||a(i[e]):r[e]&&r[e][t]||i[e]&&i[e][t]}},1246:function(e,t,n){var r=n(648),i=n(7497),a=n(5112)(\"iterator\");e.exports=function(e){if(null!=e)return e[a]||e[\"@@iterator\"]||i[r(e)]}},8554:function(e,t,n){var r=n(9670),i=n(1246);e.exports=function(e){var t=i(e);if(\"function\"!=typeof t)throw TypeError(String(e)+\" is not iterable\");return r(t.call(e))}},647:function(e,t,n){var r=n(7908),i=Math.floor,a=\"\".replace,o=/\\$([$&'`]|\\d\\d?|<[^>]*>)/g,s=/\\$([$&'`]|\\d\\d?)/g;e.exports=function(e,t,n,l,u,d){var c=n+e.length,f=l.length,p=s;return void 0!==u&&(u=r(u),p=o),a.call(d,p,function(r,a){var o;switch(a.charAt(0)){case\"$\":return\"$\";case\"&\":return e;case\"`\":return t.slice(0,n);case\"'\":return t.slice(c);case\"<\":o=u[a.slice(1,-1)];break;default:var s=+a;if(0===s)return r;if(s>f){var d=i(s/10);return 0===d?r:d<=f?void 0===l[d-1]?a.charAt(1):l[d-1]+a.charAt(1):r}o=l[s-1]}return void 0===o?\"\":o})}},7854:function(e,t,n){var r=function(e){return e&&e.Math==Math&&e};e.exports=r(\"object\"==typeof globalThis&&globalThis)||r(\"object\"==typeof window&&window)||r(\"object\"==typeof self&&self)||r(\"object\"==typeof n.g&&n.g)||function(){return this}()||Function(\"return this\")()},6656:function(e){var t={}.hasOwnProperty;e.exports=function(e,n){return t.call(e,n)}},3501:function(e){e.exports={}},490:function(e,t,n){var r=n(5005);e.exports=r(\"document\",\"documentElement\")},4664:function(e,t,n){var r=n(9781),i=n(7293),a=n(317);e.exports=!r&&!i(function(){return 7!=Object.defineProperty(a(\"div\"),\"a\",{get:function(){return 7}}).a})},1179:function(e){var t=Math.abs,n=Math.pow,r=Math.floor,i=Math.log,a=Math.LN2;e.exports={pack:function(e,o,s){var l,u,d,c=new Array(s),f=8*s-o-1,p=(1<<f)-1,h=p>>1,m=23===o?n(2,-24)-n(2,-77):0,v=e<0||0===e&&1/e<0?1:0,g=0;for((e=t(e))!=e||e===1/0?(u=e!=e?1:0,l=p):(l=r(i(e)/a),e*(d=n(2,-l))<1&&(l--,d*=2),(e+=l+h>=1?m/d:m*n(2,1-h))*d>=2&&(l++,d/=2),l+h>=p?(u=0,l=p):l+h>=1?(u=(e*d-1)*n(2,o),l+=h):(u=e*n(2,h-1)*n(2,o),l=0));o>=8;c[g++]=255&u,u/=256,o-=8);for(l=l<<o|u,f+=o;f>0;c[g++]=255&l,l/=256,f-=8);return c[--g]|=128*v,c},unpack:function(e,t){var r,i=e.length,a=8*i-t-1,o=(1<<a)-1,s=o>>1,l=a-7,u=i-1,d=e[u--],c=127&d;for(d>>=7;l>0;c=256*c+e[u],u--,l-=8);for(r=c&(1<<-l)-1,c>>=-l,l+=t;l>0;r=256*r+e[u],u--,l-=8);if(0===c)c=1-s;else{if(c===o)return r?NaN:d?-1/0:1/0;r+=n(2,t),c-=s}return(d?-1:1)*r*n(2,c-t)}}},8361:function(e,t,n){var r=n(7293),i=n(4326),a=\"\".split;e.exports=r(function(){return!Object(\"z\").propertyIsEnumerable(0)})?function(e){return\"String\"==i(e)?a.call(e,\"\"):Object(e)}:Object},9587:function(e,t,n){var r=n(111),i=n(7674);e.exports=function(e,t,n){var a,o;return i&&\"function\"==typeof(a=t.constructor)&&a!==n&&r(o=a.prototype)&&o!==n.prototype&&i(e,o),e}},2788:function(e,t,n){var r=n(5465),i=Function.toString;\"function\"!=typeof r.inspectSource&&(r.inspectSource=function(e){return i.call(e)}),e.exports=r.inspectSource},9909:function(e,t,n){var r,i,a,o=n(8536),s=n(7854),l=n(111),u=n(8880),d=n(6656),c=n(5465),f=n(6200),p=n(3501),h=s.WeakMap;if(o){var m=c.state||(c.state=new h),v=m.get,g=m.has,_=m.set;r=function(e,t){return t.facade=e,_.call(m,e,t),t},i=function(e){return v.call(m,e)||{}},a=function(e){return g.call(m,e)}}else{var y=f(\"state\");p[y]=!0,r=function(e,t){return t.facade=e,u(e,y,t),t},i=function(e){return d(e,y)?e[y]:{}},a=function(e){return d(e,y)}}e.exports={set:r,get:i,has:a,enforce:function(e){return a(e)?i(e):r(e,{})},getterFor:function(e){return function(t){var n;if(!l(t)||(n=i(t)).type!==e)throw TypeError(\"Incompatible receiver, \"+e+\" required\");return n}}}},7659:function(e,t,n){var r=n(5112),i=n(7497),a=r(\"iterator\"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(i.Array===e||o[a]===e)}},3157:function(e,t,n){var r=n(4326);e.exports=Array.isArray||function(e){return\"Array\"==r(e)}},4705:function(e,t,n){var r=n(7293),i=/#|\\.prototype\\./,a=function(e,t){var n=s[o(e)];return n==u||n!=l&&(\"function\"==typeof t?r(t):!!t)},o=a.normalize=function(e){return String(e).replace(i,\".\").toLowerCase()},s=a.data={},l=a.NATIVE=\"N\",u=a.POLYFILL=\"P\";e.exports=a},111:function(e){e.exports=function(e){return\"object\"==typeof e?null!==e:\"function\"==typeof e}},1913:function(e){e.exports=!1},7850:function(e,t,n){var r=n(111),i=n(4326),a=n(5112)(\"match\");e.exports=function(e){var t;return r(e)&&(void 0!==(t=e[a])?!!t:\"RegExp\"==i(e))}},9212:function(e,t,n){var r=n(9670);e.exports=function(e){var t=e.return;if(void 0!==t)return r(t.call(e)).value}},3383:function(e,t,n){\"use strict\";var r,i,a,o=n(7293),s=n(9518),l=n(8880),u=n(6656),d=n(5112),c=n(1913),f=d(\"iterator\"),p=!1;[].keys&&(\"next\"in(a=[].keys())?(i=s(s(a)))!==Object.prototype&&(r=i):p=!0);var h=null==r||o(function(){var e={};return r[f].call(e)!==e});h&&(r={}),c&&!h||u(r,f)||l(r,f,function(){return this}),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:p}},7497:function(e){e.exports={}},133:function(e,t,n){var r=n(7293);e.exports=!!Object.getOwnPropertySymbols&&!r(function(){return!String(Symbol())})},590:function(e,t,n){var r=n(7293),i=n(5112),a=n(1913),o=i(\"iterator\");e.exports=!r(function(){var e=new URL(\"b?a=1&b=2&c=3\",\"http://a\"),t=e.searchParams,n=\"\";return e.pathname=\"c%20d\",t.forEach(function(e,r){t.delete(\"b\"),n+=r+e}),a&&!e.toJSON||!t.sort||\"http://a/c%20d?a=1&c=3\"!==e.href||\"3\"!==t.get(\"c\")||\"a=1\"!==String(new URLSearchParams(\"?a=1\"))||!t[o]||\"a\"!==new URL(\"https://a@b\").username||\"b\"!==new URLSearchParams(new URLSearchParams(\"a=b\")).get(\"a\")||\"xn--e1aybc\"!==new URL(\"http://тест\").host||\"#%D0%B1\"!==new URL(\"http://a#б\").hash||\"a1c3\"!==n||\"x\"!==new URL(\"http://x\",void 0).host})},8536:function(e,t,n){var r=n(7854),i=n(2788),a=r.WeakMap;e.exports=\"function\"==typeof a&&/native code/.test(i(a))},1574:function(e,t,n){\"use strict\";var r=n(9781),i=n(7293),a=n(1956),o=n(5181),s=n(5296),l=n(7908),u=n(8361),d=Object.assign,c=Object.defineProperty;e.exports=!d||i(function(){if(r&&1!==d({b:1},d(c({},\"a\",{enumerable:!0,get:function(){c(this,\"b\",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},t={},n=Symbol(),i=\"abcdefghijklmnopqrst\";return e[n]=7,i.split(\"\").forEach(function(e){t[e]=e}),7!=d({},e)[n]||a(d({},t)).join(\"\")!=i})?function(e,t){for(var n=l(e),i=arguments.length,d=1,c=o.f,f=s.f;i>d;)for(var p,h=u(arguments[d++]),m=c?a(h).concat(c(h)):a(h),v=m.length,g=0;v>g;)p=m[g++],r&&!f.call(h,p)||(n[p]=h[p]);return n}:d},30:function(e,t,n){var r,i=n(9670),a=n(6048),o=n(748),s=n(3501),l=n(490),u=n(317),d=n(6200),c=\"prototype\",f=\"script\",p=d(\"IE_PROTO\"),h=function(){},m=function(e){return\"<\"+f+\">\"+e+\"</\"+f+\">\"},v=function(){try{r=document.domain&&new ActiveXObject(\"htmlfile\")}catch(e){}var e,t,n;v=r?function(e){e.write(m(\"\")),e.close();var t=e.parentWindow.Object;return e=null,t}(r):(t=u(\"iframe\"),n=\"java\"+f+\":\",t.style.display=\"none\",l.appendChild(t),t.src=String(n),(e=t.contentWindow.document).open(),e.write(m(\"document.F=Object\")),e.close(),e.F);for(var i=o.length;i--;)delete v[c][o[i]];return v()};s[p]=!0,e.exports=Object.create||function(e,t){var n;return null!==e?(h[c]=i(e),n=new h,h[c]=null,n[p]=e):n=v(),void 0===t?n:a(n,t)}},6048:function(e,t,n){var r=n(9781),i=n(3070),a=n(9670),o=n(1956);e.exports=r?Object.defineProperties:function(e,t){a(e);for(var n,r=o(t),s=r.length,l=0;s>l;)i.f(e,n=r[l++],t[n]);return e}},3070:function(e,t,n){var r=n(9781),i=n(4664),a=n(9670),o=n(7593),s=Object.defineProperty;t.f=r?s:function(e,t,n){if(a(e),t=o(t,!0),a(n),i)try{return s(e,t,n)}catch(e){}if(\"get\"in n||\"set\"in n)throw TypeError(\"Accessors not supported\");return\"value\"in n&&(e[t]=n.value),e}},1236:function(e,t,n){var r=n(9781),i=n(5296),a=n(9114),o=n(5656),s=n(7593),l=n(6656),u=n(4664),d=Object.getOwnPropertyDescriptor;t.f=r?d:function(e,t){if(e=o(e),t=s(t,!0),u)try{return d(e,t)}catch(e){}if(l(e,t))return a(!i.f.call(e,t),e[t])}},8006:function(e,t,n){var r=n(6324),i=n(748).concat(\"length\",\"prototype\");t.f=Object.getOwnPropertyNames||function(e){return r(e,i)}},5181:function(e,t){t.f=Object.getOwnPropertySymbols},9518:function(e,t,n){var r=n(6656),i=n(7908),a=n(6200),o=n(8544),s=a(\"IE_PROTO\"),l=Object.prototype;e.exports=o?Object.getPrototypeOf:function(e){return e=i(e),r(e,s)?e[s]:\"function\"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?l:null}},6324:function(e,t,n){var r=n(6656),i=n(5656),a=n(1318).indexOf,o=n(3501);e.exports=function(e,t){var n,s=i(e),l=0,u=[];for(n in s)!r(o,n)&&r(s,n)&&u.push(n);for(;t.length>l;)r(s,n=t[l++])&&(~a(u,n)||u.push(n));return u}},1956:function(e,t,n){var r=n(6324),i=n(748);e.exports=Object.keys||function(e){return r(e,i)}},5296:function(e,t){\"use strict\";var n={}.propertyIsEnumerable,r=Object.getOwnPropertyDescriptor,i=r&&!n.call({1:2},1);t.f=i?function(e){var t=r(this,e);return!!t&&t.enumerable}:n},7674:function(e,t,n){var r=n(9670),i=n(6077);e.exports=Object.setPrototypeOf||(\"__proto__\"in{}?function(){var e,t=!1,n={};try{(e=Object.getOwnPropertyDescriptor(Object.prototype,\"__proto__\").set).call(n,[]),t=n instanceof Array}catch(e){}return function(n,a){return r(n),i(a),t?e.call(n,a):n.__proto__=a,n}}():void 0)},288:function(e,t,n){\"use strict\";var r=n(1694),i=n(648);e.exports=r?{}.toString:function(){return\"[object \"+i(this)+\"]\"}},3887:function(e,t,n){var r=n(5005),i=n(8006),a=n(5181),o=n(9670);e.exports=r(\"Reflect\",\"ownKeys\")||function(e){var t=i.f(o(e)),n=a.f;return n?t.concat(n(e)):t}},857:function(e,t,n){var r=n(7854);e.exports=r},2248:function(e,t,n){var r=n(1320);e.exports=function(e,t,n){for(var i in t)r(e,i,t[i],n);return e}},1320:function(e,t,n){var r=n(7854),i=n(8880),a=n(6656),o=n(3505),s=n(2788),l=n(9909),u=l.get,d=l.enforce,c=String(String).split(\"String\");(e.exports=function(e,t,n,s){var l,u=!!s&&!!s.unsafe,f=!!s&&!!s.enumerable,p=!!s&&!!s.noTargetGet;\"function\"==typeof n&&(\"string\"!=typeof t||a(n,\"name\")||i(n,\"name\",t),(l=d(n)).source||(l.source=c.join(\"string\"==typeof t?t:\"\"))),e!==r?(u?!p&&e[t]&&(f=!0):delete e[t],f?e[t]=n:i(e,t,n)):f?e[t]=n:o(t,n)})(Function.prototype,\"toString\",function(){return\"function\"==typeof this&&u(this).source||s(this)})},7651:function(e,t,n){var r=n(4326),i=n(2261);e.exports=function(e,t){var n=e.exec;if(\"function\"==typeof n){var a=n.call(e,t);if(\"object\"!=typeof a)throw TypeError(\"RegExp exec method returned something other than an Object or null\");return a}if(\"RegExp\"!==r(e))throw TypeError(\"RegExp#exec called on incompatible receiver\");return i.call(e,t)}},2261:function(e,t,n){\"use strict\";var r,i,a=n(7066),o=n(2999),s=RegExp.prototype.exec,l=String.prototype.replace,u=s,d=(r=/a/,i=/b*/g,s.call(r,\"a\"),s.call(i,\"a\"),0!==r.lastIndex||0!==i.lastIndex),c=o.UNSUPPORTED_Y||o.BROKEN_CARET,f=void 0!==/()??/.exec(\"\")[1];(d||f||c)&&(u=function(e){var t,n,r,i,o=this,u=c&&o.sticky,p=a.call(o),h=o.source,m=0,v=e;return u&&(-1===(p=p.replace(\"y\",\"\")).indexOf(\"g\")&&(p+=\"g\"),v=String(e).slice(o.lastIndex),o.lastIndex>0&&(!o.multiline||o.multiline&&\"\\n\"!==e[o.lastIndex-1])&&(h=\"(?: \"+h+\")\",v=\" \"+v,m++),n=new RegExp(\"^(?:\"+h+\")\",p)),f&&(n=new RegExp(\"^\"+h+\"$(?!\\\\s)\",p)),d&&(t=o.lastIndex),r=s.call(u?n:o,v),u?r?(r.input=r.input.slice(m),r[0]=r[0].slice(m),r.index=o.lastIndex,o.lastIndex+=r[0].length):o.lastIndex=0:d&&r&&(o.lastIndex=o.global?r.index+r[0].length:t),f&&r&&r.length>1&&l.call(r[0],n,function(){for(i=1;i<arguments.length-2;i++)void 0===arguments[i]&&(r[i]=void 0)}),r}),e.exports=u},7066:function(e,t,n){\"use strict\";var r=n(9670);e.exports=function(){var e=r(this),t=\"\";return e.global&&(t+=\"g\"),e.ignoreCase&&(t+=\"i\"),e.multiline&&(t+=\"m\"),e.dotAll&&(t+=\"s\"),e.unicode&&(t+=\"u\"),e.sticky&&(t+=\"y\"),t}},2999:function(e,t,n){\"use strict\";var r=n(7293);function i(e,t){return RegExp(e,t)}t.UNSUPPORTED_Y=r(function(){var e=i(\"a\",\"y\");return e.lastIndex=2,null!=e.exec(\"abcd\")}),t.BROKEN_CARET=r(function(){var e=i(\"^r\",\"gy\");return e.lastIndex=2,null!=e.exec(\"str\")})},4488:function(e){e.exports=function(e){if(null==e)throw TypeError(\"Can't call method on \"+e);return e}},3505:function(e,t,n){var r=n(7854),i=n(8880);e.exports=function(e,t){try{i(r,e,t)}catch(n){r[e]=t}return t}},6340:function(e,t,n){\"use strict\";var r=n(5005),i=n(3070),a=n(5112),o=n(9781),s=a(\"species\");e.exports=function(e){var t=r(e),n=i.f;o&&t&&!t[s]&&n(t,s,{configurable:!0,get:function(){return this}})}},8003:function(e,t,n){var r=n(3070).f,i=n(6656),a=n(5112)(\"toStringTag\");e.exports=function(e,t,n){e&&!i(e=n?e:e.prototype,a)&&r(e,a,{configurable:!0,value:t})}},6200:function(e,t,n){var r=n(2309),i=n(9711),a=r(\"keys\");e.exports=function(e){return a[e]||(a[e]=i(e))}},5465:function(e,t,n){var r=n(7854),i=n(3505),a=\"__core-js_shared__\",o=r[a]||i(a,{});e.exports=o},2309:function(e,t,n){var r=n(1913),i=n(5465);(e.exports=function(e,t){return i[e]||(i[e]=void 0!==t?t:{})})(\"versions\",[]).push({version:\"3.9.0\",mode:r?\"pure\":\"global\",copyright:\"© 2021 Denis Pushkarev (zloirock.ru)\"})},6707:function(e,t,n){var r=n(9670),i=n(3099),a=n(5112)(\"species\");e.exports=function(e,t){var n,o=r(e).constructor;return void 0===o||null==(n=r(o)[a])?t:i(n)}},8710:function(e,t,n){var r=n(9958),i=n(4488),a=function(e){return function(t,n){var a,o,s=String(i(t)),l=r(n),u=s.length;return l<0||l>=u?e?\"\":void 0:(a=s.charCodeAt(l))<55296||a>56319||l+1===u||(o=s.charCodeAt(l+1))<56320||o>57343?e?s.charAt(l):a:e?s.slice(l,l+2):o-56320+(a-55296<<10)+65536}};e.exports={codeAt:a(!1),charAt:a(!0)}},3197:function(e){\"use strict\";var t=2147483647,n=/[^\\0-\\u007E]/,r=/[.\\u3002\\uFF0E\\uFF61]/g,i=\"Overflow: input needs wider integers to process\",a=Math.floor,o=String.fromCharCode,s=function(e){return e+22+75*(e<26)},l=function(e,t,n){var r=0;for(e=n?a(e/700):e>>1,e+=a(e/t);e>455;r+=36)e=a(e/35);return a(r+36*e/(e+38))},u=function(e){var n=[];e=function(e){for(var t=[],n=0,r=e.length;n<r;){var i=e.charCodeAt(n++);if(i>=55296&&i<=56319&&n<r){var a=e.charCodeAt(n++);56320==(64512&a)?t.push(((1023&i)<<10)+(1023&a)+65536):(t.push(i),n--)}else t.push(i)}return t}(e);var r,u,d=e.length,c=128,f=0,p=72;for(r=0;r<e.length;r++)(u=e[r])<128&&n.push(o(u));var h=n.length,m=h;for(h&&n.push(\"-\");m<d;){var v=t;for(r=0;r<e.length;r++)(u=e[r])>=c&&u<v&&(v=u);var g=m+1;if(v-c>a((t-f)/g))throw RangeError(i);for(f+=(v-c)*g,c=v,r=0;r<e.length;r++){if((u=e[r])<c&&++f>t)throw RangeError(i);if(u==c){for(var _=f,y=36;;y+=36){var b=y<=p?1:y>=p+26?26:y-p;if(_<b)break;var M=_-b,w=36-b;n.push(o(s(b+M%w))),_=a(M/w)}n.push(o(s(_))),p=l(f,g,m==h),f=0,++m}}++f,++c}return n.join(\"\")};e.exports=function(e){var t,i,a=[],o=e.toLowerCase().replace(r,\".\").split(\".\");for(t=0;t<o.length;t++)i=o[t],a.push(n.test(i)?\"xn--\"+u(i):i);return a.join(\".\")}},6091:function(e,t,n){var r=n(7293),i=n(1361);e.exports=function(e){return r(function(){return!!i[e]()||\"​᠎\"!=\"​᠎\"[e]()||i[e].name!==e})}},3111:function(e,t,n){var r=n(4488),i=\"[\"+n(1361)+\"]\",a=RegExp(\"^\"+i+i+\"*\"),o=RegExp(i+i+\"*$\"),s=function(e){return function(t){var n=String(r(t));return 1&e&&(n=n.replace(a,\"\")),2&e&&(n=n.replace(o,\"\")),n}};e.exports={start:s(1),end:s(2),trim:s(3)}},1400:function(e,t,n){var r=n(9958),i=Math.max,a=Math.min;e.exports=function(e,t){var n=r(e);return n<0?i(n+t,0):a(n,t)}},7067:function(e,t,n){var r=n(9958),i=n(7466);e.exports=function(e){if(void 0===e)return 0;var t=r(e),n=i(t);if(t!==n)throw RangeError(\"Wrong length or index\");return n}},5656:function(e,t,n){var r=n(8361),i=n(4488);e.exports=function(e){return r(i(e))}},9958:function(e){var t=Math.ceil,n=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?n:t)(e)}},7466:function(e,t,n){var r=n(9958),i=Math.min;e.exports=function(e){return e>0?i(r(e),9007199254740991):0}},7908:function(e,t,n){var r=n(4488);e.exports=function(e){return Object(r(e))}},4590:function(e,t,n){var r=n(3002);e.exports=function(e,t){var n=r(e);if(n%t)throw RangeError(\"Wrong offset\");return n}},3002:function(e,t,n){var r=n(9958);e.exports=function(e){var t=r(e);if(t<0)throw RangeError(\"The argument can't be less than 0\");return t}},7593:function(e,t,n){var r=n(111);e.exports=function(e,t){if(!r(e))return e;var n,i;if(t&&\"function\"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;if(\"function\"==typeof(n=e.valueOf)&&!r(i=n.call(e)))return i;if(!t&&\"function\"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;throw TypeError(\"Can't convert object to primitive value\")}},1694:function(e,t,n){var r={};r[n(5112)(\"toStringTag\")]=\"z\",e.exports=\"[object z]\"===String(r)},9843:function(e,t,n){\"use strict\";var r=n(2109),i=n(7854),a=n(9781),o=n(3832),s=n(260),l=n(3331),u=n(5787),d=n(9114),c=n(8880),f=n(7466),p=n(7067),h=n(4590),m=n(7593),v=n(6656),g=n(648),_=n(111),y=n(30),b=n(7674),M=n(8006).f,w=n(7321),k=n(2092).forEach,L=n(6340),x=n(3070),T=n(1236),S=n(9909),D=n(9587),E=S.get,Y=S.set,A=x.f,O=T.f,C=Math.round,j=i.RangeError,P=l.ArrayBuffer,H=l.DataView,I=s.NATIVE_ARRAY_BUFFER_VIEWS,F=s.TYPED_ARRAY_TAG,N=s.TypedArray,R=s.TypedArrayPrototype,V=s.aTypedArrayConstructor,$=s.isTypedArray,z=\"BYTES_PER_ELEMENT\",W=\"Wrong length\",U=function(e,t){for(var n=0,r=t.length,i=new(V(e))(r);r>n;)i[n]=t[n++];return i},B=function(e,t){A(e,t,{get:function(){return E(this)[t]}})},q=function(e){var t;return e instanceof P||\"ArrayBuffer\"==(t=g(e))||\"SharedArrayBuffer\"==t},G=function(e,t){return $(e)&&\"symbol\"!=typeof t&&t in e&&String(+t)==String(t)},J=function(e,t){return G(e,t=m(t,!0))?d(2,e[t]):O(e,t)},Z=function(e,t,n){return!(G(e,t=m(t,!0))&&_(n)&&v(n,\"value\"))||v(n,\"get\")||v(n,\"set\")||n.configurable||v(n,\"writable\")&&!n.writable||v(n,\"enumerable\")&&!n.enumerable?A(e,t,n):(e[t]=n.value,e)};a?(I||(T.f=J,x.f=Z,B(R,\"buffer\"),B(R,\"byteOffset\"),B(R,\"byteLength\"),B(R,\"length\")),r({target:\"Object\",stat:!0,forced:!I},{getOwnPropertyDescriptor:J,defineProperty:Z}),e.exports=function(e,t,n){var a=e.match(/\\d+$/)[0]/8,s=e+(n?\"Clamped\":\"\")+\"Array\",l=\"get\"+e,d=\"set\"+e,m=i[s],v=m,g=v&&v.prototype,x={},T=function(e,t){A(e,t,{get:function(){return function(e,t){var n=E(e);return n.view[l](t*a+n.byteOffset,!0)}(this,t)},set:function(e){return function(e,t,r){var i=E(e);n&&(r=(r=C(r))<0?0:r>255?255:255&r),i.view[d](t*a+i.byteOffset,r,!0)}(this,t,e)},enumerable:!0})};I?o&&(v=t(function(e,t,n,r){return u(e,v,s),D(_(t)?q(t)?void 0!==r?new m(t,h(n,a),r):void 0!==n?new m(t,h(n,a)):new m(t):$(t)?U(v,t):w.call(v,t):new m(p(t)),e,v)}),b&&b(v,N),k(M(m),function(e){e in v||c(v,e,m[e])}),v.prototype=g):(v=t(function(e,t,n,r){u(e,v,s);var i,o,l,d=0,c=0;if(_(t)){if(!q(t))return $(t)?U(v,t):w.call(v,t);i=t,c=h(n,a);var m=t.byteLength;if(void 0===r){if(m%a)throw j(W);if((o=m-c)<0)throw j(W)}else if((o=f(r)*a)+c>m)throw j(W);l=o/a}else l=p(t),i=new P(o=l*a);for(Y(e,{buffer:i,byteOffset:c,byteLength:o,length:l,view:new H(i)});d<l;)T(e,d++)}),b&&b(v,N),g=v.prototype=y(R)),g.constructor!==v&&c(g,\"constructor\",v),F&&c(g,F,s),x[s]=v,r({global:!0,forced:v!=m,sham:!I},x),z in v||c(v,z,a),z in g||c(g,z,a),L(s)}):e.exports=function(){}},3832:function(e,t,n){var r=n(7854),i=n(7293),a=n(7072),o=n(260).NATIVE_ARRAY_BUFFER_VIEWS,s=r.ArrayBuffer,l=r.Int8Array;e.exports=!o||!i(function(){l(1)})||!i(function(){new l(-1)})||!a(function(e){new l,new l(null),new l(1.5),new l(e)},!0)||i(function(){return 1!==new l(new s(2),1,void 0).length})},3074:function(e,t,n){var r=n(260).aTypedArrayConstructor,i=n(6707);e.exports=function(e,t){for(var n=i(e,e.constructor),a=0,o=t.length,s=new(r(n))(o);o>a;)s[a]=t[a++];return s}},7321:function(e,t,n){var r=n(7908),i=n(7466),a=n(1246),o=n(7659),s=n(9974),l=n(260).aTypedArrayConstructor;e.exports=function(e){var t,n,u,d,c,f,p=r(e),h=arguments.length,m=h>1?arguments[1]:void 0,v=void 0!==m,g=a(p);if(null!=g&&!o(g))for(f=(c=g.call(p)).next,p=[];!(d=f.call(c)).done;)p.push(d.value);for(v&&h>2&&(m=s(m,arguments[2],2)),n=i(p.length),u=new(l(this))(n),t=0;n>t;t++)u[t]=v?m(p[t],t):p[t];return u}},9711:function(e){var t=0,n=Math.random();e.exports=function(e){return\"Symbol(\"+String(void 0===e?\"\":e)+\")_\"+(++t+n).toString(36)}},3307:function(e,t,n){var r=n(133);e.exports=r&&!Symbol.sham&&\"symbol\"==typeof Symbol.iterator},5112:function(e,t,n){var r=n(7854),i=n(2309),a=n(6656),o=n(9711),s=n(133),l=n(3307),u=i(\"wks\"),d=r.Symbol,c=l?d:d&&d.withoutSetter||o;e.exports=function(e){return a(u,e)||(s&&a(d,e)?u[e]=d[e]:u[e]=c(\"Symbol.\"+e)),u[e]}},1361:function(e){e.exports=\"\\t\\n\\v\\f\\r                　\\u2028\\u2029\\ufeff\"},8264:function(e,t,n){\"use strict\";var r=n(2109),i=n(7854),a=n(3331),o=n(6340),s=\"ArrayBuffer\",l=a[s];r({global:!0,forced:i[s]!==l},{ArrayBuffer:l}),o(s)},2222:function(e,t,n){\"use strict\";var r=n(2109),i=n(7293),a=n(3157),o=n(111),s=n(7908),l=n(7466),u=n(6135),d=n(5417),c=n(1194),f=n(5112),p=n(7392),h=f(\"isConcatSpreadable\"),m=9007199254740991,v=\"Maximum allowed index exceeded\",g=p>=51||!i(function(){var e=[];return e[h]=!1,e.concat()[0]!==e}),_=c(\"concat\"),y=function(e){if(!o(e))return!1;var t=e[h];return void 0!==t?!!t:a(e)};r({target:\"Array\",proto:!0,forced:!g||!_},{concat:function(e){var t,n,r,i,a,o=s(this),c=d(o,0),f=0;for(t=-1,r=arguments.length;t<r;t++)if(y(a=-1===t?o:arguments[t])){if(f+(i=l(a.length))>m)throw TypeError(v);for(n=0;n<i;n++,f++)n in a&&u(c,f,a[n])}else{if(f>=m)throw TypeError(v);u(c,f++,a)}return c.length=f,c}})},7327:function(e,t,n){\"use strict\";var r=n(2109),i=n(2092).filter;r({target:\"Array\",proto:!0,forced:!n(1194)(\"filter\")},{filter:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}})},2772:function(e,t,n){\"use strict\";var r=n(2109),i=n(1318).indexOf,a=n(9341),o=[].indexOf,s=!!o&&1/[1].indexOf(1,-0)<0,l=a(\"indexOf\");r({target:\"Array\",proto:!0,forced:s||!l},{indexOf:function(e){return s?o.apply(this,arguments)||0:i(this,e,arguments.length>1?arguments[1]:void 0)}})},6992:function(e,t,n){\"use strict\";var r=n(5656),i=n(1223),a=n(7497),o=n(9909),s=n(654),l=\"Array Iterator\",u=o.set,d=o.getterFor(l);e.exports=s(Array,\"Array\",function(e,t){u(this,{type:l,target:r(e),index:0,kind:t})},function(){var e=d(this),t=e.target,n=e.kind,r=e.index++;return!t||r>=t.length?(e.target=void 0,{value:void 0,done:!0}):\"keys\"==n?{value:r,done:!1}:\"values\"==n?{value:t[r],done:!1}:{value:[r,t[r]],done:!1}},\"values\"),a.Arguments=a.Array,i(\"keys\"),i(\"values\"),i(\"entries\")},1249:function(e,t,n){\"use strict\";var r=n(2109),i=n(2092).map;r({target:\"Array\",proto:!0,forced:!n(1194)(\"map\")},{map:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}})},7042:function(e,t,n){\"use strict\";var r=n(2109),i=n(111),a=n(3157),o=n(1400),s=n(7466),l=n(5656),u=n(6135),d=n(5112),c=n(1194)(\"slice\"),f=d(\"species\"),p=[].slice,h=Math.max;r({target:\"Array\",proto:!0,forced:!c},{slice:function(e,t){var n,r,d,c=l(this),m=s(c.length),v=o(e,m),g=o(void 0===t?m:t,m);if(a(c)&&(\"function\"!=typeof(n=c.constructor)||n!==Array&&!a(n.prototype)?i(n)&&null===(n=n[f])&&(n=void 0):n=void 0,n===Array||void 0===n))return p.call(c,v,g);for(r=new(void 0===n?Array:n)(h(g-v,0)),d=0;v<g;v++,d++)v in c&&u(r,d,c[v]);return r.length=d,r}})},561:function(e,t,n){\"use strict\";var r=n(2109),i=n(1400),a=n(9958),o=n(7466),s=n(7908),l=n(5417),u=n(6135),d=n(1194)(\"splice\"),c=Math.max,f=Math.min;r({target:\"Array\",proto:!0,forced:!d},{splice:function(e,t){var n,r,d,p,h,m,v=s(this),g=o(v.length),_=i(e,g),y=arguments.length;if(0===y?n=r=0:1===y?(n=0,r=g-_):(n=y-2,r=f(c(a(t),0),g-_)),g+n-r>9007199254740991)throw TypeError(\"Maximum allowed length exceeded\");for(d=l(v,r),p=0;p<r;p++)(h=_+p)in v&&u(d,p,v[h]);if(d.length=r,n<r){for(p=_;p<g-r;p++)m=p+n,(h=p+r)in v?v[m]=v[h]:delete v[m];for(p=g;p>g-r+n;p--)delete v[p-1]}else if(n>r)for(p=g-r;p>_;p--)m=p+n-1,(h=p+r-1)in v?v[m]=v[h]:delete v[m];for(p=0;p<n;p++)v[p+_]=arguments[p+2];return v.length=g-r+n,d}})},8309:function(e,t,n){var r=n(9781),i=n(3070).f,a=Function.prototype,o=a.toString,s=/^\\s*function ([^ (]*)/,l=\"name\";r&&!(l in a)&&i(a,l,{configurable:!0,get:function(){try{return o.call(this).match(s)[1]}catch(e){return\"\"}}})},489:function(e,t,n){var r=n(2109),i=n(7293),a=n(7908),o=n(9518),s=n(8544);r({target:\"Object\",stat:!0,forced:i(function(){o(1)}),sham:!s},{getPrototypeOf:function(e){return o(a(e))}})},1539:function(e,t,n){var r=n(1694),i=n(1320),a=n(288);r||i(Object.prototype,\"toString\",a,{unsafe:!0})},4916:function(e,t,n){\"use strict\";var r=n(2109),i=n(2261);r({target:\"RegExp\",proto:!0,forced:/./.exec!==i},{exec:i})},9714:function(e,t,n){\"use strict\";var r=n(1320),i=n(9670),a=n(7293),o=n(7066),s=\"toString\",l=RegExp.prototype,u=l[s],d=a(function(){return\"/a/b\"!=u.call({source:\"a\",flags:\"b\"})}),c=u.name!=s;(d||c)&&r(RegExp.prototype,s,function(){var e=i(this),t=String(e.source),n=e.flags;return\"/\"+t+\"/\"+String(void 0===n&&e instanceof RegExp&&!(\"flags\"in l)?o.call(e):n)},{unsafe:!0})},8783:function(e,t,n){\"use strict\";var r=n(8710).charAt,i=n(9909),a=n(654),o=\"String Iterator\",s=i.set,l=i.getterFor(o);a(String,\"String\",function(e){s(this,{type:o,string:String(e),index:0})},function(){var e,t=l(this),n=t.string,i=t.index;return i>=n.length?{value:void 0,done:!0}:(e=r(n,i),t.index+=e.length,{value:e,done:!1})})},4723:function(e,t,n){\"use strict\";var r=n(7007),i=n(9670),a=n(7466),o=n(4488),s=n(1530),l=n(7651);r(\"match\",1,function(e,t,n){return[function(t){var n=o(this),r=null==t?void 0:t[e];return void 0!==r?r.call(t,n):new RegExp(t)[e](String(n))},function(e){var r=n(t,e,this);if(r.done)return r.value;var o=i(e),u=String(this);if(!o.global)return l(o,u);var d=o.unicode;o.lastIndex=0;for(var c,f=[],p=0;null!==(c=l(o,u));){var h=String(c[0]);f[p]=h,\"\"===h&&(o.lastIndex=s(u,a(o.lastIndex),d)),p++}return 0===p?null:f}]})},5306:function(e,t,n){\"use strict\";var r=n(7007),i=n(9670),a=n(7466),o=n(9958),s=n(4488),l=n(1530),u=n(647),d=n(7651),c=Math.max,f=Math.min,p=function(e){return void 0===e?e:String(e)};r(\"replace\",2,function(e,t,n,r){var h=r.REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE,m=r.REPLACE_KEEPS_$0,v=h?\"$\":\"$0\";return[function(n,r){var i=s(this),a=null==n?void 0:n[e];return void 0!==a?a.call(n,i,r):t.call(String(i),n,r)},function(e,r){if(!h&&m||\"string\"==typeof r&&-1===r.indexOf(v)){var s=n(t,e,this,r);if(s.done)return s.value}var g=i(e),_=String(this),y=\"function\"==typeof r;y||(r=String(r));var b=g.global;if(b){var M=g.unicode;g.lastIndex=0}for(var w=[];;){var k=d(g,_);if(null===k)break;if(w.push(k),!b)break;\"\"===String(k[0])&&(g.lastIndex=l(_,a(g.lastIndex),M))}for(var L=\"\",x=0,T=0;T<w.length;T++){k=w[T];for(var S=String(k[0]),D=c(f(o(k.index),_.length),0),E=[],Y=1;Y<k.length;Y++)E.push(p(k[Y]));var A=k.groups;if(y){var O=[S].concat(E,D,_);void 0!==A&&O.push(A);var C=String(r.apply(void 0,O))}else C=u(S,_,D,E,A,r);D>=x&&(L+=_.slice(x,D)+C,x=D+S.length)}return L+_.slice(x)}]})},3123:function(e,t,n){\"use strict\";var r=n(7007),i=n(7850),a=n(9670),o=n(4488),s=n(6707),l=n(1530),u=n(7466),d=n(7651),c=n(2261),f=n(7293),p=[].push,h=Math.min,m=4294967295,v=!f(function(){return!RegExp(m,\"y\")});r(\"split\",2,function(e,t,n){var r;return r=\"c\"==\"abbc\".split(/(b)*/)[1]||4!=\"test\".split(/(?:)/,-1).length||2!=\"ab\".split(/(?:ab)*/).length||4!=\".\".split(/(.?)(.?)/).length||\".\".split(/()()/).length>1||\"\".split(/.?/).length?function(e,n){var r=String(o(this)),a=void 0===n?m:n>>>0;if(0===a)return[];if(void 0===e)return[r];if(!i(e))return t.call(r,e,a);for(var s,l,u,d=[],f=(e.ignoreCase?\"i\":\"\")+(e.multiline?\"m\":\"\")+(e.unicode?\"u\":\"\")+(e.sticky?\"y\":\"\"),h=0,v=new RegExp(e.source,f+\"g\");(s=c.call(v,r))&&!((l=v.lastIndex)>h&&(d.push(r.slice(h,s.index)),s.length>1&&s.index<r.length&&p.apply(d,s.slice(1)),u=s[0].length,h=l,d.length>=a));)v.lastIndex===s.index&&v.lastIndex++;return h===r.length?!u&&v.test(\"\")||d.push(\"\"):d.push(r.slice(h)),d.length>a?d.slice(0,a):d}:\"0\".split(void 0,0).length?function(e,n){return void 0===e&&0===n?[]:t.call(this,e,n)}:t,[function(t,n){var i=o(this),a=null==t?void 0:t[e];return void 0!==a?a.call(t,i,n):r.call(String(i),t,n)},function(e,i){var o=n(r,e,this,i,r!==t);if(o.done)return o.value;var c=a(e),f=String(this),p=s(c,RegExp),g=c.unicode,_=(c.ignoreCase?\"i\":\"\")+(c.multiline?\"m\":\"\")+(c.unicode?\"u\":\"\")+(v?\"y\":\"g\"),y=new p(v?c:\"^(?:\"+c.source+\")\",_),b=void 0===i?m:i>>>0;if(0===b)return[];if(0===f.length)return null===d(y,f)?[f]:[];for(var M=0,w=0,k=[];w<f.length;){y.lastIndex=v?w:0;var L,x=d(y,v?f:f.slice(w));if(null===x||(L=h(u(y.lastIndex+(v?0:w)),f.length))===M)w=l(f,w,g);else{if(k.push(f.slice(M,w)),k.length===b)return k;for(var T=1;T<=x.length-1;T++)if(k.push(x[T]),k.length===b)return k;w=M=L}}return k.push(f.slice(M)),k}]},!v)},3210:function(e,t,n){\"use strict\";var r=n(2109),i=n(3111).trim;r({target:\"String\",proto:!0,forced:n(6091)(\"trim\")},{trim:function(){return i(this)}})},2990:function(e,t,n){\"use strict\";var r=n(260),i=n(1048),a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"copyWithin\",function(e,t){return i.call(a(this),e,t,arguments.length>2?arguments[2]:void 0)})},8927:function(e,t,n){\"use strict\";var r=n(260),i=n(2092).every,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"every\",function(e){return i(a(this),e,arguments.length>1?arguments[1]:void 0)})},3105:function(e,t,n){\"use strict\";var r=n(260),i=n(1285),a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"fill\",function(e){return i.apply(a(this),arguments)})},5035:function(e,t,n){\"use strict\";var r=n(260),i=n(2092).filter,a=n(3074),o=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"filter\",function(e){var t=i(o(this),e,arguments.length>1?arguments[1]:void 0);return a(this,t)})},7174:function(e,t,n){\"use strict\";var r=n(260),i=n(2092).findIndex,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"findIndex\",function(e){return i(a(this),e,arguments.length>1?arguments[1]:void 0)})},4345:function(e,t,n){\"use strict\";var r=n(260),i=n(2092).find,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"find\",function(e){return i(a(this),e,arguments.length>1?arguments[1]:void 0)})},2846:function(e,t,n){\"use strict\";var r=n(260),i=n(2092).forEach,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"forEach\",function(e){i(a(this),e,arguments.length>1?arguments[1]:void 0)})},4731:function(e,t,n){\"use strict\";var r=n(260),i=n(1318).includes,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"includes\",function(e){return i(a(this),e,arguments.length>1?arguments[1]:void 0)})},7209:function(e,t,n){\"use strict\";var r=n(260),i=n(1318).indexOf,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"indexOf\",function(e){return i(a(this),e,arguments.length>1?arguments[1]:void 0)})},6319:function(e,t,n){\"use strict\";var r=n(7854),i=n(260),a=n(6992),o=n(5112)(\"iterator\"),s=r.Uint8Array,l=a.values,u=a.keys,d=a.entries,c=i.aTypedArray,f=i.exportTypedArrayMethod,p=s&&s.prototype[o],h=!!p&&(\"values\"==p.name||null==p.name),m=function(){return l.call(c(this))};f(\"entries\",function(){return d.call(c(this))}),f(\"keys\",function(){return u.call(c(this))}),f(\"values\",m,!h),f(o,m,!h)},8867:function(e,t,n){\"use strict\";var r=n(260),i=r.aTypedArray,a=r.exportTypedArrayMethod,o=[].join;a(\"join\",function(e){return o.apply(i(this),arguments)})},7789:function(e,t,n){\"use strict\";var r=n(260),i=n(6583),a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"lastIndexOf\",function(e){return i.apply(a(this),arguments)})},3739:function(e,t,n){\"use strict\";var r=n(260),i=n(2092).map,a=n(6707),o=r.aTypedArray,s=r.aTypedArrayConstructor;(0,r.exportTypedArrayMethod)(\"map\",function(e){return i(o(this),e,arguments.length>1?arguments[1]:void 0,function(e,t){return new(s(a(e,e.constructor)))(t)})})},4483:function(e,t,n){\"use strict\";var r=n(260),i=n(3671).right,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"reduceRight\",function(e){return i(a(this),e,arguments.length,arguments.length>1?arguments[1]:void 0)})},9368:function(e,t,n){\"use strict\";var r=n(260),i=n(3671).left,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"reduce\",function(e){return i(a(this),e,arguments.length,arguments.length>1?arguments[1]:void 0)})},2056:function(e,t,n){\"use strict\";var r=n(260),i=r.aTypedArray,a=r.exportTypedArrayMethod,o=Math.floor;a(\"reverse\",function(){for(var e,t=this,n=i(t).length,r=o(n/2),a=0;a<r;)e=t[a],t[a++]=t[--n],t[n]=e;return t})},3462:function(e,t,n){\"use strict\";var r=n(260),i=n(7466),a=n(4590),o=n(7908),s=n(7293),l=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"set\",function(e){l(this);var t=a(arguments.length>1?arguments[1]:void 0,1),n=this.length,r=o(e),s=i(r.length),u=0;if(s+t>n)throw RangeError(\"Wrong length\");for(;u<s;)this[t+u]=r[u++]},s(function(){new Int8Array(1).set({})}))},678:function(e,t,n){\"use strict\";var r=n(260),i=n(6707),a=n(7293),o=r.aTypedArray,s=r.aTypedArrayConstructor,l=r.exportTypedArrayMethod,u=[].slice;l(\"slice\",function(e,t){for(var n=u.call(o(this),e,t),r=i(this,this.constructor),a=0,l=n.length,d=new(s(r))(l);l>a;)d[a]=n[a++];return d},a(function(){new Int8Array(1).slice()}))},7462:function(e,t,n){\"use strict\";var r=n(260),i=n(2092).some,a=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"some\",function(e){return i(a(this),e,arguments.length>1?arguments[1]:void 0)})},3824:function(e,t,n){\"use strict\";var r=n(260),i=r.aTypedArray,a=r.exportTypedArrayMethod,o=[].sort;a(\"sort\",function(e){return o.call(i(this),e)})},5021:function(e,t,n){\"use strict\";var r=n(260),i=n(7466),a=n(1400),o=n(6707),s=r.aTypedArray;(0,r.exportTypedArrayMethod)(\"subarray\",function(e,t){var n=s(this),r=n.length,l=a(e,r);return new(o(n,n.constructor))(n.buffer,n.byteOffset+l*n.BYTES_PER_ELEMENT,i((void 0===t?r:a(t,r))-l))})},2974:function(e,t,n){\"use strict\";var r=n(7854),i=n(260),a=n(7293),o=r.Int8Array,s=i.aTypedArray,l=i.exportTypedArrayMethod,u=[].toLocaleString,d=[].slice,c=!!o&&a(function(){u.call(new o(1))});l(\"toLocaleString\",function(){return u.apply(c?d.call(s(this)):s(this),arguments)},a(function(){return[1,2].toLocaleString()!=new o([1,2]).toLocaleString()})||!a(function(){o.prototype.toLocaleString.call([1,2])}))},5016:function(e,t,n){\"use strict\";var r=n(260).exportTypedArrayMethod,i=n(7293),a=n(7854).Uint8Array,o=a&&a.prototype||{},s=[].toString,l=[].join;i(function(){s.call({})})&&(s=function(){return l.call(this)});var u=o.toString!=s;r(\"toString\",s,u)},2472:function(e,t,n){n(9843)(\"Uint8\",function(e){return function(t,n,r){return e(this,t,n,r)}})},4747:function(e,t,n){var r=n(7854),i=n(8324),a=n(8533),o=n(8880);for(var s in i){var l=r[s],u=l&&l.prototype;if(u&&u.forEach!==a)try{o(u,\"forEach\",a)}catch(e){u.forEach=a}}},3948:function(e,t,n){var r=n(7854),i=n(8324),a=n(6992),o=n(8880),s=n(5112),l=s(\"iterator\"),u=s(\"toStringTag\"),d=a.values;for(var c in i){var f=r[c],p=f&&f.prototype;if(p){if(p[l]!==d)try{o(p,l,d)}catch(e){p[l]=d}if(p[u]||o(p,u,c),i[c])for(var h in a)if(p[h]!==a[h])try{o(p,h,a[h])}catch(e){p[h]=a[h]}}}},1637:function(e,t,n){\"use strict\";n(6992);var r=n(2109),i=n(5005),a=n(590),o=n(1320),s=n(2248),l=n(8003),u=n(4994),d=n(9909),c=n(5787),f=n(6656),p=n(9974),h=n(648),m=n(9670),v=n(111),g=n(30),_=n(9114),y=n(8554),b=n(1246),M=n(5112),w=i(\"fetch\"),k=i(\"Headers\"),L=M(\"iterator\"),x=\"URLSearchParams\",T=x+\"Iterator\",S=d.set,D=d.getterFor(x),E=d.getterFor(T),Y=/\\+/g,A=Array(4),O=function(e){return A[e-1]||(A[e-1]=RegExp(\"((?:%[\\\\da-f]{2}){\"+e+\"})\",\"gi\"))},C=function(e){try{return decodeURIComponent(e)}catch(t){return e}},j=function(e){var t=e.replace(Y,\" \"),n=4;try{return decodeURIComponent(t)}catch(e){for(;n;)t=t.replace(O(n--),C);return t}},P=/[!'()~]|%20/g,H={\"!\":\"%21\",\"'\":\"%27\",\"(\":\"%28\",\")\":\"%29\",\"~\":\"%7E\",\"%20\":\"+\"},I=function(e){return H[e]},F=function(e){return encodeURIComponent(e).replace(P,I)},N=function(e,t){if(t)for(var n,r,i=t.split(\"&\"),a=0;a<i.length;)(n=i[a++]).length&&(r=n.split(\"=\"),e.push({key:j(r.shift()),value:j(r.join(\"=\"))}))},R=function(e){this.entries.length=0,N(this.entries,e)},V=function(e,t){if(e<t)throw TypeError(\"Not enough arguments\")},$=u(function(e,t){S(this,{type:T,iterator:y(D(e).entries),kind:t})},\"Iterator\",function(){var e=E(this),t=e.kind,n=e.iterator.next(),r=n.value;return n.done||(n.value=\"keys\"===t?r.key:\"values\"===t?r.value:[r.key,r.value]),n}),z=function(){c(this,z,x);var e,t,n,r,i,a,o,s,l,u=arguments.length>0?arguments[0]:void 0,d=[];if(S(this,{type:x,entries:d,updateURL:function(){},updateSearchParams:R}),void 0!==u)if(v(u))if(\"function\"==typeof(e=b(u)))for(n=(t=e.call(u)).next;!(r=n.call(t)).done;){if((o=(a=(i=y(m(r.value))).next).call(i)).done||(s=a.call(i)).done||!a.call(i).done)throw TypeError(\"Expected sequence with length 2\");d.push({key:o.value+\"\",value:s.value+\"\"})}else for(l in u)f(u,l)&&d.push({key:l,value:u[l]+\"\"});else N(d,\"string\"==typeof u?\"?\"===u.charAt(0)?u.slice(1):u:u+\"\")},W=z.prototype;s(W,{append:function(e,t){V(arguments.length,2);var n=D(this);n.entries.push({key:e+\"\",value:t+\"\"}),n.updateURL()},delete:function(e){V(arguments.length,1);for(var t=D(this),n=t.entries,r=e+\"\",i=0;i<n.length;)n[i].key===r?n.splice(i,1):i++;t.updateURL()},get:function(e){V(arguments.length,1);for(var t=D(this).entries,n=e+\"\",r=0;r<t.length;r++)if(t[r].key===n)return t[r].value;return null},getAll:function(e){V(arguments.length,1);for(var t=D(this).entries,n=e+\"\",r=[],i=0;i<t.length;i++)t[i].key===n&&r.push(t[i].value);return r},has:function(e){V(arguments.length,1);for(var t=D(this).entries,n=e+\"\",r=0;r<t.length;)if(t[r++].key===n)return!0;return!1},set:function(e,t){V(arguments.length,1);for(var n,r=D(this),i=r.entries,a=!1,o=e+\"\",s=t+\"\",l=0;l<i.length;l++)(n=i[l]).key===o&&(a?i.splice(l--,1):(a=!0,n.value=s));a||i.push({key:o,value:s}),r.updateURL()},sort:function(){var e,t,n,r=D(this),i=r.entries,a=i.slice();for(i.length=0,n=0;n<a.length;n++){for(e=a[n],t=0;t<n;t++)if(i[t].key>e.key){i.splice(t,0,e);break}t===n&&i.push(e)}r.updateURL()},forEach:function(e){for(var t,n=D(this).entries,r=p(e,arguments.length>1?arguments[1]:void 0,3),i=0;i<n.length;)r((t=n[i++]).value,t.key,this)},keys:function(){return new $(this,\"keys\")},values:function(){return new $(this,\"values\")},entries:function(){return new $(this,\"entries\")}},{enumerable:!0}),o(W,L,W.entries),o(W,\"toString\",function(){for(var e,t=D(this).entries,n=[],r=0;r<t.length;)e=t[r++],n.push(F(e.key)+\"=\"+F(e.value));return n.join(\"&\")},{enumerable:!0}),l(z,x),r({global:!0,forced:!a},{URLSearchParams:z}),a||\"function\"!=typeof w||\"function\"!=typeof k||r({global:!0,enumerable:!0,forced:!0},{fetch:function(e){var t,n,r,i=[e];return arguments.length>1&&(v(t=arguments[1])&&(n=t.body,h(n)===x&&((r=t.headers?new k(t.headers):new k).has(\"content-type\")||r.set(\"content-type\",\"application/x-www-form-urlencoded;charset=UTF-8\"),t=g(t,{body:_(0,String(n)),headers:_(0,r)}))),i.push(t)),w.apply(this,i)}}),e.exports={URLSearchParams:z,getState:D}},285:function(e,t,n){\"use strict\";n(8783);var r,i=n(2109),a=n(9781),o=n(590),s=n(7854),l=n(6048),u=n(1320),d=n(5787),c=n(6656),f=n(1574),p=n(8457),h=n(8710).codeAt,m=n(3197),v=n(8003),g=n(1637),_=n(9909),y=s.URL,b=g.URLSearchParams,M=g.getState,w=_.set,k=_.getterFor(\"URL\"),L=Math.floor,x=Math.pow,T=\"Invalid scheme\",S=\"Invalid host\",D=\"Invalid port\",E=/[A-Za-z]/,Y=/[\\d+-.A-Za-z]/,A=/\\d/,O=/^(0x|0X)/,C=/^[0-7]+$/,j=/^\\d+$/,P=/^[\\dA-Fa-f]+$/,H=/[\\u0000\\t\\u000A\\u000D #%/:?@[\\\\]]/,I=/[\\u0000\\t\\u000A\\u000D #/:?@[\\\\]]/,F=/^[\\u0000-\\u001F ]+|[\\u0000-\\u001F ]+$/g,N=/[\\t\\u000A\\u000D]/g,R=function(e,t){var n,r,i;if(\"[\"==t.charAt(0)){if(\"]\"!=t.charAt(t.length-1))return S;if(!(n=$(t.slice(1,-1))))return S;e.host=n}else if(Z(e)){if(t=m(t),H.test(t))return S;if(null===(n=V(t)))return S;e.host=n}else{if(I.test(t))return S;for(n=\"\",r=p(t),i=0;i<r.length;i++)n+=G(r[i],W);e.host=n}},V=function(e){var t,n,r,i,a,o,s,l=e.split(\".\");if(l.length&&\"\"==l[l.length-1]&&l.pop(),(t=l.length)>4)return e;for(n=[],r=0;r<t;r++){if(\"\"==(i=l[r]))return e;if(a=10,i.length>1&&\"0\"==i.charAt(0)&&(a=O.test(i)?16:8,i=i.slice(8==a?1:2)),\"\"===i)o=0;else{if(!(10==a?j:8==a?C:P).test(i))return e;o=parseInt(i,a)}n.push(o)}for(r=0;r<t;r++)if(o=n[r],r==t-1){if(o>=x(256,5-t))return null}else if(o>255)return null;for(s=n.pop(),r=0;r<n.length;r++)s+=n[r]*x(256,3-r);return s},$=function(e){var t,n,r,i,a,o,s,l=[0,0,0,0,0,0,0,0],u=0,d=null,c=0,f=function(){return e.charAt(c)};if(\":\"==f()){if(\":\"!=e.charAt(1))return;c+=2,d=++u}for(;f();){if(8==u)return;if(\":\"!=f()){for(t=n=0;n<4&&P.test(f());)t=16*t+parseInt(f(),16),c++,n++;if(\".\"==f()){if(0==n)return;if(c-=n,u>6)return;for(r=0;f();){if(i=null,r>0){if(!(\".\"==f()&&r<4))return;c++}if(!A.test(f()))return;for(;A.test(f());){if(a=parseInt(f(),10),null===i)i=a;else{if(0==i)return;i=10*i+a}if(i>255)return;c++}l[u]=256*l[u]+i,2!=++r&&4!=r||u++}if(4!=r)return;break}if(\":\"==f()){if(c++,!f())return}else if(f())return;l[u++]=t}else{if(null!==d)return;c++,d=++u}}if(null!==d)for(o=u-d,u=7;0!=u&&o>0;)s=l[u],l[u--]=l[d+o-1],l[d+--o]=s;else if(8!=u)return;return l},z=function(e){var t,n,r,i;if(\"number\"==typeof e){for(t=[],n=0;n<4;n++)t.unshift(e%256),e=L(e/256);return t.join(\".\")}if(\"object\"==typeof e){for(t=\"\",r=function(e){for(var t=null,n=1,r=null,i=0,a=0;a<8;a++)0!==e[a]?(i>n&&(t=r,n=i),r=null,i=0):(null===r&&(r=a),++i);return i>n&&(t=r,n=i),t}(e),n=0;n<8;n++)i&&0===e[n]||(i&&(i=!1),r===n?(t+=n?\":\":\"::\",i=!0):(t+=e[n].toString(16),n<7&&(t+=\":\")));return\"[\"+t+\"]\"}return e},W={},U=f({},W,{\" \":1,'\"':1,\"<\":1,\">\":1,\"`\":1}),B=f({},U,{\"#\":1,\"?\":1,\"{\":1,\"}\":1}),q=f({},B,{\"/\":1,\":\":1,\";\":1,\"=\":1,\"@\":1,\"[\":1,\"\\\\\":1,\"]\":1,\"^\":1,\"|\":1}),G=function(e,t){var n=h(e,0);return n>32&&n<127&&!c(t,e)?e:encodeURIComponent(e)},J={ftp:21,file:null,http:80,https:443,ws:80,wss:443},Z=function(e){return c(J,e.scheme)},K=function(e){return\"\"!=e.username||\"\"!=e.password},X=function(e){return!e.host||e.cannotBeABaseURL||\"file\"==e.scheme},Q=function(e,t){var n;return 2==e.length&&E.test(e.charAt(0))&&(\":\"==(n=e.charAt(1))||!t&&\"|\"==n)},ee=function(e){var t;return e.length>1&&Q(e.slice(0,2))&&(2==e.length||\"/\"===(t=e.charAt(2))||\"\\\\\"===t||\"?\"===t||\"#\"===t)},te=function(e){var t=e.path,n=t.length;!n||\"file\"==e.scheme&&1==n&&Q(t[0],!0)||t.pop()},ne=function(e){return\".\"===e||\"%2e\"===e.toLowerCase()},re=function(e){return\"..\"===(e=e.toLowerCase())||\"%2e.\"===e||\".%2e\"===e||\"%2e%2e\"===e},ie={},ae={},oe={},se={},le={},ue={},de={},ce={},fe={},pe={},he={},me={},ve={},ge={},_e={},ye={},be={},Me={},we={},ke={},Le={},xe=function(e,t,n,i){var a,o,s,l,u=n||ie,d=0,f=\"\",h=!1,m=!1,v=!1;for(n||(e.scheme=\"\",e.username=\"\",e.password=\"\",e.host=null,e.port=null,e.path=[],e.query=null,e.fragment=null,e.cannotBeABaseURL=!1,t=t.replace(F,\"\")),t=t.replace(N,\"\"),a=p(t);d<=a.length;){switch(o=a[d],u){case ie:if(!o||!E.test(o)){if(n)return T;u=oe;continue}f+=o.toLowerCase(),u=ae;break;case ae:if(o&&(Y.test(o)||\"+\"==o||\"-\"==o||\".\"==o))f+=o.toLowerCase();else{if(\":\"!=o){if(n)return T;f=\"\",u=oe,d=0;continue}if(n&&(Z(e)!=c(J,f)||\"file\"==f&&(K(e)||null!==e.port)||\"file\"==e.scheme&&!e.host))return;if(e.scheme=f,n)return void(Z(e)&&J[e.scheme]==e.port&&(e.port=null));f=\"\",\"file\"==e.scheme?u=ge:Z(e)&&i&&i.scheme==e.scheme?u=se:Z(e)?u=ce:\"/\"==a[d+1]?(u=le,d++):(e.cannotBeABaseURL=!0,e.path.push(\"\"),u=we)}break;case oe:if(!i||i.cannotBeABaseURL&&\"#\"!=o)return T;if(i.cannotBeABaseURL&&\"#\"==o){e.scheme=i.scheme,e.path=i.path.slice(),e.query=i.query,e.fragment=\"\",e.cannotBeABaseURL=!0,u=Le;break}u=\"file\"==i.scheme?ge:ue;continue;case se:if(\"/\"!=o||\"/\"!=a[d+1]){u=ue;continue}u=fe,d++;break;case le:if(\"/\"==o){u=pe;break}u=Me;continue;case ue:if(e.scheme=i.scheme,o==r)e.username=i.username,e.password=i.password,e.host=i.host,e.port=i.port,e.path=i.path.slice(),e.query=i.query;else if(\"/\"==o||\"\\\\\"==o&&Z(e))u=de;else if(\"?\"==o)e.username=i.username,e.password=i.password,e.host=i.host,e.port=i.port,e.path=i.path.slice(),e.query=\"\",u=ke;else{if(\"#\"!=o){e.username=i.username,e.password=i.password,e.host=i.host,e.port=i.port,e.path=i.path.slice(),e.path.pop(),u=Me;continue}e.username=i.username,e.password=i.password,e.host=i.host,e.port=i.port,e.path=i.path.slice(),e.query=i.query,e.fragment=\"\",u=Le}break;case de:if(!Z(e)||\"/\"!=o&&\"\\\\\"!=o){if(\"/\"!=o){e.username=i.username,e.password=i.password,e.host=i.host,e.port=i.port,u=Me;continue}u=pe}else u=fe;break;case ce:if(u=fe,\"/\"!=o||\"/\"!=f.charAt(d+1))continue;d++;break;case fe:if(\"/\"!=o&&\"\\\\\"!=o){u=pe;continue}break;case pe:if(\"@\"==o){h&&(f=\"%40\"+f),h=!0,s=p(f);for(var g=0;g<s.length;g++){var _=s[g];if(\":\"!=_||v){var y=G(_,q);v?e.password+=y:e.username+=y}else v=!0}f=\"\"}else if(o==r||\"/\"==o||\"?\"==o||\"#\"==o||\"\\\\\"==o&&Z(e)){if(h&&\"\"==f)return\"Invalid authority\";d-=p(f).length+1,f=\"\",u=he}else f+=o;break;case he:case me:if(n&&\"file\"==e.scheme){u=ye;continue}if(\":\"!=o||m){if(o==r||\"/\"==o||\"?\"==o||\"#\"==o||\"\\\\\"==o&&Z(e)){if(Z(e)&&\"\"==f)return S;if(n&&\"\"==f&&(K(e)||null!==e.port))return;if(l=R(e,f))return l;if(f=\"\",u=be,n)return;continue}\"[\"==o?m=!0:\"]\"==o&&(m=!1),f+=o}else{if(\"\"==f)return S;if(l=R(e,f))return l;if(f=\"\",u=ve,n==me)return}break;case ve:if(!A.test(o)){if(o==r||\"/\"==o||\"?\"==o||\"#\"==o||\"\\\\\"==o&&Z(e)||n){if(\"\"!=f){var b=parseInt(f,10);if(b>65535)return D;e.port=Z(e)&&b===J[e.scheme]?null:b,f=\"\"}if(n)return;u=be;continue}return D}f+=o;break;case ge:if(e.scheme=\"file\",\"/\"==o||\"\\\\\"==o)u=_e;else{if(!i||\"file\"!=i.scheme){u=Me;continue}if(o==r)e.host=i.host,e.path=i.path.slice(),e.query=i.query;else if(\"?\"==o)e.host=i.host,e.path=i.path.slice(),e.query=\"\",u=ke;else{if(\"#\"!=o){ee(a.slice(d).join(\"\"))||(e.host=i.host,e.path=i.path.slice(),te(e)),u=Me;continue}e.host=i.host,e.path=i.path.slice(),e.query=i.query,e.fragment=\"\",u=Le}}break;case _e:if(\"/\"==o||\"\\\\\"==o){u=ye;break}i&&\"file\"==i.scheme&&!ee(a.slice(d).join(\"\"))&&(Q(i.path[0],!0)?e.path.push(i.path[0]):e.host=i.host),u=Me;continue;case ye:if(o==r||\"/\"==o||\"\\\\\"==o||\"?\"==o||\"#\"==o){if(!n&&Q(f))u=Me;else if(\"\"==f){if(e.host=\"\",n)return;u=be}else{if(l=R(e,f))return l;if(\"localhost\"==e.host&&(e.host=\"\"),n)return;f=\"\",u=be}continue}f+=o;break;case be:if(Z(e)){if(u=Me,\"/\"!=o&&\"\\\\\"!=o)continue}else if(n||\"?\"!=o)if(n||\"#\"!=o){if(o!=r&&(u=Me,\"/\"!=o))continue}else e.fragment=\"\",u=Le;else e.query=\"\",u=ke;break;case Me:if(o==r||\"/\"==o||\"\\\\\"==o&&Z(e)||!n&&(\"?\"==o||\"#\"==o)){if(re(f)?(te(e),\"/\"==o||\"\\\\\"==o&&Z(e)||e.path.push(\"\")):ne(f)?\"/\"==o||\"\\\\\"==o&&Z(e)||e.path.push(\"\"):(\"file\"==e.scheme&&!e.path.length&&Q(f)&&(e.host&&(e.host=\"\"),f=f.charAt(0)+\":\"),e.path.push(f)),f=\"\",\"file\"==e.scheme&&(o==r||\"?\"==o||\"#\"==o))for(;e.path.length>1&&\"\"===e.path[0];)e.path.shift();\"?\"==o?(e.query=\"\",u=ke):\"#\"==o&&(e.fragment=\"\",u=Le)}else f+=G(o,B);break;case we:\"?\"==o?(e.query=\"\",u=ke):\"#\"==o?(e.fragment=\"\",u=Le):o!=r&&(e.path[0]+=G(o,W));break;case ke:n||\"#\"!=o?o!=r&&(\"'\"==o&&Z(e)?e.query+=\"%27\":e.query+=\"#\"==o?\"%23\":G(o,W)):(e.fragment=\"\",u=Le);break;case Le:o!=r&&(e.fragment+=G(o,U))}d++}},Te=function(e){var t,n,r=d(this,Te,\"URL\"),i=arguments.length>1?arguments[1]:void 0,o=String(e),s=w(r,{type:\"URL\"});if(void 0!==i)if(i instanceof Te)t=k(i);else if(n=xe(t={},String(i)))throw TypeError(n);if(n=xe(s,o,null,t))throw TypeError(n);var l=s.searchParams=new b,u=M(l);u.updateSearchParams(s.query),u.updateURL=function(){s.query=String(l)||null},a||(r.href=De.call(r),r.origin=Ee.call(r),r.protocol=Ye.call(r),r.username=Ae.call(r),r.password=Oe.call(r),r.host=Ce.call(r),r.hostname=je.call(r),r.port=Pe.call(r),r.pathname=He.call(r),r.search=Ie.call(r),r.searchParams=Fe.call(r),r.hash=Ne.call(r))},Se=Te.prototype,De=function(){var e=k(this),t=e.scheme,n=e.username,r=e.password,i=e.host,a=e.port,o=e.path,s=e.query,l=e.fragment,u=t+\":\";return null!==i?(u+=\"//\",K(e)&&(u+=n+(r?\":\"+r:\"\")+\"@\"),u+=z(i),null!==a&&(u+=\":\"+a)):\"file\"==t&&(u+=\"//\"),u+=e.cannotBeABaseURL?o[0]:o.length?\"/\"+o.join(\"/\"):\"\",null!==s&&(u+=\"?\"+s),null!==l&&(u+=\"#\"+l),u},Ee=function(){var e=k(this),t=e.scheme,n=e.port;if(\"blob\"==t)try{return new URL(t.path[0]).origin}catch(e){return\"null\"}return\"file\"!=t&&Z(e)?t+\"://\"+z(e.host)+(null!==n?\":\"+n:\"\"):\"null\"},Ye=function(){return k(this).scheme+\":\"},Ae=function(){return k(this).username},Oe=function(){return k(this).password},Ce=function(){var e=k(this),t=e.host,n=e.port;return null===t?\"\":null===n?z(t):z(t)+\":\"+n},je=function(){var e=k(this).host;return null===e?\"\":z(e)},Pe=function(){var e=k(this).port;return null===e?\"\":String(e)},He=function(){var e=k(this),t=e.path;return e.cannotBeABaseURL?t[0]:t.length?\"/\"+t.join(\"/\"):\"\"},Ie=function(){var e=k(this).query;return e?\"?\"+e:\"\"},Fe=function(){return k(this).searchParams},Ne=function(){var e=k(this).fragment;return e?\"#\"+e:\"\"},Re=function(e,t){return{get:e,set:t,configurable:!0,enumerable:!0}};if(a&&l(Se,{href:Re(De,function(e){var t=k(this),n=String(e),r=xe(t,n);if(r)throw TypeError(r);M(t.searchParams).updateSearchParams(t.query)}),origin:Re(Ee),protocol:Re(Ye,function(e){var t=k(this);xe(t,String(e)+\":\",ie)}),username:Re(Ae,function(e){var t=k(this),n=p(String(e));if(!X(t)){t.username=\"\";for(var r=0;r<n.length;r++)t.username+=G(n[r],q)}}),password:Re(Oe,function(e){var t=k(this),n=p(String(e));if(!X(t)){t.password=\"\";for(var r=0;r<n.length;r++)t.password+=G(n[r],q)}}),host:Re(Ce,function(e){var t=k(this);t.cannotBeABaseURL||xe(t,String(e),he)}),hostname:Re(je,function(e){var t=k(this);t.cannotBeABaseURL||xe(t,String(e),me)}),port:Re(Pe,function(e){var t=k(this);X(t)||(\"\"==(e=String(e))?t.port=null:xe(t,e,ve))}),pathname:Re(He,function(e){var t=k(this);t.cannotBeABaseURL||(t.path=[],xe(t,e+\"\",be))}),search:Re(Ie,function(e){var t=k(this);\"\"==(e=String(e))?t.query=null:(\"?\"==e.charAt(0)&&(e=e.slice(1)),t.query=\"\",xe(t,e,ke)),M(t.searchParams).updateSearchParams(t.query)}),searchParams:Re(Fe),hash:Re(Ne,function(e){var t=k(this);\"\"!=(e=String(e))?(\"#\"==e.charAt(0)&&(e=e.slice(1)),t.fragment=\"\",xe(t,e,Le)):t.fragment=null})}),u(Se,\"toJSON\",function(){return De.call(this)},{enumerable:!0}),u(Se,\"toString\",function(){return De.call(this)},{enumerable:!0}),y){var Ve=y.createObjectURL,$e=y.revokeObjectURL;Ve&&u(Te,\"createObjectURL\",function(e){return Ve.apply(y,arguments)}),$e&&u(Te,\"revokeObjectURL\",function(e){return $e.apply(y,arguments)})}v(Te,\"URL\"),i({global:!0,forced:!o,sham:!a},{URL:Te})}},t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.g=function(){if(\"object\"==typeof globalThis)return globalThis;try{return this||new Function(\"return this\")()}catch(e){if(\"object\"==typeof window)return window}}(),n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})};var r={};return function(){\"use strict\";n.r(r),n.d(r,{Dropzone:function(){return b},default:function(){return x}});n(2222),n(7327),n(2772),n(6992),n(1249),n(7042),n(561),n(8264),n(8309),n(489),n(1539),n(4916),n(9714),n(8783),n(4723),n(5306),n(3123),n(3210),n(2472),n(2990),n(8927),n(3105),n(5035),n(4345),n(7174),n(2846),n(4731),n(7209),n(6319),n(8867),n(7789),n(3739),n(9368),n(4483),n(2056),n(3462),n(678),n(7462),n(3824),n(5021),n(2974),n(5016),n(4747),n(3948),n(285);function e(e,n){var r;if(\"undefined\"==typeof Symbol||null==e[Symbol.iterator]){if(Array.isArray(e)||(r=function(e,n){if(!e)return;if(\"string\"==typeof e)return t(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);\"Object\"===r&&e.constructor&&(r=e.constructor.name);if(\"Map\"===r||\"Set\"===r)return Array.from(e);if(\"Arguments\"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return t(e,n)}(e))||n&&e&&\"number\"==typeof e.length){r&&(e=r);var i=0,a=function(){};return{s:a,n:function(){return i>=e.length?{done:!0}:{done:!1,value:e[i++]}},e:function(e){throw e},f:a}}throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}var o,s=!0,l=!1;return{s:function(){r=e[Symbol.iterator]()},n:function(){var e=r.next();return s=e.done,e},e:function(e){l=!0,o=e},f:function(){try{s||null==r.return||r.return()}finally{if(l)throw o}}}}function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function i(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\"value\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}var a=function(){function t(){!function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}(this,t)}var n,r,a;return n=t,r=[{key:\"on\",value:function(e,t){return this._callbacks=this._callbacks||{},this._callbacks[e]||(this._callbacks[e]=[]),this._callbacks[e].push(t),this}},{key:\"emit\",value:function(t){this._callbacks=this._callbacks||{};for(var n=this._callbacks[t],r=arguments.length,i=new Array(r>1?r-1:0),a=1;a<r;a++)i[a-1]=arguments[a];if(n){var o,s=e(n,!0);try{for(s.s();!(o=s.n()).done;)o.value.apply(this,i)}catch(e){s.e(e)}finally{s.f()}}return this.element&&this.element.dispatchEvent(this.makeEvent(\"dropzone:\"+t,{args:i})),this}},{key:\"makeEvent\",value:function(e,t){var n={bubbles:!0,cancelable:!0,detail:t};if(\"function\"==typeof window.CustomEvent)return new CustomEvent(e,n);var r=document.createEvent(\"CustomEvent\");return r.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),r}},{key:\"off\",value:function(e,t){if(!this._callbacks||0===arguments.length)return this._callbacks={},this;var n=this._callbacks[e];if(!n)return this;if(1===arguments.length)return delete this._callbacks[e],this;for(var r=0;r<n.length;r++)if(n[r]===t){n.splice(r,1);break}return this}}],r&&i(n.prototype,r),a&&i(n,a),t}();function o(e,t){var n;if(\"undefined\"==typeof Symbol||null==e[Symbol.iterator]){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if(\"string\"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);\"Object\"===n&&e.constructor&&(n=e.constructor.name);if(\"Map\"===n||\"Set\"===n)return Array.from(e);if(\"Arguments\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return s(e,t)}(e))||t&&e&&\"number\"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}var a,o=!0,l=!1;return{s:function(){n=e[Symbol.iterator]()},n:function(){var e=n.next();return o=e.done,e},e:function(e){l=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(l)throw a}}}}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}var l={url:null,method:\"post\",withCredentials:!1,timeout:null,parallelUploads:2,uploadMultiple:!1,chunking:!1,forceChunking:!1,chunkSize:2e6,parallelChunkUploads:!1,retryChunks:!1,retryChunksLimit:3,maxFilesize:256,paramName:\"file\",createImageThumbnails:!0,maxThumbnailFilesize:10,thumbnailWidth:120,thumbnailHeight:120,thumbnailMethod:\"crop\",resizeWidth:null,resizeHeight:null,resizeMimeType:null,resizeQuality:.8,resizeMethod:\"contain\",filesizeBase:1e3,maxFiles:null,headers:null,clickable:!0,ignoreHiddenFiles:!0,acceptedFiles:null,acceptedMimeTypes:null,autoProcessQueue:!0,autoQueue:!0,addRemoveLinks:!1,previewsContainer:null,disablePreviews:!1,hiddenInputContainer:\"body\",capture:null,renameFilename:null,renameFile:null,forceFallback:!1,dictDefaultMessage:\"Drop files here to upload\",dictFallbackMessage:\"Your browser does not support drag'n'drop file uploads.\",dictFallbackText:\"Please use the fallback form below to upload your files like in the olden days.\",dictFileTooBig:\"File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.\",dictInvalidFileType:\"You can't upload files of this type.\",dictResponseError:\"Server responded with {{statusCode}} code.\",dictCancelUpload:\"Cancel upload\",dictUploadCanceled:\"Upload canceled.\",dictCancelUploadConfirmation:\"Are you sure you want to cancel this upload?\",dictRemoveFile:\"Remove file\",dictRemoveFileConfirmation:null,dictMaxFilesExceeded:\"You can not upload any more files.\",dictFileSizeUnits:{tb:\"TB\",gb:\"GB\",mb:\"MB\",kb:\"KB\",b:\"b\"},init:function(){},params:function(e,t,n){if(n)return{dzuuid:n.file.upload.uuid,dzchunkindex:n.index,dztotalfilesize:n.file.size,dzchunksize:this.options.chunkSize,dztotalchunkcount:n.file.upload.totalChunkCount,dzchunkbyteoffset:n.index*this.options.chunkSize}},accept:function(e,t){return t()},chunksUploaded:function(e,t){t()},fallback:function(){var e;this.element.className=\"\".concat(this.element.className,\" dz-browser-not-supported\");var t,n=o(this.element.getElementsByTagName(\"div\"),!0);try{for(n.s();!(t=n.n()).done;){var r=t.value;if(/(^| )dz-message($| )/.test(r.className)){e=r,r.className=\"dz-message\";break}}}catch(e){n.e(e)}finally{n.f()}e||(e=b.createElement('<div class=\"dz-message\"><span></span></div>'),this.element.appendChild(e));var i=e.getElementsByTagName(\"span\")[0];return i&&(null!=i.textContent?i.textContent=this.options.dictFallbackMessage:null!=i.innerText&&(i.innerText=this.options.dictFallbackMessage)),this.element.appendChild(this.getFallbackForm())},resize:function(e,t,n,r){var i={srcX:0,srcY:0,srcWidth:e.width,srcHeight:e.height},a=e.width/e.height;null==t&&null==n?(t=i.srcWidth,n=i.srcHeight):null==t?t=n*a:null==n&&(n=t/a);var o=(t=Math.min(t,i.srcWidth))/(n=Math.min(n,i.srcHeight));if(i.srcWidth>t||i.srcHeight>n)if(\"crop\"===r)a>o?(i.srcHeight=e.height,i.srcWidth=i.srcHeight*o):(i.srcWidth=e.width,i.srcHeight=i.srcWidth/o);else{if(\"contain\"!==r)throw new Error(\"Unknown resizeMethod '\".concat(r,\"'\"));a>o?n=t/a:t=n*a}return i.srcX=(e.width-i.srcWidth)/2,i.srcY=(e.height-i.srcHeight)/2,i.trgWidth=t,i.trgHeight=n,i},transformFile:function(e,t){return(this.options.resizeWidth||this.options.resizeHeight)&&e.type.match(/image.*/)?this.resizeImage(e,this.options.resizeWidth,this.options.resizeHeight,this.options.resizeMethod,t):t(e)},previewTemplate:'<div class=\"dz-preview dz-file-preview\"> <div class=\"dz-image\"><img data-dz-thumbnail/></div> <div class=\"dz-details\"> <div class=\"dz-size\"><span data-dz-size></span></div> <div class=\"dz-filename\"><span data-dz-name></span></div> </div> <div class=\"dz-progress\"> <span class=\"dz-upload\" data-dz-uploadprogress></span> </div> <div class=\"dz-error-message\"><span data-dz-errormessage></span></div> <div class=\"dz-success-mark\"> <svg width=\"54px\" height=\"54px\" viewBox=\"0 0 54 54\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"> <title>Check</title> <g stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\"> <path d=\"M23.5,31.8431458 L17.5852419,25.9283877 C16.0248253,24.3679711 13.4910294,24.366835 11.9289322,25.9289322 C10.3700136,27.4878508 10.3665912,30.0234455 11.9283877,31.5852419 L20.4147581,40.0716123 C20.5133999,40.1702541 20.6159315,40.2626649 20.7218615,40.3488435 C22.2835669,41.8725651 24.794234,41.8626202 26.3461564,40.3106978 L43.3106978,23.3461564 C44.8771021,21.7797521 44.8758057,19.2483887 43.3137085,17.6862915 C41.7547899,16.1273729 39.2176035,16.1255422 37.6538436,17.6893022 L23.5,31.8431458 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z\" stroke-opacity=\"0.198794158\" stroke=\"#747474\" fill-opacity=\"0.816519475\" fill=\"#FFFFFF\"></path> </g> </svg> </div> <div class=\"dz-error-mark\"> <svg width=\"54px\" height=\"54px\" viewBox=\"0 0 54 54\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"> <title>Error</title> <g stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\"> <g stroke=\"#747474\" stroke-opacity=\"0.198794158\" fill=\"#FFFFFF\" fill-opacity=\"0.816519475\"> <path d=\"M32.6568542,29 L38.3106978,23.3461564 C39.8771021,21.7797521 39.8758057,19.2483887 38.3137085,17.6862915 C36.7547899,16.1273729 34.2176035,16.1255422 32.6538436,17.6893022 L27,23.3431458 L21.3461564,17.6893022 C19.7823965,16.1255422 17.2452101,16.1273729 15.6862915,17.6862915 C14.1241943,19.2483887 14.1228979,21.7797521 15.6893022,23.3461564 L21.3431458,29 L15.6893022,34.6538436 C14.1228979,36.2202479 14.1241943,38.7516113 15.6862915,40.3137085 C17.2452101,41.8726271 19.7823965,41.8744578 21.3461564,40.3106978 L27,34.6568542 L32.6538436,40.3106978 C34.2176035,41.8744578 36.7547899,41.8726271 38.3137085,40.3137085 C39.8758057,38.7516113 39.8771021,36.2202479 38.3106978,34.6538436 L32.6568542,29 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z\"></path> </g> </g> </svg> </div> </div> ',drop:function(e){return this.element.classList.remove(\"dz-drag-hover\")},dragstart:function(e){},dragend:function(e){return this.element.classList.remove(\"dz-drag-hover\")},dragenter:function(e){return this.element.classList.add(\"dz-drag-hover\")},dragover:function(e){return this.element.classList.add(\"dz-drag-hover\")},dragleave:function(e){return this.element.classList.remove(\"dz-drag-hover\")},paste:function(e){},reset:function(){return this.element.classList.remove(\"dz-started\")},addedfile:function(e){var t=this;if(this.element===this.previewsContainer&&this.element.classList.add(\"dz-started\"),this.previewsContainer&&!this.options.disablePreviews){e.previewElement=b.createElement(this.options.previewTemplate.trim()),e.previewTemplate=e.previewElement,this.previewsContainer.appendChild(e.previewElement);var n,r=o(e.previewElement.querySelectorAll(\"[data-dz-name]\"),!0);try{for(r.s();!(n=r.n()).done;){var i=n.value;i.textContent=e.name}}catch(e){r.e(e)}finally{r.f()}var a,s=o(e.previewElement.querySelectorAll(\"[data-dz-size]\"),!0);try{for(s.s();!(a=s.n()).done;)(i=a.value).innerHTML=this.filesize(e.size)}catch(e){s.e(e)}finally{s.f()}this.options.addRemoveLinks&&(e._removeLink=b.createElement('<a class=\"dz-remove\" href=\"javascript:undefined;\" data-dz-remove>'.concat(this.options.dictRemoveFile,\"</a>\")),e.previewElement.appendChild(e._removeLink));var l,u=function(n){return n.preventDefault(),n.stopPropagation(),e.status===b.UPLOADING?b.confirm(t.options.dictCancelUploadConfirmation,function(){return t.removeFile(e)}):t.options.dictRemoveFileConfirmation?b.confirm(t.options.dictRemoveFileConfirmation,function(){return t.removeFile(e)}):t.removeFile(e)},d=o(e.previewElement.querySelectorAll(\"[data-dz-remove]\"),!0);try{for(d.s();!(l=d.n()).done;){l.value.addEventListener(\"click\",u)}}catch(e){d.e(e)}finally{d.f()}}},removedfile:function(e){return null!=e.previewElement&&null!=e.previewElement.parentNode&&e.previewElement.parentNode.removeChild(e.previewElement),this._updateMaxFilesReachedClass()},thumbnail:function(e,t){if(e.previewElement){e.previewElement.classList.remove(\"dz-file-preview\");var n,r=o(e.previewElement.querySelectorAll(\"[data-dz-thumbnail]\"),!0);try{for(r.s();!(n=r.n()).done;){var i=n.value;i.alt=e.name,i.src=t}}catch(e){r.e(e)}finally{r.f()}return setTimeout(function(){return e.previewElement.classList.add(\"dz-image-preview\")},1)}},error:function(e,t){if(e.previewElement){e.previewElement.classList.add(\"dz-error\"),\"string\"!=typeof t&&t.error&&(t=t.error);var n,r=o(e.previewElement.querySelectorAll(\"[data-dz-errormessage]\"),!0);try{for(r.s();!(n=r.n()).done;){n.value.textContent=t}}catch(e){r.e(e)}finally{r.f()}}},errormultiple:function(){},processing:function(e){if(e.previewElement&&(e.previewElement.classList.add(\"dz-processing\"),e._removeLink))return e._removeLink.innerHTML=this.options.dictCancelUpload},processingmultiple:function(){},uploadprogress:function(e,t,n){if(e.previewElement){var r,i=o(e.previewElement.querySelectorAll(\"[data-dz-uploadprogress]\"),!0);try{for(i.s();!(r=i.n()).done;){var a=r.value;\"PROGRESS\"===a.nodeName?a.value=t:a.style.width=\"\".concat(t,\"%\")}}catch(e){i.e(e)}finally{i.f()}}},totaluploadprogress:function(){},sending:function(){},sendingmultiple:function(){},success:function(e){if(e.previewElement)return e.previewElement.classList.add(\"dz-success\")},successmultiple:function(){},canceled:function(e){return this.emit(\"error\",e,this.options.dictUploadCanceled)},canceledmultiple:function(){},complete:function(e){if(e._removeLink&&(e._removeLink.innerHTML=this.options.dictRemoveFile),e.previewElement)return e.previewElement.classList.add(\"dz-complete\")},completemultiple:function(){},maxfilesexceeded:function(){},maxfilesreached:function(){},queuecomplete:function(){},addedfiles:function(){}};function u(e){return u=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},u(e)}function d(e,t){var n;if(\"undefined\"==typeof Symbol||null==e[Symbol.iterator]){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if(\"string\"==typeof e)return c(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);\"Object\"===n&&e.constructor&&(n=e.constructor.name);if(\"Map\"===n||\"Set\"===n)return Array.from(e);if(\"Arguments\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return c(e,t)}(e))||t&&e&&\"number\"==typeof e.length){n&&(e=n);var r=0,i=function(){};return{s:i,n:function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}var a,o=!0,s=!1;return{s:function(){n=e[Symbol.iterator]()},n:function(){var e=n.next();return o=e.done,e},e:function(e){s=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(s)throw a}}}}function c(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function f(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}function p(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\"value\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function h(e,t,n){return t&&p(e.prototype,t),n&&p(e,n),e}function m(e,t){return m=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},m(e,t)}function v(e){var t=function(){if(\"undefined\"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if(\"function\"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}();return function(){var n,r=y(e);if(t){var i=y(this).constructor;n=Reflect.construct(r,arguments,i)}else n=r.apply(this,arguments);return g(this,n)}}function g(e,t){return!t||\"object\"!==u(t)&&\"function\"!=typeof t?_(e):t}function _(e){if(void 0===e)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return e}function y(e){return y=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},y(e)}var b=function(e){!function(e,t){if(\"function\"!=typeof t&&null!==t)throw new TypeError(\"Super expression must either be null or a function\");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&m(e,t)}(n,e);var t=v(n);function n(e,r){var i,a,o;if(f(this,n),(i=t.call(this)).element=e,i.version=n.version,i.clickableElements=[],i.listeners=[],i.files=[],\"string\"==typeof i.element&&(i.element=document.querySelector(i.element)),!i.element||null==i.element.nodeType)throw new Error(\"Invalid dropzone element.\");if(i.element.dropzone)throw new Error(\"Dropzone already attached.\");n.instances.push(_(i)),i.element.dropzone=_(i);var s=null!=(o=n.optionsForElement(i.element))?o:{};if(i.options=n.extend({},l,s,null!=r?r:{}),i.options.previewTemplate=i.options.previewTemplate.replace(/\\n*/g,\"\"),i.options.forceFallback||!n.isBrowserSupported())return g(i,i.options.fallback.call(_(i)));if(null==i.options.url&&(i.options.url=i.element.getAttribute(\"action\")),!i.options.url)throw new Error(\"No URL provided.\");if(i.options.acceptedFiles&&i.options.acceptedMimeTypes)throw new Error(\"You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated.\");if(i.options.uploadMultiple&&i.options.chunking)throw new Error(\"You cannot set both: uploadMultiple and chunking.\");return i.options.acceptedMimeTypes&&(i.options.acceptedFiles=i.options.acceptedMimeTypes,delete i.options.acceptedMimeTypes),null!=i.options.renameFilename&&(i.options.renameFile=function(e){return i.options.renameFilename.call(_(i),e.name,e)}),\"string\"==typeof i.options.method&&(i.options.method=i.options.method.toUpperCase()),(a=i.getExistingFallback())&&a.parentNode&&a.parentNode.removeChild(a),!1!==i.options.previewsContainer&&(i.options.previewsContainer?i.previewsContainer=n.getElement(i.options.previewsContainer,\"previewsContainer\"):i.previewsContainer=i.element),i.options.clickable&&(!0===i.options.clickable?i.clickableElements=[i.element]:i.clickableElements=n.getElements(i.options.clickable,\"clickable\")),i.init(),i}return h(n,[{key:\"getAcceptedFiles\",value:function(){return this.files.filter(function(e){return e.accepted}).map(function(e){return e})}},{key:\"getRejectedFiles\",value:function(){return this.files.filter(function(e){return!e.accepted}).map(function(e){return e})}},{key:\"getFilesWithStatus\",value:function(e){return this.files.filter(function(t){return t.status===e}).map(function(e){return e})}},{key:\"getQueuedFiles\",value:function(){return this.getFilesWithStatus(n.QUEUED)}},{key:\"getUploadingFiles\",value:function(){return this.getFilesWithStatus(n.UPLOADING)}},{key:\"getAddedFiles\",value:function(){return this.getFilesWithStatus(n.ADDED)}},{key:\"getActiveFiles\",value:function(){return this.files.filter(function(e){return e.status===n.UPLOADING||e.status===n.QUEUED}).map(function(e){return e})}},{key:\"init\",value:function(){var e=this;if(\"form\"===this.element.tagName&&this.element.setAttribute(\"enctype\",\"multipart/form-data\"),this.element.classList.contains(\"dropzone\")&&!this.element.querySelector(\".dz-message\")&&this.element.appendChild(n.createElement('<div class=\"dz-default dz-message\"><button class=\"dz-button\" type=\"button\">'.concat(this.options.dictDefaultMessage,\"</button></div>\"))),this.clickableElements.length){!function t(){e.hiddenFileInput&&e.hiddenFileInput.parentNode.removeChild(e.hiddenFileInput),e.hiddenFileInput=document.createElement(\"input\"),e.hiddenFileInput.setAttribute(\"type\",\"file\"),(null===e.options.maxFiles||e.options.maxFiles>1)&&e.hiddenFileInput.setAttribute(\"multiple\",\"multiple\"),e.hiddenFileInput.className=\"dz-hidden-input\",null!==e.options.acceptedFiles&&e.hiddenFileInput.setAttribute(\"accept\",e.options.acceptedFiles),null!==e.options.capture&&e.hiddenFileInput.setAttribute(\"capture\",e.options.capture),e.hiddenFileInput.setAttribute(\"tabindex\",\"-1\"),e.hiddenFileInput.style.visibility=\"hidden\",e.hiddenFileInput.style.position=\"absolute\",e.hiddenFileInput.style.top=\"0\",e.hiddenFileInput.style.left=\"0\",e.hiddenFileInput.style.height=\"0\",e.hiddenFileInput.style.width=\"0\",n.getElement(e.options.hiddenInputContainer,\"hiddenInputContainer\").appendChild(e.hiddenFileInput),e.hiddenFileInput.addEventListener(\"change\",function(){var n=e.hiddenFileInput.files;if(n.length){var r,i=d(n,!0);try{for(i.s();!(r=i.n()).done;){var a=r.value;e.addFile(a)}}catch(e){i.e(e)}finally{i.f()}}e.emit(\"addedfiles\",n),t()})}()}this.URL=null!==window.URL?window.URL:window.webkitURL;var t,r=d(this.events,!0);try{for(r.s();!(t=r.n()).done;){var i=t.value;this.on(i,this.options[i])}}catch(e){r.e(e)}finally{r.f()}this.on(\"uploadprogress\",function(){return e.updateTotalUploadProgress()}),this.on(\"removedfile\",function(){return e.updateTotalUploadProgress()}),this.on(\"canceled\",function(t){return e.emit(\"complete\",t)}),this.on(\"complete\",function(t){if(0===e.getAddedFiles().length&&0===e.getUploadingFiles().length&&0===e.getQueuedFiles().length)return setTimeout(function(){return e.emit(\"queuecomplete\")},0)});var a=function(e){if(function(e){if(e.dataTransfer.types)for(var t=0;t<e.dataTransfer.types.length;t++)if(\"Files\"===e.dataTransfer.types[t])return!0;return!1}(e))return e.stopPropagation(),e.preventDefault?e.preventDefault():e.returnValue=!1};return this.listeners=[{element:this.element,events:{dragstart:function(t){return e.emit(\"dragstart\",t)},dragenter:function(t){return a(t),e.emit(\"dragenter\",t)},dragover:function(t){var n;try{n=t.dataTransfer.effectAllowed}catch(e){}return t.dataTransfer.dropEffect=\"move\"===n||\"linkMove\"===n?\"move\":\"copy\",a(t),e.emit(\"dragover\",t)},dragleave:function(t){return e.emit(\"dragleave\",t)},drop:function(t){return a(t),e.drop(t)},dragend:function(t){return e.emit(\"dragend\",t)}}}],this.clickableElements.forEach(function(t){return e.listeners.push({element:t,events:{click:function(r){return(t!==e.element||r.target===e.element||n.elementInside(r.target,e.element.querySelector(\".dz-message\")))&&e.hiddenFileInput.click(),!0}}})}),this.enable(),this.options.init.call(this)}},{key:\"destroy\",value:function(){return this.disable(),this.removeAllFiles(!0),(null!=this.hiddenFileInput?this.hiddenFileInput.parentNode:void 0)&&(this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput),this.hiddenFileInput=null),delete this.element.dropzone,n.instances.splice(n.instances.indexOf(this),1)}},{key:\"updateTotalUploadProgress\",value:function(){var e,t=0,n=0;if(this.getActiveFiles().length){var r,i=d(this.getActiveFiles(),!0);try{for(i.s();!(r=i.n()).done;){var a=r.value;t+=a.upload.bytesSent,n+=a.upload.total}}catch(e){i.e(e)}finally{i.f()}e=100*t/n}else e=100;return this.emit(\"totaluploadprogress\",e,n,t)}},{key:\"_getParamName\",value:function(e){return\"function\"==typeof this.options.paramName?this.options.paramName(e):\"\".concat(this.options.paramName).concat(this.options.uploadMultiple?\"[\".concat(e,\"]\"):\"\")}},{key:\"_renameFile\",value:function(e){return\"function\"!=typeof this.options.renameFile?e.name:this.options.renameFile(e)}},{key:\"getFallbackForm\",value:function(){var e,t;if(e=this.getExistingFallback())return e;var r='<div class=\"dz-fallback\">';this.options.dictFallbackText&&(r+=\"<p>\".concat(this.options.dictFallbackText,\"</p>\")),r+='<input type=\"file\" name=\"'.concat(this._getParamName(0),'\" ').concat(this.options.uploadMultiple?'multiple=\"multiple\"':void 0,' /><input type=\"submit\" value=\"Upload!\"></div>');var i=n.createElement(r);return\"FORM\"!==this.element.tagName?(t=n.createElement('<form action=\"'.concat(this.options.url,'\" enctype=\"multipart/form-data\" method=\"').concat(this.options.method,'\"></form>'))).appendChild(i):(this.element.setAttribute(\"enctype\",\"multipart/form-data\"),this.element.setAttribute(\"method\",this.options.method)),null!=t?t:i}},{key:\"getExistingFallback\",value:function(){for(var e=function(e){var t,n=d(e,!0);try{for(n.s();!(t=n.n()).done;){var r=t.value;if(/(^| )fallback($| )/.test(r.className))return r}}catch(e){n.e(e)}finally{n.f()}},t=0,n=[\"div\",\"form\"];t<n.length;t++){var r,i=n[t];if(r=e(this.element.getElementsByTagName(i)))return r}}},{key:\"setupEventListeners\",value:function(){return this.listeners.map(function(e){return function(){var t=[];for(var n in e.events){var r=e.events[n];t.push(e.element.addEventListener(n,r,!1))}return t}()})}},{key:\"removeEventListeners\",value:function(){return this.listeners.map(function(e){return function(){var t=[];for(var n in e.events){var r=e.events[n];t.push(e.element.removeEventListener(n,r,!1))}return t}()})}},{key:\"disable\",value:function(){var e=this;return this.clickableElements.forEach(function(e){return e.classList.remove(\"dz-clickable\")}),this.removeEventListeners(),this.disabled=!0,this.files.map(function(t){return e.cancelUpload(t)})}},{key:\"enable\",value:function(){return delete this.disabled,this.clickableElements.forEach(function(e){return e.classList.add(\"dz-clickable\")}),this.setupEventListeners()}},{key:\"filesize\",value:function(e){var t=0,n=\"b\";if(e>0){for(var r=[\"tb\",\"gb\",\"mb\",\"kb\",\"b\"],i=0;i<r.length;i++){var a=r[i];if(e>=Math.pow(this.options.filesizeBase,4-i)/10){t=e/Math.pow(this.options.filesizeBase,4-i),n=a;break}}t=Math.round(10*t)/10}return\"<strong>\".concat(t,\"</strong> \").concat(this.options.dictFileSizeUnits[n])}},{key:\"_updateMaxFilesReachedClass\",value:function(){return null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(this.getAcceptedFiles().length===this.options.maxFiles&&this.emit(\"maxfilesreached\",this.files),this.element.classList.add(\"dz-max-files-reached\")):this.element.classList.remove(\"dz-max-files-reached\")}},{key:\"drop\",value:function(e){if(e.dataTransfer){this.emit(\"drop\",e);for(var t=[],n=0;n<e.dataTransfer.files.length;n++)t[n]=e.dataTransfer.files[n];if(t.length){var r=e.dataTransfer.items;r&&r.length&&null!=r[0].webkitGetAsEntry?this._addFilesFromItems(r):this.handleFiles(t)}this.emit(\"addedfiles\",t)}}},{key:\"paste\",value:function(e){if(null!=(t=null!=e?e.clipboardData:void 0,n=function(e){return e.items},null!=t?n(t):void 0)){var t,n;this.emit(\"paste\",e);var r=e.clipboardData.items;return r.length?this._addFilesFromItems(r):void 0}}},{key:\"handleFiles\",value:function(e){var t,n=d(e,!0);try{for(n.s();!(t=n.n()).done;){var r=t.value;this.addFile(r)}}catch(e){n.e(e)}finally{n.f()}}},{key:\"_addFilesFromItems\",value:function(e){var t=this;return function(){var n,r=[],i=d(e,!0);try{for(i.s();!(n=i.n()).done;){var a,o=n.value;null!=o.webkitGetAsEntry&&(a=o.webkitGetAsEntry())?a.isFile?r.push(t.addFile(o.getAsFile())):a.isDirectory?r.push(t._addFilesFromDirectory(a,a.name)):r.push(void 0):null!=o.getAsFile&&(null==o.kind||\"file\"===o.kind)?r.push(t.addFile(o.getAsFile())):r.push(void 0)}}catch(e){i.e(e)}finally{i.f()}return r}()}},{key:\"_addFilesFromDirectory\",value:function(e,t){var n=this,r=e.createReader(),i=function(e){return t=console,n=\"log\",r=function(t){return t.log(e)},null!=t&&\"function\"==typeof t[n]?r(t,n):void 0;var t,n,r};return function e(){return r.readEntries(function(r){if(r.length>0){var i,a=d(r,!0);try{for(a.s();!(i=a.n()).done;){var o=i.value;o.isFile?o.file(function(e){if(!n.options.ignoreHiddenFiles||\".\"!==e.name.substring(0,1))return e.fullPath=\"\".concat(t,\"/\").concat(e.name),n.addFile(e)}):o.isDirectory&&n._addFilesFromDirectory(o,\"\".concat(t,\"/\").concat(o.name))}}catch(e){a.e(e)}finally{a.f()}e()}return null},i)}()}},{key:\"accept\",value:function(e,t){this.options.maxFilesize&&e.size>1024*this.options.maxFilesize*1024?t(this.options.dictFileTooBig.replace(\"{{filesize}}\",Math.round(e.size/1024/10.24)/100).replace(\"{{maxFilesize}}\",this.options.maxFilesize)):n.isValidFile(e,this.options.acceptedFiles)?null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(t(this.options.dictMaxFilesExceeded.replace(\"{{maxFiles}}\",this.options.maxFiles)),this.emit(\"maxfilesexceeded\",e)):this.options.accept.call(this,e,t):t(this.options.dictInvalidFileType)}},{key:\"addFile\",value:function(e){var t=this;e.upload={uuid:n.uuidv4(),progress:0,total:e.size,bytesSent:0,filename:this._renameFile(e)},this.files.push(e),e.status=n.ADDED,this.emit(\"addedfile\",e),this._enqueueThumbnail(e),this.accept(e,function(n){n?(e.accepted=!1,t._errorProcessing([e],n)):(e.accepted=!0,t.options.autoQueue&&t.enqueueFile(e)),t._updateMaxFilesReachedClass()})}},{key:\"enqueueFiles\",value:function(e){var t,n=d(e,!0);try{for(n.s();!(t=n.n()).done;){var r=t.value;this.enqueueFile(r)}}catch(e){n.e(e)}finally{n.f()}return null}},{key:\"enqueueFile\",value:function(e){var t=this;if(e.status!==n.ADDED||!0!==e.accepted)throw new Error(\"This file can't be queued because it has already been processed or was rejected.\");if(e.status=n.QUEUED,this.options.autoProcessQueue)return setTimeout(function(){return t.processQueue()},0)}},{key:\"_enqueueThumbnail\",value:function(e){var t=this;if(this.options.createImageThumbnails&&e.type.match(/image.*/)&&e.size<=1024*this.options.maxThumbnailFilesize*1024)return this._thumbnailQueue.push(e),setTimeout(function(){return t._processThumbnailQueue()},0)}},{key:\"_processThumbnailQueue\",value:function(){var e=this;if(!this._processingThumbnail&&0!==this._thumbnailQueue.length){this._processingThumbnail=!0;var t=this._thumbnailQueue.shift();return this.createThumbnail(t,this.options.thumbnailWidth,this.options.thumbnailHeight,this.options.thumbnailMethod,!0,function(n){return e.emit(\"thumbnail\",t,n),e._processingThumbnail=!1,e._processThumbnailQueue()})}}},{key:\"removeFile\",value:function(e){if(e.status===n.UPLOADING&&this.cancelUpload(e),this.files=M(this.files,e),this.emit(\"removedfile\",e),0===this.files.length)return this.emit(\"reset\")}},{key:\"removeAllFiles\",value:function(e){null==e&&(e=!1);var t,r=d(this.files.slice(),!0);try{for(r.s();!(t=r.n()).done;){var i=t.value;(i.status!==n.UPLOADING||e)&&this.removeFile(i)}}catch(e){r.e(e)}finally{r.f()}return null}},{key:\"resizeImage\",value:function(e,t,r,i,a){var o=this;return this.createThumbnail(e,t,r,i,!0,function(t,r){if(null==r)return a(e);var i=o.options.resizeMimeType;null==i&&(i=e.type);var s=r.toDataURL(i,o.options.resizeQuality);return\"image/jpeg\"!==i&&\"image/jpg\"!==i||(s=L.restore(e.dataURL,s)),a(n.dataURItoBlob(s))})}},{key:\"createThumbnail\",value:function(e,t,n,r,i,a){var o=this,s=new FileReader;s.onload=function(){e.dataURL=s.result,\"image/svg+xml\"!==e.type?o.createThumbnailFromUrl(e,t,n,r,i,a):null!=a&&a(s.result)},s.readAsDataURL(e)}},{key:\"displayExistingFile\",value:function(e,t,n,r){var i=this,a=!(arguments.length>4&&void 0!==arguments[4])||arguments[4];if(this.emit(\"addedfile\",e),this.emit(\"complete\",e),a){e.dataURL=t,this.createThumbnailFromUrl(e,this.options.thumbnailWidth,this.options.thumbnailHeight,this.options.thumbnailMethod,this.options.fixOrientation,function(t){i.emit(\"thumbnail\",e,t),n&&n()},r)}else this.emit(\"thumbnail\",e,t),n&&n()}},{key:\"createThumbnailFromUrl\",value:function(e,t,n,r,i,a,o){var s=this,l=document.createElement(\"img\");return o&&(l.crossOrigin=o),i=\"from-image\"!=getComputedStyle(document.body).imageOrientation&&i,l.onload=function(){var o=function(e){return e(1)};return\"undefined\"!=typeof EXIF&&null!==EXIF&&i&&(o=function(e){return EXIF.getData(l,function(){return e(EXIF.getTag(this,\"Orientation\"))})}),o(function(i){e.width=l.width,e.height=l.height;var o=s.options.resize.call(s,e,t,n,r),u=document.createElement(\"canvas\"),d=u.getContext(\"2d\");switch(u.width=o.trgWidth,u.height=o.trgHeight,i>4&&(u.width=o.trgHeight,u.height=o.trgWidth),i){case 2:d.translate(u.width,0),d.scale(-1,1);break;case 3:d.translate(u.width,u.height),d.rotate(Math.PI);break;case 4:d.translate(0,u.height),d.scale(1,-1);break;case 5:d.rotate(.5*Math.PI),d.scale(1,-1);break;case 6:d.rotate(.5*Math.PI),d.translate(0,-u.width);break;case 7:d.rotate(.5*Math.PI),d.translate(u.height,-u.width),d.scale(-1,1);break;case 8:d.rotate(-.5*Math.PI),d.translate(-u.height,0)}k(d,l,null!=o.srcX?o.srcX:0,null!=o.srcY?o.srcY:0,o.srcWidth,o.srcHeight,null!=o.trgX?o.trgX:0,null!=o.trgY?o.trgY:0,o.trgWidth,o.trgHeight);var c=u.toDataURL(\"image/png\");if(null!=a)return a(c,u)})},null!=a&&(l.onerror=a),l.src=e.dataURL}},{key:\"processQueue\",value:function(){var e=this.options.parallelUploads,t=this.getUploadingFiles().length,n=t;if(!(t>=e)){var r=this.getQueuedFiles();if(r.length>0){if(this.options.uploadMultiple)return this.processFiles(r.slice(0,e-t));for(;n<e;){if(!r.length)return;this.processFile(r.shift()),n++}}}}},{key:\"processFile\",value:function(e){return this.processFiles([e])}},{key:\"processFiles\",value:function(e){var t,r=d(e,!0);try{for(r.s();!(t=r.n()).done;){var i=t.value;i.processing=!0,i.status=n.UPLOADING,this.emit(\"processing\",i)}}catch(e){r.e(e)}finally{r.f()}return this.options.uploadMultiple&&this.emit(\"processingmultiple\",e),this.uploadFiles(e)}},{key:\"_getFilesWithXhr\",value:function(e){return this.files.filter(function(t){return t.xhr===e}).map(function(e){return e})}},{key:\"cancelUpload\",value:function(e){if(e.status===n.UPLOADING){var t,r=this._getFilesWithXhr(e.xhr),i=d(r,!0);try{for(i.s();!(t=i.n()).done;){t.value.status=n.CANCELED}}catch(e){i.e(e)}finally{i.f()}void 0!==e.xhr&&e.xhr.abort();var a,o=d(r,!0);try{for(o.s();!(a=o.n()).done;){var s=a.value;this.emit(\"canceled\",s)}}catch(e){o.e(e)}finally{o.f()}this.options.uploadMultiple&&this.emit(\"canceledmultiple\",r)}else e.status!==n.ADDED&&e.status!==n.QUEUED||(e.status=n.CANCELED,this.emit(\"canceled\",e),this.options.uploadMultiple&&this.emit(\"canceledmultiple\",[e]));if(this.options.autoProcessQueue)return this.processQueue()}},{key:\"resolveOption\",value:function(e){if(\"function\"==typeof e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r<t;r++)n[r-1]=arguments[r];return e.apply(this,n)}return e}},{key:\"uploadFile\",value:function(e){return this.uploadFiles([e])}},{key:\"uploadFiles\",value:function(e){var t=this;this._transformFiles(e,function(r){if(t.options.chunking){var i=r[0];e[0].upload.chunked=t.options.chunking&&(t.options.forceChunking||i.size>t.options.chunkSize),e[0].upload.totalChunkCount=Math.ceil(i.size/t.options.chunkSize)}if(e[0].upload.chunked){var a=e[0],o=r[0];a.upload.chunks=[];var s=function(){for(var r=0;void 0!==a.upload.chunks[r];)r++;if(!(r>=a.upload.totalChunkCount)){0;var i=r*t.options.chunkSize,s=Math.min(i+t.options.chunkSize,o.size),l={name:t._getParamName(0),data:o.webkitSlice?o.webkitSlice(i,s):o.slice(i,s),filename:a.upload.filename,chunkIndex:r};a.upload.chunks[r]={file:a,index:r,dataBlock:l,status:n.UPLOADING,progress:0,retries:0},t._uploadData(e,[l])}};if(a.upload.finishedChunkUpload=function(r,i){var o=!0;r.status=n.SUCCESS,r.dataBlock=null,r.xhr=null;for(var l=0;l<a.upload.totalChunkCount;l++){if(void 0===a.upload.chunks[l])return s();a.upload.chunks[l].status!==n.SUCCESS&&(o=!1)}o&&t.options.chunksUploaded(a,function(){t._finished(e,i,null)})},t.options.parallelChunkUploads)for(var l=0;l<a.upload.totalChunkCount;l++)s();else s()}else{for(var u=[],d=0;d<e.length;d++)u[d]={name:t._getParamName(d),data:r[d],filename:e[d].upload.filename};t._uploadData(e,u)}})}},{key:\"_getChunk\",value:function(e,t){for(var n=0;n<e.upload.totalChunkCount;n++)if(void 0!==e.upload.chunks[n]&&e.upload.chunks[n].xhr===t)return e.upload.chunks[n]}},{key:\"_uploadData\",value:function(e,t){var r,i=this,a=new XMLHttpRequest,o=d(e,!0);try{for(o.s();!(r=o.n()).done;){r.value.xhr=a}}catch(e){o.e(e)}finally{o.f()}e[0].upload.chunked&&(e[0].upload.chunks[t[0].chunkIndex].xhr=a);var s=this.resolveOption(this.options.method,e),l=this.resolveOption(this.options.url,e);a.open(s,l,!0),this.resolveOption(this.options.timeout,e)&&(a.timeout=this.resolveOption(this.options.timeout,e)),a.withCredentials=!!this.options.withCredentials,a.onload=function(t){i._finishedUploading(e,a,t)},a.ontimeout=function(){i._handleUploadError(e,a,\"Request timedout after \".concat(i.options.timeout/1e3,\" seconds\"))},a.onerror=function(){i._handleUploadError(e,a)},(null!=a.upload?a.upload:a).onprogress=function(t){return i._updateFilesUploadProgress(e,a,t)};var u={Accept:\"application/json\",\"Cache-Control\":\"no-cache\",\"X-Requested-With\":\"XMLHttpRequest\"};for(var c in this.options.headers&&n.extend(u,this.options.headers),u){var f=u[c];f&&a.setRequestHeader(c,f)}var p=new FormData;if(this.options.params){var h=this.options.params;for(var m in\"function\"==typeof h&&(h=h.call(this,e,a,e[0].upload.chunked?this._getChunk(e[0],a):null)),h){var v=h[m];if(Array.isArray(v))for(var g=0;g<v.length;g++)p.append(m,v[g]);else p.append(m,v)}}var _,y=d(e,!0);try{for(y.s();!(_=y.n()).done;){var b=_.value;this.emit(\"sending\",b,a,p)}}catch(e){y.e(e)}finally{y.f()}this.options.uploadMultiple&&this.emit(\"sendingmultiple\",e,a,p),this._addFormElementData(p);for(var M=0;M<t.length;M++){var w=t[M];p.append(w.name,w.data,w.filename)}this.submitRequest(a,p,e)}},{key:\"_transformFiles\",value:function(e,t){for(var n=this,r=[],i=0,a=function(a){n.options.transformFile.call(n,e[a],function(n){r[a]=n,++i===e.length&&t(r)})},o=0;o<e.length;o++)a(o)}},{key:\"_addFormElementData\",value:function(e){if(\"FORM\"===this.element.tagName){var t,n=d(this.element.querySelectorAll(\"input, textarea, select, button\"),!0);try{for(n.s();!(t=n.n()).done;){var r=t.value,i=r.getAttribute(\"name\"),a=r.getAttribute(\"type\");if(a&&(a=a.toLowerCase()),null!=i)if(\"SELECT\"===r.tagName&&r.hasAttribute(\"multiple\")){var o,s=d(r.options,!0);try{for(s.s();!(o=s.n()).done;){var l=o.value;l.selected&&e.append(i,l.value)}}catch(e){s.e(e)}finally{s.f()}}else(!a||\"checkbox\"!==a&&\"radio\"!==a||r.checked)&&e.append(i,r.value)}}catch(e){n.e(e)}finally{n.f()}}}},{key:\"_updateFilesUploadProgress\",value:function(e,t,n){if(e[0].upload.chunked){var r=e[0],i=this._getChunk(r,t);n?(i.progress=100*n.loaded/n.total,i.total=n.total,i.bytesSent=n.loaded):(i.progress=100,i.bytesSent=i.total),r.upload.progress=0,r.upload.total=0,r.upload.bytesSent=0;for(var a=0;a<r.upload.totalChunkCount;a++)r.upload.chunks[a]&&void 0!==r.upload.chunks[a].progress&&(r.upload.progress+=r.upload.chunks[a].progress,r.upload.total+=r.upload.chunks[a].total,r.upload.bytesSent+=r.upload.chunks[a].bytesSent);r.upload.progress=r.upload.progress/r.upload.totalChunkCount,this.emit(\"uploadprogress\",r,r.upload.progress,r.upload.bytesSent)}else{var o,s=d(e,!0);try{for(s.s();!(o=s.n()).done;){var l=o.value;l.upload.total&&l.upload.bytesSent&&l.upload.bytesSent==l.upload.total||(n?(l.upload.progress=100*n.loaded/n.total,l.upload.total=n.total,l.upload.bytesSent=n.loaded):(l.upload.progress=100,l.upload.bytesSent=l.upload.total),this.emit(\"uploadprogress\",l,l.upload.progress,l.upload.bytesSent))}}catch(e){s.e(e)}finally{s.f()}}}},{key:\"_finishedUploading\",value:function(e,t,r){var i;if(e[0].status!==n.CANCELED&&4===t.readyState){if(\"arraybuffer\"!==t.responseType&&\"blob\"!==t.responseType&&(i=t.responseText,t.getResponseHeader(\"content-type\")&&~t.getResponseHeader(\"content-type\").indexOf(\"application/json\")))try{i=JSON.parse(i)}catch(e){r=e,i=\"Invalid JSON response from server.\"}this._updateFilesUploadProgress(e,t),200<=t.status&&t.status<300?e[0].upload.chunked?e[0].upload.finishedChunkUpload(this._getChunk(e[0],t),i):this._finished(e,i,r):this._handleUploadError(e,t,i)}}},{key:\"_handleUploadError\",value:function(e,t,r){if(e[0].status!==n.CANCELED){if(e[0].upload.chunked&&this.options.retryChunks){var i=this._getChunk(e[0],t);if(i.retries++<this.options.retryChunksLimit)return void this._uploadData(e,[i.dataBlock]);console.warn(\"Retried this chunk too often. Giving up.\")}this._errorProcessing(e,r||this.options.dictResponseError.replace(\"{{statusCode}}\",t.status),t)}}},{key:\"submitRequest\",value:function(e,t,n){1==e.readyState?e.send(t):console.warn(\"Cannot send this request because the XMLHttpRequest.readyState is not OPENED.\")}},{key:\"_finished\",value:function(e,t,r){var i,a=d(e,!0);try{for(a.s();!(i=a.n()).done;){var o=i.value;o.status=n.SUCCESS,this.emit(\"success\",o,t,r),this.emit(\"complete\",o)}}catch(e){a.e(e)}finally{a.f()}if(this.options.uploadMultiple&&(this.emit(\"successmultiple\",e,t,r),this.emit(\"completemultiple\",e)),this.options.autoProcessQueue)return this.processQueue()}},{key:\"_errorProcessing\",value:function(e,t,r){var i,a=d(e,!0);try{for(a.s();!(i=a.n()).done;){var o=i.value;o.status=n.ERROR,this.emit(\"error\",o,t,r),this.emit(\"complete\",o)}}catch(e){a.e(e)}finally{a.f()}if(this.options.uploadMultiple&&(this.emit(\"errormultiple\",e,t,r),this.emit(\"completemultiple\",e)),this.options.autoProcessQueue)return this.processQueue()}}],[{key:\"initClass\",value:function(){this.prototype.Emitter=a,this.prototype.events=[\"drop\",\"dragstart\",\"dragend\",\"dragenter\",\"dragover\",\"dragleave\",\"addedfile\",\"addedfiles\",\"removedfile\",\"thumbnail\",\"error\",\"errormultiple\",\"processing\",\"processingmultiple\",\"uploadprogress\",\"totaluploadprogress\",\"sending\",\"sendingmultiple\",\"success\",\"successmultiple\",\"canceled\",\"canceledmultiple\",\"complete\",\"completemultiple\",\"reset\",\"maxfilesexceeded\",\"maxfilesreached\",\"queuecomplete\"],this.prototype._thumbnailQueue=[],this.prototype._processingThumbnail=!1}},{key:\"extend\",value:function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r<t;r++)n[r-1]=arguments[r];for(var i=0,a=n;i<a.length;i++){var o=a[i];for(var s in o){var l=o[s];e[s]=l}}return e}},{key:\"uuidv4\",value:function(){return\"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return(\"x\"===e?t:3&t|8).toString(16)})}}]),n}(a);b.initClass(),b.version=\"5.9.3\",b.options={},b.optionsForElement=function(e){return e.getAttribute(\"id\")?b.options[w(e.getAttribute(\"id\"))]:void 0},b.instances=[],b.forElement=function(e){if(\"string\"==typeof e&&(e=document.querySelector(e)),null==(null!=e?e.dropzone:void 0))throw new Error(\"No Dropzone found for given element. This is probably because you're trying to access it before Dropzone had the time to initialize. Use the `init` option to setup any additional observers on your Dropzone.\");return e.dropzone},b.autoDiscover=!0,b.discover=function(){var e;if(document.querySelectorAll)e=document.querySelectorAll(\".dropzone\");else{e=[];var t=function(t){return function(){var n,r=[],i=d(t,!0);try{for(i.s();!(n=i.n()).done;){var a=n.value;/(^| )dropzone($| )/.test(a.className)?r.push(e.push(a)):r.push(void 0)}}catch(e){i.e(e)}finally{i.f()}return r}()};t(document.getElementsByTagName(\"div\")),t(document.getElementsByTagName(\"form\"))}return function(){var t,n=[],r=d(e,!0);try{for(r.s();!(t=r.n()).done;){var i=t.value;!1!==b.optionsForElement(i)?n.push(new b(i)):n.push(void 0)}}catch(e){r.e(e)}finally{r.f()}return n}()},b.blockedBrowsers=[/opera.*(Macintosh|Windows Phone).*version\\/12/i],b.isBrowserSupported=function(){var e=!0;if(window.File&&window.FileReader&&window.FileList&&window.Blob&&window.FormData&&document.querySelector)if(\"classList\"in document.createElement(\"a\")){void 0!==b.blacklistedBrowsers&&(b.blockedBrowsers=b.blacklistedBrowsers);var t,n=d(b.blockedBrowsers,!0);try{for(n.s();!(t=n.n()).done;){t.value.test(navigator.userAgent)&&(e=!1)}}catch(e){n.e(e)}finally{n.f()}}else e=!1;else e=!1;return e},b.dataURItoBlob=function(e){for(var t=atob(e.split(\",\")[1]),n=e.split(\",\")[0].split(\":\")[1].split(\";\")[0],r=new ArrayBuffer(t.length),i=new Uint8Array(r),a=0,o=t.length,s=0<=o;s?a<=o:a>=o;s?a++:a--)i[a]=t.charCodeAt(a);return new Blob([r],{type:n})};var M=function(e,t){return e.filter(function(e){return e!==t}).map(function(e){return e})},w=function(e){return e.replace(/[\\-_](\\w)/g,function(e){return e.charAt(1).toUpperCase()})};b.createElement=function(e){var t=document.createElement(\"div\");return t.innerHTML=e,t.childNodes[0]},b.elementInside=function(e,t){if(e===t)return!0;for(;e=e.parentNode;)if(e===t)return!0;return!1},b.getElement=function(e,t){var n;if(\"string\"==typeof e?n=document.querySelector(e):null!=e.nodeType&&(n=e),null==n)throw new Error(\"Invalid `\".concat(t,\"` option provided. Please provide a CSS selector or a plain HTML element.\"));return n},b.getElements=function(e,t){var n,r;if(e instanceof Array){r=[];try{var i,a=d(e,!0);try{for(a.s();!(i=a.n()).done;)n=i.value,r.push(this.getElement(n,t))}catch(e){a.e(e)}finally{a.f()}}catch(e){r=null}}else if(\"string\"==typeof e){r=[];var o,s=d(document.querySelectorAll(e),!0);try{for(s.s();!(o=s.n()).done;)n=o.value,r.push(n)}catch(e){s.e(e)}finally{s.f()}}else null!=e.nodeType&&(r=[e]);if(null==r||!r.length)throw new Error(\"Invalid `\".concat(t,\"` option provided. Please provide a CSS selector, a plain HTML element or a list of those.\"));return r},b.confirm=function(e,t,n){return window.confirm(e)?t():null!=n?n():void 0},b.isValidFile=function(e,t){if(!t)return!0;t=t.split(\",\");var n,r=e.type,i=r.replace(/\\/.*$/,\"\"),a=d(t,!0);try{for(a.s();!(n=a.n()).done;){var o=n.value;if(\".\"===(o=o.trim()).charAt(0)){if(-1!==e.name.toLowerCase().indexOf(o.toLowerCase(),e.name.length-o.length))return!0}else if(/\\/\\*$/.test(o)){if(i===o.replace(/\\/.*$/,\"\"))return!0}else if(r===o)return!0}}catch(e){a.e(e)}finally{a.f()}return!1},\"undefined\"!=typeof jQuery&&null!==jQuery&&(jQuery.fn.dropzone=function(e){return this.each(function(){return new b(this,e)})}),b.ADDED=\"added\",b.QUEUED=\"queued\",b.ACCEPTED=b.QUEUED,b.UPLOADING=\"uploading\",b.PROCESSING=b.UPLOADING,b.CANCELED=\"canceled\",b.ERROR=\"error\",b.SUCCESS=\"success\";var k=function(e,t,n,r,i,a,o,s,l,u){var d=function(e){e.naturalWidth;var t=e.naturalHeight,n=document.createElement(\"canvas\");n.width=1,n.height=t;var r=n.getContext(\"2d\");r.drawImage(e,0,0);for(var i=r.getImageData(1,0,1,t).data,a=0,o=t,s=t;s>a;)0===i[4*(s-1)+3]?o=s:a=s,s=o+a>>1;var l=s/t;return 0===l?1:l}(t);return e.drawImage(t,n,r,i,a,o,s,l,u/d)},L=function(){function e(){f(this,e)}return h(e,null,[{key:\"initClass\",value:function(){this.KEY_STR=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\"}},{key:\"encode64\",value:function(e){for(var t=\"\",n=void 0,r=void 0,i=\"\",a=void 0,o=void 0,s=void 0,l=\"\",u=0;a=(n=e[u++])>>2,o=(3&n)<<4|(r=e[u++])>>4,s=(15&r)<<2|(i=e[u++])>>6,l=63&i,isNaN(r)?s=l=64:isNaN(i)&&(l=64),t=t+this.KEY_STR.charAt(a)+this.KEY_STR.charAt(o)+this.KEY_STR.charAt(s)+this.KEY_STR.charAt(l),n=r=i=\"\",a=o=s=l=\"\",u<e.length;);return t}},{key:\"restore\",value:function(e,t){if(!e.match(\"data:image/jpeg;base64,\"))return t;var n=this.decode64(e.replace(\"data:image/jpeg;base64,\",\"\")),r=this.slice2Segments(n),i=this.exifManipulation(t,r);return\"data:image/jpeg;base64,\".concat(this.encode64(i))}},{key:\"exifManipulation\",value:function(e,t){var n=this.getExifArray(t),r=this.insertExif(e,n);return new Uint8Array(r)}},{key:\"getExifArray\",value:function(e){for(var t=void 0,n=0;n<e.length;){if(255===(t=e[n])[0]&225===t[1])return t;n++}return[]}},{key:\"insertExif\",value:function(e,t){var n=e.replace(\"data:image/jpeg;base64,\",\"\"),r=this.decode64(n),i=r.indexOf(255,3),a=r.slice(0,i),o=r.slice(i),s=a;return s=(s=s.concat(t)).concat(o)}},{key:\"slice2Segments\",value:function(e){for(var t=0,n=[];;){if(255===e[t]&218===e[t+1])break;if(255===e[t]&216===e[t+1])t+=2;else{var r=t+(256*e[t+2]+e[t+3])+2,i=e.slice(t,r);n.push(i),t=r}if(t>e.length)break}return n}},{key:\"decode64\",value:function(e){var t=void 0,n=void 0,r=\"\",i=void 0,a=void 0,o=\"\",s=0,l=[];for(/[^A-Za-z0-9\\+\\/\\=]/g.exec(e)&&console.warn(\"There were invalid base64 characters in the input text.\\nValid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\\nExpect errors in decoding.\"),e=e.replace(/[^A-Za-z0-9\\+\\/\\=]/g,\"\");t=this.KEY_STR.indexOf(e.charAt(s++))<<2|(i=this.KEY_STR.indexOf(e.charAt(s++)))>>4,n=(15&i)<<4|(a=this.KEY_STR.indexOf(e.charAt(s++)))>>2,r=(3&a)<<6|(o=this.KEY_STR.indexOf(e.charAt(s++))),l.push(t),64!==a&&l.push(n),64!==o&&l.push(r),t=n=r=\"\",i=a=o=\"\",s<e.length;);return l}}]),e}();L.initClass();b._autoDiscoverFunction=function(){if(b.autoDiscover)return b.discover()},function(e,t){var n=!1,r=!0,i=e.document,a=i.documentElement,o=i.addEventListener?\"addEventListener\":\"attachEvent\",s=i.addEventListener?\"removeEventListener\":\"detachEvent\",l=i.addEventListener?\"\":\"on\",u=function r(a){if(\"readystatechange\"!==a.type||\"complete\"===i.readyState)return(\"load\"===a.type?e:i)[s](l+a.type,r,!1),!n&&(n=!0)?t.call(e,a.type||a):void 0};if(\"complete\"!==i.readyState){if(i.createEventObject&&a.doScroll){try{r=!e.frameElement}catch(e){}r&&function e(){try{a.doScroll(\"left\")}catch(t){return void setTimeout(e,50)}return u(\"poll\")}()}i[o](l+\"DOMContentLoaded\",u,!1),i[o](l+\"readystatechange\",u,!1),e[o](l+\"load\",u,!1)}}(window,b._autoDiscoverFunction),window.Dropzone=b;var x=b}(),r}()}),Dropzone.autoDiscover=!1,Dropzone.prototype.previewTemplate='    <div class=\"dz-preview dz-file-preview\">\\n        <div class=\"dz-image\"><img data-dz-thumbnail /></div>\\n        \\n        <div class=\"dz-details\">\\n            <div class=\"dz-size\"><span data-dz-size></span></div>\\n            <div class=\"dz-filename\"><span data-dz-name></span></div>\\n        </div>\\n\\n        <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress></span></div>\\n\\n        <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\\n        \\n        <div class=\"dz-success-mark\">\\n            <svg width=\"54px\" height=\"54px\" viewBox=\"0 0 54 54\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\\n                <title>Check</title>\\n                <g stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\">\\n                    <path d=\"M23.5,31.8431458 L17.5852419,25.9283877 C16.0248253,24.3679711 13.4910294,24.366835 11.9289322,25.9289322 C10.3700136,27.4878508 10.3665912,30.0234455 11.9283877,31.5852419 L20.4147581,40.0716123 C20.5133999,40.1702541 20.6159315,40.2626649 20.7218615,40.3488435 C22.2835669,41.8725651 24.794234,41.8626202 26.3461564,40.3106978 L43.3106978,23.3461564 C44.8771021,21.7797521 44.8758057,19.2483887 43.3137085,17.6862915 C41.7547899,16.1273729 39.2176035,16.1255422 37.6538436,17.6893022 L23.5,31.8431458 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z\" stroke-opacity=\"0.198794158\" stroke=\"#747474\" fill-opacity=\"0.816519475\" fill=\"#FFFFFF\"></path>\\n                </g>\\n            </svg>\\n        </div>\\n\\n        <div class=\"dz-error-mark\">\\n            <svg width=\"54px\" height=\"54px\" viewBox=\"0 0 54 54\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\\n                <title>Error</title>\\n                <g stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\">\\n                    <g stroke=\"#747474\" stroke-opacity=\"0.198794158\" fill=\"#FFFFFF\" fill-opacity=\"0.816519475\">\\n                    <path d=\"M32.6568542,29 L38.3106978,23.3461564 C39.8771021,21.7797521 39.8758057,19.2483887 38.3137085,17.6862915 C36.7547899,16.1273729 34.2176035,16.1255422 32.6538436,17.6893022 L27,23.3431458 L21.3461564,17.6893022 C19.7823965,16.1255422 17.2452101,16.1273729 15.6862915,17.6862915 C14.1241943,19.2483887 14.1228979,21.7797521 15.6893022,23.3461564 L21.3431458,29 L15.6893022,34.6538436 C14.1228979,36.2202479 14.1241943,38.7516113 15.6862915,40.3137085 C17.2452101,41.8726271 19.7823965,41.8744578 21.3461564,40.3106978 L27,34.6568542 L32.6538436,40.3106978 C34.2176035,41.8744578 36.7547899,41.8726271 38.3137085,40.3137085 C39.8758057,38.7516113 39.8771021,36.2202479 38.3106978,34.6538436 L32.6568542,29 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z\"></path>\\n                    </g>\\n                </g>\\n            </svg>\\n        </div>\\n    </div>',function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],t):t((e=\"undefined\"!=typeof globalThis?globalThis:e||self).countUp={})}(this,function(e){\"use strict\";var t=function(){return t=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var i in t=arguments[n])Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i]);return e},t.apply(this,arguments)},n=function(){function e(e,n,r){var i=this;this.endVal=n,this.options=r,this.version=\"2.9.0\",this.defaults={startVal:0,decimalPlaces:0,duration:2,useEasing:!0,useGrouping:!0,useIndianSeparators:!1,smartEasingThreshold:999,smartEasingAmount:333,separator:\",\",decimal:\".\",prefix:\"\",suffix:\"\",enableScrollSpy:!1,scrollSpyDelay:200,scrollSpyOnce:!1},this.finalEndVal=null,this.useEasing=!0,this.countDown=!1,this.error=\"\",this.startVal=0,this.paused=!0,this.once=!1,this.count=function(e){i.startTime||(i.startTime=e);var t=e-i.startTime;i.remaining=i.duration-t,i.useEasing?i.countDown?i.frameVal=i.startVal-i.easingFn(t,0,i.startVal-i.endVal,i.duration):i.frameVal=i.easingFn(t,i.startVal,i.endVal-i.startVal,i.duration):i.frameVal=i.startVal+(i.endVal-i.startVal)*(t/i.duration);var n=i.countDown?i.frameVal<i.endVal:i.frameVal>i.endVal;i.frameVal=n?i.endVal:i.frameVal,i.frameVal=Number(i.frameVal.toFixed(i.options.decimalPlaces)),i.printValue(i.frameVal),t<i.duration?i.rAF=requestAnimationFrame(i.count):null!==i.finalEndVal?i.update(i.finalEndVal):i.options.onCompleteCallback&&i.options.onCompleteCallback()},this.formatNumber=function(e){var t,n,r,a,o=e<0?\"-\":\"\";t=Math.abs(e).toFixed(i.options.decimalPlaces);var s=(t+=\"\").split(\".\");if(n=s[0],r=s.length>1?i.options.decimal+s[1]:\"\",i.options.useGrouping){a=\"\";for(var l=3,u=0,d=0,c=n.length;d<c;++d)i.options.useIndianSeparators&&4===d&&(l=2,u=1),0!==d&&u%l==0&&(a=i.options.separator+a),u++,a=n[c-d-1]+a;n=a}return i.options.numerals&&i.options.numerals.length&&(n=n.replace(/[0-9]/g,function(e){return i.options.numerals[+e]}),r=r.replace(/[0-9]/g,function(e){return i.options.numerals[+e]})),o+i.options.prefix+n+r+i.options.suffix},this.easeOutExpo=function(e,t,n,r){return n*(1-Math.pow(2,-10*e/r))*1024/1023+t},this.options=t(t({},this.defaults),r),this.formattingFn=this.options.formattingFn?this.options.formattingFn:this.formatNumber,this.easingFn=this.options.easingFn?this.options.easingFn:this.easeOutExpo,this.el=\"string\"==typeof e?document.getElementById(e):e,n=null==n?this.parse(this.el.innerHTML):n,this.startVal=this.validateValue(this.options.startVal),this.frameVal=this.startVal,this.endVal=this.validateValue(n),this.options.decimalPlaces=Math.max(this.options.decimalPlaces),this.resetDuration(),this.options.separator=String(this.options.separator),this.useEasing=this.options.useEasing,\"\"===this.options.separator&&(this.options.useGrouping=!1),this.el?this.printValue(this.startVal):this.error=\"[CountUp] target is null or undefined\",\"undefined\"!=typeof window&&this.options.enableScrollSpy&&(this.error?console.error(this.error,e):(window.onScrollFns=window.onScrollFns||[],window.onScrollFns.push(function(){return i.handleScroll(i)}),window.onscroll=function(){window.onScrollFns.forEach(function(e){return e()})},this.handleScroll(this)))}return e.prototype.handleScroll=function(e){if(e&&window&&!e.once){var t=window.innerHeight+window.scrollY,n=e.el.getBoundingClientRect(),r=n.top+window.pageYOffset,i=n.top+n.height+window.pageYOffset;i<t&&i>window.scrollY&&e.paused?(e.paused=!1,setTimeout(function(){return e.start()},e.options.scrollSpyDelay),e.options.scrollSpyOnce&&(e.once=!0)):(window.scrollY>i||r>t)&&!e.paused&&e.reset()}},e.prototype.determineDirectionAndSmartEasing=function(){var e=this.finalEndVal?this.finalEndVal:this.endVal;this.countDown=this.startVal>e;var t=e-this.startVal;if(Math.abs(t)>this.options.smartEasingThreshold&&this.options.useEasing){this.finalEndVal=e;var n=this.countDown?1:-1;this.endVal=e+n*this.options.smartEasingAmount,this.duration=this.duration/2}else this.endVal=e,this.finalEndVal=null;null!==this.finalEndVal?this.useEasing=!1:this.useEasing=this.options.useEasing},e.prototype.start=function(e){this.error||(this.options.onStartCallback&&this.options.onStartCallback(),e&&(this.options.onCompleteCallback=e),this.duration>0?(this.determineDirectionAndSmartEasing(),this.paused=!1,this.rAF=requestAnimationFrame(this.count)):this.printValue(this.endVal))},e.prototype.pauseResume=function(){this.paused?(this.startTime=null,this.duration=this.remaining,this.startVal=this.frameVal,this.determineDirectionAndSmartEasing(),this.rAF=requestAnimationFrame(this.count)):cancelAnimationFrame(this.rAF),this.paused=!this.paused},e.prototype.reset=function(){cancelAnimationFrame(this.rAF),this.paused=!0,this.resetDuration(),this.startVal=this.validateValue(this.options.startVal),this.frameVal=this.startVal,this.printValue(this.startVal)},e.prototype.update=function(e){cancelAnimationFrame(this.rAF),this.startTime=null,this.endVal=this.validateValue(e),this.endVal!==this.frameVal&&(this.startVal=this.frameVal,null==this.finalEndVal&&this.resetDuration(),this.finalEndVal=null,this.determineDirectionAndSmartEasing(),this.rAF=requestAnimationFrame(this.count))},e.prototype.printValue=function(e){var t;if(this.el){var n=this.formattingFn(e);(null===(t=this.options.plugin)||void 0===t?void 0:t.render)?this.options.plugin.render(this.el,n):\"INPUT\"===this.el.tagName?this.el.value=n:\"text\"===this.el.tagName||\"tspan\"===this.el.tagName?this.el.textContent=n:this.el.innerHTML=n}},e.prototype.ensureNumber=function(e){return\"number\"==typeof e&&!isNaN(e)},e.prototype.validateValue=function(e){var t=Number(e);return this.ensureNumber(t)?t:(this.error=\"[CountUp] invalid start or end value: \".concat(e),null)},e.prototype.resetDuration=function(){this.startTime=null,this.duration=1e3*Number(this.options.duration),this.remaining=this.duration},e.prototype.parse=function(e){var t=function(e){return e.replace(/([.,'  ])/g,\"\\\\$1\")},n=t(this.options.separator),r=t(this.options.decimal),i=e.replace(new RegExp(n,\"g\"),\"\").replace(new RegExp(r,\"g\"),\".\");return parseFloat(i)},e}();e.CountUp=n});"
  },
  {
    "path": "static/locales/de/translation.json",
    "content": "{\n    \"title\": {\n        \"setup\": \"Ersteinrichtung\",\n        \"login\": \"Anmelden\",\n        \"share_login\": \"Anmeldung teilen\",\n        \"profile\": \"Profil\",\n        \"change_password\": \"Passwort ändern\",\n        \"files\": \"Dateien\",\n        \"shares\": \"Freigaben\",\n        \"add_share\": \"Freigabe hinzufügen\",\n        \"update_share\": \"Freigabe aktualisieren\",\n        \"two_factor_auth\": \"Zwei-Faktor-Authentifizierung\",\n        \"two_factor_auth_short\": \"2FA\",\n        \"edit_file\": \"Datei bearbeiten\",\n        \"view_file\": \"Datei ansehen\",\n        \"recovery_password\": \"Passwort-Wiederherstellung\",\n        \"reset_password\": \"Passwort zurücksetzen\",\n        \"shared_files\": \"Freigegebene Dateien\",\n        \"upload_to_share\": \"Hochladen zur Freigabe\",\n        \"download_shared_file\": \"Freigegebene Datei herunterladen!\",\n        \"share_access_error\": \"Zugriff auf die Freigabe nicht möglich!\",\n        \"invalid_auth_request\": \"Ungültige Authentifizierungsanfrage!\",\n        \"error400\": \"Ungültige Anfrage!\",\n        \"error403\": \"Zugriff verweigert\",\n        \"error404\": \"Nicht gefunden!\",\n        \"error416\": \"Angeforderter Bereich nicht erfüllbar!\",\n        \"error429\": \"Zu viele Anfragen!\",\n        \"error500\": \"Interner Serverfehler!\",\n        \"errorPDF\": \"PDF-Datei kann nicht angezeigt werden!\",\n        \"error_editor\": \"Datei-Editor kann nicht geöffnet werden!\",\n        \"users\": \"Benutzer\",\n        \"groups\": \"Gruppen\",\n        \"folders\": \"Virtuelle Ordner\",\n        \"connections\": \"Aktive Verbindungen\",\n        \"event_manager\": \"Event Manager\",\n        \"event_rules\": \"Regeln\",\n        \"event_actions\": \"Aktionen\",\n        \"ip_manager\": \"IP-Manager\",\n        \"ip_lists\": \"IP-Listen\",\n        \"defender\": \"Automatische Sperrliste\",\n        \"admins\": \"Administratoren\",\n        \"roles\": \"Rollen\",\n        \"server_manager\": \"Server-Manager\",\n        \"configs\": \"Einstellungen\",\n        \"logs\": \"Protokolle\",\n        \"maintenance\": \"Wartung\",\n        \"status\": \"Status\",\n        \"add_user\": \"Benutzer hinzufügen\",\n        \"update_user\": \"Benutzer-Update\",\n        \"template_user\": \"Benutzervorlagen\",\n        \"template_admin\": \"Administrator-Vorlagen\",\n        \"add_group\": \"Gruppe hinzufügen\",\n        \"update_group\": \"Gruppe aktualisieren\",\n        \"add_folder\": \"Virtuellen Ordner hinzufügen\",\n        \"update_folder\": \"Virtuellen Ordner aktualisieren\",\n        \"template_folder\": \"Vorlage für virtuellen Ordner\",\n        \"oauth2_error\": \"OAuth2-Flow kann nicht abgeschlossen werden!\",\n        \"oauth2_success\": \"OAuth2-Flow abgeschlossen!\",\n        \"add_role\": \"Rolle hinzufügen\",\n        \"update_role\": \"Rolle aktualisieren\",\n        \"add_admin\": \"Administrator hinzufügen\",\n        \"update_admin\": \"Administrator aktualisieren\",\n        \"add_ip_list\": \"IP-Listeneintrag hinzufügen\",\n        \"update_ip_list\": \"IP-Listeneintrag aktualisieren\",\n        \"add_action\": \"Aktion hinzufügen\",\n        \"update_action\": \"Aktion übernehmen\",\n        \"add_rule\": \"Regel hinzufügen\",\n        \"update_rule\": \"Regel aktualisieren\"\n    },\n    \"setup\": {\n        \"desc\": \"Um SFTPGo verwenden zu können, müssen Sie einen Administratorbenutzer erstellen!\",\n        \"submit\": \"Administrator erstellen und anmelden\",\n        \"install_code_mismatch\": \"Der Installationscode stimmt nicht überein\",\n        \"help_text\": \"Kommerzieller Support\"\n    },\n    \"login\": {\n        \"username\": \"Benutzername\",\n        \"password\": \"Passwort\",\n        \"your_username\": \"Ihr Benutzername\",\n        \"forgot_password\": \"Passwort vergessen?\",\n        \"forgot_password_msg\": \"Geben Sie Ihren Benutzernamen unten ein, Sie erhalten einen Code zum Zurücksetzen des Passworts per E-Mail.\",\n        \"send_reset_code\": \"Code zum Zurücksetzen senden\",\n        \"signin\": \"Anmelden\",\n        \"signin_openid\": \"Mit OpenID anmelden\",\n        \"signout\": \"Abmelden\",\n        \"auth_code\": \"Authentifizierungscode\",\n        \"two_factor_help\": \"Öffnen Sie die Zwei-Faktor-Authentifizierungs-App auf Ihrem Gerät, um Ihren Authentifizierungscode anzuzeigen und Ihre Identität zu bestätigen.\",\n        \"two_factor_msg\": \"Geben Sie den Zwei-Faktor-Wiederherstellungscode ein\",\n        \"recovery_code\": \"Wiederherstellungscode\",\n        \"recovery_code_msg\": \"Geben Sie einen Wiederherstellungscode ein, falls Sie Ihr Gerät nicht mehr nutzen können.\",\n        \"reset_password\": \"Passwort zurücksetzen\",\n        \"reset_pwd_msg\": \"Überprüfen Sie Ihre E-Mail für den Bestätigungscode\",\n        \"reset_submit\": \"Passwort aktualisieren und anmelden\",\n        \"confirm_code\": \"Bestätigungscode\",\n        \"reset_pwd_forbidden\": \"Sie sind nicht berechtigt, Ihr Passwort zurückzusetzen\",\n        \"reset_pwd_no_email\": \"Ihr Konto hat keine E-Mail-Adresse, es ist nicht möglich Ihr Passwort zurückzusetzen, indem Sie einen E-Mail-Bestätigungs-Code senden\",\n        \"reset_pwd_send_email_err\": \"Bestätigungscode konnte nicht per E-Mail gesendet werden\",\n        \"reset_pwd_err_generic\": \"Unerwarteter Fehler beim Zurücksetzen des Passworts\",\n        \"reset_ok_login_error\": \"Das Passwort wurde erfolgreich zurückgesetzt, aber beim Anmelden trat ein unerwarteter Fehler auf\",\n        \"ip_not_allowed\": \"Login ist von dieser IP-Adresse nicht erlaubt\",\n        \"two_factor_required\": \"Zwei-Faktor-Authentifizierung einrichten, dies ist für folgende Protokolle erforderlich: {{val}}\",\n        \"two_factor_required_generic\": \"Zwei-Faktor-Authentifizierung einrichten, es ist erforderlich für Ihr Konto\",\n        \"link\": \"Gehe zu {{link}}\"\n    },\n    \"theme\": {\n        \"light\": \"Hell\",\n        \"dark\": \"Dunkel\",\n        \"system\": \"Auto\"\n    },\n    \"general\": {\n        \"invalid_auth_request\": \"Die Authentifizierungsanfrage erfüllt die notwendigen Sicherheitsanforderungen nicht!\",\n        \"invalid_input\": \"Ungültige Eingabe. Schrägstriche (/), Doppelpunkte (:), Steuerzeichen und reservierte Systemnamen sind nicht erlaubt\",\n        \"error400\": \"Die empfangene Anfrage ist ungültig!\",\n        \"error403\": \"Sie besitzen nicht über die erforderlichen Berechtigungen!\",\n        \"error404\": \"Die angefragte Ressource existiert nicht!\",\n        \"error416\": \"Das angeforderte Dateifragment konnte nicht zurückgegeben werden!\",\n        \"error429\": \"Zu viele Anfragen erhalten!\",\n        \"error500\": \"Der Server kann Ihre Anfrage nicht erfüllen!\",\n        \"errorPDF\": \"Diese Datei sieht nicht aus wie ein PDF!\",\n        \"error_edit_dir\": \"Verzeichnis kann nicht bearbeitet werden!\",\n        \"error_edit_size\": \"Die Dateigröße überschreitet die maximal zulässige Größe!\",\n        \"invalid_form\": \"Ungültiges Formular!\",\n        \"invalid_credentials\": \"Ungültige Anmeldedaten, bitte erneut versuchen!\",\n        \"invalid_csrf\": \"Der Formulartoken ist ungültig!\",\n        \"invalid_token\": \"Ungültige Berechtigungen!\",\n        \"confirm_logout\": \"Sind Sie sicher, dass Sie sich abmelden wollen?\",\n        \"wait\": \"Bitte warten...\",\n        \"ok\": \"OK!\",\n        \"failed\": \"Fehler!\",\n        \"cancel\": \"Abbrechen\",\n        \"submit\": \"Speichern\",\n        \"back\": \"Zurück\",\n        \"add\": \"Hinzufügen\",\n        \"enable\": \"Aktivieren\",\n        \"disable\": \"Deaktivieren\",\n        \"disable_confirm_btn\": \"Ja, deaktivieren\",\n        \"close\": \"Schließen\",\n        \"search\": \"Suchen\",\n        \"configuration\": \"Konfiguration\",\n        \"config_saved\": \"Konfiguration gespeichert\",\n        \"rename\": \"Umbenennen\",\n        \"confirm\": \"Ja, fortfahren\",\n        \"edit\": \"Bearbeiten\",\n        \"delete\": \"Löschen\",\n        \"delete_confirm_btn\": \"Ja, löschen\",\n        \"delete_confirm\": \"Möchten Sie \\\"{{- name}}\\\" löschen? Diese Aktion kann nicht rückgängig gemacht werden\",\n        \"delete_confirm_generic\": \"Möchten Sie das ausgewählte Element löschen? Diese Aktion kann nicht rückgängig gemacht werden!\",\n        \"delete_multi_confirm\": \"Möchten Sie das ausgewählte Element löschen? Diese Aktion kann nicht rückgängig gemacht werden!\",\n        \"delete_error_generic\": \"Ausgewähltes Element konnte nicht gelöscht werden!\",\n        \"delete_error_403\": \"$t(general.delete_error_generic). $t(general.error403)\",\n        \"delete_error_404\": \"$t(general.delete_error_generic). $t(general.error404)\",\n        \"loading\": \"Laden...\",\n        \"name\": \"Name\",\n        \"size\": \"Größe\",\n        \"last_modified\": \"Zuletzt geändert\",\n        \"info\": \"Info\",\n        \"datetime\": \"{{- val, datetime}}\",\n        \"selected_items_one\": \"{{count}} Element ausgewählt\",\n        \"selected_items_other\": \"{{count}} Elemente ausgewählt\",\n        \"selected_with_placeholder\": \"%d Elemente ausgewählt\",\n        \"name_required\": \"Name ist erforderlich!\",\n        \"name_different\": \"Der neue Name muss sich vom aktuellen Namen unterscheiden!\",\n        \"html5_media_not_supported\": \"Ihr Browser unterstützt keine HTML5-Audio/Video.\",\n        \"choose_target_folder\": \"Zielordner wählen\",\n        \"source_name\": \"Quellenname\",\n        \"target_folder\": \"Zielverzeichnis\",\n        \"dest_name\": \"Ziel-Name\",\n        \"my_profile\": \"Mein Profil\",\n        \"description\": \"Beschreibung\",\n        \"email\": \"E-Mail\",\n        \"api_key_auth\": \"API Key Authentifizierung\",\n        \"api_key_auth_help\": \"Erlauben, sich in der REST-API mit einem API-Schlüssel auszugeben\",\n        \"pub_keys\": \"Öffentliche Schlüssel\",\n        \"pub_key_placeholder\": \"Öffentlichen Schlüssel hier einfügen\",\n        \"pub_keys_help\": \"Öffentliche Schlüssel können für SFTP-Authentifizierung verwendet werden.\",\n        \"verify\": \"Überprüfen\",\n        \"problems\": \"Haben Sie Probleme?\",\n        \"allowed_ip_mask\": \"Erlaubte IP/Maske\",\n        \"denied_ip_mask\": \"Gesperrte IP/Maske\",\n        \"ip_mask_help\": \"Durch Komma getrennte IP/Maske im CIDR-Format, zum Beispiel \\\"192.168.1.0/24,10.8.0.100/32\\\"\",\n        \"allowed_ip_mask_invalid\": \"Ungültige erlaubte IP/Maske!\",\n        \"username_required\": \"Benutzername erforderlich!\",\n        \"password_required\": \"Passwort erforderlich!\",\n        \"foldername_required\": \"Ordnername ist erforderlich!\",\n        \"err_user\": \"Benutzer konnte nicht validiert werden!\",\n        \"err_protocol_forbidden\": \"HTTP-Protokoll ist für Ihren Benutzer nicht erlaubt!\",\n        \"pwd_login_forbidden\": \"Die Passwort-Login-Methode ist für Ihren Benutzer nicht erlaubt!\",\n        \"ip_forbidden\": \"Anmeldung von dieser IP-Adresse nicht erlaubt!\",\n        \"email_invalid\": \"Die E-Mail-Adresse ist ungültig!\",\n        \"err_password_complexity\": \"Das angegebene Passwort entspricht nicht den Passwort-Anforderungen!\",\n        \"no_oidc_feature\": \"Diese Funktion ist nicht verfügbar, wenn Sie mit OpenID angemeldet sind!\",\n        \"connection_forbidden\": \"Sie sind nicht berechtigt, sich zu verbinden!\",\n        \"no_permissions\": \"Sie sind nicht berechtigt, Änderungen durchzuführen!\",\n        \"path_invalid\": \"Ungültiger Pfad!\",\n        \"err_quota_read\": \"Lesen wegen Quotenlimit verweigert!\",\n        \"profile_updated\": \"Ihr Profil wurde erfolgreich aktualisiert\",\n        \"share_ok\": \"Zugriff erfolgreich freigegeben, Sie können nun Ihren Link verwenden!\",\n        \"qr_code\": \"QR-Code\",\n        \"copy_link\": \"Link kopieren\",\n        \"copied\": \"Kopiert\",\n        \"active\": \"Aktiv\",\n        \"inactive\": \"Inaktiv\",\n        \"colvis\": \"Spaltenanzeige\",\n        \"actions\": \"Aktionen\",\n        \"template\": \"Als Vorlage verwenden\",\n        \"quota_scan\": \"Kontingent-Scan\",\n        \"quota_scan_started\": \"Kontingentsuche gestartet. Abhängig von der Anzahl der Dateien kann dies eine Weile dauern\",\n        \"quota_scan_conflit\": \"Ein anderer Scan wird bereits ausgeführt\",\n        \"quota_scan_error\": \"Quota-Scan kann nicht gestartet werden\",\n        \"role\": \"Rolle\",\n        \"role_placeholder\": \"Rolle wählen\",\n        \"group_placeholder\": \"Gruppe wählen\",\n        \"folder_placeholder\": \"Ordner auswählen\",\n        \"blank_default_help\": \"Leer lassen für Standard\",\n        \"skip_tls_verify\": \"TLS Überprüfung überspringen. Dies sollte nur zum Testen verwendet werden\",\n        \"advanced_settings\": \"Erweiterte Einstellungen\",\n        \"default\": \"Standard\",\n        \"private_key\": \"Privater Schlüssel\",\n        \"acls\": \"ACLs\",\n        \"quota_limits\": \"Festplattenkontingent und Bandbreitenbegrenzung\",\n        \"expiration\": \"Ablaufdatum\",\n        \"expiration_help\": \"Ablaufdatum festlegen\",\n        \"additional_info\": \"Zusätzliche Information\",\n        \"permissions\": \"Berechtigungen\",\n        \"visible\": \"Sichtbar\",\n        \"hidden\": \"Verborgen\",\n        \"allowed\": \"Erlaubt\",\n        \"denied\": \"Verweigert\",\n        \"zero_no_limit_help\": \"0 bedeutet keine Begrenzung\",\n        \"global_settings\": \"Globale Einstellungen\",\n        \"mandatory_encryption\": \"Erforderliche Verschlüsselung\",\n        \"name_invalid\": \"Der angegebene Name ist ungültig, die folgenden Zeichen sind erlaubt: a-zA-Z0-9-_.~\",\n        \"associations\": \"Assoziationen\",\n        \"template_placeholders\": \"Die folgenden Platzhalter werden unterstützt\",\n        \"duplicated_username\": \"Der angegebene Benutzername existiert bereits!\",\n        \"duplicated_name\": \"Der angegebene Name existiert bereits!\",\n        \"permissions_required\": \"Berechtigungen erforderlich!\",\n        \"configs_saved\": \"Konfigurationen wurden erfolgreich aktualisiert!\",\n        \"protocol\": \"Protokoll\",\n        \"refresh\": \"Aktualisieren\",\n        \"members\": \"Mitglieder\",\n        \"members_summary\": \"Benutzer: {{users}}. Administratoren: {{admins}}\",\n        \"status\": \"Status\",\n        \"last_login\": \"Letzte Anmeldung\",\n        \"previous\": \"Vorherige\",\n        \"next\": \"Weiter\",\n        \"type\": \"Typ\",\n        \"issuer\": \"Aussteller\",\n        \"data_provider\": \"Datenbank\",\n        \"driver\": \"Treiber\",\n        \"mode\": \"Modus\",\n        \"port\": \"Port\",\n        \"domain\": \"Domäne\",\n        \"test\": \"Test\",\n        \"get\": \"Holen\",\n        \"export\": \"Exportieren\",\n        \"value\": \"Wert\",\n        \"method\": \"Methode\",\n        \"timeout\": \"Zeitüberschreitung\",\n        \"env_vars\": \"Umgebungsvariablen\",\n        \"hours\": \"Stunden\",\n        \"paths\": \"Pfade\",\n        \"hour\": \"Stunde\",\n        \"day_of_week\": \"Wochentag\",\n        \"day_of_month\": \"Tag des Monats\",\n        \"month\": \"Monat\",\n        \"options\": \"Optionen\",\n        \"expired\": \"Abgelaufen\",\n        \"unsupported\": \"Funktion wird nicht mehr unterstützt!\",\n        \"start\": \"Start (HH:MM)\",\n        \"end\": \"Ende (HH:MM)\",\n        \"monday\": \"Montag\",\n        \"tuesday\": \"Dienstag\",\n        \"wednesday\": \"Mittwoch\",\n        \"thursday\": \"Donnerstag\",\n        \"friday\": \"Freitag\",\n        \"saturday\": \"Samstag\",\n        \"sunday\": \"Sonntag\",\n        \"expired_session\": \"Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an\"\n    },\n    \"fs\": {\n        \"view_file\": \"Datei \\\"{{- path}} \\\" anzeigen\",\n        \"edit_file\": \"Datei \\\"{{- path}} \\\" bearbeiten\",\n        \"new_folder\": \"Neuer Ordner\",\n        \"select_across_pages\": \"Über Seiten hinweg auswählen\",\n        \"download\": \"Herunterladen\",\n        \"download_ready\": \"Download abgeschlossen\",\n        \"upload_queue_not_ready\": \"Die Upload-Warteschlange wird noch geladen, bitte versuche es in ein paar Augenblicken erneut!\",\n        \"upload_no_files\": \"Keine Dateien ausgewählt. Bitte wählen oder ziehen Sie Dateien zum Hochladen\",\n        \"move_copy\": \"Verschieben oder kopieren\",\n        \"share\": \"Teilen\",\n        \"home\": \"Startseite\",\n        \"create_folder_msg\": \"Ordnername\",\n        \"no_more_subfolders\": \"Keine weiteren Unterordner!\",\n        \"no_files_folders\": \"Keine Dateien oder Ordner!\",\n        \"invalid_name\": \"Dateinamen oder Ordnernamen dürfen nicht \\\"/\\\" enthalten!\",\n        \"folder_name_required\": \"Ordnername ist erforderlich!\",\n        \"deleting\": \"Löschen {{idx}}/{{total}}\",\n        \"copying\": \"Kopiere {{idx}}/{{total}}\",\n        \"moving\": \"Verschiebe {{idx}}/{{total}}\",\n        \"uploading\": \"Upload {{idx}}/{{total}}\",\n        \"err_403\": \"Zugriff verweigert!\",\n        \"err_429\": \"Zu viele gleichzeitige Anfragen!\",\n        \"err_generic\": \"Zugriff auf die angeforderte Ressource nicht möglich!\",\n        \"err_validation\": \"Ungültige Dateisystem-Konfiguration!\",\n        \"err_exists\": \"Das Ziel existiert bereits!\",\n        \"dir_list\": {\n            \"err_generic\": \"Fehler beim Abrufen des Verzeichnisbaumes!\",\n            \"err_403\": \"$t(fs.dir_list.err_generic) $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.dir_list.err_generic) $t(fs.err_429)\",\n            \"err_user\": \"$t(fs.dir_list.err_generic). $t(general.err_user)\"\n        },\n        \"create_dir\": {\n            \"err_generic\": \"Fehler beim Erstellen eines neuen Ordners!\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"save\": {\n            \"err_generic\": \"Fehler beim Speichern der Datei!\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"delete\": {\n            \"err_generic\": \"Kann \\\"{{- name}} \\\" nicht löschen!\",\n            \"err_403\": \"$t(fs.delete.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete.err_generic). $t(fs.err_429)\"\n        },\n        \"delete_multi\": {\n            \"err_generic_partial\": \"Nicht alle ausgewählten Elemente wurden gelöscht, bitte laden Sie die Seite erneut!\",\n            \"err_generic\": \"Ausgewählte Elemente konnten nicht gelöscht werden!\",\n            \"err_403_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)\",\n            \"err_429_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)\",\n            \"err_403\": \"$t(fs.delete_multi.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete_multi.err_generic). $t(fs.err_429)\"\n        },\n        \"copy\": {\n            \"msg\": \"Kopieren\",\n            \"err_generic\": \"Fehler beim Kopieren von Dateien/Verzeichnissen!\",\n            \"err_403\": \"$t(fs.copy.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.copy.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.copy.err_generic). $t(fs.err_exists)\"\n        },\n        \"move\": {\n            \"msg\": \"Verschieben\",\n            \"err_generic\": \"Fehler beim Verschieben von Dateien/Verzeichnissen!\",\n            \"err_403\": \"$t(fs.move.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.move.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.move.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"Nicht unterstützt: Wenn Sie ein Verzeichnis verschieben möchten, stellen Sie sicher, dass es leer ist!\"\n        },\n        \"rename\": {\n            \"title\": \"Umbenennen von \\\"{{- name}}\\\"\",\n            \"new_name\": \"Neuer Name\",\n            \"err_generic\": \"Umbenennen von \\\"{{- name}} \\\" nicht möglich!\",\n            \"err_403\": \"$t(fs.rename.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.rename.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.rename.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"Nicht unterstützt: Wenn Sie ein Verzeichnis umbenennen möchten, stellen Sie sicher, dass es leer ist!\"\n        },\n        \"upload\": {\n            \"text\": \"Dateien hochladen\",\n            \"success\": \"Dateien erfolgreich hochgeladen!\",\n            \"message\": \"Dateien hier ablegen oder zum Hochladen klicken.\",\n            \"message_empty\": \"Dieses Verzeichnis ist leer. $t(fs.upload.message)\",\n            \"err_generic\": \"Fehler beim Hochladen der Dateien!\",\n            \"err_403\": \"$t(fs.upload.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.upload.err_generic). $t(fs.err_429)\",\n            \"err_dir_overwrite\": \"$t(fs.upload.err_generic). Es gibt Verzeichnisse mit dem gleichen Namen wie die Dateien: {{- val}}\",\n            \"overwrite_text\": \"Konflikt erkannt. Möchten Sie die folgenden Dateien/Verzeichnisse überschreiben?\"\n        },\n        \"quota_usage\": {\n            \"title\": \"Quota-Nutzung\",\n            \"disk\": \"Disk-Quota\",\n            \"size\": \"Größe: {{- val}}\",\n            \"size_percentage\": \"Größe: {{- val}} ({{percentage}}%)\",\n            \"files\": \"Dateien: {{- val}}\",\n            \"files_percentage\": \"Dateien: {{- val}} ({{percentage}}%)\",\n            \"transfer\": \"Transfervolumen\",\n            \"total\": \"Gesamt: {{- val}}\",\n            \"total_percentage\": \"Gesamt: {{- val}} ({{percentage}}%)\",\n            \"uploads\": \"Uploads: {{- val}}\",\n            \"uploads_percentage\": \"Uploads: {{- val}} ({{percentage}}%)\",\n            \"downloads\": \"Downloads: {{- val}}\",\n            \"downloads_percentage\": \"Downloads: {{- val}} ({{percentage}}%)\"\n        }\n    },\n    \"datatable\": {\n        \"info\": \"Zeige _START_ bis _END_ von _TOTAL_ Einträgen\",\n        \"info_empty\": \"Keine Datensätze anzuzeigen!\",\n        \"info_filtered\": \"(gefiltert von _MAX_ Einträgen)\",\n        \"processing\": \"Verarbeitung...\",\n        \"no_records\": \"Kein Datensatz gefunden\"\n    },\n    \"editor\": {\n        \"keybinding\": \"Editor Tastaturbelegungen\",\n        \"search\": \"Suchfeld öffnen\",\n        \"goto\": \"Zu Zeile springen\",\n        \"indent_more\": \"Mehr einrücken\",\n        \"indent_less\": \"Weniger einrücken\"\n    },\n    \"2fa\": {\n        \"title\": \"2FA via Authenticator-App\",\n        \"msg_enabled\": \"Zwei-Faktor-Authentisierung ist aktiviert\",\n        \"msg_disabled\": \"Sichern Sie Ihr Konto\",\n        \"msg_info\": \"Zwei-Faktor-Authentifizierung fügt Ihrem Konto eine zusätzliche Ebene der Sicherheit hinzu. Um sich anzumelden, müssen Sie einen zusätzlichen Authentifizierungscode angeben.\",\n        \"require\": \"Zwei-Faktor-Authentifizierung ist für dieses Konto erforderlich\",\n        \"require_for\": \"2FA erforderlich für\",\n        \"generate\": \"Generiere neuen Geheimschlüssel\",\n        \"recovery_codes\": \"Wiederherstellungscodes\",\n        \"new_recovery_codes\": \"Neue Wiederherstellungscodes\",\n        \"recovery_codes_msg1\": \"Wiederherstellungscodes sind eine Reihe von Codes zur einmaligen Verwendung, die anstelle des Authentifizierungscodes verwendet werden können, um sich bei der Web-Benutzeroberfläche anzumelden. Sie können sie verwenden, wenn Sie den Zugriff auf Ihr Telefon verlieren, um sich bei Ihrem Konto anzumelden und die Zwei-Faktor-Authentifizierungskonfiguration zu deaktivieren oder neu zu generieren.\",\n        \"recovery_codes_msg2\": \"Um Ihr Konto sicher zu halten, teilen Sie Ihre Wiederherstellungs-Codes niemals. Wir empfehlen Ihnen, diese mit einem sicheren Passwort-Manager zu speichern.\",\n        \"recovery_codes_msg3\": \"Wenn Sie neue Wiederherstellungscodes generieren, werden die alten automatisch ungültig.\",\n        \"info_title\": \"Erfahren Sie mehr über Zwei-Faktor-Authentifizierung\",\n        \"info1\": \"Das SSH-Protokoll (SFTP/SCP/SSH-Befehle) fragt nach dem Passwort, wenn der Client die interaktive Tastaturauthentifizierung verwendet.\",\n        \"info2\": \"HTTP-Protokoll bedeutet Web UI und REST APIs. Die Web-UI fragt auf einer bestimmten Seite nach dem Kennwort. Bei der REST-API müssen Sie den Passcode über einen HTTP-Header hinzufügen.\",\n        \"info3\": \"FTP unterstützt standardmäßig keine Zwei-Faktor-Authentifizierung. Wenn Sie die FTP-Unterstützung aktivieren, müssen Sie den TOTP-Passcode nach dem Passwort hinzufügen. Wenn Ihr Passwort zum Beispiel \\\"passwort\\\" und Ihr Einmalpasscode \\\"123456\\\" ist, müssen Sie \\\"password123456\\\" als Passwort verwenden.\",\n        \"info4\": \"WebDAV wird nicht unterstützt, da jede einzelne Anfrage authentifiziert werden muss und ein Passwort nicht wiederverwendet werden kann.\",\n        \"setup_title\": \"Zwei-Faktor-Authentifizierung einrichten\",\n        \"setup_msg\": \"Verwenden Sie Ihre bevorzugte Authentifikation-App (z. B. Microsoft Authentifikator, Google Authentifikator Authy, 1Password usw.), um den QR-Code zu scannen. Es wird ein Authentifizierungscode generiert, den Sie unten eingeben können.\",\n        \"setup_help\": \"Wenn Sie Probleme mit der Verwendung des QR-Codes haben, wählen Sie die manuelle Eingabe in Ihrer App und geben Sie den Code ein\",\n        \"disable_msg\": \"2FA deaktivieren\",\n        \"disable_confirm\": \"Möchten Sie die Zwei-Faktoren-Authentifizierung für den ausgewählten Benutzer deaktivieren? Diese Aktion ist in der Regel nur erforderlich, wenn der Benutzer den Zugriff auf den zweiten Authentifizierungsfaktor verloren hat\",\n        \"disable_question\": \"Möchten Sie die Zwei-Faktor-Authentifizierung deaktivieren?\",\n        \"generate_question\": \"Möchten Sie einen neuen geheimen Schlüssel generieren und den vorherigen ungültig machen? Alle registrierten Authentifikation-Apps funktionieren dann nicht mehr\",\n        \"disabled\": \"Zwei‐Faktor‐Authentifizierung deaktiviert\",\n        \"recovery_codes_gen_err\": \"Fehler beim Generieren neuer Wiederherstellungscodes\",\n        \"recovery_codes_get_err\": \"Wiederherstellungscodes konnten nicht abgerufen werden\",\n        \"auth_code_invalid\": \"Fehler beim Validieren des angegebenen Authentifizierungscodes\",\n        \"auth_secret_gen_err\": \"Fehler beim Generieren des Authentifizierungsgeheimnisses\",\n        \"save_err\": \"Fehler beim Speichern der Zwei-Faktor-Authentifizierungskonfiguration\",\n        \"auth_code_required\": \"Der Authentifizierungscode ist erforderlich\",\n        \"no_protocol\": \"Bitte wählen Sie mindestens ein Protokoll\",\n        \"required_protocols\": \"Ihre Sicherheitsrichtlinie verlangt 2FA für diese Protokolle: {{val}}\",\n        \"recovery_codes_generate\": \"Neue Wiederherstellungscodes generieren\",\n        \"recovery_codes_view\": \"Wiederherstellungscodes anzeigen\"\n    },\n    \"share\": {\n        \"scope\": \"Bereich\",\n        \"scope_read\": \"Lesen\",\n        \"scope_write\": \"Schreiben\",\n        \"scope_read_write\": \"Lesen/Schreiben\",\n        \"scope_help\": \"Für den Bereich \\\"Schreiben\\\" und \\\"Lesen/Schreiben\\\" müssen Sie einen einzigen Pfad definieren, und es muss ein Verzeichnis sein\",\n        \"path_help\": \"Datei- oder Verzeichnispfad, z. B. /dir oder /dir/file.txt\",\n        \"password_help\": \"Wenn gesetzt, wird die Freigabe passwortgeschützt\",\n        \"max_tokens\": \"Max. Token\",\n        \"max_tokens_help\": \"Maximale Anzahl der Zugriffe auf diese Freigabe. 0 bedeutet keine Begrenzung\",\n        \"view_manage\": \"Freigaben anzeigen und verwalten\",\n        \"no_share\": \"Keine Freigaben\",\n        \"password_protected\": \"Passwortgeschütz\",\n        \"expiration_date\": \"Ablauf: {{- val, datetime}}. \",\n        \"last_use\": \"Letzte Verwendung: {{- val, datetime}}. \",\n        \"usage\": \"Verwendung: {{used}}/{{total}}. \",\n        \"used_tokens\": \"Verwendeter Token: {{used}}. \",\n        \"scope_invalid\": \"Ungültiger Bereich\",\n        \"max_tokens_invalid\": \"Ungültige max. Token\",\n        \"expiration_invalid\": \"Ungültiges Ablaufdatum\",\n        \"err_no_password\": \"Sie sind nicht berechtigt, Dateien/Ordner ohne Passwort zu teilen\",\n        \"expiration_out_of_range\": \"Legen Sie ein Ablaufdatum fest und stellen Sie sicher, dass es kleiner oder gleich {{- val, datetime}} ist\",\n        \"generic\": \"Unerwarteter Fehler beim Speichern der Freigabe\",\n        \"path_required\": \"Mindestens ein Pfad ist erforderlich\",\n        \"path_write_scope\": \"Der Schreibbereich benötigt genau einen Pfad\",\n        \"nested_paths\": \"Pfade können nicht verschachtelt werden\",\n        \"expiration_past\": \"Das Ablaufdatum muss in der Zukunft liegen\",\n        \"usage_exceed\": \"Maximale Freigabenutzung überschritten\",\n        \"expired\": \"Freigabe ist abgelaufen\",\n        \"browsable_multiple_paths\": \"Eine Freigabe mit mehreren Pfaden kann nicht durchsucht werden\",\n        \"browsable_non_dir\": \"Die Freigabe ist kein Verzeichnis, daher kann sie nicht durchsucht werden\",\n        \"go\": \"Zum Teilen\",\n        \"access_links_title\": \"Zugangslinks teilen\",\n        \"link_single_title\": \"Einzelne Zip-Datei\",\n        \"link_single_desc\": \"Sie können den freigegebenen Inhalt als einzelne Zip-Datei herunterladen\",\n        \"link_dir_title\": \"Einzelnes Verzeichnis\",\n        \"link_dir_desc\": \"Wenn die Freigabe aus einem einzigen Verzeichnis besteht, können Sie Dateien durchsuchen und herunterladen\",\n        \"link_uncompressed_title\": \"Unkomprimierte Datei\",\n        \"link_uncompressed_desc\": \"Wenn die Freigabe aus einer Datei besteht, kann sie auch unkomprimiert heruntergeladen werden\",\n        \"upload_desc\": \"Sie können eine oder mehrere Dateien in das freigegebene Verzeichnis hochladen\",\n        \"expired_desc\": \"Diese Freigabe ist nicht mehr zugänglich, weil sie abgelaufen ist\",\n        \"invalid_path\": \"Das freigegebene Verzeichnis fehlt oder ist nicht verfügbar\"\n    },\n    \"select2\": {\n        \"no_results\": \"Kein Ergebnis gefunden\",\n        \"searching\": \"Suche ...\",\n        \"removeall\": \"Entferne alle Elemente\",\n        \"remove\": \"Element entfernen\"\n    },\n    \"change_pwd\": {\n        \"info\": \"Geben Sie aus Sicherheitsgründen Ihr aktuelles Passwort ein, und dann zweimal Ihr neues Passwort, um zu überprüfen, ob Sie es richtig geschrieben haben. Sie werden abgemeldet, nachdem Sie Ihr Passwort geändert haben\",\n        \"current\": \"Aktuelles Passwort\",\n        \"new\": \"Neues Passwort\",\n        \"confirm\": \"Passwort bestätigen\",\n        \"save\": \"Ändere mein Passwort\",\n        \"required_fields\": \"Bitte geben Sie das aktuelle und das neue Passwort zweimal ein!\",\n        \"no_match\": \"Die beiden Passwortfelder stimmen nicht überein!\",\n        \"no_different\": \"Das neue Passwort muss sich von dem aktuellen unterscheiden\",\n        \"current_no_match\": \"Das neue Passwort muss sich von dem aktuellen unterscheiden\",\n        \"generic\": \"Eine Passwortänderung ist erforderlich. Legen Sie ein neues Passwort fest, um Ihr Konto weiterhin nutzen zu können\",\n        \"required\": \"Eine Passwortänderung ist erforderlich. Legen Sie ein neues Passwort fest, um Ihr Konto weiterhin nutzen zu können\"\n    },\n    \"user\": {\n        \"view_manage\": \"Benutzer anzeigen und verwalten\",\n        \"username_reserved\": \"Der angegebene Benutzername ist reserviert\",\n        \"username_invalid\": \"Der angegebene Name ist ungültig. Die folgenden Zeichen sind zulässig: a-z A-Z 0-9 - _ . ~\",\n        \"home_required\": \"Das Home-Verzeichnis ist obligatorisch\",\n        \"home_invalid\": \"Das Home-Verzeichnis muss ein absoluter Pfad sein\",\n        \"pub_key_invalid\": \"Ungültiger öffentlicher Schlüssel\",\n        \"priv_key_invalid\": \"Ungültiger privater Schlüssel\",\n        \"key_invalid_size\": \"Ungültiger öffentlicher RSA Schlüssel: Die Mindestlänge beträgt 2048\",\n        \"key_insecure\": \"Unsicheres Format des öffentlichen Schlüssels ist nicht erlaubt\",\n        \"err_primary_group\": \"Nur eine Primärgruppe ist erlaubt\",\n        \"err_duplicate_group\": \"Doppelte Gruppen erkannt\",\n        \"no_permissions\": \"Verzeichnisberechtigungen sind obligatorisch\",\n        \"no_root_permissions\": \"Home-Verzeichnis-Berechtigungen sind erforderlich\",\n        \"err_permissions_generic\": \"Ungültige Berechtigungen: Stellen Sie sicher, dass Sie gültige absolute Pfade verwenden\",\n        \"2fa_invalid\": \"Ungültige Konfiguration für Zwei-Faktor-Authentifizierung\",\n        \"recovery_codes_invalid\": \"Ungültige Wiederherstellungscodes\",\n        \"folder_path_required\": \"Der Pfad zum Mounten des virtuellen Ordners ist erforderlich\",\n        \"folder_duplicated\": \"Doppelte virtuelle Ordner erkannt\",\n        \"folder_overlapped\": \"Überlappende virtuelle Ordner erkannt\",\n        \"folder_quota_size_invalid\": \"Das Kontingentgröße der virtuellen Ordner muss größer oder gleich -1 sein\",\n        \"folder_quota_file_invalid\": \"Das Kontingent als Dateien virtueller Ordner muss größer oder gleich -1 sein\",\n        \"folder_quota_invalid\": \"Quoten als Größe und als Anzahl der Dateien in virtuellen Ordnern müssen sowohl -1 als auch größer oder gleich 0 sein\",\n        \"ip_filters_invalid\": \"Ungültige IP-Filter, stellen Sie sicher, dass Sie die CIDR-Notation respektieren, zum Beispiel 192.168.1.0/24\",\n        \"src_bw_limits_invalid\": \"Ungültige Bandbreitengeschwindigkeitsbegrenzungen pro Quelle\",\n        \"share_expiration_invalid\": \"Der Ablauf für Freigaben muss größer oder gleich dem vorgegebenen Standardwert sein\",\n        \"file_pattern_path_invalid\": \"Filterpfade für Dateinamenmuster müssen absolut sein\",\n        \"file_pattern_duplicated\": \"Filter für doppelte Dateinamenmuster erkannt\",\n        \"file_pattern_invalid\": \"Ungültige Dateinamenmusterfilter\",\n        \"disable_active_2fa\": \"Die Zwei-Faktor-Authentifizierung kann für einen Benutzer mit einer aktiven Konfiguration nicht deaktiviert werden\",\n        \"pwd_change_conflict\": \"Es ist nicht möglich, eine Passwortänderung anzufordern und gleichzeitig die Änderung des Passworts zu verhindern\",\n        \"two_factor_conflict\": \"Sie können nicht eine Zwei-Faktor-Authentifizierung verlangen und gleichzeitig deren Einrichtung verhindern\",\n        \"role_help\": \"Benutzer mit einer Rolle können von globalen Administratoren und Administratoren mit derselben Rolle verwaltet werden\",\n        \"require_pwd_change\": \"Passwortänderung erforderlich\",\n        \"require_pwd_change_help\": \"Der Benutzer muss das Passwort von WebClient ändern, um das Konto zu aktivieren\",\n        \"groups_help\": \"Gruppenmitgliedschaft vermittelt die Gruppeneinstellungen mit Ausnahme von Gruppen, die nur Mitglieder sind\",\n        \"primary_group\": \"Primärgruppe\",\n        \"secondary_groups\": \"Sekundärgruppen\",\n        \"membership_groups\": \"Mitgliedergruppen\",\n        \"template_help\": \"Legen Sie für jeden Benutzer den Benutzernamen und mindestens eines der Passwörter und den öffentlichen Schlüssel fest\",\n        \"virtual_folders_help\": \"Kontingentgröße/Dateien -1 bedeutet in Benutzer-Kontingent, 0 unbegrenzt. Nicht -1 für freigegebene Ordner festlegen. Sie können MB/GB/TB Suffix verwenden. Ohne Suffix nehmen wir Bytes an\",\n        \"disconnect\": \"Trennen Sie den Benutzer nach dem Update\",\n        \"disconnect_help\": \"Auf diese Weise zwingen Sie den Benutzer sich erneut anzumelden, wenn er verbunden ist und so die neue Konfiguration zu verwenden\",\n        \"invalid_quota_size\": \"Ungültige Quota-Größe\",\n        \"expires_in\": \"Verfällt in\",\n        \"expires_in_help\": \"Ablaufdatum des Kontos in Anzahl von Tagen seit der Erstellung. 0 bedeutet kein Ablaufdatum\",\n        \"additional_emails\": \"Zusätzliche E-Mails\",\n        \"tls_certs\": \"TLS Zertifikate\",\n        \"tls_certs_help\": \"TLS-Zertifikate können für FTP- und/oder WebDAV-Authentifizierung verwendet werden.\",\n        \"tls_cert_help\": \"Fügen Sie hier ein PEM codiertes TLS Zertifikat ein\",\n        \"tls_cert_invalid\": \"Ungültiges TLS Zertifikat\",\n        \"template_title\": \"Erstellen Sie einen oder mehrere neue Benutzer aus dieser Vorlage\",\n        \"template_username_placeholder\": \"ersetzt mit dem angegebenen Benutzernamen\",\n        \"template_password_placeholder\": \"ersetzt mit dem angegebenen Passwort\",\n        \"template_help1\": \"Platzhalter werden in Pfaden und Anmeldeinformationen des konfigurierten Speicher-Backends ersetzt\",\n        \"template_help2\": \"Die generierten Benutzer können gespeichert oder exportiert werden. Exportierte Benutzer können aus dem Abschnitt „Wartung“ dieser oder einer anderen SFTPGo-Instanz importiert werden\",\n        \"template_no_user\": \"Kein gültiger Benutzer definiert, die angeforderte Aktion kann nicht abgeschlossen werden\",\n        \"time_of_day_invalid\": \"Ungültige Tageszeit. Unterstütztes Format HH:MM\",\n        \"time_of_day_conflict\": \"Ungültige Tageszeit. Die Endzeit kann nicht vor der Startzeit liegen\"\n    },\n    \"group\": {\n        \"view_manage\": \"Gruppen ansehen und verwalten\",\n        \"err_delete_referenced\": \"Eine Gruppe mit verknüpften Benutzern kann nicht gelöscht werden. Entfernen Sie zuerst die Verknüpfungen\",\n        \"help\": \"Der Platzhalter %username% wird durch den Benutzernamen des zugehörigen Benutzers ersetzt, %role% durch dessen Rolle.\"\n    },\n    \"virtual_folders\": {\n        \"view_manage\": \"Virtuelle Ordner anzeigen und verwalten\",\n        \"mount_path\": \"Mount-Pfad, z.B. /vfolder\",\n        \"quota_size\": \"Kontingentgröße\",\n        \"quota_size_help\": \"0 bedeutet kein Limit. Sie können MB/GB/TB Suffix verwenden\",\n        \"quota_files\": \"Kontingentdateien\",\n        \"associations_summary\": \"Benutzer: {{users}}. Gruppen: {{groups}}\",\n        \"template_title\": \"Erstellen Sie einen oder mehrere virtuelle Ordner aus dieser Vorlage\",\n        \"template_name_placeholder\": \"mit dem Namen des angegebenen virtuellen Ordners ersetzt\",\n        \"template_help\": \"Die generierten virtuellen Ordner können gespeichert oder exportiert werden. Exportierte Ordner können aus dem Abschnitt „Wartung“ dieser oder einer anderen SFTPGo-Instanz importiert werden.\",\n        \"name\": \"Name des virtuellen Ordners\",\n        \"template_no_folder\": \"Kein gültiger virtueller Ordner definiert, kann die angeforderte Aktion nicht abschließen\"\n    },\n    \"storage\": {\n        \"title\": \"Dateisystem\",\n        \"label\": \"Speicher\",\n        \"local\": \"Lokaler Datenträger\",\n        \"s3\": \"S3 (kompatibel)\",\n        \"gcs\": \"GCS\",\n        \"azblob\": \"Azure Blob\",\n        \"encrypted\": \"Lokale verschlüsselte Festplatte\",\n        \"sftp\": \"SFTP\",\n        \"http\": \"HTTP\",\n        \"home_dir\": \"Root-Verzeichnis\",\n        \"home_dir_placeholder\": \"Absoluter Pfad zu einem Verzeichnis auf der lokalen Festplatte\",\n        \"home_dir_help1\": \"Für einen entsprechenden Standardwert das Feld freilassen\",\n        \"home_dir_help2\": \"Lassen Sie das Feld leer und verwenden Sie „Lokale Festplatte“, um das Stammverzeichnis nicht zu überschreiben\",\n        \"home_dir_help3\": \"Erforderlich für lokale Festplattenspeicheranbieter. Für andere Speicheranbieter wird dieser Ordner für temporäre Dateien verwendet. Sie können ihn leer lassen, um einen geeigneten Standard zu erhalten\",\n        \"home_dir_invalid\": \"Ungültiges Hauptverzeichnis, es muss ein absoluter Pfad sein\",\n        \"sftp_home_dir\": \"SFTP Hauptverzeichnis\",\n        \"sftp_home_help\": \"Beschränken Sie den Zugriff auf diesen SFTP-Pfad. Beispiel: „/somedir/subdir“\",\n        \"os_read_buffer\": \"Download-Puffer (MB)\",\n        \"os_buffer_help\": \"0 bedeutet keinen Puffer\",\n        \"os_write_buffer\": \"Upload-Puffer (MB)\",\n        \"bucket\": \"Bucket\",\n        \"region\": \"Region\",\n        \"access_key\": \"Zugriffsschlüssel\",\n        \"access_secret\": \"Zugriffsgeheimnis\",\n        \"sse_customer_key\": \"Serverseitiger Verschlüsselungsschlüssel\",\n        \"sse_customer_key_help\": \"Sie können Ihre Daten mit diesem Schlüssel verschlüsselt speichern, aber wenn Sie diesen Schlüssel verlieren oder ändern, verlieren Sie alle damit verschlüsselten Dateien. Dateien, die nicht oder mit einem anderen Schlüssel verschlüsselt sind, sind nicht zugänglich\",\n        \"endpoint\": \"Endpunkt\",\n        \"endpoint_help\": \"Lassen Sie für AWS S3 das Feld leer, um den Standardendpunkt für die angegebene Region zu verwenden\",\n        \"sftp_endpoint_help\": \"Endpunkt als Host:Port. Der Port ist immer erforderlich\",\n        \"ul_part_size\": \"Upload-Teilgröße (MB)\",\n        \"part_size_help\": \"0 bedeutet die Standardeinstellung (5 MB). Minimum ist 5\",\n        \"gcs_part_size_help\": \"0 bedeutet die Standardeinstellung (16 MB)\",\n        \"ul_concurrency\": \"Gleichzeitiges Hochladen\",\n        \"ul_concurrency_help\": \"Wie viele Teile sollen parallel hochgeladen werden. 0 bedeutet Standardeinstellung (5)\",\n        \"dl_part_size\": \"Download-Größe (MB)\",\n        \"dl_concurrency\": \"Download-Parallelität\",\n        \"dl_concurrency_help\": \"Wie viele Teile sollen parallel heruntergeladen werden. 0 bedeutet Standardeinstellung (5)\",\n        \"ul_part_timeout\": \"Zeitüberschreitung beim Hochladen von Teilen\",\n        \"ul_part_timeout_help\": \"Maximales Zeitlimit in Sekunden zum Hochladen eines einzelnen Teils. 0 bedeutet kein Limit\",\n        \"gcs_ul_part_timeout_help\": \"Maximales Zeitlimit in Sekunden zum Hochladen eines einzelnen Teils. 0 bedeutet den Standardwert (32)\",\n        \"dl_part_timeout\": \"Zeitüberschreitung beim Download von Teilen\",\n        \"dl_part_timeout_help\": \"Maximales Zeitlimit in Sekunden zum Herunterladen eines einzelnen Teils. 0 bedeutet kein Limit\",\n        \"key_prefix\": \"Schlüsselpräfix\",\n        \"key_prefix_help\": \"Beschränkt den Zugriff auf Schlüssel mit dem angegebenen Präfix. Beispiel: „somedir/subdir/“\",\n        \"class\": \"Speicherklasse\",\n        \"acl\": \"ACL\",\n        \"role_arn\": \"Rolle ARN\",\n        \"role_arn_help\": \"Optionale zu übernehmende IAM-Rollen-ARN\",\n        \"s3_path_style\": \"Pfadadressierung verwenden (z. B. „Endpunkt/BUCKET/KEY“)\",\n        \"credentials_file\": \"Anmeldeinformationsdatei\",\n        \"credentials_file_help\": \"Hinzufügen oder Aktualisieren von Anmeldeinformationen aus einer JSON-Datei\",\n        \"auto_credentials\": \"Automatische Anmeldeinformation\",\n        \"auto_credentials_help\": \"Verwenden Sie standardmäßige Anwendungsanmeldeinformationen oder Anmeldeinformationen aus Umgebungsvariablen\",\n        \"container\": \"Container\",\n        \"account_name\": \"Account-Name\",\n        \"account_key\": \"Account-Schlüssel\",\n        \"sas_url\": \"SAS URL\",\n        \"sas_url_help\": \"Die Freigabezugriffssignatur-URL kann anstelle des Kontonamens/-schlüssels verwendet werden\",\n        \"emulator\": \"Emulator verwenden\",\n        \"passphrase\": \"Passphrase\",\n        \"passphrase_help\": \"Passphrase zum Ableiten des Verschlüsselungsschlüssels pro Objekt\",\n        \"passphrase_key_help\": \"Passphrase zum Schutz Ihres privaten Schlüssels, sofern vorhanden\",\n        \"fingerprints\": \"Fingerabdrücke\",\n        \"fingerprints_help\": \"SHA256-Fingerabdrücke, die bei der Verbindung mit dem externen SFTP-Server validiert werden sollen, einer pro Zeile. Wenn leer, wird jeder Hostschlüssel akzeptiert: Dies ist ein Sicherheitsrisiko!\",\n        \"sftp_buffer\": \"Puffergröße (MB)\",\n        \"sftp_buffer_help\": \"Eine Puffergröße größer als 0 ermöglicht gleichzeitige Übertragungen\",\n        \"sftp_concurrent_reads\": \"Gleichzeitiges Lesen deaktivieren\",\n        \"relaxed_equality_check\": \"Entspannte Gleichheitsprüfung\",\n        \"relaxed_equality_check_help\": \"Aktivieren Sie diese Option, um nur den Endpunkt zu berücksichtigen und zu bestimmen, ob verschiedene Konfigurationen auf denselben Server verweisen. Standardmäßig müssen sowohl der Endpunkt als auch der Benutzername übereinstimmen\",\n        \"api_key\": \"API-Schlüssel\",\n        \"fs_error\": \"Fehler bei der Dateisystemkonfiguration\",\n        \"bucket_required\": \"$t(storage.fs_error): Bucket ist erforderlich\",\n        \"region_required\": \"$t(storage.fs_error): Region ist erforderlich\",\n        \"key_prefix_invalid\": \"$t(storage.fs_error): ungültiges Schlüsselpräfix, darf nicht mit \\\"/\\\" beginnen\",\n        \"ul_part_size_invalid\": \"$t(storage.fs_error): ungültige Upload-Teilgröße\",\n        \"ul_concurrency_invalid\": \"$t(storage.fs_error): ungültige Upload-Parallelität\",\n        \"dl_part_size_invalid\": \"$t(storage.fs_error): ungültige Downloadteilgröße\",\n        \"dl_concurrency_invalid\": \"$t(storage.fs_error): ungültige Download-Parallelität\",\n        \"access_key_required\": \"$t(storage.fs_error): Zugriffsschlüssel ist erforderlich\",\n        \"access_secret_required\": \"$t(storage.fs_error): Zugriffsgeheimnis ist erforderlich\",\n        \"credentials_required\": \"$t(storage.fs_error): Anmeldeinformationen sind erforderlich\",\n        \"container_required\": \"$t(storage.fs_error): Container ist erforderlich\",\n        \"account_name_required\": \"$t(storage.fs_error): Kontoname ist erforderlich\",\n        \"sas_url_invalid\": \"$t(storage.fs_error): Ungültige SAS-Adresse\",\n        \"passphrase_required\": \"$t(storage.fs_error): Passphrase ist erforderlich\",\n        \"endpoint_invalid\": \"$t(storage.fs_error): Endpunkt ist ungültig\",\n        \"endpoint_required\": \"$t(storage.fs_error): Endpunkt ist erforderlich\",\n        \"username_required\": \"$t(storage.fs_error): Benutzername ist erforderlich\"\n    },\n    \"oidc\": {\n        \"token_expired\": \"Ihr OpenID-Token ist abgelaufen. Bitte melden Sie sich erneut an\",\n        \"token_invalid_webadmin\": \"Ihr OpenID-Token ist für die WebAdmin-Oberfläche ungültig. Melden Sie sich beim OpenID-Server ab und an der WebAdmin an\",\n        \"token_invalid_webclient\": \"Ihr OpenID-Token ist für den WebClient ungültig. Melden Sie sich beim OpenID-Server ab und am WebClient an\",\n        \"token_exchange_err\": \"Fehler beim Austausch des OpenID-Tokens\",\n        \"token_invalid\": \"Ungültiges OpenID-Token\",\n        \"role_admin_err\": \"Falsche OpenID-Rolle: Der angemeldete Benutzer ist kein Administrator\",\n        \"role_user_err\": \"Falsche OpenID-Rolle: Der angemeldete Benutzer ist ein Administrator\",\n        \"get_user_err\": \"Fehler beim Abrufen des mit dem OpenID-Token verknüpften Benutzers\"\n    },\n    \"oauth2\": {\n        \"auth_verify_error\": \"OAuth2-Code kann nicht überprüft werden!\",\n        \"auth_validation_error\": \"OAuth2-Code kann nicht überprüft werden!\",\n        \"auth_invalid\": \"Ungültiger OAuth2-Code!\",\n        \"token_exchange_err\": \"OAuth2-Token kann nicht aus Autorisierungscode abgerufen werden!\",\n        \"no_refresh_token\": \"Der OAuth2-Anbieter gab kein Token zurück. Bei einigen Anbietern erhalten Sie das Token nur bei der ersten Autorisierung. Widerrufen Sie den Zugriff und versuchen Sie es erneut.\",\n        \"success\": \"Kopieren Sie die folgende Zeichenfolge ohne Anführungszeichen in das Konfigurationsfeld „SMTP OAuth2 Token“\",\n        \"client_id_required\": \"Kunden-ID ist erforderlich!\",\n        \"client_secret_required\": \"Kunden-Geheimnis ist erforderlich!\",\n        \"refresh_token_required\": \"Aktualisierungstoken ist erforderlich!\"\n    },\n    \"filters\": {\n        \"password_strength\": \"Passwortstärke\",\n        \"password_strength_help\": \"Für gängige Anwendungsfälle werden Werte im Bereich von 50-70 empfohlen. 0 bedeutet deaktiviert, jedes Passwort wird akzeptiert\",\n        \"password_expiration\": \"Ablaufzeit des Passworts\",\n        \"password_expiration_help\": \"Ablaufzeit des Passworts in Tagen. 0 bedeutet keine Ablaufzeit\",\n        \"default_shares_expiration\": \"Standardmäßige Ablaufzeit der Freigaben\",\n        \"default_shares_expiration_help\": \"Standardablaufdatum für neue Freigaben in Tagen\",\n        \"max_shares_expiration\": \"Maximaler Freigabeablauf\",\n        \"max_shares_expiration_help\": \"Maximal zulässige Ablaufzeit (Anzahl Tage), wenn ein Benutzer eine Freigabe erstellt oder aktualisiert\",\n        \"directory_permissions\": \"Berechtigungen pro Verzeichnis\",\n        \"directory_permissions_help\": \"Platzhalter werden in Pfaden unterstützt, beispielsweise entspricht \\\"/incoming/*\\\" jedem Verzeichnis innerhalb von „/incoming“.\",\n        \"directory_path_help\": \"Verzeichnispfad, z. B. /dir\",\n        \"directory_patterns\": \"Einschränkungen für Namensmuster pro Verzeichnis\",\n        \"directory_patterns_help\": \"Durch Kommas getrennte verbotene oder erlaubte Dateien/Verzeichnisse, basierend auf Shell-Mustern. Die Übereinstimmung ist unabhängig von Groß- und Kleinschreibung.\",\n        \"max_sessions\": \"Maximale Sitzungen\",\n        \"max_sessions_help\": \"Maximale Anzahl gleichzeitiger Sitzungen. 0 bedeutet keine Begrenzung\",\n        \"denied_protocols\": \"Abgelehnte Protokolle\",\n        \"denied_login_methods\": \"Abgelehnte Anmeldemethoden\",\n        \"denied_login_methods_help\": \"\\\"Passwort\\\" gilt für alle unterstützten Protokolle, \\\"Passwort über SSH\\\" nur für SSH/SFTP/SCP\",\n        \"web_client_options\": \"Webclient/REST-API\",\n        \"max_upload_size\": \"Maximale Upload-Größe\",\n        \"max_upload_size_help\": \"Maximale Upload-Größe für eine einzelne Datei. 0 bedeutet keine Begrenzung. Sie können das Suffix MB/GB/TB verwenden\",\n        \"max_upload_size_invalid\": \"Ungültige maximale Upload-Dateigröße!\",\n        \"upload_bandwidth\": \"Bandbreite UL (KB/s)\",\n        \"download_bandwidth\": \"Bandbreite DL (KB/s)\",\n        \"upload_bandwidth_help\": \"UL (KB/s). 0 bedeutet keine Begrenzung\",\n        \"download_bandwidth_help\": \"DL (KB/s). 0 bedeutet keine Begrenzung\",\n        \"src_bandwidth_limit\": \"Bandbreiten-Geschwindigkeitsbegrenzungen pro Quelle\",\n        \"upload_data_transfer\": \"Upload-Datenübertragung (MB)\",\n        \"upload_data_transfer_help\": \"Maximal zulässige Datenübertragung für Uploads. 0 bedeutet keine Begrenzung\",\n        \"download_data_transfer\": \"Download-Datenübertragung (MB)\",\n        \"download_data_transfer_help\": \"Maximal zulässige Datenübertragung für Downloads. 0 bedeutet keine Begrenzung\",\n        \"total_data_transfer\": \"Gesamtdatentransfer (MB)\",\n        \"total_data_transfer_help\": \"Maximal zulässige Datenübertragung für Uploads + Downloads. Ersetzen Sie die einzelnen Grenzwerte. 0 bedeutet kein Limit\",\n        \"start_directory\": \"Ausgangsverzeichnis\",\n        \"start_directory_help\": \"Alternatives Anfangsverzeichnis, das anstelle von \\\"/\\\" verwendet werden soll. Unterstützt für SFTP/FTP/HTTP\",\n        \"tls_username\": \"TLS-Benutzername\",\n        \"tls_username_help\": \"Definiert das TLS-Zertifikatsfeld, das als Benutzername verwendet werden soll. Wird ignoriert, wenn gegenseitiges TLS deaktiviert ist\",\n        \"ftp_security\": \"FTP-Sicherheit\",\n        \"ftp_security_help\": \"Wird ignoriert, wenn TLS bereits global für alle FTP-Benutzer erforderlich ist\",\n        \"hooks\": \"Hooks\",\n        \"hook_ext_auth_disabled\": \"Externe Authentifizierung deaktiviert!\",\n        \"hook_pre_login_disabled\": \"Voranmeldung deaktiviert!\",\n        \"hook_check_password_disabled\": \"Passwortprüfung deaktiviert!\",\n        \"is_anonymous\": \"Anonymer Benutzer\",\n        \"is_anonymous_help\": \"Anonyme Benutzer werden für FTP- und WebDAV-Protokolle unterstützt und haben nur Lesezugriff\",\n        \"disable_fs_checks\": \"Dateisystemprüfungen deaktivieren\",\n        \"disable_fs_checks_help\": \"Deaktiviert die Überprüfung auf Existenz und automatische Erstellung von Home-Verzeichnissen und virtuellen Ordnern\",\n        \"api_key_auth_help\": \"Erlaubt die Identitätsübernahme des Benutzers in der REST-API mit einem API-Schlüssel\",\n        \"external_auth_cache_time\": \"Externe Authentifizierungs-Cache-Zeit\",\n        \"external_auth_cache_time_help\": \"Cache-Zeit in Sekunden für Benutzer, die über einen externen Authentifizierungs-Hook authentifiziert wurden. 0 bedeutet kein Cache\",\n        \"access_time\": \"Zugriffszeiteinschränkungen\",\n        \"access_time_help\": \"Keine Einschränkung bedeutet, dass der Zugriff immer erlaubt ist. Die Uhrzeit muss im Format HH:MM angegeben werden.\"\n    },\n    \"admin\": {\n        \"role_permissions\": \"Ein Rollenadministrator kann nicht über die Berechtigung „*“ verfügen\",\n        \"view_manage\": \"Administratoren anzeigen und verwalten\",\n        \"self_delete\": \"Sie können sich nicht selbst löschen!\",\n        \"self_permissions\": \"Sie können Ihre Berechtigungen nicht ändern!\",\n        \"self_disable\": \"Sie können sich nicht selbst deaktivieren!\",\n        \"self_role\": \"Sie können Ihre Rolle nicht hinzufügen/ändern!\",\n        \"password_help\": \"Wenn leer, wird das aktuelle Passwort nicht geändert\",\n        \"role_help\": \"Durch das Festlegen einer Rolle kann der Administrator nur Benutzer mit derselben Rolle verwalten. Administratoren mit einer Rolle können keine Superadministratoren sein\",\n        \"users_groups\": \"Gruppen für Benutzer\",\n        \"users_groups_help\": \"Gruppen werden automatisch für neue Benutzer ausgewählt, die von diesem Administrator erstellt werden. Der Administrator kann weiterhin verschiedene Gruppen auswählen. Diese Einstellungen werden nur für diese Administrator-Benutzeroberfläche verwendet und werden in REST-API/Hooks ignoriert\",\n        \"group_membership\": \"Zufügen als Mitgliedschaft\",\n        \"group_primary\": \"Zufügen als Primär\",\n        \"group_secondary\": \"Zufügen als Sekundär\",\n        \"user_page_pref\": \"Benutzerseiteneinstellungen\",\n        \"user_page_pref_help\": \"Sie können einige Abschnitte der Benutzerseite ausblenden. Dies sind keine Sicherheitseinstellungen und werden serverseitig in keiner Weise erzwungen. Sie dienen lediglich der Vereinfachung der Benutzerseite zum Hinzufügen/Aktualisieren.\",\n        \"hide_sections\": \"Abschnitte ausblenden\",\n        \"default_users_expiration\": \"Ablaufdatum für Standardbenutzer\",\n        \"default_users_expiration_help\": \"Standardablaufdatum für neue Benutzer in Tagen\",\n        \"require_pwd_change_help\": \"Beim nächsten Login ist eine Passwortänderung erforderlich\"\n    },\n    \"connections\": {\n        \"view_manage\": \"Anzeigen und Verwalten von Verbindungen\",\n        \"started\": \"Gestartet \",\n        \"remote_address\": \"Remote-Adresse\",\n        \"last_activity\": \"Letzte Aktivität\",\n        \"disconnect_confirm_btn\": \"Ja, trennen\",\n        \"disconnect_confirm\": \"Möchten Sie die ausgewählte Verbindung trennen? Diese Aktion ist nicht widerrufbar\",\n        \"disconnect_ko\": \"Trennen der ausgewählten Verbindung nicht möglich\",\n        \"upload\": \"UL: \\\"{{- path}}\\\"\",\n        \"download\": \"DL: \\\"{{- path}}\\\"\",\n        \"upload_info\": \"$t(connections.upload). Größe: {{- size}}. Geschwindigkeit: {{- speed}}\",\n        \"download_info\": \"$t(connections.download) Größe: {{- size}}. Geschwindigkeit: {{- speed}}\",\n        \"client\": \"Client: {{- val}}\"\n    },\n    \"role\": {\n        \"view_manage\": \"Anzeigen und Verwalten von Rollen\",\n        \"err_delete_referenced\": \"Eine Rolle mit zugeordneten Administratoren kann nicht gelöscht werden. Entfernen Sie zuerst die Zuordnungen!\"\n    },\n    \"ip_list\": {\n        \"view_manage\": \"Anzeigen und Verwalten von IP-Listen\",\n        \"defender_list\": \"Defender\",\n        \"allow_list\": \"Zulassungsliste\",\n        \"ratelimiters_safe_list\": \"Sichere Liste der Rate-Begrenzung\",\n        \"ip_net\": \"IP/Netzwerk\",\n        \"protocols\": \"Protokolle\",\n        \"any\": \"Alle\",\n        \"allow\": \"Erlaubt\",\n        \"deny\": \"Gesperrt\",\n        \"ip_net_help\": \"IP-Adresse oder Netzwerk im Format CIDR, z. B.: \\\"192.168.1.1 oder 10.8.0.100/32 oder 2001:db8:1234::/48\\\"\",\n        \"ip_invalid\": \"Ungültige IP-Adresse\",\n        \"net_invalid\": \"Ungültiges Netzwerk\",\n        \"duplicated\": \"Die ausgewählte IP/das ausgewählte Netzwerk existiert bereits\",\n        \"search\": \"IP/Netzwerk oder Anfangsteil\",\n        \"defender_disabled\": \"Defender ist in Ihrer Konfiguration deaktiviert!\",\n        \"allow_list_disabled\": \"Zulassungsliste in Ihrer Konfiguration deaktiviert!\",\n        \"ratelimiters_disabled\": \"In Ihrer Konfiguration ist Rate-Begrenzung deaktiviert!\"\n    },\n    \"defender\": {\n        \"view_manage\": \"Anzeigen und Verwalten der automatischen Blockliste\",\n        \"ip\": \"IP-Adresse\",\n        \"ban_time\": \"Blockiert bis\",\n        \"score\": \"Punktzahl\"\n    },\n    \"status\": {\n        \"desc\": \"Server-Status\",\n        \"ssh\": \"SSH/SFTP-Server\",\n        \"active\": \"Status: aktiv\",\n        \"disabled\": \"Status: ausgeschaltet \",\n        \"error\": \"Status: Fehler\",\n        \"proxy_on\": \"PROXY-Protokoll aktiviert\",\n        \"address\": \"Adresse\",\n        \"ssh_auths\": \"Authentifizierungsmethoden\",\n        \"ssh_commands\": \"Akzeptierte Befehle\",\n        \"host_key\": \"Host-Schlüssel\",\n        \"fingeprint\": \"Fingerabdruck\",\n        \"algorithms\": \"Algorithmen\",\n        \"algorithm\": \"Algorithmus\",\n        \"ssh_pub_key_algo\": \"Authentifizierungsalgorithmen für öffentlichen Schlüssel\",\n        \"ssh_mac_algo\": \"Nachrichtenauthentifizierungscode (MAC) Algorithmen\",\n        \"ssh_kex_algo\": \"Schlüsselaustauschalgorithmen (KEX)\",\n        \"ssh_cipher_algo\": \"Verschlüsselungsverfahren\",\n        \"ftp\": \"FTP-Server\",\n        \"ftp_passive_range\": \"Passiv-Modus Port-Bereich\",\n        \"ftp_passive_ip\": \"Passiv-IP\",\n        \"tls\": \"TLS\",\n        \"tls_disabled\": \"Deaktiviert\",\n        \"tls_explicit\": \"Expliziter Modus erforderlich (FTPES)\",\n        \"tls_implicit\": \"Impliziter Modus (FTPS), veraltet, FTPES wird bevorzugt\",\n        \"tls_mixed\": \"Einfacher und expliziter Modus (FTPES)\",\n        \"webdav\": \"WebDAV-Server\",\n        \"rate_limiters\": \"Ratenbegrenzer\"\n    },\n    \"maintenance\": {\n        \"backup\": \"Sicherung\",\n        \"backup_do\": \"Daten sichern\",\n        \"backup_ok\": \"Sicherung erfolgreich wiederhergestellt!\",\n        \"backup_invalid_file\": \"Ungültige Sicherungsdatei, muss eine JSON-Datei mit gültigem Inhalt sein!\",\n        \"restore_error\": \"Wiederherstellung der Sicherung nicht möglich, für weitere Informationen siehe in den Log-Dateien!\",\n        \"restore\": \"Wiederherstellung\",\n        \"backup_file\": \"Sicherungsdatei\",\n        \"backup_file_help\": \"Importiere Daten aus einer JSON-Sicherungsdatei\",\n        \"restore_mode0\": \"Hinzufügen und Aktualisieren\",\n        \"restore_mode1\": \"Nur hinzufügen\",\n        \"restore_mode2\": \"Hinzufügen, aktualisieren und trennen\",\n        \"after_restore\": \"Nach Wiederherstellung\",\n        \"quota_mode0\": \"Keine Quotenaktualisierung\",\n        \"quota_mode1\": \"Kontingent aktualisieren\",\n        \"quota_mode2\": \"Aktualisieren des Kontingents für Benutzer mit Kontingentbeschränkungen\"\n    },\n    \"acme\": {\n        \"title\": \"ACME\",\n        \"generic_error\": \"Es konnten keine TLS-Zertifikate abgerufen werden. Weitere Einzelheiten finden Sie in den Serverprotokollen!\",\n        \"help\": \"In diesem Abschnitt können Sie kostenlose TLS-Zertifikate für Ihre SFTPGo-Dienste anfordern, indem Sie das ACME-Protokoll und den HTTP-01-Challenge-Typ verwenden. Sie müssen einen DNS-Eintrag unter einer benutzerdefinierten Domäne erstellen, die Ihnen gehört und die in Ihre öffentliche SFTPGo-IP-Adresse aufgelöst wird, und der Port 80 muss öffentlich erreichbar sein. Sie können hier die Konfigurationsoptionen für die gängigsten Anwendungsfälle und Einzelknoten-Setups festlegen. Erweiterte Konfigurationen finden Sie in den SFTPGo-Dokumenten. Zum Übernehmen der Änderungen ist ein Neustart des Dienstes erforderlich!\",\n        \"domain_help\": \"Mehrere Domänen können durch Komma oder Leerzeichen getrennt angegeben werden. Sie werden in dasselbe Zertifikat aufgenommen\",\n        \"email_help\": \"Für die Registrierung und den Wiederherstellungskontakt verwendete E-Mail-Adresse\",\n        \"port_help\": \"Wenn dieser Wert von 80 abweicht, müssen Sie einen Reverse-Proxy konfigurieren\",\n        \"protocols_help\": \"Verwenden Sie die erhaltenen Zertifikate für die angegebenen Protokolle\"\n    },\n    \"smtp\": {\n        \"title\": \"SMTP\",\n        \"err_required_fields\": \"Absenderadresse und Benutzername dürfen nicht beide leer sein!\",\n        \"help\": \"Legen Sie die SMTP-Konfiguration fest und ersetzen Sie die Konfiguration, die mit Umgebungsvariablen oder der Konfigurationsdatei (sofern vorhanden) definiert wurde.\",\n        \"host\": \"Servername\",\n        \"host_help\": \"Wenn leer, ist die Konfiguration deaktiviert\",\n        \"auth\": \"Authentifizierung\",\n        \"encryption\": \"Verschlüsselung\",\n        \"sender\": \"Sender\",\n        \"debug\": \"Debug-Logs\",\n        \"domain_help\": \"HELO-Domain. Leer lassen, um den Server-Hostnamen zu verwenden\",\n        \"test_recipient\": \"Adresse zum Senden von Test-E-Mails an\",\n        \"oauth2_provider\": \"OAuth2-Anbieter\",\n        \"oauth2_provider_help\": \"URI, zu der nach der Benutzerauthentifizierung umgeleitet werden soll\",\n        \"oauth2_tenant\": \"OAuth2-Mandant\",\n        \"oauth2_tenant_help\": \"Azure-Mandant. Typische Werte sind \\\"gemeinsam\\\", \\\"Organisationen\\\", \\\"Verbraucher\\\" oder die Mandantenkennung\",\n        \"oauth2_client_id\": \"OAuth2-Client-ID\",\n        \"oauth2_client_secret\": \"OAuth2-Clientgeheimnis\",\n        \"oauth2_token\": \"OAuth2-Token\",\n        \"recipient_required\": \"Geben Sie einen Empfänger an, um eine Test-E-Mail zu senden\",\n        \"test_error\": \"Test-E-Mail kann nicht gesendet werden. Weitere Einzelheiten finden Sie in den Serverprotokollen!\",\n        \"test_ok\": \"Die Test-E-Mail wurde erfolgreich versendet. Bitte überprüfen Sie Ihren Posteingang\",\n        \"oauth2_flow_error\": \"Der URI zum Starten des OAuth2-Flows kann nicht abgerufen werden!\",\n        \"oauth2_question\": \"Möchten Sie den OAuth2-Flow starten, um ein Token zu erhalten?\"\n    },\n    \"sftp\": {\n        \"help\": \"In diesem Abschnitt können Sie Algorithmen aktivieren, die standardmäßig deaktiviert sind. Sie müssen keine Werte festlegen, die bereits mithilfe von Umgebungsvariablen oder Konfigurationsdateien definiert wurden. Zum Übernehmen der Änderungen ist ein Neustart des Dienstes erforderlich!\",\n        \"host_key_algos\": \"Host-Key-Algorithmen\"\n    },\n    \"branding\": {\n        \"title\": \"Markendesign\",\n        \"help\": \"In diesem Abschnitt können Sie SFTPGo an Ihre Marke anpassen und den Anmeldeseiten einen Haftungsausschluss hinzufügen\",\n        \"short_name\": \"Kurzname\",\n        \"logo\": \"Logo\",\n        \"logo_help\": \"PNG-Bild (transparenter Hintergrund empfohlen), max. 512x512 Pixel. Ideal: 256x256 Pixel\",\n        \"favicon\": \"Favicon\",\n        \"disclaimer_name\": \"Titel des Haftungsausschlusses\",\n        \"disclaimer_url\": \"Haftungsausschluss-URL\",\n        \"invalid_png\": \"Ungültiges PNG-Bild\",\n        \"invalid_png_size\": \"Ungültige PNG-Bildgröße\",\n        \"invalid_disclaimer_url\": \"Die Haftungsausschluss-URL muss ein http- oder https-Link sein\"\n    },\n    \"events\": {\n        \"search\": \"Suchprotokolle\",\n        \"fs_events\": \"Dateisystemereignisse\",\n        \"provider_events\": \"Anbieter-Ereignisse\",\n        \"other_events\": \"Sonstige Ereignisse\",\n        \"quota_exceeded\": \"Kontingent überschritten\",\n        \"date_range\": \"Datumsbereich\",\n        \"upload\": \"Upload\",\n        \"download\": \"Download\",\n        \"mkdir\": \"Verzeichnis erstellen\",\n        \"rmdir\": \"Verzeichnis löschen\",\n        \"rename\": \"Umbenennen\",\n        \"delete\": \"Entfernen\",\n        \"copy\": \"Kopieren\",\n        \"first_upload\": \"Erster Upload\",\n        \"first_download\": \"Erster Download\",\n        \"ssh_cmd\": \"SSH-Befehl\",\n        \"add\": \"Zusatz\",\n        \"update\": \"Update\",\n        \"login_failed\": \"Anmeldung fehlgeschlagen!\",\n        \"login_ok\": \"Anmeldung erfolgreich!\",\n        \"login_missing_user\": \"Anmeldung mit nicht vorhandenem Benutzer!\",\n        \"no_login_tried\": \"Keine Anmeldung versucht!\",\n        \"algo_negotiation_failed\": \"Die Aushandlung der Algorithmen ist fehlgeschlagen!\",\n        \"datetime\": \"Datum und Uhrzeit\",\n        \"action\": \"Aktion\",\n        \"path\": \"Pfad\",\n        \"object\": \"Objekt\",\n        \"event\": \"Ereignis\"\n    },\n    \"provider_objects\": {\n        \"user\": \"Benutzer\",\n        \"folder\": \"Verzeichnis\",\n        \"group\": \"Gruppe\",\n        \"admin\": \"Administrator\",\n        \"api_key\": \"API-Key\",\n        \"share\": \"Freigabe\",\n        \"event_action\": \"Aktion\",\n        \"event_rule\": \"Regel\",\n        \"role\": \"Rolle\",\n        \"ip_list_entry\": \"IP-Listeneintrag\",\n        \"configs\": \"Konfigurationen\"\n    },\n    \"actions\": {\n        \"view_manage\": \"Anzeigen und Verwalten von Regelaktionen für Ereignisse\",\n        \"err_delete_referenced\": \"Eine Aktion mit verknüpften Regeln kann nicht gelöscht werden. Entfernen Sie zuerst die Verknüpfungen\",\n        \"http_url\": \"Server-URL\",\n        \"http_url_help\": \"z. B. https://host:port/path. Platzhalter werden innerhalb des URL-Pfads unterstützt\",\n        \"http_url_required\": \"URL ist erforderlich\",\n        \"http_url_invalid\": \"Die URL ist ungültig, HTTP- und HTTP-Schemata werden unterstützt\",\n        \"http_part_name_required\": \"Der HTTP-Teilname ist erforderlich\",\n        \"http_part_body_required\": \"HTTP-Teiltext ist erforderlich, wenn kein Dateipfad angegeben wird\",\n        \"http_multipart_body_error\": \"Mehrteilige Anfragen erfordern keinen separaten Textkörper. Dieser wird aus den Teilen generiert\",\n        \"http_multipart_ctype_error\": \"Der Content-Type wird für mehrteilige Anfragen automatisch gesetzt\",\n        \"path_duplicated\": \"Pfad dupliziert\",\n        \"command_required\": \"Befehl ist erforderlich\",\n        \"command_invalid\": \"Ungültiger Befehl, es muss ein absoluter Pfad sein!\",\n        \"email_recipient_required\": \"Mindestens ein E-Mail-Empfänger ist erforderlich!\",\n        \"email_subject_required\": \"E-Mail Betreff ist erforderlich!\",\n        \"email_body_required\": \"E-Mail-Text ist erforderlich!\",\n        \"retention_directory_required\": \"Mindestens ein zu prüfendes Verzeichnis erforderlich!\",\n        \"path_required\": \"Mindestens ein Pfad erforderlich!\",\n        \"source_dest_different\": \"Quell- und Zielpfad müssen unterschiedlich sein!\",\n        \"root_not_allowed\": \"Der Stammpfad (/) ist nicht zulässig!\",\n        \"archive_name_required\": \"Komprimierter Archivname ist erforderlich!\",\n        \"idp_template_required\": \"Eine Benutzer- oder Administratorvorlage ist erforderlich!\",\n        \"threshold\": \"Schwellenwert\",\n        \"threshold_help\": \"Für Benutzer, deren Passwort in einer Anzahl von Tagen abläuft, die kleiner oder gleich diesem Schwellenwert ist, wird eine E-Mail-Benachrichtigung generiert\",\n        \"disable_threshold\": \"Schwellwert deaktivieren\",\n        \"disable_threshold_help\": \"Inaktivität in Tagen seit der letzten Anmeldung oder Erstellung vor der Deaktivierung von Benutzern\",\n        \"delete_threshold\": \"Schwellenwert löschen\",\n        \"delete_threshold_help\": \"Inaktivität in Tagen seit der letzten Anmeldung oder Erstellung vor dem Löschen von Benutzern\",\n        \"inactivity_threshold_required\": \"Mindestens ein Inaktivitätsschwellenwert muss definiert sein!\",\n        \"inactivity_thresholds_invalid\": \"Löschschschwellenwert muss größer sein als der Deaktivierungsschwellenwert!\",\n        \"idp_mode_add_update\": \"Erstellen oder aktualisieren\",\n        \"idp_mode_add\": \"Erstellen, wenn es nicht existiert\",\n        \"template_user_help\": \"Vorlage für SFTPGo-Benutzer im JSON-Format. Platzhalter werden unterstützt\",\n        \"template_admin_help\": \"Vorlage für SFTPGo-Admins im JSON-Format. Platzhalter werden unterstützt\",\n        \"placeholders_help\": \"Platzhalter werden unterstützt\",\n        \"http_headers\": \"HTTP-Headers\",\n        \"query_parameters\": \"Abfragezeichen-Parameter\",\n        \"http_timeout_help\": \"Ignoriert für mehrteilige Anfragen mit Dateien als Anhänge\",\n        \"body\": \"Textkörper\",\n        \"http_body_help\": \"Platzhalter werden unterstützt. Wird bei HTTP-Get-Anfragen ignoriert. Bei mehrteiligen Anfragen leer lassen\",\n        \"multipart_body\": \"Mehrteiliger Textkörper\",\n        \"multipart_body_help\": \"HTTP-Multipart-Anfragen ermöglichen die Kombination eines oder mehrerer Datensätze zu einem einzigen Textkörper. Für jeden Teil können Sie einen Dateipfad oder einen Textkörper als Text festlegen. Platzhalter werden in Dateipfad, Textkörper und Header-Werten unterstützt\",\n        \"http_part_name\": \"Teilname\",\n        \"http_part_file\": \"Dateipfad\",\n        \"http_part_headers\": \"Zusätzliche Teilüberschriften, eine pro Zeile als \\\"Schlüssel: Wert\\\"\",\n        \"command_help\": \"Absoluter Pfad des auszuführenden Befehls\",\n        \"command_args\": \"Argumente\",\n        \"command_args_help\": \"Kommagetrennte Befehlsargumente. Platzhalter werden unterstützt\",\n        \"command_env_vars_help\": \"Platzhalter werden in Werten unterstützt. Setzen des Wertes auf \\\"$\\\" ohne Anführungszeichen bedeutet, den Schlüssel aus der Umgebung abzurufen, ohne Schlüssel, welche mit SFTPGO_ beginnen.\",\n        \"email_recipients\": \"An\",\n        \"email_recipients_help\": \"Komma getrennte Empfänger. Platzhalter werden unterstützt\",\n        \"email_bcc\": \"Bcc\",\n        \"email_bcc_help\": \"Komma getrennte BCC-Adressen. Platzhalter werden unterstützt\",\n        \"email_subject\": \"Thema\",\n        \"content_type\": \"Inhaltstyp\",\n        \"attachments\": \"Anhänge\",\n        \"attachments_help\": \"Komma getrennte Pfade zum Anhängen. Platzhalter werden unterstützt. Die Gesamtgröße ist auf 10 MB begrenzt\",\n        \"data_retention\": \"Datenaufbewahrung\",\n        \"data_retention_help\": \"Legen Sie die Datenaufbewahrung in Stunden pro Pfad fest. Die Aufbewahrung wird rekursiv angewendet. Wenn Sie 0 als Aufbewahrung festlegen, wird der angegebene Pfad ausgeschlossen\",\n        \"delete_empty_dirs\": \"Leere Verzeichnisse löschen\",\n        \"fs_action\": \"Dateisystemaktion\",\n        \"paths_src_dst_help\": \"Pfade, wie sie von SFTPGo-Benutzern gesehen werden. Platzhalter werden unterstützt. Die erforderlichen Berechtigungen werden automatisch erteilt\",\n        \"source_path\": \"Quelle\",\n        \"target_path\": \"Ziel\",\n        \"paths_help\": \"Komma getrennte Pfade, wie sie von SFTPGo-Benutzern gesehen werden. Platzhalter werden unterstützt. Die erforderlichen Berechtigungen werden automatisch erteilt\",\n        \"archive_path\": \"Archivpfad\",\n        \"archive_path_help\": \"Vollständiger Pfad, wie er von SFTPGo-Benutzern gesehen wird, zum zu erstellenden Zip-Archiv. Platzhalter werden unterstützt. Wenn die angegebene Datei bereits existiert, wird sie überschrieben\",\n        \"placeholders_modal_title\": \"Unterstützte Platzhalter\",\n        \"update_mod_times\": \"Zeitstempel aktualisieren\",\n        \"types\": {\n            \"http\": \"HTTP\",\n            \"email\": \"E-Mail\",\n            \"backup\": \"Sicherung\",\n            \"user_quota_reset\": \"Benutzerkontingente zurücksetzen\",\n            \"folder_quota_reset\": \"Ordnerkontingente zurücksetzen\",\n            \"transfer_quota_reset\": \"Transfervolumen zurücksetzen\",\n            \"data_retention_check\": \"Datenaufbewahrung prüfen\",\n            \"filesystem\": \"Dateisystem\",\n            \"password_expiration_check\": \"Passwort-Ablaufprüfung\",\n            \"user_expiration_check\": \"Benutzer-Ablaufprüfung\",\n            \"user_inactivity_check\": \"Benutzerinaktivität prüfen\",\n            \"idp_check\": \"Identitätsanbieterkonto prüfen\",\n            \"rotate_logs\": \"Protokolldatei rotieren\",\n            \"command\": \"Befehl\"\n        },\n        \"fs_types\": {\n            \"rename\": \"Umbenennen\",\n            \"delete\": \"Löschen\",\n            \"path_exists\": \"Pfade existieren\",\n            \"compress\": \"Komprimieren\",\n            \"copy\": \"Kopieren\",\n            \"create_dirs\": \"Verzeichnisse erstellen\"\n        },\n        \"placeholders_modal\": {\n            \"name\": \"Benutzername, Name des virtuellen Ordners, Administratorbenutzername für Providerereignisse, Domänenname für TLS-Zertifikatereignisse\",\n            \"event\": \"Ereignisname, z. B. \\\"Upload\\\", \\\"Download\\\" für Dateisystem-Ereignisse oder \\\"Hinzufügen\\\", \\\"Aktualisieren\\\" für Provider-Ereignisse\",\n            \"status\": \"Status für Dateisystemereignisse. 1 bedeutet, dass kein Fehler aufgetreten ist, 2 bedeutet, dass ein allgemeiner Fehler aufgetreten ist, 3 bedeutet, dass das Kontingent überschritten wurde\",\n            \"status_string\": \"Status als String. Mögliche Werte \\\"OK\\\", \\\"KO\\\"\",\n            \"error_string\": \"Fehlerdetails. Wird durch eine leere Zeichenfolge ersetzt, wenn keine Fehler auftreten\",\n            \"virtual_path\": \"Für SFTPGo-Benutzer sichtbarer Pfad, beispielsweise \\\"/adir/afile.txt\\\"\",\n            \"escaped_virtual_path\": \"Als HTTP-Abfragezeichenfolge codierter Pfad, beispielsweise \\\"%2Fadir%2Fafile.txt\\\".\",\n            \"virtual_dir_path\": \"Übergeordnetes Verzeichnis für \\\"Virtueller Pfad\\\", z. B. wenn \\\"Virtueller Pfad\\\" \\\"/adir/afile.txt\\\" ist, ist \\\"Virtueller Verzeichnispfad\\\" \\\"/adir\\\"\",\n            \"fs_path\": \"Vollständiger Dateisystempfad, zum Beispiel \\\"/user/homedir/adir/afile.txt\\\" oder \\\"C:/data/user/homedir/adir/afile.txt\\\" unter Windows\",\n            \"ext\": \"Dateierweiterung, zum Beispiel \\\".txt\\\", wenn der Dateiname \\\"afile.txt\\\" lautet\",\n            \"object_name\": \"Datei/Verzeichnisname, zum Beispiel \\\"afile.txt\\\" oder Provider Objektname\",\n            \"object_basename\": \"Dateiname ohne Erweiterung, zum Beispiel \\\"Datei\\\", wenn der Dateiname \\\"Datei.txt\\\" ist.\",\n            \"object_type\": \"Objekttyp für Provider-Ereignisse: \\\"Benutzer\\\", \\\"Gruppe\\\", \\\"Administrator\\\" usw.\",\n            \"virtual_target_path\": \"Virtueller Zielpfad für Umbenennungs- und Kopiervorgänge\",\n            \"virtual_target_dir_path\": \"Übergeordnetes Verzeichnis für \\\"Virtuellen Zielpfad\\\"\",\n            \"target_name\": \"Zielobjektname für Umbenennungs- und Kopiervorgänge\",\n            \"fs_target_path\": \"Vollständiger Zielpfad des Dateisystems für Umbenennungs- und Kopiervorgänge\",\n            \"file_size\": \"Dateigröße (Bytes)\",\n            \"elapsed\": \"Verstrichene Zeit in Millisekunden für Dateisystemereignisse\",\n            \"protocol\": \"Protokoll, zum Beispiel \\\"SFTP\\\", \\\"FTP\\\"\",\n            \"ip\": \"Client IP-Adresse\",\n            \"role\": \"Benutzer- oder Admin-Rolle\",\n            \"timestamp\": \"Ereigniszeitstempel als Nanosekunden seit Epoche\",\n            \"datetime\": \"Ereigniszeitstempel im Format JJJJ-MM-TTTHH:MM:SS.ZZZ\",\n            \"year\": \"Jahr des Vorkommnisses als vierstellige Zeichen\",\n            \"month\": \"Monat des Vorkommnisses als zweistellige Zeichen\",\n            \"day\": \"Tag des Vorkommnisses als zweistellige Zeichen\",\n            \"hour\": \"Stunde des Vorkommnisses als zweistellige Zeichen\",\n            \"minute\": \"Minute des Vorkommnisses als zweistellige Zeichen\",\n            \"email\": \"Bei Dateisystem-Ereignissen ist dies die E-Mail-Adresse des Benutzers, der die Aktion ausführt. Bei Anbieter-Ereignissen ist dies die E-Mail-Adresse des betroffenen Benutzers oder Administrators. In allen anderen Fällen ist das Feld leer\",\n            \"object_data\": \"Als JSON serialisierte Providerobjektdaten, wobei sensible Felder entfernt wurden\",\n            \"object_data_string\": \"Providerobjektdaten als JSON-Escape-String mit entfernten sensiblen Feldern\",\n            \"retention_reports\": \"Datenaufbewahrungsberichte als komprimierte ZIP-CSV-Dateien. Unterstützt als E-Mail-Anhang, Dateipfad für mehrteilige HTTP-Anfragen und als einzelner Parameter für den Text der HTTP-Anfragen\",\n            \"idp_field\": \"Benutzerdefinierte Felder des Identitätsanbieters, die eine Zeichenfolge enthalten\",\n            \"metadata\": \"Cloud-Speichermetadaten für die heruntergeladene Datei, serialisiert als JSON\",\n            \"metadata_string\": \"Cloud-Speicher-Metadaten für die heruntergeladene Datei als JSON-Escape-String\",\n            \"uid\": \"Eindeutige ID\"\n        }\n    },\n    \"rules\": {\n        \"view_manage\": \"Anzeigen und Verwalten von Regeln für Ereignisse\",\n        \"trigger\": \"Auslöser\",\n        \"run_confirm\": \"Möchten Sie diese Regel jetzt ausführen?\",\n        \"run_confirm_btn\": \"Ja, ausführen\",\n        \"run_error_generic\": \"Ausgewählte Regel kann nicht ausgeführt werden!\",\n        \"run_ok\": \"Regelaktionen gestartet\",\n        \"run\": \"Ausführen\",\n        \"invalid_fs_min_size\": \"Ungültige Mindestgröße\",\n        \"invalid_fs_max_size\": \"Ungültige maximale Größe\",\n        \"action_required\": \"Mindestens eine Aktion ist erforderlich!\",\n        \"fs_event_required\": \"Mindestens ein Dateisystemereignis ist erforderlich!\",\n        \"provider_event_required\": \"Mindestens ein Provider-Event ist erforderlich!\",\n        \"schedule_required\": \"Mindestens ein Zeitplan ist erforderlich!\",\n        \"schedule_invalid\": \"Ungültiger Zeitplan!\",\n        \"duplicate_actions\": \"Doppelte Aktionen erkannt!\",\n        \"sync_failure_actions\": \"Synchrone Ausführung wird für Fehleraktionen nicht unterstützt!\",\n        \"sync_unsupported\": \"Synchrone Ausführung wird nur für bestimmte Dateisystemereignisse und Identity-Provider-Anmeldungen unterstützt!\",\n        \"sync_unsupported_fs_event\": \"Synchrone Ausführung wird nur für Upload- und Pre-*-Dateisystemereignisse unterstützt\",\n        \"only_failure_actions\": \"Mindestens eine Nicht-Fehler-Aktion erforderlich!\",\n        \"sync_action_required\": \"Ereignis \\\"{{val}}\\\" erfordert mindestens eine synchrone Aktion!\",\n        \"scheduler_help\": \"Stunden: 0-23. Wochentag: 0-6 (Sonntag-Samstag). Tag des Monats: 1-31. Monat: 1-12. Ein Sternchen (*) zeigt eine Übereinstimmung für alle Werte des Felds an. z. B. jeder Tag der Woche, jeder Tag des Monats usw.\",\n        \"concurrent_run\": \"Gleichzeitige Ausführung von mehreren Instanzen zulassen!\",\n        \"protocol_filters\": \"Protokollfilter\",\n        \"status_filters\": \"Statusfilter\",\n        \"object_filters\": \"Objektfilter\",\n        \"name_filters\": \"Namensfilter\",\n        \"name_filters_help\": \"Shell-ähnliche Musterfilter für Benutzernamen und Ordnernamen. Beispielsweise findet \\\"user*\\\" Namen, die mit \\\"user\\\" beginnen. Bei Provider-Ereignissen wird dieser Filter auf den Benutzernamen des Administrators angewendet, der das Ereignis ausführt\",\n        \"inverse_match\": \"Umgekehrte Übereinstimmung\",\n        \"group_name_filters\": \"Gruppennamenfilter\",\n        \"group_name_filters_help\": \"Shell-ähnliche Musterfilter für Gruppennamen. Beispielsweise findet \\\"gruppe*\\\" Gruppennamen, die mit \\\"gruppe\\\" beginnen\",\n        \"role_name_filters\": \"Rollennamenfilter\",\n        \"role_name_filters_help\": \"Shell-ähnliche Musterfilter für Rollennamen. Beispielsweise findet \\\"rolle*\\\" Rollennamen, die mit \\\"rolle\\\" beginnen\",\n        \"path_filters\": \"Pfadfilter\",\n        \"path_filters_help\": \"Shell-ähnliche Musterfilter für Dateisystem-Ereignispfade. Beispielsweise stimmt \\\"/adir/*.txt\\\" mit Pfaden im Verzeichnis \\\"/adir\\\" überein, die mit \\\".txt\\\" enden. Doppelte Sternchen werden unterstützt, beispielsweise stimmt \\\"/**/*.txt\\\" mit jeder Datei überein, die mit \\\".txt\\\" endet. \\\"/mydir/**\\\" stimmt mit jedem Eintrag in \\\"/mydir\\\" überein\",\n        \"file_size_limits\": \"Dateigrößenbeschränkungen\",\n        \"file_size_limits_help\": \"0 bedeutet keine Beschränkung. Sie können das MB/GB-Suffix verwenden\",\n        \"min_size\": \"Mindestgröße\",\n        \"max_size\": \"Maximale Größe\",\n        \"actions_help\": \"Eine oder mehrere auszuführende Aktionen. Die Option \\\"Synchronisierung ausführen\\\" wird für \\\"Upload\\\"-Ereignisse unterstützt und ist für \\\"pre-*\\\"-Ereignisse und Identitätsanbieter-Anmeldeereignisse erforderlich, wenn die Aktion das Konto überprüft\",\n        \"option_failure_action\": \"Fehleraktion\",\n        \"option_stop_on_failure\": \"Bei Fehler anhalten\",\n        \"option_execute_sync\": \"Synchrone Ausführung\",\n        \"no_filter\": \"Kein Filter bedeutet immer auslösende Ereignisse!\",\n        \"action_placeholder\": \"Auswählen einer Aktion\",\n        \"triggers\": {\n            \"fs_event\": \"Dateisystemereignisse\",\n            \"provider_event\": \"Anbieterereignisse\",\n            \"ip_blocked\": \"IP blockiert\",\n            \"certificate_renewal\": \"Zertifikatserneuerung\",\n            \"on_demand\": \"Auf Anfrage\",\n            \"idp_login\": \"Identitätsanbieter-Anmeldungen\",\n            \"schedule\": \"Zeitpläne\"\n        },\n        \"idp_logins\": {\n            \"user\": \"Benutzeranmeldung\",\n            \"admin\": \"Administrator-Anmeldung\"\n        }\n    }\n}\n"
  },
  {
    "path": "static/locales/en/translation.json",
    "content": "{\n    \"title\": {\n        \"setup\": \"Initial Setup\",\n        \"login\": \"Login\",\n        \"share_login\": \"Share Login\",\n        \"profile\": \"Profile\",\n        \"change_password\": \"Change password\",\n        \"files\": \"Files\",\n        \"shares\": \"Shares\",\n        \"add_share\": \"Add share\",\n        \"update_share\": \"Update share\",\n        \"two_factor_auth\": \"Two-factor authentication\",\n        \"two_factor_auth_short\": \"2FA\",\n        \"edit_file\": \"Edit file\",\n        \"view_file\": \"View file\",\n        \"recovery_password\": \"Password recovery\",\n        \"reset_password\": \"Password reset\",\n        \"shared_files\": \"Shared files\",\n        \"upload_to_share\": \"Upload to share\",\n        \"download_shared_file\": \"Download shared file\",\n        \"share_access_error\": \"Unable to access the share\",\n        \"invalid_auth_request\": \"Invalid authentication request\",\n        \"error400\": \"Bad Request\",\n        \"error403\": \"Forbidden\",\n        \"error404\": \"Not Found\",\n        \"error416\": \"Requested Range Not Satisfiable\",\n        \"error429\": \"Too Many Requests\",\n        \"error500\": \"Internal Server Error\",\n        \"errorPDF\": \"Unable to show PDF file\",\n        \"error_editor\": \"Cannot open file editor\",\n        \"users\": \"Users\",\n        \"groups\": \"Groups\",\n        \"folders\": \"Virtual folders\",\n        \"connections\": \"Active connections\",\n        \"event_manager\": \"Event Manager\",\n        \"event_rules\": \"Rules\",\n        \"event_actions\": \"Actions\",\n        \"ip_manager\": \"IP Manager\",\n        \"ip_lists\": \"IP Lists\",\n        \"defender\": \"Auto Block List\",\n        \"admins\": \"Admins\",\n        \"roles\": \"Roles\",\n        \"server_manager\": \"Server Manager\",\n        \"configs\": \"Configurations\",\n        \"logs\": \"Logs\",\n        \"maintenance\": \"Maintenance\",\n        \"status\": \"Status\",\n        \"add_user\": \"Add user\",\n        \"update_user\": \"Update user\",\n        \"template_user\": \"User template\",\n        \"template_admin\": \"Admin template\",\n        \"add_group\": \"Add group\",\n        \"update_group\": \"Update group\",\n        \"add_folder\": \"Add virtual folder\",\n        \"update_folder\": \"Update virtual folder\",\n        \"template_folder\": \"Virtual folder template\",\n        \"oauth2_error\": \"Unable to complete OAuth2 flow\",\n        \"oauth2_success\": \"OAuth2 flow completed\",\n        \"add_role\": \"Add role\",\n        \"update_role\": \"Update role\",\n        \"add_admin\": \"Add admin\",\n        \"update_admin\": \"Update admin\",\n        \"add_ip_list\": \"Add IP list entry\",\n        \"update_ip_list\": \"Update IP list entry\",\n        \"add_action\": \"Add action\",\n        \"update_action\": \"Update action\",\n        \"add_rule\": \"Add rule\",\n        \"update_rule\": \"Update rule\"\n    },\n    \"setup\": {\n        \"desc\": \"To start using SFTPGo you need to create an administrator user\",\n        \"submit\": \"Create admin and Sign in\",\n        \"install_code_mismatch\": \"The installation code does not match\",\n        \"help_text\": \"Commercial support\"\n    },\n    \"login\": {\n        \"username\": \"Username\",\n        \"password\": \"Password\",\n        \"your_username\": \"Your username\",\n        \"forgot_password\": \"Forgot Password?\",\n        \"forgot_password_msg\": \"Enter your account username below, you will receive a password reset code by email.\",\n        \"send_reset_code\": \"Send Reset Code\",\n        \"signin\": \"Sign in\",\n        \"signin_openid\": \"Sign in with OpenID\",\n        \"signout\": \"Sign out\",\n        \"auth_code\": \"Authentication code\",\n        \"two_factor_help\": \"Open the two-factor authentication app on your device to view your authentication code and verify your identity.\",\n        \"two_factor_msg\": \"Enter a two-factor recovery code\",\n        \"recovery_code\": \"Recovery code\",\n        \"recovery_code_msg\": \"You can enter one of your recovery codes in case you lost access to your mobile device.\",\n        \"reset_password\": \"Reset Password\",\n        \"reset_pwd_msg\": \"Check your email for the confirmation code\",\n        \"reset_submit\": \"Update Password and Sign in\",\n        \"confirm_code\": \"Confirmation code\",\n        \"reset_pwd_forbidden\": \"You are not allowed to reset your password\",\n        \"reset_pwd_no_email\": \"Your account does not have an email address, it is not possible to reset your password by sending an email verification code\",\n        \"reset_pwd_send_email_err\": \"Unable to send confirmation code via email\",\n        \"reset_pwd_err_generic\": \"Unexpected error while resetting password\",\n        \"reset_ok_login_error\": \"The password reset completed successfully but an unexpected error occurred while signing in\",\n        \"ip_not_allowed\": \"Login is not allowed from this IP address\",\n        \"two_factor_required\": \"Set up two-factor authentication, it is required for the following protocols: {{val}}\",\n        \"two_factor_required_generic\": \"Set up two-factor authentication, it is mandatory for your account\",\n        \"link\": \"Go to {{link}}\"\n    },\n    \"theme\": {\n        \"light\": \"Light\",\n        \"dark\": \"Dark\",\n        \"system\": \"Auto\"\n    },\n    \"general\": {\n        \"invalid_auth_request\": \"The authentication request does not meet security requirements\",\n        \"invalid_input\": \"Invalid input. Slashes (/ ), colons (:), control characters, and reserved system names are not allowed\",\n        \"error400\": \"The received request is not valid\",\n        \"error403\": \"You do not have the required permissions\",\n        \"error404\": \"The requested resource does not exist\",\n        \"error416\": \"The requested file fragment could not be returned\",\n        \"error429\": \"Rate limit exceeded\",\n        \"error500\": \"The server is unable to fulfill your request\",\n        \"errorPDF\": \"This file does not look like a PDF\",\n        \"error_edit_dir\": \"Cannot edit a directory\",\n        \"error_edit_size\": \"The file size exceeds the maximum allowed size\",\n        \"invalid_form\": \"Invalid form\",\n        \"invalid_credentials\": \"Invalid credentials, please retry\",\n        \"invalid_csrf\": \"The form token is not valid\",\n        \"invalid_token\": \"Invalid permissions\",\n        \"confirm_logout\": \"Are you sure you want to sign out?\",\n        \"wait\": \"Please wait...\",\n        \"ok\": \"OK\",\n        \"failed\": \"Failed\",\n        \"cancel\": \"No, back\",\n        \"submit\": \"Save\",\n        \"back\": \"Back\",\n        \"add\": \"Add\",\n        \"enable\": \"Enable\",\n        \"disable\": \"Disable\",\n        \"disable_confirm_btn\": \"Yes, disable\",\n        \"close\": \"Close\",\n        \"search\": \"Search\",\n        \"configuration\": \"Configuration\",\n        \"config_saved\": \"Configuration saved\",\n        \"rename\": \"Rename\",\n        \"confirm\": \"Yes, proceed\",\n        \"edit\": \"Edit\",\n        \"delete\": \"Delete\",\n        \"delete_confirm_btn\": \"Yes, delete\",\n        \"delete_confirm\": \"Do you want to delete \\\"{{- name}}\\\"? This action is irreversible\",\n        \"delete_confirm_generic\": \"Do you want to delete the selected item? This action is irreversible\",\n        \"delete_multi_confirm\": \"Do you want to delete the selected items? This action is irreversible\",\n        \"delete_error_generic\": \"Unable to delete the selected item\",\n        \"delete_error_403\": \"$t(general.delete_error_generic). $t(general.error403)\",\n        \"delete_error_404\": \"$t(general.delete_error_generic). $t(general.error404)\",\n        \"loading\": \"Loading...\",\n        \"name\": \"Name\",\n        \"size\": \"Size\",\n        \"last_modified\": \"Last Modified\",\n        \"info\": \"Info\",\n        \"datetime\": \"{{- val, datetime}}\",\n        \"selected_items_one\": \"{{count}} item selected\",\n        \"selected_items_other\": \"{{count}} items selected\",\n        \"selected_with_placeholder\": \"%d items selected\",\n        \"name_required\": \"Name is required\",\n        \"name_different\": \"The new name must be different from the current name\",\n        \"html5_media_not_supported\": \"Your browser does not support HTML5 audio/video.\",\n        \"choose_target_folder\": \"Choose target folder\",\n        \"source_name\": \"Source name\",\n        \"target_folder\": \"Target folder\",\n        \"dest_name\": \"Destination name\",\n        \"my_profile\": \"My profile\",\n        \"description\": \"Description\",\n        \"email\": \"Email\",\n        \"api_key_auth\": \"API key authentication\",\n        \"api_key_auth_help\": \"Allow to impersonate yourself, in REST API, using an API key\",\n        \"pub_keys\": \"Public keys\",\n        \"pub_key_placeholder\": \"Paste a public key here\",\n        \"pub_keys_help\": \"Public keys can be used for SFTP authentication\",\n        \"verify\": \"Verify\",\n        \"problems\": \"Having problems?\",\n        \"allowed_ip_mask\": \"Allowed IP/Mask\",\n        \"denied_ip_mask\": \"Denied IP/Mask\",\n        \"ip_mask_help\": \"Comma separated IP/Mask in CIDR format, for example \\\"192.168.1.0/24,10.8.0.100/32\\\"\",\n        \"allowed_ip_mask_invalid\": \"Invalid allowed IP/Mask\",\n        \"username_required\": \"The username is required\",\n        \"password_required\": \"The password is required\",\n        \"foldername_required\": \"The folder name is required\",\n        \"err_user\": \"Unable to validate your user\",\n        \"err_protocol_forbidden\": \"HTTP protocol is not allowed for your user\",\n        \"pwd_login_forbidden\": \"The password login method is not allowed for your user\",\n        \"ip_forbidden\": \"Login not allowed from this IP address\",\n        \"email_invalid\": \"The email address is invalid\",\n        \"err_password_complexity\": \"The password provided does not meet the complexity requirements\",\n        \"no_oidc_feature\": \"This feature is not available if you are logged in with OpenID\",\n        \"connection_forbidden\": \"You are not allowed to connect\",\n        \"no_permissions\": \"You are not allowed to change anything\",\n        \"path_invalid\": \"Invalid path\",\n        \"err_quota_read\": \"Read denied due to quota limit\",\n        \"profile_updated\": \"Your profile has been successfully updated\",\n        \"share_ok\": \"Share access successful, you can now use your link\",\n        \"qr_code\": \"QR Code\",\n        \"copy_link\": \"Copy link\",\n        \"copied\": \"Copied\",\n        \"active\": \"Active\",\n        \"inactive\": \"Inactive\",\n        \"colvis\": \"Column visibility\",\n        \"actions\": \"Actions\",\n        \"template\": \"Use as a template\",\n        \"quota_scan\": \"Quota scan\",\n        \"quota_scan_started\": \"Quota scan started. It can take a while depending on the number of files to check\",\n        \"quota_scan_conflit\": \"Another scan is already in progress\",\n        \"quota_scan_error\": \"Unable to start quota scan\",\n        \"role\": \"Role\",\n        \"role_placeholder\": \"Select a role\",\n        \"group_placeholder\": \"Select a group\",\n        \"folder_placeholder\": \"Select a folder\",\n        \"blank_default_help\": \"Leave blank for default\",\n        \"skip_tls_verify\": \"Skip TLS verify. This should be used only for testing\",\n        \"advanced_settings\": \"Advanced settings\",\n        \"default\": \"Default\",\n        \"private_key\": \"Private key\",\n        \"acls\": \"ACLs\",\n        \"quota_limits\": \"Disk quota and bandwidth limits\",\n        \"expiration\": \"Expiration\",\n        \"expiration_help\": \"Pick an expiration date\",\n        \"additional_info\": \"Additional info\",\n        \"permissions\": \"Permissions\",\n        \"visible\": \"Visible\",\n        \"hidden\": \"Hidden\",\n        \"allowed\": \"Allowed\",\n        \"denied\": \"Denied\",\n        \"zero_no_limit_help\": \"0 means no limit\",\n        \"global_settings\": \"Global settings\",\n        \"mandatory_encryption\": \"Mandatory encryption\",\n        \"name_invalid\": \"The specified name is not valid, the following characters are allowed: a-zA-Z0-9-_.~\",\n        \"associations\": \"Associations\",\n        \"template_placeholders\": \"The following placeholders are supported\",\n        \"duplicated_username\": \"The specified username already exists\",\n        \"duplicated_name\": \"The specified name already exists\",\n        \"permissions_required\": \"Permissions are required\",\n        \"configs_saved\": \"Configurations has been successfully updated\",\n        \"protocol\": \"Protocol\",\n        \"refresh\": \"Refresh\",\n        \"members\": \"Members\",\n        \"members_summary\": \"Users: {{users}}. Admins: {{admins}}\",\n        \"status\": \"Status\",\n        \"last_login\": \"Last login\",\n        \"previous\": \"Previous\",\n        \"next\": \"Next\",\n        \"type\": \"Type\",\n        \"issuer\": \"Issuer\",\n        \"data_provider\": \"Database\",\n        \"driver\": \"Driver\",\n        \"mode\": \"Mode\",\n        \"port\": \"Port\",\n        \"domain\": \"Domain\",\n        \"test\": \"Test\",\n        \"get\": \"Get\",\n        \"export\": \"Export\",\n        \"value\": \"Value\",\n        \"method\": \"Method\",\n        \"timeout\": \"Timeout\",\n        \"env_vars\": \"Environment variables\",\n        \"hours\": \"Hours\",\n        \"paths\": \"Paths\",\n        \"hour\": \"Hour\",\n        \"day_of_week\": \"Day of week\",\n        \"day_of_month\": \"Day of month\",\n        \"month\": \"Month\",\n        \"options\": \"Options\",\n        \"expired\": \"Expired\",\n        \"unsupported\": \"Feature no longer supported\",\n        \"start\": \"Start (HH:MM)\",\n        \"end\": \"End (HH:MM)\",\n        \"monday\": \"Monday\",\n        \"tuesday\": \"Tuesday\",\n        \"wednesday\": \"Wednesday\",\n        \"thursday\": \"Thursday\",\n        \"friday\": \"Friday\",\n        \"saturday\": \"Saturday\",\n        \"sunday\": \"Sunday\",\n        \"expired_session\": \"Your session has expired. Please log in again\"\n    },\n    \"fs\": {\n        \"view_file\": \"View file \\\"{{- path}}\\\"\",\n        \"edit_file\": \"Edit file \\\"{{- path}}\\\"\",\n        \"new_folder\": \"New Folder\",\n        \"select_across_pages\": \"Select across pages\",\n        \"download\": \"Download\",\n        \"download_ready\": \"Your download is ready\",\n        \"upload_queue_not_ready\": \"The upload queue is still loading, please try again in a few moments\",\n        \"upload_no_files\": \"No files selected. Please choose or drag files to upload\",\n        \"move_copy\": \"Move or copy\",\n        \"share\": \"Share\",\n        \"home\": \"Home\",\n        \"create_folder_msg\": \"Folder name\",\n        \"no_more_subfolders\": \"No more subfolders in here\",\n        \"no_files_folders\": \"No files or folders\",\n        \"invalid_name\": \"File or folder names cannot contain \\\"/\\\"\",\n        \"folder_name_required\": \"Folder name is required\",\n        \"deleting\": \"Delete {{idx}}/{{total}}\",\n        \"copying\": \"Copy {{idx}}/{{total}}\",\n        \"moving\": \"Move {{idx}}/{{total}}\",\n        \"uploading\": \"Upload {{idx}}/{{total}}\",\n        \"err_403\": \"Permission denied\",\n        \"err_429\": \"Too many concurrent requests\",\n        \"err_generic\": \"Unable to access the requested resource\",\n        \"err_validation\": \"Invalid filesystem configuration\",\n        \"err_exists\": \"The destination already exists\",\n        \"dir_list\": {\n            \"err_generic\": \"Failed to get directory listing\",\n            \"err_403\": \"$t(fs.dir_list.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.dir_list.err_generic). $t(fs.err_429)\",\n            \"err_user\": \"$t(fs.dir_list.err_generic). $t(general.err_user)\"\n        },\n        \"create_dir\": {\n            \"err_generic\": \"Error creating new folder\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"save\": {\n            \"err_generic\": \"Error saving file\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"delete\": {\n            \"err_generic\": \"Unable to delete \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.delete.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete.err_generic). $t(fs.err_429)\"\n        },\n        \"delete_multi\": {\n            \"err_generic_partial\": \"Not all the selected items have been deleted, please reload the page\",\n            \"err_generic\": \"Unable to delete the selected items\",\n            \"err_403_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)\",\n            \"err_429_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)\",\n            \"err_403\": \"$t(fs.delete_multi.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete_multi.err_generic). $t(fs.err_429)\"\n        },\n        \"copy\": {\n            \"msg\": \"Copy\",\n            \"err_generic\": \"Error copying files/directories\",\n            \"err_403\": \"$t(fs.copy.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.copy.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.copy.err_generic). $t(fs.err_exists)\"\n        },\n        \"move\": {\n            \"msg\": \"Move\",\n            \"err_generic\": \"Error moving files/directories\",\n            \"err_403\": \"$t(fs.move.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.move.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.move.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"Unsupported: if you want to move a directory make sure it is empty\"\n        },\n        \"rename\": {\n            \"title\": \"Rename \\\"{{- name}}\\\"\",\n            \"new_name\": \"New name\",\n            \"err_generic\": \"Unable to rename \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.rename.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.rename.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.rename.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"Unsupported: if you want to rename a directory make sure it is empty\"\n        },\n        \"upload\": {\n            \"text\": \"Upload Files\",\n            \"success\": \"Files uploaded successfully\",\n            \"message\": \"Drop files here or click to upload.\",\n            \"message_empty\": \"This directory is empty. $t(fs.upload.message)\",\n            \"err_generic\": \"Error uploading files\",\n            \"err_403\": \"$t(fs.upload.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.upload.err_generic). $t(fs.err_429)\",\n            \"err_dir_overwrite\": \"$t(fs.upload.err_generic). There are directories with the same name as the files: {{- val}}\",\n            \"overwrite_text\": \"Conflict detected. Do you want to overwrite the following files/directories?\"\n        },\n        \"quota_usage\": {\n            \"title\": \"Quota usage\",\n            \"disk\": \"Disk quota\",\n            \"size\": \"Size: {{- val}}\",\n            \"size_percentage\": \"Size: {{- val}} ({{percentage}}%)\",\n            \"files\": \"Files: {{- val}}\",\n            \"files_percentage\": \"Files: {{- val}} ({{percentage}}%)\",\n            \"transfer\": \"Transfer quota\",\n            \"total\": \"Total: {{- val}}\",\n            \"total_percentage\": \"Total: {{- val}} ({{percentage}}%)\",\n            \"uploads\": \"Uploads: {{- val}}\",\n            \"uploads_percentage\": \"Uploads: {{- val}} ({{percentage}}%)\",\n            \"downloads\": \"Downloads: {{- val}}\",\n            \"downloads_percentage\": \"Downloads: {{- val}} ({{percentage}}%)\"\n        }\n    },\n    \"datatable\": {\n        \"info\": \"Showing _START_ to _END_ of _TOTAL_ records\",\n        \"info_empty\": \"Showing no record\",\n        \"info_filtered\": \"(filtered from _MAX_ total records)\",\n        \"processing\": \"Processing...\",\n        \"no_records\": \"No records found\"\n    },\n    \"editor\": {\n        \"keybinding\": \"Editor keybindings\",\n        \"search\": \"Open search panel\",\n        \"goto\": \"Jump to line\",\n        \"indent_more\": \"Indent more\",\n        \"indent_less\": \"Indent less\"\n    },\n    \"2fa\": {\n        \"title\": \"Two-factor authentication using Authenticator apps\",\n        \"msg_enabled\": \"Two-factor authentication is enabled\",\n        \"msg_disabled\": \"Secure Your Account\",\n        \"msg_info\": \"Two-factor authentication adds an extra layer of security to your account. To log in you'll need to provide an additional authentication code.\",\n        \"require\": \"Two-factor authentication is required for this account\",\n        \"require_for\": \"Require 2FA for\",\n        \"generate\": \"Generate new secret key\",\n        \"recovery_codes\": \"Recovery codes\",\n        \"new_recovery_codes\": \"New recovery codes\",\n        \"recovery_codes_msg1\": \"Recovery codes are one-time-use backup codes for logging in when you can't access your authentication app. Use them to access your account and manage two-factor authentication settings if you lose your phone.\",\n        \"recovery_codes_msg2\": \"To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.\",\n        \"recovery_codes_msg3\": \"If you generate new recovery codes, you automatically invalidate old ones.\",\n        \"info_title\": \"Learn about two-factor authentication\",\n        \"info1\": \"SSH protocol (SFTP/SCP/SSH commands) will ask for the passcode if the client uses keyboard interactive authentication.\",\n        \"info2\": \"HTTP protocol means Web UI and REST APIs. Web UI will ask for the passcode using a specific page. For REST API you have to add the passcode using an HTTP header.\",\n        \"info3\": \"FTP has no standard way to support two-factor authentication, if you enable the FTP support, you have to add the TOTP passcode after the password. For example if your password is \\\"password\\\" and your one time passcode is \\\"123456\\\" you have to use \\\"password123456\\\" as password.\",\n        \"info4\": \"WebDAV is not supported since each single request must be authenticated and a passcode cannot be reused.\",\n        \"setup_title\": \"Set up two-factor authentication\",\n        \"setup_msg\": \"Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.\",\n        \"setup_help\": \"If you have trouble using the QR code, select manual entry on your app, and enter the code:\",\n        \"disable_msg\": \"Disable 2FA\",\n        \"disable_confirm\": \"Do you want to disable two-factor authentication for the selected user? This action is generally only required if the user has lost access to the second authentication factor\",\n        \"disable_question\": \"Do you want to disable two-factor authentication?\",\n        \"generate_question\": \"Do you want to generate a new secret key and invalidate the previous one? Any registered Authenticator App will stop working\",\n        \"disabled\": \"Two-factor authentication is disabled\",\n        \"recovery_codes_gen_err\": \"Failed to generate new recovery codes\",\n        \"recovery_codes_get_err\": \"Unable to obtain recovery codes\",\n        \"auth_code_invalid\": \"Failed to validate the provided authentication code\",\n        \"auth_secret_gen_err\": \"Failed to generate authentication secret\",\n        \"save_err\": \"Failed to save two-factor authentication configuration\",\n        \"auth_code_required\": \"The authentication code is required\",\n        \"no_protocol\": \"Please select at least a protocol\",\n        \"required_protocols\": \"The security policy configured for your account requires two-factor authentication for the following protocols: {{val}}\",\n        \"recovery_codes_generate\": \"Generate new recovery codes\",\n        \"recovery_codes_view\": \"View recovery codes\"\n    },\n    \"share\": {\n        \"scope\": \"Scope\",\n        \"scope_read\": \"Read\",\n        \"scope_write\": \"Write\",\n        \"scope_read_write\": \"Read/Write\",\n        \"scope_help\": \"For scope \\\"Write\\\" and \\\"Read/Write\\\" you have to define a single path and it must be a directory\",\n        \"path_help\": \"file or directory path, i.e. /dir or /dir/file.txt\",\n        \"password_help\": \"If set the share will be password-protected\",\n        \"max_tokens\": \"Max tokens\",\n        \"max_tokens_help\": \"Maximum number of times this share can be accessed. 0 means no limit\",\n        \"view_manage\": \"View and manage shares\",\n        \"no_share\": \"No shares\",\n        \"password_protected\": \"Password protected.\",\n        \"expiration_date\": \"Expiration: {{- val, datetime}}. \",\n        \"last_use\": \"Last use: {{- val, datetime}}. \",\n        \"usage\": \"Usage: {{used}}/{{total}}. \",\n        \"used_tokens\": \"Used token: {{used}}. \",\n        \"scope_invalid\": \"Invalid scope\",\n        \"max_tokens_invalid\": \"Invalid max tokens\",\n        \"expiration_invalid\": \"Invalid expiration\",\n        \"err_no_password\": \"You are not allowed to share files/folders without password\",\n        \"expiration_out_of_range\": \"Set an expiration date and make sure it is less than or equal to {{- val, datetime}}\",\n        \"generic\": \"Unexpected error saving share\",\n        \"path_required\": \"At least a path is required\",\n        \"path_write_scope\": \"The write scope requires exactly one path\",\n        \"nested_paths\": \"Paths cannot be nested\",\n        \"expiration_past\": \"The expiration date must be in the future\",\n        \"usage_exceed\": \"Maximum sharing usage exceeded\",\n        \"expired\": \"Sharing has expired\",\n        \"browsable_multiple_paths\": \"A share with multiple paths is not browsable\",\n        \"browsable_non_dir\": \"The share is not a directory so it is not browsable\",\n        \"go\": \"Go to share\",\n        \"access_links_title\": \"Share access links\",\n        \"link_single_title\": \"Single zip file\",\n        \"link_single_desc\": \"You can download the shared content as a single zip file\",\n        \"link_dir_title\": \"Single directory\",\n        \"link_dir_desc\": \"If the share consists of a single directory, you can browse and download files\",\n        \"link_uncompressed_title\": \"Uncompressed file\",\n        \"link_uncompressed_desc\": \"If the share consists of a single file, it can also be downloaded uncompressed\",\n        \"upload_desc\": \"You can upload one or more files to the shared directory\",\n        \"expired_desc\": \"This share is no longer accessible because it has expired\",\n        \"invalid_path\": \"The shared directory is missing or not accessible\"\n    },\n    \"select2\": {\n        \"no_results\": \"No results found\",\n        \"searching\": \"Searching...\",\n        \"removeall\": \"Remove all items\",\n        \"remove\": \"Remove item\"\n    },\n    \"change_pwd\": {\n        \"info\": \"Enter your current password, for security reasons, and then your new password twice, to verify that you have written it correctly. You will be logged out after changing your password\",\n        \"current\": \"Current password\",\n        \"new\": \"New password\",\n        \"confirm\": \"Confirm password\",\n        \"save\": \"Change my password\",\n        \"required_fields\": \"Please provide the current password and the new one two times\",\n        \"no_match\": \"The two password fields do not match\",\n        \"no_different\": \"The new password must be different from the current one\",\n        \"current_no_match\": \"Current password does not match\",\n        \"generic\": \"Unexpected error while changing password\",\n        \"required\": \"Password change is required. Set a new password to continue using your account\"\n    },\n    \"user\": {\n        \"view_manage\": \"View and manage users\",\n        \"username_reserved\": \"The specified username is reserved\",\n        \"username_invalid\": \"The specified name is not valid, the following characters are allowed: a-z A-Z 0-9 - _ . ~\",\n        \"home_required\": \"The home directory is mandatory\",\n        \"home_invalid\": \"The home directory must be an absolute path\",\n        \"pub_key_invalid\": \"Invalid public key\",\n        \"priv_key_invalid\": \"Invalid private key\",\n        \"key_invalid_size\": \"Invalid RSA public key: the minimum supported size is 2048\",\n        \"key_insecure\": \"Insecure public key format not allowed\",\n        \"err_primary_group\": \"Only one primary group is allowed\",\n        \"err_duplicate_group\": \"Duplicate groups detected\",\n        \"no_permissions\": \"Directories permissions are mandatory\",\n        \"no_root_permissions\": \"Home directory permissions are required\",\n        \"err_permissions_generic\": \"Invalid permissions: Make sure you use valid absolute paths\",\n        \"2fa_invalid\": \"Invalid configuration for two-factor authentication\",\n        \"recovery_codes_invalid\": \"Invalid recovery codes\",\n        \"folder_path_required\": \"The virtual folder mount path is required\",\n        \"folder_duplicated\": \"Duplicated virtual folders detected\",\n        \"folder_overlapped\": \"Overlapping virtual folders detected\",\n        \"folder_quota_size_invalid\": \"The quota as size of virtual folders must be greater than or equal to -1\",\n        \"folder_quota_file_invalid\": \"The quota as files of virtual folders must be greater than or equal to -1\",\n        \"folder_quota_invalid\": \"Quotas as size and as number of files of virtual folders must be both -1 or greater or equal than 0\",\n        \"ip_filters_invalid\": \"Invalid IP filters, make sure they respect CIDR notation, for example 192.168.1.0/24\",\n        \"src_bw_limits_invalid\": \"Invalid per-source bandwidth speed limits\",\n        \"share_expiration_invalid\": \"The expiration for shares must be greater than or equal to the defined default value\",\n        \"file_pattern_path_invalid\": \"File name pattern filter paths must be absolute\",\n        \"file_pattern_duplicated\": \"Duplicated file name pattern filters detected\",\n        \"file_pattern_invalid\": \"Invalid file name pattern filters\",\n        \"disable_active_2fa\": \"Two-factor authentication cannot be disabled for a user with an active configuration\",\n        \"pwd_change_conflict\": \"It is not possible to request a password change and at the same time prevent the password from being changed\",\n        \"two_factor_conflict\": \"You can't require two-factor authentication and at the same time prevent it from being set up\",\n        \"role_help\": \"Users with a role can be managed by global administrators and administrators with the same role\",\n        \"require_pwd_change\": \"Require password change\",\n        \"require_pwd_change_help\": \"The user will need to change the password from WebClient to activate the account\",\n        \"groups_help\": \"Groups membership impart the groups settings with the exception of membership only groups\",\n        \"primary_group\": \"Primary group\",\n        \"secondary_groups\": \"Secondary groups\",\n        \"membership_groups\": \"Membership groups\",\n        \"template_help\": \"For each user set the username and at least one of the password and public key\",\n        \"virtual_folders_help\": \"Quota size/files -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders. You can use MB/GB/TB suffix. Without suffix we assume bytes\",\n        \"disconnect\": \"Disconnect the user after the update\",\n        \"disconnect_help\": \"This way you force the user to login again, if connected, and so to use the new configuration\",\n        \"invalid_quota_size\": \"Invalid quota size\",\n        \"expires_in\": \"Expires in\",\n        \"expires_in_help\": \"Account expiration as number of days from the creation. 0 means no expiration\",\n        \"additional_emails\": \"Additional emails\",\n        \"tls_certs\": \"TLS certificates\",\n        \"tls_certs_help\": \"TLS certificates can be used for FTP and/or WebDAV authentication\",\n        \"tls_cert_help\": \"Paste a PEM encoded TLS certificate here\",\n        \"tls_cert_invalid\": \"Invalid TLS certificate\",\n        \"template_title\": \"Create one or more new users from this template\",\n        \"template_username_placeholder\": \"replaced with the specified username\",\n        \"template_password_placeholder\": \"replaced with the specified password\",\n        \"template_help1\": \"Placeholders will be replaced in paths and credentials of the configured storage backend.\",\n        \"template_help2\": \"The generated users can be saved or exported. Exported users can be imported from the \\\"Maintenance\\\" section of this SFTPGo instance or another.\",\n        \"template_no_user\": \"No valid user defined, unable to complete the requested action\",\n        \"time_of_day_invalid\": \"Invalid time of day. Supported format HH:MM\",\n        \"time_of_day_conflict\": \"Invalid time of day. The end time cannot be earlier than the start time\"\n    },\n    \"group\": {\n        \"view_manage\": \"View and manage groups\",\n        \"err_delete_referenced\": \"Cannot delete a group with associated users, remove associations first\",\n        \"help\": \"The %username% placeholder will be replaced with the username of the associated users, %role% with their role\"\n    },\n    \"virtual_folders\": {\n        \"view_manage\": \"View and manage virtual folders\",\n        \"mount_path\": \"mount path, i.e. /vfolder\",\n        \"quota_size\": \"Quota size\",\n        \"quota_size_help\": \"0 means no limit. You can use MB/GB/TB suffix\",\n        \"quota_files\": \"Quota files\",\n        \"associations_summary\": \"Users: {{users}}. Groups: {{groups}}\",\n        \"template_title\": \"Create one or more new virtual folders from this template\",\n        \"template_name_placeholder\": \"replaced with the name of the specified virtual folder\",\n        \"template_help\": \"The generated virtual folders can be saved or exported. Exported folders can be imported from the \\\"Maintenance\\\" section of this SFTPGo instance or another.\",\n        \"name\": \"Virtual folder name\",\n        \"template_no_folder\": \"No valid virtual folder defined, unable to complete the requested action\"\n    },\n    \"storage\": {\n        \"title\": \"File system\",\n        \"label\": \"Storage\",\n        \"local\": \"Local disk\",\n        \"s3\": \"S3 (Compatible)\",\n        \"gcs\": \"GCS\",\n        \"azblob\": \"Azure Blob\",\n        \"encrypted\": \"Encrypted local disk\",\n        \"sftp\": \"SFTP\",\n        \"http\": \"HTTP\",\n        \"home_dir\": \"Root directory\",\n        \"home_dir_placeholder\": \"Absolute path to a directory on local disk\",\n        \"home_dir_help1\": \"Leave blank for an appropriate default\",\n        \"home_dir_help2\": \"Leave blank and storage to \\\"Local disk\\\" to not override the root directory\",\n        \"home_dir_help3\": \"Required for local disk storage providers. For other storage providers this folder will be used for temporary files, you can leave it blank for an appropriate default\",\n        \"home_dir_invalid\": \"Invalid root directory, make sure it is an absolute path\",\n        \"sftp_home_dir\": \"SFTP root directory\",\n        \"sftp_home_help\": \"Restrict access to this SFTP path. Example: \\\"/somedir/subdir\\\"\",\n        \"os_read_buffer\": \"Download buffer (MB)\",\n        \"os_buffer_help\": \"0 means no buffer\",\n        \"os_write_buffer\": \"Upload buffer (MB)\",\n        \"bucket\": \"Bucket\",\n        \"region\": \"Region\",\n        \"access_key\": \"Access Key\",\n        \"access_secret\": \"Access Secret\",\n        \"sse_customer_key\": \"Server-side encryption key\",\n        \"sse_customer_key_help\": \"You can store your data encrypted with this key, but if you lose or change this key, you will lose all files encrypted with it. Files that are not encrypted or encrypted with a different key will not be accessible\",\n        \"endpoint\": \"Endpoint\",\n        \"endpoint_help\": \"For AWS S3, leave blank to use the default endpoint for the specified region\",\n        \"sftp_endpoint_help\": \"Endpoint as host:port. The port is always required\",\n        \"ul_part_size\": \"Upload Part Size (MB)\",\n        \"part_size_help\": \"0 means the default (5 MB). Minimum is 5\",\n        \"gcs_part_size_help\": \"0 means the default (16 MB)\",\n        \"ul_concurrency\": \"Upload Concurrency\",\n        \"ul_concurrency_help\": \"How many parts are uploaded in parallel. 0 means the default (5)\",\n        \"dl_part_size\": \"Download Part Size (MB)\",\n        \"dl_concurrency\": \"Download Concurrency\",\n        \"dl_concurrency_help\": \"How many parts are downloaded in parallel. 0 means the default (5)\",\n        \"ul_part_timeout\": \"Upload Part timeout\",\n        \"ul_part_timeout_help\": \"Max time limit, in seconds, to upload a single part. 0 means no limit\",\n        \"gcs_ul_part_timeout_help\": \"Max time limit, in seconds, to upload a single part. 0 means the default (32)\",\n        \"dl_part_timeout\": \"Download Part timeout\",\n        \"dl_part_timeout_help\": \"Max time limit, in seconds, to download a single part. 0 means no limit\",\n        \"key_prefix\": \"Key Prefix\",\n        \"key_prefix_help\": \"Restrict access to keys with the specified prefix. Example: \\\"somedir/subdir/\\\"\",\n        \"class\": \"Storage class\",\n        \"acl\": \"ACL\",\n        \"role_arn\": \"Role ARN\",\n        \"role_arn_help\": \"Optional IAM Role ARN to assume\",\n        \"s3_path_style\": \"Use path-style addressing, i.e. \\\"endpoint/BUCKET/KEY\\\"\",\n        \"credentials_file\": \"Credentials file\",\n        \"credentials_file_help\": \"Add or update credentials from a JSON file\",\n        \"auto_credentials\": \"Automatic credentials\",\n        \"auto_credentials_help\": \"Use default application credentials or credentials from environment variables\",\n        \"container\": \"Container\",\n        \"account_name\": \"Account Name\",\n        \"account_key\": \"Account Key\",\n        \"sas_url\": \"SAS URL\",\n        \"sas_url_help\": \"Shared Access Signature URL can be used instead of account name/key\",\n        \"emulator\": \"Use emulator\",\n        \"passphrase\": \"Passphrase\",\n        \"passphrase_help\": \"Passphrase used to derive the per-object encryption key\",\n        \"passphrase_key_help\": \"Passphrase used to protect your private key, if any\",\n        \"fingerprints\": \"Fingerprints\",\n        \"fingerprints_help\": \"SHA256 fingerprints to be validated when connecting to the external SFTP server, one per line. If empty any host key will be accepted: this is a security risk!\",\n        \"sftp_buffer\": \"Buffer size (MB)\",\n        \"sftp_buffer_help\": \"A buffer size greater than 0 enables concurrent transfers\",\n        \"sftp_concurrent_reads\": \"Disable concurrent reads\",\n        \"relaxed_equality_check\": \"Relaxed equality check\",\n        \"relaxed_equality_check_help\": \"Enable to consider only the endpoint to determine if different configurations point to the same server. By default, both the endpoint and username must match\",\n        \"api_key\": \"API key\",\n        \"fs_error\": \"Filesystem configuration error\",\n        \"bucket_required\": \"$t(storage.fs_error): bucket is required\",\n        \"region_required\": \"$t(storage.fs_error): region is required\",\n        \"key_prefix_invalid\": \"$t(storage.fs_error): invalid key prefix, cannot start with \\\"/\\\"\",\n        \"ul_part_size_invalid\": \"$t(storage.fs_error): invalid upload part size\",\n        \"ul_concurrency_invalid\": \"$t(storage.fs_error): invalid upload concurrency\",\n        \"dl_part_size_invalid\": \"$t(storage.fs_error): invalid download part size\",\n        \"dl_concurrency_invalid\": \"$t(storage.fs_error): invalid download concurrency\",\n        \"access_key_required\": \"$t(storage.fs_error): access Key is required\",\n        \"access_secret_required\": \"$t(storage.fs_error): access Secret is required\",\n        \"credentials_required\": \"$t(storage.fs_error): credentials are required\",\n        \"container_required\": \"$t(storage.fs_error): container is required\",\n        \"account_name_required\": \"$t(storage.fs_error): account name is required\",\n        \"sas_url_invalid\": \"$t(storage.fs_error): invalid SAS URL\",\n        \"passphrase_required\": \"$t(storage.fs_error): passphrase is required\",\n        \"endpoint_invalid\": \"$t(storage.fs_error): endpoint is invalid\",\n        \"endpoint_required\": \"$t(storage.fs_error): endpoint is required\",\n        \"username_required\": \"$t(storage.fs_error): username is required\"\n    },\n    \"oidc\": {\n        \"token_expired\": \"Your OpenID token has expired, please log in again\",\n        \"token_invalid_webadmin\": \"Your OpenID token is not valid for the WebAdmin UI. Log out of your OpenID server and log in to WebAdmin\",\n        \"token_invalid_webclient\": \"Your OpenID token is not valid for the WebClient UI. Log out of your OpenID server and log in to the WebClient\",\n        \"token_exchange_err\": \"Failed to exchange OpenID token\",\n        \"token_invalid\": \"Invalid OpenID token\",\n        \"role_admin_err\": \"Incorrect OpenID role, logged in user is not an administrator\",\n        \"role_user_err\": \"Incorrect OpenID role, logged in user is an administrator\",\n        \"get_user_err\": \"Failed to get user associated with OpenID token\"\n    },\n    \"oauth2\": {\n        \"auth_verify_error\": \"Unable to verify OAuth2 code\",\n        \"auth_validation_error\": \"Unable to verify OAuth2 code\",\n        \"auth_invalid\": \"Invalid OAuth2 code\",\n        \"token_exchange_err\": \"Unable to get OAuth2 token from authorization code\",\n        \"no_refresh_token\": \"The OAuth2 provider returned an empty token. Some providers only return the token when the user first authorizes. If you have already registered SFTPGo with this user in the past, revoke access and try again. This way you will invalidate the previous token\",\n        \"success\": \"Copy the following string, without the quotes, into SMTP OAuth2 Token configuration field:\",\n        \"client_id_required\": \"Client ID is required\",\n        \"client_secret_required\": \"Client Secret is required\",\n        \"refresh_token_required\": \"Refresh Token is required\"\n    },\n    \"filters\": {\n        \"password_strength\": \"Password strength\",\n        \"password_strength_help\": \"Values in the 50-70 range are suggested for common use cases. 0 means disabled, any password will be accepted\",\n        \"password_expiration\": \"Password expiration\",\n        \"password_expiration_help\": \"Password expiration as number of days. 0 means no expiration\",\n        \"default_shares_expiration\": \"Default shares expiration\",\n        \"default_shares_expiration_help\": \"Default expiration for new shares as number of days\",\n        \"max_shares_expiration\": \"Maximum shares expiration\",\n        \"max_shares_expiration_help\": \"Maximum allowed expiration, as number of days, when a user creates or updates a share\",\n        \"directory_permissions\": \"Per-directory permissions\",\n        \"directory_permissions_help\": \"Wildcards are supported in paths, for example \\\"/incoming/*\\\" matches any directory within \\\"/incoming\\\"\",\n        \"directory_path_help\": \"directory path, i.e. /dir\",\n        \"directory_patterns\": \"Per-directory name patterns restrictions\",\n        \"directory_patterns_help\": \"Comma separated denied or allowed files/directories, based on shell patterns. The match is case insensitive\",\n        \"max_sessions\": \"Max sessions\",\n        \"max_sessions_help\": \"Maximum number of concurrent sessions. 0 means no limit\",\n        \"denied_protocols\": \"Denied protocols\",\n        \"denied_login_methods\": \"Denied login methods\",\n        \"denied_login_methods_help\": \"\\\"password\\\" is valid for all supported protocols, \\\"password-over-SSH\\\" only for SSH/SFTP/SCP\",\n        \"web_client_options\": \"Web client/REST API\",\n        \"max_upload_size\": \"Max upload size\",\n        \"max_upload_size_help\": \"Maximum upload size for a single file. 0 means no limit. You can use MB/GB/TB suffix\",\n        \"max_upload_size_invalid\": \"Invalid maximum upload file size\",\n        \"upload_bandwidth\": \"Bandwidth UL (KB/s)\",\n        \"download_bandwidth\": \"Bandwidth DL (KB/s)\",\n        \"upload_bandwidth_help\": \"UL (KB/s). 0 means no limit\",\n        \"download_bandwidth_help\": \"DL (KB/s). 0 means no limit\",\n        \"src_bandwidth_limit\": \"Per-source bandwidth speed limits\",\n        \"upload_data_transfer\": \"Upload data transfer (MB)\",\n        \"upload_data_transfer_help\": \"Maximum data transfer allowed for uploads. 0 means no limit\",\n        \"download_data_transfer\": \"Download data transfer (MB)\",\n        \"download_data_transfer_help\": \"Maximum data transfer allowed for downloads. 0 means no limit\",\n        \"total_data_transfer\": \"Total data transfer (MB)\",\n        \"total_data_transfer_help\": \"Maximum data transfer allowed for uploads + downloads. Replace the individual limits. 0 means no limit\",\n        \"start_directory\": \"Initial directory\",\n        \"start_directory_help\": \"Alternate initial directory to use instead of \\\"/\\\". Supported for SFTP/FTP/HTTP\",\n        \"tls_username\": \"TLS username\",\n        \"tls_username_help\": \"Defines the TLS certificate field to use as username. Ignored if mutual TLS is disabled\",\n        \"ftp_security\": \"FTP security\",\n        \"ftp_security_help\": \"Ignored if TLS is already globally required for all FTP users\",\n        \"hooks\": \"Hooks\",\n        \"hook_ext_auth_disabled\": \"External auth disabled\",\n        \"hook_pre_login_disabled\": \"Pre-login disabled\",\n        \"hook_check_password_disabled\": \"Check password disabled\",\n        \"is_anonymous\": \"Anonymous user\",\n        \"is_anonymous_help\": \"Anonymous users are supported for FTP and WebDAV protocols and have read-only access\",\n        \"disable_fs_checks\": \"Disable filesystem checks\",\n        \"disable_fs_checks_help\": \"Disable checks for existence and automatic creation of home directory and virtual folders\",\n        \"api_key_auth_help\": \"Allow to impersonate the user, in REST API, with an API key\",\n        \"external_auth_cache_time\": \"External auth cache time\",\n        \"external_auth_cache_time_help\": \"Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache\",\n        \"access_time\": \"Access time restrictions\",\n        \"access_time_help\": \"No restrictions means access is always allowed, the time must be set in the format HH:MM\"\n    },\n    \"admin\": {\n        \"role_permissions\": \"A role admin cannot have the \\\"*\\\" permission\",\n        \"view_manage\": \"View and manage admins\",\n        \"self_delete\": \"You cannot delete yourself\",\n        \"self_permissions\": \"You cannot change your permissions\",\n        \"self_disable\": \"You cannot disable yourself\",\n        \"self_role\": \"You cannot add/change your role\",\n        \"password_help\": \"If blank the current password will not be changed\",\n        \"role_help\": \"Setting a role limit the administrator to only manage users with the same role. Administrators with a role cannot be super administrators\",\n        \"users_groups\": \"Groups for users\",\n        \"users_groups_help\": \"Groups automatically selected for new users created by this admin. The admin will still be able to choose different groups. These settings are only used for this admin UI and they will be ignored in REST API/hooks\",\n        \"group_membership\": \"Add as membership\",\n        \"group_primary\": \"Add as primary\",\n        \"group_secondary\": \"Add as secondary\",\n        \"user_page_pref\": \"User page preferences\",\n        \"user_page_pref_help\": \"You can hide some sections from the user page. These are not security settings and are not enforced server side in any way. They are only intended to simplify the add/update user page\",\n        \"hide_sections\": \"Hide sections\",\n        \"default_users_expiration\": \"Default users expiration\",\n        \"default_users_expiration_help\": \"Default expiration for new users as number of days\",\n        \"require_pwd_change_help\": \"A password change is required at the next login\"\n    },\n    \"connections\": {\n        \"view_manage\": \"View and manage connections\",\n        \"started\": \"Started\",\n        \"remote_address\": \"Remote address\",\n        \"last_activity\": \"Last activity\",\n        \"disconnect_confirm_btn\": \"Yes, disconnect\",\n        \"disconnect_confirm\": \"Do you want to disconnect the selected connection? This action is irreversible\",\n        \"disconnect_ko\": \"Unable to disconnect the selected connection\",\n        \"upload\": \"UL: \\\"{{- path}}\\\"\",\n        \"download\": \"DL: \\\"{{- path}}\\\"\",\n        \"upload_info\": \"$t(connections.upload). Size: {{- size}}. Speed: {{- speed}}\",\n        \"download_info\": \"$t(connections.download). Size: {{- size}}. Speed: {{- speed}}\",\n        \"client\": \"Client: {{- val}}\"\n    },\n    \"role\": {\n        \"view_manage\": \"View and manage roles\",\n        \"err_delete_referenced\": \"Cannot delete a role with associated admins, remove associations first\"\n    },\n    \"ip_list\": {\n        \"view_manage\": \"View and manage IP lists\",\n        \"defender_list\": \"Defender\",\n        \"allow_list\": \"Allow list\",\n        \"ratelimiters_safe_list\": \"Rate limiters safe list\",\n        \"ip_net\": \"IP/Network\",\n        \"protocols\": \"Protocols\",\n        \"any\": \"Any\",\n        \"allow\": \"Allow\",\n        \"deny\": \"Deny\",\n        \"ip_net_help\": \"IP address or network in CIDR format, example: \\\"192.168.1.1 or 10.8.0.100/32 or 2001:db8:1234::/48\\\"\",\n        \"ip_invalid\": \"Invalid IP address\",\n        \"net_invalid\": \"Invalid network\",\n        \"duplicated\": \"The specified IP/network already exists\",\n        \"search\": \"IP/Network or initial part\",\n        \"defender_disabled\": \"Defender disabled in your configuration\",\n        \"allow_list_disabled\": \"Allow list disabled in your configuration\",\n        \"ratelimiters_disabled\": \"Rate limiters disabled in your configuration\"\n    },\n    \"defender\": {\n        \"view_manage\": \"View and manage auto blocklist\",\n        \"ip\": \"IP address\",\n        \"ban_time\": \"Blocked until\",\n        \"score\": \"Score\"\n    },\n    \"status\": {\n        \"desc\": \"Server status\",\n        \"ssh\": \"SSH/SFTP server\",\n        \"active\": \"Status: active\",\n        \"disabled\": \"Status: disabled\",\n        \"error\": \"Status: error\",\n        \"proxy_on\": \"PROXY protocol enabled\",\n        \"address\": \"Address\",\n        \"ssh_auths\": \"Authentication methods\",\n        \"ssh_commands\": \"Accepted commands\",\n        \"host_key\": \"Host key\",\n        \"fingeprint\": \"Fingerprint\",\n        \"algorithms\": \"Algorithms\",\n        \"algorithm\": \"Algorithm\",\n        \"ssh_pub_key_algo\": \"Public key authentication algorithms\",\n        \"ssh_mac_algo\": \"Message authentication code (MAC) algorithms\",\n        \"ssh_kex_algo\": \"Key exchange (KEX) algorithms\",\n        \"ssh_cipher_algo\": \"Ciphers\",\n        \"ftp\": \"FTP server\",\n        \"ftp_passive_range\": \"Passive mode port range\",\n        \"ftp_passive_ip\": \"Passive IP\",\n        \"tls\": \"TLS\",\n        \"tls_disabled\": \"Disabled\",\n        \"tls_explicit\": \"Explicit mode required (FTPES)\",\n        \"tls_implicit\": \"Implicit mode (FTPS), deprecated, prefer FTPES\",\n        \"tls_mixed\": \"Plain and explicit (FTPES) mode\",\n        \"webdav\": \"WebDAV server\",\n        \"rate_limiters\": \"Rate limiters\"\n    },\n    \"maintenance\": {\n        \"backup\": \"Backup\",\n        \"backup_do\": \"Backup your data\",\n        \"backup_ok\": \"Backup successfully restored\",\n        \"backup_invalid_file\": \"Invalid backup file, make sure it is a JSON file with valid content\",\n        \"restore_error\": \"Unable to restore your backup, check the server logs for more details\",\n        \"restore\": \"Restore\",\n        \"backup_file\": \"Backup file\",\n        \"backup_file_help\": \"Import data from a JSON backup file\",\n        \"restore_mode0\": \"Add and update\",\n        \"restore_mode1\": \"Add only\",\n        \"restore_mode2\": \"Add, update and disconnect\",\n        \"after_restore\": \"After restore\",\n        \"quota_mode0\": \"No quota update\",\n        \"quota_mode1\": \"Update quota\",\n        \"quota_mode2\": \"Update quota for users with quota limits\"\n    },\n    \"acme\": {\n        \"title\": \"ACME\",\n        \"generic_error\": \"Unable to obtain TLS certificates, check the server logs for more details\",\n        \"help\": \"From this section you can request free TLS certificates for your SFTPGo services using the ACME protocol and the HTTP-01 challenge type. You must create a DNS entry under a custom domain that you own which resolves to your SFTPGo public IP address and the port 80 must be publicly reachable. You can set the configuration options for the most common use cases and single node setups here, for advanced configurations refer to the SFTPGo docs. A service restart is required to apply changes\",\n        \"domain_help\": \"Multiple domains can be specified comma or space separated. They will be included in the same certificate\",\n        \"email_help\": \"Email used for registration and recovery contact\",\n        \"port_help\": \"If different from 80 you have to configure a reverse proxy\",\n        \"protocols_help\": \"Use the obtained certificates for the specified protocols\"\n    },\n    \"smtp\": {\n        \"title\": \"SMTP\",\n        \"err_required_fields\": \"From address and Username cannot be both empty\",\n        \"help\": \"Set the SMTP configuration replacing the one defined using env vars or config file if any\",\n        \"host\": \"Server name\",\n        \"host_help\": \"If blank the configuration is disabled\",\n        \"auth\": \"Authentication\",\n        \"encryption\": \"Encryption\",\n        \"sender\": \"Sender\",\n        \"debug\": \"Debug logs\",\n        \"domain_help\": \"HELO domain. Leave blank to use the server hostname\",\n        \"test_recipient\": \"Address to send test emails to\",\n        \"oauth2_provider\": \"OAuth2 provider\",\n        \"oauth2_provider_help\": \"URI to redirect to after user authentication\",\n        \"oauth2_tenant\": \"OAuth2 Tenant\",\n        \"oauth2_tenant_help\": \"Azure tenant. Typical values are \\\"common\\\", \\\"organizations\\\", \\\"consumers\\\" or the tenant identifier\",\n        \"oauth2_client_id\": \"OAuth2 Client ID\",\n        \"oauth2_client_secret\": \"OAuth2 Client Secret\",\n        \"oauth2_token\": \"OAuth2 Token\",\n        \"recipient_required\": \"Specify a recipient to send a test email\",\n        \"test_error\": \"Unable to send test email, check server logs for more details\",\n        \"test_ok\": \"No errors were reported while sending the test email. Please check your inbox to make sure\",\n        \"oauth2_flow_error\": \"Unable to get the URI to start OAuth2 flow\",\n        \"oauth2_question\": \"Do you want to start the OAuth2 flow to get a token?\"\n    },\n    \"sftp\": {\n        \"help\": \"From this section you can enable algorithms disabled by default. You don't need to set values already defined using env vars or config file. A service restart is required to apply changes\",\n        \"host_key_algos\": \"Host Key Algorithms\"\n    },\n    \"branding\": {\n        \"title\": \"Branding\",\n        \"help\": \"From this section you can customize SFTPGo to fit your brand and add a disclaimer to the login pages\",\n        \"short_name\": \"Short name\",\n        \"logo\": \"Logo\",\n        \"logo_help\": \"PNG image, max accepted size 512x512, default logo size is 256x256\",\n        \"favicon\": \"Favicon\",\n        \"disclaimer_name\": \"Disclaimer title\",\n        \"disclaimer_url\": \"Disclaimer URL\",\n        \"invalid_png\": \"Invalid PNG image\",\n        \"invalid_png_size\": \"Invalid PNG image size\",\n        \"invalid_disclaimer_url\": \"The disclaimer URL must be an http or https link\"\n    },\n    \"events\": {\n        \"search\": \"Search logs\",\n        \"fs_events\": \"Filesystem events\",\n        \"provider_events\": \"Provider events\",\n        \"other_events\": \"Other events\",\n        \"quota_exceeded\": \"Quota exceeded\",\n        \"date_range\": \"Date range\",\n        \"upload\": \"Upload\",\n        \"download\": \"Download\",\n        \"mkdir\": \"Create dir\",\n        \"rmdir\": \"Remove dir\",\n        \"rename\": \"Rename\",\n        \"delete\": \"Removal\",\n        \"copy\": \"Copy\",\n        \"first_upload\": \"First upload\",\n        \"first_download\": \"First download\",\n        \"ssh_cmd\": \"SSH command\",\n        \"add\": \"Addition\",\n        \"update\": \"Update\",\n        \"login_failed\": \"Login failed\",\n        \"login_ok\": \"Login succeeded\",\n        \"login_missing_user\": \"Login with non-existent user\",\n        \"no_login_tried\": \"No login tried\",\n        \"algo_negotiation_failed\": \"Algorithm negotiation failed\",\n        \"datetime\": \"Date and time\",\n        \"action\": \"Action\",\n        \"path\": \"Path\",\n        \"object\": \"Object\",\n        \"event\": \"Event\"\n    },\n    \"provider_objects\": {\n        \"user\": \"User\",\n        \"folder\": \"Folder\",\n        \"group\": \"Group\",\n        \"admin\": \"Admin\",\n        \"api_key\": \"API key\",\n        \"share\": \"Share\",\n        \"event_action\": \"Action\",\n        \"event_rule\": \"Rule\",\n        \"role\": \"role\",\n        \"ip_list_entry\": \"IP list entry\",\n        \"configs\": \"Configurations\"\n    },\n    \"actions\": {\n        \"view_manage\": \"View and manage rule actions for events\",\n        \"err_delete_referenced\": \"Cannot delete an action with associated rules, remove associations first\",\n        \"http_url\": \"Server URL\",\n        \"http_url_help\": \"i.e https://host:port/path. Placeholders are supported within the URL path\",\n        \"http_url_required\": \"URL is required\",\n        \"http_url_invalid\": \"The URL is invalid, http and https schemes are supported\",\n        \"http_part_name_required\": \"HTTP part name is required\",\n        \"http_part_body_required\": \"HTTP part body is required if no file path is provided\",\n        \"http_multipart_body_error\": \"Multipart requests require no body. The request body is build from the specified parts\",\n        \"http_multipart_ctype_error\": \"Content-Type is automatically set for multipart requests\",\n        \"path_duplicated\": \"Path duplicated\",\n        \"command_required\": \"Command is required\",\n        \"command_invalid\": \"Invalid command, it must be an absolute path\",\n        \"email_recipient_required\": \"At least one email recipient is required\",\n        \"email_subject_required\": \"Email subject is required\",\n        \"email_body_required\": \"Email body is required\",\n        \"retention_directory_required\": \"At least one directory to check is required\",\n        \"path_required\": \"At least a path is required\",\n        \"source_dest_different\": \"Source and target path must be different\",\n        \"root_not_allowed\": \"The root path (/) is not allowed\",\n        \"archive_name_required\": \"Compressed archive name is required\",\n        \"idp_template_required\": \"A user or admin template is required\",\n        \"threshold\": \"Threshold\",\n        \"threshold_help\": \"An email notification will be generated for users whose password expires in a number of days less than or equal to this threshold\",\n        \"disable_threshold\": \"Disable threshold\",\n        \"disable_threshold_help\": \"Inactivity in days, since last login or creation before disabling users\",\n        \"delete_threshold\": \"Delete threshold\",\n        \"delete_threshold_help\": \"Inactivity in days, since last login or creation before deleting users\",\n        \"inactivity_threshold_required\": \"At least one inactivity threshold must be defined\",\n        \"inactivity_thresholds_invalid\": \"The deletion threshold must be greater than the deactivation threshold\",\n        \"idp_mode_add_update\": \"Create or update\",\n        \"idp_mode_add\": \"Create if it doesn't exist\",\n        \"template_user_help\": \"Template for SFTPGo users in JSON format. Placeholders are supported\",\n        \"template_admin_help\": \"Template for SFTPGo admins in JSON format. Placeholders are supported\",\n        \"placeholders_help\": \"Placeholders are supported\",\n        \"http_headers\": \"HTTP headers\",\n        \"query_parameters\": \"Query string parameters\",\n        \"http_timeout_help\": \"Ignored for multipart requests with files as attachments\",\n        \"body\": \"Body\",\n        \"http_body_help\": \"Placeholders are supported. Ignored for HTTP get requested. Leave empty for multipart requests\",\n        \"multipart_body\": \"Multipart body\",\n        \"multipart_body_help\": \"HTTP Multipart requests allow to combine one or more sets of data into a single body. For each part, you can set a file path or a body as text. Placeholders are supported in file path, body, header values\",\n        \"http_part_name\": \"Part name\",\n        \"http_part_file\": \"File path\",\n        \"http_part_headers\": \"Additional part headers one per line as \\\"key: value\\\"\",\n        \"command_help\": \"Absolute path of the command to execute\",\n        \"command_args\": \"Arguments\",\n        \"command_args_help\": \"Comma separated command arguments. Placeholders are supported\",\n        \"command_env_vars_help\": \"Placeholders are supported in values. Setting the value to \\\"$\\\" without quotes means retrieving the key from the environment excluding keys starting with SFTPGO_\",\n        \"email_recipients\": \"To\",\n        \"email_recipients_help\": \"Comma separated recipients. Placeholders are supported\",\n        \"email_bcc\": \"Bcc\",\n        \"email_bcc_help\": \"Comma separated Bcc addresses. Placeholders are supported\",\n        \"email_subject\": \"Subject\",\n        \"content_type\": \"Content Type\",\n        \"attachments\": \"Attachments\",\n        \"attachments_help\": \"Comma separated paths to attach. Placeholders are supported. The total size is limited to 10 MB\",\n        \"data_retention\": \"Data retention\",\n        \"data_retention_help\": \"Set the data retention, as hours, per path. Retention applies recursively. Setting 0 as retention means excluding the specified path\",\n        \"delete_empty_dirs\": \"Delete empty dirs\",\n        \"fs_action\": \"Filesystem action\",\n        \"paths_src_dst_help\": \"Paths as seen by SFTPGo users. Placeholders are supported. The required permissions are granted automatically\",\n        \"source_path\": \"Source\",\n        \"target_path\": \"Target\",\n        \"paths_help\": \"Comma separated paths as seen by SFTPGo users. Placeholders are supported. The required permissions are granted automatically\",\n        \"archive_path\": \"Archive path\",\n        \"archive_path_help\": \"Full path, as seen by SFTPGo users, to the zip archive to create. Placeholders are supported. If the specified file already exists, it is overwritten\",\n        \"placeholders_modal_title\": \"Supported placeholders\",\n        \"update_mod_times\": \"Update timestamp\",\n        \"types\": {\n            \"http\": \"HTTP\",\n            \"email\": \"Email\",\n            \"backup\": \"Backup\",\n            \"user_quota_reset\": \"User quota reset\",\n            \"folder_quota_reset\": \"Folder quota reset\",\n            \"transfer_quota_reset\": \"Transfer quota reset\",\n            \"data_retention_check\": \"Data retention check\",\n            \"filesystem\": \"Filesystem\",\n            \"password_expiration_check\": \"Password expiration check\",\n            \"user_expiration_check\": \"User expiration check\",\n            \"user_inactivity_check\": \"User inactivity check\",\n            \"idp_check\": \"Identity Provider account check\",\n            \"rotate_logs\": \"Rotate log file\",\n            \"command\": \"Command\"\n        },\n        \"fs_types\": {\n            \"rename\": \"Rename\",\n            \"delete\": \"Delete\",\n            \"path_exists\": \"Paths exist\",\n            \"compress\": \"Compress\",\n            \"copy\": \"Copy\",\n            \"create_dirs\": \"Create directories\"\n        },\n        \"placeholders_modal\": {\n            \"name\": \"Username, virtual folder name, admin username for provider events, domain name for TLS certificate events\",\n            \"event\": \"Event name, for example \\\"upload\\\", \\\"download\\\" for filesystem events or \\\"add\\\", \\\"update\\\" for provider events\",\n            \"status\": \"Status for filesystem events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error\",\n            \"status_string\": \"Status as string. Possible values \\\"OK\\\", \\\"KO\\\"\",\n            \"error_string\": \"Error details. Replaced with an empty string if no errors occur\",\n            \"virtual_path\": \"Path seen by SFTPGo users, for example \\\"/adir/afile.txt\\\"\",\n            \"escaped_virtual_path\": \"HTTP query string encoded path, for example \\\"%2Fadir%2Fafile.txt\\\".\",\n            \"virtual_dir_path\": \"Parent directory for \\\"VirtualPath\\\", for example if \\\"VirtualPath\\\" is \\\"/adir/afile.txt\\\", \\\"VirtualDirPath\\\" is \\\"/adir\\\"\",\n            \"fs_path\": \"Full filesystem path, for example \\\"/user/homedir/adir/afile.txt\\\" or \\\"C:/data/user/homedir/adir/afile.txt\\\" on Windows\",\n            \"ext\": \"File extension, for example \\\".txt\\\" if the filename is \\\"afile.txt\\\"\",\n            \"object_name\": \"File/directory name, for example \\\"afile.txt\\\" or provider object name\",\n            \"object_basename\": \"Filename without extension, for example \\\"afile\\\" if the filename is \\\"afile.txt\\\"\",\n            \"object_type\": \"Object type for provider events: \\\"user\\\", \\\"group\\\", \\\"admin\\\", etc\",\n            \"virtual_target_path\": \"Virtual target path for rename and copy operations\",\n            \"virtual_target_dir_path\": \"Parent directory for \\\"VirtualTargetPath\\\"\",\n            \"target_name\": \"Target object name for rename and copy operations\",\n            \"fs_target_path\": \"Full filesystem target path for rename and copy operations\",\n            \"file_size\": \"File size (bytes)\",\n            \"elapsed\": \"Elapsed time as milliseconds for filesystem events\",\n            \"protocol\": \"Protocol, for example \\\"SFTP\\\", \\\"FTP\\\"\",\n            \"ip\": \"Client IP address\",\n            \"role\": \"User or admin role\",\n            \"timestamp\": \"Event timestamp as nanoseconds since epoch\",\n            \"datetime\": \"Event timestamp formatted as YYYY-MM-DDTHH:MM:SS.ZZZ\",\n            \"year\": \"Event year formatted as four digits\",\n            \"month\": \"Event month formatted as two digits\",\n            \"day\": \"Event day formatted as two digits\",\n            \"hour\": \"Event hour formatted as two digits\",\n            \"minute\": \"Event minute formatted as two digits\",\n            \"email\": \"For filesystem events, this is the email associated with the user performing the action. For the provider events, this is the email associated with the affected user or admin. Blank in all other cases\",\n            \"object_data\": \"Provider object data serialized as JSON with sensitive fields removed\",\n            \"object_data_string\": \"Provider object data as JSON escaped string with sensitive fields removed\",\n            \"retention_reports\": \"Data retention reports as zip compressed CSV files. Supported as email attachment, file path for multipart HTTP request and as single parameter for HTTP requests body\",\n            \"idp_field\": \"Identity Provider custom fields containing a string\",\n            \"metadata\": \"Cloud storage metadata for the downloaded file serialized as JSON\",\n            \"metadata_string\": \"Cloud storage metadata for the downloaded file as JSON escaped string\",\n            \"uid\": \"Unique ID\"\n        }\n    },\n    \"rules\": {\n        \"view_manage\": \"View and manage rules for events\",\n        \"trigger\": \"Trigger\",\n        \"run_confirm\": \"Do you want to execute the selected rule?\",\n        \"run_confirm_btn\": \"Yes, run\",\n        \"run_error_generic\": \"Unable to run the selected rule\",\n        \"run_ok\": \"Rule actions started\",\n        \"run\": \"Run\",\n        \"invalid_fs_min_size\": \"Invalid min size\",\n        \"invalid_fs_max_size\": \"Invalid max size\",\n        \"action_required\": \"At least one action is required\",\n        \"fs_event_required\": \"At least one filesystem event is required\",\n        \"provider_event_required\": \"At least one provider event is required\",\n        \"schedule_required\": \"At least one schedule is required\",\n        \"schedule_invalid\": \"Invalid schedule\",\n        \"duplicate_actions\": \"Duplicate actions detected\",\n        \"sync_failure_actions\": \"Synchronous execution is not supported for failure actions\",\n        \"sync_unsupported\": \"Synchronous execution is only supported for some filesystem events and Identity Provider logins\",\n        \"sync_unsupported_fs_event\": \"Synchronous execution is only supported for upload and pre-* filesystem events\",\n        \"only_failure_actions\": \"At least a non-failure action is required\",\n        \"sync_action_required\": \"Event \\\"{{val}}\\\" requires at least a synchronous action\",\n        \"scheduler_help\": \"Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on\",\n        \"concurrent_run\": \"Allow concurrent execution from multiple instances\",\n        \"protocol_filters\": \"Protocol filters\",\n        \"status_filters\": \"Status filters\",\n        \"object_filters\": \"Object filters\",\n        \"name_filters\": \"Name filters\",\n        \"name_filters_help\": \"Shell-like pattern filters for usernames, folder names. For example \\\"user*\\\"\\\" will match names starting with \\\"user\\\". For provider events, this filter is applied to the username of the admin executing the event\",\n        \"inverse_match\": \"Inverse match\",\n        \"group_name_filters\": \"Group name filters\",\n        \"group_name_filters_help\": \"Shell-like pattern filters for group names. For example \\\"group*\\\"\\\" will match group names starting with \\\"group\\\"\",\n        \"role_name_filters\": \"Role name filters\",\n        \"role_name_filters_help\": \"Shell-like pattern filters for role names. For example \\\"role*\\\"\\\" will match role names starting with \\\"role\\\"\",\n        \"path_filters\": \"Path filters\",\n        \"path_filters_help\": \"Shell-like pattern filters on filesystem event paths. For example \\\"/adir/*.txt\\\"\\\" will match paths in the \\\"/adir\\\" directory ending with \\\".txt\\\". Double asterisk is supported, for example \\\"/**/*.txt\\\" will match any file ending with \\\".txt\\\". \\\"/mydir/**\\\" will match any entry in \\\"/mydir\\\"\",\n        \"file_size_limits\": \"File size limits\",\n        \"file_size_limits_help\": \"0 means no limit. You can use MB/GB suffix\",\n        \"min_size\": \"Minimum size\",\n        \"max_size\": \"Maximum size\",\n        \"actions_help\": \"One or more actions to execute. The \\\"Execute sync\\\" option is supported for \\\"upload\\\" events and required for \\\"pre-*\\\" events and Identity provider login events if the action checks the account\",\n        \"option_failure_action\": \"Failure action\",\n        \"option_stop_on_failure\": \"Stop on failure\",\n        \"option_execute_sync\": \"Synchronous execution\",\n        \"no_filter\": \"No filter means always triggering events\",\n        \"action_placeholder\": \"Select an action\",\n        \"triggers\": {\n            \"fs_event\": \"Filesystem events\",\n            \"provider_event\": \"Provider events\",\n            \"ip_blocked\": \"IP blocked\",\n            \"certificate_renewal\": \"Certificate renewal\",\n            \"on_demand\": \"On demand\",\n            \"idp_login\": \"Identity Provider logins\",\n            \"schedule\": \"Schedules\"\n        },\n        \"idp_logins\": {\n            \"user\": \"User login\",\n            \"admin\": \"Admin login\"\n        }\n    }\n}\n"
  },
  {
    "path": "static/locales/es/translation.json",
    "content": "{\n    \"title\": {\n        \"setup\": \"Configuración inicial\",\n        \"login\": \"Iniciar sesión\",\n        \"share_login\": \"Inicio de sesión compartido\",\n        \"profile\": \"Perfil\",\n        \"change_password\": \"Cambiar contraseña\",\n        \"files\": \"Archivos\",\n        \"shares\": \"Compartidos\",\n        \"add_share\": \"Añadir compartido\",\n        \"update_share\": \"Actualizar compartido\",\n        \"two_factor_auth\": \"Autenticación de dos factores\",\n        \"two_factor_auth_short\": \"2FA\",\n        \"edit_file\": \"Editar archivo\",\n        \"view_file\": \"Ver archivo\",\n        \"recovery_password\": \"Recuperación de contraseña\",\n        \"reset_password\": \"Restablecer contraseña\",\n        \"shared_files\": \"Archivos compartidos\",\n        \"upload_to_share\": \"Subir a compartido\",\n        \"download_shared_file\": \"Descargar archivo compartido\",\n        \"share_access_error\": \"No se puede acceder al recurso compartido\",\n        \"invalid_auth_request\": \"Solicitud de autenticación no válida\",\n        \"error400\": \"Solicitud incorrecta\",\n        \"error403\": \"Prohibido\",\n        \"error404\": \"No encontrado\",\n        \"error416\": \"Rango solicitado no satisfactorio\",\n        \"error429\": \"Demasiadas solicitudes\",\n        \"error500\": \"Error interno del servidor\",\n        \"errorPDF\": \"No se puede mostrar el archivo PDF\",\n        \"error_editor\": \"No se puede abrir el editor de archivos\",\n        \"users\": \"Usuarios\",\n        \"groups\": \"Grupos\",\n        \"folders\": \"Carpetas virtuales\",\n        \"connections\": \"Conexiones activas\",\n        \"event_manager\": \"Gestor de eventos\",\n        \"event_rules\": \"Reglas\",\n        \"event_actions\": \"Acciones\",\n        \"ip_manager\": \"Gestor de IP\",\n        \"ip_lists\": \"Listas de IP\",\n        \"defender\": \"Lista de bloqueo automático\",\n        \"admins\": \"Administradores\",\n        \"roles\": \"Roles\",\n        \"server_manager\": \"Gestor del servidor\",\n        \"configs\": \"Configuraciones\",\n        \"logs\": \"Registros\",\n        \"maintenance\": \"Mantenimiento\",\n        \"status\": \"Estado\",\n        \"add_user\": \"Añadir usuario\",\n        \"update_user\": \"Actualizar usuario\",\n        \"template_user\": \"Plantilla de usuario\",\n        \"template_admin\": \"Plantilla de administrador\",\n        \"add_group\": \"Añadir grupo\",\n        \"update_group\": \"Actualizar grupo\",\n        \"add_folder\": \"Añadir carpeta virtual\",\n        \"update_folder\": \"Actualizar carpeta virtual\",\n        \"template_folder\": \"Plantilla de carpeta virtual\",\n        \"oauth2_error\": \"No se puede completar el flujo OAuth2\",\n        \"oauth2_success\": \"Flujo OAuth2 completado\",\n        \"add_role\": \"Añadir rol\",\n        \"update_role\": \"Actualizar rol\",\n        \"add_admin\": \"Añadir administrador\",\n        \"update_admin\": \"Actualizar administrador\",\n        \"add_ip_list\": \"Añadir entrada de lista IP\",\n        \"update_ip_list\": \"Actualizar entrada de lista IP\",\n        \"add_action\": \"Añadir acción\",\n        \"update_action\": \"Actualizar acción\",\n        \"add_rule\": \"Añadir regla\",\n        \"update_rule\": \"Actualizar regla\"\n    },\n    \"setup\": {\n        \"desc\": \"Para empezar a usar SFTPGo necesitas crear un usuario administrador\",\n        \"submit\": \"Crear administrador e Iniciar sesión\",\n        \"install_code_mismatch\": \"El código de instalación no coincide\",\n        \"help_text\": \"Soporte comercial\"\n    },\n    \"login\": {\n        \"username\": \"Nombre de usuario\",\n        \"password\": \"Contraseña\",\n        \"your_username\": \"Tu nombre de usuario\",\n        \"forgot_password\": \"¿Olvidaste tu contraseña?\",\n        \"forgot_password_msg\": \"Introduce el nombre de usuario de tu cuenta a continuación, recibirás un código de restablecimiento de contraseña por correo electrónico.\",\n        \"send_reset_code\": \"Enviar código de restablecimiento\",\n        \"signin\": \"Iniciar sesión\",\n        \"signin_openid\": \"Iniciar sesión con OpenID\",\n        \"signout\": \"Cerrar sesión\",\n        \"auth_code\": \"Código de autenticación\",\n        \"two_factor_help\": \"Abre la aplicación de autenticación de dos factores en tu dispositivo para ver tu código de autenticación y verificar tu identidad.\",\n        \"two_factor_msg\": \"Introduce un código de recuperación de dos factores\",\n        \"recovery_code\": \"Código de recuperación\",\n        \"recovery_code_msg\": \"Puedes introducir uno de tus códigos de recuperación en caso de perder el acceso a tu dispositivo móvil.\",\n        \"reset_password\": \"Restablecer contraseña\",\n        \"reset_pwd_msg\": \"Revisa tu correo electrónico para el código de confirmación\",\n        \"reset_submit\": \"Actualizar contraseña e Iniciar sesión\",\n        \"confirm_code\": \"Código de confirmación\",\n        \"reset_pwd_forbidden\": \"No tienes permiso para restablecer tu contraseña\",\n        \"reset_pwd_no_email\": \"Tu cuenta no tiene una dirección de correo electrónico, no es posible restablecer tu contraseña enviando un código de verificación por correo\",\n        \"reset_pwd_send_email_err\": \"No se puede enviar el código de confirmación por correo electrónico\",\n        \"reset_pwd_err_generic\": \"Error inesperado al restablecer la contraseña\",\n        \"reset_ok_login_error\": \"El restablecimiento de contraseña se completó con éxito pero ocurrió un error inesperado al iniciar sesión\",\n        \"ip_not_allowed\": \"El inicio de sesión no está permitido desde esta dirección IP\",\n        \"two_factor_required\": \"Configura la autenticación de dos factores, es obligatoria para los siguientes protocolos: {{val}}\",\n        \"two_factor_required_generic\": \"Configura la autenticación de dos factores, es obligatoria para tu cuenta\",\n        \"link\": \"Ir a {{link}}\"\n    },\n    \"theme\": {\n        \"light\": \"Claro\",\n        \"dark\": \"Oscuro\",\n        \"system\": \"Automático\"\n    },\n    \"general\": {\n        \"invalid_auth_request\": \"La solicitud de autenticación no cumple con los requisitos de seguridad\",\n        \"invalid_input\": \"Entrada no válida. No se permiten los siguientes caracteres: barra (/), dos puntos (:), caracteres de control y nombres de sistema reservados\",\n        \"error400\": \"La solicitud recibida no es válida\",\n        \"error403\": \"No tienes los permisos requeridos\",\n        \"error404\": \"El recurso solicitado no existe\",\n        \"error416\": \"El fragmento de archivo solicitado no pudo ser devuelto\",\n        \"error429\": \"Límite de tasa excedido\",\n        \"error500\": \"El servidor no puede cumplir con tu solicitud\",\n        \"errorPDF\": \"Este archivo no parece un PDF\",\n        \"error_edit_dir\": \"No se puede editar un directorio\",\n        \"error_edit_size\": \"El tamaño del archivo excede el tamaño máximo permitido\",\n        \"invalid_form\": \"Formulario inválido\",\n        \"invalid_credentials\": \"Credenciales inválidas, por favor reintenta\",\n        \"invalid_csrf\": \"El token del formulario no es válido\",\n        \"invalid_token\": \"Permisos inválidos\",\n        \"confirm_logout\": \"¿Estás seguro de que quieres cerrar sesión?\",\n        \"wait\": \"Por favor espera...\",\n        \"ok\": \"OK\",\n        \"failed\": \"Fallido\",\n        \"cancel\": \"No, volver\",\n        \"submit\": \"Guardar\",\n        \"back\": \"Atrás\",\n        \"add\": \"Añadir\",\n        \"enable\": \"Habilitar\",\n        \"disable\": \"Deshabilitar\",\n        \"disable_confirm_btn\": \"Sí, deshabilitar\",\n        \"close\": \"Cerrar\",\n        \"search\": \"Buscar\",\n        \"configuration\": \"Configuración\",\n        \"config_saved\": \"Configuración guardada\",\n        \"rename\": \"Renombrar\",\n        \"confirm\": \"Sí, proceder\",\n        \"edit\": \"Editar\",\n        \"delete\": \"Eliminar\",\n        \"delete_confirm_btn\": \"Sí, eliminar\",\n        \"delete_confirm\": \"¿Quieres eliminar \\\"{{- name}}\\\"? Esta acción es irreversible\",\n        \"delete_confirm_generic\": \"¿Quieres eliminar el elemento seleccionado? Esta acción es irreversible\",\n        \"delete_multi_confirm\": \"¿Quieres eliminar los elementos seleccionados? Esta acción es irreversible\",\n        \"delete_error_generic\": \"No se puede eliminar el elemento seleccionado\",\n        \"delete_error_403\": \"$t(general.delete_error_generic). $t(general.error403)\",\n        \"delete_error_404\": \"$t(general.delete_error_generic). $t(general.error404)\",\n        \"loading\": \"Cargando...\",\n        \"name\": \"Nombre\",\n        \"size\": \"Tamaño\",\n        \"last_modified\": \"Última modificación\",\n        \"info\": \"Info\",\n        \"datetime\": \"{{- val, datetime}}\",\n        \"selected_items_one\": \"{{count}} elemento seleccionado\",\n        \"selected_items_other\": \"{{count}} elementos seleccionados\",\n        \"selected_with_placeholder\": \"%d elementos seleccionados\",\n        \"name_required\": \"El nombre es obligatorio\",\n        \"name_different\": \"El nuevo nombre debe ser diferente del nombre actual\",\n        \"html5_media_not_supported\": \"Tu navegador no soporta audio/video HTML5.\",\n        \"choose_target_folder\": \"Elegir carpeta de destino\",\n        \"source_name\": \"Nombre de origen\",\n        \"target_folder\": \"Carpeta de destino\",\n        \"dest_name\": \"Nombre de destino\",\n        \"my_profile\": \"Mi perfil\",\n        \"description\": \"Descripción\",\n        \"email\": \"Correo electrónico\",\n        \"api_key_auth\": \"Autenticación por clave API\",\n        \"api_key_auth_help\": \"Permitir hacerse pasar por uno mismo, en la API REST, usando una clave API\",\n        \"pub_keys\": \"Claves públicas\",\n        \"pub_key_placeholder\": \"Pega una clave pública aquí\",\n        \"pub_keys_help\": \"Las claves públicas pueden ser usadas para la autenticación SFTP\",\n        \"verify\": \"Verificar\",\n        \"problems\": \"¿Tienes problemas?\",\n        \"allowed_ip_mask\": \"IP/Máscara permitida\",\n        \"denied_ip_mask\": \"IP/Máscara denegada\",\n        \"ip_mask_help\": \"IP/Máscara separadas por comas en formato CIDR, por ejemplo \\\"192.168.1.0/24,10.8.0.100/32\\\"\",\n        \"allowed_ip_mask_invalid\": \"IP/Máscara permitida inválida\",\n        \"username_required\": \"El nombre de usuario es obligatorio\",\n        \"password_required\": \"La contraseña es obligatoria\",\n        \"foldername_required\": \"El nombre de la carpeta es obligatorio\",\n        \"err_user\": \"No se puede validar tu usuario\",\n        \"err_protocol_forbidden\": \"El protocolo HTTP no está permitido para tu usuario\",\n        \"pwd_login_forbidden\": \"El método de inicio de sesión con contraseña no está permitido para tu usuario\",\n        \"ip_forbidden\": \"Inicio de sesión no permitido desde esta dirección IP\",\n        \"email_invalid\": \"La dirección de correo electrónico no es válida\",\n        \"err_password_complexity\": \"La contraseña proporcionada no cumple con los requisitos de complejidad\",\n        \"no_oidc_feature\": \"Esta característica no está disponible si has iniciado sesión con OpenID\",\n        \"connection_forbidden\": \"No tienes permiso para conectarte\",\n        \"no_permissions\": \"No tienes permiso para cambiar nada\",\n        \"path_invalid\": \"Ruta inválida\",\n        \"err_quota_read\": \"Lectura denegada debido al límite de cuota\",\n        \"profile_updated\": \"Tu perfil ha sido actualizado exitosamente\",\n        \"share_ok\": \"Acceso compartido exitoso, ahora puedes usar tu enlace\",\n        \"qr_code\": \"Código QR\",\n        \"copy_link\": \"Copiar enlace\",\n        \"copied\": \"Copiado\",\n        \"active\": \"Activo\",\n        \"inactive\": \"Inactivo\",\n        \"colvis\": \"Visibilidad de columnas\",\n        \"actions\": \"Acciones\",\n        \"template\": \"Usar como plantilla\",\n        \"quota_scan\": \"Escaneo de cuota\",\n        \"quota_scan_started\": \"Escaneo de cuota iniciado. Puede tardar un tiempo dependiendo del número de archivos a comprobar\",\n        \"quota_scan_conflit\": \"Otro escaneo ya está en progreso\",\n        \"quota_scan_error\": \"No se puede iniciar el escaneo de cuota\",\n        \"role\": \"Rol\",\n        \"role_placeholder\": \"Seleccionar un rol\",\n        \"group_placeholder\": \"Seleccionar un grupo\",\n        \"folder_placeholder\": \"Seleccionar una carpeta\",\n        \"blank_default_help\": \"Dejar en blanco para el valor predeterminado\",\n        \"skip_tls_verify\": \"Omitir verificación TLS. Esto solo debe usarse para pruebas\",\n        \"advanced_settings\": \"Configuraciones avanzadas\",\n        \"default\": \"Predeterminado\",\n        \"private_key\": \"Clave privada\",\n        \"acls\": \"ACLs\",\n        \"quota_limits\": \"Cuota de disco y límites de ancho de banda\",\n        \"expiration\": \"Expiración\",\n        \"expiration_help\": \"Elige una fecha de expiración\",\n        \"additional_info\": \"Información adicional\",\n        \"permissions\": \"Permisos\",\n        \"visible\": \"Visible\",\n        \"hidden\": \"Oculto\",\n        \"allowed\": \"Permitido\",\n        \"denied\": \"Denegado\",\n        \"zero_no_limit_help\": \"0 significa sin límite\",\n        \"global_settings\": \"Configuraciones globales\",\n        \"mandatory_encryption\": \"Cifrado obligatorio\",\n        \"name_invalid\": \"El nombre especificado no es válido, se permiten los siguientes caracteres: a-zA-Z0-9-_.~\",\n        \"associations\": \"Asociaciones\",\n        \"template_placeholders\": \"Se admiten los siguientes marcadores de posición\",\n        \"duplicated_username\": \"El nombre de usuario especificado ya existe\",\n        \"duplicated_name\": \"El nombre especificado ya existe\",\n        \"permissions_required\": \"Se requieren permisos\",\n        \"configs_saved\": \"Las configuraciones han sido actualizadas exitosamente\",\n        \"protocol\": \"Protocolo\",\n        \"refresh\": \"Actualizar\",\n        \"members\": \"Miembros\",\n        \"members_summary\": \"Usuarios: {{users}}. Administradores: {{admins}}\",\n        \"status\": \"Estado\",\n        \"last_login\": \"Último inicio de sesión\",\n        \"previous\": \"Anterior\",\n        \"next\": \"Siguiente\",\n        \"type\": \"Tipo\",\n        \"issuer\": \"Emisor\",\n        \"data_provider\": \"Base de datos\",\n        \"driver\": \"Controlador\",\n        \"mode\": \"Modo\",\n        \"port\": \"Puerto\",\n        \"domain\": \"Dominio\",\n        \"test\": \"Prueba\",\n        \"get\": \"Obtener\",\n        \"export\": \"Exportar\",\n        \"value\": \"Valor\",\n        \"method\": \"Método\",\n        \"timeout\": \"Tiempo de espera\",\n        \"env_vars\": \"Variables de entorno\",\n        \"hours\": \"Horas\",\n        \"paths\": \"Rutas\",\n        \"hour\": \"Hora\",\n        \"day_of_week\": \"Día de la semana\",\n        \"day_of_month\": \"Día del mes\",\n        \"month\": \"Mes\",\n        \"options\": \"Opciones\",\n        \"expired\": \"Expirado\",\n        \"unsupported\": \"Característica ya no soportada\",\n        \"start\": \"Inicio (HH:MM)\",\n        \"end\": \"Fin (HH:MM)\",\n        \"monday\": \"Lunes\",\n        \"tuesday\": \"Martes\",\n        \"wednesday\": \"Miércoles\",\n        \"thursday\": \"Jueves\",\n        \"friday\": \"Viernes\",\n        \"saturday\": \"Sábado\",\n        \"sunday\": \"Domingo\",\n        \"expired_session\": \"Tu sesión ha expirado. Por favor, inicia sesión de nuevo\"\n    },\n    \"fs\": {\n        \"view_file\": \"Ver archivo \\\"{{- path}}\\\"\",\n        \"edit_file\": \"Editar archivo \\\"{{- path}}\\\"\",\n        \"new_folder\": \"Nueva carpeta\",\n        \"select_across_pages\": \"Seleccionar a través de páginas\",\n        \"download\": \"Descargar\",\n        \"download_ready\": \"Tu descarga está lista\",\n        \"upload_queue_not_ready\": \"La cola de subida aún se está cargando, por favor intenta de nuevo en unos momentos\",\n        \"upload_no_files\": \"No se han seleccionado archivos. Por favor elige o arrastra archivos para subir\",\n        \"move_copy\": \"Mover o copiar\",\n        \"share\": \"Compartir\",\n        \"home\": \"Inicio\",\n        \"create_folder_msg\": \"Nombre de la carpeta\",\n        \"no_more_subfolders\": \"No hay más subcarpetas aquí\",\n        \"no_files_folders\": \"Sin archivos o carpetas\",\n        \"invalid_name\": \"Los nombres de archivo o carpeta no pueden contener \\\"/\\\"\",\n        \"folder_name_required\": \"El nombre de la carpeta es obligatorio\",\n        \"deleting\": \"Eliminar {{idx}}/{{total}}\",\n        \"copying\": \"Copiar {{idx}}/{{total}}\",\n        \"moving\": \"Mover {{idx}}/{{total}}\",\n        \"uploading\": \"Subir {{idx}}/{{total}}\",\n        \"err_403\": \"Permiso denegado\",\n        \"err_429\": \"Demasiadas solicitudes simultáneas\",\n        \"err_generic\": \"No se puede acceder al recurso solicitado\",\n        \"err_validation\": \"Configuración del sistema de archivos inválida\",\n        \"err_exists\": \"El destino ya existe\",\n        \"dir_list\": {\n            \"err_generic\": \"Error al obtener el listado del directorio\",\n            \"err_403\": \"$t(fs.dir_list.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.dir_list.err_generic). $t(fs.err_429)\",\n            \"err_user\": \"$t(fs.dir_list.err_generic). $t(general.err_user)\"\n        },\n        \"create_dir\": {\n            \"err_generic\": \"Error creando nueva carpeta\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"save\": {\n            \"err_generic\": \"Error guardando archivo\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"delete\": {\n            \"err_generic\": \"No se puede eliminar \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.delete.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete.err_generic). $t(fs.err_429)\"\n        },\n        \"delete_multi\": {\n            \"err_generic_partial\": \"No todos los elementos seleccionados han sido eliminados, por favor recarga la página\",\n            \"err_generic\": \"No se pueden eliminar los elementos seleccionados\",\n            \"err_403_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)\",\n            \"err_429_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)\",\n            \"err_403\": \"$t(fs.delete_multi.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete_multi.err_generic). $t(fs.err_429)\"\n        },\n        \"copy\": {\n            \"msg\": \"Copiar\",\n            \"err_generic\": \"Error copiando archivos/directorios\",\n            \"err_403\": \"$t(fs.copy.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.copy.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.copy.err_generic). $t(fs.err_exists)\"\n        },\n        \"move\": {\n            \"msg\": \"Mover\",\n            \"err_generic\": \"Error moviendo archivos/directorios\",\n            \"err_403\": \"$t(fs.move.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.move.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.move.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"No soportado: si quieres mover un directorio asegúrate de que esté vacío\"\n        },\n        \"rename\": {\n            \"title\": \"Renombrar \\\"{{- name}}\\\"\",\n            \"new_name\": \"Nuevo nombre\",\n            \"err_generic\": \"No se puede renombrar \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.rename.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.rename.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.rename.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"No soportado: si quieres renombrar un directorio asegúrate de que esté vacío\"\n        },\n        \"upload\": {\n            \"text\": \"Subir archivos\",\n            \"success\": \"Archivos subidos exitosamente\",\n            \"message\": \"Suelta archivos aquí o haz clic para subir.\",\n            \"message_empty\": \"Este directorio está vacío. $t(fs.upload.message)\",\n            \"err_generic\": \"Error subiendo archivos\",\n            \"err_403\": \"$t(fs.upload.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.upload.err_generic). $t(fs.err_429)\",\n            \"err_dir_overwrite\": \"$t(fs.upload.err_generic). Hay directorios con el mismo nombre que los archivos: {{- val}}\",\n            \"overwrite_text\": \"Conflicto detectado. ¿Quieres sobrescribir los siguientes archivos/directorios?\"\n        },\n        \"quota_usage\": {\n            \"title\": \"Uso de cuota\",\n            \"disk\": \"Cuota de disco\",\n            \"size\": \"Tamaño: {{- val}}\",\n            \"size_percentage\": \"Tamaño: {{- val}} ({{percentage}}%)\",\n            \"files\": \"Archivos: {{- val}}\",\n            \"files_percentage\": \"Archivos: {{- val}} ({{percentage}}%)\",\n            \"transfer\": \"Cuota de transferencia\",\n            \"total\": \"Total: {{- val}}\",\n            \"total_percentage\": \"Total: {{- val}} ({{percentage}}%)\",\n            \"uploads\": \"Subidas: {{- val}}\",\n            \"uploads_percentage\": \"Subidas: {{- val}} ({{percentage}}%)\",\n            \"downloads\": \"Descargas: {{- val}}\",\n            \"downloads_percentage\": \"Descargas: {{- val}} ({{percentage}}%)\"\n        }\n    },\n    \"datatable\": {\n        \"info\": \"Mostrando _START_ a _END_ de _TOTAL_ registros\",\n        \"info_empty\": \"Mostrando ningún registro\",\n        \"info_filtered\": \"(filtrado de _MAX_ registros totales)\",\n        \"processing\": \"Procesando...\",\n        \"no_records\": \"No se encontraron registros\"\n    },\n    \"editor\": {\n        \"keybinding\": \"Atajos de teclado del editor\",\n        \"search\": \"Abrir panel de búsqueda\",\n        \"goto\": \"Ir a la línea\",\n        \"indent_more\": \"Aumentar sangría\",\n        \"indent_less\": \"Disminuir sangría\"\n    },\n    \"2fa\": {\n        \"title\": \"Autenticación de dos factores usando aplicaciones de autenticación\",\n        \"msg_enabled\": \"La autenticación de dos factores está habilitada\",\n        \"msg_disabled\": \"Asegura tu cuenta\",\n        \"msg_info\": \"La autenticación de dos factores añade una capa extra de seguridad a tu cuenta. Para iniciar sesión necesitarás proporcionar un código de autenticación adicional.\",\n        \"require\": \"La autenticación de dos factores es requerida para esta cuenta\",\n        \"require_for\": \"Requerir 2FA para\",\n        \"generate\": \"Generar nueva clave secreta\",\n        \"recovery_codes\": \"Códigos de recuperación\",\n        \"new_recovery_codes\": \"Nuevos códigos de recuperación\",\n        \"recovery_codes_msg1\": \"Los códigos de recuperación son códigos de respaldo de un solo uso para iniciar sesión cuando no puedes acceder a tu aplicación de autenticación. Úsalos para acceder a tu cuenta y gestionar la configuración de autenticación de dos factores si pierdes tu teléfono.\",\n        \"recovery_codes_msg2\": \"Para mantener segura tu cuenta, no compartas ni distribuyas tus códigos de recuperación. Recomendamos guardarlos con un gestor de contraseñas seguro.\",\n        \"recovery_codes_msg3\": \"Si generas nuevos códigos de recuperación, invalidas automáticamente los antiguos.\",\n        \"info_title\": \"Aprende sobre la autenticación de dos factores\",\n        \"info1\": \"El protocolo SSH (comandos SFTP/SCP/SSH) pedirá el código de acceso si el cliente utiliza autenticación interactiva por teclado.\",\n        \"info2\": \"El protocolo HTTP significa interfaz web y API REST. La interfaz web pedirá el código de acceso usando una página específica. Para la API REST tienes que añadir el código de acceso usando un encabezado HTTP.\",\n        \"info3\": \"FTP no tiene una forma estándar de soportar la autenticación de dos factores, si habilitas el soporte FTP, tienes que añadir el código TOTP después de la contraseña. Por ejemplo, si tu contraseña es \\\"password\\\" y tu código de acceso de una sola vez es \\\"123456\\\", tienes que usar \\\"password123456\\\" como contraseña.\",\n        \"info4\": \"WebDAV no está soportado ya que cada solicitud individual debe ser autenticada y un código de acceso no puede ser reutilizado.\",\n        \"setup_title\": \"Configurar la autenticación de dos factores\",\n        \"setup_msg\": \"Usa tu aplicación de autenticación preferida (ej. Microsoft Authenticator, Google Authenticator, Authy, 1Password, etc.) para escanear el código QR. Generará un código de autenticación para que lo introduzcas a continuación.\",\n        \"setup_help\": \"Si tienes problemas usando el código QR, selecciona entrada manual en tu aplicación e introduce el código:\",\n        \"disable_msg\": \"Deshabilitar 2FA\",\n        \"disable_confirm\": \"¿Quieres deshabilitar la autenticación de dos factores para el usuario seleccionado? Esta acción generalmente solo se requiere si el usuario ha perdido el acceso al segundo factor de autenticación\",\n        \"disable_question\": \"¿Quieres deshabilitar la autenticación de dos factores?\",\n        \"generate_question\": \"¿Quieres generar una nueva clave secreta e invalidar la anterior? Cualquier aplicación de autenticación registrada dejará de funcionar\",\n        \"disabled\": \"La autenticación de dos factores está deshabilitada\",\n        \"recovery_codes_gen_err\": \"Error al generar nuevos códigos de recuperación\",\n        \"recovery_codes_get_err\": \"No se pueden obtener los códigos de recuperación\",\n        \"auth_code_invalid\": \"Error al validar el código de autenticación proporcionado\",\n        \"auth_secret_gen_err\": \"Error al generar el secreto de autenticación\",\n        \"save_err\": \"Error al guardar la configuración de autenticación de dos factores\",\n        \"auth_code_required\": \"El código de autenticación es obligatorio\",\n        \"no_protocol\": \"Por favor selecciona al menos un protocolo\",\n        \"required_protocols\": \"La política de seguridad configurada para tu cuenta requiere autenticación de dos factores para los siguientes protocolos: {{val}}\",\n        \"recovery_codes_generate\": \"Generar nuevos códigos de recuperación\",\n        \"recovery_codes_view\": \"Ver códigos de recuperación\"\n    },\n    \"share\": {\n        \"scope\": \"Alcance\",\n        \"scope_read\": \"Lectura\",\n        \"scope_write\": \"Escritura\",\n        \"scope_read_write\": \"Lectura/Escritura\",\n        \"scope_help\": \"Para el alcance \\\"Escritura\\\" y \\\"Lectura/Escritura\\\" tienes que definir una sola ruta y debe ser un directorio\",\n        \"path_help\": \"ruta de archivo o directorio, ej. /dir o /dir/file.txt\",\n        \"password_help\": \"Si se establece, el recurso compartido estará protegido por contraseña\",\n        \"max_tokens\": \"Tokens máximos\",\n        \"max_tokens_help\": \"Número máximo de veces que se puede acceder a este recurso compartido. 0 significa sin límite\",\n        \"view_manage\": \"Ver y gestionar recursos compartidos\",\n        \"no_share\": \"Sin recursos compartidos\",\n        \"password_protected\": \"Protegido por contraseña.\",\n        \"expiration_date\": \"Expiración: {{- val, datetime}}. \",\n        \"last_use\": \"Último uso: {{- val, datetime}}. \",\n        \"usage\": \"Uso: {{used}}/{{total}}. \",\n        \"used_tokens\": \"Token usado: {{used}}. \",\n        \"scope_invalid\": \"Alcance inválido\",\n        \"max_tokens_invalid\": \"Tokens máximos inválidos\",\n        \"expiration_invalid\": \"Expiración inválida\",\n        \"err_no_password\": \"No tienes permiso para compartir archivos/carpetas sin contraseña\",\n        \"expiration_out_of_range\": \"Establece una fecha de expiración y asegúrate de que sea menor o igual a {{- val, datetime}}\",\n        \"generic\": \"Error inesperado guardando recurso compartido\",\n        \"path_required\": \"Se requiere al menos una ruta\",\n        \"path_write_scope\": \"El alcance de escritura requiere exactamente una ruta\",\n        \"nested_paths\": \"Las rutas no pueden estar anidadas\",\n        \"expiration_past\": \"La fecha de expiración debe ser en el futuro\",\n        \"usage_exceed\": \"Uso máximo de compartición excedido\",\n        \"expired\": \"La compartición ha expirado\",\n        \"browsable_multiple_paths\": \"Un recurso compartido con múltiples rutas no es navegable\",\n        \"browsable_non_dir\": \"El recurso compartido no es un directorio por lo que no es navegable\",\n        \"go\": \"Ir al recurso compartido\",\n        \"access_links_title\": \"Enlaces de acceso compartido\",\n        \"link_single_title\": \"Archivo zip único\",\n        \"link_single_desc\": \"Puedes descargar el contenido compartido como un solo archivo zip\",\n        \"link_dir_title\": \"Directorio único\",\n        \"link_dir_desc\": \"Si el recurso compartido consiste en un solo directorio, puedes navegar y descargar archivos\",\n        \"link_uncompressed_title\": \"Archivo sin comprimir\",\n        \"link_uncompressed_desc\": \"Si el recurso compartido consiste en un solo archivo, también se puede descargar sin comprimir\",\n        \"upload_desc\": \"Puedes subir uno o más archivos al directorio compartido\",\n        \"expired_desc\": \"Este recurso compartido ya no es accesible porque ha expirado\",\n        \"invalid_path\": \"El directorio compartido falta o no es accesible\"\n    },\n    \"select2\": {\n        \"no_results\": \"No se encontraron resultados\",\n        \"searching\": \"Buscando...\",\n        \"removeall\": \"Eliminar todos los elementos\",\n        \"remove\": \"Eliminar elemento\"\n    },\n    \"change_pwd\": {\n        \"info\": \"Introduce tu contraseña actual, por razones de seguridad, y luego tu nueva contraseña dos veces, para verificar que la has escrito correctamente. Se cerrará la sesión después de cambiar tu contraseña\",\n        \"current\": \"Contraseña actual\",\n        \"new\": \"Nueva contraseña\",\n        \"confirm\": \"Confirmar contraseña\",\n        \"save\": \"Cambiar mi contraseña\",\n        \"required_fields\": \"Por favor proporciona la contraseña actual y la nueva dos veces\",\n        \"no_match\": \"Los dos campos de contraseña no coinciden\",\n        \"no_different\": \"La nueva contraseña debe ser diferente de la actual\",\n        \"current_no_match\": \"La contraseña actual no coincide\",\n        \"generic\": \"Error inesperado al cambiar la contraseña\",\n        \"required\": \"Se requiere cambio de contraseña. Establece una nueva contraseña para continuar usando tu cuenta\"\n    },\n    \"user\": {\n        \"view_manage\": \"Ver y gestionar usuarios\",\n        \"username_reserved\": \"El nombre de usuario especificado está reservado\",\n        \"username_invalid\": \"El nombre especificado no es válido, se permiten los siguientes caracteres: a-z A-Z 0-9 - _ . ~\",\n        \"home_required\": \"El directorio de inicio es obligatorio\",\n        \"home_invalid\": \"El directorio de inicio debe ser una ruta absoluta\",\n        \"pub_key_invalid\": \"Clave pública inválida\",\n        \"priv_key_invalid\": \"Clave privada inválida\",\n        \"key_invalid_size\": \"Clave pública RSA inválida: el tamaño mínimo soportado es 2048\",\n        \"key_insecure\": \"Formato de clave pública inseguro no permitido\",\n        \"err_primary_group\": \"Solo se permite un grupo primario\",\n        \"err_duplicate_group\": \"Grupos duplicados detectados\",\n        \"no_permissions\": \"Los permisos de directorios son obligatorios\",\n        \"no_root_permissions\": \"Se requieren permisos del directorio de inicio\",\n        \"err_permissions_generic\": \"Permisos inválidos: Asegúrate de usar rutas absolutas válidas\",\n        \"2fa_invalid\": \"Configuración inválida para autenticación de dos factores\",\n        \"recovery_codes_invalid\": \"Códigos de recuperación inválidos\",\n        \"folder_path_required\": \"La ruta de montaje de la carpeta virtual es obligatoria\",\n        \"folder_duplicated\": \"Carpetas virtuales duplicadas detectadas\",\n        \"folder_overlapped\": \"Carpetas virtuales superpuestas detectadas\",\n        \"folder_quota_size_invalid\": \"La cuota como tamaño de carpetas virtuales debe ser mayor o igual a -1\",\n        \"folder_quota_file_invalid\": \"La cuota como archivos de carpetas virtuales debe ser mayor o igual a -1\",\n        \"folder_quota_invalid\": \"Las cuotas como tamaño y como número de archivos de carpetas virtuales deben ser ambas -1 o mayor o igual a 0\",\n        \"ip_filters_invalid\": \"Filtros IP inválidos, asegúrate de que respetan la notación CIDR, por ejemplo 192.168.1.0/24\",\n        \"src_bw_limits_invalid\": \"Límites de velocidad de ancho de banda por fuente inválidos\",\n        \"share_expiration_invalid\": \"La expiración para recursos compartidos debe ser mayor o igual al valor predeterminado definido\",\n        \"file_pattern_path_invalid\": \"Las rutas de filtro de patrón de nombre de archivo deben ser absolutas\",\n        \"file_pattern_duplicated\": \"Filtros de patrón de nombre de archivo duplicados detectados\",\n        \"file_pattern_invalid\": \"Filtros de patrón de nombre de archivo inválidos\",\n        \"disable_active_2fa\": \"La autenticación de dos factores no se puede deshabilitar para un usuario con una configuración activa\",\n        \"pwd_change_conflict\": \"No es posible solicitar un cambio de contraseña y al mismo tiempo evitar que se cambie la contraseña\",\n        \"two_factor_conflict\": \"No puedes requerir autenticación de dos factores y al mismo tiempo evitar que se configure\",\n        \"role_help\": \"Los usuarios con un rol pueden ser gestionados por administradores globales y administradores con el mismo rol\",\n        \"require_pwd_change\": \"Requerir cambio de contraseña\",\n        \"require_pwd_change_help\": \"El usuario necesitará cambiar la contraseña desde el WebClient para activar la cuenta\",\n        \"groups_help\": \"La pertenencia a grupos imparte las configuraciones de los grupos con la excepción de los grupos de solo pertenencia\",\n        \"primary_group\": \"Grupo primario\",\n        \"secondary_groups\": \"Grupos secundarios\",\n        \"membership_groups\": \"Grupos de pertenencia\",\n        \"template_help\": \"Para cada usuario establece el nombre de usuario y al menos uno de entre la contraseña y la clave pública\",\n        \"virtual_folders_help\": \"Cuota tamaño/archivos -1 significa incluido dentro de la cuota de usuario, 0 ilimitado. No establecer -1 para carpetas compartidas. Puedes usar sufijo MB/GB/TB. Sin sufijo asumimos bytes\",\n        \"disconnect\": \"Desconectar al usuario después de la actualización\",\n        \"disconnect_help\": \"De esta manera fuerzas al usuario a iniciar sesión de nuevo, si está conectado, y así usar la nueva configuración\",\n        \"invalid_quota_size\": \"Tamaño de cuota inválido\",\n        \"expires_in\": \"Expira en\",\n        \"expires_in_help\": \"Expiración de la cuenta como número de días desde la creación. 0 significa sin expiración\",\n        \"additional_emails\": \"Correos electrónicos adicionales\",\n        \"tls_certs\": \"Certificados TLS\",\n        \"tls_certs_help\": \"Los certificados TLS pueden ser usados para autenticación FTP y/o WebDAV\",\n        \"tls_cert_help\": \"Pega un certificado TLS codificado en PEM aquí\",\n        \"tls_cert_invalid\": \"Certificado TLS inválido\",\n        \"template_title\": \"Crear uno o más usuarios nuevos desde esta plantilla\",\n        \"template_username_placeholder\": \"reemplazado con el nombre de usuario especificado\",\n        \"template_password_placeholder\": \"reemplazado con la contraseña especificada\",\n        \"template_help1\": \"Los marcadores de posición serán reemplazados en rutas y credenciales del backend de almacenamiento configurado.\",\n        \"template_help2\": \"Los usuarios generados pueden ser guardados o exportados. Los usuarios exportados pueden ser importados desde la sección \\\"Mantenimiento\\\" de esta instancia de SFTPGo u otra.\",\n        \"template_no_user\": \"Ningún usuario válido definido, no se puede completar la acción solicitada\",\n        \"time_of_day_invalid\": \"Hora del día inválida. Formato soportado HH:MM\",\n        \"time_of_day_conflict\": \"Hora del día inválida. La hora de fin no puede ser anterior a la hora de inicio\"\n    },\n    \"group\": {\n        \"view_manage\": \"Ver y gestionar grupos\",\n        \"err_delete_referenced\": \"No se puede eliminar un grupo con usuarios asociados, elimina las asociaciones primero\",\n        \"help\": \"El marcador de posición %username% será reemplazado con el nombre de usuario del usuario asociado, %role% con su rol\"\n    },\n    \"virtual_folders\": {\n        \"view_manage\": \"Ver y gestionar carpetas virtuales\",\n        \"mount_path\": \"ruta de montaje, ej. /vfolder\",\n        \"quota_size\": \"Cuota de tamaño\",\n        \"quota_size_help\": \"0 significa sin límite. Puedes usar sufijo MB/GB/TB\",\n        \"quota_files\": \"Cuota de archivos\",\n        \"associations_summary\": \"Usuarios: {{users}}. Grupos: {{groups}}\",\n        \"template_title\": \"Crear una o más nuevas carpetas virtuales desde esta plantilla\",\n        \"template_name_placeholder\": \"reemplazado con el nombre de la carpeta virtual especificada\",\n        \"template_help\": \"Las carpetas virtuales generadas pueden ser guardadas o exportadas. Las carpetas exportadas pueden ser importadas desde la sección \\\"Mantenimiento\\\" de esta instancia de SFTPGo u otra.\",\n        \"name\": \"Nombre de carpeta virtual\",\n        \"template_no_folder\": \"Ninguna carpeta virtual válida definida, no se puede completar la acción solicitada\"\n    },\n    \"storage\": {\n        \"title\": \"Sistema de archivos\",\n        \"label\": \"Almacenamiento\",\n        \"local\": \"Disco local\",\n        \"s3\": \"S3 (Compatible)\",\n        \"gcs\": \"GCS\",\n        \"azblob\": \"Azure Blob\",\n        \"encrypted\": \"Disco local cifrado\",\n        \"sftp\": \"SFTP\",\n        \"http\": \"HTTP\",\n        \"home_dir\": \"Directorio raíz\",\n        \"home_dir_placeholder\": \"Ruta absoluta a un directorio en disco local\",\n        \"home_dir_help1\": \"Dejar en blanco para un valor predeterminado apropiado\",\n        \"home_dir_help2\": \"Dejar en blanco y almacenamiento en \\\"Disco local\\\" para no anular el directorio raíz\",\n        \"home_dir_help3\": \"Requerido para proveedores de almacenamiento en disco local. Para otros proveedores de almacenamiento esta carpeta será usada para archivos temporales, puedes dejarla en blanco para un valor predeterminado apropiado\",\n        \"home_dir_invalid\": \"Directorio raíz inválido, asegúrate de que es una ruta absoluta\",\n        \"sftp_home_dir\": \"Directorio raíz SFTP\",\n        \"sftp_home_help\": \"Restringir acceso a esta ruta SFTP. Ejemplo: \\\"/somedir/subdir\\\"\",\n        \"os_read_buffer\": \"Búfer de descarga (MB)\",\n        \"os_buffer_help\": \"0 significa sin búfer\",\n        \"os_write_buffer\": \"Búfer de subida (MB)\",\n        \"bucket\": \"Cubo\",\n        \"region\": \"Región\",\n        \"access_key\": \"Clave de acceso\",\n        \"access_secret\": \"Secreto de acceso\",\n        \"sse_customer_key\": \"Clave de cifrado del lado del servidor\",\n        \"sse_customer_key_help\": \"Puedes almacenar tus datos cifrados con esta clave, pero si pierdes o cambias esta clave, perderás todos los archivos cifrados con ella. Los archivos que no estén cifrados o cifrados con una clave diferente no serán accesibles\",\n        \"endpoint\": \"Punto final\",\n        \"endpoint_help\": \"Para AWS S3, dejar en blanco para usar el punto final predeterminado para la región especificada\",\n        \"sftp_endpoint_help\": \"Punto final como host:puerto. El puerto siempre es requerido\",\n        \"ul_part_size\": \"Tamaño de parte de subida (MB)\",\n        \"part_size_help\": \"0 significa el predeterminado (5 MB). El mínimo es 5\",\n        \"gcs_part_size_help\": \"0 significa el predeterminado (16 MB)\",\n        \"ul_concurrency\": \"Concurrencia de subida\",\n        \"ul_concurrency_help\": \"Cuántas partes se suben en paralelo. 0 significa el predeterminado (5)\",\n        \"dl_part_size\": \"Tamaño de parte de descarga (MB)\",\n        \"dl_concurrency\": \"Concurrencia de descarga\",\n        \"dl_concurrency_help\": \"Cuántas partes se descargan en paralelo. 0 significa el predeterminado (5)\",\n        \"ul_part_timeout\": \"Tiempo de espera de parte de subida\",\n        \"ul_part_timeout_help\": \"Límite de tiempo máximo, en segundos, para subir una sola parte. 0 significa sin límite\",\n        \"gcs_ul_part_timeout_help\": \"Límite de tiempo máximo, en segundos, para subir una sola parte. 0 significa el predeterminado (32)\",\n        \"dl_part_timeout\": \"Tiempo de espera de parte de descarga\",\n        \"dl_part_timeout_help\": \"Límite de tiempo máximo, en segundos, para descargar una sola parte. 0 significa sin límite\",\n        \"key_prefix\": \"Prefijo de clave\",\n        \"key_prefix_help\": \"Restringir acceso a claves con el prefijo especificado. Ejemplo: \\\"somedir/subdir/\\\"\",\n        \"class\": \"Clase de almacenamiento\",\n        \"acl\": \"ACL\",\n        \"role_arn\": \"ARN de Rol\",\n        \"role_arn_help\": \"ARN de Rol IAM opcional para asumir\",\n        \"s3_path_style\": \"Usar direccionamiento estilo ruta, ej. \\\"endpoint/BUCKET/KEY\\\"\",\n        \"credentials_file\": \"Archivo de credenciales\",\n        \"credentials_file_help\": \"Añadir o actualizar credenciales desde un archivo JSON\",\n        \"auto_credentials\": \"Credenciales automáticas\",\n        \"auto_credentials_help\": \"Usar credenciales de aplicación predeterminadas o credenciales de variables de entorno\",\n        \"container\": \"Contenedor\",\n        \"account_name\": \"Nombre de cuenta\",\n        \"account_key\": \"Clave de cuenta\",\n        \"sas_url\": \"URL SAS\",\n        \"sas_url_help\": \"La URL de Firma de Acceso Compartido se puede usar en lugar del nombre/clave de cuenta\",\n        \"emulator\": \"Usar emulador\",\n        \"passphrase\": \"Frase de contraseña\",\n        \"passphrase_help\": \"Frase de contraseña usada para derivar la clave de cifrado por objeto\",\n        \"passphrase_key_help\": \"Frase de contraseña usada para proteger tu clave privada, si la hay\",\n        \"fingerprints\": \"Huellas dactilares\",\n        \"fingerprints_help\": \"Huellas dactilares SHA256 para validar al conectarse al servidor SFTP externo, una por línea. Si está vacío, se aceptará cualquier clave de host: ¡esto es un riesgo de seguridad!\",\n        \"sftp_buffer\": \"Tamaño del búfer (MB)\",\n        \"sftp_buffer_help\": \"Un tamaño de búfer mayor que 0 habilita transferencias concurrentes\",\n        \"sftp_concurrent_reads\": \"Deshabilitar lecturas concurrentes\",\n        \"relaxed_equality_check\": \"Verificación de igualdad relajada\",\n        \"relaxed_equality_check_help\": \"Habilitar para considerar solo el punto final para determinar si diferentes configuraciones apuntan al mismo servidor. Por defecto, tanto el punto final como el nombre de usuario deben coincidir\",\n        \"api_key\": \"Clave API\",\n        \"fs_error\": \"Error de configuración del sistema de archivos\",\n        \"bucket_required\": \"$t(storage.fs_error): se requiere bucket\",\n        \"region_required\": \"$t(storage.fs_error): se requiere región\",\n        \"key_prefix_invalid\": \"$t(storage.fs_error): prefijo de clave inválido, no puede empezar con \\\"/\\\"\",\n        \"ul_part_size_invalid\": \"$t(storage.fs_error): tamaño de parte de subida inválido\",\n        \"ul_concurrency_invalid\": \"$t(storage.fs_error): concurrencia de subida inválida\",\n        \"dl_part_size_invalid\": \"$t(storage.fs_error): tamaño de parte de descarga inválido\",\n        \"dl_concurrency_invalid\": \"$t(storage.fs_error): concurrencia de descarga inválida\",\n        \"access_key_required\": \"$t(storage.fs_error): se requiere Clave de acceso\",\n        \"access_secret_required\": \"$t(storage.fs_error): se requiere Secreto de acceso\",\n        \"credentials_required\": \"$t(storage.fs_error): se requieren credenciales\",\n        \"container_required\": \"$t(storage.fs_error): se requiere contenedor\",\n        \"account_name_required\": \"$t(storage.fs_error): se requiere nombre de cuenta\",\n        \"sas_url_invalid\": \"$t(storage.fs_error): URL SAS inválida\",\n        \"passphrase_required\": \"$t(storage.fs_error): se requiere frase de contraseña\",\n        \"endpoint_invalid\": \"$t(storage.fs_error): el punto final es inválido\",\n        \"endpoint_required\": \"$t(storage.fs_error): se requiere punto final\",\n        \"username_required\": \"$t(storage.fs_error): se requiere nombre de usuario\"\n    },\n    \"oidc\": {\n        \"token_expired\": \"Tu token OpenID ha expirado, por favor inicia sesión de nuevo\",\n        \"token_invalid_webadmin\": \"Tu token OpenID no es válido para la interfaz WebAdmin. Cierra sesión en tu servidor OpenID e inicia sesión en WebAdmin\",\n        \"token_invalid_webclient\": \"Tu token OpenID no es válido para la interfaz WebClient. Cierra sesión en tu servidor OpenID e inicia sesión en WebClient\",\n        \"token_exchange_err\": \"Error al intercambiar token OpenID\",\n        \"token_invalid\": \"Token OpenID inválido\",\n        \"role_admin_err\": \"Rol OpenID incorrecto, el usuario conectado no es un administrador\",\n        \"role_user_err\": \"Rol OpenID incorrecto, el usuario conectado es un administrador\",\n        \"get_user_err\": \"Error al obtener el usuario asociado con el token OpenID\"\n    },\n    \"oauth2\": {\n        \"auth_verify_error\": \"No se puede verificar el código OAuth2\",\n        \"auth_validation_error\": \"No se puede verificar el código OAuth2\",\n        \"auth_invalid\": \"Código OAuth2 inválido\",\n        \"token_exchange_err\": \"No se puede obtener el token OAuth2 del código de autorización\",\n        \"no_refresh_token\": \"El proveedor OAuth2 devolvió un token vacío. Algunos proveedores solo devuelven el token cuando el usuario autoriza por primera vez. Si ya has registrado SFTPGo con este usuario en el pasado, revoca el acceso e intenta de nuevo. De esta manera invalidarás el token anterior\",\n        \"success\": \"Copia la siguiente cadena, sin las comillas, en el campo de configuración Token OAuth2 SMTP:\",\n        \"client_id_required\": \"ID de Cliente es obligatorio\",\n        \"client_secret_required\": \"Secreto de Cliente es obligatorio\",\n        \"refresh_token_required\": \"Token de Actualización es obligatorio\"\n    },\n    \"filters\": {\n        \"password_strength\": \"Fortaleza de contraseña\",\n        \"password_strength_help\": \"Se sugieren valores en el rango 50-70 para casos de uso comunes. 0 significa deshabilitado, cualquier contraseña será aceptada\",\n        \"password_expiration\": \"Expiración de contraseña\",\n        \"password_expiration_help\": \"Expiración de contraseña como número de días. 0 significa sin expiración\",\n        \"default_shares_expiration\": \"Expiración predeterminada de compartidos\",\n        \"default_shares_expiration_help\": \"Expiración predeterminada para nuevos compartidos como número de días\",\n        \"max_shares_expiration\": \"Expiración máxima de compartidos\",\n        \"max_shares_expiration_help\": \"Expiración máxima permitida, como número de días, cuando un usuario crea o actualiza un compartido\",\n        \"directory_permissions\": \"Permisos por directorio\",\n        \"directory_permissions_help\": \"Se soportan comodines en rutas, por ejemplo \\\"/incoming/*\\\" coincide con cualquier directorio dentro de \\\"/incoming\\\"\",\n        \"directory_path_help\": \"ruta de directorio, ej. /dir\",\n        \"directory_patterns\": \"Restricciones de patrones de nombre por directorio\",\n        \"directory_patterns_help\": \"Archivos/directorios denegados o permitidos separados por comas, basado en patrones de shell. La coincidencia no distingue mayúsculas de minúsculas\",\n        \"max_sessions\": \"Sesiones máximas\",\n        \"max_sessions_help\": \"Número máximo de sesiones concurrentes. 0 significa sin límite\",\n        \"denied_protocols\": \"Protocolos denegados\",\n        \"denied_login_methods\": \"Métodos de inicio de sesión denegados\",\n        \"denied_login_methods_help\": \"\\\"password\\\" es válido para todos los protocolos soportados, \\\"password-over-SSH\\\" solo para SSH/SFTP/SCP\",\n        \"web_client_options\": \"Cliente web/API REST\",\n        \"max_upload_size\": \"Tamaño máximo de subida\",\n        \"max_upload_size_help\": \"Tamaño máximo de subida para un solo archivo. 0 significa sin límite. Puedes usar sufijo MB/GB/TB\",\n        \"max_upload_size_invalid\": \"Tamaño máximo de archivo de subida inválido\",\n        \"upload_bandwidth\": \"Ancho de banda UL (KB/s)\",\n        \"download_bandwidth\": \"Ancho de banda DL (KB/s)\",\n        \"upload_bandwidth_help\": \"UL (KB/s). 0 significa sin límite\",\n        \"download_bandwidth_help\": \"DL (KB/s). 0 significa sin límite\",\n        \"src_bandwidth_limit\": \"Límites de velocidad de ancho de banda por fuente\",\n        \"upload_data_transfer\": \"Transferencia de datos de subida (MB)\",\n        \"upload_data_transfer_help\": \"Transferencia de datos máxima permitida para subidas. 0 significa sin límite\",\n        \"download_data_transfer\": \"Transferencia de datos de descarga (MB)\",\n        \"download_data_transfer_help\": \"Transferencia de datos máxima permitida para descargas. 0 significa sin límite\",\n        \"total_data_transfer\": \"Transferencia de datos total (MB)\",\n        \"total_data_transfer_help\": \"Transferencia de datos máxima permitida para subidas + descargas. Reemplaza los límites individuales. 0 significa sin límite\",\n        \"start_directory\": \"Directorio inicial\",\n        \"start_directory_help\": \"Directorio inicial alternativo para usar en lugar de \\\"/\\\". Soportado para SFTP/FTP/HTTP\",\n        \"tls_username\": \"Nombre de usuario TLS\",\n        \"tls_username_help\": \"Define el campo del certificado TLS a usar como nombre de usuario. Ignorado si TLS mutuo está deshabilitado\",\n        \"ftp_security\": \"Seguridad FTP\",\n        \"ftp_security_help\": \"Ignorado si TLS ya es globalmente requerido para todos los usuarios FTP\",\n        \"hooks\": \"Ganchos (Hooks)\",\n        \"hook_ext_auth_disabled\": \"Autenticación externa deshabilitada\",\n        \"hook_pre_login_disabled\": \"Pre-inicio de sesión deshabilitado\",\n        \"hook_check_password_disabled\": \"Verificación de contraseña deshabilitada\",\n        \"is_anonymous\": \"Usuario anónimo\",\n        \"is_anonymous_help\": \"Los usuarios anónimos son soportados para protocolos FTP y WebDAV y tienen acceso de solo lectura\",\n        \"disable_fs_checks\": \"Deshabilitar comprobaciones del sistema de archivos\",\n        \"disable_fs_checks_help\": \"Deshabilitar comprobaciones de existencia y creación automática de directorio de inicio y carpetas virtuales\",\n        \"api_key_auth_help\": \"Permitir hacerse pasar por el usuario, en la API REST, con una clave API\",\n        \"external_auth_cache_time\": \"Tiempo de caché de autenticación externa\",\n        \"external_auth_cache_time_help\": \"Tiempo de caché, en segundos, para usuarios autenticados usando un gancho de autenticación externa. 0 significa sin caché\",\n        \"access_time\": \"Restricciones de tiempo de acceso\",\n        \"access_time_help\": \"Sin restricciones significa que el acceso siempre está permitido, la hora debe establecerse en el formato HH:MM\"\n    },\n    \"admin\": {\n        \"role_permissions\": \"Un administrador de rol no puede tener el permiso \\\"*\\\"\",\n        \"view_manage\": \"Ver y gestionar administradores\",\n        \"self_delete\": \"No puedes borrarte a ti mismo\",\n        \"self_permissions\": \"No puedes cambiar tus permisos\",\n        \"self_disable\": \"No puedes deshabilitarte a ti mismo\",\n        \"self_role\": \"No puedes añadir/cambiar tu rol\",\n        \"password_help\": \"Si se deja en blanco, la contraseña actual no se cambiará\",\n        \"role_help\": \"Establecer un rol limita al administrador a gestionar solo usuarios con el mismo rol. Los administradores con un rol no pueden ser superadministradores\",\n        \"users_groups\": \"Grupos para usuarios\",\n        \"users_groups_help\": \"Grupos seleccionados automáticamente para nuevos usuarios creados por este administrador. El administrador aún podrá elegir diferentes grupos. Estas configuraciones solo se usan para esta interfaz de administración y serán ignoradas en API REST/ganchos\",\n        \"group_membership\": \"Añadir como pertenencia\",\n        \"group_primary\": \"Añadir como primario\",\n        \"group_secondary\": \"Añadir como secundario\",\n        \"user_page_pref\": \"Preferencias de página de usuario\",\n        \"user_page_pref_help\": \"Puedes ocultar algunas secciones de la página de usuario. Estas no son configuraciones de seguridad y no se aplican del lado del servidor de ninguna manera. Solo están destinadas a simplificar la página de añadir/actualizar usuario\",\n        \"hide_sections\": \"Ocultar secciones\",\n        \"default_users_expiration\": \"Expiración predeterminada de usuarios\",\n        \"default_users_expiration_help\": \"Expiración predeterminada para nuevos usuarios como número de días\",\n        \"require_pwd_change_help\": \"Se requiere un cambio de contraseña en el próximo inicio de sesión\"\n    },\n    \"connections\": {\n        \"view_manage\": \"Ver y gestionar conexiones\",\n        \"started\": \"Iniciado\",\n        \"remote_address\": \"Dirección remota\",\n        \"last_activity\": \"Última actividad\",\n        \"disconnect_confirm_btn\": \"Sí, desconectar\",\n        \"disconnect_confirm\": \"¿Quieres desconectar la conexión seleccionada? Esta acción es irreversible\",\n        \"disconnect_ko\": \"No se puede desconectar la conexión seleccionada\",\n        \"upload\": \"Subida: \\\"{{- path}}\\\"\",\n        \"download\": \"Descarga: \\\"{{- path}}\\\"\",\n        \"upload_info\": \"$t(connections.upload). Tamaño: {{- size}}. Velocidad: {{- speed}}\",\n        \"download_info\": \"$t(connections.download). Tamaño: {{- size}}. Velocidad: {{- speed}}\",\n        \"client\": \"Cliente: {{- val}}\"\n    },\n    \"role\": {\n        \"view_manage\": \"Ver y gestionar roles\",\n        \"err_delete_referenced\": \"No se puede eliminar un rol con administradores asociados, elimina las asociaciones primero\"\n    },\n    \"ip_list\": {\n        \"view_manage\": \"Ver y gestionar listas de IP\",\n        \"defender_list\": \"Defensor\",\n        \"allow_list\": \"Lista permitida\",\n        \"ratelimiters_safe_list\": \"Lista segura de limitadores de tasa\",\n        \"ip_net\": \"IP/Red\",\n        \"protocols\": \"Protocolos\",\n        \"any\": \"Cualquiera\",\n        \"allow\": \"Permitir\",\n        \"deny\": \"Denegar\",\n        \"ip_net_help\": \"Dirección IP o red en formato CIDR, ejemplo: \\\"192.168.1.1 o 10.8.0.100/32 o 2001:db8:1234::/48\\\"\",\n        \"ip_invalid\": \"Dirección IP inválida\",\n        \"net_invalid\": \"Red inválida\",\n        \"duplicated\": \"La IP/red especificada ya existe\",\n        \"search\": \"IP/Red o parte inicial\",\n        \"defender_disabled\": \"Defensor deshabilitado en tu configuración\",\n        \"allow_list_disabled\": \"Lista permitida deshabilitada en tu configuración\",\n        \"ratelimiters_disabled\": \"Limitadores de tasa deshabilitados en tu configuración\"\n    },\n    \"defender\": {\n        \"view_manage\": \"Ver y gestionar lista de bloqueo automático\",\n        \"ip\": \"Dirección IP\",\n        \"ban_time\": \"Bloqueado hasta\",\n        \"score\": \"Puntuación\"\n    },\n    \"status\": {\n        \"desc\": \"Estado del servidor\",\n        \"ssh\": \"Servidor SSH/SFTP\",\n        \"active\": \"Estado: activo\",\n        \"disabled\": \"Estado: deshabilitado\",\n        \"error\": \"Estado: error\",\n        \"proxy_on\": \"Protocolo PROXY habilitado\",\n        \"address\": \"Dirección\",\n        \"ssh_auths\": \"Métodos de autenticación\",\n        \"ssh_commands\": \"Comandos aceptados\",\n        \"host_key\": \"Clave de host\",\n        \"fingeprint\": \"Huella dactilar\",\n        \"algorithms\": \"Algoritmos\",\n        \"algorithm\": \"Algoritmo\",\n        \"ssh_pub_key_algo\": \"Algoritmos de autenticación de clave pública\",\n        \"ssh_mac_algo\": \"Algoritmos de código de autenticación de mensajes (MAC)\",\n        \"ssh_kex_algo\": \"Algoritmos de intercambio de claves (KEX)\",\n        \"ssh_cipher_algo\": \"Cifrados\",\n        \"ftp\": \"Servidor FTP\",\n        \"ftp_passive_range\": \"Rango de puertos en modo pasivo\",\n        \"ftp_passive_ip\": \"IP pasiva\",\n        \"tls\": \"TLS\",\n        \"tls_disabled\": \"Deshabilitado\",\n        \"tls_explicit\": \"Modo explícito requerido (FTPES)\",\n        \"tls_implicit\": \"Modo implícito (FTPS), obsoleto, preferir FTPES\",\n        \"tls_mixed\": \"Modo plano y explícito (FTPES)\",\n        \"webdav\": \"Servidor WebDAV\",\n        \"rate_limiters\": \"Limitadores de tasa\"\n    },\n    \"maintenance\": {\n        \"backup\": \"Copia de seguridad\",\n        \"backup_do\": \"Haz una copia de seguridad de tus datos\",\n        \"backup_ok\": \"Copia de seguridad restaurada exitosamente\",\n        \"backup_invalid_file\": \"Archivo de copia de seguridad inválido, asegúrate de que es un archivo JSON con contenido válido\",\n        \"restore_error\": \"No se puede restaurar tu copia de seguridad, revisa los registros del servidor para más detalles\",\n        \"restore\": \"Restaurar\",\n        \"backup_file\": \"Archivo de copia de seguridad\",\n        \"backup_file_help\": \"Importar datos desde un archivo de copia de seguridad JSON\",\n        \"restore_mode0\": \"Añadir y actualizar\",\n        \"restore_mode1\": \"Solo añadir\",\n        \"restore_mode2\": \"Añadir, actualizar y desconectar\",\n        \"after_restore\": \"Después de restaurar\",\n        \"quota_mode0\": \"Sin actualización de cuota\",\n        \"quota_mode1\": \"Actualizar cuota\",\n        \"quota_mode2\": \"Actualizar cuota para usuarios con límites de cuota\"\n    },\n    \"acme\": {\n        \"title\": \"ACME\",\n        \"generic_error\": \"No se pueden obtener certificados TLS, revisa los registros del servidor para más detalles\",\n        \"help\": \"Desde esta sección puedes solicitar certificados TLS gratuitos para tus servicios SFTPGo usando el protocolo ACME y el tipo de desafío HTTP-01. Debes crear una entrada DNS bajo un dominio personalizado que poseas que resuelva a tu dirección IP pública de SFTPGo y el puerto 80 debe ser accesible públicamente. Puedes establecer las opciones de configuración para los casos de uso más comunes y configuraciones de un solo nodo aquí, para configuraciones avanzadas consulta la documentación de SFTPGo. Se requiere un reinicio del servicio para aplicar los cambios\",\n        \"domain_help\": \"Se pueden especificar múltiples dominios separados por comas o espacios. Se incluirán en el mismo certificado\",\n        \"email_help\": \"Correo electrónico usado para registro y contacto de recuperación\",\n        \"port_help\": \"Si es diferente de 80 tienes que configurar un proxy inverso\",\n        \"protocols_help\": \"Usar los certificados obtenidos para los protocolos especificados\"\n    },\n    \"smtp\": {\n        \"title\": \"SMTP\",\n        \"err_required_fields\": \"La dirección del remitente y el nombre de usuario no pueden estar ambos vacíos\",\n        \"help\": \"Establece la configuración SMTP reemplazando la definida usando variables de entorno o archivo de configuración si lo hay\",\n        \"host\": \"Nombre del servidor\",\n        \"host_help\": \"Si está en blanco, la configuración está deshabilitada\",\n        \"auth\": \"Autenticación\",\n        \"encryption\": \"Cifrado\",\n        \"sender\": \"Remitente\",\n        \"debug\": \"Registros de depuración\",\n        \"domain_help\": \"Dominio HELO. Dejar en blanco para usar el nombre de host del servidor\",\n        \"test_recipient\": \"Dirección para enviar correos de prueba\",\n        \"oauth2_provider\": \"Proveedor OAuth2\",\n        \"oauth2_provider_help\": \"URI a la que redirigir después de la autenticación del usuario\",\n        \"oauth2_tenant\": \"Inquilino OAuth2\",\n        \"oauth2_tenant_help\": \"Inquilino de Azure. Los valores típicos son \\\"common\\\", \\\"organizations\\\", \\\"consumers\\\" o el identificador del inquilino\",\n        \"oauth2_client_id\": \"ID de Cliente OAuth2\",\n        \"oauth2_client_secret\": \"Secreto de Cliente OAuth2\",\n        \"oauth2_token\": \"Token OAuth2\",\n        \"recipient_required\": \"Especifica un destinatario para enviar un correo de prueba\",\n        \"test_error\": \"No se puede enviar correo de prueba, revisa los registros del servidor para más detalles\",\n        \"test_ok\": \"No se reportaron errores al enviar el correo de prueba. Por favor revisa tu bandeja de entrada para asegurarte\",\n        \"oauth2_flow_error\": \"No se puede obtener la URI para iniciar el flujo OAuth2\",\n        \"oauth2_question\": \"¿Quieres iniciar el flujo OAuth2 para obtener un token?\"\n    },\n    \"sftp\": {\n        \"help\": \"Desde esta sección puedes habilitar algoritmos deshabilitados por defecto. No necesitas establecer valores ya definidos usando variables de entorno o archivo de configuración. Se requiere un reinicio del servicio para aplicar los cambios\",\n        \"host_key_algos\": \"Algoritmos de Clave de Host\"\n    },\n    \"branding\": {\n        \"title\": \"Marca\",\n        \"help\": \"Desde esta sección puedes personalizar SFTPGo para adaptarlo a tu marca y añadir un descargo de responsabilidad a las páginas de inicio de sesión\",\n        \"short_name\": \"Nombre corto\",\n        \"logo\": \"Logotipo\",\n        \"logo_help\": \"Imagen PNG, tamaño máximo aceptado 512x512, el tamaño predeterminado del logotipo es 256x256\",\n        \"favicon\": \"Favicon\",\n        \"disclaimer_name\": \"Título del descargo de responsabilidad\",\n        \"disclaimer_url\": \"URL del descargo de responsabilidad\",\n        \"invalid_png\": \"Imagen PNG inválida\",\n        \"invalid_png_size\": \"Tamaño de imagen PNG inválido\",\n        \"invalid_disclaimer_url\": \"La URL del descargo de responsabilidad debe ser un enlace http o https\"\n    },\n    \"events\": {\n        \"search\": \"Buscar registros\",\n        \"fs_events\": \"Eventos del sistema de archivos\",\n        \"provider_events\": \"Eventos del proveedor\",\n        \"other_events\": \"Otros eventos\",\n        \"quota_exceeded\": \"Cuota excedida\",\n        \"date_range\": \"Rango de fechas\",\n        \"upload\": \"Subida\",\n        \"download\": \"Descarga\",\n        \"mkdir\": \"Crear directorio\",\n        \"rmdir\": \"Eliminar directorio\",\n        \"rename\": \"Renombrar\",\n        \"delete\": \"Eliminación\",\n        \"copy\": \"Copia\",\n        \"first_upload\": \"Primera subida\",\n        \"first_download\": \"Primera descarga\",\n        \"ssh_cmd\": \"Comando SSH\",\n        \"add\": \"Adición\",\n        \"update\": \"Actualización\",\n        \"login_failed\": \"Inicio de sesión fallido\",\n        \"login_ok\": \"Inicio de sesión exitoso\",\n        \"login_missing_user\": \"Inicio de sesión con usuario inexistente\",\n        \"no_login_tried\": \"Ningún inicio de sesión intentado\",\n        \"algo_negotiation_failed\": \"Negociación de algoritmo fallida\",\n        \"datetime\": \"Fecha y hora\",\n        \"action\": \"Acción\",\n        \"path\": \"Ruta\",\n        \"object\": \"Objeto\",\n        \"event\": \"Evento\"\n    },\n    \"provider_objects\": {\n        \"user\": \"Usuario\",\n        \"folder\": \"Carpeta\",\n        \"group\": \"Grupo\",\n        \"admin\": \"Administrador\",\n        \"api_key\": \"Clave API\",\n        \"share\": \"Recurso compartido\",\n        \"event_action\": \"Acción\",\n        \"event_rule\": \"Regla\",\n        \"role\": \"rol\",\n        \"ip_list_entry\": \"Entrada de lista IP\",\n        \"configs\": \"Configuraciones\"\n    },\n    \"actions\": {\n        \"view_manage\": \"Ver y gestionar acciones de reglas para eventos\",\n        \"err_delete_referenced\": \"No se puede eliminar una acción con reglas asociadas, elimina las asociaciones primero\",\n        \"http_url\": \"URL del servidor\",\n        \"http_url_help\": \"ej. https://host:puerto/ruta. Se soportan marcadores de posición dentro de la ruta URL\",\n        \"http_url_required\": \"La URL es obligatoria\",\n        \"http_url_invalid\": \"La URL es inválida, se soportan esquemas http y https\",\n        \"http_part_name_required\": \"El nombre de la parte HTTP es obligatorio\",\n        \"http_part_body_required\": \"El cuerpo de la parte HTTP es obligatorio si no se proporciona ruta de archivo\",\n        \"http_multipart_body_error\": \"Las solicitudes multiparte no requieren cuerpo. El cuerpo de la solicitud se construye a partir de las partes especificadas\",\n        \"http_multipart_ctype_error\": \"Content-Type se establece automáticamente para solicitudes multiparte\",\n        \"path_duplicated\": \"Ruta duplicada\",\n        \"command_required\": \"El comando es obligatorio\",\n        \"command_invalid\": \"Comando inválido, debe ser una ruta absoluta\",\n        \"email_recipient_required\": \"Se requiere al menos un destinatario de correo electrónico\",\n        \"email_subject_required\": \"El asunto del correo electrónico es obligatorio\",\n        \"email_body_required\": \"El cuerpo del correo electrónico es obligatorio\",\n        \"retention_directory_required\": \"Se requiere al menos un directorio para comprobar\",\n        \"path_required\": \"Se requiere al menos una ruta\",\n        \"source_dest_different\": \"La ruta de origen y destino deben ser diferentes\",\n        \"root_not_allowed\": \"La ruta raíz (/) no está permitida\",\n        \"archive_name_required\": \"El nombre del archivo comprimido es obligatorio\",\n        \"idp_template_required\": \"Se requiere una plantilla de usuario o administrador\",\n        \"threshold\": \"Umbral\",\n        \"threshold_help\": \"Se generará una notificación por correo electrónico para los usuarios cuya contraseña expire en un número de días menor o igual a este umbral\",\n        \"disable_threshold\": \"Umbral de desactivación\",\n        \"disable_threshold_help\": \"Inactividad en días, desde el último inicio de sesión o creación antes de desactivar usuarios\",\n        \"delete_threshold\": \"Umbral de eliminación\",\n        \"delete_threshold_help\": \"Inactividad en días, desde el último inicio de sesión o creación antes de eliminar usuarios\",\n        \"inactivity_threshold_required\": \"Se debe definir al menos un umbral de inactividad\",\n        \"inactivity_thresholds_invalid\": \"El umbral de eliminación debe ser mayor que el umbral de desactivación\",\n        \"idp_mode_add_update\": \"Crear o actualizar\",\n        \"idp_mode_add\": \"Crear si no existe\",\n        \"template_user_help\": \"Plantilla para usuarios de SFTPGo en formato JSON. Se soportan marcadores de posición\",\n        \"template_admin_help\": \"Plantilla para administradores de SFTPGo en formato JSON. Se soportan marcadores de posición\",\n        \"placeholders_help\": \"Se soportan marcadores de posición\",\n        \"http_headers\": \"Encabezados HTTP\",\n        \"query_parameters\": \"Parámetros de cadena de consulta\",\n        \"http_timeout_help\": \"Ignorado para solicitudes multiparte con archivos como adjuntos\",\n        \"body\": \"Cuerpo\",\n        \"http_body_help\": \"Se soportan marcadores de posición. Ignorado para solicitud HTTP get. Dejar vacío para solicitudes multiparte\",\n        \"multipart_body\": \"Cuerpo multiparte\",\n        \"multipart_body_help\": \"Las solicitudes HTTP Multiparte permiten combinar uno o más conjuntos de datos en un solo cuerpo. Para cada parte, puedes establecer una ruta de archivo o un cuerpo como texto. Se soportan marcadores de posición en ruta de archivo, cuerpo, valores de encabezado\",\n        \"http_part_name\": \"Nombre de parte\",\n        \"http_part_file\": \"Ruta de archivo\",\n        \"http_part_headers\": \"Encabezados de parte adicionales uno por línea como \\\"clave: valor\\\"\",\n        \"command_help\": \"Ruta absoluta del comando a ejecutar\",\n        \"command_args\": \"Argumentos\",\n        \"command_args_help\": \"Argumentos de comando separados por comas. Se soportan marcadores de posición\",\n        \"command_env_vars_help\": \"Se soportan marcadores de posición en valores. Establecer el valor a \\\"$\\\" sin comillas significa recuperar la clave del entorno excluyendo claves que empiezan con SFTPGO_\",\n        \"email_recipients\": \"Para\",\n        \"email_recipients_help\": \"Destinatarios separados por comas. Se soportan marcadores de posición\",\n        \"email_bcc\": \"Cco\",\n        \"email_bcc_help\": \"Direcciones Cco separadas por comas. Se soportan marcadores de posición\",\n        \"email_subject\": \"Asunto\",\n        \"content_type\": \"Tipo de contenido\",\n        \"attachments\": \"Adjuntos\",\n        \"attachments_help\": \"Rutas separadas por comas para adjuntar. Se soportan marcadores de posición. El tamaño total está limitado a 10 MB\",\n        \"data_retention\": \"Retención de datos\",\n        \"data_retention_help\": \"Establece la retención de datos, como horas, por ruta. La retención se aplica recursivamente. Establecer 0 como retención significa excluir la ruta especificada\",\n        \"delete_empty_dirs\": \"Eliminar directorios vacíos\",\n        \"fs_action\": \"Acción del sistema de archivos\",\n        \"paths_src_dst_help\": \"Rutas como las ven los usuarios de SFTPGo. Se soportan marcadores de posición. Los permisos requeridos se otorgan automáticamente\",\n        \"source_path\": \"Origen\",\n        \"target_path\": \"Destino\",\n        \"paths_help\": \"Rutas separadas por comas como las ven los usuarios de SFTPGo. Se soportan marcadores de posición. Los permisos requeridos se otorgan automáticamente\",\n        \"archive_path\": \"Ruta de archivo\",\n        \"archive_path_help\": \"Ruta completa, como la ven los usuarios de SFTPGo, al archivo zip a crear. Se soportan marcadores de posición. Si el archivo especificado ya existe, se sobrescribe\",\n        \"placeholders_modal_title\": \"Marcadores de posición soportados\",\n        \"update_mod_times\": \"Actualizar marca de tiempo\",\n        \"types\": {\n            \"http\": \"HTTP\",\n            \"email\": \"Correo electrónico\",\n            \"backup\": \"Copia de seguridad\",\n            \"user_quota_reset\": \"Restablecimiento de cuota de usuario\",\n            \"folder_quota_reset\": \"Restablecimiento de cuota de carpeta\",\n            \"transfer_quota_reset\": \"Restablecimiento de cuota de transferencia\",\n            \"data_retention_check\": \"Verificación de retención de datos\",\n            \"filesystem\": \"Sistema de archivos\",\n            \"password_expiration_check\": \"Verificación de expiración de contraseña\",\n            \"user_expiration_check\": \"Verificación de expiración de usuario\",\n            \"user_inactivity_check\": \"Verificación de inactividad de usuario\",\n            \"idp_check\": \"Verificación de cuenta de Proveedor de Identidad\",\n            \"rotate_logs\": \"Rotar archivo de registro\",\n            \"command\": \"Comando\"\n        },\n        \"fs_types\": {\n            \"rename\": \"Renombrar\",\n            \"delete\": \"Eliminar\",\n            \"path_exists\": \"Las rutas existen\",\n            \"compress\": \"Comprimir\",\n            \"copy\": \"Copiar\",\n            \"create_dirs\": \"Crear directorios\"\n        },\n        \"placeholders_modal\": {\n            \"name\": \"Nombre de usuario, nombre de carpeta virtual, nombre de usuario administrador para eventos de proveedor, nombre de dominio para eventos de certificado TLS\",\n            \"event\": \"Nombre del evento, por ejemplo \\\"upload\\\", \\\"download\\\" para eventos del sistema de archivos o \\\"add\\\", \\\"update\\\" para eventos de proveedor\",\n            \"status\": \"Estado para eventos del sistema de archivos. 1 significa sin error, 2 significa que ocurrió un error genérico, 3 significa error de cuota excedida\",\n            \"status_string\": \"Estado como cadena. Posibles valores \\\"OK\\\", \\\"KO\\\"\",\n            \"error_string\": \"Detalles del error. Reemplazado con una cadena vacía si no ocurre error\",\n            \"virtual_path\": \"Ruta vista por los usuarios de SFTPGo, por ejemplo \\\"/adir/afile.txt\\\"\",\n            \"escaped_virtual_path\": \"Ruta codificada en cadena de consulta HTTP, por ejemplo \\\"%2Fadir%2Fafile.txt\\\".\",\n            \"virtual_dir_path\": \"Directorio padre para \\\"VirtualPath\\\", por ejemplo si \\\"VirtualPath\\\" es \\\"/adir/afile.txt\\\", \\\"VirtualDirPath\\\" es \\\"/adir\\\"\",\n            \"fs_path\": \"Ruta completa del sistema de archivos, por ejemplo \\\"/user/homedir/adir/afile.txt\\\" o \\\"C:/data/user/homedir/adir/afile.txt\\\" en Windows\",\n            \"ext\": \"Extensión de archivo, por ejemplo \\\".txt\\\" si el nombre de archivo es \\\"afile.txt\\\"\",\n            \"object_name\": \"Nombre de archivo/directorio, por ejemplo \\\"afile.txt\\\" o nombre de objeto de proveedor\",\n            \"object_basename\": \"Nombre de archivo sin extensión, por ejemplo \\\"afile\\\" si el nombre de archivo es \\\"afile.txt\\\"\",\n            \"object_type\": \"Tipo de objeto para eventos de proveedor: \\\"user\\\", \\\"group\\\", \\\"admin\\\", etc\",\n            \"virtual_target_path\": \"Ruta de destino virtual para operaciones de renombrado y copia\",\n            \"virtual_target_dir_path\": \"Directorio padre para \\\"VirtualTargetPath\\\"\",\n            \"target_name\": \"Nombre de objeto de destino para operaciones de renombrado y copia\",\n            \"fs_target_path\": \"Ruta de destino completa del sistema de archivos para operaciones de renombrado y copia\",\n            \"file_size\": \"Tamaño de archivo (bytes)\",\n            \"elapsed\": \"Tiempo transcurrido como milisegundos para eventos del sistema de archivos\",\n            \"protocol\": \"Protocolo, por ejemplo \\\"SFTP\\\", \\\"FTP\\\"\",\n            \"ip\": \"Dirección IP del cliente\",\n            \"role\": \"Rol de usuario o administrador\",\n            \"timestamp\": \"Marca de tiempo del evento como nanosegundos desde la época\",\n            \"datetime\": \"Marca de tiempo del evento formateada como YYYY-MM-DDTHH:MM:SS.ZZZ\",\n            \"year\": \"Año del evento formateado como cuatro dígitos\",\n            \"month\": \"Mes del evento formateado como dos dígitos\",\n            \"day\": \"Día del evento formateado como dos dígitos\",\n            \"hour\": \"Hora del evento formateada como dos dígitos\",\n            \"minute\": \"Minuto del evento formateado como dos dígitos\",\n            \"email\": \"Para eventos del sistema de archivos, este es el correo electrónico asociado con el usuario realizando la acción. Para los eventos de proveedor, este es el correo electrónico asociado con el usuario o administrador afectado. En blanco en todos los demás casos\",\n            \"object_data\": \"Datos del objeto de proveedor serializados como JSON con campos sensibles eliminados\",\n            \"object_data_string\": \"Datos del objeto de proveedor como cadena escapada JSON con campos sensibles eliminados\",\n            \"retention_reports\": \"Informes de retención de datos como archivos CSV comprimidos en zip. Soportado como adjunto de correo electrónico, ruta de archivo para solicitud HTTP multiparte y como parámetro único para cuerpo de solicitudes HTTP\",\n            \"idp_field\": \"Campos personalizados del Proveedor de Identidad que contienen una cadena\",\n            \"metadata\": \"Metadatos de almacenamiento en la nube para el archivo descargado serializados como JSON\",\n            \"metadata_string\": \"Metadatos de almacenamiento en la nube para el archivo descargado como cadena escapada JSON\",\n            \"uid\": \"ID Único\"\n        }\n    },\n    \"rules\": {\n        \"view_manage\": \"Ver y gestionar reglas para eventos\",\n        \"trigger\": \"Disparador\",\n        \"run_confirm\": \"¿Quieres ejecutar la regla seleccionada?\",\n        \"run_confirm_btn\": \"Sí, ejecutar\",\n        \"run_error_generic\": \"No se puede ejecutar la regla seleccionada\",\n        \"run_ok\": \"Acciones de regla iniciadas\",\n        \"run\": \"Ejecutar\",\n        \"invalid_fs_min_size\": \"Tamaño mínimo inválido\",\n        \"invalid_fs_max_size\": \"Tamaño máximo inválido\",\n        \"action_required\": \"Se requiere al menos una acción\",\n        \"fs_event_required\": \"Se requiere al menos un evento del sistema de archivos\",\n        \"provider_event_required\": \"Se requiere al menos un evento de proveedor\",\n        \"schedule_required\": \"Se requiere al menos un horario\",\n        \"schedule_invalid\": \"Horario inválido\",\n        \"duplicate_actions\": \"Acciones duplicadas detectadas\",\n        \"sync_failure_actions\": \"La ejecución síncrona no es soportada para acciones de fallo\",\n        \"sync_unsupported\": \"La ejecución síncrona solo es soportada para algunos eventos del sistema de archivos e inicios de sesión de Proveedor de Identidad\",\n        \"sync_unsupported_fs_event\": \"La ejecución síncrona solo es soportada para eventos de subida y pre-* del sistema de archivos\",\n        \"only_failure_actions\": \"Se requiere al menos una acción que no sea de fallo\",\n        \"sync_action_required\": \"El evento \\\"{{val}}\\\" requiere al menos una acción síncrona\",\n        \"scheduler_help\": \"Horas: 0-23. Día de la semana: 0-6 (Dom-Sáb). Día del mes: 1-31. Mes: 1-12. Asterisco (*) indica una coincidencia para todos los valores del campo. ej. cada día de la semana, cada día del mes y así sucesivamente\",\n        \"concurrent_run\": \"Permitir ejecución concurrente desde múltiples instancias\",\n        \"protocol_filters\": \"Filtros de protocolo\",\n        \"status_filters\": \"Filtros de estado\",\n        \"object_filters\": \"Filtros de objeto\",\n        \"name_filters\": \"Filtros de nombre\",\n        \"name_filters_help\": \"Filtros de patrón tipo shell para nombres de usuario, nombres de carpeta. Por ejemplo \\\"user*\\\"\\\" coincidirá con nombres que empiecen con \\\"user\\\". Para eventos de proveedor, este filtro se aplica al nombre de usuario del administrador ejecutando el evento\",\n        \"inverse_match\": \"Coincidencia inversa\",\n        \"group_name_filters\": \"Filtros de nombre de grupo\",\n        \"group_name_filters_help\": \"Filtros de patrón tipo shell para nombres de grupo. Por ejemplo \\\"group*\\\"\\\" coincidirá con nombres de grupo que empiecen con \\\"group\\\"\",\n        \"role_name_filters\": \"Filtros de nombre de rol\",\n        \"role_name_filters_help\": \"Filtros de patrón tipo shell para nombres de rol. Por ejemplo \\\"role*\\\"\\\" coincidirá con nombres de rol que empiecen con \\\"role\\\"\",\n        \"path_filters\": \"Filtros de ruta\",\n        \"path_filters_help\": \"Filtros de patrón tipo shell en rutas de eventos del sistema de archivos. Por ejemplo \\\"/adir/*.txt\\\"\\\" coincidirá con rutas en el directorio \\\"/adir\\\" que terminen con \\\".txt\\\". Se soporta doble asterisco, por ejemplo \\\"/**/*.txt\\\" coincidirá con cualquier archivo que termine con \\\".txt\\\". \\\"/midir/**\\\" coincidirá con cualquier entrada en \\\"/midir\\\"\",\n        \"file_size_limits\": \"Límites de tamaño de archivo\",\n        \"file_size_limits_help\": \"0 significa sin límite. Puedes usar sufijo MB/GB\",\n        \"min_size\": \"Tamaño mínimo\",\n        \"max_size\": \"Tamaño máximo\",\n        \"actions_help\": \"Una o más acciones a ejecutar. La opción \\\"Ejecución síncrona\\\" es soportada para eventos de \\\"subida\\\" y requerida para eventos \\\"pre-*\\\" y eventos de inicio de sesión de proveedor de Identidad si la acción comprueba la cuenta\",\n        \"option_failure_action\": \"Acción de fallo\",\n        \"option_stop_on_failure\": \"Parar en fallo\",\n        \"option_execute_sync\": \"Ejecución síncrona\",\n        \"no_filter\": \"Sin filtro significa siempre disparar eventos\",\n        \"action_placeholder\": \"Seleccionar una acción\",\n        \"triggers\": {\n            \"fs_event\": \"Eventos del sistema de archivos\",\n            \"provider_event\": \"Eventos del proveedor\",\n            \"ip_blocked\": \"IP bloqueada\",\n            \"certificate_renewal\": \"Renovación de certificado\",\n            \"on_demand\": \"Bajo demanda\",\n            \"idp_login\": \"Inicios de sesión de Proveedor de Identidad\",\n            \"schedule\": \"Horarios\"\n        },\n        \"idp_logins\": {\n            \"user\": \"Inicio de sesión de usuario\",\n            \"admin\": \"Inicio de sesión de administrador\"\n        }\n    }\n}"
  },
  {
    "path": "static/locales/fr/translation.json",
    "content": "{\n    \"title\": {\n        \"setup\": \"Configuration initiale\",\n        \"login\": \"Connexion\",\n        \"share_login\": \"Partager la connexion\",\n        \"profile\": \"Profil\",\n        \"change_password\": \"Changer le mot de passe\",\n        \"files\": \"Fichiers\",\n        \"shares\": \"Partages\",\n        \"add_share\": \"Ajouter un partage\",\n        \"update_share\": \"Mettre à jour le partage\",\n        \"two_factor_auth\": \"Authentification à deux facteurs\",\n        \"two_factor_auth_short\": \"2FA\",\n        \"edit_file\": \"Modifier le fichier\",\n        \"view_file\": \"Voir le fichier\",\n        \"recovery_password\": \"Récupération de mot de passe\",\n        \"reset_password\": \"Réinitialisation du mot de passe\",\n        \"shared_files\": \"Fichiers partagés\",\n        \"upload_to_share\": \"Téléverser dans le partage\",\n        \"download_shared_file\": \"Télécharger le fichier partagé\",\n        \"share_access_error\": \"Impossible d'accéder au partage\",\n        \"invalid_auth_request\": \"Demande d'authentification invalide\",\n        \"error400\": \"Mauvaise requête\",\n        \"error403\": \"Interdit\",\n        \"error404\": \"Non trouvé\",\n        \"error416\": \"Plage demandée non satisfaisante\",\n        \"error429\": \"Trop de requêtes\",\n        \"error500\": \"Erreur interne du serveur\",\n        \"errorPDF\": \"Impossible d'afficher le fichier PDF\",\n        \"error_editor\": \"Impossible d'ouvrir l'éditeur de fichiers\",\n        \"users\": \"Utilisateurs\",\n        \"groups\": \"Groupes\",\n        \"folders\": \"Dossiers virtuels\",\n        \"connections\": \"Connexions actives\",\n        \"event_manager\": \"Gestionnaire d'événements\",\n        \"event_rules\": \"Règles\",\n        \"event_actions\": \"Actions\",\n        \"ip_manager\": \"Gestionnaire IP\",\n        \"ip_lists\": \"Listes IP\",\n        \"defender\": \"Liste de blocage automatique\",\n        \"admins\": \"Administrateurs\",\n        \"roles\": \"Rôles\",\n        \"server_manager\": \"Gestionnaire de serveur\",\n        \"configs\": \"Paramètres\",\n        \"logs\": \"Logs\",\n        \"maintenance\": \"Maintenance\",\n        \"status\": \"Statut\",\n        \"add_user\": \"Ajouter un utilisateur\",\n        \"update_user\": \"Mettre à jour l'utilisateur\",\n        \"template_user\": \"Modèle d'utilisateur\",\n        \"template_admin\": \"Modèle d'administrateur\",\n        \"add_group\": \"Ajouter un groupe\",\n        \"update_group\": \"Mettre à jour le groupe\",\n        \"add_folder\": \"Ajouter un dossier virtuel\",\n        \"update_folder\": \"Mettre à jour le dossier virtuel\",\n        \"template_folder\": \"Modèle de dossier virtuel\",\n        \"oauth2_error\": \"Impossible de compléter le flux OAuth2\",\n        \"oauth2_success\": \"Flux OAuth2 complété\",\n        \"add_role\": \"Ajouter un rôle\",\n        \"update_role\": \"Mettre à jour le rôle\",\n        \"add_admin\": \"Ajouter un administrateur\",\n        \"update_admin\": \"Mettre à jour l'administrateur\",\n        \"add_ip_list\": \"Ajouter une entrée à la liste IP\",\n        \"update_ip_list\": \"Mettre à jour l'entrée de la liste IP\",\n        \"add_action\": \"Ajouter une action\",\n        \"update_action\": \"Mettre à jour l'action\",\n        \"add_rule\": \"Ajouter une règle\",\n        \"update_rule\": \"Mettre à jour la règle\"\n    },\n    \"setup\": {\n        \"desc\": \"Pour commencer à utiliser SFTPGo vous devez créer un utilisateur administrateur\",\n        \"submit\": \"Créer un administrateur et se connecter\",\n        \"install_code_mismatch\": \"Le code d'installation ne correspond pas\",\n        \"help_text\": \"Support commercial\"\n    },\n    \"login\": {\n        \"username\": \"Nom d'utilisateur\",\n        \"password\": \"Mot de passe\",\n        \"your_username\": \"Votre nom d'utilisateur\",\n        \"forgot_password\": \"Mot de passe oublié ?\",\n        \"forgot_password_msg\": \"Entrez votre nom d'utilisateur ci-dessous, vous recevrez un code de réinitialisation du mot de passe par email.\",\n        \"send_reset_code\": \"Envoyer le code de réinitialisation\",\n        \"signin\": \"Se connecter\",\n        \"signin_openid\": \"Se connecter avec OpenID\",\n        \"signout\": \"Se déconnecter\",\n        \"auth_code\": \"Code d'authentification\",\n        \"two_factor_help\": \"Ouvrez l'application d'authentification à deux facteurs sur votre appareil pour afficher votre code d'authentification et vérifier votre identité.\",\n        \"two_factor_msg\": \"Entrez un code de récupération à deux facteurs\",\n        \"recovery_code\": \"Code de récupération\",\n        \"recovery_code_msg\": \"Vous pouvez entrer l'un de vos codes de récupération en cas de perte d'accès à votre appareil mobile.\",\n        \"reset_password\": \"Réinitialiser le mot de passe\",\n        \"reset_pwd_msg\": \"Vérifiez votre email pour le code de confirmation\",\n        \"reset_submit\": \"Mettre à jour le mot de passe et se connecter\",\n        \"confirm_code\": \"Code de confirmation\",\n        \"reset_pwd_forbidden\": \"Vous n'êtes pas autorisé à réinitialiser votre mot de passe\",\n        \"reset_pwd_no_email\": \"Votre compte n'a pas d'adresse email, il est impossible de réinitialiser votre mot de passe en envoyant un code de vérification par email\",\n        \"reset_pwd_send_email_err\": \"Impossible d'envoyer le code de confirmation par email\",\n        \"reset_pwd_err_generic\": \"Erreur inattendue lors de la réinitialisation du mot de passe\",\n        \"reset_ok_login_error\": \"La réinitialisation du mot de passe a été complétée avec succès, mais une erreur inattendue s'est produite lors de la connexion\",\n        \"ip_not_allowed\": \"La connexion n'est pas autorisée depuis cette adresse IP\",\n        \"two_factor_required\": \"Configurez l'authentification à deux facteurs, elle est requise pour les protocoles suivants : {{val}}\",\n        \"two_factor_required_generic\": \"Configurez l'authentification à deux facteurs, elle est obligatoire pour votre compte\",\n        \"link\": \"Basculer vers {{link}}\"\n    },\n    \"theme\": {\n        \"light\": \"Clair\",\n        \"dark\": \"Sombre\",\n        \"system\": \"Automatique\"\n    },\n    \"general\": {\n        \"invalid_auth_request\": \"La demande d'authentification ne répond pas aux exigences de sécurité\",\n        \"invalid_input\": \"Entrée invalide. Les barres obliques (/), les deux-points (:), les caractères de contrôle et les noms système réservés ne sont pas autorisés\",\n        \"error400\": \"La demande reçue n'est pas valide\",\n        \"error403\": \"Vous n'avez pas les permissions requises\",\n        \"error404\": \"La ressource demandée n'existe pas\",\n        \"error416\": \"Le fragment de fichier demandé ne peut pas être retourné\",\n        \"error429\": \"Limite de taux dépassée\",\n        \"error500\": \"Le serveur ne peut pas répondre à votre demande\",\n        \"errorPDF\": \"Ce fichier ne semble pas être un PDF\",\n        \"error_edit_dir\": \"Impossible de modifier un répertoire\",\n        \"error_edit_size\": \"La taille du fichier dépasse la taille maximale autorisée\",\n        \"invalid_form\": \"Formulaire invalide\",\n        \"invalid_credentials\": \"Identifiants invalides, veuillez réessayer\",\n        \"invalid_csrf\": \"Le jeton de formulaire n'est pas valide\",\n        \"invalid_token\": \"Permissions invalides\",\n        \"confirm_logout\": \"Êtes-vous sûr de vouloir vous déconnecter ?\",\n        \"wait\": \"Veuillez patienter...\",\n        \"ok\": \"Ok\",\n        \"failed\": \"Échoué\",\n        \"cancel\": \"Non, retour\",\n        \"submit\": \"Enregistrer\",\n        \"back\": \"Retour\",\n        \"add\": \"Ajouter\",\n        \"enable\": \"Activer\",\n        \"disable\": \"Désactiver\",\n        \"disable_confirm_btn\": \"Oui, désactiver\",\n        \"close\": \"Fermer\",\n        \"search\": \"Rechercher\",\n        \"configuration\": \"Paramètres\",\n        \"config_saved\": \"Configuration enregistrée\",\n        \"rename\": \"Renommer\",\n        \"confirm\": \"Oui, procéder\",\n        \"edit\": \"Modifier\",\n        \"delete\": \"Supprimer\",\n        \"delete_confirm_btn\": \"Oui, supprimer\",\n        \"delete_confirm\": \"Voulez-vous supprimer \\\"{{- name}}\\\" ? Cette action est irréversible\",\n        \"delete_confirm_generic\": \"Voulez-vous supprimer l'élément sélectionné ? Cette action est irréversible\",\n        \"delete_multi_confirm\": \"Voulez-vous supprimer les éléments sélectionnés ? Cette action est irréversible\",\n        \"delete_error_generic\": \"Impossible de supprimer l'élément sélectionné\",\n        \"delete_error_403\": \"$t(general.delete_error_generic).$t(general.error403)\",\n        \"delete_error_404\": \"$t(general.delete_error_generic). $t(general.error404)\",\n        \"loading\": \"Chargement...\",\n        \"name\": \"Nom\",\n        \"size\": \"Taille\",\n        \"last_modified\": \"Dernière modification\",\n        \"info\": \"Information\",\n        \"datetime\": \"{{- val, datetime}}\",\n        \"selected_items_one\": \"{{count}} élément sélectionné\",\n        \"selected_items_other\": \"{{count}} éléments sélectionnés\",\n        \"selected_with_placeholder\": \"%d éléments sélectionnés\",\n        \"name_required\": \"Le nom est requis\",\n        \"name_different\": \"Le nouveau nom doit être différent du nom actuel\",\n        \"html5_media_not_supported\": \"Votre navigateur ne supporte pas l'audio/vidéo HTML5.\",\n        \"choose_target_folder\": \"Choisir le dossier cible\",\n        \"source_name\": \"Nom de la source\",\n        \"target_folder\": \"Dossier cible\",\n        \"dest_name\": \"Nom de la destination\",\n        \"my_profile\": \"Mon profil\",\n        \"description\": \"Description\",\n        \"email\": \"Courriel\",\n        \"api_key_auth\": \"Authentification par clé API\",\n        \"api_key_auth_help\": \"Permet de vous représenter, dans l'API REST, en utilisant une clé API\",\n        \"pub_keys\": \"Clés publiques\",\n        \"pub_key_placeholder\": \"Collez une clé publique ici\",\n        \"pub_keys_help\": \"Les clés publiques peuvent être utilisées pour l'authentification SFTP\",\n        \"verify\": \"Vérifier\",\n        \"problems\": \"Vous avez des problèmes ?\",\n        \"allowed_ip_mask\": \"IP/Mask autorisé\",\n        \"denied_ip_mask\": \"IP/Mask refusé\",\n        \"ip_mask_help\": \"IP/Mask séparés par des virgules au format CIDR, par exemple \\\"192.168.1.0/24,10.8.0.100/32\\\"\",\n        \"allowed_ip_mask_invalid\": \"IP/Mask autorisé invalide\",\n        \"username_required\": \"Le nom d'utilisateur est requis\",\n        \"password_required\": \"Le mot de passe est requis\",\n        \"foldername_required\": \"Le nom du dossier est requis\",\n        \"err_user\": \"Impossible de valider votre utilisateur\",\n        \"err_protocol_forbidden\": \"Le protocole HTTP n'est pas autorisé pour votre utilisateur\",\n        \"pwd_login_forbidden\": \"La méthode de connexion par mot de passe n'est pas autorisée pour votre utilisateur\",\n        \"ip_forbidden\": \"Connexion non autorisée depuis cette adresse IP\",\n        \"email_invalid\": \"L'adresse email est invalide\",\n        \"err_password_complexity\": \"Le mot de passe fourni ne répond pas aux exigences de complexité\",\n        \"no_oidc_feature\": \"Cette fonctionnalité n'est pas disponible si vous êtes connecté avec OpenID\",\n        \"connection_forbidden\": \"Vous n'êtes pas autorisé à vous connecter\",\n        \"no_permissions\": \"Vous n'êtes pas autorisé à modifier quoi que ce soit\",\n        \"path_invalid\": \"Chemin invalide\",\n        \"err_quota_read\": \"Lecture refusée en raison de la limite de quota\",\n        \"profile_updated\": \"Votre profil a été mis à jour avec succès\",\n        \"share_ok\": \"Accès au partage réussi, vous pouvez maintenant utiliser votre lien\",\n        \"qr_code\": \"Code QR\",\n        \"copy_link\": \"Copier le lien\",\n        \"copied\": \"Copié\",\n        \"active\": \"Actif\",\n        \"inactive\": \"Inactif\",\n        \"colvis\": \"Visibilité des colonnes\",\n        \"actions\": \"Actions\",\n        \"template\": \"Utiliser comme modèle\",\n        \"quota_scan\": \"Analyse de quota\",\n        \"quota_scan_started\": \"Analyse de quota commencée. Cela peut prendre un certain temps en fonction du nombre de fichiers à vérifier\",\n        \"quota_scan_conflit\": \"Une autre analyse est déjà en cours\",\n        \"quota_scan_error\": \"Impossible de démarrer l'analyse de quota\",\n        \"role\": \"Rôle\",\n        \"role_placeholder\": \"Sélectionner un rôle\",\n        \"group_placeholder\": \"Sélectionner un groupe\",\n        \"folder_placeholder\": \"Sélectionner un dossier\",\n        \"blank_default_help\": \"Laisser vide pour la valeur par défaut\",\n        \"skip_tls_verify\": \"Ignorer la vérification TLS. Ceci doit être utilisé uniquement pour les tests\",\n        \"advanced_settings\": \"Paramètres avancés\",\n        \"default\": \"Par défaut\",\n        \"private_key\": \"Clé privée\",\n        \"acls\": \"ACLs\",\n        \"quota_limits\": \"Limites de quota disque et de bande passante\",\n        \"expiration\": \"Expiration\",\n        \"expiration_help\": \"Choisissez une date d'expiration\",\n        \"additional_info\": \"Informations supplémentaires\",\n        \"permissions\": \"Permissions\",\n        \"visible\": \"Visible\",\n        \"hidden\": \"Caché\",\n        \"allowed\": \"Autorisé\",\n        \"denied\": \"Refusé\",\n        \"zero_no_limit_help\": \"0 signifie sans limite\",\n        \"global_settings\": \"Paramètres globaux\",\n        \"mandatory_encryption\": \"Cryptage obligatoire\",\n        \"name_invalid\": \"Le nom spécifié n'est pas valide, les caractères suivants sont autorisés : a-zA-Z0-9-_.~\",\n        \"associations\": \"Associations\",\n        \"template_placeholders\": \"Les espaces réservés suivants sont pris en charge\",\n        \"duplicated_username\": \"Le nom d'utilisateur spécifié existe déjà\",\n        \"duplicated_name\": \"Le nom spécifié existe déjà\",\n        \"permissions_required\": \"Les permissions sont requises\",\n        \"configs_saved\": \"Les configurations ont été mises à jour avec succès\",\n        \"protocol\": \"Protocole\",\n        \"refresh\": \"Actualiser\",\n        \"members\": \"Membres\",\n        \"members_summary\": \"Utilisateurs : {{users}}. Admins : {{admins}}\",\n        \"status\": \"Statut\",\n        \"last_login\": \"Dernière connexion\",\n        \"previous\": \"Précédent\",\n        \"next\": \"Suivant\",\n        \"type\": \"Type\",\n        \"issuer\": \"Autorité émettrice\",\n        \"data_provider\": \"Base de données\",\n        \"driver\": \"Pilote\",\n        \"mode\": \"Mode\",\n        \"port\": \"Port\",\n        \"domain\": \"Domaine\",\n        \"test\": \"Test\",\n        \"get\": \"Obtenir\",\n        \"export\": \"Exporter\",\n        \"value\": \"Valeur\",\n        \"method\": \"Méthode\",\n        \"timeout\": \"Délai d'attente\",\n        \"env_vars\": \"Variables d'environnement\",\n        \"hours\": \"Heures\",\n        \"paths\": \"Chemins\",\n        \"hour\": \"Heure\",\n        \"day_of_week\": \"Jour de la semaine\",\n        \"day_of_month\": \"Jour du mois\",\n        \"month\": \"Mois\",\n        \"options\": \"Options\",\n        \"expired\": \"Expiré\",\n        \"unsupported\": \"Fonctionnalité non supportée\",\n        \"start\": \"Début (HH:MM)\",\n        \"end\": \"Fin (HH:MM)\",\n        \"monday\": \"Lundi\",\n        \"tuesday\": \"Mardi\",\n        \"wednesday\": \"Mercredi\",\n        \"thursday\": \"Jeudi\",\n        \"friday\": \"Vendredi\",\n        \"saturday\": \"Samedi\",\n        \"sunday\": \"Dimanche\",\n        \"expired_session\": \"Votre session a expiré. Veuillez vous reconnecter\"\n    },\n    \"fs\": {\n        \"view_file\": \"Voir le fichier \\\"{{- path}}\\\"\",\n        \"edit_file\": \"Éditer le fichier \\\"{{- path}}\\\"\",\n        \"new_folder\": \"Nouveau dossier\",\n        \"select_across_pages\": \"Sélectionner à travers les pages\",\n        \"download\": \"Télécharger\",\n        \"download_ready\": \"Votre téléchargement est prêt\",\n        \"upload_queue_not_ready\": \"La file d'attente de téléchargement est en cours de chargement, veuillez réessayer dans quelques instants\",\n        \"upload_no_files\": \"Aucun fichier sélectionné. Veuillez choisir ou glisser des fichiers à téléverser\",\n        \"move_copy\": \"Déplacer ou copier\",\n        \"share\": \"Partager\",\n        \"home\": \"Accueil\",\n        \"create_folder_msg\": \"Nom du dossier\",\n        \"no_more_subfolders\": \"Pas d'autres sous-dossiers ici\",\n        \"no_files_folders\": \"Aucun fichier ou dossier\",\n        \"invalid_name\": \"Les noms de fichiers ou dossiers ne peuvent pas contenir \\\"/\\\"\",\n        \"folder_name_required\": \"Le nom du dossier est requis\",\n        \"deleting\": \"Supprimer {{idx}}/{{total}}\",\n        \"copying\": \"Copier {{idx}}/{{total}}\",\n        \"moving\": \"Déplacer {{idx}}/{{total}}\",\n        \"uploading\": \"Télécharger {{idx}}/{{total}}\",\n        \"err_403\": \"Permission refusée\",\n        \"err_429\": \"Trop de requêtes simultanées\",\n        \"err_generic\": \"Impossible d'accéder à la ressource demandée\",\n        \"err_validation\": \"Configuration du système de fichiers invalide\",\n        \"err_exists\": \"La destination existe déjà\",\n        \"dir_list\": {\n            \"err_generic\": \"Échec de l'obtention de la liste du répertoire\",\n            \"err_403\": \"$t(fs.dir_list.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.dir_list.err_generic). $t(fs.err_429)\",\n            \"err_user\": \"$t(fs.dir_list.err_generic). $t(general.err_user)\"\n        },\n        \"create_dir\": {\n            \"err_generic\": \"Erreur lors de la création du nouveau dossier\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"save\": {\n            \"err_generic\": \"Erreur lors de l'enregistrement du fichier\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"delete\": {\n            \"err_generic\": \"Impossible de supprimer \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.delete.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete.err_generic). $t(fs.err_429)\"\n        },\n        \"delete_multi\": {\n            \"err_generic_partial\": \"Tous les éléments sélectionnés n'ont pas été supprimés, veuillez recharger la page\",\n            \"err_generic\": \"Impossible de supprimer les éléments sélectionnés\",\n            \"err_403_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)\",\n            \"err_429_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)\",\n            \"err_403\": \"$t(fs.delete_multi.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete_multi.err_generic). $t(fs.err_429)\"\n        },\n        \"copy\": {\n            \"msg\": \"Copier\",\n            \"err_generic\": \"Erreur lors de la copie des fichiers/répertoires\",\n            \"err_403\": \"$t(fs.copy.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.copy.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.copy.err_generic). $t(fs.err_exists)\"\n        },\n        \"move\": {\n            \"msg\": \"Déplacer\",\n            \"err_generic\": \"Erreur lors du déplacement des fichiers/répertoires\",\n            \"err_403\": \"$t(fs.move.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.move.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.move.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"Non pris en charge : si vous souhaitez déplacer un répertoire, assurez-vous qu'il soit vide\"\n        },\n        \"rename\": {\n            \"title\": \"Renommer \\\"{{- name}}\\\"\",\n            \"new_name\": \"Nouveau nom\",\n            \"err_generic\": \"Impossible de renommer \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.rename.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.rename.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.rename.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"Non pris en charge : si vous souhaitez renommer un répertoire, assurez-vous qu'il soit vide\"\n        },\n        \"upload\": {\n            \"text\": \"Téléverser des fichiers\",\n            \"success\": \"Fichiers téléversés avec succès\",\n            \"message\": \"Déposez les fichiers ici ou cliquez pour téléverser.\",\n            \"message_empty\": \"Ce répertoire est vide. $t(fs.upload.message)\",\n            \"err_generic\": \"Erreur lors du téléversement des fichiers\",\n            \"err_403\": \"$t(fs.upload.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.upload.err_generic). $t(fs.err_429)\",\n            \"err_dir_overwrite\": \"$t(fs.upload.err_generic). Il y a des répertoires avec le même nom que les fichiers : {{- val}}\",\n            \"overwrite_text\": \"Conflit détecté. Voulez-vous écraser les fichiers/répertoires suivants ?\"\n        },\n        \"quota_usage\": {\n            \"title\": \"Utilisation du quota\",\n            \"disk\": \"Quota disque\",\n            \"size\": \"Taille : {{- val}}\",\n            \"size_percentage\": \"Taille : {{- val}} ({{percentage}}%)\",\n            \"files\": \"Fichiers : {{- val}}\",\n            \"files_percentage\": \"Fichiers : {{- val}} ({{percentage}}%)\",\n            \"transfer\": \"Quota de transfert\",\n            \"total\": \"Total : {{- val}}\",\n            \"total_percentage\": \"Total : {{- val}} ({{percentage}}%)\",\n            \"uploads\": \"Téléversements : {{- val}}\",\n            \"uploads_percentage\": \"Téléversements : {{- val}} ({{percentage}}%)\",\n            \"downloads\": \"Téléchargements : {{- val}}\",\n            \"downloads_percentage\": \"Téléchargements : {{- val}} ({{percentage}}%)\"\n        }\n    },\n    \"datatable\": {\n        \"info\": \"Affichage de _START_ à _END_ de _TOTAL_ enregistrements\",\n        \"info_empty\": \"Aucun enregistrement à afficher\",\n        \"info_filtered\": \"(filtré de _MAX_ enregistrements au total)\",\n        \"processing\": \"Traitement...\",\n        \"no_records\": \"Aucun enregistrement trouvé\"\n    },\n    \"editor\": {\n        \"keybinding\": \"Raccourcis clavier de l'éditeur\",\n        \"search\": \"Ouvrir le panneau de recherche\",\n        \"goto\": \"Aller à la ligne\",\n        \"indent_more\": \"Augmenter le retrait\",\n        \"indent_less\": \"Diminuer le retrait\"\n    },\n    \"2fa\": {\n        \"title\": \"Authentification à deux facteurs utilisant des applications Authenticator\",\n        \"msg_enabled\": \"L'authentification à deux facteurs est activée\",\n        \"msg_disabled\": \"Protégez votre compte\",\n        \"msg_info\": \"L'authentification à deux facteurs ajoute une couche supplémentaire de sécurité à votre compte. Pour vous connecter, vous devrez fournir un code d'authentification supplémentaire.\",\n        \"require\": \"L'authentification à deux facteurs est requise pour ce compte\",\n        \"require_for\": \"Exiger 2FA pour\",\n        \"generate\": \"Générer une nouvelle clé secrète\",\n        \"recovery_codes\": \"Codes de récupération\",\n        \"new_recovery_codes\": \"Nouveaux codes de récupération\",\n        \"recovery_codes_msg1\": \"Les codes de récupération sont un ensemble de codes à usage unique qui peuvent être utilisés à la place du code d'authentification pour se connecter à l'interface Web. Vous pouvez les utiliser si vous perdez l'accès à votre téléphone pour vous connecter à votre compte et désactiver ou régénérer la configuration d'authentification à deux facteurs.\",\n        \"recovery_codes_msg2\": \"Pour sécuriser votre compte, ne partagez ni ne distribuez vos codes de récupération. Nous vous recommandons de les enregistrer avec un gestionnaire de mots de passe sécurisé.\",\n        \"recovery_codes_msg3\": \"Si vous générez de nouveaux codes de récupération, vous invalidez automatiquement les anciens.\",\n        \"info_title\": \"En savoir plus sur l'authentification à deux facteurs\",\n        \"info1\": \"Le protocole SSH (SFTP/SCP/commandes SSH) demandera le code d'accès si le client utilise une authentification interactive clavier.\",\n        \"info2\": \"Le protocole HTTP inclut l'interface Web et les API REST. L'interface Web demandera le code d'accès via une page spécifique. Pour l'API REST, vous devez ajouter le code d'accès en utilisant un en-tête HTTP.\",\n        \"info3\": \"FTP n'a pas de moyen standard pour prendre en charge l'authentification à deux facteurs. Si vous activez le support FTP, vous devez ajouter le code TOTP après le mot de passe. Par exemple, si votre mot de passe est \\\"password\\\" et votre code d'accès unique est \\\"123456\\\", vous devez utiliser \\\"password123456\\\" comme mot de passe.\",\n        \"info4\": \"WebDAV n'est pas pris en charge car chaque requête doit être authentifiée et un code d'accès ne peut pas être réutilisé.\",\n        \"setup_title\": \"Configurer l'authentification à deux facteurs\",\n        \"setup_msg\": \"Utilisez votre application Authenticator préférée (par exemple Microsoft Authenticator, Google Authenticator, Authy, 1Password, etc.) pour scanner le code QR. Elle générera un code d'authentification que vous devrez entrer ci-dessous.\",\n        \"setup_help\": \"Si vous rencontrez des difficultés avec le code QR, sélectionnez l'entrée manuelle sur votre application et entrez le code :\",\n        \"disable_msg\": \"Désactiver 2FA\",\n        \"disable_confirm\": \"Voulez-vous désactiver l'authentification à deux facteurs pour l'utilisateur sélectionné ? Cette action est généralement nécessaire uniquement si l'utilisateur a perdu l'accès au second facteur d'authentification\",\n        \"disable_question\": \"Voulez-vous désactiver l'authentification à deux facteurs ?\",\n        \"generate_question\": \"Voulez-vous générer une nouvelle clé secrète et invalider l'ancienne ? Toute application Authenticator enregistrée cessera de fonctionner\",\n        \"disabled\": \"L'authentification à deux facteurs est désactivée\",\n        \"recovery_codes_gen_err\": \"Échec de la génération des nouveaux codes de récupération\",\n        \"recovery_codes_get_err\": \"Impossible d'obtenir les codes de récupération\",\n        \"auth_code_invalid\": \"Échec de la validation du code d'authentification fourni\",\n        \"auth_secret_gen_err\": \"Échec de la génération du secret d'authentification\",\n        \"save_err\": \"Échec de l'enregistrement de la configuration de l'authentification à deux facteurs\",\n        \"auth_code_required\": \"Le code d'authentification est requis\",\n        \"no_protocol\": \"Veuillez sélectionner au moins un protocole\",\n        \"required_protocols\": \"La politique de sécurité configurée pour votre compte nécessite une authentification à deux facteurs pour les protocoles suivants: {{val}}\",\n        \"recovery_codes_generate\": \"Générer de nouveaux codes de récupération\",\n        \"recovery_codes_view\": \"Voir les codes de récupération\"\n    },\n    \"share\": {\n        \"scope\": \"Portée\",\n        \"scope_read\": \"Lecture\",\n        \"scope_write\": \"Écriture\",\n        \"scope_read_write\": \"Lecture/Écriture\",\n        \"scope_help\": \"Pour les portées \\\"Écriture\\\" et \\\"Lecture/Écriture\\\", vous devez définir un seul chemin et il doit s'agir d'un répertoire\",\n        \"path_help\": \"Chemin du fichier ou du répertoire, par exemple /dir ou /dir/fichier.txt\",\n        \"password_help\": \"Si défini, le partage sera protégé par mot de passe\",\n        \"max_tokens\": \"Nombre maximum de jetons\",\n        \"max_tokens_help\": \"Nombre maximum de fois que ce partage peut être accédé. 0 signifie sans limite\",\n        \"view_manage\": \"Voir et gérer les partages\",\n        \"no_share\": \"Pas de partage\",\n        \"password_protected\": \"Protégé par mot de passe.\",\n        \"expiration_date\": \"Expiration : {{- val, datetime}}. \",\n        \"last_use\": \"Dernière utilisation : {{- val, datetime}}. \",\n        \"usage\": \"Utilisation : {{used}}/{{total}}. \",\n        \"used_tokens\": \"Jeton utilisé : {{used}}. \",\n        \"scope_invalid\": \"Portée invalide\",\n        \"max_tokens_invalid\": \"Nombre maximum de jetons invalide\",\n        \"expiration_invalid\": \"Expiration invalide\",\n        \"err_no_password\": \"Vous n'êtes pas autorisé à partager des fichiers/dossiers sans mot de passe\",\n        \"expiration_out_of_range\": \"Définissez une date d'expiration et assurez-vous qu'elle soit inférieure ou égale à {{- val, datetime}}\",\n        \"generic\": \"Erreur inattendue lors de l'enregistrement du partage\",\n        \"path_required\": \"Au moins un chemin est requis\",\n        \"path_write_scope\": \"La portée d'écriture nécessite exactement un chemin\",\n        \"nested_paths\": \"Les chemins ne peuvent pas être imbriqués\",\n        \"expiration_past\": \"La date d'expiration doit être dans le futur\",\n        \"usage_exceed\": \"Utilisation maximale du partage dépassée\",\n        \"expired\": \"Le partage a expiré\",\n        \"browsable_multiple_paths\": \"Un partage avec plusieurs chemins n'est pas navigable\",\n        \"browsable_non_dir\": \"Le partage n'est pas un répertoire donc il n'est pas navigable\",\n        \"go\": \"Aller au partage\",\n        \"access_links_title\": \"Liens d'accès au partage\",\n        \"link_single_title\": \"Fichier zip unique\",\n        \"link_single_desc\": \"Vous pouvez télécharger le contenu partagé en un seul fichier zip\",\n        \"link_dir_title\": \"Répertoire unique\",\n        \"link_dir_desc\": \"Si le partage consiste en un répertoire unique, vous pouvez parcourir et télécharger des fichiers\",\n        \"link_uncompressed_title\": \"Fichier non compressé\",\n        \"link_uncompressed_desc\": \"Si le partage consiste en un fichier unique, il peut également être téléchargé non compressé\",\n        \"upload_desc\": \"Vous pouvez téléverser un ou plusieurs fichiers dans le répertoire partagé\",\n        \"expired_desc\": \"Ce partage n'est plus accessible car il a expiré\",\n        \"invalid_path\": \"Le répertoire partagé est manquant ou inaccessible\"\n    },\n    \"select2\": {\n        \"no_results\": \"Aucun résultat trouvé\",\n        \"searching\": \"Recherche en cours...\",\n        \"removeall\": \"Supprimer tous les éléments\",\n        \"remove\": \"Supprimer l'élément\"\n    },\n    \"change_pwd\": {\n        \"info\": \"Entrez votre mot de passe actuel, pour des raisons de sécurité, puis votre nouveau mot de passe deux fois, pour vérifier que vous l'avez bien écrit. Vous serez déconnecté après avoir changé votre mot de passe\",\n        \"current\": \"Mot de passe actuel\",\n        \"new\": \"Nouveau mot de passe\",\n        \"confirm\": \"Confirmer le mot de passe\",\n        \"save\": \"Changer mon mot de passe\",\n        \"required_fields\": \"Veuillez fournir le mot de passe actuel et le nouveau deux fois\",\n        \"no_match\": \"Les deux champs de mot de passe ne correspondent pas\",\n        \"no_different\": \"Le nouveau mot de passe doit être différent de l'actuel\",\n        \"current_no_match\": \"Le mot de passe actuel ne correspond pas\",\n        \"generic\": \"Erreur inattendue lors du changement de mot de passe\",\n        \"required\": \"Le changement de mot de passe est requis. Définissez un nouveau mot de passe pour continuer à utiliser votre compte\"\n    },\n    \"user\": {\n        \"view_manage\": \"Voir et gérer les utilisateurs\",\n        \"username_reserved\": \"Le nom d'utilisateur spécifié est réservé\",\n        \"username_invalid\": \"Le nom indiqué n’est pas valide. Les caractères suivants sont autorisés : a-z A-Z 0-9 - _ . ~\",\n        \"home_required\": \"Le répertoire de base est obligatoire\",\n        \"home_invalid\": \"Le répertoire de base doit être un chemin absolu\",\n        \"pub_key_invalid\": \"Clé publique invalide\",\n        \"priv_key_invalid\": \"Clé privée invalide\",\n        \"key_invalid_size\": \"Clé publique RSA invalide : la taille minimale prise en charge est de 2048\",\n        \"key_insecure\": \"Format de clé publique non sécurisé non autorisé\",\n        \"err_primary_group\": \"Un seul groupe principal est autorisé\",\n        \"err_duplicate_group\": \"Groupes en double détectés\",\n        \"no_permissions\": \"Les permissions des répertoires sont obligatoires\",\n        \"no_root_permissions\": \"Les permissions du répertoire de base sont requises\",\n        \"err_permissions_generic\": \"Permissions invalides : assurez-vous d'utiliser des chemins absolus valides\",\n        \"2fa_invalid\": \"Configuration invalide pour l'authentification à deux facteurs\",\n        \"recovery_codes_invalid\": \"Codes de récupération invalides\",\n        \"folder_path_required\": \"Le chemin de montage du dossier virtuel est requis\",\n        \"folder_duplicated\": \"Dossiers virtuels en double détectés\",\n        \"folder_overlapped\": \"Chevauchement des dossiers virtuels détecté\",\n        \"folder_quota_size_invalid\": \"Le quota de taille des dossiers virtuels doit être supérieur ou égal à -1\",\n        \"folder_quota_file_invalid\": \"Le quota de fichiers des dossiers virtuels doit être supérieur ou égal à -1\",\n        \"folder_quota_invalid\": \"Les quotas de taille et de nombre de fichiers des dossiers virtuels doivent être tous deux à -1 ou supérieurs ou égaux à 0\",\n        \"ip_filters_invalid\": \"Filtres IP invalides, assurez-vous qu'ils respectent la notation CIDR, par exemple 192.168.1.0/24\",\n        \"src_bw_limits_invalid\": \"Limites de vitesse de bande passante par source invalides\",\n        \"share_expiration_invalid\": \"L'expiration pour les partages doit être supérieure ou égale à la valeur par défaut définie\",\n        \"file_pattern_path_invalid\": \"Les chemins de filtre de nom de fichier doivent être absolus\",\n        \"file_pattern_duplicated\": \"Filtres de nom de fichier en double détectés\",\n        \"file_pattern_invalid\": \"Filtres de nom de fichier invalides\",\n        \"disable_active_2fa\": \"L'authentification à deux facteurs ne peut pas être désactivée pour un utilisateur avec une configuration active\",\n        \"pwd_change_conflict\": \"Il n'est pas possible de demander un changement de mot de passe et en même temps d'empêcher le changement de mot de passe\",\n        \"two_factor_conflict\": \"Vous ne pouvez pas exiger l'authentification à deux facteurs et en même temps empêcher sa configuration\",\n        \"role_help\": \"Les utilisateurs avec un rôle peuvent être gérés par les administrateurs globaux et les administrateurs avec le même rôle\",\n        \"require_pwd_change\": \"Exiger le changement de mot de passe\",\n        \"require_pwd_change_help\": \"L'utilisateur devra changer le mot de passe depuis le WebClient pour activer le compte\",\n        \"groups_help\": \"L'appartenance à des groupes impose les paramètres des groupes à l'exception des groupes d'appartenance uniquement\",\n        \"primary_group\": \"Groupe principal\",\n        \"secondary_groups\": \"Groupes secondaires\",\n        \"membership_groups\": \"Groupes d'appartenance\",\n        \"template_help\": \"Pour chaque utilisateur, définissez le nom d'utilisateur et au moins l'un des mots de passe et la clé publique\",\n        \"virtual_folders_help\": \"Quota taille/fichiers -1 signifie inclus dans le quota utilisateur, 0 illimité. Ne définissez pas -1 pour les dossiers partagés. Vous pouvez utiliser les suffixes MB/GB/TB. Sans suffixe, nous supposons des octets\",\n        \"disconnect\": \"Déconnecter l'utilisateur après la mise à jour\",\n        \"disconnect_help\": \"De cette manière, vous forcez l'utilisateur à se reconnecter, s'il est connecté, et donc à utiliser la nouvelle configuration\",\n        \"invalid_quota_size\": \"Taille de quota invalide\",\n        \"expires_in\": \"Expire dans\",\n        \"expires_in_help\": \"Expiration du compte en tant que nombre de jours à partir de la création. 0 signifie aucune expiration\",\n        \"additional_emails\": \"E-mails supplémentaires\",\n        \"tls_certs\": \"Certificats TLS\",\n        \"tls_certs_help\": \"Les certificats TLS peuvent être utilisés pour l'authentification FTP et/ou WebDAV\",\n        \"tls_cert_help\": \"Collez un certificat TLS encodé en PEM ici\",\n        \"tls_cert_invalid\": \"Certificat TLS invalide\",\n        \"template_title\": \"Créer un ou plusieurs nouveaux utilisateurs à partir de ce modèle\",\n        \"template_username_placeholder\": \"remplacé par le nom d'utilisateur spécifié\",\n        \"template_password_placeholder\": \"remplacé par le mot de passe spécifié\",\n        \"template_help1\": \"Les espaces réservés seront remplacés dans les chemins et les identifiants du backend de stockage configuré.\",\n        \"template_help2\": \"Les utilisateurs générés peuvent être enregistrés ou exportés. Les utilisateurs exportés peuvent être importés depuis la section \\\"Maintenance\\\" de cette instance SFTPGo ou une autre.\",\n        \"template_no_user\": \"Aucun utilisateur valide défini, impossible de compléter l'action demandée\",\n        \"time_of_day_invalid\": \"Heure du jour invalide. Format pris en charge HH:MM\",\n        \"time_of_day_conflict\": \"Heure du jour invalide. L'heure de fin ne peut pas être antérieure à l'heure de début\"\n    },\n    \"group\": {\n        \"view_manage\": \"Voir et gérer les groupes\",\n        \"err_delete_referenced\": \"Impossible de supprimer un groupe avec des utilisateurs associés, supprimez d'abord les associations\",\n        \"help\": \"Le placeholder %username% sera remplacé par le nom d'utilisateur des utilisateurs associés, %role% par leur rôle\"\n    },\n    \"virtual_folders\": {\n        \"view_manage\": \"Voir et gérer les dossiers virtuels\",\n        \"mount_path\": \"chemin de montage, par exemple /vfolder\",\n        \"quota_size\": \"Taille du quota\",\n        \"quota_size_help\": \"0 signifie pas de limite. Vous pouvez utiliser les suffixes MB/GB/TB\",\n        \"quota_files\": \"Quota de fichiers\",\n        \"associations_summary\": \"Utilisateurs : {{users}}. Groupes : {{groups}}\",\n        \"template_title\": \"Créer un ou plusieurs nouveaux dossiers virtuels à partir de ce modèle\",\n        \"template_name_placeholder\": \"remplacé par le nom du dossier virtuel spécifié\",\n        \"template_help\": \"Les dossiers virtuels générés peuvent être enregistrés ou exportés. Les dossiers exportés peuvent être importés depuis la section \\\"Maintenance\\\" de cette instance SFTPGo ou une autre.\",\n        \"name\": \"Nom du dossier virtuel\",\n        \"template_no_folder\": \"Aucun dossier virtuel valide défini, impossible de compléter l'action demandée\"\n    },\n    \"storage\": {\n        \"title\": \"Système de fichiers\",\n        \"label\": \"Stockage\",\n        \"local\": \"Disque local\",\n        \"s3\": \"S3 (Compatible)\",\n        \"gcs\": \"GCS\",\n        \"azblob\": \"Azure Blob\",\n        \"encrypted\": \"Disque local chiffré\",\n        \"sftp\": \"SFTP\",\n        \"http\": \"HTTP\",\n        \"home_dir\": \"Répertoire racine\",\n        \"home_dir_placeholder\": \"Chemin absolu vers un répertoire sur le disque local\",\n        \"home_dir_help1\": \"Laissez vide pour une valeur par défaut appropriée\",\n        \"home_dir_help2\": \"Laissez vide et stockage sur \\\"Disque local\\\" pour ne pas remplacer le répertoire racine\",\n        \"home_dir_help3\": \"Requis pour les fournisseurs de stockage sur disque local. Pour d'autres fournisseurs de stockage, ce dossier sera utilisé pour les fichiers temporaires, vous pouvez le laisser vide pour une valeur par défaut appropriée\",\n        \"home_dir_invalid\": \"Répertoire racine invalide, assurez-vous qu'il s'agit d'un chemin absolu\",\n        \"sftp_home_dir\": \"Répertoire racine SFTP\",\n        \"sftp_home_help\": \"Restreindre l'accès à ce chemin SFTP. Exemple : \\\"/somedir/subdir\\\"\",\n        \"os_read_buffer\": \"Tampon de téléchargement (MB)\",\n        \"os_buffer_help\": \"0 signifie pas de tampon\",\n        \"os_write_buffer\": \"Tampon de téléversement (MB)\",\n        \"bucket\": \"Bucket\",\n        \"region\": \"Région\",\n        \"access_key\": \"Clé d'accès\",\n        \"access_secret\": \"Clé secrète d'accès\",\n        \"sse_customer_key\": \"Clé de chiffrement côté serveur\",\n        \"sse_customer_key_help\": \"Vous pouvez stocker vos données chiffrées avec cette clé, mais si vous perdez ou modifiez cette clé, vous perdrez tous les fichiers chiffrés avec elle. Les fichiers qui ne sont pas chiffrés ou chiffrés avec une clé différente ne seront pas accessibles\",\n        \"endpoint\": \"Point de terminaison\",\n        \"endpoint_help\": \"Pour AWS S3, laissez vide pour utiliser le point de terminaison par défaut pour la région spécifiée\",\n        \"sftp_endpoint_help\": \"Point de terminaison sous forme hôte:port. Le port est toujours requis\",\n        \"ul_part_size\": \"Taille des parties de téléversement (MB)\",\n        \"part_size_help\": \"0 signifie la valeur par défaut (5 MB). Minimum est 5\",\n        \"gcs_part_size_help\": \"0 signifie la valeur par défaut (16 MB)\",\n        \"ul_concurrency\": \"Concurrence de téléversement\",\n        \"ul_concurrency_help\": \"Nombre de parties téléversées en parallèle. 0 signifie la valeur par défaut (5)\",\n        \"dl_part_size\": \"Taille des parties de téléchargement (MB)\",\n        \"dl_concurrency\": \"Concurrence de téléchargement\",\n        \"dl_concurrency_help\": \"Nombre de parties téléchargées en parallèle. 0 signifie la valeur par défaut (5)\",\n        \"ul_part_timeout\": \"Délai d'attente de la partie de téléversement\",\n        \"ul_part_timeout_help\": \"Temps limite maximal, en secondes, pour téléverser une seule partie. 0 signifie aucune limite\",\n        \"gcs_ul_part_timeout_help\": \"Temps limite maximal, en secondes, pour téléverser une seule partie. 0 signifie la valeur par défaut (32)\",\n        \"dl_part_timeout\": \"Délai d'attente de la partie de téléchargement\",\n        \"dl_part_timeout_help\": \"Temps limite maximal, en secondes, pour télécharger une seule partie. 0 signifie aucune limite\",\n        \"key_prefix\": \"Préfixe de clé\",\n        \"key_prefix_help\": \"Restreindre l'accès aux clés avec le préfixe spécifié. Exemple : \\\"somedir/subdir/\\\"\",\n        \"class\": \"Classe de stockage\",\n        \"acl\": \"ACL\",\n        \"role_arn\": \"ARN du rôle\",\n        \"role_arn_help\": \"Role ARN IAM optionnel à assumer\",\n        \"s3_path_style\": \"Utiliser l'adressage par chemin, c'est-à-dire \\\"endpoint/BUCKET/KEY\\\"\",\n        \"credentials_file\": \"Fichier d'identifiants\",\n        \"credentials_file_help\": \"Ajouter ou mettre à jour les identifiants à partir d'un fichier JSON\",\n        \"auto_credentials\": \"Identifiants automatiques\",\n        \"auto_credentials_help\": \"Utiliser les identifiants d'application par défaut ou les identifiants des variables d'environnement\",\n        \"container\": \"Conteneur\",\n        \"account_name\": \"Nom du compte\",\n        \"account_key\": \"Clé du compte\",\n        \"sas_url\": \"URL SAS\",\n        \"sas_url_help\": \"L'URL de signature d'accès partagé peut être utilisée à la place du nom de compte/clé\",\n        \"emulator\": \"Utiliser l'émulateur\",\n        \"passphrase\": \"Mot de passe\",\n        \"passphrase_help\": \"Mot de passe utilisée pour dériver la clé de chiffrement par objet\",\n        \"passphrase_key_help\": \"Mot de passe utilisée pour protéger votre clé privée, le cas échéant\",\n        \"fingerprints\": \"Empreintes\",\n        \"fingerprints_help\": \"Empreintes SHA256 à valider lors de la connexion au serveur SFTP externe, une par ligne. Si vide, toute clé hôte sera acceptée : c'est un risque de sécurité!\",\n        \"sftp_buffer\": \"Taille du tampon (MB)\",\n        \"sftp_buffer_help\": \"Une taille de tampon supérieure à 0 permet des transferts concurrents\",\n        \"sftp_concurrent_reads\": \"Désactiver les lectures concurrentes\",\n        \"relaxed_equality_check\": \"Vérification d'égalité assouplie\",\n        \"relaxed_equality_check_help\": \"Activer pour considérer uniquement le point de terminaison afin de déterminer si des configurations différentes pointent vers le même serveur. Par défaut, le point de terminaison et le nom d'utilisateur doivent correspondre\",\n        \"api_key\": \"Clé API\",\n        \"fs_error\": \"Erreur de configuration du système de fichiers\",\n        \"bucket_required\": \"$t(storage.fs_error) : le bucket est requis\",\n        \"region_required\": \"$t(storage.fs_error) : la région est requise\",\n        \"key_prefix_invalid\": \"$t(storage.fs_error) : préfixe de clé invalide, ne peut pas commencer par \\\"/\\\"\",\n        \"ul_part_size_invalid\": \"$t(storage.fs_error) : taille de la partie de téléversement invalide\",\n        \"ul_concurrency_invalid\": \"$t(storage.fs_error) : concurrence de téléversement invalide\",\n        \"dl_part_size_invalid\": \"$t(storage.fs_error) : taille de la partie de téléchargement invalide\",\n        \"dl_concurrency_invalid\": \"$t(storage.fs_error) : concurrence de téléchargement invalide\",\n        \"access_key_required\": \"$t(storage.fs_error) : la clé d'accès est requise\",\n        \"access_secret_required\": \"$t(storage.fs_error) : la clé secrète d'accès est requise\",\n        \"credentials_required\": \"$t(storage.fs_error) : les identifiants sont requis\",\n        \"container_required\": \"$t(storage.fs_error) : le conteneur est requis\",\n        \"account_name_required\": \"$t(storage.fs_error) : le nom du compte est requis\",\n        \"sas_url_invalid\": \"$t(storage.fs_error) : URL SAS invalide\",\n        \"passphrase_required\": \"$t(storage.fs_error) : la mot de passe est requise\",\n        \"endpoint_invalid\": \"$t(storage.fs_error) : point de terminaison invalide\",\n        \"endpoint_required\": \"$t(storage.fs_error) : point de terminaison requis\",\n        \"username_required\": \"$t(storage.fs_error) : le nom d'utilisateur est requis\"\n    },\n    \"oidc\": {\n        \"token_expired\": \"Votre jeton OpenID a expiré, veuillez vous reconnecter\",\n        \"token_invalid_webadmin\": \"Votre jeton OpenID n'est pas valable pour l'interface WebAdmin. Déconnectez-vous de votre serveur OpenID et connectez-vous à WebAdmin\",\n        \"token_invalid_webclient\": \"Votre jeton OpenID n'est pas valable pour l'interface WebClient. Déconnectez-vous de votre serveur OpenID et connectez-vous au WebClient\",\n        \"token_exchange_err\": \"Échec de l'échange du jeton OpenID\",\n        \"token_invalid\": \"Jeton OpenID invalide\",\n        \"role_admin_err\": \"Rôle OpenID incorrect, l'utilisateur connecté n'est pas un administrateur\",\n        \"role_user_err\": \"Rôle OpenID incorrect, l'utilisateur connecté est un administrateur\",\n        \"get_user_err\": \"Échec de la récupération de l'utilisateur associé au jeton OpenID\"\n    },\n    \"oauth2\": {\n        \"auth_verify_error\": \"Impossible de vérifier le code OAuth2\",\n        \"auth_validation_error\": \"Impossible de valider le code OAuth2\",\n        \"auth_invalid\": \"Code OAuth2 invalide\",\n        \"token_exchange_err\": \"Impossible d'obtenir le jeton OAuth2 à partir du code d'autorisation\",\n        \"no_refresh_token\": \"Le fournisseur OAuth2 a renvoyé un jeton vide. Certains fournisseurs ne renvoient le jeton que lorsque l'utilisateur autorise pour la première fois. Si vous avez déjà enregistré SFTPGo avec cet utilisateur par le passé, révoquez l'accès et réessayez. De cette manière, vous invaliderez le jeton précédent\",\n        \"success\": \"Copiez la chaîne suivante, sans les guillemets, dans le champ de configuration du jeton SMTP OAuth2:\",\n        \"client_id_required\": \"L'ID client est requis\",\n        \"client_secret_required\": \"Le secret du client est requis\",\n        \"refresh_token_required\": \"Le jeton de rafraîchissement est requis\"\n    },\n    \"filters\": {\n        \"password_strength\": \"Robustesse du mot de passe\",\n        \"password_strength_help\": \"Des valeurs comprises entre 50 et 70 sont suggérées pour des cas d'utilisation courants. 0 signifie désactivé, tout mot de passe sera accepté\",\n        \"password_expiration\": \"Expiration du mot de passe\",\n        \"password_expiration_help\": \"Expiration du mot de passe en nombre de jours. 0 signifie pas d'expiration\",\n        \"default_shares_expiration\": \"Expiration par défaut des partages\",\n        \"default_shares_expiration_help\": \"Expiration par défaut des nouveaux partages en nombre de jours\",\n        \"max_shares_expiration\": \"Expiration maximale des partages\",\n        \"max_shares_expiration_help\": \"Expiration maximale autorisée, en nombre de jours, lorsqu'un utilisateur crée ou met à jour un partage\",\n        \"directory_permissions\": \"Permissions par répertoire\",\n        \"directory_permissions_help\": \"Les jokers sont supportés dans les chemins, par exemple \\\"/incoming/*\\\" correspond à tout répertoire dans \\\"/incoming\\\"\",\n        \"directory_path_help\": \"chemin du répertoire, c'est-à-dire /dir\",\n        \"directory_patterns\": \"Restrictions de motifs de noms par répertoire\",\n        \"directory_patterns_help\": \"Fichiers/répertoires interdits ou autorisés séparés par des virgules, basés sur des motifs shell. La correspondance est insensible à la casse\",\n        \"max_sessions\": \"Sessions maximales\",\n        \"max_sessions_help\": \"Nombre maximal de sessions simultanées. 0 signifie aucune limite\",\n        \"denied_protocols\": \"Protocoles refusés\",\n        \"denied_login_methods\": \"Méthodes de connexion refusées\",\n        \"denied_login_methods_help\": \"\\\"password\\\" est valide pour tous les protocoles supportés, \\\"password-over-SSH\\\" uniquement pour SSH/SFTP/SCP\",\n        \"web_client_options\": \"Client web/API REST\",\n        \"max_upload_size\": \"Taille maximale de téléchargement\",\n        \"max_upload_size_help\": \"Taille maximale de téléchargement pour un seul fichier. 0 signifie aucune limite. Vous pouvez utiliser les suffixes MB/GB/TB\",\n        \"max_upload_size_invalid\": \"Taille maximale de fichier de téléchargement invalide\",\n        \"upload_bandwidth\": \"Bande passante UL (KB/s)\",\n        \"download_bandwidth\": \"Bande passante DL (KB/s)\",\n        \"upload_bandwidth_help\": \"UL (KB/s). 0 signifie aucune limite\",\n        \"download_bandwidth_help\": \"DL (KB/s). 0 signifie aucune limite\",\n        \"src_bandwidth_limit\": \"Limites de vitesse de bande passante par source\",\n        \"upload_data_transfer\": \"Transfert de données en upload (MB)\",\n        \"upload_data_transfer_help\": \"Transfert de données maximal autorisé pour les uploads. 0 signifie aucune limite\",\n        \"download_data_transfer\": \"Transfert de données en download (MB)\",\n        \"download_data_transfer_help\": \"Transfert de données maximal autorisé pour les téléchargements. 0 signifie aucune limite\",\n        \"total_data_transfer\": \"Transfert de données total (MB)\",\n        \"total_data_transfer_help\": \"Transfert de données maximal autorisé pour les uploads + downloads. Remplace les limites individuelles. 0 signifie aucune limite\",\n        \"start_directory\": \"Répertoire initial\",\n        \"start_directory_help\": \"Répertoire initial alternatif à utiliser au lieu de \\\"/\\\". Supporté pour SFTP/FTP/HTTP\",\n        \"tls_username\": \"Nom d'utilisateur TLS\",\n        \"tls_username_help\": \"Définit le champ du certificat TLS à utiliser comme nom d'utilisateur. Ignoré si le TLS mutuel est désactivé\",\n        \"ftp_security\": \"Sécurité FTP\",\n        \"ftp_security_help\": \"Ignoré si le TLS est déjà globalement requis pour tous les utilisateurs FTP\",\n        \"hooks\": \"Hooks\",\n        \"hook_ext_auth_disabled\": \"Authentification externe désactivée\",\n        \"hook_pre_login_disabled\": \"Pré-connexion désactivée\",\n        \"hook_check_password_disabled\": \"Vérification du mot de passe désactivée\",\n        \"is_anonymous\": \"Utilisateur anonyme\",\n        \"is_anonymous_help\": \"Les utilisateurs anonymes sont supportés pour les protocoles FTP et WebDAV et ont un accès en lecture seule\",\n        \"disable_fs_checks\": \"Désactiver les vérifications du système de fichiers\",\n        \"disable_fs_checks_help\": \"Désactiver les vérifications d'existence et la création automatique de répertoire personnel et de dossiers virtuels\",\n        \"api_key_auth_help\": \"Permet de se faire passer pour l'utilisateur, dans l'API REST, avec une clé API\",\n        \"external_auth_cache_time\": \"Durée du cache d'authentification externe\",\n        \"external_auth_cache_time_help\": \"Durée du cache, en secondes, pour les utilisateurs authentifiés à l'aide d'un hook d'authentification externe. 0 signifie pas de cache\",\n        \"access_time\": \"Restrictions de temps d'accès\",\n        \"access_time_help\": \"Aucune restriction signifie que l'accès est toujours autorisé, le temps doit être défini au format HH:MM\"\n    },\n    \"admin\": {\n        \"role_permissions\": \"Un administrateur de rôle ne peut pas avoir la permission \\\"*\\\"\",\n        \"view_manage\": \"Voir et gérer les administrateurs\",\n        \"self_delete\": \"Vous ne pouvez pas vous supprimer\",\n        \"self_permissions\": \"Vous ne pouvez pas modifier vos permissions\",\n        \"self_disable\": \"Vous ne pouvez pas vous désactiver\",\n        \"self_role\": \"Vous ne pouvez pas ajouter/changer votre rôle\",\n        \"password_help\": \"Si vide, le mot de passe actuel ne sera pas modifié\",\n        \"role_help\": \"Attribuer un rôle limite l'administrateur à gérer uniquement les utilisateurs avec le même rôle. Les administrateurs avec un rôle ne peuvent pas être des super-administrateurs\",\n        \"users_groups\": \"Groupes pour les utilisateurs\",\n        \"users_groups_help\": \"Groupes automatiquement sélectionnés pour les nouveaux utilisateurs créés par cet administrateur. L'administrateur pourra toujours choisir d'autres groupes. Ces paramètres sont uniquement utilisés pour cette interface d'administration et seront ignorés dans l'API REST/hooks\",\n        \"group_membership\": \"Ajouter en tant que membre\",\n        \"group_primary\": \"Ajouter en tant que principal\",\n        \"group_secondary\": \"Ajouter en tant que secondaire\",\n        \"user_page_pref\": \"Préférences de la page utilisateur\",\n        \"user_page_pref_help\": \"Vous pouvez masquer certaines sections de la page utilisateur. Ce ne sont pas des paramètres de sécurité et ils ne sont pas appliqués côté serveur de quelque manière que ce soit. Ils sont uniquement destinés à simplifier la page d'ajout/mise à jour de l'utilisateur\",\n        \"hide_sections\": \"Masquer les sections\",\n        \"default_users_expiration\": \"Expiration par défaut des utilisateurs\",\n        \"default_users_expiration_help\": \"Expiration par défaut des nouveaux utilisateurs en nombre de jours\",\n        \"require_pwd_change_help\": \"Un changement de mot de passe est requis lors de la prochaine connexion\"\n    },\n    \"connections\": {\n        \"view_manage\": \"Voir et gérer les connexions\",\n        \"started\": \"Démarré\",\n        \"remote_address\": \"Adresse distante\",\n        \"last_activity\": \"Dernière activité\",\n        \"disconnect_confirm_btn\": \"Oui, déconnecter\",\n        \"disconnect_confirm\": \"Voulez-vous déconnecter la connexion sélectionnée ? Cette action est irréversible\",\n        \"disconnect_ko\": \"Impossible de déconnecter la connexion sélectionnée\",\n        \"upload\": \"UL : \\\"{{- path}}\\\"\",\n        \"download\": \"DL : \\\"{{- path}}\\\"\",\n        \"upload_info\": \"$t(connections.upload). Taille : {{- size}}. Vitesse : {{- speed}}\",\n        \"download_info\": \"$t(connections.download). Taille : {{- size}}. Vitesse : {{- speed}}\",\n        \"client\": \"Client : {{- val}}\"\n    },\n    \"role\": {\n        \"view_manage\": \"Voir et gérer les rôles\",\n        \"err_delete_referenced\": \"Impossible de supprimer un rôle avec des administrateurs associés, supprimez d'abord les associations\"\n    },\n    \"ip_list\": {\n        \"view_manage\": \"Voir et gérer les listes IP\",\n        \"defender_list\": \"Defender\",\n        \"allow_list\": \"Liste blanche\",\n        \"ratelimiters_safe_list\": \"Liste de sécurité des limiteurs de débit\",\n        \"ip_net\": \"IP/Réseau\",\n        \"protocols\": \"Protocoles\",\n        \"any\": \"Tout\",\n        \"allow\": \"Autoriser\",\n        \"deny\": \"Refuser\",\n        \"ip_net_help\": \"Adresse IP ou réseau au format CIDR, exemple : \\\"192.168.1.1 ou 10.8.0.100/32 ou 2001:db8:1234::/48\\\"\",\n        \"ip_invalid\": \"Adresse IP invalide\",\n        \"net_invalid\": \"Réseau invalide\",\n        \"duplicated\": \"L'IP/réseau spécifié existe déjà\",\n        \"search\": \"IP/Réseau ou partie initiale\",\n        \"defender_disabled\": \"Défenseur désactivé dans votre configuration\",\n        \"allow_list_disabled\": \"Liste blanche désactivée dans votre configuration\",\n        \"ratelimiters_disabled\": \"Limiteurs de débit désactivés dans votre configuration\"\n    },\n    \"defender\": {\n        \"view_manage\": \"Voir et gérer la liste noire automatique\",\n        \"ip\": \"Adresse IP\",\n        \"ban_time\": \"Bloqué jusqu'à\",\n        \"score\": \"Score\"\n    },\n    \"status\": {\n        \"desc\": \"État du serveur\",\n        \"ssh\": \"Serveur SSH/SFTP\",\n        \"active\": \"État : actif\",\n        \"disabled\": \"État : désactivé\",\n        \"error\": \"État : erreur\",\n        \"proxy_on\": \"Protocole PROXY activé\",\n        \"address\": \"Adresse\",\n        \"ssh_auths\": \"Méthodes d'authentification\",\n        \"ssh_commands\": \"Commandes acceptées\",\n        \"host_key\": \"Clé hôte\",\n        \"fingeprint\": \"Empreinte\",\n        \"algorithms\": \"Algorithmes\",\n        \"algorithm\": \"Algorithme\",\n        \"ssh_pub_key_algo\": \"Algorithmes d'authentification par clé publique\",\n        \"ssh_mac_algo\": \"Algorithmes de code d'authentification de message (MAC)\",\n        \"ssh_kex_algo\": \"Algorithmes d'échange de clés (KEX)\",\n        \"ssh_cipher_algo\": \"Chiffres\",\n        \"ftp\": \"Serveur FTP\",\n        \"ftp_passive_range\": \"Plage de ports en mode passif\",\n        \"ftp_passive_ip\": \"IP passive\",\n        \"tls\": \"TLS\",\n        \"tls_disabled\": \"Désactivé\",\n        \"tls_explicit\": \"Mode explicite requis (FTPES)\",\n        \"tls_implicit\": \"Mode implicite (FTPS), obsolète, préférez FTPES\",\n        \"tls_mixed\": \"Mode clair et explicite (FTPES)\",\n        \"webdav\": \"Serveur WebDAV\",\n        \"rate_limiters\": \"Limiteurs de débit\"\n    },\n    \"maintenance\": {\n        \"backup\": \"Sauvegarde\",\n        \"backup_do\": \"Sauvegardez vos données\",\n        \"backup_ok\": \"Sauvegarde restaurée avec succès\",\n        \"backup_invalid_file\": \"Fichier de sauvegarde invalide, assurez-vous qu'il s'agit d'un fichier JSON avec un contenu valide\",\n        \"restore_error\": \"Impossible de restaurer votre sauvegarde, vérifiez les logs du serveur pour plus de détails\",\n        \"restore\": \"Restaurer\",\n        \"backup_file\": \"Fichier de sauvegarde\",\n        \"backup_file_help\": \"Importer des données à partir d'un fichier de sauvegarde JSON\",\n        \"restore_mode0\": \"Ajouter et mettre à jour\",\n        \"restore_mode1\": \"Ajouter seulement\",\n        \"restore_mode2\": \"Ajouter, mettre à jour et déconnecter\",\n        \"after_restore\": \"Après restauration\",\n        \"quota_mode0\": \"Pas de mise à jour de quota\",\n        \"quota_mode1\": \"Mettre à jour le quota\",\n        \"quota_mode2\": \"Mettre à jour le quota pour les utilisateurs avec des limites de quota\"\n    },\n    \"acme\": {\n        \"title\": \"ACME\",\n        \"generic_error\": \"Impossible d'obtenir des certificats TLS, vérifiez les logs du serveur pour plus de détails\",\n        \"help\": \"Depuis cette section, vous pouvez demander des certificats TLS gratuits pour vos services SFTPGo en utilisant le protocole ACME et le type de défi HTTP-01. Vous devez créer une entrée DNS sous un domaine personnalisé que vous possédez qui résout à l'adresse IP publique de votre SFTPGo et le port 80 doit être publiquement accessible. Vous pouvez définir les options de configuration pour les cas d'utilisation les plus courants et les configurations de nœud unique ici, pour des configurations avancées, référez-vous à la documentation SFTPGo. Un redémarrage du service est nécessaire pour appliquer les changements\",\n        \"domain_help\": \"Plusieurs domaines peuvent être spécifiés séparés par des virgules ou des espaces. Ils seront inclus dans le même certificat\",\n        \"email_help\": \"E-mail utilisé pour l'enregistrement et le contact de récupération\",\n        \"port_help\": \"Si différent de 80, vous devez configurer un proxy inverse\",\n        \"protocols_help\": \"Utilisez les certificats obtenus pour les protocoles spécifiés\"\n    },\n    \"smtp\": {\n        \"title\": \"SMTP\",\n        \"err_required_fields\": \"L'adresse de l'expéditeur et le nom d'utilisateur ne peuvent pas être tous les deux vides\",\n        \"help\": \"Définissez la configuration SMTP en remplaçant celle définie à l'aide des variables d'environnement ou du fichier de configuration, le cas échéant\",\n        \"host\": \"Nom du serveur\",\n        \"host_help\": \"Si vide, la configuration est désactivée\",\n        \"auth\": \"Authentification\",\n        \"encryption\": \"Chiffrement\",\n        \"sender\": \"Expéditeur\",\n        \"debug\": \"Logs de débogage\",\n        \"domain_help\": \"Domaine HELO. Laissez vide pour utiliser le nom d'hôte du serveur\",\n        \"test_recipient\": \"Adresse pour envoyer des e-mails de test\",\n        \"oauth2_provider\": \"Fournisseur OAuth2\",\n        \"oauth2_provider_help\": \"URI pour rediriger après l'authentification de l'utilisateur\",\n        \"oauth2_tenant\": \"Locataire OAuth2\",\n        \"oauth2_tenant_help\": \"Locataire Azure. Les valeurs typiques sont \\\"common\\\", \\\"organizations\\\", \\\"consumers\\\" ou l'identifiant du locataire\",\n        \"oauth2_client_id\": \"ID client OAuth2\",\n        \"oauth2_client_secret\": \"Secret client OAuth2\",\n        \"oauth2_token\": \"Jeton OAuth2\",\n        \"recipient_required\": \"Spécifiez un destinataire pour envoyer un e-mail de test\",\n        \"test_error\": \"Impossible d'envoyer l'e-mail de test, vérifiez les logs du serveur pour plus de détails\",\n        \"test_ok\": \"Aucune erreur n'a été signalée lors de l'envoi de l'e-mail de test. Veuillez vérifier votre boîte de réception pour vous en assurer\",\n        \"oauth2_flow_error\": \"Impossible d'obtenir l'URI pour démarrer le flux OAuth2\",\n        \"oauth2_question\": \"Voulez-vous démarrer le flux OAuth2 pour obtenir un jeton ?\"\n    },\n    \"sftp\": {\n        \"help\": \"Depuis cette section, vous pouvez activer les algorithmes désactivés par défaut. Vous n'avez pas besoin de définir les valeurs déjà définies à l'aide des variables d'environnement ou du fichier de configuration. Un redémarrage du service est nécessaire pour appliquer les changements\",\n        \"host_key_algos\": \"Algorithmes de clé hôte\"\n    },\n    \"branding\": {\n        \"title\": \"Image de marque\",\n        \"help\": \"Dans cette section, vous pouvez personnaliser SFTPGo pour l'adapter à votre image de marque et ajouter un avertissement aux pages de connexion\",\n        \"short_name\": \"Nom abrégé\",\n        \"logo\": \"Logo\",\n        \"logo_help\": \"Image PNG, taille maximale acceptée 512x512, taille par défaut 256x256\",\n        \"favicon\": \"Favicon\",\n        \"disclaimer_name\": \"Titre de l'avertissement\",\n        \"disclaimer_url\": \"URL de l'avertissement\",\n        \"invalid_png\": \"Image PNG invalide\",\n        \"invalid_png_size\": \"Taille de l'image PNG invalide\",\n        \"invalid_disclaimer_url\": \"L'URL de l'avertissement doit être un lien http ou https\"\n    },\n    \"events\": {\n        \"search\": \"Rechercher dans les logs\",\n        \"fs_events\": \"Événements du système de fichiers\",\n        \"provider_events\": \"Événements du fournisseur\",\n        \"other_events\": \"Autres événements\",\n        \"quota_exceeded\": \"Quota dépassé\",\n        \"date_range\": \"Plage de dates\",\n        \"upload\": \"Téléchargement\",\n        \"download\": \"Téléchargement\",\n        \"mkdir\": \"Créer un dossier\",\n        \"rmdir\": \"Supprimer un dossier\",\n        \"rename\": \"Renommer\",\n        \"delete\": \"Suppression\",\n        \"copy\": \"Copier\",\n        \"first_upload\": \"Premier téléchargement\",\n        \"first_download\": \"Premier téléchargement\",\n        \"ssh_cmd\": \"Commande SSH\",\n        \"add\": \"Ajout\",\n        \"update\": \"Mise à jour\",\n        \"login_failed\": \"Échec de la connexion\",\n        \"login_ok\": \"Connexion réussie\",\n        \"login_missing_user\": \"Connexion avec un utilisateur inexistant\",\n        \"no_login_tried\": \"Aucune connexion tentée\",\n        \"algo_negotiation_failed\": \"Échec de la négociation de l'algorithme\",\n        \"datetime\": \"Date et heure\",\n        \"action\": \"Action\",\n        \"path\": \"Chemin\",\n        \"object\": \"Objet\",\n        \"event\": \"Événement\"\n    },\n    \"provider_objects\": {\n        \"user\": \"Utilisateur\",\n        \"folder\": \"Dossier\",\n        \"group\": \"Groupe\",\n        \"admin\": \"Administrateur\",\n        \"api_key\": \"Clé API\",\n        \"share\": \"Partage\",\n        \"event_action\": \"Action\",\n        \"event_rule\": \"Règle\",\n        \"role\": \"Rôle\",\n        \"ip_list_entry\": \"Entrée de la liste IP\",\n        \"configs\": \"Paramètres\"\n    },\n    \"actions\": {\n        \"view_manage\": \"Voir et gérer les actions des règles pour les événements\",\n        \"err_delete_referenced\": \"Impossible de supprimer une action avec des règles associées, supprimez d'abord les associations\",\n        \"http_url\": \"URL du serveur\",\n        \"http_url_help\": \"par exemple https://host:port/path. Les espaces réservés sont pris en charge dans le chemin de l'URL\",\n        \"http_url_required\": \"L'URL est requise\",\n        \"http_url_invalid\": \"L'URL est invalide, les schémas http et https sont pris en charge\",\n        \"http_part_name_required\": \"Le nom de la partie HTTP est requis\",\n        \"http_part_body_required\": \"Le corps de la partie HTTP est requis si aucun chemin de fichier n'est fourni\",\n        \"http_multipart_body_error\": \"Les requêtes multiparties ne nécessitent pas de corps. Le corps de la requête est construit à partir des parties spécifiées\",\n        \"http_multipart_ctype_error\": \"Le type de contenu est automatiquement défini pour les requêtes multiparties\",\n        \"path_duplicated\": \"Chemin dupliqué\",\n        \"command_required\": \"La commande est requise\",\n        \"command_invalid\": \"Commande invalide, elle doit être un chemin absolu\",\n        \"email_recipient_required\": \"Au moins un destinataire d'e-mail est requis\",\n        \"email_subject_required\": \"Le sujet de l'e-mail est requis\",\n        \"email_body_required\": \"Le corps de l'e-mail est requis\",\n        \"retention_directory_required\": \"Au moins un répertoire à vérifier est requis\",\n        \"path_required\": \"Au moins un chemin est requis\",\n        \"source_dest_different\": \"Le chemin source et le chemin cible doivent être différents\",\n        \"root_not_allowed\": \"Le chemin racine (/) n'est pas autorisé\",\n        \"archive_name_required\": \"Le nom de l'archive compressée est requis\",\n        \"idp_template_required\": \"Un modèle d'utilisateur ou d'administrateur est requis\",\n        \"threshold\": \"Seuil\",\n        \"threshold_help\": \"Une notification par e-mail sera générée pour les utilisateurs dont le mot de passe expire dans un nombre de jours inférieur ou égal à ce seuil\",\n        \"disable_threshold\": \"Seuil de désactivation\",\n        \"disable_threshold_help\": \"Inactivité en jours, depuis la dernière connexion ou la création avant de désactiver les utilisateurs\",\n        \"delete_threshold\": \"Seuil de suppression\",\n        \"delete_threshold_help\": \"Inactivité en jours, depuis la dernière connexion ou la création avant de supprimer les utilisateurs\",\n        \"inactivity_threshold_required\": \"Au moins un seuil d'inactivité doit être défini\",\n        \"inactivity_thresholds_invalid\": \"Le seuil de suppression doit être supérieur au seuil de désactivation\",\n        \"idp_mode_add_update\": \"Créer ou mettre à jour\",\n        \"idp_mode_add\": \"Créer si cela n'existe pas\",\n        \"template_user_help\": \"Modèle pour les utilisateurs de SFTPGo au format JSON. Les espaces réservés sont pris en charge\",\n        \"template_admin_help\": \"Modèle pour les administrateurs de SFTPGo au format JSON. Les espaces réservés sont pris en charge\",\n        \"placeholders_help\": \"Les espaces réservés sont pris en charge\",\n        \"http_headers\": \"En-têtes HTTP\",\n        \"query_parameters\": \"Paramètres de chaîne de requête\",\n        \"http_timeout_help\": \"Ignoré pour les requêtes multiparties avec des fichiers en pièce jointe\",\n        \"body\": \"Corps\",\n        \"http_body_help\": \"Les espaces réservés sont pris en charge. Ignoré pour les requêtes HTTP GET. Laissez vide pour les requêtes multiparties\",\n        \"multipart_body\": \"Corps multiparti\",\n        \"multipart_body_help\": \"Les requêtes HTTP multipart permettent de combiner un ou plusieurs ensembles de données en un seul corps. Pour chaque partie, vous pouvez définir un chemin de fichier ou un corps en texte. Les espaces réservés sont pris en charge dans le chemin du fichier, le corps et les valeurs des en-têtes\",\n        \"http_part_name\": \"Nom de la partie\",\n        \"http_part_file\": \"Chemin du fichier\",\n        \"http_part_headers\": \"En-têtes supplémentaires pour la partie, un par ligne comme \\\"clé: valeur\\\"\",\n        \"command_help\": \"Chemin absolu de la commande à exécuter\",\n        \"command_args\": \"Arguments\",\n        \"command_args_help\": \"Arguments de commande séparés par des virgules. Les espaces réservés sont pris en charge\",\n        \"command_env_vars_help\": \"Les espaces réservés sont supportés en valeurs. Définir la valeur à \\\"$\\\" sans guillemets signifie récupérer la clé de l'environnement à l'exclusion des clés commençant par SFTPGO_\",\n        \"email_recipients\": \"À\",\n        \"email_recipients_help\": \"Destinataires séparés par des virgules. Les espaces réservés sont pris en charge\",\n        \"email_bcc\": \"Cci\",\n        \"email_bcc_help\": \"Adresses Cci séparées par des virgules. Les espaces réservés sont pris en charge\",\n        \"email_subject\": \"Sujet\",\n        \"content_type\": \"Type de contenu\",\n        \"attachments\": \"Pièces jointes\",\n        \"attachments_help\": \"Chemins séparés par des virgules à joindre. Les espaces réservés sont pris en charge. La taille totale est limitée à 10 Mo\",\n        \"data_retention\": \"Rétention des données\",\n        \"data_retention_help\": \"Définir la rétention des données, en heures, par chemin. La rétention s'applique récursivement. Définir 0 comme rétention signifie exclure le chemin spécifié\",\n        \"delete_empty_dirs\": \"Supprimer les répertoires vides\",\n        \"fs_action\": \"Action du système de fichiers\",\n        \"paths_src_dst_help\": \"Chemins tels que vus par les utilisateurs de SFTPGo. Les espaces réservés sont pris en charge. Les permissions requises sont accordées automatiquement\",\n        \"source_path\": \"Source\",\n        \"target_path\": \"Cible\",\n        \"paths_help\": \"Chemins séparés par des virgules tels que vus par les utilisateurs de SFTPGo. Les espaces réservés sont pris en charge. Les permissions requises sont accordées automatiquement\",\n        \"archive_path\": \"Chemin de l'archive\",\n        \"archive_path_help\": \"Chemin complet, tel que vu par les utilisateurs de SFTPGo, vers l'archive zip à créer. Les espaces réservés sont pris en charge. Si le fichier spécifié existe déjà, il est écrasé\",\n        \"placeholders_modal_title\": \"Espaces réservés pris en charge\",\n        \"update_mod_times\": \"Mettre à jour l'horodatage\",\n        \"types\": {\n            \"http\": \"HTTP\",\n            \"email\": \"E-mail\",\n            \"backup\": \"Sauvegarde\",\n            \"user_quota_reset\": \"Réinitialisation du quota utilisateur\",\n            \"folder_quota_reset\": \"Réinitialisation du quota de dossier\",\n            \"transfer_quota_reset\": \"Réinitialisation du quota de transfert\",\n            \"data_retention_check\": \"Vérification de la rétention des données\",\n            \"filesystem\": \"Système de fichiers\",\n            \"password_expiration_check\": \"Vérification de l'expiration du mot de passe\",\n            \"user_expiration_check\": \"Vérification de l'expiration de l'utilisateur\",\n            \"user_inactivity_check\": \"Vérification de l'inactivité de l'utilisateur\",\n            \"idp_check\": \"Vérification du compte du fournisseur d'identité\",\n            \"rotate_logs\": \"Renouveler le fichier journal\",\n            \"command\": \"Commande\"\n        },\n        \"fs_types\": {\n            \"rename\": \"Renommer\",\n            \"delete\": \"Supprimer\",\n            \"path_exists\": \"Les chemins existent\",\n            \"compress\": \"Compresser\",\n            \"copy\": \"Copier\",\n            \"create_dirs\": \"Créer des répertoires\"\n        },\n        \"placeholders_modal\": {\n            \"name\": \"Nom d'utilisateur, nom du dossier virtuel, nom d'utilisateur administrateur pour les événements du fournisseur, nom de domaine pour les événements du certificat TLS\",\n            \"event\": \"Nom de l'événement, par exemple \\\"téléchargement\\\", \\\"téléchargement\\\" pour les événements du système de fichiers ou \\\"ajout\\\", \\\"mise à jour\\\" pour les événements du fournisseur\",\n            \"status\": \"Statut pour les événements du système de fichiers. 1 signifie aucune erreur, 2 signifie qu'une erreur générale s'est produite, 3 signifie une erreur de quota dépassé\",\n            \"status_string\": \"Statut en tant que chaîne. Valeurs possibles \\\"OK\\\", \\\"KO\\\"\",\n            \"error_string\": \"Détails de l'erreur. Remplacé par une chaîne vide si aucune erreur ne se produit\",\n            \"virtual_path\": \"Chemin vu par les utilisateurs de SFTPGo, par exemple \\\"/adir/unfichier.txt\\\"\",\n            \"escaped_virtual_path\": \"Chemin encodé, par exemple \\\"%2Fdossier%2Ffichier.txt\\\".\",\n            \"virtual_dir_path\": \"Répertoire parent pour \\\"VirtualPath\\\", par exemple si \\\"VirtualPath\\\" est \\\"/adir/unfichier.txt\\\", \\\"VirtualDirPath\\\" est \\\"/adir\\\"\",\n            \"fs_path\": \"Chemin complet du système de fichiers, par exemple \\\"/utilisateur/répertoire/adir/unfichier.txt\\\" ou \\\"C:/data/utilisateur/répertoire/adir/unfichier.txt\\\" sous Windows\",\n            \"ext\": \"Extension de fichier, par exemple \\\".txt\\\" si le nom du fichier est \\\"unfichier.txt\\\"\",\n            \"object_name\": \"Nom du fichier/répertoire, par exemple \\\"unfichier.txt\\\" ou nom de l'objet du fournisseur\",\n            \"object_basename\": \"Nom du fichier sans extension, par exemple \\\"fichier\\\" si le nom du fichier est \\\"fichier.txt\\\"\",\n            \"object_type\": \"Type d'objet pour les événements du fournisseur : \\\"utilisateur\\\", \\\"groupe\\\", \\\"admin\\\", etc.\",\n            \"virtual_target_path\": \"Chemin cible virtuel pour les opérations de renommage et de copie\",\n            \"virtual_target_dir_path\": \"Répertoire parent pour \\\"VirtualTargetPath\\\"\",\n            \"target_name\": \"Nom de l'objet cible pour les opérations de renommage et de copie\",\n            \"fs_target_path\": \"Chemin cible complet du système de fichiers pour les opérations de renommage et de copie\",\n            \"file_size\": \"Taille du fichier (octets)\",\n            \"elapsed\": \"Temps écoulé en millisecondes pour les événements du système de fichiers\",\n            \"protocol\": \"Protocole, par exemple \\\"SFTP\\\", \\\"FTP\\\"\",\n            \"ip\": \"Adresse IP du client\",\n            \"role\": \"Rôle de l'utilisateur ou de l'administrateur\",\n            \"timestamp\": \"Horodatage de l'événement en nanosecondes depuis l'époque\",\n            \"datetime\": \"Horodatage de l'événement au format AAAA-MM-JJTHH:MM:SS.ZZZ\",\n            \"year\": \"Année de l'événement formatée sur quatre chiffres\",\n            \"month\": \"Mois de l'événement formaté en deux chiffres\",\n            \"day\": \"Jour de l'événement formaté en deux chiffres\",\n            \"hour\": \"Heure de l'événement formatée en deux chiffres\",\n            \"minute\": \"La minute de l'événement est formatée en deux chiffres\",\n            \"email\": \"Pour les événements du système de fichiers, il s'agit de l'e-mail associé à l'utilisateur effectuant l'action. Pour les événements du fournisseur, il s'agit de l'e-mail associé à l'utilisateur ou à l'administrateur concerné. Vide dans tous les autres cas\",\n            \"object_data\": \"Données de l'objet du fournisseur sérialisées en JSON avec les champs sensibles supprimés\",\n            \"object_data_string\": \"Données de l'objet du fournisseur en tant que chaîne JSON échappée avec les champs sensibles supprimés\",\n            \"retention_reports\": \"Rapports de rétention des données sous forme de fichiers CSV compressés en zip. Pris en charge comme pièce jointe par e-mail, chemin de fichier pour les requêtes HTTP multiparties et comme paramètre unique pour le corps des requêtes HTTP\",\n            \"idp_field\": \"Champs personnalisés du fournisseur d'identité contenant une chaîne\",\n            \"metadata\": \"Métadonnées du stockage cloud pour le fichier téléchargé sérialisées en JSON\",\n            \"metadata_string\": \"Métadonnées du stockage cloud pour le fichier téléchargé en tant que chaîne JSON échappée\",\n            \"uid\": \"ID unique\"\n        }\n    },\n    \"rules\": {\n        \"view_manage\": \"Voir et gérer les règles pour les événements\",\n        \"trigger\": \"Déclencheur\",\n        \"run_confirm\": \"Voulez-vous exécuter la règle sélectionnée ?\",\n        \"run_confirm_btn\": \"Oui, exécuter\",\n        \"run_error_generic\": \"Impossible d'exécuter la règle sélectionnée\",\n        \"run_ok\": \"Actions de la règle démarrées\",\n        \"run\": \"Exécuter\",\n        \"invalid_fs_min_size\": \"Taille minimale invalide\",\n        \"invalid_fs_max_size\": \"Taille maximale invalide\",\n        \"action_required\": \"Au moins une action est requise\",\n        \"fs_event_required\": \"Au moins un événement du système de fichiers est requis\",\n        \"provider_event_required\": \"Au moins un événement du fournisseur est requis\",\n        \"schedule_required\": \"Au moins un calendrier est requis\",\n        \"schedule_invalid\": \"Calendrier invalide\",\n        \"duplicate_actions\": \"Actions en double détectées\",\n        \"sync_failure_actions\": \"L'exécution synchrone n'est pas prise en charge pour les actions d'échec\",\n        \"sync_unsupported\": \"L'exécution synchrone n'est prise en charge que pour certains événements du système de fichiers et les connexions au fournisseur d'identité\",\n        \"sync_unsupported_fs_event\": \"L'exécution synchrone n'est prise en charge que pour les événements de téléchargement et de pré-* du système de fichiers\",\n        \"only_failure_actions\": \"Au moins une action non liée à un échec est requise\",\n        \"sync_action_required\": \"L'événement \\\"{{val}}\\\" nécessite au moins une action synchrone\",\n        \"scheduler_help\": \"Horaires : 0-23. Jour de la semaine : 0-6 (dim-sam). Jour du mois : 1-31. Mois : 1-12. L'astérisque (*) indique une correspondance pour toutes les valeurs du champ. Par ex. chaque jour de la semaine, chaque jour du mois et ainsi de suite\",\n        \"concurrent_run\": \"Autoriser l'exécution concurrente à partir de plusieurs instances\",\n        \"protocol_filters\": \"Filtres de protocole\",\n        \"status_filters\": \"Filtres de status\",\n        \"object_filters\": \"Filtres d'objet\",\n        \"name_filters\": \"Filtres de nom\",\n        \"name_filters_help\": \"Filtres de modèle de type shell pour les noms d'utilisateur, les noms de dossier. Par exemple, \\\"user*\\\" correspondra aux noms commençant par \\\"user\\\". Pour les événements du fournisseur, ce filtre est appliqué au nom d'utilisateur de l'administrateur exécutant l'événement\",\n        \"inverse_match\": \"Correspondance inverse\",\n        \"group_name_filters\": \"Filtres de nom de groupe\",\n        \"group_name_filters_help\": \"Filtres de modèle de type shell pour les noms de groupe. Par exemple, \\\"group*\\\" correspondra aux noms de groupe commençant par \\\"group\\\"\",\n        \"role_name_filters\": \"Filtres de nom de rôle\",\n        \"role_name_filters_help\": \"Filtres de modèle de type shell pour les noms de rôle. Par exemple, \\\"role*\\\" correspondra aux noms de rôle commençant par \\\"role\\\"\",\n        \"path_filters\": \"Filtres de chemin\",\n        \"path_filters_help\": \"Filtres de modèle de type shell sur les chemins des événements du système de fichiers. Par exemple, \\\"/adir/*.txt\\\" correspondra aux chemins dans le répertoire \\\"/adir\\\" se terminant par \\\".txt\\\". La double astérisque est prise en charge, par exemple, \\\"/**/*.txt\\\" correspondra à tout fichier se terminant par \\\".txt\\\". \\\"/mydir/**\\\" correspondra à toute entrée dans \\\"/mydir\\\"\",\n        \"file_size_limits\": \"Limites de taille de fichier\",\n        \"file_size_limits_help\": \"0 signifie aucune limite. Vous pouvez utiliser les suffixes MB/GB\",\n        \"min_size\": \"Taille minimale\",\n        \"max_size\": \"Taille maximale\",\n        \"actions_help\": \"Une ou plusieurs actions à exécuter. L'option \\\"Exécuter de manière synchrone\\\" est prise en charge pour les événements de \\\"téléchargement\\\" et requise pour les événements \\\"pré-*\\\" et les événements de connexion au fournisseur d'identité si l'action vérifie le compte\",\n        \"option_failure_action\": \"Action d'échec\",\n        \"option_stop_on_failure\": \"Arrêter en cas d'échec\",\n        \"option_execute_sync\": \"Exécution synchrone\",\n        \"no_filter\": \"Aucun filtre signifie déclenchement des événements toujours\",\n        \"action_placeholder\": \"Sélectionnez une action\",\n        \"triggers\": {\n            \"fs_event\": \"Événements du système de fichiers\",\n            \"provider_event\": \"Événements du fournisseur\",\n            \"ip_blocked\": \"IP bloquée\",\n            \"certificate_renewal\": \"Renouvellement de certificat\",\n            \"on_demand\": \"À la demande\",\n            \"idp_login\": \"Connexions au fournisseur d'identité\",\n            \"schedule\": \"Calendriers\"\n        },\n        \"idp_logins\": {\n            \"user\": \"Connexion utilisateur\",\n            \"admin\": \"Connexion administrateur\"\n        }\n    }\n}\n"
  },
  {
    "path": "static/locales/it/translation.json",
    "content": "{\n    \"title\": {\n        \"setup\": \"Configurazione iniziale\",\n        \"login\": \"Accedi\",\n        \"share_login\": \"Accedi alla condivisione\",\n        \"profile\": \"Profilo\",\n        \"change_password\": \"Cambio password\",\n        \"files\": \"File\",\n        \"shares\": \"Condivisioni\",\n        \"add_share\": \"Aggiungi condivisione\",\n        \"update_share\": \"Modifica condivisione\",\n        \"two_factor_auth\": \"Autenticazione a due fattori\",\n        \"two_factor_auth_short\": \"2FA\",\n        \"edit_file\": \"Modifica file\",\n        \"view_file\": \"Visualizza file\",\n        \"recovery_password\": \"Recupero password\",\n        \"reset_password\": \"Reimpostazione password\",\n        \"shared_files\": \"File condivisi\",\n        \"upload_to_share\": \"Carica nella condivisione\",\n        \"download_shared_file\": \"Scarica file condiviso\",\n        \"share_access_error\": \"Impossibile accedere alla condivisione\",\n        \"invalid_auth_request\": \"Richiesta di autenticazione non valida\",\n        \"error400\": \"Richiesta non valida\",\n        \"error403\": \"Non permesso\",\n        \"error404\": \"Non trovato\",\n        \"error416\": \"Impossibile tornare l'intervallo richiesto\",\n        \"error429\": \"Troppe richieste\",\n        \"error500\": \"Errore interno del server\",\n        \"errorPDF\": \"Impossibile mostrare il file PDF\",\n        \"error_editor\": \"Impossibile aprire l'editor di file\",\n        \"users\": \"Utenti\",\n        \"groups\": \"Gruppi\",\n        \"folders\": \"Cartelle virtuali\",\n        \"connections\": \"Connessioni attive\",\n        \"event_manager\": \"Gestione eventi\",\n        \"event_rules\": \"Regole\",\n        \"event_actions\": \"Azioni\",\n        \"ip_manager\": \"Gestione IP\",\n        \"ip_lists\": \"Liste IP\",\n        \"defender\": \"Blocchi automatici\",\n        \"admins\": \"Amministratori\",\n        \"roles\": \"Ruoli\",\n        \"server_manager\": \"Gestione server\",\n        \"configs\": \"Configurazioni\",\n        \"logs\": \"Registro eventi\",\n        \"maintenance\": \"Manutenzione\",\n        \"status\": \"Stato\",\n        \"add_user\": \"Aggiungi utente\",\n        \"update_user\": \"Aggiorna utente\",\n        \"template_user\": \"Modello utente\",\n        \"template_admin\": \"Modello amministratore\",\n        \"add_group\": \"Aggiungi gruppo\",\n        \"update_group\": \"Aggiorna gruppo\",\n        \"add_folder\": \"Aggiungi cartella virtuale\",\n        \"update_folder\": \"Aggiorna cartella virtuale\",\n        \"template_folder\": \"Modello cartella virtuale\",\n        \"oauth2_error\": \"Impossibile completare il flusso OAuth2\",\n        \"oauth2_success\": \"Flusso OAuth2 completato\",\n        \"add_role\": \"Aggiungi ruolo\",\n        \"update_role\": \"Aggiorna ruolo\",\n        \"add_admin\": \"Aggiungi amministratore\",\n        \"update_admin\": \"Aggiorna amministratore\",\n        \"add_ip_list\": \"Aggiungi elemento a lista IP\",\n        \"update_ip_list\": \"Aggiorna elemento lista IP\",\n        \"add_action\": \"Aggiungi azione\",\n        \"update_action\": \"Aggiorna azione\",\n        \"add_rule\": \"Aggiungi regola\",\n        \"update_rule\": \"Aggiorna regola\"\n    },\n    \"setup\": {\n        \"desc\": \"Per iniziare a utilizzare SFTPGo devi creare un utente amministratore\",\n        \"submit\": \"Crea amministratore e accedi\",\n        \"install_code_mismatch\": \"Il codice di installazione non corrisponde\",\n        \"help_text\": \"Supporto commerciale\"\n    },\n    \"login\": {\n        \"username\": \"Nome utente\",\n        \"password\": \"Password\",\n        \"your_username\": \"Il tuo nome utente\",\n        \"forgot_password\": \"Password dimenticata?\",\n        \"forgot_password_msg\": \"Inserisci il nome utente del tuo account qui sotto, riceverai un codice di reimpostazione della password via e-mail\",\n        \"send_reset_code\": \"Invia codice di ripristino\",\n        \"signin\": \"Accedi\",\n        \"signin_openid\": \"Accedi con OpenID\",\n        \"signout\": \"Esci\",\n        \"auth_code\": \"Codice di autenticazione\",\n        \"two_factor_help\": \"Apri l'app di autenticazione a due fattori sul tuo dispositivo per generare il codice e verificare l'identità\",\n        \"two_factor_msg\": \"Inserisci un codice di ripristino per l'autenticazione a due fattori\",\n        \"recovery_code\": \"Codice di ripristino\",\n        \"recovery_code_msg\": \"Puoi inserire uno dei tuoi codici di ripristino nel caso in cui hai perso l'accesso al tuo dispositivo mobile\",\n        \"reset_password\": \"Reimposta password\",\n        \"reset_pwd_msg\": \"Controlla la tua e-mail per il codice di conferma\",\n        \"reset_submit\": \"Aggiorna password e accedi\",\n        \"confirm_code\": \"Codice di conferma\",\n        \"reset_pwd_forbidden\": \"Non ti è consentito reimpostare la password\",\n        \"reset_pwd_no_email\": \"Il tuo account non ha un indirizzo e-mail, non è possibile reimpostare la password inviando un codice di verifica via e-mail\",\n        \"reset_pwd_send_email_err\": \"Errore nell'invio del codice di conferma via e-mail\",\n        \"reset_pwd_err_generic\": \"Errore imprevisto durante la reimpostazione della password\",\n        \"reset_ok_login_error\": \"La reimpostazione della password è stata completata correttamente ma si è verificato un errore imprevisto durante l'accesso\",\n        \"ip_not_allowed\": \"L'accesso non è consentito da questo indirizzo IP\",\n        \"two_factor_required\": \"Configura l'autenticazione a due fattori, è obbligatoria per i seguenti protocolli: {{val}}\",\n        \"two_factor_required_generic\": \"Configura l'autenticazione a due fattori, è obbligatoria per il tuo account\",\n        \"link\": \"Vai a {{link}}\"\n    },\n    \"theme\": {\n        \"light\": \"Chiaro\",\n        \"dark\": \"Scuro\",\n        \"system\": \"Auto\"\n    },\n    \"general\": {\n        \"invalid_auth_request\": \"La richiesta di autenticazione non soddisfa i requisiti di sicurezza\",\n        \"invalid_input\": \"Input non valido. Gli slash (/), i due punti (:), i caratteri di controllo e i nomi di sistema riservati non sono consentiti\",\n        \"error400\": \"La richiesta ricevuta non è valida\",\n        \"error403\": \"Non si dispone delle autorizzazioni richieste\",\n        \"error404\": \"La risorsa richiesta non esiste\",\n        \"error416\": \"Il frammento di file richiesto non può essere restituito\",\n        \"error429\": \"Limite di richieste per unità di tempo superato\",\n        \"error500\": \"Il server non è in grado di soddisfare la tua richiesta\",\n        \"errorPDF\": \"Questo file non sembra un PDF\",\n        \"error_edit_dir\": \"Impossibile modificare una cartella\",\n        \"error_edit_size\": \"La dimensione del file supera la dimensione massima consentita\",\n        \"invalid_form\": \"Modulo non valido\",\n        \"invalid_credentials\": \"Credenziali non valide, riprovare\",\n        \"invalid_csrf\": \"Il token del modulo non è valido\",\n        \"invalid_token\": \"Permessi non validi\",\n        \"confirm_logout\": \"Sei sicuro di volerti disconnettere?\",\n        \"wait\": \"Attendere prego...\",\n        \"ok\": \"OK\",\n        \"failed\": \"Fallito\",\n        \"cancel\": \"No, indietro\",\n        \"submit\": \"Salva\",\n        \"back\": \"Indietro\",\n        \"add\": \"Aggiungi\",\n        \"enable\": \"Abilita\",\n        \"disable\": \"Disabilita\",\n        \"disable_confirm_btn\": \"Si, disabilita\",\n        \"close\": \"Chiudi\",\n        \"search\": \"Cerca\",\n        \"configuration\": \"Configurazione\",\n        \"config_saved\": \"Configurazione salvata\",\n        \"rename\": \"Rinomina\",\n        \"confirm\": \"Si, procedi\",\n        \"edit\": \"Modifica\",\n        \"delete\": \"Elimina\",\n        \"delete_confirm_btn\": \"Si, elimina\",\n        \"delete_confirm\": \"Vuoi eliminare \\\"{{- name}}\\\"? Questa azione è irreversibile\",\n        \"delete_confirm_generic\": \"Vuoi eliminare l'elemento selezionato? Questa azione è irreversibile\",\n        \"delete_multi_confirm\": \"Vuoi eliminare gli elementi selezionati? Questa azione è irreversibile\",\n        \"delete_error_generic\": \"Impossibile eliminare l'elemento selezionato\",\n        \"delete_error_403\": \"$t(general.delete_error_generic). $t(general.error403)\",\n        \"delete_error_404\": \"$t(general.delete_error_generic). $t(general.error404)\",\n        \"loading\": \"Caricamento...\",\n        \"name\": \"Nome\",\n        \"size\": \"Dimensione\",\n        \"last_modified\": \"Ultima modifica\",\n        \"info\": \"Info\",\n        \"datetime\": \"{{- val, datetime}}\",\n        \"selected_items_one\": \"{{count}} elemento selezionato\",\n        \"selected_items_other\": \"{{count}} elementi selezionati\",\n        \"selected_with_placeholder\": \"%d elementi selezionati\",\n        \"name_required\": \"Il nome è obbligatorio\",\n        \"name_different\": \"Il nuovo nome deve essere diverso dal nome attuale\",\n        \"html5_media_not_supported\": \"Il tuo browser non supporta audio/video HTML5.\",\n        \"choose_target_folder\": \"Scegli la cartella di destinazione\",\n        \"source_name\": \"Nome di origine\",\n        \"target_folder\": \"Cartella di destinazione\",\n        \"dest_name\": \"Nome di destinazione\",\n        \"my_profile\": \"Il mio profilo\",\n        \"description\": \"Descrizione\",\n        \"email\": \"E-mail\",\n        \"api_key_auth\": \"Autenticazione mediante chiave API\",\n        \"api_key_auth_help\": \"Permetti di impersonarti nelle API REST utilizzando una chiave API\",\n        \"pub_keys\": \"Chiavi pubbliche\",\n        \"pub_key_placeholder\": \"Incolla qui una chiave pubblica\",\n        \"pub_keys_help\": \"Le chiavi pubbliche possono essere utilizzate per l'autenticazione SFTP\",\n        \"verify\": \"Verifica\",\n        \"problems\": \"Hai problemi?\",\n        \"allowed_ip_mask\": \"IP/Reti permesse\",\n        \"denied_ip_mask\": \"IP/Reti non permesse\",\n        \"ip_mask_help\": \"IP/reti separate da virgola in formato CIDR, ad esempio \\\"192.168.1.0/24,10.8.0.100/32\\\"\",\n        \"allowed_ip_mask_invalid\": \"IP/reti permesse non valide\",\n        \"username_required\": \"Il nome utente è obbligatorio\",\n        \"password_required\": \"La password è obbligatoria\",\n        \"foldername_required\": \"Il nome della cartella è obbligatorio\",\n        \"err_user\": \"Errore validazione utente\",\n        \"err_protocol_forbidden\": \"Il protocollo HTTP non è consentito per il tuo utente\",\n        \"pwd_login_forbidden\": \"Il metodo di accesso tramite password non è consentito per il tuo utente\",\n        \"ip_forbidden\": \"Accesso non permesso da questo indirizzo IP\",\n        \"email_invalid\": \"L'indirizzo e-mail non è valido\",\n        \"err_password_complexity\": \"La password fornita non soddisfa i requisiti di complessità\",\n        \"no_oidc_feature\": \"Questa funzionalità non è disponibile se hai effettuato l'accesso con OpenID\",\n        \"connection_forbidden\": \"Non ti è consentito connetterti\",\n        \"no_permissions\": \"Non ti è consentito cambiare nulla\",\n        \"path_invalid\": \"Percorso non valido\",\n        \"err_quota_read\": \"Lettura negata a causa del limite di quota\",\n        \"profile_updated\": \"Il tuo profilo è stato aggiornato con successo\",\n        \"share_ok\": \"Accesso alla condivisione riuscito, ora puoi utilizzare il tuo collegamento\",\n        \"qr_code\": \"Codice QR\",\n        \"copy_link\": \"Copia collegamento\",\n        \"copied\": \"Copiato\",\n        \"active\": \"Attivo\",\n        \"inactive\": \"Inattivo\",\n        \"colvis\": \"Visibilità colonne\",\n        \"actions\": \"Azioni\",\n        \"template\": \"Usa come modello\",\n        \"quota_scan\": \"Ricalcolo quota\",\n        \"quota_scan_started\": \"Ricalcolo quota avviato. Potrebbe richiedere del tempo a seconda del numero di file da controllare\",\n        \"quota_scan_conflit\": \"Un'altra scansione è già in corso\",\n        \"quota_scan_error\": \"Impossibile avviare ricalcolo quota\",\n        \"role\": \"Ruolo\",\n        \"role_placeholder\": \"Seleziona un ruolo\",\n        \"group_placeholder\": \"Seleziona un gruppo\",\n        \"folder_placeholder\": \"Seleziona una cartella\",\n        \"blank_default_help\": \"Lasciare vuoto per l'impostazione predefinita\",\n        \"skip_tls_verify\": \"Salta la verifica TLS. Dovrebbe essere usato solo per test\",\n        \"advanced_settings\": \"Impostazioni avanzate\",\n        \"default\": \"Predefinito\",\n        \"private_key\": \"Chiave privata\",\n        \"acls\": \"ACL\",\n        \"quota_limits\": \"Quota disco e limitazioni di larghezza di banda\",\n        \"expiration\": \"Scadenza\",\n        \"expiration_help\": \"Scegli una data di scadenza\",\n        \"additional_info\": \"Informazioni aggiuntive\",\n        \"permissions\": \"Permessi\",\n        \"visible\": \"Visibile\",\n        \"hidden\": \"Nascosto\",\n        \"allowed\": \"Permesso\",\n        \"denied\": \"Negato\",\n        \"zero_no_limit_help\": \"0 significa nessun limite\",\n        \"global_settings\": \"Impostazioni globali\",\n        \"mandatory_encryption\": \"Crittografia obbligatoria\",\n        \"name_invalid\": \"Il nome specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~\",\n        \"associations\": \"Associazioni\",\n        \"template_placeholders\": \"Sono supportati i seguenti segnaposto\",\n        \"duplicated_username\": \"Il nome utente specificato esiste già\",\n        \"duplicated_name\": \"Il nome specificato esiste già\",\n        \"permissions_required\": \"I permessi sono obbligatori\",\n        \"configs_saved\": \"Configurazioni aggiornate\",\n        \"protocol\": \"Protocollo\",\n        \"refresh\": \"Aggiorna\",\n        \"members\": \"Membri\",\n        \"members_summary\": \"Utenti: {{users}}. Amministratori: {{admins}}\",\n        \"status\": \"Stato\",\n        \"last_login\": \"Ultimo accesso\",\n        \"previous\": \"Precedente\",\n        \"next\": \"Successivo\",\n        \"type\": \"Tipo\",\n        \"issuer\": \"Emittente\",\n        \"data_provider\": \"Database\",\n        \"driver\": \"Driver\",\n        \"mode\": \"Modalità\",\n        \"port\": \"Porta\",\n        \"domain\": \"Dominio\",\n        \"test\": \"Test\",\n        \"get\": \"Ottieni\",\n        \"export\": \"Esporta\",\n        \"value\": \"Valore\",\n        \"method\": \"Metodo\",\n        \"timeout\": \"Timeout\",\n        \"env_vars\": \"Variabili d'ambiente\",\n        \"hours\": \"Ore\",\n        \"paths\": \"Percorsi\",\n        \"hour\": \"Ora\",\n        \"day_of_week\": \"Giorno settimana\",\n        \"day_of_month\": \"Giorno mese\",\n        \"month\": \"Mese\",\n        \"options\": \"Opzioni\",\n        \"expired\": \"Scaduto\",\n        \"unsupported\": \"Funzionalità non più supportata\",\n        \"start\": \"Inizio (HH:MM)\",\n        \"end\": \"Fine (HH:MM)\",\n        \"monday\": \"Lunedì\",\n        \"tuesday\": \"Martedì\",\n        \"wednesday\": \"Mercoledì\",\n        \"thursday\": \"Giovedì\",\n        \"friday\": \"Venerdì\",\n        \"saturday\": \"Sabato\",\n        \"sunday\": \"Domenica\",\n        \"expired_session\": \"La tua sessione è scaduta. Effettua nuovamente l'accesso\"\n    },\n    \"fs\": {\n        \"view_file\": \"Visualizza file \\\"{{- path}}\\\"\",\n        \"edit_file\": \"Modifica file \\\"{{- path}}\\\"\",\n        \"new_folder\": \"Nuova cartella\",\n        \"select_across_pages\": \"Seleziona tra le pagine\",\n        \"download\": \"Scarica\",\n        \"download_ready\": \"Il tuo download è pronto\",\n        \"upload_queue_not_ready\": \"La coda di caricamento è in fase di elaborazione, riprova tra qualche istante\",\n        \"upload_no_files\": \"Nessun file selezionato. Scegli o trascina i file da caricare\",\n        \"move_copy\": \"Sposta o copia\",\n        \"share\": \"Condividi\",\n        \"home\": \"Home\",\n        \"create_folder_msg\": \"Nome cartella\",\n        \"no_more_subfolders\": \"Non ci sono più sottocartelle qui\",\n        \"no_files_folders\": \"Nessun file o cartella\",\n        \"invalid_name\": \"I nomi di file o cartelle non possono contenere \\\"/\\\"\",\n        \"folder_name_required\": \"Il nome della cartella è obbligatorio\",\n        \"deleting\": \"Eliminazione {{idx}}/{{total}}\",\n        \"copying\": \"Copia {{idx}}/{{total}}\",\n        \"moving\": \"Spostamento {{idx}}/{{total}}\",\n        \"uploading\": \"Caricamento {{idx}}/{{total}}\",\n        \"err_403\": \"Non si dispone delle autorizzazioni richieste\",\n        \"err_429\": \"Troppe richieste contemporanee\",\n        \"err_generic\": \"Impossibile accedere alla risorsa richiesta\",\n        \"err_validation\": \"Configurazione del filesystem non valida\",\n        \"err_exists\": \"La destinazione esiste già\",\n        \"dir_list\": {\n            \"err_generic\": \"Non è possibile visualizzare i contenuti della cartella\",\n            \"err_403\": \"$t(fs.dir_list.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.dir_list.err_generic). $t(fs.err_429)\",\n            \"err_user\": \"$t(fs.dir_list.err_generic). $t(general.err_user)\"\n        },\n        \"create_dir\": {\n            \"err_generic\": \"Errore durante la creazione delle nuova cartella\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"save\": {\n            \"err_generic\": \"Errore durante il salvataggio del file\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"delete\": {\n            \"err_generic\": \"Impossibile eliminare \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.delete.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete.err_generic). $t(fs.err_429)\"\n        },\n        \"delete_multi\": {\n            \"err_generic_partial\": \"Non tutti gli elementi selezionati sono stati eliminati, ricarica la pagina\",\n            \"err_generic\": \"Impossibile eliminare gli elementi selezionati\",\n            \"err_403_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)\",\n            \"err_429_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)\",\n            \"err_403\": \"$t(fs.delete_multi.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete_multi.err_generic). $t(fs.err_429)\"\n        },\n        \"copy\": {\n            \"msg\": \"Copia\",\n            \"err_generic\": \"Errore copia file/cartelle\",\n            \"err_403\": \"$t(fs.copy.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.copy.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.copy.err_generic). $t(fs.err_exists)\"\n        },\n        \"move\": {\n            \"msg\": \"Sposta\",\n            \"err_generic\": \"Errore nello spostamento di file/directory\",\n            \"err_403\": \"$t(fs.move.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.move.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.move.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"Non supportato: se vuoi spostare una directory assicurati che sia vuota\"\n        },\n        \"rename\": {\n            \"title\": \"Rinomina \\\"{{- name}}\\\"\",\n            \"new_name\": \"Nuovo nome\",\n            \"err_generic\": \"Impossibile rinominare \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.rename.err_generic): $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.rename.err_generic): $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.rename.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"Non supportato: se vuoi rinominare una directory assicurati che sia vuota\"\n        },\n        \"upload\": {\n            \"text\": \"Carica file\",\n            \"success\": \"File caricati correttamente\",\n            \"message\": \"Rilascia i file qui o fai clic per caricarli.\",\n            \"message_empty\": \"Questa cartella è vuota. $t(fs.upload.message)\",\n            \"err_generic\": \"Errore caricamento file\",\n            \"err_403\": \"$t(fs.upload.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.upload.err_generic). $t(fs.err_429)\",\n            \"err_dir_overwrite\": \"$t(fs.upload.err_generic). Sono presenti cartelle con lo stesso nome dei file: {{- val}}\",\n            \"overwrite_text\": \"Rilevato conflitto. Vuoi sovrascrivere i seguenti file/cartelle?\"\n        },\n        \"quota_usage\": {\n            \"title\": \"Utilizzo quota\",\n            \"disk\": \"Quota disco\",\n            \"size\": \"Dimensione: {{- val}}\",\n            \"size_percentage\": \"Dimensione: {{- val}} ({{percentage}}%)\",\n            \"files\": \"File: {{- val}}\",\n            \"files_percentage\": \"File: {{- val}} ({{percentage}}%)\",\n            \"transfer\": \"Quota trasferimenti\",\n            \"total\": \"Totale: {{- val}}\",\n            \"total_percentage\": \"Totale: {{- val}} ({{percentage}}%)\",\n            \"uploads\": \"Caricamenti: {{- val}}\",\n            \"uploads_percentage\": \"Caricamenti: {{- val}} ({{percentage}}%)\",\n            \"downloads\": \"Download: {{- val}}\",\n            \"downloads_percentage\": \"Download: {{- val}} ({{percentage}}%)\"\n        }\n    },\n    \"datatable\": {\n        \"info\": \"Risultati da _START_ a _END_ di _TOTAL_ elementi\",\n        \"info_empty\": \"Nulla da mostrare\",\n        \"info_filtered\": \"(filtrati da _MAX_ elementi totali)\",\n        \"processing\": \"Elaborazione...\",\n        \"no_records\": \"Nessun risultato\"\n    },\n    \"editor\": {\n        \"keybinding\": \"Combinazioni di tasti dell'editor\",\n        \"search\": \"Apre pannello di ricerca\",\n        \"goto\": \"Vai a linea\",\n        \"indent_more\": \"Aumenta indentazione\",\n        \"indent_less\": \"Diminuisci indentazione\"\n    },\n    \"2fa\": {\n        \"title\": \"Autenticazione a due fattori utilizzando App di autenticazione\",\n        \"msg_enabled\": \"L'autenticazione a due fattori è abilitata\",\n        \"msg_disabled\": \"Metti il tuo account in sicurezza\",\n        \"msg_info\": \"L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account. Per accedere dovrai fornire un ulteriore codice di autenticazione.\",\n        \"require\": \"L'autenticazione a due fattori è obbligatoria per questo account\",\n        \"require_for\": \"Richiedi 2FA per\",\n        \"generate\": \"Genera una nuova chiave segreta\",\n        \"recovery_codes\": \"Codici di ripristino\",\n        \"new_recovery_codes\": \"Nuovi codici di ripristino\",\n        \"recovery_codes_msg1\": \"I codici di ripristino sono un insieme di codici monouso che possono essere utilizzati al posto del codice di autenticazione per accedere all'interfaccia utente Web. Puoi utilizzarli se perdi l'accesso al tuo telefono per accedere al tuo account e disabilitare o rigenerare la configurazione dell'autenticazione a due fattori.\",\n        \"recovery_codes_msg2\": \"Per mantenere sicuro il tuo account, non condividere o distribuire i tuoi codici di ripristino. Ti consigliamo di salvarli con un gestore di password sicuro.\",\n        \"recovery_codes_msg3\": \"Se generi nuovi codici di ripristino, invalidi automaticamente quelli vecchi.\",\n        \"info_title\": \"Ulteriori informazioni sull'autenticazione a due fattori\",\n        \"info1\": \"Il protocollo SSH (comandi SFTP/SCP/SSH) richiederà il passcode se il client utilizza l'autenticazione interattiva da tastiera.\",\n        \"info2\": \"Per protocollo HTTP si intende l'interfaccia utente Web e le API REST. L'interfaccia utente Web richiederà il passcode utilizzando una pagina specifica. Per l'API REST devi aggiungere il passcode utilizzando un'intestazione HTTP.\",\n        \"info3\": \"FTP non ha un modo standard per supportare l'autenticazione a due fattori, se abiliti il supporto FTP, devi aggiungere il passcode TOTP dopo la password. Ad esempio, se la tua password è \\\"password\\\" e il tuo passcode monouso è \\\"123456\\\", devi utilizzare \\\"password123456\\\" come password.\",\n        \"info4\": \"WebDAV non è supportato poiché ogni singola richiesta deve essere autenticata e un passcode non può essere riutilizzato.\",\n        \"setup_title\": \"Configura l'autenticazione a due fattori\",\n        \"setup_msg\": \"Scansiona il codice QR con un'app di autenticazione (es. Google Authenticator, Authy, 1Password). Verrà generato un codice di autenticazione da inserire di seguito.\",\n        \"setup_help\": \"Se hai problemi con l'utilizzo del codice QR, seleziona l'inserimento manuale sulla tua app e inserisci il codice:\",\n        \"disable_msg\": \"Disabilita 2FA\",\n        \"disable_confirm\": \"Vuoi disabilitare l'autenticazione a due fattori per l'utente selezionato? Questa azione è generalmente necessaria solo se l'utente ha perso l'accesso al secondo fattore di autenticazione\",\n        \"disable_question\": \"Vuoi disattivare l'autenticazione a due fattori?\",\n        \"generate_question\": \"Vuoi generare un nuova chiave segreta e invalidare quella precedente? Qualsiasi app di autenticazione registrata smetterà di funzionare\",\n        \"disabled\": \"L'autenticazione a due fattori è disabilitata\",\n        \"recovery_codes_gen_err\": \"Impossibile generare nuovi codici di ripristino\",\n        \"recovery_codes_get_err\": \"Impossibile ottenere i codici di ripristino\",\n        \"auth_code_invalid\": \"Impossibile convalidare il codice di autenticazione fornito\",\n        \"auth_secret_gen_err\": \"Impossibile generare il segreto di autenticazione\",\n        \"save_err\": \"Impossibile salvare la configurazione dell'autenticazione a due fattori\",\n        \"auth_code_required\": \"Il codice di autenticazione è obbligatorio\",\n        \"no_protocol\": \"Seleziona almeno un protocollo\",\n        \"required_protocols\": \"La politica di sicurezza configurata per il tuo account richiede l'autenticazione a due fattori per i seguenti protocolli: {{val}}\",\n        \"recovery_codes_generate\": \"Genera nuovi codici di ripristino\",\n        \"recovery_codes_view\": \"Visualizza codici di ripristino\"\n    },\n    \"share\": {\n        \"scope\": \"Ambito\",\n        \"scope_read\": \"Lettura\",\n        \"scope_write\": \"Scrittura\",\n        \"scope_read_write\": \"Lettura/Scrittura\",\n        \"scope_help\": \"Per gli ambiti \\\"Scrittura\\\" e \\\"Lettura/Scrittura\\\" è necessario specificare un unico percorso, che deve essere una cartella\",\n        \"path_help\": \"percorso di un file o di una directory, ad esempio /dir o /dir/file.txt\",\n        \"password_help\": \"Se specificata, l'accesso alla condivisione richiederà una password\",\n        \"max_tokens\": \"Token massimi\",\n        \"max_tokens_help\": \"Numero massimo di volte in cui è possibile accedere a questa condivisione. 0 significa nessun limite\",\n        \"view_manage\": \"Visualizza e gestisci condivisioni\",\n        \"no_share\": \"Nessuna condivisione\",\n        \"password_protected\": \"Protetto da password.\",\n        \"expiration_date\": \"Scadenza: {{- val, datetime}}. \",\n        \"last_use\": \"Ultimo utilizzo: {{- val, datetime}}. \",\n        \"usage\": \"Utilizzo: {{used}}/{{total}}. \",\n        \"used_tokens\": \"Token usati: {{used}}. \",\n        \"scope_invalid\": \"Ambito non valido\",\n        \"max_tokens_invalid\": \"Token massimi non validi\",\n        \"expiration_invalid\": \"Scadenza non valida\",\n        \"err_no_password\": \"Non sei autorizzato a condividere file/cartelle senza password\",\n        \"expiration_out_of_range\": \"Imposta una data di scadenza e assicurati che sia inferiore o uguale al {{- val, datetime}}\",\n        \"generic\": \"Errore imprevisto durante il salvataggio della condivisione\",\n        \"path_required\": \"È necessario almeno un percorso\",\n        \"path_write_scope\": \"L'ambito di scrittura richiede esattamente un percorso\",\n        \"nested_paths\": \"I percorsi non possono essere contenuti l'uno dentro l'altro\",\n        \"expiration_past\": \"La data di scadenza deve essere futura\",\n        \"usage_exceed\": \"Utilizzo massimo della condivisione superato\",\n        \"expired\": \"La condivisione è scaduta\",\n        \"browsable_multiple_paths\": \"Una condivisione con più percorsi non è navigabile\",\n        \"browsable_non_dir\": \"La condivisione non è una directory quindi non è navigabile\",\n        \"go\": \"Vai alla condivisione\",\n        \"access_links_title\": \"Accesso alla condivisione\",\n        \"link_single_title\": \"File zip singolo\",\n        \"link_single_desc\": \"È possibile scaricare il contenuto condiviso come un singolo file zip\",\n        \"link_dir_title\": \"Directory singola\",\n        \"link_dir_desc\": \"Se la condivisione è costituita da un'unica directory, è possibile navigarla e scaricare file\",\n        \"link_uncompressed_title\": \"File non compresso\",\n        \"link_uncompressed_desc\": \"Se la condivisione è costituita da un unico file è possibile scaricarlo anche non compresso\",\n        \"upload_desc\": \"È possibile caricare uno o più file nella directory condivisa\",\n        \"expired_desc\": \"Questa condivisione non è più accessibile perché è scaduta\",\n        \"invalid_path\": \"La directory condivisa manca o non è accessibile\"\n    },\n    \"select2\": {\n        \"no_results\": \"Nessun risultato trovato\",\n        \"searching\": \"Ricerca...\",\n        \"removeall\": \"Rimuovi tutti gli elementi\",\n        \"remove\": \"Rimuovi elemento\"\n    },\n    \"change_pwd\": {\n        \"info\": \"Inserisci la password attuale, per ragioni di sicurezza, e poi la nuova password due volte, per verificare di averla scritta correttamente. Verrai disconnesso dopo aver modificato la tua password\",\n        \"current\": \"Password attuale\",\n        \"new\": \"Nuova password\",\n        \"confirm\": \"Conferma password\",\n        \"save\": \"Modifica la mia password\",\n        \"required_fields\": \"Si prega di fornire la password attuale e quella nuova due volte\",\n        \"no_match\": \"I due campi della password non corrispondono\",\n        \"no_different\": \"La nuova password deve essere diversa da quella attuale\",\n        \"current_no_match\": \"La password attuale non corrisponde\",\n        \"generic\": \"Errore imprevisto durante la modifica della password\",\n        \"required\": \"È richiesta la modifica della password. Imposta una nuova password per continuare a utilizzare il tuo account\"\n    },\n    \"user\": {\n        \"view_manage\": \"Visualizza e gestisci utenti\",\n        \"username_reserved\": \"Il nome utente specificato è riservato\",\n        \"username_invalid\": \"Il nome specificato non è valido. Sono consentiti i seguenti caratteri: a-z A-Z 0-9 - _ . ~\",\n        \"home_required\": \"La directory principale è obbligatoria\",\n        \"home_invalid\": \"La directory principale deve essere un path assoluto\",\n        \"pub_key_invalid\": \"Chiave pubblica non valida\",\n        \"priv_key_invalid\": \"Chiave privata non valida\",\n        \"key_invalid_size\": \"Chiave pubblica RSA non valida: la dimensione minima supportata è 2048\",\n        \"key_insecure\": \"Formato chiave pubblica non sicuro non consentito\",\n        \"err_primary_group\": \"È consentito un solo gruppo primario\",\n        \"err_duplicate_group\": \"Rilevati gruppi duplicati\",\n        \"no_permissions\": \"I permessi per le directory sono obbligatori\",\n        \"no_root_permissions\": \"I permessi per la directory principale sono obbligatori\",\n        \"err_permissions_generic\": \"Permessi non validi: assicurati di utilizzare percorsi assoluti validi\",\n        \"2fa_invalid\": \"Configurazione non valida per l'autenticazione a due fattori\",\n        \"recovery_codes_invalid\": \"Codici di ripristino non validi\",\n        \"folder_path_required\": \"Il percorso di montaggio delle cartelle virtuali è obbligatorio\",\n        \"folder_duplicated\": \"Rilevate cartelle virtuali duplicate\",\n        \"folder_overlapped\": \"Rilevate cartelle virtuali sovrapposte\",\n        \"folder_quota_size_invalid\": \"La quota come dimensione delle cartelle virtuali deve essere maggiore o uguale a -1\",\n        \"folder_quota_file_invalid\": \"La quota come numero di file delle cartelle virtuali deve essere maggiore o uguale a -1\",\n        \"folder_quota_invalid\": \"Le quote come dimensione e numero di file delle cartelle virtuali devono essere entrambe -1 o maggiore o uguale di 0\",\n        \"ip_filters_invalid\": \"Filtri IP non validi, assicurati che rispettino la notazione CIDR, ad esempio 192.168.1.0/24\",\n        \"src_bw_limits_invalid\": \"Limiti di velocità della larghezza di banda per origine non validi\",\n        \"share_expiration_invalid\": \"La scadenza per le condivisioni deve essere maggiore o uguale al valore di default definito\",\n        \"file_pattern_path_invalid\": \"I percorsi dei filtri sui modelli di nome file devono essere assoluti\",\n        \"file_pattern_duplicated\": \"Rilevati filtri su modelli di nome file duplicati\",\n        \"file_pattern_invalid\": \"Filtri su modelli di nome file non validi\",\n        \"disable_active_2fa\": \"L'autenticazione a due fattori non può essere disabilitata per un utente con una configurazione attiva\",\n        \"pwd_change_conflict\": \"Non è possibile richiedere la modifica della password e allo stesso tempo impedire la modifica della password\",\n        \"two_factor_conflict\": \"Non è possibile richiedere l'autenticazione a due fattori e allo stesso tempo impedire che venga configurata\",\n        \"role_help\": \"Gli utenti con un ruolo possono essere gestiti da amministratori globali e amministratori con lo stesso ruolo\",\n        \"require_pwd_change\": \"Richiedi modifica password\",\n        \"require_pwd_change_help\": \"L'utente dovrà modificare la password dal WebClient per attivare l'account\",\n        \"groups_help\": \"L'appartenenza ai gruppi conferisce le impostazioni dei gruppi ad eccezione dei gruppi di sola appartenenza\",\n        \"primary_group\": \"Gruppo primario\",\n        \"secondary_groups\": \"Gruppi secondari\",\n        \"membership_groups\": \"Gruppi di appartenenza\",\n        \"template_help\": \"Per ogni utente impostare il nome utente e almeno una tra password e chiave pubblica\",\n        \"virtual_folders_help\": \"Dimensione quota/numero file -1 significa incluso nella quota utente, 0 illimitato. Non impostare -1 per le cartelle condivise. È possibile utilizzare il suffisso MB/GB/TB. Senza suffisso assumiamo byte\",\n        \"disconnect\": \"Disconnettere l'utente dopo l'aggiornamento\",\n        \"disconnect_help\": \"In questo modo si obbliga l'utente a effettuare nuovamente il login, se connesso, e quindi ad utilizzare la nuova configurazione\",\n        \"invalid_quota_size\": \"Quota di spazio (dimensione) non valida\",\n        \"expires_in\": \"Scadenza\",\n        \"expires_in_help\": \"Scadenza dell'account espressa in numero di giorni dalla creazione. 0 significa nessuna scadenza\",\n        \"additional_emails\": \"E-mail aggiuntive\",\n        \"tls_certs\": \"Certificati TLS\",\n        \"tls_certs_help\": \"I certificati TLS possono essere utilizzati per l'autenticazione FTP e/o WebDAV\",\n        \"tls_cert_help\": \"Incolla qui un tuo certificato TLS codificato PEM\",\n        \"tls_cert_invalid\": \"Certificato TLS non valido\",\n        \"template_title\": \"Crea uno o più nuovi utenti da questo modello\",\n        \"template_username_placeholder\": \"sostituito con il nome utente specificato\",\n        \"template_password_placeholder\": \"sostituito con la password specificata\",\n        \"template_help1\": \"I segnaposto verranno sostituiti nei percorsi e nelle credenziali del backend di archiviazione configurato.\",\n        \"template_help2\": \"Gli utenti generati possono essere salvati o esportati. Gli utenti esportati possono essere importati dalla sezione \\\"Manutenzione\\\" di questa istanza SFTPGo o di un'altra.\",\n        \"template_no_user\": \"Nessun utente valido definito. Impossibile completare l'azione richiesta\",\n        \"time_of_day_invalid\": \"Ora del giorno non valida. Formato supportato HH:MM\",\n        \"time_of_day_conflict\": \"Ora del giorno non valida. L'ora di fine non può essere precedente all'ora di inizio\"\n    },\n    \"group\": {\n        \"view_manage\": \"Visualizza e gestisci gruppi\",\n        \"err_delete_referenced\": \"Impossibile eliminare un gruppo con utenti associati, rimuovere prima le associazioni\",\n        \"help\": \"Il segnaposto %username% verrà sostituito con il nome utente degli utenti associati, %role% con il loro ruolo\"\n    },\n    \"virtual_folders\": {\n        \"view_manage\": \"Visualizza e gestisci cartelle virtuali\",\n        \"mount_path\": \"percorso, es. /vfolder\",\n        \"quota_size\": \"Quota (dimensione)\",\n        \"quota_size_help\": \"0 significa nessun limite. E' possibile utilizzare il suffisso MB/GB/TB\",\n        \"quota_files\": \"Quota (numero file)\",\n        \"associations_summary\": \"Utenti: {{users}}. Gruppi: {{groups}}\",\n        \"template_title\": \"Crea una o più nuove cartelle virtuali da questo modello\",\n        \"template_name_placeholder\": \"sostituito con il nome della cartella virtuale specificata\",\n        \"template_help\": \"Le cartelle virtuali generate possono essere salvate o esportate. Le cartelle esportate possono essere importate dalla sezione \\\"Manutenzione\\\" di questa istanza SFTPGo o di un'altra.\",\n        \"name\": \"Nome cartella virtuale\",\n        \"template_no_folder\": \"Nessuna cartella virtuale valida definita. Impossibile completare l'azione richiesta\"\n    },\n    \"storage\": {\n        \"title\": \"File System\",\n        \"label\": \"Archiviazione\",\n        \"local\": \"Disco locale\",\n        \"s3\": \"S3 (Compatibile)\",\n        \"gcs\": \"GCS\",\n        \"azblob\": \"Azure Blob\",\n        \"encrypted\": \"Disco locale criptato\",\n        \"sftp\": \"SFTP\",\n        \"http\": \"HTTP\",\n        \"home_dir\": \"Cartella principale\",\n        \"home_dir_placeholder\": \"Percorso assoluto di una directory su disco locale\",\n        \"home_dir_help1\": \"Lasciare vuoto per un valore predefinito appropriato\",\n        \"home_dir_help2\": \"Lascia vuoto e archiviazione su \\\"Disco locale\\\" per non sovrascrivere la directory principale\",\n        \"home_dir_help3\": \"Obbligatorio per i provider di archiviazione su disco locale. Per gli altri provider di archiviazione questa cartella sarà usata per i file temporanei, puoi lasciare vuoto per un valore predefinito appropriato\",\n        \"home_dir_invalid\": \"Cartella principale non valida, assicurati che sia un path assoluto\",\n        \"sftp_home_dir\": \"Cartella principale SFTP\",\n        \"sftp_home_help\": \"Limitare l'accesso a questo percorso SFTP. Esempio: \\\"/somedir/subdir\\\"\",\n        \"os_read_buffer\": \"Buffer download (MB)\",\n        \"os_buffer_help\": \"0 significa nessun buffer\",\n        \"os_write_buffer\": \"Buffer upload (MB)\",\n        \"bucket\": \"Bucket\",\n        \"region\": \"Regione\",\n        \"access_key\": \"Chiave di accesso\",\n        \"access_secret\": \"Chiave di accesso segreta\",\n        \"sse_customer_key\": \"Chiave di crittografia\",\n        \"sse_customer_key_help\": \"Puoi archiviare i tuoi dati crittografati con questa chiave, ma se perdi o modifichi inavvertitamente questa chiave, perderai tutti i file crittografati con essa. I file non crittografati o crittografati con una chiave diversa non saranno accessibili\",\n        \"endpoint\": \"Endpoint\",\n        \"endpoint_help\": \"Per AWS S3, lasciare vuoto per utilizzare l'endpoint predefinito per la regione specificata\",\n        \"sftp_endpoint_help\": \"Endpoint come host:porta. La porta è sempre richiesta\",\n        \"ul_part_size\": \"Dimensione parte per upload (MB)\",\n        \"part_size_help\": \"0 significa il default (5 MB). Il minimo è 5\",\n        \"gcs_part_size_help\": \"0 significa il default (16 MB)\",\n        \"ul_concurrency\": \"Concorrenza upload\",\n        \"ul_concurrency_help\": \"Numero di parti caricate in parallelo. 0 significa il default (5)\",\n        \"dl_part_size\": \"Dimensione parte per download (MB)\",\n        \"dl_concurrency\": \"Concorrenza download\",\n        \"dl_concurrency_help\": \"Numero di parti scaricate in parallelo. 0 significa il default (5)\",\n        \"ul_part_timeout\": \"Timeout per upload parte\",\n        \"ul_part_timeout_help\": \"Limite, in secondi, per caricare una singola parte. 0 significa nessun limite\",\n        \"gcs_ul_part_timeout_help\": \"Limite, in secondi, per caricare una singola parte. 0 significa il default (32)\",\n        \"dl_part_timeout\": \"Timeout per download parte\",\n        \"dl_part_timeout_help\": \"Limite, in secondi, per scaricare una singola parte. 0 significa nessun limite\",\n        \"key_prefix\": \"Prefisso chiave\",\n        \"key_prefix_help\": \"Limitare l'accesso alle chiavi con il prefisso specificato. Esempio: \\\"somedir/subdir/\\\"\",\n        \"class\": \"Classe archiviazione\",\n        \"acl\": \"ACL\",\n        \"role_arn\": \"Ruolo ARN\",\n        \"role_arn_help\": \"ARN del ruolo IAM da assumere (opzionale)\",\n        \"s3_path_style\": \"Utilizza l'indirizzamento in stile percorso, ad esempio \\\"endpoint/BUCKET/KEY\\\"\",\n        \"credentials_file\": \"File delle credenziali\",\n        \"credentials_file_help\": \"Aggiungi o aggiorna le credenziali da un file JSON\",\n        \"auto_credentials\": \"Credenziali automatiche\",\n        \"auto_credentials_help\": \"Utilizza le credenziali predefinite dell'applicazione o le credenziali dalle variabili di ambiente\",\n        \"container\": \"Contenitore\",\n        \"account_name\": \"Nome account\",\n        \"account_key\": \"Chiave di accesso\",\n        \"sas_url\": \"URL SAS\",\n        \"sas_url_help\": \"È possibile utilizzare l'URL SAS al posto del nome account e chiave di accesso\",\n        \"emulator\": \"Utilizza emulatore\",\n        \"passphrase\": \"Passphrase\",\n        \"passphrase_help\": \"Passphrase usata per derivare la chiave di crittografia per oggetto\",\n        \"passphrase_key_help\": \"Passphrase utilizzata per proteggere la tua chiave privata, se necessaria\",\n        \"fingerprints\": \"Impronte chiavi\",\n        \"fingerprints_help\": \"Impronte SHA256 da convalidare durante la connessione al server SFTP esterno, una per linea. Se vuoto verrà accettata qualsiasi chiave host: questo è un rischio per la sicurezza!\",\n        \"sftp_buffer\": \"Dimensione buffer (MB)\",\n        \"sftp_buffer_help\": \"Un buffer maggiore di 0 abilita i trasferimenti concorrenti\",\n        \"sftp_concurrent_reads\": \"Disabilitare letture concorrenti\",\n        \"relaxed_equality_check\": \"Controllo di uguaglianza non rigoroso\",\n        \"relaxed_equality_check_help\": \"Abilitare per considerare solo l'endpoint per determinare se diverse configurazioni puntano allo stesso server. Per impostazione predefinita, sia l'endpoint che il nome utente devono corrispondere\",\n        \"api_key\": \"Chiave API\",\n        \"fs_error\": \"Errore configurazione filesystem\",\n        \"bucket_required\": \"$t(storage.fs_error): il bucket è obbligatorio\",\n        \"region_required\": \"$t(storage.fs_error): la regione è obbligatoria\",\n        \"key_prefix_invalid\": \"$t(storage.fs_error): prefisso chiave non valido, non può iniziare con \\\"/\\\"\",\n        \"ul_part_size_invalid\": \"$t(storage.fs_error): dimensione parte per upload non valida\",\n        \"ul_concurrency_invalid\": \"$t(storage.fs_error): concorrenza upload non valida\",\n        \"dl_part_size_invalid\": \"$t(storage.fs_error): dimensione parte per download non valida\",\n        \"dl_concurrency_invalid\": \"$t(storage.fs_error): concorrenza download non valida\",\n        \"access_key_required\": \"$t(storage.fs_error): la chiave di accesso è obbligatoria\",\n        \"access_secret_required\": \"$t(storage.fs_error): la chiave di accesso segreta è obbligatoria\",\n        \"credentials_required\": \"$t(storage.fs_error): le credenziali per il filesystem sono obbligatorie\",\n        \"container_required\": \"$t(storage.fs_error): il contenitore è obbligatorio\",\n        \"account_name_required\": \"$t(storage.fs_error): il nome account è obbligatorio\",\n        \"sas_url_invalid\": \"$t(storage.fs_error): SAS URL non valido\",\n        \"passphrase_required\": \"$t(storage.fs_error): la passphrase è obbligatoria\",\n        \"endpoint_invalid\": \"$t(storage.fs_error): endpoint non valido\",\n        \"endpoint_required\": \"$t(storage.fs_error): endpoint è obbligatorio\",\n        \"username_required\": \"$t(storage.fs_error): nome utente è obbligatorio\"\n    },\n    \"oidc\": {\n        \"token_expired\": \"Il tuo token OpenID è scaduto, effettua nuovamente l'accesso\",\n        \"token_invalid_webadmin\": \"Il tuo token OpenID non è valido per l'interfaccia utente WebAdmin. Esci dal tuo server OpenID e accedi a WebAdmin\",\n        \"token_invalid_webclient\": \"Il tuo token OpenID non è valido per l'interfaccia utente WebClient. Esci dal tuo server OpenID e accedi al WebClient\",\n        \"token_exchange_err\": \"Impossibile scambiare il token OpenID\",\n        \"token_invalid\": \"Token OpenID non valido\",\n        \"role_admin_err\": \"Ruolo OpenID errato, l'utente che ha effettuato l'accesso non è un amministratore\",\n        \"role_user_err\": \"Ruolo OpenID errato, l'utente che ha effettuato l'accesso è un amministratore\",\n        \"get_user_err\": \"Impossibile ottenere l'utente associato al token OpenID\"\n    },\n    \"oauth2\": {\n        \"auth_verify_error\": \"Impossibile verificare il codice OAuth2\",\n        \"auth_validation_error\": \"Impossibile validare il codice OAuth2\",\n        \"auth_invalid\": \"Codice OAuth2 non valido\",\n        \"token_exchange_err\": \"Impossibile ottenere il token OAuth2 dal codice di autorizzazione\",\n        \"no_refresh_token\": \"Il provider OAuth2 ha restituito un token vuoto. Alcuni provider restituiscono il token solo dopo la prima autorizzazione dell'utente. Se hai già registrato SFTPGo con questo utente in passato, revoca l'accesso e riprova. In questo modo invaliderai il token precedente\",\n        \"success\": \"Copia la seguente stringa, senza virgolette, nel campo di configurazione del token SMTP OAuth2:\",\n        \"client_id_required\": \"Il Client ID è obbligatorio\",\n        \"client_secret_required\": \"Il Client Secret è obbligatorio\",\n        \"refresh_token_required\": \"Il Refresh Token è obbligatorio\"\n    },\n    \"filters\": {\n        \"password_strength\": \"Sicurezza password\",\n        \"password_strength_help\": \"I valori nell'intervallo 50-70 sono suggeriti per i casi d'uso comuni. 0 significa disabilitato, verrà accettata qualsiasi password\",\n        \"password_expiration\": \"Scadenza password\",\n        \"password_expiration_help\": \"Scadenza della password espressa in numero di giorni. 0 significa nessuna scadenza\",\n        \"default_shares_expiration\": \"Scadenza delle condivisioni\",\n        \"default_shares_expiration_help\": \"Scadenza predefinita per le nuove condivisioni come numero di giorni\",\n        \"max_shares_expiration\": \"Scadenza massima condivisioni\",\n        \"max_shares_expiration_help\": \"Scadenza massima consentita, come numero di giorni, quando un utente crea o aggiorna una condivisione\",\n        \"directory_permissions\": \"Permessi per cartella\",\n        \"directory_permissions_help\": \"I caratteri jolly sono supportati nei percorsi, ad esempio \\\"/incoming/*\\\" corrisponde a qualsiasi directory all'interno di \\\"/incoming\\\"\",\n        \"directory_path_help\": \"percorso cartella, es. /dir\",\n        \"directory_patterns\": \"Restrizioni sui modelli di nome per directory\",\n        \"directory_patterns_help\": \"File/directory consentiti o negati, in base ad espressioni regolari shell, separati da virgole. La corrispondenza non fa distinzione tra maiuscole e minuscole\",\n        \"max_sessions\": \"Sessioni massime\",\n        \"max_sessions_help\": \"Massimo numero di sessioni contemporanee. 0 significa nessun limite\",\n        \"denied_protocols\": \"Protocolli non permessi\",\n        \"denied_login_methods\": \"Metodi di accesso non permessi\",\n        \"denied_login_methods_help\": \"\\\"password\\\" è valido per tutti i protocolli supportati, \\\"password-over-SSH\\\" solo per SSH/SFTP/SCP\",\n        \"web_client_options\": \"Client Web/REST API\",\n        \"max_upload_size\": \"Dimensione massima upload\",\n        \"max_upload_size_help\": \"Dimensione massima (in MB/GB/TB) per un singolo file. 0 = nessun limite\",\n        \"max_upload_size_invalid\": \"Dimensione massima upload file non valida\",\n        \"upload_bandwidth\": \"Banda upload (KB/s)\",\n        \"download_bandwidth\": \"Banda download (KB/s)\",\n        \"upload_bandwidth_help\": \"UL (KB/s). 0 significa nessun limite\",\n        \"download_bandwidth_help\": \"DL (KB/s). 0 significa nessun limite\",\n        \"src_bandwidth_limit\": \"Limiti di velocità della larghezza di banda per sorgente\",\n        \"upload_data_transfer\": \"Trasferimento dati upload (MB)\",\n        \"upload_data_transfer_help\": \"Trasferimento dati massimo consentito per gli upload. 0 significa nessun limite\",\n        \"download_data_transfer\": \"Trasferimento dati download (MB)\",\n        \"download_data_transfer_help\": \"Trasferimento dati massimo consentito per i download. 0 significa nessun limite\",\n        \"total_data_transfer\": \"Trasferimento dati totale (MB)\",\n        \"total_data_transfer_help\": \"Trasferimento dati massimo consentito per upload + download. Sostituire i limiti individuali. 0 significa nessun limite\",\n        \"start_directory\": \"Directory iniziale\",\n        \"start_directory_help\": \"Directory iniziale alternativa da utilizzare al posto di \\\"/\\\". Supportato per SFTP/FTP/HTTP\",\n        \"tls_username\": \"Username per TLS\",\n        \"tls_username_help\": \"Definisce il campo del certificato TLS da utilizzare come nome utente. Ignorato se il TLS reciproco è disabilitato\",\n        \"ftp_security\": \"Sicurezza FTP\",\n        \"ftp_security_help\": \"Ignorato se TLS è già richiesto a livello globale per tutti gli utenti FTP\",\n        \"hooks\": \"Hooks\",\n        \"hook_ext_auth_disabled\": \"Autenticazione esterna disabilitata\",\n        \"hook_pre_login_disabled\": \"Pre-login disabilitato\",\n        \"hook_check_password_disabled\": \"Check password disabilitato\",\n        \"is_anonymous\": \"Utente anonimo\",\n        \"is_anonymous_help\": \"Gli utenti anonimi sono supportati per i protocolli FTP e WebDAV e hanno accesso di sola lettura\",\n        \"disable_fs_checks\": \"Disabilita i controlli del filesystem\",\n        \"disable_fs_checks_help\": \"Disabilita i controlli sull'esistenza e la creazione automatica della directory home e delle cartelle virtuali\",\n        \"api_key_auth_help\": \"Permetti di impersonare l'utente nelle API REST utilizzando una chiave API\",\n        \"external_auth_cache_time\": \"Cache per autenticazione esterna\",\n        \"external_auth_cache_time_help\": \"Tempo di memorizzazione nella cache, in secondi, per gli utenti autenticati utilizzando un hook di autenticazione esterno. 0 significa nessuna cache\",\n        \"access_time\": \"Limitazioni temporali all'accesso\",\n        \"access_time_help\": \"Nessuna restrizione significa che l'accesso è sempre consentito, l'ora deve essere impostata nel formato HH:MM\"\n    },\n    \"admin\": {\n        \"role_permissions\": \"Un amministratore di ruolo non può avere il permesso \\\"*\\\"\",\n        \"view_manage\": \"Visualizza e gestisci amministratori\",\n        \"self_delete\": \"Impossibile eliminare il proprio account\",\n        \"self_permissions\": \"Non puoi cambiare i tuoi permessi\",\n        \"self_disable\": \"Non puoi disabilitare te stesso\",\n        \"self_role\": \"Non puoi aggiungere/modificare il tuo ruolo\",\n        \"password_help\": \"Se vuoto la password corrente non verrà modificata\",\n        \"role_help\": \"L'impostazione di un ruolo limita l'amministratore a gestire solo gli utenti con lo stesso ruolo. Gli amministratori con un ruolo non possono essere super amministratori\",\n        \"users_groups\": \"Gruppi per gli utenti\",\n        \"users_groups_help\": \"Gruppi selezionati automaticamente per i nuovi utenti creati da questo amministratore. L'amministratore potrà comunque scegliere gruppi differenti. Queste impostazioni vengono utilizzate solo per questa interfaccia utente di amministrazione e verranno ignorate negli hook/API REST\",\n        \"group_membership\": \"Aggiungi come di appartenenza\",\n        \"group_primary\": \"Aggiungi come primario\",\n        \"group_secondary\": \"Aggiungi come secondario\",\n        \"user_page_pref\": \"Preferenze della pagina utente\",\n        \"user_page_pref_help\": \"Puoi nascondere alcune sezioni dalla pagina utente. Queste non sono impostazioni di sicurezza e non vengono verificate lato server. Hanno il solo scopo di semplificare la pagina di creazione/modifica utenti\",\n        \"hide_sections\": \"Nascondi sezioni\",\n        \"default_users_expiration\": \"Scadenza predefinita utenti\",\n        \"default_users_expiration_help\": \"Scadenza predefinita per i nuovi utenti espressa in numero di giorni\",\n        \"require_pwd_change_help\": \"Il cambio password è obbligatorio al prossimo accesso\"\n    },\n    \"connections\": {\n        \"view_manage\": \"Visualizza e gestisci connessioni attive\",\n        \"started\": \"Iniziata\",\n        \"remote_address\": \"Indirizzo remoto\",\n        \"last_activity\": \"Ultima attività\",\n        \"disconnect_confirm_btn\": \"Si, disconnetti\",\n        \"disconnect_confirm\": \"Vuoi disconnettere la connessione selezionata? Questa azione è irreversibile\",\n        \"disconnect_ko\": \"Impossibile disconnettere la connessione selezionata\",\n        \"upload\": \"UL: \\\"{{- path}}\\\"\",\n        \"download\": \"DL: \\\"{{- path}}\\\"\",\n        \"upload_info\": \"$t(connections.upload). Dimensione: {{- size}}. Velocità: {{- speed}}\",\n        \"download_info\": \"$t(connections.download). Dimensione: {{- size}}. Velocità: {{- speed}}\",\n        \"client\": \"Client: {{- val}}\"\n    },\n    \"role\": {\n        \"view_manage\": \"Visualizza e gestisci ruoli\",\n        \"err_delete_referenced\": \"Impossibile eliminare un ruolo con amministratori associati, rimuovere prima le associazioni\"\n    },\n    \"ip_list\": {\n        \"view_manage\": \"Visualizza e gestisci liste IP\",\n        \"defender_list\": \"Defender\",\n        \"allow_list\": \"Lista IP consentiti\",\n        \"ratelimiters_safe_list\": \"Lista IP esclusi dai rate limiters\",\n        \"ip_net\": \"IP/Rete\",\n        \"protocols\": \"Protocolli\",\n        \"any\": \"Qualunque\",\n        \"allow\": \"Permesso\",\n        \"deny\": \"Non permesso\",\n        \"ip_net_help\": \"Indirizzo IP o rete in formato CIDR, ad esempio: \\\"192.168.1.1 o 10.8.0.100/32 o 2001:db8:1234::/48\\\"\",\n        \"ip_invalid\": \"Indirizzo IP non valido\",\n        \"net_invalid\": \"Rete non valida\",\n        \"duplicated\": \"L'IP/Rete specificato esiste già\",\n        \"search\": \"IP/Rete o parte iniziale\",\n        \"defender_disabled\": \"Defender disabilitato in configurazione\",\n        \"allow_list_disabled\": \"Lista IP consentiti disabilitata in configurazione\",\n        \"ratelimiters_disabled\": \"Rate limiters disabilitati in configurazione\"\n    },\n    \"defender\": {\n        \"view_manage\": \"Visualizza e gestisci la blocklist automatica\",\n        \"ip\": \"Indirizzo IP\",\n        \"ban_time\": \"Bloccato fino a\",\n        \"score\": \"Punteggio\"\n    },\n    \"status\": {\n        \"desc\": \"Stato del server\",\n        \"ssh\": \"Server SSH/SFTP\",\n        \"active\": \"Stato: attivo\",\n        \"disabled\": \"Stato: disabilitato\",\n        \"error\": \"Stato: errore\",\n        \"proxy_on\": \"Protocollo PROXY abilitato\",\n        \"address\": \"Indirizzo\",\n        \"ssh_auths\": \"Metodi di autenticazione\",\n        \"ssh_commands\": \"Comandi accettati\",\n        \"host_key\": \"Chiave host\",\n        \"fingeprint\": \"Impronta\",\n        \"algorithms\": \"Algoritmi\",\n        \"algorithm\": \"Algoritmo\",\n        \"ssh_pub_key_algo\": \"Algoritmi per l'autenticazione con chiave pubblica\",\n        \"ssh_mac_algo\": \"Algoritmi MAC\",\n        \"ssh_kex_algo\": \"Algoritmi KEX\",\n        \"ssh_cipher_algo\": \"Cifrari\",\n        \"ftp\": \"Server FTP\",\n        \"ftp_passive_range\": \"Intervallo di porte in modalità passiva\",\n        \"ftp_passive_ip\": \"IP per FTP passivo\",\n        \"tls\": \"TLS\",\n        \"tls_disabled\": \"Disabilitato\",\n        \"tls_explicit\": \"Modalità esplicita richiesta (FTPES)\",\n        \"tls_implicit\": \"Modalità implicita (FTPS), sconsigliato, FTPES è preferibile\",\n        \"tls_mixed\": \"In chiaro e modalità esplicita (FTPES)\",\n        \"webdav\": \"Server WebDAV\",\n        \"rate_limiters\": \"Rate limiters\"\n    },\n    \"maintenance\": {\n        \"backup\": \"Backup\",\n        \"backup_do\": \"Effettua il backup dei tuoi dati\",\n        \"backup_ok\": \"Backup ripristinato correttamente\",\n        \"backup_invalid_file\": \"File di backup non valido, assicurati che sia un file JSON con contenuto valido\",\n        \"restore_error\": \"Impossibile ripristinare il backup, controlla i log del server per maggiori dettagli\",\n        \"restore\": \"Ripristino\",\n        \"backup_file\": \"File di backup\",\n        \"backup_file_help\": \"Importa i dati da un file di backup in formato JSON\",\n        \"restore_mode0\": \"Aggiungi e aggiorna\",\n        \"restore_mode1\": \"Aggiungi\",\n        \"restore_mode2\": \"Aggiungi, aggiorna e disconnetti\",\n        \"after_restore\": \"Dopo il ripristino\",\n        \"quota_mode0\": \"Non aggiornare quota\",\n        \"quota_mode1\": \"Aggiorna quota\",\n        \"quota_mode2\": \"Aggiorna quota per gli utenti con limiti di quota\"\n    },\n    \"acme\": {\n        \"title\": \"ACME\",\n        \"generic_error\": \"Impossibile ottenere certificati TLS, controlla i log del server per maggiori dettagli\",\n        \"help\": \"Da questa sezione puoi richiedere certificati TLS gratuiti per i tuoi servizi SFTPGo utilizzando il protocollo ACME e la tipologia di challenge HTTP-01. Devi creare una voce DNS sotto un dominio personalizzato di tua proprietà che si risolve nel tuo indirizzo IP pubblico SFTPGo e la porta 80 deve essere raggiungibile pubblicamente. Qui è possibile impostare le opzioni di configurazione per i casi d'uso più comuni e le configurazioni a nodo singolo, per le configurazioni avanzate fare riferimento alla documentazione SFTPGo. Per applicare le modifiche è necessario il riavvio del servizio\",\n        \"domain_help\": \"È possibile specificare più domini separati da virgole o spazi. Saranno inclusi nello stesso certificato\",\n        \"email_help\": \"E-mail utilizzata per la registrazione e il contatto di recupero\",\n        \"port_help\": \"Se diverso da 80 è necessario configurare un proxy inverso\",\n        \"protocols_help\": \"Utilizzare i certificati ottenuti per i protocolli specificati\"\n    },\n    \"smtp\": {\n        \"title\": \"SMTP\",\n        \"err_required_fields\": \"L'indirizzo del mittente e lo username non possono essere entrambi vuoti\",\n        \"help\": \"Imposta la configurazione SMTP sostituendo quella definita utilizzando env vars o il file di configurazione, se presente\",\n        \"host\": \"Nome server\",\n        \"host_help\": \"Se vuoto la configurazione è disabilitata\",\n        \"auth\": \"Autenticazione\",\n        \"encryption\": \"Crittografia\",\n        \"sender\": \"Mittente\",\n        \"debug\": \"Log a livello debug\",\n        \"domain_help\": \"Dominio HELO. Lascia vuoto per utilizzare il nome host del server\",\n        \"test_recipient\": \"Indirizzo a cui inviare e-mail di test\",\n        \"oauth2_provider\": \"Provider OAuth2\",\n        \"oauth2_provider_help\": \"URI a cui reindirizzare dopo l'autenticazione dell'utente\",\n        \"oauth2_tenant\": \"Tenant OAuth2\",\n        \"oauth2_tenant_help\": \"Azure tenant. Valori tipici sono \\\"common\\\", \\\"organizations\\\", \\\"consumers\\\" or l'ID del tenant\",\n        \"oauth2_client_id\": \"OAuth2 Client ID\",\n        \"oauth2_client_secret\": \"Segreto Client OAuth2\",\n        \"oauth2_token\": \"Token OAuth2\",\n        \"recipient_required\": \"Specifica un destinatario per inviare un'e-mail di prova\",\n        \"test_error\": \"Impossibile inviare e-mail di prova, controlla i log del server per maggiori dettagli\",\n        \"test_ok\": \"Non si sono verificati errori durante l'invio dell'e-mail di prova. Controlla la tua casella di posta per essere sicuro\",\n        \"oauth2_flow_error\": \"Impossibile ottenere l'URI per avviare il flusso OAuth2\",\n        \"oauth2_question\": \"Vuoi avviare il flusso OAuth2 per ottenere un token?\"\n    },\n    \"sftp\": {\n        \"help\": \"Da questa sezione è possibile abilitare gli algoritmi disabilitati di default. Non è necessario impostare valori già definiti utilizzando env vars o il file di configurazione. Per applicare le modifiche è necessario il riavvio del servizio\",\n        \"host_key_algos\": \"Algoritmi per chiavi host\"\n    },\n    \"branding\": {\n        \"title\": \"Branding\",\n        \"help\": \"Da questa sezione puoi personalizzare SFTPGo per adattarlo al tuo marchio e aggiungere un disclaimer alle pagine di accesso\",\n        \"short_name\": \"Nome breve\",\n        \"logo\": \"Logo\",\n        \"logo_help\": \"Immagine PNG, dimensione massima accettata 512x512, la dimensione predefinita del logo è 256x256\",\n        \"favicon\": \"Favicon\",\n        \"disclaimer_name\": \"Titolo Disclaimer\",\n        \"disclaimer_url\": \"URL Disclaimer\",\n        \"invalid_png\": \"Immagine PNG non valida\",\n        \"invalid_png_size\": \"Dimensione immagine PNG non valida\",\n        \"invalid_disclaimer_url\": \"L'URL del Disclaimer deve essere un link http o https\"\n    },\n    \"events\": {\n        \"search\": \"Cerca eventi\",\n        \"fs_events\": \"Eventi filesystem\",\n        \"provider_events\": \"Eventi provider\",\n        \"other_events\": \"Altri eventi\",\n        \"quota_exceeded\": \"Quota superata\",\n        \"date_range\": \"Intervallo di date\",\n        \"upload\": \"Caricamento\",\n        \"download\": \"Download\",\n        \"mkdir\": \"Creazione cartella\",\n        \"rmdir\": \"Rimozione cartella\",\n        \"rename\": \"Rinomina\",\n        \"delete\": \"Rimozione\",\n        \"copy\": \"Copia\",\n        \"first_upload\": \"Primo caricamento\",\n        \"first_download\": \"Primo download\",\n        \"ssh_cmd\": \"Comando SSH\",\n        \"add\": \"Aggiunta\",\n        \"update\": \"Aggiornamento\",\n        \"login_failed\": \"Accesso fallito\",\n        \"login_ok\": \"Accesso riuscito\",\n        \"login_missing_user\": \"Accesso con utente inesistente\",\n        \"no_login_tried\": \"Nessun accesso tentato\",\n        \"algo_negotiation_failed\": \"Negoziazione algoritmo fallita\",\n        \"datetime\": \"Data e ora\",\n        \"action\": \"Azione\",\n        \"path\": \"Percorso\",\n        \"object\": \"Oggetto\",\n        \"event\": \"Evento\"\n    },\n    \"provider_objects\": {\n        \"user\": \"Utente\",\n        \"folder\": \"Cartella virtuale\",\n        \"group\": \"Gruppo\",\n        \"admin\": \"Amministratore\",\n        \"api_key\": \"Chiave API\",\n        \"share\": \"Condivisione\",\n        \"event_action\": \"Azione\",\n        \"event_rule\": \"Regola\",\n        \"role\": \"ruolo\",\n        \"ip_list_entry\": \"Elemento lista IP\",\n        \"configs\": \"Configurazioni\"\n    },\n    \"actions\": {\n        \"view_manage\": \"Visualizza e gestisci le azioni delle regole per gli eventi\",\n        \"err_delete_referenced\": \"Impossibile eliminare un'azione con regole associate, rimuovere prima le associazioni\",\n        \"http_url\": \"URL Server\",\n        \"http_url_help\": \"Ad es. \\\"https://host:port/path\\\". I segnaposto sono supportati nel path dell'URL\",\n        \"http_url_required\": \"L'URL è obbligatorio\",\n        \"http_url_invalid\": \"L'URL non è valido, gli schemi http e https sono supportati\",\n        \"http_part_name_required\": \"Il nome della parte HTTP è obbligatorio\",\n        \"http_part_body_required\": \"Il body della parte HTTP è obbligatorio se non viene fornito alcun percorso file\",\n        \"http_multipart_body_error\": \"Le richieste multipart non richiedono body. Il body della richiesta è costruito dalle parti specificate\",\n        \"http_multipart_ctype_error\": \"Il Content-Type è impostato automaticamente per le richieste multipart\",\n        \"path_duplicated\": \"Percorso duplicato\",\n        \"command_required\": \"Il comando è obbligatorio\",\n        \"command_invalid\": \"Il comando non è valido, deve essere un percorso assoluto\",\n        \"email_recipient_required\": \"È obbligatorio almeno un destinatario e-mail\",\n        \"email_subject_required\": \"L'oggetto dell'e-mail è obbligatorio\",\n        \"email_body_required\": \"Il corpo dell'e-mail è obbligatorio\",\n        \"retention_directory_required\": \"È obbligatoria almeno una cartella da controllare\",\n        \"path_required\": \"Almeno un percorso è obbligatorio\",\n        \"source_dest_different\": \"Il percorso di origine e destinazione devono essere differenti\",\n        \"root_not_allowed\": \"La directory radice (/) non è permessa\",\n        \"archive_name_required\": \"Il nome dell'archivio compresso è obbligatorio\",\n        \"idp_template_required\": \"Un modello di utenti o amministratori è obbligatorio\",\n        \"threshold\": \"Soglia\",\n        \"threshold_help\": \"Verrà generata una notifica e-mail per gli utenti la cui password scade tra un numero di giorni inferiore o uguale a questa soglia\",\n        \"disable_threshold\": \"Soglia disabilitazione\",\n        \"disable_threshold_help\": \"Inattività in giorni, dall'ultimo login o dalla creazione prima della disabilitazione degli utenti\",\n        \"delete_threshold\": \"Soglia eliminazione\",\n        \"delete_threshold_help\": \"Inattività in giorni, dall'ultimo login o dalla creazione prima dell'eliminazione degli utenti\",\n        \"inactivity_threshold_required\": \"È necessario definire almeno una soglia di inattività\",\n        \"inactivity_thresholds_invalid\": \"La soglia di eliminazione deve essere maggiore della soglia di disattivazione\",\n        \"idp_mode_add_update\": \"Crea o aggiorna\",\n        \"idp_mode_add\": \"Crea se non esiste\",\n        \"template_user_help\": \"Modello per gli utenti SFTPGo in formato JSON. I segnaposto sono supportati\",\n        \"template_admin_help\": \"Modello per gli amministratori SFTPGo in formato JSON. I segnaposto sono supportati\",\n        \"placeholders_help\": \"I segnaposto sono supportati\",\n        \"http_headers\": \"Header HTTP\",\n        \"query_parameters\": \"Parametri query-string\",\n        \"http_timeout_help\": \"Ignorato per richieste multipart con file allegati\",\n        \"body\": \"Body\",\n        \"http_body_help\": \"I segnaposto sono supportati. Ignorato per la richiesta HTTP. Lasciare vuoto per richieste multipart\",\n        \"multipart_body\": \"Body multipart\",\n        \"multipart_body_help\": \"Le richieste HTTP multipart consentono di combinare uno o più set di dati in un unico body. Per ciascuna parte è possibile impostare un percorso file o un body come testo. I segnaposto sono supportati nel percorso del file, nel body e nei valori degli header\",\n        \"http_part_name\": \"Nome parte\",\n        \"http_part_file\": \"Percorso file\",\n        \"http_part_headers\": \"Header aggiuntivi, una per riga come \\\"chiave: valore\\\"\",\n        \"command_help\": \"Percorso assoluto del comando da eseguire\",\n        \"command_args\": \"Argomenti\",\n        \"command_args_help\": \"Argomenti del comando separati da virgole. I segnaposto sono supportati\",\n        \"command_env_vars_help\": \"I segnaposto sono supportati nei valori. Impostare il valore su \\\"$\\\" senza virgolette significa recuperare il nome dall'ambiente ad esclusione dei nomi che iniziano con SFTPGO_\",\n        \"email_recipients\": \"A\",\n        \"email_recipients_help\": \"Destinatari separati da virgole. I segnaposto sono supportati\",\n        \"email_bcc\": \"Ccn\",\n        \"email_bcc_help\": \"Indirizzi Ccn separati da virgole. I segnaposto sono supportati\",\n        \"email_subject\": \"Oggetto\",\n        \"content_type\": \"Content Type\",\n        \"attachments\": \"Allegati\",\n        \"attachments_help\": \"Percorsi da allegare separati da virgole. I segnaposto sono supportati. La dimensione totale è limitata a 10 MB\",\n        \"data_retention\": \"Conservazione dati\",\n        \"data_retention_help\": \"Imposta la conservazione dei dati, in ore, per percorso. La conservazione si applica in modo ricorsivo. Impostare 0 come conservazione significa escludere il percorso specificato\",\n        \"delete_empty_dirs\": \"Cancella cartelle vuote\",\n        \"fs_action\": \"Azione del filesystem\",\n        \"paths_src_dst_help\": \"Percorsi visti dagli utenti SFTPGo. I segnaposto sono supportati. Le autorizzazioni richieste vengono concesse automaticamente\",\n        \"source_path\": \"Origine\",\n        \"target_path\": \"Destinazione\",\n        \"paths_help\": \"Percorsi visti dagli utenti SFTPGo separati da virgole. I segnaposto sono supportati. Le autorizzazioni richieste vengono concesse automaticamente\",\n        \"archive_path\": \"Percorso dell'archivio\",\n        \"archive_path_help\": \"Percorso completo, come visto dagli utenti SFTPGo, dell'archivio zip da creare. I segnaposto sono supportati. Se il file specificato esiste già, verrà sovrascritto\",\n        \"placeholders_modal_title\": \"Segnaposto supportati\",\n        \"update_mod_times\": \"Aggiorna timestamp\",\n        \"types\": {\n            \"http\": \"HTTP\",\n            \"email\": \"E-mail\",\n            \"backup\": \"Backup\",\n            \"user_quota_reset\": \"Ricalcolo quota utente\",\n            \"folder_quota_reset\": \"Ricalcolo quota cartella virtuale\",\n            \"transfer_quota_reset\": \"Reimpostazione quota trasferimenti\",\n            \"data_retention_check\": \"Controllo conservazione dati\",\n            \"filesystem\": \"Filesystem\",\n            \"password_expiration_check\": \"Controllo password scadute\",\n            \"user_expiration_check\": \"Controllo utenti scaduti\",\n            \"user_inactivity_check\": \"Controllo inattività utente\",\n            \"idp_check\": \"Controllo account Identity Provider\",\n            \"rotate_logs\": \"Rotazione file di log\",\n            \"command\": \"Comando\"\n        },\n        \"fs_types\": {\n            \"rename\": \"Rinomina\",\n            \"delete\": \"Eliminazione\",\n            \"path_exists\": \"Esistenza percorsi\",\n            \"compress\": \"Compressione\",\n            \"copy\": \"Copia\",\n            \"create_dirs\": \"Creazione directory\"\n        },\n        \"placeholders_modal\": {\n            \"name\": \"Nome utente, nome cartella, nome utente amministratore per eventi provider, nome dominio per eventi relativi ai certificati TLS\",\n            \"event\": \"Nome dell'evento, ad esempio \\\"upload\\\", \\\"download\\\" per eventi del file system o \\\"add\\\", \\\"update\\\" per eventi del provider\",\n            \"status\": \"Stato per eventi del file system. 1 significa nessun errore, 2 significa che si è verificato un errore generico, 3 significa errore di superamento quota\",\n            \"status_string\": \"Stato come stringa. Valori possibili \\\"OK\\\", \\\"KO\\\"\",\n            \"error_string\": \"Dettagli circa l'errore. Sostituito con una stringa vuota se non si verificano errori\",\n            \"virtual_path\": \"Percorso visualizzato dagli utenti SFTPGo, ad esempio \\\"/adir/afile.txt\\\"\",\n            \"escaped_virtual_path\": \"Percorso codificato per HTTP query string, ad esempio \\\"%2Fadir%2Fafile.txt\\\".\",\n            \"virtual_dir_path\": \"Directory superiore per \\\"VirtualPath\\\", ad esempio se \\\"VirtualPath\\\" è \\\"/adir/afile.txt\\\", \\\"VirtualDirPath\\\" è \\\"/adir\\\"\",\n            \"fs_path\": \"Percorso completo del filesystem, ad esempio \\\"/user/homedir/adir/afile.txt\\\" o \\\"C:/data/user/homedir/adir/afile.txt\\\" su Windows\",\n            \"ext\": \"Estensione del file, ad esempio \\\".txt\\\" se il nome del file è \\\"afile.txt\\\"\",\n            \"object_name\": \"Nome del file/directory, ad esempio \\\"afile.txt\\\" o nome dell'oggetto del provider\",\n            \"object_basename\": \"Nome del file senza estensione, ad esempio \\\"afile\\\" se il nome del file è \\\"afile.txt\\\"\",\n            \"object_type\": \"Tipo di oggetto per gli eventi provider: \\\"user\\\", \\\"group\\\", \\\"admin\\\", ecc\",\n            \"virtual_target_path\": \"Percorso di destinazione virtuale per le operazioni di ridenominazione e copia\",\n            \"virtual_target_dir_path\": \"Cartella superiore per \\\"VirtualTargetPath\\\"\",\n            \"target_name\": \"Nome dell'oggetto di destinazione per le operazioni di ridenominazione e copia\",\n            \"fs_target_path\": \"Percorso di destinazione completo su file system per le operazioni di ridenominazione e copia\",\n            \"file_size\": \"Dimensione file (bytes)\",\n            \"elapsed\": \"Tempo trascorso in millisecondi per gli eventi del file system\",\n            \"protocol\": \"Protocollo, ad esempio \\\"SFTP\\\", \\\"FTP\\\"\",\n            \"ip\": \"Indirizzo IP del client\",\n            \"role\": \"Ruolo dell'utente o dell'amministratore\",\n            \"timestamp\": \"Timestamp dell'evento in nanosecondi dall'epoch time\",\n            \"datetime\": \"Timestamp dell'evento formattato come YYYY-MM-DDTHH:MM:SS.ZZZ\",\n            \"year\": \"Anno dell'evento formattato a quattro cifre\",\n            \"month\": \"Mese dell'evento formattato a due cifre\",\n            \"day\": \"Giorno dell'evento formattato a due cifre\",\n            \"hour\": \"Ora dell'evento formattata a due cifre\",\n            \"minute\": \"Minuto dell'evento formattato a due cifre\",\n            \"email\": \"Per gli eventi del file system, questa è l'e-mail associata all'utente che esegue l'azione. Per gli eventi del provider, si tratta dell'e-mail associata all'utente o all'amministratore interessato. Vuoto in tutti gli altri casi\",\n            \"object_data\": \"Dati dell'oggetto provider serializzati come JSON con campi sensibili rimossi\",\n            \"object_data_string\": \"Dati dell'oggetto provider serializzati come stringa JSON escaped con campi sensibili rimossi\",\n            \"retention_reports\": \"Report sulla conservazione dei dati come file CSV compressi zip. Supportato come allegato e-mail, percorso file per richieste HTTP multipart e come parametro singolo per il body delle richieste HTTP\",\n            \"idp_field\": \"Campi personalizzati dell'Identity Provdider contenenti una stringa\",\n            \"metadata\": \"Metadati del Cloud Storage Provider serializzati come JSON per i file scaricati\",\n            \"metadata_string\": \"Metadati del Cloud Storage Provider serializzati come stringa JSON escaped per i file scaricati\",\n            \"uid\": \"ID univoco\"\n        }\n    },\n    \"rules\": {\n        \"view_manage\": \"Visualizza e gestisci le regole per gli eventi\",\n        \"trigger\": \"Attivazione\",\n        \"run_confirm\": \"Vuoi eseguire la regola selezionata?\",\n        \"run_confirm_btn\": \"Si, esegui\",\n        \"run_error_generic\": \"Impossibile eseguire la regola selezionata\",\n        \"run_ok\": \"Azioni delle regola avviate\",\n        \"run\": \"Esegui\",\n        \"invalid_fs_min_size\": \"Dimensione minima non valida\",\n        \"invalid_fs_max_size\": \"Dimensione massima non valida\",\n        \"action_required\": \"Almeno un'azione è obbligatoria\",\n        \"fs_event_required\": \"Almeno un evento file system è obbligatorio\",\n        \"provider_event_required\": \"Almeno un evento provider è obbligatorio\",\n        \"schedule_required\": \"Almeno una schedulazione è obbligatoria\",\n        \"schedule_invalid\": \"Schedulazione non valida\",\n        \"duplicate_actions\": \"Rilevata azioni duplicate\",\n        \"sync_failure_actions\": \"L'esecuzione sincrona non è supportata per le azioni su errore\",\n        \"sync_unsupported\": \"L'esecuzione sincrona è supportata solo per alcuni eventi del file system e per gli accessi tramite Identity Provider\",\n        \"sync_unsupported_fs_event\": \"L'esecuzione sincrona è supporta solo per gli eventi \\\"upload\\\" e \\\"pre-*\\\"\",\n        \"only_failure_actions\": \"E' richiesta almeno un'azione che non venga eseguita su errore\",\n        \"sync_action_required\": \"L'evento \\\"{{val}}\\\" richiede almeno un'azione da eseguire sincronamente\",\n        \"scheduler_help\": \"Orari: 0-23. Giorno della settimana: 0-6 (dom-sab). Giorno del mese: 1-31. Mese: 1-12. L'asterisco (*) indica una corrispondenza per tutti i valori del campo. per esempio. ogni giorno della settimana, ogni giorno del mese e così via\",\n        \"concurrent_run\": \"Consentire l'esecuzione simultanea da più istanze\",\n        \"protocol_filters\": \"Filtro su protocolli\",\n        \"status_filters\": \"Filtro su stati\",\n        \"object_filters\": \"Filtro su oggetti\",\n        \"name_filters\": \"Filtro su nomi\",\n        \"name_filters_help\": \"Filtri per nomi utente e nomi di cartelle. Ad esempio, \\\"user*\\\"\\\" corrisponderà per i nomi che iniziano con \\\"user\\\". Per gli eventi del provider, questo filtro viene applicato al nome utente dell'amministratore che esegue l'evento\",\n        \"inverse_match\": \"Corrispondenza inversa\",\n        \"group_name_filters\": \"Filtro su nome gruppi\",\n        \"group_name_filters_help\": \"Filtri per nomi dei gruppi. Ad esempio \\\"group*\\\"\\\" corrisponderà ai nomi dei gruppi che iniziano con \\\"group\\\"\",\n        \"role_name_filters\": \"Filtri su nome ruoli\",\n        \"role_name_filters_help\": \"Filtri per nomi dei ruoli. Ad esempio \\\"role*\\\"\\\" corrisponderà ai nomi dei gruppi che iniziano con \\\"role\\\"\",\n        \"path_filters\": \"Filtri sui percorsi\",\n        \"path_filters_help\": \"Filtri sui percorsi degli eventi del file system. Ad esempio \\\"/adir/*.txt\\\"\\\" corrisponderà ai percorsi nella directory \\\"/adir\\\" che terminano con \\\".txt\\\". È supportato il doppio asterisco, ad esempio \\\"/**/*. txt\\\" corrisponderà a qualsiasi file che termina con \\\".txt\\\". \\\"/mydir/**\\\" corrisponderà a qualsiasi voce in \\\"/mydir\\\"\",\n        \"file_size_limits\": \"Filtri sulla dimensione file\",\n        \"file_size_limits_help\": \"0 significa nessun limite. È possibile utilizzare il suffisso MB/GB\",\n        \"min_size\": \"Dimensione min\",\n        \"max_size\": \"Dimensione max\",\n        \"actions_help\": \"Una o più azioni da eseguire. L'opzione \\\"Esecuzione sincrona\\\" è supportata per gli eventi di \\\"upload\\\" ed è richiesta per gli eventi \\\"pre-*\\\" e gli eventi di accesso tramite Identity provider se l'azione controlla l'account\",\n        \"option_failure_action\": \"Azione su errore\",\n        \"option_stop_on_failure\": \"Termina su errore\",\n        \"option_execute_sync\": \"Esecuzione sincrona\",\n        \"no_filter\": \"Nessun filtro significa attivare sempre gli eventi\",\n        \"action_placeholder\": \"Seleziona un'azione\",\n        \"triggers\": {\n            \"fs_event\": \"Eventi file system\",\n            \"provider_event\": \"Eventi provider\",\n            \"ip_blocked\": \"IP bloccato\",\n            \"certificate_renewal\": \"Rinnovo certificato\",\n            \"on_demand\": \"Su richiesta\",\n            \"idp_login\": \"Accessi tramite Identity Provider\",\n            \"schedule\": \"Schedulazioni\"\n        },\n        \"idp_logins\": {\n            \"user\": \"Accesso utente\",\n            \"admin\": \"Accesso amministratore\"\n        }\n    }\n}\n"
  },
  {
    "path": "static/locales/zh-CN/translation.json",
    "content": "{\n    \"title\": {\n        \"setup\": \"初始设置\",\n        \"login\": \"登录\",\n        \"share_login\": \"分享登录\",\n        \"profile\": \"个人资料\",\n        \"change_password\": \"修改密码\",\n        \"files\": \"文件\",\n        \"shares\": \"分享\",\n        \"add_share\": \"添加分享\",\n        \"update_share\": \"更新分享\",\n        \"two_factor_auth\": \"两步验证\",\n        \"two_factor_auth_short\": \"两步验证\",\n        \"edit_file\": \"编辑文件\",\n        \"view_file\": \"查看文件\",\n        \"recovery_password\": \"密码恢复\",\n        \"reset_password\": \"密码重置\",\n        \"shared_files\": \"分享的文件\",\n        \"upload_to_share\": \"上传以分享\",\n        \"download_shared_file\": \"下载共享文件\",\n        \"share_access_error\": \"无法访问共享\",\n        \"invalid_auth_request\": \"认证请求无效\",\n        \"error400\": \"错误请求\",\n        \"error403\": \"禁止访问\",\n        \"error404\": \"未找到\",\n        \"error416\": \"请求的范围不可满足\",\n        \"error429\": \"请求次数过多\",\n        \"error500\": \"服务器内部错误\",\n        \"errorPDF\": \"无法显示 PDF 文件\",\n        \"error_editor\": \"无法打开文件编辑器\",\n        \"users\": \"用户\",\n        \"groups\": \"组\",\n        \"folders\": \"虚拟文件夹\",\n        \"connections\": \"活动连接\",\n        \"event_manager\": \"事件管理器\",\n        \"event_rules\": \"规则\",\n        \"event_actions\": \"操作\",\n        \"ip_manager\": \"IP 管理器\",\n        \"ip_lists\": \"IP 列表\",\n        \"defender\": \"自动屏蔽列表\",\n        \"admins\": \"管理员\",\n        \"roles\": \"角色\",\n        \"server_manager\": \"服务器管理\",\n        \"configs\": \"配置\",\n        \"logs\": \"日志\",\n        \"maintenance\": \"维护\",\n        \"status\": \"状态\",\n        \"add_user\": \"添加用户\",\n        \"update_user\": \"更新用户\",\n        \"template_user\": \"用户模板\",\n        \"template_admin\": \"管理模板\",\n        \"add_group\": \"添加组\",\n        \"update_group\": \"更新组\",\n        \"add_folder\": \"添加虚拟文件夹\",\n        \"update_folder\": \"更新虚拟文件夹\",\n        \"template_folder\": \"虚拟文件夹录模板\",\n        \"oauth2_error\": \"无法完成 OAuth2 流程\",\n        \"oauth2_success\": \"OAuth2 流程完成\",\n        \"add_role\": \"添加角色\",\n        \"update_role\": \"更新角色\",\n        \"add_admin\": \"添加管理员\",\n        \"update_admin\": \"更新管理员\",\n        \"add_ip_list\": \"添加 IP 列表项\",\n        \"update_ip_list\": \"更新 IP 列表项\",\n        \"add_action\": \"添加动作\",\n        \"update_action\": \"更新动作\",\n        \"add_rule\": \"添加规则\",\n        \"update_rule\": \"更新规则\"\n    },\n    \"setup\": {\n        \"desc\": \"要开始使用 SFTPGo，您需要创建管理员用户\",\n        \"submit\": \"创建管理员并登录\",\n        \"install_code_mismatch\": \"安装代码不匹配\",\n        \"help_text\": \"商业支持\"\n    },\n    \"login\": {\n        \"username\": \"用户名\",\n        \"password\": \"密码\",\n        \"your_username\": \"您的用户名\",\n        \"forgot_password\": \"忘记密码？\",\n        \"forgot_password_msg\": \"请在下面输入您的帐户用户名，您将收到一封电子邮件密码重置代码。\",\n        \"send_reset_code\": \"发送重置码\",\n        \"signin\": \"登录\",\n        \"signin_openid\": \"使用 OpenID 登录\",\n        \"signout\": \"注销\",\n        \"auth_code\": \"验证码\",\n        \"two_factor_help\": \"在您的设备上打开双因素认证应用程序，以查看您的认证代码并验证您的身份。\",\n        \"two_factor_msg\": \"输入双因素认证恢复密钥\",\n        \"recovery_code\": \"恢复代码\",\n        \"recovery_code_msg\": \"如果您无法访问您的移动设备，您可以输入一个恢复码。\",\n        \"reset_password\": \"重置密码\",\n        \"reset_pwd_msg\": \"检查您邮箱中的确认码\",\n        \"reset_submit\": \"更新密码并登录\",\n        \"confirm_code\": \"确认码\",\n        \"reset_pwd_forbidden\": \"您无权重置密码\",\n        \"reset_pwd_no_email\": \"您的账户没有电子邮件地址，无法通过发送电子邮件验证码重置您的密码\",\n        \"reset_pwd_send_email_err\": \"无法通过电子邮件发送确认码\",\n        \"reset_pwd_err_generic\": \"重置密码时发生意外错误\",\n        \"reset_ok_login_error\": \"密码重置成功完成，但在登录时发生异常错误\",\n        \"ip_not_allowed\": \"此 IP 地址不允许登录\",\n        \"two_factor_required\": \"设置双因素认证，它是以下协议所必需的：{{val}}。\",\n        \"two_factor_required_generic\": \"设置双因素认证，这是您帐户的强制要求\",\n        \"link\": \"跳转 {{link}}\"\n    },\n    \"theme\": {\n        \"light\": \"明亮\",\n        \"dark\": \"暗黑\",\n        \"system\": \"自动\"\n    },\n    \"general\": {\n        \"invalid_auth_request\": \"身份验证请求不符合安全要求\",\n        \"invalid_input\": \"无效的输入。斜线(/ ), 冒号 (:), 控制字符和保留系统名称是不允许的\",\n        \"error400\": \"收到的请求无效\",\n        \"error403\": \"您没有所需的权限。\",\n        \"error404\": \"请求的资源不存在\",\n        \"error416\": \"无法返回请求的文件片段\",\n        \"error429\": \"已超过速率限制\",\n        \"error500\": \"服务器无法满足您的请求\",\n        \"errorPDF\": \"此文件看起来不是一个 PDF\",\n        \"error_edit_dir\": \"无法编辑目录\",\n        \"error_edit_size\": \"文件大小超过允许的最大尺寸\",\n        \"invalid_form\": \"无效的表单\",\n        \"invalid_credentials\": \"无效的凭据，请重试\",\n        \"invalid_csrf\": \"表单令牌无效\",\n        \"invalid_token\": \"无效权限\",\n        \"confirm_logout\": \"您确定要退出吗？\",\n        \"wait\": \"请稍候...\",\n        \"ok\": \"确定\",\n        \"failed\": \"失败\",\n        \"cancel\": \"不，返回\",\n        \"submit\": \"保存\",\n        \"back\": \"返回\",\n        \"add\": \"添加\",\n        \"enable\": \"启用\",\n        \"disable\": \"停用\",\n        \"disable_confirm_btn\": \"是的，禁用\",\n        \"close\": \"关闭\",\n        \"search\": \"搜索\",\n        \"configuration\": \"配置\",\n        \"config_saved\": \"配置已保存\",\n        \"rename\": \"重命名\",\n        \"confirm\": \"是，继续\",\n        \"edit\": \"编辑\",\n        \"delete\": \"删除\",\n        \"delete_confirm_btn\": \"是，删除\",\n        \"delete_confirm\": \"您想删除 \\\"{{- name}}\\\"吗？ 此操作不可逆转\",\n        \"delete_confirm_generic\": \"您想要删除所选项目吗？此操作是不可逆的\",\n        \"delete_multi_confirm\": \"您想要删除选定的项目吗？此操作是不可逆的\",\n        \"delete_error_generic\": \"无法删除选中的项目\",\n        \"delete_error_403\": \"$t(general.delete_error_generic). $t(general.error403)\",\n        \"delete_error_404\": \"$t(general.delete_error_generic). $t(general.error404)\",\n        \"loading\": \"加载中…\",\n        \"name\": \"名称\",\n        \"size\": \"大小\",\n        \"last_modified\": \"最近修改\",\n        \"info\": \"信息\",\n        \"datetime\": \"{{- val, datetime}}\",\n        \"selected_items_one\": \"已选择 {{count}} 个项目\",\n        \"selected_items_other\": \"已选择 {{count}} 个项目\",\n        \"selected_with_placeholder\": \"已选择 %d 项\",\n        \"name_required\": \"名称是必填的\",\n        \"name_different\": \"新名称必须不同于当前名称\",\n        \"html5_media_not_supported\": \"您的浏览器不支持 HTML5 音频/视频。\",\n        \"choose_target_folder\": \"选择目标文件夹\",\n        \"source_name\": \"源名称\",\n        \"target_folder\": \"目标文件夹\",\n        \"dest_name\": \"目标名称\",\n        \"my_profile\": \"我的资料\",\n        \"description\": \"描述\",\n        \"email\": \"邮箱\",\n        \"api_key_auth\": \"API 密钥认证\",\n        \"api_key_auth_help\": \"允许使用一个 API 密钥模拟访问\",\n        \"pub_keys\": \"公钥\",\n        \"pub_key_placeholder\": \"在此粘贴公钥\",\n        \"pub_keys_help\": \"公钥可以用于 SFTP 认证\",\n        \"verify\": \"验证\",\n        \"problems\": \"遇到问题?\",\n        \"allowed_ip_mask\": \"允许的 IP/掩码\",\n        \"denied_ip_mask\": \"拒绝的 IP/掩码\",\n        \"ip_mask_help\": \"CIDR 格式的 IP 地址，用逗号分隔，例如：\\\"192.168.1.0/24, 10.8.0.100/32\\\"\",\n        \"allowed_ip_mask_invalid\": \"无效的允许 IP/掩码\",\n        \"username_required\": \"必须输入用户名\",\n        \"password_required\": \"密码为必填项\",\n        \"foldername_required\": \"文件夹名称为必填项\",\n        \"err_user\": \"无法验证您的用户\",\n        \"err_protocol_forbidden\": \"您的帐号不允许使用 HTTP 协议\",\n        \"pwd_login_forbidden\": \"您的用户不允许使用密码登录\",\n        \"ip_forbidden\": \"此 IP 地址不允许登录\",\n        \"email_invalid\": \"邮件地址无效\",\n        \"err_password_complexity\": \"提供的密码不符合复杂性要求\",\n        \"no_oidc_feature\": \"如果您使用 OpenID 登录，此功能不可用\",\n        \"connection_forbidden\": \"您不被允许连接\",\n        \"no_permissions\": \"您无权修改任何内容\",\n        \"path_invalid\": \"非法路径\",\n        \"err_quota_read\": \"由于配额限制，读取被拒绝\",\n        \"profile_updated\": \"你的个人信息更新成功\",\n        \"share_ok\": \"分享访问成功，您现在可以使用您的链接\",\n        \"qr_code\": \"二维码\",\n        \"copy_link\": \"复制链接\",\n        \"copied\": \"已复制\",\n        \"active\": \"活跃\",\n        \"inactive\": \"非活动\",\n        \"colvis\": \"列可见性\",\n        \"actions\": \"操作\",\n        \"template\": \"用作模板\",\n        \"quota_scan\": \"配额扫描\",\n        \"quota_scan_started\": \"配额扫描开始。它可能需要一段时间，取决于文件的数量\",\n        \"quota_scan_conflit\": \"另一个扫描正在进行中\",\n        \"quota_scan_error\": \"无法开始配额扫描\",\n        \"role\": \"角色\",\n        \"role_placeholder\": \"选择角色\",\n        \"group_placeholder\": \"选择一个组\",\n        \"folder_placeholder\": \"选择文件夹\",\n        \"blank_default_help\": \"留空以使用默认值\",\n        \"skip_tls_verify\": \"跳过 TLS 验证。这只能用于测试\",\n        \"advanced_settings\": \"高级设置\",\n        \"default\": \"默认值\",\n        \"private_key\": \"私钥\",\n        \"acls\": \"访问控制列表\",\n        \"quota_limits\": \"磁盘配额和带宽限制\",\n        \"expiration\": \"过期\",\n        \"expiration_help\": \"选择过期日期\",\n        \"additional_info\": \"附加信息\",\n        \"permissions\": \"权限\",\n        \"visible\": \"可见\",\n        \"hidden\": \"隐藏\",\n        \"allowed\": \"已允许\",\n        \"denied\": \"已拒绝\",\n        \"zero_no_limit_help\": \"0 表示没有限制\",\n        \"global_settings\": \"全局设置\",\n        \"mandatory_encryption\": \"强制加密\",\n        \"name_invalid\": \"指定名称无效，允许以下字符：a-zA-Z0-9-。~\",\n        \"associations\": \"关联\",\n        \"template_placeholders\": \"支持以下占位符\",\n        \"duplicated_username\": \"该用户名已被使用\",\n        \"duplicated_name\": \"指定名称已存在\",\n        \"permissions_required\": \"需要权限\",\n        \"configs_saved\": \"配置已成功更新\",\n        \"protocol\": \"协议\",\n        \"refresh\": \"刷新\",\n        \"members\": \"成员\",\n        \"members_summary\": \"用户：{{users}}. 管理员：{{admins}}\",\n        \"status\": \"状态\",\n        \"last_login\": \"上次登录\",\n        \"previous\": \"返回\",\n        \"next\": \"下一页\",\n        \"type\": \"类型\",\n        \"issuer\": \"发行人\",\n        \"data_provider\": \"数据库\",\n        \"driver\": \"驱动\",\n        \"mode\": \"模式\",\n        \"port\": \"端口\",\n        \"domain\": \"域名\",\n        \"test\": \"测试\",\n        \"get\": \"获取\",\n        \"export\": \"导出\",\n        \"value\": \"值\",\n        \"method\": \"方法\",\n        \"timeout\": \"超时\",\n        \"env_vars\": \"环境变量\",\n        \"hours\": \"小时\",\n        \"paths\": \"路径\",\n        \"hour\": \"小时\",\n        \"day_of_week\": \"星期几\",\n        \"day_of_month\": \"一个月中的某天\",\n        \"month\": \"月\",\n        \"options\": \"选项\",\n        \"expired\": \"过期\",\n        \"unsupported\": \"不再支持功能\",\n        \"start\": \"开始 (HH:MM)\",\n        \"end\": \"结束 (HH:MM)\",\n        \"monday\": \"星期一\",\n        \"tuesday\": \"星期二\",\n        \"wednesday\": \"星期三\",\n        \"thursday\": \"星期四\",\n        \"friday\": \"星期五\",\n        \"saturday\": \"星期六\",\n        \"sunday\": \"星期日\",\n        \"expired_session\": \"会话已过期，请重新登录。\"\n    },\n    \"fs\": {\n        \"view_file\": \"查看文件 \\\"{{- path}}\\\"\",\n        \"edit_file\": \"编辑文件 \\\"{{- path}}\\\"\",\n        \"new_folder\": \"新文件夹\",\n        \"select_across_pages\": \"跨页选择\",\n        \"download\": \"下载\",\n        \"download_ready\": \"您的下载已准备好\",\n        \"upload_queue_not_ready\": \"上传队列仍在加载中，请稍后再试。\",\n        \"upload_no_files\": \"未选择文件。请选择或拖动文件以上传\",\n        \"move_copy\": \"移动或复制\",\n        \"share\": \"分享\",\n        \"home\": \"首页\",\n        \"create_folder_msg\": \"文件夹名\",\n        \"no_more_subfolders\": \"这里没有更多子文件夹\",\n        \"no_files_folders\": \"没有文件或文件夹\",\n        \"invalid_name\": \"文件或文件夹名称不能包含 \\\"/\\\"\",\n        \"folder_name_required\": \"文件夹名称是必填项\",\n        \"deleting\": \"删除 {{idx}}/{{total}}\",\n        \"copying\": \"复制 {{idx}}/{{total}}\",\n        \"moving\": \"移动 {{idx}}/{{total}}\",\n        \"uploading\": \"上传 {{idx}}/{{total}}\",\n        \"err_403\": \"权限被拒绝\",\n        \"err_429\": \"过多的并发请求\",\n        \"err_generic\": \"无法访问请求的资源\",\n        \"err_validation\": \"文件系统配置无效\",\n        \"err_exists\": \"目标已经存在\",\n        \"dir_list\": {\n            \"err_generic\": \"获取目录列表失败\",\n            \"err_403\": \"$t(fs.dir_list.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.dir_list.err_generic). $t(fs.err_429)\",\n            \"err_user\": \"$t(fs.dir_list.err_generic). $t(general.err_user)\"\n        },\n        \"create_dir\": {\n            \"err_generic\": \"创建文件夹出现错误\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"save\": {\n            \"err_generic\": \"保存文件时出错\",\n            \"err_403\": \"$t(fs.create_dir.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.create_dir.err_generic). $t(fs.err_429)\"\n        },\n        \"delete\": {\n            \"err_generic\": \"无法删除 \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.delete.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete.err_generic). $t(fs.err_429)\"\n        },\n        \"delete_multi\": {\n            \"err_generic_partial\": \"并非所有选中项目都已删除，请重新加载页面\",\n            \"err_generic\": \"无法删除选中的项目\",\n            \"err_403_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)\",\n            \"err_429_partial\": \"$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)\",\n            \"err_403\": \"$t(fs.delete_multi.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.delete_multi.err_generic). $t(fs.err_429)\"\n        },\n        \"copy\": {\n            \"msg\": \"复制\",\n            \"err_generic\": \"复制文件/目录时出错\",\n            \"err_403\": \"$t(fs.copy.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.copy.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.copy.err_generic). $t(fs.err_exists)\"\n        },\n        \"move\": {\n            \"msg\": \"移动\",\n            \"err_generic\": \"复制文件/目录时出错\",\n            \"err_403\": \"$t(fs.move.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.move.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.move.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"不支持：如果您想移动一个目录，请确保它是空的\"\n        },\n        \"rename\": {\n            \"title\": \"重命名 \\\"{{- name}}\\\"\",\n            \"new_name\": \"新名称\",\n            \"err_generic\": \"无法重命名 \\\"{{- name}}\\\"\",\n            \"err_403\": \"$t(fs.rename.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.rename.err_generic). $t(fs.err_429)\",\n            \"err_exists\": \"$t(fs.rename.err_generic). $t(fs.err_exists)\",\n            \"err_unsupported\": \"不支持：如果您想要重命名一个目录，请确保它为空\"\n        },\n        \"upload\": {\n            \"text\": \"上传文件\",\n            \"success\": \"文件上传成功\",\n            \"message\": \"拖曳文件到此处或单击上传\",\n            \"message_empty\": \"目录为空. $t(fs.upload.message)\",\n            \"err_generic\": \"上传文件错误\",\n            \"err_403\": \"$t(fs.upload.err_generic). $t(fs.err_403)\",\n            \"err_429\": \"$t(fs.upload.err_generic). $t(fs.err_429)\",\n            \"err_dir_overwrite\": \"$t(fs.upload.err_generic). 存在与文件同名的目录：{{- val}}\",\n            \"overwrite_text\": \"检测到冲突，你要覆盖以下文件或目录吗？\"\n        },\n        \"quota_usage\": {\n            \"title\": \"配额使用量\",\n            \"disk\": \"磁盘配额\",\n            \"size\": \"大小: {{- val}}\",\n            \"size_percentage\": \"大小: {{- val}} ({{percentage}}%)\",\n            \"files\": \"文件: {{- val}}\",\n            \"files_percentage\": \"文件: {{- val}} ({{percentage}}%)\",\n            \"transfer\": \"转让额度\",\n            \"total\": \"总共: {{- val}}\",\n            \"total_percentage\": \"总共: {{- val}} ({{percentage}}%)\",\n            \"uploads\": \"上传: {{- val}}\",\n            \"uploads_percentage\": \"上传: {{- val}} ({{percentage}}%)\",\n            \"downloads\": \"下载: {{- val}}\",\n            \"downloads_percentage\": \"下载: {{- val}} ({{percentage}}%)\"\n        }\n    },\n    \"datatable\": {\n        \"info\": \"Showing _START_ to _END_ of _TOTAL_ records\",\n        \"info_empty\": \"无记录显示\",\n        \"info_filtered\": \"(从 _MAX_ 条总记录中筛选)\",\n        \"processing\": \"处理中...\",\n        \"no_records\": \"没有找到记录\"\n    },\n    \"editor\": {\n        \"keybinding\": \"编辑器按键绑定\",\n        \"search\": \"打开搜索面板\",\n        \"goto\": \"跳转到指定行\",\n        \"indent_more\": \"更多缩进\",\n        \"indent_less\": \"减少缩进\"\n    },\n    \"2fa\": {\n        \"title\": \"使用身份验证器应用程序进行双因素认证。\",\n        \"msg_enabled\": \"已启用双因素验证\",\n        \"msg_disabled\": \"保护您的帐户\",\n        \"msg_info\": \"双因素认证为您的帐户增加了一层额外的安全保护。登录时，您需要提供额外的认证代码。\",\n        \"require\": \"此帐户需要启用双因素认证\",\n        \"require_for\": \"需要双因素验证\",\n        \"generate\": \"生成一个新密钥\",\n        \"recovery_codes\": \"恢复代码\",\n        \"new_recovery_codes\": \"新恢复代码\",\n        \"recovery_codes_msg1\": \"恢复代码是当您无法访问身份验证应用时用于登录的一次性备用代码。如果您丢失了手机，可以使用它们访问您的帐户并管理双重身份验证设置。\",\n        \"recovery_codes_msg2\": \"为了保持账户安全，请不要分享或分发您的恢复码。我们建议用一个安全的密码管理器保存它们。\",\n        \"recovery_codes_msg3\": \"如果你生成新的恢复码，旧的将自动失效.\",\n        \"info_title\": \"了解双因素认证\",\n        \"info1\": \"如果客户端使用键盘交互认证，SSH 协议（SFTP/SCP/SSH 命令）将要求输入密码。\",\n        \"info2\": \"HTTP 协议指的是 Web UI 和 REST API。Web UI 会通过特定页面要求输入密码。对于 REST API，您需要通过 HTTP 头部添加密码。\",\n        \"info3\": \"FTP 没有支持双因素认证的标准方法。如果您启用了 FTP 支持，您必须在密码后添加 TOTP 密码。 例如，如果您的密码是 \\\"password\\\"，一次性密码是 \\\"123456\\\"，您必须使用 “password123456\\\" 作为密码。\",\n        \"info4\": \"由于每个请求都必须经过身份验证，而密码无法重复使用，因此不支持 WebDAV。\",\n        \"setup_title\": \"设置双因素认证\",\n        \"setup_msg\": \"使用您首选的身份验证应用程序（例如 Microsoft Authenticator、Google Authenticator、Authy、1Password 等）扫描二维码。它将生成一个身份验证代码供您输入。\",\n        \"setup_help\": \"如果您使用二维码有问题，请在应用上选择手动输入，然后输入代码：\",\n        \"disable_msg\": \"关闭双因素验证\",\n        \"disable_confirm\": \"您是否要为选定的用户禁用双因素认证？此操作通常仅在用户失去第二个认证因素的访问权限时才需要执行。\",\n        \"disable_question\": \"您确定要关闭双因素认证吗？\",\n        \"generate_question\": \"您想要生成一个新的密钥并使前一个密钥失效吗？任何注册的身份验证器应用都将停止工作\",\n        \"disabled\": \"已禁用双因素认证\",\n        \"recovery_codes_gen_err\": \"生成新恢复码失败\",\n        \"recovery_codes_get_err\": \"无法获取恢复代码\",\n        \"auth_code_invalid\": \"验证提供的认证码失败\",\n        \"auth_secret_gen_err\": \"无法生成身份验证密钥\",\n        \"save_err\": \"保存双因素认证配置失败\",\n        \"auth_code_required\": \"需要授权码。\",\n        \"no_protocol\": \"请至少选择一个协议\",\n        \"required_protocols\": \"你帐户的安全策略配置需要您给以下协议开启双因素认证: {{val}}\",\n        \"recovery_codes_generate\": \"生成新的恢复代码\",\n        \"recovery_codes_view\": \"查看恢复代码\"\n    },\n    \"share\": {\n        \"scope\": \"范围\",\n        \"scope_read\": \"读取\",\n        \"scope_write\": \"写入\",\n        \"scope_read_write\": \"读/写\",\n        \"scope_help\": \"对于 \\\"写入\\\" 和 \\\"读/写\\\" 范围，您必须定义一个单一的路径，并且该路径必须是一个目录。\",\n        \"path_help\": \"文件或目录路径，例如 /dir 或 /dir/file.txt\",\n        \"password_help\": \"如果设置，共享将受密码保护\",\n        \"max_tokens\": \"最大令牌数\",\n        \"max_tokens_help\": \"可以访问此分享的最大次数。0 表示没有限制\",\n        \"view_manage\": \"查看和管理分享\",\n        \"no_share\": \"没有共享\",\n        \"password_protected\": \"密码保护.\",\n        \"expiration_date\": \"过期: {{- val, datetime}}. \",\n        \"last_use\": \"上次使用: {{- val, datetime}}. \",\n        \"usage\": \"使用量: {{used}}/{{total}}. \",\n        \"used_tokens\": \"令牌使用次数: {{used}}. \",\n        \"scope_invalid\": \"无效范围\",\n        \"max_tokens_invalid\": \"无效的最大令牌\",\n        \"expiration_invalid\": \"无效过期时间\",\n        \"err_no_password\": \"您无权共享没有密码的文件/文件夹\",\n        \"expiration_out_of_range\": \"设置一个过期时间，并且确保它小于或等于 {{- val, datetime}}\",\n        \"generic\": \"保存分享时发生意外错误\",\n        \"path_required\": \"至少需要一个路径\",\n        \"path_write_scope\": \"写入范围只需要一个路径\",\n        \"nested_paths\": \"路径不能嵌套\",\n        \"expiration_past\": \"到期日必须在未来\",\n        \"usage_exceed\": \"超过最大共享使用量\",\n        \"expired\": \"分享已过期\",\n        \"browsable_multiple_paths\": \"带有多个路径的分享不可浏览\",\n        \"browsable_non_dir\": \"分享不是目录，所以它不能浏览\",\n        \"go\": \"转到共享\",\n        \"access_links_title\": \"共享访问链接\",\n        \"link_single_title\": \"单个压缩文件\",\n        \"link_single_desc\": \"您可以将共享内容下载为单个压缩文件\",\n        \"link_dir_title\": \"单个目录\",\n        \"link_dir_desc\": \"如果共享包含一个目录，您可以浏览和下载文件\",\n        \"link_uncompressed_title\": \"未压缩的文件\",\n        \"link_uncompressed_desc\": \"如果共享包含单个文件，它也可以不压缩下载\",\n        \"upload_desc\": \"您可以上传一个或多个文件到共享目录\",\n        \"expired_desc\": \"由于这个分享已过期，无法再访问\",\n        \"invalid_path\": \"共享目录丢失或不可访问\"\n    },\n    \"select2\": {\n        \"no_results\": \"未找到结果\",\n        \"searching\": \"正在搜索...\",\n        \"removeall\": \"删除所有项目\",\n        \"remove\": \"删除项目\"\n    },\n    \"change_pwd\": {\n        \"info\": \"出于安全考虑，请先输入您当前的密码，然后两次输入新密码，以验证您输入正确。更改密码后，您将被注销。\",\n        \"current\": \"当前密码\",\n        \"new\": \"新密码\",\n        \"confirm\": \"确认密码\",\n        \"save\": \"修改密码\",\n        \"required_fields\": \"请输入当前密码和新密码（两次），以确保密码正确。\",\n        \"no_match\": \"两次输入密码不一致\",\n        \"no_different\": \"新密码不能与当前密码相同\",\n        \"current_no_match\": \"当前密码不匹配\",\n        \"generic\": \"更改密码时发生意外错误\",\n        \"required\": \"需要更改密码。设置新密码以继续使用您的帐户\"\n    },\n    \"user\": {\n        \"view_manage\": \"查看和管理用户\",\n        \"username_reserved\": \"指定的用户名被保留\",\n        \"username_invalid\": \"指定名称无效，允许以下字符：a-zA-Z0-9 - _ . ~\",\n        \"home_required\": \"主目录是必填项\",\n        \"home_invalid\": \"主目录必须是绝对路径\",\n        \"pub_key_invalid\": \"无效公钥\",\n        \"priv_key_invalid\": \"无效的私钥\",\n        \"key_invalid_size\": \"无效的 RSA 公钥：支持的最小长度是 2048\",\n        \"key_insecure\": \"不允许使用不安全的公钥格式\",\n        \"err_primary_group\": \"只允许一个主要组\",\n        \"err_duplicate_group\": \"检测到重复的组\",\n        \"no_permissions\": \"目录权限是必填项\",\n        \"no_root_permissions\": \"主目录权限是必需的\",\n        \"err_permissions_generic\": \"无效的权限：请确保使用有效的绝对路径。\",\n        \"2fa_invalid\": \"双因素认证配置无效\",\n        \"recovery_codes_invalid\": \"无效的恢复代码\",\n        \"folder_path_required\": \"虚拟文件夹挂载路径是必需的\",\n        \"folder_duplicated\": \"检测到重复的虚拟文件夹\",\n        \"folder_overlapped\": \"检测到的虚拟文件夹重叠\",\n        \"folder_quota_size_invalid\": \"虚拟文件夹的容量大小配额必须大于或等于 -1\",\n        \"folder_quota_file_invalid\": \"虚拟文件夹的文件个数配额必须大于或等于 -1\",\n        \"folder_quota_invalid\": \"虚拟文件夹的大小和文件数量必须是 -1 或大于或等于 0\",\n        \"ip_filters_invalid\": \"无效的 IP 过滤器，请确保它们遵循 CIDR 表示法，例如 192.168.1.0/24。\",\n        \"src_bw_limits_invalid\": \"无效的单源带宽速度限制\",\n        \"share_expiration_invalid\": \"分享的过期时间必须大于或等于定义的默认值\",\n        \"file_pattern_path_invalid\": \"文件名模式过滤路径必须是绝对路径。\",\n        \"file_pattern_duplicated\": \"检测到重复的文件名模式过滤器\",\n        \"file_pattern_invalid\": \"无效的文件名模式过滤器\",\n        \"disable_active_2fa\": \"不能禁用具有活动配置的用户的双因素认证\",\n        \"pwd_change_conflict\": \"无法请求更改密码，同时防止密码被更改\",\n        \"two_factor_conflict\": \"您不能同时要求启用双因素认证并阻止其设置。\",\n        \"role_help\": \"具有角色的用户可以由具有相同角色的全球管理员和管理员管理\",\n        \"require_pwd_change\": \"需要修改密码\",\n        \"require_pwd_change_help\": \"用户需要更改 Web 客户端的密码才能激活帐户\",\n        \"groups_help\": \"组成员可以发布群组设置，但只是组成员除外。\",\n        \"primary_group\": \"主要组\",\n        \"secondary_groups\": \"次要组\",\n        \"membership_groups\": \"成员组\",\n        \"template_help\": \"每个用户设置用户名和至少一个密码和公钥\",\n        \"virtual_folders_help\": \"配额大小/文件数，-1 表示包含在用户配额内，0 表示无限制。不要为共享文件夹设置 -1。可以使用 MB/GB/TB 后缀。如果没有后缀，则默认为字节。\",\n        \"disconnect\": \"更新后断开用户\",\n        \"disconnect_help\": \"这种方式您强制用户重新登录，如果连接，则使用新的配置\",\n        \"invalid_quota_size\": \"无效的容量大小配额\",\n        \"expires_in\": \"有效期\",\n        \"expires_in_help\": \"帐户从创建起的天数过期。0 表示没有过期\",\n        \"additional_emails\": \"其它电子邮件\",\n        \"tls_certs\": \"TLS 证书\",\n        \"tls_certs_help\": \"TLS 证书可以用于 FTP 或 WebDAV 身份验证\",\n        \"tls_cert_help\": \"在此处粘贴一个PEM 编码的TLS证书\",\n        \"tls_cert_invalid\": \"TLS 证书无效\",\n        \"template_title\": \"从此模板创建一个或多个新用户\",\n        \"template_username_placeholder\": \"由指定用户名替换\",\n        \"template_password_placeholder\": \"由指定的密码替换\",\n        \"template_help1\": \"占位符将在已配置的存储后端的路径和凭据中被替换。\",\n        \"template_help2\": \"生成的用户可以保存或导出。导出的用户可以从此 SFTPGo 实例的 “维护” 部分或其他部分导入。\",\n        \"template_no_user\": \"未定义有效的用户，无法完成请求的操作\",\n        \"time_of_day_invalid\": \"日期时间无效。支持的格式 HH:MM\",\n        \"time_of_day_conflict\": \"无效的时间。结束时间不能早于开始时间\"\n    },\n    \"group\": {\n        \"view_manage\": \"查看和管理群组\",\n        \"err_delete_referenced\": \"不能删除带有关联用户的群组，请先删除关联\",\n        \"help\": \"%username% 占位符将被替换为相关用户的用户名，%role% 将被替换为他们的角色。\"\n    },\n    \"virtual_folders\": {\n        \"view_manage\": \"查看并管理虚拟文件夹\",\n        \"mount_path\": \"挂载路径，如 /vfolder\",\n        \"quota_size\": \"容量大小配额\",\n        \"quota_size_help\": \"0 表示没有限制。您可以使用 MB/GB/TB 后缀\",\n        \"quota_files\": \"文件个数配额\",\n        \"associations_summary\": \"用户：{{users}}. 群组：{{groups}}\",\n        \"template_title\": \"从此模板创建一个或多个新的虚拟文件夹\",\n        \"template_name_placeholder\": \"替换为指定虚拟文件夹的名称\",\n        \"template_help\": \"生成的虚拟文件夹可以保存或导出。导出的文件夹可以从此 SFTPGo 实例的 “维护” 部分或其他部分导入。\",\n        \"name\": \"虚拟文件夹名称\",\n        \"template_no_folder\": \"未定义有效的虚拟文件夹，无法完成请求的操作\"\n    },\n    \"storage\": {\n        \"title\": \"文件系统\",\n        \"label\": \"存储\",\n        \"local\": \"本地磁盘\",\n        \"s3\": \"S3 (兼容)\",\n        \"gcs\": \"GCS\",\n        \"azblob\": \"Azure Blob\",\n        \"encrypted\": \"加密的本地磁盘\",\n        \"sftp\": \"SFTP\",\n        \"http\": \"HTTP\",\n        \"home_dir\": \"根目录\",\n        \"home_dir_placeholder\": \"本地磁盘上目录的绝对路径\",\n        \"home_dir_help1\": \"留空为适当的默认值\",\n        \"home_dir_help2\": \"将该字段留空，并将存储设置为“本地磁盘”，以避免覆盖根目录。\",\n        \"home_dir_help3\": \"对于本地磁盘存储提供商，必须指定此文件夹。对于其他存储提供商，此文件夹将用于临时文件，您可以将其留空，系统将使用适当的默认值。\",\n        \"home_dir_invalid\": \"无效的根目录，请确保它是一个绝对路径\",\n        \"sftp_home_dir\": \"SFTP 根目录\",\n        \"sftp_home_help\": \"限制访问此 SFTP 路径。例如：“/somedir/subdir”\",\n        \"os_read_buffer\": \"下载缓存(MB)\",\n        \"os_buffer_help\": \"0 表示没有缓冲区\",\n        \"os_write_buffer\": \"上传缓存(MB)\",\n        \"bucket\": \"桶\",\n        \"region\": \"区域\",\n        \"access_key\": \"访问键\",\n        \"access_secret\": \"访问密钥\",\n        \"sse_customer_key\": \"服务器端加密密钥\",\n        \"sse_customer_key_help\": \"您可以用此密钥存储您的数据，但如果丢失或更改此密钥，您将丢失所有加密的文件。 未加密或使用不同密钥加密的文件将无法访问\",\n        \"endpoint\": \"端点\",\n        \"endpoint_help\": \"对于AWS S3，请留空以使用指定区域的默认端点\",\n        \"sftp_endpoint_help\": \"端点作为主机:端口。端口总是必需的\",\n        \"ul_part_size\": \"上传部分大小 (MB)\",\n        \"part_size_help\": \"0 表示默认值(5 MB)，最小值为5\",\n        \"gcs_part_size_help\": \"0 表示默认 (16 MB)\",\n        \"ul_concurrency\": \"上传并发\",\n        \"ul_concurrency_help\": \"并行上传的部分数。0 表示默认值 (5)\",\n        \"dl_part_size\": \"下载部分大小 (MB)\",\n        \"dl_concurrency\": \"下载并发\",\n        \"dl_concurrency_help\": \"同时下载多少个部分。 0 表示默认值 (5)\",\n        \"ul_part_timeout\": \"上传部分超时\",\n        \"ul_part_timeout_help\": \"最大时间限制，以秒为单位上传单个零件。0 表示无限制\",\n        \"gcs_ul_part_timeout_help\": \"最大时间限制，以秒为单位上传单个零件。0 表示默认值(32)\",\n        \"dl_part_timeout\": \"上传部分超时\",\n        \"dl_part_timeout_help\": \"最大时间限制，以秒为单位下载单个部分。0 表示无限制\",\n        \"key_prefix\": \"密钥前缀\",\n        \"key_prefix_help\": \"限制对具有指定前缀的密钥的访问。例如：\\\"somedir/subdir/\\\"。\",\n        \"class\": \"存储类别\",\n        \"acl\": \"访问控制列表\",\n        \"role_arn\": \"角色 ARN\",\n        \"role_arn_help\": \"可选的 IAM ARN 角色\",\n        \"s3_path_style\": \"使用 path 风格地址，即 \\\"endpoint/BUCKET/KEY\\\"\",\n        \"credentials_file\": \"凭据文件\",\n        \"credentials_file_help\": \"从 JSON 文件添加或更新凭据\",\n        \"auto_credentials\": \"自动凭据\",\n        \"auto_credentials_help\": \"使用默认应用凭据或来自环境变量的凭据。\",\n        \"container\": \"容器\",\n        \"account_name\": \"账号名称\",\n        \"account_key\": \"帐户密钥\",\n        \"sas_url\": \"SAS URL\",\n        \"sas_url_help\": \"共享访问签名（SAS）URL可以替代账户名/密钥使用。\",\n        \"emulator\": \"使用仿真器\",\n        \"passphrase\": \"密码\",\n        \"passphrase_help\": \"用于计算每个对象加密密钥的密码\",\n        \"passphrase_key_help\": \"用于保护您的私钥的密码短语（如果有的话）\",\n        \"fingerprints\": \"指纹\",\n        \"fingerprints_help\": \"SHA256 指纹用于连接到外部 SFTP 服务器时验证主机密钥，每行一个。如果为空，将会接受任何主机密钥：这是个安全风险！\",\n        \"sftp_buffer\": \"缓冲区大小 (MB)\",\n        \"sftp_buffer_help\": \"缓冲区大小大于 0 可以同时传输\",\n        \"sftp_concurrent_reads\": \"禁用并行读取\",\n        \"relaxed_equality_check\": \"宽松的平等检查\",\n        \"relaxed_equality_check_help\": \"启用后只考虑端点来确定不同配置是否指向同一服务器，默认情况下，端点和用户名都必须匹配\",\n        \"api_key\": \"API 密钥\",\n        \"fs_error\": \"文件系统配置错误\",\n        \"bucket_required\": \"$t(storage.fs_error)：需要存储桶\",\n        \"region_required\": \"$t(storage.fs_error): 需要区域\",\n        \"key_prefix_invalid\": \"$t(storage.fs_error): 无效的密钥前缀，不能以 “/” 开头\",\n        \"ul_part_size_invalid\": \"$t(storage.fs_error): 无效的上传部分尺寸\",\n        \"ul_concurrency_invalid\": \"$t(storage.fs_error): 并行上传无效\",\n        \"dl_part_size_invalid\": \"$t(storage.fs_error): 无效下载部分大小\",\n        \"dl_concurrency_invalid\": \"$t(storage.fs_error): 并行下载无效\",\n        \"access_key_required\": \"$t(storage.fs_error)：需要访问密钥\",\n        \"access_secret_required\": \"$t(storage.fs_error)：需要访问密码\",\n        \"credentials_required\": \"$t(storage.fs_error)：需要认证\",\n        \"container_required\": \"$t(storage.fs_error)：需要容器\",\n        \"account_name_required\": \"$t(storage.fs_error)：需要帐户名\",\n        \"sas_url_invalid\": \"$t(storage.fs_error): 无效 SAS 链接\",\n        \"passphrase_required\": \"$t(storage.fs_error)：需要密码\",\n        \"endpoint_invalid\": \"$t(storage.fs_error): 端点无效\",\n        \"endpoint_required\": \"$t(storage.fs_error): 需要端点\",\n        \"username_required\": \"$t(storage.fs_error)：需要用户名\"\n    },\n    \"oidc\": {\n        \"token_expired\": \"您的 OpenID 令牌已过期，请重新登录\",\n        \"token_invalid_webadmin\": \"您的 OpenID 令牌对 WebAdmin UI 无效。请退出您的 OpenID 服务器并登录 WebAdmin\",\n        \"token_invalid_webclient\": \"您的 OpenID 令牌对 WebClient UI 无效。请退出您的 OpenID 服务器并登录 WebClient\",\n        \"token_exchange_err\": \"无法交换 OpenID 令牌\",\n        \"token_invalid\": \"无效的 OpenID 令牌\",\n        \"role_admin_err\": \"OpenID 角色不正确，登录用户不是管理员\",\n        \"role_user_err\": \"OpenID 角色不正确，登录用户是管理员\",\n        \"get_user_err\": \"无法获取与 OpenID 令牌关联的用户\"\n    },\n    \"oauth2\": {\n        \"auth_verify_error\": \"无法验证 OAuth2 代码\",\n        \"auth_validation_error\": \"无法验证 OAuth2 代码\",\n        \"auth_invalid\": \"无效的 OAuth2 代码\",\n        \"token_exchange_err\": \"无法从验证码中获取 OAuth2 令牌\",\n        \"no_refresh_token\": \"OAuth2 提供商返回了一个空的令牌。一些提供商只在用户首次授权时返回令牌。 如果您在过去已经与该用户注册过 SFTPGo，撤销访问权限并再试一次。这样您就会使之前的令牌无效 \",\n        \"success\": \"将以下字符串复制到 SMTP OAuth2 Token 配置字段：\",\n        \"client_id_required\": \"需要客户端 ID\",\n        \"client_secret_required\": \"需要客户端密钥\",\n        \"refresh_token_required\": \"需要刷新令牌\"\n    },\n    \"filters\": {\n        \"password_strength\": \"密码强度\",\n        \"password_strength_help\": \"建议常用情况下使用50-70范围内的值。0 表示禁用，将接受任何密码\",\n        \"password_expiration\": \"密码失效\",\n        \"password_expiration_help\": \"密码过期天数。0 表示不过期\",\n        \"default_shares_expiration\": \"默认共享过期\",\n        \"default_shares_expiration_help\": \"新分享的默认到期天数\",\n        \"max_shares_expiration\": \"最大共享过期时间\",\n        \"max_shares_expiration_help\": \"当用户创建或更新共享时允许的最大到期天数\",\n        \"directory_permissions\": \"单目录权限\",\n        \"directory_permissions_help\": \"通道支持通配符，例如 “/incoming/*” 匹配 “/incoming” 中的任何目录\",\n        \"directory_path_help\": \"目录路径，例如：'/dir'\",\n        \"directory_patterns\": \"目录名称模式限制\",\n        \"directory_patterns_help\": \"以逗号分隔的被拒绝或允许的文件/目录，基于 shell 模式。匹配是不区分大小写的。\",\n        \"max_sessions\": \"最大会话\",\n        \"max_sessions_help\": \"最大并行会话数。0 表示没有限制\",\n        \"denied_protocols\": \"被拒绝的协议\",\n        \"denied_login_methods\": \"被拒绝的登录方法\",\n        \"denied_login_methods_help\": \"\\\"password\\\" 对所有支持的协议有效，而 \\\"password-over-SSH\\\" 仅对 SSH/SFTP/SCP 协议有效。\",\n        \"web_client_options\": \"Web 客户端/REST API\",\n        \"max_upload_size\": \"最大上传大小\",\n        \"max_upload_size_help\": \"单个文件的最大上传大小。0 表示没有限制。您可以使用 MB/GB/TB 后缀\",\n        \"max_upload_size_invalid\": \"无效的最大上传文件大小\",\n        \"upload_bandwidth\": \"带宽上传(KB/s)\",\n        \"download_bandwidth\": \"带宽下载 (KB/s)\",\n        \"upload_bandwidth_help\": \"上传 (KB/s). 0 表示没有限制\",\n        \"download_bandwidth_help\": \"下载 (KB/s). 0 表示没有限制\",\n        \"src_bandwidth_limit\": \"每个源带宽速度限制\",\n        \"upload_data_transfer\": \"上传数据传输(MB)\",\n        \"upload_data_transfer_help\": \"允许上传的最大数据传输。0 表示没有限制\",\n        \"download_data_transfer\": \"下载数据传输(MB)\",\n        \"download_data_transfer_help\": \"允许下载的最大数据传输。0 表示没有限制\",\n        \"total_data_transfer\": \"总数据传输(MB)\",\n        \"total_data_transfer_help\": \"允许上传的最大数据传输量 + 下载。替换单个限制。0 表示没有限制\",\n        \"start_directory\": \"初始目录\",\n        \"start_directory_help\": \"用于替代 \\\"/\\\" 的初始目录。适用于 SFTP/FTP/HTTP。\",\n        \"tls_username\": \"TLS 用户名\",\n        \"tls_username_help\": \"定义要用作用户名的 TLS 证书字段。如果禁用了互相的 TLS 则忽略\",\n        \"ftp_security\": \"FTP 安全\",\n        \"ftp_security_help\": \"如果已经为所有FTP用户全局要求TLS，则此设置将被忽略。\",\n        \"hooks\": \"钩子\",\n        \"hook_ext_auth_disabled\": \"外部认证已禁用\",\n        \"hook_pre_login_disabled\": \"登录前已禁用\",\n        \"hook_check_password_disabled\": \"检查密码已禁用\",\n        \"is_anonymous\": \"匿名用户\",\n        \"is_anonymous_help\": \"匿名用户支持 FTP 和 WebDAV 协议，并且拥有只读访问权限\",\n        \"disable_fs_checks\": \"禁用文件系统检查\",\n        \"disable_fs_checks_help\": \"禁止检查是否存在并自动创建主目录和虚拟文件夹\",\n        \"api_key_auth_help\": \"允许在REST API中用API密钥模拟用户\",\n        \"external_auth_cache_time\": \"外部认证缓存时间\",\n        \"external_auth_cache_time_help\": \"缓存时间，以秒为单位的用户使用外部认证连接。0 表示没有缓存\",\n        \"access_time\": \"访问时间限制\",\n        \"access_time_help\": \"没有限制意味着始终允许访问，时间必须以 HH:MM 格式设置。\"\n    },\n    \"admin\": {\n        \"role_permissions\": \"管理员角色不能拥有“*”权限\",\n        \"view_manage\": \"查看和管理管理员\",\n        \"self_delete\": \"不能删除自己\",\n        \"self_permissions\": \"您无法更改您的权限\",\n        \"self_disable\": \"您不能禁用自己\",\n        \"self_role\": \"您不能添加/更改您的角色\",\n        \"password_help\": \"如果留空，当前密码将不会被更改\",\n        \"role_help\": \"将角色限制为仅允许管理员管理具有相同角色的用户。具有角色的管理员不能是超级管理员。\",\n        \"users_groups\": \"用户的组\",\n        \"users_groups_help\": \"此管理员自动为新用户选择组。管理员仍然可以选择不同的组。 这些设置仅用于此管理员界面，它们将在REST API/钩子中被忽略\",\n        \"group_membership\": \"添加为成员\",\n        \"group_primary\": \"添加为主要的\",\n        \"group_secondary\": \"添加为次要项\",\n        \"user_page_pref\": \"用户页首选项\",\n        \"user_page_pref_help\": \"您可以从用户页面隐藏某些部分。这些并不是安全设置，服务器端不会强制执行它们。它们仅用于简化添加/更新用户页面。\",\n        \"hide_sections\": \"隐藏版块\",\n        \"default_users_expiration\": \"默认用户过期\",\n        \"default_users_expiration_help\": \"新用户的默认到期天数\",\n        \"require_pwd_change_help\": \"下次登录需要更改密码\"\n    },\n    \"connections\": {\n        \"view_manage\": \"查看和管理连接\",\n        \"started\": \"已开始\",\n        \"remote_address\": \"远程地址\",\n        \"last_activity\": \"最后活动\",\n        \"disconnect_confirm_btn\": \"是，断开连接\",\n        \"disconnect_confirm\": \"您想要删除所选项目吗？此操作是不可逆的\",\n        \"disconnect_ko\": \"无法断开选中的连接\",\n        \"upload\": \"上传: \\\"{{- path}}\\\"\",\n        \"download\": \"下载: \\\"{{- path}}\\\"\",\n        \"upload_info\": \"$t(connections.upload). 大小: {{- size}}. 速度: {{- speed}}\",\n        \"download_info\": \"$t(connections.download). 大小: {{- size}}. 速度: {{- speed}}\",\n        \"client\": \"客户端: {{- val}}\"\n    },\n    \"role\": {\n        \"view_manage\": \"查看和管理角色\",\n        \"err_delete_referenced\": \"不能删除与关联管理员关联的角色，请先删除关联\"\n    },\n    \"ip_list\": {\n        \"view_manage\": \"查看和管理 IP 列表\",\n        \"defender_list\": \"守护者\",\n        \"allow_list\": \"允许列表\",\n        \"ratelimiters_safe_list\": \"速率限制安全列表\",\n        \"ip_net\": \"IP/网络\",\n        \"protocols\": \"协议\",\n        \"any\": \"任何\",\n        \"allow\": \"允许\",\n        \"deny\": \"拒绝\",\n        \"ip_net_help\": \"CIDR 格式的 IP 地址或网络，例如：\\\"192.168.1.1 或 10.8.0.100/32 或 2001:db8:1234:/48\\\"\",\n        \"ip_invalid\": \"IP 地址无效\",\n        \"net_invalid\": \"无效的网络\",\n        \"duplicated\": \"指定的 IP /网络已存在\",\n        \"search\": \"IP/网络或初始部分\",\n        \"defender_disabled\": \"在您的配置中禁用了守护程序\",\n        \"allow_list_disabled\": \"允许列表在您的配置中被禁用\",\n        \"ratelimiters_disabled\": \"在您的配置中禁用速率限制\"\n    },\n    \"defender\": {\n        \"view_manage\": \"查看和管理自动阻止列表\",\n        \"ip\": \"IP 地址\",\n        \"ban_time\": \"屏蔽直到\",\n        \"score\": \"分数\"\n    },\n    \"status\": {\n        \"desc\": \"服务器状态\",\n        \"ssh\": \"SSH/SFTP 服务器\",\n        \"active\": \"状态：活跃\",\n        \"disabled\": \"状态：禁止\",\n        \"error\": \"状态：错误\",\n        \"proxy_on\": \"PROXY 协议已启用\",\n        \"address\": \"地址\",\n        \"ssh_auths\": \"身份验证方法\",\n        \"ssh_commands\": \"接受的命令\",\n        \"host_key\": \"主机密钥\",\n        \"fingeprint\": \"指纹\",\n        \"algorithms\": \"算法\",\n        \"algorithm\": \"算法\",\n        \"ssh_pub_key_algo\": \"公钥验证算法\",\n        \"ssh_mac_algo\": \"消息验证码 (MAC) 算法\",\n        \"ssh_kex_algo\": \"密钥交换算法 (KEX)\",\n        \"ssh_cipher_algo\": \"加密方法\",\n        \"ftp\": \"FTP 服务器\",\n        \"ftp_passive_range\": \"被动模式端口范围\",\n        \"ftp_passive_ip\": \"被动 IP\",\n        \"tls\": \"TLS\",\n        \"tls_disabled\": \"停用\",\n        \"tls_explicit\": \"需要明确模式 (FTPES)\",\n        \"tls_implicit\": \"隐式模式 (FTPS)，废弃，推荐FTPES\",\n        \"tls_mixed\": \"普通和显式(FTPES) 模式\",\n        \"webdav\": \"WebDAV 服务器\",\n        \"rate_limiters\": \"速率限制\"\n    },\n    \"maintenance\": {\n        \"backup\": \"备份\",\n        \"backup_do\": \"备份您的数据\",\n        \"backup_ok\": \"已成功恢复备份\",\n        \"backup_invalid_file\": \"无效的备份文件，请确保它是一个有效内容的 JSON 文件\",\n        \"restore_error\": \"无法恢复您的备份，请检查服务器日志获取更多详细信息\",\n        \"restore\": \"恢复\",\n        \"backup_file\": \"备份文件\",\n        \"backup_file_help\": \"从 JSON 备份文件导入数据\",\n        \"restore_mode0\": \"添加和更新\",\n        \"restore_mode1\": \"只添加\",\n        \"restore_mode2\": \"添加、更新和断开\",\n        \"after_restore\": \"恢复后\",\n        \"quota_mode0\": \"无配额更新\",\n        \"quota_mode1\": \"更新配额信息\",\n        \"quota_mode2\": \"更新配额限制的用户\"\n    },\n    \"acme\": {\n        \"title\": \"ACME\",\n        \"generic_error\": \"无法获取 TLS 证书，请检查服务器日志获取更多详细信息\",\n        \"help\": \"您可在此使用 ACME 协议和 HTTP-01 挑战类型为您的 SFTPGo 服务请求免费的 TLS 证书。您必须在您拥有的自定义域下创建一个 DNS 记录，该记录解析为您的 SFTPGo 公共 IP 地址，并且端口 80 必须是公开可访问的。您可以在此处设置最常见用例和单节点配置的配置选项，对于高级配置，请参阅 SFTPGo 文档。应用更改需要重启服务。\",\n        \"domain_help\": \"可以指定多个域名，使用逗号或空格分隔。这些域名将包含在同一个证书中。\",\n        \"email_help\": \"用于注册和恢复联系人的电子邮件\",\n        \"port_help\": \"如果不是80，您必须配置反向代理\",\n        \"protocols_help\": \"使用获得的证书用于指定的协议\"\n    },\n    \"smtp\": {\n        \"title\": \"SMTP\",\n        \"err_required_fields\": \"发件人地址和用户名不能同时为空\",\n        \"help\": \"如果有，请设置 SMTP 配置，使用 env vars 或配置文件替换定义的配置\",\n        \"host\": \"服务器名称\",\n        \"host_help\": \"如果空白，配置被禁用\",\n        \"auth\": \"认证\",\n        \"encryption\": \"加密\",\n        \"sender\": \"发送方\",\n        \"debug\": \"调试日志\",\n        \"domain_help\": \"HELO 域名。留空则使用服务器主机名\",\n        \"test_recipient\": \"发送测试邮件的地址\",\n        \"oauth2_provider\": \"OAuth2 提供程序\",\n        \"oauth2_provider_help\": \"用户身份验证后重定向到的 URI\",\n        \"oauth2_tenant\": \"OAuth2 租户\",\n        \"oauth2_tenant_help\": \"Azure 租户。典型值包括 \\\"common\\\"、\\\"organizations\\\"、\\\"consumers\\\" 或租户标识符。\",\n        \"oauth2_client_id\": \"OAuth2 客户端 ID\",\n        \"oauth2_client_secret\": \"OAuth2 客户端密钥\",\n        \"oauth2_token\": \"OAuth2 令牌\",\n        \"recipient_required\": \"指定要发送测试邮件的收件人\",\n        \"test_error\": \"无法发送测试电子邮件，请检查服务器日志获取更多详细信息\",\n        \"test_ok\": \"发送测试邮件时没有报告错误。请检查您的收件箱以确认\",\n        \"oauth2_flow_error\": \"无法获取 URI 来启动 OAuth2 流程\",\n        \"oauth2_question\": \"您是否要开始 OAuth2 流程以获取令牌？\"\n    },\n    \"sftp\": {\n        \"help\": \"您可在此启用默认禁用的算法。您无需设置已通过环境变量或配置文件定义的值。应用更改后需要重新启动服务。\",\n        \"host_key_algos\": \"主机密钥算法\"\n    },\n    \"branding\": {\n        \"title\": \"品牌\",\n        \"help\": \"您可以在此自定义 SFTPGo 以适合您的品牌，并在登录页面添加免责声明。\",\n        \"short_name\": \"简称\",\n        \"logo\": \"徽标\",\n        \"logo_help\": \"PNG 图像，最大接受尺寸为 512x512，默认徽标尺寸为 256x256。\",\n        \"favicon\": \"收藏夹图标\",\n        \"disclaimer_name\": \"免责标题\",\n        \"disclaimer_url\": \"免责声明 URL\",\n        \"invalid_png\": \"无效的 PNG 图像\",\n        \"invalid_png_size\": \"无效的 PNG 图像尺寸\",\n        \"invalid_disclaimer_url\": \"免责声明 URL 必须是 http 或 https 链接\"\n    },\n    \"events\": {\n        \"search\": \"搜索日志\",\n        \"fs_events\": \"文件系统事件\",\n        \"provider_events\": \"提供者事件\",\n        \"other_events\": \"其他事件\",\n        \"quota_exceeded\": \"已超过配额\",\n        \"date_range\": \"日期范围\",\n        \"upload\": \"上传\",\n        \"download\": \"下载\",\n        \"mkdir\": \"创建目录\",\n        \"rmdir\": \"删除目录\",\n        \"rename\": \"重命名\",\n        \"delete\": \"移除\",\n        \"copy\": \"复制\",\n        \"first_upload\": \"首次上传\",\n        \"first_download\": \"首次下载\",\n        \"ssh_cmd\": \"SSH命令\",\n        \"add\": \"添加项\",\n        \"update\": \"更新\",\n        \"login_failed\": \"登录失败\",\n        \"login_ok\": \"登录成功\",\n        \"login_missing_user\": \"使用不存在的用户登录\",\n        \"no_login_tried\": \"未尝试登录\",\n        \"algo_negotiation_failed\": \"算法协商失败\",\n        \"datetime\": \"日期和时间\",\n        \"action\": \"操作\",\n        \"path\": \"路径\",\n        \"object\": \"对象\",\n        \"event\": \"事件\"\n    },\n    \"provider_objects\": {\n        \"user\": \"用户\",\n        \"folder\": \"文件夹\",\n        \"group\": \"组\",\n        \"admin\": \"管理\",\n        \"api_key\": \"API 密钥\",\n        \"share\": \"分享\",\n        \"event_action\": \"操作\",\n        \"event_rule\": \"规则\",\n        \"role\": \"角色\",\n        \"ip_list_entry\": \"IP 列表项\",\n        \"configs\": \"配置\"\n    },\n    \"actions\": {\n        \"view_manage\": \"查看和管理事件的规则操作\",\n        \"err_delete_referenced\": \"无法删除带有关联规则的操作，请先删除关联\",\n        \"http_url\": \"服务器 URL\",\n        \"http_url_help\": \"例如：`https://host:port/path`。URL 路径中支持使用占位符。\",\n        \"http_url_required\": \"必须填写 URL\",\n        \"http_url_invalid\": \"URL 无效，仅支持 http 和 https 协议。\",\n        \"http_part_name_required\": \"需要提供 HTTP 部件名称\",\n        \"http_part_body_required\": \"如果未提供文件路径，则需要提供 HTTP 部件正文\",\n        \"http_multipart_body_error\": \"多部分请求不需要正文。请求正文将根据指定的部件构建。\",\n        \"http_multipart_ctype_error\": \"对于多部分请求，Content-Type 会自动设置。\",\n        \"path_duplicated\": \"路径重复\",\n        \"command_required\": \"命令是必需的\",\n        \"command_invalid\": \"命令无效，它必须是绝对路径\",\n        \"email_recipient_required\": \"至少需要一个电子邮件收件人\",\n        \"email_subject_required\": \"邮件主题为必填项\",\n        \"email_body_required\": \"电子邮件正文是必填项\",\n        \"retention_directory_required\": \"至少有一个需要检查的目录\",\n        \"path_required\": \"至少需要一个路径\",\n        \"source_dest_different\": \"来源和目标路径必须是不同的\",\n        \"root_not_allowed\": \"不允许使用根路径（/）\",\n        \"archive_name_required\": \"压缩归档名称是必需的\",\n        \"idp_template_required\": \"需要一个用户或管理员模板\",\n        \"threshold\": \"阈值\",\n        \"threshold_help\": \"当用户的密码在剩余天数小于或等于此阈值时，将生成电子邮件通知。\",\n        \"disable_threshold\": \"禁用阈值\",\n        \"disable_threshold_help\": \"自上次登录或创建后几天内停留在禁用用户\",\n        \"delete_threshold\": \"删除阈值\",\n        \"delete_threshold_help\": \"自上次登录或创建后几天内不活动即可删除用户\",\n        \"inactivity_threshold_required\": \"必须定义至少一个非活动阈值\",\n        \"inactivity_thresholds_invalid\": \"删除阈值必须大于停用阈值\",\n        \"idp_mode_add_update\": \"创建或更新\",\n        \"idp_mode_add\": \"如果不存在则创建\",\n        \"template_user_help\": \"JSON 格式的 SFTPGo 用户模板。支持占位符。\",\n        \"template_admin_help\": \"JSON 格式的 SFTPGo 管理员模板。支持占位符\",\n        \"placeholders_help\": \"支持占位符\",\n        \"http_headers\": \"HTTP 标头\",\n        \"query_parameters\": \"查询字符串参数\",\n        \"http_timeout_help\": \"对于以文件作为附件的多部分请求，此项会被忽略。\",\n        \"body\": \"主体\",\n        \"http_body_help\": \"支持占位符。对于 HTTP GET 请求将被忽略。对于多部分请求，请保持为空。\",\n        \"multipart_body\": \"多主体\",\n        \"multipart_body_help\": \"HTTP 多部分请求允许将一个或多个数据集组合成一个请求正文。对于每个部分，您可以设置文件路径或将正文作为文本。文件路径、正文和头部值中均支持占位符。\",\n        \"http_part_name\": \"部件名称\",\n        \"http_part_file\": \"文件路径\",\n        \"http_part_headers\": \"额外的部件头信息，每行一个，格式为 \\\"key: value\\\"。\",\n        \"command_help\": \"要执行命令的绝对路径\",\n        \"command_args\": \"参数\",\n        \"command_args_help\": \"逗号分隔的命令参数。支持占位符。\",\n        \"command_env_vars_help\": \"值支持占位符。将值设置为不带引号的“$”意味着从环境中检索键，但不包括以SFTPGO_开头的键\",\n        \"email_recipients\": \"至\",\n        \"email_recipients_help\": \"逗号分隔的收件人。支持占位符\",\n        \"email_bcc\": \"密件抄送\",\n        \"email_bcc_help\": \"逗号分隔的密件抄送地址。支持占位符\",\n        \"email_subject\": \"主题\",\n        \"content_type\": \"内容类型\",\n        \"attachments\": \"附件\",\n        \"attachments_help\": \"以逗号分隔的路径列表，支持占位符。总大小限制为 10 MB。\",\n        \"data_retention\": \"数据保留\",\n        \"data_retention_help\": \"为每个路径设置数据保留时间（以小时为单位）。保留规则将递归应用。将保留时间设置为 0 表示排除指定路径。\",\n        \"delete_empty_dirs\": \"删除空目录\",\n        \"fs_action\": \"文件系统操作\",\n        \"paths_src_dst_help\": \"SFTPGo 用户看到的路径。支持占位符。所需的权限被自动授予。\",\n        \"source_path\": \"来源\",\n        \"target_path\": \"目标\",\n        \"paths_help\": \"SFTPGo 用户可以看到的以逗号分隔的路径。支持占位符。所需的权限将自动授予。\",\n        \"archive_path\": \"归档路径\",\n        \"archive_path_help\": \"SFTPGo 用户可以看到的完整路径，用于创建 zip 存档。支持占位符。如果指定的文件已存在，它将被覆盖。\",\n        \"placeholders_modal_title\": \"支持的占位符\",\n        \"update_mod_times\": \"更新时间戳\",\n        \"types\": {\n            \"http\": \"HTTP\",\n            \"email\": \"邮箱\",\n            \"backup\": \"备份\",\n            \"user_quota_reset\": \"用户配额重置\",\n            \"folder_quota_reset\": \"文件夹配额重置\",\n            \"transfer_quota_reset\": \"转账配额重置\",\n            \"data_retention_check\": \"数据保留检查\",\n            \"filesystem\": \"文件系统\",\n            \"password_expiration_check\": \"密码过期检查\",\n            \"user_expiration_check\": \"用户过期检查\",\n            \"user_inactivity_check\": \"用户不活跃检查\",\n            \"idp_check\": \"身份提供者帐户检查\",\n            \"rotate_logs\": \"轮换日志文件\",\n            \"command\": \"命令\"\n        },\n        \"fs_types\": {\n            \"rename\": \"重命名\",\n            \"delete\": \"删除\",\n            \"path_exists\": \"路径已存在\",\n            \"compress\": \"压缩\",\n            \"copy\": \"复制\",\n            \"create_dirs\": \"创建目录\"\n        },\n        \"placeholders_modal\": {\n            \"name\": \"用户名、虚拟文件夹名称、提供商事件的管理员用户名、TLS证书事件的域名\",\n            \"event\": \"事件名称，例如 \\\"upload\\\"（上传）、\\\"download\\\"（下载）用于文件系统事件，或 \\\"add\\\"（添加）、\\\"update\\\"（更新）用于提供程序事件。\",\n            \"status\": \"文件系统事件的状态。1 表示没有错误，2 表示发生了一个通用错误，3 表示超过配额错误。\",\n            \"status_string\": \"作为字符串的状态。可能的值 \\\"OK\\\", \\\"KO\\\"\",\n            \"error_string\": \"错误详细信息。如果没有发生错误，则替换为空字符串\",\n            \"virtual_path\": \"SFTPGo 用户看到的路径，例如 \\\"/adir/afile.txt\\\"。\",\n            \"escaped_virtual_path\": \"HTTP 请求字符串编码路径，例如 “%2Fadir%2Fafile.txt”。\",\n            \"virtual_dir_path\": \"\\\"VirtualPath\\\" 的父目录，例如，如果 \\\"VirtualPath\\\" 是 \\\"/adir/afile.txt\\\"，那么 \\\"VirtualDirPath\\\" 就是 \\\"/adir\\\"。\",\n            \"fs_path\": \"完整的文件系统路径，例如 \\\"/user/homedir/adir/afile.txt\\\" 或者在 Windows 上的 \\\"C:/data/user/homedir/adir/afile.txt\\\"。\",\n            \"ext\": \"文件扩展名，例如 \\\".txt\\\" 如果文件名是 \\\"afile.txt\\\"\",\n            \"object_name\": \"文件/目录名，例如 \\\"afile.txt\\\" 或提供者对象名称\",\n            \"object_basename\": \"无需文件拓展名；例如：文件名是\\\"afile.txt\\\"，仅需\\\"afile\\\" ；\",\n            \"object_type\": \"提供者事件的对象类型：如 \\\"user\\\"（用户）、\\\"group\\\"（组）、\\\"admin\\\"（管理员）等。\",\n            \"virtual_target_path\": \"重命名和复制操作的虚拟目标路径\",\n            \"virtual_target_dir_path\": \"\\\"虚拟目标路径\\\" 的父目录\",\n            \"target_name\": \"重命名和复制操作的目标对象名称\",\n            \"fs_target_path\": \"重命名和复制操作的完整文件系统目标路径\",\n            \"file_size\": \"文件大小 (字节)\",\n            \"elapsed\": \"文件系统事件已用毫秒时间\",\n            \"protocol\": \"协议，例如，\\\"SFTP\\\", \\\"FTP\\\"\",\n            \"ip\": \"客户端 IP 地址\",\n            \"role\": \"用户或管理员角色\",\n            \"timestamp\": \"自纪元以来的纳秒事件时间戳\",\n            \"datetime\": \"事件时间戳格式为 YYYY-MM-DDTHH:MM:SS.ZZZ\",\n            \"year\": \"事件年份格式为四位数\",\n            \"month\": \"事件月份格式化为两位数\",\n            \"day\": \"事件天数格式为两位数\",\n            \"hour\": \"事件小时数格式为两位数\",\n            \"minute\": \"事件分钟数格式为两位数\",\n            \"email\": \"对于文件系统事件，这是执行操作的用户关联的电子邮件。对于提供者事件，这是与受影响的用户或管理员关联的电子邮件。在其他情况下为空。\",\n            \"object_data\": \"提供者对象数据已序列化为 JSON，并删除了敏感字段\",\n            \"object_data_string\": \"提供者对象数据已转义为 JSON 字符串，并删除了敏感字段\",\n            \"retention_reports\": \"数据保留报告以 ZIP 压缩的 CSV 文件形式提供。支持作为电子邮件附件、多部分 HTTP 请求的文件路径以及 HTTP 请求主体的单个参数。\",\n            \"idp_field\": \"包含字符串的身份提供者自定义字段\",\n            \"metadata\": \"已下载文件的云存储元数据序列化为 JSON\",\n            \"metadata_string\": \"已下载文件的 JSON 云存储元数据已跳过字符串\",\n            \"uid\": \"唯一 ID\"\n        }\n    },\n    \"rules\": {\n        \"view_manage\": \"查看和管理事件规则\",\n        \"trigger\": \"触发器\",\n        \"run_confirm\": \"您想要执行选定的规则吗？\",\n        \"run_confirm_btn\": \"是的，运行\",\n        \"run_error_generic\": \"无法运行选中的规则\",\n        \"run_ok\": \"规则动作已启动\",\n        \"run\": \"运行\",\n        \"invalid_fs_min_size\": \"无效的最小值\",\n        \"invalid_fs_max_size\": \"无效的最大值\",\n        \"action_required\": \"至少需要一个操作\",\n        \"fs_event_required\": \"至少需要一个文件系统事件\",\n        \"provider_event_required\": \"至少需要一个提供者事件\",\n        \"schedule_required\": \"至少需要一个计划\",\n        \"schedule_invalid\": \"无效的计划\",\n        \"duplicate_actions\": \"检测到重复操作\",\n        \"sync_failure_actions\": \"故障操作不支持同步执行\",\n        \"sync_unsupported\": \"仅某些文件系统事件和身份提供者登录支持同步执行\",\n        \"sync_unsupported_fs_event\": \"同步执行仅支持上传（upload）和前置（pre-*）文件系统事件。\",\n        \"only_failure_actions\": \"至少需要一个非失败的操作\",\n        \"sync_action_required\": \"事件 “{{val}}” 至少需要一个同步操作\",\n        \"scheduler_help\": \"小时：0-23。星期几：0-6（星期日-星期六）。日期：1-31。月份：1-12。星号（*）表示匹配字段的所有值，例如每天、每月等。\",\n        \"concurrent_run\": \"允许多个实例的并行执行\",\n        \"protocol_filters\": \"协议过滤器\",\n        \"status_filters\": \"状态筛选器\",\n        \"object_filters\": \"对象过滤器\",\n        \"name_filters\": \"名称过滤器\",\n        \"name_filters_help\": \"类似于 Shell 的模式过滤器，用于用户名、文件夹名称。例如，\\\"user*\\\" 将匹配以 \\\"user\\\" 开头的名称。对于提供者事件，此过滤器应用于执行事件的管理员的用户名。\",\n        \"inverse_match\": \"反向匹配\",\n        \"group_name_filters\": \"组名过滤器\",\n        \"group_name_filters_help\": \"用于组名的类 shell 模式过滤器。例如，\\\"group*\\\" 将匹配以 \\\"group\\\" 开头的组名。\",\n        \"role_name_filters\": \"角色名过滤器\",\n        \"role_name_filters_help\": \"用于角色名的类 shell 模式过滤器。例如，\\\"role*\\\" 将匹配以 \\\"role\\\" 开头的角色名。\",\n        \"path_filters\": \"路径过滤器\",\n        \"path_filters_help\": \"类似于 Shell 的模式过滤器，应用于文件系统事件路径。例如，\\\"/adir/*.txt\\\" 将匹配 \\\"/adir\\\" 目录中以 \\\".txt\\\" 结尾的路径。支持双星号（`**`），例如 \\\"/**/*.txt\\\" 将匹配任何以 \\\".txt\\\" 结尾的文件。\\\"/mydir/**\\\" 将匹配 \\\"/mydir\\\" 中的任何条目。\",\n        \"file_size_limits\": \"文件大小限制\",\n        \"file_size_limits_help\": \"0 表示没有限制。您可以使用 MB/GB 后缀\",\n        \"min_size\": \"最小\",\n        \"max_size\": \"最大\",\n        \"actions_help\": \"一个或多个要执行的操作。对于“上传”事件，“执行同步”选项是支持的，并且对于“pre-*”事件以及如果操作检查账户的身份提供者登录事件是必需的。\",\n        \"option_failure_action\": \"故障操作\",\n        \"option_stop_on_failure\": \"失败时停止\",\n        \"option_execute_sync\": \"同步执行\",\n        \"no_filter\": \"如果没有设置过滤器，那么所有的事件都会被触发\",\n        \"action_placeholder\": \"选择一个操作\",\n        \"triggers\": {\n            \"fs_event\": \"文件系统事件\",\n            \"provider_event\": \"提供者事件\",\n            \"ip_blocked\": \"IP 已屏蔽\",\n            \"certificate_renewal\": \"证书续期\",\n            \"on_demand\": \"按需发送\",\n            \"idp_login\": \"身份提供商登录\",\n            \"schedule\": \"定时任务\"\n        },\n        \"idp_logins\": {\n            \"user\": \"用户登录\",\n            \"admin\": \"管理员登录\"\n        }\n    }\n}\n"
  },
  {
    "path": "templates/common/base.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- define \"errmsg\"}}\n<div id=\"errorMsg\" class=\"{{if not . }}d-none {{end}}rounded border-warning border border-dashed bg-light-warning d-flex align-items-center p-5 mb-10\">\n    <i class=\"ki-duotone ki-information-5 fs-3x text-warning me-5\">\n        <span class=\"path1\"></span>\n        <span class=\"path2\"></span>\n        <span class=\"path3\"></span>\n    </i>\n    <div class=\"text-gray-800 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10\">\n\t\t<span {{if .}}data-i18n=\"{{.Message}}\" {{if .HasArgs}}data-i18n-options=\"{{.Args}}\"{{end}}{{end}} id=\"errorTxt\"></span>\n\t</div>\n    <button id=\"id_dismiss_error_msg\" type=\"button\" class=\"position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 btn btn-icon btn-sm btn-active-light-primary ms-sm-auto\">\n        <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n    </button>\n</div>\n{{- end}}\n\n{{- define \"theme-setup\"}}\n<script type=\"text/javascript\" {{- if .}} nonce=\"{{.}}\"{{- end}}>\n    var defaultThemeMode = \"system\";\n    var themeMode;\n\tif ( document.documentElement ) {\n\t\tif ( document.documentElement.hasAttribute(\"data-bs-theme-mode\")) {\n\t\t\tthemeMode = document.documentElement.getAttribute(\"data-bs-theme-mode\");\n\t\t} else {\n\t\t\tif ( localStorage.getItem(\"data-bs-theme\") !== null ) {\n\t\t\t\tthemeMode = localStorage.getItem(\"data-bs-theme\");\n\t\t\t} else {\n\t\t\t\tthemeMode = defaultThemeMode;\n\t\t\t}\n\t\t}\n\t\tif (themeMode === \"system\") {\n\t\t\tthemeMode = window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n\t\t}\n\t\tdocument.documentElement.setAttribute(\"data-bs-theme\", themeMode);\n\t}\n</script>\n{{- end }}\n\n{{- define \"commonjs\"}}\n<script type=\"text/javascript\" {{- if .}} nonce=\"{{.}}\"{{- end}}>\n    window.addEventListener(\"pageshow\", function (event) {\n        if (event.persisted) {\n\t\t\tlet loadings = document.querySelectorAll('[data-kt-indicator=on]');\n            if (loadings){\n\t\t\t\t[].forEach.call(loadings, function (loading) {\n\t\t\t\t\tloading.removeAttribute('data-kt-indicator');\n\t\t\t\t\tloading.disabled = false;\n\t\t\t\t});\n            }\n\t\t}\n\t});\n</script>\n{{- end}}\n\n{{- define \"basejs\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    // https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode\n    function escapeHTML(str) {\n        var div = document.createElement('div');\n        div.appendChild(document.createTextNode(str));\n        return div.innerHTML;\n    }\n\n    function fileSizeIEC(bytes){\n        const thresh = 1024;\n        if (bytes < thresh){\n            return bytes + ' Bytes';\n        }\n\n        const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];\n        let i = Math.floor(Math.log(bytes) / Math.log(thresh));\n        let size = bytes / Math.pow(thresh, i);\n\n        return size.toFixed(1) + ' ' + units[i - 1];\n    }\n\n    function humanizeSpeed(a,b,c,d,e){\n        return (b=Math,c=b.log,d=1024,e=c(a)/c(d)|0,a/b.pow(d,e)).toFixed(1)\n            +' '+(e?'KMGTPEZY'[--e]+'B/s':'Bytes/s')\n    }\n\n    function initRepeaterItems() {\n        let repeaterDeleteButtons = document.querySelectorAll('[data-repeater-delete]');\n        let repeaterCreateButtons = document.querySelectorAll('[data-repeater-create]');\n\n        repeaterDeleteButtons.forEach(d => {\n            d.addEventListener(\"click\", function(e){\n                e.preventDefault();\n            });\n        });\n\n        repeaterCreateButtons.forEach(d => {\n            d.addEventListener(\"click\", function(e){\n                e.preventDefault();\n            });\n        });\n    }\n\n    function initRepeater(selector) {\n        $(selector).repeater({\n            initEmpty: false,\n\n            show: function () {\n                $(this).find('.select-repetear').each(function(){\n                    initializeSelect2El(this);\n                    let el = $(this);\n                    if (el.hasClass('select-first')){\n                        let firstVal = el.find(\"option:first-child\").val();\n                        el.val(firstVal).trigger('change');\n                    }\n                });\n                $(this).localize();\n                $(this).slideDown();\n                $(this).find('[data-repeater-delete]').on(\"click\", function(e){\n                    e.preventDefault();\n                });\n            },\n\n            hide: function (deleteElement) {\n                $(this).slideUp(deleteElement);\n            }\n        });\n\n        $(selector).find('.select-repetear').each(function(){\n            initializeSelect2El(this);\n        });\n    }\n\n    function clearChilds(el) {\n        while (el.firstChild) {\n            el.removeChild(el.firstChild);\n        }\n    }\n\n    function initializeSelect2El(element) {\n        if (element.getAttribute(\"data-kt-initialized\") === \"1\") {\n            return;\n        }\n        let options = {\n            dir: document.body.getAttribute('direction'),\n            language: {\n                noResults: function () {\n                    return $.t('select2.no_results');\n                },\n                searching: function () {\n                    return $.t('select2.searching');\n                },\n                removeAllItems: function () {\n                    return $.t('select2.removeall');\n                },\n                removeItem: function () {\n                    return $.t('select2.remove');\n                },\n                search: function() {\n                    return $.t('general.search');\n                }\n            }\n        };\n\n        if (element.getAttribute('data-hide-search') == 'true') {\n            options.minimumResultsForSearch = Infinity;\n        }\n\n        $(element).select2(options);\n        element.setAttribute(\"data-kt-initialized\", \"1\");\n    }\n\n    const lngs = {};\n    //{{- range .Languages}}\n    //{{- if eq . \"en\"}}\n    lngs.en = { nativeName: 'English' };\n    //{{- else if eq . \"it\" }}\n    lngs.it = { nativeName: 'Italiano' };\n    //{{- else if eq . \"de\" }}\n    lngs.de = { nativeName: 'Deutsch' };\n    //{{- else if eq . \"fr\" }}\n    lngs.fr = { nativeName: 'Français' };\n    //{{- else if eq . \"es\" }}\n    lngs.es = { nativeName: 'Español' };\n    //{{- else if eq . \"zh-CN\" }}\n    lngs[\"zh-CN\"] = { nativeName: '简体中文' };\n    //{{- end}}\n    //{{- end}}\n    if (Object.keys(lngs).length == 0){\n        lngs.en = { nativeName: 'English' };\n    }\n\n    const renderI18n = () => {\n        document.documentElement.setAttribute('lang', i18next.resolvedLanguage);\n        $('title').text('{{.Branding.Name}} - '+$.t('{{.Title}}'));\n        $('body').localize();\n        let select2elements = [].slice.call(document.querySelectorAll('[data-control=\"i18n-select2\"]'));\n        select2elements.map(function (element){\n            initializeSelect2El(element);\n        });\n    }\n\n    function initLocalizer() {\n        i18next\n            .use(i18nextChainedBackend)\n            .use(i18nextBrowserLanguageDetector)\n            .init({\n                debug: false,\n                supportedLngs: Object.keys(lngs),\n                fallbackLng: Object.keys(lngs)[0],\n                backend: {\n                    backends: [\n                        i18nextLocalStorageBackend,\n                        i18nextHttpBackend\n                    ],\n                    backendOptions: [{\n                        expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days\n                        defaultVersion: '{{.Version}}'\n                    }, {\n                        loadPath: '{{.StaticURL}}/locales/{{\"{{lng}}\"}}/{{\"{{ns}}\"}}.json?_='+new Date().getTime().toString()\n                    }\n                    ]\n                }\n            }, (err, t) => {\n                if (err) {\n                    console.error(err);\n                } else {\n                    jqueryI18next.init(i18next, $, { useOptionsAttr: true });\n\n                    var languageSwitcher = $('#languageSwitcher');\n                    if (languageSwitcher.length){\n                        if (Object.keys(lngs).length > 1) {\n                            languageSwitcher.removeClass(\"d-none\");\n                            Object.keys(lngs).map((lng) => {\n                                const opt = new Option(lngs[lng].nativeName, lng);\n                                if (lng === i18next.resolvedLanguage) {\n                                    opt.setAttribute(\"selected\", \"selected\");\n                                }\n                                languageSwitcher.append(opt);\n                            });\n                            languageSwitcher.on('change', function () {\n                                const chosenLng = $(this).find(\"option:selected\").attr('value');\n                                i18next.changeLanguage(chosenLng, () => {\n                                    renderI18n();\n                                });\n                            });\n                        } else {\n                            languageSwitcher.addClass(\"d-none\");\n                        }\n                    }\n\n                    renderI18n();\n                    $.event.trigger({\n                        type: \"i18nload\"\n                    });\n                }\n                $('#app_loader').addClass(\"d-none\");\n                $('#app_root').removeClass(\"d-none\");\n                $.event.trigger({\n                    type: \"i18nshow\"\n                });\n            });\n    }\n\n    function setI18NData(el, value, options) {\n        el.removeAttr(\"data-i18n-options\");\n        el.attr(\"data-i18n\", value);\n        el.localize(options);\n    }\n\n    function flatpickrLocale() {\n        const currentLang = i18next.resolvedLanguage;\n        if (currentLang === 'zh-CN') {\n            return 'zh';\n        }\n        return currentLang;\n    }\n\n    function humanizeDurationLocale() {\n        const currentLang = i18next.resolvedLanguage;\n        if (currentLang === 'zh-CN') {\n            return 'zh_CN';\n        }\n        return currentLang;\n    }\n\n    function handlePasswordInputVisibility(el) {\n\t\tlet pwdVisibility = $(el.querySelector('[data-password-control=\"visibility\"]'));\n\t\tlet passwordInput = el.querySelector('[data-password-control=\"input\"]');\n        pwdVisibility.off(\"click\");\n\t\tpwdVisibility.on('click', function(){\n\t\t\tlet visibleIcon = this.querySelector(':scope > i:not(.d-none)');\n            let hiddenIcon = this.querySelector(':scope > i.d-none');\n            visibleIcon.classList.add('d-none');\n            hiddenIcon.classList.remove('d-none');\n            if (passwordInput){\n\t\t\t\tif (passwordInput.getAttribute('type').toLowerCase() === 'password' ) {\n            \t\tpasswordInput.setAttribute('type', 'text');\n        \t\t} else {\n            \t\tpasswordInput.setAttribute('type', 'password');\n        \t\t}\n\t\t\t\tpasswordInput.focus();\n\t\t\t}\n\t\t});\n\t}\n\n    function getCurrentURI(){\n        let port = window.location.port;\n        if (port){\n            return window.location.protocol+\"//\"+window.location.hostname+\":\"+port;\n        }\n        return window.location.protocol+\"//\"+window.location.hostname;\n    }\n\n    function onFilesystemChanged(val){\n        const supportedFs = [\"local\", \"s3\", \"gcs\", \"azblob\", \"crypt\", \"sftp\", \"http\"];\n        let fsName = \"local\";\n        switch (val){\n            case '1':\n                fsName = \"s3\";\n                break;\n            case '2':\n                fsName = \"gcs\";\n                break;\n            case '3':\n                fsName = \"azblob\";\n                break;\n            case '4':\n                fsName = \"crypt\";\n                break;\n            case '5':\n                fsName = \"sftp\";\n                break;\n            case '6':\n                fsName = \"http\";\n                break;\n        }\n        for (let i = 0; i < supportedFs.length; i++){\n            if (supportedFs[i] == fsName){\n                $('.form-group.fsconfig-'+fsName).show();\n            } else {\n                $('.form-group.fsconfig-'+supportedFs[i]).hide();\n            }\n        }\n    }\n\n    function clearLoading() {\n        $('#loading_info').text(\"\");\n        $('#loading_message').text(\"\");\n    }\n\n    function setLoadingText(info, message) {\n        $('#loading_info').text(info);\n        $('#loading_message').text(message);\n    }\n\n    KTUtil.onDOMContentLoaded(function () {\n        var dismissErrorBtn = $('#id_dismiss_error_msg');\n        if (dismissErrorBtn.length){\n            dismissErrorBtn.on(\"click\", function(){\n                $('#errorMsg').addClass(\"d-none\");\n            });\n        }\n\n        var logoutBtn = $('#id_logout_link');\n        if (logoutBtn.length){\n            logoutBtn.on(\"click\", function(){\n                doLogout();\n            });\n        }\n\n        document.querySelectorAll('[data-password-control=\"container\"]').forEach(el => {\n\t\t\thandlePasswordInputVisibility(el);\n\t\t});\n\n        initLocalizer();\n        // Workaround to fix scrollbar in navigation sidebar.\n        window.dispatchEvent(new Event('resize'));\n\t});\n</script>\n{{- end}}\n\n{{- define \"globalstyle\"}}\n<style {{- if .}} nonce=\"{{.}}\"{{- end}}>\n    .text-sidebar {\n\t\tcolor: var(--bs-white);\n\t}\n\n\t[data-bs-theme=\"dark\"] .text-sidebar {\n\t\tcolor: var(--bs-white);\n    }\n\n    .wrap-word {\n        overflow-wrap: break-word;\n    }\n\n    .line-through {\n        text-decoration: line-through;\n    }\n\n    .section-title {\n        color: var(--bs-text-gray-800);\n        font-weight: 600 !important;\n        font-size: 1.45rem !important;\n    }\n\n    .section-title-inner {\n        color: var(--bs-text-gray-800);\n        font-weight: 600 !important;\n        font-size: 1.3rem !important;\n    }\n\n    .readonly-input {\n        color: var(--bs-text-gray-800);\n        font-weight: 500 !important;\n        font-size: 1.1rem !important;\n    }\n\n    .shortcut {\n        font-family: monospace; color: #666;\n    }\n\n    .overflow-auto {\n        overflow: auto;\n    }\n    .visibility-auto {\n        content-visibility: auto;\n    }\n</style>\n{{- end}}\n\n{{- define \"fonts\"}}\n<style {{- if .}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    /* cyrillic-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 300;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2) format('woff2');\n        unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n    }\n\n    /* cyrillic */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 300;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2) format('woff2');\n        unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n    }\n\n    /* greek-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 300;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2) format('woff2');\n        unicode-range: U+1F00-1FFF;\n    }\n\n    /* greek */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 300;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2) format('woff2');\n        unicode-range: U+0370-03FF;\n    }\n\n    /* vietnamese */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 300;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2) format('woff2');\n        unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n    }\n\n    /* latin-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 300;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2) format('woff2');\n        unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n    }\n\n    /* latin */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 300;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2) format('woff2');\n        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n    }\n\n    /* cyrillic-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 400;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2) format('woff2');\n        unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n    }\n\n    /* cyrillic */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 400;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2) format('woff2');\n        unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n    }\n\n    /* greek-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 400;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2) format('woff2');\n        unicode-range: U+1F00-1FFF;\n    }\n\n    /* greek */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 400;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2) format('woff2');\n        unicode-range: U+0370-03FF;\n    }\n\n    /* vietnamese */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 400;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2) format('woff2');\n        unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n    }\n\n    /* latin-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 400;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2) format('woff2');\n        unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n    }\n\n    /* latin */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 400;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2) format('woff2');\n        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n    }\n\n    /* cyrillic-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 500;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2) format('woff2');\n        unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n    }\n\n    /* cyrillic */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 500;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2) format('woff2');\n        unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n    }\n\n    /* greek-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 500;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2) format('woff2');\n        unicode-range: U+1F00-1FFF;\n    }\n\n    /* greek */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 500;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2) format('woff2');\n        unicode-range: U+0370-03FF;\n    }\n\n    /* vietnamese */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 500;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2) format('woff2');\n        unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n    }\n\n    /* latin-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 500;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2) format('woff2');\n        unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n    }\n\n    /* latin */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 500;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2) format('woff2');\n        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n    }\n\n    /* cyrillic-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 600;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2) format('woff2');\n        unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n    }\n\n    /* cyrillic */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 600;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2) format('woff2');\n        unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n    }\n\n    /* greek-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 600;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2) format('woff2');\n        unicode-range: U+1F00-1FFF;\n    }\n\n    /* greek */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 600;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2) format('woff2');\n        unicode-range: U+0370-03FF;\n    }\n\n    /* vietnamese */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 600;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2) format('woff2');\n        unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n    }\n\n    /* latin-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 600;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2) format('woff2');\n        unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n    }\n\n    /* latin */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 600;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2) format('woff2');\n        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n    }\n\n    /* cyrillic-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 700;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2) format('woff2');\n        unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n    }\n\n    /* cyrillic */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 700;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2) format('woff2');\n        unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n    }\n\n    /* greek-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 700;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2) format('woff2');\n        unicode-range: U+1F00-1FFF;\n    }\n\n    /* greek */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 700;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2) format('woff2');\n        unicode-range: U+0370-03FF;\n    }\n\n    /* vietnamese */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 700;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2) format('woff2');\n        unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n    }\n\n    /* latin-ext */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 700;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2) format('woff2');\n        unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n    }\n\n    /* latin */\n    @font-face {\n        font-family: 'Inter';\n        font-style: normal;\n        font-weight: 700;\n        src: url({{.StaticURL}}/vendor/fonts/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2) format('woff2');\n        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n    }\n</style>\n{{- end}}\n\n{{- define \"theme-switcher\"}}\n<div class=\"d-flex align-items-center ms-2 ms-lg-3\">\n    <a href=\"#\" class=\"btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px\" data-kt-menu-trigger=\"{default:'click', lg: 'hover'}\" data-kt-menu-attach=\"parent\" data-kt-menu-placement=\"bottom-end\">\n        <i class=\"ki-duotone ki-night-day theme-light-show fs-2\">\n            <span class=\"path1\"></span>\n            <span class=\"path2\"></span>\n            <span class=\"path3\"></span>\n            <span class=\"path4\"></span>\n            <span class=\"path5\"></span>\n            <span class=\"path6\"></span>\n            <span class=\"path7\"></span>\n            <span class=\"path8\"></span>\n            <span class=\"path9\"></span>\n            <span class=\"path10\"></span>\n        </i>\n        <i class=\"ki-duotone ki-moon theme-dark-show fs-2\">\n            <span class=\"path1\"></span>\n            <span class=\"path2\"></span>\n        </i>\n    </a>\n    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-gray-500 menu-active-bg menu-state-color fw-semibold py-4 fs-base w-150px\" data-kt-menu=\"true\" data-kt-element=\"theme-mode-menu\">\n        <div class=\"menu-item px-3 my-0\">\n            <a href=\"#\" class=\"menu-link px-3 py-2\" data-kt-element=\"mode\" data-kt-value=\"light\">\n                <span class=\"menu-icon\" data-kt-element=\"icon\">\n                    <i class=\"ki-duotone ki-night-day fs-2\">\n                        <span class=\"path1\"></span>\n                        <span class=\"path2\"></span>\n                        <span class=\"path3\"></span>\n                        <span class=\"path4\"></span>\n                        <span class=\"path5\"></span>\n                        <span class=\"path6\"></span>\n                        <span class=\"path7\"></span>\n                        <span class=\"path8\"></span>\n                        <span class=\"path9\"></span>\n                        <span class=\"path10\"></span>\n                    </i>\n                </span>\n                <span data-i18n=\"theme.light\" class=\"menu-title\">Light</span>\n            </a>\n        </div>\n        <div class=\"menu-item px-3 my-0\">\n            <a href=\"#\" class=\"menu-link px-3 py-2\" data-kt-element=\"mode\" data-kt-value=\"dark\">\n                <span class=\"menu-icon\" data-kt-element=\"icon\">\n                    <i class=\"ki-duotone ki-moon fs-2\">\n                        <span class=\"path1\"></span>\n                        <span class=\"path2\"></span>\n                    </i>\n                </span>\n                <span data-i18n=\"theme.dark\" class=\"menu-title\">Dark</span>\n            </a>\n        </div>\n        <div class=\"menu-item px-3 my-0\">\n            <a href=\"#\" class=\"menu-link px-3 py-2\" data-kt-element=\"mode\" data-kt-value=\"system\">\n                <span class=\"menu-icon\" data-kt-element=\"icon\">\n                    <i class=\"ki-duotone ki-screen fs-2\">\n                        <span class=\"path1\"></span>\n                        <span class=\"path2\"></span>\n                        <span class=\"path3\"></span>\n                        <span class=\"path4\"></span>\n                    </i>\n                </span>\n                <span data-i18n=\"theme.system\" class=\"menu-title\">System</span>\n            </a>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"infomsg\"}}\n<div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-4 mb-10\">\n    <div class=\"d-flex flex-stack flex-grow-1\">\n        <div class=\"fs-5 fw-semibold text-break text-wrap text-gray-800\">\n            <span data-i18n=\"{{.}}\"></span>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"infomsg-no-mb\"}}\n<div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-4\">\n    <div class=\"d-flex flex-stack flex-grow-1\">\n        <div class=\"fs-5 fw-semibold text-break text-wrap text-gray-800\">\n            <span data-i18n=\"{{.}}\"></span>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"base\"}}\n<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <title></title>\n        <meta charset=\"utf-8\" />\n        <meta name=\"description\" content=\"\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <meta name=\"robots\" content=\"noindex\">\n        <link rel=\"icon\" type=\"image/png\" href=\"{{.StaticURL}}{{.Branding.FaviconPath}}\" />\n\t\t{{- template \"fonts\" . }}\n        {{- block \"extra_css\" .}}{{- end}}\n        {{- range .Branding.DefaultCSS}}\n        <link href=\"{{$.StaticURL}}{{.}}\" rel=\"stylesheet\" type=\"text/css\">\n        {{- end}}\n        {{- template \"globalstyle\" .CSPNonce }}\n        {{- range .Branding.ExtraCSS}}\n        <link href=\"{{$.StaticURL}}{{.}}\" rel=\"stylesheet\" type=\"text/css\">\n        {{- end}}\n        {{- template \"commonjs\" .CSPNonce }}\n    </head>\n\n    <body data-kt-app-header-fixed=\"true\" data-kt-app-header-fixed-mobile=\"true\" data-kt-app-toolbar-enabled=\"true\" data-kt-app-sidebar-enabled=\"true\" data-kt-app-sidebar-fixed=\"true\" data-kt-app-sidebar-push-header=\"true\" data-kt-app-sidebar-push-toolbar=\"true\" data-kt-app-sidebar-push-footer=\"true\"  class=\"app-default\">\n        {{- template \"theme-setup\" .CSPNonce }}\n        <div id=\"app_loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n        </div>\n        <div class=\"d-flex flex-column flex-root app-root d-none\" id=\"app_root\">\n            <div class=\"app-page  flex-column flex-column-fluid \" id=\"kt_app_page\">\n                {{- if .LoggedUser.Username}}\n                <div id=\"kt_app_header\" class=\"app-header\" data-kt-sticky=\"true\" data-kt-sticky-activate=\"{default: true, lg: true}\" data-kt-sticky-name=\"app-header-minimize\" data-kt-sticky-offset=\"{default: '200px', lg: '300px'}\" data-kt-sticky-animation=\"false\">\n                    <div class=\"app-container  container-fluid d-flex align-items-stretch flex-stack \" id=\"kt_app_header_container\">\n                        <div class=\"d-flex align-items-center d-block d-lg-none ms-n3\" title=\"Show sidebar menu\">\n                            <div class=\"btn btn-icon btn-color-gray-800 btn-active-color-primary w-35px h-35px me-1\" id=\"kt_app_sidebar_mobile_toggle\">\n                                <i class=\"ki-duotone ki-abstract-14 fs-2\">\n                                    <span class=\"path1\"></span>\n                                    <span class=\"path2\"></span>\n                                </i>\n                            </div>\n                            <span>\n                                <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-30px\" />\n                            </span>\n                        </div>\n                        <div class=\"app-navbar flex-lg-grow-1\" id=\"kt_app_header_navbar\">\n                            <div class=\"app-navbar-item d-flex align-items-center flex-lg-grow-1 me-2 me-lg-0\">\n                            </div>\n                            <!-- <div class=\"d-flex align-self-center flex-center flex-shrink-0\">\n                            </div> -->\n                            <div class=\"d-flex align-self-center flex-center flex-shrink-0\">\n                                {{- template \"navitems\" .}}\n\t\t\t\t\t\t\t</div>\n                        </div>\n                    </div>\n                </div>\n                {{- else if .IsLoggedToShare}}\n                <div class=\"app-container container-fluid d-flex mt-5\">\n                    <div class=\"d-flex align-items-center d-block ms-3\">\n                        <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-50px\" />\n                    </div>\n                    <div class=\"flex-grow-1\">\n                        <div class=\"d-flex justify-content-end\">\n                            {{- template \"navitems\" .}}\n                        </div>\n                    </div>\n                </div>\n                {{- end}}\n                <div class=\"{{- if .LoggedUser.Username}}app-wrapper{{- end}} flex-column flex-row-fluid \" id=\"kt_app_wrapper\">\n                    {{- if .LoggedUser.Username}}\n                    <!-- <div id=\"kt_app_toolbar\" class=\"app-toolbar\">\n                        <div id=\"kt_app_toolbar_container\" class=\"app-container  container-fluid d-flex flex-lg-column py-3 py-lg-6 \">\n\n                        </div>\n                    </div> -->\n                    <div id=\"kt_app_sidebar\" class=\"app-sidebar flex-column\" data-kt-drawer=\"true\" data-kt-drawer-name=\"app-sidebar\" data-kt-drawer-activate=\"{default: true, lg: false}\" data-kt-drawer-overlay=\"true\" data-kt-drawer-width=\"300px\" data-kt-drawer-direction=\"start\" data-kt-drawer-toggle=\"#kt_app_sidebar_mobile_toggle\">\n                        <div class=\"app-sidebar-header flex-column mx-10 pt-8\" id=\"kt_app_sidebar_header\">\n                            <div class=\"d-flex flex-stack d-none d-lg-flex mb-13\">\n                                <div class=\"app-sidebar-logo\">\n\t\t\t\t\t\t\t\t\t<img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-40px app-sidebar-logo-default\" />\n\t\t\t\t\t\t\t\t\t<span class=\"text-sidebar fs-4 fw-semibold ps-5\">{{.Branding.ShortName}}</span>\n\t\t\t\t\t\t\t\t</div>\n                            </div>\n                        </div>\n                        <div class=\"app-sidebar-navs flex-column-fluid pb-6\" id=\"kt_app_sidebar_navs\">\n                            <div id=\"kt_app_sidebar_navs_wrappers\" class=\"hover-scroll-y my-2 mx-4\" data-kt-scroll=\"true\" data-kt-scroll-activate=\"true\" data-kt-scroll-height=\"auto\" data-kt-scroll-dependencies=\"#kt_app_sidebar_header\" data-kt-scroll-wrappers=\"#kt_app_sidebar_navs\" data-kt-scroll-offset=\"5px\">\n                                <div id=\"#kt_app_sidebar_menu\" data-kt-menu=\"true\" data-kt-menu-expand=\"false\" class=\"menu menu-column menu-rounded menu-sub-indention menu-active-bg mb-7\">\n                                    {{- template \"sidebaritems\" .}}\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    {{- end}}\n                    <div class=\"app-main flex-column flex-row-fluid \" id=\"kt_app_main\">\n                        <div class=\"d-flex flex-column flex-column-fluid\">\n                            <div id=\"kt_app_content\" class=\"app-content flex-column-fluid\">\n                                <div id=\"kt_app_content_container\" class=\"app-container container-fluid\">\n                                    {{- template \"page_body\" .}}\n                                </div>\n                            </div>\n                        </div>\n                        {{- if .LoggedUser.Username}}\n                        <div id=\"kt_app_footer\" class=\"app-footer\">\n                            <div class=\"app-container  container-fluid d-flex flex-column flex-md-row flex-center flex-md-stack py-3 align-items-center justify-content-center\">\n                                <div class=\"text-gray-900 order-2 order-md-1\">\n                                    <span class=\"text-gray-700 fw-semibold me-1\">{{.Version}}</span>\n                                </div>\n                            </div>\n                        </div>\n                        {{- end}}\n                    </div>\n                </div>\n            </div>\n        </div>\n        <div id=\"kt_scrolltop\" class=\"scrolltop\" data-kt-scrolltop=\"true\">\n            <i class=\"ki-duotone ki-arrow-up\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n        </div>\n        <div class=\"page-loader flex-column\">\n\t\t\t<span class=\"spinner-border text-primary\" role=\"status\"></span>\n\t\t\t<span id=\"loading_info\" class=\"text-muted fs-4 fw-semibold mt-5\"></span>\n            <span id=\"loading_message\" class=\"text-muted fs-4 fw-semibold mt-5\"></span>\n\t\t</div>\n\n        <div class=\"modal fade\" tabindex=\"-1\" id=\"modal_alert\">\n            <div class=\"modal-dialog modal-dialog-centered\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-body\">\n                        <div class=\"d-flex flex-center flex-column\">\n                            <span id=\"modal_alert_icon\">\n                            </span>\n                            <div class=\"mt-10 text-center\">\n                                <span id=\"modal_alert_text\" class=\"fs-6 text-gray-900 fw-semibold\"></span>\n                            </div>\n                        </div>\n                        <div id=\"modal_alert_items\" class=\"d-flex flex-column mt-5 d-none\">\n                        </div>\n                    </div>\n                    <div class=\"modal-footer border-0 justify-content-center\">\n                        <button id=\"modal_alert_cancel\" type=\"button\" class=\"btn btn-secondary m-2\"></button>\n                        <button id=\"modal_alert_ok\" type=\"button\" class=\"btn btn-primary m-2\"></button>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"toast-container position-fixed top-0 end-0 p-3\">\n            <div id=\"toast_container\" class=\"toast\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\" data-bs-autohide=\"false\">\n                <div class=\"toast-body border-0 d-flex align-items-center\">\n                    <span id=\"toast_icon\"></span>\n                    <span id=\"toast_msg\" class=\"fs-5 fw-semibold text-gray-800 me-auto\"></span>\n                    <button data-i18n=\"[aria-label]general.close\" type=\"button\" class=\"btn btn-icon btn-sm btn-active-light-primary position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 ms-sm-auto\" data-bs-dismiss=\"toast\" aria-label=\"Close\">\n                        <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                    </button>\n                </div>\n            </div>\n        </div>\n\n        {{- block \"modals\" .}}{{- end}}\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/global/plugins.bundle.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/js/scripts.bundle.js\"></script>\n        <script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18next.min.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/jquery-i18next.min.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18nextBrowserLanguageDetector.min.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18nextChainedBackend.min.js\"></script>\n        <script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18nextLocalStorageBackend.min.js\"></script>\n        <script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18nextHttpBackend.min.js\"></script>\n\t\t{{- template \"basejs\" . }}\n        <script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n            var ModalAlert = function () {\n                var modal;\n                var promiseResolve;\n                var isResolved;\n\n                function resolvePromise(result) {\n                    if (!isResolved) {\n                        isResolved = true;\n                        promiseResolve({\n                            isConfirmed: result\n                        });\n                    }\n                }\n\n                var cancelFn = function() {\n                    resolvePromise(false);\n                    modal.hide();\n                }\n\n                var okFn = function() {\n                    resolvePromise(true);\n                    modal.hide();\n                }\n\n                var hideFn = function() {\n                    resolvePromise(false);\n                }\n\n                return {\n                    fire: function (params) {\n                        modal = new bootstrap.Modal('#modal_alert');\n                        let modalEl = $('#modal_alert');\n                        let okBtn = $(\"#modal_alert_ok\");\n                        let cancelBtn = $(\"#modal_alert_cancel\");\n                        let itemsList = $('#modal_alert_items');\n\n                        modalEl.off('hide.bs.modal');\n                        modalEl.on('hide.bs.modal', hideFn);\n                        cancelBtn.off(\"click\");\n                        okBtn.off(\"click\");\n                        cancelBtn.on(\"click\", cancelFn);\n                        okBtn.on(\"click\", okFn);\n                        okBtn.removeClass();\n                        okBtn.addClass(params.customClass.confirmButton);\n                        okBtn.addClass(\"m-2\");\n                        okBtn.text(params.confirmButtonText);\n                        if (params.cancelButtonText){\n                            cancelBtn.text(params.cancelButtonText);\n                            cancelBtn.removeClass();\n                            cancelBtn.addClass(params.customClass.cancelButton);\n                            cancelBtn.addClass(\"m-2\");\n                        } else {\n                            cancelBtn.addClass(\"d-none\");\n                        }\n\n                        $(\"#modal_alert_text\").text(params.text);\n                        itemsList.empty();\n                        itemsList.addClass(\"d-none\");\n                        if (params.items && params.items.length > 0){\n                            itemsList.removeClass(\"d-none\");\n                            $.each(params.items, function(key, item) {\n                                itemText = escapeHTML(item);\n                                itemsList.append(`<li class=\"d-flex align-items-center py-2 fw-bold fs-6 text-gray-800\"><span class=\"bullet bullet-dot bg-primary me-2\"></span>${itemText}</li>`);\n                            });\n                        }\n\n                        switch (params.icon){\n                            case \"warning\":\n                                $(\"#modal_alert_icon\").html(`<i class=\"ki-duotone ki-information-5 fs-5x text-danger\">\n                                                                <span class=\"path1\"></span>\n                                                                <span class=\"path2\"></span>\n                                                                <span class=\"path3\"></span>\n                                                            </i>`);\n                                break;\n                            case \"success\":\n                                $(\"#modal_alert_icon\").html(`<i class=\"ki-duotone ki-check-square fs-5x text-success\">\n                                                                <span class=\"path1\"></span>\n                                                                <span class=\"path2\"></span>\n                                                            </i>`);\n                                break;\n                            default:\n                                $(\"#modal_alert_icon\").html(`<i class=\"ki-duotone ki-question-2 fs-5x text-primary\">\n                                                                <span class=\"path1\"></span>\n                                                                <span class=\"path2\"></span>\n                                                                <span class=\"path3\"></span>\n                                                            </i>`);\n                        }\n\n                        return new Promise(function(resolve, reject) {\n                            promiseResolve = resolve;\n                            isResolved = false;\n                            modal.show();\n                        });\n                    }\n                }\n            }();\n\n            function showToast(status, msg, options) {\n                const toast = bootstrap.Toast.getOrCreateInstance(document.getElementById('toast_container'));\n                if (status === 1){\n                    $(\"#toast_icon\").html(`<i class=\"ki-duotone ki-check-square fs-3x text-success me-5\">\n                                            <span class=\"path1\"></span>\n                                            <span class=\"path2\"></span>\n                                        </i>`);\n                } else {\n                    $(\"#toast_icon\").html(`<i class=\"ki-duotone ki-information-5 fs-3x text-warning me-5\">\n                                            <span class=\"path1\"></span>\n                                            <span class=\"path2\"></span>\n                                            <span class=\"path3\"></span>\n                                        </i>`);\n                }\n                setI18NData($('#toast_msg'), msg, options)\n                toast.show();\n            }\n\n            function doLogout() {\n                ModalAlert.fire({\n                    text: $.t(\"general.confirm_logout\"),\n                    icon: \"question\",\n                    confirmButtonText: $.t(\"login.signout\"),\n                    cancelButtonText: $.t(\"general.cancel\"),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\",\n                        cancelButton: 'btn btn-secondary'\n                    }\n                }).then((result) =>{\n                    if (result.isConfirmed){\n                        window.location.replace('{{.LogoutURL}}');\n                    }\n                });\n            }\n        </script>\n\t\t{{- block \"extra_js\" .}}{{- end}}\n    </body>\n</html>\n{{- end}}"
  },
  {
    "path": "templates/common/baselogin.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- define \"baselogin\"}}\n<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n\t\t<title></title>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"description\" content=\"\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<meta name=\"robots\" content=\"noindex\">\n\t\t<link rel=\"icon\" type=\"image/png\" href=\"{{.StaticURL}}{{.Branding.FaviconPath}}\" />\n\t\t{{- template \"fonts\" . }}\n\t\t{{- range .Branding.DefaultCSS}}\n    \t<link href=\"{{$.StaticURL}}{{.}}\" rel=\"stylesheet\" type=\"text/css\">\n    \t{{- end}}\n\t\t{{- template \"globalstyle\" .CSPNonce }}\n        {{- range .Branding.ExtraCSS}}\n    \t<link href=\"{{$.StaticURL}}{{.}}\" rel=\"stylesheet\" type=\"text/css\">\n    \t{{- end}}\n\t\t{{- template \"commonjs\" .CSPNonce}}\n\t</head>\n\n    <body class=\"app-blank\">\n        {{- template \"theme-setup\" .CSPNonce}}\n\t\t<div id=\"app_loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n        </div>\n        <div class=\"d-flex flex-column flex-root d-none\" id=\"app_root\">\n\t\t\t<div class=\"d-flex flex-column flex-column-fluid bgi-position-y-bottom position-x-center bgi-no-repeat bgi-size-contain bgi-attachment-fixed\">\n\t\t\t\t<div class=\"d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20\">\n\t\t\t\t\t<div class=\"w-lg-500px w-md-450px w-sm-400px bg-body rounded shadow-sm p-10 p-lg-15 mx-auto\">\n                        {{template \"content\" .}}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/global/plugins.bundle.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/js/scripts.bundle.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18next.min.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/jquery-i18next.min.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18nextBrowserLanguageDetector.min.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18nextChainedBackend.min.js\"></script>\n        <script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18nextLocalStorageBackend.min.js\"></script>\n\t\t<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/i18next/i18nextHttpBackend.min.js\"></script>\n\t\t{{- template \"basejs\" . }}\n\t\t<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\t\t\tKTUtil.onDOMContentLoaded(function () {\n\t\t\t\t$('#sign_in_form').submit(function (event) {\n\t\t\t\t\tlet next = null;\n\t\t\t\t\t//{{- if .CheckRedirect}}\n\t\t\t\t\tconst urlParams = new URLSearchParams(window.location.search);\n\t\t\t\t\tnext = urlParams.get('next');\n\t\t\t\t\t//{{- end}}\n\t\t\t\t\tif (!next){\n\t\t\t\t\t\tlet submitButton = document.querySelector('#sign_in_submit');\n\t\t\t\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\t\t\t\tsubmitButton.disabled = true;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t</script>\n    </body>\n</html>\n{{- end}}\n"
  },
  {
    "path": "templates/common/changepassword.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"title.change_password\" class=\"card-title section-title\">Change password</h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"infomsg\" \"change_pwd.info\"}}\n        {{- template \"errmsg\" .Error}}\n        <form id=\"change_pwd_form\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n            <div class=\"form-group row\">\n                <label data-i18n=\"change_pwd.current\" class=\"col-md-3 col-form-label required\">Current password</label>\n                <div class=\"col-md-9\">\n                    <div class=\"position-relative\" data-password-control=\"container\">\n                        <input type=\"password\" data-password-control=\"input\" class=\"form-control\" placeholder=\"\"\n                            name=\"current_password\" spellcheck=\"false\" required />\n                        <span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                            <i class=\"ki-duotone ki-eye-slash fs-1\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                                <span class=\"path3\"></span>\n                                <span class=\"path4\"></span>\n                            </i>\n                            <i class=\"ki-duotone ki-eye d-none fs-1\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                                <span class=\"path3\"></span>\n                            </i>\n                        </span>\n                    </div>\n                </div>\n            </div>\n            <div class=\"form-group row mt-10\">\n                <label data-i18n=\"change_pwd.new\" class=\"col-md-3 col-form-label required\">New password</label>\n                <div class=\"col-md-9\">\n                    <div class=\"position-relative\" data-password-control=\"container\">\n                        <input type=\"password\" data-password-control=\"input\" class=\"form-control\" placeholder=\"\"\n                            name=\"new_password1\" autocomplete=\"new-password\" spellcheck=\"false\" required />\n                        <span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                            <i class=\"ki-duotone ki-eye-slash fs-1\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                                <span class=\"path3\"></span>\n                                <span class=\"path4\"></span>\n                            </i>\n                            <i class=\"ki-duotone ki-eye d-none fs-1\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                                <span class=\"path3\"></span>\n                            </i>\n                        </span>\n                    </div>\n                </div>\n            </div>\n            <div class=\"form-group row mt-10\">\n                <label data-i18n=\"change_pwd.confirm\" class=\"col-md-3 col-form-label required\">Confirm password</label>\n                <div class=\"col-md-9\">\n                    <div class=\"position-relative\" data-password-control=\"container\">\n                        <input type=\"password\" data-password-control=\"input\" class=\"form-control\" placeholder=\"\"\n                            name=\"new_password2\" autocomplete=\"new-password\" spellcheck=\"false\" required />\n                        <span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                            <i class=\"ki-duotone ki-eye-slash fs-1\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                                <span class=\"path3\"></span>\n                                <span class=\"path4\"></span>\n                            </i>\n                            <i class=\"ki-duotone ki-eye d-none fs-1\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                                <span class=\"path3\"></span>\n                            </i>\n                        </span>\n                    </div>\n                </div>\n            </div>\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\">\n                    <span data-i18n=\"change_pwd.save\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n    </div>\n</div>\n{{end}}\n\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    KTUtil.onDOMContentLoaded(function () {\n        $('#change_pwd_form').submit(function (event) {\n            let submitButton = document.querySelector('#form_submit');\n            submitButton.setAttribute('data-kt-indicator', 'on');\n            submitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/common/forgot-password.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"baselogin\" .}}\n\n{{- define \"content\"}}\n<form class=\"form w-100\" id=\"sign_in_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n    <div class=\"container mb-10\">\n        <div class=\"row align-items-center\">\n            <div class=\"col-5 align-items-center\">\n                <a href=\"{{.LoginURL}}\">\n                    <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n                </a>\n            </div>\n            <div class=\"col-7\">\n                <a href=\"{{.LoginURL}}\" class=\"text-gray-900 mb-3 ms-3 fs-1 fw-bold\">\n                    {{.Branding.ShortName}}\n                </a>\n            </div>\n        </div>\n    </div>\n    <div class=\"text-center mb-10\">\n        <h2 data-i18n=\"login.forgot_password\" class=\"text-gray-900 mb-3\">\n            Forgot Password ?\n        </h2>\n        <div class=\"text-gray-700 fw-semibold fs-4\">\n            <span data-i18n=\"login.forgot_password_msg\">\n                Enter your account username below, you will receive a password reset code by email.\n            </span>\n        </div>\n    </div>\n    {{- template \"errmsg\" .Error}}\n    <div class=\"fv-row mb-10\">\n        <input data-i18n=\"[placeholder]login.your_username\" class=\"form-control form-control-lg form-control-solid\" type=\"text\" placeholder=\"Your username\" name=\"username\" spellcheck=\"false\" required />\n    </div>\n    <div class=\"text-center\">\n        <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n        <button type=\"submit\" id=\"sign_in_submit\" class=\"btn btn-lg btn-primary w-100 mb-5\">\n            <span data-i18n=\"login.send_reset_code\" class=\"indicator-label\">Send Reset Code</span>\n            <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                Please wait...\n                <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n            </span>\n        </button>\n    </div>\n</form>\n{{- end}}"
  },
  {
    "path": "templates/common/login.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"baselogin\" .}}\n\n{{- define \"content\"}}\n\t\t\t\t\t\t<form class=\"form w-100\" id=\"sign_in_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n\t\t\t\t\t\t\t<div class=\"container mb-10\">\n\t\t\t\t\t\t\t\t<div class=\"row align-items-center\">\n\t\t\t\t\t\t\t\t\t<div class=\"col-5 align-items-center\">\n\t\t\t\t\t\t\t\t\t\t<img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"col-7\">\n\t\t\t\t\t\t\t\t\t\t<h1 class=\"text-gray-900 mb-3 ms-3\">\n\t\t\t\t\t\t\t\t\t\t\t{{.Branding.ShortName}}\n\t\t\t\t\t\t\t\t\t\t</h1>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{{- template \"errmsg\" .Error}}\n\t\t\t\t\t\t\t{{- if not .FormDisabled}}\n\t\t\t\t\t\t\t<div class=\"fv-row mb-10\">\n\t\t\t\t\t\t\t\t<input data-i18n=\"[placeholder]login.username\" class=\"form-control form-control-lg form-control-solid\" type=\"text\" name=\"username\" placeholder=\"Username\" autocomplete=\"on\" spellcheck=\"false\" required />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"fv-row mb-10\">\n\t\t\t\t\t\t\t\t<div class=\"position-relative\" data-password-control=\"container\">\n\t\t\t\t\t\t\t\t\t<input data-i18n=\"[placeholder]login.password\" data-password-control=\"input\" class=\"form-control form-control-lg form-control-solid\" type=\"password\" name=\"password\" placeholder=\"Password\" autocomplete=\"current-password\" spellcheck=\"false\" required />\n\t\t\t\t\t\t\t\t\t<span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                    \t\t\t\t\t<i class=\"ki-duotone ki-eye-slash fs-1\">\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"path1\"></span>\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"path2\"></span>\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"path3\"></span>\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"path4\"></span>\n\t\t\t\t\t\t\t\t\t\t</i>\n                    \t\t\t\t\t<i class=\"ki-duotone ki-eye d-none fs-1\">\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"path1\"></span>\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"path2\"></span>\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"path3\"></span>\n\t\t\t\t\t\t\t\t\t\t</i>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"d-flex justify-content-end mt-2\">\n\t\t\t\t\t\t\t\t\t{{- if .ForgotPwdURL}}\n\t\t\t\t\t\t\t\t\t<a data-i18n=\"login.forgot_password\" href=\"{{.ForgotPwdURL}}\" class=\"link-primary fs-6 fw-bold\">Forgot Password ?</a>\n\t\t\t\t\t\t\t\t\t{{- end}}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{{- end}}\n\t\t\t\t\t\t\t<div class=\"text-center\">\n\t\t\t\t\t\t\t\t{{- if not .FormDisabled}}\n\t\t\t\t\t\t\t\t<input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n\t\t\t\t\t\t\t\t<button type=\"submit\" id=\"sign_in_submit\" class=\"btn btn-lg btn-primary w-100 mb-5\">\n\t\t\t\t\t\t\t\t\t<span data-i18n=\"login.signin\" class=\"indicator-label\">Sign in</span>\n\t\t\t\t\t\t\t\t\t<span data-i18n=\"general.wait\" class=\"indicator-progress\">\n\t\t\t\t\t\t\t\t\t\tPlease wait...\n\t\t\t\t\t\t\t\t\t\t<span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t{{- end}}\n\t\t\t\t\t\t\t\t{{- if .OpenIDLoginURL}}\n\t\t\t\t\t\t\t\t<a href=\"{{.OpenIDLoginURL}}\" class=\"btn btn-flex btn-outline flex-center {{if .FormDisabled}}btn-primary{{else}}btn-active-color-primary bg-state-light{{end}} btn-lg w-100 my-5\">\n\t\t\t\t\t\t\t\t\t<img alt=\"Logo\" src=\"{{.StaticURL}}/img/openid-logo.png\" class=\"h-20px me-3\" />\n\t\t\t\t\t\t\t\t\t<span data-i18n=\"login.signin_openid\">Sign in with OpenID</span>\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t{{- end}}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</form>\n\t\t\t\t\t\t{{- if or (.AltLoginURL) (gt (len .Languages) 1) (and .Branding.DisclaimerName .Branding.DisclaimerPath)}}\n\t\t\t\t\t\t<hr>\n\t\t\t\t\t\t{{- end}}\n\t\t\t\t\t\t<div class=\"d-flex flex-stack pt-5 mt-3\">\n\t\t\t\t\t\t\t<div class=\"me-10\">\n\t\t\t\t\t\t\t\t<select id=\"languageSwitcher\" name=\"language\" class=\"form-select form-select-solid form-select-sm\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"d-flex fw-semibold text-primary\">\n\t\t\t\t\t\t\t\t{{- if .AltLoginURL}}\n\t\t\t\t\t\t\t\t<a href=\"{{.AltLoginURL}}\" class=\"px-2\">\n\t\t\t\t\t\t\t\t\t<span data-i18n=\"login.link\" data-i18n-options='{ \"link\": \"{{.AltLoginName}}\" }'></span>\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t{{- end}}\n\t\t\t\t\t\t\t\t{{- if and .Branding.DisclaimerName .Branding.DisclaimerPath}}\n\t\t\t\t\t\t\t\t<a href=\"{{.Branding.DisclaimerPath}}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"px-2\">\n\t\t\t\t\t\t\t\t\t<span data-i18n=\"custom.disclaimer_webclient\">{{.Branding.DisclaimerName}}</span>\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t{{- end}}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n{{- end}}"
  },
  {
    "path": "templates/common/message.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20\">\n    {{- if not .LoggedUser.Username}}\n    {{- if .LoginURL}}\n    <div class=\"mb-12\">\n        <a href=\"{{.LoginURL}}\">\n            <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n        </a>\n        <a href=\"{{.LoginURL}}\" class=\"text-gray-900 fs-1 fw-bold ms-3 ps-5\">\n\t\t    {{.Branding.ShortName}}\n        </a>\n    </div>\n    {{- else}}\n    <div class=\"mb-12\">\n        <span>\n            <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n        </span>\n        <span class=\"text-gray-900 fs-1 fw-bold ms-3 ps-5\">\n\t\t    {{.Branding.ShortName}}\n        </span>\n    </div>\n    {{- end}}\n    {{- end}}\n    <div class=\"card shadow-sm w-lg-600px\">\n        <div class=\"card-header bg-light\">\n            <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n        </div>\n        <div class=\"card-body\">\n        {{- if .Error}}\n            <div class=\"notice d-flex align-items-center bg-light-warning rounded border-warning border border-dashed p-6\">\n                <i class=\"ki-duotone ki-information-5 fs-3x text-warning me-4\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                </i>\n                <div class=\"d-flex flex-stack flex-grow-1\">\n                    <div class=\"fs-4 fw-semibold text-gray-800 text-break text-wrap\">\n                        <span data-i18n=\"{{.Error.Message}}\" {{if .Error.HasArgs}}data-i18n-options=\"{{.Error.Args}}\"{{end}}></span>\n                    </div>\n                </div>\n            </div>\n        {{- end}}\n        {{- if .Success}}\n            <div class=\"notice d-flex align-items-center bg-light-primary rounded border-primary border border-dashed p-6\">\n                <i class=\"ki-duotone ki-information-2 fs-3x text-primary me-4\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                </i>\n                <div class=\"d-flex flex-stack flex-grow-1\">\n                    <div class=\"fs-4 fw-semibold text-gray-800 text-break text-wrap\">\n                        <span data-i18n=\"{{.Success}}\"></span>\n                        {{- if .Text}}\n                        <span> {{.Text}}</span>\n                        {{- end}}\n                    </div>\n                </div>\n            </div>\n        {{- end}}\n        </div>\n    </div>\n</div>\n{{- end}}"
  },
  {
    "path": "templates/common/reset-password.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"baselogin\" .}}\n\n{{- define \"content\"}}\n<form class=\"form w-100\" id=\"sign_in_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n    <div class=\"container mb-10\">\n        <div class=\"row align-items-center\">\n            <div class=\"col-5 align-items-center\">\n                <a href=\"{{.LoginURL}}\">\n                    <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n                </a>\n            </div>\n            <div class=\"col-7\">\n                <a href=\"{{.LoginURL}}\" class=\"text-gray-900 mb-3 ms-3 fs-1 fw-bold\">\n                    {{.Branding.ShortName}}\n                </a>\n            </div>\n        </div>\n    </div>\n    <div class=\"text-center mb-10\">\n        <h2 data-i18n=\"login.reset_password\" class=\"text-gray-900 mb-3\">\n            Reset Password\n        </h2>\n        <div class=\"text-gray-700 fw-semibold fs-4\">\n            <span data-i18n=\"login.reset_pwd_msg\">\n                Check your email for the confirmation code\n            </span>\n        </div>\n    </div>\n    {{- template \"errmsg\" .Error}}\n    <div class=\"fv-row mb-10\">\n        <input data-i18n=\"[placeholder]login.confirm_code\" class=\"form-control form-control-lg form-control-solid\" type=\"text\" placeholder=\"Confirmation code\" name=\"code\" spellcheck=\"false\" required />\n    </div>\n    <div class=\"fv-row mb-10\">\n        <div class=\"position-relative\" data-password-control=\"container\">\n            <input data-i18n=\"[placeholder]general.new_password\" data-password-control=\"input\" class=\"form-control form-control-lg form-control-solid\"\n                type=\"password\" name=\"password\" placeholder=\"New Password\" autocomplete=\"new-password\" spellcheck=\"false\" required />\n            <span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                <i class=\"ki-duotone ki-eye-slash fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                    <span class=\"path4\"></span>\n                </i>\n                <i class=\"ki-duotone ki-eye d-none fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                </i>\n            </span>\n        </div>\n    </div>\n    <div class=\"fv-row mb-10\">\n        <div class=\"position-relative\" data-password-control=\"container\">\n            <input data-i18n=\"[placeholder]change_pwd.confirm\" data-password-control=\"input\" class=\"form-control form-control-lg form-control-solid\"\n                type=\"password\" name=\"confirm_password\" placeholder=\"Confirm Password\" autocomplete=\"new-password\" spellcheck=\"false\" required />\n            <span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                <i class=\"ki-duotone ki-eye-slash fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                    <span class=\"path4\"></span>\n                </i>\n                <i class=\"ki-duotone ki-eye d-none fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                </i>\n            </span>\n        </div>\n    </div>\n    <div class=\"text-center\">\n        <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n        <button type=\"submit\" id=\"sign_in_submit\" class=\"btn btn-lg btn-primary w-100 mb-5\">\n            <span data-i18n=\"login.reset_submit\" class=\"indicator-label\">Update Password & Login</span>\n            <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                Please wait...\n                <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n            </span>\n        </button>\n    </div>\n</form>\n{{- end}}"
  },
  {
    "path": "templates/common/twofactor-recovery.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"baselogin\" .}}\n\n{{- define \"content\"}}\n<form class=\"form w-100\" id=\"sign_in_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n    <div class=\"container mb-10\">\n        <div class=\"row align-items-center\">\n            <div class=\"col-5 align-items-center\">\n                <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n            </div>\n            <div class=\"col-7\">\n                <h1 class=\"text-gray-900 mb-3 ms-3\">\n                    {{.Branding.ShortName}}\n                </h1>\n            </div>\n        </div>\n    </div>\n    {{- template \"errmsg\" .Error}}\n    <div class=\"fv-row mb-10\">\n        <input data-i18n=\"[placeholder]login.recovery_code\" class=\"form-control form-control-lg form-control-solid\" type=\"text\" name=\"recovery_code\" placeholder=\"Recovery code\" spellcheck=\"false\" required />\n    </div>\n    <div class=\"text-center\">\n        <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n        <button type=\"submit\" id=\"sign_in_submit\" class=\"btn btn-lg btn-primary w-100 mb-5\">\n            <span data-i18n=\"general.verify\" class=\"indicator-label\">Verify</span>\n            <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                Please wait...\n                <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n            </span>\n        </button>\n    </div>\n</form>\n<div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5\">\n    <i class=\"ki-duotone ki-shield-tick fs-2tx text-primary me-4\">\n        <span class=\"path1\"></span>\n        <span class=\"path2\"></span>\n    </i>\n    <div class=\"d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap\">\n        <div class=\"mb-3 mb-md-0 fw-semibold\">\n            <div class=\"fs-6 text-gray-800\">\n                <span data-i18n=\"login.recovery_code_msg\">\n                    You can enter one of your recovery codes in case you lost access to your mobile device.\n                </span>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n"
  },
  {
    "path": "templates/common/twofactor.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"baselogin\" .}}\n\n{{- define \"content\"}}\n<form class=\"form w-100\" id=\"sign_in_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n    <div class=\"container mb-10\">\n        <div class=\"row align-items-center\">\n            <div class=\"col-5 align-items-center\">\n                <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n            </div>\n            <div class=\"col-7\">\n                <h1 class=\"text-gray-900 mb-3 ms-3\">\n                    {{.Branding.ShortName}}\n                </h1>\n            </div>\n        </div>\n    </div>\n    {{- template \"errmsg\" .Error}}\n    <div class=\"fv-row mb-10\">\n        <input data-i18n=\"[placeholder]login.auth_code\" class=\"form-control form-control-lg form-control-solid\" type=\"text\" placeholder=\"Authentication code\" name=\"passcode\" spellcheck=\"false\" required />\n    </div>\n    <div class=\"text-center\">\n        <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n        <button type=\"submit\" id=\"sign_in_submit\" class=\"btn btn-lg btn-primary w-100 mb-5\">\n            <span data-i18n=\"general.verify\" class=\"indicator-label\">Verify</span>\n            <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                Please wait...\n                <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n            </span>\n        </button>\n    </div>\n</form>\n\n<div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5\">\n    <i class=\"ki-duotone ki-shield-tick fs-2tx text-primary me-4\">\n        <span class=\"path1\"></span>\n        <span class=\"path2\"></span>\n    </i>\n    <div class=\"d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap\">\n        <div class=\"mb-3 mb-md-0 fw-semibold\">\n            <div class=\"fs-6 text-gray-700\">\n                <span data-i18n=\"login.two_factor_help\">\n                    Open the two-factor authentication app on your device to view your authentication code and verify your identity.\n                </span>\n            </div>\n            <div class=\"fs-6 text-gray-800 mt-5\">\n                <p data-i18n=\"general.problems\" class=\"fw-bold\">Having problems?</p>\n                <p><a data-i18n=\"login.two_factor_msg\" href=\"{{.RecoveryURL}}\">Enter a two-factor recovery code</a></p>\n            </div>\n        </div>\n    </div>\n</div>\n\n{{- end}}"
  },
  {
    "path": "templates/email/password-expiration.html",
    "content": "Hi {{.Username}},\n<br>\n<p>your SFTPGo password {{if le .Days 0}}has expired{{else}}expires in {{.Days}} {{if eq .Days 1}}day{{else}}days{{end}}{{end}}.</p>\n<p>Please login to the WebClient and set a new password.</p>"
  },
  {
    "path": "templates/email/reset-password.html",
    "content": "Hello there!\n<br>\n<p>Your SFTPGo email verification code is \"{{.Code}}\", this code is valid for 10 minutes.</p>\n<p>Please enter this code in SFTPGo to confirm your email address.</p>\n"
  },
  {
    "path": "templates/webadmin/admin.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"admin_form\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n            <div class=\"form-group row\">\n                <label for=\"idUsername\" data-i18n=\"login.username\" class=\"col-md-3 col-form-label\">Username</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idUsername\" type=\"text\" placeholder=\"\" name=\"username\" value=\"{{.Admin.Username}}\" maxlength=\"255\" autocomplete=\"off\"\n                        spellcheck=\"false\" required {{if .IsAdd}}class=\"form-control\"{{else}}class=\"form-control-plaintext readonly-input\" readonly{{end}} />\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idPassword\" data-i18n=\"login.password\" class=\"col-md-3 col-form-label\">Password</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idPassword\" type=\"password\" class=\"form-control\" name=\"password\" autocomplete=\"new-password\"\n                        spellcheck=\"false\" value=\"\" {{if not .IsAdd}}aria-describedby=\"idPasswordHelp\"{{end}} />\n                    {{- if not .IsAdd}}\n                    <div id=\"idPasswordHelp\" class=\"form-text\" data-i18n=\"admin.password_help\"></div>\n                    {{- end}}\n                </div>\n            </div>\n\n            <div class=\"form-group row align-items-center mt-10 {{if eq .LoggedUser.Username .Admin.Username}}d-none{{end}}\">\n                <label data-i18n=\"user.require_pwd_change\" class=\"col-md-3 col-form-label\" for=\"idRequirePasswordChange\">Require password change</label>\n                <div class=\"col-md-9\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idRequirePasswordChange\" name=\"require_password_change\" {{if .Admin.Filters.RequirePasswordChange}}checked=\"checked\"{{end}}/>\n                        <label data-i18n=\"admin.require_pwd_change_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idRequirePasswordChange\">\n                            A password change is required at the next login\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idStatus\" data-i18n=\"general.status\" class=\"col-md-3 col-form-label\">Status</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idStatus\" name=\"status\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        <option data-i18n=\"general.active\" value=\"1\" {{- if eq .Admin.Status 1 }} selected{{- end}}>Active</option>\n                        <option data-i18n=\"general.inactive\" value=\"0\" {{- if eq .Admin.Status 0 }} selected{{- end}}>Inactive</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idPermissions\" data-i18n=\"general.permissions\" class=\"col-md-3 col-form-label\">Permissions</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idPermissions\" name=\"permissions\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                        {{- range $validPerm := .Admin.GetValidPerms}}\n                        <option value=\"{{$validPerm}}\" {{- range $perm :=$.Admin.Permissions }}{{- if eq $perm $validPerm}} selected{{- end}}{{- end}}>{{$validPerm}}</option>\n                        {{- end}}\n                    </select>\n                </div>\n            </div>\n\n            {{- if .Roles}}\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"general.role\" class=\"card-title section-title-inner\">Role</h3>\n                </div>\n                <div class=\"card-body\">\n                    {{- template \"infomsg\" \"admin.role_help\"}}\n                    <div class=\"form-group row\">\n                        <label for=\"idRole\" data-i18n=\"general.role\" class=\"col-md-3 col-form-label\">Role</label>\n                        <div class=\"col-md-9\">\n                            <select id=\"idRole\" name=\"role\" data-i18n=\"[data-placeholder]general.role_placeholder\" class=\"form-select\" data-control=\"i18n-select2\" data-placeholder=\"Select a role\" data-allow-clear=\"true\">\n                                <option value=\"\"></option>\n                                {{- range .Roles}}\n                                <option value=\"{{.Name}}\" {{if eq $.Admin.Role .Name}}selected{{end}}>{{.Name}}</option>\n                                {{- end}}\n                            </select>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            {{- if .Groups}}\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"admin.users_groups\" class=\"card-title section-title-inner\">Groups for users</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"groups\">\n                        {{- template \"infomsg-no-mb\" \"admin.users_groups_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"groups\">\n                                {{range $idx, $val := .Admin.Groups}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                <select name=\"group\" data-i18n=\"[data-placeholder]general.group_placeholder\" class=\"form-select select-repetear\" data-allow-clear=\"true\">\n                                                    <option value=\"\"></option>\n                                                    {{- range $.Groups}}\n                                                    <option value=\"{{.Name}}\" {{- if eq $val.Name .Name}} selected{{- end}}>{{.Name}}</option>\n                                                    {{- end}}\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                <select name=\"group_type\" class=\"form-select select-repetear\">\n                                                    <option value=\"0\" data-i18n=\"admin.group_membership\" {{if eq $val.Options.AddToUsersAs 0}}selected{{end}}>Add as membership</option>\n                                                    <option value=\"1\" data-i18n=\"admin.group_primary\" {{if eq $val.Options.AddToUsersAs 1}}selected{{end}}>Add as primary</option>\n                                                    <option value=\"2\" data-i18n=\"admin.group_secondary\" {{if eq $val.Options.AddToUsersAs 2}}selected{{end}}>Add as secondary</option>\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                            <select name=\"group\" data-i18n=\"[data-placeholder]general.group_placeholder\" class=\"form-select select-repetear\" data-allow-clear=\"true\">\n                                                <option value=\"\"></option>\n                                                {{- range $.Groups}}\n                                                <option value=\"{{.Name}}\">{{.Name}}</option>\n                                                {{- end}}\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <select name=\"group_type\" class=\"form-select select-repetear select-first\">\n                                                <option value=\"0\" data-i18n=\"admin.group_membership\">Add as membership</option>\n                                                <option value=\"1\" data-i18n=\"admin.group_primary\">Add as primary</option>\n                                                <option value=\"2\" data-i18n=\"admin.group_secondary\">Add as secondary</option>\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"admin.user_page_pref\" class=\"card-title section-title-inner\">User page preferences</h3>\n                </div>\n                <div class=\"card-body\">\n                    {{- template \"infomsg\" \"admin.user_page_pref_help\"}}\n\n                    <div class=\"form-group row\">\n                        <label for=\"idUserPageHiddenSections\" data-i18n=\"admin.hide_sections\" class=\"col-md-3 col-form-label\">Hide sections</label>\n                        <div class=\"col-md-9\">\n                            <select id=\"idUserPageHiddenSections\" name=\"user_page_hidden_sections\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                                <option value=\"1\" data-i18n=\"title.groups\" {{if .Admin.Filters.Preferences.HideGroups}}selected{{end}}>Groups</option>\n                                <option value=\"2\" data-i18n=\"storage.title\" {{if .Admin.Filters.Preferences.HideFilesystem}}selected{{end}}>Filesystem</option>\n                                <option value=\"3\" data-i18n=\"title.folders\" {{if .Admin.Filters.Preferences.HideVirtualFolders}}selected{{end}}>Virtual Folders</option>\n                                <option value=\"4\" data-i18n=\"title.profile\" {{if .Admin.Filters.Preferences.HideProfile}}selected{{end}}>Profile</option>\n                                <option value=\"5\" data-i18n=\"general.acls\" {{if .Admin.Filters.Preferences.HideACLs}}selected{{end}}>ACLs</option>\n                                <option value=\"6\" data-i18n=\"general.quota_limits\" {{if .Admin.Filters.Preferences.HideDiskQuotaAndBandwidthLimits}}selected{{end}}>Disk quota and bandwidth limits</option>\n                                <option value=\"7\" data-i18n=\"general.advanced_settings\" {{if .Admin.Filters.Preferences.HideAdvancedSettings}}selected{{end}}>Advanced settings</option>\n                            </select>\n                        </div>\n                    </div>\n\n                    <div class=\"form-group row mt-10\">\n                        <label for=\"idDefaultUsersExpiration\" data-i18n=\"admin.default_users_expiration\" class=\"col-md-3 col-form-label\">Default users expiration</label>\n                        <div class=\"col-md-9\">\n                            <input id=\"idDefaultUsersExpiration\" type=\"number\" min=\"0\" class=\"form-control\" name=\"default_users_expiration\" value=\"{{.Admin.Filters.Preferences.DefaultUsersExpiration}}\" aria-describedby=\"idDefaultUsersExpirationHelp\"/>\n                            <div id=\"idDefaultUsersExpirationHelp\" class=\"form-text\" data-i18n=\"admin.default_users_expiration_help\"></div>\n                        </div>\n                    </div>\n\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idEmail\" data-i18n=\"general.email\" class=\"col-md-3 col-form-label\">Email</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idEmail\" type=\"email\" class=\"form-control\" placeholder=\"\" name=\"email\" value=\"{{.Admin.Email}}\"\n                        maxlength=\"255\" autocomplete=\"off\" spellcheck=\"false\" />\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idDescription\" type=\"text\" class=\"form-control\" name=\"description\" value=\"{{.Admin.Description}}\" maxlength=\"255\">\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idAllowedIP\" data-i18n=\"general.allowed_ip_mask\" class=\"col-md-3 col-form-label\">Allowed IP/Mask</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idAllowedIP\" name=\"allowed_ip\" aria-describedby=\"idAllowedIPHelp\"\n                        rows=\"3\">{{.Admin.GetAllowedIPAsString}}</textarea>\n                    <div id=\"idAllowedIPHelp\" class=\"form-text\" data-i18n=\"general.ip_mask_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row align-items-center mt-10\">\n                <label data-i18n=\"general.api_key_auth\" class=\"col-md-3 col-form-label\" for=\"idAllowAPIKeyAuth\">API key authentication</label>\n                <div class=\"col-md-9\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idAllowAPIKeyAuth\" name=\"allow_api_key_auth\" {{if .Admin.Filters.AllowAPIKeyAuth}}checked{{end}}/>\n                        <label data-i18n=\"filters.api_key_auth_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idAllowAPIKeyAuth\">\n                            Allow to impersonate yourself, in REST API, with an API key\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row align-items-center mt-10 {{if eq .LoggedUser.Username .Admin.Username}}d-none{{end}}\">\n                <label data-i18n=\"title.two_factor_auth_short\" class=\"col-md-3 col-form-label\" for=\"idRequire2FA\">2FA</label>\n                <div class=\"col-md-9\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idRequire2FA\" name=\"require_two_factor\" {{if .Admin.Filters.RequireTwoFactor}}checked{{end}}/>\n                        <label data-i18n=\"2fa.require\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idRequire2FA\">\n                            Two-factor authentication is required\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idAdditionalInfo\" data-i18n=\"general.additional_info\" class=\"col-md-3 col-form-label\">Additional info</label>\n                <div class=\"col-md-9\">\n                    <textarea id=\"idAdditionalInfo\" class=\"form-control\" name=\"additional_info\" rows=\"3\">{{.Admin.AdditionalInfo}}</textarea>\n                </div>\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    $(document).on(\"i18nload\", function(){\n        initRepeater('#groups');\n        initRepeaterItems();\n    });\n\n    $(document).on(\"i18nshow\", function(){\n        $('#admin_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/admins.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"admin.view_manage\" class=\"card-title section-title\">View and manage admins</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                    <button id=\"export_button\" type=\"button\" class=\"btn btn-light-primary ms-3\" data-table-filter=\"export\">\n                        <span data-i18n=\"general.export\" class=\"indicator-label\">\n                            Export\n                        </span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                </div>\n\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <button type=\"button\" class=\"btn btn-light-primary rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom\" data-kt-menu-permanent=\"true\">\n                        <span data-i18n=\"general.colvis\">Column visibility</span>\n                        <i class=\"ki-duotone ki-down fs-3 rotate-180 ms-3 me-0\"></i>\n                    </button>\n                    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4\" data-kt-menu=\"true\">\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColStatus\" />\n                            <label class=\"form-check-label\" for=\"checkColStatus\">\n                                <span data-i18n=\"general.status\" class=\"text-gray-800 fs-6\">Status</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColLastLogin\" />\n                            <label class=\"form-check-label\" for=\"checkColLastLogin\">\n                                <span data-i18n=\"general.last_login\" class=\"text-gray-800 fs-6\">Last login</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColEmail\" />\n                            <label class=\"form-check-label\" for=\"checkColEmail\">\n                                <span data-i18n=\"general.email\" class=\"text-gray-800 fs-6\">Email</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColRole\" />\n                            <label class=\"form-check-label\" for=\"checkColRole\">\n                                <span data-i18n=\"general.role\" class=\"text-gray-800 fs-6\">Role</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkCol2FA\" />\n                            <label class=\"form-check-label\" for=\"checkCol2FA\">\n                                <span data-i18n=\"title.two_factor_auth_short\" class=\"text-gray-800 fs-6\">2FA</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColDesc\" />\n                            <label class=\"form-check-label\" for=\"checkColDesc\">\n                                <span data-i18n=\"general.description\" class=\"text-gray-800 fs-6\">Description</span>\n                            </label>\n                        </div>\n                    </div>\n                    {{- if .LoggedUser.HasPermission \"*\"}}\n                    <a href=\"{{.AdminURL}}\" class=\"btn btn-primary ms-5\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                    {{- end}}\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"login.username\">Username</th>\n                        <th data-i18n=\"general.status\">Status</th>\n                        <th data-i18n=\"general.last_login\">Last login</th>\n                        <th data-i18n=\"general.email\">Email</th>\n                        <th data-i18n=\"general.role\">Role</th>\n                        <th data-i18n=\"title.two_factor_auth_short\">2FA</th>\n                        <th data-i18n=\"general.description\">Description</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/papaparse/papaparse.min.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/file-saver/FileSaver.min.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function deleteAction(username) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.AdminURL}}' + \"/\" + encodeURIComponent(username);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 400:\n                                if (error.response.data && error.response.data.error){\n                                    if (error.response.data.error.includes(\"delete yourself\")){\n                                        errorMessage = \"admin.self_delete\";\n                                    }\n                                }\n                                break;\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    function disableSeconFactorAction(username) {\n        ModalAlert.fire({\n            text: $.t('2fa.disable_confirm'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.disable_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.AdminURL}}' + \"/\" + encodeURIComponent(username)+\"/2fa/disable\";\n\n                axios.put(path, null, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    ModalAlert.fire({\n                        text: $.t('2fa.save_err'),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.AdminsURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"username\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"status\",\n                        name: \"status\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case 1:\n                                        return $.t('general.active');\n                                    default:\n                                        return $.t('general.inactive');\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"last_login\",\n                        name: \"last_login\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data > 0){\n                                    return $.t('general.datetime', {\n                                        val: parseInt(data, 10),\n                                        formatParams: {\n                                            val: { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },\n                                        }\n                                    });\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"email\",\n                        name: \"email\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\";\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"role\",\n                        name: \"role\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"filters.totp_config\",\n                        name: \"two_factor\",\n                        visible: false,\n                        defaultContent: false,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data && data.enabled){\n                                    return $.t('general.active');\n                                }\n                                return $.t('general.inactive')\n                            }\n                            return data && data.enabled;\n                        }\n                    },\n                    {\n                        data: \"description\",\n                        name: \"description\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let numActions = 0;\n                                let username = \"{{.LoggedUser.Username}}\";\n                                let actions = `<button class=\"btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                                    <span data-i18n=\"general.actions\" class=\"fs-6\">Actions</span>\n\t\t\t\t\t\t\t\t\t\t            <i class=\"ki-duotone ki-down fs-5 ms-1 rotate-180\"></i>\n                                                </button>\n                                                <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">`;\n                                //{{- if .LoggedUser.HasPermission \"*\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                //{{- if .LoggedUser.HasPermission \"disable_mfa\"}}\n                                if (row.filters.totp_config && row.filters.totp_config.enabled && username != row.username){\n                                    numActions++;\n                                    actions+=`<div class=\"menu-item px-3\">\n                                                <a data-i18n=\"2fa.disable_msg\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"disable_2fa_row\">Disable 2FA</a>\n\t\t\t\t\t\t\t\t\t\t      </div>`;\n                                }\n                                //{{- end}}\n                                //{{- if .LoggedUser.HasPermission \"*\"}}\n                                if (username != row.username){\n                                    numActions++;\n                                    actions+=`<div class=\"menu-item px-3\">\n                                                <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t     </div>`;\n                                }\n                                //{{- end}}\n                                if (numActions > 0){\n                                    actions+=`</div>`;\n                                    return actions;\n                                }\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                colReorder: {\n                    enable: true,\n                    fixedColumnsLeft: 1\n                },\n                stateLoadParams: function (settings, data) {\n                        if (data.search.search){\n                            const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                            filterSearch.value = data.search.search;\n                        }\n                    },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n            dt.on('columns-reordered', function(e, settings, details){\n                drawAction();\n            });\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        function handleColVisibilityCheckbox(el, selector) {\n            let index = dt.column(selector).index();\n            el.off(\"change\");\n            el.prop('checked', dt.column(index).visible());\n            el.on(\"change\", function(e){\n                let index = dt.column(selector).index();\n                dt.column(index).visible($(this).is(':checked'));\n                dt.draw('page');\n            });\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n            handleColVisibilityCheckbox($('#checkColStatus'), \"status:name\");\n            handleColVisibilityCheckbox($('#checkColLastLogin'), \"last_login:name\");\n            handleColVisibilityCheckbox($('#checkColEmail'), \"email:name\");\n            handleColVisibilityCheckbox($('#checkColRole'), \"role:name\");\n            handleColVisibilityCheckbox($('#checkCol2FA'), \"two_factor:name\");\n            handleColVisibilityCheckbox($('#checkColDesc'), \"description:name\");\n\n            const exportButton = $(document.querySelector('[data-table-filter=\"export\"]'));\n            exportButton.on('click', function(e){\n                e.preventDefault();\n                this.blur();\n                this.setAttribute('data-kt-indicator', 'on');\n\t\t\t    this.disabled = true;\n\n                let data = [];\n                dt.rows({ search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                    let line = {};\n                    let rowData = dt.row(rowIdx).data();\n                    let filters = rowData[\"filters\"];\n\n                    line[\"Username\"] = rowData[\"username\"];\n                    let status = \"Inactive\";\n                    if (rowData[\"status\"] == 1){\n                        status = \"Active\";\n                    }\n                    line[\"Status\"] = status;\n                    line[\"Permissions\"] = rowData[\"permissions\"].join(\", \");\n\n                    if (rowData[\"created_at\"]) {\n                        line[\"Created At\"] = moment(rowData[\"created_at\"]).format(\"YYYY-MM-DD HH:mm\");\n                    } else {\n                        line[\"Created At\"] = \"\";\n                    }\n                    if (rowData[\"updated_at\"]) {\n                        line[\"Updated At\"] = moment(rowData[\"updated_at\"]).format(\"YYYY-MM-DD HH:mm\");\n                    } else {\n                        line[\"Updated At\"] = \"\";\n                    }\n                    if (rowData[\"last_login\"]) {\n                        line[\"Last Login\"] = moment(rowData[\"last_login\"]).format(\"YYYY-MM-DD HH:mm\");\n                    } else {\n                        line[\"Last Login\"] = \"\";\n                    }\n                    if (rowData[\"email\"]){\n                        line[\"Email\"] = rowData[\"email\"];\n                    } else {\n                        line[\"Email\"] = \"\";\n                    }\n                    let totpConfig = filters[\"totp_config\"];\n                    if (totpConfig && totpConfig[\"enabled\"]){\n                        line[\"Two-factor auth\"] = \"Enabled\";\n                    } else {\n                        line[\"Two-factor auth\"] = \"Disabled\";\n                    }\n                    if (filters[\"allow_list\"]){\n                        line[\"Allow List\"] = filters[\"allow_list\"].join(\", \");\n                    } else {\n                        line[\"Allow List\"] = \"\";\n                    }\n                    let groups = [];\n                    let rowGroups = rowData[\"groups\"];\n                    if (rowGroups){\n                        for (i = 0; i < rowGroups.length; i++ ){\n                            groups.push(rowGroups[i].name);\n                        }\n                    }\n                    line[\"Groups\"] = groups.join(\", \");\n                    if (rowData[\"role\"]){\n                        line[\"Role\"] = rowData[\"role\"];\n                    } else {\n                        line[\"Role\"] = \"\";\n                    }\n                    if (rowData[\"description\"]){\n                        line[\"Description\"] = rowData[\"description\"];\n                    } else {\n                        line[\"Description\"] = \"\";\n                    }\n                    if (rowData[\"additional_info\"]){\n                        line[\"Additional info\"] = rowData[\"additional_info\"];\n                    } else {\n                        line[\"Additional info\"] = \"\";\n                    }\n\n                    data.push(line);\n                });\n\n                let csv = Papa.unparse(data, {\n                    quotes: false,\n\t                quoteChar: '\"',\n\t                escapeChar: '\"',\n\t                delimiter: \",\",\n\t                header: true,\n\t                newline: \"\\r\\n\",\n\t                skipEmptyLines: false,\n\t                columns: null,\n                    escapeFormulae: true\n                });\n                let blob = new Blob([csv], {type: \"text/csv\"});\n                let ts = moment().format(\"YYYY_MM_DD_HH_mm_ss\");\n                saveAs(blob, `admins_${ts}.csv`);\n\n                this.removeAttribute('data-kt-indicator');\n                this.disabled = false;\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.AdminURL}}' + \"/\" + encodeURIComponent(rowData['username']));\n                });\n            });\n\n            const diable2FAButtons = document.querySelectorAll('[data-table-action=\"disable_2fa_row\"]');\n            diable2FAButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    disableSeconFactorAction(rowData['username']);\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    deleteAction(dt.row(parent).data()['username']);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/adminsetup.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"baselogin\" .}}\n\n{{- define \"content\"}}\n<form class=\"form w-100\" id=\"sign_in_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n    <div class=\"container mb-10\">\n        <div class=\"row align-items-center\">\n            <div class=\"col-5 align-items-center\">\n                <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n            </div>\n            <div class=\"col-7\">\n                <span class=\"text-gray-900 mb-3 ms-3 fs-1 fw-bold\">\n                    {{.Branding.ShortName}}\n                </span>\n            </div>\n        </div>\n    </div>\n    <div class=\"text-center mb-10\">\n        <h2 data-i18n=\"title.setup\" class=\"text-gray-900 mb-3\"></h2>\n        <div class=\"text-gray-700 fw-semibold fs-4\">\n            <span data-i18n=\"setup.desc\"></span>\n        </div>\n    </div>\n    {{- template \"errmsg\" .Error}}\n    {{- if .HasInstallationCode}}\n    <div class=\"fv-row mb-10\">\n        <input class=\"form-control form-control-lg form-control-solid\" type=\"text\" placeholder=\"{{.InstallationCodeHint}}\" name=\"install_code\" spellcheck=\"false\" required />\n    </div>\n    {{- end}}\n    <div class=\"fv-row mb-10\">\n        <input data-i18n=\"[placeholder]login.username\" class=\"form-control form-control-lg form-control-solid\" type=\"text\" name=\"username\" placeholder=\"Username\" autocomplete=\"on\" spellcheck=\"false\" required />\n    </div>\n    <div class=\"fv-row mb-10\">\n        <div class=\"position-relative\" data-password-control=\"container\">\n            <input data-i18n=\"[placeholder]login.password\" data-password-control=\"input\" class=\"form-control form-control-lg form-control-solid\"\n                type=\"password\" name=\"password\" placeholder=\"Password\" autocomplete=\"new-password\" spellcheck=\"false\" required />\n            <span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                <i class=\"ki-duotone ki-eye-slash fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                    <span class=\"path4\"></span>\n                </i>\n                <i class=\"ki-duotone ki-eye d-none fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                </i>\n            </span>\n        </div>\n    </div>\n    <div class=\"fv-row mb-10\">\n        <div class=\"position-relative\" data-password-control=\"container\">\n            <input data-i18n=\"[placeholder]change_pwd.confirm\" data-password-control=\"input\" class=\"form-control form-control-lg form-control-solid\"\n                type=\"password\" name=\"confirm_password\" placeholder=\"Confirm Password\" autocomplete=\"new-password\" spellcheck=\"false\" required />\n            <span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                <i class=\"ki-duotone ki-eye-slash fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                    <span class=\"path4\"></span>\n                </i>\n                <i class=\"ki-duotone ki-eye d-none fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                </i>\n            </span>\n        </div>\n    </div>\n    <div class=\"text-center\">\n        <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n        <button type=\"submit\" id=\"sign_in_submit\" class=\"btn btn-lg btn-primary w-100 mb-5\">\n            <span data-i18n=\"setup.submit\" class=\"indicator-label\"></span>\n            <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                Please wait...\n                <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n            </span>\n        </button>\n    </div>\n</form>\n{{- if or (not .HideSupportLink) (gt (len .Languages) 1)}}\n<hr>\n{{- end}}\n<div class=\"d-flex flex-stack pt-5 mt-3\">\n    <div class=\"me-10\">\n        <select id=\"languageSwitcher\" name=\"language\" class=\"form-select form-select-solid form-select-sm\" data-control=\"i18n-select2\" data-hide-search=\"true\"></select>\n    </div>\n    {{- if not .HideSupportLink}}\n    <div class=\"d-flex fw-semibold text-primary\">\n        <a href=\"https://sftpgo.com/on-premises\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"px-2\">\n            <span data-i18n=\"setup.help_text\"></span>\n        </a>\n    </div>\n    {{- end}}\n</div>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/base.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- define \"navitems\"}}\n{{- block \"additionalnavitems\" .}}{{- end}}\n{{- template \"theme-switcher\"}}\n<div class=\"d-flex align-items-center ms-2 ms-lg-3\">\n    <div class=\"btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px\" data-kt-menu-trigger=\"{default:'click', lg: 'hover'}\" data-kt-menu-attach=\"parent\" data-kt-menu-placement=\"bottom-end\">\n        <i class=\"ki-duotone ki-user fs-2\">\n            <i class=\"path1\"></i>\n            <i class=\"path2\"></i>\n        </i>\n    </div>\n    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-gray-500 menu-active-bg menu-state-color fw-semibold py-4 w-250px\" data-kt-menu=\"true\">\n        <div class=\"menu-item px-3 my-0\">\n            <div class=\"menu-content d-flex align-items-center px-3 py-2\">\n                <div class=\"me-5\">\n                    <i class=\"ki-duotone ki-user fs-2\">\n                        <i class=\"path1\"></i>\n                        <i class=\"path2\"></i>\n                    </i>\n                </div>\n                <div class=\"d-flex flex-column\">\n                    <div class=\"fw-semibold d-flex align-items-center fs-5\">\n                        <span class=\"w-175px wrap-word\">{{.LoggedUser.Username}}</span>\n                    </div>\n                </div>\n            </div>\n        </div>\n        <div class=\"separator my-2\"></div>\n        {{if not .HasExternalLogin}}\n        <div class=\"menu-item px-3 my-0\">\n            <a href=\"{{.ProfileURL}}\" class=\"menu-link px-3 py-2\">\n                <span data-i18n=\"title.profile\" class=\"menu-title\">Profile</span>\n            </a>\n        </div>\n        <div class=\"menu-item px-3 my-0\">\n            <a href=\"{{.ChangePwdURL}}\" class=\"menu-link px-3 py-2\">\n                <span data-i18n=\"title.change_password\" class=\"menu-title\">Change password</span>\n            </a>\n        </div>\n        {{if .LoggedUser.CanManageMFA}}\n        <div class=\"menu-item px-3 my-0\">\n            <a href=\"{{.MFAURL}}\" class=\"menu-link px-3 py-2\">\n                <span data-i18n=\"title.two_factor_auth\" class=\"menu-title\">Two-factor authentication</span>\n            </a>\n        </div>\n        {{- end}}\n        {{- end}}\n        <div class=\"menu-item px-3 my-0\">\n            <a id=\"id_logout_link\" href=\"#\" class=\"menu-link px-3 py-2\">\n                <span data-i18n=\"login.signout\" class=\"menu-title\">Sign out</span>\n            </a>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"sidebaritems\"}}\n{{- if .LoggedUser.HasPermission \"view_users\"}}\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .UsersURL}} active{{- end}}\" href=\"{{.UsersURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-people fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n                <span class=\"path3\"></span>\n                <span class=\"path4\"></span>\n                <span class=\"path5\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.users\" class=\"menu-title\">Users</span>\n    </a>\n</div>\n{{- end}}\n{{- if .LoggedUser.HasPermission \"manage_groups\"}}\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .GroupsURL}} active{{- end}}\" href=\"{{.GroupsURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-profile-user fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n                <span class=\"path3\"></span>\n                <span class=\"path4\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.groups\" class=\"menu-title\">Groups</span>\n    </a>\n</div>\n{{- end}}\n{{- if .LoggedUser.HasPermission \"manage_folders\"}}\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .FoldersURL}} active{{- end}}\" href=\"{{.FoldersURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-folder fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.folders\" class=\"menu-title\">Folders</span>\n    </a>\n</div>\n{{- end}}\n{{- if .LoggedUser.HasPermission \"view_conns\"}}\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .ConnectionsURL}} active{{- end}}\" href=\"{{.ConnectionsURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-arrow-up-down fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.connections\" class=\"menu-title\">Connections</span>\n    </a>\n</div>\n{{- end}}\n{{ if .LoggedUser.HasPermission \"*\"}}\n<div data-kt-menu-trigger=\"click\" class=\"menu-item menu-accordion {{- if .IsEventManagerPage}} here show{{- end}}\">\n    <span class=\"menu-link\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-calendar fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.event_manager\" class=\"menu-title\">Event Manager</span>\n        <span class=\"menu-arrow\"></span>\n    </span>\n    <div class=\"menu-sub menu-sub-accordion\">\n        <div class=\"menu-item\">\n            <a class=\"menu-link {{- if eq .CurrentURL .EventRulesURL}} active{{- end}}\" href=\"{{.EventRulesURL}}\">\n                <span class=\"menu-bullet\">\n                    <span class=\"bullet bullet-dot\"></span>\n                </span>\n                <span data-i18n=\"title.event_rules\" class=\"menu-title fs-5 fw-semibold\">Rules</span>\n            </a>\n        </div>\n        <div class=\"menu-item\">\n            <a class=\"menu-link {{- if eq .CurrentURL .EventActionsURL}} active{{- end}}\" href=\"{{.EventActionsURL}}\">\n                <span class=\"menu-bullet\">\n                    <span class=\"bullet bullet-dot\"></span>\n                </span>\n                <span data-i18n=\"title.event_actions\" class=\"menu-title fs-5 fw-semibold\">Actions</span>\n            </a>\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- if or (.LoggedUser.HasPermission \"*\") (and .HasDefender (.LoggedUser.HasPermission \"view_defender\"))}}\n<div data-kt-menu-trigger=\"click\" class=\"menu-item menu-accordion {{- if .IsIPManagerPage}} here show{{- end}}\">\n    <span class=\"menu-link\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-solid ki-shield fs-1\"></i>\n        </span>\n        <span data-i18n=\"title.ip_manager\" class=\"menu-title\">IP Manager</span>\n        <span class=\"menu-arrow\"></span>\n    </span>\n    <div class=\"menu-sub menu-sub-accordion\">\n        {{- if .LoggedUser.HasPermission \"*\"}}\n        <div class=\"menu-item\">\n            <a class=\"menu-link {{- if eq .CurrentURL .IPListsURL}} active{{- end}}\" href=\"{{.IPListsURL}}\">\n                <span class=\"menu-bullet\">\n                    <span class=\"bullet bullet-dot\"></span>\n                </span>\n                <span data-i18n=\"title.ip_lists\" class=\"menu-title fs-5 fw-semibold\">IP lists</span>\n            </a>\n        </div>\n        {{- end}}\n        {{- if and .HasDefender (.LoggedUser.HasPermission \"view_defender\")}}\n        <div class=\"menu-item\">\n            <a class=\"menu-link {{- if eq .CurrentURL .DefenderURL}} active{{- end}}\" href=\"{{.DefenderURL}}\">\n                <span class=\"menu-bullet\">\n                    <span class=\"bullet bullet-dot\"></span>\n                </span>\n                <span data-i18n=\"title.defender\" class=\"menu-title fs-5 fw-semibold\">Auto Blocklist</span>\n            </a>\n        </div>\n        {{- end}}\n    </div>\n</div>\n{{- end}}\n{{- if or (.LoggedUser.HasPermission \"*\") (.LoggedUser.HasPermission \"view_status\") (and .HasSearcher (.LoggedUser.HasPermission \"view_events\"))}}\n<div data-kt-menu-trigger=\"click\" class=\"menu-item menu-accordion {{- if .IsServerManagerPage}} here show{{- end}}\">\n    <span class=\"menu-link\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-setting-3 fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n                <span class=\"path3\"></span>\n                <span class=\"path4\"></span>\n                <span class=\"path5\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.server_manager\" class=\"menu-title\">Server Manager</span>\n        <span class=\"menu-arrow\"></span>\n    </span>\n    <div class=\"menu-sub menu-sub-accordion\">\n        {{- if .LoggedUser.HasPermission \"*\"}}\n        <div class=\"menu-item\">\n            <a class=\"menu-link {{- if eq .CurrentURL .ConfigsURL}} active{{- end}}\" href=\"{{.ConfigsURL}}\">\n                <span class=\"menu-bullet\">\n                    <span class=\"bullet bullet-dot\"></span>\n                </span>\n                <span data-i18n=\"title.configs\" class=\"menu-title fs-5 fw-semibold\">Configurations</span>\n            </a>\n        </div>\n        {{- end}}\n        {{ if and .HasSearcher (.LoggedUser.HasPermission \"view_events\")}}\n        <div class=\"menu-item\">\n            <a class=\"menu-link {{- if eq .CurrentURL .EventsURL}} active{{- end}}\" href=\"{{.EventsURL}}\">\n                <span class=\"menu-bullet\">\n                    <span class=\"bullet bullet-dot\"></span>\n                </span>\n                <span data-i18n=\"title.logs\" class=\"menu-title fs-5 fw-semibold\">Logs</span>\n            </a>\n        </div>\n        {{- end}}\n        {{- if .LoggedUser.HasPermission \"*\"}}\n        <div class=\"menu-item\">\n            <a class=\"menu-link {{- if eq .CurrentURL .MaintenanceURL}} active{{- end}}\" href=\"{{.MaintenanceURL}}\">\n                <span class=\"menu-bullet\">\n                    <span class=\"bullet bullet-dot\"></span>\n                </span>\n                <span data-i18n=\"title.maintenance\" class=\"menu-title fs-5 fw-semibold\">Maintenance</span>\n            </a>\n        </div>\n        {{- end}}\n        {{- if .LoggedUser.HasPermission \"view_status\"}}\n        <div class=\"menu-item\">\n            <a class=\"menu-link {{- if eq .CurrentURL .StatusURL}} active{{- end}}\" href=\"{{.StatusURL}}\">\n                <span class=\"menu-bullet\">\n                    <span class=\"bullet bullet-dot\"></span>\n                </span>\n                <span data-i18n=\"title.status\" class=\"menu-title fs-5 fw-semibold\">Status</span>\n            </a>\n        </div>\n        {{- end}}\n    </div>\n</div>\n{{- end}}\n{{- if .LoggedUser.HasPermission \"*\"}}\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .AdminsURL}} active{{- end}}\" href=\"{{.AdminsURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-solid ki-security-user fs-1\"></i>\n        </span>\n        <span data-i18n=\"title.admins\" class=\"menu-title\">Admins</span>\n    </a>\n</div>\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .RolesURL}} active{{- end}}\" href=\"{{.RolesURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-user-tick fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n                <span class=\"path3\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.roles\" class=\"menu-title\">Roles</span>\n    </a>\n</div>\n{{- end}}\n\n{{- end}}"
  },
  {
    "path": "templates/webadmin/configs.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<style {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    .image-input-placeholder {\n        background-image: url('{{.StaticURL}}{{.Branding.DefaultLogoPath}}');\n    }\n\n    /*{{- if ne .Branding.DefaultLogoPath .Branding.LogoPath}}*/\n    .image-input-webadmin-current {\n        background-image: url('{{.StaticURL}}{{.Branding.LogoPath}}');\n    }\n    /*{{- end}}*/\n    /*{{- if ne .Branding.DefaultFaviconPath .Branding.FaviconPath}}*/\n    .image-input-webadmin-fav-current {\n        background-image: url('{{.StaticURL}}{{.Branding.FaviconPath}}');\n    }\n    /*{{- end}}*/\n\n    /*{{- if ne .Branding.DefaultLogoPath .WebClientBranding.LogoPath}}*/\n    .image-input-webclient-current {\n        background-image: url('{{.StaticURL}}{{.WebClientBranding.LogoPath}}');\n    }\n    /*{{- end}}*/\n    /*{{- if ne .Branding.DefaultFaviconPath .WebClientBranding.FaviconPath}}*/\n    .image-input-webclient-fav-current {\n        background-image: url('{{.StaticURL}}{{.WebClientBranding.FaviconPath}}');\n    }\n    /*{{- end}}*/\n</style>\n{{- end}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <div class=\"accordion\" id=\"accordion_configs\">\n            <div class=\"accordion-item\">\n                <h2 class=\"accordion-header\" id=\"accordion_header_sftp\">\n                    <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#accordion_sftp_body\" aria-expanded=\"{{if eq .ConfigSection 1}}true{{else}}false{{end}}\" aria-controls=\"accordion_sftp_body\">\n                        <span data-i18n=\"storage.sftp\">SFTP</span>\n                    </button>\n                </h2>\n                <div id=\"accordion_sftp_body\" class=\"accordion-collapse collapse {{if eq .ConfigSection 1}}show{{end}}\" aria-labelledby=\"accordion_header_sftp\" data-bs-parent=\"#accordion_configs\">\n                    <div class=\"accordion-body\">\n                        {{- template \"infomsg\" \"sftp.help\"}}\n\n                        <form id=\"configs_sftp_form\" enctype=\"multipart/form-data\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n                            <div class=\"form-group row\">\n                                <label for=\"idHostKeyAlgos\" data-i18n=\"sftp.host_key_algos\" class=\"col-md-3 col-form-label\">\n                                    Host Key Algos\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idHostKeyAlgos\" name=\"sftp_host_key_algos\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $val := .Configs.SFTPD.GetSupportedHostKeyAlgos}}\n                                        <option value=\"{{$val}}\" {{range $algo :=$.Configs.SFTPD.HostKeyAlgos }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idPubKeyAlgos\" data-i18n=\"status.ssh_pub_key_algo\" class=\"col-md-3 col-form-label\">\n                                    Public Key Algos\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idPubKeyAlgos\" name=\"sftp_pub_key_algos\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $val := .Configs.SFTPD.GetSupportedPublicKeyAlgos}}\n                                        <option value=\"{{$val}}\" {{range $algo :=$.Configs.SFTPD.PublicKeyAlgos }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idKEXAlgos\" data-i18n=\"status.ssh_kex_algo\" class=\"col-md-3 col-form-label\">\n                                    KEX Algos\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idKEXAlgos\" name=\"sftp_kex_algos\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $val := .Configs.SFTPD.GetSupportedKEXAlgos}}\n                                        <option value=\"{{$val}}\" {{range $algo :=$.Configs.SFTPD.KexAlgorithms }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idMACAlgos\" data-i18n=\"status.ssh_mac_algo\" class=\"col-md-3 col-form-label\">\n                                    MAC Algos\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idMACAlgos\" name=\"sftp_macs\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $val := .Configs.SFTPD.GetSupportedMACs}}\n                                        <option value=\"{{$val}}\" {{range $algo :=$.Configs.SFTPD.MACs }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idCiphers\" data-i18n=\"status.ssh_cipher_algo\" class=\"col-md-3 col-form-label\">\n                                    KEX Algos\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idCiphers\" name=\"sftp_ciphers\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{range $val := .Configs.SFTPD.GetSupportedCiphers}}\n                                        <option value=\"{{$val}}\" {{range $algo :=$.Configs.SFTPD.Ciphers }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>\n                                        {{end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"d-flex justify-content-end mt-12\">\n                                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                                <input type=\"hidden\" name=\"form_action\" value=\"sftp_submit\">\n                                <button type=\"submit\" id=\"sftp_form_submit\" class=\"btn btn-primary px-10\">\n                                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                                        Submit\n                                    </span>\n                                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                                        Please wait...\n                                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                                    </span>\n                                </button>\n                            </div>\n                        </form>\n\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"accordion-item\">\n                <h2 class=\"accordion-header\" id=\"accordion_header_acme\">\n                    <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#accordion_acme_body\" aria-expanded=\"{{if eq .ConfigSection 2}}true{{else}}false{{end}}\" aria-controls=\"accordion_acme_body\">\n                        <span data-i18n=\"acme.title\">ACME</span>\n                    </button>\n                </h2>\n                <div id=\"accordion_acme_body\" class=\"accordion-collapse collapse {{if eq .ConfigSection 2}}show{{end}}\" aria-labelledby=\"accordion_header_acme\" data-bs-parent=\"#accordion_configs\">\n                    <div class=\"accordion-body\">\n                        {{- template \"infomsg\" \"acme.help\"}}\n\n                        <form id=\"configs_acme_form\" enctype=\"multipart/form-data\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n\n                            <div class=\"form-group row\">\n                                <label for=\"idACMEDomain\" data-i18n=\"general.domain\" class=\"col-md-3 col-form-label\">Domain</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idACMEDomain\" type=\"text\" class=\"form-control\" name=\"acme_domain\" value=\"{{.Configs.ACME.Domain}}\" aria-describedby=\"idACMEDomainHelp\" />\n                                    <div id=\"idACMEDomainHelp\" class=\"form-text\" data-i18n=\"acme.domain_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idACMEEmail\" data-i18n=\"general.email\" class=\"col-md-3 col-form-label\">Email</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idACMEEmail\" type=\"email\" class=\"form-control\" placeholder=\"\" name=\"acme_email\" value=\"{{.Configs.ACME.Email}}\" aria-describedby=\"idACMEEmailHelp\"\n                                        maxlength=\"255\" autocomplete=\"off\" spellcheck=\"false\" />\n                                    <div id=\"idACMEEmailHelp\" class=\"form-text\" data-i18n=\"acme.email_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idACMEPort\" data-i18n=\"general.port\" class=\"col-md-3 col-form-label\">Port</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idACMEPort\" type=\"number\" min=\"1\" max=\"65535\" class=\"form-control\" name=\"acme_port\" value=\"{{.Configs.ACME.HTTP01Challenge.Port}}\" aria-describedby=\"idACMEPortHelp\" />\n                                    <div id=\"idACMEPortHelp\" class=\"form-text\" data-i18n=\"acme.port_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idACMEProtocols\" data-i18n=\"ip_list.protocols\" class=\"col-md-3 col-form-label\">\n                                    Protocols\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idACMEProtocols\" name=\"acme_protocols\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple aria-describedby=\"idACMEProtocolsHelp\">\n                                        <option value=\"1\" {{if .Configs.ACME.HasProtocol \"HTTP\"}}selected{{end}}>HTTP</option>\n                                        <option value=\"2\" {{if .Configs.ACME.HasProtocol \"FTP\"}}selected{{end}}>FTP</option>\n                                        <option value=\"3\" {{if .Configs.ACME.HasProtocol \"DAV\"}}selected{{end}}>DAV</option>\n                                    </select>\n                                    <div id=\"idACMEProtocolsHelp\" class=\"form-text\" data-i18n=\"acme.protocols_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"d-flex justify-content-end mt-12\">\n                                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                                <input type=\"hidden\" name=\"form_action\" value=\"acme_submit\">\n                                <button type=\"submit\" id=\"acme_form_submit\" class=\"btn btn-primary px-10\">\n                                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                                        Submit\n                                    </span>\n                                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                                        Please wait...\n                                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                                    </span>\n                                </button>\n                            </div>\n                        </form>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"accordion-item\">\n                <h2 class=\"accordion-header\" id=\"accordion_header_smtp\">\n                    <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#accordion_smtp_body\" aria-expanded=\"{{if eq .ConfigSection 3}}true{{else}}false{{end}}\" aria-controls=\"accordion_smtp_body\">\n                        <span data-i18n=\"smtp.title\">SMTP</span>\n                    </button>\n                </h2>\n                <div id=\"accordion_smtp_body\" class=\"accordion-collapse collapse {{if eq .ConfigSection 3}}show{{end}}\" aria-labelledby=\"accordion_header_smtp\" data-bs-parent=\"#accordion_configs\">\n                    <div class=\"accordion-body\">\n                        {{- template \"infomsg\" \"smtp.help\"}}\n\n                        <form id=\"configs_smtp_form\" enctype=\"multipart/form-data\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n\n                            <div class=\"form-group row\">\n                                <label for=\"idSMTPHost\" data-i18n=\"smtp.host\" class=\"col-md-3 col-form-label\">Server name</label>\n                                <div class=\"col-md-5\">\n                                    <input id=\"idSMTPHost\" type=\"text\" class=\"form-control\" name=\"smtp_host\" value=\"{{.Configs.SMTP.Host}}\" maxlength=\"512\" spellcheck=\"false\" aria-describedby=\"idSMTPHostHelp\" />\n                                    <div id=\"idSMTPHostHelp\" class=\"form-text\" data-i18n=\"smtp.host_help\"></div>\n                                </div>\n                                <div class=\"col-md-1\"></div>\n                                <label for=\"idSMTPPort\" data-i18n=\"general.port\" class=\"col-md-1 col-form-label\">Port</label>\n                                <div class=\"col-md-2\">\n                                    <input id=\"idSMTPPort\" type=\"number\" min=\"1\" max=\"65535\" class=\"form-control\" name=\"smtp_port\" value=\"{{.Configs.SMTP.Port}}\"/>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idSMTPUsername\" data-i18n=\"login.username\" class=\"col-md-3 col-form-label\">Username</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idSMTPUsername\" type=\"text\" placeholder=\"\" name=\"smtp_username\" value=\"{{.Configs.SMTP.User}}\" maxlength=\"255\" autocomplete=\"off\"\n                                        spellcheck=\"false\" class=\"form-control\" />\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idSMTPPassword\" data-i18n=\"login.password\" class=\"col-md-3 col-form-label\">Password</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idSMTPPassword\" type=\"password\" class=\"form-control\" name=\"smtp_password\" autocomplete=\"new-password\"\n                                        spellcheck=\"false\" value=\"{{if .Configs.SMTP.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Configs.SMTP.Password.GetPayload}}{{end}}\" />\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idSMTPAuth\" data-i18n=\"smtp.auth\" class=\"col-md-3 col-form-label\">Auth</label>\n                                <div class=\"col-md-3\">\n                                    <select id=\"idSMTPAuth\" name=\"smtp_auth\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                                        <option value=\"0\" {{if eq .Configs.SMTP.AuthType 0}}selected{{end}}>Plain</option>\n                                        <option value=\"1\" {{if eq .Configs.SMTP.AuthType 1}}selected{{end}}>Login</option>\n                                        <option value=\"2\" {{if eq .Configs.SMTP.AuthType 2}}selected{{end}}>CRAM-MD5</option>\n                                        <option value=\"3\" {{if eq .Configs.SMTP.AuthType 3}}selected{{end}}>OAuth2</option>\n                                    </select>\n                                </div>\n                                <div class=\"col-md-1\"></div>\n                                <label for=\"idSMTPEncryption\" data-i18n=\"smtp.encryption\" class=\"col-md-2 col-form-label\">Encryption</label>\n                                <div class=\"col-md-3\">\n                                    <select id=\"idSMTPEncryption\" name=\"smtp_encryption\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                                        <option value=\"0\" {{if eq .Configs.SMTP.Encryption 0}}selected{{end}}>---</option>\n                                        <option value=\"1\" {{if eq .Configs.SMTP.Encryption 1}}selected{{end}}>TLS</option>\n                                        <option value=\"2\" {{if eq .Configs.SMTP.Encryption 2}}selected{{end}}>STARTTLS</option>\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row smtp-oauth2 mt-10\">\n                                <label for=\"idSMTPOAuth2Provider\" data-i18n=\"smtp.oauth2_provider\" class=\"col-md-3 col-form-label\">OAuth2 provider</label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idSMTPOAuth2Provider\" name=\"smtp_oauth2_provider\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" aria-describedby=\"idSMTPOAuth2ProviderHelp\">\n                                        <option value=\"0\" {{if eq .Configs.SMTP.OAuth2.Provider 0}}selected{{end}}>Google</option>\n                                        <option value=\"1\" {{if eq .Configs.SMTP.OAuth2.Provider 1}}selected{{end}}>Microsoft</option>\n                                    </select>\n                                    <div id=\"idSMTPOAuth2ProviderHelp\" class=\"form-text\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row smtp-oauth2 smtp-oauth2-microsoft mt-10\">\n                                <label for=\"idSMTPOauth2Tenant\" data-i18n=\"smtp.oauth2_tenant\" class=\"col-md-3 col-form-label\">OAuth2 Tenant</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idSMTPOauth2Tenant\" type=\"text\" class=\"form-control\" name=\"smtp_oauth2_tenant\" value=\"{{.Configs.SMTP.OAuth2.Tenant}}\" aria-describedby=\"idSMTPOauth2TenantHelp\" />\n                                    <div id=\"idSMTPOauth2TenantHelp\" class=\"form-text\" data-i18n=\"smtp.oauth2_tenant_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row smtp-oauth2 mt-10\">\n                                <label for=\"idSMTPOauth2ClientID\" data-i18n=\"smtp.oauth2_client_id\" class=\"col-md-3 col-form-label\">OAuth2 Client ID</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idSMTPOauth2ClientID\" type=\"text\" class=\"form-control\" name=\"smtp_oauth2_client_id\" value=\"{{.Configs.SMTP.OAuth2.ClientID}}\" spellcheck=\"false\" />\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row smtp-oauth2 mt-10\">\n                                <label for=\"idSMTPOAuth2ClientSecret\" data-i18n=\"smtp.oauth2_client_secret\" class=\"col-md-3 col-form-label\">OAuth2 Client secret</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idSMTPOAuth2ClientSecret\" type=\"password\" class=\"form-control\" name=\"smtp_oauth2_client_secret\" spellcheck=\"false\" autocomplete=\"new-password\"\n                                        value=\"{{if .Configs.SMTP.OAuth2.ClientSecret.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Configs.SMTP.OAuth2.ClientSecret.GetPayload}}{{end}}\" />\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row smtp-oauth2 mt-10\">\n                                <label for=\"idSMTPOAuth2RefreshToken\" data-i18n=\"smtp.oauth2_token\" class=\"col-md-3 col-form-label\">OAuth2 Token</label>\n                                <div class=\"col-md-9\">\n                                    <div class=\"input-group\">\n                                        <input id=\"idSMTPOAuth2RefreshToken\" type=\"password\" class=\"form-control rounded-left\" name=\"smtp_oauth2_refresh_token\" spellcheck=\"false\" autocomplete=\"new-password\"\n                                            value=\"{{if .Configs.SMTP.OAuth2.RefreshToken.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Configs.SMTP.OAuth2.RefreshToken.GetPayload}}{{end}}\" />\n                                        <button id=\"refresh_token_button\" type=\"button\" class=\"btn btn-primary\">\n                                            <span data-i18n=\"general.get\">Get</span>\n                                        </button>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idSMTPFrom\" data-i18n=\"smtp.sender\" class=\"col-md-3 col-form-label\">From</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idSMTPFrom\" type=\"text\" placeholder=\"\" name=\"smtp_from\" value=\"{{.Configs.SMTP.From}}\" maxlength=\"512\" autocomplete=\"off\"\n                                        spellcheck=\"false\" class=\"form-control\" />\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idSMTPDomain\" data-i18n=\"general.domain\" class=\"col-md-3 col-form-label\">Domain</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idSMTPDomain\" type=\"text\" placeholder=\"\" name=\"smtp_domain\" value=\"{{.Configs.SMTP.Domain}}\" maxlength=\"512\"\n                                        spellcheck=\"false\" class=\"form-control\" aria-describedby=\"idSMTPDomainHelp\" />\n                                    <div id=\"idSMTPDomainHelp\" class=\"form-text\" data-i18n=\"smtp.domain_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row align-items-center mt-10\">\n                                <label data-i18n=\"smtp.debug\" class=\"col-md-3 col-form-label\" for=\"idSMTPDebug\">Debug logs</label>\n                                <div class=\"col-md-9\">\n                                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idSMTPDebug\" name=\"smtp_debug\" />\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <div class=\"col-md-12\">\n                                    <div class=\"input-group\">\n                                        <input id=\"idSMTPRecipient\" type=\"email\" class=\"form-control rounded-left\" name=\"smtp_oauth2_refresh_token\"\n                                            data-i18n=\"[placeholder]smtp.test_recipient\" spellcheck=\"false\" />\n                                        <button id=\"smtp_test_button\" type=\"button\" class=\"btn btn-primary\">\n                                            <span data-i18n=\"general.test\">Test</span>\n                                        </button>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"d-flex justify-content-end mt-12\">\n                                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                                <input type=\"hidden\" name=\"form_action\" value=\"smtp_submit\">\n                                <button type=\"submit\" id=\"smtp_form_submit\" class=\"btn btn-primary px-10\">\n                                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                                        Submit\n                                    </span>\n                                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                                        Please wait...\n                                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                                    </span>\n                                </button>\n                            </div>\n                        </form>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"accordion-item\">\n                <h2 class=\"accordion-header\" id=\"accordion_header_branding\">\n                    <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#accordion_branding_body\" aria-expanded=\"{{if eq .ConfigSection 4}}true{{else}}false{{end}}\" aria-controls=\"accordion_branding_body\">\n                        <span data-i18n=\"branding.title\">Branding</span>\n                    </button>\n                </h2>\n                <div id=\"accordion_branding_body\" class=\"accordion-collapse collapse {{if eq .ConfigSection 4}}show{{end}}\" aria-labelledby=\"accordion_header_branding\" data-bs-parent=\"#accordion_configs\">\n                    <div class=\"accordion-body\">\n                        {{- template \"infomsg\" \"branding.help\"}}\n                        <form id=\"configs_branding_form\" enctype=\"multipart/form-data\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n                            <div class=\"card\">\n                                <div class=\"card-header bg-light\">\n                                    <h3 class=\"card-title section-title-inner\">WebAdmin</h3>\n                                </div>\n                                <div class=\"card-body\">\n                                    <div class=\"form-group row\">\n                                        <label for=\"idBrandingWebAdminName\" data-i18n=\"general.name\" class=\"col-md-3 col-form-label\">Name</label>\n                                        <div class=\"col-md-9\">\n                                            <input id=\"idBrandingWebAdminName\" type=\"text\" placeholder=\"SFTPGo WebAdmin\" name=\"branding_webadmin_name\" value=\"{{.Configs.Branding.WebAdmin.Name}}\" maxlength=\"255\"\n                                                class=\"form-control\" />\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebAdminShortName\" data-i18n=\"branding.short_name\" class=\"col-md-3 col-form-label\">Short Name</label>\n                                        <div class=\"col-md-9\">\n                                            <input id=\"idBrandingWebAdminShortName\" type=\"text\" placeholder=\"WebAdmin\" name=\"branding_webadmin_short_name\" value=\"{{.Configs.Branding.WebAdmin.ShortName}}\" maxlength=\"255\"\n                                                class=\"form-control\" />\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebAdminLogo\" data-i18n=\"branding.logo\" class=\"col-md-3 col-form-label\">Logo</label>\n                                        <div class=\"col-md-9\">\n                                            <div class=\"image-input image-input-outline {{if eq .Branding.DefaultLogoPath .Branding.LogoPath}}image-input-empty{{end}} image-input-placeholder\" data-kt-image-input=\"true\">\n                                                <div class=\"image-input-wrapper w-125px h-125px image-input-webadmin-current\"></div>\n\n                                                <label class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"change\">\n                                                    <i class=\"ki-duotone ki-pencil fs-6\"><span class=\"path1\"></span><span class=\"path2\"></span></i>\n                                                    <input type=\"file\" id=\"idBrandingWebAdminLogo\" name=\"branding_webadmin_logo\" accept=\".png\" aria-describedby=\"idBrandingWebAdminLogoHelp\"/>\n                                                    <input type=\"hidden\"name=\"branding_webadmin_logo_remove\" />\n                                                </label>\n\n                                                <span class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"cancel\">\n                                                    <i class=\"ki-outline ki-cross fs-3\"></i>\n                                                </span>\n\n                                                <span class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"remove\">\n                                                    <i class=\"ki-outline ki-cross fs-3\"></i>\n                                                </span>\n                                            </div>\n                                            <div id=\"idBrandingWebAdminLogoHelp\" class=\"form-text mt-3\" data-i18n=\"branding.logo_help\"></div>\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebAdminFavicon\" data-i18n=\"branding.favicon\" class=\"col-md-3 col-form-label\">Favicon</label>\n                                        <div class=\"col-md-9\">\n                                            <div class=\"image-input image-input-outline {{if eq .Branding.DefaultFaviconPath .Branding.FaviconPath}}image-input-empty{{end}} image-input-placeholder\" data-kt-image-input=\"true\">\n                                                <div class=\"image-input-wrapper w-50px h-50px image-input-webadmin-fav-current\"></div>\n\n                                                <label class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"change\">\n                                                    <i class=\"ki-duotone ki-pencil fs-6\"><span class=\"path1\"></span><span class=\"path2\"></span></i>\n                                                    <input type=\"file\" id=\"idBrandingWebAdminFavicon\" name=\"branding_webadmin_favicon\" accept=\".png\" />\n                                                    <input type=\"hidden\"name=\"branding_webadmin_favicon_remove\" />\n                                                </label>\n\n                                                <span class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"cancel\">\n                                                    <i class=\"ki-outline ki-cross fs-3\"></i>\n                                                </span>\n\n                                                <span class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"remove\">\n                                                    <i class=\"ki-outline ki-cross fs-3\"></i>\n                                                </span>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebAdminDisclaimerName\" data-i18n=\"branding.disclaimer_name\" class=\"col-md-3 col-form-label\">Disclaimer Name</label>\n                                        <div class=\"col-md-9\">\n                                            <input id=\"idBrandingWebAdminDisclaimerName\" type=\"text\" placeholder=\"\" name=\"branding_webadmin_disclaimer_name\" value=\"{{.Configs.Branding.WebAdmin.DisclaimerName}}\" maxlength=\"255\"\n                                                class=\"form-control\" />\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebAdminDisclaimerURL\" data-i18n=\"branding.disclaimer_url\" class=\"col-md-3 col-form-label\">Disclaimer URL</label>\n                                        <div class=\"col-md-9\">\n                                            <input id=\"idBrandingWebAdminDisclaimerURL\" type=\"text\" placeholder=\"\" name=\"branding_webadmin_disclaimer_url\" value=\"{{.Configs.Branding.WebAdmin.DisclaimerURL}}\" maxlength=\"1024\"\n                                                class=\"form-control\" />\n                                        </div>\n                                    </div>\n\n                                </div>\n                            </div>\n\n                            <div class=\"card mt-10\">\n                                <div class=\"card-header bg-light\">\n                                    <h3 class=\"card-title section-title-inner\">WebClient</h3>\n                                </div>\n                                <div class=\"card-body\">\n                                    <div class=\"form-group row\">\n                                        <label for=\"idBrandingWebClientName\" data-i18n=\"general.name\" class=\"col-md-3 col-form-label\">Name</label>\n                                        <div class=\"col-md-9\">\n                                            <input id=\"idBrandingWebClientName\" type=\"text\" placeholder=\"SFTPGo WebClient\" name=\"branding_webclient_name\" value=\"{{.Configs.Branding.WebClient.Name}}\" maxlength=\"255\"\n                                                class=\"form-control\" />\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebClientShortName\" data-i18n=\"branding.short_name\" class=\"col-md-3 col-form-label\">Short Name</label>\n                                        <div class=\"col-md-9\">\n                                            <input id=\"idBrandingWebClientShortName\" type=\"text\" placeholder=\"WebClient\" name=\"branding_webclient_short_name\" value=\"{{.Configs.Branding.WebClient.ShortName}}\" maxlength=\"255\"\n                                                class=\"form-control\" />\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebClientLogo\" data-i18n=\"branding.logo\" class=\"col-md-3 col-form-label\">Logo</label>\n                                        <div class=\"col-md-9\">\n                                            <div class=\"image-input image-input-outline {{if eq .Branding.DefaultLogoPath .WebClientBranding.LogoPath}}image-input-empty{{end}} image-input-placeholder\" data-kt-image-input=\"true\">\n                                                <div class=\"image-input-wrapper w-125px h-125px image-input-webclient-current\"></div>\n\n                                                <label class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"change\">\n                                                    <i class=\"ki-duotone ki-pencil fs-6\"><span class=\"path1\"></span><span class=\"path2\"></span></i>\n                                                    <input type=\"file\" id=\"idBrandingWebClientLogo\" name=\"branding_webclient_logo\" accept=\".png\" aria-describedby=\"idBrandingWebClientLogoHelp\"/>\n                                                    <input type=\"hidden\"name=\"branding_webclient_logo_remove\" />\n                                                </label>\n\n                                                <span class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"cancel\">\n                                                    <i class=\"ki-outline ki-cross fs-3\"></i>\n                                                </span>\n\n                                                <span class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"remove\">\n                                                    <i class=\"ki-outline ki-cross fs-3\"></i>\n                                                </span>\n                                            </div>\n                                            <div id=\"idBrandingWebClientLogoHelp\" class=\"form-text mt-3\" data-i18n=\"branding.logo_help\"></div>\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebClientFavicon\" data-i18n=\"branding.favicon\" class=\"col-md-3 col-form-label\">Favicon</label>\n                                        <div class=\"col-md-9\">\n                                            <div class=\"image-input image-input-outline {{if eq .Branding.DefaultFaviconPath .WebClientBranding.FaviconPath}}image-input-empty{{end}} image-input-placeholder\" data-kt-image-input=\"true\">\n                                                <div class=\"image-input-wrapper w-50px h-50px image-input-webclient-fav-current\"></div>\n\n                                                <label class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"change\">\n                                                    <i class=\"ki-duotone ki-pencil fs-6\"><span class=\"path1\"></span><span class=\"path2\"></span></i>\n                                                    <input type=\"file\" id=\"idBrandingWebClientFavicon\" name=\"branding_webclient_favicon\" accept=\".png\" />\n                                                    <input type=\"hidden\"name=\"branding_webclient_favicon_remove\" />\n                                                </label>\n\n                                                <span class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"cancel\">\n                                                    <i class=\"ki-outline ki-cross fs-3\"></i>\n                                                </span>\n\n                                                <span class=\"btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow\"\n                                                    data-kt-image-input-action=\"remove\">\n                                                    <i class=\"ki-outline ki-cross fs-3\"></i>\n                                                </span>\n                                            </div>\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebClientDisclaimerName\" data-i18n=\"branding.disclaimer_name\" class=\"col-md-3 col-form-label\">Disclaimer Name</label>\n                                        <div class=\"col-md-9\">\n                                            <input id=\"idBrandingWebClientDisclaimerName\" type=\"text\" placeholder=\"\" name=\"branding_webclient_disclaimer_name\" value=\"{{.Configs.Branding.WebClient.DisclaimerName}}\" maxlength=\"255\"\n                                                class=\"form-control\" />\n                                        </div>\n                                    </div>\n\n                                    <div class=\"form-group row mt-10\">\n                                        <label for=\"idBrandingWebClientDisclaimerURL\" data-i18n=\"branding.disclaimer_url\" class=\"col-md-3 col-form-label\">Disclaimer URL</label>\n                                        <div class=\"col-md-9\">\n                                            <input id=\"idBrandingWebClientDisclaimerURL\" type=\"text\" placeholder=\"\" name=\"branding_webclient_disclaimer_url\" value=\"{{.Configs.Branding.WebClient.DisclaimerURL}}\" maxlength=\"1024\"\n                                                class=\"form-control\" />\n                                        </div>\n                                    </div>\n\n                                </div>\n                            </div>\n\n                            <div class=\"d-flex justify-content-end mt-12\">\n                                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                                <input type=\"hidden\" name=\"form_action\" value=\"branding_submit\">\n                                <button type=\"submit\" id=\"branding_form_submit\" class=\"btn btn-primary px-10\">\n                                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                                        Submit\n                                    </span>\n                                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                                        Please wait...\n                                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                                    </span>\n                                </button>\n                            </div>\n\n                        </form>\n                    </div>\n                </div>\n            </div>\n\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function onSMTPOAuth2ProviderChanged(val){\n        if (val == '1'){\n            $('.smtp-oauth2-microsoft').show();\n            return;\n        }\n        $('.smtp-oauth2-microsoft').hide();\n    }\n\n    function onSMTPAuthChanged(val){\n        if (val == '3'){\n            $('.smtp-oauth2').show();\n            onSMTPOAuth2ProviderChanged($('#idSMTPOAuth2Provider').val());\n            return;\n        }\n        $('.smtp-oauth2').hide();\n    }\n\n    function getRefreshToken() {\n        let clientID = $('#idSMTPOauth2ClientID').val();\n        let clientSecret = $('#idSMTPOAuth2ClientSecret').val();\n        if (!clientID){\n            ModalAlert.fire({\n                text: $.t('oauth2.client_id_required'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n        if (!clientSecret){\n            ModalAlert.fire({\n                text: $.t('oauth2.client_secret_required'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n\n        clearLoading();\n        KTApp.showPageLoading();\n\n        let data = {\"base_redirect_url\": getCurrentURI(), \"provider\": parseInt($('#idSMTPOAuth2Provider').val()),\n            \"tenant\": $('#idSMTPOauth2Tenant').val(), \"client_id\": clientID,\n            \"client_secret\": clientSecret};\n\n        axios.post(\"{{.OAuth2TokenURL}}\", data, {\n            timeout: 15000,\n            headers: {\n                'X-CSRF-TOKEN': '{{.CSRFToken}}'\n            },\n            validateStatus: function (status) {\n                return status == 200;\n            }\n        }).then(function (response){\n            KTApp.hidePageLoading();\n            if (response.data && response.data.message){\n                ModalAlert.fire({\n                    text: $.t('smtp.oauth2_question'),\n                    icon: \"success\",\n                    confirmButtonText: $.t('general.confirm'),\n                    cancelButtonText: $.t('general.cancel'),\n                    customClass: {\n                        confirmButton: \"btn btn-danger\",\n                        cancelButton: 'btn btn-secondary'\n                    }\n                }).then((result) => {\n                    if (result.isConfirmed){\n                        window.open(response.data.message,'_blank');\n                    }\n                });\n            } else {\n                ModalAlert.fire({\n                    text: $.t(\"smtp.oauth2_flow_error\"),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            }\n        }).catch(function (error){\n            KTApp.hidePageLoading();\n            ModalAlert.fire({\n                text: $.t(\"smtp.oauth2_flow_error\"),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n        });\n    }\n\n    function testSMTP() {\n        let recipient = $('#idSMTPRecipient').val();\n        if (!recipient){\n            ModalAlert.fire({\n                text: $.t('smtp.recipient_required'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n\n        let authType = parseInt($('#idSMTPAuth').val());\n        let clientID = $('#idSMTPOauth2ClientID').val();\n        let clientSecret = $('#idSMTPOAuth2ClientSecret').val();\n        let refreshToken = $('#idSMTPOAuth2RefreshToken').val();\n        if (authType === 3){\n            let message;\n            if (!clientID){\n                message = \"oauth2.client_id_required\";\n            }\n            if (!clientSecret){\n                message = \"oauth2.client_secret_required\";\n            }\n            if (!refreshToken){\n                message = \"oauth2.refresh_token_required\"\n            }\n            if (message){\n                ModalAlert.fire({\n                    text: $.t(message),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n                return;\n            }\n        }\n\n        clearLoading();\n        KTApp.showPageLoading();\n        let debug = 0;\n        if ($('#idSMTPDebug').is(':checked')){\n            debug = 1;\n        }\n\n        let data = {\"host\": $('#idSMTPHost').val(),\"port\": parseInt($('#idSMTPPort').val()),\n            \"from\": $('#idSMTPFrom').val(),\"user\": $('#idSMTPUsername').val(),\"password\": $('#idSMTPPassword').val(),\n            \"auth_type\": authType,\"encryption\": parseInt($('#idSMTPEncryption').val()),\n            \"domain\": $('#idSMTPDomain').val(),\"debug\": debug, \"oauth2\": {\"provider\": parseInt($('#idSMTPOAuth2Provider').val()),\n            \"tenant\": $('#idSMTPOauth2Tenant').val(), \"client_id\": clientID,\n            \"client_secret\": clientSecret, \"refresh_token\": refreshToken},\n            \"recipient\": recipient};\n\n        axios.post(\"{{.ConfigsURL}}/smtp/test\", data, {\n            timeout: 15000,\n            headers: {\n                'X-CSRF-TOKEN': '{{.CSRFToken}}'\n            },\n            validateStatus: function (status) {\n                return status == 200;\n            }\n        }).then(function (response){\n            KTApp.hidePageLoading();\n            ModalAlert.fire({\n                text: $.t('smtp.test_ok'),\n                icon: \"success\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: 'btn btn-primary'\n                }\n            });\n        }).catch(function (error){\n            KTApp.hidePageLoading();\n            ModalAlert.fire({\n                text: $.t('smtp.test_error'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n        });\n    }\n\n    $(document).on(\"i18nload\", function(){\n        $('#idSMTPAuth').on(\"change\", function(){\n            onSMTPAuthChanged(this.value);\n        });\n\n        $('#idSMTPOAuth2Provider').on(\"change\", function(){\n            onSMTPOAuth2ProviderChanged(this.value);\n        });\n\n        $('#refresh_token_button').on(\"click\", function(e){\n            e.preventDefault();\n            this.blur();\n            getRefreshToken();\n        });\n\n        $('#smtp_test_button').on(\"click\", function(e){\n            e.preventDefault();\n            this.blur();\n            testSMTP();\n        });\n\n        let currentURI = getCurrentURI();\n        let providerHelpText = $.t('smtp.oauth2_provider_help') + \" \\\"\"+currentURI+\"{{.OAuth2RedirectURL}}\\\"\";\n        $('#idSMTPOAuth2ProviderHelp').text(providerHelpText);\n        onSMTPAuthChanged('{{.Configs.SMTP.AuthType}}');\n        onSMTPOAuth2ProviderChanged('{{.Configs.SMTP.OAuth2.Provider}}');\n    });\n\n    $(document).on(\"i18nload\", function(){\n        $('#configs_sftp_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#sftp_form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n\n        $('#configs_acme_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#acme_form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n\n        $('#configs_smtp_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#smtp_form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n\n        $('#configs_branding_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#branding_form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/connections.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"connections.view_manage\" class=\"card-title section-title\">View and manage connections</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                </div>\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <a href=\"{{.ConnectionsURL}}\" class=\"btn btn-primary\">\n                        <i class=\"ki-solid ki-arrows-circle fs-2\"></i>\n                        <span data-i18n=\"general.refresh\">Refresh</span>\n                    </a>\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th>ID</th>\n                        <th>Node</th>\n                        <th data-i18n=\"login.username\">Username</th>\n                        <th data-i18n=\"connections.started\">Started</th>\n                        <th data-i18n=\"connections.remote_address\">Remote address</th>\n                        <th data-i18n=\"general.protocol\">Protocol</th>\n                        <th data-i18n=\"connections.last_activity\">Last activity</th>\n                        <th data-i18n=\"general.info\">Info</th>\n                        <th></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    function disconnectAction(connectionID, node) {\n        ModalAlert.fire({\n            text: $.t('connections.disconnect_confirm'),\n            icon: \"warning\",\n            confirmButtonText: $.t('connections.disconnect_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.ConnectionsURL}}' + \"/\" + encodeURIComponent(connectionID);\n                if (node) {\n                    path+=\"?node=\"+ encodeURIComponent(node);\n                }\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    setTimeout(function() {\n                        location.reload();\n                    },250);\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    ModalAlert.fire({\n                        text: $.t('connections.disconnect_ko'),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.ConnectionsURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"connection_id\",\n                        visible: false,\n                        searchable: false,\n                        orderable: false,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"node\",\n                        visible: false,\n                        searchable: false,\n                        orderable: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"username\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"connection_time\",\n                        searchable: false,\n                        defaultContent: 0,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data > 0){\n                                    return $.t('general.datetime', {\n                                        val: parseInt(data, 10),\n                                        formatParams: {\n                                            val: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },\n                                        }\n                                    });\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"remote_address\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"protocol\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"last_activity\",\n                        searchable: false,\n                        defaultContent: 0,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data > 0){\n                                    return $.t('general.datetime', {\n                                        val: parseInt(data, 10),\n                                        formatParams: {\n                                            val: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },\n                                        }\n                                    });\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"active_transfers\",\n                        searchable: false,\n                        orderable: false,\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let result = \"\";\n                                if (row.active_transfers && row.active_transfers.length > 0) {\n                                    for (let i = 0; i < row.active_transfers.length; i++) {\n                                        if (i > 0){\n                                            result+=\". \";\n                                        }\n                                        let transfer = row.active_transfers[i];\n                                        let path = escapeHTML(transfer.path);\n                                        let elapsed = row.current_time - transfer.start_time;\n                                        if (elapsed > 0 && transfer.size > 0) {\n                                            let speed = (transfer.size * 1.0) / (elapsed / 1000.0);\n                                            if (transfer.operation_type === 'upload') {\n                                                result += $.t('connections.upload_info', { path: path, size: fileSizeIEC(transfer.size), speed: humanizeSpeed(speed) });\n                                            } else {\n                                                result += $.t('connections.download_info', { path: path, size: fileSizeIEC(transfer.size), speed: humanizeSpeed(speed) });\n                                            }\n                                        } else {\n                                            if (transfer.operation_type === 'upload') {\n                                                result += $.t('connections.upload', { path: path });\n                                            } else {\n                                                result += $.t('connections.download', { path: path });\n                                            }\n                                        }\n                                    }\n                                }\n                                if (row.client_version){\n                                    if (result){\n                                        result+= \". \";\n                                    }\n                                    result+= $.t('connections.client', {val: escapeHTML(row.client_version)});\n                                }\n                                return result;\n                            }\n                            return \"\";\n                        }\n                    },\n                    {\n                        data: \"\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                //{{- if .LoggedUser.HasPermission \"close_conns\"}}\n                                return `<div class=\"d-flex justify-content-end\">\n                                            <div class=\"ms-2\">\n                                                <a href=\"#\" class=\"btn btn-sm btn-icon btn-light-danger\" data-table-action=\"close_conn\">\n                                                    <i class=\"ki-solid ki-cross fs-1\"></i>\n                                                </a>\n                                            </div>\n                                    </div>`;\n                                //{{- end}}\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                colReorder: {\n                    enable: true\n                },\n                stateLoadParams: function (settings, data) {\n                        if (data.search.search){\n                            const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                            filterSearch.value = data.search.search;\n                        }\n                    },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n            dt.on('columns-reordered', function(e, settings, details){\n                drawAction();\n            });\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n        }\n\n        function handleRowActions() {\n            const closeButtons = document.querySelectorAll('[data-table-action=\"close_conn\"]');\n            closeButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    let data = dt.row(parent).data();\n                    disconnectAction(data.connection_id, data.node);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/defender.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"defender.view_manage\" class=\"card-title section-title\">View and manage auto blocklis</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                </div>\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <a href=\"{{.DefenderURL}}\" class=\"btn btn-primary\">\n                        <i class=\"ki-solid ki-arrows-circle fs-2\"></i>\n                        <span data-i18n=\"general.refresh\">Refresh</span>\n                    </a>\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"defender.ip\">IP</th>\n                        <th data-i18n=\"defender.ban_time\">Blocked until</th>\n                        <th data-i18n=\"defender.scopre\">Score</th>\n                        <th></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    function deleteAction(id) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.DefenderHostsURL}}' + \"/\" + encodeURIComponent(id);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.DefenderHostsURL}}\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"ip\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"ban_time\",\n                        searchable: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    let parsed = Date.parse(data);\n                                    return $.t('general.datetime', {\n                                        val: parseInt(parsed, 10),\n                                        formatParams: {\n                                            val: { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },\n                                        }\n                                    });\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"score\",\n                        defaultContent: 0,\n                        render: function(data, type, row) {\n                            if (data){\n                                return data;\n                            }\n                            return \"\";\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                //{{- if .LoggedUser.HasPermission \"manage_defender\"}}\n                                return `<div class=\"d-flex justify-content-end\">\n                                            <div class=\"ms-2\">\n                                                <a href=\"#\" class=\"btn btn-sm btn-icon btn-light-danger\" data-table-action=\"delete_row\">\n                                                    <i class=\"ki-solid ki-cross fs-1\"></i>\n                                                </a>\n                                            </div>\n                                    </div>`;\n                                //{{- end}}\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                stateLoadParams: function (settings, data) {\n                        if (data.search.search){\n                            const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                            filterSearch.value = data.search.search;\n                        }\n                    },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n        }\n\n        function handleRowActions() {\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    deleteAction(dt.row(parent).data().id);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/eventaction.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"eventaction_form\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n\n            <div class=\"form-group row\">\n                <label for=\"idName\" data-i18n=\"general.name\" class=\"col-md-3 col-form-label\">Name</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idName\" type=\"text\" placeholder=\"\" name=\"name\" value=\"{{.Action.Name}}\" maxlength=\"255\" autocomplete=\"off\"\n                        spellcheck=\"false\" required {{if eq .Mode 2}}class=\"form-control-plaintext readonly-input\" readonly{{else}}class=\"form-control\"{{end}} />\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idDescription\" type=\"text\" class=\"form-control\" name=\"description\" value=\"{{.Action.Description}}\" maxlength=\"255\">\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idType\" data-i18n=\"general.type\" class=\"col-md-3 col-form-label\">Type</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idType\" name=\"type\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        {{- range .ActionTypes}}\n                        {{- if eq .Value 2}}\n                            {{- if not $.EnabledCommands}}\n                                {{- continue}}\n                            {{- end}}\n                        {{- end}}\n                        <option value=\"{{.Value}}\" {{if eq $.Action.Type .Value }}selected{{end}} data-i18n=\"{{.Name}}\"></option>\n                        {{- end}}\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-pwd-expiration mt-10\">\n                <label for=\"idPwdExpirationThreshold\" data-i18n=\"actions.threshold\" class=\"col-md-3 col-form-label\">Threshold</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idPwdExpirationThreshold\" type=\"number\" min=\"1\" class=\"form-control\" name=\"pwd_expiration_threshold\" value=\"{{.Action.Options.PwdExpirationConfig.Threshold}}\" aria-describedby=\"idPwdExpirationThresholdHelp\" />\n                    <div id=\"idPwdExpirationThresholdHelp\" class=\"form-text\" data-i18n=\"actions.threshold_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-user-inactivity mt-10\">\n                <label for=\"idInactivityThresholdDisable\" data-i18n=\"actions.disable_threshold\" class=\"col-md-3 col-form-label\">Disable Threshold</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idInactivityThresholdDisable\" type=\"number\" min=\"0\" class=\"form-control\" name=\"inactivity_disable_threshold\" value=\"{{.Action.Options.UserInactivityConfig.DisableThreshold}}\" aria-describedby=\"idInactivityThresholdDisableHelp\" />\n                    <div id=\"idInactivityThresholdDisableHelp\" class=\"form-text\" data-i18n=\"actions.disable_threshold_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-user-inactivity mt-10\">\n                <label for=\"idInactivityThresholdDelete\" data-i18n=\"actions.delete_threshold\" class=\"col-md-3 col-form-label\">Delete Threshold</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idInactivityThresholdDelete\" type=\"number\" min=\"0\" class=\"form-control\" name=\"inactivity_delete_threshold\" value=\"{{.Action.Options.UserInactivityConfig.DeleteThreshold}}\" aria-describedby=\"idInactivityThresholdDeleteHelp\" />\n                    <div id=\"idInactivityThresholdDeleteHelp\" class=\"form-text\" data-i18n=\"actions.delete_threshold_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group action-type action-idp row mt-10\">\n                <label for=\"idIDPMode\" data-i18n=\"general.mode\" class=\"col-md-3 col-form-label\">Mode</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idIDPMode\" name=\"idp_mode\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        <option value=\"0\" data-i18n=\"actions.idp_mode_add_update\" {{ if eq .Action.Options.IDPConfig.Mode 0 }}selected{{end}}>Create or update</option>\n                        <option value=\"1\" data-i18n=\"actions.idp_mode_add\" {{ if eq .Action.Options.IDPConfig.Mode 1 }}selected{{end}}>Create if it doesn't exist</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-idp mt-10\">\n                <label for=\"idIDPUser\" data-i18n=\"title.template_user\" class=\"col-md-3 col-form-label\">User template</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idIDPUser\" name=\"idp_user\" aria-describedby=\"idIDPUserHelp\"\n                        rows=\"4\">{{.Action.Options.IDPConfig.TemplateUser}}</textarea>\n                    <div id=\"idIDPUserHelp\" class=\"form-text\" data-i18n=\"actions.template_user_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-idp mt-10\">\n                <label for=\"idIDPAdmin\" data-i18n=\"title.template_admin\" class=\"col-md-3 col-form-label\">Admin template</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idIDPAdmin\" name=\"idp_admin\" aria-describedby=\"idIDAdminHelp\"\n                        rows=\"4\">{{.Action.Options.IDPConfig.TemplateAdmin}}</textarea>\n                    <div id=\"idIDAdminHelp\" class=\"form-text\" data-i18n=\"actions.template_admin_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-http mt-10\">\n                <label for=\"idHTTPEndpoint\" data-i18n=\"actions.http_url\" class=\"col-md-3 col-form-label\">Endpoint</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idHTTPEndpoint\" type=\"text\" class=\"form-control\" name=\"http_endpoint\" value=\"{{.Action.Options.HTTPConfig.Endpoint}}\" aria-describedby=\"idHTTPEndpointHelp\" />\n                    <div id=\"idHTTPEndpointHelp\" class=\"form-text\" data-i18n=\"actions.http_url_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-http mt-10\">\n                <label for=\"idHTTPUsername\" data-i18n=\"login.username\" class=\"col-md-3 col-form-label\">Username</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idHTTPUsername\" type=\"text\" class=\"form-control\" name=\"http_username\" value=\"{{.Action.Options.HTTPConfig.Username}}\" aria-describedby=\"idHTTPUsernameHelp\" autocomplete=\"off\" />\n                    <div id=\"idHTTPUsernameHelp\" class=\"form-text\" data-i18n=\"actions.placeholders_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-http mt-10\">\n                <label for=\"idHTTPPassword\" data-i18n=\"login.password\" class=\"col-md-3 col-form-label\">Password</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idHTTPPassword\" type=\"password\" class=\"form-control\" name=\"http_password\" autocomplete=\"new-password\"\n                        spellcheck=\"false\" value=\"{{if .Action.Options.HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Action.Options.HTTPConfig.Password.GetPayload}}{{end}}\" />\n                </div>\n            </div>\n\n            <div class=\"card action-type action-http mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"actions.http_headers\" class=\"card-title section-title-inner\">HTTP headers</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"http_headers\">\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"http_headers\">\n                                {{- range $idx, $val := .Action.Options.HTTPConfig.Headers}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.name\" type=\"text\" class=\"form-control\" name=\"http_header_key\" value=\"{{$val.Key}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.value\" type=\"text\" class=\"form-control\" name=\"http_header_value\" value=\"{{$val.Value}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.name\" type=\"text\" class=\"form-control\" name=\"http_header_key\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.value\" type=\"text\" class=\"form-control\" name=\"http_header_value\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card action-type action-http mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"actions.query_parameters\" class=\"card-title section-title-inner\">Query parameters</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"query_parameters\">\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"query_parameters\">\n                                {{- range $idx, $val := .Action.Options.HTTPConfig.QueryParameters}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.name\" type=\"text\" class=\"form-control\" name=\"http_query_key\" value=\"{{$val.Key}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.value\" type=\"text\" class=\"form-control\" name=\"http_query_value\" value=\"{{$val.Value}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.name\" type=\"text\" class=\"form-control\" name=\"http_query_key\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.value\" type=\"text\" class=\"form-control\" name=\"http_query_value\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group action-type action-http row mt-10\">\n                <label for=\"idHTTPMethod\" data-i18n=\"general.method\" class=\"col-md-3 col-form-label\">Method</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idHTTPMethod\" name=\"http_method\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        {{- range .HTTPMethods}}\n                        <option value=\"{{.}}\" {{if eq $.Action.Options.HTTPConfig.Method . }}selected{{end}}>{{.}}</option>\n                        {{- end}}\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-http mt-10\">\n                <label for=\"idHTTPTimeout\" data-i18n=\"general.timeout\" class=\"col-md-3 col-form-label\">Timeout</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idHTTPTimeout\" type=\"number\" min=\"1\" max=\"180\" class=\"form-control\" name=\"http_timeout\" value=\"{{.Action.Options.HTTPConfig.Timeout}}\" aria-describedby=\"idHTTPTimeoutHelp\" />\n                    <div id=\"idHTTPTimeoutHelp\" class=\"form-text\" data-i18n=\"actions.http_timeout_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row align-items-center action-type action-http mt-10\">\n                <div class=\"col-md-12\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idHTTPSkipTLSVerify\" name=\"http_skip_tls_verify\" {{if .Action.Options.HTTPConfig.SkipTLSVerify}}checked{{end}}/>\n                        <label data-i18n=\"general.skip_tls_verify\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idHTTPSkipTLSVerify\">\n                            Skip TLS verify\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-http mt-10\">\n                <label for=\"idHTTPBody\" data-i18n=\"actions.body\" class=\"col-md-3 col-form-label\">Body</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idHTTPBody\" name=\"http_body\" aria-describedby=\"idHTTPBodyHelp\"\n                        rows=\"4\">{{.Action.Options.HTTPConfig.Body}}</textarea>\n                    <div id=\"idHTTPBodyHelp\" class=\"form-text\" data-i18n=\"actions.http_body_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"card action-type action-http mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"actions.multipart_body\" class=\"card-title section-title-inner\">Multipart body</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"multipart_body\">\n                        {{- template \"infomsg-no-mb\" \"actions.multipart_body_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"multipart_body\">\n                                {{- range $idx, $val := .Action.Options.HTTPConfig.Parts}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]actions.http_part_name\" type=\"text\" class=\"form-control\" name=\"http_part_name\" value=\"{{$val.Name}}\" />\n                                            </div>\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]actions.http_part_file\" type=\"text\" class=\"form-control\" name=\"http_part_file\" value=\"{{$val.Filepath}}\" />\n                                            </div>\n                                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                <textarea class=\"form-control\" name=\"http_part_headers\" data-i18n=\"[placeholder]actions.http_part_headers\"\n                                                    rows=\"2\">{{range $val.Headers}}{{.Key}}: {{.Value}}&#010;{{end}}</textarea>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-11 mt-3 mt-md-8\">\n                                                <textarea class=\"form-control\" name=\"http_part_body\" data-i18n=\"[placeholder]actions.body\"\n                                                    rows=\"3\">{{$val.Body}}</textarea>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]actions.http_part_name\" type=\"text\" class=\"form-control\" name=\"http_part_name\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]actions.http_part_file\" type=\"text\" class=\"form-control\" name=\"http_part_file\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <textarea class=\"form-control\" name=\"http_part_headers\" data-i18n=\"[placeholder]actions.http_part_headers\"\n                                                rows=\"2\"></textarea>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-11 mt-3 mt-md-8\">\n                                            <textarea class=\"form-control\" name=\"http_part_body\" data-i18n=\"[placeholder]actions.body\"\n                                                rows=\"3\"></textarea>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            {{ if .EnabledCommands}}\n            <div class=\"form-group row action-type action-cmd mt-10\">\n                <label for=\"idCmdPath\" data-i18n=\"actions.types.command\" class=\"col-md-3 col-form-label\">Command</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idCmdPath\" name=\"cmd_path\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"false\">\n                        {{- range .EnabledCommands}}\n                        <option value=\"{{.}}\" {{if eq $.Action.Options.CmdConfig.Cmd . }}selected{{end}}>{{.}}</option>\n                        {{- end}}\n                    </select>\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"form-group row action-type action-cmd mt-10\">\n                <label for=\"idCommandArgs\" data-i18n=\"actions.command_args\" class=\"col-md-3 col-form-label\">Arguments</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idCommandArgs\" name=\"cmd_arguments\" aria-describedby=\"idCommandArgsHelp\"\n                        rows=\"2\">{{.Action.Options.CmdConfig.GetArgumentsAsString}}</textarea>\n                    <div id=\"idCommandArgsHelp\" class=\"form-text\" data-i18n=\"actions.command_args_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-cmd mt-10\">\n                <label for=\"idCmdTimeout\" data-i18n=\"general.timeout\" class=\"col-md-3 col-form-label\">Timeout</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idCmdTimeout\" type=\"number\" min=\"1\" max=\"120\" class=\"form-control\" name=\"cmd_timeout\"\n                        value=\"{{.Action.Options.CmdConfig.Timeout}}\" />\n                </div>\n            </div>\n\n            <div class=\"card action-type action-cmd mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"general.env_vars\" class=\"card-title section-title-inner\">Environment variables</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"env_vars\">\n                        {{- template \"infomsg-no-mb\" \"actions.command_env_vars_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"env_vars\">\n                                {{- range $idx, $val := .Action.Options.CmdConfig.EnvVars}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.name\" type=\"text\" class=\"form-control\" name=\"cmd_env_key\" value=\"{{$val.Key}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.value\" type=\"text\" class=\"form-control\" name=\"cmd_env_value\" value=\"{{$val.Value}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.name\" type=\"text\" class=\"form-control\" name=\"cmd_env_key\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.value\" type=\"text\" class=\"form-control\" name=\"cmd_env_value\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-smtp mt-10\">\n                <label for=\"idEmailRecipients\" data-i18n=\"actions.email_recipients\" class=\"col-md-3 col-form-label\">To</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idEmailRecipients\" name=\"email_recipients\" aria-describedby=\"idEmailRecipientsHelp\"\n                        rows=\"2\">{{.Action.Options.EmailConfig.GetRecipientsAsString}}</textarea>\n                    <div id=\"idEmailRecipientsHelp\" class=\"form-text\" data-i18n=\"actions.email_recipients_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-smtp mt-10\">\n                <label for=\"idEmailBcc\" data-i18n=\"actions.email_bcc\" class=\"col-md-3 col-form-label\">Bcc</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idEmailBcc\" name=\"email_bcc\" aria-describedby=\"idEmailBccHelp\"\n                        rows=\"2\">{{.Action.Options.EmailConfig.GetBccAsString}}</textarea>\n                    <div id=\"idEmailBccHelp\" class=\"form-text\" data-i18n=\"actions.email_bcc_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-smtp mt-10\">\n                <label for=\"idEmailSubject\" data-i18n=\"actions.email_subject\" class=\"col-md-3 col-form-label\">Subject</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idEmailSubject\" type=\"text\" class=\"form-control\" name=\"email_subject\" maxlength=\"255\" value=\"{{.Action.Options.EmailConfig.Subject}}\" aria-describedby=\"idEmailSubjectHelp\" />\n                    <div id=\"idEmailSubjectHelp\" class=\"form-text\" data-i18n=\"actions.placeholders_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-smtp mt-10\">\n                <label for=\"idEmailContentType\" data-i18n=\"actions.content_type\" class=\"col-md-3 col-form-label\">Content Type</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idEmailContentType\" name=\"email_content_type\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        <option value=\"0\" {{ if eq .Action.Options.EmailConfig.ContentType 0 }}selected{{end}}>Text/plain</option>\n                        <option value=\"1\" {{ if eq .Action.Options.EmailConfig.ContentType 1 }}selected{{end}}>Text/html</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-smtp mt-10\">\n                <label for=\"idEmailBody\" data-i18n=\"actions.body\" class=\"col-md-3 col-form-label\">Body</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idEmailBody\" name=\"email_body\" aria-describedby=\"idEmailBodyHelp\"\n                        rows=\"4\">{{.Action.Options.EmailConfig.Body}}</textarea>\n                    <div id=\"idEmailBodyHelp\" class=\"form-text\" data-i18n=\"actions.placeholders_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-smtp mt-10\">\n                <label for=\"idEmailAttachments\" data-i18n=\"actions.attachments\" class=\"col-md-3 col-form-label\">Attachments</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idEmailAttachments\" name=\"email_attachments\" aria-describedby=\"idEmailAttachmentsHelp\"\n                        rows=\"2\">{{.Action.Options.EmailConfig.GetAttachmentsAsString}}</textarea>\n                    <div id=\"idEmailAttachmentsHelp\" class=\"form-text\" data-i18n=\"actions.attachments_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"card action-type action-dataretention mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"actions.data_retention\" class=\"card-title section-title-inner\">Data retention</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"data_retention\">\n                        {{- template \"infomsg-no-mb\" \"actions.data_retention_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"data_retention\">\n                                {{- range $idx, $val := .Action.Options.RetentionConfig.Folders}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]events.path\" type=\"text\" class=\"form-control\" name=\"folder_retention_path\" value=\"{{$val.Path}}\" />\n                                            </div>\n                                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.hours\" type=\"text\" class=\"form-control\" name=\"folder_retention_val\" value=\"{{$val.Retention}}\" />\n                                            </div>\n                                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                                <select name=\"folder_retention_options\" data-i18n=\"[data-placeholder]general.options\" class=\"form-select select-repetear\" data-allow-clear=\"true\" data-close-on-select=\"false\" data-hide-search=\"true\" multiple>\n                                                    <option value=\"\"></option>\n                                                    <option value=\"1\" data-i18n=\"actions.delete_empty_dirs\" {{if $val.DeleteEmptyDirs}}selected{{end}}>Delete empty dirs</option>\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]events.path\" type=\"text\" class=\"form-control\" name=\"folder_retention_path\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-2 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.hours\" type=\"text\" class=\"form-control\" name=\"folder_retention_val\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-4 mt-3 mt-md-8\">\n                                            <select name=\"folder_retention_options\" data-i18n=\"[data-placeholder]general.options\" class=\"form-select select-repetear\" data-allow-clear=\"true\" data-close-on-select=\"false\" data-hide-search=\"true\" multiple>\n                                                <option value=\"\"></option>\n                                                <option value=\"1\" data-i18n=\"actions.delete_empty_dirs\">Delete empty dirs</option>\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-fs mt-10\">\n                <label for=\"idFsActionType\" data-i18n=\"actions.fs_action\" class=\"col-md-3 col-form-label\">Fs action</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idFsActionType\" name=\"fs_action_type\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        {{- range .FsActions}}\n                        <option value=\"{{.Value}}\" data-i18n=\"{{.Name}}\" {{if eq $.Action.Options.FsConfig.Type .Value }}selected{{end}}></option>\n                        {{- end}}\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"card action-type action-fs-type action-fs-rename mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"actions.fs_types.rename\" class=\"card-title section-title-inner\">Rename</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"fs_rename\">\n                        {{- template \"infomsg-no-mb\" \"actions.paths_src_dst_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"fs_rename\">\n                                {{- range $idx, $val := .Action.Options.FsConfig.Renames}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]actions.source_path\" type=\"text\" class=\"form-control\" name=\"fs_rename_source\" value=\"{{$val.Key}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]actions.target_path\" type=\"text\" class=\"form-control\" name=\"fs_rename_target\" value=\"{{$val.Value}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <select name=\"fs_rename_options\" data-i18n=\"[data-placeholder]general.options\" class=\"form-select select-repetear\" data-allow-clear=\"true\" data-close-on-select=\"false\" data-hide-search=\"true\" multiple>\n                                                    <option value=\"\"></option>\n                                                    <option value=\"1\" data-i18n=\"actions.update_mod_times\" {{if $val.UpdateModTime}}selected{{end}}>Change times</option>\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-4 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]actions.source_path\" type=\"text\" class=\"form-control\" name=\"fs_rename_source\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-4 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]actions.target_path\" type=\"text\" class=\"form-control\" name=\"fs_rename_target\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <select name=\"fs_rename_options\" data-i18n=\"[data-placeholder]general.options\" class=\"form-select select-repetear\" data-allow-clear=\"true\" data-close-on-select=\"false\" data-hide-search=\"true\" multiple>\n                                                <option value=\"\"></option>\n                                                <option value=\"1\" data-i18n=\"actions.update_mod_times\">Update timestamps</option>\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-fs-type action-fs-delete mt-10\">\n                <label for=\"idFsDelete\" data-i18n=\"general.paths\" class=\"col-md-3 col-form-label\">Paths</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idFsDelete\" name=\"fs_delete_paths\" aria-describedby=\"idFsDeleteHelp\"\n                        rows=\"2\">{{.Action.Options.FsConfig.GetDeletesAsString}}</textarea>\n                    <div id=\"idFsDeleteHelp\" class=\"form-text\" data-i18n=\"actions.paths_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-fs-type action-fs-mkdir mt-10\">\n                <label for=\"idFsMkdir\" data-i18n=\"general.paths\" class=\"col-md-3 col-form-label\">Paths</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idFsMkdir\" name=\"fs_mkdir_paths\" aria-describedby=\"idFsMkdirHelp\"\n                        rows=\"2\">{{.Action.Options.FsConfig.GetMkDirsAsString}}</textarea>\n                    <div id=\"idFsMkdirHelp\" class=\"form-text\" data-i18n=\"actions.paths_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-fs-type action-fs-exist mt-10\">\n                <label for=\"idFsExist\" data-i18n=\"general.paths\" class=\"col-md-3 col-form-label\">Paths</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idFsExist\" name=\"fs_exist_paths\" aria-describedby=\"idFsExistHelp\"\n                        rows=\"2\">{{.Action.Options.FsConfig.GetExistAsString}}</textarea>\n                    <div id=\"idFsExistHelp\" class=\"form-text\" data-i18n=\"actions.paths_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"card action-type action-fs-type action-fs-copy mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"actions.fs_types.copy\" class=\"card-title section-title-inner\">Copy</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"fs_copy\">\n                        {{- template \"infomsg-no-mb\" \"actions.paths_src_dst_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"fs_copy\">\n                                {{- range $idx, $val := .Action.Options.FsConfig.Copy}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]actions.source_path\" type=\"text\" class=\"form-control\" name=\"fs_copy_source\" value=\"{{$val.Key}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]actions.target_path\" type=\"text\" class=\"form-control\" name=\"fs_copy_target\" value=\"{{$val.Value}}\" spellcheck=\"false\" />\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]actions.source_path\" type=\"text\" class=\"form-control\" name=\"fs_copy_source\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]actions.target_path\" type=\"text\" class=\"form-control\" name=\"fs_copy_target\" value=\"\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-fs-type action-fs-compress mt-10\">\n                <label for=\"idFsCompressName\" data-i18n=\"actions.archive_path\" class=\"col-md-3 col-form-label\">Archive path</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idFsCompressName\" type=\"text\" class=\"form-control\" name=\"fs_compress_name\" maxlength=\"255\" value=\"{{.Action.Options.FsConfig.Compress.Name}}\" aria-describedby=\"idFsCompressNameHelp\" />\n                    <div id=\"idFsCompressNameHelp\" class=\"form-text\" data-i18n=\"actions.archive_path_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row action-type action-fs-type action-fs-compress mt-10\">\n                <label for=\"idFsCompressPaths\" data-i18n=\"general.paths\" class=\"col-md-3 col-form-label\">Paths</label>\n                <div class=\"col-md-9\">\n                    <textarea class=\"form-control\" id=\"idFsCompressPaths\" name=\"fs_compress_paths\" aria-describedby=\"idFsCompressPathsHelp\"\n                        rows=\"2\">{{.Action.Options.FsConfig.GetCompressPathsAsString}}</textarea>\n                    <div id=\"idFsCompressPathsHelp\" class=\"form-text\" data-i18n=\"actions.paths_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"additionalnavitems\"}}\n<div class=\"d-flex align-items-center ms-2 ms-lg-3\" id=\"kt_header_user_menu_toggle\">\n    <div class=\"btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px\" data-bs-toggle=\"modal\" data-bs-target=\"#info_modal\">\n        <i class=\"ki-duotone ki-information-2 fs-2\">\n            <i class=\"path1\"></i>\n            <i class=\"path2\"></i>\n            <i class=\"path3\"></i>\n        </i>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"modals\"}}\n<div class=\"modal fade\" id=\"info_modal\" tabindex=\"-1\">\n    <div class=\"modal-dialog modal-lg\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header\">\n                <h3 data-i18n=\"actions.placeholders_modal_title\" class=\"modal-title\">Supported placeholders</h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body fs-5 fw-semibold\">\n                <p>\n                    <span class=\"shortcut\">{{`{{.Name}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.name\">Username, folder name, admin username for provider events, domain name for certificate events.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Event}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.event\">Event name, for example \"upload\", \"download\" for filesystem events or \"add\", \"update\" for provider events.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Status}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.status\">Status for \"upload\", \"download\" and \"ssh_cmd\" events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.StatusString}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.status_string\">Status as string. Possible values \"OK\", \"KO\".</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.ErrorString}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.error_string\">Error details. Replaced with an empty string if no errors occur.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.VirtualPath}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.virtual_path\">Path seen by SFTPGo users, for example \"/adir/afile.txt\".</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.EscapedVirtualPath}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.escaped_virtual_path\">HTTP query string encoded path, for example \"%2Fadir%2Fafile.txt\".</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.VirtualDirPath}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.virtual_dir_path\">Parent directory for VirtualPath, for example if VirtualPath is \"/adir/afile.txt\", VirtualDirPath is \"/adir\".</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.FsPath}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.fs_path\">Full filesystem path, for example \"/user/homedir/adir/afile.txt\" or \"C:/data/user/homedir/adir/afile.txt\" on Windows.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Ext}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.ext\">File extension, for example \".txt\" if the filename is \"afile.txt\".</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.ObjectName}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.object_name\">File/directory name, for example \"afile.txt\" or provider object name.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.ObjectBaseName}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.object_basename\">Filename without extension, for example \"afile\" if the filename is \"afile.txt\".</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.ObjectType}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.object_type\">Object type for provider events: \"user\", \"group\", \"admin\", etc.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.VirtualTargetPath}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.virtual_target_path\">Virtual target path for renames.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.VirtualTargetDirPath}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.virtual_target_dir_path\">Parent directory for VirtualTargetPath.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.TargetName}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.target_name\">Target object name for renames.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.FsTargetPath}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.fs_target_path\">Full filesystem target path for renames.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.FileSize}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.file_size\">File size.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Elapsed}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.elapsed\">Elapsed time as milliseconds for filesystem events.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Protocol}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.protocol\">Protocol, for example \"SFTP\", \"FTP\".</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.IP}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.ip\">Client IP address.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Role}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.role\">User or admin role.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Timestamp}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.timestamp\">Event timestamp as nanoseconds since epoch.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.DateTime}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.datetime\">Timestamp formatted as YYYY-MM-DDTHH:MM:SS.ZZZ.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Year}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.year\">Event year formatted as four digits.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Month}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.month\">Event month formatted as two digits.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Day}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.day\">Event day formatted as two digits.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Hour}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.hour\">Event hour formatted as two digits.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Minute}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.minute\">Event minute formatted as two digits.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Email}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.email\">For filesystem events, this is the email associated with the user performing the action. For the provider events, this is the email associated with the affected user or admin. Blank in all other cases.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.ObjectData}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.object_data\">Provider object data serialized as JSON with sensitive fields removed.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.ObjectDataString}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.object_data_string\">Provider object data as JSON escaped string with sensitive fields removed.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.RetentionReports}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.retention_reports\">Data retention reports as zip compressed CSV files. Supported as email attachment, file path for multipart HTTP request and as single parameter for HTTP requests body.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.IDPField<fieldname>}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.idp_field\">Identity Provider custom fields containing a string.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.Metadata}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.metadata\">Cloud storage metadata for the downloaded file serialized as JSON.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.MetadataString}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.metadata_string\">Cloud storage metadata for the downloaded file as JSON escaped string.</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">{{`{{.UID}}`}}</span> => <span data-i18n=\"actions.placeholders_modal.uid\">Unique ID.</span>\n                </p>\n            </div>\n            <div class=\"modal-footer\">\n                <button data-i18n=\"general.ok\" class=\"btn btn-primary\" type=\"button\" data-bs-dismiss=\"modal\">OK</button>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function onTypeChanged(val){\n        $('.action-type').hide();\n        switch (val) {\n            case '1':\n                $('.action-http').show();\n                break;\n            case '2':\n                $('.action-cmd').show();\n                break;\n            case '3':\n                $('.action-smtp').show();\n                break;\n            case '8':\n                $('.action-dataretention').show();\n                break;\n            case '9':\n                $('.action-fs').show();\n                onFsActionChanged($(\"#idFsActionType\").val());\n                break;\n            case '11':\n                $('.action-pwd-expiration').show();\n                break;\n            case '13':\n                $('.action-idp').show();\n                break;\n            case '14':\n                $('.action-user-inactivity').show();\n                break;\n        }\n    }\n\n    function onFsActionChanged(val){\n        $('.action-fs-type').hide();\n        switch (val) {\n            case '1':\n                $('.action-fs-rename').show();\n                break;\n            case '2':\n                $('.action-fs-delete').show();\n                break;\n            case '3':\n                $('.action-fs-mkdir').show();\n                break;\n            case '4':\n                $('.action-fs-exist').show();\n                break;\n            case '5':\n                $('.action-fs-compress').show();\n                break;\n            case '6':\n                $('.action-fs-copy').show();\n                break;\n        }\n    }\n\n    $(document).on(\"i18nload\", function(){\n        onTypeChanged('{{.Action.Type}}');\n        onFsActionChanged('{{.Action.Options.FsConfig.Type}}');\n    });\n\n    $(document).on(\"i18nshow\", function(){\n        initRepeater('#http_headers');\n        initRepeater('#query_parameters');\n        initRepeater('#multipart_body');\n        initRepeater('#env_vars');\n        initRepeater('#data_retention');\n        initRepeater('#fs_rename');\n        initRepeater('#fs_copy');\n        initRepeaterItems();\n\n        $('#idType').on(\"change\", function(){\n            onTypeChanged(this.value);\n        });\n\n        $('#idFsActionType').on(\"change\", function(){\n            onFsActionChanged(this.value);\n        });\n\n        $('#eventaction_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/eventactions.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"actions.view_manage\" class=\"card-title section-title\">View and manage event actions</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                </div>\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    {{- if .LoggedUser.HasPermission \"*\"}}\n                    <a href=\"{{.EventActionURL}}\" class=\"btn btn-primary ms-5\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                    {{- end}}\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"general.name\">Name</th>\n                        <th data-i18n=\"general.type\">Type</th>\n                        <th data-i18n=\"title.event_rules\">Rules</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function deleteAction(name) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.EventActionURL}}' + \"/\" + encodeURIComponent(name);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.EventActionsURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"name\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"type\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case 1:\n                                        return $.t('actions.types.http');\n                                    case 2:\n                                        return $.t('actions.types.command');\n                                    case 3:\n                                        return $.t('actions.types.email');\n                                    case 4:\n                                        return $.t('actions.types.backup');\n                                    case 5:\n                                        return $.t('actions.types.user_quota_reset');\n                                    case 6:\n                                        return $.t('actions.types.folder_quota_reset');\n                                    case 7:\n                                        return $.t('actions.types.transfer_quota_reset');\n                                    case 8:\n                                        return $.t('actions.types.data_retention_check');\n                                    case 9:\n                                        return $.t('actions.types.filesystem');\n                                    case 10:\n                                        // metadata check was removed\n                                        return $.t('general.unsupported');;\n                                    case 11:\n                                        return $.t('actions.types.password_expiration_check');\n                                    case 12:\n                                        return $.t('actions.types.user_expiration_check');\n                                    case 13:\n                                        return $.t('actions.types.idp_check');\n                                    case 14:\n                                        return $.t('actions.types.user_inactivity_check');\n                                    case 15:\n                                        return $.t('actions.types.rotate_logs');\n                                    default:\n                                        return \"\";\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"rules\",\n                        defaultContent: [],\n                        searchable: false,\n                        orderable: false,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data.join(', '));\n                                }\n                                return \"\"\n                            }\n                            return \"\";\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let numActions = 0;\n                                let actions = `<button class=\"btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                                    <span data-i18n=\"general.actions\" class=\"fs-6\">Actions</span>\n\t\t\t\t\t\t\t\t\t\t            <i class=\"ki-duotone ki-down fs-5 ms-1 rotate-180\"></i>\n                                                </button>\n                                                <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">`;\n\n                                //{{- if .LoggedUser.HasPermission \"*\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n                                             <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                if (numActions > 0){\n                                    actions+=`</div>`;\n                                    return actions;\n                                }\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                stateLoadParams: function (settings, data) {\n                    if (data.search.search){\n                        const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                        filterSearch.value = data.search.search;\n                    }\n                },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n            dt.on('columns-reordered', function(e, settings, details){\n                drawAction();\n            });\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.EventActionURL}}' + \"/\" + encodeURIComponent(rowData['name']));\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    let row = dt.row(parent).data();\n                    if (row.rules && row.rules.length > 0){\n                        ModalAlert.fire({\n                            text: $.t('actions.err_delete_referenced'),\n                            icon: \"warning\",\n                            confirmButtonText: $.t('general.ok'),\n                            customClass: {\n                                confirmButton: \"btn btn-primary\"\n                            }\n                        });\n                        return;\n                    }\n                    deleteAction(row['name']);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/eventrule.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"eventrule_form\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n\n            <div class=\"form-group row\">\n                <label for=\"idName\" data-i18n=\"general.name\" class=\"col-md-3 col-form-label\">Name</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idName\" type=\"text\" placeholder=\"\" name=\"name\" value=\"{{.Rule.Name}}\" maxlength=\"255\" autocomplete=\"off\"\n                        spellcheck=\"false\" required {{if eq .Mode 2}}class=\"form-control-plaintext readonly-input\" readonly{{else}}class=\"form-control\"{{end}} />\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idStatus\" data-i18n=\"general.status\" class=\"col-md-3 col-form-label\">Status</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idStatus\" name=\"status\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        <option data-i18n=\"general.active\" value=\"1\" {{- if eq .Rule.Status 1 }} selected{{- end}}>Active</option>\n                        <option data-i18n=\"general.inactive\" value=\"0\" {{- if eq .Rule.Status 0 }} selected{{- end}}>Inactive</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idDescription\" type=\"text\" class=\"form-control\" name=\"description\" value=\"{{.Rule.Description}}\" maxlength=\"255\">\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idTrigger\" data-i18n=\"rules.trigger\" class=\"col-md-3 col-form-label\">Trigger</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idTrigger\" name=\"trigger\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        {{- range .TriggerTypes}}\n                        <option value=\"{{.Value}}\" {{if eq $.Rule.Trigger .Value }}selected{{end}} data-i18n=\"{{.Name}}\"></option>\n                        {{- end}}\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row trigger trigger-fs mt-10\">\n                <label for=\"idFsEvents\" data-i18n=\"rules.triggers.fs_events\" class=\"col-md-3 col-form-label\">Fs events</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idFsEvents\" name=\"fs_events\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                        {{- range $event := .FsEvents}}\n                        <option value=\"{{$event}}\" {{- range $.Rule.Conditions.FsEvents }}{{- if eq . $event}}selected{{- end}}{{- end}}>{{$event}}</option>\n                        {{- end}}\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row trigger trigger-provider mt-10\">\n                <label for=\"idProviderEvents\" data-i18n=\"rules.triggers.provider_events\" class=\"col-md-3 col-form-label\">Provider events</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idProviderEvents\" name=\"provider_events\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                        {{- range $event := .ProviderEvents}}\n                        <option value=\"{{$event}}\" {{- range $.Rule.Conditions.ProviderEvents }}{{- if eq . $event}}selected{{- end}}{{- end}}>{{$event}}</option>\n                        {{- end}}\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row trigger trigger-idp mt-10\">\n                <label for=\"idIDPEvent\" data-i18n=\"general.type\" class=\"col-md-3 col-form-label\">IDP Login event</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idIDPEvent\" name=\"idp_login_event\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        <option value=\"0\" data-i18n=\"ip_list.any\" {{if eq .Rule.Conditions.IDPLoginEvent 0}}selected{{end}}>Any</option>\n                        <option value=\"1\" data-i18n=\"rules.idp_logins.user\" {{if eq .Rule.Conditions.IDPLoginEvent 1}}selected{{end}}>User login</option>\n                        <option value=\"2\" data-i18n=\"rules.idp_logins.admin\" {{if eq .Rule.Conditions.IDPLoginEvent 2}}selected{{end}}>Admin login</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"card trigger trigger-schedule mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"rules.triggers.schedule\" class=\"card-title section-title-inner\">Schedules</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"schedules\">\n                        {{- template \"infomsg-no-mb\" \"rules.scheduler_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"schedules\">\n                                {{- range $idx, $val := .Rule.Conditions.Schedules}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.hour\" type=\"text\" class=\"form-control\" name=\"schedule_hour\" value=\"{{$val.Hours}}\" />\n                                            </div>\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.day_of_week\" type=\"text\" class=\"form-control\" name=\"schedule_day_of_week\" value=\"{{$val.DayOfWeek}}\" />\n                                            </div>\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.day_of_month\" type=\"text\" class=\"form-control\" name=\"schedule_day_of_month\" value=\"{{$val.DayOfMonth}}\" />\n                                            </div>\n                                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]general.month\" type=\"text\" class=\"form-control\" name=\"schedule_month\" value=\"{{$val.Month}}\" />\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.hour\" type=\"text\" class=\"form-control\" name=\"schedule_hour\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.day_of_week\" type=\"text\" class=\"form-control\" name=\"schedule_day_of_week\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.day_of_month\" type=\"text\" class=\"form-control\" name=\"schedule_day_of_month\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-2 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]general.month\" type=\"text\" class=\"form-control\" name=\"schedule_month\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            {{- if .IsShared}}\n            <div class=\"form-group row align-items-center trigger trigger-schedule mt-10\">\n                <div class=\"col-md-12\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idConcurrentExecution\" name=\"concurrent_execution\" {{if .Rule.Conditions.Options.ConcurrentExecution}}checked{{end}}/>\n                        <label data-i18n=\"rules.concurrent_run\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idConcurrentExecution\">\n                            Concurrent execution\n                        </label>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"form-group row trigger trigger-fs mt-10\">\n                <label for=\"idFsProtocols\" data-i18n=\"rules.protocol_filters\" class=\"col-md-3 col-form-label\">Protocol filters</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idFsProtocols\" name=\"fs_protocols\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple aria-describedby=\"idFsProtocolsHelp\">\n                        {{- range $p := .Protocols}}\n                        <option value=\"{{$p}}\" {{- range $.Rule.Conditions.Options.Protocols }}{{- if eq . $p}}selected{{- end}}{{- end}}>{{$p}}</option>\n                        {{- end}}\n                    </select>\n                    <div id=\"idFsProtocolsHelp\" data-i18n=\"rules.no_filter\" class=\"form-text\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row trigger trigger-fs mt-10\">\n                <label for=\"idFsStatuses\" data-i18n=\"rules.status_filters\" class=\"col-md-3 col-form-label\">Status filters</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idFsStatuses\" name=\"fs_statuses\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple aria-describedby=\"idFsStatusesHelp\">\n                        <option value=\"1\" data-i18n=\"general.ok\" {{- range $.Rule.Conditions.Options.EventStatuses }}{{- if eq . 1}}selected{{- end}}{{- end}}>OK</option>\n                        <option value=\"2\" data-i18n=\"general.failed\" {{- range $.Rule.Conditions.Options.EventStatuses }}{{- if eq . 2}}selected{{- end}}{{- end}}>Failed</option>\n                        <option value=\"3\" data-i18n=\"events.quota_exceeded\" {{- range $.Rule.Conditions.Options.EventStatuses }}{{- if eq . 3}}selected{{- end}}{{- end}}>Quota exceeded</option>\n                    </select>\n                    <div id=\"idFsStatusesHelp\" data-i18n=\"rules.no_filter\" class=\"form-text\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row trigger trigger-provider mt-10\">\n                <label for=\"idProviderObjects\" data-i18n=\"rules.object_filters\" class=\"col-md-3 col-form-label\">Object filters</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idProviderObjects\" name=\"provider_objects\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple aria-describedby=\"idProviderObjectsHelp\">\n                        {{- range $p := .ProviderObjects}}\n                        <option value=\"{{$p}}\" {{- range $.Rule.Conditions.Options.ProviderObjects }}{{- if eq . $p}}selected{{- end}}{{- end}}>{{$p}}</option>\n                        {{- end}}\n                    </select>\n                    <div id=\"idProviderObjectsHelp\" data-i18n=\"rules.no_filter\" class=\"form-text\"></div>\n                </div>\n            </div>\n\n            <div class=\"card trigger trigger-fs trigger-provider trigger-schedule trigger-on-demand trigger-idp mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"rules.name_filters\" class=\"card-title section-title-inner\">Name filters</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"name_filters\">\n                        {{- template \"infomsg-no-mb\" \"rules.name_filters_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"name_filters\">\n                                {{- range $idx, $val := .Rule.Conditions.Options.Names}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-7 mt-3 mt-md-8\">\n                                                <input type=\"text\" class=\"form-control\" name=\"name_pattern\" value=\"{{$val.Pattern}}\" />\n                                            </div>\n                                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                                <select name=\"type_name_pattern\" data-i18n=\"[data-placeholder]general.mode\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-allow-clear=\"true\">\n                                                    <option value=\"\"></option>\n                                                    <option value=\"inverse\" data-i18n=\"rules.inverse_match\" {{if $val.InverseMatch}}selected{{end}}>Inverse match</option>\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-7 mt-3 mt-md-8\">\n                                            <input type=\"text\" class=\"form-control\" name=\"name_pattern\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-4 mt-3 mt-md-8\">\n                                            <select name=\"type_name_pattern\" data-i18n=\"[data-placeholder]general.mode\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-allow-clear=\"true\">\n                                                <option value=\"\"></option>\n                                                <option value=\"inverse\" data-i18n=\"rules.inverse_match\">Inverse match</option>\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card trigger trigger-fs trigger-provider trigger-schedule trigger-on-demand mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"rules.group_name_filters\" class=\"card-title section-title-inner\">Group name filters</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"group_name_filters\">\n                        {{- template \"infomsg-no-mb\" \"rules.group_name_filters_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"group_name_filters\">\n                                {{- range $idx, $val := .Rule.Conditions.Options.GroupNames}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-7 mt-3 mt-md-8\">\n                                                <input type=\"text\" class=\"form-control\" name=\"group_name_pattern\" value=\"{{$val.Pattern}}\" />\n                                            </div>\n                                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                                <select name=\"type_group_name_pattern\" data-i18n=\"[data-placeholder]general.mode\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-allow-clear=\"true\">\n                                                    <option value=\"\"></option>\n                                                    <option value=\"inverse\" data-i18n=\"rules.inverse_match\" {{if $val.InverseMatch}}selected{{end}}>Inverse match</option>\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-7 mt-3 mt-md-8\">\n                                            <input type=\"text\" class=\"form-control\" name=\"group_name_pattern\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-4 mt-3 mt-md-8\">\n                                            <select name=\"type_group_name_pattern\" data-i18n=\"[data-placeholder]general.mode\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-allow-clear=\"true\">\n                                                <option value=\"\"></option>\n                                                <option value=\"inverse\" data-i18n=\"rules.inverse_match\">Inverse match</option>\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card trigger trigger-fs trigger-schedule trigger-provider trigger-on-demand mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"rules.role_name_filters\" class=\"card-title section-title-inner\">Role name filters</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"role_name_filters\">\n                        {{- template \"infomsg-no-mb\" \"rules.role_name_filters_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"role_name_filters\">\n                                {{- range $idx, $val := .Rule.Conditions.Options.RoleNames}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-7 mt-3 mt-md-8\">\n                                                <input type=\"text\" class=\"form-control\" name=\"role_name_pattern\" value=\"{{$val.Pattern}}\" />\n                                            </div>\n                                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                                <select name=\"type_role_name_pattern\" data-i18n=\"[data-placeholder]general.mode\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-allow-clear=\"true\">\n                                                    <option value=\"\"></option>\n                                                    <option value=\"inverse\" data-i18n=\"rules.inverse_match\" {{if $val.InverseMatch}}selected{{end}}>Inverse match</option>\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-7 mt-3 mt-md-8\">\n                                            <input type=\"text\" class=\"form-control\" name=\"role_name_pattern\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-4 mt-3 mt-md-8\">\n                                            <select name=\"type_role_name_pattern\" data-i18n=\"[data-placeholder]general.mode\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-allow-clear=\"true\">\n                                                <option value=\"\"></option>\n                                                <option value=\"inverse\" data-i18n=\"rules.inverse_match\">Inverse match</option>\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card trigger trigger-fs mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"rules.path_filters\" class=\"card-title section-title-inner\">Path filters</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"path_filters\">\n                        {{- template \"infomsg-no-mb\" \"rules.path_filters_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"path_filters\">\n                                {{- range $idx, $val := .Rule.Conditions.Options.FsPaths}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-7 mt-3 mt-md-8\">\n                                                <input type=\"text\" class=\"form-control\" name=\"fs_path_pattern\" value=\"{{$val.Pattern}}\" />\n                                            </div>\n                                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                                <select name=\"type_fs_path_pattern\" data-i18n=\"[data-placeholder]general.mode\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-allow-clear=\"true\">\n                                                    <option value=\"\"></option>\n                                                    <option value=\"inverse\" data-i18n=\"rules.inverse_match\" {{if $val.InverseMatch}}selected{{end}}>Inverse match</option>\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-7 mt-3 mt-md-8\">\n                                            <input type=\"text\" class=\"form-control\" name=\"fs_path_pattern\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-4 mt-3 mt-md-8\">\n                                            <select name=\"type_fs_path_pattern\" data-i18n=\"[data-placeholder]general.mode\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-allow-clear=\"true\">\n                                                <option value=\"\"></option>\n                                                <option value=\"inverse\" data-i18n=\"rules.inverse_match\">Inverse match</option>\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card trigger trigger-fs mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"rules.file_size_limits\" class=\"card-title section-title-inner\">\n                        File size limits\n                    </h3>\n                </div>\n                <div class=\"card-body\">\n                    {{- template \"infomsg\" \"rules.file_size_limits_help\"}}\n                    <div class=\"form-group row\">\n                        <label for=\"idFsMinSize\" data-i18n=\"rules.min_size\" class=\"col-md-3 col-form-label\">Min size</label>\n                        <div class=\"col-md-3\">\n                            <input id=\"idFsMinSize\" type=\"text\" class=\"form-control\" name=\"fs_min_size\" value=\"{{HumanizeBytes .Rule.Conditions.Options.MinFileSize}}\" />\n                        </div>\n                        <div class=\"col-md-1\"></div>\n                        <label for=\"idFsMaxSize\" data-i18n=\"rules.max_size\" class=\"col-md-2 col-form-label\">Max size</label>\n                        <div class=\"col-md-3\">\n                            <input id=\"idFsMaxSize\" type=\"text\" class=\"form-control\" name=\"fs_max_size\" value=\"{{HumanizeBytes .Rule.Conditions.Options.MaxFileSize}}\" />\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"title.event_actions\" class=\"card-title section-title-inner\">Actions</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"actions\">\n                        {{- template \"infomsg-no-mb\" \"rules.actions_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"actions\">\n                                {{- range $idx, $val := .Rule.Actions}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                <select name=\"action_name\" data-i18n=\"[data-placeholder]rules.action_placeholder\" class=\"form-select select-repetear\" data-allow-clear=\"true\">\n                                                    <option value=\"\"></option>\n                                                    {{- range $.Actions}}\n                                                    <option value=\"{{.Name}}\" {{if eq $val.Name .Name}}selected{{end}}>{{.Name}}</option>\n                                                    {{- end}}\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                <select name=\"action_options\" class=\"form-select select-repetear\" data-i18n=\"[data-placeholder]general.options\" data-close-on-select=\"false\" data-allow-clear=\"true\" data-hide-search=\"true\" multiple>\n                                                    <option value=\"\"></option>\n                                                    <option value=\"2\" data-i18n=\"rules.option_stop_on_failure\" {{if $val.Options.StopOnFailure}}selected{{end}}>Stop on failure</option>\n                                                    <option value=\"3\" data-i18n=\"rules.option_execute_sync\" {{if $val.Options.ExecuteSync}}selected{{end}}>Execute sync</option>\n                                                    <option value=\"1\" data-i18n=\"rules.option_failure_action\" {{if $val.Options.IsFailureAction}}selected{{end}}>Is failure action</option>\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                            <select name=\"action_name\" data-i18n=\"[data-placeholder]rules.action_placeholder\" class=\"form-select select-repetear\" data-allow-clear=\"true\">\n                                                <option value=\"\"></option>\n                                                {{- range $.Actions}}\n                                                <option value=\"{{.Name}}\">{{.Name}}</option>\n                                                {{- end}}\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <select name=\"action_options\" class=\"form-select select-repetear\" data-i18n=\"[data-placeholder]general.options\" data-close-on-select=\"false\" data-allow-clear=\"true\" data-hide-search=\"true\" multiple>\n                                                <option value=\"\"></option>\n                                                <option value=\"2\" data-i18n=\"rules.option_stop_on_failure\">Stop on failure</option>\n                                                <option value=\"3\" data-i18n=\"rules.option_execute_sync\">Execute sync</option>\n                                                <option value=\"1\" data-i18n=\"rules.option_failure_action\">Is failure action</option>\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n\n        </form>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function onTriggerChanged(val){\n        $('.trigger').hide();\n        switch (val) {\n            case '1':\n                $('.trigger-fs').show();\n                break;\n            case '2':\n                $('.trigger-provider').show();\n                break;\n            case '3':\n                $('.trigger-schedule').show();\n                break;\n            case '4':\n            case '5':\n                break;\n            case '6':\n                $('.trigger-on-demand').show();\n                break;\n            case '7':\n                $('.trigger-idp').show();\n                break;\n            default:\n                console.log(`unsupported event trigger type: ${val}`);\n        }\n    }\n\n    $(document).on(\"i18nload\", function(){\n        onTriggerChanged('{{.Rule.Trigger}}');\n    });\n\n    $(document).on(\"i18nshow\", function(){\n        initRepeater('#schedules');\n        initRepeater('#name_filters');\n        initRepeater('#group_name_filters');\n        initRepeater('#role_name_filters');\n        initRepeater('#path_filters');\n        initRepeater('#actions');\n        initRepeaterItems();\n\n        $('#idTrigger').on(\"change\", function(){\n            onTriggerChanged(this.value);\n        });\n\n        $('#eventrule_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/eventrules.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"rules.view_manage\" class=\"card-title section-title\">View and manage event rules</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                </div>\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    {{- if .LoggedUser.HasPermission \"*\"}}\n                    <a href=\"{{.EventRuleURL}}\" class=\"btn btn-primary ms-5\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                    {{- end}}\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"general.name\">Name</th>\n                        <th data-i18n=\"general.status\">Status</th>\n                        <th data-i18n=\"rules.trigger\">Trigger</th>\n                        <th data-i18n=\"title.event_actions\">Actions</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function deleteAction(name) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.EventRuleURL}}' + \"/\" + encodeURIComponent(name);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    function runAction(name) {\n        ModalAlert.fire({\n            text: $.t('rules.run_confirm'),\n            icon: \"warning\",\n            confirmButtonText: $.t('rules.run_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.EventRuleURL}}' + \"/run/\" + encodeURIComponent(name);\n\n                axios.post(path, null, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 202;\n                    }\n                }).then(function(response){\n                    KTApp.hidePageLoading();\n                    ModalAlert.fire({\n                        text: $.t(\"rules.run_ok\"),\n                        icon: \"success\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    ModalAlert.fire({\n                        text: $.t(\"rules.run_error_generic\"),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.EventRulesURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"name\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"status\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case 1:\n                                        return $.t('general.active');\n                                    default:\n                                        return $.t('general.inactive');\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"trigger\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case 1:\n                                        return $.t('rules.triggers.fs_event');\n                                    case 2:\n                                        return $.t('rules.triggers.provider_event');\n                                    case 3:\n                                        return $.t('rules.triggers.schedule');\n                                    case 4:\n                                        return $.t('rules.triggers.ip_blocked');\n                                    case 5:\n                                        return $.t('rules.triggers.certificate_renewal');\n                                    case 6:\n                                        return $.t('rules.triggers.on_demand');\n                                    case 7:\n                                        return $.t('rules.triggers.idp_login');\n                                    default:\n                                        return \"\";\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"actions\",\n                        defaultContent: [],\n                        render: function(data, type, row) {\n                            if (!data){\n                                return \"\";\n                            }\n                            let actions = [];\n                            let result = \"\";\n                            for (i = 0; i < data.length; i++){\n                                actions.push(data[i].name);\n                            }\n                            result = actions.join(\", \");\n                            if (type === 'display') {\n                                return escapeHTML(result);\n                            }\n                            return result;\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let numActions = 0;\n                                let actions = `<button class=\"btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                                    <span data-i18n=\"general.actions\" class=\"fs-6\">Actions</span>\n\t\t\t\t\t\t\t\t\t\t            <i class=\"ki-duotone ki-down fs-5 ms-1 rotate-180\"></i>\n                                                </button>\n                                                <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">`;\n\n                                //{{- if .LoggedUser.HasPermission \"*\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                numActions++;\n                                if (row.trigger === 6){\n                                    actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"rules.run\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"run_row\">Run</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                numActions++;\n                                }\n                                actions+=`<div class=\"menu-item px-3\">\n                                             <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                if (numActions > 0){\n                                    actions+=`</div>`;\n                                    return actions;\n                                }\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                stateLoadParams: function (settings, data) {\n                        if (data.search.search){\n                            const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                            filterSearch.value = data.search.search;\n                        }\n                    },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n            dt.on('columns-reordered', function(e, settings, details){\n                drawAction();\n            });\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.EventRuleURL}}' + \"/\" + encodeURIComponent(rowData['name']));\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    deleteAction(dt.row(parent).data()['name']);\n                });\n            });\n\n            const runButtons = document.querySelectorAll('[data-table-action=\"run_row\"]');\n            runButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    runAction(dt.row(parent).data()['name']);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/events.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"events.search\" class=\"card-title section-title\">Search logs</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div class=\"form-group row\">\n            <div class=\"col-md-3 mt-5\">\n                <select class=\"form-select\" id=\"idEventType\" name=\"events_type\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                    <option value=\"1\" data-i18n=\"events.fs_events\">Fs events</option>\n                    <option value=\"2\" data-i18n=\"events.provider_events\">Provider events</option>\n                    <option value=\"3\" data-i18n=\"events.other_events\">Other events</option>\n                </select>\n            </div>\n            <div class=\"col-md-4 mt-5\">\n                <select class=\"form-select\" id=\"idActions\" name=\"actions\" data-control=\"i18n-select2\" data-hide-search=\"true\"\n                    data-close-on-select=\"false\" data-i18n=\"[data-placeholder]general.actions\" multiple>\n                </select>\n            </div>\n            <div class=\"col-md-3 mt-5\">\n                <input type=\"text\" class=\"form-control\" id=\"idUsername\" name=\"username\" data-i18n=\"[placeholder]login.username\" spellcheck=\"false\">\n            </div>\n            <div class=\"col-md-2 mt-5\">\n                <input type=\"text\" class=\"form-control\" id=\"idIp\" name=\"ip\" data-i18n=\"[placeholder]defender.ip\">\n            </div>\n        </div>\n        <div class=\"form-group row\">\n            <div class=\"col-md-3 mt-5\">\n                <select class=\"form-select fs-events\" id=\"idStatuses\" name=\"statuses\" data-control=\"i18n-select2\" data-hide-search=\"true\"\n                    data-close-on-select=\"false\" data-i18n=\"[data-placeholder]general.status\" multiple>\n                    <option value=\"1\" data-i18n=\"general.ok\">OK</option>\n                    <option value=\"2\" data-i18n=\"general.failed\">KO</option>\n                    <option value=\"3\" data-i18n=\"events.quota_exceeded\">Quota exceeded</option>\n                </select>\n            </div>\n            <div class=\"col-md-4 mt-5\">\n                <select class=\"form-select fs-events log-events\" id=\"idProtocols\" name=\"protocols\" data-control=\"i18n-select2\" data-hide-search=\"true\"\n                    data-close-on-select=\"false\" data-i18n=\"[data-placeholder]ip_list.protocols\" multiple>\n                    <option value=\"SFTP\">SFTP</option>\n                    <option value=\"SCP\">SCP</option>\n                    <option value=\"SSH\">SSH</option>\n                    <option value=\"FTP\">FTP</option>\n                    <option value=\"DAV\">DAV</option>\n                    <option value=\"HTTP\">HTTP</option>\n                    <option value=\"OIDC\">OIDC</option>\n                    <option value=\"HTTPShare\">HTTPShare</option>\n                    <option value=\"DataRetention\">DataRetention</option>\n                    <option value=\"EventAction\">EventAction</option>\n                </select>\n            </div>\n            <div class=\"col-md-5 mt-5\">\n                <input id=\"dateTimeRange\" class=\"form-control\" data-i18n=\"[placeholder]events.date_range\" />\n            </div>\n        </div>\n\n        <div class=\"d-flex justify-content-end mt-10 mb-10\">\n            <button id=\"export_button\" class=\"btn btn-secondary px-10 me-10\">\n                <span data-i18n=\"general.export\" class=\"indicator-label\">\n                    Export\n                </span>\n                <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                    Please wait...\n                    <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                </span>\n            </button>\n            <button id=\"search_button\" class=\"btn btn-primary px-10\">\n                <span data-i18n=\"general.search\" class=\"indicator-label\">\n                    Search\n                </span>\n                <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                    Please wait...\n                    <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                </span>\n            </button>\n        </div>\n\n        <table id=\"dataTableFs\" class=\"table align-middle table-row-dashed fs-6 gy-5 fs-events\">\n            <thead>\n                <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                    <th data-i18n=\"events.datetime\">Date and time</th>\n                    <th data-i18n=\"events.action\">Action</th>\n                    <th data-i18n=\"events.path\">Path</th>\n                    <th data-i18n=\"login.username\">Username</th>\n                    <th data-i18n=\"general.protocol\">Protocol</th>\n                    <th data-i18n=\"defender.ip\">IP</th>\n                    <th data-i18n=\"general.info\">Info</th>\n                </tr>\n            </thead>\n            <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n        </table>\n\n        <table id=\"dataTableProvider\" class=\"table align-middle table-row-dashed fs-6 gy-5 provider-events\">\n            <thead>\n                <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                    <th data-i18n=\"events.datetime\">Date and time</th>\n                    <th data-i18n=\"events.action\">Action</th>\n                    <th data-i18n=\"events.object\">Object</th>\n                    <th data-i18n=\"login.username\">Username</th>\n                    <th data-i18n=\"defender.ip\">IP</th>\n                </tr>\n            </thead>\n            <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n        </table>\n\n        <table id=\"dataTableLog\" class=\"table align-middle table-row-dashed fs-6 gy-5 log-events\">\n            <thead>\n                <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                    <th data-i18n=\"events.datetime\">Date and time</th>\n                    <th data-i18n=\"events.event\">Event</th>\n                    <th data-i18n=\"login.username\">Username</th>\n                    <th data-i18n=\"general.protocol\">Protocol</th>\n                    <th data-i18n=\"defender.ip\">IP</th>\n                    <th data-i18n=\"general.info\">Info</th>\n                </tr>\n            </thead>\n            <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n        </table>\n\n        <div id=\"paginationContainer\" class=\"d-flex mt-4 mb-4 justify-content-end d-none\">\n            <div class=\"btn-group\" role=\"group\" aria-label=\"Pagination\">\n                <button id=\"pagePrevious\" data-i18n=\"general.previous\" type=\"button\" class=\"btn btn-outline btn-active-primary disabled\">Previous</button>\n                <button id=\"pageNext\" data-i18n=\"general.next\" type=\"button\" class=\"btn btn-outline btn-active-primary disabled\">Next</button>\n            </div>\n        </div>\n\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/it.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/de.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/fr.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/zh.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/es.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/humanize-duration/humanize-duration.min.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    const pageSize = 20;\n    const paginationData = new Map();\n\n    function resetPagination() {\n        $('#pagePrevious').addClass(\"disabled\");\n        $('#pageNext').addClass(\"disabled\");\n        $('#paginationContainer').addClass(\"d-none\");\n        paginationData.delete(\"firstId\");\n        paginationData.delete(\"firstTs\");\n        paginationData.delete(\"lastId\");\n        paginationData.delete(\"lastTs\");\n        paginationData.set(\"prevClicked\",false);\n        paginationData.set(\"nextClicked\",false);\n    }\n\n    function prevClicked(){\n        paginationData.set(\"prevClicked\",true);\n        paginationData.set(\"nextClicked\",false);\n        doSearch();\n    }\n\n    function nextClicked(){\n        paginationData.set(\"prevClicked\",false);\n        paginationData.set(\"nextClicked\",true);\n        doSearch();\n    }\n\n    function handleResponseData(data) {\n        let length = data.length;\n        let isNext = paginationData.get(\"nextClicked\");\n        let isPrev = paginationData.get(\"prevClicked\");\n\n        if (length > pageSize) {\n            data.pop();\n            length--;\n            if (isPrev || isNext){\n                $('#pagePrevious').removeClass(\"disabled\");\n            }\n            $('#pageNext').removeClass(\"disabled\");\n        } else {\n            if (isPrev){\n                $('#pagePrevious').addClass(\"disabled\");\n                $('#pageNext').removeClass(\"disabled\");\n            } else if (isNext){\n                $('#pagePrevious').removeClass(\"disabled\");\n                $('#pageNext').addClass(\"disabled\");\n            } else {\n                $('#pageNext').addClass(\"disabled\");\n            }\n        }\n        if (isPrev){\n            data = data.reverse();\n        }\n        if (length > 0){\n            paginationData.set(\"lastId\",data[0].id);\n            paginationData.set(\"lastTs\",data[0].timestamp);\n            paginationData.set(\"firstId\",data[length-1].id);\n            paginationData.set(\"firstTs\",data[length-1].timestamp);\n            $('#paginationContainer').removeClass(\"d-none\");\n        } else {\n            resetPagination();\n        }\n        return data;\n    }\n\n    function humanizeMilliseconds(val) {\n        let units = [\"d\", \"h\", \"m\", \"s\", \"ms\"];\n        let decimalPoints = 1;\n        if (val > 1000){\n            units = [\"d\", \"h\", \"m\", \"s\"]\n        }\n        if (val > 60000){\n            decimalPoints = 0;\n        }\n        return humanizeDuration(val, {\n            language: humanizeDurationLocale(),\n            fallbacks: [\"en\"],\n            maxDecimalPoints: decimalPoints,\n            units: units\n        })\n    }\n\n    function getSearchURL(csvExport) {\n        let url = \"\";\n        let eventType = $('#idEventType').val();\n        let order = \"DESC\";\n        let limit = pageSize + 1;\n        if (csvExport){\n            order = \"ASC\";\n        }\n        if (eventType == 1){\n            url = \"{{.FsEventsSearchURL}}?limit=\"+limit;\n            let protocols = [];\n            $('#idProtocols').find('option:selected').each(function(){\n                protocols.push($(this).val());\n            });\n            if (protocols.length > 0){\n                url+=\"&protocols=\"+encodeURIComponent(String(protocols));\n            }\n            let statuses = [];\n            $('#idStatuses').find('option:selected').each(function(){\n                statuses.push($(this).val());\n            });\n            if (statuses.length > 0){\n                url+=\"&statuses=\"+encodeURIComponent(String(statuses));\n            }\n        } else if (eventType == 2) {\n            url = \"{{.ProviderEventsSearchURL}}?omit_object_data=true&limit=\"+limit;\n        } else {\n            url = \"{{.LogEventsSearchURL}}?limit=\"+limit;\n            let protocols = [];\n            $('#idProtocols').find('option:selected').each(function(){\n                protocols.push($(this).val());\n            });\n            if (protocols.length > 0){\n                url+=\"&protocols=\"+encodeURIComponent(String(protocols));\n            }\n        }\n        let actions = [];\n        $('#idActions').find('option:selected').each(function(){\n            actions.push($(this).val());\n        });\n        if (actions.length > 0){\n            if (eventType == 3){\n                url+=\"&events=\"+encodeURIComponent(String(actions));\n            } else {\n                url+=\"&actions=\"+encodeURIComponent(String(actions));\n            }\n        }\n        let username = $('#idUsername').val();\n        if (username){\n            url+=\"&username=\"+encodeURIComponent(username);\n        }\n        let ip = $('#idIp').val();\n        if (ip){\n            url+=\"&ip=\"+encodeURIComponent(ip);\n        }\n        const dateRangePicker = document.querySelector(\"#dateTimeRange\")._flatpickr;\n        let drp = dateRangePicker.selectedDates;\n        let fromID = \"\";\n        let start_ts = 0;\n        if (!csvExport && paginationData.get(\"prevClicked\") && paginationData.has(\"lastId\") && paginationData.has(\"lastTs\")){\n            order = \"ASC\";\n            start_ts = paginationData.get(\"lastTs\");\n            fromID = paginationData.get(\"lastId\");\n        } else {\n            if (drp.length > 0){\n                let d = drp[0];\n                if (d) {\n                    start_ts = d.getTime()*1000000;\n                }\n            }\n        }\n        let end_ts = 0;\n        if (!csvExport && paginationData.get(\"nextClicked\") && paginationData.has(\"firstId\") && paginationData.has(\"firstTs\")){\n            end_ts = paginationData.get(\"firstTs\");\n            fromID = paginationData.get(\"firstId\");\n        } else {\n            if (drp.length > 1){\n                let d = drp[1];\n                if (d) {\n                    end_ts = d.getTime()*1000000;\n                }\n            }\n        }\n        url+=\"&start_timestamp=\"+encodeURIComponent(start_ts);\n        url+=\"&end_timestamp=\"+encodeURIComponent(end_ts);\n        if (fromID){\n            url+=\"&from_id=\"+encodeURIComponent(fromID);\n        }\n        url+=\"&order=\"+order;\n        if (csvExport){\n            url+=\"&csv_export=true\";\n        }\n        return url;\n    }\n\n    function onExportClicked() {\n        paginationData.set(\"prevClicked\",false);\n        paginationData.set(\"nextClicked\",false);\n        let exportURL = getSearchURL(true);\n        let ts = new Date().getTime().toString();\n        window.open(`${exportURL}&_=${ts}`);\n    }\n\n    function onSearchClicked() {\n        resetPagination();\n        doSearch();\n    }\n\n    function doSearch() {\n        let eventType = $('#idEventType').val();\n        switch (eventType){\n            case \"1\":\n                datatableFsEvents.init();\n                return;\n            case \"2\":\n                datatableProviderEvents.init();\n                return;\n            case \"3\":\n                datatableLogEvents.init();\n                return;\n            default:\n                console.log(`unsupported event type \"${eventType}\"`);\n        }\n    }\n\n    function selectFsEvents(){\n        let idActions = $('#idActions');\n        idActions.empty();\n        idActions.append(new Option($.t('events.upload'),\"upload\",false,false));\n        idActions.append(new Option($.t('events.download'),\"download\",false,false));\n        idActions.append(new Option($.t('events.mkdir'),\"mkdir\",false,false));\n        idActions.append(new Option($.t('events.rmdir'),\"rmdir\",false,false));\n        idActions.append(new Option($.t('events.rename'),\"rename\",false,false));\n        idActions.append(new Option($.t('events.copy'),\"copy\",false,false));\n        idActions.append(new Option($.t('events.delete'),\"delete\",false,false));\n        idActions.append(new Option($.t('events.first_upload'),\"first-upload\",false,false));\n        idActions.append(new Option($.t('events.first_download'),\"first-download\",false,false));\n        idActions.append(new Option($.t('events.ssh_cmd'),\"ssh_cmd\",false,false));\n        idActions.trigger('change');\n        $('#idUsername').val(\"\");\n        $('#idIp').val(\"\");\n        $('.provider-events').hide();\n        $('.log-events').hide();\n        $('.fs-events').show();\n        onSearchClicked();\n    }\n\n    function selectLogEvents(){\n        let idActions = $('#idActions');\n        idActions.empty();\n        idActions.append(new Option($.t('events.login_ok'),\"5\",false,false));\n        idActions.append(new Option($.t('events.login_failed'),\"1\",false,false));\n        idActions.append(new Option($.t('events.login_missing_user'),\"2\",false,false));\n        idActions.append(new Option($.t('events.no_login_tried'),\"3\",false,false));\n        idActions.append(new Option($.t('events.algo_negotiation_failed'),\"4\",false,false));\n        idActions.trigger('change');\n        $('#idUsername').val(\"\");\n        $('#idIp').val(\"\");\n        $('.provider-events').hide();\n        $('.fs-events').hide();\n        $('.log-events').show();\n        onSearchClicked();\n    }\n\n    function selectProviderEvents(){\n        let idActions = $('#idActions');\n        idActions.empty();\n        idActions.append(new Option($.t('events.add'),\"add\",false,false));\n        idActions.append(new Option($.t('events.update'),\"update\",false,false));\n        idActions.append(new Option($.t('events.delete'),\"delete\",false,false));\n        idActions.trigger('change');\n        $('#idUsername').val(\"\");\n        $('#idIp').val(\"\");\n        $('.fs-events').hide();\n        $('.log-events').hide();\n        $('.provider-events').show();\n        onSearchClicked();\n    }\n\n    function onEventChanged(val){\n        switch (val){\n            case '1':\n                selectFsEvents();\n                break;\n            case '2':\n                selectProviderEvents();\n                break;\n            case '3':\n                selectLogEvents();\n                break;\n            default:\n                console.log(`unsupported event type: ${val}`);\n        }\n    }\n\n    var datatableFsEvents = function(){\n        var dt;\n\n        var initDatatable = function () {\n            if (dt){\n                dt.clear().draw();\n                dt.ajax.url(getSearchURL(false)).load();\n                return;\n            }\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTableFs').DataTable({\n                ajax: {\n                    url: getSearchURL(false),\n                    dataSrc: handleResponseData,\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"timestamp\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    let ts = data/1000000;\n                                    if (ts){\n                                        return $.t('general.datetime', {\n                                            val: new Date(ts),\n                                            formatParams: {\n                                                val: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },\n                                            }\n                                        });\n                                    }\n                                }\n                                return \"\";\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"action\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case \"upload\":\n                                        return  $.t('events.upload');\n                                    case \"download\":\n                                        return  $.t('events.download');\n                                    case \"mkdir\":\n                                        return  $.t('events.mkdir');\n                                    case \"rmdir\":\n                                        return  $.t('events.rmdir');\n                                    case \"rename\":\n                                        return  $.t('events.rename');\n                                    case \"delete\":\n                                        return  $.t('events.delete');\n                                    case \"first-upload\":\n                                        return  $.t('events.first_upload');\n                                    case \"first-download\":\n                                        return  $.t('events.first_download');\n                                    case \"ssh_cmd\":\n                                        return  $.t('events.ssh_cmd');\n                                    case \"copy\":\n                                        return  $.t('events.copy');\n                                    default:\n                                        console.log(`unknown fs action \"${data}\"`);\n                                        return \"\";\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"virtual_path\",\n                        defaultContent: \"\",\n                        width: '20%',\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                if (row.virtual_target_path){\n                                    return escapeHTML(`${data} => ${row.virtual_target_path}`);\n                                }\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"username\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"protocol\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (!data){\n                                    return \"\";\n                                }\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"ip\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"status\",\n                        defaultContent: \"\",\n                        width: '15%',\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                let info = \"\";\n                                switch (data){\n                                    case 1:\n                                        info = $.t('general.ok');\n                                        break;\n                                    case 2:\n                                        info = $.t('general.failed');\n                                        break;\n                                    case 3:\n                                        info = $.t('events.quota_exceeded');\n                                        break;\n                                    default:\n                                        console.log(`unknow status ${data}`);\n                                }\n                                if (row.file_size || row.file_size == 0){\n                                    let humanSize = fileSizeIEC(row[\"file_size\"]);\n                                    info+=`. ${humanSize}`;\n                                }\n                                if (row.elapsed){\n                                    let elapsed = humanizeMilliseconds(row.elapsed);\n                                    info+=`. ${elapsed}`;\n                                }\n                                if (row.ssh_cmd){\n                                    info+=\". \"+$.t('events.ssh_cmd')+` \"${row.ssh_cmd}\"`;\n                                }\n                                return info;\n                            }\n                            return data;\n                        }\n                    },\n                ],\n                deferRender: true,\n                processing: true,\n                lengthChange: false,\n                searching: false,\n                paging: false,\n                info: false,\n                ordering: false,\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n            });\n\n            dt.on('draw.dt', drawAction);\n        }\n\n        function drawAction() {\n            $('#table_body').localize();\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n            }\n        }\n    }();\n\n    var datatableProviderEvents = function(){\n        var dt;\n\n        var initDatatable = function () {\n            if (dt){\n                dt.clear().draw();\n                dt.ajax.url(getSearchURL(false)).load();\n                return;\n            }\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTableProvider').DataTable({\n                ajax: {\n                    url: getSearchURL(false),\n                    dataSrc: handleResponseData,\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"timestamp\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    let ts = data/1000000;\n                                    if (ts){\n                                        return $.t('general.datetime', {\n                                            val: new Date(ts),\n                                            formatParams: {\n                                                val: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },\n                                            }\n                                        });\n                                    }\n                                }\n                                return \"\";\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"action\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case \"add\":\n                                        return  $.t('events.add');\n                                    case \"update\":\n                                        return  $.t('events.update');\n                                    case \"delete\":\n                                        return  $.t('events.delete');\n                                    console.log(`unknown provider action \"${data}\"`);\n                                        return \"\";\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"object_type\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                let message;\n                                switch (data){\n                                    case \"user\":\n                                        message = $.t('provider_objects.user');\n                                        break;\n                                    case \"folder\":\n                                        message = $.t('provider_objects.folder');\n                                        break;\n                                    case \"group\":\n                                        message = $.t('provider_objects.group');\n                                        break;\n                                    case \"admin\":\n                                        message = $.t('provider_objects.admin');\n                                        break;\n                                    case \"api_key\":\n                                        message = $.t('provider_objects.api_key');\n                                        break;\n                                    case \"share\":\n                                        message = $.t('provider_objects.share');\n                                        break;\n                                    case \"event_action\":\n                                        message = $.t('provider_objects.event_action');\n                                        break;\n                                    case \"event_rule\":\n                                        message = $.t('provider_objects.event_rule');\n                                        break;\n                                    case \"role\":\n                                        message = $.t('provider_objects.role');\n                                        break;\n                                    case \"ip_list_entry\":\n                                        message = $.t('provider_objects.ip_list_entry');\n                                        break;\n                                    case \"configs\":\n                                        message = $.t('provider_objects.configs');\n                                        break;\n                                    default:\n                                        console.log(\"uknown object type: \"+data);\n                                        return \"\"\n                                }\n                                if (row.object_name && data != \"configs\"){\n                                    return message+= escapeHTML(` \"${row.object_name}\"`);\n                                }\n                                return message;\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"username\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"ip\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                ],\n                deferRender: true,\n                processing: true,\n                lengthChange: false,\n                searching: false,\n                paging: false,\n                info: false,\n                ordering: false,\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n            });\n\n            dt.on('draw.dt', drawAction);\n        }\n\n        function drawAction() {\n            $('#table_body').localize();\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n            }\n        }\n    }();\n\n    var datatableLogEvents = function(){\n        var dt;\n\n        var initDatatable = function () {\n            if (dt){\n                dt.clear().draw();\n                dt.ajax.url(getSearchURL(false)).load();\n                return;\n            }\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTableLog').DataTable({\n                ajax: {\n                    url: getSearchURL(false),\n                    dataSrc: handleResponseData,\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"timestamp\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    let ts = data/1000000;\n                                    if (ts){\n                                        return $.t('general.datetime', {\n                                            val: new Date(ts),\n                                            formatParams: {\n                                                val: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },\n                                            }\n                                        });\n                                    }\n                                }\n                                return \"\";\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"event\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case 1:\n                                        return  $.t('events.login_failed');\n                                    case 2:\n                                        return  $.t('events.login_missing_user');\n                                    case 3:\n                                        return  $.t('events.no_login_tried');\n                                    case 4:\n                                        return  $.t('events.algo_negotiation_failed');\n                                    case 5:\n                                        return  $.t('events.login_ok');\n                                    default:\n                                        console.log(`unknown log action \"${data}\"`);\n                                        return \"\";\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"username\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"protocol\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"ip\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (!data){\n                                    return \"\";\n                                }\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"message\",\n                        defaultContent: \"\",\n                        width: '25%',\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                processing: true,\n                lengthChange: false,\n                searching: false,\n                paging: false,\n                info: false,\n                ordering: false,\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n            });\n\n            dt.on('draw.dt', drawAction);\n        }\n\n        function drawAction() {\n            $('#table_body').localize();\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nload\", function(){\n        $('#dateTimeRange').flatpickr({\n            enableTime: true,\n            time_24hr: true,\n            formatDate: (date, format, locale) => {\n                return $.t('general.datetime', {\n                            val: new Date(date),\n                            formatParams: {\n                                val: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },\n                            }\n                        });\n            },\n            mode: \"range\",\n            defaultDate: [moment().add(-1,'hour').format('YYYY-MM-DD HH:mm')],\n            minuteIncrement: 1,\n            locale: flatpickrLocale()\n        });\n        onEventChanged('1');\n    });\n\n    $(document).on(\"i18nshow\", function(){\n        $('#idEventType').on(\"change\", function(){\n            onEventChanged(this.value);\n        });\n\n        $('#search_button').on(\"click\", function(e){\n            e.preventDefault();\n            this.blur();\n            onSearchClicked();\n        });\n\n        $('#export_button').on(\"click\", function(e){\n            e.preventDefault();\n            this.blur();\n            onExportClicked();\n        });\n\n        $('#pagePrevious').on(\"click\", function(e){\n            e.preventDefault();\n            this.blur();\n            prevClicked();\n        });\n\n        $('#pageNext').on(\"click\", function(e){\n            e.preventDefault();\n            this.blur();\n            nextClicked();\n        });\n\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/folder.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- if eq .Mode 3}}\n        <div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5\">\n            <i class=\"ki-duotone ki-shield-tick fs-2tx text-primary me-4\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n            <div class=\"d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap\">\n                <div class=\"mb-3 mb-md-0 fw-semibold\">\n                    <h4 class=\"text-gray-900 fw-bold\">\n                        <span data-i18n=\"virtual_folders.template_title\">Create one or more new folders from this template</span>\n                    </h4>\n                    <div class=\"fs-6 text-gray-800 pe-7\">\n                        <p data-i18n=\"general.template_placeholders\" class=\"mt-5\">The following placeholders are supported</p>\n                        <ul>\n                            <li><span class=\"text-info\">%name%</span>, <span data-i18n=\"virtual_folders.template_name_placeholder\">replaced with the specified folder name</span></li>\n                        </ul>\n                        <p data-i18n=\"virtual_folders.template_help\">The generated folders can be saved or exported. Exported folders can be imported from the \"Maintenance\" section of this SFTPGo instance or another.</p>\n                    </div>\n                </div>\n            </div>\n        </div>\n        {{- end}}\n        {{- template \"errmsg\" .Error}}\n        <form id=\"folder_form\" enctype=\"multipart/form-data\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\" {{if eq .Mode 3}}target=\"_blank\" rel=\"noopener noreferrer\"{{end}}>\n            {{- if eq .Mode 3}}\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"title.folders\" class=\"card-title section-title-inner\">Folders</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"template_folders\">\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"template_folders\">\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]virtual_folders.name\" type=\"text\" class=\"form-control\" name=\"tpl_foldername\" autocomplete=\"off\" spellcheck=\"false\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <input type=\"hidden\" name=\"name\" id=\"idFolderName\" value=\"{{.Folder.Name}}\">\n            {{- else}}\n            <div class=\"form-group row\">\n                <label for=\"idFolderName\" data-i18n=\"general.name\" class=\"col-md-3 col-form-label\">Name</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idFolderName\" type=\"text\" placeholder=\"\" name=\"name\" value=\"{{.Folder.Name}}\" maxlength=\"255\" autocomplete=\"off\"\n                        spellcheck=\"false\" required {{if ge .Mode 2}}class=\"form-control-plaintext readonly-input\" readonly{{else}}class=\"form-control\"{{end}} />\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idDescription\" type=\"text\" class=\"form-control\" name=\"description\" value=\"{{.Folder.Description}}\" maxlength=\"255\">\n                </div>\n            </div>\n\n            {{- template \"fshtml\" .FsWrapper}}\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    $(document).on(\"i18nload\", function(){\n        onFilesystemChanged('{{.Folder.FsConfig.Provider}}');\n\n        $('#idFilesystem').on(\"change\", function(){\n            onFilesystemChanged(this.value);\n        });\n    });\n\n    $(document).on(\"i18nshow\", function(){\n        //{{- if eq .Mode 3}}\n        initRepeater('#template_folders');\n        initRepeaterItems();\n        //{{- end}}\n\n        $(\"#folder_form\").submit(function (event) {\n            //{{- if ne .Mode 3}}\n            let submitButton = document.querySelector('#form_submit');\n            submitButton.setAttribute('data-kt-indicator', 'on');\n            submitButton.disabled = true;\n            //{{- end}}\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/folders.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"virtual_folders.view_manage\" class=\"card-title section-title\">View and manage folders</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                    <button id=\"export_button\" type=\"button\" class=\"btn btn-light-primary ms-3\" data-table-filter=\"export\">\n                        <span data-i18n=\"general.export\" class=\"indicator-label\">\n                            Export\n                        </span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                </div>\n\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <button type=\"button\" class=\"btn btn-light-primary rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom\" data-kt-menu-permanent=\"true\">\n                        <span data-i18n=\"general.colvis\">Column visibility</span>\n                        <i class=\"ki-duotone ki-down fs-3 rotate-180 ms-3 me-0\"></i>\n                    </button>\n                    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4\" data-kt-menu=\"true\">\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColStorage\" />\n                            <label class=\"form-check-label\" for=\"checkColStorage\">\n                                <span data-i18n=\"storage.label\" class=\"text-gray-800 fs-6\">Storage</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColQuota\" />\n                            <label class=\"form-check-label\" for=\"checkColQuota\">\n                                <span data-i18n=\"fs.quota_usage.disk\" class=\"text-gray-800 fs-6\">Disk quota</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColAssociations\" />\n                            <label class=\"form-check-label\" for=\"checkColAssociations\">\n                                <span data-i18n=\"general.associations\" class=\"text-gray-800 fs-6\">Associations</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColDesc\" />\n                            <label class=\"form-check-label\" for=\"checkColDesc\">\n                                <span data-i18n=\"general.description\" class=\"text-gray-800 fs-6\">Description</span>\n                            </label>\n                        </div>\n                    </div>\n                    {{- if .LoggedUser.HasPermission \"manage_folders\"}}\n                    <a href=\"{{.FolderURL}}\" class=\"btn btn-primary ms-5\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                    {{- end}}\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"general.name\">Name</th>\n                        <th data-i18n=\"storage.label\">Storage</th>\n                        <th data-i18n=\"fs.quota_usage.disk\">Disk quota</th>\n                        <th data-i18n=\"general.associations\">Associations</th>\n                        <th data-i18n=\"general.description\">Description</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/papaparse/papaparse.min.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/file-saver/FileSaver.min.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function deleteAction(name) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.FolderURL}}' + \"/\" + encodeURIComponent(name);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.FoldersURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"name\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"filesystem.provider\",\n                        name: \"storage\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case 1:\n                                        return $.t('storage.s3');\n                                    case 2:\n                                        return $.t('storage.gcs');\n                                    case 3:\n                                        return $.t('storage.azblob');\n                                    case 4:\n                                        return $.t('storage.encrypted');\n                                    case 5:\n                                        return $.t('storage.sftp');\n                                    case 6:\n                                        return $.t('storage.http');\n                                    default:\n                                        return $.t('storage.local');\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"used_quota_size\",\n                        name: \"quota\",\n                        visible: false,\n                        searchable: false,\n                        orderable: false,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                let val = \"\";\n                                if (row.used_quota_size) {\n                                    let usage = fileSizeIEC(row.used_quota_size);\n                                    val += $.t('fs.quota_usage.size', {val: usage})+\". \";\n                                }\n                                if (row.used_quota_files){\n                                    val += $.t('fs.quota_usage.files', {val: row.used_quota_files});\n                                }\n                                return val\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"users\",\n                        name: \"associations\",\n                        defaultContent: \"\",\n                        visible: false,\n                        searchable: false,\n                        orderable: false,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                let users = 0;\n                                if (row.users){\n                                    users = row.users.length;\n                                }\n                                let groups = 0;\n                                if (row.groups){\n                                    groups = row.groups.length;\n                                }\n                                return $.t('virtual_folders.associations_summary', {users: users, groups: groups});\n                            }\n                            return \"\";\n                        }\n                    },\n                    {\n                        data: \"description\",\n                        name: \"description\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let numActions = 0;\n                                let actions = `<button class=\"btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                                    <span data-i18n=\"general.actions\" class=\"fs-6\">Actions</span>\n\t\t\t\t\t\t\t\t\t\t            <i class=\"ki-duotone ki-down fs-5 ms-1 rotate-180\"></i>\n                                                </button>\n                                                <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">`;\n                                //{{- if .LoggedUser.HasPermission \"manage_folders\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.template\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"template_row\">Template</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n                                             <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                if (numActions > 0){\n                                    actions+=`</div>`;\n                                    return actions;\n                                }\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                colReorder: {\n                    enable: true,\n                    fixedColumnsLeft: 1\n                },\n                stateLoadParams: function (settings, data) {\n                        if (data.search.search){\n                            const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                            filterSearch.value = data.search.search;\n                        }\n                    },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n            dt.on('columns-reordered', function(e, settings, details){\n                drawAction();\n            });\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        function handleColVisibilityCheckbox(el, selector) {\n            let index = dt.column(selector).index();\n            el.off(\"change\");\n            el.prop('checked', dt.column(index).visible());\n            el.on(\"change\", function(e){\n                let index = dt.column(selector).index();\n                dt.column(index).visible($(this).is(':checked'));\n                dt.draw('page');\n            });\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n            handleColVisibilityCheckbox($('#checkColStorage'), \"storage:name\");\n            handleColVisibilityCheckbox($('#checkColQuota'), \"quota:name\");\n            handleColVisibilityCheckbox($('#checkColAssociations'), \"associations:name\");\n            handleColVisibilityCheckbox($('#checkColDesc'), \"description:name\");\n\n            const exportButton = $(document.querySelector('[data-table-filter=\"export\"]'));\n            exportButton.on('click', function(e){\n                e.preventDefault();\n                this.blur();\n                this.setAttribute('data-kt-indicator', 'on');\n\t\t\t    this.disabled = true;\n\n                let data = [];\n                dt.rows({ search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                    let line = {};\n                    let rowData = dt.row(rowIdx).data();\n\n                    line[\"Name\"] = rowData[\"name\"];\n                    let fsProvider = \"Local storage\";\n                    if (rowData[\"mapped_path\"]){\n                        fsProvider+=\": \"+rowData[\"mapped_path\"];\n                    }\n                    switch (rowData[\"filesystem\"][\"provider\"]){\n                        case 1:\n                            fsProvider = \"S3 Compatible\";\n                            break;\n                        case 2:\n                            fsProvider = \"Google Cloud Storage\";\n                            break;\n                        case 3:\n                            fsProvider = \"Azure Blob\";\n                            break;\n                        case 4:\n                            fsProvider = \"Local storage encrypted: \"+rowData[\"home_dir\"];\n                            break;\n                        case 5:\n                            fsProvider = \"SFTP\";\n                            break;\n                        case 6:\n                            fsProvider = \"HTTP\";\n                            break;\n                    }\n                    line[\"Filesystem\"] = fsProvider;\n\n                    if (rowData[\"users\"]){\n                        line[\"Users\"] = rowData[\"users\"].join(\", \");\n                    } else {\n                        line[\"Users\"] = \"\";\n                    }\n                    if (rowData[\"groups\"]){\n                        line[\"Groups\"] = rowData[\"groups\"].join(\", \");\n                    } else {\n                        line[\"Groups\"] = \"\";\n                    }\n                    if (rowData[\"description\"]){\n                        line[\"Description\"] = rowData[\"description\"];\n                    } else {\n                        line[\"Description\"] = \"\";\n                    }\n\n                    data.push(line);\n                });\n\n                let csv = Papa.unparse(data, {\n                    quotes: false,\n\t                quoteChar: '\"',\n\t                escapeChar: '\"',\n\t                delimiter: \",\",\n\t                header: true,\n\t                newline: \"\\r\\n\",\n\t                skipEmptyLines: false,\n\t                columns: null,\n                    escapeFormulae: true\n                });\n                let blob = new Blob([csv], {type: \"text/csv\"});\n                let ts = moment().format(\"YYYY_MM_DD_HH_mm_ss\");\n                saveAs(blob, `folders_${ts}.csv`);\n\n                this.removeAttribute('data-kt-indicator');\n                this.disabled = false;\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.FolderURL}}' + \"/\" + encodeURIComponent(rowData['name']));\n                });\n            });\n\n            const templateButtons = document.querySelectorAll('[data-table-action=\"template_row\"]');\n            templateButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.FolderTemplateURL}}' + \"?from=\" + encodeURIComponent(rowData['name']));\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    deleteAction(dt.row(parent).data()['name']);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/fsconfig.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- define \"fshtml\"}}\n<div class=\"card mt-10 {{if .IsHidden}}d-none{{end}}\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"storage.title\" class=\"card-title section-title-inner\">File system</h3>\n    </div>\n    <div class=\"card-body\">\n        <div class=\"form-group row\">\n            <label for=\"idFilesystem\" data-i18n=\"storage.label\" class=\"col-md-3 col-form-label\">Storage</label>\n            <div class=\"col-md-9\">\n                <select id=\"idFilesystem\" name=\"fs_provider\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                    <option value=\"0\" data-i18n=\"storage.local\" {{if eq .Provider 0 }}selected{{end}}>Local</option>\n                    <option value=\"4\" data-i18n=\"storage.encrypted\" {{if eq .Provider 4 }}selected{{end}}>Local encrypted</option>\n                    <option value=\"1\" data-i18n=\"storage.s3\" {{if eq .Provider 1 }}selected{{end}}>AWS S3 (Compatible)</option>\n                    <option value=\"2\" data-i18n=\"storage.gcs\" {{if eq .Provider 2 }}selected{{end}}>Google Cloud Storage</option>\n                    <option value=\"3\" data-i18n=\"storage.azblob\" {{if eq .Provider 3 }}selected{{end}}>Azure Blob Storage</option>\n                    <option value=\"5\" data-i18n=\"storage.sftp\" {{if eq .Provider 5 }}selected{{end}}>SFTP</option>\n                    <option value=\"6\" data-i18n=\"storage.http\" {{if eq .Provider 6 }}selected{{end}}>HTTP</option>\n                </select>\n            </div>\n        </div>\n        {{- if or .IsUserPage .IsGroupPage}}\n        <div class=\"form-group row mt-10\">\n            <label for=\"idHomeDir\" data-i18n=\"storage.home_dir\" class=\"col-md-3 col-form-label\">Home Dir</label>\n            <div class=\"col-md-9\">\n                <input id=\"idHomeDir\" type=\"text\" data-i18n=\"[placeholder]storage.home_dir_placeholder\" class=\"form-control\" name=\"home_dir\" value=\"{{.DirPath}}\" aria-describedby=\"idHomeDirHelp\" />\n                <div id=\"idHomeDirHelp\" class=\"form-text\" {{if not .DirPath}}{{if .HasUsersBaseDir}}data-i18n=\"storage.home_dir_help1\"{{else}}{{if .IsGroupPage}}data-i18n=\"storage.home_dir_help2\"{{else}}data-i18n=\"storage.home_dir_help3\"{{end}}{{end}}{{end}}></div>\n            </div>\n        </div>\n        {{- else}}\n        <div class=\"form-group row mt-10\">\n            <label for=\"idMappedPath\" data-i18n=\"storage.home_dir\" class=\"col-md-3 col-form-label\">Home Dir</label>\n            <div class=\"col-md-9\">\n                <input id=\"idMappedPath\" type=\"text\" data-i18n=\"[placeholder]storage.home_dir_placeholder\" class=\"form-control\" name=\"mapped_path\" value=\"{{.DirPath}}\" aria-describedby=\"idMappedPathHelp\" />\n                <div id=\"idMappedPathHelp\" class=\"form-text\" {{if not .DirPath}}{{if .HasUsersBaseDir}}data-i18n=\"storage.home_dir_help1\"{{else}}{{if .IsGroupPage}}data-i18n=\"storage.home_dir_help2\"{{else}}data-i18n=\"storage.home_dir_help3\"{{end}}{{end}}{{end}}></div>\n            </div>\n        </div>\n        {{- end}}\n        <div class=\"form-group row mt-10 fsconfig-local\">\n            <label for=\"idOsReadBufferSize\" data-i18n=\"storage.os_read_buffer\" class=\"col-md-3 col-form-label\">Read buffer (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idOsReadBufferSize\" type=\"number\" min=\"0\" max=\"10\" class=\"form-control\" name=\"osfs_read_buffer_size\" value=\"{{.OSConfig.ReadBufferSize}}\" aria-describedby=\"idOsReadBufferSizeHelp\" />\n                <div id=\"idOsReadBufferSizeHelp\" class=\"form-text\" data-i18n=\"storage.os_buffer_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idOsWriteBufferSize\" data-i18n=\"storage.os_write_buffer\" class=\"col-md-2 col-form-label\">Write buffer (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idOsWriteBufferSize\" type=\"number\" min=\"0\" max=\"10\" class=\"form-control\" name=\"osfs_write_buffer_size\" value=\"{{.OSConfig.WriteBufferSize}}\" aria-describedby=\"idOsWriteBufferSizeHelp\" />\n                <div id=\"idOsWriteBufferSizeHelp\" class=\"form-text\" data-i18n=\"storage.os_buffer_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3Bucket\" data-i18n=\"storage.bucket\" class=\"col-md-3 col-form-label\">Bucket</label>\n            <div class=\"col-md-9\">\n                <input id=\"idS3Bucket\" type=\"text\" class=\"form-control\" name=\"s3_bucket\" value=\"{{.S3Config.Bucket}}\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3Region\" data-i18n=\"storage.region\" class=\"col-md-3 col-form-label\">Region</label>\n            <div class=\"col-md-9\">\n                <input id=\"idS3Region\" type=\"text\" class=\"form-control\" name=\"s3_region\" value=\"{{.S3Config.Region}}\" maxlength=\"512\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3AccessKey\" data-i18n=\"storage.access_key\" class=\"col-md-3 col-form-label\">Access Key</label>\n            <div class=\"col-md-9\">\n                <input id=\"idS3AccessKey\" type=\"text\" class=\"form-control\" name=\"s3_access_key\" value=\"{{.S3Config.AccessKey}}\" maxlength=\"512\" spellcheck=\"false\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3AccessSecret\" data-i18n=\"storage.access_secret\" class=\"col-md-3 col-form-label\">Access Secret</label>\n            <div class=\"col-md-9\">\n                <input id=\"idS3AccessSecret\" type=\"password\" class=\"form-control\" name=\"s3_access_secret\" autocomplete=\"new-password\" spellcheck=\"false\"\n                    value=\"{{if .S3Config.AccessSecret.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.S3Config.AccessSecret.GetPayload}}{{end}}\"/>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3KeyPrefix\" data-i18n=\"storage.key_prefix\" class=\"col-md-3 col-form-label\">Key Prefix</label>\n            <div class=\"col-md-9\">\n                <input id=\"idS3KeyPrefix\" type=\"text\" class=\"form-control\" name=\"s3_key_prefix\" value=\"{{.S3Config.KeyPrefix}}\" aria-describedby=\"idS3KeyPrefixHelp\"/>\n                <div id=\"idS3KeyPrefixHelp\" class=\"form-text\" data-i18n=\"storage.key_prefix_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3Endpoint\" data-i18n=\"storage.endpoint\" class=\"col-md-3 col-form-label\">Endpoint</label>\n            <div class=\"col-md-9\">\n                <input id=\"idS3Endpoint\" type=\"text\" class=\"form-control\" name=\"s3_endpoint\" value=\"{{.S3Config.Endpoint}}\" aria-describedby=\"idS3EndpointHelp\"/>\n                <div id=\"idS3EndpointHelp\" class=\"form-text\" data-i18n=\"storage.endpoint_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3RoleARN\" data-i18n=\"storage.role_arn\" class=\"col-md-3 col-form-label\">Role ARN</label>\n            <div class=\"col-md-9\">\n                <input id=\"idS3RoleARN\" type=\"text\" class=\"form-control\" name=\"s3_role_arn\" value=\"{{.S3Config.RoleARN}}\" aria-describedby=\"idS3RoleARNHelp\"/>\n                <div id=\"idS3RoleARNHelp\" class=\"form-text\" data-i18n=\"storage.role_arn_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3StorageClass\" data-i18n=\"storage.class\" class=\"col-md-3 col-form-label\">Storage Class</label>\n            <div class=\"col-md-3\">\n                <input id=\"idS3StorageClass\" type=\"text\" class=\"form-control\" name=\"s3_storage_class\" value=\"{{.S3Config.StorageClass}}\" aria-describedby=\"idS3StorageClassHelp\" />\n                <div id=\"idS3StorageClassHelp\" class=\"form-text\" data-i18n=\"general.blank_default_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idS3ACL\" data-i18n=\"storage.acl\" class=\"col-md-2 col-form-label\">ACL</label>\n            <div class=\"col-md-3\">\n                <input id=\"idS3ACL\" type=\"text\" class=\"form-control\" name=\"s3_acl\" value=\"{{.S3Config.ACL}}\" aria-describedby=\"idS3ACLHelp\" />\n                <div id=\"idS3ACLHelp\" class=\"form-text\" data-i18n=\"general.blank_default_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3PartSize\" data-i18n=\"storage.ul_part_size\" class=\"col-md-3 col-form-label\">Upload Part Size (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idS3PartSize\" type=\"number\" min=\"0\" max=\"5000\" class=\"form-control\" name=\"s3_upload_part_size\" value=\"{{.S3Config.UploadPartSize}}\" aria-describedby=\"idS3PartSizeHelp\" />\n                <div id=\"idS3PartSizeHelp\" class=\"form-text\" data-i18n=\"storage.part_size_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idS3UploadConcurrency\" data-i18n=\"storage.ul_concurrency\" class=\"col-md-2 col-form-label\">Upload concurrency</label>\n            <div class=\"col-md-3\">\n                <input id=\"idS3UploadConcurrency\" type=\"number\" min=\"0\" max=\"64\" class=\"form-control\" name=\"s3_upload_concurrency\" value=\"{{.S3Config.UploadConcurrency}}\" aria-describedby=\"idS3UploadConcurrencyHelp\" />\n                <div id=\"idS3UploadConcurrencyHelp\" class=\"form-text\" data-i18n=\"storage.ul_concurrency_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3DLPartSize\" data-i18n=\"storage.dl_part_size\" class=\"col-md-3 col-form-label\">Download Part Size (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idS3DLPartSize\" type=\"number\" min=\"0\" max=\"5000\" class=\"form-control\" name=\"s3_download_part_size\" value=\"{{.S3Config.DownloadPartSize}}\" aria-describedby=\"idS3DLPartSizeHelp\" />\n                <div id=\"idS3DLPartSizeHelp\" class=\"form-text\" data-i18n=\"storage.part_size_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idS3DownloadConcurrency\" data-i18n=\"storage.dl_concurrency\" class=\"col-md-2 col-form-label\">Download concurrency</label>\n            <div class=\"col-md-3\">\n                <input id=\"idS3DownloadConcurrency\" type=\"number\" min=\"0\" max=\"64\" class=\"form-control\" name=\"s3_download_concurrency\" value=\"{{.S3Config.DownloadConcurrency}}\" aria-describedby=\"idS3DownloadConcurrencyHelp\" />\n                <div id=\"idS3DownloadConcurrencyHelp\" class=\"form-text\" data-i18n=\"storage.dl_concurrency_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3UploadTimeout\" data-i18n=\"storage.ul_part_timeout\" class=\"col-md-3 col-form-label\">UL Part Timeout (secs)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idS3UploadTimeout\" type=\"number\" min=\"0\" class=\"form-control\" name=\"s3_upload_part_max_time\" value=\"{{.S3Config.UploadPartMaxTime}}\" aria-describedby=\"idS3UploadTimeoutHelp\" />\n                <div id=\"idS3UploadTimeoutHelp\" class=\"form-text\" data-i18n=\"storage.ul_part_timeout_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idS3DownloadTimeout\" data-i18n=\"storage.dl_part_timeout\" class=\"col-md-2 col-form-label\">DL Part Timeout (secs)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idS3DownloadTimeout\" type=\"number\" min=\"0\" class=\"form-control\" name=\"s3_download_part_max_time\" value=\"{{.S3Config.DownloadPartMaxTime}}\" aria-describedby=\"idS3DownloadTimeoutHelp\" />\n                <div id=\"idS3DownloadTimeoutHelp\" class=\"form-text\" data-i18n=\"storage.dl_part_timeout_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-s3\">\n            <label for=\"idS3SSECustomerKey\" data-i18n=\"storage.sse_customer_key\" class=\"col-md-3 col-form-label\">SSE Customer Key</label>\n            <div class=\"col-md-9\">\n                <input id=\"idS3SSECustomerKey\" type=\"password\" class=\"form-control\" name=\"s3_sse_customer_key\" autocomplete=\"new-password\" spellcheck=\"false\"\n                    value=\"{{if .S3Config.SSECustomerKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.S3Config.SSECustomerKey.GetPayload}}{{end}}\" aria-describedby=\"idS3SSECustomerKeyHelp\"/>\n                <div id=\"idS3SSECustomerKeyHelp\" class=\"form-text\" data-i18n=\"storage.sse_customer_key_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row align-items-center mt-10 fsconfig-s3\">\n            <div class=\"col-md-5\">\n                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idS3ForcePathStyle\" name=\"s3_force_path_style\" {{if .S3Config.ForcePathStyle}}checked{{end}}/>\n                    <label data-i18n=\"storage.s3_path_style\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idS3ForcePathStyle\">\n                        Use path-style addressing, i.e., \"`endpoint`/BUCKET/KEY\"\n                    </label>\n                </div>\n            </div>\n            <div class=\"col-md-2\"></div>\n            <div class=\"col-md-5\">\n                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idS3SkipTLSVerify\" name=\"s3_skip_tls_verify\" {{if .S3Config.SkipTLSVerify}}checked{{end}}/>\n                    <label data-i18n=\"general.skip_tls_verify\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idS3SkipTLSVerify\">\n                        Skip TLS verify. This should be used only for testing\n                    </label>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-gcs\">\n            <label for=\"idGCSBucket\" data-i18n=\"storage.bucket\" class=\"col-md-3 col-form-label\">Bucket</label>\n            <div class=\"col-md-9\">\n                <input id=\"idGCSBucket\" type=\"text\" class=\"form-control\" name=\"gcs_bucket\" value=\"{{.GCSConfig.Bucket}}\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-gcs\">\n            <label for=\"idGCSCredentialFile\" data-i18n=\"storage.credentials_file\" class=\"col-md-3 col-form-label\">Credential files</label>\n            <div class=\"col-md-9\">\n                <input id=\"idGCSCredentialFile\" type=\"file\" accept=\"application/json\" class=\"form-control\" name=\"gcs_credential_file\" aria-describedby=\"idGCSCredentialFileHelp\" />\n                <div id=\"idGCSCredentialFileHelp\" class=\"form-text\" data-i18n=\"storage.credentials_file_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row align-items-center mt-10 fsconfig-gcs\">\n            <label data-i18n=\"storage.auto_credentials\" class=\"col-md-3 col-form-label\" for=\"idGCSAutoCredentials\">Automatic credentials</label>\n            <div class=\"col-md-9\">\n                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idGCSAutoCredentials\" name=\"gcs_auto_credentials\" {{if gt .GCSConfig.AutomaticCredentials 0}}checked{{end}}/>\n                    <label data-i18n=\"storage.auto_credentials_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idGCSAutoCredentials\">\n                        Use default application credentials or credentials from environment\n                    </label>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-gcs\">\n            <label for=\"idGCSKeyPrefix\" data-i18n=\"storage.key_prefix\" class=\"col-md-3 col-form-label\">Key Prefix</label>\n            <div class=\"col-md-9\">\n                <input id=\"idGCSKeyPrefix\" type=\"text\" class=\"form-control\" name=\"gcs_key_prefix\" value=\"{{.GCSConfig.KeyPrefix}}\" aria-describedby=\"idGCSKeyPrefixHelp\" />\n                <div id=\"idGCSKeyPrefixHelp\" class=\"form-text\" data-i18n=\"storage.key_prefix_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-gcs\">\n            <label for=\"idGCSStorageClass\" data-i18n=\"storage.class\" class=\"col-md-3 col-form-label\">Storage Class</label>\n            <div class=\"col-md-3\">\n                <input id=\"idGCSStorageClass\" type=\"text\" class=\"form-control\" name=\"gcs_storage_class\" value=\"{{.GCSConfig.StorageClass}}\" aria-describedby=\"idGCSStorageClassHelp\" />\n                <div id=\"idGCSStorageClassHelp\" class=\"form-text\" data-i18n=\"general.blank_default_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idGCSACL\" data-i18n=\"storage.acl\" class=\"col-md-2 col-form-label\">ACL</label>\n            <div class=\"col-md-3\">\n                <input id=\"idGCSACL\" type=\"text\" class=\"form-control\" name=\"gcs_acl\" value=\"{{.GCSConfig.ACL}}\" aria-describedby=\"idGCSACLHelp\"/>\n                <div id=\"idGCSACLHelp\" class=\"form-text\" data-i18n=\"general.blank_default_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-gcs\">\n            <label for=\"idGCSUploadPartSize\" data-i18n=\"storage.ul_part_size\" class=\"col-md-3 col-form-label\">Upload Part Size (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idGCSUploadPartSize\" type=\"number\" min=\"0\" class=\"form-control\" name=\"gcs_upload_part_size\" value=\"{{.GCSConfig.UploadPartSize}}\" aria-describedby=\"idGCSUploadPartSizeHelp\" />\n                <div id=\"idGCSUploadPartSizeHelp\" class=\"form-text\" data-i18n=\"storage.gcs_part_size_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idGCSUploadTimeout\" data-i18n=\"storage.ul_part_timeout\" class=\"col-md-2 col-form-label\">UL Part Timeout (secs)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idGCSUploadTimeout\" type=\"number\" min=\"0\" class=\"form-control\" name=\"gcs_upload_part_max_time\" value=\"{{.GCSConfig.UploadPartMaxTime}}\" aria-describedby=\"idGCSUploadTimeoutHelp\" />\n                <div id=\"idGCSUploadTimeoutHelp\" class=\"form-text\" data-i18n=\"storage.gcs_ul_part_timeout_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzContainer\" data-i18n=\"storage.container\" class=\"col-md-3 col-form-label\">Container</label>\n            <div class=\"col-md-9\">\n                <input id=\"idAzContainer\" type=\"text\" class=\"form-control\" name=\"az_container\" value=\"{{.AzBlobConfig.Container}}\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzAccountName\" data-i18n=\"storage.account_name\" class=\"col-md-3 col-form-label\">Account Name</label>\n            <div class=\"col-md-9\">\n                <input id=\"idAzAccountName\" type=\"text\" class=\"form-control\" name=\"az_account_name\" value=\"{{.AzBlobConfig.AccountName}}\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzAccountKey\" data-i18n=\"storage.account_key\" class=\"col-md-3 col-form-label\">Account Key</label>\n            <div class=\"col-md-9\">\n                <input id=\"idAzAccountKey\" type=\"password\" class=\"form-control\" name=\"az_account_key\" autocomplete=\"new-password\" spellcheck=\"false\"\n                    value=\"{{if .AzBlobConfig.AccountKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.AccountKey.GetPayload}}{{end}}\"/>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzSASURL\" data-i18n=\"storage.sas_url\" class=\"col-md-3 col-form-label\">SAS URL</label>\n            <div class=\"col-md-9\">\n                <input id=\"idAzSASURL\" type=\"password\" class=\"form-control\" name=\"az_sas_url\" autocomplete=\"new-password\" spellcheck=\"false\" aria-describedby=\"idAzSASURLHelp\"\n                    value=\"{{if .AzBlobConfig.SASURL.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.SASURL.GetPayload}}{{end}}\"/>\n                <div id=\"idAzSASURLHelp\" class=\"form-text\" data-i18n=\"storage.sas_url_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzKeyPrefix\" data-i18n=\"storage.key_prefix\" class=\"col-md-3 col-form-label\">Key Prefix</label>\n            <div class=\"col-md-9\">\n                <input id=\"idAzKeyPrefix\" type=\"text\" class=\"form-control\" name=\"az_key_prefix\" value=\"{{.AzBlobConfig.KeyPrefix}}\" aria-describedby=\"idAzKeyPrefixHelp\" />\n                <div id=\"idAzKeyPrefixHelp\" class=\"form-text\" data-i18n=\"storage.key_prefix_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzAccessTier\" data-i18n=\"storage.class\" class=\"col-md-3 col-form-label\">Storage Class</label>\n            <div class=\"col-md-9\">\n                <select id=\"idAzAccessTier\" name=\"az_access_tier\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                    <option value=\"\" {{if eq .AzBlobConfig.AccessTier \"\" }}selected{{end}}>Default</option>\n                    <option value=\"Hot\" {{if eq .AzBlobConfig.AccessTier \"Hot\" }}selected{{end}}>Hot</option>\n                    <option value=\"Cool\" {{if eq .AzBlobConfig.AccessTier \"Cool\" }}selected{{end}}>Cool</option>\n                    <option value=\"Archive\" {{if eq .AzBlobConfig.AccessTier \"Archive\" }}selected{{end}}>Archive</option>\n                </select>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzUploadPartSize\" data-i18n=\"storage.ul_part_size\" class=\"col-md-3 col-form-label\">Upload Part Size (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idAzUploadPartSize\" type=\"number\" min=\"0\" max=\"2000\" class=\"form-control\" name=\"az_upload_part_size\" value=\"{{.AzBlobConfig.UploadPartSize}}\" aria-describedby=\"idAzUploadPartSizeHelp\" />\n                <div id=\"idAzUploadPartSizeHelp\" class=\"form-text\" data-i18n=\"storage.part_size_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idAzUploadConcurrency\" data-i18n=\"storage.ul_concurrency\" class=\"col-md-2 col-form-label\">Upload concurrency</label>\n            <div class=\"col-md-3\">\n                <input id=\"idAzUploadConcurrency\" type=\"number\" min=\"0\" max=\"64\" class=\"form-control\" name=\"az_upload_concurrency\" value=\"{{.AzBlobConfig.UploadConcurrency}}\" aria-describedby=\"idAzUploadConcurrencyHelp\" />\n                <div id=\"idAzUploadConcurrencyHelp\" class=\"form-text\" data-i18n=\"storage.ul_concurrency_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzDownloadPartSize\" data-i18n=\"storage.dl_part_size\" class=\"col-md-3 col-form-label\">Download Part Size (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idAzDownloadPartSize\" type=\"number\" min=\"0\" max=\"2000\" class=\"form-control\" name=\"az_download_part_size\" value=\"{{.AzBlobConfig.DownloadPartSize}}\" aria-describedby=\"idAzDownloadPartSizeHelp\" />\n                <div id=\"idAzDownloadPartSizeHelp\" class=\"form-text\" data-i18n=\"storage.part_size_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idAzDownloadConcurrency\" data-i18n=\"storage.dl_concurrency\" class=\"col-md-2 col-form-label\">Download concurrency</label>\n            <div class=\"col-md-3\">\n                <input id=\"idAzDownloadConcurrency\" type=\"number\" min=\"0\" max=\"64\" class=\"form-control\" name=\"az_download_concurrency\" value=\"{{.AzBlobConfig.DownloadConcurrency}}\" aria-describedby=\"idAzDownloadConcurrencyHelp\"/>\n                <div id=\"idAzDownloadConcurrencyHelp\" class=\"form-text\" data-i18n=\"storage.dl_concurrency_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-azblob\">\n            <label for=\"idAzEndpoint\" data-i18n=\"storage.endpoint\" class=\"col-md-3 col-form-label\">Endpoint</label>\n            <div class=\"col-md-9\">\n                <input id=\"idAzEndpoint\" type=\"text\" class=\"form-control\" name=\"az_endpoint\" value=\"{{.AzBlobConfig.Endpoint}}\" aria-describedby=\"idAzEndpointHelp\" />\n                <div id=\"idAzEndpointHelp\" class=\"form-text\" data-i18n=\"general.blank_default_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row align-items-center mt-10 fsconfig-azblob\">\n            <label data-i18n=\"storage.emulator\" class=\"col-md-3 col-form-label\" for=\"idUseEmulator\">Use Azure Blob emulator</label>\n            <div class=\"col-md-9\">\n                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idUseEmulator\" name=\"az_use_emulator\" {{if .AzBlobConfig.UseEmulator}}checked{{end}}/>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-crypt\">\n            <label for=\"idCryptPassphrase\" data-i18n=\"storage.passphrase\" class=\"col-md-3 col-form-label\">Passphrase</label>\n            <div class=\"col-md-9\">\n                <input id=\"idCryptPassphrase\" type=\"password\" class=\"form-control\" name=\"crypt_passphrase\" autocomplete=\"new-password\" spellcheck=\"false\" aria-describedby=\"idCryptPassphraseHelp\"\n                    value=\"{{if .CryptConfig.Passphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.CryptConfig.Passphrase.GetPayload}}{{end}}\"/>\n                <div id=\"idCryptPassphraseHelp\" class=\"form-text\" data-i18n=\"storage.passphrase_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-crypt\">\n            <label for=\"idCryptFsReadBufferSize\" data-i18n=\"storage.os_read_buffer\" class=\"col-md-3 col-form-label\">Read buffer (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idCryptFsReadBufferSize\" type=\"number\" min=\"0\" max=\"10\" class=\"form-control\" name=\"cryptfs_read_buffer_size\" value=\"{{.CryptConfig.ReadBufferSize}}\" aria-describedby=\"idCryptFsReadBufferSizeHelp\" />\n                <div id=\"idCryptFsReadBufferSizeHelp\" class=\"form-text\" data-i18n=\"storage.os_buffer_help\"></div>\n            </div>\n            <div class=\"col-md-1\"></div>\n            <label for=\"idCryptFsWriteBufferSize\" data-i18n=\"storage.os_write_buffer\" class=\"col-md-2 col-form-label\">Write buffer (MB)</label>\n            <div class=\"col-md-3\">\n                <input id=\"idCryptFsWriteBufferSize\" type=\"number\" min=\"0\" max=\"10\" class=\"form-control\" name=\"cryptfs_write_buffer_size\" value=\"{{.CryptConfig.WriteBufferSize}}\" aria-describedby=\"idCryptFsWriteBufferSizeHelp\" />\n                <div id=\"idCryptFsWriteBufferSizeHelp\" class=\"form-text\" data-i18n=\"storage.os_buffer_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-sftp\">\n            <label for=\"idSFTPEndpoint\" data-i18n=\"storage.endpoint\" class=\"col-md-3 col-form-label\">Endpoint</label>\n            <div class=\"col-md-9\">\n                <input id=\"idSFTPEndpoint\" type=\"text\" class=\"form-control\" name=\"sftp_endpoint\" value=\"{{.SFTPConfig.Endpoint}}\" aria-describedby=\"idSFTPEndpointHelp\"/>\n                <div id=\"idSFTPEndpointHelp\" class=\"form-text\" data-i18n=\"storage.sftp_endpoint_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-sftp\">\n            <label for=\"idSFTPUsername\" data-i18n=\"login.username\" class=\"col-md-3 col-form-label\">Username</label>\n            <div class=\"col-md-9\">\n                <input id=\"idSFTPUsername\" type=\"text\" class=\"form-control\" name=\"sftp_username\" value=\"{{.SFTPConfig.Username}}\" spellcheck=\"false\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-sftp\">\n            <label for=\"idSFTPPassword\" data-i18n=\"login.password\" class=\"col-md-3 col-form-label\">Password</label>\n            <div class=\"col-md-9\">\n                <input id=\"idSFTPPassword\" type=\"password\" class=\"form-control\" name=\"sftp_password\" autocomplete=\"new-password\" spellcheck=\"false\"\n                    value=\"{{if .SFTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.Password.GetPayload}}{{end}}\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-sftp\">\n            <label for=\"idSFTPPrivateKey\" data-i18n=\"general.private_key\" class=\"col-md-3 col-form-label\">Private key</label>\n            <div class=\"col-md-9\">\n                <textarea class=\"form-control\" id=\"idSFTPPrivateKey\" name=\"sftp_private_key\" spellcheck=\"false\"\n                    rows=\"3\">{{if .SFTPConfig.PrivateKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.PrivateKey.GetPayload}}{{end}}</textarea>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-sftp\">\n            <label for=\"idSFTPPassphrase\" data-i18n=\"storage.passphrase\" class=\"col-md-3 col-form-label\">Passphrase</label>\n            <div class=\"col-md-9\">\n                <input id=\"idSFTPPassphrase\" type=\"password\" class=\"form-control\" name=\"sftp_key_passphrase\" autocomplete=\"new-password\" spellcheck=\"false\" aria-describedby=\"idSFTPPassphraseHelp\"\n                    value=\"{{if .SFTPConfig.KeyPassphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.KeyPassphrase.GetPayload}}{{end}}\" />\n                <div id=\"idSFTPPassphraseHelp\" class=\"form-text\" data-i18n=\"storage.passphrase_key_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-sftp\">\n            <label for=\"idSFTPPrefix\" data-i18n=\"storage.sftp_home_dir\" class=\"col-md-3 col-form-label\">Root directory</label>\n            <div class=\"col-md-9\">\n                <input id=\"idSFTPPrefix\" type=\"text\" class=\"form-control\" name=\"sftp_prefix\" value=\"{{.SFTPConfig.Prefix}}\" aria-describedby=\"idSFTPPrefixHelp\"/>\n                <div id=\"idSFTPPrefixHelp\" class=\"form-text\" data-i18n=\"storage.sftp_home_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-sftp\">\n            <label for=\"idSFTPFingerprints\" data-i18n=\"storage.fingerprints\" class=\"col-md-3 col-form-label\">Fingerprints</label>\n            <div class=\"col-md-9\">\n                <textarea class=\"form-control\" id=\"idSFTPFingerprints\" name=\"sftp_fingerprints\" spellcheck=\"false\" aria-describedby=\"idSFTPFingerprintsHelp\"\n                    rows=\"3\">{{- range .SFTPConfig.Fingerprints}}{{.}}&#010;{{- end}}</textarea>\n                <div id=\"idSFTPFingerprintsHelp\" class=\"form-text\" data-i18n=\"storage.fingerprints_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-sftp\">\n            <label for=\"idSFTPUploadBufferSize\" data-i18n=\"storage.sftp_buffer\" class=\"col-md-3 col-form-label\">Buffer size (MB)</label>\n            <div class=\"col-md-9\">\n                <input id=\"idSFTPUploadBufferSize\" type=\"number\" min=\"0\" max=\"16\" class=\"form-control\" name=\"sftp_buffer_size\" value=\"{{.SFTPConfig.BufferSize}}\" aria-describedby=\"idSFTPUploadBufferSizeHelp\" />\n                <div id=\"idSFTPUploadBufferSizeHelp\" class=\"form-text\" data-i18n=\"storage.os_buffer_help\"></div>\n            </div>\n        </div>\n\n        <div class=\"form-group row align-items-center mt-10 fsconfig-sftp\">\n            <div class=\"col-md-5\">\n                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idDisableConcurrentReads\" name=\"sftp_disable_concurrent_reads\" {{if .SFTPConfig.DisableCouncurrentReads}}checked{{end}}/>\n                    <label data-i18n=\"storage.sftp_concurrent_reads\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idDisableConcurrentReads\">\n                        Disable concurrent reads\n                    </label>\n                </div>\n            </div>\n            <div class=\"col-md-2\"></div>\n            <div class=\"col-md-5\">\n                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idSFTPEqualityCheckMode\" name=\"sftp_equality_check_mode\" {{if eq .SFTPConfig.EqualityCheckMode 1}}checked{{end}}/>\n                    <label data-i18n=\"storage.relaxed_equality_check\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idSFTPEqualityCheckMode\">\n                        Relaxed equality check\n                    </label>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-http\">\n            <label for=\"idHTTPEndpoint\" data-i18n=\"storage.endpoint\" class=\"col-md-3 col-form-label\">Endpoint</label>\n            <div class=\"col-md-9\">\n                <input id=\"idHTTPEndpoint\" type=\"text\" class=\"form-control\" name=\"http_endpoint\" value=\"{{.HTTPConfig.Endpoint}}\"/>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-http\">\n            <label for=\"idHTTPUsername\" data-i18n=\"login.username\" class=\"col-md-3 col-form-label\">Username</label>\n            <div class=\"col-md-9\">\n                <input id=\"idHTTPUsername\" type=\"text\" class=\"form-control\" name=\"http_username\" value=\"{{.HTTPConfig.Username}}\" spellcheck=\"false\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-http\">\n            <label for=\"idHTTPPassword\" data-i18n=\"login.password\" class=\"col-md-3 col-form-label\">Password</label>\n            <div class=\"col-md-9\">\n                <input id=\"idHTTPPassword\" type=\"password\" class=\"form-control\" name=\"http_password\" autocomplete=\"new-password\" spellcheck=\"false\"\n                    value=\"{{if .HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.Password.GetPayload}}{{end}}\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10 fsconfig-http\">\n            <label for=\"idHTTPAPIKey\" data-i18n=\"storage.api_key\" class=\"col-md-3 col-form-label\">API Key</label>\n            <div class=\"col-md-9\">\n                <input id=\"idHTTPAPIKey\" type=\"password\" class=\"form-control\" name=\"http_api_key\" autocomplete=\"new-password\" spellcheck=\"false\"\n                    value=\"{{if .HTTPConfig.APIKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.APIKey.GetPayload}}{{end}}\" />\n            </div>\n        </div>\n\n        <div class=\"form-group row align-items-center mt-10 fsconfig-http\">\n            <div class=\"col-md-5\">\n                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idHTTPSkipTLSVerify\" name=\"http_skip_tls_verify\" {{if .HTTPConfig.SkipTLSVerify}}checked{{end}} />\n                    <label data-i18n=\"general.skip_tls_verify\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idHTTPSkipTLSVerify\">\n                        Skip TLS verify. This should be used only for testing\n                    </label>\n                </div>\n            </div>\n            <div class=\"col-md-2\"></div>\n            <div class=\"col-md-5\">\n                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idHTTPEqualityCheckMode\" name=\"http_equality_check_mode\" {{if eq .HTTPConfig.EqualityCheckMode 1}}checked{{end}}/>\n                    <label data-i18n=\"storage.relaxed_equality_check\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idHTTPEqualityCheckMode\">\n                        Relaxed equality check\n                    </label>\n                </div>\n            </div>\n        </div>\n\n    </div>\n</div>\n{{- end}}\n\n{{- define \"user_group_perms\"}}\n<div class=\"card mt-10\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"filters.directory_patterns\" class=\"card-title section-title-inner\">Per-directory name patterns restrictions</h3>\n    </div>\n    <div class=\"card-body\">\n        <div id=\"directory_patterns\">\n            {{- template \"infomsg-no-mb\" \"filters.directory_patterns_help\"}}\n            <div class=\"form-group\">\n                <div data-repeater-list=\"directory_patterns\">\n                    {{- range $idx, $pattern := .GetFlatFilePatterns -}}\n                    <div data-repeater-item>\n                        <div class=\"form-group row\">\n                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                <input data-i18n=\"[placeholder]filters.directory_path_help\" type=\"text\" class=\"form-control\" name=\"pattern_path\" value=\"{{$pattern.Path}}\" />\n                            </div>\n                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                <input type=\"text\" class=\"form-control\" name=\"patterns\" placeholder=\"*.png,*.zip\" value=\"{{$pattern.GetCommaSeparatedPatterns}}\" />\n                            </div>\n                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                <select name=\"pattern_type\" class=\"form-select select-repetear select-first\" data-hide-search=\"true\">\n                                    <option value=\"denied\" data-i18n=\"general.denied\" {{- if $pattern.IsDenied}} selected{{- end}}>Denied</option>\n                                    <option value=\"allowed\" data-i18n=\"general.allowed\" {{- if $pattern.IsAllowed}} selected{{- end}}>Allowed</option>\n                                </select>\n                            </div>\n                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                <select name=\"pattern_policy\" class=\"form-select select-repetear select-first\" data-hide-search=\"true\">\n                                    <option value=\"0\" data-i18n=\"general.visible\" {{- if eq $pattern.DenyPolicy 0}} selected{{- end}}>Visible</option>\n                                    <option value=\"1\" data-i18n=\"general.hidden\" {{- if eq $pattern.DenyPolicy 1}} selected{{- end}}>Hidden</option>\n                                </select>\n                            </div>\n                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                <a href=\"#\" data-repeater-delete\n                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                        <span class=\"path1\"></span>\n                                        <span class=\"path2\"></span>\n                                        <span class=\"path3\"></span>\n                                        <span class=\"path4\"></span>\n                                        <span class=\"path5\"></span>\n                                    </i>\n                                </a>\n                            </div>\n                        </div>\n                    </div>\n                    {{- else}}\n                    <div data-repeater-item>\n                        <div class=\"form-group row\">\n                            <div class=\"col-md-4 mt-3 mt-md-8\">\n                                <input data-i18n=\"[placeholder]filters.directory_path_help\" type=\"text\" class=\"form-control\" name=\"pattern_path\" value=\"\" />\n                            </div>\n                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                <input type=\"text\" class=\"form-control\" name=\"patterns\" placeholder=\"*.png,*.zip\" value=\"\" />\n                            </div>\n                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                <select name=\"pattern_type\" class=\"form-select select-repetear select-first\" data-hide-search=\"true\">\n                                    <option value=\"denied\" data-i18n=\"general.denied\">Denied</option>\n                                    <option value=\"allowed\" data-i18n=\"general.allowed\">Allowed</option>\n                                </select>\n                            </div>\n                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                <select name=\"pattern_policy\" class=\"form-select select-repetear select-first\" data-hide-search=\"true\">\n                                    <option value=\"0\" data-i18n=\"general.visible\">Visible</option>\n                                    <option value=\"1\" data-i18n=\"general.hidden\">Hidden</option>\n                                </select>\n                            </div>\n                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                <a href=\"#\" data-repeater-delete\n                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                        <span class=\"path1\"></span>\n                                        <span class=\"path2\"></span>\n                                        <span class=\"path3\"></span>\n                                        <span class=\"path4\"></span>\n                                        <span class=\"path5\"></span>\n                                    </i>\n                                </a>\n                            </div>\n                        </div>\n                    </div>\n                    {{- end}}\n                </div>\n            </div>\n\n            <div class=\"form-group mt-5\">\n                <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                    <i class=\"ki-duotone ki-plus fs-3\"></i>\n                    <span data-i18n=\"general.add\">Add</span>\n                </a>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"user_group_access_time\"}}\n<div class=\"card mt-10\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"filters.access_time\" class=\"card-title section-title-inner\">Access time restrictions</h3>\n    </div>\n    <div class=\"card-body\">\n        <div id=\"access_time_restrictions\">\n            {{- template \"infomsg-no-mb\" \"filters.access_time_help\"}}\n            <div class=\"form-group\">\n                <div data-repeater-list=\"access_time_restrictions\">\n                    {{- range $idx, $period := .AccessTime -}}\n                    <div data-repeater-item>\n                        <div data-repeater-item>\n                            <div class=\"form-group row\">\n                                <div class=\"col-md-5 mt-3 mt-md-8\">\n                                    <select name=\"access_time_day_of_week\" class=\"form-select select-repetear select-first\" data-hide-search=\"true\">\n                                        <option value=\"0\" data-i18n=\"general.sunday\" {{- if eq $period.DayOfWeek 0}} selected{{- end}}>Sunday</option>\n                                        <option value=\"1\" data-i18n=\"general.monday\" {{- if eq $period.DayOfWeek 1}} selected{{- end}}>Monday</option>\n                                        <option value=\"2\" data-i18n=\"general.tuesday\" {{- if eq $period.DayOfWeek 2}} selected{{- end}}>Tuesday</option>\n                                        <option value=\"3\" data-i18n=\"general.wednesday\" {{- if eq $period.DayOfWeek 3}} selected{{- end}}>Wednesday</option>\n                                        <option value=\"4\" data-i18n=\"general.thursday\" {{- if eq $period.DayOfWeek 4}} selected{{- end}}>Thursday</option>\n                                        <option value=\"5\" data-i18n=\"general.friday\" {{- if eq $period.DayOfWeek 5}} selected{{- end}}>Friday</option>\n                                        <option value=\"6\" data-i18n=\"general.saturday\" {{- if eq $period.DayOfWeek 6}} selected{{- end}}>Saturday</option>\n                                    </select>\n                                </div>\n                                <div class=\"col-md-3 mt-3 mt-md-8\">\n                                    <input data-i18n=\"[placeholder]general.start\" type=\"text\" class=\"form-control\" name=\"access_time_start\" value=\"{{$period.From}}\" />\n                                </div>\n                                <div class=\"col-md-3 mt-3 mt-md-8\">\n                                    <input data-i18n=\"[placeholder]general.end\" type=\"text\" class=\"form-control\" name=\"access_time_end\" value=\"{{$period.To}}\" />\n                                </div>\n                                <div class=\"col-md-1 mt-3 mt-md-8\">\n                                    <a href=\"#\" data-repeater-delete\n                                        class=\"btn btn-light-danger ps-5 pe-4\">\n                                        <i class=\"ki-duotone ki-trash fs-2\">\n                                            <span class=\"path1\"></span>\n                                            <span class=\"path2\"></span>\n                                            <span class=\"path3\"></span>\n                                            <span class=\"path4\"></span>\n                                            <span class=\"path5\"></span>\n                                        </i>\n                                    </a>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    {{- else}}\n                    <div data-repeater-item>\n                        <div class=\"form-group row\">\n                            <div class=\"col-md-5 mt-3 mt-md-8\">\n                                <select name=\"access_time_day_of_week\" class=\"form-select select-repetear select-first\" data-hide-search=\"true\">\n                                    <option value=\"0\" data-i18n=\"general.sunday\">Sunday</option>\n                                    <option value=\"1\" data-i18n=\"general.monday\">Monday</option>\n                                    <option value=\"2\" data-i18n=\"general.tuesday\">Tuesday</option>\n                                    <option value=\"3\" data-i18n=\"general.wednesday\">Wednesday</option>\n                                    <option value=\"4\" data-i18n=\"general.thursday\">Thursday</option>\n                                    <option value=\"5\" data-i18n=\"general.friday\">Friday</option>\n                                    <option value=\"6\" data-i18n=\"general.saturday\">Saturday</option>\n                                </select>\n                            </div>\n                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                <input data-i18n=\"[placeholder]general.start\" type=\"text\" class=\"form-control\" name=\"access_time_start\" value=\"\" />\n                            </div>\n                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                <input data-i18n=\"[placeholder]general.end\" type=\"text\" class=\"form-control\" name=\"access_time_end\" value=\"\" />\n                            </div>\n                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                <a href=\"#\" data-repeater-delete\n                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                        <span class=\"path1\"></span>\n                                        <span class=\"path2\"></span>\n                                        <span class=\"path3\"></span>\n                                        <span class=\"path4\"></span>\n                                        <span class=\"path5\"></span>\n                                    </i>\n                                </a>\n                            </div>\n                        </div>\n                    </div>\n                    {{- end}}\n                </div>\n            </div>\n\n            <div class=\"form-group mt-5\">\n                <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                    <i class=\"ki-duotone ki-plus fs-3\"></i>\n                    <span data-i18n=\"general.add\">Add</span>\n                </a>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"user_group_quota\"}}\n<div class=\"form-group row mt-10\">\n    <label for=\"idQuotaSize\" data-i18n=\"virtual_folders.quota_size\" class=\"col-md-3 col-form-label\">Quota size</label>\n    <div class=\"col-md-3\">\n        <input id=\"idQuotaSize\" type=\"text\" class=\"form-control\" name=\"quota_size\" value=\"{{HumanizeBytes .QuotaSize}}\" aria-describedby=\"idQuotaSizeHelp\" />\n        <div id=\"idQuotaSizeHelp\" class=\"form-text\" data-i18n=\"virtual_folders.quota_size_help\"></div>\n    </div>\n    <div class=\"col-md-1\"></div>\n    <label for=\"idQuotaFiles\" data-i18n=\"virtual_folders.quota_files\" class=\"col-md-2 col-form-label\">Quota files</label>\n    <div class=\"col-md-3\">\n        <input id=\"idQuotaFiles\" type=\"number\" min=\"0\" class=\"form-control\" name=\"quota_files\" value=\"{{.QuotaFiles}}\" aria-describedby=\"idQuotaFilesHelp\" />\n        <div id=\"idQuotaFilesHelp\" class=\"form-text\" data-i18n=\"general.zero_no_limit_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idMaxUploadSize\" data-i18n=\"filters.max_upload_size\" class=\"col-md-3 col-form-label\">Max file upload size</label>\n    <div class=\"col-md-9\">\n        <input id=\"idMaxUploadSize\" type=\"text\" class=\"form-control\" name=\"max_upload_file_size\" value=\"{{HumanizeBytes .Filters.MaxUploadFileSize}}\" aria-describedby=\"idMaxUploadSizeHelp\" />\n        <div id=\"idMaxUploadSizeHelp\" class=\"form-text\" data-i18n=\"filters.max_upload_size_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idUploadBandwidth\" data-i18n=\"filters.upload_bandwidth\" class=\"col-md-3 col-form-label\">Bandwidth UL (KB/s)</label>\n    <div class=\"col-md-3\">\n        <input id=\"idUploadBandwidth\" type=\"number\" min=\"0\" class=\"form-control\" name=\"upload_bandwidth\" value=\"{{.UploadBandwidth}}\" aria-describedby=\"idUploadBandwidthHelp\" />\n        <div id=\"idUploadBandwidthHelp\" class=\"form-text\" data-i18n=\"general.zero_no_limit_help\"></div>\n    </div>\n    <div class=\"col-md-1\"></div>\n    <label for=\"idDownloadBandwidth\" data-i18n=\"filters.download_bandwidth\" class=\"col-md-2 col-form-label\">Bandwidth DL (KB/s)</label>\n    <div class=\"col-md-3\">\n        <input id=\"idDownloadBandwidth\" type=\"number\" min=\"0\" class=\"form-control\" name=\"download_bandwidth\" value=\"{{.DownloadBandwidth}}\" aria-describedby=\"idDownloadBandwidthHelp\" />\n        <div id=\"idDownloadBandwidthHelp\" class=\"form-text\" data-i18n=\"general.zero_no_limit_help\"></div>\n    </div>\n</div>\n\n<div class=\"card mt-10\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"filters.src_bandwidth_limit\" class=\"card-title section-title-inner\">Per-source bandwidth speed limits</h3>\n    </div>\n    <div class=\"card-body\">\n        <div id=\"src_bandwidth_limits\">\n            <div class=\"form-group\">\n                <div data-repeater-list=\"src_bandwidth_limits\">\n                    {{- range $idx, $bwLimit := .Filters.BandwidthLimits -}}\n                    <div data-repeater-item>\n                        <div class=\"form-group row\">\n                            <div class=\"col-md-7 mt-3 mt-md-8\">\n                                <textarea class=\"form-control\" name=\"bandwidth_limit_sources\" rows=\"4\">{{$bwLimit.GetSourcesAsString}}</textarea>\n                                <div class=\"form-text\" data-i18n=\"general.ip_mask_help\"></div>\n                            </div>\n                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                <input type=\"number\" min=\"0\" class=\"form-control\" name=\"upload_bandwidth_source\" value=\"{{$bwLimit.UploadBandwidth}}\" />\n                                <div class=\"form-text\" data-i18n=\"filters.upload_bandwidth_help\"></div>\n                            </div>\n                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                <input type=\"number\" min=\"0\" class=\"form-control\" name=\"download_bandwidth_source\" value=\"{{$bwLimit.DownloadBandwidth}}\" />\n                                <div class=\"form-text\" data-i18n=\"filters.download_bandwidth_help\"></div>\n                            </div>\n                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                <a href=\"#\" data-repeater-delete\n                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                        <span class=\"path1\"></span>\n                                        <span class=\"path2\"></span>\n                                        <span class=\"path3\"></span>\n                                        <span class=\"path4\"></span>\n                                        <span class=\"path5\"></span>\n                                    </i>\n                                </a>\n                            </div>\n                        </div>\n                    </div>\n                    {{- else}}\n                    <div data-repeater-item>\n                        <div class=\"form-group row\">\n                            <div class=\"col-md-7 mt-3 mt-md-8\">\n                                <textarea class=\"form-control\" name=\"bandwidth_limit_sources\" rows=\"4\"></textarea>\n                                <div class=\"form-text\" data-i18n=\"general.ip_mask_help\"></div>\n                            </div>\n                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                <input type=\"number\" min=\"0\" class=\"form-control\" name=\"upload_bandwidth_source\" value=\"\" />\n                                <div class=\"form-text\" data-i18n=\"filters.upload_bandwidth_help\"></div>\n                            </div>\n                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                <input type=\"number\" min=\"0\" class=\"form-control\" name=\"download_bandwidth_source\" value=\"\" />\n                                <div class=\"form-text\" data-i18n=\"filters.download_bandwidth_help\"></div>\n                            </div>\n                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                <a href=\"#\" data-repeater-delete\n                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                        <span class=\"path1\"></span>\n                                        <span class=\"path2\"></span>\n                                        <span class=\"path3\"></span>\n                                        <span class=\"path4\"></span>\n                                        <span class=\"path5\"></span>\n                                    </i>\n                                </a>\n                            </div>\n                        </div>\n                    </div>\n                    {{- end}}\n                </div>\n            </div>\n\n            <div class=\"form-group mt-5\">\n                <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                    <i class=\"ki-duotone ki-plus fs-3\"></i>\n                    <span data-i18n=\"general.add\">Add</span>\n                </a>\n            </div>\n        </div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idTransferUL\" data-i18n=\"filters.upload_data_transfer\" class=\"col-md-3 col-form-label\">Upload data transfer (MB)</label>\n    <div class=\"col-md-3\">\n        <input id=\"idTransferUL\" type=\"number\" min=\"0\" class=\"form-control\" name=\"upload_data_transfer\" value=\"{{.UploadDataTransfer}}\" aria-describedby=\"idTransferULHelp\" />\n        <div id=\"idTransferULHelp\" class=\"form-text\" data-i18n=\"filters.upload_data_transfer_help\"></div>\n    </div>\n    <div class=\"col-md-1\"></div>\n    <label for=\"idTransferDL\" data-i18n=\"filters.download_data_transfer\" class=\"col-md-2 col-form-label\">Download data transfer (MB)</label>\n    <div class=\"col-md-3\">\n        <input id=\"idTransferDL\" type=\"number\" min=\"0\" class=\"form-control\" name=\"download_data_transfer\" value=\"{{.DownloadDataTransfer}}\" aria-describedby=\"idTransferDLhHelp\" />\n        <div id=\"idTransferDLhHelp\" class=\"form-text\" data-i18n=\"filters.download_data_transfer_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idTransferTotal\" data-i18n=\"filters.total_data_transfer\" class=\"col-md-3 col-form-label\">Total data transfer (MB)</label>\n    <div class=\"col-md-9\">\n        <input id=\"idTransferTotal\" type=\"number\" min=\"0\" class=\"form-control\" name=\"total_data_transfer\" value=\"{{.TotalDataTransfer}}\" aria-describedby=\"idTransferTotalHelp\" />\n        <div id=\"idTransferTotalHelp\" class=\"form-text\" data-i18n=\"filters.total_data_transfer_help\"></div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"user_group_advanced\"}}\n<div class=\"form-group row mt-10\">\n    <label for=\"idTLSUsername\" data-i18n=\"filters.tls_username\" class=\"col-md-3 col-form-label\">TLS username</label>\n    <div class=\"col-md-9\">\n        <select id=\"idTLSUsername\" name=\"tls_username\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" aria-describedby=\"idTLSUsernameHelp\">\n            <option value=\"\" {{if or (eq .TLSUsername \"None\") (eq .TLSUsername \"\") }}selected{{end}}>---</option>\n            <option value=\"CommonName\" {{if eq .TLSUsername \"CommonName\" }}selected{{end}}>Common Name</option>\n        </select>\n        <div id=\"idTLSUsernameHelp\" class=\"form-text\" data-i18n=\"filters.tls_username_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idFTPSecurity\" data-i18n=\"filters.ftp_security\" class=\"col-md-3 col-form-label\">FTP security</label>\n    <div class=\"col-md-9\">\n        <select id=\"idFTPSecurity\" name=\"ftp_security\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" aria-describedby=\"idFTPSecurityHelp\">\n            <option value=\"\" data-i18n=\"general.global_settings\" {{if eq .FTPSecurity 0 }}selected{{end}}>Server settings</option>\n            <option value=\"1\" data-i18n=\"general.mandatory_encryption\" {{if eq .FTPSecurity 1 }}selected{{end}}>Mandatory encryption</option>\n        </select>\n        <div id=\"idFTPSecurityHelp\" class=\"form-text\" data-i18n=\"filters.ftp_security_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idStartDirectory\" data-i18n=\"filters.start_directory\" class=\"col-md-3 col-form-label\">Start directory</label>\n    <div class=\"col-md-9\">\n        <input id=\"idStartDirectory\" type=\"text\" class=\"form-control\" name=\"start_directory\" value=\"{{.StartDirectory}}\" aria-describedby=\"idStartDirectoryHelp\" />\n        <div id=\"idStartDirectoryHelp\" class=\"form-text\" data-i18n=\"filters.start_directory_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idHooks\" data-i18n=\"filters.hooks\" class=\"col-md-3 col-form-label\">Hooks</label>\n    <div class=\"col-md-9\">\n        <select id=\"idHooks\" name=\"hooks\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n            <option value=\"external_auth_disabled\" data-i18n=\"filters.hook_ext_auth_disabled\" {{if .Hooks.ExternalAuthDisabled}}selected{{end}}>\n                External auth disabled\n            </option>\n            <option value=\"pre_login_disabled\" data-i18n=\"filters.hook_pre_login_disabled\" {{if .Hooks.PreLoginDisabled}}selected{{end}}>\n                Pre-login disabled\n            </option>\n            <option value=\"check_password_disabled\" data-i18n=\"filters.hook_check_password_disabled\" {{if .Hooks.CheckPasswordDisabled}}selected{{end}}>\n                Check password disabled\n            </option>\n        </select>\n    </div>\n</div>\n\n<div class=\"form-group row align-items-center mt-10\">\n    <label data-i18n=\"filters.is_anonymous\" class=\"col-md-3 col-form-label\" for=\"idAnonymous\">Is Anonymous</label>\n    <div class=\"col-md-9\">\n        <div class=\"form-check form-switch form-check-custom form-check-solid\">\n            <input class=\"form-check-input\" type=\"checkbox\" id=\"idAnonymous\" name=\"is_anonymous\" {{if .IsAnonymous}}checked{{end}}/>\n            <label data-i18n=\"filters.is_anonymous_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idAnonymous\">\n                Anonymous users are supported for FTP and WebDAV protocols and have read-only access\n            </label>\n        </div>\n    </div>\n</div>\n\n<div class=\"form-group row align-items-center mt-10\">\n    <label data-i18n=\"filters.disable_fs_checks\" class=\"col-md-3 col-form-label\" for=\"idDisableFsChecks\">Disable filesystem checks</label>\n    <div class=\"col-md-9\">\n        <div class=\"form-check form-switch form-check-custom form-check-solid\">\n            <input class=\"form-check-input\" type=\"checkbox\" id=\"idDisableFsChecks\" name=\"disable_fs_checks\" {{if .DisableFsChecks}}checked{{end}}/>\n            <label data-i18n=\"filters.disable_fs_checks_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idDisableFsChecks\">\n                Disable checks for existence and automatic creation of home directory and virtual folders\n            </label>\n        </div>\n    </div>\n</div>\n\n<div class=\"form-group row align-items-center mt-10\">\n    <label data-i18n=\"general.api_key_auth\" class=\"col-md-3 col-form-label\" for=\"idAllowAPIKeyAuth\">API key authentication</label>\n    <div class=\"col-md-9\">\n        <div class=\"form-check form-switch form-check-custom form-check-solid\">\n            <input class=\"form-check-input\" type=\"checkbox\" id=\"idAllowAPIKeyAuth\" name=\"allow_api_key_auth\" {{if .AllowAPIKeyAuth}}checked{{end}}/>\n            <label data-i18n=\"filters.api_key_auth_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idAllowAPIKeyAuth\">\n                Allow to impersonate this user, in REST API, with an API key\n            </label>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"user_group_profile\"}}\n<div class=\"form-group row mt-10\">\n    <label for=\"idPasswordStrength\" data-i18n=\"filters.password_strength\" class=\"col-md-3 col-form-label\">Password strength</label>\n    <div class=\"col-md-9\">\n        <input id=\"idPasswordStrength\" type=\"number\" min=\"0\" class=\"form-control\" name=\"password_strength\" value=\"{{.PasswordStrength}}\" aria-describedby=\"idPasswordStrengthHelp\"/>\n        <div id=\"idPasswordStrengthHelp\" class=\"form-text\" data-i18n=\"filters.password_strength_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idPasswordExpiration\" data-i18n=\"filters.password_expiration\" class=\"col-md-3 col-form-label\">Password expiration</label>\n    <div class=\"col-md-9\">\n        <input id=\"idPasswordExpiration\" type=\"number\" min=\"0\" class=\"form-control\" name=\"password_expiration\" value=\"{{.PasswordExpiration}}\" aria-describedby=\"idPasswordExpirationHelp\"/>\n        <div id=\"idPasswordExpirationHelp\" class=\"form-text\" data-i18n=\"filters.password_expiration_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idDefaultSharesExpiration\" data-i18n=\"filters.default_shares_expiration\" class=\"col-md-3 col-form-label\">Default shares expiration</label>\n    <div class=\"col-md-9\">\n        <input id=\"idDefaultSharesExpiration\" type=\"number\" min=\"0\" class=\"form-control\" name=\"default_shares_expiration\" value=\"{{.DefaultSharesExpiration}}\" aria-describedby=\"idDefaultSharesExpirationHelp\"/>\n        <div id=\"idDefaultSharesExpirationHelp\" class=\"form-text\" data-i18n=\"filters.default_shares_expiration_help\"></div>\n    </div>\n</div>\n\n<div class=\"form-group row mt-10\">\n    <label for=\"idMaxSharesExpiration\" data-i18n=\"filters.max_shares_expiration\" class=\"col-md-3 col-form-label\">Max shares expiration</label>\n    <div class=\"col-md-9\">\n        <input id=\"idMaxSharesExpiration\" type=\"number\" min=\"0\" class=\"form-control\" name=\"max_shares_expiration\" value=\"{{.MaxSharesExpiration}}\" aria-describedby=\"idMaxSharesExpirationHelp\"/>\n        <div id=\"idMaxSharesExpirationHelp\" class=\"form-text\" data-i18n=\"filters.max_shares_expiration_help\"></div>\n    </div>\n</div>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/group.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"infomsg\" \"group.help\"}}\n        {{- template \"errmsg\" .Error}}\n        <form id=\"group_form\" enctype=\"multipart/form-data\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n\n            <div class=\"form-group row\">\n                <label for=\"idGroupName\" data-i18n=\"general.name\" class=\"col-md-3 col-form-label\">Name</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idGroupName\" type=\"text\" placeholder=\"\" name=\"name\" value=\"{{.Group.Name}}\" maxlength=\"255\" autocomplete=\"off\"\n                        spellcheck=\"false\" required {{if eq .Mode 2}}class=\"form-control-plaintext readonly-input\" readonly{{else}}class=\"form-control\"{{end}} />\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idDescription\" type=\"text\" class=\"form-control\" name=\"description\" value=\"{{.Group.Description}}\" maxlength=\"255\">\n                </div>\n            </div>\n\n            {{- template \"fshtml\" .FsWrapper}}\n            {{- if .VirtualFolders}}\n            <div class=\"card mt-10 {{if .LoggedUser.Filters.Preferences.HideVirtualFolders}}d-none{{end}}\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"title.folders\" class=\"card-title section-title-inner\">Virtual folders</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"virtual_folders\">\n                        {{- template \"infomsg-no-mb\" \"user.virtual_folders_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"virtual_folders\">\n                                {{- range $idx, $val := .Group.VirtualFolders}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]virtual_folders.mount_path\" type=\"text\" class=\"form-control\" name=\"vfolder_path\" value=\"{{$val.VirtualPath}}\" />\n                                            </div>\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <select name=\"vfolder_name\" data-i18n=\"[data-placeholder]general.folder_placeholder\" class=\"form-select select-repetear\" data-placeholder=\"Select a folder\" data-allow-clear=\"true\">\n                                                    <option value=\"\"></option>\n                                                    {{- range $.VirtualFolders}}\n                                                    <option value=\"{{.Name}}\" {{- if eq $val.Name .Name}} selected{{- end}}>{{.Name}}</option>\n                                                    {{- end}}\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input type=\"text\" class=\"form-control\" name=\"vfolder_quota_size\" value=\"{{HumanizeBytes $val.QuotaSize}}\" />\n                                                <div class=\"form-text\" data-i18n=\"virtual_folders.quota_size\"></div>\n                                            </div>\n                                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                                <input type=\"number\" min=\"-1\" class=\"form-control\" name=\"vfolder_quota_files\" value=\"{{$val.QuotaFiles}}\" />\n                                                <div class=\"form-text\" data-i18n=\"virtual_folders.quota_files\"></div>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]virtual_folders.mount_path\" type=\"text\" class=\"form-control\" name=\"vfolder_path\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <select name=\"vfolder_name\" data-i18n=\"[data-placeholder]general.folder_placeholder\" class=\"form-select select-repetear\" data-placeholder=\"Select a folder\" data-allow-clear=\"true\">\n                                                <option value=\"\"></option>\n                                                {{- range .VirtualFolders}}\n                                                <option value=\"{{.Name}}\">{{.Name}}</option>\n                                                {{- end}}\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input type=\"text\" class=\"form-control\" name=\"vfolder_quota_size\" value=\"\" />\n                                            <div class=\"form-text\" data-i18n=\"virtual_folders.quota_size\"></div>\n                                        </div>\n                                        <div class=\"col-md-2 mt-3 mt-md-8\">\n                                            <input type=\"number\" min=\"-1\" class=\"form-control\" name=\"vfolder_quota_files\" value=\"\" />\n                                            <div class=\"form-text\" data-i18n=\"virtual_folders.quota_files\"></div>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"accordion shadow-sm mt-10\" id=\"accordionUser\">\n\n                <div class=\"accordion-item\">\n                    <h2 class=\"accordion-header\" id=\"headingPermissions\">\n                        <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#collapsePermissions\" aria-expanded=\"true\" aria-controls=\"collapsePermissions\">\n                            <span data-i18n=\"general.acls\">ACLs</span>\n                        </button>\n                    </h2>\n                    <div id=\"collapsePermissions\" class=\"accordion-collapse collapse\" aria-labelledby=\"headingPermissions\" data-bs-parent=\"#accordionUser\">\n                        <div class=\"accordion-body\">\n\n                            <div class=\"card mt-10\">\n                                <div class=\"card-header bg-light\">\n                                    <h3 data-i18n=\"filters.directory_permissions\" class=\"card-title section-title-inner\">Per-directory permissions</h3>\n                                </div>\n                                <div class=\"card-body\">\n                                    <div id=\"directory_permissions\">\n                                        {{- template \"infomsg-no-mb\" \"filters.directory_permissions_help\"}}\n                                        <div class=\"form-group\">\n                                            <div data-repeater-list=\"directory_permissions\">\n                                                {{- range $idx, $dirPerms := .Group.GetPermissions -}}\n                                                <div data-repeater-item>\n                                                    <div class=\"form-group row\">\n                                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                            <input data-i18n=\"[placeholder]filters.directory_path_help\" type=\"text\" class=\"form-control\" name=\"sub_perm_path\" value=\"{{$dirPerms.Path}}\" />\n                                                        </div>\n                                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                            <select name=\"sub_perm_permissions\" data-i18n=\"[data-placeholder]general.permissions\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                                                                {{- range $validPerm := $.ValidPerms}}\n                                                                <option value=\"{{$validPerm}}\" {{- range $perm := $dirPerms.Permissions }}{{- if eq $perm $validPerm}} selected{{- end}}{{- end}}>{{$validPerm}}</option>\n                                                                {{- end}}\n                                                            </select>\n                                                        </div>\n                                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                            <a href=\"#\" data-repeater-delete\n                                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                                    <span class=\"path1\"></span>\n                                                                    <span class=\"path2\"></span>\n                                                                    <span class=\"path3\"></span>\n                                                                    <span class=\"path4\"></span>\n                                                                    <span class=\"path5\"></span>\n                                                                </i>\n                                                            </a>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                                {{- else}}\n                                                <div data-repeater-item>\n                                                    <div class=\"form-group row\">\n                                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                            <input data-i18n=\"[placeholder]filters.directory_path_help\" type=\"text\" class=\"form-control\" name=\"sub_perm_path\" value=\"\" />\n                                                        </div>\n                                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                            <select name=\"sub_perm_permissions\" data-i18n=\"[data-placeholder]general.permissions\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                                                                {{- range $validPerm := .ValidPerms}}\n                                                                <option value=\"{{$validPerm}}\">{{$validPerm}}</option>\n                                                                {{- end}}\n                                                            </select>\n                                                        </div>\n                                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                            <a href=\"#\" data-repeater-delete\n                                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                                    <span class=\"path1\"></span>\n                                                                    <span class=\"path2\"></span>\n                                                                    <span class=\"path3\"></span>\n                                                                    <span class=\"path4\"></span>\n                                                                    <span class=\"path5\"></span>\n                                                                </i>\n                                                            </a>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                                {{- end}}\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group mt-5\">\n                                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                                <span data-i18n=\"general.add\">Add</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n                            {{- template \"user_group_perms\" .Group.UserSettings.Filters}}\n\n                            {{- template \"user_group_access_time\" .Group.UserSettings.Filters}}\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idMaxSessions\" data-i18n=\"filters.max_sessions\" class=\"col-md-3 col-form-label\">Max sessions</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idMaxSessions\" type=\"number\" min=\"0\" class=\"form-control\" name=\"max_sessions\" value=\"{{.Group.UserSettings.MaxSessions}}\" aria-describedby=\"idMaxSessionsHelp\" />\n                                    <div id=\"idMaxSessionsHelp\" class=\"form-text\" data-i18n=\"filters.max_sessions_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idProtocols\" data-i18n=\"filters.denied_protocols\" class=\"col-md-3 col-form-label\">\n                                    Denied protocols\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idProtocols\" name=\"denied_protocols\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $protocol := .ValidProtocols}}\n                                        <option value=\"{{$protocol}}\" {{- range $p :=$.Group.UserSettings.Filters.DeniedProtocols }}{{- if eq $p $protocol}} selected{{- end}}{{- end}}>{{$protocol}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idLoginMethods\" data-i18n=\"filters.denied_login_methods\" class=\"col-md-3 col-form-label\">\n                                    Denied login methods\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idLoginMethods\" name=\"denied_login_methods\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple aria-describedby=\"idLoginMethodsHelp\">\n                                        {{- range $method := .ValidLoginMethods}}\n                                        <option value=\"{{$method}}\" {{- range $m :=$.Group.UserSettings.Filters.DeniedLoginMethods }}{{- if eq $m $method}} selected{{- end}}{{- end}}>{{$method}}</option>\n                                        {{- end}}\n                                    </select>\n                                    <div id=\"idLoginMethodsHelp\" data-i18n=\"filters.denied_login_methods_help\" class=\"form-text\">\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idTwoFactorProtocols\" data-i18n=\"2fa.require_for\" class=\"col-md-3 col-form-label\">\n                                    Require 2FA for\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idTwoFactorProtocols\" name=\"required_two_factor_protocols\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $protocol := .TwoFactorProtocols}}\n                                        <option value=\"{{$protocol}}\" {{- range $p :=$.Group.UserSettings.Filters.TwoFactorAuthProtocols }}{{- if eq $p $protocol}} selected{{- end}}{{- end}}>{{$protocol}}</option>\n                                        {{end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idWebClient\" data-i18n=\"filters.web_client_options\" class=\"col-md-3 col-form-label\">\n                                    Web client/REST API\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idWebClient\" name=\"web_client_options\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $option := .WebClientOptions}}\n                                        <option value=\"{{$option}}\" {{- range $p :=$.Group.UserSettings.Filters.WebClient }}{{- if eq $p $option}}selected{{- end}}{{- end}}>{{$option}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idDeniedIP\" data-i18n=\"general.denied_ip_mask\" class=\"col-md-3 col-form-label\">Denied IP/Mask</label>\n                                <div class=\"col-md-9\">\n                                    <textarea class=\"form-control\" id=\"idDeniedIP\" name=\"denied_ip\" aria-describedby=\"idDeniedIPHelp\"\n                                        rows=\"3\">{{.Group.GetDeniedIPAsString}}</textarea>\n                                    <div id=\"idDeniedIPHelp\" class=\"form-text\" data-i18n=\"general.ip_mask_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idAllowedIP\" data-i18n=\"general.allowed_ip_mask\" class=\"col-md-3 col-form-label\">Allowed IP/Mask</label>\n                                <div class=\"col-md-9\">\n                                    <textarea class=\"form-control\" id=\"idAllowedIP\" name=\"allowed_ip\" aria-describedby=\"idAllowedIPHelp\"\n                                        rows=\"3\">{{.Group.GetAllowedIPAsString}}</textarea>\n                                    <div id=\"idAllowedIPHelp\" class=\"form-text\" data-i18n=\"general.ip_mask_help\"></div>\n                                </div>\n                            </div>\n\n                        </div>\n                    </div>\n\n                </div>\n\n                <div class=\"accordion-item\">\n                    <h2 class=\"accordion-header\" id=\"headingQuota\">\n                        <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#collapseQuota\" aria-expanded=\"true\" aria-controls=\"collapseQuota\">\n                            <span data-i18n=\"general.quota_limits\">Disk quota and bandwidth limits</span>\n                        </button>\n                    </h2>\n                    <div id=\"collapseQuota\" class=\"accordion-collapse collapse\" aria-labelledby=\"headingQuota\" data-bs-parent=\"#accordionUser\">\n                        <div class=\"accordion-body\">\n                            {{- template \"user_group_quota\" .Group.UserSettings}}\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"accordion-item\">\n                    <h2 class=\"accordion-header\" id=\"headingAdvanced\">\n                        <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#collapseAdvanced\" aria-expanded=\"true\" aria-controls=\"collapseAdvanced\">\n                            <span data-i18n=\"general.advanced_settings\">Advanced settings</span>\n                        </button>\n                    </h2>\n                    <div id=\"collapseAdvanced\" class=\"accordion-collapse collapse\" aria-labelledby=\"headingAdvanced\" data-bs-parent=\"#accordionUser\">\n                        <div class=\"accordion-body\">\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idExpiresIn\" data-i18n=\"user.expires_in\" class=\"col-md-3 col-form-label\">Expires in</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idExpiresIn\" type=\"number\" min=\"0\" class=\"form-control\" name=\"expires_in\" value=\"{{.Group.UserSettings.ExpiresIn}}\" aria-describedby=\"idExpiresIn\" />\n                                    <div id=\"idExpiresInHelp\" class=\"form-text\" data-i18n=\"user.expires_in_help\"></div>\n                                </div>\n                            </div>\n\n                            {{- template \"user_group_profile\" .Group.UserSettings.Filters}}\n\n                            {{- template \"user_group_advanced\" .Group.UserSettings.Filters}}\n\n                            <div class=\"form-group row mt-10 {{if not .Group.HasExternalAuth}}d-none{{end}}\">\n                                <label for=\"idExtAuthCacheTime\" data-i18n=\"filters.external_auth_cache_time\" class=\"col-md-3 col-form-label\">External auth cache time</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idExtAuthCacheTime\" type=\"number\" min=\"0\" class=\"form-control\" name=\"external_auth_cache_time\" value=\"{{.Group.UserSettings.Filters.ExternalAuthCacheTime}}\" aria-describedby=\"idExtAuthCacheTimeHelp\" />\n                                    <div id=\"idExtAuthCacheTimeHelp\" class=\"form-text\" data-i18n=\"filters.external_auth_cache_time_help\"></div>\n                                </div>\n                            </div>\n\n                        </div>\n                    </div>\n                </div>\n\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n    </div>\n</div>\n{{- end}}\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    $(document).on(\"i18nload\", function(){\n        initRepeater('#virtual_folders');\n        initRepeater('#directory_permissions');\n        initRepeater('#directory_patterns');\n        initRepeater('#src_bandwidth_limits');\n        initRepeater('#access_time_restrictions');\n        initRepeaterItems();\n        //{{- if .Error}}\n        $('#accordionUser .collapse').removeAttr(\"data-bs-parent\").collapse('show');\n        //{{- end}}\n        onFilesystemChanged('{{.Group.UserSettings.FsConfig.Provider}}');\n\n        $('#idFilesystem').on(\"change\", function(){\n            onFilesystemChanged(this.value);\n        });\n    });\n\n    $(document).on(\"i18nshow\", function(){\n        $('#group_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/groups.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"group.view_manage\" class=\"card-title section-title\">View and manage groups</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                    <button id=\"export_button\" type=\"button\" class=\"btn btn-light-primary ms-3\" data-table-filter=\"export\">\n                        <span data-i18n=\"general.export\" class=\"indicator-label\">\n                            Export\n                        </span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                </div>\n\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <button type=\"button\" class=\"btn btn-light-primary rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom\" data-kt-menu-permanent=\"true\">\n                        <span data-i18n=\"general.colvis\">Column visibility</span>\n                        <i class=\"ki-duotone ki-down fs-3 rotate-180 ms-3 me-0\"></i>\n                    </button>\n                    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4\" data-kt-menu=\"true\">\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColMembers\" />\n                            <label class=\"form-check-label\" for=\"checkColMembers\">\n                                <span data-i18n=\"general.members\" class=\"text-gray-800 fs-6\">Members</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColDesc\" />\n                            <label class=\"form-check-label\" for=\"checkColDesc\">\n                                <span data-i18n=\"general.description\" class=\"text-gray-800 fs-6\">Description</span>\n                            </label>\n                        </div>\n                    </div>\n                    {{- if .LoggedUser.HasPermission \"manage_groups\"}}\n                    <a href=\"{{.GroupURL}}\" class=\"btn btn-primary ms-5\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                    {{- end}}\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"general.name\">Name</th>\n                        <th data-i18n=\"general.members\">Members</th>\n                        <th data-i18n=\"general.description\">Description</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/papaparse/papaparse.min.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/file-saver/FileSaver.min.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function deleteAction(name) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.GroupURL}}' + \"/\" + encodeURIComponent(name);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.GroupsURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"name\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"users\",\n                        name: \"members\",\n                        defaultContent: \"\",\n                        searchable: false,\n                        orderable: false,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                let users = 0;\n                                if (row.users){\n                                    users = row.users.length;\n                                }\n                                let admins = 0;\n                                if (row.admins){\n                                    admins = row.admins.length;\n                                }\n                                return $.t('general.members_summary', {users: users, admins: admins});\n                            }\n                            return \"\";\n                        }\n                    },\n                    {\n                        data: \"description\",\n                        name: \"description\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let numActions = 0;\n                                let actions = `<button class=\"btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                                    <span data-i18n=\"general.actions\" class=\"fs-6\">Actions</span>\n\t\t\t\t\t\t\t\t\t\t            <i class=\"ki-duotone ki-down fs-5 ms-1 rotate-180\"></i>\n                                                </button>\n                                                <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">`;\n\n                                //{{- if .LoggedUser.HasPermission \"manage_groups\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n                                             <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                if (numActions > 0){\n                                    actions+=`</div>`;\n                                    return actions;\n                                }\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                colReorder: {\n                    enable: true,\n                    fixedColumnsLeft: 1\n                },\n                stateLoadParams: function (settings, data) {\n                    if (data.search.search){\n                        const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                        filterSearch.value = data.search.search;\n                    }\n                },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n            dt.on('columns-reordered', function(e, settings, details){\n                drawAction();\n            });\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        function handleColVisibilityCheckbox(el, selector) {\n            let index = dt.column(selector).index();\n            el.off(\"change\");\n            el.prop('checked', dt.column(index).visible());\n            el.on(\"change\", function(e){\n                let index = dt.column(selector).index();\n                dt.column(index).visible($(this).is(':checked'));\n                dt.draw('page');\n            });\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n            handleColVisibilityCheckbox($('#checkColMembers'), \"members:name\");\n            handleColVisibilityCheckbox($('#checkColDesc'), \"description:name\");\n\n            const exportButton = $(document.querySelector('[data-table-filter=\"export\"]'));\n            exportButton.on('click', function(e){\n                e.preventDefault();\n                this.blur();\n                this.setAttribute('data-kt-indicator', 'on');\n\t\t\t    this.disabled = true;\n\n                let data = [];\n                dt.rows({ search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                    let line = {};\n                    let rowData = dt.row(rowIdx).data();\n                    let userSettings = rowData[\"user_settings\"];\n                    let filters = userSettings[\"filters\"];\n\n                    line[\"Name\"] = rowData[\"name\"];\n                    let fsProvider = \"\";\n                    switch (userSettings[\"filesystem\"][\"provider\"]){\n                        case 0:\n                            if (userSettings[\"home_dir\"]){\n                                fsProvider = \"Local storage: \"+userSettings[\"home_dir\"];\n                            }\n                            break;\n                        case 1:\n                            fsProvider = \"S3 Compatible\";\n                            break;\n                        case 2:\n                            fsProvider = \"Google Cloud Storage\";\n                            break;\n                        case 3:\n                            fsProvider = \"Azure Blob\";\n                            break;\n                        case 4:\n                            fsProvider = \"Local storage encrypted: \"+userSettings[\"home_dir\"];\n                            break;\n                        case 5:\n                            fsProvider = \"SFTP\";\n                            break;\n                        case 6:\n                            fsProvider = \"HTTP\";\n                            break;\n                    }\n                    line[\"Filesystem\"] = fsProvider;\n                    let virtualFolders = [];\n                    let rowFolders = rowData[\"virtual_folders\"];\n                    if (rowFolders){\n                        for (i = 0; i < rowFolders.length; i++){\n                            virtualFolders.push(`${rowFolders[i].virtual_path} (${rowFolders[i].name})`);\n                        }\n                    }\n                    line[\"Virtual Folders\"] = virtualFolders.join(\", \");\n\n                    let permissions = \"\";\n                    for (let key in userSettings[\"permissions\"]){\n                        let values = userSettings[\"permissions\"][key];\n                        if (values){\n                            permissions+=`${key}: [${values.join(\", \")}]; `;\n                        } else {\n                            permissions+=`${key}: []; `;\n                        }\n                    }\n                    line[\"Permissions\"] = permissions;\n\n                    if (userSettings[\"max_sessions\"] > 0){\n                        line[\"Max sessions\"] = userSettings[\"max_sessions\"];\n                    } else {\n                        line[\"Max sessions\"] = \"\"\n                    }\n                    if (userSettings[\"quota_size\"] > 0){\n                        line[\"Quota size\"] = fileSizeIEC(userSettings[\"quota_size\"]);\n                    }  else {\n                        line[\"Quota size\"] = \"\";\n                    }\n                    if (userSettings[\"quota_files\"] > 0){\n                        line[\"Quota files\"] = userSettings[\"quota_files\"];\n                    } else {\n                        line[\"Quota files\"] = \"\";\n                    }\n\n                    if (userSettings[\"total_data_transfer\"] > 0) {\n                        line[\"Data transfer\"] = fileSizeIEC(userSettings[\"total_data_transfer\"] * 1048576);\n                    } else {\n                        let val = \"\";\n                        if (userSettings[\"upload_data_transfer\"] > 0) {\n                            val = \"Upload: \"+fileSizeIEC(userSettings[\"upload_data_transfer\"] * 1048576);\n                        }\n                        if (userSettings[\"download_data_transfer\"] > 0) {\n                            if (val){\n                                val += \". \";\n                            }\n                            val += \"Download: \"+fileSizeIEC(userSettings[\"download_data_transfer\"] * 1048576);\n                        }\n                        line[\"Data transfer\"] = val;\n                    }\n                    if (filters[\"allowed_ip\"]){\n                        line[\"Allowed IPs/Networks\"] = filters[\"allowed_ip\"].join(\", \");\n                    } else {\n                        line[\"Allowed IPs/Networks\"] = \"\";\n                    }\n                    if (filters[\"denied_ip\"]){\n                        line[\"Denied IPs/Networks\"] = filters[\"denied_ip\"].join(\", \");\n                    } else {\n                        line[\"Denied IPs/Networks\"] = \"\";\n                    }\n                    if (filters[\"denied_protocols\"]){\n                        line[\"Denied protocols\"] = filters[\"denied_protocols\"].join(\", \");\n                    } else {\n                        line[\"Denied protocols\"] = \"\";\n                    }\n                    if (filters[\"denied_login_methods\"]){\n                        line[\"Denied login methods\"] = filters[\"denied_login_methods\"].join(\", \");\n                    } else {\n                        line[\"Denied login methods\"] = \"\";\n                    }\n                    if (filters[\"max_upload_file_size\"]){\n                        line[\"Max upload size\"] = fileSizeIEC(filters[\"max_upload_file_size\"]);\n                    } else {\n                        line[\"Max upload size\"] = \"\";\n                    }\n                    let webClientPermissions = filters[\"web_client\"];\n                    if (webClientPermissions){\n                        line[\"WebClient permissions\"] = webClientPermissions.join(\", \");\n                    } else {\n                        line[\"WebClient permissions\"] = \"\";\n                    }\n                    if (rowData[\"users\"]){\n                        line[\"Users\"] = rowData[\"users\"].join(\", \");\n                    } else {\n                        line[\"Users\"] = \"\";\n                    }\n                    if (rowData[\"admins\"]){\n                        line[\"Admins\"] = rowData[\"admins\"].join(\", \");\n                    } else {\n                        line[\"Admins\"] = \"\";\n                    }\n                    if (rowData[\"description\"]){\n                        line[\"Description\"] = rowData[\"description\"];\n                    } else {\n                        line[\"Description\"] = \"\";\n                    }\n\n                    data.push(line);\n                });\n\n                let csv = Papa.unparse(data, {\n                    quotes: false,\n\t                quoteChar: '\"',\n\t                escapeChar: '\"',\n\t                delimiter: \",\",\n\t                header: true,\n\t                newline: \"\\r\\n\",\n\t                skipEmptyLines: false,\n\t                columns: null,\n                    escapeFormulae: true\n                });\n                let blob = new Blob([csv], {type: \"text/csv\"});\n                let ts = moment().format(\"YYYY_MM_DD_HH_mm_ss\");\n                saveAs(blob, `groups_${ts}.csv`);\n\n                this.removeAttribute('data-kt-indicator');\n                this.disabled = false;\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.GroupURL}}' + \"/\" + encodeURIComponent(rowData['name']));\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    let row = dt.row(parent).data();\n                    if (row.users && row.users.length > 0){\n                        ModalAlert.fire({\n                            text: $.t('group.err_delete_referenced'),\n                            icon: \"warning\",\n                            confirmButtonText: $.t('general.ok'),\n                            customClass: {\n                                confirmButton: \"btn btn-primary\"\n                            }\n                        });\n                        return;\n                    }\n                    deleteAction(row['name']);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/iplist.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"iplist_form\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n\n            <div class=\"form-group row\">\n                <label for=\"idType\" data-i18n=\"general.type\" class=\"col-md-3 col-form-label\">Type</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idType\" type=\"text\" {{if eq .Entry.Type 1}}data-i18n=\"[value]ip_list.allow_list\"{{end}}{{if eq .Entry.Type 2}}data-i18n=\"[value]ip_list.defender_list\"{{end}}{{if eq .Entry.Type 3}}data-i18n=\"[value]ip_list.ratelimiters_safe_list\"{{end}} name=\"type\" maxlength=\"50\"\n                        class=\"form-control-plaintext readonly-input\" readonly />\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idIPOrNet\" data-i18n=\"ip_list.ip_net\" class=\"col-md-3 col-form-label\">IP/Network</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idIPOrNet\" type=\"text\" name=\"ipornet\" value=\"{{.Entry.IPOrNet}}\" maxlength=\"50\" autocomplete=\"off\"\n                        required {{if eq .Mode 2}}class=\"form-control-plaintext readonly-input\" readonly{{else}}class=\"form-control\" aria-describedby=\"idIPOrNetHelp\"{{end}} />\n                    {{- if ne .Mode 2}}\n                    <div id=\"idIPOrNetHelp\" class=\"form-text\" data-i18n=\"ip_list.ip_net_help\"></div>\n                    {{- end}}\n                </div>\n            </div>\n\n            {{- if eq .Entry.Type 2}}\n            <div class=\"form-group row mt-10\">\n                <label for=\"idMode\" data-i18n=\"general.mode\" class=\"col-md-3 col-form-label\">Mode</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idMode\" name=\"mode\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        <option value=\"2\" data-i18n=\"ip_list.deny\" {{if eq .Entry.Mode 2 }}selected{{end}}>Deny</option>\n                        <option value=\"1\" data-i18n=\"ip_list.allow\" {{if eq .Entry.Mode 1 }}selected{{end}}>Allow</option>\n                    </select>\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idProtocols\" data-i18n=\"ip_list.protocols\" class=\"col-md-3 col-form-label\">\n                    Protocols\n                </label>\n                <div class=\"col-md-9\">\n                    <select id=\"idProtocols\" name=\"protocols\" data-i18n=\"[data-placeholder]ip_list.any\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                        <option value=\"1\" {{if .Entry.HasProtocol \"SSH\" }}selected{{end}}>SSH</option>\n                        <option value=\"2\" {{if .Entry.HasProtocol \"FTP\" }}selected{{end}}>FTP</option>\n                        <option value=\"4\" {{if .Entry.HasProtocol \"DAV\" }}selected{{end}}>DAV</option>\n                        <option value=\"8\" {{if .Entry.HasProtocol \"HTTP\" }}selected{{end}}>HTTP</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idDescription\" type=\"text\" class=\"form-control\" name=\"description\" value=\"{{.Entry.Description}}\" maxlength=\"512\">\n                </div>\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n\n        </form>\n    </div>\n</div>\n{{- end}}\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    $(document).on(\"i18nshow\", function(){\n        $('#iplist_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/iplists.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"ip_list.view_manage\" class=\"card-title section-title\">View and manage IP Lists</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <div class=\"input-group\">\n                        <input name=\"search\" id=\"idSearch\" data-i18n=\"[placeholder]ip_list.search\" type=\"text\" class=\"form-control rounded-left w-250px\" placeholder=\"Search\" />\n                        <button id=\"search_button\" type=\"button\" class=\"btn btn-primary\">\n                            <i class=\"ki-solid ki-magnifier fs-2\"></i>\n                        </button>\n                    </div>\n                </div>\n\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <div>\n                        <select id=\"idListType\" name=\"list_type\" class=\"form-select me-3\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                            <option data-i18n=\"ip_list.defender_list\" value=\"2\">Defender</option>\n                            <option data-i18n=\"ip_list.allow_list\" value=\"1\">Allow list</option>\n                            <option data-i18n=\"ip_list.ratelimiters_safe_list\" value=\"3\">Rate limiters safe list</option>\n                        </select>\n                    </div>\n\n                    <a href=\"#\" id=\"idAdd\" class=\"btn btn-primary ms-5\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"ip_list.ip_net\">IP/Network</th>\n                        <th data-i18n=\"ip_list.protocols\">Protocols</th>\n                        <th data-i18n=\"general.mode\">Mode</th>\n                        <th data-i18n=\"general.description\">Description</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n\n            <div id=\"paginationContainer\" class=\"d-flex mt-4 mb-4 justify-content-end d-none\">\n                <div class=\"btn-group\" role=\"group\" aria-label=\"Pagination\">\n                    <button id=\"pagePrevious\" data-i18n=\"general.previous\" type=\"button\" class=\"btn btn-outline btn-active-primary disabled\">Previous</button>\n                    <button id=\"pageNext\" data-i18n=\"general.next\" type=\"button\" class=\"btn btn-outline btn-active-primary disabled\">Next</button>\n                </div>\n            </div>\n\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    const prefListTypeName = 'sftpgo_pref_{{.LoggedUser.Username}}_iplist_type';\n    const prefListFilter = 'sftpgo_pref_{{.LoggedUser.Username}}_iplist_search_filter';\n    const listType = getListType();\n    const listFilter = getSearchFilter();\n\n    const pageSize = 15;\n    const paginationData = new Map();\n\n    function saveListType(val) {\n        localStorage.setItem(prefListTypeName, val);\n    }\n\n    function getListType() {\n        return localStorage.getItem(prefListTypeName);\n    }\n\n    function saveSearchFilter() {\n        let val = $(\"#idSearch\").val();\n        if (val){\n            localStorage.setItem(prefListFilter, val);\n        } else {\n            localStorage.removeItem(prefListFilter);\n        }\n    }\n\n    function getSearchFilter() {\n        return localStorage.getItem(prefListFilter);\n    }\n\n    function resetPagination() {\n        $('#pagePrevious').addClass(\"disabled\");\n        $('#pageNext').addClass(\"disabled\");\n        $('#paginationContainer').addClass(\"d-none\");\n        paginationData.delete(\"firstIpOrNet\");\n        paginationData.delete(\"lastIpOrNet\");\n        paginationData.set(\"prevClicked\",false);\n        paginationData.set(\"nextClicked\",false);\n    }\n\n    function handleResponseData(data) {\n        let length = data.length;\n        let isNext = paginationData.get(\"nextClicked\");\n        let isPrev = paginationData.get(\"prevClicked\");\n\n        if (length > pageSize) {\n            data.pop();\n            length--;\n            if (isPrev || isNext){\n                $('#pagePrevious').removeClass(\"disabled\");\n            }\n            $('#pageNext').removeClass(\"disabled\");\n        } else {\n            if (isPrev){\n                $('#pagePrevious').addClass(\"disabled\");\n                $('#pageNext').removeClass(\"disabled\");\n            } else if (isNext){\n                $('#pagePrevious').removeClass(\"disabled\");\n                $('#pageNext').addClass(\"disabled\");\n            } else {\n                $('#pageNext').addClass(\"disabled\");\n            }\n        }\n        if (isPrev){\n            data = data.reverse();\n        }\n        if (length > 0){\n            paginationData.set(\"firstIpOrNet\",data[0].ipornet);\n            paginationData.set(\"lastIpOrNet\",data[length-1].ipornet);\n            $('#paginationContainer').removeClass(\"d-none\");\n        } else {\n            resetPagination();\n        }\n\n        return data;\n    }\n\n    function getSearchURL(){\n        let listType = encodeURIComponent($(\"#idListType\").val());\n        let filter = encodeURIComponent($(\"#idSearch\").val());\n        let limit = pageSize + 1;\n        let from = \"\";\n        let order = \"ASC\"\n        if (paginationData.get(\"nextClicked\") && paginationData.has(\"lastIpOrNet\")){\n            from = encodeURIComponent(paginationData.get(\"lastIpOrNet\"));\n        }\n        if (paginationData.get(\"prevClicked\") && paginationData.has(\"firstIpOrNet\")){\n            from = encodeURIComponent(paginationData.get(\"firstIpOrNet\"));\n            order = \"DESC\";\n        }\n        return \"{{.IPListsURL}}\"+`/${listType}?filter=${filter}&from=${from}&limit=${limit}&order=${order}`;\n    }\n\n    function checkSelectedListType(val) {\n        switch (val){\n            case \"1\":\n                //{{- if not .IsAllowListEnabled}}\n                showToast(0, 'ip_list.allow_list_disabled');\n                //{{- end}}\n                break;\n            case \"2\":\n                //{{- if not .HasDefender}}\n                showToast(0, 'ip_list.defender_disabled');\n                //{{- end}}\n                break;\n            case \"3\":\n                //{{- if not .RateLimitersStatus}}\n                showToast(0, 'ip_list.ratelimiters_disabled');\n                //{{- end}}\n                break;\n        }\n    }\n\n    function deleteAction(listType, ipNet) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.IPListURL}}' + \"/\" + encodeURIComponent(listType)+ \"/\" + encodeURIComponent(ipNet);\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: getSearchURL(),\n                    dataSrc: handleResponseData,\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"ipornet\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"protocols\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data == 0){\n                                    return $.t('ip_list.any');\n                                }\n                                const protocols = [];\n                                if ((data & 1) != 0){\n                                    protocols.push('SSH');\n                                }\n                                if ((data & 2) != 0){\n                                    protocols.push('FTP');\n                                }\n                                if ((data & 4) != 0){\n                                    protocols.push('DAV');\n                                }\n                                if ((data & 8) != 0){\n                                    protocols.push('HTTP');\n                                }\n                                return protocols.join(', ');\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"mode\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data == 1){\n                                    return $.t('ip_list.allow');\n                                }\n                                return $.t('ip_list.deny');\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"description\",\n                        visible: true,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                return `<button class=\"btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                                    <span data-i18n=\"general.actions\" class=\"fs-6\">Actions</span>\n\t\t\t\t\t\t\t\t\t\t            <i class=\"ki-duotone ki-down fs-5 ms-1 rotate-180\"></i>\n                                                </button>\n                                                <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">\n                                                    <div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t                <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n\t\t\t\t\t\t\t\t\t\t            </div>\n                                                    <div class=\"menu-item px-3\">\n                                                        <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t            </div>\n                                                </div>`;\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                processing: true,\n                lengthChange: false,\n                searching: false,\n                paging: false,\n                info: false,\n                ordering: false,\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                initComplete: function(settings, json) {\n                    handleColumnVisibility($('#idListType').val());\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        function handleColumnVisibility(val) {\n            switch (val){\n                case '2':\n                dt.column(2).visible(true);\n                break;\n            default:\n                dt.column(2).visible(false);\n            }\n        }\n\n        function doSearch(){\n            dt.clear().draw();\n            dt.ajax.url(getSearchURL()).load();\n        }\n\n        var handleDatatableActions = function () {\n            $('#idListType').on(\"change\", function(e){\n                let val = $(this).find(\"option:selected\").attr('value');\n                saveListType(val);\n                handleColumnVisibility(val);\n                doSearch();\n                checkSelectedListType(val);\n            });\n\n            $('#search_button').on(\"click\", function(){\n                resetPagination();\n                doSearch();\n                saveSearchFilter();\n            });\n\n            $('#pagePrevious').on(\"click\", function(e){\n                e.preventDefault();\n                this.blur();\n                paginationData.set(\"prevClicked\",true);\n                paginationData.set(\"nextClicked\",false);\n                doSearch();\n            });\n            $('#pageNext').on(\"click\", function(e){\n                e.preventDefault();\n                this.blur();\n                paginationData.set(\"prevClicked\",false);\n                paginationData.set(\"nextClicked\",true);\n                doSearch();\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.IPListURL}}' + \"/\" + encodeURIComponent(rowData['type'])+\"/\"+encodeURIComponent(rowData['ipornet']));\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    let rowData = dt.row(parent).data();\n                    deleteAction(rowData['type'],rowData['ipornet']);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nload\", function(){\n        resetPagination();\n        if (listType === '1' || listType === '3'){\n            $('#idListType').val(listType);\n        } else {\n            $('#idListType').val('2');\n        }\n        $('#idListType').trigger('change');\n\n        if (listFilter){\n            $('#idSearch').val(listFilter);\n        } else {\n            $('#idSearch').val('');\n        }\n        $(\"#idAdd\").on(\"click\", function(){\n            window.location.href = '{{.IPListURL}}'+\"/\"+encodeURIComponent($(\"#idListType\").val());\n        });\n    });\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n        checkSelectedListType(listType);\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/maintenance.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"maintenance.restore\" class=\"card-title section-title\">Restore</h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"restore_form\" enctype=\"multipart/form-data\" action=\"{{.RestorePath}}\" method=\"POST\">\n\n            <div class=\"form-group row\">\n                <label for=\"idBackupFile\" data-i18n=\"maintenance.backup_file\" class=\"col-md-3 col-form-label\">Backup file</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idBackupFile\" type=\"file\" accept=\"application/json\" required class=\"form-control\" name=\"backup_file\" aria-describedby=\"idBackupFileHelp\" />\n                    <div id=\"idBackupFileHelp\" class=\"form-text\" data-i18n=\"maintenance.backup_file_help\"></div>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idMode\" data-i18n=\"general.mode\" class=\"col-md-3 col-form-label\">Mode</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idMode\" name=\"mode\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        <option data-i18n=\"maintenance.restore_mode1\" value=\"1\">add only</option>\n                        <option data-i18n=\"maintenance.restore_mode0\" value=\"0\">add and update</option>\n                        <option data-i18n=\"maintenance.restore_mode2\" value=\"2\">add, update and disconnect</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idQuota\" data-i18n=\"maintenance.after_restore\" class=\"col-md-3 col-form-label\">After restore</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idQuota\" name=\"quota\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                        <option data-i18n=\"maintenance.quota_mode0\" value=\"0\">no quota update</option>\n                        <option data-i18n=\"maintenance.quota_mode1\" value=\"1\">update quota</option>\n                        <option data-i18n=\"maintenance.quota_mode2\" value=\"2\">update quota if the user has quota restrictions</option>\n                    </select>\n                </div>\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"maintenance.restore\" class=\"indicator-label\">\n                        Restore\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n    </div>\n</div>\n\n<div class=\"card shadow-sm mt-10\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"maintenance.backup\" class=\"card-title section-title\">Backup</h3>\n    </div>\n    <div class=\"card-body\">\n        <div>\n            <a href=\"{{.BackupPath}}?output-data=1\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn btn-primary btn-block\">\n                <span data-i18n=\"maintenance.backup_do\">Backup your data</span>\n            </a>\n        </div>\n    </div>\n</div>\n\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    $(document).on(\"i18nshow\", function(){\n        $('#restore_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/mfa.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n{{- if .TOTPConfig.Enabled}}\n<div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5\">\n    <i class=\"ki-duotone ki-shield-tick fs-2tx text-primary me-4\">\n        <span class=\"path1\"></span>\n        <span class=\"path2\"></span>\n    </i>\n    <div class=\"d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap\">\n        <div class=\"mb-3 mb-md-0 fw-semibold\">\n            <h4 class=\"text-gray-900 fw-bold\">\n                <span data-i18n=\"2fa.msg_enabled\"></span> {{- if gt (len .TOTPConfigs) 1 }}\n                ({{$.TOTPConfig.ConfigName}}) {{- end}}\n            </h4>\n            <div class=\"fs-6 text-gray-800 pe-7\">\n                <span data-i18n=\"2fa.msg_info\"></span>\n            </div>\n        </div>\n        <button type=\"button\" id=\"disable_btn\" class=\"btn btn-danger ms-4 px-6 align-self-center text-nowrap\">\n            <span data-i18n=\"general.disable\" class=\"indicator-label\">\n                Disable\n            </span>\n            <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                Please wait...\n                <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n            </span>\n        </button>\n    </div>\n</div>\n{{- end}}\n\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"2fa.title\" class=\"card-title section-title\">Two-factor authentication using Authenticator apps</h3>\n    </div>\n    <div class=\"card-body\">\n        {{- if not .TOTPConfig.Enabled}}\n        <div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5\">\n            <i class=\"ki-duotone ki-shield-tick fs-2tx text-primary me-4\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n            <div class=\"d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap\">\n                <div class=\"mb-3 mb-md-0 fw-semibold\">\n                    <h4 class=\"text-gray-900 fw-bold\">\n                        <span data-i18n=\"2fa.msg_disabled\">Secure Your Account</span>\n                    </h4>\n                    <div class=\"fs-6 text-gray-800 pe-7\">\n                        <span data-i18n=\"2fa.msg_info\"></span>\n                    </div>\n                </div>\n            </div>\n        </div>\n        {{- end}}\n        <div class=\"form-group row mt-10\">\n            <label for=\"id_config\" data-i18n=\"general.configuration\" class=\"col-md-3 col-form-label\">Configuration</label>\n            <div class=\"col-md-9\">\n                <select id=\"id_config\" name=\"config_name\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                    <option value=\"\">---</option>\n                    {{range .TOTPConfigs}}\n                    <option value=\"{{.}}\" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>\n                    {{end}}\n                </select>\n            </div>\n        </div>\n\n        <div class=\"d-flex justify-content-end mt-15\">\n            {{- if .TOTPConfig.Enabled }}\n            <button type=\"button\" id=\"generate_secret_btn\" class=\"btn btn-light-primary px-10 me-10 d-none\">\n                <span data-i18n=\"2fa.generate\" class=\"indicator-label\">\n                    Generate new secret\n                </span>\n                <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                    Please wait...\n                    <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                </span>\n            </button>\n            {{- end}}\n            <button type=\"button\" id=\"save_btn\" class=\"btn btn-primary px-10 d-none\">\n                <span id=\"save_label\" class=\"indicator-label\"></span>\n                <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                    Please wait...\n                    <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                </span>\n            </button>\n        </div>\n\n    </div>\n</div>\n\n{{- if .TOTPConfig.Enabled}}\n<div class=\"accordion shadow-sm my-10\" id=\"id_accordion\">\n    <div class=\"accordion-item\">\n        <h2 class=\"accordion-header\" id=\"accordion_rec_codes\">\n            <button class=\"accordion-button section-title text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#accordion_rec_codes_body\" aria-expanded=\"false\" aria-controls=\"accordion_rec_codes_body\">\n                <span data-i18n=\"2fa.recovery_codes\">\n                    Recovery codes\n                </span>\n            </button>\n        </h2>\n        <div id=\"accordion_rec_codes_body\" class=\"accordion-collapse collapse\" aria-labelledby=\"accordion_rec_codes\" data-bs-parent=\"#id_accordion\">\n            <div class=\"accordion-body\">\n                <div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6\">\n                    <div class=\"d-flex flex-stack flex-grow-1\">\n                        <div class=\"fs-5 fw-semibold text-break text-wrap text-gray-800\">\n                            <p data-i18n=\"2fa.recovery_codes_msg1\">Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor configuration.</p>\n                            <p data-i18n=\"2fa.recovery_codes_msg2\">To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>\n                            <p data-i18n=\"2fa.recovery_codes_msg3\" class=\"fs-4 fw-bold\">If you generate new recovery codes, you automatically invalidate old ones.</p>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"d-flex justify-content-end mt-10\">\n                    <button type=\"button\" id=\"generate_recovery_code_btn\" class=\"btn btn-light-primary px-10 me-10\">\n                        <span data-i18n=\"2fa.recovery_codes_generate\" class=\"indicator-label\">\n                            Generate\n                        </span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                    <button type=\"button\" id=\"view_recovery_code_btn\" class=\"btn btn-primary px-10\">\n                        <span data-i18n=\"2fa.recovery_codes_view\" id=\"save_label\" class=\"indicator-label\">View</span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- end}}\n\n{{- define \"modals\"}}\n<div class=\"modal fade\" id=\"recovery_codes_modal\" tabindex=\"-1\">\n    <div class=\"modal-dialog\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header\">\n                <h3 id=\"idRecoveryCodesTitle\" class=\"modal-title\"></h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body\">\n                <div id=\"idRecoveryCodesList\" class=\"d-flex flex-column\">\n                </div>\n            </div>\n            <div class=\"modal-footer\">\n                <button data-i18n=\"general.ok\" class=\"btn btn-primary\" type=\"button\" data-bs-dismiss=\"modal\">OK</button>\n            </div>\n        </div>\n    </div>\n</div>\n\n<div class=\"modal fade\" id=\"qrcode_modal\" tabindex=\"-1\">\n    <div class=\"modal-dialog modal-dialog-centered modal-lg\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header\">\n                <h3 data-i18n=\"2fa.setup_title\" class=\"modal-title\">Set up two-factor authentication</h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body scroll-y pt-10 pb-15 px-lg-17\">\n                <div class=\"text-gray-700 fw-semibold fs-6 mb-10\">\n                    <span data-i18n=\"2fa.setup_msg\">\n                        Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.\n                    </span>\n                </div>\n                <div id=\"id_qr_code_container\" class=\"pt-5 text-center\">\n                </div>\n                <div class=\"notice d-flex bg-light-warning rounded border-warning border border-dashed my-10 p-6\">\n                    <i class=\"ki-duotone ki-information-5 fs-2tx text-warning me-4\">\n                        <span class=\"path1\"></span>\n                        <span class=\"path2\"></span>\n                        <span class=\"path3\"></span>\n                    </i>\n                    <div class=\"d-flex flex-stack flex-grow-1\">\n                        <div class=\"fw-semibold\">\n                            <div class=\"fs-6 text-gray-800\">\n                                <span data-i18n=\"2fa.setup_help\">If you have trouble using the QR code, select manual entry on your app, and enter the code:</span>\n                                <span id=\"id_secret\" class=\"fw-bold text-gray-900 pt-2\"></span>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n\n                <div id=\"errorModalMsg\" class=\"d-none rounded border-warning border border-dashed bg-light-warning d-flex align-items-center p-5 mb-10\">\n                    <i class=\"ki-duotone ki-information-5 fs-3x text-warning me-5\"><span class=\"path1\"></span><span class=\"path2\"></span><span class=\"path3\"></span></i>\n                    <div class=\"text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10\">\n                        <span id=\"errorModalTxt\"></span>\n                    </div>\n                    <button id=\"id_dismiss_error_modal_msg\" type=\"button\" class=\"position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 btn btn-icon btn-sm btn-active-light-primary ms-sm-auto\">\n                        <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                    </button>\n                </div>\n\n                <div class=\"fv-row\">\n                    <input data-i18n=\"[placeholder]login.auth_code\" type=\"text\" id=\"id_passcode\" name=\"passcode\" class=\"form-control form-control-lg form-control-solid\" placeholder=\"Enter authentication code\" spellcheck=\"false\" />\n                </div>\n            </div>\n            <div class=\"modal-footer\">\n                <button data-i18n=\"general.cancel\" type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button>\n                <button type=\"button\" class=\"btn btn-primary ms-6\" id=\"passcode_btn\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    const qrModal = new bootstrap.Modal('#qrcode_modal');\n    const recCodesModal = new bootstrap.Modal('#recovery_codes_modal');\n\n    function onConfigChanged() {\n        let selectedConfig = $('#id_config option:selected').val();\n        if (selectedConfig == \"\"){\n            $('#save_btn').addClass(\"d-none\");\n            $('#generate_secret_btn').addClass(\"d-none\");\n        } else {\n            //{{- if .TOTPConfig.Enabled }}\n            if (selectedConfig == \"{{.TOTPConfig.ConfigName}}\"){\n                $('#save_label').text($.t('general.submit'));\n            } else {\n                $('#save_label').text($.t('general.enable'));\n            }\n            $('#save_btn').removeClass(\"d-none\");\n            $('#generate_secret_btn').removeClass(\"d-none\");\n            //{{- else}}\n            $('#save_btn').removeClass(\"d-none\");\n            $('#save_label').text($.t('general.enable'));\n            $('#generate_secret_btn').addClass(\"d-none\");\n            //{{- end}}\n        }\n    }\n\n    function generateRecoveryCodes() {\n        el = document.querySelector('#generate_recovery_code_btn');\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.post('{{.RecCodesURL}}', null, {\n            \ttimeout: 15000,\n                headers: {\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n       \t    }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                $('#idRecoveryCodesTitle').text($.t('2fa.new_recovery_codes'));\n                let recList = $('#idRecoveryCodesList');\n                recList.empty();\n                $.each(response.data, function(key, item) {\n                    itemCode = escapeHTML(item);\n                    recList.append(`<li class=\"d-flex align-items-center py-2 fw-semibold fs-5 text-gray-800\"><span class=\"bullet bullet-dot me-5\"></span>${itemCode}</li>`);\n                });\n                recCodesModal.show();\n            }).catch(function (error){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                ModalAlert.fire({\n                    text: $.t('2fa.recovery_codes_gen_err'),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            });\n    }\n\n    function viewRecoveryCodes() {\n        el = document.querySelector('#view_recovery_code_btn');\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.get('{{.RecCodesURL}}',{\n            \ttimeout: 15000,\n                headers: {\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n       \t    }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                $('#idRecoveryCodesTitle').text($.t('2fa.recovery_codes'));\n                let recList = $('#idRecoveryCodesList');\n                recList.empty();\n                $.each(response.data, function(key, item) {\n                    itemCode = escapeHTML(item.code);\n                    let txtStyleClass = \"\";\n                    if (item.used) {\n                        txtStyleClass = \"line-through\";\n                    }\n                    recList.append(`<li class=\"d-flex align-items-center py-2 fw-semibold fs-5 text-gray-800 ${txtStyleClass}\"><span class=\"bullet bullet-dot me-5\"></span>${itemCode}</li>`);\n                });\n                recCodesModal.show();\n            }).catch(function (error){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                ModalAlert.fire({\n                    text: $.t('2fa.recovery_codes_get_err'),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            });\n    }\n\n    function disableConfig() {\n        //{{- if .RequireTwoFactor}}\n        ModalAlert.fire({\n            text: $.t('2fa.require'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.ok'),\n            customClass: {\n                confirmButton: \"btn btn-primary\"\n            }\n        });\n        //{{- else}}\n        ModalAlert.fire({\n            text: $.t('2fa.disable_question'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.confirm'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                doSaveConfig(document.querySelector('#disable_btn'), null, false, true);\n            }\n        });\n        //{{- end}}\n    }\n\n    function validatePasscode() {\n        el = document.querySelector('#passcode_btn');\n        let errDivEl = $('#errorModalMsg');\n        let errTxtEl = $('#errorModalTxt');\n        let errorMessage = '2fa.auth_code_invalid';\n        let passcode = $('#id_passcode').val();\n\n        errDivEl.addClass(\"d-none\");\n        if (passcode == \"\") {\n            setI18NData(errTxtEl, '2fa.auth_code_required');\n            errDivEl.removeClass(\"d-none\");\n            return;\n        }\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.post(\"{{.ValidateTOTPURL}}\", {\n                passcode: passcode,\n                config_name: $('#id_config option:selected').val(),\n                secret: $('#id_secret').text()\n            }, {\n                headers: {\n                    timeout: 15000,\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n            }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                if (!response.data.message) {\n                    setI18NData(errTxtEl, errorMessage);\n                    errDivEl.removeClass(\"d-none\");\n                    return;\n                }\n                qrModal.hide();\n                doSaveConfig(null, null, true, false);\n            }).catch(function (error){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                setI18NData(errTxtEl, errorMessage);\n                errDivEl.removeClass(\"d-none\");\n            });\n    }\n\n    function confirmGenerateSecret() {\n        ModalAlert.fire({\n            text: $.t('2fa.generate_question'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.confirm'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                el = document.querySelector('#generate_secret_btn');\n                configName = $('#id_config option:selected').val();\n                generateSecret(el,configName);\n            }\n        });\n    }\n\n    function generateSecret(el, configName) {\n        if (!el || !configName){\n            confirmGenerateSecret();\n            return;\n        }\n\n        let errorMessage =  '2fa.auth_secret_gen_err';\n        $('#id_secret').text(\"\");\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.post(\"{{.GenerateTOTPURL}}\", {\n                config_name: configName\n            }, {\n                headers: {\n                    timeout: 15000,\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n            }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                if (!response.data.secret) {\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                    return;\n                }\n                $('#id_secret').text(response.data.secret);\n                $('#errorModalMsg').addClass(\"d-none\");\n                $('#id_passcode').val(\"\");\n                let qrCodeContainer = document.getElementById(\"id_qr_code_container\");\n                clearChilds(qrCodeContainer);\n                let qrCodeImg = document.createElement(\"img\");\n                qrCodeImg.classList.add(\"mw-150px\");\n                qrCodeImg.src = \"{{.MFAURL}}/qrcode?url=\"+encodeURIComponent(response.data.url);\n                qrCodeImg.alt = $.t('general.qr_code');\n                qrCodeContainer.appendChild(qrCodeImg);\n                qrModal.show();\n            }).catch(function (error){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                ModalAlert.fire({\n                    text: $.t(errorMessage),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            });\n    }\n\n    function doSaveConfig(el, configName, full, disabled) {\n        if (!el){\n            el = document.querySelector('#save_btn');\n        }\n        if (!configName) {\n            configName = $('#id_config option:selected').val();\n        }\n        let errorMessage = '2fa.save_err';\n        let postData = {};\n        if (full){\n            postData.config_name = configName;\n            postData.secret = {\n                status: \"Plain\",\n                payload: $('#id_secret').text()\n            }\n        }\n        if (disabled){\n            postData.enabled = false;\n        } else {\n            postData.enabled = true;\n        }\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.post(\"{{.SaveTOTPURL}}\", postData, {\n                headers: {\n                    timeout: 15000,\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n            }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                if (!response.data.message) {\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                    return;\n                }\n                ModalAlert.fire({\n                    text: $.t('general.config_saved'),\n                    icon: \"success\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: 'btn btn-primary'\n                    }\n                }).then((result) => {\n                    if (result.isConfirmed){\n                        location.reload();\n                    }\n                });\n            }).catch(function (error) {\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                ModalAlert.fire({\n                    text: $.t(errorMessage),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            });\n\n    }\n\n    function saveConfig() {\n        let selectedConfig = $('#id_config option:selected').val();\n        let saveBtn = document.querySelector('#save_btn');\n        if (selectedConfig == \"{{.TOTPConfig.ConfigName}}\"){\n            doSaveConfig(saveBtn, selectedConfig, false, false);\n            return;\n        }\n        generateSecret(saveBtn, selectedConfig);\n    }\n\n    $(document).on(\"i18nshow\", function(){\n        onConfigChanged();\n\n        var dismissErrorModalBtn = $('#id_dismiss_error_modal_msg');\n        if (dismissErrorModalBtn){\n            dismissErrorModalBtn.on(\"click\",function(){\n                $('#errorModalMsg').addClass(\"d-none\");\n            });\n        }\n\n        var disableBtn = $('#disable_btn');\n        if (disableBtn){\n            disableBtn.on(\"click\", function(){\n                disableConfig();\n            });\n        }\n\n        var generateSecretBtn = $('#generate_secret_btn');\n        if (generateSecretBtn){\n            generateSecretBtn.on(\"click\", function(){\n                generateSecret();\n            });\n        }\n\n        var saveBtn = $('#save_btn');\n        if (saveBtn){\n            saveBtn.on(\"click\", function(){\n                saveConfig();\n            });\n        }\n\n        var generateRecoveryCodeBtn = $('#generate_recovery_code_btn');\n        if (generateRecoveryCodeBtn){\n            generateRecoveryCodeBtn.on(\"click\", function(){\n                generateRecoveryCodes();\n            });\n        }\n\n        var viewRecoveryCodesBtn = $('#view_recovery_code_btn');\n        if (viewRecoveryCodesBtn){\n            viewRecoveryCodesBtn.on(\"click\", function(){\n                viewRecoveryCodes();\n            });\n        }\n\n        var passcodeBtn = $('#passcode_btn');\n        if (passcodeBtn){\n            passcodeBtn.on(\"click\", function() {\n                validatePasscode();\n            });\n        }\n\n        var configSelect = $('#id_config');\n        if (configSelect){\n            configSelect.on(\"change\", function(){\n                onConfigChanged();\n            });\n        }\n\n    });\n\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/profile.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"general.my_profile\" class=\"card-title section-title\">My profile</h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"page_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n            <div class=\"form-group row\">\n                <label for=\"idEmail\" data-i18n=\"general.email\" class=\"col-md-3 col-form-label\">Email</label>\n                <div class=\"col-md-9\">\n                    <input type=\"email\" class=\"form-control\" id=\"idEmail\" name=\"email\" placeholder=\"\" spellcheck=\"false\"\n                        value=\"{{.Email}}\" maxlength=\"255\" autocomplete=\"off\">\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input type=\"text\" class=\"form-control\" id=\"idDescription\" name=\"description\" placeholder=\"\"\n                        value=\"{{.Description}}\" maxlength=\"255\">\n                </div>\n            </div>\n\n            <div class=\"form-group row align-items-center mt-10\">\n                <label data-i18n=\"general.api_key_auth\" class=\"col-md-3 col-form-label\" for=\"idAllowAPIKeyAuth\">API key authentication</label>\n                <div class=\"col-md-9\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idAllowAPIKeyAuth\" name=\"allow_api_key_auth\" {{if .AllowAPIKeyAuth}}checked=\"checked\"{{end}}/>\n                        <label data-i18n=\"general.api_key_auth_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idAllowAPIKeyAuth\">\n                            Allow to impersonate yourself, in REST API, using an API key\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n\n        </form>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    KTUtil.onDOMContentLoaded(function () {\n        $(\"#page_form\").submit(function (event) {\n            let submitButton = document.querySelector('#form_submit');\n            submitButton.setAttribute('data-kt-indicator', 'on');\n            submitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/role.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"role_form\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n\n            <div class=\"form-group row\">\n                <label for=\"idRoleName\" data-i18n=\"general.name\" class=\"col-md-3 col-form-label\">Name</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idRoleName\" type=\"text\" placeholder=\"\" name=\"name\" value=\"{{.Role.Name}}\" maxlength=\"255\" autocomplete=\"off\"\n                        spellcheck=\"false\" required {{if eq .Mode 2}}class=\"form-control-plaintext readonly-input\" readonly{{else}}class=\"form-control\"{{end}} />\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idDescription\" type=\"text\" class=\"form-control\" name=\"description\" value=\"{{.Role.Description}}\" maxlength=\"255\">\n                </div>\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    $(document).on(\"i18nshow\", function(){\n        $('#role_form').submit(function (event) {\n\t\t\tlet submitButton = document.querySelector('#form_submit');\n\t\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\t\tsubmitButton.disabled = true;\n        });\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/roles.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"role.view_manage\" class=\"card-title section-title\">View and manage roles</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                    <button id=\"export_button\" type=\"button\" class=\"btn btn-light-primary ms-3\" data-table-filter=\"export\">\n                        <span data-i18n=\"general.export\" class=\"indicator-label\">\n                            Export\n                        </span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                </div>\n\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <button type=\"button\" class=\"btn btn-light-primary rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom\" data-kt-menu-permanent=\"true\">\n                        <span data-i18n=\"general.colvis\">Column visibility</span>\n                        <i class=\"ki-duotone ki-down fs-3 rotate-180 ms-3 me-0\"></i>\n                    </button>\n                    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4\" data-kt-menu=\"true\">\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColMembers\" />\n                            <label class=\"form-check-label\" for=\"checkColMembers\">\n                                <span data-i18n=\"general.members\" class=\"text-gray-800 fs-6\">Members</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColDesc\" />\n                            <label class=\"form-check-label\" for=\"checkColDesc\">\n                                <span data-i18n=\"general.description\" class=\"text-gray-800 fs-6\">Description</span>\n                            </label>\n                        </div>\n                    </div>\n                    {{- if .LoggedUser.HasPermission \"*\"}}\n                    <a href=\"{{.RoleURL}}\" class=\"btn btn-primary ms-5\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                    {{- end}}\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"general.name\">Name</th>\n                        <th data-i18n=\"general.members\">Members</th>\n                        <th data-i18n=\"general.description\">Description</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/papaparse/papaparse.min.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/file-saver/FileSaver.min.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function deleteAction(name) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.RoleURL}}' + \"/\" + encodeURIComponent(name);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.RolesURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"name\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"users\",\n                        name: \"members\",\n                        defaultContent: \"\",\n                        searchable: false,\n                        orderable: false,\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                let users = 0;\n                                if (row.users){\n                                    users = row.users.length;\n                                }\n                                let admins = 0;\n                                if (row.admins){\n                                    admins = row.admins.length;\n                                }\n                                return $.t('general.members_summary', {users: users, admins: admins});\n                            }\n                            return \"\";\n                        }\n                    },\n                    {\n                        data: \"description\",\n                        name: \"description\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let numActions = 0;\n                                let actions = `<button class=\"btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                                    <span data-i18n=\"general.actions\" class=\"fs-6\">Actions</span>\n\t\t\t\t\t\t\t\t\t\t            <i class=\"ki-duotone ki-down fs-5 ms-1 rotate-180\"></i>\n                                                </button>\n                                                <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">`;\n\n                                //{{- if .LoggedUser.HasPermission \"*\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n                                             <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                if (numActions > 0){\n                                    actions+=`</div>`;\n                                    return actions;\n                                }\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                colReorder: {\n                    enable: true,\n                    fixedColumnsLeft: 1\n                },\n                stateLoadParams: function (settings, data) {\n                    if (data.search.search){\n                        const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                        filterSearch.value = data.search.search;\n                    }\n                },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n            dt.on('columns-reordered', function(e, settings, details){\n                drawAction();\n            });\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        function handleColVisibilityCheckbox(el, selector) {\n            let index = dt.column(selector).index();\n            el.off(\"change\");\n            el.prop('checked', dt.column(index).visible());\n            el.on(\"change\", function(e){\n                let index = dt.column(selector).index();\n                dt.column(index).visible($(this).is(':checked'));\n                dt.draw('page');\n            });\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n            handleColVisibilityCheckbox($('#checkColMembers'), \"members:name\");\n            handleColVisibilityCheckbox($('#checkColDesc'), \"description:name\");\n\n            const exportButton = $(document.querySelector('[data-table-filter=\"export\"]'));\n            exportButton.on('click', function(e){\n                e.preventDefault();\n                this.blur();\n                this.setAttribute('data-kt-indicator', 'on');\n\t\t\t    this.disabled = true;\n\n                let data = [];\n                dt.rows({ search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                    let line = {};\n                    let rowData = dt.row(rowIdx).data();\n\n                    line[\"Name\"] = rowData[\"name\"];\n                    if (rowData[\"users\"]){\n                        line[\"Users\"] = rowData[\"users\"].join(\", \");\n                    } else {\n                        line[\"Users\"] = \"\";\n                    }\n                    if (rowData[\"admins\"]){\n                        line[\"Admins\"] = rowData[\"admins\"].join(\", \");\n                    } else {\n                        line[\"Admins\"] = \"\";\n                    }\n                    if (rowData[\"description\"]){\n                        line[\"Description\"] = rowData[\"description\"];\n                    } else {\n                        line[\"Description\"] = \"\";\n                    }\n\n                    data.push(line);\n                });\n\n                let csv = Papa.unparse(data, {\n                    quotes: false,\n\t                quoteChar: '\"',\n\t                escapeChar: '\"',\n\t                delimiter: \",\",\n\t                header: true,\n\t                newline: \"\\r\\n\",\n\t                skipEmptyLines: false,\n\t                columns: null,\n                    escapeFormulae: true\n                });\n                let blob = new Blob([csv], {type: \"text/csv\"});\n                let ts = moment().format(\"YYYY_MM_DD_HH_mm_ss\");\n                saveAs(blob, `roles_${ts}.csv`);\n\n                this.removeAttribute('data-kt-indicator');\n                this.disabled = false;\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.RoleURL}}' + \"/\" + encodeURIComponent(rowData['name']));\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    let row = dt.row(parent).data();\n                    if (row.admins && row.admins.length > 0){\n                        ModalAlert.fire({\n                            text: $.t('role.err_delete_referenced'),\n                            icon: \"warning\",\n                            confirmButtonText: $.t('general.ok'),\n                            customClass: {\n                                confirmButton: \"btn btn-primary\"\n                            }\n                        });\n                        return;\n                    }\n                    deleteAction(row['name']);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/status.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n\n        <div class=\"card\">\n            <div class=\"card-header bg-light\">\n                <h3 data-i18n=\"status.ssh\" class=\"card-title section-title-inner\">SSH/SFTP server</h3>\n            </div>\n            <div class=\"card-body\">\n                <p class=\"fs-3 fw-semibold mb-4\" {{if .Status.SSH.IsActive}}data-i18n=\"status.active\"{{else}}data-i18n=\"status.disabled\"{{end}}></p>\n                {{- if .Status.SSH.IsActive}}\n                <div class=\"d-flex flex-column\">\n                    {{- range .Status.SSH.Bindings}}\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.address\"></span> \"{{.GetAddress}}\"\n                    </p>\n                    {{- if .HasProxy}}\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.proxy_on\"></span>\n                    </p>\n                    {{- end}}\n                    {{- end}}\n                </div>\n                {{- range .Status.SSH.HostKeys}}\n                <div class=\"d-flex flex-column mt-10\">\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.host_key\"></span> \"{{.Path}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.fingeprint\"></span> \"{{.Fingerprint}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.algorithms\"></span> \"{{.GetAlgosAsString}}\"\n                    </p>\n                </div>\n                {{- end}}\n                <div class=\"d-flex flex-column mt-10\">\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ssh_commands\"></span> \"{{.Status.SSH.GetSSHCommandsAsString}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ssh_auths\"></span> \"{{.Status.SSH.GetSupportedAuthsAsString}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ssh_pub_key_algo\"></span> \"{{.Status.SSH.GetPublicKeysAlgosAsString}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ssh_mac_algo\"></span> \"{{.Status.SSH.GetMACsAsString}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ssh_kex_algo\"></span> \"{{.Status.SSH.GetKEXsAsString}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ssh_cipher_algo\"></span> \"{{.Status.SSH.GetCiphersAsString}}\"\n                    </p>\n                </div>\n                {{- end}}\n            </div>\n        </div>\n\n        <div class=\"card mt-10\">\n            <div class=\"card-header bg-light\">\n                <h3 data-i18n=\"status.ftp\" class=\"card-title section-title-inner\">FTP server</h3>\n            </div>\n            <div class=\"card-body\">\n                <p class=\"fs-3 fw-semibold mb-4\" {{if .Status.FTP.IsActive}}data-i18n=\"status.active\"{{else}}data-i18n=\"status.disabled\"{{end}}></p>\n                {{- if .Status.FTP.IsActive}}\n                <div class=\"d-flex flex-column\">\n                    {{- range .Status.FTP.Bindings}}\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.address\"></span> \"{{.GetAddress}}\"\n                    </p>\n                    {{- if .HasProxy}}\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.proxy_on\"></span>\n                    </p>\n                    {{- end}}\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.tls\"></span>&nbsp;<span data-i18n=\"{{.GetTLSDescription}}\"></span>\n                    </p>\n                    {{- if .ForcePassiveIP}}\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ftp_passive_ip\"></span> \"{{.ForcePassiveIP}}\"\n                    </p>\n                    {{- end}}\n                    {{- range .PassiveIPOverrides}}\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ftp_passive_ip\"></span> \"{{.IP}} ({{.GetNetworksAsString}})\"\n                    </p>\n                    {{- end}}\n                    {{- end}}\n                </div>\n                <div class=\"d-flex flex-column mt-10\">\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.ftp_passive_range\"></span> \"{{.Status.FTP.PassivePortRange.Start}}-{{.Status.FTP.PassivePortRange.End}}\"\n                    </p>\n                </div>\n                {{- end}}\n            </div>\n        </div>\n\n        <div class=\"card mt-10\">\n            <div class=\"card-header bg-light\">\n                <h3 data-i18n=\"status.webdav\" class=\"card-title section-title-inner\">WebDAV server</h3>\n            </div>\n            <div class=\"card-body\">\n                <p class=\"fs-3 fw-semibold mb-4\" {{if .Status.WebDAV.IsActive}}data-i18n=\"status.active\"{{else}}data-i18n=\"status.disabled\"{{end}}></p>\n                {{- if .Status.WebDAV.IsActive}}\n                <div class=\"d-flex flex-column\">\n                    {{- range .Status.WebDAV.Bindings}}\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.address\"></span> \"{{.GetAddress}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"general.protocol\"></span> {{if .EnableHTTPS}} HTTPS {{else}} HTTP {{end}}\n                    </p>\n                    {{- end}}\n                </div>\n                {{- end}}\n            </div>\n        </div>\n\n        <div class=\"card mt-10\">\n            <div class=\"card-header bg-light\">\n                <h3 data-i18n=\"general.data_provider\" class=\"card-title section-title-inner\">Database</h3>\n            </div>\n            <div class=\"card-body\">\n                <p class=\"fs-3 fw-semibold mb-4\">\n                    <span {{if .Status.DataProvider.IsActive}}data-i18n=\"status.active\"{{else}}data-i18n=\"status.error\" class=\"text-warning\"{{end}}></span>\n                    {{if .Status.DataProvider.Error}}&nbsp;<span class=\"text-warning\">\"{{.Status.DataProvider.Error}}\"</span>{{end}}\n                </p>\n                <div class=\"d-flex flex-column\">\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"general.driver\"></span> \"{{.Status.DataProvider.Driver}}\"\n                    </p>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"card mt-10\">\n            <div class=\"card-header bg-light\">\n                <h3 data-i18n=\"ip_list.defender_list\" class=\"card-title section-title-inner\">Defender</h3>\n            </div>\n            <div class=\"card-body\">\n                <p class=\"fs-3 fw-semibold mb-4\" {{if .Status.Defender.IsActive}}data-i18n=\"status.active\"{{else}}data-i18n=\"status.disabled\"{{end}}></p>\n            </div>\n        </div>\n\n        <div class=\"card mt-10\">\n            <div class=\"card-header bg-light\">\n                <h3 data-i18n=\"ip_list.allow_list\" class=\"card-title section-title-inner\">Allow list</h3>\n            </div>\n            <div class=\"card-body\">\n                <p class=\"fs-3 fw-semibold mb-4\" {{if .Status.AllowList.IsActive}}data-i18n=\"status.active\"{{else}}data-i18n=\"status.disabled\"{{end}}></p>\n            </div>\n        </div>\n\n        <div class=\"card mt-10\">\n            <div class=\"card-header bg-light\">\n                <h3 data-i18n=\"status.rate_limiters\" class=\"card-title section-title-inner\">Rate limiters</h3>\n            </div>\n            <div class=\"card-body\">\n                <p class=\"fs-3 fw-semibold mb-4\" {{if .Status.RateLimiters.IsActive}}data-i18n=\"status.active\"{{else}}data-i18n=\"status.disabled\"{{end}}></p>\n                {{- if .Status.RateLimiters.IsActive}}\n                <div class=\"d-flex flex-column\">\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"ip_list.protocols\"></span> \"{{.Status.RateLimiters.GetProtocolsAsString}}\"\n                    </p>\n                </div>\n                {{- end}}\n            </div>\n        </div>\n\n        <div class=\"card mt-10\">\n            <div class=\"card-header bg-light\">\n                <h3 data-i18n=\"title.two_factor_auth\" class=\"card-title section-title-inner\">Two-factor authentication</h3>\n            </div>\n            <div class=\"card-body\">\n                <p class=\"fs-3 fw-semibold mb-4\" {{if .Status.MFA.IsActive}}data-i18n=\"status.active\"{{else}}data-i18n=\"status.disabled\"{{end}}></p>\n                {{- if .Status.MFA.IsActive}}\n                {{range .Status.MFA.TOTPConfigs}}\n                <div class=\"d-flex flex-column\">\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"general.configuration\"></span> \"{{.Name}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"general.issuer\"></span> \"{{.Issuer}}\"\n                    </p>\n                    <p class=\"fs-5 fw-semibold\">\n                        <span class=\"text-muted\" data-i18n=\"status.algorithm\"></span> \"{{.Algo}}\"\n                    </p>\n                </div>\n                {{- end}}\n                {{- end}}\n            </div>\n        </div>\n\n    </div>\n</div>\n{{- end}}"
  },
  {
    "path": "templates/webadmin/user.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"{{.Title}}\" class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- if eq .Mode 3}}\n        <div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5\">\n            <i class=\"ki-duotone ki-shield-tick fs-2tx text-primary me-4\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n            <div class=\"d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap\">\n                <div class=\"mb-3 mb-md-0 fw-semibold\">\n                    <h4 class=\"text-gray-900 fw-bold\">\n                        <span data-i18n=\"user.template_title\">Create one or more new users from this template</span>\n                    </h4>\n                    <div class=\"fs-6 text-gray-800 pe-7\">\n                        <p data-i18n=\"general.template_placeholders\" class=\"mt-5\">The following placeholders are supported</p>\n                        <ul>\n                            <li><span class=\"text-info\">%username%</span>, <span data-i18n=\"user.template_username_placeholder\">replaced with the specified username</span></li>\n                            <li><span class=\"text-info\">%password%</span>, <span data-i18n=\"user.template_password_placeholder\">replaced with the specified password</span></li>\n                        </ul>\n                        <p data-i18n=\"user.template_help1\">Placeholders will be replaced in paths and credentials of the configured storage backend.</p>\n                        <p data-i18n=\"user.template_help2\">The generated users can be saved or exported. Exported users can be imported from the \"Maintenance\" section of this SFTPGo instance or another.</p>\n                    </div>\n                </div>\n            </div>\n        </div>\n        {{- end}}\n        {{- template \"errmsg\" .Error}}\n        <form id=\"user_form\" enctype=\"multipart/form-data\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\" {{if eq .Mode 3}}target=\"_blank\" rel=\"noopener noreferrer\"{{end}}>\n            {{- if eq .Mode 3}}\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"title.users\" class=\"card-title section-title-inner\">Users</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"template_users\">\n                        <p class=\"fs-5 fw-semibold mb-4\" data-i18n=\"user.template_help\">For each user set the username and at least one of the password and public key</p>\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"template_users\">\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]login.username\" type=\"text\" class=\"form-control\" name=\"tpl_username\" autocomplete=\"off\" spellcheck=\"false\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]login.password\" type=\"password\" class=\"form-control\" name=\"tpl_password\" autocomplete=\"new-password\" spellcheck=\"false\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                            <textarea class=\"form-control\" name=\"tpl_public_keys\" rows=\"5\" placeholder=\"Paste your public key here\"></textarea>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n\n                        <div class=\"form-group row align-items-center mt-10\">\n                            <label data-i18n=\"user.require_pwd_change\" class=\"col-md-3 col-form-label\" for=\"idTmplRequirePasswordChange\">Require password change</label>\n                            <div class=\"col-md-9\">\n                                <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                                    <input class=\"form-check-input\" type=\"checkbox\" id=\"idTmplRequirePasswordChange\" name=\"tpl_require_password_change\"/>\n                                </div>\n                            </div>\n                        </div>\n\n                    </div>\n                </div>\n            </div>\n            <input type=\"hidden\" name=\"username\" id=\"idUsername\" value=\"{{.User.Username}}\">\n            {{- else}}\n            <div class=\"form-group row\">\n                <label for=\"idUsername\" data-i18n=\"login.username\" class=\"col-md-3 col-form-label\">Username</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idUsername\" type=\"text\" placeholder=\"\" name=\"username\" value=\"{{.User.Username}}\" maxlength=\"255\" autocomplete=\"off\"\n                        spellcheck=\"false\" required {{if ge .Mode 2}}class=\"form-control-plaintext readonly-input\" readonly{{else}}class=\"form-control\"{{end}} />\n                </div>\n            </div>\n            {{- end}}\n\n            {{- if .Roles}}\n            <div class=\"form-group row mt-10\">\n                <label for=\"idRole\" data-i18n=\"general.role\" class=\"col-md-3 col-form-label\">Role</label>\n                <div class=\"col-md-9\">\n                    <select id=\"idRole\" name=\"role\" data-i18n=\"[data-placeholder]general.role_placeholder\" class=\"form-select\" data-control=\"i18n-select2\" data-placeholder=\"Select a role\" data-allow-clear=\"true\" aria-describedby=\"idRoleHelp\">\n                        <option value=\"\"></option>\n                        {{- range .Roles}}\n                        <option value=\"{{.Name}}\" {{if eq $.User.Role .Name}}selected{{end}}>{{.Name}}</option>\n                        {{- end}}\n                    </select>\n                    <div id=\"idRoleHelp\" data-i18n=\"user.role_help\" class=\"form-text\">\n                        Users with a role can be managed by global administrators and administrators with the same role\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            {{- if ne .Mode 3}}\n            <div class=\"form-group row mt-10\">\n                <label for=\"idPassword\" data-i18n=\"login.password\" class=\"col-md-3 col-form-label\">Password</label>\n                <div class=\"col-md-9\">\n                    <input id=\"idPassword\" type=\"password\" class=\"form-control\" name=\"password\" autocomplete=\"new-password\"\n                        spellcheck=\"false\" value=\"{{.User.Password}}\" />\n                </div>\n            </div>\n\n            <div class=\"form-group row align-items-center mt-10\">\n                <label data-i18n=\"user.require_pwd_change\" class=\"col-md-3 col-form-label\" for=\"idRequirePasswordChange\">Require password change</label>\n                <div class=\"col-md-9\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idRequirePasswordChange\" name=\"require_password_change\" {{if .User.Filters.RequirePasswordChange}}checked=\"checked\"{{end}}/>\n                        <label data-i18n=\"user.require_pwd_change_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idRequirePasswordChange\">\n                            User must change password from the WebClient at next login\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"general.pub_keys\" class=\"card-title section-title-inner\">Public keys</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"public_keys\">\n                        {{- template \"infomsg-no-mb\" \"general.pub_keys_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"public_keys\">\n                                {{- range $idx, $val := .User.PublicKeys}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <textarea data-i18n=\"[placeholder]general.pub_key_placeholder\" class=\"form-control\" name=\"public_key\" rows=\"4\"\n                                                placeholder=\"Paste your public key here\">{{$val}}</textarea>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <textarea data-i18n=\"[placeholder]general.pub_key_placeholder\" class=\"form-control\" name=\"public_key\" rows=\"4\"\n                                                placeholder=\"Paste your public key here\"></textarea>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card mt-10 {{if not .CanUseTLSCerts}}d-none{{end}}\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"user.tls_certs\" class=\"card-title section-title-inner\">TLS certificates</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"tls_certs\">\n                        {{- template \"infomsg-no-mb\" \"user.tls_certs_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"tls_certs\">\n                                {{- range $idx, $val := .User.Filters.TLSCerts}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <textarea data-i18n=\"[placeholder]user.tls_cert_help\" class=\"form-control\" name=\"tls_cert\" rows=\"4\">{{$val}}</textarea>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <textarea data-i18n=\"[placeholder]user.tls_cert_help\" class=\"form-control\" name=\"tls_cert\" rows=\"4\"></textarea>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            {{- if .Groups}}\n            <div class=\"card mt-10 {{if .LoggedUser.Filters.Preferences.HideGroups}}d-none{{end}}\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"title.groups\" class=\"card-title section-title-inner\">Groups</h3>\n                </div>\n                <div class=\"card-body\">\n                    {{- template \"infomsg\" \"user.groups_help\"}}\n                    <div class=\"form-group row\">\n                        <label for=\"idPrimaryGroup\" data-i18n=\"user.primary_group\" class=\"col-md-3 col-form-label\">Primary group</label>\n                        <div class=\"col-md-9\">\n                            <select id=\"idPrimaryGroup\" name=\"primary_group\" data-i18n=\"[data-placeholder]general.group_placeholder\" class=\"form-select\" data-control=\"i18n-select2\" data-placeholder=\"Select a group\" data-allow-clear=\"true\">\n                                <option value=\"\"></option>\n                                {{- range .Groups}}\n                                <option value=\"{{.Name}}\" {{if $.User.HasPrimaryGroup .Name}}selected{{end}}>{{.Name}}</option>\n                                {{- end}}\n                            </select>\n                        </div>\n                    </div>\n                    <div class=\"form-group row mt-10\">\n                        <label for=\"idSecondaryGroup\" data-i18n=\"user.secondary_groups\" class=\"col-md-3 col-form-label\">\n                            Secondary groups\n                        </label>\n                        <div class=\"col-md-9\">\n                            <select id=\"idSecondaryGroup\" name=\"secondary_groups\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                {{- range .Groups}}\n                                <option value=\"{{.Name}}\" {{if $.User.HasSecondaryGroup .Name}}selected{{end}}>{{.Name}}</option>\n                                {{- end}}\n                            </select>\n                        </div>\n                    </div>\n                    <div class=\"form-group row mt-10\">\n                        <label for=\"idMembershipGroup\" data-i18n=\"user.membership_groups\" class=\"col-md-3 col-form-label\">\n                            Membership groups\n                        </label>\n                        <div class=\"col-md-9\">\n                            <select id=\"idMembershipGroup\" name=\"membership_groups\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                {{- range .Groups}}\n                                <option value=\"{{.Name}}\" {{if $.User.HasMembershipGroup .Name}}selected{{end}}>{{.Name}}</option>\n                                {{- end}}\n                            </select>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            {{- template \"fshtml\" .FsWrapper}}\n            {{- if .VirtualFolders}}\n            <div class=\"card mt-10 {{if .LoggedUser.Filters.Preferences.HideVirtualFolders}}d-none{{end}}\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"title.folders\" class=\"card-title section-title-inner\">Virtual folders</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"virtual_folders\">\n                        {{- template \"infomsg-no-mb\" \"user.virtual_folders_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"virtual_folders\">\n                                {{- range $idx, $val := .User.VirtualFolders}}\n                                <div data-repeater-item>\n                                    <div data-repeater-item>\n                                        <div class=\"form-group row\">\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input data-i18n=\"[placeholder]virtual_folders.mount_path\" type=\"text\" class=\"form-control\" name=\"vfolder_path\" value=\"{{$val.VirtualPath}}\" />\n                                            </div>\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <select name=\"vfolder_name\" data-i18n=\"[data-placeholder]general.folder_placeholder\" class=\"form-select select-repetear\" data-placeholder=\"Select a folder\" data-allow-clear=\"true\">\n                                                    <option value=\"\"></option>\n                                                    {{- range $.VirtualFolders}}\n                                                    <option value=\"{{.Name}}\" {{- if eq $val.Name .Name}} selected{{- end}}>{{.Name}}</option>\n                                                    {{- end}}\n                                                </select>\n                                            </div>\n                                            <div class=\"col-md-3 mt-3 mt-md-8\">\n                                                <input type=\"text\" class=\"form-control\" name=\"vfolder_quota_size\" value=\"{{HumanizeBytes $val.QuotaSize}}\" />\n                                                <div class=\"form-text\" data-i18n=\"virtual_folders.quota_size\"></div>\n                                            </div>\n                                            <div class=\"col-md-2 mt-3 mt-md-8\">\n                                                <input type=\"number\" min=\"-1\" class=\"form-control\" name=\"vfolder_quota_files\" value=\"{{$val.QuotaFiles}}\" />\n                                                <div class=\"form-text\" data-i18n=\"virtual_folders.quota_files\"></div>\n                                            </div>\n                                            <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                <a href=\"#\" data-repeater-delete\n                                                    class=\"btn btn-light-danger ps-5 pe-4\">\n                                                    <i class=\"ki-duotone ki-trash fs-2\">\n                                                        <span class=\"path1\"></span>\n                                                        <span class=\"path2\"></span>\n                                                        <span class=\"path3\"></span>\n                                                        <span class=\"path4\"></span>\n                                                        <span class=\"path5\"></span>\n                                                    </i>\n                                                </a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]virtual_folders.mount_path\" type=\"text\" class=\"form-control\" name=\"vfolder_path\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <select name=\"vfolder_name\" data-i18n=\"[data-placeholder]general.folder_placeholder\" class=\"form-select select-repetear\" data-placeholder=\"Select a folder\" data-allow-clear=\"true\">\n                                                <option value=\"\"></option>\n                                                {{- range .VirtualFolders}}\n                                                <option value=\"{{.Name}}\">{{.Name}}</option>\n                                                {{- end}}\n                                            </select>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <input type=\"text\" class=\"form-control\" name=\"vfolder_quota_size\" value=\"\" />\n                                            <div class=\"form-text\" data-i18n=\"virtual_folders.quota_size\"></div>\n                                        </div>\n                                        <div class=\"col-md-2 mt-3 mt-md-8\">\n                                            <input type=\"number\" min=\"-1\" class=\"form-control\" name=\"vfolder_quota_files\" value=\"\" />\n                                            <div class=\"form-text\" data-i18n=\"virtual_folders.quota_files\"></div>\n                                        </div>\n                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"accordion shadow-sm mt-10 {{- if eq .LoggedUser.Filters.Preferences.VisibleUserPageSections 0}} d-none{{- end}}\" id=\"accordionUser\">\n\n                <div class=\"accordion-item {{- if .LoggedUser.Filters.Preferences.HideProfile}} d-none{{- end}}\">\n                    <h2 class=\"accordion-header\" id=\"headingProfile\">\n                        <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#collapseProfile\" aria-expanded=\"true\" aria-controls=\"collapseProfile\">\n                            <span data-i18n=\"title.profile\">Profile</span>\n                        </button>\n                    </h2>\n                    <div id=\"collapseProfile\" class=\"accordion-collapse collapse\" aria-labelledby=\"headingProfile\" data-bs-parent=\"#accordionUser\">\n                        <div class=\"accordion-body\">\n\n                            <div class=\"form-group row\">\n                                <label for=\"idStatus\" data-i18n=\"general.status\" class=\"col-md-3 col-form-label\">Status</label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idStatus\" name=\"status\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                                        <option data-i18n=\"general.active\" value=\"1\" {{- if eq .User.Status 1 }} selected{{- end}}>Active</option>\n                                        <option data-i18n=\"general.inactive\" value=\"0\" {{- if eq .User.Status 0 }} selected{{- end}}>Inactive</option>\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"id_expiration\" data-i18n=\"general.expiration\" class=\"col-md-3 col-form-label\">Expiration</label>\n                                <div class=\"col-md-9 d-flex\">\n                                    <input data-i18n=\"[placeholder]general.expiration_help\" id=\"id_expiration\" class=\"form-control\" placeholder=\"Pick an expiration date\" />\n                                    <button class=\"btn btn-icon btn-light-danger ms-2 d-none\" id=\"id_expiration_clear\">\n                                        <i class=\"ki-solid ki-cross fs-1\"></i>\n                                    </button>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idEmail\" data-i18n=\"general.email\" class=\"col-md-3 col-form-label\">Email</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idEmail\" type=\"email\" class=\"form-control\" placeholder=\"\" name=\"email\" value=\"{{.User.Email}}\"\n                                        maxlength=\"255\" autocomplete=\"off\" spellcheck=\"false\" />\n                                </div>\n                            </div>\n\n                            <div class=\"card mt-10\">\n                                <div class=\"card-header bg-light\">\n                                    <h3 data-i18n=\"user.additional_emails\" class=\"card-title section-title-inner\">Additional emails</h3>\n                                </div>\n                                <div class=\"card-body\">\n                                    <div id=\"additional_emails\">\n                                        <div class=\"form-group\">\n                                            <div data-repeater-list=\"additional_emails\">\n                                                {{- range $idx, $val := .User.Filters.AdditionalEmails}}\n                                                <div data-repeater-item>\n                                                    <div class=\"form-group row\">\n                                                        <div class=\"col-md-10 mt-3 mt-md-8\">\n                                                            <input type=\"email\" class=\"form-control\" placeholder=\"\" name=\"additional_email\" value=\"{{$val}}\" maxlength=\"255\" autocomplete=\"off\" spellcheck=\"false\" />\n                                                        </div>\n                                                        <div class=\"col-md-2 mt-3 mt-md-8\">\n                                                            <a href=\"#\" data-repeater-delete\n                                                                class=\"btn btn-light-danger\">\n                                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                                    <span class=\"path1\"></span>\n                                                                    <span class=\"path2\"></span>\n                                                                    <span class=\"path3\"></span>\n                                                                    <span class=\"path4\"></span>\n                                                                    <span class=\"path5\"></span>\n                                                                </i>\n                                                                <span data-i18n=\"general.delete\">Delete</span>\n                                                            </a>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                                {{- else}}\n                                                <div data-repeater-item>\n                                                    <div class=\"form-group row\">\n                                                        <div class=\"col-md-10 mt-3 mt-md-8\">\n                                                            <input type=\"email\" class=\"form-control\" placeholder=\"\" name=\"additional_email\" value=\"\" maxlength=\"255\" autocomplete=\"off\" spellcheck=\"false\" />\n                                                        </div>\n                                                        <div class=\"col-md-2 mt-3 mt-md-8\">\n                                                            <a href=\"#\" data-repeater-delete\n                                                                class=\"btn btn-light-danger\">\n                                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                                    <span class=\"path1\"></span>\n                                                                    <span class=\"path2\"></span>\n                                                                    <span class=\"path3\"></span>\n                                                                    <span class=\"path4\"></span>\n                                                                    <span class=\"path5\"></span>\n                                                                </i>\n                                                                <span data-i18n=\"general.delete\">Delete</span>\n                                                            </a>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                                {{- end}}\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group mt-5\">\n                                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                                <span data-i18n=\"general.add\">Add</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n                            {{- template \"user_group_profile\" .User.Filters}}\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idDescription\" type=\"text\" class=\"form-control\" name=\"description\" value=\"{{.User.Description}}\" maxlength=\"255\">\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idAdditionalInfo\" data-i18n=\"general.additional_info\" class=\"col-md-3 col-form-label\">Additional info</label>\n                                <div class=\"col-md-9\">\n                                    <textarea id=\"idAdditionalInfo\" class=\"form-control\" name=\"additional_info\" rows=\"3\">{{.User.AdditionalInfo}}</textarea>\n                                </div>\n                            </div>\n\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"accordion-item {{- if .LoggedUser.Filters.Preferences.HideACLs}} d-none{{- end}}\">\n                    <h2 class=\"accordion-header\" id=\"headingPermissions\">\n                        <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#collapsePermissions\" aria-expanded=\"true\" aria-controls=\"collapsePermissions\">\n                            <span data-i18n=\"general.acls\">ACLs</span>\n                        </button>\n                    </h2>\n                    <div id=\"collapsePermissions\" class=\"accordion-collapse collapse\" aria-labelledby=\"headingPermissions\" data-bs-parent=\"#accordionUser\">\n                        <div class=\"accordion-body\">\n\n                            <div class=\"form-group row\">\n                                <label for=\"idPermissions\" data-i18n=\"general.permissions\" class=\"col-md-3 col-form-label\">\n                                    Permissions\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idPermissions\" name=\"permissions\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                                        {{- range $validPerm := .ValidPerms}}\n                                        <option value=\"{{$validPerm}}\" {{- range $perm :=$.RootDirPerms }}{{- if eq $perm $validPerm}} selected{{- end}}{{- end}}>{{$validPerm}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"card mt-10\">\n                                <div class=\"card-header bg-light\">\n                                    <h3 data-i18n=\"filters.directory_permissions\" class=\"card-title section-title-inner\">Per-directory permissions</h3>\n                                </div>\n                                <div class=\"card-body\">\n                                    <div id=\"directory_permissions\">\n                                        {{- template \"infomsg-no-mb\" \"filters.directory_permissions_help\"}}\n                                        <div class=\"form-group\">\n                                            <div data-repeater-list=\"directory_permissions\">\n                                                {{- range $idx, $dirPerms := .User.GetSubDirPermissions -}}\n                                                <div data-repeater-item>\n                                                    <div class=\"form-group row\">\n                                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                            <input data-i18n=\"[placeholder]filters.directory_path_help\" type=\"text\" class=\"form-control\" name=\"sub_perm_path\" value=\"{{$dirPerms.Path}}\" />\n                                                        </div>\n                                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                            <select name=\"sub_perm_permissions\" data-i18n=\"[data-placeholder]general.permissions\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                                                                {{- range $validPerm := $.ValidPerms}}\n                                                                <option value=\"{{$validPerm}}\" {{- range $perm := $dirPerms.Permissions }}{{- if eq $perm $validPerm}} selected{{- end}}{{- end}}>{{$validPerm}}</option>\n                                                                {{- end}}\n                                                            </select>\n                                                        </div>\n                                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                            <a href=\"#\" data-repeater-delete\n                                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                                    <span class=\"path1\"></span>\n                                                                    <span class=\"path2\"></span>\n                                                                    <span class=\"path3\"></span>\n                                                                    <span class=\"path4\"></span>\n                                                                    <span class=\"path5\"></span>\n                                                                </i>\n                                                            </a>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                                {{- else}}\n                                                <div data-repeater-item>\n                                                    <div class=\"form-group row\">\n                                                        <div class=\"col-md-6 mt-3 mt-md-8\">\n                                                            <input data-i18n=\"[placeholder]filters.directory_path_help\" type=\"text\" class=\"form-control\" name=\"sub_perm_path\" value=\"\" />\n                                                        </div>\n                                                        <div class=\"col-md-5 mt-3 mt-md-8\">\n                                                            <select name=\"sub_perm_permissions\" data-i18n=\"[data-placeholder]general.permissions\" class=\"form-select select-repetear\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple>\n                                                                {{- range $validPerm := .ValidPerms}}\n                                                                <option value=\"{{$validPerm}}\">{{$validPerm}}</option>\n                                                                {{- end}}\n                                                            </select>\n                                                        </div>\n                                                        <div class=\"col-md-1 mt-3 mt-md-8\">\n                                                            <a href=\"#\" data-repeater-delete\n                                                                class=\"btn btn-light-danger ps-5 pe-4\">\n                                                                <i class=\"ki-duotone ki-trash fs-2\">\n                                                                    <span class=\"path1\"></span>\n                                                                    <span class=\"path2\"></span>\n                                                                    <span class=\"path3\"></span>\n                                                                    <span class=\"path4\"></span>\n                                                                    <span class=\"path5\"></span>\n                                                                </i>\n                                                            </a>\n                                                        </div>\n                                                    </div>\n                                                </div>\n                                                {{- end}}\n                                            </div>\n                                        </div>\n\n                                        <div class=\"form-group mt-5\">\n                                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                                <span data-i18n=\"general.add\">Add</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n\n                            {{- template \"user_group_perms\" .User.Filters}}\n\n                            {{- template \"user_group_access_time\" .User.Filters}}\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idMaxSessions\" data-i18n=\"filters.max_sessions\" class=\"col-md-3 col-form-label\">Max sessions</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idMaxSessions\" type=\"number\" min=\"0\" class=\"form-control\" name=\"max_sessions\" value=\"{{.User.MaxSessions}}\" aria-describedby=\"idMaxSessionsHelp\" />\n                                    <div id=\"idMaxSessionsHelp\" class=\"form-text\" data-i18n=\"filters.max_sessions_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idProtocols\" data-i18n=\"filters.denied_protocols\" class=\"col-md-3 col-form-label\">\n                                    Denied protocols\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idProtocols\" name=\"denied_protocols\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $protocol := .ValidProtocols}}\n                                        <option value=\"{{$protocol}}\" {{- range $p :=$.User.Filters.DeniedProtocols }}{{- if eq $p $protocol}} selected{{- end}}{{- end}}>{{$protocol}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idLoginMethods\" data-i18n=\"filters.denied_login_methods\" class=\"col-md-3 col-form-label\">\n                                    Denied login methods\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idLoginMethods\" name=\"denied_login_methods\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple aria-describedby=\"idLoginMethodsHelp\">\n                                        {{- range $method := .ValidLoginMethods}}\n                                        <option value=\"{{$method}}\" {{- range $m :=$.User.Filters.DeniedLoginMethods }}{{- if eq $m $method}} selected{{- end}}{{- end}}>{{$method}}</option>\n                                        {{- end}}\n                                    </select>\n                                    <div id=\"idLoginMethodsHelp\" data-i18n=\"filters.denied_login_methods_help\" class=\"form-text\">\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idTwoFactorProtocols\" data-i18n=\"2fa.require_for\" class=\"col-md-3 col-form-label\">\n                                    Require 2FA for\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idTwoFactorProtocols\" name=\"required_two_factor_protocols\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $protocol := .TwoFactorProtocols}}\n                                        <option value=\"{{$protocol}}\" {{- range $p :=$.User.Filters.TwoFactorAuthProtocols }}{{- if eq $p $protocol}} selected{{- end}}{{- end}}>{{$protocol}}</option>\n                                        {{end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idWebClient\" data-i18n=\"filters.web_client_options\" class=\"col-md-3 col-form-label\">\n                                    Web client/REST API\n                                </label>\n                                <div class=\"col-md-9\">\n                                    <select id=\"idWebClient\" name=\"web_client_options\" class=\"form-select\" data-control=\"i18n-select2\" data-close-on-select=\"false\" multiple>\n                                        {{- range $option := .WebClientOptions}}\n                                        <option value=\"{{$option}}\" {{- range $p :=$.User.Filters.WebClient }}{{- if eq $p $option}}selected{{- end}}{{- end}}>{{$option}}</option>\n                                        {{- end}}\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idDeniedIP\" data-i18n=\"general.denied_ip_mask\" class=\"col-md-3 col-form-label\">Denied IP/Mask</label>\n                                <div class=\"col-md-9\">\n                                    <textarea class=\"form-control\" id=\"idDeniedIP\" name=\"denied_ip\" aria-describedby=\"idDeniedIPHelp\"\n                                        rows=\"3\">{{.User.GetDeniedIPAsString}}</textarea>\n                                    <div id=\"idDeniedIPHelp\" class=\"form-text\" data-i18n=\"general.ip_mask_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10\">\n                                <label for=\"idAllowedIP\" data-i18n=\"general.allowed_ip_mask\" class=\"col-md-3 col-form-label\">Allowed IP/Mask</label>\n                                <div class=\"col-md-9\">\n                                    <textarea class=\"form-control\" id=\"idAllowedIP\" name=\"allowed_ip\" aria-describedby=\"idAllowedIPHelp\"\n                                        rows=\"3\">{{.User.GetAllowedIPAsString}}</textarea>\n                                    <div id=\"idAllowedIPHelp\" class=\"form-text\" data-i18n=\"general.ip_mask_help\"></div>\n                                </div>\n                            </div>\n\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"accordion-item {{- if .LoggedUser.Filters.Preferences.HideDiskQuotaAndBandwidthLimits}} d-none{{- end}}\">\n                    <h2 class=\"accordion-header\" id=\"headingQuota\">\n                        <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#collapseQuota\" aria-expanded=\"true\" aria-controls=\"collapseQuota\">\n                            <span data-i18n=\"general.quota_limits\">Disk quota and bandwidth limits</span>\n                        </button>\n                    </h2>\n                    <div id=\"collapseQuota\" class=\"accordion-collapse collapse\" aria-labelledby=\"headingQuota\" data-bs-parent=\"#accordionUser\">\n                        <div class=\"accordion-body\">\n\n                            {{template \"user_group_quota\" .User}}\n\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"accordion-item {{- if .LoggedUser.Filters.Preferences.HideAdvancedSettings}} d-none{{- end}}\">\n                    <h2 class=\"accordion-header\" id=\"headingAdvanced\">\n                        <button class=\"accordion-button section-title-inner text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#collapseAdvanced\" aria-expanded=\"true\" aria-controls=\"collapseAdvanced\">\n                            <span data-i18n=\"general.advanced_settings\">Advanced settings</span>\n                        </button>\n                    </h2>\n                    <div id=\"collapseAdvanced\" class=\"accordion-collapse collapse\" aria-labelledby=\"headingAdvanced\" data-bs-parent=\"#accordionUser\">\n                        <div class=\"accordion-body\">\n\n                            {{template \"user_group_advanced\" .User.Filters}}\n\n                            <div class=\"form-group row mt-10 {{if not .User.HasExternalAuth}}d-none{{end}}\">\n                                <label for=\"idExtAuthCacheTime\" data-i18n=\"filters.external_auth_cache_time\" class=\"col-md-3 col-form-label\">External auth cache time</label>\n                                <div class=\"col-md-9\">\n                                    <input id=\"idExtAuthCacheTime\" type=\"number\" min=\"0\" class=\"form-control\" name=\"external_auth_cache_time\" value=\"{{.User.Filters.ExternalAuthCacheTime}}\" aria-describedby=\"idExtAuthCacheTimeHelp\" />\n                                    <div id=\"idExtAuthCacheTimeHelp\" class=\"form-text\" data-i18n=\"filters.external_auth_cache_time_help\"></div>\n                                </div>\n                            </div>\n\n                            <div class=\"form-group row mt-10 {{if not .CanImpersonate}}d-none{{end}}\">\n                                <label for=\"idUID\" class=\"col-md-3 col-form-label\">UID</label>\n                                <div class=\"col-md-3\">\n                                    <input id=\"idUID\" type=\"number\" min=\"0\" max=\"2147483647\" class=\"form-control\" name=\"uid\" value=\"{{.User.UID}}\" />\n                                </div>\n                                <div class=\"col-md-1\"></div>\n                                <label for=\"idGID\" class=\"col-md-2 col-form-label\">GID</label>\n                                <div class=\"col-md-3\">\n                                    <input id=\"idGID\" type=\"number\" min=\"0\" max=\"2147483647\" class=\"form-control\" name=\"gid\" value=\"{{.User.GID}}\" />\n                                </div>\n                            </div>\n\n                        </div>\n                    </div>\n                </div>\n\n            </div>\n\n            {{- if eq .Mode 2}}\n            <div class=\"form-group row align-items-center mt-10\">\n                <label data-i18n=\"user.disconnect\" class=\"col-md-3 col-form-label\" for=\"idDisconnect\">Disconnect the user after the update</label>\n                <div class=\"col-md-9\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idDisconnect\" name=\"disconnect\"/>\n                        <label data-i18n=\"user.disconnect_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idDisconnect\">\n                            This way you force the user to login again, if connected, and so to use the new configuration\n                        </label>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"expiration_date\" id=\"hidden_start_datetime\" value=\"\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\" name=\"form_action\" value=\"submit\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/it.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/de.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/fr.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/zh.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/es.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    $(document).on(\"i18nload\", function(){\n        onFilesystemChanged('{{.User.FsConfig.Provider}}');\n\n        $('#idFilesystem').on(\"change\", function(){\n            onFilesystemChanged(this.value);\n        });\n    });\n\n    $(document).on(\"i18nshow\", function(){\n            //{{- if eq .Mode 3}}\n            initRepeater('#template_users');\n            //{{- else}}\n            initRepeater('#public_keys');\n            //{{- end}}\n            initRepeater('#virtual_folders');\n            initRepeater('#directory_permissions');\n            initRepeater('#directory_patterns');\n            initRepeater('#src_bandwidth_limits');\n            initRepeater('#tls_certs');\n            initRepeater('#access_time_restrictions');\n            initRepeater('#additional_emails');\n            initRepeaterItems();\n            //{{- if .Error}}\n            //{{- if ne .LoggedUser.Filters.Preferences.VisibleUserPageSections 0}}\n            $('#accordionUser .collapse').removeAttr(\"data-bs-parent\").collapse('show');\n            //{{- end}}\n            //{{- end}}\n            const picker = $('#id_expiration').flatpickr({\n                enableTime: false,\n                time_24hr: true,\n                formatDate: (date, format, locale) => {\n                    return $.t('general.datetime', {\n                                val: new Date(date),\n                                formatParams: {\n                                    val: { year: 'numeric', month: 'numeric', day: 'numeric' },\n                                }\n                            });\n                },\n                defaultHour: 23,\n                defaultMinute: 59,\n                locale: flatpickrLocale(),\n                onChange: function(selectedDates, dateStr, instance) {\n                    if (selectedDates.length > 0){\n                        $('#id_expiration_clear').removeClass(\"d-none\");\n                    } else {\n                        $('#id_expiration_clear').addClass(\"d-none\");\n                    }\n                }\n            });\n            //{{ if gt .User.ExpirationDate 0 }}\n            let input_dt = moment('{{.User.ExpirationDate }}', 'x').format('YYYY-MM-DD');\n            picker.setDate(input_dt, true);\n            //{{ end }}\n\n            $('#id_expiration_clear').on(\"click\", function(e){\n                e.preventDefault();\n                picker.clear();\n            });\n\n            $(\"#user_form\").submit(function (event) {\n                $('#hidden_start_datetime').val(\"\");\n                let dt = picker.selectedDates;\n                if (dt.length > 0) {\n                    let d = dt[0];\n                    if (d) {\n                        let dateString = moment.utc(d).format('YYYY-MM-DD HH:mm:ss');\n                        $('#hidden_start_datetime').val(dateString);\n                    }\n                }\n                //{{- if ne .Mode 3}}\n                let submitButton = document.querySelector('#form_submit');\n                submitButton.setAttribute('data-kt-indicator', 'on');\n                submitButton.disabled = true;\n                //{{- end}}\n            });\n\n        });\n</script>\n{{- end}}\n"
  },
  {
    "path": "templates/webadmin/users.html",
    "content": "<!--\nCopyright (C) 2024 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"user.view_manage\" class=\"card-title section-title\">View and manage users</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                    <button id=\"export_button\" type=\"button\" class=\"btn btn-light-primary ms-3\" data-table-filter=\"export\">\n                        <span data-i18n=\"general.export\" class=\"indicator-label\">\n                            Export\n                        </span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                </div>\n\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <button type=\"button\" class=\"btn btn-light-primary rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom\" data-kt-menu-permanent=\"true\">\n                        <span data-i18n=\"general.colvis\">Column visibility</span>\n                        <i class=\"ki-duotone ki-down fs-3 rotate-180 ms-3 me-0\"></i>\n                    </button>\n                    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4\" data-kt-menu=\"true\">\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColStatus\" />\n                            <label class=\"form-check-label\" for=\"checkColStatus\">\n                                <span data-i18n=\"general.status\" class=\"text-gray-800 fs-6\">Status</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColLastLogin\" />\n                            <label class=\"form-check-label\" for=\"checkColLastLogin\">\n                                <span data-i18n=\"general.last_login\" class=\"text-gray-800 fs-6\">Last login</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColEmail\" />\n                            <label class=\"form-check-label\" for=\"checkColEmail\">\n                                <span data-i18n=\"general.email\" class=\"text-gray-800 fs-6\">Email</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColStorage\" />\n                            <label class=\"form-check-label\" for=\"checkColStorage\">\n                                <span data-i18n=\"storage.label\" class=\"text-gray-800 fs-6\">Storage</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColRole\" />\n                            <label class=\"form-check-label\" for=\"checkColRole\">\n                                <span data-i18n=\"general.role\" class=\"text-gray-800 fs-6\">Role</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkCol2FA\" />\n                            <label class=\"form-check-label\" for=\"checkCol2FA\">\n                                <span data-i18n=\"title.two_factor_auth_short\" class=\"text-gray-800 fs-6\">2FA</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColDiskQuota\" />\n                            <label class=\"form-check-label\" for=\"checkColDiskQuota\">\n                                <span data-i18n=\"fs.quota_usage.disk\" class=\"text-gray-800 fs-6\">Disk quota</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColTransferQuota\" />\n                            <label class=\"form-check-label\" for=\"checkColTransferQuota\">\n                                <span data-i18n=\"fs.quota_usage.transfer\" class=\"text-gray-800 fs-6\">Transfer quota</span>\n                            </label>\n                        </div>\n                        <div class=\"menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid\">\n                            <input type=\"checkbox\" class=\"form-check-input\" value=\"\" id=\"checkColGroups\" />\n                            <label class=\"form-check-label\" for=\"checkColGroups\">\n                                <span data-i18n=\"title.groups\" class=\"text-gray-800 fs-6\">Groups</span>\n                            </label>\n                        </div>\n                    </div>\n                    {{- if .LoggedUser.HasPermission \"add_users\"}}\n                    <a href=\"{{.UserURL}}\" class=\"btn btn-primary ms-5\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                    {{- end}}\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"login.username\">Username</th>\n                        <th data-i18n=\"general.status\">Status</th>\n                        <th data-i18n=\"general.last_login\">Last login</th>\n                        <th data-i18n=\"general.email\">Email</th>\n                        <th data-i18n=\"storage.label\">Storage</th>\n                        <th data-i18n=\"general.role\">Role</th>\n                        <th data-i18n=\"title.two_factor_auth_short\">2FA</th>\n                        <th data-i18n=\"fs.quota_usage.disk\">Disk quota</th>\n                        <th data-i18n=\"fs.quota_usage.transfer\">Transfer quota</th>\n                        <th data-i18n=\"title.groups\">Groups</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/papaparse/papaparse.min.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/file-saver/FileSaver.min.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    function deleteAction(username) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.UserURL}}' + \"/\" + encodeURIComponent(username);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    function disableSeconFactorAction(username) {\n        ModalAlert.fire({\n            text: $.t('2fa.disable_confirm'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.disable_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.UserURL}}' + \"/\" + encodeURIComponent(username)+\"/2fa/disable\";\n\n                axios.put(path, null, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    ModalAlert.fire({\n                        text: $.t('2fa.save_err'),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    function quotaScanAction(username) {\n        clearLoading();\n        KTApp.showPageLoading();\n        let path = '{{.QuotaScanURL}}' + \"/\" + encodeURIComponent(username);\n        axios.post(path, null, {\n            timeout: 15000,\n            headers: {\n                'X-CSRF-TOKEN': '{{.CSRFToken}}'\n            },\n            validateStatus: function (status) {\n                return status == 202;\n            }\n        }).then(function (response) {\n            KTApp.hidePageLoading();\n            showToast(1, 'general.quota_scan_started');\n        }).catch(function (error) {\n            KTApp.hidePageLoading();\n            let errorMessage;\n            if (error && error.response) {\n                switch (error.response.status) {\n                    case 409:\n                        errorMessage = \"general.quota_scan_conflit\";\n                        break;\n                }\n            }\n            if (!errorMessage) {\n                errorMessage = \"general.quota_scan_error\";\n            }\n            ModalAlert.fire({\n                text: $.t(errorMessage),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n        });\n    }\n\n    var datatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            $('#errorMsg').addClass(\"d-none\");\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.UsersURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"username\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"status\",\n                        name: \"status\",\n                        render: function(data, type, row) {\n                            let result = data;\n                            if (row.expiration_date){\n                                if (row.expiration_date < Date.now()){\n                                    result = -1;\n                                }\n                            }\n                            if (type === 'display') {\n                                switch (result){\n                                    case 1:\n                                        return $.t('general.active');\n                                    case -1:\n                                        return $.t('general.expired');\n                                    default:\n                                        return $.t('general.inactive');\n                                }\n                            }\n                            return result;\n                        }\n                    },\n                    {\n                        data: \"last_login\",\n                        name: \"last_login\",\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data > 0){\n                                    return $.t('general.datetime', {\n                                        val: parseInt(data, 10),\n                                        formatParams: {\n                                            val: { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },\n                                        }\n                                    });\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"email\",\n                        name: \"email\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\";\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"filesystem.provider\",\n                        name: \"storage\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case 1:\n                                        return $.t('storage.s3');\n                                    case 2:\n                                        return $.t('storage.gcs');\n                                    case 3:\n                                        return $.t('storage.azblob');\n                                    case 4:\n                                        return $.t('storage.encrypted');\n                                    case 5:\n                                        return $.t('storage.sftp');\n                                    case 6:\n                                        return $.t('storage.http');\n                                    default:\n                                        return $.t('storage.local');\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"role\",\n                        name: \"role\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return escapeHTML(data);\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"filters.totp_config\",\n                        name: \"two_factor\",\n                        visible: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            let protocols = \"\";\n                            if (data && data.enabled){\n                                protocols = data.protocols.join(', ');\n                            }\n                            if (type === 'display') {\n                                if (protocols){\n                                    return escapeHTML(protocols);\n                                }\n                            }\n                            return protocols;\n                        }\n                    },\n                    {\n                        data: \"quota_files\",\n                        name: \"disk_quota\",\n                        visible: false,\n                        searchable: false,\n                        orderable: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                let val = \"\";\n                                if (row.quota_size > 0) {\n                                    let used = row.used_quota_size;\n                                    if (!used){\n                                        used = 0;\n                                    }\n                                    let usage = fileSizeIEC(used)+\"/\"+fileSizeIEC(row.quota_size);\n                                    val += $.t('fs.quota_usage.size', {val: usage})+\". \";\n                                } else if (row.used_quota_size && row.used_quota_size > 0) {\n                                    val += $.t('fs.quota_usage.size', {val: fileSizeIEC(row.used_quota_size)})+\". \";\n                                }\n                                if (row.quota_files > 0){\n                                    let used = row.used_quota_files;\n                                    if (!used){\n                                        used = 0;\n                                    }\n                                    let usage = used+\"/\"+row.quota_files;\n                                    val += $.t('fs.quota_usage.files', {val: usage});\n                                } else if (row.used_quota_files && row.used_quota_files > 0) {\n                                    val += $.t('fs.quota_usage.files', {val: row.used_quota_files});\n                                }\n                                return val\n                            }\n                            return \"\";\n                        }\n                    },\n                    {\n                        data: \"total_data_transfer\",\n                        name: \"transfer_quota\",\n                        visible: false,\n                        searchable: false,\n                        orderable: false,\n                        defaultContent: \"\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                let val = \"\";\n                                if (row.total_data_transfer > 0) {\n                                    let used;\n                                    if (row.used_download_data_transfer){\n                                        used+=row.used_download_data_transfer;\n                                    }\n                                    if (row.used_upload_data_transfer){\n                                        used+=row.used_upload_data_transfer;\n                                    }\n                                    let usage = fileSizeIEC(used)+\"/\"+fileSizeIEC(row.total_data_transfer*1048576);\n                                    val = $.t('fs.quota_usage.total', {val: usage});\n                                } else {\n                                    if (row.upload_data_transfer > 0) {\n                                        let used = row.used_upload_data_transfer;\n                                        if (!used){\n                                            used = 0;\n                                        }\n                                        let usage = fileSizeIEC(used)+\"/\"+fileSizeIEC(row.upload_data_transfer*1048576);\n                                        val = $.t('fs.quota_usage.uploads', {val: usage})+\". \";\n                                    }\n                                    if (row.download_data_transfer > 0) {\n                                        let used = row.used_download_data_transfer;\n                                        if (!used){\n                                            used = 0;\n                                        }\n                                        let usage = fileSizeIEC(used)+\"/\"+fileSizeIEC(row.download_data_transfer*1048576);\n                                        if (val) {\n                                            val += \". \";\n                                        }\n                                        val += $.t('fs.quota_usage.downloads', {val: usage});\n                                    }\n                                }\n                                return val;\n                            }\n                            return \"\";\n                        }\n                    },\n                    {\n                        data: \"groups\",\n                        name: \"groups\",\n                        visible: false,\n                        defaultContent: [],\n                        render: function(data, type, row) {\n                            if (!data){\n                                return \"\";\n                            }\n                            let groups = [];\n                            let result = \"\";\n                            for (i = 0; i < data.length; i++){\n                                groups.push(data[i].name);\n                            }\n                            result = groups.join(\", \");\n                            if (type === 'display') {\n                                return escapeHTML(result);\n                            }\n                            return result;\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let numActions = 0;\n                                let actions = `<button class=\"btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                                    <span data-i18n=\"general.actions\" class=\"fs-6\">Actions</span>\n\t\t\t\t\t\t\t\t\t\t            <i class=\"ki-duotone ki-down fs-5 ms-1 rotate-180\"></i>\n                                                </button>\n                                                <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">`;\n\n                                //{{- if .LoggedUser.HasPermission \"edit_users\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                //{{- if .LoggedUser.HasPermissions \"add_users\" \"edit_users\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.template\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"template_row\">Template</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                //{{- if .LoggedUser.HasPermission \"quota_scans\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t      <a data-i18n=\"general.quota_scan\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"quota_scan_row\">Quota scan</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                //{{- if .LoggedUser.HasPermission \"disable_mfa\"}}\n                                if (row.filters.totp_config && row.filters.totp_config.enabled){\n                                    numActions++;\n                                    actions+=`<div class=\"menu-item px-3\">\n                                                <a data-i18n=\"2fa.disable_msg\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"disable_2fa_row\">Disable 2FA</a>\n\t\t\t\t\t\t\t\t\t\t      </div>`;\n                                }\n                                //{{- end}}\n                                //{{- if .LoggedUser.HasPermission \"del_users\"}}\n                                numActions++;\n                                actions+=`<div class=\"menu-item px-3\">\n                                             <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t  </div>`;\n                                //{{- end}}\n                                if (numActions > 0){\n                                    actions+=`</div>`;\n                                    return actions;\n                                }\n                            }\n                            return \"\";\n                        }\n                    },\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                colReorder: {\n                    enable: true,\n                    fixedColumnsLeft: 1\n                },\n                stateLoadParams: function (settings, data) {\n                        if (data.search.search){\n                            const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                            filterSearch.value = data.search.search;\n                        }\n                    },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('datatable.no_records')\n                },\n                order: [[0, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n            dt.on('columns-reordered', function(e, settings, details){\n                drawAction();\n            });\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        function handleColVisibilityCheckbox(el, selector) {\n            let index = dt.column(selector).index();\n            el.off(\"change\");\n            el.prop('checked', dt.column(index).visible());\n            el.on(\"change\", function(e){\n                let index = dt.column(selector).index();\n                dt.column(index).visible($(this).is(':checked'));\n                dt.draw('page');\n            });\n        }\n\n        var handleDatatableActions = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n            handleColVisibilityCheckbox($('#checkColStatus'), \"status:name\");\n            handleColVisibilityCheckbox($('#checkColLastLogin'), \"last_login:name\");\n            handleColVisibilityCheckbox($('#checkColEmail'), \"email:name\");\n            handleColVisibilityCheckbox($('#checkColStorage'), \"storage:name\");\n            handleColVisibilityCheckbox($('#checkColRole'), \"role:name\");\n            handleColVisibilityCheckbox($('#checkCol2FA'), \"two_factor:name\");\n            handleColVisibilityCheckbox($('#checkColDiskQuota'), \"disk_quota:name\");\n            handleColVisibilityCheckbox($('#checkColTransferQuota'), \"transfer_quota:name\");\n            handleColVisibilityCheckbox($('#checkColGroups'), \"groups:name\");\n\n            const exportButton = $(document.querySelector('[data-table-filter=\"export\"]'));\n            exportButton.on('click', function(e){\n                e.preventDefault();\n                this.blur();\n                this.setAttribute('data-kt-indicator', 'on');\n\t\t\t    this.disabled = true;\n\n                let data = [];\n                dt.rows({ search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                    let line = {};\n                    let rowData = dt.row(rowIdx).data();\n                    let filters = rowData[\"filters\"];\n\n                    line[\"Username\"] = rowData[\"username\"];\n                    let status = \"Inactive\";\n                    if (rowData[\"status\"] == 1){\n                        status = \"Active\";\n                    } else if (rowData[\"status\"] == -1){\n                        status = \"Expired\";\n                    }\n                    line[\"Status\"] = status;\n                    if (rowData[\"has_password\"]){\n                        line[\"Password\"] = \"[redacted]\";\n                    } else {\n                        line[\"Password\"] = \"\";\n                    }\n                    if (rowData[\"created_at\"]) {\n                        line[\"Created At\"] = moment(rowData[\"created_at\"]).format(\"YYYY-MM-DD HH:mm\");\n                    } else {\n                        line[\"Created At\"] = \"\";\n                    }\n                    if (rowData[\"updated_at\"]) {\n                        line[\"Updated At\"] = moment(rowData[\"updated_at\"]).format(\"YYYY-MM-DD HH:mm\");\n                    } else {\n                        line[\"Updated At\"] = \"\";\n                    }\n                    if (rowData[\"last_login\"]) {\n                        line[\"Last Login\"] = moment(rowData[\"last_login\"]).format(\"YYYY-MM-DD HH:mm\");\n                    } else {\n                        line[\"Last Login\"] = \"\";\n                    }\n                    if (rowData[\"expiration_date\"]) {\n                        line[\"Expiration Date\"] = moment(rowData[\"expiration_date\"]).format(\"YYYY-MM-DD HH:mm\");\n                    } else {\n                        line[\"Expiration Date\"] = \"\";\n                    }\n                    if (rowData[\"last_password_change\"]){\n                        line[\"Last password change\"] = moment(rowData[\"last_password_change\"]).format(\"YYYY-MM-DD HH:mm\");\n                    } else {\n                        line[\"Last password change\"] = \"\";\n                    }\n                    if (rowData[\"email\"]){\n                        line[\"Email\"] = rowData[\"email\"];\n                    } else {\n                        line[\"Email\"] = \"\";\n                    }\n                    let fsProvider = \"Local storage: \"+rowData[\"home_dir\"];\n                    switch (rowData[\"filesystem\"][\"provider\"]){\n                        case 1:\n                            fsProvider = \"S3 Compatible\";\n                            break;\n                        case 2:\n                            fsProvider = \"Google Cloud Storage\";\n                            break;\n                        case 3:\n                            fsProvider = \"Azure Blob\";\n                            break;\n                        case 4:\n                            fsProvider = \"Local storage encrypted: \"+rowData[\"home_dir\"];\n                            break;\n                        case 5:\n                            fsProvider = \"SFTP\";\n                            break;\n                        case 6:\n                            fsProvider = \"HTTP\";\n                            break;\n                    }\n                    line[\"Filesystem\"] = fsProvider;\n                    let virtualFolders = [];\n                    let rowFolders = rowData[\"virtual_folders\"];\n                    if (rowFolders){\n                        for (i = 0; i < rowFolders.length; i++){\n                            virtualFolders.push(`${rowFolders[i].virtual_path} (${rowFolders[i].name})`);\n                        }\n                    }\n                    line[\"Virtual Folders\"] = virtualFolders.join(\", \");\n\n                    let permissions = \"\";\n                    for (let key in rowData[\"permissions\"]){\n                        let values = rowData[\"permissions\"][key];\n                        if (values){\n                            permissions+=`${key}: [${values.join(\", \")}]; `;\n                        } else {\n                            permissions+=`${key}: []; `;\n                        }\n                    }\n                    line[\"Permissions\"] = permissions;\n\n                    if (rowData[\"max_sessions\"] > 0){\n                        line[\"Max sessions\"] = rowData[\"max_sessions\"];\n                    } else {\n                        line[\"Max sessions\"] = \"\"\n                    }\n                    if (rowData[\"quota_size\"] > 0){\n                        let used = rowData[\"used_quota_size\"];\n                        if (!used){\n                            used = 0;\n                        }\n                        line[\"Quota size\"] = fileSizeIEC(used)+\"/\"+fileSizeIEC(rowData[\"quota_size\"]);\n                    } else if (rowData[\"used_quota_size\"] && rowData[\"used_quota_size\"] > 0){\n                        let used = rowData[\"used_quota_size\"];\n                        if (!used){\n                            used = 0;\n                        }\n                        line[\"Quota size\"] = fileSizeIEC(used);\n                    } else {\n                        line[\"Quota size\"] = \"\";\n                    }\n\n                    if (rowData[\"quota_files\"] > 0){\n                        let used = rowData[\"used_quota_files\"];\n                        if (!used){\n                            used = 0;\n                        }\n                        line[\"Quota files\"] = used+\"/\"+rowData[\"quota_files\"];\n                    } else if (rowData[\"used_quota_files\"] && rowData[\"used_quota_files\"] > 0){\n                        let used = rowData[\"used_quota_files\"];\n                        if (!used){\n                            used = 0;\n                        }\n                        line[\"Quota files\"] = used;\n                    } else {\n                        line[\"Quota files\"] = \"\";\n                    }\n\n                    if (rowData[\"total_data_transfer\"] > 0) {\n                        let used;\n                        if (rowData[\"used_download_data_transfer\"]){\n                            used+=rowData[\"used_download_data_transfer\"];\n                        }\n                        if (rowData[\"used_upload_data_transfer\"]){\n                            used+=rowData[\"used_upload_data_transfer\"];\n                        }\n                        line[\"Data transfer\"] = fileSizeIEC(used) + \"/\" + fileSizeIEC(rowData[\"total_data_transfer\"] * 1048576);\n                    } else {\n                        let val = \"\";\n                        if (rowData[\"upload_data_transfer\"] > 0) {\n                            let used = rowData[\"used_upload_data_transfer\"];\n                            if (!used){\n                                used = 0;\n                            }\n                            val = \"Upload: \"+fileSizeIEC(used)+\"/\"+fileSizeIEC(rowData[\"upload_data_transfer\"] * 1048576);\n                        }\n                        if (rowData[\"download_data_transfer\"] > 0) {\n                            let used = rowData[\"used_download_data_transfer\"];\n                            if (!used){\n                                used = 0;\n                            }\n                            if (val){\n                                val += \". \";\n                            }\n                            val += \"Download: \"+fileSizeIEC(used)+\"/\"+fileSizeIEC(rowData[\"download_data_transfer\"] * 1048576);\n                        }\n                        line[\"Data transfer\"] = val;\n                    }\n\n                    if (filters[\"allowed_ip\"]){\n                        line[\"Allowed IPs/Networks\"] = filters[\"allowed_ip\"].join(\", \");\n                    } else {\n                        line[\"Allowed IPs/Networks\"] = \"\";\n                    }\n                    if (filters[\"denied_ip\"]){\n                        line[\"Denied IPs/Networks\"] = filters[\"denied_ip\"].join(\", \");\n                    } else {\n                        line[\"Denied IPs/Networks\"] = \"\";\n                    }\n                    if (filters[\"denied_protocols\"]){\n                        line[\"Denied protocols\"] = filters[\"denied_protocols\"].join(\", \");\n                    } else {\n                        line[\"Denied protocols\"] = \"\";\n                    }\n                    if (filters[\"denied_login_methods\"]){\n                        line[\"Denied login methods\"] = filters[\"denied_login_methods\"].join(\", \");\n                    } else {\n                        line[\"Denied login methods\"] = \"\";\n                    }\n                    if (filters[\"max_upload_file_size\"]){\n                        line[\"Max upload size\"] = fileSizeIEC(filters[\"max_upload_file_size\"]);\n                    } else {\n                        line[\"Max upload size\"] = \"\";\n                    }\n\n                    let totpConfig = filters[\"totp_config\"];\n                    let totpProtocols = \"\";\n                    if (totpConfig && totpConfig[\"enabled\"]){\n                        totpProtocols = totpConfig[\"protocols\"].join(\", \");\n                        line[\"Two-factor auth\"] = totpProtocols;\n                    } else {\n                        line[\"Two-factor auth\"] = \"Disabled\";\n                    }\n\n                    let webClientPermissions = filters[\"web_client\"];\n                    if (webClientPermissions){\n                        line[\"WebClient permissions\"] = webClientPermissions.join(\", \");\n                    } else {\n                        line[\"WebClient permissions\"] = \"\";\n                    }\n\n                    let groups = [];\n                    let rowGroups = rowData[\"groups\"];\n                    if (rowGroups){\n                        for (i = 0; i < rowGroups.length; i++ ){\n                            groups.push(rowGroups[i].name);\n                        }\n                    }\n                    line[\"Groups\"] = groups.join(\", \");\n                    if (rowData[\"role\"]){\n                        line[\"Role\"] = rowData[\"role\"];\n                    } else {\n                        line[\"Role\"] = \"\";\n                    }\n                    if (rowData[\"public_keys\"]){\n                        line[\"Public keys\"] = rowData[\"public_keys\"].length;\n                    } else {\n                        line[\"Public keys\"] = \"0\";\n                    }\n                    if (filters[\"tls_certs\"]){\n                        line[\"TLS certificates\"] = filters[\"tls_certs\"].length;\n                    } else {\n                        line[\"TLS certificates\"] = \"0\";\n                    }\n                    if (rowData[\"description\"]){\n                        line[\"Description\"] = rowData[\"description\"];\n                    } else {\n                        line[\"Description\"] = \"\";\n                    }\n                    if (rowData[\"additional_info\"]){\n                        line[\"Additional info\"] = rowData[\"additional_info\"];\n                    } else {\n                        line[\"Additional info\"] = \"\";\n                    }\n\n                    data.push(line);\n                });\n\n                let csv = Papa.unparse(data, {\n                    quotes: false,\n\t                quoteChar: '\"',\n\t                escapeChar: '\"',\n\t                delimiter: \",\",\n\t                header: true,\n\t                newline: \"\\r\\n\",\n\t                skipEmptyLines: false,\n\t                columns: null,\n                    escapeFormulae: true\n                });\n                let blob = new Blob([csv], {type: \"text/csv\"});\n                let ts = moment().format(\"YYYY_MM_DD_HH_mm_ss\");\n                saveAs(blob, `users_${ts}.csv`);\n\n                this.removeAttribute('data-kt-indicator');\n                this.disabled = false;\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.UserURL}}' + \"/\" + encodeURIComponent(rowData['username']));\n                });\n            });\n\n            const templateButtons = document.querySelectorAll('[data-table-action=\"template_row\"]');\n            templateButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    window.location.replace('{{.UserTemplateURL}}' + \"?from=\" + encodeURIComponent(rowData['username']));\n                });\n            });\n\n            const quotaScanButtons = document.querySelectorAll('[data-table-action=\"quota_scan_row\"]');\n            quotaScanButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    quotaScanAction(rowData['username']);\n                });\n            });\n\n            const diable2FAButtons = document.querySelectorAll('[data-table-action=\"disable_2fa_row\"]');\n            diable2FAButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    disableSeconFactorAction(rowData['username']);\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    deleteAction(dt.row(parent).data()['username']);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleDatatableActions();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        datatable.init();\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webclient/base.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- define \"navitems\"}}\n{{- block \"additionalnavitems\" .}}{{- end}}\n{{- if ne .CurrentURL .EditURL }}\n{{- template \"theme-switcher\"}}\n{{- end}}\n<div class=\"d-flex align-items-center ms-2 ms-lg-3\">\n    <div class=\"btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px\" data-kt-menu-trigger=\"{default:'click', lg: 'hover'}\" data-kt-menu-attach=\"parent\" data-kt-menu-placement=\"bottom-end\">\n        <i class=\"ki-duotone ki-user fs-2\">\n            <i class=\"path1\"></i>\n            <i class=\"path2\"></i>\n        </i>\n    </div>\n    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-gray-500 menu-active-bg menu-state-color fw-semibold py-4 w-250px\" data-kt-menu=\"true\">\n        {{- if not .IsLoggedToShare }}\n        <div class=\"menu-item px-3 my-0\">\n            <div class=\"menu-content d-flex align-items-center px-3 py-2\">\n                <div class=\"me-5\">\n                    <i class=\"ki-duotone ki-user fs-2\">\n                        <i class=\"path1\"></i>\n                        <i class=\"path2\"></i>\n                    </i>\n                </div>\n                <div class=\"d-flex flex-column\">\n                    <div class=\"fw-semibold d-flex align-items-center fs-5\">\n                        <span class=\"w-175px wrap-word\">{{.LoggedUser.Username}}</span>\n                    </div>\n                </div>\n            </div>\n        </div>\n        <div class=\"separator my-2\"></div>\n        <div class=\"menu-item px-3 my-0\">\n            <a href=\"{{.ProfileURL}}\" class=\"menu-link px-3 py-2\">\n                <span data-i18n=\"title.profile\" class=\"menu-title\">Profile</span>\n            </a>\n        </div>\n        {{- if .LoggedUser.CanChangePassword}}\n        <div class=\"menu-item px-3 my-0\">\n            <a href=\"{{.ChangePwdURL}}\" class=\"menu-link px-3 py-2\">\n                <span data-i18n=\"title.change_password\" class=\"menu-title\">Change password</span>\n            </a>\n        </div>\n        {{- end}}\n        {{- end}}\n        <div class=\"menu-item px-3 my-0\">\n            <a id=\"id_logout_link\" href=\"#\" class=\"menu-link px-3 py-2\">\n                <span data-i18n=\"login.signout\" class=\"menu-title\">Sign out</span>\n            </a>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"sidebaritems\"}}\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .FilesURL}} active{{- end}}\" href=\"{{.FilesURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-folder fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.files\" class=\"menu-title\">Files</span>\n    </a>\n</div>\n{{- if .LoggedUser.CanManageShares}}\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .SharesURL}} active{{- end}}\" href=\"{{.SharesURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-share fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n                <span class=\"path3\"></span>\n                <span class=\"path4\"></span>\n                <span class=\"path5\"></span>\n                <span class=\"path6\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.shares\" class=\"menu-title\">Shares</span>\n    </a>\n</div>\n{{- end}}\n{{- if .LoggedUser.CanManageMFA}}\n<div class=\"menu-item\">\n    <a class=\"menu-link {{- if eq .CurrentURL .MFAURL}} active{{- end}}\" href=\"{{.MFAURL}}\">\n        <span class=\"menu-icon\">\n            <i class=\"ki-duotone ki-shield fs-1\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n        </span>\n        <span data-i18n=\"title.two_factor_auth_short\" class=\"menu-title\">2FA</span>\n    </a>\n</div>\n{{- end}}\n{{- end}}\n"
  },
  {
    "path": "templates/webclient/editfile.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<style {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    .cm-editor {\n        height: 100%;\n        width: 100%;\n    }\n\n</style>\n{{- end}}\n\n{{- define \"additionalnavitems\"}}\n<div class=\"d-flex align-items-center ms-2 ms-lg-3\" id=\"kt_header_user_menu_toggle\">\n    <div class=\"btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px\" data-bs-toggle=\"modal\" data-bs-target=\"#info_modal\">\n        <i class=\"ki-duotone ki-information-2 fs-2\">\n            <i class=\"path1\"></i>\n            <i class=\"path2\"></i>\n            <i class=\"path3\"></i>\n        </i>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header\">\n        <h6 id=\"card_title\" class=\"card-title section-title\"></h6>\n        <div class=\"card-toolbar\">\n            <a data-i18n=\"general.back\" class=\"btn btn-light-primary px-10 me-5\" href='{{.FilesURL}}?path={{.CurrentDir}}' role=\"button\">Back</a>\n            {{- if not .ReadOnly}}\n            <a id=\"save_button\" type=\"button\" class=\"btn btn-primary px-10\" href=\"#\" role=\"button\">\n                <span data-i18n=\"general.submit\" class=\"indicator-label\">Save</span>\n\t\t\t    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                    Please wait...\n\t\t\t\t\t<span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n\t\t\t\t</span>\n            </a>\n            {{- end}}\n        </div>\n    </div>\n    <div class=\"card-body\">\n        <div id=\"editor\" class=\"col-sm-12 border border-light-primary\"></div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"modals\"}}\n<div class=\"modal fade\" id=\"info_modal\" tabindex=\"-1\">\n    <div class=\"modal-dialog\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header\">\n                <h3 data-i18n=\"editor.keybinding\" class=\"modal-title\">Editor keybindings</h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body fs-5 fw-semibold\">\n                <p>\n                    <span class=\"shortcut\">Ctrl-F / Cmd-F</span> => <span data-i18n=\"editor.search\">Open search panel</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">Alt-G</span> => <span data-i18n=\"editor.goto\">Jump to line</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">Tab</span> => <span data-i18n=\"editor.indent_more\">Indent more</span>\n                </p>\n                <p>\n                    <span class=\"shortcut\">Shift-Tab</span> => <span data-i18n=\"editor.indent_less\">Indent less</span>\n                </p>\n            </div>\n            <div class=\"modal-footer\">\n                <button data-i18n=\"general.ok\" class=\"btn btn-primary\" type=\"button\" data-bs-dismiss=\"modal\">OK</button>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/codemirror/cm6.bundle.min.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    var cmView;\n\n    function keepAlive() {\n        axios.get('{{.PingURL}}',{\n            timeout: 15000,\n            responseType: 'text'\n       \t}).catch(function (error){});\n    }\n\n    //{{- if not .ReadOnly}}\n    function saveFile() {\n        $('#errorMsg').addClass(\"d-none\");\n        let saveButton = document.querySelector('#save_button');\n\t    saveButton.setAttribute('data-kt-indicator', 'on');\n\t\tsaveButton.disabled = true;\n\n        let uploadPath = '{{.FileURL}}?path='+encodeURIComponent('{{.CurrentDir}}/{{.Name}}');\n        let blob = new Blob([cmView.state.doc.toString()]);\n\n        axios.post(uploadPath, blob, {\n            headers: {\n                'X-CSRF-TOKEN': '{{.CSRFToken}}'\n            },\n            timeout: 600000,\n            validateStatus: function (status) {\n                    return status == 201;\n                }\n        }).then(function (response) {\n            window.location.replace('{{.FilesURL}}?path='+encodeURIComponent('{{.CurrentDir}}'));\n        }).catch(function (error) {\n            saveButton.removeAttribute('data-kt-indicator');\n\t\t    saveButton.disabled = false;\n\n            let errorMessage = \"\";\n            if (error && error.response) {\n                switch (error.response.status) {\n                    case 403:\n                        errorMessage = \"fs.save.err403\";\n                        break;\n                    case 429:\n                        errorMessage = \"fs.save.err429\";\n                        break;\n                }\n            }\n            if (!errorMessage){\n                errorMessage = \"fs.save.err_generic\";\n            }\n            setI18NData($('#errorTxt'), errorMessage);\n            $('#errorMsg').removeClass(\"d-none\");\n        });\n    }\n    //{{- end}}\n\n    $(document).on(\"i18nload\", function(){\n        let message = 'fs.edit_file';\n        //{{- if .ReadOnly}}\n        message = 'fs.view_file';\n        //{{- end}}\n        setI18NData($('#card_title'), message, { path: '{{.Path}}'});\n    });\n\n    $(document).on(\"i18nshow\", function(){\n        let filename = \"{{.Name}}\";\n        let extension = filename.slice((filename.lastIndexOf(\".\") - 1 >>> 0) + 2).toLowerCase();\n        let options = {\n            oneDark: KTThemeMode.getMode() == \"dark\",\n            fileExt: extension\n        };\n        //{{- if .ReadOnly}}\n        options.readOnly = true;\n        //{{- end}}\n        //{{- if .CSPNonce}}\n        options.cspNonce = \"{{.CSPNonce}}\";\n        //{{- end}}\n        let editorState = cm6.createEditorState(\"{{.Data}}\", options);\n        cmView = cm6.createEditorView(editorState, document.getElementById(\"editor\"));\n\n        var saveBtn = $('#save_button');\n        if (saveBtn){\n            saveBtn.on(\"click\", function(){\n                saveFile();\n            });\n        }\n\n        setInterval(keepAlive, 300000);\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webclient/files.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" .Error}}\n\n{{- $move_copy_msg := \"\"}}\n{{- if and .CanRename .CanCopy}}\n{{- $move_copy_msg = \"fs.move_copy\"}}\n{{- else}}\n{{- if .CanRename}}\n{{- $move_copy_msg = \"fs.move.msg\"}}\n{{- else}}\n{{- if .CanCopy}}\n{{- $move_copy_msg = \"fs.copy.msg\"}}\n{{- end}}\n{{- end}}\n{{- end}}\n\n<div class=\"card card-flush shadow-sm\">\n    <div class=\"card-header pt-8\">\n        <div class=\"card-title\">\n            <div class=\"d-flex align-items-center position-relative my-1\">\n                <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-kt-filemanager-table-filter=\"search\" class=\"form-control rounded-1 w-250px ps-15\" placeholder=\"Search Files & Folders\" />\n            </div>\n        </div>\n        <div class=\"card-toolbar\">\n            <div class=\"d-flex justify-content-end\" data-kt-filemanager-table-toolbar=\"base\">\n                {{- if .CanCreateDirs}}\n                <button id=\"id_create_dir_button\" type=\"button\" class=\"btn btn-flex btn-light-primary me-3\">\n                    <i class=\"ki-duotone ki-add-folder fs-2\">\n                        <span class=\"path1\"></span>\n                        <span class=\"path2\"></span>\n                    </i>\n                    <span data-i18n=\"fs.new_folder\">New Folder</span>\n                </button>\n                {{- end}}\n                {{- if .CanAddFiles}}\n                <button type=\"button\" class=\"btn btn-flex btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#modal_upload\">\n                    <i class=\"ki-duotone ki-folder-up fs-2\">\n                        <span class=\"path1\"></span>\n                        <span class=\"path2\"></span>\n                    </i>\n                    <span data-i18n=\"fs.upload.text\">Upload Files</span>\n                </button>\n                {{- end}}\n            </div>\n        </div>\n    </div>\n    <div class=\"card-body\">\n        <div class=\"d-flex flex-stack\">\n            <div class=\"badge badge-lg badge-light-primary\" data-kt-filemanager-table-nav-bar=\"base\">\n                <div class=\"d-flex align-items-center flex-wrap\">\n                    <i class=\"ki-duotone ki-home fs-1 text-primary me-3\"></i>\n                    <a data-i18n=\"fs.home\" href=\"{{.FilesURL}}?path=%2F\">Home</a>\n                    {{- range .Paths}}\n                    <i class=\"ki-duotone ki-right fs-2x text-primary mx-1\"></i>\n                    {{- if eq .Href \"\"}}\n                    <span>{{.DirName}}</span>\n                    {{- else}}\n                    <a href=\"{{.Href}}\">{{.DirName}}</a>\n                    {{- end}}\n                    {{- end}}\n                </div>\n            </div>\n            <div class=\"d-flex align-items-center d-none\" data-kt-filemanager-table-toolbar=\"selected\">\n                <div class=\"fw-bold me-5\">\n                    <span class=\"me-2\" data-kt-filemanager-table-select=\"selected_count\"></span>\n                </div>\n                <div class=\"form-check form-switch form-check-custom form-check-solid me-5\" data-kt-filemanager-table-select=\"select_all_pages_container\">\n                    <input class=\"form-check-input\" type=\"checkbox\" id=\"id_select_all_pages\" data-kt-filemanager-table-select=\"select_all_pages\" />\n                    <label data-i18n=\"fs.select_across_pages\" class=\"form-check-label fw-semibold text-gray-900\" for=\"id_select_all_pages\">\n                        Select across pages\n                    </label>\n                </div>\n                {{- if or .CanDownload .CanDelete}}\n                <div>\n                    <button type=\"button\" class=\"btn btn-light-primary rotate\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom\">\n                        <span data-i18n=\"general.actions\">Actions</span>\n                        <i class=\"ki-duotone ki-down fs-3 rotate-180 ms-3 me-0\"></i>\n                    </button>\n                    <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4\" data-kt-menu=\"true\">\n                        {{- if .CanDownload}}\n                        <div class=\"menu-item px-3\">\n                            <a data-i18n=\"fs.download\" href=\"#\" class=\"menu-link px-3 fs-6\" data-kt-filemanager-table-select=\"download_selected\">\n                                Download\n                            </a>\n                        </div>\n                        {{- end}}\n                        {{- if not .ShareUploadBaseURL}}\n                        {{- if or .CanRename .CanAddFiles}}\n\t\t\t\t\t\t<div class=\"menu-item px-3\">\n                            <a data-i18n=\"{{$move_copy_msg}}\" href=\"#\" class=\"menu-link px-3 fs-6\" data-kt-filemanager-table-select=\"move_or_copy_selected\">\n                                Move or copy\n                            </a>\n                        </div>\n                        {{- end}}\n                        {{- if .CanShare}}\n                        <div class=\"menu-item px-3\">\n                            <a data-i18n=\"fs.share\" href=\"#\" class=\"menu-link px-3 fs-6\" data-kt-filemanager-table-select=\"share_selected\">\n                                Share\n                            </a>\n                        </div>\n                        {{- end}}\n                        {{- end}}\n                        {{- if .CanDelete}}\n                        <div class=\"menu-item px-3\">\n                            <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link px-3 text-danger fs-6\" data-kt-filemanager-table-select=\"delete_selected\">\n                                Delete\n                            </a>\n                        </div>\n                        {{- end}}\n                    </div>\n                </div>\n                {{- end}}\n            </div>\n        </div>\n        <div class=\"new_folder_divider py-2 d-none\">\n            <hr>\n        </div>\n        <div id=\"file_manager_new_folder\" class=\"d-flex align-items-center d-none\">\n            <span>\n                <i class=\"ki-duotone ki-folder fs-2x text-primary me-4\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                </i>\n            </span>\n            <input data-i18n=\"[placeholder]fs.create_folder_msg\" id=\"file_manager_new_folder_input\" type=\"text\" name=\"new_folder_name\" placeholder=\"Enter the new folder name\" class=\"form-control mw-250px me-3\" />\n            <button class=\"btn btn-icon btn-light-primary me-3\" id=\"file_manager_add_folder\">\n                <span class=\"indicator-label\">\n                    <i class=\"ki-duotone ki-check fs-1\"></i>\n                </span>\n                <span class=\"indicator-progress\">\n                    <span class=\"spinner-border spinner-border-sm align-middle\"></span>\n                </span>\n            </button>\n            <button class=\"btn btn-icon btn-light-danger\" id=\"file_manager_cancel_folder\">\n                <i class=\"ki-solid ki-cross fs-1\"></i>\n            </button>\n        </div>\n        <div class=\"new_folder_divider py-2 d-none\">\n            <hr>\n        </div>\n        <div id=\"upload_files_empty_container\" class=\"d-none mt-10\">\n            <form id=\"upload_files_empty_form\" action=\"{{.FilesURL}}?path={{.CurrentDir}}\" method=\"POST\" enctype=\"multipart/form-data\">\n                <div class=\"fv-row\">\n                    <div class=\"dropzone mh-350px overflow-auto visibility-auto\" id=\"upload_files_empty\">\n                        <div class=\"dz-message needsclick align-items-center\">\n                            <i class=\"ki-duotone ki-file-up fs-3x text-primary\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                            </i>\n                            <div class=\"ms-4\">\n                                <h3 data-i18n=\"fs.upload.message_empty\" class=\"fs-5 fw-bold text-gray-900 mb-1\"></h3>\n                                <!-- <span class=\"fs-7 fw-semibold text-gray-500\">Upload up to 30 files</span> -->\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </form>\n            <div class=\"d-flex justify-content-end mt-5\">\n                <button data-i18n=\"general.submit\" type=\"button\" id=\"upload_files_empty_button\" class=\"btn btn-primary\">Submit</button>\n            </div>\n        </div>\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"file_manager_list_container\" class=\"d-none\">\n            <table id=\"file_manager_list\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0 text-gray-500\">\n                        <th class=\"w-10px pe-2\">\n                            <div class=\"form-check form-check-sm form-check-custom form-check-solid me-3\">\n                                <input id=\"select_checkbox\" class=\"form-check-input\" type=\"checkbox\" />\n                            </div>\n                        </th>\n                        <th></th>\n                        <th data-i18n=\"general.name\" class=\"min-w-250px\">Name</th>\n                        <th data-i18n=\"general.size\" class=\"min-w-10px\">Size</th>\n                        <th data-i18n=\"general.last_modified\" class=\"min-w-125px\">Last Modified</th>\n                        <th class=\"w-125px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"file_manager_list_body\" class=\"text-gray-700 fw-semibold\">\n                </tbody>\n            </table>\n        </div>\n    </div>\n</div>\n\n{{- end}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n<link href=\"{{.StaticURL}}/vendor/glightbox/glightbox.min.css\" rel=\"stylesheet\" type=\"text/css\"/>\n<style {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    .gslide-description-bg {\n\t\tbackground: var(--bs-app-bg-color) !important;\n        opacity: 0.9;\n\t}\n</style>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/glightbox/glightbox.min.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/pdfobject/pdfobject.min.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    //{{- $move_copy_msg := \"\"}}\n    //{{- if and .CanRename .CanCopy}}\n    //{{- $move_copy_msg = \"fs.move_copy\"}}\n    //{{- else}}\n    //{{- if .CanRename}}\n    //{{- $move_copy_msg = \"fs.move.msg\"}}\n    //{{- else}}\n    //{{- if .CanCopy}}\n    //{{- $move_copy_msg = \"fs.copy.msg\"}}\n    //{{- end}}\n    //{{- end}}\n    //{{- end}}\n\n    const dzPreviewTemplate = `<div class=\"d-flex align-items-center mb-2\">\n                                 <span class=\"bullet bullet-dot bg-primary me-2\"></span>\n                                 <div class=\"text-break text-wrap text-left\"><span class=\"fs-5 fw-semibold\" data-dz-name></span>&nbsp;(<span class=\"fs-5 fw-semibold\" data-custom-size></span>)</div>\n                               </div>\n                               <div class=\"dz-error-message d-none\" data-dz-errormessage></div>\n                               <div class=\"dz-progress d-none\"><span class=\"dz-upload\" data-dz-uploadprogress></span></div>\n                              `;\n\n    //{{- if not .ShareUploadBaseURL}}\n    const supportedEditExtensions = [\"csv\", \"bat\", \"dyalog\", \"apl\", \"asc\", \"sig\", \"asn\", \"asn1\", \"b\", \"bf\",\n            \"c\", \"h\", \"ino\", \"cpp\", \"c++\", \"cc\", \"cxx\", \"hpp\", \"h++\", \"hh\", \"hxx\", \"cob\", \"cpy\", \"cbl\", \"cs\", \"clj\",\n            \"cljc\", \"cljx\", \"cljs\", \"gss\", \"cmake\", \"cmake.in\", \"coffee\", \"cl\", \"lisp\", \"el\", \"cyp\", \"cypher\",\n            \"pyx\", \"pxd\", \"pxi\", \"cr\", \"css\", \"cql\", \"d\", \"dart\", \"diff\", \"patch\", \"dtd\", \"dylan\", \"dyl\", \"intr\",\n            \"ecl\", \"edn\", \"e\", \"elm\", \"ejs\", \"erb\", \"erl\", \"sql\", \"factor\", \"forth\", \"fth\", \"4th\", \"f\", \"for\", \"f77\",\n            \"f90\", \"f95\", \"fsharp\", \"s\", \"go\", \"groovy\", \"gradle\", \"haml\", \"hs\", \"lhs\", \"hx\", \"hxml\", \"aspx\",\n            \"html\", \"htm\", \"handlebars\", \"hbs\", \"pro\", \"jade\", \"pug\", \"java\", \"jsp\", \"js\", \"json\", \"map\", \"jsonld\",\n            \"jsx\", \"j2\", \"jinja\", \"jinja2\", \"jl\", \"kt\", \"less\", \"ls\", \"lua\", \"markdown\", \"md\", \"mkd\", \"m\", \"nb\", \"wl\",\n            \"wls\", \"mo\", \"mps\", \"mbox\", \"nsh\", \"nsi\", \"nt\", \"nq\", \"m\", \"mm\", \"ml\", \"mli\", \"mll\", \"mly\", \"oz\", \"p\",\n            \"pas\", \"pl\", \"pm\", \"php\", \"php3\", \"php4\", \"php5\", \"php7\", \"phtml\", \"pig\", \"txt\", \"text\", \"conf\", \"def\",\n            \"list\", \"log\", \"pls\", \"ps1\", \"psd1\", \"psm1\", \"properties\", \"ini\", \"in\", \"proto\", \"BUILD\", \"bzl\", \"py\",\n            \"pyw\", \"pp\", \"q\", \"r\", \"rst\", \"spec\", \"rb\", \"rs\", \"sas\", \"sass\", \"scala\", \"scm\", \"ss\", \"scss\", \"sh\",\n            \"ksh\", \"bash\", \"siv\", \"sieve\", \"slim\", \"st\", \"tpl\", \"sml\", \"sig\", \"fun\", \"smackspec\", \"soy\", \"rq\", \"sparql\",\n            \"nut\", \"styl\", \"swift\", \"text\", \"ltx\", \"tex\", \"v\", \"sv\", \"svh\", \"tcl\", \"textile\", \"toml\", \"1\", \"2\", \"3\", \"4\",\n            \"5\", \"6\", \"7\", \"8\", \"9\", \"ttcn\", \"ttcn3\", \"ttcnpp\", \"cfg\", \"ttl\", \"ts\", \"tsx\", \"webidl\", \"vb\", \"vbs\",\n            \"vtl\", \"vhd\", \"vhdl\", \"vue\", \"xml\", \"xsl\", \"xsd\", \"svg\", \"xy\", \"xquery\", \"ys\", \"yaml\", \"yml\", \"z80\",\n            \"mscgen\", \"mscin\", \"msc\", \"xu\", \"msgenny\", \"wat\", \"wast\", \"env\"];\n    const supportedEditFilenames = [\"readme\", \"dockerfile\", \"pkgbuild\"];\n    //{{- end}}\n    const keepAliveInterval = '{{.KeepAliveInterval}}';\n    function keepAlive() {\n        //{{- if not .ShareUploadBaseURL}}\n        axios.get('{{.PingURL}}',{\n            timeout: 15000,\n            responseType: 'text'\n       \t}).catch(function (error){});\n        //{{- end}}\n    }\n\n    var taskStatusWaiter = function () {\n        var promiseResolve;\n\n        function getTaskStatus(taskID, numErrors) {\n            axios.get('{{.TasksURL}}'+\"/\"+encodeURIComponent(taskID),{\n                timeout: 15000,\n                headers: {\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n            }).then(function(response){\n                let status = response.data.status;\n                if (status == 0){\n                    setTimeout(function() {\n                        getTaskStatus(taskID, numErrors);\n                    }, 2500);\n                } else {\n                    promiseResolve({\n                        status: status\n                    });\n                }\n            }).catch(function(error){\n                numErrors++;\n                if (numErrors >= 3){\n                    promiseResolve({\n                        status: 0\n                    });\n                    return;\n                }\n                if (error && error.response && error.response.status == 404){\n                    promiseResolve({\n                        status: 404\n                    });\n                    return;\n                }\n                setTimeout(function() {\n                    getTaskStatus(taskID, numErrors);\n                }, 2500);\n            });\n        }\n\n        return {\n            wait: function(params){\n                setTimeout(function() {\n                    getTaskStatus(params.taskID, 0);\n                }, params.delay);\n\n                return new Promise(function(resolve, reject) {\n                    promiseResolve = resolve;\n                });\n            }\n        }\n    }();\n\n    //{{- if not .ShareUploadBaseURL}}\n    var KTDatatablesFoldersExplorer = function () {\n        var dt;\n        var curDir;\n\n        var initDatatable = function (dirsURL) {\n            $('#errorModalMsg').addClass(\"d-none\");\n            dt = $(\"#dirsbrowser_list\").DataTable({\n                ajax: {\n                    url: dirsURL,\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message) {\n                                    txt = json.message;\n                                }\n                            } else {\n                                txt = \"general.expired_session\";\n                            }\n                        }\n                        if (!txt){\n                            txt = \"fs.dir_list.err_generic\";\n                        }\n                        setI18NData($('#errorModalTxt'), txt);\n                        $('#errorModalMsg').removeClass(\"d-none\");\n                    }\n                },\n                deferRender: true,\n                processing: true,\n                stateSave: false,\n                columns: [\n                    {\n                        data: \"meta\"\n                    },\n                    {\n                        data: \"name\",\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                data = escapeHTML(data);\n                                return `<div class=\"d-flex align-items-center\">\n                                                <i class=\"ki-duotone ki-folder fs-1 text-primary me-4\">\n                                                    <i class=\"path1\"></i>\n                                                    <i class=\"path2\"></i>\n                                                </i>\n                                                <a href=\"#\" class=\"text-gray-700 text-hover-primary\" data-dirbrowser-table-row=\"open\">${data}</a>\n                                            </div>`\n                            }\n                            return data;\n                        }\n                    }\n                ],\n                lengthChange: true,\n                columnDefs: [\n                    {\n                        targets: 0,\n                        visible: false,\n                        searchable: false\n                    },\n                    {\n                        targets: 1,\n                        visible: true,\n                        searchable: true\n                    }\n                ],\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('fs.no_more_subfolders')\n                },\n                order: [1, 'asc']\n            });\n\n            dt.on('draw', function () {\n                let dirBrowserNav = document.getElementById(\"dirs_browser_nav\");\n                clearChilds(dirBrowserNav);\n                let mainNavIcon = document.createElement(\"i\");\n                mainNavIcon.classList.add(\"ki-duotone\", \"ki-home\", \"fs-1\", \"text-primary\", \"me-3\");\n                let mainNavLink = document.createElement(\"a\");\n                mainNavLink.textContent = $.t('fs.home');\n                mainNavLink.href = \"#\";\n                mainNavLink.addEventListener(\"click\", function(e){\n                    e.preventDefault();\n                    onDirBrowserClick(\"%2F\");\n                });\n                dirBrowserNav.appendChild(mainNavIcon);\n                dirBrowserNav.appendChild(mainNavLink);\n\n                let p = \"/\";\n                if (curDir && curDir != \"%2F\") {\n                    p = decodeURIComponent(curDir.replace(/\\+/g, '%20'));\n                    const dirPaths = p.split(\"/\");\n                    let fullPath = \"%2F\";\n                    for (idx in dirPaths) {\n                        let dir = dirPaths[idx];\n                        if (dir) {\n                            if (fullPath.endsWith(\"%2F\")){\n                                fullPath += encodeURIComponent(dir);\n                            } else {\n                                fullPath += encodeURIComponent(\"/\" + dir);\n                            }\n                            dir = escapeHTML(dir);\n                            let navIcon = document.createElement(\"i\");\n                            navIcon.classList.add(\"ki-duotone\", \"ki-right\", \"fs-2x\", \"text-primary\", \"mx-1\");\n                            let navLink = document.createElement(\"a\");\n                            navLink.textContent = dir;\n                            navLink.href = \"#\";\n                            let pathCopy = fullPath\n                            navLink.addEventListener(\"click\", function(e){\n                                e.preventDefault();\n                                onDirBrowserClick(pathCopy);\n                            });\n                            dirBrowserNav.appendChild(navIcon);\n                            dirBrowserNav.appendChild(navLink);\n                        }\n                    }\n                }\n\n                $('#move_copy_folder').val(p);\n                handleRowActions();\n            });\n        }\n\n        var handleCreateNewFolder = function() {\n            $('#dirsbrowser_add_folder').click(function(){\n                let errDivEl = $('#errorModalMsg');\n                let errTxtEl = $('#errorModalTxt');\n                let dirName = $(\"#dirsbrowser_new_folder_input\").val();\n                let submitButton  = document.querySelector('#dirsbrowser_add_folder');\n                let cancelButton = document.querySelector('#dirsbrowser_cancel_folder');\n                errDivEl.addClass(\"d-none\");\n                if (!dirName){\n                    setI18NData(errTxtEl, \"fs.folder_name_required\");\n                    errDivEl.removeClass(\"d-none\");\n                    return;\n                }\n                if (dirName.includes(\"/\")){\n                    setI18NData(errTxtEl, \"fs.invalid_name\");\n                    errDivEl.removeClass(\"d-none\");\n                    return;\n                }\n                let path = '{{.DirsURL}}?path='+ curDir + encodeURIComponent(\"/\"+dirName);\n                submitButton.setAttribute('data-kt-indicator', 'on');\n\t\t        submitButton.disabled = true;\n                cancelButton.disabled = true;\n\n                axios.post(path, null, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 201;\n                    }\n                }).then(function (response) {\n                    dt.ajax.reload();\n                    errDivEl.addClass(\"d-none\");\n                    submitButton.removeAttribute('data-kt-indicator');\n\t\t            submitButton.disabled = false;\n                    cancelButton.disabled = false;\n                    $('.dir_browser_folder_divider').addClass(\"d-none\");\n                    $('#dirsbrowser_new_folder').addClass(\"d-none\");\n                }).catch(function (error) {\n                    let errorMessage = \"\";\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 200:\n                                errorMessage = \"general.expired_session\";\n                                break;\n                            case 403:\n                                errorMessage = \"fs.create_dir.err_403\";\n                                break;\n                            case 429:\n                                errorMessage = \"fs.create_dir.err_429\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"fs.create_dir.err_generic\";\n                    }\n                    setI18NData(errTxtEl, errorMessage);\n                    errDivEl.removeClass(\"d-none\");\n                    submitButton.removeAttribute('data-kt-indicator');\n\t\t            submitButton.disabled = false;\n                    cancelButton.disabled = false;\n                });\n            });\n        }\n\n        function handleRowActions(){\n            const openLinks = document.querySelectorAll('[data-dirbrowser-table-row=\"open\"]');\n\n            openLinks.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    onDirBrowserClick(dt.row(parent).data()['dir_path']);\n                });\n            });\n        }\n\n        return {\n            init: function (url, dirPath) {\n                curDir = dirPath;\n                if (dt) {\n                    dt.ajax.url(url).load();\n                    return;\n                }\n                initDatatable(url);\n                handleCreateNewFolder();\n            }\n        }\n    }();\n    //{{- end}}\n</script>\n\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    var KTDatatablesServerSide = function () {\n        // Shared variables\n        var dt;\n        var lightbox;\n\n\n        // Private functions\n        var initDatatable = function () {\n            dt = $(\"#file_manager_list\").DataTable({\n                ajax: {\n                    url: \"{{.DirsURL}}?path={{.CurrentDir}}\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"fs.dir_list.err_generic\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                deferRender: true,\n                processing: true,\n                lengthMenu: [ 10, 25, 50, 100, 250, 500 ],\n                select: {\n                    style: 'multi',\n                    selector: 'td:first-child input[type=\"checkbox\"]',\n                    className: 'row-selected'\n                },\n                stateSave: true,\n                stateDuration: 0,\n                stateSaveParams: function (settings, data) {\n                    data.sftpgo_dir = '{{.CurrentDir}}';\n                },\n                stateLoadParams: function (settings, data) {\n                    if (!data.sftpgo_dir || data.sftpgo_dir != '{{.CurrentDir}}'){\n                        data.start = 0;\n                        data.search.search = \"\";\n                    }\n                    if (data.search.search){\n                        const filterSearch = document.querySelector('[data-kt-filemanager-table-filter=\"search\"]');\n                        filterSearch.value = data.search.search;\n                    }\n                },\n                columns: [\n                    {\n                        data: \"meta\",\n                        render: function (data, type) {\n                            if (type == 'display'){\n                                return `\n                                    <div class=\"form-check form-check-sm form-check-custom form-check-solid\">\n                                        <input class=\"form-check-input\" type=\"checkbox\" />\n                                    </div>`;\n                            }\n                            return data;\n                        }\n                    },\n                    { data: \"type\" },\n                    {\n                        data: \"name\",\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                data = escapeHTML(data);\n                                let icon_name = \"ki-file\";\n\n                                if (row[\"type\"] == \"1\") {\n                                    icon_name = \"ki-folder\";\n                                } else if (row[\"size\"] === \"\") {\n                                    icon_name = \"ki-file-right\"\n                                }\n\n                                return `<div class=\"d-flex align-items-center\">\n                                            <i class=\"ki-duotone ${icon_name} fs-2x text-primary me-4\">\n                                                <i class=\"path1\"></i>\n                                                <i class=\"path2\"></i>\n                                            </i>\n                                            <a href=\"${row['url']}\" class=\"text-gray-800 text-hover-primary\">${data}</a>\n                                        </div>`\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"size\",\n                        render: function (data, type) {\n                            if (type === 'display') {\n                                if (data || data === 0){\n                                    return fileSizeIEC(data);\n                                }\n                                return \"\";\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"last_modified\",\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                if (data){\n                                    return $.t('general.datetime', {\n                                        val: new Date(data),\n                                        formatParams: {\n                                            val: { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },\n                                        }\n                                    });\n                                }\n                                return \"\"\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"edit_url\",\n                        defaultContent: '',\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let previewDiv = \"\";\n                                if (row[\"type\"] == \"2\") {\n                                    let filename = escapeHTML(row[\"name\"]);\n                                    let extension = filename.slice((filename.lastIndexOf(\".\") - 1 >>> 0) + 2).toLowerCase();\n                                    switch (extension) {\n                                        case \"jpeg\":\n                                        case \"jpg\":\n                                        case \"png\":\n                                        case \"gif\":\n                                        case \"webp\":\n                                        case \"bmp\":\n                                        case \"svg\":\n                                            let desc = escapeHTML(filename);\n                                            let descHtml = `<span class=\"fs-5 fw-bold\">${desc}</span>`;\n                                            let descId = `desc-${row.id}`;\n                                            previewDiv = `<div class=\"ms-2\" data-kt-filemanger-table=\"view_item\">\n                                                    <a href=\"${row['url']}\" data-gallery=\"gallery\" data-description=\".${descId}\" class=\"btn btn-sm btn-icon btn-light btn-active-light-primary glightbox\">\n                                                        <i class=\"ki-duotone ki-eye fs-6 m-0\">\n                                                            <span class=\"path1\"></span>\n                                                            <span class=\"path2\"></span>\n                                                            <span class=\"path3\"></span>\n                                                        </i>\n                                                    </a>\n                                                    <div class=\"glightbox-desc ${descId} d-none\">${descHtml}</div>\n                                                </div>`;\n                                            break;\n                                        case \"mp4\":\n                                        case \"mov\":\n                                        case \"webm\":\n                                        case \"ogv\":\n                                        case \"ogg\":\n                                        case \"mp3\":\n                                        case \"wav\":\n                                            previewDiv = `<div class=\"ms-2\" data-kt-filemanger-table=\"view_item\">\n\t\t\t\t\t\t\t\t\t\t\t\t    <a href=\"#\" class=\"btn btn-sm btn-icon btn-light btn-active-light-primary\" data-kt-filemanager-table-action=\"view_media\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t    <i class=\"ki-duotone ki-eye fs-6 m-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t    <span class=\"path1\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t    <span class=\"path2\"></span>\n                                                            <span class=\"path3\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t    </i>\n\t\t\t\t\t\t\t\t\t\t\t\t    </a>\n\t\t\t\t\t\t\t\t\t\t\t    </div>`;\n                                            break;\n                                        case \"pdf\":\n                                            if (PDFObject.supportsPDFs){\n                                                let view_url = row['url'];\n                                                view_url = view_url.replace('{{.FilesURL}}','{{.ViewPDFURL}}');\n                                                previewDiv = `<div class=\"ms-2\" data-kt-filemanger-table=\"view_item\">\n\t\t\t\t\t\t\t\t\t\t\t\t    <a href=\"${view_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn btn-sm btn-icon btn-light btn-active-light-primary\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t    <i class=\"ki-duotone ki-eye fs-6 m-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t    <span class=\"path1\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t    <span class=\"path2\"></span>\n                                                            <span class=\"path3\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t    </i>\n\t\t\t\t\t\t\t\t\t\t\t\t    </a>\n\t\t\t\t\t\t\t\t\t\t\t    </div>`;\n                                            }\n                                            break;\n                                        default:\n                                            //{{- if not .ShareUploadBaseURL}}\n                                            if (data && (supportedEditExtensions.includes(extension) || supportedEditFilenames.includes(filename.toLowerCase()))){\n                                                previewDiv = `<div class=\"ms-2\" data-kt-filemanger-table=\"view_item\">\n\t\t\t\t\t\t\t\t\t\t\t\t    <a href=\"${data}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn btn-sm btn-icon btn-light btn-active-light-primary\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t    <i class=\"ki-duotone ki-eye fs-6 m-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t    <span class=\"path1\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t    <span class=\"path2\"></span>\n                                                            <span class=\"path3\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t    </i>\n\t\t\t\t\t\t\t\t\t\t\t\t    </a>\n\t\t\t\t\t\t\t\t\t\t\t    </div>`;\n                                            }\n                                            //{{- end}}\n                                    }\n                                }\n                                let more = `{{- if not .ShareUploadBaseURL}}\n                                            {{- if or .CanRename .CanAddFiles .CanShare .CanDelete }}\n                                            <div class=\"ms-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-sm btn-icon btn-light btn-active-light-primary\" data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<i class=\"ki-duotone ki-dots-square fs-5 m-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"path1\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"path2\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"path3\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"path4\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</i>\n\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4\" data-kt-menu=\"true\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{{- if .CanRename}}\n                                                    <div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a data-i18n=\"general.rename\" href=\"#\" class=\"menu-link px-3\" data-kt-filemanager-table-action=\"rename\">Rename</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n                                                    {{- end}}\n                                                    {{- if or .CanRename .CanCopy}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a data-i18n=\"{{$move_copy_msg}}\" href=\"#\" class=\"menu-link px-3\" data-kt-filemanager-table-action=\"move_or_copy\">Move or copy</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n                                                    {{- end}}\n                                                    {{- if .CanShare}}\n                                                    <div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a data-i18n=\"fs.share\" href=\"#\" class=\"menu-link px-3\" data-kt-filemanager-table-action=\"share\">Share</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n                                                    {{- end}}\n                                                    {{- if .CanDelete}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"menu-item px-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-kt-filemanager-table-action=\"delete\">Delete</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n                                                    {{- end}}\n\t\t\t\t\t\t\t\t\t\t\t    </div>\n\t\t\t\t\t\t\t\t\t\t    </div>\n                                            {{- end}}\n                                            {{- end}}`;\n                                return `<div class=\"d-flex justify-content-end\">\n\t\t\t\t\t\t\t\t\t\t\t${previewDiv}\n\t\t\t\t\t\t\t\t\t\t\t${more}\n                                        </div>\n                                `;\n                            }\n                            return \"\";\n                        }\n                    }\n                ],\n                lengthChange: true,\n                columnDefs: [\n                    {\n                        targets: 0,\n                        orderable: false,\n                        searchable: false,\n                    },\n                    {\n                        targets: 1,\n                        visible: false,\n                        searchable: false\n                    },\n                    {\n                        targets: 2,\n                        visible: true,\n                        searchable: true\n                    },\n                    {\n                        targets: [3,4],\n                        searchable: false\n                    },\n                    {\n                        targets: 5,\n                        orderable: false,\n                        searchable: false\n                    }\n                ],\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('fs.no_files_folders'),\n                    select: {\n                        rows: {\n                            _: $.t('general.selected_with_placeholder'),\n                            0: '',\n                            1: $.t('general.selected_items', { count: 1})\n                        }\n                    }\n                },\n                orderFixed: [1, 'asc'],\n                order: [2, 'asc'],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#file_manager_list_container').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n\n            function drawAction() {\n                //{{- if .CanAddFiles}}\n                if (dt.rows().count() === 0) {\n                    $('#file_manager_list_container').addClass(\"d-none\");\n                    $('#upload_files_empty_container').removeClass(\"d-none\");\n                    return;\n                }\n                $('#upload_files_empty_container').addClass(\"d-none\");\n                $('#file_manager_list_container').removeClass(\"d-none\");\n                //{{- end}}\n                lightbox.reload();\n                KTMenu.createInstances();\n\n                let targets = document.querySelectorAll('#file_manager_list_body tr');\n                if (targets) {\n                    KTUtil.each(targets, function (target) {\n                        if (!target){\n                            return;\n                        }\n                        let checkbox = target.querySelector('.form-check-input');\n                        if (!checkbox) {\n                            return;\n                        }\n                        if (target.classList.contains('row-selected')) {\n                            checkbox.checked = true;\n                        } else {\n                            checkbox.checked = false;\n                        }\n                    });\n                }\n                toggleToolbars();\n                handleRowActions();\n                $('#file_manager_list_body').localize();\n            }\n\n            dt.on('user-select', function(e, dt, type, cell, originalEvent){\n                let pageSelected = dt.rows({ selected: true, page: 'current' }).count();\n                let totalSelected = dt.rows({ selected: true, search: 'applied' }).count();\n                if ($(originalEvent.target).is(':checked')){\n                    pageSelected++;\n                    totalSelected++;\n                } else {\n                    pageSelected--;\n                    totalSelected--;\n                }\n\n                handleToogleToolbar(pageSelected, totalSelected);\n            });\n        }\n\n        function handleToogleToolbar(pageSelected, totalSelected) {\n            const navBar = document.querySelector('[data-kt-filemanager-table-nav-bar=\"base\"]');\n            const toolbarSelected = document.querySelector('[data-kt-filemanager-table-toolbar=\"selected\"]');\n            const selectedCount = document.querySelector('[data-kt-filemanager-table-select=\"selected_count\"]');\n            const selectAllContainer = document.querySelector('[data-kt-filemanager-table-select=\"select_all_pages_container\"]');\n            const selectAllCheck = document.querySelector('[data-kt-filemanager-table-select=\"select_all_pages\"]');\n\n            if (totalSelected > 0) {\n                let pageTotal = dt.rows({ page: 'current' }).count();\n                if (pageSelected === pageTotal){\n                    selectAllContainer.classList.remove(\"d-none\");\n                    $('#select_checkbox').prop(\"checked\", true);\n                } else {\n                    $('#select_checkbox').prop(\"checked\", false);\n                    selectAllCheck.checked = false;\n                    selectAllContainer.classList.add('d-none');\n                }\n                if (selectedCount){\n                    selectedCount.innerHTML =  $.t('general.selected_items', { count: totalSelected});\n                }\n                if (navBar){\n                    navBar.classList.add('d-none');\n                }\n                if (toolbarSelected){\n                    toolbarSelected.classList.remove('d-none');\n                }\n            } else {\n                $('#select_checkbox').prop(\"checked\", false);\n                selectAllCheck.checked = false;\n                if (navBar) {\n                    navBar.classList.remove('d-none');\n                }\n                if (toolbarSelected) {\n                    toolbarSelected.classList.add('d-none');\n                }\n            }\n        }\n\n        function handleRowActions() {\n            const renameButtons = document.querySelectorAll('[data-kt-filemanager-table-action=\"rename\"]');\n\n            renameButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    renameItem(dt.row(parent).data()[\"meta\"]);\n                });\n            });\n\n            const moveCopyButtons = document.querySelectorAll('[data-kt-filemanager-table-action=\"move_or_copy\"]');\n\n            moveCopyButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    moveOrCopyItem(dt.row(parent).data()[\"meta\"]);\n                });\n            });\n\n            const shareButtons = document.querySelectorAll('[data-kt-filemanager-table-action=\"share\"]');\n\n            shareButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    shareItem(dt.row(parent).data()[\"meta\"]);\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-kt-filemanager-table-action=\"delete\"]');\n\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    deleteItem(dt.row(parent).data()[\"meta\"]);\n                });\n            });\n\n            const viewMediaLinks = document.querySelectorAll('[data-kt-filemanager-table-action=\"view_media\"]');\n\n            viewMediaLinks.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    let rowData = dt.row(parent).data();\n                    openMediaPlayer(rowData[\"name\"], rowData[\"url\"]);\n                });\n            });\n        }\n\n        var initLightbox = function() {\n            lightbox = GLightbox({\n                    slideHTML: `<div class=\"gslide\">\n                                    <div class=\"gslide-inner-content\">\n                                        <div class=\"ginner-container\">\n                                            <div class=\"gslide-media\">\n                                            </div>\n                                            <div class=\"gslide-description gslide-description-bg\">\n                                                <div class=\"gdesc-inner\">\n                                                    <h4 class=\"gslide-title\"></h4>\n                                                    <div class=\"gslide-desc\"></div>\n                                                </div>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>`,\n                    preload: false,\n                    closeOnOutsideClick: false\n                });\n        }\n\n        var handleSearchDatatable = function () {\n            const filterSearch = $(document.querySelector('[data-kt-filemanager-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n        }\n\n        var initToggleToolbar = function () {\n            const selectAllCheck = document.querySelector('[data-kt-filemanager-table-select=\"select_all_pages\"]');\n            if (selectAllCheck){\n                let el = $(selectAllCheck);\n                el.off(\"change\");\n                el.on('change', function(e){\n                    if (this.checked){\n                        dt.rows({ search: 'applied' }).select();\n                    } else {\n                        let selectedIndexes = [];\n                        dt.rows({ selected: true, page: 'current' }).every(function (rowIdx, tableLoop, rowLoop){\n                            selectedIndexes.push(rowIdx);\n                        });\n                        dt.rows().deselect();\n                        selectedIndexes.forEach((idx) => {\n                            dt.row(idx).select();\n                        });\n                    }\n                    toggleToolbars();\n                })\n            }\n            const downloadButton = document.querySelector('[data-kt-filemanager-table-select=\"download_selected\"]');\n            if (downloadButton){\n                let el = $(downloadButton);\n                el.off(\"click\");\n                el.on('click', function(e){\n                    let filesArray = [];\n                    dt.rows({ selected: true, search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                        let row = dt.row(rowIdx);\n                        filesArray.push(getNameFromMeta(row.data()['meta']));\n                    });\n                    let token = \"{{.CSRFToken}}\";\n                    let downloadURL = '{{.DownloadURL}}';\n                    let currentDir = '{{.CurrentDir}}';\n                    let ts = new Date().getTime().toString();\n                    let files = JSON.stringify(filesArray);\n                    $(`<form method=\"post\" action=\"${downloadURL}?path=${currentDir}&_=${ts}\" target=\"_blank\" rel=\"noopener noreferrer\">\n                        <input type=\"hidden\" name=\"_form_token\" value=\"${token}\">\n                        <textarea name=\"files\" hidden>${files}</textarea>\n                       </form>`).appendTo('body').submit().remove();\n                });\n            }\n\n            const moveOrCopyButton = document.querySelector('[data-kt-filemanager-table-select=\"move_or_copy_selected\"]');\n            if (moveOrCopyButton){\n                let el = $(moveOrCopyButton);\n                el.off(\"click\");\n                el.on('click', function(e){\n                    $('#errorMsg').addClass(\"d-none\");\n                    $('#move_copy_name_container').addClass(\"d-none\");\n                    $('#move_copy_source').val(\"\");\n                    $('#modal_move_or_copy').modal('show');\n                    KTDatatablesFoldersExplorer.init('{{.DirsURL}}?dirtree=1&path={{.CurrentDir}}', '{{.CurrentDir}}');\n                });\n            }\n\n            const shareButton = document.querySelector('[data-kt-filemanager-table-select=\"share_selected\"]');\n            if (shareButton){\n                let el = $(shareButton);\n                el.off(\"click\");\n                el.on('click', function(e){\n                    let filesArray = [];\n                    dt.rows({ selected: true, search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                        let row = dt.row(rowIdx);\n                        filesArray.push(getNameFromMeta(row.data()['meta']));\n                    });\n                    let files = encodeURIComponent(JSON.stringify(filesArray));\n                    let shareURL = '{{.ShareURL}}';\n                    let currentDir = '{{.CurrentDir}}';\n                    let ts = new Date().getTime().toString();\n                    window.open(`${shareURL}?path=${currentDir}&files=${files}&_=${ts}`,'_blank');\n                });\n            }\n\n            const deleteButton = document.querySelector('[data-kt-filemanager-table-select=\"delete_selected\"]');\n            if (deleteButton) {\n\n                function getMultiDeleteErrorMessage(status, deleted) {\n                    let errorMessage;\n                    switch (status) {\n                        case 403:\n                            if (deleted > 0){\n                                errorMessage = \"fs.delete_multi.err_403_partial\";\n                            } else {\n                                errorMessage = \"fs.delete_multi.err_403\";\n                            }\n                            break;\n                        case 429:\n                        if (deleted > 0){\n                                errorMessage = \"fs.delete_multi.err_429_partial\";\n                            } else {\n                                errorMessage = \"fs.delete_multi.err_429\";\n                            }\n                            break;\n                    }\n                    if (!errorMessage){\n                        if (deleted > 0){\n                            errorMessage = \"fs.delete_multi.err_generic_partial\";\n                        } else {\n                            errorMessage = \"fs.delete_multi.err_generic\";\n                        }\n                    }\n                    return errorMessage;\n                }\n\n                let el = $(deleteButton);\n                el.off(\"click\");\n                el.on('click', function(e){\n                    ModalAlert.fire({\n                        text: $.t('general.delete_multi_confirm'),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.delete_confirm_btn'),\n                        cancelButtonText: $.t('general.cancel'),\n                        customClass: {\n                            confirmButton: \"btn btn-danger\",\n                            cancelButton: 'btn btn-secondary'\n                        }\n                    }).then((result) => {\n                        if (result.isConfirmed){\n                            let hasError = false;\n                            let deleted = 0;\n                            let index = 0;\n                            let errDivEl = $('#errorMsg');\n                            let errTxtEl = $('#errorTxt');\n                            errDivEl.addClass(\"d-none\");\n                            let selectedRowsIdx = [];\n                            dt.rows({ selected: true, search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                                selectedRowsIdx.push(rowIdx);\n                            });\n                            if (selectedRowsIdx.length == 0){\n                                return;\n                            }\n                            clearLoading();\n                            KTApp.showPageLoading();\n\n                            function deleteSelected() {\n                                if (index >= selectedRowsIdx.length || hasError){\n                                    KTApp.hidePageLoading();\n                                    if (!hasError){\n                                        location.reload();\n                                    }\n                                    return;\n                                }\n                                let meta = dt.row(selectedRowsIdx[index]).data()['meta'];\n                                let itemName = getNameFromMeta(meta);\n                                let isDir = (getTypeFromMeta(meta) == \"1\");\n                                let path;\n                                if (isDir){\n                                    path = '{{.DirsURL}}';\n                                } else {\n                                    path = '{{.FilesURL}}';\n                                }\n                                path+='?path={{.CurrentDir}}'+encodeURIComponent(\"/\"+itemName);\n\n                                if (selectedRowsIdx.length > 1){\n                                    let info = $.t('fs.deleting', {\n                                        idx : index + 1,\n                                        total: selectedRowsIdx.length\n                                    });\n                                    setLoadingText(info,itemName);\n                                }\n\n                                axios.delete(path,{\n                                    timeout: 15000,\n                                    headers: {\n                                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                                    },\n                                    validateStatus: function (status) {\n                                        if (isDir){\n                                            return status == 202;\n                                        }\n                                        return status == 200;\n                                    }\n                                }).then(function(response){\n                                    if (isDir){\n                                        taskStatusWaiter.wait({\n                                            taskID: response.data.message,\n                                            delay: 500\n                                        }).then((result) => {\n                                            index++;\n                                            if (result.status == 200){\n                                                deleted++;\n                                            } else {\n                                                hasError = true;\n                                                let errorMessage = getMultiDeleteErrorMessage(result.status, deleted);\n                                                setI18NData(errTxtEl, errorMessage);\n                                                errDivEl.removeClass(\"d-none\");\n                                            }\n                                            deleteSelected();\n                                        });\n                                    } else {\n                                        index++;\n                                        deleted++;\n                                        deleteSelected();\n                                    }\n                                }).catch(function(error){\n                                    index++;\n                                    hasError = true;\n                                    let status = 0;\n                                    if (error && error.response) {\n                                        status = error.response.status;\n                                    }\n                                    let errorMessage = getMultiDeleteErrorMessage(status, deleted);\n                                    setI18NData(errTxtEl, errorMessage);\n                                    errDivEl.removeClass(\"d-none\");\n                                    deleteSelected();\n                                });\n                            }\n\n                            deleteSelected();\n                        }\n                    });\n                });\n            }\n\n            $('#select_checkbox').on(\"click\", function(){\n                let targets = document.querySelectorAll('#file_manager_list_body .form-check-input');\n                if (!targets){\n                    return;\n                }\n                dt.rows().deselect();\n                let isChecked = $(this).is(':checked');\n                if (isChecked){\n                    dt.rows({page: 'current'}).select();\n                }\n                KTUtil.each(targets, function (target) {\n                    if (target){\n                        target.checked = isChecked;\n                    }\n                });\n                toggleToolbars();\n            });\n\n            toggleToolbars();\n        }\n\n        var toggleToolbars = function () {\n            let pageSelected = dt.rows({ selected: true, page: 'current' }).count();\n            let totalSelected = dt.rows({ selected: true , search: 'applied'}).count();\n            handleToogleToolbar(pageSelected, totalSelected);\n        }\n\n        return {\n            init: function () {\n                initLightbox();\n                initDatatable();\n                handleSearchDatatable();\n                initToggleToolbar();\n            }\n        }\n    }();\n\n    function getNameFromMeta(meta) {\n        return meta.split('_').slice(1).join('_');\n    }\n\n    function getTypeFromMeta(meta) {\n        return meta.split('_')[0];\n    }\n\n    //{{- if not .ShareUploadBaseURL}}\n    function onDirBrowserClick(dirPath) {\n        KTDatatablesFoldersExplorer.init('{{.DirsURL}}?dirtree=1&path='+dirPath, dirPath);\n    }\n\n    function moveOrCopyItem(meta) {\n        $('#errorMsg').addClass(\"d-none\");\n        $('#move_copy_name_container').removeClass(\"d-none\");\n        $('#move_copy_source').val(meta);\n        $('#move_copy_name').val(getNameFromMeta(meta));\n        $('#modal_move_or_copy').modal('show');\n        KTDatatablesFoldersExplorer.init('{{.DirsURL}}?dirtree=1&path={{.CurrentDir}}', '{{.CurrentDir}}');\n    }\n\n    function getMoveOrCopyItems() {\n        let items = [];\n        let targetDir = $(\"#move_copy_folder\").val();\n        if (targetDir != \"/\") {\n            targetDir = targetDir.endsWith('/') ? targetDir.slice(0, -1) : targetDir;\n        }\n        if (targetDir.trim() == \"\"){\n            targetDir = \"{{.CurrentDir}}\";\n        } else {\n            targetDir = encodeURIComponent(targetDir);\n        }\n\n        if ($('#move_copy_name_container').hasClass(\"d-none\")){\n            // bulk action\n            let dt = $('#file_manager_list').DataTable();\n            dt.rows({ selected: true, search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){\n                let row = dt.row(rowIdx);\n                let sourceName = getNameFromMeta(row.data()['meta']);\n                items.push({\n                    targetDir: targetDir,\n                    sourceName: sourceName,\n                    targetName: sourceName\n                });\n            });\n        } else {\n            let meta = $('#move_copy_source').val();\n            let sourceName = getNameFromMeta(meta);\n            items.push({\n                targetDir: targetDir,\n                sourceName: sourceName,\n                targetName: $(\"#move_copy_name\").val()\n            });\n        }\n        return items;\n    }\n\n    function checkMoveCopyItems(items) {\n        let hasSlash = items.some(item => item.targetName.includes(\"/\"));\n        if (hasSlash){\n            return [];\n        }\n        return items;\n    }\n\n    function doCopy() {\n        $('#errorMsg').addClass(\"d-none\");\n        let items = getMoveOrCopyItems();\n        if (items.length == 0){\n            return;\n        }\n        items = checkMoveCopyItems(items)\n        if (items.length == 0){\n            ModalAlert.fire({\n                text: $.t('fs.invalid_name'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n\n        function showCopyError(status) {\n            KTApp.hidePageLoading();\n            let errorMessage = \"\";\n            switch (status) {\n                case 403:\n                    errorMessage = \"fs.copy.err_403\";\n                    break;\n                case 429:\n                    errorMessage = \"fs.copy.err_429\";\n                    break;\n                default:\n                    errorMessage = \"fs.copy.err_generic\";\n            }\n            ModalAlert.fire({\n                text: $.t(errorMessage),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n        }\n\n        let hasError = false;\n        let index = 0;\n\n        clearLoading();\n        KTApp.showPageLoading();\n\n        function copyItem() {\n            if (index >= items.length || hasError){\n                KTApp.hidePageLoading();\n                if (!hasError){\n                    location.reload();\n                }\n                return;\n            }\n            let item = items[index];\n            let sourcePath = decodeURIComponent('{{.CurrentDir}}');\n            if (!sourcePath.endsWith('/')){\n                sourcePath+=\"/\";\n            }\n            sourcePath+=item.sourceName;\n\n            let targetPath = decodeURIComponent(item.targetDir);\n            if (!targetPath.endsWith('/')){\n                targetPath+=\"/\";\n            }\n            targetPath+=item.targetName;\n\n            if (items.length > 1) {\n                let info = $.t('fs.copying', {\n                    idx: index + 1,\n                    total: items.length\n                });\n                setLoadingText(info,`${sourcePath} => ${targetPath}`);\n            }\n            let path = '{{.FileActionsURL}}/copy';\n            path+='?path={{.CurrentDir}}'+encodeURIComponent(\"/\"+item.sourceName)+'&target='+item.targetDir+encodeURIComponent(\"/\"+item.targetName);\n\n            axios.post(path, null, {\n                timeout: 15000,\n                headers: {\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 202;\n                }\n            }).then(function (response) {\n                taskStatusWaiter.wait({\n                    taskID: response.data.message,\n                    delay: 500\n                }).then((result) => {\n                    index++;\n                    if (result.status != 200){\n                        hasError = true;\n                        showCopyError(result.status);\n                    }\n                    copyItem();\n                });\n            }).catch(function (error) {\n                index++;\n                hasError = true;\n                let status = 0;\n                if (error && error.response) {\n                    status = error.response.status;\n                }\n                showCopyError(status);\n                copyItem();\n            });\n        }\n\n        let filesArray = [];\n        for (let i = 0; i < items.length; i++){\n            filesArray.push(items[i].targetName);\n        }\n\n        CheckExist.fire({\n            operation: \"copy\",\n            files: filesArray,\n            path: items[0].targetDir\n        }).then((result)=>{\n            if (result.error) {\n                hasError = true;\n                let message = \"fs.copy.err_generic\";\n                if (result.is_redirect){\n                    message = \"general.expired_session\";\n                }\n                ModalAlert.fire({\n                    text: $.t(message),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            } else if (result.data.length > 0){\n                hasError = true;\n                ModalAlert.fire({\n                    text: $.t(\"fs.copy.err_exists\"),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            }\n            copyItem();\n        });\n    }\n\n    function doMove() {\n        $('#errorMsg').addClass(\"d-none\");\n        let items = getMoveOrCopyItems();\n        if (items.length == 0){\n            return;\n        }\n        items = checkMoveCopyItems(items)\n        if (items.length == 0){\n            ModalAlert.fire({\n                text: $.t(\"fs.invalid_name\"),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n\n        function showMoveError(status) {\n            KTApp.hidePageLoading();\n            let errorMessage = \"\";\n            switch (status) {\n                case 400:\n                    errorMessage = \"fs.move.err_unsupported\";\n                    break;\n                case 403:\n                    errorMessage = \"fs.move.err_403\";\n                    break;\n                case 429:\n                    errorMessage = \"fs.move.err_429\";\n                    break;\n                default:\n                    errorMessage = \"fs.move.err_generic\";\n            }\n            ModalAlert.fire({\n                text: $.t(errorMessage),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n        }\n\n        let hasError = false;\n        let index = 0;\n\n        clearLoading();\n        KTApp.showPageLoading();\n\n        function moveItem() {\n            if (index >= items.length || hasError){\n                KTApp.hidePageLoading();\n                if (!hasError){\n                    location.reload();\n                }\n                return;\n            }\n            let item = items[index];\n            let sourcePath = decodeURIComponent('{{.CurrentDir}}');\n            if (!sourcePath.endsWith('/')){\n                sourcePath+=\"/\";\n            }\n            sourcePath+=item.sourceName;\n\n            let targetPath = decodeURIComponent(item.targetDir);\n            if (!targetPath.endsWith('/')){\n                targetPath+=\"/\";\n            }\n            targetPath+=item.targetName;\n\n            if (items.length > 1) {\n                let info = $.t('fs.moving', {\n                    idx: index + 1,\n                    total: items.length\n                });\n                setLoadingText(info,`${sourcePath} => ${targetPath}`);\n            }\n            let path = '{{.FileActionsURL}}/move';\n            path+='?path={{.CurrentDir}}'+encodeURIComponent(\"/\"+item.sourceName)+'&target='+item.targetDir+encodeURIComponent(\"/\"+item.targetName);\n\n            axios.post(path, null, {\n                timeout: 15000,\n                headers: {\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 202;\n                }\n            }).then(function (response) {\n                taskStatusWaiter.wait({\n                    taskID: response.data.message,\n                    delay: 500\n                }).then((result) => {\n                    index++;\n                    if (result.status != 200){\n                        hasError = true;\n                        showMoveError(result.status);\n                    }\n                    moveItem();\n                });\n            }).catch(function (error) {\n                index++;\n                hasError = true;\n                let status = 0;\n                if (error && error.response) {\n                    status = error.response.status;\n                }\n                showMoveError(status);\n                moveItem();\n            });\n        }\n\n        let filesArray = [];\n        for (let i = 0; i < items.length; i++){\n            filesArray.push(items[i].targetName);\n        }\n\n        CheckExist.fire({\n            operation: \"move\",\n            files: filesArray,\n            path: items[0].targetDir\n        }).then((result)=>{\n            if (result.error) {\n                hasError = true;\n                let message = \"fs.move.err_generic\";\n                if (result.is_redirect){\n                    message = \"general.expired_session\";\n                }\n                ModalAlert.fire({\n                    text: $.t(message),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            } else if (result.data.length > 0){\n                hasError = true;\n                ModalAlert.fire({\n                text: $.t(\"fs.move.err_exists\"),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            }\n            moveItem();\n        });\n    }\n\n    function showDeleteItemError(status, itemName) {\n        KTApp.hidePageLoading();\n        let errorMessage;\n        switch (status) {\n            case 405:\n                errorMessage = \"general.expired_session\";\n                break;\n            case 403:\n                errorMessage = \"fs.delete.err_403\";\n                break;\n            case 429:\n                errorMessage = \"fs.delete.err_429\";\n                break;\n            default:\n                errorMessage = \"fs.delete.err_generic\";\n        }\n        ModalAlert.fire({\n            text: $.t(errorMessage, {name: itemName}),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.ok'),\n            customClass: {\n                confirmButton: \"btn btn-primary\"\n            }\n        });\n    }\n\n    function deleteItem(meta) {\n        $('#errorMsg').addClass(\"d-none\");\n        let itemName = getNameFromMeta(meta);\n\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm', {name: itemName}),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let isDir = (getTypeFromMeta(meta) == \"1\");\n                let path;\n                if (isDir){\n                    path = '{{.DirsURL}}';\n                } else {\n                    path = '{{.FilesURL}}';\n                }\n                path+='?path={{.CurrentDir}}'+encodeURIComponent(\"/\"+itemName);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        if (isDir){\n                            return status == 202;\n                        }\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    if (isDir){\n                        taskStatusWaiter.wait({\n                            taskID: response.data.message,\n                            delay: 500\n                        }).then((result) => {\n                            if (result.status == 200){\n                                location.reload();\n                            } else {\n                                showDeleteItemError(result.status, itemName);\n                            }\n                        });\n                    } else {\n                        location.reload();\n                    }\n                }).catch(function(error){\n                    let status = 0;\n                    if (error && error.response) {\n                        status = error.response.status;\n                    }\n                    showDeleteItemError(status, itemName);\n                });\n            }\n        });\n    }\n\n    function shareItem(meta) {\n        let filesArray = [];\n        filesArray.push(getNameFromMeta(meta));\n        let files = encodeURIComponent(JSON.stringify(filesArray));\n        let shareURL = '{{.ShareURL}}';\n        let currentDir = '{{.CurrentDir}}';\n        let ts = new Date().getTime().toString();\n        window.open(`${shareURL}?path=${currentDir}&files=${files}&_=${ts}`,'_blank');\n    }\n\n    function renameItem(meta) {\n        $('#errorMsg').addClass(\"d-none\");\n        let oldName = getNameFromMeta(meta);\n        $('#rename_old_name').val(meta);\n        $('#rename_new_name').val(oldName);\n        $('#rename_title').text($.t('fs.rename.title', { name: oldName}));\n        $('#modal_rename').modal('show');\n    }\n\n    function showRenameItemError(status, oldName) {\n        KTApp.hidePageLoading();\n        let errorMessage;\n        switch (status) {\n            case 400:\n                errorMessage = \"fs.rename.err_unsupported\";\n                break;\n            case 403:\n                errorMessage = \"fs.rename.err_403\";\n                break;\n            case 429:\n                errorMessage = \"fs.rename.err_429\";\n                break;\n            default:\n                errorMessage = \"fs.rename.err_generic\";\n        }\n\n        ModalAlert.fire({\n            text: $.t(errorMessage, {name: oldName}),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.ok'),\n            customClass: {\n                confirmButton: \"btn btn-primary\"\n            }\n        });\n    }\n\n    function doRename() {\n        let meta = $('#rename_old_name').val();\n        let oldName = getNameFromMeta(meta);\n        let newName = $('#rename_new_name').val();\n        if (!newName){\n            ModalAlert.fire({\n                text: $.t('general.name_required'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n        if (newName == oldName){\n            ModalAlert.fire({\n                text: $.t('general.name_different'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n        if (newName.includes(\"/\")){\n            ModalAlert.fire({\n                text: $.t('fs.invalid_name'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n\n        clearLoading();\n        KTApp.showPageLoading();\n\n        function executeRename() {\n            let path = '{{.FileActionsURL}}/move';\n            path += '?path={{.CurrentDir}}' + encodeURIComponent(\"/\" + oldName) + '&target={{.CurrentDir}}' + encodeURIComponent(\"/\" + newName);\n            axios.post(path, null, {\n                timeout: 15000,\n                headers: {\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 202;\n                }\n            }).then(function (response) {\n                taskStatusWaiter.wait({\n                    taskID: response.data.message,\n                    delay: 500\n                }).then((result) => {\n                    if (result.status == 200){\n                        location.reload();\n                    } else {\n                        showRenameItemError(result.status, oldName);\n                    }\n                });\n            }).catch(function (error) {\n                let status = 0;\n                if (error && error.response) {\n                    status = error.response.status;\n                }\n                showRenameItemError(status, oldName);\n            });\n        }\n\n        CheckExist.fire({\n            operation: \"move\",\n            files: [newName],\n            path: '{{.CurrentDir}}'\n        }).then((result)=>{\n            if (result.error) {\n                KTApp.hidePageLoading();\n                let message = $.t('fs.rename.err_generic', { name: oldName });\n                if (result.is_redirect){\n                    message = $.t('general.expired_session');\n                }\n                ModalAlert.fire({\n                    text: message,\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n                return;\n            }\n            if (result.data.length > 0){\n                KTApp.hidePageLoading();\n                ModalAlert.fire({\n                    text: $.t('fs.rename.err_exists', { name: oldName }),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n                return;\n            }\n            executeRename();\n        });\n    }\n\n    function showCreateNewFolder(sender) {\n        if (sender == 0) {\n            $('.new_folder_divider').removeClass(\"d-none\");\n            $('#file_manager_new_folder').removeClass(\"d-none\");\n            $('#errorMsg').addClass(\"d-none\");\n            let el = $('#file_manager_new_folder_input');\n            el.val(\"\");\n            el.focus();\n            return;\n        }\n        $('.dir_browser_folder_divider').removeClass(\"d-none\");\n        $('#dirsbrowser_new_folder').removeClass(\"d-none\");\n        $('#errorModalMsg').addClass(\"d-none\");\n        let el = $('#dirsbrowser_new_folder_input');\n        el.val(\"\");\n        el.focus();\n    }\n\n    function hideCreateNewFolder(sender) {\n        if (sender == 0){\n            $('.new_folder_divider').addClass(\"d-none\");\n            $('#file_manager_new_folder').addClass(\"d-none\");\n        } else {\n            $('.dir_browser_folder_divider').addClass(\"d-none\");\n            $('#dirsbrowser_new_folder').addClass(\"d-none\");\n        }\n    }\n\n    function createNewFolder() {\n        let errDivEl = $('#errorMsg');\n        let errTxtEl = $('#errorTxt');\n        let dirName = $(\"#file_manager_new_folder_input\").val();\n        let submitButton  = document.querySelector('#file_manager_add_folder');\n        let cancelButton = document.querySelector('#file_manager_cancel_folder');\n        errDivEl.addClass(\"d-none\");\n        if (!dirName){\n            setI18NData(errTxtEl, \"fs.folder_name_required\");\n            errDivEl.removeClass(\"d-none\");\n            return;\n        }\n        if (dirName.includes(\"/\")){\n            setI18NData(errTxtEl, \"fs.invalid_name\");\n            errDivEl.removeClass(\"d-none\");\n            return;\n        }\n\n        let path = '{{.DirsURL}}?path={{.CurrentDir}}' + encodeURIComponent(\"/\"+dirName);\n\t\tsubmitButton.setAttribute('data-kt-indicator', 'on');\n\t\tsubmitButton.disabled = true;\n        cancelButton.disabled = true;\n\n        axios.post(path, null, {\n            timeout: 15000,\n            headers: {\n                'X-CSRF-TOKEN': '{{.CSRFToken}}'\n            },\n            validateStatus: function (status) {\n                return status == 201;\n            }\n            }).then(function (response) {\n                location.reload();\n            }).catch(function (error) {\n                let errorMessage = \"\";\n                if (error && error.response) {\n                    switch (error.response.status) {\n                        case 200:\n                            errorMessage = \"general.expired_session\";\n                            break;\n                        case 403:\n                            errorMessage = \"fs.create_dir.err_403\";\n                            break;\n                        case 429:\n                            errorMessage = \"fs.create_dir.err_429\";\n                            break;\n                    }\n                }\n                if (!errorMessage){\n                    errorMessage = \"fs.create_dir.err_generic\";\n                }\n                setI18NData(errTxtEl, errorMessage);\n                errDivEl.removeClass(\"d-none\");\n                submitButton.removeAttribute('data-kt-indicator');\n\t\t        submitButton.disabled = false;\n                cancelButton.disabled = false;\n            });\n    }\n    //{{- end}}\n\n    var playerKeepAlive;\n\n    function uploadFiles(files) {\n        if (files.length == 0) {\n            ModalAlert.fire({\n                text: $.t('fs.upload_no_files'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n        keepAlive();\n        let keepAliveTimer = setInterval(keepAlive, keepAliveInterval);\n\n        let has_errors = false;\n        let index = 0;\n        let success = 0;\n        let checkedDirs = [];\n        $('#errorMsg').addClass(\"d-none\");\n        clearLoading();\n        KTApp.showPageLoading();\n\n        function uploadFile() {\n            if (index >= files.length || has_errors) {\n                //console.log(\"upload done, index: \"+index+\" has errors: \"+has_errors+\" ok: \"+success);\n                clearInterval(keepAliveTimer);\n                KTApp.hidePageLoading();\n                if (!has_errors) {\n                    location.reload();\n                }\n                return;\n            }\n\n            let f = files[index];\n            let uploadPath;\n            let name = f.name;\n            let mkdirParents = \"false\";\n            if (f.fullPath){\n                name = f.fullPath;\n                let dirName = name.substr(0, name.lastIndexOf(\"/\"));\n                if (!checkedDirs.includes(dirName)){\n                    mkdirParents = \"true\";\n                    checkedDirs.push(dirName);\n                }\n            }\n            //{{- if .ShareUploadBaseURL}}\n            uploadPath = '{{.ShareUploadBaseURL}}' + encodeURIComponent(\"/\" + name)+\"?mkdir_parents=\"+mkdirParents;\n            //{{- else}}\n            uploadPath = '{{.FileURL}}?path={{.CurrentDir}}' + encodeURIComponent(\"/\" + name)+\"&mkdir_parents=\"+mkdirParents;\n            //{{- end}}\n            let lastModified;\n            try {\n                lastModified = f.lastModified;\n            } catch (e) {\n                console.error(\"unable to get last modified time from file: \" + e.message);\n                lastModified = \"\";\n            }\n\n            let info = \"\";\n            if (files.length > 1){\n                info = $.t('fs.uploading', {\n                    idx: index + 1,\n                    total: files.length\n                });\n            }\n\n            setLoadingText(info,f.name);\n\n            axios.post(uploadPath, f, {\n                headers: {\n                    'X-SFTPGO-MTIME': lastModified,\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                onUploadProgress: function (progressEvent) {\n                    if (!progressEvent.total){\n                        return;\n                    }\n                    const percentage = Math.round((100 * progressEvent.loaded) / progressEvent.total);\n                    if (percentage > 0 && percentage < 100){\n                        setLoadingText(info,`${f.name} ${percentage}%`);\n                    }\n                },\n                validateStatus: function (status) {\n                    return status == 201;\n                }\n            }).then(function (response) {\n                index++;\n                success++;\n                uploadFile();\n            }).catch(function (error) {\n                let errorMessage;\n                if (error && error.response) {\n                    switch (error.response.status) {\n                        case 403:\n                            errorMessage = \"fs.upload.err_403\";\n                            break;\n                        case 429:\n                            errorMessage = \"fs.upload.err_429\";\n                            break;\n                    }\n                }\n                if (!errorMessage){\n                    errorMessage = \"fs.upload.err_generic\";\n                }\n                index++;\n                has_errors = true;\n                setI18NData($('#errorTxt'), errorMessage);\n                $('#errorMsg').removeClass(\"d-none\");\n                uploadFile();\n            });\n        }\n\n        let filesArray = [];\n        let dirsArray = [];\n        if (files.length > 0){\n            for (let i = 0; i < files.length; i++){\n                if (files[i].fullPath){\n                    let dirName = files[i].fullPath.split('/')[0];\n                    if (!dirsArray.includes(dirName)){\n                        dirsArray.push(dirName);\n                    }\n                    if (!filesArray.includes(dirName)){\n                        filesArray.push(dirName);\n                    }\n                } else {\n                    filesArray.push(files[i].name);\n                }\n            }\n        }\n\n        CheckExist.fire({\n            operation: \"upload\",\n            files: filesArray,\n            path: \"{{.CurrentDir}}\"\n        }).then((result)=> {\n            if (result.error) {\n                has_errors = true;\n                let message = \"fs.upload.err_generic\";\n                if (result.is_redirect){\n                    message = \"general.expired_session\";\n                }\n                setI18NData($('#errorTxt'), message);\n                $('#errorMsg').removeClass(\"d-none\");\n                uploadFile();\n                return;\n            }\n            let existingEntries = [];\n            let fileOverwriteDirs = [];\n            $.each(result.data, function (key, item) {\n                if (item.type === \"1\" && !dirsArray.includes(item.name)) {\n                    fileOverwriteDirs.push(item.name);\n                } else {\n                    existingEntries.push(item.name);\n                }\n            });\n            if (fileOverwriteDirs.length > 0) {\n                has_errors = true;\n                setI18NData($('#errorTxt'), \"fs.upload.err_dir_overwrite\", {val: fileOverwriteDirs.join(\", \")});\n                $('#errorMsg').removeClass(\"d-none\");\n                uploadFile();\n                return;\n            }\n            if (existingEntries.length > 0) {\n                KTApp.hidePageLoading();\n                ModalAlert.fire({\n                    text: $.t('fs.upload.overwrite_text'),\n                    items: existingEntries,\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.confirm'),\n                    cancelButtonText: $.t('general.cancel'),\n                    customClass: {\n                        confirmButton: \"btn btn-danger\",\n                        cancelButton: 'btn btn-secondary'\n                    }\n                }).then((result) => {\n                    if (result.isConfirmed){\n                        KTApp.showPageLoading();\n                    } else {\n                        has_errors = true;\n                    }\n                    uploadFile();\n                });\n                return;\n            }\n            uploadFile();\n        });\n    }\n\n    var CheckExist = function () {\n            var promiseResolve;\n\n            function doCheck(operation, files, target) {\n                let path = '{{.CheckExistURL}}?op='+encodeURIComponent(operation)+\"&path=\"+target;\n                axios.post(path, {\n                    files: files\n                }, {\n                    headers: {\n                        timeout: 15000,\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    let isJsonResponse = response.headers['content-type'] === 'application/json'\n                    promiseResolve({\n                        error: !isJsonResponse,\n                        is_redirect: !isJsonResponse,\n                        data: response.data\n                    });\n                }).catch(function(error){\n                    promiseResolve({\n                        error: true,\n                        is_redirect: false\n                    });\n                });\n            }\n\n            return {\n                fire: function (params) {\n                    return new Promise(function (resolve, reject) {\n                        promiseResolve = resolve;\n                        doCheck(params.operation, params.files, params.path);\n                    });\n                }\n            }\n        }();\n\n    function openMediaPlayer(name, url){\n        $(\"#video_title\").text(name);\n        $(\"#video_player\").attr(\"src\", url);\n        $(\"#video_player\").get(0).load();\n        $('#modal_video_player').modal('show');\n        keepAlive();\n        playerKeepAlive = setInterval(keepAlive, keepAliveInterval);\n    }\n\n    $(document).on(\"i18nshow\", function(){\n        KTDatatablesServerSide.init();\n\n        var lastAddedFile = Date.now();\n\n        function canUpload() {\n            // Ugly hack to detect if we are still loading a large file list when the user try to upload files.\n            // The Dropzone addedfiles event is fired for directories before the files within them are added to the queue.\n            // TODO: investigate if there is a better way.\n            if (Date.now() - lastAddedFile < 500) {\n                ModalAlert.fire({\n                    text: $.t('fs.upload_queue_not_ready'),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n                return false;\n            }\n            return true;\n        }\n\n        var dropzone =  new Dropzone(\"#upload_files\", {\n            url: \"{{.FilesURL}}?path={{.CurrentDir}}\",\n            paramName: \"filenames\",\n            createImageThumbnails: false,\n            maxFiles: null,\n            maxFilesize: null,\n            autoQueue: false,\n            addRemoveLinks: false,\n            autoProcessQueue: false,\n            filesizeBase: 1000,\n            previewTemplate: dzPreviewTemplate,\n            init: function() {\n                var dropzone = this;\n                $(\"#upload_files_button\").click(function(){\n                    if (canUpload()){\n                        uploadFiles(dropzone.getAcceptedFiles());\n                    }\n                });\n                $(\"#upload_files_cancel\").click(function(){\n                    dropzone.removeAllFiles();\n                });\n            }\n        });\n\n        dropzone.on(\"addedfile\", file => {\n            for (node of file.previewElement.querySelectorAll(\"[data-custom-size]\")) {\n                node.textContent = fileSizeIEC(file.size);\n            }\n            if (file.fullPath){\n                for (var node of file.previewElement.querySelectorAll(\"[data-dz-name]\")) {\n                    node.textContent = file.fullPath;\n                }\n            }\n            lastAddedFile = Date.now();\n        });\n\n        var dropzoneEmpty =  new Dropzone(\"#upload_files_empty\", {\n            url: \"{{.FilesURL}}?path={{.CurrentDir}}\",\n            paramName: \"filenames\",\n            createImageThumbnails: false,\n            maxFiles: null,\n            maxFilesize: null,\n            autoQueue: false,\n            addRemoveLinks: false,\n            autoProcessQueue: false,\n            filesizeBase: 1000,\n            previewTemplate: dzPreviewTemplate,\n            init: function() {\n                var dropzoneEmpty = this;\n                $(\"#upload_files_empty_button\").click(function(){\n                    if (canUpload()){\n                        uploadFiles(dropzoneEmpty.getAcceptedFiles());\n                    }\n                });\n            }\n        });\n\n        dropzoneEmpty.on(\"addedfile\", file => {\n            for (node of file.previewElement.querySelectorAll(\"[data-custom-size]\")) {\n                node.textContent = fileSizeIEC(file.size);\n            }\n            if (file.fullPath){\n                for (var node of file.previewElement.querySelectorAll(\"[data-dz-name]\")) {\n                    node.textContent = file.fullPath;\n                }\n            }\n            lastAddedFile = Date.now();\n        });\n\n        $('#modal_video_player').on('hide.bs.modal', function () {\n            $(\"#video_player\").get(0).pause();\n            if (playerKeepAlive != null) {\n                clearInterval(playerKeepAlive);\n                playerKeepAlive = null;\n            }\n        });\n\n        //{{- if not .ShareUploadBaseURL}}\n        $('#modal_rename').on('shown.bs.modal', function () {\n            let newNameEl = $('#rename_new_name');\n            newNameEl.focus();\n            newNameEl.select();\n        });\n        //{{- end}}\n\n        // onclick handlers\n        var createDirBtn = $('#id_create_dir_button');\n        if (createDirBtn){\n            createDirBtn.on(\"click\", function (){\n                showCreateNewFolder(0);\n            });\n        }\n\n        var dirBrowserCreateDir = $('#id_dir_browser_create_dir');\n        if (dirBrowserCreateDir){\n            dirBrowserCreateDir.on(\"click\", function(){\n                showCreateNewFolder(1);\n            });\n        }\n\n        var fileManagerAddFolder = $('#file_manager_add_folder');\n        if (fileManagerAddFolder){\n            fileManagerAddFolder.on(\"click\", function() {\n                createNewFolder();\n            });\n        }\n\n        var fileManagerCancelCreateFolder = $('#file_manager_cancel_folder');\n        if (fileManagerCancelCreateFolder){\n            fileManagerCancelCreateFolder.on(\"click\", function(){\n                hideCreateNewFolder(0);\n            })\n        }\n\n        var dirBrowserCancelCreateFolder = $('#dirsbrowser_cancel_folder');\n        if (dirBrowserCancelCreateFolder){\n            dirBrowserCancelCreateFolder.on(\"click\", function(){\n                hideCreateNewFolder(1);\n            });\n        }\n\n        var doRenameBtn = $('#id_do_rename_button');\n        if (doRenameBtn){\n            doRenameBtn.on(\"click\", function() {\n                doRename();\n            });\n        }\n\n        var doCopyBtn = $('#id_copy_button');\n        if (doCopyBtn){\n            doCopyBtn.on(\"click\", function(){\n                doCopy();\n            });\n        }\n\n        var doMoveBtn = $('#id_move_button');\n        if (doMoveBtn){\n            doMoveBtn.on(\"click\", function(){\n                doMove();\n            });\n        }\n\n        var dismissErrorModalBtn = $('#id_dismiss_error_modal_msg');\n        if (dismissErrorModalBtn){\n            dismissErrorModalBtn.on(\"click\",function(){\n                $('#errorModalMsg').addClass(\"d-none\");\n            });\n        }\n\n    });\n</script>\n{{- end}}\n\n{{- define \"additionalnavitems\"}}\n{{- if .QuotaUsage.HasQuotaInfo}}\n<div class=\"d-flex align-items-center ms-2 ms-lg-3\">\n    <div class=\"btn btn-icon btn-active-light-primary position-relative w-35px h-35px w-md-40px h-md-40px\" data-kt-menu-trigger=\"{default:'click', lg: 'hover'}\" data-kt-menu-attach=\"parent\" data-kt-menu-placement=\"bottom-end\">\n        <i class=\"ki-duotone {{if .QuotaUsage.IsQuotaLow}}ki-information-5 text-warning{{else}}ki-information-2{{end}} fs-2\">\n            <span class=\"path1\"></span>\n            <span class=\"path2\"></span>\n            <span class=\"path3\"></span>\n        </i>\n    </div>\n    <div class=\"menu menu-sub menu-sub-dropdown menu-column w-375px\" data-kt-menu=\"true\">\n        <div class=\"card\">\n            <div class=\"card-header\">\n                <h3 class=\"card-title\"><span data-i18n=\"fs.quota_usage.title\" class=\"text-gray-700 fw-bold fs-6\">Quota usage</span></h3>\n            </div>\n            <div class=\"card-body p-0\">\n                {{- if .QuotaUsage.HasDiskQuota}}\n                {{- $size := .QuotaUsage.GetQuotaSize}}\n                {{- $files := .QuotaUsage.GetQuotaFiles}}\n                <div class=\"d-flex align-items-center bg-hover-lighten py-3 px-9\">\n                    <div class=\"symbol symbol-40px symbol-circle me-5\">\n                        <span class=\"symbol-label {{ if .QuotaUsage.IsDiskQuotaLow }}bg-light-warning{{end}}\">\n                            <i class=\"ki-duotone ki-external-drive {{ if .QuotaUsage.IsDiskQuotaLow }}text-warning{{end}} fs-1\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                                <span class=\"path3\"></span>\n                                <span class=\"path4\"></span>\n                                <span class=\"path5\"></span>\n                            </i>\n                        </span>\n                    </div>\n                    <div class=\"mb-1 pe-3 flex-grow-1\">\n                        <span data-i18n=\"fs.quota_usage.disk\" class=\"fs-6 text-gray-900 fw-semibold\">Disk quota</span>\n                        {{- if $size}}\n                        {{- $percentage := .QuotaUsage.GetQuotaSizePercentage}}\n                        <div class=\"{{if .QuotaUsage.IsQuotaSizeLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6\">\n                            <span {{if gt $percentage 0}}data-i18n=\"fs.quota_usage.size_percentage\" data-i18n-options='{ \"val\": \"{{$size}}\", \"percentage\": {{$percentage}} }'{{else}}data-i18n=\"fs.quota_usage.size\" data-i18n-options='{ \"val\": \"{{$size}}\" }'{{end}}></span>\n                        </div>\n                        {{- end}}\n                        {{- if $files}}\n                        {{- $percentage := .QuotaUsage.GetQuotaFilesPercentage}}\n                        <div class=\"{{if .QuotaUsage.IsQuotaFilesLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6\">\n                            <span {{if gt $percentage 0}}data-i18n=\"fs.quota_usage.files_percentage\" data-i18n-options='{ \"val\": \"{{$files}}\", \"percentage\": {{$percentage}} }'{{else}}data-i18n=\"fs.quota_usage.files\" data-i18n-options='{ \"val\": \"{{$files}}\" }'{{end}}></span>\n                        </div>\n                        {{- end}}\n                    </div>\n                </div>\n                {{- end}}\n                {{- if .QuotaUsage.HasTranferQuota}}\n                {{- $total := .QuotaUsage.GetTotalTransferQuota}}\n                {{- $upload := .QuotaUsage.GetUploadTransferQuota}}\n                {{- $download := .QuotaUsage.GetDownloadTransferQuota}}\n                <div class=\"d-flex align-items-center bg-hover-lighten py-3 px-9\">\n                    <div class=\"symbol symbol-40px symbol-circle me-5\">\n                        <span class=\"symbol-label {{ if .QuotaUsage.IsTransferQuotaLow }}bg-light-warning{{end}}\">\n                            <i class=\"ki-duotone ki-arrow-right-left {{ if .QuotaUsage.IsTransferQuotaLow }}text-warning{{end}} fs-1\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                            </i>\n                        </span>\n                    </div>\n                    <div class=\"mb-1 pe-3 flex-grow-1\">\n                        <span data-i18n=\"fs.quota_usage.transfer\" class=\"fs-6 text-gray-900 fw-semibold\">Transfer quota</span>\n                        {{- if $total}}\n                        {{$percentage := .QuotaUsage.GetTotalTransferQuotaPercentage}}\n                        <div class=\"{{if .QuotaUsage.IsTotalTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6\">\n                            <span {{if gt $percentage 0}}data-i18n=\"fs.quota_usage.total_percentage\" data-i18n-options='{ \"val\": \"{{$total}}\", \"percentage\": {{$percentage}} }'{{else}}data-i18n=\"fs.quota_usage.total\" data-i18n-options='{ \"val\": \"{{$total}}\" }'{{end}}>\n                            </span>\n                        </div>\n                        {{- end}}\n                        {{- if $download}}\n                        {{$percentage := .QuotaUsage.GetDownloadTransferQuotaPercentage}}\n                        <div class=\"{{if .QuotaUsage.IsDownloadTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6\">\n                            <span {{if gt $percentage 0}}data-i18n=\"fs.quota_usage.downloads_percentage\" data-i18n-options='{ \"val\": \"{{$download}}\", \"percentage\": {{$percentage}} }'{{else}}data-i18n=\"fs.quota_usage.downloads\" data-i18n-options='{ \"val\": \"{{$download}}\" }'{{end}}>\n                            </span>\n                        </div>\n                        {{- end}}\n                        {{- if $upload}}\n                        {{$percentage := .QuotaUsage.GetUploadTransferQuotaPercentage}}\n                        <div class=\"{{if .QuotaUsage.IsUploadTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6\">\n                            <span {{if gt $percentage 0}}data-i18n=\"fs.quota_usage.uploads_percentage\" data-i18n-options='{ \"val\": \"{{$upload}}\", \"percentage\": {{$percentage}} }'{{else}}data-i18n=\"fs.quota_usage.uploads\" data-i18n-options='{ \"val\": \"{{$upload}}\" }'{{end}}>\n                            </span>\n                        </div>\n                        {{- end}}\n                    </div>\n                </div>\n                {{- end}}\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- end}}\n\n{{- define \"modals\"}}\n<div class=\"modal fade\" tabindex=\"-1\" id=\"modal_upload\">\n    <div class=\"modal-dialog modal-dialog-centered mw-600px\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header border-0\">\n                <h3 data-i18n=\"fs.upload.text\" class=\"modal-title\">Upload files</h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body\">\n                <form id=\"upload_files_form\" action=\"{{.FilesURL}}?path={{.CurrentDir}}\" method=\"POST\" enctype=\"multipart/form-data\">\n                    <div class=\"fv-row\">\n                        <div class=\"dropzone mh-350px overflow-auto visibility-auto\" id=\"upload_files\">\n                            <div class=\"dz-message needsclick align-items-center\">\n                                <i class=\"ki-duotone ki-file-up fs-3x text-primary\"><span class=\"path1\"></span><span class=\"path2\"></span></i>\n                                <div class=\"ms-4\">\n                                    <h3 data-i18n=\"fs.upload.message\" class=\"fs-5 fw-bold text-gray-900 mb-1\">Drop files here or click to upload.</h3>\n                                    <!-- <span class=\"fs-7 fw-semibold text-gray-500\">Upload up to 30 files</span> -->\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </form>\n            </div>\n            <div class=\"modal-footer border-0\">\n                <button data-i18n=\"general.cancel\" type=\"button\" id=\"upload_files_cancel\" class=\"btn btn-light me-5\" data-bs-dismiss=\"modal\">Cancel</button>\n                <button data-i18n=\"general.submit\" type=\"button\" id=\"upload_files_button\" class=\"btn btn-primary\" data-bs-dismiss=\"modal\">Submit</button>\n            </div>\n        </div>\n    </div>\n</div>\n\n<div class=\"modal fade\" tabindex=\"-1\" id=\"modal_video_player\" data-bs-backdrop=\"static\" data-bs-keyboard=\"false\">\n    <div class=\"modal-dialog modal-dialog-centered modal-lg\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header border-0\">\n                <h5 class=\"modal-title\">\n                    <span id=\"video_title\"></span>\n                </h5>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n\n            <div class=\"modal-body\">\n                <video id=\"video_player\" width=\"100%\" height=\"auto\" controls preload=\"metadata\">\n                    <span data-i18n=\"general.html5_media_not_supported\"></span>\n                </video>\n            </div>\n        </div>\n    </div>\n</div>\n\n{{- if not .ShareUploadBaseURL}}\n<div class=\"modal fade\" tabindex=\"-1\" id=\"modal_rename\">\n    <div class=\"modal-dialog modal-dialog-centered\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header border-0\">\n                <h5 class=\"modal-title\">\n                    <span id=\"rename_title\"></span>\n                </h5>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n\n            <div class=\"modal-body\">\n                <input id=\"rename_old_name\" type=\"text\" class=\"d-none\"/>\n                <div class=\"mb-10\">\n                    <label data-i18n=\"fs.rename.new_name\" for=\"rename_new_name\" class=\"form-label\"></label>\n                    <input id=\"rename_new_name\" type=\"text\" class=\"form-control\"/>\n                </div>\n            </div>\n\n            <div class=\"modal-footer border-0\">\n                <button data-i18n=\"general.cancel\" type=\"button\" class=\"btn btn-secondary me-5\" data-bs-dismiss=\"modal\">Cancel</button>\n                <button data-i18n=\"general.confirm\" id=\"id_do_rename_button\" type=\"button\" class=\"btn btn-primary\" data-bs-dismiss=\"modal\">Submit</button>\n            </div>\n        </div>\n    </div>\n</div>\n\n<div class=\"modal fade\" tabindex=\"-1\" id=\"modal_move_or_copy\">\n    <div class=\"modal-dialog modal-dialog-centered modal-lg\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header border-0\">\n                <h3 data-i18n=\"general.choose_target_folder\" class=\"modal-title\">\n                    Choose target folder\n                </h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n\n            <div class=\"modal-body\">\n                <div id=\"errorModalMsg\" class=\"d-none rounded border-warning border border-dashed bg-light-warning d-flex align-items-center p-5 mb-10\">\n                    <i class=\"ki-duotone ki-information-5 fs-3x text-warning me-5\"><span class=\"path1\"></span><span class=\"path2\"></span><span class=\"path3\"></span></i>\n                    <div class=\"text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10\">\n                        <span id=\"errorModalTxt\"></span>\n                    </div>\n                    <button id=\"id_dismiss_error_modal_msg\" type=\"button\" class=\"position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 btn btn-icon btn-sm btn-active-light-primary ms-sm-auto\">\n                        <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                    </button>\n                </div>\n                <div class=\"row\">\n                    <div class=\"col-md-9 align-items-center d-flex flex-stack\">\n                        <div class=\"badge badge-lg badge-light-primary\">\n                            <div id=\"dirs_browser_nav\" class=\"d-flex align-items-center flex-wrap\">\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"col-md-3 align-items-center d-flex justify-content-end\">\n                        <button id=\"id_dir_browser_create_dir\" type=\"button\" class=\"btn btn-flex btn-primary\">\n                            <i class=\"ki-duotone ki-add-folder fs-2\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                            </i>\n                            <span data-i18n=\"general.add\"></span>\n                        </button>\n                    </div>\n                </div>\n                <div class=\"dir_browser_folder_divider py-2 d-none\">\n                    <hr>\n                </div>\n                <div id=\"dirsbrowser_new_folder\" class=\"d-flex align-items-center d-none\">\n                    <span>\n                        <i class=\"ki-duotone ki-folder fs-2x text-primary me-4\">\n                            <span class=\"path1\"></span>\n                            <span class=\"path2\"></span>\n                        </i>\n                    </span>\n                    <input data-i18n=\"[placeholder]fs.create_folder_msg\" id=\"dirsbrowser_new_folder_input\" type=\"text\" name=\"new_folder_name\" placeholder=\"Enter the new folder name\" class=\"form-control mw-250px me-3\" />\n                    <button class=\"btn btn-icon btn-light-primary me-3\" id=\"dirsbrowser_add_folder\">\n                        <span class=\"indicator-label\">\n                            <i class=\"ki-duotone ki-check fs-1\"></i>\n                        </span>\n                        <span class=\"indicator-progress\">\n                            <span class=\"spinner-border spinner-border-sm align-middle\"></span>\n                        </span>\n                    </button>\n                    <button class=\"btn btn-icon btn-light-danger\" id=\"dirsbrowser_cancel_folder\">\n                        <i class=\"ki-solid ki-cross fs-1\"></i>\n                    </button>\n                </div>\n                <div class=\"dir_browser_folder_divider py-2 d-none\">\n                    <hr>\n                </div>\n                <table id=\"dirsbrowser_list\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                    <thead>\n                        <tr class=\"text-start text-muted fw-bold fs-7 text-uppercase gs-0\">\n                            <th></th>\n                            <th data-i18n=\"general.name\" class=\"min-w-250px\">Name</th>\n                        </tr>\n                    </thead>\n                    <tbody id=\"dirsbrowser_list_body\" class=\"text-gray-600 fw-semibold\">\n                    </tbody>\n                </table>\n\n                <div class=\"form-floating d-none\">\n                    <input type=\"text\" class=\"form-control form-control-solid\" id=\"move_copy_source\"/>\n                    <label data-i18n=\"general.source_name\" for=\"move_copy_source\">Source name</label>\n                </div>\n\n                <div class=\"form-floating mb-5 mt-7\">\n                    <input type=\"text\" class=\"form-control form-control-solid\" id=\"move_copy_folder\"/>\n                    <label data-i18n=\"general.target_folder\" for=\"move_copy_folder\">Target folder</label>\n                </div>\n\n                <div class=\"form-floating\" id=\"move_copy_name_container\">\n                    <input type=\"text\" class=\"form-control form-control-solid\" id=\"move_copy_name\"/>\n                    <label data-i18n=\"general.dest_name\" for=\"move_copy_name\">Destination name</label>\n                </div>\n\n            </div>\n            <div class=\"modal-footer border-0\">\n                {{- if .CanCopy }}\n                <button id=\"id_copy_button\" type=\"button\" class=\"btn {{if .CanRename}}btn-light-primary me-5{{else}}btn-primary{{end}}\" data-bs-dismiss=\"modal\">\n                    <span data-i18n=\"fs.copy.msg\">Copy</span>\n                </button>\n                {{- end}}\n                {{- if .CanRename }}\n                <button id=\"id_move_button\" type=\"button\" class=\"btn btn-primary\" data-bs-dismiss=\"modal\">\n                    <span data-i18n=\"fs.move.msg\">Move</span>\n                </button>\n                {{- end}}\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- end}}"
  },
  {
    "path": "templates/webclient/mfa.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n\n{{- if .TOTPConfig.Enabled}}\n<div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5\">\n    <i class=\"ki-duotone ki-shield-tick fs-2tx text-primary me-4\">\n        <span class=\"path1\"></span>\n        <span class=\"path2\"></span>\n    </i>\n    <div class=\"d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap\">\n        <div class=\"mb-3 mb-md-0 fw-semibold\">\n            <h4 class=\"text-gray-900 fw-bold\">\n                <span data-i18n=\"2fa.msg_enabled\"></span> {{- if gt (len .TOTPConfigs) 1 }}\n                ({{$.TOTPConfig.ConfigName}}) {{- end}}\n            </h4>\n            <div class=\"fs-6 text-gray-800 pe-7\">\n                <span data-i18n=\"2fa.msg_info\"></span>\n            </div>\n        </div>\n        <button type=\"button\" id=\"disable_btn\" class=\"btn btn-danger ms-4 px-6 align-self-center text-nowrap\">\n            <span data-i18n=\"general.disable\" class=\"indicator-label\">\n                Disable\n            </span>\n            <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                Please wait...\n                <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n            </span>\n        </button>\n    </div>\n</div>\n{{- end}}\n\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"2fa.title\" class=\"card-title section-title\">Two-factor authentication using Authenticator apps</h3>\n    </div>\n    <div class=\"card-body\">\n        {{- if not .TOTPConfig.Enabled}}\n        <div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5\">\n            <i class=\"ki-duotone ki-shield-tick fs-2tx text-primary me-4\">\n                <span class=\"path1\"></span>\n                <span class=\"path2\"></span>\n            </i>\n            <div class=\"d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap\">\n                <div class=\"mb-3 mb-md-0 fw-semibold\">\n                    <h4 class=\"text-gray-900 fw-bold\">\n                        <span data-i18n=\"2fa.msg_disabled\">Secure Your Account</span>\n                    </h4>\n                    <div class=\"fs-6 text-gray-800 pe-7\">\n                        <span data-i18n=\"2fa.msg_info\"></span>\n                    </div>\n                </div>\n            </div>\n        </div>\n        {{- end}}\n        <div class=\"form-group row mt-10\">\n            <label for=\"id_config\" data-i18n=\"general.configuration\" class=\"col-md-3 col-form-label\">Configuration</label>\n            <div class=\"col-md-9\">\n                <select id=\"id_config\" name=\"config_name\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n                    <option value=\"\">---</option>\n                    {{range .TOTPConfigs}}\n                    <option value=\"{{.}}\" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>\n                    {{end}}\n                </select>\n            </div>\n        </div>\n\n        <div class=\"form-group row mt-10\">\n            <label for=\"id_protocols\" data-i18n=\"2fa.require_for\" class=\"col-md-3 col-form-label required\">\n                Require 2FA for\n            </label>\n            <div class=\"col-md-9\">\n                <select id=\"id_protocols\" name=\"multi_factor_protocols\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" data-close-on-select=\"false\" multiple=\"multiple\">\n                    <option></option>\n                    {{range $protocol := .Protocols}}\n                    <option value=\"{{$protocol}}\" {{range $p :=$.TOTPConfig.Protocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}\n                    </option>\n                    {{end}}\n                </select>\n            </div>\n        </div>\n\n        <div class=\"d-flex justify-content-end mt-15\">\n            {{- if .TOTPConfig.Enabled }}\n            <button type=\"button\" id=\"generate_secret_btn\" class=\"btn btn-light-primary px-10 me-10 d-none\">\n                <span data-i18n=\"2fa.generate\" class=\"indicator-label\">\n                    Generate new secret\n                </span>\n                <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                    Please wait...\n                    <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                </span>\n            </button>\n            {{- end}}\n            <button type=\"button\" id=\"save_btn\" class=\"btn btn-primary px-10 d-none\">\n                <span id=\"save_label\" class=\"indicator-label\"></span>\n                <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                    Please wait...\n                    <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                </span>\n            </button>\n        </div>\n\n    </div>\n</div>\n\n{{- if .TOTPConfig.Enabled}}\n<div class=\"accordion shadow-sm my-10\" id=\"id_accordion\">\n    <div class=\"accordion-item\">\n        <h2 class=\"accordion-header\" id=\"accordion_rec_codes\">\n            <button class=\"accordion-button section-title text-primary collapsed\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#accordion_rec_codes_body\" aria-expanded=\"false\" aria-controls=\"accordion_rec_codes_body\">\n                <span data-i18n=\"2fa.recovery_codes\">\n                    Recovery codes\n                </span>\n            </button>\n        </h2>\n        <div id=\"accordion_rec_codes_body\" class=\"accordion-collapse collapse\" aria-labelledby=\"accordion_rec_codes\" data-bs-parent=\"#id_accordion\">\n            <div class=\"accordion-body\">\n                <div class=\"notice d-flex bg-light-primary rounded border-primary border border-dashed p-6\">\n                    <div class=\"d-flex flex-stack flex-grow-1\">\n                        <div class=\"fs-5 fw-semibold text-break text-wrap text-gray-800\">\n                            <p data-i18n=\"2fa.recovery_codes_msg1\">Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor configuration.</p>\n                            <p data-i18n=\"2fa.recovery_codes_msg2\">To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>\n                            <p data-i18n=\"2fa.recovery_codes_msg3\" class=\"fs-4 fw-bold\">If you generate new recovery codes, you automatically invalidate old ones.</p>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"d-flex justify-content-end mt-10\">\n                    <button type=\"button\" id=\"generate_recovery_code_btn\" class=\"btn btn-light-primary px-10 me-10\">\n                        <span data-i18n=\"2fa.recovery_codes_generate\" class=\"indicator-label\">\n                            Generate\n                        </span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                    <button type=\"button\" id=\"view_recovery_code_btn\" class=\"btn btn-primary px-10\">\n                        <span data-i18n=\"2fa.recovery_codes_view\" id=\"save_label\" class=\"indicator-label\">View</span>\n                        <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                            Please wait...\n                            <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                        </span>\n                    </button>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n{{- end}}\n\n{{- define \"additionalnavitems\"}}\n<div class=\"d-flex align-items-center ms-2 ms-lg-3\" id=\"kt_header_user_menu_toggle\">\n    <div class=\"btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px\" data-bs-toggle=\"modal\" data-bs-target=\"#info_modal\">\n        <i class=\"ki-duotone ki-information-2 fs-2\">\n            <i class=\"path1\"></i>\n            <i class=\"path2\"></i>\n            <i class=\"path3\"></i>\n        </i>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"modals\"}}\n<div class=\"modal fade\" id=\"recovery_codes_modal\" tabindex=\"-1\">\n    <div class=\"modal-dialog\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header\">\n                <h3 id=\"idRecoveryCodesTitle\" class=\"modal-title\"></h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body\">\n                <div id=\"idRecoveryCodesList\" class=\"d-flex flex-column\">\n                </div>\n            </div>\n            <div class=\"modal-footer\">\n                <button data-i18n=\"general.ok\" class=\"btn btn-primary\" type=\"button\" data-bs-dismiss=\"modal\">OK</button>\n            </div>\n        </div>\n    </div>\n</div>\n\n<div class=\"modal fade\" id=\"info_modal\" tabindex=\"-1\">\n    <div class=\"modal-dialog\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header\">\n                <h3 data-i18n=\"2fa.info_title\" class=\"modal-title\">Learn about two-factor authentication</h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body fw-semibold fs-6\">\n                <p data-i18n=\"2fa.info1\">SSH protocol (SFTP/SCP/SSH commands) will ask for the passcode if the client uses keyboard interactive authentication.</p>\n                <p data-i18n=\"2fa.info2\">HTTP protocol means Web UI and REST APIs. Web UI will ask for the passcode using a specific page. For REST API\n                    you have to add the passcode using an HTTP header.</p>\n                <p data-i18n=\"2fa.info3\">FTP has no standard way to support two factor authentication, if you enable the FTP support, you have to add the\n                    TOTP passcode after the password. For example if your password is \"password\" and your one time passcode is\n                    \"123456\" you have to use \"password123456\" as password.</p>\n                <p data-i18n=\"2fa.info4\">WebDAV is not supported since each single request must be authenticated and a passcode cannot be reused.</p>\n            </div>\n            <div class=\"modal-footer\">\n                <button data-i18n=\"general.ok\" class=\"btn btn-primary\" type=\"button\" data-bs-dismiss=\"modal\">OK</button>\n            </div>\n        </div>\n    </div>\n</div>\n\n<div class=\"modal fade\" id=\"qrcode_modal\" tabindex=\"-1\">\n    <div class=\"modal-dialog modal-dialog-centered modal-lg\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header\">\n                <h3 data-i18n=\"2fa.setup_title\" class=\"modal-title\">Set up two-factor authentication</h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body scroll-y pt-10 pb-15 px-lg-17\">\n                <div class=\"text-gray-700 fw-semibold fs-6 mb-10\">\n                    <span data-i18n=\"2fa.setup_msg\">\n                        Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.\n                    </span>\n                </div>\n                <div id=\"id_qr_code_container\" class=\"pt-5 text-center\">\n                </div>\n                <div class=\"notice d-flex bg-light-warning rounded border-warning border border-dashed my-10 p-6\">\n                    <i class=\"ki-duotone ki-information-5 fs-2tx text-warning me-4\">\n                        <span class=\"path1\"></span>\n                        <span class=\"path2\"></span>\n                        <span class=\"path3\"></span>\n                    </i>\n                    <div class=\"d-flex flex-stack flex-grow-1\">\n                        <div class=\"fw-semibold\">\n                            <div class=\"fs-6 text-gray-800\">\n                                <span data-i18n=\"2fa.setup_help\">If you have trouble using the QR code, select manual entry on your app, and enter the code:</span>\n                                <span id=\"id_secret\" class=\"fw-bold text-gray-900 pt-2\"></span>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n\n                <div id=\"errorModalMsg\" class=\"d-none rounded border-warning border border-dashed bg-light-warning d-flex align-items-center p-5 mb-10\">\n                    <i class=\"ki-duotone ki-information-5 fs-3x text-warning me-5\"><span class=\"path1\"></span><span class=\"path2\"></span><span class=\"path3\"></span></i>\n                    <div class=\"text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10\">\n                        <span id=\"errorModalTxt\"></span>\n                    </div>\n                    <button id=\"id_dismiss_error_modal_msg\" type=\"button\" class=\"position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 btn btn-icon btn-sm btn-active-light-primary ms-sm-auto\">\n                        <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                    </button>\n                </div>\n\n                <div class=\"fv-row\">\n                    <input data-i18n=\"[placeholder]login.auth_code\" type=\"text\" id=\"id_passcode\" name=\"passcode\" class=\"form-control form-control-lg form-control-solid\" placeholder=\"Enter authentication code\" spellcheck=\"false\" />\n                </div>\n            </div>\n            <div class=\"modal-footer\">\n                <button data-i18n=\"general.cancel\" type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button>\n                <button type=\"button\" class=\"btn btn-primary ms-6\" id=\"passcode_btn\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    const qrModal = new bootstrap.Modal('#qrcode_modal');\n    const recCodesModal = new bootstrap.Modal('#recovery_codes_modal');\n    const requiredProtocols = [];\n    {{- range .RequiredProtocols}}\n    requiredProtocols.push('{{.}}');\n    {{- end}}\n\n    function onConfigChanged() {\n        let selectedConfig = $('#id_config option:selected').val();\n        if (selectedConfig == \"\"){\n            $('#save_btn').addClass(\"d-none\");\n            $('#generate_secret_btn').addClass(\"d-none\");\n        } else {\n            //{{- if .TOTPConfig.Enabled }}\n            if (selectedConfig == \"{{.TOTPConfig.ConfigName}}\"){\n                $('#save_label').text($.t('general.submit'));\n            } else {\n                $('#save_label').text($.t('general.enable'));\n            }\n            $('#save_btn').removeClass(\"d-none\");\n            $('#generate_secret_btn').removeClass(\"d-none\");\n            //{{- else}}\n            $('#save_btn').removeClass(\"d-none\");\n            $('#save_label').text($.t('general.enable'));\n            $('#generate_secret_btn').addClass(\"d-none\");\n            //{{- end}}\n        }\n    }\n\n    function generateRecoveryCodes() {\n        el = document.querySelector('#generate_recovery_code_btn');\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.post('{{.RecCodesURL}}', null, {\n            \ttimeout: 15000,\n                headers: {\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n       \t    }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                $('#idRecoveryCodesTitle').text($.t('2fa.new_recovery_codes'));\n                let recList = $('#idRecoveryCodesList');\n                recList.empty();\n                $.each(response.data, function(key, item) {\n                    itemCode = escapeHTML(item);\n                    recList.append(`<li class=\"d-flex align-items-center py-2 fw-semibold fs-5 text-gray-800\"><span class=\"bullet bullet-dot me-5\"></span>${itemCode}</li>`);\n                });\n                recCodesModal.show();\n            }).catch(function (error){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                ModalAlert.fire({\n                    text: $.t('2fa.recovery_codes_gen_err'),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            });\n    }\n\n    function viewRecoveryCodes() {\n        el = document.querySelector('#view_recovery_code_btn');\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.get('{{.RecCodesURL}}',{\n            \ttimeout: 15000,\n                headers: {\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n       \t    }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                $('#idRecoveryCodesTitle').text($.t('2fa.recovery_codes'));\n                let recList = $('#idRecoveryCodesList');\n                recList.empty();\n                $.each(response.data, function(key, item) {\n                    itemCode = escapeHTML(item.code);\n                    let txtStyleClass = \"\";\n                    if (item.used) {\n                        txtStyleClass = \"line-through\";\n                    }\n                    recList.append(`<li class=\"d-flex align-items-center py-2 fw-semibold fs-5 text-gray-800 ${txtStyleClass}\"><span class=\"bullet bullet-dot me-5\"></span>${itemCode}</li>`);\n                });\n                recCodesModal.show();\n            }).catch(function (error){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                ModalAlert.fire({\n                    text: $.t('2fa.recovery_codes_get_err'),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            });\n    }\n\n    function disableConfig() {\n        if (requiredProtocols.length > 0){\n            ModalAlert.fire({\n                text: $.t('2fa.required_protocols', {val: requiredProtocols.join(', ')}),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n        ModalAlert.fire({\n            text: $.t('2fa.disable_question'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.confirm'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                doSaveConfig(document.querySelector('#disable_btn'), null, false, true);\n            }\n        });\n    }\n\n    function validatePasscode() {\n        el = document.querySelector('#passcode_btn');\n        let errDivEl = $('#errorModalMsg');\n        let errTxtEl = $('#errorModalTxt');\n        let errorMessage = '2fa.auth_code_invalid';\n        let passcode = $('#id_passcode').val();\n\n        errDivEl.addClass(\"d-none\");\n        if (passcode == \"\") {\n            setI18NData(errTxtEl, '2fa.auth_code_required');\n            errDivEl.removeClass(\"d-none\");\n            return;\n        }\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.post(\"{{.ValidateTOTPURL}}\", {\n                passcode: passcode,\n                config_name: $('#id_config option:selected').val(),\n                secret: $('#id_secret').text()\n            }, {\n                headers: {\n                    timeout: 15000,\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n            }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                if (!response.data.message) {\n                    setI18NData(errTxtEl, errorMessage);\n                    errDivEl.removeClass(\"d-none\");\n                    return;\n                }\n                qrModal.hide();\n                doSaveConfig(null, null, true, false);\n            }).catch(function (error){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                setI18NData(errTxtEl, errorMessage);\n                errDivEl.removeClass(\"d-none\");\n            });\n    }\n\n    function confirmGenerateSecret() {\n        ModalAlert.fire({\n            text: $.t('2fa.generate_question'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.confirm'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                el = document.querySelector('#generate_secret_btn');\n                configName = $('#id_config option:selected').val();\n                generateSecret(el,configName);\n            }\n        });\n    }\n\n    function generateSecret(el, configName) {\n        if (!el || !configName){\n            confirmGenerateSecret();\n            return;\n        }\n\n        if ($('#id_protocols').find('option:selected').length == 0){\n            ModalAlert.fire({\n                text: $.t('2fa.no_protocol'),\n                icon: \"warning\",\n                confirmButtonText: $.t('general.ok'),\n                customClass: {\n                    confirmButton: \"btn btn-primary\"\n                }\n            });\n            return;\n        }\n\n        let errorMessage =  '2fa.auth_secret_gen_err';\n        $('#id_secret').text(\"\");\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.post(\"{{.GenerateTOTPURL}}\", {\n                config_name: configName\n            }, {\n                headers: {\n                    timeout: 15000,\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n            }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                if (!response.data.secret) {\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                    return;\n                }\n                $('#id_secret').text(response.data.secret);\n                $('#errorModalMsg').addClass(\"d-none\");\n                $('#id_passcode').val(\"\");\n                let qrCodeContainer = document.getElementById(\"id_qr_code_container\");\n                clearChilds(qrCodeContainer);\n                let qrCodeImg = document.createElement(\"img\");\n                qrCodeImg.classList.add(\"mw-150px\");\n                qrCodeImg.src = \"{{.MFAURL}}/qrcode?url=\"+encodeURIComponent(response.data.url);\n                qrCodeImg.alt = $.t('general.qr_code');\n                qrCodeContainer.appendChild(qrCodeImg);\n                qrModal.show();\n            }).catch(function (error){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                ModalAlert.fire({\n                    text: $.t(errorMessage),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            });\n    }\n\n    function doSaveConfig(el, configName, full, disabled) {\n        if (!el){\n            el = document.querySelector('#save_btn');\n        }\n        if (!configName) {\n            configName = $('#id_config option:selected').val();\n        }\n        let errorMessage = '2fa.save_err';\n        let protocolsArray = [];\n        if (!disabled) {\n            $('#id_protocols').find('option:selected').each(function(){\n                protocolsArray.push($(this).val());\n            });\n            if (protocolsArray.length == 0){\n                ModalAlert.fire({\n                    text: $.t('2fa.no_protocol'),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n                return;\n            }\n        }\n        for (let i = 0; i < requiredProtocols.length > 0; i++){\n            if (!protocolsArray.includes(requiredProtocols[i])){\n                ModalAlert.fire({\n                    text: $.t('2fa.required_protocols', {val: requiredProtocols.join(', ')}),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n                return;\n            }\n        }\n        let postData = {\n            protocols: protocolsArray\n        };\n        if (full){\n            postData.config_name = configName;\n            postData.secret = {\n                status: \"Plain\",\n                payload: $('#id_secret').text()\n            }\n        }\n        if (disabled){\n            postData.enabled = false;\n        } else {\n            postData.enabled = true;\n        }\n        el.setAttribute('data-kt-indicator', 'on');\n\t\tel.disabled = true;\n\n        axios.post(\"{{.SaveTOTPURL}}\", postData, {\n                headers: {\n                    timeout: 15000,\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                validateStatus: function (status) {\n                    return status == 200;\n                }\n            }).then(function (response){\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                if (!response.data.message) {\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                    return;\n                }\n                ModalAlert.fire({\n                    text: $.t('general.config_saved'),\n                    icon: \"success\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: 'btn btn-primary'\n                    }\n                }).then((result) => {\n                    if (result.isConfirmed){\n                        location.reload();\n                    }\n                });\n            }).catch(function (error) {\n                el.removeAttribute('data-kt-indicator');\n                el.disabled = false;\n                ModalAlert.fire({\n                    text: $.t(errorMessage),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n            });\n\n    }\n\n    function saveConfig() {\n        let selectedConfig = $('#id_config option:selected').val();\n        let saveBtn = document.querySelector('#save_btn');\n        if (selectedConfig == \"{{.TOTPConfig.ConfigName}}\"){\n            doSaveConfig(saveBtn, selectedConfig, false, false);\n            return;\n        }\n        generateSecret(saveBtn, selectedConfig);\n    }\n\n    $(document).on(\"i18nshow\", function(){\n        onConfigChanged();\n\n        var dismissErrorModalBtn = $('#id_dismiss_error_modal_msg');\n        if (dismissErrorModalBtn){\n            dismissErrorModalBtn.on(\"click\",function(){\n                $('#errorModalMsg').addClass(\"d-none\");\n            });\n        }\n\n        var disableBtn = $('#disable_btn');\n        if (disableBtn){\n            disableBtn.on(\"click\", function(){\n                disableConfig();\n            });\n        }\n\n        var generateSecretBtn = $('#generate_secret_btn');\n        if (generateSecretBtn){\n            generateSecretBtn.on(\"click\", function(){\n                generateSecret();\n            });\n        }\n\n        var saveBtn = $('#save_btn');\n        if (saveBtn){\n            saveBtn.on(\"click\", function(){\n                saveConfig();\n            });\n        }\n\n        var generateRecoveryCodeBtn = $('#generate_recovery_code_btn');\n        if (generateRecoveryCodeBtn){\n            generateRecoveryCodeBtn.on(\"click\", function(){\n                generateRecoveryCodes();\n            });\n        }\n\n        var viewRecoveryCodesBtn = $('#view_recovery_code_btn');\n        if (viewRecoveryCodesBtn){\n            viewRecoveryCodesBtn.on(\"click\", function(){\n                viewRecoveryCodes();\n            });\n        }\n\n        var passcodeBtn = $('#passcode_btn');\n        if (passcodeBtn){\n            passcodeBtn.on(\"click\", function() {\n                validatePasscode();\n            });\n        }\n\n        var configSelect = $('#id_config');\n        if (configSelect){\n            configSelect.on(\"change\", function(){\n                onConfigChanged();\n            });\n        }\n\n    });\n\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webclient/profile.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"general.my_profile\" class=\"card-title section-title\">My profile</h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"page_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n            <div class=\"form-group row\">\n                <label for=\"idEmail\" data-i18n=\"general.email\" class=\"col-md-3 col-form-label\">Email</label>\n                <div class=\"col-md-9\">\n                    <input type=\"email\" id=\"idEmail\" name=\"email\" placeholder=\"\" spellcheck=\"false\" value=\"{{.Email}}\" maxlength=\"255\"\n                        autocomplete=\"off\" {{if not .LoggedUser.CanChangeInfo}}{{if .Email}}class=\"form-control-plaintext readonly-input\"{{else}}class=\"form-control\"{{end}} readonly{{else}}class=\"form-control\"{{end}}>\n                </div>\n            </div>\n\n            {{- if .LoggedUser.CanChangeInfo}}\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"user.additional_emails\" class=\"card-title section-title-inner\">Additional emails</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"additional_emails\">\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"additional_emails\">\n                                {{- range $idx, $val := .AdditionalEmails}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-10 mt-3 mt-md-8\">\n                                            <input type=\"email\" class=\"form-control\" placeholder=\"\" name=\"additional_email\" value=\"{{$val}}\" maxlength=\"255\" autocomplete=\"off\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-2 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-10 mt-3 mt-md-8\">\n                                            <input type=\"email\" class=\"form-control\" placeholder=\"\" name=\"additional_email\" value=\"\" maxlength=\"255\" autocomplete=\"off\" spellcheck=\"false\" />\n                                        </div>\n                                        <div class=\"col-md-2 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- else}}\n            <div class=\"form-group row mt-10\">\n                <label for=\"idAdditionalEmails\" data-i18n=\"user.additional_emails\" class=\"col-md-3 col-form-label\">Additional emails</label>\n                <div class=\"col-md-9\">\n                    <input type=\"text\" id=\"idAdditionalEmails\" name=\"description\" placeholder=\"\" value=\"{{.AdditionalEmailsString}}\"\n                        class=\"form-control-plaintext readonly-input\" readonly>\n                </div>\n            </div>\n            {{- end}}\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"idDescription\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <input type=\"text\" id=\"idDescription\" name=\"description\" placeholder=\"\" value=\"{{.Description}}\" maxlength=\"255\"\n                        {{if not .LoggedUser.CanChangeInfo}}{{if .Description}}class=\"form-control-plaintext readonly-input\"{{else}}class=\"form-control\"{{end}} readonly{{else}}class=\"form-control\"{{end}}>\n                </div>\n            </div>\n\n            <div class=\"form-group row align-items-center mt-10\">\n                <label data-i18n=\"general.api_key_auth\" class=\"col-md-3 col-form-label\" for=\"idAllowAPIKeyAuth\">API key authentication</label>\n                <div class=\"col-md-9\">\n                    <div class=\"form-check form-switch form-check-custom form-check-solid\">\n                        <input class=\"form-check-input\" type=\"checkbox\" id=\"idAllowAPIKeyAuth\" name=\"allow_api_key_auth\" {{if not .LoggedUser.CanChangeAPIKeyAuth}}disabled=\"disabled\"{{end}} {{if .AllowAPIKeyAuth}}checked=\"checked\"{{end}}/>\n                        <label data-i18n=\"general.api_key_auth_help\" class=\"form-check-label fw-semibold text-gray-800\" for=\"idAllowAPIKeyAuth\">\n                            Allow to impersonate yourself, in REST API, using an API key\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            {{- if .LoggedUser.CanManagePublicKeys}}\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"general.pub_keys\" class=\"card-title section-title-inner\">Public keys</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"public_keys\">\n                        {{- template \"infomsg-no-mb\" \"general.pub_keys_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"public_keys\">\n                                {{- range $idx, $val := .PublicKeys}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <textarea data-i18n=\"[placeholder]general.pub_key_placeholder\" class=\"form-control\" name=\"public_key\" rows=\"4\"\n                                                placeholder=\"Paste your public key here\">{{$val}}</textarea>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <textarea data-i18n=\"[placeholder]general.pub_key_placeholder\" class=\"form-control\" name=\"public_key\" rows=\"4\"\n                                                placeholder=\"Paste your public key here\"></textarea>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n            {{- if .LoggedUser.CanManageTLSCerts}}\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"user.tls_certs\" class=\"card-title section-title-inner\">TLS certificates</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"tls_certs\">\n                        {{- template \"infomsg-no-mb\" \"user.tls_certs_help\"}}\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"tls_certs\">\n                                {{- range $idx, $val := .TLSCerts}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <textarea data-i18n=\"[placeholder]user.tls_cert_help\" class=\"form-control\" name=\"tls_cert\" rows=\"4\">{{$val}}</textarea>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <textarea data-i18n=\"[placeholder]user.tls_cert_help\" class=\"form-control\" name=\"tls_cert\" rows=\"4\"></textarea>\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            {{- end}}\n            {{if .CanSubmit}}\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n            {{- end}}\n        </form>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n{{- if or .LoggedUser.CanManagePublicKeys .LoggedUser.CanManageTLSCerts}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js\"></script>\n{{- end}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    KTUtil.onDOMContentLoaded(function () {\n        //{{- if or .LoggedUser.CanManagePublicKeys .LoggedUser.CanManageTLSCerts}}\n        //{{- if .LoggedUser.CanManagePublicKeys}}\n        initRepeater('#public_keys');\n        //{{- end}}\n        //{{- if .LoggedUser.CanManageTLSCerts}}\n        initRepeater('#tls_certs');\n        //{{- end}}\n        //{{- if .LoggedUser.CanChangeInfo}}\n        initRepeater('#additional_emails');\n        //{{- end}}\n        initRepeaterItems();\n        //{{- end}}\n\n        //{{if .CanSubmit}}\n        $(\"#page_form\").submit(function (event) {\n            let submitButton = document.querySelector('#form_submit');\n            submitButton.setAttribute('data-kt-indicator', 'on');\n            submitButton.disabled = true;\n        });\n        //{{- end}}\n    });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webclient/share.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 {{if .IsAdd}}data-i18n=\"title.add_share\"{{else}}data-i18n=\"title.update_share\"{{end}} class=\"card-title section-title\"></h3>\n    </div>\n    <div class=\"card-body\">\n        {{- template \"errmsg\" .Error}}\n        <form id=\"share_form\" action=\"{{.CurrentURL}}\" method=\"POST\" autocomplete=\"off\">\n            <div class=\"form-group row\">\n                <label for=\"name\" data-i18n=\"general.name\" class=\"col-md-3 col-form-label\">Name</label>\n                <div class=\"col-md-9\">\n                    <input id=\"name\" type=\"text\" placeholder=\"\" name=\"name\" value=\"{{.Share.Name}}\" maxlength=\"255\" autocomplete=\"off\"\n                        required {{if not .IsAdd}}class=\"form-control-plaintext readonly-input\" readonly{{else}}class=\"form-control\"{{end}} />\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"scope\" data-i18n=\"share.scope\" class=\"col-md-3 col-form-label\">Scope</label>\n                <div class=\"col-md-9\">\n                    <select id=\"scope\" name=\"scope\" class=\"form-select\" data-control=\"i18n-select2\" data-hide-search=\"true\" aria-describedby=\"scopeHelp\">\n                        <option data-i18n=\"share.scope_read\" value=\"1\" {{if eq .Share.Scope 1 }}selected{{end}}>Read</option>\n                        <option data-i18n=\"share.scope_write\" value=\"2\" {{if eq .Share.Scope 2 }}selected{{end}}>Write</option>\n                        <option data-i18n=\"share.scope_read_write\" value=\"3\" {{if eq .Share.Scope 3 }}selected{{end}}>Read/Write</option>\n                    </select>\n                    <div id=\"scopeHelp\" data-i18n=\"share.scope_help\" class=\"form-text\">\n                        For scope \"Write\" and \"Read/Write\" you have to define one path and it must be a directory\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"card mt-10\">\n                <div class=\"card-header bg-light\">\n                    <h3 data-i18n=\"general.paths\" class=\"card-title section-title-inner\">Paths</h3>\n                </div>\n                <div class=\"card-body\">\n                    <div id=\"paths\">\n                        <div class=\"form-group\">\n                            <div data-repeater-list=\"paths\">\n                                {{- range $idx, $val := .Share.Paths}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]share.path_help\" type=\"text\" class=\"form-control\"\n                                                name=\"path\" value=\"{{$val}}\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- else}}\n                                <div data-repeater-item>\n                                    <div class=\"form-group row\">\n                                        <div class=\"col-md-9 mt-3 mt-md-8\">\n                                            <input data-i18n=\"[placeholder]share.path_help\" type=\"text\" class=\"form-control\"\n                                                name=\"path\" value=\"\" />\n                                        </div>\n                                        <div class=\"col-md-3 mt-3 mt-md-8\">\n                                            <a href=\"#\" data-repeater-delete\n                                                class=\"btn btn-light-danger\">\n                                                <i class=\"ki-duotone ki-trash fs-5\">\n                                                    <span class=\"path1\"></span>\n                                                    <span class=\"path2\"></span>\n                                                    <span class=\"path3\"></span>\n                                                    <span class=\"path4\"></span>\n                                                    <span class=\"path5\"></span>\n                                                </i>\n                                                <span data-i18n=\"general.delete\">Delete</span>\n                                            </a>\n                                        </div>\n                                    </div>\n                                </div>\n                                {{- end}}\n                            </div>\n                        </div>\n\n                        <div class=\"form-group mt-5\">\n                            <a href=\"#\" data-repeater-create class=\"btn btn-light-primary\">\n                                <i class=\"ki-duotone ki-plus fs-3\"></i>\n                                <span data-i18n=\"general.add\">Add</span>\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"password\" data-i18n=\"login.password\" class=\"col-md-3 col-form-label\">Password</label>\n                <div class=\"col-md-9\">\n                    <input id=\"password\" type=\"password\" class=\"form-control\" name=\"password\" autocomplete=\"new-password\"\n                        placeholder=\"\" spellcheck=\"false\" value=\"{{.Share.Password}}\" aria-describedby=\"passwordHelp\" />\n                    <div id=\"passwordHelp\" data-i18n=\"share.password_help\" class=\"form-text\">\n                        If set the share will be password-protected\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"id_expiration\" data-i18n=\"general.expiration\" class=\"col-md-3 col-form-label\">Expiration</label>\n                <div class=\"col-md-9 d-flex\">\n                    <input data-i18n=\"[placeholder]general.expiration_help\" id=\"id_expiration\" class=\"form-control\" placeholder=\"Pick an expiration date\" />\n                    <button class=\"btn btn-icon btn-light-danger ms-2 d-none\" id=\"id_expiration_clear\">\n                        <i class=\"ki-solid ki-cross fs-1\"></i>\n                    </button>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"max_tokens\" data-i18n=\"share.max_tokens\" class=\"col-md-3 col-form-label\">Max tokens</label>\n                <div class=\"col-md-9\">\n                    <input id=\"max_tokens\" type=\"number\" min=\"0\" class=\"form-control\" name=\"max_tokens\" value=\"{{.Share.MaxTokens}}\" aria-describedby=\"max_tokens_help\" />\n                    <div id=\"max_tokens_help\" data-i18n=\"share.max_tokens_help\" class=\"form-text\">\n                        Maximum number of times this share can be accessed. 0 means no limit\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"allowed_ip\" data-i18n=\"general.allowed_ip_mask\" class=\"col-md-3 col-form-label\">Allowed IP/Mask</label>\n                <div class=\"col-md-9\">\n                    <textarea id=\"allowed_ip\" class=\"form-control\" name=\"allowed_ip\" rows=\"3\" aria-describedby=\"allowed_ip_help\"\n                        placeholder=\"\">{{.Share.GetAllowedFromAsString}}</textarea>\n                    <div id=\"allowed_ip_help\" data-i18n=\"general.ip_mask_help\" class=\"form-text\">\n                        Comma separated IP/Mask in CIDR format, for example \"192.168.1.0/24,10.8.0.100/32\"\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"form-group row mt-10\">\n                <label for=\"description\" data-i18n=\"general.description\" class=\"col-md-3 col-form-label\">Description</label>\n                <div class=\"col-md-9\">\n                    <textarea id=\"description\" class=\"form-control\" name=\"description\" rows=\"3\"\n                        placeholder=\"\">{{.Share.Description}}</textarea>\n                </div>\n            </div>\n\n            <div class=\"d-flex justify-content-end mt-12\">\n                <input type=\"hidden\" name=\"expiration_date\" id=\"hidden_start_datetime\" value=\"\">\n                <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n                <button type=\"submit\" id=\"form_submit\" class=\"btn btn-primary px-10\">\n                    <span data-i18n=\"general.submit\" class=\"indicator-label\">\n                        Submit\n                    </span>\n                    <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                        Please wait...\n                        <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n                    </span>\n                </button>\n            </div>\n        </form>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/it.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/de.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/fr.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/zh.js\"></script>\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/flatpickr/l10n/es.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n    $(document).on(\"i18nshow\", function(){\n            initRepeater('#paths');\n            initRepeaterItems();\n\n            const picker = $('#id_expiration').flatpickr({\n                enableTime: false,\n                time_24hr: true,\n                formatDate: (date, format, locale) => {\n                    return $.t('general.datetime', {\n                                val: new Date(date),\n                                formatParams: {\n                                    val: { year: 'numeric', month: 'numeric', day: 'numeric' },\n                                }\n                            });\n                },\n                defaultHour: 23,\n                defaultMinute: 59,\n                locale: flatpickrLocale(),\n                onChange: function(selectedDates, dateStr, instance) {\n                    if (selectedDates.length > 0){\n                        $('#id_expiration_clear').removeClass(\"d-none\");\n                    } else {\n                        $('#id_expiration_clear').addClass(\"d-none\");\n                    }\n                }\n            });\n            //{{ if gt .Share.ExpiresAt 0 }}\n            let input_dt = moment('{{.Share.ExpiresAt }}', 'x').format('YYYY-MM-DD');\n            picker.setDate(input_dt, true);\n            //{{ end }}\n\n            $('#id_expiration_clear').on(\"click\", function(e){\n                e.preventDefault();\n                picker.clear();\n            });\n\n            $(\"#share_form\").submit(function (event) {\n                $('#hidden_start_datetime').val(\"\");\n                let dt = picker.selectedDates;\n                if (dt.length > 0) {\n                    let d = dt[0];\n                    if (d) {\n                        let dateString = moment.utc(d).format('YYYY-MM-DD HH:mm:ss');\n                        $('#hidden_start_datetime').val(dateString);\n                    }\n                }\n                let submitButton = document.querySelector('#form_submit');\n                submitButton.setAttribute('data-kt-indicator', 'on');\n                submitButton.disabled = true;\n            });\n        });\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webclient/sharedownload.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20\">\n    <div class=\"mb-12\">\n        <span>\n            <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n        </span>\n        <span class=\"text-gray-900 fs-1 fw-bold ms-3 ps-5\">\n\t\t    {{.Branding.ShortName}}\n        </span>\n    </div>\n    <div class=\"card shadow-sm w-lg-600px w-md-450px\">\n        <div class=\"card-header bg-light\">\n            <h3 data-i18n=\"fs.download_ready\" class=\"card-title section-title\">Your download is ready</h3>\n            {{- if .LogoutURL}}\n            <div class=\"card-toolbar\">\n                <a id=\"id_logout_link\" href=\"#\" class=\"btn btn-light-primary\">\n                    <span data-i18n=\"login.signout\">Sign out</span>\n                </a>\n            </div>\n            {{- end}}\n        </div>\n        <div class=\"card-body\">\n            <div>\n                <a href=\"{{.DownloadLink}}\" class=\"btn btn-primary btn-block\">\n                    <i class=\"ki-duotone ki-folder-down fs-2\">\n                        <span class=\"path1\"></span>\n                        <span class=\"path2\"></span>\n                    </i>\n                    <span data-i18n=\"fs.download\">Download</span>\n                </a>\n            </div>\n        </div>\n    </div>\n</div>\n{{- end}}\n"
  },
  {
    "path": "templates/webclient/sharelogin.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"baselogin\" .}}\n\n{{- define \"content\"}}\n<form class=\"form w-100\" id=\"sign_in_form\" action=\"{{.CurrentURL}}\" method=\"POST\">\n    <div class=\"container mb-10\">\n        <div class=\"row align-items-center\">\n            <div class=\"col-5 align-items-center\">\n                <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px h-md-90px h-lg-100px\" />\n            </div>\n            <div class=\"col-7\">\n                <h1 class=\"text-gray-900 mb-3 ms-3\">\n                    {{.Branding.ShortName}}\n                </h1>\n            </div>\n        </div>\n    </div>\n    {{- template \"errmsg\" .Error}}\n    <div class=\"fv-row mb-10\">\n        <div class=\"position-relative\" data-password-control=\"container\">\n            <input data-i18n=\"[placeholder]login.password\" data-password-control=\"input\" class=\"form-control form-control-lg form-control-solid\"\n                type=\"password\" name=\"share_password\" placeholder=\"Password\" spellcheck=\"false\" required />\n            <span class=\"btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2\" data-password-control=\"visibility\">\n                <i class=\"ki-duotone ki-eye-slash fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                    <span class=\"path4\"></span>\n                </i>\n                <i class=\"ki-duotone ki-eye d-none fs-1\">\n                    <span class=\"path1\"></span>\n                    <span class=\"path2\"></span>\n                    <span class=\"path3\"></span>\n                </i>\n            </span>\n        </div>\n    </div>\n    <div class=\"text-center\">\n        <input type=\"hidden\" name=\"_form_token\" value=\"{{.CSRFToken}}\">\n        <button type=\"submit\" id=\"sign_in_submit\" class=\"btn btn-lg btn-primary w-100 mb-5\">\n            <span data-i18n=\"login.signin\" class=\"indicator-label\">Sign in</span>\n            <span data-i18n=\"general.wait\" class=\"indicator-progress\">\n                Please wait...\n                <span class=\"spinner-border spinner-border-sm align-middle ms-2\"></span>\n            </span>\n        </button>\n    </div>\n</form>\n{{- if or (gt (len .Languages) 1) (and .Branding.DisclaimerName .Branding.DisclaimerPath)}}\n<hr>\n{{- end}}\n<div class=\" d-flex flex-stack pt-5 mt-5\">\n    <div class=\"me-10\">\n        <select id=\"languageSwitcher\" name=\"language\" class=\"form-select form-select-solid form-select-sm\" data-control=\"i18n-select2\" data-hide-search=\"true\">\n        </select>\n    </div>\n    <div class=\"d-flex fw-semibold text-primary\">\n        {{- if and .Branding.DisclaimerName .Branding.DisclaimerPath}}\n        <a href=\"{{.Branding.DisclaimerPath}}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"px-2\">\n            <span data-i18n=\"custom.disclaimer_webclient\">{{.Branding.DisclaimerName}}</span>\n        </a>\n        {{- end}}\n    </div>\n</div>\n{{- end}}"
  },
  {
    "path": "templates/webclient/shares.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"base\" .}}\n\n{{- define \"extra_css\"}}\n<link href=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css\" rel=\"stylesheet\" type=\"text/css\"/>\n{{- end}}\n\n{{- define \"page_body\"}}\n{{- template \"errmsg\" \"\"}}\n<div class=\"card shadow-sm\">\n    <div class=\"card-header bg-light\">\n        <h3 data-i18n=\"share.view_manage\" class=\"card-title section-title\">View and manage shares</h3>\n    </div>\n    <div id=\"card_body\" class=\"card-body\">\n        <div id=\"loader\" class=\"align-items-center text-center my-10\">\n            <span class=\"spinner-border w-15px h-15px text-muted align-middle me-2\"></span>\n            <span data-i18n=\"general.loading\" class=\"text-gray-700\">Loading...</span>\n        </div>\n        <div id=\"card_content\" class=\"d-none\">\n            <div class=\"d-flex flex-stack flex-wrap mb-5\">\n                <div class=\"d-flex align-items-center position-relative my-2\">\n                    <i class=\"ki-solid ki-magnifier fs-1 position-absolute ms-6\"></i>\n                    <input name=\"search\" data-i18n=\"[placeholder]general.search\" type=\"text\" data-table-filter=\"search\"\n                        class=\"form-control rounded-1 w-250px ps-15 me-5\" placeholder=\"Search\" />\n                </div>\n                <div class=\"d-flex justify-content-end my-2\" data-table-toolbar=\"base\">\n                    <a href=\"{{.ShareURL}}\" class=\"btn btn-primary\">\n                        <i class=\"ki-duotone ki-plus fs-2\"></i>\n                        <span data-i18n=\"general.add\">Add</span>\n                    </a>\n                </div>\n            </div>\n\n            <table id=\"dataTable\" class=\"table align-middle table-row-dashed fs-6 gy-5\">\n                <thead>\n                    <tr class=\"text-start text-muted fw-bold fs-6 gs-0\">\n                        <th data-i18n=\"general.name\">Name</th>\n                        <th data-i18n=\"share.scope\">Scope</th>\n                        <th data-i18n=\"general.info\">Info</th>\n                        <th class=\"min-w-100px\"></th>\n                    </tr>\n                </thead>\n                <tbody id=\"table_body\" class=\"text-gray-800 fw-semibold\"></tbody>\n            </table>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"modals\"}}\n<div class=\"modal fade\" id=\"link_modal\" tabindex=\"-1\">\n    <div class=\"modal-dialog\" role=\"document\">\n        <div class=\"modal-content\">\n            <div class=\"modal-header border-0\">\n                <h3 data-i18n=\"share.access_links_title\" class=\"modal-title\">\n                    Share access links\n                </h3>\n                <div data-i18n=\"[aria-label]general.close\" class=\"btn btn-icon btn-sm btn-active-light-primary\" data-bs-dismiss=\"modal\" aria-label=\"Close\">\n                    <i class=\"ki-solid ki-cross fs-2x text-gray-700\"></i>\n                </div>\n            </div>\n            <div class=\"modal-body fs-5\">\n                <div id=\"readShare\" class=\"mb-5\">\n                    <div class=\"mb-3\">\n                        <h4 data-i18n=\"share.link_single_title\">Single zip file</h4>\n                        <p data-i18n=\"share.link_single_desc\">You can download shared content as a single zip file</p>\n                        <div class=\"d-flex\">\n                            <button id=\"readLinkCopy\" type=\"button\" class=\"btn btn-flex btn-light-primary btn-clipboard-copy me-3\">\n                                <i class=\"ki-duotone ki-fasten fs-2\">\n                                    <span class=\"path1\"></span>\n                                    <span class=\"path2\"></span>\n                                </i>\n                                <span data-i18n=\"general.copy_link\">Copy link</span>\n                            </button>\n                            <a id=\"readLink\" href=\"#\" target=\"_blank\" rel=\"noopener noreferrer\" type=\"button\" class=\"btn btn-flex btn-primary\">\n                                <i class=\"ki-duotone ki-folder-down fs-2\">\n                                    <span class=\"path1\"></span>\n                                    <span class=\"path2\"></span>\n                                </i>\n                                <span data-i18n=\"fs.download\">Download</span>\n                            </a>\n                        </div>\n                    </div>\n                    <hr>\n                    <div class=\"mb-3 mt-10\">\n                        <h4 data-i18n=\"share.link_dir_title\">Single directory</h4>\n                        <p data-i18n=\"share.link_dir_desc\">If the share consists of a single directory you can browse and download files</p>\n                        <button id=\"readBrowseLinkCopy\" data-clipboard-target=\"#readBrowseLink\" type=\"button\" class=\"btn btn-flex btn-light-primary btn-clipboard-copy me-3\">\n                            <i class=\"ki-duotone ki-fasten fs-2\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                            </i>\n                            <span data-i18n=\"general.copy_link\">Copy link</span>\n                        </button>\n                        <a id=\"readBrowseLink\" href=\"#\" target=\"_blank\" rel=\"noopener noreferrer\" type=\"button\" class=\"btn btn-flex btn-primary\">\n                            <i class=\"ki-duotone ki-arrow-up-right fs-2\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                            </i>\n                            <span data-i18n=\"share.go\">Go to share</span>\n                        </a>\n                    </div>\n                    <hr>\n                    <div class=\"mt-10\">\n                        <h4 data-i18n=\"share.link_uncompressed_title\">Uncompressed file</h4>\n                        <p data-i18n=\"share.link_uncompressed_desc\">If the share consists of a single file you can download it uncompressed</p>\n                        <button id=\"readUncompressedLinkCopy\"  data-clipboard-target=\"#readUncompressedLink\" type=\"button\" class=\"btn btn-flex btn-light-primary btn-clipboard-copy me-3\">\n                            <i class=\"ki-duotone ki-fasten fs-2\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                            </i>\n                            <span data-i18n=\"general.copy_link\">Copy link</span>\n                        </button>\n                        <a id=\"readUncompressedLink\" href=\"#\" target=\"_blank\" rel=\"noopener noreferrer\" type=\"button\" class=\"btn btn-flex btn-primary\">\n                            <i class=\"ki-duotone ki-folder-down fs-2\">\n                                <span class=\"path1\"></span>\n                                <span class=\"path2\"></span>\n                            </i>\n                            <span data-i18n=\"fs.download\">Download</span>\n                        </a>\n                    </div>\n                </div>\n                <div id=\"writeShare\" class=\"mb-5\">\n                    <p data-i18n=\"share.upload_desc\">You can upload one or more files to the shared directory</p>\n                    <button id=\"writePageLinkCopy\"  data-clipboard-target=\"#writePageLink\" type=\"button\" class=\"btn btn-flex btn-light-primary btn-clipboard-copy me-3\">\n                        <i class=\"ki-duotone ki-fasten fs-2\">\n                            <span class=\"path1\"></span>\n                            <span class=\"path2\"></span>\n                        </i>\n                        <span data-i18n=\"general.copy_link\">Copy link</span>\n                    </button>\n                    <a id=\"writePageLink\" href=\"#\" target=\"_blank\" rel=\"noopener noreferrer\" type=\"button\" class=\"btn btn-flex btn-primary\">\n                        <i class=\"ki-duotone ki-folder-up fs-2\">\n                            <span class=\"path1\"></span>\n                            <span class=\"path2\"></span>\n                        </i>\n                        <span data-i18n=\"fs.upload.text\">Upload</span>\n                    </a>\n                </div>\n                <div data-i18n=\"share.expired_desc\" id=\"expiredShare\" class=\"fw-semibold fs-4 mb-5\">\n                    This share is no longer accessible because it has expired\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n{{end}}\n\n{{- define \"extra_js\"}}\n<script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js\"></script>\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    function deleteAction(shareID) {\n        ModalAlert.fire({\n            text: $.t('general.delete_confirm_generic'),\n            icon: \"warning\",\n            confirmButtonText: $.t('general.delete_confirm_btn'),\n            cancelButtonText: $.t('general.cancel'),\n            customClass: {\n                confirmButton: \"btn btn-danger\",\n                cancelButton: 'btn btn-secondary'\n            }\n        }).then((result) => {\n            if (result.isConfirmed){\n                clearLoading();\n                KTApp.showPageLoading();\n                let path = '{{.ShareURL}}' + \"/\" + encodeURIComponent(shareID);\n\n                axios.delete(path, {\n                    timeout: 15000,\n                    headers: {\n                        'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                    },\n                    validateStatus: function (status) {\n                        return status == 200;\n                    }\n                }).then(function(response){\n                    location.reload();\n                }).catch(function(error){\n                    KTApp.hidePageLoading();\n                    let errorMessage;\n                    if (error && error.response) {\n                        switch (error.response.status) {\n                            case 403:\n                                errorMessage = \"general.delete_error_403\";\n                                break;\n                            case 404:\n                                errorMessage = \"general.delete_error_404\";\n                                break;\n                        }\n                    }\n                    if (!errorMessage){\n                        errorMessage = \"general.delete_error_generic\";\n                    }\n                    ModalAlert.fire({\n                        text: $.t(errorMessage),\n                        icon: \"warning\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: \"btn btn-primary\"\n                        }\n                    });\n                });\n            }\n        });\n    }\n\n    function editAction(shareID) {\n        window.location.replace('{{.ShareURL}}' + \"/\" + encodeURIComponent(shareID));\n    }\n\n    function showShareLink(shareID, shareScope, expiresAt) {\n        if (expiresAt < Date.now()) {\n            $('#expiredShare').show();\n            $('#writeShare').hide();\n            $('#readShare').hide();\n        } else {\n            let baseURL;\n            //{{- if .BaseURL}}\n            baseURL = '{{.BaseURL}}';\n            //{{- else}}\n            baseURL = getCurrentURI();\n            //{{- end}}\n            let shareURL = '{{.BasePublicSharesURL}}' + \"/\" + encodeURIComponent(shareID);\n            if (shareScope == '1') {\n                $('#expiredShare').hide();\n                $('#writeShare').hide();\n                $('#readShare').show();\n                $('#readLink').attr(\"href\", shareURL + \"/download\");\n                $('#readLink').attr(\"title\", shareURL + \"/download\");\n                $('#readLinkCopy').attr(\"data-clipboard-text\",baseURL+shareURL + \"/download\");\n                $('#readUncompressedLink').attr(\"href\", shareURL + \"/download?compress=false\");\n                $('#readUncompressedLink').attr(\"title\", shareURL + \"/download?compress=false\");\n                $('#readUncompressedLinkCopy').attr(\"data-clipboard-text\",baseURL+shareURL + \"/download?compress=false\");\n                $('#readBrowseLink').attr(\"href\", shareURL + \"/browse\");\n                $('#readBrowseLink').attr(\"title\", shareURL + \"/browse\");\n                $('#readBrowseLinkCopy').attr(\"data-clipboard-text\",baseURL+shareURL + \"/browse\");\n            } else {\n                $('#expiredShare').hide();\n                $('#writeShare').show();\n                $('#readShare').hide();\n                $('#writePageLink').attr(\"href\", shareURL + \"/upload\");\n                $('#writePageLink').attr(\"title\", shareURL + \"/upload\");\n                $('#writePageLinkCopy').attr(\"data-clipboard-text\",baseURL+shareURL + \"/upload\");\n            }\n        }\n        $('#link_modal').modal('show');\n    }\n\n    var sharesDatatable = function(){\n        var dt;\n\n        var initDatatable = function () {\n            dt = $('#dataTable').DataTable({\n                ajax: {\n                    url: \"{{.SharesURL}}/json\",\n                    dataSrc: \"\",\n                    error: function ($xhr, textStatus, errorThrown) {\n                        $(\".dt-processing\").hide();\n                        $('#loader').addClass(\"d-none\");\n                        let txt = \"\";\n                        if ($xhr) {\n                            let json = $xhr.responseJSON;\n                            if (json) {\n                                if (json.message){\n                                    txt = json.message;\n                                }\n                            }\n                        }\n                        if (!txt){\n                            txt = \"general.error500\";\n                        }\n                        setI18NData($('#errorTxt'), txt);\n                        $('#errorMsg').removeClass(\"d-none\");\n                    }\n                },\n                columns: [\n                    {\n                        data: \"name\",\n                        render: function(data, type, row) {\n                            if (type === 'display') {\n                                return escapeHTML(data);\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"scope\",\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                switch (data){\n                                    case 2:\n                                        return $.t('share.scope_write');\n                                    case 3:\n                                        return $.t('share.scope_read_write');\n                                    default:\n                                        return $.t('share.scope_read');\n                                }\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"expires_at\",\n                        defaultContent: 0,\n                        searchable: false,\n                        orderable: false,\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                let info = \"\";\n                                if (row.expires_at && row.expires_at > 0){\n                                    info+= $.t('share.expiration_date', {\n                                        val: row.expires_at,\n                                        formatParams: {\n                                            val: { year: 'numeric', month: 'numeric', day: 'numeric' },\n                                        }\n                                    });\n                                }\n                                if (row.last_use_at && row.last_use_at > 0){\n                                    info+= $.t('share.last_use', {\n                                        val: row.last_use_at,\n                                        formatParams: {\n                                            val: { year: 'numeric', month: 'numeric', day: 'numeric' },\n                                        }\n                                    });\n                                }\n                                let used_tokens = 0;\n                                if (row.used_tokens && row.used_tokens > 0){\n                                    used_tokens = row.used_tokens;\n                                }\n                                if (row.max_tokens && row.max_tokens > 0){\n                                    info+= $.t('share.usage', {used: used_tokens, total: row.max_tokens});\n                                } else {\n                                    info+= $.t('share.used_tokens', {used: used_tokens});\n                                }\n                                if (row.password){\n                                    info+= $.t('share.password_protected')\n                                }\n                                return info;\n                            }\n                            return data;\n                        }\n                    },\n                    {\n                        data: \"id\",\n                        searchable: false,\n                        orderable: false,\n                        className: 'text-end',\n                        render: function (data, type, row) {\n                            if (type === 'display') {\n                                return `<div class=\"d-flex justify-content-end\">\n                                    <div class=\"ms-2\">\n                                        <a href=\"#\" class=\"btn btn-sm btn-icon btn-light btn-active-light-primary\" data-table-action=\"show_link\">\n                                            <i class=\"ki-duotone ki-fasten fs-5 m-0\">\n                                                <span class=\"path1\"></span>\n                                                <span class=\"path2\"></span>\n                                            </i>\n                                        </a>\n                                    </div>\n                                    <div class=\"ms-2\">\n                                        <button type=\"button\" class=\"btn btn-sm btn-icon btn-light btn-active-light-primary\"\n                                            data-kt-menu-trigger=\"click\" data-kt-menu-placement=\"bottom-end\">\n                                            <i class=\"ki-duotone ki-dots-square fs-5 m-0\">\n                                                <span class=\"path1\"></span>\n                                                <span class=\"path2\"></span>\n                                                <span class=\"path3\"></span>\n                                                <span class=\"path4\"></span>\n                                            </i>\n                                        </button>\n                                        <div class=\"menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-150px py-4\" data-kt-menu=\"true\">\n                                            <div class=\"menu-item px-3\">\n                                                <a data-i18n=\"general.edit\" href=\"#\" class=\"menu-link px-3\" data-table-action=\"edit_row\">Edit</a>\n                                            </div>\n                                            <div class=\"menu-item px-3\">\n                                                <a data-i18n=\"general.delete\" href=\"#\" class=\"menu-link text-danger px-3\" data-table-action=\"delete_row\">Delete</a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>`;\n                            }\n                            return \"\";\n                        }\n                    }\n                ],\n                deferRender: true,\n                stateSave: true,\n                stateDuration: 0,\n                stateLoadParams: function (settings, data) {\n                        if (data.search.search){\n                            const filterSearch = document.querySelector('[data-table-filter=\"search\"]');\n                            filterSearch.value = data.search.search;\n                        }\n                    },\n                language: {\n                    info: $.t('datatable.info'),\n                    infoEmpty: $.t('datatable.info_empty'),\n                    infoFiltered: $.t('datatable.info_filtered'),\n                    loadingRecords: \"\",\n                    processing: $.t('datatable.processing'),\n                    zeroRecords: \"\",\n                    emptyTable: $.t('share.no_share')\n                },\n                order: [[1, 'asc']],\n                initComplete: function(settings, json) {\n                    $('#loader').addClass(\"d-none\");\n                    $('#card_content').removeClass(\"d-none\");\n                    let api = $.fn.dataTable.Api(settings);\n                    api.columns.adjust().draw(\"page\");\n                }\n            });\n\n            dt.on('draw.dt', drawAction);\n        }\n\n        function drawAction() {\n            KTMenu.createInstances();\n            handleRowActions();\n            $('#table_body').localize();\n        }\n\n        var handleSearchDatatable = function () {\n            const filterSearch = $(document.querySelector('[data-table-filter=\"search\"]'));\n            filterSearch.off(\"keyup\");\n            filterSearch.on('keyup', function (e) {\n                dt.rows().deselect();\n                dt.search(e.target.value).draw();\n            });\n        }\n\n        function handleRowActions() {\n            const editButtons = document.querySelectorAll('[data-table-action=\"edit_row\"]');\n\n            editButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    editAction(dt.row(parent).data()[\"id\"]);\n                });\n            });\n\n            const deleteButtons = document.querySelectorAll('[data-table-action=\"delete_row\"]');\n\n            deleteButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    const parent = e.target.closest('tr');\n                    deleteAction(dt.row(parent).data()[\"id\"]);\n                });\n            });\n\n            const showLinkButtons = document.querySelectorAll('[data-table-action=\"show_link\"]');\n\n            showLinkButtons.forEach(d => {\n                let el = $(d);\n                el.off(\"click\");\n                el.on(\"click\", function(e){\n                    e.preventDefault();\n                    let rowData = dt.row(e.target.closest('tr')).data();\n                    showShareLink(rowData[\"id\"], rowData[\"scope\"], rowData[\"expires_at\"]);\n                });\n            });\n        }\n\n        return {\n            init: function () {\n                initDatatable();\n                handleSearchDatatable();\n            }\n        }\n    }();\n\n    $(document).on(\"i18nshow\", function(){\n        sharesDatatable.init();\n\n        var clipboard = new ClipboardJS('.btn-clipboard-copy',{\n            container: document.getElementById('link_modal')\n        });\n\n        clipboard.on('success', function (e) {\n            e.trigger.querySelectorAll('span').forEach(spanEl => {\n                if (spanEl.getAttribute('data-i18n')){\n                    e.trigger.classList.remove(\"btn-light-primary\");\n                    e.trigger.classList.add(\"btn-success\");\n                    setI18NData($(spanEl),\"general.copied\");\n                    setTimeout(function(){\n                        e.trigger.classList.remove(\"btn-success\");\n                        e.trigger.classList.add(\"btn-light-primary\");\n                        setI18NData($(spanEl),\"general.copy_link\");\n                    }, 3000)\n                }\n            });\n            e.clearSelection();\n        });\n\n    });\n</script>\n{{end}}"
  },
  {
    "path": "templates/webclient/shareupload.html",
    "content": "<!--\nCopyright (C) 2023 Nicola Murino\n\nThis WebUI uses the KeenThemes Bootstrap Templates:\n\nhttps://keenthemes.com/bootstrap-templates\n\nKeenThemes HTML/CSS/JS components are allowed for use only within the\nSFTPGo product and restricted to be used in a resealable HTML template\nthat can compete with KeenThemes products anyhow.\n\nThis WebUI is allowed for use only within the SFTPGo product and\ntherefore cannot be used in derivative works/products without an\nexplicit grant from the SFTPGo Team (support@sftpgo.com).\n-->\n{{- template \"base\" .}}\n\n{{- define \"page_body\"}}\n<div class=\"d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20\">\n    <div class=\"mb-12\">\n        <span>\n            <img alt=\"Logo\" src=\"{{.StaticURL}}{{.Branding.LogoPath}}\" class=\"h-80px\" />\n        </span>\n        <span class=\"text-gray-800 fs-2 fw-semibold ps-5\">\n\t\t    {{.Branding.ShortName}}\n        </span>\n    </div>\n    <div class=\"card shadow-sm w-lg-600px w-md-450px\">\n        <div class=\"card-header bg-light\">\n            <h3 data-i18n=\"title.upload_to_share\" class=\"card-title section-title\">Upload one or more files to share</h3>\n            {{- if .LogoutURL}}\n            <div class=\"card-toolbar\">\n                <a id=\"id_logout_link\" href=\"#\" class=\"btn btn-light-primary\">\n                    <span data-i18n=\"login.signout\">Sign out</span>\n                </a>\n            </div>\n            {{- end}}\n        </div>\n        <div class=\"card-body\">\n            {{- template \"errmsg\" \"\"}}\n            <form id=\"upload_files_form\" action=\"{{.UploadBasePath}}\" method=\"POST\" enctype=\"multipart/form-data\">\n                <div class=\"fv-row\">\n                    <div class=\"dropzone mh-350px overflow-auto visibility-auto\" id=\"upload_files\">\n                        <div class=\"dz-message needsclick align-items-center\">\n                            <i class=\"ki-duotone ki-file-up fs-3x text-primary\"><span class=\"path1\"></span><span class=\"path2\"></span></i>\n                            <div class=\"ms-4\">\n                                <h3 data-i18n=\"fs.upload.message\" class=\"fs-5 fw-bold text-gray-900 mb-1\">Drop files here or click to upload.</h3>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"d-flex justify-content-end mt-10\">\n                    <button data-i18n=\"general.submit\" type=\"button\" id=\"upload_files_button\" class=\"btn btn-primary\">Submit</button>\n                </div>\n            </form>\n        </div>\n    </div>\n</div>\n{{- end}}\n\n{{- define \"extra_js\"}}\n<script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n\n    function uploadFiles(files) {\n        let has_errors = false;\n        let index = 0;\n        let success = 0;\n        let checkedDirs = [];\n        $('#errorMsg').addClass(\"d-none\");\n        clearLoading();\n        KTApp.showPageLoading();\n\n        function uploadFile() {\n            if (index >= files.length || has_errors) {\n                KTApp.hidePageLoading();\n                if (!has_errors) {\n                    ModalAlert.fire({\n                        text: $.t('fs.upload.success'),\n                        icon: \"success\",\n                        confirmButtonText: $.t('general.ok'),\n                        customClass: {\n                            confirmButton: 'btn btn-primary'\n                        }\n                    }).then((result) => {\n                        if (result.isConfirmed){\n                            location.reload();\n                        }\n                    });\n                }\n                return;\n            }\n\n            let f = files[index];\n            let name = f.name;\n            let mkdirParents = \"false\";\n            if (f.fullPath){\n                name = f.fullPath;\n                let dirName = name.substr(0, name.lastIndexOf(\"/\"));\n                if (!checkedDirs.includes(dirName)){\n                    mkdirParents = \"true\";\n                    checkedDirs.push(dirName);\n                }\n            }\n            let uploadPath = '{{.UploadBasePath}}/'+encodeURIComponent(name)+\"?mkdir_parents=\"+mkdirParents;\n            let lastModified;\n            try {\n                lastModified = f.lastModified;\n            } catch (e) {\n                console.error(\"unable to get last modified time from file: \" + e.message);\n                lastModified = \"\";\n            }\n\n            let info = \"\";\n            if (files.length > 1){\n                info = $.t('fs.uploading', {\n                    idx: index + 1,\n                    total: files.length\n                });\n            }\n\n            setLoadingText(info,f.name);\n\n            axios.post(uploadPath, f, {\n                headers: {\n                    'X-SFTPGO-MTIME': lastModified,\n                    'X-CSRF-TOKEN': '{{.CSRFToken}}'\n                },\n                onUploadProgress: function (progressEvent) {\n                    if (!progressEvent.total){\n                        return;\n                    }\n                    const percentage = Math.round((100 * progressEvent.loaded) / progressEvent.total);\n                    if (percentage > 0 && percentage < 100){\n                        setLoadingText(info,`${f.name} ${percentage}%`);\n                    }\n                },\n                validateStatus: function (status) {\n                    return status == 201;\n                }\n            }).then(function (response) {\n                index++;\n                success++;\n                uploadFile();\n            }).catch(function (error) {\n                let errorMessage;\n                if (error && error.response) {\n                    switch (error.response.status) {\n                        case 403:\n                            errorMessage = \"fs.upload.err_403\";\n                            break;\n                        case 429:\n                            errorMessage = \"fs.upload.err_429\";\n                            break;\n                    }\n                }\n                if (!errorMessage){\n                    errorMessage = \"fs.upload.err_generic\";\n                }\n                index++;\n                has_errors = true;\n                setI18NData($('#errorTxt'), errorMessage);\n                $('#errorMsg').removeClass(\"d-none\");\n                uploadFile();\n            });\n        }\n\n        uploadFile();\n    }\n\n    KTUtil.onDOMContentLoaded(function () {\n        var lastAddedFile = Date.now();\n\n        function canUpload() {\n            // Ugly hack to detect if we are still loading a large file list when the user try to upload files.\n            // The Dropzone addedfiles event is fired for directories before the files within them are added to the queue.\n            // TODO: investigate if there is a better way.\n            if (Date.now() - lastAddedFile < 500) {\n                ModalAlert.fire({\n                    text: $.t('fs.upload_queue_not_ready'),\n                    icon: \"warning\",\n                    confirmButtonText: $.t('general.ok'),\n                    customClass: {\n                        confirmButton: \"btn btn-primary\"\n                    }\n                });\n                return false;\n            }\n            return true;\n        }\n\n        var dropzone =  new Dropzone(\"#upload_files\", {\n            url: \"{{.UploadBasePath}}\",\n            paramName: \"filenames\",\n            createImageThumbnails: false,\n            maxFiles: null,\n            maxFilesize: null,\n            autoQueue: false,\n            addRemoveLinks: false,\n            autoProcessQueue: false,\n            filesizeBase: 1000,\n            previewTemplate: `<div class=\"d-flex align-items-center mb-2\">\n                                 <span class=\"bullet bullet-dot bg-primary me-2\"></span>\n                                 <div class=\"text-break text-wrap text-left\"><span class=\"fs-5 fw-semibold\" data-dz-name></span>&nbsp;(<span class=\"fs-5 fw-semibold\" data-custom-size></span>)</div>\n                               </div>\n                               <div class=\"dz-error-message d-none\" data-dz-errormessage></div>\n                               <div class=\"dz-progress d-none\"><span class=\"dz-upload\" data-dz-uploadprogress></span></div>\n                              `,\n            init: function() {\n                var dropzone = this;\n                $(\"#upload_files_button\").click(function(){\n                    if (canUpload()){\n                        uploadFiles(dropzone.getAcceptedFiles());\n                    }\n                });\n            }\n        });\n\n        dropzone.on(\"addedfile\", file => {\n            for (node of file.previewElement.querySelectorAll(\"[data-custom-size]\")) {\n                node.textContent = fileSizeIEC(file.size);\n            }\n            if (file.fullPath){\n                for (var node of file.previewElement.querySelectorAll(\"[data-dz-name]\")) {\n                    node.textContent = file.fullPath;\n                }\n            }\n            lastAddedFile = Date.now();\n        });\n    });\n\n</script>\n{{- end}}"
  },
  {
    "path": "templates/webclient/viewpdf.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <title></title>\n        <meta charset=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <meta name=\"robots\" content=\"noindex\">\n        <link rel=\"icon\" type=\"image/png\" href=\"{{.StaticURL}}{{.Branding.FaviconPath}}\" />\n\n        {{range .Branding.ExtraCSS}}\n        <link href=\"{{$.StaticURL}}{{.}}\" rel=\"stylesheet\" type=\"text/css\">\n        {{end}}\n    </head>\n\n<body>\n    <script {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}} src=\"{{.StaticURL}}/vendor/pdfobject/pdfobject.min.js\"></script>\n    <script type=\"text/javascript\" {{- if .CSPNonce}} nonce=\"{{.CSPNonce}}\"{{- end}}>\n        PDFObject.embed(\"{{.URL}}\", document.body);\n    </script>\n</body>\n</html>"
  },
  {
    "path": "tests/eventsearcher/go.mod",
    "content": "module github.com/drakkan/sftpgo/tests/eventsearcher\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/hashicorp/go-plugin v1.7.0\n\tgithub.com/sftpgo/sdk v0.1.9\n)\n\nrequire (\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/hashicorp/go-hclog v1.6.3 // indirect\n\tgithub.com/hashicorp/yamux v0.1.2 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/oklog/run v1.2.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n)\n"
  },
  {
    "path": "tests/eventsearcher/go.sum",
    "content": "github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=\ngithub.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=\ngithub.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=\ngithub.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=\ngithub.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=\ngithub.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=\ngithub.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=\ngithub.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sftpgo/sdk v0.1.9 h1:onBWfibCt34xHeKC2KFYPZ1DBqXGl9um/cAw+AVdgzY=\ngithub.com/sftpgo/sdk v0.1.9/go.mod h1:ehimvlTP+XTEiE3t1CPwWx9n7+6A6OGvMGlZ7ouvKFk=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44 h1:sRy++txmErSjyVWlIgQB5nB+U75+Di+AH7eEZ002B/s=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "tests/eventsearcher/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/hashicorp/go-plugin\"\n\t\"github.com/sftpgo/sdk/plugin/eventsearcher\"\n)\n\nvar (\n\terrNotSupported = errors.New(\"unsupported parameter\")\n)\n\ntype fsEvent struct {\n\tID                string `json:\"id\"`\n\tTimestamp         int64  `json:\"timestamp\"`\n\tAction            string `json:\"action\"`\n\tUsername          string `json:\"username\"`\n\tFsPath            string `json:\"fs_path\"`\n\tFsTargetPath      string `json:\"fs_target_path,omitempty\"`\n\tVirtualPath       string `json:\"virtual_path\"`\n\tVirtualTargetPath string `json:\"virtual_target_path,omitempty\"`\n\tSSHCmd            string `json:\"ssh_cmd,omitempty\"`\n\tFileSize          int64  `json:\"file_size,omitempty\"`\n\tElapsed           int64  `json:\"elapsed,omitempty\"`\n\tStatus            int    `json:\"status\"`\n\tProtocol          string `json:\"protocol\"`\n\tIP                string `json:\"ip,omitempty\"`\n\tSessionID         string `json:\"session_id\"`\n\tFsProvider        int    `json:\"fs_provider\"`\n\tBucket            string `json:\"bucket,omitempty\"`\n\tEndpoint          string `json:\"endpoint,omitempty\"`\n\tOpenFlags         int    `json:\"open_flags,omitempty\"`\n\tRole              string `json:\"role,omitempty\"`\n\tInstanceID        string `json:\"instance_id,omitempty\"`\n}\n\ntype providerEvent struct {\n\tID         string `json:\"id\" gorm:\"primaryKey\"`\n\tTimestamp  int64  `json:\"timestamp\"`\n\tAction     string `json:\"action\"`\n\tUsername   string `json:\"username\"`\n\tIP         string `json:\"ip,omitempty\"`\n\tObjectType string `json:\"object_type\"`\n\tObjectName string `json:\"object_name\"`\n\tObjectData []byte `json:\"object_data,omitempty\"`\n\tRole       string `json:\"role,omitempty\"`\n\tInstanceID string `json:\"instance_id,omitempty\"`\n}\n\ntype logEvent struct {\n\tID         string `json:\"id\" gorm:\"primaryKey\"`\n\tTimestamp  int64  `json:\"timestamp\"`\n\tEvent      int    `json:\"event\"`\n\tProtocol   string `json:\"protocol,omitempty\"`\n\tUsername   string `json:\"username,omitempty\"`\n\tIP         string `json:\"ip,omitempty\"`\n\tMessage    string `json:\"message,omitempty\"`\n\tRole       string `json:\"role,omitempty\"`\n\tInstanceID string `json:\"instance_id,omitempty\"`\n}\n\ntype Searcher struct{}\n\nfunc (s *Searcher) SearchFsEvents(filters *eventsearcher.FsEventSearch) ([]byte, error) {\n\tif filters.StartTimestamp < 0 {\n\t\treturn nil, errNotSupported\n\t}\n\n\tresults := []fsEvent{\n\t\t{\n\t\t\tID:                \"1\",\n\t\t\tTimestamp:         100,\n\t\t\tAction:            \"upload\",\n\t\t\tUsername:          \"username1\",\n\t\t\tFsPath:            \"/tmp/file.txt\",\n\t\t\tFsTargetPath:      \"/tmp/target.txt\",\n\t\t\tVirtualPath:       \"file.txt\",\n\t\t\tVirtualTargetPath: \"target.txt\",\n\t\t\tSSHCmd:            \"scp\",\n\t\t\tFileSize:          123,\n\t\t\tElapsed:           1250,\n\t\t\tStatus:            1,\n\t\t\tProtocol:          \"SFTP\",\n\t\t\tIP:                \"::1\",\n\t\t\tSessionID:         \"1234\",\n\t\t\tInstanceID:        \"instance1\",\n\t\t\tFsProvider:        0,\n\t\t\tBucket:            \"bucket\",\n\t\t\tEndpoint:          \"endpoint\",\n\t\t\tOpenFlags:         512,\n\t\t\tRole:              \"role1\",\n\t\t},\n\t}\n\n\tdata, err := json.Marshal(results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, nil\n}\n\nfunc (s *Searcher) SearchProviderEvents(filters *eventsearcher.ProviderEventSearch) ([]byte, error) {\n\tif filters.StartTimestamp < 0 {\n\t\treturn nil, errNotSupported\n\t}\n\n\tvar objectData []byte\n\tif !filters.OmitObjectData {\n\t\tobjectData = []byte(\"data\")\n\t}\n\n\tresults := []providerEvent{\n\t\t{\n\t\t\tID:         \"1\",\n\t\t\tTimestamp:  100,\n\t\t\tAction:     \"add\",\n\t\t\tUsername:   \"username1\",\n\t\t\tIP:         \"127.0.0.1\",\n\t\t\tObjectType: \"api_key\",\n\t\t\tObjectName: \"123\",\n\t\t\tObjectData: objectData,\n\t\t\tRole:       \"role2\",\n\t\t\tInstanceID: \"instance1\",\n\t\t},\n\t}\n\n\tdata, err := json.Marshal(results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, nil\n}\n\nfunc (s *Searcher) SearchLogEvents(filters *eventsearcher.LogEventSearch) ([]byte, error) {\n\tif filters.StartTimestamp < 0 {\n\t\treturn nil, errNotSupported\n\t}\n\n\tresults := []logEvent{\n\t\t{\n\t\t\tID:         \"1\",\n\t\t\tTimestamp:  100,\n\t\t\tEvent:      1,\n\t\t\tProtocol:   \"SSH\",\n\t\t\tIP:         \"127.0.1.1\",\n\t\t\tMessage:    \"Invalid credentials\",\n\t\t\tRole:       \"role3\",\n\t\t\tInstanceID: \"instance2\",\n\t\t},\n\t}\n\n\tdata, err := json.Marshal(results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, nil\n}\n\nfunc main() {\n\tplugin.Serve(&plugin.ServeConfig{\n\t\tHandshakeConfig: eventsearcher.Handshake,\n\t\tPlugins: map[string]plugin.Plugin{\n\t\t\teventsearcher.PluginName: &eventsearcher.Plugin{Impl: &Searcher{}},\n\t\t},\n\t\tGRPCServer: plugin.DefaultGRPCServer,\n\t})\n}\n"
  },
  {
    "path": "tests/ipfilter/go.mod",
    "content": "module github.com/drakkan/sftpgo/tests/ipfilter\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/hashicorp/go-plugin v1.7.0\n\tgithub.com/sftpgo/sdk v0.1.9\n)\n\nrequire (\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/hashicorp/go-hclog v1.6.3 // indirect\n\tgithub.com/hashicorp/yamux v0.1.2 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/oklog/run v1.2.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n)\n"
  },
  {
    "path": "tests/ipfilter/go.sum",
    "content": "github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=\ngithub.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=\ngithub.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=\ngithub.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=\ngithub.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=\ngithub.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=\ngithub.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=\ngithub.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sftpgo/sdk v0.1.9 h1:onBWfibCt34xHeKC2KFYPZ1DBqXGl9um/cAw+AVdgzY=\ngithub.com/sftpgo/sdk v0.1.9/go.mod h1:ehimvlTP+XTEiE3t1CPwWx9n7+6A6OGvMGlZ7ouvKFk=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44 h1:sRy++txmErSjyVWlIgQB5nB+U75+Di+AH7eEZ002B/s=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319171110-e3a33c96fb44/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "tests/ipfilter/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hashicorp/go-plugin\"\n\t\"github.com/sftpgo/sdk/plugin/ipfilter\"\n)\n\ntype Filter struct{}\n\nfunc (f *Filter) CheckIP(ip, protocol string) error {\n\tif ip == \"192.168.1.12\" {\n\t\treturn fmt.Errorf(\"ip %q is not allowed\", ip)\n\t}\n\treturn nil\n}\n\nfunc (f *Filter) Reload() error {\n\treturn nil\n}\n\nfunc main() {\n\tplugin.Serve(&plugin.ServeConfig{\n\t\tHandshakeConfig: ipfilter.Handshake,\n\t\tPlugins: map[string]plugin.Plugin{\n\t\t\tipfilter.PluginName: &ipfilter.Plugin{Impl: &Filter{}},\n\t\t},\n\t\tGRPCServer: plugin.DefaultGRPCServer,\n\t})\n\n}\n"
  },
  {
    "path": "windows-installer/LICENSE_with_NOTICE.txt",
    "content": "AGPL-3.0-only with additional terms\n\n\n                    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\n\nADDITIONAL TERMS\n\nAdditional terms under GNU AGPL version 3 section 7.3(b) and 13.1:\n\nIf you have included SFTPGo so that it is offered through any network\ninteractions, including by means of an external user interface, or\nany other integration, even without modifying its source code and then\nSFTPGo is partially, fully or optionally configured via your frontend,\nyou must provide reasonable but clear attribution to the SFTPGo project\nand its author(s), not imply any endorsement by or affiliation with the\nSFTPGo project, and you must prominently offer all users interacting\nwith it remotely through a computer network an opportunity to receive\nthe Corresponding Source of the SFTPGo version you include by providing\na link to the Corresponding Source in the SFTPGo source code repository."
  },
  {
    "path": "windows-installer/README.txt",
    "content": "SFTPGo allows you to securely share your files over SFTP and optionally over HTTP/S, FTP/S and WebDAV as well.\r\nSeveral storage backends are supported: local filesystem, encrypted local filesystem, S3 (compatible) Object Storage,\r\nGoogle Cloud Storage, Azure Blob Storage, other SFTP servers.\r\n\r\nIf this is your first installation please open the web administration panel:\r\n\r\nhttp://localhost:8080/web/admin\r\n\r\nand complete the initial setup.\r\n\r\nThe SFTP service is available, by default, on port 2022.\r\n\r\nIf the SFTPGo service does not start, make sure that TCP ports 2022 and 8080 are not used by other services\r\nor change the SFTPGo configuration to suit your needs.\r\n\r\nDefault data location:\r\n\r\nC:\\ProgramData\\SFTPGo\r\n\r\nConfiguration file location:\r\n\r\nC:\\ProgramData\\SFTPGo\\sftpgo.json\r\n\r\nDirectory to create environment variable files to set configuration options:\r\n\r\nC:\\ProgramData\\SFTPGo\\env.d\r\n\r\nIt is recommended that you set custom configurations as environment variables by creating files in\r\nthe env.d directory.\r\nThis eliminates the need to merge your changes with the default configuration file after each update.\r\nYou can simply replace the configuration file with the default one after updating SFTPGo.\r\n\r\nDocumentation:\r\n\r\nhttps://docs.sftpgo.com/\r\n\r\nCommercial support:\r\n\r\nhttps://sftpgo.com/\r\n\r\nSource code:\r\n\r\nhttps://github.com/drakkan/sftpgo\r\n\r\nIf you find a bug please open an issue:\r\n\r\nhttps://github.com/drakkan/sftpgo/issues\r\n\r\nIf you want to suggest a new feature or have a question, please start a new discussion:\r\n\r\nhttps://github.com/drakkan/sftpgo/discussions\r\n"
  },
  {
    "path": "windows-installer/sftpgo.iss",
    "content": "#define MyAppName \"SFTPGo\"\r\n#if GetEnv(\"SFTPGO_ISS_VERSION\") != \"\"\r\n    #define MyAppVersion GetEnv(\"SFTPGO_ISS_VERSION\")\r\n#else\r\n    #define MyAppVersion GetEnv(\"SFTPGO_ISS_DEV_VERSION\")\r\n#endif\r\n#if GetEnv(\"SFTPGO_ISS_ARCH\") != \"\"\r\n    #define MyAppArch GetEnv(\"SFTPGO_ISS_ARCH\")\r\n    #define MySetupName \"sftpgo_windows_\" + MyAppArch\r\n    #if MyAppArch == \"x86\"\r\n        #define MyAppArch64 \"\"\r\n    #else\r\n        #define MyAppArch64 GetEnv(\"SFTPGO_ISS_ARCH\")\r\n    #endif\r\n#else\r\n    #define MyAppArch \"x64\"\r\n    #define MyAppArch64 \"x64\"\r\n    #define MySetupName \"sftpgo_windows_x86_64\"\r\n#endif\r\n#define MyAppURL \"https://sftpgo.com\"\r\n#define MyVersionInfo StringChange(MyAppVersion,\"v\",\"\")\r\n#define DocURL \"https://docs.sftpgo.com\"\r\n#define MyAppExeName \"sftpgo.exe\"\r\n#define MyAppDir \"..\\output\"\r\n#define MyOutputDir \"..\"\r\n\r\n[Setup]\r\nAppId={{1FB9D57F-00DD-4B1B-8798-1138E5CE995D}\r\nAppName={#MyAppName}\r\nAppVersion={#MyAppVersion}\r\nAppVerName={#MyAppName} {#MyAppVersion}\r\nAppPublisher=Nicola Murino\r\nAppPublisherURL={#MyAppURL}\r\nAppSupportURL={#MyAppURL}\r\nAppUpdatesURL={#MyAppURL}\r\nAppCopyright=2019-2025 Nicola Murino\r\nDefaultDirName={autopf}\\{#MyAppName}\r\nDefaultGroupName={#MyAppName}\r\nLicenseFile=LICENSE_with_NOTICE.txt\r\nOutputDir={#MyOutputDir}\r\nOutputBaseFilename={#MySetupName}\r\nSetupIconFile=icon.ico\r\nSolidCompression=yes\r\nUninstallDisplayIcon={app}\\sftpgo.exe\r\nWizardStyle=modern\r\nArchitecturesInstallIn64BitMode={#MyAppArch64}\r\nPrivilegesRequired=admin\r\nArchitecturesAllowed={#MyAppArch}\r\nMinVersion=10.0.14393\r\nVersionInfoVersion={#MyVersionInfo}\r\nVersionInfoCopyright=2019-2025 Nicola Murino\r\n\r\n[Languages]\r\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\r\n\r\n[Files]\r\nSource: \"{#MyAppDir}\\sftpgo.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"{#MyAppDir}\\sftpgo.db\"; DestDir: \"{commonappdata}\\{#MyAppName}\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"{#MyAppDir}\\LICENSE.txt\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"{#MyAppDir}\\NOTICE.txt\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"{#MyAppDir}\\sftpgo.json\"; DestDir: \"{commonappdata}\\{#MyAppName}\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"{#MyAppDir}\\sftpgo.json\"; DestDir: \"{commonappdata}\\{#MyAppName}\"; DestName: \"sftpgo_default.json\"; Flags: ignoreversion\r\nSource: \"{#MyAppDir}\\templates\\*\"; DestDir: \"{commonappdata}\\{#MyAppName}\\templates\"; Flags: ignoreversion recursesubdirs createallsubdirs\r\nSource: \"{#MyAppDir}\\static\\*\"; DestDir: \"{commonappdata}\\{#MyAppName}\\static\"; Flags: ignoreversion recursesubdirs createallsubdirs\r\nSource: \"{#MyAppDir}\\openapi\\*\"; DestDir: \"{commonappdata}\\{#MyAppName}\\openapi\"; Flags: ignoreversion recursesubdirs createallsubdirs\r\nSource: \"README.txt\"; DestDir: \"{app}\"; Flags: ignoreversion isreadme\r\n\r\n[Dirs]\r\nName: \"{commonappdata}\\{#MyAppName}\\logs\"; Permissions: everyone-full\r\nName: \"{commonappdata}\\{#MyAppName}\\backups\"; Permissions: everyone-full\r\nName: \"{commonappdata}\\{#MyAppName}\\env.d\"; Permissions: everyone-full\r\nName: \"{commonappdata}\\{#MyAppName}\\certs\"; Permissions: everyone-full\r\n\r\n[Icons]\r\nName: \"{group}\\Web Admin\"; Filename: \"http://localhost:8080/web/admin\";\r\nName: \"{group}\\Web Client\"; Filename: \"http://localhost:8080/web/client\";\r\nName: \"{group}\\OpenAPI\"; Filename: \"http://localhost:8080/openapi\";\r\nName: \"{group}\\Service Control\";  WorkingDir: \"{app}\"; Filename: \"powershell.exe\"; Parameters: \"-Command \"\"Start-Process cmd \\\"\"/k cd {app} & {#MyAppExeName} service --help\\\"\" -Verb RunAs\"; Comment: \"Manage SFTPGo Service\"\r\nName: \"{group}\\Documentation\"; Filename: \"{#DocURL}\";\r\nName: \"{group}\\{cm:UninstallProgram,{#MyAppName}}\"; Filename: \"{uninstallexe}\"\r\n\r\n[Run]\r\nFilename: \"netsh\"; Parameters: \"advfirewall firewall delete rule name=\"\"SFTPGo Service\"\"\"; Flags: runhidden\r\nFilename: \"netsh\"; Parameters: \"advfirewall firewall add rule name=\"\"SFTPGo Service\"\" dir=in action=allow program=\"\"{app}\\{#MyAppExeName}\"\"\"; Flags: runhidden\r\nFilename: \"{app}\\{#MyAppExeName}\"; Parameters: \"service stop\"; Flags: runhidden\r\nFilename: \"{app}\\{#MyAppExeName}\"; Parameters: \"service uninstall\"; Flags: runhidden\r\nFilename: \"{app}\\{#MyAppExeName}\"; Parameters: \"service install -c \"\"{commonappdata}\\{#MyAppName}\"\" -l \"\"logs\\sftpgo.log\"\"\"; Description: \"Install SFTPGo Windows Service\"; Flags: runhidden\r\nFilename: \"{app}\\{#MyAppExeName}\"; Parameters: \"service start\";  Description: \"Start SFTPGo Windows Service\"; Flags: runhidden\r\n\r\n[UninstallRun]\r\nFilename: \"{app}\\{#MyAppExeName}\"; Parameters: \"service stop\"; Flags: runhidden; RunOnceId: \"Stop SFTPGo service\"\r\nFilename: \"{app}\\{#MyAppExeName}\"; Parameters: \"service uninstall\"; Flags: runhidden; RunOnceId: \"Uninstall SFTPGo service\"\r\nFilename: \"netsh\"; Parameters: \"advfirewall firewall delete rule name=\"\"SFTPGo Service\"\"\"; Flags: runhidden; RunOnceId: \"Remove SFTPGo firewall rule\"\r\n\r\n[Messages]\r\nFinishedLabel=Setup has finished installing SFTPGo on your computer. SFTPGo should already be running as a Windows service, it uses TCP port 8080 for HTTP service and TCP port 2022 for SFTP service by default, make sure the configured ports are not used by other services or edit the configuration according to your needs.\r\n\r\n[Code]\r\n\r\nfunction PrepareToInstall(var NeedsRestart: Boolean): String;\r\nvar\r\n  Code: Integer;\r\nbegin\r\n  if (FileExists(ExpandConstant('{app}\\{#MyAppExeName}'))) then\r\n  begin\r\n    Exec(ExpandConstant('{app}\\{#MyAppExeName}'), 'service stop', '', SW_HIDE, ewWaitUntilTerminated, Code);\r\n  end\r\nend;"
  }
]